[Offloader] Prefetch weight offloading for large MoE models, plus a schedule planner - #51710
lucifer1004 wants to merge 3 commits into
Conversation
Implements group-based async H2D prefetch of MoE expert weights so
DeepSeek-V4-Flash fits on 2x SM120 (96 GiB) with routed-expert weights
held in pinned host memory. Verified at 96.0% GSM8K accuracy
(50 questions, 5-shot, max_tokens=256, T=0) with ~16.1 GiB of per-GPU
memory saved against ~3.2 GiB of double-buffered runtime slabs.
New CLI flags (vllm/config/offload.py):
--offload-backend {auto,uva,prefetch}
--offload-group-size N
--offload-num-in-group K
--offload-prefetch-step S
--offload-selectors {routed_experts,shared_experts,dense_mlp,attention}
--offload-params <name-segment>...
--cpu-offload-gb <gib> (UVA backend only)
Adds 11 new modules under vllm/model_executor/offloader/ covering planner,
slab allocator, runtime accounting, semantic selectors, NUMA-local CPU
pinning, prefetch helpers, double-buffered runtime buffers, on-demand
onload path, tail-of-group D2H copy scheduler, diagnostics, and a pybind
extension surface. 13 tests under tests/weight_offload/ cover config,
lifecycle, runtime, planner, selectors, slab, hook deps, instrumentation,
entrypoints, tail scheduler, cudagraph compile-compat, and CPU master
tracking.
(cherry picked from commit f064592)
Signed-off-by: Tian Zheng <29906817+Tom-Zheng@users.noreply.github.com>
Follow-up fixes on top of the DSv4-Flash prefetch offloading change. - The slab allocator could request a pinned host block per layout with no upper bound. Bound it. - H2D prefetch copies and tensor-parallel collectives contend for the same links, so an unpaced copy can delay a collective that every rank is blocked on. Chunk the copies and gate them while a collective is in flight, behind `offload_comm_aware`. The offloader carries the flag and exposes `gates_collectives`, so the collective path does not read `vllm.envs` from low-level distributed code and the common no-op case costs one boolean rather than a context manager per collective. The pacing only engages at TP > 1. - Move the offloader tests from a new top-level `tests/weight_offload/` into `tests/model_executor/offloader/`. The old location was in no CI area's file list and outside the mypy hook's scope, so those tests were neither run by CI nor type-checked. Run the Model Executor area when `vllm/config/offload.py` changes, since offloader behaviour now depends on it. - Drop `docs/contributing/weight-offloading-upstreaming.md`, a record of an earlier internal cherry-pick attempt rather than contributor documentation. Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
Choosing `offload_group_size` / `offload_num_in_group` / `offload_prefetch_step` is guesswork today, and a schedule that does not fit says so only by failing to start. Finding out by launching each candidate is expensive. Two quantities decide viability, and both are exact arithmetic once the per-position weight and buffer sizes are known: the bytes that stay resident, and the bytes re-copied host-to-device each forward. Those sizes depend on the model, its quantization and the parallel split, so only a running engine knows them -- which is what the post-init manifest added earlier in this series reports. This reads that manifest and scores other schedules from it, so choosing between N of them costs one launch rather than N. Results are a delta against the recorded run. Everything that does not depend on the schedule -- KV cache, non-offloadable weights, allocator slack, graph reserve -- is identical across candidates and cancels, so no absolute memory model is needed and the operator hand-enters nothing. It models memory, not latency, and says so. Whether a transfer hides behind compute depends on measured bandwidth and per-layer compute time under real contention, which this cannot obtain; H2D volume per forward is reported because it is exact, not as a proxy for time. Buffer layouts are finalized per selected unit, so a schedule reaching positions the recorded run never offloaded is refused rather than guessed at when the run used more than one layout. Ranks are likewise never merged. Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
|
Documentation preview: https://vllm--51710.org.readthedocs.build/en/51710/ |
|
This pull request has merge conflicts that must be resolved before it can be |
ddd09dc to
c2c5175
Compare
|
I found #51710 while doing the final duplicate/overlap check for #54975. The focused PR fixes the same non-divisible static-buffer slot reuse problem that your new The focused fix was independently derived from vllm-project/vllm-ascend#10932 and validated with real Qwen3-0.6B weights on Ascend 910B3 in eager and ACLGraph modes. Since #51710 is a much broader draft, #54975 isolates only this correctness fix plus six small ring-shape regression cases so it can potentially land first. I called out the overlap explicitly in the PR description. If #54975 lands first, the corresponding scheduling portion here can be rebased onto it or dropped while retaining the broader runtime-controller design. Please let me know if you would prefer a different coordination path. |
Purpose
Extends the prefetch weight offloader (#29941) so DeepSeek-V4 class MoE models
run on hardware their expert weights do not fit on, and adds a way to choose an
offload schedule without launching one server per candidate.
Applying #29941 to DSv4 exposed three gaps: expert weights are heterogeneous
after quantization, H2D copies contend with tensor-parallel collectives on the
same links, and nothing reported which units were selected or how many bytes
each moved.
Scope. The mechanism is model-agnostic: units are discovered through
named_modules/named_parametersand selected by parameter-name segment, withno model-class dispatch and no architecture-specific branches. The selectors
cover fused (
experts.w13_weight/w2_weight) and unfused(
experts.gate_up_proj/down_proj) MoE layouts as well asfeed_forward.w*and attention. DeepSeek-V4 is what it was developed and measured against, not
what it is limited to — but it is the only architecture I have evidence for,
so treat other models as untested rather than unsupported.
Commit 1 is @Tom-Zheng's, submitted on his behalf with his authorization and
unmodified. Commits 2–3 are mine.
Not a duplicate
Checked
weight offloading,prefetch offload,offload selectors,offload NUMA,prefetch slab pinned,offloader diagnostics manifest,offload planner,weight offload configuration.cuMemcpyBatchAsync) is closest: it changesthe copy submission API, this changes what is copied and when. Intents
compose and I have not duplicated its
swap_blocks_batchchange, but bothtouch
prefetch.pyand will conflict textually — happy to rebase either way.OffloadConfig) is a June draft; thatmigration landed independently, so commit 2 follows the existing convention.
Hybrid KV offload: planner, MultiConnector, and mamba alignment for hybrid models #38261 is a KV offload planner — a different subsystem.
What is in it
1. Prefetch offloading for DSv4-Flash (@Tom-Zheng) — semantic parameter
selectors (
routed_experts,shared_experts,dense_mlp,attention),pooled slab and storage-group runtime buffers with a direct fallback for
layouts that cannot be pooled, a planner resolving which ordered module
positions become offload units, NUMA-aware process binding, and a read-only
post-init manifest describing the resulting schedule.
2. Bounded slabs and collective-aware pacing — the slab allocator could
request a pinned host block per layout with no upper bound; now bounded. H2D
copies are chunked and gated while a collective is in flight, behind
--offload-comm-aware, so an unpaced copy cannot delay a collective every rankis blocked on. The offloader carries the flag and exposes
gates_collectives,so low-level distributed code reads a boolean instead of
vllm.envsand thecommon no-op case costs no context manager. Pacing engages only at TP > 1.
Also moves the offloader tests from a new top-level
tests/weight_offload/into
tests/model_executor/offloader/. Not cosmetic: the old location was inno CI area's file list and outside the mypy hook's scope, so those tests were
neither run by CI nor type-checked. The Model Executor area now also triggers
on
vllm/config/offload.py.3. A schedule planner —
python -m vllm.benchmarks.weight_offload.plannerChoosing
offload_group_size/offload_num_in_group/offload_prefetch_stepis guesswork, and a schedule that does not fit says soonly by failing to start. Two exact quantities decide viability — bytes that
stay resident, and bytes re-copied H2D each forward — but both need
per-position sizes that only a running engine knows, which is what commit 1's
manifest reports. Serve once with
VLLM_PREFETCH_LOG_SCHEDULE=1, then scoreevery other schedule offline.
Results are deltas against that run, so schedule-independent terms (KV,
non-offloadable weights, allocator slack, graph reserve) cancel and the
operator enters nothing. It models memory, not latency, and says so: no
bandwidth setting, because bytes over bandwidth gives the PCIe time a schedule
occupies, not whether that time is hidden. This is also why the manifest lands
with it — it is the only artifact that makes the identities checkable.
Evidence
All clean on
main@3a79957b62; the three commits cherry-pick onto itwithout conflicts.
Serving, TP8
DeepSeek-V4-Pro, TP8, RTX PRO 6000,
offload_group_size=3 offload_num_in_group=1 offload_prefetch_step=3 offload_params=experts,gpu_memory_utilization=0.90, 8000-token prompts with one output token atconcurrency 1/2/4/8, cold prefix cache. Each arm is a separate server with the
torch.compilecache disabled, so no arm inherits another's compiled graphs.Every flag in the baseline case already parses on
3a79957b62.main@3a79957b62determine_available_memorywithcudaErrorStreamCaptureUnjoinedatbreakable_cudagraph.pycapture_end, on six of eight ranks — preciselywhat commit 1's
join_after_forwardhook prevents. Commit 1 is aprerequisite for this configuration, not a speedup on a working one.
independent instances on different trees.
mean of the pacing-off arms, uniform across the sweep, at ~75× the observed
run-to-run spread (≤0.13%).
KV holds 10,171 tokens here, barely more than one 8000-token request, so
concurrency above 1 measures queueing — TTFT @1 and steady throughput are the
load-bearing numbers.
Memory and transfer identities
Same model and schedule. Byte counts from the post-init manifest, transfer
figures from an Nsight Systems trace over explicit GPU-time windows. Measured
2026-08-04 on this code before the rebase onto current main; the rebase touched
no selector or buffer arithmetic.
Manifest against the analytical model: −46,880 B offloaded and −12,032 B
runtime buffer, both inside the log rounding interval. Reading that same
manifest, the planner reproduces all of it — 20 units, identical byte counts,
zero delta scoring the recorded schedule against itself — and reduces 10,446
enumerated schedules to 331 distinct outcomes.
Caveats: per-position compute was not measured (aggregate divided uniformly
gives 40.36 ms/position, a proxy). Synchronous TP is bounded by the slowest
participant, 24.68 GB/s, not the 25.98 GB/s mean. These figures are
instrumented and the latency table is not — separate measurements.
One interaction worth knowing
At
gpu_memory_utilization=0.952both B and C reach readiness and then dieidentically on the first request: FlashInfer's CUTLASS fused-MoE workspace
requests 1.32 GiB from
FusedMoeRunner::getWorkspaceInfoafter memoryprofiling has already passed. Offloading converts weight memory into headroom
that gets respent on KV, so a workspace allocated outside vLLM's profiling
becomes marginal exactly when offloading is doing its job. Not a defect in
these commits and the fix is headroom rather than code, but it is the first
failure an operator will hit.
AI assistance
Produced with AI assistance. Every changed line has been reviewed by the
submitter, and the tests and measurements above were executed on the hardware
described, with results reported as measured.