Prhub

#32388 Observability enhancement for HiCache

原始 PR 作者 xiezhq-hermann 合并时间 2026-08-06 05:13 文件变更 12 提交数 3 评论 1 代码增减 +420 / -49

执行摘要

HiCache 新增分层命中、备份回载与丢弃指标

PR body 明确说明动机:'This PR introduce several new metrics for HiCache observability including: separated cache hit counter from tiers, backuped token, load back token and dropped token (for write-back policy) counter, more accurate timing and memory footprint counter for live bandwidth computation.' 即现有 HiCache 只有整体 evict/load-back 计数,缺少按 device/host/storage 层的命中拆分,缺少 write-back 场景下 D->H 备份与 H->D 回载的真实流量和耗时,无法计算 L2 实际带宽,也无法观测内存压力下 KV 被丢弃的损失,因此需要这套更细粒度的指标。

值得精读。重点关注三类设计:

1) HiCacheAck 同时携带按 pool 的 token 数与总字节数,让带宽计算和分池监控只依赖 ack 快照;
2) make_timing_event_pair() 与设备端事件的配合,使 backup/load-back 耗时统计避开调度器同步噪声;
3) 新旧缓存路径共用 _log_write_ack_metrics 的接入方式。建议结合 PR #33580(统一树核心接口)一起阅读,理解 UnifiedRadixCache 对旧缓存路径的替代进程中观测性如何保持对齐。若团队在用 write-back 策略,可基于新指标建立丢弃率与 L2 带宽告警。

讨论亮点

