DSA backward SM100: support zero-length top-k rows in the kernel - #439
Conversation
📝 WalkthroughWalkthroughThe SM100 sparse-attention backward kernel now handles zero or negative ChangesSM100 empty sparse-row handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Thanks @jiayus-nvidia — this is a better resolution than the interface guard in #433. Handling the empty row inside the kernel avoids the per-call D2H sync, keeps CUDA-graph capture behavior consistent, and the early exit before any pipeline/TMEM setup addresses the deadlock at its root. I also looked at the sm100 Closing #433 in favor of this. |
|
@cudnn-ci-bot run |
|
🚀 Running mirror pipeline Branch: cudnn-gh/pr-439-ed665d8 |
…ckward All three are reachable from documented inputs to sparse_attention_backward_wrapper on SM90. 1. Nonpositive topk_length corrupts memory or hangs. n_block_max is 0 and n_block is -1, but WG0 still runs its unconditional first n_block, so the KV gather indexes topk_idxs[-64 + row] out of bounds and dereferences the result as a KV row. WG1 runs zero mainloop iterations, leaving WG0 to wait alone on G4_half_ready and sdS_consumed, both 256-thread named barriers. Whichever lands first decides whether the symptom is cudaErrorIllegalAddress or a hang. WG1's acc_dQ_2/3 are also never zero-initialised in this case, so the epilogue TMA-stores stale registers into the caller's dq. Guard both warpgroups on the same CTA-uniform topK, so every cross-warpgroup barrier stays unarrived on both sides, and zero the four dQ accumulators so the existing TMA epilogue writes the zero tile. Reusing the epilogue keeps the addressing, row predication and d=576 tail handling identical to the normal path and needs no new kernel parameter. The guard covers WG0's whole prologue, not just the barriers: the Q/dO TMA lands in sQ, which WG1's epilogue also writes, and the sP_ready/sdS_ready handshake that normally orders the load ahead of both epilogues is gone once the mainloop is skipped. 2. Padded top-k columns are never masked. The gather zero-fills their KV row in SMEM, so their score is 0 rather than -inf and their probability is exp2(-LSE). A sufficiently negative LSE overflows to +inf and GEMM4 turns inf * 0 into NaN across the whole dQ tile. Mask them to probability zero in the softmax. The compact-tail test is emitted only for the peeled first n_block, and the negative-index test only when topk_length is absent, matching the non-compact contract documented at dsa_bwd_sm100.py:321. 3. A saturating attn_sink NaNs every gradient. Folding the sink into the LSE shifts a logaddexp by fmax(lse_log2, sink_log2); once that maximum is infinite the shift evaluates inf - inf. attn_sink need not be infinite -- the log2(e) rescale saturates for any finite |sink| > 3.4e38 / log2(e). Compute p_sink as an algebraically identical sigmoid, and shift the LSE-with-sink logaddexp by its maximum only while that maximum is finite. dq and dkv are bit-identical to develop on ordinary inputs; d_sink moves by at most 2.5e-6 relative from the sigmoid rewrite. No measurable cost on the compact path at topk=1024; +0.35% at topk=64, where the tail mask cannot amortise over n_blocks, and +0.87% on the non-compact path for the per-column topk_idxs read. Eleven test cases newly execute on SM90 -- four from widening NVIDIA#439's zero top-k test to SM90+ rather than duplicating it, seven new -- of which ten fail on develop. Related to NVIDIA#676. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es (#785) * Fix three top-k / attention-sink boundary failures in the SM90 DSA backward All three are reachable from documented inputs to sparse_attention_backward_wrapper on SM90. 1. Nonpositive topk_length corrupts memory or hangs. n_block_max is 0 and n_block is -1, but WG0 still runs its unconditional first n_block, so the KV gather indexes topk_idxs[-64 + row] out of bounds and dereferences the result as a KV row. WG1 runs zero mainloop iterations, leaving WG0 to wait alone on G4_half_ready and sdS_consumed, both 256-thread named barriers. Whichever lands first decides whether the symptom is cudaErrorIllegalAddress or a hang. WG1's acc_dQ_2/3 are also never zero-initialised in this case, so the epilogue TMA-stores stale registers into the caller's dq. Guard both warpgroups on the same CTA-uniform topK, so every cross-warpgroup barrier stays unarrived on both sides, and zero the four dQ accumulators so the existing TMA epilogue writes the zero tile. Reusing the epilogue keeps the addressing, row predication and d=576 tail handling identical to the normal path and needs no new kernel parameter. The guard covers WG0's whole prologue, not just the barriers: the Q/dO TMA lands in sQ, which WG1's epilogue also writes, and the sP_ready/sdS_ready handshake that normally orders the load ahead of both epilogues is gone once the mainloop is skipped. 2. Padded top-k columns are never masked. The gather zero-fills their KV row in SMEM, so their score is 0 rather than -inf and their probability is exp2(-LSE). A sufficiently negative LSE overflows to +inf and GEMM4 turns inf * 0 into NaN across the whole dQ tile. Mask them to probability zero in the softmax. The compact-tail test is emitted only for the peeled first n_block, and the negative-index test only when topk_length is absent, matching the non-compact contract documented at dsa_bwd_sm100.py:321. 3. A saturating attn_sink NaNs every gradient. Folding the sink into the LSE shifts a logaddexp by fmax(lse_log2, sink_log2); once that maximum is infinite the shift evaluates inf - inf. attn_sink need not be infinite -- the log2(e) rescale saturates for any finite |sink| > 3.4e38 / log2(e). Compute p_sink as an algebraically identical sigmoid, and shift the LSE-with-sink logaddexp by its maximum only while that maximum is finite. dq and dkv are bit-identical to develop on ordinary inputs; d_sink moves by at most 2.5e-6 relative from the sigmoid rewrite. No measurable cost on the compact path at topk=1024; +0.35% at topk=64, where the tail mask cannot amortise over n_blocks, and +0.87% on the non-compact path for the per-column topk_idxs read. Eleven test cases newly execute on SM90 -- four from widening #439's zero top-k test to SM90+ rather than duplicating it, seven new -- of which ten fail on develop. Related to #676. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Bound the non-compact top-k index read to the row The peeled tile still spans all tile_n columns even when it is the partial one, so the per-column topk_idxs load could index past the end of the query's top-k row whenever topk_idxs.shape[-1] is not a multiple of tile_n -- by 28 entries at max_topk=100, 58 at max_topk=70. Those lanes are already zeroed by the tail mask and this guard only ever zeroes, so the result was unaffected, but the read itself is out of bounds. Clamp the index; the clamped entry cannot change the outcome. Cover it by sizing the non-compact test's topk_idxs to a non-multiple of 64. Reported by CodeRabbit on #785. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Tighten SM90 DSA backward sink/pad regressions and trim kernel comments Name the sink-inclusive LSE with explicit ±inf arms so CuTeDSL SSA is defined before the staged ifs. Document the compact/non-compact top-k contract. Drive the padded-column test past FP32 exp2 overflow and generate saturating-sink out/lse from the actual sink. Co-Authored-By: Claude <noreply@anthropic.com> * Simplify SM90 DSA backward sink fold and slim pad/sink regressions Drop is_first: the peeled n_block already passes dQ_accumulate=False, so the tail mask, KV_empty wait and GEMM4 zero_init share one flag. Fold sink into LSE with isfinite instead of explicit ±inf arms. Compute p_sink only when KV LSE is not +inf so the develop convention stays 0 rather than 0.5. Move n_block_max / tail rows inside the topK > 0 guard. Slim the new tests to one compact and one non-compact pad case, and to the two positive saturating sinks that actually NaN on develop. Compact padding leaves the ignored tail as ordinary KV indices. Zero masked reference weights so a +inf sink is not NaN from -inf - +inf. Co-Authored-By: Claude <noreply@anthropic.com> * Keep num_valid_rows as the develop helper name Renaming it to valid_rows was noise; is_first is still folded into dQ_accumulate. Co-Authored-By: Claude <noreply@anthropic.com> * Format DSA backward SM90 Black --line-length 160 collapses the sink logaddexp sum_exp2 onto one line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Avoid cute.math.isfinite on SM90 DSA backward LSE fold CI's nvidia-cutlass-dsl (floor 4.5.0, unpinned) has no cute.math.isfinite. Use the same ±inf compares this kernel already uses on develop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Add native SM100 kernel support for sparse-attention backward rows whose
topk_lengthis zero.The kernel now detects an empty row before initializing any asynchronous
pipeline or allocating TMEM. All threads in the CTA cooperatively write zero
to the corresponding
dQrow and then exit. The row contributes nothing todKVord_sink, matching the mathematical gradient of a query attendingto no KV entries.
Malformed negative lengths take the same defensive early-exit path so they
cannot enter the zero-tile pipeline path.
Why
The SM100 backward kernel previously computed:
For topk == 0, this produces zero tiles, but the warp-specialized pipeline
assumes that at least one tile is handed between its producer and consumer
stages. On B200 this can deadlock the launch at 100% SM utilization. If
execution reached the epilogue, dQ could also be written from TMEM
accumulators that no MMA instruction had initialized.
Handling the row inside the kernel:
Implementation
For each query-token/head-block CTA, the kernel reads the CTA-uniform
topk_length before setting up pipelines or TMEM.
When the value is nonpositive, it:
The normal path reuses the same topk value, so positive rows do not incur
an additional load.
API and compatibility impact
allowed to enter the deadlocking pipeline path.
Related work
This is an alternative kernel-side resolution for #433.
Thanks to @zkyue for identifying the zero-tile failure mode, providing the
B200 hang reproducer, and analyzing the likely pipeline deadlock. That
investigation directly motivated this implementation.
Unlike the interface guard proposed in #433, this change supports empty rows
without adding a host synchronization or capture-specific behavior.
Testing
Tested on an NVIDIA B200 (SM100):
PYTHONPATH=/code/github/cudnn-frontend/python
pytest -q -rs
fe_api/dsa/test_DSA_sparse_attention_backward.py
-k sm100_zero_topk_length
Result:
2 passed, 6 deselected
Coverage includes:
Summary by CodeRabbit
Bug Fixes
Tests