Execution Backends (the Driver System)¶
TransformerLens v4 separates what you study (the bridge’s hook names, cache, and intervention surface) from what runs the forward pass (a Driver). Every backend — local HuggingFace transformers, vLLM, or an inspect_ai provider — satisfies the same protocol, so the same hook names work everywhere; what changes is which hooks each backend can fire and whether gradients exist at all.
This page covers the user-facing surface. The contract lives in transformer_lens/model_bridge/driver_protocol.py.
The Driver protocol¶
A driver is anything that implements:
def forward(input_ids, *, capture=(), intervene=None,
max_new_tokens=1, return_logits=True, **kwargs) -> ForwardResult
def close() -> None
def supports(feature: str) -> bool
plus two declared hook-name sets:
supported_hook_points— canonical bridge hook names this backend can fire (e.g.blocks.0.hook_out).non_fireable_hook_points— names the backend structurally cannot serve (fused kernels, sampler shortcuts).
ForwardResult carries logits, a captured mapping of hook name → activation, and the engine’s raw_output. Tensors are native to the driver’s framework and converted at the bridge boundary. validate_driver checks the contract when a bridge is constructed.
Interventions come in two dialects:
Callables (
InterventionFn) — arbitrary Python hook functions, for drivers that can dispatch Python at the engine boundary (the transformers backend).Declarative specs (
InterventionSpec) — plain mappings like{"op": "suppress"}, for drivers that can’t run Python mid-forward (vLLM undertorch.compile, remote providers).
Capability tiers follow from this: circuit-finding and anything gradient-based runs on the transformers backend; capture and steering scale out on vLLM; both use the same hook names, so analyses transfer between them.
The three backends¶
transformers — full hooks + gradients¶
The reference backend. Wraps a local HF nn.Module; the full HookPoint tree fires, backward hooks and gradients work, and parameters() / state_dict() / weight access are all available.
from transformer_lens.model_bridge import TransformerBridge
bridge = TransformerBridge.boot_transformers("gpt2", device="cpu")
logits, cache = bridge.run_with_cache("Hello, world")
There is also TransformerBridge.boot_native(config), which builds a small randomly-initialized TL-native model on the same driver — no HuggingFace Hub call — useful for tests and toy-model experiments.
vLLM — high-throughput capture + declarative interventions¶
RemoteBridge.boot_vllm constructs a vLLM engine (PagedAttention, torch.compile, CUDA graphs) and installs capture hooks inside the worker before compilation. Activations come back over collective_rpc and replay through the bridge’s HookPoint tree. This is the throughput path for SAE/probe data collection; unlike observation-only tools (vllm-lens), each hook also applies an affine transform output = output * scale + bias, so declarative interventions (suppress / scale / add / set) propagate to downstream layers.
import torch
from transformer_lens.model_bridge import RemoteBridge
bridge = RemoteBridge.boot_vllm(
"meta-llama/Llama-3.2-1B",
dtype=torch.float16,
max_model_len=2048, # cap the KV-cache reservation
)
logits, cache = bridge.run_with_cache("Hello, world")
# Declarative intervention: zero the embedding output for this forward only.
logits2, cache2 = bridge.run_with_cache(
"Hello, world",
intervene={"embed.hook_out": {"op": "suppress"}},
)
Notes grounded in the source docstrings (sources/vllm/source.py):
Fireable hooks (decoder-only overlay):
embed.hook_out,blocks.{i}.hook_out/attn.hook_out/mlp.hook_out,ln_final.hook_normalized.Returned logits are reconstructed full-sequence logits: vLLM’s sampler bypasses
lm_head, so the driver rebuilds them host-side asln_final @ lm_head.weight.T(+ bias, + Gemma soft-cap) — valid at every position, so loss works. If the unembedding weight is unreachable it falls back to final-position log-probs and the bridge rejectsreturn_type="loss".Convention alignment: vLLM materializes
ln_finalpost-weight, but the driver un-folds the exposed capture (÷ weight, or ÷ (1 + weight) for Gemma) soln_final.hook_normalizedmatches the pre-weight valueboot_transformersserves. If the norm weight is unreachable it warns and serves the raw post-weight value. Seesources/vllm/overlays/decoder_only.pyfor which hooks diverge from HF conventions.enable_batching=Trueswitches to the eager batched path (batch_size > 1, chunked prefill) for data collection;enable_position_interventions=Truelets a spec carry aposfield (int or list) to scope an edit to specific sequence positions.tensor_parallel_size=2enables single-node tensor parallelism (GPU-validated: capture/intervention/logit parity vs TP=1 within the standard band): every served hook point is post-all-reduce and replicated across a stage’s TP ranks, with a first-forward cross-rank check that fails loud if that ever stops holding; vocab-sharded unembeddings are gathered for logit reconstruction. Incompatible withenable_batching; multi-node (Ray) remains unsupported.pipeline_parallel_size=2enables single-node pipeline parallelism (GPU-validated: capture/intervention/logit parity vs single-rank within the standard band): each stage owns a disjoint layer slice, so the driver merges per-rank capture reads (fail-loud if a requested hook is served by no rank) and each rank applies only the intervention specs whose hooks it owns. Ownership is determined by whether a hook actually fired, not by module presence — tied-embedding models aliasembed_tokensonto the last stage and some builds instantiate the final norm everywhere. The first-forward layout check also verifies TP replicas of each stage fired identically under vLLM’s PP microbatching. Composes withtensor_parallel_size; sameenable_batchingrestriction.Requires a CUDA GPU. Install with
pip install "transformer-lens[vllm]"(oruv sync --extra vllm). The extra is Linux-only (vLLM ships no macOS/Windows wheels), pins the validatedvllm 0.20.xband — which in turn pins its matchingtorch— and cannot co-install with the[lit]extra (numpy version conflict). Seesources/vllm/internals.pybefore bumping the band.
Inspect — interp inside inspect_ai evals¶
RemoteBridge.boot_inspect wraps an inspect_ai model provider in a bridge, so activation capture and interventions run inside the same harness as behavioral evals. Install with the inspect extra (uv sync --extra inspect, or pip install "transformer-lens[inspect]").
from transformer_lens.model_bridge import RemoteBridge
bridge = RemoteBridge.boot_inspect("HuggingFaceTB/SmolLM2-135M") # provider="tl_bridge"
logits, cache = bridge.run_with_cache("Hello, world")
From the boot_inspect docstring:
The default
tl_bridgeprovider is HF-backed: residual/attn/mlp capture, full affine interventions, and full-sequence logits.tl_bridge_vllmis the vLLM-backed sibling;provider="vllm-lens"targets a running vllm-lens provider (residual-only, additive-steering-only).Fireable hooks (
tl_bridge):blocks.{i}.hook_in(resid_pre),ln2.hook_in(resid_mid),hook_out(resid_post),attn.hook_out,mlp.hook_out, plus head-splitattn.hook_q/k/v/attn.hook_z/attn.hook_patternwhere a per-model structural self-check finds them.embed,ln_final, andattn.hook_attn_scoresare always non-fireable — useboot_transformers()for those.For parity with
boot_transformers, the provider loads with the same dtype (fp32 by default) and eager attention.For capture during an eval, add the
capture_activations([...])solver fromsources/inspect/eval.pyto a Task’s solver chain: full activations go to per-sample.npzartifacts, and a compact summary lands in the sample store forsamples_dfanalysis.
Fundamental limits¶
Serving engines are not autograd engines. On the vLLM backend (and any future serving backend):
No gradients. PagedAttention and the compiled graph have no autograd surface; backward hooks, attribution patching, and anything gradient-based need
boot_transformers.No attention patterns or scores. The QKᵀ → softmax path is fused into the attention kernel (
attn.hook_pattern/attn.hook_attn_scoresare declared non-fireable, along with pre/post-RoPE Q/K).Interventions are declarative only. Arbitrary Python hook functions cannot run inside the compiled worker; the spec vocabulary (
suppress/scale/add/set, optionalpos) is the intervention surface.
The Inspect tl_bridge provider is HF-backed, so its captures are numerically faithful to boot_transformers — but the driver surface is capture/intervene over a wire format, not a local module: no gradients or weight mutation through the bridge.
Verifying parity¶
Two scripts diff each remote backend against boot_transformers on real models, comparing every hook the driver claims to serve:
uv run python scripts/vllm_parity_report.py— GPU-only; boots both backends in fp32 and diffs all fireable capture points (with a refold diagnostic that distinguishes anln_finalun-fold regression from a mapping error), plus an argmax agreement check.uv run python scripts/inspect_parity_report.py— validates the provider’s structural self-check: every boundary it offers must matchboot_transformers.
demos/vLLM_Bridge_Integration_Test.ipynb is the end-to-end GPU validation of the vLLM capture and mutation path (a manual Colab run, not CI).