该 PR 没有人工 review 评论(唯一的 issue 评论是 Gemini Code Assist 的停止服务提示),因此没有可引用的审阅争论。值得注意的设计取舍都写在代码注释里:一是 eviction_duration_seconds 桶范围扩大到 60 秒,注释说明 write-back 下 eviction 包含阻塞式 D->H 备份,可能持续数秒;二是 _num_tokens_by_pool 明确排除 sidecar 传输(sidecar transfers reusing another pool's indices are excluded),而 _transfer_num_bytes 则相反地包含它们,两个口径互补,避免重复计数或漏计。

实现拆解

实现按 5 个层次推进:

  1. 指标注册层metrics_collector.pyStorageMetricsCollector 新增 sglang:prefill_effective_tokens_totalsglang:hicache_backup_tokens_totalsglang:hicache_backup_bytes_totalsglang:hicache_backup_duration_secondssglang:load_back_bytes_totalsglang:hicache_dropped_tokens_total 共 6 个 Prometheus 序列,并预置 input/device_hit/host_hit/storage_hit 四档 mode 与各 pool 的 0 值序列;同时把 eviction 直方图默认桶上限从 1 秒扩展到 60 秒,因为 write-back 的阻塞式合并备份可能持续数秒。配套新增 increment_effective_prefill_tokensincrement_backup_num_tokensincrement_backup_num_bytesincrement_load_back_num_bytesobserve_backup_durationincrement_dropped_tokens 等方法。

  2. 传输量定义managers/cache_controller.pyHiCacheAck 增加 num_tokens_by_poolnum_bytes 字段;基础版 _transfer_num_bytes 只计算 kv + draft 池字节;hybrid_cache_controller.py 提供增强版 _num_tokens_by_pool(按 pool 统计且排除复用其他 pool 索引的 sidecar 传输)与 _transfer_num_bytes(计入 draft piggyback 与 sidecar 字节)。start_writingstart_loading 改用 make_timing_event_pair() 生成 ack 专用事件对,使耗时统计只覆盖真正的 D->H/H->D 拷贝。

  3. 消费端上报hiradix_cache.pyunified_radix_cache.pyhi_mamba_radix_cache.pywriting_check/loading_check 在 ack 完成时按 pool 累加 backup/load-back token 与字节,并观测耗时直方图;unified 侧新增 _record_dropped_tokens(用 drop 前后 tracker 差值计算各池丢弃量),旧 hiradix 侧在 _drop_subtree_no_host 中直接上报释放的 token。

  4. 分层命中统计schedule_batch.py 抽出纯函数 split_cached_prefix_by_tierschedule_policy.pyPrefillAdder_update_prefill_budget 中按 device/host/storage 拆分非 retracted 请求的命中 prefix 并累加到新增字段;metrics_reporter.pyPrefillStats 读取这些字段并在 report_prefill_stats 中调用 increment_effective_prefill_tokens

  5. 测试配套test_hicache_staged_write_back_dispatch.py 为 fake host pool 补齐 anchor_entry/entry_map/size_per_token,并在 setUp/tearDown 中清理 _timing_events_supported 缓存;test_hicache_load_back_timing.pytest_prefill_adder.py 补充新字段断言。

文件 模块 状态 重要度
python/sglang/srt/observability/metrics_collector.py 指标采集 modified 8.45
python/sglang/srt/mem_cache/hybrid_cache/hybrid_cache_controller.py 混合缓存 modified 7.39
python/sglang/srt/mem_cache/unified_radix_cache.py 统一缓存 modified 7.44
python/sglang/srt/managers/cache_controller.py 缓存控制 modified 6.69
python/sglang/srt/mem_cache/hiradix_cache.py 缓存核心 modified 6.68
python/sglang/srt/managers/schedule_batch.py 批量调度 modified 6.21
python/sglang/srt/managers/schedule_policy.py 调度策略 modified 5.99
python/sglang/srt/managers/scheduler_components/metrics_reporter.py 指标上报 modified 6.01
python/sglang/srt/mem_cache/hi_mamba_radix_cache.py Mamba 缓存 modified 5.04
test/registered/unit/mem_cache/test_hicache_staged_write_back_dispatch.py 单元测试 modified 5.98
test/registered/unit/mem_cache/test_hicache_load_back_timing.py 单元测试 modified 4.11
test/registered/unit/managers/test_prefill_adder.py 单元测试 modified 3.42

关键符号

increment_effective_prefill_tokens increment_backup_num_tokens increment_backup_num_bytes increment_load_back_num_tokens increment_load_back_num_bytes observe_backup_duration increment_dropped_tokens split_cached_prefix_by_tier _transfer_num_bytes _num_tokens_by_pool _log_write_ack_metrics _record_dropped_tokens writing_check loading_check _update_prefill_budget start_writing start_loading

关键源码片段

python/sglang/srt/observability/metrics_collector.py core-logic

全部新指标的注册地与上报方法所在,是本次观测性增强的核心数据面;新增 prefill 分层命中、backup/load-back 按池 token 与字节计数、耗时直方图及 dropped tokens 计数,并扩展 eviction 直方图桶。

        # ======== HiCache L2 (host DRAM) 备份 / 回载与丢弃指标 ========
        # D->H 备份在 --hicache-write-policy write_back 下是阻塞式合并操作,
        # 可能持续数秒,因此默认直方图桶比 load-back 更宽
        bucket_backup_duration = [
            0.001, 0.002, 0.005, 0.01, 0.02, 0.05,
            0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0,
        ]
​
        # D->H 备份的按池 token 计数;覆盖所有 write policy,
        # 与 L3 层的 sglang:backuped_tokens_total 相区分
        self.backup_num_tokens = Counter(
            name='sglang:hicache_backup_tokens_total',
            documentation='The number of tokens backed up from GPU to local '
            'host DRAM (L2), by host pool (kv, swa, mamba, ...).',
            labelnames=list(labels.keys()) + ['pool'],
        )
​
        # write-back 在主机内存压力下未备份即销毁的 token 计数,
        # 按 reason 与 pool 两个维度区分
        self.hicache_dropped_tokens = Counter(
            name='sglang:hicache_dropped_tokens_total',
            documentation='The number of device KV tokens destroyed without a '
            'host backup, by pool (kv, swa, ...) and reason.',
            labelnames=list(labels.keys()) + ['reason', 'pool'],
        )
​
    def increment_backup_num_tokens(self, num_tokens: int, pool: str) -> None:
        # 每次 ack 完成时按池累加,配合耗时直方图可算 D->H 带宽
        self.backup_num_tokens.labels(**self.labels, pool=pool).inc(num_tokens)
​
    def increment_dropped_tokens(self, num_tokens: int, reason: str, pool: str) -> None:
        self.hicache_dropped_tokens.labels(**self.labels, reason=reason, pool=pool).inc(
            num_tokens
        )
python/sglang/srt/mem_cache/hybrid_cache/hybrid_cache_controller.py entrypoint

为 merged transfer op 计算按 pool 的 token 数与总字节数的核心定义点;start_writing/start_loading 用 timing event pair 精确测时并把结果写入 ack,是带宽指标的数据来源。

    def _num_tokens_by_pool(self, op: CacheOperation) -> dict[str, int]:
        # 按 host pool 统计一次合并传输的 token 数(anchor + extra pools),
        # D->H write 与 H->D load 的 ack 共用;复用其他 pool 索引的
        # sidecar 传输不计入,避免重复计数
        counts = {self.mem_pool_host.anchor_entry.name.value: len(op.device_indices)}
        for transfer in op.pool_transfers or []:
            if transfer.indices_from_pool is not None or transfer.host_indices is None:
                continue
            name = transfer.name.value
            counts[name] = counts.get(name, 0) + len(transfer.host_indices)
        return counts
​
    def _transfer_num_bytes(self, op: CacheOperation) -> int:
        # 全 pool 合计字节数,包含 draft piggyback 与 sidecar 传输;
        # 与耗时直方图 sum 相除即可得 D->H / H->D 的实际带宽
        kv_tokens = len(op.device_indices)
        num_bytes = kv_tokens * self.mem_pool_host.anchor_entry.host_pool.size_per_token
        if self.has_draft:
            num_bytes += kv_tokens * self.mem_pool_host_draft.size_per_token
        # 统计 sidecar 可复用的源 pool 槽位数
        source_len = {self.mem_pool_host.anchor_entry.name: kv_tokens}
        for t in op.pool_transfers or []:
            if t.indices_from_pool is None and t.host_indices is not None:
                source_len[t.name] = len(t.host_indices)
        for t in op.pool_transfers or []:
            entry = self.mem_pool_host.entry_map.get(t.name)
            if entry is None:
                continue
            if t.indices_from_pool is not None:
                num_slots = source_len.get(t.indices_from_pool, 0)
            else:
                num_slots = len(t.host_indices) if t.host_indices is not None else 0
            num_bytes += num_slots * entry.host_pool.size_per_token
        return num_bytes
python/sglang/srt/mem_cache/unified_radix_cache.py core-logic

新缓存路径的指标接入点,包含 _record_dropped_tokens 与 _log_write_ack_metrics,以及 evict 指标口径从全部层改为 full 层的调整。

# 组件类型到 host pool 标签名的映射;与 hicache_backup_tokens_total
# 以及 host 占用 gauge 使用的 pool 名保持一致
_COMPONENT_POOL_LABEL = {
    ComponentType.FULL: PoolName.KV.value,
    ComponentType.SWA: PoolName.SWA.value,
    ComponentType.MAMBA: PoolName.MAMBA.value,
}def _record_dropped_tokens(
    self,
    tracker: dict[ComponentType, int],
    freed_before_drop: dict[ComponentType, int],
) -> None:
    """记录主机内存压力下未备份即被丢弃的按池 token 数。    调用方在 drop 前用 freed_before_drop = dict(tracker) 保存快照,
    drop 完成后 tracker 中各组件的新增释放量即为丢弃量。
    """
    if self.metrics_collector is None:
        return
    for ct, freed in tracker.items():
        dropped = freed - freed_before_drop[ct]
        if dropped > 0:
            self.metrics_collector.increment_dropped_tokens(
                num_tokens=dropped,
                reason='host_pressure',
                pool=_COMPONENT_POOL_LABEL[ct],
            )

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  • 共享结构扩展HiCacheAck 是 NamedTuple,新增字段有默认值,向后兼容;但所有构造点需要显式填充 num_tokens_by_pool/num_bytes,遗漏只会导致指标缺失而非崩溃,不易察觉。
  • 签名变更increment_load_back_num_tokens(num_tokens) 变为 (num_tokens, pool),三个调用点已同步修改;仓库外或旧分支的自定义调用会直接失败。
  • 统计口径调整unified_radix_cache.pyevictsum(tracker.values()) 改为 tracker[BASE_COMPONENT_TYPE]sglang:evicted_tokens_total 将不再包含 SWA/MAMBA 层,依赖旧口径的告警或面板可能出现跳变。
  • 指标膨胀:新增多个带 pool/reason/mode 标签的 Counter 与预置 0 序列,PROMETHEUS_MULTIPROC_DIR 下序列数随 host pool 数量线性增加。
  • 性能:ack 处理循环增加了字典遍历与 Prometheus 累加;直方图桶扩展对内存影响很小,风险低。
  • 运维/用户:新增 sglang:prefill_effective_tokens_total{device_hit,host_hit,storage_hit}hicache_backup_*load_back_*hicache_dropped_tokens_totalhicache_backup_duration_seconds 等指标,可直接绘制分层命中率与 L2 实时带宽,量化 write-back 在内存压力下的丢弃损失。
  • 系统:启用 HiCache 的实例会多上报若干时间序列;未启用 HiCache 的部署仅在 collector 初始化时多注册空序列,开销可忽略。
  • 团队:新老两条缓存路径(UnifiedRadixCache/HiRadixCache)使用同一套 _log_write_ack_metrics 口径,降低后续迁移与对比成本;调度侧 split_cached_prefix_by_tier 成为命中拆分的单一事实来源。
HiCacheAck 扩展影响所有调用点 evict 指标统计口径调整 新增标签序列导致指标膨胀 新旧缓存路径需同步维护

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论