interp-engine is the inference engine behind Neuronpedia, released under Apache-2.0 and announced on August 31. It gives 34 hook points one name each across model families, checks them against TransformerLens and nnsight on 35 committed models, and runs them inside vLLM for throughput. Its correctness claim is stronger than its speed claim. The speed needs a CUDA machine and a fixed set of taps. The parity table can be checked on a laptop, and I checked it on mine.

The best argument for the engine is a number that looked fine. Neuronpedia's old inference server translated TransformerLens's blocks.4.hook_mlp_out into the raw MLP output for gemma-2-2b and fed it to a Gemma Scope sparse autoencoder that had been trained on a different tensor. Nothing raised. The SAE's reconstruction error came back at 9.8 instead of 0.26, worse than predicting the mean, with 8 active features instead of the declared 85. The endpoint returned zeros, and a whole SAE source stopped firing on the very text its dashboards were built from. The maintainers tell this story themselves in the hook-mapping guide.

34
named points; 28 of them reachable on vLLM
35
models in the committed cross-engine parity table
6.9×
gemma-2-2b, one stream, B200; 41× with eight requests

A wrong tensor with the right name

A Gemma-2 block is a sandwich. Each sublayer has a norm before it and a second norm after it, and only the normed output is added to the residual stream. TransformerLens's block-level hook_mlp_out fires after that second norm; the source comment says it does so "so hook_attn_out captures that which is added." On a Llama-shaped block there is no second norm, so the raw MLP output and the residual contribution are the same tensor and the name is safe. On Gemma they are two tensors, and the name picks one of them without telling you.

interp-engine's answer is to spell both. mlp_out is the raw module output. mlp_out_post is what gets added. On families without a post-sublayer norm the second aliases the first, so asking for the contribution point is safe everywhere. The architecture facts detect the norm structurally, on a real block. Checking whether the model is called Gemma would not do: Gemma-1 has none of these norms and VaultGemma removes them.

Point on a Gemma-2 blockWhat it isTransformerLensnnsight / nnterp
resid_preblock inputblocks.4.hook_resid_prelayers_input[4]
attn_outraw attention outputblocks.4.attn.hook_out (v3 bridge)attentions_output[4]
attn_out_postafter the post-attention norm; what is addedblocks.4.hook_attn_out
resid_midafter the attention addblocks.4.hook_resid_midno accessor
mlp_outraw MLP outputblocks.4.mlp.hook_out (v3 bridge)mlps_output[4]
mlp_out_postafter the post-MLP norm; what is addedblocks.4.hook_mlp_out
resid_postblock outputblocks.4.hook_resid_postlayers_output[4]

The two highlighted rows are the trap. TransformerLens and nnterp default to different sides of the norm, both call their choice the MLP output, and the mapping between them is model-dependent. interp-engine ships a mapper that translates hook names in both directions and refuses names that have no faithful equivalent, such as a norm's hook_normalized, which TransformerLens fires between the scale and the gain and which no Hugging Face module ever outputs.

One address, three engines

The eager backend is plain Hugging Face. load_model loads the checkpoint, the point registry turns Address("mlp_out_post", 4) into a module, a tensor side and an optional transform, and run_with_cache installs ordinary PyTorch hooks around one forward pass. Nothing about this path needs a GPU. It is the same trick every interpretability library uses, with a fixed vocabulary on top and tests that reconstruct the sandwich equations to make sure raw and post outputs stay distinct.

vLLM is where the engineering is. A hook fires while a CUDA graph is being recorded and never again during replay, so the hooked backend runs vLLM with enforce_eager=True and prefix caching off, or salted per request when a steer would poison a cached prefix. That keeps every point reachable and gives up most of what vLLM is for: gemma-2-2b decodes at 31.5 tokens per second hooked against 30.9 eager. The static backend goes the other way. It wraps the modules before graph capture so that a copy_ into a capture buffer and an add_ from a steering buffer are recorded into the graph and replayed with it. A self-test writes a sentinel and checks it survives replay. The price is that you declare the taps at load time, hold extra VRAM for the buffers, and lose most of the batch window, from 16,384 tokens to 1,024.

gemma-2-2b on a B200, bf16HF eagerhooked vLLMstatic vLLMvanilla vLLM
decode, one stream (tok/s)30.931.5214354
decode, eight requests (aggregate tok/s)30.12261,2381,733
capture one middle point (ms)34.985.390.2
generate 32 tokens with capture (ms)1,1321,051185
logit lens, top 10 (ms)3.3202202205

