[kernel] sm_100a CUDA backward for VSA block-sparse attention (blk64) - #1819
Conversation
…en blocks) Native GB200 (sm_100a) backward pairing with the sm_100a forward: one warp-specialized tcgen05 kernel per (batch, head, kv64 block) computing dK, dV and the dQ partials (fp32 accumulator, cp.reduce.async.bulk), plus a preprocess (Delta, Q^T, dO^T, dqaccum zero, exact-zero dK/dV rows for unselected kv blocks) and a postprocess (dQ unscramble + sm_scale). Consumes the forward's lse (Triton M format) and invert_indices' k2q metadata unchanged; returns (dq, dk, dv) in bf16 with the Triton backward's scaling. Wiring: with FASTVIDEO_VSA_SM100A=1 the autograd backward of the sm_100a forward op runs this kernel when block_sparse_attn_bwd_sm100a.is_supported passes (bf16, head_dim 128, 64-token blocks, even block count, sm_100a device, op built) and the Triton backward otherwise. No new environment variable. primitives.cuh gains only the backward's primitives (additions only; the forward's definitions and SASS are untouched). Built for sm_100a only: the kernel is validated on GB200, not yet on B300/GB300, so sm_103a devices keep the Triton backward. The H3 backend's grad gate is unchanged (separate PR). Semantics: per-row k2q counts may differ arbitrarily (one work item is one kv row; count 0 is skipped identically by every warp and gets zero dK/dV rows); variable_block_sizes masks padded kv rows per lane; is_supported reads no tensor contents. Work-item order: a device kernel sorts by list length from 1024 kv blocks per sequence on; below that a cached identity array is passed. Tests (GB200, extension built from this branch): tests/test_block_sparse_bwd_sm100a.py 15 passed (fp32 masked-dense autograd reference; ragged vbs, zero-count kv blocks, top-k 1/2/3/5/7, batch 2), tests/test_block_sparse_sm100a_dispatch.py 10 passed (sm_100a route vs all-Triton grads, ragged vbs, Triton backward monkeypatched to fail on a supported input), tests/test_block_sparse_sm100a.py 37 passed (forward regression). Perf (kernels only, same window, B=1 H=8 D=128, 25% density, fp32 accumulator, TFLOPS on selected blocks) Triton backward vs this kernel: 4k 198/381 (1.93x), 8k 313/575 (1.84x), 16k 389/715 (1.84x), 32k 433/794 (1.84x), 65k 448/821 (1.83x), 131k 452/736 (1.63x), 262k 452/784 (1.73x), 524k 437/771 (1.76x). FastVideo autograd path incl. invert_indices: 1.6-1.7x at Wan 480P/720P-like shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merge Protections🟠 1 of 1 protections blocking · waiting on 🤖 CI
🟠 PR merge requirementsWaiting for
Waiting checks:
|
SolitaryThinker
left a comment
There was a problem hiding this comment.
Correctness pass over the sm_100a backward. The math and the barrier/phase accounting look right to me (scaling matches the Triton backward, lse consumed correctly in M format, padded kv rows and the clamped tail quad contribute nothing, dqaccum scrambled layout is consistent between epilogue and postprocess, count-0 zeroing covers every row). Two issues below, one in the kernel and one in the binding.
|
|
||
| const uint32_t tmem_lane_base = (uint32_t)(lane_group * 32) << 16; | ||
| const uint32_t tmem_f32_offset = tmem_lane_base + (uint32_t)(col_half * HALF_COLS); | ||
| const uint32_t tmem_bf16x2_offset = tmem_lane_base + (uint32_t)(col_half * HALF_COLS / 2); |
There was a problem hiding this comment.
Unsynchronized TMEM write/read overlap between softmax warps of the same lane group
Each softmax warp loads its 64 fp32 columns of S^T from tmem_st + tmem_f32_offset, where tmem_f32_offset = tmem_lane_base + col_half * 64, and afterwards stores 32 bf16x2 columns of P^T to tmem_pt_bf16 + tmem_bf16x2_offset, where tmem_bf16x2_offset = tmem_lane_base + col_half * 32 (the store is at L653). Because tmem_pt_bf16 == tmem_st, the warp with col_half == 1 writes columns [32, 64) of its lane group, and those columns lie inside the fp32 range [0, 64) that the col_half == 0 warp of the same lane group is reading. For lane group 0 these are warps 4 and 8; there is no synchronization between warp 4's tcgen05.ld and warp 8's tcgen05.st. Both wait on full_bar_st and then proceed independently. If the col_half == 0 warp is delayed past the col_half == 1 warp's ld + wait::ld + exp2 loop + cvt + st sequence (a few hundred cycles), it reads bf16 P^T bit patterns where it expects fp32 S^T, and the corrupted values propagate into P^T, dS^T, dV, dK and the dQ partials for that work item.
The dS^T store at L681 has the same overlap: tmem_dst_bf16 == tmem_dpt, so the col_half == 1 warp's write to columns [32, 64) overlaps the col_half == 0 warp's fp32 dP^T load from columns [0, 64).
In practice both warps are released by the same mbarrier phase and the load is issued almost immediately, so the window is narrow and the tests pass. It is nevertheless a timing-dependent data race rather than a guaranteed ordering, and it is not covered by the existing barriers (full_bar_pt / full_bar_dst are arrived at only after the store). For comparison, the forward kernel does not have this problem because each warp's P overwrites only the S columns it has itself already loaded (p_tmem_addr = s_tmem_addr); the M=64 fold used here is what introduces the cross-warp overlap.
Suggested fix, which requires no additional synchronization: keep each warp's bf16 output inside the fp32 range it has already consumed.
const uint32_t tmem_bf16x2_offset = tmem_lane_base + (uint32_t)(col_half * HALF_COLS);and adjust the A-operand addressing in gemm35_dv_dk (L399) so that slot p is read from column 64 * p rather than 32 * p:
const uint32_t tmem_a = tmem_a_base + (uint32_t)(p * HALF_COLS + ki * BF16X2_COLS_PER_K16);As far as I can tell nothing else depends on the placement of the bf16 P^T / dS^T inside the 128-column st / dpt regions (the dQ GEMM overwrites tmem_dpt only after the dK GEMM has consumed dS^T, which is already ordered by MMA issue order), so these two edits should be sufficient. The alternative is a named barrier over the eight softmax warps between tcgen05_wait_ld() and the tcgen05_st, at the cost of two extra bar.sync per quad.
There was a problem hiding this comment.
Reproduced on GB200; the race is real and the column-offset fix closes it.
Method: I built the kernel from this PR's head (749a9c5) four ways with torch.utils.cpp_extension (nvcc 13.0, -gencode arch=compute_100a,code=sm_100a -DVSA_BHSD=true) and ran tests/test_block_sparse_bwd_sm100a.py against each build on a GB200 (hpc-rack-3-8, then hpc-rack-3-7), injecting the built module through block_sparse_attn_bwd_sm100a.set_extension.
- base: unmodified.
- delay: the only change is a ~1 ms
__nanosleeploop in thecol_half == 0softmax warps between thefull_bar_st/full_bar_dptwait and their fp32tcgen05.ld. Nothing about what is computed changes, only warp timing, so that thecol_half == 1warps' bf16tcgen05.stlands before the fp32 load is issued. - fixdelay: the same delay plus the two-line fix (bf16 offset
col_half * HALF_COLS; ts-MMA A operand atp * SUB_COLS_BF16 + ki * BF16X2_COLS_PER_K16). - fix: the fix alone.
Results (rel_max = max|got - ref| / max|ref| against the test's fp32 reference, case num_blocks=8, topk=4, heads=4, ragged=True):
| variant | PR test suite | dq rel_max | dk rel_max | dv rel_max |
|---|---|---|---|---|
| base | 15 passed | 4.5e-3 | 3.1e-3 | 5.3e-3 |
| delay | 13 failed, 2 passed | 9.5e-1 | 9.4e-1 | 6.3e-1 |
| fixdelay | 15 passed | 4.5e-3 | 3.1e-3 | 5.3e-3 |
| fix | 15 passed | 4.5e-3 | 3.1e-3 | 5.3e-3 |
The two tests that still pass under delay are the ones that check no numerics (test_invert_indices_torch_matches_q2k, test_unsupported_is_rejected). The corrupted values are finite rather than NaN, which is what one expects from bf16 P^T bit patterns being read as fp32 S^T. The fixed builds produce error statistics identical to base, and their dk/dv are bitwise equal to base on fixed inputs, so relocating the bf16 tiles does not change the arithmetic.
Under natural timing the window did not open: 6000 back-to-back runs of the unmodified kernel on fixed inputs gave zero dk/dv mismatches. So the current layout works today by scheduling margin rather than by ordering; the delay experiment shows the hardware itself provides no ordering between the two warps' TMEM accesses.
Performance of the fix: kernel-only timing (preprocess + main + postprocess), B=1, H=8, D=128, 25% density, medians of two interleaved passes of 25 iterations:
| S | base ms | fix ms | fix / base |
|---|---|---|---|
| 4096 | 0.142 | 0.140 | 0.991 |
| 8192 | 0.333 | 0.334 | 1.006 |
| 16384 | 1.000 | 0.996 | 0.996 |
| 32768 | 3.635 | 3.675 | 1.011 |
| 65536 | 14.586 | 14.516 | 0.995 |
| 131072 | 63.637 | 63.528 | 0.998 |
Neutral within noise, as expected: the fix changes only which TMEM columns hold the bf16 P^T and dS^T tiles, with identical instruction count, TMEM/SMEM traffic, MMA shapes and barrier structure. The alternative fix (a named barrier across the eight softmax warps between tcgen05_wait_ld() and the store) would put two extra bar.sync per quad on the softmax path, so the offset change is the one I would take.
One correction to my comment above: HALF_COLS is a local constexpr of the softmax branch and is not in scope inside gemm35_dv_dk; the A-operand line needs the namespace-scope constant of the same value, i.e.
const uint32_t tmem_a = tmem_a_base + (uint32_t)(p * SUB_COLS_BF16 + ki * BF16X2_COLS_PER_K16);(the compile fails with identifier "HALF_COLS" is undefined otherwise). The tmem_bf16x2_offset line is inside the softmax branch and can keep HALF_COLS.
There was a problem hiding this comment.
good catch. I fixed the issue on blk128, but missed blk64. made 3 commits. please test the latest version. 88319b7. The latest test shows perf stays the same, no change. Sorry, 88319b7 is not the latest commit. It is the commit that fixed the issue. Please test the latest, which has 2 more commits to remove the identity cache. Btw, ST_QBLOCK_COLS is the new "HALF_COLS" in the namespace scope.
| const int* identity_remap(int64_t items, const torch::Device& device) { | ||
| static std::vector<torch::Tensor> cache(64); |
There was a problem hiding this comment.
identity_remap cache is neither stream-safe nor thread-safe
The cached identity array is created with torch::arange on whatever stream is current at the first call for a given device and stored in a static per-device vector. Two issues follow:
-
When a call arrives with a larger
itemsthan the cached tensor, the old tensor is dropped and its memory is returned to the caching allocator, while a previously launched backward on a different stream may still be reading it throughworkitem_remap. The replacementarangekernel is likewise only ordered with respect to the allocating stream, so a launch on another stream can read the buffer before it has been filled. Autograd normally replays the backward on the forward's stream, so this is unlikely to bite in FastVideo's current use, but it is a latent cross-stream use-after-free / read-before-write. -
The static
std::vector<torch::Tensor>is read and written without a lock, so concurrent calls from multiple host threads (for example two backward passes on different devices driven from different threads) race on the cache.
Options: allocate the identity array per call (torch::arange(num_items, ...) on the current stream; below ORDER_MIN_KV_BLOCKS the array is at most a few hundred KB and the launch cost is negligible next to the preprocess kernel), or keep the cache but call c10::cuda::CUDACachingAllocator::recordStream on the tensor for the current stream and guard the vector with a mutex.
There was a problem hiding this comment.
This is a legacy issue of my previous version. I removed this identity cache and remap host call completely. Instead, we will pass nullptr as a signal for the identity assignment on the kernel side. Code is cleaner.
…de its own fp32 columns Review finding: the two softmax warps of a TMEM lane group split the 128-column S^T (and dP^T) tile into fp32 halves, but the col_half == 1 warp packed its bf16x2 P^T / dS^T into columns 32-63, inside the fp32 range the col_half == 0 warp was still loading. Nothing but the shared full_bar_st / full_bar_dpt wait ordered the two, so a delayed col_half == 0 warp could read bf16 pairs where it expected fp32 scores. Fix, no extra synchronization: a warp's bf16x2 overlay now starts at the same column as the 64 fp32 columns it loads (ST_QBLOCK_COLS = 64 per q64 block), so it only overwrites data it has itself consumed; the dV / dK TS MMAs read slot p's bf16 atoms from column p * ST_QBLOCK_COLS instead of 32 * p. The dQ GEMM already overwrote the dP^T tile only after the dK GEMM consumed dS^T (MMA issue order), unchanged. Verified: CPU-reference checks incl. odd top-k, ragged variable_block_sizes and zero-count kv blocks; tests/test_block_sparse_bwd_sm100a.py 15 passed, tests/test_block_sparse_sm100a_dispatch.py 10 passed, tests/test_block_sparse_sm100a.py 37 passed (extension rebuilt from this tree). Interleaved A/B vs the previous build: neutral (4k..131k within noise). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ptr remap = identity Review finding: the binding's per-device static cache of a torch::arange identity array was neither stream-safe (a larger call replaced the tensor while an earlier backward on another stream could still read it; the replacement arange was only ordered with its own stream) nor thread-safe (unguarded static std::vector). Fix: no identity array at all. decode_workitem reads `real_item_id = workitem_remap ? workitem_remap[workitem_id] : workitem_id`; the launch leaves workitem_remap nullptr below ORDER_MIN_KV_BLOCKS (one launch of every item, so the work id is the item id) and runs the order kernel into order_workspace from there on. Slice launches (>= 524288 tokens) always sit in the order-kernel regime and keep passing `work_remap + base`. block_sparse_bwd_supported requires the workspace only when the order kernel will run. The binding passes nullptr always and allocates the workspace from ORDER_MIN_KV_BLOCKS on, as before; identity_remap and the cache are gone. Verified: CPU-reference checks incl. odd top-k, ragged variable_block_sizes and zero-count kv blocks on the nullptr path; device order == host sort check at 1024 kv blocks; tests/test_block_sparse_bwd_sm100a.py 15 passed, tests/test_block_sparse_sm100a_dispatch.py 10 passed, tests/test_block_sparse_sm100a.py 37 passed (extension rebuilt from this tree). Interleaved A/B vs the previous build: neutral (4k..262k within +-1.2%). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The destination registers of tcgen05.ld are scoreboarded, so the consumers wait on their own; the four waits (dV/dK merge, S^T, dP^T and dQ loads) are removed, the fence stays. Same change as the reference kernel in books/. Verified: CPU-reference checks incl. ragged variable_block_sizes, zero-count kv blocks and STRESS_N=5 bitwise reruns; tests/test_block_sparse_bwd_sm100a.py 15 passed, tests/test_block_sparse_sm100a_dispatch.py 10 passed, tests/test_block_sparse_sm100a.py 37 passed (extension rebuilt from this tree). Interleaved A/B 4k..524k: -1.2..+0.9%, neutral. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
alexzms
left a comment
There was a problem hiding this comment.
Request changes. Two things.
1. Cannot reproduce "validated on GB200" on hpc-rack-1-12 (GB200, 152 SMs, driver 580.126.20, VBIOS 97.00.B9.00.99, torch 2.12.1+cu130). tests/test_block_sparse_bwd_sm100a.py fails 15/15 with cudaErrorIllegalAddress, deterministically, on both the PR head and 749a9c5, with nvcc 13.0.88 (same as Will's run) and 13.1.115, CLC on or off, wait::ld dropped or restored. The merged forward passes and is compute-sanitizer-clean in the same build on the same node, so it is not the toolchain or the harness. compute-sanitizer with -lineinfo:
Out-of-range shared or local address
at tcgen05_mma_ws_f16_ss_1sm_predicated(...) primitives.cuh:890 (gemm12_st_dpt)
by thread (384,0,0) (warp 12 = W_MMA)
All-zero k2q counts run fine and return zeros; the smallest non-trivial case (B=H=1, nb=2, one item with count 1) faults on the first .ws GEMM. Repro below. Please run this on a Blackwell node other than 3-7/3-8 before merging — CI has no Blackwell and does not collect top-level tests/, so nothing else will catch it.
2. d2c9d612 (drop tcgen05.wait::ld) should be reverted, at minimum for the dQ epilogue. It landed after the last review pass. In SASS the register consumers do scoreboard-wait on the LDTM barriers, so that part of the commit message holds — but the mbarrier.arrive on empty_bar_dq does not:
LDTM.x64 R68 <- tmem_dq wr=B4
LDTM.x64 R4 <- tmem_dq wr=B1
@P0 SYNCS.ARRIVE [empty_bar_dq] wait={} <- releases the MMA warp to overwrite tmem_dq
IMAD.SHL ... wait={B4} <- first wait on the loads, after the arrive
tcgen05.fence::before_thread_sync emits no instruction there. With the wait restored the arrive sits behind NOP wait={B1}. That is the same "works by scheduling margin" class as the TMEM race from the earlier review; CUTLASS and FA4 keep wait::ld between a TMEM load and the arrive for exactly this reason ("Need a fence bw TMEM_LOAD and arrive"), and your own A/B puts the cost at noise. tcgen05_wait_ld() in primitives.cuh is dead now, and books/ is not in the repo.
Minor: tests/jit_ext.py referenced in the binding docstring does not exist; work_remap + base on a null remap is pointer UB that only stays safe because ORDER_MIN_KV_BLOCKS*64 < CACHE_WAVE_MIN_SEQ_LEN — worth a static_assert; the nb >= 1024 order-kernel path has no unit test.
The two fixes from the earlier review (TMEM column offsets, nullptr remap) look right.
Repro
Standalone build, same as Will's (TORCH_CUDA_ARCH_LIST=10.0a):
from torch.utils.cpp_extension import load
m = load(name="vsa_bwd", sources=["jit_bind.cpp", "csrc/attention/block_sparse_bwd_sm100a.cu"],
extra_include_paths=["csrc/attention"], extra_ldflags=["-lcuda"],
extra_cuda_cflags=["-O3", "-std=c++20", "--use_fast_math", "--expt-extended-lambda",
"--expt-relaxed-constexpr", "-DVSA_BHSD=true"])
# jit_bind.cpp: declare block_sparse_sm100a_bwd and m.def() it.
from fastvideo_kernel import block_sparse_attn_bwd_sm100a as bwd
bwd.set_extension(m)Minimal case (CUDA_LAUNCH_BLOCKING=1 compute-sanitizer --tool memcheck python repro.py):
import torch
B, H, D, nb = 1, 1, 128, 2; S = nb * 64
q, k, v, go, o = (torch.randn(B, H, S, D, device="cuda", dtype=torch.bfloat16) for _ in range(5))
lse = torch.full((B, H, S), 8.0, device="cuda")
k2q = torch.zeros(B, H, nb, nb, dtype=torch.int32, device="cuda")
k2n = torch.tensor([1, 0], dtype=torch.int32, device="cuda").view(B, H, nb) # [0, 0] runs and returns zeros
vbs = torch.full((nb,), 64, dtype=torch.int32, device="cuda")
dq, dk, dv = bwd.block_sparse_attn_backward_sm100a_from_k2q(go, q, k, v, o, lse, k2q, k2n, vbs)
torch.cuda.synchronize()…ackward Reverts the effect of d2c9d61. tcgen05.ld is asynchronous and PTX requires tcgen05.wait::ld before its destination registers are used. ptxas does scoreboard the register consumers, so three of the four sites were safe in practice, but the dQ epilogue arrives on empty_bar_dq right after its two loads and before any consumer: in SASS the SYNCS.ARRIVE carries an empty wait mask and precedes the first wait on the loads' barriers (B3/B1). That hands tmem_dq to the MMA warp while the loads may still be reading it; the next GEMM overwrites the same columns (tmem_dq aliases tmem_dpt), and the mixed registers are reduce-added into dqaccum as a valid partial. With the wait restored ptxas emits `NOP wait={B1}` ahead of the arrive. Measured cost on GB200: none (0.4-0.9% in favour of the build with waits, within noise); outputs bitwise identical where the race does not fire. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4KbxfFobc4FUXLZV3H4QF
…nsion docstring The identity work order (nullptr remap) cannot be offset per chunk, so the DQ_L2_KEEP chunked launches must only run with a device-computed order. Today that holds because the L2 transition (256K tokens) lies above the order-kernel threshold (64K), but nothing tied the two constants together; a static_assert now does, plus a runtime guard for external callers of the launch API. Also drops the reference to a tests/jit_ext.py that does not exist in the repo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4KbxfFobc4FUXLZV3H4QF
The existing backward tests stop at 16 kv blocks. The TMEM race fixed in 88319b7 never fired there but corrupted dk/dv on every rerun from 32K tokens, and the nb >= 1024 device-computed work order had no test at all. This case (1024 kv blocks, 12.5% density) covers both: sm_100a route vs all-Triton within the existing tolerances, and two sm_100a runs within summation-order noise (invert_indices orders each kv row's q list with atomics, so exact bitwise equality is not available through this path; measured delta 5e-3 rel_max / 7e-6 mean, the race gave 0.6-1.0 / 4e-2). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4KbxfFobc4FUXLZV3H4QF
|
Follow-up on @alexzms's review, with the SASS evidence for point 2, a cross-rack attempt at point 1, and a branch carrying the fixes. Point 2 (
|
…or bytes; order kernel whenever the launch is chunked CACHE_WAVE_MIN_SEQ_LEN (524288) was the f16-accumulator transition in tokens, compared as S * sizeof(dq_accum_t) >= 2 * it. It is now L2_RESIDENT_DQ_ACCUM_BYTES_PER_HEAD = 128 MiB, one head's accumulator S x HEAD_DIM x sizeof(dq_accum_t) (about the GB200 L2), compared directly; the same S as before for either dtype. The order kernel now also runs whenever the launch is chunked (nb >= ORDER_MIN_KV_BLOCKS || keep_dq_l2) -- in the launch, block_sparse_bwd_supported and the binding's workspace allocation -- so a chunk can never be handed a null order by construction; the static_assert stays as a guard. No behaviour change at the shipped constants; the kernel is untouched. Verified: extension rebuilt from this tree, tests/test_block_sparse_bwd_sm100a.py 15 passed, tests/test_block_sparse_sm100a_dispatch.py 11 passed, tests/test_block_sparse_sm100a.py 37 passed; standalone bench CPU-reference checks incl. ragged, zero-count kv blocks and the order-kernel path (1024 kv blocks). Interleaved A/B vs the previous kernel build: neutral. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…KS; shorter comments The static_assert already implies that a chunked launch has the device order (the L2 transition sits at or above the order-kernel threshold), so the `|| keep_dq_l2` order gate added in c3b05a8 was dead code; the launch, block_sparse_bwd_supported and the binding use the original predicate again. Comments around the two thresholds cut to one line each. No behaviour change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_sequence_lengths now also runs 2 kv blocks (S = 128; one quad tail-clamped to the two real q blocks), topk clamped to the block count. Standalone bench checks at nb = 2 (topk 1/2, batch 2, ragged, all kv blocks empty) match the CPU reference; the suite is 16 passed and compute-sanitizer memcheck clean on GB200 (driver 580.178.04). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@SolitaryThinker @alexzms Cherry-picked the above 3 commits of vsa_bwd_fix branch. Added a test to cover nb=2 for "Cannot reproduce "validated on GB200" on hpc-rack-1-12". For "Point 1 (illegal address on hpc-rack-1-12): could not reproduce on four other GB200s", i ran your repro code on our racks. It passed. Give you our config:
I might need more info re: the error. Can you verify the code again? Thanks for your time! |
Summary
Adds the native GB200 (sm_100a) backward that pairs with the sm_100a block-sparse forward: one
warp-specialized tcgen05 kernel per (batch, head, kv64 block) computing dK, dV and the dQ
partials (dS^T K reduce-added into an fp32 accumulator), plus a preprocess (Delta, Q^T, dO^T,
dqaccum zero, exact-zero dK/dV rows for kv blocks no q block selects) and a postprocess (dQ
unscramble + sm_scale). It consumes the forward's
lse(Triton M format) and FastVideo'sinvert_indicesk2q metadata unchanged and returns(dq, dk, dv)in bf16 with the Tritonbackward's scaling.
With
FASTVIDEO_VSA_SM100A=1the autograd backward of the sm_100a forward op now runs thiskernel when
block_sparse_attn_bwd_sm100a.is_supportedpasses (bf16, head_dim 128, 64-tokenblocks, even block count, sm_100a device, op built) and the Triton backward otherwise. Same
selection rules as the forward, no new environment variable.
Semantics (the points raised on the forward review)
between rows. Count 0: every warp skips the item identically (no barrier traffic) and the
preprocess writes exact-zero dK/dV rows.
variable_block_sizesmasks padded kv rows per lane inside the P^T select. Padded q rows relyon FastVideo's zero
grad_othere, as the Triton backward does (no q-row mask).is_supportedreads no tensor contents.order into a per-call workspace; below that the binding passes a cached
torch.arangeidentity (one per device). No host sorting, no sync.
Scope
devices keep the Triton backward. Enabling it is a one-line gencode + guard change once tested
on B300/GB300 (separate PR).
opening it means switching H3 to the
need_lse=Trueop and is a separate PR.Tests (GB200, extension built from this branch with TORCH_CUDA_ARCH_LIST=10.0a)
Performance (GB200, 25% density, D=128; TFLOPS = 2.5 * 4 * D * selected-block pairs / time)
Kernels only, same window (GPU 0 solo, interleaved Triton/blk64/blk64/Triton per size), B=1
H=8, bit-identical inputs, fp32 dQ accumulator (the shipped configuration), warmup 5 / 25 timed
iterations, medians of the two passes. Triton = FastVideo's Triton backward kernels
(
tflops_kern); blk64 = this kernel (preprocess + main + postprocess):