-
-
Notifications
You must be signed in to change notification settings - Fork 19.6k
[BugFix] Make PD work with Ray #21072
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
951096e
b629b86
c0f9c92
80d861e
1c63f8e
913cd52
9d4c583
ac43f24
2013ef6
7e4bf72
c6c48c5
5e97ce6
f04be9f
23409ae
c70c5c1
ee04a92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,17 @@ | ||||||||||||||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||||||||||||||||||
|
|
||||||||||||||||||
| from concurrent.futures import Future | ||||||||||||||||||
| from typing import Union | ||||||||||||||||||
| from collections import defaultdict | ||||||||||||||||||
| from concurrent.futures import CancelledError, Future | ||||||||||||||||||
| from typing import Optional, Sequence, Union, cast | ||||||||||||||||||
|
|
||||||||||||||||||
| from vllm.executor.ray_distributed_executor import ( # noqa | ||||||||||||||||||
| RayDistributedExecutor as RayDistributedExecutorV0) | ||||||||||||||||||
| from vllm.v1.executor.abstract import Executor | ||||||||||||||||||
| from vllm.v1.outputs import ModelRunnerOutput | ||||||||||||||||||
| from vllm.logger import init_logger, logger | ||||||||||||||||||
|
|
||||||||||||||||||
| init_logger("vllm") | ||||||||||||||||||
|
|
||||||||||||||||||
| class FutureWrapper(Future): | ||||||||||||||||||
| """A wrapper around a Ray output reference to meet the interface | ||||||||||||||||||
|
|
@@ -28,6 +31,19 @@ def result(self, timeout=None): | |||||||||||||||||
| class RayDistributedExecutor(RayDistributedExecutorV0, Executor): | ||||||||||||||||||
| """Ray distributed executor using Ray Compiled Graphs.""" | ||||||||||||||||||
|
|
||||||||||||||||||
| def _init_executor(self) -> None: | ||||||||||||||||||
| super()._init_executor() | ||||||||||||||||||
|
|
||||||||||||||||||
| # KV connector setup | ||||||||||||||||||
| self.has_connector = self.vllm_config.kv_transfer_config is not None | ||||||||||||||||||
|
|
||||||||||||||||||
| # Complete transfer tracker. Used by to track finished requests | ||||||||||||||||||
| # [req_id -> n_finished_workers] | ||||||||||||||||||
| self._recv_remaining_count = defaultdict[str, | ||||||||||||||||||
| int](lambda: self.parallel_config.world_size) | ||||||||||||||||||
| self._send_remaining_count = defaultdict[str, | ||||||||||||||||||
| int](lambda: self.parallel_config.world_size) | ||||||||||||||||||
|
|
||||||||||||||||||
| @property | ||||||||||||||||||
| def max_concurrent_batches(self) -> int: | ||||||||||||||||||
| """Ray distributed executor supports pipeline parallelism, | ||||||||||||||||||
|
|
@@ -37,6 +53,80 @@ def max_concurrent_batches(self) -> int: | |||||||||||||||||
| return 2 | ||||||||||||||||||
| return self.parallel_config.pipeline_parallel_size | ||||||||||||||||||
|
|
||||||||||||||||||
| def _aggregate_workers_output( | ||||||||||||||||||
| self, outputs: list[ModelRunnerOutput]) -> ModelRunnerOutput: | ||||||||||||||||||
| # aggregate finished_sending, finished_recving from all workers | ||||||||||||||||||
|
|
||||||||||||||||||
| finished_sending = set[str]() | ||||||||||||||||||
| finished_recving = set[str]() | ||||||||||||||||||
| for output in outputs: | ||||||||||||||||||
| # update finished_sending | ||||||||||||||||||
| for req_id in output.finished_sending or []: | ||||||||||||||||||
| new_count = self._send_remaining_count[req_id] - 1 | ||||||||||||||||||
| if new_count == 0: | ||||||||||||||||||
| # got response from all workers, report back to scheduler | ||||||||||||||||||
| finished_sending.add(req_id) | ||||||||||||||||||
| del self._send_remaining_count[req_id] | ||||||||||||||||||
| else: | ||||||||||||||||||
| self._send_remaining_count[req_id] = new_count | ||||||||||||||||||
|
|
||||||||||||||||||
| # update finished_recving | ||||||||||||||||||
| for req_id in output.finished_recving or []: | ||||||||||||||||||
| new_count = self._recv_remaining_count[req_id] - 1 | ||||||||||||||||||
| if new_count == 0: | ||||||||||||||||||
| # got response from all workers, report back to scheduler | ||||||||||||||||||
| finished_recving.add(req_id) | ||||||||||||||||||
| del self._recv_remaining_count[req_id] | ||||||||||||||||||
| else: | ||||||||||||||||||
| self._recv_remaining_count[req_id] = new_count | ||||||||||||||||||
|
|
||||||||||||||||||
| # select output of the worker specified by output_rank | ||||||||||||||||||
| output = outputs[0] | ||||||||||||||||||
|
|
||||||||||||||||||
| # set the aggregated finished_sending / finished_recving | ||||||||||||||||||
| if finished_sending: | ||||||||||||||||||
| output.finished_sending = finished_sending | ||||||||||||||||||
| if finished_recving: | ||||||||||||||||||
| output.finished_recving = finished_recving | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
See also #21048 which has a fix for this that we should get in. |
||||||||||||||||||
|
|
||||||||||||||||||
| return output | ||||||||||||||||||
|
|
||||||||||||||||||
| def _async_aggregate_workers_output( | ||||||||||||||||||
| self, output_futures: Sequence[Union[Future[ModelRunnerOutput], FutureWrapper]] | ||||||||||||||||||
| ) -> Future[ModelRunnerOutput]: | ||||||||||||||||||
| """Takes a list of futures and returns a single future which resolves | ||||||||||||||||||
| to the respective list of outputs.""" | ||||||||||||||||||
| result_future: Future[ModelRunnerOutput] = Future() | ||||||||||||||||||
|
|
||||||||||||||||||
| outputs: list[Optional[ModelRunnerOutput]] = [None | ||||||||||||||||||
| ] * len(output_futures) | ||||||||||||||||||
|
|
||||||||||||||||||
| def make_callback(idx): | ||||||||||||||||||
|
|
||||||||||||||||||
| def callback(fut): | ||||||||||||||||||
| if result_future.done(): | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| try: | ||||||||||||||||||
| outputs[idx] = fut.result() | ||||||||||||||||||
| except CancelledError: | ||||||||||||||||||
| result_future.cancel() | ||||||||||||||||||
| except Exception as e: | ||||||||||||||||||
| result_future.set_exception(e) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Check if all outputs are ready | ||||||||||||||||||
| if all(outputs): | ||||||||||||||||||
| result_future.set_result( | ||||||||||||||||||
| self._aggregate_workers_output( | ||||||||||||||||||
| cast(list[ModelRunnerOutput], outputs))) | ||||||||||||||||||
|
|
||||||||||||||||||
| return callback | ||||||||||||||||||
|
|
||||||||||||||||||
| for i, output_future in enumerate(output_futures): | ||||||||||||||||||
| output_future.add_done_callback(make_callback(i)) | ||||||||||||||||||
|
|
||||||||||||||||||
| return result_future | ||||||||||||||||||
|
|
||||||||||||||||||
| def execute_model( | ||||||||||||||||||
| self, | ||||||||||||||||||
| scheduler_output, | ||||||||||||||||||
|
|
@@ -55,10 +145,22 @@ def execute_model( | |||||||||||||||||
|
|
||||||||||||||||||
| refs = self.forward_dag.execute(scheduler_output) # type: ignore | ||||||||||||||||||
|
|
||||||||||||||||||
| # When PP is not used, we block here until the result is available. | ||||||||||||||||||
| if self.max_concurrent_batches == 1: | ||||||||||||||||||
| return refs[0].get() | ||||||||||||||||||
| if not self.has_connector: | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the changes below here in this file should be all that's needed in this PR (apart from moving those other two functions so that they can be shared). |
||||||||||||||||||
| # get output only from a single worker (output_rank) | ||||||||||||||||||
| # When PP is not used, we block here until the result is available. | ||||||||||||||||||
| if self.max_concurrent_batches == 1: | ||||||||||||||||||
| return refs[0].get() | ||||||||||||||||||
|
|
||||||||||||||||||
| # When PP is used, we return a FutureWrapper immediately so that | ||||||||||||||||||
| # the scheduler can yield to the next batch. | ||||||||||||||||||
| return FutureWrapper(refs[0]) | ||||||||||||||||||
| # When PP is used, we return a FutureWrapper immediately so that | ||||||||||||||||||
| # the scheduler can yield to the next batch. | ||||||||||||||||||
| return FutureWrapper(refs[0]) | ||||||||||||||||||
|
|
||||||||||||||||||
| # get output from all workers when connector is present | ||||||||||||||||||
| if self.max_concurrent_batches == 1: | ||||||||||||||||||
| # Block and get results from all workers | ||||||||||||||||||
| outputs = [ref.get() for ref in refs] | ||||||||||||||||||
| return self._aggregate_workers_output(outputs) | ||||||||||||||||||
| else: | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redundant |
||||||||||||||||||
| # Return a future that will aggregate outputs from all workers | ||||||||||||||||||
| output_futures = [FutureWrapper(ref) for ref in refs] | ||||||||||||||||||
| return self._async_aggregate_workers_output(output_futures) | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
|
|
||
| import copy | ||
| import gc | ||
| import time | ||
| import weakref | ||
|
|
@@ -1233,6 +1234,8 @@ def _pool( | |
| hidden_states: torch.Tensor, | ||
| num_scheduled_tokens: int, | ||
| num_scheduled_tokens_np: np.ndarray, | ||
| finished_sending: Optional[set[str]], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are all of these changes to
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so ray distributed executor runs the runner directly (not gpu_worker), it's better for the model_runner to own the logic of filling in the finished_sending and finished_recving fields in the output. So I basically reverted the changes to gpu_model_runner introduced in #19555. In that PR, for some reason that I don't understand, the logic is taken out of the model_runner and is put into the worker.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change was made to support PP.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@kouroshHakha could you point to where this is the case? From a quick look it appears that it's still a bit entangled with V0 logic but ultimately uses a
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So here is the flow with Ray as the distributed backend in v1: v1.RayDistributedExecutor.execute_model() --> RayWrapper.execute_model_ray() (or RayWrapper.execute_model_spmd()) --> worker.model_runner.execute_model() Therefore the worker.execute_model() logic is skipped and it directly interacts with model_runner.execute_model().
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. code for execute_model_ray: https://github.com/vllm-project/vllm/blob/main/vllm/executor/ray_utils.py#L135
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @orozery Is there a test / script that I should target to make sure the intended pp logic still works with my changes?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, there should be no regression for test_pipeline_parallel.py , not sure if there is a test for PP + P/D
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @orozery I see what you are saying. Basically for PP, each pp rank (except the last one) will forward an empty output back to the scheduler. You want those empty outputs to have the correct finished/recved attributes. I added that logic back but leaving the main model_runner logic intact. Let's get this merged asap so that it can catch the 0.10.0 train since it's a massive regression in the ray behavior. We can follow up with better solutions after.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Based on my research there is none. I also couldn't get the master run PP + P/D so not sure if at any point that combo was supported and now it's regressed? Or was it not supported at all. |
||
| finished_recving: Optional[set[str]], | ||
| ) -> ModelRunnerOutput: | ||
| assert self.input_batch.num_reqs ==\ | ||
| len(self.input_batch.pooling_params), \ | ||
|
|
@@ -1267,6 +1270,8 @@ def _pool( | |
| logprobs=None, | ||
| prompt_logprobs_dict={}, | ||
| pooler_output=pooler_output, | ||
| finished_sending=finished_sending, | ||
| finished_recving=finished_recving, | ||
| ) | ||
|
|
||
| @torch.inference_mode() | ||
|
|
@@ -1277,12 +1282,12 @@ def execute_model( | |
| ) -> Union[ModelRunnerOutput, IntermediateTensors]: | ||
| self._update_states(scheduler_output) | ||
| if not scheduler_output.total_num_scheduled_tokens: | ||
| if has_kv_transfer_group(): | ||
| with set_forward_context(None, self.vllm_config): | ||
| self.maybe_setup_kv_connector(scheduler_output) | ||
| if not has_kv_transfer_group(): | ||
| # Return empty ModelRunnerOutput if there's no work to do. | ||
| return EMPTY_MODEL_RUNNER_OUTPUT | ||
|
|
||
| return self.kv_connector_no_forward(scheduler_output) | ||
|
|
||
| # Return empty ModelRunnerOutput if there's no work to do. | ||
| return EMPTY_MODEL_RUNNER_OUTPUT | ||
|
|
||
| # Prepare the decoder inputs. | ||
| (attn_metadata, attention_cuda_graphs, logits_indices, | ||
|
|
@@ -1375,6 +1380,8 @@ def execute_model( | |
| ) | ||
|
|
||
| self.maybe_wait_for_kv_save() | ||
| finished_sending, finished_recving = ( | ||
| self.get_finished_kv_transfers(scheduler_output)) | ||
|
|
||
| if self.use_aux_hidden_state_outputs: | ||
| hidden_states, aux_hidden_states = model_output | ||
|
|
@@ -1400,7 +1407,7 @@ def execute_model( | |
| else: | ||
| if self.input_batch.pooling_params: | ||
| return self._pool(hidden_states, num_scheduled_tokens, | ||
| num_scheduled_tokens_np) | ||
| num_scheduled_tokens_np, finished_sending, finished_recving) | ||
|
|
||
| sample_hidden_states = hidden_states[logits_indices] | ||
| logits = self.model.compute_logits(sample_hidden_states, None) | ||
|
|
@@ -1694,6 +1701,31 @@ def maybe_wait_for_kv_save() -> None: | |
| if has_kv_transfer_group(): | ||
| get_kv_transfer_group().wait_for_save() | ||
|
|
||
| @staticmethod | ||
| def get_finished_kv_transfers( | ||
| scheduler_output: "SchedulerOutput", | ||
| ) -> tuple[Optional[set[str]], Optional[set[str]]]: | ||
| if has_kv_transfer_group(): | ||
| return get_kv_transfer_group().get_finished( | ||
| scheduler_output.finished_req_ids) | ||
| return None, None | ||
|
|
||
| def kv_connector_no_forward( | ||
| self, scheduler_output: "SchedulerOutput") -> ModelRunnerOutput: | ||
| # KV send/recv even if no work to do. | ||
| with set_forward_context(None, self.vllm_config): | ||
| self.maybe_setup_kv_connector(scheduler_output) | ||
| finished_sending, finished_recving = ( | ||
| self.get_finished_kv_transfers(scheduler_output)) | ||
|
|
||
| if not finished_sending and not finished_recving: | ||
| return EMPTY_MODEL_RUNNER_OUTPUT | ||
|
|
||
| output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT) | ||
| output.finished_sending = finished_sending | ||
| output.finished_recving = finished_recving | ||
| return output | ||
|
|
||
| def propose_ngram_draft_token_ids( | ||
| self, | ||
| sampled_token_ids: list[list[int]], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -332,22 +332,6 @@ def execute_model( | |
| output = EMPTY_MODEL_RUNNER_OUTPUT | ||
|
|
||
| assert isinstance(output, ModelRunnerOutput) | ||
| if has_kv_transfer_group(): | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The changes in |
||
| finished_sending, finished_recving = ( | ||
| get_kv_transfer_group().get_finished( | ||
| scheduler_output.finished_req_ids)) | ||
| if finished_sending or finished_recving: | ||
| if output is EMPTY_MODEL_RUNNER_OUTPUT: | ||
| output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT) | ||
| output.finished_sending = finished_sending | ||
| output.finished_recving = finished_recving | ||
|
|
||
| # Clear KVConnector state for this step. | ||
| get_kv_transfer_group().clear_connector_metadata() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was |
||
|
|
||
| # with a connector, the scheduler expects output from all workers | ||
| return output | ||
|
|
||
| # return output only from the driver worker | ||
| return output if self.is_driver_worker else None | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move these to utility functions (shared with multiproc executor), or maybe a mixin superclass also containing the
_send_remaining_countand_recv_remaining_countfields?