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分钟搞定。
为什么会这样?
核心原因有三个:
- GPU利用率提升:单条推理时,GPU经常处于“吃不饱”的状态。批量推理把多个请求打包,让GPU的算力被充分压榨。你想想看,一张A100有312 TFLOPS的算力,只算一条prompt,多浪费。
- 减少CPU-GPU通信开销:每次调用
generate(),都需要把数据从CPU拷贝到GPU。批量一次拷贝,比分多次拷贝快得多。这个开销在数据量大时尤其明显。 - 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倍。数据量越大,差距越明显。
batch_size=32或64开始试,逐步往上加,找到你硬件能承受的上限。
4.4 知识体系一览
为了让你更直观地理解本章的核心逻辑,我画了张图:
嗯,这张图把整个流程串起来了。从输入列表,到vLLM内部处理,再到输出结果,最后对比性能。你写代码的时候,脑子里可以一直挂着这张图。
好了,批量推理的核心就这些。说白了就是一句话:能用列表就别用循环,能批量就别单条。这不仅是vLLM的用法,也是所有高性能推理框架的通用原则。
公众号:蓝海资料掘金营,微信deep3321