Prhub

#52789 [Perf] Support internal prefill checkpoints for Mamba prefix caching, 9%~25% TTFT improvement

原始 PR 作者 yewentao256 合并时间 2026-08-23 01:32 文件变更 12 提交数 14 评论 37 代码增减 +562 / -48

执行摘要

Mamba 前缀缓存支持内部 checkpoint,TTFT 提升 9%~25%

PR body 明确指出:Mamba prefix caching 目前会在最后一个 block 边界拆分 prefill,导致同一请求被拆成两段模型 forward——以 8k 输入为例,原来是 7680 token 的 FlashKDA 计算保存 checkpoint 后,还要再走一次 320 token 的模型 forward 与 FlashKDA 调用。作者的目标是“avoid a second full-model pass through attention, MoE, routing, and TP collectives”,即通过内部 checkpoint 把主段计算合并到一次前向中,实测 8k 输入 TTFT 提升 9.4%(并发 1)/ 25.5%(并发 4)/ 20.7%(并发 16)。

值得精读。重点看三处设计:一是调度切分(_mamba_block_aligned_split)与缓存分配(_needs_internal_checkpoint)如何通过 MambaSpec 新字段建立跨层契约,避免硬编码模型名;二是 metadata 层用“block 边界 + FLASHKDA_CHUNK_SIZE 对齐”双条件做安全降级,保证非对齐场景不产生错误 checkpoint;三是 Triton 落盘 kernel 一次覆盖 conv 状态与 recurrent 状态的写法。建议与 #53196、#53212、#52971 以及 FlashKDA 上游 PR 连读,理解一个性能特性从内核到调度器的完整演进链。

讨论亮点

