main
cu 362 lines 15.8 KB
Raw
1 #include <optional>
2 #include <algorithm>
3 #include <limits>
4
5 #include "../torch_utils.h"
6 #include "../dispatch_utils.h"
7 #include <torch/headeronly/core/ScalarType.h>
8
9 #include "../../attention/attention_dtypes.h"
10 #include "attention_utils.cuh"
11 #include "../../quantization/w8a8/fp8/common.cuh"
12
13 namespace vllm {
14
15 // Implements section 2.2 of https://www.arxiv.org/pdf/2501.01005
16 // can be used to combine partial attention results (in the split-KV case)
17 template <typename scalar_t, typename output_t, const uint NUM_THREADS,
18 bool USE_FP8_OUTPUT>
19 __global__ void merge_attn_states_kernel(
20 output_t* output, float* output_lse, const scalar_t* prefix_output,
21 const float* prefix_lse, const scalar_t* suffix_output,
22 const float* suffix_lse, const uint num_tokens, const uint num_heads,
23 const uint head_size, const uint prefix_head_stride,
24 const uint output_head_stride, const uint prefix_lse_head_stride,
25 const uint prefix_lse_token_stride, const uint suffix_lse_head_stride,
26 const uint suffix_lse_token_stride, const uint output_lse_head_stride,
27 const uint output_lse_token_stride, const uint prefix_num_tokens,
28 const float* output_scale) {
29 // Inputs always load 128-bit packs (pack_size elements of scalar_t).
30 // Outputs store pack_size elements of output_t, which is smaller for FP8.
31 using input_pack_t = uint4;
32 using output_pack_t =
33 std::conditional_t<USE_FP8_OUTPUT,
34 std::conditional_t<sizeof(scalar_t) == 4, uint, uint2>,
35 uint4>;
36 const uint pack_size = 16 / sizeof(scalar_t);
37 const uint threads_per_head = head_size / pack_size;
38
39 const uint global_idx = blockIdx.x * NUM_THREADS + threadIdx.x;
40 const uint token_head_threads = num_tokens * num_heads * threads_per_head;
41
42 if (global_idx >= token_head_threads) return;
43
44 // global_idx -> token_idx + head_idx + pack_idx
45 const uint token_head_idx = global_idx / threads_per_head;
46 const uint pack_idx = global_idx % threads_per_head;
47
48 const uint token_idx = token_head_idx / num_heads;
49 const uint head_idx = token_head_idx % num_heads;
50
51 const uint pack_offset = pack_idx * pack_size; // (0~15)*8, etc.
52 const uint src_head_offset = token_idx * num_heads * prefix_head_stride +
53 head_idx * prefix_head_stride;
54 const uint dst_head_offset = token_idx * num_heads * output_head_stride +
55 head_idx * output_head_stride;
56 const scalar_t* prefix_head_ptr = prefix_output + src_head_offset;
57 const scalar_t* suffix_head_ptr = suffix_output + src_head_offset;
58 output_t* output_head_ptr = output + dst_head_offset;
59
60 // Pre-invert scale: multiplication is faster than division
61 float fp8_scale_inv = 1.0f;
62 if constexpr (USE_FP8_OUTPUT) {
63 fp8_scale_inv = 1.0f / *output_scale;
64 }
65
66 // If token_idx >= prefix_num_tokens, just copy from suffix
67 if (token_idx >= prefix_num_tokens) {
68 if (pack_offset < head_size) {
69 input_pack_t s_out_pack = reinterpret_cast<const input_pack_t*>(
70 suffix_head_ptr)[pack_offset / pack_size];
71
72 if constexpr (USE_FP8_OUTPUT) {
73 output_t o_out_pack[pack_size];
74 #pragma unroll
75 for (uint i = 0; i < pack_size; ++i) {
76 const float val =
77 vllm::to_float(reinterpret_cast<const scalar_t*>(&s_out_pack)[i]);
78 o_out_pack[i] =
79 vllm::scaled_fp8_conversion<true, output_t>(val, fp8_scale_inv);
80 }
81 reinterpret_cast<output_pack_t*>(
82 output_head_ptr)[pack_offset / pack_size] =
83 *reinterpret_cast<output_pack_t*>(o_out_pack);
84 } else {
85 reinterpret_cast<output_pack_t*>(
86 output_head_ptr)[pack_offset / pack_size] = s_out_pack;
87 }
88 }
89 if (output_lse != nullptr && pack_idx == 0) {
90 float s_lse = suffix_lse[head_idx * suffix_lse_head_stride +
91 token_idx * suffix_lse_token_stride];
92 output_lse[head_idx * output_lse_head_stride +
93 token_idx * output_lse_token_stride] = s_lse;
94 }
95 return;
96 }
97
98 // For tokens within prefix range, merge prefix and suffix
99 float p_lse = prefix_lse[head_idx * prefix_lse_head_stride +
100 token_idx * prefix_lse_token_stride];
101 float s_lse = suffix_lse[head_idx * suffix_lse_head_stride +
102 token_idx * suffix_lse_token_stride];
103 p_lse = std::isinf(p_lse) ? -std::numeric_limits<float>::infinity() : p_lse;
104 s_lse = std::isinf(s_lse) ? -std::numeric_limits<float>::infinity() : s_lse;
105
106 const float max_lse = fmaxf(p_lse, s_lse);
107
108 /* In certain edge cases, MLA can produce p_lse = s_lse = -inf;
109 continuing the pipeline then yields NaN. Root cause: with chunked prefill
110 a batch may be split into two chunks; if a request in that batch has no
111 prefix hit, every LSE entry for that request's position is -inf, and at
112 this moment we merge cross-attention at first. For now we simply emit
113 prefix_output (expected to be all zeros) and prefix_lse (-inf) to fix
114 this problem.
115 */
116 if (std::isinf(max_lse)) {
117 if (pack_offset < head_size) {
118 input_pack_t p_out_pack = reinterpret_cast<const input_pack_t*>(
119 prefix_head_ptr)[pack_offset / pack_size];
120
121 if constexpr (USE_FP8_OUTPUT) {
122 // Convert prefix values to FP8 (since -inf means no data,
123 // prefix_output is expected to be zeros)
124 output_t o_out_pack[pack_size];
125 #pragma unroll
126 for (uint i = 0; i < pack_size; ++i) {
127 const float val =
128 vllm::to_float(reinterpret_cast<const scalar_t*>(&p_out_pack)[i]);
129 o_out_pack[i] =
130 vllm::scaled_fp8_conversion<true, output_t>(val, fp8_scale_inv);
131 }
132 reinterpret_cast<output_pack_t*>(
133 output_head_ptr)[pack_offset / pack_size] =
134 *reinterpret_cast<output_pack_t*>(o_out_pack);
135 } else {
136 reinterpret_cast<output_pack_t*>(
137 output_head_ptr)[pack_offset / pack_size] = p_out_pack;
138 }
139 }
140 // We only need to write to output_lse once per head.
141 if (output_lse != nullptr && pack_idx == 0) {
142 output_lse[head_idx * output_lse_head_stride +
143 token_idx * output_lse_token_stride] = max_lse;
144 }
145 return;
146 }
147
148 p_lse = p_lse - max_lse;
149 s_lse = s_lse - max_lse;
150 const float p_se = expf(p_lse);
151 const float s_se = expf(s_lse);
152 const float out_se = p_se + s_se;
153 const float p_scale = p_se / out_se;
154 const float s_scale = s_se / out_se;
155
156 if (pack_offset < head_size) {
157 input_pack_t p_out_pack = reinterpret_cast<const input_pack_t*>(
158 prefix_head_ptr)[pack_offset / pack_size];
159 input_pack_t s_out_pack = reinterpret_cast<const input_pack_t*>(
160 suffix_head_ptr)[pack_offset / pack_size];
161
162 // Compute merged values in float32
163 float o_out_f[pack_size];
164 #pragma unroll
165 for (uint i = 0; i < pack_size; ++i) {
166 const float p_out_f =
167 vllm::to_float(reinterpret_cast<const scalar_t*>(&p_out_pack)[i]);
168 const float s_out_f =
169 vllm::to_float(reinterpret_cast<const scalar_t*>(&s_out_pack)[i]);
170 o_out_f[i] = p_out_f * p_scale + (s_out_f * s_scale);
171 }
172
173 // Convert and store
174 if constexpr (USE_FP8_OUTPUT) {
175 output_t o_out_pack[pack_size];
176 #pragma unroll
177 for (uint i = 0; i < pack_size; ++i) {
178 o_out_pack[i] = vllm::scaled_fp8_conversion<true, output_t>(
179 o_out_f[i], fp8_scale_inv);
180 }
181 reinterpret_cast<output_pack_t*>(
182 output_head_ptr)[pack_offset / pack_size] =
183 *reinterpret_cast<output_pack_t*>(o_out_pack);
184 } else {
185 output_pack_t o_out_pack;
186 #pragma unroll
187 for (uint i = 0; i < pack_size; ++i) {
188 vllm::from_float(reinterpret_cast<scalar_t*>(&o_out_pack)[i],
189 o_out_f[i]);
190 }
191 reinterpret_cast<output_pack_t*>(
192 output_head_ptr)[pack_offset / pack_size] = o_out_pack;
193 }
194 }
195 // We only need to write to output_lse once per head.
196 if (output_lse != nullptr && pack_idx == 0) {
197 float out_lse = logf(out_se) + max_lse;
198 output_lse[head_idx * output_lse_head_stride +
199 token_idx * output_lse_token_stride] = out_lse;
200 }
201 }
202
203 } // namespace vllm
204
205 // The following macro is used to dispatch the conversion function based on
206 // the output data type. The FN is a macro that calls a function with
207 // template<typename scalar_t>.
208 #define DISPATCH_BY_SCALAR_DTYPE(scalar_dtype, fn) \
209 { \
210 if (scalar_dtype == torch::headeronly::ScalarType::Float) { \
211 fn(float); \
212 } else if (scalar_dtype == torch::headeronly::ScalarType::Half) { \
213 fn(uint16_t); \
214 } else if (scalar_dtype == torch::headeronly::ScalarType::BFloat16) { \
215 fn(__nv_bfloat16); \
216 } else { \
217 STD_TORCH_CHECK(false, "Unsupported data type of O: ", scalar_dtype); \
218 } \
219 }
220
221 #define LAUNCH_MERGE_ATTN_STATES(scalar_t, output_t, NUM_THREADS, \
222 USE_FP8_OUTPUT) \
223 { \
224 vllm::merge_attn_states_kernel<scalar_t, output_t, NUM_THREADS, \
225 USE_FP8_OUTPUT> \
226 <<<grid, block, 0, stream>>>( \
227 reinterpret_cast<output_t*>(output.data_ptr()), output_lse_ptr, \
228 reinterpret_cast<scalar_t*>(prefix_output.data_ptr()), \
229 reinterpret_cast<float*>(prefix_lse.data_ptr()), \
230 reinterpret_cast<scalar_t*>(suffix_output.data_ptr()), \
231 reinterpret_cast<float*>(suffix_lse.data_ptr()), num_tokens, \
232 num_heads, head_size, prefix_head_stride, output_head_stride, \
233 prefix_lse_head_stride, prefix_lse_token_stride, \
234 suffix_lse_head_stride, suffix_lse_token_stride, \
235 output_lse_head_stride, output_lse_token_stride, \
236 prefix_num_tokens, output_scale_ptr); \
237 }
238
239 /*@brief Merges the attention states from prefix and suffix
240 * into the output tensor. NUM_TOKENS: n, NUM_HEADS: h, HEAD_SIZE: d
241 *
242 * @param output [n,h,d] The output tensor to store the merged attention states.
243 * @param output_lse [h,n] Optional tensor to store the log-sum-exp values.
244 * @param prefix_output [n,h,d] The prefix attention states.
245 * @param prefix_lse [h,n] The log-sum-exp values for the prefix attention
246 * states.
247 * @param suffix_output [n,h,d] The suffix attention states.
248 * @param suffix_lse [h,n] The log-sum-exp values for the suffix attention
249 * states.
250 * @param prefill_tokens_with_context Number of prefill tokens with context
251 * For the first p tokens (0 <= token_idx < prefill_tokens_with_context), output
252 * is computed by merging prefix_output and suffix_output. For remaining tokens
253 * (prefill_tokens_with_context <= token_idx < n), output is copied directly
254 * from suffix_output.
255 * @param output_scale Optional scalar tensor for FP8 static quantization.
256 * When provided, output must be FP8 dtype.
257 */
258 template <typename scalar_t>
259 void merge_attn_states_launcher(
260 torch::stable::Tensor& output,
261 std::optional<torch::stable::Tensor> output_lse,
262 const torch::stable::Tensor& prefix_output,
263 const torch::stable::Tensor& prefix_lse,
264 const torch::stable::Tensor& suffix_output,
265 const torch::stable::Tensor& suffix_lse,
266 const std::optional<int64_t> prefill_tokens_with_context,
267 const std::optional<torch::stable::Tensor>& output_scale) {
268 constexpr uint NUM_THREADS = 128;
269 const uint num_tokens = output.size(0);
270 const uint num_heads = output.size(1);
271 const uint head_size = output.size(2);
272 const uint prefix_head_stride = prefix_output.stride(1);
273 const uint output_head_stride = output.stride(1);
274 // lse tensors are [NUM_HEADS, NUM_TOKENS] but may be non-contiguous views
275 // (e.g. a transpose of a backend's [NUM_TOKENS, NUM_HEADS] output), so index
276 // them by their actual strides rather than assuming a contiguous layout.
277 const uint prefix_lse_head_stride = prefix_lse.stride(0);
278 const uint prefix_lse_token_stride = prefix_lse.stride(1);
279 const uint suffix_lse_head_stride = suffix_lse.stride(0);
280 const uint suffix_lse_token_stride = suffix_lse.stride(1);
281 uint output_lse_head_stride = 0;
282 uint output_lse_token_stride = 0;
283 if (output_lse.has_value()) {
284 output_lse_head_stride = output_lse.value().stride(0);
285 output_lse_token_stride = output_lse.value().stride(1);
286 }
287 // Thread mapping is based on input BF16 pack_size
288 const uint pack_size = 16 / sizeof(scalar_t);
289 STD_TORCH_CHECK(head_size % pack_size == 0,
290 "headsize must be multiple of pack_size:", pack_size);
291
292 const uint prefix_num_tokens =
293 prefill_tokens_with_context.has_value()
294 ? static_cast<uint>(prefill_tokens_with_context.value())
295 : num_tokens;
296 STD_TORCH_CHECK(prefix_num_tokens <= num_tokens,
297 "prefix_num_tokens must be <= num_tokens");
298
299 float* output_lse_ptr = nullptr;
300 if (output_lse.has_value()) {
301 output_lse_ptr = output_lse.value().mutable_data_ptr<float>();
302 }
303 float* output_scale_ptr = nullptr;
304 if (output_scale.has_value()) {
305 output_scale_ptr = output_scale.value().mutable_data_ptr<float>();
306 }
307 // Process one pack elements per thread. for float, the
308 // pack_size is 4 for half/bf16, the pack_size is 8.
309 const uint threads_per_head = head_size / pack_size;
310 const uint total_threads = num_tokens * num_heads * threads_per_head;
311
312 dim3 block(NUM_THREADS);
313 dim3 grid((total_threads + NUM_THREADS - 1) / NUM_THREADS);
314
315 const torch::stable::accelerator::DeviceGuard device_guard(
316 prefix_output.get_device_index());
317 auto stream = get_current_cuda_stream();
318
319 if (output_scale.has_value()) {
320 // FP8 output path - dispatch on output FP8 type
321 VLLM_STABLE_DISPATCH_FP8_TYPES(
322 output.scalar_type(), "merge_attn_states_fp8",
323 [&] { LAUNCH_MERGE_ATTN_STATES(scalar_t, fp8_t, NUM_THREADS, true); });
324 } else {
325 // Original BF16/FP16/FP32 output path
326 LAUNCH_MERGE_ATTN_STATES(scalar_t, scalar_t, NUM_THREADS, false);
327 }
328 }
329
330 #define CALL_MERGE_ATTN_STATES_LAUNCHER(scalar_t) \
331 { \
332 merge_attn_states_launcher<scalar_t>( \
333 output, output_lse, prefix_output, prefix_lse, suffix_output, \
334 suffix_lse, prefill_tokens_with_context, output_scale); \
335 }
336
337 void merge_attn_states(
338 torch::stable::Tensor& output,
339 std::optional<torch::stable::Tensor> output_lse,
340 const torch::stable::Tensor& prefix_output,
341 const torch::stable::Tensor& prefix_lse,
342 const torch::stable::Tensor& suffix_output,
343 const torch::stable::Tensor& suffix_lse,
344 const std::optional<int64_t> prefill_tokens_with_context,
345 const std::optional<torch::stable::Tensor>& output_scale) {
346 if (output_scale.has_value()) {
347 STD_TORCH_CHECK(
348 output.scalar_type() == torch::headeronly::ScalarType::Float8_e4m3fn ||
349 output.scalar_type() ==
350 torch::headeronly::ScalarType::Float8_e4m3fnuz,
351 "output must be FP8 when output_scale is provided, got: ",
352 output.scalar_type());
353 } else {
354 STD_TORCH_CHECK(
355 output.scalar_type() == prefix_output.scalar_type(), "output dtype (",
356 output.scalar_type(), ") must match prefix_output dtype (",
357 prefix_output.scalar_type(), ") when output_scale is not set");
358 }
359 // Always dispatch on prefix_output (input) dtype
360 DISPATCH_BY_SCALAR_DTYPE(prefix_output.scalar_type(),
361 CALL_MERGE_ATTN_STATES_LAUNCHER);
362 }