ml-models / Model architectures / State-space models advanced

State-space models

A state-space model reads a sequence through a fixed-size state, like an RNN, but its update is linear, so the same layer can also be run as one long convolution and trained in parallel. S4 made that work for long sequences with a carefully structured state matrix; Mamba made the update depend on the input, so the model can choose what to remember. This page discretizes the equations three ways, checks that the recurrence and the convolution give the same 8 outputs, runs a selective scan that holds a value a fixed step cannot, scans it in 3 parallel steps, and compares memory with a KV cache. Every number is computed and every library link is pinned.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The continuous system
  5. Discretization
  6. Recurrence and convolution
  7. S4: a structured state matrix
  8. Mamba: selection
  9. The parallel scan
  10. Worked example
  11. A HiPPO SSM, run both ways
  12. Remembering the flagged value
  13. Scanning it in parallel
  14. State versus KV cache
  15. Implementation
  16. Tradeoffs
  17. Check yourself
  18. Connections
  19. Read first
  20. Read next
  21. References

Problem

A Transformer compares every token with every earlier token. Training on a sequence of length L costs time proportional to L², and generating each new token reads a KV cache that grows by one entry per token. At 8,000 tokens that is manageable; at half a million it dominates both memory and time. An RNN has the opposite profile: a fixed-size state and constant cost per token at inference, but training must step through time one position at a time, and gradients fade over long distances.

State-space models (SSMs) aim for both: constant-size state and linear cost at inference, and parallel training over the whole sequence. The price is a linear recurrence, which on its own cannot decide what to keep. The history of the family is how to make that recurrence remember long and, in Mamba, how to let it choose.

Intuition

Picture a set of leaky buckets, the state. Each time step some input pours in, every bucket leaks a fixed fraction, and the output is a weighted reading of the levels. Because the leaking and pouring are linear, an input at time 0 contributes to the output at time t by a fixed amount that depends only on the gap t. So you can compute the output either by simulating the buckets step by step (a recurrence) or by sliding one fixed filter over the input (a convolution). Training uses the convolution, which is parallel; generation uses the recurrence, which needs only the current levels.

The leak rate is controlled by a step size Δ. A large Δ means the state is mostly replaced by the new input; a tiny Δ means the input is ignored and the state carries on unchanged. Mamba's idea is to let each token pick its own Δ: an important token takes a large step and is written in, filler takes a tiny step and is skipped. The filter is then different at every position, so the convolution view is lost, and an associative scan recovers the parallelism.

One linear SSM, two ways to compute it Top: the recurrent view. Inputs u0 to u3 enter one at a time; the state h is updated as h_t = A-bar h_(t-1) + B-bar u_t and read out as y_t = C h_t, using constant memory. Bottom: the convolutional view. The kernel K = (C B-bar, C A-bar B-bar, C A-bar squared B-bar, ...) is computed once and slid over the whole input in parallel. Both produce the same outputs when A-bar, B-bar and C do not depend on the input. recurrent: one step at a time, state of size N h0 u0 → B̄ y0 = C h0 Ā h1 u1 → B̄ y1 = C h1 Ā h2 u2 → B̄ y2 = C h2 Ā h3 u3 → B̄ y3 = C h3 convolutional: kernel K computed once, applied in parallel K = (CB̄, CĀB̄, C²B̄, …, CĀ^(L−1)B̄) y = K ∗ u (causal convolution) same y in both views, as long as Ā, B̄, C are the same at every step

The duality holds only for a linear, time-invariant (LTI) system. Selection breaks the time invariance, which is why Mamba needs a scan instead of a convolution.

Mechanics

The continuous system

An SSM maps a one-dimensional input signal u(t) to an output y(t) through an N-dimensional state h(t):

h′(t) = A h(t) + B u(t),   y(t) = C h(t) (+ D u(t), a skip term)

