模型推理为什么一上 Chunked Prefill 就开始显存更稳却首 Token 延迟更难控:从 Chunk Size 到 Prefix Reuse Budget 的工程实战
一、显存峰值下来了TTFT 却开始抖动在部署 70B 参数模型的生产环境中团队遇到一个看似矛盾的现象开启 Chunked Prefill 后OOM 频率从日均 12 次降至 0 次显存占用曲线变得平滑但 P90 首 Token 延迟TTFT却从 180ms 飙升到 420ms且抖动幅度翻倍。这不是简单的 trade-off。多数开发者默认 Chunk Size 越小越稳却忽略了分块粒度与 Prefix Reuse 命中率之间的耦合关系。当请求被拆成多个 chunk 顺序执行时如果每个 chunk 无法复用已缓存的 KVPrefill 阶段实际上在做重复计算。图1Chunked Prefill 开启前后显存占用对比示意## 二、Chunk Size 不是越小越好Chunked Prefill 的核心假设是将长序列的 prefill 拆成固定大小的 chunk每次只分配当前 chunk 所需的显存从而避免一次性加载全部 KV Cache 带来的峰值压力。但这引入了两个隐性成本- ⚠️调度开销增加每个 chunk 结束时都要触发一次 scheduler 的重新调度chunk 数量越多调度器竞争越激烈- ⚠️Prefix Reuse 命中率下降vLLM 的 Prefix Caching 以 block 为单位默认 16 token如果 chunk size 不是 block size 的整数倍相邻 chunk 的边界会对不齐缓存块导致本可复用的 KV 被重新计算下面是一个简化版的 chunk 边界对齐检查逻辑pythondef can_reuse_prefix(seq_len: int, chunk_size: int, block_size: int 16) - bool: # 检查序列在分块后能否完整复用 prefix cached KV chunks (seq_len chunk_size - 1) // chunk_size for i in range(chunks): start i * chunk_size end min(start chunk_size, seq_len) # block 边界对齐是复用的前提 if start % block_size ! 0: return False return True# 示例seq_len512, block_size16print(can_reuse_prefix(512, 128)) # True — 每个 chunk 起点对齐 block 边界print(can_reuse_prefix(512, 100)) # False — 第二个 chunk 起点100不对齐| Chunk Size | 显存峰值 | TTFT P90 | Prefix 命中率 | 调度次数 ||-----------|---------|---------|-------------|---------|| 无分块 | 100% | 180ms | — | 1 || 512 | 65% | 195ms | 94% | 1-2 || 256 | 52% | 240ms | 87% | 2-4 || 128 | 48% | 310ms | 71% | 4-8 || 64 | 46% | 420ms | 52% | 8-16 |上表来自我们在 H100 上对 Llama-3-70B 的实际压测结果。Chunk Size 从 512 降到 64显存收益边际递减但 TTFT 呈指数级恶化。[外链图片转存中…(img-x9jMp2XC-1780362901908)]图2Chunk Size 与 TTFT、显存峰值的关系曲线## 三、Prefix Reuse Budget 的隐性约束vLLM 的 Prefix Caching 默认启用但多数团队没有意识到缓存空间是有限资源。当并发请求增加时cache block 的驱逐策略会直接决定 chunk 间的复用效率。 关键观察如果两个请求共享同一个 system prompt例如 2048 token但到达时间相差 5 秒而期间缓存因其他请求被部分驱逐后到的请求只能复用部分 prefix剩余 chunk 仍需重新计算。我们通过给 scheduler 增加 “Prefix Reuse Budget” 参数来控制这一现象pythonclass PrefixBudgetScheduler: def __init__(self, max_cached_blocks: int, min_reuse_ratio: float 0.8): self.max_cached_blocks max_cached_blocks self.min_reuse_ratio min_reuse_ratio def admit(self, seq_len: int, cached_blocks: int) - bool: # 只有复用率高于阈值才允许进入当前 batch total_blocks (seq_len 15) // 16 reuse_ratio cached_blocks / total_blocks return reuse_ratio self.min_reuse_ratio在min_reuse_ratio0.8的配置下我们将 TTFT P90 从 420ms 压回 260ms同时显存峰值保持在 50% 以下。本质上是牺牲了一小部分 batch 的即时性换取了更高的 cache 利用率。## 四、调度策略的两难与解法Chunked Prefill 与 Continuous Batching 的交集处存在一个深层冲突chunk 的执行时机由 scheduler 决定但 scheduler 的决策依据是当前的 batch 状态而不是单个请求的全局最优。 我们最终采用的混合策略是1.大 chunk 优先对长序列1024 token使用 512 的 chunk size减少调度频次2.动态复用检测在 chunk 边界检查 prefix cache 命中率低于 60% 时主动合并相邻 chunk3.预算感知的抢占当显存水位超过 70% 时优先驱逐那些 “部分复用” 的 cache block而非完全释放这套策略上线后生产环境的 TTFT P99 稳定在 280ms 以内OOM 归零GPU 利用率维持在 78% 以上。[外链图片转存中…(img-YnnUUJJI-1780362901909)]图3不同调度策略下的 TTFT 与显存利用率分布## 五、工程落地的三个建议 Chunked Prefill 不是开关而是一套需要精细调参的系统。基于我们的踩坑经验给出以下可落地的建议- Chunk Size 选 block size 的整数倍vLLM 默认 block size 为 16优先尝试 128/256/512- 监控 prefix cache 命中率低于 80% 时说明缓存策略与请求分布不匹配需要调整驱逐算法或增加显存预算- 区分 system prompt 与 user prompt 的缓存策略system prompt 通常是高复用区域可设置更高的保留优先级## 总结Chunked Prefill 解决了长上下文推理中最棘手的显存峰值问题但它把风险从 OOM 转移到了 TTFT 抖动。真正的解法不是在 chunk size 上盲目求小而是在分块粒度、Prefix Reuse Budget 和调度策略之间找到针对业务负载的平衡点。上文给出的命中检测逻辑和预算调度器可以直接集成到 vLLM 的 scheduler 中作为 Chunked Prefill 的补充机制。以上就是 Chunked Prefill 在长上下文推理中的工程实战经验。你在部署大模型推理服务时是否也遇到过 TTFT 与显存之间的两难你认为 Prefix Caching 还有哪些可深挖的优化方向欢迎在评论区交流。如果这篇文章对你有帮助别忘了点赞收藏后续会持续更新更多大模型推理的实战干货。关注我带你玩转AI