94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
310 lines
12 KiB
Python
310 lines
12 KiB
Python
"""HiCache integration mixins for the decode side of PD disaggregation"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, Any, List, Optional
|
|
|
|
import torch
|
|
|
|
from sglang.srt.disaggregation.base import KVPoll
|
|
from sglang.srt.managers.schedule_policy import match_prefix_for_req
|
|
from sglang.srt.mem_cache.base_prefix_cache import InitLoadBackParams
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.disaggregation.decode import DecodeRequest
|
|
from sglang.srt.managers.schedule_batch import Req
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class DecodePrefixMatch:
|
|
prefix_indices: torch.Tensor
|
|
l2_host_hit_length: int
|
|
l3_storage_hit_length: int
|
|
last_device_node: Any
|
|
last_host_node: Any = None
|
|
prefetch_registered: bool = False
|
|
|
|
@property
|
|
def l1_prefix_len(self) -> int:
|
|
return len(self.prefix_indices)
|
|
|
|
@property
|
|
def decode_prefix_len(self) -> int:
|
|
return self.l1_prefix_len + self.l2_host_hit_length + self.l3_storage_hit_length
|
|
|
|
@property
|
|
def needs_local_restore(self) -> bool:
|
|
return self.decode_prefix_len > self.l1_prefix_len
|
|
|
|
@property
|
|
def restore_token_count(self) -> int:
|
|
"""Number of tokens that need L2/L3 load_back to device."""
|
|
return self.decode_prefix_len - self.l1_prefix_len
|
|
|
|
|
|
class HiCacheRestoreResult(Enum):
|
|
"""Outcome of one tick of the HiCache local-restore state machine."""
|
|
|
|
PENDING = "pending"
|
|
READY = "ready"
|
|
FAILED = "failed"
|
|
|
|
|
|
class DecodeHiCachePreallocMixin:
|
|
"""HiCache hooks for ``DecodePreallocQueue``: issue prefetch + reserve tokens."""
|
|
|
|
def _build_decode_prefix_match(self, req: Req, result: Any) -> DecodePrefixMatch:
|
|
"""Convert a ``match_prefix_for_req`` result into ``DecodePrefixMatch``.
|
|
|
|
Performs the optional L3 storage hit length query when decode-side
|
|
HiCache is enabled and the last host node is backed up.
|
|
"""
|
|
prefix_indices = result.device_indices
|
|
l1_prefix_len = len(prefix_indices)
|
|
l2_host_hit_length = result.host_hit_length
|
|
|
|
l3_storage_hit_length = 0
|
|
last_host_node = None
|
|
if self.scheduler.enable_decode_hicache:
|
|
last_host_node = result.last_host_node
|
|
if last_host_node.backuped or last_host_node is self.tree_cache.root_node:
|
|
matched_len = l1_prefix_len + l2_host_hit_length
|
|
suffix_tokens = req.origin_input_ids[matched_len:]
|
|
last_hash = last_host_node.get_last_hash_value()
|
|
prefix_keys = (
|
|
last_host_node.get_prefix_hash_values(last_host_node.parent)
|
|
if self.tree_cache.hicache_storage_pass_prefix_keys
|
|
else None
|
|
)
|
|
l3_storage_hit_length = self.tree_cache.query_storage_hit_length(
|
|
last_host_node,
|
|
suffix_tokens,
|
|
last_hash,
|
|
prefix_keys,
|
|
)
|
|
|
|
return DecodePrefixMatch(
|
|
prefix_indices=prefix_indices,
|
|
l2_host_hit_length=l2_host_hit_length,
|
|
l3_storage_hit_length=l3_storage_hit_length,
|
|
last_device_node=result.last_device_node,
|
|
last_host_node=last_host_node if l3_storage_hit_length > 0 else None,
|
|
)
|
|
|
|
def _start_hicache_prefetch(
|
|
self, req: Req, prefix_match: Optional[DecodePrefixMatch]
|
|
) -> None:
|
|
"""Issue L3 storage prefetch after admission succeeds.
|
|
|
|
On failure, degrades to L2-only restore by clearing l3 fields.
|
|
"""
|
|
if (
|
|
prefix_match is None
|
|
or prefix_match.l3_storage_hit_length <= 0
|
|
or prefix_match.last_host_node is None
|
|
):
|
|
return
|
|
try:
|
|
node = prefix_match.last_host_node
|
|
matched_len = prefix_match.l1_prefix_len + prefix_match.l2_host_hit_length
|
|
suffix = req.origin_input_ids[
|
|
matched_len : matched_len + prefix_match.l3_storage_hit_length
|
|
]
|
|
last_hash = node.get_last_hash_value()
|
|
prefix_keys = (
|
|
node.get_prefix_hash_values(node.parent)
|
|
if self.tree_cache.hicache_storage_pass_prefix_keys
|
|
else None
|
|
)
|
|
self.tree_cache.prefetch_from_storage(
|
|
req.rid, node, suffix, last_hash, prefix_keys
|
|
)
|
|
prefix_match.prefetch_registered = (
|
|
req.rid in self.tree_cache.ongoing_prefetch
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
"HiCache L3 prefetch failed for rid=%s: %s; falling back to L2-only LoadingBack",
|
|
req.rid,
|
|
e,
|
|
)
|
|
prefix_match.l3_storage_hit_length = 0
|
|
prefix_match.prefetch_registered = False
|
|
|
|
def _hicache_pending_restore_tokens(self) -> int:
|
|
"""Total device tokens reserved for pending HiCache L2/L3 load_back."""
|
|
if not self.scheduler.enable_decode_hicache:
|
|
return 0
|
|
return sum(
|
|
dr.prefix_match.restore_token_count
|
|
for dr in self.transfer_queue.queue
|
|
if dr.prefix_match is not None
|
|
and dr.hicache_restore_status == HiCacheRestoreResult.PENDING
|
|
and dr.hicache_restored_node is None
|
|
)
|
|
|
|
|
|
class HiCacheRestoreGatedKVReceiver:
|
|
"""Wraps a kv_receiver so KVPoll.Success is gated on HiCache restore READY."""
|
|
|
|
def __init__(self, decode_req: DecodeRequest):
|
|
self.decode_req = decode_req
|
|
|
|
def poll(self) -> KVPoll:
|
|
poll = self.decode_req.kv_receiver.poll()
|
|
if (
|
|
poll == KVPoll.Success
|
|
and self.decode_req.hicache_restore_status == HiCacheRestoreResult.PENDING
|
|
):
|
|
return KVPoll.Transferring
|
|
return poll
|
|
|
|
|
|
class DecodeHiCacheTransferMixin:
|
|
"""HiCache hooks for ``DecodeTransferQueue``: drive restore state machine."""
|
|
|
|
def _clean_hicache_prefetch_resources(self, decode_req: DecodeRequest) -> None:
|
|
if (
|
|
decode_req.prefix_match is not None
|
|
and decode_req.prefix_match.prefetch_registered
|
|
):
|
|
self.tree_cache.release_aborted_request(decode_req.req.rid)
|
|
if decode_req.hicache_restored_node is not None:
|
|
self.tree_cache.dec_lock_ref(decode_req.hicache_restored_node)
|
|
decode_req.hicache_restored_node = None
|
|
|
|
def _try_hicache_queue_load_back(self, dr: DecodeRequest) -> bool:
|
|
"""Queue one L2->L1 load_back op for ``dr``; True iff a DMA was queued.
|
|
|
|
On success, ``dr.hicache_restored_node`` and ``hicache_restored_kv_indices``
|
|
are populated, and an inc_lock_ref is held until commit/abort.
|
|
Trivial cases (all-on-device / no needed coverage) auto-flip to READY.
|
|
Failback paths flip to FAILED.
|
|
"""
|
|
pm = dr.prefix_match
|
|
|
|
# Wait for L3 -> L2 prefetch to drain (skip when no L3 hit).
|
|
if pm.l3_storage_hit_length > 0:
|
|
if not self.tree_cache.check_prefetch_progress(dr.req.rid):
|
|
return False
|
|
self.tree_cache.pop_prefetch_loaded_tokens(dr.req.rid)
|
|
|
|
# Re-match: req.last_node / prefix_indices updated to current device state.
|
|
rematch = match_prefix_for_req(
|
|
self.tree_cache,
|
|
dr.req,
|
|
dr.req.origin_input_ids,
|
|
cow_mamba=False,
|
|
include_req=True,
|
|
)
|
|
new_indices, restored_node = self.tree_cache.init_load_back(
|
|
InitLoadBackParams(
|
|
best_match_node=rematch.best_match_node,
|
|
host_hit_length=rematch.host_hit_length,
|
|
req=dr.req,
|
|
)
|
|
)
|
|
# Failback: total coverage < required prefix means device alloc likely failed.
|
|
if len(rematch.device_indices) + len(new_indices) < pm.decode_prefix_len:
|
|
logger.warning(
|
|
"HiCache load_back failed for rid=%s: device_indices=%d, "
|
|
"new_indices=%d, expected decode_prefix_len=%d (l1=%d, l2=%d, l3=%d)",
|
|
dr.req.rid,
|
|
len(rematch.device_indices),
|
|
len(new_indices),
|
|
pm.decode_prefix_len,
|
|
pm.l1_prefix_len,
|
|
pm.l2_host_hit_length,
|
|
pm.l3_storage_hit_length,
|
|
)
|
|
dr.hicache_restore_status = HiCacheRestoreResult.FAILED
|
|
return False
|
|
|
|
dr.hicache_restored_kv_indices = torch.cat(
|
|
[rematch.device_indices[pm.l1_prefix_len :], new_indices]
|
|
)
|
|
dr.hicache_restored_node = restored_node
|
|
self.tree_cache.inc_lock_ref(restored_node)
|
|
|
|
if len(new_indices) == 0:
|
|
# Whole prefix already on device; no DMA needed.
|
|
dr.hicache_restore_status = HiCacheRestoreResult.READY
|
|
return False
|
|
return True
|
|
|
|
def _process_hicache_local_restores(self, decode_reqs: List[DecodeRequest]) -> None:
|
|
if not hasattr(self.tree_cache, "is_load_back_event_done"):
|
|
return
|
|
|
|
# Filter once: keep only PENDING reqs that still need restore work;
|
|
# trivially-done reqs (no prefix_match / nothing to restore) flip to READY.
|
|
active: List[DecodeRequest] = []
|
|
for dr in decode_reqs:
|
|
if dr.hicache_restore_status != HiCacheRestoreResult.PENDING:
|
|
continue
|
|
pm = dr.prefix_match
|
|
if pm is None or not pm.needs_local_restore:
|
|
dr.hicache_restore_status = HiCacheRestoreResult.READY
|
|
continue
|
|
active.append(dr)
|
|
|
|
# Phase A: advance in-flight DMAs to READY.
|
|
for dr in active:
|
|
if (
|
|
dr.hicache_restored_node is not None
|
|
and self.tree_cache.is_load_back_event_done(
|
|
dr.hicache_load_consumer_index
|
|
)
|
|
):
|
|
dr.hicache_restore_status = HiCacheRestoreResult.READY
|
|
|
|
# Phase B: queue new load_back ops if the next slot is free.
|
|
# The (producer_index + 1) check ensures we never overwrite a still-in-flight slot:
|
|
# if a previous req holds that slot and isn't done, its event won't be signaled.
|
|
counter = self.tree_cache.cache_controller.layer_done_counter
|
|
if not self.tree_cache.is_load_back_event_done(
|
|
(counter.producer_index + 1) % counter.num_counters
|
|
):
|
|
return
|
|
queued = [
|
|
dr
|
|
for dr in active
|
|
if dr.hicache_restored_node is None
|
|
and self._try_hicache_queue_load_back(dr)
|
|
]
|
|
if not queued:
|
|
return
|
|
|
|
# Phase C: kick off merged DMA, bind consumer_index for Phase A polling next tick.
|
|
consumer_index = self.tree_cache.ready_to_load_host_cache()
|
|
if consumer_index < 0:
|
|
for dr in queued:
|
|
dr.hicache_restore_status = HiCacheRestoreResult.READY
|
|
return
|
|
for dr in queued:
|
|
dr.hicache_load_consumer_index = consumer_index
|
|
|
|
def _commit_hicache_local_restore_to_req(self, decode_req: DecodeRequest) -> None:
|
|
prefix_match = decode_req.prefix_match
|
|
if prefix_match is None or not prefix_match.needs_local_restore:
|
|
return
|
|
|
|
self.tree_cache.dec_lock_ref(prefix_match.last_device_node)
|
|
|
|
self.tree_cache.req_to_token_pool.write(
|
|
(
|
|
decode_req.req.req_pool_idx,
|
|
slice(prefix_match.l1_prefix_len, prefix_match.decode_prefix_len),
|
|
),
|
|
decode_req.hicache_restored_kv_indices,
|
|
)
|
|
decode_req.req.prefix_indices = torch.cat(
|
|
[prefix_match.prefix_indices, decode_req.hicache_restored_kv_indices]
|
|
)
|
|
decode_req.req.last_node = decode_req.hicache_restored_node
|