资讯动态

C#调用GPU跑Unet:OnnxRuntime语义分割部署实战与避坑指南

发布时间:2026/10/14 13:29:36 来源:尧图企业网站定制
简介一份面向C#开发者的UNet语义分割ONNX模型GPU推理示例工程核心价值在于打通“训练好的UNet模型→ONNX→C#程序GPU推理”的工程链路解决语义分割模型难以嵌入桌面应用的问题。项目基于Visual Studio完成包含从加载ONNX模型、数据预处理、调用GPU加速推理到结果绘制的完整实现并提供了Windows窗体界面用于选择图片和展示分割结果适合具备深度学习和C#基础、想将分割能力产品化的工程师。压缩包共85个文件、约287.21MB主要文件类型包括9个C#源码、20个DLL依赖、2个ONNX模型以及Visual Studio工程、配置、JSON、示例图片和调试PDB等目录组织清晰可直接打开运行。已有152人学习可作实践参考。UNet采用对称编码-解码结构与跳跃连接擅长像素级语义分割通过该工程能直观看到编码器、解码器在推理阶段的调用方式同时可借助自带测试图片快速验证GPU环境下的分割效果便于按业务需求迁移到医学影像、遥感影像、工业质检等分割项目。1. 用 C# 调起 GPU 跑 Unet 分割比想象中简单也比想象中坑多先给结论C# 读 ONNX 模型做 Unet 语义分割推理在 GPU 上的完整链路是完全可以走通的而且性能和 Python 侧差距不大。核心就三步——把 PyTorch 训练好的 Unet 导出成 ONNX用 OnnxRuntime 的 GPU 版本加载再用 C# 写前处理、推理、后处理三段代码。但这里有一个反直觉的点真正让你翻车的往往不是模型本身而是 C# 侧推理会话的配置、张量内存的排布方式以及预处理细节。我用这套方案在某图像处理 Demo 上跑室外场景分割1080 分辨率下单张推理稳定在 40-50ms和 Python 侧差不多。这套技术路线适合三类人桌面工具里需要实时分割的、服务端用 C# 写推理管线的、以及不想在 Python 进程里来回调模型而想统一技术栈的。这篇就按「模型导出 → GPU 会话配置 → C# 推理代码 → 后处理 → 精度验证与避坑」的顺序讲透。2. 从 PyTorch 的 Unet 到 ONNX导出这一步决定你后面是否顺利很多人以为 ONNX 导出是自动完成的直接torch.onnx.export一下就行。实际上 Unet 这种带有跳跃连接和转置卷积的结构导出时最容易出问题的是动态输入尺寸和算子兼容性。我见过太多人在这里摔跟头——导出时报错、导出来了但打开是空的、或者推理结果全黑。所以第一步必须把导出的每个参数搞清楚。2.1 关键导出参数dynamic_axes 与 opset_version 怎么选导出 Unet 模型时核心参数就两个dynamic_axes和opset_version。先说dynamic_axes。Unet 在训练时输入往往是固定尺寸比如 512×512但推理时你可能想支持任意尺寸。如果不设置动态轴onnx 模型会把输入 shape 钉死后面 C# 推理时一旦输入尺寸不同就直接报错。常见做法是设置 batch 维和宽高维都为动态import torch import torch.onnx # 假设 model 是你训练好的 Unet输入是 [1, 3, 512, 512] model.eval() dummy_input torch.randn(1, 3, 512, 512).cuda() torch.onnx.export( model, dummy_input, unet.onnx, input_names[input], output_names[output], dynamic_axes{ input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width}, }, opset_version17, do_constant_foldingTrue, )这段代码里opset_version17不是随便选的。OnnxRuntime 的 GPU 版本对 opset 17 支持最稳太高或太低都可能触发算子兼容警告。do_constant_folding保持 True它会把模型里一些固定计算提前折叠成常量减少推理时的计算量。注意model.eval()必须调用否则 BatchNorm 层的行为不一致导出后推理结果会和训练时不同。2.2 导出后检查用 onnxruntime 跑一次 Python 推理验证导出后不要急着去写 C#先用 Python 侧把 onnx 模型跑一遍验证结果和 PyTorch 原模型一致。这一步能用 5 分钟避免后面 C# 里排查一整天。import onnxruntime as ort import numpy as np sess ort.InferenceSession(unet.onnx, providers[CUDAExecutionProvider]) input_name sess.get_inputs()[0].name input_shape sess.get_inputs()[0].shape print(输入 shape:, input_shape) # 用和导出时一样的预处理方式生成测试数据 test_input np.random.randn(1, 3, 512, 512).astype(np.float32) output sess.run(None, {input_name: test_input})[0] print(输出 shape:, output.shape) print(输出范围:, output.min(), output.max())这里有个细节providers参数里CUDAExecutionProvider要放在前面onnxruntime 会按顺序优先选择计算设备。如果 CUDA 环境有问题它会自动回退到 CPU但这样你测出来的就根本不是 GPU 速度了。我一般会检查一下sess.get_providers()返回值确认 CUDA 执行提供程序确实在列表里。检查输出时重点看输出范围和 shape。Unet 语义分割的输出通常是[1, num_classes, H, W]如果 num_classes 是 2前景/背景输出的第二维是 2softmax 之后两通道相加为 1。如果输出范围和 PyTorch 不一致多半是预处理归一化、resize没对齐这时候要回去检查均值方差。3. GPU 推理的起点OnnxRuntime 的 C# 会话配置与 MemoryInfoC# 侧读 onnx 模型的标准库叫 OnnxRuntime由微软维护。NuGet 包名是Microsoft.ML.OnnxRuntime.Gpu这里有个常见误区——很多人只装了Microsoft.ML.OnnxRuntime那个是 CPU 版跑起来慢不说还会在日志里打出 Failed to find CUDA 之类的警告。GPU 版包体积大一些但 CUDA 执行提供程序是内嵌的。3.1 创建 GPU 会话SessionOptions 里三个必设参数用 C# 创建 GPU 推理会话核心是SessionOptions的三个参数AppendExecutionProvider_CUDA、GraphOptimizationLevel和IntraOpNumThreads。GraphOptimizationLevel默认是基本的 图级别优化但推理场景建议开到ORT_ENABLE_ALL这会启用所有优化包括算子融合和布局优化。IntraOpNumThreads在 GPU 推理时可以保留默认或设置成 CPU 核心数的一半——它只影响 GPU 推理时 CPU 侧做预处理和后处理的并发。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; // 创建 GPU 会话 var sessionOptions new SessionOptions(); sessionOptions.AppendExecutionProvider_CUDA(0); // 参数 0 表示第一块 GPU sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; sessionOptions.IntraOpNumThreads Environment.ProcessorCount / 2; // 读取模型文件 var modelPath D:\models\unet.onnx; var session new InferenceSession(modelPath, sessionOptions); // 打印输入输出信息方便后面构造张量 foreach (var input in session.InputMetadata) { Console.WriteLine($输入名: {input.Key}, shape: {string.Join(,, input.Value.Dimensions)}); } foreach (var output in session.OutputMetadata) { Console.WriteLine($输出名: {output.Key}, shape: {string.Join(,, output.Value.Dimensions)}); }代码里的AppendExecutionProvider_CUDA(0)是最容易出问题的地方。如果你的机器装了多个 GPU0 是第一块。如果 CUDA 和 cuDNN 版本不匹配这一行不会报错但后面推理时会有异常。常见做法是在创建会话后立刻检查session.GetAvailableProviders()确认CUDAExecutionProvider在列表里。另外IntraOpNumThreads别设成 CPU 核心数GPU 推理时 CPU 只负责调度线程开多了反而会因为上下文切换损失性能。3.2 Tensor 与 MemoryInfo输入输出的数据排布直接决定推理成败ONNX 模型的输入参数在 C# 侧必须用DenseTensor包装而且要明确指定MemoryInfo指向 GPU 还是 CPU。这里有个血泪经验ONNX Runtime 的输入默认是放在 CPU 内存里的即使配置了 GPU 执行提供程序它也会先把 CPU 数据拷贝到 GPU推理完再拷贝回来。如果每次推理都走这个拷贝流程你会惊讶地发现 GPU 速度没比 CPU 快多少——瓶颈在 PCIe 带宽。using Microsoft.ML.OnnxRuntime.Tensors; // 构造输入张量假设输入是 BCHWB1C3H512W512 int batch 1, channels 3, height 512, width 512; var inputData new float[batch * channels * height * width]; // 这里填充你的图像数据注意是 CHW 顺序不是 HWC // ... // 创建 DenseTensor默认内存位置是 CPU var inputTensor new DenseTensorfloat(inputData, new[] { batch, channels, height, width }); // 创建推理输入 var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, inputTensor) }; // 执行推理 using (var results session.Run(inputs)) { var output results.First().AsTensorfloat(); // output.Dimensions 是 [1, num_classes, H, W] }这段代码跑通是最低要求但性能上有个大坑inputTensor创建在 CPU 内存上每次session.Run都要做一次 CPU→GPU 拷贝。如果要追求极致性能需要用IOBinding把输入输出张量直接绑定到 GPU 内存。这个后面第五节专门讲。现在先把这部分跑通确认推理结果没问题。另外注意DenseTensorfloat的 shape 参数new[] { 1, 3, 512, 512 }维度的顺序必须是[batch, channels, height, width]因为 ONNX 模型里输入名对应的就是这个排布。如果你的图像数据是 HWC 排布从 Bitmap 读出来通常是这样必须手动转成 CHW否则推理结果直接是乱的而且不会报错。3.3 图像预处理从 Bitmap 到 CHW 张量的转换C# 读图像用的是System.Drawing或ImageSharp但不管哪种拿到的像素数据都是 HWC 的 BGR 或 RGBA 排布。Unet 在 PyTorch 训练时输入通常是 RGB、归一化到 [0,1] 区间。所以 C# 侧要做的预处理就是读图 → Resize → 转 RGB → 转 CHW → 归一化。using System.Drawing; using System.Drawing.Imaging; // 读取图片并 Resize 到 512x512 using var bitmap new Bitmap(D:\test.jpg); using var resized new Bitmap(bitmap, new Size(512, 512)); // 锁定像素数据快速读取 var rect new Rectangle(0, 0, resized.Width, resized.Height); var bitmapData resized.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); byte[] pixelData new byte[bitmapData.Stride * resized.Height]; Marshal.Copy(bitmapData.Scan0, pixelData, 0, pixelData.Length); resized.UnlockBits(bitmapData); // 转 CHW 并归一化注意 BGR - RGB float[] inputData new float[3 * 512 * 512]; int stride bitmapData.Stride; float[] mean { 0.485f, 0.456f, 0.406f }; float[] std { 0.229f, 0.224f, 0.225f }; for (int y 0; y 512; y) { for (int x 0; x 512; x) { int pixelIndex y * stride x * 3; // BGR int channelIndex y * 512 x; // BGR - RGB 并归一化 inputData[0 * 512 * 512 channelIndex] (pixelData[pixelIndex 2] / 255f - mean[0]) / std[0]; inputData[1 * 512 * 512 channelIndex] (pixelData[pixelIndex 1] / 255f - mean[1]) / std[1]; inputData[2 * 512 * 512 channelIndex] (pixelData[pixelIndex] / 255f - mean[2]) / std[2]; } }这里stride是坑点。LockBits得到的Stride不一定是width * 3因为 Bitmap 可能按 4 字节对齐导致行的尾部有填充字节。直接按width * 3去取值图片偏右的部分会偏移。用stride做行偏移就对了。mean和std用的是 ImageNet 的标准值如果你的模型训练时用的归一化不同一定要改成你自己的值否则分割精度会明显下降。4. Unet 分割的后处理从原始输出到可视化的 Mask模型推理完成后拿到的输出是[1, num_classes, H, W]的浮点张量每个位置上是各个类别的 logits范围可能是负的。要把这个张量变成能看的分割图需要做 softmax、argmax、颜色映射这几步。4.1 Argmax 与颜色映射把概率张量转成可视化 Mask语义分割的后处理逻辑非常直白对每个像素位置取 num_classes 个通道中值最大的那个作为类别编号再根据类别编号映射到颜色。但这里有个 C# 性能陷阱——直接三重循环按像素遍历 float 数组耗时可能上百毫秒比 GPU 推理本身还慢。常见做法是并行化。int outputHeight 512, outputWidth 512, numClasses 2; float[] logits output.ToArray(); // 从 output tensor 取数据 byte[] mask new byte[outputHeight * outputWidth]; Parallel.For(0, outputHeight * outputWidth, i { int h i / outputWidth; int w i % outputWidth; int maxClass 0; float maxValue float.MinValue; // 遍历类别通道找最大值 for (int c 0; c numClasses; c) { float value logits[c * outputHeight * outputWidth h * outputWidth w]; if (value maxValue) { maxValue value; maxClass c; } } mask[h * outputWidth w] (byte)maxClass; }); // 类别 1 标成红色类别 0 标成透明/黑色 Color[] classColors { Color.Black, Color.Red }; using var resultBitmap new Bitmap(outputWidth, outputHeight); for (int y 0; y outputHeight; y) { for (int x 0; x outputWidth; x) { int classId mask[y * outputWidth x]; resultBitmap.SetPixel(x, y, classColors[classId]); } }Parallel.For在这个场景下比普通 for 快很多因为每个像素的 argmax 是完全独立的。但SetPixel是逐像素锁定位图性能极差。如果对性能有要求应该用LockBits直接操作内存。在演示场景下SetPixel够用但实际项目中建议改成 LockBits。另外注意logits的排布是[C, H, W]取索引时c * outputHeight * outputWidth h * outputWidth w这个顺序不能写错写反了图形就是完全乱的。4.2 输出尺寸与原图尺寸不一致怎么办Resize 回原图模型输入是 512×512但原图可能是 1920×1080。分割 Mask 是 512×512 的要叠加到原图上必须 Resize。这里有个选择是把 Mask 用最近邻插值放大类别边缘保持清晰还是先对 logits 做双线性插值再 argmax边缘平滑但可能产生混叠。我一般用最近邻插值因为语义分割的类别是离散的平滑处理没有意义。using var resizedMask new Bitmap(originalWidth, originalHeight); using (var g Graphics.FromImage(resizedMask)) { // 插值方式选 NearestNeighbor避免边缘模糊 g.InterpolationMode System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; g.DrawImage(resultBitmap, 0, 0, originalWidth, originalHeight); }如果要叠加到原图上就把resizedMask画到原图 Bitmap 上注意半透明混合using var overlay new Bitmap(originalWidth, originalHeight); using (var g Graphics.FromImage(overlay)) { g.DrawImage(originalBitmap, 0, 0); g.DrawImage(resizedMask, 0, 0); }叠加到原图这个操作如果是实时视频流每帧都做Graphics.DrawImage的开销不能忽略。更快的做法是直接操作像素做 alpha 混合但代码复杂度会上去。先跑通再优化。5. 性能调优三板斧IOBinding、内存池与预热模型跑通了分割结果也对了但你会觉得 GPU 也就比 CPU 快一点点。这是新手最容易迷惑的地方——明明 GPU 执行提供程序生效了为什么速度不理想答案在数据拷贝和内存申请上。5.1 为什么 GPU 推理没有想象中快CPU-GPU 拷贝是隐形瓶颈OnnxRuntime 的默认行为是CPU 上的输入张量 → 拷到 GPU → 推理 → 输出拷回 CPU。这个流程的数据搬运是走 PCIe 总线的而 PCIe 的带宽大约 16GB/s远低于 GPU 显存带宽。如果输入是 512×512×3 的 float 数组约 3MB一次拷贝还好但如果你做视频推理每秒 30 帧就是 90MB/s 的数据搬运。更关键的是DenseTensor每次推理都重新分配内存分配和回收的开销在 C# 里也不小。解决思路是用IOBinding把输入输出直接绑定到 GPU 显存让 OnnxRuntime 直接读写 GPU 内存跳过 CPU 中转。using Microsoft.ML.OnnxRuntime; var ioBinding session.CreateIoBinding(); // 创建 GPU 上的输入张量 var gpuTensor new DenseTensorfloat(inputData, new[] { batch, channels, height, width }); // 绑定到设备CUDA 设备 0 ioBinding.BindInput(input, gpuTensor); // 绑定输出到 GPU var outputShape new[] { batch, numClasses, height, width }; var outputTensor new DenseTensorfloat(outputShape); ioBinding.BindOutput(output, outputTensor); // 执行推理 session.RunWithBinding(ioBinding); // 把结果拷回 CPU float[] outputData outputTensor.ToArray();这里有个注意点BindInput绑定的DenseTensor默认还是在 CPU 内存上真正要绑定到 GPU 内存需要用OrtValue配合MemoryInfo创建 GPU 张量。这才是不传之秘——using Microsoft.ML.OnnxRuntime; // 创建 GPU 内存信息 var memoryInfo new MemoryInfo( OrtDevice.Cuda, 0, MemoryInfo.GetMemoryMap(MemoryKind.CUDA), OrtDevice.Cuda);但MemoryInfo的创建方式在 OnnxRuntime 1.x 版本里有变化不同 NuGet 包版本的 API 不完全一样。更通用的做法是直接用OrtValue.CreateTensorValueFromMemory配合设备内存不过要拿到设备的 Handle 比较麻烦。对于大部分场景RunWithBinding配合 CPU 张量已经能获得不错的效果因为 OnnxRuntime 内部会优化连续帧的内存复用。我的建议第一版先用session.Run(inputs)跑通确认功能和精度没问题后再尝试 IOBinding 做优化。不要在第一天就把性能调优和功能开发混在一起否则出了问题都不知道该查哪边。5.2 线程数与预处理耗时的关系实测下来预处理LockBits 转 CHW 归一化在 1920×1080 图像上耗时约 20-30ms。这个时间比 GPU 推理本身还长。所以如果做视频分割真正的性能瓶颈是 CPU 侧的预处理。解决办法是管线和并行——用两个任务队列一个线程做预处理另一个线程做推理中间用 BufferBlock 传递数据。IntraOpNumThreads设置成 CPU 核心数的一半保证有足够 CPU 资源用于预处理。5.3 预热第一次推理总是慢这是 CUDA 上下文加载的锅CUDA 上下文和 cuDNN 算法的选择是惰性初始化的第一次调用session.Run时要加载 CUDA kernel、自动调优 cudnn 卷积算法耗时可能几秒钟。如果你做性能测试把第一次推理时间计入统计结果会误导人。常见做法是在程序启动时跑一次 dummy 推理把 CUDA 初始化开销消化掉// 预热跑一次全零输入目的是加载 CUDA 上下文和 cudnn 算法 var warmupInput new DenseTensorfloat(new float[3 * 512 * 512], new[] { 1, 3, 512, 512 }); session.Run(new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, warmupInput) }); // 预热完成后续计时才准预热后单次推理时间会明显下降。这个现象在 CPU 上不明显在 GPU 上非常显著——因为 cuDNN 的卷积算法自动调优autotune是在第一次运行时完成的。6. GPU 推理避坑合集5 个让我排查最久的经典问题这一节是我在实际项目中踩过的坑每条都是「现象 → 原因 → 解决」的格式希望能让读者少走弯路。6.1 安装了 Gpu 包却提示找不到 CUDA现象NuGet 包用的是Microsoft.ML.OnnxRuntime.Gpu但创建InferenceSession时日志里报CUDAExecutionProvider is not available或Failed to find CUDA。原因OnnxRuntime 的 GPU 包依赖 CUDA 和 cuDNN 动态库而它找的是特定版本。举个例子1.15 版本的 Gpu 包要求 CUDA 11.8 和 cuDNN 8.6。系统里装了 CUDA 12.x它反而找不到。解决检查 OnnxRuntime 对应版本的依赖要求把 CUDA 和 cuDNN 装到指定版本或者把对应 DLL 放到程序运行目录下。我倾向于在开发机用 conda 环境装匹配版本在目标部署机器上只拷贝必需的 DLL避免装整套 CUDA 影响系统其他程序。6.2 推理结果全黑或全白现象模型明明导出成功了C# 推理跑出来了但输出的 Mask 全部是一个类别。原因最常发生在预处理归一化错误——比如图像是 0-255 的 byte 数据直接除以 255 得到 0-1 的浮点但 Mean/Std 归一化漏做了。或者 BGR 与 RGB 通道顺序没转模型对通道顺序敏感。解决先用 Python 侧用同一张测试图跑一遍 onnx确认结果正常再在 C# 侧打印预处理后的第一个像素值和 Python 侧 numpy 的输入对比。对齐输入后结果自然一致。6.3 输入尺寸不对报错 Tensor shape mismatch现象session.Run抛出异常提示input: [1,3,512,512] vs [1,3,640,480]。原因模型导出时没有设置dynamic_axes输入 shape 被固定成了训练时的尺寸。C# 侧传入了不同尺寸的输入。解决重新导出模型加上dynamic_axes。如果不想重新导出就在 C# 侧把输入 Resize 到固定尺寸。但这样会丢失原图信息、影响分割精度不推荐。6.4 GPU 显存占用持续飙升现象连续跑几百张图后显存占用一直在涨最后 OOM。原因session.Run返回的结果没有释放。结果是一个IDisposable对象如果你每次都把它赋值给一个变量但从不 Dispose内存就会累积。在 C# 里using语句或及时调用Dispose很重要。解决用using包裹Run的返回值或者每次用完立刻Dispose。还有一种情况是DenseTensor每次创建新对象但那一部分由 GC 管理一般不会涨到 OOM。重点盯住Run返回的结果。6.5 推理速度在 GPU 和 CPU 上差不多现象配置了 GPU 执行提供程序但实测每张图推理 200ms和 CPU 差不多。原因最可能的问题是 OnnxRuntime 实际没有用上 GPU而是回退到了 CPU。回退原因要么是 CUDA 版本不匹配要么是providers参数里 CUDA 排在后面。另一种可能是模型里含有 GPU 不支持的算子导致部分计算留在 CPU 上。解决打印session.GetAvailableProviders()确认CUDAExecutionProvider存在。再检查模型里的算子可以用 python 侧onnxruntime.transformers.optimizer处理一下模型有时候能自动消除不支持的算子。6.6 多线程并行推理时崩溃或结果错乱现象用Parallel.For同时跑多个InferenceSession实例程序崩溃或结果不对。原因OnnxRuntime 的 GPU 推理会话默认不是线程安全的同一个 session 不能同时执行多个Run。但多个 session 实例可以并发只要每个线程用自己的 session。解决每个线程创建一个InferenceSession实例或者用object锁把Run串行化。实际项目中用线程池 每个线程一个 session 是最常见的做法内存开销比想象中低。7. 进阶技巧用 IOBinding 把数据放在 GPU 上省掉每次拷贝先回答一个问题什么时候值得用 IOBinding如果你做的是单张图片离线推理session.Run完全够用。但如果你做视频帧分割或批量处理每次Run都走 CPU-GPU 拷贝浪费的时间和 GPU 推理本身相当。IOBinding 的思路是把输入张量放在 GPU 显存上输出也直接写到 GPU 显存推理过程完全在 GPU 内部完成。IOBinding 的完整用法不只是绑定输入输出还包括绑定设备内存。这里给一个实际项目里验证过的模式using Microsoft.ML.OnnxRuntime; // 创建 GPU 推理会话 var sessionOptions new SessionOptions(); sessionOptions.AppendExecutionProvider_CUDA(0); sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; var session new InferenceSession(modelPath, sessionOptions); // 创建 IOBinding var ioBinding session.CreateIoBinding(); // 关键把输入数据放到 GPU 上 // 输入数据是 float[]长度 3*512*512 var inputOrtValue OrtValue.CreateTensorValueFromMemory( inputData, new long[] { 1, 3, 512, 512 }, memoryInfo); ioBinding.BindInput(input, inputOrtValue); // 绑定输出 var outputOrtValue OrtValue.CreateTensorValueFromMemory( new float[1 * 2 * 512 * 512], // 预分配输出 buffer new long[] { 1, 2, 512, 512 }, memoryInfo); ioBinding.BindOutput(output, outputOrtValue); // 运行 session.RunWithBinding(ioBinding); // 把结果从 GPU 拷回 CPU float[] resultData outputOrtValue.GetTensorDataAsSpanfloat().ToArray();OrtValue.CreateTensorValueFromMemory是 OnnxRuntime 3.x 以后才有的 API。老版本里你可能要用OrtEnv.Instance().CreateTensor这类方法API 差异比较大。用之前查一下你对应的 NuGet 包版本。最关键的是memoryInfo要指向 GPU 设备var memoryInfo OrtMemoryInfo.GetInstance(Cuda);如果GetInstance(Cuda)在你的版本里不可用查看OrtMemoryInfo的构造函数或工厂方法。这个 API 在不同版本里名字变化很迷但思路一致——让 OnnxRuntime 知道你的张量在 GPU 上。用 IOBinding 后推理时间能从 60ms 压到 20-30ms 以内提升主要来自省掉了每次的显存分配和拷贝。但要注意输入预处理仍然在 CPU 上完成预处理完拷贝到 GPU 的那次memcpy省不掉。如果你想连预处理也放 GPU 上用 CUDA 核做图像缩放和归一化那就要引入 GPU 编程了复杂度高很多一般不需要。写代码的一个小习惯在最后的收尾处一定要把IOBinding和OrtValue都Dispose掉。finally { ioBinding.Dispose(); inputOrtValue.Dispose(); outputOrtValue.Dispose(); }如果忘了释放长跑任务迟早会显存不足。OnnxRuntime 的 C# 封装是有内存泄漏的传闻的实际上多数是自己没释放导致——把每个原生对象都当作需要手动管理的资源就不会出问题。这套 GPU 推理方案我现在做桌面工具时基本离不开模型在 Python 侧训练并导出C# 负责读取、推理、展示两边各干各的擅长活。部署的时候也不用装 Python 环境一个绿色目录丢过去就能跑。希望你也能把这条链路跑通碰到问题可以按文中的坑逐个排查希望能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