Prhub

#39480 [HiCache] Optimize buffer-mode storage existence bookkeeping

原始 PR 作者 xiezhq-hermann 合并时间 2026-09-16 01:49 文件变更 1 提交数 1 评论 3 代码增减 +36 / -14

执行摘要

HiCache 存在缓存 LRU 扩至 512K 并优化查询热路径

PR body 明确指出:StorageExistenceCache 位于 buffer-mode 热路径,每次 prefetch 决策和 write-through 准入都要通过 contains_all / covers_all 走 page-hash 链;在长上下文负载下,128K 条目的 LRU 把 positive 挤出 belief set 的速度快于被复用的速度,导致缓存对已存储页重复写入。因此需要让查询每页开销更小、并让缓存记住更多页,从而减少冗余的 D2H 与存储写。

值得精读的 Python 热路径微优化范例:展示了方法绑定(hoisting)、EAFP 与 LBYL 的取舍、批量淘汰、以及容量-内存权衡的工程分析,适合对缓存设计和性能优化感兴趣的工程师学习。但应意识到它缺少 benchmark 和专项测试,落地到线上前建议补一次 buffer-mode 长上下文对比测试验证收益。

讨论亮点

本 PR 没有任何 review 评论,是一次作者自合并的改动(merged_by: xiezhq-hermann,带 bypass-fastfail 标签)。唯一可观察到的外部信号是 CI:PR Test (Extra) 曾失败一次(Run #34939396445),作者连续 3 次发出 /tag-and-rerun-ci 触发重跑,最终主 CI (Base) 通过。失败原因未在评论中说明,结合上下文推测为 flaky 而非本次改动引入的确定性失败。

实现拆解

变更入口为 python/sglang/srt/mem_cache/buffer_mode/storage_existence_cache.py,从容量与每页开销两个维度改造:

  1. 容量放大:将 HICACHE_EXISTENCE_CACHE_MAX_ENTRIES 从 128 * 1024 提升为 512 * 1024,注释同步更新为约 125 MiB 最坏内存、约 32M tokens(page size 64,跨所有 pool)覆盖量。容量是构造 StorageExistenceCache 的默认值,外部未暴露配置项,因此对调用方无接口影响。
  2. 查询路径重写:contains_all 弃用 all(self.contains(...) for ...) 生成器,改为单次绑定 move_to_end 的循环,用 move_to_end 抛出的 KeyError 作为 miss 信号(EAFP 风格),命中时每个 key 只做一次 dict 查找;covers_all 在 extra_cover 为空时直接退化为 contains_all,仅在非空时才逐页做额外成员判断,避免默认空 frozenset() 带来的多余检查。
  3. 写入与失效路径重写:add 将 move_to_end / popitem 绑定到局部变量,插入完成后按 len(entries) - self.max_entries 计算淘汰数量并连续弹出最老条目,替代旧版 while 循环中反复的 len 检查;invalidate_beyond 同样将 pop 绑定到循环外。两个方法最终保留的条目集合与旧实现等价。
  4. 导入调整与测试配套:Container / Iterable / Sequence 从 typing 改为 collections.abc 导入(运行时类型判定的正确来源)。未新增测试,依赖现有 test_buffer_mode_sidecar.py 与 test_unified_radix_cache_unittest.py 覆盖,PR 说明本地均通过。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/buffer_mode/storage_existence_cache.py 存在缓存 modified 6.61

关键符号

StorageExistenceCache.add StorageExistenceCache.contains_all StorageExistenceCache.covers_all StorageExistenceCache.invalidate_beyond

关键源码片段

python/sglang/srt/mem_cache/buffer_mode/storage_existence_cache.py core-logic

唯一变更文件,既是容量配置的来源(`HICACHE_EXISTENCE_CACHE_MAX_ENTRIES`),也是 buffer-mode 热路径上所有存在性判断的载体;本次改动重写了 `contains_all` / `covers_all` / `add` / `invalidate_beyond` 四个方法的控制流。

from collections import OrderedDict
from collections.abc import Container, Iterable, Sequence
​
# 512K 条目 ≈ 125 MiB 最坏情况,覆盖约 32M tokens(page size 64,跨所有 pool)。
# 相比原先 128K 条目,长上下文下 positive 更不容易在复用前被 LRU 挤出。
HICACHE_EXISTENCE_CACHE_MAX_ENTRIES = 512 * 1024
​
​
class StorageExistenceCache:
    """Buffer 模式下 host 侧“哪些页已写入 L3 存储”的 LRU 信念缓存。
​
    命中可跳过冗余 D2H 与存储写;误报最多导致一次冷重算,不影响正确性。
    所有变更都必须发生在 scheduler 线程的 lockstep 点,保证 TP 副本一致。
    """
​
    def __init__(self, max_entries: int = HICACHE_EXISTENCE_CACHE_MAX_ENTRIES):
        self.max_entries = max_entries
        self._entries: OrderedDict[tuple[str, str], None] = OrderedDict()
​
    def add(self, pool: str, hashes: Iterable[str]) -> None:
        entries = self._entries
        move_to_end = entries.move_to_end # 方法绑定到局部变量,省去循环内属性查找
        for h in hashes:
            key = (pool, h)
            entries[key] = None
            move_to_end(key) # 新 belief 标记为最新使用
        # 批量淘汰:先算超额数量再连续弹出,避免旧版 while 循环里反复 len 检查
        pop_oldest = entries.popitem
        for _ in range(len(entries) - self.max_entries):
            pop_oldest(last=False)
​
    def contains(self, pool: str, page_hash: str) -> bool:
        entries = self._entries
        key = (pool, page_hash)
        if key not in entries:
            return False
        entries.move_to_end(key) # 命中即 LRU-touch
        return True
​
    def contains_all(self, pool: str, hashes: Iterable[str]) -> bool:
        entries = self._entries
        move_to_end = entries.move_to_end
        for h in hashes:
            key = (pool, h)
            try:
                # EAFP 风格:move_to_end 内部只做一次 dict 查找,
                # 命中时比“先 in 检查再 move_to_end”少一次哈希查找
                move_to_end(key)
            except KeyError:
                return False # 链上任意一页缺失即整体不可用,提前短路
        return True
​
    def covers_all(
        self,
        pool: str,
        hashes: Iterable[str],
        extra_cover: Container[str] = frozenset(),
    ) -> bool:
        """每页相信已存储,或位于 extra_cover(如 D2H 已发起、必达 storage-ack 的内容)即返回 True。"""
        entries = self._entries
        move_to_end = entries.move_to_end
        if extra_cover:
            # 只在 extra_cover 非空时逐页判断,避免默认空集合时的多余成员检查
            for h in hashes:
                key = (pool, h)
                if key in entries:
                    move_to_end(key) # 相信已存储,同时维护 LRU 顺序
                elif h not in extra_cover:
                    return False
            return True
        # extra_cover 为空时退化为纯存在检查,复用优化后的 contains_all
        return self.contains_all(pool, hashes)
​
    def invalidate_beyond(
        self, pool: str, hashes: Sequence[str], keep_pages: int
    ) -> None:
        """来自 prefetch hit 查询的 ground-truth 修复:丢弃 keep_pages 之后的信念。"""
        pop = self._entries.pop # 方法绑定到局部变量,循环外只查一次属性
        for h in hashes[keep_pages:]:
            pop((pool, h), None) # 默认值 None,缺页时静默跳过

评论区精华

CI Extra 失败与重跑 other

PR Test (Extra) 一次失败(Run #34939396445),作者连续 3 次发出 `/tag-and-rerun-ci` 触发重跑;全程无 review 评论,改动由作者自合并(带 `bypass-fastfail` 标签)。

结论:主 CI (Base) 最终通过;Extra 失败未见根因说明,推测为已存在的 flaky。 · 已解决

风险与影响

  1. 缺少量化验证:PR 明确写 'Benchmarking and Profiling: Not run for this PR',性能收益是推断性的;EAFP 的 try/except KeyError 在命中率低的场景下反而可能因异常开销变慢,而容量放大后命中率是否显著提升没有数据支撑。
  2. 内存占用上升:512K 条目最坏约 125 MiB,且是调度器进程常驻内存;在小内存国产卡或 CPU 部署环境中可能引入额外压力。
  3. 无专项测试变更:extra_cover 非空分支、批量淘汰、KeyError 短路等新控制流没有对应单元测试,回归风险靠现有两个测试文件兜底。
  4. 批量淘汰的瞬时峰值:add 先全部插入再统一淘汰,当一次插入量大时,瞬时条目数可达 max_entries + len(hashes),略高于旧实现的逐个淘汰峰值。
  5. TP 确定性约束:文件注释强调所有变更必须发生在 scheduler 线程的 lockstep 点;本次改动未触碰该约束,但后续维护者若在类外新增调用点仍可能破坏确定性。

影响范围严格限定在 HiCache buffer-mode(Prefill-Disaggregation 中 KV 卸载到 L3 存储的场景):每次 prefetch 决策和 write-through 准入都会走 contains_all / covers_all,长上下文负载下可减少重复存储写与 D2H 传输。对非 HiCache 用户无任何影响;无接口、配置或数据契约变化。团队侧,这是 HiCache 子系统近期优化序列中的一环,与 Router 存储层级感知、KV 事件归因等改动属于同一演进方向。

缺少 benchmark 数据 无专项测试变更 LRU 容量 4 倍提升推高内存占用

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论