cma-1M-exp / modeling_cma.py
User01110's picture
Best step 1000: Open SLM average 32.03%
3560071 verified
Raw
History Blame Contribute Delete
13.8 kB
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
# CMA is text-only. Disable Transformers' optional vision backend before it
# imports PreTrainedModel: some cloud images expose a system torchvision built
# against a different PyTorch, which otherwise crashes on torchvision::nms.
from transformers.utils import import_utils as _transformers_import_utils
_transformers_import_utils._torchvision_available = False
_transformers_import_utils.is_torchvision_available = lambda: False
try:
from transformers import GenerationMixin
except ImportError:
from transformers.generation import GenerationMixin
from transformers import PretrainedConfig, PreTrainedModel
from transformers.modeling_outputs import CausalLMOutputWithPast
class CMAConfig(PretrainedConfig):
model_type = "cma"
def __init__(
self,
vocab_size=256,
seq_len=4096,
d_model=96,
n_layers=12,
n_heads=6,
n_kv_heads=2,
chunk=8,
cma_heads=2,
expand=2,
cma_identity_prob=0.90,
max_position_embeddings=None,
n_positions=None,
n_ctx=None,
**kwargs,
):
super().__init__(**kwargs)
self.vocab_size = vocab_size
self.seq_len = seq_len
self.max_position_embeddings = max_position_embeddings or seq_len
self.n_positions = n_positions or self.max_position_embeddings
self.n_ctx = n_ctx or self.max_position_embeddings
self.d_model = d_model
self.n_layers = n_layers
self.n_heads = n_heads
self.n_kv_heads = n_kv_heads
self.chunk = chunk
self.cma_heads = cma_heads
self.expand = expand
self.cma_identity_prob = cma_identity_prob
class RMSNorm(nn.Module):
def __init__(self, d):
super().__init__()
self.w = nn.Parameter(torch.ones(d))
def forward(self, x):
return F.rms_norm(
x, (x.shape[-1],), self.w.to(dtype=x.dtype), eps=1e-6
)
def rope_cache(seq_len, head_dim, device, base=10000.0):
inv = 1.0 / (
base ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim)
)
t = torch.arange(seq_len, device=device).float()
freqs = torch.outer(t, inv)
return torch.cos(freqs), torch.sin(freqs)
def apply_rope(x, cos, sin):
x1, x2 = x.chunk(2, dim=-1)
return torch.cat([x1 * cos - x2 * sin, x2 * cos + x1 * sin], dim=-1)
class TokenAttention(nn.Module):
def __init__(self, d, n_heads, n_kv_heads):
super().__init__()
if d % n_heads or n_heads % n_kv_heads:
raise ValueError("d_model % n_heads and n_heads % n_kv_heads must be zero.")
self.h, self.kv_h, self.hd = n_heads, n_kv_heads, d // n_heads
self.q = nn.Linear(d, d, bias=False)
self.k = nn.Linear(d, n_kv_heads * self.hd, bias=False)
self.v = nn.Linear(d, n_kv_heads * self.hd, bias=False)
self.o = nn.Linear(d, d, bias=False)
self.qn, self.kn = RMSNorm(self.hd), RMSNorm(self.hd)
def forward(self, x, cos, sin):
B, T, d = x.shape
q = self.q(x).view(B, T, self.h, self.hd).transpose(1, 2)
k = self.k(x).view(B, T, self.kv_h, self.hd).transpose(1, 2)
v = self.v(x).view(B, T, self.kv_h, self.hd).transpose(1, 2)
q, k = self.qn(q), self.kn(k)
q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
# Native GQA keeps compact K/V heads and lets SDPA select a fused CUDA
# implementation without materializing repeated K/V activations.
y = F.scaled_dot_product_attention(
q,
k,
v,
is_causal=True,
enable_gqa=self.h != self.kv_h,
)
return self.o(y.transpose(1, 2).reshape(B, T, d))
class CMA(nn.Module):
def __init__(self, d, chunk=32, heads=4, expand=2, identity_prob=0.90):
super().__init__()
if min(d, chunk, heads, expand) <= 0:
raise ValueError("CMA dimensions and expansion must be positive.")
if d % chunk or chunk % heads:
raise ValueError("CMA requires d % chunk == 0 and chunk % heads == 0.")
if d // chunk < 2:
raise ValueError("CMA requires at least two channel chunks.")
self.n, self.c, self.h = d // chunk, chunk, heads
self.hd = chunk // heads
self.chunk_emb = nn.Parameter(torch.randn(self.n, chunk) * 0.02)
self.wqk = nn.Parameter(torch.randn(self.n, chunk, 2 * chunk) * 0.02)
self.global_proj = nn.Linear(d, chunk, bias=False)
self.wv = nn.Linear(d, d * expand, bias=False)
self.bias = nn.Parameter(torch.zeros(heads, self.n, self.n))
self.qn, self.kn = RMSNorm(self.hd), RMSNorm(self.hd)
self.logit_scale = nn.Parameter(torch.zeros(heads))
self.layer_gain = nn.Parameter(torch.zeros(heads))
self.route_gate_weight = nn.Parameter(
torch.randn(heads, self.hd) * 0.02
)
self.route_gate_bias = nn.Parameter(torch.zeros(heads))
self.gate = nn.Linear(d, d * expand, bias=False)
self.o = nn.Linear(d * expand, d, bias=False)
nn.init.zeros_(self.o.weight)
if not 0.0 < identity_prob < 1.0:
raise ValueError("cma_identity_prob must be between zero and one.")
diagonal_bias = math.log(
(self.n - 1) * identity_prob / (1.0 - identity_prob)
)
with torch.no_grad():
self.bias.add_(torch.eye(self.n) * diagonal_bias)
self.expand = expand
self.record_diagnostics = False
self.last_diagnostics = None
def forward(self, x):
B, T, d = x.shape
xc = x.view(B * T, self.n, self.c)
global_state = self.global_proj(x).reshape(B * T, 1, self.c)
qk_in = xc + self.chunk_emb + global_state
value_slots = self.wv(x).reshape(
B * T, self.n, self.h, self.hd, self.expand
)
# Key j summarizes value slot j, restoring the key/value contract while
# retaining the full dense value projection and exact parameter count.
key_in = value_slots.mean(dim=-1).reshape(B * T, self.n, self.c)
key_in = key_in + self.chunk_emb
q = torch.einsum("bnc,nco->bno", qk_in, self.wqk[..., : self.c])
k = torch.einsum("bnc,nco->bno", key_in, self.wqk[..., self.c :])
v = value_slots.reshape(
B * T, self.n, self.h, self.hd * self.expand
)
q = self.qn(q.view(B * T, self.n, self.h, self.hd)).transpose(1, 2)
k = self.kn(k.view(B * T, self.n, self.h, self.hd)).transpose(1, 2)
v = v.transpose(1, 2)
q = F.normalize(q.float(), dim=-1).to(v.dtype)
k = F.normalize(k.float(), dim=-1).to(v.dtype)
scale = self.logit_scale.clamp(max=math.log(100.0)).exp()
scale = scale.view(1, self.h, 1, 1)
attn_logits = (q @ k.transpose(-2, -1)) * scale.to(q.dtype)
attn_logits = attn_logits + self.bias.unsqueeze(0).to(q.dtype)
attn = F.softmax(attn_logits, dim=-1, dtype=torch.float32).to(v.dtype)
routed = attn @ v
route_signal = (
torch.einsum(
"bhnd,hd->bhn", q.float(), self.route_gate_weight.float()
)
+ self.route_gate_bias.float().view(1, self.h, 1)
+ self.layer_gain.float().view(1, self.h, 1)
)
route_coeff = torch.tanh(route_signal).to(v.dtype)
contribution = route_coeff.unsqueeze(-1) * (routed - v)
y = v + contribution
if self.record_diagnostics:
probs = attn.float().clamp_min(1e-9)
entropy = -(probs * probs.log()).sum(dim=-1) / math.log(self.n)
diagonal_mass = probs.diagonal(dim1=-2, dim2=-1).mean()
if self.h > 1:
flattened = probs.transpose(0, 1).reshape(self.h, -1)
normalized = F.normalize(flattened, dim=-1)
similarity = normalized @ normalized.mT
off_diagonal = (
similarity.sum() - similarity.diagonal().sum()
) / (self.h * (self.h - 1))
else:
off_diagonal = probs.new_zeros(())
self.last_diagnostics = {
"entropy": entropy.mean().item(),
"diagonal_mass": diagonal_mass.item(),
"head_similarity": off_diagonal.item(),
"gate_mean": route_coeff.float().mean().item(),
"gate_std": route_coeff.float().std(unbiased=False).item(),
"gate_abs_mean": route_coeff.float().abs().mean().item(),
"gate_saturation": (
route_coeff.float().abs() > 0.95
).float().mean().item(),
"contribution_ratio": (
contribution.float().norm() / (v.float().norm() + 1e-9)
).item(),
}
y = y.transpose(1, 2).reshape(B, T, d * self.expand)
return self.o(y * F.silu(self.gate(x)))
class Block(nn.Module):
def __init__(self, config):
super().__init__()
d = config.d_model
self.n1, self.n2 = RMSNorm(d), RMSNorm(d)
self.attn = TokenAttention(d, config.n_heads, config.n_kv_heads)
self.mix = CMA(
d,
config.chunk,
config.cma_heads,
config.expand,
config.cma_identity_prob,
)
def forward(self, x, cos, sin):
x = x + self.attn(self.n1(x), cos, sin)
x = x + self.mix(self.n2(x))
return x
class CMAModel(nn.Module):
def __init__(self, config):
super().__init__()
d = config.d_model
self.config = config
self.emb = nn.Embedding(config.vocab_size, d)
self.blocks = nn.ModuleList(Block(config) for _ in range(config.n_layers))
self.norm = RMSNorm(d)
hd = d // config.n_heads
cos, sin = rope_cache(config.seq_len, hd, "cpu")
self.register_buffer("cos", cos)
self.register_buffer("sin", sin)
def forward_hidden(self, idx):
if idx.size(1) > self.config.seq_len:
idx = idx[:, -self.config.seq_len :]
x = self.emb(idx)
device_type = x.device.type
rope_dtype = (
torch.get_autocast_dtype(device_type)
if torch.is_autocast_enabled(device_type)
else x.dtype
)
cos = self.cos[: idx.size(1)].to(device=idx.device, dtype=rope_dtype)
sin = self.sin[: idx.size(1)].to(device=idx.device, dtype=rope_dtype)
for b in self.blocks:
x = b(x, cos, sin)
return self.norm(x)
class CMAForCausalLM(PreTrainedModel, GenerationMixin):
config_class = CMAConfig
base_model_prefix = "model"
_tied_weights_keys = {"head.weight": "model.emb.weight"}
all_tied_weights_keys = {"head.weight": "model.emb.weight"}
def __init__(self, config):
super().__init__(config)
self.model = CMAModel(config)
self.head = nn.Linear(config.d_model, config.vocab_size, bias=False)
# Modern Transformers creates loader metadata and performs configured
# tying in post_init(); omitting it leaves all_tied_weights_keys absent.
self.post_init()
self.head.weight = self.model.emb.weight
def get_input_embeddings(self):
return self.model.emb
def set_input_embeddings(self, value):
self.model.emb = value
def get_output_embeddings(self):
return self.head
def set_output_embeddings(self, new_embeddings):
self.head = new_embeddings
def raw_logits(self, idx):
return self.head(self.model.forward_hidden(idx))
def logits(self, idx):
return self.raw_logits(idx)
def _masked_logits(self, input_ids, attention_mask):
if attention_mask is None or bool(attention_mask.all()):
return self.logits(input_ids)
B, T = input_ids.shape
out = None
for i in range(B):
keep = attention_mask[i].bool().nonzero(as_tuple=False).flatten()
if keep.numel() == 0:
keep = torch.tensor([T - 1], device=input_ids.device)
trimmed = input_ids[i, keep].unsqueeze(0)
logits_i = self.logits(trimmed)
if out is None:
out = logits_i.new_zeros(B, T, logits_i.size(-1))
out[i, keep, :] = logits_i[0, -keep.numel() :, :]
return out
def forward(
self,
input_ids=None,
attention_mask=None,
labels=None,
use_cache=False,
past_key_values=None,
**kwargs,
):
if input_ids.size(1) > self.config.seq_len:
input_ids = input_ids[:, -self.config.seq_len :]
if attention_mask is not None:
attention_mask = attention_mask[:, -self.config.seq_len :]
if labels is not None:
labels = labels[:, -self.config.seq_len :]
logits = self._masked_logits(input_ids, attention_mask)
loss = None
if labels is not None:
shift_logits = logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()
loss = F.cross_entropy(
shift_logits.view(-1, shift_logits.size(-1)).float(),
shift_labels.view(-1),
ignore_index=-100,
)
return CausalLMOutputWithPast(loss=loss, logits=logits, past_key_values=None)
def prepare_inputs_for_generation(self, input_ids, **kwargs):
attention_mask = kwargs.get("attention_mask")
result = {"input_ids": input_ids[:, -self.config.seq_len :]}
if attention_mask is not None:
result["attention_mask"] = attention_mask[:, -self.config.seq_len :]
return result