简介:面向C#开发者和计算机视觉学习者,此Demo演示了如何在.NET环境中借助OnnxRuntime加载YoloV8模型,完成视频流的实时目标检测,并针对GPU加速场景优化了推理链路,降低了模型部署与集成门槛。压缩包共353个文件,体积约984.69MB,其中包含大量dll动态库与onnxruntime依赖、多个已转换好的onnx模型、C#源码工程、xml配置文件、测试视频及项目说明文件,sln解决方案可直接在Visual Studio中打开调试。目前已有322人学习,适用于安防监控、交通流量统计、智能零售等实时分析场景的快速验证。开发者可通过工程目录清晰了解GPU推理所需的初始化、预处理、推理及后处理步骤,结合业务需求灵活替换模型或扩展检测逻辑,从而有效缩短视频分析系统的开发周期。
1. 拿到这份 C# YoloV8 视频检测 Demo 源码,先别急着解压
做上位机和工业视觉的人,迟早都要面对一件事:用 C# 把 YoloV8 的检测能力接进自己的程序。Python 里跑 YoloV8 到处都是教程,换到 C# 就卡住了——不是模型训练不出来,是把 OnnxRuntime 接进 WinForms 或 WPF 时,环境、版本、GPU 加速这些环节能把人磨掉一两天。标题里这份带 GPU 环境的 Demo 源码,价值就在帮你把“能跑”这两个字先挣到手:模型是现成的,推理代码是现成的,连 CUDA 和 OnnxRuntime 的版本坑都替你趟过一遍。适合谁?刚接触 C# 与 YoloV8 集成的上位机工程师,或者想把现有检测 Demo 从 CPU 切到 NVIDIA 显卡加速的开发者。不过源码能帮你省时间,帮不了你避掉所有坑,这篇就把跑通这套东西的完整路径和血泪经验讲清楚。
2. 开跑之前先看懂推理链路:OnnxRuntime 在 C# 里到底怎么调用 GPU
2.1 为什么主流做法是 OnnxRuntime 而不是 TensorRT 或 OpenVINO
做 YoloV8 推理部署,常见选项就三个:TensorRT、OpenVINO、OnnxRuntime。如果项目是 C# 写的,你会很快发现 TensorRT 的官方 C# 绑定并不好用,多数情况得包一层 C++/CLI 或 P/Invoke,调试一次够你折腾半天。OpenVINO 对 Intel 核显和部分独立显卡友好,但如果你手里的机器是 NVIDIA 显卡,它走的是 GPU 插件而非 CUDA 原生态,性能释放不彻底。OnnxRuntime 是微软维护的跨平台推理引擎,原生提供 C# NuGet 包,不需要你写 C++ 包装层,这是选它的第一理由。
第二个理由是模型转换链路短。YoloV8 训练完导出 ONNX 格式,直接用 OnnxRuntime 加载,不需要像 TensorRT 那样先做 engine 序列化,也不用处理动态 shape 的额外配置。对视频检测这种输入尺寸固定的场景,OnnxRuntime 顺手就把动态轴给固定了,省掉一批部署期的幺蛾子。第三个理由是它的 Execution Provider(EP)机制可以灵活切换 CPU 和 GPU。你在代码里注册 CUDA EP,它就调显卡;注册 CPU EP,它就回落到 CPU 跑,同一个模型文件两边通用。
关于 onnx 和 onnxruntime 的概念差别,这里也顺带说清楚:ONNX 是一种模型文件格式,相当于“菜谱”;OnnxRuntime 是执行这个菜谱的“厨师”。你用 YoloV8 导出的 .onnx 文件里存的是计算图结构和权重,不是可执行代码,必须由 OnnxRuntime 读进去并调度到 CPU 或 GPU 上才算活模型。网上很多人报错说“我导出了 onnx 但跑不起来”,多半是把格式和运行时混为一谈了。
2.2 GPU 版的环境结构与依赖链:CUDA、cuDNN 和 OnnxRuntime 三个版本必须对齐
GPU 版 OnnxRuntime 的依赖链比 CPU 版长很多。CPU 版你只需要一个 NuGet 包直接拉进项目就能跑;GPU 版除了 Microsoft.ML.OnnxRuntime.Gpu 这个包,还要求系统里预先装好 NVIDIA 显卡驱动、CUDA Toolkit、cuDNN。驱动和 CUDA 的对应关系、CUDA 和 cuDNN 的版本匹配、这两者再和 OnnxRuntime 要求的版本匹配,三层对齐,错一层就出问题。
常见版本对应关系大致如下,这是我实际部署时验证过的组合。
| 组件 | 版本范围 | 用途 |
|---|---|---|
| NVIDIA 驱动 | 530 以上 | 驱动 GPU,驱动里已含运行时 CUDA 库 |
| CUDA Toolkit | 11.8 或 12.x | 提供 nvrtc64_*.dll 等运行时库,OnnxRuntime 需要它 |
| cuDNN | 8.x(匹配 CUDA 11.8)或 9.x(匹配 CUDA 12.x) | 深度网络算子加速库 |
| Microsoft.ML.OnnxRuntime.Gpu | 1.16+ | C# 侧 Gpu 版 NuGet 包 |
注意一个细节:OnnxRuntime 官网对 GPU 包的说明里写的是“需要 CUDA 和 cuDNN 的特定版本”,比如某些旧版只支持 CUDA 11.8 + cuDNN 8.2。你机器上装的是 CUDA 12.1,理论上驱动能跑新游戏,但 OnnxRuntime 调用 CUDA EP 时加载你系统里的 cudart 和 cublas 库,版本不匹配就会静默回退到 CPU,或者直接抛异常。这就是很多人的 GPU 版跑起来却是 CPU 速度的根因。
从代码层面看,GPU 推理的完整调用链是这样的:C# 代码里先 new 一个 SessionOptions,往里面 AppendExecutionProvider(CUDA_FP16) 注册 CUDA 执行提供程序,再用 OrtSession 加载 onnx 模型文件。构造 session 的那一刻,OnnxRuntime 会在系统目录里找 CUDA 相关动态库,找到并校验版本通过后,后续算子就调度到 GPU 上执行。整个链路里最黑匣子的部分是“更底层的东西没配对”,因为 C# 侧代码编译没问题,运行也不报错,就是速度和 CPU 没区别,这种翻车最让人头疼。所以拿到 GPU 版 Demo 的第一件事,不是看业务代码,是先确认 EP 真的被注册成功。
3. 把 Demo 跑通的第一遍:从解压到画面里出现检测框
3.1 GPU 环境自检:用一行命令确认 OnnxRuntime 真的在用显卡
不管 Demo 源码里写得多完善,你自己机器上的 CUDA 环境是独立的,所以第一遍跑通前,先做一个两分钟的自检。打开命令行,执行以下命令确认显卡驱动和 CUDA 版本的匹配关系。
nvidia-smi正常输出里会显示驱动版本和 CUDA 版本,例如 “Driver Version: 532.03 CUDA Version: 12.1”。注意:这里的 CUDA Version 表示当前驱动支持的最高版本,不代表你装了 CUDA Toolkit。你的机器上可能没装完整 Toolkit,但只要驱动足够新,OnnxRuntime 很多时候也能跑起来,因为它自带部分依赖;最稳的做法还是装对应版本的 CUDA Toolkit 和 cuDNN。
接着用一段最小化的 C# 代码验证 OnnxRuntime 的 GPU EP 是否可用。这段代码也可以在拿到 Demo 源码后单独跑,用来区分是环境问题还是业务代码问题。
using Microsoft.ML.OnnxRuntime; // 关键验证:检查当前 OnnxRuntime 提供了哪些 Execution Provider var providers = OrtEnv.Instance().GetAvailableProviders(); foreach (var p in providers) { Console.WriteLine(p); }输出里如果包含"CUDAExecutionProvider",说明这个包是 GPU 版,并且本机动态库依赖被正确识别。如果只有"CPUExecutionProvider",说明你现在引用的要么是 CPU 版 NuGet 包,要么是 GPU 版包但 CUDA 依赖缺失。很多 Demo 里写的是Microsoft.ML.OnnxRuntime.Gpu,但项目实际引用了普通版,这个检查一分钟就能看出来。
3.2 最小推理代码拆解:加载模型、预处理、Run 和解析输出
我见过不少 Demo 源码喜欢把整个推理封装成一个大类,新手看半天不知道入口在哪。这里我把最小可运行路径拆成四段代码,每段对应一个环节,拿这份源码里的实现对照着看,能快速定位自己要改的位置。
第一段是构造 session 并注册 GPU provider。
using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; // 初始化 SessionOptions,并追加 CUDA EP var sessionOptions = new SessionOptions(); sessionOptions.AppendExecutionProvider_CUDA(); // 注意:fp16 模式能显著提速,但需要显卡支持;老显卡直接写上面一行即可 // sessionOptions.AppendExecutionProvider_CUDA(0); // 0 表示使用第一张显卡 // 加载模型; modelPath 指向 demo 里自带的 .onnx 文件 string modelPath = @"D:\yolov8n.onnx"; using var session = new InferenceSession(modelPath, sessionOptions);第二段是输入预处理。YoloV8 的标准输入是 1x3x640x640,归一化到 0~1,通道顺序是 RGB。OpenCvSharp 读出来的是 BGR,所以要转换通道顺序再归一化。这里用DenseTensor构造输入,是 OnnxRuntime 最常用的方式。
using OpenCvSharp; // 读取一帧图像 Mat image = Cv2.ImRead(@"D:\test.jpg"); int inputSize = 640; // 等比缩放 + 填充,保持宽高比,避免物体变形 var resized = new Mat(); float ratio = Math.Min((float)inputSize / image.Width, (float)inputSize / image.Height); int newW = (int)(image.Width * ratio); int newH = (int)(image.Height * ratio); Cv2.Resize(image, resized, new Size(newW, newH)); var canvas = new Mat(inputSize, inputSize, MatType.CV_8UC3, Scalar.All(114)); resized.CopyTo(canvas[new Rect(0, 0, newW, newH)]); // BGR -> RGB,并归一化到 [0,1] var inputTensor = new DenseTensor<float>(new[] { 1, 3, inputSize, inputSize }); for (int y = 0; y < inputSize; y++) { for (int x = 0; x < inputSize; x++) { Vec3b color = canvas.At<Vec3b>(y, x); int idx = y * inputSize + x; inputTensor[0, 0, y, x] = color.Item2 / 255f; // R inputTensor[0, 1, y, x] = color.Item1 / 255f; // G inputTensor[0, 2, y, x] = color.Item0 / 255f; // B } }第三段是执行推理。YoloV8 模型的输出通常只有一个名称,例如"output0",维度是 1x84x8400。84 的含义是 4 个框坐标(cx, cy, w, h)加 80 个类别置信度;8400 是三个尺度特征图上的候选框总数。
var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", inputTensor) }; using (var results = session.Run(inputs)) { var output = results.First().AsTensor<float>(); // output 的维度: [1, 84, 8400] // 后续后处理需要转置成 [1, 8400, 84] 再遍历 }第四段是后处理,从 8400 个候选框里筛出最终的检测框。这里先简单拉取置信度满足阈值的结果,NMS 部分后面单独说。
float confThreshold = 0.25f; var boxes = new List<Rect>(); var scores = new List<float>(); var classIds = new List<int>(); // 假设 output 已转置为 [8400, 84],逐行读取 for (int i = 0; i < 8400; i++) { float maxScore = 0; int maxClass = -1; for (int c = 4; c < 84; c++) { if (output[0, i, c] > maxScore) { maxScore = output[0, i, c]; maxClass = c - 4; } } if (maxScore > confThreshold) { float cx = output[0, i, 0]; float cy = output[0, i, 1]; float w = output[0, i, 2]; float h = output[0, i, 3]; // 坐标是相对 640x640 的,需要按缩放比例映射回原图 int x = (int)((cx - w / 2) / (inputSize / newW)); int y = (int)((cy - h / 2) / (inputSize / newH)); int bw = (int)(w / (inputSize / newW)); int bh = (int)(h / (inputSize / newH)); boxes.Add(new Rect(x, y, bw, bh)); scores.Add(maxScore); classIds.Add(maxClass); } }这里有个参数值得注意:inputSize / newW是填充后图像与原图宽度方向的缩放比例,因为前面做了等比缩放和填充,后处理时所有坐标必须除以这个比例才能映射回原图坐标。很多 Demo 在画框时出现偏移,多半是这里只做了缩放没考虑填充。
3.3 视频循环里的帧处理和 FPS 显示
视频检测和单张图片检测的差别在于帧循环的写法。用 OpenCvSharp 的VideoCapture读取视频文件或摄像头,每一帧做一次推理。这里要特别注意Mat对象的释放,否则长时间跑视频显存和内存会同时暴涨。
using var capture = new VideoCapture(@"D:\test.mp4"); using var window = new Window("detection"); Mat frame = new Mat(); var stopwatch = new System.Diagnostics.Stopwatch(); while (capture.Read(frame)) { if (frame.Empty()) break; stopwatch.Restart(); // 这里调用上面封装好的推理函数,返回检测框列表 var detections = DetectFrame(frame); stopwatch.Stop(); double fps = 1000.0 / stopwatch.ElapsedMilliseconds; // 画框:遍历检测框,用 PutText 标出类别和置信度 foreach (var det in detections) { Cv2.Rectangle(frame, det.Box, Scalar.Red, 2); string label = $"{det.Label} {det.Score:F2}"; Cv2.PutText(frame, label, new Point(det.Box.X, det.Box.Y - 5), HersheyFonts.HersheySimplex, 0.6, Scalar.Red, 2); } // 在左上角显示实时 FPS Cv2.PutText(frame, $"FPS: {fps:F1}", new Point(10, 30), HersheyFonts.HersheySimplex, 0.8, Scalar.Green, 2); window.ShowImage(frame); int key = Cv2.WaitKey(1); if (key == 27) break; // ESC 退出 } frame.Dispose();说明:DetectFrame是把 3.2 节的预处理、推理和后处理封成一个函数后的逻辑单元,输入一帧Mat,输出自定义的检测结果集合。stopwatch计时从预处理开始到后处理结束,包含了完整推理耗时,这个数值能够直接反映 GPU 加速是否生效。如果 FPS 数字和纯 CPU 跑一样低,那就要回头查 EP 是否真的挂在 CUDA 上。
4. 让检测结果真正能用:后处理参数和性能调优
4.1 8400 个候选框怎么过滤:置信度阈值与 NMS 的选择
YoloV8 和 YoloV5 的最大区别在于解耦头,但输出结构从 V5 到 V8 也从“每个 anchor 预测”变成“直接预测”。8400 个候选框对应 640x640 输入下三个尺度的特征图:80x80、40x40、20x20。80x80 负责小目标,20x20 负责大目标。实际使用时你会发现同一辆车可能在多个尺度上都有框,而且同一个目标附近会有好几个重叠框,这时就需要 NMS 把它们合并成一个。
NMS 的逻辑不复杂:先把所有候选框按置信度从高到低排序,保留最高分的框,然后删除所有与它 IoU 大于阈值的框,重复这个过程。C# 里没有像 Python 那样可以直接调用的torchvision.ops.nms,需要自己实现或引入 OpenCvSharp 的Cv2.Dnn.NMSBoxes。
// 使用 OpenCvSharp 自带的 DNN NMS 实现,避免手写排序合并 float nmsThreshold = 0.45f; var indices = new int[boxes.Count]; Cv2.Dnn.NMSBoxes(boxes, scores, confThreshold, nmsThreshold, out indices);参数confThreshold和nmsThreshold是最影响检出效果的旋钮。confThreshold 一般取 0.25 到 0.4 之间:取低一点能捡到更多低置信度的目标,但误检也会变多;取高了漏检风险上升。nmsThreshold 取 0.45 到 0.6,这个值决定了两个重叠框算不算同一个目标,值越大越容易合并相邻目标,适合人群密集场景;值越小越能区分紧挨着的不同个体,但一个目标可能出现多个框。
一个常见的翻车点:用同一个模型在工业产品检测场景沿用默认的 0.25 / 0.45,结果缺陷漏检率特别高。这类场景需要把 confThreshold 压低到 0.1 左右,宁可在后处理里多做一步面积过滤,也不要让真正的缺陷从置信度这关被卡掉。置信度阈值不是越高越好,它需要跟随你的漏检和误检成本来调整。
4.2 GPU 推理的四个松紧旋钮:批大小、半精度、线程数与输入尺寸
后处理调完之后,真正决定视频检测流畅度的是推理开销。GPU 版 Demo 里通常会暴露几个关键参数,改对它们,性能有明显变化。
第一个旋钮是输入尺寸。YoloV8 的训练默认是 640x640,但推理时可以降到 480x480 或 416x416,检测速度提升明显,代价是小目标召回率下降。视频检测通常不需要对每一帧做全分辨率推理,跑监控画面时把输入降到 480 是常规操作。
第二个旋钮是半精度推理。CUDA EP 默认是 FP32,你可以调用以下方式开启 FP16:
sessionOptions.AppendExecutionProvider_CUDA(0); // 开启半精度,部分算子走 FP16 加速 sessionOptions.EnableMemoryPattern = true; sessionOptions.ExecutionMode = ExecutionMode.ORT_SEQUENTIAL;严格来说,FP16 加速不是通过AppendExecutionProvider_CUDA的参数开启的,而是在模型侧或通过OrtCUDAProviderOptions的cudnn_conv_use_tensor_ops等参数控制。在 C# 侧较简便的做法是在导出 ONNX 时就指定float16=True,或者在推理时给每个输入张量转成Float16类型。不是所有算子都支持 FP16,开了之后如果模型里有不支持的层,OnnxRuntime 会自动把这些算子回退到 CPU 或 FP32,整体速度不一定线性提升。我的经验是:新显卡(RTX 30/40 系)开 FP16 收益明显,老显卡(GTX 16 系)收益不大,还会增加显存换页的抖动。
第三个旋钮是ExecutionMode。ORT_SEQUENTIAL是逐算子执行,ORT_PARALLEL是开启多线程并行。视频检测场景我建议保持 SEQUENTIAL,因为并行模式对单帧推理的提升有限,反而可能因为线程竞争导致延迟波动。
第四个旋钮是输入输出名字。你的模型输入节点名不一定是"images",这取决于导出时怎么命名的。用 Netron 打开 onnx 文件,看输入节点名和输出节点名,然后在代码里对应改掉。Demo 里写死"images",你换成自己的模型时最容易在这翻车,报错信息通常会显示Invalid input name。
5. GPU 版最常见的排错记录:五条从崩溃到卡死的踩坑
5.1 现象:程序直接崩溃,错误信息是 Access Violation c0000005
运行到session.Run那一步时程序直接闪退,Windows 事件查看器里记着Access Violation c0000005。这个报错在 C# 调用 C++ 层时最常出现,但很多人第一反应是去查托管代码里有没有空引用,方向错了。
原因:OnnxRuntime 的 GPU EP 在加载时读取了 CUDA DLL,但这个 DLL 版本和当前驱动不匹配,导致运行时内存访问越界。另一种常见情况是模型输入 shape 和代码里构造的 tensor shape 不一致,OnnxRuntime 在把数据拷贝到 GPU 显存时越界访问。
解决:先做一个隔离实验,用 3.1 节那段 provider 检查代码确认 CUDA EP 被识别。如果检查通过,再打印 session 的输入和输出元数据,逐一比对维度。这两个步骤能排除掉 80% 的 c0000005。我还是经常会提醒自己:遇到 access violation,先别改业务代码,先查环境。
5.2 现象:加载模型直接报 DllNotFoundException
这种错误通常在程序启动时发生,错误信息是找不到onnxruntime.dll或cudart64_*.dll。
原因:项目里引用了 NuGet 包,但拷贝输出目录时没有把 native 运行时 DLL 一起拷过去。CPU 版 NuGet 包通常会在runtimes/win-x64/native下带 DLL,但 GPU 版还需要 CUDA 的 DLL,这一部分不在包内。
解决:确认Microsoft.ML.OnnxRuntime.Gpu包的Copy Local属性为 true,并到项目输出目录检查onnxruntime.dll是否存在。如果存在,再检查系统 Path 里能否找到cudart64_12.dll。CUDA 安装目录默认在C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.x\bin,把它的 bin 目录加进系统 Path 是常规解法。注意加完要重启 Visual Studio 或终端,否则环境变量不生效。
5.3 现象:显存持续增长,跑 10 分钟后占满显存
视频检测循环里 FPS 正常,但监控任务管理器可以看到显存和内存一起稳步上涨,最后系统卡死或推理速度暴跌。
原因:典型的托管资源未释放问题。session.Run返回的IDisposableResults没释放,或者Mat帧对象在循环里没有 Dispose,非托管内存被反复占用。显存那一侧,DenseTensor在 GPU EP 下会分配设备内存,如果结果对象没释放,显存同样增长。
解决:把using包住所有session.Run的返回值,帧处理完显式Mat.Dispose()。我在自己项目里常用一个技巧:把视频循环内的所有临时对象都放进using块或try/finally,让它们生命周期在单帧内结束。经验值是跑 5000 帧后显存波动不超过 100MB 才算正常。如果仍在涨,考虑是不是模型在每次推理时动态创建了 session——不要在循环内 new InferenceSession。
5.4 现象:CUDA 版本不对,报 “There is no provider registered”
这个报错直译是“没有注册的 provider”,但从现象上看有人会在AppendExecutionProvider_CUDA()这行代码上怀疑是不是 API 写错。
原因:OnnxRuntime 的 GPU 版在初始化 CUDA EP 时先去系统找 CUDA 运行时库,找到的库版本要求必须落在它支持的范围内。比如 OnnxRuntime 1.16 的 GPU 包对应 CUDA 12.x,你机器上只有 CUDA 11.8,那个库在加载时校验失败,EP 注册不了,运行时就报这句。
解决:调整 CUDA Toolkit 版本与 OnnxRuntime 对应。这里有一个比较实用的保守做法:直接安装 CUDA 12.1 或 12.4,配合最新版Microsoft.ML.OnnxRuntime.Gpu包,这是目前兼容面最广的组合。如果项目历史包袱重,不能升级 OnnxRuntime,就保留旧包并找到它官网上标注的 CUDA 版本再装对应 Toolkit。装多个 CUDA Toolkit 是可以共存的,Path 里哪个排在前面就优先用哪个,注意别让旧版本的 bin 路径覆盖掉新版本。
5.5 现象:视频帧率上不去,GPU 利用率却只有 20%
打开任务管理器能看到 GPU 3D 引擎有一些占用,但 CPU 占用也不低,FPS 卡在十几帧上不去。
原因:视频解码、预处理和后处理都在 CPU 上跑,只有模型推理在 GPU 上跑。如果你的视频分辨率是 1080p 甚至 4K,每帧都要先从 H.264 解码出 YUV 转成 BGR,再做缩放、通道转换和归一化,这串操作比 GPU 推理本身还慢,瓶颈根本不在推理。
解决:一是把输入分辨率压低,比如摄像机画面先缩到 960x540 再进推理管线,让预处理时间缩短一大截;二是把预处理合并成一次矩阵操作,用 OpenCvSharp 的一次 Resize 和一次 ConvertTo 完成,而不是逐像素用At<Vec3b>赋值。上面 3.2 节那段逐像素逻辑是教学用的,实际项目里应该用矩阵操作替代,能差出 5 倍的时间。三是如果管线允许,把输入图像缓存为 GPU 张量,跳过 CPU 到 GPU 的拷贝,但这需要更深的 CUDA 介入,一般 Demo 不会做到这层。
6. 把 Demo 改造成能持续运行的视频检测器:模型热切换与稳定性验证
6.1 模型热切换与按需释放
Demo 程序往往是单模型跑完就退出,但实际项目里经常要换模型:上午跑安全帽检测,下午跑人员入侵,模型文件不同但程序不能重启。常见做法是封装一个ModelManager,在运行时把旧的InferenceSession释放,再加载新的。注意 GPU 显存里的旧模型资源不会立刻全部释放,GC 触发后才会真正回收,所以切换完后调用一次GC.Collect()配合WaitForPendingFinalizers,能大概率避免显存瞬时翻倍。
public class ModelManager : IDisposable { private InferenceSession _session; private readonly object _lock = new object(); public void Load(string onnxPath) { lock (_lock) { _session?.Dispose(); var opts = new SessionOptions(); opts.AppendExecutionProvider_CUDA(); _session = new InferenceSession(onnxPath, opts); } } public void Dispose() { lock (_lock) { _session?.Dispose(); } } }6.2 用日志验证 GPU 真的在干活
最后推荐一个最笨也最有效的验证方法:在每帧推理的前后打时间戳,算单帧耗时,连续跑 10 分钟,把耗时分布记录下来。如果单帧耗时曲线平直,说明 GPU 加速和内存管理都正常;如果耗时呈锯齿状爬升,说明有不释放的资源在累积。这比盯着任务管理器里的 GPU 利用率更直接,也更容易在后续接入业务系统时定位性能退化。这套跑下来,你就会意识到,所谓 GPU 版 Demo 真正值钱的不是那几行推理代码,而是把环境、资源和异常处理这些盘根错节的细节替你压实了。我自己的习惯是拿到任何源码先按这套流程推一遍,确认没有隐藏依赖之后再进业务改造,这套做法帮我在不少项目里避开了周五下班前的翻车,希望帮到你。
本文还有配套的精品资源,点击获取