[Core][KV Connector] fix scheduler KV connector stats aggregation (#43877)

Fixes scheduler-side KV connector stats collection so that:

1. update_connector_output() runs before scheduler-side stats are collected.
2. worker-side and scheduler-side KV connector stats are aggregated when both are present.
3. scheduler-only KV connector stats are still emitted when no worker-side stats exist.

Signed-off-by: srinivas_oo7 <sklinkedin0120@gmail.com>
Co-authored-by: srinivas_oo7 <sklinkedin0120@gmail.com>
This commit is contained in:
Srinivas Krovvidi
2026-06-12 14:51:55 +00:00
committed by GitHub
co-authored by srinivas_oo7
parent c7aa3d2630
commit 9ff278b1d2
3 changed files with 102 additions and 8 deletions
+82
View File
@@ -15,6 +15,7 @@ from vllm.config import (
SpeculativeConfig,
VllmConfig,
)
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStats
from vllm.multimodal.inputs import (
MultiModalFeatureSpec,
MultiModalKwargsItem,
@@ -3990,6 +3991,87 @@ def test_delayed_kv_connector_free_keeps_scheduler_active():
assert not scheduler.has_finished_requests()
def test_scheduler_kv_connector_stats():
"""Test worker-side, scheduler-side, and combined KV connector stats."""
class GenericKVConnectorStats(KVConnectorStats):
def reset(self):
self.data = {}
def aggregate(self, other: KVConnectorStats) -> KVConnectorStats:
self.data.update(other.data)
return self
def reduce(self) -> dict[str, int | float]:
return {}
def is_empty(self) -> bool:
return not self.data
test_cases = (
({"worker": 1}, None, {"worker": 1}),
(None, {"scheduler": 2}, {"scheduler": 2}),
({"worker": 1}, {"scheduler": 2}, {"worker": 1, "scheduler": 2}),
)
for worker_data, scheduler_data, expected_data in test_cases:
scheduler = create_scheduler()
worker_stats = (
GenericKVConnectorStats(data=worker_data) if worker_data else None
)
scheduler_stats = (
GenericKVConnectorStats(data=scheduler_data) if scheduler_data else None
)
scheduler.connector = Mock()
scheduler.connector.get_kv_connector_stats.return_value = (
scheduler_stats if worker_stats is None else None
)
scheduler.connector.take_events.return_value = []
def update_connector_output(
kv_connector_output: KVConnectorOutput,
scheduler=scheduler,
scheduler_stats=scheduler_stats,
):
scheduler.connector.get_kv_connector_stats.return_value = scheduler_stats
scheduler.connector.update_connector_output.side_effect = (
update_connector_output
)
model_output = ModelRunnerOutput(
req_ids=["req_0"],
req_id_to_index={"req_0": 0},
sampled_token_ids=[[123]],
logprobs=None,
prompt_logprobs_dict={},
pooler_output=[None],
kv_connector_output=KVConnectorOutput(kv_connector_stats=worker_stats)
if worker_stats
else None,
)
scheduler_output = SchedulerOutput(
scheduled_new_reqs=[],
scheduled_cached_reqs=None,
num_scheduled_tokens={"req_0": 1},
total_num_scheduled_tokens=1,
scheduled_spec_decode_tokens={},
scheduled_encoder_inputs={},
num_common_prefix_blocks=[0],
finished_req_ids=set(),
free_encoder_mm_hashes=[],
)
engine_core_outputs = scheduler.update_from_output(
scheduler_output, model_output
)
final_stats = next(
iter(engine_core_outputs.values())
).scheduler_stats.kv_connector_stats
assert final_stats == expected_data
# ==============================================================================
# Variable-length encoder cross-attention block allocation tests
# ==============================================================================
@@ -58,6 +58,7 @@ class MockConnector(KVConnectorBase_V1):
mock = MagicMock(spec_set=KVConnectorBase_V1)
# Override just build_kv_connector_stats
mock.build_kv_connector_stats = cls.build_kv_connector_stats
mock.get_kv_connector_stats.return_value = None
return mock
@classmethod
@@ -93,6 +94,7 @@ class MockHMAConnector(KVConnectorBase_V1, SupportsHMA):
def __new__(cls, *args, **kwargs):
mock = MagicMock(spec_set=cls)
mock.get_kv_connector_stats.return_value = None
return mock
def start_load_kv(self, forward_context, **kwargs):
@@ -368,7 +370,7 @@ def test_multi_example_connector_consistency():
def _ignore_event_collection(events: list[str]) -> list[str]:
# Filter out per-step polling hooks that the scheduler calls repeatedly
# and which are not meaningful state transitions for these assertions.
ignored = {"take_events", "has_pending_push_work"}
ignored = {"get_kv_connector_stats", "has_pending_push_work", "take_events"}
return [event for event in events if event not in ignored]
+17 -7
View File
@@ -1412,13 +1412,6 @@ class Scheduler(SchedulerInterface):
outputs: dict[int, list[EngineCoreOutput]] = defaultdict(list)
spec_decoding_stats: SpecDecodingStats | None = None
kv_connector_stats: KVConnectorStats | None = (
kv_connector_output.kv_connector_stats if kv_connector_output else None
)
if kv_connector_stats and self.connector:
kv_stats = self.connector.get_kv_connector_stats()
if kv_stats:
kv_connector_stats = kv_connector_stats.aggregate(kv_stats)
failed_kv_load_req_ids = None
if kv_connector_output and kv_connector_output.invalid_block_ids:
@@ -1665,6 +1658,23 @@ class Scheduler(SchedulerInterface):
if kv_connector_output:
self._update_from_kv_xfer_finished(kv_connector_output)
# Worker-side KV connector stats from the model runner output.
kv_connector_stats: KVConnectorStats | None = (
kv_connector_output.kv_connector_stats if kv_connector_output else None
)
if self.connector:
# Scheduler-side KV connector stats collected after connector update.
scheduler_kv_connector_stats = self.connector.get_kv_connector_stats()
if (
scheduler_kv_connector_stats is not None
and not scheduler_kv_connector_stats.is_empty()
):
kv_connector_stats = (
kv_connector_stats.aggregate(scheduler_kv_connector_stats)
if kv_connector_stats is not None
else scheduler_kv_connector_stats
)
# collect KV cache events from KV cache manager
events = self.kv_cache_manager.take_events()