Skip to content

DSA backward SM100: reject topk_length entries < 1 (a zero entry hangs the kernel) - #433

Closed
zkyue wants to merge 3 commits into
NVIDIA:developfrom
zkyue:fix/dsa-bwd-topk-length-zero-guard
Closed

DSA backward SM100: reject topk_length entries < 1 (a zero entry hangs the kernel)#433
zkyue wants to merge 3 commits into
NVIDIA:developfrom
zkyue:fix/dsa-bwd-topk-length-zero-guard

Conversation

@zkyue

@zkyue zkyue commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Before submitting

  • I agree to license this contribution under the terms of LICENSE.txt.
  • I ran the repo's Python formatting hook (black 26.3.1 -l 160) and committed any changes — files unchanged. (clang-format n/a: Python only.)

Affected area

FE OSS kernels or CuTeDSL

Summary

sparse_attention_backward accepts a per-query topk_length, but the SM100
kernel has no defined behavior for a zero entry. In dsa_bwd_sm100.py the
per-token tile count is

topk = mTopkLength[token_idx]
...
tile_count = cute.ceil_div(topk, self.block_tile)

with no zero guard. In practice a topk_length containing zero entries
hangs the launch: on the configuration we measured, the call never
returns — 100% SM utilization until the process is SIGKILLed — while the
identical call with every length >= 1 completes normally. Code inspection
suggests a zero-tile pipeline deadlock (the warp-specialized stages have no
tiles to hand through for that token), and that a zero-tile token reaching
the dq epilogue would store accumulator TMEM that no MMA has written; the
mathematically correct dq for a query attending to nothing is exactly 0.

This PR adds minimal interface validation, with no kernel changes:

  • _interface_sm100.flash_attn_bwd_sm100 raises ValueError when any
    topk_length entry is < 1. Zero entries are the measured hazard;
    negative values are rejected by the same check. The check needs a
    device-to-host sync, so it runs outside CUDA graph capture only
    under capture the caller must uphold the precondition, which the
    docstrings state explicitly;
  • the precondition is documented on the interface and on
    sparse_attention_backward_wrapper;
  • a test locks the guard in (0 and a negative value both raise).

We deliberately did not implement kernel-side support (skipping the
pipeline handshake and zero-filling dq for zero-tile tokens): that is a
semantic decision about whether empty rows are supported input, and it
touches the same kernel region #421 is currently changing. If you would
rather support topk_length == 0 in the kernel, we are happy to follow up
with that variant instead — this guard simply becomes removable in that
world.

Why

Repro on unmodified develop @3a9ed3f (B200, CUDA 13.3, nvidia-cutlass-dsl
4.6.1, torch 2.13): s_q=128, s_kv=4096, h=64, head_dim=head_dim_v=512,
topk=512; topk_length[{3, 64, 127}] = 0 (3 of 128 rows), all other rows
512:

STEP: priming...                                   # all topk_length = 512
priming run finished; max|dq_ok| = 5.71875         # ~40 s incl. first-call JIT
STEP: bad run 1...                                 # topk_length[{3,64,127}] = 0
<never returns — killed after 66 minutes>

While hung: GPU at 100% utilization, host thread parked in
torch.cuda.synchronize() (py-spy), no forward progress. Reproduces from a
fresh process. With this PR the same call raises ValueError up front.

The current test generator floors lengths at 1
(torch.randint(1, topk_k + 1, ...)), so nothing in CI exercises 0 — the
case is reachable only from user input, which is exactly the case that
should fail loudly rather than hang the device.

Related issues

None filed; found while auditing the DSA OSS kernel contracts. Adjacent but
independent of #421 (which changes the same kernel's sink normalization and
extends the same test file — this PR touches only the Python interface layer
plus one appended test; trivial to rebase in either order).

API and compatibility impact

  • Calls with any topk_length entry < 1 now raise ValueError outside
    CUDA graph capture (previously: zero entries hung the device on the
    configuration we measured). Under capture the check cannot run and the
    documented precondition applies. Calls satisfying the precondition are
    unaffected in behavior.
  • Cost on valid calls: one (topk_length < 1).any() reduction + D2H sync
    per call on the SM100 path (skipped under capture). If that is unwanted on
    hot paths, we can gate it behind an env var or debug flag instead —
    maintainer preference.
  • SM90 path: not investigated (we have no SM90 evidence either way), so the
    guard is scoped to the SM100 interface where the failure is demonstrated.

Testing