Read the headline against that table. The launch post says "over 40x the throughput vs HF transformers." The 41× is the eight-request aggregate on the static backend, where eager serializes requests that vLLM batches. One stream is 6.9×. Static capture still costs vLLM 40% of its own decode speed, and a single capture or a logit lens is slower through vLLM than through eager, because the tensor has to cross a worker boundary. The benchmark is one B200, bf16, interp-engine 1.2.0 on vLLM 0.26, run on August 19. The audited release is 1.5.1 and now requires vLLM 0.28.

What a green cell means

The part of the repository I would keep if the engine vanished is the validator. It runs each model through eager, hooked vLLM, static vLLM, TransformerLens 2 and 3, and nnsight, and commits a per-point comparison. The thresholds are the fine print. A raw Hugging Face pair passes at a maximum absolute error of 0.002 and cosine similarity of 0.9999. A pair involving TransformerLens or a fused kernel passes at cosine 0.99 and relative error 0.5, falls to a warning below that, and fails only when shapes differ or cosine drops under 0.5. Layers are sampled at the first, middle and last block, plus three-quarter depth. Twenty-seven of the 34 points are compared; resid_pre, mlp_in, attn_probs and four others are not.

gemma-2-2b is green across the board, and the detail file shows what green tolerates. The vLLM column is bf16 against an fp32 reference. Whole-tensor cosines sit above 0.997; the worst single token on final_norm sits at 0.961. gpt2 is fp32 everywhere and differs nowhere. DeepSeek-V4-Flash fails on both vLLM backends at its last layer, with mlp_out at cosine 0.235, and because no other engine runs that model the table cannot say which side is wrong. Gemma-4 12B and 26B carry warnings on vLLM that the bug registry does not explain, while 31B passes.

Coverage

The validator README says "50+ models." The committed table has 35 rows and the same page counts 31 verified architectures with 46 unaudited. Ordinary CI scores those committed files without loading weights, runs gpt2 parity on CPU, two sub-billion models, and one L4 job with real vLLM. The full cross-engine sweep is a manual workflow on self-hosted hardware, so a green cell carries the engine and vLLM version it was made with, not today's.

Whose layer is this

On August 21, Neuronpedia's repository took a 1,119-file commit whose message reads "Migrated inference, autointerp and graph services to new engine." The inference README now says the engine "replaced the previous TransformerLens + nnsight stack," and the graph service depends on interp-engine with no TransformerLens in its manifest. In January the same site said nnsight powered its backends. That is the maintainer thesis in one move: interpretability at production scale runs inside a serving engine, and the semantic layer that names the points belongs to whoever runs it.

The skeptic's case is that this is Neuronpedia's hosting convenience and researchers will keep their tools. It has evidence. Nothing differentiates through vLLM; the worker runs under inference mode and its kernels have no backward. The vLLM worker exposes a closed list of remote calls, so an arbitrary patching function cannot cross into it, though the eager backend does accept callables and dotted module paths. TransformerLens is not retreating: version 3.8.1 shipped the day after the launch post, and its TransformerBridge now wraps native Hugging Face models where version 2 reimplemented them. nnsight 0.7.0 ships its own vLLM server. The incumbents are converging on the same design from the other side.

I read the field as segmenting rather than switching. interp-engine wants repeatable, concurrent, product-facing inference. TransformerLens wants the research surface. nnsight wants programmable traces and remote execution. The contest that is left is over who names the points, and that is why the validator is the durable piece. It already records a real production failure, six execution paths, and version-pinned diffs. Whether it becomes a standard other engines score against is speculative; no outside project uses its vocabulary yet, and the repository had zero issues and zero pull requests from anyone else when I checked.

The maintenance picture is thin in the way a two-week-old project is thin. Thirty-one commits since August 20, one human author, thirteen commits co-signed by the Cursor agent, seven releases in twelve days. The vLLM integration monkeypatches the worker's load_model, reaches into private model-runner attributes, and pins nothing above its floor. The README says Gemma 4 needs transformers 5.14.1; the Gemma-4 validator cell ran on 5.16.1. Pin the engine, vLLM, transformers and torch together, and replay your own parity before you move any of them.

What runs on my Mac

I care about this engine because I run the workload it serves. My research harness extracts contrastive activation vectors from gemma-2-2b through TransformerLens 3.5.1 on Apple Silicon and injects them at resid_post to measure how many directions a frozen model can hold at once. Every number I have depends on reading the tensor I think I am reading. So the question I could answer on a laptop was the one the validator answers on a B200: does the eager backend agree with the tool I already trust, and does the trap show up when I pair the names naively?

