Steering + capture on the v2 model runner - #185
Conversation
…to the v2 model runner
… fallback guard); fix global-steering row for untracked requests
|
Also validated cross-node on 2×RTX 3090 (Ray, NCCL over bond0): steering passes under both TP=2 and PP=2 — global |
…le-node, TP/PP, preemption, streaming)
Full validation matrix (GPU, 2×RTX 3090)Extended the validation well beyond the initial pass. All on the v2 runner ( Steering
Capture
Distributed (Ray, NCCL/bond0)
Hard edges
Coverage: Qwen3-0.6B and gemma-3-4b-it (v2 supports gemma-3; the port is not Qwen3-specific). Plus CPU glue tests for the v2-specific projection logic. Out of scope / not exercised: spec-decode, DP, async-dispatch overload policies, and the store serve path (dormant by design — |
…preemption resume
|
Follow-up fix: capture registration on re-add paths Discovered while auditing whether the "untested" items actually touch the port's diff. Two paths re-enter
The runtime change threads GPU-validated on Qwen3-0.6B (RTX 3090, v2 runner), clean before/after:
CPU glue tests added for the three branches (fresh / streaming re-add / preemption resume) + non-capturer rank; full v2 glue suite 19 passed. Also confirmed the async-dispatch overload policies ( Commit: 40133ec |
|
Follow-up tracked in #188: the |
What
Ports the activation steering and capture control planes to the experimental v2 GPU model runner (
vllm/v1/worker/gpu/model_runner.py), so they work there the same way they do on the v1 runner.Why
The steering/capture data plane (the
apply_steering/capture_residualcustom ops, per-layer buffers, kernels,SteeringManager/CaptureManager/CaptureStepGate/ActivationStore) already lives inmodel_executor/+v1/capture/and is shared by both runners — both load the same model, so the in-forward hooks already fire on v2 and safely no-op when nothing drives them. Only the runner-side control plane was v1-only. Without it, turning on the v2 runner with steering/capture configured silently did nothing.How
Two v2-native modules wire the existing subsystems into v2's lifecycle (
add_requests/finish_requests/execute_model/sample_tokens); the v1 runner is untouched.gpu/capture_runner_mixin.py(CaptureRunnerMixin): manager/gate/store init, request register/finalize, the per-step force-eager decision (client-spec captures only — global specs ride the cudagraph-safe persistent-buffer path), gather-plan build, and draining results ontoModelRunnerOutput.capture_results.gpu/steering_runner_mixin.py(SteeringRunnerMixin): subclasses the v1SteeringModelRunnerMixin, reusing init / layer discovery / validation / the public RPC API /_resolve_request_steeringunchanged, and overrides only the three methods that touched v1-runner state. It keeps its own per-request state (v2 retains noCachedRequestState), drives prefill→decode transitions and the per-token steering index, and needs no force-eager (persistent buffers are cudagraph-safe).gpu_worker.pyalready forwards the steering RPCs.Two v2-specific correctness fixes are included:
get_row_for_config(0, …)so any global steering vectors still apply (instead of the row-0 no-steer sentinel)._capture_add_requestnow takeswas_present):add_requestscalls_remove_requestfirst, which does not touch capture state, so a re-admitted request would re-register an already-registered id (the manager raisesalready registered). A streaming re-add (still-live request, grown prompt) discards the stale registration and re-registers; a preemption resume (v2 foldsscheduled_resumed_reqsintoscheduled_new_reqs) keeps the surviving registration. This mirrors the symmetric handling steering already had.Design notes:
docs/design/v2_runner_steering_capture.md.Validation
CPU glue tests —
tests/v1/worker/test_gpu_v2_{capture,steering}_glue.py(19 tests: state bookkeeping, view construction, gate decisions, and the re-add/preemption/non-capturer branches).GPU — Qwen3-0.6B unless noted,
VLLM_USE_V2_MODEL_RUNNER=1, on 1×/2× RTX 3090. Mirrors the v1 validation matrix.Steering:
register_steering_modules+steering_module_ref); per-request scale (0 → baseline, 1 → steered); all three hook points (pre_attn/post_attn/post_mlp); the prefill-only tier.locally_owned_layersfiltering confirmed; no NCCL hang).Capture:
(1, hidden)bf16 row to a consumer'son_capture) and under cudagraph (the force-eager gate fires for that step); global-spec under cudagraph (persistent-buffer path, no force-eager).last_prompt,all_generated,all, and an explicit index list.all_generated(cache hit on the 2nd request) and recaptures underall_prompt(full re-forward) — identical to the v1 behavior.local_layer_rangefiltering correct).15 gen × 1024 × bf16capture and no other rank did.Performance — v1-vs-v2 latency (Qwen3-0.6B, steering-bench, cudagraphs on): every point within ±1% (measurement noise). The v2 port adds no latency penalty, and the steering-overhead profile matches v1 (~4% fixed for steering-on-idle, ~8–10% for real per-request steering).
Not exercised (out of scope / unreachable on this setup, all shared with or matching v1): spec-decode; DP; combined 2-D TP+PP and TP/PP > 2 (each validated independently; intersection needs ≥4 GPUs); the async-dispatch overload policies (
spill/drop/block— runner-agnostic transport shared with v1); the store serve path (doesn't trigger forall_prompteven on v1 — full recapture by design); and the steering admit-straight-to-decode branch (num_computed >= num_prompt), which the single-engine scheduler never produces — it always reserves the last block to recompute logits — so it's reachable only via a KV connector / disaggregated prefill, and mirrors v1's identical defensive branch.Notes