当你有 5 个互不相关的问题,是打包成一条消息发给 LLM,还是同时发 5 个请求?哪种更快?
先说结论
拆成多个独立请求并行发送,几乎总是更快。
这不是直觉判断,而是由 LLM 的底层推理机制决定的。下面从原理层面逐步论证。
一、LLM 生成文本的基本机制:自回归
要理解这个问题,必须先搞清楚 LLM 是怎么”写字”的。
LLM(如 GPT-4、Claude)采用自回归生成(autoregressive generation):每次只生成一个 token,然后把这个 token 拼回输入,再生成下一个 token。循环往复,直到生成结束。
关键点:生成 N 个 token,就需要 N 次前向推理(forward pass)。
这意味着:
– 输出 100 个 token 的回答,需要 100 次推理步骤
– 输出 500 个 token 的回答,需要 500 次推理步骤
– 总输出长度直接决定了总耗时
二、合并请求:总输出量叠加,延迟线性增长
假设你有 5 个独立问题,每个问题的回答大约 200 tokens。
方案 A:合并成一个请求
你把 5 个问题塞进一条消息:
请分别回答以下问题:
1. xxx
2. xxx
3. xxx
4. xxx
5. xxx
LLM 需要生成的总输出 ≈ 5 × 200 = 1000 tokens。由于自回归机制,这 1000 个 token 是串行生成的——第 201 个 token 必须等前 200 个生成完才能开始。
总耗时 ≈ 1000 × 单 token 生成时间
而且还有额外开销:
– LLM 需要在回答之间维护上下文切换(”现在回答第 3 题”)
– 更长的 KV Cache 导致每一步 attention 计算量递增
– 实际输出往往超过 1000 tokens(格式化、过渡语句等)
三、拆分请求:并行推理,耗时取决于最慢的那个
方案 B:5 个问题拆成 5 个独立请求,同时发送
每个请求独立生成 ~200 tokens。如果服务端有足够的并发处理能力(现代 LLM 服务都有),这 5 个请求会被并行处理。
总耗时 ≈ max(各请求耗时) ≈ 200 × 单 token 生成时间
对比一下:
| 方案 | 总输出 tokens | 实际耗时(相对值) |
|---|---|---|
| 合并一个请求 | ~1000+ | ~1000 步(串行) |
| 拆分 5 个请求 | 每个 ~200 | ~200 步(并行) |
理论加速比 ≈ 5x(等于问题数量)。
四、为什么并行能成立?——服务端的 Continuous Batching
你可能会问:LLM 服务器不是也有容量限制吗?5 个请求同时来,不会排队吗?
现代 LLM 推理引擎(vLLM、TensorRT-LLM、TGI 等)都实现了 Continuous Batching(连续批处理):
- 多个请求共享同一次 GPU 矩阵运算:GPU 擅长的就是并行计算。把 5 个请求的 token 拼成一个 batch,一次 forward pass 就能同时为 5 个请求各生成一个 token。
- 动态调度:不同请求的输出长度不同,短的先结束,空出的 slot 立刻给新请求用。
- 吞吐量 vs 延迟的解耦:batch 越大,GPU 利用率越高,单位时间处理的 token 总量越多。
所以从服务端视角看:
– 5 个短请求并行 → GPU 做 5 路 batch 推理,每步同时产出 5 个 token
– 1 个长请求 → GPU 做 1 路推理,每步只产出 1 个 token
GPU 的并行计算能力在合并请求时被浪费了。
五、Prefill 阶段的差异
LLM 推理分两个阶段:
- Prefill(预填充):处理输入 prompt,计算所有输入 token 的 KV Cache。这一步可以并行处理所有输入 token,耗时与输入长度近似线性。
- Decode(解码):逐 token 生成输出。这一步是串行的。
合并请求时:
– Prefill 阶段:输入更长(5 个问题的描述拼在一起),prefill 时间更长
– Decode 阶段:输出更长,decode 时间更长
拆分请求时:
– 每个请求的 prefill 更短,且 5 个 prefill 可以并行或 pipeline 执行
– 每个请求的 decode 更短,且并行进行
两个阶段都是拆分更优。
六、还有一个容易忽略的因素:质量
除了速度,合并请求还有质量风险:
- 注意力稀释:LLM 在一次生成中处理多个不相关任务时,对每个任务的”专注度”下降。研究表明,prompt 中无关信息越多,回答质量越低(Lost in the Middle 现象)。
- 格式混乱:5 个问题的回答容易出现编号错乱、遗漏、答非所问。
- 错误传播:如果第 2 个问题的回答出了问题,LLM 可能在后续回答中受到干扰(自回归的”惯性”)。
拆分请求则完全隔离了上下文,每个问题都能获得 LLM 的”全部注意力”。
七、什么时候合并反而更好?
公平起见,有少数场景合并可能更合适:
- 问题之间有隐含关联:虽然你认为独立,但 LLM 如果看到全貌可能给出更一致的回答(比如同一份报告的不同章节)。
- API 调用有严格的 rate limit:如果你的 API 配额是每分钟 3 次请求,那 5 个问题只能合并。
- 网络延迟远大于生成时间:如果每次 API 调用的网络往返是 2 秒,而生成只要 0.5 秒,那拆分 5 次的网络开销(5 × 2s = 10s)可能超过合并的生成时间。但这种情况在实际中很少见——现代 API 的网络延迟通常在 100-300ms,远小于生成时间。
- 极短回答:如果每个问题只需要一两个词的回答,那 prefill 的开销占比更大,合并可以减少重复的 prefill 成本。
八、实际验证思路
如果你想自己验证,可以这样测:
import asyncio
import time
import aiohttp
async def ask_single(session, question):
start = time.time()
# 调用 LLM API
resp = await session.post(API_URL, json={"prompt": question})
result = await resp.json()
return time.time() - start
async def benchmark():
questions = ["问题1", "问题2", "问题3", "问题4", "问题5"]
async with aiohttp.ClientSession() as session:
# 方案 A:合并
start = time.time()
combined = "请分别回答:\n" + "\n".join(questions)
await ask_single(session, combined)
time_combined = time.time() - start
# 方案 B:并行
start = time.time()
await asyncio.gather(*[ask_single(session, q) for q in questions])
time_parallel = time.time() - start
print(f"合并: {time_combined:.2f}s")
print(f"并行: {time_parallel:.2f}s")
print(f"加速比: {time_combined / time_parallel:.1f}x")
根据经验,5 个中等复杂度的独立问题,并行通常能获得 3-5x 的加速。
总结
| 维度 | 合并一个请求 | 拆分多个并行请求 |
|---|---|---|
| 生成速度 | 慢(串行输出所有答案) | 快(并行生成,取最慢值) |
| GPU 利用率 | 低(单序列推理) | 高(batch 并行推理) |
| 回答质量 | 可能下降(注意力稀释) | 更好(独立上下文) |
| API 调用数 | 1 次 | N 次 |
| 适用场景 | 有 rate limit / 问题极短 | 问题独立且需要详细回答 |
核心原理一句话总结:LLM 的自回归机制决定了输出是串行的,合并请求 = 强制串行所有输出;拆分请求 = 利用服务端并行能力同时生成多个输出。独立问题拆开发,是用空间(并发 slot)换时间的经典策略。
发表回复