Two things surprised me before a single forward pass. First, automatic device selection would have put gemma-2-2b on the CPU. The checkpoint is bf16-native and the engine treats MPS as unsafe for bf16 weights, so unless you ask for the device and a dtype explicitly you get a correct, slow run and no warning about speed. I asked for MPS and fp32. Second, TransformerLens greets that same device with a warning that "MPS backend may produce silently incorrect results" on the PyTorch I have. Two tools, two different opinions about my machine, and no third party to break the tie except the numbers.

The numbers broke the tie. I captured four points at five layers on both models with one 29-token prompt, both engines in fp32 on MPS, TransformerLens loaded without weight folding so the tensors are comparable. Where the names mean the same tensor, the two engines agree to floating-point noise: the largest absolute difference across twenty gemma-2-2b comparisons is 5.3e-4, at the last layer's resid_post. Then I paired the names the way Neuronpedia's old server did, raw mlp_out against blocks.N.hook_mlp_out, and the same run reproduced the trap.

Last-token cosinegemma-2-2b L0L13L25gpt2, every layer
mlp_out_post ↔ hook_mlp_out (matched)0.999991.000001.000001.00000
mlp_out ↔ hook_mlp_out (naive)0.8740.8030.8951.00000
attn_out_post ↔ hook_attn_out (matched)1.000000.999991.000001.00000
attn_out ↔ hook_attn_out (naive)0.8290.7120.7911.00000

A cosine of 0.87 is the dangerous kind of wrong. It is far from random, it has the right shape, and an SAE trained on the other tensor will encode it into something that looks like sparse features. On gpt2 the same naive pairing is exact, which is why code that was tested on gpt2 carries the bug to Gemma without noticing. The maximum absolute difference on the naive gemma rows runs from 15 to 272; on the matched rows it never passes 0.001.

Steering held too. I built a contrastive vector the way my harness does, six sentiment pairs, mean difference at the final token of resid_post, layer 13 on gemma-2-2b and layer 6 on gpt2, and injected it at four times its own norm from the last prompt token onward. interp-engine's steer takes that mask as a list of excluded prompt positions and applies the delta to every generated token; a hand-written TransformerLens hook did the same arithmetic. Next-token logits differed by at most 7.7e-5 with the same argmax, and twenty greedy tokens came out identical on both models. At that strength the text is already degenerate, which is the point: the two engines agree even where the model has stopped making sense.

Speed on a laptop is the part the benchmark cannot tell you, so I measured it: a 128-token prompt, 64 greedy tokens, fp32 on MPS, one warm-up and three timed runs, capturing resid_post at one layer throughout. The two engines capture differently. interp-engine generates and then runs one extra forward over the finished sequence to collect the point; TransformerLens hooks each position during cached decoding. The numbers are medians in tokens per second, with the sampled peak of Metal driver memory beside them.

MPS, fp32, 128 + 64 tokensplain transformers, no hooksinterp-engine eager + captureTransformerLens + caching hook
gemma-2-2b (tok/s)7.66.21.9
gemma-2-2b peak driver memory11.1 GB12.4 GB17.0 GB
gpt2 (tok/s)57.885.939.6

Nobody gets the B200 story here, and nobody should expect to. What the table does say is that the eager backend costs almost nothing over bare transformers on the model I use, and that my current harness is the slow one. On gpt2 interp-engine's own decode loop beats generate outright. On gemma-2-2b it gives back a fifth of the speed for the recapture pass and stays 4.6 gigabytes under TransformerLens on a machine with 24.

What changed in my harness is small and specific. My vectors come from resid_post, which is the one name every engine agrees on, so the trap was not in my extraction. It is waiting in the next phase, where I move from raw directions to a Gemma Scope SAE basis and the SAE's declared hook is exactly the kind of name that means two tensors. I now translate every hook string through the mapper before I trust it, and I keep the naive-pairing check in the test suite as a tripwire.

Who should use it

Try it now if you serve capture or residual steering to many concurrent callers on Linux with a CUDA card, you can fix the taps you need, and you can pin the whole stack and replay parity before upgrades. The verified configurations are Qwen3-4B on an A40 and a B200. Try the eager backend now if you consume block-level hook names from someone else's SAE, transcoder or lens and want a mapper that refuses to guess.

Wait if you expected the speedup on a Mac, a free Colab, or a consumer GPU; nothing is verified there. Wait if your work needs gradients through the model, arbitrary patching inside the fast path, head-level tensors on more than one GPU, neuron-basis points on a sparse MoE, or exact parity on DeepSeek-V4-Flash or Gemma-4 12B and 26B.

