Skip to content

state_space

state_space

StateSpace: a Mamba-2 (SSD) mixer's values, at the scan kernel call.

SSDQueries module-attribute

SSDQueries = Float[Tensor, f'{BATCH} {SEQ} {GROUPS} {STATE_DIM}']

SSDKeys module-attribute

SSDKeys = Float[Tensor, f'{BATCH} {SEQ} {GROUPS} {STATE_DIM}']

SSDValues module-attribute

SSDValues = Float[Tensor, f'{BATCH} {SEQ} {MIXER_HEADS} {HEAD_DIM}']

SSDHeadOutputs module-attribute

SSDHeadOutputs = Float[Tensor, f'{BATCH} {SEQ} {MIXER_HEADS} {HEAD_DIM}']

NO_STATE_OCCURRENCES module-attribute

NO_STATE_OCCURRENCES = "the chunk scan computes every token's state in one tensor per call, not one occurrence per token to walk with tracer.iter; read states, or state_after(t), after nnterp.chunk_per_token(model)"

NO_STATE_WRITES module-attribute

NO_STATE_WRITES = "the chunk scan computes every boundary state in one cumulative step from the initial state, so a state written at token t does not flow into later tokens' states; assign state_input to change where a call starts"

StateSpace

StateSpace(*args: Any, **kwargs: Any)

Bases: RecurrentMixer

A Mamba-2 (SSD) mixer (Mamba-2, Nemotron-H, Bamba, Falcon-H1, Zamba2, GraniteMoeHybrid): a selective state space.

SSD, the state-space dual, is linear attention with a scalar decay per head. Per head, with the state h a head_dim by state_dim matrix, each token does::

h = exp(dt * A) * h + dt * x B^T
y = h C + D * x

so the shared names mean what they mean on a gated DeltaNet (LinearAttention):

  • attention_queries is C, what reads the state; attention_keys is B, where a token writes into it; both [batch, seq, groups, state_dim], one per group of heads (n_groups), shared by the heads in the group.
  • attention_values is x (hidden_states at the call), what is written, [batch, seq, heads, head_dim].
  • betas is dt = softplus(dt + dt_bias), the write strength, [batch, seq, heads]; decays is A * dt, the log of how much of the state each token keeps, [batch, seq, heads], float32 and non-positive. Both are the kernel's dt argument seen through dt_bias, the softplus and A; an assignment is carried back into dt (_dt_for), so the kernel computes the assigned gate.
  • state_input / state_output: the state entering and leaving the call, served as [batch, heads, state_dim, head_dim] (key side first, the shared State layout), the transpose of the cache's [batch, heads, head_dim, state_dim]; an assignment is transposed back. state_input is None on a fresh prompt.
  • attention_head_outputs is y, the scan's output with the D skip, before the gated norm and out_proj, [batch, seq, heads, head_dim].

Everything but attention_output is read at the scan call: transformers' mamba2_chunk_scan on a prompt, its mamba2_selective_state_update on a decode step (RecurrentMixer.KERNEL). The two take their arguments in different places and the update has no sequence axis, so each value selects by the kernel that fires (argument) and a decode step's tensors are served with a sequence axis of 1, removed again on assignment. The update writes the new state into the cache's buffer and returns only y, so a decode step's state_output is read inside it, at UPDATED_STATE.

The chunk scan materializes the state at every chunk boundary, in one tensor (new_states, CHUNK_STATES); with a chunk size of 1 (chunk_per_token) every token is a boundary, and states and state_after(t) read the state after every token of the call (a decode step's is its state_output). There is no per-token occurrence to walk with tracer.iter (state is unavailable, STATE_OP is None), and no per-token write: the scan computes every boundary state in one cumulative step from the initial state, so set_state_after is unavailable too. With mamba_ssm installed the kernels have no Python source; route_kernels(model.family, "torch") binds transformers' pure-torch ones (RecurrentMixer).

A value that needs another argument of the call (betas needs dt_bias, a prompt's state_output whether the scan returns a state) reads the call's arguments once per call (_arguments), at the call's first need, and finds them there after the model has moved into the kernel. A value read after the model has moved past its location is an out-of-order read.

CHUNK_KERNEL class-attribute instance-attribute

CHUNK_KERNEL = 'mamba2_chunk_scan_0'

RECURRENT_KERNEL class-attribute instance-attribute

RECURRENT_KERNEL = 'mamba2_selective_state_update_0'

STATE_OP class-attribute instance-attribute

STATE_OP = None

CHUNK_STATES class-attribute instance-attribute

CHUNK_STATES = 'new_states_0'

UPDATED_STATE class-attribute instance-attribute

UPDATED_STATE = 'ssm_states_0'

CHUNK_ARGUMENTS class-attribute instance-attribute

CHUNK_ARGUMENTS = {'hidden_states': 0, 'dt': 1, 'A': 2, 'B': 3, 'C': 4, 'dt_bias': 'dt_bias', 'state': 'initial_states'}

RECURRENT_ARGUMENTS class-attribute instance-attribute

RECURRENT_ARGUMENTS = {'state': 0, 'hidden_states': 1, 'dt': 2, 'A': 3, 'B': 4, 'C': 5, 'dt_bias': 'dt_bias'}

head_dim property

head_dim: int

Width of one head's values and outputs: the mixer's head_dim, which on a hybrid is not its softmax attention's.

states class-attribute instance-attribute

states = DerivedEProperty(_states, description='The state after every token of this call; needs nnterp.chunk_per_token(model)', unavailable=needs_per_token_chunks)

state class-attribute instance-attribute

set_state_after class-attribute instance-attribute

set_state_after = unavailable(NO_STATE_WRITES)

attention_queries

attention_queries(value: Tensor) -> Tensor

attention_keys

attention_keys(value: Tensor) -> Tensor

attention_values

attention_values(value: Tensor) -> Tensor

betas

betas(value: Tensor) -> Tensor

decays

decays(value: Tensor) -> Tensor

state_input

state_input(value: Any) -> Any

attention_head_outputs

attention_head_outputs(value: Tensor) -> Tensor

state_output

state_output(value: Tensor) -> Tensor

state_after

state_after(t: int) -> Tensor

The state after token t of this call, [batch, heads, state_dim, head_dim]: states[:, t].

argument

argument(name: str)

A select function: where the scan call that fires takes name (the two kernels' call sites differ).

chunk_per_token

chunk_per_token(model: Any, enabled: bool = True) -> None

Run every Mamba-2 mixer of model with a chunk size of 1, so the chunk scan keeps the state after every token.

The chunk scan materializes the state at each chunk boundary (new_states, StateSpace.states); with chunk_size == 1 every token is a boundary. The outputs are the same; the cost is not: the recurrence between chunks is quadratic in the number of chunks, so a long prompt runs markedly slower. Unlike route_kernels, which binds a family's kernels process-wide, this sets an attribute of each mixer module of this one model (chunk_size, which the forward reads on every call), so it applies from the next trace and to no other model. Call it on a loaded model: a lazily built model's modules are replaced when its weights arrive. enabled=False restores the chunk size each mixer was built with (config.chunk_size, mamba_chunk_size on Bamba, Falcon-H1 and GraniteMoeHybrid).

needs_per_token_chunks

needs_per_token_chunks(envoy: Any) -> str | None

Why StateSpace.states is unavailable: the kernels have no source to read, or the chunk scan's chunks are longer than a token.