Skip to content

Hidden-state capture

easysteer.hidden_states extracts intermediate activations from a running vLLM engine — the raw material for steering vectors.

Engine requirements

from vllm import LLM
import easysteer.hidden_states as hs

llm = LLM(
    model="Qwen/Qwen2.5-1.5B-Instruct",
    enforce_eager=True,           # capture requires eager execution
    enable_prefix_caching=False,  # cache-hit tokens are never recomputed,
                                  # so they cannot be captured
)

hs.capture()

One call captures a batch of prompts and returns a labelled CaptureResult:

result = hs.capture(
    llm,
    prompts=["What is steering?", "Explain PCA."],
    max_tokens=1,          # 1 = prompt-only forward pass
    layers=[10, 11, 12],   # true layer ids; None = all hooked layers
    dtype="float16",       # engine-side storage dtype (optional)
)

Key arguments (full signature in the API reference):

Argument Meaning
max_tokens Tokens to generate; 1 captures only the prompt forward pass.
layers Layer-id subset (None = all). Layers are keyed by true layer id everywhere, never positional index.
dtype Engine-side storage dtype, e.g. "float16".
select Global SelectSpec (or wire dict) choosing which rows to keep.
per_prompt_selects One SelectSpec per prompt, overriding the global selection (None entries keep the global one).
stream "hidden_states" (default) or "router_logits" (MoE).
**generate_kwargs Forwarded into SamplingParams (e.g. temperature).

Select clauses

Row selection reuses the same SelectSpec language as steering's ApplySpec — phases, token/position filters, exclusions, generation window — resolved identically by the engine, so a clause means the same thing in both systems:

from vllm.steer_vectors import SelectSpec

# Keep only the last prompt token of each sample
result = hs.capture(llm, prompts,
                    select=SelectSpec(phases=["prompt"], positions=[-1]))

per_prompt_selects requires positions='all' semantics (no reductions).

Working with CaptureResult

Rows are grouped by their owning request via engine labels and ordered by sequence position — the only correct grouping under continuous batching.

result.layer_ids          # sorted true layer ids
result.rows(12)           # Tensor(total_rows, dim) for layer 12, all samples
result.sample(0)          # {layer_id: Tensor(rows, dim)} for sample 0
result.sample_positions(0)  # absolute sequence positions of sample 0's rows
result.sample_token_ids(0)  # input token ids of sample 0's rows
result.outputs            # the vLLM RequestOutput list, prompt order
result.to_nested()        # legacy shape: [sample][layer_pos] tensors

result.meta(layer) exposes the raw row labels (req_ids / positions / token_ids); result.labelled tells you whether per-sample views are available.

MoE router logits

Pass stream="router_logits" to hs.capture() on an MoE model to capture per-token router logits instead of hidden states (used e.g. by the SteerMoE replication).

Compatibility wrappers

get_all_hidden_states_generate and get_moe_router_logits_generate predate capture() and keep their original nested-list signatures, but are now thin wrappers over it — splitting is always exact and label-driven. Prefer capture() for new code; it additionally exposes select clauses, layer subsets and per-sample metadata. The embed-task variants (get_all_hidden_states, get_moe_router_logits) and the vllm.hidden_states alias package were removed.