A is N × N, B is N × 1 and C is 1 × N. A model with D channels runs D independent copies, one per channel, usually with N between 4 and 64. The eigenvalues of A must have negative real parts, or the state blows up.

Discretization

Tokens arrive at discrete steps, so the equation is turned into a recurrence ht = Ā ht−1 + B̄ ut with a step size Δ. Three rules are common:

  • Euler: Ā = I + ΔA, B̄ = ΔB. Cheap, but unstable once Δ is large: for scalar a = −1, |1 − Δ| > 1 when Δ > 2.
  • Bilinear (Tustin): Ā = (I − ΔA/2)⁻¹(I + ΔA/2), B̄ = (I − ΔA/2)⁻¹ΔB. Stable for every Δ whenever A is. S4 uses it.
  • Zero-order hold (ZOH): assumes the input is constant over each step, giving Ā = exp(ΔA) and B̄ = A⁻¹(exp(ΔA) − I)B. Exact for piecewise-constant input. S4D and Mamba use it; for a diagonal A both are elementwise.
Δ (a = −1)exact eaΔEuler 1 + aΔbilinearZOH B̄
0.10.90480.90000.90480.0952
0.50.60650.50000.60000.3935
1.00.36790.00000.33330.6321
2.50.0821−1.5000 (unstable)−0.11110.9179

At small Δ all three agree (bilinear is closer to exact than Euler at Δ = 0.1). At Δ = 1 Euler forgets everything in one step; at Δ = 2.5 its factor of −1.5 makes the state grow and flip sign, while bilinear stays below 1 in magnitude and ZOH is exact by construction.

Recurrence and convolution

Unroll the recurrence from h−1 = 0: yt = Σk=0..t CĀkB̄ ut−k. That is a causal convolution with kernel Kk = CĀkB̄. In training, the whole sequence is known, so the kernel is computed once per layer and applied with an FFT in O(L log L) or as a Conv1d. In generation, the recurrence advances the N-dimensional state by one token at constant cost. The kernel is the system's impulse response: feed a single 1 followed by zeros through the recurrence and the outputs are K.

S4: a structured state matrix

A random A forgets quickly and its powers are expensive. S4 (Gu, Goel and Ré, 2021) starts from HiPPO-LegS, a matrix derived so that the state holds the coefficients of a Legendre-polynomial approximation of the whole input history so far:

Ank = −√((2n+1)(2k+1)) for n > k,   −(n+1) for n = k,   0 above the diagonal;   Bn = √(2n+1)

This A is not diagonalizable in a numerically useful way, but it is normal plus low rank (NPLR): adding ppT with pn = √(n + ½) gives −½I plus a skew-symmetric matrix, which is normal and so unitarily diagonalizable. S4 uses that split and the Woodbury identity to compute the length-L kernel in nearly linear time instead of raising Ā to L powers. Later work found simpler choices nearly as good: S4D (Gu et al., 2022) keeps only a diagonal A, initialized from the diagonal part of the NPLR form or with real values −(n+1), and the kernel becomes a Vandermonde product.

Mamba: selection

An LTI system applies the same filter to every input. It cannot do something as simple as "remember the last value that was flagged and ignore everything else", because which inputs matter depends on their content. Mamba (Gu and Dao, 2023) makes Δ, B and C functions of the current token:

Δt = softplus(WΔxt + b),   Bt = WBxt,   Ct = WCxt,   Āt = exp(ΔtA)

softplus keeps Δ positive. A large Δt drives Āt towards 0 and writes the token in; a Δt near 0 leaves Āt ≈ 1 and B̄t ≈ 0, so the token is skipped. This is a gate, the same idea as an LSTM's forget gate, but the recurrence stays linear in h. A stays fixed and diagonal (S4D-real initialization), stored as log(−A). Each channel of the expanded inner dimension has its own A and Δ, and all channels share Bt and Ct.

