【infra之路】11-Continuous Batching 与调度策略

发布时间:2026/7/1 14:56:58
【infra之路】11-Continuous Batching 与调度策略 学习目标理解为什么 Static Batching 导致 GPU 利用率低以及 Continuous BatchingOrca和 RadixAttentionSGLang如何通过请求级动态调度让推理吞吐量再提升 2-10 倍。1. Static Batching 的问题1.1 什么是 Static Batching传统做法是把多个请求组成一个 batch整个 batch 一起开始、一起结束时刻 0: Batch [请求A, 请求B, 请求C] - 请求 A: 你好 → 需要生成 50 个 token - 请求 B: 请写一篇论文 → 需要生成 2000 个 token - 请求 C: 翻译这句话 → 需要生成 100 个 token 时刻 1: 三个请求同时开始 Prefill 时刻 2: 三个请求同时进入 Decode 时刻 3: 请求 A 生成完 50 个 token → **必须等 B 和 C** 时刻 4: 请求 C 生成完 100 个 token → **必须等 B** 时刻 5: 请求 B 生成完 2000 个 token → **整个 batch 结束** 问题A 和 C 早就完成了但 GPU 还在为 B 工作A 和 C 的算力被浪费1.2 GPU 利用率分析假设 batch_size 32但请求长度差异很大请求长度分布真实场景: 短请求100 tokens: 40% 中等请求100-500 tokens: 40% 长请求500 tokens: 20% Static Batching 的问题: - 短请求完成后GPU 算力被浪费它们在等长请求 - 新请求必须等当前 batch 全部完成才能加入 - 为了不让短请求等太久只能限制 max_new_tokens影响长请求 GPU 利用率: 通常只有 30-50%2. Continuous BatchingOrca2.1 核心思想Iteration-level SchedulingOrca 论文的关键洞察调度粒度应该是 token 而不是请求。Static Batching: 调度单位 请求 整个 batch 一起开始、一起结束 新请求必须等当前 batch 完成 Continuous Batching: 调度单位 token一次 decode iteration 每个请求独立进出 完成即退出新请求随时加入2.2 工作流程时刻 0: 等待队列 [A, B, C, D, E] Batch [] 时刻 1: 调度器选择 A, B, C 进入 batch假设 max_batch_size3 Batch [A, B, C] 执行 1 次 Prefill 1 次 Decode 时刻 2: A 完成生成了 EOS→ 退出 batch D 从等待队列加入 Batch [B, C, D] 执行 1 次 Decode 时刻 3: C 完成 → 退出 E 加入 Batch [B, D, E] 执行 1 次 Decode 时刻 4: B 完成 → 退出 等待队列为空 Batch [D, E] 执行 1 次 Decode ...关键点每次 Decode iteration 后都检查哪些请求完成了哪些新请求可以加入。2.3 与 PagedAttention 的配合Continuous Batching 需要 PagedAttention 才能高效实现请求 A 完成释放 Block → 新请求 D 立即使用这些 Block 请求 E 加入只需分配几个 Block不需要预分配连续显存 如果没有 PagedAttention: - 预分配的显存无法动态回收给新请求 - 新请求需要等待整个 batch 完成才能分配显存2.4 性能提升Orca 论文的实验结果相比 Static BatchingContinuous Batching 实现了3-10 倍的吞吐量提升取决于请求长度分布。核心原因GPU 始终在处理活跃的请求没有等待长请求的空闲时间。3. 调度策略3.1 请求生命周期一个请求在推理系统中的状态到达 → 等待队列Waiting→ Prefill → DecodeRunning→ 完成Finished ↓ 抢占Preempted→ 等待队列抢占如果显存不足调度器可以暂停某些 Decode 中的请求把它们的 KV Cache 换出到 CPU 内存或磁盘等显存充足时再换回来继续。3.2 调度算法FCFSFirst-Come-First-Served最简单的策略按到达顺序处理。等待队列: [A(到达时间0), B(到达时间1), C(到达时间2)] 调度顺序: A → B → C 优点: 简单公平 缺点: 无法区分优先级短请求可能被长请求阻塞Priority-based优先级调度给不同请求分配优先级优先级规则示例: - VIP 用户请求: 优先级 3 - 普通用户请求: 优先级 2 - 后台批处理: 优先级 1 等待队列: [A(优先级2), B(优先级3), C(优先级1)] 调度顺序: B → A → CPreemption抢占策略当显存不足时选择低优先级或长请求暂停当前 Batch: [A(已生成500 tokens), B(已生成10 tokens), C(已生成50 tokens)] 显存不足需要加入新的高优先级请求 D 抢占策略: 1. 选择 A 抢占已生成最多换出代价最小 2. 把 A 的 KV Cache 换出到 CPU 内存 3. A 的状态变为 Preempted放回等待队列 4. D 加入 batch 5. 等显存充足时A 再换回来继续vLLM 的抢占策略是swap or recomputeSwap: 把 KV Cache 换出到 CPU 内存恢复时换回来Recompute: 直接丢弃 KV Cache恢复时重新做 Prefill适用于短请求3.3 调度时机Iteration-level schedulingOrca/vLLM每次 Decode iteration 后都调度whileTrue:# 1. 执行一次 Decode iterationbatchrun_decode_step(batch)# 2. 检查哪些请求完成了finished[reqforreqinbatchifreq.is_finished()]batch[reqforreqinbatchifnotreq.is_finished()]# 3. 从等待队列加入新请求whilewaiting_queueandcan_add_to_batch():new_reqwaiting_queue.pop(0)batch.append(new_req)# 4. 对新加入的请求做 Prefillifnew_requests:run_prefill(new_requests)优点响应快GPU 利用率高。缺点调度开销稍大每 iteration 都要检查。Step-level scheduling早期方案整个 batch 运行完才调度whileTrue:batchselect_from_waiting_queue(max_batch_size)run_prefill(batch)run_decode_until_all_finished(batch)# 所有请求都完成才退出# 然后才能调度新 batch缺点短请求等长请求利用率低。4. Chunked Prefill4.1 问题Prefill 和 Decode 的计算量差异Prefill 是 compute-bound大量矩阵乘法Decode 是 memory-bound逐个 token 生成。如果把一个新请求的 Prefill 和现有请求的 Decode 放在同一个 batch 中Batch [A(Decode), B(Decode), C(Prefill 1000 tokens)] 问题: - C 的 Prefill 计算量巨大拖慢整个 batch - A 和 B 的 Decode 被阻塞TPOT每个 token 的延迟飙升 - 用户体验差正在生成的请求突然变慢4.2 Chunked Prefill 解决方案把长 Prefill 切成多个 chunk分散到多个 iteration 中请求 C 需要 Prefill 1000 tokenschunk_size 256 Iteration 1: Batch [A(Decode), B(Decode), C(Prefill tokens 0-255)] Iteration 2: Batch [A(Decode), B(Decode), C(Prefill tokens 256-511)] Iteration 3: Batch [A(Decode), B(Decode), C(Prefill tokens 512-767)] Iteration 4: Batch [A(Decode), B(Decode), C(Prefill tokens 768-999)] Iteration 5: C 的 Prefill 完成进入 Decode Batch [A(Decode), B(Decode), C(Decode)]每次 iteration 中C 只 Prefill 一小部分不会严重拖慢 A 和 B 的 Decode。4.3 vLLM 的实现vLLM 支持enable_chunked_prefillTrue可以配置max_num_batched_tokens控制每次 iteration 的 Prefill token 上限fromvllmimportLLM llmLLM(modelmeta-llama/Llama-2-7b-hf,enable_chunked_prefillTrue,max_num_batched_tokens2048,# 每次最多 Prefill 2048 tokens)5. SGLang 的 RadixAttention5.1 前缀共享问题很多请求共享相同的 prompt 前缀请求 A: 你是一个翻译专家。请把以下文本翻译成中文Hello world 请求 B: 你是一个翻译专家。请把以下文本翻译成中文Good morning 请求 C: 你是一个翻译专家。请把以下文本翻译成中文Thank you 共享前缀: 你是一个翻译专家。请把以下文本翻译成中文 (20 tokens)如果每个请求都独立计算这个前缀的 KV Cache浪费了 3 倍的计算和显存。5.2 RadixAttention基于 Radix Tree 的前缀缓存SGLang 用Radix Tree基数树管理所有 KV Cache BlockRadix Tree 结构: Root ├─ 你是一个翻译专家 (Block 0-2) │ └─ 。请把以下文本翻译成中文 (Block 3-5) │ ├─ Hello world (Block 6) ← 请求 A │ ├─ Good morning (Block 7) ← 请求 B │ └─ Thank you (Block 8) ← 请求 C当请求 A 计算完前缀后Block 0-5 的 KV Cache 被缓存在 Radix Tree 中。请求 B 和 C 到达时发现前缀匹配直接复用 Block 0-5只需计算自己的后缀部分。5.3 自动前缀匹配SGLang 的调度器在分配 Block 时自动查找 Radix Treedefallocate_blocks(request):promptrequest.prompt# 在 Radix Tree 中查找最长匹配前缀matched_noderadix_tree.find_longest_prefix(prompt)ifmatched_node:# 复用已缓存的 Blockcached_blocksmatched_node.blocks remaining_promptprompt[len(matched_node.text):]# 只为剩余部分分配新 Blocknew_blocksallocate_new_blocks(len(remaining_prompt))request.blockscached_blocksnew_blockselse:# 没有匹配全部分配新 Blockrequest.blocksallocate_new_blocks(len(prompt))5.4 性能提升SGLang 论文的实验结果场景相比 vLLM 的提升共享 system prompt2-5 倍吞吐Few-shot learning共享示例2-3 倍吞吐Multi-turn dialogue共享历史1.5-2 倍吞吐核心收益来自避免重复计算前缀尤其在 system prompt 很长如 1000 tokens的场景下效果显著。5.5 Cache Eviction缓存淘汰显存有限时需要淘汰旧的缓存 Block淘汰策略: LRU (Least Recently Used) 当显存不足时: 1. 找到最久未使用的 Radix Tree 叶节点 2. 释放其 Block 3. 如果后续请求需要相同前缀重新计算 类似 CPU cache 的淘汰机制6. 调度器架构对比vLLM 调度器Scheduler: - waiting_queue: 等待队列 - running: 当前运行的请求 - swapped: 被抢占换出的请求 每次 iteration: 1. 从 swapped 中尝试换回请求 2. 从 waiting_queue 中加入新请求 3. 如果显存不足抢占 running 中的请求 4. 执行 Prefill DecodevLLM 的调度相对简单重点是保证吞吐量和显存利用率。SGLang 调度器Scheduler: - Radix Tree: 管理所有 KV Cache 和前缀缓存 - waiting_queue: 等待队列 - running: 当前运行的请求 每次 iteration: 1. 根据 Radix Tree 选择能最大化前缀复用的请求 2. 分配 Block 时自动复用已缓存前缀 3. 执行 Prefill Decode 4. 更新 Radix Tree新计算的前缀加入缓存SGLang 的调度器更智能会主动选择能复用缓存的请求优先执行。7. 面试高频问题Q1: Continuous Batching 为什么需要 PagedAttentionContinuous Batching 需要请求随时加入/退出这就要求显存可以动态分配和回收。如果用预分配的连续显存新请求无法使用已完成的请求释放的空间因为不连续。PagedAttention 的 Block 机制让显存可以碎片化管理新请求只需分配几个 Block 即可。Q2: 抢占Preemption的代价是什么Swap 到 CPU 内存的代价是 PCIe 带宽~25 GB/s换回时需要等待传输完成。Recompute 的代价是重新做 Prefill 的计算量。vLLM 会根据请求长度选择短请求128 tokens倾向于 recompute长请求倾向于 swap。Q3: RadixAttention 和 Copy-on-Write 的关系RadixAttention 是前缀缓存机制多个请求共享相同前缀的 KV Cache Block。Copy-on-Write 是 PagedAttention 的特性多个请求的 Block Table 可以指向同一组物理 Block只有在需要修改时才复制。RadixAttention 利用了 Copy-on-Write 实现高效的前缀共享。Q4: 为什么不用更大的 batch size 来弥补 Static Batching 的浪费batch size 受限于 GPU 显存。Static Batching 的浪费正是显存被预分配但未使用导致可用显存减少batch size 反而更小。Continuous Batching PagedAttention 让同样的显存可以容纳更大的有效 batch size。总结概念要点Static Batching整个 batch 一起开始结束短请求等长请求GPU 利用率 30-50%Continuous Batching请求级动态调度完成即退出新请求随时加入利用率 90%Iteration-level scheduling每次 Decode iteration 后调度Orca/vLLM 的核心Preemption显存不足时暂停低优先级请求swap 或 recomputeChunked Prefill长 Prefill 切分成多个 chunk避免阻塞 DecodeRadixAttentionSGLang 的前缀缓存机制复用共享前缀提升 2-5 倍吞吐