Capabilities and limits¶
There is no capability table to consult: any module on the tree is a location, and most of what the engine cannot do it refuses with an error that names the location and the fix. What is worth knowing is which questions to ask up front, which errors mean what — and the two constraints nothing enforces for you.
Ask the model¶
from nnsight.modeling.vllm import VLLM
model = VLLM("Qwen/Qwen3-8B", taps=["model.layers.*.output"], dispatch=True)
print(model.dispatched) # is the engine built?
# True
print(model.taps[:3], len(model.taps)) # under graphs: the only module locations served
# ('model.model.layers.0.output', 'model.model.layers.1.output', 'model.model.layers.2.output') 36
print(type(model.vllm_entrypoint).__name__) # LLM or AsyncLLM
# LLM
print(model.model.layers[10]) # what is there to read
# Qwen3DecoderLayer(
# (self_attn): Qwen3Attention(
# (qkv_proj): QKVParallelLinear(in_features=4096, output_features=6144, ...)
# ...
model.taps is empty on an eager engine, where every location is served.
Refusals¶
What the engine cannot do it refuses loudly. Each row is a real error, reproduced on this
model. Two cases are not refusals and are covered below: a write that changes the request's
row count reaches the model unchecked, and a tracer.iter loop that outruns the request is
cut short with a warning.
| You did | You get |
|---|---|
| read a location the model already ran past (layer 20, then layer 10) | OutOfOrderError: '...layers.10.output.i0' was requested but the model already ran past it |
read a non-tap location on a taps= engine |
OutOfOrderError: '...' is not a tap on this engine, so a replayed CUDA graph never reaches it. Declare it at construction ... |
taps=["model.nope.output"] |
ValueError at construction: names no module |
VLLM(..., taps=[...], enforce_eager=True) |
ValueError: enforce_eager contradicts taps |
tracer.invoke(["a", "b"]) |
error: one prompt per invoke |
tracer.barrier(n) |
RuntimeError: NotImplementedError: tracer.barrier(2) cannot work on vLLM ... — each invoke is its own request; the blocks never share a forward |
trace(prompt, temperatur=0.0) |
TypeError from SamplingParams — not silently ignored |
an empty tracer.invoke() that reads or writes something |
error: its work would vanish (a do-nothing empty invoke is a no-op) |
enable_chunked_prefill=True and a prompt that got chunked |
the request's error: prompt split across steps, so no block could see it whole |
.backward(), .grad |
NotImplementedError: the forward runs under torch.inference_mode, so there is no autograd graph |
.scan() |
NotImplementedError: scan is unavailable on vLLM: it runs the model's forward under a fake-tensor mode ... Trace a prompt and read the shapes off the activations it serves. |
an exception in your block (1/0) |
re-raised in your process with the block's own traceback; the engine keeps serving |
tracer.iter[:20] on a request that made 4 steps |
not a refusal: the loop is cut short at step 4 with UserWarning: 'model.samples.i4' was never reached ... — emitted in the EngineCore subprocess, not yours. The 4 reached steps come home; the statements after the loop never run. Hold the run to the count with ignore_eos=True or min_tokens=N, and check the len() of what you collected. |
from nnsight.modeling.vllm import VLLM
model = VLLM("Qwen/Qwen3-8B", dispatch=True)
try:
with model.trace("The capital of France is", temperature=0.0):
late = model.model.layers[20].output[0].clone().save()
early = model.model.layers[10].output[0].clone().save()
except Exception as e:
print(type(e).__name__, str(e).splitlines()[0][:90])
# RuntimeError OutOfOrderError: 'model.model.layers.10.output.i0' was requested but the model already ran
with model.trace("The capital of France is", temperature=0.0): # the engine is fine
ok = model.logits.argmax(-1).save()
print(model.tokenizer.decode(ok))
# Paris
An ordinary Python error in a block — an exception the block itself raises — errors its request. Other clients' requests in the same batch, and every request after, are unaffected — this is what makes engine-wide edits and shared servers safe to run. The invokes of one trace are not separate that way: they are one block, and it raises as a whole, so a sweep loses the invokes that were fine along with the one that was not.
That isolation covers what happens in the block. What the block hands the model is another
matter: a replacement (layer.output = t) is spliced back into the rows the block owns, and it
must keep the request's row count — nothing checks it for you. A wrong-height replacement
reaches the next kernels as given, and a shape mismatch there can land as a device-side assert,
which poisons the CUDA context and takes the engine down with every request in it. A patching
sweep is where you meet this: a donor activation captured at a different prompt length is the
usual source of a short one. Slice the donor to the rows you are writing
(served[POS] = donor[POS]) rather than assigning a shorter tensor.
Where the message is¶
An error inside a block comes home: it is re-raised in your process as a RuntimeError carrying
the original type, its message and an "Intervention traceback" pointing at your line. Because the
type rides the message rather than the class, catch RuntimeError and match on the text.
Two things do not come home, because they happen in vLLM's EngineCore subprocess:
- Warnings. A
warnings.catch_warnings()around a trace records nothing; the text is in the engine's own output, prefixed(EngineCore pid=...). This is the one place the vLLM path is not the local one — the same block warns catchably against a HuggingFace model. What you will meet here is atracer.iterloop — bounded or open — that outruns the request: the warning that it was cut short stays on the engine, what the loop saved comes home, and the statements after the loop never run. Your process sees a result that looks complete, so check thelen()of what you collected. - Anything that fails while the engine builds, including a bad
taps=entry. The caller seesRuntimeError: Engine core initialization failed. See root cause above.; the message that names the ops a forward actually has is in the(EngineCore pid=...)lines above it. The same shape coversAssertionError: Error in memory profiling ..., which is a shared GPU whose free memory moved while vLLM profiled — build again.
Chunked prefill is enabled with max_num_batched_tokens=..., printed on every construction, is
not the engine you get: it comes from the meta tree built first. The engine's real arguments are
logged a few lines later as non-default args: {... 'enable_chunked_prefill': False ...}.
Two behaviours that differ from a HuggingFace trace¶
- Tensors alias engine memory. Clone what you keep (Locations).
- One prompt per invoke, no barrier. Prompts become separate requests the scheduler batches; cross-prompt patching is a saved value from one trace used in the next, not a barrier.
- Numbers depend on the batch a request was scheduled with. Reduction order inside a fused
kernel follows the batch, and in bf16 that moves the last digit, so a sweep on this page
reproduces to about a part in a hundred rather than exactly, and a nearly-tied greedy argmax
can land differently between runs.
temperature=0.0pins the sampler, not the arithmetic. Read paired differences taken inside one trace rather than differencing two runs.
Not on vLLM¶
| Where instead | |
|---|---|
| Gradients and backward | TransformersModel — same block |
.scan() |
TransformersModel; on vLLM, trace a prompt and read the shapes off what it serves |
| The attention pattern as a location | rebuilt from q/k, Attention |
| Individual MoE experts as modules | mask the router logit, Steering |
| Multimodal vLLM models | text-only today |
Pipeline parallelism (pipeline_parallel_size > 1) |
tensor parallelism, Tensor parallelism |
| Speculative decoding | off |
| vLLM's Ray v1 executor | the default (v2) Ray executor works, Tensor parallelism |
.source ops inside a fused CUDA kernel |
the kernel's inputs and outputs are locations; its interior is not Python |
Versions¶
nnsight targets vLLM's V1 engine and imports its internals directly, so the release matters. On
0.27 it selects the V1 GPUModelRunner (VLLM_USE_V2_MODEL_RUNNER=0) and refuses to come up on
any other. Graph taps need a vLLM with breakable CUDA graphs
(vllm.compilation.breakable_cudagraph); without it, taps= is refused at construction with that
message. The vllm extra carries no upper bound, so pip install "nnsight[vllm]" takes the
current release, and one that moves an imported name fails at import nnsight.modeling.vllm.
Everything in this section was run on 0.27.1.
How this is known¶
tests/vllm/ in the nnsight repository: ~195 tests on two GPUs covering tracing, request
accounting, async, edits, tensor parallelism, taps, preemption, chunked prefill, serve, Ray, LoRA
and mixture-of-experts batching, on gpt2, Qwen2.5-0.5B, Qwen1.5-MoE, Llama-3.x and
DeepSeek-V2-Lite. Every value read in this section was checked against a HuggingFace forward of
the same checkpoint.