Mixers¶
The five sequence mixers and the two container modules. Signatures and parameter contracts are generated from the source docstrings; for why each mixer behaves as it does, see the explanations, and for which to use, Choose a mixer.
mingru.MinGRU ¶
MinGRU(input_size: int, hidden_size: int, bias: bool = True, learnable_h0: bool = False, decay: Decay = None, decay_rate: float = 1.0, log1p_delta: bool = False)
Bases: DecayMixin, Module
minGRU layer: log-space parallel-scan training, recurrent inference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Dimensionality of the inputs |
required |
hidden_size
|
int
|
Dimensionality of the hidden states |
required |
bias
|
bool
|
Whether the two linear maps carry bias terms. |
True
|
learnable_h0
|
bool
|
If True, the module owns a learned initial state, stored as an
unconstrained pre-activation and mapped through |
False
|
decay
|
(fixed, learnable, None)
|
Exponential time decay of the carried state, driven by
|
"fixed"
|
decay_rate
|
float
|
Fixed decay rate, or the learnable rate's init target. |
1.0
|
log1p_delta
|
bool
|
If True, |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Shapes: forward maps x (B, T, input_size) with optional
h_0 (B, 1, hidden_size) to (B, T, hidden_size); step
maps x_t (B, input_size) with optional h_prev
(B, hidden_size) to (B, hidden_size). delta_t follows the
same optionality: (B, T) or (B, T, 1) for forward,
(B,) or (B, 1) for step. Invalid delta_t entries
(negative/NaN/inf) are always sanitized to finite,
non-negative values; the courtesy warning about them is CPU-only
(silent on CUDA, avoiding a host sync) — the sanitizing clamp
itself always applies, on every device.
References
Feng et al., "Were RNNs All We Needed?", arXiv:2410.01201.
Heinsen (2023) -- the log-space parallel scan used by forward.
Examples:
>>> import torch
>>> from mingru import MinGRU
>>> layer = MinGRU(input_size=32, hidden_size=64)
>>> x = torch.randn(4, 128, 32)
>>> h = layer(x) # parallel training-mode forward
>>> tuple(h.shape)
(4, 128, 64)
>>> h_t = layer.step(x[:, 0]) # one O(1)-memory streaming step
>>> tuple(h_t.shape)
(4, 64)
forward ¶
Parallel (training-mode) forward over a full sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Input sequence, shape |
required |
h_0
|
Tensor
|
Initial hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gaps preceding each event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
All hidden states |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
ValueError
|
If |
step ¶
Single recurrent step for streaming/token-by-token inference.
Numerically equivalent to forward() one timestep at a time
(g is applied to the candidate to match the log-space
parameterization). h_prev is a real hidden state — the same
convention as forward()'s h_0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Input at the current timestep, shape |
required |
h_prev
|
Tensor
|
Previous hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gap preceding this event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
New hidden state |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
mingru.SignedMinGRU ¶
SignedMinGRU(input_size: int, hidden_size: int, bias: bool = True, learnable_h0: bool = False, coupled: bool = False, decay: Decay = None, decay_rate: float = 1.0, log1p_delta: bool = False)
Bases: DecayMixin, Module
minGRU variant with signed diagonal transitions (linear-space scan).
The recurrence is h_t = a_t * h_{t-1} + z_t * h~_t with
a_t ranging over (-1, 1) instead of (0, 1). Motivated by
Merrill, Petty & Sabharwal (2024): still diagonal, hence commutative
and TC0, but negative eigenvalues restore per-layer parity /
sign-alternation dynamics (the mechanism identified by Grazzi et al.,
2025).
Two parameterizations of a_t are available:
coupled=False(default):a_t = tanh(Linear_s(x_t)), the eigenvalue decoupled from the update gatez_t. This is the experimentally superior form (parity accuracy @1024: 0.61 -> 0.996, 6-seed mean, current-env) and is now the default.coupled=True:a_t = (1 - z_t) * tanh(Linear_s(x_t)), the original parameterization (eigenvalue coupled to the update gate, as in the vanilla minGRU's(1 - z_t)retention coefficient). Bit-exact reproduction of the pre-promotion class: identical parameter shapes and construction order (linear_z,linear_h,linear_s), so identical seeds give identical weights.
When the sign head saturates positive, the coupled form reduces to
the vanilla (Appendix A) minGRU's retention dynamics (a = 1 - z);
the decoupled form instead saturates to a = 1, a perfect
integrator — reaching the interval boundary is exactly what the
coupling prevents, and why the decoupled form length-generalizes.
States are unconstrained reals: no g, no positivity checks, no
underflow handling. Same forward/step API and shapes as MinGRU;
h_0 is any real state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Dimensionality of the inputs |
required |
hidden_size
|
int
|
Dimensionality of the hidden states |
required |
bias
|
bool
|
Whether the three linear maps carry bias terms. |
True
|
learnable_h0
|
bool
|
If True, the module owns a learned initial state (an
unconstrained parameter used directly — no |
False
|
coupled
|
bool
|
If True, use the legacy coupled eigenvalue
|
False
|
decay
|
(fixed, learnable, None)
|
Exponential time decay of the carried state:
|
"fixed"
|
decay_rate
|
float
|
Fixed decay rate, or the learnable rate's init target. |
1.0
|
log1p_delta
|
bool
|
If True, |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Parameter count is 3 linear heads vs. MinGRU's 2 (the extra sign
head) — account for this in parameter-matched comparisons. Invalid
delta_t entries (negative/NaN/inf) are always sanitized
to finite, non-negative values; the courtesy warning about them is
CPU-only (silent on CUDA, avoiding a host sync) — the sanitizing
clamp itself always applies, on every device.
References
Merrill, Petty & Sabharwal (2024) -- expressivity gains from signed (negative-eigenvalue) diagonal linear recurrences. Grazzi et al. (2025) -- sign-alternation / parity dynamics enabled by negative transition eigenvalues.
Examples:
>>> import torch
>>> from mingru import SignedMinGRU
>>> layer = SignedMinGRU(input_size=32, hidden_size=64) # decoupled default
>>> x = torch.randn(4, 128, 32)
>>> tuple(layer(x).shape)
(4, 128, 64)
forward ¶
Parallel forward over a full sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Input sequence, shape |
required |
h_0
|
Tensor
|
Initial hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gaps preceding each event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
All hidden states |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
step ¶
Single recurrent step; same real-state convention as forward().
Computed from the same _coeffs helper forward() uses, so
decayed and non-decayed dynamics cannot drift between the two
call paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Input at the current timestep, shape |
required |
h_prev
|
Tensor
|
Previous hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gap preceding this event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
New hidden state, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
mingru.RotationMinGRU ¶
RotationMinGRU(input_size: int, hidden_size: int, bias: bool = True, snap: tuple[int, ...] | None = (2, 3, 4, 6), decay: Decay = None, decay_rate: float = 1.0, log1p_delta: bool = False)
Bases: DecayMixin, Module
minGRU variant with 2x2 block rotation transitions (non-diagonal).
State is n = hidden_size / 2 independent planar (2D) blocks.
Per block, the recurrence is a full 2x2 affine map instead of a
scalar one:
M_t = R(theta_t) @ diag(1, tanh(u_t))
h_t = M_t @ h_{t-1} + b_t, b_t = z_t * Linear_h(x_t)
(h_{t-1}, h_t, b_t viewed as 2-vectors per block).
Unlike MinGRU and SignedMinGRU, per-block transitions do
not commute (2x2 rotation/reflection matrices form a non-abelian
group under composition), so this mixer — with a non-commutative
parallel scan, matrix_scan — can represent state-tracking
automata over non-abelian groups that a diagonal (commutative)
scan provably cannot. D3 (isomorphic to S3, the smallest
non-abelian group) embeds in O(2), so one layer of this mixer can
represent the S3 running product exactly; see
experiments/SUMMARY.md for the mechanism verification
(per-block matrices extracted from a trained model satisfy the D3
composition table to ~1e-4).
Angle snapping (snap): with snap set, theta_t is
quantized per block to an exact multiple of 2*pi/K via a
straight-through estimator — forward uses the snapped angle,
gradient passes through the pre-snap "soft" angle unchanged. K
is cycled across blocks from the snap tuple (block j uses
snap[j % len(snap)]). This manufactures attractors at exact
group elements -- exactly for the snapped angle; the scale channel
tanh(u_t) reaches a group element (+-1) only as the u-head
saturates, so full-map exactness is empirical -- the same way
tanh's asymptote manufactures an
attractor at eigenvalue -1 for SignedMinGRU: without snapping,
plain rotation angles have no attractor and drift with sequence
length (error compounds with T). The snap grid must contain the
group being tracked: choose K values whose rotations
(2*pi/K) generate, or coincide with, the target group's
rotation subgroup, or the exact automaton is not representable on
the grid at all (e.g. tracking Z/5 needs a multiple of 5 in
snap). The default snap=(2, 3, 4, 6) was chosen for the
D3/S3 task; other state-tracking targets need their own grid.
snap=None gives continuous (unsnapped) rotations — a
legitimate, documented ladder rung, but angles then drift under
length generalization since there is no attractor at the task's
true transition angle; use only when exact length generalization
is not required.
Depth: single-layer use is the recorded baseline; stacks holding
rotation blocks (with or without other mixer types) are validated
at L=2 on the S3 probe under the best-val@128 protocol, deeper is
untested. The straight-through discontinuity can compound across
rotation layers, so multi-rotation stacks warn at construction —
see MinGRUStack and the README's Rotation variant section.
Training protocol: the exact automaton is reachable but is NOT a
stable attractor of standard training — runs wander in and out of
it during optimization. The validated protocol is best-checkpoint
selection by validation accuracy at a length LONGER than the
training length (e.g. T=128 when training at T=64; not one of the
eventual test lengths), evaluated over the full step budget instead
of early-stopping, plus a retry-on-flag rule: a best validation
score at that checkpoint length below 1.0 flags the run as failed.
The flag is one-directional: a sub-1.0 score reliably marks a bad
run, but a perfect checkpoint score does not guarantee exact length
generalization (in the recorded evidence every seed passed the
checkpoint yet most still decayed at the longest lengths). See
experiments/SUMMARY.md for the full protocol, per-seed success
rate, and mechanism verification.
Excludes refuted experiment-loop mechanisms: no full orthogonality
constraint (ortho), no grid-attraction regularizer (reg),
no post-hoc projection/ablation masks. All were tried and either
hurt length generalization or were redundant with the best-val
selection protocol above; see experiments/SUMMARY.md rounds 5
and 8.
h_0 is an unconditional learnable parameter (no
learnable_h0 flag, unlike the module's other two mixers):
h_0 = 0 has no orbit under the group action (a fixed point
cannot demonstrate state tracking), and a state vector lying on a
reflection axis collapses reflections onto rotations. A random
nonzero learned vector avoids both failure modes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Dimensionality of the inputs |
required |
hidden_size
|
int
|
Dimensionality of the hidden states |
required |
bias
|
bool
|
Whether the four linear maps carry bias terms. |
True
|
snap
|
tuple of int, or None
|
Per-block angle-snap grid orders |
(2, 3, 4, 6)
|
decay
|
(fixed, learnable, None)
|
Exponential time decay of the carried state: |
"fixed"
|
decay_rate
|
float
|
Fixed decay rate, or the learnable rate's init target. |
1.0
|
log1p_delta
|
bool
|
If True, |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Parameter count is 4 linear heads (z, h, theta, u) vs.
SignedMinGRU's 3 — account for this in parameter-matched
comparisons. Same forward/step shapes as the module's other
mixers: forward maps x (B, T, input_size) with optional
h_0 (B, 1, hidden_size) to (B, T, hidden_size); step
maps x_t (B, input_size) with optional h_prev
(B, hidden_size) to (B, hidden_size). delta_t follows the
same optionality: (B, T) or (B, T, 1) for forward,
(B,) or (B, 1) for step. Invalid delta_t entries
(negative/NaN/inf) are always sanitized to finite,
non-negative values; the courtesy warning about them is CPU-only
(silent on CUDA, avoiding a host sync) — the sanitizing clamp
itself always applies, on every device.
References
Merrill, Petty & Sabharwal (2024) and Grazzi et al. (2025) -- state tracking over non-abelian groups needs non-commutative (matrix) transitions, which a diagonal scan cannot express.
Examples:
>>> import torch
>>> from mingru import RotationMinGRU
>>> layer = RotationMinGRU(input_size=32, hidden_size=64) # snap=(2,3,4,6)
>>> x = torch.randn(4, 128, 32)
>>> tuple(layer(x).shape)
(4, 128, 64)
forward ¶
Parallel forward over a full sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Input sequence, shape |
required |
h_0
|
Tensor
|
Initial hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gaps preceding each event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
All hidden states |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
step ¶
Single recurrent step; same real-state convention as forward().
Computed from the same _coeffs helper forward() uses
(applied per-step instead of over the full sequence), so the
two paths cannot drift apart — mirrors how SignedMinGRU
shares _coeffs between its forward/step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Input at the current timestep, shape |
required |
h_prev
|
Tensor
|
Previous hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gap preceding this event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
New hidden state, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
mingru.GivensMinGRU ¶
GivensMinGRU(input_size: int, hidden_size: int, bias: bool = True, block_size: int = 8, rounds: int = 3, decay: Decay = None, decay_rate: float = 1.0, log1p_delta: bool = False)
Bases: DecayMixin, Module
minGRU variant with k-dim block-rotation transitions (non-diagonal).
The k > 2 generalization of RotationMinGRU's snap=None
regime. State is n_blocks = hidden_size / block_size independent
blocks of k = block_size dims (64 state elements total at the
repo's d_model = 512: the same per-token state as every other
promoted mixer). Per block and token the transition is a product of
rounds layers of Givens rotations on fixed brick-wall planes,
M_t = G_{rounds-1} @ ... @ G_1 @ G_0,
h_t = M_t @ h_{t-1} + b_t, b_t = sigmoid(z_t) * Linear_h(x_t)
(h_{t-1}, h_t, b_t viewed as k-vectors per block).
Each round r rotates a set of disjoint coordinate planes by
input-dependent angles from one linear head: even rounds pair planes
(0,1),(2,3),...; odd rounds the staggered
(1,2),(3,4),...,(k-1,0). Every G_r is orthogonal with
determinant +1 by construction, so M_t is exactly
special-orthogonal — continuous, with no straight-through snap.
The brick-wall plane layout is the standard rectangular mesh from
the orthogonal/unitary-RNN literature (EUNN: Jing et al. 2017,
arXiv:1612.05231; mesh design: Clements et al. 2016) built from
Givens plane rotations (Golub & Van Loan, Matrix Computations);
the departure here is input-dependent angles per token.
Where RotationMinGRU manufactures attractors at exact group
elements by snapping 2x2 angles, GivensMinGRU deliberately does
not: each per-token map lives on a rounds * block_size / 2-angle
submanifold of SO(k) — 12 of SO(8)'s 28 dimensions at the
defaults; products of enough Givens rotations (about k - 1
brick-wall rounds) generate all of SO(k), so three rounds is a
deliberate budget rather than full per-token coverage — giving
richer non-abelian per-token maps at matched state capacity, at the
cost of having no attractor at any particular
transition — angles drift under length generalization exactly as
RotationMinGRU(snap=None)'s do. Like the other matrix-transition
mixers it runs on a non-commutative parallel scan
(matrix_affine_scan, the k-dim generalization of
matrix_scan), so it can represent non-abelian state tracking a
diagonal (commutative) scan provably cannot.
Depth: unlike RotationMinGRU, stacks holding multiple Givens
blocks construct WITHOUT a warning — the multi-rotation
UserWarning names straight-through snap compounding, which the
continuous (unsnapped) Givens transition does not have (see
MinGRUStack).
h_0 is an unconditional learnable parameter (no learnable_h0
flag, same convention and rationale as RotationMinGRU):
h_0 = 0 has no orbit under the group action, so a zero initial
state cannot demonstrate state tracking; a random nonzero learned
vector avoids that failure mode.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Dimensionality of the inputs |
required |
hidden_size
|
int
|
Dimensionality of the hidden states |
required |
bias
|
bool
|
Whether the three linear maps carry bias terms. |
True
|
block_size
|
int
|
Per-block state dimension |
8
|
rounds
|
int
|
Number of brick-wall Givens layers composed per transition; must
be at least 1. Full |
3
|
decay
|
(fixed, learnable, None)
|
Exponential time decay of the carried state: |
"fixed"
|
decay_rate
|
float
|
Fixed decay rate, or the learnable rate's init target. |
1.0
|
log1p_delta
|
bool
|
If True, |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Same forward/step shapes as the module's other mixers: forward
maps x (B, T, input_size) with optional h_0 (B, 1,
hidden_size) to (B, T, hidden_size); step maps x_t (B,
input_size) with optional h_prev (B, hidden_size) to (B,
hidden_size). delta_t follows the same optionality and shapes
as RotationMinGRU ((B, T) or (B, T, 1) for forward;
(B,) or (B, 1) for step), with the identical
decay/delta_t pairing contract (ValueError both directions,
see _validate_delta_t_pairing) and CPU-only invalid-entry
warning. Parameter count is 3 linear heads (theta, z, h): the angle
head emits n_blocks * rounds * (block_size / 2) angles.
References
Jing et al. (2017), "Tunable Efficient Unitary Neural Networks (EUNN)", arXiv:1612.05231 -- the brick-wall mesh of plane rotations. Clements et al. (2016) -- the rectangular (brick-wall) mesh design. Golub & Van Loan, Matrix Computations -- Givens plane rotations.
Examples:
>>> import torch
>>> from mingru import GivensMinGRU
>>> layer = GivensMinGRU(input_size=32, hidden_size=64) # block_size=8, rounds=3
>>> x = torch.randn(4, 128, 32)
>>> tuple(layer(x).shape)
(4, 128, 64)
forward ¶
Parallel forward over a full sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Input sequence, shape |
required |
h_0
|
Tensor
|
Initial hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gaps preceding each event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
All hidden states |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
step ¶
Single recurrent step; same real-state convention as forward().
Computed from the same _coeffs helper forward() uses
(applied per-step instead of over the full sequence), so the two
paths cannot drift apart — mirrors RotationMinGRU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Input at the current timestep, shape |
required |
h_prev
|
Tensor
|
Previous hidden state, shape |
None
|
delta_t
|
Tensor
|
Time gap preceding this event, shape |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
New hidden state, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
mingru.DeltaMinGRU ¶
DeltaMinGRU(input_size: int, hidden_size: int, bias: bool = True, n_heads: int = 4, nh: int = 1, d_k: int | None = None, d_v: int | None = None, chunk_size: int = 64, decay: Decay = None, decay_rate: float = 1.0, log1p_delta: bool = False)
Bases: Module
DeltaNet/DeltaProduct generalized-Householder delta-rule mixer.
Departs from the other four mixers' diagonal/rotation-scan
mechanism entirely: state is a per-head d_k x d_v
associative-memory matrix H (not a vector), updated by nh
generalized-Householder rank-1 corrections per token rather than a
linear/affine scan. Reimplements Yang et al.'s DeltaNet (nh=1)
and Siems et al.'s DeltaProduct generalization (nh>1) as a
package citizen, mirroring the parameter layout of the lab's
DeltaNetMixer (experiments/variants.py, not imported here)
so state dicts are relatable across lab and package.
Per head, per token t, in order for micro-step j = 1..nh:
k_{t,j} = normalize(Linear_k^(j)(x_t)) # L2, last dim
v_{t,j} = Linear_v^(j)(x_t)
beta_{t,j} = 2 * sigmoid(Linear_beta^(j)(x_t)) # in (0, 2)
H <- (I - beta_{t,j} k_{t,j} k_{t,j}^T) H + beta_{t,j} k_{t,j} v_{t,j}^T
beta = 2 with a unit k makes the update an exact Householder
reflection (I - 2 k k^T is orthogonal, determinant -1); (0,
2) interpolates between the identity (beta=0, no update) and a
full reflection. Readout after the token's final micro-step is
y_t = H_t^T q_t per head (q_t = Linear_q(x_t)), heads
concatenated and projected by out_proj to hidden_size. H
is zero-initialized (empty associative memory) unless h_0 is
supplied -- unlike RotationMinGRU/GivensMinGRU, there is no
intrinsic learned initial state: H = 0 is the delta rule's
natural origin, not a degenerate point the way it is for a group
action.
step implements the sequential per-token recurrence above
directly (correct, O(T * nh) per-sequence cost) and is
forward's correctness oracle. forward instead runs the
chunked-WY parallel form (the standard efficient algorithm for this
recurrence -- Yang et al. 2024, arXiv:2406.06484): the nh * T
ordered rank-1 updates are processed chunk_size tokens at a
time, each chunk's cumulative transition carried via one
unit-lower-triangular solve (the UT transform) instead of a
per-token loop. Both forms are exactly the same function
(semantically identical within floating-point tolerance,
chunk_size has no effect on the result) and neither ever
materializes a per-token (or per-chunk) d_k x d_k transition
matrix: each micro-step is a rank-1 correction, and the chunked
form's UT transform operates on nh * chunk_size-sized matrices
built from k/v/beta, never on an explicit d_k x d_k
matrix. The fp32 agreement is chunk-size-bounded at large T,
though: at T = 1024 with the default chunk_size = 64 the
sequential/chunked deviation is ~3e-6, but a single-chunk run
(chunk_size >= T) at the same T accumulates to ~1.2e-5
through the one (nh * C)-deep forward substitution the UT
transform solves in a single shot rather than T separate
nh-deep ones -- still inside the repo's atol=1e-5 forward
tolerance, but close enough to it that the default (not an
oversized) chunk size is recommended for long sequences.
Time decay is not supported: the constructor rejects any decay is
not None (decay/decay_rate/log1p_delta are accepted
only for signature uniformity with the other mixers -- folding a
per-token decay into the delta-rule composition is unimplemented),
and both forward/step reject any delta_t unconditionally.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Dimensionality of the inputs |
required |
hidden_size
|
int
|
Output dimensionality ( |
required |
bias
|
bool
|
Whether every linear head ( |
True
|
n_heads
|
int
|
Number of parallel associative-memory heads. |
4
|
nh
|
int
|
Number of generalized-Householder micro-steps per token
( |
1
|
d_k
|
int
|
Per-head key/query dimension. Defaults to |
None
|
d_v
|
int
|
Per-head value dimension. Defaults to |
None
|
chunk_size
|
int
|
Number of tokens processed per UT-transform chunk in the
chunked-WY |
64
|
decay
|
(fixed, learnable, None)
|
Must be |
"fixed"
|
decay_rate
|
float
|
Unused ( |
1.0
|
log1p_delta
|
bool
|
Unused ( |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
carries_matrix_state = True (class attribute): the state
forward(..., return_state=True) returns and step carries is
the flattened H matrix, not a readout vector -- y_t != h_t,
unlike the other four mixers, where the scanned state doubles as
the output. forward maps x (B, T, input_size) with optional
h_0 (B, 1, n_heads*d_k*d_v) (flattened in (n_heads, d_k,
d_v) row-major order) to y (B, T, hidden_size), or (y,
h_T) when return_state=True. step maps x_t (B,
input_size) with optional h_prev (B, n_heads*d_k*d_v) to
(y_t, h_t), each following the same flattening. Chaining
forward(x2, h_0=s) after (y1, s) = forward(x1,
return_state=True) equals forward(cat(x1, x2)) on both outputs
and gradients.
On CUDA, torch.compile is the recommended execution path (it
recovers most of the chunked-WY forward's fusion headroom with no
engineering). A hand-written Triton kernel for the forward also
exists: under MINGRU_SCAN=auto it engages only in a narrow,
measured win region (long sequences with narrow head dims), where it
is faster than eager and lighter on memory; MINGRU_SCAN=triton
forces the full kernel envelope; MINGRU_SCAN=eager disables it.
The kernel does not reach torch.compile-class speed and is not a
substitute for it -- see the docs' "Choose a mixer" guide (GPU
execution path) for the full picture.
References
Yang et al., "Parallelizing Linear Transformers with the Delta Rule over Sequence Length" (DeltaNet), arXiv:2406.06484. Siems et al., "DeltaProduct: Increasing the Expressivity of DeltaNet Through Products of Householders" (DeltaProduct).
Examples:
>>> import torch
>>> from mingru import DeltaMinGRU
>>> layer = DeltaMinGRU(input_size=32, hidden_size=64, n_heads=4)
>>> x = torch.randn(4, 128, 32)
>>> tuple(layer(x).shape)
(4, 128, 64)
forward ¶
forward(x: Tensor, h_0: Tensor | None = None, delta_t: Tensor | None = None, return_state: bool = False) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]
Chunked-WY forward over a full sequence.
Computes exactly the function step computes token-by-token
(chunk_size is a performance-only knob -- see
_forward_chunked), via the chunked-WY UT transform instead
of a per-token Python loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Input sequence, shape |
required |
h_0
|
Tensor
|
Initial per-head associative-memory state, flattened shape
|
None
|
delta_t
|
Tensor
|
Must be |
None
|
return_state
|
bool
|
If True, also return the final state |
False
|
Returns:
| Type | Description |
|---|---|
torch.Tensor or tuple of torch.Tensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
step ¶
step(x_t: Tensor, h_prev: Tensor | None = None, delta_t: Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]
Single recurrent step; same real-state convention as forward().
Computed from the same _coeffs helper forward() uses
(applied once instead of over the full sequence). Unlike the
other mixers, forward() does not also reuse this method's
per-micro-step update (_micro_step) -- it runs the
chunked-WY UT transform instead (_forward_chunked), a
different but algebraically equivalent computation -- so
forward/step parity here is guaranteed by the WY-transform
derivation, not by shared code, and is pinned by the dual
sequential/affine-scan oracle tests (TestDeltaMinGRU,
TestMixerOracles).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Input at the current timestep, shape |
required |
h_prev
|
Tensor
|
Previous per-head associative-memory state, flattened shape
|
None
|
delta_t
|
Tensor
|
Must be |
None
|
Returns:
| Type | Description |
|---|---|
tuple of torch.Tensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
mingru.MinGRUBlock ¶
MinGRUBlock(d_model: int, mlp_expansion: int = 4, dropout: float = 0.0, learnable_h0: bool = False, mixer: str = 'log', mixer_kwargs: dict | None = None)
Bases: Module
Pre-norm residual block: LN -> minGRU -> +x, then LN -> MLP -> +x.
The minGRU is the (linear-in-state) sequence mixer; the MLP is the position-wise channel mixer. Because the scan's transition is diagonal, cross-channel interaction happens only here and in the next block's input projections.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
d_model
|
int
|
Residual stream width (the minGRU maps |
required |
mlp_expansion
|
int
|
Hidden width multiplier for the MLP. |
4
|
dropout
|
float
|
Applied after the minGRU output and inside the MLP. |
0.0
|
learnable_h0
|
bool
|
Routed to the block's mixer when |
False
|
mixer
|
(log, signed, rotation, givens, delta)
|
Selects the sequence mixer: |
"log"
|
mixer_kwargs
|
dict
|
Extra constructor kwargs forwarded to the selected mixer class
(e.g. |
None
|
Notes
delta_t threads through forward/step straight to the
mixer, unchanged; the block performs no decay-mode validation of
its own. The mixer's own decay/delta_t pairing contract
(ValueError both directions, see _validate_delta_t_pairing)
is what fires whether the block is used standalone or inside a
MinGRUStack.
Matrix-state mixers (carries_matrix_state = True -- currently
only "delta") are called as forward(..., return_state=True)
and their returned state is passed through as this block's state,
instead of the output[:, -1:] convention every other mixer
uses; this is the only behavior difference such a mixer causes, and
it is why the returned state's last dim can differ from
d_model for "delta" (it is n_heads*d_k*d_v, not
d_model).
Examples:
>>> import torch
>>> from mingru import MinGRUBlock
>>> block = MinGRUBlock(d_model=64, mixer="signed")
>>> x = torch.randn(4, 128, 64)
>>> out, state = block(x) # always returns (output, state)
>>> tuple(out.shape), tuple(state.shape)
((4, 128, 64), (4, 1, 64))
forward ¶
forward(x: Tensor, h_0: Tensor | None = None, delta_t: Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]
Parallel forward over a full sequence.
Always returns (output, state), matching step() and the
nn.GRU convention; discard the state when not carrying it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Residual-stream input, shape |
required |
h_0
|
Tensor
|
This block's mixer state carried from a previous chunk,
shape |
None
|
delta_t
|
Tensor
|
Time gaps preceding each event, shape |
None
|
Returns:
| Type | Description |
|---|---|
tuple of torch.Tensor
|
The output |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mixer's |
step ¶
step(x_t: Tensor, h_prev: Tensor | None, delta_t: Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]
Single streaming step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Residual-stream input at time |
required |
h_prev
|
Tensor or None
|
This block's mixer state from |
required |
delta_t
|
Tensor
|
Time gap preceding this event, shape |
None
|
Returns:
| Type | Description |
|---|---|
tuple of torch.Tensor
|
The block output, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mixer's |
mingru.MinGRUStack ¶
MinGRUStack(input_size: int, d_model: int, n_layers: int, mlp_expansion: int = 4, dropout: float = 0.0, learnable_h0: bool = False, mixer: str | list[str] = 'log', mixer_kwargs: dict | None = None, decay_layers: str = 'all')
Bases: Module
N stacked MinGRUBlocks with input projection and final norm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Dimensionality of the raw inputs |
required |
d_model
|
int
|
Residual stream width. |
required |
n_layers
|
int
|
Number of blocks. |
required |
mlp_expansion
|
int
|
Per-block MLP expansion; |
4
|
dropout
|
float
|
Per-block dropout. |
0.0
|
learnable_h0
|
bool
|
Passed through to every block; routed to the block's mixer
when |
False
|
mixer
|
str or list of str
|
Sequence mixer for the blocks; one of |
"log"
|
mixer_kwargs
|
dict
|
Schema is decided by the type of
A flat dict alongside a list |
None
|
decay_layers
|
(all, last)
|
Which blocks receive the decay-related keys
( Trap in mixed stacks: |
"all"
|
Notes
Shapes: forward maps (B, T, input_size) to
(B, T, d_model); step maps (B, input_size) and a state
(a list of n_layers tensors, one per block) to the output
(B, d_model) and the updated state. Per-block state entries are
opaque to MinGRUStack -- init_state() returns None
placeholders (no shape assumed) and step() only ever passes a
state entry through to its own block's .step(), never inspects
or reshapes it -- so this is uniform across mixer types with one
caveat: the entry's shape is (B, d_model) for every mixer except
"delta" (carries_matrix_state = True), whose entry is
(B, n_heads*d_k*d_v) (see MinGRUBlock.step). Both
forward/step also accept an optional
delta_t (same shapes as the mixers' delta_t): it is routed
to a given block only if that block's mixer has decay enabled
(checked via the mixer's own decay attribute), so a
decay_layers="last" stack silently gives delta_t only to
its final block. If NO block in the stack has decay enabled,
passing delta_t raises ValueError (the mode-error rule);
if at least one block decays, that block's own pairing contract
raises if delta_t was left out.
More than one "rotation" block in mixer triggers exactly
one UserWarning per construction: the straight-through snap
discontinuity can compound across rotation layers, and multi-
rotation stacks are validated only at L=2 on the S3 probe (deeper
is untested — see the README's Rotation variant section, "Depth:
L=2 is validated"); construction proceeds regardless. A single
"rotation" block in a mixed stack does not warn. Multiple
"givens" blocks do NOT warn: the warning is specific to the
straight-through snap discontinuity, and GivensMinGRU is
continuous (unsnapped), so it has no such discontinuity to compound.
Examples:
>>> import torch
>>> from mingru import MinGRUStack
>>> model = MinGRUStack(input_size=32, d_model=64, n_layers=3, mixer="log")
>>> x = torch.randn(4, 128, 32)
>>> out, state = model(x) # state: one entry per block
>>> tuple(out.shape), len(state)
((4, 128, 64), 3)
forward ¶
forward(x: Tensor, state: list[Tensor | None] | None = None, delta_t: Tensor | None = None) -> tuple[torch.Tensor, list[torch.Tensor]]
Parallel training-mode forward.
Always returns (output, state), matching step() and the
nn.GRU convention; discard the state when not carrying it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Tensor
|
Input sequence, shape |
required |
state
|
list of (torch.Tensor or None)
|
Per-block minGRU states from a previous chunk — as returned
by a prior |
None
|
delta_t
|
Tensor
|
Time gaps preceding each event, shape |
None
|
Returns:
| Type | Description |
|---|---|
tuple
|
The output |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
init_state ¶
Fresh streaming state.
Returns:
| Type | Description |
|---|---|
list of None
|
One slot per block; pass to |
step ¶
step(x_t: Tensor, state: list[Tensor | None], delta_t: Tensor | None = None) -> tuple[torch.Tensor, list[torch.Tensor]]
Streaming step.
Total cached state is n_layers * d_model per sample, except
that a "delta" block's entry is n_heads*d_k*d_v instead
of d_model (see the class "Notes" section).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
Tensor
|
Input at the current timestep, shape |
required |
state
|
list of (torch.Tensor or None)
|
From |
required |
delta_t
|
Tensor
|
Time gap preceding this event, shape |
None
|
Returns:
| Type | Description |
|---|---|
tuple
|
The output, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |