@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / a6fed02068
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 15, 11:00 UTC

[V1][PP] Support PP for MultiprocExecutor (#14219)

Signed-off-by: jiang1.li <jiang1.li@intel.com> Signed-off-by: jiang.li <jiang1.li@intel.com>

Li, Jiang committed May 6, 2025 at 22:58 UTC a6fed02068e7f2898715f80fbac93de4bea076be
5 files changed +98 -28
tests/distributed/test_pipeline_parallel.py
+7 -3
@@ -100,9 +100,8 @@ class PPTestSettings:
100 eager_mode=True,
101 chunked_prefill=False),
102 ],
103 - # only ray is supported for V1
104 - distributed_backends=["mp", "ray", "ray"],
105 - vllm_major_versions=["0", "0", "1"],
103 + distributed_backends=["mp", "mp", "ray", "ray"],
104 + vllm_major_versions=["0", "1", "0", "1"],
105 task=task,
106 test_options=PPTestOptions(multi_node_only=multi_node_only,
107 load_format=load_format),
@@ -350,6 +349,11 @@ def _compare_tp(
349 # Temporary. Currently when zeromq + SPMD is used, it does not properly
350 # terminate because of a Ray Compiled Graph issue.
351 common_args.append("--disable-frontend-multiprocessing")
352 + elif distributed_backend == "mp":
353 + # Both V0/V1 of multiprocessing executor support PP
354 + pp_env = {
355 + "VLLM_USE_V1": vllm_major_version,
356 + }
357 else:
358 pp_env = None
359
vllm/engine/arg_utils.py
+3 -4
@@ -1338,11 +1338,10 @@ class EngineArgs:
1338 and _warn_or_fallback("Engine in background thread")):
1339 return False
1340
1341 - # PP is supported on V1 with Ray distributed executor,
1342 - # but off for MP distributed executor for now.
1341 if (self.pipeline_parallel_size > 1
1344 - and self.distributed_executor_backend != "ray"):
1345 - name = "Pipeline Parallelism without Ray distributed executor"
1342 + and self.distributed_executor_backend not in ["ray", "mp"]):
1343 + name = "Pipeline Parallelism without Ray distributed executor " \
1344 + "or multiprocessing executor"
1345 _raise_or_fallback(feature_name=name, recommend_to_remove=False)
1346 return False
1347
vllm/v1/executor/multiproc_executor.py
+69 -18
@@ -8,7 +8,7 @@ import threading
8 import time
9 import traceback
10 import weakref
11 -from concurrent.futures import Future
11 +from concurrent.futures import Future, ThreadPoolExecutor
12 from dataclasses import dataclass
13 from enum import Enum, auto
14 from functools import partial
@@ -53,10 +53,11 @@ class MultiprocExecutor(Executor):
53
54 self.world_size = self.parallel_config.world_size
55 tensor_parallel_size = self.parallel_config.tensor_parallel_size
56 - assert self.world_size == tensor_parallel_size, (
56 + pp_parallel_size = self.parallel_config.pipeline_parallel_size
57 + assert self.world_size == tensor_parallel_size * pp_parallel_size, (
58 f"world_size ({self.world_size}) must be equal to the "
58 - f"tensor_parallel_size ({tensor_parallel_size}). "
59 - f"Pipeline parallelism is not yet implemented in v1")
59 + f"tensor_parallel_size ({tensor_parallel_size}) x pipeline"
60 + f"_parallel_size ({pp_parallel_size}). ")
61
62 # Set multiprocessing envs that are common to V0 and V1
63 set_multiprocessing_worker_envs(self.parallel_config)
@@ -104,6 +105,17 @@ class MultiprocExecutor(Executor):
105 self._ensure_worker_termination(
106 [w.proc for w in unready_workers])
107
108 + # For pipeline parallel, we use a thread pool for asynchronous
109 + # execute_model.
110 + self.io_thread_pool: Optional[ThreadPoolExecutor] = None
111 + if self.max_concurrent_batches > 1:
112 + # Note: must use only 1 IO thread to keep dequeue sequence
113 + # from the response queue
114 + self.io_thread_pool = ThreadPoolExecutor(
115 + max_workers=1, thread_name_prefix="mp_exec_io")
116 +
117 + self.output_rank = self._get_output_rank()
118 +
119 def start_worker_monitor(self):
120 workers = self.workers
121 self_ref = weakref.ref(self)
@@ -145,7 +157,9 @@ class MultiprocExecutor(Executor):
157 ) -> Union[ModelRunnerOutput, Future[ModelRunnerOutput]]:
158 (output, ) = self.collective_rpc("execute_model",
159 args=(scheduler_output, ),
148 - rank0_reply_only=True,
160 + unique_reply_rank=self.output_rank,
161 + non_block=self.max_concurrent_batches
162 + > 1,
163 timeout=EXECUTE_MODEL_TIMEOUT_S)
164 return output
165
@@ -154,7 +168,8 @@ class MultiprocExecutor(Executor):
168 timeout: Optional[float] = None,
169 args: tuple = (),
170 kwargs: Optional[dict] = None,
157 - rank0_reply_only: bool = False) -> list[Any]:
171 + non_block: bool = False,
172 + unique_reply_rank: Optional[int] = None) -> list[Any]:
173 if self.is_failed:
174 raise RuntimeError("Executor failed.")
175
@@ -171,22 +186,35 @@ class MultiprocExecutor(Executor):
186 send_method = cloudpickle.dumps(
187 method, protocol=pickle.HIGHEST_PROTOCOL)
188 self.rpc_broadcast_mq.enqueue(
174 - (send_method, args, kwargs, rank0_reply_only))
189 + (send_method, args, kwargs, unique_reply_rank))
190
176 - workers = (self.workers[0], ) if rank0_reply_only else self.workers
177 - responses = [None] * len(workers)
178 - for w in workers:
179 - dequeue_timeout = None if deadline is None else (
180 - deadline - time.monotonic())
191 + workers = (self.workers[unique_reply_rank],
192 + ) if unique_reply_rank is not None else self.workers
193 + responses = []
194 +
195 + def get_response(w: WorkerProcHandle,
196 + dequeue_timeout: Optional[float] = None,
197 + cancel_event: Optional[threading.Event] = None):
198 status, result = w.worker_response_mq.dequeue(
182 - timeout=dequeue_timeout, cancel=self.shutdown_event)
199 + timeout=dequeue_timeout, cancel=cancel_event)
200
201 if status != WorkerProc.ResponseStatus.SUCCESS:
202 raise RuntimeError(
203 f"Worker failed with error '{result}', please check the"
204 " stack trace above for the root cause")
205 + return result
206
189 - responses[w.rank] = result
207 + for w in workers:
208 + dequeue_timeout = None if deadline is None else (
209 + deadline - time.monotonic())
210 +
211 + if non_block:
212 + result = self.io_thread_pool.submit( # type: ignore
213 + get_response, w, dequeue_timeout, self.shutdown_event)
214 + else:
215 + result = get_response(w, dequeue_timeout)
216 +
217 + responses.append(result)
218
219 return responses
220 except TimeoutError as e:
@@ -225,6 +253,11 @@ class MultiprocExecutor(Executor):
253 if not getattr(self, 'shutting_down', False):
254 self.shutting_down = True
255 self.shutdown_event.set()
256 +
257 + if self.io_thread_pool is not None:
258 + self.io_thread_pool.shutdown(wait=False, cancel_futures=True)
259 + self.io_thread_pool = None
260 +
261 for w in self.workers:
262 w.worker_response_mq = None
263 self._ensure_worker_termination([w.proc for w in self.workers])
@@ -235,6 +268,22 @@ class MultiprocExecutor(Executor):
268 self.collective_rpc("check_health", timeout=10)
269 return
270
271 + @property
272 + def max_concurrent_batches(self) -> int:
273 + return self.parallel_config.pipeline_parallel_size
274 +
275 + def _get_output_rank(self) -> int:
276 + # Only returns ModelRunnerOutput from TP rank=0 and PP rank=-1
277 + # (the first TP worker of the last PP stage).
278 + # Example:
279 + # Assuming TP=8, PP=4, then the world_size=32
280 + # 0-7, PP rank 0
281 + # 8-15, PP rank 1
282 + # 16-23, PP rank 2
283 + # 24-31, PP rank 3
284 + # so world_size - tp_size = 32 - 8 = 24 should be PP rank = -1 (i.e. 3)
285 + return self.world_size - self.parallel_config.tensor_parallel_size
286 +
287
288 @dataclass
289 class UnreadyWorkerProcHandle:
@@ -280,12 +329,14 @@ class WorkerProc:
329 all_kwargs: list[dict] = [
330 {} for _ in range(vllm_config.parallel_config.world_size)
331 ]
332 + is_driver_worker = (
333 + rank % vllm_config.parallel_config.tensor_parallel_size == 0)
334 all_kwargs[rank] = {
335 "vllm_config": vllm_config,
336 "local_rank": local_rank,
337 "rank": rank,
338 "distributed_init_method": distributed_init_method,
288 - "is_driver_worker": rank == 0,
339 + "is_driver_worker": is_driver_worker,
340 }
341 wrapper.init_worker(all_kwargs)
342 self.worker = wrapper
@@ -455,7 +506,7 @@ class WorkerProc:
506 def worker_busy_loop(self):
507 """Main busy loop for Multiprocessing Workers"""
508 while True:
458 - method, args, kwargs, rank0_only = self.rpc_broadcast_mq.dequeue()
509 + method, args, kwargs, output_rank = self.rpc_broadcast_mq.dequeue()
510
511 try:
512 if isinstance(method, str):
@@ -470,11 +521,11 @@ class WorkerProc:
521 logger.exception("WorkerProc hit an exception.")
522 # exception might not be serializable, so we convert it to
523 # string, only for logging purpose.
473 - if not rank0_only or self.rank == 0:
524 + if output_rank is None or self.rank == output_rank:
525 self.worker_response_mq.enqueue(
526 (WorkerProc.ResponseStatus.FAILURE, str(e)))
527 continue
528
478 - if not rank0_only or self.rank == 0:
529 + if output_rank is None or self.rank == output_rank:
530 self.worker_response_mq.enqueue(
531 (WorkerProc.ResponseStatus.SUCCESS, output))
vllm/v1/worker/gpu_model_runner.py
+1 -1
@@ -1016,7 +1016,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1016 self,
1017 scheduler_output: "SchedulerOutput",
1018 intermediate_tensors: Optional[IntermediateTensors] = None,
1019 - ) -> Union[ModelRunnerOutput, torch.Tensor]:
1019 + ) -> Union[ModelRunnerOutput, IntermediateTensors]:
1020 # Update KVConnector with the KVConnector metadata forward().
1021 if has_kv_transfer_group():
1022 get_kv_transfer_group().bind_connector_metadata(
vllm/v1/worker/gpu_worker.py
+18 -2
@@ -15,11 +15,12 @@ from vllm.distributed import (ensure_model_parallel_initialized,
15 init_distributed_environment,
16 set_custom_all_reduce)
17 from vllm.distributed.kv_transfer import ensure_kv_transfer_initialized
18 -from vllm.distributed.parallel_state import get_pp_group
18 +from vllm.distributed.parallel_state import get_pp_group, get_tp_group
19 from vllm.logger import init_logger
20 from vllm.lora.request import LoRARequest
21 from vllm.model_executor import set_random_seed
22 from vllm.platforms import current_platform
23 +from vllm.sequence import IntermediateTensors
24 from vllm.utils import GiB_bytes
25 from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
26 from vllm.v1.outputs import ModelRunnerOutput
@@ -266,7 +267,22 @@ class Worker(WorkerBase):
267 self,
268 scheduler_output: "SchedulerOutput",
269 ) -> Optional[ModelRunnerOutput]:
269 - output = self.model_runner.execute_model(scheduler_output)
270 + intermediate_tensors = None
271 + if not get_pp_group().is_first_rank:
272 + intermediate_tensors = IntermediateTensors(
273 + get_pp_group().recv_tensor_dict(
274 + all_gather_group=get_tp_group()))
275 +
276 + output = self.model_runner.execute_model(scheduler_output,
277 + intermediate_tensors)
278 +
279 + if not get_pp_group().is_last_rank:
280 + assert isinstance(output, IntermediateTensors)
281 + get_pp_group().send_tensor_dict(output.tensors,
282 + all_gather_group=get_tp_group())
283 + return None
284 +
285 + assert isinstance(output, ModelRunnerOutput)
286 return output if self.is_driver_worker else None
287
288 def profile(self, is_start: bool = True):