SimdYolo:纯 C# YOLO 推理库,CPU SIMD / Vulkan 架构与 PyTorch、ORT 实测对比
SimdYolo:纯 C# YOLO 推理实现,CPU SIMD / Vulkan 架构与 PyTorch、ORT 实测 - 中国.NET研究协会
纯托管 C# 的 YOLO 推理库 SimdYolo 发布 0.7.1,可直接解析 .pt 和 ONNX,CPU 走 SIMD/intrinsics、GPU 通过系统 Vulkan 执行嵌入 SPIR-V,无需 CUDA Toolkit、cuDNN 或 cuBLAS。

项目仓库: https://github.com/hnlyf168/SimdYolo
我实现了一个面向 .NET 应用的 YOLO 推理库 SimdYolo:C# 直接解析支持范围内的 .pt 和 ONNX,CPU 使用 SIMD/intrinsics,GPU 通过系统 Vulkan、Metal API 执行嵌入着色器。目前已验证常见 YOLOv8、YOLOv9、YOLOv10、YOLO11 的检测模型,以及 YOLOv8、YOLO11 的分割和姿态模型。
本文介绍模型加载、CPU/GPU 执行路径、同机性能排名和复测方法。项目主库为纯托管 C# 程序集,没有外部 NuGet 运行依赖;GPU 路径会调用系统原生图形 API。Python、PyTorch 和 ORT 用于开发时的导出与对照,主库推理不调用它们。
当前发布版本为 0.7.1,性能表对应 0.7.0 冻结 DLL。 0.7.1 增补中文注释与 XML 文档;方法签名和 IL 共 1,636 个与 0.7.0 一致。本文沿用已有正式测量数据,不把旧测量写成新一轮基准。
1. 模型兼容范围
已验证范围如下,其他尺寸和自训练模型应结合模块、算子及导出结构单独验证:
| 模型系列 | 已验证检测模型 | 已验证分割/姿态 | 已验证格式 |
|---|---|---|---|
| YOLOv8 | yolov8n | yolov8n-seg、yolov8n-pose | .pt、opset 17 ONNX |
| YOLOv9 | Ultralytics yolov9t | 当前未验证 | .pt、opset 17 ONNX |
| YOLOv10 | yolov10n | 当前未验证 | .pt、opset 17 ONNX |
| YOLO11 | yolo11n | yolo11n-seg、yolo11n-pose | .pt、opset 17 ONNX |
YOLOv9 这里特指实际参与测试的 Ultralytics YOLOv9t。原 YOLOv9 仓库的部分模块也有实现,但原仓库检查点仍需单独验证,不能按模型文件名推断兼容性。
.pt:恢复执行结构,再读取权重
PtModel 与 PickleReader 在 C# 中读取 Torch ZIP 的元数据和 storage,恢复支持的模块连接与权重。GLOBAL、REDUCE 按数据或符号对象处理,不导入或执行 Python。支持 FP32、FP16、BF16 storage,内部计算使用 FP32。
检查点需要保存完整 YOLO nn.Module,例如 model 或 ema,也支持直接保存完整模型。只有 state_dict 时缺少执行结构,当前加载器不能自动推断任意自定义网络。 TorchScript、旧式非 ZIP pickle、未知自定义模块和量化图不在当前支持范围内。
ONNX:在 C# 中解析图,而后执行已支持算子
OnnxReader 解析 protobuf,OnnxModel 构建和执行算子图,支持动态输入及相对模型目录解析的 external data。普通 ai.onnx 域中已实现的算子覆盖主要 YOLO 路径;opset 的版本范围不等于该版本内所有算子均受支持。
包含未知算子、其他域、内嵌 NonMaxSuppression 的导出,以及分类/OBB 等任务需要按当前实现范围判断。Resize 主要覆盖 YOLO 常用的 nearest/asymmetric/floor 路径。通用 shape/index 运算使用 FP32,也有精确整数范围限制。

2. CPU 优化:算力与数据访问要一起处理
源码可以从以下入口读起:
| 文件 | 主要职责 |
|---|---|
| CpuConvolution.cs | 通用卷积空间面板打包与实际地址对齐调度 |
| CpuGemm.cs | AVX/FMA 分块 GEMM 与回退路径 |
| CpuPackedGemm.cs | 固定权重布局缓存 |
| CpuActivation.cs | 向量指数、Sigmoid 和 SiLU |
| CpuExecutor.cs | 后端专属工作线程、任务发布和休眠交接 |
| CpuTensorPool.cs | 临时张量复用与输出所有权 |
2.1 通用卷积按需打包空间面板
通用卷积每次物化当前微内核需要的 16 个空间位置,将输入打包成 K×16 面板,再执行矩阵乘法。这样可以减少完整 im2col 中间矩阵的分配和内存往返。
适用的固定权重可缓存打包布局。微内核写入结果后补加 bias,并在需要时融合 SiLU,减少单独遍历输出。适用的 Concat→1×1 Conv 还可以从多个输入直接打包,省去完整拼接张量。
矩阵块宽度需要结合寄存器、尾部和调度成本选择。项目保留适用的 16/24/48 列等路径;某些更宽内核、F6 或分块调参没有表现出稳定收益,没有设为默认发布路径。
2.2 适用卷积与激活使用 SIMD
适用的 3×3 卷积使用 F2/F4 Winograd;输入、输出变换与边界有 SIMD 路径。depthwise、池化、转置和常见激活也有专用实现。
CpuActivation 的常见有限值指数路径使用 FP32 范围缩减、多项式近似和 AVX2/FMA。极值、NaN 或硬件条件不满足时保留原有回退行为;数值误差由输出对照测试约束。CPU 计算仍为 FP32,没有把结果当作 FP16/INT8 加速成绩。
.NET 提供 Vector<T>、固定宽度向量和硬件 intrinsics 等层次的 SIMD API。项目根据具体路径选择 API,并检查硬件支持;不同 CPU 上的实际性能仍需测量。Microsoft 的 SIMD 文档介绍了这些 API 的分工。
2.3 实际地址对齐与线程预算
并行写入时,相邻任务可能竞争同一缓存行。通用卷积的适用路径会在 fixed 作用域中读取输出数组的实际地址,按 64 字节边界划分任务,并把首尾碎片交给同一线程。
数组索引按 16 个 float 对齐,仍需考虑底层数组地址。实现以实际地址为准,并通过固定作用域维持地址有效性。
CPU 后端拥有专属执行器,调用线程也参与计算、计入线程上限;工作线程在有界自旋后休眠。任务发布、休眠和唤醒之间使用完整内存屏障,并通过强制休眠压力测试检查交接行为。内部嵌套计算也要避免扩张线程数量。

图中展示实现思路,各优化项没有在本文中被单独测量收益。历史版本耗时变化也不能全部归因于某个内核。
2.4 临时池与公开输出的生命周期
中间张量按计算图存活引用及时回收。固定权重与形状计划可缓存,但缓存受到适用条件和保留上限约束。
公开输出需要脱离临时池:下一帧不能覆盖上一帧的返回结果,调用方修改输出也不能破坏模型常量。内部设备分片可以共享底层存储,但必须遵守帧、后端和偏移约束;这种内部借用视图与公开结果的所有权不同。
3. 为什么 GPU 选择 Vulkan:部署、可移植性与执行成本
性能通过实测评估,部署依赖和设备覆盖也是选择后端时的重要因素。SimdYolo 的 Vulkan 路径直接调用系统 API,配合嵌入的 SPIR-V,减少额外计算运行库的分发与配置。
3.1 运行不依赖 CUDA Toolkit、cuDNN 或 cuBLAS
使用本库的 Vulkan GPU 推理,无需安装 CUDA Toolkit,也无需打包 cuDNN、cuBLAS 等 CUDA 计算库。 模型解析、计算调度由 C# 实现,GPU 算子由项目自己的着色器执行,省去维护 CUDA 计算库版本组合的成本。
库已包含预编译 SPIR-V,普通构建和运行无需 Vulkan SDK。只有修改 GLSL 着色器并重新编译时,才需要 glslang 等开发工具。Vulkan loader 与兼容显卡驱动仍然是必要条件;Windows 通常随显卡驱动提供 Vulkan 支持,Linux 则需确保 loader 与设备驱动已安装。NVIDIA 的驱动说明也列出了 Windows/Linux 的 Vulkan 支持。
0.7.1 的 NuGet 包为 726,875 字节,约 710 KiB,包含库、中文文档和嵌入着色器;模型、.NET 运行时及显卡驱动不包含在这个数字里。具体文件和依赖清单见 包审计。
需要准确区分 CUDA 的开发与运行环境:CUDA 应用可以分发所需运行库,部分框架也会携带依赖,因此“每个 CUDA 程序都必须安装完整 Toolkit”并不成立。这里描述的是 SimdYolo 自身的部署优势——Vulkan 路径没有这些 CUDA 运行依赖。
3.2 Vulkan API 跨平台、跨厂商
Vulkan 是面向多平台、多厂商的标准 API。支持所需计算队列和着色器能力的 NVIDIA、AMD、Intel 等设备可以沿用同一后端设计,减少对单一厂商计算库的依赖;核函数、设备能力、内存类型和工作组限制仍需按设备检查与验证。Khronos Vulkan 官网介绍了标准的可移植性方向。
Windows、Linux、Android 等平台提供 Vulkan 生态,计算逻辑与 SPIR-V 内核具有复用基础。API 的跨平台能力与项目完成的平台验证分别说明:
| 平台 | 本项目的后端情况 | 验证状态 |
|---|---|---|
| Windows | Vulkan,加载 vulkan-1.dll |
已在 GTX 1050 Ti 上实测 |
| Linux | Vulkan 加载路径使用 libvulkan.so.1 |
尚未实机验证 |
| macOS | 使用独立 Metal 后端 | 尚未实机验证 |
| Android 等移动平台 | Vulkan API 有生态支持;本库仍需平台适配与验证 | 尚未验证 |
macOS 的路径需要单独考虑;本项目选择 Metal,不能把 Windows/Vulkan 结果直接外推到该平台。Khronos 的平台说明区分了原生 Vulkan 与可移植层的情况。

