Applications normally call
easysteer.hidden_states.capture(),
which starts capture, generates requests, fetches labelled tensors, and stops the
stream. The fork exports the selection, serialization, and worker-session types
documented below for integrations.
Decoder-layer output; models returning a separate residual are combined for capture.
ATTENTION_HEADS
"attention_heads"
Concatenated query-head outputs before the attention output projection.
ROUTER_LOGITS
"router_logits"
Accessible MoE gate scores before routing.
SelectSpec
defines which prompt and generation rows to collect. It is the same class
exported from vllm.steer_vectors; its field reference is rendered once on the
specs page. See the capture guide for
model support and graph/cache behavior.
The decoded layer tensors and their CaptureMeta labels have matching row
counts. Request labels identify the owning request under continuous batching;
sequence positions order rows within a sample.
Tensors arrive as raw bytes of their stored dtype (bf16 rides as
int16 bytes and is reinterpreted here) — see StreamStore.serialize.
Returns (tensors, meta) where meta[layer] labels each row of
tensors[layer] with (request id, absolute position, token id).
Every layer must carry one label per row. Incomplete or older unlabeled
payloads raise instead of silently losing sample alignment.
Source code in vllm-steer/vllm/model_hooks/capture/serialization.py
defdeserialize_captured(serialized_data:dict[int,dict[str,Any]],)->tuple[dict[int,torch.Tensor],dict[int,CaptureMeta]]:"""Rebuild per-layer tensors AND their row labels. Tensors arrive as raw bytes of their stored dtype (bf16 rides as int16 bytes and is reinterpreted here) — see StreamStore.serialize. Returns ``(tensors, meta)`` where ``meta[layer]`` labels each row of ``tensors[layer]`` with (request id, absolute position, token id). Every layer must carry one label per row. Incomplete or older unlabeled payloads raise instead of silently losing sample alignment. """tensors:dict[int,torch.Tensor]={}meta:dict[int,CaptureMeta]={}forlayer_id,infoinserialized_data.items():encoding=info.get("encoding")ifencoding!="raw":raiseValueError(f"unknown capture wire encoding {encoding!r} for layer "f"{layer_id}; engine and client versions do not match")wire_dtype=info["dtype"]ifwire_dtypenotin_WIRE_DTYPES:raiseValueError(f"unknown capture wire dtype {wire_dtype!r} for layer {layer_id}")shape=tuple(info["shape"])array=np.frombuffer(info["data"],dtype=_RAW_DTYPES[_WIRE_DTYPES[wire_dtype]])tensor=torch.from_numpy(array.reshape(shape).copy())ifwire_dtype=="torch.bfloat16":tensor=tensor.view(torch.bfloat16)tensors[layer_id]=tensorm=info.get("meta")ifmisNone:raiseValueError(f"capture layer {layer_id} is missing mandatory row labels")req_idx=np.frombuffer(m["req_idx"],dtype=np.int32)table=m["req_table"]positions=np.frombuffer(m["positions"],dtype=np.int32)token_ids=np.frombuffer(m["token_ids"],dtype=np.int32)rows=tensor.shape[0]iflen(req_idx)!=rowsorlen(positions)!=rowsorlen(token_ids)!=rows:raiseValueError(f"capture layer {layer_id} has inconsistent row labels")if(req_idx<0).any()or(req_idx>=len(table)).any():raiseValueError(f"capture layer {layer_id} has invalid request indices")meta[layer_id]=CaptureMeta(req_ids=[table[i]foriinreq_idx],positions=torch.from_numpy(positions.copy()),token_ids=torch.from_numpy(token_ids.copy()),)returntensors,meta
serialized_data is the per-layer dictionary returned by a worker's
fetch_captured RPC. The function returns (tensors, meta), both keyed by true
layer ID. Optional attention layout metadata stays in the original wire
dictionary; the application helper attaches it to CaptureResult.layouts.
Whether a captured row's label belongs to a client request.
Rows are labelled with the engine-internal request id, which is the
client-visible id plus an 8-hex-char uniqueness suffix
({request_id}-{8 hex}, see InputProcessor) — or identical to it
when suffixing is disabled.
Source code in vllm-steer/vllm/model_hooks/capture/serialization.py
defmatch_capture_request_id(label_req_id:str,request_id:str)->bool:"""Whether a captured row's label belongs to a client request. Rows are labelled with the engine-internal request id, which is the client-visible id plus an 8-hex-char uniqueness suffix (``{request_id}-{8 hex}``, see InputProcessor) — or identical to it when suffixing is disabled. """iflabel_req_id==request_id:returnTruereturn(label_req_id.startswith(request_id+"-")andlen(label_req_id)==len(request_id)+9andall(cin"0123456789abcdef"forcinlabel_req_id[-8:]))
Use this helper to compare an engine row label with a client request ID. It
accepts an exact match or the engine's recognized uniqueness suffix and returns
a boolean; arbitrary prefix matching can mix different requests.
These classes are exported for engine integrations. Ordinary callers should
use capture() instead of creating or attaching a second CaptureSession.
StreamConfig is constructed when a session enables a stream:
Parameter
Meaning
layers
True layer IDs; None selects all available layers.
dtype
Optional storage dtype, independent of model compute dtype.
reduce
"all" retains rows; "last" or "mean" reduces within a request.
select
A SelectSpec.to_wire() dictionary; requires reduce="all".
budget_rows
Optional nonnegative row limit per layer.
The constructor validates the selection and resolves dtype/layer storage. These
low-level reduction and budget arguments are not arguments to the high-level
hs.capture() helper, which returns selected rows without a reduction.
Row selection uses the shared SelectSpec where-clause language
(select, wire form of vllm.model_hooks.selection.spec.SelectSpec) — the
same clause semantics steering resolves, so "which rows to capture"
and "which tokens to steer" mean the same thing. reduce applies a
within-sample reduction ("all" | "last" | "mean"); budget_rows
caps stored rows per layer.
Source code in vllm-steer/vllm/model_hooks/capture/store.py
def__init__(self,layers:list[int]|None=None,dtype:str|None=None,reduce:str="all",select:dict|None=None,budget_rows:int|None=None,):ifreducenotin("all","last","mean"):raiseValueError(f"reduce must be 'all', 'last' or 'mean', got {reduce!r}")ifreduce!="all"andselectisnotNone:raiseValueError("row selection cannot combine with the 'last'/'mean' ""reductions; use reduce='all' with a select clause")ifselectisnotNone:# Validate at enable time instead of failing mid-forward.select=SelectSpec.from_wire(select).to_wire()self.layers=set(layers)iflayersisnotNoneelseNoneself.dtype=resolve_storage_dtype(dtype)ifdtypeisnotNoneelseNoneself.reduce=reduceself.select=selectifbudget_rowsisnotNoneandbudget_rows<0:raiseValueError("budget_rows must be nonnegative")self.budget_rows=budget_rowsifreduce=="all"andselectisNoneandbudget_rowsisNone:logger.warning_once("Capture enabled with reduce='all', no select clause and ""no budget_rows: every position of every request ""accumulates in CPU memory. Prefer a select clause, a ""reduction, or an explicit budget for long corpora.")
The model runner owns a CaptureSession, attaches the discovered components at
model load, and calls its lifecycle methods for requests and batches. For stream
management, the relevant operations are:
Method
Effect / result
enable_stream(stream, **config_kwargs)
Validate a StreamConfig and start a fresh store; hooks must already be attached.
The supported RPCs (start_capture, stop_capture, fetch_captured,
clear_captured, capture_status) delegate to these operations in the worker.
The high-level helper also coordinates prefix-cache read policy before admission.
def__init__(self):self._streams:dict[str,StreamStore|None]=dict.fromkeys(COMPONENTS)# Engine-internal request id -> {stream: SelectSpec wire dict}.self._request_selects:dict[str,dict[str,dict]]={}# Retain cache-hit metadata even with no stream enabled. A stream# started later must check rows that were already skipped.self._cache_hits:dict[str,tuple[list[int],int]]={}self._unsupported_requests:set[str]=set()self._hook_handles:list=[]self._hooked_layers:dict[str,set[int]]={name:set()fornameinCOMPONENTS}self._layouts:dict[str,dict[int,dict[str,int]]]={}self._attached=Falseself.graph_state:CaptureGraphState|None=Noneself._graph_replays=0self._eager_capture_forwards=0
defattach(self,model:nn.Module,components:ModelComponents)->None:ifself._attached:returnforcomponentinCOMPONENTS.values():self._layouts[component.id]={target.layer_id:{key:valueforkeyin("width","num_heads","head_size")if(value:=getattr(target,key))isnotNone}fortargetincomponents[component.id]iftarget.widthisnotNone}self._hooked_layers[component.id]=self._attach_stream_hooks(components[component.id],component)defflush_hook(mod,args,output):ifself.graph_stateisnotNoneandself.graph_state.recording:returnifany(storeisnotNoneandstore._pendingforstoreinself._streams.values()):self._eager_capture_forwards+=1forstoreinself._streams.values():ifstoreisnotNone:store.flush()# One post-forward flush per step: per-layer hooks only stage# GPU rows; the D2H copies coalesce here (pinned, non-blocking,# single sync).self._hook_handles.append(model.register_forward_hook(flush_hook))self._attached=True
defenable_stream(self,stream:str,**config_kwargs)->None:ifstreamnotinself._streams:raiseValueError(f"Unknown capture stream: {stream}")ifnotself._attached:raiseRuntimeError("Capture hooks are not attached to this model; ""attachment happens once at model load.")config=StreamConfig(**config_kwargs)ifstream==ATTENTION_HEADS:available=self._hooked_layers[stream]ifnotavailable:raiseValueError("No supported decoder attention head outputs found")ifconfig.layersisnotNoneandconfig.layers-available:raiseValueError("Unsupported attention head capture layers: "f"{sorted(config.layers-available)}")ifnotself._hooked_layers[stream]:logger.warning("Enabling %s capture but no usable component hooks were found.",stream,)self._streams[stream]=StreamStore(config)
deffetch_stream(self,stream:str,clear:bool=True,layers:list[int]|None=None,req_ids:list[str]|None=None,)->dict[int,dict[str,Any]]:store=self._streams.get(stream)ifstoreisNone:return{}store.flush()# capture any rows staged since the last stepifreq_idsisnotNone:result=store.serialize(layers=layers,req_ids=req_ids,clear_selected=clear)else:result=store.serialize(layers=layers)ifclear:iflayersisNone:store.clear()else:store.drop_layers(list(result))forlayer,infoinresult.items():iflayout:=self._layouts.get(stream,{}).get(layer):info["layout"]=layout.copy()returnresult
StreamStore(config) holds one stream's captured rows and metadata. A session
owns it; integrations can inspect the storage/serialization contract here.
serialize() returns the per-layer wire dictionaries consumed by
deserialize_captured(). Request and layer filters operate on retained data.
Bounded, chunk-appending CPU store for one capture stream.
Every stored row carries labels — (req_idx, position, token_id)
int32 columns, where req_idx indexes req_table (request id
strings) — so clients never re-derive sample alignment. Missing or
malformed labels are rejected before any rows enter the store.
Source code in vllm-steer/vllm/model_hooks/capture/store.py
def__init__(self,config:StreamConfig):self.config=configself.chunks:dict[int,dict[int,_StoredChunk]]={}self._next_chunk=0self._request_chunks:dict[int,dict[int,set[int]]]={}self.layer_names:dict[int,str]={}self.req_table:dict[int,str]={}self._req_index:dict[str,int]={}self._request_aliases:dict[str,set[int]]={}self._free_request_indices:list[int]=[]self._next_request_index=0self._unused_request_indices:set[int]=set()# Active requests retain capture history across draining and preemption.self._captured_requests:set[str]=set()# Cache hits that omitted selected prompt rows, including requests# already running when this stream started (fetch raises).self.elided_reqs:set[str]=set()self._failed_requests:dict[str,str]={}# Rows stored per layer; the budget caps each layer at# budget_rows rows (i.e. sequence tokens, per layer).self._layer_rows:dict[int,int]={}self.tokens_dropped=0self._warned_budget=Falseself._pending:list[tuple[int,torch.Tensor,torch.Tensor,str]]=[]# Geometry and request selections are fixed during one forward pass.# Layers share its row plans; a fresh geometry invalidates every plan.self._row_plan_geometry:BatchGeometry|None=Noneself._row_plan_request_selects:dict[str,dict[str,dict]]|None=Noneself._row_plans:dict={}self.lock=threading.Lock()
Stage one layer's selected rows for this step (GPU side).
meta is an int32 [rows, 3] tensor of
(req_idx, position, token_id) labels. The device-to-host copy happens once
per step in
flush(): per-layer synchronous .cpu() calls stall the
GPU stream once per hooked layer, which serializes
capture-heavy runs.
Source code in vllm-steer/vllm/model_hooks/capture/store.py
defappend(self,layer_id:int,tensor:torch.Tensor,meta:torch.Tensor,layer_name:str,):"""Stage one layer's selected rows for this step (GPU side). ``meta`` is an int32 ``[rows, 3]`` tensor of (req_idx, position, token_id) labels. The device-to-host copy happens once per step in ``flush()``: per-layer synchronous ``.cpu()`` calls stall the GPU stream once per hooked layer, which serializes capture-heavy runs. """withself.lock:if(notisinstance(meta,torch.Tensor)ormeta.ndim!=2ormeta.shape[1]!=3ormeta.dtype!=torch.int32):raiseRuntimeError(f"capture layer {layer_id} requires int32 [rows, 3] row labels")ifmeta.shape[0]!=tensor.shape[0]:raiseRuntimeError(f"capture meta rows ({meta.shape[0]}) != data rows "f"({tensor.shape[0]}) for layer {layer_id}")keep=self._limit_rows(layer_id,tensor.shape[0])ifkeep==0:returntensor=tensor[:keep]meta=meta[:keep]ifself.config.dtypeisnotNone:tensor=tensor.to(self.config.dtype)# `tensor` is owned by the hook (freshly materialized), so an# async copy in flush() cannot race buffer reuse.self._pending.append((layer_id,tensor.detach(),meta.detach(),layer_name,))
Move this step's staged rows to CPU in one coalesced pass.
Copies go through pinned staging buffers with non_blocking=True
(one stream sync at the end), amortizing D2H latency across all
hooked layers instead of paying it per layer.
Source code in vllm-steer/vllm/model_hooks/capture/store.py
defflush(self):"""Move this step's staged rows to CPU in one coalesced pass. Copies go through pinned staging buffers with non_blocking=True (one stream sync at the end), amortizing D2H latency across all hooked layers instead of paying it per layer. """withself.lock:ifnotself._pending:returnpinned=[]any_cuda=Falseforlayer_id,tensor,meta,layer_nameinself._pending:host_pair=[]fortin(tensor,meta):ift.is_cuda:host=torch.empty_like(t,device="cpu",pin_memory=True)host.copy_(t,non_blocking=True)any_cuda=Trueelse:host=thost_pair.append(host)pinned.append((layer_id,host_pair[0],host_pair[1],layer_name))ifany_cuda:torch.cuda.current_stream().synchronize()self._pending.clear()forlayer_id,host,meta_host,layer_nameinpinned:requests,counts=torch.unique_consecutive(meta_host[:,0],return_counts=True)start=0forrequest_index,countinzip(requests.tolist(),counts.tolist()):tensor=host[start:start+count]meta=meta_host[start:start+count]# Each request owns its storage. Draining one must release# its values without copying or pinning another request's rows.iflen(counts)>1:tensor,meta=tensor.clone(),meta.clone()chunk_id=self._next_chunkself._next_chunk+=1self.chunks.setdefault(layer_id,{})[chunk_id]=_StoredChunk(tensor,meta,request_index)self._request_chunks.setdefault(request_index,{}).setdefault(layer_id,set()).add(chunk_id)self._unused_request_indices.discard(request_index)start+=countself.layer_names[layer_id]=layer_nameself._layer_rows[layer_id]=(self._layer_rows.get(layer_id,0)+host.shape[0])self._reclaim_requests()
Concatenate selected chunks and pack for RPC transmission.
Tensors ship as raw bytes of their stored dtype (bf16 rides as
int16 bytes and is reinterpreted client-side) — no float32
upcast, so the wire volume equals the stored volume. Passing
layers serializes a subset, letting clients fetch layer by
layer instead of one monolithic message.
Each layer entry carries a meta sub-dict labelling its rows
(req_table request-id strings + int32 req_idx /
positions / token_ids columns).
req_ids (client-visible ids) restricts the payload to rows
of those requests; with clear_selected the emitted rows are
also removed from the store, so clients can drain request by
request with bounded peak message size.
Source code in vllm-steer/vllm/model_hooks/capture/store.py
defserialize(self,layers:list[int]|None=None,req_ids:list[str]|None=None,clear_selected:bool=False,)->dict[int,dict[str,Any]]:"""Concatenate selected chunks and pack for RPC transmission. Tensors ship as raw bytes of their stored dtype (bf16 rides as int16 bytes and is reinterpreted client-side) — no float32 upcast, so the wire volume equals the stored volume. Passing ``layers`` serializes a subset, letting clients fetch layer by layer instead of one monolithic message. Each layer entry carries a ``meta`` sub-dict labelling its rows (``req_table`` request-id strings + int32 ``req_idx`` / ``positions`` / ``token_ids`` columns). ``req_ids`` (client-visible ids) restricts the payload to rows of those requests; with ``clear_selected`` the emitted rows are also removed from the store, so clients can drain request by request with bounded peak message size. """withself.lock:failed={rid:reasonforrid,reasoninself._failed_requests.items()ifreq_idsisNoneorany(match_capture_request_id(rid,ext)forextinreq_ids)}iffailed:raiseRuntimeError(f"capture contains failed request(s) {list(failed.items())[:5]}; ""their rows do not represent valid capture. Fetch with ""req_ids excluding them, or clear the stream.")elided=self.elided_reqsifreq_idsisnotNoneandelided:elided={ridforridinelidedifany(match_capture_request_id(rid,ext)forextinreq_ids)}ifelided:raiseRuntimeError("prefix-cache hits skipped recomputation of the prompt "f"head for request(s) {sorted(elided)[:5]}; their early ""rows were never computed, so this capture is ""incomplete. Submit capture requests with ""SamplingParams(skip_reading_prefix_cache=True), ""fetch with req_ids excluding them, or ""clear the stream.")table_match:set[int]|None=Noneifreq_idsisnotNone:table_match={iforreq_idinreq_idsforiinself._request_aliases.get(req_id,())}result:dict[int,dict[str,Any]]={}wanted=(sorted(self.chunks)iflayersisNoneelse[lidforlidinlayersiflidinself.chunks])forlayer_idinwanted:layer_name=self.layer_names[layer_id]chunks=self.chunks[layer_id]iftable_matchisnotNone:chunk_ids=sorted(chunkforindexintable_matchforchunkinself._request_chunks.get(index,{}).get(layer_id,()))else:chunk_ids=list(chunks)ifnotchunk_ids:continueselected=[chunks[index]forindexinchunk_ids]tensor=torch.cat([chunk.tensorforchunkinselected],dim=0)meta=torch.cat([chunk.metaforchunkinselected],dim=0)result[layer_id]=serialize_capture_layer(tensor,meta,self.req_table,layer_name)ifclear_selected:forindexinchunk_ids:self._remove_chunk(layer_id,index)ifclear_selected:self._reclaim_requests()returnresult
defclear(self)->None:"""Reset captured rows and counters, retaining active request history."""withself.lock:self.chunks.clear()self._request_chunks.clear()self._next_chunk=0self.layer_names.clear()self.req_table.clear()self._req_index.clear()self._request_aliases.clear()self._free_request_indices.clear()self._next_request_index=0self._unused_request_indices.clear()self.elided_reqs.clear()self._failed_requests.clear()self._layer_rows.clear()self.tokens_dropped=0self._warned_budget=Falseself._pending.clear()self._row_plans.clear()self._row_plan_geometry=Noneself._row_plan_request_selects=None