Prhub

#28185 [GDN][KDA][mem_cache] int8 checkpoint pool for the linear-attn prefix cache

原始 PR 作者 yuan-luo 合并时间 2026-06-18 11:41 文件变更 10 提交数 2 评论 26 代码增减 +1010 / -18

执行摘要

为线性注意力模型添加 int8 压缩 checkpoint 池,使缓存前缀容量翻倍。

线性注意力模型每序列维护一个紧凑的循环状态(state),SGLang 使用 MambaRadixCache 缓存这些状态以实现前缀复用。问题在于容量:活动的 bf16 MambaPool 大小固定,当不同的缓存前缀数量超过池大小时,复用率骤降,状态被驱逐并重算。对于多文档 RAG、多短会话等高区分前缀的工作负载,这是主要成本。本 PR 通过引入独立的 int8 checkpoint 池来解耦缓存与工作集,在相同显存下缓存约 2 倍的前缀,从而将复用崩溃的拐点向外推移 2 倍。

值得精读,特别是量化策略选择(int8 vs fp8)和策略无关的 donate 钩子设计。建议关注后续 PR #28813 中对测试文件和 CI 标签的清理。

讨论亮点
  • 设备放置与精度:gemini-code-assist 指出 store 方法可能因 slots 位于 CPU 导致设备不匹配,建议通过 torch.as_tensor(slots, device=self.device) 修正;量化应在 float32 中进行以避免 bf16 精度损失。作者均已采纳。
  • 文件组织与 API 设计:yizhang2077 建议将 Int8CheckpointStore 合并入 mamba_checkpoint_pool.py,并添加更详细的内存估计检查;将 memory_pool.py 中过长的初始化逻辑抽出为独立函数。作者照做。
  • 测试覆盖:yizhang2077 要求为混合模型(如 Qwen3.5)添加端到端测试;merrymercy 指出 test/srt 已弃用,新测试应放入 test/manual;端到端测试应标记为 extra 避免 CI 超时。作者回应已通过后续 PR #28813 处理测试位置与标签。
  • Tensor 整形健壮性:gemini-code-assist 建议使用 .view(-1) 代替 .unsqueeze(0) 以兼容 0D 和 1D tensor,已采纳。

实现拆解

  1. 新建 int8 checkpoint 池类:在 mamba_checkpoint_pool.py 中定义 Int8CheckpointStore(编码/解码核心)和 MambaCheckpointPool(管理池生命周期)。量化采用每 (head, k-channel) 对称 int8,scale 在 float32 中计算以避免精度损失。
  2. 与 radix 缓存集成:修改 mamba_radix_cache.pycache_finished_reqcache_unfinished_req,在插入 radix 树时通过 _commit_int8_checkpoint 将活动状态量化存储,在缓存命中时通过 copy_to_pool 去量化回 bf16。新增钩子兼容两种调度策略(no_buffer / extra_buffer)。
  3. CLI 参数与配置验证:在 server_args.py 添加 --enable-int8-mamba-checkpoint--int8-mamba-ckpt-size,并验证与 --enable-hierarchical-cache 及自定义 radix 缓存后端的冲突。
  4. 内存池初始化:在 memory_pool.py_init_mamba_pool 中根据参数创建 int8 checkpoint 池,并封装为 maybe_init_int8_mamba_checkpoint_pool 函数(移至 mamba_checkpoint_pool.py)。
  5. 不变量检查与统计:更新 invariant_checker.py 添加 _check_mamba_pool_with_int8,对活动池和 int8 池独立检查;更新 pool_stats_observer.py 支持双池统计。
  6. 测试配套:新增 test_int8_checkpoint_store.py 测试编解码精度、存储/加载往返、COW 辅助函数及内存占用;新增 test_int8_mamba_checkpoint_e2e.py 端到端测试(Qwen3-Next 模型,验证 KL 散度)。
  7. 基准测试:新增 bench_int8_checkpoint_reuse.py,通过测量多个不同前缀下的缓存命中率与吞吐量来对比 int8 与 bf16 路径。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/mamba_checkpoint_pool.py 缓存池 added 9.18
python/sglang/srt/mem_cache/mamba_radix_cache.py 前缀缓存 modified 8.41
python/sglang/srt/server_args.py 配置 modified 7.07
python/sglang/srt/managers/scheduler_components/invariant_checker.py 校验器 modified 7.21
python/sglang/srt/mem_cache/memory_pool.py 内存池 modified 6.45
benchmark/bench_linear_attention/bench_int8_checkpoint_reuse.py 基准测试 added 8.91
test/srt/mem_cache/test_int8_checkpoint_store.py 单元测试 added 7.51
test/registered/radix_cache/test_int8_mamba_checkpoint_e2e.py 端到端测试 added 6.68

