"""Senryu-2: a Gated DeltaNet-2 + Parallax hybrid on a 4-branch gated residual stream. Every sublayer reads its input from the residual branches through Qwen's Gated Residual (per-branch RMSNorm, elementwise sigmoid gate from a low-rank bottleneck over all branches) and writes its output back to every branch with one data-dependent scalar per branch. A hashed n-gram memory, whose tables live in host memory during training, is injected before block ``ngram_layer_index`` (docs/architecture.md, "Senryu-2"). Training and scoring run packed: the batch is flattened into one row and split into documents at every BOS token, so GDN-2, Parallax, the n-gram hashes and the n-gram convolution never look across a document boundary. """ from __future__ import annotations import math from dataclasses import dataclass, field from pathlib import Path from typing import Any import torch import torch.nn.functional as F from fla.layers.gdn2 import GatedDeltaNet2 from fla.models.utils import Cache as FlaCache from fla.modules.rotary import rotary_embedding from safetensors.torch import save_model from torch import nn from torch.utils.checkpoint import checkpoint from tiny_gdn.config import TinyGDNConfig from tiny_gdn.gated_residual_ops import fused_gated_residual_read, fused_gated_residual_write from tiny_gdn.haiku_layers import SiTUGLU from tiny_gdn.model import TinyGDNOutput, linear_cross_entropy from tiny_gdn.ngram import ( NGRAM_TABLES_EMA_FILE, NGRAM_TABLES_FILE, NGramLayout, document_distance, load_table_file, ngram_row_indices, ) from tiny_gdn.nn_common import RMSNorm from tiny_gdn.parallax_ops import parallax_attention, parallax_decode_cached GATED_RESIDUAL_CHUNK_ROWS = 4096 KV_CACHE_GRANULE = 256 def mark_muon_row_blocks(parameter: nn.Parameter, blocks: int) -> None: """Orthogonalize ``parameter`` as ``blocks`` equal row blocks (heads, fused halves).""" if parameter.shape[0] % blocks: raise ValueError(f"{blocks} row blocks do not divide {parameter.shape[0]} rows") parameter._muon_row_blocks = blocks def mark_adamw_only(*parameters: nn.Parameter) -> None: """Keep 2D weights that are gates or elongated low-rank maps on AdamW.""" for parameter in parameters: parameter._adamw_only = True def mark_gain(*parameters: nn.Parameter) -> None: """2D per-branch norm gains and filters: AdamW without weight decay, like 1D gains.""" mark_adamw_only(*parameters) for parameter in parameters: parameter._no_weight_decay = True # --------------------------------------------------------------------------- layout @dataclass class Senryu2Context: """Per-forward state shared by every block (passes through activation checkpointing).""" batch: int length: int cu_seqlens: torch.Tensor | None distance: torch.Tensor | None rotary: tuple[torch.Tensor, torch.Tensor] position_offset: int = 0 cache: Senryu2Cache | None = None class RotaryTables: """Partial-RoPE cos/sin rows ``[length, rotary_dim / 2]``, built in FP32 and cached.""" def __init__(self, rotary_dim: int, theta: float) -> None: self.rotary_dim = rotary_dim self.theta = theta self._cached: tuple[torch.Tensor, torch.Tensor] | None = None self._key: tuple[torch.device, torch.dtype, bool] | None = None def get(self, length: int, device: torch.device, dtype: torch.dtype) -> tuple[torch.Tensor, torch.Tensor]: key = (device, dtype, torch.is_inference_mode_enabled()) cached = self._cached if cached is None or self._key != key or cached[0].shape[0] < length: rows = max(length, 4096 if cached is None else 2 * cached[0].shape[0]) inverse_frequency = 1.0 / ( self.theta ** (torch.arange(0, self.rotary_dim, 2, device=device, dtype=torch.float32) / self.rotary_dim) ) angles = torch.outer(torch.arange(rows, device=device, dtype=torch.float32), inverse_frequency) cached = (angles.cos().to(dtype), angles.sin().to(dtype)) self._cached = cached self._key = key return cached def parallax_reference( q: torch.Tensor, r: torch.Tensor, k: torch.Tensor, v: torch.Tensor, *, scale: float, cu_seqlens: torch.Tensor | None = None, ) -> torch.Tensor: """FP32 Parallax for queries at the last ``Tq`` of ``Tk`` key positions. ``out = O1 / d1 * (1 + d2 / d1) - O2 / d1`` with ``p1 = exp(scale q·k)``, ``p2 = p1 * (r·k)``; ``cu_seqlens`` (batch 1, ``Tq == Tk``) masks across packed documents. """ batch, queries, query_heads, head_dim = q.shape keys, kv_heads = k.shape[1], k.shape[2] groups = query_heads // kv_heads qf = q.float().reshape(batch, queries, kv_heads, groups, head_dim) rf = r.float().reshape(batch, queries, kv_heads, groups, head_dim) kf, vf = k.float(), v.float() s1 = torch.einsum("bqhgd,bkhd->bhgqk", qf, kf) * scale s2 = torch.einsum("bqhgd,bkhd->bhgqk", rf, kf) query_positions = torch.arange(keys - queries, keys, device=q.device)[:, None] key_positions = torch.arange(keys, device=q.device)[None, :] masked = key_positions > query_positions if cu_seqlens is not None: segment = torch.zeros(keys, dtype=torch.long, device=q.device) segment[cu_seqlens[1:-1].long()] = 1 segment = segment.cumsum(0) masked = masked | (segment[keys - queries :, None] != segment[None, :]) s1 = s1.masked_fill(masked, float("-inf")) p1 = (s1 - s1.amax(dim=-1, keepdim=True)).exp() d1 = p1.sum(dim=-1) p2 = p1 * s2 d2 = p2.sum(dim=-1) o1 = torch.einsum("bhgqk,bkhd->bqhgd", p1, vf) o2 = torch.einsum("bhgqk,bkhd->bqhgd", p2, vf) inverse_d1 = (1.0 / d1).permute(0, 3, 1, 2)[..., None] correction = (d2 / d1).permute(0, 3, 1, 2)[..., None] out = o1 * inverse_d1 * (1.0 + correction) - o2 * inverse_d1 return out.reshape(batch, queries, query_heads, head_dim).to(q.dtype) # ------------------------------------------------------------------ gated residual class _GatedResidualRead(torch.autograd.Function): """``x = (1/n) Σ σ(W_u SiLU(W_d vec(R̂) / n))_i ⊙ R̂_i`` and ``s = 2σ(W_w vec(R̂) / n)``. Saves only the branches, the per-branch inverse RMS and the bottleneck pre-activation; the normalized branches and gates are recomputed in chunks during backward, so a 32k-token read keeps ~0.4 GB instead of ~4 GB. CUDA runs the fused kernels in ``gated_residual_ops`` instead. """ @staticmethod def forward(ctx, residual, gamma, down, up, write, eps): rows, branches, hidden_size = residual.shape dtype = down.dtype gain = 1.0 + gamma.float() out = residual.new_empty(rows, hidden_size) rstd = residual.new_empty(rows, branches, dtype=torch.float32) hidden = residual.new_empty(rows, down.shape[0]) scales = residual.new_empty(rows, 0 if write is None else branches, dtype=torch.float32) for start in range(0, rows, GATED_RESIDUAL_CHUNK_ROWS): stop = min(start + GATED_RESIDUAL_CHUNK_ROWS, rows) values = residual[start:stop].float() chunk_rstd = torch.rsqrt(values.square().mean(dim=-1, keepdim=True) + eps) hat = values * chunk_rstd * gain flat = hat.to(dtype).flatten(1) chunk_hidden = (flat @ down.t()) / branches activated = F.silu(chunk_hidden.float()).to(dtype) gates = torch.sigmoid((activated @ up.t()).float()).view(stop - start, branches, hidden_size) out[start:stop] = ((gates * hat).sum(dim=1) / branches).to(out.dtype) rstd[start:stop] = chunk_rstd.squeeze(-1) hidden[start:stop] = chunk_hidden if write is not None: scales[start:stop] = 2.0 * torch.sigmoid((flat @ write.t()).float() / branches) ctx.save_for_backward(residual, gamma, down, up, write, rstd, hidden, scales) scales_out = scales.to(residual.dtype, copy=True) if write is None: ctx.mark_non_differentiable(scales_out) return out, scales_out @staticmethod def backward(ctx, grad_out, grad_scales): residual, gamma, down, up, write, rstd, hidden, scales = ctx.saved_tensors rows, branches, hidden_size = residual.shape dtype = down.dtype gain = 1.0 + gamma.float() grad_residual = torch.empty(residual.shape, dtype=residual.dtype, device=residual.device) grad_gamma = torch.zeros(branches, hidden_size, dtype=torch.float32, device=residual.device) grad_down = torch.zeros(down.shape, dtype=torch.float32, device=down.device) grad_up = torch.zeros(up.shape, dtype=torch.float32, device=up.device) grad_write = None if write is None else torch.zeros(write.shape, dtype=torch.float32, device=write.device) use_scales = write is not None and grad_scales is not None for start in range(0, rows, GATED_RESIDUAL_CHUNK_ROWS): stop = min(start + GATED_RESIDUAL_CHUNK_ROWS, rows) count = stop - start normalized = residual[start:stop].float() * rstd[start:stop, :, None] hat = normalized * gain flat = hat.to(dtype).flatten(1) pre = hidden[start:stop].float() sigmoid_pre = torch.sigmoid(pre) activated = (pre * sigmoid_pre).to(dtype) gates = torch.sigmoid((activated @ up.t()).float()).view(count, branches, hidden_size) grad_x = grad_out[start:stop].float().unsqueeze(1) / branches grad_hat = gates * grad_x grad_gate_logits = (grad_x * hat * gates * (1.0 - gates)).flatten(1).to(dtype) grad_up += (grad_gate_logits.t() @ activated).float() grad_activated = (grad_gate_logits @ up).float() grad_pre = grad_activated * sigmoid_pre * (1.0 + pre * (1.0 - sigmoid_pre)) grad_pre = (grad_pre / branches).to(dtype) grad_down += (grad_pre.t() @ flat).float() grad_flat = (grad_pre @ down).float() if use_scales: scale_values = scales[start:stop] grad_scale_logits = grad_scales[start:stop].float() * scale_values * (1.0 - scale_values / 2.0) grad_scale_logits = (grad_scale_logits / branches).to(dtype) grad_write += (grad_scale_logits.t() @ flat).float() grad_flat += (grad_scale_logits @ write).float() grad_hat = grad_hat + grad_flat.view(count, branches, hidden_size) grad_gamma += (grad_hat * normalized).sum(dim=0) grad_normalized = grad_hat * gain grad_residual[start:stop] = ( rstd[start:stop, :, None] * (grad_normalized - normalized * (grad_normalized * normalized).mean(dim=-1, keepdim=True)) ).to(grad_residual.dtype) return ( grad_residual, grad_gamma.to(gamma.dtype), grad_down.to(down.dtype), grad_up.to(up.dtype), None if grad_write is None else grad_write.to(write.dtype), None, ) def gated_residual_read_reference( residual: torch.Tensor, gamma: torch.Tensor, down: torch.Tensor, up: torch.Tensor, write: torch.Tensor | None, eps: float, ) -> tuple[torch.Tensor, torch.Tensor]: """Eager autograd version of :class:`_GatedResidualRead`, for tests.""" rows, branches, hidden_size = residual.shape gain = 1.0 + gamma.float() values = residual.float() hat = values * torch.rsqrt(values.square().mean(dim=-1, keepdim=True) + eps) * gain flat = hat.to(down.dtype).flatten(1) hidden = (flat @ down.t()) / branches gates = torch.sigmoid((F.silu(hidden.float()).to(up.dtype) @ up.t()).float()).view(rows, branches, hidden_size) out = ((gates * hat).sum(dim=1) / branches).to(residual.dtype) if write is None: return out, residual.new_empty(rows, 0) scales = 2.0 * torch.sigmoid((flat @ write.t()).float() / branches) return out, scales.to(residual.dtype) class GatedResidual(nn.Module): """Qwen's Gated Residual read (and write scales) over ``branches`` residual branches.""" def __init__(self, branches: int, hidden_size: int, rank: int, eps: float, *, write: bool) -> None: super().__init__() self.branches = branches self.eps = eps self.weight = nn.Parameter(torch.zeros(branches, hidden_size)) self.down_proj = nn.Linear(branches * hidden_size, rank, bias=False) self.up_proj = nn.Linear(rank, branches * hidden_size, bias=False) self.write_proj = nn.Linear(branches * hidden_size, branches, bias=False) if write else None mark_gain(self.weight) mark_adamw_only(self.down_proj.weight, self.up_proj.weight) if self.write_proj is not None: mark_adamw_only(self.write_proj.weight) def read(self, residual: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """``(x [N, d], scales [N, n])`` from branches ``[N, n, d]``.""" write = None if self.write_proj is None else self.write_proj.weight read = fused_gated_residual_read if residual.is_cuda else _GatedResidualRead.apply return read(residual, self.weight, self.down_proj.weight, self.up_proj.weight, write, self.eps) @staticmethod def write(residual: torch.Tensor, scales: torch.Tensor, update: torch.Tensor) -> torch.Tensor: if residual.is_cuda: return fused_gated_residual_write(residual, scales, update) return residual + scales.unsqueeze(-1) * update.unsqueeze(-2) # ------------------------------------------------------------------- token mixers class ParallaxAttention(nn.Module): """Parallax attention (softmax plus a learned first-order correction from ``r``). Grouped query/key/value heads with per-head zero-centered RMSNorm on q, r and k, partial RoPE on q, r and k, and a full-rank sigmoid output gate. """ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None: super().__init__() self.layer_index = layer_index self.num_heads = config.num_attention_heads self.num_kv_heads = config.num_key_value_heads self.head_dim = config.attention_head_dim self.scale = self.head_dim ** -0.5 query_width = self.num_heads * self.head_dim kv_width = self.num_kv_heads * self.head_dim hidden = config.hidden_size self.q_proj = nn.Linear(hidden, query_width, bias=False) self.r_proj = nn.Linear(hidden, query_width, bias=False) self.k_proj = nn.Linear(hidden, kv_width, bias=False) self.v_proj = nn.Linear(hidden, kv_width, bias=False) self.g_proj = nn.Linear(hidden, query_width, bias=True) self.o_proj = nn.Linear(query_width, hidden, bias=False) self.q_norm = RMSNorm(self.head_dim, config.rms_norm_eps) self.r_norm = RMSNorm(self.head_dim, config.rms_norm_eps) self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps) for projection in (self.q_proj, self.r_proj): mark_muon_row_blocks(projection.weight, self.num_heads) for projection in (self.k_proj, self.v_proj): mark_muon_row_blocks(projection.weight, self.num_kv_heads) mark_adamw_only(self.g_proj.weight) def forward(self, hidden_states: torch.Tensor, context: Senryu2Context) -> torch.Tensor: batch, length, _ = hidden_states.shape q = self.q_norm(self.q_proj(hidden_states).view(batch, length, self.num_heads, self.head_dim)) r = self.r_norm(self.r_proj(hidden_states).view(batch, length, self.num_heads, self.head_dim)) k = self.k_norm(self.k_proj(hidden_states).view(batch, length, self.num_kv_heads, self.head_dim)) v = self.v_proj(hidden_states).view(batch, length, self.num_kv_heads, self.head_dim) cosine, sine = context.rotary q, r, k = ( rotary_embedding( tensor, cosine, sine, seqlen_offsets=context.position_offset, cu_seqlens=context.cu_seqlens, ) for tensor in (q, r, k) ) use_kernel = q.is_cuda and q.dtype in (torch.bfloat16, torch.float16) cache = context.cache if cache is not None: keys, values = cache.append_attention(self.layer_index, k, v, context.position_offset) if not use_kernel: out = parallax_reference(q, r, keys, values, scale=self.scale) elif keys.shape[1] == length: out = parallax_attention(q, r, keys, values, scale=self.scale) else: out = parallax_decode_cached(q, r, keys, values, scale=self.scale) elif use_kernel: out = parallax_attention(q, r, k, v, scale=self.scale, cu_seqlens=context.cu_seqlens) else: out = parallax_reference(q, r, k, v, scale=self.scale, cu_seqlens=context.cu_seqlens) gate = torch.sigmoid(self.g_proj(hidden_states)).view(batch, length, self.num_heads, self.head_dim) return self.o_proj((out * gate).reshape(batch, length, self.num_heads * self.head_dim)) def build_gated_deltanet2(config: TinyGDNConfig, layer_index: int) -> GatedDeltaNet2: mixer = GatedDeltaNet2( hidden_size=config.hidden_size, expand_v=config.linear_expand_v, head_dim=config.linear_head_dim, num_heads=config.linear_num_heads, num_v_heads=config.linear_num_value_heads, mode="chunk", use_short_conv=True, allow_neg_eigval=config.allow_negative_eigenvalues, conv_size=config.linear_conv_kernel_dim, conv_bias=False, layer_idx=layer_index, norm_eps=config.rms_norm_eps, ) if config.linear_full_rank_gate: # The layer calls `self.g_proj(hidden_states)`, so a single full-rank # map drops in for fla's low-rank pair. mixer.g_proj = nn.Linear(config.hidden_size, mixer.value_dim, bias=True) mark_adamw_only(mixer.g_proj.weight) else: mark_adamw_only(*(layer.weight for layer in mixer.g_proj)) mark_adamw_only(*(layer.weight for layer in mixer.f_proj)) for projection in (mixer.q_proj, mixer.k_proj, mixer.b_proj): mark_muon_row_blocks(projection.weight, mixer.num_heads) for projection in (mixer.v_proj, mixer.w_proj): mark_muon_row_blocks(projection.weight, mixer.num_v_heads) return mixer # ------------------------------------------------------------------- n-gram memory class BranchRMSNorm(nn.Module): """Zero-centered RMSNorm over the last dim with one gain vector per branch.""" def __init__(self, branches: int, hidden_size: int, eps: float) -> None: super().__init__() self.weight = nn.Parameter(torch.zeros(branches, hidden_size)) self.eps = eps mark_gain(self.weight) def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: values = hidden_states.float() values = values * torch.rsqrt(values.square().mean(dim=-1, keepdim=True) + self.eps) return values * (1.0 + self.weight.float()) class NGramMemory(nn.Module): """Engram-style memory: hashed n-gram rows, gated per residual branch, then a short conv. ``e`` concatenates one row per table. Each branch gates the shared value ``W_V e`` with ``σ(RMSNorm(R_m) · RMSNorm(W_K,m e) / √d)``; the gated values pass a depthwise causal convolution (dilation = largest order, zero init) and are added to their branches. """ def __init__(self, config: TinyGDNConfig) -> None: super().__init__() self.layout = NGramLayout.from_config(config) self.branches = config.residual_branches self.hidden_size = config.hidden_size self.kernel = config.ngram_conv_kernel self.dilation = self.layout.max_order memory_dim = self.layout.memory_dim self.value_proj = nn.Linear(memory_dim, config.hidden_size, bias=False) self.key_proj = nn.Linear(memory_dim, self.branches * config.hidden_size, bias=False) self.hidden_norm = BranchRMSNorm(self.branches, config.hidden_size, config.rms_norm_eps) self.key_norm = BranchRMSNorm(self.branches, config.hidden_size, config.rms_norm_eps) self.conv_norm = RMSNorm(config.hidden_size, config.rms_norm_eps) self.conv_weight = nn.Parameter(torch.zeros(config.hidden_size, self.kernel)) mark_muon_row_blocks(self.key_proj.weight, self.branches) mark_gain(self.conv_weight) self.register_buffer("canonical_ids", torch.arange(config.vocab_size, dtype=torch.int64), persistent=False) self.register_buffer("tables", None, persistent=False) @property def conv_history(self) -> int: return (self.kernel - 1) * self.dilation def set_canonical_ids(self, canonical: torch.Tensor) -> None: if canonical.shape != self.canonical_ids.shape: raise ValueError("canonical map does not match the vocabulary") self.canonical_ids = canonical.to(device=self.canonical_ids.device, dtype=torch.int64) def attach_tables(self, tables: torch.Tensor, canonical: torch.Tensor) -> None: """Look rows up in ``tables [total_rows, embedding_dim]``. The tables may stay in host memory while the model runs on the GPU: decoding reads only ``table_count`` rows per token. """ if tables.shape != (self.layout.total_rows, self.layout.embedding_dim): raise ValueError(f"tables have shape {tuple(tables.shape)}") self.set_canonical_ids(canonical) self.tables = tables def load_tables(self, path: Path, *, device: torch.device | str, dtype: torch.dtype) -> None: tables, canonical = load_table_file(path, self.layout, device=device, dtype=dtype) self.attach_tables(tables, canonical) def lookup( self, input_ids: torch.Tensor, *, distance: torch.Tensor | None = None, history: torch.Tensor | None = None, ) -> torch.Tensor: """Concatenated rows ``[..., memory_dim]`` for every position of ``input_ids``.""" if self.tables is None: raise RuntimeError( "Senryu-2 needs n-gram embeddings: pass ngram_embeddings or attach tables " "with model.ngram.load_tables(...)" ) canonical = self.canonical_ids[input_ids] indices = ngram_row_indices(canonical, self.layout, distance=distance, history=history) tables = self.tables if tables.device == indices.device: return F.embedding(indices, tables).flatten(-2) rows = F.embedding(indices.to(tables.device), tables).flatten(-2) return rows.to(indices.device, non_blocking=True) def _convolve( self, values: torch.Tensor, distance: torch.Tensor | None, history: torch.Tensor | None, ) -> tuple[torch.Tensor, torch.Tensor]: """Causal dilated depthwise conv over ``[B, T, n, d]``; returns ``(out, new_history)``.""" batch, length = values.shape[:2] span = self.conv_history if history is None: history = values.new_zeros(batch, span, self.branches, self.hidden_size) extended = torch.cat([history, values], dim=1) weight = self.conv_weight.to(values.dtype) out = values * weight[:, -1] for tap in range(1, self.kernel): shift = tap * self.dilation shifted = extended[:, span - shift : span - shift + length] if distance is not None: shifted = shifted * (distance >= shift).to(values.dtype)[..., None, None] out = out + shifted * weight[:, -1 - tap] return out, extended[:, -span:] def forward( self, residual: torch.Tensor, embeddings: torch.Tensor, *, batch: int, distance: torch.Tensor | None, conv_state: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Branch updates ``[N, n, d]`` for branches ``[N, n, d]`` and rows ``[N, memory_dim]``.""" rows = residual.shape[0] value = self.value_proj(embeddings) keys = self.key_proj(embeddings).view(rows, self.branches, self.hidden_size) similarity = (self.hidden_norm(residual) * self.key_norm(keys)).sum(dim=-1) alpha = torch.sigmoid(similarity / math.sqrt(self.hidden_size)).to(value.dtype) gated = alpha.unsqueeze(-1) * value.unsqueeze(1) normalized = self.conv_norm(gated).view(batch, rows // batch, self.branches, self.hidden_size) convolved, new_state = self._convolve( normalized, None if distance is None else distance.view(batch, rows // batch), conv_state, ) return F.silu(convolved).view(rows, self.branches, self.hidden_size) + gated, new_state # ------------------------------------------------------------------------- blocks class Senryu2Block(nn.Module): def __init__(self, config: TinyGDNConfig, layer_index: int) -> None: super().__init__() self.layer_index = layer_index self.layer_type = config.layer_types[layer_index] residual = (config.residual_branches, config.hidden_size, config.residual_gate_rank, config.rms_norm_eps) self.mixer_residual = GatedResidual(*residual, write=True) self.mlp_residual = GatedResidual(*residual, write=True) if self.layer_type == "gdn2": self.token_mixer: nn.Module = build_gated_deltanet2(config, layer_index) elif self.layer_type == "parallax": self.token_mixer = ParallaxAttention(config, layer_index) else: raise ValueError(f"Senryu-2 has no {self.layer_type} layers") self.mlp = SiTUGLU(config) mark_muon_row_blocks(self.mlp.gate_up_proj.weight, 2) def _mix(self, hidden_states: torch.Tensor, context: Senryu2Context) -> torch.Tensor: batched = hidden_states.view(context.batch, context.length, -1) if self.layer_type == "parallax": mixed = self.token_mixer(batched, context) else: cache = context.cache mixed, _, _ = self.token_mixer( batched, past_key_values=None if cache is None else cache.linear, use_cache=cache is not None, cu_seqlens=context.cu_seqlens, ) return mixed.reshape(hidden_states.shape) def forward(self, residual: torch.Tensor, context: Senryu2Context) -> torch.Tensor: hidden_states, scales = self.mixer_residual.read(residual) residual = GatedResidual.write(residual, scales, self._mix(hidden_states, context)) hidden_states, scales = self.mlp_residual.read(residual) return GatedResidual.write(residual, scales, self.mlp(hidden_states)) # -------------------------------------------------------------------------- cache @dataclass class Senryu2Cache: """Decode state: GDN-2 states, Parallax keys/values, and the n-gram tails.""" linear: Any = field(default_factory=FlaCache) keys: dict[int, torch.Tensor] = field(default_factory=dict) values: dict[int, torch.Tensor] = field(default_factory=dict) length: int = 0 ngram_tokens: torch.Tensor | None = None ngram_conv: torch.Tensor | None = None def append_attention( self, layer_index: int, keys: torch.Tensor, values: torch.Tensor, start: int, ) -> tuple[torch.Tensor, torch.Tensor]: """Write ``[B, T, H, D]`` keys/values at ``start``; return views of the whole prefix.""" stop = start + keys.shape[1] stored_keys = self.keys.get(layer_index) if stored_keys is None or stored_keys.shape[1] < stop: capacity = max(stop, 0 if stored_keys is None else 2 * stored_keys.shape[1]) capacity = -(-capacity // KV_CACHE_GRANULE) * KV_CACHE_GRANULE grown_keys = keys.new_empty(keys.shape[0], capacity, *keys.shape[2:]) grown_values = values.new_empty(values.shape[0], capacity, *values.shape[2:]) if stored_keys is not None: grown_keys[:, :start] = stored_keys[:, :start] grown_values[:, :start] = self.values[layer_index][:, :start] self.keys[layer_index] = stored_keys = grown_keys self.values[layer_index] = grown_values stored_values = self.values[layer_index] stored_keys[:, start:stop] = keys stored_values[:, start:stop] = values return stored_keys[:, :stop], stored_values[:, :stop] # -------------------------------------------------------------------------- model @dataclass class _Packing: """How a ``[B, T]`` batch maps onto one packed row of ``valid`` tokens.""" batch: int length: int index: torch.Tensor | None row_starts: torch.Tensor def pack(self, tensor: torch.Tensor) -> torch.Tensor: flat = tensor.reshape(self.batch * self.length, *tensor.shape[2:]) return flat if self.index is None else flat.index_select(0, self.index) def unpack(self, tensor: torch.Tensor) -> torch.Tensor: if self.index is None: return tensor.view(self.batch, self.length, *tensor.shape[1:]) out = tensor.new_zeros(self.batch * self.length, *tensor.shape[1:]) out.index_copy_(0, self.index, tensor) return out.view(self.batch, self.length, *tensor.shape[1:]) class Senryu2ForCausalLM(nn.Module): def __init__(self, config: TinyGDNConfig) -> None: super().__init__() if config.residual_mode != "gated_residual": raise ValueError("Senryu2ForCausalLM needs residual_mode 'gated_residual'") self.config = config self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size) self.layers = nn.ModuleList(Senryu2Block(config, index) for index in range(config.num_hidden_layers)) self.final_residual = GatedResidual( config.residual_branches, config.hidden_size, config.residual_gate_rank, config.rms_norm_eps, write=False, ) self.ngram = NGramMemory(config) if config.ngram_orders else None self.rotary = RotaryTables(config.rotary_dim, config.rope_theta) self.checkpointed_layers: frozenset[int] = frozenset() self.apply(self._initialize_module) self._initialize_residual_projections() # ---------------------------------------------------------------- setup def _initialize_module(self, module: nn.Module) -> None: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range) if module.bias is not None: nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range) def _initialize_residual_projections(self) -> None: residual_std = self.config.initializer_range / math.sqrt(2 * self.config.num_hidden_layers) for layer in self.layers: nn.init.normal_(layer.token_mixer.o_proj.weight, mean=0.0, std=residual_std) nn.init.normal_(layer.mlp.down_proj.weight, mean=0.0, std=residual_std) def enable_gradient_checkpointing(self, enabled: bool = True, *, layers: int | None = None) -> None: """Recompute block activations in backward; ``layers`` picks GDN-2 blocks first.""" if not enabled: if layers is not None: raise ValueError("layers needs gradient checkpointing enabled") self.checkpointed_layers = frozenset() return count = len(self.layers) if layers is None else layers if not 1 <= count <= len(self.layers): raise ValueError(f"layers must be between 1 and {len(self.layers)}") cheapest_replay_first = sorted( range(len(self.layers)), key=lambda index: self.layers[index].layer_type != "gdn2", ) self.checkpointed_layers = frozenset(cheapest_replay_first[:count]) def describe_gradient_checkpointing(self) -> str: if not self.checkpointed_layers: return "off" layer_types = sorted({self.layers[index].layer_type for index in self.checkpointed_layers}) return f"{len(self.checkpointed_layers)}/{len(self.layers)} blocks ({', '.join(layer_types)})" def project_to_vocabulary(self, hidden_states: torch.Tensor) -> torch.Tensor: return F.linear(hidden_states, self.embed_tokens.weight) # -------------------------------------------------------------- packing def _packing(self, input_ids: torch.Tensor, attention_mask: torch.Tensor | None) -> _Packing: batch, length = input_ids.shape device = input_ids.device if attention_mask is None or bool(attention_mask.all()): row_starts = torch.arange(0, batch * length, length, device=device) return _Packing(batch, length, None, row_starts) if attention_mask.shape != input_ids.shape: raise ValueError("attention_mask must match input_ids") valid = attention_mask.reshape(-1).bool() index = valid.nonzero().flatten() lengths = attention_mask.bool().sum(dim=1) if bool((lengths == 0).any()): raise ValueError("every row needs at least one unmasked token") row_starts = torch.cumsum(lengths, dim=0) - lengths return _Packing(batch, length, index, row_starts) def packed_layout( self, tokens: torch.Tensor, row_starts: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """``(cu_seqlens int32, distance int64)`` for a flat token stream split at BOS.""" distance = document_distance(tokens, self.config.bos_token_id, row_starts) starts = (distance == 0).nonzero().flatten() cu_seqlens = torch.cat([starts, starts.new_tensor([tokens.numel()])]).to(torch.int32) return cu_seqlens, distance # -------------------------------------------------------------- forward def _run_block(self, index: int, residual: torch.Tensor, context: Senryu2Context) -> torch.Tensor: layer = self.layers[index] if self.training and index in self.checkpointed_layers: return checkpoint(layer, residual, context, use_reentrant=False) return layer(residual, context) def _inject_ngrams( self, residual: torch.Tensor, embeddings: torch.Tensor, context: Senryu2Context, ) -> torch.Tensor: assert self.ngram is not None cache = context.cache conv_state = None if cache is None else cache.ngram_conv distance = context.distance if cache is None else None if self.training and self.checkpointed_layers: update, new_state = checkpoint( self.ngram, residual, embeddings, batch=context.batch, distance=distance, conv_state=conv_state, use_reentrant=False, ) else: update, new_state = self.ngram( residual, embeddings, batch=context.batch, distance=distance, conv_state=conv_state, ) if cache is not None: cache.ngram_conv = new_state return residual + update def _ngram_rows( self, input_ids: torch.Tensor, packing: _Packing | None, distance: torch.Tensor | None, ngram_embeddings: torch.Tensor | None, cache: Senryu2Cache | None, ) -> torch.Tensor: assert self.ngram is not None layout = self.ngram.layout if cache is not None: history = cache.ngram_tokens if history is None: history = input_ids.new_full((input_ids.shape[0], layout.max_order - 1), layout.pad_token_id) canonical = self.ngram.canonical_ids[input_ids] cache.ngram_tokens = torch.cat([history, canonical], dim=1)[:, -(layout.max_order - 1) :] if ngram_embeddings is not None: return ngram_embeddings.reshape(-1, layout.memory_dim) return self.ngram.lookup(input_ids, history=history).reshape(-1, layout.memory_dim) assert packing is not None if ngram_embeddings is not None: if ngram_embeddings.shape != (*input_ids.shape, layout.memory_dim): raise ValueError( f"ngram_embeddings must have shape {(*input_ids.shape, layout.memory_dim)}" ) return packing.pack(ngram_embeddings) return self.ngram.lookup(packing.pack(input_ids), distance=distance) def forward( self, input_ids: torch.Tensor, labels: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, past_key_values: Senryu2Cache | None = None, *, use_cache: bool = False, return_logits: bool = True, return_hidden_states: bool = False, labels_are_shifted: bool = False, include_mtp_loss: bool = True, mtp_loss_weight: float | None = None, z_loss_coefficient: float = 0.0, logits_to_keep: int | None = None, ngram_embeddings: torch.Tensor | None = None, ) -> TinyGDNOutput: del include_mtp_loss, mtp_loss_weight if input_ids.ndim != 2: raise ValueError("input_ids must have shape [batch, sequence]") if labels is not None and labels.shape != input_ids.shape: raise ValueError("labels must have the same shape as input_ids") if z_loss_coefficient < 0.0: raise ValueError("z_loss_coefficient cannot be negative") if logits_to_keep is not None and logits_to_keep <= 0: raise ValueError("logits_to_keep must be positive") if use_cache and labels is not None: raise ValueError("use_cache is not supported with labels") if past_key_values is not None and not isinstance(past_key_values, Senryu2Cache): raise TypeError("past_key_values must be a Senryu2Cache or None") cached = use_cache or past_key_values is not None batch, length = input_ids.shape offset = 0 if past_key_values is None else past_key_values.length if offset + length > self.config.max_position_embeddings: raise ValueError("Input exceeds max_position_embeddings") embeddings = self.embed_tokens(input_ids) device, dtype = embeddings.device, embeddings.dtype packing: _Packing | None = None distance: torch.Tensor | None = None if cached: if attention_mask is not None and not bool(attention_mask.all()): raise ValueError("Senryu-2 decodes unpadded rows; drop the padding instead of masking it") cache = past_key_values if past_key_values is not None else Senryu2Cache() context = Senryu2Context( batch=batch, length=length, cu_seqlens=None, distance=None, rotary=self.rotary.get(offset + length, device, dtype), position_offset=offset, cache=cache, ) hidden = embeddings.reshape(batch * length, -1) else: cache = None packing = self._packing(input_ids, attention_mask) tokens = packing.pack(input_ids) cu_seqlens, distance = self.packed_layout(tokens, packing.row_starts) context = Senryu2Context( batch=1, length=tokens.numel(), cu_seqlens=cu_seqlens, distance=distance, rotary=self.rotary.get(tokens.numel(), device, dtype), ) hidden = packing.pack(embeddings) residual = hidden.unsqueeze(1).expand(-1, self.config.residual_branches, -1) for index in range(len(self.layers)): if self.ngram is not None and index == self.config.ngram_layer_index: rows = self._ngram_rows(input_ids, packing, distance, ngram_embeddings, cache) residual = self._inject_ngrams(residual, rows.to(dtype), context) residual = self._run_block(index, residual, context) hidden_states, _ = self.final_residual.read(residual) if cache is not None: cache.length = offset + length main_loss = None z_loss = None total_loss = None if labels is not None: assert packing is not None if labels_are_shifted: targets = labels else: targets = torch.cat([labels[:, 1:], labels.new_full((batch, 1), -100)], dim=1) total_loss, main_loss, z_loss = linear_cross_entropy( hidden_states, self.embed_tokens.weight, packing.pack(targets).reshape(-1), z_loss_coefficient=z_loss_coefficient, ) if z_loss_coefficient == 0.0: z_loss = None batched_states = ( hidden_states.view(batch, length, -1) if packing is None else packing.unpack(hidden_states) ) output_states = batched_states if logits_to_keep is None else batched_states[:, -logits_to_keep:] logits = self.project_to_vocabulary(output_states) if return_logits else None return TinyGDNOutput( loss=total_loss, logits=logits, main_loss=main_loss, mtp_loss=None, z_loss=z_loss, hidden_states=batched_states if return_hidden_states else None, past_key_values=cache if use_cache else None, ) # ------------------------------------------------------------ reporting def parameter_report(self) -> dict[str, int]: total = sum(parameter.numel() for parameter in self.parameters()) embeddings = self.embed_tokens.weight.numel() tables = 0 if self.ngram is None else self.ngram.layout.total_rows * self.ngram.layout.embedding_dim return { "deployable_core": total, "training_total": total, "embedding": embeddings, "mtp_auxiliary": 0, "non_embedding_core": total - embeddings, "ngram_table": tables, } def save_checkpoint(self, output_dir: Path) -> None: output_dir.mkdir(parents=True, exist_ok=True) self.config.save_json(output_dir / "config.json") save_model(self, output_dir / "model.safetensors") def load_ngram_tables( self, checkpoint_dir: Path, *, weights: str, device: str | torch.device, dtype: torch.dtype, ) -> None: """Attach the checkpoint's tables that match ``weights`` ("normal" or "ema").""" if self.ngram is None: return table_file = checkpoint_dir / (NGRAM_TABLES_EMA_FILE if weights == "ema" else NGRAM_TABLES_FILE) if not table_file.is_file(): raise FileNotFoundError(f"Senryu-2 checkpoint has no n-gram tables: {table_file}") self.ngram.load_tables(table_file, device=device, dtype=dtype) def extra_repr(self) -> str: report = self.parameter_report() return f"core_parameters={report['deployable_core']:,}, ngram_table_parameters={report['ngram_table']:,}" def get_architecture_metadata(self) -> dict[str, Any]: return { "architecture": self.config.architecture, "layer_types": list(self.config.layer_types), "effective_num_layers": self.config.effective_num_layers, "shared_layer_indices": [], "parameter_report": self.parameter_report(), "features": [ "3:1 Gated DeltaNet-2 + Parallax attention hybrid", "4-branch Gated Residual stream (gated read, per-branch write scales)", "hashed n-gram memory (orders 2-3) with per-branch context gating", "full-rank sigmoid output gates", "partial RoPE, per-head QK/R normalization", "SiTU-GLU", "document-masked packed training", "tied input-output embeddings", ], }