Skip to content

Commit 4efa557

Browse files
committed
[refactor]: move FA default custom-op registration to attention/utils/
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern (vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in attention/utils/, not inline in the backend dispatcher. Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:` block — including the FA version probe, custom_op + register_fake + setup_context + register_autograd, and `flash_attn_func_compilable` — from `attention/backends/flash_attn.py` to a new sibling at `attention/utils/flash_attn_default.py`. The backend now imports `fa_version` and `flash_attn_func_compilable` from there; the rest of the backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers, masked-branch routing) is unchanged. Benefits: - Internal consistency with flash_attn_cute.py (the FP4 template the file-level comment claims to mirror) and flash_attn_no_pad.py (the masked/varlen wrappers, already in utils/). - A future backend that wants the same traceable FA2/FA3/FA4 op can import from utils/ without depending on the FA backend's dispatcher. - Cleaner separation between backend registry/dispatch logic and per-kernel custom-op plumbing. No semantic change. Verified there is exactly one registration of `fastvideo::_flash_attn_default_forward` after the move (grep clean). Update `test_flash_attn_default_custom_op.py` to import from the utils module (the test referenced `fa_backend._fa_default`, a name that lived inside the moved block).
1 parent e30cb3c commit 4efa557

3 files changed

Lines changed: 267 additions & 237 deletions

File tree

fastvideo/attention/backends/flash_attn.py

Lines changed: 5 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3-
import importlib.util
43
import os
5-
64
import torch
75
import torch.nn.functional as F
86
from dataclasses import dataclass
97

10-
from fastvideo import envs
8+
from fastvideo.attention.utils.flash_attn_default import (
9+
fa_version,
10+
flash_attn_func_compilable,
11+
)
12+
1113
from fastvideo.attention.backends.abstract import (
1214
AttentionBackend,
1315
AttentionImpl,
@@ -17,233 +19,6 @@
1719
from fastvideo.logger import init_logger
1820

1921
logger = init_logger(__name__)
20-
21-
# FA4 (flash_attn.cute) is explicit opt-in via FASTVIDEO_FA4=1, mirroring the
22-
# kernel package's FASTVIDEO_VSA_CUTEDSL: its CuTeDSL kernels JIT-compile per
23-
# shape family and can fail at runtime on some arch/shape combinations, so it
24-
# is never auto-selected just because it is installed. Below sm90 a capability
25-
# gate in flash_attn_cute routes to FA2 the calls FA4 cannot serve there:
26-
# grad-enabled (its backward asserts sm90+) and GQA (pack_gqa fails CuTeDSL
27-
# JIT, observed on sm_89).
28-
if envs.FASTVIDEO_FA4:
29-
try:
30-
from fastvideo.attention.utils.flash_attn_cute import flash_attn_func
31-
except ImportError as e:
32-
raise RuntimeError(f"FASTVIDEO_FA4=1 but flash_attn.cute (FA4) is not usable ({e}); "
33-
"fix the FA4 install (see the flash-attn-4 pin in pyproject.toml) "
34-
"or unset FASTVIDEO_FA4.") from e
35-
fa_version = "4"
36-
else:
37-
try:
38-
from flash_attn_interface import flash_attn_func as flash_attn_3_func
39-
40-
# flash_attn 3 no longer have a different API, see following commit:
41-
# https://github.com/Dao-AILab/flash-attention/commit/ed209409acedbb2379f870bbd03abce31a7a51b7
42-
flash_attn_func = flash_attn_3_func
43-
fa_version = "3"
44-
except ImportError:
45-
from flash_attn import flash_attn_func as flash_attn_2_func
46-
flash_attn_func = flash_attn_2_func
47-
fa_version = "2"
48-
try:
49-
if importlib.util.find_spec("flash_attn.cute") is not None:
50-
logger.info("flash_attn.cute (FA4) is installed but not enabled; "
51-
"set FASTVIDEO_FA4=1 to use it for inference.")
52-
except ImportError:
53-
pass
54-
55-
# torch.compile traceability: the FA4/cute path (fa_version=="4") is
56-
# already a registered torch.library custom op, so dynamo treats it as a
57-
# graph node. The external FA2/FA3 `flash_attn_func` is NOT — dynamo
58-
# breaks the graph at the call site (observed: wanvideo.py self-attn,
59-
# once per layer every step), which fragments the compiled region and
60-
# blocks CUDA-graph capture. Wrap the FA2/FA3 default call in a custom
61-
# op (mirrors the FP4 `flash_attn_cute` template) so it becomes an
62-
# opaque-but-traceable node. The kernel still runs eager inside the op
63-
# (correct — flash-attn must run eager); only dynamo's treatment of the
64-
# boundary changes, so numerics are unchanged (SSIM-gate to confirm).
65-
#
66-
# Autograd: FA2 has full register_autograd parity — the custom op's
67-
# backward calls flash_attn's `_flash_attn_backward` directly, so
68-
# training backprops *through* the op (no graph break on the training
69-
# path either). FA3 currently keeps the no-backward + carve-out pattern
70-
# from PR #1373 because FA3's private backward signature wants
71-
# validation on a real Hopper box (gated on Kuan-Hao's Modal FA3 setup
72-
# PR). Once that lands the FA3 path can mirror FA2.
73-
if fa_version == "2":
74-
# Scope: this op covers exactly the q/k/v + softmax_scale + causal
75-
# call shape used by FlashAttentionImpl.forward's default branch
76-
# (see `flash_attn_func_compilable(...)` call site below). The
77-
# masked/no-pad and varlen / cross-attn paths use different entry
78-
# points (`flash_attn_no_pad`, `flash_attn_varlen_*`) which are
79-
# intentionally out of scope for this PR — wrapping them is a
80-
# natural follow-up. The wrapper's signature is the contract: any
81-
# extra kwarg (dropout_p, window_size, alibi_slopes, deterministic,
82-
# return_attn_probs, ...) raises TypeError at the call site, so
83-
# silent loss of kwargs is not a failure mode.
84-
from flash_attn.flash_attn_interface import _flash_attn_backward as _fa2_backward
85-
_fa_default = flash_attn_func
86-
87-
@torch.library.custom_op(
88-
"fastvideo::_flash_attn_default_forward",
89-
mutates_args=(),
90-
device_types="cuda",
91-
)
92-
def _flash_attn_default_forward(
93-
q: torch.Tensor,
94-
k: torch.Tensor,
95-
v: torch.Tensor,
96-
softmax_scale: float | None,
97-
causal: bool,
98-
) -> tuple[torch.Tensor, torch.Tensor]:
99-
# `return_attn_probs=True` asks FA2 to also return softmax_lse +
100-
# S_dmask. We need softmax_lse to feed the backward; S_dmask is the
101-
# dropout mask (always None here since dropout_p is fixed at 0).
102-
out, softmax_lse, _ = _fa_default(q, k, v, softmax_scale=softmax_scale,
103-
causal=causal, return_attn_probs=True)
104-
return out, softmax_lse
105-
106-
@torch.library.register_fake("fastvideo::_flash_attn_default_forward")
107-
def _flash_attn_default_forward_fake(
108-
q: torch.Tensor,
109-
k: torch.Tensor,
110-
v: torch.Tensor,
111-
softmax_scale: float | None,
112-
causal: bool,
113-
) -> tuple[torch.Tensor, torch.Tensor]:
114-
del softmax_scale, causal
115-
# FA2 default path: out = [batch, seqlen_q, nheads, head_dim_v],
116-
# softmax_lse = [batch, nheads, seqlen_q], fp32 regardless of q dtype.
117-
b, sq, hq = q.shape[0], q.shape[1], q.shape[2]
118-
out = q.new_empty(b, sq, hq, v.shape[-1])
119-
lse = q.new_empty(b, hq, sq, dtype=torch.float32)
120-
return out, lse
121-
122-
def _flash_attn_default_setup_context(ctx, inputs, output):
123-
q, k, v, softmax_scale, causal = inputs
124-
out, lse = output
125-
ctx.save_for_backward(q, k, v, out, lse)
126-
# `lse` is an auxiliary output we save to feed FA2's backward; nobody
127-
# should differentiate through it. Mark it non-differentiable so
128-
# autograd errors loudly if a caller wires it into a loss, rather
129-
# than silently producing zero/None grads through the `del grad_lse`
130-
# in our backward.
131-
ctx.mark_non_differentiable(lse)
132-
# FA2's *forward* substitutes `1 / sqrt(head_dim)` for `softmax_scale=None`
133-
# internally; FA2's *backward* (`_flash_attn_backward`) demands a concrete
134-
# float in its C++ schema and rejects None at the binding boundary. Resolve
135-
# the default here so the value saved on ctx (and passed to backward) is
136-
# always a real float — matches what FA2's own autograd.Function does.
137-
if softmax_scale is None:
138-
softmax_scale = q.shape[-1] ** -0.5
139-
ctx.softmax_scale = softmax_scale
140-
ctx.causal = causal
141-
142-
def _flash_attn_default_backward(ctx, grad_out, grad_lse):
143-
# We only differentiate `out`; softmax_lse is saved-for-backward, not
144-
# a real differentiable output. (Mirrors the FP4 cute template.)
145-
del grad_lse
146-
q, k, v, out, lse = ctx.saved_tensors
147-
dq = torch.empty_like(q)
148-
dk = torch.empty_like(k)
149-
dv = torch.empty_like(v)
150-
# FA2's `_flash_attn_backward` writes into dq/dk/dv in place. The
151-
# extra kwargs (window_size_*, softcap, alibi_slopes, deterministic,
152-
# rng_state) are pinned to the same defaults the forward wrapper
153-
# uses — flash-attn==2.8.1 (the version FastVideo pins) requires
154-
# all of them explicitly. `rng_state=None` is correct for our
155-
# `dropout_p=0` configuration.
156-
_fa2_backward(
157-
grad_out, q, k, v, out, lse,
158-
dq, dk, dv,
159-
dropout_p=0.0,
160-
softmax_scale=ctx.softmax_scale,
161-
causal=ctx.causal,
162-
window_size_left=-1,
163-
window_size_right=-1,
164-
softcap=0.0,
165-
alibi_slopes=None,
166-
deterministic=False,
167-
rng_state=None,
168-
)
169-
return dq, dk, dv, None, None
170-
171-
torch.library.register_autograd(
172-
"fastvideo::_flash_attn_default_forward",
173-
_flash_attn_default_backward,
174-
setup_context=_flash_attn_default_setup_context,
175-
)
176-
177-
def flash_attn_func_compilable(q, k, v, softmax_scale=None, causal=False):
178-
# Backward is registered: autograd flows through the op (training
179-
# path is also traceable; no carve-out needed). Public API matches
180-
# `flash_attn_func` — returns just `out`; we drop the saved-for-
181-
# backward `lse` here so callers see the original single-tensor
182-
# contract.
183-
out, _ = torch.ops.fastvideo._flash_attn_default_forward(q, k, v, softmax_scale, causal)
184-
return out
185-
elif fa_version == "3":
186-
# FA3 path: same forward+fake custom op as the original PR #1373, with
187-
# the autograd carve-out kept. The full backward (mirroring the FA2
188-
# leg above) wants a Hopper box for grad-check validation, which we
189-
# don't have until Kuan-Hao's Modal FA3 setup PR lands. Until then
190-
# this keeps inference traceable + training correct (via the original
191-
# autograd.Function path + a pre-PR-style graph break on training).
192-
_fa_default = flash_attn_func
193-
194-
@torch.library.custom_op(
195-
"fastvideo::_flash_attn_default_forward",
196-
mutates_args=(),
197-
device_types="cuda",
198-
)
199-
def _flash_attn_default_forward(
200-
q: torch.Tensor,
201-
k: torch.Tensor,
202-
v: torch.Tensor,
203-
softmax_scale: float | None,
204-
causal: bool,
205-
) -> torch.Tensor:
206-
return _fa_default(q, k, v, softmax_scale=softmax_scale, causal=causal)
207-
208-
@torch.library.register_fake("fastvideo::_flash_attn_default_forward")
209-
def _flash_attn_default_forward_fake(
210-
q: torch.Tensor,
211-
k: torch.Tensor,
212-
v: torch.Tensor,
213-
softmax_scale: float | None,
214-
causal: bool,
215-
) -> torch.Tensor:
216-
del softmax_scale, causal
217-
return q.new_empty(q.shape[0], q.shape[1], q.shape[2], v.shape[-1])
218-
219-
def flash_attn_func_compilable(q, k, v, softmax_scale=None, causal=False):
220-
# Autograd carve-out. The custom op above registers a forward + fake
221-
# kernel but NO backward (register_autograd), so it is opaque to
222-
# autograd. Inference runs under no_grad / inference_mode and routes
223-
# through the traceable custom op — that is the torch.compile win, and
224-
# the only path this PR claims. Training backprops through attention,
225-
# so route grad-enabled calls to the original FA2/FA3 `flash_attn_func`
226-
# (itself an autograd.Function, so backward is correct) at the cost of a
227-
# dynamo graph break on the training path — i.e. pre-PR behavior, no
228-
# regression. Full autograd parity for the custom op (mirroring the FP4
229-
# cute template) is a tracked follow-up.
230-
if torch.is_grad_enabled() and (q.requires_grad or k.requires_grad or v.requires_grad):
231-
return _fa_default(q, k, v, softmax_scale=softmax_scale, causal=causal)
232-
return torch.ops.fastvideo._flash_attn_default_forward(q, k, v, softmax_scale, causal)
233-
elif fa_version == "4":
234-
# FA4 path: `flash_attn_func` (from `flash_attn_cute`) goes through a
235-
# registered torch.library custom op (with an FA4 backward on sm90+;
236-
# grad-enabled and GQA calls below sm90 route to FA2), so a passthrough
237-
# is enough — no extra registration needed.
238-
def flash_attn_func_compilable(q, k, v, softmax_scale=None, causal=False):
239-
return flash_attn_func(q, k, v, softmax_scale=softmax_scale, causal=causal)
240-
else:
241-
# Defensive: the probe above only ever sets fa_version to "2", "3",
242-
# or "4"; an unexpected value means an import/probe regression and
243-
# we want a loud error at import, not a silent NameError later.
244-
raise RuntimeError(f"Unsupported FlashAttention version: {fa_version!r} — expected "
245-
f"'2', '3', or '4' from the import probe above.")
246-
24722
logger.info("Using FlashAttention-%s backend", fa_version)
24823

24924
# FP4 FA4 support: quantize Q/K to NVFP4 E2M1 for block-scaled MMA on Blackwell.

0 commit comments

Comments
 (0)