4. 基准条件与统计方式
| 项目 | 配置 |
|---|---|
| 测量日期 | 2026-10-14,北京时间 |
| CPU | Intel Core i7-8700K,12 个逻辑处理器 |
| GPU | NVIDIA GeForce GTX 1050 Ti |
| 系统 / SDK | Windows 11、.NET SDK 10.0.401 |
| 输入 | 同一张示例图预处理后的 FP32 张量,batch=1、640×640 |
| CPU 线程预算 | 12 |
| 测量安排 | 独立进程,配置串行执行,两轮 |
| 预热与窗口 | 30 次预热;每窗口至少 8 秒且至少 40 次推理 |
| 总量 | 8 模型 × 8 配置 × 2 轮,128 个窗口、57,628 次计时推理 |
PyTorch 使用原版 model.fuse();ORT 开启图优化、默认自旋及自动 CUDA 算法。ORT 对照使用 .NET 宿主,输入、输出在主机,结果复制为托管数组,与本库公开返回方式对应。
此机器的 NuGet ORT GPU 原生构建存在 Pascal 内核兼容问题,对照通过 .NET 调用官方 GPU wheel 中同版本、已验证的原生 DLL。DLL 哈希保存在 JSON。ORT 模型执行由 .NET 调用完成,没有用 Python 进程执行这条对照路径。
计时包含网络前向和输出头解码;GPU 包含上传与全部回读。排除图片预处理、NMS、最终 mask 后处理和冷首帧。
每个独立窗口先求单帧中位耗时,再对两轮窗口中位数求中位数。按未舍入值排名,展示值保留两位小数。 两轮不足以证明小差距具有统计显著性;图中的细线是窗口中位数范围,不是置信区间。
测量数据保留了模型、输入和 DLL 哈希,以及每次计时、资源采样和输出对照。它们适合核对本次实现,也为后续复测提供条件记录。
5. CPU 完整排名
每格为中位耗时 ms + 名次,①最快、④最慢,粗体为数值上的第一名。
| 模型 | PyTorch CPU | C# .pt CPU | C# ONNX CPU | ORT CPU(.NET) |
|---|---|---|---|---|
| yolov8n | 44.87 ④ | 21.99 ① | 22.64 ② | 24.25 ③ |
| yolov9t | 77.42 ④ | 29.80 ② | 31.45 ③ | 28.48 ① |
| yolov10n | 60.57 ④ | 22.33 ① | 22.74 ③ | 22.36 ② |
| yolo11n | 51.45 ④ | 22.21 ① | 22.94 ② | 23.34 ③ |
| yolov8n-seg | 56.83 ④ | 28.41 ① | 28.81 ② | 33.84 ③ |
| yolov8n-pose | 47.69 ④ | 23.14 ① | 23.95 ② | 25.17 ③ |
| yolo11n-seg | 63.39 ④ | 28.29 ① | 29.21 ② | 33.03 ③ |
| yolo11n-pose | 54.54 ④ | 23.80 ① | 24.91 ③ | 24.81 ② |


