🚀 The feature, motivation and pitch
TRITON_MLA_SPARSE (#47629) is bf16-KV-only, and on 24 GB cards that is the binding constraint. I have a working sm_80/sm_86 fp8 KV path and would like to know whether the project wants it before I polish it into a mergeable PR.
The gap, in the PR's own words
TritonMLASparseBackend (vllm/v1/attention/backends/mla/triton_mla_sparse.py):
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = ["auto", "float16", "bfloat16"]
its decode entry point is literally _forward_bf16_kv, and the base it inherits refuses quantized KV outright (xpu_mla_sparse.py:238):
if is_quantized_kv_cache(self.kv_cache_dtype):
raise NotImplementedError("FP8 kv is not supported with XPU MLA Sparse yet")
The stated reason fp8 is excluded on Ampere is that Triton cannot handle fp8e4nv there. That is true for native fp8 — and irrelevant for fp8 as storage.
The technique is already in this very PR
vllm/v1/attention/ops/mqa_logits_triton.py:191, added by #47629 itself:
# Kernel decodes FP8 from uint8 via LUT (SM80 Triton can't load fp8e4nv).
The indexer already reads fp8 pages on SM80 by loading them as uint8 and decoding in-register. The KV path simply hasn't had the same treatment. (models/deepseek_v4/xpu/xpu_sparse_decode_fp8.py does the same thing for XPU: "dequantize FP8 KV cache pages to BF16 on the fly, then reuse the BF16 sparse MLA attention kernel".) So the precedent is accepted in-tree, twice, and neither conflicts with #43914 / #47060's "native FP8 requires SM89+" — software dequant is a different mechanism.
What I did
An IS_FP8: tl.constexpr branch inside your own _sparse_mla_compute_tile — the single function through which both _sparse_mla_kernel_final and _sparse_mla_kernel_split load KV — so fp8 inherits your split-KV, your merge kernel, your autotune and your masking for free. The standard 656-byte fp8_ds_mla page is loaded as raw uint8 and decoded with bit-math that is bit-exact vs torch.float8_e4m3fn over all 256 byte values. Nothing about the cache layout changes; the existing C++ concat_and_cache_ds_mla writer already runs on sm_86 unmodified (ENABLE_FP8 is CUDA-version-gated, not arch-gated — verified by execution).
Patch (224 lines, applies to bbe2ab4d6, one flag / one branch / one dispatcher arg): https://github.com/nickus/vllm-fp8kv/blob/main/patches/triton_mla_sparse_fp8.patch
Measured on RTX 3090 (sm_86) — including the part that is not good news
|
|
| fp8 dequant, all 256 byte values |
bit-exact vs torch.float8_e4m3fn |
Decode vs fp32 reference, through vLLM's own C++ ds_mla writer and own triton_convert_req_index_to_global_index |
cosine 0.999996 |
| KV pool capacity |
1.756× (656 B vs 1152 B/token); ~1.63× effective once the indexer's own K-cache is counted |
| Decode speed vs bf16, same kernel, bs=8 / topk=2048 |
0.92× |
| Decode speed vs bf16, same kernel, bs=32 / topk=2048 |
0.45× |
So this is a capacity-for-speed trade, not a free win, and I would rather say so up front than have it found in review. At bs=32 fp8 decode is currently ~2.2× slower. My working hypothesis is that the autotune config lists (_FINAL_AUTOTUNE_CONFIGS: BLOCK_N=16 only; _SPLIT_AUTOTUNE_CONFIGS: BLOCK_N=32 only) are tuned for bf16's 1152 B rows and that fp8's 656 B rows plus a scale load want larger blocks — but I have not measured that on the real kernel yet, so I am flagging it as a hypothesis, not a finding. On a 24 GB card the 1.76× capacity may well be worth the decode cost; on an A100 it plainly is not, which is an argument for keeping this opt-in.
What is NOT done
- The patch above is kernel-only and therefore inert on its own: the backend still declares bf16-only dtypes,
get_kv_cache_shape returns a 576-wide row for every dtype (fp8_ds_mla needs 656 — flashmla_sparse.py already handles this), and _canonicalize_sparse_mla_kv_cache_dtype maps fp8_e4m3 -> fp8_ds_mla only for FLASHMLA_SPARSE / SM120. I have that wiring working out-of-tree; it is ~40 more lines.
- No engine-level boot yet. All correctness numbers above are kernel-level. I have not yet run
LLM(..., kv_cache_dtype="fp8_e4m3") end-to-end and compared logits against a bf16 run.
The questions
- Does the project want fp8 KV on the Ampere sparse-MLA path, given it buys ~1.76× KV capacity at a decode cost that is currently significant at larger batch?
- If yes: kernel-only PR first (the diff above, dispatch dormant until the backend declares the dtype), or one PR including the backend wiring?
- Any objection to the fp8-as-storage / software-dequant approach in the shared
_sparse_mla_compute_tile, given mqa_logits_triton.py already does exactly this on SM80?
Happy to do the engine boot + logits parity and the config-list measurement before opening the PR — I just want to know the feature is wanted first.
Unrelated bug found while working on this path, already filed: #48364 / #48366 (fully-masked leading index chunks NaN-poison xpu_mla_sparse).
Alternatives considered
- Native fp8 (
fp8e4nv) on Ampere — impossible; that is the constraint the PR itself documents.
- Dequantize whole pages to bf16 before the kernel — what the XPU path does; costs a full bf16 materialization of the gathered KV, which throws away the bandwidth saving that is the entire point on a 24 GB card.
- Keep bf16 KV and shrink the model instead — what 3090/4090 owners do today; it is why GLM-5.2-class DSA models are effectively unusable at long context on consumer Ampere.
Before submitting a new issue...
🚀 The feature, motivation and pitch
TRITON_MLA_SPARSE(#47629) is bf16-KV-only, and on 24 GB cards that is the binding constraint. I have a working sm_80/sm_86 fp8 KV path and would like to know whether the project wants it before I polish it into a mergeable PR.The gap, in the PR's own words
TritonMLASparseBackend(vllm/v1/attention/backends/mla/triton_mla_sparse.py):its decode entry point is literally
_forward_bf16_kv, and the base it inherits refuses quantized KV outright (xpu_mla_sparse.py:238):The stated reason fp8 is excluded on Ampere is that Triton cannot handle
fp8e4nvthere. That is true for native fp8 — and irrelevant for fp8 as storage.The technique is already in this very PR
vllm/v1/attention/ops/mqa_logits_triton.py:191, added by #47629 itself:# Kernel decodes FP8 from uint8 via LUT (SM80 Triton can't load fp8e4nv).The indexer already reads fp8 pages on SM80 by loading them as
uint8and decoding in-register. The KV path simply hasn't had the same treatment. (models/deepseek_v4/xpu/xpu_sparse_decode_fp8.pydoes the same thing for XPU: "dequantize FP8 KV cache pages to BF16 on the fly, then reuse the BF16 sparse MLA attention kernel".) So the precedent is accepted in-tree, twice, and neither conflicts with #43914 / #47060's "native FP8 requires SM89+" — software dequant is a different mechanism.What I did
An
IS_FP8: tl.constexprbranch inside your own_sparse_mla_compute_tile— the single function through which both_sparse_mla_kernel_finaland_sparse_mla_kernel_splitload KV — so fp8 inherits your split-KV, your merge kernel, your autotune and your masking for free. The standard 656-bytefp8_ds_mlapage is loaded as rawuint8and decoded with bit-math that is bit-exact vstorch.float8_e4m3fnover all 256 byte values. Nothing about the cache layout changes; the existing C++concat_and_cache_ds_mlawriter already runs on sm_86 unmodified (ENABLE_FP8is CUDA-version-gated, not arch-gated — verified by execution).Patch (224 lines, applies to
bbe2ab4d6, one flag / one branch / one dispatcher arg): https://github.com/nickus/vllm-fp8kv/blob/main/patches/triton_mla_sparse_fp8.patchMeasured on RTX 3090 (sm_86) — including the part that is not good news
torch.float8_e4m3fnds_mlawriter and owntriton_convert_req_index_to_global_indexSo this is a capacity-for-speed trade, not a free win, and I would rather say so up front than have it found in review. At bs=32 fp8 decode is currently ~2.2× slower. My working hypothesis is that the autotune config lists (
_FINAL_AUTOTUNE_CONFIGS:BLOCK_N=16only;_SPLIT_AUTOTUNE_CONFIGS:BLOCK_N=32only) are tuned for bf16's 1152 B rows and that fp8's 656 B rows plus a scale load want larger blocks — but I have not measured that on the real kernel yet, so I am flagging it as a hypothesis, not a finding. On a 24 GB card the 1.76× capacity may well be worth the decode cost; on an A100 it plainly is not, which is an argument for keeping this opt-in.What is NOT done
get_kv_cache_shapereturns a 576-wide row for every dtype (fp8_ds_mla needs 656 —flashmla_sparse.pyalready handles this), and_canonicalize_sparse_mla_kv_cache_dtypemapsfp8_e4m3 -> fp8_ds_mlaonly for FLASHMLA_SPARSE / SM120. I have that wiring working out-of-tree; it is ~40 more lines.LLM(..., kv_cache_dtype="fp8_e4m3")end-to-end and compared logits against a bf16 run.The questions
_sparse_mla_compute_tile, givenmqa_logits_triton.pyalready does exactly this on SM80?Happy to do the engine boot + logits parity and the config-list measurement before opening the PR — I just want to know the feature is wanted first.
Unrelated bug found while working on this path, already filed: #48364 / #48366 (fully-masked leading index chunks NaN-poison
xpu_mla_sparse).Alternatives considered
fp8e4nv) on Ampere — impossible; that is the constraint the PR itself documents.Before submitting a new issue...