从理论到实践:vLLM 如何实现连续批处理与吞吐优化
你是否曾遇到过这样的情况:线上服务的 GPU 利用率显示仅为 30%,但用户却频繁反馈“生成速度慢”“响应排队时间长”?这在大模型推理部署中其实极为常见。表面上看,GPU 正在全力运行,但实际上大量计算资源被浪费在无谓的等待上——等待请求齐备、等待长序列完成、等待内存释放……
vLLM 的出现,如同为系统装上了一套“智能调度引擎”,极大地减少了 GPU 空闲时间,使其几乎始终处于高效运转状态。它是如何做到这一点的?关键在于两个字:
灵活
它并非依赖硬件堆叠,而是通过系统层面的精巧设计,尤其是 PagedAttention 和 连续批处理(Continuous Batching) 这两项核心技术,打破了传统 LLM 推理中的“内存墙”和“调度僵局”,真正实现了显存与算力的极致利用。
我们先思考一个问题:为什么传统的 Transformer 推理方式显得如此低效?
每生成一个 token,都需要重新读取之前所有的 Key-Value 缓存。为了保证访问效率,这些缓存通常必须存放在连续的显存空间中。因此,大多数框架的做法是:
为每个请求预分配最大可能长度的 KV 缓存空间。
这种策略看似稳妥,但在实际应用中却问题重重:
- 用户仅输入 100 个 token,系统却为其预留了 8192 的空间,造成超过 7000 的显存浪费;
- 多个短请求因某个长文本的存在而无法及时释放内存,导致整体吞吐受阻;
- 显存使用率接近上限,但真实利用率可能还不到一半。
这一系列问题催生了 vLLM 的第一项突破性技术:PagedAttention。
它引入了一个极具创意的机制——将 KV 缓存像操作系统管理内存一样进行分页处理。不再要求物理上的连续存储,只需保持逻辑上的顺序一致即可。每个“页”具有固定大小(例如 2048 个 token),一个序列可以跨越多个离散的页,通过一张“页表”来映射逻辑位置到具体的物理页及其偏移量。
这是否让你联想到虚拟内存机制?没错,这正是将操作系统中的经典思想成功迁移至深度学习推理领域的体现!
采用该方案后:
- 短请求仅占用少量页面,避免资源浪费;
- 长请求可动态扩展页数,无需担心溢出;
- 当多个请求共享相同 prompt 时,前缀部分的页可以直接复用,进一步节省开销。
此外,vLLM 还专门开发了高性能的 CUDA kernel 来处理“跨页注意力”计算,确保即使数据分布不连续,也能维持高效的运算速度。这才是真正的软硬协同优化。
来看一个简化的页表结构示意:
class PageTable:
def __init__(self, page_size: int):
self.page_size = page_size
self.pages = [] # 物理页 ID 列表
def append_token(self, token_idx: int):
page_id = token_idx // self.page_size
if len(self.pages) <= page_id:
self.pages.append(self._allocate_new_page())
return self.pages[page_id], token_idx % self.page_size
def _allocate_new_page(self):
return hash(f"page_{len(self.pages)}")
尽管图示以 Python 模拟呈现,但实际实现是在 C++/CUDA 层级完成,具备极低延迟。其中,页大小的选择至关重要:若设置过小,会导致页表膨胀和寻址开销上升;若过大,则又回归粗粒度分配的老路。实践中,2048 或 4096 是较为平衡的选择。
小贴士:如果你发现 P99 延迟波动明显,建议检查当前页大小是否适配你的典型上下文长度分布。
解决了显存浪费问题后,下一个瓶颈浮出水面:为何 GPU 总是“忙一阵、歇一阵”?
传统批处理模式类似于公交车——必须等所有乘客上车后才发车。即便部分任务早已就绪,也只能被动等待最后一个请求加入。结果就是:GPU 要么满载运行,要么陷入空转。
vLLM 采取的策略更接近地铁系统:随时有人上下车,列车持续前行。这就是所谓的 连续批处理(Continuous Batching)。
其核心理念非常直接:
只要系统中仍有活跃请求,就继续执行下一步 decode 操作。
新请求到达?立即插入当前批次;某请求完成?立刻移除,不影响其他任务。整个过程完全异步,batch size 动态调整,使得 GPU 几乎始终有任务可执行。实测表明,吞吐量可轻松提升 5–10 倍。
以下是该调度机制的核心流程示意:
import asyncio
from typing import List, Dict
class ContinuousBatchScheduler:
def __init__(self, max_batch_size: int = 256):
self.max_batch_size = max_batch_size
self.active_sequences: Dict[str, SequenceState] = {}
self.request_queue = asyncio.Queue()
async def run(self):
while True:
# 实时接入新请求
if not self.request_queue.empty():
req = await self.request_queue.get()
self.active_sequences[req.id] = SequenceState(req)
# 构建当前仍在生成的序列列表
running_seqs = [s for s in self.active_sequences.values() if not s.is_finished()]
if not running_seqs:
await asyncio.sleep(0.001)
continue
# 统一前向传播
batch_logits = model_forward([s.get_inputs() for s in running_seqs])
# 逐个更新状态
for seq, logits in zip(running_seqs, batch_logits):
next_token = sample_token(logits)
seq.append_output(next_token)
if seq.should_stop():
seq.mark_finished()
# 清理已完成任务
self._cleanup_finished()
可以看到,没有“等待齐整再启动”,只有“边进边出”的动态流转。每一个推理 step 都是对当前所有活跃请求的一次并行推进。
不过,在实施过程中也需注意几个潜在风险:
- 必须严格管理每个 sequence 的生命周期,防止内存泄漏;
- 应设置单个请求的最大生成长度限制,避免个别超长任务长期占用资源;
- 对于高优先级任务,可扩展支持优先级队列机制,防止被长尾请求拖慢整体响应。
max_tokens
接下来,我们将这些技术置于真实应用场景中进行验证。
假设你正在构建一个类似“模力方舟”的 AI 平台,整体架构大致如下:
[客户端]
↓
[API 网关 + LB]
↓
[vLLM 推理集群]
↓
[监控 / 存储 / 日志]
接入 vLLM 后,一次典型的文本生成流程如下:
- 用户发送 prompt,通过 OpenAI 兼容接口传入;
- vLLM 实例接收请求,创建对应的 sequence 对象,并初始化页表;
- 执行第一步 decode,生成首个 token;
- 后续每一步中,该请求与其他活跃请求组成动态 batch 并行处理;
- 输出结果通过 streaming 方式实时返回前端;
- 请求完成后或超时,相关页资源被回收;
- 最终结果返回,性能指标同步上报至 Prometheus。
/v1/completions
在整个流程中,PagedAttention 精准掌控显存分配,连续批处理确保 GPU 始终高负载运行。二者协同作用,显著提升单机吞吐能力,从而有效降低单位推理成本。
下面我们总结一下 vLLM 所解决的关键痛点:
| 痛点 | 传统方案 | vLLM 改进 |
|---|---|---|
| 显存浪费严重 | 预分配导致利用率 <40% | 按需分页分配,利用率可达 80%+ |
| 吞吐受限于最长请求 | 静态批处理,整体节奏由最慢者决定 | 连续批处理,各请求独立推进 |
| 响应延迟波动大 | 批量等待造成 P99 延迟飙升 | 动态调度平滑延迟分布 |
| 共享前缀无法复用 | KV 缓存重复存储 | 页级缓存共享,减少冗余计算 |
综上所述,vLLM 并非简单地加速模型推理,而是从系统架构层面重构了 LLM 服务的运行范式。通过 PagedAttention 与连续批处理的深度融合,它实现了资源利用率、吞吐能力和响应稳定性的全面提升,为大规模 AI 应用落地提供了坚实的技术支撑。
分页机制实现按需分配,资源利用率超过80%,显著提升GPU使用效率。传统静态批处理常导致设备空闲,而通过动态合并请求的方式,系统可接近饱和运行,充分发挥硬件性能。
面对扩容成本高、并发能力弱的问题,多数方案只能依赖横向堆叠机器来应对负载压力。相比之下,单实例承载更高请求量成为更优选择,有效降低整体TCO(总拥有成本)。
值得一提的是,该架构原生支持包括LLaMA、Qwen、ChatGLM在内的主流大语言模型,并兼容GPTQ与AWQ等量化格式。这意味着在有限显存条件下也能运行更大规模的模型,极大提升了推理性价比。
class PageTable:
def __init__(self, page_size: int):
self.page_size = page_size
self.pages = [] # 物理页 ID 列表
def append_token(self, token_idx: int):
page_id = token_idx // self.page_size
if len(self.pages) <= page_id:
self.pages.append(self._allocate_new_page())
return self.pages[page_id], token_idx % self.page_size
def _allocate_new_page(self):
return hash(f"page_{len(self.pages)}")
工程实践中的关键优化建议
合理设置页大小:避免直接采用默认配置。应根据实际业务中平均context长度进行调优,通常2048至4096为较优范围。设置过高易引发内存碎片,过低则增加页表管理开销。
积极启用模型量化:对于非敏感性任务,推荐使用GPTQ或AWQ技术,可进一步减少30%~50%显存占用。结合PagedAttention机制,能更彻底地释放资源潜力。
完善监控体系:重点关注P99延迟、OOM发生次数、平均批处理大小以及页命中率等核心指标。这些数据是评估系统稳定性和性能表现的关键“生命体征”。
结合Kubernetes HPA实现弹性伸缩:依据实时流量自动扩缩容,在高峰期保障服务稳定性,低峰期节省资源支出,兼顾成本与可靠性。
import asyncio
from typing import List, Dict
class ContinuousBatchScheduler:
def __init__(self, max_batch_size: int = 256):
self.max_batch_size = max_batch_size
self.active_sequences: Dict[str, SequenceState] = {}
self.request_queue = asyncio.Queue()
async def run(self):
while True:
# 实时接入新请求
if not self.request_queue.empty():
req = await self.request_queue.get()
self.active_sequences[req.id] = SequenceState(req)
# 构建当前仍在生成的序列列表
running_seqs = [s for s in self.active_sequences.values() if not s.is_finished()]
if not running_seqs:
await asyncio.sleep(0.001)
continue
# 统一前向传播
batch_logits = model_forward([s.get_inputs() for s in running_seqs])
# 逐个更新状态
for seq, logits in zip(running_seqs, batch_logits):
next_token = sample_token(logits)
seq.append_output(next_token)
if seq.should_stop():
seq.mark_finished()
# 清理已完成任务
self._cleanup_finished()
从绿色计算角度出发:单位token能耗的下降不仅意味着更低的运营成本,也体现了对环境友好的技术路径。未来在ESG报告中亦可作为可持续发展的有力支撑。
vLLM:重新定义大模型服务范式
vLLM不仅仅是一个推理加速工具,它代表了对现有LLM服务架构的深度重构。其核心启示在于——性能瓶颈往往不来自算力本身,而是源于资源调度方式的低效。
借鉴操作系统的设计思想,如分页管理、任务调度与内存虚拟化,vLLM成功将昂贵的GPU资源转化为高效的流水线作业模式。
展望未来,更多前沿方向正在展开:MoE架构下的专家路由调度、动态稀疏注意力机制、CPU-GPU协同卸载策略等。而vLLM已在这些领域率先布局,持续引领技术创新。
当你下次遇到“GPU利用率居高不下但吞吐迟迟无法提升”的困境时,不妨思考一个问题:
是模型能力不足?还是调度逻辑不够智能?
答案,或许就藏在 PagedAttention + Continuous Batching 这八个字之中。


雷达卡


京公网安备 11010802022788号







