Emma Scharfmann commited on
Commit
a788ec6
Β·
1 Parent(s): 8ae4133

fix compat

Browse files
Files changed (1) hide show
  1. aifs/compat.py +145 -28
aifs/compat.py CHANGED
@@ -12,45 +12,154 @@ import torch.nn.functional as F
12
 
13
  # ── SDPA-based attention replacement ─────────────────────────────────────────
14
 
15
- def _sdpa_compat(q, k, v, causal=False, window_size=(-1, -1), dropout_p=0.0, softcap=None, alibi_slopes=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  """
17
  Drop-in replacement for ``flash_attn_func``.
18
 
19
- Parameters mirror the flash-attn 2.x signature that Anemoi calls.
20
- Input tensors are shaped ``(batch, seq, heads, dim)``.
21
  """
 
 
 
 
 
 
 
22
  t0 = time.perf_counter()
23
 
24
  # flash-attn layout: (B, S, H, D) β†’ SDPA layout: (B, H, S, D)
25
  q, k, v = (t.permute(0, 2, 1, 3) for t in (q, k, v))
26
- ws = window_size[0] if isinstance(window_size, (tuple, list)) else int(window_size)
 
 
 
 
 
 
 
27
 
28
  if q.device.type == "cuda":
29
- # Full global attention; SDPA dispatches to flash-attn kernel when available
30
- out = F.scaled_dot_product_attention(q, k, v, dropout_p=dropout_p)
31
-
32
- elif ws > 0:
33
- # MPS: chunked sliding-window attention (avoids OOM on large sequences)
34
- B, H, S, D = q.shape
35
- out = torch.zeros_like(q)
36
- for i in range(0, S, ws):
37
- k_start = max(0, i - ws)
38
- k_end = min(S, i + ws + ws)
39
- out[:, :, i : i + ws] = F.scaled_dot_product_attention(
40
- q[:, :, i : i + ws],
41
- k[:, :, k_start:k_end],
42
- v[:, :, k_start:k_end],
43
- dropout_p=dropout_p,
 
 
 
 
 
 
 
44
  )
45
 
46
  else:
47
- # CPU fallback β€” move to CPU in case tensors are on an unsupported device
48
- out = F.scaled_dot_product_attention(
49
- q.cpu(), k.cpu(), v.cpu(), dropout_p=dropout_p
50
- ).to(q.device)
 
 
 
 
 
 
 
51
 
 
 
52
  elapsed = time.perf_counter() - t0
53
- print(f" [compat] attn {elapsed:.2f}s device={q.device.type} ws={ws}")
54
 
55
  return out.permute(0, 2, 1, 3)
56
 
@@ -64,11 +173,19 @@ def _patch():
64
 
65
  flash_attn = types.ModuleType("flash_attn")
66
  flash_attn.__version__ = "2.6.0" # version Anemoi checks against
 
67
 
68
  # flash_attn.layers.rotary (imported but only used on specific GPU paths)
69
  layers_mod = types.ModuleType("flash_attn.layers")
70
  rotary_mod = types.ModuleType("flash_attn.layers.rotary")
71
- rotary_mod.RotaryEmbedding = None
 
 
 
 
 
 
 
72
  layers_mod.rotary = rotary_mod
73
  flash_attn.layers = layers_mod
74
 
@@ -77,9 +194,9 @@ def _patch():
77
  interface_mod.flash_attn_func = _sdpa_compat
78
  flash_attn.flash_attn_interface = interface_mod
79
 
80
- sys.modules["flash_attn"] = flash_attn
81
- sys.modules["flash_attn.layers"] = layers_mod
82
- sys.modules["flash_attn.layers.rotary"] = rotary_mod
83
  sys.modules["flash_attn.flash_attn_interface"] = interface_mod
84
 
85
 
 
12
 
13
  # ── SDPA-based attention replacement ─────────────────────────────────────────
14
 
15
+ def _window_bounds(seq_len, left, right, causal, device):
16
+ """
17
+ Build the per-query (lower, upper) inclusive key bounds implied by
18
+ ``window_size=(left, right)`` and ``causal``, following flash-attn semantics:
19
+
20
+ - left < 0 -> no lower-bound restriction from the window
21
+ - right < 0 -> no upper-bound restriction from the window
22
+ - causal=True additionally forces key <= query, regardless of `right`
23
+ """
24
+ idx = torch.arange(seq_len, device=device)
25
+ lower = torch.zeros(seq_len, dtype=torch.long, device=device) if left < 0 else torch.clamp(idx - left, min=0)
26
+ if causal:
27
+ upper = idx.clone()
28
+ elif right < 0:
29
+ upper = torch.full((seq_len,), seq_len - 1, dtype=torch.long, device=device)
30
+ else:
31
+ upper = torch.clamp(idx + right, max=seq_len - 1)
32
+ return lower, upper
33
+
34
+
35
+ def _masked_attention(q, k, v, lower, upper, dropout_p, softmax_scale):
36
+ """
37
+ Full (non-chunked) windowed/causal attention via an explicit boolean mask.
38
+ q, k, v: (B, H, S, D). lower/upper: (S,) per-query inclusive key bounds.
39
+ Suitable when S is small enough that an S x S bool mask is affordable.
40
+ """
41
+ S = q.shape[-2]
42
+ key_idx = torch.arange(S, device=q.device).view(1, S) # (1, S)
43
+ allowed = (key_idx >= lower.view(S, 1)) & (key_idx <= upper.view(S, 1)) # (S, S)
44
+ return F.scaled_dot_product_attention(
45
+ q, k, v, attn_mask=allowed, dropout_p=dropout_p, scale=softmax_scale
46
+ )
47
+
48
+
49
+ def _chunked_windowed_attention(q, k, v, left, right, causal, dropout_p, softmax_scale, chunk_size):
50
+ """
51
+ Memory-friendly windowed/causal attention: iterate over query chunks and
52
+ only materialize the (small) slice of keys/values each chunk can attend
53
+ to, with a per-row mask inside that slice to get exact bounds right.
54
+ """
55
+ B, H, S, D = q.shape
56
+ out = torch.empty_like(q)
57
+ lower_full, upper_full = _window_bounds(S, left, right, causal, q.device)
58
+
59
+ for qs in range(0, S, chunk_size):
60
+ qe = min(S, qs + chunk_size)
61
+
62
+ # Superset of keys any query in [qs, qe) could need.
63
+ k_start = int(lower_full[qs:qe].min().item())
64
+ k_end = int(upper_full[qs:qe].max().item()) + 1
65
+
66
+ q_chunk = q[:, :, qs:qe]
67
+ k_chunk = k[:, :, k_start:k_end]
68
+ v_chunk = v[:, :, k_start:k_end]
69
+
70
+ # Per-row mask within this (small) chunk to enforce exact bounds.
71
+ key_idx = torch.arange(k_start, k_end, device=q.device).view(1, -1)
72
+ lower_c = lower_full[qs:qe].view(-1, 1)
73
+ upper_c = upper_full[qs:qe].view(-1, 1)
74
+ allowed = (key_idx >= lower_c) & (key_idx <= upper_c)
75
+
76
+ out[:, :, qs:qe] = F.scaled_dot_product_attention(
77
+ q_chunk, k_chunk, v_chunk, attn_mask=allowed, dropout_p=dropout_p, scale=softmax_scale
78
+ )
79
+
80
+ return out
81
+
82
+
83
+ def _sdpa_compat(
84
+ q,
85
+ k,
86
+ v,
87
+ dropout_p=0.0,
88
+ softmax_scale=None,
89
+ causal=False,
90
+ window_size=(-1, -1),
91
+ softcap=0.0,
92
+ alibi_slopes=None,
93
+ deterministic=False,
94
+ return_attn_probs=False,
95
+ ):
96
  """
97
  Drop-in replacement for ``flash_attn_func``.
98
 
99
+ Signature mirrors flash-attn 2.x. Input tensors are shaped (batch, seq, heads, dim).
 
100
  """
101
+ if softcap not in (None, 0.0):
102
+ raise NotImplementedError("softcap is not supported by the SDPA compatibility shim")
103
+ if alibi_slopes is not None:
104
+ raise NotImplementedError("alibi_slopes is not supported by the SDPA compatibility shim")
105
+ if return_attn_probs:
106
+ raise NotImplementedError("return_attn_probs is not supported by the SDPA compatibility shim")
107
+
108
  t0 = time.perf_counter()
109
 
110
  # flash-attn layout: (B, S, H, D) β†’ SDPA layout: (B, H, S, D)
111
  q, k, v = (t.permute(0, 2, 1, 3) for t in (q, k, v))
112
+
113
+ if isinstance(window_size, (tuple, list)):
114
+ left, right = window_size
115
+ else:
116
+ left = right = int(window_size)
117
+
118
+ S = q.shape[-2]
119
+ no_window = left < 0 and right < 0
120
 
121
  if q.device.type == "cuda":
122
+ if no_window:
123
+ # Full attention; SDPA dispatches to a flash-attn kernel when available.
124
+ out = F.scaled_dot_product_attention(
125
+ q, k, v, dropout_p=dropout_p, is_causal=causal, scale=softmax_scale
126
+ )
127
+ else:
128
+ # Windowed: chunk to bound peak memory, even though CUDA could
129
+ # often afford a full S x S mask.
130
+ out = _chunked_windowed_attention(
131
+ q, k, v, left, right, causal, dropout_p, softmax_scale, chunk_size=2048
132
+ )
133
+
134
+ elif q.device.type == "mps":
135
+ if no_window:
136
+ out = F.scaled_dot_product_attention(
137
+ q, k, v, dropout_p=dropout_p, is_causal=causal, scale=softmax_scale
138
+ )
139
+ else:
140
+ # MPS: chunked to avoid OOM on large sequences.
141
+ chunk = max(left if left > 0 else 0, right if right > 0 else 0) or 512
142
+ out = _chunked_windowed_attention(
143
+ q, k, v, left, right, causal, dropout_p, softmax_scale, chunk_size=chunk
144
  )
145
 
146
  else:
147
+ # CPU fallback β€” move to CPU in case tensors are on an unsupported device.
148
+ q_cpu, k_cpu, v_cpu = q.cpu(), k.cpu(), v.cpu()
149
+ if no_window:
150
+ out = F.scaled_dot_product_attention(
151
+ q_cpu, k_cpu, v_cpu, dropout_p=dropout_p, is_causal=causal, scale=softmax_scale
152
+ )
153
+ else:
154
+ out = _chunked_windowed_attention(
155
+ q_cpu, k_cpu, v_cpu, left, right, causal, dropout_p, softmax_scale, chunk_size=1024
156
+ )
157
+ out = out.to(q.device)
158
 
159
+ if q.device.type == "cuda":
160
+ torch.cuda.synchronize()
161
  elapsed = time.perf_counter() - t0
162
+ print(f" [compat] attn {elapsed:.3f}s device={q.device.type} S={S} window=({left},{right}) causal={causal}")
163
 
164
  return out.permute(0, 2, 1, 3)
165
 
 
173
 
174
  flash_attn = types.ModuleType("flash_attn")
175
  flash_attn.__version__ = "2.6.0" # version Anemoi checks against
176
+ flash_attn.flash_attn_func = _sdpa_compat # top-level re-export, matches real package
177
 
178
  # flash_attn.layers.rotary (imported but only used on specific GPU paths)
179
  layers_mod = types.ModuleType("flash_attn.layers")
180
  rotary_mod = types.ModuleType("flash_attn.layers.rotary")
181
+
182
+ def _rotary_not_implemented(*args, **kwargs):
183
+ raise NotImplementedError(
184
+ "flash_attn.layers.rotary.RotaryEmbedding is not available in the SDPA "
185
+ "compatibility shim; this code path requires real flash-attn on CUDA."
186
+ )
187
+
188
+ rotary_mod.RotaryEmbedding = _rotary_not_implemented
189
  layers_mod.rotary = rotary_mod
190
  flash_attn.layers = layers_mod
191
 
 
194
  interface_mod.flash_attn_func = _sdpa_compat
195
  flash_attn.flash_attn_interface = interface_mod
196
 
197
+ sys.modules["flash_attn"] = flash_attn
198
+ sys.modules["flash_attn.layers"] = layers_mod
199
+ sys.modules["flash_attn.layers.rotary"] = rotary_mod
200
  sys.modules["flash_attn.flash_attn_interface"] = interface_mod
201
 
202