[HiSparse] Log steady-state max concurrency and expose host-tier utilization gauges (#58949)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Matthew Bonanni <mbonanni@redhat.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Matthew Bonanni
parent
8542c3eff5
commit
2e3154aa18
@@ -37,6 +37,14 @@ memory consumption is therefore topology- and implementation-dependent. The
|
||||
realized capacity may be slightly smaller because the budget is rounded down
|
||||
to complete host blocks.
|
||||
|
||||
Startup logs report two concurrency bounds at `max_model_len`. The generic
|
||||
`Maximum concurrency` line charges each request its full admission footprint,
|
||||
including the in-flight window of every resident group. The `HiSparse
|
||||
steady-state maximum concurrency` line charges running requests that read from
|
||||
host only their active tail pages, plus one request being admitted at its full
|
||||
footprint. Host-pool metrics are listed in
|
||||
[Metrics](../usage/metrics.md#hisparse-kv-connector-metrics).
|
||||
|
||||
## Ownership
|
||||
|
||||
| Thing | Owner | What “owner” means |
|
||||
|
||||
@@ -23,6 +23,10 @@ METRIC_SOURCE_FILES = [
|
||||
"path": "vllm/distributed/kv_transfer/kv_connector/v1/nixl/stats.py",
|
||||
"key": "metrics-nixl",
|
||||
},
|
||||
{
|
||||
"path": "vllm/distributed/kv_transfer/kv_connector/v1/hisparse/stats.py",
|
||||
"key": "metrics-hisparse",
|
||||
},
|
||||
{
|
||||
"path": "vllm/v1/simple_kv_offload/metrics.py",
|
||||
"key": "metrics-simple-kv-offload",
|
||||
|
||||
@@ -45,6 +45,15 @@ The following metrics are exposed:
|
||||
|
||||
--8<-- "gen:metrics-nixl"
|
||||
|
||||
## HiSparse KV Connector Metrics
|
||||
|
||||
These metrics are exposed when the `HiSparseConnector` KV connector is
|
||||
configured. The host-pool gauges are reported by the scheduler once per engine
|
||||
step, for the host pool shared by all tensor-parallel ranks; evictable cached
|
||||
blocks count as free.
|
||||
|
||||
--8<-- "gen:metrics-hisparse"
|
||||
|
||||
## Simple CPU Offload Connector Metrics
|
||||
|
||||
These metrics are exposed when the `SimpleCPUOffloadConnector` KV connector
|
||||
|
||||
@@ -60,6 +60,7 @@ from vllm.v1.core.kv_cache_utils import (
|
||||
from vllm.v1.hisparse.layout import (
|
||||
create_hisparse_layout,
|
||||
get_hisparse_kv_cache_groups,
|
||||
get_hisparse_steady_state_concurrency,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
ChunkedLocalAttentionSpec,
|
||||
@@ -198,6 +199,48 @@ def test_hisparse_hma_uses_resolved_gpu_block_size(
|
||||
assert scheduler_block_size == hash_block_size == gpu_block_size
|
||||
|
||||
|
||||
def test_hisparse_steady_state_concurrency_excludes_spilled_resident_pages():
|
||||
"""Running requests pin only their resident tail; the newest still needs its
|
||||
full in-flight window to be admitted."""
|
||||
block_size = 16
|
||||
config = SimpleNamespace(
|
||||
model_config=SimpleNamespace(max_model_len=10 * block_size),
|
||||
parallel_config=SimpleNamespace(decode_context_parallel_size=1),
|
||||
max_in_flight_tokens=10 * block_size,
|
||||
)
|
||||
attn_spec = FullAttentionSpec(
|
||||
block_size=block_size, num_kv_heads=1, head_size=64, dtype=torch.float16
|
||||
)
|
||||
page_size = attn_spec.page_size_bytes
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=64,
|
||||
kv_cache_tensors=[],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["source"], attn_spec, host_resident=True),
|
||||
KVCacheGroupSpec(["indexer"], attn_spec),
|
||||
KVCacheGroupSpec(
|
||||
["resident"],
|
||||
HiSparseResidentSpec(block_size=block_size, page_size=page_size),
|
||||
),
|
||||
KVCacheGroupSpec(
|
||||
["hot"],
|
||||
HiSparseHotSpec(
|
||||
block_size=block_size, page_size=page_size, blocks_per_request=4
|
||||
),
|
||||
),
|
||||
],
|
||||
hisparse_host_num_blocks=100,
|
||||
)
|
||||
|
||||
# Admission takes indexer 10 + resident 10 + hot 4 = 24 blocks and steady
|
||||
# state 16, so 3 fit (2 x 16 + 24 <= 64) but not 4 (3 x 16 + 24 > 64).
|
||||
assert get_hisparse_steady_state_concurrency(config, kv_cache_config) == 3.5
|
||||
# The worst-case bound charges all 10 resident pages.
|
||||
assert get_max_concurrency_for_kv_cache_config(
|
||||
config, kv_cache_config
|
||||
) == pytest.approx(64 / 24)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("extra_blocks,ok", [(1, False), (2, True)])
|
||||
def test_hisparse_host_pool_must_fit_max_model_len(monkeypatch, extra_blocks, ok):
|
||||
"""Every page needs a host block, so one max_model_len request must fit."""
|
||||
|
||||
@@ -64,6 +64,42 @@ def test_hisparse_requires_block_outermost_device_layout():
|
||||
assert HiSparseConnector.get_required_kvcache_layout(MagicMock()) == "BLHNC"
|
||||
|
||||
|
||||
def test_scheduler_stats_report_host_pool_usage():
|
||||
"""The scheduler-side stats sample the coordinator's host pool level."""
|
||||
from tests.v1.core.test_prefix_caching import (
|
||||
HISPARSE_BLOCK_SIZE,
|
||||
make_hisparse_kv_cache_manager,
|
||||
make_request,
|
||||
sha256,
|
||||
)
|
||||
from vllm.v1.core.kv_cache_utils import init_none_hash
|
||||
|
||||
init_none_hash(sha256)
|
||||
|
||||
manager = make_hisparse_kv_cache_manager(32, host_num_blocks=8)
|
||||
scheduler = HiSparseConnectorScheduler(async_speculative=False)
|
||||
scheduler.bind_coordinator(get_hisparse_coordinator(manager))
|
||||
coordinator = scheduler.coordinator
|
||||
|
||||
stats = scheduler.get_kv_connector_stats()
|
||||
assert stats.data["host_cache_usage_perc"] == [0.0]
|
||||
assert stats.data["pending_page_transfers"] == [0]
|
||||
|
||||
request = make_request(
|
||||
"request", list(range(4 * HISPARSE_BLOCK_SIZE)), HISPARSE_BLOCK_SIZE, sha256
|
||||
)
|
||||
assert manager.allocate_slots(request, num_new_tokens=64) is not None
|
||||
host_blocks = coordinator.host_manager.req_to_blocks[request.request_id]
|
||||
num_used = sum(not block.is_null for block in host_blocks)
|
||||
|
||||
# The pool reserves the null block, so 8 configured blocks yield 7 usable.
|
||||
expected = pytest.approx(num_used / 7)
|
||||
assert 0 < num_used <= 7
|
||||
for _ in range(2):
|
||||
stats = scheduler.get_kv_connector_stats()
|
||||
assert stats.data["host_cache_usage_perc"] == [expected]
|
||||
|
||||
|
||||
def test_no_forward_enqueues_deferred_hisparse_transfers():
|
||||
"""A zero-token step must still enqueue deferred post-forward transfers."""
|
||||
connector = object.__new__(ActiveKVConnector)
|
||||
|
||||
@@ -26,6 +26,28 @@ def test_record_snapshot_and_reduce():
|
||||
assert reduced["HiSparse host-to-device bytes"] == 64
|
||||
|
||||
|
||||
def test_record_host_usage_reports_latest_level():
|
||||
stats = HiSparseKVConnectorStats()
|
||||
stats.record_host_usage(usage=0.4, pending_page_transfers=2)
|
||||
stats.record_host_usage(usage=0.8, pending_page_transfers=0)
|
||||
|
||||
reduced = stats.reduce()
|
||||
assert reduced["HiSparse host KV cache usage %"] == 80.0
|
||||
|
||||
|
||||
def test_aggregate_keeps_latest_host_usage_level():
|
||||
first = HiSparseKVConnectorStats()
|
||||
first.record_host_usage(usage=0.4, pending_page_transfers=2)
|
||||
second = HiSparseKVConnectorStats()
|
||||
second.record_host_usage(usage=0.8, pending_page_transfers=0)
|
||||
second.record_host_usage(usage=0.6, pending_page_transfers=1)
|
||||
|
||||
first.aggregate(second)
|
||||
|
||||
assert first.data["host_cache_usage_perc"] == [0.4, 0.6]
|
||||
assert first.data["pending_page_transfers"] == [2, 1]
|
||||
|
||||
|
||||
def test_aggregate_extends_snapshot_deltas():
|
||||
first = HiSparseKVConnectorStats()
|
||||
first.record_snapshot(hits=7, misses=3, host_to_device_bytes=48)
|
||||
@@ -34,11 +56,9 @@ def test_aggregate_extends_snapshot_deltas():
|
||||
|
||||
first.aggregate(second)
|
||||
|
||||
assert first.data == {
|
||||
"cache_hits": [7, 5],
|
||||
"cache_misses": [3, 1],
|
||||
"host_to_device_bytes": [48, 16],
|
||||
}
|
||||
assert first.data["cache_hits"] == [7, 5]
|
||||
assert first.data["cache_misses"] == [3, 1]
|
||||
assert first.data["host_to_device_bytes"] == [48, 16]
|
||||
|
||||
|
||||
def test_aggregate_skips_empty_stats():
|
||||
@@ -60,3 +80,54 @@ def test_build_kv_connector_stats_round_trip():
|
||||
assert rebuilt is not None
|
||||
assert isinstance(rebuilt, HiSparseKVConnectorStats)
|
||||
assert rebuilt.reduce() == stats.reduce()
|
||||
|
||||
|
||||
def test_prom_metrics_observe_host_usage_gauges():
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from prometheus_client import Counter, Gauge, Histogram
|
||||
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.hisparse.stats import (
|
||||
HiSparsePromMetrics,
|
||||
)
|
||||
|
||||
class _FakeMetric:
|
||||
def __init__(self, **kwargs: Any):
|
||||
self.kwargs = kwargs
|
||||
self.increments: list[int | float] = []
|
||||
self.set_values: list[int | float] = []
|
||||
|
||||
def labels(self, *labelvalues: object) -> "_FakeMetric":
|
||||
return self
|
||||
|
||||
def inc(self, value: int | float) -> None:
|
||||
self.increments.append(value)
|
||||
|
||||
def set(self, value: int | float) -> None:
|
||||
self.set_values.append(value)
|
||||
|
||||
created: dict[str, _FakeMetric] = {}
|
||||
|
||||
class _NamedFake(_FakeMetric):
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
created[kwargs["name"]] = self
|
||||
|
||||
prom = HiSparsePromMetrics(
|
||||
vllm_config=SimpleNamespace(kv_transfer_config=None),
|
||||
metric_types={Gauge: _NamedFake, Counter: _NamedFake, Histogram: _NamedFake},
|
||||
labelnames=["model_name"],
|
||||
per_engine_labelvalues={0: ["model"]},
|
||||
)
|
||||
|
||||
stats = HiSparseKVConnectorStats()
|
||||
stats.record_snapshot(hits=3, misses=2, host_to_device_bytes=32)
|
||||
stats.record_host_usage(usage=0.5, pending_page_transfers=1)
|
||||
stats.record_host_usage(usage=0.75, pending_page_transfers=3)
|
||||
prom.observe(stats.to_dict())
|
||||
|
||||
assert created["vllm:hisparse_cache_hits"].increments == [3]
|
||||
assert created["vllm:hisparse_host_to_device_bytes"].increments == [32]
|
||||
assert created["vllm:hisparse_host_cache_usage_perc"].set_values == [0.75]
|
||||
assert created["vllm:hisparse_pending_page_transfers"].set_values == [3]
|
||||
|
||||
@@ -92,6 +92,8 @@ def test_hisparse_worker_get_kv_connector_stats_reads_completed_snapshot(monkeyp
|
||||
"cache_hits": [12],
|
||||
"cache_misses": [4],
|
||||
"host_to_device_bytes": [64],
|
||||
"host_cache_usage_perc": [],
|
||||
"pending_page_transfers": [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,10 +30,12 @@ from vllm.distributed.kv_transfer.kv_connector.v1.metrics import (
|
||||
PromMetric,
|
||||
PromMetricT,
|
||||
)
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.core.kv_cache_utils import KVCacheBlockCopy
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.hisparse.coordinator import get_hisparse_coordinator
|
||||
from vllm.v1.hisparse.layout import get_hisparse_steady_state_concurrency
|
||||
from vllm.v1.hisparse.types import SparseKVOffloadCommand, SparseKVRowMirror
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
@@ -44,6 +46,8 @@ if TYPE_CHECKING:
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
from vllm.v1.request import Request
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HiSparseConnectorMetadata(KVConnectorMetadata):
|
||||
@@ -99,6 +103,17 @@ class HiSparseConnectorScheduler:
|
||||
assert self.coordinator is None
|
||||
self.coordinator = coordinator
|
||||
|
||||
def get_kv_connector_stats(self) -> HiSparseKVConnectorStats | None:
|
||||
if self.coordinator is None:
|
||||
return None
|
||||
host_pool = self.coordinator.get_host_block_pool()
|
||||
assert host_pool is not None
|
||||
stats = HiSparseKVConnectorStats()
|
||||
stats.record_host_usage(
|
||||
host_pool.get_usage(), len(self.coordinator.pending_spills)
|
||||
)
|
||||
return stats
|
||||
|
||||
def build_connector_meta(
|
||||
self, scheduler_output: SchedulerOutput
|
||||
) -> HiSparseConnectorMetadata:
|
||||
@@ -223,6 +238,16 @@ class HiSparseConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
),
|
||||
draft_kv_lookahead=vllm_config.num_lookahead_tokens,
|
||||
)
|
||||
max_model_len = vllm_config.model_config.max_model_len
|
||||
steady_concurrency = get_hisparse_steady_state_concurrency(
|
||||
vllm_config, kv_cache_config
|
||||
)
|
||||
logger.info_once(
|
||||
"HiSparse steady-state maximum concurrency for %s tokens per "
|
||||
"request: %.2fx (running requests reading from host).",
|
||||
f"{max_model_len:,}",
|
||||
steady_concurrency,
|
||||
)
|
||||
elif role == KVConnectorRole.WORKER:
|
||||
self.connector_worker = HiSparseConnectorWorker(
|
||||
vllm_config, kv_cache_config
|
||||
@@ -263,9 +288,11 @@ class HiSparseConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
self.connector_worker.reset_hot_state()
|
||||
|
||||
def get_kv_connector_stats(self) -> KVConnectorStats | None:
|
||||
if self.connector_worker is None:
|
||||
return None
|
||||
return self.connector_worker.get_kv_connector_stats()
|
||||
if self.connector_worker is not None:
|
||||
return self.connector_worker.get_kv_connector_stats()
|
||||
if self.connector_scheduler is not None:
|
||||
return self.connector_scheduler.get_kv_connector_stats()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def build_kv_connector_stats(
|
||||
|
||||
@@ -14,14 +14,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.metrics import (
|
||||
)
|
||||
from vllm.v1.metrics.utils import create_metric_per_engine
|
||||
|
||||
_HISPARSE_COUNTERS: tuple[tuple[str, str], ...] = (
|
||||
("cache_hits", "Number of HiSparse device hot-buffer hits."),
|
||||
("cache_misses", "Number of HiSparse device hot-buffer misses."),
|
||||
(
|
||||
"host_to_device_bytes",
|
||||
"Bytes transferred from host KV storage to HiSparse hot buffers.",
|
||||
),
|
||||
)
|
||||
_HISPARSE_LEVEL_KEYS = frozenset({"host_cache_usage_perc", "pending_page_transfers"})
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -30,6 +23,8 @@ class HiSparseKVConnectorStats(KVConnectorStats):
|
||||
|
||||
Each list entry is the delta recorded over one device counter snapshot
|
||||
interval, so a list can hold multiple snapshots per logging interval.
|
||||
The host-tier gauges are level values, not deltas: aggregation keeps
|
||||
only the most recent observation.
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -39,10 +34,12 @@ class HiSparseKVConnectorStats(KVConnectorStats):
|
||||
|
||||
def reset(self):
|
||||
# Must be serializable
|
||||
self.data: dict[str, list[int]] = {
|
||||
self.data: dict[str, list[int | float]] = {
|
||||
"cache_hits": [],
|
||||
"cache_misses": [],
|
||||
"host_to_device_bytes": [],
|
||||
"host_cache_usage_perc": [],
|
||||
"pending_page_transfers": [],
|
||||
}
|
||||
|
||||
def record_snapshot(self, hits: int, misses: int, host_to_device_bytes: int):
|
||||
@@ -50,21 +47,32 @@ class HiSparseKVConnectorStats(KVConnectorStats):
|
||||
self.data["cache_misses"].append(misses)
|
||||
self.data["host_to_device_bytes"].append(host_to_device_bytes)
|
||||
|
||||
def record_host_usage(self, usage: float, pending_page_transfers: int) -> None:
|
||||
self.data["host_cache_usage_perc"].append(usage)
|
||||
self.data["pending_page_transfers"].append(pending_page_transfers)
|
||||
|
||||
def aggregate(self, other: KVConnectorStats) -> KVConnectorStats:
|
||||
if not other.is_empty():
|
||||
for k, v in other.data.items():
|
||||
accumulator = self.data[k]
|
||||
assert isinstance(accumulator, list)
|
||||
accumulator.extend(v)
|
||||
if k in _HISPARSE_LEVEL_KEYS:
|
||||
accumulator.extend(v[-1:])
|
||||
else:
|
||||
accumulator.extend(v)
|
||||
return self
|
||||
|
||||
def reduce(self) -> dict[str, int | float]:
|
||||
# Compute compact representative stats suitable for CLI logging.
|
||||
return {
|
||||
reduced: dict[str, int | float] = {
|
||||
"HiSparse hot-buffer hits": sum(self.data["cache_hits"]),
|
||||
"HiSparse hot-buffer misses": sum(self.data["cache_misses"]),
|
||||
"HiSparse host-to-device bytes": sum(self.data["host_to_device_bytes"]),
|
||||
}
|
||||
if self.data["host_cache_usage_perc"]:
|
||||
usage = self.data["host_cache_usage_perc"][-1]
|
||||
reduced["HiSparse host KV cache usage %"] = round(100 * usage, 1)
|
||||
return reduced
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return all(len(values) == 0 for values in self.data.values())
|
||||
@@ -80,18 +88,66 @@ class HiSparsePromMetrics(KVConnectorPromMetrics):
|
||||
):
|
||||
super().__init__(vllm_config, metric_types, labelnames, per_engine_labelvalues)
|
||||
|
||||
self.hisparse_counters: dict[str, Any] = {}
|
||||
for name, documentation in _HISPARSE_COUNTERS:
|
||||
counter = self._counter_cls(
|
||||
name=f"vllm:hisparse_{name}",
|
||||
documentation=documentation,
|
||||
labelnames=labelnames,
|
||||
)
|
||||
self.hisparse_counters[name] = create_metric_per_engine(
|
||||
counter, self.per_engine_labelvalues
|
||||
)
|
||||
counter_cache_hits = self._counter_cls(
|
||||
name="vllm:hisparse_cache_hits",
|
||||
documentation="Number of HiSparse device hot-buffer hits.",
|
||||
labelnames=labelnames,
|
||||
)
|
||||
counter_cache_misses = self._counter_cls(
|
||||
name="vllm:hisparse_cache_misses",
|
||||
documentation="Number of HiSparse device hot-buffer misses.",
|
||||
labelnames=labelnames,
|
||||
)
|
||||
counter_host_to_device_bytes = self._counter_cls(
|
||||
name="vllm:hisparse_host_to_device_bytes",
|
||||
documentation=(
|
||||
"Bytes transferred from host KV storage to HiSparse hot buffers."
|
||||
),
|
||||
labelnames=labelnames,
|
||||
)
|
||||
self.hisparse_counters: dict[str, Any] = {
|
||||
"cache_hits": create_metric_per_engine(
|
||||
counter_cache_hits, self.per_engine_labelvalues
|
||||
),
|
||||
"cache_misses": create_metric_per_engine(
|
||||
counter_cache_misses, self.per_engine_labelvalues
|
||||
),
|
||||
"host_to_device_bytes": create_metric_per_engine(
|
||||
counter_host_to_device_bytes, self.per_engine_labelvalues
|
||||
),
|
||||
}
|
||||
|
||||
gauge_host_cache_usage = self._gauge_cls(
|
||||
name="vllm:hisparse_host_cache_usage_perc",
|
||||
documentation=(
|
||||
"HiSparse host KV-cache usage. 1 means 100 percent usage. "
|
||||
"Evictable cached blocks count as free."
|
||||
),
|
||||
multiprocess_mode="mostrecent",
|
||||
labelnames=labelnames,
|
||||
)
|
||||
gauge_pending_page_transfers = self._gauge_cls(
|
||||
name="vllm:hisparse_pending_page_transfers",
|
||||
documentation=(
|
||||
"HiSparse page transfers (write-backs and restores) not yet completed."
|
||||
),
|
||||
multiprocess_mode="mostrecent",
|
||||
labelnames=labelnames,
|
||||
)
|
||||
self.hisparse_gauges: dict[str, Any] = {
|
||||
"host_cache_usage_perc": create_metric_per_engine(
|
||||
gauge_host_cache_usage, self.per_engine_labelvalues
|
||||
),
|
||||
"pending_page_transfers": create_metric_per_engine(
|
||||
gauge_pending_page_transfers, self.per_engine_labelvalues
|
||||
),
|
||||
}
|
||||
|
||||
def observe(self, transfer_stats_data: dict[str, Any], engine_idx: int = 0):
|
||||
for name, counter in self.hisparse_counters.items():
|
||||
for value in transfer_stats_data.get(name, []):
|
||||
counter[engine_idx].inc(value)
|
||||
for name, gauge in self.hisparse_gauges.items():
|
||||
values = transfer_stats_data.get(name, [])
|
||||
if values:
|
||||
gauge[engine_idx].set(values[-1])
|
||||
|
||||
@@ -19,6 +19,7 @@ from vllm.v1.core.single_type_kv_cache_manager import (
|
||||
SingleTypeKVCacheManager,
|
||||
)
|
||||
from vllm.v1.hisparse.types import (
|
||||
ACTIVE_TAIL_PAGES,
|
||||
SparseKVOffloadCommand,
|
||||
SparseKVPageTransfer,
|
||||
SparseKVRowMirror,
|
||||
@@ -32,10 +33,6 @@ from vllm.v1.request import Request
|
||||
if TYPE_CHECKING:
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheManager
|
||||
|
||||
# Sealed pages this many positions behind the block-table tail stay pinned so
|
||||
# a page written by an in-flight step is never handed out under it.
|
||||
_ACTIVE_TAIL_PAGES = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class _PendingPublication:
|
||||
@@ -343,7 +340,7 @@ class HiSparseCoordinator:
|
||||
req_blocks = manager.req_to_blocks.get(request_id)
|
||||
if (
|
||||
req_blocks is None
|
||||
or page_idx >= len(req_blocks) - _ACTIVE_TAIL_PAGES
|
||||
or page_idx >= len(req_blocks) - ACTIVE_TAIL_PAGES
|
||||
or req_blocks[page_idx].is_null
|
||||
):
|
||||
return None
|
||||
|
||||
@@ -14,6 +14,7 @@ from vllm.v1.hisparse.runtime import (
|
||||
get_hisparse_host_block_stride,
|
||||
use_shared_hisparse_host_pool,
|
||||
)
|
||||
from vllm.v1.hisparse.types import ACTIVE_TAIL_PAGES
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
HiSparseHotSpec,
|
||||
HiSparseResidentSpec,
|
||||
@@ -373,3 +374,34 @@ def get_hisparse_kv_cache_config(
|
||||
vllm_config.cache_config.prefix_cache_retention_interval
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_hisparse_steady_state_concurrency(
|
||||
vllm_config: VllmConfig, kv_cache_config: KVCacheConfig
|
||||
) -> float:
|
||||
"""Max concurrency at max_model_len once running requests read from host.
|
||||
|
||||
A request reading from host pins only its active tail of each resident
|
||||
group, but admitting one still takes its full in-flight window, so the
|
||||
bound is all-but-one requests at steady state plus one being admitted.
|
||||
"""
|
||||
admission_blocks = 0
|
||||
steady_blocks = 0
|
||||
host_blocks = 0
|
||||
for group in kv_cache_config.kv_cache_groups:
|
||||
spec = group.kv_cache_spec
|
||||
required = cdiv(spec.max_memory_usage_bytes(vllm_config), spec.page_size_bytes)
|
||||
if group.host_resident:
|
||||
host_blocks += required
|
||||
continue
|
||||
admission_blocks += required
|
||||
if isinstance(spec, HiSparseResidentSpec):
|
||||
required = min(required, ACTIVE_TAIL_PAGES)
|
||||
steady_blocks += required
|
||||
assert kv_cache_config.hisparse_host_num_blocks is not None
|
||||
num_blocks = kv_cache_config.num_blocks
|
||||
if num_blocks < admission_blocks:
|
||||
gpu_concurrency = num_blocks / admission_blocks
|
||||
else:
|
||||
gpu_concurrency = 1 + (num_blocks - admission_blocks) / steady_blocks
|
||||
return min(gpu_concurrency, kv_cache_config.hisparse_host_num_blocks / host_blocks)
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Sealed pages this many positions behind the block-table tail stay pinned so
|
||||
# a page written by an in-flight step is never handed out under it.
|
||||
ACTIVE_TAIL_PAGES = 2
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SparseKVPageTransfer:
|
||||
|
||||
Reference in New Issue
Block a user