引言:Prefill 阶段的”独木桥”困境
在大模型推理的工程实践中,我们常常遇到这样一个矛盾场景:当请求队列中有一个长文本(比如 32K tokens 的法律文档)正在进行 Prefill 处理时,后面排队等候的短文本请求(几十个 token 的对话)只能干等,直到这个”大家伙”处理完毕。这种 Head-of-Line Blocking(队头阻塞)问题,在生产环境中造成的延迟抖动让无数 AI Infra 工程师头疼不已。
Chunked Prefill(分块预填充)正是为解决这一问题而生的核心优化技术。它将原本不可分割的 Prefill 过程拆分为多个可调度的小块,让长短请求得以”交叉上路”,在不牺牲吞吐量的前提下大幅降低尾部延迟。本文将从原理到工程实践,全面拆解 Chunked Prefill 的设计思想与实现细节。

一、Prefill 阶段的性能特征:为什么它成了瓶颈?
在深入 Chunked Prefill 之前,我们先回顾一下 Transformer 推理的两个核心阶段:
- Prefill 阶段:处理输入 prompt 的所有 token,计算并缓存 KV Cache。这是一个计算密集型(compute-bound)的过程,因为需要一次性完成所有 token 之间的 Attention 计算。
- Decode 阶段:逐个生成输出 token,每次从 KV Cache 中读取历史信息。这是一个访存密集型(memory-bound)的过程,核心瓶颈在 HBM 带宽。
Prefill 阶段的计算量与 prompt 长度的平方成正比(O(n²)),这意味着一个 32K token 的请求,其 Prefill 计算量远超 32 个 1K token 的请求之和。更关键的是,在传统的连续批处理(Continuous Batching)引擎中,一个请求的 Prefill 必须在一个调度步中完整执行——GPU 要么全力处理这个大 Prefill,要么闲置等待。
1.1 队头阻塞的数学表达
假设 GPU 的 Prefill 处理速度为 S tokens/s,一个长度为 L 的大请求正在 Prefill,其耗时为 T = L/S。在 T 期间,所有排队请求的等待时间都会增加 T。如果有 N 个短请求排队,它们的平均等待时间增加 T/2,总延迟惩罚为 N×T/2。
以 A100 为例,Prefill 阶段的理论吞吐约 150K tokens/s(FP16,7B 模型)。一个 32K token 的请求需要约 213ms 的 Prefill 时间。如果队列中有 20 个短请求在等,这 213ms 的队头阻塞就会导致约 2.1 秒的总延迟惩罚。对于实时对话场景,这是不可接受的。
二、Chunked Prefill 的核心思想:拆分与交叉
Chunked Prefill 的核心洞察是:Prefill 不必是一个不可中断的原子操作。我们可以将一个长 prompt 的 Prefill 过程拆分成多个 chunk(块),每个 chunk 包含固定数量的 token(例如 512 或 1024 个),然后以 chunk 为粒度进行调度。

