A scan turns a sequence into all its running combinations. If the combine operation is associative — — you can group work freely and compute every prefix in doubling rounds:
Same lattice, richer — each element is the affine step , composed as (apply earlier, then later):
step() still exists for O(1)-memory streaming inference — training never runs sequentially.
A group = reversible operations closed under composition; Sₙ = all n! permutations of n objects. The word problem: the running product of a stream — our streaming-label setup.
S₃, S₄ are solvable — buildable from a tower of commutative layers; S₅ is not (Abel–Ruffini): the cliff two slides ahead. (Parity = the word problem of the 2-element group.)
Picture computation as gates stacked in layers: inputs at the bottom, answer at the top. Depth = the longest chain of dependencies — the part you cannot parallelize.
TC⁰ = computable at constant depth with counting gates. The theorem (not obvious!): a fixed scan stack's function always admits such a circuit; an RNN's chain is the depth.
A fixed stack of parallel scans is a constant-depth parallel circuit — the class TC⁰. The transition is chosen before reading memory, and stacking layers doesn't change that:
S3 (solvable) sits inside TC⁰ — the ladder ahead is about which parameterizations realize and train it. The question: how much GRU-ness can you buy back inside the scan?
Multi-seed means, ≥3 seeds per claim (a seed = one independent run: fresh init + data order). Rotation-family rows select the best checkpoint by val@128: longer than train (64), so it rewards generalizing solutions — shorter than every reported test length, so selection can't leak.
These are real tensors from seed 0's first batch — integer ids in, integer ids out, a label at every step. Nothing else reaches the model.
mixer="log")Log-space scan → coefficients are positive: . The state can only decay and blend — it cannot flip, cannot rotate.
Result: chance on both tasks, at any depth.
The scan is fine. The coefficient family is the cage — a parameterization limit, no amount of data fixes it.
mixer="signed")Linear-space scan, transition goes negative:
Length generalization comes from attractors at exact solutions, not from fit accuracy.
Recovery: matrix transitions. — same doubling scan, but composition is matrix multiplication (matrix_scan).
The dial's final pose is the running group element — a linear readout reads the label. This is exactly RotationMinGRU's transition, : the rotation supplies the turn, the factor (eigenvalue → −1) supplies the flip.
mixer="rotation")2×2 blocks: . S3 ≅ D3, the triangle's turns + mirrors — and D3 embeds in O(2), the plane's rotations + reflections → one layer can be exact.
Representable ≠ trainable — two protocol pieces:
0.958 mean @16× (n=8); 1 of 8 seeds lands the exact automaton. L=1 only: stacking snapped layers compounds the snap discontinuity (the code warns).
One interface: MinGRUBlock(d_model, mixer=...) — stackable, mixable per layer. (Impatient for "which one do I use"? — a practical chooser table closes the deck.)
A Latin square (think Sudoku row/column rule): every operation appears exactly once per row and column — so the distribution of ops given one symbol is uniform. No shortcut exists.
S3-hier needs two jobs at once — and each half fails alone:
...but with a continuous 2D composer: 1 of 12 seeds fits.
The bottleneck moved from expressivity to trainability.
Expressivity: do weights exist? (theorems.) Trainability: does SGD find them? (only seeds answer.)
The smallest possible rotation: pick one coordinate plane, rotate by θ, leave every other direction exactly alone.
mixer="givens")Compose the atoms — three brick-wall rounds per 8-dim block, angles from one linear head:
— the pure rotations of 8-dim space (det +1, no reflections); continuous, no snap, same parallel scan. (Not from the three papers — built here from the 1-of-12 finding: DeltaProduct's products-of-atoms move on the rotation atom; brick-wall mesh from EUNN — Jing et al. 2017 — and Clements et al. 2016.)
Givens is the small-state group-composition specialist — its edge is map-richness at a matched state, not a ladder summit. The broad cross-task workhorse is the delta rule (next, and the benchmark round ahead).
Numerical linear algebra has two orthogonal atoms — Givens rotations and Householder reflections. The second powers the DeltaNet family (β = the token's write strength):
Different lever: delta's dial is state size and Householder count (nh), not the rotation family's map-richness. Held to a matched 64-element state it trails Givens on S3-hier; grown to its native state — and across the public benchmark suite (the cross-task round ahead) — the delta family is the broad workhorse, while Givens stays the small-state specialist.
State tracking = making the composition of these maps mirror the group. Exactness, richness, mirror-atoms, and storage are four different bets on how to survive T steps.
Same task, same 64-element state, same protocol, same ["signed", ·] stack — only the composer changes. (Middle bar: the delta rule, held to the matched 64-element state.) The significant bracket is within-family (2D→8D rotation, p ≈ 0.009). Givens-vs-delta is suggestive only — p ≈ 0.22, params unmatched (14.6k vs 3.3k): not a like-for-like race. [Superseded at n=36 on GPU: givens 25/36 vs delta 10/36, p=0.00084 — see "Choosing a mixer."]
No attractor → -sized angle errors compound as . Reliability vs. exactness — you currently pick one.
The case for the parallel composer was never CPU speed: depth on parallel hardware, 8/12 vs 1/12 fit reliability, best fit-only length-gen profile. Update: the packaged chunked-WY forward changes the CPU number too — 0.0577 s vs eager Givens' 0.9493 s at this shape (~16×), pinned CPU stratum. GPU is a different story — next slide.
| arm | fits | per-seed wall |
|---|---|---|
| givens@64, Triton scan | 25/36 | 15.3 s |
delta@64-matched, torch.compile |
10/36 | 17.9 s |
delta@1024 (native), torch.compile |
35/36 | 17.2 s |
Cost is at parity across all three arms (~15–18 s/seed) — the CPU stratum's 16× cost gap for Givens disappears once it runs its Triton scan path, and delta's torch.compile path is no cheaper at these shapes. Reliability still separates the mixers: delta at native state remains the most reliable composer measured (35/36); at matched 64-element state, givens is now decisively more reliable (25/36 vs 10/36, Fisher exact ).
Delta runs via torch.compile, not a shipped Triton kernel — a fusion-headroom probe found torch.compile recovers 70–91% of the eager-vs-floor headroom, so a delta Triton kernel wasn't judged worth building yet.
torch 2.8 / NVIDIA L4 / Triton 3.4 — a separate stratum, not comparable to this deck's pinned-CPU numbers.
Past S3-hier, the accepted benchmark round runs the whole mixer family on three public tasks. The pattern is one-sided — the delta family is the broad workhorse; Givens wins none of them:
| task (L4 stratum) | what it stresses | Givens | delta family |
|---|---|---|---|
| MQAR | associative recall (key→value) | 0/36 | delta / signed-delta 36/36 |
| psMNIST | long pixel accumulation | 0/12 (raw 0.29, worst) | signed-delta 12/12 (best) |
| S5 | 5-cup word problem (the hard cliff) | signed-givens 1/36 |
signed-delta-nh4 7/36 |
Delta's dial is state size and Householder count (nh): a -cycle needs reflections, so the matched nh=2 arm is 0/36 by design, and nh=4 is what first gets real traction on S5 (7/36 vs 1/36 — still partial, as theory predicts for this cliff). Givens' win stays real but narrow — small-state group-composition richness (S3-hier, 25/36 vs 10/36). Two dials, not one winner.
torch 2.8 / NVIDIA L4 / Triton 3.4 — a separate stratum, not comparable to this deck's pinned-CPU or S3-hier numbers.
Real event streams are irregularly spaced — a 3-second gap and a 3-day gap should not move memory equally. All four mixers accept an optional decay channel:
decay="fixed" — = decay_rate, a buffer, zero new parametersdecay="learnable" — , a smooth ramp keeping : per channel (log/signed), per block (rotation/givens)delta_t provided, else ValueErrorlog1p_delta=True compresses wide gap ranges before scalingInitialization matters (measured): start near persist-by-default (decay_rate=0.05, ) and let gradients pull decay in where time pays. Started at heavy decay, the mechanism barely moved; with the small init it wins consistently on session-boundary tasks.
Promoted default: delta at native/full state — most reliable trained composer measured (GPU: 35/36 fits; CPU: 12/12), cost flat in state size. Two axes still move you off the default:
| your problem looks like | reach for |
|---|---|
| decay-and-blend memory, positive evidence | log (base) |
| sign-flip structure, must hold at length | signed |
| order-sensitive composition, exactness required, retries OK | rotation (snap + CKPT, L=1) |
| state must stay small (matched 64-elem), reliability first | givens — decisively more reliable there (GPU 25/36 vs delta 10/36, p=0.00084) |
| state free to grow, reliability + accuracy at/near train length | delta at native state (promoted default) |
| extreme-length fit-only extrapolation (≥16× train length), small-state OK | givens or delta@64 — their fit cohorts hold up best (0.679 / 0.699 vs delta@1024's 0.570) |
| irregular event times anywhere above | decay="learnable", small decay_rate, log1p_delta=True |
| none of the above, scan not required | a GRU — still the ceiling |
GPU numbers: torch 2.8, NVIDIA L4, Triton 3.4 stratum — not comparable to this deck's pinned-CPU numbers elsewhere. Cost is at parity across arms on that stratum (~15–18 s/seed); the CPU-side chunked-WY speed advantage does not carry over.
stack = MinGRUStack(32, 256, n_layers=2, mixer=["signed", "givens"])
y, state = stack(x) # parallel training path
y_t, state = stack.step(x_t, state) # O(1) streaming inference
# promoted default: full-state delta, single mixer
stack = MinGRUStack(32, 256, n_layers=2, mixer="delta")
# full self-test suite (algebra, scan≡sequential, forward≡step, decay contracts)
uv run --python 3.12 --with 'torch==2.5.1' python min_gru.py
# any single cell: task × model (CKPT=1 = the best-val@128 checkpoint protocol)
uv run --python 3.12 --with 'torch==2.5.1' python probes.py parity minGRU-signed
CKPT=1 uv run --python 3.12 --with 'torch==2.5.1' python probes.py S3-hier minGRU-hetero-sg8
# the whole grid
uv run --python 3.12 --with 'torch==2.5.1' python probes.py all
github.com/chris-santiago/minGRU — the README carries every number here, multi-seed and protocol-pinnedQuestions → the README's "ladder" section is this deck in prose.