[Perf][DSA] Pass topk_length to flash_mla_sparse_fwd in the sparse attention path - #31128
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
DarkSharpness
left a comment
There was a problem hiding this comment.
LGTM. Actually I don't know why this is missing. Perhaps FlashMLA updated its interface 2 months ago and we didn't follow up correctly?
|
Thanks for the review! Your guess matches what we found when tracing this: |
…path The flash_mla_sparse_fwd kernel API (and the vendored FlashMLA sparse prefill kernels for both sm90 and sm100) accepts an optional per-row topk_length tensor and early-exits the top-k loop after ceil_div(topk_length[row], B_TOPK) blocks. The DSA backend never passed it, so rows whose context is shorter than index_topk (the first topk tokens of every prefill sequence, short decode/MTP rows) scanned the full -1-padded topk width for nothing. metadata.dsa_cache_seqlens_int32 (seqlens_expanded clamped to index_topk) is exactly the per-row count of valid indices, is already int32/contiguous, row-aligned with q by the DP/CP padding helpers, and is already updated in place under CUDA graph replay (the flashmla_kv path consumes it inside graphs today). Output is unchanged: all top-k emitters pad invalid tail entries with -1, which the kernel masks either way.
07cd88b to
66f89b3
Compare
|
Friendly bump 🙏 This is a small (+16/-0) perf-only change on the DSA sparse-attention path (passing |
|
/rerun-test test/registered/models_e2e/test_dsa_glm52_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_dp_mtp.py |
|
Results for 🚀 |
Motivation
sgl_kernel.flash_mla_sparse_fwdaccepts an optional per-rowtopk_lengthtensor, and the vendored FlashMLA sparse prefill kernels (both sm90 and sm100) early-exit their top-k loop afterceil_div(topk_length[row], B_TOPK)blocks. The DSA backend (dsa_backend.py::_forward_flashmla_sparse, the DeepSeek-V3.2 / GLM-5 path) never passed it, so every row whose context is shorter thanindex_topkscanned the full-1-padded top-k width for nothing. Withindex_topk = 2048that is:topk / (2L)of the sparse-prefill kernel work: ~25% for a 4k prompt, ~12% at 8k),This is pure padding tax: the kernel masks
-1indices either way, so the skipped tail contributes nothing to the output.deepseek_v4_backend.pyalready passestopk_lengthon its sparse-prefill path; this PR brings the DSA backend in line. Prefill performance on the V3.2/DSA path has been a reported concern (e.g. #14498) — this recovers part of the sparse-prefill kernel time for short/medium contexts for free. The mechanism (trailing fully-invalid top-k blocks) is analyzed in deepseek-ai/FlashMLA#196.Modifications
16-line diff in
python/sglang/srt/layers/attention/dsa_backend.py:_forward_flashmla_sparsegains an optionaltopk_lengthparameter, forwarded toflash_mla_sparse_fwd(with a defensive row-count guard that falls back to the old full-width behavior on mismatch).metadata.dsa_cache_seqlens_int32— the existing "seqlens clipped totopk" metadata, which is exactly the per-row count of valid indices. No new tensors or allocations in the hot path:pad_dsa_cache_seqlens/_pad_topk_indicespad to the same row count; pad rows get length 0, matching their all--1indices);fused_dsa_*_metadata/.copy_()); theflashmla_kvdecode path already consumes it inside captured graphs today.Correctness relies on top-k emitters writing all valid indices in the first
min(seqlen, topk)slots with-1padding at the tail, which holds for all backends: sgl-kerneltopk.cu(short rows:indice[i] = (i < length) ? i : -1; long rows fill all slots), the torch fallback (torch.topksorted output puts masked-infentries last), and the flashinfer / fused-v2 transforms (documented "-1padded" contract). The DSA test fixture codifies the same contract (_make_dsa_sparse_topk_rowstail-pads every pattern, including the non-trailingstrided/head_tailselections).Accuracy Tests
Kernel-level A/B on B200 (sm100, torch 2.11.0, sglang-kernel 0.4.4), emulating the exact call-site layout (q
[s_q, 128, 576]bf16, kv bf16,d_v=512,topk=2048, indices tail-padded with-1,topk_length[i] = min(i+1, 2048)): out, lse and max_logits are bitwise identical with vs withouttopk_length, for the ramp case, the fully-valid case (topk_length == topkeverywhere — no behavior change when there is nothing to skip), and empty rows (topk_length == 0gives the same output as an all--1row).test/registered/attention/unittests/dsa/test_dsa.py(B200): results with this patch are identical to an unpatched upstream/main baseline run in the same environment — all sparse-path tests pass (test_sparse_topk_cases,test_sparse_non_trailing_index_cases(strided/head_tail),test_sparse_prefill_impl_variants/test_sparse_decode_impl_variants/test_sparse_cuda_graph_decode_impl_variants(flashmla_sparse + flashmla_kv), fp8 prefill/decode, speculative forward modes, sparse layout robustness). The only failures in both runs are pre-existing environment issues in the dense MHA fallback and the trtllm variant (failure sets byte-identical between baseline and patch).sgl-kernel/tests/test_flashmla.py -k prefill: 20 passed.Speed Tests and Profiling
Paired interleaved CUDA-event timing (median of 50 pairs) of
flash_mla_sparse_fwdon B200, same layout as above. Note: shared node with ~85-90% ambient neighbor load, so treat these as a noise band; the controlled-environment number for the same kernel mechanism is in deepseek-ai/FlashMLA#196.topk_lengthtopk_lengthA second order-balanced run at s_q=8192 under heavier ambient load gave 1.05-1.07x (ramp) and 0.997x (fully-valid), i.e. the ramp win tracks the expected
topk/(2L)work reduction minus fixed overheads, and the fully-valid case is unchanged.Checklist
CI States
Latest PR Test (Base): ⏳ Run #30599366957
Latest PR Test (Extra): ❌ Run #30599366700