一、动态批处理基础:从排队打饭说起
大家好,我是你们这堂课的主讲人。先聊个题外话——你有没有在食堂排过队?
想象一下:打饭窗口只有一个阿姨。你点红烧肉,后面的人点清蒸鱼,再后面的人点番茄炒蛋。阿姨必须一个一个来,做完一份再做下一份。这就是典型的静态批处理——请求必须按顺序排队,前一个没处理完,后一个就得等着。
但LLM推理的场景比食堂复杂得多。用户发来的请求,有的长(比如“请写一篇5000字的论文”),有的短(比如“你好”)。如果让所有请求都排队等,短请求会被长请求活活拖死。这就是动态批处理要解决的问题。
核心定义:动态批处理(Dynamic Batching)是指在推理过程中,根据当前系统负载和请求特征,实时地将多个请求组合成一个批次进行并行处理,并在处理过程中动态调整批次构成。
1.1 静态批处理 vs 动态批处理
先看一张对比表,我当年刚接触这个领域时,就是靠这张表理清思路的:
| 对比维度 | 静态批处理 | 动态批处理 |
|---|---|---|
| 批次构成 | 请求到达时立即组批,批次固定 | 请求动态加入/退出批次 |
| 等待策略 | 必须等批次填满或超时 | 请求到达即可开始处理 |
| 长请求影响 | 短请求被长请求阻塞 | 短请求可提前完成并退出 |
| GPU利用率 | 批次满时利用率高,否则低 | 持续保持较高利用率 |
| 延迟表现 | 尾部延迟高,波动大 | 延迟更平滑,P99显著降低 |
说白了,静态批处理就像食堂阿姨必须凑够10个人才开火——人少时大家干等,人多时后面的人饿死。动态批处理则是:来一个人就做一份,做到一半又来一个人?行,把新人的菜也下锅,一起炒。
嗯,这里要注意:动态批处理不是简单的“来一个处理一个”。它背后有一套精密的调度机制。
1.2 动态批处理的核心挑战
我在项目中遇到过不少坑,总结下来,动态批处理要面对三大挑战:
- 挑战一:请求长度差异巨大
一个请求生成1个token,另一个要生成1024个token。把它们放在同一个批次里,短的早跑完了,长的还在跑。GPU资源怎么分配? - 挑战二:内存管理复杂
每个请求的KV Cache大小不同,动态加入退出意味着内存碎片化严重。我曾经见过一个系统,跑着跑着内存利用率只剩40%,剩下的全成了碎片。 - 挑战三:调度决策实时性
什么时候把新请求加入批次?什么时候让完成的请求退出?决策延迟超过1毫秒,整个系统的吞吐就会下降。你想想看,这就像打篮球——传球时机差0.1秒,机会就没了。
避坑指南:我曾经在一个项目中,为了追求极致吞吐,把调度间隔设得太短(0.1ms)。结果调度器本身占用了大量CPU资源,反而拖慢了推理速度。后来我调整到1ms的调度间隔,配合预取机制,效果反而更好。调度不是越快越好,要找到平衡点。
1.3 vLLM的解决方案概览
vLLM是怎么解决这些问题的?我画了一张图,帮你快速建立整体认知:
vLLM的解决方案可以概括为三个核心组件:
- 调度器(Scheduler):负责决定哪些请求进入当前批次,哪些请求等待。它考虑的因素包括:请求的等待时间、请求的生成进度、当前GPU内存使用情况。
- 内存管理器(Memory Manager):基于PagedAttention技术,把KV Cache分页管理。每个请求按需分配内存页,请求完成后立即回收。这解决了内存碎片化的问题。
- 执行引擎(Execution Engine):负责实际的GPU推理计算。它接收调度器传来的批次,执行前向传播,然后返回结果。
个人经验:我刚开始看vLLM源码时,最让我惊艳的就是PagedAttention的设计。它把操作系统虚拟内存的思想搬到了LLM推理中。你想想看,操作系统用分页解决了内存碎片,vLLM用同样的思路解决了KV Cache碎片。这种跨领域的借鉴,才是真正的工程智慧。
这三个组件协同工作,形成了一个闭环:请求到达→调度器决策→内存分配→执行推理→输出结果→释放内存→调度器继续决策。每一步都环环相扣,缺一不可。
好了,这一章我们打下了基础。你理解了动态批处理是什么,也知道了vLLM是怎么解决核心挑战的。下一章,我们会深入调度器的内部,看看它到底是怎么做决策的。