Spaces:
Sleeping
Sleeping
File size: 10,148 Bytes
a42bb99 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | """
Morphable Hybrid Attention — layer-level linearization with learnable selection,
combined with TriAttention token-level KV compression on the surviving full layers.
Implements the four-stage pipeline from the "Morphable Layer" figure:
(1) Morphable Layer Construction
Every attention layer gets a frozen Full-Attention path A_full (the pretrained
MLADerfXSAAttention) and a trainable Linear-Attention sibling A_lin that reuses
the frozen Q/K/V/O projections but replaces softmax with a learnable positive
feature map (LoLCATs-style). A_lin is trained to match A_full per layer:
L_hidden = (1/L) Σ_l || H_lin^(l) - H_full^(l) ||²
(2) Layer Selection via Joint Opt. + Linearization Reg.
Each layer mixes the two paths through a learnable gate α^(l) ∈ (0,1):
H_mix^(l) = α^(l) · H_full^(l) + (1 - α^(l)) · H_lin^(l)
trained on synthetic passkey-retrieval data with
L_total = L_align + λ L_reg
= (1/(L|T|)) Σ_l Σ_t || H_mix,t^(l) - H_full,t^(l) ||² + λ Σ_l α^(l)
The α-penalty pushes layers toward linear unless full attention is genuinely
needed (retrieval heads), so α ranks layer importance.
(3) Discretize Hybrid Layers
Keep the top-k layers by α as Full Attention; linearize the rest.
(4) Distillation & Finetuning
Logits KL distillation (student hybrid vs. frozen teacher) + long-context FT.
Combined with triattention.py: the discretized "full" layers still hold a KV cache,
so TriAttention prunes them to a token budget; the "linear" layers carry an O(1)
recurrent state instead. See model.py generate_hybrid().
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
# --------------------------------------------------------------------------- #
# trainable linear attention (reuses the frozen full-attention projections)
# --------------------------------------------------------------------------- #
def _feature_map(x, temp):
"""Positive feature map φ(x) = elu(x·temp) + 1 (LoLCATs-style, learnable temp)."""
return F.elu(x * temp) + 1.0
class LinearAttention(nn.Module):
"""Linear-attention twin of MLADerfXSAAttention.
Shares (by reference) the frozen full-attention Q/K/V/O projections, QK-norm and
RoPE, and only learns a small per-head feature-map temperature. Softmax(qkᵀ) is
replaced by the kernel weight φ(q)·φ(k), giving causal linear attention with an
O(d²) recurrent state for decoding:
out_t = Σ_{s≤t} (φ(q_t)·φ(k_s)) v_s / Σ_{s≤t} (φ(q_t)·φ(k_s))
"""
def __init__(self, full_attn, cfg):
super().__init__()
self.full = full_attn # frozen; used only for its weights
self.num_heads = cfg.n_head
self.num_kv_heads = cfg.num_key_value_heads
self.head_dim = cfg.head_dim
self.nope_head_dim = cfg.nope_head_dim
self.kv_groups = self.num_heads // self.num_kv_heads
self.use_qk_norm = cfg.use_qk_norm
# learnable per-head feature-map temperatures (init 1 -> φ = elu+1)
self.q_temp = nn.Parameter(torch.ones(self.num_heads, 1))
self.k_temp = nn.Parameter(torch.ones(self.num_kv_heads, 1))
def _project(self, x, position_ids):
"""Reuse the frozen full path's projections + RoPE to get post-RoPE q,k,v."""
f = self.full
B, S, _ = x.shape
q = f.q_b_proj(f.q_a_norm(f.q_a_proj(x)))
q = q.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
k = f.k_proj(x).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2)
v = f.v_proj(x).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2)
if self.use_qk_norm:
q, k = f.q_norm(q), f.k_norm(k)
d = self.nope_head_dim
q = torch.cat([q[..., :d], f.rope(q[..., d:], position_ids)], dim=-1)
k = torch.cat([k[..., :d], f.rope(k[..., d:], position_ids)], dim=-1)
return q, k, v
def _out(self, y):
f = self.full
B, H, S, D = y.shape
y = y.transpose(1, 2).contiguous().view(B, S, H * D)
return f.o_b_proj(f.o_a_proj(y))
def forward(self, x, position_ids, past_kv=None, use_cache=False):
f = self.full
B, S, _ = x.shape
q, k, v = self._project(x, position_ids)
# expand kv heads to query heads (GQA)
if self.kv_groups > 1:
k = k.repeat_interleave(self.kv_groups, dim=1)
v = v.repeat_interleave(self.kv_groups, dim=1)
qt = self.q_temp.repeat_interleave(1, 0).view(1, self.num_heads, 1, 1)
kt = self.k_temp.repeat_interleave(self.kv_groups, 0).view(1, self.num_heads, 1, 1)
phi_q = _feature_map(q, qt) # [B,H,S,D]
phi_k = _feature_map(k, kt)
# recurrent decoding: carry state (KV = Σ φk⊗v, Z = Σ φk)
if use_cache and past_kv is not None:
state_kv, state_z = past_kv[1], past_kv[2] # ('linear', KV, Z)
# accumulate this step's tokens into the state, then read out causally
outs = []
for t in range(S):
pk, vv = phi_k[:, :, t], v[:, :, t] # [B,H,D]
state_kv = state_kv + pk.unsqueeze(-1) * vv.unsqueeze(-2) # [B,H,D,D]
state_z = state_z + pk # [B,H,D]
pq = phi_q[:, :, t]
num = (pq.unsqueeze(-2) @ state_kv).squeeze(-2) # [B,H,D]
den = (pq * state_z).sum(-1, keepdim=True).clamp_min(1e-6)
outs.append(num / den)
y = torch.stack(outs, dim=2) # [B,H,S,D]
present = ("linear", state_kv, state_z)
return self._out(y), present
# parallel form (exact same result), O(S²) — used for training / prefill
w = torch.matmul(phi_q, phi_k.transpose(-2, -1)) # [B,H,S,S] kernel weights
offset = 0 if past_kv is None else 0
qpos = torch.arange(S, device=x.device).view(S, 1)
kpos = torch.arange(S, device=x.device).view(1, S)
w = w.masked_fill((kpos > qpos).unsqueeze(0).unsqueeze(0), 0.0)
w = w / w.sum(-1, keepdim=True).clamp_min(1e-6)
y = torch.matmul(w, v) # [B,H,S,D]
if use_cache:
# build the recurrent state from the full prefix for later decoding
state_kv = torch.einsum("bhsd,bhse->bhde", phi_k, v) # [B,H,D,D]
state_z = phi_k.sum(2) # [B,H,D]
return self._out(y), ("linear", state_kv, state_z)
return self._out(y)
# --------------------------------------------------------------------------- #
# morphable wrapper: full + linear + learnable gate α
# --------------------------------------------------------------------------- #
class MorphableAttention(nn.Module):
"""Wraps the pretrained full attention with a linear twin and a per-layer gate.
mode:
'mix' — H = α·H_full + (1-α)·H_lin (stages 1-2; captures alignment loss)
'full' — H = H_full (discretized: selected layer)
'linear' — H = H_lin (discretized: linearized layer)
"""
def __init__(self, full_attn, cfg, alpha_init=0.5):
super().__init__()
self.full = full_attn
self.lin = LinearAttention(full_attn, cfg)
# gate stored as a logit; α = sigmoid(logit)
self.alpha_logit = nn.Parameter(torch.tensor(math.log(alpha_init / (1 - alpha_init))))
self.mode = "mix"
self.last_hidden_align = None # ||H_lin - H_full||² captured on the last forward
@property
def alpha(self):
return torch.sigmoid(self.alpha_logit)
def freeze_full(self):
for p in self.full.parameters():
p.requires_grad_(False)
def forward(self, x, position_ids, past_kv=None, use_cache=False):
if self.mode == "full":
return self.full(x, position_ids, past_kv=past_kv, use_cache=use_cache)
if self.mode == "linear":
return self.lin(x, position_ids, past_kv=past_kv, use_cache=use_cache)
# 'mix': run both paths (no cache during training/selection)
h_full = self.full(x, position_ids)
h_lin = self.lin(x, position_ids)
self.last_hidden_align = ((h_lin - h_full) ** 2).mean()
a = self.alpha
h_mix = a * h_full + (1.0 - a) * h_lin
if use_cache:
return h_mix, None
return h_mix
# --------------------------------------------------------------------------- #
# stage-3 discretization + loss helpers
# --------------------------------------------------------------------------- #
@torch.no_grad()
def discretize(model, k_full):
"""Keep the top-k layers by α as full attention; linearize the rest.
Returns the list of selected (full) layer indices."""
morphs = [layer.attn for layer in model.layers
if isinstance(layer.attn, MorphableAttention)]
alphas = torch.stack([m.alpha.detach() for m in morphs])
keep = set(torch.topk(alphas, min(k_full, len(morphs))).indices.tolist())
for i, m in enumerate(morphs):
m.mode = "full" if i in keep else "linear"
return sorted(keep)
def hidden_alignment_loss(model):
"""L_hidden = mean_l ||H_lin^(l) - H_full^(l)||² (stage 1)."""
terms = [layer.attn.last_hidden_align for layer in model.layers
if isinstance(layer.attn, MorphableAttention)
and layer.attn.last_hidden_align is not None]
if not terms:
return None
return torch.stack(terms).mean()
def linearization_reg(model):
"""L_reg = Σ_l α^(l) (stage 2 penalty that pushes layers toward linear)."""
terms = [layer.attn.alpha for layer in model.layers
if isinstance(layer.attn, MorphableAttention)]
if not terms:
return None
return torch.stack(terms).sum()
def set_mode(model, mode):
for layer in model.layers:
if isinstance(layer.attn, MorphableAttention):
layer.attn.mode = mode
|