C#生产环境加载ONNX模型首选Microsoft.ML.OnnxRuntime,需安装对应平台原生包(如Microsoft.ML.OnnxRuntime.Gpu),路径用绝对路径,输入输出严格匹配shape/dtype/布局,优先使用Span<float>避免内存拷贝。

用 Microsoft.ML.OnnxRuntime 加载 .onnx 模型最稳
直接上结论:C# 生产环境跑 ONNX 模型,首选 Microsoft.ML.OnnxRuntime(简称 ORT),不是 ONNXRuntime.Managed,也不是自己手写 Tensor 解析。前者是微软官方维护、跨平台、支持 GPU/CPU/MLAS 多后端的 C++ 核心封装,后者纯托管实现性能差、算子支持少、更新滞后。
常见错误现象:System.DllNotFoundException: onnxruntime.dll —— 这是因为没装原生运行时依赖,只 NuGet 了 Microsoft.ML.OnnxRuntime 包还不够,得确保对应平台的 onnxruntime 本地库在运行时路径下。
- 安装时选对包:
Microsoft.ML.OnnxRuntime(CPU)或Microsoft.ML.OnnxRuntime.Gpu(CUDA 11.x/12.x,需匹配显卡驱动) - 不推荐
ONNXRuntime.Managed:它连Softmax的 axis 参数都常解析错,模型一复杂就输出 NaN - Windows 下若报 DLL 找不到,检查
bin/Debug/net6.0/runtimes/win-x64/native/是否存在onnxruntime.dll;Linux/macOS 同理看对应runtimes/子目录
InferenceSession 初始化必须传对路径和选项
模型加载失败往往不是模型本身有问题,而是 InferenceSession 构造时参数没对齐。尤其注意路径是否含中文、空格、特殊符号——.NET 的 FileStream 在某些版本下会静默截断路径。
使用场景:模型文件在项目外(如用户上传、配置指定)、或部署到容器中路径动态拼接时,最容易栽在这里。
- 路径务必用绝对路径:
Path.GetFullPath("model.onnx"),别信相对路径在所有环境下都可靠 - 启用日志调试加
new SessionOptions { LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_INFO },能看到加载时是否识别出输入输出节点 - CPU 推理默认用
ExecutionMode.ORT_SEQUENTIAL;多线程预测要手动设sessionOptions.ExecutionMode = ExecutionMode.ORT_PARALLEL,否则吞吐上不去
输入数据喂不对,Run() 直接抛 OnnxRuntimeException
ONNX 模型对输入张量的 shape、dtype、内存布局(row-major vs column-major)极其敏感。Run() 报错信息里带 "Invalid input shape" 或 "Data type mismatch" 是最常见两类问题。
参数差异关键点:Python PyTorch/TensorFlow 导出的模型,默认输入是 float32、NHWC 或 NCHW 布局,C# 里用 float[] 创建 OrtValue 时容易忽略维度顺序和 channel 位置。
- 先用
session.InputMetadata看清期望 shape,比如{"input": {Type: "tensor(float)", Shape: [1,3,224,224]}}→ 要填 NCHW,不是 NHWC - 创建输入用
OrtValue.CreateTensorValueFromMemory(),别用CreateTensor再拷贝——后者多一次内存分配,还可能触发 GC 干扰实时性 - 图像预处理后的 float 数组,必须按模型要求 reshape 成
new long[]{1,3,224,224},不能只传new long[]{224*224*3}然后指望 ONNX 自动推
输出结果取值别直接读 GetTensorDataAsFloats()
看似方便的 GetTensorDataAsFloats() 方法,在大 tensor(比如分割模型输出 [1,21,512,512])上会触发完整内存拷贝,实测比原地指针访问慢 3–5 倍,且 GC 压力陡增。
性能影响明显:单次推理耗时从 8ms 拉到 35ms,批量推理时延迟毛刺频发。
- 优先用
outputTensor.GetTensorShape()+outputTensor.GetTensorMemoryBuffer()获取原始Span<float> - 如果必须转数组,至少用
ToArray()替代ToList().ToArray()这种低效链式调用 - 注意
GetTensorMemoryBuffer()返回的是未托管内存,别在using块外长期持有引用,否则可能访问已释放区域
模型输入输出节点名、shape、dtype 这些信息,靠猜不如用 Netron 打开 .onnx 文件确认一眼。很多“跑不通”的问题,其实只是把 input.1 当成了 input,或者把 logits 和 probabilities 输出搞反了。