.pt CPU 比 PyTorch CPU 更快 8/8 个模型,按汇总数值比 ORT 更快 7/8 个;C# ONNX CPU 比 ORT 更快 5/8 个。
YOLOv9t 的 ORT 为 28.48 ms,C# .pt 为 29.80 ms,仍有差距。YOLOv10n 的 C# .pt 与 ORT 未舍入差值只有 0.030775 ms,约 0.138%,两轮排名交换,应视为接近持平。
6. GPU 完整排名
比较 PyTorch CUDA、两条 C# Vulkan 路径与 ORT CUDA;仍包含输入上传和全部输出回读。
| 模型 | PyTorch CUDA | C# .pt Vulkan | C# ONNX Vulkan | ORT CUDA(.NET) |
|---|---|---|---|---|
| yolov8n | 9.89 ① | 10.46 ② | 10.55 ③ | 12.69 ④ |
| yolov9t | 21.76 ④ | 12.86 ① | 14.12 ② | 17.73 ③ |
| yolov10n | 12.80 ③ | 10.08 ① | 10.24 ② | 13.53 ④ |
| yolo11n | 10.61 ② | 10.64 ③ | 10.60 ① | 13.72 ④ |
| yolov8n-seg | 12.89 ① | 13.72 ③ | 13.58 ② | 16.53 ④ |
| yolov8n-pose | 10.17 ① | 10.52 ② | 10.78 ③ | 13.05 ④ |
| yolo11n-seg | 13.79 ③ | 13.52 ① | 13.65 ② | 17.56 ④ |
| yolo11n-pose | 10.95 ① | 11.05 ② | 11.21 ③ | 14.23 ④ |
![]() |
||||
![]() |
两条 Vulkan 路径在本次 8 模型汇总中均低于 ORT CUDA。相对 PyTorch CUDA,.pt Vulkan 数值更快 3/8 个,ONNX Vulkan 更快 4/8 个。
其中 YOLO11n 的 ONNX Vulkan 与 PyTorch CUDA 只有 0.008325 ms 的差值,约 0.079%,两轮名次交换。同样不能据此宣称稳定领先。
CUDA 与 Vulkan 是不同实现路径;这些结果应结合算子、融合、缓存及传输处理理解,不能直接推导为语言本身的快慢。
7. CPU 时间与内存成本
CPU% 按全机逻辑核心归一化。CPU ms/帧来自测量窗口内进程累计 CPU 时间除以帧数;并行执行时,它可能大于单帧墙钟时间。
以下是逐模型比值的范围:
| 路径 | CPU ms/帧 ÷ PyTorch | CPU ms/帧 ÷ ORT | RSS ÷ PyTorch | RSS ÷ ORT |
|---|---|---|---|---|
.pt CPU |
0.33~0.45× | 0.73~0.96× | 1.24~1.45× | 2.04~3.60× |
| ONNX CPU | 0.32~0.45× | 0.73~0.98× | 0.75~1.24× | 1.51~3.09× |
RSS 包含运行库、模型、常量和缓存,不等于算子工作区。在当前测量里,C# CPU 相对 ORT 的内存成本更高,后续仍需优化。

