Prhub

#33613 Remove revoke queue after hit-then-alloc refactoring

原始 PR 作者 xiezhq-hermann 合并时间 2026-08-06 18:10 文件变更 4 提交数 2 评论 0 代码增减 +29 / -52

执行摘要

删除 prefetch revoke 队列,撤销判断并入调度线程

PR body 明确说明这是 #19320 的 follow-up code cleaning:#19320 将主机内存分配推迟到 storage hit 之后(hit-then-alloc),使得调度线程在 drain 时已经拿到 operation.storage_hit_count,可以就地决定 revoke 或分配内存,原来用于跨线程通知撤销的 prefetch_revoke_queue 不再是必需。移除它可以消除一条队列、一次跨线程投递和一组参数传递,让异步控制流更简洁。

值得快速浏览,作为 #19320(hit-then-alloc)重构的收尾,展示了「当决策所需信息到达时再决策」如何简化跨线程通信。如果想深入 HiCache 异步管线,建议按 #19320 → 本 PR 的顺序阅读。本 PR 本身无讨论、无测试配套,不需要精读。

讨论亮点

本 PR 无任何 review 评论或 issue 讨论,唯一审核记录为 hzh0425 的 APPROVED(同意,无附加说明)。设计的核心取舍由 PR body 引用的 #19320 支撑:内存分配推迟后,撤销决策所需的信息在 drain 时已齐备,独立 revoke 队列不再有意义。

实现拆解

  1. 删除队列生命周期管理(python/sglang/srt/managers/cache_controller.py):_start_storage_threads() 不再创建 prefetch_revoke_queuereset() 不再对该队列做 clear(),避免 attach/reset 路径残留无用队列。
  2. 简化 prefetch 线程(python/sglang/srt/managers/cache_controller.py):prefetch_thread_func() 移除 storage_hit_count < self.prefetch_threshold 的分流分支,不再向 revoke 队列投递 request_id;所有结果统一截断 hash_value 并写入 storage_hit_count 后投入 prefetch_hit_queue,把决策权完整交给调度线程。
  3. 收敛 drain 决策(三个 radix cache 实现):python/sglang/srt/mem_cache/hiradix_cache.py、python/sglang/srt/mem_cache/unified_radix_cache.py、python/sglang/srt/mem_cache/hi_mamba_radix_cache.py 中删除 _drain_revoke() 嵌套函数、_drain_storage_control_queues_impl()n_revoke 形参及末尾的 _drain_revoke() 调用;在 _drain_and_alloc_storage_hit() 头部新增 operation.storage_hit_count < self.prefetch_threshold 分支,触发时调用 _revoke_pending_prefetch(req_id) 并 continue。
  4. 调整队列统计与 TP 同步:drain_storage_control_queues()check_hicache_events() 及 unified 的 _sync_hicache_ready_counts() 中移除 prefetch_revoke_queue.qsize(),all-reduce 向量从 4 元缩为 3 元;unified 中 extra_release_counts 的切片索引相应从 [4:] 改为 [3:]
  5. 配套与测试:没有新增或修改测试文件,依赖现有 HiCache 测试回归;由于是行为等价迁移,对模型输出无影响。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/hiradix_cache.py 缓存层 modified 6.12
python/sglang/srt/mem_cache/unified_radix_cache.py 缓存层 modified 6.12
python/sglang/srt/mem_cache/hi_mamba_radix_cache.py 缓存层 modified 5.69
python/sglang/srt/managers/cache_controller.py 缓存层 modified 5.37

关键符号

prefetch_thread_func _drain_storage_control_queues_impl _drain_and_alloc_storage_hit drain_storage_control_queues check_hicache_events _sync_hicache_ready_counts _drain_revoke

关键源码片段

python/sglang/srt/mem_cache/hiradix_cache.py core-logic

HiCache 非 Mamba 路径的调度线程 drain 逻辑所在文件:删除 _drain_revoke 与 n_revoke 参数,threshold 撤销判断迁入 _drain_and_alloc_storage_hit。

# 该函数是 _drain_storage_control_queues_impl 的嵌套函数,由调度线程调用;
# cc 即 self.cache_controller。drain 时 storage hit 数已经由 prefetch 线程
# 经过 TP all-reduce 后写入 operation.storage_hit_count。
def _drain_and_alloc_storage_hit():
    # hit-then-alloc 的核心:storage hit 数此刻已确定,只需预留恰好那么多的 host 内存,
    # 不再提前过度分配。alloc/evict 是 rank 局部的,但对 TP 各 rank 是确定性的:
    # host 池变更只发生在调度线程的 lockstep 点,按 TP-min 计数 drain,
    # 因此每个 rank 都会得到相同的成功 / fallback / revoke 结论。
    for operation in _drain_queue(cc.prefetch_hit_queue, n_storage_hit):
        req_id = operation.request_id
        info = self.ongoing_prefetch.get(req_id)
        if info is None:
            # 请求已被 abort/ 清理,直接跳过
            continue
        if operation.is_terminated():
            # 请求在 storage 查询期间被 abort,撤销其 pending prefetch
            self._revoke_pending_prefetch(req_id)
            continue
        if operation.storage_hit_count < self.prefetch_threshold:
            # 新增的阈值判断:收益不足不预取,就地撤销。
            # 原先该判断在 prefetch 线程中通过独立 revoke 队列异步通知,
            # 现在随 prefetch_revoke_queue 一起被删除,决策统一收拢到这里。
            self._revoke_pending_prefetch(req_id)
            logger.debug(
                f"Revoking prefetch for request {req_id} due to insufficient hits ({operation.storage_hit_count})."
            )
            continue
