Skip to content

Circuit Analysis

What is a circuit?

A circuit is the minimal subset of model components (attention heads, MLP layers) that is causally sufficient to implement a specific behaviour.

Finding a circuit means: 1. You have identified which components matter 2. You have verified this causally (ablating non-circuit components doesn't hurt performance; ablating circuit components does) 3. You understand what algorithm the circuit implements


CircuitAblator

from kamui.mechinterp import CircuitAblator

ablator = CircuitAblator(model)

# Zero-ablate specific components and measure effect
result = ablator.ablate(
    components=["blocks.1.attn", "blocks.2.attn"],
    token_ids=token_ids,
    metric=logit_difference,
)
print(result.delta)   # how much the metric changed

# Mean ablation (more principled than zero ablation)
result = ablator.mean_ablate(
    components=["blocks.1.attn"],
    token_ids=token_ids,
    baseline_ids=random_baseline_ids,
    metric=logit_difference,
)

Zero ablation vs. mean ablation

Zero ablation: replace the component's output with zeros. - Fast and simple - Can produce out-of-distribution activations (the model has never seen a zero activation stream at that point)

Mean ablation: replace with the mean activation over a baseline dataset. - More principled: removes the task-specific contribution while keeping baseline statistics intact - Preferred for published results


Circuit discovery workflow

Step 1: Identify candidate layers
    → ActivationPatcher.patch_all_layers()
    → Find layers with effect > 0.3

Step 2: Identify candidate heads within those layers
    → ActivationPatcher.patch_all_heads()
    → Find (layer, head) pairs with effect > 0.3

Step 3: Verify the circuit
    → CircuitAblator: ablate ONLY the circuit components
    → Verify performance is preserved (circuit is sufficient)
    → CircuitAblator: ablate everything EXCEPT the circuit
    → Verify performance drops (circuit is necessary)

Step 4: Understand the algorithm
    → AttentionVisualizer: what does each head attend to?
    → LogitLens: what does each head contribute to the residual stream?
    → Write a natural-language description of the algorithm

Automatic circuit discovery

find_minimal_circuit implements a greedy search:

from kamui.mechinterp.circuits import find_minimal_circuit

circuit = find_minimal_circuit(
    model=model,
    task_ids=task_token_ids,
    task_metric=logit_difference,
    threshold=0.95,   # preserve 95% of full-model performance
)
print(circuit)
# ["blocks.1.attn.head_2", "blocks.1.attn.head_5", "blocks.0.attn.head_0"]

Warning: greedy search is approximate. It may miss circuits where multiple components together matter but individually don't. Always verify the result manually.


Example: the induction circuit

Components: {(layer=0, head=?), (layer=1, head=2), (layer=1, head=5)}

Algorithm:
    Layer 0, prev-token head:
        At position t, attends to t-1
        Writes "previous token" signal to residual stream

    Layer 1, induction heads (2 and 5):
        Read "previous token" signal
        Attend to the last occurrence of (current token preceded by prev token)
        Boost the next-token prediction accordingly

This is sufficient to explain in-context copying behaviour.

References

Wang et al. (2022). Interpretability in the Wild. https://arxiv.org/abs/2211.00593

Conmy et al. (2023). Towards Automated Circuit Discovery. NeurIPS 2023. https://arxiv.org/abs/2304.14997