family mamba2
No attention and no MLP: each block is one norm and a Mamba-2 (SSD) mixer whose heads each keep a state matrix, with one scalar decay per head and token.
One block of the stack. Hover any part for its nnterp name; click to pin the card.
One block of the stack. Hover any part for its nnterp name; click to pin the card.
One block of the stack. Hover any part for its nnterp name; click to pin the card.
identity layers[i].input + linear_attn.attention_output == layer_output
⚠︎ marks a value this checkpoint has only under some load: hover it for the condition.
What print(model) shows on AntonV/mamba2-130m-hf: the native modules, standard names and every standard value with its layout.
Mamba2ForCausalLM(
(lm_head): Linear(in_features=768, out_features=50288, bias=False)
(generator): Generator(
(streamer): Streamer()
)
(embed_tokens): Embedding(50288, 768)
(layers): ModuleList(
(0-23): 24 x Mamba2Block(
(norm): Mamba2RMSNorm()
(linear_attn/mixer): Mamba2Mixer(
(act): SiLUActivation()
(conv1d): Conv1d(1792, 1792, kernel_size=(4,), stride=(1,), padding=(3,), groups=1792)
(in_proj): Linear(in_features=768, out_features=3352, bias=False)
(norm): MambaRMSNormGated()
(out_proj): Linear(in_features=1536, out_features=768, bias=False)
(attention_queries) -> SSDQueries [batch seq groups state_dim]: C, the queries reading the state
(attention_keys) -> SSDKeys [batch seq groups state_dim]: B, the keys writing into the state
(attention_values) -> SSDValues [batch seq heads head_dim]: x, the values written into the state
(betas) -> Gates [batch seq heads]: dt, the per-token write strength into the state
(decays) -> Gates [batch seq heads]: A * dt, the per-token log decay of the state
(state_input) -> State | None [batch heads key_dim value_dim]: The state entering the layer, or None at the start of a prompt (a copy of the cache's buffer)
(attention_head_outputs) -> SSDHeadOutputs [batch seq heads head_dim]: y, the per-head outputs before the gated norm and the output projection
(state_output) -> State [batch heads key_dim value_dim]: The state leaving the layer
(states) -> States [batch seq heads key_dim value_dim]: The state after every token of this call; needs nnterp.chunk_per_token(model)
(state): Unavailable: 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)
(set_state_after): Unavailable: 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
(attention_output) -> Residual [batch seq hidden_size]: What the mixer adds to the residual stream
)
(attention_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the attention
(layer_mid): Unavailable: a state-space block: one sublayer, the mixer, so there is no stream between sublayers and no MLP
(layer_input) -> Residual [batch seq hidden_size]: The residual stream entering the block
(mlp_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the MLP
(mlp_input) -> Residual [batch seq hidden_size]: What the MLP is called with
(layer_output) -> Residual [batch seq hidden_size]: The residual stream leaving the block, a tensor even when the block returns a tuple
)
)
(norm): Mamba2RMSNorm()
(logits) -> Logits [batch seq vocab_size]: The logits the model returns, after anything it does past lm_head (a softcap, a scale)
(token_embeddings) -> Residual [batch seq hidden_size]: The embedding module's output; layers[0].input is what enters the first block
(final_norm_output) -> Residual [batch seq hidden_size]: The residual stream after the final norm, what lm_head reads
(lm_head_output) -> Logits [batch seq vocab_size]: The head's logits before anything the model does past lm_head (a softcap, a scale)
(next_token_probs) -> NextTokenProbs [batch vocab_size]: The next-token distribution at the last position; derived, read-only
(input_ids) -> Tokens [batch seq]: The token ids the model was called with
(attention_mask) -> Tokens [batch seq]: The attention mask the model was called with; zeros are padding
(input_size): [batch, seq] of the current call; read-only
)What print(model) shows on AntonV/mamba2-370m-hf: the native modules, standard names and every standard value with its layout.
Mamba2ForCausalLM(
(lm_head): Linear(in_features=1024, out_features=50288, bias=False)
(generator): Generator(
(streamer): Streamer()
)
(embed_tokens): Embedding(50288, 1024)
(layers): ModuleList(
(0-47): 48 x Mamba2Block(
(norm): Mamba2RMSNorm()
(linear_attn/mixer): Mamba2Mixer(
(act): SiLUActivation()
(conv1d): Conv1d(2304, 2304, kernel_size=(4,), stride=(1,), padding=(3,), groups=2304)
(in_proj): Linear(in_features=1024, out_features=4384, bias=False)
(norm): MambaRMSNormGated()
(out_proj): Linear(in_features=2048, out_features=1024, bias=False)
(attention_queries) -> SSDQueries [batch seq groups state_dim]: C, the queries reading the state
(attention_keys) -> SSDKeys [batch seq groups state_dim]: B, the keys writing into the state
(attention_values) -> SSDValues [batch seq heads head_dim]: x, the values written into the state
(betas) -> Gates [batch seq heads]: dt, the per-token write strength into the state
(decays) -> Gates [batch seq heads]: A * dt, the per-token log decay of the state
(state_input) -> State | None [batch heads key_dim value_dim]: The state entering the layer, or None at the start of a prompt (a copy of the cache's buffer)
(attention_head_outputs) -> SSDHeadOutputs [batch seq heads head_dim]: y, the per-head outputs before the gated norm and the output projection
(state_output) -> State [batch heads key_dim value_dim]: The state leaving the layer
(states) -> States [batch seq heads key_dim value_dim]: The state after every token of this call; needs nnterp.chunk_per_token(model)
(state): Unavailable: 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)
(set_state_after): Unavailable: 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
(attention_output) -> Residual [batch seq hidden_size]: What the mixer adds to the residual stream
)
(attention_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the attention
(layer_mid): Unavailable: a state-space block: one sublayer, the mixer, so there is no stream between sublayers and no MLP
(layer_input) -> Residual [batch seq hidden_size]: The residual stream entering the block
(mlp_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the MLP
(mlp_input) -> Residual [batch seq hidden_size]: What the MLP is called with
(layer_output) -> Residual [batch seq hidden_size]: The residual stream leaving the block, a tensor even when the block returns a tuple
)
)
(norm): Mamba2RMSNorm()
(logits) -> Logits [batch seq vocab_size]: The logits the model returns, after anything it does past lm_head (a softcap, a scale)
(token_embeddings) -> Residual [batch seq hidden_size]: The embedding module's output; layers[0].input is what enters the first block
(final_norm_output) -> Residual [batch seq hidden_size]: The residual stream after the final norm, what lm_head reads
(lm_head_output) -> Logits [batch seq vocab_size]: The head's logits before anything the model does past lm_head (a softcap, a scale)
(next_token_probs) -> NextTokenProbs [batch vocab_size]: The next-token distribution at the last position; derived, read-only
(input_ids) -> Tokens [batch seq]: The token ids the model was called with
(attention_mask) -> Tokens [batch seq]: The attention mask the model was called with; zeros are padding
(input_size): [batch, seq] of the current call; read-only
)What print(model) shows on AntonV/mamba2-780m-hf: the native modules, standard names and every standard value with its layout.
Mamba2ForCausalLM(
(lm_head): Linear(in_features=1536, out_features=50288, bias=False)
(generator): Generator(
(streamer): Streamer()
)
(embed_tokens): Embedding(50288, 1536)
(layers): ModuleList(
(0-47): 48 x Mamba2Block(
(norm): Mamba2RMSNorm()
(linear_attn/mixer): Mamba2Mixer(
(act): SiLUActivation()
(conv1d): Conv1d(3328, 3328, kernel_size=(4,), stride=(1,), padding=(3,), groups=3328)
(in_proj): Linear(in_features=1536, out_features=6448, bias=False)
(norm): MambaRMSNormGated()
(out_proj): Linear(in_features=3072, out_features=1536, bias=False)
(attention_queries) -> SSDQueries [batch seq groups state_dim]: C, the queries reading the state
(attention_keys) -> SSDKeys [batch seq groups state_dim]: B, the keys writing into the state
(attention_values) -> SSDValues [batch seq heads head_dim]: x, the values written into the state
(betas) -> Gates [batch seq heads]: dt, the per-token write strength into the state
(decays) -> Gates [batch seq heads]: A * dt, the per-token log decay of the state
(state_input) -> State | None [batch heads key_dim value_dim]: The state entering the layer, or None at the start of a prompt (a copy of the cache's buffer)
(attention_head_outputs) -> SSDHeadOutputs [batch seq heads head_dim]: y, the per-head outputs before the gated norm and the output projection
(state_output) -> State [batch heads key_dim value_dim]: The state leaving the layer
(states) -> States [batch seq heads key_dim value_dim]: The state after every token of this call; needs nnterp.chunk_per_token(model)
(state): Unavailable: 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)
(set_state_after): Unavailable: 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
(attention_output) -> Residual [batch seq hidden_size]: What the mixer adds to the residual stream
)
(attention_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the attention
(layer_mid): Unavailable: a state-space block: one sublayer, the mixer, so there is no stream between sublayers and no MLP
(layer_input) -> Residual [batch seq hidden_size]: The residual stream entering the block
(mlp_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the MLP
(mlp_input) -> Residual [batch seq hidden_size]: What the MLP is called with
(layer_output) -> Residual [batch seq hidden_size]: The residual stream leaving the block, a tensor even when the block returns a tuple
)
)
(norm): Mamba2RMSNorm()
(logits) -> Logits [batch seq vocab_size]: The logits the model returns, after anything it does past lm_head (a softcap, a scale)
(token_embeddings) -> Residual [batch seq hidden_size]: The embedding module's output; layers[0].input is what enters the first block
(final_norm_output) -> Residual [batch seq hidden_size]: The residual stream after the final norm, what lm_head reads
(lm_head_output) -> Logits [batch seq vocab_size]: The head's logits before anything the model does past lm_head (a softcap, a scale)
(next_token_probs) -> NextTokenProbs [batch vocab_size]: The next-token distribution at the last position; derived, read-only
(input_ids) -> Tokens [batch seq]: The token ids the model was called with
(attention_mask) -> Tokens [batch seq]: The attention mask the model was called with; zeros are padding
(input_size): [batch, seq] of the current call; read-only
)What print(model) shows on AntonV/mamba2-1.3b-hf: the native modules, standard names and every standard value with its layout.
Mamba2ForCausalLM(
(lm_head): Linear(in_features=2048, out_features=50288, bias=False)
(generator): Generator(
(streamer): Streamer()
)
(embed_tokens): Embedding(50288, 2048)
(layers): ModuleList(
(0-47): 48 x Mamba2Block(
(norm): Mamba2RMSNorm()
(linear_attn/mixer): Mamba2Mixer(
(act): SiLUActivation()
(conv1d): Conv1d(4352, 4352, kernel_size=(4,), stride=(1,), padding=(3,), groups=4352)
(in_proj): Linear(in_features=2048, out_features=8512, bias=False)
(norm): MambaRMSNormGated()
(out_proj): Linear(in_features=4096, out_features=2048, bias=False)
(attention_queries) -> SSDQueries [batch seq groups state_dim]: C, the queries reading the state
(attention_keys) -> SSDKeys [batch seq groups state_dim]: B, the keys writing into the state
(attention_values) -> SSDValues [batch seq heads head_dim]: x, the values written into the state
(betas) -> Gates [batch seq heads]: dt, the per-token write strength into the state
(decays) -> Gates [batch seq heads]: A * dt, the per-token log decay of the state
(state_input) -> State | None [batch heads key_dim value_dim]: The state entering the layer, or None at the start of a prompt (a copy of the cache's buffer)
(attention_head_outputs) -> SSDHeadOutputs [batch seq heads head_dim]: y, the per-head outputs before the gated norm and the output projection
(state_output) -> State [batch heads key_dim value_dim]: The state leaving the layer
(states) -> States [batch seq heads key_dim value_dim]: The state after every token of this call; needs nnterp.chunk_per_token(model)
(state): Unavailable: 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)
(set_state_after): Unavailable: 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
(attention_output) -> Residual [batch seq hidden_size]: What the mixer adds to the residual stream
)
(attention_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the attention
(layer_mid): Unavailable: a state-space block: one sublayer, the mixer, so there is no stream between sublayers and no MLP
(layer_input) -> Residual [batch seq hidden_size]: The residual stream entering the block
(mlp_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the MLP
(mlp_input) -> Residual [batch seq hidden_size]: What the MLP is called with
(layer_output) -> Residual [batch seq hidden_size]: The residual stream leaving the block, a tensor even when the block returns a tuple
)
)
(norm): Mamba2RMSNorm()
(logits) -> Logits [batch seq vocab_size]: The logits the model returns, after anything it does past lm_head (a softcap, a scale)
(token_embeddings) -> Residual [batch seq hidden_size]: The embedding module's output; layers[0].input is what enters the first block
(final_norm_output) -> Residual [batch seq hidden_size]: The residual stream after the final norm, what lm_head reads
(lm_head_output) -> Logits [batch seq vocab_size]: The head's logits before anything the model does past lm_head (a softcap, a scale)
(next_token_probs) -> NextTokenProbs [batch vocab_size]: The next-token distribution at the last position; derived, read-only
(input_ids) -> Tokens [batch seq]: The token ids the model was called with
(attention_mask) -> Tokens [batch seq]: The attention mask the model was called with; zeros are padding
(input_size): [batch, seq] of the current call; read-only
)What print(model) shows on AntonV/mamba2-2.7b-hf: the native modules, standard names and every standard value with its layout.
Mamba2ForCausalLM(
(lm_head): Linear(in_features=2560, out_features=50288, bias=False)
(generator): Generator(
(streamer): Streamer()
)
(embed_tokens): Embedding(50288, 2560)
(layers): ModuleList(
(0-63): 64 x Mamba2Block(
(norm): Mamba2RMSNorm()
(linear_attn/mixer): Mamba2Mixer(
(act): SiLUActivation()
(conv1d): Conv1d(5376, 5376, kernel_size=(4,), stride=(1,), padding=(3,), groups=5376)
(in_proj): Linear(in_features=2560, out_features=10576, bias=False)
(norm): MambaRMSNormGated()
(out_proj): Linear(in_features=5120, out_features=2560, bias=False)
(attention_queries) -> SSDQueries [batch seq groups state_dim]: C, the queries reading the state
(attention_keys) -> SSDKeys [batch seq groups state_dim]: B, the keys writing into the state
(attention_values) -> SSDValues [batch seq heads head_dim]: x, the values written into the state
(betas) -> Gates [batch seq heads]: dt, the per-token write strength into the state
(decays) -> Gates [batch seq heads]: A * dt, the per-token log decay of the state
(state_input) -> State | None [batch heads key_dim value_dim]: The state entering the layer, or None at the start of a prompt (a copy of the cache's buffer)
(attention_head_outputs) -> SSDHeadOutputs [batch seq heads head_dim]: y, the per-head outputs before the gated norm and the output projection
(state_output) -> State [batch heads key_dim value_dim]: The state leaving the layer
(states) -> States [batch seq heads key_dim value_dim]: The state after every token of this call; needs nnterp.chunk_per_token(model)
(state): Unavailable: 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)
(set_state_after): Unavailable: 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
(attention_output) -> Residual [batch seq hidden_size]: What the mixer adds to the residual stream
)
(attention_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the attention
(layer_mid): Unavailable: a state-space block: one sublayer, the mixer, so there is no stream between sublayers and no MLP
(layer_input) -> Residual [batch seq hidden_size]: The residual stream entering the block
(mlp_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the MLP
(mlp_input) -> Residual [batch seq hidden_size]: What the MLP is called with
(layer_output) -> Residual [batch seq hidden_size]: The residual stream leaving the block, a tensor even when the block returns a tuple
)
)
(norm): Mamba2RMSNorm()
(logits) -> Logits [batch seq vocab_size]: The logits the model returns, after anything it does past lm_head (a softcap, a scale)
(token_embeddings) -> Residual [batch seq hidden_size]: The embedding module's output; layers[0].input is what enters the first block
(final_norm_output) -> Residual [batch seq hidden_size]: The residual stream after the final norm, what lm_head reads
(lm_head_output) -> Logits [batch seq vocab_size]: The head's logits before anything the model does past lm_head (a softcap, a scale)
(next_token_probs) -> NextTokenProbs [batch vocab_size]: The next-token distribution at the last position; derived, read-only
(input_ids) -> Tokens [batch seq]: The token ids the model was called with
(attention_mask) -> Tokens [batch seq]: The attention mask the model was called with; zeros are padding
(input_size): [batch, seq] of the current call; read-only
)What print(model) shows on mistralai/Mamba-Codestral-7B-v0.1: the native modules, standard names and every standard value with its layout.
Mamba2ForCausalLM(
(lm_head): Linear(in_features=4096, out_features=32768, bias=False)
(generator): Generator(
(streamer): Streamer()
)
(embed_tokens): Embedding(32768, 4096)
(layers): ModuleList(
(0-63): 64 x Mamba2Block(
(norm): Mamba2RMSNorm()
(linear_attn/mixer): Mamba2Mixer(
(act): SiLUActivation()
(conv1d): Conv1d(10240, 10240, kernel_size=(4,), stride=(1,), padding=(3,), groups=10240)
(in_proj): Linear(in_features=4096, out_features=18560, bias=False)
(norm): MambaRMSNormGated()
(out_proj): Linear(in_features=8192, out_features=4096, bias=False)
(attention_queries) -> SSDQueries [batch seq groups state_dim]: C, the queries reading the state
(attention_keys) -> SSDKeys [batch seq groups state_dim]: B, the keys writing into the state
(attention_values) -> SSDValues [batch seq heads head_dim]: x, the values written into the state
(betas) -> Gates [batch seq heads]: dt, the per-token write strength into the state
(decays) -> Gates [batch seq heads]: A * dt, the per-token log decay of the state
(state_input) -> State | None [batch heads key_dim value_dim]: The state entering the layer, or None at the start of a prompt (a copy of the cache's buffer)
(attention_head_outputs) -> SSDHeadOutputs [batch seq heads head_dim]: y, the per-head outputs before the gated norm and the output projection
(state_output) -> State [batch heads key_dim value_dim]: The state leaving the layer
(states) -> States [batch seq heads key_dim value_dim]: The state after every token of this call; needs nnterp.chunk_per_token(model)
(state): Unavailable: 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)
(set_state_after): Unavailable: 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
(attention_output) -> Residual [batch seq hidden_size]: What the mixer adds to the residual stream
)
(attention_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the attention
(layer_mid): Unavailable: a state-space block: one sublayer, the mixer, so there is no stream between sublayers and no MLP
(layer_input) -> Residual [batch seq hidden_size]: The residual stream entering the block
(mlp_norm_output) -> Residual [batch seq hidden_size]: The normed stream entering the MLP
(mlp_input) -> Residual [batch seq hidden_size]: What the MLP is called with
(layer_output) -> Residual [batch seq hidden_size]: The residual stream leaving the block, a tensor even when the block returns a tuple
)
)
(norm): Mamba2RMSNorm()
(logits) -> Logits [batch seq vocab_size]: The logits the model returns, after anything it does past lm_head (a softcap, a scale)
(token_embeddings) -> Residual [batch seq hidden_size]: The embedding module's output; layers[0].input is what enters the first block
(final_norm_output) -> Residual [batch seq hidden_size]: The residual stream after the final norm, what lm_head reads
(lm_head_output) -> Logits [batch seq vocab_size]: The head's logits before anything the model does past lm_head (a softcap, a scale)
(next_token_probs) -> NextTokenProbs [batch vocab_size]: The next-token distribution at the last position; derived, read-only
(input_ids) -> Tokens [batch seq]: The token ids the model was called with
(attention_mask) -> Tokens [batch seq]: The attention mask the model was called with; zeros are padding
(input_size): [batch, seq] of the current call; read-only
)out = x + mixer(norm(x)) # added in float32
mixer(h): z, xBC, dt = in_proj(h)
x, B, C = silu(conv1d(xBC)) # causal, width 4
y = ssd(x, dt, A, B, C) + D * x # attention_head_outputs
return out_proj(norm(y * silu(z)))
The block's norm keeps its native name, norm, and model.layers[i].norm is the norm before
the mixer; model.layers[i].linear_attn.norm is the mixer's own gated norm and model.norm the
final norm_f. The convolution runs over x, B and C together. The gated norm multiplies y by silu(z) and then normalizes over the mixer's
whole width (1536 on 130m), whatever n_groups is; attention_head_outputs is read before the gate
and the norm.
attention_output is the mixer's output (out_proj's), and it is all the block adds:
layers[i].input + attention_output == layer_output exactly, so ablating attention_output
ablates the block. The add is in float32: attention_output is in the load dtype and
layer_output is float32.
with model.trace(prompt):
x = model.layers[1].input.save()
attn = model.layers[1].linear_attn.attention_output.save()
out = model.layers[1].layer_output.save()
torch.testing.assert_close(x + attn.float(), out)
The stream grows through the stack: on 130m, at the last token of a 14-token prompt, its norm is 7.0 entering block 0 and 1316 entering block 23, and block 23 adds a vector of norm 1376.
With mamba_ssm installed transformers sends the prompt's chunk scan and the decode step's update
to its Triton kernels. On a CPU every
trace then fails inside the chunk scan, even one that reads only layer_output
(ValueError: Pointer argument cannot be accessed from Triton (cpu tensor?), or
RuntimeError: invalid argument to exchangeDevice with no GPU visible). On a GPU, unrouted,
layer_output, attention_output and logits read normally and every other linear_attn
value reports the kernel in support(). Route before the first trace:
import nnterp
from nnterp import StandardizedTransformer, route_kernels
route_kernels(nnterp.families.mamba2, "torch")
model = StandardizedTransformer("AntonV/mamba2-130m-hf")
On 130m in float32 the kernel and the routed torch scan give logits within 0.06 of each other (logits up to 120), with the same top token at every position; in bfloat16 within 2.25. The torch scan took 146 ms on a 14-token prompt against the kernel's 23 ms.
On 130m each mixer has 24 heads of 64 channels and a state of 128 per head, in one group:
attention_queries (C) and attention_keys (B) are [batch, seq, 1, 128], shared by all
24 heads, and attention_values (x) and attention_head_outputs (y) are
[batch, seq, 24, 64]. betas is the step softplus(dt + dt_bias) and decays is
betas * A, both [batch, seq, 24]: one scalar decay per head and token. Mamba-Codestral has 128 heads in 8 groups, so C and B are
[batch, seq, 8, 128] and each serves 16 heads.
The rates are far apart on real weights. A = -exp(A_log) runs from -4e-4 to -36316 over the
576 heads of 130m (median -0.21). Over every block of a 14-token prompt, exp(decays) has a
median of 0.89; 5% of entries keep less than 1% of the state and 19% keep more than 99%.
Every Mamba-2 checkpoint here has time_step_limit (0.0, inf), so the chunk scan clamps
nothing, and a written zero runs as zero: the token writes nothing and the state does not decay.
On 130m, with chunk_per_token, the state after token 3 then equals the state after token 2 bit
for bit, and a read of betas after the write returns the zero.
nnterp.chunk_per_token(model)
mix = model.layers[0].linear_attn
with model.trace(prompt):
betas = mix.betas.clone()
betas[:, 3] = 0
mix.betas = betas
states = mix.states.save()
assert torch.equal(states[:, 3], states[:, 2])
Writing every block's betas back unchanged moves 130m's float32 logits by up to 1.2e-4.
Per head the scan is a causal linear map over tokens: y_t = sum_s alpha[t, s] x_s + D x_t with
alpha[t, s] = (C_t . B_s) * exp(decays_{s+1} + ... + decays_t) * betas_s, one scalar per head
and query-key pair, so the map is a [seq, seq] matrix per head. The served values rebuild y
exactly (float32):
mix = model.layers[1].linear_attn
with model.trace(prompt):
C = mix.attention_queries[0].save() # [seq, groups, state_dim]
B = mix.attention_keys[0].save() # [seq, groups, state_dim]
x = mix.attention_values[0].save() # [seq, heads, head_dim]
beta = mix.betas[0].save() # [seq, heads]
decay = mix.decays[0].save() # [seq, heads]
y = mix.attention_head_outputs[0].save() # [seq, heads, head_dim]
D = mix._module.D
per_group = x.shape[1] // C.shape[1]
C, B = C.repeat_interleave(per_group, 1), B.repeat_interleave(per_group, 1)
cum = decay.cumsum(0)
causal = torch.ones(len(x), len(x), dtype=torch.bool, device=x.device).tril()[..., None]
kept = (cum[:, None] - cum[None, :]).masked_fill(~causal, -torch.inf).exp()
alpha = torch.einsum("thn,shn->tsh", C, B) * kept * beta # [query t, key s, head]
torch.testing.assert_close(torch.einsum("tsh,shd->thd", alpha, x) + D[:, None] * x, y)
state_output is the state after the call's last token, [batch, 24, 128, 64] on 130m, key
side first. states, the state after every token, needs nnterp.chunk_per_token(model): the
chunk scan keeps the state only every chunk_size tokens (256). Then states[:, -1] equals
state_output.
nnterp.chunk_per_token(model)
with model.trace(prompt):
states = mix.states.save() # [batch, seq, 24, 128, 64] on 130m
final = mix.state_output.save()
assert torch.equal(states[:, -1], final)
On a decode step the update comes before the read, so under generate read state_output
before attention_head_outputs on every step after the first:
heads, leaving = [], []
with model.generate(prompt, max_new_tokens=3, do_sample=False) as tracer:
for step in tracer.iter[:3]:
if step == 0:
heads.append(mix.attention_head_outputs.save())
leaving.append(mix.state_output.save())
else:
leaving.append(mix.state_output.save())
heads.append(mix.attention_head_outputs.save())
The width-4 convolution runs over x, B and C, so a state written through state_input
is not all a block carries into the next tokens.
On the 130m to 2.7b conversions lm_head is embed_tokens' weight, so an edit to one is an edit
to the other; Mamba-Codestral's head has its own. project_on_vocab at the last block's
layer_output equals logits exactly. The final norm's gain is its weight (mean 0.94, up to 4.9
on 130m). Nothing is added after embed_tokens, so token_embeddings equals layers[0].input.
state-spaces/mamba2-130m to -2.7b are the original release, stored for the mamba_ssm
package with no model_type; transformers loads the AntonV/mamba2-*-hf conversions, which
nnterp's docs use. They tokenize with GPT-NeoX-20B's BPE (GPTNeoXTokenizer, 50277 tokens; the
embedding has 50288 rows), prepend no BOS, and use <|endoftext|> (id 0) as BOS, EOS and
padding. Mamba-Codestral-7B is a code model with a 32768-token vocabulary.
C, B and x in the roles of attention_queries, attention_keys and
attention_values, with the decay mask built from decays.Mamba-2 (Mamba2ForCausalLM).
A pure state-space model: no attention and no MLP. The tree::
backbone.embeddings
backbone.layers[i] Mamba2Block
.norm RMSNorm, the block's only norm
.mixer Mamba2Mixer, an SSD mixer
backbone.norm_f
lm_head
Each block is hidden + mixer(norm(hidden)) (the residual in float32 when
residual_in_fp32), so the block's one contribution is the mixer's output
and layer.input + linear_attn.attention_output == layer_output. The mixer
is nnterp.StateSpace under the standard name linear_attn (the recurrent
mixer's name on every hybrid). The block's
norm keeps its name: a "norm" key would also match the mixer's own
gated norm, and its output is linear_attn.input. There is no
self_attn and no mlp, so support lists neither.
The sizes are the mixer's: num_heads and head_dim are the SSD heads
(config.num_heads, config.head_dim), and intermediate_size is the
mixer's inner width, expand * hidden_size, what in_proj expands to and
out_proj reads from (the model has no MLP). With residual_in_fp32 the
residual stream is float32 whatever the weights are, and the model casts it
to lm_head's dtype and its logits to float32, so project_on_vocab is the
family's own.