[KV Offloading] Add basic offloading metrics (#45958)

Signed-off-by: srinivas_oo7 <sklinkedin0120@gmail.com> Signed-off-by: Srinivas Krovvidi <194645829+Srinivasoo7@users.noreply.github.com> Co-authored-by: srinivas_oo7 <sklinkedin0120@gmail.com> Co-authored-by: Or Ozeri <or@ozery.com> Co-authored-by: Or Ozeri <oro@il.ibm.com>

Srinivas Krovvidi committed Jul 7, 2026 at 01:26 UTC e040899a006ffd07c51151b32d29649f30cb21e9
10 files changed +320 -16
tests/v1/kv_connector/unit/offloading_connector/test_metrics.py
+38
@@ -10,9 +10,11 @@ from prometheus_client import Counter, Gauge, Histogram
10 from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
11 OffloadingConnectorStats,
12 OffloadPromMetrics,
13 + _ConnectorMetricName,
14 _MetricType,
15 _StatsKey,
16 _TransferMetricName,
17 + get_connector_metric_definitions,
18 )
19 from vllm.distributed.kv_transfer.kv_connector.v1.offloading_connector import (
20 OffloadingConnector,
@@ -37,6 +39,42 @@ MY_COUNTER = "my_counter"
39 MY_LABEL = "my_label"
40
41
42 +def test_connector_metric_histogram_buckets():
43 + metadata = get_connector_metric_definitions()
44 +
45 + sync_delay = metadata[_ConnectorMetricName.LOOKUP_SYNC_DELAY]
46 + assert isinstance(sync_delay, OffloadingHistogramMetadata)
47 + assert sync_delay.buckets == (
48 + 0.00001,
49 + 0.00005,
50 + 0.0001,
51 + 0.0005,
52 + 0.001,
53 + 0.005,
54 + 0.01,
55 + 0.05,
56 + 0.1,
57 + 0.5,
58 + 1,
59 + )
60 +
61 + async_delay = metadata[_ConnectorMetricName.LOOKUP_ASYNC_DELAY]
62 + assert isinstance(async_delay, OffloadingHistogramMetadata)
63 + assert async_delay.buckets == (
64 + 0.0001,
65 + 0.0005,
66 + 0.001,
67 + 0.005,
68 + 0.01,
69 + 0.05,
70 + 0.1,
71 + 0.5,
72 + 1,
73 + 5,
74 + 10,
75 + )
76 +
77 +
78 class _FakeMetric:
79 def __init__(self, **kwargs):
80 self.kwargs = kwargs
tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py
+70
@@ -11,6 +11,10 @@ from tests.v1.kv_connector.unit.offloading_connector.utils import (
11 to_keys,
12 )
13 from tests.v1.kv_connector.unit.utils import EOS_TOKEN_ID
14 +from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
15 + OffloadingConnectorStats,
16 + _ConnectorMetricName,
17 +)
18 from vllm.distributed.kv_transfer.kv_connector.v1.offloading.scheduler import (
19 OffloadingConnectorScheduler,
20 RequestOffloadState,
@@ -32,6 +36,72 @@ from vllm.v1.kv_offload.base import (
36 from vllm.v1.request import RequestStatus
37
38
39 +def _reduce_kv_connector_stats(runner):
40 + reduced: dict[str, int | float] = {}
41 + for payload in runner.kv_connector_stats:
42 + stats = (
43 + payload
44 + if hasattr(payload, "reduce")
45 + else OffloadingConnectorStats(data=payload)
46 + )
47 + for key, value in stats.reduce().items():
48 + reduced[key] = reduced.get(key, 0) + value
49 + return reduced
50 +
51 +
52 +def test_scheduler_reports_allocation_failure(request_runner):
53 + runner = request_runner(
54 + block_size=4,
55 + num_gpu_blocks=10,
56 + async_scheduling=False,
57 + )
58 + runner.new_request(token_ids=[0] * 4)
59 + runner.manager.prepare_store.side_effect = lambda keys, req_context: None
60 +
61 + runner.run(decoded_tokens=[EOS_TOKEN_ID])
62 +
63 + reduced = _reduce_kv_connector_stats(runner)
64 + assert reduced[_ConnectorMetricName.ALLOCATION_FAILURE] == 1
65 +
66 +
67 +def test_scheduler_reports_lookup_sync_delay(request_runner):
68 + runner = request_runner(
69 + block_size=4,
70 + num_gpu_blocks=10,
71 + async_scheduling=False,
72 + )
73 + runner.new_request(token_ids=[1] * 4)
74 + runner.manager.prepare_store.side_effect = lambda keys, req_context: (
75 + generate_store_output([])
76 + )
77 +
78 + runner.run(decoded_tokens=[EOS_TOKEN_ID])
79 +
80 + reduced = _reduce_kv_connector_stats(runner)
81 + assert reduced[f"{_ConnectorMetricName.LOOKUP_SYNC_DELAY}_count"] == 1
82 + assert reduced[f"{_ConnectorMetricName.LOOKUP_SYNC_DELAY}_sum"] > 0
83 +
84 +
85 +def test_scheduler_reports_lookup_async_delay_on_resolve(request_runner):
86 + """A deferred lookup reports its async delay once it resolves."""
87 + runner = request_runner(
88 + block_size=4,
89 + num_gpu_blocks=10,
90 + async_scheduling=False,
91 + )
92 + runner.manager.lookup.side_effect = [LookupResult.RETRY, LookupResult.MISS]
93 + runner.manager.prepare_store.side_effect = lambda keys, req_context: (
94 + generate_store_output([])
95 + )
96 +
97 + runner.new_request(token_ids=[1] * 4)
98 + runner.run(decoded_tokens=[EOS_TOKEN_ID])
99 +
100 + reduced = _reduce_kv_connector_stats(runner)
101 + assert reduced[f"{_ConnectorMetricName.LOOKUP_ASYNC_DELAY}_count"] == 1
102 + assert reduced[f"{_ConnectorMetricName.LOOKUP_ASYNC_DELAY}_sum"] > 0
103 +
104 +
105 @pytest.mark.parametrize("async_scheduling", [True, False])
106 def test_offloading_connector(request_runner, async_scheduling: bool):
107 block_size = 4
tests/v1/kv_connector/unit/offloading_connector/utils.py
+16 -3
@@ -133,6 +133,7 @@ class MockOffloadingSpec(OffloadingSpec):
133 self.manager = MagicMock(spec=OffloadingManager)
134 self.manager.prepare_load = lambda keys, req_context: MockLoadStoreSpec(keys)
135 self.manager.lookup.return_value = LookupResult.MISS
136 + self.manager.get_stats.return_value = None
137 self.manager.on_new_request.return_value = RequestOffloadingContext()
138 self.handler = MockOffloadingWorker()
139
@@ -335,6 +336,7 @@ class RequestRunner:
336 self.completed_loads: list[TransferSummary] = []
337 self.completed_stores: list[TransferSummary] = []
338 self.flushed_gpu_blocks: set[GPUBlock] = set()
339 + self.kv_connector_stats: list[Any] = []
340
341 # block_id -> GPUBlock
342 self.gpu_blocks: dict[int, GPUBlock] = {}
@@ -348,6 +350,12 @@ class RequestRunner:
350 slot_mapping={},
351 )
352
353 + def _record_kv_connector_stats(self, engine_outputs: dict[int, Any]) -> None:
354 + for output in engine_outputs.values():
355 + scheduler_stats = output.scheduler_stats
356 + if scheduler_stats is not None and scheduler_stats.kv_connector_stats:
357 + self.kv_connector_stats.append(scheduler_stats.kv_connector_stats)
358 +
359 def new_request(
360 self,
361 token_ids: list[int],
@@ -524,13 +532,17 @@ class RequestRunner:
532 if self.async_scheduling:
533 # in async scheduling we update the output of the previous step
534 if prev_model_runner_output is not None:
527 - self.scheduler.update_from_output(
535 + engine_outputs = self.scheduler.update_from_output(
536 prev_scheduler_output, prev_model_runner_output
537 )
538 + self._record_kv_connector_stats(engine_outputs)
539 prev_scheduler_output = scheduler_output
540 prev_model_runner_output = model_runner_output
541 else:
533 - self.scheduler.update_from_output(scheduler_output, model_runner_output)
542 + engine_outputs = self.scheduler.update_from_output(
543 + scheduler_output, model_runner_output
544 + )
545 + self._record_kv_connector_stats(engine_outputs)
546
547 if post_step_fn is not None:
548 post_step_fn()
@@ -546,9 +558,10 @@ class RequestRunner:
558 if token_id is None:
559 if self.async_scheduling:
560 # sample last token
549 - self.scheduler.update_from_output(
561 + engine_outputs = self.scheduler.update_from_output(
562 prev_scheduler_output, prev_model_runner_output
563 )
564 + self._record_kv_connector_stats(engine_outputs)
565 break
566
567 self._parse_transfers()
tests/v1/kv_offload/cpu/test_manager.py
+58
@@ -224,6 +224,64 @@ def test_cpu_manager_reports_cache_usage_gauge():
224 check_usage_stats(manager, 0.0)
225
226
227 +def test_cpu_manager_reports_allocation_size_histogram():
228 + manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
229 +
230 + manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
231 + manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
232 + manager.prepare_store(to_keys([1, 2, 3]), _EMPTY_REQ_CTX)
233 +
234 + stats = manager.get_stats()
235 +
236 + assert stats is not None
237 + reduced = stats.reduce()
238 + assert reduced[f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_count"] == 2
239 + assert reduced[f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_sum"] == 3
240 +
241 + # The cache-usage gauge is always reported, so get_stats() never returns
242 + # None, but the histogram has nothing new once its samples are consumed.
243 + second_stats = manager.get_stats()
244 + assert second_stats is not None
245 + assert f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_count" not in (
246 + second_stats.reduce()
247 + )
248 +
249 +
250 +def test_cpu_manager_reports_allocation_size_on_allocation_failure(monkeypatch):
251 + manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
252 +
253 + def fail_allocate_blocks(keys):
254 + raise RuntimeError("allocation failed")
255 +
256 + monkeypatch.setattr(manager, "_allocate_blocks", fail_allocate_blocks)
257 +
258 + with pytest.raises(RuntimeError, match="allocation failed"):
259 + manager.prepare_store(to_keys([1, 2, 3]), _EMPTY_REQ_CTX)
260 +
261 + stats = manager.get_stats()
262 +
263 + assert stats is not None
264 + reduced = stats.reduce()
265 + assert reduced[f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_count"] == 1
266 + assert reduced[f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_sum"] == 3
267 +
268 +
269 +def test_cpu_manager_reports_allocation_size_on_eviction_failure():
270 + manager = make_cpu_manager(num_blocks=1, cache_policy="lru")
271 +
272 + manager.prepare_store(to_keys([1]), _EMPTY_REQ_CTX)
273 + manager.get_stats()
274 +
275 + assert manager.prepare_store(to_keys([2]), _EMPTY_REQ_CTX) is None
276 +
277 + stats = manager.get_stats()
278 +
279 + assert stats is not None
280 + reduced = stats.reduce()
281 + assert reduced[f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_count"] == 1
282 + assert reduced[f"{CPUOffloadingMetrics.CPU_ALLOCATION_SIZE}_sum"] == 1
283 +
284 +
285 def test_cpu_manager():
286 """
287 Tests CPUOffloadingManager with lru policy.
tests/v1/kv_offload/test_factory.py
+29 -3
@@ -21,7 +21,7 @@ from vllm.v1.kv_cache_interface import (
21 KVCacheGroupSpec,
22 KVCacheTensor,
23 )
24 -from vllm.v1.kv_offload.base import OffloadingSpec
24 +from vllm.v1.kv_offload.base import OffloadingHistogramMetadata, OffloadingSpec
25 from vllm.v1.kv_offload.cpu.spec import CPUOffloadingSpec
26 from vllm.v1.kv_offload.factory import OffloadingSpecFactory
27 from vllm.v1.kv_offload.tiering.spec import TieringOffloadingSpec
@@ -248,8 +248,8 @@ def test_duplicate_registration_raises():
248 # ---------------------------------------------------------------------------
249
250
251 -def test_build_metric_definitions_empty_below_threshold():
252 - """store_threshold < 2 → only base metric (no stores_skipped)."""
251 +def test_build_metric_definitions_below_threshold():
252 + """store_threshold < 2 keeps stores_skipped disabled."""
253 from vllm.v1.kv_offload.cpu.common import CPUOffloadingMetrics
254
255 config = _make_vllm_config(store_threshold=1)
@@ -258,6 +258,32 @@ def test_build_metric_definitions_empty_below_threshold():
258 config.kv_transfer_config.kv_connector_extra_config
259 )
260 assert CPUOffloadingMetrics.STORES_SKIPPED not in metrics
261 + assert CPUOffloadingMetrics.CPU_ALLOCATION_SIZE in metrics
262 +
263 +
264 +def test_build_metric_definitions_allocation_size_histogram():
265 + """CPU allocation size is always reported as a histogram."""
266 + from vllm.v1.kv_offload.cpu.common import CPUOffloadingMetrics
267 +
268 + config = _make_vllm_config(store_threshold=0)
269 + spec_cls = OffloadingSpecFactory.get_spec_cls(config)
270 + metrics = spec_cls.build_metric_definitions(
271 + config.kv_transfer_config.kv_connector_extra_config
272 + )
273 + metadata = metrics[CPUOffloadingMetrics.CPU_ALLOCATION_SIZE]
274 + assert isinstance(metadata, OffloadingHistogramMetadata)
275 + assert metadata.buckets == (
276 + 1,
277 + 4,
278 + 16,
279 + 64,
280 + 256,
281 + 1024,
282 + 4096,
283 + 16384,
284 + 65536,
285 + 262144,
286 + )
287
288
289 def test_build_metric_definitions_returns_counter_at_threshold():
vllm/distributed/kv_transfer/kv_connector/v1/offloading/metrics.py
+53 -1
@@ -30,6 +30,14 @@ class _TransferMetricName:
30 STORE_SIZE = "vllm:kv_offload_store_size"
31
32
33 +class _ConnectorMetricName:
34 + """Connector-side metrics emitted by scheduler-side offloading code."""
35 +
36 + LOOKUP_SYNC_DELAY = "vllm:kv_offload_lookup_sync_delay_seconds"
37 + LOOKUP_ASYNC_DELAY = "vllm:kv_offload_lookup_async_delay_seconds"
38 + ALLOCATION_FAILURE = "vllm:kv_offload_allocation_failure"
39 +
40 +
41 class _TransferType:
42 """Transfer direction labels for deprecated CPU offload metrics."""
43
@@ -74,6 +82,50 @@ def get_connector_metric_definitions() -> dict[str, OffloadingMetricMetadata]:
82 documentation="Histogram of KV offload store operation size, in bytes.",
83 buckets=TRANSFER_SIZE_BUCKETS,
84 ),
85 + _ConnectorMetricName.LOOKUP_SYNC_DELAY: OffloadingHistogramMetadata(
86 + documentation=(
87 + "Histogram of the time spent in a single offload lookup call, "
88 + "in seconds."
89 + ),
90 + buckets=(
91 + 0.00001,
92 + 0.00005,
93 + 0.0001,
94 + 0.0005,
95 + 0.001,
96 + 0.005,
97 + 0.01,
98 + 0.05,
99 + 0.1,
100 + 0.5,
101 + 1,
102 + ),
103 + ),
104 + _ConnectorMetricName.LOOKUP_ASYNC_DELAY: OffloadingHistogramMetadata(
105 + documentation=(
106 + "Histogram of time between a request's offload lookup first "
107 + "deferring and the following lookup resolving, or request "
108 + "finish, in seconds."
109 + ),
110 + buckets=(
111 + 0.0001,
112 + 0.0005,
113 + 0.001,
114 + 0.005,
115 + 0.01,
116 + 0.05,
117 + 0.1,
118 + 0.5,
119 + 1,
120 + 5,
121 + 10,
122 + ),
123 + ),
124 + _ConnectorMetricName.ALLOCATION_FAILURE: OffloadingCounterMetadata(
125 + documentation=(
126 + "Number of KV offload store allocation attempts that failed."
127 + ),
128 + ),
129 }
130
131
@@ -223,7 +275,7 @@ class OffloadingConnectorStats(KVConnectorStats):
275 def increase_counter(
276 self,
277 counter_name: str,
226 - counter_increase_value: int | float,
278 + counter_increase_value: int | float = 1,
279 labelvalues: tuple[str, ...] = (),
280 ) -> None:
281 """Increase a counter on the stats payload."""
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
+37 -8
@@ -1,5 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import time
4 from collections.abc import Iterable, Sequence
5 from dataclasses import dataclass, field
6 from itertools import islice
@@ -21,6 +22,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.offloading.events import (
22 )
23 from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
24 OffloadingConnectorStats,
25 + _ConnectorMetricName,
26 _TransferMetricName,
27 )
28 from vllm.logger import init_logger
@@ -246,6 +248,9 @@ class RequestOffloadState:
248 # In-flight job IDs. Per the connector's invariant, at any given time
249 # this contains either a single load job, or one or more store jobs.
250 transfer_jobs: set[int] = field(default_factory=set)
251 + # time.monotonic() of this request's first deferred offload lookup;
252 + # None once consumed (observed) or while no lookup is pending.
253 + deferred_lookup_start_time: float | None = None
254
255 def __post_init__(self) -> None:
256 self.group_states = tuple(
@@ -325,7 +330,7 @@ class OffloadingConnectorScheduler:
330 ):
331 self.config = SchedulerOffloadConfig.from_spec(spec)
332 self.manager: OffloadingManager = spec.get_manager()
328 - self._connector_stats: OffloadingConnectorStats | None = None
333 + self._connector_stats = OffloadingConnectorStats()
334
335 full_attention_groups: list[int] = []
336 sliding_window_groups: list[int] = []
@@ -375,6 +380,18 @@ class OffloadingConnectorScheduler:
380
381 self._events_tracker = OffloadingEventsTracker(spec.kv_events_config)
382
383 + def _maybe_observe_lookup_async_delay(
384 + self, req_status: RequestOffloadState
385 + ) -> None:
386 + start_time = req_status.deferred_lookup_start_time
387 + if start_time is None:
388 + return
389 + req_status.deferred_lookup_start_time = None
390 + self._connector_stats.observe_histogram(
391 + _ConnectorMetricName.LOOKUP_ASYNC_DELAY,
392 + time.monotonic() - start_time,
393 + )
394 +
395 def _generate_job_id(self) -> int:
396 job_id = self._job_counter
397 self._job_counter += 1
@@ -685,7 +702,17 @@ class OffloadingConnectorScheduler:
702 if request.skip_reading_prefix_cache:
703 num_hit_tokens = 0
704 else:
705 + lookup_start = time.monotonic()
706 num_hit_tokens = self._lookup(req_status)
707 + self._connector_stats.observe_histogram(
708 + _ConnectorMetricName.LOOKUP_SYNC_DELAY,
709 + time.monotonic() - lookup_start,
710 + )
711 + if num_hit_tokens is None:
712 + if req_status.deferred_lookup_start_time is None:
713 + req_status.deferred_lookup_start_time = lookup_start
714 + else:
715 + self._maybe_observe_lookup_async_delay(req_status)
716 req_status.update_num_hit_blocks(num_computed_tokens + (num_hit_tokens or 0))
717
718 self._touch(req_status)
@@ -925,6 +952,9 @@ class OffloadingConnectorScheduler:
952 new_offload_keys, req_status.req_context
953 )
954 if store_output is None:
955 + self._connector_stats.increase_counter(
956 + _ConnectorMetricName.ALLOCATION_FAILURE
957 + )
958 logger.warning("Request %s: cannot store blocks", req_id)
959 continue
960
@@ -1115,10 +1145,7 @@ class OffloadingConnectorScheduler:
1145 transfer_stats.observe_histogram(
1146 _TransferMetricName.STORE_SIZE, size
1147 )
1118 - if self._connector_stats is None:
1119 - self._connector_stats = transfer_stats
1120 - else:
1121 - self._connector_stats.aggregate(transfer_stats)
1148 + self._connector_stats.aggregate(transfer_stats)
1149
1150 for job_id, count in meta.completed_jobs.items():
1151 assert count > 0
@@ -1159,8 +1186,10 @@ class OffloadingConnectorScheduler:
1186 del self._req_status[job_status.req_id]
1187
1188 def get_stats(self) -> OffloadingConnectorStats | None:
1162 - stats = self._connector_stats
1163 - self._connector_stats = None
1189 + stats: OffloadingConnectorStats | None = None
1190 + if not self._connector_stats.is_empty():
1191 + stats = self._connector_stats
1192 + self._connector_stats = OffloadingConnectorStats()
1193
1194 manager_stats = self.manager.get_stats()
1195 if manager_stats is not None:
@@ -1198,7 +1227,7 @@ class OffloadingConnectorScheduler:
1227 return False, None
1228
1229 self.manager.on_request_finished(req_status.req_context)
1201 -
1230 + self._maybe_observe_lookup_async_delay(req_status)
1231 if not req_status.transfer_jobs:
1232 # No in-flight jobs: no later complete_store()/complete_load() calls
1233 # need this request's state.
vllm/v1/kv_offload/cpu/common.py
+1
@@ -8,6 +8,7 @@ from vllm.v1.kv_offload.base import BlockIDsLoadStoreSpec
8 class CPUOffloadingMetrics:
9 STORES_SKIPPED = "vllm:kv_offload_stores_skipped"
10 CPU_CACHE_USAGE_PERC = "vllm:kv_offload_cpu_cache_usage_perc"
11 + CPU_ALLOCATION_SIZE = "vllm:kv_offload_cpu_allocation_size"
12
13
14 class CPULoadStoreSpec(BlockIDsLoadStoreSpec):
vllm/v1/kv_offload/cpu/manager.py
+9
@@ -69,6 +69,7 @@ class CPUOffloadingManager(OffloadingManager):
69 self.store_threshold: int = store_threshold
70 self.max_tracker_size: int = max_tracker_size
71 self.stores_skipped_in_current_batch: int = 0
72 + self.allocation_sizes_in_current_batch: list[int] = []
73
74 # Number of block references. It is ordered so can evict the LRU entry in O(1).
75 self.counts: OrderedDict[OffloadKey, int] | None = (
@@ -185,6 +186,7 @@ class CPUOffloadingManager(OffloadingManager):
186 evicted_keys=[],
187 )
188
189 + self.allocation_sizes_in_current_batch.append(len(keys_to_store))
190 num_blocks_to_evict = len(keys_to_store) - self._get_num_free_blocks()
191
192 to_evict: list[OffloadKey] = []
@@ -300,10 +302,17 @@ class CPUOffloadingManager(OffloadingManager):
302 usage = num_used / self._num_blocks if self._num_blocks > 0 else 0.0
303 stats.set_gauge(CPUOffloadingMetrics.CPU_CACHE_USAGE_PERC, usage)
304
305 + for allocation_size in self.allocation_sizes_in_current_batch:
306 + stats.observe_histogram(
307 + CPUOffloadingMetrics.CPU_ALLOCATION_SIZE, allocation_size
308 + )
309 + self.allocation_sizes_in_current_batch.clear()
310 +
311 if self.store_threshold >= 2:
312 stats.increase_counter(
313 CPUOffloadingMetrics.STORES_SKIPPED,
314 self.stores_skipped_in_current_batch,
315 )
316 self.stores_skipped_in_current_batch = 0
317 +
318 return stats
vllm/v1/kv_offload/cpu/spec.py
+9 -1
@@ -12,6 +12,7 @@ from vllm.v1.kv_offload.base import (
12 CanonicalKVCaches,
13 OffloadingCounterMetadata,
14 OffloadingGaugeMetadata,
15 + OffloadingHistogramMetadata,
16 OffloadingManager,
17 OffloadingMetricMetadata,
18 OffloadingSpec,
@@ -37,7 +38,14 @@ class CPUOffloadingSpec(OffloadingSpec):
38 "values indicate transfers (stores or promotions) may be "
39 "dropped due to insufficient capacity."
40 ),
40 - )
41 + ),
42 + CPUOffloadingMetrics.CPU_ALLOCATION_SIZE: OffloadingHistogramMetadata(
43 + documentation=(
44 + "Histogram of the number of CPU blocks requested by each "
45 + "KV offload prepare_store call."
46 + ),
47 + buckets=(1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144),
48 + ),
49 }
50 store_threshold = int(extra_config.get("store_threshold", 0))
51 if store_threshold >= 2: