执行摘要
- 一句话:为 PD 分解 decode 端添加 HiCache 预取与增量传输
- 推荐动作:PR 核心设计合理,性能收益显著。但需在后续迭代中优化 all_reduce 批量化、支持并发加载,并加强错误处理。值得学习的是如何通过
DecodePrefixMatch 抽象缓存层次,以及 Mixin 方式组织代码。建议在合并前至少解决高优先级性能问题,但根据讨论已有折中,可以合并后优化。
功能与动机
该变更源于 PD 分解路线图(#21703),目标是为 decode 节点提供历史缓存访问能力。在之前的实现中,decode 端只能依赖预填传输的 KV 缓存,而无法利用本机缓存和更外层的存储(L2/L3)。通过引入 HiCache,使 decode 节点能够在接收增量 KV 的同时,并行地从宿主内存和存储后端恢复匹配的前缀缓存,从而在多轮对话和智能体场景中显著改善首 token 延迟。
实现拆解
- 新增
decode_hicache_mixin.py:定义了 DecodePrefixMatch dataclass,包含前缀索引、L2/L3 命中长度、节点引用和预取注册标志。提供属性计算总前缀长度、是否需要本地恢复。还定义了 HiCacheRestoreResult 枚举和两个 Mixin 类:DecodeHiCachePreallocMixin 和 DecodeHiCacheTransferMixin。_build_decode_prefix_match 方法调用 match_prefix_for_req 后,额外通过 query_storage_hit_length 查询 L3 存储命中长度。
- 修改
decode.py:导入新模块,在 DecodeRequest 中添加 HiCache 状态字段(prefix_match、hicache_restored_kv_indices 等)。将 DecodePreallocQueue 继承 DecodeHiCachePreallocMixin,修改 _match_prefix_and_lock 返回 DecodePrefixMatch 而非元组,并在前缀匹配后启动 HiCache 预取。修改 pop_preallocated 和 pop_transferred 集成 HiCache 恢复状态机。
- 增强
hiradix_cache.py:新增 query_storage_hit_length 方法:对给定的宿主节点和新 token 构造 RadixKey,通过 _storage_hit_query 查询存储层,然后对 TP 分组执行 all_reduce 取最小值,以保持一致性。新增 is_load_back_event_done 方法:检查指定消费索引的加载事件是否完成,完成后调用 loading_check 清理。
- 配置支持:在
scheduler.py 中增加 enable_decode_hicache 属性,根据 disaggregation_decode_enable_radix_cache 和 enable_hierarchical_cache 的组合推导,用于启用/禁用 HiCache 相关分支。
- 测试与集成:新增
TestDisaggregationDecodeRadixHiCacheFileBackend 测试类,配置 HiCache 文件后端和 Mooncake 传输后端,编写 test_decode_hicache_file_backend_l3_reuses_decode_output_after_flush 测试,验证内存缓存刷新后 decode 节点能从 L3 存储恢复前缀。调整了单元测试 test_priority_scheduling_disaggregation.py 和 test_decode_radix_lock_ref.py 以适应新增的 total_prefix_len 参数。
关键文件:
python/sglang/srt/disaggregation/decode_hicache_mixin.py(模块 解码接口层;类别 source;类型 dependency-wiring;符号 DecodePrefixMatch, l1_prefix_len, decode_prefix_len, needs_local_restore): 新增文件,定义了 HiCache 解码侧的核心数据结构和 Mixin 接口,是整个变更的基石。
python/sglang/srt/disaggregation/decode.py(模块 解码队列;类别 source;类型 dependency-wiring;符号 DecodePreallocQueue, _match_prefix_and_lock, DecodeTransferQueue): 主解码流程文件,集成了 HiCache 状态和 Mixin,修改了请求生命周期中的关键步骤。
python/sglang/srt/mem_cache/hiradix_cache.py(模块 分层缓存;类别 source;类型 core-logic;符号 is_load_back_event_done, query_storage_hit_length): 在分层缓存实现中新增了 query_storage_hit_length 和 is_load_back_event_done 方法,用于 L3 存储查询和 L2 加载完成检查。
test/registered/disaggregation/test_disaggregation_decode_radix_cache.py(模块 集成测试;类别 test;类型 test-coverage;符号 TestDisaggregationDecodeRadixHiCacheFileBackend, setUpClass, tearDownClass, _post_ok): 新增了针对文件后端 HiCache 的集成测试类,验证 L3 复用功能。
python/sglang/srt/managers/scheduler.py(模块 调度配置;类别 source;类型 core-logic): 添加了 enable_decode_hicache 标志,用于控制 HiCache 分支的启用。
test/registered/unit/managers/test_priority_scheduling_disaggregation.py(模块 单元测试;类别 test;类型 test-coverage;符号 pre_alloc_mock): 调整单元测试以适应新增的 total_prefix_len 参数。
test/registered/unit/mem_cache/test_decode_radix_lock_ref.py(模块 单元测试;类别 test;类型 test-coverage): 调整现有测试以适应接口变化。
关键符号: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
新增文件,定义了 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 InitLoadBackParams
if TYPE_CHECKING:
from sglang.srt.disaggregation.decode import DecodeRequest
from sglang.srt.managers.schedule_batch import Req
logger = 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
主解码流程文件,集成了 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)
评论区精华
- 性能同步开销:gemini-code-assist 指出
query_storage_hit_length 和 check_hicache_events 中的 all_reduce 在 per-request 循环中会导致严重性能开销,建议批量化或移到循环外。作者 hzh0425 回应 query_storage_hit_length 已有 TP 同步,prefetch_occupied 也是同步的。ShangmingCai 表示未来可关注大 batch 场景。
- 冗余前缀匹配:gemini-code-assist 指出在
_process_hicache_local_restore 中再次调用 match_prefix_for_req 是冗余的,可能导致不一致。作者未直接回应,在后续提交中保留了该逻辑。
- 串行化恢复:gemini 指出
is_load_back_event_done 使得一次只能恢复一个请求,可能增加排队延迟。作者未改动,可能出于防拥塞考虑。
- TP 分歧:ShangmingCai 多次询问如
query_storage_hit_length 提前 return 绕过 all_reduce 是否导致 TP 分歧,以及 backuped 判断是否一致。作者解释相关依赖已同步,确保安全。ShangmingCai 接受。
- 错误处理:ShangmingCai 要求对
HiCacheRestoreResult.FAILED 添加详细日志,并询问 elif 变 if 的原因。作者添加了失败日志,并改回 elif。
- 性能同步开销:all_reduce 在 per-request 循环中 (performance): hzh0425 回应 query_storage_hit_length 已有 TP 同步;ShangmingCai 表示未来可关注大 batch 场景。
- 冗余前缀匹配:_process_hicache_local_restore 中重复调用 match_prefix_for_req (correctness): 作者未直接回应,逻辑保持。
- 串行化恢复:L2->L1 restore 一次只能一个请求 (performance): 作者未改动;可能为防拥塞设计。
- TP 分歧:query_storage_hit_length 提前返回绕过 all_reduce 等 (correctness): hzh0425 解释依赖已同步,安全;ShangmingCai 接受。
- 错误处理:FAILED 状态日志和 elif 变 if (testing): hzh0425 添加了失败日志,并改回 elif。
风险与影响
- 风险:
- 性能风险:
query_storage_hit_length 和 check_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 性能优化。
- 风险标记:核心路径变更, 性能退化风险, 分布式同步开销, 并发控制风险, 错误恢复不完整
关联脉络
参与讨论