A Mamba block wraps the selective SSM: a linear projection to twice the expanded width, a depthwise causal convolution of width 4 and a silu on one half, the SSM, multiplication by silu of the other half, and a projection back. Blocks are stacked with residual connections and normalization, with no attention and no MLP.

The parallel scan

With per-token Āt there is no single kernel. But the update ht = atht−1 + bt composes: applying (a1, b1) then (a2, b2) is the single step (a1a2, a2b1 + b2). That combine is associative, so a prefix scan computes every ht in log₂L parallel rounds, the same trick as a parallel prefix sum (cumsum, or cumprod for the a half). Mamba's CUDA kernel goes further: it keeps the expanded state (L × D × N) in on-chip SRAM, never writes it to GPU memory, and recomputes it in the backward pass, the same IO-aware idea as FlashAttention (see GPU execution). Mamba-2 (Dao and Gu, 2024) restricts A to a scalar times the identity per head, which turns the scan into blocks of matrix multiplication that use tensor cores, and shows the result is a form of masked linear attention.

ComponentShape/ParamsMeaningSource
A(D, N) diagonal, negativeDecay rate of each state; S4 uses HiPPO, Mamba −exp(A_log)S4 §3; Mamba §3.6
Δ(B, L, D) after softplusStep size: how much of the token to write infunctional.py 2115-2127
Bt, Ct(B, L, N)Input and output maps; input-dependent in Mamba, fixed in S4Mamba Algorithm 2
Kernel K(D, L)CĀkB̄ for LTI models; computed with einsumfunctional.py 175-382
Causal convolutionkernel L (S4) or 4 (Mamba, depthwise)Applies the kernel; pad on the left onlyconv.py 225-385
F.conv1dinput (B, C, L), weight (out, in/groups, k)Cross-correlation, so flip the kernel for a convolutionfunctional.py 53-99
silu gate(B, L, E·D)Gates the SSM output in a Mamba blockfunctional.py 2429-2451
Scan state(B, E·D, N) per layerAll a layer carries between tokens at inferenceMamba §3.3
Why can S4 be trained with a convolution but Mamba cannot?

The convolution kernel CĀkB̄ assumes the same Ā, B̄ and C at every step. In Mamba they depend on each token, so the contribution of us to yt is CtĀt…Ās+1B̄s, which changes with both s and t, not only with their gap. The associative scan replaces the convolution.

Worked example

A HiPPO SSM, run both ways

Take N = 4 with the HiPPO-LegS A and B above, C = (1.0, 0.5, 0.25, 0.125), Δ = 0.1 and the bilinear rule. Adding ppT to A gives a diagonal of −0.500, −0.500, −0.500, −0.500 and off-diagonal pairs such as 0.866 and −0.866: −½I plus a skew-symmetric matrix, as claimed. Discretized, Ā has diagonal 0.9048, 0.8182, 0.7391, 0.6667 and B̄ = (0.0952, 0.15, 0.1599, 0.1419).

tutkernel Ktrecurrent ytconvolution yt
01.00.22790.22790.2279
10.50.15110.26510.2651
2−0.50.10400.06560.0656
32.00.07510.50750.5075
40.00.05710.34490.3449
5−1.00.04570.01690.0169
60.50.03820.14560.1456
71.00.03290.34290.3429

The two columns agree to 1.1e-16, rounding error. The kernel decays smoothly from 0.2279 to 0.0329 rather than geometrically at one rate: the four states decay at different speeds and the HiPPO coupling mixes them, which is how the model keeps a long, graded memory.

The HiPPO SSM's convolution kernel Bar chart of the eight kernel values K_0 to K_7 of the worked example, from 0.2279 down to 0.0329, each the weight given to an input k steps in the past. 0.228 K0 0.151 K1 0.104 K2 0.075 K3 0.057 K4 0.046 K5 0.038 K6 0.033 K7 weight on an input k steps back

Feeding a single 1 through the recurrence produces exactly these values: the kernel is the impulse response.

