FA4 consumer Blackwell (sm_120) integration — squashed history - #3
Open
thad0ctor wants to merge 100 commits into
Open
FA4 consumer Blackwell (sm_120) integration — squashed history#3thad0ctor wants to merge 100 commits into
thad0ctor wants to merge 100 commits into
Conversation
`FlashAttentionForwardSm80.__call__` sets
self.use_tma_O = self.arch >= Arch.sm_90
but the base class kernel never constructs a TMA atom for O — it
passes None as `tma_atom_O` to `self.epilogue` (line ~1069). The check
exists because the file once intended to support a Hopper-style TMA-O
path that was never wired up here.
On Hopper / SM_100 hardware this is dead code because those archs use
their own forward classes (`FlashAttentionForwardSm90` /
`FlashAttentionForwardSm100`) with their own `__call__`. But
`FlashAttentionForwardSm120` inherits from this class, and
`FlashAttentionForwardBase.__init__` reads `self.arch` from the DSL,
which is `Arch.sm_120` on consumer Blackwell. The epilogue then takes
the TMA-output branch and crashes inside
`quack.copy_utils.tma_get_copy_fn` -> `cpasync.tma_partition` with
`AttributeError: 'NoneType' object has no attribute '_trait'`.
Static `arch = 80` on `FlashAttentionForwardSm120` was intended to
prevent this but is overwritten by `__init__`.
Force `use_tma_O = False` here; SM90 and SM100 are unaffected because
they have their own `__call__`.
Reproduced on RTX 5090 (SM_120, cuTeDSL 4.4.2, torch 2.10.0+cu128).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`FlashAttentionForwardSm80.epilogue` chose the rmem->smem store atom
via `get_smem_store_atom(self.arch.major*10 + self.arch.minor, ...)`,
which returns
- `CopyUniversalOp` for arch < 90 (or non-16-bit data), and
- `StMatrix8x8x16bOp(num_matrices=4)` (Hopper `stmatrix`) for
arch >= 90 on 16-bit data.
`stmatrix` is hardware-paired with WGMMA's output register layout. The
SM80 base class uses `mma.sync.aligned.m16n8k16` whose output register
layout is *not* what `stmatrix` consumes. With WGMMA-output the atom
permutes bytes from a fixed pattern of threads/registers; feeding it
SM80-MMA-output silently scrambles values across nearby register
lanes during the store.
On native SM_80 hardware this branch never fires because the DSL arch
is sm_80 < sm_90. The bug only surfaces when this class is reused on
SM_120 via `FlashAttentionForwardSm120`, where `self.arch` is read
from the DSL as `sm_120` and the >= 90 branch picks `stmatrix`.
Symptom: the kernel completes without error and returns the correct
output shape and a roughly correct output norm (each scrambled value
is replaced by a same-magnitude neighbour), but element-wise diffs vs
fp32 SDPA are 0.5-1.2 (non-causal) and 3.4-3.9 (causal), versus
SDPA-bf16's own ~0.003 and ~0.008. Determinism still holds and error
scales linearly with input magnitude — the precision/permutation
signature, not a logic bug.
Fix: pass a fixed `80` to `get_smem_store_atom` here so the SM80 base
class always takes the universal-copy path, matching its actual MMA
output layout.
Verified on RTX 5090 (SM_120, cuTeDSL 4.4.2, torch 2.10.0+cu128):
240/240 correctness configs pass against fp32 SDPA reference across
{fp16, bf16} x {causal, full} x B in {1,2} x S in {128..4096} x
{MHA, GQA, MQA} x D in {64, 128}, with max abs diff matching
SDPA-bf16's own (~0.012 worst case, ~0.0027 mean).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pack_gqa.compute_ptr` calls
utils.elem_pointer(tensor, ((h_idx, m_idx),))
with a tensor whose layout is supposed to keep a composite
`(qhead_per_kvhead, seqlen_q)` first mode (created by
`pack_gqa_layout`). The slice `mO[None, 0]` that lands in
`compute_ptr` is meant to preserve that compositeness so the rank-2
coord matches.
On SM_120 with `cuTeDSL==4.4.2` the slice collapses the composite
mode into a rank-1 layout. `cute.crd2idx` then refuses the rank-2
coord and raises at trace time with
unable to compute crd2idx with
'!cute.layout<"(?):(?{i64 div=8})">'
and '!cute.coord<"((?,?))">'
resulting in a `ValueError: Operation creation failed` before the
kernel can run. Every default-policy GQA / MQA shape on consumer
Blackwell hits this.
The non-packed GQA path is numerically identical (pack_gqa is a
perf-only optimization for the GQA Q-load / O-store), so flipping
the auto-default to False on SM_120 makes GQA / MQA work out of the
box while a deeper cuTeDSL fix is investigated. Explicit
`pack_gqa=True` from the caller is still honoured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add FlashAttentionForwardSm120Tma class that uses TMA (cp.async.bulk) for Q/K/V loads with 1 DMA warp + 4 MMA warps, enabling producer/consumer overlap via PipelineTmaAsync with mbarrier synchronization. Key design: - TMA-compatible SMEM swizzle: Swizzle(B, 4, 3) instead of (B, 3, 3) - KV double-buffering (kv_stages=2), 160 threads (5 warps), 99KB SMEM - All pipeline operations inlined in the mainloop (not delegated to a separate @cute.jit method), which avoids CuTe DSL compiler hangs when pipeline states flow through method boundaries - is_first=False with pre-reset softmax state eliminates the need for a compile-time is_first flag in the single-loop mainloop - Dispatch: TMA default for SM120 non-paged, non-varlen. Falls back to CpAsync for paged KV and varlen (TMA addressing constraints). Validated on SM121a (DGX Spark): - 8/8 configs pass: non-causal + causal, B=1/2, Sq=64/128/256, Sk=128/256/512, H=4/8, D=128 - All diffs 0.002-0.008 vs reference Contributed by Second Nature Computing (https://joinsecondnature.com) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Override self.arch = Arch.sm_80 after parent __init__ to prevent base class code paths from seeing the runtime arch (12.x) and enabling SM90+ features. The parent __init__ overwrites the class-level arch=80 attribute with the actual GPU arch. This was found by @2imi9 in Dao-AILab#2420 for the CpAsync kernel — same bug applies here. Add can_implement() check before TMA dispatch in interface.py so that configs exceeding SM120's 99KB SMEM (e.g. hdim=192 with kv_stages=2) fall back to the CpAsync kernel instead of failing at instantiation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Blake Ledden <blake@secondnaturecomputing.com>
The base FlashAttentionForwardSm80.__call__ and FlashAttentionForwardSm100.__call__ both keep `stream` as the final parameter, with a comment: "Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI)". cute.compile binds arguments positionally against the compile_args list in interface.py, which ends with `current_stream`. The TMA kernel had `stream` at position 7 (right after softmax_scale). On this branch alone the kernel still works as advertised, but the mismatch breaks when composed with PRs that append further positional arguments to the compile path — most visibly when combined with Dao-AILab#2348's paged-KV plumbing or Dao-AILab#2439's dropout seeds, where the extra positions push `current_stream` onto a parameter that no longer exists or has the wrong type. Aligning with the base-class convention is mechanical and preserves correctness in isolation: Validation on SM121a (DGX Spark GB10), causal ∈ {False, True}, dtype ∈ {bf16, fp16}, B=1 S=256 H=4 D=64: causal=False bf16: max_diff=0.0020 PASS causal=False fp16: max_diff=0.0002 PASS causal=True bf16: max_diff=0.0078 PASS causal=True fp16: max_diff=0.0010 PASS Signed-off-by: Blake Ledden <blake@secondnaturecomputing.com>
Block-sparse attention processes only the KV blocks specified by block_sparse_tensors rather than the full KV sequence. Two block types are supported: mask_blocks (partially masked, apply mask_mod per element) and full_blocks (fully unmasked, skip masking entirely). Design follows the same mma_one_n_block callback pattern as SM90/SM100. The SM80 base class gets a new mma_one_n_block_bs method (load K, load V, wait, GEMM QK, score_mod, mask, softmax, GEMM PV) and a corresponding run_block_sparse_mainloop_sm80 utility in block_sparse_utils.py that iterates mask blocks then full blocks, mirroring consume_block_sparse_loads. Key implementation details: - run_block_sparse_mainloop_sm80: iterate mask_blocks first (highest n), then full_blocks. First full block always gets mask_seqlen=True since it may be at a higher n position than any mask block. - mma_one_n_block_bs: no pipeline overlap (block address unknown ahead of time), load K then V with separate cp_async_wait_group(1)/wait_group(0). - SM120 inherits SM80 base class and gets block sparsity for free. - FlashAttentionForwardSm120.__init__ forces self.arch = Arch.sm_80 to prevent the SM80 epilogue from using TMA-O (which would crash on SM121a since tma_atom_O is None in this kernel variant). - SM120: num_splits clamped to 1 in interface.py (no SplitKV support yet). - Block sparsity assert removed from SM120 interface path. Validated on SM121a (DGX Spark GB10): - test_block_sparsity.py: 4621 passed, 40 skipped - causal/non-causal, various head dims and sequence lengths Contributed by Second Nature Computing (https://joinsecondnature.com) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline blocksparse_tensors[0]/[2] index access with the get_total_block_count() utility from block_sparse_utils.py. This keeps variable naming consistent with the rest of the block-sparse codebase (which unpacks by name, not by index position). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…b#2572) (Dao-AILab#2590) * Fix bwd postprocess 2CTA gating to include sm_11x The 2CTA gating in flash_bwd_postprocess.py used `arch // 10 == 10`, which only matches SM 10.x (B100/B200/B300) and misses SM 11.x (Thor). The rest of the codebase (e.g. interface.py:549, 563, 834) consistently gates Blackwell-family 2CTA features as `arch // 10 in [10, 11]`. Bring the two postprocess sites in line with that convention. Flagged by @jayhshah in Dao-AILab#2572 follow-up discussion. * Include sm_110 in interface.py Blackwell-family heuristics Three sites in interface.py gate Blackwell-family behavior using `arch // 10 == 10`, which appears inconsistent with the rest of the file's `arch // 10 in [10, 11]` convention (used at lines 549, 563, 834, 974, 1035, etc.): - L533: `q_stage` heuristic for Blackwell forward - L579: `use_dedicated_hd256_kernel` (forward) - L1335: `use_dedicated_hd256_kernel` (backward) The dispatch in `_flash_attn_fwd` already routes both sm_10x and sm_11x through the same `FlashAttentionForwardSm100` / MLA classes, so these gates likely should treat them the same. NOTE FOR REVIEWERS: I'm not certain these are all oversight vs. intentional SM100-only paths. If any of them is intentional, please flag so I can revert just that hunk. The FP8 assert at L480 is left untouched on purpose — its error message reads as deliberate. * Apply ruff format to flash_bwd_sm100.py Pre-existing format drift surfaced by pre-commit. Not in the cute_exclude pattern, so it gets auto-fixed when other files in flash_attn/cute/ are touched in the same commit chain.
* Use is_family_of for sm_90 and sm_103 arch checks Follow-up to Dao-AILab#2572 — apply the same is_family_of pattern to the two remaining range-style arch checks for consistency: - flash_fwd_sm90.py:69 (SM 9.x assert) - flash_fwd_sm100.py:195 (is_sm103 flag) Same semantic narrowing as Dao-AILab#2572: bare-base SMs (sm_90, sm_103) are excluded. These kernels rely on wgmma / UMMA / 2CTA paths that require the a/f PTX variant anyway, so bare-base targets could not compile. * Clarify is_sm103 forward-inclusive semantics is_family_of(sm_103f) also matches any future sm_10x with x > 3, not just sm_103a/f. This was raised in PR review (@ocss884) — adding an inline comment clarifying that this forward-inclusive behavior is intentional: the flag gates ex2 emulation, sm_103 (B300) has fast hardware ex2, and later Blackwell variants in the same family are assumed to inherit it. No code-behavior change.
* Bump aiter submodule commit Co-authored-by: sstamenk <170634954+sstamenk@users.noreply.github.com> * Bump aiter submodule to 3b2e6f48ce97e1d494e8b3f1af5c65f74e304b28 (#2) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sstamenk <170634954+sstamenk@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sstamenk <170634954+sstamenk@users.noreply.github.com>
…ao-AILab#2594) * Clamp kv_stage to avoid SMEM overflow for small head_dims on SM100 Fixes Dao-AILab#2591. The unbounded formula at flash_fwd_sm100.py:335 ignores per-stage state (mbarriers, sScale, pipeline counters) and yields kv_stage values that overflow the sm_100a 227 KB SMEM cap when head_dim_padded=16 (head_dim in {8, ..., 16}). Repro: hd=8/16 + seqlen >= 256 + bf16 fails with cudaErrorInvalidValue ("launch shared memory exceeds current GPU arch sm_100a allowed. Allocated: 233472 bytes. Max: 232448 bytes."). Clamp kv_stage at 32. Surgical to the broken case: the unbounded formula maxes at 26 stages for head_dim_padded >= 32, and the 2CTA gate at interface.py:572 restricts 2CTA to hd_padded in {128, 192} (both no-op), so the clamp only fires at hd_padded in {8, 16}. Verified across 24 configs (hd in {8,16,32,64,96,128} x causal in {T,F} x seqlen in {128,2048}) on B200 with max_err vs torch SDPA <= 0.0078. * Add test_flash_attn_small_head_dim regression test The main test_flash_attn_output parametrizes d over {64, 96, 128, 192, 256} and never exercises head_dim < 64, even though _validate_head_dims accepts head_dim >= 8 for sm_100/110. That coverage gap let the SMEM-overflow bug in Dao-AILab#2591 slip through. This focused test covers d in {8, 16, 32} x causal x seqlen in {128, 2048}. The seqlen=2048 cases push q_stage 1->2 (the actual bug trigger); the seqlen=128 cases also exercise the q_stage=1 boundary that fits on main today but is structurally adjacent. d=32 serves as a canary against any future tighter kv_stage clamp regressing it.
…#2595) apply_exp2_convert selected the exp2 implementation based on mask_fn presence: hardware ex2.approx.ftz for causal-masked tiles, polynomial emulation for unmasked tiles. Different q_stage values (1 for decode, 2 for prefill) compute different m_block for the same logical Q row, shifting which tiles are processed with vs without mask_fn. The same K tile could receive different exp2 methods across variants. Fix: always pass self.ex2_emu_freq regardless of mask_fn presence. Add regression test for decode↔prefill bitwise consistency on MLA (192,128) shapes.
…AILab#2605) cutlass 4.5.2 is safe to update, and quack 0.5.0 has been published, so bump the FA4 (flash_attn/cute) requirement floors to match. Updates the dependencies and the cu13 extra in pyproject.toml, and the documented versions in CLAUDE.md. Verified on NVIDIA GB300 (SM100, CUDA 13.2): deps resolve cleanly (nvidia-cutlass-dsl 4.5.2 base+cu13, quack-kernels 0.5.0), imports OK, and a representative GPU sample of tests/cute/test_flash_attn.py passes (6 passed / 6 skipped / 0 failed across hd 64/96/128/192, causal, mha/gqa/mqa, fwd+bwd).
…ILab#2558) * refactor mla sm100 forward * add benchmark; address deprecation warnings; tweak ptx gemm dispatch * update interface and tests
…Lab#2617) v0.2.30 only ships URLs up to CUDA 13.1.0; bumping to v0.2.35 adds 13.1.1, 13.2.0, and the matching aarch64 SBSA installers. Signed-off-by: oliver könig <okoenig@nvidia.com>
* graph capture fix * rm env flag
stack-info: PR: Dao-AILab#2625, branch: drisspg/stack/42
ruff format flagged flash_attn/cute/flash_bwd_sm100.py (trailing whitespace in a comment and an over-split call). It was missed by the lint sweep in Dao-AILab#2625.
Passing weights_only=False (the pre-2.4 default) to torch.load allows arbitrary Python object deserialization from the checkpoint file. A malicious .pt/.pth file can execute arbitrary code on the machine loading it — a well-known PyTorch deserialization vector (CWE-502). Four call sites updated: training/src/utils/checkpoint.py load_checkpoint() training/src/eval.py eval checkpoint loader flash_attn/utils/pretrained.py partial(torch.load, ...) loader flash_attn/models/llama.py state_dicts_from_checkpoint() weights_only=True restricts deserialization to tensors, dicts, lists, tuples, and other primitive types — no arbitrary Python objects. Requires PyTorch >= 1.13; FA4's CuTeDSL dependency already requires a modern PyTorch 2.x build, so no compatibility regression. Fixes Dao-AILab#2583
…ressions (Dao-AILab#2616) stack-info: PR: Dao-AILab#2616, branch: drisspg/stack/41
…k-GQA, correctness fixes Builds on the SM120 forward foundation (TMA + cp.async paths): per-shape tile selection, LPT scheduling, SplitKV, paged-KV plumbing, pack-GQA folding (incl. the pack-gqa-aware SplitKV partial epilogue), and the forward correctness fixes (varlen guard, pack_gqa+SplitKV, block-sparse WAR race, strict is_sm120 gating, PDL/combine). All sm120-gated.
… dispatch arch==120-gated backward: dQ-accum scatter, pack-GQA M-split policy, sm120 postprocess, local-window fix. Real SM80/SM90/SM100 backward unchanged.
…rrectness fixes Local/sliding-window + block-sparse + mask_mod masking, block-sparse runtime utils, paged-KV manager, varlen seqlen clamps, softmax sink (sm120-gated), pack-GQA, shared utils. Shared-file changes are sm120-gated or behavior-identical for other archs.
Public-API dispatch for sm120: tile selection, backward M-split policy, SplitKV/paged routing, compile-cache keying, q_subtile_factor, and fp8 KV-cache decode routing with a cutlass-dsl version guard. sm120-only branches gated on arch//10==12.
Quantized-KV decode kernel: fp8 K/V cache (half the bytes) with per-(batch,head) descale, bf16 compute. Memory-bandwidth win for GQA decode; auto-routes when fp8 K/V is supplied.
* Fix CuTe SM120 compile-time argument handling * clean up * guard empty SM120 local backward tiles --------- Co-authored-by: Kevin-Li-2025 <2242139@qq.com> Co-authored-by: drisspg <drisspguessous@gmail.com>
…tKV fallback) (Dao-AILab#2656) * [CuTe,Fwd,sm120] Fix use_tma_O crash on SM120 (issue Dao-AILab#2649) On SM120 (Blackwell GeForce / RTX PRO 6000 / DGX Spark) the forward kernel set `use_tma_O = self.arch >= Arch.sm_90`, enabling the TMA-based O-store epilogue. But SM120 does not build the TMA store atom (tma_atom_O is None), so any forward call crashes in cpasync.tma_partition with: AttributeError: 'NoneType' object has no attribute '_trait' This makes the CuTe-DSL forward unusable on every SM120 GPU. Restrict the TMA O-store to sm_90..sm_119, which is where the WGMMA-era epilogue path is actually available: self.use_tma_O = Arch.sm_90 <= self.arch < Arch.sm_120 SM120 falls back to the non-TMA register->gmem O store (already used for the SM80 path), which is correct and what the CpAsync SM120 kernel expects. Verified on RTX PRO 6000 Blackwell (sm_120, cc 12.0), torch 2.12.0+cu130, nvidia-cutlass-dsl 4.5.2: forward now runs and matches PyTorch SDPA reference for hdim 64/96/128, causal and non-causal (max abs err <= 8e-3 in bf16). Before this fix every SM120 forward call raised the AttributeError above. * [CuTe,Fwd,sm120] Implement Pack-GQA on SM120; graceful SplitKV fallback Pack-GQA was only half-wired in the SM80/SM120 CpAsync forward: the epilogue referenced PackGQA.store_O/store_LSE, but the Q-load and head-indexing used the plain (unpacked) path. So pack_gqa=True crashed in pack_gqa.store_O (crd2idx on a packed (h_idx, m_idx) coordinate against an unpacked mO layout). This implements Pack-GQA end to end on SM120 (and SM80), mirroring the SM90 path: - Reshape mQ/mO (head_idx=2) and mLSE (head_idx=1) via pack_gqa_layout so qhead_per_kvhead folds into the seqlen mode ((qhead, seqlen)). - Scheduler args use cute.size(mQ.shape[0]) (packed total rows) and seqlen_q_static = mQ.shape[0][1] (logical seqlen), so causal/mask q_idx stay correct. - Kernel head-indexing: when pack_gqa, num_head from the scheduler already indexes the KV head (mQ/mK share nheads_kv); no division. - Q-load: gather rows via PackGQA.load_Q (per-row (h_idx, m_idx) gmem pointers) instead of the contiguous local_tile path. SplitKV (num_splits>1) is an SM100-only feature (SM80/SM90 also assert it unsupported); SM120 has no forward+combine path. Fall back to num_splits=1, which is numerically correct, instead of crashing in _check_type on the fp32 partials. Verified on RTX PRO 6000 Blackwell (sm_120): pack_gqa=True matches PyTorch SDPA GQA/MQA reference (err <= 8.4e-3 bf16) AND is bit-identical to the unpacked path (max |packed - unpacked| = 0.0) across MHA/GQA/MQA, causal/non-causal, hd 64/128, seqlen 512-2048. num_splits=3 falls back and matches reference (err 6.8e-4). Stacked on the SM120 use_tma_O fix (Dao-AILab#2649). * re-enable SM120 pack-gqa after rebase * clean up SM120 pack-gqa split handling * fix SM120 varlen pack-gqa offset --------- Co-authored-by: drisspg <drisspguessous@gmail.com>
stack-info: PR: Dao-AILab#2696, branch: drisspg/stack/47
…n is a tensor (Dao-AILab#2507) * Fix backward compile key instability when max_seqlen is a tensor When max_seqlen_q/max_seqlen_k are passed as torch.Tensor (e.g. by HuggingFace Transformers _prepare_from_posids), the arithmetic in _flash_attn_bwd produces tensor results that leak into the compile key tuple. Since pickle.dumps(torch.Tensor) produces a unique hash per object, every backward call generates a new compile key, causing infinite kernel recompilation and filling the persistent JIT cache with identical .o files. Cast max_seqlen_q/k to int() before they enter the seqlen_q/k computation path, ensuring the compile key contains only Python scalars. * Replace int() with host-scalar guard to avoid CPU-GPU sync
… symlink escape (Dao-AILab#2702) * hopper/setup.py: harden tarfile extraction against path traversal and symlink escape download_and_copy() extracted NVIDIA toolchain archives with a bare tarfile.extractall() into the predictable ~/.flashattn/nvidia/<name> cache, allowing arbitrary file write at build time via a pre-planted symlink or a malicious archive member (issue Dao-AILab#2637). - Add safe_extractall(): use the PEP 706 data filter when available (3.12, backported to 3.10.12/3.11.4), else fall back to per-member path containment and link rejection (stream-safe, single pass). - Refuse extraction into a symlinked cache path, closing the primary pre-planted-symlink vector on all Python versions. Signed-off-by: Aryan Putta <aryansputta@gmail.com> * hopper/setup.py: allow in-destination links in extractall fallback Address review on Dao-AILab#2702: 1. The no-data-filter fallback rejected every link member, which regressed real builds: the cuda_nvcc archives ship intra-package symlinks (e.g. libnvvm.so -> libnvvm.so.4) that the data filter permits. Allow links whose resolved target stays inside the extract dir instead, matching the data-filter behavior, and keep rejecting escaping and absolute-target links. 2. Harden the cache-path check: os.path.islink only inspects the leaf, so also require the fully resolved tmp_path to stay under the cache root, catching a symlinked parent directory. Signed-off-by: Aryan <aryansputta@gmail.com> --------- Signed-off-by: Aryan Putta <aryansputta@gmail.com> Signed-off-by: Aryan <aryansputta@gmail.com>
) The split-KV kernel (compute_attn_1rowblock_splitkv) indexes block_table[n_block * kBlockN / page_block_size], bounded only by actual_seqlen_k. In the kvcache path actual_seqlen_k is seqlens_k[b] + seqlen_knew, but block_table only has max_num_blocks_per_seq columns per sequence. If a caller passes a cache_seqlens (or appends new keys) exceeding max_num_blocks_per_seq * page_block_size, the kernel reads block_table out of bounds with no in-kernel check (see issue Dao-AILab#2709). Validate the caller contract host-side and raise a clear error instead. The .max().item() sync is only paid on the paged-KV path. Add test_flash_attn_kvcache_paged_block_table_bounds covering both the cache-length overflow and the appended-new-keys overflow, plus a positive control exactly at capacity. Co-authored-by: yunweili3 <yunweili3@users.noreply.github.com>
Resolve conflicts in 6 files. Pull out SM120 work now upstreamed independently; keep the more complete SM120 implementation where it is entangled with paged-KV / SplitKV / varlen-GQA that upstream's base path lacks. Pulled out (took upstream's version): - flash_fwd.py use_tma_O: replaced hardcoded False (my ead67d3) with upstream Dao-AILab#2656 'Arch.sm_90 <= self.arch < Arch.sm_120' (evaluates identically for SM80/SM120; re-added the dropped Arch import). - flash_bwd.py softmax_scale_log2: replaced my inline compute with upstream Dao-AILab#2671 'softmax_scale_log2, _ = compute_softmax_scale_log2(...)' (equivalent; discards the None-adjusted scale via _). Kept (my implementation; upstream's is a subset): - flash_fwd.py pack-GQA fold / num_head_kv / prologue: is_sm120-gated, carries paged-KV, SplitKV, and the varlen-GQA offset fix absent from upstream Dao-AILab#2656's base __call__. Upstream's cute.size() composite-shape wrapping auto-merged and is compatible. - flash_bwd.py local-window backward mainloop: my version handles pack_gqa packed-rows, pack_gqa_m_splits, r2p gating and the skip_full_causal_mask two-phase loop that upstream Dao-AILab#2671's BlockInfo.get_m_block_min_max path does not. - interface.py backward: adopted upstream Dao-AILab#2621 sparse-MLA (qv is not None) dispatch, re-added pack_gqa=ctx.pack_gqa to the dense _flash_attn_bwd calls. Complementary (both kept, no conflict): - interface.py max_seqlen: my host-int coercion (SM120 skip_full_causal const_expr) + upstream Dao-AILab#2507 single_block tensor guard fix different symptoms of the same root cause.
… call
The _flash_attn_bwd dispatch passed 'local' both as the 12th positional
arg AND as the is_local=local keyword to FlashAttentionBackward{Sm80,Sm120}.
Since __init__ takes is_local as a trailing keyword (position 11 is
is_causal, 12 is SdP_swapAB), the positional 'local' shifted every
following arg by one and collided with V_in_regs:
TypeError: __init__() got multiple values for argument 'V_in_regs'
so every SM120 backward call raised. This was latent in the branch (a
leftover from an earlier main-merge that added is_local positionally
upstream while this branch keeps it as a trailing kwarg); it only fires
when the backward actually runs. Drop the redundant positional; is_local
is still passed via the keyword.
Validated on RTX PRO 6000 (sm_120, torch 2.12+cu130, cutlass-dsl
4.6.0.dev0): fwd matches SDPA (maxerr<=4e-3 bf16) and bwd yields finite
gradients for dense / causal / GQA / D128 / local(causal+bidirectional).
The seqlen_q<=8 decode auto-split (interface.py:~1313) rewrites
num_splits 1->0 to request the SplitKV heuristic, but the SM120 guard
immediately did 'assert num_splits == 1', so every small-seqlen (decode)
SM120 forward crashed:
AssertionError: SM120 forward only supports num_splits=1
Route the auto sentinel (num_splits < 1) through num_splits_heuristic on
SM120 as well; it returns 1 when the grid is already filled and >1 only
for underfilled decode/small-batch shapes, so the SplitKV forward path
runs only where it helps. An explicit user-requested num_splits > 1 is
still rejected (test_flash_attn_sm120_rejects_splitkv stays green).
This was latent in the branch (the auto-split predates this guard);
it only fires for seqlen_q<=8, which is why the seqlen>=64 suite was green.
Validated on RTX PRO 6000 (sm_120): decode S=1/3/4 MHA+GQA match SDPA
(maxerr 0); explicit num_splits=3 still raises.
…void P saturation (Dao-AILab#2717) * [CuTe, SM100] Make FP8 max_offset dtype-aware to avoid e4m3 P saturation With rescale_threshold=4 the online-softmax row max can be stale by up to 4 (in log2 units), so P reaches 2^(max_offset + 4). max_offset=8 puts that at 4096, past e4m3fn's 448 ceiling: the largest probabilities saturate on the f32->fp8 satfinite convert and e4m3 accuracy degrades below e5m2 (up to 1.6x worse rel_l2, growing with seqlen). Cap max_offset at 4 for e4m3 so the worst case is 2^8 = 256 <= 448; e5m2 keeps 8 (57344 ceiling absorbs the overshoot). B200: restores e4m3 to ~2x lower error than e5m2 across seqlen 256-4096, uniform and peaked softmax, matching quantization-only emulation; LSE consistent; fwd timing unchanged (0.387 vs 0.390 ms, hd128 s4096). Related: Dao-AILab#2716 * [CuTe, Tests] Unrot the FP8 dtype path in test_flash_attn_output Running the suite with dtype=float8_e4m3fn has bit-rotted: - the test sets requires_grad on fp8 tensors, which the interface now rejects (FP8 is forward-only); gate it on non-fp8 dtypes. - it generates random descales and applies them in attention_ref, but the flash_attn_func call site has no descale kwargs (only _flash_attn_fwd takes them), so kernel and reference disagreed by construction; stop generating them. With these, the fp8 sweep runs cleanly (378 cases on SM100 with the e4m3 max_offset fix; 190 of them fail without it). fp8 stays out of the default dtype parametrize. Related: Dao-AILab#2716
* allow varlen score mod in backward; add tests and examples * add recompute fastdiv_mods to sm90 bwd * remove softcap != 0 limitation in test * fix linter error * guard use 2cta against softcap in bwd * undo formatting in test_flash_attn.py * reset test_flash_attn * update tests for score mod varlen bwd, guard blocksparse varlen bwd * aux_tensors -> aux_data; unpack args in test * aux_tensors -> aux_data for sm90 backward * make_fragment -> make_rmem_tensor in score_mod_definitions * predicate on aux_data.tensors, not aux_data * relax test tolerance in vectorized score mod tests - bitwise equality failing on sm103 though within tolerance * revert erroneous test reformatting to main
Dao-AILab#2669) * Expand FLASHATTENTION_DISABLE_DROPOUT to not bring in unneeded headers Summary: Previously, using the FLASHATTENTION_DISABLE_DROPOUT flag still pulled in unneed dependencies from ATen for at::Generator and Philox related headers. This change sets up the codebase so that using the flag will not pull in these unnecessary headers. There are two major changes of note: 1. We remove needing an RNG gen in the schema--the Python frontend always passed in None so this should not be BC breaking to most users. 2. Instead of referencing the PhiloxState directly, in order to detach dependencies when dropout is not needed, we introduce an opaque buffer that will hold the philox state when dropout is desired. Test Plan: pytest tests/test_flash_attn.py::test_flash_attn_output -k "113-203-64 and dtype0 and mha" pytest tests/test_flash_attn.py::test_flash_attn_varlen_output -k "113-203-64 and dtype0 and mha" g++ -c -O1 -std=c++17 -D_GLIBCXX_USE_CXX11_ABI=1 <torch+cutlass+cuda -I flags> \ csrc/flash_attn/flash_api.cpp -o /tmp/fa.o nm -C /tmp/fa.o | grep -E 'mha_(fwd|bwd|varlen)\(' | grep -c Generator returns 0 g++ -E -DFLASHATTENTION_DISABLE_DROPOUT <same -I flags> csrc/flash_attn/flash_api.cpp \ | grep -c 'CUDAGeneratorImpl.h\|philox_unpack.cuh' also returns 0 Reviewers: Subscribers: Tasks: Tags: Add trivially copyable assert Add back gen * use mark.skipIf
…Lab#2692) * fix combine kernel bug for full cudagraph * linearzie kernel
stack-info: PR: Dao-AILab#2731, branch: drisspg/stack/49
…Lab#2559) * add dynamicpersistentvarlenscheduler to flash_fwd_sm100 and prepare kernel * mild refactor to tile scheduler protocol, guard num_m_blocks_ptr for sm100, update tests to use scheduler metadata * rename varlen_batch_idx -> virtual_batch_idx, because it is relevant for non-varlen blocksparse batch sorting * split out VarlenSchedulerBase to share code between SingleTile and DynamicPersistent schedulers * add benchmark script for varlen dynamic persistent scheduler * minor clean up * updates to has_work logic, tile scheduler selection, and varlen test suite * fix tile scheduler dispatch logic * integrate binary batch search for single tile varlen * refactor tile scheduler for compositionality * work PR 2520 into interface and kernels * fix linter errors * wip: modify scheduler metadata public api * clean up scheduler metadata API; add docstrings; split out _get_fwd_config method; remove cluster_size==1 restriction; guard architectures against unused scheduler metadata args * address driss' comments * fix compute_tile_cumsum guards in interface * simplfiy benchmark, guard against _compute_tile_cumsum with small batch size * fix compute_tile_cumsum guard * update to 4.6.0 * fix linter error * fix rebase bug * add cu_blocks_kernel to replace _compute_tile_cumsum * fix linter error * address comments on PR * add seqlen_k_per_split, add single tile varlen scheduler to combine kernel * add blocks to batch idx O(1) lookup path to varlen scheduler * fix lint errors * various legibility improvements and bug fixes
* add methodology doc * revise method * concision pass
…ces (Dao-AILab#2755) * [CuTe,Sm100] Sparse MLA bwd: skip dK/dV scatter at -1 sentinel indices The sparse-MLA (gather_kv_indices) backward scatter epilogues atomically accumulated dV/dK at row indices read straight from gather_kv_indices with no validity guard, while every load path treats -1 (the documented sentinel for invalid top-k slots, which any causal top-k index tensor contains as padding) as invalid and predicates the gather. The masked math is correct -- p/dS are exactly 0.0 at sentinel slots -- but the atomic itself is destructive: index -1 addresses one row before the (batch-sliced) buffer base, and red.add.f32 flushes subnormal destinations to +0.0 and canonicalizes NaN payloads even when the addend is 0.0. For batch 0 this lands out of bounds in whatever tensor the caching allocator placed before dv/dk (silently corrupting e.g. int32 tensors, whose small values are all subnormal fp32 bit patterns); for later batches it lands in the previous batch's last row. Symptoms depend purely on allocation layout: bitwise-correct results, silently wrong grads, or IMA. Fix: skip the atomic when the index is negative, mirroring the load-side guard. The skipped contribution is mathematically 0.0, so numerics for valid slots are unchanged. Also fix _flash_attn_bwd_sparse_mla discarding caller-supplied dq=/dk= buffers (dq = dk = None right after recording prealloc_dq/dk, after which the reallocation is skipped because prealloc is set, so passing dq=/dk= crashed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [CuTe,Sm100] Test sparse MLA bwd with -1-padded gather_kv_indices Adds test_flash_attn_mla_sparse_bwd_sentinel and a varlen counterpart (causal x shared_kv, seqlen 512/1024 non-varlen, packed docs [512, 4, 1024] varlen): builds causal top-k indices with -1 tail padding, checks out/lse/grads against attention_ref through the public autograd path, then reruns the backward with preallocated dk/dv buffers surrounded by int32 canaries (values 1..N, all subnormal fp32 bit patterns, so one misdirected red.add.f32 -- even of +0.0 -- flushes them to zero) and asserts the canaries are untouched. The varlen kernels are separate compile-time specializations, and the dK epilogue guard must apply to the doc-relative index before seqlen_k_offset is added; the varlen test pins that down (doc 0's row -1 is the canary-visible case) and includes a doc shorter than topk_len whose index rows are almost entirely sentinels. Fails deterministically without the sentinel-scatter guard; existing sparse-MLA tests never hit the bug because they generate gather_kv_indices as full argsort permutations with no -1 slots. Also makes attention_ref's top-k mask sentinel-aware: its scatter_ used to route -1 indices into key 0 (unmasking it) and trip scatter's bounds check; out-of-range indices now also map to the dummy column, and the mask applies regardless of topk_len vs seqlen_k (equivalent for permutation indices). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…, matching the dense path (Dao-AILab#2756) Co-authored-by: qaf <qaf@example.org>
…ab#2706) * Support learnable sink backward * Simplify learnable sink backward plumbing * Format learnable sink postprocess * Support learnable sink with hd256 and frozen QKV * Format standalone sink reduction * Tighten learnable sink backward checks * Keep learnable sink scope lean * Remove standalone learnable sink varlen test * Always test learnable sink backward * Simplify learnable sink test setup * Detach learnable sink test tensors * Use Tuple return type for backward * Remove sink backward tensor wrapper * Clarify sink reduction CTA selection * Refine learnable sink backward handling * Handle sink-only rows in backward * Address learnable sink review feedback * Separate learnable sink dtype coverage * Use dtype-aware learnable sink gradient tolerance * Handle empty learnable sink backward * Address learnable sink follow-up feedback
…ao-AILab#2705) * [CuTe, FA4] Preserve first-tile flag during scheduler reconstruction * Pin nvidia-cutlass-dsl to 4.7.0 * Keep scheduler fix separate from DSL upgrade
…Dao-AILab#2761) When scheduler metadata provides per-batch dynamic num_splits, the varlen tile schedulers pack num_splits into the top 16 bits of split_idx. The dense path unpacks it inside BlockInfo.get_n_block_min_max, but the SM100 block-sparse paths pass split_idx to the block-sparse helpers as is: the load and MMA warps passed the packed value, for which split_block_range yields an empty block range, while the softmax and correction warps passed the unpacked value, yielding a non-empty range. The warps then disagree on whether a tile has work, the softmax/correction/MMA mbarrier handshake never completes, and the kernel spins forever. Every varlen + block-sparse + SplitKV forward hangs this way. Also pass the dynamic per-batch num_splits (instead of the static maximum) to the block-sparse helpers so the split ranges cover the whole block list, matching the dense path.
Fix forward issues exposed by dynamic-shape and layout fuzzing. Canonicalize unaligned inputs, distinguish static broadcast and auxiliary tensor ABIs in the compile cache, and compile SplitKV combine optional operands exactly as called. Use target-SKU SM metadata during fake selection. Size aliased SM100 K/V shared memory for the larger staged layout and ceil-divide non-TMA paged-loader entries so partial row waves receive page pointers. Add one focused regression for each underlying bug.
PR Dao-AILab#2559 added static- and dynamic-persistent dispatch for varlen SM100 kernels, but test_clc_fuzz still required the pre-change single-tile scheduler, so the scheduler assertions failed before numerical validation could run. Validate each scheduler class against its scheduling mode, account for dynamic SplitKV dispatch, and update the two static fallback expectations. STATIC mode accepts both StaticPersistentTileScheduler and SingleTileVarlenScheduler for varlen because the dispatch depends on whether every batch fits in a single m-block. Verification: the full tests/cute/test_clc_fuzz.py suite passes on SM100 (B200, CC 10.0) — 189 passed — both on this branch's base (1cc7ff6) and cherry-picked onto c68c592. The GQA + SplitKV varlen cases were confirmed to still select the CLC SingleTileVarlenScheduler, matching the assertion precedence.
Brings the branch up to date with 35 upstream commits. 22 conflict hunks across 7 files. Redundant SM120 fixes dropped in favour of upstream's: - softmax.py: upstream Dao-AILab#2706 implements the all-masked-row (row_max == -inf) sink guard unconditionally and with a proper max-shift, so the is_sm120-gated version and its plumbing are removed (3 call sites in flash_fwd.py). - paged_kv.py: upstream landed the same page_entry_per_thread ceil-div. - interface.py: dropped the local maybe_contiguous, which was shadowing upstream's alignment-aware version everywhere in the file. Kept, with upstream's additions merged in: - flash_fwd.py / flash_bwd.py: TileSchedulerArguments gained cu_total_m_blocks alongside the existing is_split_kv. - flash_fwd_combine.py: upstream rewrote it for linearized scheduling (Dao-AILab#2692); the SM120 use_pdl gate (griddepcontrol.wait is illegal on sm_120) is re-applied on top. - bwd postprocess: compile key carries both upstream's cu_total_m_blocks / learnable_sink_dtype and the SM120 pack_gqa pair, in signature order. Forward config refactor: upstream extracted tile selection into _get_fwd_config(), now shared with the public get_scheduler_metadata(). The SM120 per-shape tuning moved into _get_fwd_config_sm120() behind an arch-12 branch, with an Sm120FwdContext for the extra inputs and FwdConfig carrying the SM120 outputs (num_threads, cp.async stages, Q-in-regs). Verified dispatch-identical to the pre-merge branch over 1080 shapes. Fixes required by the merge: - flash_fwd_sm120_tma.py: upstream added mCuTotalMBlocks/mCuTotalSplitsMBlocks to the arch 8/9/12 launch args, but the SM120 TMA kernel had no such parameters, so every d64 TMA forward failed to bind. Wired through to its SingleTileVarlenScheduler. - interface.py: current_stream moved into upstream's compile-miss block; the fp8 decode path compiles earlier and now makes its own. - interface.py: learnable-sink backward. Upstream Dao-AILab#2706 asserts SM90/SM100/110 whenever a sink is present, which the autograd backward always passes. That removed working SM120 behaviour: dSink is a pure side-output of the dQ postprocess and dq/dk/dv receive the sink only through LSE. The assert is now gated on compute_dsink, derived from needs_input_grad, so a frozen sink still backprops on SM120 and only a real dSink request errors out. Pre-existing bug fixed: _bwd_postprocess_dkv_sm120 unpacked 14 values from make_fake_bwd_tensors, which returns 15 (mScaleP). Tested on RTX PRO 6000 (sm_120): 2652 passed / 246 skipped across the SM120 suites, the combine suite and a 350-case random sample of test_flash_attn.py; forward and backward match SDPA for causal, GQA, D256, sliding-window, non-square and decode shapes.
Supersedes the compute_dsink approach in the merge commit. Same behaviour, but _flash_attn_bwd is now byte-identical to origin/main again: instead of teaching it to skip dSink, the autograd Functions simply don't hand it the sink when the sink is frozen. dSink is a pure side-output of the dQ postprocess (only SM90/SM100/SM110 implement the reduction); dq/dk/dv never read sink_tensors and already receive the sink's contribution through LSE. So passing learnable_sink=None for a frozen sink yields identical gradients while avoiding upstream Dao-AILab#2706's arch assert, which the autograd backward would otherwise trip on SM120 for any sink-using model. An actual dSink request still raises upstream's original error. Verified on RTX PRO 6000: frozen-sink fwd/bwd match SDPA (1.6e-3 / 3.9e-3, same as before), dSink request errors loudly, 256 passed / 246 skipped across the SM120 suites and the 350-case sample.
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.
FA4 consumer Blackwell (sm_120) integration
Adds FlashAttention-4 (CuTeDSL) support for consumer/workstation Blackwell — RTX 50-series / RTX PRO 6000 / DGX Spark (compute capability 12.x). Dispatch and tile selection are auto-tuned for the arch; no environment variables are required for normal use.
Background
Consumer/workstation Blackwell (
sm_120) ships 5th-gen tensor cores with new fp4/fp8 data types but — unlike datacenter Blackwell (sm_100) — keeps the Ampere-style warp-levelmma.syncprogramming model: no WGMMA, no tcgen05, no Tensor Memory (confirmed against the CUDA 13.x toolkit and ptxas SASS — the tensor-memory subsystem is absent in silicon, not just unexposed). So FA4'ssm_100path does not apply;sm_120reuses the SM80-base kernels (cp.async, plus a TMA + warp-specialized forward) and needs its own dispatch, tiling, and correctness work.This builds on the in-flight upstream FA4
sm_120kernels (Dao-AILab Dao-AILab#2553 / Dao-AILab#2349 / Dao-AILab#2389), which landed initial kernels but left public-API routing, correctness, and coverage gaps:sm_120paths via the public API, auto-tuned dispatchsm_120GQA shapessm_120correctlyhead_dim ≤ 256, validatedhead_dim > head_dim_vsm_120TMA hangsm_120byte-identical tomain); CodeRabbit follow-upsWhat's supported
score_mod/mask_mod; learnable sink. Head dims 64/96/128/192/256.seqlen_q == 1with a quantized K/V cache + bf16/fp16 query (pass fp8k/v+ per-(batch, kv_head)k_descale/v_descale). Auto-routes to a memory-efficient GEMV decode kernel; ~1.6–1.9× faster than bf16 at GQA ratio ≤ 4 while halving KV bandwidth, within ~2e-3 of an fp8-quantized reference.torch.compilecompatible: the FA4 (CuTe-DSL) public entry points are made opaque to TorchDynamo (graph-break → run eagerly) and tensormax_seqlenis coerced to a host int in the varlen backward, so full-modeltorch.compileworks for forward, backward, and variable-length with output bit-identical to eager — no symbolic/fake tensors leak into the DSLconst()path. Non-FA4 / non-sm120 kernels are behaviourally unchanged.Performance vs FA2 (and SDPA)
Two
sm_120parts: RTX 5090 (consumer, 170 SM, 32 GB) and RTX PRO 6000 Blackwell (workstation, 188 SM, 96 GB). bf16. In-process round-wise interleaved A/B — each round times the impls back-to-back under the same clock state, so the ratios are clock-robust; medians reported.Legend.
D= head dim ·B= batch size ·r= GQA ratio = query-heads per KV-head (r1/MHA= one KV head per query head;r8= 8 query heads share 1 KV head;MQA= all heads share 1) · dense = full attention, causal = causal mask · prefill =seqlen_q == seqlen_k, decode =seqlen_q == 1(one new query against a length-SkKV cache) · ratio = FA4 / FA2 throughput, >1 = FA4 faster (so1.15= FA4 15% faster,0.40= FA4 60% slower). Config labels are(Hq/Hkv). Short seqlens have higher run-to-run variance — read the trend.Prefill forward — per seqlen (B=1)
The headline is D256; D128 is the other common case. (Full matrix incl. D64/D96/MHA and B=2 in the collapsibles below.)
D256 GQA (16/2), FA4/FA2:
D128 GQA (32/4), FA4/FA2:
Geomean rollup (over seqlen 512–32k, FA4/FA2)
Full per-seqlen prefill matrix — RTX 5090 (all configs, B=1 & B=2, fwd+bwd, dense+causal)
Full per-seqlen prefill matrix — RTX PRO 6000 (all configs, B=1 & B=2, fwd+bwd, dense+causal)
Prefill vs PyTorch SDPA (best torch backend)
FA4 matches or beats the best PyTorch SDPA on prefill (FA4/SDPA, >1 = FA4 faster); on the 6000 D256 it leads by 14–18%:
Decode (
seqlen_q == 1)Decode is memory-bandwidth-bound, so absolute latency (not a FLOP ratio) is the meaningful metric. fp8 K/V also halves cache memory (1 byte/elem vs 2). Latency at
Sk=16384(µs, lower = better):Across the full decode sweep (batch 16/64/128 ×
Sk4k–64k): fp8 decode wins at GQA r2/r4 with batch ≥ 64 — geomean ~1.4–2.0× vs FA2, up to ~2.2× — and scales up with context length, while halving KV memory. It loses at GQA r8 (~0.3–0.5×: the packed GQA-decode GEMV becomes compute-bound) and at tiny-batch + short-context. bf16 decode is at parity for MHA/r2 and trails at r4/r8.PyTorch SDPA is not a viable GQA-decode baseline — it has no GQA-decode path, so it materializes the expanded K/V (17–34 GB here): where it fits it is 23–45× slower than FA4-fp8 (e.g. 101 ms vs 3 ms), and otherwise OOMs. FA4 is the only efficient GQA-decode path of the three.
Achieved throughput (prefill forward, TFLOP/s)
Algorithmic TFLOP/s = standard attention FLOPs ÷ measured time; the FLOP count is ncu-verified exact (the hardware tensor-instruction count matches the analytical formula — dense/backward exact, causal +0.4%).
FA4 forward sustains ~180–191 TF/s on the 5090 (~86–91% of its ~209 TF/s bf16 dense peak) and ~245–261 TF/s on the 6000.
Honest sub-parity regimes (so they aren't a surprise to a reviewer)
Correctness
Validated against PyTorch SDPA across the full
tests/cutematrix, which is green onsm_120(with documentedskips for the unsupported configs below). Everysm_120change is arch-gated: SM80 / SM90 / SM100 / SM110 are byte-identical tomain(statically audited across the diff; their dedicated kernels are untouched).Real bugs found and properly fixed during development (not merely guarded):
learnable_sink+ SplitKV double-counted the sink across splits → wrong forward output. Fixed: the sink is folded into LSE only in split 0, so it's counted exactly once.window_size=(None,-X)/(-X,None)) produced wrong dK/dV (empty/inverted m-block prune). Fixed: the prune is skipped per-side for negative bounds (forward was already correct).sm_120(flash_fwd_combine.py):arch>=90wrongly enabledgriddepcontrolon the sm_80-compiled target. Fixed to excludesm_120.TypeError(head_dim ≤ 128withoutmax_seqlen_q): a decode-tile guard compared aNoneseqlen before its short-circuit, breaking the entire general varlen forward (a regression from a decode-tile optimization). Fixed (guard reorder).split_idx) → NaN/garbage withcache_seqlens+num_splits > 1. Fixed (threadsplit_idx+ empty-split guard).mask_modforward had a write-after-read shared-memory race (no inter-block barrier on the single-stage K/V buffer) → nondeterministic wrong output at multi-tile seqlens. Fixed (barrier).q_subtile_factoronsm_120→ out-of-bounds read (wrong output + illegal memory access) when the sparse block exceeded the kernel tile. Fixed.Independently double-checked: every fix re-verified cross-device (RTX 5090 + RTX PRO 6000),
compute-sanitizerclean on the formerly-IMA paths, and the arch-gating re-audited end-to-end.Known limitations / floors on sm_120 (documented in
flash_attn/cute/README.md)qhead_per_kvhead==1) atseqlen ≥ 8192is ~0.95× FA2 — an intrinsic register/occupancy wall (255 regs/thread → 1 CTA/SM). GQA (the common case) is at parity or faster.learnable_sinkruns single-split (no decode SplitKV speedup); negative-offset windows are forward-only;deterministic=Truebackward is unsupported. (Proper fixes for the last items are follow-ups.)m16n8k32thread-layout mismatch).Testing
New sm_120 test coverage:
test_fp8_decode_sm120.py,test_flash_attn_sm120_local.py,test_paged_kv_sm120.py,test_flash_attn_sm120_dgtdv.py,test_flash_attn_bwd_sm120_pack_gqa.py,test_flash_attn_bwd_sm120_postprocess.py. Thetest_flash_attn.pysuite is green on sm_120 modulo documentedskips for the unsupported configs above.Summary by CodeRabbit
New Features
seqlen_q=1.Improvements
Documentation
Tests