Files
vLLM/vllm/entrypoints/cli/preload.py
T
af5b4857e1 [Fast Start] Support PP (#55477)
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Xun Sun <UNIDY2002@outlook.com>
2026-09-29 09:37:43 +00:00

350 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""The `preload` subcommand for the vLLM CLI.
Launches one weight cache daemon per GPU; each daemon holds the
post-quantized, TP-sharded weights of its rank in GPU memory and serves
CUDA IPC handles to vLLM engines over a Unix domain socket, so restarting
engines can map the weights via zero-copy IPC instead of reloading from disk:
vllm preload --model /path/to/model --tensor-parallel-size 4
Engines then load from the daemons with:
vllm serve /path/to/model --tensor-parallel-size 4 --load-format ipc_cache
Tensor, expert and data parallelism are supported, including across nodes;
see vllm/model_executor/model_loader/weight_cache/daemon.py for the rank
layout and rendezvous details.
"""
import argparse
import multiprocessing
import queue
import signal
import sys
import threading
import typing
from collections.abc import Sequence
from itertools import product
from multiprocessing.process import BaseProcess
from fastapi import FastAPI
from fastapi.responses import Response
from vllm.engine.arg_utils import EngineArgs
from vllm.entrypoints.cli.types import CLISubcommand
from vllm.entrypoints.serve.utils.api_utils import VLLM_SUBCMD_PARSER_EPILOG
from vllm.logger import init_logger
from vllm.model_executor.model_loader.weight_cache.protocol import (
check_ipc_platform_support,
)
from vllm.utils.network_utils import get_distributed_init_method, get_open_port
if typing.TYPE_CHECKING:
from vllm.config import VllmConfig
from vllm.utils.argparse_utils import FlexibleArgumentParser
else:
FlexibleArgumentParser = argparse.ArgumentParser
logger = init_logger(__name__)
class _HealthState:
def __init__(self, processes: Sequence[BaseProcess]) -> None:
self.processes = processes
self._ready = False
self._lock = threading.Lock()
def mark_ready(self) -> None:
with self._lock:
self._ready = True
@property
def is_ready(self) -> bool:
with self._lock:
ready = self._ready
return ready and not any(proc.exitcode is not None for proc in self.processes)
def _build_health_app(state: _HealthState) -> FastAPI:
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
@app.get("/health", include_in_schema=False)
async def health() -> Response:
return Response(status_code=200 if state.is_ready else 503)
return app
def _start_health_server(
host: str, port: int, state: _HealthState
) -> tuple[typing.Any, threading.Thread]:
import uvicorn
config = uvicorn.Config(
_build_health_app(state),
host=host,
port=port,
log_level="error",
access_log=False,
)
server = uvicorn.Server(config)
thread = threading.Thread(
target=server.run,
name="vllm-weight-cache-health",
daemon=True,
)
thread.start()
return server, thread
class PreloadSubcommand(CLISubcommand):
"""The `preload` subcommand for the vLLM CLI."""
name = "preload"
@staticmethod
def add_cli_args(parser: FlexibleArgumentParser) -> None:
EngineArgs.add_cli_args(parser)
parser.add_argument(
"--weight-cache-socket-dir",
type=str,
default=None,
help="Directory for the daemon Unix sockets (default: tempdir).",
)
parser.add_argument(
"--weight-cache-master-port",
type=int,
default=None,
help="Rendezvous port for the daemon's own TP group. Must differ "
"from the engine's --master-port (the daemon holds its group open "
"while serving) and match across nodes. Required when --nnodes > "
"1; defaults to a free port for single-node.",
)
parser.add_argument(
"--weight-cache-draft-master-port",
type=int,
default=None,
help="Rendezvous port for the MTP draft daemon group. Defaults to "
"--weight-cache-master-port + 1 for multi-node, or a free port "
"for single-node.",
)
parser.add_argument(
"--weight-cache-health-port",
type=int,
default=None,
help="Port for the parent /health endpoint; disabled by default.",
)
parser.add_argument(
"--weight-cache-health-host",
type=str,
default="0.0.0.0",
help="Host for the parent /health endpoint (default: 0.0.0.0).",
)
@staticmethod
def cmd(args: argparse.Namespace) -> None:
# Imported here so `vllm --help` and friends stay free of torch.
from vllm.model_executor.model_loader.weight_cache.daemon import (
_reject_unsupported_parallelism,
_run_daemon,
get_draft_daemon_config,
plan_local_ranks,
)
from vllm.model_executor.model_loader.weight_cache.utils import (
format_daemon_role,
)
engine_args = EngineArgs.from_cli_args(args)
vllm_config = engine_args.create_engine_config()
if vllm_config.load_config.load_format == "ipc_cache":
raise ValueError(
"The weight cache daemon itself must load from disk; use the "
"default --load-format"
)
# Config-only so it can fail before any model loading; the quant
# method check needs the created model and runs in
# WeightCacheDaemon.get_model.
check_ipc_platform_support()
parallel_config = vllm_config.parallel_config
_reject_unsupported_parallelism(parallel_config)
tp_size = parallel_config.tensor_parallel_size
pp_size = parallel_config.pipeline_parallel_size
dp_size = parallel_config.data_parallel_size
placements = plan_local_ranks(parallel_config)
local_world_size = len(placements)
if dp_size == 1:
nnodes = parallel_config.nnodes
node_rank = parallel_config.node_rank
master_addr = parallel_config.master_addr
else:
master_addr = parallel_config.data_parallel_master_ip
if parallel_config.nnodes > 1:
nnodes = parallel_config.nnodes
node_rank = parallel_config.node_rank
else:
nnodes = dp_size // parallel_config.data_parallel_size_local
node_rank = parallel_config.data_parallel_rank // (
parallel_config.data_parallel_size_local
)
if nnodes > 1 and master_addr in ("127.0.0.1", "localhost"):
raise ValueError(
"Data parallelism across nodes requires a reachable "
"--data-parallel-address for the daemon rendezvous"
)
# The daemon forms its own world group and holds it open while
# serving, so it needs a rendezvous port distinct from the engine's.
# All nodes must agree on it; single-node can auto-pick a free port.
# The master address is the engine's --master-addr (TP across nodes)
# or --data-parallel-address.
if nnodes > 1 and args.weight_cache_master_port is None:
raise ValueError(
"--weight-cache-master-port is required when the daemons span nodes"
)
master_port = args.weight_cache_master_port or get_open_port()
distributed_init_method = get_distributed_init_method(master_addr, master_port)
# (is_draft, vllm_config, rendezvous) per daemon group.
groups: list[tuple[bool, VllmConfig, str]] = [
(False, vllm_config, distributed_init_method)
]
draft_vllm_config = get_draft_daemon_config(vllm_config)
if draft_vllm_config is not None:
draft_master_port = args.weight_cache_draft_master_port or (
master_port + 1 if nnodes > 1 else get_open_port()
)
if draft_master_port == master_port:
raise ValueError(
"--weight-cache-draft-master-port must differ from "
"--weight-cache-master-port"
)
groups.append(
(
True,
draft_vllm_config,
get_distributed_init_method(master_addr, draft_master_port),
)
)
ctx = multiprocessing.get_context("spawn")
ready_queue: multiprocessing.Queue[tuple[str, int]] = ctx.Queue()
# Local index == device index; global rank enumerates DP, PP, then TP.
expected_ready = {
(
format_daemon_role(is_draft),
dp_rank * pp_size * tp_size + pp_rank * tp_size + tp_rank,
)
for (is_draft, _, _), (_, dp_rank, pp_rank, tp_rank) in product(
groups, placements
)
}
procs = [
ctx.Process(
target=_run_daemon,
args=(
dp_rank * pp_size * tp_size + pp_rank * tp_size + tp_rank,
local_rank,
config,
init_method,
args.weight_cache_socket_dir,
ready_queue,
is_draft,
dp_rank,
pp_rank,
),
name=(
f"vllm-weight-cache-{format_daemon_role(is_draft)}-"
f"{(dp_rank * pp_size * tp_size + pp_rank * tp_size + tp_rank)}"
),
)
for (is_draft, config, init_method), (
local_rank,
dp_rank,
pp_rank,
tp_rank,
) in product(groups, placements)
]
health_state = _HealthState(procs)
health_server = None
health_thread = None
if args.weight_cache_health_port is not None:
health_server, health_thread = _start_health_server(
args.weight_cache_health_host,
args.weight_cache_health_port,
health_state,
)
for proc in procs:
proc.start()
def _shutdown(signum, frame):
if health_server is not None:
health_server.should_exit = True
for proc in procs:
proc.terminate()
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
ready: set[tuple[str, int]] = set()
while len(ready) < len(expected_ready):
try:
ready.add(ready_queue.get(timeout=1.0))
except queue.Empty:
dead = [p for p in procs if p.exitcode is not None]
if dead:
logger.error(
"Weight cache daemon rank(s) exited during startup "
"(exitcodes=%s); shutting down.",
[p.exitcode for p in dead],
)
for proc in procs:
proc.terminate()
for proc in procs:
proc.join()
if health_server is not None:
health_server.should_exit = True
if health_thread is not None:
health_thread.join()
sys.exit(max((p.exitcode or 0) for p in procs))
health_state.mark_ready()
socket_dir_msg = args.weight_cache_socket_dir or "the default socket dir"
logger.info_once(
"===== Weight cache daemon READY: node %d/%d serving %d local "
"rank(s) x %d role(s) in %s =====",
node_rank,
nnodes,
local_world_size,
len(groups),
socket_dir_msg,
)
for proc in procs:
proc.join()
if health_server is not None:
health_server.should_exit = True
if health_thread is not None:
health_thread.join()
sys.exit(max(proc.exitcode or 0 for proc in procs))
def subparser_init(
self, subparsers: argparse._SubParsersAction
) -> FlexibleArgumentParser:
preload_parser = subparsers.add_parser(
self.name,
help="Launch weight cache daemons (one per GPU) for fast engine restarts.",
description="Launch weight cache daemons (one per GPU) that "
"keep post-quantized weights in GPU memory and serve them to "
"engines over CUDA IPC.",
usage="vllm preload --model <model> [options]",
)
self.add_cli_args(preload_parser)
preload_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(subcmd=self.name)
return preload_parser
def cmd_init() -> list[CLISubcommand]:
return [PreloadSubcommand()]