main
py 506 lines 19.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """V4.1 state saving/compression and independently schedulable cache insertion."""
4
5 import torch
6
7 from vllm.models.deepseek_v4.common.ops.fused_indexer_q import _fp32x2_to_fp4x2
8 from vllm.platforms import current_platform
9 from vllm.triton_utils import tl, triton
10
11 if current_platform.is_rocm():
12 from vllm.platforms.rocm import _ON_GFX950
13 else:
14 _ON_GFX950 = False
15
16 # The ring and raw rows a ratio-2 request program pools are not adjacent, so
17 # reading them as one tile means joining two pointers. ROCm's Triton fails to
18 # legalize `tt.join` on pointers, so there the two rows are loaded separately.
19 _JOIN_ROW_PTRS = not current_platform.is_rocm()
20
21
22 def fused_save_compress_norm(
23 kv_score: torch.Tensor,
24 positions: torch.Tensor,
25 state_cache: torch.Tensor | None,
26 slot_mapping: torch.Tensor,
27 query_start_loc: torch.Tensor | None,
28 token_to_req_indices: torch.Tensor | None,
29 rms_norm_weight: torch.Tensor,
30 rms_norm_eps: float,
31 compress_ratio: int,
32 latent_out: torch.Tensor,
33 ) -> None:
34 """Pool each closed group into a normalized BF16 latent; save FP32 states.
35
36 The latent feeds the main-cache insert and the indexer K path, which the
37 attention layer schedules on separate streams.
38
39 Ratio 2 keeps one ring block per request holding the open group's rows:
40 position ``p`` lives in row ``p % capacity`` and ``slot_mapping`` encodes
41 ``block * capacity + p % capacity``. The grid has one program per request
42 followed by one per pair of packed tokens. A request program handles the
43 group that the chunk's first token closes with its predecessor's ring row,
44 then stores the chunk's last ``capacity`` rows to the ring; because the
45 same program does both, ring reads and writes never race. A pair program
46 handles the group that ends inside its pair, reading both rows from the
47 raw input. Ratio 1 has no ring and one program per token; ``slot_mapping``
48 then only marks valid tokens.
49
50 Args:
51 kv_score: FP32 [tokens, 512] for CR1, [tokens, 1024] for CR2.
52 positions: Absolute positions of the packed request tokens.
53 state_cache: Ring FP32 [blocks, capacity, 1024] KV/score states (CR2).
54 slot_mapping: Ring slots (CR2) or main-cache slots (CR1).
55 query_start_loc: [num_reqs + 1] token offsets of each request's chunk.
56 token_to_req_indices: Request indices for the packed token rows.
57 rms_norm_weight: BF16 [512] normalization weight.
58 rms_norm_eps: RMSNorm epsilon.
59 compress_ratio: Group size, either 1 or 2.
60 latent_out: BF16 [tokens, 512], written only at valid group boundaries.
61
62 """
63 assert compress_ratio in (1, 2)
64 assert kv_score.dtype == torch.float32
65 assert kv_score.shape[1] == 512 * compress_ratio and kv_score.stride(1) == 1
66 assert latent_out.shape == (kv_score.shape[0], 512)
67 assert latent_out.is_contiguous() and latent_out.dtype == torch.bfloat16
68 assert positions.is_contiguous() and slot_mapping.is_contiguous()
69 # Rows stay 64-byte aligned, as the kernel's tl.multiple_of hint promises.
70 assert kv_score.stride(0) % 16 == 0
71 if compress_ratio == 2:
72 assert state_cache is not None and query_start_loc is not None
73 assert token_to_req_indices is not None
74 assert query_start_loc.is_contiguous()
75 assert token_to_req_indices.is_contiguous()
76 assert state_cache.dtype == torch.float32
77 assert state_cache.shape[2] == 1024 and state_cache.stride(2) == 1
78 assert state_cache.stride(1) % 16 == 0
79 state_stride, state_row_stride, state_block = (
80 state_cache.stride(0),
81 state_cache.stride(1),
82 state_cache.shape[1],
83 )
84 num_reqs = query_start_loc.numel() - 1
85 else:
86 state_cache = query_start_loc = token_to_req_indices = None
87 state_stride = state_row_stride = state_block = 1
88 num_reqs = 0
89 num_tokens = slot_mapping.numel()
90 assert num_tokens <= min(kv_score.shape[0], positions.numel())
91 if num_tokens == 0:
92 return
93 grid = num_reqs + triton.cdiv(num_tokens, compress_ratio)
94 _fused_save_compress_norm_kernel[(grid,)](
95 kv_score,
96 positions,
97 state_cache,
98 slot_mapping,
99 query_start_loc,
100 token_to_req_indices,
101 rms_norm_weight,
102 latent_out,
103 num_tokens,
104 num_reqs,
105 RAW_STRIDE=kv_score.stride(0),
106 STATE_STRIDE=state_stride,
107 STATE_ROW_STRIDE=state_row_stride,
108 STATE_BLOCK=state_block,
109 COMPRESS_RATIO=compress_ratio,
110 EPS=rms_norm_eps,
111 JOIN_ROW_PTRS=_JOIN_ROW_PTRS,
112 num_warps=4,
113 **({"launch_pdl": False} if current_platform.is_cuda() else {}),
114 )
115
116
117 @triton.jit(do_not_specialize=["num_tokens", "num_reqs"])
118 def _fused_save_compress_norm_kernel(
119 raw,
120 positions,
121 state,
122 state_slots,
123 query_start_loc,
124 req_ids,
125 norm_weight,
126 latent,
127 num_tokens,
128 num_reqs,
129 RAW_STRIDE: tl.constexpr,
130 STATE_STRIDE: tl.constexpr,
131 STATE_ROW_STRIDE: tl.constexpr,
132 STATE_BLOCK: tl.constexpr,
133 COMPRESS_RATIO: tl.constexpr,
134 EPS: tl.constexpr,
135 JOIN_ROW_PTRS: tl.constexpr,
136 ):
137 pid = tl.program_id(0)
138 d = tl.arange(0, 512)
139
140 if COMPRESS_RATIO == 2: # noqa: SIM102 (constexpr branch, then runtime)
141 if pid < num_reqs:
142 # Request program: the group closed by the chunk's first token,
143 # then the ring tail store.
144 start = tl.load(query_start_loc + pid)
145 end = tl.load(query_start_loc + pid + 1)
146 if start >= end:
147 return
148 state_slot = tl.load(state_slots + start)
149 if state_slot < 0:
150 return
151 position = tl.load(positions + start)
152 if (position + 1) % 2 == 0:
153 ring = state + (state_slot // STATE_BLOCK).to(tl.int64) * STATE_STRIDE
154 prev = ring + ((position - 1) % STATE_BLOCK) * STATE_ROW_STRIDE
155 current = raw + start.to(tl.int64) * RAW_STRIDE
156 if JOIN_ROW_PTRS:
157 # Joining pointers hides row alignment from Triton; restate it.
158 rows = tl.multiple_of(tl.join(prev, current)[:, None], (16, 16))
159 kv = tl.load(rows + d[None, :])
160 score = tl.load(rows + 512 + d[None, :])
161 # Every lane has read the ring before the tail store below
162 # writes it.
163 tl.debug_barrier()
164 pooled = tl.sum(kv * tl.softmax(score, 0), 0)
165 else:
166 kv_prev = tl.load(prev + d)
167 score_prev = tl.load(prev + 512 + d)
168 kv_current = tl.load(current + d)
169 score_current = tl.load(current + 512 + d)
170 # Every lane has read the ring before the tail store below
171 # writes it.
172 tl.debug_barrier()
173 peak = tl.maximum(score_prev, score_current)
174 weight_prev = tl.exp(score_prev - peak)
175 weight_current = tl.exp(score_current - peak)
176 pooled = (kv_prev * weight_prev + kv_current * weight_current) / (
177 weight_prev + weight_current
178 )
179 _store_latent(pooled, start, norm_weight, latent, EPS)
180 num_rows = tl.minimum(end - start, STATE_BLOCK)
181 for k in tl.range(0, num_rows):
182 token = end - num_rows + k
183 slot = tl.load(state_slots + token)
184 if slot >= 0:
185 row = (
186 state
187 + (slot // STATE_BLOCK).to(tl.int64) * STATE_STRIDE
188 + (slot % STATE_BLOCK) * STATE_ROW_STRIDE
189 )
190 src = raw + token.to(tl.int64) * RAW_STRIDE
191 tl.store(row + d, tl.load(src + d))
192 tl.store(row + 512 + d, tl.load(src + 512 + d))
193 return
194
195 # Group program: one token (ratio 1) or the token of the pair that ends
196 # a group (ratio 2). A chunk's first token belongs to its request program.
197 t = (pid - num_reqs) * COMPRESS_RATIO
198 if COMPRESS_RATIO == 2:
199 t += (tl.load(positions + t) + 1) % 2
200 state_slot = tl.load(state_slots + t, t < num_tokens, other=-1)
201 if state_slot < 0:
202 return
203 if COMPRESS_RATIO == 1:
204 pooled = tl.load(raw + t.to(tl.int64) * RAW_STRIDE + d)
205 else:
206 if t == tl.load(query_start_loc + tl.load(req_ids + t)):
207 return
208 rows = raw + (t.to(tl.int64) - 1 + tl.arange(0, 2)[:, None]) * RAW_STRIDE
209 kv = tl.load(rows + d[None, :])
210 score = tl.load(rows + 512 + d[None, :])
211 pooled = tl.sum(kv * tl.softmax(score, 0), 0)
212 _store_latent(pooled, t, norm_weight, latent, EPS)
213
214
215 @triton.jit
216 def _store_latent(pooled, t, norm_weight, latent, EPS: tl.constexpr):
217 d = tl.arange(0, 512)
218 weight = tl.load(norm_weight + d).to(tl.float32)
219 variance = tl.sum(pooled * pooled, 0) / 512
220 normed = pooled * tl.rsqrt(variance + EPS) * weight
221 tl.store(latent + t.to(tl.int64) * 512 + d, normed.to(tl.bfloat16))
222
223
224 def rope_quant_insert(
225 latent: torch.Tensor,
226 positions: torch.Tensor,
227 cos_sin_cache: torch.Tensor,
228 kv_cache: torch.Tensor,
229 slot_mapping: torch.Tensor,
230 compress_ratio: int,
231 fp8_scale: torch.Tensor | None = None,
232 ) -> None:
233 """Apply GPT-J RoPE and publish a latent to the compressed KV cache.
234
235 The BF16 latent supplies both NoPE quantization and RoPE input. It is read
236 only for valid slots at group boundaries. The cache dtype selects the
237 layout: ``uint8`` is a paged FlashMLA layout, whose record the per-token
238 byte width names -- 584 B for V4 (576 value bytes and eight segregated
239 UE8M0 scale bytes, including one zero padding scale), 528 B for V4.1
240 (512 MXFP8 value bytes covering the RoPE dims too, then 16 UE8M0 scales of
241 32 dims each), or 288 B for V4.1 NVFP4 (256 bytes of e2m1 pairs then 32
242 e4m3 scales of 16 dims each), which only the compressed cache uses.
243 ``bfloat16`` and ``float8_e4m3fn`` are the plain [448 NoPE | 64 RoPE] rows
244 read by FlashInfer, the latter scaled by the per-tensor ``fp8_scale``.
245 """
246 assert compress_ratio in (1, 2)
247 assert latent.shape[1] == 512 and latent.dtype == torch.bfloat16
248 assert latent.is_contiguous()
249 num_tokens = slot_mapping.numel()
250 assert num_tokens <= min(latent.shape[0], positions.numel())
251 if num_tokens == 0:
252 return
253 launch_kwargs = {"launch_pdl": False} if current_platform.is_cuda() else {}
254 if kv_cache.dtype == torch.uint8:
255 kernel = {
256 584: _rope_quant_insert_kernel,
257 528: _rope_quant_insert_mxfp8_kernel,
258 288: _rope_quant_insert_nvfp4_kernel,
259 }.get(kv_cache.shape[-1])
260 assert kernel is not None, (
261 f"unsupported paged KV record width {kv_cache.shape[-1]}"
262 )
263 kernel[(num_tokens,)](
264 latent,
265 positions,
266 cos_sin_cache,
267 kv_cache,
268 slot_mapping,
269 COS_STRIDE=cos_sin_cache.stride(0),
270 CACHE_STRIDE=kv_cache.stride(0),
271 CACHE_BLOCK=kv_cache.shape[1],
272 COMPRESS_RATIO=compress_ratio,
273 SANITIZE_CACHE_NANS=_ON_GFX950,
274 num_warps=4,
275 **launch_kwargs,
276 )
277 return
278
279 assert kv_cache.dtype in (torch.bfloat16, torch.float8_e4m3fn)
280 assert kv_cache.shape[-1] == 512 and kv_cache.stride(-1) == 1
281 store_fp8 = kv_cache.dtype == torch.float8_e4m3fn
282 if store_fp8:
283 assert fp8_scale is not None and fp8_scale.numel() == 1
284 assert fp8_scale.dtype == torch.float32
285 _rope_plain_insert_kernel[(num_tokens,)](
286 latent,
287 positions,
288 cos_sin_cache,
289 kv_cache,
290 slot_mapping,
291 fp8_scale if store_fp8 else None,
292 COS_STRIDE=cos_sin_cache.stride(0),
293 CACHE_STRIDE=kv_cache.stride(0),
294 ROW_STRIDE=kv_cache.stride(1),
295 CACHE_BLOCK=kv_cache.shape[1],
296 COMPRESS_RATIO=compress_ratio,
297 STORE_FP8=store_fp8,
298 num_warps=4,
299 **launch_kwargs,
300 )
301
302
303 @triton.jit
304 def _rope_quant_insert_kernel(
305 latent,
306 positions,
307 cos_sin,
308 cache,
309 cache_slots,
310 COS_STRIDE: tl.constexpr,
311 CACHE_STRIDE: tl.constexpr,
312 CACHE_BLOCK: tl.constexpr,
313 COMPRESS_RATIO: tl.constexpr,
314 SANITIZE_CACHE_NANS: tl.constexpr,
315 ):
316 t = tl.program_id(0)
317 slot = tl.load(cache_slots + t)
318 if slot < 0:
319 return
320 position = tl.load(positions + t)
321 if (position + 1) % COMPRESS_RATIO != 0:
322 return
323 d = tl.arange(0, 512)
324 normed = tl.load(latent + t.to(tl.int64) * 512 + d).to(tl.float32)
325 page = cache + (slot // CACHE_BLOCK).to(tl.int64) * CACHE_STRIDE
326 values = page + (slot % CACHE_BLOCK) * 576
327 scales = page + CACHE_BLOCK * 576 + (slot % CACHE_BLOCK) * 8
328
329 quant = tl.reshape(normed, (8, 64))
330 amax = tl.maximum(tl.max(tl.abs(quant), 1), 1e-4)
331 exponent = tl.ceil(tl.log2(amax * (1.0 / 448.0)))
332 scaled = quant * tl.reshape(tl.exp2(-exponent), (8, 1))
333 fp8 = tl.clamp(scaled, -448.0, 448.0).to(tl.float8e4nv)
334 packed = tl.reshape(fp8.to(tl.uint8, bitcast=True), (512,))
335 tl.store(values + d, packed, d < 448)
336 s = tl.arange(0, 8)
337 max_encoded: tl.constexpr = 254.0 if SANITIZE_CACHE_NANS else 255.0
338 encoded = tl.minimum(tl.maximum(exponent + 127.0, 0.0), max_encoded)
339 tl.store(scales + s, encoded.to(tl.uint8), s < 7)
340 tl.store(scales + 7, tl.full((), 0, tl.uint8))
341
342 even, odd = tl.split(tl.reshape(normed, (256, 2)))
343 pair = tl.arange(0, 256) - 224
344 cs = cos_sin + (position // COMPRESS_RATIO * COMPRESS_RATIO) * COS_STRIDE
345 c = tl.load(cs + tl.maximum(pair, 0), pair >= 0, other=1.0).to(tl.float32)
346 s = tl.load(cs + 32 + tl.maximum(pair, 0), pair >= 0, other=0.0).to(tl.float32)
347 rotated = tl.interleave(even * c - odd * s, odd * c + even * s)
348 if SANITIZE_CACHE_NANS:
349 rotated = tl.where(rotated == rotated, rotated, 0.0)
350 rope_dst = (values + 448).to(tl.pointer_type(tl.bfloat16))
351 tl.store(rope_dst + d - 448, rotated.to(tl.bfloat16), d >= 448)
352
353
354 @triton.jit
355 def _rope_quant_insert_mxfp8_kernel(
356 latent,
357 positions,
358 cos_sin,
359 cache,
360 cache_slots,
361 COS_STRIDE: tl.constexpr,
362 CACHE_STRIDE: tl.constexpr,
363 CACHE_BLOCK: tl.constexpr,
364 COMPRESS_RATIO: tl.constexpr,
365 SANITIZE_CACHE_NANS: tl.constexpr,
366 ):
367 """V4.1 record: RoPE first, then MXFP8-quantize all 512 dims.
368
369 The RoPE dims are quantized here too, so unlike the V4 kernel the rotation
370 has to happen before the scales are picked.
371 """
372 t = tl.program_id(0)
373 slot = tl.load(cache_slots + t)
374 if slot < 0:
375 return
376 position = tl.load(positions + t)
377 if (position + 1) % COMPRESS_RATIO != 0:
378 return
379 d = tl.arange(0, 512)
380 normed = tl.load(latent + t.to(tl.int64) * 512 + d).to(tl.float32)
381
382 # NoPE pairs load (cos, sin) = (1, 0), so the rotation is the identity there.
383 even, odd = tl.split(tl.reshape(normed, (256, 2)))
384 pair = tl.arange(0, 256) - 224
385 cs = cos_sin + (position // COMPRESS_RATIO * COMPRESS_RATIO) * COS_STRIDE
386 c = tl.load(cs + tl.maximum(pair, 0), pair >= 0, other=1.0).to(tl.float32)
387 s = tl.load(cs + 32 + tl.maximum(pair, 0), pair >= 0, other=0.0).to(tl.float32)
388 rotated = tl.interleave(even * c - odd * s, odd * c + even * s)
389 if SANITIZE_CACHE_NANS:
390 rotated = tl.where(rotated == rotated, rotated, 0.0)
391
392 page = cache + (slot // CACHE_BLOCK).to(tl.int64) * CACHE_STRIDE
393 values = page + (slot % CACHE_BLOCK) * 512
394 scales = page + CACHE_BLOCK * 512 + (slot % CACHE_BLOCK) * 16
395
396 quant = tl.reshape(rotated, (16, 32))
397 amax = tl.maximum(tl.max(tl.abs(quant), 1), 1e-4)
398 exponent = tl.ceil(tl.log2(amax * (1.0 / 448.0)))
399 scaled = quant * tl.reshape(tl.exp2(-exponent), (16, 1))
400 fp8 = tl.clamp(scaled, -448.0, 448.0).to(tl.float8e4nv)
401 tl.store(values + d, tl.reshape(fp8.to(tl.uint8, bitcast=True), (512,)))
402
403 max_encoded: tl.constexpr = 254.0 if SANITIZE_CACHE_NANS else 255.0
404 encoded = tl.minimum(tl.maximum(exponent + 127.0, 0.0), max_encoded)
405 tl.store(scales + tl.arange(0, 16), encoded.to(tl.uint8))
406
407
408 @triton.jit
409 def _rope_quant_insert_nvfp4_kernel(
410 latent,
411 positions,
412 cos_sin,
413 cache,
414 cache_slots,
415 COS_STRIDE: tl.constexpr,
416 CACHE_STRIDE: tl.constexpr,
417 CACHE_BLOCK: tl.constexpr,
418 COMPRESS_RATIO: tl.constexpr,
419 SANITIZE_CACHE_NANS: tl.constexpr,
420 ):
421 """V4.1 NVFP4 record: RoPE, then e2m1 with one e4m3 scale per 16 dims.
422
423 The scale is ``amax / 6`` (6 is e2m1's largest magnitude) clamped to the
424 e4m3 range, with no per-tensor scale on top.
425 """
426 t = tl.program_id(0)
427 slot = tl.load(cache_slots + t)
428 if slot < 0:
429 return
430 position = tl.load(positions + t)
431 if (position + 1) % COMPRESS_RATIO != 0:
432 return
433 d = tl.arange(0, 512)
434 normed = tl.load(latent + t.to(tl.int64) * 512 + d).to(tl.float32)
435
436 # NoPE pairs load (cos, sin) = (1, 0), so the rotation is the identity there.
437 even, odd = tl.split(tl.reshape(normed, (256, 2)))
438 pair = tl.arange(0, 256) - 224
439 cs = cos_sin + (position // COMPRESS_RATIO * COMPRESS_RATIO) * COS_STRIDE
440 c = tl.load(cs + tl.maximum(pair, 0), pair >= 0, other=1.0).to(tl.float32)
441 s = tl.load(cs + 32 + tl.maximum(pair, 0), pair >= 0, other=0.0).to(tl.float32)
442 rotated = tl.interleave(even * c - odd * s, odd * c + even * s)
443 if SANITIZE_CACHE_NANS:
444 rotated = tl.where(rotated == rotated, rotated, 0.0)
445
446 tiles = tl.reshape(rotated, (32, 16))
447 amax = tl.max(tl.abs(tiles), 1)
448 # 2**-9 is the smallest normal e4m3 magnitude; 448 the largest.
449 scale = tl.clamp(amax * (1.0 / 6.0), 0.001953125, 448.0).to(tl.float8e4nv)
450 # Round-to-nearest division: Triton's default div.full misplaces values
451 # that land exactly on an e2m1 tie.
452 scaled = tl.math.div_rn(tiles, tl.reshape(scale.to(tl.float32), (32, 1)))
453 lo, hi = tl.split(tl.reshape(tl.reshape(scaled, (512,)), (256, 2)))
454
455 page = cache + (slot // CACHE_BLOCK).to(tl.int64) * CACHE_STRIDE
456 tl.store(
457 page + (slot % CACHE_BLOCK) * 256 + tl.arange(0, 256),
458 _fp32x2_to_fp4x2(lo, hi),
459 )
460 scales = page + CACHE_BLOCK * 256 + (slot % CACHE_BLOCK) * 32
461 tl.store(scales + tl.arange(0, 32), scale.to(tl.uint8, bitcast=True))
462
463
464 @triton.jit
465 def _rope_plain_insert_kernel(
466 latent,
467 positions,
468 cos_sin,
469 cache,
470 cache_slots,
471 fp8_scale,
472 COS_STRIDE: tl.constexpr,
473 CACHE_STRIDE: tl.constexpr,
474 ROW_STRIDE: tl.constexpr,
475 CACHE_BLOCK: tl.constexpr,
476 COMPRESS_RATIO: tl.constexpr,
477 STORE_FP8: tl.constexpr,
478 ):
479 t = tl.program_id(0)
480 slot = tl.load(cache_slots + t)
481 if slot < 0:
482 return
483 position = tl.load(positions + t)
484 if (position + 1) % COMPRESS_RATIO != 0:
485 return
486 d = tl.arange(0, 512)
487 normed = tl.load(latent + t.to(tl.int64) * 512 + d).to(tl.float32)
488
489 # NoPE pairs load (cos, sin) = (1, 0), so the rotation is the identity there.
490 even, odd = tl.split(tl.reshape(normed, (256, 2)))
491 pair = tl.arange(0, 256) - 224
492 cs = cos_sin + (position // COMPRESS_RATIO * COMPRESS_RATIO) * COS_STRIDE
493 c = tl.load(cs + tl.maximum(pair, 0), pair >= 0, other=1.0).to(tl.float32)
494 s = tl.load(cs + 32 + tl.maximum(pair, 0), pair >= 0, other=0.0).to(tl.float32)
495 row = tl.interleave(even * c - odd * s, odd * c + even * s).to(tl.bfloat16)
496
497 dst = (
498 cache
499 + (slot // CACHE_BLOCK).to(tl.int64) * CACHE_STRIDE
500 + (slot % CACHE_BLOCK) * ROW_STRIDE
501 )
502 if STORE_FP8:
503 scaled = row.to(tl.float32) * (1.0 / tl.load(fp8_scale))
504 tl.store(dst + d, tl.clamp(scaled, -448.0, 448.0).to(tl.float8e4nv))
505 else:
506 tl.store(dst + d, row)