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张的请求得排队。吞吐量上不去。

动态批处理就是为了解决这类问题。它的核心价值有三个:

  1. 降低延迟:请求少时用小batch,不用等。用户少的时候,我见过延迟从500ms降到50ms。
  2. 提高吞吐:请求多时用大batch,充分利用GPU并行能力。GPU这玩意儿,你喂的数据越多,它越开心。
  3. 节省显存:你想想看,如果固定最大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++代码里配置、怎么在业务层做请求合并。到时候你就知道,这东西用起来其实没那么复杂。