2、动态批处理概念:什么是动态批处理、为什么需要动态批处理、静态批处理 vs 动态批处理
什么是动态批处理?
动态批处理,说白了就是让模型在推理时,能灵活处理不同数量的输入。
我举个例子你就明白了。假设你开了一家快递站,每天来寄件的人数不一样。有时候来10个人,有时候来100个人。静态批处理就像是你规定「必须凑够50个人才发车」,而动态批处理则是「来几个人就发几辆车,人少用小巴,人多用大巴」。
在TensorRT里,动态批处理允许你在运行时指定batch size。你不需要在编译模型时就定死一个数字。比如:
// 编译时设置动态范围
builder->setMaxBatchSize(32); // 最大支持32
// 推理时灵活指定
context->enqueue(1, buffers, stream, nullptr); // batch=1
context->enqueue(8, buffers, stream, nullptr); // batch=8
context->enqueue(32, buffers, stream, nullptr); // batch=32
嗯,这里要注意:动态批处理不是「自动帮你合并请求」。它只是允许你传入不同大小的batch。合并请求这件事,还得你自己在业务层做。
为什么需要动态批处理?
我在项目中遇到过这样一个场景:一个在线图片识别服务,用户上传图片的时间完全随机。有时候一秒来1张,有时候一秒来100张。
如果我用静态批处理,比如固定batch=16,会出现两个问题:
- 用户少的时候,我得等凑够16张才处理。用户等得花儿都谢了。
- 用户多的时候,超过16张的请求得排队。吞吐量上不去。
动态批处理就是为了解决这类问题。它的核心价值有三个:
- 降低延迟:请求少时用小batch,不用等。用户少的时候,我见过延迟从500ms降到50ms。
- 提高吞吐:请求多时用大batch,充分利用GPU并行能力。GPU这玩意儿,你喂的数据越多,它越开心。
- 节省显存:你想想看,如果固定最大batch=64,就得一直占着64份的显存。动态的话,实际用多少占多少。
核心观点:动态批处理不是性能银弹,它是「延迟」和「吞吐」之间的一个调节旋钮。你可以在不同负载下,找到那个最舒服的平衡点。
静态批处理 vs 动态批处理
咱们直接上对比表,这样最清楚:
| 对比维度 | 静态批处理 | 动态批处理 |
|---|---|---|
| batch size | 编译时固定 | 运行时可变 |
| 显存占用 | 按最大batch分配 | 按实际batch分配 |
| 推理延迟 | 低负载时偏高 | 低负载时更低 |
| 吞吐量 | 高负载时稳定 | 高负载时灵活 |
| 优化程度 | 编译器可做极致优化 | 优化空间略小 |
| 适用场景 | 负载稳定的离线批处理 | 负载波动的在线服务 |
我个人习惯是这么选的:
- 如果是离线任务,比如晚上批量处理100万张图片,我直接用静态batch=64。省心,性能也最好。
- 如果是在线API,用户请求忽多忽少,我必用动态批处理。配合一个动态batching调度器,效果非常好。
一个小技巧:动态批处理并不意味着每次推理都要重新分配显存。TensorRT内部会预分配一个最大容量的显存池。你实际用的batch只要不超过最大值,就不会有额外开销。
我曾经踩过的坑:动态批处理模式下,如果你的模型里有reshape或者squeeze操作,一定要小心。这些操作对输入形状很敏感。我有一回就是因为没处理好动态shape,结果推理出来的结果全是乱的。排查了整整一个下午。
最后说一句,动态批处理不是TensorRT独有的。PyTorch的TorchScript、ONNX Runtime都支持。但TensorRT的实现更底层,优化得更狠。你想想看,NVIDIA自家出的东西,对自家GPU的调度肯定最了解。
嗯,这一节就到这里。下一节我会带你手写一个动态批处理的完整示例,包括怎么在C++代码里配置、怎么在业务层做请求合并。到时候你就知道,这东西用起来其实没那么复杂。