7.1 连续批处理与 Chunked Prefill:调度层的进一步优化


文档摘要

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 计算单元吃不饱,瓶颈在显存带宽。

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(6.3 节)解决了「请求级」的批处理问题——多个请求可以动态进出 batch。但它隐含一个假设:每步要么是 Prefill 要么是 Decode,二者不混合。这导致一个新问题。

7.1.2 长 Prefill 阻塞 Decode 的问题

考虑这个场景:batch 中有 10 个请求正在 Decode,突然来了一个新请求,它的 prompt 长 8000 token。这个新请求要做一次 Prefill——一次性处理 8000 token,这是「计算密集」的大任务。

传统调度中,这次 Prefill 会作为一个独立步骤执行。但它要算 8000 token,耗时数百毫秒。在这段时间里:

  • 那 10 个 Decode 请求全部被阻塞,无法推进。
  • 用户感受:正在流式生成的回复「卡顿」了几百毫秒。
<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 都被冻结。

7.1.3 Chunked Prefill:把霸主切成小块

Chunked Prefill(分块预填充) 的核心思想:把一次大 Prefill 切成多个小块,与 Decode 请求混合调度

具体做法:

  • 把 8000 token 的 Prefill 切成 8 块,每块 1000 token。
  • 每个 iteration,GPU 同时处理:1 块 Prefill(1000 token)+ 所有正在 Decode 的请求。
  • 8 个 iteration 后,Prefill 完成,新请求开始 Decode。

这样,每个 iteration 既有 Prefill 的计算(GPU 计算单元不闲着)又有 Decode 的推进(用户感受不到卡顿),实现了「GPU 始终满载且 Decode 流畅」的双赢。

7.1.4 为什么 Chunked Prefill 有效

Chunked Prefill 有效,根因是它「平衡了计算与内存两种负载」:

  • Decode 阶段:每步算少量 token,计算单元吃不饱(瓶颈在显存带宽)。
  • Prefill 阶段:算大量 token,计算单元满载(瓶颈在算力)。

把二者混合到一个 iteration:

  • Decode 的「算力闲置」被 Prefill 的「算力需求」填满。
  • Prefill 的「显存带宽空闲」被 Decode 的「带宽需求」利用。
  • GPU 的计算单元与显存带宽同时被充分利用。
<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 章训练-推理混部的思路异曲同工。

7.1.5 Chunked Prefill 的工程实现

Chunked Prefill 的工程实现有几个关键点:

Chunk 大小选择

切多大切?这是一个权衡:

  • 块太小:调度开销大、kernel launch 多,反而降低效率。
  • 块太大:失去「与 Decode 混合」的意义,退化回长 Prefill。

典型取值在 512-2048 token 之间,vLLM 默认 2048。具体值要根据硬件、模型、负载实测调优。

Prefill 与 Decode 的混合比例

每个 iteration 算多少 Prefill 块、多少 Decode 步?这是调度器的核心决策。常见策略:

  • 优先 Decode:保证 Decode 的 TTFT/TPOT 不被影响,Prefill 在剩余预算内推进。
  • 优先 Prefill:尽快完成 Prefill,让新请求早开始流式输出。
  • 均衡:按 token 数比例分配算力。

不同业务偏好不同策略——实时聊天偏好「优先 Decode」(保证已生成回复流畅),批量处理偏好「优先 Prefill」(尽快开始任务)。

KV Cache 的连续性

Chunked Prefill 把 Prefill 切成多块,每块产生一部分 KV Cache。要保证这些 KV Cache 在逻辑上是「连续」的,最终拼成完整的 Prefill KV。这与第 6 章 PagedAttention 的 Block Table 天然契合——每个 chunk 的 KV 写入物理块,Block Table 自动维护逻辑连续。

7.1.6 Chunked Prefill 的额外红利

除了「避免长 Prefill 阻塞 Decode」,Chunked Prefill 还有几个红利:

红利一:更大的有效 batch

由于 GPU 利用率提升,相同显存下可以容纳更多并发请求,间接提升吞吐。

红利二:更稳定的延迟

