Source code for mojolearn._samba_impl

# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Andrew Hendel. Part of mojolearn, https://doi.org/10.5281/zenodo.22068632
"""A Samba-shaped stack on the GPU: embedding -> [Mamba3Block |
TransformerBlock]* (each block carries its own pre-norm and residual) ->
final RMSNorm -> tied or untied LM head -> cross-entropy, with a full
backward, AdamW with clipping, a learning-rate schedule, clause 9.2 gradient
accumulation, a position-keyed RNG and streamed checksummed checkpoints.

PRIVATE MODULE, AND IT HOLDS NO NUMERICS. Every arithmetic step is a call
into `_mojolearn_training` (embedding, RMSNorm, head GEMM, accumulate, RNG,
loss, optimizer), `_mojolearn_mamba` (Mamba3Block forward/backward) or
`_mojolearn_transformer` (TransformerBlock forward and IDENTICAL zero-state
prefill backward). Attention layers use full causal attention, and each
backward recomputes its saved layer input from a fresh cache. This file
decides the ORDER of the tensor registry (part of the clipped answer), the
order of the blocks, and what goes in a checkpoint.

The tensor registry, in order, is the clip's cross-tensor summation order:

    embed.weight
    layers.{i}.<block tensor names, in the block class's _W_NAMES order>
    norm_f.weight
    lm_head.weight            only when tie_embeddings is False
"""

from . import _buffer as _buffers, _bufcheck as _checks
from ._array import Array as _Array
import hashlib
import json
from pathlib import Path

from . import _portable_math as math
from ._training_impl import _round_f32

from . import _backend
from . import _numeric_profile
from . import _ragged
from . import _training_impl as T
from ._mamba_impl import Mamba3Block
from ._transformer_impl import TransformerBlock

__all__ = ["SambaConfig", "SambaStack", "SambaState"]

PROFILE = "mojolearn.samba-stack.fp32.v1"
_STATE_SCHEMA = "mojolearn.samba-stack-state.v1"
_CHECKPOINT_SCHEMA = "mojolearn.samba-stack-json-checkpoint.v1"
_CHECKPOINT_LIMIT = 256 * 1024 * 1024

_M3_HEADDIM = 64
_M3_EXPAND = 2
_M3_D_STATE = 128
_M3_NGROUPS = 1
_M3_NUM_ROPE_ANGLES = 32

_LAYER_KINDS = ("mamba3", "attention")


def _canonical(value):
    return json.dumps(value, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=True, allow_nan=False).encode("ascii")


