Prhub

#26227 [PD]: Support HiCache prefetching and pd-incremental transfer on decode side

原始 PR 作者 hzh0425 合并时间 2026-06-02 15:40 文件变更 7 提交数 18 评论 27 代码增减 +630 / -56

执行摘要

为 PD 分解 decode 端添加 HiCache 预取与增量传输

该变更源于 PD 分解路线图(#21703),目标是为 decode 节点提供历史缓存访问能力。在之前的实现中,decode 端只能依赖预填传输的 KV 缓存,而无法利用本机缓存和更外层的存储(L2/L3)。通过引入 HiCache,使 decode 节点能够在接收增量 KV 的同时,并行地从宿主内存和存储后端恢复匹配的前缀缓存,从而在多轮对话和智能体场景中显著改善首 token 延迟。

PR 核心设计合理,性能收益显著。但需在后续迭代中优化 all_reduce 批量化、支持并发加载,并加强错误处理。值得学习的是如何通过 DecodePrefixMatch 抽象缓存层次,以及 Mixin 方式组织代码。建议在合并前至少解决高优先级性能问题,但根据讨论已有折中,可以合并后优化。

讨论亮点
  1. 性能同步开销:gemini-code-assist 指出 query_storage_hit_lengthcheck_hicache_events 中的 all_reduce 在 per-request 循环中会导致严重性能开销,建议批量化或移到循环外。作者 hzh0425 回应 query_storage_hit_length 已有 TP 同步,prefetch_occupied 也是同步的。ShangmingCai 表示未来可关注大 batch 场景。
  2. 冗余前缀匹配:gemini-code-assist 指出在 _process_hicache_local_restore 中再次调用 match_prefix_for_req 是冗余的,可能导致不一致。作者未直接回应,在后续提交中保留了该逻辑。
  3. 串行化恢复:gemini 指出 is_load_back_event_done 使得一次只能恢复一个请求,可能增加排队延迟。作者未改动,可能出于防拥塞考虑。
  4. TP 分歧:ShangmingCai 多次询问如 query_storage_hit_length 提前 return 绕过 all_reduce 是否导致 TP 分歧,以及 backuped 判断是否一致。作者解释相关依赖已同步,确保安全。ShangmingCai 接受。
  5. 错误处理:ShangmingCai 要求对 HiCacheRestoreResult.FAILED 添加详细日志,并询问 elifif 的原因。作者添加了失败日志,并改回 elif

实现拆解

  1. 新增 decode_hicache_mixin.py:定义了 DecodePrefixMatch dataclass,包含前缀索引、L2/L3 命中长度、节点引用和预取注册标志。提供属性计算总前缀长度、是否需要本地恢复。还定义了 HiCacheRestoreResult 枚举和两个 Mixin 类:DecodeHiCachePreallocMixinDecodeHiCacheTransferMixin_build_decode_prefix_match 方法调用 match_prefix_for_req 后,额外通过 query_storage_hit_length 查询 L3 存储命中长度。
  2. 修改 decode.py:导入新模块,在 DecodeRequest 中添加 HiCache 状态字段(prefix_matchhicache_restored_kv_indices 等)。将 DecodePreallocQueue 继承 DecodeHiCachePreallocMixin,修改 _match_prefix_and_lock 返回 DecodePrefixMatch 而非元组,并在前缀匹配后启动 HiCache 预取。修改 pop_preallocatedpop_transferred 集成 HiCache 恢复状态机。
  3. 增强 hiradix_cache.py:新增 query_storage_hit_length 方法:对给定的宿主节点和新 token 构造 RadixKey,通过 _storage_hit_query 查询存储层,然后对 TP 分组执行 all_reduce 取最小值,以保持一致性。新增 is_load_back_event_done 方法:检查指定消费索引的加载事件是否完成,完成后调用 loading_check 清理。
  4. 配置支持:在 scheduler.py 中增加 enable_decode_hicache 属性,根据 disaggregation_decode_enable_radix_cacheenable_hierarchical_cache 的组合推导,用于启用/禁用 HiCache 相关分支。
  5. 测试与集成:新增 TestDisaggregationDecodeRadixHiCacheFileBackend 测试类,配置 HiCache 文件后端和 Mooncake 传输后端,编写 test_decode_hicache_file_backend_l3_reuses_decode_output_after_flush 测试,验证内存缓存刷新后 decode 节点能从 L3 存储恢复前缀。调整了单元测试 test_priority_scheduling_disaggregation.pytest_decode_radix_lock_ref.py 以适应新增的 total_prefix_len 参数。
文件 模块 状态 重要度
python/sglang/srt/disaggregation/decode_hicache_mixin.py 解码接口层 added 9.28
python/sglang/srt/disaggregation/decode.py 解码队列 modified 8.5
python/sglang/srt/mem_cache/hiradix_cache.py 分层缓存 modified 7.64
test/registered/disaggregation/test_disaggregation_decode_radix_cache.py 集成测试 modified 7.29
python/sglang/srt/managers/scheduler.py 调度配置 modified 5.28
test/registered/unit/managers/test_priority_scheduling_disaggregation.py 单元测试 modified 4.59
test/registered/unit/mem_cache/test_decode_radix_lock_ref.py 单元测试 modified 4.37

关键符号

DecodePrefixMatch l1_prefix_len decode_prefix_len needs_local_restore restore_token_count HiCacheRestoreResult DecodeHiCachePreallocMixin _build_decode_prefix_match _start_hicache_prefetch _clean_hicache_prefetch_resources _process_hicache_local_restore is_load_back_event_done query_storage_hit_length _match_prefix_and_lock enable_decode_hicache

关键源码片段

python/sglang/srt/disaggregation/decode_hicache_mixin.py dependency-wiring

新增文件,定义了 HiCache 解码侧的核心数据结构和 Mixin 接口,是整个变更的基石。

"""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 InitLoadBackParamsif TYPE_CHECKING:
    from sglang.srt.disaggregation.decode import DecodeRequest
    from sglang.srt.managers.schedule_batch import Reqlogger = logging.getLogger(__name__)
​
​
@dataclass
class DecodePrefixMatch:
    """封装一次前缀匹配的结果,包含设备级、主机级、存储级的命中信息。"""
    prefix_indices: torch.Tensor # 匹配到的设备前缀索引
    l2_host_hit_length: int # L2 宿主节点命中 token 数
    l3_storage_hit_length: int # L3 存储命中 token 数
    last_device_node: Any # 匹配到的设备端节点
    last_host_node: Any = None # 匹配到的宿主节点(仅在 L3 命中时有效)
    prefetch_registered: bool = False # 是否已发起存储预取
​
    @property
    def l1_prefix_len(self) -> int:
        return len(self.prefix_indices)
​
    @property
    def decode_prefix_len(self) -> int:
        # 总前缀长度 = L1 ( 设备 ) + L2 ( 主机 ) + L3 ( 存储 )
        return self.l1_prefix_len + self.l2_host_hit_length + self.l3_storage_hit_length
​
    @property
    def needs_local_restore(self) -> bool:
        # 当总前缀长度超过设备前缀时,需要从 L2/L3 加载到设备
        return self.decode_prefix_len > self.l1_prefix_len
​
    @property
    def restore_token_count(self) -> int:
        # 需要从 L2/L3 加载回设备的 token 数
        return self.decode_prefix_len - self.l1_prefix_len
​
​
class HiCacheRestoreResult(Enum):
    """本地恢复状态机的枚举值。"""
    PENDING = "pending"
    READY = "ready"
    FAILED = "failed"
​
​
class DecodeHiCachePreallocMixin:
    """为 DecodePreallocQueue 提供的 HiCache 钩子:发起预取、保留 token。"""
​
    def _build_decode_prefix_match(self, req: "Req", result: Any) -> DecodePrefixMatch:
        """将 match_prefix_for_req 的结果转换为 DecodePrefixMatch,并可选查询 L3 命中长度。"""
        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,
        )
python/sglang/srt/disaggregation/decode.py dependency-wiring

主解码流程文件,集成了 HiCache 状态和 Mixin,修改了请求生命周期中的关键步骤。

@dataclass
class DecodeRequest:
    req: Req
    kv_receiver: CommonKVReceiver
    waiting_for_input: bool = False
    metadata_buffer_index: int = -1
​
    # HiCache Status —— 记录该请求在解码端的分层缓存状态
    prefix_match: Optional[DecodePrefixMatch] = None # 从前缀匹配获得的结构
    hicache_restored_kv_indices: Optional[torch.Tensor] = None # 已恢复的 KV 索引
    hicache_restored_node: Any = None # 恢复后对应的节点
    hicache_load_consumer_index: int = -1 # 用于跟踪加载事件的消费索引
    hicache_restore_status: HiCacheRestoreResult = HiCacheRestoreResult.PENDING # 恢复状态机
​
    @property
    def seqlen(self) -> int:
        return self.req.seqlen
​
    @property
    def priority(self) -> Optional[int]:
        return self.req.priority
​
​
class DecodePreallocQueue(DecodeHiCachePreallocMixin):
    """继承 HiCache Mixin 后,_match_prefix_and_lock 将返回 DecodePrefixMatch 而非元组。"""
    def _match_prefix_and_lock(self, req: Req) -> DecodePrefixMatch:
        """执行前缀匹配,锁定节点以避免驱逐,并查询 L3 存储命中长度。"""
        result = match_prefix_for_req(
            req,
            self.tree_cache,
            disable=False,
            is_out_done=False,
            return_cached_tokens=False,
            skip_radix_cache=False,
            cow_mamba=self.tree_cache.supports_mamba(),
            include_req=True,
        )
        self.tree_cache.inc_lock_ref(result.last_device_node)
        # 调用 Mixin 方法构建带有 L3 信息的 DecodePrefixMatch
        return self._build_decode_prefix_match(req, result)

评论区精华

性能同步开销:all_reduce 在 per-request 循环中 性能

gemini-code-assist 指出 query_storage_hit_length 和 check_hicache_events 中的 all_reduce 在每个请求的循环中调用,将成为性能瓶颈。建议批量化或移到循环外。

结论:hzh0425 回应 query_storage_hit_length 已有 TP 同步;ShangmingCai 表示未来可关注大 batch 场景。 · 已解决

冗余前缀匹配:_process_hicache_local_restore 中重复调用 match_prefix_for_req 正确性

gemini-code-assist 指出该处前缀匹配冗余且可能因树状态变化导致不一致,应复用之前存储的 prefix_match。

结论:作者未直接回应,逻辑保持。 · 待处理

串行化恢复:L2->L1 restore 一次只能一个请求 性能

gemini-code-assist 指出 is_load_back_event_done 的使用导致串行化恢复,可能增加排队延迟。

结论:作者未改动;可能为防拥塞设计。 · 待处理

TP 分歧:query_storage_hit_length 提前返回绕过 all_reduce 等 正确性

ShangmingCai 多次提问关于 query_storage_hit_length 提前返回是否导致 TP 分歧,以及 backuped 判断是否一致。

结论:hzh0425 解释依赖已同步,安全;ShangmingCai 接受。 · 已解决

错误处理:FAILED 状态日志和 elif 变 if 测试

ShangmingCai 要求对 HiCacheRestoreResult.FAILED 添加详细日志,并询问 elif 变 if 的原因。

结论:hzh0425 添加了失败日志,并改回 elif。 · 已解决

风险与影响

  • 性能风险query_storage_hit_lengthcheck_hicache_events 中的 all_reduce 在 per-request 循环中调用,当 batch 增大时可能成为瓶颈。建议后续批量化。
  • 并发风险:L2->L1 恢复串行化(is_load_back_event_done 等待前一个完成),可能在高并发场景下增加排队延迟。需要确认底层是否支持并发加载。
  • 一致性风险_process_hicache_local_restore 中重复前缀匹配可能导致在不同时间点使用不同节点,产生不一致。尽管作者视为 fallback,但未详细说明保护措施。
  • 错误处理不足FAILED 状态仅记录日志,没有重试或回滚逻辑,可能使得异常请求静默丢失。
  • 用户影响:PD 分解集群用户将体验显著更低的 TTFT(尤其在多轮对话中),解码吞吐基本不变。需要配置 HiCache 存储后端(如文件后端)和传输后端(Mooncake/NIXL)。新增配置组合 --enable-hierarchical-cache--disaggregation-decode-enable-radix-cache
  • 系统影响:增加 decode 节点的内存和存储 I/O 负载(L2/L3),可能需要调整 hicache-ratio 等参数。增加分布式同步开销。
  • 团队影响:引入 Mixin 模式,后续可在 prefill 端复用。需持续关注 per-request all_reduce 性能优化。
核心路径变更 性能退化风险 分布式同步开销 并发控制风险 错误恢复不完整

关联 Issue

#21703 [Roadmap] Prefill-Decode Disaggregation Roadmap (2026 Q2)

完整报告

参与讨论