多个独立问题:批量为一次请求还是拆分为多个?——LLM 并发处理分析
Source: Dev.to
请提供您希望翻译的具体内容(文章正文、代码块之外的文字),我将为您翻译成简体中文并保持原有的格式和 Markdown 语法。
哪个更快?
简短回答:
将问题拆分为 多个相互独立的并行请求 几乎总是更快。
为什么? – 从第一性原理看 LLM 的“写作”方式
| 步骤 | 发生了什么 | 延迟影响 |
|---|---|---|
| Autoregressive generation | 模型一次生成 一个 token,将其追加到提示中,然后生成下一个 token。 | N tokens → N forward passes |
| Prefill | 整个输入提示只处理一次,以构建 KV‑cache。 | 与输入长度线性相关 |
| Decode | Token 按顺序(逐个)生成。 | 主导总体延迟 |
后果
- 一个 100‑token 的答案大约需要 100 次推理步骤。
- 一个 500‑token 的答案大约需要 500 次推理步骤。
- 输出总长度直接决定总延迟。
场景:5 个独立问题,每个约 200 Token
方法 A – 将所有问题合并为一个请求
Please answer the following questions separately:
1. …
2. …
3. …
4. …
5. …
模型需要做的事情
- 预填充: 处理一个长的拼接提示(所有 5 个问题)。
- 解码: 生成 ≈ 5 × 200 = 1000 个 token 顺序进行。
- 额外开销:
- 上下文切换(“现在回答问题 3”)。
- 更大的 KV‑cache → 每步更多注意力计算。
- 格式化/过渡文本常常使 token 数超过 1000。
估计延迟 ≈ 1000 × (每 token 生成时间)。
方法 B – 并行发送 5 个独立请求
每个请求包含一个问题,生成 ≈ 200 个 token。
服务器的工作方式
- 预填充: 每个请求的提示更短。
- 解码: 5 条独立的解码流 并行 运行(或一起批处理)。
- 现代推理引擎(vLLM、TensorRT‑LLM、TGI 等)使用 连续批处理:单次 GPU 前向传播可以同时为 5 个请求各生成一个 token。
估计延迟 ≈ max(单个请求延迟)≈ 200 × (每 token 生成时间)。
直接比较
| 方法 | 总输出 Token 数 | 大致延迟(相对) |
|---|---|---|
| 合并请求 | ~1000+ | ~1000 次解码步骤(顺序) |
| 5 个并行请求 | ~200 每个 | ~200 次解码步骤(并行) |
理论加速比: ~5×(等于问题数量)。
为什么服务器端的并行请求更快
-
连续批处理 – GPU 在并行矩阵运算方面表现出色。
- 5 个短请求 → 5‑路批处理 前向传播,每一步产生 5 个 token。
- 1 个长请求 → 单序列前向传播,每一步仅产生 1 个 token。
-
更高的 GPU 利用率 – 批处理许多短序列可以让 GPU 保持忙碌,而单个长序列会浪费并行能力。
-
Prefill 与 Decode –
- 合并: 更长的 prefill + 更长的 decode。
- 拆分: 每个请求的 prefill 更短;所有 prefill 可以流水化或并发运行,每个 decode 也很短。
质量考量(超越速度)
| 问题 | 合并提示 | 拆分请求 |
|---|---|---|
| 注意力稀释 | 无关的上下文可能降低答案质量(“中途失焦”)。 | 完全集中在单一问题上。 |
| 格式错误 | 更容易出现编号/遗漏错误。 | 输出独立 → 格式更整洁。 |
| 错误传播 | Q2 的错误可能影响 Q3‑Q5(自回归惯性)。 | 错误仅局限于出错的请求。 |
当合并仍可能合理时
| 情况 | 原因 |
|---|---|
| 隐藏的相关性 | 如果问题之间有关联(例如,同一报告的不同部分),共享上下文可以提升一致性。 |
| 严格的 API 速率限制 | 如果只能每分钟调用 3 次,可能需要合并请求。 |
| 网络延迟占主导 | 非常高的往返延迟(例如 > 2 秒)可能导致 5 次单独调用比一次合并调用更慢。现代 API 通常在 100‑300 毫秒之间,这种情况很少见。 |
| 极短的答案 | 当每个答案只有一两个词时,前置填充的开销占主导;一次请求可以减少冗余的前置填充。 |
快速经验基准(异步 Python)
import asyncio
import time
import aiohttp
API_URL = "https://api.your-llm.com/v1/completions"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
async def ask_single(session, prompt):
start = time.time()
async with session.post(
API_URL,
json={"model": "gpt-4o-mini", "prompt": prompt, "max_tokens": 300},
headers=HEADERS,
) as resp:
await resp.json() # ignore content, just wait for response
return time.time() - start
async def benchmark():
questions = [
"Question 1: …",
"Question 2: …",
"Question 3: …",
"Question 4: …",
"Question 5: …",
]
async with aiohttp.ClientSession() as session:
# ---- Approach A: Combined -------------------------------------------------
combined_prompt = "Please answer each question separately:\n" + "\n".join(questions)
t_combined = await ask_single(session, combined_prompt)
# ---- Approach B: Parallel -------------------------------------------------
tasks = [ask_single(session, q) for q in questions]
t_parallel = max(await asyncio.gather(*tasks))
print(f"Combined request latency : {t_combined:.2f}s")
print(f"Parallel requests latency: {t_parallel:.2f}s")
print(f"Speed‑up factor : {t_combined / t_parallel:.2f}×")
if __name__ == "__main__":
asyncio.run(benchmark())
多次运行脚本后,你通常会看到并行版本在处理独立的中等长度答案时快约 4‑5 倍。
TL;DR
- 速度: 5 个并行请求 ≈ 比单个合并请求快 5 倍(前提是服务能够批处理它们)。
- 质量: 并行请求让模型的注意力保持集中,避免跨问题的相互干扰。
- 例外情况: 仅在问题真正相互依赖、受到严格速率限制,或网络延迟远大于生成时间时才考虑合并。
结论: 当问题相互无关时,发起 独立的并发请求。 🚀
并行 vs. 合并请求
# Parallel execution
start = time.time()
await asyncio.gather(*[ask_single(session, q) for q in questions])
time_parallel = time.time() - start
print(f"Combined: {time_combined:.2f}s")
print(f"Parallel: {time_parallel:.2f}s")
print(f"Speedup: {time_combined / time_parallel:.1f}x")
在实际使用中,5 个中等复杂度的独立问题 通常能够通过并行请求获得 3–5 倍的加速。
对比
| 维度 | 合并请求 | 拆分并行请求 |
|---|---|---|
| 生成速度 | 慢(所有答案顺序输出) | 快(并行生成,延迟等于最慢的) |
| GPU 利用率 | 低(单序列推理) | 高(批量并行推理) |
| 答案质量 | 可能下降(注意力稀释) | 更好(上下文隔离) |
| API 调用次数 | 1 | N(每个问题一次) |
| 最佳适用场景 | 受速率限制 / 极短答案 | 需要详细答案的独立问题 |
核心原则(一句话)
LLM 的自回归机制意味着输出是顺序的;合并请求会将所有输出强制为单一的串行流,而拆分请求则利用服务器端的并行性同时生成多个输出——这是一种使用更多并发槽位(空间)来换取时间的经典权衡。