[Unified Cache][6/N]: Add UMBP external linker (#37578)
Co-authored-by: Zhangheng <hzh0425@apache.org>
This commit is contained in:
@@ -204,6 +204,12 @@ def _create_unified_radix_cache(
|
|||||||
)
|
)
|
||||||
|
|
||||||
linker_cls = MooncakeDirectLinker
|
linker_cls = MooncakeDirectLinker
|
||||||
|
elif backend == "mori":
|
||||||
|
from sglang.srt.mem_cache.storage.umbp.umbp_direct_linker import (
|
||||||
|
UMBPDirectLinker,
|
||||||
|
)
|
||||||
|
|
||||||
|
linker_cls = UMBPDirectLinker
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unknown unified cache external linker backend: {backend!r}"
|
f"Unknown unified cache external linker backend: {backend!r}"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,9 @@ class UMBPHostTensorAllocator(HostTensorAllocator):
|
|||||||
)
|
)
|
||||||
self._numa_node = _int_env("SGLANG_HICACHE_HOST_NUMA_NODE", -1)
|
self._numa_node = _int_env("SGLANG_HICACHE_HOST_NUMA_NODE", -1)
|
||||||
self._prefault = _bool_env("SGLANG_HICACHE_HOST_PREFAULT", True)
|
self._prefault = _bool_env("SGLANG_HICACHE_HOST_PREFAULT", True)
|
||||||
|
# Standalone mode needs fd-shareable backing; allocation precedes
|
||||||
|
# config parsing.
|
||||||
|
self._standalone_process = bool(os.getenv("UMBP_STANDALONE_ADDRESS"))
|
||||||
self._handles: Dict[int, Any] = {}
|
self._handles: Dict[int, Any] = {}
|
||||||
|
|
||||||
def allocate(
|
def allocate(
|
||||||
@@ -62,6 +65,13 @@ class UMBPHostTensorAllocator(HostTensorAllocator):
|
|||||||
element_size = torch.empty((), dtype=dtype).element_size()
|
element_size = torch.empty((), dtype=dtype).element_size()
|
||||||
nbytes = math.prod(int(dim) for dim in dims) * element_size
|
nbytes = math.prod(int(dim) for dim in dims) * element_size
|
||||||
|
|
||||||
|
if self._standalone_process:
|
||||||
|
requested_backing = (
|
||||||
|
self._mod.UMBPHostBufferBacking.AnonymousShmHugetlb
|
||||||
|
if self._use_hugepage
|
||||||
|
else self._mod.UMBPHostBufferBacking.AnonymousShm
|
||||||
|
)
|
||||||
|
else:
|
||||||
requested_backing = (
|
requested_backing = (
|
||||||
self._mod.UMBPHostBufferBacking.AnonymousHugetlb
|
self._mod.UMBPHostBufferBacking.AnonymousHugetlb
|
||||||
if self._use_hugepage
|
if self._use_hugepage
|
||||||
@@ -101,15 +111,19 @@ class UMBPHostTensorAllocator(HostTensorAllocator):
|
|||||||
handle.mapped_size,
|
handle.mapped_size,
|
||||||
self._numa_node,
|
self._numa_node,
|
||||||
)
|
)
|
||||||
if (
|
demoted = handle.actual_backing == (
|
||||||
self._use_hugepage
|
self._mod.UMBPHostBufferBacking.AnonymousShm
|
||||||
and handle.actual_backing == self._mod.UMBPHostBufferBacking.Anonymous
|
if self._standalone_process
|
||||||
):
|
else self._mod.UMBPHostBufferBacking.Anonymous
|
||||||
|
)
|
||||||
|
if self._use_hugepage and demoted:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"UMBPHostTensorAllocator: requested AnonymousHugetlb backing "
|
"UMBPHostTensorAllocator: requested %s backing but kernel "
|
||||||
"but kernel demoted to Anonymous (4 KiB pages). Check "
|
"demoted to %s (4 KiB pages). Check vm.nr_hugepages and "
|
||||||
"vm.nr_hugepages and HugePages_Free in /proc/meminfo. "
|
"HugePages_Free in /proc/meminfo. Performance and AINIC "
|
||||||
"Performance and AINIC MR-size benefits will not apply."
|
"MR-size benefits will not apply.",
|
||||||
|
requested_backing,
|
||||||
|
handle.actual_backing,
|
||||||
)
|
)
|
||||||
|
|
||||||
return tensor.view(dims)
|
return tensor.view(dims)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ def _import_umbp_client():
|
|||||||
UMBPIoBackend = getattr(umbp_mod, "UMBPIoBackend", None)
|
UMBPIoBackend = getattr(umbp_mod, "UMBPIoBackend", None)
|
||||||
UMBPDurabilityMode = getattr(umbp_mod, "UMBPDurabilityMode", None)
|
UMBPDurabilityMode = getattr(umbp_mod, "UMBPDurabilityMode", None)
|
||||||
UMBPDistributedConfig = getattr(umbp_mod, "UMBPDistributedConfig", None)
|
UMBPDistributedConfig = getattr(umbp_mod, "UMBPDistributedConfig", None)
|
||||||
|
UMBPStandaloneProcessConfig = getattr(umbp_mod, "UMBPStandaloneProcessConfig", None)
|
||||||
|
UMBPDeploymentMode = getattr(umbp_mod, "UMBPDeploymentMode", None)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
UMBPClient,
|
UMBPClient,
|
||||||
@@ -46,6 +48,8 @@ def _import_umbp_client():
|
|||||||
UMBPIoBackend,
|
UMBPIoBackend,
|
||||||
UMBPDurabilityMode,
|
UMBPDurabilityMode,
|
||||||
UMBPDistributedConfig,
|
UMBPDistributedConfig,
|
||||||
|
UMBPStandaloneProcessConfig,
|
||||||
|
UMBPDeploymentMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -134,6 +138,10 @@ def _select_rank_config_value(
|
|||||||
# knobs outside this list go through the "spdk_passthrough" escape hatch.
|
# knobs outside this list go through the "spdk_passthrough" escape hatch.
|
||||||
_COMMON_EXTRA_KEYS = frozenset(
|
_COMMON_EXTRA_KEYS = frozenset(
|
||||||
{
|
{
|
||||||
|
"node_address",
|
||||||
|
"node_id",
|
||||||
|
"node_tags",
|
||||||
|
"tags",
|
||||||
"dram_capacity_bytes",
|
"dram_capacity_bytes",
|
||||||
"ssd_enabled",
|
"ssd_enabled",
|
||||||
"ssd_storage_dir",
|
"ssd_storage_dir",
|
||||||
@@ -164,6 +172,8 @@ _COMMON_EXTRA_KEYS = frozenset(
|
|||||||
"kv_events_subscriber",
|
"kv_events_subscriber",
|
||||||
"kv_events_endpoint",
|
"kv_events_endpoint",
|
||||||
"kv_events_topic",
|
"kv_events_topic",
|
||||||
|
"disable_zero_copy_register",
|
||||||
|
"extra_backend_tag",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,24 +185,25 @@ _STANDALONE_ONLY_EXTRA_KEYS = frozenset(
|
|||||||
"eviction_policy",
|
"eviction_policy",
|
||||||
"eviction_candidate_window",
|
"eviction_candidate_window",
|
||||||
"auto_promote_on_read",
|
"auto_promote_on_read",
|
||||||
|
"standalone_address",
|
||||||
|
"standalone_auto_start",
|
||||||
|
"standalone_startup_timeout_ms",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_DISTRIBUTED_ONLY_EXTRA_KEYS = frozenset(
|
_DISTRIBUTED_ONLY_EXTRA_KEYS = frozenset(
|
||||||
{
|
{
|
||||||
"master_address",
|
"master_address",
|
||||||
"node_address",
|
|
||||||
"node_id",
|
|
||||||
"auto_heartbeat",
|
"auto_heartbeat",
|
||||||
"io_engine_host",
|
"io_engine_host",
|
||||||
"io_engine_port",
|
"io_engine_port",
|
||||||
"staging_buffer_size",
|
"staging_buffer_size",
|
||||||
|
"ranged_scratch_size",
|
||||||
"ssd_staging_buffer_size",
|
"ssd_staging_buffer_size",
|
||||||
"ssd_staging_buffer_slots",
|
"ssd_staging_buffer_slots",
|
||||||
"peer_service_port",
|
"peer_service_port",
|
||||||
"cache_remote_fetches",
|
"cache_remote_fetches",
|
||||||
"dram_page_size",
|
"dram_page_size",
|
||||||
"disable_zero_copy_register",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -238,7 +249,13 @@ class UMBPStore(HiCacheStorage):
|
|||||||
self,
|
self,
|
||||||
storage_config: HiCacheStorageConfig = None,
|
storage_config: HiCacheStorageConfig = None,
|
||||||
mem_pool_host: HostKVCache = None,
|
mem_pool_host: HostKVCache = None,
|
||||||
|
*,
|
||||||
|
per_rank_keyspace: bool = False,
|
||||||
):
|
):
|
||||||
|
# per_rank_keyspace: the direct linker already organises keys by its own
|
||||||
|
# cache-group rank, so HiCache's shared-SSD leader/follower deduplication
|
||||||
|
# must not be layered on top. Default False preserves the existing
|
||||||
|
# HiCache L3 behaviour.
|
||||||
(
|
(
|
||||||
UMBPClient,
|
UMBPClient,
|
||||||
UMBPConfig,
|
UMBPConfig,
|
||||||
@@ -246,6 +263,8 @@ class UMBPStore(HiCacheStorage):
|
|||||||
UMBPIoBackend,
|
UMBPIoBackend,
|
||||||
UMBPDurabilityMode,
|
UMBPDurabilityMode,
|
||||||
UMBPDistributedConfig,
|
UMBPDistributedConfig,
|
||||||
|
UMBPStandaloneProcessConfig,
|
||||||
|
UMBPDeploymentMode,
|
||||||
) = _import_umbp_client()
|
) = _import_umbp_client()
|
||||||
|
|
||||||
if storage_config is not None:
|
if storage_config is not None:
|
||||||
@@ -260,6 +279,7 @@ class UMBPStore(HiCacheStorage):
|
|||||||
self.pp_rank = 0
|
self.pp_rank = 0
|
||||||
self.pp_size = 1
|
self.pp_size = 1
|
||||||
self.tp_size = 1
|
self.tp_size = 1
|
||||||
|
self._umbp_deployment_mode_enum = UMBPDeploymentMode
|
||||||
|
|
||||||
cfg = UMBPConfig.from_environment()
|
cfg = UMBPConfig.from_environment()
|
||||||
# UMBPStore owns role selection explicitly. Do not inherit LOCAL_RANK /
|
# UMBPStore owns role selection explicitly. Do not inherit LOCAL_RANK /
|
||||||
@@ -268,6 +288,12 @@ class UMBPStore(HiCacheStorage):
|
|||||||
# and skip writes.
|
# and skip writes.
|
||||||
cfg.role = UMBPRole.Standalone
|
cfg.role = UMBPRole.Standalone
|
||||||
extra = getattr(storage_config, "extra_config", None) or {}
|
extra = getattr(storage_config, "extra_config", None) or {}
|
||||||
|
prefix_parts = []
|
||||||
|
if extra.get("extra_backend_tag") is not None:
|
||||||
|
prefix_parts.append(str(extra["extra_backend_tag"]))
|
||||||
|
if storage_config is not None and storage_config.model_name:
|
||||||
|
prefix_parts.append("-".join(storage_config.model_name.split("/")))
|
||||||
|
self.config_prefix = "_".join(prefix_parts) if prefix_parts else None
|
||||||
explicit_tenant_id = (
|
explicit_tenant_id = (
|
||||||
os.getenv("UMBP_SPDK_PROXY_TENANT_ID") is not None
|
os.getenv("UMBP_SPDK_PROXY_TENANT_ID") is not None
|
||||||
or "spdk_proxy_tenant_id" in extra
|
or "spdk_proxy_tenant_id" in extra
|
||||||
@@ -461,6 +487,58 @@ class UMBPStore(HiCacheStorage):
|
|||||||
master_address = extra.get(
|
master_address = extra.get(
|
||||||
"master_address", _optional_env_str("UMBP_MASTER_ADDRESS")
|
"master_address", _optional_env_str("UMBP_MASTER_ADDRESS")
|
||||||
)
|
)
|
||||||
|
standalone_extra_address = extra.get("standalone_address")
|
||||||
|
standalone_env_address = _optional_env_str("UMBP_STANDALONE_ADDRESS")
|
||||||
|
standalone_address = standalone_extra_address or standalone_env_address
|
||||||
|
# Verify the client did not silently fall back to local mode.
|
||||||
|
self._standalone_process_expected = bool(standalone_address)
|
||||||
|
if master_address and standalone_address:
|
||||||
|
raise ValueError(
|
||||||
|
"master_address and standalone_address are mutually exclusive "
|
||||||
|
"(distributed vs. standalone-process mode)."
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
mem_pool_host is not None
|
||||||
|
and standalone_extra_address
|
||||||
|
and not standalone_env_address
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"standalone_address in hicache-storage-backend-extra-config is "
|
||||||
|
"not supported when a host KV pool is present. The host memory "
|
||||||
|
"pool allocator chooses Anonymous vs. AnonymousShm before "
|
||||||
|
"extra_config is parsed, so set UMBP_STANDALONE_ADDRESS in the "
|
||||||
|
"process environment instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Both remote modes use the same worker identity.
|
||||||
|
def _resolve_node_address() -> str:
|
||||||
|
node_address = extra.get(
|
||||||
|
"node_address", _optional_env_str("UMBP_NODE_ADDRESS")
|
||||||
|
)
|
||||||
|
if node_address is None:
|
||||||
|
return _default_node_address()
|
||||||
|
return _select_rank_config_value(
|
||||||
|
node_address, unique_rank, "node_address", str
|
||||||
|
)
|
||||||
|
|
||||||
|
def _resolve_node_id(node_address: str) -> str:
|
||||||
|
node_id = extra.get("node_id", _optional_env_str("UMBP_NODE_ID"))
|
||||||
|
if node_id is None:
|
||||||
|
return (
|
||||||
|
f"{node_address}:dp{dp_rank_hint if dp_rank_hint is not None else 0}"
|
||||||
|
f":pp{self.pp_rank}:tp{self.local_rank}"
|
||||||
|
)
|
||||||
|
return _select_rank_config_value(node_id, unique_rank, "node_id", str)
|
||||||
|
|
||||||
|
def _resolve_node_tags() -> List[str]:
|
||||||
|
raw_tags = extra.get("node_tags", extra.get("tags"))
|
||||||
|
if raw_tags is None:
|
||||||
|
raw_tags = _optional_env_str("UMBP_NODE_TAGS")
|
||||||
|
if raw_tags is None:
|
||||||
|
return []
|
||||||
|
if isinstance(raw_tags, str):
|
||||||
|
return [tag.strip() for tag in raw_tags.split(",") if tag.strip()]
|
||||||
|
return [str(tag) for tag in raw_tags]
|
||||||
|
|
||||||
_warn_extra_config_scope(extra, distributed_enabled=bool(master_address))
|
_warn_extra_config_scope(extra, distributed_enabled=bool(master_address))
|
||||||
if master_address and UMBPDistributedConfig is not None:
|
if master_address and UMBPDistributedConfig is not None:
|
||||||
@@ -470,33 +548,11 @@ class UMBPStore(HiCacheStorage):
|
|||||||
if "ssd_copy_worker_threads" not in extra:
|
if "ssd_copy_worker_threads" not in extra:
|
||||||
cfg.copy_pipeline.worker_threads = 1
|
cfg.copy_pipeline.worker_threads = 1
|
||||||
|
|
||||||
node_address = extra.get(
|
node_address = _resolve_node_address()
|
||||||
"node_address", _optional_env_str("UMBP_NODE_ADDRESS")
|
|
||||||
)
|
|
||||||
if node_address is None:
|
|
||||||
node_address = _default_node_address()
|
|
||||||
else:
|
|
||||||
node_address = _select_rank_config_value(
|
|
||||||
node_address,
|
|
||||||
unique_rank,
|
|
||||||
"node_address",
|
|
||||||
str,
|
|
||||||
)
|
|
||||||
dist_cfg.master_config.node_address = node_address
|
dist_cfg.master_config.node_address = node_address
|
||||||
|
dist_cfg.master_config.node_id = _resolve_node_id(node_address)
|
||||||
node_id = extra.get("node_id", _optional_env_str("UMBP_NODE_ID"))
|
if hasattr(dist_cfg.master_config, "tags"):
|
||||||
if node_id is None:
|
dist_cfg.master_config.tags = _resolve_node_tags()
|
||||||
dist_cfg.master_config.node_id = (
|
|
||||||
f"{node_address}:dp{dp_rank_hint if dp_rank_hint is not None else 0}"
|
|
||||||
f":pp{self.pp_rank}:tp{self.local_rank}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
dist_cfg.master_config.node_id = _select_rank_config_value(
|
|
||||||
node_id,
|
|
||||||
unique_rank,
|
|
||||||
"node_id",
|
|
||||||
str,
|
|
||||||
)
|
|
||||||
|
|
||||||
if "auto_heartbeat" in extra:
|
if "auto_heartbeat" in extra:
|
||||||
dist_cfg.master_config.auto_heartbeat = _strict_bool(
|
dist_cfg.master_config.auto_heartbeat = _strict_bool(
|
||||||
@@ -531,6 +587,10 @@ class UMBPStore(HiCacheStorage):
|
|||||||
|
|
||||||
if "staging_buffer_size" in extra:
|
if "staging_buffer_size" in extra:
|
||||||
dist_cfg.staging_buffer_size = int(extra["staging_buffer_size"])
|
dist_cfg.staging_buffer_size = int(extra["staging_buffer_size"])
|
||||||
|
if "ranged_scratch_size" in extra and hasattr(
|
||||||
|
dist_cfg, "ranged_scratch_size"
|
||||||
|
):
|
||||||
|
dist_cfg.ranged_scratch_size = int(extra["ranged_scratch_size"])
|
||||||
|
|
||||||
if "ssd_staging_buffer_size" in extra and hasattr(
|
if "ssd_staging_buffer_size" in extra and hasattr(
|
||||||
dist_cfg, "ssd_staging_buffer_size"
|
dist_cfg, "ssd_staging_buffer_size"
|
||||||
@@ -604,8 +664,8 @@ class UMBPStore(HiCacheStorage):
|
|||||||
meta = mem_pool_host.get_split_heads_page_buffer_meta(dummy, sf)
|
meta = mem_pool_host.get_split_heads_page_buffer_meta(dummy, sf)
|
||||||
else:
|
else:
|
||||||
meta = mem_pool_host.get_page_buffer_meta(dummy)
|
meta = mem_pool_host.get_page_buffer_meta(dummy)
|
||||||
# meta is None for a logical-anchor group (see note above);
|
# A hybrid logical anchor returns None here by design; leave
|
||||||
# esz is the per-page element-size list otherwise.
|
# dram_page_size at 0 and let the per-pool v2 sizes handle it.
|
||||||
esz = meta[1] if meta else None
|
esz = meta[1] if meta else None
|
||||||
page_byte_size = int(esz[0]) if esz else 0
|
page_byte_size = int(esz[0]) if esz else 0
|
||||||
|
|
||||||
@@ -647,6 +707,52 @@ class UMBPStore(HiCacheStorage):
|
|||||||
dist_cfg.io_engine.port,
|
dist_cfg.io_engine.port,
|
||||||
dist_cfg.peer_service_port,
|
dist_cfg.peer_service_port,
|
||||||
)
|
)
|
||||||
|
elif standalone_address:
|
||||||
|
if UMBPStandaloneProcessConfig is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Installed mori does not expose UMBPStandaloneProcessConfig"
|
||||||
|
)
|
||||||
|
standalone_cfg = UMBPStandaloneProcessConfig()
|
||||||
|
standalone_cfg.address = str(standalone_address)
|
||||||
|
auto_start = extra.get(
|
||||||
|
"standalone_auto_start",
|
||||||
|
_optional_env_str("UMBP_STANDALONE_AUTO_START"),
|
||||||
|
)
|
||||||
|
if auto_start is not None:
|
||||||
|
standalone_cfg.auto_start = _strict_bool(
|
||||||
|
auto_start, "standalone_auto_start"
|
||||||
|
)
|
||||||
|
startup_timeout_ms = extra.get(
|
||||||
|
"standalone_startup_timeout_ms",
|
||||||
|
_optional_env_int("UMBP_STANDALONE_STARTUP_TIMEOUT_MS"),
|
||||||
|
)
|
||||||
|
if startup_timeout_ms is not None:
|
||||||
|
standalone_cfg.startup_timeout_ms = int(startup_timeout_ms)
|
||||||
|
if standalone_cfg.startup_timeout_ms <= 0:
|
||||||
|
raise ValueError("standalone_startup_timeout_ms must be > 0")
|
||||||
|
if all(
|
||||||
|
hasattr(standalone_cfg, field)
|
||||||
|
for field in ("worker_node_address", "worker_node_id", "tags")
|
||||||
|
):
|
||||||
|
worker_node_address = _resolve_node_address()
|
||||||
|
standalone_cfg.worker_node_address = worker_node_address
|
||||||
|
standalone_cfg.worker_node_id = _resolve_node_id(worker_node_address)
|
||||||
|
standalone_cfg.tags = _resolve_node_tags()
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"UMBPStore standalone-process mode: installed mori does not "
|
||||||
|
"expose worker identity on UMBPStandaloneProcessConfig; a "
|
||||||
|
"distributed-backed standalone server cannot build per-worker "
|
||||||
|
"external-KV identities."
|
||||||
|
)
|
||||||
|
cfg.standalone_process = standalone_cfg
|
||||||
|
logger.info(
|
||||||
|
"UMBPStore standalone-process mode: address=%s, auto_start=%s, "
|
||||||
|
"startup_timeout_ms=%s",
|
||||||
|
standalone_cfg.address,
|
||||||
|
standalone_cfg.auto_start,
|
||||||
|
standalone_cfg.startup_timeout_ms,
|
||||||
|
)
|
||||||
|
|
||||||
self.storage_config = storage_config
|
self.storage_config = storage_config
|
||||||
|
|
||||||
@@ -657,8 +763,25 @@ class UMBPStore(HiCacheStorage):
|
|||||||
self.is_mla_follower = False
|
self.is_mla_follower = False
|
||||||
tp_size = self.tp_size
|
tp_size = self.tp_size
|
||||||
use_spdk = cfg.ssd.ssd_backend in ("spdk", "spdk_proxy")
|
use_spdk = cfg.ssd.ssd_backend in ("spdk", "spdk_proxy")
|
||||||
distributed_enabled = cfg.distributed is not None
|
remote_process_enabled = (
|
||||||
if not distributed_enabled and self.is_mla_backend and tp_size > 1:
|
cfg.distributed is not None or cfg.standalone_process is not None
|
||||||
|
)
|
||||||
|
# Shared SSD exists to deduplicate MLA KV, which TP replicates: one
|
||||||
|
# rank owns the bytes and the others read them back. A caller that
|
||||||
|
# already keys per rank has nothing to deduplicate -- and would be
|
||||||
|
# broken by the scheme, because a follower would be sent looking for
|
||||||
|
# keys the leader never wrote under the follower's own suffix.
|
||||||
|
#
|
||||||
|
# It is also the reason embedded mode could not run: followers are the
|
||||||
|
# one role whose client reports no ranged multi-buffer I/O, which
|
||||||
|
# page-granular objects require. Standalone never hit this, not by
|
||||||
|
# design but because remote_process_enabled short-circuits it there.
|
||||||
|
if (
|
||||||
|
not remote_process_enabled
|
||||||
|
and not per_rank_keyspace
|
||||||
|
and self.is_mla_backend
|
||||||
|
and tp_size > 1
|
||||||
|
):
|
||||||
cfg.ssd.enabled = True
|
cfg.ssd.enabled = True
|
||||||
if self.local_rank == 0:
|
if self.local_rank == 0:
|
||||||
# Leader: copy every DRAM write to shared SSD.
|
# Leader: copy every DRAM write to shared SSD.
|
||||||
@@ -802,11 +925,10 @@ class UMBPStore(HiCacheStorage):
|
|||||||
safe_cap = int(cfg.ssd.capacity_bytes * 0.95)
|
safe_cap = int(cfg.ssd.capacity_bytes * 0.95)
|
||||||
cfg.ssd.spdk_proxy_tenant_quota_bytes = max(1, safe_cap // dp_size_hint)
|
cfg.ssd.spdk_proxy_tenant_quota_bytes = max(1, safe_cap // dp_size_hint)
|
||||||
|
|
||||||
# Initialize registration state before the optional constructor-time
|
# Initialize before the optional constructor-time pool registration.
|
||||||
# register_mem_pool_host() call below. In particular, do not overwrite
|
|
||||||
# the logical-anchor flag after that call has detected a LogicalHostPool.
|
|
||||||
self.registered_pools: dict = {}
|
self.registered_pools: dict = {}
|
||||||
self._kv_anchor_is_logical = False
|
self._kv_anchor_is_logical = False
|
||||||
|
self._registered_regions: set = set()
|
||||||
|
|
||||||
self.client = UMBPClient(cfg)
|
self.client = UMBPClient(cfg)
|
||||||
if mem_pool_host is not None:
|
if mem_pool_host is not None:
|
||||||
@@ -888,45 +1010,99 @@ class UMBPStore(HiCacheStorage):
|
|||||||
"page_head",
|
"page_head",
|
||||||
], "UMBP store only supports page_first, page_first_direct, or page_head layout"
|
], "UMBP store only supports page_first, page_first_direct, or page_head layout"
|
||||||
|
|
||||||
# Hybrid logical anchors (e.g. DeepSeek-V4's KV anchor LogicalHostPool)
|
# A logical anchor owns indices; side pools carry the data.
|
||||||
# own only allocation indices and hold no physical KV tensor. Compute
|
|
||||||
# this once and reuse: there is nothing to register for RDMA here, v1
|
|
||||||
# I/O no-ops on it, and the real per-pool buffers are registered through
|
|
||||||
# register_mem_host_pool_v2().
|
|
||||||
self._kv_anchor_is_logical = self.mem_pool_host.kv_buffer is None
|
self._kv_anchor_is_logical = self.mem_pool_host.kv_buffer is None
|
||||||
|
|
||||||
self._zero_copy_registered = False
|
self._zero_copy_registered = False
|
||||||
|
|
||||||
|
# Side-pool registration needs the mode even for a logical anchor.
|
||||||
|
self._is_standalone_process = False
|
||||||
|
if self.client is not None:
|
||||||
|
deployment_mode = None
|
||||||
|
mode_enum = self._umbp_deployment_mode_enum
|
||||||
|
try:
|
||||||
|
deployment_mode = self.client.get_deployment_mode()
|
||||||
|
if mode_enum is not None:
|
||||||
|
self._is_standalone_process = (
|
||||||
|
deployment_mode == mode_enum.StandaloneProcess
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if self._standalone_process_expected:
|
||||||
|
raise RuntimeError(
|
||||||
|
"UMBPStore expected standalone-process mode from "
|
||||||
|
"UMBP_STANDALONE_ADDRESS, but get_deployment_mode() failed."
|
||||||
|
) from exc
|
||||||
|
if self._standalone_process_expected:
|
||||||
|
if mode_enum is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"UMBPStore expected standalone-process mode, but "
|
||||||
|
"UMBPDeploymentMode is not exposed by mori.umbp."
|
||||||
|
)
|
||||||
|
if deployment_mode != mode_enum.StandaloneProcess:
|
||||||
|
raise RuntimeError(
|
||||||
|
"UMBPStore expected standalone-process mode, but the "
|
||||||
|
f"UMBP client reported deployment_mode={deployment_mode!r}."
|
||||||
|
)
|
||||||
|
|
||||||
if self._kv_anchor_is_logical:
|
if self._kv_anchor_is_logical:
|
||||||
return
|
return
|
||||||
|
|
||||||
# In distributed mode, pre-register the entire host KV buffer with the
|
self._zero_copy_registered = self._register_host_buffer_for_zero_copy(
|
||||||
# underlying RDMA IOEngine so PoolClient can take the zero-copy path
|
mem_pool_host
|
||||||
# for batch_get_into_ptr / batch_put_from_ptr (skips the staging
|
)
|
||||||
# buffer memcpy + lock and removes the per-call `staging_buffer_size`
|
|
||||||
# cap). Standalone returns true as no-op by IUMBPClient contract;
|
@staticmethod
|
||||||
# we still gate on is_distributed() below to avoid a pointless call.
|
def _pool_physical_buffers(host_pool: HostKVCache) -> List[Any]:
|
||||||
if self._register_host_buffer_for_zero_copy(mem_pool_host):
|
"""Return every non-empty physical tensor exposed by a host pool."""
|
||||||
self._zero_copy_registered = True
|
getter = getattr(host_pool, "get_hybrid_pool_buffer", None)
|
||||||
|
buffers = getter() if getter is not None else None
|
||||||
|
if not buffers:
|
||||||
|
buffers = [getattr(host_pool, "kv_buffer", None)]
|
||||||
|
flat: List[Any] = []
|
||||||
|
for buffer in buffers:
|
||||||
|
if buffer is None:
|
||||||
|
continue
|
||||||
|
for tensor in buffer if isinstance(buffer, (list, tuple)) else [buffer]:
|
||||||
|
# Empty views may share storage; register_memory rejects zero bytes.
|
||||||
|
if tensor is not None and tensor.numel() > 0:
|
||||||
|
flat.append(tensor)
|
||||||
|
return flat
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _buffer_extent(buffer, allocator) -> tuple:
|
||||||
|
"""Registerable (base pointer, size) of the allocation behind a tensor."""
|
||||||
|
storage = buffer.untyped_storage()
|
||||||
|
base = int(storage.data_ptr())
|
||||||
|
size = int(storage.nbytes())
|
||||||
|
# Hugepage-backed mmaps are rounded up to the hugepage boundary, and
|
||||||
|
# ibv_reg_mr on AINIC / ROCm needs whole hugepages covered.
|
||||||
|
mapped_size_fn = getattr(allocator, "mapped_size_for", None)
|
||||||
|
mapped_size = (
|
||||||
|
mapped_size_fn(base)
|
||||||
|
if mapped_size_fn is not None
|
||||||
|
else getattr(allocator, "mapped_size", 0)
|
||||||
|
)
|
||||||
|
return base, max(size, int(mapped_size or 0))
|
||||||
|
|
||||||
def _register_host_buffer_for_zero_copy(self, host_pool: HostKVCache) -> bool:
|
def _register_host_buffer_for_zero_copy(self, host_pool: HostKVCache) -> bool:
|
||||||
"""Register a host pool's KV buffer with the RDMA IOEngine for zero-copy.
|
"""Register host buffers; standalone failures are fatal without fallback."""
|
||||||
|
|
||||||
Shared by the single-pool path (register_mem_pool_host) and the
|
|
||||||
multi-pool path (register_mem_host_pool_v2). Returns True when the
|
|
||||||
buffer was successfully registered, False on any skip/failure (the
|
|
||||||
caller then transparently falls back to the staging-buffer path).
|
|
||||||
"""
|
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
return False
|
return False
|
||||||
|
is_standalone_process = getattr(self, "_is_standalone_process", False)
|
||||||
try:
|
try:
|
||||||
is_distributed = bool(self.client.is_distributed())
|
is_distributed = bool(self.client.is_distributed())
|
||||||
except Exception:
|
except Exception:
|
||||||
is_distributed = False
|
is_distributed = False
|
||||||
if not is_distributed:
|
if not (is_distributed or is_standalone_process):
|
||||||
return False
|
return False
|
||||||
if not hasattr(self.client, "register_memory"):
|
if not hasattr(self.client, "register_memory"):
|
||||||
return False
|
return False
|
||||||
if getattr(self, "_disable_zero_copy_register", False):
|
if getattr(self, "_disable_zero_copy_register", False):
|
||||||
|
if is_standalone_process:
|
||||||
|
raise RuntimeError(
|
||||||
|
"disable_zero_copy_register is not supported in UMBP "
|
||||||
|
"standalone-process mode: there is no staging-buffer "
|
||||||
|
"fallback path."
|
||||||
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"UMBPStore: skipping host KV buffer RDMA registration because "
|
"UMBPStore: skipping host KV buffer RDMA registration because "
|
||||||
"disable_zero_copy_register=true (UMBP_DISABLE_ZERO_COPY_REGISTER). "
|
"disable_zero_copy_register=true (UMBP_DISABLE_ZERO_COPY_REGISTER). "
|
||||||
@@ -934,36 +1110,32 @@ class UMBPStore(HiCacheStorage):
|
|||||||
"size is capped by distributed.staging_buffer_size."
|
"size is capped by distributed.staging_buffer_size."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
# NOTE(layer_first): this only handles the page_first layout, where a
|
buffers = self._pool_physical_buffers(host_pool)
|
||||||
# host pool exposes a single contiguous `kv_buffer` that we can register
|
if not buffers:
|
||||||
# for RDMA in one shot. If UMBP later supports a layer_first layout, or
|
if is_standalone_process:
|
||||||
# side pools that expose multiple buffers via get_hybrid_pool_buffer()
|
raise RuntimeError(
|
||||||
# (e.g. DSAIndexerPoolHost, whose buffer lives in
|
f"UMBPStore: {type(host_pool).__name__} exposes no host buffer "
|
||||||
# index_k_with_scale_buffer rather than kv_buffer), this branch must be
|
"to register; standalone-process mode has no fallback path."
|
||||||
# extended to register every per-layer / per-buffer region. Otherwise
|
)
|
||||||
# such pools bypass zero-copy and silently fall back to the slower
|
|
||||||
# staging-buffer path.
|
|
||||||
kv_buffer = getattr(host_pool, "kv_buffer", None)
|
|
||||||
if kv_buffer is None:
|
|
||||||
return False
|
return False
|
||||||
try:
|
|
||||||
host_ptr = int(kv_buffer.data_ptr())
|
mode = "standalone-process" if is_standalone_process else "distributed"
|
||||||
host_size = int(kv_buffer.numel() * kv_buffer.element_size())
|
|
||||||
# When the buffer is backed by hugepages the mmap region is
|
|
||||||
# rounded up to the hugepage boundary. RDMA ibv_reg_mr on
|
|
||||||
# some NICs (AINIC / ROCm) requires the registered region to
|
|
||||||
# cover complete hugepages, so use the full mapped_size
|
|
||||||
# instead of the logical tensor size.
|
|
||||||
allocator = getattr(host_pool, "allocator", None)
|
allocator = getattr(host_pool, "allocator", None)
|
||||||
mapped_size_fn = getattr(allocator, "mapped_size_for", None)
|
# Already registered storage counts as covered.
|
||||||
if mapped_size_fn is not None:
|
covered = 0
|
||||||
mapped_size = mapped_size_fn(host_ptr)
|
for buffer in buffers:
|
||||||
else:
|
try:
|
||||||
mapped_size = getattr(allocator, "mapped_size", 0)
|
host_ptr, host_size = self._buffer_extent(buffer, allocator)
|
||||||
if mapped_size > host_size:
|
if host_ptr in self._registered_regions:
|
||||||
host_size = mapped_size
|
covered += 1
|
||||||
|
continue
|
||||||
ok = bool(self.client.register_memory(host_ptr, host_size))
|
ok = bool(self.client.register_memory(host_ptr, host_size))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
if is_standalone_process:
|
||||||
|
raise RuntimeError(
|
||||||
|
"UMBPStore: register_memory failed in standalone-process "
|
||||||
|
f"mode and cannot fall back: {exc}"
|
||||||
|
) from exc
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"UMBPStore: register_memory failed (%s); falling back to staging "
|
"UMBPStore: register_memory failed (%s); falling back to staging "
|
||||||
"buffer path. Per-transfer size will be capped by "
|
"buffer path. Per-transfer size will be capped by "
|
||||||
@@ -971,32 +1143,29 @@ class UMBPStore(HiCacheStorage):
|
|||||||
exc,
|
exc,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
if ok:
|
if not ok:
|
||||||
logger.info(
|
if is_standalone_process:
|
||||||
"UMBPStore: registered host KV buffer for RDMA zero-copy "
|
raise RuntimeError(
|
||||||
"(ptr=0x%x, size=%d MB)",
|
"UMBPStore: register_memory returned false in "
|
||||||
host_ptr,
|
"standalone-process mode; no fallback path exists."
|
||||||
host_size // (1024 * 1024),
|
|
||||||
)
|
)
|
||||||
return True
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"UMBPStore: register_memory returned false; staying on staging "
|
"UMBPStore: register_memory returned false; staying on staging "
|
||||||
"buffer fallback path."
|
"buffer fallback path."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
self._registered_regions.add(host_ptr)
|
||||||
|
covered += 1
|
||||||
|
logger.info(
|
||||||
|
"UMBPStore: registered host buffer for zero-copy "
|
||||||
|
"(ptr=0x%x, size=%d MB, mode=%s)",
|
||||||
|
host_ptr,
|
||||||
|
host_size // (1024 * 1024),
|
||||||
|
mode,
|
||||||
|
)
|
||||||
|
return covered == len(buffers)
|
||||||
|
|
||||||
def register_mem_host_pool_v2(self, host_pool: HostKVCache, host_pool_name):
|
def register_mem_host_pool_v2(self, host_pool: HostKVCache, host_pool_name):
|
||||||
"""Register an additional hybrid side pool (DeepSeek-V4 HostPoolGroup).
|
|
||||||
|
|
||||||
The controller calls this once per PoolEntry in the group, including the
|
|
||||||
KV anchor. The KV anchor is logical (no physical tensor) so we skip it;
|
|
||||||
its allocation-index role is unrelated to storage I/O. Every other pool
|
|
||||||
(SWA / compressed KV / indexer / state) carries a real page_first KV
|
|
||||||
buffer that must be (a) resolvable by name at v2 I/O time and (b)
|
|
||||||
registered with the RDMA IOEngine for zero-copy transfers.
|
|
||||||
"""
|
|
||||||
# KV anchor is either already registered via register_mem_pool_host()
|
|
||||||
# (non-hybrid single pool) or purely logical (hybrid group). Skip it.
|
|
||||||
if host_pool_name == PoolName.KV:
|
if host_pool_name == PoolName.KV:
|
||||||
return
|
return
|
||||||
self.registered_pools[host_pool_name] = host_pool
|
self.registered_pools[host_pool_name] = host_pool
|
||||||
@@ -1078,8 +1247,6 @@ class UMBPStore(HiCacheStorage):
|
|||||||
extra_info: Optional[HiCacheStorageExtraInfo] = None,
|
extra_info: Optional[HiCacheStorageExtraInfo] = None,
|
||||||
) -> List[bool]:
|
) -> List[bool]:
|
||||||
if self._kv_anchor_is_logical:
|
if self._kv_anchor_is_logical:
|
||||||
# DeepSeek-V4's KV anchor is logical only; the physical KV data is
|
|
||||||
# carried by the v2 side pools, so there is nothing to read here.
|
|
||||||
return [True] * len(keys)
|
return [True] * len(keys)
|
||||||
|
|
||||||
key_strs, buffer_ptrs, buffer_sizes = self._batch_preprocess(keys, host_indices)
|
key_strs, buffer_ptrs, buffer_sizes = self._batch_preprocess(keys, host_indices)
|
||||||
@@ -1152,8 +1319,6 @@ class UMBPStore(HiCacheStorage):
|
|||||||
return [True] * page_count
|
return [True] * page_count
|
||||||
|
|
||||||
if self._kv_anchor_is_logical:
|
if self._kv_anchor_is_logical:
|
||||||
# DeepSeek-V4's KV anchor is logical only; the physical KV data is
|
|
||||||
# written by the v2 side pools, so there is nothing to write here.
|
|
||||||
return [True] * len(keys)
|
return [True] * len(keys)
|
||||||
|
|
||||||
key_strs, buffer_ptrs, buffer_sizes = self._batch_preprocess(keys, host_indices)
|
key_strs, buffer_ptrs, buffer_sizes = self._batch_preprocess(keys, host_indices)
|
||||||
@@ -1219,47 +1384,66 @@ class UMBPStore(HiCacheStorage):
|
|||||||
return hit_count // key_multiplier
|
return hit_count // key_multiplier
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Multi-pool v2 interface (DeepSeek-V4 hybrid HiCache HostPoolGroup)
|
# Multi-pool v2 interface
|
||||||
#
|
|
||||||
# The DeepSeek-V4 HiCache stack splits KV state across several page_first
|
|
||||||
# side pools (SWA / compressed KV / indexer / state), coordinated by a
|
|
||||||
# logical KV anchor that owns only page indices. The controller registers
|
|
||||||
# each real pool through register_mem_host_pool_v2() and drives storage
|
|
||||||
# via these _v2 methods, one PoolTransfer per pool. This mirrors the proven
|
|
||||||
# MooncakeStore / HiCacheHF3FS design, specialized for UMBP's page_first,
|
|
||||||
# single-object-per-page layout (each page -> exactly one storage object).
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def _get_hybrid_page_component_keys(self, page_keys, transfer: PoolTransfer):
|
def _get_hybrid_page_component_keys(
|
||||||
"""Map per-page logical keys to per-object storage keys for a side pool.
|
self, page_keys, transfer: PoolTransfer, *, rank_suffix: Optional[str] = None
|
||||||
|
):
|
||||||
For UMBP every registered side pool is page_first and stores one object
|
"""Expand logical page keys for one registered hybrid side pool."""
|
||||||
per page (MLA: a single K object; MHA: a K and a V object), so the
|
|
||||||
component-key count is an exact multiple of the page count. The pool
|
|
||||||
name is embedded in the suffix so pages that share a hash across pools
|
|
||||||
never collide.
|
|
||||||
"""
|
|
||||||
pool_name = transfer.name
|
pool_name = transfer.name
|
||||||
host_pool = self.registered_pools.get(pool_name)
|
host_pool = self.registered_pools.get(pool_name)
|
||||||
if host_pool is None:
|
if host_pool is None:
|
||||||
raise ValueError(f"Unregistered UMBP hybrid pool: {pool_name}")
|
raise ValueError(f"Unregistered UMBP hybrid pool: {pool_name}")
|
||||||
|
|
||||||
if self.is_mla_backend:
|
mla_suffix = self.mla_suffix if rank_suffix is None else rank_suffix
|
||||||
# Single compressed object per page.
|
mha_suffix = (
|
||||||
suffixes = [f"_{self.mla_suffix}_{pool_name}"]
|
getattr(self, "mha_suffix", mla_suffix)
|
||||||
|
if rank_suffix is None
|
||||||
|
else rank_suffix
|
||||||
|
)
|
||||||
|
|
||||||
|
components = getattr(host_pool, "components", None)
|
||||||
|
if pool_name == PoolName.MAMBA:
|
||||||
|
conv_num = len(getattr(host_pool, "conv_buffer", None) or [])
|
||||||
|
suffixes = [f"_{mha_suffix}_conv_{i}" for i in range(conv_num)]
|
||||||
|
if getattr(host_pool, "temporal_state_elem_size", 1) > 0:
|
||||||
|
suffixes = [f"_{mha_suffix}_temporal"] + suffixes
|
||||||
|
elif components is not None and len(components) == 1:
|
||||||
|
suffixes = [f"_{mla_suffix}_{pool_name}"]
|
||||||
|
elif components is not None and len(components) == 2:
|
||||||
|
# Packed DevicePoolEntry K/V components share one stored object.
|
||||||
|
suffixes = (
|
||||||
|
[f"_{mha_suffix}_{pool_name}"]
|
||||||
|
if host_pool.packed
|
||||||
|
else [
|
||||||
|
f"_{mha_suffix}_{pool_name}_k",
|
||||||
|
f"_{mha_suffix}_{pool_name}_v",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif components is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported UMBP component count for pool {pool_name}: "
|
||||||
|
f"{len(components)}"
|
||||||
|
)
|
||||||
|
elif self.is_mla_backend:
|
||||||
|
suffixes = [f"_{mla_suffix}_{pool_name}"]
|
||||||
elif getattr(host_pool, "v_buffer", None) is not None:
|
elif getattr(host_pool, "v_buffer", None) is not None:
|
||||||
# Ordinary MHA side pool mirrors a K/V pool.
|
|
||||||
suffixes = [
|
suffixes = [
|
||||||
f"_{self.mha_suffix}_{pool_name}_k",
|
f"_{mha_suffix}_{pool_name}_k",
|
||||||
f"_{self.mha_suffix}_{pool_name}_v",
|
f"_{mha_suffix}_{pool_name}_v",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
suffixes = [f"_{self.mha_suffix}_{pool_name}"]
|
suffixes = [f"_{mha_suffix}_{pool_name}"]
|
||||||
|
|
||||||
key_multiplier = len(suffixes)
|
|
||||||
component_keys = [
|
component_keys = [
|
||||||
f"{page_key}{suffix}" for page_key in page_keys for suffix in suffixes
|
f"{page_key}{suffix}" for page_key in page_keys for suffix in suffixes
|
||||||
]
|
]
|
||||||
return component_keys, key_multiplier
|
if self.config_prefix:
|
||||||
|
component_keys = [
|
||||||
|
f"{self.config_prefix}_{component_key}"
|
||||||
|
for component_key in component_keys
|
||||||
|
]
|
||||||
|
return component_keys, len(suffixes)
|
||||||
|
|
||||||
def batch_exists_v2(
|
def batch_exists_v2(
|
||||||
self,
|
self,
|
||||||
@@ -1267,20 +1451,18 @@ class UMBPStore(HiCacheStorage):
|
|||||||
pool_transfers: Optional[List[PoolTransfer]] = None,
|
pool_transfers: Optional[List[PoolTransfer]] = None,
|
||||||
extra_info: Optional[HiCacheStorageExtraInfo] = None,
|
extra_info: Optional[HiCacheStorageExtraInfo] = None,
|
||||||
) -> PoolTransferResult:
|
) -> PoolTransferResult:
|
||||||
if self._kv_anchor_is_logical:
|
kv_pages = (
|
||||||
# Logical KV anchor: no physical KV object exists in UMBP, so the
|
len(keys)
|
||||||
# usable prefix is bounded entirely by the required side pools.
|
if self._kv_anchor_is_logical
|
||||||
kv_pages = len(keys)
|
else self.batch_exists(keys, extra_info)
|
||||||
else:
|
)
|
||||||
kv_pages = self.batch_exists(keys, extra_info)
|
|
||||||
|
|
||||||
hit_count: dict = {PoolName.KV: kv_pages} if kv_pages else {}
|
hit_count: dict = {PoolName.KV: kv_pages} if kv_pages else {}
|
||||||
final_pages = kv_pages
|
final_pages = kv_pages
|
||||||
|
|
||||||
for transfer in pool_transfers or []:
|
for transfer in pool_transfers or []:
|
||||||
if final_pages == 0:
|
if final_pages == 0:
|
||||||
break
|
break
|
||||||
component_keys, key_multiplier = self._get_hybrid_page_component_keys(
|
component_keys, multiplier = self._get_hybrid_page_component_keys(
|
||||||
keys[:final_pages], transfer
|
keys[:final_pages], transfer
|
||||||
)
|
)
|
||||||
exists = list(self.client.batch_exists(component_keys))
|
exists = list(self.client.batch_exists(component_keys))
|
||||||
@@ -1294,18 +1476,15 @@ class UMBPStore(HiCacheStorage):
|
|||||||
)
|
)
|
||||||
final_pages = 0
|
final_pages = 0
|
||||||
break
|
break
|
||||||
# Collapse per-object results into per-page presence.
|
|
||||||
page_exists = [
|
page_exists = [
|
||||||
all(exists[i * key_multiplier : (i + 1) * key_multiplier])
|
all(exists[i * multiplier : (i + 1) * multiplier])
|
||||||
for i in range(final_pages)
|
for i in range(final_pages)
|
||||||
]
|
]
|
||||||
|
|
||||||
boundary = 0
|
boundary = 0
|
||||||
if transfer.hit_policy == PoolHitPolicy.ALL_PAGES:
|
if transfer.hit_policy == PoolHitPolicy.ALL_PAGES:
|
||||||
try:
|
boundary = (
|
||||||
boundary = page_exists.index(False)
|
page_exists.index(False) if False in page_exists else final_pages
|
||||||
except ValueError:
|
)
|
||||||
boundary = final_pages
|
|
||||||
elif transfer.hit_policy == PoolHitPolicy.TRAILING_PAGES:
|
elif transfer.hit_policy == PoolHitPolicy.TRAILING_PAGES:
|
||||||
trailing = max(1, len(transfer.keys) if transfer.keys else 1)
|
trailing = max(1, len(transfer.keys) if transfer.keys else 1)
|
||||||
for prefix_len in range(final_pages, 0, -1):
|
for prefix_len in range(final_pages, 0, -1):
|
||||||
@@ -1334,38 +1513,26 @@ class UMBPStore(HiCacheStorage):
|
|||||||
if not keys or host_indices is None:
|
if not keys or host_indices is None:
|
||||||
results[transfer.name] = [False] * len(keys)
|
results[transfer.name] = [False] * len(keys)
|
||||||
continue
|
continue
|
||||||
assert len(keys) == len(host_indices) // page_size
|
if len(keys) != len(host_indices) // page_size:
|
||||||
|
raise ValueError(
|
||||||
key_strs, key_multiplier = self._get_hybrid_page_component_keys(
|
f"UMBP v2 pool {transfer.name} has {len(keys)} keys for "
|
||||||
keys, transfer
|
f"{len(host_indices)} indices with page_size={page_size}."
|
||||||
)
|
)
|
||||||
ptr_list, element_size_list = host_pool.get_page_buffer_meta(host_indices)
|
|
||||||
# page_first side pools emit exactly one (ptr, size) per component
|
key_strs, multiplier = self._get_hybrid_page_component_keys(keys, transfer)
|
||||||
# key; assert the invariant so any future layout change is caught
|
ptrs, sizes = host_pool.get_page_buffer_meta(host_indices)
|
||||||
# loudly instead of silently corrupting the key<->buffer zip.
|
if not len(key_strs) == len(ptrs) == len(sizes):
|
||||||
assert len(key_strs) == len(ptr_list) == len(element_size_list), (
|
raise ValueError(
|
||||||
f"UMBP v2 buffer-meta mismatch for pool {transfer.name}: "
|
f"UMBP v2 buffer-meta mismatch for pool {transfer.name}: "
|
||||||
f"keys={len(key_strs)} ptrs={len(ptr_list)} sizes={len(element_size_list)}"
|
f"keys={len(key_strs)} ptrs={len(ptrs)} sizes={len(sizes)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_set:
|
operation = (
|
||||||
# UMBP performs its own key-level deduplication, so skip the
|
self.client.batch_put_from_ptr
|
||||||
# extra batch_exists round-trip and put directly (mirrors
|
if is_set
|
||||||
# batch_set_v1).
|
else self.client.batch_get_into_ptr
|
||||||
io_results = [
|
|
||||||
bool(r)
|
|
||||||
for r in self.client.batch_put_from_ptr(
|
|
||||||
key_strs, list(ptr_list), list(element_size_list)
|
|
||||||
)
|
)
|
||||||
]
|
io_results = [bool(value) for value in operation(key_strs, ptrs, sizes)]
|
||||||
else:
|
|
||||||
io_results = [
|
|
||||||
bool(r)
|
|
||||||
for r in self.client.batch_get_into_ptr(
|
|
||||||
key_strs, list(ptr_list), list(element_size_list)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(io_results) != len(key_strs):
|
if len(io_results) != len(key_strs):
|
||||||
logger.error(
|
logger.error(
|
||||||
"UMBP v2 %s result-size mismatch for pool %s: "
|
"UMBP v2 %s result-size mismatch for pool %s: "
|
||||||
@@ -1378,9 +1545,8 @@ class UMBPStore(HiCacheStorage):
|
|||||||
results[transfer.name] = [False] * len(keys)
|
results[transfer.name] = [False] * len(keys)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Collapse per-object results back to per-page results.
|
|
||||||
results[transfer.name] = [
|
results[transfer.name] = [
|
||||||
all(io_results[i * key_multiplier : (i + 1) * key_multiplier])
|
all(io_results[i * multiplier : (i + 1) * multiplier])
|
||||||
for i in range(len(keys))
|
for i in range(len(keys))
|
||||||
]
|
]
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -2842,7 +2842,7 @@ class ServerArgs:
|
|||||||
str,
|
str,
|
||||||
Arg(
|
Arg(
|
||||||
help="Storage backend for --enable-unified-cache-external-linker.",
|
help="Storage backend for --enable-unified-cache-external-linker.",
|
||||||
choices=["mooncake"],
|
choices=["mooncake", "mori"],
|
||||||
),
|
),
|
||||||
NS("memory"),
|
NS("memory"),
|
||||||
] = "mooncake"
|
] = "mooncake"
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class TestHiCacheStorageUMBPBackend(CustomTestCase):
|
|||||||
# An absent master address keeps every TP rank in standalone local mode,
|
# An absent master address keeps every TP rank in standalone local mode,
|
||||||
# so this E2E does not require an RDMA-capable CI runner.
|
# so this E2E does not require an RDMA-capable CI runner.
|
||||||
env.pop("UMBP_MASTER_ADDRESS", None)
|
env.pop("UMBP_MASTER_ADDRESS", None)
|
||||||
|
env.pop("UMBP_STANDALONE_ADDRESS", None)
|
||||||
env.update(
|
env.update(
|
||||||
{
|
{
|
||||||
"SGLANG_ENABLE_DETERMINISTIC_INFERENCE": "1",
|
"SGLANG_ENABLE_DETERMINISTIC_INFERENCE": "1",
|
||||||
|
|||||||
@@ -336,6 +336,57 @@ class TestDefaultRadixCacheFactory(CustomTestCase):
|
|||||||
ctx.tp_worker.register_hicache_layer_transfer_counter.assert_called_once()
|
ctx.tp_worker.register_hicache_layer_transfer_counter.assert_called_once()
|
||||||
self.assertIs(result, fake_radix.UnifiedRadixCache.return_value)
|
self.assertIs(result, fake_radix.UnifiedRadixCache.return_value)
|
||||||
|
|
||||||
|
def test_unified_radix_cache_with_mori_external_linker(self):
|
||||||
|
from sglang.srt.mem_cache.storage.umbp import umbp_direct_linker
|
||||||
|
|
||||||
|
ctx = _make_ctx(self)
|
||||||
|
object.__setattr__(
|
||||||
|
ctx.server_args, "enable_unified_cache_external_linker", True
|
||||||
|
)
|
||||||
|
object.__setattr__(
|
||||||
|
ctx.server_args, "unified_cache_external_linker_backend", "mori"
|
||||||
|
)
|
||||||
|
self.assertTrue(ctx.server_args.enable_unified_cache_external_linker)
|
||||||
|
self.assertEqual(ctx.server_args.unified_cache_external_linker_backend, "mori")
|
||||||
|
fake_components = MagicMock()
|
||||||
|
fake_components.ComponentType.FULL = "full"
|
||||||
|
fake_radix = MagicMock()
|
||||||
|
cache = fake_radix.UnifiedRadixCache.return_value
|
||||||
|
cache.components = ("full",)
|
||||||
|
counter = MagicMock(name="layer_done_counter")
|
||||||
|
cache.linker.layer_done_counter = counter
|
||||||
|
linker = MagicMock(name="linker")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict(
|
||||||
|
"sys.modules",
|
||||||
|
{
|
||||||
|
"sglang.srt.mem_cache.unified_cache.components": fake_components,
|
||||||
|
"sglang.srt.mem_cache.unified_radix_cache": fake_radix,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
umbp_direct_linker,
|
||||||
|
"UMBPDirectLinker",
|
||||||
|
return_value=linker,
|
||||||
|
) as linker_cls,
|
||||||
|
):
|
||||||
|
result = default_radix_cache_factory(ctx)
|
||||||
|
|
||||||
|
linker_cls.assert_called_once_with(
|
||||||
|
ctx.server_args,
|
||||||
|
ctx.params,
|
||||||
|
components={"full"},
|
||||||
|
)
|
||||||
|
cache.init_cache_linker.assert_called_once_with(linker)
|
||||||
|
ctx.params.token_to_kv_pool_allocator.get_kvcache.return_value.register_layer_transfer_counter.assert_called_once_with(
|
||||||
|
counter
|
||||||
|
)
|
||||||
|
ctx.tp_worker.register_hicache_layer_transfer_counter.assert_called_once_with(
|
||||||
|
counter
|
||||||
|
)
|
||||||
|
self.assertIs(result, cache)
|
||||||
|
|
||||||
def test_swa_radix_cache_when_hybrid_swa(self):
|
def test_swa_radix_cache_when_hybrid_swa(self):
|
||||||
ctx = _make_ctx(self, is_hybrid_swa=True)
|
ctx = _make_ctx(self, is_hybrid_swa=True)
|
||||||
# SWA hybrid models now default to the unified radix tree.
|
# SWA hybrid models now default to the unified radix tree.
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
|||||||
class FakeBacking(Enum):
|
class FakeBacking(Enum):
|
||||||
Anonymous = 0
|
Anonymous = 0
|
||||||
AnonymousHugetlb = 1
|
AnonymousHugetlb = 1
|
||||||
|
AnonymousShm = 2
|
||||||
|
AnonymousShmHugetlb = 3
|
||||||
|
|
||||||
|
|
||||||
class FakeHandle:
|
class FakeHandle:
|
||||||
@@ -64,7 +66,10 @@ class FakeHostMemAllocator:
|
|||||||
mapped_size=size,
|
mapped_size=size,
|
||||||
actual_backing=backing,
|
actual_backing=backing,
|
||||||
actual_alignment=(
|
actual_alignment=(
|
||||||
hugepage_size if backing == FakeBacking.AnonymousHugetlb else 4096
|
hugepage_size
|
||||||
|
if backing
|
||||||
|
in (FakeBacking.AnonymousHugetlb, FakeBacking.AnonymousShmHugetlb)
|
||||||
|
else 4096
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.alloc_calls.append(
|
self.alloc_calls.append(
|
||||||
@@ -142,6 +147,37 @@ class TestUMBPHostAllocator(unittest.TestCase):
|
|||||||
tensor.fill_(3.0)
|
tensor.fill_(3.0)
|
||||||
self.assertEqual(float(tensor[0, 0]), 3.0)
|
self.assertEqual(float(tensor[0, 0]), 3.0)
|
||||||
|
|
||||||
|
def test_standalone_process_uses_shareable_backing(self):
|
||||||
|
self._install_fake_mori()
|
||||||
|
|
||||||
|
from sglang.srt.mem_cache.storage.umbp.umbp_host_allocator import (
|
||||||
|
UMBPHostTensorAllocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
cases = (
|
||||||
|
("0", FakeBacking.AnonymousShm),
|
||||||
|
("1", FakeBacking.AnonymousShmHugetlb),
|
||||||
|
)
|
||||||
|
for use_hugepage, expected in cases:
|
||||||
|
with (
|
||||||
|
self.subTest(use_hugepage=use_hugepage),
|
||||||
|
mock.patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
"UMBP_STANDALONE_ADDRESS": "unix:///tmp/umbp-test.sock",
|
||||||
|
"SGLANG_HICACHE_HOST_HUGEPAGE": use_hugepage,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
):
|
||||||
|
allocator = UMBPHostTensorAllocator()
|
||||||
|
tensor = allocator.allocate((16,), dtype=torch.uint8, device="cpu")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
allocator._allocator.alloc_calls[0]["backing"], expected
|
||||||
|
)
|
||||||
|
del tensor
|
||||||
|
allocator.__del__()
|
||||||
|
|
||||||
def test_umbp_allocator_del_calls_free_once(self):
|
def test_umbp_allocator_del_calls_free_once(self):
|
||||||
self._install_fake_mori()
|
self._install_fake_mori()
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,55 @@ def make_indices(indices):
|
|||||||
|
|
||||||
|
|
||||||
class TestUMBPStore(unittest.TestCase):
|
class TestUMBPStore(unittest.TestCase):
|
||||||
|
def test_standalone_process_configuration(self):
|
||||||
|
from sglang.srt.mem_cache.storage.umbp import umbp_store
|
||||||
|
|
||||||
|
imported = list(umbp_store._import_umbp_client())
|
||||||
|
captured = []
|
||||||
|
|
||||||
|
def make_client(config):
|
||||||
|
captured.append(config)
|
||||||
|
client = MagicMock()
|
||||||
|
client.flush.return_value = True
|
||||||
|
return client
|
||||||
|
|
||||||
|
imported[0] = make_client
|
||||||
|
config = MockStorageConfig(
|
||||||
|
extra_config={
|
||||||
|
"standalone_address": "unix:///tmp/umbp-test.sock",
|
||||||
|
"standalone_auto_start": False,
|
||||||
|
"standalone_startup_timeout_ms": 1234,
|
||||||
|
"ssd_enabled": False,
|
||||||
|
"extra_backend_tag": "tenant-a",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with patch.object(
|
||||||
|
umbp_store, "_import_umbp_client", return_value=tuple(imported)
|
||||||
|
):
|
||||||
|
store = umbp_store.UMBPStore(config, mem_pool_host=None)
|
||||||
|
|
||||||
|
self.assertEqual(len(captured), 1)
|
||||||
|
self.assertIsNone(captured[0].distributed)
|
||||||
|
self.assertEqual(
|
||||||
|
captured[0].standalone_process.address, "unix:///tmp/umbp-test.sock"
|
||||||
|
)
|
||||||
|
self.assertFalse(captured[0].standalone_process.auto_start)
|
||||||
|
self.assertEqual(captured[0].standalone_process.startup_timeout_ms, 1234)
|
||||||
|
self.assertEqual(store.config_prefix, "tenant-a_test-model")
|
||||||
|
store.close()
|
||||||
|
|
||||||
|
def test_standalone_and_distributed_addresses_are_mutually_exclusive(self):
|
||||||
|
from sglang.srt.mem_cache.storage.umbp import umbp_store
|
||||||
|
|
||||||
|
config = MockStorageConfig(
|
||||||
|
extra_config={
|
||||||
|
"master_address": "127.0.0.1:1234",
|
||||||
|
"standalone_address": "unix:///tmp/umbp-test.sock",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "mutually exclusive"):
|
||||||
|
umbp_store.UMBPStore(config, mem_pool_host=None)
|
||||||
|
|
||||||
def test_basic_set_get(self):
|
def test_basic_set_get(self):
|
||||||
from sglang.srt.mem_cache.storage.umbp.umbp_store import UMBPStore
|
from sglang.srt.mem_cache.storage.umbp.umbp_store import UMBPStore
|
||||||
|
|
||||||
@@ -326,6 +375,7 @@ class TestUMBPStoreDefensiveSemantics(unittest.TestCase):
|
|||||||
store.is_mla_backend = True
|
store.is_mla_backend = True
|
||||||
store.mla_suffix = ""
|
store.mla_suffix = ""
|
||||||
store.mha_suffix = "0"
|
store.mha_suffix = "0"
|
||||||
|
store.config_prefix = None
|
||||||
store.register_mem_host_pool_v2(MockHybridSidePool(), PoolName.DEEPSEEK_V4_C4)
|
store.register_mem_host_pool_v2(MockHybridSidePool(), PoolName.DEEPSEEK_V4_C4)
|
||||||
return store
|
return store
|
||||||
|
|
||||||
@@ -345,6 +395,7 @@ class TestUMBPStoreDefensiveSemantics(unittest.TestCase):
|
|||||||
spdk_proxy_tenant_quota_bytes=0,
|
spdk_proxy_tenant_quota_bytes=0,
|
||||||
)
|
)
|
||||||
self.distributed = None
|
self.distributed = None
|
||||||
|
self.standalone_process = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_environment(cls):
|
def from_environment(cls):
|
||||||
@@ -366,6 +417,8 @@ class TestUMBPStoreDefensiveSemantics(unittest.TestCase):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
config = MockStorageConfig(
|
config = MockStorageConfig(
|
||||||
extra_config={"dram_capacity_bytes": 1024, "ssd_enabled": False}
|
extra_config={"dram_capacity_bytes": 1024, "ssd_enabled": False}
|
||||||
|
|||||||
Reference in New Issue
Block a user