[Bugfix][HiCache] measure load-back duration with CUDA events (#26411)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: vuuihc <vuuihc@users.noreply.github.com>
Co-authored-by: Zhiqiang Xie <xiezhq@stanford.edu>
This commit is contained in:
Ethan ZHU
2026-07-16 00:11:04 -07:00
committed by GitHub
co-authored by Claude Opus 4.7 vuuihc Zhiqiang Xie
parent f8eac995aa
commit 095a817612
10 changed files with 253 additions and 71 deletions
@@ -217,9 +217,9 @@ class DecodeKVCacheOffloadManager:
def _check_offload_progress(self, finish_count):
"""Check the progress of offload from device to host."""
while finish_count > 0:
_, finish_event, ack_list = self.cache_controller.ack_write_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = self.cache_controller.ack_write_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
(
req,
host_indices,
+31 -2
View File
@@ -16,6 +16,7 @@ limitations under the License.
import logging
import threading
import time
from functools import cache
from queue import Empty, Queue
from typing import TYPE_CHECKING, List, NamedTuple, Optional
@@ -46,6 +47,26 @@ logger = logging.getLogger(__name__)
device_module = get_device_module()
@cache
def _timing_events_supported() -> bool:
try:
device_module.Event(enable_timing=True)
return True
except (TypeError, NotImplementedError):
logger.warning(
"%s.Event does not support enable_timing=True; load-back "
"duration metric will be skipped on this backend.",
device_module.__name__,
)
return False
def make_timing_event_pair():
timing_enabled = _timing_events_supported()
kwargs = {"enable_timing": True} if timing_enabled else {}
return device_module.Event(**kwargs), device_module.Event(**kwargs), timing_enabled
class LayerLoadingEvent:
def __init__(self, num_layers: int):
self._num_layers = num_layers
@@ -140,6 +161,8 @@ class HiCacheAck(NamedTuple):
start_event: device_module.Event
finish_event: device_module.Event
node_ids: List[int]
num_tokens: int = 0
timing_enabled: bool = False
class StorageOperation:
@@ -767,8 +790,11 @@ class HiCacheController:
producer_event = self.layer_done_counter.events[producer_id]
producer_event.start_event.record()
ack_start_event, ack_finish_event, timing_enabled = make_timing_event_pair()
with device_module.stream(self.load_stream):
producer_event.start_event.wait(self.load_stream)
ack_start_event.record()
for i in range(self.layer_num):
self.mem_pool_host.load_to_device_per_layer(
self.mem_pool_device,
@@ -786,6 +812,7 @@ class HiCacheController:
self.io_backend,
)
producer_event.complete(i)
ack_finish_event.record()
# NOTE: We must save the host indices and device indices here,
# this is because we need to guarantee that these tensors are
# still alive when the load stream is executing.
@@ -796,9 +823,11 @@ class HiCacheController:
self.ack_load_queue.append(
HiCacheAck(
start_event=producer_event.start_event,
finish_event=producer_event.finish_event,
start_event=ack_start_event,
finish_event=ack_finish_event,
node_ids=op.node_ids,
num_tokens=len(op.device_indices),
timing_enabled=timing_enabled,
)
)
return producer_id
@@ -385,9 +385,9 @@ class HiMambaRadixCache(MambaRadixCache):
if write_back:
# blocking till all write back complete
while len(self.ongoing_write_through) > 0:
for _, finish_event, ack_list in self.cache_controller.ack_write_queue:
finish_event.synchronize()
for ack_id in ack_list:
for ack in self.cache_controller.ack_write_queue:
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
backuped_node = self.ongoing_write_through.pop(ack_id)
self._record_store_event(
backuped_node, medium=StorageMedium.CPU
@@ -403,8 +403,8 @@ class HiMambaRadixCache(MambaRadixCache):
# independently (no cross-rank sync).
finish_count = 0
if len(self.ongoing_write_through) > 0:
for _, finish_event, ack_list in self.cache_controller.ack_write_queue:
if not finish_event.query():
for ack in self.cache_controller.ack_write_queue:
if not ack.finish_event.query():
break
finish_count += 1
@@ -418,9 +418,9 @@ class HiMambaRadixCache(MambaRadixCache):
finish_count = int(queue_size.item())
while finish_count > 0:
_, finish_event, ack_list = self.cache_controller.ack_write_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = self.cache_controller.ack_write_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
backuped_node = self.ongoing_write_through.pop(ack_id)
self._record_store_event(backuped_node, medium=StorageMedium.CPU)
self.dec_lock_ref(backuped_node)
@@ -432,8 +432,8 @@ class HiMambaRadixCache(MambaRadixCache):
# Every rank must enter the all_reduce below; ongoing_load_back can
# diverge across ranks.
finish_count = 0
for _, finish_event, ack_list in self.cache_controller.ack_load_queue:
if not finish_event.query():
for ack in self.cache_controller.ack_load_queue:
if not ack.finish_event.query():
break
finish_count += 1
@@ -447,11 +447,19 @@ class HiMambaRadixCache(MambaRadixCache):
finish_count = int(queue_size.item())
while finish_count > 0:
_, finish_event, ack_list = self.cache_controller.ack_load_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = self.cache_controller.ack_load_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
end_node = self.ongoing_load_back.pop(ack_id)
self.dec_lock_ref(end_node)
if self.metrics_collector is not None:
self.metrics_collector.increment_load_back_num_tokens(ack.num_tokens)
if ack.timing_enabled:
duration_ms = ack.start_event.elapsed_time(ack.finish_event)
self.metrics_collector.observe_load_back_duration(
duration_ms / 1000.0
)
finish_count -= 1
def ready_to_load_host_cache(self) -> int:
+21 -20
View File
@@ -938,9 +938,9 @@ class HiRadixCache(RadixCache):
if write_back:
# blocking till all write back complete
while len(self.ongoing_write_through) > 0:
for _, finish_event, ack_list in self.cache_controller.ack_write_queue:
finish_event.synchronize()
for ack_id in ack_list:
for ack in self.cache_controller.ack_write_queue:
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
self._finish_write_through_ack(ack_id, release_lock=False)
self.cache_controller.ack_write_queue.clear()
assert len(self.ongoing_write_through) == 0
@@ -952,8 +952,8 @@ class HiRadixCache(RadixCache):
# sequence and deadlocks under TP > 1. (Matches UnifiedRadixCache.)
finish_count = 0
if self.pp_rank == 0:
for _, finish_event, ack_list in self.cache_controller.ack_write_queue:
if not finish_event.query():
for ack in self.cache_controller.ack_write_queue:
if not ack.finish_event.query():
break
finish_count += 1
finish_count_tensor = torch.tensor(finish_count, dtype=torch.int, device="cpu")
@@ -963,17 +963,17 @@ class HiRadixCache(RadixCache):
if finish_count > 0:
logger.debug(f"Process {finish_count} write back operations")
while finish_count > 0:
_, finish_event, ack_list = self.cache_controller.ack_write_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = self.cache_controller.ack_write_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
self._finish_write_through_ack(ack_id, release_lock=True)
finish_count -= 1
def loading_check(self):
finish_count = 0
if self.pp_rank == 0:
for _, finish_event, ack_list in self.cache_controller.ack_load_queue:
if not finish_event.query():
for ack in self.cache_controller.ack_load_queue:
if not ack.finish_event.query():
break
finish_count += 1
finish_count_tensor = torch.tensor(finish_count, dtype=torch.int, device="cpu")
@@ -983,11 +983,19 @@ class HiRadixCache(RadixCache):
if finish_count > 0:
logger.debug(f"Process {finish_count} load operations")
while finish_count > 0:
_, finish_event, ack_list = self.cache_controller.ack_load_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = self.cache_controller.ack_load_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
end_node = self.ongoing_load_back.pop(ack_id)
self.dec_lock_ref(end_node)
if self.metrics_collector is not None:
self.metrics_collector.increment_load_back_num_tokens(ack.num_tokens)
if ack.timing_enabled:
duration_ms = ack.start_event.elapsed_time(ack.finish_event)
self.metrics_collector.observe_load_back_duration(
duration_ms / 1000.0
)
finish_count -= 1
def is_load_back_event_done(self, consumer_index: int) -> bool:
@@ -1243,7 +1251,6 @@ class HiRadixCache(RadixCache):
self, node: TreeNode, mem_quota: Optional[int] = None
) -> Optional[torch.Tensor]:
start_time = time.perf_counter()
last_hit_node = node
nodes_to_load = []
while node.evicted:
@@ -1311,12 +1318,6 @@ class HiRadixCache(RadixCache):
self.evictable_size_ += len(device_indices)
self.inc_lock_ref(last_hit_node)
if self.metrics_collector is not None:
self.metrics_collector.observe_load_back_duration(
time.perf_counter() - start_time
)
self.metrics_collector.increment_load_back_num_tokens(len(device_indices))
return device_indices
def init_load_back(
@@ -23,6 +23,9 @@ from sglang.srt.managers.cache_controller import (
from sglang.srt.managers.cache_controller import (
StorageOperation as BaseStorageOperation,
)
from sglang.srt.managers.cache_controller import (
make_timing_event_pair,
)
from sglang.srt.mem_cache.hicache_storage import (
HiCacheStorageExtraInfo,
PoolHitPolicy,
@@ -490,8 +493,12 @@ class HybridCacheController(BaseHiCacheController):
self.load_queue.clear()
producer_event = self.layer_done_counter.events[producer_id]
producer_event.start_event.record()
ack_start_event, ack_finish_event, timing_enabled = make_timing_event_pair()
with device_module.stream(self.load_stream):
producer_event.start_event.wait(self.load_stream)
ack_start_event.record()
for i in range(self.layer_num):
self.mem_pool_host.load_to_device_per_layer(
self.mem_pool_device,
@@ -514,6 +521,7 @@ class HybridCacheController(BaseHiCacheController):
self.io_backend,
)
producer_event.complete(i)
ack_finish_event.record()
self._record_transfer_indices_on_stream(
self.load_stream,
host_indices,
@@ -522,9 +530,11 @@ class HybridCacheController(BaseHiCacheController):
)
self.ack_load_queue.append(
HiCacheAck(
producer_event.start_event,
producer_event.finish_event,
ack_start_event,
ack_finish_event,
op.node_ids,
num_tokens=len(op.device_indices),
timing_enabled=timing_enabled,
)
)
return producer_id
@@ -1668,7 +1668,6 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
if self.cache_controller is None:
return False
start_time = time.perf_counter()
host_anchor_params = self.inc_host_lock_ref(best_match_node).to_dec_params()
# Build KV transfer
kv_xfer = self.components[BASE_COMPONENT_TYPE].build_hicache_transfers(
@@ -1753,12 +1752,6 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
host_anchor_params,
)
if self.metrics_collector is not None:
self.metrics_collector.observe_load_back_duration(
time.perf_counter() - start_time
)
self.metrics_collector.increment_load_back_num_tokens(len(device_indices))
return True
def _build_sidecar_transfers(
@@ -2350,9 +2343,9 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
if write_back:
# Blocking: wait for all pending write-backs
while self.ongoing_write_through:
for _, finish_event, ack_list in cc.ack_write_queue:
finish_event.synchronize()
for ack_id in ack_list:
for ack in cc.ack_write_queue:
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
if ack_id in self.ongoing_write_through:
self._finish_write_through_ack(ack_id)
cc.ack_write_queue.clear()
@@ -2363,8 +2356,8 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
# diverge across ranks (e.g. write_backup returning 0 on a subset).
finish_count = 0
if self.pp_rank == 0:
for _, finish_event, ack_list in cc.ack_write_queue:
if not finish_event.query():
for ack in cc.ack_write_queue:
if not ack.finish_event.query():
break
finish_count += 1
@@ -2374,9 +2367,9 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
# Process completed acks
while finish_count > 0:
_, finish_event, ack_list = cc.ack_write_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = cc.ack_write_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
self._finish_write_through_ack(ack_id)
finish_count -= 1
@@ -2389,8 +2382,8 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
# diverge across ranks.
finish_count = 0
if self.pp_rank == 0:
for _, finish_event, ack_list in cc.ack_load_queue:
if not finish_event.query():
for ack in cc.ack_load_queue:
if not ack.finish_event.query():
break
finish_count += 1
finish_count_tensor = torch.tensor(finish_count, dtype=torch.int, device="cpu")
@@ -2398,12 +2391,20 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
finish_count = finish_count_tensor.item()
while finish_count > 0:
_, finish_event, ack_list = cc.ack_load_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list:
ack = cc.ack_load_queue.pop(0)
ack.finish_event.synchronize()
for ack_id in ack.node_ids:
node, lock_params, host_lock_params = self.ongoing_load_back.pop(ack_id)
self.dec_lock_ref(node, lock_params)
self.dec_host_lock_ref(node, host_lock_params)
if self.metrics_collector is not None:
self.metrics_collector.increment_load_back_num_tokens(ack.num_tokens)
if ack.timing_enabled:
duration_ms = ack.start_event.elapsed_time(ack.finish_event)
self.metrics_collector.observe_load_back_duration(
duration_ms / 1000.0
)
finish_count -= 1
# ---- HiCache: Scheduler Entry Points ----
@@ -1934,7 +1934,7 @@ class RadixCacheMetricsCollector(_StatLoggerDIMixin):
self.load_back_duration_seconds = Histogram(
name="sglang:load_back_duration_seconds",
documentation="Time taken to load memory from CPU to GPU in seconds.",
documentation="Time taken to load KV cache from CPU back to GPU in seconds.",
labelnames=labels.keys(),
buckets=bucket_load_back_duration,
)