BananaMind-2.1-Unified / modeling_relay.py
Banaxi-Tech's picture
Upload 43 files
70ffce3 verified
Raw
History Blame Contribute Delete
27.5 kB
#!/usr/bin/env python3
"""BananaMind 2.1 Unified - three towers, relay middle, 35.0M params.
Three independent transformer stacks share one input embedding. A and C are
the outer towers; each owns an output head and contributes to the mixture. B is
the relay: it has no head, no solo loss term, and no gradient of its own except
what arrives through its four bridge directions. There is no direct A<->C path,
so anything the two outer towers share about a token has to survive a trip
through B.
Exchange schedule
-----------------
Three rounds. Each round reads the outer towers, lands the sum in B, lets B
process, then reads B and lands the result back in both outer towers.
round | A read | C read | -> B | B runs | B read | -> A | -> C
1 | 5 | 2 | pre-1 | 1-2 | 2 | 7 | 3
2 | 9 | 4 | pre-3 | 3-4 | 4 | 11 | 5
3 | 12 | 5 | pre-5 | 5 | 5 | 14 | 6
Bridge output is added to the residual *before* the receiving block rather than
after it, which is what guarantees B always has at least one full block between
taking a signal in and handing one back out.
Gates are per-channel and initialise to 0.01, not 0. B is the only path between
A and C and it carries no solo loss, so a zero-init would leave the middle with
no gradient signal at all on step 0 and a real chance of never waking up. 0.01
is small enough that the towers still start as three near-independent models.
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
import torch
import torch.nn as nn
import torch.nn.functional as F
# --------------------------------------------------------------------------- #
# config
# --------------------------------------------------------------------------- #
@dataclass
class TowerSpec:
hidden_size: int
num_hidden_layers: int
num_attention_heads: int
num_key_value_heads: int
intermediate_size: int
@dataclass
class RelayConfig:
# inherited from Mini / 2.0 Unified, unchanged
vocab_size: int = 8192
head_dim: int = 64
max_position_embeddings: int = 4096
rope_theta: float = 100000.0
rms_norm_eps: float = 1e-6
# the shared embedding lives at tower C's width, so C reads it natively and
# A and B each get one projection down.
embed_width: int = 384
tower_a: TowerSpec = field(
default_factory=lambda: TowerSpec(
hidden_size=256,
num_hidden_layers=14,
num_attention_heads=4,
num_key_value_heads=1,
intermediate_size=704,
)
)
tower_b: TowerSpec = field(
default_factory=lambda: TowerSpec(
hidden_size=320,
num_hidden_layers=5,
num_attention_heads=5,
num_key_value_heads=1,
intermediate_size=960,
)
)
tower_c: TowerSpec = field(
default_factory=lambda: TowerSpec(
hidden_size=384,
num_hidden_layers=6,
num_attention_heads=6,
num_key_value_heads=2,
intermediate_size=1024,
)
)
# 1-indexed exchange schedule. `*_read` is where a bridge takes a tower's
# state; `*_land` is where the returning signal is added. Biased deep on
# the outer towers, because in the 2.0 run the deep bridges carried 4-25x
# the gate magnitude of the shallow ones.
a_read: tuple[int, ...] = (5, 9, 12)
a_land: tuple[int, ...] = (7, 11, 14)
c_read: tuple[int, ...] = (2, 4, 5)
c_land: tuple[int, ...] = (3, 5, 6)
b_land: tuple[int, ...] = (1, 3, 5)
b_read: tuple[int, ...] = (2, 4, 5)
gate_init: float = 0.01
# untied on purpose: A and C each own an output space, so either one still
# decodes by itself once the bridges come off.
tie_word_embeddings: bool = False
@property
def n_rounds(self) -> int:
return len(self.a_read)
def validate(self) -> None:
if self.embed_width != self.tower_c.hidden_size:
raise ValueError("shared embedding width must match tower C width")
n = self.n_rounds
for name in ("a_read", "a_land", "c_read", "c_land", "b_land", "b_read"):
if len(getattr(self, name)) != n:
raise ValueError(f"{name} must have {n} entries")
# Every tower is visited strictly left to right, and every bridge reads
# a state that already exists. If this passes, the forward pass below
# is a valid topological order.
for read, land, spec, tag in (
(self.a_read, self.a_land, self.tower_a, "A"),
(self.c_read, self.c_land, self.tower_c, "C"),
):
cursor = 0
for k in range(n):
# `==` is legal: the previous round's signal is added before that
# layer runs, so reading right after it is still causal.
if not cursor <= read[k]:
raise ValueError(f"tower {tag} round {k}: read layer {read[k]} is behind cursor {cursor}")
if not read[k] < land[k]:
raise ValueError(f"tower {tag} round {k}: land {land[k]} must follow read {read[k]}")
cursor = land[k]
if max(land) > spec.num_hidden_layers or min(read) < 1:
raise ValueError(f"tower {tag} schedule leaves the stack")
cursor = 0
for k in range(n):
if not cursor < self.b_land[k]:
raise ValueError(f"tower B round {k}: land {self.b_land[k]} is not ahead of {cursor}")
if not self.b_land[k] <= self.b_read[k]:
raise ValueError(f"tower B round {k}: read {self.b_read[k]} precedes land {self.b_land[k]}")
cursor = self.b_read[k]
if max(self.b_read) > self.tower_b.num_hidden_layers or min(self.b_land) < 1:
raise ValueError("tower B schedule leaves the stack")
# --------------------------------------------------------------------------- #
# primitives (identical to train_mini.py / 2.0 Unified)
# --------------------------------------------------------------------------- #
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
x_float = x.float()
rms = torch.rsqrt(x_float.pow(2).mean(-1, keepdim=True) + self.eps)
return (x_float * rms * self.weight.float()).type_as(x)
def build_rope_inv_freq(head_dim: int, theta: float = 100000.0) -> torch.Tensor:
return 1.0 / (theta ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
def precompute_freqs_cis(head_dim: int, seq_len: int, theta: float = 100000.0) -> torch.Tensor:
freqs = torch.outer(torch.arange(seq_len, dtype=torch.float32), build_rope_inv_freq(head_dim, theta))
return torch.polar(torch.ones_like(freqs), freqs)
def apply_rotary_emb(
q: torch.Tensor, k: torch.Tensor, freqs_cis: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
q_complex = torch.view_as_complex(q.float().reshape(*q.shape[:-1], -1, 2))
k_complex = torch.view_as_complex(k.float().reshape(*k.shape[:-1], -1, 2))
freqs_cis = freqs_cis.unsqueeze(0).unsqueeze(0)
q_out = torch.view_as_real(q_complex * freqs_cis).flatten(-2)
k_out = torch.view_as_real(k_complex * freqs_cis).flatten(-2)
return q_out.type_as(q), k_out.type_as(k)
class TowerAttention(nn.Module):
def __init__(self, spec: TowerSpec, head_dim: int, rms_norm_eps: float):
super().__init__()
self.n_head = spec.num_attention_heads
self.n_kv_heads = spec.num_key_value_heads
self.head_dim = head_dim
self.n_rep = self.n_head // self.n_kv_heads
self.q_proj = nn.Linear(spec.hidden_size, self.n_head * head_dim, bias=False)
self.k_proj = nn.Linear(spec.hidden_size, self.n_kv_heads * head_dim, bias=False)
self.v_proj = nn.Linear(spec.hidden_size, self.n_kv_heads * head_dim, bias=False)
self.o_proj = nn.Linear(self.n_head * head_dim, spec.hidden_size, bias=False)
self.o_proj.NANOGPT_SCALE_INIT = 1
self.q_norm = RMSNorm(head_dim, eps=rms_norm_eps)
self.k_norm = RMSNorm(head_dim, eps=rms_norm_eps)
def forward(self, x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
bsz, seq_len, _ = x.size()
q = self.q_proj(x).view(bsz, seq_len, self.n_head, self.head_dim).transpose(1, 2)
k = self.k_proj(x).view(bsz, seq_len, self.n_kv_heads, self.head_dim).transpose(1, 2)
v = self.v_proj(x).view(bsz, seq_len, self.n_kv_heads, self.head_dim).transpose(1, 2)
q = self.q_norm(q)
k = self.k_norm(k)
q, k = apply_rotary_emb(q, k, freqs_cis)
# .contiguous() is required for torch.compile: without it the expanded
# view reaches the flash-attention backward with strides Inductor
# rejects. Pure layout change, no effect on the math.
k = (
k.unsqueeze(2)
.expand(bsz, self.n_kv_heads, self.n_rep, seq_len, self.head_dim)
.reshape(bsz, self.n_head, seq_len, self.head_dim)
.contiguous()
)
v = (
v.unsqueeze(2)
.expand(bsz, self.n_kv_heads, self.n_rep, seq_len, self.head_dim)
.reshape(bsz, self.n_head, seq_len, self.head_dim)
.contiguous()
)
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
y = y.transpose(1, 2).contiguous().view(bsz, seq_len, self.n_head * self.head_dim)
return self.o_proj(y)
class TowerSwiGLUMLP(nn.Module):
def __init__(self, spec: TowerSpec):
super().__init__()
self.w_gate = nn.Linear(spec.hidden_size, spec.intermediate_size, bias=False)
self.w_up = nn.Linear(spec.hidden_size, spec.intermediate_size, bias=False)
self.w_down = nn.Linear(spec.intermediate_size, spec.hidden_size, bias=False)
self.w_down.NANOGPT_SCALE_INIT = 1
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x))
class TowerBlock(nn.Module):
"""A plain Mini block. Nothing in here knows about bridges."""
def __init__(self, spec: TowerSpec, head_dim: int, rms_norm_eps: float):
super().__init__()
self.ln_1 = RMSNorm(spec.hidden_size, eps=rms_norm_eps)
self.attn = TowerAttention(spec, head_dim, rms_norm_eps)
self.ln_2 = RMSNorm(spec.hidden_size, eps=rms_norm_eps)
self.mlp = TowerSwiGLUMLP(spec)
def forward(self, x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
x = x + self.attn(self.ln_1(x), freqs_cis)
x = x + self.mlp(self.ln_2(x))
return x
# --------------------------------------------------------------------------- #
# bridge
# --------------------------------------------------------------------------- #
class Edge(nn.Module):
"""One direction of one exchange point.
W is a plain linear, g a per-channel gate. The gate starts small but not
zero so the relay gets traffic on step 0.
"""
def __init__(self, dim_in: int, dim_out: int, gate_init: float):
super().__init__()
self.w = nn.Linear(dim_in, dim_out, bias=False)
self.g = nn.Parameter(torch.full((dim_out,), float(gate_init)))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.g * self.w(x)
# --------------------------------------------------------------------------- #
# model
# --------------------------------------------------------------------------- #
class BananaMindRelay(nn.Module):
def __init__(self, config: RelayConfig):
super().__init__()
config.validate()
self.config = config
a, b, c = config.tower_a, config.tower_b, config.tower_c
self.wte = nn.Embedding(config.vocab_size, config.embed_width)
self.in_proj_a = nn.Linear(config.embed_width, a.hidden_size, bias=False)
self.in_proj_b = nn.Linear(config.embed_width, b.hidden_size, bias=False)
# tower C reads the shared embedding natively; no projection
self.blocks_a = nn.ModuleList(
[TowerBlock(a, config.head_dim, config.rms_norm_eps) for _ in range(a.num_hidden_layers)]
)
self.blocks_b = nn.ModuleList(
[TowerBlock(b, config.head_dim, config.rms_norm_eps) for _ in range(b.num_hidden_layers)]
)
self.blocks_c = nn.ModuleList(
[TowerBlock(c, config.head_dim, config.rms_norm_eps) for _ in range(c.num_hidden_layers)]
)
n = config.n_rounds
gi = config.gate_init
self.edges_a2b = nn.ModuleList([Edge(a.hidden_size, b.hidden_size, gi) for _ in range(n)])
self.edges_c2b = nn.ModuleList([Edge(c.hidden_size, b.hidden_size, gi) for _ in range(n)])
self.edges_b2a = nn.ModuleList([Edge(b.hidden_size, a.hidden_size, gi) for _ in range(n)])
self.edges_b2c = nn.ModuleList([Edge(b.hidden_size, c.hidden_size, gi) for _ in range(n)])
# B has no final norm because it has no head: its only outputs are the
# bridge reads, which are normalised by the receiving tower's blocks.
self.ln_f_a = RMSNorm(a.hidden_size, eps=config.rms_norm_eps)
self.ln_f_c = RMSNorm(c.hidden_size, eps=config.rms_norm_eps)
self.lm_head_a = nn.Linear(a.hidden_size, config.vocab_size, bias=False)
self.lm_head_c = nn.Linear(c.hidden_size, config.vocab_size, bias=False)
if config.tie_word_embeddings:
self.lm_head_c.weight = self.wte.weight
# still a 2-way mixer: it reads the two towers that have heads
self.mix_head = nn.Linear(a.hidden_size + c.hidden_size, 1, bias=True)
self._embd_scale = math.sqrt(config.embed_width)
self._freqs_cis_cache: torch.Tensor | None = None
self._init_all()
# ---- init -------------------------------------------------------------- #
def _init_module(self, module: nn.Module, depth: int) -> None:
std = 0.02
if hasattr(module, "NANOGPT_SCALE_INIT"):
std *= 2 * depth**-0.5
if isinstance(module, nn.Linear):
torch.nn.init.normal_(module.weight, mean=0.0, std=std)
if module.bias is not None:
torch.nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
def _init_all(self) -> None:
cfg = self.config
depth_a = cfg.tower_a.num_hidden_layers
depth_b = cfg.tower_b.num_hidden_layers
depth_c = cfg.tower_c.num_hidden_layers
for blocks, depth in (
(self.blocks_a, depth_a),
(self.blocks_b, depth_b),
(self.blocks_c, depth_c),
):
for m in blocks.modules():
self._init_module(m, depth)
for m in (
self.wte,
self.in_proj_a,
self.in_proj_b,
self.lm_head_a,
self.lm_head_c,
self.mix_head,
):
self._init_module(m, depth_a)
for edges, depth in (
(self.edges_a2b, depth_b),
(self.edges_c2b, depth_b),
(self.edges_b2a, depth_a),
(self.edges_b2c, depth_c),
):
for edge in edges:
self._init_module(edge.w, depth)
torch.nn.init.constant_(edge.g, cfg.gate_init)
# start the mixer at alpha = 0.5 so neither outer tower is favoured
torch.nn.init.zeros_(self.mix_head.weight)
torch.nn.init.zeros_(self.mix_head.bias)
# ---- rope cache -------------------------------------------------------- #
def _get_freqs_cis(self, seq_len: int, device: torch.device) -> torch.Tensor:
cache = self._freqs_cis_cache
if cache is None or cache.device != device or cache.size(0) < seq_len:
cache = precompute_freqs_cis(
self.config.head_dim, seq_len, self.config.rope_theta
).to(device)
self._freqs_cis_cache = cache
return cache[:seq_len]
# ---- forward ----------------------------------------------------------- #
@staticmethod
def _advance(
blocks: nn.ModuleList,
x: torch.Tensor,
cursor: int,
target: int,
freqs_cis: torch.Tensor,
pending: dict[int, torch.Tensor],
) -> tuple[torch.Tensor, int]:
"""Run `blocks` from `cursor` up to and including 1-indexed `target`.
A pending bridge value keyed by a 1-indexed layer is added to the
residual immediately before that layer runs.
"""
while cursor < target:
layer = cursor + 1
arriving = pending.pop(layer, None)
if arriving is not None:
x = x + arriving
x = blocks[cursor](x, freqs_cis)
cursor += 1
return x, cursor
def forward(
self,
input_ids: torch.Tensor,
cut_bridges: bool = False,
collect_diagnostics: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]:
"""Returns (h_a, h_c, mix_logit, diagnostics).
With `cut_bridges=True` the three stacks run completely independently:
A and C become ordinary transformers and B becomes dead weight that
influences nothing.
"""
cfg = self.config
_, seq_len = input_ids.size()
freqs_cis = self._get_freqs_cis(seq_len, input_ids.device)
embedded = self.wte(input_ids) * self._embd_scale
x_a = self.in_proj_a(embedded)
x_b = self.in_proj_b(embedded)
x_c = embedded
pend_a: dict[int, torch.Tensor] = {}
pend_b: dict[int, torch.Tensor] = {}
pend_c: dict[int, torch.Tensor] = {}
cur_a = cur_b = cur_c = 0
diagnostics: dict = {}
for k in range(cfg.n_rounds):
# 1. outer towers walk to their read points, applying whatever the
# previous round handed them on the way.
x_a, cur_a = self._advance(self.blocks_a, x_a, cur_a, cfg.a_read[k], freqs_cis, pend_a)
x_c, cur_c = self._advance(self.blocks_c, x_c, cur_c, cfg.c_read[k], freqs_cis, pend_c)
# 2. both outer signals land in the relay at the same layer.
if not cut_bridges:
pend_b[cfg.b_land[k]] = self.edges_a2b[k](x_a) + self.edges_c2b[k](x_c)
# 3. the relay processes, then hands one signal back to each side.
x_b, cur_b = self._advance(self.blocks_b, x_b, cur_b, cfg.b_read[k], freqs_cis, pend_b)
if not cut_bridges:
pend_a[cfg.a_land[k]] = self.edges_b2a[k](x_b)
pend_c[cfg.c_land[k]] = self.edges_b2c[k](x_b)
if collect_diagnostics:
with torch.no_grad():
fa, fb, fc = x_a.float(), x_b.float(), x_c.float()
diagnostics[f"cos_a2b/round{k}"] = float(
F.cosine_similarity(self.edges_a2b[k].w(fa), fb, dim=-1).mean()
)
diagnostics[f"cos_c2b/round{k}"] = float(
F.cosine_similarity(self.edges_c2b[k].w(fc), fb, dim=-1).mean()
)
diagnostics[f"cos_b2a/round{k}"] = float(
F.cosine_similarity(self.edges_b2a[k].w(fb), fa, dim=-1).mean()
)
diagnostics[f"cos_b2c/round{k}"] = float(
F.cosine_similarity(self.edges_b2c[k].w(fb), fc, dim=-1).mean()
)
# how much of A survives the trip to C and back through B
diagnostics[f"rms_a/round{k}"] = float(fa.pow(2).mean().sqrt())
diagnostics[f"rms_b/round{k}"] = float(fb.pow(2).mean().sqrt())
diagnostics[f"rms_c/round{k}"] = float(fc.pow(2).mean().sqrt())
# 4. finish every stack
x_a, cur_a = self._advance(
self.blocks_a, x_a, cur_a, cfg.tower_a.num_hidden_layers, freqs_cis, pend_a
)
x_c, cur_c = self._advance(
self.blocks_c, x_c, cur_c, cfg.tower_c.num_hidden_layers, freqs_cis, pend_c
)
# B's tail runs only so its parameters see gradient in later rounds; with
# the default schedule B is already finished here.
x_b, cur_b = self._advance(
self.blocks_b, x_b, cur_b, cfg.tower_b.num_hidden_layers, freqs_cis, pend_b
)
if pend_a or pend_b or pend_c:
raise RuntimeError(f"bridge values were never consumed: {sorted(pend_a) + sorted(pend_b) + sorted(pend_c)}")
h_a = self.ln_f_a(x_a)
h_c = self.ln_f_c(x_c)
mix_logit = self.mix_head(torch.cat([h_a, h_c], dim=-1)).squeeze(-1)
return h_a, h_c, mix_logit, diagnostics
# ---- diagnostics ------------------------------------------------------- #
@torch.no_grad()
def gate_stats(self) -> dict[str, float]:
"""Per-round, per-direction gate magnitudes."""
cfg = self.config
stats: dict[str, float] = {}
groups = (
("a2b", self.edges_a2b, cfg.b_land),
("c2b", self.edges_c2b, cfg.b_land),
("b2a", self.edges_b2a, cfg.a_land),
("b2c", self.edges_b2c, cfg.c_land),
)
for name, edges, layers in groups:
for i, edge in enumerate(edges):
g = edge.g.detach().float()
stats[f"gate_{name}/L{layers[i]}_absmean"] = float(g.abs().mean())
stats[f"gate_{name}/L{layers[i]}_absmax"] = float(g.abs().max())
stats[f"gate_{name}/L{layers[i]}_rms"] = float(g.pow(2).mean().sqrt())
return stats
# ---- parameter accounting ---------------------------------------------- #
def param_breakdown(self) -> dict[str, int]:
def n(module: nn.Module) -> int:
return sum(p.numel() for p in module.parameters())
bridges = sum(n(e) for e in (self.edges_a2b, self.edges_c2b, self.edges_b2a, self.edges_b2c))
return {
"shared_embedding": n(self.wte),
"in_proj_384_to_256": n(self.in_proj_a),
"in_proj_384_to_320": n(self.in_proj_b),
"tower_a_layers": n(self.blocks_a),
"tower_b_layers": n(self.blocks_b),
"tower_c_layers": n(self.blocks_c),
"bridges": bridges,
"ln_f_a": n(self.ln_f_a),
"ln_f_c": n(self.ln_f_c),
"lm_head_a": n(self.lm_head_a),
"lm_head_c": n(self.lm_head_c),
"mix_head": n(self.mix_head),
"total": sum(p.numel() for p in self.parameters()),
}
def matmul_parameters(self) -> int:
"""Total excluding norms, gates and the mixer.
This is the number the architecture is specified against: everything
that is a weight matrix, and nothing that is a per-channel scalar.
"""
skip = set()
for module in self.modules():
if isinstance(module, RMSNorm):
skip.add(id(module.weight))
for edges in (self.edges_a2b, self.edges_c2b, self.edges_b2a, self.edges_b2c):
for edge in edges:
skip.add(id(edge.g))
for p in self.mix_head.parameters():
skip.add(id(p))
return sum(p.numel() for p in self.parameters() if id(p) not in skip)
# --------------------------------------------------------------------------- #
# loss
# --------------------------------------------------------------------------- #
def _loss_chunk(
h_a: torch.Tensor,
h_c: torch.Tensor,
mix_logit: torch.Tensor,
targets: torch.Tensor,
w_a: torch.Tensor,
w_c: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""One chunk of positions. Returns summed NLLs and summed squared logits."""
logits_a = F.linear(h_a, w_a).float()
logits_c = F.linear(h_c, w_c).float()
log_p_a = F.log_softmax(logits_a, dim=-1)
log_p_c = F.log_softmax(logits_c, dim=-1)
# Mixing happens in probability space, computed in log space:
# log(a*p_A + (1-a)*p_C) = logaddexp(log a + log p_A, log(1-a) + log p_C)
log_alpha = F.logsigmoid(mix_logit).unsqueeze(-1)
log_one_minus_alpha = F.logsigmoid(-mix_logit).unsqueeze(-1)
log_p_mix = torch.logaddexp(log_alpha + log_p_a, log_one_minus_alpha + log_p_c)
idx = targets.unsqueeze(-1)
nll_mix = -log_p_mix.gather(-1, idx).squeeze(-1).sum()
nll_a = -log_p_a.gather(-1, idx).squeeze(-1).sum()
nll_c = -log_p_c.gather(-1, idx).squeeze(-1).sum()
sq_logits = logits_a.pow(2).sum() + logits_c.pow(2).sum()
return nll_mix, nll_a, nll_c, sq_logits
def relay_loss(
model: BananaMindRelay,
h_a: torch.Tensor,
h_c: torch.Tensor,
mix_logit: torch.Tensor,
targets: torch.Tensor,
solo_lambda: float,
z_loss_coeff: float = 0.0,
chunk_size: int = 16384,
) -> dict[str, torch.Tensor]:
"""L = L_mix + lambda * (L_A + L_C).
There is no L_B term. B has no head to compute one with, and giving it one
would turn the relay into a third predictor instead of a channel. B trains
entirely on gradient arriving through its four bridge directions.
Positions are processed in chunks under gradient checkpointing: three
vocab-sized float tensors per position do not fit at 4096 context otherwise.
"""
w_a = model.lm_head_a.weight
w_c = model.lm_head_c.weight
vocab = w_a.size(0)
flat_a = h_a.reshape(-1, h_a.size(-1))
flat_c = h_c.reshape(-1, h_c.size(-1))
flat_z = mix_logit.reshape(-1)
flat_y = targets.reshape(-1)
n_pos = flat_y.numel()
if chunk_size <= 0 or chunk_size >= n_pos:
bounds = [(0, n_pos)]
else:
bounds = [(i, min(i + chunk_size, n_pos)) for i in range(0, n_pos, chunk_size)]
mix_sum = flat_a.new_zeros((), dtype=torch.float32)
a_sum = flat_a.new_zeros((), dtype=torch.float32)
c_sum = flat_a.new_zeros((), dtype=torch.float32)
sq_sum = flat_a.new_zeros((), dtype=torch.float32)
use_ckpt = torch.is_grad_enabled() and len(bounds) > 1
for lo, hi in bounds:
args = (flat_a[lo:hi], flat_c[lo:hi], flat_z[lo:hi], flat_y[lo:hi], w_a, w_c)
if use_ckpt:
part = torch.utils.checkpoint.checkpoint(_loss_chunk, *args, use_reentrant=False)
else:
part = _loss_chunk(*args)
mix_sum = mix_sum + part[0]
a_sum = a_sum + part[1]
c_sum = c_sum + part[2]
sq_sum = sq_sum + part[3]
l_mix = mix_sum / n_pos
l_a = a_sum / n_pos
l_c = c_sum / n_pos
z_loss = sq_sum / (2.0 * n_pos * vocab)
loss = l_mix + solo_lambda * (l_a + l_c) + z_loss_coeff * z_loss
return {"loss": loss, "l_mix": l_mix, "l_a": l_a, "l_c": l_c, "z_loss": z_loss}
def count_parameters(model: nn.Module) -> int:
return sum(p.numel() for p in model.parameters())
def estimate_training_flops_per_token(config: RelayConfig, param_count: int, seq_len: int) -> int:
dense_flops = 6 * param_count
attention_flops = 0
for spec in (config.tower_a, config.tower_b, config.tower_c):
attention_flops += (
12 * spec.num_hidden_layers * spec.num_attention_heads * config.head_dim * seq_len
)
return dense_flops + attention_flops