Summary
On H100 (SM90), ffpa_attn_varlen_func(...).sum().backward() never returns for head_dim=512. The forward is fine and fast; only the backward hangs. It reproduces at every shape I tried, including one as small as total_q=128, num_heads=2, so it is not a size or tiling boundary.
The dense entry point ffpa_attn_func with an explicit TritonBackend(backward=True, enable_tma=True, persist_dkdv=True) works perfectly on the same machine, in the same process, at the same geometry — 29.2 ms fwd+bwd for 3x2048, 32q/4kv, D512, bf16, causal, which is ~8x faster than what we run today. So this is specifically the varlen path.
Environment
|
|
| ffpa-attn |
0.2.2 (latest on PyPI) |
| torch |
2.13.0+cu130 |
| triton |
3.7.1 |
| GPU |
NVIDIA H100 80GB HBM3, sm90 |
| python |
3.12 |
Your own test suite passes here unmodified: pytest tests/test_ffpa_bwd.py -k sm90 → 8 passed in 4.87 s. So this is not a broken install or an environment/version skew.
Minimal repro
import torch
from ffpa_attn import ffpa_attn_varlen_func
nseq, S, H, D = 1, 128, 2, 512 # smallest shape that reproduces
dt = torch.bfloat16
cu = torch.arange(0, (nseq + 1) * S, S, device="cuda", dtype=torch.int32)
q = torch.randn(nseq * S, H, D, device="cuda", dtype=dt, requires_grad=True)
k = torch.randn(nseq * S, H, D, device="cuda", dtype=dt, requires_grad=True)
v = torch.randn(nseq * S, H, D, device="cuda", dtype=dt, requires_grad=True)
o = ffpa_attn_varlen_func(q, k, v, cu, cu, S, S, causal=True)
torch.cuda.synchronize() # returns; 6.79 s cold (compile), 0.07 s warm
o.sum().backward()
torch.cuda.synchronize() # never returns
Shape sweep
Each shape in its own child process, killed at 120 s. head_dim=512, bf16, causal, H100.
| shape |
forward |
backward |
nseq=1 S=128 HQ=2 HKV=2 |
ok 6.79 s (cold compile) |
hang |
nseq=1 S=128 HQ=32 HKV=4 |
ok 0.07 s |
hang |
nseq=1 S=512 HQ=32 HKV=4 |
ok 0.07 s |
hang |
nseq=1 S=2048 HQ=32 HKV=4 |
ok 0.08 s |
hang |
nseq=2 S=2048 HQ=32 HKV=4 |
ok 0.09 s |
hang |
nseq=3 S=2048 HQ=32 HKV=4 |
ok 0.08 s |
hang |
Where it stops
Python stack, captured by a watchdog thread after 120 s (identical at every shape):
ffpa_attn/cute/__init__.py:919 _varlen_fwd_backward
dq, dk, dv = torch.ops.ffpa_attn._varlen_bwd_cute(
ffpa_attn/cute/__init__.py:828 _varlen_bwd_custom
ffpa_attn/cute/_ffpa_bwd_sm90.py:447 _ffpa_attn_backward_sm90
_call_with_tvm_ffi_current_stream( # <- the dQ launch
ffpa_attn/cute/_utils.py:165 _call_with_tvm_ffi_current_stream
return fn(*args)
nvidia_cutlass_dsl/python_packages/cutlass/cutlass_dsl/tvm_ffi_provider.py:629
Line 447 is the dQ launch, i.e. the dKdV launch at ~431 already returned. But since _call_with_tvm_ffi_current_stream does not synchronise (_utils.py:159-165), the launches are async and I cannot tell from this which of the two kernels is actually spinning — I have not yet run with CUDA_LAUNCH_BLOCKING=1 to disambiguate. Happy to, if useful.
GPU stays at 100% utilisation throughout.
What is ruled out
- Not compilation. The first forward compiles in 6.79 s and every later one takes 0.07 s, so compilation is cached and costs seconds. The backward got 120 s at every shape, including that tiny one.
- Not the compile cache. Reproduced with
FLASH_ATTENTION_CUTE_DSL_CACHE_DIR on fast local disk (an earlier attempt on a network mount had its own unrelated problem; this run did not).
- Not shape. See the sweep — the smallest shape hangs too.
- Not a version skew. Your sm90 backward tests pass here, 8/8.
- Not the call site, as far as I can tell:
ffpa_attn_varlen_func exposes no backward_backend, and functional.py:278-284 requires forward_backend == backward_backend. Since the varlen entry dispatches straight to _ffpa_attn_varlen_cute, its backward is pinned to CuTeDSL with no way to select the Triton backward that the dense tests exercise. If there is a supported way to steer this, I would be glad to be corrected.
Possibly relevant: test coverage of this path
I went looking for an existing test to run before filing, and could not find one for this combination. Across all 9 files in tests/, exactly one test exercises varlen together with a gradient: tests/test_ffpa_cute_sm80.py::test_sm80_cutedsl_varlen_autograd_matches_sdpa. It differs from the failing case on four axes:
|
that test |
this report |
| path |
SM80 |
SM90 |
| head_dim |
320 |
512 |
| causal |
no |
yes |
| heads |
2q/2kv (MHA) |
2q/2kv and 32q/4kv (GQA) — both hang |
tests/test_ffpa_bwd.py has 29 backward tests and 0 mentions of varlen; all 10 of its head_dim=512 backward tests pass an explicit backward_backend=TritonBackend(...).
So the SM90 varlen backward looks uncovered rather than regressed — which would explain why this survived a release. Mentioning it only because it might help you decide where to look; apologies if I have missed a test elsewhere.
Context
This is relevant to #276, where it is stated that "Hopper works great currently for packed/padded training because its backward is cu_seqlens-aware and compiles once". That does not match what I measure on H100 with head_dim=512 — the forward is indeed fast and compiles once, but the backward does not return. It is possible the difference is head_dim (320 vs 512) or causal; I have not yet isolated which, and would run whichever variant is most useful to you.
We were evaluating FFPA for packed-sequence RL training with Gemma-4's 512-dim global-attention layers, where it benchmarks ~8x faster than our current backend. Very happy to run further diagnostics on H100 — this is a machine we have access to.
Summary
On H100 (SM90),
ffpa_attn_varlen_func(...).sum().backward()never returns forhead_dim=512. The forward is fine and fast; only the backward hangs. It reproduces at every shape I tried, including one as small astotal_q=128, num_heads=2, so it is not a size or tiling boundary.The dense entry point
ffpa_attn_funcwith an explicitTritonBackend(backward=True, enable_tma=True, persist_dkdv=True)works perfectly on the same machine, in the same process, at the same geometry — 29.2 ms fwd+bwd for3x2048, 32q/4kv, D512, bf16, causal, which is ~8x faster than what we run today. So this is specifically the varlen path.Environment
Your own test suite passes here unmodified:
pytest tests/test_ffpa_bwd.py -k sm90→ 8 passed in 4.87 s. So this is not a broken install or an environment/version skew.Minimal repro
Shape sweep
Each shape in its own child process, killed at 120 s.
head_dim=512, bf16, causal, H100.nseq=1 S=128 HQ=2 HKV=2nseq=1 S=128 HQ=32 HKV=4nseq=1 S=512 HQ=32 HKV=4nseq=1 S=2048 HQ=32 HKV=4nseq=2 S=2048 HQ=32 HKV=4nseq=3 S=2048 HQ=32 HKV=4Where it stops
Python stack, captured by a watchdog thread after 120 s (identical at every shape):
Line 447 is the dQ launch, i.e. the dKdV launch at ~431 already returned. But since
_call_with_tvm_ffi_current_streamdoes not synchronise (_utils.py:159-165), the launches are async and I cannot tell from this which of the two kernels is actually spinning — I have not yet run withCUDA_LAUNCH_BLOCKING=1to disambiguate. Happy to, if useful.GPU stays at 100% utilisation throughout.
What is ruled out
FLASH_ATTENTION_CUTE_DSL_CACHE_DIRon fast local disk (an earlier attempt on a network mount had its own unrelated problem; this run did not).ffpa_attn_varlen_funcexposes nobackward_backend, andfunctional.py:278-284requiresforward_backend == backward_backend. Since the varlen entry dispatches straight to_ffpa_attn_varlen_cute, its backward is pinned to CuTeDSL with no way to select the Triton backward that the dense tests exercise. If there is a supported way to steer this, I would be glad to be corrected.Possibly relevant: test coverage of this path
I went looking for an existing test to run before filing, and could not find one for this combination. Across all 9 files in
tests/, exactly one test exercises varlen together with a gradient:tests/test_ffpa_cute_sm80.py::test_sm80_cutedsl_varlen_autograd_matches_sdpa. It differs from the failing case on four axes:tests/test_ffpa_bwd.pyhas 29 backward tests and 0 mentions ofvarlen; all 10 of itshead_dim=512backward tests pass an explicitbackward_backend=TritonBackend(...).So the SM90 varlen backward looks uncovered rather than regressed — which would explain why this survived a release. Mentioning it only because it might help you decide where to look; apologies if I have missed a test elsewhere.
Context
This is relevant to #276, where it is stated that "Hopper works great currently for packed/padded training because its backward is cu_seqlens-aware and compiles once". That does not match what I measure on H100 with
head_dim=512— the forward is indeed fast and compiles once, but the backward does not return. It is possible the difference ishead_dim(320 vs 512) orcausal; I have not yet isolated which, and would run whichever variant is most useful to you.We were evaluating FFPA for packed-sequence RL training with Gemma-4's 512-dim global-attention layers, where it benchmarks ~8x faster than our current backend. Very happy to run further diagnostics on H100 — this is a machine we have access to.