Skip to main content

Attention

attn_scores, attn_probs and value come from one call rather than as points: no module boundary holds a score matrix the paged kernel never forms, so both backends reconstruct it.

Scores, probs and values

from interp_engine import capture_attention, load_model

model = load_model("Qwen/Qwen3-8B")
out = capture_attention(model, model.to_tokens("The capital of France is"), [10])

out[10]["scores"] # [n_heads, dest, src]
out[10]["probs"] # softmax of scores
out[10]["value"] # [src, n_kv_heads, head_dim]

No batch axis, on either backend. On eager the model must be loaded with attn_implementation="eager", which backend="eager" does by default.

Several layers

from interp_engine import capture_attention, load_model

model = load_model("Qwen/Qwen3-8B")
out = capture_attention(model, model.to_tokens("Hello"), [0, 10, 20])

On the static backend

attn_probs is not a tap of its own. Declare the attn tap it is rebuilt from.

from interp_engine import Address, capture_attention, load_model

tap = Address("attn", 10) # q/k, which the matrix is rebuilt from
model = load_model("Qwen/Qwen3-8B", backend="vllm-static", static_points=[tap])

out = capture_attention(model, model.to_tokens("Hello"), [10])

Direct feature attribution

The identity probs @ value == z needs the per-head values the family actually feeds attention, not the raw value point.

from interp_engine import load_model, per_head_value, run_with_cache

model = load_model("google/gemma-2-2b-it", backend="eager")
cache = run_with_cache(model, model.to_tokens("Hello"), ["value.10", "attn_probs.10"])

v = per_head_value(model, cache, 10) # [batch, src, n_kv_heads, head_dim]
probs = cache.get("attn_probs", 10) # [batch, n_heads, dest, src]

On a gated-attention model (Qwen3-Next, Qwen3.5) the gate sits inside z, so DFA derived from probs @ value alone is off by exactly that factor:

from interp_engine import attn_out_gate, load_model, run_with_cache

model = load_model("Qwen/Qwen3-Next-80B-A3B-Instruct", backend="eager")
cache = run_with_cache(model, model.to_tokens("Hello"), ["attn_gate.10"])
gate = attn_out_gate(model, cache, 10) # [batch, pos, n_heads, head_dim]

Fused QKV

Splitting with the wrong layout returns a plausibly-scaled, meaningless tensor rather than raising, so use the helper.

from interp_engine import load_model, run_with_cache, split_fused_qkv

model = load_model("openai-community/gpt2", backend="eager")
cache = run_with_cache(model, model.to_tokens("Hello"), ["value.5"])
qkv = split_fused_qkv(model, cache.get("value", 5))
qkv["q"], qkv["k"], qkv["v"]