关键符号

Int8CheckpointStore.quantize Int8CheckpointStore.dequantize Int8CheckpointStore.store Int8CheckpointStore.load Int8CheckpointStore.copy_to_pool Int8CheckpointStore.store_from_pool MambaCheckpointPool.__init__ MambaRadixCache._commit_int8_checkpoint MambaRadixCache._alloc_int8_ckpt_slot ServerArgs._handle_int8_mamba_checkpoint InvariantChecker._check_mamba_pool_with_int8

关键源码片段

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

核心实现文件,包含 Int8CheckpointStore(量化 / 去量化编解码)和 MambaCheckpointPool(池管理与 COW 辅助函数),所有 int8 压缩逻辑于此定义。

class Int8CheckpointStore:
    """int8 store for cached multi-layer linear-attention states.    Per slot: qdata int8 [L, H, d_v, d_k], scale [L, H, 1, d_k].
    Quantization occurs over d_v per (head, k-channel) – matching
    the per-k-channel decay diag(alpha) alignment.
    All math is performed in float32 to avoid precision loss when
    input is bf16/fp16.
    """
​
    QMAX = 127
​
    def __init__(self, *, num_layers, num_slots, num_heads, head_v_dim, head_k_dim, device, scale_dtype=torch.bfloat16):
        # Preallocate tensors on the target device
        self.qdata = torch.empty(num_layers, num_slots, num_heads, head_v_dim, head_k_dim, dtype=torch.int8, device=device)
        self.scale = torch.empty(num_layers, num_slots, num_heads, 1, head_k_dim, dtype=scale_dtype, device=device)
​
    @classmethod
    def quantize(cls, state: torch.Tensor):
        """symmetric per-channel quantize: state [..., H, d_v, d_k] -> (qint8, scale [..., H, 1, d_k])."""
        # Cast to float32 for safe accumulation
        state_fp32 = state.to(torch.float32)
        amax = state_fp32.abs().amax(dim=-2, keepdim=True).clamp(min=1e-8)
        scale = amax / cls.QMAX
        # Round and clamp to int8 range
        q = torch.round(state_fp32 / scale).clamp(-cls.QMAX - 1, cls.QMAX).to(torch.int8)
        return q, scale.to(state.dtype) # keep scale in original dtype storage
​
    @classmethod
    def dequantize(cls, q: torch.Tensor, scale: torch.Tensor, out_dtype: torch.dtype):
        """Dequantize: q * scale -> float32 -> out_dtype."""
        return (q.to(torch.float32) * scale.to(torch.float32)).to(out_dtype)
​
    def store(self, slots: torch.Tensor, state: torch.Tensor):
        """Quantize and write states for given slots."""
        slots = torch.as_tensor(slots, device=self.device)
        q, scale = self.quantize(state)
        self.qdata[:, slots] = q
        self.scale[:, slots] = scale
​
    def load(self, slots: torch.Tensor, out_dtype: torch.dtype):
        """Load and dequantize states from slots."""
        slots = torch.as_tensor(slots, device=self.device)
        return self.dequantize(self.qdata[:, slots], self.scale[:, slots], out_dtype)
​
    def store_from_pool(self, active_pool, active_slots, ckpt_slots):
        """Quantize active slotted states into checkpoint slots."""
        state = active_pool[:, active_slots].detach()
        self.store(ckpt_slots, state)
​
    def copy_to_pool(self, target_pool, ckpt_slots, target_slots):
        """Dequantize checkpoint into target pool slots (cache-hit COW)."""
        target_pool[:, target_slots] = self.load(ckpt_slots, target_pool.dtype)

注释:量化在 float32 中完成以保证精度;store_from_poolcopy_to_pool 是外部调用的主要 COW 入口。

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

radix 缓存集成点,修改 cache_finished_req 和 cache_unfinished_req 以支持 int8 路径,并新增 _commit_int8_checkpoint 等辅助函数。

def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
    # ... (existing logic up to mamba value selection)
    if self.enable_mamba_extra_buffer:
        mamba_ping_pong_track_buffer_to_keep = (
            self.req_to_token_pool.get_mamba_ping_pong_keep_idx(req))
        src_active = req.mamba_ping_pong_track_buffer[
            mamba_ping_pong_track_buffer_to_keep].unsqueeze(-1)
        # int8 path: quantize the active slot into the checkpoint pool
        if self.int8_ckpt_pool is not None:
            mamba_value = self._commit_int8_checkpoint(src_active)
            # After quantization, the ping-pong track buffer is no longer needed
            mamba_ping_pong_track_buffer_to_keep = None
        else:
            mamba_value = src_active.clone()
    else:
        # no_buffer path
        if self.int8_ckpt_pool is not None:
            mamba_value = self._commit_int8_checkpoint(
                req.mamba_pool_idx.unsqueeze(-1))
        else:
            mamba_value = req.mamba_pool_idx.unsqueeze(-1).clone()
        mamba_ping_pong_track_buffer_to_keep = None
