Add sparse MLA backward (DSA top-k), sliding-window attention + attention sink, and block-sparse MLA forward/backward for SM100 - #198
Open
kmswin1 wants to merge 3 commits into
Conversation
CUTLASS-3 warp-specialized backward for absorbed-MLA over per-token sparse top-k indices (d_qk=576, d_v=512), completing the training loop for the existing flash_mla_sparse_fwd. - FA3-style backward: dP = dO @ V^T, dS = P * (dP - sum(O*dO)), dQ += dS @ K, dK/dV scattered per selected token - D_QK=576 split across 3 cluster CTAs (192-col slices); D_V=512 in 4 chunks per K-tile; CTA = one Q-token iterating its top-k K-tiles - dK/dV via FP32 atomicAdd scatter into indices[k]; dQ accumulated into a caller-provided fp32 workspace via SM90_TMA_REDUCE_ADD - K/V gather via a new kerutils tma_gather4_cta_group_1_pipe intrinsic - optional fused kl_target emission (fuse_reducesum) for indexer losses - exposed as flash_mla.flash_mla_sparse_bwd (base-2 LSE contract)
Causal sliding-window attention (SWA), runtime-configurable via a FlashAttention-style window_size=(left, right) argument (right must be 0; (-1,-1) disables), on: - SM100 dense prefill fwd + bwd (MLA 192/128 and generic 128/128): CausalMask gains a window lower bound applied identically in fwd and bwd (keep key k for query q iff q - window_size < k <= q). The forward additionally skips K-tiles entirely below the window (get_trip_start shifts the TMA loads and the softmax cursor together), so a windowed prefill does proportionally less work instead of just masking. The backward uses the mask-only safe baseline (full causal trip range). - SM90 dense decode (split-KV MLA): per-row left-border masking, runtime no-op when the window is disabled. - window_size disabled is bit-identical to plain causal. Also adds a gpt-oss-style per-head attention sink for SWA layers (sink_bias on flash_attn_varlen_func, [h_q]): a value-less virtual key folded entirely into the existing kernels. The forward folds it into the softmax denominator at finalization (O and LSE come back sink-aware), and the backward accumulates d(sink) inside the existing sum_OdO preprocessing pass, so neither direction launches extra work. Decode reuses it via attn_sink on flash_mla_with_kvcache (post-rescale, matching the sparse path's attn_sink convention). Tests: tests/test_swa_correctness.py drives the existing tests/test_fmha_sm100.py fwd+bwd checker (which already models windowed attention in its SDPA reference) with window > 0; window == 0 is the plain-causal regression check. tests/test_attention_sink.py validates O/LSE/dQ/dK/dV/d_sink against a sink-column reference; tests/bench_swa_scaling.py and tests/bench_attention_sink_ablation.py cover perf.
…path) A matched fwd/bwd pair for the non-absorbed 192/128 MLA training shape (per-head K = nope128 + rope64, V = 128), forked from the dense MLA kernels: each query-block attends only its selected key-blocks (per Q-block selection q2k), giving O(s * selected) compute instead of O(s^2). Forward (block_sparse_prefill_fwd, q_block=256, kv_block=128): the contiguous K-tile loop becomes a per-Q-block walk over the selected K-blocks; only diagonal-region tiles apply the causal mask (selected blocks fully below the diagonal skip it). Emits O + LSE (base-e) in the dense fwd convention so it feeds the matched backward. Running it with a full-causal selection reproduces the stock dense kernel time to ~1%. Backward (block_sparse_prefill_bwd, q_block=64, kv_block=128): keeps the dense MLA bwd's KV-outer structure via a k2q reverse CSR (K-block -> attending Q-blocks, built and sorted on device): a K-block CTA iterates only the Q-blocks that selected it, accumulates dK/dV in TMEM and stores once -- no atomic scatter, bitwise-deterministic dK/dV. With k2q_row_ptr == nullptr it falls back to the dense path, so one kernel serves dense / dense+SWA / block-sparse / block-sparse+SWA. Composes with window_size and the per-head attention sink (d_sink accumulated in the existing sum_OdO pass). expand_block_selection reconciles the fwd (q_block=256) and bwd (q_block=64) granularities for an end-to-end training step: replicating a 256-block's selection to its four 64-sub-blocks yields the identical (q,k) attended set because both kernels mask causally per row. Numerics vs the autograd oracle (full-causal and scattered selections): fwd O/LSE cos 0.999998, bwd dQ/dK/dV cos 0.999997, dK/dV bitwise- deterministic across runs. Scaling benches (B200, sink+window selection, h=8/16): fwd 144 ms -> ~1.7 ms and bwd 1094 ms -> 18.7 ms at 256K vs the stock dense MLA kernels. Tests: tests/test_block_sparse_*.py (fwd/bwd correctness vs tests/ref_block_sparse_mla.py oracle, e2e train step, sink composition, inference parity) and tests/bench_block_sparse_*.py scaling benches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds three training-oriented kernel extensions, targeting SM100 (B200/B300) with the SM90 dense-decode path also covered. Commits are split per feature — happy to split into separate PRs if that is easier to review.
Sparse MLA backward (per-token top-k, DSA) — a CUTLASS-3 warp-specialized SM100 backward for absorbed MLA (d_qk=576, d_v=512), completing the training loop for the existing
flash_mla_sparse_fwd. FA3-style: D_QK split across 3 cluster CTAs (192-col slices), D_V in 4 chunks per K-tile; CTA = one Q-token iterating its top-k K-tiles; dK/dV scattered with FP32atomicAdd, dQ accumulated viaSM90_TMA_REDUCE_ADDinto a caller-provided fp32 workspace. Optionally emits a fusedkl_targetfor indexer losses. Exposed asflash_mla_sparse_bwd(base-2 LSE contract).Sliding-window attention (SWA) on dense MLA — runtime
window_size(FlashAttention-style(left, right), causal left window,rightmust be 0,(-1,-1)disables) threaded through SM100 dense prefill fwd+bwd (MLA 192/128 and generic 128/128) and the SM90 dense decode kernel. The mask predicate is applied identically in fwd and bwd so gradients stay consistent; the forward additionally skips K-tiles entirely below the window (get_trip_startshifts the TMA loads and softmax cursor together), so a windowed prefill does proportionally less work instead of just masking (~20x fewer K-tiles at 64K with a 128-wide window). Disabled is bit-identical to plain causal.Includes an optional gpt-oss-style per-head attention sink (
sink_biasonflash_attn_varlen_func,[h_q]): a value-less virtual key folded entirely into the existing kernels — forward folds it into the softmax denominator at finalization (O and LSE come back sink-aware), backward accumulatesd(sink)inside the existingsum_OdOpass. No extra launches, ~0% overhead. Decode reuses it viaattn_sinkonflash_mla_with_kvcache.Block-sparse MLA forward + KV-outer backward (non-absorbed 192/128 training shape) — a matched fwd/bwd pair forked from the dense MLA kernels where each query-block attends only its selected key-blocks (
q2k), giving O(s·selected) compute. The backward keeps the dense kernel's KV-outer structure via ak2qreverse CSR (K-block → attending Q-blocks): a K-block CTA iterates only the Q-blocks that selected it, accumulates dK/dV in TMEM and stores once — no atomic scatter, bitwise-deterministic dK/dV. Withk2q_row_ptr == nullptrit falls back to the dense path, so one kernel serves dense / dense+SWA / block-sparse / block-sparse+SWA.expand_block_selectionreconciles the fwd (q_block=256) and bwd (q_block=64) granularities exactly for an end-to-end training step.API additions
flash_mla_sparse_bwd(q, kv, o, do, indices, lse, dq_acc_ws, dkv_acc_ws, sm_scale, d_v, fuse_reducesum)window_size=(left, right)onflash_attn_varlen_func/flash_attn_varlen_{qkv,kv}packed_funcandflash_mla_with_kvcachesink_biasonflash_attn_varlen_func(autograd populates its grad);attn_sinkon denseflash_mla_with_kvcacheblock_sparse_prefill_fwd/block_sparse_prefill_bwd/expand_block_selectionBenchmarks (B200, 192/128 MLA)
Selection = global sink + 4-block window (~5 selected K-blocks ≈ 640 keys, constant in seqlen); dense is O(s²), block-sparse O(s·selected), so the speedup grows with context. Running the block-sparse kernel with a full-causal selection reproduces the stock dense time to ~1%, confirming the gain is the sparsity, not a kernel artifact.
Forward (
tests/bench_block_sparse_fwd_vs_dense.py, h=8):Backward (
tests/bench_block_sparse_bwd_scale.py, h=16):The benchmark uses a fixed sink+window pattern to show scaling behavior, not a claim that every workload sees ~80x; the value is contingent on a trained block-indexer picking the important K-blocks.
Numerics
tests/test_fmha_sm100.py(which already models windowed attention), fixed + varlen, MLA (192/128) and generic (128/128) shapes.Tests
tests/test_swa_correctness.py,tests/test_attention_sink.py,tests/test_block_sparse_{e2e,integrated,existing,fwd_192_integrated,fwd_inference,sink}.py,tests/ref_block_sparse_mla.py(oracle), plus scaling benches (tests/bench_swa_scaling.py,tests/bench_attention_sink_ablation.py,tests/bench_block_sparse_*.py,tests/bench_sparse_bwd_576.py).Notes for reviewers
flash_mla.cuda) viacsrc/api/api.cpp; the standalonePYBIND11_MODULEblocks in the pybind TUs are compiled out (#ifdef *_STANDALONE).