Skip to main content

Capture

run_with_cache is the same call on both backends. Switching backend is the backend= argument and nothing else.

One point

from interp_engine import Address, load_model, run_with_cache

model = load_model("Qwen/Qwen3-8B")
point = Address("resid_post", 10)

cache = run_with_cache(model, model.to_tokens("The capital of France is"), [point])
cache[point] # [batch, pos, d_model]

Several points

from interp_engine import load_model, run_with_cache

model = load_model("Qwen/Qwen3-8B")
tokens = model.to_tokens("The capital of France is")

cache = run_with_cache(model, tokens, ["resid_post.10", "mlp_out.10", "attn_out.10"])
cache.get("mlp_out", 10)

Every layer

from interp_engine import Address, load_model, run_with_cache

model = load_model("Qwen/Qwen3-8B")
points = [Address("resid_post", layer) for layer in range(model.n_layers)]

cache = run_with_cache(model, model.to_tokens("Hello"), points)

Naming a point

from interp_engine import Address, format_address, parse_address, to_address

Address("resid_post", 10) # canonical
parse_address("resid_post.10") # from the string form
to_address(("mlp_out", 3)) # the tuple form still works
Address("resid_streams", 5, 2) # name, layer, stream
format_address(Address("z", 7)) # "z.7"

A Cache accepts either an Address or its string form on lookup. The async capture method returns a plain dict keyed by Address, where a string is a KeyError.

While generating

Captures at prompt and generated positions, in one request.

from interp_engine import capture_generation, load_model

model = load_model("Qwen/Qwen3-8B")
tokens = model.to_tokens("The capital of France is")

completion, cache = capture_generation(model, tokens, ["resid_post.10"], max_tokens=8)
print(completion.text, completion.token_ids)
cache.get("resid_post", 10).shape[1] # len(prompt) + len(generated) - 1

One row short of the total: the final sampled token is never fed back through the model.

A batch

Eager only. vLLM takes one prompt per call, at its true length.

from interp_engine import load_model, run_with_cache

model = load_model("google/gemma-2-2b-it", backend="eager")
tokens = model.tok.to_tokens(["Paris is in", "Berlin is in"])

cache = run_with_cache(model, tokens, ["resid_post.10"])
cache.get("resid_post", 10) # [2, pos, d_model]

MoE routing

router_logits is served on both backends. The selection is eager-only, because it is formed inside a fused kernel vLLM never unfolds.

from interp_engine import expert_assignment, load_model, run_with_cache

model = load_model("Qwen/Qwen3-30B-A3B", backend="eager")
cache = run_with_cache(
model,
model.to_tokens("Hello"),
["router_logits.10", "expert_weights.10", "expert_indices.10"],
)
dense = expert_assignment(cache, 10, n_experts=128) # [batch, pos, n_experts]

Per-head contributions

n_heads times the size of z, so it is a helper rather than a point.

from interp_engine import head_contributions, load_model, run_with_cache

model = load_model("google/gemma-2-2b-it", backend="eager")
cache = run_with_cache(model, model.to_tokens("Hello"), ["z.10"])
head_contributions(model, cache, 10) # [batch, pos, n_heads, d_model]

Gradients

Eager only, and the model has to be loaded for it.

from interp_engine import load_model, run_with_cache

model = load_model("google/gemma-2-2b-it", backend="eager", requires_grad=True)
cache = run_with_cache(model, model.to_tokens("Hello"), ["resid_post.10"], detach=False)
cache.get("resid_post", 10).sum().backward()