|
1 | 1 | # SPDX-License-Identifier: Apache-2.0 |
2 | 2 |
|
3 | | -import importlib.util |
4 | 3 | import os |
5 | | - |
6 | 4 | import torch |
7 | 5 | import torch.nn.functional as F |
8 | 6 | from dataclasses import dataclass |
9 | 7 |
|
10 | | -from fastvideo import envs |
| 8 | +from fastvideo.attention.utils.flash_attn_default import ( |
| 9 | + fa_version, |
| 10 | + flash_attn_func_compilable, |
| 11 | +) |
| 12 | + |
11 | 13 | from fastvideo.attention.backends.abstract import ( |
12 | 14 | AttentionBackend, |
13 | 15 | AttentionImpl, |
|
17 | 19 | from fastvideo.logger import init_logger |
18 | 20 |
|
19 | 21 | 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 | | - |
247 | 22 | logger.info("Using FlashAttention-%s backend", fa_version) |
248 | 23 |
|
249 | 24 | # FP4 FA4 support: quantize Q/K to NVFP4 E2M1 for block-scaled MMA on Blackwell. |
|
0 commit comments