Skip to content

Fix latent TMEM WAR race in the SM100 DSA backward dKV drain (head_dim 576)#396

Open
zkyue wants to merge 3 commits into
NVIDIA:developfrom
zkyue:fix/dsa-bwd-drain-ordering
Open

Fix latent TMEM WAR race in the SM100 DSA backward dKV drain (head_dim 576)#396
zkyue wants to merge 3 commits into
NVIDIA:developfrom
zkyue:fix/dsa-bwd-drain-ordering

Conversation

@zkyue

@zkyue zkyue commented Jul 15, 2026

Copy link
Copy Markdown

Before submitting

  • I agree to license this contribution under the terms of LICENSE.txt.
  • I ran pre-commit run and committed any formatting changes. (clang-format skipped — no C/CUDA files; black passes.)

Affected area

FE OSS kernels or CuTeDSL

Summary

In FlashAttentionDSABackwardSm100.reduce_dKV, the part2 reduction reads
dKV2/dKV3 out of TMEM inside store_dKV. On the head_dim == 576
(not same_hdim_kv) layout those columns alias dKV0/dKV1, and the next
iteration's mma_reduce_dKV producer_acquire only orders against the
consumer release two generations back (the dKV4 generation) — not against these
dKV2/dKV3 reads. The MMA warp can therefore overwrite the columns before the
reduce warp has read them: a write-after-read race on TMEM that silently
corrupts dKV.

This PR reads dKV2/dKV3 into registers and arrives on a dedicated named barrier
(id 9) before the (slow) global atomic reduction, so the TMEM columns are free
for the next iteration's dKV0/dKV1 overwrite. This mirrors the T2R-then-arrive
pattern the kernel already uses in part1. The same_hdim_kv (head_dim 512)
path is unchanged — there dKV2/dKV3 map to distinct free TMEM columns and are
already safe (verified below).

Why

The race is timing-hidden: today's scheduling keeps the tensor-core queue
backed up so the reduce warp wins, but any perturbation that lets the MMA warp
catch up before the reads complete corrupts dKV, with no code change required.
Minimal reproduction: a 1 µs delay inserted between the two part2 store_dKV
calls on the 576 path corrupts dKV 3/3 runs (dkv relL2 5.6e-3 → 1.0), and the
corrupted TMEM column blocks match the missing-edge analysis exactly.

Related issues

Companion to #395 (two other latent synchronization bugs in the same
kernel); independent fix, different code path.

API and compatibility impact

No functional change to any current result; dq stays bitwise-identical.
This fix only touches the not same_hdim_kv (head_dim 576) path.

  • head_dim 512 is byte-unchanged: +0.03% (noise).
  • head_dim 576: +8.0% (p25 +7.8% / p75 +8.5%). This is the structural cost
    of closing the window — an extra commit→wake→2×T2R→barrier handshake per
    iteration on the MMA warp's critical path (~0.5 µs/iter, matching the measured
    delta). The stock kernel's lower 576 time is precisely the un-synchronized race.
    This is not the default/production configuration; we consider correctness-first
    the right call for a silent-gradient-corruption bug and are happy to discuss the
    tradeoff, but wanted to flag the regression explicitly for maintainer judgment.

Testing

B200 (SM100), CUDA 13.3, nvidia-cutlass-dsl[cu13] 4.5.2, torch 2.13.
Adversarial-delay matrix, S=2048 H=64 D=576 topk=512 bf16 sink-on; control dkv
relL2 5.64e-3 (5 TMEM column blocks uniform):

config dkv relL2 blocks b0/b1/b2/b3/b4 verdict
stock ctrl ×5 5.64e-3 uniform clean
delay before store_dKV(dKV2) @10/100µs ×3 9.4e-1 b2+b3 = 1.35e0, rest clean corrupt (as predicted)
delay between the two store_dKV @1/10/100µs ×3 5–6.6e-1 b3 = 1.0–1.35e0 only corrupt (as predicted)
negative controls (delay off-path) @100µs ×3 5.64e-3 uniform clean

Exactly and only the two read points the static edge table predicts have no
ordering flip under delay; protected blocks (b0/b1/b4) clean in all cases; dq
bitwise throughout. On the fixed kernel every previously-triggering delay is
clean (×3), and dq sha is byte-identical to stock on both 576 and 512. The 512
path is verified safe unmodified
: its adversarial matrix (including the case a
naive alias reading predicts should fire) is all clean.