核心讨论与结论如下:

  • num_prefill_checkpoint_blocks 是否必要的质疑:审阅者 ZJY0516 在 vllm/v1/kv_cache_interface.py 评论 “looks like no one change this value. And I don't think this variable is needed”。作者回应:该字段在 kda.get_kv_cache_spec() 里由 replace() 写入,调度器依赖它避免第二次模型 forward,并承认 “Not very elegant now”,计划后续 PR 支持其他后端。

  • 大 batch 性能疑问:ZeldaHuang 询问 “Will these multiple flashkda calls cause performance degradation under large batch size(like bs64)?”。作者确认这是明确优化点,希望通过 FlashKDA 上游 PR(FlashKDA#7)落地后合并为单次原生调用,本 PR 先行合入。

  • 冗余判断清理:njhill 建议把 use_eagle 并入 mamba_has_prefill_checkpoint_blocks 的定义(附带 TODO 注释),避免在 _mamba_block_aligned_split 里重复判断,作者已修复。

  • spec-decoding 分工协调:作者原本计划在 follow-up 支持 spec decoding;ZeldaHuang 指出其 vllm#52971 “already supports spec-decoding”,无需重复劳动,作者随即取消该计划,双方约定 #52971 在本 PR 之后 rebase。

  • 已知限制:ZJY0516 在 issue 评论提醒 “current implementation may not support internal checkpoint when request has multimodal input”,属于明确的未覆盖场景。

实现拆解

按数据通路自底向上拆解为 6 步:

  1. 能力声明与跨层契约(vllm/v1/kv_cache_interface.py、vllm/models/kimi_k3/nvidia/kda.py、vllm/v1/core/sched/scheduler.py):MambaSpec 新增 num_prefill_checkpoint_blocks 字段,并在 max_memory_usage_bytes 与 is_uniform_with_collection 中同步扩维,保证容量预算和组内一致性判断覆盖新块类型;KDA 层重写 get_kv_cache_spec(),仅当 kda_prefill_backend == "flashkda" 时通过 dataclasses.replace 把该字段置 1;Scheduler 汇总所有 kv cache group 得到 mamba_has_prefill_checkpoint_blocks(要求非 eagle 模式),作为后续切分的总开关。

  2. 调度切分放宽(scheduler.py 的 _mamba_block_aligned_split):当内部 checkpoint 可用且本次 chunk 起点块对齐(start % block_size == 0)时,把 last_cache_position 置 0 并跳过强制对齐分支,允许一整段 8000 token 只触发一次模型 forward,FlashKDA 内部完成“7680 主段 + 320 尾段”的计算与状态导出。

  3. 缓存块分配(vllm/v1/core/single_type_kv_cache_manager.py):MambaManager 新增 _needs_internal_checkpoint() 判断与 _num_checkpoint_blocks 字典;get_num_blocks_to_allocate 在估算块数时叠加 checkpoint_block,allocate_new_blocks 用 num_skipped_blocks - checkpoint_block 调整 null 块填充区间,把 checkpoint 块夹在最后一个计算块与 running 块之间,不破坏“最后一块保存运行态”的不变量;pop_blocks_for_free 同步清理状态。

  4. metadata 契约(vllm/models/kimi_k3/nvidia/kda_metadata.py):新增 KDACheckpointMetadata(checkpoint_offsets, state_indices) 并挂到 KimiK3KDAMetadata.checkpoint;build() 在 mamba_cache_mode == "align" 时逐请求计算导出偏移:仅当 seq_len 未对齐、当前 query 真实跨过最后一个 block 边界、且偏移对 FLASHKDA_CHUNK_SIZE = 16 对齐时才导出,否则静默降级为不导出。

  5. 模型侧单次导出(kda.py、csrc/flashkda_registration.cpp):_flashkda_prefill 新增可选参数 checkpoint_state / checkpoint_offsets,STABLE_TORCH_LIBRARY 注册同步扩展;_prefill_conv 在 checkpoint 存在时以全量 query 调用一次 FlashKDA,随后用新增 Triton kernel _store_cache_checkpoints_kernel 把 conv 输入尾窗与 recurrent checkpoint 一次性写入目标缓存块,替代逐序列 Python 循环。

  6. 测试与构建配套:test_internal_checkpoint_split(chunk 切分)、test_partial_hit_then_internal_checkpoint_uses_distinct_mamba_blocks(部分命中与 checkpoint 块共存)、test_internal_checkpoint_requires_block_aligned_start 与 test_internal_checkpoint_metadata_skips_unaligned_offset(非对齐起点退化)、test_flashkda_correctness(带 checkpoint 的 FlashKDA 数值一致性)共 5 个测试文件覆盖;cmake/external_projects/flashkda.cmake 同步 FlashKDA 子模块引用。

文件 模块 状态 重要度
vllm/models/kimi_k3/nvidia/kda.py KDA 内核 modified 8.47
vllm/models/kimi_k3/nvidia/kda_metadata.py KDA 元数据 modified 7.7
vllm/v1/core/single_type_kv_cache_manager.py 缓存管理器 modified 7.65
tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py 前缀缓存 modified 6.91
vllm/v1/core/sched/scheduler.py 调度器 modified 6.74
tests/v1/core/test_mamba_align_chunk_split.py 切分测试 modified 6.69
tests/models/kimi_k3/test_kda_metadata.py 元数据测试 modified 6.65
tests/models/kimi_k3/test_kda.py KDA 测试 modified 6.22
vllm/v1/kv_cache_interface.py 缓存接口 modified 5.31
csrc/flashkda_registration.cpp 算子注册 modified 4.7
tests/v1/core/test_prefix_caching.py 缓存测试 modified 3.81
cmake/external_projects/flashkda.cmake 构建配置 modified 2.24

关键符号

_store_cache_checkpoints_kernel _flashkda_prefill get_kv_cache_spec KDACheckpointMetadata build _needs_internal_checkpoint get_num_blocks_to_allocate allocate_new_blocks _mamba_block_aligned_split max_memory_usage_bytes

关键源码片段

vllm/models/kimi_k3/nvidia/kda_metadata.py data-contract

新增 KDACheckpointMetadata 数据契约并挂到 KimiK3KDAMetadata.checkpoint;build() 逐请求计算 checkpoint 偏移与目标缓存块下标,用 block 边界 + FLASHKDA_CHUNK_SIZE 对齐双条件保证非对齐场景安全降级。

# 文件:vllm/models/kimi_k3/nvidia/kda_metadata.py(KimiK3KDAMetadataBuilder.build)
# 说明:为每个 prefill 请求计算“内部 checkpoint”的落点。目标是让 FlashKDA
# 在一次调用里导出倒数第二个 block 边界处的状态,供后续 chunk 复用。
​
checkpoint = None
if (
    num_prefills > 0
    and self.kv_cache_spec.num_prefill_checkpoint_blocks > 0
    and self.vllm_config.cache_config.mamba_cache_mode == "align"
):
    # 逐请求计算:offset 表示 checkpoint 相对本次 query 起点的偏移;
    # 只有当 seq_len 未 block 对齐、当前 query 真实跨过最后一个 block 边界、
    # 且偏移对 FLASHKDA_CHUNK_SIZE(16)对齐时才导出,否则退化为不导出
    assert m.seq_lens_cpu_upper_bound is not None
    request_rows = list(range(m.num_reqs))
    if spec_sequence_masks_cpu is not None:
        # spec decode 场景只取非草稿请求行
        request_rows = active_non_spec_mask_cpu.nonzero().flatten().tolist()
    all_query_lens = query_start_loc_cpu.diff().tolist()
    query_lens = [all_query_lens[row] for row in request_rows]
    seq_lens = m.seq_lens_cpu_upper_bound.tolist()
    block_size = self.kv_cache_spec.block_size
    checkpoint_splits = []
    checkpoint_cols = []
    for row, query_len in zip(request_rows, query_lens):
        seq_len = seq_lens[row]
        # 最后一个 block 边界(seq_len 向下取整)相对 query 起点的距离
        offset = seq_len // block_size * block_size - (seq_len - query_len)
        valid = (
            seq_len % block_size != 0
            and 0 < offset < query_len
            and offset % FLASHKDA_CHUNK_SIZE == 0
        )
        offset = offset if valid else 0
        first_len = offset or query_len
        # ( 前段长度 , 尾段长度 );尾段为 0 表示无需导出 checkpoint
        checkpoint_splits.append((first_len, query_len - first_len))
        # 目标缓存块列号:最后一个 block 的块下标,无效时置 -1
        checkpoint_cols.append(seq_len // block_size - 1 if valid else -1)
    if any(tail for _, tail in checkpoint_splits):
        checkpoint_offsets_tensor = async_tensor_h2d(
            [first if tail else 0 for first, tail in checkpoint_splits],
            dtype=torch.int32,
            device=query_start_loc.device,
        )
        request_rows_tensor = async_tensor_h2d(
            request_rows, dtype=torch.int64, device=query_start_loc.device
        )
        checkpoint_cols_tensor = async_tensor_h2d(
            checkpoint_cols, dtype=torch.int64, device=query_start_loc.device
        )
        # 从 block table 取目标块下标;无效行以 NULL_BLOCK_ID 填充
        checkpoint_state_indices = m.block_table_tensor[
            request_rows_tensor, checkpoint_cols_tensor
        ]
        checkpoint_state_indices = torch.where(
            checkpoint_cols_tensor >= 0,
            checkpoint_state_indices,
            NULL_BLOCK_ID,
        )
        checkpoint = KDACheckpointMetadata(
            checkpoint_offsets_tensor,
            checkpoint_state_indices,
        )
vllm/v1/core/single_type_kv_cache_manager.py core-logic

MambaManager 新增 _needs_internal_checkpoint 判断与 _num_checkpoint_blocks 记账,get_num_blocks_to_allocate / allocate_new_blocks 的块数估算与 null 块填充逻辑都围绕 checkpoint 块扩展,是调度分配正确性的关键。

# 文件:vllm/v1/core/single_type_kv_cache_manager.py(MambaManager)
# 说明:判断本次 allocation 是否需要多预留一个“内部 checkpoint”块,
# 并把该块插到 running 块之前,维持“最后一块保存运行态”的不变量。
​
def _needs_internal_checkpoint(
    self,
    request_id: str,
    num_tokens: int,
    num_computed_tokens: int,
) -> bool:
    assert isinstance(self.kv_cache_spec, MambaSpec)
    # checkpoint 块位于倒数第二个块(cdiv(num_tokens, block_size) - 2)
    checkpoint_idx = cdiv(num_tokens, self.block_size) - 2
    blocks = self.req_to_blocks[request_id]
    return (
        self.kv_cache_spec.num_prefill_checkpoint_blocks > 0
        # 仅当整段 prefill 未在 block 边界结束才需要导出
        and num_tokens % self.block_size != 0
        # 且本次排程从 block 边界开始(对齐起点约束,见 #53212)
        and num_computed_tokens % self.block_size == 0
        and checkpoint_idx >= 0
        # 该槽位尚未被真实块占用,避免覆盖已有状态
        and (checkpoint_idx >= len(blocks) or blocks[checkpoint_idx].is_null)
    )
​
# 在 get_num_blocks_to_allocate 中调用并暂存结论,供 allocate_new_blocks 复用:
# checkpoint_block = int(self._needs_internal_checkpoint(...))
# if not apply_admission_cap:
# self._num_checkpoint_blocks[request_id] = checkpoint_block
# allocate_new_blocks 里用 num_skipped_blocks - checkpoint_block 控制 null 块填充,
# 使 checkpoint 块保持真实块身份,running 块顺延一位。

评论区精华

num_prefill_checkpoint_blocks 字段是否多余 question

ZJY0516 在 kv_cache_interface.py 评论该字段无人修改、可能不需要;作者回应它在 get_kv_cache_spec() 里通过 replace() 写入,调度器需要它来避免第二次模型 forward。

结论:字段保留;作者承认当前写法不够优雅,后续 PR 会扩展到其他 KDA 后端。 · 已解决

大 batch 下多次 FlashKDA 调用是否拖慢性能 性能

ZeldaHuang 询问 bs64 等大 batch 下多次 flashkda 调用是否导致性能退化;作者确认这是明确优化点,计划等 FlashKDA 上游 PR 落地后改为原生单次调用。

结论:本 PR 先行合入,优化由 FlashKDA#7 与 vllm#52971 承接。 · 已解决

use_eagle 判断冗余 style

njhill 建议把 use_eagle 并入 mamba_has_prefill_checkpoint_blocks 定义,避免在 _mamba_block_aligned_split 中重复检查。

结论:作者已按建议修复。 · 已解决

spec-decoding 支持的分工协调 设计

作者原计划 follow-up 支持 spec decoding;ZeldaHuang 指出 vllm#52971 已支持,双方避免重复劳动并约定 rebase 顺序。

结论:spec decoding 由 #52971 承担,本 PR 不再实现。 · 已解决

多模态输入不支持内部 checkpoint other

ZJY0516 在 issue 评论提醒:当前实现可能不支持多模态输入下的内部 checkpoint。

结论:作为已知限制接受,未在本 PR 内修复。 · 待处理

风险与影响

风险集中在以下四点:

  1. 调度器与模型层的契约耦合:_mamba_block_aligned_split 的行为依赖 MambaSpec.num_prefill_checkpoint_blocks 与 mamba_has_prefill_checkpoint_blocks 的 all() 汇总逻辑,任一层的字段不一致都会导致优化静默关闭或块分配偏差;后续新增 Mamba 后端时必须同步维护该契约。

  2. 大 batch 性能未验证:ZeldaHuang 提出的 bs64 多段 FlashKDA 调用叠加 Triton 落盘 kernel 的开销尚无基准数据,作者将其挂起到 FlashKDA 上游与 #52971,存在长尾场景回归可能。

  3. 覆盖边界明确收窄:仅 FlashKDA 后端生效;Triton 后端、eagle/spec decode、多模态输入均不走内部 checkpoint 路径。调度器在 mamba_has_prefill_checkpoint_blocks 中显式排除 use_eagle,且对齐约束(起点块对齐、偏移为 16 的倍数)不满足时静默降级,行为差异需要测试守护。

  4. 内存预留敏感性:MambaSpec.max_memory_usage_bytes 已把 checkpoint 块计入容量预算,但若其他 kv cache manager 分支或未来新缓存模式未同步该字段,可能出现块预算低估导致的调度等待。

影响评估如下:

  • 用户/服务:开启 prefix caching 的 Kimi-K3 + FlashKDA 场景 TTFT 提升 9.4%(并发 1)/ 25.5%(并发 4)/ 20.7%(并发 16),提示吞吐提升 1.4%~4.0%,端到端时延下降 0.2%~3.8%,并发越高收益越大。

  • 系统行为:消除了 prefill 尾段的第二次全模型 forward(attention、MoE、routing、TP collectives),同一请求从两次调度、两次 kernel 启动收敛为一次;代价是每个 checkpoint 请求多预留一个 Mamba 缓存块。

  • 团队协作:本 PR 与 FlashKDA 上游(FlashKDA#7)和 vllm#52971 形成分层配合,Kimi-K3 KDA 性能线进入快速迭代阶段;同时为其他 Mamba 模型复刻“单次前向导出 checkpoint”提供了可参考的契约模板。

核心调度路径变更 仅 FlashKDA 后端生效 多模态输入未覆盖 大 batch 性能未验证 跨模块契约耦合

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论