Steering¶
Steering adds a direction to the residual stream while the model generates. This example builds the direction from a contrast between two sets of prompts and adds it at one layer.
Build a direction¶
Read the residual stream after layer 12 for each prompt, one invoke per prompt, and take the difference of the two groups' means. The first token is left out: its norm is far larger than the others'.
import torch
from nnsight.modeling.vllm import VLLM
model = VLLM("Qwen/Qwen3-8B", dispatch=True)
layer = model.model.layers[12]
HAPPY = ["I love this. It is wonderful and", "What a great, happy day. I feel"]
SAD = ["I hate this. It is terrible and", "What an awful, sad day. I feel"]
with model.trace(temperature=0.0, max_tokens=1) as tracer:
for prompt in HAPPY + SAD:
with tracer.invoke(prompt):
stream = sum(layer.output)[1:]
mean = stream.mean(0).save()
norm = stream.norm(dim=-1).median().save()
vector = torch.stack(mean[:2]).mean(0) - torch.stack(mean[2:]).mean(0)
vector = vector / vector.norm()
typical = torch.stack(norm).mean().item()
print(vector.shape, round(typical))
# torch.Size([4096]) 74
typical is the usual norm of a token's residual stream at this layer. Set the steering
strength as a multiple of it.
Add it while generating¶
Three invokes steer the same prompt against the direction, not at all, and along it. The edit
is under tracer.iter, so it applies on every step. An edit in the block body would apply to
the prompt only.
PROMPT = "I think that this movie is"
with model.trace(temperature=0.0, max_tokens=16) as tracer:
for strength in (-1.0, 0.0, 1.0):
with tracer.invoke(PROMPT):
for _ in tracer.iter[:16]:
layer.output[0][:] += strength * typical * vector
out = tracer.result.save()
for strength, o in zip((-1.0, 0.0, 1.0), out):
print(strength, repr(o.outputs[0].text))
# -1.0 ' not good. I think that this movie is not good. I think that this'
# 0.0 ' a very good example of how to make a movie that is not only entertaining,'
# 1.0 ' a great example of a family film. The story is very engaging and the characters'
layer.output is the pair (hidden, residual) and the next layer adds them, so adding to
either one adds to the residual stream.
Remove a direction¶
Any expression is allowed. This removes the direction from the residual stream at every step by projecting it out of both halves of the pair.
with model.trace(PROMPT, temperature=0.0, max_tokens=16) as tracer:
for _ in tracer.iter[:16]:
for half in layer.output:
half[:] -= (half @ vector).unsqueeze(-1) * vector
out = tracer.result.save()
print(repr(out.outputs[0].text))
# " a very good example of how to make a bad movie. It's not that"
Steer the sampler¶
model.logits and model.samples are writable too. This bans one token on every step.