[DSA] Enable tilelang fp8_e4m3 KV cache on CUDA (raw layout) — verified on sm_121 - #36904
beastllama wants to merge 3 commits into
Conversation
The tilelang DSA backend ships a generic-TileLang raw-fp8 sparse kernel that was reachable only on HIP. Route the raw 512 B/token MLA layout on CUDA when both DSA backends are tilelang (SM89+), key the raw fused-quant write on layout instead of platform, and keep the scaled-layout MHA one-shot fast path off for raw fp8. Validated end-to-end on sm_121 (2x DGX Spark GB10, GLM-5.3-Flash NVFP4, TP=2): decode parity with bf16, temp-0 4/5 exact, 32k recall pass, TTFT@16k ~17 percent faster. Addresses sgl-project#36830. Concurrency companion: sgl-project#36889.
…d-within-budget, scrambled negative control)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06a1db7b6f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| set_mla_kv_buffer_triton_fp8_quant( | ||
| dst_buffer, | ||
| loc, |
There was a problem hiding this comment.
Preserve DCP mapping in raw-FP8 cache writes
When CUDA TileLang FP8 is combined with --dcp-size > 1, loc contains widened logical token IDs, but set_mla_kv_buffer_triton_fp8_quant writes directly to dst_buffer[loc] without filtering by DCP ownership or dividing by the DCP world size. The normal MLA writer passes DCP_RANK/DCP_WORLD_SIZE and performs both operations, so this newly enabled branch can write the wrong row or access beyond the rank-local pool before attention runs. Add the same DCP mapping to the fused quant writer or reject this configuration.
Useful? React with 👍 / 👎.
| if _is_hip or is_fp8_kv: | ||
| assert ( | ||
| not return_lse | ||
| ), "tilelang partial+combine sparse fwd does not return LSE" |
There was a problem hiding this comment.
Support or reject DCP before routing CUDA FP8
With --dcp-size > 1, both DSA forward paths request LSE during decode and target verification via _should_return_dsa_dcp_lse, but CUDA FP8 now enters this partial-and-combine branch and immediately asserts that return_lse is false. Consequently, an otherwise accepted CUDA TileLang FP8 configuration aborts on its first DCP decode or verify step. The FP8 path must return LSE for cross-rank reduction, or argument validation must reject TileLang FP8 with DCP.
Useful? React with 👍 / 👎.
…reject tilelang-fp8 + DCP (raw writer lacks the DCP rank filter)
|
Friendly ping for a maintainer with triage rights — CI has never executed on this PR. All 9 red checks are the same fast-failing gate job, not a test failure: Every gate exits in 2–7 s and 71 downstream jobs are Could someone add Review feedback so far is addressed in
Worth noting for whoever reviews: the registered test exercises the fp8 kernel on the H100 CI |
Validated on SM90 (8x H20-3e) — works, ~1.8x KV capacity at identical footprintFollowing up from #36830, where you offered to share early. Your gate is Setup8x NVIDIA H20-3e (sm_90, 140 GB each), single node. Because this PR branches from --tp-size 8 --data-parallel-size 8 --enable-dp-attention --expert-parallel-size 8 \
--quantization fp8 --disable-shared-experts-fusion --moe-runner-backend deep_gemm \
--dsa-prefill-backend tilelang --dsa-decode-backend tilelang \
--context-length 32768 --max-running-requests 256 --mem-fraction-static 0.80 \
--mm-enable-dp-encoder --kv-cache-dtype {bfloat16 | fp8_e4m3}Everything identical between the two arms except The gate admits sm_90 and logs the expected line: Result: same bytes, 1.795x the tokens
1.795x more tokens in the same 32.5 GB. Short of the 2x the layout implies, which is expected: only the 11 full-attention layers carry the halved KV, and the DSA indexer cells appear not to halve with it. The mamba pool is untouched at 211 either way, so the gain is purely the KV side. Output parity at
|
| prompt | bf16 | fp8_e4m3 |
|---|---|---|
| "中国的首都是哪里?只回答城市名。" | 北京 (57 tok) |
北京 (59 tok) |
| "2+3等于几?只回答数字。" | 5 (76 tok) |
5 (76 tok) |
| "9.11和9.8哪个大?只回答数字。" | 9.8 (195 tok) |
9.8 (141 tok) |
3/3 final answers identical. Completion length differs on 2 of 3 — the reasoning trajectory shifts slightly, which is what one would expect from fp8 KV, and it converges on the same answer each time. Worth noting the 9.11-vs-9.8 case, which is a known trip-up for many models: fp8 got there in 141 tokens against bf16's 195.
I have not attempted a broad accuracy suite; this is parity spot-checking, not a GSM8K run.
On your open item — max_running_requests resolving lower than requested
I think this may not be the raw layout. On an earlier run I passed --max-running-requests 8 and the engine resolved 1; that is pool_configurator.py:714:
num_reqs = get_schedule().max_running_requests // kvc.ps.attn_dp_size8 // 8 = 1. The logged value is per DP rank, not global. Raising the flag to 256 in the runs above made it a non-issue, with both arms behaving identically.
There is a second, independent cap on this model worth ruling out before attributing anything to the layout — the mamba state cache:
max_running_requests is capped to N by the mamba state cache
(max_mamba_cache_size=M, R state slots per request)
We measured the per-slot state scaling exactly with attn_tp_size (conv 1.166 → 2.329 → 4.695 MB, ssm 33.24 → 66.54 → 133.4 MB at dp2/dp4/dp8), and --mamba-ssm-dtype bfloat16 roughly doubles that pool with byte-identical output at temperature=0. If the mamba pool is what is binding, that flag moves it and the KV layout does not — an easy way to tell the two apart. (This is also what #36889 is about, I think.)
Summary
sm_90 behaves as your gate assumes. Happy to run anything else that would help this land — a longer prompt set, deeper context, or the boot-gate negative control on Hopper. Full logs available on request.
|
Tested this PR on 2x DGX Spark (GB10, sm_121) over 10GbE TCP, TP=2, serving GLM-5.3-Flash 1. Bug + fix: crashes for models with
|
…5.3-Flash (SM90) Port of sgl-project#36904 (beastllama, closed unmerged) re-based onto main, plus the tilelang return_lse removed by sgl-project#37487's GLM-5.3-Flash decode-CP removal, plus DCP integration for glm-dcp-0907. - tilelang_kernel: dispatch sparse_mla_fwd_decode_partial_fp8 on CUDA (block_I=32/threads=128 tiles sized for the fp8 smem class, SM count via _cuda_sm_count); restore return_lse on sparse_attention_fwd_kernel_v1/v2 (caller-allocated base-2 LSE kernel arg, written only when traced) and extend sparse_mla_fwd_decode_combine with a global base-2 LSE output so the partial+combine fp8 path can also return LSE. - overrides (_check_tilelang_dsa_fp8_kv): allow CUDA + tilelang + fp8_e4m3 (SM89+ required, both DSA backends must be tilelang); DCP allowed since the raw-fp8 writer now applies the DCP owner filter. - kv_cache_configurator (calculate_mla_kv_cache_dim): route the raw 512B/token MLA pool layout on CUDA when both DSA backends are tilelang, keyed on the backend pair instead of the platform. - memory_pool (_write_mla_kv_buffer): key the raw fused-quant write branch on layout (not _is_hip); integrate with our DCP-aware write dispatch — DCP-widened locs go through set_mla_kv_buffer_dcp_sharded_triton_fp8_quant. - mla_buffer: add DCP_RANK/DCP_WORLD_SIZE owner filter to set_mla_kv_buffer_fp8_quant_kernel (mirrors the bf16 kernel) and a set_mla_kv_buffer_dcp_sharded_triton_fp8_quant wrapper. This closes the gap that made sgl-project#36904 refuse tilelang-fp8 + DCP. - dsa_backend: tilelang/tilelang added to the DCP backend-pair whitelist (SM80+, bf16 or raw-fp8 KV; SM89+ belt-and-braces check for fp8); _forward_tilelang gains return_lse with the DCP contract (base-2 LSE, [T,H,D]/[T,H] fp32, fixup_zero_kv_rows for owner-less rows — same contract as flashmla_kv/trtllm); supports_mha_one_shot disabled for the CUDA raw-fp8 tilelang layout (sgl-project#36904). - test: port of sgl-project#36904's CUDA raw-fp8 tilelang probe (one-hot exactness + spread-case fp8 budget with scrambled-index negative control). - test(hicache): register the pre-existing kv_cache_dtype storage-key unit test with CI and fix its bare pytest.main (pre-commit requirement). Provenance: internal deployment branch glm-dcp-0907; refs sgl-project#36904, sgl-project#37487 (removal reverted for tilelang), sgl-project#36989 (c-KV localization pattern).
The tilelang fp8 gate referenced self.dsa_prefill_impl/self.dsa_decode_impl before they were assigned (AttributeError at backend construction, caught at init_attention_backends on SM90), and sat before the unconditional supports_mha_one_shot=True which would have overwritten its False anyway. Move the gate after the impl assignments and after the True default (matches sgl-project#36904's original placement). Caught on ack-h20: Glm5NextForConditionalGeneration, TP4/DCP4, fp8 raw layout pool allocated correctly (1599808 tokens, 17.05 GB/rank) before the attribute-order crash.
GLM-5.3-Flash (NoPE, qk_rope_head_dim=0, kv_lora_rank=512, v_head_dim=256) crashes in sparse_mla_fwd_decode_partial_fp8 during decode CUDA-graph capture on SM90/H20 with 'ValueError: Unsupported k_dim 0': the kernel unconditionally emits q_tail_buf/k_tail_shared shared buffers of width d_tail=0 and a K=0 tail T.gemm, which tilelang's mma generator cannot lower (k_dim = min(256//8, chunk) = 0). The fp8 partial kernel was written for the rope geometry (d_v=512, d_tail=64; GB10/SM121 validation in sgl-project#36904) and the norope case was never exercised. Fix: mirror the existing has_tail specialization-time guard pattern from sparse_attention_fwd_kernel_v1/v2 (python-level 'if has_tail:' around the tail allocs/copies/gemms — d_tail is a plain int at jit specialization time). Applied to both sparse_mla_fwd_decode_partial_fp8 and the bf16 sparse_mla_fwd_decode_partial, which had the same latent hazard on the partial+combine path. sparse_mla_fwd_decode_combine has no tail dims and is unaffected. When d_tail == 0 the attention loop only runs the 4 K=128 fp8 GEMM tiles over dim_quant_fp8 = d_v = 512 channels; d_tail > 0 behavior is unchanged. Crash signature: ack-h20 8xH20, TP4/DCP4, fp8_e4m3 raw layout, decode CUDA-graph capture.
Stacked on #36507; mergeable once that lands. Opening now for visibility per #36830.
Summary
The tilelang DSA backend already ships a complete raw-fp8 sparse attention kernel (
sparse_mla_fwd_decode_partial_fp8— generic TileLang, no HIP intrinsics). It is unreachable on CUDA because three plumbing sites are HIP-gated, and CUDA is always handed the scaled 528 B/token pool layout the kernel cannot parse. This PR routes the raw 512 B/token layout on CUDA when both DSA backends are tilelang, relaxes the arg gate accordingly (SM89+, hard error on mixed backends), and keys the raw fused-quant write on layout rather than platform (strictly more correct on HIP as well).Changes (5 files + test)
srt/arg_groups/overrides.py—_check_tilelang_dsa_fp8_kv: allow CUDA when prefill+decode backends are both tilelang and SM>=89; explicit ValueError on mixed backends; warning log on enablement (serves as a point-of-effect check)srt/mem_cache/kv_cache_configurator.py—calculate_mla_kv_cache_dim: CUDA + both-tilelang -> raw dim -> raw fp8 poolsrt/mem_cache/memory_pool.py—_write_mla_kv_buffer: raw-write branch keyed onnot dsa_kv_cache_store_fp8instead of_is_hip; None-guard forcache_k_rope(NoPE models: GLM-5.3-Flash hasqk_rope_head_dim=0)kernels/ops/attention/dsa/tilelang_kernel.py— dispatch to the fp8 partial kernel on CUDA under the same condition, with fp8-sized tiles for the ~100KB dynamic-smem class. The broader GB10 tile retune is deliberately NOT in this PR (separate arch-conditional proposal per our GLM-5.3-Flash support #36507 comment) — this diff is fp8 routing only.srt/layers/attention/dsa_backend.py—supports_mha_one_shot=Falsefor raw-fp8 (the one-shot fp8 dequant helpers assume the scaled layout)test/registered/attention/test_dsa_tilelang_fp8_kv_cuda.py— one-hot exactness vs gathered reference, spread case within the analytic fp8-prob budget, and a scrambled-index negative control that must fail (guards against a vacuous comparator). Executed on sm_121: 2 passed.Hardware coverage — stated plainly
Validated end-to-end ONLY on sm_121 (2x DGX Spark GB10). The SM89+ gate follows the kernel's own requirements, but SM89/SM90 are untested by us. Review/CI verification on other archs invited.
Validation (2x DGX Spark GB10 sm_121, GLM-5.3-Flash NVFP4, TP=2, DFlash2 drafter bf16)
Credits
Kernel-path analysis and port from a 2x DGX Spark homelab; validation protocol and full experiment ladder: https://github.com/beastllama/GLM-5.3-Flash-DFlash2-SGLang-2x-DGX-Spark
Addresses #36830. Concurrency companion: #36889. Relevant to #36390 (ROCm fp8 accuracy) and the sm_121 bring-up in #36845/#36806.
CI States
Latest PR Test (Base): ❌ Run #33206743796
Latest PR Test (Extra): ❌ Run #33206743584
Latest PR Test (AMD ROCm 7.2): ❌ Run #33206743786