Scan operations¶
The four associative scan primitives the mixers are built on. Each accepts MINGRU_SCAN dispatch (see Control scan dispatch); the eager signatures and shape contracts are generated from the source docstrings.
mingru.parallel_scan_log ¶
Heinsen (2023) parallel scan in log-space.
Solves h_t = a_t * h_{t-1} + b_t for all t simultaneously,
given log(a_t) and log(b_t), assuming a_t, b_t > 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_coeffs
|
Tensor
|
Shape |
required |
log_values
|
Tensor
|
Shape |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
References
Heinsen (2023), "Efficient Parallelization of a Ubiquitous Sequential Computation" -- the log-space cumulative-sum / logcumsumexp scan.
Notes
Under MINGRU_SCAN=auto (the default) on CUDA, this op dispatches to
a Triton kernel that is measured SLOWER than eager for a full forward+
backward training step at benchmarked shapes (0.70-0.80x eager; see
experiments/bench/scan_bench.md), even though its forward-only
latency is roughly on par with eager. MINGRU_SCAN=eager is the
right choice for training MinGRU/SignedMinGRU today.
Examples:
mingru.linear_scan ¶
Linear-space associative scan for h_t = a_t * h_{t-1} + b_t.
Hillis-Steele doubling over the segment-composition monoid
(A1, B1) o (A2, B2) = (A2*A1, A2*B1 + B2): O(T log T) work,
O(log T) depth, pure torch ops, differentiable. Unlike
parallel_scan_log, coefficients may be NEGATIVE — this is what
enables signed transition eigenvalues (see SignedMinGRU).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Tensor
|
Shape |
required |
b
|
Tensor
|
Shape |
required |
Returns:
| Type | Description |
|---|---|
tuple of torch.Tensor
|
|
Notes
Hillis-Steele is work-inefficient (O(T log T) vs O(T) for a
work-efficient Blelloch scan) and retains O(log T) full (B, T, D)
tensors for autograd. That is a deliberate simplicity-over-efficiency
choice: the overhead is negligible at the short training lengths
this repo targets (probes train at T=64), and no stable
torch.associative_scan primitive exists to lean on. Revisit for
long-sequence training.
Examples:
mingru.matrix_scan ¶
Associative scan for h_t = M_t @ h_{t-1} + b_t, 2x2 block transitions.
The non-commutative generalization of linear_scan: transitions
are 2x2 matrices composed by matrix multiplication instead of
scalars multiplied together, so the running product depends on
order. This is what lets a mixer built on matrix_scan
(RotationMinGRU) express non-abelian state tracking that a
diagonal/commutative scan (linear_scan, parallel_scan_log)
provably cannot. Same Hillis-Steele doubling scheme as
linear_scan, over the affine 2x2 monoid
(A1, B1) o (A2, B2) = (A2 @ A1, A2 @ B1 + B2): O(T log T) work,
O(log T) depth, pure torch ops, differentiable. The identity
padding is the 2x2 identity matrix (not 1.0), and composition order
is A_current @ A_earlier — matrix multiplication does not
commute, unlike linear_scan's scalar product.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
M
|
Tensor
|
Shape |
required |
b
|
Tensor
|
Shape |
required |
Returns:
| Type | Description |
|---|---|
tuple of torch.Tensor
|
|
Notes
Same simplicity-over-efficiency tradeoff as linear_scan:
Hillis-Steele is work-inefficient (O(T log T) vs O(T)) and retains
O(log T) full (B, T, n, 2, 2) tensors for autograd. Fine at
the short training lengths this repo targets (probes train at
T=64); revisit for long-sequence training.
Examples:
mingru.matrix_affine_scan ¶
Associative scan for H_t = A_t @ H_{t-1} + B_t, k x k transitions.
The k x k generalization of matrix_scan: transitions are square
k x k matrices (instead of matrix_scan's fixed 2x2 blocks)
and injections are rectangular k x v matrix-valued states
(instead of matrix_scan's 2-vectors, i.e. v = 1). Both scan
the same affine monoid
(A1, B1) o (A2, B2) = (A2 @ A1, A2 @ B1 + B2) by the same
Hillis-Steele doubling scheme; matrix_scan is exactly the
k = 2, v = 1 special case, and matrix_affine_scan is what
the k > 2 block-rotation mixer (GivensMinGRU, with the state
as a v = 1 column) and matrix-valued delta-rule states run on.
O(T log T) work, O(log T) depth, pure torch ops, differentiable.
Composition order is A_current @ A_earlier — matrix
multiplication does not commute — so, as in matrix_scan, the
running product depends on order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Tensor
|
Shape |
required |
Bm
|
Tensor
|
Shape |
required |
Returns:
| Type | Description |
|---|---|
tuple of torch.Tensor
|
|
Notes
Same simplicity-over-efficiency tradeoff as matrix_scan /
linear_scan: Hillis-Steele is work-inefficient (O(T log T) vs
O(T)) and retains O(log T) full (B, T, n, k, k) tensors for
autograd. Fine at the short training lengths this repo targets
(probes train at T=64); revisit for long-sequence training. Kept as
a distinct helper beside matrix_scan (rather than a
generalization of it) so the recorded 2x2 rotation evidence's
floating-point path is preserved byte-for-byte.
Examples:
>>> import torch
>>> from mingru import matrix_affine_scan
>>> B, T, n, k, v = 2, 5, 4, 8, 1
>>> A = torch.eye(k).expand(B, T, n, k, k) # identity transitions
>>> Bm = torch.randn(B, T, n, k, v)
>>> Abar, Bbar = matrix_affine_scan(A, Bm)
>>> tuple(Abar.shape), tuple(Bbar.shape)
((2, 5, 4, 8, 8), (2, 5, 4, 8, 1))