@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 379689d533
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 7, 02:45 UTC

[Perf] Support FP8 KV cache for Flashinfer MLA Sparse (#35891)

Wei Zhao committed Mar 7, 2026 at 16:51 UTC 379689d533642cfc1d3ab2cf4dc02f09a8318a5f
8 files changed +89 -17
docs/design/attention_backends.md
+1 -1
@@ -206,7 +206,7 @@ configuration.
206 |---------|--------|-----------|-------------|------------|------|--------|-----------|-----|-----------------|--------------|
207 | `CUTLASS_MLA` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
208 | `FLASHINFER_MLA` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
209 -| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `bfloat16` | 32, 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
209 +| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
210 | `FLASHMLA` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
211 | `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
212 | `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
tests/v1/attention/test_mla_backends.py
+18 -2
@@ -327,6 +327,12 @@ class MockSparseMLAAttentionLayer:
327 self._k_scale_float = 1.0
328 self._v_scale_float = 1.0
329
330 + self._decode_concat_quant_fp8_op = _DecodeConcatQuantFP8(
331 + static=True,
332 + group_shape=GroupShape.PER_TENSOR,
333 + compile_native=True,
334 + )
335 +
336 def forward_impl(
337 self,
338 q: torch.Tensor,
@@ -338,6 +344,7 @@ class MockSparseMLAAttentionLayer:
344 ) -> torch.Tensor:
345 """Forward for sparse MLA - uses forward_mqa for all tokens."""
346 kv_cache_dtype = getattr(self.impl, "kv_cache_dtype", "auto")
347 + fp8_attention = kv_cache_dtype.startswith("fp8")
348
349 # Write to KV cache
350 if kv_cache.numel() > 0:
@@ -350,6 +357,9 @@ class MockSparseMLAAttentionLayer:
357 scale=self._k_scale,
358 )
359
360 + if fp8_attention and kv_cache_dtype != "fp8_ds_mla":
361 + kv_cache = kv_cache.view(current_platform.fp8_dtype())
362 +
363 num_tokens = q.shape[0]
364
365 # Sparse MLA uses forward_mqa for all tokens
@@ -367,8 +377,14 @@ class MockSparseMLAAttentionLayer:
377 # Convert from (N, B, L) to (B, N, L)
378 mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
379
370 - # Pass as tuple to forward_mqa
371 - mqa_q = (mqa_ql_nope, mqa_q_pe)
380 + if fp8_attention and self.impl.supports_quant_query_input:
381 + assert mqa_ql_nope.shape[0] == mqa_q_pe.shape[0]
382 + assert mqa_ql_nope.shape[1] == mqa_q_pe.shape[1]
383 + mqa_q = self._decode_concat_quant_fp8_op(
384 + mqa_ql_nope, mqa_q_pe, self._q_scale
385 + )
386 + else:
387 + mqa_q = (mqa_ql_nope, mqa_q_pe)
388
389 attn_out, _ = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self)
390
tests/v1/attention/test_sparse_mla_backends.py
+11 -1
@@ -191,6 +191,16 @@ def test_sparse_backend_decode_correctness(
191 if kv_cache_dtype not in backend_cls.supported_kv_cache_dtypes:
192 pytest.skip(f"{backend_cls.get_name()} does not support {kv_cache_dtype}")
193
194 + if (
195 + backend_cls == FlashMLASparseBackend
196 + and kv_cache_dtype.startswith("fp8")
197 + and kv_cache_dtype != "fp8_ds_mla"
198 + ):
199 + pytest.skip(
200 + "FlashMLA Sparse Attention backend fp8 only supports "
201 + "fp8_ds_mla kv-cache dtype"
202 + )
203 +
204 supported_block_sizes = backend_cls.get_supported_kernel_block_sizes()
205 if block_size not in supported_block_sizes:
206 pytest.skip(
@@ -419,7 +429,7 @@ def test_sparse_backend_decode_correctness(
429 num_blocks=vllm_config.cache_config.num_gpu_blocks,
430 common_attn_metadata=common_attn_metadata,
431 randomize_blocks=False,
422 - kv_cache_dtype=kv_cache_dtype if use_fp8_ds_mla_quantization else "auto",
432 + kv_cache_dtype=kv_cache_dtype,
433 scale=kv_cache_scale,
434 )
435
tools/pre_commit/generate_attention_backend_docs.py
+15 -1
@@ -49,6 +49,11 @@ MLA_ATTENTION_FILE = (
49 # Backends to skip during doc generation
50 SKIP_BACKENDS = {"CUSTOM", "TORCH_SDPA"}
51
52 +BACKEND_KV_DTYPE_EXCLUDES: dict[str, set[str]] = {
53 + # fp8 is an alias for fp8_ds_mla for FlashMLA Sparse
54 + "FLASHMLA_SPARSE": {"fp8"},
55 +}
56 +
57
58 def is_relevant_file(filepath: str) -> bool:
59 """Check if a file matches any of the relevant patterns."""
@@ -546,10 +551,19 @@ def analyze_backend(backend_name: str, class_path: str) -> dict[str, Any] | None
551 tree, impl_class_name, "can_return_lse_for_decode", False, file_path
552 )
553
554 + kv_cache_dtypes = parse_kv_cache_dtypes(class_node)
555 + if backend_name in BACKEND_KV_DTYPE_EXCLUDES:
556 + excluded = BACKEND_KV_DTYPE_EXCLUDES[backend_name]
557 + kv_cache_dtypes = ", ".join(
558 + d
559 + for d in (d.strip() for d in kv_cache_dtypes.split(","))
560 + if d not in excluded
561 + )
562 +
563 return {
564 "name": backend_name,
565 "dtypes": parse_supported_dtypes(class_node),
552 - "kv_cache_dtypes": parse_kv_cache_dtypes(class_node),
566 + "kv_cache_dtypes": kv_cache_dtypes,
567 "block_sizes": parse_block_sizes(class_node),
568 "head_sizes": parse_head_sizes(class_node),
569 "attn_types": parse_attention_types(class_node),
vllm/model_executor/layers/attention/mla_attention.py
+30 -5
@@ -331,11 +331,6 @@ class MLAAttention(nn.Module, AttentionLayerBase):
331 calculate_kv_scales = False
332 self.quant_config = quant_config
333
334 - # Initialize KV cache quantization attributes
335 - self.kv_cache_dtype = kv_cache_dtype
336 - self.calculate_kv_scales = calculate_kv_scales
337 - _init_kv_cache_quant(self, quant_config, prefix)
338 -
334 dtype = torch.get_default_dtype()
335 self.attn_backend = get_attn_backend(
336 self.head_size,
@@ -347,6 +342,36 @@ class MLAAttention(nn.Module, AttentionLayerBase):
342 num_heads=self.num_heads,
343 )
344
345 + # FlashMLA Sparse Attention fp8 backend uses "fp8_ds_mla" kv-cache format
346 + # Automatically convert fp8 kv-cache format to "fp8_ds_mla"
347 + if (
348 + self.attn_backend.get_name() == "FLASHMLA_SPARSE"
349 + and kv_cache_dtype.startswith("fp8")
350 + and kv_cache_dtype != "fp8_ds_mla"
351 + ):
352 + assert cache_config is not None
353 + cache_config.cache_dtype = "fp8_ds_mla"
354 + kv_cache_dtype = "fp8_ds_mla"
355 + logger.info_once(
356 + "Using DeepSeek's fp8_ds_mla KV cache format. To use standard "
357 + "fp8 kv-cache format, please set `--attention-backend "
358 + "FLASHINFER_MLA_SPARSE`"
359 + )
360 +
361 + if (
362 + self.attn_backend.get_name() == "FLASHINFER_MLA_SPARSE"
363 + and kv_cache_dtype.startswith("fp8")
364 + ):
365 + logger.info_once(
366 + "Using standard fp8 KV cache format. To use DeepSeek's fp8_ds_mla "
367 + "KV cache format, please set `--attention-backend FLASHMLA_SPARSE`"
368 + )
369 +
370 + # Initialize KV cache quantization attributes
371 + self.kv_cache_dtype = kv_cache_dtype
372 + self.calculate_kv_scales = calculate_kv_scales
373 + _init_kv_cache_quant(self, quant_config, prefix)
374 +
375 if (
376 cache_config is not None
377 and cache_config.enable_prefix_caching
vllm/model_executor/models/config.py
-7
@@ -31,20 +31,13 @@ class VerifyAndUpdateConfig:
31 class DeepseekV32ForCausalLM(VerifyAndUpdateConfig):
32 @classmethod
33 def verify_and_update_config(cls, vllm_config: "VllmConfig") -> None:
34 - """
35 - Updated fp8 cache to custom "fp8_ds_mla" format for DeepSeekV32
36 - """
34 hf_config = vllm_config.model_config.hf_config
35
36 # Mirror the check in vllm/model_executor/models/deepseek_v2.py
37 is_v32 = hasattr(hf_config, "index_topk")
38 assert is_v32
39
43 - # For DeepSeekV3.2, a custom fp8 format is used when fp8 kv-cache is enabled.
40 cache_config = vllm_config.cache_config
45 - if cache_config.cache_dtype.startswith("fp8"):
46 - cache_config.cache_dtype = "fp8_ds_mla"
47 - logger.info("Using custom fp8 kv-cache format for DeepSeekV3.2")
41 if cache_config.cache_dtype == "bfloat16":
42 cache_config.cache_dtype = "auto"
43 logger.info("Using bfloat16 kv-cache for DeepSeekV3.2")
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
+7
@@ -63,6 +63,8 @@ class FlashInferMLASparseBackend(AttentionBackend):
63 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
64 "auto",
65 "bfloat16",
66 + "fp8",
67 + "fp8_e4m3",
68 ]
69
70 @staticmethod
@@ -304,6 +306,11 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
306 self.bmm1_scale: float | None = None
307 self.bmm2_scale: float | None = None
308
309 + # fp8 query quantization is required when using fp8 kv_cache,
310 + # as the TRTLLM-GEN sparse MLA kernel requires matching dtypes
311 + # for query and kv_cache (mixed bf16+fp8 is not supported).
312 + self.supports_quant_query_input = True
313 +
314 def forward_mqa(
315 self,
316 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
vllm/v1/attention/backends/mla/flashmla_sparse.py
+7
@@ -83,6 +83,7 @@ class FlashMLASparseBackend(AttentionBackend):
83 "auto",
84 "bfloat16",
85 "fp8_ds_mla",
86 + "fp8", # alias for fp8_ds_mla
87 ]
88
89 @staticmethod
@@ -567,6 +568,12 @@ class FlashMLASparseImpl(SparseMLAAttentionImpl[FlashMLASparseMetadata]):
568 )
569 self.fp8_decode_padded_heads = self._compute_fp8_decode_padded_heads(num_heads)
570
571 + if kv_cache_dtype.startswith("fp8"):
572 + assert kv_cache_dtype == "fp8_ds_mla", (
573 + "FlashMLA Sparse Attention backend fp8 only supports "
574 + "fp8_ds_mla kv-cache dtype"
575 + )
576 +
577 if kv_cache_dtype == "fp8_ds_mla":
578 # Reserve workspace during initialization
579 vllm_config = get_current_vllm_config()