Prhub

#23106 [Perf] Make EAGLE bigram key an O(1) view on `RadixKey`

原始 PR 作者 hnyls2002 合并时间 2026-04-21 03:01 文件变更 5 提交数 12 评论 3 代码增减 +187 / -106

执行摘要

将 EAGLE bigram key 从 O(N) 元组物化改为 O(1) 视图,显著提升缓存插入和匹配性能。

根据PR body,EAGLE radix keys传统上存储为List[Tuple[int, int]],通过convert_to_bigram_key生成,每次插入和匹配都需分配N-1个Python元组,导致在1M上下文时占用约138ms(其中insert约84ms、match_prefix约54ms),成为cache_unfinished_req等热点路径的性能瓶颈。

值得精读,特别是RadixKey类的设计展示了如何通过视图模式避免物化开销,是性能优化和数据结构设计的典型案例。建议关注__getitem__切片逻辑和maybe_to_bigram_view的O(1)实现。

讨论亮点

review中未出现实质性争议,PR被ispobock直接批准,表明设计得到团队认可。提交历史显示多次合并和修复(如修复索引越界、迭代器分支丢失),侧面反映了开发过程中的细致调整。

实现拆解

  1. 重构RadixKey类(python/sglang/srt/mem_cache/radix_cache.py)

    • 添加__slots__优化内存,引入is_bigram标志区分视图模式。
    • 修改__len__:bigram模式下长度计算为max(0, len(token_ids) - 1)
    • 重写__iter__:bigram模式下迭代返回(t[i], t[i+1])元组,普通模式直接迭代token_ids。
    • 调整__getitem__:支持bigram切片时共享边界token,逻辑为token_ids[start:stop+1]
    • 新增maybe_to_bigram_view方法:O(1)切换is_bigram标志,替代旧的maybe_bigram_convert。
  2. 更新页面对齐函数(同文件)

    • page_align_keys新增is_bigram参数,bigram模式时保留额外边界token以确保对齐逻辑长度。
  3. 调整调用站点(python/sglang/srt/mem_cache/unified_radix_cache.py等)

    • 移除maybe_bigram_convert导入和调用,改用key.maybe_to_bigram_view
    • cache_finished_reqcache_unfinished_req中,直接传递原始token_ids给page_align_keys,通过is_bigram标志控制语义。
  4. 测试配套(test/registered/unit/mem_cache/test_swa_unittest.py)

    • 更新测试以验证bigram视图行为,例如检查list(last_node.key)返回元组列表而非直接访问token_ids。
  5. 哈希兼容性维护

    • compute_node_hash_values等函数直接处理原始token_ids的字节对,确保向后兼容的哈希流。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/radix_cache.py 缓存核心 modified 8.66
python/sglang/srt/mem_cache/unified_radix_cache.py 统一缓存 modified 5.67
python/sglang/srt/mem_cache/swa_radix_cache.py SWA 缓存 modified 5.64
python/sglang/srt/mem_cache/hiradix_cache.py 高基数缓存 modified 4.5
test/registered/unit/mem_cache/test_swa_unittest.py 单元测试 modified 3.49

关键符号

RadixKey.__init__ RadixKey.__len__ RadixKey.__iter__ RadixKey.__getitem__ RadixKey.maybe_to_bigram_view page_align_keys

关键源码片段

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

核心数据结构变更,重新设计 RadixKey 以支持 bigram 视图,是性能优化的基础。

class RadixKey:
    """is_bigram=True: token_ids 持有原始令牌(N个bigrams对应N+1个tokens);切片共享一个边界令牌。"""
​
    __slots__ = ("token_ids", "extra_key", "is_bigram") # 优化内存使用
​
    def __init__(self, token_ids: List[int], extra_key: Optional[str] = None, is_bigram: bool = False):
        self.token_ids = token_ids # 原始令牌序列,两种模式下都是整型列表
        self.extra_key = extra_key # 额外键(如 lora_id、cache_salt)
        self.is_bigram = is_bigram # bigram 视图标志:True 时长度 = max(0, len(token_ids) - 1)
​
    def __len__(self) -> int:
        if self.is_bigram:
            n = len(self.token_ids)
            return n - 1 if n > 0 else 0 # bigram 模式下逻辑长度为原始令牌数减一
        return len(self.token_ids)
​
    def __iter__(self) -> Iterator:
        if self.is_bigram:
            t = self.token_ids
            for i in range(len(t) - 1):
                yield (t[i], t[i + 1]) # 迭代返回 bigram 元组,避免物化整个列表
        else:
            yield from self.token_ids # 普通模式直接迭代令牌
​
    def __getitem__(self, idx: Union[int, slice]) -> "RadixKey":
        # 将整数索引规范化为切片,简化后续处理
        if isinstance(idx, int):
            if idx < 0:
                idx += len(self)
            if idx < 0 or idx >= len(self):
                raise IndexError(f"RadixKey索引越界: {idx}")
            idx = slice(idx, idx + 1)
        start, stop, step = idx.indices(len(self))
        if step != 1:
            raise ValueError("RadixKey切片步长必须为1")
​
        if self.is_bigram:
            # bigrams [start, stop) 对应原始令牌 [start, stop + 1);空切片返回空列表
            raw = self.token_ids[start : stop + 1] if stop > start else []
            return RadixKey(raw, self.extra_key, is_bigram=True) # 新视图共享边界令牌
        return RadixKey(self.token_ids[start:stop], self.extra_key)
​
    def maybe_to_bigram_view(self, is_eagle: bool, value: Optional[torch.Tensor] = None):
        """O(1)转换:翻转bigram标志而非物化元组列表。"""
        if is_eagle and not self.is_bigram:
            self.is_bigram = True # 直接切换视图模式
            if value is not None:
                value = value[: len(self)] # 截断张量以匹配 bigram 长度
        return self, value

评论区精华

批准与无争议 other

reviewer ispobock 直接批准 PR,未提出具体评论。

结论:设计被接受,无修改意见。 · 已解决

风险与影响

边界条件处理风险:RadixKey切片逻辑在bigram模式下需精确处理空切片和边界token共享,若实现有误可能导致数据不一致或崩溃。兼容性风险:虽然哈希计算保持相同,但外部依赖hiradix_cache.py仍使用convert_to_bigram_key作为存储键,需确保交互无影响。回归风险:核心路径变更可能影响EAGLE模式下的缓存匹配和插入正确性,依赖测试覆盖。

性能影响:在1M令牌基准测试中,convert步骤从~47ms降至~0ms(>100,000倍优化),匹配时间从113ms降至16ms(约7倍提升),整体cache_unfinished_req周期从70ms降至23ms(约3倍加速)。系统影响:降低长上下文EAGLE推理的延迟,提升KV缓存子系统吞吐量。团队影响:引入更高效的视图模式,减少Python对象分配,但增加了RadixKey的复杂度,需确保团队成员理解新语义。

核心路径变更 边界条件风险 测试覆盖调整

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论