Skip to content

Commit 00338aa

Browse files
ita9naiwaHyunsung Leealexzms
authored
[perf] Add FA4 CuTe backward support for VSA-256 (#1639)
Co-authored-by: Hyunsung Lee <hyunsungl@sizigistudios.com> Co-authored-by: alexzms <3036648523@qq.com>
1 parent 8537dcd commit 00338aa

13 files changed

Lines changed: 931 additions & 142 deletions

File tree

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ ARG FLASH_ATTN_WHEEL_RELEASE_ARM64=https://github.com/mjun0812/flash-attention-p
6868
# cutlass-4.4 `cute.core.ThrMma` API, which crashes on the cutlass-dsl 4.5 that
6969
# flashinfer/quack pull in. After the wheel install we overlay this cutlass-4.5-safe
7070
# upstream cute (flash-attn-4) so the image runs FA4 instead of the FA2 fallback.
71-
ARG FA4_CUTE_REF=82d6441eec5d4dfec120153db2c0145ae855a083
71+
ARG FA4_CUTE_REF=14c377950125c70b7a9dabf9c561fca53715ac7d
7272

7373
# Provided automatically by BuildKit/buildx (e.g. "amd64" / "arm64") and used to
7474
# select the prebuilt flash-attn wheel. Empty under a plain `docker build` without

fastvideo-kernel/README.md

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Runtime-JIT kernels (no build step, ship in every wheel/image):
1717
| Kernels | Where | Used when |
1818
|---|---|---|
1919
| Triton: STA, VSA block-sparse, SLA, fused compress+topk, FP4 QAT training, quant/norm utils | `python/fastvideo_kernel/triton_kernels/` | automatic fallback when the matching C++ op is absent (`ops.py`, `turbodiffusion_ops.py`) |
20-
| FA4 CuTe-DSL block-sparse (VSA-256 fastpath on `sm_100`) | `block_sparse_attn_cute_fwd.py` | optional `flash_attn.cute` dependency, see below |
20+
| FA4 CuTe-DSL block-sparse forward/backward (VSA-128/256 fastpath on `sm_100`) | `block_sparse_attn_cute_fwd.py` | optional `flash_attn.cute` dependency, see below |
2121
| VMoBA `moba_attn_varlen` | `vmoba.py` | wraps flash-attn varlen |
2222

2323
## What gets built where, and when
@@ -64,28 +64,49 @@ cd fastvideo-kernel
6464
./build.sh --rocm
6565
```
6666

67-
### Optional: FA4 CuTe block-sparse backend (VSA-256 fastpath)
67+
### Optional: FA4 CuTe block-sparse backend (VSA-128/256 fastpath)
6868

69-
The VSA-256 fastpath (tile volume 256, on NVIDIA Blackwell / sm_100) routes to the
69+
The VSA-128/256 fastpaths (tile volume 128 or 256, on NVIDIA Blackwell / sm_100) route to the
7070
FlashAttention-4 CuTe-DSL block-sparse kernel exposed as `flash_attn.cute`. This is
7171
an **optional** dependency: it is imported lazily, and `video_sparse_attn`
7272
transparently falls back to the Triton backend when it is absent (so the package is
7373
fully usable without it).
7474

75-
The symbols the fastpath needs (`flash_attn.cute.block_sparsity.BlockSparseTensorsTorch`,
76-
`flash_attn.cute.interface._flash_attn_fwd`) are provided upstream by
75+
The symbols the fastpath needs (`flash_attn.cute.block_sparsity.BlockSparseTensorsTorch`
76+
and the public/private forward-backward bridges in `flash_attn.cute.interface`) are provided upstream by
7777
[Dao-AILab/flash-attention](https://github.com/Dao-AILab/flash-attention). Pin to
78-
commit `940cd9680f3315f2f06b43ab5bea2c2cf2d96806`, the revision FastVideo pins as
78+
commit `14c377950125c70b7a9dabf9c561fca53715ac7d`, the revision FastVideo pins as
7979
the `flash-attn-4` source in the repo-root `pyproject.toml`; other revisions may
80-
have an incompatible `_flash_attn_fwd` signature.
80+
have incompatible block-sparse forward/backward interfaces.
81+
82+
Install it under its distribution name so its own runtime stack resolves with it.
83+
Do **not** pre-install `nvidia-cutlass-dsl` by hand: this revision pins
84+
`nvidia-cutlass-dsl==4.6.0.dev0` exactly, and a hand-installed 4.5.x floor either
85+
gets silently upgraded or, if something else holds it back, leaves the CuTe
86+
kernels broken.
8187

8288
```bash
83-
pip install "nvidia-cutlass-dsl>=4.5.0" torchvision
84-
pip install "git+https://github.com/Dao-AILab/flash-attention.git@940cd9680f3315f2f06b43ab5bea2c2cf2d96806#subdirectory=flash_attn/cute"
89+
pip install torchvision
90+
pip install "flash-attn-4 @ git+https://github.com/Dao-AILab/flash-attention.git@14c377950125c70b7a9dabf9c561fca53715ac7d#subdirectory=flash_attn/cute"
8591
```
8692

87-
The CuTe kernel JIT-compiles on first use. Verified on Blackwell (sm_100) against
88-
`tests/test_vsa256_forward*.py`.
93+
That resolves `nvidia-cutlass-dsl` to 4.6.0.dev0 and `quack-kernels` to 0.5.3, a
94+
combination this revision works with. A mismatched CuTe DSL only surfaces when the
95+
kernel JIT-compiles, so the error points at CuTe internals rather than at the
96+
install:
97+
98+
| Error on first VSA-128/256 CuTe call | Cause |
99+
|---|---|
100+
| `TypeError: fmax() missing 1 required positional argument: 'b'` | `nvidia-cutlass-dsl` 4.5.x |
101+
| `AttributeError: module 'cutlass.cute.core' has no attribute 'ThrMma'` | `quack-kernels` older than 0.5.1 |
102+
| `ImportError: cannot import name 'alloc_reserved_mbarrier'` | `quack-kernels` 0.6.2 or newer |
103+
104+
An environment whose `flash_attn.cute` came from a prebuilt flash-attn wheel rather
105+
than from this pin hits the first row; that is what the overlay step in
106+
`docker/Dockerfile` works around.
107+
108+
The CuTe kernels JIT-compile on first use. Forward and backward are verified on
109+
Blackwell (sm_100) against `tests/test_vsa128_*.py` and `tests/test_vsa256_*.py`.
89110

90111
## Usage
91112

@@ -142,6 +163,14 @@ After building/installing `fastvideo-kernel`, run:
142163
```bash
143164
cd fastvideo-kernel
144165
python benchmarks/bench_vsa.py --batch_size 1 --num_heads 16 --head_dim 128 --q_seq_lens 49152 --topk 64
166+
167+
# VSA-256 FA4 CuTe forward/backward on Blackwell
168+
python benchmarks/bench_vsa.py --block_size 256 --use_cute \
169+
--batch_size 1 --num_heads 12 --head_dim 128 --q_seq_lens 39936 --topk 20
170+
171+
# VSA-128 FA4 CuTe forward/backward on Blackwell
172+
python benchmarks/bench_vsa.py --block_size 128 --use_cute \
173+
--batch_size 1 --num_heads 12 --head_dim 128 --q_seq_lens 39936 --topk 40
145174
```
146175

147176
### TurboDiffusion Kernels

fastvideo-kernel/benchmarks/bench_vsa.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"""
33
Benchmark VSA *wrapper* performance (forward + backward) and report TFLOPs.
44
5-
This script benchmarks the autograd-enabled wrapper:
6-
- fastvideo_kernel.block_sparse_attn.block_sparse_attn
5+
This script benchmarks the autograd-enabled wrappers:
6+
- 64-token TK/Triton: fastvideo_kernel.block_sparse_attn.block_sparse_attn
7+
- 128/256-token Triton/CuTe: fastvideo_kernel.block_sparse_attn_256
78
89
So measured time includes wrapper overhead (map->index conversion, dispatch) plus kernel time.
910
"""
@@ -23,9 +24,6 @@
2324
except Exception as e: # pragma: no cover
2425
raise ImportError("This benchmark requires triton (for triton.testing.do_bench).") from e
2526

26-
BLOCK_M = 64
27-
BLOCK_N = 64
28-
2927

3028
def set_seed(seed: int = 42) -> None:
3129
random.seed(seed)
@@ -41,7 +39,11 @@ def parse_arguments() -> argparse.Namespace:
4139
p.add_argument("--num_heads", type=int, default=12)
4240
p.add_argument("--head_dim", type=int, default=128, choices=[64, 128])
4341
p.add_argument("--topk", type=int, default=None, help="KV blocks per Q block (default: ~90%% sparsity)")
44-
p.add_argument("--q_seq_lens", type=int, nargs="+", default=[49152], help="Q sequence lengths (must be /64)")
42+
p.add_argument("--q_seq_lens",
43+
type=int,
44+
nargs="+",
45+
default=[49152],
46+
help="Q sequence lengths (must be divisible by --block_size)")
4547
p.add_argument("--kv_seq_lens",
4648
type=int,
4749
nargs="+",
@@ -51,9 +53,13 @@ def parse_arguments() -> argparse.Namespace:
5153
p.add_argument("--rep", type=int, default=20)
5254
p.add_argument("--seed", type=int, default=42)
5355
p.add_argument("--dtype", type=str, default="bf16", choices=["bf16", "fp16"])
56+
p.add_argument("--block_size", type=int, default=64, choices=[64, 128, 256])
5457
p.add_argument("--force_triton",
5558
action="store_true",
5659
help="Force wrapper to use Triton path (if supported by shapes).")
60+
p.add_argument("--use_cute",
61+
action="store_true",
62+
help="Use the optional FA4 CuTe forward/backward path (requires --block_size 128 or 256).")
5763
return p.parse_args()
5864

5965

@@ -84,18 +90,38 @@ def bench_ms(fn: Callable[[], object], warmup: int, rep: int) -> float:
8490
return do_bench(fn, warmup=warmup, rep=rep, quantiles=None)
8591

8692

93+
def _configure_backend(args: argparse.Namespace) -> None:
94+
if args.use_cute and args.block_size not in (128, 256):
95+
raise ValueError("--use_cute requires --block_size 128 or 256")
96+
if args.use_cute and args.force_triton:
97+
raise ValueError("--use_cute and --force_triton are mutually exclusive")
98+
99+
if args.force_triton:
100+
os.environ.pop("FASTVIDEO_VSA_CUTEDSL", None)
101+
os.environ["FASTVIDEO_KERNEL_VSA_FORCE_TRITON"] = "1"
102+
elif args.use_cute:
103+
os.environ.pop("FASTVIDEO_VSA_TRITON", None)
104+
os.environ.pop("FASTVIDEO_KERNEL_VSA_FORCE_TRITON", None)
105+
os.environ["FASTVIDEO_VSA_CUTEDSL"] = "1"
106+
107+
87108
def main() -> None:
88109
args = parse_arguments()
89110
set_seed(args.seed)
111+
_configure_backend(args)
90112

91113
dtype = torch.bfloat16 if args.dtype == "bf16" else torch.float16
92114

93-
if args.force_triton:
94-
os.environ["FASTVIDEO_KERNEL_VSA_FORCE_TRITON"] = "1"
95-
96115
from fastvideo_kernel.block_sparse_attn import block_sparse_attn
116+
from fastvideo_kernel.block_sparse_attn_256 import block_sparse_attn_128, block_sparse_attn_256
97117

98118
bs, h, d = args.batch_size, args.num_heads, args.head_dim
119+
block_size = args.block_size
120+
attention = {
121+
64: block_sparse_attn,
122+
128: block_sparse_attn_128,
123+
256: block_sparse_attn_256,
124+
}[block_size]
99125
kv_seq_lens = args.kv_seq_lens
100126
if kv_seq_lens is None:
101127
kv_seq_lens = args.q_seq_lens
@@ -105,20 +131,22 @@ def main() -> None:
105131
print("VSA Block-Sparse Attention Benchmark (WRAPPER)")
106132
print(f"device: {torch.cuda.get_device_name(0)}")
107133
print(f"batch={bs}, heads={h}, head_dim={d}, dtype={args.dtype}")
108-
print(f"BLOCK_M={BLOCK_M}, BLOCK_N={BLOCK_N}")
134+
print(f"block_size={block_size}")
109135
print("NOTE: timings include wrapper overhead (map->index + dispatch).")
110-
if args.force_triton:
136+
if args.use_cute:
137+
print("dispatch: FA4 CuTe")
138+
elif args.force_triton:
111139
print("dispatch: forced Triton (FASTVIDEO_KERNEL_VSA_FORCE_TRITON=1)")
112140
else:
113141
print("dispatch: SM90 if available, else Triton")
114142

115143
for q_len, kv_len in zip(args.q_seq_lens, kv_seq_lens):
116-
if q_len % BLOCK_M != 0 or kv_len % BLOCK_N != 0:
117-
print(f"[skip] q_len={q_len}, kv_len={kv_len} must be divisible by 64")
144+
if q_len % block_size != 0 or kv_len % block_size != 0:
145+
print(f"[skip] q_len={q_len}, kv_len={kv_len} must be divisible by {block_size}")
118146
continue
119147

120-
num_q_blocks = q_len // BLOCK_M
121-
num_kv_blocks = kv_len // BLOCK_N
148+
num_q_blocks = q_len // block_size
149+
num_kv_blocks = kv_len // block_size
122150
topk = args.topk if args.topk is not None else max(1, num_kv_blocks // 10)
123151
topk = min(topk, num_kv_blocks)
124152

@@ -129,11 +157,11 @@ def main() -> None:
129157
q, k, v = create_qkv(bs, h, q_len, kv_len, d, dtype)
130158
block_map = make_block_map(bs, h, num_q_blocks, num_kv_blocks, topk)
131159

132-
# Variable block sizes: default full blocks (64 tokens per KV block)
133-
variable_block_sizes = torch.full((num_kv_blocks, ), BLOCK_N, dtype=torch.int32, device="cuda")
160+
# Variable block sizes: default full logical blocks.
161+
variable_block_sizes = torch.full((num_kv_blocks, ), block_size, dtype=torch.int32, device="cuda")
134162

135163
def _fwd():
136-
return block_sparse_attn(q, k, v, block_map, variable_block_sizes)
164+
return attention(q, k, v, block_map, variable_block_sizes)
137165

138166
fwd_ms = bench_ms(_fwd, warmup=args.warmup, rep=args.rep)
139167

@@ -142,7 +170,7 @@ def _fwd():
142170
q_ = q.detach().requires_grad_(True)
143171
k_ = k.detach().requires_grad_(True)
144172
v_ = v.detach().requires_grad_(True)
145-
o_, _aux_ = block_sparse_attn(q_, k_, v_, block_map, variable_block_sizes)
173+
o_, _aux_ = attention(q_, k_, v_, block_map, variable_block_sizes)
146174
og = torch.randn_like(o_)
147175
loss = (o_ * og).sum()
148176

@@ -156,7 +184,7 @@ def _fwd():
156184
rep=max(5, args.rep // 2),
157185
)
158186

159-
flops = flops_sparse_attention(bs, h, d, q_len, topk, BLOCK_N)
187+
flops = flops_sparse_attention(bs, h, d, q_len, topk, block_size)
160188
fwd_tflops = flops / fwd_ms * 1e-12 * 1e3
161189
# Rough backward multiplier (attention backward typically ~2-3x forward)
162190
bwd_tflops = (2.5 * flops) / bwd_ms * 1e-12 * 1e3

fastvideo-kernel/python/fastvideo_kernel/block_sparse_attn_256.py

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""VSA-256 block-sparse attention wrapper.
1+
"""VSA-128/256 block-sparse attention wrappers.
22
33
The default 256-block path is Triton: it expands the logical 256-block map
44
to the existing 64-block Triton kernel via a dense 4x4 expansion per logical
@@ -7,8 +7,8 @@
77
The FA4 CuTe block-sparse fastpath (intended for Blackwell sm_100+) is
88
*opt-in* via ``FASTVIDEO_VSA_CUTEDSL=1``. It routes to
99
:mod:`fastvideo_kernel.block_sparse_attn_cute_fwd`, which natively operates
10-
on 128-token KV blocks (this wrapper expands the logical 256-block map /
11-
sizes into that physical 128-block representation). The CuTe kernel
10+
on 128-token Q/KV blocks (the 256 wrapper expands its logical KV map and
11+
sizes into that physical representation). The CuTe kernel
1212
(``flash_attn.cute`` with block-sparsity) is an optional dependency,
1313
imported lazily only when this fastpath is selected.
1414
@@ -35,7 +35,7 @@
3535

3636

3737
def _resolve_backend() -> str:
38-
"""Pick the backend for the 256-block VSA path.
38+
"""Pick the backend for the 128/256-block VSA paths.
3939
4040
Default is Triton (no optional deps). The FA4 CuTe fastpath is opt-in
4141
via ``FASTVIDEO_VSA_CUTEDSL=1`` and requires the optional FA4 CuTe
@@ -49,6 +49,26 @@ def _resolve_backend() -> str:
4949
return "triton"
5050

5151

52+
def _expand_mask_and_sizes_128_to_64(
53+
logical_mask_128: torch.Tensor,
54+
logical_kv_sizes_128: torch.Tensor,
55+
) -> Tuple[torch.Tensor, torch.Tensor]:
56+
"""Expand a [B, H, Qb128, KVb128] map to 64-token Triton tiles."""
57+
expanded_mask = logical_mask_128.repeat_interleave(2, dim=2).repeat_interleave(2, dim=3)
58+
sizes_i32 = logical_kv_sizes_128.to(torch.int32)
59+
offsets = torch.tensor(
60+
[0, _KV_BLOCK_TRITON],
61+
dtype=torch.int32,
62+
device=sizes_i32.device,
63+
)
64+
expanded_sizes = torch.clamp(
65+
sizes_i32[:, None] - offsets[None, :],
66+
min=0,
67+
max=_KV_BLOCK_TRITON,
68+
).reshape(-1)
69+
return expanded_mask, expanded_sizes
70+
71+
5272
def _expand_mask_and_sizes_256_to_128(
5373
logical_mask_256: torch.Tensor,
5474
logical_kv_sizes_256: torch.Tensor,
@@ -112,6 +132,63 @@ def _triton_via_route_a(
112132
return block_sparse_attn_triton(q, k, v, q2k_idx, q2k_num, sizes_64)
113133

114134

135+
def _triton_via_route_a_128(
136+
q: torch.Tensor,
137+
k: torch.Tensor,
138+
v: torch.Tensor,
139+
logical_mask_128: torch.Tensor,
140+
logical_kv_sizes_128: torch.Tensor,
141+
) -> Tuple[torch.Tensor, torch.Tensor]:
142+
from .triton_kernels.index import map_to_index as triton_map_to_index
143+
144+
mask_64, sizes_64 = _expand_mask_and_sizes_128_to_64(logical_mask_128, logical_kv_sizes_128)
145+
q2k_idx, q2k_num = triton_map_to_index(mask_64.to(torch.bool))
146+
return block_sparse_attn_triton(q, k, v, q2k_idx, q2k_num, sizes_64)
147+
148+
149+
def block_sparse_attn_128(
150+
q: torch.Tensor,
151+
k: torch.Tensor,
152+
v: torch.Tensor,
153+
logical_block_map_128: torch.Tensor,
154+
logical_variable_block_sizes_128: torch.Tensor,
155+
) -> Tuple[torch.Tensor, torch.Tensor]:
156+
"""VSA-128 sparse-branch entrypoint for [B, H, S, D] inputs."""
157+
if logical_block_map_128.dim() == 3:
158+
logical_block_map_128 = logical_block_map_128.unsqueeze(0)
159+
160+
if _resolve_backend() == "triton":
161+
return _triton_via_route_a_128(q, k, v, logical_block_map_128, logical_variable_block_sizes_128)
162+
163+
from .block_sparse_attn_cute_fwd import block_sparse_attn_cute_fwd
164+
return block_sparse_attn_cute_fwd(q, k, v, logical_block_map_128, logical_variable_block_sizes_128)
165+
166+
167+
def block_sparse_attn_128_bshd(
168+
q: torch.Tensor,
169+
k: torch.Tensor,
170+
v: torch.Tensor,
171+
logical_block_map_128: torch.Tensor,
172+
logical_variable_block_sizes_128: torch.Tensor,
173+
) -> Tuple[torch.Tensor, torch.Tensor]:
174+
"""VSA-128 sparse-branch entrypoint for [B, S, H, D] inputs."""
175+
if logical_block_map_128.dim() == 3:
176+
logical_block_map_128 = logical_block_map_128.unsqueeze(0)
177+
178+
if _resolve_backend() == "triton":
179+
out_bhsd, aux = _triton_via_route_a_128(
180+
q.transpose(1, 2).contiguous(),
181+
k.transpose(1, 2).contiguous(),
182+
v.transpose(1, 2).contiguous(),
183+
logical_block_map_128,
184+
logical_variable_block_sizes_128,
185+
)
186+
return out_bhsd.transpose(1, 2).contiguous(), aux
187+
188+
from .block_sparse_attn_cute_fwd import block_sparse_attn_cute_fwd_bshd
189+
return block_sparse_attn_cute_fwd_bshd(q, k, v, logical_block_map_128, logical_variable_block_sizes_128)
190+
191+
115192
def block_sparse_attn_256(
116193
q: torch.Tensor,
117194
k: torch.Tensor,

0 commit comments

Comments
 (0)