sparse_prefill_fwd (sm100): skip trailing fully-invalid top-k blocks when topk_length is not provided - #196
Open
zkyue wants to merge 1 commit into
Open
Conversation
When topk_length is not passed to flash_mla_sparse_fwd, each row of the
sm100 sparse prefill forward kernel runs ceil(topk / B_TOPK) k-blocks,
including trailing blocks whose indices are all invalid (e.g. rows padded
with -1 to a fixed width). Such blocks already skip the TMA gather, but
the MMA and softmax warpgroups have no skip path: both UMMA GEMMs, the
TMEM P retrieval, the masking/exp2 pass and the whole barrier chain still
execute, at a constant ~1024 tensor-pipe busy cycles per block, so every
fully-padded block costs a full compute iteration while contributing
nothing to the output.
Since the pipeline barrier phases are indexed by the k-block index,
blocks cannot simply be skipped from the middle of the loop. Instead,
when the topk_length argument is absent, derive an equivalent value in the
kernel prologue: each warp scans the row's indices from the tail for the
last valid entry (0 <= idx < s_kv) and clamps the k-block loop exactly as
an explicitly-passed topk_length would. The scan inspects 256 indices per
round starting from the tail, so it typically finishes within the first
round; to keep that round's memory latency off the critical path its
loads are issued at the very beginning of the kernel (in flight while the
prologue runs) and only consumed after the prologue's __syncthreads().
Invalid indices before the last valid one keep the existing masking path,
so arbitrary (not just front-packed) index layouts remain correct; rows
with no valid index at all behave exactly like topk_length = 0. When
topk_length is passed, the scan is skipped and behavior is unchanged.
Applies to fwd/head64 and fwd/head128 (both CTAs of the head128 cluster
derive the same value from the same read-only data). The
fwd_for_small_topk kernel computes its per-job topk_length inside the CLC
persistent loop from divergent contexts and is left unchanged.
Benchmark on B200 (bf16, s_q = 8192, h_q = 64, d_qk = d_v = 512,
front-packed rows with valid counts ramping causally up to 1027, padded
with -1 to topk = 1152; medians of 100-iteration interleaved runs, 3
alternating rounds per build):
before after
no topk_length 1.248 ms 1.144 ms (-8.4%)
topk_length passed 1.103 ms 1.101 ms (unchanged)
fully-valid rows (no padding) 1.280 ms 1.287 ms (+0.5%)
Nsight Compute on the same workload: tensor-pipe busy cycles per SM drop
from 1.020M (no topk_length, before) to 0.844M (after) -- exactly the
level of the topk_length-passed run (0.844M) -- with DRAM bytes
unchanged, i.e. the padded blocks' GEMM/softmax iterations are fully
eliminated.
out/lse/max_logits stay bit-identical to the unpatched kernel with and
without topk_length across front-packed, scattered-interior-invalid (-1
and >= s_kv mid-row), all-invalid and fully-valid index layouts, with and
without attn_sink, for head64 and head128 at d_qk 512 and 576.
tests/test_flash_mla_sparse_prefill.py passes (617 cases).
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When
topk_lengthis not passed toflash_mla_sparse_fwd, every row of the sm100 sparseprefill forward kernel runs
ceil(topk / B_TOPK)k-blocks, including trailing blocks whoseindices are all invalid (e.g. rows padded with
-1to a fixed width, which the docstringexplicitly allows). Those blocks already skip the TMA gather (
should_skip_tma), but the MMAand softmax warpgroups have no skip path: both UMMA GEMMs, the TMEM P retrieval, the
masking/exp2 pass and the whole barrier chain still execute. ncu shows tensor-pipe busy
cycles are a constant ~1024 cycles per k-block whether or not the block contains any valid
index (busy-cycle ratios match tile-count ratios exactly across padded widths), while DRAM
bytes stay identical — i.e. each fully-padded block costs a full compute iteration and buys
nothing.
On B200 (bf16, s_q = 8192, h_q = 64, d_qk = d_v = 512), with front-packed rows whose valid
counts ramp causally up to ~1027 and are padded with -1 to topk = 1152, this is a ~9% wall
time overhead vs. passing
topk_length(and ~14% in noisier ambient conditions).Fix
The pipeline's barrier phases are indexed by the k-block index, so blocks cannot simply be
skipped from the middle of the loop (that would deadlock the 3-warpgroup pipeline). Instead,
when the
topk_lengthargument is absent, the kernel prologue derives an equivalent value:each warp scans the row's index vector from the tail for the last valid entry
(
0 <= idx < s_kv) and clamps the k-block loop exactly as an explicitly-passedtopk_lengthwould.
To keep the scan off the critical path, its first-round loads (the trailing 256 indices) are
issued at the very beginning of the kernel — so they are in flight while the prologue
(barrier init, Q TMA, TMEM allocation) runs — and the result is consumed right after the
existing
__syncthreads(). Only rows whose padding exceeds 256 indices pay additional scanrounds, and those rows then save whole k-blocks. Every warp computes the same value from the
same read-only data, so the loop bound stays uniform across all warpgroups (and across the
2-CTA cluster of the head128 kernel) with no extra synchronization.
Correctness notes:
(not just front-packed) index layouts remain correct.
topk_length = 0and take exactly today'stopk_length = 0path (num_k_blocks = max(..., 1), one fully-masked block, output 0,lse = +inf,max_logits = -inf).explicitly-passed
topk_length(a valid index beyondtopk_length[i]pointing at NaNpayload) cannot be triggered by this path.
topk_lengthis passed, the scan is skipped and behavior is unchanged.Scope:
fwd/head64andfwd/head128. Thefwd_for_small_topkkernel (selected forh_q = 128 with topk <= 1280) computes its per-job
topk_lengthinside the CLC persistentouter loop from divergent contexts (single-elected-thread and 16-lane callers), so the same
warp-cooperative prologue scan does not drop in; it is left unchanged and can be addressed
separately if desired.
Numbers
B200, CUDA 13.3, bf16, s_q = 8192, h_q = 64, h_kv = 1, d_qk = d_v = 512. "Ramp-padded" =
front-packed valid counts ramping causally up to ~1027, padded with -1 to width 1152;
"fully-valid" = width 1152 with no invalid index in any row. Medians of 100-iteration
interleaved runs, 3 alternating rounds per build, same GPU:
tests/test_flash_mla_sparse_prefill.py: all 617 cases pass on the patched build.out/lse/max_logitsare bit-identical between patched and unpatchedbuilds across ramp-padded, scattered-interior-invalid (
-1and>= s_kvmid-row),all-invalid, and fully-valid layouts, for head64 and head128 (regular kernel, topk 2048)
at d_qk 512 and 576, with and without attn_sink, with and without topk_length
(32 workload dumps x 3 tensors).
tensor-pipe busy cycles per SM drop from 1.020M (no topk_length, before) to 0.844M
(after), exactly matching the topk_length-passed run (0.844M);
dram__bytes_readisunchanged (587.2 MB -> 587.5 MB);
sass global_ldinstructions drop 2.39M -> 2.27M(fewer per-block index loads outweigh the scan's tail reads). The residual wall-time gap
vs. the topk_length path (~4%) is scan latency on the ramp rows, not tensor work.
spill warnings (the STACK:8 on fwd_for_small_topk decode pre-exists at 9241ae3).
Alternatives considered
round trip per CTA on the critical path: with 1 CTA/SM and ~55 sequential waves this
showed up as +2.7% .. +19% on unpadded workloads. The issue-early/consume-late split
removes that (fully-valid regression 0.2-0.9% across measurement windows).
fully-valid overhead (more registers held across the prologue).
barrier phases across the three warpgroups (phases are indexed by k), which this PR
deliberately avoids.