Text Generation
Transformers
Safetensors
PyTorch
English
bananamind21_unified
causal-lm
language-model
base-model
small-language-model
bananamind
bananamind2
bananamind21-unified
three-tower
relay
multi-tower
fineweb-hq
dclm
cosmopedia-v2
finemath
digit-tokenizer
custom-code
trust-remote-code
custom-architecture
custom_code
Instructions to use BananaMind/BananaMind-2.1-Unified with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use BananaMind/BananaMind-2.1-Unified with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="BananaMind/BananaMind-2.1-Unified", trust_remote_code=True)# Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("BananaMind/BananaMind-2.1-Unified", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use BananaMind/BananaMind-2.1-Unified with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "BananaMind/BananaMind-2.1-Unified" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "BananaMind/BananaMind-2.1-Unified", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/BananaMind/BananaMind-2.1-Unified
- SGLang
How to use BananaMind/BananaMind-2.1-Unified with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "BananaMind/BananaMind-2.1-Unified" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "BananaMind/BananaMind-2.1-Unified", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "BananaMind/BananaMind-2.1-Unified" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "BananaMind/BananaMind-2.1-Unified", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use BananaMind/BananaMind-2.1-Unified with Docker Model Runner:
docker model run hf.co/BananaMind/BananaMind-2.1-Unified
| #!/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 | |
| # --------------------------------------------------------------------------- # | |
| class TowerSpec: | |
| hidden_size: int | |
| num_hidden_layers: int | |
| num_attention_heads: int | |
| num_key_value_heads: int | |
| intermediate_size: int | |
| 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 | |
| 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 ----------------------------------------------------------- # | |
| 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 ------------------------------------------------------- # | |
| 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 | |