4、离线批处理基础:使用vLLM的离线API进行批量文本生成、输入输出格式、批处理参数设置
vLLM 的离线批处理(Offline Batch Inference)是生产环境中最高频的使用模式之一。与在线服务(Online Serving)不同,离线批处理不依赖 HTTP 服务器,而是通过 Python 直接调用 LLM 和 SamplingParams 两个核心类,在单进程内完成大批量样本的推理。这种模式特别适合数据清洗、语料扩充、离线评测、知识库构建等不需要实时响应的场景。本章将系统讲解离线批处理的核心原理、输入输出格式、关键参数设置,并通过一个完整的案例拆解帮助您掌握从原始文本到批量生成结果的全流程。
上图展示了 vLLM 离线批处理的完整链路。从左侧输入层开始,用户以 Python 列表形式传入多个 prompt 字典;中间层通过 SamplingParams 控制生成策略;推理引擎 LLM 负责模型加载与显存管理;最终输出层返回 RequestOutput 对象列表。整个流程的核心优势在于 vLLM 的 PagedAttention 和 Continuous Batching 机制,使得批处理吞吐量远高于朴素实现。
4.1 核心原理:从 LLM 类到 Continuous Batching
vLLM 离线批处理的核心是 LLM 类。该类在初始化时完成模型权重加载、KV Cache 分配、CUDA 图捕获等准备工作。与在线模式不同,离线模式不需要启动 API 服务器,所有推理都在当前 Python 进程中同步执行。其底层依赖的 PagedAttention 技术将 KV Cache 划分为固定大小的块(Block),按需分配,避免了传统注意力机制中的显存碎片和预分配浪费。
Continuous Batching(连续批处理)是 vLLM 吞吐量优势的关键。传统静态批处理必须等待整个 batch 中所有序列生成完毕才能释放显存并接收新请求,而连续批处理在每次前向传播后,立即将已经完成生成的序列移出,同时将新请求插入空闲槽位。这种细粒度的调度方式使得 GPU 计算资源始终处于饱和状态。
核心机制对比:
| 维度 | 静态批处理 | vLLM 连续批处理 |
|---|---|---|
| 调度粒度 | 整个 batch 完成 | 单个 token 生成后 |
| 显存利用率 | 存在空闲等待 | 动态分配,接近饱和 |
| 新请求插入 | 需等待当前 batch 结束 | 随时插入空闲槽位 |
| 吞吐量(相对) | 1x | 2-4x |
| 适用场景 | 小批量、固定长度 | 大批量、长度差异大 |
在实际使用中,LLM 类还支持 tensor_parallel_size 参数实现多 GPU 张量并行,以及 gpu_memory_utilization 控制显存占用比例。这些参数直接影响批处理的最大并发数。
4.2 输入输出格式:从 prompts 到 RequestOutput
离线批处理的输入是一个 Python 列表,列表中的每个元素是一个字典。最简单的格式为 {"prompt": "文本内容"}。对于需要多轮对话的场景,可以使用 {"prompt": "对话历史", "multi_modal_data": ...} 或 Chat 模板格式。vLLM 会自动应用模型自带的对话模板(Chat Template)进行格式化。
from vllm import LLM, SamplingParams
# 输入格式:List[dict]
prompts = [
{"prompt": "请用一句话解释什么是量子纠缠:"},
{"prompt": "写一首关于秋天的五言绝句:"},
{"prompt": "将以下英文翻译成中文:'Artificial Intelligence is transforming the world.'"},
]
# 设置采样参数
sampling_params = SamplingParams(
temperature=0.8,
top_p=0.95,
max_tokens=256,
n=2, # 每个 prompt 生成 2 个候选
)
# 初始化 LLM(加载模型)
llm = LLM(model="Qwen/Qwen2.5-7B-Instruct", gpu_memory_utilization=0.9)
# 执行批处理
outputs = llm.generate(prompts, sampling_params)
# 输出格式:List[RequestOutput]
for output in outputs:
print(f"Prompt: {output.prompt}")
for seq in output.outputs:
print(f"Generated: {seq.text}")
print(f"Finish reason: {seq.finish_reason}")
print(f"Token IDs: {seq.token_ids}")
RequestOutput 对象包含以下关键属性:prompt(原始输入文本)、prompt_token_ids(输入 token ID 列表)、outputs(一个 CompletionOutput 列表)。每个 CompletionOutput 包含 text(生成的文本)、token_ids、cumulative_logprob(累积对数概率)、finish_reason(结束原因,如 stop 或 length)。
经验提醒:当设置 n > 1 时,每个 prompt 会生成多个候选结果。此时 output.outputs 是一个列表,需要遍历处理。如果只需要最佳结果,建议设置 n=1 并配合 best_of 参数使用。
4.3 批处理参数设置:吞吐量与质量的平衡
批处理参数设置直接影响推理速度、显存占用和生成质量。以下参数需要重点关注:
| 参数名 | 默认值 | 作用 | 调优建议 |
|---|---|---|---|
temperature | 1.0 | 控制随机性,越低越确定 | 代码生成用 0.2,创意写作用 0.8 |
top_p | 1.0 | 核采样,累积概率截断 | 0.9-0.95 兼顾多样性与质量 |
max_tokens | 16 | 最大生成 token 数 | 根据任务设置,避免过长或截断 |
n | 1 | 每个 prompt 生成候选数 | 评测场景可设 3-5,生产环境设 1 |
best_of | None | 生成 n 个候选并返回最优 | 需要与 n 配合,消耗更多显存 |
stop | None | 停止词列表 | 如 ["\n\n", "###"] 防止生成过长 |
use_beam_search | False | 是否使用束搜索 | 追求高质量时开启,速度较慢 |
在批处理场景中,max_tokens 的设置尤为关键。如果设置过小,长文本生成会被截断;设置过大,会占用更多显存并降低批处理并发数。建议根据任务类型统计历史生成长度,设置一个覆盖 95% 情况的值。
风险提示:当 best_of 大于 1 时,vLLM 会为每个 prompt 生成多个候选序列,这会显著增加显存占用和计算时间。在批处理大批量数据时,务必评估 GPU 显存容量,必要时降低 best_of 或减小 max_tokens。
4.4 案例拆解:批量生成产品描述并过滤低质量结果
假设我们有一个电商场景,需要为 500 个商品生成营销描述。每个商品提供名称、核心卖点和目标人群三个字段。我们使用 vLLM 离线批处理完成生成,并通过关键词过滤和长度检查筛选低质量结果。
初始数据样例:
products = [
{"name": "无线降噪耳机", "feature": "40小时续航,主动降噪", "audience": "通勤族"},
{"name": "智能手环", "feature": "血氧监测,14天续航", "audience": "运动爱好者"},
# ... 共 500 条
]
批处理脚本:
from vllm import LLM, SamplingParams
import json
# 构造 prompts
prompts = []
for p in products:
prompt = f"""请为以下商品撰写一段吸引人的营销描述(50字以内):
商品名称:{p['name']}
核心卖点:{p['feature']}
目标人群:{p['audience']}
营销描述:"""
prompts.append({"prompt": prompt})
# 设置参数:温度适中,限制长度
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=80,
stop=["\n\n", "。"],
)
# 初始化模型(使用量化减少显存)
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct",
gpu_memory_utilization=0.85,
dtype="float16",
)
# 分批处理,每批 100 条
batch_size = 100
all_outputs = []
for i in range(0, len(prompts), batch_size):
batch = prompts[i:i+batch_size]
outputs = llm.generate(batch, sampling_params)
all_outputs.extend(outputs)
print(f"Processed {min(i+batch_size, len(prompts))}/{len(prompts)}")
# 后处理:过滤低质量结果
valid_results = []
for idx, output in enumerate(all_outputs):
text = output.outputs[0].text.strip()
# 过滤条件:长度大于 10 且包含商品名
if len(text) >= 10 and products[idx]["name"] in text:
valid_results.append({
"product_id": idx,
"name": products[idx]["name"],
"description": text
})
else:
print(f"Filtered: {products[idx]['name']} -> {text[:20]}...")
print(f"Valid results: {len(valid_results)}/{len(products)}")
# 保存结果
with open("product_descriptions.json", "w", encoding="utf-8") as f:
json.dump(valid_results, f, ensure_ascii=False, indent=2)
执行结果与复盘:
- 吞吐量:500 条数据在单张 A100-80G 上耗时约 3 分 20 秒,平均每条 0.4 秒,吞吐量约 2.5 条/秒。
- 过滤结果:有效结果 437 条(87.4%),过滤掉 63 条。主要原因是部分生成文本过短(如"好产品!")或未包含商品名。
- 参数调整:初次运行时
max_tokens=50导致约 15% 的生成被截断,调整到 80 后截断率降至 2%。 - 显存监控:批处理 100 条时峰值显存占用约 42GB,设置
gpu_memory_utilization=0.85后稳定在 68GB 左右,留有安全余量。
复盘经验:批处理场景中,建议先在小批量(20-50 条)上验证参数效果,再扩展到全量数据。同时,后处理过滤逻辑是保证输出质量的关键环节,不要省略。对于生成质量要求更高的场景,可以设置 n=3 并选择 cumulative_logprob 最高的结果。
4.5 常见误区与总结
在实际使用 vLLM 离线批处理时,以下误区需要特别注意:
- 误区一:忽略
max_tokens的截断风险。很多用户设置过小的max_tokens导致输出被截断,且finish_reason为length而非stop。建议在解析输出时检查finish_reason,对截断结果进行标记或重试。 - 误区二:盲目增大
n或best_of提升质量。这会线性增加显存和计算开销。在资源受限时,优先通过优化 prompt 或调整temperature来提升质量。 - 误区三:不设置
stop词导致生成失控。对于结构化输出任务,务必设置stop词(如["\n\n", "###", "```"])来限制生成范围。 - 误区四:忽略
dtype和量化选项。在显存不足时,可以使用dtype="float16"或加载 AWQ/GPTQ 量化模型,但需注意量化可能带来轻微的质量损失。 - 误区五:在循环中重复初始化
LLM对象。模型加载非常耗时,应只初始化一次,通过循环调用generate处理多批数据。
总结而言,vLLM 离线批处理的核心在于理解 LLM 与 SamplingParams 的配合方式,掌握输入输出格式,并根据任务特点合理设置批处理参数。通过 Continuous Batching 机制,vLLM 能够在大批量场景下保持高吞吐量,是构建离线推理管线的理想选择。在实际项目中,建议将批处理逻辑封装为独立模块,便于复用和参数调优。