Evo1-1.5-7B-8K / rotary.py
Taykhoom's picture
RoPE: flash-attn 2.8 support, interleaved/xPos in PyTorch fallback
5b63c84 verified
Raw History Blame Contribute Delete
13.1 kB
"""Rotary embeddings for the Evo1 HF port.
Two modes:
* **Fast path** - when ``flash_attn`` is installed, we delegate to
``flash_attn.layers.rotary.RotaryEmbedding``, whose Triton kernel does
the rotary multiply in fp32 internally (and is bit-exact with our
pure-PyTorch path below).
* **Fallback** - pure-PyTorch implementation, mathematically identical to
flash_attn's kernel (multiply done in fp32 then cast back to bf16). Used
when ``flash_attn`` isn't available.
The ``LinearlyScaledRotaryEmbedding`` subclass (used for the 131k variant)
overrides ``_update_cos_sin_cache`` to scale position indices, which works
identically against either parent class.
"""
from __future__ import annotations
import torch
import torch.nn as nn
try:
from flash_attn.layers.rotary import RotaryEmbedding as _FlashRotaryEmbedding
_HAS_FLASH_ROTARY = True
except ImportError: # pragma: no cover - optional dep
_FlashRotaryEmbedding = None # type: ignore[assignment]
_HAS_FLASH_ROTARY = False
def _rotate_half(x: torch.Tensor, interleaved: bool = False) -> torch.Tensor:
"""[x1, x2] -> [-x2, x1] over halves, or over even/odd pairs if interleaved."""
if not interleaved:
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
x1, x2 = x[..., ::2], x[..., 1::2]
return torch.stack((-x2, x1), dim=-1).flatten(-2)
@torch.no_grad()
def restore_rotary_inv_freq(
rotary_emb: nn.Module,
*,
device=None,
invalidate_cache: bool = True,
) -> None:
"""Rebuild ``inv_freq`` from RoPE metadata in fp32.
Checkpoint-loaded values may already contain bf16 rounding even when the
destination buffer is fp32. Reconstructing from ``base`` and ``dim`` is
the only way to recover the exact frequencies.
"""
if not hasattr(rotary_emb, "inv_freq"):
return
device = rotary_emb.inv_freq.device if device is None else device
if hasattr(rotary_emb, "_compute_inv_freq"):
inv_freq = rotary_emb._compute_inv_freq(device=device)
else:
inv_freq = 1.0 / (
float(rotary_emb.base)
** (
torch.arange(
0,
int(rotary_emb.dim),
2,
device=device,
dtype=torch.float32,
)
/ int(rotary_emb.dim)
)
)
rotary_emb.inv_freq = inv_freq.to(dtype=torch.float32)
if invalidate_cache:
rotary_emb._seq_len_cached = 0
for name in (
"_cos_cached",
"_sin_cached",
"_cos_k_cached",
"_sin_k_cached",
):
if hasattr(rotary_emb, name):
setattr(rotary_emb, name, None)
def _apply_rotary(
x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, interleaved: bool = False
) -> torch.Tensor:
"""Apply non-interleaved RoPE to the last `2 * cos.shape[-1]` dims of x.
cos / sin shape: (T, rot_dim/2). x shape: (..., T, ..., D), where the rot
is applied along the last dim. We expand cos/sin to broadcast over the
leading dims.
The multiplication is performed in fp32 internally (then cast back to
x.dtype) to match flash_attn's Triton rotary kernel bit-exactly. Doing
the multiply in bf16 directly compounds rounding error of ~3e-2 per
layer, which becomes a ~1% relative error after 32 transformer blocks.
"""
rot_dim = cos.shape[-1] * 2
x_rot = x[..., :rot_dim]
x_pass = x[..., rot_dim:]
orig_dtype = x.dtype
if interleaved:
cos_full = torch.repeat_interleave(cos, 2, dim=-1).float()
sin_full = torch.repeat_interleave(sin, 2, dim=-1).float()
else:
cos_full = torch.cat((cos, cos), dim=-1).float()
sin_full = torch.cat((sin, sin), dim=-1).float()
x_rot_f = x_rot.float()
rotated = (x_rot_f * cos_full) + (_rotate_half(x_rot_f, interleaved) * sin_full)
rotated = rotated.to(orig_dtype)
return torch.cat((rotated, x_pass), dim=-1)
def _set_cos_sin_tables(rope: nn.Module, freqs: torch.Tensor, seqlen: int, dtype) -> None:
"""Fill a RoPE module's cos/sin caches from ``freqs``.
Same as flash_attn / vortex RotaryEmbedding: with xPos (``rope.scale``
set), queries use cos*scale and keys cos/scale, multiplied in fp32.
"""
if rope.scale is None:
rope._cos_cached = torch.cos(freqs).to(dtype)
rope._sin_cached = torch.sin(freqs).to(dtype)
return
power = (
torch.arange(seqlen, dtype=rope.scale.dtype, device=rope.scale.device) - seqlen // 2
) / rope.scale_base
scale = rope.scale.to(device=power.device) ** power[:, None]
rope._cos_cached = (torch.cos(freqs) * scale).to(dtype)
rope._sin_cached = (torch.sin(freqs) * scale).to(dtype)
rope._cos_k_cached = (torch.cos(freqs) / scale).to(dtype)
rope._sin_k_cached = (torch.sin(freqs) / scale).to(dtype)
class _PureRotaryEmbedding(nn.Module):
"""Pure-PyTorch fallback RoPE (used when flash_attn is unavailable).
Mirrors the public surface of ``flash_attn.layers.rotary.RotaryEmbedding``
for the subset used by the Evo1 attention block: exposes ``inv_freq`` as
a non-persistent runtime buffer and a forward(qkv) -> qkv method that
rotates Q and K.
"""
def __init__(
self,
dim: int,
base: float = 10000.0,
interleaved: bool = False,
scale_base: float | None = None,
pos_idx_in_fp32: bool = True,
device=None,
):
super().__init__()
self.dim = dim
self.base = float(base)
self.interleaved = interleaved
self.scale_base = scale_base
self.pos_idx_in_fp32 = pos_idx_in_fp32
inv_freq = self._compute_inv_freq(device=device)
self.register_buffer("inv_freq", inv_freq, persistent=False)
scale = (
(torch.arange(0, dim, 2, device=device, dtype=torch.float32) + 0.4 * dim) / (1.4 * dim)
if scale_base is not None
else None
)
self.register_buffer("scale", scale, persistent=False)
self._seq_len_cached = 0
self._cos_cached: torch.Tensor | None = None
self._sin_cached: torch.Tensor | None = None
self._cos_k_cached: torch.Tensor | None = None
self._sin_k_cached: torch.Tensor | None = None
def _compute_inv_freq(self, device=None) -> torch.Tensor:
return 1.0 / (
self.base
** (torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) / self.dim)
)
def _update_cos_sin_cache(self, seqlen: int, device=None, dtype=None):
if (
seqlen > self._seq_len_cached
or self._cos_cached is None
or self._cos_cached.device != device
or self._cos_cached.dtype != dtype
or (self.training and self._cos_cached.is_inference())
):
self._seq_len_cached = seqlen
if self.pos_idx_in_fp32:
t = torch.arange(seqlen, device=device, dtype=torch.float32)
inv_freq = self._compute_inv_freq(device=device)
else:
t = torch.arange(seqlen, device=device, dtype=self.inv_freq.dtype)
inv_freq = self.inv_freq
_set_cos_sin_tables(self, torch.outer(t, inv_freq), seqlen, dtype)
def forward(
self,
qkv: torch.Tensor,
seqlen_offset: int | torch.Tensor = 0,
max_seqlen: int | None = None,
) -> torch.Tensor:
"""Rotate Q and K of a packed (B, T, 3, H, D) qkv tensor.
seqlen_offset is supported as int only (no per-sample offsets); for
the inference KV-cache fast path we fall back to int(seqlen_offset).
"""
if isinstance(seqlen_offset, torch.Tensor):
seqlen_offset = int(seqlen_offset.max().item())
T = qkv.shape[1]
seqlen = max_seqlen if max_seqlen is not None else (T + seqlen_offset)
self._update_cos_sin_cache(seqlen, device=qkv.device, dtype=qkv.dtype)
span = slice(seqlen_offset, seqlen_offset + T)
cos = self._cos_cached[span][None, :, None, :]
sin = self._sin_cached[span][None, :, None, :]
if self.scale is None:
cos_k, sin_k = cos, sin
else:
cos_k = self._cos_k_cached[span][None, :, None, :]
sin_k = self._sin_k_cached[span][None, :, None, :]
q, k, v = qkv.unbind(dim=2)
q = _apply_rotary(q, cos, sin, self.interleaved)
k = _apply_rotary(k, cos_k, sin_k, self.interleaved)
return torch.stack((q, k, v), dim=2)
if _HAS_FLASH_ROTARY:
import inspect as _inspect
# flash-attn >= 2.8 dropped this argument and always uses fp32 positions.
_FLASH_TAKES_POS_IDX = (
"pos_idx_in_fp32" in _inspect.signature(_FlashRotaryEmbedding.__init__).parameters
)
class RotaryEmbedding(_FlashRotaryEmbedding):
"""Flash RoPE with exact fp32 frequency reconstruction."""
def __init__(self, *args, pos_idx_in_fp32: bool = True, **kwargs):
if _FLASH_TAKES_POS_IDX:
kwargs["pos_idx_in_fp32"] = pos_idx_in_fp32
elif not pos_idx_in_fp32:
raise ValueError(
"pos_idx_in_fp32=False needs flash-attn < 2.8, which always uses fp32 "
"positions; uninstall flash-attn to use the PyTorch fallback instead."
)
super().__init__(*args, **kwargs)
self.pos_idx_in_fp32 = pos_idx_in_fp32
def _update_cos_sin_cache(self, seqlen, device=None, dtype=None):
needs_update = (
seqlen > self._seq_len_cached
or self._cos_cached is None
or self._cos_cached.device != device
or self._cos_cached.dtype != dtype
or (self.training and self._cos_cached.is_inference())
)
if needs_update and self.pos_idx_in_fp32:
restore_rotary_inv_freq(
self,
device=device,
invalidate_cache=False,
)
return super()._update_cos_sin_cache(
seqlen,
device=device,
dtype=dtype,
)
else:
RotaryEmbedding = _PureRotaryEmbedding
class LinearlyScaledRotaryEmbedding(RotaryEmbedding):
"""RoPE with linear interpolation of position indices.
Used for evo-1-131k-base: positions are divided by ``scaling_factor``
before the cos/sin tables are computed, effectively stretching the
trained context. The override is the same shape regardless of whether
the parent class is flash_attn's RotaryEmbedding or our pure-PyTorch
fallback (both expose the same ``_update_cos_sin_cache`` hook).
"""
def __init__(self, dim: int, scaling_factor: float = 1.0, **kwargs):
super().__init__(dim=dim, **kwargs)
self._linear_scaling_factor = float(scaling_factor)
def _update_cos_sin_cache(self, seqlen, device=None, dtype=None):
# Mirrors the parent body but divides position indices by the linear
# scaling factor before computing the cos/sin tables.
if (
seqlen <= self._seq_len_cached
and self._cos_cached is not None
and self._cos_cached.device == device
and self._cos_cached.dtype == dtype
and not (self.training and self._cos_cached.is_inference())
):
return
self._seq_len_cached = seqlen
if self.pos_idx_in_fp32:
t = torch.arange(seqlen, device=device, dtype=torch.float32)
t = t / self._linear_scaling_factor
inv_freq = self._compute_inv_freq(device=device)
else:
t = torch.arange(seqlen, device=device, dtype=self.inv_freq.dtype)
t = t / self._linear_scaling_factor
inv_freq = self.inv_freq
_set_cos_sin_tables(self, torch.outer(t, inv_freq), seqlen, dtype)
def swap_mha_rope(mha, new_rope=LinearlyScaledRotaryEmbedding, kwargs_new_rope=None):
"""Replace ``mha.rotary_emb`` with a freshly-constructed scaled RoPE.
Mirrors ``stripedhyena.positional_embeddings.swap_mha_rope``: inherits
dim/base/interleaved/scale_base/pos_idx_in_fp32 from the existing rope,
deletes the old module, and attaches a new one of ``new_rope`` type
configured with ``kwargs_new_rope``.
"""
weight_attr = "Wq" if getattr(mha, "cross_attn", False) else "Wqkv"
weight = getattr(mha, weight_attr).weight
dtype = weight.dtype
kwargs_old_rope = dict(
dim=mha.rotary_emb.dim,
base=mha.rotary_emb.base,
interleaved=mha.rotary_emb.interleaved,
scale_base=mha.rotary_emb.scale_base,
pos_idx_in_fp32=mha.rotary_emb.pos_idx_in_fp32,
device=mha.rotary_emb.inv_freq.device,
)
del mha.rotary_emb
kwargs_new_rope = kwargs_new_rope or {"scaling_factor": 1.0}
scaled = new_rope(**kwargs_new_rope, **kwargs_old_rope).to(dtype)
restore_rotary_inv_freq(scaled)
mha.rotary_emb = scaled
return mha