Repository navigation
[BUG] CuTe DSL 4.7/4.8 regression: FlashAttention-4 SM100 forward kernel spills registers (0 -> 176 B) vs 4.5.1, ~3% slower on B300 #3701
Description
Describe the bug
With identical kernel source, CuTe DSL 4.7.x/4.8.0 generates PTX for the FlashAttention-4 SM100 forward kernel (flash_attn.cute.flash_fwd_sm100.FlashAttentionForwardSm100) that spills registers, where CuTe DSL 4.5.1 generated PTX that does not. The kernel is at the 128-register cap in both cases (512 threads per CTA), so the extra pressure becomes local-memory spills. The varlen causal fwd+bwd step is about 2–3% slower on B300.
The regression is in the generated PTX, not in ptxas: the 4.8.0 PTX still spills when assembled with the older ptxas 13.2, while the 4.5.1 PTX assembled with that same ptxas does not.
Resource usage, same FA4 source (fa4-v4.0.0.beta16), same quack-kernels 0.4.1, sm_103a
| Kernel | CuTe DSL 4.5.1 | CuTe DSL 4.8.0 |
|---|---|---|
FlashAttentionForwardSm100 |
128 regs, 0 B stack, 0 spill stores/loads, 5272 SASS instr | 128 regs, 112 B stack, 176 B spill stores / 176 B spill loads (44 STL / 33 LDL), 5768 SASS instr |
FlashAttentionBackwardSm100 |
128 regs, 8 B stack, 1 STL / 5 LDL | 128 regs, 24 B stack, 24 B spill stores / 60 B spill loads (4 STL / 13 LDL) |
MMA (UTCHMMA/UTCQMMA), MUFU, and packed FP32 (FFMA2/FMUL2/FADD2) instruction counts in the forward kernel are identical between the two versions, so the difference is spill traffic rather than different math.
PTX vs ptxas cross-check (forward kernel PTX from CUTE_DSL_KEEP=ptx)
| PTX from | assembled with | Result |
|---|---|---|
| CuTe DSL 4.5.1 (PTX ISA 9.1) | ptxas 13.1 and 13.2 | 0 B stack, 0 B spills |
CuTe DSL 4.8.0 (PTX ISA 9.4, .version lowered to 9.2) |
ptxas 13.2 | 112 B stack, 176 B spill stores / 176 B spill loads |
Neither PTX contains .local declarations, so the spills are introduced by ptxas from register pressure in the 4.8.0 PTX.
CUTE_DSL_COMPILER_OPT="remarks{ptx}" with CUTE_DSL_LINEINFO=1 on 4.8.0 reports the spills for both forward and backward, and marks flash_attn/cute/tile_scheduler.py:923 (the varlen scheduler's _get_num_m_blocks, loading cu_seqlens) as the likely pressure region.
Timing (B300, varlen causal, bf16, head_dim 128, fwd+bwd, median of 20 steps after 3 warmups, runs interleaved on one GPU)
| Shape | CuTe DSL 4.5.1 | CuTe DSL 4.8.0 |
|---|---|---|
| 8192 tokens × 32 heads, 4 docs | 1.206–1.245 ms | 1.222–1.234 ms |
| 32768 tokens × 32 heads, 8 docs | 5.300–5.329 ms | 5.486–5.488 ms (+3%) |
| 32768 tokens × 16 heads, 1 doc | 12.35–12.38 ms | 12.71–12.84 ms (+3%) |
CuTe DSL 4.7.0 and 4.7.1 (measured with quack-kernels 0.6.5) show the same slowdown as 4.8.0. The current FA4 release (fa4-v4.0.0.beta32) with quack-kernels 0.6.5 on CuTe DSL 4.8.0 is within noise of beta16 on 4.8.0, so the difference tracks the DSL version, not the FA4 or quack version. I could not test 4.5.3 or 4.6.x with this FA4 source (import errors unrelated to the kernel), so the regression landed after 4.5.1 and by 4.7.0.
Steps/Code to reproduce bug
- Environment: NVIDIA B300 SXM6 (sm_103a), driver 590.48.01, PyTorch 2.11 (CUDA 13.0), Python 3.12.
- FA4 source:
Dao-AILab/flash-attentionat tagfa4-v4.0.0.beta16, installed withpip install --no-deps --no-build-isolation flash_attn/cute. - Install
quack-kernels==0.4.1(--no-deps) and one of:nvidia-cutlass-dsl==4.5.1withnvidia-cutlass-dsl-libs-cu13==4.5.1(apache-tvm-ffi 0.1.6)nvidia-cutlass-dsl==4.8.0withnvidia-cutlass-dsl-libs-cu13==4.8.0(apache-tvm-ffi 0.1.11)
- Run the script below with
CUTE_DSL_KEEP=ptx,cubin CUTE_DSL_DUMP_DIR=<dir>, thencuobjdump --dump-resource-usage <dir>/*flash_fwd_sm100*.cubin.
quack-kernels 0.4.1 annotates with cute.core.ThrMma/ThrCopy, which 4.8.0 exposes only on cute; the script aliases them so the same quack source imports on both DSL versions.
import importlib.metadata as md
import json
import sys
import torch
import cutlass.cute as cute
for _name in ("ThrMma", "ThrCopy", "TiledMma", "TiledCopy"):
if not hasattr(cute.core, _name) and hasattr(cute, _name):
setattr(cute.core, _name, getattr(cute, _name))
from flash_attn.cute.interface import flash_attn_varlen_func
def bench(total, heads, head_dim, docs):
torch.manual_seed(0)
q, k, v = (
torch.randn(total, heads, head_dim, device="cuda", dtype=torch.bfloat16, requires_grad=True) for _ in range(3)
)
cu = torch.linspace(0, total, docs + 1, device="cuda").round().to(torch.int32)
max_seqlen = int((cu[1:] - cu[:-1]).max())
def step():
out = flash_attn_varlen_func(
q, k, v, cu_seqlens_q=cu, cu_seqlens_k=cu, max_seqlen_q=max_seqlen, max_seqlen_k=max_seqlen, causal=True
)
out = out[0] if isinstance(out, tuple) else out
out.float().square().mean().backward()
for _ in range(3):
step()
torch.cuda.synchronize()
times = []
for _ in range(20):
start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
start.record()
step()
end.record()
torch.cuda.synchronize()
times.append(start.elapsed_time(end))
return round(sorted(times)[10], 3)
shapes = [(8192, 32, 128, 4), (32768, 32, 128, 8), (32768, 16, 128, 1)]
print(json.dumps({
"cutlass_dsl": md.version("nvidia-cutlass-dsl"),
"gpu": torch.cuda.get_device_name(),
"median_ms": {f"{t}x{h}h_d{d}_{n}docs": bench(t, h, d, n) for t, h, d, n in shapes},
}))Expected behavior
CuTe DSL 4.8.0 generates PTX for this unchanged kernel source with register pressure comparable to 4.5.1, so the forward kernel does not spill and runs at 4.5.1 speed. If this is a deliberate lowering change, guidance on a compile option or kernel-side change that avoids the spill would help.
Environment details
- GPU: NVIDIA B300 SXM6 (sm_103a), driver 590.48.01
- CuTe DSL: 4.5.1 (good), 4.7.0 / 4.7.1 / 4.8.0 (spills), CUDA 13 libs
- ptxas used in the cross-check: 13.1, 13.2
- PyTorch 2.11, CUDA 13.0, Python 3.12, Linux x86_64
- FlashAttention-4:
fa4-v4.0.0.beta16(control) andfa4-v4.0.0.beta32 - quack-kernels: 0.4.1 (control) and 0.6.5
Additional context
Default compile options are identical in both versions (opt-level=3, empty ptxas options, no line info, assertions off), and none of the new 4.8 environment variables were set.
Activity
LaelaZorana commentedon Oct 8, 2026
Managed to reproduce this with the forward kernel by compiling the PTX for each arch and checking the ptxas -v output. Used varlen causal bf16 with head dim 128 and 32 heads, 32768 tokens in 8 docs of 4096 each.
4.5.1 uses quack 0.4.1 with FA4 beta16, which seems to be your setup with 176 B.
4.8.0 uses quack 0.6.5 with FA4 beta32, but you mentioned that beta32 seems to have the same behaviour as beta16 with 4.8.0.
On sm_100a, both use exactly 128 regs and have 0 B of stack and 0 B of spill. On sm_103a, 4.5.1 uses 128 regs with 0 B spill but 4.8.0 has 112 B of stack and 156 B of spill stores and 152 B of spill loads. So for me, the spills only happen on sm_103a.
In the 4.5.1 PTX there are 89 ld.param instructions which load kernel args close to where they are used, whereas there are only 24 in the 4.8.0 PTX. 6 of them are on lines 45 to 53 so very close to the entry, and the remaining 18 are spread out between lines 192 and 6899. So it looks like the new version reloads the args much less often, which means that they stay live in registers for very long. This matches what you saw with the load of cu_seqlens in tile_scheduler.py on line 923, in the varlen scheduler. With such a long live range and a limit of 128 regs, it seems that ptxas spills regs, only on sm_103a from my side.
It would seem that the fix belongs in the DSL lowering and not in FA4, by reloading kernel params from param space at the point of use again. Does the sm_100a version of the same kernel spill for you? I can attach the 2 PTX files and the ptxas logs.
ptxas is 13.2.86 and is called with O3.