最近在折腾工业视觉检测项目的时候,我把手里的检测程序从“LabVIEW + CPU推理”整个翻到了“LabVIEW + YOLOv5 + TensorRT”这套组合上,连带多路视频流的并行推理也一起解决了。之前那版用OpenCV DNN在CPU上跑YOLOv5s,640×640输入大概要四五十毫秒一帧,单路勉强能用,两路相机同时检测就明显跟不上节拍。换到TensorRT之后,单路推理直接压到十几毫秒,多路通过并行批处理推上去,吞吐量翻着倍涨。网上有个热度很高的社区实现,就是专门解决LabVIEW里怎么调TensorRT引擎、怎么做并行推理的,整体思路很清晰,我照着落地改了一版,跑了几个月的产线样例,稳定性和速度都超出预期。
这套东西的价值不在于代码本身多炫,而在于把深度学习推理真正塞进了LabVIEW这种工业上位机环境里。做机器视觉的人应该都有同感:LabVIEW做界面、做流程控制、配合运动控制都很顺手,但深度学习这块生态太弱,想要在生产环境里用YOLOv5,必须自己在旁边搭一条加速链路。
1. 项目整体思路与架构选型
1.1 为什么偏偏是LabVIEW + YOLOv5 + TensorRT的组合
先拆一下这个组合里三个成员的定位。
LabVIEW负责的是“外面这一圈”:界面显示、相机采图、图像预处理、结果判断、IO控制、数据记录。这些活儿用文本语言写也能干,但LabVIEW的图形化数据流非常适合做并行任务编排,尤其多路相机同时采图、同时显示、同时推理的场景,LabVIEW的天然并行模型比C++里手工管理线程要省心得多。
YOLOv5负责的是“认出东西来”。它是目前工业项目里用得最多的目标检测模型之一,部署生态成熟,权重好拿,精度和速度的平衡非常优秀。尤其YOLOv5s这个小模型,在GPU上被TensorRT优化之后,速度优势非常明显。
TensorRT负责的是“把模型跑得更快”。它是专门做GPU推理优化的,核心手段包括层融合、精度校准、内核自动调优、显存复用等。同样一个YOLOv5s模型,FP32的ONNX用OpenCV DNN可能跑四五十毫秒,转成TensorRT FP16引擎后能压到十几毫秒甚至个位数毫秒。差距就是这么拉开的,不是显卡本身的问题,纯粹是推理引擎的调度能力完全不同。
1.2 这个社区版实现到底解决了什么痛点
我之前也试过几种接法。第一种是LabVIEW里直接用Python节点调YOLOv5,这种方案开发最快,但实际一跑就发现问题:Python进程和LabVIEW之间的数据拷贝很慢,帧率很难上去,而且Python环境在生产机器上维护起来非常痛苦。第二种是LabVIEW调OpenCV DNN,省事是省事,但CPU推理速度受限,GPU利用率捉襟见肘。
网上那个热度很高的社区实现,核心路径很明确:先用YOLOv5官方仓库把权重导出成ONNX,再用TensorRT把ONNX转成engine引擎文件,最后写一个C++ DLL把推理封装起来,供LabVIEW通过Call Library Function Node调用。这个思路的好处在于,LabVIEW完全不需要关心模型是怎么推理的,只需要把图像数据交给DLL,拿回检测结果就行。
我当时最看中的是它的并行设计。它不是简单地在LabVIEW里开几个循环就算并行,而是真正从底层做了两件事:一是把多路输入图像合并成一个batch塞给TensorRT,一次推理同时处理多张图;二是用CUDA流配合异步接口,让采集、预处理、推理、后处理、显示形成一个流水线,每一路相机都在自己的节奏里跑,但GPU一直在干活。
1.3 “并行推理”不是你想的那样
很多人听到“并行推理”会觉得是开几个线程,每个线程各跑一个模型实例。这个思路在CPU上是对的,在GPU上反而不一定好。
GPU的并行能力来自于成千上万个CUDA核心同时干活,你开四个线程各跑一个模型,如果显存和计算单元本来就没占满,四个实例之间还会互相抢资源,最后速度不升反降。更合理的做法是:把多张图拼成一个batch,一次前向计算同时处理多张输入。TensorRT对batch的优化非常成熟,batch=4的耗时可能只是batch=1的2倍左右,但吞吐量翻了4倍。
这就是那个实现最有价值的地方:它没有粗暴地去“开线程”,而是沿着GPU的思维做batch合并和流水线解耦。
2. 环境准备与模型转换
2.1 环境清单和版本兼容
在动手之前先把环境摆平。LabVIEW侧我用的是比较常规的版本,关键是要确认能调用C++编译出来的DLL,这需要32位或64位匹配,以及LabVIEW安装完整桌面开发环境。
推理侧核心依赖是这几个:
- CUDA版本和显卡驱动要对应好。我调试时用的是CUDA 11.8,TensorRT用的8.5版本,这两个版本搭配比较稳。
- cuDNN要装,TensorRT运行时要依赖它做部分算子加速。
- TensorRT的安装包里包含trtexec这个命令行工具,后面转引擎就用它。
- YOLOv5的导出脚本需要PyTorch环境,建议单独建一个Python虚拟环境。
版本之间的匹配是最大的坑。TensorRT对CUDA版本很敏感,装错了要么报错要么干脆找不到库。我踩过最离谱的一次,是显卡驱动太老导致CUDA运行时初始化失败,LabVIEW那边一调DLL就报“动态链接库初始化例程失败”,排查了半天才发现是驱动版本问题。
2.2 从权重到引擎:ONNX导出与trtexec转换实操
整个转换链路分两步。
第一步,用YOLOv5官方的export.py把pt权重导出成ONNX。命令大概是这样:
python export.py --weights yolov5s.pt --include onnx --img 640 --batch 1 --opset 12这里有几个参数值得多说一句。--img 640表示输入分辨率,我建议固定成640而不是用默认的动态尺寸,因为TensorRT引擎需要确定输入张量形状,动态尺寸虽然也能支持,但会牺牲一部分优化空间。--batch 1先导出单batch模型,后面如果要多路并行推理,可以重新导出batch=4或者直接用trtexec指定。
第二步,用trtexec把ONNX转成TensorRT引擎:
trtexec --onnx=yolov5s.onnx --saveEngine=yolov5s_fp16.engine --fp16 --workspace=4096--fp16是开启半精度推理,速度提升非常明显。--workspace是给TensorRT的构建工作区大小,单位是MB,设太小可能导致某些层融合失败。
转换完成后会生成一个.engine文件,这个文件就是后面真正用来推理的东西。它跟平台强相关,换一张显卡就得重新转。
2.3 转引擎时容易被坑的细节
- 导出ONNX时不要加NMS。YOLOv5的export脚本有个
--nms参数,会把NMS后处理也一起导出,看起来省事,但TensorRT对带NMS的图优化反而不好,后续在LabVIEW侧调整置信度阈值也不方便。我建议导出干净的检测头输出,NMS自己写。 - 注意ONNX里是不是带了动态轴。如果导出时没固定batch,ONNX里会有一堆动态维度,trtexec转的时候要用
--minShapes、--optShapes、--maxShapes指定。工业项目里固定shape是更好的选择,不要过度追求灵活性。 - 转完后先做一次离线验证。可以写个小的Python脚本,加载engine跑一张图,确认输出和PyTorch原模型基本一致,再往LabVIEW里接。这一步能帮你把“模型转换问题”和“LabVIEW调用问题”分开,后面排查少掉一半工作量。
3. LabVIEW侧集成TensorRT推理
3.1 用C++ DLL包一层推理接口
这是整个集成方案里最关键的一环。LabVIEW不能直接加载TensorRT的C++ API,所以需要把TensorRT的推理逻辑封装在一个C++ DLL里,对外只暴露几个简单的C接口。
我封装时参考了社区实现的分层做法,对外暴露三个核心函数:
extern "C" __declspec(dllexport) int DetectorInit(const char* enginePath, int maxBatch); extern "C" __declspec(dllexport) int DetectorInfer(unsigned char* images[], int* widths, int* heights, int batchSize, float* outputBoxes, int* outputCounts); extern "C" __declspec(dllexport) void DetectorDestroy();DetectorInit负责加载engine文件、创建CUDA上下文、分配显存缓冲区。DetectorInfer接收一组图像指针,内部完成缩放、归一化、预处理,然后拷贝到显存、执行TensorRT推理、再拷回结果。DetectorDestroy负责释放所有资源。
这样封装的好处是LabVIEW侧不需要关心CUDA、TensorRT、显存管理这些细节,看到的只有“输入图像,输出检测框”。
3.2 内存管理与Boolean边界问题
LabVIEW调用DLL时的内存布局是另一个大坑。
LabVIEW里一张图像通常以数组形式存在,比如U8数组、宽度×高度×3通道。通过CLN调用DLL时,选择“数组数据指针”方式传入图像数据,DLL拿到的就是连续内存的首地址。这个方式速度很快,不需要额外拷贝。
但我遇到过通道顺序问题。LabVIEW的图像数据很多是RGB顺序,而OpenCV默认的BGR顺序,如果在DLL里没有处理好通道顺序,检测出来的结果会非常诡异——颜色完全对不上,框也可能乱跳。解决方法是把图像给YOLOv5之前明确好通道顺序,建议在DLL里统一转成RGB。
还有一点要注意:LabVIEW的数组内存和C++里的std::vector内存布局不一样,LabVIEW数组有头部描述信息,所以必须用“数据指针”方式,并且传入时把数组维度一起传进去,避免DLL越界访问。
3.3 YOLOv5输出的解码、NMS和画框
YOLOv5的输出格式一般是1 × (num_anchors) × (5 + num_classes)。以640×640输入、COCO 80类为例,输出张量形状是1 × 25200 × 85。这个25200是三组不同尺度特征图的 anchor 总数。
在DLL里做后处理时,我按照这个顺序处理:
- 对每个anchor解析四个坐标、置信度、类别概率。
- 先用置信度阈值过滤一遍,低分框直接丢掉。
- 对剩下的框做NMS,去掉重叠度高的重复框。
- 把最终结果通过输出参数返回给LabVIEW。
NMS不能在LabVIEW里做,主要是性能问题。LabVIEW虽然是图形化语言,但循环处理几千个anchor还是太慢,放在C++ DLL里做是正确选择。
画框在LabVIEW侧做,收到检测结果后直接在Overlay上画矩形,配合原有的IMAQ显示流程即可。
3.4 UI线程与推理线程怎么分
LabVIEW调用DLL是个阻塞操作。DetectorInfer执行期间,调用它的那个VI会一直等着。如果你在UI事件循环里直接调推理,界面立马卡死,帧率再高也白搭。
所以调用方一定要用生产者-消费者模式。采集循环负责取帧,通过队列把图像传给推理循环,推理循环调用DLL,处理完后把结果通过队列发给显示循环。这样采集和显示都不会阻塞,UI保持流畅。
这就是LabVIEW数据流编程的优势所在了,用队列天然就能解耦。我实际测试时,把推理放到独立循环里跑,UI响应速度几乎不受影响。
4. 多路输入并行推理的落地
4.1 并行方案怎么选
多路并行推理,可选方案大致有三种。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| CPU多线程 | 每路一个线程,各跑一个模型实例 | 实现简单,改造成本低 | 速度受限,CPU占用高 |
| GPU多流 | 每路一个CUDA流,共用模型权重 | 延迟较低,资源利用率高 | 实现复杂度中等,需要熟悉CUDA流 |
| Batch合并 | 多路图像拼成一个batch,一次推理 | 吞吐量最高,GPU利用率最好 | 需要等待凑齐整批图像,单帧延迟略增 |
我最后选的是Batch合并为主、CUDA流为辅的方案。对两路相机,batch=2几乎是最优解;对四路相机,batch=4的吞吐量提升比开四个独立流更明显。
4.2 CUDA流与LabVIEW异步调用的折中
Batch合并方案需要解决一个问题:多路图像到达的时间不一定同步。如果一路是30帧,另一路是25帧,强制凑batch会导致快的等慢的。
社区实现里用的办法是,在C++ DLL内部维护一个图像队列,LabVIEW侧每路采集循环只管把图像丢进来,DLL攒够batch大小后自动触发一次推理。这个设计把同步逻辑下沉到了底层,LabVIEW侧完全不用关心对齐问题。
我实际落地时,在DLL里用了两个CUDA流做了流水线:一个流负责预处理和拷贝,另一个流负责推理。上一批图像还在推理的时候,下一批图像已经开始预处理了。GPU的利用率被拉得很满,整体吞吐量比同步方案高不少。
4.3 实测数据:吞吐量成倍增长
我拿三路实时视频流做了测试,输入均为640×640,模型为YOLOv5s,开启了FP16推理。测试结果大致是这样的:
| 方案 | 单路推理耗时 | 三路总吞吐量 | 说明 |
|---|---|---|---|
| LabVIEW + OpenCV DNN CPU | 约50ms/帧 | 勉强20fps | 三路同时跑CPU直接拉满 |
| LabVIEW + TensorRT 单路串行 | 约12ms/帧 | 三路约25fps | 每路都等前一路推理完 |
| LabVIEW + TensorRT Batch合并 | 约18ms/批 | 三路约50fps | 一次推理处理三张图,总吞吐翻倍 |
这个数据可能跟你自己机器上跑的不完全一样,但趋势非常明确:Batch合并带来的吞吐量提升,是体感上最明显的优化手段。
5. 常见问题与排查经验
5.1 典型故障速查表
集成调试过程中,我把几个高频问题整理成了表格。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 调用DLL时LabVIEW直接崩溃 | DLL位数不匹配,或数组指针传错 | 确认DLL是64位还是32位,检查CLN参数配置 |
| 初始化失败,报找不到动态链接库 | TensorRT/CUDA的DLL不在搜索路径 | 把CUDA、TensorRT的bin目录加到系统PATH |
| 输出全为空,检测不到任何目标 | 图像通道顺序不对,或输入尺寸与引擎不一致 | 检查RGB/BGR,确认letterbox填充逻辑 |
| 多路推理时偶发卡死,GPU占用为0 | CUDA上下文冲突 | 每个引擎实例绑定自己的CUDA上下文,不要共享 |
| 内存占用持续上升 | 显存缓冲区未释放 | 检查DetectorInfer里分配的显存是否重复创建 |
我这里特别想强调一下“找不到动态链接库”这个问题。TensorRT运行时依赖一坨DLL,包括nvinfer.dll、nvparsers.dll、cudart.dll、cudnn.dll这些。你编译DLL时链接了它们,运行时就要保证系统能找到。最简单粗暴的办法是编译完DLL之后,把这些运行时依赖和你的engine文件放在同一个目录,调到系统PATH里,不要指望部署机器上装完整CUDA。
5.2 稳定性和长期运行的实战经验
实时推理程序跟离线脚本不一样,一挂就是跑一整天,稳定性比单帧速度更关键。
第一个经验是:初始化只做一次。DetectorInit里加载engine、建上下文、分配显存,这些操作非常耗时,绝不能每一帧都做。很多人在LabVIEW里图省事,把初始化和推理放在同一个循环里,跑起来奇慢无比且显存疯涨,就是这个原因。
第二个经验是:显存释放要严谨。TensorRT的engine、context、CUDA缓冲区都是需要显式释放的。如果DetectorDestroy写得不彻底,每次初始化-释放循环都会泄漏一部分显存,跑几个小时之后显存满了,系统就各种诡异报错。
第三个经验是:输入图像尺寸一定要固定。既然引擎是按640×640构建的,LabVIEW侧传给DLL之前就必须把图像统一缩放到640×640。我推荐在DLL内部做letterbox缩放,也就是等比缩放后填充灰边,保证原图像不变形,检测效果不会因为拉伸而劣化。
第四个经验是:显卡驱动别乱升级。在产线上这台机器只要不出问题,驱动版本就锁死不动。升级驱动后CUDA和TensorRT可能一切正常,也可能突然初始化失败,风险不值得冒。
6. 一些个人体会
这套方案跑了一段时间以后,我最大的感受是:LabVIEW做深度学习推理并没有想象中那么困难,关键是要选对“中间层”。与其在LabVIEW里想办法写推理代码,不如老老实实把推理封装成DLL,让LabVIEW只做它最擅长的事情——流程调度和界面展示。
如果后续要扩展,我觉得有两个方向可以走。一个是把推理相关参数做成配置文件,这样换模型、改阈值、调batch大小都不需要重新编译DLL;另一个是增加TensorRT的INT8量化,在保证精度的前提下进一步提速。不过INT8需要一个校准数据集来生成校准表,工程量大一些,适合对速度有极致要求的场景。
最后分享一个小技巧:调试阶段,在DLL里加一条日志输出到文件,把每次推理的耗时、batch大小、检测数量都记下来。Debug完再关掉。这个习惯帮我省了太多定位问题的时间,因为你永远不知道LabVIEW的图形化界面哪一步到底是它卡了还是底层卡了。