| |
| """Train a tiny byte-level GPT with full Coppola projection from step 0. |
| |
| This is the local in-repo pretraining path for the full-Coppola experiment. |
| It avoids external trainer dependencies and keeps the architecture small |
| enough that we can iterate on the projection itself. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import math |
| import random |
| from dataclasses import asdict, dataclass |
| from pathlib import Path |
| from typing import Dict, List, Sequence |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| ROOT = Path(__file__).resolve().parent.parent |
| import sys |
|
|
| if str(ROOT) not in sys.path: |
| sys.path.insert(0, str(ROOT)) |
|
|
| from coppola_pretraining import ( |
| ComponentNorms, |
| CoppolaPretrainingConfig, |
| CoppolaPretrainingController, |
| FamilyScales, |
| RankPolicy, |
| ) |
|
|
| try: |
| import pyarrow.parquet as pq |
| except ImportError: |
| pq = None |
|
|
|
|
| def set_seed(seed: int) -> None: |
| random.seed(seed) |
| torch.manual_seed(seed) |
| if torch.cuda.is_available(): |
| torch.cuda.manual_seed_all(seed) |
|
|
|
|
| @dataclass |
| class GPTConfig: |
| seq_len: int = 256 |
| vocab_size: int = 256 |
| n_layer: int = 8 |
| n_head: int = 8 |
| n_embd: int = 256 |
| mlp_mult: int = 4 |
| mlp_activation: str = "silu" |
| dropout: float = 0.0 |
| loss_kind: str = "ce" |
| |
| logit_rmsnorm_scale: float = 0.0 |
| |
| sparsity_lambda: float = 0.0 |
| |
| block_attractor_indices: tuple[int, ...] = () |
| block_attractor_lambda: float = 0.0 |
| |
| |
| block_gates: bool = False |
| block_gate_kind: str = "linear" |
| |
| block_gate_l1: float = 0.0 |
| |
| |
| |
| |
| |
| |
| loop_block_indices: tuple[int, ...] = () |
| loop_block_k: int = 1 |
| |
| |
| |
| |
| loop_unit_indices: tuple[int, ...] = () |
| |
| |
| |
| |
| n_heads_per_block: tuple[int, ...] = () |
| |
| |
| |
| |
| mlp_mult_per_block: tuple[int, ...] = () |
| |
| |
| |
| |
| |
| mlp_activation_per_block: tuple[str, ...] = () |
| |
| |
| |
| |
| |
| mlp_output_rmsnorm_scale: float = 0.0 |
| |
| |
| |
| |
| |
| |
| |
| mlp_act_sparsity_lambda: float = 0.0 |
| |
| |
| |
| |
| |
| |
| mlp_act_topk: int = 0 |
| |
| |
| |
| |
| |
| |
| mlp_byte_concentration_lambda: float = 0.0 |
| mlp_byte_concentration_topk: int = 4 |
| mlp_byte_concentration_layers: tuple[int, ...] = () |
| |
| |
| |
| |
| attn_kind: str = "mha" |
| tucker_r_p: int = 4 |
| tucker_r_q: int = 4 |
| |
| tucker_tied_v: bool = False |
| tucker_tied_o: bool = False |
| |
| |
| |
| |
| tucker_d_v: int = 0 |
| |
| |
| |
| block_diag_size: int = 4 |
| |
| |
| |
| block_diag_proj: bool = True |
| |
| |
| |
| |
| |
| smr_n_operators: int = 4 |
| smr_d_v: int = 32 |
| smr_r_kind: str = "rotor" |
| smr_rotor_block: int = 4 |
| |
| |
| |
| |
| smr_operator_kinds: tuple[str, ...] = () |
| |
| |
| |
| |
| smr_shared_wo: bool = False |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| attn_norm: str = "softmax" |
| topk_k: int = 4 |
| |
| |
| |
| |
| |
| position_encoding: str = "learned" |
| |
| |
| |
| rope_base: float = 10000.0 |
| |
| |
| alibi_slope_scale: float = 1.0 |
| |
| |
| |
| |
| |
| |
| |
| |
| res_attn: str = "none" |
| |
| |
| |
| res_attn_norm: str = "softmax" |
| |
| |
| |
| |
| |
| |
| |
| |
| res_attn_temp: float = 1.0 |
| res_attn_entropy_lambda: float = 0.0 |
|
|
|
|
| class CausalSelfAttention(nn.Module): |
| def __init__(self, cfg: GPTConfig, n_head: int | None = None): |
| super().__init__() |
| n_head_eff = cfg.n_head if n_head is None else n_head |
| if cfg.n_embd % n_head_eff != 0: |
| raise ValueError(f"n_embd ({cfg.n_embd}) must be divisible by n_head ({n_head_eff})") |
| self.n_head = n_head_eff |
| self.head_dim = cfg.n_embd // n_head_eff |
| self.c_attn = nn.Linear(cfg.n_embd, 3 * cfg.n_embd, bias=False) |
| self.c_proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False) |
| self.dropout = nn.Dropout(cfg.dropout) |
| |
| |
| |
| |
| self.attn_norm = cfg.attn_norm |
| self.attn_gain = nn.Parameter(torch.tensor(0.1)) if cfg.attn_norm == "linear_gain" else None |
| self.topk_k = cfg.topk_k |
|
|
| def forward(self, x: torch.Tensor, |
| attn_bias: torch.Tensor | None = None, |
| rope_freqs: tuple[torch.Tensor, torch.Tensor] | None = None) -> torch.Tensor: |
| bsz, seqlen, hidden = x.shape |
| qkv = self.c_attn(x) |
| q, k, v = qkv.chunk(3, dim=-1) |
| q = q.view(bsz, seqlen, self.n_head, self.head_dim).transpose(1, 2) |
| k = k.view(bsz, seqlen, self.n_head, self.head_dim).transpose(1, 2) |
| v = v.view(bsz, seqlen, self.n_head, self.head_dim).transpose(1, 2) |
| if rope_freqs is not None: |
| cos, sin = rope_freqs |
| q = _apply_rope(q, cos, sin) |
| k = _apply_rope(k, cos, sin) |
| if self.attn_norm == "softmax": |
| |
| if attn_bias is None: |
| y = F.scaled_dot_product_attention(q, k, v, dropout_p=0.0, is_causal=True) |
| else: |
| T = seqlen |
| causal_mask = torch.zeros(T, T, device=x.device, dtype=q.dtype) |
| causal_mask.masked_fill_( |
| torch.triu(torch.ones(T, T, device=x.device, dtype=torch.bool), diagonal=1), |
| float("-inf"), |
| ) |
| mask = attn_bias.to(q.dtype) + causal_mask |
| y = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0.0, is_causal=False) |
| else: |
| |
| scores = torch.einsum("bhid,bhjd->bhij", q, k) / (self.head_dim ** 0.5) |
| attn = _apply_attn_norm(scores, self.attn_norm, self.attn_gain, |
| topk_k=self.topk_k, attn_bias=attn_bias) |
| y = torch.einsum("bhij,bhjd->bhid", attn, v) |
| y = y.transpose(1, 2).contiguous().view(bsz, seqlen, hidden) |
| return self.dropout(self.c_proj(y)) |
|
|
|
|
| class BlockDiagAttention(nn.Module): |
| """Block-diagonal-(a,b) attention with configurable output projection. |
| |
| Partition d_model into G = d_model / block_size disjoint coordinate groups. |
| Each group g has its own input projections Q_g, K_g, V_g of shape (b, b) |
| that operate ONLY on its own block of x — no cross-group mixing in the |
| input projection (block-diagonal c_attn). |
| |
| The output projection is configurable: |
| block_diag_proj=True → block-diagonal c_proj (per-group W_O_g, no |
| cross-group mixing). Lossy on cross-feature |
| output integration. |
| block_diag_proj=False → full d×d c_proj. Each group's per-position |
| attention output gets cross-mixed across all |
| groups. Tests whether the cost of pure block- |
| diagonal is on the input or output side. |
| |
| Parameter count per layer: |
| block_diag_proj=True: G·4·b² = 4·d·b (32× compression at d=128, b=4) |
| block_diag_proj=False: G·3·b² + d·d = 3·d·b + d² (≈ MHA on c_proj, much smaller c_attn) |
| """ |
|
|
| def __init__(self, cfg: GPTConfig, block_size: int, block_diag_proj: bool = True): |
| super().__init__() |
| d = cfg.n_embd |
| if d % block_size != 0: |
| raise ValueError(f"n_embd ({d}) must be divisible by block_diag_size ({block_size})") |
| self.G = d // block_size |
| self.b = block_size |
| self.block_diag_proj = block_diag_proj |
| scale_in = self.b ** -0.5 |
| self.q_w = nn.Parameter(torch.randn(self.G, self.b, self.b) * scale_in) |
| self.k_w = nn.Parameter(torch.randn(self.G, self.b, self.b) * scale_in) |
| self.v_w = nn.Parameter(torch.randn(self.G, self.b, self.b) * scale_in) |
| if block_diag_proj: |
| |
| self.o_w = nn.Parameter(torch.randn(self.G, self.b, self.b) * scale_in) |
| self._c_proj_full = None |
| else: |
| |
| self.o_w = None |
| self._c_proj_full = nn.Linear(d, d, bias=False) |
| self.dropout = nn.Dropout(cfg.dropout) |
|
|
| @property |
| def c_proj(self): |
| """Adapter for iter_projected_params and RankPolicy. |
| Returns the actual nn.Linear when full c_proj is enabled, or a |
| synthetic dense view of the block-diagonal o_w otherwise.""" |
| if self.block_diag_proj: |
| return _BlockDiagCProjView(self) |
| return self._c_proj_full |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| B, T, d = x.shape |
| xg = x.view(B, T, self.G, self.b) |
| Q = torch.einsum("btgi,gij->btgj", xg, self.q_w) |
| K = torch.einsum("btgi,gij->btgj", xg, self.k_w) |
| V = torch.einsum("btgi,gij->btgj", xg, self.v_w) |
| Q = Q.transpose(1, 2) |
| K = K.transpose(1, 2) |
| V = V.transpose(1, 2) |
| out = F.scaled_dot_product_attention(Q, K, V, dropout_p=0.0, is_causal=True) |
| out = out.transpose(1, 2) |
| if self.block_diag_proj: |
| out = torch.einsum("btgi,gij->btgj", out, self.o_w) |
| out = out.reshape(B, T, d) |
| else: |
| |
| out = out.reshape(B, T, d) |
| out = self._c_proj_full(out) |
| return self.dropout(out) |
|
|
|
|
| class _BlockDiagCProjView: |
| """Read-only weight-shape adapter so existing iter_projected_params / |
| RankPolicy / projection-related code can introspect a BlockDiagAttention |
| block without crashing. The underlying training parameter is `o_w`.""" |
|
|
| def __init__(self, attn: "BlockDiagAttention"): |
| self._attn = attn |
|
|
| @property |
| def weight(self) -> torch.Tensor: |
| |
| d = self._attn.G * self._attn.b |
| out = self._attn.o_w.new_zeros(d, d) |
| for g in range(self._attn.G): |
| i = g * self._attn.b |
| out[i:i + self._attn.b, i:i + self._attn.b] = self._attn.o_w[g] |
| return out |
|
|
|
|
| def _make_rope_freqs(head_dim: int, T: int, base: float, device, dtype) -> tuple[torch.Tensor, torch.Tensor]: |
| """Standard RoPE cos/sin tables of shape [T, head_dim/2].""" |
| if head_dim % 2 != 0: |
| raise ValueError(f"RoPE requires even head_dim, got {head_dim}") |
| inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim)) |
| pos = torch.arange(T, device=device, dtype=torch.float32) |
| freqs = torch.outer(pos, inv_freq) |
| return freqs.cos().to(dtype), freqs.sin().to(dtype) |
|
|
|
|
| def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor: |
| """Rotate every pair of dims in x by position-dependent angle. |
| |
| x: [..., T, head_dim]; cos, sin: [T, head_dim/2]. |
| """ |
| x1 = x[..., 0::2] |
| x2 = x[..., 1::2] |
| |
| rot1 = x1 * cos - x2 * sin |
| rot2 = x1 * sin + x2 * cos |
| out = torch.empty_like(x) |
| out[..., 0::2] = rot1 |
| out[..., 1::2] = rot2 |
| return out |
|
|
|
|
| def _alibi_slopes(n_head: int) -> torch.Tensor: |
| """Standard ALiBi per-head slopes (Press et al. 2021, eq. 2). |
| |
| For n_head a power of two: m_h = 2^(-8h/n_head) for h = 1..n_head. |
| Otherwise interpolate following the reference implementation. |
| """ |
| def _power_of_two(n: int) -> list[float]: |
| start = 2.0 ** (-(2.0 ** -(math.log2(n) - 3))) |
| return [start * (start ** i) for i in range(n)] |
|
|
| if (n_head & (n_head - 1)) == 0: |
| slopes = _power_of_two(n_head) |
| else: |
| closest = 1 << (n_head - 1).bit_length() >> 1 |
| a = _power_of_two(closest) |
| b = _power_of_two(2 * closest)[0::2][: n_head - closest] |
| slopes = a + b |
| return torch.tensor(slopes, dtype=torch.float32) |
|
|
|
|
| def _alibi_bias(n_head: int, T: int, slopes: torch.Tensor, device, dtype) -> torch.Tensor: |
| """Return [n_head, T, T] additive bias: m_h * (key_pos - query_pos), 0 for j > i. |
| |
| The causal mask blocks j > i separately, so this fills only the lower-triangular |
| band with the linearly-decaying offset. Result is added to attention scores. |
| """ |
| pos = torch.arange(T, device=device) |
| rel = pos.unsqueeze(0) - pos.unsqueeze(1) |
| rel = rel.to(dtype) |
| |
| |
| return slopes.to(device=device, dtype=dtype).view(n_head, 1, 1) * (-rel.abs()).unsqueeze(0) |
|
|
|
|
| def _sparsemax(z: torch.Tensor, dim: int = -1) -> torch.Tensor: |
| """Exact sparsemax (Martins & Astudillo 2016) — Euclidean projection onto |
| the simplex; produces exact zeros. Closed form, autograd-differentiable.""" |
| z_sorted, _ = torch.sort(z, descending=True, dim=dim) |
| rng = torch.arange(1, z.size(dim) + 1, device=z.device, dtype=z.dtype) |
| shape = [1] * z.dim() |
| shape[dim] = -1 |
| rng = rng.view(shape) |
| cssv = z_sorted.cumsum(dim) - 1.0 |
| support = rng * z_sorted > cssv |
| k = support.sum(dim=dim, keepdim=True).clamp_min(1) |
| tau = cssv.gather(dim, k - 1) / k.to(z.dtype) |
| return torch.clamp(z - tau, min=0.0) |
|
|
|
|
| def _entmax15(z: torch.Tensor, dim: int = -1) -> torch.Tensor: |
| """Exact 1.5-entmax (Peters, Niculae & Martins 2019). Fallback only — |
| prefer the `entmax` package (see _depth_attn_norm). Autograd through |
| `sqrt(delta)` is singular as delta→0; the +1e-8 below bounds the |
| backward (≈1/2e-4) so this stays finite, but it is not the exact |
| entmax Jacobian. Forward is exact.""" |
| z = z - z.max(dim=dim, keepdim=True).values |
| z = z / 2.0 |
| z_sorted, _ = torch.sort(z, descending=True, dim=dim) |
| rng = torch.arange(1, z.size(dim) + 1, device=z.device, dtype=z.dtype) |
| shape = [1] * z.dim() |
| shape[dim] = -1 |
| rng = rng.view(shape) |
| mean = z_sorted.cumsum(dim) / rng |
| mean_sq = (z_sorted ** 2).cumsum(dim) / rng |
| ss = rng * (mean_sq - mean ** 2) |
| delta = (1.0 - ss) / rng |
| tau = mean - torch.sqrt(torch.clamp(delta, min=0.0) + 1e-8) |
| support_size = (tau <= z_sorted).sum(dim=dim, keepdim=True).clamp_min(1) |
| tau_star = tau.gather(dim, support_size - 1) |
| return torch.clamp(z - tau_star, min=0.0) ** 2 |
|
|
|
|
| try: |
| from entmax import entmax15 as _pkg_entmax15, sparsemax as _pkg_sparsemax |
| _HAVE_ENTMAX_PKG = True |
| except ImportError: |
| _HAVE_ENTMAX_PKG = False |
|
|
|
|
| def _depth_attn_norm(scores: torch.Tensor, mode: str) -> torch.Tensor: |
| """Normalize depth-attention scores over the source axis (last dim). |
| Used by Attention Residuals (arXiv:2603.15031) to weight preceding |
| layer outputs. softmax = the paper's default (dense, full support); |
| entmax15/sparsemax = exact-sparse variants (this repo's BP-structure |
| extension — a sparse depth-mixing matrix is the BP factor graph). |
| |
| entmax15/sparsemax route to the `entmax` package when installed: its |
| custom backward is exact, whereas autograd through the inline closed |
| form differentiates `sqrt(clamp(delta,0))`, whose gradient is |
| unbounded at the sparse-support boundary (delta→0) — a latent NaN |
| that A2′ score-pressure drives straight into. The inline path is a |
| forward-correct, eps-stabilised fallback for when the pkg is absent.""" |
| if mode == "softmax": |
| return torch.softmax(scores, dim=-1) |
| if mode == "entmax15": |
| return (_pkg_entmax15(scores, dim=-1) if _HAVE_ENTMAX_PKG |
| else _entmax15(scores, dim=-1)) |
| if mode == "sparsemax": |
| return (_pkg_sparsemax(scores, dim=-1) if _HAVE_ENTMAX_PKG |
| else _sparsemax(scores, dim=-1)) |
| raise ValueError(f"unknown res_attn_norm: {mode!r}") |
|
|
|
|
| def _apply_attn_norm(scores: torch.Tensor, mode: str, |
| gain: torch.Tensor | None, |
| topk_k: int = 4, |
| attn_bias: torch.Tensor | None = None) -> torch.Tensor: |
| """Apply causal mask + chosen normalization to raw attention scores. |
| |
| scores: [..., T, T] where the last two dims are (query_pos, key_pos). |
| gain: optional learnable scalar Parameter for 'linear_gain' mode. |
| topk_k: keys per query for 'topk_ste' mode. |
| attn_bias: optional additive bias broadcast onto scores before mask/norm |
| (ALiBi). Shape compatible with `scores`. |
| """ |
| T = scores.size(-1) |
| if attn_bias is not None: |
| scores = scores + attn_bias |
| causal = torch.triu(torch.ones(T, T, device=scores.device), diagonal=1).bool() |
| if mode == "softmax": |
| scores = scores.masked_fill(causal, float("-inf")) |
| return F.softmax(scores, dim=-1) |
| if mode == "linear_gain": |
| attn = scores.masked_fill(causal, 0.0) |
| return attn * (gain if gain is not None else 1.0) |
| if mode == "rms_signed": |
| |
| attn = scores.masked_fill(causal, 0.0) |
| rms = (attn * attn).mean(dim=-1, keepdim=True).clamp(min=1e-8).sqrt() |
| return attn / rms |
| if mode == "kernel_relu2": |
| attn = F.relu(scores) ** 2 |
| attn = attn.masked_fill(causal, 0.0) |
| denom = attn.sum(dim=-1, keepdim=True).clamp(min=1e-8) |
| return attn / denom |
| if mode == "kernel_elu_plus1": |
| attn = F.elu(scores) + 1.0 |
| attn = attn.masked_fill(causal, 0.0) |
| denom = attn.sum(dim=-1, keepdim=True).clamp(min=1e-8) |
| return attn / denom |
| if mode == "sparsemax": |
| |
| |
| |
| from entmax import sparsemax |
| scores_masked = scores.masked_fill(causal, float("-inf")) |
| return sparsemax(scores_masked, dim=-1) |
| if mode == "entmax15": |
| |
| |
| |
| |
| scores_masked = scores.masked_fill(causal, float("-inf")) |
| return (_pkg_entmax15(scores_masked, dim=-1) if _HAVE_ENTMAX_PKG |
| else _entmax15(scores_masked, dim=-1)) |
| if mode == "topk_ste": |
| |
| |
| scores_masked = scores.masked_fill(causal, float("-inf")) |
| soft = F.softmax(scores_masked, dim=-1) |
| k = min(topk_k, T) |
| |
| _, topk_idx = scores_masked.topk(k, dim=-1) |
| topk_mask = torch.zeros_like(scores).scatter_(-1, topk_idx, 1.0) |
| hard = soft * topk_mask |
| hard = hard / hard.sum(dim=-1, keepdim=True).clamp(min=1e-9) |
| |
| return hard.detach() + soft - soft.detach() |
| raise ValueError(f"unknown attn_norm: {mode!r}") |
|
|
|
|
| class SharedMixerRotorAttention(nn.Module): |
| """Shared global mixer + cheap per-operator specialization + parallel output operators. |
| |
| Architecture (per the friend's Run-3 analysis): |
| |
| U = M · x [T, d] shared mixer |
| Q = U · W_Q [T, d_v] shared attention pattern |
| K = U · W_K [T, d_v] |
| A = softmax(Q K^T / √d_v) [T, T] shared routing |
| For each operator s = 1..m: |
| V_s = R_s · U [T, d] cheap per-op specialization |
| Vp_s = V_s · W_V_s [T, d_v] down-project per op |
| G_s = A · Vp_s [T, d_v] gathered |
| Y_s = G_s · W_O_s [T, d] lift back per op |
| Y = sum_s Y_s [T, d] sum of operator contributions |
| |
| R_s can be: |
| 'diag': diagonal d-vector per operator (cheapest, gain only) |
| 'rotor': block-diagonal SO(b) per operator (local subspace rotation) |
| 'full': dense (d, d) per operator (control / upper bound) |
| 'none': identity (no per-op specialization) |
| |
| The output Y is the SUM of operator contributions — providing implicit |
| cross-operator mixing without an explicit c_proj projection. This is the |
| key idea of the friend's architecture: avoid duplicating a full c_proj |
| per operator; instead, let lifted contributions add in the residual. |
| |
| Parameter count at d=128, m=4, d_v=32, R='rotor' b=4: |
| M: d² = 16384 |
| Q, K shared: 2 × d × d_v = 8192 |
| m × R rotor: m × (d/b) × b² = m × d × b = 4 × 128 × 4 = 2048 |
| m × W_V_s: m × d × d_v = 16384 |
| m × W_O_s: m × d_v × d = 16384 |
| Total: 59392 vs MHA's 65536 → ~10% attention compression |
| """ |
|
|
| def __init__(self, cfg: GPTConfig, n_operators: int, d_v: int, |
| r_kind: str, rotor_block: int, |
| operator_kinds: tuple[str, ...] = (), |
| attn_norm: str = "softmax", |
| shared_wo: bool = False, |
| topk_k: int = 4): |
| super().__init__() |
| d = cfg.n_embd |
| if d % d_v != 0: |
| raise ValueError(f"n_embd ({d}) must be divisible by smr_d_v ({d_v})") |
| |
| |
| if operator_kinds: |
| kinds_list = list(operator_kinds) |
| self.m_ops = len(kinds_list) |
| else: |
| if r_kind not in ("diag", "rotor", "full", "none"): |
| raise ValueError(f"unknown smr_r_kind: {r_kind!r}") |
| kinds_list = [r_kind] * n_operators |
| self.m_ops = n_operators |
| valid = {"diag", "rotor", "full", "none"} |
| for k in kinds_list: |
| if k not in valid: |
| raise ValueError(f"unknown operator kind: {k!r} (valid: {valid})") |
| if "rotor" in kinds_list and d % rotor_block != 0: |
| raise ValueError(f"n_embd ({d}) must be divisible by smr_rotor_block ({rotor_block})") |
| self.d_v = d_v |
| self.rotor_block = rotor_block |
| self.G_blocks = d // rotor_block |
| self.operator_kinds: tuple[str, ...] = tuple(kinds_list) |
| |
| self.M = nn.Linear(d, d, bias=False) |
| |
| self.q_proj = nn.Linear(d, d_v, bias=False) |
| self.k_proj = nn.Linear(d, d_v, bias=False) |
| |
| |
| self.R_diag = nn.ParameterList() |
| self.R_rotor = nn.ParameterList() |
| self.R_full = nn.ParameterList() |
| |
| self._r_lookup: list[tuple[str, int]] = [] |
| for k in kinds_list: |
| if k == "diag": |
| self.R_diag.append(nn.Parameter(torch.ones(d))) |
| self._r_lookup.append(("diag", len(self.R_diag) - 1)) |
| elif k == "rotor": |
| self.R_rotor.append(nn.Parameter( |
| torch.eye(rotor_block).repeat(self.G_blocks, 1, 1) |
| + torch.randn(self.G_blocks, rotor_block, rotor_block) * 0.02 |
| )) |
| self._r_lookup.append(("rotor", len(self.R_rotor) - 1)) |
| elif k == "full": |
| self.R_full.append(nn.Parameter( |
| torch.eye(d) + torch.randn(d, d) * 0.02 |
| )) |
| self._r_lookup.append(("full", len(self.R_full) - 1)) |
| elif k == "none": |
| self._r_lookup.append(("none", -1)) |
| |
| self.W_V = nn.Parameter(torch.randn(self.m_ops, d, d_v) * (d ** -0.5)) |
| |
| |
| |
| self.shared_wo = shared_wo |
| if shared_wo: |
| self.W_O = nn.Parameter(torch.randn(d_v, d) * (d_v ** -0.5)) |
| else: |
| self.W_O = nn.Parameter(torch.randn(self.m_ops, d_v, d) * (d_v ** -0.5)) |
| |
| self.attn_norm = attn_norm |
| self.attn_gain = nn.Parameter(torch.tensor(0.1)) if attn_norm == "linear_gain" else None |
| self.topk_k = topk_k |
| self.dropout = nn.Dropout(cfg.dropout) |
|
|
| @property |
| def c_proj(self) -> "_SmrCProjView": |
| return _SmrCProjView(self) |
|
|
| def _apply_R(self, U: torch.Tensor) -> torch.Tensor: |
| """U: [B, T, d] → V_s: [B, m, T, d] after per-operator R_s applied. |
| |
| Iterates over operators (m_ops is small, typically 4–8) so each op |
| can use a different specialization kind. If all operators have the |
| same kind there's a small overhead vs the bulk-tensor path; for |
| heterogeneous configs this is the cleanest implementation. |
| """ |
| B, T, d = U.shape |
| outs: list[torch.Tensor] = [] |
| Ug_cached = None |
| for kind, idx in self._r_lookup: |
| if kind == "none": |
| outs.append(U) |
| elif kind == "diag": |
| outs.append(U * self.R_diag[idx][None, None, :]) |
| elif kind == "rotor": |
| if Ug_cached is None: |
| Ug_cached = U.view(B, T, self.G_blocks, self.rotor_block) |
| rotated = torch.einsum("btgi,gij->btgj", Ug_cached, self.R_rotor[idx]) |
| outs.append(rotated.reshape(B, T, d)) |
| elif kind == "full": |
| outs.append(torch.einsum("btj,jk->btk", U, self.R_full[idx])) |
| return torch.stack(outs, dim=1) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| B, T, d = x.shape |
| U = self.M(x) |
| Q = self.q_proj(U) |
| K = self.k_proj(U) |
| scores = torch.einsum("bid,bjd->bij", Q, K) / (self.d_v ** 0.5) |
| attn = _apply_attn_norm(scores, self.attn_norm, self.attn_gain, topk_k=self.topk_k) |
| V_per_op = self._apply_R(U) |
| |
| Vp = torch.einsum("bmtd,mdk->bmtk", V_per_op, self.W_V) |
| |
| gathered = torch.einsum("bij,bmjk->bmik", attn, Vp) |
| if self.shared_wo: |
| |
| summed = gathered.sum(dim=1) |
| out = torch.einsum("btk,kd->btd", summed, self.W_O) |
| else: |
| |
| out = torch.einsum("bmtk,mkd->btd", gathered, self.W_O) |
| return self.dropout(out) |
|
|
|
|
| class _SmrCProjView: |
| """Read-only weight-shape adapter so iter_projected_params + RankPolicy can walk this |
| attention type. Returns a (d, d) dense projection equivalent to summing per-operator |
| W_O contributions (useful for shape introspection, not training).""" |
|
|
| def __init__(self, attn: "SharedMixerRotorAttention"): |
| self._attn = attn |
|
|
| @property |
| def weight(self) -> torch.Tensor: |
| |
| if self._attn.shared_wo: |
| d = self._attn.W_O.size(1) |
| return self._attn.W_O.T @ self._attn.W_O |
| d = self._attn.W_O.size(2) |
| out = self._attn.W_O.new_zeros(d, d) |
| for s in range(self._attn.m_ops): |
| out = out + self._attn.W_O[s].T @ self._attn.W_O[s] |
| return out |
|
|
|
|
| class TuckerAttention(nn.Module): |
| """Tucker decomposition of R^ℓ[i,a,j,b]: |
| |
| R[i,a,j,b] = sum_{p,q} core[p,q] · attn_p[i,j] · W_OV,q[a,b] |
| |
| r_p attention patterns (each with its own (Q_p, K_p) → softmax(Q_p K_p^T) attention map) |
| r_q OV kernels (each with its own V_q and contribution to output) |
| core ∈ R^{r_p × r_q} mixes which attention pattern feeds which kernel. |
| |
| When r_p = r_q = n_head and core = I, this reduces exactly to standard multi-head |
| softmax attention. With r_p ≠ r_q, the count of attention patterns and OV kernels |
| decouple. |
| |
| `tied_v` / `tied_o` controls (Phase B Run 2): test whether observed wins from |
| multiple OV kernels are due to operator multiplicity or to total rank/parameter |
| budget by sharing parameters across operators. |
| |
| tied_v: single V projection (d → d_v) shared across all r_q operators. |
| Different attention patterns applied to the *same* V, then mixed. |
| Note: at r_p == 1 this is degenerate (all per-operator outputs become |
| scalar multiples of one gather-and-V vector). Meaningful only at r_p ≥ 2. |
| tied_o: per-operator d_v contributions all use the same (d_v → d) output |
| projection, then summed. Tests whether output-side multiplicity |
| matters. |
| |
| Either flag reduces parameter count without changing the per-operator structure |
| visible to the model — the rank-vs-diversity discriminant the friend's Run 2 |
| analysis calls for. |
| """ |
|
|
| def __init__(self, cfg: GPTConfig, r_p: int, r_q: int, |
| tied_v: bool = False, tied_o: bool = False, |
| attn_norm: str = "softmax", d_v: int = 0, |
| topk_k: int = 4): |
| super().__init__() |
| d = cfg.n_embd |
| if d % r_p != 0: |
| raise ValueError(f"n_embd ({d}) must be divisible by tucker_r_p ({r_p})") |
| |
| if d_v <= 0: |
| if d % r_q != 0: |
| raise ValueError( |
| f"n_embd ({d}) must be divisible by tucker_r_q ({r_q}) when d_v is auto" |
| ) |
| d_v_eff = d // r_q |
| else: |
| d_v_eff = d_v |
| self.r_p = r_p |
| self.r_q = r_q |
| self.d_qk = d // r_p |
| self.d_v = d_v_eff |
| self.tied_v = tied_v |
| self.tied_o = tied_o |
| if tied_v and r_p == 1: |
| |
| import warnings |
| warnings.warn( |
| "TuckerAttention(r_p=1, tied_v=True) is degenerate: per-operator " |
| "outputs are scalar multiples of the same gather-and-V vector. Use " |
| "r_p >= 2 for a meaningful tied_v test.", |
| stacklevel=2, |
| ) |
| |
| self.q_proj = nn.Linear(d, r_p * self.d_qk, bias=False) |
| self.k_proj = nn.Linear(d, r_p * self.d_qk, bias=False) |
| |
| v_out = self.d_v if tied_v else r_q * self.d_v |
| self.v_proj = nn.Linear(d, v_out, bias=False) |
| |
| |
| |
| if tied_o: |
| self.c_proj = nn.Linear(self.d_v, d, bias=False) |
| else: |
| self.c_proj = nn.Linear(d, d, bias=False) |
| |
| if r_p == r_q: |
| self.core = nn.Parameter(torch.eye(r_p)) |
| else: |
| self.core = nn.Parameter(torch.randn(r_p, r_q) / (r_p * r_q) ** 0.5) |
| |
| self.attn_norm = attn_norm |
| self.attn_gain = nn.Parameter(torch.tensor(0.1)) if attn_norm == "linear_gain" else None |
| self.topk_k = topk_k |
| self.dropout = nn.Dropout(cfg.dropout) |
|
|
| def forward(self, x: torch.Tensor, |
| attn_bias: torch.Tensor | None = None, |
| rope_freqs: tuple[torch.Tensor, torch.Tensor] | None = None) -> torch.Tensor: |
| B, T, _ = x.shape |
| Q = self.q_proj(x).view(B, T, self.r_p, self.d_qk).transpose(1, 2) |
| K = self.k_proj(x).view(B, T, self.r_p, self.d_qk).transpose(1, 2) |
| if rope_freqs is not None: |
| cos, sin = rope_freqs |
| Q = _apply_rope(Q, cos, sin) |
| K = _apply_rope(K, cos, sin) |
| if self.tied_v: |
| V_shared = self.v_proj(x) |
| V = V_shared.unsqueeze(1).expand(B, self.r_q, T, self.d_v) |
| else: |
| V = self.v_proj(x).view(B, T, self.r_q, self.d_v).transpose(1, 2) |
| |
| |
| scores = torch.einsum("bpid,bpjd->bpij", Q, K) / (self.d_qk ** 0.5) |
| attn = _apply_attn_norm(scores, self.attn_norm, self.attn_gain, |
| topk_k=self.topk_k, attn_bias=attn_bias) |
| |
| attn_mixed = torch.einsum("pq,bpij->bqij", self.core, attn) |
| |
| out = torch.einsum("bqij,bqjd->bqid", attn_mixed, V) |
| if self.tied_o: |
| |
| out_lifted = self.c_proj(out) |
| return self.dropout(out_lifted.sum(dim=1)) |
| else: |
| out = out.transpose(1, 2).contiguous().view(B, T, -1) |
| return self.dropout(self.c_proj(out)) |
|
|
|
|
| class MLP(nn.Module): |
| def __init__(self, cfg: GPTConfig, mlp_mult: int | None = None, |
| mlp_activation: str | None = None): |
| super().__init__() |
| mult = cfg.mlp_mult if mlp_mult is None else mlp_mult |
| inter = mult * cfg.n_embd |
| self._activation = cfg.mlp_activation if mlp_activation is None else mlp_activation |
| if self._activation not in {"gelu", "silu", "sigmoid", "linear", "silu_entmax_gate"}: |
| raise ValueError(f"unknown mlp_activation: {self._activation!r}") |
| self.c_fc = nn.Linear(cfg.n_embd, inter, bias=False) |
| self.c_proj = nn.Linear(inter, cfg.n_embd, bias=False) |
| self.dropout = nn.Dropout(cfg.dropout) |
| |
| |
| |
| self._out_rms_scale = float(cfg.mlp_output_rmsnorm_scale) |
| |
| |
| self._act_sparsity_lambda = float(cfg.mlp_act_sparsity_lambda) |
| self._last_act_l1 = None |
| |
| self._act_topk = int(cfg.mlp_act_topk) |
| self._inter = inter |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| x = self.c_fc(x) |
| if self._activation == "linear": |
| pass |
| elif self._activation == "gelu": |
| x = F.gelu(x, approximate="tanh") |
| elif self._activation == "sigmoid": |
| x = torch.sigmoid(x) |
| elif self._activation == "silu_entmax_gate": |
| |
| |
| |
| v = F.silu(x) |
| g = (_pkg_entmax15(x, dim=-1) if _HAVE_ENTMAX_PKG else _entmax15(x, dim=-1)) |
| x = v * g |
| else: |
| x = F.silu(x) |
| if self._act_topk > 0 and self._act_topk < self._inter: |
| |
| |
| k = self._act_topk |
| with torch.no_grad(): |
| _, top_idx = x.abs().topk(k, dim=-1) |
| mask = torch.zeros_like(x).scatter_(-1, top_idx, 1.0) |
| |
| x = x + (x * mask - x).detach() |
| if self._act_sparsity_lambda > 0.0: |
| |
| self._last_act_l1 = x.abs().mean() |
| else: |
| self._last_act_l1 = None |
| x = self.c_proj(x) |
| if self._out_rms_scale > 0.0: |
| rms = x.pow(2).mean(dim=-1, keepdim=True).clamp(min=1e-8).sqrt() |
| x = x / rms * self._out_rms_scale |
| return self.dropout(x) |
|
|
|
|
| class Block(nn.Module): |
| def __init__(self, cfg: GPTConfig, n_head: int | None = None, mlp_mult: int | None = None, |
| mlp_activation: str | None = None): |
| super().__init__() |
| self.ln_1 = nn.LayerNorm(cfg.n_embd) |
| if cfg.attn_kind == "mha": |
| self.attn = CausalSelfAttention(cfg, n_head=n_head) |
| elif cfg.attn_kind == "tucker": |
| self.attn = TuckerAttention( |
| cfg, r_p=cfg.tucker_r_p, r_q=cfg.tucker_r_q, |
| tied_v=cfg.tucker_tied_v, tied_o=cfg.tucker_tied_o, |
| attn_norm=cfg.attn_norm, d_v=cfg.tucker_d_v, |
| topk_k=cfg.topk_k, |
| ) |
| elif cfg.attn_kind == "blockdiag": |
| self.attn = BlockDiagAttention( |
| cfg, block_size=cfg.block_diag_size, |
| block_diag_proj=cfg.block_diag_proj, |
| ) |
| elif cfg.attn_kind == "shared_mixer_rotor": |
| self.attn = SharedMixerRotorAttention( |
| cfg, n_operators=cfg.smr_n_operators, d_v=cfg.smr_d_v, |
| r_kind=cfg.smr_r_kind, rotor_block=cfg.smr_rotor_block, |
| operator_kinds=cfg.smr_operator_kinds, |
| attn_norm=cfg.attn_norm, |
| shared_wo=cfg.smr_shared_wo, |
| topk_k=cfg.topk_k, |
| ) |
| else: |
| raise ValueError(f"unknown attn_kind: {cfg.attn_kind!r}") |
| self.ln_2 = nn.LayerNorm(cfg.n_embd) |
| self.mlp = MLP(cfg, mlp_mult=mlp_mult, mlp_activation=mlp_activation) |
|
|
| def forward(self, x: torch.Tensor, |
| attn_bias: torch.Tensor | None = None, |
| rope_freqs: tuple[torch.Tensor, torch.Tensor] | None = None) -> torch.Tensor: |
| attn_kwargs = {} |
| if attn_bias is not None: |
| attn_kwargs["attn_bias"] = attn_bias |
| if rope_freqs is not None: |
| attn_kwargs["rope_freqs"] = rope_freqs |
| if attn_kwargs: |
| x = x + self.attn(self.ln_1(x), **attn_kwargs) |
| else: |
| x = x + self.attn(self.ln_1(x)) |
| x = x + self.mlp(self.ln_2(x)) |
| return x |
|
|
|
|
| class TransformerCore(nn.Module): |
| def __init__(self, cfg: GPTConfig): |
| super().__init__() |
| self.wte = nn.Embedding(cfg.vocab_size, cfg.n_embd) |
| self.wpe = nn.Embedding(cfg.seq_len, cfg.n_embd) |
| if cfg.n_heads_per_block: |
| if len(cfg.n_heads_per_block) != cfg.n_layer: |
| raise ValueError( |
| f"n_heads_per_block has {len(cfg.n_heads_per_block)} entries; " |
| f"expected {cfg.n_layer} (one per block)." |
| ) |
| heads_per_block = cfg.n_heads_per_block |
| else: |
| heads_per_block = (cfg.n_head,) * cfg.n_layer |
| if cfg.mlp_mult_per_block: |
| if len(cfg.mlp_mult_per_block) != cfg.n_layer: |
| raise ValueError( |
| f"mlp_mult_per_block has {len(cfg.mlp_mult_per_block)} entries; " |
| f"expected {cfg.n_layer} (one per block)." |
| ) |
| mlp_mult_per_block = cfg.mlp_mult_per_block |
| else: |
| mlp_mult_per_block = (cfg.mlp_mult,) * cfg.n_layer |
| if cfg.mlp_activation_per_block: |
| if len(cfg.mlp_activation_per_block) != cfg.n_layer: |
| raise ValueError( |
| f"mlp_activation_per_block has {len(cfg.mlp_activation_per_block)} entries; " |
| f"expected {cfg.n_layer} (one per block)." |
| ) |
| mlp_act_per_block = cfg.mlp_activation_per_block |
| else: |
| mlp_act_per_block = (cfg.mlp_activation,) * cfg.n_layer |
| self.h = nn.ModuleList([ |
| Block(cfg, n_head=heads_per_block[i], mlp_mult=mlp_mult_per_block[i], |
| mlp_activation=mlp_act_per_block[i]) |
| for i in range(cfg.n_layer) |
| ]) |
| self.ln_f = nn.LayerNorm(cfg.n_embd) |
|
|
|
|
| class ByteGPT(nn.Module): |
| def __init__(self, cfg: GPTConfig): |
| super().__init__() |
| if cfg.loss_kind not in {"ce", "bce"}: |
| raise ValueError(f"unknown loss_kind: {cfg.loss_kind!r}") |
| if cfg.position_encoding not in {"learned", "alibi", "rope", "none"}: |
| raise ValueError(f"unknown position_encoding: {cfg.position_encoding!r}") |
| if cfg.position_encoding in {"alibi", "rope"} and cfg.attn_kind not in {"mha", "tucker"}: |
| raise ValueError( |
| f"position_encoding={cfg.position_encoding!r} not supported for " |
| f"attn_kind={cfg.attn_kind!r} (only mha and tucker supported)" |
| ) |
| self.config = cfg |
| self.transformer = TransformerCore(cfg) |
| self.lm_head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False) |
| |
| if cfg.position_encoding == "alibi": |
| n_slopes = cfg.tucker_r_p if cfg.attn_kind == "tucker" else cfg.n_head |
| slopes = _alibi_slopes(n_slopes) * cfg.alibi_slope_scale |
| self.register_buffer("_alibi_slopes", slopes, persistent=False) |
| self._alibi_n_slopes = n_slopes |
| else: |
| self._alibi_slopes = None |
| self._alibi_n_slopes = 0 |
| |
| if cfg.position_encoding == "rope": |
| if cfg.attn_kind == "tucker": |
| self._rope_head_dim = cfg.n_embd // cfg.tucker_r_p |
| else: |
| self._rope_head_dim = cfg.n_embd // cfg.n_head |
| if self._rope_head_dim % 2 != 0: |
| raise ValueError(f"RoPE requires even head_dim, got {self._rope_head_dim}") |
| else: |
| self._rope_head_dim = 0 |
| if cfg.block_gates: |
| if cfg.block_gate_kind == "linear": |
| init_val = 1.0 |
| elif cfg.block_gate_kind in ("sigmoid", "ste"): |
| |
| init_val = 4.0 |
| else: |
| raise ValueError(f"unknown block_gate_kind: {cfg.block_gate_kind!r}") |
| self.block_gate = nn.Parameter(torch.full((cfg.n_layer,), init_val)) |
| else: |
| self.block_gate = None |
| |
| |
| |
| |
| |
| if cfg.res_attn != "none": |
| self.res_attn_q = nn.Parameter(torch.zeros(cfg.n_layer + 1, cfg.n_embd)) |
| else: |
| self.res_attn_q = None |
| |
| |
| |
| self._capture_depth_alpha = False |
| self._depth_alpha_rows: List[torch.Tensor] | None = None |
|
|
| def _gate_values(self) -> torch.Tensor | None: |
| """Resolve raw block_gate parameter to per-block gate values γ_ℓ in the chosen parameterization.""" |
| if self.block_gate is None: |
| return None |
| kind = self.config.block_gate_kind |
| if kind == "linear": |
| return self.block_gate |
| soft = torch.sigmoid(self.block_gate) |
| if kind == "sigmoid": |
| return soft |
| |
| hard = (soft > 0.5).float() |
| return hard + (soft - soft.detach()) |
|
|
| def _depth_aggregate(self, vs: List[torch.Tensor], q: torch.Tensor |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| """One Attention-Residuals aggregation (Eq. 2–4 of arXiv:2603.15031). |
| vs: list of S source tensors [B,T,d] (v_0=embedding, v_i=block Δ). |
| q: this aggregation's learned pseudo-query [d]. |
| Returns (h [B,T,d], alpha [B,T,S]). RMSNorm on keys makes the |
| routing magnitude-invariant; α is normalized over the S sources.""" |
| V = torch.stack(vs, dim=2) |
| k = V * torch.rsqrt(V.pow(2).mean(-1, keepdim=True) + 1e-6) |
| scores = torch.einsum("btsd,d->bts", k, q) |
| scores = scores * self.config.res_attn_temp |
| alpha = _depth_attn_norm(scores, self.config.res_attn_norm) |
| h = torch.einsum("bts,btsd->btd", alpha, V) |
| return h, alpha |
|
|
| def _forward_attnres(self, x: torch.Tensor, |
| attn_bias: torch.Tensor | None, |
| rope_freqs: tuple[torch.Tensor, torch.Tensor] | None |
| ) -> torch.Tensor: |
| """Block-granular Full Attention Residuals. The depth-unit is one |
| transformer block; v_0 is the (post-PE) embedding and v_{i+1} is |
| block i's residual-free contribution f_i(h_i) = Block_i(h_i)−h_i. |
| Each block input and the final readout is a learned aggregation |
| over all preceding v's. See docs/attnres-bp-research-program.md.""" |
| blk_kwargs = {} |
| if attn_bias is not None: |
| blk_kwargs["attn_bias"] = attn_bias |
| if rope_freqs is not None: |
| blk_kwargs["rope_freqs"] = rope_freqs |
| vs: List[torch.Tensor] = [x] |
| rows: List[torch.Tensor] | None = [] if self._capture_depth_alpha else None |
| ent_terms: List[torch.Tensor] = [] |
|
|
| def _entropy(a: torch.Tensor) -> torch.Tensor: |
| |
| return -(a * a.clamp_min(1e-12).log()).sum(dim=-1).mean() |
|
|
| for b, block in enumerate(self.transformer.h): |
| h_in, alpha = self._depth_aggregate(vs, self.res_attn_q[b]) |
| if rows is not None: |
| rows.append(alpha.detach().float().mean(dim=(0, 1)).cpu()) |
| if alpha.size(-1) > 1: |
| ent_terms.append(_entropy(alpha)) |
| out = block(h_in, **blk_kwargs) if blk_kwargs else block(h_in) |
| vs.append(out - h_in) |
| x_out, alpha = self._depth_aggregate(vs, self.res_attn_q[len(self.transformer.h)]) |
| if rows is not None: |
| rows.append(alpha.detach().float().mean(dim=(0, 1)).cpu()) |
| self._depth_alpha_rows = rows |
| if alpha.size(-1) > 1: |
| ent_terms.append(_entropy(alpha)) |
| self._depth_entropy = (torch.stack(ent_terms).mean() if ent_terms |
| else x_out.new_zeros(())) |
| return x_out |
|
|
| def forward(self, idx: torch.Tensor, targets: torch.Tensor | None = None, |
| loop_block_k_override: int | None = None): |
| bsz, seqlen = idx.shape |
| if seqlen > self.config.seq_len: |
| raise ValueError(f"sequence length {seqlen} exceeds model limit {self.config.seq_len}") |
| tok = self.transformer.wte(idx) |
| attn_bias: torch.Tensor | None = None |
| rope_freqs: tuple[torch.Tensor, torch.Tensor] | None = None |
| pe = self.config.position_encoding |
| if pe == "learned": |
| pos = torch.arange(seqlen, device=idx.device) |
| pos_emb = self.transformer.wpe(pos)[None, :, :] |
| x = tok + pos_emb |
| elif pe == "alibi": |
| x = tok |
| attn_bias = _alibi_bias( |
| self._alibi_n_slopes, seqlen, self._alibi_slopes, |
| device=idx.device, dtype=x.dtype, |
| ) |
| elif pe == "rope": |
| x = tok |
| rope_freqs = _make_rope_freqs( |
| self._rope_head_dim, seqlen, base=self.config.rope_base, |
| device=idx.device, dtype=x.dtype, |
| ) |
| else: |
| x = tok |
| gates = self._gate_values() |
| attractor_terms: List[torch.Tensor] = [] |
| attractor_set = set(self.config.block_attractor_indices) |
| loop_set = set(self.config.loop_block_indices) |
| unit_idx = tuple(self.config.loop_unit_indices) |
| if unit_idx: |
| |
| assert all(unit_idx[k + 1] - unit_idx[k] == 1 for k in range(len(unit_idx) - 1)), \ |
| f"loop_unit_indices must be contiguous, got {unit_idx}" |
| assert not (set(unit_idx) & loop_set), \ |
| "loop_unit_indices and loop_block_indices must not overlap" |
| unit_start = unit_idx[0] if unit_idx else None |
| unit_end = unit_idx[-1] if unit_idx else None |
| K_loop = max(1, int(loop_block_k_override if loop_block_k_override is not None else self.config.loop_block_k)) |
|
|
| if self.config.res_attn != "none": |
| |
| |
| |
| |
| if (self.block_gate is not None or self.config.loop_block_indices |
| or self.config.loop_unit_indices |
| or self.config.block_attractor_indices): |
| raise ValueError( |
| "res_attn is not yet composable with block gates / loop-block / " |
| "block-attractor (v1); see docs/attnres-bp-research-program.md." |
| ) |
| x = self._forward_attnres(x, attn_bias, rope_freqs) |
| else: |
| def _apply_block(block, x_in, i): |
| if attn_bias is not None or rope_freqs is not None: |
| y = block(x_in, attn_bias=attn_bias, rope_freqs=rope_freqs) |
| else: |
| y = block(x_in) |
| contribution = (y - x_in) if gates is None else gates[i] * (y - x_in) |
| return x_in + contribution |
|
|
| i = 0 |
| n_blocks = len(self.transformer.h) |
| while i < n_blocks: |
| if unit_start is not None and i == unit_start: |
| |
| x_pre_unit = x |
| for _ in range(K_loop): |
| for j in range(unit_start, unit_end + 1): |
| x = _apply_block(self.transformer.h[j], x, j) |
| if targets is not None: |
| for j in range(unit_start, unit_end + 1): |
| if j in attractor_set: |
| attractor_terms.append((x - x_pre_unit).pow(2).mean()) |
| break |
| i = unit_end + 1 |
| continue |
| block = self.transformer.h[i] |
| x_in_pos = x |
| iters = K_loop if i in loop_set else 1 |
| for _ in range(iters): |
| x = _apply_block(block, x, i) |
| if targets is not None and i in attractor_set: |
| attractor_terms.append((x - x_in_pos).pow(2).mean()) |
| i += 1 |
| x = self.transformer.ln_f(x) |
| logits = self.lm_head(x) |
| loss = None |
| if targets is not None: |
| loss = self._compute_loss(logits, targets) |
| if (self.config.res_attn != "none" |
| and self.config.res_attn_entropy_lambda > 0): |
| loss = loss + self.config.res_attn_entropy_lambda * self._depth_entropy |
| if attractor_terms and self.config.block_attractor_lambda > 0: |
| loss = loss + self.config.block_attractor_lambda * sum(attractor_terms) |
| if gates is not None and self.config.block_gate_l1 > 0: |
| loss = loss + self.config.block_gate_l1 * gates.abs().sum() |
| if self.config.mlp_act_sparsity_lambda > 0.0: |
| act_terms = [blk.mlp._last_act_l1 for blk in self.transformer.h |
| if blk.mlp._last_act_l1 is not None] |
| if act_terms: |
| loss = loss + self.config.mlp_act_sparsity_lambda * sum(act_terms) |
| if (self.config.mlp_byte_concentration_lambda > 0.0 |
| and self.config.mlp_byte_concentration_layers): |
| conc_loss = self._mlp_byte_concentration_loss() |
| |
| loss = loss - self.config.mlp_byte_concentration_lambda * conc_loss |
| return logits, loss |
|
|
| def _mlp_byte_concentration_loss(self) -> torch.Tensor: |
| """Mean top-K row-mass fraction of M = W_lm · W_proj · W_fc · W_wte^T |
| across selected layers. Higher = MLP is more BP-readable as a sparse |
| transition factor (each output byte explained by few input bytes).""" |
| wte = self.transformer.wte.weight |
| lm_head = self.lm_head.weight |
| K = self.config.mlp_byte_concentration_topk |
| layers = self.config.mlp_byte_concentration_layers |
| terms: List[torch.Tensor] = [] |
| for i in layers: |
| blk = self.transformer.h[i] |
| W_fc = blk.mlp.c_fc.weight |
| W_proj = blk.mlp.c_proj.weight |
| W_lin = W_proj @ W_fc |
| M = lm_head @ W_lin @ wte.T |
| M_sq = M.pow(2) |
| row_sums = M_sq.sum(dim=-1, keepdim=True).clamp(min=1e-8) |
| topk_vals, _ = M_sq.topk(K, dim=-1) |
| topk_mass = topk_vals.sum(dim=-1, keepdim=True) / row_sums |
| terms.append(topk_mass.mean()) |
| return torch.stack(terms).mean() if terms else torch.tensor(0.0, |
| device=self.lm_head.weight.device) |
|
|
| def _compute_loss(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: |
| flat_logits = logits.view(-1, logits.size(-1)) |
| flat_targets = targets.reshape(-1) |
| if self.config.loss_kind == "ce": |
| return F.cross_entropy(flat_logits, flat_targets, ignore_index=-100) |
| |
| |
| |
| |
| valid_mask = flat_targets != -100 |
| if not valid_mask.all(): |
| flat_logits = flat_logits[valid_mask] |
| flat_targets = flat_targets[valid_mask] |
| if flat_targets.numel() == 0: |
| return flat_logits.new_zeros(()) |
| if self.config.logit_rmsnorm_scale > 0: |
| flat_logits = F.rms_norm(flat_logits, (flat_logits.size(-1),)) * self.config.logit_rmsnorm_scale |
| one_hot = F.one_hot(flat_targets, num_classes=flat_logits.size(-1)).to(flat_logits.dtype) |
| per_elem = F.binary_cross_entropy_with_logits(flat_logits, one_hot, reduction="none") |
| loss = per_elem.sum(dim=-1).mean() |
| if self.config.sparsity_lambda > 0: |
| neg_mask = 1.0 - one_hot |
| sparsity = (torch.sigmoid(flat_logits) * neg_mask).sum(dim=-1).mean() |
| loss = loss + self.config.sparsity_lambda * sparsity |
| return loss |
|
|
| @torch.no_grad() |
| def generate(self, idx: torch.Tensor, max_new_tokens: int) -> torch.Tensor: |
| for _ in range(max_new_tokens): |
| idx_cond = idx[:, -self.config.seq_len :] |
| logits, _ = self(idx_cond) |
| next_id = torch.argmax(logits[:, -1, :], dim=-1, keepdim=True) |
| idx = torch.cat([idx, next_id], dim=1) |
| return idx |
|
|
|
|
| class RandomByteCorpus: |
| def sample_batch(self, batch_size: int, seq_len: int, device: str) -> tuple[torch.Tensor, torch.Tensor]: |
| raise NotImplementedError |
|
|
| def stats(self) -> Dict[str, int | str]: |
| raise NotImplementedError |
|
|
|
|
| class MixtureByteCorpus(RandomByteCorpus): |
| def __init__(self, corpora: Sequence[RandomByteCorpus], weights: Sequence[float]): |
| if len(corpora) != len(weights): |
| raise ValueError("corpora and weights must have the same length") |
| if not corpora: |
| raise ValueError("mixture corpus needs at least one member") |
| total = float(sum(weights)) |
| if total <= 0.0: |
| raise ValueError("mixture weights must sum to a positive value") |
| self.corpora = list(corpora) |
| self.weights = [float(weight) / total for weight in weights] |
|
|
| def sample_batch(self, batch_size: int, seq_len: int, device: str) -> tuple[torch.Tensor, torch.Tensor]: |
| choices = random.choices(range(len(self.corpora)), weights=self.weights, k=batch_size) |
| counts: Dict[int, int] = {} |
| for idx in choices: |
| counts[idx] = counts.get(idx, 0) + 1 |
| xs = [] |
| ys = [] |
| for idx, count in counts.items(): |
| x, y = self.corpora[idx].sample_batch(batch_size=count, seq_len=seq_len, device=device) |
| xs.append(x) |
| ys.append(y) |
| x_full = torch.cat(xs, dim=0) |
| y_full = torch.cat(ys, dim=0) |
| perm = torch.randperm(x_full.size(0), device=x_full.device) |
| return x_full[perm], y_full[perm] |
|
|
| def stats(self) -> Dict[str, int | str]: |
| payload = {"kind": "mixture"} |
| for idx, (corpus, weight) in enumerate(zip(self.corpora, self.weights)): |
| payload[f"member_{idx}_weight"] = round(weight, 6) |
| payload[f"member_{idx}_kind"] = corpus.stats().get("kind", "unknown") |
| return payload |
|
|
|
|
| def iter_projected_params(model: ByteGPT): |
| for layer_idx, block in enumerate(model.transformer.h): |
| yield ( |
| f"transformer.h.{layer_idx}.attn.c_proj.weight", |
| block.attn.c_proj.weight, |
| layer_idx, |
| "attn_out", |
| ) |
| yield ( |
| f"transformer.h.{layer_idx}.mlp.c_proj.weight", |
| block.mlp.c_proj.weight, |
| layer_idx, |
| "mlp_down", |
| ) |
|
|
|
|
| class InMemoryByteCorpus(RandomByteCorpus): |
| def __init__(self, data: bytes): |
| self.data = torch.tensor(list(data), dtype=torch.long) |
|
|
| def sample_batch(self, batch_size: int, seq_len: int, device: str) -> tuple[torch.Tensor, torch.Tensor]: |
| max_start = self.data.size(0) - seq_len - 1 |
| if max_start <= 0: |
| raise ValueError("corpus too small for requested sequence length") |
| ix = torch.randint(0, max_start, (batch_size,)) |
| x = torch.stack([self.data[i : i + seq_len] for i in ix]).to(device) |
| y = torch.stack([self.data[i + 1 : i + 1 + seq_len] for i in ix]).to(device) |
| return x, y |
|
|
| def stats(self) -> Dict[str, int | str]: |
| return {"kind": "in_memory_bytes", "bytes": int(self.data.numel())} |
|
|
|
|
| def _make_addition_example(max_digits: int, reverse: bool = True) -> str: |
| n_digits_a = random.randint(1, max_digits) |
| n_digits_b = random.randint(1, max_digits) |
| a = random.randint(0, 10 ** n_digits_a - 1) |
| b = random.randint(0, 10 ** n_digits_b - 1) |
| answer = str(a + b) |
| if reverse: |
| answer = answer[::-1] |
| return f"{a}+{b}={answer}\n" |
|
|
|
|
| class ArithmeticByteCorpus(RandomByteCorpus): |
| """Synthetic addition examples encoded as ASCII bytes (vocab=256). |
| |
| Reverse-LSB answer aligns autoregressive generation with the carry chain |
| direction, which is the BP-canonical schedule for addition. |
| """ |
|
|
| def __init__(self, max_digits: int = 3, reverse: bool = True): |
| if max_digits < 1: |
| raise ValueError("max_digits must be >= 1") |
| self.max_digits = max_digits |
| self.reverse = reverse |
|
|
| def sample_batch(self, batch_size: int, seq_len: int, device: str) -> tuple[torch.Tensor, torch.Tensor]: |
| need = seq_len + 1 |
| xs = [] |
| ys = [] |
| for _ in range(batch_size): |
| buf = bytearray() |
| while len(buf) < need: |
| ex = _make_addition_example(self.max_digits, reverse=self.reverse) |
| buf.extend(ex.encode("ascii")) |
| ids = torch.tensor(list(buf[:need]), dtype=torch.long) |
| xs.append(ids[:-1]) |
| ys.append(ids[1:]) |
| return torch.stack(xs).to(device), torch.stack(ys).to(device) |
|
|
| def stats(self) -> Dict[str, int | str]: |
| return { |
| "kind": "arithmetic", |
| "max_digits": self.max_digits, |
| "reverse": "true" if self.reverse else "false", |
| } |
|
|
|
|
| class ParquetTextCorpus(RandomByteCorpus): |
| def __init__(self, parquet_paths: Sequence[Path], text_column: str = "text", |
| cache_target: int = 1024): |
| if pq is None: |
| raise RuntimeError("pyarrow is required for parquet corpora") |
| if not parquet_paths: |
| raise ValueError("no parquet shards provided") |
| self.text_column = text_column |
| self.paths = [Path(p) for p in parquet_paths] |
| self._files = [pq.ParquetFile(path) for path in self.paths] |
| self._row_groups: List[tuple[int, int, int]] = [] |
| self._total_docs = 0 |
| for file_idx, pf in enumerate(self._files): |
| for rg_idx in range(pf.num_row_groups): |
| meta = pf.metadata.row_group(rg_idx) |
| rows = meta.num_rows |
| self._row_groups.append((file_idx, rg_idx, rows)) |
| self._total_docs += rows |
| |
| |
| |
| |
| self._cache_target = int(cache_target) |
| self._cache: List[bytes] = [] |
|
|
| def _refill_cache(self) -> None: |
| while len(self._cache) < self._cache_target: |
| file_idx, rg_idx, _ = random.choice(self._row_groups) |
| rg = self._files[file_idx].read_row_group(rg_idx, columns=[self.text_column]) |
| texts = rg.column(self.text_column).to_pylist() |
| for t in texts: |
| if t: |
| self._cache.append(t.encode("utf-8")) |
| random.shuffle(self._cache) |
|
|
| def _sample_document_bytes(self, min_bytes: int) -> bytes: |
| for _ in range(64): |
| if not self._cache: |
| self._refill_cache() |
| raw = self._cache.pop() |
| if len(raw) >= min_bytes: |
| return raw |
| |
| self._refill_cache() |
| for _ in range(64): |
| if not self._cache: |
| self._refill_cache() |
| raw = self._cache.pop() |
| if len(raw) >= min_bytes: |
| return raw |
| raise ValueError(f"could not sample a document with at least {min_bytes} bytes") |
|
|
| def sample_batch(self, batch_size: int, seq_len: int, device: str) -> tuple[torch.Tensor, torch.Tensor]: |
| xs = [] |
| ys = [] |
| need = seq_len + 1 |
| for _ in range(batch_size): |
| raw = self._sample_document_bytes(need) |
| start_max = len(raw) - need |
| start = 0 if start_max <= 0 else random.randint(0, start_max) |
| chunk = raw[start : start + need] |
| ids = torch.tensor(list(chunk), dtype=torch.long) |
| xs.append(ids[:-1]) |
| ys.append(ids[1:]) |
| return torch.stack(xs).to(device), torch.stack(ys).to(device) |
|
|
| def stats(self) -> Dict[str, int | str]: |
| return { |
| "kind": "parquet_text", |
| "shards": len(self.paths), |
| "row_groups": len(self._row_groups), |
| "documents": int(self._total_docs), |
| } |
|
|
|
|
| def load_tokenized_corpus(paths: Sequence[Path], text_key: str, tokenizer) -> List[int]: |
| """Encode JSONL/text files into a flat list of token IDs using a |
| HuggingFace `tokenizers.Tokenizer` instance. A single newline-id (encoded |
| from "\\n") separates documents so that downstream chunking does not |
| splice two unrelated stories without any boundary signal.""" |
| sep_ids: List[int] = tokenizer.encode("\n").ids or [] |
| ids: List[int] = [] |
| for path in paths: |
| if path.suffix == ".jsonl": |
| with path.open() as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| obj = json.loads(line) |
| text = obj.get(text_key, "") |
| if text: |
| ids.extend(tokenizer.encode(text).ids) |
| ids.extend(sep_ids) |
| else: |
| ids.extend(tokenizer.encode(path.read_text()).ids) |
| ids.extend(sep_ids) |
| if not ids: |
| raise ValueError("no corpus text found") |
| return ids |
|
|
|
|
| def split_tokenized(ids: List[int], val_fraction: float) -> tuple[List[int], List[int]]: |
| cut = max(1, min(len(ids) - 1, int(len(ids) * (1.0 - val_fraction)))) |
| return ids[:cut], ids[cut:] |
|
|
|
|
| def load_text_corpus(paths: Sequence[Path], text_key: str) -> bytes: |
| chunks: List[bytes] = [] |
| for path in paths: |
| if path.suffix == ".jsonl": |
| with path.open() as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| obj = json.loads(line) |
| text = obj.get(text_key, "") |
| if text: |
| chunks.append(text.encode("utf-8") + b"\n") |
| else: |
| chunks.append(path.read_bytes() + b"\n") |
| if not chunks: |
| raise ValueError("no corpus text found") |
| return b"".join(chunks) |
|
|
|
|
| def split_corpus(data: bytes, val_fraction: float) -> tuple[bytes, bytes]: |
| cut = max(1, min(len(data) - 1, int(len(data) * (1.0 - val_fraction)))) |
| train = data[:cut] |
| val = data[cut:] |
| return train, val |
|
|
|
|
| def build_corpora( |
| paths: Sequence[Path], |
| text_key: str, |
| val_fraction: float, |
| parquet_mix_weight: float, |
| text_mix_weight: float, |
| tokenizer=None, |
| ) -> tuple[RandomByteCorpus, RandomByteCorpus]: |
| parquet_paths = [p for p in paths if p.suffix == ".parquet"] |
| non_parquet = [p for p in paths if p.suffix != ".parquet"] |
| if parquet_paths: |
| if tokenizer is not None: |
| raise ValueError("tokenizer mode is not supported with parquet shards yet") |
| if len(parquet_paths) < 2: |
| raise ValueError("need at least 2 parquet shards so the last shard can serve as validation") |
| parquet_paths = sorted(parquet_paths) |
| parquet_train = ParquetTextCorpus(parquet_paths[:-1], text_column=text_key) |
| parquet_val = ParquetTextCorpus(parquet_paths[-1:], text_column=text_key) |
| if not non_parquet: |
| return ( |
| parquet_train, |
| parquet_val, |
| ) |
| raw = load_text_corpus(non_parquet, text_key=text_key) |
| train_raw, val_raw = split_corpus(raw, val_fraction=val_fraction) |
| text_train = InMemoryByteCorpus(train_raw) |
| text_val = InMemoryByteCorpus(val_raw) |
| return ( |
| MixtureByteCorpus([parquet_train, text_train], [parquet_mix_weight, text_mix_weight]), |
| MixtureByteCorpus([parquet_val, text_val], [parquet_mix_weight, text_mix_weight]), |
| ) |
| if tokenizer is not None: |
| ids = load_tokenized_corpus(non_parquet, text_key=text_key, tokenizer=tokenizer) |
| train_ids, val_ids = split_tokenized(ids, val_fraction=val_fraction) |
| return InMemoryByteCorpus(train_ids), InMemoryByteCorpus(val_ids) |
| raw = load_text_corpus(non_parquet, text_key=text_key) |
| train_raw, val_raw = split_corpus(raw, val_fraction=val_fraction) |
| return InMemoryByteCorpus(train_raw), InMemoryByteCorpus(val_raw) |
|
|
|
|
| def bits_per_byte(loss: float) -> float: |
| return loss / math.log(2.0) |
|
|
|
|
| def _json_safe(value): |
| if isinstance(value, Path): |
| return str(value) |
| if isinstance(value, list): |
| return [_json_safe(v) for v in value] |
| if isinstance(value, dict): |
| return {k: _json_safe(v) for k, v in value.items()} |
| return value |
|
|
|
|
| def load_probe_rows(paths: Sequence[Path]) -> Dict[str, List[Dict]]: |
| groups: Dict[str, List[Dict]] = {} |
| for path in paths: |
| rows: List[Dict] = [] |
| with path.open() as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| rows.append(json.loads(line)) |
| groups[path.stem] = rows |
| return groups |
|
|
|
|
| def complete_bytes(model: ByteGPT, device: str, prompt: str, max_new_tokens: int) -> str: |
| ids = torch.tensor([list(prompt.encode("utf-8"))], dtype=torch.long, device=device) |
| out = model.generate(ids, max_new_tokens=max_new_tokens)[0].tolist() |
| return bytes(out).decode("utf-8", errors="ignore") |
|
|
|
|
| def format_sample(model: ByteGPT, device: str, prompt: str, max_new_tokens: int = 80) -> str: |
| try: |
| return complete_bytes(model, device=device, prompt=prompt, max_new_tokens=max_new_tokens) |
| except Exception: |
| return repr(prompt) |
|
|
|
|
| @torch.no_grad() |
| def evaluate_probe_groups(model: ByteGPT, probe_groups: Dict[str, List[Dict]], device: str) -> Dict[str, Dict]: |
| if not probe_groups: |
| return {} |
| model.eval() |
| payload: Dict[str, Dict] = {} |
| for name, rows in probe_groups.items(): |
| results: List[Dict] = [] |
| for row in rows: |
| prompt = row["prompt"] |
| target = row["target"] |
| match_mode = row.get("match", "exact") |
| max_new = int(row.get("max_new_tokens", max(len(target.encode("utf-8")), 1) + 8)) |
| generated = complete_bytes(model, device=device, prompt=prompt, max_new_tokens=max_new) |
| completion = generated[len(prompt) :] if generated.startswith(prompt) else generated |
| got = completion.strip() |
| want = target.strip() |
| if match_mode == "contains": |
| correct = want in got |
| else: |
| correct = got.startswith(want) |
| results.append( |
| { |
| "prompt": prompt, |
| "target": target, |
| "generated": got, |
| "correct": bool(correct), |
| "match": match_mode, |
| } |
| ) |
| payload[name] = { |
| "n": len(results), |
| "correct": sum(1 for row in results if row["correct"]), |
| "examples": results[:20], |
| } |
| return payload |
|
|
|
|
| def save_checkpoint( |
| path: Path, |
| *, |
| model: ByteGPT, |
| optimizer: torch.optim.Optimizer, |
| step: int, |
| history: List[Dict], |
| mode: str, |
| args: argparse.Namespace, |
| model_config: GPTConfig, |
| ) -> None: |
| path.parent.mkdir(parents=True, exist_ok=True) |
| payload = { |
| "step": step, |
| "mode": mode, |
| "args": _json_safe(vars(args)), |
| "model_config": asdict(model_config), |
| "model_state": model.state_dict(), |
| "optimizer_state": optimizer.state_dict(), |
| "history": history, |
| "rng_state": torch.random.get_rng_state(), |
| "cuda_rng_state": torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None, |
| } |
| torch.save(payload, path) |
|
|
|
|
| def load_checkpoint(path: Path, model: ByteGPT, optimizer: torch.optim.Optimizer, device: str) -> Dict: |
| payload = torch.load(path, map_location=device, weights_only=False) |
| model.load_state_dict(payload["model_state"]) |
| optimizer.load_state_dict(payload["optimizer_state"]) |
| torch.random.set_rng_state(payload["rng_state"].cpu()) |
| if torch.cuda.is_available() and payload.get("cuda_rng_state") is not None: |
| torch.cuda.set_rng_state_all([state.cpu() for state in payload["cuda_rng_state"]]) |
| return payload |
|
|
|
|
| def infer_mlp_activation(args: argparse.Namespace, device: str) -> str: |
| if args.mlp_activation is not None: |
| return args.mlp_activation |
| if args.resume is None: |
| return "silu" |
| payload = torch.load(args.resume, map_location=device, weights_only=False) |
| model_cfg = payload.get("model_config", {}) |
| return str(model_cfg.get("mlp_activation", "gelu")) |
|
|
|
|
| def infer_loss_kind(args: argparse.Namespace, device: str) -> str: |
| if args.loss is not None: |
| return args.loss |
| if args.resume is None: |
| return "ce" |
| payload = torch.load(args.resume, map_location=device, weights_only=False) |
| model_cfg = payload.get("model_config", {}) |
| return str(model_cfg.get("loss_kind", "ce")) |
|
|
|
|
| def infer_bce_reg(args: argparse.Namespace, device: str) -> tuple[float, float]: |
| """Resolve --logit-rmsnorm-scale / --sparsity-lambda, falling back to checkpoint if resuming.""" |
| rms = args.logit_rmsnorm_scale |
| spars = args.sparsity_lambda |
| if args.resume is not None and (rms is None or spars is None): |
| payload = torch.load(args.resume, map_location=device, weights_only=False) |
| model_cfg = payload.get("model_config", {}) |
| if rms is None: |
| |
| old_bool = bool(model_cfg.get("logit_rmsnorm", False)) |
| rms = float(model_cfg.get("logit_rmsnorm_scale", 1.0 if old_bool else 0.0)) |
| if spars is None: |
| spars = float(model_cfg.get("sparsity_lambda", 0.0)) |
| return float(rms) if rms is not None else 0.0, float(spars) if spars is not None else 0.0 |
|
|
|
|
| def apply_coppola_projection( |
| model: ByteGPT, |
| controller: CoppolaPretrainingController, |
| mode: str, |
| ) -> Dict[str, Dict[str, float]]: |
| stats: Dict[str, Dict[str, float]] = {"attn_out": {}, "mlp_down": {}} |
| for layer_idx, block in enumerate(model.transformer.h): |
| attn_param = block.attn.c_proj.weight |
| if attn_param.grad is not None: |
| norms = controller.gradient_component_norms(layer_idx, attn_param.grad, param_kind="attn_out") |
| attn_param.grad = controller.project_gradient(layer_idx, attn_param.grad, param_kind="attn_out", mode=mode) |
| stats["attn_out"][str(layer_idx)] = { |
| "routing": norms.routing, |
| "remainder": norms.remainder, |
| } |
| mlp_param = block.mlp.c_proj.weight |
| if mlp_param.grad is not None: |
| norms = controller.gradient_component_norms(layer_idx, mlp_param.grad, param_kind="mlp_down") |
| mlp_param.grad = controller.project_gradient(layer_idx, mlp_param.grad, param_kind="mlp_down", mode=mode) |
| stats["mlp_down"][str(layer_idx)] = asdict(norms) |
| return stats |
|
|
|
|
| def projected_param_names(model: ByteGPT) -> set[str]: |
| return {name for name, _param, _layer_idx, _kind in iter_projected_params(model)} |
|
|
|
|
| @torch.no_grad() |
| def snapshot_projected_params(model: ByteGPT): |
| return [ |
| (param, param.detach().clone(), layer_idx, kind) |
| for _name, param, layer_idx, kind in iter_projected_params(model) |
| ] |
|
|
|
|
| @torch.no_grad() |
| def project_parameter_updates( |
| snapshots, |
| controller: CoppolaPretrainingController, |
| ) -> None: |
| for param, before, layer_idx, kind in snapshots: |
| delta = param - before |
| delta = controller.project_update_support(layer_idx, delta, param_kind=kind) |
| param.copy_(before + delta) |
|
|
|
|
| def build_optimizer( |
| model: ByteGPT, |
| lr: float, |
| weight_decay: float, |
| projected_weight_decay: float, |
| ) -> torch.optim.Optimizer: |
| proj_names = projected_param_names(model) |
| projected: List[torch.nn.Parameter] = [] |
| other: List[torch.nn.Parameter] = [] |
| for name, param in model.named_parameters(): |
| if not param.requires_grad: |
| continue |
| if name in proj_names: |
| projected.append(param) |
| else: |
| other.append(param) |
| groups = [] |
| if projected: |
| groups.append({"params": projected, "weight_decay": projected_weight_decay}) |
| if other: |
| groups.append({"params": other, "weight_decay": weight_decay}) |
| return torch.optim.AdamW(groups, lr=lr) |
|
|
|
|
| @torch.no_grad() |
| def evaluate_arithmetic_accuracy( |
| model: ByteGPT, |
| device: str, |
| n_problems: int, |
| max_digits: int, |
| reverse: bool, |
| ) -> Dict[str, float]: |
| """Per-problem addition accuracy: greedy generation, compare to ground truth.""" |
| model.eval() |
| correct = 0 |
| by_len: Dict[int, List[int]] = {} |
| nl_id = ord("\n") |
| for _ in range(n_problems): |
| n_a = random.randint(1, max_digits) |
| n_b = random.randint(1, max_digits) |
| a = random.randint(0, 10 ** n_a - 1) |
| b = random.randint(0, 10 ** n_b - 1) |
| true_answer = str(a + b) |
| if reverse: |
| true_answer = true_answer[::-1] |
| prompt = f"{a}+{b}=" |
| out_ids = torch.tensor([list(prompt.encode("ascii"))], dtype=torch.long, device=device) |
| max_new = len(true_answer) + 2 |
| gen = bytearray() |
| for _ in range(max_new): |
| logits, _ = model(out_ids) |
| next_id = int(logits[0, -1].argmax()) |
| if next_id == nl_id: |
| break |
| gen.append(next_id) |
| out_ids = torch.cat([out_ids, torch.tensor([[next_id]], device=device)], dim=1) |
| try: |
| generated = gen.decode("ascii", errors="ignore") |
| except Exception: |
| generated = "" |
| is_correct = generated == true_answer |
| n_digits = max(n_a, n_b) |
| bucket = by_len.setdefault(n_digits, [0, 0]) |
| bucket[1] += 1 |
| if is_correct: |
| bucket[0] += 1 |
| correct += 1 |
| out: Dict[str, float] = { |
| "accuracy": correct / max(1, n_problems), |
| "n": float(n_problems), |
| } |
| for k, (c, t) in sorted(by_len.items()): |
| out[f"acc_{k}d"] = c / max(1, t) |
| return out |
|
|
|
|
| @torch.no_grad() |
| def evaluate( |
| model: ByteGPT, |
| corpus: RandomByteCorpus, |
| batch_size: int, |
| seq_len: int, |
| device: str, |
| eval_batches: int, |
| ) -> Dict[str, float]: |
| model.eval() |
| train_losses = [] |
| ce_losses = [] |
| for _ in range(eval_batches): |
| x, y = corpus.sample_batch(batch_size=batch_size, seq_len=seq_len, device=device) |
| logits, loss = model(x, y) |
| train_losses.append(float(loss.item())) |
| |
| ce = F.cross_entropy(logits.view(-1, logits.size(-1)), y.reshape(-1)) |
| ce_losses.append(float(ce.item())) |
| avg_loss = sum(train_losses) / max(1, len(train_losses)) |
| avg_ce = sum(ce_losses) / max(1, len(ce_losses)) |
| return {"loss": avg_loss, "ce_loss": avg_ce, "bpb": bits_per_byte(avg_ce)} |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser(description=__doc__) |
| ap.add_argument("--input", nargs="+", type=Path, default=[ROOT / "content" / "generic_gutenberg" / "raw"]) |
| ap.add_argument("--text-key", default="text") |
| ap.add_argument("--tokenizer-path", type=Path, default=None, |
| help="Path to a HuggingFace tokenizers .json file. If set, " |
| "text is tokenized with it and vocab_size is taken from the " |
| "tokenizer. If unset (default), training is byte-level.") |
| ap.add_argument( |
| "--corpus", |
| choices=["text", "arithmetic"], |
| default="text", |
| help="'text' = byte-level text from --input; " |
| "'arithmetic' = synthetic a+b=c examples (no --input needed).", |
| ) |
| ap.add_argument("--arith-max-digits", type=int, default=3, |
| help="Max digits per operand for --corpus arithmetic.") |
| ap.add_argument("--arith-reverse", action=argparse.BooleanOptionalAction, default=True, |
| help="Reverse-LSB answer (BP-canonical carry direction).") |
| ap.add_argument("--arith-eval-problems", type=int, default=200, |
| help="Per-eval probe count for --corpus arithmetic.") |
| ap.add_argument("--mode", choices=["baseline", "uniform", "zoned"], default="zoned") |
| ap.add_argument("--seq-len", type=int, default=256) |
| ap.add_argument("--batch-size", type=int, default=16) |
| ap.add_argument("--steps", type=int, default=2000) |
| ap.add_argument("--eval-every", type=int, default=100) |
| ap.add_argument("--eval-batches", type=int, default=20) |
| ap.add_argument("--lr", type=float, default=3e-4) |
| ap.add_argument("--lr-schedule", choices=["constant", "cosine", "linear"], default="constant", |
| help="LR schedule: constant (default), cosine to lr-min over --steps, " |
| "or linear to lr-min over --steps. Warmup applies first if --warmup-steps > 0.") |
| ap.add_argument("--lr-min", type=float, default=0.0, |
| help="Floor LR for cosine/linear schedules.") |
| ap.add_argument("--warmup-steps", type=int, default=0, |
| help="Linear warmup from 0 to --lr over this many steps; 0 disables.") |
| ap.add_argument("--weight-decay", type=float, default=0.01) |
| ap.add_argument("--projected-weight-decay", type=float, default=0.0) |
| ap.add_argument("--n-layer", type=int, default=8) |
| ap.add_argument("--n-head", type=int, default=8) |
| ap.add_argument("--n-embd", type=int, default=256) |
| ap.add_argument("--mlp-mult", type=int, default=4) |
| ap.add_argument("--mlp-activation", |
| choices=["gelu", "silu", "sigmoid", "linear", "silu_entmax_gate"], |
| default=None, |
| help="MLP activation. silu_entmax_gate = silu(x) * entmax15(x, dim=-1) — " |
| "BP-canonical sparse gating; entmax15 produces exact zeros on low-scoring " |
| "channels per token while silu carries the unbounded value on the rest.") |
| ap.add_argument( |
| "--loss", |
| choices=["ce", "bce"], |
| default=None, |
| help="Per-token loss: 'ce' = softmax cross-entropy (default); " |
| "'bce' = per-vocab Bernoulli (decoupled gradient, BP-aligned).", |
| ) |
| ap.add_argument( |
| "--logit-rmsnorm-scale", |
| type=float, |
| default=None, |
| help="BCE only. Post-RMSNorm temperature scale on logits before BCE; 0 disables. " |
| "Pure RMSNorm (scale=1) hits a structural BCE floor (~80 for V=256); use 8.0 to " |
| "preserve sphere structure while giving σ enough dynamic range. Default 0 (off).", |
| ) |
| ap.add_argument( |
| "--sparsity-lambda", |
| type=float, |
| default=None, |
| help="BCE only. Coefficient on penalty mean_{j!=t} σ(z_j); drives idle vocab logits " |
| "to zero ('clean nop signal'). Try 1e-3.", |
| ) |
| ap.add_argument( |
| "--block-attractor-indices", |
| type=str, |
| default="", |
| help="Comma-separated block indices to drive toward identity (e.g. '3' or '2,3'). " |
| "Empty = no attractor.", |
| ) |
| ap.add_argument( |
| "--block-attractor-lambda", |
| type=float, |
| default=0.0, |
| help="Coefficient on Σ_ℓ mean((block_ℓ(x) - x)²) for ℓ in --block-attractor-indices. " |
| "Try 0.1 as a starting point.", |
| ) |
| ap.add_argument( |
| "--block-gates", |
| action=argparse.BooleanOptionalAction, |
| default=False, |
| help="Add learnable per-block gates γ_ℓ initialized to 1, applied as " |
| "x_{ℓ+1} = x_ℓ + γ_ℓ * (block_ℓ(x_ℓ) - x_ℓ). γ → 0 makes block identity.", |
| ) |
| ap.add_argument( |
| "--block-gate-kind", |
| choices=["linear", "sigmoid", "ste"], |
| default="linear", |
| help="Gate parameterization. 'linear' = γ = α (init 1.0); 'sigmoid' = γ = σ(α) " |
| "init α=4 (γ≈0.982), bistable under L1; 'ste' = forward γ ∈ {0,1} via threshold, " |
| "backward via sigmoid (interpretable per-block on/off).", |
| ) |
| ap.add_argument( |
| "--block-gate-l1", |
| type=float, |
| default=0.0, |
| help="L1 penalty on block gates: λ * Σ_ℓ |γ_ℓ|. Drives unneeded blocks toward identity. " |
| "For sigmoid/ste γ ∈ (0,1) so penalty is bounded — try λ=0.05 (linear: 0.01).", |
| ) |
| ap.add_argument( |
| "--loop-block-indices", |
| type=str, |
| default="", |
| help="Comma-separated block indices to apply K times during forward (e.g. '1' or '1,2'). " |
| "Each designated block iterates --loop-block-k times. Tests recurrent BP in the " |
| "designated block(s); other blocks remain feedforward. Empty = no looping.", |
| ) |
| ap.add_argument( |
| "--loop-block-k", |
| type=int, |
| default=1, |
| help="K iterations for each block in --loop-block-indices. K=1 (default) = no looping. " |
| "Backprop through K applications during training. Also the eval/inference K.", |
| ) |
| ap.add_argument( |
| "--loop-unit-indices", |
| type=str, |
| default="", |
| help="Comma-separated CONTIGUOUS block indices applied K times as a UNIT (e.g. '1,2' " |
| "applies block1→block2 K times together, vs --loop-block-indices '1,2' which " |
| "applies block1 K times then block2 K times). Deeper loop body for BP iterations " |
| "that need >1 block per step. K = --loop-block-k. Empty = no unit loop.", |
| ) |
| ap.add_argument( |
| "--loop-block-k-min", |
| type=int, |
| default=0, |
| help="Minimum K for variable-K training: if > 0, each training batch samples K ~ " |
| "Uniform({k_min..k_max}). Encourages the loop block to be stable across " |
| "iteration counts (toward a BP fixed-point operator). 0 = disabled (use fixed " |
| "--loop-block-k). Eval/inference still uses --loop-block-k.", |
| ) |
| ap.add_argument( |
| "--loop-block-k-max", |
| type=int, |
| default=0, |
| help="Maximum K for variable-K training. Active only if --loop-block-k-min > 0.", |
| ) |
| ap.add_argument( |
| "--n-heads-per-block", |
| type=str, |
| default="", |
| help="Comma-separated per-block n_head values (one per block, length must equal --n-layer). " |
| "Each must divide --n-embd. Empty = uniform --n-head across all blocks. " |
| "Example: '4,1,1,1' for OR-dominant block 0 with 4 heads, focused single-head elsewhere.", |
| ) |
| ap.add_argument( |
| "--mlp-mult-per-block", |
| type=str, |
| default="", |
| help="Comma-separated per-block mlp_mult values (one per block, length must equal --n-layer). " |
| "Empty = uniform --mlp-mult across all blocks. " |
| "Example for 12L: '2,1,1,1,1,1,1,1,1,1,1,1' to keep block 0 full-width and shrink the rest.", |
| ) |
| ap.add_argument( |
| "--mlp-activation-per-block", |
| type=str, |
| default="", |
| help="Comma-separated per-block MLP activation, choices {linear,sigmoid,silu,gelu}. " |
| "Empty = uniform --mlp-activation. " |
| "Per reverse-compiled adder (docs/coppola-bp-weight-compilation.md): early blocks want " |
| "linear (assembly), middle blocks want sigmoid (conjunction-threshold) or silu, " |
| "late blocks want sigmoid for commitment. " |
| "Example for 12L: 'linear,linear,sigmoid,silu,silu,silu,silu,silu,silu,silu,sigmoid,silu'.", |
| ) |
| ap.add_argument( |
| "--mlp-output-rmsnorm-scale", |
| type=float, |
| default=0.0, |
| help="If > 0, apply RMSNorm to each MLP's output before residual add, with this scale. " |
| "Calibrates per-MLP delta to a fixed log-odds magnitude, mirroring " |
| "--logit-rmsnorm-scale on the LM head side. Hypothesis: enables BP-canonical " |
| "byte-identity-emergence (signature 5) on the MLP side. 0 = off.", |
| ) |
| ap.add_argument( |
| "--mlp-act-sparsity-lambda", |
| type=float, |
| default=0.0, |
| help="L1 penalty on the post-activation inner MLP tensor (analogue of --sparsity-lambda " |
| "but on the MLP intermediate, not vocab probabilities). Pushes the model toward " |
| "per-token typed-channel firing — only a small fraction of inner neurons should " |
| "activate per token. Typical values: 1e-3 to 1e-1. 0 = off.", |
| ) |
| ap.add_argument( |
| "--mlp-act-topk", |
| type=int, |
| default=0, |
| help="Hard top-K mask on inner MLP activations per (batch, token) with " |
| "straight-through estimator. Each token keeps only K of " |
| "(mlp_mult * n_embd) inner neurons active; gradients flow through " |
| "as if the mask weren't there. Tests whether MLPs implement typed " |
| "BP channels when forced to structural per-token sparse firing. " |
| "Typical: 8, 16, 32 out of 512. 0 = off.", |
| ) |
| ap.add_argument( |
| "--mlp-byte-concentration-lambda", |
| type=float, |
| default=0.0, |
| help="Direct gradient pressure on byte-basis row-top-K mass of " |
| "M = W_lm · W_proj · W_fc · W_wte^T. Loss subtracts λ · mean_top_k_fraction. " |
| "Pushes MLP toward sparse-transition BP-readability. Use with " |
| "--mlp-byte-concentration-layers. Typical: 0.1 to 1.0. 0 = off.", |
| ) |
| ap.add_argument( |
| "--mlp-byte-concentration-topk", |
| type=int, |
| default=4, |
| help="K for top-K mass fraction in --mlp-byte-concentration-lambda. Default 4.", |
| ) |
| ap.add_argument( |
| "--mlp-byte-concentration-layers", |
| type=str, |
| default="", |
| help="Comma-separated layer indices to apply MLP byte-concentration loss to. " |
| "Empty = off. Example: '9,10,11' for late blocks only.", |
| ) |
| ap.add_argument( |
| "--attn-kind", |
| choices=["mha", "tucker", "blockdiag", "shared_mixer_rotor"], |
| default="mha", |
| help="Attention factorization. 'mha' = standard multi-head softmax. 'tucker' = " |
| "Tucker decomposition of R: r_p attention patterns × r_q OV kernels mixed by a " |
| "[r_p,r_q] core. r_p=r_q=n_head with core=I matches MHA exactly. 'blockdiag' = " |
| "block-diagonal in (a,b) feature axis: G = d_model/block_diag_size independent " |
| "per-group attentions, no cross-group mixing in c_attn or c_proj. " |
| "'shared_mixer_rotor' = Y = sum_s O_s · A · R_s · M · x with shared mixer M, " |
| "shared attention A, cheap per-operator R_s (diag/rotor/full/none) and per-op " |
| "down-project + lift. The friend's Run-3 architecture.", |
| ) |
| ap.add_argument( |
| "--tucker-r-p", |
| type=int, |
| default=4, |
| help="Tucker only: number of attention patterns r_p. Must divide --n-embd.", |
| ) |
| ap.add_argument( |
| "--tucker-r-q", |
| type=int, |
| default=4, |
| help="Tucker only: number of OV kernels r_q. Must divide --n-embd.", |
| ) |
| ap.add_argument( |
| "--tucker-tied-v", |
| action=argparse.BooleanOptionalAction, |
| default=False, |
| help="Tucker only: tie V projection across all r_q operators (rank-vs-diversity " |
| "Run-2 control). Degenerate at r_p=1; meaningful at r_p≥2.", |
| ) |
| ap.add_argument( |
| "--tucker-tied-o", |
| action=argparse.BooleanOptionalAction, |
| default=False, |
| help="Tucker only: tie output projection (single d_v→d shared across r_q operators, " |
| "sum operator outputs). Tests output-side multiplicity.", |
| ) |
| ap.add_argument( |
| "--tucker-d-v", |
| type=int, |
| default=0, |
| help="Tucker only: explicit per-operator value dim. 0 = auto (d / r_q). " |
| "Setting > 0 decouples d_v from r_q so reader count can scale without " |
| "shrinking the writer (tied-O W_O size = d_v · d).", |
| ) |
| ap.add_argument( |
| "--block-diag-size", |
| type=int, |
| default=4, |
| help="blockdiag only: per-group block size b. Must divide --n-embd. " |
| "Default 4 matches RotorQuant findings.", |
| ) |
| ap.add_argument( |
| "--block-diag-proj", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| help="blockdiag only: if True (default), output projection is also block-diagonal " |
| "(no cross-group mixing). If False, output projection is full d×d, restoring " |
| "cross-group mixing while keeping input-side c_attn block-diagonal.", |
| ) |
| ap.add_argument( |
| "--smr-n-operators", |
| type=int, |
| default=4, |
| help="shared_mixer_rotor only: number of parallel operators (parallel feature " |
| "transforms). Each gets its own R_s, W_V_s, W_O_s.", |
| ) |
| ap.add_argument( |
| "--smr-d-v", |
| type=int, |
| default=32, |
| help="shared_mixer_rotor only: per-operator value dim. Must divide --n-embd.", |
| ) |
| ap.add_argument( |
| "--smr-r-kind", |
| choices=["diag", "rotor", "full", "none"], |
| default="rotor", |
| help="shared_mixer_rotor only: per-operator R_s specialization. 'diag' = diagonal " |
| "scaling, 'rotor' = block-diagonal SO(b) rotation, 'full' = dense d×d (control), " |
| "'none' = identity (only W_V_s and W_O_s differ across operators).", |
| ) |
| ap.add_argument( |
| "--smr-rotor-block", |
| type=int, |
| default=4, |
| help="shared_mixer_rotor only: when --smr-r-kind=rotor, block size for SO(b) " |
| "rotor. Must divide --n-embd.", |
| ) |
| ap.add_argument( |
| "--smr-operator-kinds", |
| type=str, |
| default="", |
| help="shared_mixer_rotor only: heterogeneous per-operator R kinds (Run 4). " |
| "Comma-separated list, one entry per operator (overrides --smr-n-operators " |
| "and --smr-r-kind). Each ∈ {diag, rotor, full, none}. Examples: " |
| "'full,diag,diag,diag' for one rich + cheap tail; " |
| "'full,rotor,rotor,rotor' for one rich + rotor tail.", |
| ) |
| ap.add_argument( |
| "--attn-norm", |
| choices=["softmax", "linear_gain", "rms_signed", "kernel_relu2", "kernel_elu_plus1", |
| "sparsemax", "entmax15", "topk_ste"], |
| default="softmax", |
| help="Attention score normalization (applies to tucker / shared_mixer_rotor). " |
| "softmax (default, BP-foreign convex combination), " |
| "linear_gain (signed unbounded, most BP-canonical), " |
| "rms_signed (signed but bounded), " |
| "kernel_relu2/kernel_elu_plus1 (positive but unbounded), " |
| "sparsemax/entmax15 (sparse exact-zero attention via simplex projection), " |
| "topk_ste (forward = hard top-k softmax, backward = soft via STE).", |
| ) |
| ap.add_argument( |
| "--topk-k", |
| type=int, |
| default=4, |
| help="topk_ste only: keys per query at inference. Default 4. " |
| "Smaller = sparser attention pattern.", |
| ) |
| ap.add_argument( |
| "--res-attn-temp", type=float, default=1.0, |
| help="A2′: fixed scalar τ multiplying the depth scores w_lᵀk before " |
| "Φ (res-attn only). τ>1 gives entmax15 the score range it needs " |
| "to sparsify; the depth analogue of --logit-rmsnorm-scale.") |
| ap.add_argument( |
| "--res-attn-entropy-lambda", type=float, default=0.0, |
| help="A2′: coefficient on +mean H(α_depth) added to the loss " |
| "(res-attn only). Minimised ⇒ peaked/sparse depth attention; " |
| "gives the otherwise-idle L=4 router a reason to specialise.") |
| ap.add_argument( |
| "--position-encoding", |
| choices=["learned", "alibi", "rope", "none"], |
| default="learned", |
| help="'learned' (default) = absolute learned wpe (BP-incompatible). " |
| "'alibi' = no wpe; per-head linear-bias position encoding (Press et al. " |
| "2021), aligning with BP framing where position enters as additive log-odds. " |
| "'rope' = rotary position encoding (Su et al. 2021); relative positions via " |
| "rotation of Q/K, no learned position parameters.", |
| ) |
| ap.add_argument("--rope-base", type=float, default=10000.0, |
| help="RoPE base (default 10000). For short byte context, try 1000 or 100 " |
| "so slow-frequency dims actually rotate noticeably across the window.") |
| ap.add_argument("--alibi-slope-scale", type=float, default=1.0, |
| help="Multiplier on standard ALiBi slopes (default 1.0). Lower values " |
| "(e.g. 0.5) make positional bias gentler — useful when r_p is small.") |
| ap.add_argument( |
| "--res-attn", |
| choices=["none", "full"], |
| default="none", |
| help="Attention Residuals (Kimi, arXiv:2603.15031). 'none' (default) = " |
| "standard additive residual. 'full' = block-granular Full AttnRes: " |
| "each block input is a learned softmax/entmax aggregation over ALL " |
| "preceding block outputs. See docs/attnres-bp-research-program.md.") |
| ap.add_argument( |
| "--res-attn-norm", |
| choices=["softmax", "entmax15", "sparsemax"], |
| default="softmax", |
| help="Normalization Φ for the depth-attention weights (res-attn only). " |
| "'softmax' = paper default (dense). 'entmax15'/'sparsemax' = " |
| "exact-sparse: the learned depth-mixing matrix becomes a sparse " |
| "BP factor graph (this repo's BP-structure extension).") |
| ap.add_argument( |
| "--smr-shared-wo", |
| action=argparse.BooleanOptionalAction, |
| default=False, |
| help="shared_mixer_rotor only: 'many readers, one writer' compressed form. " |
| "Single (d_v, d) W_O shared across operators (sum operator outputs in d_v " |
| "space then one shared lift). Empirically motivated by Tucker tied-O win.", |
| ) |
| ap.add_argument("--dropout", type=float, default=0.0) |
| ap.add_argument("--val-fraction", type=float, default=0.02) |
| ap.add_argument("--parquet-mix-weight", type=float, default=0.5) |
| ap.add_argument("--text-mix-weight", type=float, default=0.5) |
| ap.add_argument("--basis-update-interval", type=int, default=250) |
| ap.add_argument("--uniform-routing-scale", type=float, default=1.0) |
| ap.add_argument("--uniform-and-scale", type=float, default=1.0) |
| ap.add_argument("--uniform-or-scale", type=float, default=1.0) |
| ap.add_argument("--uniform-remainder-scale", type=float, default=0.0) |
| ap.add_argument("--uniform-coupling-scale", type=float, default=1.0) |
| ap.add_argument("--rank-energy", type=float, default=0.99) |
| ap.add_argument("--max-rank", type=int, default=0) |
| ap.add_argument("--seed", type=int, default=0) |
| ap.add_argument("--sample-prompt", default="The ") |
| ap.add_argument("--sample-tokens", type=int, default=80) |
| ap.add_argument("--probe-jsonl", nargs="*", type=Path, default=[]) |
| ap.add_argument("--skip-update-support-projection", action="store_true") |
| ap.add_argument("--save-every", type=int, default=0) |
| ap.add_argument("--checkpoint-dir", type=Path, default=ROOT / "results" / "coppola_pretrain_tiny_ckpts") |
| ap.add_argument("--resume", type=Path, default=None) |
| ap.add_argument("-o", "--output", type=Path, default=ROOT / "results" / "coppola_pretrain_tiny.json") |
| args = ap.parse_args() |
|
|
| set_seed(args.seed) |
| if args.corpus == "arithmetic": |
| train_corpus = ArithmeticByteCorpus(max_digits=args.arith_max_digits, reverse=args.arith_reverse) |
| val_corpus = ArithmeticByteCorpus(max_digits=args.arith_max_digits, reverse=args.arith_reverse) |
| else: |
| input_paths: List[Path] = [] |
| for path in args.input: |
| if path.is_dir(): |
| input_paths.extend(sorted(p for p in path.iterdir() if p.is_file())) |
| else: |
| input_paths.append(path) |
| tokenizer = None |
| if args.tokenizer_path is not None: |
| from tokenizers import Tokenizer |
| tokenizer = Tokenizer.from_file(str(args.tokenizer_path)) |
| print(f"loaded tokenizer ({tokenizer.get_vocab_size()} vocab) from {args.tokenizer_path}") |
| train_corpus, val_corpus = build_corpora( |
| input_paths, |
| text_key=args.text_key, |
| val_fraction=args.val_fraction, |
| parquet_mix_weight=args.parquet_mix_weight, |
| text_mix_weight=args.text_mix_weight, |
| tokenizer=tokenizer, |
| ) |
| if tokenizer is not None: |
| args.vocab_size = tokenizer.get_vocab_size() |
| else: |
| args.vocab_size = 256 |
| probe_groups = load_probe_rows(args.probe_jsonl) |
|
|
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| mlp_activation = infer_mlp_activation(args, device=device) |
| args.mlp_activation = mlp_activation |
| loss_kind = infer_loss_kind(args, device=device) |
| args.loss = loss_kind |
| logit_rmsnorm_scale, sparsity_lambda = infer_bce_reg(args, device=device) |
| args.logit_rmsnorm_scale = logit_rmsnorm_scale |
| args.sparsity_lambda = sparsity_lambda |
| if loss_kind != "bce" and (logit_rmsnorm_scale > 0 or sparsity_lambda > 0): |
| print("warning: --logit-rmsnorm-scale / --sparsity-lambda are BCE-only; ignoring under loss=ce") |
|
|
| block_attractor_indices: tuple[int, ...] = ( |
| tuple(int(x.strip()) for x in args.block_attractor_indices.split(",") if x.strip()) |
| if args.block_attractor_indices |
| else () |
| ) |
| loop_block_indices: tuple[int, ...] = ( |
| tuple(int(x.strip()) for x in args.loop_block_indices.split(",") if x.strip()) |
| if args.loop_block_indices |
| else () |
| ) |
| loop_unit_indices: tuple[int, ...] = ( |
| tuple(int(x.strip()) for x in args.loop_unit_indices.split(",") if x.strip()) |
| if args.loop_unit_indices |
| else () |
| ) |
| n_heads_per_block: tuple[int, ...] = ( |
| tuple(int(x.strip()) for x in args.n_heads_per_block.split(",") if x.strip()) |
| if args.n_heads_per_block |
| else () |
| ) |
| mlp_mult_per_block: tuple[int, ...] = ( |
| tuple(int(x.strip()) for x in args.mlp_mult_per_block.split(",") if x.strip()) |
| if args.mlp_mult_per_block |
| else () |
| ) |
| mlp_activation_per_block: tuple[str, ...] = ( |
| tuple(s.strip() for s in args.mlp_activation_per_block.split(",") if s.strip()) |
| if args.mlp_activation_per_block |
| else () |
| ) |
| smr_operator_kinds: tuple[str, ...] = ( |
| tuple(s.strip() for s in args.smr_operator_kinds.split(",") if s.strip()) |
| if args.smr_operator_kinds |
| else () |
| ) |
|
|
| cfg = GPTConfig( |
| vocab_size=getattr(args, "vocab_size", 256), |
| seq_len=args.seq_len, |
| n_layer=args.n_layer, |
| n_head=args.n_head, |
| n_embd=args.n_embd, |
| mlp_mult=args.mlp_mult, |
| mlp_activation=mlp_activation, |
| dropout=args.dropout, |
| loss_kind=loss_kind, |
| logit_rmsnorm_scale=logit_rmsnorm_scale, |
| sparsity_lambda=sparsity_lambda, |
| block_attractor_indices=block_attractor_indices, |
| block_attractor_lambda=float(args.block_attractor_lambda), |
| block_gates=bool(args.block_gates), |
| block_gate_kind=str(args.block_gate_kind), |
| block_gate_l1=float(args.block_gate_l1), |
| loop_block_indices=loop_block_indices, |
| loop_block_k=int(args.loop_block_k), |
| loop_unit_indices=loop_unit_indices, |
| n_heads_per_block=n_heads_per_block, |
| mlp_mult_per_block=mlp_mult_per_block, |
| mlp_activation_per_block=mlp_activation_per_block, |
| mlp_output_rmsnorm_scale=float(args.mlp_output_rmsnorm_scale), |
| mlp_act_sparsity_lambda=float(args.mlp_act_sparsity_lambda), |
| mlp_act_topk=int(args.mlp_act_topk), |
| mlp_byte_concentration_lambda=float(args.mlp_byte_concentration_lambda), |
| mlp_byte_concentration_topk=int(args.mlp_byte_concentration_topk), |
| mlp_byte_concentration_layers=( |
| tuple(int(x.strip()) for x in args.mlp_byte_concentration_layers.split(",") if x.strip()) |
| if args.mlp_byte_concentration_layers else () |
| ), |
| attn_kind=str(args.attn_kind), |
| tucker_r_p=int(args.tucker_r_p), |
| tucker_r_q=int(args.tucker_r_q), |
| tucker_tied_v=bool(args.tucker_tied_v), |
| tucker_tied_o=bool(args.tucker_tied_o), |
| tucker_d_v=int(args.tucker_d_v), |
| block_diag_size=int(args.block_diag_size), |
| block_diag_proj=bool(args.block_diag_proj), |
| smr_n_operators=int(args.smr_n_operators), |
| smr_d_v=int(args.smr_d_v), |
| smr_r_kind=str(args.smr_r_kind), |
| smr_rotor_block=int(args.smr_rotor_block), |
| smr_operator_kinds=smr_operator_kinds, |
| attn_norm=str(args.attn_norm), |
| smr_shared_wo=bool(args.smr_shared_wo), |
| topk_k=int(args.topk_k), |
| position_encoding=str(args.position_encoding), |
| rope_base=float(args.rope_base), |
| alibi_slope_scale=float(args.alibi_slope_scale), |
| res_attn=str(args.res_attn), |
| res_attn_norm=str(args.res_attn_norm), |
| res_attn_temp=float(args.res_attn_temp), |
| res_attn_entropy_lambda=float(args.res_attn_entropy_lambda), |
| ) |
| model = ByteGPT(cfg).to(device) |
|
|
| controller = None |
| if args.mode != "baseline": |
| rank_policy = RankPolicy(energy=args.rank_energy, max_rank=(args.max_rank or None)) |
| controller = CoppolaPretrainingController( |
| CoppolaPretrainingConfig( |
| n_head=args.n_head, |
| attn_output_policy=rank_policy, |
| mlp_output_policy=rank_policy, |
| mlp_input_policy=rank_policy, |
| basis_update_interval=args.basis_update_interval, |
| uniform_scales=FamilyScales( |
| routing=args.uniform_routing_scale, |
| and_transport=args.uniform_and_scale, |
| or_update=args.uniform_or_scale, |
| remainder=args.uniform_remainder_scale, |
| coupling=args.uniform_coupling_scale, |
| ), |
| ) |
| ) |
| controller.refresh_from_model(model) |
|
|
| optimizer = build_optimizer( |
| model, |
| lr=args.lr, |
| weight_decay=args.weight_decay, |
| projected_weight_decay=args.projected_weight_decay if controller is not None else args.weight_decay, |
| ) |
|
|
| history: List[Dict] = [] |
| start_step = 1 |
| if args.resume is not None: |
| payload = load_checkpoint(args.resume, model, optimizer, device=device) |
| history = payload.get("history", []) |
| start_step = int(payload["step"]) + 1 |
| if controller is not None: |
| controller.refresh_from_model(model) |
|
|
| last_projection_stats: Dict[str, Dict[str, float]] | None = None |
|
|
| def _current_lr(step: int) -> float: |
| lr_max = args.lr |
| lr_min = args.lr_min |
| if args.warmup_steps > 0 and step <= args.warmup_steps: |
| return lr_max * step / max(1, args.warmup_steps) |
| if args.lr_schedule == "constant": |
| return lr_max |
| |
| decay_start = args.warmup_steps |
| decay_total = max(1, args.steps - decay_start) |
| progress = min(1.0, max(0.0, (step - decay_start) / decay_total)) |
| if args.lr_schedule == "cosine": |
| return lr_min + 0.5 * (lr_max - lr_min) * (1.0 + math.cos(math.pi * progress)) |
| if args.lr_schedule == "linear": |
| return lr_max - (lr_max - lr_min) * progress |
| return lr_max |
|
|
| for step in range(start_step, args.steps + 1): |
| model.train() |
| if controller is not None and step % args.basis_update_interval == 1 and step > 1: |
| controller.refresh_from_model(model) |
| cur_lr = _current_lr(step) |
| for pg in optimizer.param_groups: |
| pg["lr"] = cur_lr |
| x, y = train_corpus.sample_batch(batch_size=args.batch_size, seq_len=args.seq_len, device=device) |
| optimizer.zero_grad(set_to_none=True) |
| |
| k_override = None |
| if args.loop_block_k_min > 0 and args.loop_block_k_max >= args.loop_block_k_min: |
| k_override = random.randint(args.loop_block_k_min, args.loop_block_k_max) |
| _, loss = model(x, y, loop_block_k_override=k_override) |
| loss.backward() |
| param_snapshots = None |
| if controller is not None: |
| last_projection_stats = apply_coppola_projection(model, controller, mode=args.mode) |
| if not args.skip_update_support_projection: |
| param_snapshots = snapshot_projected_params(model) |
| optimizer.step() |
| if controller is not None and param_snapshots is not None: |
| project_parameter_updates(param_snapshots, controller) |
|
|
| if step == 1 or step % args.eval_every == 0 or step == args.steps: |
| metrics = evaluate( |
| model, |
| val_corpus, |
| batch_size=args.batch_size, |
| seq_len=args.seq_len, |
| device=device, |
| eval_batches=args.eval_batches, |
| ) |
| sample = format_sample(model, device=device, prompt=args.sample_prompt, max_new_tokens=args.sample_tokens) |
| payload = { |
| "step": step, |
| "train_loss": float(loss.item()), |
| "val_loss": metrics["loss"], |
| "val_ce": metrics["ce_loss"], |
| "val_bpb": metrics["bpb"], |
| "sample": sample, |
| } |
| if last_projection_stats is not None: |
| payload["projection_stats"] = last_projection_stats |
| probe_payload = evaluate_probe_groups(model, probe_groups, device=device) |
| if probe_payload: |
| payload["probe_eval"] = probe_payload |
| if args.corpus == "arithmetic": |
| payload["arith"] = evaluate_arithmetic_accuracy( |
| model, |
| device=device, |
| n_problems=args.arith_eval_problems, |
| max_digits=args.arith_max_digits, |
| reverse=args.arith_reverse, |
| ) |
| history.append(payload) |
| line = ( |
| f"step={step:5d} train_loss={loss.item():.4f} " |
| f"val_loss={metrics['loss']:.4f} val_ce={metrics['ce_loss']:.4f} " |
| f"val_bpb={metrics['bpb']:.4f}" |
| ) |
| if "arith" in payload: |
| line += f" arith_acc={payload['arith']['accuracy']:.3f}" |
| if model.block_gate is not None: |
| with torch.no_grad(): |
| gate_vals = model._gate_values() |
| gates = gate_vals.detach().cpu().tolist() if gate_vals is not None else [] |
| line += " γ=[" + " ".join(f"{g:+.3f}" for g in gates) + "]" |
| payload["block_gates"] = gates |
| print(line, flush=True) |
| if args.save_every > 0 and (step % args.save_every == 0 or step == args.steps): |
| ckpt_path = args.checkpoint_dir / f"{args.mode}_step_{step}.pt" |
| save_checkpoint( |
| ckpt_path, |
| model=model, |
| optimizer=optimizer, |
| step=step, |
| history=history, |
| mode=args.mode, |
| args=args, |
| model_config=cfg, |
| ) |
| save_checkpoint( |
| args.checkpoint_dir / f"{args.mode}_latest.pt", |
| model=model, |
| optimizer=optimizer, |
| step=step, |
| history=history, |
| mode=args.mode, |
| args=args, |
| model_config=cfg, |
| ) |
|
|
| args.output.parent.mkdir(parents=True, exist_ok=True) |
| output = { |
| "mode": args.mode, |
| "steps": args.steps, |
| "seed": args.seed, |
| "config": _json_safe(vars(args)), |
| "model_config": asdict(cfg), |
| "train_corpus": _json_safe(train_corpus.stats()), |
| "val_corpus": _json_safe(val_corpus.stats()), |
| "history": history, |
| } |
| if history: |
| output["final_val_bpb"] = history[-1].get("val_bpb") |
| output["best_val_bpb"] = min( |
| row["val_bpb"] for row in history if row.get("val_bpb") is not None |
| ) |
| args.output.write_text(json.dumps(output, indent=2)) |
| print(f"saved {args.output}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|