ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FairMOT TensorRT部署实战:从ONNX导出到MOT跟踪的C++工程化落地

FairMOT TensorRT部署实战:从ONNX导出到MOT跟踪的C++工程化落地 简介这份资源面向具备一定深度学习与C基础的算法工程师和研究人员聚焦如何借助NVIDIA TensorRT推理平台加速FairMOT多目标跟踪算法的实际部署解决模型在GPU平台上推理延迟高、吞吐不足的问题。压缩包共92个文件约96.35MB以hpp、cpp、h等C头文件与源码为主体配合cu核函数、onnx模型、py导出脚本及sln、vcxproj工程文件覆盖模型导出、插件实现、跟踪匹配与卡尔曼滤波等模块并附有演示视频与说明文档。已有112人学习。读者可从中获得从FairMOT模型转ONNX、TensorRT优化到行人重识别与跟踪落地的完整工程参考理解层融合、精度校准与内核自动调整等优化手段并借鉴推理性能分析与排错思路适合智能监控、视频分析等场景的部署实践。1. 从 PyTorch 到 TensorRTFairMOT 部署到底卡在哪FairMOT 的论文指标很好看MOT16 上 MOTA 跑到 70 以上但真把它塞进项目里跑视频你会发现 PyTorch 原生推理在 1080p 视频上连 15 FPS 都费劲。问题不在模型本身而在推理框架——PyTorch 的动态图机制、频繁的 kernel launch、FP32 全精度计算每一条都在拖后腿。TensorRT 要解决的就是这件事把训练好的网络重新编译成针对特定 GPU 架构优化过的推理引擎层融合、精度校准、内核自动调优三板斧下去同样的 FairMOT 模型推理速度通常能翻 2 到 4 倍。这份资源包的核心价值在于它给出了一条完整的链路从 FairMOT 的 PyTorch 权重导出 ONNX再用 ONNX Parser 把模型吃进 TensorRT中间还带了自定义 plugin 处理 DCNv2 可变形卷积这种 TensorRT 原生不支持的算子最后接上 MOT 跟踪逻辑卡尔曼滤波 匈牙利匹配跑通整段视频。适合两类人一是手里已经有 FairMOT 权重、想把它推到生产环境做实时行人跟踪的工程师二是想学 TensorRT 插件开发和 ONNX 模型改造的算法部署从业者。如果你只是想在 Python 里调个 API 看效果这个包可能偏重了但如果你要的是 C 工程化落地里面的main.cpp、tracker.cpp、onnx_parser目录就是现成的骨架。2. 环境搭建与工程结构先把编译链路跑通2.1 依赖版本锁定与 CUDA/cuDNN 对齐TensorRT 部署最怕的就是版本不对齐。这份工程用的是 Visual Studio 的.sln方案说明作者是在 Windows 上做的开发但代码本身跨平台Linux 下改 CMake 也能编。关键依赖就三个CUDA、cuDNN、TensorRT三者版本必须严格匹配。TensorRT 8.x 对应 CUDA 11.xTensorRT 10.x 对应 CUDA 12.x混搭必翻车。我一般会先确认显卡驱动支持的 CUDA 上限再倒推 TensorRT 版本。比如 RTX 30 系卡驱动 520 可以上 CUDA 11.8 TensorRT 8.6这是目前最稳的组合。GTX 10 系卡要注意TensorRT 10.x 虽然理论上支持 Compute Capability 6.1但部分 plugin 在 Pascal 架构上会有兼容性问题建议降到 TensorRT 8.5 或 8.6。# 确认 CUDA 版本 nvcc --version # 确认 cuDNN 版本Linux cat /usr/local/cuda/include/cudnn_version.h | grep CUDNN_MAJOR -A 2 # 确认 TensorRT 版本 dpkg -l | grep tensorrt # 或 Windows 下查看环境变量 TRT_PATH 指向的目录这三个命令跑完版本号记下来。TensorRT 的libnvinfer.so和 CUDA 的libcudart.so如果版本错位编译期不报错运行期直接 segfault这是血泪经验。2.2 工程目录拆解与编译入口资源包的目录结构信息量很大我按功能模块拆一下目录/文件作用dladcn_export_onnx.pyPyTorch 权重导出 ONNX含 DCNv2 算子处理onnx_parser/ONNX 解析与 TensorRT 网络构建plugin/自定义 TensorRT pluginDCNv2 等src/推理主逻辑、前后处理mot/跟踪模块卡尔曼滤波、匈牙利匹配models/存放 ONNX 和 TensorRT engine 文件MOT16-03.mp4测试视频TensorRT.slnVS 工程入口编译顺序有讲究先编plugin再编onnx_parser最后编主工程。因为 plugin 是动态库主工程链接时依赖它。Windows 下直接在 VS 里右键解决方案→生成即可但要注意TensorRT.vcxproj里的附加包含目录和库目录是否指向你本机的 TensorRT 路径。!-- TensorRT.vcxproj 中需要确认的关键配置 -- PropertyGroup CUDA_PATHC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8/CUDA_PATH TRT_PATHD:\TensorRT-8.6.1.6/TRT_PATH /PropertyGroup Link AdditionalLibraryDirectories $(TRT_PATH)\lib;$(CUDA_PATH)\lib\x64 /AdditionalLibraryDirectories AdditionalDependencies nvinfer.lib;nvinfer_plugin.lib;nvonnxparser.lib;cudart.lib /AdditionalDependencies /Link这段配置是编译能否通过的关键。TRT_PATH指向 TensorRT 解压目录CUDA_PATH指向 CUDA 安装目录。如果链接时报LNK2019: 无法解析的外部符号九成是这里路径写错了或者 Debug/Release 配置选错了——TensorRT 的 lib 分 debug 和 release 版本混用会报错。Linux 下没有现成的 CMakeLists需要自己写一个。核心就是find_package(CUDA)和手动指定 TensorRT 的 include/lib 路径然后target_link_libraries把nvinfer、nvonnxparser、nvinfer_plugin链进去。编译产物是一个可执行文件接受视频路径和 engine 路径作为参数。3. ONNX 导出与 TensorRT 引擎构建模型转换的四个关键决策3.1 FairMOT 的 ONNX 导出与 DCNv2 算子处理FairMOT 的 backbone 是 DLA-34里面用了 DCNv2可变形卷积。PyTorch 导出 ONNX 时DCNv2 默认会变成一个DeformConv节点但 TensorRT 的 ONNX Parser 不认这个算子。所以dladcn_export_onnx.py里做的事情本质上是在导出前把 DCNv2 替换成普通卷积或者用自定义符号注册的方式把 DCNv2 映射成一组基础算子。# dladcn_export_onnx.py 核心逻辑示意 import torch from model import FairMOT model FairMOT(num_classes1, reid_dim128) model.load_state_dict(torch.load(fairmot_dla34.pth)) model.eval() dummy_input torch.randn(1, 3, 1088, 608) torch.onnx.export( model, dummy_input, fairmot.onnx, opset_version11, # 建议 11兼容性最好 input_names[input], output_names[hm, wh, id_feat, reg], dynamic_axes{ input: {0: batch, 2: height, 3: width} } )opset_version选 11 而不是最新的 17是因为 TensorRT 8.x 对 opset 11 的支持最成熟高版本 opset 里的新算子反而容易触发 Parser 报错。dynamic_axes把 batch 和 H/W 设为动态这样同一个 engine 可以处理不同分辨率的输入但代价是 TensorRT 优化时会保守一些。如果实际部署分辨率固定建议去掉动态轴让 TensorRT 做更激进的优化。导出完成后用onnxsim做一次简化把冗余的Identity、Constant节点去掉能减少 Parser 的工作量python -m onnxsim fairmot.onnx fairmot_sim.onnx3.2 TensorRT Builder 配置FP16 与 INT8 的取舍拿到 ONNX 后下一步是用 TensorRT 的 Builder API 构建 engine。核心配置项有三个maxWorkspaceSize、fp16Mode、int8Mode。// builder 配置核心代码 IBuilder* builder createInferBuilder(gLogger); INetworkDefinition* network builder-createNetworkV2( 1U static_castuint32_t(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH) ); // ONNX Parser nvonnxparser::IParser* parser nvonnxparser::createParser(*network, gLogger); parser-parseFromFile(fairmot_sim.onnx, static_castint(ILogger::Severity::kWARNING)); // Builder 配置 IBuilderConfig* config builder-createBuilderConfig(); config-setMaxWorkspaceSize(1 30); // 1GB workspace config-setFlag(BuilderFlag::kFP16); // 开启 FP16 // 如果有校准集可以开 INT8 // config-setFlag(BuilderFlag::kINT8); // IInt8Calibrator* calibrator new EntropyCalibrator(...); // config-setInt8Calibrator(calibrator); ICudaEngine* engine builder-buildEngineWithConfig(*network, *config);maxWorkspaceSize给 1GB 是个经验值。FairMOT 的 DLA-34 backbone 加上 detection head 和 ReID head层数不少workspace 太小会导致某些层无法用最优 kernel推理速度反而下降。FP16 开启后精度损失通常在 0.5% 以内但速度提升接近 2 倍性价比最高。INT8 需要校准集而且 FairMOT 的 ReID 分支对量化比较敏感量化后 ID switch 会明显增加除非你对精度要求不高否则不建议一上来就上 INT8。3.3 自定义 Plugin 的注册与加载DCNv2 如果没在 ONNX 层面替换掉就需要写 TensorRT plugin。资源包的plugin/目录里应该有DCNv2Plugin的实现核心是继承IPluginV2DynamicExt实现enqueue方法。// plugin 注册 class DCNv2PluginCreator : public IPluginCreator { public: const char* getPluginName() const override { return DCNv2; } const char* getPluginVersion() const override { return 1; } IPluginV2* createPlugin(const char* name, const PluginFieldCollection* fc) override { // 从 fc 中读取 offset、mask 等参数 return new DCNv2Plugin(...); } }; // 注册到 TensorRT REGISTER_TENSORRT_PLUGIN(DCNv2PluginCreator);Plugin 编译成动态库后主程序启动时需要dlopen加载或者在链接时静态链进去。如果运行时提示Plugin not found检查两点plugin 库是否在LD_LIBRARY_PATHLinux或PATHWindows里REGISTER_TENSORRT_PLUGIN宏是否真的被编译进了库。3.4 Engine 序列化与反序列化构建 engine 很慢DLA-34 这种规模的模型可能要几分钟。所以构建一次后要序列化到磁盘下次直接反序列化加载。// 序列化 IHostMemory* serializedModel engine-serialize(); std::ofstream p(fairmot.engine, std::ios::binary); p.write(static_castconst char*(serializedModel-data()), serializedModel-size()); // 反序列化 std::ifstream file(fairmot.engine, std::ios::binary); file.seekg(0, std::ios::end); size_t size file.tellg(); file.seekg(0, std::ios::beg); std::vectorchar buffer(size); file.read(buffer.data(), size); IRuntime* runtime createInferRuntime(gLogger); ICudaEngine* engine runtime-deserializeCudaEngine( buffer.data(), size, nullptr);注意engine 是和 GPU 架构绑定的。在 RTX 3090 上构建的 engine拿到 RTX 4090 上反序列化会失败。跨机器部署时要么在目标机器上重新构建要么确保 GPU 架构一致。4. 推理管线与 MOT 跟踪从检测框到稳定 ID4.1 前后处理letterbox 与解码逻辑FairMOT 的输入是 letterbox 后的图像保持长宽比短边缩放到 608长边按比例缩放后 padding 到 1088。推理输出四个张量heatmap中心点热力图、wh宽高、id_featReID 特征、reg偏移量。// 前处理letterbox cv::Mat letterbox(cv::Mat img, int target_h, int target_w) { float scale std::min(target_h / (float)img.rows, target_w / (float)img.cols); int new_h int(img.rows * scale); int new_w int(img.cols * scale); cv::Mat resized; cv::resize(img, resized, cv::Size(new_w, new_h)); cv::Mat padded cv::Mat::zeros(target_h, target_w, CV_8UC3); resized.copyTo(padded(cv::Rect(0, 0, new_w, new_h))); return padded; }后处理的核心是 heatmap 峰值提取。对 heatmap 做 3x3 max pooling保留局部最大值再取 top-100 个峰值作为检测框中心。每个中心点对应的 wh 和 reg 解码出实际框坐标id_feat 用于 ReID 匹配。// heatmap 峰值提取 std::vectorDetection decode(float* hm, float* wh, float* reg, float* id_feat, int hm_h, int hm_w) { std::vectorDetection dets; for (int y 1; y hm_h - 1; y) { for (int x 1; x hm_w - 1; x) { float val hm[y * hm_w x]; if (val 0.3) continue; // 置信度阈值 // 3x3 max pooling 判断 bool is_peak true; for (int dy -1; dy 1; dy) for (int dx -1; dx 1; dx) if (hm[(ydy)*hm_w (xdx)] val) is_peak false; if (!is_peak) continue; // 解码框 float w wh[y * hm_w x]; float h wh[(hm_h y) * hm_w x]; float ox reg[y * hm_w x]; float oy reg[(hm_h y) * hm_w x]; Detection d; d.x1 (x ox - w/2) * 4; // stride4 d.y1 (y oy - h/2) * 4; d.x2 (x ox w/2) * 4; d.y2 (y oy h/2) * 4; d.score val; // id_feat 拷贝... dets.push_back(d); } } return dets; }stride4是因为 FairMOT 的输出特征图是输入的 1/4 大小。置信度阈值 0.3 是经验值调高会漏检调低会误检实际项目里要根据场景微调。4.2 卡尔曼滤波与匈牙利匹配的工程实现跟踪模块在mot/目录下核心是kalmanfilter.cpp和Hungarian.cpp。卡尔曼滤波负责预测轨迹下一帧的位置匈牙利算法负责把当前帧的检测框和已有轨迹做匹配。// 卡尔曼滤波预测与更新 KalmanFilter kf; kf.initiate(measurement); // 用第一帧检测框初始化 // 预测 kf.predict(); // 更新匹配成功后 kf.update(measurement); // 匈牙利匹配代价矩阵构建 std::vectorstd::vectorfloat cost_matrix; for (auto track : tracks) { std::vectorfloat row; for (auto det : detections) { // 代价 1 - IoU ReID 特征距离 float iou_cost 1.0f - IoU(track.bbox, det.bbox); float reid_cost CosineDistance(track.feature, det.feature); row.push_back(0.7f * iou_cost 0.3f * reid_cost); } cost_matrix.push_back(row); } // 匈牙利求解 HungarianAlgorithm hungarian; std::vectorint assignment hungarian.Solve(cost_matrix);代价矩阵的权重分配是关键。0.7 * IoU 0.3 * ReID是 FairMOT 论文里的默认值但实际场景中如果行人外观变化大比如光照突变ReID 权重要调低如果遮挡严重IoU 权重要调低。这个参数没有万能值得根据视频质量调。4.3 推理与跟踪的线程流水线单线程串行跑推理和跟踪GPU 利用率上不去。常见做法是把推理和跟踪拆到两个线程用队列做缓冲。// 推理线程 while (true) { cv::Mat frame capture.read(); if (frame.empty()) break; auto input preprocess(frame); context-executeV2(buffers); auto dets postprocess(outputs); std::lock_guardstd::mutex lock(queue_mutex); det_queue.push(dets); queue_cv.notify_one(); } // 跟踪线程 while (true) { std::unique_lockstd::mutex lock(queue_mutex); queue_cv.wait(lock, []{ return !det_queue.empty(); }); auto dets det_queue.front(); det_queue.pop(); lock.unlock(); tracker.update(dets); tracker.draw(frame); }这个流水线能让 GPU 推理和 CPU 跟踪重叠执行整体帧率提升 30% 左右。但要注意队列长度控制不设上限的话内存会涨。5. 避坑与排查部署 FairMOT 时最容易翻车的五个点5.1 Engine 反序列化失败版本与架构不匹配现象deserializeCudaEngine返回 nullptr日志提示Serialization assertion failed。原因engine 文件是在另一台机器或另一个 TensorRT 版本下构建的。TensorRT 的 engine 格式不跨版本兼容8.5 构建的 engine 在 8.6 上加载会失败。GPU 架构不同也会导致失败。解决在目标机器上用相同 TensorRT 版本重新构建 engine。如果必须跨机器确保 CUDA、cuDNN、TensorRT 版本完全一致且 GPU 计算能力相同。5.2 DCNv2 Plugin 未注册推理时算子缺失现象构建 engine 时报Unsupported ONNX op: DeformConv或者运行时提示Plugin not found。原因ONNX 里的 DCNv2 节点没有被正确替换或映射或者 plugin 动态库没有被加载。解决检查dladcn_export_onnx.py里是否做了算子替换如果走 plugin 路线确认REGISTER_TENSORRT_PLUGIN宏被编译且 plugin 库在运行时能被找到。Linux 下用ldd检查依赖Windows 下用dumpbin /dependents。5.3 跟踪 ID 频繁跳变ReID 特征没对齐现象同一个行人ID 在几帧内反复切换。原因ReID 特征提取时的预处理和训练时不一致比如归一化参数不同、输入尺寸不对。或者代价矩阵里 ReID 权重太低IoU 主导了匹配。解决确认 ReID 分支的输入归一化是(x - 0.485) / 0.229还是x / 255必须和训练时一致。调高 ReID 代价权重到 0.5 以上试试。5.4 显存溢出Workspace 和 Batch 设置过大现象buildEngineWithConfig返回 nullptr日志提示out of memory。原因maxWorkspaceSize设得太大或者动态 batch 的 maxBatchSize 超过了显存容量。解决把 workspace 从 1GB 降到 512MB 试试如果用了动态 batch把 maxBatchSize 从 4 降到 1。FairMOT 单帧推理其实不需要大 batch。5.5 视频推理结果偏移letterbox 逆变换漏了现象画面上画的框位置整体偏移或者框的大小不对。原因前处理做了 letterbox后处理解码出框后没有做逆变换把 padding 和缩放的影响算进去。解决在解码框坐标后减去 padding 偏移再除以缩放比例。注意 letterbox 时如果 padding 在右侧和底部偏移量是(target_w - new_w, target_h - new_h)别搞反了。6. 进阶技巧用 trtexec 验证 engine 性能与精度6.1 trtexec 快速 benchmarkTensorRT 自带trtexec工具不用写代码就能测 engine 的推理延迟和吞吐。# 基本性能测试 trtexec --loadEnginefairmot.engine --shapesinput:1x3x1088x608 \ --iterations100 --avgRuns10 --fp16 # 输出关键指标 # GPU Compute Time: 平均推理耗时 # Throughput: 每秒处理帧数 # Latency: 端到端延迟--shapes指定输入维度必须和构建 engine 时的 profile 一致。--iterations100跑 100 次取平均--avgRuns10每 10 次输出一次滑动平均。如果GPU Compute Time波动很大说明 GPU 被其他进程干扰了关掉浏览器和无关程序再测。6.2 精度对比TensorRT vs PyTorchTensorRT 的 FP16 优化会带来精度损失需要量化评估。最直接的方法是对比同一帧图像在 PyTorch 和 TensorRT 下的输出差异。# PyTorch 输出 with torch.no_grad(): hm_pt, wh_pt, id_pt, reg_pt model(input_tensor) # TensorRT 输出通过 pycuda 或 cuda-python 读取 # 假设已经拿到 hm_trt, wh_trt, id_trt, reg_trt # 计算 heatmap 的 max 绝对误差 hm_diff np.abs(hm_pt.cpu().numpy() - hm_trt).max() print(fHeatmap max diff: {hm_diff:.6f}) # 计算 ReID 特征的余弦相似度 id_sim np.dot(id_pt.cpu().numpy().flatten(), id_trt.flatten()) / \ (np.linalg.norm(id_pt.cpu().numpy()) * np.linalg.norm(id_trt)) print(fReID cosine similarity: {id_sim:.4f})经验阈值heatmap 最大误差小于 0.01ReID 余弦相似度大于 0.99说明 FP16 量化没有破坏模型精度。如果 ReID 相似度掉到 0.95 以下跟踪 ID switch 会明显增加这时候要么回退 FP32要么对 ReID 分支单独做精度保留。6.3 一个具体技巧用 polygraphy 做逐层精度对比polygraphy是 TensorRT 生态里的调试利器可以逐层对比 ONNX 和 TensorRT 的输出差异快速定位是哪一层量化出了问题。# 安装 pip install polygraphy # 逐层对比 polygraphy run fairmot_sim.onnx \ --trt --fp16 \ --onnxrt \ --validate \ --layer-outputs \ --save-onnx fairmot_debug.onnx输出会列出每一层的最大误差误差突然变大的那一层就是问题所在。常见的是 concat 层和 upsample 层在 FP16 下精度损失较大如果这些层对最终结果影响大可以在构建 engine 时用config-setFlag(BuilderFlag::kFP16)之后对特定层调用network-getLayer(i)-setPrecision(DataType::kFLOAT)强制保留 FP32。从那以后我每次部署新模型都强制走一遍trtexecbenchmark polygraphy逐层对比确认精度和速度都达标了再上生产。这套流程帮我省了至少三次返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表