Skip to content
Merged
108 changes: 108 additions & 0 deletions tests/v1/kv_connector/unit/test_output_aggreagator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the tests and bug fix from #21048

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's now been merged to main so can rebase.

# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from concurrent.futures import Future
from typing import Optional

from vllm.distributed.kv_transfer.kv_connector.utils import KVOutputAggregator
from vllm.v1.outputs import ModelRunnerOutput


class DummyModelRunnerOutput(ModelRunnerOutput):

def __init__(self,
finished_sending: Optional[set[str]] = None,
finished_recving: Optional[set[str]] = None):
self.finished_sending = finished_sending
self.finished_recving = finished_recving


def test_aggregate_workers_output():
aggregator = KVOutputAggregator(world_size=2)

output1 = DummyModelRunnerOutput(finished_sending={'req1'},
finished_recving={'req2'})
output2 = DummyModelRunnerOutput(finished_sending=None,
finished_recving=None)

aggregated = aggregator.aggregate([output1, output2])

assert aggregated is output1
assert aggregated.finished_sending is None
assert aggregated.finished_recving is None

output1 = DummyModelRunnerOutput(finished_sending=None,
finished_recving=None)
output2 = DummyModelRunnerOutput(finished_sending={'req1'},
finished_recving=None)

aggregated = aggregator.aggregate([output1, output2])

assert aggregated is output1
assert aggregated.finished_sending == {'req1'}
assert aggregated.finished_recving is None

output1 = DummyModelRunnerOutput(finished_sending=None,
finished_recving=None)
output2 = DummyModelRunnerOutput(finished_sending={'req1'},
finished_recving={'req2'})

aggregated = aggregator.aggregate([output1, output2])

assert aggregated is output1
assert aggregated.finished_sending is None
assert aggregated.finished_recving == {'req2'}


def test_async_aggregate_workers_output():
aggregator = KVOutputAggregator(world_size=2)

future1: Future[DummyModelRunnerOutput] = Future()
future2: Future[DummyModelRunnerOutput] = Future()
result_future = aggregator.async_aggregate([future1, future2])

output1 = DummyModelRunnerOutput(finished_sending={'req1'},
finished_recving={'req2'})
output2 = DummyModelRunnerOutput(finished_sending=None,
finished_recving=None)
future1.set_result(output1)
future2.set_result(output2)

assert result_future.done()
aggregated = result_future.result()
assert aggregated is output1
assert aggregated.finished_sending is None
assert aggregated.finished_recving is None

future1 = Future()
future2 = Future()
result_future = aggregator.async_aggregate([future1, future2])

output1 = DummyModelRunnerOutput(finished_sending=None,
finished_recving=None)
output2 = DummyModelRunnerOutput(finished_sending={'req1'},
finished_recving=None)
future1.set_result(output1)
future2.set_result(output2)

assert result_future.done()
aggregated = result_future.result()
assert aggregated is output1
assert aggregated.finished_sending == {'req1'}
assert aggregated.finished_recving is None

future1 = Future()
future2 = Future()
result_future = aggregator.async_aggregate([future1, future2])

output1 = DummyModelRunnerOutput(finished_sending=None,
finished_recving=None)
output2 = DummyModelRunnerOutput(finished_sending={'req1'},
finished_recving={'req2'})
future1.set_result(output1)
future2.set_result(output2)

assert result_future.done()
aggregated = result_future.result()
assert aggregated is output1
assert aggregated.finished_sending is None
assert aggregated.finished_recving == {'req2'}
87 changes: 87 additions & 0 deletions vllm/distributed/kv_transfer/kv_connector/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
"""
KV cache helper for store.
"""
from collections import defaultdict
from collections.abc import Sequence
from concurrent.futures import CancelledError, Future
from typing import Optional, cast

import torch

import vllm.envs as envs
from vllm import _custom_ops as ops
from vllm.config import VllmConfig, get_current_vllm_config
from vllm.logger import init_logger
from vllm.v1.outputs import ModelRunnerOutput

logger = init_logger(__name__)

Expand Down Expand Up @@ -107,3 +113,84 @@ def get_kv_connector_cache_layout():
"layout to HND for better xfer performance.")
return "HND"
return "NHD"


class KVOutputAggregator:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility class LGTM

"""Utility class to aggregate the output of all workers into a single
output corresponding to Rank 0 for scheduler."""

def __init__(self, world_size: int):
self.world_size = world_size
Comment thread
njhill marked this conversation as resolved.
Outdated
# Complete transfer tracker. Used by to track finished requests
# [req_id -> n_finished_workers]
self._recv_remaining_count = defaultdict[str, int](lambda: world_size)
self._send_remaining_count = defaultdict[str, int](lambda: world_size)

def aggregate(self,
outputs: list[ModelRunnerOutput],
output_rank: int = 0) -> ModelRunnerOutput:
# aggregate finished_sending, finished_recving from all workers

