[UMBP][1/10] Add shared connector, runtime contracts, and request lifecycle (#61789)
Signed-off-by: CharlesWu <Charles.Wu@amd.com> Co-authored-by: Yichao Zhu <Yichao.Zhu@amd.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from .connector import UMBPStoreConnector
|
||||
from .data import (
|
||||
BlockIdentityCodec,
|
||||
BlockTransferPlan,
|
||||
KVLayoutDescriptor,
|
||||
KVLayoutPlanner,
|
||||
KVRange,
|
||||
KVRegion,
|
||||
LoadSpec,
|
||||
RankTopology,
|
||||
RequestTracker,
|
||||
TransferJobState,
|
||||
TransferJobStatus,
|
||||
UMBPConnectorMetadata,
|
||||
UMBPConnectorWorkerMetadata,
|
||||
UMBPNamespace,
|
||||
)
|
||||
from .runtime import UMBPRuntimeConfig, UMBPRuntimeFactory
|
||||
|
||||
__all__ = [
|
||||
"UMBPStoreConnector",
|
||||
"BlockIdentityCodec",
|
||||
"BlockTransferPlan",
|
||||
"KVLayoutPlanner",
|
||||
"KVLayoutDescriptor",
|
||||
"KVRange",
|
||||
"KVRegion",
|
||||
"LoadSpec",
|
||||
"RequestTracker",
|
||||
"RankTopology",
|
||||
"TransferJobState",
|
||||
"TransferJobStatus",
|
||||
"UMBPConnectorMetadata",
|
||||
"UMBPConnectorWorkerMetadata",
|
||||
"UMBPNamespace",
|
||||
"UMBPRuntimeConfig",
|
||||
"UMBPRuntimeFactory",
|
||||
]
|
||||
@@ -0,0 +1,220 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""vLLM-facing shared UMBP connector."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
|
||||
KVConnectorBase_V1,
|
||||
KVConnectorMetadata,
|
||||
KVConnectorRole,
|
||||
KVConnectorTransferResults,
|
||||
)
|
||||
from vllm.forward_context import ForwardContext
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
|
||||
from .data import (
|
||||
BlockIdentityCodec,
|
||||
KVLayoutPlanner,
|
||||
RankTopology,
|
||||
UMBPConnectorMetadata,
|
||||
UMBPConnectorWorkerMetadata,
|
||||
UMBPNamespace,
|
||||
)
|
||||
from .runtime import UMBPRuntimeConfig, UMBPRuntimeFactory
|
||||
from .scheduler import UMBPStoreConnectorScheduler
|
||||
from .worker import UMBPStoreConnectorWorker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
from vllm.v1.request import Request
|
||||
|
||||
|
||||
class UMBPStoreConnector(KVConnectorBase_V1):
|
||||
"""vLLM connector lifecycle shared by every UMBP runtime mode."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
role: KVConnectorRole,
|
||||
kv_cache_config: KVCacheConfig,
|
||||
) -> None:
|
||||
super().__init__(vllm_config, role, kv_cache_config)
|
||||
topology = RankTopology.from_vllm_config(vllm_config)
|
||||
runtime_config = UMBPRuntimeConfig.from_vllm(
|
||||
vllm_config
|
||||
).resolve_for_rank_count(topology.rank_count)
|
||||
if topology.pp_size > 1:
|
||||
raise NotImplementedError(
|
||||
f"{runtime_config.mode} UMBP does not support pipeline parallelism"
|
||||
)
|
||||
runtime = UMBPRuntimeFactory.build(runtime_config)
|
||||
namespace = UMBPNamespace.from_vllm_config(vllm_config, kv_cache_config)
|
||||
codec = BlockIdentityCodec(
|
||||
namespace=namespace,
|
||||
tp_rank=topology.tp_rank,
|
||||
pp_rank=topology.pp_rank,
|
||||
pcp_rank=topology.pcp_rank,
|
||||
dcp_rank=topology.dcp_rank,
|
||||
)
|
||||
layout = KVLayoutPlanner.from_kv_cache_config(kv_cache_config)
|
||||
layout_descriptor = layout.describe(topology)
|
||||
self._runtime = runtime
|
||||
self._store_submitted = False
|
||||
self.connector_scheduler: UMBPStoreConnectorScheduler | None = None
|
||||
self.connector_worker: UMBPStoreConnectorWorker | None = None
|
||||
if role == KVConnectorRole.SCHEDULER:
|
||||
self.connector_scheduler = UMBPStoreConnectorScheduler(
|
||||
vllm_config,
|
||||
kv_cache_config,
|
||||
runtime.create_scheduler_handle(
|
||||
namespace.value, topology, layout_descriptor
|
||||
),
|
||||
codec,
|
||||
topology,
|
||||
)
|
||||
else:
|
||||
self.connector_worker = UMBPStoreConnectorWorker(
|
||||
runtime.create_worker_handle(
|
||||
namespace.value, topology, layout_descriptor
|
||||
),
|
||||
layout,
|
||||
codec=codec,
|
||||
)
|
||||
|
||||
def get_num_new_matched_tokens(
|
||||
self, request: Request, num_computed_tokens: int
|
||||
) -> tuple[int | None, bool]:
|
||||
assert self.connector_scheduler is not None
|
||||
return self.connector_scheduler.get_num_new_matched_tokens(
|
||||
request, num_computed_tokens
|
||||
)
|
||||
|
||||
def update_state_after_alloc(
|
||||
self,
|
||||
request: Request,
|
||||
blocks: KVCacheBlocks,
|
||||
num_external_tokens: int,
|
||||
) -> None:
|
||||
assert self.connector_scheduler is not None
|
||||
self.connector_scheduler.update_state_after_alloc(
|
||||
request, blocks, num_external_tokens
|
||||
)
|
||||
|
||||
def bind_gpu_block_pool(self, gpu_block_pool: Any) -> None:
|
||||
assert self.connector_scheduler is not None
|
||||
self.connector_scheduler.bind_gpu_block_pool(gpu_block_pool)
|
||||
|
||||
def build_connector_meta(
|
||||
self, scheduler_output: SchedulerOutput
|
||||
) -> KVConnectorMetadata:
|
||||
assert self.connector_scheduler is not None
|
||||
return self.connector_scheduler.build_connector_meta(scheduler_output)
|
||||
|
||||
def request_finished(
|
||||
self, request: Request, block_ids: list[int]
|
||||
) -> tuple[bool, dict[str, Any] | None]:
|
||||
assert self.connector_scheduler is not None
|
||||
return self.connector_scheduler.request_finished(request, (block_ids,))
|
||||
|
||||
def update_connector_output(self, connector_output: Any) -> None:
|
||||
assert self.connector_scheduler is not None
|
||||
self.connector_scheduler.update_connector_output(connector_output)
|
||||
|
||||
def has_pending_push_work(self) -> bool:
|
||||
assert self.connector_scheduler is not None
|
||||
return self.connector_scheduler.has_pending_push_work()
|
||||
|
||||
def reset_cache(self) -> bool | None:
|
||||
if self.connector_scheduler is None:
|
||||
return None
|
||||
return self.connector_scheduler.reset_store()
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]) -> None:
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.register_kv_caches(kv_caches)
|
||||
|
||||
def bind_connector_metadata(self, connector_metadata: KVConnectorMetadata) -> None:
|
||||
super().bind_connector_metadata(connector_metadata)
|
||||
self._store_submitted = False
|
||||
|
||||
def clear_connector_metadata(self) -> None:
|
||||
# No-forward steps skip the model runner's wait_for_save hook.
|
||||
if self.connector_worker is not None and self.has_connector_metadata():
|
||||
self.wait_for_save()
|
||||
super().clear_connector_metadata()
|
||||
|
||||
def handle_preemptions(self, kv_connector_metadata: KVConnectorMetadata) -> None:
|
||||
assert self.connector_worker is not None
|
||||
assert isinstance(kv_connector_metadata, UMBPConnectorMetadata)
|
||||
self.connector_worker.handle_preemptions(kv_connector_metadata)
|
||||
|
||||
def start_load_kv(self, forward_context: ForwardContext, **kwargs: Any) -> None:
|
||||
del kwargs
|
||||
assert self.connector_worker is not None
|
||||
metadata = self._get_connector_metadata()
|
||||
assert isinstance(metadata, UMBPConnectorMetadata)
|
||||
self.connector_worker.start_load_kv(forward_context, metadata)
|
||||
|
||||
def wait_for_layer_load(self, layer_name: str) -> None:
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.wait_for_layer_load(layer_name)
|
||||
|
||||
def save_kv_layer(
|
||||
self,
|
||||
layer_name: str,
|
||||
kv_layer: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Stores are submitted once per step, from wait_for_save.
|
||||
return
|
||||
|
||||
def wait_for_save(self) -> None:
|
||||
if self._store_submitted:
|
||||
return
|
||||
assert self.connector_worker is not None
|
||||
metadata = self._get_connector_metadata()
|
||||
assert isinstance(metadata, UMBPConnectorMetadata)
|
||||
self.connector_worker.enqueue_stores(metadata)
|
||||
self.connector_worker.wait_for_save()
|
||||
self._store_submitted = True
|
||||
|
||||
def get_finished(
|
||||
self, finished_req_ids: set[str]
|
||||
) -> tuple[set[str] | None, set[str] | None]:
|
||||
assert self.connector_worker is not None
|
||||
return self.connector_worker.get_finished(finished_req_ids)
|
||||
|
||||
def get_transfer_results(
|
||||
self, finished_req_ids: set[str]
|
||||
) -> KVConnectorTransferResults:
|
||||
assert self.connector_worker is not None
|
||||
finished_sending, finished_recving = self.connector_worker.get_finished(
|
||||
finished_req_ids
|
||||
)
|
||||
return KVConnectorTransferResults(
|
||||
finished_sending=set(finished_sending or ()),
|
||||
finished_recving=set(finished_recving or ()),
|
||||
)
|
||||
|
||||
def get_block_ids_with_load_errors(self) -> set[int]:
|
||||
assert self.connector_worker is not None
|
||||
return self.connector_worker.get_block_ids_with_load_errors()
|
||||
|
||||
def build_connector_worker_meta(self) -> UMBPConnectorWorkerMetadata:
|
||||
assert self.connector_worker is not None
|
||||
return self.connector_worker.build_connector_worker_meta()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self.connector_scheduler is not None:
|
||||
self.connector_scheduler.close()
|
||||
if self.connector_worker is not None:
|
||||
self.connector_worker.close()
|
||||
@@ -0,0 +1,620 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Shared data types for the UMBP KV connector.
|
||||
|
||||
The types in this module deliberately do not depend on a UMBP client. They
|
||||
describe vLLM's logical KV objects and the physical ranges a runtime adapter
|
||||
will transfer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field, replace
|
||||
from enum import Enum
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
|
||||
KVConnectorMetadata,
|
||||
KVConnectorWorkerMetadata,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RankTopology:
|
||||
"""Model-parallel ranks that participate in one KV object."""
|
||||
|
||||
tp_rank: int = 0
|
||||
tp_size: int = 1
|
||||
pp_rank: int = 0
|
||||
pp_size: int = 1
|
||||
pcp_rank: int = 0
|
||||
pcp_size: int = 1
|
||||
dcp_rank: int = 0
|
||||
dcp_size: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for name in ("tp_size", "pp_size", "pcp_size", "dcp_size"):
|
||||
if getattr(self, name) <= 0:
|
||||
raise ValueError(f"{name} must be positive")
|
||||
for rank, size, name in (
|
||||
(self.tp_rank, self.tp_size, "tp_rank"),
|
||||
(self.pp_rank, self.pp_size, "pp_rank"),
|
||||
(self.pcp_rank, self.pcp_size, "pcp_rank"),
|
||||
(self.dcp_rank, self.dcp_size, "dcp_rank"),
|
||||
):
|
||||
if rank < 0 or rank >= size:
|
||||
raise ValueError(f"{name}={rank} is outside [0, {size})")
|
||||
|
||||
@classmethod
|
||||
def from_vllm_config(cls, vllm_config: Any) -> RankTopology:
|
||||
parallel = vllm_config.parallel_config
|
||||
tp_size = getattr(parallel, "tensor_parallel_size", 1)
|
||||
pp_size = getattr(parallel, "pipeline_parallel_size", 1)
|
||||
pcp_size = getattr(parallel, "prefill_context_parallel_size", 1)
|
||||
# An engine's workers are laid out as [PP, PCP, TP], TP innermost
|
||||
# (initialize_model_parallel).
|
||||
rank = getattr(parallel, "rank", 0)
|
||||
tp_rank = rank % tp_size
|
||||
dcp_size = getattr(parallel, "decode_context_parallel_size", 1)
|
||||
return cls(
|
||||
tp_rank=tp_rank,
|
||||
tp_size=tp_size,
|
||||
pp_rank=rank // (tp_size * pcp_size) % pp_size,
|
||||
pp_size=pp_size,
|
||||
pcp_rank=rank // tp_size % pcp_size,
|
||||
pcp_size=pcp_size,
|
||||
# DCP partitions the TP workers; it does not create a second
|
||||
# Cartesian worker dimension. ParallelConfig is constructed before
|
||||
# process groups, so derive this stable identity from TP rank.
|
||||
dcp_rank=tp_rank % dcp_size,
|
||||
dcp_size=dcp_size,
|
||||
)
|
||||
|
||||
@property
|
||||
def local_namespace(self) -> tuple[int, int, int, int]:
|
||||
return (self.tp_rank, self.pcp_rank, self.dcp_rank, self.pp_rank)
|
||||
|
||||
@property
|
||||
def rank_count(self) -> int:
|
||||
return len(self.all_namespaces())
|
||||
|
||||
def all_namespaces(self) -> tuple[tuple[int, int, int, int], ...]:
|
||||
if self.dcp_size > 1:
|
||||
if self.tp_size % self.dcp_size:
|
||||
raise ValueError("dcp_size must divide tp_size")
|
||||
return tuple(
|
||||
(tp, pcp, tp % self.dcp_size, pp)
|
||||
for pp in range(self.pp_size)
|
||||
for pcp in range(self.pcp_size)
|
||||
for tp in range(self.tp_size)
|
||||
)
|
||||
return tuple(
|
||||
(tp, pcp, dcp, pp)
|
||||
for pp in range(self.pp_size)
|
||||
for dcp in range(self.dcp_size)
|
||||
for pcp in range(self.pcp_size)
|
||||
for tp in range(self.tp_size)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UMBPNamespace:
|
||||
"""Compatibility namespace for objects stored outside the engine."""
|
||||
|
||||
value: str
|
||||
|
||||
@classmethod
|
||||
def from_vllm_config(
|
||||
cls,
|
||||
vllm_config: Any,
|
||||
kv_cache_config: KVCacheConfig,
|
||||
) -> UMBPNamespace:
|
||||
extra = vllm_config.kv_transfer_config.kv_connector_extra_config
|
||||
configured = extra.get("key_namespace", "auto")
|
||||
if configured != "auto":
|
||||
if not isinstance(configured, str) or not configured:
|
||||
raise ValueError("key_namespace must be a non-empty string or 'auto'")
|
||||
return cls(configured)
|
||||
|
||||
model_config = vllm_config.model_config
|
||||
parallel_config = vllm_config.parallel_config
|
||||
# Draft-model KV (e.g. DSpark/EAGLE layers) is stored alongside the
|
||||
# target's, so a different drafter must not reuse it.
|
||||
speculative_config = getattr(vllm_config, "speculative_config", None)
|
||||
draft_model_config = getattr(speculative_config, "draft_model_config", None)
|
||||
speculative = (
|
||||
None
|
||||
if speculative_config is None
|
||||
else {
|
||||
"method": getattr(speculative_config, "method", None),
|
||||
"model": getattr(draft_model_config, "model", None),
|
||||
"revision": getattr(draft_model_config, "revision", None),
|
||||
}
|
||||
)
|
||||
fields = {
|
||||
"model": getattr(model_config, "model", ""),
|
||||
"revision": getattr(model_config, "revision", None),
|
||||
"block_size": vllm_config.cache_config.block_size,
|
||||
"cache_layout": getattr(kv_cache_config, "kv_cache_layout", None),
|
||||
"groups": [
|
||||
{
|
||||
"layers": group.layer_names,
|
||||
"block_size": group.kv_cache_spec.block_size,
|
||||
"kind": type(group.kv_cache_spec).__name__,
|
||||
"dtype": str(getattr(group.kv_cache_spec, "dtype", None)),
|
||||
}
|
||||
for group in kv_cache_config.kv_cache_groups
|
||||
],
|
||||
"tp": parallel_config.tensor_parallel_size,
|
||||
"pp": parallel_config.pipeline_parallel_size,
|
||||
"pcp": getattr(parallel_config, "prefill_context_parallel_size", 1),
|
||||
"dcp": getattr(parallel_config, "decode_context_parallel_size", 1),
|
||||
"layout_version": 1,
|
||||
}
|
||||
if speculative is not None:
|
||||
# Added only when set, so namespaces without a drafter are unchanged.
|
||||
fields["speculative"] = speculative
|
||||
payload = json.dumps(fields, sort_keys=True, separators=(",", ":")).encode()
|
||||
return cls(hashlib.sha256(payload).hexdigest()[:32])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BlockIdentityCodec:
|
||||
"""Build stable rank-local keys from vLLM block hashes."""
|
||||
|
||||
namespace: UMBPNamespace
|
||||
tp_rank: int = 0
|
||||
pp_rank: int = 0
|
||||
pcp_rank: int = 0
|
||||
dcp_rank: int = 0
|
||||
|
||||
def key(self, block_hash: bytes, group_id: int = 0) -> str:
|
||||
return self.key_for_namespace(
|
||||
block_hash,
|
||||
group_id,
|
||||
(self.tp_rank, self.pcp_rank, self.dcp_rank, self.pp_rank),
|
||||
)
|
||||
|
||||
def key_for_namespace(
|
||||
self,
|
||||
block_hash: bytes,
|
||||
group_id: int,
|
||||
namespace: tuple[int, int, int, int],
|
||||
) -> str:
|
||||
if not isinstance(block_hash, bytes) or not block_hash:
|
||||
raise ValueError("block_hash must be non-empty bytes")
|
||||
if group_id < 0:
|
||||
raise ValueError("group_id must be non-negative")
|
||||
tp_rank, pcp_rank, dcp_rank, pp_rank = namespace
|
||||
return (
|
||||
f"umbp:vllm:v1:{self.namespace.value}:"
|
||||
f"tp{tp_rank}:pcp{pcp_rank}:dcp{dcp_rank}:"
|
||||
f"pp{pp_rank}:g{group_id}:"
|
||||
f"{block_hash.hex()}"
|
||||
)
|
||||
|
||||
def keys_for_topology(
|
||||
self,
|
||||
block_hash: bytes,
|
||||
topology: RankTopology,
|
||||
group_ids: Sequence[int],
|
||||
) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
self.key_for_namespace(block_hash, group_id, namespace)
|
||||
for namespace in topology.all_namespaces()
|
||||
for group_id in group_ids
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KVRegion:
|
||||
"""One deterministic region in a rank-local KV object."""
|
||||
|
||||
layer_name: str
|
||||
group_id: int
|
||||
block_stride: int
|
||||
block_bytes: int
|
||||
object_offset: int
|
||||
block_size: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.block_stride <= 0 or self.block_bytes <= 0:
|
||||
raise ValueError("KV region sizes must be positive")
|
||||
if self.block_size <= 0:
|
||||
raise ValueError("KV region block_size must be positive")
|
||||
if self.block_bytes > self.block_stride:
|
||||
raise ValueError(
|
||||
f"KV region bytes ({self.block_bytes}) exceed its stride "
|
||||
f"({self.block_stride}) for {self.layer_name!r}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KVLayoutDescriptor:
|
||||
"""Backend-neutral description of one rank-local KV object layout."""
|
||||
|
||||
regions: tuple[KVRegion, ...]
|
||||
topology: RankTopology
|
||||
layout_format: str = "rank_local"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.layout_format:
|
||||
raise ValueError("layout_format must not be empty")
|
||||
names = [region.layer_name for region in self.regions]
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("KV layout contains duplicate layer regions")
|
||||
|
||||
@property
|
||||
def object_size(self) -> int:
|
||||
return sum(region.block_bytes for region in self.regions)
|
||||
|
||||
|
||||
class KVLayoutPlanner:
|
||||
"""Create a deterministic all-layer object layout from KVCacheConfig."""
|
||||
|
||||
def __init__(self, regions: Sequence[KVRegion]) -> None:
|
||||
self.regions = tuple(regions)
|
||||
self._regions_by_group = {
|
||||
group_id: tuple(r for r in self.regions if r.group_id == group_id)
|
||||
for group_id in {r.group_id for r in self.regions}
|
||||
}
|
||||
self._base_addresses: dict[str, int] = {}
|
||||
self._physical_strides: dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def from_kv_cache_config(cls, config: KVCacheConfig) -> KVLayoutPlanner:
|
||||
group_by_layer = {
|
||||
layer: group_id
|
||||
for group_id, group in enumerate(config.kv_cache_groups)
|
||||
for layer in group.layer_names
|
||||
if group.enable_kv_transfer
|
||||
}
|
||||
regions: list[KVRegion] = []
|
||||
offset = 0
|
||||
for tensor in config.kv_cache_tensors:
|
||||
for layer_name in tensor.layers:
|
||||
group_id = group_by_layer.get(layer_name)
|
||||
if group_id is None:
|
||||
continue
|
||||
spec = config.kv_cache_groups[group_id].kv_cache_spec
|
||||
block_bytes = spec.page_size_bytes
|
||||
regions.append(
|
||||
KVRegion(
|
||||
layer_name=layer_name,
|
||||
group_id=group_id,
|
||||
block_stride=tensor.block_stride,
|
||||
block_bytes=block_bytes,
|
||||
object_offset=offset,
|
||||
block_size=spec.block_size,
|
||||
)
|
||||
)
|
||||
offset += block_bytes
|
||||
return cls(regions)
|
||||
|
||||
@property
|
||||
def object_size(self) -> int:
|
||||
return sum(region.block_bytes for region in self.regions)
|
||||
|
||||
def describe(
|
||||
self,
|
||||
topology: RankTopology,
|
||||
layout_format: str = "rank_local",
|
||||
) -> KVLayoutDescriptor:
|
||||
return KVLayoutDescriptor(
|
||||
regions=self.regions,
|
||||
topology=topology,
|
||||
layout_format=layout_format,
|
||||
)
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]) -> None:
|
||||
"""Capture stable base addresses for the planned KV regions."""
|
||||
required = {region.layer_name for region in self.regions}
|
||||
missing = required - kv_caches.keys()
|
||||
if missing:
|
||||
raise ValueError(f"missing KV cache layers: {sorted(missing)}")
|
||||
addresses: dict[str, int] = {}
|
||||
for layer_name in required:
|
||||
cache = kv_caches[layer_name]
|
||||
if cache.ndim == 0:
|
||||
raise ValueError(f"KV cache {layer_name!r} must be a tensor")
|
||||
block_stride = cache.stride(0) * cache.element_size()
|
||||
addresses[layer_name] = cache.data_ptr()
|
||||
for region in self.regions:
|
||||
if region.layer_name != layer_name:
|
||||
continue
|
||||
if (
|
||||
block_stride > region.block_stride
|
||||
or region.block_stride % block_stride != 0
|
||||
or (
|
||||
region.block_bytes > block_stride
|
||||
and region.block_bytes % block_stride != 0
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
f"KV block stride mismatch for {layer_name!r}: "
|
||||
f"config={region.block_stride}, tensor={block_stride}"
|
||||
)
|
||||
self._physical_strides[layer_name] = block_stride
|
||||
self._base_addresses = addresses
|
||||
|
||||
def plan_registered_block(
|
||||
self,
|
||||
key: str,
|
||||
block_id: int,
|
||||
*,
|
||||
group_id: int | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> BlockTransferPlan:
|
||||
if not self._base_addresses:
|
||||
raise RuntimeError("KV caches must be registered before planning")
|
||||
return self.plan_for_block(
|
||||
key,
|
||||
block_id,
|
||||
self._base_addresses,
|
||||
group_id=group_id,
|
||||
request_id=request_id,
|
||||
)
|
||||
|
||||
def plan_for_block(
|
||||
self,
|
||||
key: str,
|
||||
block_id: int,
|
||||
base_addresses: dict[str, int],
|
||||
*,
|
||||
group_id: int | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> BlockTransferPlan:
|
||||
"""Lower one GPU block into a deterministic scatter/gather plan."""
|
||||
return BlockTransferPlan(
|
||||
key=key,
|
||||
block_id=block_id,
|
||||
ranges=self._ranges_for_block(block_id, base_addresses, group_id),
|
||||
request_id=request_id,
|
||||
)
|
||||
|
||||
def materialize(self, plan: BlockTransferPlan) -> BlockTransferPlan:
|
||||
if not self._base_addresses:
|
||||
raise RuntimeError("KV caches must be registered before planning")
|
||||
return replace(
|
||||
plan,
|
||||
ranges=self._ranges_for_block(
|
||||
plan.block_id, self._base_addresses, plan.group_id
|
||||
),
|
||||
)
|
||||
|
||||
def _ranges_for_block(
|
||||
self,
|
||||
block_id: int,
|
||||
base_addresses: dict[str, int],
|
||||
group_id: int | None,
|
||||
) -> tuple[KVRange, ...]:
|
||||
if block_id < 0:
|
||||
raise ValueError("block_id must be non-negative")
|
||||
regions = (
|
||||
self.regions
|
||||
if group_id is None
|
||||
else self._regions_by_group.get(group_id, ())
|
||||
)
|
||||
if group_id is not None and not regions:
|
||||
raise ValueError(f"KV layout does not contain cache group {group_id}")
|
||||
ranges: list[KVRange] = []
|
||||
object_offset = 0
|
||||
for region in regions:
|
||||
try:
|
||||
base_address = base_addresses[region.layer_name]
|
||||
except KeyError as exc:
|
||||
raise ValueError(
|
||||
f"missing base address for layer {region.layer_name!r}"
|
||||
) from exc
|
||||
physical_stride = self._physical_strides.get(
|
||||
region.layer_name, region.block_stride
|
||||
)
|
||||
if region.block_bytes <= physical_stride:
|
||||
ranges.append(
|
||||
KVRange(
|
||||
layer_name=region.layer_name,
|
||||
group_id=region.group_id,
|
||||
block_id=block_id,
|
||||
base_address=base_address + block_id * physical_stride,
|
||||
stride=physical_stride,
|
||||
length=region.block_bytes,
|
||||
object_offset=(
|
||||
object_offset
|
||||
if group_id is not None
|
||||
else region.object_offset
|
||||
),
|
||||
)
|
||||
)
|
||||
object_offset += region.block_bytes
|
||||
continue
|
||||
physical_parts = max(1, region.block_bytes // physical_stride)
|
||||
for part in range(physical_parts):
|
||||
part_start = part * physical_stride
|
||||
part_end = min(part_start + physical_stride, region.block_bytes)
|
||||
physical_block_id = block_id * physical_parts + part
|
||||
ranges.append(
|
||||
KVRange(
|
||||
layer_name=region.layer_name,
|
||||
group_id=region.group_id,
|
||||
block_id=block_id,
|
||||
base_address=base_address + physical_block_id * physical_stride,
|
||||
stride=physical_stride,
|
||||
length=part_end - part_start,
|
||||
object_offset=(
|
||||
object_offset + part_start
|
||||
if group_id is not None
|
||||
else region.object_offset + part * physical_stride
|
||||
),
|
||||
)
|
||||
)
|
||||
object_offset += region.block_bytes
|
||||
return tuple(ranges)
|
||||
|
||||
|
||||
class KVRange(NamedTuple):
|
||||
"""A source/destination range in a runtime transfer."""
|
||||
|
||||
layer_name: str
|
||||
group_id: int
|
||||
block_id: int
|
||||
base_address: int
|
||||
stride: int
|
||||
length: int
|
||||
object_offset: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BlockTransferPlan:
|
||||
"""One complete logical block transfer."""
|
||||
|
||||
key: str
|
||||
block_id: int
|
||||
ranges: tuple[KVRange, ...] = ()
|
||||
request_id: str | None = None
|
||||
group_id: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadSpec:
|
||||
"""Scheduler decision for one externally loaded prefix."""
|
||||
|
||||
local_tokens: int
|
||||
external_tokens: int
|
||||
|
||||
@property
|
||||
def num_tokens_to_load(self) -> int:
|
||||
return max(self.external_tokens - self.local_tokens, 0)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestTracker:
|
||||
"""Core request state shared by every runtime mode."""
|
||||
|
||||
saved_tokens: int = 0
|
||||
block_ids: tuple[list[int], ...] = ()
|
||||
load_spec: LoadSpec | None = None
|
||||
|
||||
def reset(self) -> None:
|
||||
self.saved_tokens = 0
|
||||
self.block_ids = ()
|
||||
self.load_spec = None
|
||||
|
||||
def update_blocks(self, block_ids: tuple[list[int], ...]) -> None:
|
||||
if not self.block_ids:
|
||||
self.block_ids = tuple(list(group) for group in block_ids)
|
||||
return
|
||||
if len(self.block_ids) != len(block_ids):
|
||||
raise ValueError("KV cache group count changed for a request")
|
||||
for current, new in zip(self.block_ids, block_ids, strict=True):
|
||||
if new[: len(current)] == current:
|
||||
current.extend(new[len(current) :])
|
||||
elif not new or current[-len(new) :] == new:
|
||||
continue
|
||||
else:
|
||||
current.extend(new)
|
||||
|
||||
def replace_blocks(self, block_ids: tuple[list[int], ...]) -> None:
|
||||
"""Replace stale pre-preemption tables with resumed full tables."""
|
||||
self.block_ids = tuple(list(group) for group in block_ids)
|
||||
self.saved_tokens = 0
|
||||
|
||||
def mark_saved(self, token_count: int, block_size: int) -> int:
|
||||
"""Advance the save watermark only to a complete block boundary."""
|
||||
complete_tokens = token_count // block_size * block_size
|
||||
if complete_tokens < self.saved_tokens:
|
||||
return self.saved_tokens
|
||||
self.saved_tokens = complete_tokens
|
||||
return complete_tokens
|
||||
|
||||
|
||||
class TransferJobStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransferJobState:
|
||||
"""Per-key completion state; one failed key does not fail its siblings."""
|
||||
|
||||
plans: tuple[BlockTransferPlan, ...]
|
||||
status: TransferJobStatus = TransferJobStatus.PENDING
|
||||
completed_keys: set[str] = field(default_factory=set)
|
||||
failed_keys: set[str] = field(default_factory=set)
|
||||
error: str | None = None
|
||||
|
||||
@property
|
||||
def keys(self) -> Sequence[str]:
|
||||
return tuple(plan.key for plan in self.plans)
|
||||
|
||||
def start(self) -> None:
|
||||
if self.status != TransferJobStatus.PENDING:
|
||||
raise RuntimeError(f"cannot start job in state {self.status}")
|
||||
self.status = TransferJobStatus.RUNNING
|
||||
|
||||
def complete(self, keys: Sequence[str] = ()) -> None:
|
||||
self.completed_keys.update(keys or self.keys)
|
||||
self._finish_if_done()
|
||||
|
||||
def fail(self, keys: Sequence[str], error: str | None = None) -> None:
|
||||
self.failed_keys.update(keys)
|
||||
self.error = error
|
||||
self._finish_if_done()
|
||||
|
||||
@property
|
||||
def failed_block_ids(self) -> set[int]:
|
||||
return {plan.block_id for plan in self.plans if plan.key in self.failed_keys}
|
||||
|
||||
def _finish_if_done(self) -> None:
|
||||
keys = set(self.keys)
|
||||
if self.completed_keys | self.failed_keys >= keys:
|
||||
self.status = (
|
||||
TransferJobStatus.FAILED
|
||||
if self.failed_keys
|
||||
else TransferJobStatus.COMPLETED
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UMBPConnectorMetadata(KVConnectorMetadata):
|
||||
"""Scheduler-to-worker metadata for one engine step."""
|
||||
|
||||
async_load: bool = False
|
||||
store_plans: list[BlockTransferPlan] = field(default_factory=list)
|
||||
load_requests: dict[str, list[BlockTransferPlan]] = field(default_factory=dict)
|
||||
store_requests: dict[str, list[BlockTransferPlan]] = field(default_factory=dict)
|
||||
preempted_request_ids: set[str] = field(default_factory=set)
|
||||
store_event: int = -1
|
||||
|
||||
|
||||
@dataclass
|
||||
class StoreEventResult:
|
||||
"""Number of workers that finished one store event."""
|
||||
|
||||
completed_workers: int = 0
|
||||
|
||||
def merge(self, other: StoreEventResult) -> None:
|
||||
self.completed_workers += other.completed_workers
|
||||
|
||||
|
||||
@dataclass
|
||||
class UMBPConnectorWorkerMetadata(KVConnectorWorkerMetadata):
|
||||
"""Worker completion metadata aggregated across ranks."""
|
||||
|
||||
store_events: dict[int, StoreEventResult] = field(default_factory=dict)
|
||||
|
||||
def aggregate(
|
||||
self, other: KVConnectorWorkerMetadata
|
||||
) -> UMBPConnectorWorkerMetadata:
|
||||
if not isinstance(other, UMBPConnectorWorkerMetadata):
|
||||
raise TypeError("cannot aggregate incompatible UMBP worker metadata")
|
||||
for event, result in other.store_events.items():
|
||||
self.store_events.setdefault(event, StoreEventResult()).merge(result)
|
||||
return self
|
||||
@@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from .base import IUMBPRuntime, UMBPSchedulerHandle, UMBPWorkerHandle
|
||||
from .factory import UMBPRuntimeConfig, UMBPRuntimeFactory
|
||||
|
||||
__all__ = [
|
||||
"IUMBPRuntime",
|
||||
"UMBPSchedulerHandle",
|
||||
"UMBPWorkerHandle",
|
||||
"UMBPRuntimeConfig",
|
||||
"UMBPRuntimeFactory",
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Backend-neutral runtime contract for UMBP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Protocol
|
||||
|
||||
import torch
|
||||
|
||||
from ..data import (
|
||||
BlockTransferPlan,
|
||||
KVLayoutDescriptor,
|
||||
RankTopology,
|
||||
TransferJobState,
|
||||
)
|
||||
|
||||
|
||||
class UMBPSchedulerHandle(Protocol):
|
||||
def lookup(self, keys: Sequence[str]) -> Sequence[bool]:
|
||||
"""Return authoritative existence for each key."""
|
||||
|
||||
def clear(self) -> bool:
|
||||
"""Clear all objects visible to this scheduler handle."""
|
||||
|
||||
def close(self) -> None:
|
||||
"""Release scheduler-side resources."""
|
||||
|
||||
|
||||
class UMBPWorkerHandle(Protocol):
|
||||
def register_buffers(self, kv_caches: dict[str, torch.Tensor]) -> None:
|
||||
"""Register each unique KV allocation with the runtime."""
|
||||
|
||||
def load(self, plans: Sequence[BlockTransferPlan]) -> TransferJobState:
|
||||
"""Start loading the requested plans."""
|
||||
|
||||
def store(self, plans: Sequence[BlockTransferPlan]) -> TransferJobState:
|
||||
"""Start storing the requested plans."""
|
||||
|
||||
def wait(self, job: TransferJobState) -> TransferJobState:
|
||||
"""Block until the job is final and its buffers are safe to reuse."""
|
||||
|
||||
def poll(self, job: TransferJobState) -> TransferJobState | None:
|
||||
"""Return a finished job without blocking, or None if still pending."""
|
||||
|
||||
def publish(self, job: TransferJobState) -> None:
|
||||
"""Accept a completed store; a runtime may defer visibility until then."""
|
||||
|
||||
def close(self) -> None:
|
||||
"""Release worker-side resources."""
|
||||
|
||||
|
||||
class IUMBPRuntime(Protocol):
|
||||
def create_scheduler_handle(
|
||||
self,
|
||||
namespace: str,
|
||||
topology: RankTopology,
|
||||
layout: KVLayoutDescriptor,
|
||||
) -> UMBPSchedulerHandle:
|
||||
"""Create a scheduler-side handle."""
|
||||
|
||||
def create_worker_handle(
|
||||
self,
|
||||
namespace: str,
|
||||
topology: RankTopology,
|
||||
layout: KVLayoutDescriptor,
|
||||
) -> UMBPWorkerHandle:
|
||||
"""Create a worker-side handle."""
|
||||
@@ -0,0 +1,76 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Runtime configuration and adapter factory for UMBP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
|
||||
from vllm.config import VllmConfig
|
||||
|
||||
from .base import IUMBPRuntime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UMBPRuntimeConfig:
|
||||
"""Shared connector options passed to the selected runtime adapter."""
|
||||
|
||||
mode: str
|
||||
options: dict[str, Any]
|
||||
rank_count: int = 1
|
||||
dp_index: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_vllm(cls, vllm_config: VllmConfig) -> UMBPRuntimeConfig:
|
||||
transfer_config = vllm_config.kv_transfer_config
|
||||
if transfer_config is None:
|
||||
raise ValueError("UMBP requires kv_transfer_config")
|
||||
options = dict(transfer_config.kv_connector_extra_config)
|
||||
mode = options.get("mode", "embedded")
|
||||
if mode not in {"embedded"}:
|
||||
raise ValueError(f"unknown UMBP mode: {mode!r}")
|
||||
namespace = options.get("key_namespace", "auto")
|
||||
if namespace != "auto" and (not isinstance(namespace, str) or not namespace):
|
||||
raise ValueError("key_namespace must be a non-empty string or 'auto'")
|
||||
return cls(
|
||||
mode=mode,
|
||||
options=options,
|
||||
dp_index=int(
|
||||
getattr(vllm_config.parallel_config, "data_parallel_index", 0) or 0
|
||||
),
|
||||
)
|
||||
|
||||
def resolve_for_rank_count(self, rank_count: int) -> UMBPRuntimeConfig:
|
||||
"""Pass topology to the adapter without interpreting its capacity options."""
|
||||
if rank_count <= 0:
|
||||
raise ValueError("rank_count must be positive")
|
||||
return replace(self, rank_count=rank_count)
|
||||
|
||||
|
||||
RuntimeBuilder = Callable[[UMBPRuntimeConfig], IUMBPRuntime]
|
||||
|
||||
|
||||
class UMBPRuntimeFactory:
|
||||
"""Registry used by UMBP adapters without importing MORI in vLLM."""
|
||||
|
||||
_builders: dict[str, RuntimeBuilder] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, mode: str, builder: RuntimeBuilder) -> None:
|
||||
if mode not in {"embedded"}:
|
||||
raise ValueError(f"unknown UMBP mode: {mode!r}")
|
||||
if mode in cls._builders:
|
||||
raise ValueError(f"UMBP runtime mode {mode!r} is already registered")
|
||||
cls._builders[mode] = builder
|
||||
|
||||
@classmethod
|
||||
def build(cls, config: UMBPRuntimeConfig) -> IUMBPRuntime:
|
||||
try:
|
||||
builder = cls._builders[config.mode]
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"no UMBP runtime adapter is registered for mode {config.mode!r}"
|
||||
) from exc
|
||||
return builder(config)
|
||||
@@ -0,0 +1,518 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Shared scheduler-side UMBP connector behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import KVConnectorMetadata
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
|
||||
from vllm.v1.core.block_pool import BlockPool
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
|
||||
from vllm.v1.core.kv_cache_utils import (
|
||||
resolve_dcp_kv_block_size,
|
||||
resolve_kv_cache_block_sizes,
|
||||
)
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
KVCacheConfig,
|
||||
)
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
from vllm.v1.request import Request
|
||||
|
||||
from .data import (
|
||||
BlockIdentityCodec,
|
||||
BlockTransferPlan,
|
||||
LoadSpec,
|
||||
RankTopology,
|
||||
RequestTracker,
|
||||
StoreEventResult,
|
||||
UMBPConnectorMetadata,
|
||||
UMBPConnectorWorkerMetadata,
|
||||
)
|
||||
from .runtime import UMBPSchedulerHandle
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8192)
|
||||
def _rank_keys(
|
||||
codec: BlockIdentityCodec,
|
||||
topology: RankTopology,
|
||||
block_hash: bytes,
|
||||
group_id: int,
|
||||
) -> tuple[str, ...]:
|
||||
"""Reuse immutable object identities, not runtime residency."""
|
||||
return codec.keys_for_topology(block_hash, topology, (group_id,))
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StoreEvent:
|
||||
plans: tuple[BlockTransferPlan, ...]
|
||||
result: StoreEventResult = field(default_factory=StoreEventResult)
|
||||
|
||||
|
||||
class UMBPStoreConnectorScheduler:
|
||||
"""Own vLLM prefix matching while the runtime owns lookup transport."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
kv_cache_config: KVCacheConfig,
|
||||
runtime: UMBPSchedulerHandle,
|
||||
codec: BlockIdentityCodec,
|
||||
topology: RankTopology | None = None,
|
||||
) -> None:
|
||||
self.block_size, resolved_hash_block_size = resolve_kv_cache_block_sizes(
|
||||
kv_cache_config, vllm_config
|
||||
)
|
||||
self.kv_cache_config = kv_cache_config
|
||||
dcp_size = vllm_config.parallel_config.decode_context_parallel_size
|
||||
self.group_block_sizes = {
|
||||
group_id: resolve_dcp_kv_block_size(group.kv_cache_spec, dcp_size)
|
||||
for group_id, group in enumerate(kv_cache_config.kv_cache_groups)
|
||||
}
|
||||
self.runtime = runtime
|
||||
self.codec = codec
|
||||
self.topology = topology or RankTopology()
|
||||
transfer_config = vllm_config.kv_transfer_config
|
||||
assert transfer_config is not None
|
||||
extra = transfer_config.kv_connector_extra_config
|
||||
self.load_async = bool(extra.get("load_async", True))
|
||||
self.enable_lookup = bool(extra.get("enable_lookup", True))
|
||||
self.save_decode_cache = bool(extra.get("save_decode_cache", False))
|
||||
# A kv_consumer only restores from the pool, as with other store
|
||||
# connectors; it never offloads its own KV.
|
||||
self.store_enabled = getattr(transfer_config, "kv_role", None) != "kv_consumer"
|
||||
# Request.block_hashes are computed at this size; keys index into them.
|
||||
self.hash_block_size = resolved_hash_block_size
|
||||
speculative_config = getattr(vllm_config, "speculative_config", None)
|
||||
self.use_eagle = bool(
|
||||
speculative_config is not None and speculative_config.use_eagle_block_drop()
|
||||
)
|
||||
self._pending_loads: dict[str, list[BlockTransferPlan]] = {}
|
||||
self._load_specs: dict[str, LoadSpec] = {}
|
||||
self._lookup_units = {
|
||||
group_id: self.group_block_sizes[group_id]
|
||||
for group_id in kv_cache_config.prefix_cacheable_group_ids
|
||||
}
|
||||
self._requests: dict[str, Request] = {}
|
||||
self._request_trackers: dict[str, RequestTracker] = {}
|
||||
self._gpu_block_pool: BlockPool | None = None
|
||||
self._store_events: dict[int, _StoreEvent] = {}
|
||||
self._num_workers = getattr(vllm_config.parallel_config, "world_size", 1)
|
||||
self._store_event_counter = 0
|
||||
|
||||
def bind_gpu_block_pool(self, gpu_block_pool: BlockPool) -> None:
|
||||
self._gpu_block_pool = gpu_block_pool
|
||||
|
||||
def get_num_new_matched_tokens(
|
||||
self, request: Request, num_computed_tokens: int
|
||||
) -> tuple[int | None, bool]:
|
||||
if not self.enable_lookup:
|
||||
return 0, False
|
||||
hashes = request.block_hashes
|
||||
if not hashes or request.num_tokens < self.block_size:
|
||||
return 0, False
|
||||
if num_computed_tokens % self.block_size != 0:
|
||||
return 0, False
|
||||
group_ids = self.kv_cache_config.prefix_cacheable_group_ids
|
||||
# vLLM must execute at least the final prompt token. Returning a hit
|
||||
# for the entire prompt leaves the scheduler with no new token to run.
|
||||
max_external_token = request.num_tokens - 1
|
||||
if max_external_token <= num_computed_tokens:
|
||||
self._load_specs.pop(request.request_id, None)
|
||||
return 0, False
|
||||
logical_objects_by_group, keys = self._lookup_objects(
|
||||
request, num_computed_tokens, hashes
|
||||
)
|
||||
try:
|
||||
hits = list(self.runtime.lookup(keys))
|
||||
except Exception as exc:
|
||||
logger.debug("UMBP lookup failed request=%s: %s", request.request_id, exc)
|
||||
return 0, False
|
||||
if len(hits) != len(keys):
|
||||
logger.debug(
|
||||
"UMBP lookup returned an invalid result length request=%s",
|
||||
request.request_id,
|
||||
)
|
||||
return 0, False
|
||||
per_rank = self.topology.rank_count
|
||||
offset = 0
|
||||
logical_hits_by_group: dict[int, list[bool]] = {}
|
||||
for group_id in group_ids:
|
||||
group_size = len(logical_objects_by_group[group_id]) * per_rank
|
||||
group_hits = hits[offset : offset + group_size]
|
||||
offset += group_size
|
||||
logical_hits_by_group[group_id] = (
|
||||
group_hits
|
||||
if per_rank == 1
|
||||
else [
|
||||
all(group_hits[index : index + per_rank])
|
||||
for index in range(0, len(group_hits), per_rank)
|
||||
]
|
||||
)
|
||||
group_id = group_ids[0]
|
||||
matched_blocks = 0
|
||||
for hit in logical_hits_by_group[group_id]:
|
||||
if not hit:
|
||||
break
|
||||
matched_blocks += 1
|
||||
if self.use_eagle:
|
||||
# As core's coordinator: the drafter recomputes the last block.
|
||||
matched_blocks = max(0, matched_blocks - 1)
|
||||
need_to_load = matched_blocks * self._lookup_units[group_id]
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"UMBP lookup result request=%s keys=%s group_hits=%s load_tokens=%d",
|
||||
request.request_id,
|
||||
keys,
|
||||
logical_hits_by_group,
|
||||
need_to_load,
|
||||
)
|
||||
matched_tokens = num_computed_tokens + need_to_load
|
||||
if need_to_load <= 0:
|
||||
return 0, False
|
||||
self._load_specs[request.request_id] = LoadSpec(
|
||||
local_tokens=num_computed_tokens,
|
||||
external_tokens=matched_tokens,
|
||||
)
|
||||
return need_to_load, self.load_async
|
||||
|
||||
def _lookup_objects(
|
||||
self, request: Request, local_tokens: int, hashes: Sequence[bytes]
|
||||
) -> tuple[dict[int, list[tuple[bytes, tuple[str, ...]]]], list[str]]:
|
||||
objects: dict[int, list[tuple[bytes, tuple[str, ...]]]] = {}
|
||||
keys: list[str] = []
|
||||
for group_id, unit in self._lookup_units.items():
|
||||
group_objects = []
|
||||
for token_end in range(local_tokens + unit, request.num_tokens, unit):
|
||||
block_hash = self._object_hash_at_token_end(hashes, token_end)
|
||||
rank_keys = _rank_keys(self.codec, self.topology, block_hash, group_id)
|
||||
group_objects.append((block_hash, rank_keys))
|
||||
keys.extend(rank_keys)
|
||||
objects[group_id] = group_objects
|
||||
return objects, keys
|
||||
|
||||
def update_state_after_alloc(
|
||||
self,
|
||||
request: Request,
|
||||
blocks: KVCacheBlocks,
|
||||
num_external_tokens: int,
|
||||
) -> None:
|
||||
self._requests[request.request_id] = request
|
||||
block_groups = blocks.get_block_ids(
|
||||
group_ids=self.kv_cache_config.prefix_cacheable_group_ids
|
||||
)
|
||||
if not block_groups:
|
||||
return
|
||||
tracker = self._tracker_for_request(request.request_id)
|
||||
# Core passes the request's whole table on every admission. A request
|
||||
# whose load failed is recomputed into newly allocated blocks.
|
||||
tracker.replace_blocks(tuple(list(group) for group in block_groups))
|
||||
if num_external_tokens <= 0:
|
||||
self._pending_loads.pop(request.request_id, None)
|
||||
self._load_specs.pop(request.request_id, None)
|
||||
return
|
||||
spec = self._load_specs.get(request.request_id)
|
||||
if spec is not None:
|
||||
tracker.load_spec = spec
|
||||
# A request admitted without its load would wait for it forever.
|
||||
assert num_external_tokens == spec.num_tokens_to_load, (
|
||||
f"UMBP matched {spec.num_tokens_to_load} external tokens for "
|
||||
f"request {request.request_id} but {num_external_tokens} were "
|
||||
"allocated"
|
||||
)
|
||||
plans = self._load_plans_for_external_tokens(
|
||||
request,
|
||||
tracker,
|
||||
block_groups,
|
||||
num_external_tokens,
|
||||
)
|
||||
self._pending_loads[request.request_id] = plans
|
||||
|
||||
def build_connector_meta(
|
||||
self, scheduler_output: SchedulerOutput
|
||||
) -> KVConnectorMetadata:
|
||||
meta = UMBPConnectorMetadata(async_load=self.load_async)
|
||||
for request_id in scheduler_output.finished_req_ids:
|
||||
self._pending_loads.pop(request_id, None)
|
||||
self._load_specs.pop(request_id, None)
|
||||
self._request_trackers.pop(request_id, None)
|
||||
self._requests.pop(request_id, None)
|
||||
for request in scheduler_output.scheduled_new_reqs:
|
||||
load_plans = self._pending_loads.pop(request.req_id, [])
|
||||
self._load_specs.pop(request.req_id, None)
|
||||
if load_plans:
|
||||
meta.load_requests[request.req_id] = load_plans
|
||||
else:
|
||||
tracker = self._tracker_for_request(request.req_id)
|
||||
total_tokens = (
|
||||
request.num_computed_tokens
|
||||
+ scheduler_output.num_scheduled_tokens[request.req_id]
|
||||
)
|
||||
tracked_request = self._requests.get(request.req_id)
|
||||
selected_block_ids = tuple(
|
||||
request.block_ids[group_id]
|
||||
for group_id in (self.kv_cache_config.prefix_cacheable_group_ids)
|
||||
)
|
||||
store_plans = (
|
||||
[]
|
||||
if not self.store_enabled or tracked_request is None
|
||||
else self._store_plans(
|
||||
tracked_request,
|
||||
tracker,
|
||||
total_tokens,
|
||||
block_ids_override=selected_block_ids,
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
"UMBP store planning request=%s tokens=%d plans=%d keys=%s",
|
||||
request.req_id,
|
||||
total_tokens,
|
||||
len(store_plans),
|
||||
tuple(plan.key for plan in store_plans),
|
||||
)
|
||||
meta.store_plans.extend(store_plans)
|
||||
if store_plans:
|
||||
meta.store_requests[request.req_id] = store_plans
|
||||
|
||||
block_state = getattr(scheduler_output, "kv_connector_block_state", None)
|
||||
cached_reqs = scheduler_output.scheduled_cached_reqs
|
||||
resumed_ids: set[str] = set(getattr(cached_reqs, "resumed_req_ids", ()))
|
||||
for request_id in cached_reqs.req_ids:
|
||||
load_plans = self._pending_loads.pop(request_id, [])
|
||||
self._load_specs.pop(request_id, None)
|
||||
if load_plans:
|
||||
meta.load_requests[request_id] = load_plans
|
||||
elif self.store_enabled:
|
||||
cached_request = self._requests.get(request_id)
|
||||
cached_tracker = self._request_trackers.get(request_id)
|
||||
if cached_request is not None and cached_tracker is not None:
|
||||
request_index = cached_reqs.req_ids.index(request_id)
|
||||
num_computed_tokens = cached_reqs.num_computed_tokens[request_index]
|
||||
# num_tokens counts sampled tokens, so a decode step is still
|
||||
# short of it; only the prompt bounds the prefill.
|
||||
is_prefill = num_computed_tokens < cached_request.num_prompt_tokens
|
||||
if not is_prefill and not self.save_decode_cache:
|
||||
continue
|
||||
new_block_ids = cached_reqs.new_block_ids[request_index]
|
||||
if new_block_ids:
|
||||
selected = tuple(
|
||||
new_block_ids[group_id]
|
||||
for group_id in (
|
||||
self.kv_cache_config.prefix_cacheable_group_ids
|
||||
)
|
||||
)
|
||||
if request_id in resumed_ids:
|
||||
cached_tracker.replace_blocks(selected)
|
||||
else:
|
||||
cached_tracker.update_blocks(selected)
|
||||
# The appended table keeps blocks core has since freed, such
|
||||
# as sliding-window blocks out of the window, which a store
|
||||
# would read; core's current table does not.
|
||||
current = (
|
||||
block_state.get_block_ids(request_id)
|
||||
if block_state is not None
|
||||
else None
|
||||
)
|
||||
if current is not None:
|
||||
cached_tracker.block_ids = tuple(
|
||||
list(current[group_id])
|
||||
for group_id in (
|
||||
self.kv_cache_config.prefix_cacheable_group_ids
|
||||
)
|
||||
)
|
||||
total_tokens = (
|
||||
num_computed_tokens
|
||||
+ scheduler_output.num_scheduled_tokens[request_id]
|
||||
)
|
||||
if not self.save_decode_cache:
|
||||
total_tokens = min(
|
||||
total_tokens, cached_request.num_prompt_tokens
|
||||
)
|
||||
store_plans = self._store_plans(
|
||||
cached_request,
|
||||
cached_tracker,
|
||||
total_tokens,
|
||||
block_ids_override=cached_tracker.block_ids,
|
||||
)
|
||||
meta.store_plans.extend(store_plans)
|
||||
if store_plans:
|
||||
meta.store_requests[request_id] = store_plans
|
||||
|
||||
# Async loads are admitted without scheduling model tokens, so those
|
||||
# requests are absent from both scheduled request collections above.
|
||||
# Forward their plans explicitly so workers can start the transfer and
|
||||
# eventually move the request out of WAITING_FOR_REMOTE_KVS.
|
||||
for request_id, load_plans in list(self._pending_loads.items()):
|
||||
self._pending_loads.pop(request_id, None)
|
||||
self._load_specs.pop(request_id, None)
|
||||
if load_plans:
|
||||
meta.load_requests[request_id] = load_plans
|
||||
|
||||
for request_id in scheduler_output.preempted_req_ids or set():
|
||||
meta.preempted_request_ids.add(request_id)
|
||||
self._load_specs.pop(request_id, None)
|
||||
if preempted_tracker := self._request_trackers.get(request_id):
|
||||
preempted_tracker.reset()
|
||||
|
||||
event_plans = tuple(meta.store_plans)
|
||||
if event_plans:
|
||||
event = self._store_event_counter
|
||||
self._store_event_counter += 1
|
||||
meta.store_event = event
|
||||
self._store_events[event] = _StoreEvent(event_plans)
|
||||
if self._gpu_block_pool is not None:
|
||||
self._gpu_block_pool.touch(
|
||||
[
|
||||
self._gpu_block_pool.blocks[block_id]
|
||||
for block_id in dict.fromkeys(
|
||||
plan.block_id for plan in event_plans
|
||||
)
|
||||
]
|
||||
)
|
||||
return meta
|
||||
|
||||
def _tracker_for_request(self, request_id: str) -> RequestTracker:
|
||||
return self._request_trackers.setdefault(request_id, RequestTracker())
|
||||
|
||||
def _load_plans_for_external_tokens(
|
||||
self,
|
||||
request: Any,
|
||||
tracker: RequestTracker,
|
||||
block_groups: tuple[list[int], ...],
|
||||
num_external_tokens: int,
|
||||
) -> list[BlockTransferPlan]:
|
||||
"""Plan the logical objects selected by the external hit window."""
|
||||
group_ids = self.kv_cache_config.prefix_cacheable_group_ids
|
||||
spec = tracker.load_spec
|
||||
local_tokens = spec.local_tokens if spec is not None else 0
|
||||
end_tokens = local_tokens + num_external_tokens
|
||||
plans: list[BlockTransferPlan] = []
|
||||
for group_index, group_id in enumerate(group_ids):
|
||||
group_block_size = self.group_block_sizes[group_id]
|
||||
start_block = local_tokens // group_block_size
|
||||
num_full_blocks = end_tokens // group_block_size
|
||||
for block_index in range(start_block, num_full_blocks):
|
||||
token_end = (block_index + 1) * group_block_size
|
||||
block_hash = self._object_hash_at_token_end(
|
||||
request.block_hashes, token_end
|
||||
)
|
||||
plans.append(
|
||||
BlockTransferPlan(
|
||||
key=self.codec.key(block_hash, group_id=group_id),
|
||||
block_id=block_groups[group_index][block_index],
|
||||
request_id=request.request_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
)
|
||||
return plans
|
||||
|
||||
def _object_hash_at_token_end(
|
||||
self, hashes: Sequence[bytes], token_end: int
|
||||
) -> bytes:
|
||||
hash_index = token_end // self.hash_block_size - 1
|
||||
if hash_index < 0 or hash_index >= len(hashes):
|
||||
raise ValueError(
|
||||
f"request has {len(hashes)} hashes, needs index {hash_index}"
|
||||
)
|
||||
return hashes[hash_index]
|
||||
|
||||
def _store_plans(
|
||||
self,
|
||||
request: Any,
|
||||
tracker: RequestTracker,
|
||||
token_count: int,
|
||||
block_ids_override: tuple[list[int], ...] | None = None,
|
||||
) -> list[BlockTransferPlan]:
|
||||
"""Describe full blocks produced by a scheduled prefill."""
|
||||
request_id = getattr(request, "request_id", None) or request.req_id
|
||||
group_ids = self.kv_cache_config.prefix_cacheable_group_ids
|
||||
# Scheduled speculative tokens are not yet verified and have no block
|
||||
# hash; a block is storable only once its hash exists.
|
||||
token_count = min(token_count, len(request.block_hashes) * self.hash_block_size)
|
||||
previous_saved_tokens = tracker.saved_tokens
|
||||
# Hash-aligned rather than block-aligned: a group with smaller blocks
|
||||
# than the largest must not wait for that group's block to fill.
|
||||
save_to = tracker.mark_saved(token_count, self.hash_block_size)
|
||||
plans: list[BlockTransferPlan] = []
|
||||
block_groups = (
|
||||
block_ids_override
|
||||
if block_ids_override is not None
|
||||
else tuple(request.block_ids[group_id] for group_id in group_ids)
|
||||
)
|
||||
block_ranges = {
|
||||
group_id: range(
|
||||
previous_saved_tokens // self.group_block_sizes[group_id],
|
||||
min(save_to // self.group_block_sizes[group_id], len(block_ids)),
|
||||
)
|
||||
for group_id, block_ids in zip(group_ids, block_groups)
|
||||
}
|
||||
for group_id, block_ids in zip(group_ids, block_groups):
|
||||
if group_id not in block_ranges:
|
||||
continue
|
||||
group_block_size = self.group_block_sizes[group_id]
|
||||
for index in block_ranges[group_id]:
|
||||
if block_ids[index] == NULL_BLOCK_ID:
|
||||
continue
|
||||
token_end = (index + 1) * group_block_size
|
||||
block_hash = self._object_hash_at_token_end(
|
||||
request.block_hashes, token_end
|
||||
)
|
||||
plans.append(
|
||||
BlockTransferPlan(
|
||||
key=self.codec.key(block_hash, group_id=group_id),
|
||||
block_id=block_ids[index],
|
||||
request_id=request_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
)
|
||||
return plans
|
||||
|
||||
def request_finished(
|
||||
self, request: Request, block_ids: tuple[list[int], ...]
|
||||
) -> tuple[bool, dict[str, Any] | None]:
|
||||
self._pending_loads.pop(request.request_id, None)
|
||||
self._load_specs.pop(request.request_id, None)
|
||||
self._requests.pop(request.request_id, None)
|
||||
return False, None
|
||||
|
||||
def update_connector_output(self, output: KVConnectorOutput) -> None:
|
||||
metadata = output.kv_connector_worker_meta
|
||||
if not isinstance(metadata, UMBPConnectorWorkerMetadata):
|
||||
return
|
||||
pool = self._gpu_block_pool
|
||||
for event_id, result in metadata.store_events.items():
|
||||
event = self._store_events.get(event_id)
|
||||
if event is None:
|
||||
continue
|
||||
event.result.merge(result)
|
||||
if event.result.completed_workers < self._num_workers:
|
||||
continue
|
||||
del self._store_events[event_id]
|
||||
if pool is not None:
|
||||
block_ids = dict.fromkeys(plan.block_id for plan in event.plans)
|
||||
pool.free_blocks(
|
||||
pool.blocks[block_id] for block_id in reversed(block_ids)
|
||||
)
|
||||
|
||||
def has_pending_push_work(self) -> bool:
|
||||
return bool(self._store_events)
|
||||
|
||||
def reset_store(self) -> bool:
|
||||
if self._store_events:
|
||||
return False
|
||||
self._load_specs.clear()
|
||||
self._pending_loads.clear()
|
||||
return self.runtime.clear()
|
||||
|
||||
def close(self) -> None:
|
||||
self.runtime.close()
|
||||
@@ -0,0 +1,240 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Shared worker-side UMBP connector behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.forward_context import ForwardContext
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from .data import (
|
||||
BlockIdentityCodec,
|
||||
BlockTransferPlan,
|
||||
KVLayoutPlanner,
|
||||
StoreEventResult,
|
||||
TransferJobState,
|
||||
TransferJobStatus,
|
||||
UMBPConnectorMetadata,
|
||||
UMBPConnectorWorkerMetadata,
|
||||
)
|
||||
from .runtime import UMBPWorkerHandle
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StoreBatch:
|
||||
event_id: int
|
||||
jobs: dict[str, TransferJobState]
|
||||
|
||||
|
||||
class UMBPStoreConnectorWorker:
|
||||
"""Translate shared metadata into runtime load/store jobs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
runtime: UMBPWorkerHandle,
|
||||
layout: KVLayoutPlanner | None = None,
|
||||
*,
|
||||
codec: BlockIdentityCodec | None = None,
|
||||
) -> None:
|
||||
self.runtime = runtime
|
||||
self.layout = layout
|
||||
self.codec = codec
|
||||
self._load_jobs: dict[str, dict[str | None, TransferJobState]] = {}
|
||||
self._store_jobs: dict[str, TransferJobState] = {}
|
||||
self._pending_stores: list[_StoreBatch] = []
|
||||
self._worker_meta = UMBPConnectorWorkerMetadata()
|
||||
self._finished_recving: set[str] = set()
|
||||
self._load_error_block_ids: set[int] = set()
|
||||
self._report_load_completions = False
|
||||
self._active_store_event = -1
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]) -> None:
|
||||
if self.layout is not None and self.layout.regions:
|
||||
self.layout.register_kv_caches(kv_caches)
|
||||
self.runtime.register_buffers(kv_caches)
|
||||
|
||||
def _materialize_plans(
|
||||
self, plans: list[BlockTransferPlan]
|
||||
) -> list[BlockTransferPlan]:
|
||||
plans = [self._localize_plan(plan) for plan in plans]
|
||||
if self.layout is None or not self.layout.regions:
|
||||
return plans
|
||||
materialized: list[BlockTransferPlan] = []
|
||||
for plan in plans:
|
||||
if plan.ranges:
|
||||
materialized.append(plan)
|
||||
continue
|
||||
materialized.append(self.layout.materialize(plan))
|
||||
return materialized
|
||||
|
||||
def _localize_plan(self, plan: BlockTransferPlan) -> BlockTransferPlan:
|
||||
if self.codec is None or plan.group_id is None:
|
||||
return plan
|
||||
key = self._localize_key(plan.key, plan.group_id)
|
||||
if key == plan.key:
|
||||
return plan
|
||||
return replace(plan, key=key)
|
||||
|
||||
def _localize_key(self, key: str, group_id: int) -> str:
|
||||
if self.codec is None:
|
||||
return key
|
||||
try:
|
||||
block_hash = bytes.fromhex(key.rsplit(":", 1)[1])
|
||||
except (IndexError, ValueError):
|
||||
return key
|
||||
return self.codec.key(block_hash, group_id)
|
||||
|
||||
def start_load_kv(
|
||||
self, forward_context: ForwardContext, metadata: UMBPConnectorMetadata
|
||||
) -> None:
|
||||
del forward_context
|
||||
self._report_load_completions = metadata.async_load
|
||||
for request_id, plans in metadata.load_requests.items():
|
||||
if plans:
|
||||
self._submit_loads(request_id, plans)
|
||||
|
||||
def _submit_loads(self, request_id: str, plans: list[BlockTransferPlan]) -> None:
|
||||
materialized = self._materialize_plans(plans)
|
||||
logger.debug(
|
||||
"UMBP load submitted request=%s plans=%d ranges=%d bytes=%d",
|
||||
request_id,
|
||||
len(materialized),
|
||||
sum(len(plan.ranges) for plan in materialized),
|
||||
sum(item.length for plan in materialized for item in plan.ranges),
|
||||
)
|
||||
self._load_jobs[request_id] = {None: self.runtime.load(materialized)}
|
||||
|
||||
def _finish_job(
|
||||
self, request_id: str, result: TransferJobState, *, is_load: bool
|
||||
) -> bool:
|
||||
operation = "load" if is_load else "store"
|
||||
succeeded = result.status == TransferJobStatus.COMPLETED
|
||||
if not succeeded:
|
||||
logger.debug(
|
||||
"UMBP %s failed request=%s status=%s error=%s",
|
||||
operation,
|
||||
request_id,
|
||||
result.status.value,
|
||||
result.error,
|
||||
)
|
||||
if is_load:
|
||||
self._load_error_block_ids.update(result.failed_block_ids)
|
||||
elif succeeded:
|
||||
self.runtime.publish(result)
|
||||
return succeeded
|
||||
|
||||
def wait_for_layer_load(self, layer_name: str) -> None:
|
||||
if self._report_load_completions:
|
||||
# Asynchronous loads fill blocks of requests that do not run until
|
||||
# get_finished reports them, so no forward pass reads those blocks.
|
||||
return
|
||||
self._drain_load_jobs(wait=True)
|
||||
|
||||
def wait_for_save(self) -> None:
|
||||
if self._store_jobs or self._active_store_event >= 0:
|
||||
self._pending_stores.append(
|
||||
_StoreBatch(self._active_store_event, self._store_jobs)
|
||||
)
|
||||
self._store_jobs = {}
|
||||
self._active_store_event = -1
|
||||
self._drain_store_jobs(wait=False)
|
||||
|
||||
def _drain_store_jobs(self, *, wait: bool) -> None:
|
||||
for batch in list(self._pending_stores):
|
||||
for request_id, job in list(batch.jobs.items()):
|
||||
result = self.runtime.wait(job) if wait else self.runtime.poll(job)
|
||||
if result is None:
|
||||
continue
|
||||
# A partly failed job is not published, including its stored keys.
|
||||
self._finish_job(request_id, result, is_load=False)
|
||||
del batch.jobs[request_id]
|
||||
if not batch.jobs:
|
||||
if batch.event_id >= 0:
|
||||
self._worker_meta.store_events[batch.event_id] = StoreEventResult(
|
||||
completed_workers=1
|
||||
)
|
||||
self._pending_stores.remove(batch)
|
||||
|
||||
def _submit_store_plans(
|
||||
self, plans: list[BlockTransferPlan], request_id: str
|
||||
) -> None:
|
||||
if not plans:
|
||||
return
|
||||
materialized = self._materialize_plans(plans)
|
||||
self._store_jobs[request_id] = self.runtime.store(materialized)
|
||||
|
||||
def enqueue_stores(self, metadata: UMBPConnectorMetadata) -> None:
|
||||
self._active_store_event = metadata.store_event
|
||||
store_requests = {
|
||||
request_id: list(plans)
|
||||
for request_id, plans in metadata.store_requests.items()
|
||||
}
|
||||
grouped = {plan for plans in store_requests.values() for plan in plans}
|
||||
for plan in metadata.store_plans:
|
||||
if plan not in grouped:
|
||||
store_requests.setdefault(
|
||||
plan.request_id or "__umbp_batch__", []
|
||||
).append(plan)
|
||||
grouped.add(plan)
|
||||
for request_id, plans in store_requests.items():
|
||||
self._submit_store_plans(plans, request_id)
|
||||
|
||||
def handle_preemptions(self, metadata: UMBPConnectorMetadata) -> None:
|
||||
"""Finish request-local jobs before vLLM reuses their GPU blocks."""
|
||||
preempted = metadata.preempted_request_ids
|
||||
if not preempted:
|
||||
return
|
||||
for request_id in preempted:
|
||||
for job in self._load_jobs.pop(request_id, {}).values():
|
||||
self.runtime.wait(job)
|
||||
for jobs in [self._store_jobs, *(batch.jobs for batch in self._pending_stores)]:
|
||||
for request_id, job in jobs.items():
|
||||
if request_id in preempted or any(
|
||||
plan.request_id in preempted for plan in job.plans
|
||||
):
|
||||
jobs[request_id] = self.runtime.wait(job)
|
||||
|
||||
def get_finished(
|
||||
self, finished_req_ids: set[str]
|
||||
) -> tuple[set[str] | None, set[str] | None]:
|
||||
del finished_req_ids
|
||||
self._drain_load_jobs(wait=False)
|
||||
self._drain_store_jobs(wait=False)
|
||||
recving = set(self._finished_recving)
|
||||
self._finished_recving.difference_update(recving)
|
||||
return None, recving or None
|
||||
|
||||
def _drain_load_jobs(self, *, wait: bool) -> None:
|
||||
for request_id, jobs in list(self._load_jobs.items()):
|
||||
for layer, job in list(jobs.items()):
|
||||
result = self.runtime.wait(job) if wait else self.runtime.poll(job)
|
||||
if result is None:
|
||||
continue
|
||||
del jobs[layer]
|
||||
self._finish_job(request_id, result, is_load=True)
|
||||
if not jobs:
|
||||
del self._load_jobs[request_id]
|
||||
if self._report_load_completions:
|
||||
self._finished_recving.add(request_id)
|
||||
|
||||
def build_connector_worker_meta(self) -> UMBPConnectorWorkerMetadata:
|
||||
result = self._worker_meta
|
||||
self._worker_meta = UMBPConnectorWorkerMetadata()
|
||||
return result
|
||||
|
||||
def get_block_ids_with_load_errors(self) -> set[int]:
|
||||
failed = self._load_error_block_ids
|
||||
self._load_error_block_ids = set()
|
||||
return failed
|
||||
|
||||
def close(self) -> None:
|
||||
self.wait_for_save()
|
||||
self._drain_store_jobs(wait=True)
|
||||
self._drain_load_jobs(wait=True)
|
||||
self.runtime.close()
|
||||
Reference in New Issue
Block a user