
1. 从一次显存告警说起chunked-prefill到底在解决什么第一次真正意识到 chunked-prefill 的价值是在一个长上下文推理的压测场景里。当时服务端跑着一个 7B 级别的模型单条请求的 prompt 长度拉到 8K 以上并发一上来显存直接顶到天花板日志里开始刷 OOM 告警。奇怪的是GPU 的算力利用率并不高大部分时间都在等——等显存分配、等 KV Cache 腾地方、等一个超长 prefill 阶段跑完才能轮到下一个请求。这种算力闲着、显存爆着的割裂感就是 chunked-prefill 要解决的核心矛盾。先把概念说清楚。大模型推理分两个阶段prefill预填充和decode解码。prefill 阶段把用户输入的整段 prompt 一次性喂进去计算所有 token 的 KV Cachedecode 阶段则是一个 token 一个 token 地往外吐每步只算一个新 token。问题出在 prefill当 prompt 很长时这一步的计算量和显存占用都是一次性的它像一个巨大的原子操作要么全做完要么占着资源不动。而 chunked-prefill分块预填充的思路很朴素——把一条长 prompt 切成若干个小块分多次前向计算每次只处理一块从而把一次巨大的显存峰值摊平成一连串小峰值。这个思路听起来简单但它牵动的东西非常多KV Cache 怎么分块写入、块与块之间怎么保证注意力计算正确、decode 请求怎么和 prefill 块交错调度、显存碎片怎么控制。这篇内容就是把这些细节一层层拆开结合我在实际调优中踩过的坑讲清楚 chunked-prefill 的机制、实现要点和落地时的取舍。适合正在做推理服务优化、被长上下文和并发问题困扰的工程师也适合想搞明白 vLLM 这类框架调度逻辑的读者。提示本文讨论的是推理调度层面的通用机制不涉及任何具体网络环境或访问方式所有示例均为原理性说明。2. 为什么一次性 prefill会成为吞吐瓶颈2.1 prefill 与 decode 的资源画像完全不同要理解 chunked-prefill得先接受一个事实prefill 和 decode 是两种性格完全不同的负载。prefill 是计算密集型的矩阵乘法规模大GPU 算力能吃满但它的显存占用是脉冲式的——一瞬间要放下整段 prompt 的中间激活和 KV Cache。decode 则是访存密集型的每步只算一个 token算力用不满但对 KV Cache 的读取非常频繁显存带宽是瓶颈。这两种负载混在一起调度时最尴尬的情况是一个长 prompt 的 prefill 正在跑它占着大量显存导致后面的 decode 请求没法插入而 prefill 自己又因为要等所有块算完才能释放把整个 batch 的节奏拖慢。我实测过一个对比在固定显存预算下纯 decode 的吞吐能到每秒几百 token一旦混入长 prefill整体吞吐会掉到原来的三分之一甚至更低。这不是算力不够是调度粒度太粗。2.2 显存峰值的木桶效应显存峰值决定了你能开多大的 batch。假设单条 8K prompt 的 prefill 峰值占用是 10GB那你 24GB 的卡最多同时跑两条第三条就 OOM。但注意这 10GB 里真正必须同时存在的部分其实没那么多——中间激活是逐层产生的KV Cache 是逐 token 写入的。一次性 prefill 把这些本该错峰的需求强行叠在了一起制造了一个虚高的峰值。chunked-prefill 做的事情本质上是把时间维度上的错峰能力还给显存。切成 4 块每块峰值 2.5GB理论上同一张卡能塞下更多并发。当然实际不会这么理想因为块之间还有依赖但方向是对的。2.3 长上下文场景把矛盾放大上下文越长这个矛盾越尖锐。2K 的时候大家还能忍8K、32K 甚至 128K 的时候一次性 prefill 的显存需求会线性甚至超线性增长注意力是 O(n²) 的中间开销。我见过一个案例某团队把上下文从 4K 提到 16K什么都没改只是 prompt 变长服务直接不可用了。这时候 chunked-prefill 不是优化项而是能不能跑起来的生死线。3. 分块之后注意力计算为什么还能对得上3.1 KV Cache 的分块写入逻辑这是很多人第一个会问的问题把 prompt 切成块注意力不是要看到全部历史吗切了之后后面的块怎么看到前面的内容答案在 KV Cache 的写入方式上。注意力计算需要三样东西Query、Key、Value。对于第 t 个 token它的输出依赖于它自己和它之前所有 token 的 K、V。chunked-prefill 的做法是按顺序处理块每处理完一块就把这块产生的 K、V 追加写入 KV Cache。处理第 2 块时第 1 块的 K、V 已经在 Cache 里了第 2 块的 Query 可以正常和它们做注意力。所以只要保证块的处理顺序是从前到后因果注意力的正确性就不会被破坏。这里有个关键细节块内是并行的块间是串行的。第 1 块内部所有 token 可以并行算因为它们之间的因果掩码是块内的事但第 2 块必须等第 1 块的 KV 写完才能开始。这个串行依赖决定了 chunked-prefill 不能无限加速它只是把一次大串行变成了多次小串行。3.2 块大小怎么选一个被低估的调参点块大小chunk size是 chunked-prefill 最核心的参数但很多文档一笔带过。我踩过的坑是块太小调度开销和 kernel 启动开销占比上升吞吐反而下降块太大显存峰值压不下来等于没切。我的经验值是块大小在512 到 2048 token之间比较合理具体要看模型层数、hidden size 和卡的显存。一个粗略的估算方法先测出单块 prefill 的峰值显存让它不超过总显存的 1/4 到 1/3留出空间给 KV Cache 和 decode 请求。下面是一个简单的估算表帮助建立直觉块大小单块峰值显存相对调度开销适用场景256低高显存极度紧张短并发多512较低中通用推荐起点1024中低显存充裕追求吞吐2048高很低大显存卡长上下文注意这张表是相对值不是绝对值。实际一定要用你自己的模型和硬件压测别照搬别人的数字。3.3 位置编码与块边界的坑还有一个容易被忽略的点位置编码。如果用的是 RoPE 这类相对位置编码块边界处理相对自然因为位置信息是编码在 Q、K 的相对关系里的。但如果实现时对每个块重新计算位置偏移就可能出现位置错乱。我遇到过一次诡异的现象分块后模型输出开始重复、胡言乱语排查半天发现是块内位置索引没有累加全局偏移每个块都从 0 开始编号。这种 bug 不会报错只会让结果悄悄变差非常隐蔽。所以实现或使用 chunked-prefill 时务必确认每个 token 的全局位置索引是连续且正确的块只是计算的分组不是位置的重新开始。4. 调度器视角prefill 块和 decode 请求怎么共处4.1 连续批处理与分块的化学反应chunked-prefill 真正发挥威力是它和**连续批处理continuous batching**结合之后。连续批处理允许每个 decode step 动态地加入新请求、移除完成的请求。而 chunked-prefill 让一个长 prompt 的 prefill 可以分几次插入到这些 decode step 之间。具体来说调度器每个 step 可以做这样的决策这一轮我处理一个 prefill 块下一轮我处理一批 decode token再下一轮再处理下一个 prefill 块。这样长 prompt 不再独占资源decode 请求的延迟也不会被一个超长 prefill 卡死。这是吞吐和延迟双赢的关键。4.2 抢占与优先级谁先谁后调度就必然涉及优先级。我的实践里prefill 块和 decode 请求的优先级需要仔细权衡。如果 decode 优先级过高长 prompt 的 prefill 会被无限推迟用户等半天看不到第一个 token首 token 延迟 TTFT 爆炸。如果 prefill 优先级过高又回到独占资源的老问题。一个比较稳的策略是给 prefill 块设置一个最小推进量每个调度周期至少推进一个 prefill 块保证长请求有进展剩下的算力给 decode。这样 TTFT 有上界decode 的吞吐也不会被完全牺牲。这个策略在 vLLM 的调度逻辑里有类似体现但具体参数要自己调。4.3 显存碎片分块带来的新麻烦分块不是没有代价的。KV Cache 是动态增长的分块写入意味着显存分配是多次小块的这比一次性大块分配更容易产生碎片。如果用的是 PagedAttention 这类分页机制碎片问题会好很多因为页是固定大小的块写入就是往页里填。但如果用的是朴素的连续显存分配分块会显著加剧碎片跑久了可能出现总显存够但找不到连续空间的尴尬。我的建议是上 chunked-prefill 的同时尽量配合分页 KV Cache。这两者是天然搭档一个管时间维度的错峰一个管空间维度的碎片。5. 实测数据与调参心得别迷信理论值5.1 吞吐提升到底有多少理论讲再多不如看数。我在一个 7B 模型、单卡 24GB、混合长短请求的场景下做过对比以下为相对值非绝对性能承诺配置吞吐相对首 token 延迟显存峰值一次性 prefill1.0高且抖动大高chunked块5121.6明显降低中chunked块10241.8较低中高chunked块20481.7低高可以看到块大小不是越大越好也不是越小越好1024 左右是个甜点。块2048 时吞吐反而略降因为显存峰值上来了能并发的请求数变少。这个倒 U 型曲线是调参时最需要抓住的规律。5.2 那些文档不会告诉你的坑第一个坑块大小和 batch size 是耦合的。你调大块大小单请求显存涨能开的 batch 就小调小块大小单请求省显存但调度开销涨。这两个参数要一起调单独调一个往往得不到最优。第二个坑短 prompt 用 chunked 反而亏。如果 prompt 只有几百 token切块带来的调度开销大于收益。所以成熟的实现会做判断短 prompt 直接一次性 prefill长 prompt 才走分块。这个阈值也要根据实际负载定。第三个坑压测数据和线上数据会打架。压测时请求长度均匀线上往往是长尾分布——大部分短请求加少量超长请求。chunked-prefill 对长尾场景的收益比对均匀场景更明显因为长请求正是那个拖后腿的。所以压测时一定要模拟真实的长尾分布否则会低估或高估收益。5.3 监控什么指标才能判断调对了调 chunked-prefill光看吞吐不够要盯这几个指标首 token 延迟的 P99长请求用户最敏感、显存峰值与均值之比比值越小说明错峰越成功、prefill 块的平均等待时间反映调度是否公平、KV Cache 碎片率反映分页机制是否跟得上。我一般会把这四个指标放在一个面板上调参时盯着它们联动变化比单看一个数字靠谱得多。6. 从原理到落地一套可复现的验证流程6.1 先建立基线再谈优化任何优化都要有基线。我的流程是先用一次性 prefill 跑一组标准负载记录吞吐、TTFT、显存峰值然后开启 chunked-prefill块大小从 512 开始逐步往上调每次只改一个变量。这样你能清楚看到每个参数带来的边际收益而不是一锅乱炖。6.2 一个最小验证脚本的思路验证 chunked-prefill 正确性最直接的方法是对比分块前后的输出是否一致。同一段 prompt一次跑完整 prefill一次跑分块 prefill在贪心解码下输出应该完全相同浮点误差范围内。如果不同说明块边界或位置编码有问题。这个对比测试应该作为上线前的必过项。# 伪代码示意对比分块与不分块的输出一致性 def check_consistency(prompt, model, chunk_size): out_full model.generate(prompt, chunkedFalse) out_chunked model.generate(prompt, chunkedTrue, chunk_sizechunk_size) # 贪心解码下应逐 token 一致 assert out_full out_chunked, 分块导致输出不一致检查位置编码与KV写入6.3 上线后的灰度与回滚chunked-prefill 涉及调度逻辑改动面不小上线一定要灰度。我的做法是先切 10% 流量重点观察 TTFT 的 P99 和错误率稳定后再逐步放量。同时保留一键回滚到一次性 prefill 的能力因为一旦调度器出问题表现可能是部分请求卡死比直接报错更难排查。提示灰度期间建议单独记录分块请求的日志方便出问题时快速定位是块调度问题还是模型本身问题。7. 我对 chunked-prefill 的一点个人判断用了这么久我最大的体会是chunked-prefill 不是一个开了就变快的开关它是一个需要和你的负载特征、硬件配置、调度策略一起调的系统工程。块大小、优先级、分页机制、长短请求比例这些变量互相牵制没有放之四海皆准的最优解。另一个体会是它的收益在长上下文和高并发场景下最明显如果你的服务全是短 prompt、低并发那它带来的复杂度可能不划算。判断要不要上先问自己三个问题prompt 长度分布是不是长尾并发是不是上不去显存峰值是不是卡住了 batch三个里有两个是是那 chunked-prefill 大概率值得投入。最后分享一个小技巧调块大小时别只盯着吞吐把 TTFT 的 P99 一起看。很多时候吞吐只涨了 10%但 P99 延迟降了一半对用户体验的提升远比吞吐数字更有价值。这个权衡只有真正跑过线上服务的人才会懂。