MP-01-100M-base / vbq /model.py
SURELYNOTVAIN's picture
Add MP-01 (9500) 100M base: weights, config, tokenizer, model code, model card and eval results
11e2c7c verified
Raw History Blame Contribute Delete
15.3 kB
"""VBQ transformer: Llama-style decoder (RMSNorm, RoPE, GQA, QK-norm, SwiGLU, tied embeddings)."""
from __future__ import annotations
import math
from dataclasses import dataclass, asdict
import torch
import torch.nn as nn
import torch.nn.functional as F
@dataclass
class VBQConfig:
vocab_size: int = 32768
d_model: int = 640
n_layers: int = 12
n_heads: int = 10
n_kv_heads: int = 5
mlp_hidden: int = 1664
max_seq_len: int = 2048
rope_theta: float = 10000.0
norm_eps: float = 1e-5
qk_norm: bool = True
tie_embeddings: bool = True
# Mixture of experts (0 = dense SwiGLU). Top-1 routing, Switch-style.
n_experts: int = 0
capacity_factor: float = 1.25
@property
def head_dim(self) -> int:
return self.d_model // self.n_heads
def n_params(self) -> int:
d, hd = self.d_model, self.head_dim
attn = d * d * 2 + d * (self.n_kv_heads * hd) * 2
mlp = 3 * d * self.mlp_hidden * max(1, self.n_experts) + (d * self.n_experts if self.n_experts else 0)
per_layer = attn + mlp + 2 * d + (2 * hd if self.qk_norm else 0)
emb = self.vocab_size * d * (1 if self.tie_embeddings else 2)
return emb + self.n_layers * per_layer + d
def to_dict(self):
return asdict(self)
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
return F.rms_norm(x, (x.shape[-1],), self.weight.to(x.dtype), self.eps)
def build_rope(seq_len: int, head_dim: int, theta: float, device=None):
inv = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
t = torch.arange(seq_len, device=device, dtype=torch.float32)
freqs = torch.outer(t, inv)
return freqs.cos(), freqs.sin() # [T, hd/2]
def apply_rope(x, cos, sin):
# x: [B, H, T, hd]; half-split rotation
x1, x2 = x.chunk(2, dim=-1)
cos = cos[None, None, : x.shape[2]].to(x.dtype)
sin = sin[None, None, : x.shape[2]].to(x.dtype)
return torch.cat((x1 * cos - x2 * sin, x2 * cos + x1 * sin), dim=-1)
class Attention(nn.Module):
def __init__(self, c: VBQConfig):
super().__init__()
self.c = c
hd = c.head_dim
self.wq = nn.Linear(c.d_model, c.n_heads * hd, bias=False)
self.wk = nn.Linear(c.d_model, c.n_kv_heads * hd, bias=False)
self.wv = nn.Linear(c.d_model, c.n_kv_heads * hd, bias=False)
self.wo = nn.Linear(c.n_heads * hd, c.d_model, bias=False)
if c.qk_norm:
self.q_norm = RMSNorm(hd, c.norm_eps)
self.k_norm = RMSNorm(hd, c.norm_eps)
def forward(self, x, cos, sin):
B, T, _ = x.shape
c, hd = self.c, self.c.head_dim
q = self.wq(x).view(B, T, c.n_heads, hd).transpose(1, 2)
k = self.wk(x).view(B, T, c.n_kv_heads, hd).transpose(1, 2)
v = self.wv(x).view(B, T, c.n_kv_heads, hd).transpose(1, 2)
if c.qk_norm:
q, k = self.q_norm(q), self.k_norm(k)
q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
if c.n_kv_heads != c.n_heads:
rep = c.n_heads // c.n_kv_heads
k = k[:, :, None].expand(B, c.n_kv_heads, rep, T, hd).reshape(B, c.n_heads, T, hd)
v = v[:, :, None].expand(B, c.n_kv_heads, rep, T, hd).reshape(B, c.n_heads, T, hd)
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
return self.wo(y.transpose(1, 2).reshape(B, T, c.n_heads * hd))
class MLP(nn.Module):
def __init__(self, c: VBQConfig):
super().__init__()
self.gate_up = nn.Linear(c.d_model, 2 * c.mlp_hidden, bias=False)
self.down = nn.Linear(c.mlp_hidden, c.d_model, bias=False)
def forward(self, x):
g, u = self.gate_up(x).chunk(2, dim=-1)
return self.down(F.silu(g) * u)
class MoE(nn.Module):
"""Top-1 mixture of SwiGLU experts (Switch Transformer routing).
Fixed shapes (compile-friendly): every expert gets a buffer of
capacity = ceil(capacity_factor * tokens / experts) slots while training
(tokens past it skip the MLP and keep only the residual); at eval every
token fits. The chosen expert's output is scaled by p / p.detach(), which
is 1 in value, so a model upcycled from identical copies computes exactly
what the dense model did, while the router still gets gradients.
Returns (y, aux) with aux = Switch load-balancing loss (1.0 = balanced).
"""
def __init__(self, c: VBQConfig):
super().__init__()
self.c = c
E, d, h = c.n_experts, c.d_model, c.mlp_hidden
self.router = nn.Linear(d, E, bias=False)
self.gate_up = nn.Parameter(torch.empty(E, 2 * h, d))
self.down = nn.Parameter(torch.empty(E, d, h))
nn.init.normal_(self.gate_up, 0.0, 0.02)
nn.init.normal_(self.down, 0.0, 0.02)
def forward(self, x):
shape = x.shape
x = x.reshape(-1, shape[-1])
N, d = x.shape
E = self.c.n_experts
probs = F.softmax(self.router(x).float(), dim=-1) # [N, E]
gate, idx = probs.max(-1) # [N]
onehot = F.one_hot(idx, E) # [N, E]
pos = ((onehot.cumsum(0) - 1) * onehot).sum(-1) # slot within its expert
C = math.ceil(self.c.capacity_factor * N / E) if self.training else N
keep = pos < C
slot = torch.where(keep, idx * C + pos, torch.full_like(pos, E * C)) # overflow -> dump slot
buf = x.new_zeros(E * C + 1, d).index_put((slot,), x)
hbuf = buf[: E * C].view(E, C, d)
g, u = torch.bmm(hbuf, self.gate_up.to(x.dtype).transpose(1, 2)).chunk(2, dim=-1)
y = torch.bmm(F.silu(g) * u, self.down.to(x.dtype).transpose(1, 2)) # [E, C, d]
y = torch.cat([y.reshape(E * C, d), y.new_zeros(1, d)])[slot]
scale = (gate / gate.detach()) * keep
y = y * scale.to(y.dtype).unsqueeze(-1)
aux = E * (onehot.float().mean(0) * probs.mean(0)).sum()
return y.reshape(shape), aux
class Block(nn.Module):
def __init__(self, c: VBQConfig):
super().__init__()
self.attn_norm = RMSNorm(c.d_model, c.norm_eps)
self.attn = Attention(c)
self.mlp_norm = RMSNorm(c.d_model, c.norm_eps)
self.mlp = MoE(c) if c.n_experts else MLP(c)
def forward(self, x, cos, sin):
x = x + self.attn(self.attn_norm(x), cos, sin)
return x + self.ffn(self.mlp_norm(x))[0]
def ffn(self, x):
"""(output, load-balancing loss) for dense and MoE blocks alike."""
y = self.mlp(x)
return y if isinstance(y, tuple) else (y, x.new_zeros((), dtype=torch.float32))
class FusedLinearCE(torch.autograd.Function):
"""Cross-entropy over a huge vocab without ever materialising full logits.
Gradients w.r.t. hidden states and the (tied) output matrix are computed chunk by chunk
during the forward pass, so the backward pass is a cheap scale. Saves ~4 GB at 32k vocab.
"""
@staticmethod
def forward(ctx, h, w, targets, chunk: int, z_loss: float):
N = h.shape[0]
wb = w.to(h.dtype)
grad_h = torch.empty_like(h)
grad_w = torch.zeros_like(w, dtype=torch.float32)
total = torch.zeros((), device=h.device, dtype=torch.float32)
for i in range(0, N, chunk):
hc, tc = h[i : i + chunk], targets[i : i + chunk]
logits = (hc @ wb.t()).float()
lse = torch.logsumexp(logits, dim=-1)
tgt = logits.gather(1, tc[:, None]).squeeze(1)
total += (lse - tgt).sum()
p = torch.exp(logits - lse[:, None])
if z_loss:
total += z_loss * (lse * lse).sum()
p.mul_(1.0 + 2.0 * z_loss * lse[:, None])
p.scatter_add_(1, tc[:, None], torch.full_like(tc[:, None], -1, dtype=p.dtype))
p.mul_(1.0 / N)
pb = p.to(h.dtype)
grad_h[i : i + chunk] = pb @ wb
grad_w += (pb.t() @ hc).float()
ctx.save_for_backward(grad_h, grad_w)
return total / N
@staticmethod
def backward(ctx, g):
grad_h, grad_w = ctx.saved_tensors
return grad_h * g.to(grad_h.dtype), grad_w * g, None, None, None
class VBQ(nn.Module):
def __init__(self, c: VBQConfig):
super().__init__()
self.c = c
self.tok_emb = nn.Embedding(c.vocab_size, c.d_model)
self.blocks = nn.ModuleList(Block(c) for _ in range(c.n_layers))
self.final_norm = RMSNorm(c.d_model, c.norm_eps)
self.lm_head = None if c.tie_embeddings else nn.Linear(c.d_model, c.vocab_size, bias=False)
cos, sin = build_rope(c.max_seq_len, c.head_dim, c.rope_theta)
self.register_buffer("rope_cos", cos, persistent=False)
self.register_buffer("rope_sin", sin, persistent=False)
self.apply(self._init)
for n, p in self.named_parameters(): # scaled init on residual output projections
if n.endswith("wo.weight") or n.endswith("down.weight"):
nn.init.normal_(p, 0.0, 0.02 / math.sqrt(2 * c.n_layers))
@staticmethod
def _init(m):
if isinstance(m, (nn.Linear, nn.Embedding)):
nn.init.normal_(m.weight, 0.0, 0.02)
@property
def out_weight(self):
return self.tok_emb.weight if self.lm_head is None else self.lm_head.weight
def hidden(self, idx):
return self.hidden_aux(idx)[0]
def hidden_aux(self, idx):
"""Final hidden states and the mean MoE load-balancing loss (0 for dense)."""
return self._blocks(self.tok_emb(idx))
def hidden_patch(self, idx, k: int):
"""Patch-level training (Shao et al. 2024): idx [B, P*k]; each group of k tokens enters as the mean of its
token embeddings, so the transformer runs on P positions. Same weights as token-level training."""
B, L = idx.shape
return self._blocks(self.tok_emb(idx).view(B, L // k, k, -1).mean(2))
def _blocks(self, x):
aux = x.new_zeros((), dtype=torch.float32)
for blk in self.blocks:
x = x + blk.attn(blk.attn_norm(x), self.rope_cos, self.rope_sin)
y, a = blk.ffn(blk.mlp_norm(x))
x = x + y
aux = aux + a
return self.final_norm(x), aux / len(self.blocks)
def forward(self, idx, targets=None, z_loss: float = 0.0, ce_chunk: int = 4096):
h = self.hidden(idx)
if targets is None:
return (h @ self.out_weight.to(h.dtype).t()).float()
loss = FusedLinearCE.apply(h.reshape(-1, h.shape[-1]), self.out_weight, targets.reshape(-1), ce_chunk, z_loss)
return loss
@torch.no_grad()
def generate(self, idx, max_new_tokens=128, temperature=0.8, top_k=50, top_p=0.95, eos_id=None, repetition_penalty=1.0):
self.eval()
for _ in range(max_new_tokens):
ctx = idx[:, -self.c.max_seq_len :]
logits = self(ctx)[:, -1]
if repetition_penalty != 1.0:
for tok in set(ctx[0].tolist()):
logits[0, tok] = logits[0, tok] / repetition_penalty if logits[0, tok] > 0 else logits[0, tok] * repetition_penalty
if temperature <= 0:
nxt = logits.argmax(-1, keepdim=True)
else:
logits = logits / temperature
if top_k:
v, _ = torch.topk(logits, min(top_k, logits.shape[-1]))
logits[logits < v[:, [-1]]] = -float("inf")
probs = F.softmax(logits, dim=-1)
if top_p < 1.0:
sp, si = torch.sort(probs, descending=True)
cum = sp.cumsum(-1)
sp[cum - sp > top_p] = 0
probs = torch.zeros_like(probs).scatter_(1, si, sp)
probs = probs / probs.sum(-1, keepdim=True)
nxt = torch.multinomial(probs, 1)
idx = torch.cat([idx, nxt], dim=1)
yield int(nxt.item())
if eos_id is not None and int(nxt.item()) == eos_id:
break
@torch.no_grad()
def generate_cached(self, idx, max_new_tokens=128, temperature=0.8, top_k=50, top_p=0.95, eos_id=None, repetition_penalty=1.0):
"""KV-cached sampling. Yields token ids one by one. idx: [1, T] prompt."""
self.eval()
c = self.c
idx = idx[:, -(c.max_seq_len - 1):]
stop_ids = set() if eos_id is None else ({eos_id} if isinstance(eos_id, int) else set(eos_id))
caches: list = [None] * c.n_layers
seen = set(idx[0].tolist())
def forward(ids, pos):
T = ids.shape[1]
x = self.tok_emb(ids)
cos, sin = self.rope_cos[pos : pos + T], self.rope_sin[pos : pos + T]
for i, blk in enumerate(self.blocks):
a = blk.attn
h = blk.attn_norm(x)
q = a.wq(h).view(1, T, c.n_heads, c.head_dim).transpose(1, 2)
k = a.wk(h).view(1, T, c.n_kv_heads, c.head_dim).transpose(1, 2)
v = a.wv(h).view(1, T, c.n_kv_heads, c.head_dim).transpose(1, 2)
if c.qk_norm:
q, k = a.q_norm(q), a.k_norm(k)
q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
if caches[i] is not None:
k, v = torch.cat([caches[i][0], k], 2), torch.cat([caches[i][1], v], 2)
caches[i] = (k, v)
rep = c.n_heads // c.n_kv_heads
kk = k.repeat_interleave(rep, dim=1) if rep > 1 else k
vv = v.repeat_interleave(rep, dim=1) if rep > 1 else v
y = F.scaled_dot_product_attention(q, kk, vv, is_causal=(T > 1))
x = x + a.wo(y.transpose(1, 2).reshape(1, T, c.n_heads * c.head_dim))
x = x + blk.ffn(blk.mlp_norm(x))[0]
return (self.final_norm(x[:, -1]) @ self.out_weight.to(x.dtype).t()).float()
logits = forward(idx, 0)
pos = idx.shape[1]
for _ in range(max_new_tokens):
if pos >= c.max_seq_len:
break
lg = logits[0].clone()
if repetition_penalty != 1.0:
for tok in seen:
lg[tok] = lg[tok] / repetition_penalty if lg[tok] > 0 else lg[tok] * repetition_penalty
if temperature <= 0:
nxt = int(lg.argmax())
else:
lg = lg / temperature
if top_k:
v, _ = torch.topk(lg, min(top_k, lg.numel()))
lg[lg < v[-1]] = -float("inf")
probs = F.softmax(lg, dim=-1)
if top_p < 1.0:
sp, si = torch.sort(probs, descending=True)
sp[sp.cumsum(-1) - sp > top_p] = 0
probs = torch.zeros_like(probs).scatter_(0, si, sp)
probs = probs / probs.sum()
nxt = int(torch.multinomial(probs, 1))
yield nxt
if nxt in stop_ids:
break
seen.add(nxt)
logits = forward(torch.tensor([[nxt]], device=idx.device), pos)
pos += 1