main
cu 423 lines 16.5 KB
Raw
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 // NVFP4 KV cache store kernel.
5 // Quantizes bf16 key/value to packed FP4 + FP8 block scales and writes them
6 // into the paged KV cache.
7 //
8 // Per page layout: [K_data | K_scale | V_data | V_scale]
9 // Both data and scale regions are contiguous per head, enabling direct
10 // TMA descriptor use.
11 //
12 // Reuses device functions from nvfp4_utils.cuh:
13 // - cvt_warp_fp16_to_fp4() for bf16 → fp4 quantization + block scale
14 // - pack_fp4() for packing float pairs to fp4
15 // - reciprocal_approximate_ftz() for fast reciprocal
16
17 #define NVFP4_ENABLE_ELTS16 1
18 #include "libtorch_stable/quantization/fp4/nvfp4_utils.cuh"
19
20 #include "libtorch_stable/dispatch_utils.h"
21 #include "libtorch_stable/torch_utils.h"
22
23 #include <cmath>
24 #include <string>
25
26 namespace vllm {
27
28 // The cache layout is identical; this only selects the store-time scale search.
29 enum class NVFP4KVScaleSearch {
30 DEFAULT,
31 FOUR_OVER_SIX,
32 };
33
34 // Compute swizzled scale offset for SM100 trtllm-gen MHA kernel.
35 // The swizzle pattern for HND layout is:
36 // [T//4, 4, 4, S//4] → permute(0, 2, 3, 1) → reshape to [T, S]
37 // where T = block_size (page_size), S = scale_dim = head_size // 16.
38 //
39 // For a linear (t, s) position, the swizzled position is:
40 // swizzled_t = (t / 4) * 4 + (s / (S / 4))
41 // swizzled_s = (s % (S / 4)) * 4 + (t % 4)
42 __device__ __forceinline__ int swizzle_scale_offset(int t, int s,
43 int scale_dim) {
44 int s_group = scale_dim / 4;
45 int swizzled_t = (t / 4) * 4 + (s / s_group);
46 int swizzled_s = (s % s_group) * 4 + (t % 4);
47 return swizzled_t * scale_dim + swizzled_s;
48 }
49
50 __device__ __forceinline__ float round_to_nearest_e2m1(float x) {
51 const float ax = fabsf(x);
52 float q;
53 // Match cvt.rn.satfinite.e2m1x2.f32, including ties-to-even boundaries.
54 if (ax <= 0.25f) {
55 q = 0.0f;
56 } else if (ax < 0.75f) {
57 q = 0.5f;
58 } else if (ax <= 1.25f) {
59 q = 1.0f;
60 } else if (ax < 1.75f) {
61 q = 1.5f;
62 } else if (ax <= 2.5f) {
63 q = 2.0f;
64 } else if (ax < 3.5f) {
65 q = 3.0f;
66 } else if (ax <= 5.0f) {
67 q = 4.0f;
68 } else {
69 q = 6.0f;
70 }
71 return copysignf(q, x);
72 }
73
74 __device__ __forceinline__ void nvfp4_candidate_scale(float SFScaleVal,
75 float vecMax,
76 float denominator,
77 uint8_t* fp8_sf,
78 float* outputScale) {
79 float SFValue =
80 SFScaleVal * (vecMax * reciprocal_approximate_ftz(denominator));
81 __nv_fp8_e4m3 tmp = __nv_fp8_e4m3(SFValue);
82 reinterpret_cast<__nv_fp8_e4m3&>(*fp8_sf) = tmp;
83 SFValue = float(tmp);
84 *outputScale = SFValue != 0.0f
85 ? reciprocal_approximate_ftz(
86 SFValue * reciprocal_approximate_ftz(SFScaleVal))
87 : 0.0f;
88 }
89
90 template <class Type, int CVT_FP4_NUM_THREADS_PER_SF>
91 __device__ __forceinline__ float nvfp4_reconstruction_error(
92 PackedVec<Type, CVT_FP4_PACK16>& vec, float outputScale) {
93 float error = 0.0f;
94 const float dequantScale =
95 outputScale != 0.0f ? reciprocal_approximate_ftz(outputScale) : 0.0f;
96
97 #pragma unroll
98 for (int i = 0; i < CVT_FP4_ELTS_PER_THREAD / 2; i++) {
99 float2 vals = cast_to_float2(vec.elts[i]);
100 if (outputScale == 0.0f) {
101 error += vals.x * vals.x + vals.y * vals.y;
102 } else {
103 float qx = round_to_nearest_e2m1(vals.x * outputScale);
104 float qy = round_to_nearest_e2m1(vals.y * outputScale);
105 float dx = qx * dequantScale - vals.x;
106 float dy = qy * dequantScale - vals.y;
107 error += dx * dx + dy * dy;
108 }
109 }
110
111 if constexpr (CVT_FP4_NUM_THREADS_PER_SF == 2) {
112 error += __shfl_xor_sync(0xffffffffu, error, 1);
113 }
114 return error;
115 }
116
117 template <class Type, int CVT_FP4_NUM_THREADS_PER_SF>
118 __device__ __forceinline__ fp4_packed_t cvt_warp_fp16_to_fp4_4over6(
119 PackedVec<Type, CVT_FP4_PACK16>& vec, float SFScaleVal, uint8_t* SFout) {
120 auto localMax = __habs2(vec.elts[0]);
121
122 #pragma unroll
123 for (int i = 1; i < CVT_FP4_ELTS_PER_THREAD / 2; i++) {
124 localMax = __hmax2(localMax, __habs2(vec.elts[i]));
125 }
126
127 if constexpr (CVT_FP4_NUM_THREADS_PER_SF == 2) {
128 localMax = __hmax2(__shfl_xor_sync(0xffffffffu, localMax, 1), localMax);
129 }
130 const float vecMax = float(__hmax(localMax.x, localMax.y));
131
132 uint8_t sf6;
133 uint8_t sf4;
134 float outputScale6;
135 float outputScale4;
136 nvfp4_candidate_scale(SFScaleVal, vecMax, 6.0f, &sf6, &outputScale6);
137 nvfp4_candidate_scale(SFScaleVal, vecMax, 4.0f, &sf4, &outputScale4);
138
139 const float err6 =
140 nvfp4_reconstruction_error<Type, CVT_FP4_NUM_THREADS_PER_SF>(
141 vec, outputScale6);
142 const float err4 =
143 nvfp4_reconstruction_error<Type, CVT_FP4_NUM_THREADS_PER_SF>(
144 vec, outputScale4);
145 const bool use4 = err4 < err6;
146 const float outputScale = use4 ? outputScale4 : outputScale6;
147
148 if (SFout) *SFout = use4 ? sf4 : sf6;
149
150 float2 fp2Vals[CVT_FP4_ELTS_PER_THREAD / 2];
151
152 #pragma unroll
153 for (int i = 0; i < CVT_FP4_ELTS_PER_THREAD / 2; i++) {
154 fp2Vals[i] = cast_to_float2(vec.elts[i]);
155 fp2Vals[i].x *= outputScale;
156 fp2Vals[i].y *= outputScale;
157 }
158
159 return pack_fp4(fp2Vals);
160 }
161
162 // Kernel: quantize bf16 key/value to NVFP4 and store in paged KV cache.
163 //
164 // Takes separate data and scale cache pointers for K and V.
165 // Within each KV side, data and scale are separate contiguous regions.
166 //
167 // Threading: one CUDA block per token, threads process heads and
168 // groups of 16 elements within each head.
169 template <typename scalar_t, NVFP4KVScaleSearch SCALE_SEARCH>
170 __global__ void reshape_and_cache_nvfp4_kernel(
171 const scalar_t* __restrict__ key, // [num_tokens, num_heads, head_size]
172 const scalar_t* __restrict__ value, // [num_tokens, num_heads, head_size]
173 uint8_t* __restrict__ key_data_cache, // data region for K
174 uint8_t* __restrict__ value_data_cache, // data region for V
175 uint8_t* __restrict__ key_scale_cache, // scale region for K
176 uint8_t* __restrict__ value_scale_cache, // scale region for V
177 const int64_t* __restrict__ slot_mapping, // [num_actual_tokens]
178 const float* __restrict__ k_scale_ptr, // pointer to checkpoint k_scale
179 const float* __restrict__ v_scale_ptr, // pointer to checkpoint v_scale
180 const int64_t key_stride, // key.stride(0) in elements
181 const int64_t value_stride, // value.stride(0) in elements
182 const int num_heads, const int head_size, const int block_size,
183 const int64_t data_block_stride, // data cache stride for dim 0
184 const int64_t data_head_stride, // data cache stride for heads
185 const int64_t data_block_offset_stride, // data cache stride for tokens
186 const int64_t scale_block_stride, // scale cache stride for dim 0
187 const int64_t scale_head_stride, // scale cache stride for heads
188 const int64_t scale_block_offset_stride // scale cache stride for tokens
189 ) {
190 using CudaType = typename CUDATypeConverter<scalar_t>::Type;
191 using PVec = PackedVec<CudaType, CVT_FP4_PACK16>;
192
193 static constexpr int ELTS = CVT_FP4_ELTS_PER_THREAD; // 16 or 8
194 static constexpr int THREADS_PER_SF = CVT_FP4_SF_VEC_SIZE / ELTS;
195
196 const int64_t token_idx = blockIdx.x;
197 const int64_t slot_idx = slot_mapping[token_idx];
198 if (slot_idx < 0) return;
199
200 const int64_t block_idx = slot_idx / block_size;
201 const int block_offset = static_cast<int>(slot_idx % block_size);
202
203 const int scale_dim = head_size / 16;
204 const int groups_per_head = head_size / CVT_FP4_SF_VEC_SIZE;
205
206 const int total_groups = num_heads * groups_per_head;
207 const int tid = threadIdx.x;
208 const int num_thread_groups = blockDim.x / THREADS_PER_SF;
209 const int tg_id = tid / THREADS_PER_SF;
210 const int tg_lane = tid % THREADS_PER_SF;
211
212 // Process both K (kv=0) and V (kv=1)
213 #pragma unroll
214 for (int kv = 0; kv < 2; kv++) {
215 const scalar_t* __restrict__ src = (kv == 0) ? key : value;
216 const float global_scale = 1.0f / ((kv == 0) ? *k_scale_ptr : *v_scale_ptr);
217 const int64_t src_stride = (kv == 0) ? key_stride : value_stride;
218 uint8_t* __restrict__ data_cache =
219 (kv == 0) ? key_data_cache : value_data_cache;
220 uint8_t* __restrict__ sc_cache =
221 (kv == 0) ? key_scale_cache : value_scale_cache;
222
223 // Source pointer for this token (use actual stride, not assumed contiguous)
224 const CudaType* __restrict__ token_src =
225 reinterpret_cast<const CudaType*>(src) + token_idx * src_stride;
226
227 // Destination bases in data and scale caches for this token's block
228 uint8_t* __restrict__ data_block =
229 data_cache + block_idx * data_block_stride;
230 uint8_t* __restrict__ scale_block =
231 sc_cache + block_idx * scale_block_stride;
232
233 for (int g = tg_id; g < total_groups; g += num_thread_groups) {
234 const int head = g / groups_per_head;
235 const int group_in_head = g % groups_per_head;
236
237 // Load 16 (or 8) bf16 elements from source
238 PVec in_vec;
239 const CudaType* __restrict__ src_ptr =
240 token_src + head * head_size + group_in_head * CVT_FP4_SF_VEC_SIZE +
241 tg_lane * ELTS;
242
243 #pragma unroll
244 for (int i = 0; i < ELTS / 2; i++) {
245 in_vec.elts[i] = reinterpret_cast<
246 const typename PackedTypeConverter<CudaType>::Type*>(src_ptr)[i];
247 }
248
249 // Quantize: produces packed fp4 and writes scale factor.
250 uint8_t sf_val;
251 uint8_t* sf_out_ptr = (tg_lane == 0) ? &sf_val : nullptr;
252
253 fp4_packed_t packed;
254 if constexpr (SCALE_SEARCH == NVFP4KVScaleSearch::FOUR_OVER_SIX) {
255 packed = cvt_warp_fp16_to_fp4_4over6<CudaType, THREADS_PER_SF>(
256 in_vec, global_scale, sf_out_ptr);
257 } else {
258 packed = cvt_warp_fp16_to_fp4<CudaType, THREADS_PER_SF>(
259 in_vec, global_scale, sf_out_ptr);
260 }
261
262 // Write packed FP4 data to data cache
263 uint8_t* __restrict__ data_dst = data_block + head * data_head_stride +
264 block_offset * data_block_offset_stride;
265
266 #if CVT_FP4_PACK16
267 {
268 // 16 elements → 8 bytes (u32x2)
269 int data_byte_offset = group_in_head * 8;
270 reinterpret_cast<uint64_t*>(data_dst + data_byte_offset)[0] =
271 (uint64_t(packed.hi) << 32) | uint64_t(packed.lo);
272 }
273 #else
274 {
275 // 8 elements → 4 bytes (uint32_t)
276 int data_byte_offset =
277 group_in_head * CVT_FP4_SF_VEC_SIZE / 2 + tg_lane * ELTS / 2;
278 reinterpret_cast<uint32_t*>(data_dst + data_byte_offset)[0] = packed;
279 }
280 #endif
281
282 // Write block scale to scale cache.
283 // K (kv==0): linear layout (no swizzle).
284 // V (kv==1): swizzled layout for SM100 trtllm-gen MHA kernel.
285 if (sf_out_ptr != nullptr) {
286 int scale_idx = group_in_head;
287 uint8_t* __restrict__ scale_dst;
288 if (kv == 0) {
289 scale_dst = scale_block + head * scale_head_stride +
290 block_offset * scale_block_offset_stride + scale_idx;
291 } else {
292 int swizzled_offset =
293 swizzle_scale_offset(block_offset, scale_idx, scale_dim);
294 int swizzled_t = swizzled_offset / scale_dim;
295 int swizzled_s = swizzled_offset % scale_dim;
296 scale_dst = scale_block + head * scale_head_stride +
297 swizzled_t * scale_block_offset_stride + swizzled_s;
298 }
299 *scale_dst = sf_val;
300 }
301 }
302 }
303 }
304
305 } // namespace vllm
306
307 // Non-template entry point callable from cache_kernels.cu.
308 // Receives key_cache/value_cache as kv_cache[:, 0] and kv_cache[:, 1].
309 // Each KV side contains both data and scale:
310 // page = [K_data | K_scale | V_data | V_scale]
311 void reshape_and_cache_nvfp4_dispatch(
312 torch::stable::Tensor& key, torch::stable::Tensor& value,
313 torch::stable::Tensor& key_cache, torch::stable::Tensor& value_cache,
314 torch::stable::Tensor& slot_mapping, torch::stable::Tensor& k_scale,
315 torch::stable::Tensor& v_scale, const std::string& kv_cache_dtype) {
316 int num_tokens = slot_mapping.size(0);
317 int num_heads = key.size(1);
318 int head_size = key.size(2);
319 int data_dim = head_size / 2;
320 int scale_dim = head_size / 16;
321 int full_dim = data_dim + scale_dim;
322
323 // key_cache is kv_cache[:, 0] with shape
324 // [num_blocks, block_size, num_heads, full_dim] in logical order.
325 // Strides encode the physical layout (HND or NHD).
326 STD_TORCH_CHECK(key_cache.dim() == 4, "key_cache must be 4D");
327 STD_TORCH_CHECK(key_cache.size(3) == full_dim,
328 "key_cache last dim must be data_dim + scale_dim, got ",
329 key_cache.size(3), " expected ", full_dim);
330
331 int block_size = key_cache.size(1);
332
333 STD_TORCH_CHECK(head_size % 16 == 0,
334 "head_size must be divisible by 16 for NVFP4 KV cache");
335 STD_TORCH_CHECK(block_size % 4 == 0,
336 "block_size must be divisible by 4 for NVFP4 KV cache "
337 "swizzle");
338
339 // Detect physical layout from strides (based on full_dim).
340 // HND: head stride > block_offset stride.
341 bool is_hnd = key_cache.stride(2) > key_cache.stride(1);
342
343 int64_t data_block_stride = key_cache.stride(0); // page_bytes
344 int64_t data_head_stride, data_block_offset_stride;
345 if (is_hnd) {
346 data_head_stride = (int64_t)block_size * data_dim;
347 data_block_offset_stride = data_dim;
348 } else {
349 data_head_stride = data_dim;
350 data_block_offset_stride = (int64_t)num_heads * data_dim;
351 }
352
353 // Page layout: [K_data | K_scale | V_data | V_scale]
354 // Scale follows data within each KV side.
355 int64_t data_per_kv = (int64_t)num_heads * block_size * data_dim;
356
357 uint8_t* key_scale_ptr = key_cache.mutable_data_ptr<uint8_t>() + data_per_kv;
358 uint8_t* value_scale_ptr =
359 value_cache.mutable_data_ptr<uint8_t>() + data_per_kv;
360
361 // Scale strides: same page stride, inner strides from layout.
362 int64_t scale_block_stride = data_block_stride;
363 int64_t scale_head_stride, scale_block_offset_stride;
364 if (is_hnd) {
365 scale_head_stride = (int64_t)block_size * scale_dim;
366 scale_block_offset_stride = scale_dim;
367 } else {
368 scale_head_stride = scale_dim;
369 scale_block_offset_stride = (int64_t)num_heads * scale_dim;
370 }
371
372 const float* k_scale_ptr = k_scale.const_data_ptr<float>();
373 const float* v_scale_ptr = v_scale.const_data_ptr<float>();
374
375 int groups_per_head = head_size / CVT_FP4_SF_VEC_SIZE;
376 int total_groups = num_heads * groups_per_head;
377 constexpr int THREADS_PER_SF = CVT_FP4_SF_VEC_SIZE / CVT_FP4_ELTS_PER_THREAD;
378 int num_threads = std::min(total_groups * THREADS_PER_SF, 512);
379 num_threads = ((num_threads + 31) / 32) * 32;
380
381 dim3 grid(num_tokens);
382 dim3 block(num_threads);
383
384 const torch::stable::accelerator::DeviceGuard device_guard(
385 key.get_device_index());
386 const cudaStream_t stream = get_current_cuda_stream();
387
388 VLLM_STABLE_DISPATCH_HALF_TYPES(
389 key.scalar_type(), "reshape_and_cache_nvfp4", [&] {
390 if (kv_cache_dtype == "nvfp4") {
391 vllm::reshape_and_cache_nvfp4_kernel<
392 scalar_t, vllm::NVFP4KVScaleSearch::DEFAULT>
393 <<<grid, block, 0, stream>>>(
394 key.const_data_ptr<scalar_t>(),
395 value.const_data_ptr<scalar_t>(),
396 key_cache.mutable_data_ptr<uint8_t>(),
397 value_cache.mutable_data_ptr<uint8_t>(), key_scale_ptr,
398 value_scale_ptr, slot_mapping.const_data_ptr<int64_t>(),
399 k_scale_ptr, v_scale_ptr, key.stride(0), value.stride(0),
400 num_heads, head_size, block_size, data_block_stride,
401 data_head_stride, data_block_offset_stride,
402 scale_block_stride, scale_head_stride,
403 scale_block_offset_stride);
404 } else if (kv_cache_dtype == "nvfp4_4over6") {
405 vllm::reshape_and_cache_nvfp4_kernel<
406 scalar_t, vllm::NVFP4KVScaleSearch::FOUR_OVER_SIX>
407 <<<grid, block, 0, stream>>>(
408 key.const_data_ptr<scalar_t>(),
409 value.const_data_ptr<scalar_t>(),
410 key_cache.mutable_data_ptr<uint8_t>(),
411 value_cache.mutable_data_ptr<uint8_t>(), key_scale_ptr,
412 value_scale_ptr, slot_mapping.const_data_ptr<int64_t>(),
413 k_scale_ptr, v_scale_ptr, key.stride(0), value.stride(0),
414 num_heads, head_size, block_size, data_block_stride,
415 data_head_stride, data_block_offset_stride,
416 scale_block_stride, scale_head_stride,
417 scale_block_offset_stride);
418 } else {
419 STD_TORCH_CHECK(false,
420 "Unsupported NVFP4 KV cache dtype: ", kv_cache_dtype);
421 }
422 });
423 }