Conversation
module.param("weight") returns the module's parameter or buffer by name.
On one device it is the parameter itself, the object module.weight holds.
Under transformers tensor parallelism it is the DTensor reassembled on
every rank. Under vLLM tensor parallelism it is this rank's slice
gathered across the group by the parameter's own sharding metadata: a
row-parallel layer along its input axis, other parallel layers along
their output axis, a merged projection regrouped per component with a
replicated k or v head kept once, and a vocab-parallel table reindexed
out of its padded shards into token order. The attribute module.weight
keeps its meaning on every backend.
Tests cover the single-device identity, a trace computing with the
parameter, the six vLLM layer kinds at tp=2 against a single-rank
engine, and three transformers-TP parameters against a single-GPU run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
A module that answers attribute lookups itself, such as a wrapper standing in for a module held elsewhere, answers param(name) the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
A module that defines _nnsight_parameter(name) serves the read, which lets a module standing in for one held elsewhere fetch the parameter on demand. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
The pieces a rank needs to read what another stage owns. `pp.py` maps module paths to owning stages from the load-time exchange of which modules are real on which rank, and stubs the modules an architecture builds only on some ranks so every path a request names resolves everywhere. `pp_listener.py` carries values between stages over a dedicated gloo group: a listener thread answers pull requests from a per-request buffer, parking a request until its value is published, while a requester's pull completes asynchronously so the transfer overlaps the forward. `lazy_remote_tensor.py` is the handle a remote-owned read returns, materialized on first use. `pp_tls_swap.py` isolates torch's thread-local dispatcher state per greenlet, so a worker parked inside a torch call leaves the forward's state intact. `collect.py` gains the per-stage save merge: each stage ships the slots it owns and a sentinel for the rest, lists and dicts union slot-wise, cache views union their recordings, and two real copies that differ raise PPRankDivergenceWarning. The engine's merge_collected applies it to a name several ranks report when either copy carries a sentinel or is a cache. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
… seams Three seams on the base interleaver, each a no-op there. `intercept` lets an interleaver see a worker's event before it parks: the PP interleaver answers a remote-owned read with a lazy handle, absorbs a remote-owned write, and serves an upstream-owned force in place when its round has run. `publish` receives the value as the served workers saw it, before any fragment re-split: the PP interleaver buffers it, per served request, under the occurrence those workers waited for, so a location enters the buffer only when a worker read it. The step gate paces an open-ended tracer.iter whose body never parks: the loop parks on STEP_GATE between steps, the base interleaver serves it as the root module's output handoff completes, and a driver that owns the step boundary (the vLLM runner) constructs the interleaver with step_gate_at_root=False and serves it at its own boundary. A gate park pins to the serves already seen, at least one past the loop's previous gate park; a worker left parked on the gate at the end is the loop's exit and unwinds without a message. Cross-stage reads tag their occurrence with the pinned iter step, kept for every read of the step's body, or with the request's completed-round count. The publish copies each value to host on the forward thread, ordered on the forward's stream at that point of the forward; the pull thread ships the buffered value as is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
A pipeline-parallel worker builds the full model on the meta device before its real distributed groups exist, from a copy of the engine's own config narrowed to one rank, and hands it to the runner in load_model. The runner stubs the rank-gated modules the meta tree names, exchanges each rank's real modules to derive ownership, opens one gloo pull group per TP column, and builds the envoy tree over a PPInterleaver; the meta tree's children are grafted under each PPMissingLayer envoy so sub-stub paths resolve. Serve points along the step: before the forward, pulls whose round has run complete (transfer only); after it, the step gate is served, the request's round count advances, and pulls that have already landed complete without waiting; the sampling path completes produced-round stragglers before the logits and samples offers. At collect, the finished requests' workers are named explicitly and served under the same produced-round gate, the drain barrier holds every rank until the peers' pulls are in, the finished requests' buffer entries are cleared, and each released worker's pull records go with it. Under PP every stage's TP rank 0 reports its slots, stripped of lazies, for the engine-side merge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
tests/vllm/pp runs as one command. `_support.py` holds what the tiers share: the two-rank harness (`Stage` opens one rank's listener and PPInterleaver over an ownership map; `run_two_ranks` spawns both), the subprocess engine runner the reference comparisons use, GPU selection, and the model and layer constants. `conftest.py` provides one session-scoped PP=2 engine and the `gpu` marker, so `-m "not gpu"` runs the CPU tiers alone. Unit tier (`test_unit_*`, one process): ownership and rank-gated stubbing, the save merge and the engine's per-rank merge, lazy tensors, occurrence tags and the round guard, the step gate. Harness tier (`test_harness_interleaver.py`, two gloo ranks, no engine): the wire protocol and its error replies, cross-stage reads, the fragments gather ahead of the publish, publish scoping, collect-time serving under the produced-round gate, and error delivery onto the worker greenlet. Engine tier (`test_engine_*`, real engines): the read, write, save, and cache paths on the shared engine, then each tracer.iter shape against the pull machinery with an elapsed-time bound where a stall is the failure mode: a save before an open loop, two cross-stage reads per step on a warmed engine, a bounded loop over an upstream layer, an open loop of per-step pulls, a bounded loop past generation end, per-step pulls in both directions, and a concurrent request finishing first; then every rank's request count back at zero. Parity and topology keep their per-configuration subprocess engines on the shared runner and gain the release check; a rank-gated architecture (granite) runs when its checkpoint is cached. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
…p per stage A rank opens a round at the start of each step, and an upstream stage has finished that round by then, while a downstream stage has finished only the rounds this rank completed. The PP interleaver keeps both counts: `opened`, set by the runner at step start for every scheduled request, and `rounds`, advanced after the forward. An upstream-owned pull of the opened round or earlier is producible (served in place when forced during the forward, completed at a serve point when parked); a downstream-owned pull is producible below `rounds`. A bounded loop that runs ahead of the model therefore completes its upstream pull for the next round at that round's step start, before this rank's own visit of the layer the peer is pulling. A registered block's harvested values ship as one stage's slots: lazies strip to sentinels the way a trace's saves do, and the engine merge unions registered names across stages like saves, on the flat and the per-sequence entries. Tests: the in-place guard against `opened` after a forward; registered values unioning at the merge; on the engine, a bounded loop forcing one value from each stage per step, and a registered block saving a stage-1 layer for every request (a proxy without the strip). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
… independent lifecycle check `free_gpus` returns physical indices from the process's own CUDA_VISIBLE_DEVICES when it is set, in its order, so the engines the suite boots stay inside the allocation it was given; an empty allocation selects nothing. `run_two_ranks` joins with a deadline and kills the ranks past it, so a harness deadlock fails the test instead of hanging the run. The release check generates its own request before reading every rank's count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
A cross-stage reply is a fixed header [status, meta_nbytes, data_nbytes]
followed by one byte message: the value pickled with every tensor
replaced by its dtype, shape and byte offset, then the tensors' raw
bytes at 64-byte-aligned offsets. The consumer sizes its recv from the
header and views each tensor over the received bytes in place, so a
one-element tuple, a namedtuple, a module's ((args), {kwargs}) inputs
and a mix of dtypes arrive as produced, with any number of tensors.
A leaf that cannot be pickled produces an error reply.
Tests: the wire-level structure test sends each of those shapes across
two gloo ranks; the engine test reads an upstream layer's .inputs on the
downstream rank and checks a write computed from them against the same
write computed from known values.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
…y before trimming serve_pulls throws a failed pull's error into the parked worker. Whatever exception then comes out of the worker, the thrown one or one the block raised in its place, is what is recorded on the mediator and what propagates when exceptions are not deferred. The error-path harness test covers both modes. Saved values from every PP rank merge in one pass: merge_saved_all folds the copies without trimming and drops the trailing no-real overshoot tail once from the result, so a rank's sentinel at a position a later rank owns still counts as a position it reached. merge_collected gathers each name's copies from all payloads and merges them together. The three-stage merge test passes without a divergence warning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
… debug switches PP_PULL_TIMEOUT_S is float() of the environment value, so a malformed override raises at import. The runner module imports without the SIGUSR1 stack dump and the garbage-collector switch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
A round this rank has completed for a request published every value it was going to. A pull that names an earlier occurrence with nothing buffered for it is answered with an error reply on arrival, and pulls already parked are answered the same way when their round closes; the runner runs that sweep as each round completes. The consumer's worker raises at the line that forced the value, saying the owner's forward ran past the location with no worker reading it there. A block that forces a downstream layer and then reads an upstream layer now fails on both ranks within the step: the upstream rank raises the out-of-order error for its local read, and the downstream rank's pull of the same layer is answered by the owner. The engine test bounds the elapsed time and checks that the next trace runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
…ner's failure in a refused pull A module the tree holds under several paths has one envoy and an alias at every other path, and a request may name it by any of them. The persistent-id table now carries every alias path, so a rank that holds the shared module under a different first path resolves the request. GPT-2 under PP=2 is the case: vLLM's activation registry hands every block one activation instance, the client names it by block 0, and stage 1 first wraps it under block 6. The architecture suite runs GPT-2 at PP=2 for it. A pull refused because the owning rank's side of the request failed (its block did not deserialize, or its worker raised) names that failure in its error reply; the runner records the cause per request as each round closes. A lazily pulled tuple answers .shape and .device with the same guidance as its methods: index the element first. The wheel ships pp_tls_state.cpp, which the PP worker compiles at runtime. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
…l or a parameter On a rank that does not hold a module, its path carries a RemoteShell: a PPMissingLayer whose forward raises naming the owning stage, and whose attribute lookup raises the same way for a parameter or buffer the real module has, read off the meta copy. Shells replace the placeholders before the envoy tree is built, and the children of a shell are grafted from the meta tree as shells too, so every path under a remote module resolves at request deserialization and answers alike. The behavior lives on the module because a request's envoys are rebuilt on the worker around the modules the worker holds. Reads and writes of .output, .input and .inputs cross stages as before, and a .skip() of a remote layer applies on its owner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
A shell answers param(name) by pulling the parameter from the owning stage: the request names "<module path>.param.<name>", and the owner's listener answers it from its own module through a resolver the runner installs, whatever the request's round, since a parameter is not produced by a forward. The pull completes in place and returns a real tensor on this rank; every call pulls afresh. The attribute form keeps raising and names the owner. Under tensor parallelism inside a stage the read raises for now. Tests: the wire-level serve across two ranks, the shell against a fake listener, both directions on the PP=2 engine against the single-GPU value, and on SmolLM2, whose head is its own module on the last stage, the head read on stage 0, a weight lens against the model's own logits, and one row per decode step. The vLLM parallelism doc gains the pipeline parallelism section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
…er's state A shell's forward pulls the owner's state dict through a "<module path>.state" request, which the owner's listener answers from its module, materializes the meta copy on this rank, loads the state, runs the call, and drops the copy back to the meta device. The block gets the value the owner computes, in place, with no worker on the owning stage involved. A buffer a module keeps out of its state dict is not carried over, so such a module is called on its owner. Under tensor parallelism inside a stage the call raises for now. Tests: the state serve across two ranks, the shell against a fake listener including the drop, the final norm and a late MLP applied to an early layer's output on the PP=2 engine against the single-GPU values, and on SmolLM2 the benchmark's lens, the norm called on a block's output then the head weight matmul, against the model's own logits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
…elism The meta tree is built after the real groups exist, at this stage's tensor-parallel size, with a single stage standing in for the pipeline group during the build so every layer, the embeddings, the norm and the head are constructed; it gets a forward context of its own, since the attention layers register by name in the current one. A shell's meta copy therefore has the shard shapes this rank's own layers have: a pulled parameter, this rank's column peer's shard, is gathered with the meta parameter's sharding stamps, and a call loads the pulled shards and runs the module's own collectives in this stage's group. hook=True on a module another stage owns runs the same local call; the meta copy's internals are not part of the tree, so the call's value is what is observable. Topology tests compare tp=2 pp=2 parameter reads on both sharding axes and remote calls against the tp=2 single-stage engine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds pipeline parallelism to the vLLM runtime: a trace written against the client's meta tree runs unchanged on an engine sharded across PP stages (including TP within each stage), with reads, writes, saves, and caches working across stage boundaries. Four commits on the current
0.8.How it works
Every rank runs every intervention block; each rank's forward only visits its own stage's modules. Three seams on the base interleaver close the gap, each a no-op on a single rank:
Mediator.event): the PP interleaver sees a worker's event before it parks. A read of a location another stage owns is answered with aLazyRemoteTensor; forcing it issues a cross-stage pull at the moment the worker parks, so the transfer overlaps the rest of the forward. A write to a remote-owned location is absorbed, since the owning rank runs the same line locally. A force of an upstream-owned value whose round this rank has opened is served in place inside the handoff; a force for a later round parks like a downstream pull.Interleaver.publish, called after the workers parked on a visit are served and before a fragment is re-split): the PP interleaver copies the value to host on the forward thread and buffers it, per served request, under the occurrence those workers waited for. A location enters the buffer only when a worker read it. Pull traffic uses dedicated per-TP-column gloo groups (pp_listener.py).tracer.iter[:]whose body never parks (a body of remote reads builds lazies and returns) parks on a gate the driver serves once per generation step; the vLLM runner serves it at its own step boundary.Serve points along a step: before the forward, pulls whose round has run complete; after it, the round count advances and landed pulls complete without waiting; the sampling path completes produced-round stragglers before the logits and samples offers. At collect, the finished requests' workers are served under the same produced-round gate, the drain barrier holds every rank until the peers' pulls are in, and the finished requests' buffer entries are cleared.
Ownership is derived from a load-time exchange of which modules are real on which rank (
pp.py); modules an architecture builds only on some ranks are stubbed from the full meta tree the worker builds before its groups exist, and the meta tree's children are grafted under eachPPMissingLayerenvoy so sub-stub paths resolve. Each stage's TP rank 0 ships the slots it owns; the engine's merge unions them slot-wise (collect.py), including per-stagetracer.cache()recordings.Also included: a C-level per-greenlet swap of torch's thread-local dispatcher state (
pp_tls_swap.py), so a worker parked inside a torch call leaves the forward's state intact.Tests
tests/vllm/ppruns as one command in three tiers: unit (one process), harness (two gloo ranks over the real interleaver and listener, no engine), and engine (a shared PP=2 engine for the behavior tests, per-configuration subprocess engines for PP=1 vs PP=2 parity, PP=3, and TP=2 x PP=2 topology).pytest tests/vllm/pp -m "not gpu"runs the CPU tiers in about 40 seconds (101 tests);-m gpuruns the engines (29 tests, about 18 minutes).Validation
tracer.itershape against the pull machinery with elapsed-time bounds, the request-count gauge at zero on every rank, and a rank-gated architecture (granite) under PP.origin/0.8checkout in this environment exactly.🤖 Generated with Claude Code
https://claude.ai/code/session_015fELrvmHWpBLcv7akbQ1oR