This document is relevant for: Trn2, Trn3
Tensor Capture Design#
Overview#
Tensor capture extracts intermediate tensor values from compiled models for accuracy debugging. It enables comparison to identify which module/op first diverges.
Architecture#
Hook-based capture using ModelCapture that works with torch.compile(fullgraph=True).
Note
torch.compile() is a JIT compiler — it returns a wrapper immediately
and defers tracing/compilation to the first forward call (warmup). Hooks
registered before warmup are traced into the compiled NEFF.
┌─────────────────────────────────────────────────────────────────┐
│ 1. torch.compile(model, backend="vllm_neuron", fullgraph=True) │
│ - Returns OptimizedModule wrapper (NO tracing/compilation) │
│ - Original model stored as wrapper._orig_mod │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. ModelCapture (after torch.compile(), before warmup) │
│ - Registers forward hooks on _orig_mod submodules │
│ - No compilation has happened yet — hooks are pre-trace │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Warmup (first forward call triggers actual compilation) │
│ - Dynamo traces _orig_mod forward, sees hooks │
│ - Hook clone()/detach() ops baked into FX graph │
│ - neuronx_cc compiles FX graph → single NEFF per bucket │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Runtime: setup_tensor_capture() / model(**kwargs) / save_tensor_captures()│
│ - setup_tensor_capture: activates this model's TensorRegistry as │
│ the global singleton so inline capture_tensor() calls │
│ route to the correct registry (needed for multi-model) │
│ - model(**kwargs): compiled NEFF executes, hooks fire, │
│ tensors are cloned into registry (inside the graph) │
│ - save_tensor_captures: reads tensors from registry and writes │
│ them to disk with metadata (outside the graph); restores │
│ previous global registry for multi-model isolation │
└─────────────────────────────────────────────────────────────────┘
ModelCapture is used uniformly for target, draft, and vision encoder models.
Each model gets its own instance with an isolated TensorRegistry.
API#
vLLM Configuration#
Configure tensor capture via neuron_config in vLLM’s additional_config:
from vllm import LLM
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
additional_config={
"neuron_config": {
"tensor_capture": {
"modules": ["model.layers.0-31", "lm_head"],
"capture_dir": "/tmp/captures"
}
}
}
)
Pattern Syntax#
Range:
model.layers.0-31expands to individual patterns before matchingRegex: All patterns use Python regex (e.g.,
model\.layers\.\d+,.*self_attn$)Vision prefix:
visual.blocks.0routes to vision encoder capture
Programmatic Usage#
from vllm_neuron.accuracy import ModelCapture
# After compile — no wrapping needed
compiled = torch.compile(model, backend="vllm_neuron", fullgraph=True)
raw_model = compiled._orig_mod
capture = ModelCapture(
model=raw_model,
modules=["model.layers.0-31"],
capture_dir="/tmp/captures",
)
capture.register_clear_hook(compiled)
capture.enable()
# Each forward pass:
capture.setup_tensor_capture()
output = compiled(**kwargs)
capture.save_tensor_captures(positions=positions, is_prefill=True, req_ids=[...])
Manual Capture#
For capturing tensors inside model code:
from vllm_neuron.accuracy import capture_tensor
class LlamaAttention(nn.Module):
def forward(self, hidden_states, ...):
attn_output = self._compute_attention(...)
capture_tensor("attn_output", attn_output) # Captured
return attn_output
Inline capture_tensor() calls require TensorRegistry._instance to be set
before warmup so dynamo traces them as active code.
Multi-Prompt Capture#
When running multiple prompts, the capture system automatically organizes captures by request ID and phase (prefill/decode).
Output Structure#
/tmp/captures/
├── dp0/ # Target model captures
│ ├── prefill_s128_0/ # First prefill, bucket size 128
│ │ ├── model.layers.0/
│ │ │ └── rank0.pt # TP-local rank
│ │ └── prefill_s128_0_meta.json # Request IDs, positions
│ └── decode_b1_0/ # First decode, batch size 1
│ ├── model.layers.0/
│ │ └── rank0.pt
│ └── decode_b1_0_meta.json
├── draft/
│ └── dp0/ # Draft model captures
│ └── decode_b4_0/
└── vision/
└── dp0/ # Vision encoder captures
└── prefill_s4096_0/
Core Classes#
ModelCapture#
Unified tensor capture for any compiled model (target, draft, or vision):
Registers forward hooks on
_orig_modsubmodules aftertorch.compile()but before warmup (i.e., before Dynamo traces the graph)Hooks are traced by Dynamo during the first forward (warmup) and captured tensors become part of the compiled NEFF — single compilation, no recompile
register_clear_hook(compiled_model)registers a pre-forward hook on the compiled wrapper to clear the registry before each forward passsetup_tensor_capture()activates this instance’s registry as the global singletonsave_tensor_captures()writes captures to disk and restores the previous registryenable()/disable()control whether disk writes actually happen (disabled during warmup to avoid capturing synthetic inputs)Each instance has its own
TensorRegistryfor multi-model isolation
TensorRegistry#
Stores captured tensors during forward pass. Two capture modes:
Hook-based: Hooks call
register_module_tensor()during forwardInline:
capture_tensor()callsregister_manual_tensor()via global singleton
Methods:
register_module_tensor(name, tensor)— Called by hooksregister_manual_tensor(name, tensor)— Called bycapture_tensor()get_all_tensors()— Returns tensors in registration orderclear()— Called at start of each forward passconfigure(enabled=True)— Enable/disable tensor registration
CaptureWriter#
Disk I/O component (internal to ModelCapture):
Saves tensors to disk when enabled via
write()Organizes captures by phase (prefill/decode) and bucket size
Supports per-DP-rank and per-TP-rank directories
Optional
capture_filterfor write-time filtering
Integration Points#
Model Runner Integration#
In neuron_model_runner.py:
# In init_tensor_capture() — called after load_model(), before warmup
def init_tensor_capture(self):
capture_config = self.neuron_config.tensor_capture
if not capture_config:
return
# Target model
self._target_tensor_capture = self._setup_capture(
self.model, text_modules,
capture_filter=..., # None means write all; set filters disk output
)
TensorRegistry._instance = self._target_tensor_capture._registry
# Vision encoder
if vision_modules:
self._vision_tensor_capture = self._setup_capture(
inner_model.visual, vision_modules, subdirectory="vision"
)
# Draft model
if self.drafter:
self._draft_tensor_capture = self._setup_capture(
self.drafter.model, draft_modules, subdirectory="draft"
)
In neuron_worker.py (after warmup):
self.model_runner.enable_capture()
self.model_runner._draft_tensor_capture.enable()
Forward Pass Integration#
Each model’s forward is bracketed with setup_tensor_capture() / save_tensor_captures():
setup_tensor_capture()— saves the current globalTensorRegistry._instanceand replaces it with this model’s registry. This ensures inlinecapture_tensor()calls route to the correct registry when multiple models (target, draft, vision) each have their own capture instance.save_tensor_captures()— writes captured tensors to disk with scheduler metadata (request IDs, positions, phase) and restores the previous global registry.
Example with speculative decoding (target + draft):
# Target forward — registry_target is active
self._target_tensor_capture.setup_tensor_capture() # _instance = registry_target
model_output = self.model(**kwargs) # hooks + inline captures → registry_target
self._target_tensor_capture.save_tensor_captures(...) # write to disk, _instance = previous
# Draft forward — registry_draft is active
self._draft_tensor_capture.setup_tensor_capture() # _instance = registry_draft
draft_output = self.drafter.propose(...) # hooks + inline captures → registry_draft
self._draft_tensor_capture.save_tensor_captures(...) # write to disk, _instance = previous
# Target model (in execute_model)
self._target_tensor_capture.setup_tensor_capture()
model_output = self.model(**model_kwargs)
self._target_tensor_capture.save_tensor_captures(positions, is_prefill, req_ids)
# Draft model (in propose)
self._draft_tensor_capture.setup_tensor_capture()
draft_output = self.drafter.propose(...)
self._draft_tensor_capture.save_tensor_captures(positions, is_prefill, req_ids)
# Vision encoder (in _execute_mm_encoder)
self._vision_tensor_capture.setup_tensor_capture()
encoder_outputs = self.model.embed_multimodal(...)
self._vision_tensor_capture.save_tensor_captures(positions, is_prefill, req_ids)
Multi-Model Support#
For speculative decoding and multimodal models, each sub-model has its own
ModelCapture instance with isolated registry and output directory.
Configuration:
"tensor_capture": {
"modules": ["model.layers.0", "lm_head"], # target model
"draft_modules": ["model.layers.0"], # draft model
"capture_dir": "/tmp/captures",
"capture_filter": ["model.layers.0"], # write-time filter
}
For vision models, use the visual. prefix:
"modules": [
"visual.blocks.0", # vision encoder
"language_model.layers.0", # text decoder
]
Limitations#
Sharding must match: CPU and Neuron runs must use identical sharding. If sharding differs, users must reconcile tensor shapes manually.
Multi-host captures: Each rank saves locally. For multi-host setups, users must aggregate files from each host for comparison.
Single tensor per module: Currently captures only the first tensor from module output. Complex output structures (nested tuples, dicts) are not fully supported.
This document is relevant for: Trn2, Trn3