[ROCm][DSv4] Functional fixes for DeepSeek V4 on MI300X/MI325X (#45681)

Signed-off-by: ganyi <ygan@amd.com> Signed-off-by: Markus Hartikainen <markus.hartikainen@amd.com> Signed-off-by: Tuukka Sarvi <tuukka.sarvi@amd.com> Co-authored-by: ganyi <ygan@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Markus Hartikainen <markus.hartikainen@amd.com> Co-authored-by: Jin Tao <jintao12@amd.com>

Tuukka Sarvi committed Jun 18, 2026 at 15:21 UTC afdcbd5d39eaf2b37b616c8ee8aabc51e15e70ef
8 files changed +545 -52
csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu
+11 -4
@@ -18,7 +18,7 @@
18 * ROPE_DIM = 64 (RoPE applied to dims [NOPE_DIM, HEAD_DIM))
19 * NOPE_DIM = 448
20 * QUANT_BLOCK = 64 (UE8M0 FP8 quant block)
21 - * FP8_MAX = 448.0f
21 + * FP8_MAX = 224.0f on ROCm FNUZ / 448.0f on OCP
22 * is_neox=false (GPT-J interleaved pairs)
23 * cos_sin_cache layout [max_pos, rope_dim] = cos || sin (cos first, sin
24 * second along last dim; each half is rope_dim/2 = 32 values)
@@ -61,10 +61,11 @@
61 #ifdef USE_ROCM
62 // ROCm-compatible FP8 conversion helpers
63 __device__ __forceinline__ uint8_t rocm_cvt_float_to_fp8_e4m3(float val) {
64 - #if defined(HIP_FP8_TYPE_OCP)
65 - __hip_fp8_e4m3 fp8_val(val);
66 - #else
64 + // gfx942 uses FNUZ FP8; other ROCm targets use OCP E4M3.
65 + #if defined(__gfx942__)
66 __hip_fp8_e4m3_fnuz fp8_val(val);
67 + #else
68 + __hip_fp8_e4m3 fp8_val(val);
69 #endif
70 return reinterpret_cast<uint8_t&>(fp8_val);
71 }
@@ -90,7 +91,13 @@ constexpr int kQuantBlock = 64;
91 constexpr int kNumQuantBlocks = kNopeDim / kQuantBlock; // 7
92 constexpr int kScaleBytesPerToken = kNumQuantBlocks + 1; // 8 (7 real + 1 pad)
93 constexpr int kTokenDataBytes = kNopeDim + kRopeDim * 2; // 448 + 128 = 576
94 +// FNUZ on gfx942 / OCP elsewhere. FNUZ uses 224.0 (not the dtype's raw
95 +// 240.0) to match the rest of vLLM's FNUZ pipeline.
96 +#if defined(USE_ROCM) && defined(__gfx942__)
97 +constexpr float kFp8Max = 224.0f;
98 +#else
99 constexpr float kFp8Max = 448.0f;
100 +#endif
101
102 #ifndef USE_ROCM
103 // When num_tokens is less than this threshold,
tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py
+147 -24
@@ -19,17 +19,28 @@ The kernel is imported via
19 import pytest
20 import torch
21
22 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
23 + get_fp8_min_max,
24 +)
25 from vllm.models.deepseek_v4.common.ops import (
26 dequantize_and_gather_k_cache,
27 quantize_and_insert_k_cache,
28 )
29 +from vllm.platforms import current_platform
30
31 # ── Constants matching the kernel ────────────────────────────────────────────
32 HEAD_DIM = 512
33 ROPE_DIM = 64
34 NOPE_DIM = HEAD_DIM - ROPE_DIM # 448
35 QUANT_BLOCK = 64
32 -FP8_MAX = 448.0
36 +# Match the C++ SWA-K encoder: FNUZ on gfx942, OCP elsewhere.
37 +USE_FNUZ = current_platform.is_fp8_fnuz()
38 +_, FP8_MAX = get_fp8_min_max()
39 +# The kernel emits FNUZ-encoded fp8 bytes on gfx942 (rocm_cvt_float_to_fp8_e4m3)
40 +# but stores them into float8_e4m3fn-typed tensors, matching vLLM's ROCm cache
41 +# convention. References must encode under the same scheme and the kernel's
42 +# e4m3fn-typed outputs must be reinterpreted under it before decoding.
43 +FP8_STORE_DTYPE = torch.float8_e4m3fnuz if USE_FNUZ else torch.float8_e4m3fn
44 HEAD_BYTES = NOPE_DIM + ROPE_DIM * 2 + 8 # 448 + 128 + 8 = 584
45
46
@@ -81,10 +92,11 @@ def apply_rope_gptj_last_k(
92 cos = cos.unsqueeze(1)
93 sin = sin.unsqueeze(1)
94
84 - # Use addcmul (compiles to FMA on CUDA) for the 2x2 rotation. nvcc lowers
85 - # the kernel's `e*c - o*s` to fma(e, c, -o*s); matching that here keeps
86 - # near-cancellation pairs on the same bf16 grid as the kernel output and
87 - # avoids spurious 1-ULP boundary flips at high num_tokens.
95 + # Use addcmul (an FMA) for the 2x2 rotation to mirror the kernel's
96 + # `e*c - o*s` fused form. This keeps the reference close to the kernel, but
97 + # the fp32 reference and the fp32 GPU kernel can still round to bf16 on
98 + # opposite sides of a round-to-nearest tie for a tiny number of elements at
99 + # high positions, so callers compare the RoPE region within 1 bf16 ULP.
100 new_even = torch.addcmul(-odd * sin, even, cos)
101 new_odd = torch.addcmul(odd * cos, even, sin)
102 rope_rotated = torch.stack((new_even, new_odd), dim=-1).reshape(shape)
@@ -148,6 +160,86 @@ def _call_fused(
160 )
161
162
163 +def _bf16_ulp_distance(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
164 + """Representable-step distance between two bf16 tensors.
165 +
166 + Reinterprets the bf16 bit patterns under the IEEE-754 total ordering so
167 + that adjacent representable values differ by exactly 1.
168 + """
169 +
170 + def key(t: torch.Tensor) -> torch.Tensor:
171 + u = t.contiguous().view(torch.int16).to(torch.int64) & 0xFFFF
172 + return torch.where(u >= 0x8000, 0xFFFF - u, u + 0x8000)
173 +
174 + return (key(a) - key(b)).abs()
175 +
176 +
177 +def _fp8_ulp_distance(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
178 + """Representable-step distance between two 8-bit fp8 tensors.
179 +
180 + Reinterprets the fp8 bytes under a sign-magnitude total ordering so that
181 + adjacent representable values differ by exactly 1. Inputs must already share
182 + the same fp8 encoding (e.g. both FP8_STORE_DTYPE).
183 + """
184 +
185 + def key(t: torch.Tensor) -> torch.Tensor:
186 + u = t.contiguous().view(torch.uint8).to(torch.int64)
187 + return torch.where(u >= 0x80, 0xFF - u, u + 0x80)
188 +
189 + return (key(a) - key(b)).abs()
190 +
191 +
192 +def _as_stored_fp8(t: torch.Tensor) -> torch.Tensor:
193 + """Reinterpret a float8_e4m3fn-typed kernel output under the real (FNUZ on
194 + gfx942) encoding the kernel actually wrote, without touching the bytes."""
195 + return t.contiguous().view(torch.uint8).view(FP8_STORE_DTYPE)
196 +
197 +
198 +def _dequant_cache(k_cache_2d, num_tokens, num_blocks, block_size):
199 + """Round-trip a [num_blocks, block_size*HEAD_BYTES] K-cache back to bf16."""
200 + device = k_cache_2d.device
201 + out = torch.zeros(1, num_tokens, HEAD_DIM, dtype=torch.bfloat16, device=device)
202 + seq_lens = torch.tensor([num_tokens], dtype=torch.int32, device=device)
203 + block_table = torch.arange(num_blocks, dtype=torch.int32, device=device).unsqueeze(
204 + 0
205 + )
206 + k_cache_3d = k_cache_2d.view(num_blocks, block_size, HEAD_BYTES)
207 + dequantize_and_gather_k_cache(
208 + out,
209 + k_cache_3d,
210 + seq_lens,
211 + None,
212 + block_table,
213 + block_size,
214 + offset=0,
215 + use_fnuz=USE_FNUZ,
216 + )
217 + return out[0, :num_tokens]
218 +
219 +
220 +def _assert_kv_cache_parity(
221 + k_cache_fused, k_cache_ref, num_tokens, num_blocks, block_size
222 +):
223 + """Assert the fused and reference K-caches agree after decoding.
224 +
225 + The NoPE region is deterministic UE8M0 FP8, so its round-trip must be
226 + bit-identical. The RoPE region is stored as bf16 after an fp32 rotation:
227 + the GPU kernel and the PyTorch reference can fall on opposite sides of a
228 + round-to-nearest tie and differ by at most one bf16 ULP. (Spot checks show
229 + the kernel value is the correctly-rounded one; the fp32 torch reference is
230 + the one that lands on the wrong side near a midpoint.) Allow <=1 ULP there.
231 + """
232 + rec_fused = _dequant_cache(k_cache_fused, num_tokens, num_blocks, block_size)
233 + rec_ref = _dequant_cache(k_cache_ref, num_tokens, num_blocks, block_size)
234 + torch.testing.assert_close(
235 + rec_fused[:, :NOPE_DIM], rec_ref[:, :NOPE_DIM], rtol=0, atol=0
236 + )
237 + max_ulp = int(
238 + _bf16_ulp_distance(rec_fused[:, NOPE_DIM:], rec_ref[:, NOPE_DIM:]).max().item()
239 + )
240 + assert max_ulp <= 1, f"RoPE bf16 region differs by {max_ulp} ULP (>1)"
241 +
242 +
243 # ── Test 1: Q path numerical parity ──────────────────────────────────────────
244
245
@@ -241,7 +333,7 @@ def test_kv_path_matches_reference(num_tokens: int, block_size: int):
333 num_blocks, block_size * HEAD_BYTES, dtype=torch.uint8, device=device
334 )
335 quantize_and_insert_k_cache(
244 - kv_ref, k_cache_ref, slot_mapping, block_size=block_size
336 + kv_ref, k_cache_ref, slot_mapping, block_size=block_size, use_fnuz=USE_FNUZ
337 )
338
339 # ── Fused path (dummy q, padded to FlashMLA's min head count 64) ───────
@@ -273,7 +365,14 @@ def test_kv_path_matches_reference(num_tokens: int, block_size: int):
365 # gather_lens arg is None (use seq_lens)
366 k_cache_3d = k_cache_2d.view(num_blocks, block_size, HEAD_BYTES)
367 dequantize_and_gather_k_cache(
276 - out, k_cache_3d, seq_lens, None, block_table, block_size, offset=0
368 + out,
369 + k_cache_3d,
370 + seq_lens,
371 + None,
372 + block_table,
373 + block_size,
374 + offset=0,
375 + use_fnuz=USE_FNUZ,
376 )
377 return out[0, :num_tokens]
378
@@ -297,12 +396,10 @@ def test_kv_path_matches_reference(num_tokens: int, block_size: int):
396 f"fused NoPE token {t} diff {diff_fused} > {max_allowed}"
397 )
398
300 - # RoPE region: bf16 stored exactly → zero diff.
301 - rope_diff = (recovered_fused[:, NOPE_DIM:] - kv_ref[:, NOPE_DIM:]).abs().max()
302 - assert rope_diff.item() == 0.0, f"RoPE portion not exact: {rope_diff.item()}"
303 -
304 - # Exact byte equality of the two cache buffers — strong parity.
305 - torch.testing.assert_close(k_cache_fused, k_cache_ref, rtol=0, atol=0)
399 + # Strong parity: NoPE FP8 round-trip bit-identical, RoPE bf16 within 1 ULP.
400 + _assert_kv_cache_parity(
401 + k_cache_fused, k_cache_ref, num_tokens, num_blocks, block_size
402 + )
403
404
405 # ── Test 2b: DP padding (slot_mapping shorter than q/kv) ─────────────────────
@@ -336,7 +433,7 @@ def test_kv_path_with_dp_padding(num_tokens: int, pad: int, block_size: int):
433 num_blocks, block_size * HEAD_BYTES, dtype=torch.uint8, device=device
434 )
435 quantize_and_insert_k_cache(
339 - kv_ref, k_cache_ref, slot_mapping, block_size=block_size
436 + kv_ref, k_cache_ref, slot_mapping, block_size=block_size, use_fnuz=USE_FNUZ
437 )
438
439 # Fused: pass full-sized q/kv/positions, shorter slot_mapping.
@@ -354,7 +451,9 @@ def test_kv_path_with_dp_padding(num_tokens: int, pad: int, block_size: int):
451 block_size,
452 )
453
357 - torch.testing.assert_close(k_cache_fused, k_cache_ref, rtol=0, atol=0)
454 + _assert_kv_cache_parity(
455 + k_cache_fused, k_cache_ref, num_tokens, num_blocks, block_size
456 + )
457
458
459 # ── Test 3: combined single-call Q + KV parity ───────────────────────────────
@@ -403,7 +502,7 @@ def test_combined_q_and_kv(
502 num_blocks, block_size * HEAD_BYTES, dtype=torch.uint8, device=device
503 )
504 quantize_and_insert_k_cache(
406 - kv_ref, k_cache_ref, slot_mapping, block_size=block_size
505 + kv_ref, k_cache_ref, slot_mapping, block_size=block_size, use_fnuz=USE_FNUZ
506 )
507
508 # Fused single call.
@@ -426,7 +525,9 @@ def test_combined_q_and_kv(
525 assert pad_region.abs().max().item() == 0.0, (
526 "padded head slots must be exact zero"
527 )
429 - torch.testing.assert_close(k_cache_fused, k_cache_ref, rtol=0, atol=0)
528 + _assert_kv_cache_parity(
529 + k_cache_fused, k_cache_ref, num_tokens, num_blocks, block_size
530 + )
531
532
533 # ── Full-cache (FlashInfer) path parity ──────────────────────────────────────
@@ -499,7 +600,7 @@ def _fp8_full_cache_reference(
600 q_ref = apply_rope_gptj_last_k(q_ref, positions, cos_sin_cache)
601 q_fp8.copy_(
602 torch.clamp(q_ref.float() * q_fp8_scale_inv, -FP8_MAX, FP8_MAX).to(
502 - torch.float8_e4m3fn
603 + FP8_STORE_DTYPE
604 )
605 )
606
@@ -510,7 +611,7 @@ def _fp8_full_cache_reference(
611 pos_in_block = slots % block_size
612 k_cache[block_idx, pos_in_block] = torch.clamp(
613 kv_ref[valid].float() / fp8_scale, -FP8_MAX, FP8_MAX
513 - ).to(torch.float8_e4m3fn)
614 + ).to(FP8_STORE_DTYPE)
615
616
617 def _bf16_full_cache_reference(
@@ -565,12 +666,17 @@ def test_full_cache_per_tensor_fp8_matches_reference(
666 fp8_scale = torch.tensor([1.0], dtype=torch.float32, device=device)
667 q_fp8_scale_inv = torch.tensor([1.0], dtype=torch.float32, device=device)
668
568 - q_fp8_ref = torch.empty_like(q, dtype=torch.float8_e4m3fn)
669 + # References are encoded under the scheme the kernel actually writes
670 + # (FNUZ on gfx942); the kernel's own outputs must stay float8_e4m3fn-typed
671 + # because the op asserts that dtype.
672 + q_fp8_ref = torch.empty_like(q, dtype=FP8_STORE_DTYPE)
673 q_fp8_fused = torch.empty_like(q, dtype=torch.float8_e4m3fn)
674 k_cache_ref = torch.zeros(
675 + num_blocks, block_size, HEAD_DIM, dtype=FP8_STORE_DTYPE, device=device
676 + )
677 + k_cache_fused = torch.zeros(
678 num_blocks, block_size, HEAD_DIM, dtype=torch.float8_e4m3fn, device=device
679 )
573 - k_cache_fused = torch.zeros_like(k_cache_ref)
680
681 _fp8_full_cache_reference(
682 q,
@@ -599,12 +705,29 @@ def test_full_cache_per_tensor_fp8_matches_reference(
705 block_size,
706 )
707
708 + # Q is RMSNorm(no-weight)+RoPE in fp32 before fp8 quant; the RMSNorm
709 + # reduction and RoPE rotation can land the kernel and the torch reference on
710 + # opposite sides of an fp8 round-to-nearest tie, so allow <=1 fp8 ULP.
711 + q_fused = _as_stored_fp8(q_fp8_fused)
712 + q_max_ulp = int(_fp8_ulp_distance(q_fused, q_fp8_ref).max().item())
713 + assert q_max_ulp <= 1, f"Q fp8 differs by {q_max_ulp} ULP (>1)"
714 +
715 + # K-cache NoPE region [0, NOPE_DIM) is a deterministic per-tensor fp8 quant
716 + # of the (un-rotated) KV input, so it must be bit-identical. The RoPE region
717 + # [NOPE_DIM, HEAD_DIM) is rotated in fp32 and may differ by <=1 fp8 ULP.
718 + k_fused = _as_stored_fp8(k_cache_fused)
719 torch.testing.assert_close(
603 - q_fp8_fused.float(), q_fp8_ref.float(), rtol=0, atol=0.25
720 + k_fused[..., :NOPE_DIM].float(),
721 + k_cache_ref[..., :NOPE_DIM].float(),
722 + rtol=0,
723 + atol=0,
724 )
605 - torch.testing.assert_close(
606 - k_cache_fused.float(), k_cache_ref.float(), rtol=0, atol=0.25
725 + k_max_ulp = int(
726 + _fp8_ulp_distance(k_fused[..., NOPE_DIM:], k_cache_ref[..., NOPE_DIM:])
727 + .max()
728 + .item()
729 )
730 + assert k_max_ulp <= 1, f"K-cache RoPE fp8 differs by {k_max_ulp} ULP (>1)"
731
732
733 @pytest.mark.skipif(
vllm/model_executor/layers/quantization/utils/fp8_utils.py
+22 -3
@@ -1363,9 +1363,28 @@ def process_fp8_weight_block_strategy(
1363 )
1364
1365 if current_platform.is_fp8_fnuz() and weight.dtype == torch.float8_e4m3fn:
1366 - weight, weight_scale, _ = normalize_e4m3fn_to_e4m3fnuz(
1367 - weight=weight, weight_scale=weight_scale
1368 - )
1366 + if weight_scale.dtype == torch.float8_e8m0fnu:
1367 + # UE8M0 scales: e8m0 stores exponent-only values (2^(exp-127)),
1368 + # so doubling the dequant scale == incrementing the exponent byte
1369 + # by 1. Convert the OCP E4M3 weight bytes to FNUZ in place by
1370 + # reinterpreting and patching the NaN sentinel (-128 in int8),
1371 + # then double the UE8M0 exponent so the dequantized magnitudes
1372 + # match.
1373 + weight_as_int8 = weight.view(torch.int8)
1374 + ROCM_FP8_NAN_AS_INT = -128
1375 + weight_as_int8[weight_as_int8 == ROCM_FP8_NAN_AS_INT] = 0
1376 + weight = weight_as_int8.view(torch.float8_e4m3fnuz)
1377 + exp_bytes = weight_scale.view(torch.uint8)
1378 + weight_scale = (
1379 + (exp_bytes.to(torch.int16) + 1)
1380 + .clamp(max=254)
1381 + .to(torch.uint8)
1382 + .view(torch.float8_e8m0fnu)
1383 + )
1384 + else:
1385 + weight, weight_scale, _ = normalize_e4m3fn_to_e4m3fnuz(
1386 + weight=weight, weight_scale=weight_scale
1387 + )
1388
1389 weight = _maybe_pad_fp8_weight(weight)
1390 return weight, weight_scale
vllm/models/deepseek_v4/amd/rocm.py
+4
@@ -14,6 +14,7 @@ from vllm.models.deepseek_v4.sparse_mla import (
14 DeepseekV4FlashMLAMetadata,
15 DeepseekV4FlashMLAMetadataBuilder,
16 )
17 +from vllm.platforms import current_platform
18 from vllm.triton_utils import tl, triton
19 from vllm.v1.attention.backend import (
20 CommonAttentionMetadata,
@@ -796,6 +797,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
797 assert attn_metadata is not None
798 assert compressed_k_cache is not None
799 block_table = attn_metadata.block_table[num_decodes:]
800 + # compressed_k_cache is OCP on every platform (Triton encoder).
801 dequantize_and_gather_k_cache(
802 kv[:chunk_size],
803 compressed_k_cache,
@@ -804,6 +806,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
806 block_table=block_table[chunk_start:chunk_end],
807 block_size=attn_metadata.block_size // self.compress_ratio,
808 offset=0,
809 + use_fnuz=False,
810 )
811
812 swa_block_table = swa_metadata.block_table[num_decodes:]
@@ -815,6 +818,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
818 block_table=swa_block_table[chunk_start:chunk_end],
819 block_size=swa_metadata.block_size,
820 offset=N,
821 + use_fnuz=current_platform.is_fp8_fnuz(),
822 )
823
824 query_start = (
vllm/models/deepseek_v4/common/ops/cache_utils.py
+49 -6
@@ -16,6 +16,10 @@ preparation.
16
17 import torch
18
19 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
20 + get_fp8_min_max,
21 +)
22 +from vllm.platforms import current_platform
23 from vllm.triton_utils import tl, triton
24 from vllm.utils.import_utils import has_cutedsl
25
@@ -39,6 +43,7 @@ def quantize_and_insert_k_kernel(
43 block_stride: tl.constexpr, # total bytes per block (padded)
44 fp8_max: tl.constexpr,
45 n_quant_blocks: tl.constexpr, # 8 (7 real + 1 padding)
46 + use_fnuz: tl.constexpr = False,
47 ):
48 """
49 Quantize K tensor and insert into paged K cache.
@@ -49,6 +54,9 @@ def quantize_and_insert_k_kernel(
54 - [64*576 + 64*8, block_stride): Padding
55
56 One program per token.
57 +
58 + ``use_fnuz=True`` selects FNUZ (``tl.float8e4b8``); default OCP
59 + (``tl.float8e4nv``) matches every production caller.
60 """
61 pid = tl.program_id(0)
62
@@ -112,8 +120,11 @@ def quantize_and_insert_k_kernel(
120 x_scaled = x / scale
121 x_clamped = tl.clamp(x_scaled, -fp8_max, fp8_max)
122
115 - # Convert to fp8, then bitcast to uint8 for storage
116 - x_fp8 = x_clamped.to(tl.float8e4nv)
123 + # Convert to fp8 (FNUZ on gfx942, OCP elsewhere), then bitcast to uint8.
124 + if use_fnuz:
125 + x_fp8 = x_clamped.to(tl.float8e4b8)
126 + else:
127 + x_fp8 = x_clamped.to(tl.float8e4nv)
128 x_uint8 = x_fp8.to(tl.uint8, bitcast=True)
129
130 # Store as uint8 (1 byte each)
@@ -145,6 +156,7 @@ def quantize_and_insert_k_cache(
156 slot_mapping: torch.Tensor, # [num_tokens] int64
157 block_size: int = 64,
158 is_ue8m0: bool = True,
159 + use_fnuz: bool = False,
160 ):
161 """
162 Quantize K tensor and insert into paged K cache.
@@ -155,6 +167,10 @@ def quantize_and_insert_k_cache(
167 - Next 64 * 8 = 512 bytes: Scales
168 - Each token: 8 bytes (uint8 scales, 7 real + 1 padding)
169 - Padded to multiple of 576
170 +
171 + ``use_fnuz=True`` selects FNUZ E4M3 cache encoding and is only valid on
172 + platforms whose FP8 format is FNUZ. ``use_fnuz=False`` selects OCP E4M3,
173 + which is used by OCP-encoded caches even on gfx942.
174 """
175 assert k.dim() == 2 and k.shape[1] == 512, (
176 f"K must be [num_tokens, 512], got {k.shape}"
@@ -171,7 +187,12 @@ def quantize_and_insert_k_cache(
187 TOKEN_BF16_DIM = 64
188 TOKEN_SCALE_DIM = 8
189 QUANT_BLOCK_SIZE = 64
174 - FP8_MAX = 448.0
190 + if use_fnuz:
191 + if not current_platform.is_fp8_fnuz():
192 + raise ValueError("use_fnuz=True requires a platform using FNUZ FP8")
193 + _, FP8_MAX = get_fp8_min_max()
194 + else:
195 + FP8_MAX = torch.finfo(torch.float8_e4m3fn).max
196 TOKEN_DATA_SIZE = TOKEN_FP8_DIM + TOKEN_BF16_DIM * 2
197
198 grid = (num_tokens,)
@@ -191,6 +212,7 @@ def quantize_and_insert_k_cache(
212 block_stride=block_stride,
213 fp8_max=FP8_MAX,
214 n_quant_blocks=8,
215 + use_fnuz=use_fnuz,
216 )
217
218
@@ -216,6 +238,7 @@ def _dequantize_and_gather_k_kernel(
238 output_dim: tl.constexpr, # 512
239 fp8_max: tl.constexpr,
240 n_quant_blocks: tl.constexpr, # 7 real blocks
241 + use_fnuz: tl.constexpr = False,
242 ):
243 batch_idx = tl.program_id(0)
244 worker_id = tl.program_id(1)
@@ -273,8 +296,11 @@ def _dequantize_and_gather_k_kernel(
296 # Load quantized fp8 values (stored as uint8)
297 x_uint8 = tl.load(token_fp8_ptr + offsets, mask=mask, other=0)
298
276 - # Bitcast uint8 back to fp8
277 - x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
299 + # Bitcast uint8 back to fp8 (FNUZ on gfx942, OCP elsewhere).
300 + if use_fnuz:
301 + x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True)
302 + else:
303 + x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
304
305 # Convert fp8 to float32 for computation
306 x_float = x_fp8.to(tl.float32)
@@ -317,6 +343,7 @@ def dequantize_and_gather_k_cache_triton(
343 block_table: torch.Tensor,
344 block_size: int,
345 offset: int,
346 + use_fnuz: bool = False,
347 ) -> None:
348 TOKEN_FP8_DIM = 448
349 TOKEN_BF16_DIM = 64
@@ -347,6 +374,7 @@ def dequantize_and_gather_k_cache_triton(
374 output_dim=512,
375 fp8_max=FP8_MAX,
376 n_quant_blocks=7,
377 + use_fnuz=use_fnuz,
378 )
379
380
@@ -363,7 +391,15 @@ def dequantize_and_gather_k_cache(
391 block_table: torch.Tensor,
392 block_size: int,
393 offset: int,
394 + use_fnuz: bool = False,
395 ) -> None:
396 + """Dequantize and gather a paged DSv4 K cache.
397 +
398 + ``use_fnuz`` MUST match the encoder of the specific cache being read:
399 + ``False`` for ``compressed_k_cache`` (Triton encoder is OCP everywhere),
400 + ``current_platform.is_fp8_fnuz()`` for ``swa_k_cache`` (C++ encoder
401 + writes FNUZ on gfx942 and OCP on gfx950).
402 + """
403 if has_cutedsl():
404 # lazily import, otherwise some tests fail due to CUDA driver init failure.
405 from vllm.models.deepseek_v4.nvidia.ops.dequant_gather_k_cutedsl import (
@@ -376,7 +412,14 @@ def dequantize_and_gather_k_cache(
412 return
413
414 dequantize_and_gather_k_cache_triton(
379 - out, k_cache, seq_lens, gather_lens, block_table, block_size, offset
415 + out,
416 + k_cache,
417 + seq_lens,
418 + gather_lens,
419 + block_table,
420 + block_size,
421 + offset,
422 + use_fnuz=use_fnuz,
423 )
424
425
vllm/models/deepseek_v4/nvidia/ops/o_proj.py
+3 -1
@@ -3,7 +3,9 @@
3 import torch
4 import torch.nn as nn
5
6 -from vllm.models.deepseek_v4.common.ops import fused_inv_rope_fp8_quant
6 +from vllm.models.deepseek_v4.common.ops.fused_inv_rope_fp8_quant import (
7 + fused_inv_rope_fp8_quant,
8 +)
9 from vllm.platforms import current_platform
10 from vllm.utils.deep_gemm import fp8_einsum
11
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+47 -14
@@ -504,7 +504,13 @@ def fp8_mqa_logits_torch(
504 )
505 mask = mask_lo & mask_hi
506
507 - score = torch.einsum("mhd,nd->hmn", q, k).float() * scale
507 + # ``score`` is [H, M, N]; ``scale`` is the per-KV-token scale, which
508 + # vLLM callers hand us as ``[N, 1]`` (a ``[N, 4]`` uint8 buffer cast
509 + # to fp32). PyTorch right-aligns dimensions for broadcasting, so a
510 + # naked ``score * scale`` would align ``scale``'s leading dim with
511 + # ``score``'s M dim and raise a shape mismatch. Flatten to ``[N]`` so
512 + # broadcasting lines up with the last dim of ``score``.
513 + score = torch.einsum("mhd,nd->hmn", q, k).float() * scale.reshape(-1)
514 logits = (score.relu() * weights.unsqueeze(-1).transpose(0, 1)).sum(dim=0)
515 logits = logits.masked_fill(~mask, float("-inf"))
516
@@ -557,13 +563,26 @@ def rocm_fp8_mqa_logits(
563 # path after aiter merge this kernel into main
564 from vllm._aiter_ops import rocm_aiter_ops
565
566 + k_fp8, scale = kv
567 +
568 + # Temporarily route gfx942 to the vendored ROCm/aiter#3257 workaround.
569 + # Remove this branch once vLLM bumps AITER to a version that includes
570 + # ROCm/aiter#3257.
571 + if _ON_GFX942 and rocm_aiter_ops.is_enabled():
572 + from vllm.v1.attention.ops.triton_fp8_mqa_logits import (
573 + fp8_mqa_logits_gfx942,
574 + )
575 +
576 + return fp8_mqa_logits_gfx942(
577 + q, k_fp8, scale, weights, cu_seqlen_ks, cu_seqlen_ke
578 + )
579 +
580 aiter_mqa_logits_module = None
581 if rocm_aiter_ops.is_enabled():
582 aiter_mqa_logits_module = mqa_logits_module()
583
584 if aiter_mqa_logits_module is not None:
585 fp8_mqa_logits = aiter_mqa_logits_module.fp8_mqa_logits
566 - k_fp8, scale = kv
586 return fp8_mqa_logits(q, k_fp8, scale, weights, cu_seqlen_ks, cu_seqlen_ke)
587 else:
588 return fp8_mqa_logits_torch(q, kv, weights, cu_seqlen_ks, cu_seqlen_ke)
@@ -1249,7 +1268,10 @@ def _sparse_attn_decode_ragged_kernel(
1268 NOPE_DIM: tl.constexpr,
1269 NOPE_BLOCK: tl.constexpr,
1270 ROPE_DIM: tl.constexpr,
1252 - IS_FNUZ: tl.constexpr,
1271 + # SWA K-cache (main): C++ encoder writes FNUZ on gfx942, OCP on gfx950.
1272 + # Compressed K-cache (extra): Triton encoder writes OCP everywhere.
1273 + IS_FNUZ_MAIN: tl.constexpr,
1274 + IS_FNUZ_EXTRA: tl.constexpr,
1275 BLOCK_H: tl.constexpr,
1276 BLOCK_K: tl.constexpr,
1277 ):
@@ -1306,8 +1328,8 @@ def _sparse_attn_decode_ragged_kernel(
1328 mask=valid[:, None] & nope_mask[None, :],
1329 other=0,
1330 )
1309 - if IS_FNUZ:
1310 - x_fp8 = x_uint8.to(tl.float8e4b15, bitcast=True)
1331 + if IS_FNUZ_MAIN:
1332 + x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True)
1333 else:
1334 x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
1335 encoded_scales = tl.load(
@@ -1374,8 +1396,8 @@ def _sparse_attn_decode_ragged_kernel(
1396 mask=valid[:, None] & nope_mask[None, :],
1397 other=0,
1398 )
1377 - if IS_FNUZ:
1378 - x_fp8 = x_uint8.to(tl.float8e4b15, bitcast=True)
1399 + if IS_FNUZ_EXTRA:
1400 + x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True)
1401 else:
1402 x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
1403 encoded_scales = tl.load(
@@ -1485,7 +1507,12 @@ def _sparse_attn_decode_partial_kernel(
1507 NOPE_DIM: tl.constexpr,
1508 NOPE_BLOCK: tl.constexpr,
1509 ROPE_DIM: tl.constexpr,
1488 - IS_FNUZ: tl.constexpr,
1510 + # `main_cache` is the SWA K-cache (written by the C++ encoder, FNUZ on
1511 + # gfx942 / OCP on gfx950). `extra_cache` is the compressed K-cache
1512 + # (Triton encoder, OCP on every platform). Reading both with the same
1513 + # `IS_FNUZ` would decode one of them with the wrong FNUZ/OCP scale ratio.
1514 + IS_FNUZ_MAIN: tl.constexpr,
1515 + IS_FNUZ_EXTRA: tl.constexpr,
1516 BLOCK_H: tl.constexpr,
1517 BLOCK_K: tl.constexpr,
1518 NUM_SPLITS: tl.constexpr,
@@ -1551,8 +1578,8 @@ def _sparse_attn_decode_partial_kernel(
1578 mask=valid[:, None] & nope_mask[None, :],
1579 other=0,
1580 )
1554 - if IS_FNUZ:
1555 - x_fp8 = x_uint8.to(tl.float8e4b15, bitcast=True)
1581 + if IS_FNUZ_MAIN:
1582 + x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True)
1583 else:
1584 x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
1585 encoded_scales = tl.load(
@@ -1622,8 +1649,8 @@ def _sparse_attn_decode_partial_kernel(
1649 mask=valid[:, None] & nope_mask[None, :],
1650 other=0,
1651 )
1625 - if IS_FNUZ:
1626 - x_fp8 = x_uint8.to(tl.float8e4b15, bitcast=True)
1652 + if IS_FNUZ_EXTRA:
1653 + x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True)
1654 else:
1655 x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
1656 encoded_scales = tl.load(
@@ -2095,7 +2122,8 @@ def _rocm_sparse_attn_decode_ragged_triton(
2122 NOPE_DIM=nope_head_dim,
2123 NOPE_BLOCK=nope_block,
2124 ROPE_DIM=rope_head_dim,
2098 - IS_FNUZ=is_fnuz,
2125 + IS_FNUZ_MAIN=is_fnuz,
2126 + IS_FNUZ_EXTRA=False,
2127 BLOCK_H=block_h,
2128 BLOCK_K=block_k,
2129 num_warps=8,
@@ -2153,7 +2181,12 @@ def _rocm_sparse_attn_decode_ragged_triton(
2181 NOPE_DIM=nope_head_dim,
2182 NOPE_BLOCK=nope_block,
2183 ROPE_DIM=rope_head_dim,
2156 - IS_FNUZ=is_fnuz,
2184 + # main_cache = swa_k_cache (C++ encoder, FNUZ on gfx942 / OCP on gfx950).
2185 + # extra_cache = compressed kv_cache (Triton encoder, OCP everywhere).
2186 + # Reading both with a single IS_FNUZ would decode one of them with the
2187 + # wrong FNUZ/OCP scale ratio (~1.87×).
2188 + IS_FNUZ_MAIN=is_fnuz,
2189 + IS_FNUZ_EXTRA=False,
2190 BLOCK_H=block_h,
2191 BLOCK_K=block_k,
2192 NUM_SPLITS=num_splits,
vllm/v1/attention/ops/triton_fp8_mqa_logits.py new
+262
@@ -0,0 +1,262 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Temporary gfx942 fallback for AITER's fp8_mqa_logits kernel.
4 +
5 +This module vendors AITER's Triton fp8_mqa_logits kernel with the gfx942
6 +tile-size workaround from ROCm/aiter#3257. It is used only while vLLM's
7 +pinned AITER version lacks that fix.
8 +
9 +TODO: Remove this vendored copy once vLLM pins an AITER version that includes
10 +ROCm/aiter#3257 bugfix for gfx942.
11 +"""
12 +
13 +import torch
14 +
15 +from vllm.triton_utils import tl, triton
16 +
17 +# gfx942 (MI300X) has 64 KiB of LDS per CU. We accept the default
18 +# (BLOCK_KV=128, num_stages=2) tile only when *both* of these hold:
19 +#
20 +# 1. Occupancy gate. With waves_per_eu=2 and num_warps=4 we target two
21 +# workgroups co-resident on a CU -> per-WG LDS budget = 32 KiB. Triton
22 +# keeps Q in registers (loop-invariant) and the fp32 scores accumulator
23 +# in VGPRs (heavy VALU), so only the double-buffered KV tile is
24 +# expected to live in LDS. A 0.9 safety factor leaves headroom for any
25 +# LDS overhead the compiler may add.
26 +#
27 +# 2. Hardware ceiling. Defensive upper bound that also counts Q and
28 +# scores against the 64 KiB CU limit, in case a Triton version (older
29 +# or future) decides to spill them to LDS. False positives here only
30 +# shrink the tile; false negatives are JIT-aborts, so we lean
31 +# conservative.
32 +_GFX942_CU_LDS_BYTES = 64 * 1024
33 +_GFX942_PER_WG_LDS_BUDGET_BYTES = _GFX942_CU_LDS_BYTES * 9 // 20 # ~28.8 KiB
34 +
35 +
36 +def _gfx942_default_tile_fits_lds(num_heads: int, head_size: int) -> bool:
37 + """Return True iff (BLOCK_KV=128, num_stages=2) fits in MI300X LDS."""
38 + BLOCK_KV = 128
39 + NUM_STAGES = 2
40 + kv_bytes = head_size * BLOCK_KV * NUM_STAGES
41 + scores_bytes = num_heads * BLOCK_KV * 4
42 + q_bytes = num_heads * head_size
43 + fits_occupancy = kv_bytes < _GFX942_PER_WG_LDS_BUDGET_BYTES
44 + fits_hardware = q_bytes + kv_bytes + scores_bytes <= _GFX942_CU_LDS_BYTES
45 + return fits_occupancy and fits_hardware
46 +
47 +
48 +@triton.jit
49 +def _fp8_mqa_logits_kernel(
50 + Q_ptr, # fp8e4m3 [seq_len, H, D]
51 + KV_ptr, # fp8e4m3 [seq_len_kv, D]
52 + kv_scales_ptr, # fp32 [seq_len_kv]
53 + weights_ptr, # fp32 [seq_len, H]
54 + cu_start_ptr, # int32 [seq_len]
55 + cu_end_ptr, # int32 [seq_len]
56 + logits_ptr, # fp32 [seq_len, seq_len_kv]
57 + seq_len,
58 + seq_len_kv,
59 + NUM_HEADS: tl.constexpr,
60 + HEAD_SIZE: tl.constexpr,
61 + # strides
62 + stride_q_s: tl.int64,
63 + stride_q_h: tl.constexpr,
64 + stride_q_d: tl.constexpr,
65 + stride_kv_s: tl.int64,
66 + stride_kv_d: tl.constexpr,
67 + stride_w_s: tl.int64,
68 + stride_w_h: tl.constexpr,
69 + stride_logits_s: tl.int64,
70 + stride_logits_k: tl.int64,
71 + # block sizes
72 + BLOCK_KV: tl.constexpr,
73 +):
74 + row_id = tl.program_id(0)
75 + # go from larger to smaller in terms of work
76 + # to reduce the tail effect
77 + row_id = tl.num_programs(0) - row_id - 1
78 + tl.assume(row_id >= 0)
79 + tl.assume(stride_q_s > 0)
80 + tl.assume(stride_q_h > 0)
81 + tl.assume(stride_q_d > 0)
82 + tl.assume(stride_kv_s > 0)
83 + tl.assume(stride_kv_d > 0)
84 + tl.assume(stride_w_s > 0)
85 + tl.assume(stride_w_h > 0)
86 +
87 + logits_row_ptrs = logits_ptr + row_id * stride_logits_s
88 +
89 + h_inds = tl.arange(0, NUM_HEADS)[:, None]
90 + d_inds = tl.arange(0, HEAD_SIZE)
91 +
92 + # load Q[BLOCK_Q, NUM_HEADS, HEAD_SIZE]
93 + q_ptrs = (
94 + Q_ptr + row_id * stride_q_s + h_inds * stride_q_h + d_inds[None, :] * stride_q_d
95 + )
96 +
97 + q_block = tl.load(q_ptrs, cache_modifier=".cg")
98 + w_ptrs = weights_ptr + row_id * stride_w_s + h_inds * stride_w_h
99 + w_block = tl.load(w_ptrs, cache_modifier=".cg").to(tl.float32)
100 +
101 + # Load start/end for each row in this block
102 + start_ind = tl.load(cu_start_ptr + row_id)
103 + end_ind = tl.load(cu_end_ptr + row_id)
104 +
105 + start_ind = tl.maximum(start_ind, 0)
106 + end_ind = tl.minimum(end_ind, seq_len_kv)
107 + shifted_end = end_ind - start_ind
108 + shifted_unmasked_end = shifted_end // BLOCK_KV * BLOCK_KV
109 +
110 + kv_col_offsets = tl.arange(0, BLOCK_KV) + start_ind
111 + kv_ptrs = (
112 + KV_ptr + kv_col_offsets[None, :] * stride_kv_s + d_inds[:, None] * stride_kv_d
113 + )
114 +
115 + kv_scales_ptrs = kv_scales_ptr + kv_col_offsets
116 +
117 + logits_ptrs = logits_row_ptrs + kv_col_offsets * stride_logits_k
118 +
119 + # Loop over KV tiles
120 + for _ in tl.range(0, shifted_unmasked_end, BLOCK_KV):
121 + kv_block = tl.load(kv_ptrs)
122 + kv_scales = tl.load(kv_scales_ptrs)
123 +
124 + # [NUM_HEADS, BLOCK_KV] = [NUM_HEADS, HEAD_SIZE] x [HEAD_SIZE, BLOCK_KV]
125 + scores = tl.dot(q_block, kv_block, input_precision="ieee")
126 + # Multiply by kv_scales (broadcast along rows)
127 + scores = scores * kv_scales[None, :]
128 + # ReLU
129 + scores = tl.maximum(scores, 0.0)
130 + scores = scores * w_block
131 + # [NUM_HEADS, BLOCK_KV] -> [BLOCK_KV, ]
132 + scores = tl.sum(scores, axis=0)
133 + tl.store(logits_ptrs, scores)
134 +
135 + kv_ptrs += BLOCK_KV * stride_kv_s
136 + kv_scales_ptrs += BLOCK_KV
137 + logits_ptrs += BLOCK_KV * stride_logits_k
138 + kv_col_offsets += BLOCK_KV
139 +
140 + # masked load
141 + kv_col_mask = kv_col_offsets < end_ind
142 + kv_block = tl.load(kv_ptrs, mask=kv_col_mask[None, :], other=0.0)
143 + kv_scales = tl.load(kv_scales_ptrs, mask=kv_col_mask, other=0.0)
144 +
145 + # [NUM_HEADS, BLOCK_KV] = [NUM_HEADS, HEAD_SIZE] x [HEAD_SIZE, BLOCK_KV]
146 + scores = tl.dot(q_block, kv_block, input_precision="ieee")
147 + # Multiply by kv_scales (broadcast along rows)
148 + scores = scores * kv_scales[None, :]
149 + # ReLU
150 + scores = tl.maximum(scores, 0.0)
151 + scores = scores * w_block
152 + # [NUM_HEADS, BLOCK_KV] -> [BLOCK_KV, ]
153 + scores = tl.sum(scores, axis=0)
154 + # masked store
155 + in_window = (kv_col_offsets >= start_ind) & (kv_col_offsets < end_ind)
156 + tl.store(logits_ptrs, scores, mask=in_window)
157 +
158 +
159 +def fp8_mqa_logits_gfx942(
160 + q: torch.Tensor,
161 + k_fp8: torch.Tensor,
162 + kv_scales: torch.Tensor,
163 + weights: torch.Tensor,
164 + cu_starts: torch.Tensor,
165 + cu_ends: torch.Tensor,
166 +) -> torch.Tensor:
167 + """Compute FP8 MQA logits on MI300X (gfx942) using the vendored kernel.
168 +
169 + Drop-in replacement for ``aiter.ops.triton.attention.fp8_mqa_logits.
170 + fp8_mqa_logits`` on MI300X. Selects ``(BLOCK_KV, num_stages)`` based on
171 + whether the default tile fits within the 64 KiB LDS budget of a gfx942
172 + CU (see module docstring).
173 +
174 + Args:
175 + q: Query tensor of shape ``[M, H, D]``, FP8 dtype.
176 + k_fp8: Key tensor of shape ``[N, D]``, FP8 dtype.
177 + kv_scales: K scales of shape ``[N]`` (or ``[N, 1]`` -- viewed as
178 + ``[N]``), float32.
179 + weights: Per-head weights of shape ``[M, H]``, float32.
180 + cu_starts: Start indices (inclusive) of shape ``[M]``, int32.
181 + cu_ends: End indices (exclusive) of shape ``[M]``, int32.
182 +
183 + Returns:
184 + Logits of shape ``[M, N]``, float32 -- positions outside
185 + ``[cu_starts[i], cu_ends[i])`` for row ``i`` are pre-filled with
186 + ``-inf`` so the caller can run a top-k without masking.
187 + """
188 + seq_len, num_heads, head_size = q.shape
189 + seq_len_kv = k_fp8.shape[0]
190 + assert num_heads & (num_heads - 1) == 0, (
191 + f"num_heads must be a power of two (got {num_heads})"
192 + )
193 + assert head_size & (head_size - 1) == 0, (
194 + f"head_size must be a power of two (got {head_size})"
195 + )
196 +
197 + # The kernel walks ``kv_scales`` as a 1-D contiguous array of size N
198 + # (it indexes by ``kv_scales_ptr + kv_col_offsets``). The vLLM caller
199 + # passes a ``[N, 4]`` uint8 view-cast-to-float32 which lands as
200 + # ``[N, 1]`` contiguous -- byte-identical to ``[N]`` -- but flatten
201 + # explicitly to keep the kernel's pointer arithmetic intent clear.
202 + kv_scales_1d = kv_scales.reshape(-1)
203 +
204 + # Initialise with -inf so positions outside [cu_starts, cu_ends) read
205 + # as ``-inf`` after the masked store path -- this matches AITER's
206 + # ``fp8_mqa_logits`` semantics and is what the top-k consumer expects.
207 + logits = torch.full(
208 + (seq_len, seq_len_kv),
209 + fill_value=-float("inf"),
210 + dtype=torch.float32,
211 + device=q.device,
212 + )
213 +
214 + if _gfx942_default_tile_fits_lds(num_heads, head_size):
215 + block_kv = 128
216 + num_stages = 2
217 + else:
218 + # DSv4 sparse indexer (NUM_HEADS=64, HEAD_SIZE=128) lands here:
219 + # default tile spills past gfx942's 64 KiB LDS budget. (64, 1)
220 + # needs ~33 KiB and clears the per-WG budget with margin.
221 + block_kv = 64
222 + num_stages = 1
223 +
224 + # heuristic for MFMA instruction shape, identical to AITER's choice
225 + matrix_instr_nonkdim = 32
226 + if seq_len <= 1024:
227 + matrix_instr_nonkdim = 16
228 +
229 + stride_q_s, stride_q_h, stride_q_d = q.stride()
230 + stride_kv_s, stride_kv_d = k_fp8.stride()
231 + stride_w_s, stride_w_h = weights.stride()
232 + stride_logits_s, stride_logits_k = logits.stride()
233 +
234 + _fp8_mqa_logits_kernel[(seq_len,)](
235 + Q_ptr=q,
236 + KV_ptr=k_fp8,
237 + kv_scales_ptr=kv_scales_1d,
238 + weights_ptr=weights,
239 + cu_start_ptr=cu_starts,
240 + cu_end_ptr=cu_ends,
241 + logits_ptr=logits,
242 + seq_len=seq_len,
243 + seq_len_kv=seq_len_kv,
244 + NUM_HEADS=num_heads,
245 + HEAD_SIZE=head_size,
246 + stride_q_s=stride_q_s,
247 + stride_q_h=stride_q_h,
248 + stride_q_d=stride_q_d,
249 + stride_kv_s=stride_kv_s,
250 + stride_kv_d=stride_kv_d,
251 + stride_w_s=stride_w_s,
252 + stride_w_h=stride_w_h,
253 + stride_logits_s=stride_logits_s,
254 + stride_logits_k=stride_logits_k,
255 + BLOCK_KV=block_kv,
256 + num_warps=4,
257 + num_stages=num_stages,
258 + waves_per_eu=2,
259 + matrix_instr_nonkdim=matrix_instr_nonkdim,
260 + )
261 +
262 + return logits