一、动态批处理基础:从排队打饭说起

大家好,我是你们这堂课的主讲人。先聊个题外话——你有没有在食堂排过队?

想象一下:打饭窗口只有一个阿姨。你点红烧肉,后面的人点清蒸鱼,再后面的人点番茄炒蛋。阿姨必须一个一个来,做完一份再做下一份。这就是典型的静态批处理——请求必须按顺序排队,前一个没处理完,后一个就得等着。

但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动态批处理核心架构 请求队列 调度器 调度策略 • 等待时间优先 • 请求长度优先 • 公平调度 内存管理器 PagedAttention • 分页管理KV Cache • 按需分配/回收 • 零碎片化 执行引擎 GPU推理 • 批量矩阵运算 • 注意力计算 • 采样输出 输出 反馈:已完成请求退出,释放内存

vLLM的解决方案可以概括为三个核心组件:

  1. 调度器(Scheduler):负责决定哪些请求进入当前批次,哪些请求等待。它考虑的因素包括:请求的等待时间、请求的生成进度、当前GPU内存使用情况。
  2. 内存管理器(Memory Manager):基于PagedAttention技术,把KV Cache分页管理。每个请求按需分配内存页,请求完成后立即回收。这解决了内存碎片化的问题。
  3. 执行引擎(Execution Engine):负责实际的GPU推理计算。它接收调度器传来的批次,执行前向传播,然后返回结果。

个人经验:我刚开始看vLLM源码时,最让我惊艳的就是PagedAttention的设计。它把操作系统虚拟内存的思想搬到了LLM推理中。你想想看,操作系统用分页解决了内存碎片,vLLM用同样的思路解决了KV Cache碎片。这种跨领域的借鉴,才是真正的工程智慧。

这三个组件协同工作,形成了一个闭环:请求到达→调度器决策→内存分配→执行推理→输出结果→释放内存→调度器继续决策。每一步都环环相扣,缺一不可。

好了,这一章我们打下了基础。你理解了动态批处理是什么,也知道了vLLM是怎么解决核心挑战的。下一章,我们会深入调度器的内部,看看它到底是怎么做决策的。


专注资料整理