anlp-assignment2 / src /part1 /model.py
sahoosk's picture
Upload folder using huggingface_hub
5407bdc verified
Raw History Blame Contribute Delete
6.94 kB
"""A decoder-only transformer whose feed-forward block is swappable.
Attention, RoPE and RMSNorm are carried over from Assignment 1 and are held
fixed across all five ablations -- the feed-forward network is the only thing
that changes, which is what makes the comparison mean anything.
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from .config import TransformerConfig
from .ffn import build_ffn, MoE
class RMSNorm(nn.Module):
def __init__(self, d_model, eps=1e-6):
super().__init__()
self.gamma = nn.Parameter(torch.ones(d_model))
self.eps = eps
def forward(self, x):
dtype = x.dtype
x = x.float()
rms = torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
return (self.gamma.float() * x * rms).to(dtype)
class RotaryEmbedding(nn.Module):
"""Rotary position embedding, applied to q and k inside attention."""
def __init__(self, head_dim, max_len, base=10000.0):
super().__init__()
inv = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim))
t = torch.arange(max_len).float()
freqs = torch.outer(t, inv)
emb = torch.cat((freqs, freqs), dim=-1)
self.register_buffer("cos", emb.cos()[None, None], persistent=False)
self.register_buffer("sin", emb.sin()[None, None], persistent=False)
@staticmethod
def _rotate_half(x):
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
def forward(self, q, k, offset=0):
n = q.size(-2)
cos = self.cos[..., offset:offset + n, :].to(q.dtype)
sin = self.sin[..., offset:offset + n, :].to(q.dtype)
q = q * cos + self._rotate_half(q) * sin
k = k * cos + self._rotate_half(k) * sin
return q, k
class CausalSelfAttention(nn.Module):
def __init__(self, config: TransformerConfig, rope: RotaryEmbedding):
super().__init__()
self.n_heads = config.n_heads
self.n_kv = config.n_kv_heads or config.n_heads
assert self.n_heads % self.n_kv == 0
self.n_groups = self.n_heads // self.n_kv
self.d_head = config.d_head
self.rope = rope
d = config.d_model
self.wq = nn.Linear(d, self.n_heads * self.d_head, bias=False)
self.wk = nn.Linear(d, self.n_kv * self.d_head, bias=False)
self.wv = nn.Linear(d, self.n_kv * self.d_head, bias=False)
self.wo = nn.Linear(self.n_heads * self.d_head, d, bias=False)
self.dropout = config.dropout
def forward(self, x):
B, N, _ = x.shape
q = self.wq(x).view(B, N, self.n_heads, self.d_head).transpose(1, 2)
k = self.wk(x).view(B, N, self.n_kv, self.d_head).transpose(1, 2)
v = self.wv(x).view(B, N, self.n_kv, self.d_head).transpose(1, 2)
q, k = self.rope(q, k)
if self.n_groups > 1:
k = k.repeat_interleave(self.n_groups, dim=1)
v = v.repeat_interleave(self.n_groups, dim=1)
out = F.scaled_dot_product_attention(
q, k, v, is_causal=True,
dropout_p=self.dropout if self.training else 0.0)
out = out.transpose(1, 2).reshape(B, N, -1)
return self.wo(out)
class Block(nn.Module):
"""Pre-norm decoder block; ``self.ffn`` is the MoE/MLP drop-in point."""
def __init__(self, config: TransformerConfig, rope: RotaryEmbedding):
super().__init__()
self.norm1 = RMSNorm(config.d_model)
self.attn = CausalSelfAttention(config, rope)
self.norm2 = RMSNorm(config.d_model)
self.ffn = build_ffn(config)
def forward(self, x):
x = x + self.attn(self.norm1(x))
x = x + self.ffn(self.norm2(x))
return x
class Transformer(nn.Module):
def __init__(self, config: TransformerConfig):
super().__init__()
self.config = config
self.embed = nn.Embedding(config.vocab_size, config.d_model)
self.rope = RotaryEmbedding(config.d_head, config.n_ctx, config.rope_base)
self.blocks = nn.ModuleList(
[Block(config, self.rope) for _ in range(config.n_layers)])
self.norm_f = RMSNorm(config.d_model)
self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
if config.tie_embeddings:
self.lm_head.weight = self.embed.weight
self.apply(self._init)
# Scale the residual-path output projections by 1/sqrt(2*L) so the
# residual stream does not blow up with depth (GPT-2 initialisation).
for name, p in self.named_parameters():
if name.endswith("wo.weight") or name.endswith("fc2.weight"):
nn.init.normal_(p, std=0.02 / math.sqrt(2 * config.n_layers))
@staticmethod
def _init(m):
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Embedding):
nn.init.normal_(m.weight, std=0.02)
def forward(self, idx, targets=None, loss_mask=None):
# Input shape: (B, N_ctx); logits shape: (B, N_ctx, N_vocab)
x = self.embed(idx)
for block in self.blocks:
x = block(x)
logits = self.lm_head(self.norm_f(x))
if targets is None:
return logits, None, self.aux_losses()
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)).float(), targets.reshape(-1),
ignore_index=-100, reduction="none")
if loss_mask is not None:
m = loss_mask.reshape(-1).float()
loss = (loss * m).sum() / m.sum().clamp_min(1.0)
else:
loss = loss[targets.reshape(-1) != -100].mean()
return logits, loss, self.aux_losses()
def aux_losses(self):
"""Summed router losses over every MoE layer (zero for the dense model)."""
aux = torch.zeros((), device=self.embed.weight.device)
z = torch.zeros((), device=self.embed.weight.device)
for block in self.blocks:
if isinstance(block.ffn, MoE):
aux = aux + block.ffn.aux_loss
z = z + block.ffn.z_loss
return aux, z
def expert_counts(self):
"""Per-layer token counts from the last forward, or None if dense."""
out = []
for block in self.blocks:
if isinstance(block.ffn, MoE):
out.append(block.ffn.last_counts)
return out or None
def expert_assignments(self):
"""Per-layer (B, N, k) expert ids from the last forward."""
out = []
for block in self.blocks:
if isinstance(block.ffn, MoE):
out.append(block.ffn.last_assign)
return out or None
def num_params(self, non_embedding=False):
n = sum(p.numel() for p in self.parameters())
if non_embedding:
n -= self.embed.weight.numel()
return n