ajh-code commited on
Commit
a387084
·
verified ·
1 Parent(s): 295b452

Add vendor/mage_flow/models/modules/_attn_backend.py

Browse files
vendor/mage_flow/models/modules/_attn_backend.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attention backend shim — switchable between Flash Attention 2 and 4.
2
+
3
+ Exports a single ``flash_attn_varlen_func`` with the FA2 calling convention.
4
+ The underlying kernel is selected at runtime via ``set_attn_backend(name)``
5
+ (default: ``"flash2"``). The selected kernel is resolved lazily on the first
6
+ call so model-config-driven selection (which happens after this module is
7
+ imported) takes effect.
8
+
9
+ Modules that previously did ``from flash_attn import flash_attn_varlen_func``
10
+ should import from here instead.
11
+
12
+ For the FA4 path, calling-convention differences are normalised:
13
+
14
+ * ``window_size=(-1, -1)`` (FA2 "no window") -> ``(None, None)`` (FA4).
15
+ * ``block_table`` -> ``page_table``.
16
+ * FA4's optional ``(out, lse)`` tuple return is unwrapped to ``out``.
17
+ * ``dropout_p>0`` / ``alibi_slopes`` / ``return_attn_probs`` raise on FA4.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any, Callable
23
+
24
+ _FA2_ALIASES = {"flash2", "fa2", "flash_attention_2", "flash_attn_2"}
25
+ _FA4_ALIASES = {"flash4", "fa4", "flash_attention_4", "flash_attn_4"}
26
+ _SDPA_ALIASES = {"sdpa", "torch_sdpa", "scaled_dot_product_attention"}
27
+
28
+ _BACKEND: str = "flash2"
29
+ _RESOLVED_FN: Callable[..., Any] | None = None
30
+
31
+
32
+ def _normalize(name: str) -> str:
33
+ n = name.lower().strip()
34
+ if n in _FA2_ALIASES:
35
+ return "flash2"
36
+ if n in _FA4_ALIASES:
37
+ return "flash4"
38
+ if n in _SDPA_ALIASES:
39
+ return "sdpa"
40
+ raise ValueError(
41
+ f"Unknown attention backend {name!r}; expected one of "
42
+ f"{sorted(_FA2_ALIASES | _FA4_ALIASES | _SDPA_ALIASES)}"
43
+ )
44
+
45
+
46
+ def set_attn_backend(name: str) -> None:
47
+ """Select the flash-attn backend used by ``flash_attn_varlen_func``.
48
+
49
+ Safe to call multiple times; clears the cached resolution on change.
50
+ """
51
+ global _BACKEND, _RESOLVED_FN
52
+ new = _normalize(name)
53
+ if new != _BACKEND:
54
+ _RESOLVED_FN = None
55
+ _BACKEND = new
56
+
57
+
58
+
59
+ def _resolve_fa2() -> Callable[..., Any]:
60
+ from flash_attn import flash_attn_varlen_func as _fn
61
+ return _fn
62
+
63
+
64
+ def _resolve_fa4() -> Callable[..., Any]:
65
+ from flash_attn.cute import flash_attn_varlen_func as _fa4_fn
66
+
67
+ def _fa4_wrapper(
68
+ q,
69
+ k,
70
+ v,
71
+ cu_seqlens_q=None,
72
+ cu_seqlens_k=None,
73
+ max_seqlen_q=None,
74
+ max_seqlen_k=None,
75
+ dropout_p: float = 0.0,
76
+ softmax_scale=None,
77
+ causal: bool = False,
78
+ window_size=(-1, -1),
79
+ softcap: float = 0.0,
80
+ alibi_slopes=None,
81
+ deterministic: bool = False,
82
+ return_attn_probs: bool = False,
83
+ block_table=None,
84
+ **_unused: Any,
85
+ ):
86
+ if dropout_p and dropout_p > 0:
87
+ raise NotImplementedError("FA4 backend does not support dropout_p>0")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("FA4 backend does not support alibi_slopes")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("FA4 backend does not support return_attn_probs")
92
+
93
+ win_l, win_r = window_size
94
+ if win_l == -1:
95
+ win_l = None
96
+ if win_r == -1:
97
+ win_r = None
98
+
99
+ out = _fa4_fn(
100
+ q,
101
+ k,
102
+ v,
103
+ cu_seqlens_q=cu_seqlens_q,
104
+ cu_seqlens_k=cu_seqlens_k,
105
+ max_seqlen_q=max_seqlen_q,
106
+ max_seqlen_k=max_seqlen_k,
107
+ softmax_scale=softmax_scale,
108
+ causal=causal,
109
+ window_size=(win_l, win_r),
110
+ softcap=softcap,
111
+ deterministic=deterministic,
112
+ page_table=block_table,
113
+ return_lse=False,
114
+ )
115
+ if isinstance(out, tuple):
116
+ out = out[0]
117
+ return out
118
+
119
+ return _fa4_wrapper
120
+
121
+
122
+ def _resolve_sdpa() -> Callable[..., Any]:
123
+ """FA2 varlen → per-sequence torch.SDPA fallback.
124
+
125
+ Use when flash-attn is unavailable (e.g. CUDA 13 has no prebuilt wheel
126
+ and source build is brittle). Slower than FA2 (one SDPA dispatch per
127
+ sequence), but functionally equivalent for the dense / causal / no-alibi
128
+ paths mageflow actually uses. Window / softcap / alibi / paged-attn /
129
+ return_attn_probs are not supported and will raise.
130
+ """
131
+ import torch
132
+ import torch.nn.functional as F
133
+
134
+ def _sdpa_wrapper(
135
+ q,
136
+ k,
137
+ v,
138
+ cu_seqlens_q=None,
139
+ cu_seqlens_k=None,
140
+ max_seqlen_q=None,
141
+ max_seqlen_k=None,
142
+ dropout_p: float = 0.0,
143
+ softmax_scale=None,
144
+ causal: bool = False,
145
+ window_size=(-1, -1),
146
+ softcap: float = 0.0,
147
+ alibi_slopes=None,
148
+ deterministic: bool = False,
149
+ return_attn_probs: bool = False,
150
+ block_table=None,
151
+ **_unused: Any,
152
+ ):
153
+ if dropout_p and dropout_p > 0:
154
+ raise NotImplementedError("SDPA backend does not support dropout_p>0")
155
+ if alibi_slopes is not None:
156
+ raise NotImplementedError("SDPA backend does not support alibi_slopes")
157
+ if return_attn_probs:
158
+ raise NotImplementedError("SDPA backend does not support return_attn_probs")
159
+ if softcap and softcap > 0:
160
+ raise NotImplementedError("SDPA backend does not support softcap")
161
+ if window_size not in ((-1, -1), (None, None), (0, 0)):
162
+ raise NotImplementedError(
163
+ f"SDPA backend does not support sliding window (got {window_size})"
164
+ )
165
+ if block_table is not None:
166
+ raise NotImplementedError("SDPA backend does not support paged attention")
167
+ if cu_seqlens_q is None or cu_seqlens_k is None:
168
+ raise ValueError("SDPA backend requires cu_seqlens_q and cu_seqlens_k")
169
+
170
+ # GQA: FA2 broadcasts k/v across query head groups natively; torch SDPA
171
+ # does not (the q vs k head-dim mismatch is the AssertionError "tensor
172
+ # a (32) must match tensor b (8) at non-singleton dimension 1" we'd see
173
+ # otherwise). Repeat k/v along the head dim to match q before the loop.
174
+ n_heads_q = q.shape[1]
175
+ n_heads_kv = k.shape[1]
176
+ if n_heads_q != n_heads_kv:
177
+ if n_heads_q % n_heads_kv != 0:
178
+ raise ValueError(
179
+ f"SDPA backend GQA expansion requires q heads ({n_heads_q}) "
180
+ f"to be divisible by k/v heads ({n_heads_kv})"
181
+ )
182
+ repeat = n_heads_q // n_heads_kv
183
+ k = k.repeat_interleave(repeat, dim=1)
184
+ v = v.repeat_interleave(repeat, dim=1)
185
+
186
+ # q/k/v: (total_tokens, nheads, head_dim). Dispatch SDPA per sequence,
187
+ # then concat. Python-level loop is fine since nseq is small (one per
188
+ # image in the pack) and image-gen latency is dominated by sampling.
189
+ cu_q = cu_seqlens_q.tolist()
190
+ cu_k = cu_seqlens_k.tolist()
191
+ outs = []
192
+ for qs, qe, ks, ke in zip(cu_q[:-1], cu_q[1:], cu_k[:-1], cu_k[1:]):
193
+ # (s, h, d) → (1, h, s, d)
194
+ q_i = q[qs:qe].transpose(0, 1).unsqueeze(0)
195
+ k_i = k[ks:ke].transpose(0, 1).unsqueeze(0)
196
+ v_i = v[ks:ke].transpose(0, 1).unsqueeze(0)
197
+ out_i = F.scaled_dot_product_attention(
198
+ q_i,
199
+ k_i,
200
+ v_i,
201
+ attn_mask=None,
202
+ dropout_p=0.0,
203
+ is_causal=causal,
204
+ scale=softmax_scale,
205
+ )
206
+ # (1, h, s, d) → (s, h, d)
207
+ outs.append(out_i.squeeze(0).transpose(0, 1))
208
+ return torch.cat(outs, dim=0).contiguous()
209
+
210
+ return _sdpa_wrapper
211
+
212
+
213
+ def _resolve() -> Callable[..., Any]:
214
+ global _RESOLVED_FN
215
+ if _RESOLVED_FN is None:
216
+ if _BACKEND == "flash4":
217
+ _RESOLVED_FN = _resolve_fa4()
218
+ elif _BACKEND == "sdpa":
219
+ _RESOLVED_FN = _resolve_sdpa()
220
+ else:
221
+ _RESOLVED_FN = _resolve_fa2()
222
+ return _RESOLVED_FN
223
+
224
+
225
+ def flash_attn_varlen_func(*args, **kwargs):
226
+ return _resolve()(*args, **kwargs)
227
+
228
+
229
+ __all__ = ["flash_attn_varlen_func", "set_attn_backend"]