Adversarial-delay repro harness Method: a `__nanosleep(DSA_PROBE_NS)` is inserted at a chosen reduce-warp read site (selected by an env key) in a copy of the kernel, and the result is compared against the fp32 autograd reference; per-128-column-block dkv relL2 isolates which TMEM columns are corrupted. dq (TMA single-writer) is the determinism control. The full probe harness + delay-injected tree are available on request.

Summary by CodeRabbit

  • Bug Fixes
    • Improved TMEM synchronization in sparse attention backward to prevent overwrite hazards during dKV processing.
    • Fixed potential reduction ordering/race issues affecting dKV2/dKV3 handling, particularly when key/value dimensions differ.
    • Refined completion/barrier handling across different KV layout paths to ensure reductions proceed safely and consistently.

…6 path)

In the not-same_hdim_kv path (head_dim 576 / head_dim_v 512), the reduce
warps read dKV2/dKV3 from TMEM inside store_dKV, after their
t2r_dKV4_done arrive. The MMA warp's next-iteration dKV0/dKV1 gemms
overwrite the same TMEM columns (tmem_dKV2_offset == tmem_dKV0_offset,
tmem_dKV3_offset == tmem_dKV1_offset) with no happens-before edge to
those reads: mma_reduce_dKV_pipeline has 2 stages, so the part1
producer_acquire of generation 3i+3 only orders against the
consumer_release of generation 3i+1 (the dKV4 generation), and no named
barrier separates part2's TMEM reads (generation 3i+2) from the issue of
the next part1's overwrites. Correctness currently depends on the tensor
core still draining queued work when the reduce warps issue their loads.