Two results would change this verdict. A fp16 eager run on Apple Silicon across every applicable point at four layers and three prompt lengths, passing at mean cosine 0.9999 and max absolute error 0.002 with identical greedy tokens, would move Mac users from "unverified" to "supported." A Qwen3-4B static run on one RTX 4090 at 8,192 context, holding at least 5× one-stream and 20× aggregate over eager with no cross-request contamination, would make the headline relevant to hardware researchers own.

A hook name is a promise about a tensor. This engine is the first one I have used that writes the promise down and checks it.

interp-engine earns a place in a CUDA serving stack today and a place in any harness that consumes hook names, and it does not yet earn the speed story on the hardware most researchers have. This is the third issue in a row where the interesting move happens inside a runtime: Herdr turned a terminal multiplexer into one, Bun 1.4 pulled package jobs into one, and here a serving engine becomes the bench interpretability has to run on. The next issue stays there: DFlash 2, a block-diffusion drafter for speculative decoding that also only started to matter once it landed inside vLLM and llama.cpp, and that I can run on this Mac through MLX.


Sources & Evidence
interp-engine repository at the audited commit, Apache-2.0, 34 points, benchmark table Every code claim in this issue is pinned to this commit; the README carries the throughput table the headline is read against.
decoderesearch/interp-engine @ 7471609
Launch post: "34 standardized hook points," "over 40x the throughput vs HF transformers," "this is the v1" The maintainers’ own framing, quoted so the reader can judge the headline against the benchmark rows.
Neuronpedia blog, 2026-08-31
Neuronpedia served gemmascope-mlp-16k off the raw MLP output; FVU 9.8 against 0.26; L0 8 against 85; the TransformerLens naming collision The production failure that motivates the engine and the mapping rules, told by the people who made the mistake.
ENGINE_HOOK_MAPPINGS.md
Which of the 34 points each backend serves; the six unreachable on vLLM; attention matrices by recompute Defines the contract the "34 points" claim actually makes.
SUPPORTED_POINTS.md
TransformerLens applies Gemma-2’s post-sublayer norm before hook_attn_out and hook_mlp_out The source comment that explains why the block-level name is the contribution, not the raw output.
transformer_block.py, TransformerLens 3.5.1
Validator tolerances: 0.002 / 0.9999 for raw HF pairs, cosine 0.99 and relative error 0.5 otherwise; sampled layers; 27 compared points What a green cell means is decided here, not in the summary table.
validator/comparison/spec.py
gemma-2-2b cross-engine result: vLLM bf16 38 agree, TransformerLens 2 and 3 36 agree, nnsight 32 agree; worst-token cosine 0.961 The committed evidence behind the green cell for the model I use.
gemma-2-2b result details
DeepSeek-V4-Flash fails both vLLM backends at layer 42 with mlp_out cosine 0.235 The clearest exception to any blanket correctness claim.
DeepSeek-V4-Flash result details
Full benchmark report: B200, bf16, interp-engine 1.2.0, vLLM 0.26, 2026-08-19; capture, steering and lens latencies The environment and the slower-than-eager capture and lens rows that the README table omits.
benchmarks/results-latest.md
Hooked vLLM defaults to enforce_eager and no prefix caching; static taps recorded into CUDA graphs; batch window 16,384 to 1,024 The mechanism and the price of each backend.
PERFORMANCE.md
Neuronpedia migrated inference, autointerp and graph services to interp-engine in one 1,119-file commit on 2026-08-21 The first production deployment and the reversal from the nnsight stack announced in January.
hijohnnylin/neuronpedia @ 17bc391
Neuronpedia inference README: the engine "replaced the previous TransformerLens + nnsight stack" Confirms the outcome of the migration in the maintainers’ own words; live main, not pinned.
apps/inference/README.md
TransformerLens 3.8.1 released 2026-09-01 after weekly August releases Kills the story that the incumbent is dormant.
transformer-lens on PyPI
nnsight 0.7.0 adds a vLLM-backed server with the same trace interface The rival converging on the serving-engine design from the research side.
nnsight releases
Verified GPU fits: Qwen3-4B on A40 and B200; no passing consumer-GPU row Where the speed claim has been demonstrated and where it has not.
gpu-sizer/VERIFIED.md
MPS device selection: bf16-native checkpoints fall back to CPU unless fp16 is requested The default that would have silently moved my run off the GPU.
interp_engine/select.py
Reproduction scripts, JSON results and environment for the Apple Silicon parity, steering and throughput runs Every number in the laptop section comes from these files; rerun them to check me.
Research artifacts