静态批处理(Static Batching)

静态批处理(Static Batching)
Chase Woo在调用本地昇腾部署的大模型时,观察到一个很有意思的现象:在执行高并发问答时,所有的回答都趋于同一个短暂的时间窗口完成,即使有不同的首字延迟时间。
1 | 2025-10-10 11:04:08 [INFO] LLM图像分析455成功响应,用时 86.08秒 | 首字延迟: 63.554秒 | 输入tokens: 872 | 输出tokens: 8 | 总tokens: 880 | tokens/秒: 0.09 |
在我的实践中,本地部署的服务可能由于某些原因退回到了**静态批处理:**因为张量矩阵必须对齐,早生成的请求会一直输出 Padding(填充)字符,直到最长的那个请求生成完毕,这会导致“快的等慢的”。
1 | 时间轴: |
虽然首字返回时间不同,但服务器会:
- 将同时到达的请求组成一个批次
- 在GPU上并行处理这个批次
- 批次内的请求会相互等待,直到整批完成
总时间 = 首字延迟 + 生成时间,这也就意味着部分请求可能已经很早就完成了,但是需要等待其他请求。
服务器调度策略可能使用了公平调度或批次同步机制:
- 保证同一批次的请求不会因为完成早就立即返回
- 而是等待批次内所有请求都处理完
- 这样可以优化GPU利用率和简化调度逻辑
为什么不是”快的先完成”?
虽然是异步流式响应,但服务器端的批处理机制决定了:
- 即使客户端异步接收
- 服务器端仍然是批量同步推理
- 这导致同一批次的请求几乎同时完成
通过以下代码测试了一下
1 | # 在循环开始时记录每个chunk到达的时间 |
1. Chunk 完全交错到达(最重要的证据)
看第一批请求(115-128)的chunk顺序:
1 | 时间戳 1760063687.956-958: |
这说明服务器并非逐个处理请求,而是:
- 在GPU批次中并行生成所有请求的token
- 每生成一轮,就轮流返回每个请求的chunk
- 类似”时间片轮转”的调度方式
2.首字延迟差异巨大,但总时间趋同
| 请求ID | 首字延迟 | 总时间 | 首字后耗时 |
|---|---|---|---|
| 129 | 0.913秒 | 5.70秒 | 4.79秒 |
| 131 | 3.676秒 | 5.68秒 | 2.00秒 |
| 140 | 5.396秒 | 5.60秒 | 0.20秒 |
| 142 | 5.374秒 | 5.58秒 | 0.21秒 |
公式:首字延迟 + 首字后耗时 ≈ 恒定值(~5.6秒)
3. 批处理的三个阶段
阶段1:等待队列调度(首字延迟)
1 | 1760063688.79秒 ← 请求129, 135先进入 |
阶段2:并行推理(chunk交错返回)
1 | 1760063693.46-52秒: 所有16个请求的chunk交错返回 |
每轮间隔约50毫秒,说明GPU批次推理速度很快
阶段3:同步完成
1 | 所有请求在 10:34:53 的同一秒内完成 |
服务器采用了 静态批处理 + 同步返回策略
1 | # 伪代码:服务器端推理逻辑 |
这样设计的优势
- GPU利用率最大化:所有请求并行推理,充分利用GPU并行能力
- 吞吐量最优:批处理比逐个处理快得多
- 调度简化:批次内同步,避免频繁的上下文切换
代价是快的请求需要等待慢的请求,所以即使首字延迟只有0.9秒,也要等到5.7秒才能完成。
总结
观察到的现象:首字快的应该先完成,但实际没有。原因:服务器端批处理策略强制同步了所有请求,为了最大化吞吐量,牺牲了单个请求的延迟优化。这是典型的**吞吐量优先(Throughput-oriented)而非延迟优先(Latency-oriented)**的设计。
在高并发场景下,虽然使用了流式输出和异步请求,但由于算力瓶颈(特别是 Prefill 抢占)以及调度策略更偏向于吞吐量(允许新请求打断老请求的生成节奏),最终导致用户端的体验是“早连接的请求被拖慢,最终大家一起完成”。
然而,这种策略现在看来已经不适用于现在的计算设备了,vLLM 等框架之所以能称霸,恰恰是因为它们打破了这种等待。它是在 Token 级别(迭代级别) 进行调度的。只要某个请求生成了结束符(EOS),它会立刻被踢出当前的 Batch,并将结果返回给客户端,然后引擎会立刻把队列里的新请求塞进这个空出来的 Batch 槽位中。这就引申出了一个新的概念:连续批处理(Continuous Batching)。







