7.1 连续批处理与 Chunked Prefill:调度层的进一步优化 Continuous Batching 解决了「请求级」的批处理低效,但还有一类问题没解决:当一个超长 prompt 进来时,它的 Prefill 会「霸占」GPU 几百毫秒,期间所有 Decode 请求都被阻塞。Chunked Prefill 的任务就是把这块霸主切成小块,让所有请求都能公平推进。 7.1.1 回顾:两阶段的不对称性 回顾第 6 章 6.2 节的诊断:LLM 推理分两个阶段,特性截然不同: Prefill:计算密集(并行处理整个 prompt),GPU 计算单元满载。 Decode:内存密集(每步算 1 个 token),GPU 计算单元吃不饱,瓶颈在显存带宽。
Continuous Batching 解决了「请求级」的批处理低效,但还有一类问题没解决:当一个超长 prompt 进来时,它的 Prefill 会「霸占」GPU 几百毫秒,期间所有 Decode 请求都被阻塞。Chunked Prefill 的任务就是把这块霸主切成小块,让所有请求都能公平推进。
回顾第 6 章 6.2 节的诊断:LLM 推理分两个阶段,特性截然不同:
Continuous Batching(6.3 节)解决了「请求级」的批处理问题——多个请求可以动态进出 batch。但它隐含一个假设:每步要么是 Prefill 要么是 Decode,二者不混合。这导致一个新问题。
考虑这个场景:batch 中有 10 个请求正在 Decode,突然来了一个新请求,它的 prompt 长 8000 token。这个新请求要做一次 Prefill——一次性处理 8000 token,这是「计算密集」的大任务。
传统调度中,这次 Prefill 会作为一个独立步骤执行。但它要算 8000 token,耗时数百毫秒。在这段时间里:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 240" font-family="sans-serif" font-size="12"> <text x="440" y="22" text-anchor="middle" font-size="15" font-weight="bold">长 Prefill 阻塞 Decode 的问题</text> <!-- 时间轴 --> <text x="40" y="60" font-weight="bold">传统调度:</text> <rect x="40" y="70" width="800" height="30" rx="4" fill="#dbeafe" stroke="#2563eb"/> <text x="440" y="90" text-anchor="middle">10 个 Decode 请求推进</text> <rect x="40" y="105" width="800" height="50" rx="4" fill="#fecaca" stroke="#dc2626"/> <text x="440" y="135" text-anchor="middle">新请求 Prefill(8000 token,耗时 300ms)→ 10 个 Decode 全部阻塞</text> <rect x="40" y="160" width="800" height="30" rx="4" fill="#dbeafe" stroke="#2563eb"/> <text x="440" y="180" text-anchor="middle">10 个 Decode 请求继续</text> <text x="440" y="215" text-anchor="middle" font-size="11" fill="#dc2626" font-style="italic">用户感受:流式输出突然卡顿 300ms</text> </svg>
这种「长 Prefill 霸占 GPU、阻塞 Decode」的问题在长上下文场景(RAG、长文档分析)尤其严重。一个 32K 上下文的请求 Prefill 可能要 1-2 秒,期间所有 Decode 都被冻结。
Chunked Prefill(分块预填充) 的核心思想:把一次大 Prefill 切成多个小块,与 Decode 请求混合调度。
具体做法:
这样,每个 iteration 既有 Prefill 的计算(GPU 计算单元不闲着)又有 Decode 的推进(用户感受不到卡顿),实现了「GPU 始终满载且 Decode 流畅」的双赢。
Chunked Prefill 有效,根因是它「平衡了计算与内存两种负载」:
把二者混合到一个 iteration:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 280" font-family="sans-serif" font-size="12"> <text x="360" y="22" text-anchor="middle" font-size="15" font-weight="bold">Chunked Prefill:平衡计算与内存负载</text> <!-- 纯 Decode --> <rect x="40" y="60" width="640" height="50" rx="6" fill="#fce7f3" stroke="#db2777"/> <text x="60" y="85" font-weight="bold">纯 Decode:</text> <rect x="180" y="65" width="200" height="40" fill="#db2777" opacity="0.3"/> <text x="280" y="90" text-anchor="middle" font-size="11">算力闲置 30%</text> <rect x="380" y="65" width="280" height="40" fill="#db2777"/> <text x="520" y="90" text-anchor="middle" font-size="11" fill="#fff">显存带宽满载</text> <!-- 纯 Prefill --> <rect x="40" y="120" width="640" height="50" rx="6" fill="#dbeafe" stroke="#2563eb"/> <text x="60" y="145" font-weight="bold">纯 Prefill:</text> <rect x="180" y="125" width="480" height="40" fill="#2563eb"/> <text x="420" y="150" text-anchor="middle" font-size="11" fill="#fff">算力满载</text> <rect x="660" y="125" width="20" height="40" fill="#2563eb" opacity="0.3"/> <text x="640" y="150" text-anchor="middle" font-size="10">带宽</text> <!-- 混合 --> <rect x="40" y="190" width="640" height="60" rx="6" fill="#dcfce7" stroke="#16a34a"/> <text x="60" y="215" font-weight="bold">Chunked Prefill 混合:</text> <rect x="280" y="195" width="380" height="50" fill="#16a34a"/> <text x="470" y="225" text-anchor="middle" font-size="11" fill="#fff">算力 + 带宽 双满载</text> <text x="360" y="270" text-anchor="middle" font-size="11" fill="#16a34a" font-style="italic">GPU 利用率最大化</text> </svg>
💡 判读:Chunked Prefill 的精髓是「让两类负载互补」——Decode 缺算力、Prefill 缺带宽,二者混合恰好填满 GPU 的两个维度。这是一种「异构负载混部」的思想,与第 3 章训练-推理混部的思路异曲同工。
Chunked Prefill 的工程实现有几个关键点:
切多大切?这是一个权衡:
典型取值在 512-2048 token 之间,vLLM 默认 2048。具体值要根据硬件、模型、负载实测调优。
每个 iteration 算多少 Prefill 块、多少 Decode 步?这是调度器的核心决策。常见策略:
不同业务偏好不同策略——实时聊天偏好「优先 Decode」(保证已生成回复流畅),批量处理偏好「优先 Prefill」(尽快开始任务)。
Chunked Prefill 把 Prefill 切成多块,每块产生一部分 KV Cache。要保证这些 KV Cache 在逻辑上是「连续」的,最终拼成完整的 Prefill KV。这与第 6 章 PagedAttention 的 Block Table 天然契合——每个 chunk 的 KV 写入物理块,Block Table 自动维护逻辑连续。
除了「避免长 Prefill 阻塞 Decode」,Chunked Prefill 还有几个红利:
由于 GPU 利用率提升,相同显存下可以容纳更多并发请求,间接提升吞吐。
没有「长 Prefill 卡顿」,每个 Decode 步骤的延迟更稳定,用户感受更流畅。
新请求不会「饿死」(必须等当前 batch 全 Prefill 完),也不会「霸占」(长 Prefill 不会冻结他人),调度更公平。
| 维度 | 无 Chunked Prefill | 有 Chunked Prefill |
|---|---|---|
| 长 Prefill 影响 | 阻塞所有 Decode | 与 Decode 混合 |
| GPU 利用率 | 波动(Prefill 时满、Decode 时空闲) | 稳定高 |
| Decode 延迟 | 偶发卡顿 | 稳定 |
| 公平性 | 长 Prefill 霸占 | 公平推进 |
| 有效 batch | 受限 | 更大 |
Chunked Prefill 是 Continuous Batching 的「进一步细化」:
二者是叠加关系而非替代——现代推理引擎(vLLM 0.4+、SGLang、TGI)都同时支持。它们共同实现了「任意时刻 GPU 都满载、任意请求都不被卡顿」的理想调度。
实际部署中,Chunked Prefill 的调度策略要根据业务选:
| 业务场景 | 推荐策略 | 理由 |
|---|---|---|
| 实时聊天 | 优先 Decode,小 chunk | 保证 TTFT 与流式流畅 |
| RAG/长文档分析 | 中等 chunk,Prefill 优先 | 长 prompt 需快速 prefill |
| 批量处理 | 大 chunk,吞吐优先 | 离线任务不在乎单请求延迟 |
| 多租户混合 | 公平调度 | 平衡各类请求 |
vLLM 等引擎通常提供这些策略的开关,可以按业务调整。但要避免过度调优——大多数场景默认配置已经够好。
⚠️ 常见坑:Chunked Prefill 的一个坑是「chunk 太小导致调度开销过大」。如果 chunk 设为 64 token,每个 iteration 要调度很多次 Prefill,CPU 调度开销可能反而拖慢整体。建议 chunk 至少 512 token,并在实际负载上 benchmark。
DeepSpeed-FastGen 提出了一个 Chunked Prefill 的变体——Dynamic Splitfuse。它的核心思想是:
Dynamic Splitfuse 是 Chunked Prefill 思想的精细化,在 DeepSpeed-FastGen 上实测比静态 Chunked Prefill 进一步提升吞吐。这反映了 LLM 推理调度的「自适应化」趋势——调度器越来越智能,能根据实时负载动态调整策略。
调度层的优化除了 Chunked Prefill,还有几个值得了解的方向:
给不同请求设优先级(付费用户高、免费用户低),高优先级请求的 Prefill/Decode 优先处理。许多商业 LLM API 用这种策略区分用户等级。
根据请求的 prompt 长度预测生成长度,提前规划资源分配。例如长 prompt 通常生成也长,可以提前预留 KV Cache。
支持请求取消(用户中途停止生成)与超时,避免「僵尸请求」占着 KV Cache 不释放。这对长轮次 Agent 应用尤其重要。
这些调度优化共同构成了「LLM 推理调度器」这一活跃研究领域,与 6.3 节的 PagedAttention 一起,决定了推理引擎的核心性能。
下一节《7.2 模型量化:INT8/INT4、AWQ 与 GPTQ》将讲清模型压缩这一降本的关键武器。