def update_finished_set(req_ids: Optional[set[str]],
Comment thread
kouroshHakha marked this conversation as resolved.
remaining_count_dict: dict[str, int],
finished_set: set[str]) -> None:
for req_id in req_ids or ():
new_count = remaining_count_dict[req_id] - 1
if new_count == 0:
finished_set.add(req_id)
del remaining_count_dict[req_id]
else:
remaining_count_dict[req_id] = new_count

finished_sending = set[str]()
finished_recving = set[str]()
for output in outputs:
update_finished_set(output.finished_sending,
self._send_remaining_count, finished_sending)
update_finished_set(output.finished_recving,
self._recv_remaining_count, finished_recving)

# select output of the worker specified by output_rank
output = outputs[output_rank]

# set the aggregated finished_sending / finished_recving
output.finished_sending = finished_sending if finished_sending else None
output.finished_recving = finished_recving if finished_recving else None

return output

def async_aggregate(self,
output_futures: Sequence[Future[ModelRunnerOutput]],
output_rank: int = 0) -> 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)

# this check assumes io_thread_pool uses a single thread
if all(outputs):
result_future.set_result(
self.aggregate(cast(list[ModelRunnerOutput], outputs),
output_rank))

return callback

for i, output_future in enumerate(output_futures):
output_future.add_done_callback(make_callback(i))

return result_future
2 changes: 1 addition & 1 deletion vllm/distributed/kv_transfer/kv_connector/v1/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def get_finished(
"""
Notifies worker-side connector ids of requests that have
finished generating tokens on the worker.
The scheduler process (via the MultiprocExecutor) will use this output
The scheduler process (via the Executors) will use this output
to track which workers are done.

Returns:
Expand Down
88 changes: 7 additions & 81 deletions vllm/v1/executor/multiproc_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import time
import traceback
import weakref
from collections import defaultdict
from concurrent.futures import CancelledError, Future, ThreadPoolExecutor
from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass
from enum import Enum, auto
from functools import partial
Expand All @@ -27,6 +26,7 @@
destroy_model_parallel)
from vllm.distributed.device_communicators.shm_broadcast import (Handle,
MessageQueue)
from vllm.distributed.kv_transfer.kv_connector.utils import KVOutputAggregator
from vllm.executor.multiproc_worker_utils import (
_add_prefix, set_multiprocessing_worker_envs)
from vllm.logger import init_logger
Expand Down Expand Up @@ -118,13 +118,8 @@ def _init_executor(self) -> None:

self.output_rank = self._get_output_rank()
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.world_size)
self._send_remaining_count = defaultdict[str,
int](lambda: self.world_size)
self.kv_output_aggregator = KVOutputAggregator(
self.parallel_config.world_size)

def start_worker_monitor(self):
workers = self.workers
Expand Down Expand Up @@ -186,8 +181,9 @@ def execute_model(

# aggregate all workers output to a single output
if non_block:
return self._async_aggregate_workers_output(outputs)
return self._aggregate_workers_output(outputs)
return self.kv_output_aggregator.async_aggregate(
outputs, self.output_rank)
return self.kv_output_aggregator.aggregate(outputs, self.output_rank)

def collective_rpc(self,
method: Union[str, Callable],
Expand Down Expand Up @@ -246,76 +242,6 @@ def get_response(w: WorkerProcHandle,
except TimeoutError as e:
raise TimeoutError(f"RPC call to {method} timed out.") from e

def _aggregate_workers_output(
self, outputs: list[ModelRunnerOutput]) -> ModelRunnerOutput:
# aggregate finished_sending, finished_recving from all workers

def update_finished_set(req_ids: Optional[set[str]],
remaining_count_dict: dict[str, int],
finished_set: set[str]) -> None:
for req_id in req_ids or ():
new_count = remaining_count_dict[req_id] - 1
if new_count == 0:
finished_set.add(req_id)
del remaining_count_dict[req_id]
else:
remaining_count_dict[req_id] = new_count

finished_sending = set[str]()
finished_recving = set[str]()
for output in outputs:
update_finished_set(output.finished_sending,
self._send_remaining_count, finished_sending)
update_finished_set(output.finished_recving,
self._recv_remaining_count, finished_recving)

# select output of the worker specified by output_rank
output = outputs[self.output_rank]

# set the aggregated finished_sending / finished_recving
if finished_sending:
output.finished_sending = finished_sending
if finished_recving:
output.finished_recving = finished_recving

return output

def _async_aggregate_workers_output(
self, output_futures: list[Future[ModelRunnerOutput]]
) -> (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)

# this check assumes io_thread_pool uses a single thread
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

@staticmethod
def _ensure_worker_termination(worker_procs: list[BaseProcess]):
"""Ensure that all worker processes are terminated. Assumes workers have
Expand Down
Loading