​
        alloc_len = operation.storage_hit_count
        host_indices = cc.mem_pool_host.alloc(alloc_len)
        if host_indices is None:
            self.evict_host(alloc_len)
            host_indices = cc.mem_pool_host.alloc(alloc_len)
        if host_indices is None:
            # 内存压力 fallback:退而求其次,分配页对齐的较短前缀
            available_size = cc.mem_pool_host.available_size()
            alloc_len = min(
                operation.storage_hit_count,
                available_size - (available_size % self.page_size),
            )
            if alloc_len >= self.prefetch_threshold:
                host_indices = cc.mem_pool_host.alloc(alloc_len)
        if host_indices is None:
            # 仍分配失败则撤销本次 prefetch,不阻塞请求
            self._revoke_pending_prefetch(req_id)
            logger.debug(
                f"Revoking prefetch for request {req_id} due to host memory allocation failure."
            )
            continue
​
        operation.storage_hit_count = alloc_len
        operation.hash_value = operation.hash_value[: alloc_len // self.page_size]
        operation.host_indices = host_indices
        cc.prefetch_buffer.put(operation)
python/sglang/srt/managers/cache_controller.py entrypoint

队列的「所有者」:prefetch_revoke_queue 在这里创建、清理并在 prefetch_thread_func 中投递;本 PR 移除其创建和 reset 清理,并让 prefetch 线程不再做阈值分流。

def prefetch_thread_func(self):
    """管理从 storage 后端到 host 内存的 prefetch 操作。"""
    self.prefetch_buffer = Queue()
    self.prefetch_io_aux_thread = threading.Thread(
        target=self.prefetch_io_aux_func, daemon=True
    )
    self.prefetch_io_aux_thread.start()
    while (not self.storage_stop_event.is_set()) or not self.prefetch_queue.empty():
        try:
            operation = self.prefetch_queue.get(block=True, timeout=1)
            if operation is None:
                continue
            if operation.is_terminated():
                hash_value, storage_hit_count = [], 0
            else:
                hash_value, storage_hit_count = self._storage_hit_query(operation)
            storage_hit_count_tensor = torch.tensor(storage_hit_count, dtype=torch.int)
            self._all_reduce_prefetch_groups(
                storage_hit_count_tensor, torch.distributed.ReduceOp.MIN
            )
            storage_hit_count = storage_hit_count_tensor.item()
​
            # 这里不再判断 storage_hit_count 是否低于 prefetch_threshold:
            # 阈值判断被下沉到调度线程的 drain 阶段(_drain_and_alloc_storage_hit),
            # 因此本线程只需把结果统一投递到 prefetch_hit_queue 即可。
            operation.hash_value = hash_value[: (storage_hit_count // self.page_size)]
            operation.storage_hit_count = storage_hit_count
            self.prefetch_hit_queue.put(operation)
        except Empty:
            continue

评论区精华

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

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

风险与影响

行为等价性:_drain_revoke() 原先在 _drain_and_alloc_storage_hit() 之前执行,现在同一 operation 的撤销要在它排到 prefetch_hit_queue 头部时触发;由于分配同样依赖 hit 队列顺序,二者对同一请求不存在竞争窗口,语义保持一致。遗漏引用风险:prefetch_revoke_queue 在创建与 reset 路径的引用均已删除,_stop_storage_threads() 从可见代码看未引用该队列;仍需确认 detach/re-attach 组合路径不残留对该属性名的访问。回归面:改动仅影响 enable_storage 开启时的 HiCache storage 路径,普通推理不触碰;缺少针对该队列删除的直接单测,依赖既有 HiCache 测试覆盖。性能影响:减少一个队列、一次跨线程投递与一个 TP 同步向量元素,调度线程仅增加一次阈值比较,整体开销可忽略甚至略优。

对用户:HiCache storage 后端(prefetch/backup 到远端存储)路径行为不变,模型输出不受影响;普通推理路径完全不触碰这些代码。对系统:调度线程 drain 逻辑成为 revoke 与分配的唯一决策点,异步控制流更简单,减少一个潜在的跨线程不一致来源(例如队列长度在 TP 之间不同步时需要特殊处理)。对团队:后续改动 HiCache 存储管线只需关注 prefetch_hit_queue 单队列语义;结合 #19320 的 hit-then-alloc 设计,新成员更容易理解本 PR 的清理动机。

核心异步控制流变更 缺少测试覆盖 多文件同步删除需防遗漏引用

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论