Skip to content
ACS Infra

Steer with a contrast vector

Build a Steering vector from two sets of prompts with the same meanings, one in a pirate style and one plain. The difference of their mean Activations at one Layer points from plain text toward pirate text. Add that difference to the residual stream at a few scales, and compare the texts with the scale-0 baseline.

You need ACS_API_BASE and an API key with tokens in ACS_API_KEY, as Set up in the Quickstart shows. You also need Python 3.14 (for compression.zstd), NumPy, and the openai SDK.

Build the contrast vector and steer with it

The program below does four steps:

  1. Read Layer 15 of each prompt with max_tokens: 1. Layer 15 is near the middle of the model's 32 Layers.
  2. Take the mean over the tokens of each prompt, without the BOS token.
  3. Subtract the mean of the plain set from the mean of the pirate set. The difference is the contrast vector.
  4. Add the contrast at Layer 15 to one prompt at the scales 0, 4, and 8, at temperature: 0. Scale 0 adds nothing, so it is the baseline (Why compare with scale 0).

Download contrast.py, or save the program below as contrast.py. Run it with python contrast.py:

import base64
import os
from compression import zstd

import numpy as np
from openai import OpenAI

LAYER = 15


def decode(tensor):
    raw = zstd.decompress(base64.b64decode(tensor["data"]))
    array = np.frombuffer(raw, dtype=tensor["dtype"]).reshape(tensor["shape"])
    if tensor["original_dtype"] == "torch.bfloat16":
        array = (array.view(np.uint16).astype(np.uint32) << 16).view(np.float32)
    return array


def mean_over_tokens(prompt):
    completion = client.completions.create(
        model="llama-8b",
        prompt=prompt,
        max_tokens=1,
        extra_body={"output_residual_stream": [LAYER]},
    )
    residual_stream = decode(completion.activations["residual_stream"])
    return residual_stream[0, 1:].mean(axis=0)


client = OpenAI(base_url=os.environ["ACS_API_BASE"], api_key=os.environ["ACS_API_KEY"])
pirate = [
    "Arr, the wind be fair and the sea be calm.",
    "Ahoy, me hearties, hoist the sails!",
    "Shiver me timbers, that be a fine ship.",
    "Avast, ye scallywags, the treasure be ours!",
]
plain = [
    "The wind is fair and the sea is calm.",
    "Hello, friends, raise the sails!",
    "Goodness, that is a fine ship.",
    "Stop, you rascals, the treasure is ours!",
]
contrast = np.mean([mean_over_tokens(p) for p in pirate], axis=0) - np.mean(
    [mean_over_tokens(p) for p in plain], axis=0
)
print(contrast.shape, round(float(np.linalg.norm(contrast)), 2))

for scale in [0, 4, 8]:
    completion = client.completions.create(
        model="llama-8b",
        prompt="My advice for your first day at a new job is",
        max_tokens=16,
        temperature=0,
        extra_body={
            "apply_steering_vectors": [{"activations": [contrast.tolist()], "layer_indices": [LAYER], "scale": scale}]
        },
    )
    print(scale, repr(completion.choices[0].text))

The program prints the shape and the norm of the contrast, and then the scale and the text of each Completion:

Output
(4096,) 3.59
0 ' to be yourself. Don’t try to be someone you’re not. You’ll'
4 ' to swag yer’ scollies ye’ ye’ me’ ye’'
8 ' ye Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers Buccaneers'
  • Each capture asks for max_tokens: 1, so its Activations have one row for each prompt token. Read Activations for text you already have explains why.
  • mean_over_tokens skips row 0, the BOS token. BOS is the same at the start of every prompt, and its Activations are much larger than those of the other tokens.
  • mean_over_tokens takes the mean of the other rows, so a long prompt and a short prompt count the same.
  • The contrast has one number for each dimension of the model's hidden size, 4096.
  • decode is the helper from Decode the tensor.

Read the result

Compare each text with the scale-0 baseline. Here, the contrast has a norm of 3.59. At scale 4, the text moves toward the pirate style. At scale 8, the model repeats one word, so the scale is too large. Lower the scale when the text starts to repeat phrases or break into fragments.

More prompts in each set give a contrast that depends less on any one prompt. Choose a scale helps you pick the next scale to try. To set a scale relative to the residual stream at each token, use norm_match, as Match the norm explains.

On this page