4、批量推理入门:使用LLM.generate()进行列表输入、理解返回的RequestOutput对象、批量推理的性能优势

好,咱们今天来聊聊批量推理。

说实话,很多刚接触vLLM的朋友,上来就习惯一条一条地喂数据。一条请求,等一个结果,再发下一条。这就像去食堂打饭,你每次只打一个菜,端回座位吃完,再去打第二个。累不累?效率高不高?

我个人习惯是,能批量就批量。vLLM的LLM.generate()接口,天生就支持一次塞进去一堆prompt。你想想看,这背后省了多少次函数调用、多少次内存拷贝、多少次GPU kernel启动的开销。

4.1 从单条到列表:LLM.generate()的输入进化

先看个最简单的例子。假设你有一个prompt:

from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-7B-Instruct")
sampling_params = SamplingParams(temperature=0.7, max_tokens=512)

# 单条输入
prompt = "请用一句话解释什么是量子纠缠。"
output = llm.generate(prompt, sampling_params)
print(output[0].outputs[0].text)

嗯,这没问题。但如果你有100个问题要问呢?

别写循环。直接上列表:

prompts = [
    "请用一句话解释什么是量子纠缠。",
    "请用一句话解释什么是相对论。",
    "请用一句话解释什么是黑洞。",
    # ... 可以加更多
]

outputs = llm.generate(prompts, sampling_params)

就这么简单。你把所有prompt装进一个Python列表,传给generate()。vLLM内部会自动做动态batching——它会把这些prompt拼成一个batch,一起送进GPU计算。

核心要点: 输入从字符串变成字符串列表,代码改动只有一行。但背后的性能差距,可能是数量级的。

4.2 理解返回的RequestOutput对象

调用generate()之后,返回的是什么?

是一个List[RequestOutput]。每个RequestOutput对应你输入列表里的一条prompt。

我刚开始用的时候,也懵过。打印出来一看,是个对象,不是字符串。咱们拆开看看:

outputs = llm.generate(prompts, sampling_params)

for i, output in enumerate(outputs):
    print(f"=== 第 {i+1} 条请求 ===")
    print(f"请求ID: {output.request_id}")
    print(f"输入prompt: {output.prompt}")
    print(f"生成的token数: {len(output.outputs[0].token_ids)}")
    print(f"生成文本: {output.outputs[0].text}")
    print(f"结束原因: {output.outputs[0].finish_reason}")
    print()

RequestOutput里最常用的几个字段:

字段 类型 说明
request_id str vLLM自动分配的请求ID,方便追踪
prompt str 你输入的原始prompt文本
prompt_token_ids List[int] prompt被tokenizer编码后的token ID列表
outputs List[CompletionOutput] 生成结果列表。通常只有一个元素,除非你设置了n > 1

CompletionOutput里,我们最关心的是:

字段 说明
index 第几个生成结果(0开始)
text 生成的文本内容
token_ids 生成文本对应的token ID列表
cumulative_logprob 整个生成序列的对数概率之和。可以用来评估生成质量
finish_reason 停止原因:stop(正常结束)、length(达到max_tokens)等
小技巧: 如果你需要批量提取所有生成文本,一行代码搞定:
all_texts = [output.outputs[0].text for output in outputs]

4.3 批量推理的性能优势

好了,理论说完了。咱们来点实际的。

我曾经在一个项目中,需要处理10万条客服问答。一开始用单条循环,跑了快两个小时。改成批量之后,同样的数据,15分钟搞定。

为什么会这样?

核心原因有三个:

  1. GPU利用率提升:单条推理时,GPU经常处于“吃不饱”的状态。批量推理把多个请求打包,让GPU的算力被充分压榨。你想想看,一张A100有312 TFLOPS的算力,只算一条prompt,多浪费。
  2. 减少CPU-GPU通信开销:每次调用generate(),都需要把数据从CPU拷贝到GPU。批量一次拷贝,比分多次拷贝快得多。这个开销在数据量大时尤其明显。
  3. vLLM的PagedAttention优化:vLLM内部使用了PagedAttention,它能把不同请求的KV cache高效地组织在一起。批量推理时,这些cache可以共享内存页,进一步减少显存浪费。

咱们做个简单对比:

import time

# 准备100条prompt
prompts = ["给我讲一个关于{}的故事。".format(i) for i in range(100)]

# 方式一:单条循环
start = time.time()
for p in prompts:
    llm.generate(p, sampling_params)
print(f"单条循环耗时: {time.time() - start:.2f}秒")

# 方式二:批量推理
start = time.time()
llm.generate(prompts, sampling_params)
print(f"批量推理耗时: {time.time() - start:.2f}秒")

在我本地的测试中,批量推理比单条循环快了5到10倍。数据量越大,差距越明显。

注意: 批量不是越大越好。受限于GPU显存,如果batch size太大,可能会OOM(显存溢出)。我建议从batch_size=3264开始试,逐步往上加,找到你硬件能承受的上限。

4.4 知识体系一览

为了让你更直观地理解本章的核心逻辑,我画了张图:

批量推理核心流程 输入:List[str] 多个prompt组成的列表 LLM.generate() 动态batching PagedAttention优化 List[RequestOutput] 每个prompt对应一个 性能对比 单条循环:100条 → 约120秒 GPU利用率低,通信开销大 批量推理:100条 → 约15秒 GPU满载,通信一次完成 提速约 8 倍!数据量越大,优势越明显 ⚠️ 注意控制batch size,避免显存溢出

嗯,这张图把整个流程串起来了。从输入列表,到vLLM内部处理,再到输出结果,最后对比性能。你写代码的时候,脑子里可以一直挂着这张图。

避坑指南: 我曾经遇到过一个问题——批量推理时,如果prompt长度差异很大(有的10个token,有的2000个token),vLLM会以最长的prompt为准做padding。这会导致短prompt浪费计算资源。我的建议是:尽量把长度相近的prompt放在同一个batch里。如果数据长短不一,可以先按长度排序,再分批。

好了,批量推理的核心就这些。说白了就是一句话:能用列表就别用循环,能批量就别单条。这不仅是vLLM的用法,也是所有高性能推理框架的通用原则。


公众号:蓝海资料掘金营,微信deep3321