@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 577df69b26
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 6, 10:45 UTC

[Bugfix] Fix KV scales inconsistency in fp8 MLA & FlashInfer kv_cache_dtype "auto" leading to gibberish (#37054)

Signed-off-by: Andy Lo <andy@mistral.ai>

Andy Lo committed Mar 18, 2026 at 23:07 UTC 577df69b26491aaa8f3fef2ea44d6ac256172032
8 files changed +71 -43
tests/v1/attention/test_mla_backends.py
+31 -28
@@ -266,22 +266,6 @@ def create_and_prepopulate_kv_cache(
266 return kv_cache
267
268
269 -class MockAttentionLayer:
270 - """A mock attention layer for testing."""
271 -
272 - def __init__(self, device: torch.device):
273 - self._q_scale = torch.tensor(1.0, device=device)
274 - self._k_scale = torch.tensor(1.0, device=device)
275 - self._v_scale = torch.tensor(1.0, device=device)
276 - self._prob_scale = torch.tensor(1.0, device=device)
277 - self._q_scale_float = 1.0
278 - self._k_scale_float = 1.0
279 - self._v_scale_float = 1.0
280 -
281 - def forward(self, *_args, **_kwargs):
282 - raise NotImplementedError
283 -
284 -
269 class MockSparseMLAAttentionLayer:
270 """A mock sparse MLA attention layer for testing.
271
@@ -304,6 +288,8 @@ class MockSparseMLAAttentionLayer:
288 device: torch.device,
289 W_UK: torch.Tensor,
290 W_UV: torch.Tensor,
291 + q_scale: float,
292 + k_scale: float,
293 ):
294 self.impl = impl
295 self.num_heads = num_heads
@@ -319,13 +305,13 @@ class MockSparseMLAAttentionLayer:
305 self.W_UV = W_UV.transpose(0, 1)
306
307 # Scale attributes needed by attention backends
322 - self._q_scale = torch.tensor(1.0, device=device)
323 - self._k_scale = torch.tensor(1.0, device=device)
324 - self._v_scale = torch.tensor(1.0, device=device)
308 + self._q_scale = torch.tensor(q_scale, device=device)
309 + self._k_scale = torch.tensor(k_scale, device=device)
310 + self._v_scale = torch.tensor(float("nan"), device=device)
311 self._prob_scale = torch.tensor(1.0, device=device)
326 - self._q_scale_float = 1.0
327 - self._k_scale_float = 1.0
328 - self._v_scale_float = 1.0
312 + self._q_scale_float = q_scale
313 + self._k_scale_float = k_scale
314 + self._v_scale_float = float("nan")
315
316 self._decode_concat_quant_fp8_op = _DecodeConcatQuantFP8(
317 static=True,
@@ -420,6 +406,8 @@ class MockMLAAttentionLayer(AttentionLayerBase):
406 kv_lora_rank: int,
407 device: torch.device,
408 kv_b_proj,
409 + q_scale: float,
410 + k_scale: float,
411 ):
412 self.impl = impl
413 self.num_heads = num_heads
@@ -443,13 +431,13 @@ class MockMLAAttentionLayer(AttentionLayerBase):
431 self.W_UK_T = W_UK.permute(1, 2, 0)
432
433 # Scale attributes needed by attention backends
446 - self._q_scale = torch.tensor(1.0, device=device)
447 - self._k_scale = torch.tensor(1.0, device=device)
448 - self._v_scale = torch.tensor(1.0, device=device)
434 + self._q_scale = torch.tensor(q_scale, device=device)
435 + self._k_scale = torch.tensor(k_scale, device=device)
436 + self._v_scale = torch.tensor(float("nan"), device=device)
437 self._prob_scale = torch.tensor(1.0, device=device)
450 - self._q_scale_float = 1.0
451 - self._k_scale_float = 1.0
452 - self._v_scale_float = 1.0
438 + self._q_scale_float = q_scale
439 + self._k_scale_float = k_scale
440 + self._v_scale_float = float("nan")
441
442 self._decode_concat_quant_fp8_op = _DecodeConcatQuantFP8(
443 static=True,
@@ -568,6 +556,8 @@ def run_attention_backend(
556 qk_rope_head_dim: int,
557 v_head_dim: int,
558 mock_kv_b_proj,
559 + q_scale: float,
560 + k_scale: float,
561 kv_cache_dtype: str = "auto",
562 ) -> torch.Tensor:
563 """Run attention computation using the specified backend's AttentionImpl."""
@@ -625,6 +615,8 @@ def run_attention_backend(
615 kv_lora_rank=kv_lora_rank,
616 device=device,
617 kv_b_proj=mock_kv_b_proj,
618 + q_scale=q_scale,
619 + k_scale=k_scale,
620 )
621
622 # Populate static_forward_context with mock attention layers
@@ -674,6 +666,7 @@ def run_attention_backend(
666 @pytest.mark.parametrize("model", ["deepseek-ai/DeepSeek-R1"])
667 @pytest.mark.parametrize("tensor_parallel_size", [1, 4, 8, 16])
668 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_e4m3"])
669 +@pytest.mark.parametrize(("q_scale", "k_scale"), [(1.0, 1.0), (2.0, 3.0)])
670 def test_backend_correctness(
671 default_vllm_config,
672 dist_init,
@@ -681,6 +674,8 @@ def test_backend_correctness(
674 model: str,
675 tensor_parallel_size: int,
676 kv_cache_dtype: str,
677 + q_scale: float,
678 + k_scale: float,
679 ):
680 """
681 Test that all backends produce similar outputs to a reference implementation
@@ -709,6 +704,11 @@ def test_backend_correctness(
704 for b in BACKENDS_TO_TEST
705 if kv_cache_dtype in b.get_class().supported_kv_cache_dtypes
706 ]
707 + if (
708 + q_scale != 1.0 or k_scale != 1.0
709 + ) and AttentionBackendEnum.CUTLASS_MLA in backends_to_test:
710 + # CUTLASS_MLA does not support non-1 Q/K scales
711 + backends_to_test.remove(AttentionBackendEnum.CUTLASS_MLA)
712 if not backends_to_test:
713 pytest.skip(f"No backends support kv_cache_dtype={kv_cache_dtype}")
714
@@ -1029,6 +1029,7 @@ def test_backend_correctness(
1029 common_attn_metadata=common_attn_metadata,
1030 randomize_blocks=True,
1031 kv_cache_dtype=kv_cache_dtype,
1032 + scale=k_scale,
1033 )
1034 kv_cache_per_block_size[block_size] = kv_cache
1035
@@ -1072,6 +1073,8 @@ def test_backend_correctness(
1073 qk_rope_head_dim,
1074 v_head_dim,
1075 mock_kv_b_proj,
1076 + q_scale=q_scale,
1077 + k_scale=k_scale,
1078 kv_cache_dtype=kv_cache_dtype,
1079 )
1080
tests/v1/attention/test_sparse_mla_backends.py
+9 -2
@@ -178,6 +178,7 @@ def _quantize_dequantize_fp8_ds_mla(
178 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_ds_mla"])
179 @pytest.mark.parametrize("tensor_parallel_size", [1, 2, 4])
180 @pytest.mark.parametrize("block_size", [32, 64])
181 +@pytest.mark.parametrize(("q_scale", "k_scale"), [(1.0, 1.0), (2.0, 3.0)])
182 def test_sparse_backend_decode_correctness(
183 default_vllm_config,
184 dist_init,
@@ -187,6 +188,8 @@ def test_sparse_backend_decode_correctness(
188 tensor_parallel_size,
189 block_size,
190 workspace_init,
191 + q_scale: float,
192 + k_scale: float,
193 ):
194 if kv_cache_dtype not in backend_cls.supported_kv_cache_dtypes:
195 pytest.skip(f"{backend_cls.get_name()} does not support {kv_cache_dtype}")
@@ -332,7 +335,7 @@ def test_sparse_backend_decode_correctness(
335 kv_c_contexts, k_pe_contexts = [], []
336 reference_outputs = []
337
335 - kv_cache_scale = torch.tensor(1.0, dtype=torch.float32, device=device)
338 + kv_cache_scale = torch.tensor(k_scale, dtype=torch.float32, device=device)
339 global_token_idx = 0
340
341 for i in range(batch_spec.batch_size):
@@ -490,6 +493,8 @@ def test_sparse_backend_decode_correctness(
493 device=device,
494 W_UK=W_UK,
495 W_UV=W_UV,
496 + q_scale=q_scale,
497 + k_scale=k_scale,
498 )
499
500 out_buffer = torch.empty(
@@ -513,7 +518,9 @@ def test_sparse_backend_decode_correctness(
518 # FP8 quantization introduces some error, but should be within reasonable bounds
519 # BF16 (auto) should be very accurate, FP8 allows slightly more tolerance
520 if kv_cache_dtype.startswith("fp8"):
516 - torch.testing.assert_close(backend_output, sdpa_reference, rtol=0.05, atol=0.05)
521 + torch.testing.assert_close(
522 + backend_output, sdpa_reference, rtol=0.065, atol=0.05
523 + )
524 else:
525 torch.testing.assert_close(backend_output, sdpa_reference, rtol=0.01, atol=0.01)
526
tests/v1/attention/test_trtllm_attention_integration.py
+6 -6
@@ -43,12 +43,12 @@ class MockAttentionLayer:
43 """Minimal mock of an attention layer for testing."""
44
45 def __init__(self, device: torch.device):
46 - self._q_scale = torch.tensor(1.0, device=device)
47 - self._k_scale = torch.tensor(1.0, device=device)
48 - self._v_scale = torch.tensor(1.0, device=device)
49 - self._q_scale_float = 1.0
50 - self._k_scale_float = 1.0
51 - self._v_scale_float = 1.0
46 + self._q_scale = torch.tensor(2.0, device=device)
47 + self._k_scale = torch.tensor(3.0, device=device)
48 + self._v_scale = torch.tensor(4.0, device=device)
49 + self._q_scale_float = 2.0
50 + self._k_scale_float = 3.0
51 + self._v_scale_float = 4.0
52 self._o_scale_float = None
53
54
vllm/v1/attention/backends/flashinfer.py
+6 -2
@@ -1319,10 +1319,14 @@ class FlashInferImpl(AttentionImpl):
1319 )
1320
1321 if self.bmm1_scale is None:
1322 - self.bmm1_scale = layer._q_scale_float * layer._k_scale_float * self.scale
1322 + self.bmm1_scale = self.scale
1323 + if self.kv_cache_dtype.startswith("fp8"):
1324 + self.bmm1_scale *= layer._q_scale_float * layer._k_scale_float
1325
1326 if self.bmm2_scale is None:
1325 - self.bmm2_scale = layer._v_scale_float
1327 + self.bmm2_scale = 1.0
1328 + if self.kv_cache_dtype.startswith("fp8"):
1329 + self.bmm2_scale *= layer._v_scale_float
1330
1331 prefill_use_trtllm = isinstance(attn_metadata.prefill, TRTLLMPrefill)
1332 decode_use_trtllm = isinstance(attn_metadata.decode, TRTLLMDecode)
vllm/v1/attention/backends/mla/cutlass_mla.py
+5
@@ -255,6 +255,11 @@ class CutlassMLAImpl(MLACommonImpl[MLACommonMetadata]):
255 assert kv_c_and_k_pe_cache.numel() > 0
256 assert attn_metadata.decode is not None
257
258 + if layer._q_scale_float != 1.0 or layer._k_scale_float != 1.0:
259 + raise NotImplementedError(
260 + "CutlassMLAImpl does not support scaling for q and kv_latent yet"
261 + )
262 +
263 if type(q) is tuple:
264 q_nope, q_pe = q
265 else:
vllm/v1/attention/backends/mla/flashinfer_mla.py
+7 -2
@@ -177,9 +177,14 @@ class FlashInferMLAImpl(MLACommonImpl[MLACommonMetadata]):
177 q = q.view(attn_metadata.num_decodes, -1, q.shape[-2], q.shape[-1])
178
179 if self.bmm1_scale is None:
180 - self.bmm1_scale = layer._q_scale_float * layer._k_scale_float * self.scale
180 + self.bmm1_scale = self.scale
181 + if self.kv_cache_dtype.startswith("fp8"):
182 + self.bmm1_scale *= layer._q_scale_float * layer._k_scale_float
183 +
184 if self.bmm2_scale is None:
182 - self.bmm2_scale = layer._v_scale_float
185 + self.bmm2_scale = 1.0
186 + if self.kv_cache_dtype.startswith("fp8"):
187 + self.bmm2_scale *= layer._k_scale_float
188
189 o = trtllm_batch_decode_with_kv_cache_mla(
190 query=q,
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
+6 -2
@@ -340,9 +340,13 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
340 self._workspace_buffer = _get_workspace_buffer(q.device)
341
342 if self.bmm1_scale is None:
343 - self.bmm1_scale = layer._q_scale_float * layer._k_scale_float * self.scale
343 + self.bmm1_scale = self.scale
344 + if self.kv_cache_dtype.startswith("fp8"):
345 + self.bmm1_scale *= layer._q_scale_float * layer._k_scale_float
346 if self.bmm2_scale is None:
345 - self.bmm2_scale = layer._v_scale_float
347 + self.bmm2_scale = 1.0
348 + if self.kv_cache_dtype.startswith("fp8"):
349 + self.bmm2_scale *= layer._k_scale_float
350
351 o = trtllm_batch_decode_with_kv_cache_mla(
352 query=q.unsqueeze(1),
vllm/v1/attention/backends/mla/triton_mla.py
+1 -1
@@ -187,7 +187,7 @@ class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
187 self.scale,
188 PAGE_SIZE,
189 k_scale=layer._k_scale,
190 - v_scale=layer._v_scale,
190 + v_scale=layer._k_scale,
191 )
192
193 return o, lse