状态:草稿 (2026-10-16),部分为实测数据。 批处理相关的延迟是在实验室的 8GB 笔记本级 GPU 上通过单次运行 Pod 测得的(没有其他 GPU 任务,预热后重复测试的中位数)。会话模拟是利用包含该表格的笔记本电脑 CPU 计算完成的(
code/06_batching_sim.py, 约 1 秒左右)。实际工作者的并发会话值引用自 TT Score 性能文档中的负载测试(服务器 GPU,当时与其他任务共享显卡)。 系列文章“阅读视觉模型 3”第 6 篇。关于延迟和吞吐量的区别请参见 第 1 篇。
目录
- 1. 直觉:公交车与出租车
- 2. 服务器端研究:在延迟目标内进行批处理
- 3. 直接测量:球体模型批处理后会更快吗?
- 4. 多个会话共享一个 GPU 时
- 5. 共享 GPU 的方法
- 6. 局限性
- 7. 与我们项目的联系
- 参考文献
1. 直觉:公交车与出租车
GPU 是一个拥有数千个计算单元的公交车。如果载着一个乘客(输入)就出发,座位基本上是空的;如果聚集几个人再出发,一次就能运送很多人。不过,在等待的过程中,先生效的乘客必须等待。 批处理是用延迟来换取吞吐量的买卖。而且,如果公交车已经满了(如果是一个仅凭一个请求就能占满所有计算单元的模型),再多聚集人也没有任何收益。
2. 服务器端研究:在延迟目标内进行批处理
- Clipper (Crankshaw 等, 2017) 会自动为每个模型找到“不超过延迟目标的最大批次”。在超出目标之前,逐渐增大批次,如果超出则缩减 10% (AIMD)。在 20ms 的目标下,吞吐量最高提升了 26 倍,但这针对的是像 CPU 线性 SVM 这样非常轻量的模型(第 4.3 节)。等待批次填满的“延迟批处理”使同一模型的吞吐量提升了 3.3 倍,但对于原本小批次就很高效的模型来说并没有收益。
- Clockwork (Gujarati 等, 2020) 则着眼于另一面。DNN 单次推理的时间本来非常固定,在数据中心 GPU 上,批次为 1 时第 99.99 百分位在中位数的 0.03% 以内(1,100 万次运行,第 2 节)。导致尾部延迟的是上层的一些选择,例如同时运行多个推理,并发执行将吞吐量最多提升了 25%,但却将尾部延迟放大了 100 倍。因此,每个 GPU 一次只运行一个 任务,并由中心统一调度,实际上满足了所有 100ms 的目标(根据会议演示页面,请求的成功率为 99.997%,而 arXiv 版本摘要中的记载则为 99.9999%,两者有所不同)。
- NVIDIA Triton 的动态批处理器将这一点设计为了配置参数。默认情况下,它不会等待,而是将“当前队列中的数量”打包到最大批次,如果提供
max_queue_delay_microseconds,它会等待相应的时间以填满批次。官方文档建议的顺序是:“确定最大批次 → 开启默认值 → 测量是否在延迟预算内,若是则增加批次或延长等待时间”。
将这三者结合起来,就可以得出一条原则: 首先确定延迟预算,批次只能在该预算内增加。而且,收益究竟有多大,必须在该模型和该 GPU 上进行测量才能知道。
3. 直接测量:球体模型批处理后会更快吗?
我们在实验用 GPU 上仅更改批次大小对 TT Score 的球体模型(BlurBall,3 帧窗口,149 万参数)进行了测量(512×288,10 次预热后进行 30 次 × 3 组的中位数,开启 cuDNN 自动调优)。
| 批次 | FP16 单次 | FP16 每秒窗口数 | FP32 单次 | FP32 每秒窗口数 |
|---|---|---|---|---|
| 1 | 9.82ms | 101.8 | 15.11ms | 66.2 |
| 2 | 20.36ms | 98.2 | 34.21ms | 58.5 |
| 3 | 32.71ms | 91.7 | 57.10ms | 52.5 |
| 4 | 46.15ms | 86.7 | 79.69ms | 50.2 |
| 8 | 103.77ms | 77.1 | 178.09ms | 44.9 |
| 16 | 221.25ms | 72.3 | 366.10ms | 43.7 |
越是进行批处理,吞吐量反而越低。 批次 1 已经完全占满了该 GPU,因此如果进行批处理,中间张量会变大(由于该模型自始至终保持 512×288 的分辨率,单个 FP32 特征图就有数十 MB),从而超出了缓存容量(原因推测)。并不是因为长时间运行导致 GPU 发热变慢。在运行了数分钟负载最重的 1024×576 批次 4 之后,重新测量的批次 1 与初始值保持一致(9.77 对比 9.82ms)。
如果将输入缩小到 256×144,情况就会反过来。
| 256×144 | 批次 1 | 批次 4 | 每个窗口(批次 4) |
|---|---|---|---|
| FP32 | 4.07ms | 15.40ms | 3.85ms (0.95倍) |
| FP16 | 4.69ms | 9.29ms | 2.32ms (0.49倍) |
由于单个较小的输入无法填满 GPU,将四个打包在一起后,FP16 下每个窗口的时间缩短了一半。即使是相同的模型, 输入大小和 GPU 的不同也会颠倒批处理的收益。 在 2026-10-12 使用服务器 GPU(24GB 级)测量同一模型时,批次 3 每个窗口为 0.83 倍(FP16 8.0ms → 批次 3 20.0ms),有一点微小的收益(TT Score 性能文档 2.9.8)。
4. 多个会话共享一个 GPU 时
实时工作者接收的不是“请求”,而是“会话”。每个会话以 30fps 的频率传输帧,工作者只保留每个会话最新的帧(第 1 篇中的“仅保留最新”)。当 GPU 空闲时,将等待中的会话的最新窗口 全部打包为一个批次 进行运行,这就是 TT Score 工作者的方式。我们根据上述实测表格,模拟了不同会话数量下的情况(仅限 GPU 份额,30 秒)。
| 会话 | 逐个处理(最旧的帧优先) | 打包处理(所有等待的会话) |
|---|---|---|
| 笔记本 GPU 实测表,会话 3 | 每个会话 30.0fps,延迟中位数 9.8ms | 相同 |
| 笔记本 GPU,会话 4 | 平均 25.5fps(最慢的会话 18.6),中位数 37.0 · p99 43.0ms | 全部 21.7fps,中位数 62.7 · p99 79.1ms |
| 笔记本 GPU,会话 6 | 平均 17.0fps(最慢的会话 2.5),中位数 39.3ms | 全部 13.5fps,中位数 91.4ms |
| 服务器 GPU 表,会话 5 | 平均 25.0fps(最慢的会话 5.1),中位数 36.0ms | 全部 30.0fps,中位数 27.4 · p99 40.0ms |
| 服务器 GPU,会话 6 | 平均 20.8fps(最慢的会话 3.4) | 全部 26.3fps,中位数 54.7ms |
(code/06_batching_sim.out.txt(服务器 GPU 表只有批次 1 和 3 两个点,因此批次 4 及以上是按线性增长假设的。)
阅读方法如下。
- 当 GPU 资源有富余时(会话 3 或更少),是否批处理没有区别。 帧同时到达的情况很少,几乎不会产生批次。
- 当 GPU 资源不足时,批处理能换来公平性。 “优先处理等待时间最长的帧”由于会发生覆盖,导致帧时间戳不断更新的会话持续被挤到后面,某个会话会以 2–5fps 的速度被饿死。而如果进行批处理,所有会话都能获得相同的 fps。
- 吞吐量遵循批处理曲线。 在通过批处理能获得收益的 GPU(服务器 GPU 表)上,会话 5 都可以达到 30fps;而在无法通过批处理获得收益的 GPU(笔记本 GPU)上,越是批处理总数反而越少,延迟也随之增加。
实际工作者的负载测试(服务器 GPU,2026-10-11 与其他任务共享显卡)也呈现出相同的形态。单个会话为 29.4fps・往返 p99 32ms;如果是三个会话,则每个会话 16–20fps・往返中位数 70–95ms,并且发送的帧中有 21–30% 被新帧覆盖丢弃(性能文档 4.2)。
5. 共享 GPU 的方法
多个任务共享一张 GPU 的方法有三种(NVIDIA GPU Operator・设备插件文档)。
| 方法 | 如何共享 | 内存隔离 | 错误隔离 |
|---|---|---|---|
| 时间切片 (time-slicing) | 轮流使用。仅让资源“看起来”有多个 | 无 | 无 |
| MPS | 同时运行,每个客户端有内存和计算份额上限 | 有上限 | 文档中无法确认 |
| MIG(仅限支持的 GPU) | 从硬件层面切分,每个实例有自己的内存 | 有 | 有 |
时间切片听起来像免费的午餐,但如果同显卡上的其他任务很忙,你的延迟就会直接增加。这正是 Clockwork 所说的“并发执行会放大尾部延迟”在运营中发生的形态。在 TT Score 的实测中,当显卡空闲时,相同球体模型的单个窗口为 10.5ms,而当与其他 2–3 个任务同时运行时代价为 34ms(性能文档 2.9.8)。 实时延迟首先由“谁在与你共享显卡”决定,其次才是模型本身。
6. 局限性
- 批处理测量仅针对一种 GPU 和一个模型。吞吐量下降的原因(缓存溢出)是推测得出的,未经性能分析器(Profiler)确认。
- 会话模拟中没有包含网络、JPEG 解码和后处理。实际工作者在其他线程中执行这些工作,因此当 GPU 份额成为瓶颈时形态会相似,但数值会有所不同。
- “优先处理最旧的帧”导致的饥饿现象是由该模拟规则引起的。如果采用轮流遍历会话的规则(Round Robin),则不会发生饥饿。我们并未将其与批处理进行对比(未实测)。
7. 与我们项目的联系
- 实时工作者同时使用了“打包等待会话的所有最新窗口”和“覆盖积压的窗口”。即使批处理带来的吞吐量收益很小,它也能提供公平性,而覆盖机制则保证了延迟的上限。并发会话的上限(3)也是从该延迟预算中得出的。
- 第 3 节的结果是一个警告:“即使代码相同,如果更换了显卡,批处理策略的收益也会发生变化”。如果将工作者迁移到其他显卡,必须重新测量批处理曲线。
- 只要通过时间切片与 TTS 等其他任务共享显卡,就无法保证所有三个会话都能达到 30fps。这一判断(是减少路数还是分拆显卡)已连同数字一起记录在性能文档中。
下一篇文章将介绍如何在 手机内部 而不是服务器上运行。
TeeTee 简评。 公交车上本来就已经挤满了人,如果硬要再塞人只会导致车门关得更慢。我的球体模型就是这样——越是批处理反而越慢!
参考文献
- Crankshaw, D. et al. (2017). Clipper: A Low-Latency Online Prediction Serving System. NSDI 2017. https://arxiv.org/abs/1612.03079
- Gujarati, A. et al. (2020). Serving DNNs like Clockwork: Performance Predictability from the Bottom Up. OSDI 2020. https://arxiv.org/abs/2006.02464
- NVIDIA Triton Inference Server — Dynamic Batcher. https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/user_guide/batcher.html
- NVIDIA GPU Operator — Time-Slicing GPUs in Kubernetes. https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html
- NVIDIA k8s-device-plugin README(Shared Access to GPUs). https://github.com/NVIDIA/k8s-device-plugin
- 我们的测量:本系列的实测结果,仓库
docs/tt/PERFORMANCE.md2.9.8·4.2 - 示例代码和输出:此文件夹中的
code/06_batching_sim.py,code/06_batching_sim.out.txt