Remembering the flagged value

The task: a stream of values, some flagged, and the output should always be the most recent flagged value. Inputs are (2.0, 0.7, −0.4, 0.9, −1.0, 0.8, 0.5, −0.2) with flags (1, 0, 0, 0, 1, 0, 0, 0), so the target is (2.0, 2.0, 2.0, 2.0, −1.0, −1.0, −1.0, −1.0). A two-state selective SSM uses A = (−1.0, −2.0), B = (1.0, 2.0) times the flag, C = (0.5, 0.5) and Δt = softplus(10.0·flag − 6.0) with ZOH.

tvalueflagΔtĀtB̄thtytfixed Δ = 0.5
02.014.01810.0180, 0.00030.982, 1.0001.964, 1.9991.98171.026
10.700.00250.9975, 0.99510.000, 0.0001.959, 1.9891.97430.830
2−0.400.00250.9975, 0.99510.000, 0.0001.954, 1.9801.96700.190
30.900.00250.9975, 0.99510.000, 0.0001.949, 1.9701.95970.567
4−1.014.01810.0180, 0.00030.982, 1.000−0.947, −0.999−0.9730−0.240
50.800.00250.9975, 0.99510.000, 0.000−0.945, −0.994−0.96940.314
60.500.00250.9975, 0.99510.000, 0.000−0.942, −0.989−0.96570.404
7−0.200.00250.9975, 0.99510.000, 0.000−0.940, −0.984−0.96210.089

Flagged tokens get Δ = 4.018, so Ā is nearly 0 and the state is overwritten; unflagged tokens get Δ = 0.0025, so Ā ≈ 1 and nothing is written. The largest error is 0.040, from the slow leak of Ā slightly below 1. The same SSM with one fixed Δ = 0.5 (and B applied to every token) blends all the inputs and misses by up to 1.81. No choice of a fixed Δ fixes this: a time-invariant filter weights inputs by position, not by content.

Scanning it in parallel

Take the first state of the selective run as pairs (at, bt) = (Āt, B̄tut). A Hillis-Steele scan combines each element with the one 1, then 2, then 4 places to its left. After round r, element t covers the 2r steps ending at t, and its b is the state ht.

roundt = 0t = 1t = 2t = 3t = 4t = 5t = 6t = 7
01.9640.0000.0000.000−0.9820.0000.0000.000
11.9641.9590.0000.000−0.982−0.9800.0000.000
21.9641.9591.9541.949−0.982−0.980−0.977−0.975
31.9641.9591.9541.949−0.947−0.945−0.942−0.940
sequential1.9641.9591.9541.949−0.947−0.945−0.942−0.940

3 rounds instead of 8 sequential steps, at the cost of 17 combines instead of 7. A work-efficient (Blelloch) scan does about 2L combines in 2 log₂L rounds. The final row matches the sequential loop exactly.

Hillis-Steele scan over eight steps Four rows of eight boxes, rounds 0 to 3. In round 1 each box from position 1 combines with its left neighbour, in round 2 with the box two to the left, in round 3 with the box four to the left. After round 3 every position holds the full prefix, that is the state h_t. round 0 1.96 0.00 0.00 0.00 −0.98 0.00 0.00 0.00 round 1 1.96 1.96 0.00 0.00 −0.98 −0.98 0.00 0.00 round 2 1.96 1.96 1.95 1.95 −0.98 −0.98 −0.98 −0.97 round 3 1.96 1.96 1.95 1.95 −0.95 −0.94 −0.94 −0.94

Each dashed line is one combine. Round r reaches 2r−1 places to the left, so log₂ 8 = 3 rounds cover every prefix.

State versus KV cache

