Skip to content

DSA backward SM100: support zero-length top-k rows in the kernel - #439

Merged
Anerudhan merged 1 commit into
NVIDIA:developfrom
jiayus-nvidia:fix/dsa-bwd-zero-topk-kernel
Jul 27, 2026
Merged

DSA backward SM100: support zero-length top-k rows in the kernel#439
Anerudhan merged 1 commit into
NVIDIA:developfrom
jiayus-nvidia:fix/dsa-bwd-zero-topk-kernel

Conversation

@jiayus-nvidia

@jiayus-nvidia jiayus-nvidia commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Add native SM100 kernel support for sparse-attention backward rows whose
topk_length is 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 dQ row and then exit. The row contributes nothing to
dKV or d_sink, matching the mathematical gradient of a query attending
to 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:

tile_count = cute.ceil_div(topk, self.block_tile)

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:

  • avoids a per-call device reduction and device-to-host synchronization;
  • behaves consistently during CUDA graph capture;
  • gives empty rows a defined and mathematically natural result;
  • leaves the existing positive-topk_length path unchanged.

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:

  1. writes zero to every valid dQ element owned by the CTA;
  2. skips all KV, MMA, reduction, and pipeline work;
  3. exits the CTA before any asynchronous synchronization state is created.

The normal path reuses the same topk value, so positive rows do not incur
an additional load.

API and compatibility impact

  • topk_length == 0 is now supported on SM100.
  • The corresponding dQ row is exactly zero.
  • The row contributes zero to dKV and d_sink.
  • Calls with positive lengths are unchanged.
  • Negative lengths are handled defensively as empty rows rather than being
    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:

  • mixed lengths around the 64-row tile boundary;
  • zero and defensive negative lengths;
  • all-empty inputs;
  • head dimensions 512 and 576;
  • full and partial head CTAs;
  • caller-provided dQ and dKV buffers;
  • numerical comparison with the PyTorch reference;
  • exact-zero checks for all-empty gradients.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed sparse attention backward processing for empty or invalid attention rows.
    • Empty rows now produce zero gradients without affecting valid rows.
    • Improved handling of invalid indices in empty rows and prevented potential GPU synchronization issues.
  • Tests

    • Added regression coverage for zero and negative attention lengths across multiple configurations.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The SM100 sparse-attention backward kernel now handles zero or negative topk_length rows by zeroing mdQ and exiting early. A parametrized GPU regression test validates outputs, gradients, buffer overwrites, invalid indices, all-empty cases, and synchronization.

Changes

SM100 empty sparse-row handling

Layer / File(s) Summary
Kernel empty-row fast path
python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py
The kernel computes topk before pipeline setup, zeros mdQ for non-positive rows, exits the CTA, and removes the later redundant initialization.
Empty-row regression coverage
test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py
A parametrized SM100 test validates zero and negative lengths, empty-row outputs and gradients, caller-owned buffer overwrites, ignored invalid indices, all-empty gradients, and synchronization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: cat-enhancements

Suggested reviewers: zkyue, anerudhan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed Title clearly summarizes the main change: SM100 sparse-attention backward now supports zero-length top-k rows.
Description check ✅ Passed The description covers summary, why, API impact, related issue, and testing; only template-specific headings like Affected area are omitted.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@zkyue

zkyue commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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 score_recompute path while reviewing this: its block-skip loop participates in the mbarrier handshake for every skipped slot, so it is already safe for zero-length rows — with this PR the whole family is closed for empty rows.

Closing #433 in favor of this.

@Anerudhan Anerudhan added cat-bug Reports of incorrect behavior, crashes, regressions, or unexpected results. orig-external Reported or requested by an external user, customer, or community contributor. mod-cutedsl CuTeDSL kernels, generated kernels, examples, or related integration work. labels Jul 27, 2026
@Anerudhan Anerudhan added this to the Frontend 1.27.0 milestone Jul 27, 2026
@Anerudhan

Copy link
Copy Markdown
Collaborator

@cudnn-ci-bot run

@cudnn-ci-bot

Copy link
Copy Markdown

🚀 Running mirror pipeline

Branch: cudnn-gh/pr-439-ed665d8
Pipeline: 59802840

@Anerudhan
Anerudhan merged commit c5012bd into NVIDIA:develop Jul 27, 2026
1 check passed
@Anerudhan Anerudhan mentioned this pull request Aug 6, 2026
SuperGoodGame added a commit to SuperGoodGame/cudnn-frontend that referenced this pull request Sep 4, 2026
…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>
jiayus-nvidia pushed a commit that referenced this pull request Sep 4, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cat-bug Reports of incorrect behavior, crashes, regressions, or unexpected results. mod-cutedsl CuTeDSL kernels, generated kernels, examples, or related integration work. orig-external Reported or requested by an external user, customer, or community contributor.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants