Skip to content

Commit afb29e5

Browse files
[feat]: FA4-FP4 ATTN_QAT_INFER on sm_100/sm_103 + NVFP4 weight purge
1 parent 7a592ff commit afb29e5

12 files changed

Lines changed: 708 additions & 33 deletions

File tree

docs/inference/optimizations.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ pip install "nvidia-cutlass-dsl>=4.5.2" apache-tvm-ffi flashinfer-python
119119
The `--no-deps` flag prevents upgrading torch/torchvision. Use the supported
120120
PyTorch 2.12.0 and CUDA 13 environment for this kernel.
121121

122+
Branch-to-`nvidia-cutlass-dsl` compatibility (the fork tracks the CuTe DSL API
123+
surface closely):
124+
125+
| fork branch | cutlass-dsl | notes |
126+
|---|---|---|
127+
| `fp4` | `==4.4.2` (+ `nvidia-cutlass-dsl-libs-base==4.4.2`) | validated set on GB200: `quack-kernels==0.4.1`, `flashinfer-python==0.6.8`, `CUTE_DSL_ENABLE_TVM_FFI=1`, `FASTVIDEO_FA4=1` |
128+
| `fix/cutlass-dsl-4.5` | `>=4.5.2` | carries the `cute.core.ThrMma` -> `cute.ThrMma` fix |
129+
| any | 4.6-era | unsupported: `cute.make_fragment` was removed at module level; fails at CuTe JIT trace |
130+
131+
`FASTVIDEO_FA4=1` is required alongside the fork: it ships no compiled
132+
FlashAttention-2, so dense attention paths raise ImportError without the FA4
133+
opt-in. The same kernel also serves `ATTN_QAT_INFER` on sm_100a/sm_103a
134+
(datacenter Blackwell) — the selection log's receipt line
135+
(`ATTN_QAT_INFER resolved: ...`) records the arch, kernel, and quantization
136+
mode that actually bound.
137+
122138
#### Usage
123139

124140
Enable FP4 attention via the `--nvfp4_fa4` flag:

examples/inference/basic/basic_ltx2_distilled_fast_profile.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@ def main() -> None:
191191
print(f"Using refine upsampler: {refine_upsampler_path}")
192192

193193
pipeline_config = PipelineConfig.from_pretrained(model_root)
194+
# LTX-2 NVFP4 deploy contract (train==deploy surface):
195+
# * Linears: NVFP4 block-scaled GEMMs (per-16 E2M1 + E4M3 SFs) on every
196+
# arch, via flashinfer.
197+
# * ATTN_QAT_INFER attention differs per arch: sm_120a/sm_121a use the
198+
# fastvideo-kernel CUTLASS (SageAttention3-FP4) scheme that
199+
# ATTN_QAT_TRAIN simulates; sm_100a (GB200) / sm_103a (GB300) use the
200+
# FP4 FA4 kernel (flash-attention-fp4) with per-16 block-scaled NVFP4
201+
# Q/K and BF16 P/V -- a train-sim mismatch that is gated by MS-SSIM
202+
# measurement, not assumed equal. The selection receipt is logged at
203+
# backend resolution ("ATTN_QAT_INFER resolved: ...").
204+
# Original-weight retention: the default purges the always-FP4 layers'
205+
# bf16 originals after conversion. Refine-only layers (the cross-modal
206+
# AV projections) always keep theirs: the base stage profile runs them
207+
# dense by deployment contract -- in the two-stage fast profile AND the
208+
# distilled single-stage deploy. retain_original_weights=True keeps
209+
# everything (debugging).
194210
pipeline_config.dit_config.quant_config = NVFP4Config()
195211
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
196212
torch_compile_kwargs = {

examples/train/configs/overfit_ltx2_t2v_nvfp4_qat.yaml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@
1010
# quantized forward and STE backward, then ATTN_QAT_INFER during validation.
1111
# Head-dim-64 audio attention and masked text attention remain dense.
1212
#
13-
# Validation requires an sm_120 GPU with the attn_qat_infer extension.
13+
# Validation-time ATTN_QAT_INFER is arch-aware:
14+
# * sm_120a/sm_121a: fastvideo-kernel CUTLASS extension -- the exact
15+
# quantization scheme ATTN_QAT_TRAIN simulates.
16+
# * sm_100a (GB200) / sm_103a (GB300): FP4 FA4 kernel
17+
# (github.com/hao-ai-lab/flash-attention-fp4, branch fp4; per-16
18+
# block-scaled NVFP4 Q/K, BF16 P/V; validated install set:
19+
# nvidia-cutlass-dsl==4.4.2, quack-kernels==0.4.1,
20+
# flashinfer-python==0.6.8, FASTVIDEO_FA4=1 — see
21+
# docs/inference/optimizations.md). This scheme DIFFERS from the
22+
# CUTLASS one the training simulation matches, so sm_100/sm_103
23+
# validation and deployment carry a train-sim mismatch -- gate quality
24+
# by MS-SSIM measurement rather than assuming parity. The resolution
25+
# receipt ("ATTN_QAT_INFER resolved: ...") records arch + scheme.
1426
#
1527
# Preprocess data first (same data as the bf16 overfit):
1628
# CUDA_VISIBLE_DEVICES=0 python fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py
@@ -19,8 +31,8 @@
1931
# NUM_GPUS=4 \
2032
# bash examples/train/run.sh examples/train/configs/overfit_ltx2_t2v_nvfp4_qat.yaml
2133
#
22-
# GB200 can train and validate with ATTN_QAT_TRAIN, but cannot load the
23-
# sm_120-only inference kernel. Disable only the validation-time swap:
34+
# On GB200 without flash-attention-fp4 installed (or any other arch with no
35+
# ATTN_QAT_INFER kernel), disable only the validation-time swap:
2436
# NUM_GPUS=4 \
2537
# bash examples/train/run.sh examples/train/configs/overfit_ltx2_t2v_nvfp4_qat.yaml \
2638
# --callbacks.validation.attn_qat_infer false

fastvideo/attention/backends/attn_qat_infer.py

Lines changed: 176 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,139 @@ def _get_attn_qat_infer() -> Callable[..., torch.Tensor] | None:
5454
# kernel is compiled for (sm_120a / sm_121a -- see fastvideo-kernel/README.md).
5555
_SUPPORTED_DEVICE_CAPABILITIES = frozenset({(12, 0), (12, 1)})
5656

57-
58-
def _device_capability_supported() -> bool:
57+
# Datacenter-Blackwell capabilities served by the FP4 FA4 kernel
58+
# (flash-attention-fp4 @ fp4, sm_100a/sm_103a) through #1221's plumbing:
59+
# per-16 block-scaled NVFP4 Q/K (E4M3 scale factors), BF16 P/V. This is a
60+
# DIFFERENT quantization scheme from the sm_12x CUTLASS extension above --
61+
# ATTN_QAT_TRAIN simulates the CUTLASS scheme, so sm_100/sm_103 deployment
62+
# carries a train-sim mismatch that is measured (MS-SSIM gate), not assumed.
63+
_FA4_FP4_CAPABILITIES = frozenset({(10, 0), (10, 3)})
64+
65+
# The fork is written against the cutlass-dsl 4.4 API surface; the validated
66+
# install set (GB200-proven) is nvidia-cutlass-dsl==4.4.2 +
67+
# nvidia-cutlass-dsl-libs-base==4.4.2 + quack-kernels==0.4.1 +
68+
# flashinfer-python==0.6.8, with the fork on PYTHONPATH,
69+
# CUTE_DSL_ENABLE_TVM_FFI=1, and FASTVIDEO_FA4=1 (the fork ships no compiled
70+
# FA2, so dense attention paths need the FA4 opt-in). dsl 4.6-era installs
71+
# fail at CuTe JIT trace (cute.make_fragment was removed at module level).
72+
_FA4_INSTALL_HINT = ("install flash-attention-fp4 (branch fp4) from "
73+
"https://github.com/hao-ai-lab/flash-attention-fp4 with "
74+
"nvidia-cutlass-dsl==4.4.2, quack-kernels==0.4.1, "
75+
"flashinfer-python==0.6.8 and FASTVIDEO_FA4=1; "
76+
"see docs/inference/optimizations.md")
77+
78+
_fa4_fp4_import_ok: bool | None = None
79+
80+
81+
def _fa4_fp4_available() -> bool:
82+
"""flash_attn.cute (FA4) import probe, cached. Reuses #1221's guarded
83+
import chain in fastvideo.attention.utils.flash_attn_cute (which maps
84+
cutlass-dsl version skew to ImportError with a loud warning)."""
85+
global _fa4_fp4_import_ok
86+
if _fa4_fp4_import_ok is None:
87+
try:
88+
from fastvideo.attention.utils.flash_attn_cute import ( # noqa: F401
89+
flash_attn_fp4_func, )
90+
_fa4_fp4_import_ok = True
91+
except ImportError:
92+
_fa4_fp4_import_ok = False
93+
return _fa4_fp4_import_ok
94+
95+
96+
def _active_capability() -> tuple[int, int] | None:
5997
if not torch.cuda.is_available():
60-
return False
98+
return None
6199
try:
62-
return tuple(torch.cuda.get_device_capability()) in _SUPPORTED_DEVICE_CAPABILITIES
100+
return tuple(torch.cuda.get_device_capability())
63101
except Exception: # pragma: no cover - defensive: never break backend selection
64-
return False
102+
return None
103+
104+
105+
def _resolved_kernel() -> str | None:
106+
"""Which ATTN_QAT_INFER kernel serves the active device, or None.
107+
108+
Per-arch resolution (single source of truth -- extend the capability
109+
sets above, do not add equality checks elsewhere):
110+
* sm_12x consumer Blackwell -> fastvideo-kernel CUTLASS extension
111+
(modified SageAttention3 FP4).
112+
* sm_100a/sm_103a datacenter Blackwell -> FP4 FA4 (flash-attention-fp4)
113+
via the merged #1221 plumbing.
114+
"""
115+
cap = _active_capability()
116+
if cap in _SUPPORTED_DEVICE_CAPABILITIES and _get_attn_qat_infer() is not None:
117+
return "cutlass_sm12x"
118+
if cap in _FA4_FP4_CAPABILITIES and _fa4_fp4_available():
119+
return "fa4_fp4"
120+
return None
121+
122+
123+
def attn_qat_infer_receipt() -> str:
124+
"""One-line receipt of the resolution decision (arch + kernel + quant
125+
knobs), for the selection log and for tooling. The FA4 knobs are the
126+
repo's tuned defaults passed through verbatim: qk_mode=nvfp4
127+
(per-16 E4M3 SFs), pv_mode=bf16 -- see flash_attn/cute/README.md in
128+
the kernel repo."""
129+
cap = _active_capability()
130+
arch = f"sm_{cap[0]}{cap[1]}" if cap is not None else "no-cuda"
131+
kernel = _resolved_kernel()
132+
if kernel == "cutlass_sm12x":
133+
return f"arch={arch} kernel=fastvideo-kernel-cutlass scheme=sage3-fp4-sm120"
134+
if kernel == "fa4_fp4":
135+
return (f"arch={arch} kernel=flash-attention-fp4 qk_mode=nvfp4(per-16-e4m3-sf) "
136+
f"pv_mode=bf16 train_sim_mismatch=measured")
137+
supported = "sm_120a/sm_121a via fastvideo-kernel build.sh; sm_100a/sm_103a via flash-attention-fp4"
138+
if cap is not None and cap in _FA4_FP4_CAPABILITIES:
139+
return f"arch={arch} kernel=none (flash_attn.cute not importable -- {_FA4_INSTALL_HINT})"
140+
return f"arch={arch} kernel=none (supported: {supported})"
141+
142+
143+
_FA4_ROUTE_OPS: tuple | None = None
144+
145+
146+
def _import_fa4_route_ops() -> tuple:
147+
"""Slow path (own function so tests pin it runs once per process):
148+
resolves the FA4 quantize helper and kernel entry point."""
149+
from fastvideo.attention.backends.flash_attn import (
150+
_nvfp4_quantize_for_fa4, )
151+
from fastvideo.attention.utils.flash_attn_cute import (
152+
flash_attn_fp4_func, )
153+
return (_nvfp4_quantize_for_fa4, flash_attn_fp4_func)
154+
155+
156+
def _resolve_fa4_route_ops() -> tuple:
157+
# Lazy but memoized: per-forward resolution graph-breaks dynamo every
158+
# step and blocks fullgraph compilation of the NVFP4 path.
159+
global _FA4_ROUTE_OPS
160+
if _FA4_ROUTE_OPS is None:
161+
_FA4_ROUTE_OPS = _import_fa4_route_ops()
162+
return _FA4_ROUTE_OPS
163+
164+
165+
_receipt_logged = False
166+
167+
168+
def _log_receipt_once() -> None:
169+
# One line per process, not per layer (the validation swap constructs
170+
# one impl per attention layer).
171+
global _receipt_logged
172+
if not _receipt_logged:
173+
_receipt_logged = True
174+
logger.info("ATTN_QAT_INFER resolved: %s", attn_qat_infer_receipt())
65175

66176

67177
def is_attn_qat_infer_available() -> bool:
68-
"""True only when the extension imports AND the active device is a
69-
consumer-Blackwell (sm_120/sm_121) GPU the kernel is compiled for.
178+
"""True only when the active device has a built ATTN_QAT_INFER kernel.
70179
71180
The import check alone is not sufficient: CUDA 13 wheel builds can
72-
carry the sm_120/sm_121 extension on any host (e.g. H100, GB200),
73-
where the import succeeds, backend selection picks this backend, and
74-
the first kernel call then fails with an unsupported-capability error
75-
instead of ever reaching the documented FlashAttention fallback in
181+
carry the sm_12x extension on any host (e.g. H100, GB200), where the
182+
import succeeds, backend selection picks this backend, and the first
183+
kernel call then fails with an unsupported-capability error instead of
184+
ever reaching the documented FlashAttention fallback in
76185
fastvideo.platforms.cuda. Gating on the active device's capability
77-
keeps that fallback working on every non-sm_120/121 GPU.
186+
keeps that fallback working on every unsupported GPU, while
187+
sm_100a/sm_103a now resolve to the FP4 FA4 kernel (#1221).
78188
"""
79-
return _device_capability_supported() and _get_attn_qat_infer() is not None
189+
return _resolved_kernel() is not None
80190

81191

82192
class AttnQatInferBackend(AttentionBackend):
@@ -122,6 +232,12 @@ def __init__(
122232
if dropout_p > 0:
123233
raise NotImplementedError(f"attn_qat_infer does not support dropout (got dropout_p={dropout_p}). "
124234
"The QAT inference kernel applies no stochastic dropout.")
235+
# Kernel resolution is per-forward, not per-construction: callers
236+
# (the validation swap, backend selection) gate on
237+
# is_attn_qat_infer_available() first, and constructing an impl on a
238+
# host without the kernel must stay legal (pre-existing contract the
239+
# validation-swap test pins).
240+
_log_receipt_once()
125241

126242
def forward(
127243
self,
@@ -130,10 +246,18 @@ def forward(
130246
value: torch.Tensor,
131247
attn_metadata: AttentionMetadata,
132248
) -> torch.Tensor:
249+
# Dispatch on the single per-arch resolution: importability of the
250+
# bundled sm_12x extension is NOT sufficient (CUDA 13 wheels carry it
251+
# on unsupported hosts, where calling it is the wrong binary).
252+
kernel = _resolved_kernel()
253+
if kernel == "fa4_fp4":
254+
return self._forward_fa4_fp4(query, key, value)
255+
if kernel is None:
256+
raise ImportError(f"attn_qat_infer is not available ({attn_qat_infer_receipt()}). "
257+
"Please ensure an ATTN_QAT_INFER kernel is installed for this device.")
258+
133259
attn_qat_infer = _get_attn_qat_infer()
134-
if attn_qat_infer is None:
135-
raise ImportError("attn_qat_infer is not available. Please ensure the "
136-
"attn_qat_infer kernel package is installed.")
260+
assert attn_qat_infer is not None # kernel == "cutlass_sm12x" implies the import succeeded
137261

138262
query = query.transpose(1, 2).contiguous()
139263
key = key.transpose(1, 2).contiguous()
@@ -148,3 +272,39 @@ def forward(
148272
sm_scale=self.softmax_scale,
149273
)
150274
return output.transpose(1, 2).contiguous()
275+
276+
def _forward_fa4_fp4(
277+
self,
278+
query: torch.Tensor,
279+
key: torch.Tensor,
280+
value: torch.Tensor,
281+
) -> torch.Tensor:
282+
"""sm_100a/sm_103a path: FP4 FA4 with the repo's tuned defaults
283+
(NVFP4 per-16 block-scaled Q/K, BF16 V) -- mirrors
284+
FlashAttentionImpl._forward_nvfp4 (#1221). Inputs/outputs are
285+
(batch, seqlen, nheads, headdim); no transpose."""
286+
_nvfp4_quantize_for_fa4, flash_attn_fp4_func = _resolve_fa4_route_ops()
287+
288+
orig_seqlen_q = query.shape[1]
289+
orig_seqlen_k = key.shape[1]
290+
291+
q_fp4, q_sf = _nvfp4_quantize_for_fa4(query)
292+
k_fp4, k_sf = _nvfp4_quantize_for_fa4(key)
293+
294+
# FP4/SF buffers are padded to a 128 multiple; FA4 masks to the
295+
# original lengths so padding never biases the softmax.
296+
q_fp4 = q_fp4[:, :orig_seqlen_q]
297+
k_fp4 = k_fp4[:, :orig_seqlen_k]
298+
299+
output = flash_attn_fp4_func(
300+
q_fp4,
301+
k_fp4,
302+
value,
303+
q_sf,
304+
k_sf,
305+
softmax_scale=self.softmax_scale,
306+
causal=self.causal,
307+
)
308+
if isinstance(output, tuple):
309+
output = output[0]
310+
return output

fastvideo/attention/backends/flash_attn.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,30 @@
3232
flash_attn_fp4_func = None
3333
_FA4_FP4_AVAILABLE = False
3434

35+
_FA4_QUANT_OPS: tuple | None = None
3536

36-
def _nvfp4_quantize_for_fa4(tensor_4d: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor]:
37+
38+
def _import_fa4_quant_ops() -> tuple:
39+
"""The slow path: resolves flashinfer's FP4 quantization entry points
40+
(imports + JIT-module lookup, which probes the CUDA toolchain via a
41+
subprocess on first use). Kept as its own function so tests can pin
42+
that it runs at most once per process."""
43+
from flashinfer.quantization import SfLayout, nvfp4_quantize
44+
return (nvfp4_quantize, SfLayout)
45+
46+
47+
def _resolve_fa4_quant_ops() -> tuple:
48+
# Lazy (construct-anywhere/fail-at-forward stays intact) but memoized:
49+
# re-resolving per forward graph-breaks dynamo every step (making
50+
# fullgraph compilation impossible for the NVFP4 path) and keeps eager
51+
# dispatch overhead on the hot path.
52+
global _FA4_QUANT_OPS
53+
if _FA4_QUANT_OPS is None:
54+
_FA4_QUANT_OPS = _import_fa4_quant_ops()
55+
return _FA4_QUANT_OPS
56+
57+
58+
def _nvfp4_quantize_for_fa4_impl(tensor_4d: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
3759
"""Quantize a (batch, seqlen, nheads, headdim) BF16 tensor to FP4.
3860
3961
Returns:
@@ -42,7 +64,7 @@ def _nvfp4_quantize_for_fa4(tensor_4d: torch.Tensor, ) -> tuple[torch.Tensor, to
4264
Caller should slice [:, :orig_seqlen] before passing to FA4.
4365
sf_tensor: torch.uint8, shape (32, 4, rest_m, 4, rest_k, nheads, batch) with stride[3]=1
4466
"""
45-
from flashinfer.quantization import nvfp4_quantize, SfLayout
67+
nvfp4_quantize, SfLayout = _resolve_fa4_quant_ops()
4668

4769
batch, seqlen, nheads, headdim = tensor_4d.shape
4870
sf_vec_size = 16
@@ -82,6 +104,39 @@ def _nvfp4_quantize_for_fa4(tensor_4d: torch.Tensor, ) -> tuple[torch.Tensor, to
82104
return fp4_tensor, sf_mma
83105

84106

107+
# Dynamo boundary for the FP4 quantize path (same pattern as the masked
108+
# flash-attention entry points in flash_attn_no_pad.py): tracing a python
109+
# body that resolves flashinfer's JIT module descends into its toolchain
110+
# probe (a subprocess) regardless of any runtime memoization -- a cache hit
111+
# is invisible at trace time. Registering the whole quantize step as a
112+
# custom op makes it one opaque graph node, unlocking
113+
# torch.compile(fullgraph=True) for the NVFP4 attention path.
114+
@torch.library.custom_op(
115+
"fastvideo::nvfp4_quantize_fa4",
116+
mutates_args=(),
117+
device_types="cuda",
118+
)
119+
def _nvfp4_quantize_fa4_op(tensor_4d: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
120+
return _nvfp4_quantize_for_fa4_impl(tensor_4d)
121+
122+
123+
@torch.library.register_fake("fastvideo::nvfp4_quantize_fa4")
124+
def _nvfp4_quantize_fa4_fake(tensor_4d: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
125+
batch, seqlen, nheads, headdim = tensor_4d.shape
126+
seqlen_padded = (seqlen + 127) // 128 * 128
127+
fp4 = tensor_4d.new_empty((batch, seqlen_padded, nheads, headdim // 2), dtype=torch.float4_e2m1fn_x2)
128+
rest_m = seqlen_padded // 128
129+
rest_k = (headdim // 16) // 4
130+
sf = tensor_4d.new_empty((32, 4, rest_m, 4, rest_k, nheads, batch), dtype=torch.uint8)
131+
return fp4, sf
132+
133+
134+
def _nvfp4_quantize_for_fa4(tensor_4d: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
135+
"""Quantize (batch, seqlen, nheads, headdim) BF16 to FP4 via the custom
136+
op boundary; see _nvfp4_quantize_for_fa4_impl for the layout contract."""
137+
return torch.ops.fastvideo.nvfp4_quantize_fa4(tensor_4d)
138+
139+
85140
class FlashAttentionBackend(AttentionBackend):
86141
accept_output_buffer: bool = True
87142

0 commit comments

Comments
 (0)