GPU 使用率与显存是整卡采样,包含桌面和其他进程,不能当作本库的单进程显存或功耗。速度、累计 CPU 时间和内存分别反映不同成本,应一起评估。
8. 在 .NET 项目中使用
8.1 获取源码与模型
需要 .NET 10 SDK。使用 Git LFS 下载:
git lfs install
git clone https://github.com/hnlyf168/SimdYolo.git
cd SimdYolo
git lfs pull
dotnet build -c Release
Release 也提供源码下载与独立模型 ZIP。模型目录有 8 个 .pt 和 16 个 ONNX;通常静态 opset-17 模型为 640×640,动态模型另有标记,*-opset12.onnx 则是固定 128×128。
应用可引用仓库中的 src/SimdYolo/SimdYolo.csproj。也可从 Release 下载 SimdYolo.0.7.1.nupkg 并使用本地 NuGet 源;当前文章提供的是 GitHub 包下载,不假设 NuGet.org 已有同名包。
8.2 先检查原始推理结果
以下为控制台顶层代码,使用常量张量演示 API 调用和输出形状;它不验证真实图片的检测效果。
using SimdYolo;
using var detector = new YoloDetector("models/pt/yolov8n.pt",
new DetectorOptions
{
Device = ComputeDevice.Cpu,
CpuThreads = 12
});
var input = new Tensor(1, 3, 640, 640);
Array.Fill(input.Data, 0.5f);
// 复用同一会话,先预热。
for (int i = 0; i < 3; i++) detector.PredictRaw(input);
Tensor[] outputs = detector.PredictRaw(input);
foreach (Tensor output in outputs)
Console.WriteLine(string.Join(" × ", output.Shape));
支持的 ONNX 只需更换模型路径。显式选择 Vulkan 或 Metal 时,初始化失败会报错;Auto 允许在已知初始化失败条件下回退 CPU,可通过 FallbackReason 查看原因。
PredictRaw 输入按 NCHW、RGB、0~1 排列,batch 取决于模型支持范围;.pt 输入宽高必须是 32 的倍数。
8.3 图像检测、分割和姿态
下面假定调用方已解码 RGB24 字节,并得到图像宽高:
using SimdYolo;
using var detector = new YoloDetector("models/pt/yolo11n-seg.pt",
new DetectorOptions
{
Device = ComputeDevice.Cpu,
CpuThreads = 12,
Confidence = 0.25f,
IouThreshold = 0.45f
});
IReadOnlyList<YoloInstance> instances =
detector.Predict(rgbBytes, imageWidth, imageHeight);
foreach (YoloInstance instance in instances)
{
Detection box = instance.Box;
InstanceMask? mask = instance.Mask;
IReadOnlyList<Keypoint> points = instance.Keypoints;
Console.WriteLine($"类别={box.ClassId}, 置信度={box.Confidence:F3}");
}
Predict 返回任务实例;Detect 只返回框并跳过 mask 解码;PredictRaw 返回原始张量。换成姿态模型可以获取关键点,换成检测模型可以获取框。
InstanceMask 是原图坐标中的裁剪掩码:X/Y 为起点,Width/Height 为尺寸,概率按行保存在 Probabilities,可用阈值生成二值掩码。关键点也使用原图坐标。
会话应复用并提前预热,最终调用 Dispose 或使用 using。公开输出保留独立存储,但同一会话串行执行推理;多路并发场景还需按工作负载规划会话和线程预算。
9. 复测与正确性检查
先用单模型 CLI 检查 CPU 与 Vulkan:
dotnet run --project src/SimdYolo.Cli -c Release -- bench models/pt/yolov8n.pt cpu 640 40 12 30
dotnet run --project src/SimdYolo.Cli -c Release -- bench models/onnx/yolov8n.onnx vulkan 640 40 12 30
这是上手检查。完整比较需要另外准备开发环境,包含 NumPy、psutil、Ultralytics、CPU/CUDA PyTorch、ONNX 及对应 ORT/CUDA 原生库;脚本的缓存目录和依赖要求见仓库 README。以下命令不会自动安装整套对照环境。
准备一张 P6 RGB PPM 图片,生成共同的输入张量,再运行两轮正式口径的比较。新结果写到独立目录,以保留已有测量:
dotnet build -c Release
dotnet build tools/OrtBenchmark/OrtBenchmark.csproj -c Release
New-Item -ItemType Directory -Force artifacts/local-retest | Out-Null
dotnet src/SimdYolo.Cli/bin/Release/net10.0/SimdYolo.Cli.dll preprocess image.ppm artifacts/local-retest/input.f32 640
python tools/compare-runtimes.py `
--model-dir models/pt --onnx-dir models/onnx `
--vision --threads 12 --warmup 30 --iterations 40 `
--duration 8 --repeats 2 `
--engines torch-cpu net-cpu net-onnx-cpu ort-net-cpu net-vulkan net-onnx-vulkan torch-cuda ort-net-cuda `
--input artifacts/local-retest/input.f32 `
--output artifacts/local-retest/comparison.json
复测时运行的是当前源码,输入图片、驱动和环境也可能变化,应以新 JSON 为准。0.7.0 的原始结果继续保持冻结。用于对照的 Python/ORT 依赖不会变成 SimdYolo 主库的运行依赖。
当前 0.7.1 的回归命令:
dotnet run --project tests/SimdYolo.Tests -c Release -- --vulkan --models models/pt
验证记录包括:
- 0.7.1 Release 构建零警告、零错误;CPU/Vulkan 与 4 个真实模型共 90 项回归通过。
- 0.7.0 正式比较的 160 个输出校验通过,其中 20 个是 PyTorch CPU 自对照,140 个是跨实现对照。
- 另有真实图片、改输入、多帧重放、动态形状、输出所有权、SIMD 尾部及特殊值等记录。
- 中文文档覆盖主库 625 个声明、测试 144 个声明;包内 XML 含 1,064 个中文成员说明。
10. 资料与下载
当前实现的优势,是在 .NET 中提供模型加载、CPU 算子和任务解码的一条完整路径,同时保留可检查的性能与正确性证据。继续优化的重点包括 YOLOv9 CPU、内存成本、更多模型变体和 Metal 实机验证。
模型保留上游 AGPL-3.0 许可证和来源,项目源码当前未新增许可证,详见仓库的 第三方说明。
来源:博客园 · cnblogs.com