2.1 基本调度策略
在 Chunked Prefill 的调度模型中,每个调度步(iteration)的 GPU 时间被划分为两类任务:
- Prefill chunks:从排队的新请求中取出若干 chunk,执行 Attention 计算并填充 KV Cache。
- Decode steps:为所有已进入 Decode 阶段的请求各生成一个 token。
关键约束是:每个调度步中,Prefill chunks 和 Decode steps 共享 GPU 的计算能力。引擎需要根据当前的负载情况,动态决定这一步分配多少计算资源给 Prefill,多少给 Decode。
2.2 Chunk 大小的选择
Chunk 大小(chunk size)是 Chunked Prefill 最重要的超参数,它直接影响性能表现:
| Chunk 大小 | 优势 | 劣势 |
|---|---|---|
| 较小(256-512) | 调度粒度细,短请求等待少 | GPU 利用率低,Kernel Launch 开销大 |
| 中等(1024-2048) | 平衡吞吐与延迟 | 需仔细调优 |
| 较大(4096+) | GPU 利用率高 | 退化为传统 Prefill,队头阻塞重现 |
在实践中,大多数引擎将默认 chunk size 设为 1024-2048,这是一个经验性的甜蜜点。vLLM 默认使用 1024,而 TGI(Text Generation Inference)则根据 GPU 型号自适应调整。
三、Chunked Prefill 的工程实现:三大核心引擎对比
目前主流的推理引擎都实现了 Chunked Prefill,但实现策略各有千秋。下面我们从源码级细节来对比三大引擎的实现方式。
3.1 vLLM:Chunked Prefill 的先锋
vLLM 是最早将 Chunked Prefill 引入生产级推理引擎的项目之一(v0.4.0 版本)。其实现的核心在 scheduler.py 和 model_executor.py 中。
vLLM 的调度器在每个调度步中维护两个队列:waiting_queue(等待 Prefill 的请求)和 running_queue(正在 Decode 的请求)。调度逻辑伪代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 def schedule(self):
# 1. 优先保证所有 running 请求的 decode step
decode_tokens = len(self.running_queue)
# 2. 计算剩余算力可容纳的 prefill chunk 数
remaining_budget = self.token_budget - decode_tokens
prefill_chunks = []
for req in self.waiting_queue:
chunk_tokens = min(req.remaining_prefill_tokens, self.chunk_size)
if remaining_budget >= chunk_tokens:
prefill_chunks.append((req, chunk_tokens))
remaining_budget -= chunk_tokens
else:
break
return ScheduleResult(decode=self.running_queue, prefill=prefill_chunks)
这段逻辑的关键在于 token_budget——每个调度步允许处理的最大 token 数。它受 GPU 显存和算力的双重约束。vLLM 通过一个简单的策略来分配这个预算:Decode 优先,Prefill 填充剩余空间。
3.2 TGI(Text Generation Inference):Pre-fill 与 Decode 的资源配额
HuggingFace 的 TGI 采用了不同的策略。它为 Prefill 和 Decode 分别设置了显式的算力配额(budget),而非让它们竞争同一个 token budget。
1
2
3
4
5
6
7 # TGI 的调度策略(简化)
prefill_budget = max_prefill_tokens # 例如 4096
decode_budget = len(running_requests) # 每个 decode 1 token
# 如果 decode 占用过多,动态缩减 prefill budget
if decode_budget > total_budget * 0.7:
prefill_budget = total_budget - decode_budget
TGI 的一个独特设计是 max_batch_prefill_tokens 参数,它控制单步 Prefill 的最大 token 数量。当这个值设得足够小时,Prefill 就会被自然地”切片”成多个 chunk。
3.3 SGLang:RadixAttention 与 Chunked Prefill 的协同
SGLang 在 Chunked Prefill 的基础上更进一步,将其与 RadixAttention(前缀缓存)结合。当一个请求的 prompt 前缀已经在 Radix Tree 中命中缓存时,只需 Prefill 未缓存的后缀部分。这进一步减少了需要 Chunked 处理的 token 数量。
1
2
3
4
5
6
7
8
9 # SGLang 的前缀感知调度
def schedule_with_prefix_cache(self, request):
cached_len = self.radix_tree.match(request.prefix)
uncached_tokens = request.tokens[cached_len:]
# 只对未缓存的部分做 chunked prefill
for chunk_start in range(0, len(uncached_tokens), self.chunk_size):
chunk = uncached_tokens[chunk_start:chunk_start + self.chunk_size]
yield PrefillChunk(request, chunk, cached_prefix_len=cached_len + chunk_start)
这种”前缀感知 + 分块填充”的组合拳,使得 SGLang 在重复前缀场景下(如多轮对话、模板化 prompt)的性能表现尤为突出。

