Source code for mojolearn._x_sequence_moe

# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Andrew Hendel. Part of mojolearn, https://doi.org/10.5281/zenodo.22068632
"""MoEBlock: the sparse mixture-of-experts feed-forward block of HF
transformers' Mixtral (`MixtralSparseMoeBlock`: `MixtralTopKRouter` +
`MixtralExperts`), forward on the GPU (`sequence/moe.mojo`): top-k routing
over the softmax of the router logits with the probabilities renormalised,
each chosen expert down_proj(silu(gate) * up), the weighted sum. The
weights keep HF's layout: `router` (E, D), `gate_up_proj` (E, 2F, D),
`down_proj` (E, D, F). Routing ties go to the lower expert index.

Refused (sequence/NOT_IMPLEMENTED.tsv): the backward pass, router jitter
noise, activations other than SiLU, the auxiliary load-balancing loss,
float64.

THE WEIGHTS STAY ON THE DEVICE (lane gap-neural-overhead2, 2026-10-02). On
the GPU binding the forward reads device copies of the three weight arrays
made once (`moe_weights_put`) instead of uploading all of them on every call
(277 MB at the board's shape). The copies follow the attributes: assigning
`router`, `gate_up_proj` or `down_proj` (or `load_state_dict`) makes the
next forward copy again. So that a stale copy can never run, the layer
holds its own float32 copy of each array and makes it READ-ONLY: edit a
weight by assigning a new array, not in place.

DEVICE I/O (lane neural-io-2, 2026-10-09): an x made by
`mojolearn._x_sequence_device.to_device` stays on the device: the forward
returns y as a `SequenceDeviceTensor` and keeps the routing outputs there
too (`router_logits_`, `selected_experts_`, `routing_weights_` are read
back when first accessed), so nothing crosses the bus inside the call
(`sequence/moe_dev.mojo`, the same launches on the same values). A binding
built with -D MOJOLEARN_SEQ_MOE_DEVICE_IO_OFF has no `moe_forward_dev`; a
resident x is then read back once and the host-array entry runs. The
host-array entry's outputs are `np.empty` (the binding stores every cell)."""
from ._optional_numpy import require_numpy
np = require_numpy('_x_sequence_moe')

from . import _backend
from ._buffer import InitStream
from ._x_sequence_device import SequenceDeviceTensor, is_seq_tensor


_WEIGHTS = ("router", "gate_up_proj", "down_proj")