Compare a 48-layer model of width 2,048 in bf16 (2 bytes). The attention version caches a key and a value per layer per token: 2 × 48 × context × 2,048 × 2 bytes (no grouped-query sharing). A Mamba version with expansion 2 and N = 16 carries 48 × 4,096 × (16 + 3) × 2 = 7,471,104 bytes: the SSM state plus the last 3 inputs of the convolution, whatever the context. Per-token FLOPs count the attention read as 4 × layers × context × width and the SSM update as 6 × layers × inner width × N; training FLOPs count causal attention scores and values (2 × layers × context² × width) against the scan.

contextKV cacheSSM stateattention FLOPs / tokenSSM FLOPs / tokenattention mixing, trainingSSM scan, training
1,0240.38 GiB7.12 MiB4.03e+081.89e+072.06e+111.93e+10
8,1923.00 GiB7.12 MiB3.22e+091.89e+071.32e+131.55e+11
65,53624.00 GiB7.12 MiB2.58e+101.89e+078.44e+141.24e+12
524,288192.00 GiB7.12 MiB2.06e+111.89e+075.40e+169.90e+12

Going from 1,024 to 524,288 tokens multiplies the KV cache and per-token attention cost by 512 and the attention mixing cost in training by 512² = 262,144; the SSM's state does not change and its training cost grows 512-fold. These count only the sequence-mixing part; the projections and MLPs cost the same per token in both and dominate at short context.

Implementation

From primitives: an S4-style LTI layer computed both as a kernel and as a convolution, and a Mamba-style selective SSM as a plain loop. The kernel uses torch.einsum, and the convolution uses F.conv1d with left padding so no output sees the future.

import torch
import torch.nn.functional as F

def discretize_bilinear(A, B, dt):
    I = torch.eye(A.shape[0], dtype=A.dtype)
    left = torch.linalg.inv(I - dt / 2 * A)
    return left @ (I + dt / 2 * A), left @ (dt * B)

def ssm_kernel(A_bar, B_bar, C, L):
    powers = [torch.eye(A_bar.shape[0], dtype=A_bar.dtype)]
    for _ in range(L - 1):
        powers.append(A_bar @ powers[-1])
    return torch.einsum("n,lnm,m->l", C, torch.stack(powers), B_bar)   # K[l] = C A^l B

def ssm_conv(u, K):                                   # u: (batch, L)
    L = u.shape[-1]
    x = F.pad(u.unsqueeze(1), (L - 1, 0))             # causal: pad the left only
    return F.conv1d(x, K.flip(-1).view(1, 1, L)).squeeze(1)  # conv1d correlates, so flip

def selective_scan(x, delta, A, B, C):
    # x, delta: (batch, L, D); A: (D, N); B, C: (batch, L, N)
    h = x.new_zeros(x.shape[0], x.shape[2], A.shape[1])
    ys = []
    for t in range(x.shape[1]):
        dA = torch.exp(delta[:, t, :, None] * A)          # ZOH for diagonal A
        dB = (dA - 1) / A * B[:, t, None, :]
        h = dA * h + dB * x[:, t, :, None]
        ys.append(torch.einsum("bdn,bn->bd", h, C[:, t]))
    return torch.stack(ys, dim=1)

class SelectiveSSM(torch.nn.Module):
    def __init__(self, d, n=16):
        super().__init__()
        self.A_log = torch.nn.Parameter(torch.log(torch.arange(1, n + 1).float()).repeat(d, 1))
        self.to_delta = torch.nn.Linear(d, d)
        self.to_B = torch.nn.Linear(d, n, bias=False)
        self.to_C = torch.nn.Linear(d, n, bias=False)
        self.D = torch.nn.Parameter(torch.ones(d))

    def forward(self, x):                             # x: (batch, L, d)
        A = -torch.exp(self.A_log)                    # S4D-real: -1, -2, ..., -n
        delta = F.softplus(self.to_delta(x))
        return selective_scan(x, delta, A, self.to_B(x), self.to_C(x)) + self.D * x