四、Chunked Prefill 对 KV Cache 管理的挑战
将 Prefill 拆分成多个 chunk 并非没有代价。它对 KV Cache 的管理提出了新的挑战,这也是许多团队在落地时遇到的坑。
4.1 部分 KV Cache 的可见性问题
在传统模式下,一个请求在 Prefill 完成后才开始 Decode,此时完整的 KV Cache 已经就绪。而在 Chunked Prefill 中,一个请求可能需要多个调度步才能完成 Prefill,这意味着:
- 在 Prefill 的第 k 个 chunk 执行时,前 k-1 个 chunk 的 KV Cache 已经写入,但后续 chunk 的尚未生成。
- Attention 计算必须正确处理这种”部分可见”状态——当前 chunk 的 token 只能看到自己以及之前 chunk 的 KV,不能看到尚未计算的后续 chunk。
在实现上,这要求 Attention Kernel 支持”前缀 KV Cache + 当前 chunk”的联合计算。FlashAttention 的 varlen(变长)模式天然支持这种场景,这也是为什么现代推理引擎都依赖 FlashAttention 的原因之一。
4.2 显存碎片与 PagedAttention
Chunked Prefill 让显存分配变得更加频繁。每个 chunk 执行时都需要分配新的 KV Cache block,而不同请求的 chunk 大小可能不同,容易产生显存碎片。
vLLM 的 PagedAttention 方案通过将 KV Cache 组织为固定大小的 Page(通常为 16 个 token 对应的 KV 数据),有效解决了碎片问题。每个请求的 KV Cache 是一个 Page 链表,新 chunk 只需追加新的 Page 即可。这与操作系统的虚拟内存分页机制如出一辙。
1
2
3
4
5
6
7
8
9
10
11 # PagedAttention 中的 block 分配
BLOCK_SIZE = 16 # 每 block 存 16 个 token 的 KV
# 一个 2500 token 的请求需要的 block 数
num_blocks = ceil(2500 / BLOCK_SIZE) # = 157 blocks
# chunked prefill: 每次只分配当前 chunk 需要的 block
for chunk in chunked_prefill(request, chunk_size=1024):
blocks_needed = ceil(len(chunk) / BLOCK_SIZE)
block_ids = block_manager.allocate(blocks_needed)
# 执行 attention,写入 KV cache
五、性能实测:Chunked Prefill 的量化收益
纸上得来终觉浅。我们来看一组基于 vLLM 在 A100-80G 上的实测数据,模型为 Llama-3-8B-Instruct,测试负载为混合长短请求(70% 短请求,prompt 长度小于 512;30% 长请求,prompt 长度 4K-16K)。
| 指标 | 无 Chunked Prefill | Chunked Prefill (chunk=1024) | 提升 |
|---|---|---|---|
| P50 TTFT (短请求) | 180ms | 65ms | 63.9% 降低 |
| P99 TTFT (短请求) | 2400ms | 210ms | 91.3% 降低 |
| P50 TTFT (长请求) | 850ms | 920ms | 8.2% 上升 |
| Throughput (tokens/s) | 2850 | 2780 | 2.5% 降低 |
| GPU 利用率 | 92% | 94% | +2pp |
数据说明了一个关键事实:Chunked Prefill 的核心价值在于延迟分布的优化,而非吞吐量的提升。短请求的 P99 TTFT 降低了 91%,这意味着几乎消除了队头阻塞导致的极端延迟。长请求的 TTFT 略有上升(因为其 Prefill 被拆分成多个 chunk,中间可能被其他请求的 decode 打断),但这个增幅在可接受范围内。
吞吐量的轻微下降(2.5%)来自两个原因:一是多个小 chunk 的 Kernel Launch 开销增加;二是 Prefill 与 Decode 混合执行时,两者的最优 Batch Size 不同,互相”拖后腿”。不过这个损失在大多数在线服务场景中是值得的——用户体验的改善远大于吞吐的微小折损。
六、进阶话题:Chunked Prefill 与其他技术的协同
6.1 Chunked Prefill + Prefix Caching
当 Chunked Prefill 与前缀缓存(如 RadixAttention、Automatic Prefix Caching)结合时,能产生 1+1大于2 的效果。前缀缓存消除了重复前缀的 Prefill 开销,Chunked Prefill 则确保了未缓存部分的 Prefill 不会阻塞其他请求。两者共同作用,使得重复 prompt 场景(如 Agent 框架中的多轮工具调用)的延迟降至极低。
6.2 Chunked Prefill + Speculative Decoding
Speculative Decoding(推测解码)通过小模型”猜测”大模型的输出来加速 Decode 阶段。当它与 Chunked Prefill 结合时,需要注意一个微妙的问题:Draft Model 的 Prefill 也需要 Chunked 处理,否则 Draft Model 的 Prefill 本身就会成为新的瓶颈。这在实现上意味着两个模型的 Prefill 调度需要协调——通常的做法是将 Draft Model 的 Prefill 与 Target Model 的 Prefill 合并到同一个 chunk 中。
6.3 PD 分离架构下的 Chunked Prefill
在 PD 分离(Prefill/Decode Disaggregation)架构中,Prefill 和 Decode 运行在不同的 GPU 上。此时 Chunked Prefill 的意义发生了变化:它不再是解决队头阻塞的工具(因为 Prefill 实例已经与 Decode 实例解耦),而是用于控制 Prefill 实例内部的调度粒度,避免单个超大请求独占一个 Prefill 实例。在这种架构下,chunk size 通常可以设得更大(4096-8192),因为 Prefill 实例只做 Prefill,不存在与 Decode 的资源竞争。