# Weights stay on the device (lane gap-neural-overhead2): the forward reads device copies made
# once by `moe_weights_put` instead of uploading the three arrays every call; any assignment of
# router / gate_up_proj / down_proj (or load_state_dict) re-copies on the next forward, and the
# read-only host copies make a stale device copy impossible. Device I/O (lane neural-io-2): a
# resident x keeps y and the routing outputs on the device (`sequence/moe_dev.mojo`); a binding
# built with -D MOJOLEARN_SEQ_MOE_DEVICE_IO_OFF has no `moe_forward_dev` and the host entry runs.
[docs] class MoEBlock: """Sparse mixture-of-experts feed-forward block, as Mixtral's `MixtralSparseMoeBlock`. A router scores every token against `num_experts` experts; the `top_k` highest softmax probabilities are kept (renormalized to sum to one when `norm_topk_prob`), each chosen expert computes ``down_proj(silu(gate) * up)``, and the output is the weighted sum. Routing ties go to the lower expert index. Weights keep the Hugging Face layout. Forward only. The weight arrays are stored as read-only float32 copies: to change a weight, assign a new array (``block.router = new``) rather than editing it in place. Not supported: the backward pass, router jitter noise, activations other than SiLU, the auxiliary load-balancing loss, and float64 input. Parameters ---------- hidden_size : int Model dimension D. intermediate_size : int Expert hidden dimension F. num_experts : int, default 8 Number of experts E. top_k : int, default 2 Experts per token; ``1 <= top_k <= num_experts``, otherwise `ValueError`. norm_topk_prob : bool, default True Renormalize the kept routing probabilities. hidden_act : str, default 'silu' Expert activation; anything else raises `NotImplementedError`. random_state : int, default 0 Seed of the N(0, 0.02**2) weight initialization. numeric_mode : {'identical', 'deterministic', 'fast'} or None, default None Numeric mode. None uses the library default. Attributes ---------- router : ndarray, shape (E, D) Router weights. gate_up_proj : ndarray, shape (E, 2F, D) Gate and up projections of every expert. down_proj : ndarray, shape (E, D, F) Down projection of every expert. router_logits_ : ndarray, shape (n_tokens, E) Router logits of the last forward. selected_experts_ : ndarray of int64, shape (n_tokens, top_k) Experts chosen for each token by the last forward. routing_weights_ : ndarray, shape (n_tokens, top_k) Routing weights of the chosen experts. Examples -------- >>> import numpy as np, mojolearn >>> block = mojolearn.MoEBlock(hidden_size=64, intermediate_size=128, num_experts=8, top_k=2) >>> x = np.random.default_rng(0).random((4, 16, 64), dtype=np.float32) >>> y = block(x) """ #: the board and callers put x on the sequence binding's device with #: `mojolearn._x_sequence_device.to_device` (lane neural-io-2) _seq_device_io = True
[docs] def to_device(self, a): """Upload a float32 array once so `forward` keeps its input and outputs on the device. When this install has no device path for the block, the array itself is returned. Non-float32 input raises `TypeError` on that path. Parameters ---------- a : ndarray of float32 Array to upload. Returns ------- device tensor or ndarray """ from ._x_sequence_device import to_device b = _backend.binding("_mojolearn_x_sequence", self.numeric_mode) if not callable(getattr(b, "moe_forward_dev", None)): a = np.asarray(a) if a.dtype != np.float32: raise TypeError(f"mojolearn: to_device takes float32 (got {a.dtype}); convert it yourself") return np.ascontiguousarray(a) return to_device(a, self.numeric_mode)
def _routing_out(self, i): r = self.__dict__.get("_routing") if r is None: raise AttributeError("MoEBlock: call forward first") v = r[i] if is_seq_tensor(v): # resident (the device entry): read back on first access v = v.numpy() r[i] = v return v @property def router_logits_(self): """Router logits of the last forward, (n_tokens, num_experts).""" return self._routing_out(0) @property def selected_experts_(self): """Experts chosen for each token by the last forward, int64 (n_tokens, top_k).""" r = self.__dict__.get("_routing") if r is not None and r[3] is None: r[3] = self._routing_out(1).astype(np.int64) # glue: the picks' dtype for the caller if r is None: raise AttributeError("MoEBlock: call forward first") return r[3] @property def routing_weights_(self): """Routing weights of the chosen experts, (n_tokens, top_k).""" return self._routing_out(2) def __setattr__(self, name, value): if name in _WEIGHTS: a = np.array(value, dtype=np.float32, copy=True, order="C") a.flags.writeable = False object.__setattr__(self, name, a) object.__setattr__(self, "_wver", self.__dict__.get("_wver", 0) + 1) return object.__setattr__(self, name, value) def _weights_handle(self, b): """The device copies' handle on binding `b`, put again after any weight assignment (the old copies freed).""" cur = self.__dict__.get("_wdev") if cur is not None and cur[0] is b and cur[1] == self._wver: return cur[2] self._free_weights() h = int(b.moe_weights_put([self.router.ctypes.data, self.gate_up_proj.ctypes.data, self.down_proj.ctypes.data], [self.E, self.D, self.F])) object.__setattr__(self, "_wdev", (b, self._wver, h)) return h def _free_weights(self): cur = self.__dict__.get("_wdev") object.__setattr__(self, "_wdev", None) if cur is not None: try: cur[0].moe_weights_free(cur[2]) except Exception: # noqa: BLE001 (interpreter shutdown) pass def __del__(self): self._free_weights()
[docs] def __init__(self, hidden_size, intermediate_size, num_experts=8, top_k=2, norm_topk_prob=True, hidden_act="silu", random_state=0, numeric_mode=None): if hidden_act != "silu": raise NotImplementedError("MoEBlock: hidden_act other than 'silu' is not implemented") if not 1 <= int(top_k) <= int(num_experts): raise ValueError("MoEBlock: 1 <= top_k <= num_experts") self.D, self.F, self.E, self.k = int(hidden_size), int(intermediate_size), int(num_experts), int(top_k) self.norm_topk_prob = bool(norm_topk_prob) self.numeric_mode = numeric_mode # N(0, 0.02**2) weights drawn in Mojo (InitStream.fill_normal, the # base binding's counter-based normal_init_f32; lane pyglue-numeric: # numpy's Generator drew them in Python), one stream in this order stream = InitStream(random_state) for name, shape in (("router", (self.E, self.D)), ("gate_up_proj", (self.E, 2 * self.F, self.D)), # glue: three named expert weight shapes ("down_proj", (self.E, self.D, self.F))): a = np.empty(shape, np.float32) stream.fill_normal(a.ctypes.data, a.size, 0.0, 0.02) setattr(self, name, a) # a read-only copy (__setattr__)
[docs] def load_state_dict(self, sd): """Load `router`, `gate_up_proj` and `down_proj` from a dict of arrays; returns self. Shapes must match the block's, otherwise `ValueError`. """ for name, shape in (("router", (self.E, self.D)), ("gate_up_proj", (self.E, 2 * self.F, self.D)), # glue: the four named MoE parameter tensors ("down_proj", (self.E, self.D, self.F))): a = np.asarray(sd[name], dtype=np.float32) if a.shape != shape: raise ValueError(f"MoEBlock: {name} has shape {a.shape}, expected {shape}") setattr(self, name, a) # a read-only copy (__setattr__) return self
def _check_weights(self): # `want`, not `shape`: the caller's `shape` holds x's shape for the final reshape for name, want in (("router", (self.E, self.D)), ("gate_up_proj", (self.E, 2 * self.F, self.D)), # glue: checks three named expert weight shapes ("down_proj", (self.E, self.D, self.F))): if getattr(self, name).shape != want: raise ValueError(f"MoEBlock: {name} has shape {getattr(self, name).shape}, expected {want}") def _forward_dev(self, x): """`forward` on a resident x (lane neural-io-2): y and the routing outputs as resident tensors; None when x's binding has no device MoE entry (the caller reads x back and runs the host-array entry).""" b = x.b if not (callable(getattr(b, "moe_forward_dev", None)) and hasattr(b, "moe_weights_put")): return None if x.ndim < 1 or x.shape[-1] != self.D or x.size == 0: raise ValueError(f"MoEBlock: x must have shape (..., {self.D})") T = x.size // self.D self._check_weights() h = self._weights_handle(b) # every cell of the four outputs is stored by the route and the combine: no fill y = SequenceDeviceTensor._new(b, x.shape) logits = SequenceDeviceTensor._new(b, (T, self.E)) sel = SequenceDeviceTensor._new(b, (T, self.k)) w = SequenceDeviceTensor._new(b, (T, self.k)) b.moe_forward_dev([x.h, y.h, logits.h, sel.h, w.h], [T, self.D, self.F, self.E, self.k, int(self.norm_topk_prob), h]) object.__setattr__(self, "_routing", [logits, sel, w, None]) return y
[docs] def forward(self, x): """Apply the block to `x` of shape (..., hidden_size); also callable as ``block(x)``. Sets `router_logits_`, `selected_experts_` and `routing_weights_`. float64 input raises `TypeError`. An input from `to_device` gives an output on the device. Parameters ---------- x : ndarray of float32 Token activations. Returns ------- y : ndarray of float32 Output with the shape of `x`. """ if is_seq_tensor(x): y = self._forward_dev(x) if y is not None: return y x = x.numpy() x = np.asarray(x) if x.dtype == np.float64: raise TypeError("MoEBlock: float64 is refused; pass float32") shape = x.shape X = np.ascontiguousarray(x, dtype=np.float32).reshape(-1, self.D) T = X.shape[0] # the binding downloads every cell of the four outputs (lane neural-io-2: np.empty, # not np.zeros: no host zero pass over 4 T (D + E + 2k) bytes; the words are the same) y = np.empty((T, self.D), np.float32) logits = np.empty((T, self.E), np.float32) sel = np.empty((T, self.k), np.float32) w = np.empty((T, self.k), np.float32) b = _backend.binding("_mojolearn_x_sequence", self.numeric_mode) ip = [T, self.D, self.F, self.E, self.k, int(self.norm_topk_prob)] if hasattr(b, "moe_weights_put"): self._check_weights() h = self._weights_handle(b) b.moe_forward([X.ctypes.data, X.ctypes.data, X.ctypes.data, X.ctypes.data, y.ctypes.data, logits.ctypes.data, sel.ctypes.data, w.ctypes.data], ip + [h]) else: r, gu, dn = (np.ascontiguousarray(a) for a in (self.router, self.gate_up_proj, self.down_proj)) # glue: three contiguous weight buffers for the binding b.moe_forward([X.ctypes.data, r.ctypes.data, gu.ctypes.data, dn.ctypes.data, y.ctypes.data, logits.ctypes.data, sel.ctypes.data, w.ctypes.data], ip) object.__setattr__(self, "_routing", [logits, sel, w, None]) return y.reshape(shape)
__call__ = forward