共计 2419 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:为什么你的 YOLO 在 C# 里跑得慢
最近在做一个工业质检项目,需要把训练好的 YOLOv5 模型部署到 C# 开发的工控系统里。本以为直接调用 ONNX 模型就行,结果发现单帧推理要 300ms,CPU 占用直接飙到 90%。排查后发现几个典型问题:

- GPU/CPU 切换开销:每次推理都要把数据从 CPU 拷贝到 GPU,结果后处理又得拷回来
- 内存重复分配 :每帧都新建 byte[] 存放图像数据,触发频繁 GC
- 单线程阻塞:UI 线程直接调用推理,界面卡成 PPT
技术选型:Windows 平台的加速方案对比
折腾了两周后,我测试了三种主流方案:
- ONNX Runtime(推荐新手首选)
- 优点:官方维护,支持 CPU/GPU/DML 多种 EP,.NET 封装完善
-
缺点:默认配置下不会自动启用全部优化
-
DirectML(AMD/Intel 显卡友好)
- 优点:微软亲儿子,Win10+ 系统开箱即用
-
缺点:算子支持不如 CUDA 全面
-
TensorRT(NVIDIA 显卡终极方案)
- 优点:极致优化,实测比 ONNX Runtime 快 2 - 3 倍
- 缺点:需要额外转换模型,依赖 CUDA 环境
核心实现:从加载模型到多线程优化
1. 用 ML.NET 加载 ONNX 模型
// 注意:需要安装 Microsoft.ML.OnnxRuntime >= 1.14.0
var options = new SessionOptions()
{
GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
EnableMemoryPattern = true // 启用内存复用
};
// 显式指定 GPU(比如第二块显卡)options.AppendExecutionProvider_DML(1);
using var session = new InferenceSession("yolov5s.onnx", options);
2. 内存复用 + 多线程批处理
关键技巧:用 NativeMemory 替代 byte[],配合SafeHandle 实现安全释放:
class ImageBuffer : SafeHandleZeroOrMinusOneIsInvalid
{public ImageBuffer(int size) : base(true)
{SetHandle((IntPtr)NativeMemory.Alloc((nuint)size));
}
protected override bool ReleaseHandle()
{NativeMemory.Free(handle.ToPointer());
return true;
}
}
// 多线程示例(注意:.NET 6+ 才支持 Parallel.For 的 Async 重载)await Parallel.ForEachAsync(imageFiles, async (file, ct) =>
{using var buffer = new ImageBuffer(1920 * 1080 * 3);
await LoadImageAsync(file, buffer);
lock (inferenceLock)
{var outputs = session.Run(new[] {NamedOnnxValue.CreateFromTensor("images", tensor) });
ProcessOutputs(outputs);
}
});
3. SIMD 加速后处理
YOLO 的输出解析涉及大量矩阵运算,用 System.Numerics 优化关键代码:
// 需要开启 AllowUnsafeBlocks
private unsafe void DecodeOutput(float* output, int length)
{Vector<float> confThreshold = new Vector<float>(0.5f);
for (int i = 0; i < length; i += Vector<float>.Count)
{var conf = new Vector<float>(output + i);
if (Vector.GreaterThanAll(conf, confThreshold))
{// 命中条件的检测框处理...}
}
}
性能测试:实测数据对比
用 BenchmarkDotNet 测试同一张 RTX 3060 显卡下的表现(单位:ms):
| 方案 | 显存占用 | 平均延迟 | 吞吐量(fps) |
|---|---|---|---|
| ONNX CPU | 0MB | 152 | 6.5 |
| ONNX+DML 默认 | 1243MB | 89 | 11.2 |
| ONNX+DML 优化后 | 983MB | 46 | 21.7 |
| TensorRT(fp16) | 572MB | 22 | 45.4 |
避坑指南:血泪教训总结
-
Tensor 生命周期:千万不要在 using 块外保存
DisposableTensor,否则 GC 时会导致显存泄漏 -
多 GPU 绑定 :当服务器有多张显卡时,务必用
CUDA_VISIBLE_DEVICES环境变量限制可见设备 -
模型输入校验:ONNX 模型的输入名称可能因导出方式不同而变化,建议用 Netron 查看:
var input = session.InputMetadata;
Console.WriteLine(string.Join(",", input.Keys)); // 输出类似:images
延伸思考:还能更快吗?
如果项目需要部署到多台设备,可以试试 NVIDIA Triton 推理服务器:
- 支持动态批处理(自动合并多个请求)
- 提供 HTTP/gRPC 接口,方便 C# 调用
- 能同时托管 ONNX/TensorRT 等多种模型
最近在测试中发现,用 Triton+TensorRT 的组合,吞吐量还能再提升 40% 左右。不过这个方案更适合云端部署,本地开发还是先用 ONNX Runtime 更省心。
最后说点心里话
其实 YOLO 加速没有银弹,我的经验是:先确保代码没有明显性能问题(比如内存泄漏),再考虑上 TensorRT 这种大杀器。另外 GPU 利用率不是越高越好,我们遇到过 90% 利用率但实际吞吐下降的情况,后来发现是 PCI- E 带宽成了瓶颈。建议边优化边用 Nsight 工具分析,避免无效优化。