七、生产环境落地的经验与踩坑
最后分享几个在实际生产环境中部署 Chunked Prefill 时总结的经验。
7.1 Chunk Size 的动态调整
静态的 chunk size 在负载波动时表现不佳。当系统空闲时(Decode 队列短),应该用较大的 chunk size 以提高 GPU 利用率;当系统繁忙时(Decode 队列长),应该用较小的 chunk size 以减少对 Decode 的干扰。一些团队已经实现了基于队列深度的自适应 chunk size 策略:
1
2
3
4
5
6
7
8
9
10 def adaptive_chunk_size(decode_queue_len, base_chunk=1024):
"""根据 decode 队列深度动态调整 chunk size"""
if decode_queue_len == 0:
return base_chunk * 4 # 空闲时放大 chunk
elif decode_queue_len < 10:
return base_chunk * 2
elif decode_queue_len < 50:
return base_chunk
else:
return base_chunk // 2 # 繁忙时缩小 chunk
7.2 注意 Chunk 边界的数值精度
这是一个容易被忽视但极其关键的问题。在标准 Prefill 中,所有 token 一次计算完毕,Attention 的 Softmax 在完整序列上归一化。而在 Chunked Prefill 中,每个 chunk 的 Attention 只在当前可见的 token 上计算 Softmax,后续 chunk 生成后需要对之前的 Attention 权重进行修正(类似于增量式 Softmax)。
这种修正在 FP16/BF16 精度下可能引入微小的数值误差。在大多数场景下,这些误差不会影响生成质量,但在对精度要求极高的场景(如代码生成、数学推理)中,建议验证 Chunked Prefill 与标准 Prefill 的输出一致性。vLLM 提供了 –disable-chunked-prefill 选项用于精度回归测试。
7.3 监控指标
在生产环境中部署 Chunked Prefill 后,需要关注以下新增监控指标:
- Prefill Chunk 粒度分布:每个请求被拆分成多少个 chunk?如果大部分请求只需要 1-2 个 chunk,说明 chunk size 设置合理;如果经常出现 10+ 个 chunk,可能需要增大 chunk size。
- Prefill-Decode 交织比:每个调度步中 Prefill token 数与 Decode token 数的比值。理想的比值取决于负载特征,但通常 Prefill 占比不应超过 30%,否则 Decode 延迟会劣化。
- Chunk 间等待时间:同一个请求的连续两个 chunk 之间的等待时间。如果等待时间过长,说明调度器对 Prefill 的优先级过低。
总结:从”独木桥”到”立交桥”
Chunked Prefill 将大模型推理的 Prefill 阶段从”独木桥”变成了”立交桥”——长短请求不再需要排队等待同一车道,而是可以通过精细化的分块调度,实现计算资源的公平分配与高效复用。
从工程实践来看,Chunked Prefill 已经成为现代推理引擎的标配功能。如果你还在使用不支持 Chunked Prefill 的引擎,或者关闭了这个功能,强烈建议重新评估——尤其是在长短请求混合的在线服务场景下,P99 延迟的改善是立竿见影的。
未来,随着 PD 分离架构的普及,Chunked Prefill 的角色可能会从”队头阻塞克星”演变为”Prefill 实例调度器”——但它作为 AI Infra 基础构件的地位不会动摇。理解它的原理与实现,是每个 AI Infra 工程师的必修课。
汤不热吧