Skip to content

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

Merged
Anerudhan merged 3 commits into
NVIDIA:developfrom
zkyue:fix/dsa-bwd-drain-ordering
Jul 20, 2026
Merged

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

Conversation

@zkyue

@zkyue zkyue commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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 synchronization during sparse attention backward to prevent TMEM overwrite hazards during dKV processing.
    • Fixed reduction ordering for dKV2/dKV3 to behave consistently when key/value dimensions differ.
    • Refined “completion” handling so dKV reductions wait correctly across KV layout paths, including first/transition iterations.

@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: e7e7024b-8377-4adb-a04f-5f25ddee5a64

📥 Commits

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

📒 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 dKV2/dKV3 T2R completion barriers, 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
Adds and selects T2R completion barriers 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 and dKV2/dKV3 into registers, synchronizes completion, and performs reductions from registers instead of TMEM-backed stores.

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

Sequence Diagram(s)

sequenceDiagram
  participant MMA
  participant TMEM
  participant T2R
  participant reduce_dKV
  MMA->>TMEM: overwrite dKV columns
  reduce_dKV->>T2R: load dKV values
  T2R->>reduce_dKV: signal completion barrier
  reduce_dKV->>reduce_dKV: reduce register values
  reduce_dKV->>MMA: allow subsequent overwrite
Loading

Possibly related PRs

Suggested reviewers: anerudhan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main TMEM race fix in the SM100 DSA backward dKV drain.
Description check ✅ Passed The description follows the template and covers the required sections with concrete summary, rationale, compatibility, 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
@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!

@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
@zkyue

zkyue commented Jul 18, 2026

Copy link
Copy Markdown
Contributor 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

@Anerudhan

Copy link
Copy Markdown
Collaborator

Hi @zkyue , after I merged your PR #395 , I see there is a merge conflict. Can you please help resolve it , so that I can merge.

Thanks

zkyue and others added 3 commits July 20, 2026 07:06
…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.

Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
Co-authored-by: Jie Fang <jief@nvidia.com>
Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
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).

Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
@zkyue
zkyue force-pushed the fix/dsa-bwd-drain-ordering branch from 8578906 to b980ba7 Compare July 20, 2026 08:16
@zkyue

zkyue commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Anerudhan — done. I've rebased fix/dsa-bwd-drain-ordering onto the latest develop (with #395 merged) and resolved the conflict.

The conflict was a NamedBarrier id collision: #395 added tmem_dealloc_barrier at barrier_id=9, and this PR also introduced t2r_dKV23_done_barrier at barrier_id=9. I kept #395's barrier at id 9 and moved this PR's t2r_dKV23_done_barrier to the next free slot, barrier_id=10 — all barrier ids stay distinct and in range, and the WAR-race fix logic is otherwise unchanged.

Re-validated on SM100 for both head_dim 512 and 576: dKV/d_sink match both post-#395 develop and the pre-rebase tip within fp32-atomic noise (no NaNs), compute-sanitizer racecheck is clean, and the DSA backward tests match develop. Should be ready to merge on your next CI run — thanks!

@Anerudhan
Anerudhan merged commit 6c577ac into NVIDIA:develop Jul 20, 2026
1 check passed
@Anerudhan

Copy link
Copy Markdown
Collaborator

Thanks @zkyue merging.

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