Wiola360M / components /srpe.py
aryuemaan's picture
Upload folder using huggingface_hub
2db32a1 verified
Raw
History Blame Contribute Delete
4.61 kB
# coding=utf-8
"""Spiral Rotary Positional Encoding (SRPE).
Implements Section VI of the Wiola paper. For position ``p`` and dimension-pair
index ``j in [d_h/2]``:
omega_j = theta_0 ** (-2j / d_h)
Theta_j(p)= p * omega_j * (1 + 1/k_s)
r_j(p) = 1 + a_s * sin(p * f_s * omega_j)
c_j(p) = r_j(p) * cos(Theta_j(p))
s_j(p) = r_j(p) * sin(Theta_j(p))
and the rotation is applied to q/k with the standard rotate-half trick, which
is algebraically identical to Eqs. (12)-(13):
out_lo = q_lo * c - q_hi * s
out_hi = q_hi * c + q_lo * s
Unlike RoPE, ``Theta`` carries a second winding angle (via ``1 + 1/k_s``) and
the magnitude is modulated by a radial term ``r_j(p)``, embedding positions on a
3D helical manifold with no learned parameters.
"""
import torch
import torch.nn as nn
def rotate_half(x: torch.Tensor) -> torch.Tensor:
"""Rotate the two halves of the last dimension: [x1, x2] -> [-x2, x1]."""
half = x.shape[-1] // 2
x1 = x[..., :half]
x2 = x[..., half:]
return torch.cat((-x2, x1), dim=-1)
class SpiralRotaryEmbedding(nn.Module):
def __init__(
self,
head_dim: int,
max_position_embeddings: int = 2048,
theta: float = 10000.0,
spiral_divisor: int = 8,
radial_amplitude: float = 0.1,
radial_frequency: float = 0.01,
):
super().__init__()
if head_dim % 2 != 0:
raise ValueError(f"head_dim must be even, got {head_dim}.")
self.head_dim = head_dim
self.max_position_embeddings = max_position_embeddings
self.theta = theta
self.k_s = spiral_divisor
self.a_s = radial_amplitude
self.f_s = radial_frequency
# omega_j = theta ** (-2j / d_h), j in [0, d_h/2).
j = torch.arange(0, head_dim, 2, dtype=torch.float32)
inv_freq = theta ** (-(j / head_dim)) # shape [d_h/2]
self.register_buffer("inv_freq", inv_freq, persistent=False)
self._cached_len = 0
self._build_cache(max_position_embeddings)
def _build_cache(self, seq_len: int, device=None, dtype=torch.float32):
positions = torch.arange(
seq_len,
dtype=torch.float32,
device=device if device is not None else self.inv_freq.device,
)
inv_freq = self.inv_freq.to(positions.device)
# Combined winding angle Theta_j(p) = p * omega_j * (1 + 1/k_s).
combined = 1.0 + 1.0 / self.k_s
angle = torch.outer(positions, inv_freq) * combined # [T, d_h/2]
# Radial modulation r_j(p) = 1 + a_s * sin(p * f_s * omega_j).
radial = 1.0 + self.a_s * torch.sin(torch.outer(positions, inv_freq) * self.f_s)
cos = radial * torch.cos(angle)
sin = radial * torch.sin(angle)
# Duplicate to full head_dim so it lines up with rotate_half.
cos = torch.cat((cos, cos), dim=-1) # [T, d_h]
sin = torch.cat((sin, sin), dim=-1)
self.register_buffer("cos_cached", cos.to(dtype), persistent=False)
self.register_buffer("sin_cached", sin.to(dtype), persistent=False)
self._cached_len = seq_len
@torch.no_grad()
def _maybe_extend(self, seq_len: int, device, dtype):
if seq_len > self._cached_len or self.cos_cached.device != device:
self._build_cache(max(seq_len, self._cached_len), device=device, dtype=dtype)
def forward(self, x: torch.Tensor, position_ids: torch.Tensor):
"""Return (cos, sin) gathered for ``position_ids``.
Args:
x: any tensor only used to read dtype/device.
position_ids: LongTensor of shape [batch, seq_len].
Returns:
cos, sin each of shape [batch, seq_len, head_dim].
"""
max_pos = int(position_ids.max().item()) + 1
self._maybe_extend(max_pos, x.device, torch.float32)
cos = self.cos_cached[position_ids] # [B, S, d_h]
sin = self.sin_cached[position_ids]
return cos.to(x.dtype), sin.to(x.dtype)
def apply_srpe(q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
"""Apply SRPE rotation to query and key tensors.
Args:
q, k: shape [batch, num_heads, seq_len, head_dim].
cos, sin: shape [batch, seq_len, head_dim].
"""
cos = cos.unsqueeze(1) # [B, 1, S, d_h] -> broadcast over heads
sin = sin.unsqueeze(1)
q_rot = (q * cos) + (rotate_half(q) * sin)
k_rot = (k * cos) + (rotate_half(k) * sin)
return q_rot, k_rot