The window reproduces deterministically under adversarial delay: a 1 us
spin inserted in the reduce warps between the two part2 store_dKV calls
(no other change) corrupts dkv columns [384:512) on every run at
S=2048 H=64 D=576 topk=512 bf16 (rel L2 vs fp32 autograd 1.0e0 vs 5.6e-3
baseline); a 10 us spin before both calls corrupts [256:384) as well.
Delays at already-synchronized points (after part1's register-staged
T2Rs, or before the MMA warp's dKV0 issue) never corrupt.

Fix, mirroring part1's existing register-staging pattern: T2R dKV2/dKV3
into registers, fence, arrive on a new t2r_dKV23_done named barrier
(id 9), and only then run the global-memory atomic reduction; the MMA
warp waits on that barrier before issuing the next iteration's dKV0
(skipped on the first iteration, balanced by a final arrive after the
loop, like t2r_dKV4_done). Register peak is unchanged (part1 already
holds two fragments). The same_hdim_kv (head_dim 512) path is untouched
and compiles identically.

Measured on B200 (S=8192 H=64 topk=1024 bf16, ABAB paired):
head_dim 576: 5.398 -> 5.840 ms median (+8.0%), the cost of no longer
overlapping the next iteration's dKV issue with the dKV2/dKV3 readback;
head_dim 512: +0.03% (noise). dq stays bitwise-identical to develop on
both head dims; dkv/d_sink match the fp32 autograd reference at the
baseline rel L2 on both, and the adversarial-delay matrix that trips
develop is clean on the fixed kernel.
@coderabbitai

coderabbitai Bot commented Jul 15, 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: ded97e24-ebd0-4355-8ed4-944c5c7d16fa

📥 Commits

Reviewing files that changed from the base of the PR and between d8e4093 and 8578906.

📒 Files selected for processing (1)
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py

📝 Walkthrough

Walkthrough

The backward kernel adds a barrier for dKV2/dKV3 T2R completion, updates synchronization around MMA and loop transitions, and changes dKV reductions to use registers before global atomic reduction.

Changes

Sparse attention backward TMEM synchronization

Layer / File(s) Summary
T2R barrier protocol
python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py
Declares barrier 9 and selects the appropriate T2R completion barrier during MMA transitions, iterations, and final backward-loop synchronization.
Register-based dKV reduction
python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py
Loads dKV0/dKV1 or dKV2/dKV3 into registers, synchronizes completion, and performs reduction from registers instead of direct TMEM reads.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR's main change: fixing the SM100 DSA backward TMEM race for head_dim 576.
Description check ✅ Passed The description covers all required template sections with concrete summary, rationale, 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.

@Anerudhan Anerudhan added this to the Frontend 1.27.0 milestone Jul 16, 2026
@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 16, 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-396-ef50ddc
Pipeline: 58369771

Comment thread python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py Outdated
Comment thread python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py Outdated
Comment thread python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py Outdated
Comment thread python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py Outdated
@Jie-Fang

Copy link
Copy Markdown
Contributor

@zkyue

Thanks for helping fix the TMEM WAR race when head_dim=576.

During my review, I realized that head_dim=512 also has such issue. My comments are all about the head_dim=512, could you help fix it in your PR?

Your fix for head_dim=576 is good for me!

Co-authored-by: Jie Fang <jief@nvidia.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: 2

🤖 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/dsa_bwd_sm100.py`:
- Around line 2201-2209: Remove the duplicate unconditional dKV0/dKV1 reduction
block following the preceding branch, including its barrier arrival. Refactor
that branch into one unified register-based path that performs t2r_dKV, the TMEM
fence, barrier synchronization, and reduce_dKV_from_reg exactly once for both
values.
- Around line 1651-1655: Remove the stray outer if cutlass.const_expr(not
self.same_hdim_kv) preceding the “Gemm dKV = dO @ P part2” comments, leaving
only the intended conditional that follows them. Ensure the surrounding block
has valid indentation and parsing.
🪄 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: c50a772f-a26b-4ba2-93dc-624a5f908cf7

📥 Commits

Reviewing files that changed from the base of the PR and between ef50ddc and d8e4093.

📒 Files selected for processing (1)
  • python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py

Comment thread python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py Outdated
Comment thread python/cudnn/deepseek_sparse_attention/sparse_attention_backward/dsa_bwd_sm100.py Outdated
Completes the remove/relocate edits from the code review that the inline
suggestion blocks could not express on their own:
- NVIDIA#2: remove the now-duplicate same_hdim t2r_dKV4 wait before dKV1
- NVIDIA#4: remove the now-duplicate same_hdim t2r_dKV01 wait before dKV3
- NVIDIA#6: switch same_hdim dKV2/dKV3 drain to split T2R + reduce_dKV_from_reg

Also repairs line-drift from batch-applying the earlier suggestions
(their anchors shifted after the first suggestion inserted lines):
- NVIDIA#3: remove a leftover dangling `if cutlass.const_expr(not
  self.same_hdim_kv):` with no body (IndentationError) left because the
  original lines were not replaced
- NVIDIA#5: remove the leftover old dKV0/dKV1 if/else block that sat before the
  new unified block, which would otherwise double the reduce/atomic_add
  and the t2r_dKV01 barrier arrival

Final state is the intended split-T2R + signal drain refactor: every
reduce site loads dKV into registers, fences, signals the MMA warp that
the TMEM columns are free, then does the global atomic_add; same_hdim and
not-same_hdim now follow the same protocol. dq is bitwise-identical to
ef50ddc; head_dim 576 and 512 both verified race-free (compute-sanitizer
racecheck 0).
@zkyue

zkyue commented Jul 18, 2026

Copy link
Copy Markdown
Author

@Jie-Fang Thanks for the thorough refactor — the split T2R + signal protocol is much cleaner, and pulling the same_hdim path onto it is a good catch (it removes the store_dKV-reads-TMEM-after-the-barrier pattern that was the latent race window).

I've applied all three suggestion blocks. Batching them hit a bit of line-drift: since the first suggestion inserts lines, the anchors for the part-2 dKV2 (else branch) and the dKV0/dKV1 reduce suggestions landed slightly off, so the originals weren't fully replaced — that left a dangling if cutlass.const_expr(not self.same_hdim_kv): with no body and a duplicated dKV0/dKV1 reduce block. I've pushed a follow-up commit that (a) completes the remove/relocate edits from your other two comments — dropping the now-duplicate same_hdim t2r_dKV4/t2r_dKV01 waits before dKV1/dKV3 and switching the same_hdim dKV2/dKV3 drain to split T2R + reduce_dKV_from_reg — and (b) fixes that drift.

The branch now matches the refactor exactly as you laid it out: every reduce site loads dKV into registers, fences, signals the MMA warp that the TMEM columns are free, then does the global atomic_add, with same_hdim and not-same_hdim on the same protocol. dq is bitwise-identical to the pre-refactor commit, and I re-verified both layouts (head_dim 576 and 512) under compute-sanitizer racecheck — 0 hazards on each.

(Minor note: store_dKV (the 128-wide variant) has no callers left after this — happy to drop it in a follow-up if you'd like.)

@Jie-Fang

Copy link
Copy Markdown
Contributor

@zkyue

LGTM, thanks!

@Anerudhan this PR can be merged after the #395

@Anerudhan

Copy link
Copy Markdown
Collaborator

@cudnn-ci-bot run

@cudnn-ci-bot

Copy link
Copy Markdown

🚀 Running mirror pipeline

Branch: cudnn-gh/pr-396-8578906
Pipeline: 58685245

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