​
    result = self.insert(InsertParams(
        key=RadixKey(token_ids[:page_aligned_len], req.extra_key),
        value=page_aligned_kv_indices,
        mamba_value=mamba_value,
        prev_prefix_len=req.cache_protected_len,
    ))
    mamba_exist = result.mamba_exist
    # If the state already existed in radix (dedup), free the redundant int8 slot
    if mamba_exist and self.int8_ckpt_pool is not None:
        self.int8_ckpt_pool.free(mamba_value)
​
    # With int8 checkpoints, the active mamba slot must always be returned
    free_mamba_cache = (
        True if (self.enable_mamba_extra_buffer or self.int8_ckpt_pool is not None)
        else mamba_exist
    )
    if free_mamba_cache:
        self.req_to_token_pool.free_mamba_cache(
            req, mamba_ping_pong_track_buffer_to_keep=mamba_ping_pong_track_buffer_to_keep)
    self.dec_lock_ref(req.last_node)

注释:关键改动在于两个 else 分支前的 int8 检查,以及在 radix 插入后对重复 int8 slot 的回收。同时强制活动 slot 必须释放。

评论区精华

设备放置与精度 正确性

gemini-code-assist 指出 store 方法中 slots 可能位于 CPU,建议使用 torch.as_tensor(slots, device=self.device) 确保设备一致;量化应全部在 float32 中进行以避免 bf16 中间精度损失。

结论:作者均已采纳。 · 已解决

文件组织与 API 设计 设计

yizhang2077 建议将 Int8CheckpointStore 合并入 mamba_checkpoint_pool.py 并增加内存估计检查;将 memory_pool.py 中过长的初始化逻辑抽出为独立函数。

结论:作者已将所有逻辑合并到 mamba_checkpoint_pool.py,并添加了 estimate_mem_usage_bytes 和 mem_usage_bytes 方法。 · 已解决

测试覆盖与 CI 标签 测试

yizhang2077 要求为混合模型(如 Qwen3.5)添加端到端测试;merrymercy 指出 test/srt 已弃用,新测试应放入 test/manual;端到端测试应标记为 extra 避免 CI 超时。

结论:作者通过后续 PR #28813 处理了测试文件位置与 extra 标签。 · 已解决

Tensor 整形健壮性 style

gemini-code-assist 建议在 cache_unfinished_req 中使用 req.mamba_pool_idx.view(-1) 而非 .unsqueeze(0),以同时兼容 0D 标量和 1D tensor。

结论:已采纳。 · 已解决

风险与影响

  • 量化精度风险:int8 量化引入单次舍入误差,但设计保证误差仅在存储和加载时各出现一次,不进入循环递推。测试显示相对误差小于 1%。
  • 双池不变量一致性:活动 bf16 池与 int8 缓存池的 slot 计数耦合复杂,新增的 _check_mamba_pool_with_int8 通过独立检查两池状态降低风险,但仍有遗漏边界情况(如并发场景)的可能。
  • 兼容性限制:int8 checkpoint 池与 --enable-hierarchical-cache(主机卸载)及自定义 radix 缓存后端不兼容,已在 server_args.py 中显式抛出 ValueError。
  • 默认关闭的维护负担:功能默认关闭,但引入了新代码路径和配置项,长期维护中需确保双路径测试覆盖不退化。
  • 用户:为线性注意力模型用户提供可选的显存优化,在相同 HBM 下缓存前缀数量翻倍,对多前缀场景(RAG、多会话)效果明显。默认不开启,不影响现有用户。
  • 系统:增加内存池初始化和量化/去量化计算开销,但仅发生在缓存写入/命中时,不堵塞 decode 主路径。基准测试显示吞吐影响可忽略。
  • 团队:代码结构清晰,int8 相关逻辑集中在 mamba_checkpoint_pool.py,对原有调度路径改动最小。未来支持 speculative 路径时可直接复用 donate 钩子。
量化精度风险(单次舍入,误差可控) 双池不变量一致性(独立校验但并发边界待观察) 兼容性限制(不与 hierarchical cache 和自定义 radix cache 共用) 默认关闭导致后续维护易疏漏

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论