C# YOLO推理加速实战:从模型加载到性能优化的完整指南

1次阅读
没有评论

共计 2419 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

背景痛点:为什么你的 YOLO 在 C# 里跑得慢

最近在做一个工业质检项目,需要把训练好的 YOLOv5 模型部署到 C# 开发的工控系统里。本以为直接调用 ONNX 模型就行,结果发现单帧推理要 300ms,CPU 占用直接飙到 90%。排查后发现几个典型问题:

C# YOLO 推理加速实战:从模型加载到性能优化的完整指南

  • GPU/CPU 切换开销:每次推理都要把数据从 CPU 拷贝到 GPU,结果后处理又得拷回来
  • 内存重复分配 :每帧都新建 byte[] 存放图像数据,触发频繁 GC
  • 单线程阻塞:UI 线程直接调用推理,界面卡成 PPT

技术选型:Windows 平台的加速方案对比

折腾了两周后,我测试了三种主流方案:

  1. ONNX Runtime(推荐新手首选)
  2. 优点:官方维护,支持 CPU/GPU/DML 多种 EP,.NET 封装完善
  3. 缺点:默认配置下不会自动启用全部优化

  4. DirectML(AMD/Intel 显卡友好)

  5. 优点:微软亲儿子,Win10+ 系统开箱即用
  6. 缺点:算子支持不如 CUDA 全面

  7. TensorRT(NVIDIA 显卡终极方案)

  8. 优点:极致优化,实测比 ONNX Runtime 快 2 - 3 倍
  9. 缺点:需要额外转换模型,依赖 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

避坑指南:血泪教训总结

  1. Tensor 生命周期:千万不要在 using 块外保存DisposableTensor,否则 GC 时会导致显存泄漏

  2. 多 GPU 绑定 :当服务器有多张显卡时,务必用CUDA_VISIBLE_DEVICES 环境变量限制可见设备

  3. 模型输入校验: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 工具分析,避免无效优化。

正文完
 0
评论(没有评论)