Fixed recurrent state is lossy, not an unlimited exact transcript. The simple recurrence is an educational SSM, not the full selective Mamba model.
Beyond fixed-compute, fixed-memory transformers — models that route compute where it's needed and remember across time.
MoE · MoD · early exit — different tokens, different compute
KV cache · SSMs · external memory banks
An end-to-end implementation in PyTorch
Can we select experts, accumulate state, and keep memory scoped to a user?
Understand the data, the rule, and the required result. Then run the complete Python program and modify one thing at a time.
No companion presentation is required. Python syntax and any mathematical notation are explained as they are introduced.
Scores which experts receive a row.
Only selected experts process that row.
A finite summary passed from one step to the next.
Explicit durable state outside the neural function.
Trace each step and name what changed. Ask which fact stayed true and what should happen on an empty, invalid, or boundary input.
A dense transformer usually applies the same layer stack to each token. Per-token cost also depends on context length, caching, and implementation. Cross-session storage is a runtime choice, not a property supplied automatically by attention.
Dense blocks use the same parameterized transformations at each position, but attention cost varies with prefix length and batching. Adaptive routing changes which blocks or experts participate.
"hello," (probably " world") takes the same compute as predicting the next token in a complex math proof.The model has no state outside its current context window:
Public systems demonstrate several combinations. Architecture and memory claims should follow dated documentation; do not infer undisclosed proprietary model internals.
A learned router decides which sub-network fires for each token at each layer. Most parameters stay dormant for any given token.
Replace each FFN with N experts. Router picks top-k per token.
Public, documented examples include Switch Transformer, Mixtral, and DeepSeek-V3. Undisclosed proprietary architecture should not be inferred.
Per-token, per-block routing. Router decides which tokens enter each transformer block; the rest skip via the residual.
In the wild: Google's "Mixture-of-Depths" (Raposo et al. 2024); related: PonderNet, Adaptive Computation Time
Stop early when confident. Each layer outputs an "exit signal." When confidence crosses a threshold, this token skips the remaining layers and is predicted from the current layer.
In the wild: CALM (Schuster et al. 2022), DeeBERT, Adaptive Computation Time (Graves 2016)
class Router(nn.Module):
def __init__(self, d_model, n_choices):
super().__init__()
self.gate = nn.Linear(d_model, n_choices, bias=False)
def forward(self, x, top_k=1):
scores = self.gate(x) # (B, T, n_choices)
weights, idx = scores.topk(top_k, dim=-1) # pick top-k
weights = F.softmax(weights, dim=-1) # normalize
return weights, idx # downstream uses these
MoE typically ranks experts for each token; MoD may rank tokens for a fixed block budget; ACT/early-exit methods use halting policies. They are related adaptive-compute ideas with different mechanisms and guarantees.
Sparse dispatch changes active compute, not total parameter storage. Uniform balancing statistic equals 1; valid imbalanced routing can give values below 1.
Replace the dense FFN with N specialized FFNs. A router picks top-k of them per token, weighted by softmax of the gate scores.
With 8 experts and top-k=2, only ¼ of FFN weights are touched per token. Mixtral 8×7B has 47B total params but only ~13B fire per token.
import torch
import torch.nn as nn
import torch.nn.functional as F
class Expert(nn.Module):
def __init__(self, d):
super().__init__()
self.fc1 = nn.Linear(d, 4*d)
self.fc2 = nn.Linear(4*d, d)
def forward(self, x):
return self.fc2(F.gelu(self.fc1(x)))
class MoEFFN(nn.Module):
def __init__(self, d, n_experts=8, top_k=2):
super().__init__()
self.top_k = top_k
self.router = nn.Linear(d, n_experts, bias=False)
self.experts = nn.ModuleList([
Expert(d) for _ in range(n_experts)
])
def forward(self, x): # x: (B, T, d)
scores = self.router(x) # (B, T, N)
# Pick top-k experts per token
weights, idx = scores.topk(self.top_k, dim=-1)
weights = F.softmax(weights, dim=-1)
# Sparse dispatch: send each token only
# to its chosen experts
out = torch.zeros_like(x)
for e in range(len(self.experts)):
for k in range(self.top_k):
mask = (idx[..., k] == e)
if mask.any():
out[mask] += (
weights[..., k:k+1][mask]
* self.experts[e](x[mask])
)
return out
# Drop-in replacement for the FFN sublayer
# in a standard transformer block:
# self.ffn = MoEFFN(d=128, n_experts=8, top_k=2)
Real production MoE adds load-balancing loss (so all experts get used) and capacity factor (token dropping when an expert overflows). Omitted for clarity.
Sparse dispatch changes active compute, not total parameter storage. Uniform balancing statistic equals 1; valid imbalanced routing can give values below 1.
Where does "47B total, 13B active" come from? Count it by hand for Mixtral 8×7B.
A triage nurse (the router) sends each patient (token) to 2 of 8 specialists (experts). The hospital employs everyone (memory), but each patient only takes up two doctors' time (compute).
| Piece | Arithmetic | Params |
|---|---|---|
| One expert (SwiGLU, 3 matrices) | 3 × 4096 × 14336 | 176M |
| All experts, all layers | 176M × 8 × 32 | 45.1B |
| Attention (GQA), all layers | 41.9M × 32 | 1.3B |
| Embeddings + LM head | 2 × 32000 × 4096 | 0.26B |
| Total (stored) | ≈ 46.7B | |
| Active per token | 176M × 2 × 32 + 1.3B + 0.26B | ≈ 12.9B |
Not 8 × 7B = 56B: the experts share attention and embeddings. Only the FFNs are copied.
Left alone, the router tends to collapse: a few experts get every token, the rest never train. Switch Transformer adds:
L_aux = α · N · Σᵢ fᵢ · Pᵢ (α ≈ 0.01)
fᵢ = share of tokens sent to expert iPᵢ = average router probability for expert i| 8 experts | N · Σ fᵢPᵢ |
|---|---|
| Perfectly balanced (all 1/8) | 1.0 (uniform-routing reference) |
| Collapsed (all tokens → expert 0, P₀ = 0.9) | 8 · (1 · 0.9) = 7.2 |
probs = F.softmax(scores, dim=-1) # (B, T, N) f = F.one_hot(idx[..., 0], N).float().mean((0, 1)) # token share P = probs.mean((0, 1)) # mean prob loss = lm_loss + alpha * N * (f * P).sum()
All expert weights must be stored somewhere accessible, but serving can distribute experts across devices or offload weights. Sparse activation reduces active computation, not total parameter storage.
Fixed recurrent state is lossy, not an unlimited exact transcript. The simple recurrence is an educational SSM, not the full selective Mamba model.
Three flavors of accumulating state — from microsecond working memory to cross-session persistent knowledge.
Working memory within one generation.
Universal — every modern LLM serving framework uses it.
Compressed running summary. Replace attention with a fixed-size state vector updated each step.
In the wild: Mamba (Gu & Dao 2023), RWKV, RetNet, Jamba (hybrid).
Persistent across sessions. Learnable key-value store the model writes to and retrieves from.
In the wild: MemGPT (now Letta), Memorizing Transformers, RAG (a degenerate form).
Fixed recurrent state is lossy, not an unlimited exact transcript. The simple recurrence is an educational SSM, not the full selective Mamba model.
Replace attention with a tiny recurrent state updated each step. Inference becomes linear-time and constant-memory.
h_t = A · h_{t-1} + B · x_t # update state
y_t = C · h_t # emit output
A classical recurrence. The whole sequence is summarized into h_t — a fixed-size vector. Compute per step is constant.
| Attention | SSM | |
|---|---|---|
| Compute / token | O(N) | O(1) |
| Total compute | O(N²) | O(N) |
| State / token | grows with N | fixed |
| Random access | perfect | lossy compression |
| Trainable in parallel | yes | yes (with tricks) |
Mamba's clever bit: the step size Δ and B, C are computed from the input (so the effective decay eΔA is input-dependent too). That gives attention-like selectivity — "remember this, forget that" — while keeping linear complexity.
class MinimalSSM(nn.Module):
"""Linear recurrent layer (simplified Mamba-style)."""
def __init__(self, d_model, d_state=16):
super().__init__()
self.d_state = d_state
# Learnable state-transition matrices
self.A_log = nn.Parameter(torch.randn(d_state))
self.B = nn.Linear(d_model, d_state, bias=False)
self.C = nn.Linear(d_state, d_model, bias=False)
def forward(self, x): # x: (B, T, d_model)
B, T, _ = x.shape
A = -torch.exp(self.A_log) # stable, negative
h = torch.zeros(B, self.d_state, device=x.device)
outputs = []
for t in range(T):
# Recurrence — same shape every step
h = torch.exp(A) * h + self.B(x[:, t])
y = self.C(h)
outputs.append(y)
return torch.stack(outputs, dim=1)
# Drop-in replacement for self-attention.
# Notice: no QK^T matrix → no O(N²) cost.
# 'h' carries all information about the past in d_state floats.
Real Mamba: parallel scan for fast training, hardware-aware kernel, selective parameters that gate input flow. This minimal version is the conceptual core.
Fixed recurrent state is lossy, not an unlimited exact transcript. The simple recurrence is an educational SSM, not the full selective Mamba model.
Shrink the state to one number and the whole idea fits on a napkin: memory is a leaky running total.
| t | input xt | state ht | what happened |
|---|---|---|---|
| 1 | 1 | 0.5·0 + 1 = 1 | store the 1 |
| 2 | 0 | 0.5·1 + 0 = 0.5 | memory halves |
| 3 | 0 | 0.5·0.5 + 0 = 0.25 | halves again |
| 4 | 2 | 0.5·0.25 + 2 = 2.125 | new input dominates |
Unrolled: ht = Σs at−s xs. Old inputs fade by a factor a per step. This sum is a convolution — which is how SSMs train in parallel.
Half-life in steps = ln 0.5 / ln a.
a = 0.5 → 1 step. a = 0.99 → ≈ 69 steps.a — some forget fast, some remember long.| Context length | Llama-2 7B KV cache | Mamba-2.8B state |
|---|---|---|
| 4,096 tokens | 2 GB | ≈ 10 MB 64 layers × 5,120 × 16 ≈ 5.2M numbers — at any length |
| 32,768 tokens | 16 GB | |
| 131,072 tokens | 64 GB |
KV storage grows with retained context. This comparison assumes full retained histories at the specified lengths; context limits, eviction, or compression prevent literal unbounded growth in real serving systems.
Millions of numbers cannot store a 100k-token book word for word. Pure SSMs struggle with exact recall, like copying a long random string (Jelassi et al. 2024). That's why hybrids such as Jamba keep a few attention layers.
Solve 0.9n < 0.01 → n > ln 0.01 / ln 0.9 ≈ 43.7, so 44 steps.
Fixed recurrent state is lossy, not an unlimited exact transcript. The simple recurrence is an educational SSM, not the full selective Mamba model.
A learnable key-value store that lives outside the context window — written to during conversation, read from across sessions.
class MemoryBank(nn.Module):
"""Persistent KV bank — read by attention, write by gate."""
def __init__(self, d_model, capacity=256):
super().__init__()
self.capacity = capacity
self.d_model = d_model
# Slots stored as buffers (saved with state_dict)
self.register_buffer("keys", torch.zeros(capacity, d_model))
self.register_buffer("vals", torch.zeros(capacity, d_model))
self.register_buffer("size", torch.tensor(0))
self.write_gate = nn.Linear(d_model, 1)
def read(self, q): # q: (B, T, d)
if self.size == 0:
return torch.zeros_like(q)
K = self.keys[:self.size] # (M, d)
V = self.vals[:self.size]
scores = q @ K.T / (self.d_model ** 0.5) # (B, T, M)
return F.softmax(scores, dim=-1) @ V
def write(self, x): # x: (N, d) flattened
importance = torch.sigmoid(self.write_gate(x))
keep = (importance > 0.5).squeeze(-1)
new = x[keep]
n = min(new.size(0), self.capacity - self.size)
if n == 0: return
self.keys[self.size:self.size+n] = new[:n]
self.vals[self.size:self.size+n] = new[:n]
self.size += n
def save(self, path):
torch.save({"keys": self.keys, "vals": self.vals,
"size": self.size}, path)
The bank can be saved/loaded across sessions — that's what makes it persistent. Real systems add eviction policies, sharding, learned compression.
A drop-in transformer block that's adaptive (MoE FFN) AND has accumulating memory (external bank). ~80 lines of PyTorch.
Both adaptations are drop-in replacements. You can stack AnimaBlocks exactly like vanilla transformer blocks.
class AnimaBlock(nn.Module):
"""
Transformer block with:
- adaptive compute (MoE FFN)
- accumulating memory (external bank)
"""
def __init__(self, d_model, n_heads=4,
n_experts=8, top_k=2,
mem_capacity=256):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = nn.MultiheadAttention(
d_model, n_heads, batch_first=True)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = MoEFFN(d_model, n_experts, top_k)
self.memory = MemoryBank(d_model, mem_capacity)
def forward(self, x):
# 1. Recall: pull relevant past memory
recall = self.memory.read(x)
h = self.ln1(x + recall)
# 2. Causal attention sublayer (True = blocked)
T = x.size(1)
causal = torch.triu(torch.ones(T, T, dtype=torch.bool,
device=x.device), diagonal=1)
attn_out, _ = self.attn(h, h, h, attn_mask=causal,
need_weights=False)
x = x + attn_out
# 3. Adaptive FFN sublayer
x = x + self.ffn(self.ln2(x))
# 4. Persist what mattered
with torch.no_grad():
self.memory.write(x.reshape(-1, x.size(-1)))
return x
# Stack them like normal transformer blocks:
blocks = nn.ModuleList([
AnimaBlock(d_model=128) for _ in range(6)
])
# Forward pass:
for block in blocks:
x = block(x)
Writes run under no_grad, so saved slots are detached — no gradients flow through memory across batches. Without the causal mask a decoder would peek at future tokens during training.
These dated public examples illustrate published architectures and research proposals. This is not a verified inventory of live production systems; some entries are papers rather than deployed products.
| System | Adaptive compute | Memory accumulation | Notable |
|---|---|---|---|
| Mixtral 8×7B (Mistral) | MoE — 8 experts, top-2 | Standard KV cache | 47B total params, ~13B active. Open-weights. |
| DeepSeek-V3 | MoE — 256 experts, top-8 + 1 shared | Multi-head Latent Attention (compressed KV) | 671B total / 37B active. Open weights. |
| Mamba / Mamba-2 | — | Selective State-Space (recurrent) | Linear-time inference; competitive with transformers up to 7B. |
| Jamba (AI21) | MoE | Hybrid: SSM + attention layers | Best of both worlds — long context + adaptive compute. |
| RWKV | — | Linear-attention recurrent state | RNN-flavored transformer; very efficient inference. |
| MemGPT / Letta | — | External memory bank with self-managed read/write | Persistent personality / facts across sessions. |
| Memorizing Transformers | — | kNN over external memory cache (one upper layer) | Showed external memory works at scale (Wu et al. 2022). |
| Mixture of Depths (Google) | Per-token block routing | Standard KV cache | Variable depth per token. Paper reports matching baseline quality with far fewer FLOPs per forward pass. |
Dense attention transformers remain common, especially at smaller sizes. At the frontier, MoE has become a mainstream way to get cheap inference; SSM hybrids and external memory are still rarer and more experimental. Even "dense" models now differ a lot from GPT-2 (RoPE, GQA, SwiGLU, RMSNorm).
Each of these has bitten real projects. Several show up in the simplified code on earlier slides — on purpose, so you can spot them.
Router collapse. A few experts win early, get all the gradient, and win forever. Fix: load-balancing loss, router noise, or capacity limits.
Sparse routing does not remove expert weights. Placement, expert parallelism, quantization, and offloading determine where the parameters reside and when they are moved.
Dropped tokens. With a capacity factor, an overloaded expert skips extra tokens — they pass through only the residual. Too low a capacity silently hurts quality.
Early exit vs. KV cache. A token that exits at layer 6 has no K/V for layers 7+. Later tokens need them, so systems like CALM copy the exited state upward.
The gate never learns. In MemoryBank.write, the hard > 0.5 threshold under no_grad gives the gate zero gradient. Real systems use an auxiliary loss or a straight-through trick.
The bank fills up and freezes. The demo stops writing once all 256 slots are used — often after one batch. Real banks need eviction (FIFO, LRU, least-used).
Memory leaks between users. A shared bank written during one user's session can be read in another's. Persistent memory needs per-user scoping and deletion.
"Constant memory = infinite context." An SSM runs forever, but its fixed-size state keeps a lossy summary, not a perfect transcript.
Try each one before you open the answer.
4 / 64 = 1/16 ≈ 6%. Attention and embeddings are always fully used, so the whole-model fraction is higher.
Only the FFNs are copied into 8 experts. Attention, norms and embeddings are shared by all experts.
Uniform routing gives 1 before the coefficient, but the statistic can be below 1 for imbalanced routing. It is an auxiliary objective, not an exact inequality characterizing balance.
h₁ = 5, h₂ = 4, h₃ = 3.2. Each step keeps 80% of the state.
The transformer keeps K and V for every past token (the KV cache grows linearly). Mamba keeps one fixed-size state and overwrites it each step.
Exact recall — copying a long string or looking up a phone number from far back. Attention can look at any past token directly; an SSM only has its compressed state.
RAG retrieves external evidence—often text using embeddings, lexical search, or reranking—and conditions generation on it. A neural vector bank is a different design; neither retrieval nor write policy must be fixed or differentiable.
AnimaBlock?During training each token could attend to the tokens after it, so next-token prediction becomes copying. The model looks great in training and fails at generation.
| FFN | The feed-forward network in each block; what MoE replaces. |
| Expert | One of several FFNs in an MoE layer. |
| Router / gate | Small linear layer that scores experts (or tokens). |
| Top-k routing | Send each token to its k highest-scoring experts. |
| Active params | Weights actually used for one token. |
| Load-balancing loss | Extra loss term that spreads tokens across experts. |
| Capacity factor | Max tokens per expert per batch; overflow is dropped. |
| MoD | Mixture of Depths — only top-k tokens enter each block. |
| Early exit / ACT | Stop processing a token once it is confident enough. |
| KV cache | Saved keys and values of past tokens. |
| GQA / MLA | Ways to shrink the KV cache by sharing or compressing K/V. |
| SSM | State space model — a linear recurrence with a fixed-size state. |
| Selective SSM | SSM whose update depends on the input (Mamba). |
| Hybrid | Model mixing SSM and attention layers (Jamba). |
| Memory bank | Key-value store outside the context, read by attention. |
| Eviction | Rule for which memory slot to overwrite when full. |
MoEFFN in ~30 linesMinimalSSM as a recurrence primitiveMemoryBank with gated writes & attention readsAnimaBlock wiring it all togetherQuestions?
Can we select experts, accumulate state, and keep memory scoped to a user?
The file contains its own imports, data generator or fixture, implementation, and checks.
python3 -m venv .venv source .venv/bin/activate python -m pip install torch python adaptive_neural_infra.py python adaptive_neural_infra.py --self-test
Use Python 3.10+. On Windows, activate with .venv\Scripts\Activate.ps1. Required packages: torch>=2.2.
Read the contract first. Identify the inputs, intermediate state, and return value in this excerpt from the verified file.
def forward(self, inputs):
probabilities = self.router(inputs).softmax(-1)
weights, selected = probabilities.topk(self.k, dim=-1)
weights = weights/weights.sum(-1, keepdim=True)
result = torch.zeros_like(inputs)
# Invariant: result contains contributions of earlier experts only.
# Bound: remaining experts; each receives only its selected rows.
for index, expert in enumerate(self.experts):
rows, slots = (selected == index).nonzero(as_tuple=True)
if rows.numel():
contribution = weights[rows, slots, None] * expert(inputs[rows])
result = result.index_add(0, rows, contribution)
return result, probabilities, selectedMoE initial/final regression MSE: 3.9880120754241943 0.042410463094711304 Scalar state for [1,0,0,2]: ([1.0, 0.5, 0.25, 2.125], 2.125) Ada's value: Python Another user's value: None Valid top-1 routing statistic below 1: 0.9359999299049377 These primitives are educational; the recurrence is not a full Mamba implementation.
Output was captured from the complete program running in an isolated temporary directory. Floating-point digits and compressed sizes may vary across versions/hardware.
8 checks passed, including boundary cases and independent reference comparisons where applicable. These executed checks complement the reasoning; they are not a universal proof.
Reset a user's memory, exceed the capacity, and query another user's store. Then compare uniform and imbalanced routing-loss examples.
State lifetime, capacity/eviction, and user identity are explicit API choices. Uniform auxiliary routing loss equals 1 but is not its universal minimum.