标签: Cache

  • 多个独立问题,该合并成一个请求还是拆开发?——LLM 并发处理的原理分析

    当你有 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(连续批处理):

    1. 多个请求共享同一次 GPU 矩阵运算:GPU 擅长的就是并行计算。把 5 个请求的 token 拼成一个 batch,一次 forward pass 就能同时为 5 个请求各生成一个 token。
    2. 动态调度:不同请求的输出长度不同,短的先结束,空出的 slot 立刻给新请求用。
    3. 吞吐量 vs 延迟的解耦:batch 越大,GPU 利用率越高,单位时间处理的 token 总量越多。

    所以从服务端视角看:
    – 5 个短请求并行 → GPU 做 5 路 batch 推理,每步同时产出 5 个 token
    – 1 个长请求 → GPU 做 1 路推理,每步只产出 1 个 token

    GPU 的并行计算能力在合并请求时被浪费了。

    五、Prefill 阶段的差异

    LLM 推理分两个阶段:

    1. Prefill(预填充):处理输入 prompt,计算所有输入 token 的 KV Cache。这一步可以并行处理所有输入 token,耗时与输入长度近似线性。
    2. Decode(解码):逐 token 生成输出。这一步是串行的。

    合并请求时:
    – Prefill 阶段:输入更长(5 个问题的描述拼在一起),prefill 时间更长
    – Decode 阶段:输出更长,decode 时间更长

    拆分请求时:
    – 每个请求的 prefill 更短,且 5 个 prefill 可以并行或 pipeline 执行
    – 每个请求的 decode 更短,且并行进行

    两个阶段都是拆分更优。

    六、还有一个容易忽略的因素:质量

    除了速度,合并请求还有质量风险:

    • 注意力稀释:LLM 在一次生成中处理多个不相关任务时,对每个任务的”专注度”下降。研究表明,prompt 中无关信息越多,回答质量越低(Lost in the Middle 现象)。
    • 格式混乱:5 个问题的回答容易出现编号错乱、遗漏、答非所问。
    • 错误传播:如果第 2 个问题的回答出了问题,LLM 可能在后续回答中受到干扰(自回归的”惯性”)。

    拆分请求则完全隔离了上下文,每个问题都能获得 LLM 的”全部注意力”。

    七、什么时候合并反而更好?

    公平起见,有少数场景合并可能更合适:

    1. 问题之间有隐含关联:虽然你认为独立,但 LLM 如果看到全貌可能给出更一致的回答(比如同一份报告的不同章节)。
    2. API 调用有严格的 rate limit:如果你的 API 配额是每分钟 3 次请求,那 5 个问题只能合并。
    3. 网络延迟远大于生成时间:如果每次 API 调用的网络往返是 2 秒,而生成只要 0.5 秒,那拆分 5 次的网络开销(5 × 2s = 10s)可能超过合并的生成时间。但这种情况在实际中很少见——现代 API 的网络延迟通常在 100-300ms,远小于生成时间。
    4. 极短回答:如果每个问题只需要一两个词的回答,那 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)换时间的经典策略。