[UX][Frontend] Introduce `vllm preload` cli for fast restart (#56680)
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
Isotr0py committed
Sep 25, 2026 at 14:56 UTC
24928a4d62d4d317ba070d219ce0ab6c3e58bae4
8 files changed
+496
-193
docs/cli/README.md
+28
-1
@@ -9,7 +9,7 @@ vllm --help
9
Available Commands:
10
11
```bash
12
-vllm {chat,complete,serve,launch,bench,collect-env,run-batch}
12
+vllm {chat,complete,serve,launch,bench,collect-env,run-batch,preload}
13
```
14
15
## serve
@@ -212,6 +212,33 @@ vllm run-batch \
212
213
See [vllm run-batch](./run-batch.md) for the full reference of all available arguments.
214
215
+## preload
216
+
217
+Launch weight cache daemons (one per GPU) that hold the post-quantized,
218
+TP-sharded weights in GPU memory and serve CUDA IPC handles to vLLM engines
219
+over a Unix domain socket. Restarting engines then map the weights via
220
+zero-copy IPC instead of reloading from disk, enabling fast engine restarts.
221
+
222
+```bash
223
+# Launch one daemon per GPU
224
+vllm preload --model meta-llama/Llama-3.2-1B-Instruct --tensor-parallel-size 4
225
+
226
+# Engines then load from the daemons
227
+vllm serve meta-llama/Llama-3.2-1B-Instruct --tensor-parallel-size 4 \
228
+ --load-format ipc_cache
229
+```
230
+
231
+The daemon accepts the standard engine arguments (model, dtype, quantization,
232
+tensor-parallel-size, ...) plus `--weight-cache-socket-dir` to override the
233
+directory holding the per-GPU Unix sockets, and `--weight-cache-master-port` /
234
+`--weight-cache-draft-master-port` to pin the daemon rendezvous ports for
235
+multi-node and speculative-decoding setups. Tensor, expert and data
236
+parallelism are supported; pipeline parallelism is rejected at launch.
237
+
238
+See [Preload](../features/preload.md) for how it works, cache modes, and
239
+limitations, and [vllm preload](./preload.md) for the full reference of all
240
+available arguments.
241
+
242
## More Help
243
244
For detailed options of any subcommand, use:
docs/features/preload.md
new
+203
@@ -0,0 +1,203 @@
1
+# Preload
2
+
3
+vLLM's preload feature keeps model artifacts resident in GPU memory across
4
+engine restarts, so a restarting engine reuses them instead of rebuilding them
5
+from scratch. The design is extensible to different kinds of artifacts; it
6
+currently supports **model weights** via the weight cache daemon.
7
+
8
+With weight preloading, a daemon process per GPU holds its rank's
9
+post-quantized, TP-sharded weights and serves CUDA IPC handles to vLLM engines
10
+over a Unix domain socket, so a restarting engine maps the weights via
11
+zero-copy IPC instead of reloading them from disk.
12
+
13
+Key benefits:
14
+
15
+- **Fast engine restarts**: weight loading reduces to mapping existing GPU
16
+ memory, cutting restart time from minutes (disk load + quantization
17
+ processing) to seconds.
18
+- **Zero-copy sharing**: in the default `zero_copy` mode the engine directly
19
+ shares the daemon's GPU memory, so a restart adds no extra GPU memory
20
+ footprint.
21
+- **Safe fallback**: if the daemon is unavailable or its cached weights don't
22
+ match the engine's configuration, the engine falls back to loading from
23
+ disk.
24
+
25
+!!! note
26
+ Weight preloading is only supported on CUDA and ROCm platforms. Tensor,
27
+ expert and data parallelism are supported, including across nodes;
28
+ pipeline parallelism is rejected when launching the daemon.
29
+
30
+## Quick start
31
+
32
+Launch one weight cache daemon per GPU with a single command:
33
+
34
+```bash
35
+vllm preload --model meta-llama/Llama-3.1-8B-Instruct --tensor-parallel-size 4
36
+```
37
+
38
+The daemon process loads and post-processes the weights once, then waits for
39
+engines. Each rank binds its Unix socket only after its shard is fully cached,
40
+so engines connecting before that simply fall back to disk loading.
41
+
42
+Then start (or restart) engines with the `ipc_cache` load format:
43
+
44
+```bash
45
+vllm serve meta-llama/Llama-3.1-8B-Instruct \
46
+ --tensor-parallel-size 4 \
47
+ --load-format ipc_cache
48
+```
49
+
50
+The daemon itself must load from disk; passing `--load-format ipc_cache` to
51
+`vllm preload` is an error.
52
+
53
+## How it works
54
+
55
+1. `vllm preload` spawns one daemon process per GPU. Each daemon loads its
56
+ shard from disk using the configured loader, runs quantization
57
+ post-processing, and exports every parameter/buffer as a CUDA IPC handle.
58
+2. An engine started with `--load-format ipc_cache` builds its model on the
59
+ meta device (no storage) and requests the tensors from the daemon on its
60
+ GPU.
61
+3. Before serving anything, the engine and daemon compare a fingerprint of the
62
+ cached weights: checkpoint content (hashed from safetensors metadata, so
63
+ identical weights in different directories still match), model
64
+ architecture, TP/DP size/rank, dtype, quantization method and config, model
65
+ revision, and vLLM version. On any mismatch the engine falls back to disk
66
+ loading (unless `fallback` is disabled).
67
+4. The engine also verifies the daemon's GPU UUID matches its own device, so a
68
+ stale socket cannot serve weights for the wrong GPU.
69
+
70
+Tied weights (e.g. `lm_head.weight` sharing storage with
71
+`embed_tokens.weight`) are exported once and re-established as aliases in the
72
+engine, preserving parameter identity.
73
+
74
+## Multi-node and data parallelism
75
+
76
+CUDA IPC handles are node-local, so each node serves only its local GPUs'
77
+shards. For multi-node tensor parallelism, run one `vllm preload` launcher per
78
+node with a shared rendezvous: reuse the `--nnodes` / `--node-rank` /
79
+`--master-addr` flags you pass the engine, plus a `--weight-cache-master-port`
80
+distinct from the engine's `--master-port`:
81
+
82
+```bash
83
+# node 0 (8 local GPUs)
84
+vllm preload --model /path/to/model --tensor-parallel-size 16 \
85
+ --nnodes 2 --node-rank 0 --master-addr 10.0.0.1 \
86
+ --weight-cache-master-port 29600
87
+# node 1 (8 local GPUs)
88
+vllm preload --model /path/to/model --tensor-parallel-size 16 \
89
+ --nnodes 2 --node-rank 1 --master-addr 10.0.0.1 \
90
+ --weight-cache-master-port 29600
91
+```
92
+
93
+For data parallelism (e.g. a TP1 x DP16 x EP decode fleet), run one launcher
94
+per node with the engine's DP placement flags. Local GPU `i` serves DP rank
95
+`start_rank + i // tp_size` and TP rank `i % tp_size`, and all
96
+`dp_size * tp_size` daemons form one world group on `--data-parallel-address`
97
+/ `--weight-cache-master-port` so the expert shards are laid out exactly as in
98
+the engine:
99
+
100
+```bash
101
+# node r (4 local GPUs)
102
+vllm preload --model /path/to/model --tensor-parallel-size 1 \
103
+ --enable-expert-parallel \
104
+ --data-parallel-size 16 --data-parallel-size-local 4 \
105
+ --data-parallel-start-rank 4r --data-parallel-address 10.0.0.1 \
106
+ --weight-cache-master-port 29600
107
+```
108
+
109
+Data parallelism also combines with multi-node tensor parallelism: pass both
110
+flag sets. Each node then serves a contiguous block of the
111
+`dp_size * tp_size` global ranks.
112
+
113
+## Speculative decoding
114
+
115
+With MTP, EAGLE or EAGLE3 speculative decoding, `vllm preload` additionally
116
+starts a draft daemon group that caches the draft model. It uses its own cache
117
+key, Unix sockets (`*_draft.sock`) and rendezvous port
118
+(`--weight-cache-draft-master-port`, default `--weight-cache-master-port + 1`),
119
+so each daemon process serves exactly one model role. Other draft types are
120
+not cached and keep loading from disk in the engine.
121
+
122
+## Cache modes
123
+
124
+The loader supports two modes, selected via `--model-loader-extra-config`:
125
+
126
+- `zero_copy` (default): the engine maps the daemon's CUDA allocations
127
+ directly. The daemon must stay alive for the engine's lifetime, and each
128
+ GPU carries only one copy of the weights.
129
+- `copy`: the engine clones every tensor into its own GPU memory and then
130
+ asks the daemon to release its cache. Use this when the daemon should free
131
+ GPU memory after handing off, at the cost of a full copy per restart.
132
+
133
+!!! warning
134
+ In `zero_copy` mode the weights live in the daemon's CUDA IPC allocations,
135
+ so [sleep mode](sleep_mode.md) (CuMemAllocator weight offloading) must not
136
+ be used with this loader.
137
+
138
+## Loader configuration
139
+
140
+The `ipc_cache` loader accepts extra keys via `--model-loader-extra-config`:
141
+
142
+```bash
143
+vllm serve meta-llama/Llama-3.1-8B-Instruct \
144
+ --load-format ipc_cache \
145
+ --model-loader-extra-config '{"mode": "copy", "fallback": false}'
146
+```
147
+
148
+| Key | Default | Description |
149
+| --- | ------- | ----------- |
150
+| `socket_path` | per-GPU path derived from the GPU UUID | Explicit daemon socket path. |
151
+| `socket_dir` | per-user private dir under the temp dir | Directory containing the daemon sockets. |
152
+| `mode` | `zero_copy` | `zero_copy` or `copy` (see above). |
153
+| `fallback` | `true` | Fall back to disk loading when the daemon is unavailable or the fingerprints mismatch. |
154
+| `connect_timeout_s` | `5.0` | Socket connect timeout in seconds. |
155
+| `state_timeout_s` | `300.0` | Timeout for the weight-transfer request in seconds. |
156
+
157
+The daemon's `--weight-cache-socket-dir` and the loader's `socket_dir` /
158
+`socket_path` must agree when the default per-user directory is not used.
159
+Socket paths are derived from the GPU UUID, so they are stable regardless of
160
+`CUDA_VISIBLE_DEVICES` index remapping.
161
+
162
+## Limitations
163
+
164
+- **Platform**: CUDA and ROCm only; other platforms raise
165
+ `UnsupportedPlatformForIPCError` even when `fallback` is enabled, since it
166
+ is a permanent misconfiguration rather than a transient daemon outage.
167
+- **Parallelism**: tensor, expert and data parallelism are supported;
168
+ launching the daemon with pipeline parallelism is rejected.
169
+- **Quantization**: every quantization method in the model must declare
170
+ support for pre-processed weights (the daemon transfers weights *after*
171
+ quantization post-processing). Unsupported methods raise
172
+ `UnsupportedQuantForIPCError`.
173
+- **Consistency**: the daemon and engine must run the same vLLM version and
174
+ agree on model, dtype, quantization, and TP/DP layout, otherwise the
175
+ fingerprint mismatch triggers the disk fallback.
176
+- **Localhost only**: the daemon serves over a Unix domain socket; both the
177
+ daemon and the engines must run on the same node as the same user.
178
+
179
+## Security
180
+
181
+The socket protocol uses pickle and is intended only for trusted local
182
+processes owned by the same user:
183
+
184
+- Daemon sockets live in a per-user private directory (mode `0700`) and the
185
+ socket files are restricted to the owner (`0600`).
186
+- Both sides reject symlinked or non-owned socket paths; the auto-derived
187
+ directory is additionally rejected if it is group/world accessible.
188
+- On Linux the daemon verifies the connecting peer's UID via `SO_PEERCRED`.
189
+
190
+See the [security documentation](../usage/security.md) for vLLM's general
191
+threat model.
192
+
193
+## Daemon lifecycle
194
+
195
+- Each GPU's socket path is guarded by an exclusive lock file, so a second
196
+ daemon for the same GPU fails fast instead of clobbering the live socket.
197
+- `vllm preload` reports readiness once every rank is serving; if any rank
198
+ dies during startup, the remaining ranks are terminated and the command
199
+ exits with that rank's exit code.
200
+- `SIGINT`/`SIGTERM` to the `vllm preload` process terminates all daemon
201
+ ranks, which unlinks their sockets and releases the GPU memory.
202
+
203
+See [vllm preload](../cli/preload.md) for the full CLI reference.
docs/mkdocs/gen_files/generate_argparse.py
+2
@@ -177,6 +177,7 @@ BenchmarkMMProcessorSubcommand = auto_mock(
177
LaunchSubcommandBase = auto_mock("vllm.entrypoints.cli.launch", "LaunchSubcommandBase")
178
launch_description = auto_mock("vllm.entrypoints.cli.launch", "DESCRIPTION")
179
RenderSubcommand = auto_mock("vllm.entrypoints.cli.launch", "RenderSubcommand")
180
+PreloadSubcommand = auto_mock("vllm.entrypoints.cli.preload", "PreloadSubcommand")
181
sweep_subcommands = auto_mock("vllm.benchmarks.sweep.cli", "SUBCOMMANDS")
182
openai_cli_args = auto_mock("vllm.entrypoints.launchers", "cli_args")
183
run_batch = auto_mock("vllm.entrypoints.launchers", "run_batch")
@@ -330,6 +331,7 @@ pages = {
331
"cli/chat.md": (create_parser(ChatCommand.add_cli_args), False),
332
"cli/complete.md": (create_parser(CompleteCommand.add_cli_args), False),
333
"cli/run-batch.md": (create_parser(run_batch.make_arg_parser), True),
334
+ "cli/preload.md": (create_parser(PreloadSubcommand.add_cli_args), True),
335
"cli/launch/render.md": (create_parser(RenderSubcommand.add_cli_args), True),
336
"cli/bench/latency.md": (create_parser(bench_latency.add_cli_args), True),
337
# URL kept as `mm_processor` for back-compat; command name is `mm-processor`
tests/model_executor/model_loader/test_weight_cache.py
+2
-1
@@ -36,7 +36,8 @@ class WeightCacheDaemon:
36
self._cmd = [
37
sys.executable,
38
"-m",
39
- "vllm.model_executor.model_loader.weight_cache.daemon",
39
+ "vllm.entrypoints.cli.main",
40
+ "preload",
41
"--model",
42
model,
43
"--tensor-parallel-size",
vllm/config/load.py
+2
-3
@@ -39,9 +39,8 @@ class LoadConfig:
39
InstantTensor, which enables distributed loading with pipelined prefetching
40
and fast direct I/O.
41
- "ipc_cache" will map post-quantized weights from a local weight cache
42
- daemon via CUDA IPC for fast engine restarts. See
43
- `vllm/model_executor/model_loader/weight_cache/daemon.py` for how to
44
- launch the daemon.
42
+ daemon via CUDA IPC for fast engine restarts. Launch the daemon with
43
+ `vllm preload` first.
44
- "npcache" will load the weights in pytorch format and store a numpy cache
45
to speed up the loading.
46
- "dummy" will initialize the weights with random values, which is mainly
vllm/entrypoints/cli/main.py
+2
@@ -44,6 +44,7 @@ def main():
44
import vllm.entrypoints.cli.collect_env
45
import vllm.entrypoints.cli.launch
46
import vllm.entrypoints.cli.openai
47
+ import vllm.entrypoints.cli.preload
48
import vllm.entrypoints.cli.run_batch
49
import vllm.entrypoints.cli.serve
50
import vllm.entrypoints.cli.snapshot
@@ -59,6 +60,7 @@ def main():
60
vllm.entrypoints.cli.launch,
61
vllm.entrypoints.cli.benchmark.main,
62
vllm.entrypoints.cli.collect_env,
63
+ vllm.entrypoints.cli.preload,
64
vllm.entrypoints.cli.run_batch,
65
vllm.entrypoints.cli.snapshot,
66
]
vllm/entrypoints/cli/preload.py
new
+252
@@ -0,0 +1,252 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""The `preload` subcommand for the vLLM CLI.
4
+
5
+Launches one weight cache daemon per GPU; each daemon holds the
6
+post-quantized, TP-sharded weights of its rank in GPU memory and serves
7
+CUDA IPC handles to vLLM engines over a Unix domain socket, so restarting
8
+engines can map the weights via zero-copy IPC instead of reloading from disk:
9
+
10
+ vllm preload --model /path/to/model --tensor-parallel-size 4
11
+
12
+Engines then load from the daemons with:
13
+
14
+ vllm serve /path/to/model --tensor-parallel-size 4 --load-format ipc_cache
15
+
16
+Tensor, expert and data parallelism are supported, including across nodes;
17
+see vllm/model_executor/model_loader/weight_cache/daemon.py for the rank
18
+layout and rendezvous details.
19
+"""
20
+
21
+import argparse
22
+import multiprocessing
23
+import queue
24
+import signal
25
+import sys
26
+import typing
27
+from itertools import product
28
+
29
+from vllm.engine.arg_utils import EngineArgs
30
+from vllm.entrypoints.cli.types import CLISubcommand
31
+from vllm.entrypoints.serve.utils.api_utils import VLLM_SUBCMD_PARSER_EPILOG
32
+from vllm.logger import init_logger
33
+from vllm.model_executor.model_loader.weight_cache.protocol import (
34
+ check_ipc_platform_support,
35
+)
36
+from vllm.utils.network_utils import get_distributed_init_method, get_open_port
37
+
38
+if typing.TYPE_CHECKING:
39
+ from vllm.config import VllmConfig
40
+ from vllm.utils.argparse_utils import FlexibleArgumentParser
41
+else:
42
+ FlexibleArgumentParser = argparse.ArgumentParser
43
+
44
+logger = init_logger(__name__)
45
+
46
+
47
+class PreloadSubcommand(CLISubcommand):
48
+ """The `preload` subcommand for the vLLM CLI."""
49
+
50
+ name = "preload"
51
+
52
+ @staticmethod
53
+ def add_cli_args(parser: FlexibleArgumentParser) -> None:
54
+ EngineArgs.add_cli_args(parser)
55
+ parser.add_argument(
56
+ "--weight-cache-socket-dir",
57
+ type=str,
58
+ default=None,
59
+ help="Directory for the daemon Unix sockets (default: tempdir).",
60
+ )
61
+ parser.add_argument(
62
+ "--weight-cache-master-port",
63
+ type=int,
64
+ default=None,
65
+ help="Rendezvous port for the daemon's own TP group. Must differ "
66
+ "from the engine's --master-port (the daemon holds its group open "
67
+ "while serving) and match across nodes. Required when --nnodes > "
68
+ "1; defaults to a free port for single-node.",
69
+ )
70
+ parser.add_argument(
71
+ "--weight-cache-draft-master-port",
72
+ type=int,
73
+ default=None,
74
+ help="Rendezvous port for the MTP draft daemon group. Defaults to "
75
+ "--weight-cache-master-port + 1 for multi-node, or a free port "
76
+ "for single-node.",
77
+ )
78
+
79
+ @staticmethod
80
+ def cmd(args: argparse.Namespace) -> None:
81
+ # Imported here so `vllm --help` and friends stay free of torch.
82
+ from vllm.model_executor.model_loader.weight_cache.daemon import (
83
+ _reject_unsupported_parallelism,
84
+ _run_daemon,
85
+ get_draft_daemon_config,
86
+ plan_local_ranks,
87
+ )
88
+ from vllm.model_executor.model_loader.weight_cache.utils import (
89
+ format_daemon_role,
90
+ )
91
+
92
+ engine_args = EngineArgs.from_cli_args(args)
93
+ vllm_config = engine_args.create_engine_config()
94
+ if vllm_config.load_config.load_format == "ipc_cache":
95
+ raise ValueError(
96
+ "The weight cache daemon itself must load from disk; use the "
97
+ "default --load-format"
98
+ )
99
+ # Config-only so it can fail before any model loading; the quant
100
+ # method check needs the created model and runs in
101
+ # WeightCacheDaemon.get_model.
102
+ check_ipc_platform_support()
103
+ parallel_config = vllm_config.parallel_config
104
+ _reject_unsupported_parallelism(parallel_config)
105
+ tp_size = parallel_config.tensor_parallel_size
106
+ dp_size = parallel_config.data_parallel_size
107
+
108
+ placements = plan_local_ranks(parallel_config)
109
+ local_world_size = len(placements)
110
+ if dp_size == 1:
111
+ nnodes = parallel_config.nnodes
112
+ node_rank = parallel_config.node_rank
113
+ master_addr = parallel_config.master_addr
114
+ else:
115
+ master_addr = parallel_config.data_parallel_master_ip
116
+ if parallel_config.nnodes > 1:
117
+ nnodes = parallel_config.nnodes
118
+ node_rank = parallel_config.node_rank
119
+ else:
120
+ nnodes = dp_size // parallel_config.data_parallel_size_local
121
+ node_rank = parallel_config.data_parallel_rank // (
122
+ parallel_config.data_parallel_size_local
123
+ )
124
+ if nnodes > 1 and master_addr in ("127.0.0.1", "localhost"):
125
+ raise ValueError(
126
+ "Data parallelism across nodes requires a reachable "
127
+ "--data-parallel-address for the daemon rendezvous"
128
+ )
129
+
130
+ # The daemon forms its own world group and holds it open while
131
+ # serving, so it needs a rendezvous port distinct from the engine's.
132
+ # All nodes must agree on it; single-node can auto-pick a free port.
133
+ # The master address is the engine's --master-addr (TP across nodes)
134
+ # or --data-parallel-address.
135
+ if nnodes > 1 and args.weight_cache_master_port is None:
136
+ raise ValueError(
137
+ "--weight-cache-master-port is required when the daemons span nodes"
138
+ )
139
+ master_port = args.weight_cache_master_port or get_open_port()
140
+ distributed_init_method = get_distributed_init_method(master_addr, master_port)
141
+
142
+ # (is_draft, vllm_config, rendezvous) per daemon group.
143
+ groups: list[tuple[bool, VllmConfig, str]] = [
144
+ (False, vllm_config, distributed_init_method)
145
+ ]
146
+ draft_vllm_config = get_draft_daemon_config(vllm_config)
147
+ if draft_vllm_config is not None:
148
+ draft_master_port = args.weight_cache_draft_master_port or (
149
+ master_port + 1 if nnodes > 1 else get_open_port()
150
+ )
151
+ if draft_master_port == master_port:
152
+ raise ValueError(
153
+ "--weight-cache-draft-master-port must differ from "
154
+ "--weight-cache-master-port"
155
+ )
156
+ groups.append(
157
+ (
158
+ True,
159
+ draft_vllm_config,
160
+ get_distributed_init_method(master_addr, draft_master_port),
161
+ )
162
+ )
163
+
164
+ ctx = multiprocessing.get_context("spawn")
165
+ ready_queue: multiprocessing.Queue[tuple[str, int]] = ctx.Queue()
166
+ # Local index == device index; global rank enumerates DP then TP.
167
+ expected_ready = {
168
+ (format_daemon_role(is_draft), dp_rank * tp_size + tp_rank)
169
+ for (is_draft, _, _), (_, dp_rank, tp_rank) in product(groups, placements)
170
+ }
171
+ procs = [
172
+ ctx.Process(
173
+ target=_run_daemon,
174
+ args=(
175
+ tp_rank,
176
+ local_rank,
177
+ config,
178
+ init_method,
179
+ args.weight_cache_socket_dir,
180
+ ready_queue,
181
+ is_draft,
182
+ dp_rank,
183
+ ),
184
+ name=f"vllm-weight-cache-{format_daemon_role(is_draft)}-"
185
+ f"{dp_rank * tp_size + tp_rank}",
186
+ )
187
+ for (is_draft, config, init_method), (
188
+ local_rank,
189
+ dp_rank,
190
+ tp_rank,
191
+ ) in product(groups, placements)
192
+ ]
193
+ for proc in procs:
194
+ proc.start()
195
+
196
+ def _shutdown(signum, frame):
197
+ for proc in procs:
198
+ proc.terminate()
199
+
200
+ signal.signal(signal.SIGINT, _shutdown)
201
+ signal.signal(signal.SIGTERM, _shutdown)
202
+
203
+ ready: set[tuple[str, int]] = set()
204
+ while len(ready) < len(expected_ready):
205
+ try:
206
+ ready.add(ready_queue.get(timeout=1.0))
207
+ except queue.Empty:
208
+ dead = [p for p in procs if p.exitcode is not None]
209
+ if dead:
210
+ logger.error(
211
+ "Weight cache daemon rank(s) exited during startup "
212
+ "(exitcodes=%s); shutting down.",
213
+ [p.exitcode for p in dead],
214
+ )
215
+ for proc in procs:
216
+ proc.terminate()
217
+ for proc in procs:
218
+ proc.join()
219
+ sys.exit(max((p.exitcode or 0) for p in procs))
220
+ socket_dir_msg = args.weight_cache_socket_dir or "the default socket dir"
221
+ logger.info_once(
222
+ "===== Weight cache daemon READY: node %d/%d serving %d local "
223
+ "rank(s) x %d role(s) in %s =====",
224
+ node_rank,
225
+ nnodes,
226
+ local_world_size,
227
+ len(groups),
228
+ socket_dir_msg,
229
+ )
230
+
231
+ for proc in procs:
232
+ proc.join()
233
+ sys.exit(max(proc.exitcode or 0 for proc in procs))
234
+
235
+ def subparser_init(
236
+ self, subparsers: argparse._SubParsersAction
237
+ ) -> FlexibleArgumentParser:
238
+ preload_parser = subparsers.add_parser(
239
+ self.name,
240
+ help="Launch weight cache daemons (one per GPU) for fast engine restarts.",
241
+ description="Launch weight cache daemons (one per GPU) that "
242
+ "keep post-quantized weights in GPU memory and serve them to "
243
+ "engines over CUDA IPC.",
244
+ usage="vllm preload --model <model> [options]",
245
+ )
246
+ self.add_cli_args(preload_parser)
247
+ preload_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(subcmd=self.name)
248
+ return preload_parser
249
+
250
+
251
+def cmd_init() -> list[CLISubcommand]:
252
+ return [PreloadSubcommand()]
vllm/model_executor/model_loader/weight_cache/daemon.py
+5
-188
@@ -9,7 +9,7 @@ reloading from disk.
9
10
Launch one daemon per TP rank with a single command:
11
12
- python -m vllm.model_executor.model_loader.weight_cache.daemon \\
12
+ vllm preload \\
13
--model /path/to/model --tensor-parallel-size 4
14
15
Engines then load from the daemons with:
@@ -27,12 +27,12 @@ node-local, so each node serves only its local GPUs' shards). Reuse the same
27
``--weight-cache-master-port`` distinct from the engine's ``--master-port``:
28
29
# node 0 (8 local GPUs)
30
- python -m vllm.model_executor.model_loader.weight_cache.daemon \\
30
+ vllm preload \\
31
--model /path/to/model --tensor-parallel-size 16 \\
32
--nnodes 2 --node-rank 0 --master-addr 10.0.0.1 \\
33
--weight-cache-master-port 29600
34
# node 1 (8 local GPUs)
35
- python -m vllm.model_executor.model_loader.weight_cache.daemon \\
35
+ vllm preload \\
36
--model /path/to/model --tensor-parallel-size 16 \\
37
--nnodes 2 --node-rank 1 --master-addr 10.0.0.1 \\
38
--weight-cache-master-port 29600
@@ -50,7 +50,7 @@ node with the engine's DP placement flags. Local GPU ``i`` serves DP rank
50
shards are laid out exactly as in the engine:
51
52
# node r (4 local GPUs)
53
- python -m vllm.model_executor.model_loader.weight_cache.daemon \\
53
+ vllm preload \\
54
--model /path/to/model --tensor-parallel-size 1 --enable-expert-parallel \\
55
--data-parallel-size 16 --data-parallel-size-local 4 \\
56
--data-parallel-start-rank 4r --data-parallel-address 10.0.0.1 \\
@@ -62,7 +62,7 @@ a reachable ``--data-parallel-address``). Each node then serves a contiguous
62
block of the ``dp_size * tp_size`` global ranks, e.g. TP8 x DP2 on 4 nodes:
63
64
# node r (4 local GPUs)
65
- python -m vllm.model_executor.model_loader.weight_cache.daemon \\
65
+ vllm preload \\
66
--model /path/to/model --tensor-parallel-size 8 --enable-expert-parallel \\
67
--nnodes 4 --node-rank r --master-addr 10.0.0.1 \\
68
--data-parallel-size 2 --data-parallel-address 10.0.0.1 \\
@@ -80,12 +80,8 @@ import contextlib
80
import fcntl
81
import multiprocessing
82
import os
83
-import queue
84
-import signal
83
import socket
86
-import sys
84
from collections.abc import Callable
88
-from itertools import product
85
86
import torch
87
@@ -99,7 +95,6 @@ from vllm.distributed import (
95
ensure_model_parallel_initialized,
96
init_distributed_environment,
97
)
102
-from vllm.engine.arg_utils import EngineArgs
98
from vllm.logger import init_logger
99
from vllm.model_executor.model_loader import get_model_loader
100
from vllm.model_executor.model_loader.utils import process_weights_after_loading
@@ -107,7 +102,6 @@ from vllm.model_executor.model_loader.weight_cache.protocol import (
102
TensorEntry,
103
WeightCacheKey,
104
WeightCacheUnavailableError,
110
- check_ipc_platform_support,
105
check_ipc_quant_support,
106
ensure_private_socket_dir,
107
get_current_device_uuid,
@@ -122,8 +116,6 @@ from vllm.model_executor.model_loader.weight_cache.utils import (
116
is_draft_model_cacheable,
117
)
118
from vllm.platforms import current_platform
125
-from vllm.utils.argparse_utils import FlexibleArgumentParser
126
-from vllm.utils.network_utils import get_distributed_init_method, get_open_port
119
from vllm.utils.torch_utils import set_default_torch_dtype
120
from vllm.v1.worker.workspace import init_workspace_manager
121
@@ -504,178 +496,3 @@ def _reject_unsupported_parallelism(parallel_config: ParallelConfig) -> None:
496
"--data-parallel-start-rank + --data-parallel-size-local "
497
f"({end_rank}) exceeds --data-parallel-size ({dp_size})"
498
)
507
-
508
-
509
-def main() -> None:
510
- parser = FlexibleArgumentParser(
511
- description="Launch weight cache daemons (one per TP rank)."
512
- )
513
- EngineArgs.add_cli_args(parser)
514
- parser.add_argument(
515
- "--weight-cache-socket-dir",
516
- type=str,
517
- default=None,
518
- help="Directory for the daemon Unix sockets (default: tempdir).",
519
- )
520
- parser.add_argument(
521
- "--weight-cache-master-port",
522
- type=int,
523
- default=None,
524
- help="Rendezvous port for the daemon's own TP group. Must differ from "
525
- "the engine's --master-port (the daemon holds its group open while "
526
- "serving) and match across nodes. Required when --nnodes > 1; defaults "
527
- "to a free port for single-node.",
528
- )
529
- parser.add_argument(
530
- "--weight-cache-draft-master-port",
531
- type=int,
532
- default=None,
533
- help="Rendezvous port for the MTP draft daemon group. Defaults to "
534
- "--weight-cache-master-port + 1 for multi-node, or a free port for "
535
- "single-node.",
536
- )
537
- args = parser.parse_args()
538
- engine_args = EngineArgs.from_cli_args(args)
539
- vllm_config = engine_args.create_engine_config()
540
- if vllm_config.load_config.load_format == "ipc_cache":
541
- raise ValueError(
542
- "The weight cache daemon itself must load from disk; use the "
543
- "default --load-format"
544
- )
545
- # Config-only so it can fail before any model loading; the quant method
546
- # check needs the created model and runs in get_daemon_model.
547
- check_ipc_platform_support()
548
- parallel_config = vllm_config.parallel_config
549
- _reject_unsupported_parallelism(parallel_config)
550
- tp_size = parallel_config.tensor_parallel_size
551
- dp_size = parallel_config.data_parallel_size
552
-
553
- placements = plan_local_ranks(parallel_config)
554
- local_world_size = len(placements)
555
- if dp_size == 1:
556
- nnodes = parallel_config.nnodes
557
- node_rank = parallel_config.node_rank
558
- master_addr = parallel_config.master_addr
559
- else:
560
- master_addr = parallel_config.data_parallel_master_ip
561
- if parallel_config.nnodes > 1:
562
- nnodes = parallel_config.nnodes
563
- node_rank = parallel_config.node_rank
564
- else:
565
- nnodes = dp_size // parallel_config.data_parallel_size_local
566
- node_rank = parallel_config.data_parallel_rank // (
567
- parallel_config.data_parallel_size_local
568
- )
569
- if nnodes > 1 and master_addr in ("127.0.0.1", "localhost"):
570
- raise ValueError(
571
- "Data parallelism across nodes requires a reachable "
572
- "--data-parallel-address for the daemon rendezvous"
573
- )
574
-
575
- # The daemon forms its own world group and holds it open while serving, so
576
- # it needs a rendezvous port distinct from the engine's. All nodes must
577
- # agree on it; single-node can auto-pick a free port. The master address is
578
- # the engine's --master-addr (TP across nodes) or --data-parallel-address.
579
- if nnodes > 1 and args.weight_cache_master_port is None:
580
- raise ValueError(
581
- "--weight-cache-master-port is required when the daemons span nodes"
582
- )
583
- master_port = args.weight_cache_master_port or get_open_port()
584
- distributed_init_method = get_distributed_init_method(master_addr, master_port)
585
-
586
- # (is_draft, vllm_config, rendezvous) per daemon group.
587
- groups: list[tuple[bool, VllmConfig, str]] = [
588
- (False, vllm_config, distributed_init_method)
589
- ]
590
- draft_vllm_config = get_draft_daemon_config(vllm_config)
591
- if draft_vllm_config is not None:
592
- draft_master_port = args.weight_cache_draft_master_port or (
593
- master_port + 1 if nnodes > 1 else get_open_port()
594
- )
595
- if draft_master_port == master_port:
596
- raise ValueError(
597
- "--weight-cache-draft-master-port must differ from "
598
- "--weight-cache-master-port"
599
- )
600
- groups.append(
601
- (
602
- True,
603
- draft_vllm_config,
604
- get_distributed_init_method(master_addr, draft_master_port),
605
- )
606
- )
607
-
608
- ctx = multiprocessing.get_context("spawn")
609
- ready_queue: multiprocessing.Queue[tuple[str, int]] = ctx.Queue()
610
- # Local index == device index; global rank enumerates DP then TP.
611
- expected_ready = {
612
- (format_daemon_role(is_draft), dp_rank * tp_size + tp_rank)
613
- for (is_draft, _, _), (_, dp_rank, tp_rank) in product(groups, placements)
614
- }
615
- procs = [
616
- ctx.Process(
617
- target=_run_daemon,
618
- args=(
619
- tp_rank,
620
- local_rank,
621
- config,
622
- init_method,
623
- args.weight_cache_socket_dir,
624
- ready_queue,
625
- is_draft,
626
- dp_rank,
627
- ),
628
- name=f"vllm-weight-cache-{format_daemon_role(is_draft)}-"
629
- f"{dp_rank * tp_size + tp_rank}",
630
- )
631
- for (is_draft, config, init_method), (
632
- local_rank,
633
- dp_rank,
634
- tp_rank,
635
- ) in product(groups, placements)
636
- ]
637
- for proc in procs:
638
- proc.start()
639
-
640
- def _shutdown(signum, frame):
641
- for proc in procs:
642
- proc.terminate()
643
-
644
- signal.signal(signal.SIGINT, _shutdown)
645
- signal.signal(signal.SIGTERM, _shutdown)
646
-
647
- ready: set[tuple[str, int]] = set()
648
- while len(ready) < len(expected_ready):
649
- try:
650
- ready.add(ready_queue.get(timeout=1.0))
651
- except queue.Empty:
652
- dead = [p for p in procs if p.exitcode is not None]
653
- if dead:
654
- logger.error(
655
- "Weight cache daemon rank(s) exited during startup "
656
- "(exitcodes=%s); shutting down.",
657
- [p.exitcode for p in dead],
658
- )
659
- for proc in procs:
660
- proc.terminate()
661
- for proc in procs:
662
- proc.join()
663
- sys.exit(max((p.exitcode or 0) for p in procs))
664
- socket_dir_msg = args.weight_cache_socket_dir or "the default socket dir"
665
- logger.info_once(
666
- "===== Weight cache daemon READY: node %d/%d serving %d local rank(s) "
667
- "x %d role(s) in %s =====",
668
- node_rank,
669
- nnodes,
670
- local_world_size,
671
- len(groups),
672
- socket_dir_msg,
673
- )
674
-
675
- for proc in procs:
676
- proc.join()
677
- sys.exit(max(proc.exitcode or 0 for proc in procs))
678
-
679
-
680
-if __name__ == "__main__":
681
- main()