[docs] class SambaConfig(object): """Shape of a `SambaStack`: vocabulary, width and the sequence of layer kinds. Parameters ---------- vocab : int Vocabulary size, >= 2. d_model : int Model dimension, >= 1. Must be a multiple of 32 when any layer is 'mamba3', and equal ``n_heads * head_dim`` when any layer is 'attention'. layers : sequence of {'mamba3', 'attention'} Layer kinds in stack order; at least one. n_heads : int or None, default None Attention heads. Required when any layer is 'attention'. n_kv_heads : int or None, default None Key/value heads for grouped-query attention. None uses `n_heads`. head_dim : int or None, default None Attention head dimension. None uses ``d_model // n_heads``. intermediate : int or None, default None Hidden size of the attention layers' gated MLP. Required when any layer is 'attention'. tie_embeddings : bool, default True Share the embedding matrix with the output head. norm_eps : float, default 1e-5 Epsilon of the final RMSNorm (>= 0). Each block keeps its own constants. dropout : float, default 0.0 Dropout rate after the embedding during `SambaStack.train_step`, in [0, 1). Invalid values raise `ValueError`. Examples -------- >>> import mojolearn >>> cfg = mojolearn.SambaConfig(vocab=256, d_model=64, layers=("mamba3", "attention"), ... n_heads=4, intermediate=128) """
[docs] def __init__(self, vocab, d_model, layers, n_heads=None, n_kv_heads=None, head_dim=None, intermediate=None, tie_embeddings=True, norm_eps=1e-5, dropout=0.0): self.vocab = int(vocab) self.d_model = int(d_model) self.layers = tuple(str(k) for k in layers) # glue: layer kind config names self.n_heads = None if n_heads is None else int(n_heads) self.n_kv_heads = (None if n_kv_heads is None else int(n_kv_heads)) self.head_dim = None if head_dim is None else int(head_dim) self.intermediate = None if intermediate is None else int(intermediate) self.tie_embeddings = bool(tie_embeddings) self.norm_eps = float(_round_f32(norm_eps)) self.dropout = float(_round_f32(dropout)) if self.vocab < 2 or self.d_model < 1 or not self.layers: raise ValueError("mojolearn.SambaConfig: vocab >= 2, d_model >= 1 " "and at least one layer are required") for k in self.layers: # glue: validates layer kind names if k not in _LAYER_KINDS: raise ValueError("mojolearn.SambaConfig: layer kind %r; the " "kinds are %r" % (k, _LAYER_KINDS)) if "mamba3" in self.layers and self.d_model % (_M3_HEADDIM // _M3_EXPAND): raise ValueError("mojolearn.SambaConfig: a mamba3 layer needs " "d_model to be a multiple of 32 (Mamba3Block)") if "attention" in self.layers: if self.n_heads is None or self.intermediate is None: raise ValueError("mojolearn.SambaConfig: an attention layer " "needs n_heads and intermediate") if self.head_dim is None: self.head_dim = self.d_model // self.n_heads if self.n_kv_heads is None: self.n_kv_heads = self.n_heads if self.n_heads * self.head_dim != self.d_model: raise ValueError("mojolearn.SambaConfig: d_model must equal " "n_heads * head_dim") if not (0.0 <= self.dropout < 1.0): raise ValueError("mojolearn.SambaConfig: dropout must be in [0, 1)") if not (self.norm_eps >= 0.0): raise ValueError("mojolearn.SambaConfig: norm_eps must be >= 0")
[docs] def to_dict(self): """The configuration as a plain dict (the inverse of `from_dict`).""" return {"vocab": self.vocab, "d_model": self.d_model, "layers": list(self.layers), "n_heads": self.n_heads, "n_kv_heads": self.n_kv_heads, "head_dim": self.head_dim, "intermediate": self.intermediate, "tie_embeddings": self.tie_embeddings, "norm_eps": self.norm_eps, "dropout": self.dropout}
[docs] @classmethod def from_dict(cls, d): """Build a configuration from a dict made by `to_dict`.""" return cls(d["vocab"], d["d_model"], d["layers"], d["n_heads"], d["n_kv_heads"], d["head_dim"], d["intermediate"], d["tie_embeddings"], d["norm_eps"], d["dropout"])
# -- the registry ---------------------------------------------------
[docs] def block_shapes(self, kind): """Names and shapes of one block's tensors. Parameters ---------- kind : {'mamba3', 'attention'} Layer kind. Returns ------- list of (str, tuple) ``(name, shape)`` pairs in the block's weight order. """ dm = self.d_model if kind == "mamba3": di = _M3_EXPAND * dm nh = di // _M3_HEADDIM dip = 2 * di + 2 * _M3_NGROUPS * _M3_D_STATE + 3 * nh + _M3_NUM_ROPE_ANGLES shapes = { "block_norm.weight": (dm,), "in_proj.weight": (dip, dm), "dt_bias": (nh,), "B_norm.weight": (_M3_D_STATE,), "C_norm.weight": (_M3_D_STATE,), "B_bias": (nh, _M3_D_STATE), "C_bias": (nh, _M3_D_STATE), "D": (nh,), "out_proj.weight": (dm, di), } return [(n, shapes[n]) for n in Mamba3Block._W_NAMES] # glue: weight names and shapes qw = self.n_heads * self.head_dim kw = self.n_kv_heads * self.head_dim it = self.intermediate shapes = { "input_layernorm.weight": (dm,), "post_attention_layernorm.weight": (dm,), "q_proj.weight": (qw, dm), "k_proj.weight": (kw, dm), "v_proj.weight": (kw, dm), "o_proj.weight": (dm, qw), "gate_proj.weight": (it, dm), "up_proj.weight": (it, dm), "down_proj.weight": (dm, it), } return [(n, shapes[n]) for n in TransformerBlock._W_NAMES] # glue: weight names and shapes
[docs] def registry(self): """Names and shapes of every parameter tensor of the stack, in order. The order is ``embed.weight``, then ``layers.{i}.<block tensor>`` for each layer, then ``norm_f.weight``, then ``lm_head.weight`` when embeddings are not tied. It is also the summation order of gradient clipping. Returns ------- list of (str, tuple) """ out = [("embed.weight", (self.vocab, self.d_model))] for i, kind in enumerate(self.layers): # glue: registry of named tensors out.extend(("layers.%d.%s" % (i, n), s) for n, s in self.block_shapes(kind)) # glue: registry of named tensors out.append(("norm_f.weight", (self.d_model,))) if not self.tie_embeddings: out.append(("lm_head.weight", (self.vocab, self.d_model))) return out
def _init_tensor(gen, name, shape): """Initial bits from the position-keyed stream: normal(0, 0.02) for the embedding and an untied head, torch's Linear default (kaiming-uniform, bound 1/sqrt(fan_in)) for every projection, ones for the norms / D / B_bias / C_bias, and uniform(-4, -2) for dt_bias (softplus of that is a dt near 0.02 to 0.13).""" last = name.split(".")[-1] if name in ("embed.weight", "lm_head.weight"): return gen.normal(shape, 0.0, 0.02) if last == "weight" and len(shape) == 2: return gen.kaiming_uniform(shape, fan_in=shape[1]) if last == "dt_bias": return gen.uniform(shape, -4.0, -2.0) return _buffers.full(shape, 1.0, '<f4') def _count_targets(y): """How many int32 targets are not the ignore index. The equality mask and its integer sum run in the core Mojo helpers (`Array.__eq__` against a scalar and `Array.sum`, bindings/hotpath_helpers.mojo); Python only subtracts (pyglue-sweep 2026-10-03: the Python scan is gone).""" return int(y.size) - int((y == T._IGNORE_INDEX_DEFAULT).sum()) # glue: mask and sum run in Mojo helpers class SambaState(object): """The stack's decode state (2026-09-15): `layers[i]` is layer i's own block state (`Mamba3State` or `TransformerState`), caller-owned and laid out as its block class documents, for `batch_size` rows and up to `max_tokens` positions. `SambaStack.forward(inputs, state)` and `SambaStack.step` update every piece in place.""" def __init__(self, batch_size, max_tokens, layers): self.batch_size = int(batch_size) self.max_tokens = int(max_tokens) self.layers = list(layers) # Accumulation follows optimizer contract clause 9.2 (leaf size, T mod L, A divides P, A a power of # two). The embedding backward is a run-sorted fold over each row's occurrences, not a leaf tree, # so A > 1 can change embed.weight's last bits; tests/test_samba_surface.py records it as "no claim". # Holds no numerics: every arithmetic step calls _mojolearn_training / _mojolearn_mamba / # _mojolearn_transformer (see the module docstring).
[docs] class SambaStack(object): """A Samba-style language model with its own training loop. Embedding, then a stack of `Mamba3Block` and `TransformerBlock` layers (each with its own pre-norm and residual), a final RMSNorm, a tied or untied LM head and the cross-entropy loss, with a full backward pass. Training uses AdamW over the parameter registry (`SambaConfig.registry`), optional gradient-norm clipping, an optional learning-rate schedule and gradient accumulation. All parameters live in one flat float32 buffer. Attention layers use full causal attention. Pass exactly one of `weights` or `generator` to supply the initial values; otherwise `ValueError`. With ``accumulation_steps > 1`` the batch rows are split into microbatches whose gradients are combined in a fixed tree. A split the accumulation rule does not allow is refused with `ValueError` before any gradient is computed. An allowed split reproduces the unsplit step's block, final-norm and LM-head gradients bit for bit, but not the embedding gradient's, which is summed per token occurrence: after a step with accumulation the embedding can differ from the unsplit step in the last bits. Parameters ---------- config : SambaConfig Model shape. Anything else raises `TypeError`. weights : dict of str to float32 buffer or None, default None Initial parameters keyed by the registry names, with exact names and shapes. generator : Generator or None, default None Seeded `mojolearn.training.Generator` used to initialize the parameters and to draw dropout masks. lr : float, default 1e-3 Learning rate, used when `lr_schedule` is None. betas : tuple of float, default (0.9, 0.999) AdamW moment decays. eps : float, default 1e-8 AdamW denominator term. weight_decay : float, default 0.01 AdamW decoupled weight decay. lr_schedule : schedule or None, default None For example `WarmupCosineLR` or `WarmupLinearLR`. max_norm : float or None, default None Clip the global gradient L2 norm to this value; None turns clipping off. accumulation_steps : int, default 1 Number of microbatches per optimizer step; must divide the batch rows. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default None Numeric mode. None uses the library default. Attributes ---------- config : SambaConfig names : list of str Registry names in order. optimizer : AdamW The optimizer. Examples -------- >>> import numpy as np, mojolearn >>> from mojolearn.training import Generator, WarmupCosineLR >>> cfg = mojolearn.SambaConfig(vocab=256, d_model=64, layers=("mamba3", "mamba3")) >>> stack = mojolearn.SambaStack(cfg, generator=Generator(7), lr=1e-3, ... lr_schedule=WarmupCosineLR(1e-3, 8, 64), ... max_norm=1.0, accumulation_steps=2) >>> ids = np.random.default_rng(0).integers(0, 256, (4, 32)).astype(np.int32) >>> out = stack.train_step(ids[:, :-1], ids[:, 1:]) >>> stack.save_checkpoint("run.ckpt") """
[docs] def __init__(self, config, weights=None, generator=None, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01, lr_schedule=None, max_norm=None, accumulation_steps=1, numeric_mode=None): if not isinstance(config, SambaConfig): raise TypeError("mojolearn.SambaStack: config must be a SambaConfig") if (weights is None) == (generator is None): raise ValueError("mojolearn.SambaStack: pass exactly one of " "weights= or generator=") self.config = config self.numeric_mode = numeric_mode self.names = [n for n, _ in config.registry()] # glue: registry of named tensors self.shapes = {n: tuple(s) for n, s in config.registry()} # glue: registry of named tensors self.offsets = [0] for n in self.names: # glue: offsets over named tensors self.offsets.append(self.offsets[-1] + int(math.prod(self.shapes[n]))) # glue: offsets over named tensors self.n_total = self.offsets[-1] self.flat = _buffers.zeros(self.n_total, '<f4') self.arrays = {} for j, n in enumerate(self.names): # glue: views per named tensor self.arrays[n] = _Array.from_buffer(_checks.flat_view(self.flat, 'f')[self.offsets[j]:self.offsets[j + 1]]).reshape(self.shapes[n]) self.generator = generator if generator is not None else T.Generator(0, numeric_mode) if weights is not None: self.load_weights(weights) else: for n in self.names: # glue: copies each named tensor _checks.flat_view(self.arrays[n], 'f')[:] = _checks.flat_view(_init_tensor(self.generator, n, self.shapes[n]), 'f') self.max_norm = None if max_norm is None else float(max_norm) self.optimizer = T.AdamW([self.arrays[n] for n in self.names], lr=lr, # glue: optimizer gets named tensors betas=betas, eps=eps, weight_decay=weight_decay, lr_schedule=lr_schedule, accumulation_steps=accumulation_steps, numeric_mode=numeric_mode) # The block wrappers retain views of ``self.flat``; optimizer steps # and checkpoint restores update that storage in place. Constructing # them again for every forward/backward only repeats Python shape and # option validation, and also prevents TransformerBlock from reusing # its per-instance runtime resources. Keep one wrapper per registry # block, just as SambaInference does. No tensor is copied or cached. self._blocks = [self._make_block(i) for i in range(len(config.layers))] # glue: builds one block per layer self.last_ = None
# -- weights ----------------------------------------------------------
[docs] def load_weights(self, weights): """Copy parameters in from a dict keyed by registry name. The key set and every shape must match exactly; values must be float32. """ missing = [n for n in self.names if n not in weights] # glue: checks weight dict names extra = [n for n in weights if n not in self.shapes] # glue: checks weight dict names if missing or extra: raise ValueError("mojolearn.SambaStack: weight dict mismatch; " "missing %r, unknown %r" % (missing, extra)) for n in self.names: # glue: checks each named tensor a = weights[n] pb = _checks.probe(a) if not _checks.is_native_f32(pb.format): raise TypeError("mojolearn.SambaStack: %s has dtype %s; float32 only" % (n, pb.format)) if pb.shape != self.shapes[n]: raise ValueError("mojolearn.SambaStack: %s has shape %r, want %r" % (n, pb.shape, self.shapes[n])) a = _buffers.as_f32_c(a, ndim=None, name=n)[0] if not _buffers.all_finite(a): raise ValueError("mojolearn.SambaStack: %s is not finite" % n) _checks.flat_view(self.arrays[n], 'f')[:] = _checks.flat_view(a, 'f')
[docs] def parameters(self): """The parameters as ``{name: array}``, views of the flat buffer in registry order.""" return {n: self.arrays[n] for n in self.names} # glue: dict of named tensors
def _make_block(self, i): kind = self.config.layers[i] w = {n: self.arrays["layers.%d.%s" % (i, n)] for n, _ in self.config.block_shapes(kind)} # glue: dict of named tensors if kind == "mamba3": block = Mamba3Block(w, numeric_mode=self.numeric_mode) # lane/fam2-lm: the stack reads no report stage of its blocks. block._discard_reports = True return block c = self.config return TransformerBlock(w, n_heads=c.n_heads, n_kv_heads=c.n_kv_heads, head_dim=c.head_dim, numeric_mode=self.numeric_mode) def _block(self, i): # A small number of wiring tests deliberately construct a partial # stack with ``__new__`` so they can substitute the arithmetic. Keep # that diagnostic path able to materialize a wrapper on demand; all # ordinarily constructed stacks use the cache above. blocks = getattr(self, "_blocks", None) return self._make_block(i) if blocks is None else blocks[i] def _head_weight(self): return self.arrays["embed.weight" if self.config.tie_embeddings else "lm_head.weight"] # -- the Apple FAST fused tail (lane afn-samba, 2026-10-03) --------------- _AFN_ENTRIES = ("samba_afn_norm_head_forward", "samba_afn_tail_train", "samba_afn_embedding_backward_tied") def _afn(self): """The training binding's fused Samba entries as `{name: fn}`, or None. They are registered only by an Apple FAST build with MOJOLEARN_AFN_SAMBA_FUSE (or MOJOLEARN_AFN_SAMBA_ALL); the IDENTICAL binding and every other build have none, and MOJOLEARN_HOTPATH=python asks for the per-op reference arm. The kernels, operands and order are the per-op path's; what changes is that the final norm's output, the logits' gradient and the tied embedding pair never cross the bus between binding calls (training/samba_afn.mojo).""" cached = getattr(self, "_afn_cache", None) if cached is not None: return cached or None entries = None if _buffers.hotpath_enabled(): binding = T._load(self.numeric_mode) found = {} for name in self._AFN_ENTRIES: # glue: looks up the named binding entries try: found[name] = getattr(binding, name, None) except (AttributeError, ImportError): found[name] = None if all(callable(found[name]) for name in self._AFN_ENTRIES): # glue: checks the named binding entries entries = found self._afn_cache = entries if entries is not None else {} return entries def _afn_norm_head(self, afn, h2): """`logits (M, V)` of the final norm then the head over `h2 (M, D)`, one binding call.""" c = self.config h2 = T._c32(h2, "h", "samba_afn_norm_head_forward") nw = T._c32(self.arrays["norm_f.weight"], "weight", "samba_afn_norm_head_forward") hw = T._c32(self._head_weight(), "weight", "samba_afn_norm_head_forward") m, k = h2.shape n = c.vocab logits = _buffers.empty((m, n), '<f4') afn["samba_afn_norm_head_forward"]( [T._addr(logits), T._addr_ro(h2), T._addr_ro(nw), T._addr_ro(hw)], [int(m), int(n), int(k), float(c.norm_eps)]) return logits def _afn_tail_train(self, afn, h2, y, items): """`(loss, dh (M, D), d_norm_w (D,), d_head_w (V, D))` of the final norm, the head, the sum-reduced loss over `items` and both backwards, one binding call.""" c = self.config h2 = T._c32(h2, "h", "samba_afn_tail_train") nw = T._c32(self.arrays["norm_f.weight"], "weight", "samba_afn_tail_train") hw = T._c32(self._head_weight(), "weight", "samba_afn_tail_train") m, k = h2.shape n = c.vocab loss_out = _buffers.zeros((1,), '<f4') row_out = _buffers.empty((m,), '<f4') dh = _buffers.empty((m, k), '<f4') dnw = _buffers.empty((k,), '<f4') dhw = _buffers.empty((n, k), '<f4') afn["samba_afn_tail_train"]( [T._addr(loss_out), T._addr(row_out), T._addr(dh), T._addr(dnw), T._addr(dhw), T._addr_ro(h2), T._addr_ro(nw), T._addr_ro(hw), T._addr_ro(y)], [int(m), int(n), int(k), float(c.norm_eps), int(T._IGNORE_INDEX_DEFAULT), 1, int(items), 0.0]) return float(loss_out[0]), dh, dnw, dhw def _afn_embedding_backward_tied(self, afn, dy2, ids1, pair): """`embedding_backward(dy2, ids1) + pair`, the tied gradient, one binding call.""" c = self.config dy2 = T._c32(dy2, "dy", "samba_afn_embedding_backward_tied") pair = T._c32(pair, "pair", "samba_afn_embedding_backward_tied") n, d = dy2.shape dw = _buffers.empty((c.vocab, d), '<f4') afn["samba_afn_embedding_backward_tied"]( [T._addr(dw), T._addr_ro(dy2), T._addr_ro(ids1), T._addr_ro(pair)], [int(n), int(c.vocab), int(d)]) return dw # -- the device-resident forward and step (lane S1 samba-resident, 2026-10-07) _RESIDENT_ENTRIES = ("samba_resident_open", "samba_resident_forward", "samba_resident_step") def _resident(self): """The training binding when it carries the IDENTICAL resident Samba entries (the IDENTICAL default since 2026-10-08; -D MOJOLEARN_IDN_SAMBA_RESIDENT_STEP_OFF drops them; training/samba_resident.mojo) and this stack can take them: no dropout (the dropout mask is the generator's, drawn per microbatch on the per-op route). None otherwise, and the per-op route below runs as before (arm B). The chunked-head profile (NI34) spells a different head, so a chunked-head build leaves the entries out (training/neural_identical_experiments.mojo); it is re-checked here so a mixed build still takes one route.""" cached = getattr(self, "_resident_cache", None) if cached is not None: return cached or None binding = None if self.config.dropout == 0.0: try: b = T._load(self.numeric_mode) enabled = getattr(b, "samba_resident_enabled", None) ok = callable(enabled) and bool(enabled()) if ok: chunked = getattr(b, "training_chunked_lm_head_enabled", None) ok = not (callable(chunked) and bool(chunked())) if ok and all(callable(getattr(b, n, None)) for n in self._RESIDENT_ENTRIES): # glue: checks binding entry names binding = b except (AttributeError, ImportError): binding = None self._resident_cache = binding if binding is not None else False return binding def _resident_session(self, binding): """The stack's `_SambaResidentSession` (opened once: the registry and its gradient on the device, the per-layer weight views). The host registry `self.flat` stays authoritative: every call uploads it and the step writes it back, so checkpoints, `parameters()` and `load_state_dict` are unchanged.""" session = getattr(self, "_resident_handle", None) if session is None: c = self.config kinds = [0 if k == "mamba3" else 1 for k in c.layers] # glue: layer kind codes for the binding session = binding.samba_resident_open( kinds, [int(c.vocab), int(c.d_model), int(c.n_heads or 0), int(c.n_kv_heads or 0), int(c.head_dim or 0), int(c.intermediate or 0), 1 if c.tie_embeddings else 0, float(c.norm_eps), int(self.n_total)], T._addr_ro(self.flat)) self._resident_handle = session return session def _forward_resident(self, binding, ids): """`(B, L)` ids -> `(B, L, vocab)` logits in one binding call. The id range check is the embedding entry's own device refusal, so no NumPy pass over the ids runs here.""" c = self.config session = self._resident_session(binding) b, l = ids.shape logits = _buffers.empty((b * l, c.vocab), '<f4') binding.samba_resident_forward( session, [T._addr_ro(self.flat), T._addr_ro(ids), T._addr(logits)], [int(b), int(l)]) return logits.reshape((b, l, c.vocab)) def _train_step_resident(self, binding, ids, y): """One optimizer step in one binding call (accumulation_steps 1): the forward, the sum-reduced loss over the step's own target count, the backward and `identical_optimizer_step` with the optimizer's resident moments. The optimizer's scalar bookkeeping is its own (`_step_begin` / `_step_finish`). None when the optimizer has no resident moments in this build (the per-op route then runs).""" o = self.optimizer o._open_resident(binding) if o._res is None: return None session = self._resident_session(binding) b, l = ids.shape cfg, plist = o._step_begin(self.max_norm) info = _buffers.zeros((3,), '<f4') loss_out = _buffers.zeros((1,), '<f4') binding.samba_resident_step( session, [T._addr(self.flat), T._addr_ro(ids), T._addr_ro(y), T._addr_ro(o.offsets), T._addr(o.buf_initialized), T._addr(info), T._addr(loss_out)], [int(b), int(l)] + plist, int(o._res)) # the moments moved on the device; the host copies are stale until read o._host_fresh = False total = o._step_finish(info, cfg) self.last_ = {"loss": float(loss_out[0]), "lr": o.lr_, "step": o.t, "total_norm": total} return dict(self.last_) # -- forward ------------------------------------------------------------ @staticmethod def _ids(x, what): pb = _checks.probe(x) if len(pb.shape) != 2 or not _checks.is_integer(pb.format): raise ValueError("mojolearn.SambaStack: %s must be (B, L) integer ids" % what) return _buffers.as_i32_c(x, ndim=2, name=what)[0] def _forward(self, inputs, dropout_stream=None, token_offset=0, head=True, norm=True, retain_tapes=False): """The forward with every block input kept for the backward. `head=False` stops after the final norm (`logits` is None): the fused `samba_head_loss` runs the head itself. `norm=False` (with `head=False`) stops after the last block (`hn` is None too): the Apple FAST tail (`_afn`) runs the final norm itself. With `_afn` present and both on, the final norm and the head are one call.""" c = self.config ids = self._ids(inputs, "inputs") b, l = ids.shape if ids.min() < 0 or ids.max() >= c.vocab: raise ValueError("mojolearn.SambaStack: inputs must be in [0, vocab)") x = T.embedding_forward(self.arrays["embed.weight"], ids.reshape(-1), self.numeric_mode).reshape((b, l, c.d_model)) key = None if c.dropout > 0.0 and dropout_stream is not None: x, key = self.generator.dropout(x, c.dropout, offset=token_offset * c.d_model, stream=dropout_stream) xs = [] tapes = [] for i in range(len(c.layers)): # glue: dispatches each layer block xs.append(x) block = self._block(i) forward_tape = getattr(block, "_forward_with_tape", None) if retain_tapes and callable(forward_tape): x, tape = forward_tape(x) else: x, tape = block.forward(x), None tapes.append(tape) hn = None logits = None afn = self._afn() if (norm and head) else None if afn is not None: logits = self._afn_norm_head(afn, x.reshape((b * l, c.d_model))) elif norm: hn = T.rms_norm_forward(x, self.arrays["norm_f.weight"], c.norm_eps, self.numeric_mode) if head: logits = T.linear_forward(hn.reshape((b * l, c.d_model)), self._head_weight(), self.numeric_mode) return {"ids": ids, "key": key, "xs": xs, "tapes": tapes, "h": x, "hn": hn, "logits": logits}
[docs] def forward(self, inputs, state=None, *, lengths=None): """Compute logits for a batch of token ids, without dropout. Parameters ---------- inputs : int array, shape (B, L) Token ids. state : SambaState or None, default None None runs every block from a zero state. Pass a state from `allocate_state` to carry it: each block's state is read at entry and updated in place, so a later `forward` or `step` continues the sequence. lengths : sequence of int or None, default None Makes the batch ragged: B integers in [1, L]. Row i is real at positions [0, lengths[i]) and padding after. Each real position's logits equal the row run alone at its own length, and every padding position's logits are exactly +0.0. Not allowed together with `state`. Returns ------- logits : mojolearn.Array, shape (B, L, vocab) """ if lengths is not None: if state is not None: raise ValueError("mojolearn.SambaStack.forward: lengths= applies to the " "stateless forward only; pass state=None") ids = self._ids(inputs, "inputs") return _ragged.ragged_forward(self.forward, ids, None, lengths, "<i4", "SambaStack.forward")[0] if state is not None: return self._forward_state(inputs, state, step=False) resident = self._resident() if resident is not None: return self._forward_resident(resident, self._ids(inputs, "inputs")) acts = self._forward(inputs) b, l = acts["ids"].shape return acts["logits"].reshape((b, l, self.config.vocab))
# -- decode (2026-09-15) --------------------------------------------------
[docs] def allocate_state(self, batch_size, max_tokens): """Return a zero decode state for `batch_size` sequences of up to `max_tokens` positions. The state holds one block state per layer in stack order (`Mamba3State` or `TransformerState`), owned by the caller. Parameters ---------- batch_size : int max_tokens : int Returns ------- SambaState """ b, smax = int(batch_size), int(max_tokens) if b < 1 or smax < 1: raise ValueError("mojolearn.SambaStack.allocate_state: batch_size " "and max_tokens must be positive") layers = [] for i, kind in enumerate(self.config.layers): # glue: allocates state per layer blk = self._block(i) layers.append(blk.allocate_state(b) if kind == "mamba3" else blk.allocate_state(b, smax)) return SambaState(b, smax, layers)
[docs] def step(self, inputs, state): """Decode one token per row, updating `state` in place. The stateful `forward` at L = 1. No dropout. Parameters ---------- inputs : int array, shape (B,) or (B, 1) Token ids. state : SambaState Decode state from `allocate_state`. Returns ------- logits : mojolearn.Array, shape (B, vocab) """ if state is None: raise ValueError("mojolearn.SambaStack.step: state is required " "(allocate_state(B, max_tokens) makes the fresh one)") pb = _checks.probe(inputs) if not _checks.is_integer(pb.format) or len(pb.shape) not in (1, 2) \ or (len(pb.shape) == 2 and pb.shape[1] != 1): raise ValueError("mojolearn.SambaStack.step: inputs must be (B,) or " "(B, 1) integer ids") ids = _buffers.as_i32_c(inputs, ndim=None, name="inputs")[0].reshape((pb.shape[0], 1)) out = self._forward_state(ids, state, step=True) return out.reshape((pb.shape[0], self.config.vocab))
def _forward_state(self, inputs, state, step): c = self.config what = "mojolearn.SambaStack.step" if step else "mojolearn.SambaStack.forward" if not isinstance(state, SambaState): raise TypeError("%s: state must be a SambaState (allocate_state)" % what) ids = self._ids(inputs, "inputs") b, l = ids.shape if ids.min() < 0 or ids.max() >= c.vocab: raise ValueError("%s: inputs must be in [0, vocab)" % what) if state.batch_size != b or len(state.layers) != len(c.layers): raise ValueError("%s: the state holds %d rows and %d layers, the call " "has B = %d and the stack %d layers" % (what, state.batch_size, len(state.layers), b, len(c.layers))) x = T.embedding_forward(self.arrays["embed.weight"], ids.reshape(-1), self.numeric_mode).reshape((b, l, c.d_model)) for i in range(len(c.layers)): # glue: dispatches each layer block blk = self._block(i) x = blk.step(x, state.layers[i]) if step else blk.forward(x, state.layers[i]) hn = T.rms_norm_forward(x, self.arrays["norm_f.weight"], c.norm_eps, self.numeric_mode) logits = T.linear_forward(hn.reshape((b * l, c.d_model)), self._head_weight(), self.numeric_mode) return logits.reshape((b, l, c.vocab))
[docs] def loss(self, inputs, targets): """Mean cross-entropy of `targets` given `inputs`, both (B, L) ids; no dropout, no gradient.""" acts = self._forward(inputs) y = self._ids(targets, "targets").reshape(-1) return float(T.cross_entropy(acts["logits"], y, numeric_mode=self.numeric_mode))
# -- backward ----------------------------------------------------------- def _refuse_no_backward(self): for i, kind in enumerate(self.config.layers): # glue: checks each layer kind if kind == "attention" and not callable(getattr(TransformerBlock, "backward", None)): raise NotImplementedError( "mojolearn.SambaStack: layer %d is an attention block and " "loaded TransformerBlock wrapper has no callable backward; " "install a matching wrapper and IDENTICAL transformer " "extension. The layer gradient cannot be skipped " "(python/mojolearn/_samba_impl.py)" % i)
[docs] def loss_and_grads(self, inputs, targets, num_items=None, dropout_stream=None, token_offset=0): """Run one forward and the full backward pass. Parameters ---------- inputs, targets : int array, shape (B, L) Token ids and next-token targets. Shapes must match. num_items : int or None, default None Divisor of the summed loss and gradients. None uses this call's own number of targets; pass the whole step's count to make a microbatch's gradient its slice of the unsplit one. dropout_stream : int or None, default None Generator stream for the dropout mask; None applies no dropout. token_offset : int, default 0 Position offset of this batch in the dropout stream. Returns ------- loss : float grads : list of mojolearn.Array One gradient per parameter, in registry order. """ self._refuse_no_backward() c = self.config # The fused head (lane/py-lm): the head GEMM, the loss and the head # backward in one call, the logits never crossing back and forth. # Same kernels, operands and order as the three-call arm below, # which stays as the reference for a binding without the entry. # The Apple FAST tail (lane afn-samba): the final norm, the head, the # loss and both backwards in one call, then the tied embedding # gradient in one call. The per-op arms below are the reference. afn = self._afn() head_binding = T._load(self.numeric_mode) fused = afn is None and callable(T._optional_samba_head(head_binding)) try: chunked_capability = getattr(head_binding, "training_chunked_lm_head_enabled", None) except (ImportError, AttributeError): chunked_capability = None chunked = afn is None and callable(chunked_capability) and chunked_capability() acts = self._forward(inputs, dropout_stream, token_offset, head=not fused and not chunked and afn is None, norm=afn is None, retain_tapes=True) try: ids = acts["ids"] b, l = ids.shape y = self._ids(targets, "targets") if y.shape != ids.shape: raise ValueError("mojolearn.SambaStack: targets must match inputs' shape") y = y.reshape(-1) count = _count_targets(y) items = count if num_items is None else int(num_items) grads = {} if afn is not None: loss, dh, grads["norm_f.weight"], dw_head = self._afn_tail_train( afn, acts["h"].reshape((b * l, c.d_model)), y, items) dh = dh.reshape((b, l, c.d_model)) else: hn2 = acts["hn"].reshape((b * l, c.d_model)) # NI34's existing chunked profile defines an ordinary mean # with no ignored targets. Other objective shapes retain the # original sum/custom-divisor path; no rescaling in Python. if chunked and count == b * l and items == count: out = T.chunked_lm_head_loss(hn2, self._head_weight(), y, return_grad=True, numeric_mode=self.numeric_mode) else: out = T.samba_head_loss(hn2, self._head_weight(), y, items, self.numeric_mode) if fused else None if out is not None: loss, dhn, dw_head = out else: logits = acts["logits"] if logits is None: logits = T.linear_forward(hn2, self._head_weight(), self.numeric_mode) loss, dlogits = T.cross_entropy(logits, y, reduction="sum", num_items=items, return_grad=True, numeric_mode=self.numeric_mode) dhn, dw_head = T.linear_backward(dlogits, hn2, self._head_weight(), self.numeric_mode) dh, grads["norm_f.weight"] = T.rms_norm_backward( dhn.reshape((b, l, c.d_model)), acts["h"], self.arrays["norm_f.weight"], c.norm_eps, self.numeric_mode) for i in reversed(range(len(c.layers))): # glue: dispatches each layer backward block = self._block(i) tape = acts["tapes"][i] if tape is None: g = block.backward(acts["xs"][i], dh) else: try: g = block._backward_from_tape(tape, dh) finally: block._close_forward_tape(tape) acts["tapes"][i] = None dh = g.pop("x") for n, v in g.items(): # glue: renames gradient dict keys grads["layers.%d.%s" % (i, n)] = v if acts["key"] is not None: dh = self.generator.dropout_backward(dh, acts["key"]) if c.tie_embeddings and afn is not None: d_emb = self._afn_embedding_backward_tied( afn, dh.reshape((b * l, c.d_model)), ids.reshape(-1), dw_head) else: d_emb = T.embedding_backward(dh.reshape((b * l, c.d_model)), ids.reshape(-1), c.vocab, self.numeric_mode) if c.tie_embeddings: # The tied gradient is ONE pair add, embedding first, no # alignment claim (tokens=None). d_emb = T.accumulate_grads([d_emb, dw_head], tokens=None, numeric_mode=self.numeric_mode) if not c.tie_embeddings: grads["lm_head.weight"] = dw_head grads["embed.weight"] = d_emb return float(loss), [grads[n] for n in self.names] # glue: orders gradients by name finally: for i, tape in enumerate(acts["tapes"]): # glue: native tape owners if tape is not None: self._block(i)._close_forward_tape(tape) acts["tapes"][i] = None
[docs] def train_step(self, inputs, targets): """Run one optimizer step on a batch. The batch rows are split into `accumulation_steps` microbatches; each one's gradient uses the whole step's divisor and the pieces are combined in a fixed tree before clipping and the AdamW update. B must be divisible by `accumulation_steps`, otherwise `ValueError`. Parameters ---------- inputs, targets : int array, shape (B, L) Token ids and next-token targets. Returns ------- dict ``loss``, ``lr``, ``step`` and ``total_norm`` (the gradient norm before clipping, or None when `max_norm` is None). """ ids = self._ids(inputs, "inputs") y = self._ids(targets, "targets") if y.shape != ids.shape: raise ValueError("mojolearn.SambaStack: targets must match inputs' shape") b, l = ids.shape a = self.optimizer.accumulation_steps tokens = b * l if b % a != 0: raise ValueError("mojolearn.SambaStack.train_step: batch rows B=%d " "are not divisible by accumulation_steps=%d" % (b, a)) if a > 1 and not T.accumulation_is_aligned(tokens, a, self.numeric_mode): raise ValueError( "mojolearn.SambaStack.train_step: MISALIGNED microbatch split, " "T = %d tokens at A = %d does not satisfy optimizer contract " "clause 9.2 (leaf size, T mod L, A divides P, A a power of " "two); refused before any gradient is computed " "(python/mojolearn/_samba_impl.py)" % (tokens, a)) if a == 1: resident = self._resident() if resident is not None: out = self._train_step_resident(resident, ids, y) if out is not None: return out count = _count_targets(y) stream = (self.generator.next_stream() if self.config.dropout > 0.0 else None) rows = b // a losses, parts = [], [] for k in range(a): # glue: dispatches each microbatch binding call sl = slice(k * rows, (k + 1) * rows) loss_k, g_k = self.loss_and_grads(ids[sl], y[sl], num_items=count, dropout_stream=stream, token_offset=k * rows * l) losses.append(_Array.from_list([loss_k], '<f4')) parts.append(g_k) if a == 1: loss = float(losses[0][0]) total = self.optimizer.step(parts[0], max_norm=self.max_norm) else: loss = float(T.accumulate_grads(losses, tokens=None, numeric_mode=self.numeric_mode)[0]) total = self.optimizer.step_accumulated(parts, tokens, max_norm=self.max_norm) self.last_ = {"loss": loss, "lr": self.optimizer.lr_, "step": self.optimizer.t, "total_norm": total} return dict(self.last_)
# -- state ---------------------------------------------------------------- def _experiment_profile(self): """Native numerical-contract metadata; no tensor work in this shell.""" try: profile = getattr(T._load(self.numeric_mode), "training_experiment_profile", None) except (ImportError, AttributeError): profile = None return str(profile()) if callable(profile) else "baseline"
[docs] def state_dict(self, *, _copy_arrays=True): """The complete training state as a dict. Holds the configuration, parameter registry and values, optimizer moments and step, schedule, generator state and numeric mode, so `load_state_dict` resumes training exactly. """ o = self.optimizer sched = None if o.lr_schedule is None else o.lr_schedule.config() return { "schema": _STATE_SCHEMA, "profile": PROFILE, "experiment_profile": self._experiment_profile(), "numeric_mode": _backend._CODE_MODE.get( T._load(self.numeric_mode).training_numeric_mode(), "unknown"), "config": self.config.to_dict(), "registry": [{"name": n, "shape": list(self.shapes[n]), "offset": self.offsets[j], "size": self.offsets[j + 1] - self.offsets[j]} for j, n in enumerate(self.names)], # glue: checkpoint registry of tensors "parameters": self.flat.copy() if _copy_arrays else self.flat, "exp_avg": o.exp_avg.copy() if _copy_arrays else o.exp_avg, "exp_avg_sq": o.exp_avg_sq.copy() if _copy_arrays else o.exp_avg_sq, "buf_initialized": (o.buf_initialized.copy() if _copy_arrays else o.buf_initialized), "t": int(o.t), "optimizer": {"kind": "adamw", "lr": o.lr, "beta1": o.betas[0], "beta2": o.betas[1], "eps": o.eps, "weight_decay": o.weight_decay, "max_norm": self.max_norm, "accumulation_steps": o.accumulation_steps}, "schedule": sched, "rng": self.generator.state_dict(), }
[docs] def load_state_dict(self, state): """Restore a state made by `state_dict`; returns self. Raises `ValueError` when the schema, configuration, numeric profile or sizes do not match this stack. """ if state.get("schema") != _STATE_SCHEMA or state.get("profile") != PROFILE: raise ValueError("mojolearn.SambaStack: state schema/profile mismatch") if state.get("experiment_profile", "baseline") != self._experiment_profile(): raise ValueError("mojolearn.SambaStack: experiment numerical profile mismatch") if state["config"] != self.config.to_dict(): raise ValueError("mojolearn.SambaStack: state config differs from this stack's") # A state written under another numeric profile is refused by name; a # state with no field is fp32_v1, which is what this stack computes. _numeric_profile.check_saved(state, _numeric_profile.TRAINING_DEFAULT, "mojolearn.SambaStack state") p = _buffers.as_f32_c(state['parameters'], ndim=1, name='parameters')[0] if p.shape != (self.n_total,): raise ValueError("mojolearn.SambaStack: parameters hold %d floats, " "the registry is %d" % (p.size, self.n_total)) _checks.flat_view(self.flat, 'f')[:] = _checks.flat_view(p, 'f') self.optimizer.load_state_dict({ "t": state["t"], "exp_avg": state["exp_avg"], "exp_avg_sq": state["exp_avg_sq"], "buf_initialized": state["buf_initialized"]}) oc = state["optimizer"] self.optimizer.lr = float(oc["lr"]) self.optimizer.betas = (float(oc["beta1"]), float(oc["beta2"])) self.optimizer.eps = float(oc["eps"]) self.optimizer.weight_decay = float(oc["weight_decay"]) self.max_norm = None if oc["max_norm"] is None else float(oc["max_norm"]) if int(oc["accumulation_steps"]) != self.optimizer.accumulation_steps: raise ValueError("mojolearn.SambaStack: accumulation_steps in the " "state differs from this stack's; the microbatch " "count is part of the run's numerical specification") self.optimizer.lr_schedule = (None if state["schedule"] is None else T._Schedule.from_config(state["schedule"])) self.generator.load_state_dict(state["rng"]) return self
# -- checkpoint (streamed arrays; legacy JSON remains readable) -------- _ARRAYS = (("parameters", "<f4"), ("exp_avg", "<f4"), ("exp_avg_sq", "<f4"), ("buf_initialized", "<i4"))
[docs] def save_checkpoint(self, path): """Write the training state to `path` as a checksummed checkpoint. The file is written atomically. Saving reads the live parameter and optimizer storage, so do not train or modify the stack while it saves. """ from ._samba_checkpoint import save return save(path, self.state_dict(_copy_arrays=False))
[docs] @classmethod def from_checkpoint(cls, path, numeric_mode=None): """Build a stack from a checkpoint written by `save_checkpoint`. Parameters ---------- path : str or path-like numeric_mode : str or None, default None None uses the mode stored in the checkpoint. Returns ------- SambaStack Raises `ValueError` when the checkpoint fails its integrity or schema checks. """ from ._samba_checkpoint import MAGIC, load with Path(path).open("rb") as stream: streamed = stream.read(len(MAGIC)) == MAGIC if streamed: payload = load(path) else: payload = cls._read_legacy_checkpoint(path) if numeric_mode is None: numeric_mode = payload["numeric_mode"] config = SambaConfig.from_dict(payload["config"]) oc = payload["optimizer"] sched = (None if payload["schedule"] is None else T._Schedule.from_config(payload["schedule"])) weights = {} flat = _checks.flat_view(payload["parameters"], 'f') for entry in payload["registry"]: # glue: views per checkpoint tensor weights[entry["name"]] = _Array.from_buffer(flat[ entry["offset"]:entry["offset"] + entry["size"]]).reshape(entry["shape"]) stack = cls(config, weights=weights, lr=oc["lr"], betas=(oc["beta1"], oc["beta2"]), eps=oc["eps"], weight_decay=oc["weight_decay"], lr_schedule=sched, max_norm=oc["max_norm"], accumulation_steps=oc["accumulation_steps"], numeric_mode=numeric_mode) return stack.load_state_dict(payload)
@classmethod def _read_legacy_checkpoint(cls, path): with Path(path).open("rb") as stream: encoded = stream.read(_CHECKPOINT_LIMIT + 1) if len(encoded) > _CHECKPOINT_LIMIT: raise ValueError("mojolearn.SambaStack: checkpoint exceeds the size limit") envelope = json.loads(encoded) if (not isinstance(envelope, dict) or set(envelope) != {"schema", "payload", "payload_sha256"} or envelope["schema"] != _CHECKPOINT_SCHEMA): raise ValueError("mojolearn.SambaStack: checkpoint schema mismatch") payload = envelope["payload"] if hashlib.sha256(_canonical(payload)).hexdigest() != envelope["payload_sha256"]: raise ValueError("mojolearn.SambaStack: checkpoint integrity mismatch") for key, dtype in cls._ARRAYS: # glue: checks each checkpoint array d = payload[key] if d["dtype"] != dtype: raise ValueError("mojolearn.SambaStack: checkpoint tensor dtype mismatch") raw = bytes.fromhex(d["hex"]) payload[key] = _buffers.frombytes(raw, dtype, d["shape"]) return payload