Coming from RepE¶
This page is for people who read and steer a model with representation-engineering (RepE)
control vectors, in the repeng or the original RepE style, and want the same workflow in
TorchLens. TorchLens fits the direction itself and can hand it to repeng
(direction.to_repeng(), torchlens[repeng] extra, repeng~=0.4). repeng is not executed
here; the TorchLens side runs on a tiny model built in the fence. Checked 2026-11-03.
The same task in both¶
Fit a direction from positive and negative prompts at one layer, then steer with it.
In repeng¶
repeng trains a control vector from a dataset of contrastive pairs and applies it through a
ControlModel. You wrap the base model with ControlModel(base_model, layer_ids=[0]), train a
vector with ControlVector.train(model, tokenizer, dataset) from a list of DatasetEntry
pairs, and switch it on with model.set_control(vector, coeff=4.0). Training needs a tokenizer
and a real checkpoint, so this is described here rather than run; check the calls against your
repeng version.
In TorchLens¶
fit_direction reads the saved activations of the positive and negative traces at a site and
fits the direction ("mean_diff", "pca" or "logistic"); direction.steer(strength) is the
edit, placed with tl.when(tl.module(site), ...).
# migration-test: tool=torchlens requires=transformers expected=((32,), 'mean_diff', True)
import torch
import torchlens as tl
from torchlens.intervention import fit_direction
from transformers import LlamaConfig, LlamaForCausalLM
torch.manual_seed(0)
lm = LlamaForCausalLM(
LlamaConfig(
vocab_size=128,
hidden_size=32,
intermediate_size=64,
num_hidden_layers=2,
num_attention_heads=4,
num_key_value_heads=2,
)
).eval()
lm.config.use_cache = False # traces read plain forwards
site = "model.layers.0.mlp"
pos_ids = torch.randint(0, 128, (4, 6))
neg_ids = torch.randint(0, 128, (4, 6))
pos = tl.trace(lm, pos_ids, save="all")
neg = tl.trace(lm, neg_ids, save="all")
direction = fit_direction(pos, neg, site)
steer = tl.when(tl.module(site), direction.steer(4.0))
with torch.no_grad():
plain = lm(pos_ids).logits
steered = steer.bind(lm)(pos_ids).logits
RESULT = (tuple(direction.vector.shape), direction.method, not torch.equal(plain, steered))
Translation table¶
| RepE operation | TorchLens idiom | Parity |
|---|---|---|
| Apply a representation direction | tl.steer(direction, strength=...) |
Equivalent for visible activation sites. |
| Remove a direction | tl.project_out(direction) |
Equivalent helper. |
| Project onto a direction | tl.project_onto(direction) |
Equivalent helper. |
| Choose layer/token positions | Discover the activation site, then use tensor-shaped directions or custom hooks | Equivalent when shape is visible; no named token abstraction. |
| Run controlled generations | spec.bind(model).generate(ids, ...) steers every decoding step, KV cache included |
Equivalent for local generation; see docs/guides/fast_paths.md. |
| Compare control strengths | Fork logs and call a metric on a tl.Bundle of the steered variants |
Equivalent analysis pattern. |
| Save steering recipe | .tlspec/ portable save when direction tensors and built-ins are used |
Equivalent publication path. |
| Train/read representation directions | torchlens.intervention.fit_direction(log_pos, log_neg, "model.layers.<i>", method="pca") fits the direction from saved last-token activations ("mean_diff", "pca" or "logistic"), and direction.to_repeng() returns a real repeng.ControlVector (direction.to_steering_vectors() does the same for steering-vectors, and direction.to_dialz() for dialz) |
Changed: TorchLens fits the direction itself instead of replicating ControlVector.train, so the vector differs from repeng's pca_diff; ControlModel.set_control(vector, coeff=m) on the converted vector reproduces the logits of direction.steer(m) at that layer. Padded batches read the last real token when the trace saved its attention_mask input; without a mask, read_token_index=-1 reads the last position, a pad token on right-padded rows. |
| Batch evaluation | tl.when(site, action).bind(model) to steer many forwards, or trace.run_transaction(inputs=batch) per batch |
Equivalent; see docs/guides/fast_paths.md. |
| Intervene inside fused attention | Default fused SDPA load: by-head and by-pattern patching through torchlens.mechinterp.patch_head (no eager reload); eager-attention load: the softmax and score ops are ordinary intervention sites |
Head-level and pattern-level edits work on fused models; other edits inside the fused kernel need an eager recapture. |
What TorchLens adds¶
- Any executed op is a site, so the same direction can be read or applied at a module output,
a head, or a position (
tl.module,tl.in_module,tl.units). - Steering through the model's own
generate()keeps the edit on every decoding step, KV cache included:spec.bind(model).generate(ids, ...). - Provenance with the vector:
fit_directionrecords the method, the number of pairs and the site with the direction (direction.provenance), and ado()edit is stamped into the trace's audit record like any other. - Removing or isolating a direction with
tl.project_out(direction)andtl.project_onto(direction). - A tested route to repeng, steering-vectors and dialz objects from one fitted direction
(
direction.to_repeng(),direction.to_steering_vectors(),direction.to_dialz()).
What RepE does that TorchLens does not¶
- repeng's
ControlVector.trainandControlModelare a packaged training and application loop; TorchLens fits the direction itself with a different procedure, so the vectors differ. - The original RepE reading and control toolkits (pipelines for honesty and similar concepts) are not reproduced; TorchLens gives you the sites and edits to build them.
- Ember (Goodfire) is a hosted alternative: a hosted API that steers a model through its
learned features, with the model running on Goodfire's servers. Its Python SDK repository
(
goodfire-ai/goodfire-sdk) is archived. - EasySteer is the serving-speed path: steering inside vLLM. Build and check a steering vector in TorchLens (where every site is visible and the effect can be measured), then serve it in EasySteer when generation speed matters. See also where to go instead.
Using both¶
repeng feeds TorchLens¶
There is no bridge from a repeng ControlVector into a TorchLens edit; convert the
layer's direction to a tensor yourself and pass it to tl.steer(direction, strength=...),
which accepts a plain tensor direction.
TorchLens feeds repeng¶
direction.to_repeng() returns a real repeng.ControlVector;
direction.to_steering_vectors() does the same for steering-vectors and direction.to_dialz()
for dialz. ControlModel.set_control(vector, coeff=m) on the converted vector reproduces the
logits of direction.steer(m) at that layer (see the "Train/read representation directions"
row). For serving, build and check the vector in TorchLens, then serve it in EasySteer.
Semantic differences¶
- Direction differs. TorchLens fits the direction itself instead of replicating
ControlVector.train, so the vector differs from repeng'spca_diff. - Padded batches. The last real token is read when the trace saved its
attention_maskinput; without a mask,read_token_index=-1reads the last position, a pad token on right-padded rows. - Strength.
direction.steer(m)addsmtimes the vector on the trailing axis; repeng'scoeffis the same multiplier on its own vector. - Fused attention. Head-level and pattern-level edits work on fused models through
torchlens.mechinterp.patch_head; other edits inside the fused kernel need an eager recapture.