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

1次阅读
没有评论

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

image.webp

背景痛点:为什么 C# 跑 YOLO 这么慢?

最近在做一个工业质检项目,需要把 YOLOv5 部署到 C# 开发的 Windows 客户端上。刚开始直接用 Python 转 ONNX 再调 ONNX Runtime,发现两个致命问题:

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

  • 每帧推理都要重新分配内存,GC 频繁触发导致卡顿
  • GPU 利用率不到 30%,大部分时间耗在数据传输上

用 VS 的性能诊断工具抓取数据后,发现三个典型瓶颈:

  1. 每帧创建新的 Tensor 对象,引发 GC 频繁回收(约占用 15% 时间)
  2. CPU 到 GPU 的数据拷贝没有批处理(占时 40%)
  3. 后处理的非极大值抑制 (NMS) 跑在 CPU 上(占时 25%)

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

测试环境:i7-11800H + RTX 3060 Laptop

方案 易用性 峰值吞吐量(FPS) 显存占用(MB) 适用场景
ONNX Runtime(CPU) ★★★★☆ 8.2 120 无 GPU 的老旧设备
ONNX Runtime(CUDA) ★★★☆☆ 34.7 2100 需要快速验证原型
DirectML ★★★★☆ 28.5 1800 多显卡异构环境
TensorRT ★★☆☆☆ 52.3 1600 生产环境长期运行

实际选择建议:

  • 开发阶段用 ONNX Runtime 快速迭代
  • 部署时换 TensorRT(需转换模型格式)
  • 多显卡笔记本优先考虑 DirectML

核心实现:从模型加载到推理优化

1. 用 ML.NET 加载 ONNX 模型的最佳姿势

注意.NET 6+ 的环境依赖:

// 必须引用这两个 NuGet 包
// Microsoft.ML.OnnxRuntime 1.14+
// Microsoft.ML.OnnxRuntime.Gpu 1.14+

var options = new SessionOptions
{
    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
    EnableMemoryPattern = true, // 关键!减少内存碎片
    ExecutionMode = ExecutionMode.ORT_PARALLEL
};

// 显式指定 CUDA 设备
options.AppendExecutionProvider_CUDA(0); 

using var session = new InferenceSession("yolov5s.onnx", options);

2. 多线程批处理实现(带内存池)

class BatchInference : IDisposable
{
    private readonly InferenceSession _session;
    private readonly FixedSizeMemoryPool<DisposableNamedOnnxValue> _memoryPool;

    public BatchInference(string modelPath, int batchSize)
    {_session = new InferenceSession(modelPath);
        _memoryPool = new FixedSizeMemoryPool<DisposableNamedOnnxValue>(batchSize * 2);
    }

    public IList<IDisposableReadOnlyCollection<DisposableNamedOnnxValue>> RunBatch(IEnumerable<float[]> inputs)
    {var results = new ConcurrentBag<IDisposableReadOnlyCollection<DisposableNamedOnnxValue>>();

        Parallel.ForEach(inputs, input =>
        {using var rented = _memoryPool.Rent();
            var tensor = new DenseTensor<float>(input, new[] {1, 3, 640, 640});
            var inputValues = new List<NamedOnnxValue> 
            {NamedOnnxValue.CreateFromTensor("images", tensor) 
            };

            var result = _session.Run(inputValues);
            results.Add(result);
        });

        return results.ToList();}

    public void Dispose()
    {_session.Dispose();
        _memoryPool.Dispose();}
}

关键点说明:

  • 使用对象池复用 DisposableNamedOnnxValue
  • Parallel.ForEach 自动处理线程调度
  • 输入张量维度必须与模型匹配(这里是 1×3×640×640)

3. SIMD 加速后处理

NMS 优化的核心代码:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void FastNMS(Span<float> boxes, Span<float> scores, float iouThreshold)
{if (Vector.IsHardwareAccelerated)
    {var simdThreshold = new Vector<float>(iouThreshold);
        for (int i = 0; i < boxes.Length - Vector<float>.Count; i += Vector<float>.Count)
        {var current = new Vector<float>(boxes.Slice(i, Vector<float>.Count));
            // SIMD 并行计算 IOU...
        }
    }
    else
    {// 回退到普通循环}
}

性能测试:优化前后的硬核数据

测试配置:
– 模型:YOLOv5s 640×640
– 硬件:RTX 3060 + 16GB RAM
– 测试工具:BenchmarkDotNet v0.13.2

优化阶段 平均延迟(ms) 吞吐量(FPS) 显存占用(MB)
初始版本 38.2 26.2 2100
+ 内存池 29.7 ↓22.3% 33.7 ↑28.6% 1800 ↓14.3%
+ 批处理(4 帧) 18.4 ↓51.8% 54.3 ↑107% 2200 ↑4.8%
+SIMD 后处理 15.1 ↓60.5% 66.2 ↑153% 2200
+TensorRT 9.8 ↓74.3% 102 ↑289% 1600 ↓23.8%

避坑指南:血泪经验总结

1. Tensor 生命周期管理

错误示例(引发内存泄漏):

// 错误!result 实现了 IDisposable 但没释放
var result = session.Run(inputs); 
var predictions = result.First().AsTensor<float>();

正确做法:

using var result = session.Run(inputs);
var predictions = result.First().AsTensor<float>();
// 注意:predictions 的生命周期不能超过 result

2. 多 GPU 设备绑定

当有多个 GPU 时,必须显式指定设备:

var options = SessionOptions.MakeSessionOptionWithCudaProvider(deviceId: 1);

3. ONNX 模型输入校验

建议在加载模型时检查输入输出:

var inputMeta = session.InputMetadata;
if (!inputMeta.TryGetValue("images", out var input) || 
    input.Dimensions[2] != 640)
{throw new InvalidOperationException("模型输入尺寸不匹配");
}

延伸思考:走向生产级部署

如果项目需要更高并发,可以考虑:

  1. 使用 NVIDIA Triton 推理服务器,通过 gRPC 调用
  2. 将预处理移到 GPU(用 CUDA 实现)
  3. 量化模型到 FP16/INT8(TensorRT 自动完成)

给初学者的建议路线:

  1. 先用 ONNX Runtime 跑通流程
  2. 加入内存池和多线程
  3. 最后上 TensorRT 优化

经过两周的调优,我们的工业质检系统最终实现了单机 120FPS 的稳定推理性能。关键收获是:C# 做 AI 部署不像 Python 那样有现成轮子,但通过合理利用运行时特性,完全可以达到生产级性能要求。

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