Coverage for transformer_lens/model_bridge/generalized_components/ssm_protocol.py: 100%
23 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-08-11 18:50 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-08-11 18:50 +0000
1"""Family-agnostic discovery + shared state-mutation surface for SSM/recurrent mixers.
3Defines the structural contract (``SSMMixerProtocol``) that ``SSM2MixerBridge``
4(Mamba-2), ``SSMMixerBridge`` (Mamba-1), and ``GatedDeltaNetBridge`` all satisfy,
5plus a lookup that finds a block's SSM mixer regardless of which variant slot it
6occupies (``.mixer`` for Mamba, ``.linear_attn`` for gated-delta-net). A Protocol
7(not a base class) keeps each family's own hook set — only the discovery surface
8is shared. ``SSMStateHookMixin`` is the one place the canonical eager-scan
9intervention hooks (``hook_ssm_state`` + the ``eager_scan`` opt-in) are defined, so
10the three families share a single definition rather than repeating it ad hoc.
11"""
12from __future__ import annotations
14from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
16import torch
17import torch.nn as nn
19from transformer_lens.hook_points import HookPoint
20from transformer_lens.model_bridge.generalized_components.block import (
21 VARIANT_SUBMODULE_NAMES,
22)
24if TYPE_CHECKING:
25 from transformer_lens.ActivationCache import ActivationCache
28@runtime_checkable
29class SSMMixerProtocol(Protocol):
30 """An SSM/recurrent mixer that can materialize an effective-attention matrix.
32 Only ``compute_effective_attention(cache, layer_idx)`` is required; families
33 add their own optional keyword options (e.g. ``include_dt_scaling``,
34 ``per_state_coord``) which callers discover by signature introspection.
35 """
37 def compute_effective_attention(self, cache: "ActivationCache", layer_idx: int) -> torch.Tensor:
38 ...
41class SSMStateHookMixin:
42 """Canonical state-mutation hooks shared by the SSM/recurrent mixers.
44 One definition point so Mamba-1/Mamba-2/gated-delta-net expose the same names:
46 - ``hook_ssm_state`` — post-scan state trajectory ``S_t`` (created here for all);
47 patching it changes only the same-position readout, not the recurrence.
48 - ``hook_ssm_write`` — per-step write influence: a real HookPoint for the input-
49 linear families (Mamba-1/2 ``dt·(x⊗B)``, added by the subclass), an alias onto
50 ``hook_beta`` for the state-dependent delta rule; patching it re-runs the scan.
52 ``eager_scan`` (opt-in, prefill only) swaps HF's fused kernel for a Python scan so
53 these hooks fire; default False leaves the cached path bit-for-bit untouched. Must
54 precede ``GeneralizedComponent`` in the bases so ``super().__init__`` reaches it.
55 """
57 eager_scan: bool = False
59 def __init__(self, *args: Any, **kwargs: Any) -> None:
60 super().__init__(*args, **kwargs)
61 self.hook_ssm_state = HookPoint()
64# Children present on *every* SSM2MixerBridge regardless of whether it wraps a
65# real Mamba layer — universal I/O hooks, the wrapped module, and the eager-scan
66# analysis hooks. None of these signal that the mixer is realized.
67_PASSTHROUGH_CHILDREN = frozenset(
68 {"hook_in", "hook_out", "_original_component", "hook_ssm_write", "hook_ssm_state"}
69)
72def _is_realized_ssm_mixer(mixer: object) -> bool:
73 """True unless ``mixer`` is a no-op passthrough wrapper.
75 A hybrid like NemotronH wires a single ``SSM2MixerBridge`` ``.mixer`` slot on
76 *every* layer; on attention / MLP / MoE layers its optional projection
77 submodules are skipped, leaving only the universal hooks. A realized mixer
78 always has something more — projection submodules (Mamba-1/2) or interior
79 hooks like ``hook_q`` / ``hook_log_decay`` (gated-delta-net). This structural
80 check needs no ``cfg.layers_block_type``.
81 """
82 return any(name not in _PASSTHROUGH_CHILDREN for name in getattr(mixer, "_modules", {}))
85def find_ssm_mixer(block: nn.Module) -> Optional[SSMMixerProtocol]:
86 """Return the block's SSM mixer submodule, or None if it has none.
88 Scans the layer-type variant slots (``.mixer`` / ``.linear_attn`` / ``.mamba``
89 / ``.ssm``) and returns the first that conforms to ``SSMMixerProtocol`` and is
90 a realized SSM mixer (not a passthrough wrapper). An attention layer (only
91 ``.attn``), or a hybrid's passthrough ``.mixer`` on a non-SSM layer, returns
92 None.
93 """
94 modules = getattr(block, "_modules", {})
95 for name in VARIANT_SUBMODULE_NAMES:
96 sub = modules.get(name)
97 if isinstance(sub, SSMMixerProtocol) and _is_realized_ssm_mixer(sub):
98 return sub
99 return None