Wiola360M / components /gcla.py
aryuemaan's picture
Upload folder using huggingface_hub
2db32a1 verified
Raw
History Blame Contribute Delete
6.17 kB
# coding=utf-8
"""Gated Cross-Layer Attention (GCLA).
Implements Section VII of the Wiola paper. Each decoder layer performs:
* GQA self-attention with SRPE on the local sequence, and
* cross-attention to compressed summaries of up to ``Lambda`` preceding
layers (supplied by the model as ``context_summaries``),
blended by a scalar gate ``beta = sigmoid(phi)`` and modulated by a sigmoid
output gate ``G``:
O = (1 - beta) * O_self + beta * O_ctx
A = (G * concat(O)) W_O
The context tensor is provided *per query position* as a causal cumulative mean
of prior-layer outputs (see :class:`WiolaModel`), so a cached incremental decode
reproduces a full forward pass exactly.
"""
import math
from typing import Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from .srpe import SpiralRotaryEmbedding, apply_srpe
def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
"""Expand [B, H_kv, T, d] -> [B, H_kv*n_rep, T, d] (GQA)."""
if n_rep == 1:
return x
b, kv, t, d = x.shape
x = x[:, :, None, :, :].expand(b, kv, n_rep, t, d)
return x.reshape(b, kv * n_rep, t, d)
class GatedCrossLayerAttention(nn.Module):
def __init__(self, config, layer_idx: int):
super().__init__()
self.layer_idx = layer_idx
self.hidden_size = config.hidden_size
self.num_heads = config.num_attention_heads
self.num_kv_heads = config.num_key_value_heads
self.head_dim = config.head_dim
self.n_rep = self.num_heads // self.num_kv_heads
self.lookback = config.gcla_lookback
q_dim = self.num_heads * self.head_dim
kv_dim = self.num_kv_heads * self.head_dim
self.q_proj = nn.Linear(self.hidden_size, q_dim, bias=False)
self.k_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
self.v_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
self.o_proj = nn.Linear(q_dim, self.hidden_size, bias=False)
# Cross-layer context projections.
self.k_ctx_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
self.v_ctx_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
# Sigmoid output gate.
self.gate_proj = nn.Linear(self.hidden_size, q_dim, bias=False)
# Scalar blend gate beta = sigmoid(phi).
self.blend_logit = nn.Parameter(torch.tensor(float(config.gcla_gate_init)))
self.srpe = SpiralRotaryEmbedding(
head_dim=self.head_dim,
max_position_embeddings=config.max_position_embeddings,
theta=config.srpe_theta,
spiral_divisor=config.srpe_spiral_divisor,
radial_amplitude=config.srpe_radial_amplitude,
radial_frequency=config.srpe_radial_frequency,
)
def forward(
self,
hidden_states: torch.Tensor, # [B, T, d]
position_ids: torch.Tensor, # [B, T]
attention_mask: Optional[torch.Tensor], # [B, 1, T, T_k] additive
context_summaries: Optional[torch.Tensor] = None, # [B, T, Lambda, d]
past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
use_cache: bool = False,
):
bsz, q_len, _ = hidden_states.shape
q = self.q_proj(hidden_states)
k = self.k_proj(hidden_states)
v = self.v_proj(hidden_states)
q = q.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(bsz, q_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
v = v.view(bsz, q_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
# SRPE rotation on q/k (per head).
cos, sin = self.srpe(hidden_states, position_ids)
q, k = apply_srpe(q, k, cos, sin)
# Concatenate cached KV (incremental decoding).
# Concatenate cached KV (incremental decoding).
if (
past_key_value is not None
and past_key_value[0] is not None # ← guard against placeholder None
and past_key_value[1] is not None
):
past_k, past_v = past_key_value
k = torch.cat([past_k, k], dim=2)
v = torch.cat([past_v, v], dim=2)
present = (k, v) if use_cache else None
# GQA expansion.
k_rep = repeat_kv(k, self.n_rep)
v_rep = repeat_kv(v, self.n_rep)
scale = 1.0 / math.sqrt(self.head_dim)
scores = torch.matmul(q, k_rep.transpose(-1, -2)) * scale # [B,H,T,T_k]
if attention_mask is not None:
scores = scores + attention_mask
attn = F.softmax(scores, dim=-1, dtype=torch.float32).to(q.dtype)
o_self = torch.matmul(attn, v_rep) # [B,H,T,dh]
# Cross-layer context attention.
if context_summaries is not None and context_summaries.shape[2] > 0:
lam = context_summaries.shape[2]
k_ctx = self.k_ctx_proj(context_summaries) # [B,T,Lam,kv_dim]
v_ctx = self.v_ctx_proj(context_summaries)
k_ctx = k_ctx.view(bsz, q_len, lam, self.num_kv_heads, self.head_dim)
v_ctx = v_ctx.view(bsz, q_len, lam, self.num_kv_heads, self.head_dim)
# Expand kv heads to full head count.
k_ctx = k_ctx.repeat_interleave(self.n_rep, dim=3) # [B,T,Lam,H,dh]
v_ctx = v_ctx.repeat_interleave(self.n_rep, dim=3)
# scores[b,h,t,l] = q[b,h,t,:] . k_ctx[b,t,l,h,:]
ctx_scores = torch.einsum("bhtd,btlhd->bhtl", q, k_ctx) * scale
ctx_attn = F.softmax(ctx_scores, dim=-1, dtype=torch.float32).to(q.dtype)
o_ctx = torch.einsum("bhtl,btlhd->bhtd", ctx_attn, v_ctx)
beta = torch.sigmoid(self.blend_logit)
o = (1.0 - beta) * o_self + beta * o_ctx
else:
o = o_self
# Merge heads.
o = o.transpose(1, 2).contiguous().view(bsz, q_len, self.num_heads * self.head_dim)
# Sigmoid output gate.
g = torch.sigmoid(self.gate_proj(hidden_states))
o = self.o_proj(g * o)
return o, present