Skip to content

Commit af4046f

Browse files
committed
[Feat] Add HMA connector metrics
1 parent 84f396a commit af4046f

2 files changed

Lines changed: 416 additions & 42 deletions

File tree

test/test_ucm_connector_metrics.py

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,26 @@ def get_config(self):
275275

276276

277277
def _install_stubs():
278+
_install_module(
279+
"numpy",
280+
ndarray=object,
281+
bool_=bool,
282+
uint64="uint64",
283+
int64="int64",
284+
zeros=lambda shape, *args, **kwargs: [
285+
[0 for _ in range(shape[1])] for _ in range(shape[0])
286+
],
287+
vstack=lambda rows: list(rows),
288+
concatenate=lambda arrays, axis=0: [
289+
item for array in arrays for item in list(array)
290+
],
291+
asarray=lambda values, dtype=None: values,
292+
arange=lambda *args, **kwargs: list(range(*args)),
293+
isscalar=lambda value: isinstance(value, (int, float, bool, str, bytes)),
294+
)
278295
_install_package("ucm", REPO_ROOT / "ucm")
279296
_install_package("ucm.integration", REPO_ROOT / "ucm" / "integration")
280297
_install_package("ucm.integration.vllm", REPO_ROOT / "ucm" / "integration" / "vllm")
281-
_install_module("numpy", ndarray=type("ndarray", (), {}))
282298
_install_module("torch", Tensor=type("Tensor", (), {}))
283299
_install_module(
284300
"prometheus_client",
@@ -354,7 +370,13 @@ def _install_stubs():
354370

355371
_install_stubs()
356372

373+
import ucm.integration.vllm.hma_connector as hma_connector_module
357374
import ucm.integration.vllm.ucm_connector as ucm_connector_module
375+
from ucm.integration.vllm.hma_connector import (
376+
FAWARequestDispatchMeta,
377+
UCMFAWAConnector,
378+
UCMFAWAConnectorMetadata,
379+
)
358380
from ucm.integration.vllm.metrics import UCMConnectorStats, UCMPromMetrics
359381
from ucm.integration.vllm.ucm_connector import (
360382
PendingDumpTask,
@@ -460,6 +482,38 @@ def _reset_fakes():
460482
dispatcher_module._DISPATCHER = None
461483

462484

485+
class FakeHMAStore:
486+
def __init__(self, label, fail_wait=False):
487+
self.label = label
488+
self.fail_wait = fail_wait
489+
self.loaded = []
490+
self.dumped = []
491+
self.waited = []
492+
493+
def load_data(self, keys, shard_indices, ptrs):
494+
task = f"{self.label}-load-{len(keys)}-{len(self.loaded)}"
495+
self.loaded.append((list(keys), list(shard_indices), ptrs, task))
496+
return task
497+
498+
def dump_data(self, keys, shard_indices, ptrs, event_handle):
499+
task = f"{self.label}-dump-{len(keys)}-{len(self.dumped)}"
500+
self.dumped.append((list(keys), list(shard_indices), ptrs, event_handle, task))
501+
return task
502+
503+
def wait(self, task):
504+
self.waited.append(task)
505+
if self.fail_wait:
506+
raise RuntimeError(f"{self.label} wait failed")
507+
508+
509+
class FakeHMADevice:
510+
def __init__(self):
511+
self.destroyed = []
512+
513+
def destroy_event_handle(self, event_handle):
514+
self.destroyed.append(event_handle)
515+
516+
463517
def test_config_definitions_register_enable_list_and_metric_names():
464518
_reset_fakes()
465519
config = _metrics_config()
@@ -969,6 +1023,212 @@ def destroy_event_handle(self, event_handle):
9691023
]
9701024

9711025

1026+
def _fawa_request(load_keys=None, dump_keys=None):
1027+
return FAWARequestDispatchMeta(
1028+
load_keys=list(load_keys or []),
1029+
load_hash_start=0,
1030+
load_hash_end=len(load_keys or []),
1031+
load_vllm_block_ids=([1, 2],),
1032+
dump_keys=list(dump_keys or []),
1033+
dump_hash_start=0,
1034+
dump_hash_end=len(dump_keys or []),
1035+
dump_vllm_block_ids=([1, 2],),
1036+
)
1037+
1038+
1039+
def _new_hma_connector(metadata, fa_store=None, wa_store=None):
1040+
connector = object.__new__(UCMFAWAConnector)
1041+
connector.fa_store = fa_store or FakeHMAStore("FA")
1042+
connector.wa_store = wa_store or FakeHMAStore("WA")
1043+
connector._fawa_store_block_sizes = {"FA": 100, "WA": 20}
1044+
connector._invalid_block_ids = set()
1045+
connector.tp_size = 1
1046+
connector.tp_rank = 0
1047+
connector.wa_dump_block_wise = True
1048+
connector.fa_group_ids = set()
1049+
connector.window_group_ids = set()
1050+
connector.group_metas = []
1051+
connector.tp_dump_tasks = {}
1052+
connector._fawa_dump_group_metrics = {}
1053+
connector._fawa_next_dump_group_id = 0
1054+
connector._direct_task_has_load = False
1055+
connector._direct_task_load_duration_ms = 0.0
1056+
connector._direct_task_compute_start_ms = None
1057+
connector.device = FakeHMADevice()
1058+
connector._get_connector_metadata = lambda: metadata
1059+
connector._extract_fa_ptr = lambda *args, **kwargs: [[0], [0]]
1060+
connector._extract_wa_ptr = lambda *args, **kwargs: [[0], [0]]
1061+
return connector
1062+
1063+
1064+
def test_hma_connector_records_load_metrics(monkeypatch):
1065+
_reset_fakes()
1066+
times = iter([1.0, 1.1, 1.11])
1067+
monkeypatch.setattr(
1068+
hma_connector_module.time,
1069+
"perf_counter",
1070+
lambda: next(times),
1071+
)
1072+
metadata = UCMFAWAConnectorMetadata(
1073+
request_meta={"req-1": _fawa_request(load_keys=[b"a", b"b"])}
1074+
)
1075+
connector = _new_hma_connector(metadata)
1076+
1077+
connector.start_load_kv(None)
1078+
1079+
stats = fake_ucmmetrics.updated[-1]
1080+
assert stats["load_requests_num"] == 1
1081+
assert stats["load_blocks_num"] == 3
1082+
assert stats["load_duration"] == pytest.approx(100.0)
1083+
assert stats["load_bytes_total"] == 220
1084+
assert stats["load_speed"] == pytest.approx(220 / 100 / 1024 / 1024)
1085+
assert connector._direct_task_has_load is True
1086+
assert connector._direct_task_load_duration_ms == pytest.approx(100.0)
1087+
assert connector._direct_task_compute_start_ms == pytest.approx(1110.0)
1088+
1089+
1090+
def test_hma_connector_load_metrics_ignore_wait_failure_for_counting(monkeypatch):
1091+
_reset_fakes()
1092+
times = iter([1.0, 1.1, 1.11])
1093+
monkeypatch.setattr(
1094+
hma_connector_module.time,
1095+
"perf_counter",
1096+
lambda: next(times),
1097+
)
1098+
metadata = UCMFAWAConnectorMetadata(
1099+
request_meta={"req-1": _fawa_request(load_keys=[b"a", b"b"])}
1100+
)
1101+
connector = _new_hma_connector(metadata, wa_store=FakeHMAStore("WA", True))
1102+
1103+
connector.start_load_kv(None)
1104+
1105+
stats = fake_ucmmetrics.updated[-1]
1106+
assert stats["load_requests_num"] == 1
1107+
assert stats["load_blocks_num"] == 3
1108+
assert stats["load_bytes_total"] == 220
1109+
assert stats["load_speed"] == pytest.approx(220 / 100 / 1024 / 1024)
1110+
assert connector._invalid_block_ids == {1, 2}
1111+
1112+
1113+
@pytest.mark.parametrize("drain_method", ["get_finished", "preemption"])
1114+
def test_hma_connector_records_grouped_save_completion_metrics(
1115+
monkeypatch, drain_method
1116+
):
1117+
_reset_fakes()
1118+
times = iter([1.0, 1.01, 1.02, 1.10, 1.12, 1.13, 1.15])
1119+
monkeypatch.setattr(
1120+
hma_connector_module.time,
1121+
"perf_counter",
1122+
lambda: next(times),
1123+
)
1124+
metadata = UCMFAWAConnectorMetadata(
1125+
request_meta={"req-1": _fawa_request(dump_keys=[b"a", b"b"])}
1126+
)
1127+
connector = _new_hma_connector(metadata)
1128+
handles = iter([11, 12])
1129+
connector._get_dump_event_handle = lambda: next(handles)
1130+
connector._direct_task_has_load = True
1131+
connector._direct_task_load_duration_ms = 10.0
1132+
connector._direct_task_compute_start_ms = 900.0
1133+
1134+
connector.wait_for_save()
1135+
1136+
assert fake_ucmmetrics.updated == [
1137+
{
1138+
"save_requests_num": 1,
1139+
"save_blocks_num": 4,
1140+
"save_bytes_total": 240,
1141+
}
1142+
]
1143+
if drain_method == "get_finished":
1144+
connector.get_finished({"req-1"})
1145+
else:
1146+
connector.handle_preemptions(
1147+
UCMFAWAConnectorMetadata(preempted_req_ids={"req-1"})
1148+
)
1149+
1150+
completion_stats = fake_ucmmetrics.updated[-2]
1151+
assert completion_stats["save_duration"] == pytest.approx(140.0)
1152+
assert completion_stats["save_speed"] == pytest.approx(240 / 140 / 1024 / 1024)
1153+
assert completion_stats["save_completion_wait_duration"] == pytest.approx(50.0)
1154+
assert completion_stats["save_completion_wait_speed"] == pytest.approx(
1155+
240 / 50 / 1024 / 1024
1156+
)
1157+
task_stats = fake_ucmmetrics.updated[-1]
1158+
assert task_stats["connector_task_load_stage_load_dump_ms"] == 10.0
1159+
assert task_stats["connector_task_compute_stage_load_dump_ms"] == pytest.approx(
1160+
100.0
1161+
)
1162+
assert task_stats["connector_task_dump_stage_load_dump_ms"] == pytest.approx(140.0)
1163+
assert task_stats["connector_task_total_load_dump_ms"] == pytest.approx(250.0)
1164+
assert connector.device.destroyed == [11, 12]
1165+
assert connector.tp_dump_tasks == {}
1166+
assert connector._fawa_dump_group_metrics == {}
1167+
1168+
1169+
def test_hma_connector_save_metrics_count_only_submitted_requests_without_changing_tp_keys(
1170+
monkeypatch,
1171+
):
1172+
_reset_fakes()
1173+
times = iter([1.0, 1.01, 1.02])
1174+
monkeypatch.setattr(
1175+
hma_connector_module.time,
1176+
"perf_counter",
1177+
lambda: next(times),
1178+
)
1179+
metadata = UCMFAWAConnectorMetadata(
1180+
request_meta={
1181+
"req-no-rank-work": _fawa_request(dump_keys=[b"a"]),
1182+
"req-rank-work": _fawa_request(dump_keys=[b"b", b"c"]),
1183+
}
1184+
)
1185+
connector = _new_hma_connector(metadata)
1186+
connector.tp_size = 2
1187+
connector.tp_rank = 0
1188+
handles = iter([11, 12])
1189+
connector._get_dump_event_handle = lambda: next(handles)
1190+
1191+
connector.wait_for_save()
1192+
1193+
assert list(connector.tp_dump_tasks) == [("req-no-rank-work", "req-rank-work")]
1194+
assert fake_ucmmetrics.updated == [
1195+
{
1196+
"save_requests_num": 1,
1197+
"save_blocks_num": 2,
1198+
"save_bytes_total": 120,
1199+
}
1200+
]
1201+
1202+
1203+
def test_hma_connector_no_dump_records_load_only_breakdown(monkeypatch):
1204+
_reset_fakes()
1205+
times = iter([1.0])
1206+
monkeypatch.setattr(
1207+
hma_connector_module.time,
1208+
"perf_counter",
1209+
lambda: next(times),
1210+
)
1211+
metadata = UCMFAWAConnectorMetadata(
1212+
request_meta={"req-1": _fawa_request(load_keys=[b"a"])}
1213+
)
1214+
connector = _new_hma_connector(metadata)
1215+
connector._direct_task_has_load = True
1216+
connector._direct_task_load_duration_ms = 7.0
1217+
connector._direct_task_compute_start_ms = 900.0
1218+
1219+
connector.wait_for_save()
1220+
1221+
task_stats = fake_ucmmetrics.updated[-1]
1222+
assert task_stats["connector_task_load_stage_load_only_ms"] == 7.0
1223+
assert task_stats["connector_task_compute_stage_load_only_ms"] == pytest.approx(
1224+
100.0
1225+
)
1226+
assert task_stats["connector_task_total_load_only_ms"] == pytest.approx(107.0)
1227+
assert connector._direct_task_has_load is False
1228+
assert connector._direct_task_load_duration_ms == 0.0
1229+
assert connector._direct_task_compute_start_ms is None
1230+
1231+
9721232
def test_multiproc_logger_uses_prefix_and_dispatcher_snapshot(tmp_path):
9731233
_reset_fakes()
9741234
import ucm.observability as observability

0 commit comments

Comments
 (0)