SSM / Recurrent-Model Interpretability¶
TransformerBridge exposes a family-agnostic interpretability surface for state-space
and linear-attention models — Mamba-1, Mamba-2, gated-delta-net (Qwen3.5 / Qwen3-Next),
and the SSM hybrids (NemotronH, GraniteMoeHybrid, Jamba, Zamba2, Falcon-H1). The same
three ActivationCache methods work across every family, dispatching to each block’s
SSM mixer regardless of which slot it occupies (.mixer for Mamba, .linear_attn for
gated-delta-net) and skipping a hybrid’s passthrough (attention / MLP / MoE) layers
automatically.
Jamba is Mamba-1 with selective-param RMSNorms (dt_layernorm / b_layernorm /
c_layernorm) after x_proj. SSMMixerBridge detects those norms for
compute_ssm_* reconstruction and the opt-in eager_scan path; stock Mamba-1 is
unchanged.
These features are bridge-only — there is no
HookedTransformercounterpart for SSM models. Load withTransformerBridge.boot_transformers(...).
Discovery¶
bridge = TransformerBridge.boot_transformers("state-spaces/mamba-130m-hf")
_, cache = bridge.run_with_cache(tokens, use_cache=False)
cache.ssm_layers() # -> [0, 1, 2, ...] block indices whose mixer is recurrent
ssm_layers() is purely structural (no dependence on cfg.layers_block_type): a
hybrid’s attention/MLP layers are excluded, so on NemotronH you get only the Mamba
layers.
Read-only analysis (no forward re-run)¶
Both methods reconstruct their quantities post-hoc from cached hooks. They return a
single tensor when every block is an SSM layer, else a {layer_idx: tensor} dict
over the SSM layers.
# Effective ("hidden") attention M = L ⊙ (C Bᵀ) [batch, heads, seq, seq]
M = cache.compute_ssm_effective_attention() # all SSM layers
M0 = cache.compute_ssm_effective_attention(layer=0) # one layer
# Recurrent state trajectory S_t
S = cache.compute_ssm_state() # all SSM layers
S5 = cache.compute_ssm_state(layer=5, time_step=-1) # one layer, final step (memory-bounded)
State shape is family-specific (Mamba-1 is per-channel; Mamba-2 and gated-delta-net
are per-head), so compute_ssm_state() returns a dict except when one mixer type
covers every block.
use_cache=Falseis required for gated-delta-net. Its interior hooks (hook_q/k/v,hook_beta,hook_log_decay) fire only on the hooked prefill path; the default cached path exposes onlyhook_in/hook_out, and the reconstruction methods then raise. Mamba reconstructs fromin_proj/conv1dhooks either way, but passinguse_cache=Falseuniformly is safe.
Canonical hook vocabulary¶
Every family exposes the same state-mutation names (defined once, in
SSMStateHookMixin), so interp tooling can address the same quantity across families:
Hook |
Meaning |
Kind |
|---|---|---|
|
post-scan recurrent-state trajectory |
real HookPoint (fires on the eager-scan path) |
|
per-step write influence |
real for Mamba-1/2 ( |
Additional canonical aliases resolve per family where the quantity exists:
hook_ssm_out, hook_ssm_B, hook_ssm_C, hook_ssm_decay, hook_ssm_dt.
Causal intervention: the eager_scan opt-in¶
By default the mixer runs HF’s fused recurrence kernel, which is opaque — the state
trajectory is never materialized, so it cannot be patched. Setting eager_scan = True
on a realized SSM mixer swaps the kernel for a readable Python scan that fires
hook_ssm_state (and, for the input-linear families, hook_ssm_write), so you can
read and edit the state mid-recurrence.
from transformer_lens.model_bridge.generalized_components.ssm_protocol import find_ssm_mixer
# Enable on every realized SSM mixer (skips hybrid passthrough slots).
mixers = [m for b in bridge.blocks if (m := find_ssm_mixer(b)) is not None]
for m in mixers:
m.eager_scan = True
# hook path uses the mixer's slot name: `.mixer` (Mamba) or `.linear_attn` (gated-delta-net)
STATE = "blocks.0.mixer.hook_ssm_state"
def zero_state(state, hook):
return torch.zeros_like(state) # ablate the whole recurrent state
logits = bridge.run_with_hooks(tokens, use_cache=False, fwd_hooks=[(STATE, zero_state)])
for m in mixers:
m.eager_scan = False # restore the default (bit-identical) fused path
Two intervention semantics (mirroring Mamba-Knockout and state-patching):
Patching
hook_ssm_statechanges only the same-position readout (y_t = C_t·S_torS_t^T q_t); it does not alter the forward recurrence.Patching
hook_ssm_writere-runs the recurrence, so the edit propagates to every later state. For gated-delta-net this is the write-strength gate (hook_beta), so zeroing it suppresses all writes.
Caveats¶
Prefill only |
The eager scan runs when |
Numerical |
The Python scan matches the fused kernel only to floating-point tolerance (≈1e-6 fp32), never bit-for-bit. |
Cost |
O(seq) Python with an O(batch·seq·…) state tensor — orders of magnitude slower/heavier than the kernel. Use short sequences. |
Default untouched |
With |
See
ssm2_mixer.py,ssm_mixer.py, andgated_delta_net.pyfor the per-family recurrences and reconstruction identities.