main
py 184 lines 6.98 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import torch
5
6 from vllm.platforms import current_platform
7
8
9 def _merge_attn_states_torch(
10 output: torch.Tensor,
11 prefix_output: torch.Tensor,
12 prefix_lse: torch.Tensor,
13 suffix_output: torch.Tensor,
14 suffix_lse: torch.Tensor,
15 output_lse: torch.Tensor | None = None,
16 prefill_tokens_with_context: int | None = None,
17 output_scale: torch.Tensor | None = None,
18 ) -> None:
19 assert output_scale is None, "CPU fallback does not support FP8 output"
20
21 num_tokens = output.shape[0]
22 if prefill_tokens_with_context is None:
23 prefill_tokens_with_context = num_tokens
24
25 prefix_lse = torch.where(
26 torch.isinf(prefix_lse) & (prefix_lse > 0),
27 torch.full_like(prefix_lse, float("-inf")),
28 prefix_lse,
29 )
30 suffix_lse = torch.where(
31 torch.isinf(suffix_lse) & (suffix_lse > 0),
32 torch.full_like(suffix_lse, float("-inf")),
33 suffix_lse,
34 )
35
36 if prefill_tokens_with_context < num_tokens:
37 output[prefill_tokens_with_context:].copy_(
38 suffix_output[prefill_tokens_with_context:]
39 )
40 if output_lse is not None:
41 output_lse[:, prefill_tokens_with_context:].copy_(
42 suffix_lse[:, prefill_tokens_with_context:]
43 )
44
45 if prefill_tokens_with_context == 0:
46 return
47
48 prefix_lse = prefix_lse[:, :prefill_tokens_with_context].float()
49 suffix_lse = suffix_lse[:, :prefill_tokens_with_context].float()
50 max_lse = torch.maximum(prefix_lse, suffix_lse)
51 prefix_se = torch.exp(prefix_lse - max_lse)
52 suffix_se = torch.exp(suffix_lse - max_lse)
53 out_se = prefix_se + suffix_se
54 both_empty = max_lse == float("-inf")
55
56 prefix_scale = (prefix_se / out_se).transpose(0, 1).unsqueeze(-1)
57 suffix_scale = (suffix_se / out_se).transpose(0, 1).unsqueeze(-1)
58 merged = (
59 prefix_output[:prefill_tokens_with_context].float() * prefix_scale
60 + suffix_output[:prefill_tokens_with_context].float() * suffix_scale
61 )
62 merged.masked_fill_(both_empty.transpose(0, 1).unsqueeze(-1), 0.0)
63 output[:prefill_tokens_with_context].copy_(merged.to(output.dtype))
64
65 if output_lse is not None:
66 merged_lse = torch.log(out_se) + max_lse
67 merged_lse.masked_fill_(both_empty, float("-inf"))
68 output_lse[:, :prefill_tokens_with_context].copy_(merged_lse)
69
70
71 def merge_attn_states(
72 output: torch.Tensor,
73 prefix_output: torch.Tensor,
74 prefix_lse: torch.Tensor,
75 suffix_output: torch.Tensor,
76 suffix_lse: torch.Tensor,
77 output_lse: torch.Tensor | None = None,
78 prefill_tokens_with_context: int | None = None,
79 output_scale: torch.Tensor | None = None,
80 ) -> None:
81 """Merge partial attention outputs from prefix (KV cache) and suffix
82 (new tokens) into a single output tensor using the log-sum-exp (LSE)
83 rescaling method described in section 2.2 of
84 https://www.arxiv.org/pdf/2501.01005.
85
86 For tokens that have prefix context (token index < prefill_tokens_with_context),
87 the prefix and suffix partial outputs are combined as a weighted sum.
88 For tokens without prefix context, the suffix output is copied directly.
89
90 Args:
91 output: Output tensor of shape [NUM_TOKENS, NUM_HEADS, HEAD_SIZE].
92 prefix_output: Partial attention output over the prefix (KV cache),
93 shape [NUM_TOKENS, NUM_HEADS, HEAD_SIZE].
94 prefix_lse: Log-sum-exp values for the prefix attention,
95 shape [NUM_HEADS, NUM_TOKENS].
96 suffix_output: Partial attention output over the suffix (new KV),
97 shape [NUM_TOKENS, NUM_HEADS, HEAD_SIZE].
98 suffix_lse: Log-sum-exp values for the suffix attention,
99 shape [NUM_HEADS, NUM_TOKENS].
100 output_lse: Optional tensor to store the merged LSE values,
101 shape [NUM_HEADS, NUM_TOKENS]. If None, LSE is not written out.
102 prefill_tokens_with_context: Number of prefill tokens that have
103 prefix context and therefore require merging. Tokens at indices
104 >= this value are decode or context-free prefill tokens whose
105 output is taken directly from suffix_output. If None, all tokens
106 are treated as having context.
107 output_scale: Optional scalar tensor for FP8 static quantization.
108 When provided, output must be FP8 dtype.
109
110 """
111 # Both the CUDA and Triton kernels derive the suffix head stride from
112 # prefix_output, so suffix_output must share the same head stride.
113 assert prefix_output.stride(1) == suffix_output.stride(1), (
114 "merge_attn_states requires prefix_output and suffix_output to have "
115 f"matching head strides, got {prefix_output.stride(1)} and "
116 f"{suffix_output.stride(1)}"
117 )
118
119 # NOTE(DefTruth): Currently, custom merge_attn_states CUDA kernel
120 # does not support FP8 dtype for inputs, fallback to use Triton kernel.
121 # However, when output_scale is provided, the inputs are still BF16/FP16
122 # and the output is FP8 — both CUDA and Triton support this.
123 # FP8 output requires output_scale to be set.
124 if output.dtype not in (torch.float32, torch.half, torch.bfloat16):
125 assert output_scale is not None, (
126 f"output_scale is required when output is {output.dtype}"
127 )
128
129 def supported_dtypes(prefix: torch.Tensor) -> bool:
130 return prefix.dtype in [torch.float32, torch.half, torch.bfloat16]
131
132 # NOTE(DefTruth): Currently, custom merge_attn_states CUDA
133 # kernel load/store 128b(16 bytes) per memory issue within
134 # thread. Namely, the headsize(headdim) must be multiple of
135 # pack_size based on input dtype (float32 -> 4, half/bfloat16 -> 8).
136 def supported_headdim(prefix: torch.Tensor) -> bool:
137 headdim = prefix.shape[2] # [NUM_TOKENS, NUM_HEADS, HEAD_SIZE]
138 if prefix.dtype == torch.float32:
139 return headdim % 4 == 0
140 return headdim % 8 == 0
141
142 if (
143 current_platform.is_cuda()
144 and supported_dtypes(prefix_output)
145 and supported_headdim(prefix_output)
146 ):
147 from vllm._custom_ops import merge_attn_states
148
149 return merge_attn_states(
150 output,
151 prefix_output,
152 prefix_lse,
153 suffix_output,
154 suffix_lse,
155 output_lse,
156 prefill_tokens_with_context,
157 output_scale,
158 )
159 elif current_platform.is_cpu():
160 return _merge_attn_states_torch(
161 output,
162 prefix_output,
163 prefix_lse,
164 suffix_output,
165 suffix_lse,
166 output_lse,
167 prefill_tokens_with_context,
168 output_scale,
169 )
170 else:
171 from vllm.v1.attention.ops.triton_merge_attn_states import (
172 merge_attn_states,
173 )
174
175 return merge_attn_states(
176 output,
177 prefix_output,
178 prefix_lse,
179 suffix_output,
180 suffix_lse,
181 output_lse,
182 prefill_tokens_with_context,
183 output_scale,
184 )