Environment: B200 (SM100), CUDA 13.3, nvidia-cutlass-dsl 4.6.1, torch 2.13.

  • Full test/python/fe_api/dsa suite on this branch: 4 failed, 27 passed,
    4 skipped
    — the failure set is identical to unpatched develop @3a9ed3f
    (2× test_DSA_indexer_top_k[705], 2×
    test_DSA_sparse_attention_backward_wrapper[576], pre-existing and in
    DSA: fix SM100 sink normalization for 576/512 backward #421's scope); the +1 passed is the new guard test.
  • New test: test_DSA_sparse_attention_backward_topk_length_zero_raises
    (the guard raises before any kernel compile/launch, so it runs in ~1 s).
    The hang itself was demonstrated with the separate repro configuration
    above, not by this test.

Summary by CodeRabbit

  • Bug Fixes

    • Added SM100-specific pre-launch validation for sparse-attention backward to reject topk_length entries less than 1 (including 0 and negatives), raising a clear ValueError.
    • Clarified that this validation is skipped during CUDA graph capture due to undefined behavior for topk_length == 0.
    • Improved behavior guidance when invalid values are provided outside capture (callers should prevent empty rows upstream).
  • Tests

    • Added regression coverage for invalid topk_length rejection.
    • Added capture-aware tests, including scenarios with an explicitly provided non-current launch stream.

A topk_length entry of 0 gives the SM100 kernel zero KV tiles for that
token (tile_count = ceil_div(0, block_tile) = 0 in dsa_bwd_sm100), a
case the kernel has no defined behavior for. Measured on B200
(head_dim 512, head_dim_v 512, h 64, topk 512, s_q 128, s_kv 4096): a
call whose topk_length contains zero entries (3 of 128 rows) never
terminates -- the launch hangs at 100% SM until killed -- while the
identical call with all lengths >= 1 completes normally (~40 s
including first-call JIT compilation). Code inspection suggests a
zero-tile pipeline deadlock, and that a zero-tile token reaching the
dq epilogue would store accumulator TMEM that no MMA instruction has
written. The mathematically correct dq for a query attending to
nothing is exactly 0.

Make the failure loud at the interface layer instead of hanging the
device: flash_attn_bwd_sm100 raises ValueError when any topk_length
entry is < 1 (zero entries are the measured hazard; negatives are
rejected by the same check). The check needs a device-to-host sync, so
it runs outside CUDA graph capture only -- under capture the caller
must uphold the precondition, which is documented on the interface and
on sparse_attention_backward_wrapper. Add a test covering 0 and a
negative entry.

No kernel change: whether topk_length == 0 should instead be supported
input (skipping the pipeline handshake and zero-filling dq for
zero-tile tokens) is left as an upstream semantic decision; this is
minimal interface validation and is trivially removable in that world.

Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c016e1bd-04da-410d-bbcf-ed34356f66e7

📥 Commits

Reviewing files that changed from the base of the PR and between 19fb64f and 46d7187.

📒 Files selected for processing (1)
  • test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py

📝 Walkthrough

Walkthrough

The SM100 sparse-attention backward path validates topk_length >= 1 on the resolved launch stream outside CUDA graph capture. Documentation defines capture behavior and undefined zero-length handling, while SM100 tests cover invalid values and explicit-stream capture.

Changes

SM100 topk_length validation

Layer / File(s) Summary
Document and enforce topk_length precondition
python/cudnn/deepseek_sparse_attention/sparse_attention_backward/_interface_sm100.py, python/cudnn/deepseek_sparse_attention/sparse_attention_backward/api.py
The interface resolves the launch stream, rejects entries below one outside capture, and documents the capture-time precondition and undefined zero-length behavior.
Validate invalid and captured launches
test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py
SM100 tests cover zero and negative entries, explicit launch streams, and capture-time bypass of the guard.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific, concise, and accurately reflects the SM100 validation change for invalid topk_length entries.
Description check ✅ Passed The description follows the template closely and includes the required sections, rationale, compatibility impact, and testing details.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py (1)

307-317: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reconsider the L0 classification.

On a cold cache, the wrapper compiles the SM100 object before execute() reaches this validation (see Lines [176]-[192] in api.py). This test can therefore pay the full CuTe compilation cost despite never launching the kernel. Move it to the appropriate higher level, or verify that compilation is already warmed in this test suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py` around lines
307 - 317, Reclassify test_DSA_sparse_attention_backward_topk_length_zero_raises
from L0 because wrapper compilation occurs before validation and can incur a
cold-cache CuTe compile. Move the test to the appropriate higher-level suite, or
ensure the suite explicitly warms the SM100 compilation before running this
validation-only test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@python/cudnn/deepseek_sparse_attention/sparse_attention_backward/_interface_sm100.py`:
- Around line 87-102: Resolve the launch stream from current_stream before the
CUDA graph-capture and topk_length validation in
SparseAttentionBackward.execute. Use that resolved stream for capture detection
and the required topk_length synchronization, or explicitly reject non-current
streams; ensure explicit-stream capture cannot perform an illegal sync and
empty-row validation remains enforced on the launch stream.

In `@test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py`:
- Around line 324-326: Update the test’s SM100 guard to reuse the repository’s
existing DSA support check and backend-version gate, including
cudnn.backend_version() alongside torch.cuda.get_device_capability(). Skip
unsupported architecture, dtype, or backend combinations before the DSA wrapper
setup, while preserving execution for supported configurations.

---

Nitpick comments:
In `@test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py`:
- Around line 307-317: Reclassify
test_DSA_sparse_attention_backward_topk_length_zero_raises from L0 because
wrapper compilation occurs before validation and can incur a cold-cache CuTe
compile. Move the test to the appropriate higher-level suite, or ensure the
suite explicitly warms the SM100 compilation before running this validation-only
test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2d1ade90-6ab9-4108-9e5b-d12dedd29480

📥 Commits

Reviewing files that changed from the base of the PR and between 3a9ed3f and d7b76ff.

📒 Files selected for processing (3)
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/_interface_sm100.py
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/api.py
  • test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py

Comment on lines +324 to +326
major, _ = torch.cuda.get_device_capability()
if major != 10:
pytest.skip("the topk_length >= 1 guard is implemented on the SM100 interface")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add the repository’s backend/support gate.

The changed test checks only major == 10 and whether cudnn.DSA imports. Reuse the existing DSA support check and backend-version gating so an unsupported SM100/backend combination skips instead of failing during wrapper setup.

As per coding guidelines, test/python/**/*.py tests must gate unsupported architecture, dtype, and backend-version combinations using support checks, cudnn.backend_version(), and torch.cuda.get_device_capability().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py` around lines
324 - 326, Update the test’s SM100 guard to reuse the repository’s existing DSA
support check and backend-version gate, including cudnn.backend_version()
alongside torch.cuda.get_device_capability(). Skip unsupported architecture,
dtype, or backend combinations before the DSA wrapper setup, while preserving
execution for supported configurations.

Source: Coding guidelines

…tream, test gating

_interface_sm100: resolve the launch stream before the topk_length guard
and evaluate both the capture check and the device-to-host sync inside
torch_stream_context(current_stream). Previously the guard consulted
torch.cuda.is_current_stream_capturing() on whatever torch stream was
current: with an explicit current_stream, a capture on that stream was
not detected (validation ran mid-capture, syncing a stream it must not),
and outside capture the sync was not ordered against the caller producer
work on the launch stream. Add a test that exercises both sides on a
real explicit side stream: outside capture a zero row is still rejected;
during capture on the explicit stream -- while the torch-current stream
is a different, non-capturing stream -- the guard detects the capture on
the resolved stream and skips validation per the documented capture
semantics (caller upholds the precondition), the launch is recorded into
the graph, and the graph is never replayed. Verified red on the previous
guard placement (ValueError raised mid-capture) and green with this one.

Test gating: replace the bare major == 10 check with the suite's DSA
capability gate (major * 10 + minor < 100, matching
test_DSA_sparse_attention_backward_staged_store and dsa_init's
min_compute_capability=100). The cudnn[cutedsl] import skip remains the
backend gate: this CuTe-DSL-only path does not depend on
cudnn.backend_version().

L0 classification of the zero-raises test: kept, with the reasoning
documented in the test docstring -- SparseAttentionBackward.compile()
defers compilation to execute(), and the guard raises before the
interface consults its compile cache, so the validation-only test pays
no CuTe compilation cost even on a cold cache.

test/python fe_api/dsa: 4 failed, 28 passed, 4 skipped; the failure set
is identical to the pre-change baseline.

Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py`:
- Around line 360-378: Change the pytest marker on
test_DSA_sparse_attention_backward_topk_length_guard_explicit_stream from L0 to
L1, since its pre-capture call performs compile-cache warmup and real kernel
execution. Leave the test logic and neighboring markers unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 14d5f5cc-70e6-4e49-b9d5-401263c417c4

📥 Commits

Reviewing files that changed from the base of the PR and between d7b76ff and 19fb64f.

📒 Files selected for processing (3)
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/_interface_sm100.py
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/api.py
  • test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/api.py
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/_interface_sm100.py

Comment thread test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py Outdated
…fore capturing)

Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
@Anerudhan

Copy link
Copy Markdown
Collaborator

Thanks @zkyue for the contribution.
Have asked the relevant folks to review your contribution.

@Anerudhan Anerudhan added orig-external Reported or requested by an external user, customer, or community contributor. mod-cutedsl CuTeDSL kernels, generated kernels, examples, or related integration work. cat-enhancements labels Jul 24, 2026
@Anerudhan Anerudhan added this to the Frontend 1.27.0 milestone Jul 24, 2026
@jiayus-nvidia

Copy link
Copy Markdown
Contributor

Thanks @zkyue for identifying and carefully reproducing this issue. After exploring the implementation options, I opened #439 as a kernel-side alternative that supports empty rows directly, avoids the per-call D2H synchronization, and behaves consistently during CUDA graph capture.

@zkyue

zkyue commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of #439, which supports empty rows inside the kernel — no host-side synchronization and consistent behavior under CUDA graph capture. Thanks for the quick turnaround!

@zkyue zkyue closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cat-enhancements 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.

3 participants