ECMWF_experiments / aifs /compat.py
Emma Scharfmann
fix compat
a788ec6
Raw
History Blame Contribute Delete
7.77 kB
"""
Import this module *before* importing anything from ``anemoi`` to allow the compatibility with available GPU / CPU
"""
import sys
import time
import types
import torch
import torch.nn.functional as F
# ── SDPA-based attention replacement ─────────────────────────────────────────
def _window_bounds(seq_len, left, right, causal, device):
"""
Build the per-query (lower, upper) inclusive key bounds implied by
``window_size=(left, right)`` and ``causal``, following flash-attn semantics:
- left < 0 -> no lower-bound restriction from the window
- right < 0 -> no upper-bound restriction from the window
- causal=True additionally forces key <= query, regardless of `right`
"""
idx = torch.arange(seq_len, device=device)
lower = torch.zeros(seq_len, dtype=torch.long, device=device) if left < 0 else torch.clamp(idx - left, min=0)
if causal:
upper = idx.clone()
elif right < 0:
upper = torch.full((seq_len,), seq_len - 1, dtype=torch.long, device=device)
else:
upper = torch.clamp(idx + right, max=seq_len - 1)
return lower, upper
def _masked_attention(q, k, v, lower, upper, dropout_p, softmax_scale):
"""
Full (non-chunked) windowed/causal attention via an explicit boolean mask.
q, k, v: (B, H, S, D). lower/upper: (S,) per-query inclusive key bounds.
Suitable when S is small enough that an S x S bool mask is affordable.
"""
S = q.shape[-2]
key_idx = torch.arange(S, device=q.device).view(1, S) # (1, S)
allowed = (key_idx >= lower.view(S, 1)) & (key_idx <= upper.view(S, 1)) # (S, S)
return F.scaled_dot_product_attention(
q, k, v, attn_mask=allowed, dropout_p=dropout_p, scale=softmax_scale
)
def _chunked_windowed_attention(q, k, v, left, right, causal, dropout_p, softmax_scale, chunk_size):
"""
Memory-friendly windowed/causal attention: iterate over query chunks and
only materialize the (small) slice of keys/values each chunk can attend
to, with a per-row mask inside that slice to get exact bounds right.
"""
B, H, S, D = q.shape
out = torch.empty_like(q)
lower_full, upper_full = _window_bounds(S, left, right, causal, q.device)
for qs in range(0, S, chunk_size):
qe = min(S, qs + chunk_size)
# Superset of keys any query in [qs, qe) could need.
k_start = int(lower_full[qs:qe].min().item())
k_end = int(upper_full[qs:qe].max().item()) + 1
q_chunk = q[:, :, qs:qe]
k_chunk = k[:, :, k_start:k_end]
v_chunk = v[:, :, k_start:k_end]
# Per-row mask within this (small) chunk to enforce exact bounds.
key_idx = torch.arange(k_start, k_end, device=q.device).view(1, -1)
lower_c = lower_full[qs:qe].view(-1, 1)
upper_c = upper_full[qs:qe].view(-1, 1)
allowed = (key_idx >= lower_c) & (key_idx <= upper_c)
out[:, :, qs:qe] = F.scaled_dot_product_attention(
q_chunk, k_chunk, v_chunk, attn_mask=allowed, dropout_p=dropout_p, scale=softmax_scale
)
return out
def _sdpa_compat(
q,
k,
v,
dropout_p=0.0,
softmax_scale=None,
causal=False,
window_size=(-1, -1),
softcap=0.0,
alibi_slopes=None,
deterministic=False,
return_attn_probs=False,
):
"""
Drop-in replacement for ``flash_attn_func``.
Signature mirrors flash-attn 2.x. Input tensors are shaped (batch, seq, heads, dim).
"""
if softcap not in (None, 0.0):
raise NotImplementedError("softcap is not supported by the SDPA compatibility shim")
if alibi_slopes is not None:
raise NotImplementedError("alibi_slopes is not supported by the SDPA compatibility shim")
if return_attn_probs:
raise NotImplementedError("return_attn_probs is not supported by the SDPA compatibility shim")
t0 = time.perf_counter()
# flash-attn layout: (B, S, H, D) β†’ SDPA layout: (B, H, S, D)
q, k, v = (t.permute(0, 2, 1, 3) for t in (q, k, v))
if isinstance(window_size, (tuple, list)):
left, right = window_size
else:
left = right = int(window_size)
S = q.shape[-2]
no_window = left < 0 and right < 0
if q.device.type == "cuda":
if no_window:
# Full attention; SDPA dispatches to a flash-attn kernel when available.
out = F.scaled_dot_product_attention(
q, k, v, dropout_p=dropout_p, is_causal=causal, scale=softmax_scale
)
else:
# Windowed: chunk to bound peak memory, even though CUDA could
# often afford a full S x S mask.
out = _chunked_windowed_attention(
q, k, v, left, right, causal, dropout_p, softmax_scale, chunk_size=2048
)
elif q.device.type == "mps":
if no_window:
out = F.scaled_dot_product_attention(
q, k, v, dropout_p=dropout_p, is_causal=causal, scale=softmax_scale
)
else:
# MPS: chunked to avoid OOM on large sequences.
chunk = max(left if left > 0 else 0, right if right > 0 else 0) or 512
out = _chunked_windowed_attention(
q, k, v, left, right, causal, dropout_p, softmax_scale, chunk_size=chunk
)
else:
# CPU fallback β€” move to CPU in case tensors are on an unsupported device.
q_cpu, k_cpu, v_cpu = q.cpu(), k.cpu(), v.cpu()
if no_window:
out = F.scaled_dot_product_attention(
q_cpu, k_cpu, v_cpu, dropout_p=dropout_p, is_causal=causal, scale=softmax_scale
)
else:
out = _chunked_windowed_attention(
q_cpu, k_cpu, v_cpu, left, right, causal, dropout_p, softmax_scale, chunk_size=1024
)
out = out.to(q.device)
if q.device.type == "cuda":
torch.cuda.synchronize()
elapsed = time.perf_counter() - t0
print(f" [compat] attn {elapsed:.3f}s device={q.device.type} S={S} window=({left},{right}) causal={causal}")
return out.permute(0, 2, 1, 3)
# ── Build stub modules ────────────────────────────────────────────────────────
def _patch():
"""Install the flash_attn stub into ``sys.modules``."""
if "flash_attn" in sys.modules:
return
flash_attn = types.ModuleType("flash_attn")
flash_attn.__version__ = "2.6.0" # version Anemoi checks against
flash_attn.flash_attn_func = _sdpa_compat # top-level re-export, matches real package
# flash_attn.layers.rotary (imported but only used on specific GPU paths)
layers_mod = types.ModuleType("flash_attn.layers")
rotary_mod = types.ModuleType("flash_attn.layers.rotary")
def _rotary_not_implemented(*args, **kwargs):
raise NotImplementedError(
"flash_attn.layers.rotary.RotaryEmbedding is not available in the SDPA "
"compatibility shim; this code path requires real flash-attn on CUDA."
)
rotary_mod.RotaryEmbedding = _rotary_not_implemented
layers_mod.rotary = rotary_mod
flash_attn.layers = layers_mod
# flash_attn.flash_attn_interface (the one Anemoi actually calls)
interface_mod = types.ModuleType("flash_attn.flash_attn_interface")
interface_mod.flash_attn_func = _sdpa_compat
flash_attn.flash_attn_interface = interface_mod
sys.modules["flash_attn"] = flash_attn
sys.modules["flash_attn.layers"] = layers_mod
sys.modules["flash_attn.layers.rotary"] = rotary_mod
sys.modules["flash_attn.flash_attn_interface"] = interface_mod
_patch()