没有「长 Prefill 卡顿」,每个 Decode 步骤的延迟更稳定,用户感受更流畅。

红利三:更公平的调度

新请求不会「饿死」(必须等当前 batch 全 Prefill 完),也不会「霸占」(长 Prefill 不会冻结他人),调度更公平。

维度 无 Chunked Prefill 有 Chunked Prefill
长 Prefill 影响 阻塞所有 Decode 与 Decode 混合
GPU 利用率 波动(Prefill 时满、Decode 时空闲) 稳定高
Decode 延迟 偶发卡顿 稳定
公平性 长 Prefill 霸占 公平推进
有效 batch 受限 更大

7.1.7 Chunked Prefill 与 Continuous Batching 的关系

Chunked Prefill 是 Continuous Batching 的「进一步细化」:

  • Continuous Batching(6.3 节):请求级别动态进出 batch。
  • Chunked Prefill:Prefill 内部也切块,与 Decode 混合。

二者是叠加关系而非替代——现代推理引擎(vLLM 0.4+、SGLang、TGI)都同时支持。它们共同实现了「任意时刻 GPU 都满载、任意请求都不被卡顿」的理想调度。

7.1.8 工程实践:调度器策略选择

实际部署中,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。

7.1.9 动态 SplitFuse:DeepSpeed 的变体

DeepSpeed-FastGen 提出了一个 Chunked Prefill 的变体——Dynamic Splitfuse。它的核心思想是:

  • 不仅把 Prefill 切块,还动态调整每步的 chunk 大小。
  • 根据 batch 内 Decode 请求的数量与长度,自适应分配 Prefill 算力。
  • Decode 多时少做 Prefill,Decode 少时多做 Prefill。

Dynamic Splitfuse 是 Chunked Prefill 思想的精细化,在 DeepSpeed-FastGen 上实测比静态 Chunked Prefill 进一步提升吞吐。这反映了 LLM 推理调度的「自适应化」趋势——调度器越来越智能,能根据实时负载动态调整策略。

7.1.10 Chunked Prefill 之外的调度优化

调度层的优化除了 Chunked Prefill,还有几个值得了解的方向:

优先级调度

给不同请求设优先级(付费用户高、免费用户低),高优先级请求的 Prefill/Decode 优先处理。许多商业 LLM API 用这种策略区分用户等级。

预测式调度

根据请求的 prompt 长度预测生成长度,提前规划资源分配。例如长 prompt 通常生成也长,可以提前预留 KV Cache。

取消与超时

支持请求取消(用户中途停止生成)与超时,避免「僵尸请求」占着 KV Cache 不释放。这对长轮次 Agent 应用尤其重要。

这些调度优化共同构成了「LLM 推理调度器」这一活跃研究领域,与 6.3 节的 PagedAttention 一起,决定了推理引擎的核心性能。

本节小结

  • Chunked Prefill 解决「长 Prefill 阻塞 Decode」的问题,把一次大 Prefill 切成多块与 Decode 混合调度。
  • 有效的根因是「异构负载互补」:Decode 缺算力、Prefill 缺带宽,混合后 GPU 计算单元与显存带宽双满载。
  • 工程实现:chunk 大小(512-2048)、混合比例策略(优先 Decode/Prefill/均衡)、KV Cache 用 Block Table 维护连续性。
  • 额外红利:更大有效 batch、更稳定延迟、更公平调度。
  • Chunked Prefill 是 Continuous Batching 的进一步细化,二者叠加实现「任意时刻 GPU 满载、任意请求不被卡顿」。
  • 业务选策略:实时聊天优先 Decode、RAG 优先 Prefill、批量吞吐优先、多租户公平。
  • DeepSpeed Dynamic Splitfuse 是变体,动态调整 chunk 大小,反映调度自适应化趋势。
  • 其他调度优化:优先级、预测式、取消与超时,共同构成 LLM 推理调度器这一活跃领域。

下一节《7.2 模型量化:INT8/INT4、AWQ 与 GPTQ》将讲清模型压缩这一降本的关键武器。


发布者: 作者: 灏天文库 转发
评论区 (0)
U