16、深度学习推理优化:模型剪枝与量化、TensorRT动态形状、多流推理、Batch Size调优
各位同学,今天我们来聊聊推理优化。说实话,这块内容是我在Jetson平台上花时间最多的地方。模型训练好了,跑不起来,或者跑起来卡成PPT,那前面所有工作都白费了。
推理优化说白了就两件事:让模型跑得更快,让模型占得更少。在Jetson这种资源受限的嵌入式平台上,这两件事尤其重要。我见过太多人,模型在服务器上跑得飞起,一部署到Jetson上就崩了——显存爆了,或者帧率只有个位数。
今天我会把模型剪枝、量化、TensorRT动态形状、多流推理、Batch Size调优这几个核心话题串起来讲。它们不是孤立的,而是环环相扣的一套组合拳。
16.1 模型剪枝:去掉冗余,轻装上阵
模型剪枝,说白了就是把神经网络里那些「不干活」的神经元或者通道砍掉。你想想看,一个训练好的模型,很多权重其实接近于零,留着它们干嘛?
我个人习惯把剪枝分成两类:结构化剪枝和非结构化剪枝。
- 结构化剪枝:直接砍掉整个通道或卷积核。好处是剪完后模型结构变了,推理引擎能直接受益。我在项目中遇到过,剪掉30%的通道,精度只掉了0.5%,但推理速度提升了近一倍。
- 非结构化剪枝:把单个权重置零。这种方式比较细粒度,但说实话,在Jetson上效果一般——因为硬件对稀疏矩阵的支持有限。
剪枝的流程一般是:训练→剪枝→微调。嗯,这里要注意,剪枝后一定要微调,否则精度会掉得你怀疑人生。
16.2 模型量化:FP16还是INT8?这是个问题
量化,就是把模型从FP32压缩到FP16甚至INT8。在Jetson上,这一步几乎是必做的。我记得第一次在Xavier上跑一个FP32的ResNet-50,显存直接爆了——后来换成INT8,不仅显存降了4倍,速度还快了3倍多。
TensorRT支持两种量化方式:
- FP16量化:精度损失极小,几乎可以忽略。适合对精度要求高的场景。
- INT8量化:需要校准数据集。精度会有一定损失,但速度提升明显。
INT8量化有个关键步骤——校准。你需要准备几百张有代表性的图片,让TensorRT去统计激活值的分布。我曾经因为偷懒,只用了50张图做校准,结果模型在边缘场景下精度崩了。后来老老实实用了500张,问题解决。
核心经验:INT8校准数据集一定要覆盖实际部署场景的分布。比如你做安防监控,就别拿网上随便下载的风景图去校准。
16.3 TensorRT动态形状:应对变尺寸输入
很多场景下,输入图像的尺寸不是固定的。比如目标检测,不同视频帧的分辨率可能不一样。这时候就需要TensorRT的动态形状支持。
动态形状说白了就是告诉TensorRT:我的输入尺寸会变,你帮我准备好应对方案。配置方式如下:
// C++ 示例:配置动态形状
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
// 定义输入张量,指定动态维度
ITensor* input = network->addInput(
"input", DataType::kFLOAT, Dims4{-1, 3, -1, -1});
// 配置优化策略
IOptimizationProfile* profile = builder->createOptimizationProfile();
profile->setDimensions(input, OptProfileSelector::kMIN, Dims4{1, 3, 224, 224});
profile->setDimensions(input, OptProfileSelector::kOPT, Dims4{4, 3, 640, 640});
profile->setDimensions(input, OptProfileSelector::kMAX, Dims4{8, 3, 1280, 1280});
// 构建配置
IConfig* config = builder->createBuilderConfig();
config->addOptimizationProfile(profile);
16.4 多流推理:榨干硬件每一分性能
多流推理,就是同时处理多个推理请求。在Jetson上,这招特别管用。为什么?因为GPU的并行能力很强,单流推理往往喂不饱它。
我做过一个测试:在Jetson Orin上,单流推理一个分类模型,GPU利用率只有40%左右。开了4个流之后,利用率直接飙到95%,吞吐量提升了2.5倍。
实现多流推理,核心是创建多个cudaStream:
// 创建多个CUDA流
cudaStream_t streams[4];
for (int i = 0; i < 4; i++) {
cudaStreamCreate(&streams[i]);
}
// 每个流独立执行推理
for (int i = 0; i < 4; i++) {
context->enqueueV2(buffers, streams[i], nullptr);
}
// 同步等待所有流完成
cudaDeviceSynchronize();
这里有个坑——显存竞争。多个流同时访问显存,如果模型太大,可能会互相干扰。我的做法是:先估算每个流需要的显存,然后乘以流数量,确保总显存不超过硬件上限。
16.5 Batch Size调优:找到那个甜蜜点
Batch Size调优,说白了就是找「吞吐量最大」和「延迟最低」之间的平衡点。在Jetson上,这个平衡点往往比你想象的要小。
我整理了一份常见模型的调优参考:
| 模型 | Jetson Orin (FP16) | Jetson Orin (INT8) | Jetson Xavier (FP16) |
|---|---|---|---|
| ResNet-50 | Batch=8 | Batch=16 | Batch=4 |
| YOLOv8s | Batch=4 | Batch=8 | Batch=2 |
| BERT-base | Batch=2 | Batch=4 | Batch=1 |
调优方法其实很简单:从Batch=1开始,逐步增加,同时监控帧率和显存。当帧率不再明显提升,或者显存接近上限时,那就是你的甜蜜点。
16.6 组合拳:把优化串起来
上面讲的这些技术,单独用效果有限,组合起来才是王炸。我分享一个实际案例:
之前做一个智慧零售项目,需要在Jetson Orin上跑一个目标检测模型。原始模型FP32,单流推理,帧率只有15fps。我做了三步优化:
- 结构化剪枝:砍掉20%的通道,精度几乎不变。
- INT8量化:用500张门店图片做校准,精度损失不到1%。
- 多流推理 + Batch=4:开了3个流,每个流Batch=4。
最终帧率从15fps提升到了120fps,显存占用反而降了30%。嗯,这就是组合拳的威力。
好了,关于推理优化,核心就是这些。记住一个原则:先剪枝,再量化,最后调Batch和多流。顺序别搞反了,否则效果会打折扣。