☰
chunked-prefill 分块预填充:长上下文推理显存优化与调度实战
2026/10/10 7:19:42 网站建设 项目流程

1. 从一次显存告警说起:chunked-prefill到底在解决什么

第一次真正意识到 chunked-prefill 的价值,是在一个长上下文推理的压测场景里。当时服务端跑着一个 7B 级别的模型,单条请求的 prompt 长度拉到 8K 以上,并发一上来,显存直接顶到天花板,日志里开始刷 OOM 告警。奇怪的是,GPU 的算力利用率并不高,大部分时间都在等——等显存分配、等 KV Cache 腾地方、等一个超长 prefill 阶段跑完才能轮到下一个请求。这种"算力闲着、显存爆着"的割裂感,就是 chunked-prefill 要解决的核心矛盾。

先把概念说清楚。大模型推理分两个阶段:prefill(预填充)和decode(解码)。prefill 阶段把用户输入的整段 prompt 一次性喂进去,计算所有 token 的 KV Cache;decode 阶段则是一个 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, chunked=False) out_chunked = model.generate(prompt, chunked=True, chunk_size=chunk_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 延迟降了一半,对用户体验的提升远比吞吐数字更有价值。这个权衡,只有真正跑过线上服务的人才会懂。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询