在Jetson Nano/Orin上实现高效C++视觉推理,必须全链路CUDA化:绕开CPU预处理、复用GPU显存、隔离I/O、使用cudaStream流水线、显存池管理及CUDA后处理,否则无法压低延迟、稳住帧率、扛住内存压力。

直接说结论:在 Jetson Nano / Orin 上跑 C++ 视觉推理流水线,**不能靠“把 Python 代码改写成 C++”来实现高效**;真正压低延迟、稳住帧率、扛住内存压力的关键,在于绕开 CPU 路径、复用 GPU 显存、隔离 I/O 阶段,并把预处理/推理/后处理全链路 CUDA 化。否则你写的 cv::resize + cv::cvtColor 就已经卡死在 CPU 瓶颈里了。
为什么 cv::Mat 在 Jetson 上是性能杀手
OpenCV 的 CPU 实现(尤其是 cv::resize、cv::normalize)在 ARM Cortex-A78AE 或 A72 上吞吐极低,且会触发频繁的 malloc/free 和 CPU↔GPU 数据拷贝。实测一张 1080p 图像走 OpenCV CPU 预处理平均耗时 42ms(Orin Nano),而等效 CUDA kernel 只需 2.3ms。
- 所有图像预处理必须用自研或封装好的 CUDA kernel,比如
cuda_resize_bilinear、cuda_yuv2bgr,输入输出都保持在 GPU 显存中 - 避免
cv::Mat构造/析构——它默认分配 CPU 内存;改用cudaMalloc+cv::cuda::GpuMat(注意:JetPack 6.0 中cv::cuda::GpuMat默认使用 Unified Memory,要显式绑定到 GPU 页帧) - 不要用
cv::dnn::Net::setInput()直接传cv::Mat;它内部会触发隐式 CPU→GPU 拷贝,改用cv::cuda::GpuMat::download()或直接绑定void*指针到 TensorRTIExecutionContext::setBindingDimensions()
TensorRT 推理必须启用 ZeroCopyRun + 内存池
默认的 context->executeV2() 是同步阻塞调用,且每次推理都重新分配中间 tensor 显存。在 30FPS 场景下,这会造成大量显存碎片和上下文切换开销。
- 务必开启
context->setOptimizationProfile(0)并调用context->setBindingDimensions()预设 shape,否则动态 shape 会强制降级为慢速路径 - 启用
ZeroCopyRun(Paddle Inference)或等效的enqueueV3()+cudaEventRecord()同步机制,让 GPU 异步执行,CPU 去干别的事 - 手写显存池管理:对固定 batch size 的场景(如单图推理),提前
cudaMalloc一块大 buffer,用 offset + size 切分给 input/output tensor,避免反复申请释放 - 特别注意 JetPack 6.0 + TensorRT 10.3 中
ICudaEngine::createExecutionContextWithoutDeviceMemory()必须配合手动cudaMalloc分配getDeviceMemorySize()大小的 buffer,否则enqueueV3会 segfault
生产者-消费者流水线不能只靠 std::thread
单纯起几个 std::thread 跑 load → preprocess → infer → postprocess,在线程竞争、锁等待、cache line false sharing 下,实际吞吐可能比单线程还差。
- 用
cudaStream_t划分阶段:一个 stream 专管图像采集(V4L2 / MIPI CSI),一个 stream 专管预处理,一个 stream 专管 TensorRT 推理,stream 间用cudaEventRecord()/cudaStreamWaitEvent()同步 - 避免
std::queue存 raw image:它会引发 heap 分配;改用环形 buffer(std::array<:unique_ptr>, 8></:unique_ptr>)+ 原子索引(std::atomic<int></int>)管理 slot - 后处理(如 NMS)也必须 CUDA 化:
cuda_nms比cv::dnn::NMSBoxes快 17×,且不依赖 CPU 内存;参考trt_common/nms.cuh中的 block-scan 实现 - 如果接入 ROS 2,别用
rclcpp::Node::create_subscription直接绑回调;用rmw_wait_set_t+ 自定义 event loop,防止 DDS 线程抢占 GPU stream 优先级
模型部署前必须做三件事:校准、剪枝、shape 固化
ONNX 导出时保留 dynamic axes?在嵌入式上等于自废武功。TensorRT 对动态 shape 的支持在 JetPack 6.0 中仍不成熟,尤其涉及 Resize、Concat 等算子时极易 fallback 到 CPU。
- 导出 ONNX 时强制指定
dynamic_axes={},用torch.onnx.export(..., opset_version=17),避免Unsqueeze插入无意义维度 - INT8 校准必须用真实边缘数据:Jetson 上的摄像头 ISP 输出存在 Bayer pattern、AWB 偏移、gamma 曲线,拿合成图校准会导致
quantization error爆表 - 对 YOLO 类模型,删掉 Neck 中冗余的
Conv层(如 YOLOv8 的 C2f 最后一个分支)、合并连续SiLU+Conv,能减少 12%~18% 的 kernel launch 开销 - 检查
trtexec --onnx=model.onnx --verbose输出中是否有[WARNING] No implementation of layer xxx available—— 这类层会被 TensorRT 丢给cuBLASLt或 CPU fallback,必须重写或替换
最常被忽略的一点:Jetson 的 LPDDR5 是 CPU/GPU 共享内存,但访问带宽不对称。GPU 读显存快,写显存慢;CPU 写内存快,读慢。所以 preprocess → GPU memory 要用 pinned memory + cudaMemcpyAsync,而 postprocess → CPU memory 必须用 cudaMemcpyDefault + cudaStreamSynchronize(),否则你会看到 GPU 利用率 99%、CPU 利用率 5%、整体帧率卡在 8FPS 的诡异现象。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











