做工业视觉这行的人,应该都遇到过这种尴尬:现场设备用的是LabVIEW,上位机、运动控制、通信全都在里面跑得好好的,结果一到深度学习检测这块就卡住了。Python那边模型训得再快、精度再高,到了LabVIEW里要么调不通,要么慢得没法用。我最近把一个YOLOv5检测模型完整地塞进了LabVIEW,结合TensorRT做加速,实测下来并行推理的速度比纯PyTorch快了好几倍,整个集成过程踩了不少坑,今天把完整的思路和能直接抄作业的步骤整理出来,给同样被困在LabVIEW和深度学习对接问题里的朋友一个参考。
这个方案主要解决的是:LabVIEW作为主控框架不动,YOLOv5模型通过TensorRT的engine形式部署,利用GPU做并行推理,把检测延迟从几百毫秒压到几十毫秒甚至更低。适合那些已经有LabVIEW视觉检测系统基础、想在现有框架里引入深度学习能力,又不想把整个上位机推倒重写的工程师。我会把从环境配置、模型转换、推理封装到并行优化的完整链路都讲一遍,过程中会标出哪些是我实测踩过的深坑。
1. 先搞清楚为什么LabVIEW里跑深度学习这么折腾
1.1 LabVIEW在深度学习这件事上的先天短板
LabVIEW是一门图形化编程语言,它的强项是数据采集、仪器控制、工业通信和界面搭建,这决定了它在自动化检测设备里占有很高的份额。但恰恰是这种"图形化+数据流"的架构,在深度学习推理面前遇到了麻烦:深度学习的推理链条是模型加载、预处理、前向传播、后处理,这一整套几乎都是Python/C++生态的东西,LabVIEW原生根本碰不到模型文件。
早期大家的做法很原始:在LabVIEW里调用Python脚本节点,让Python去跑PyTorch或者ONNX Runtime。这个方案能跑通,但问题非常明显。Python节点的启动开销很大,每次调用都要初始化解释器,模型加载进去之后每次推理还有额外的中间拷贝开销。我试过在LabVIEW里通过Python节点跑YOLOv5,一张640乘640的图像,检测一次要600到800毫秒,这里面的时间大头根本不是推理本身,而是Python环境交互和数据来回倒腾。
更致命的是,LabVIEW的主循环是单线程数据流,一个检测任务占着不放,后面图像采集、运动控制全都在排队。现场设备一跑起来就是采集、检测、判定、动作四步联动,检测慢了整台机器的节拍全崩。所以方案从一开始就不能是"在LabVIEW里调用Python",而必须把推理彻底下沉到高性能原生层,LabVIEW只做结果消费。
1.2 TensorRT为什么是这条路上最值得投入的选择
先把推理加速这件事的候选人摆一摆:PyTorch直接推理、ONNX Runtime、OpenVINO、TensorRT,我逐个试过,但凡最后要接LabVIEW,TensorRT的优先级就应该排到最前面。
推理延迟由两部分组成:框架执行计算的时间和框架本身引入的系统开销。PyTorch在CPU或者普通GPU上跑,算子调度是动态的,图优化程度低,计算效率完全依赖GPU驱动层面的动态加速,对于工业产线这种对延迟极其敏感的场景并不理想。ONNX Runtime比PyTorch好不少,但它在GPU上的优化深度依然有限。
TensorRT是专门针对NVIDIA系列GPU做的推理优化引擎。它的加速手段是实打实的:把网络结构里的卷积层、偏置项、激活函数做层融合,减少算子的启动次数;支持FP16和INT8精度推理,把显存带宽和计算吞吐压榨到最大限度;针对特定GPU架构自动选择最优算子实现,生成序列化的engine文件,运行的时候直接加载反序列化,不走动态图解析那套开销。我在某工控机上实测,同样的YOLOv5s模型,PyTorch GPU推理单张要120毫秒左右,TensorRT FP16压到20到30毫秒,速度快了四倍以上。
还有一个对LabVIEW集成至关重要的理由:TensorRT本身就是C++库,导出后可以封装成DLL。LabVIEW对DLL的调用是几十年积累的成熟能力,通过调用库函数节点就能直接对接。这就意味着可以把Python彻底踢出在线运行链路,推理逻辑全部留给C++和TensorRT,LabVIEW只做图像输入输出和结果展示,数据链路简洁稳定。
2. 整体架构:LabVIEW、C++封装层、TensorRT三个角色怎么分工
2.1 数据流怎么设计才是最舒服的
架构这件事其实可以这样理解:LabVIEW是老车间主任,只管派活和收结果;C++封装层是工段长,负责把任务翻译给GPU执行;TensorRT是具体干活的工人。我在项目里定的数据流是这样的:相机采集图像进LabVIEW内存,LabVIEW把图像数据转成字节数组,通过DLL调用传给C++封装层,C++拿到数据后做预处理、TensorRT推理、后处理,返回检测到的目标框坐标、类别和置信度。整个链路图像数据只存在一份原始拷贝,尽量不做冗余转换。
这里有个很多人在架构阶段就踩进去的坑:在LabVIEW里想把图像转给Python,往往要先把图像转成标准格式,再编码成JPEG或者PNG,Python那边再解码回来。一来一回两次编解码,单张图像至少多花20毫秒。我的方案是LabVIEW里直接把图像的Raw数据指针传给DLL,C++侧直接按内存地址读数据,零拷贝进去做推理,接口的最终形态是传入图像指针、宽、高、通道数,传出目标框数组指针和长度。
2.2 那个开源封装版本值得直接拿来用
标题里提到的Wangxingyu版封装,指的是社区里一个专门把YOLOv5和TensorRT封装成DLL供LabVIEW调用的开源实现。它的设计思路很实用:C++侧把engine加载、预处理、推理、后处理全封好,对外导出一组简单到不能再简单的C接口,LabVIEW侧只需要调用几个函数就能完成初始化、推理和释放。这套接口设计思路是我见过最适合LabVIEW集成的,接口数量控制在五个以内,参数全是基础类型,不需要处理复杂的结构体,对图形化编程极其友好。
第一次用这套封装的时候,我先在纯C++环境里单独验证了DLL的可用性,确认单帧推理功能没问题,再接入LabVIEW。事实证明这个步骤很关键,因为LabVIEW本身出来报错信息不太直观,如果先确保底层DLL是好的,后面排查LabVIEW侧的调用问题就会快很多。后续我在这套开源封装的基础上做了一个二次开发——把原本的同步推理改成流式异步推理,同一时间可以塞进去多张图,由GPU流水线处理,并行度一下子就上来了。
2.3 版本配套问题:一步错步步错的连锁反应
深度学习框架版本之间的兼容性往往在方案设计阶段被忽视,这个项目所有编译和运行问题里,版本不匹配占了大头。我的环境最终定成这样一套经过实测的组合:TensorRT 8.5.3、CUDA 11.8、cuDNN 8.6、Python 3.8、PyTorch 1.13.1、OpenCV 4.7.0。这套组合相对和谐的原因是:TensorRT 8.5以后对ONNX的解析稳定性好了不少,而YOLOv5的官方导出脚本正好能生成兼容的ONNX模型。
下面这个表格是我整理出来的版本对应关系,建议直接照着配,不要自己乱组合:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| 显卡驱动 | 最新稳定版即可 | 必须比CUDA要求的版本高 |
| CUDA | 11.8 | TensorRT 8.5官方支持名单里最稳的之一 |
| cuDNN | 8.6 | 和CUDA 11.8配套发布 |
| TensorRT | 8.5.3 | 支持FP16和INT8的成熟版本 |
| Python | 3.8 | 用于导出和转换ONNX,不在线推理 |
| PyTorch | 1.13.1 | 和Python 3.8配对 |
| ONNX | 1.14.0 | 太高版本导出的算子可能TensorRT不认识 |
版本联动的一个真实案例是我一开始用了TensorRT 8.2,结果在解析YOLOv5导出模型时频繁报错,后来追查发现是某些算子需要新版本才支持。换了8.5.3之后,同样的导出流程一次通过。所以建议从一开始就严格按表配,别在省事上心存侥幸。
3. 模型落地实操:从PyTorch权重到TensorRT引擎文件的完整链路
3.1 用官方脚本导出ONNX,注意参数与时机的细节
YOLOv5的权重文件转engine,中间一定会经过ONNX这一步。执行导出的是YOLOv5官方仓库自带的export.py脚本,命令大概是这样的:
python export.py --weights yolov5s.pt --include onnx --opset 12 --simplify这里面几个参数值得逐个说清楚。--opset 12控制ONNX算子集版本,版本太高的话TensorRT的某些旧解析器可能不认识,版本太低又可能导致模型表达能力不够,12是目前兼容性最好的一个选择。--simplify会调用ONNX Simplifier对计算图做简化,把一些冗余节点和常量折叠删掉,这个步骤对TensorRT能否解析成功影响很大,不加的话有些版本导出之后解析就是会卡住。
导出完成之后,建议先不要急着转engine,先在Python里用ONNX Runtime跑一遍检查输出是否和PyTorch原始结果一致。这一步叫精度对标,防止ONNX导出时某些算子被替换之后数值对不上。我当时就对不齐过一次,后来发现是导出的最后一个检测头里有一个reshape操作没有正确展开,模型结构变了,输出自然就错了,把这个地方手动修正后结果才统一。
3.2 用trtexec命令生成engine,这些参数要理解透
ONNX转TensorRT的engine,官方推荐直接用trtexec工具最省事。我用的命令和逐项解释如下:
trtexec --onnx=yolov5s.onnx --saveEngine=yolov5s.engine --fp16 --workspace=4096 --minShapes=images:1x3x640x640 --optShapes=images:1x3x640x640 --maxShapes=images:4x3x640x640--fp16开启半精度推理,这一步是加速收益最大的一次跳跃,在绝大多数工控机GPU上FP16会比FP32快60%以上,检测精度损失可以忽略不计。--workspace=4096给的是TensorRT的构建工作空间,单位是MB,如果你的显卡显存大可以继续调高,但实际推理时内存占用来自运行时配置,构建工作区大小本身不直接影响运行。--minShapes、--optShapes、--maxShapes这三件套很关键,因为YOLOv5的ONNX模型默认是动态batch,TensorRT构建时得告诉它一个可接受的动态范围,否则build阶段不知道该怎么优化内存布局,这三组值决定了engine以后能接受的batch从1到4变化。
这里有个细节要提醒:--maxShapes设多大,后面的推理时显存按这个上限去预留。我设成4是因为现场工控机显存只有8个G,再大就有点吃紧了。如果你的显存够,把上限提到8或者16,给并行推理留更大空间。
3.3 C++侧推理封装的骨架,移植性很强的模板
engine文件生成之后,C++封装层是整个方案的核心。代码骨架思路可以总结成这样:
模型初始化阶段用IRuntime反序列化engine文件,拿到执行上下文。推理阶段核心动作是:预处理、申请显存、拷贝输入、执行推理、拷贝输出、后处理。我直接放一个精简版的核心逻辑在这里,做了部分脱敏简化,使用时用你自己的实际模型名替换即可。
// 初始化推理引擎 std::unique_ptr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger())}; std::ifstream file("yolov5s.engine", std::ios::binary); std::vector<char> data{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>{}}; std::unique_ptr<nvinfer1::ICudaEngine> engine{runtime->deserializeCudaEngine(data.data(), data.size())}; std::unique_ptr<nvinfer1::IExecutionContext> context{engine->createExecutionContext()}; // 推理一张图的简化流程 void infer(float* inputData, float* outputData, int batchSize) { // 申请GPU显存 void* buffers[2]; cudaMalloc(&buffers[0], batchSize * 3 * 640 * 640 * sizeof(float)); cudaMalloc(&buffers[1], batchSize * kOutputSize * sizeof(float)); // 设置动态batch并执行 context->setBindingDimensions(0, nvinfer1::Dims4{batchSize, 3, 640, 640}); cudaMemcpy(buffers[0], inputData, batchSize * 3 * 640 * 640 * sizeof(float), cudaMemcpyHostToDevice); context->enqueueV2(buffers, 0, nullptr); cudaMemcpy(outputData, buffers[1], batchSize * kOutputSize * sizeof(float), cudaMemcpyDeviceToHost); }代码的核心逻辑是:先用descerializeCudaEngine把离线构建好的engine恢复到内存里,然后每次推理前设定动态输入形状,第三部执行enqueueV2把输入从显存中送入GPU,最后同步拿回输出。在实际工程版本里,预处理、归一化、坐标解码、NMS过滤都在这层做完,对外只暴露干净的结果结构体。
3.4 LabVIEW侧调用DLL:从加载到拿结果的每一步
LabVIEW调用DLL,核心是调用库函数节点(Call Library Function Node),简称CLFN。创建方法是在程序框图里放一个CLFN节点,双击配置函数路径、调用约定、参数类型和返回值。
接口设计中需要注意的几个点我列一下:
- 调用约定选
C,不选stdcall,因为C++侧导出用的是extern "C",约定匹配才拿得到正确地址。 - 传入图像时,用
数组数据指针方式传,先通过Get Image Array把图像数据转成一维U8数组,再配置成指针传递,避免LabVIEW复制数据的时间开销。 - 输出结果用
数组句柄方式传递。C++侧要把检测结果写入一个预分配的数组,LabVIEW侧声明一个大小合适的空数组,传进去让DLL往里面填充,再返回实际长度。 - 每次推理调用前的图像尺寸必须和engine训练尺寸一致,YOLOv5默认是640乘640,C++侧预处理时会自己resize,但LabVIEW侧传入的原始尺寸最好也提前做好约束,能省一次不必要的拉伸。
CLFN配置示例时,参数顺序要和DLL导出函数完全一致,否则经常出现不报错但结果全错的情况,特别是数组指针类型配错的时候,数据读出来是乱的,排查起来相当费劲。我第一次接入的时候,参数配置成数值指针而不是数组数据指针,结果读出来的目标框全是莫名其妙的大坐标,花了一晚上排查才发现是参数类型配置错了。
4. 并行推理的关键策略:从batch到流再到多路采集
4.1 Batch维度的并行:一次喂进去四张图
很多人对并行推理的理解停留在"同时跑多个推理任务"这个层面,这没错,但落地方式要分清楚。TensorRT里最简单直接的并行是在batch维度做文章:把多张图像拼成一个batch喂给模型,一次前向计算同时处理四张图。这种并行吃满了GPU的吞吐能力,因为它让GPU的算子同时处理多张图的数据,计算密度足够高。
我在实践里做了这样一组性能对比,模型是YOLOv5s,TensorRT FP16,某工控机显卡:
| 推理方式 | 单张平均延迟 | 实际吞吐 |
|---|---|---|
| PyTorch GPU单张 | 约120ms | 约8帧/秒 |
| TensorRT FP16单张 | 约28ms | 约35帧/秒 |
| TensorRT FP16 batch=4 | 约48ms/4张 | 约83帧/秒 |
| 两路并行 + batch=2 | 约32ms/2张 | 约62帧/秒 |
可以看到,batch=4的时候,单张平均延迟看起来和单张差不多,但吞吐量翻了一倍多,这就是batch并行的价值。对生产线来说,重要的是每秒钟能检测多少张,而不是单张多少毫秒,所以这个数据看起来非常关键。要达成这个效果,C++侧要维护一个环形缓冲队列,每来一张图就塞进队列,凑够batch=4或者等待超时就统一推理一次。
4.2 多路相机任务并行:真正贴合产线的场景
回到实际产线场景:一条设备同时装了两个相机,一个拍产品正面一个拍反面,两个检测任务互相独立。如果只用batch并行,两路图像混在一个队列里,一是batch无法打满,二是两路的处理和响应时间会互相影响。
我的做法是双路双队列,每个相机对应的业务逻辑自己维护一个流,各自独立填batch、独立推理。每一路C++侧创建一个独立的TensorRT执行上下文,因为一个上下文同时只能提交一个推理任务,多路并行就需要多上下文。显存空间做好统一管理,加载一个engine实例,但为每路创建独立的IExecutionContext,这样底层权重共享,但推理任务上下文互不干扰。
这套架构跑下来最明显的好处是:一路图像卡顿或者传输延迟时,另一路完全不受影响。原先单上下文排队时,一个相机的慢图像会把另一个相机的检测也拖慢,现在各自独立,稳定性上升不少。如果你现场的相机路数超过两路,这个方案只是从2扩到N的问题,每一路都有自己的输入线程、队列、执行上下文和输出回调,整体思路不变。
4.3 异步流水线的效果:同一时刻CPU和GPU各自都在忙
深度学习推理有个和传统上位机编程不同的特点:主机和设备之间的数据搬运、预处理在CPU上做,前向计算在GPU上做。同步推理的逻辑是"CPU放进数据等GPU算完拿结果",这一步等待的时间里GPU其实也未必忙得过来,因为CPU侧预处理可能还没跟上。
我把推理改成异步之后,整个链路变成了流水线:CPU在做第N张图的预处理和拷贝时,GPU正在算第N-1张图,两边重叠。C++侧用两个线程来管理,一个线程负责数据预处理和cudaMemcpy进显存,另一个线程负责enqueueV2和结果回收,两者通过CUDA流机制串起来。实验数据表明,改成异步流水线之后,端到端单张延迟又减少了约20%到30%。
对LabVIEW侧来说,异步改造带来的体验是调用推理函数不再阻塞。LabVIEW主循环发一张图给DLL之后可以立刻返回去做图像采集和逻辑判断,检测结果通过回调函数或者队列形式在后面异步拿回来。这一点在原本的同步调用里做不到——LabVIEW会死等检测完才继续走下一步,视觉检测一旦慢,运动控制就得跟着停下来等,整机节拍就是这么被拖垮的。
5. 实测数据与结果评估:这套架构到底能跑多快
5.1 完整链路延迟的实测拆解
我拿一套实际项目做了完整统计,从相机采图到LabVIEW界面显示结果框,全链路时延分解如下。相机是千兆网工业相机,分辨率1920乘1080,采集端耗时约12毫秒;图像从LabVIEW传给DLL加C++侧预处理约6毫秒;TensorRT FP16推理,单张batch=1约25毫秒;后处理加NMS约4毫秒;结果回到LabVIEW加绘制叠加约5毫秒。整个链路单张总耗时大约52毫秒。
换成batch=4流水线之后,四张图的平均单张耗时稳定在40毫秒左右,如果用双路异步加batch=2,单路单张约35毫秒,两路合起来每秒能处理约57张。这个数据对大多数视觉检测产线已经是绰绰有余了。相比之下,早期的纯Python节点方案单张800毫秒显得漫长到无法接受。
这里把三种方案的实测对比完整贴出来,方便你判断自己该选哪条路线:
| 方案 | 单张端到端延迟 | 帧率 | 部署复杂度 | 稳定性 |
|---|---|---|---|---|
| LabVIEW直接调Python跑PyTorch | 600-800ms | 1-1.5帧/秒 | 低 | 差,Python崩溃会拖垮主程序 |
| Python+ONNX Runtime,C/S通信 | 200-300ms | 3-5帧/秒 | 中 | 中,网络通信有IO瓶颈 |
| C++封装TensorRT,DLL直调 | 35-50ms | 20-28帧/秒 | 中高 | 高,原生线程无解释器开销 |
从表格可以明显看出,前两种方案根本撑不起高速产线的实时判定需求。第三种方案的数据才是深度学习视觉系统上线的及格线。整个改造值得投入,省下来的每一秒都是在给产线节拍松绑。
5.2 精度对标:FP16到底损失了多少检测能力
有人听到FP16会紧张,担心精度掉得厉害。我在同一批测试集上分别用FP32和FP16两个engine跑了一遍,对比mAP和单类目标检测的召回率。结果FP16相比FP32,mAP下降基本在0.3个百分点以内,而单张推理延迟下降了一半以上。对工业检测来说,这个精度损失完全可以接受,毕竟场景中的目标相对规整,背景变化也不像自动驾驶那样复杂。
真正影响精度的往往不是FP16本身,而是预处理细节。YOLOv5的预处理包含标准化和缩放,这一步的代码如果写错了,坐标偏移和置信度下降的问题随之而来。建议在接入后处理之前,先随便挑一张测试图直接输出模型的原始Tensor,和PyTorch的同样数据做数值对比,误差在1%以内就可以认为预处理正确。
如果检测目标是极小的缺陷,比如丝印上的字符缺失、晶圆表面的微小划痕这类对精度极其敏感的场景,FP16就必须谨慎。建议先用FP32跑通全流程拿基线,再切FP16对比检测效果,肉眼确认缺陷仍能稳定检出,再决定要不要用FP16上线。INT8方案我就暂时不推荐了,它的加速比FP16进一步翻倍,但量化标定流程复杂度高,而且稍不注意精度就会崩盘,工控设备上性价比并不划算。
6. 部署现场最容易绊倒人的几个深坑,我帮你先踩了一遍
6.1 显存爆炸:动态batch和内存池没配合好
并行推理最容易炸的环节就是显存。动态batch的engine会把最大batch对应的显存全部预留,如果你的--maxShapes设了8,那显存分配就按8张图的最大规模来。同时开多路多上下文,每路一张engine各留一份输出缓冲,显存立刻吃紧。
我的经验是两个:一是别贪心设大maxShapes,够用就行,实测产线同时待处理的图像最多四到六张,设成8就是浪费;二是在C++侧复用输入输出缓冲,不要在每帧推理里重复cudaMalloc。申请显存开销比推理还大,复用缓冲之后显存占用稳定控制在700MB以下。
6.2 LabVIEW长时间运行后内存越占越大
LabVIEW调用DLL的典型毛病是:调用一次分配一次,结果不释放,跑一晚上内存就爆了。最初我的DLL设计是每次推理都动态分配输出数组,LabVIEW读完后没释放干净,内存只涨不跌。后来改成初始化时一次性分配好缓冲,每个推理周期只在同一个内存块里覆盖写入,内存曲线就变得平坦了。
排查这类问题不用猜,LabVIEW自带的性能分析工具能看内存分配热点,配合DLL侧自己打印调用日志。一旦发现某块内存在每个周期都在申请但没有对应释放,问题基本就定位了。
6.3 显卡驱动更新之后engine文件突然不能用了
TensorRT生成的engine文件其实和运行环境强绑定,尤其是和显卡驱动、CUDA、TensorRT的版本强相关。有时候我控制电脑重装了驱动,原来的engine加载直接报兼容性错误。这个问题的根源在于TensorRT的序列化格式会针对特定GPU架构做优化,换了驱动或者CUDA版本,优化信息就可能失效。
应对方法非常朴素:engine文件不要只留一份,重装环境之后重新生成一遍。更稳妥的做法是保留好ONNX源文件和转换脚本,出了问题随时可以重新构建。把转换命令写成脚本放项目目录里,注释清楚用了哪些参数,是最省心的管理方式。
6.4 多线程同时调用同一个执行上下文,莫名其妙卡死或报错
TensorRT的执行上下文(IExecutionContext)不是线程安全的,多个线程同时提交推理任务,轻则卡顿,重则直接崩溃。很多人理解成"engine是线程安全的就行",但实际的执行提交确实不行,这是并行化改造里最常见的坑了。
标准解法是为每个业务线程创建独立上下文,engine全局共享。engine只包含模型权重和结构描述,本身占显存不大,一份就够;上下文保存的是每次推理需要的临时缓冲和执行状态,多路一路一份互不干扰。这个代码模式和databases连接池的道理是共通的,每个线程都配自己的连接,不共享同一个执行通道。
7. 后续还能怎么扩展:这套底子还有不少可挖的空间
7.1 从YOLOv5换到YOLOv8或自定义检测头
有人会问:这套方案是不是只能绑死YOLOv5?其实不是。TensorRT解析的是ONNX,任何能转成ONNX的检测模型都可以走同一条路径。我就试过把YOLOv8的模型转成ONNX,再生成engine,C++封装层完全不用改,因为输入的张量维度都是1x3x640x640,输出的格式在代码层面做一次适配就行。
自定义检测头的模型同理,只要保证预处理和后处理对应上。关键是C++封装层的后处理要设计得灵活,把坐标解码、置信度过滤、NMS这几个环节做成参数可调,换模型之后不用改代码,只改配置就能适配。我现在项目里封装层的输出结构是通用的:目标类别、置信度、中心坐标、宽高,任何检测模型都能映射到这套结构上,手动适配成本很小。
7.2 界面上做多线程接结果:LabVIEW主循环不再被拖累
LabVIEW侧接入用的一套模式是队列加状态机:采集线程不断把图像发给推理线程,推理线程把结果通过队列回传,显示线程用生产者消费者模式刷新界面。这个模式改造的最初目的只是让界面不卡,跑起来之后发现还白捡了一个好处:图像采集和推理并行,采集完一张的同时推理正在进行上一张,整机节拍直接趋近于采集节拍而不是采集加推理之和。
这个改造对手持设备或者需要人机交互的场景特别有用。你先在LabVIEW里建立状态机框架,把采集、检测、判定、动作分成四个状态,检测节点通过DLL异步调用的方式触发,不等结果返回,等回调事件传入之后再切到判定状态,整个GUI线程就永远不被阻塞了。
7.3 多GPU节点:单机扩展不是问题,只是成本问题
当一路推理满足不了吞吐需求的时候,第二个方向就是多GPU并行。TensorRT天然支持多卡部署,在每个显卡上都加载一份engine,然后用路由策略决定哪张图进哪块卡。这套方案我实测扩展性很好,两张大显存并行处理后,吞吐量基本上是翻倍增长的。
唯一的顾虑是成本。工业工控机要上双卡,主板PCIe通道数量、电源功率、散热结构都要跟着升级。所以在决定多卡之前,还是先确认一下单卡的batch并行和流水线有没有压榨干净,大概率压力不够值得再加卡投入。实测数据里batch优化到位之后,单卡吞吐从八帧提到五十帧以上,这个提升幅度已经能覆盖绝大多数产线了。
从最开始在LabVIEW里面对Python节点望而生叹,到现在C++封装加TensorRT跑流水线并行,整个方案本质上是传统上位机和深度学习推理的一次架构和解。我给做这套方案最大的体会是:技术选型最重要的不是追逐某个框架的流行度,而是找到能跟你现有系统稳定衔接的那条路。LabVIEW加TensorRT这条路,最终稳定性的表现,比花哨的框架对比数据可靠得多。