The snippet has not been run against PyTorch here; it uses only the calls linked above, and the helpers behind this page compute the same recurrence, kernel and selective scan in plain Python. The loop is O(L) sequential steps: fine for checking, far too slow for training. The official Mamba package replaces it with a fused CUDA scan, and its reference implementation uses the simpler B̄ = ΔB rather than the exact ZOH form for B.

Tradeoffs

ChoiceGainsCosts
SSM instead of attentionConstant memory and cost per generated token; linear training costEverything must pass through a fixed-size state, so exact recall of a long span (copying, lookup of a phone number from far back) is harder
LTI (S4) vs selective (Mamba)LTI: FFT convolution, simple and fast on continuous signals such as audioLTI cannot filter by content, and does poorly on language; selective needs a custom scan kernel
HiPPO / NPLR vs diagonal AHiPPO: principled long memoryNPLR needs complex arithmetic and Cauchy kernels; diagonal (S4D, Mamba) is nearly as good and much simpler
Bilinear vs ZOHBoth stable for any ΔZOH is exact for held inputs and elementwise with a diagonal A; bilinear needs a matrix inverse for a full A
Hybrids (some attention layers)Recover precise recall with a few attention layers, keep most of the memory savingsThe attention layers bring back a (smaller) KV cache
A Mamba model and a Transformer of the same size both answer questions about a 100-page document. Which is more likely to quote a specific number from page 3 exactly, and why?

The Transformer. Its KV cache keeps every token, so attention can look the number up directly. The SSM compressed the whole document into a fixed state of a few megabytes and only kept the number if selection decided, when it read page 3, that it was worth storing. This recall gap is the main reason many recent large models mix SSM layers with a few attention layers.

Why must Δ be positive, and what happens at the two extremes?

With A negative, Ā = exp(ΔA) lies in (0, 1) only when Δ > 0; a negative Δ would make the state grow. As Δ → 0, Ā → 1 and B̄ → 0: the state is kept and the token ignored. As Δ → ∞, Ā → 0 and B̄ → −B/A: the state is reset to the current token. Softplus keeps Δ positive and smooth between these.

Check yourself

  • Write the convolution kernel of a discretized LTI SSM in terms of Ā, B̄ and C.
  • Why does Euler discretization fail for large Δ while bilinear and ZOH do not?
  • What does the associative combine (a1, b1) ∘ (a2, b2) compute, and why does associativity matter?
  • How much memory does a Mamba layer need at inference for a 1M-token context, compared with 1K?

Connections

Read first

Read next

No page builds on this one yet.

SSMs are linear RNNs with gates like an LSTM's, and they compete with the Transformer and its attention. The memory comparison is with the KV cache, and the per-token cost matters for latency and throughput. The convolution view is a one-dimensional CNN with a kernel as long as the sequence. Why the fused scan kernel is fast is on GPU execution; matrix powers and eigenvalues are on the linear algebra toolkit. As language models, SSMs are trained like any decoder-only LLM; long time series are covered on time series.

References

  • PyTorch v2.15.0: Conv1d, _conv_forward, einsum, conv1d, softplus, silu, cumsum, cumprod.
  • Gu, Dao, Ermon, Rudra and Ré. HiPPO: Recurrent Memory with Optimal Polynomial Projections. NeurIPS 2020. arXiv:2008.07669
  • Gu, Goel and Ré. Efficiently Modeling Long Sequences with Structured State Spaces. ICLR 2022. arXiv:2111.00396
  • Gu, Gupta, Goel and Ré. On the Parameterization and Initialization of Diagonal State Space Models. NeurIPS 2022. arXiv:2206.11893
  • Gu and Dao. Mamba: Linear-Time Sequence Modeling with Selective State Spaces. 2023. arXiv:2312.00752
  • Dao and Gu. Transformers are SSMs: Generalized Models and Efficient Algorithms Through Structured State Space Duality. ICML 2024. arXiv:2405.21060
  • Blelloch. Prefix Sums and Their Applications. CMU-CS-90-190, 1990.