ONNX Runtime 算子内核支持矩阵详解:OperatorKernels.md 的生成机制、执行提供程序差异与源码级解析
【免费下载链接】onnxruntimeONNX Runtime: cross-platform, high performance ML inferencing and training accelerator项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime
OperatorKernels.md 是 ONNX Runtime 仓库中一张"算子 × 执行提供程序(Execution Provider,EP)× OpSet 版本 × 数据类型"的支持矩阵总表,直接决定了"某个模型在某条硬件路径上能不能跑、用哪种精度跑"。本文以该文档为主体,逐节解读其结构(版本记号法、EP 章节、算子域分组),结合 gen_opkernel_doc.py 的生成逻辑与 build.py 的 CI 流程,还原这张表是如何从内核注册表中自动产出的;并以 Clip、Cast、Gemm 等真实算子为线索,对照 CPU 实现、CUDA 与 DML 的注册代码,说明三个 EP 在类型覆盖上的关键差异(如 CUDA 的 fp16/bf16 优先策略、DML 的 float/float16 限定与com.microsoft.dml专属融合算子域),帮助你在选型 EP、排查"算子不支持/类型不支持"错误时快速定位依据。
一、文档主体:三个执行提供程序的算子支持总览
OperatorKernels.md 的目录部分只列出三个 EP:
- CPUExecutionProvider(CPU,第 23 行起,锚点
#cpuexecutionprovider) - CUDAExecutionProvider(CUDA,第 661 行起,锚点
#cudaexecutionprovider) - DmlExecutionProvider(DirectML,第 1194 行起,锚点
#dmlexecutionprovider)
这不是因为 ONNX Runtime 只有这三个 EP,而是文档生成时被刻意限制了一个子集:build.py 中generate_documentation(第 2366~2396 行)在构建后调用:
python gen_opkernel_doc.py --output_path <source>/docs/OperatorKernels.md \ --providers CPU CUDA DML源码注释写得很直白:"we currently limit the documentation created by a build to a subset of EP's. Run get_opkernel_doc.py directly if you need/want documentation from other EPs that are enabled in the build."(第 2382~2383 行)。也就是说,XNNPACK、TensorRT、ROCM、OpenVINO、CoreML 等 EP 的支持表不会出现在这份 checked-in 文档里;若你的构建里启用了这些 EP,可以自行完整运行生成脚本(不带--providers过滤)得到全量表格。
每个 EP 章节内部按"算子域(domain)"分块,用一行加粗的分隔行标注,例如|**Operator Domain:** *ai.onnx*|||。三个 EP 的域覆盖各有侧重:
| 执行提供程序 | 文档中出现的算子域 | 特点 |
|---|---|---|
| CPUExecutionProvider | ai.onnx、ai.onnx.ml、com.microsoft | 基线实现最全,com.microsoft域含 MoE、BeamSearch、MatMulNBits 等 70 余个贡献算子 |
| CUDAExecutionProvider | ai.onnx、ai.onnx.ml、com.microsoft、com.ms.internal.nhwc | 额外覆盖 NHWC 内部域(Conv/ConvTranspose/BatchNormalization等 NHWC 变体,第 1153~1188 行) |
| DmlExecutionProvider | ai.onnx、com.microsoft、com.microsoft.dml | 独有com.microsoft.dml域(第 1618 行起):DmlFusedAdd、DmlFusedConv、DmlFusedGemm、DmlFusedMatMul等 9 个融合算子,均为tensor(float), tensor(float16) |
注意ai.onnx.ml(传统机器学习域:TreeEnsemble、LinearClassifier、SVM 等)只有 CPU EP 提供了完整实现,CUDA 域里仅列了LabelEncoder(第 1066~1068 行)——这也印证了 CUDA EP 聚焦深度学习算子的定位。
二、版本记号法(Version Notation)如何读
文档开头(第 5~12 行)定义了 OpSet Version 列的三种记号,这与生成脚本 gen_opkernel_doc.py 的format_version_range(第 14~21 行)一一对应:
| 记号 | 含义 | 生成逻辑 |
|---|---|---|
N(如13) | 仅注册到 opset N 这一个版本 | v[0] == v[1]时输出单值 |
[N, M](如[6, 12]) | 注册到 opset N 到 M(含两端) | 区间且终点为有限值 |
N+(如16+) | 从 opset N 起注册,并延续到后续版本,直到被更新的注册"接管" | 终点>= 2147483647(即 INT_MAX)时输出N+ |
N+的"被更新注册接管"这一点在真实表格里随处可见。例如 CPU EP 的Acos(第 30~31 行)同时存在22+(tensor(float))与[7, 21]两行:opset 22 的算子 Schema 变化后,内核注册拆成了新的版本段。而 CUDA 的Acos只有7+一行(文档中 CUDA 章节按类型列出),DML 则显示7+——同一算子在不同 EP 的版本切分粒度不同,直接反映各 EP 注册内核时选择的start~end版本区间写法。
三、表格的生成机制:从注册表到 Markdown
OperatorKernels.md 是自动生成的,文件头声明"Do not modify directly",对应生成器 gen_opkernel_doc.py。其核心数据流是:
- 拉取算子 Schema:
rtpy.get_all_operator_schema()(第 114 行)遍历所有 ONNX 算子定义,把每个算子的输入/输出名与类型串拼成 Parameters 列文本(*in* X:**T**形式,第 122~139 行),按domain.name存进paramdict。同一算子在不同 opset 下 Schema 可能不同(如Clip的input/min/max三输入形态与单输入形态),脚本用集合去重后以<br><br>or<br><br>分隔并列展示——这就是表格里"or"多形态的来源。 - 拉取内核注册表:
rtpy.get_all_opkernel_def()(第 149 行)返回所有已注册内核,按provider → domain → op_name三级索引(第 148~153 行)。 - 合并类型约束:对每个算子,把各版本区间(
op.version_range)下的类型约束(op.type_constraints)聚合成version_type_index[opset 区间][T] = {类型集合}(第 181~185 行),再按版本从新到旧输出(reverse=True,第 188 行)——所以表格中同一算子的多行总是"最新版本段在最上"。 - 插件 EP 扩展:
--plugin-ep NAME:PATH(第 61~93、219~226 行)会通过ort.register_execution_provider_library加载插件 EP 动态库(例如CUDAExecutionProvider:/path/to/libonnxruntime_providers_cuda.so),再经get_registered_ep_kernel_defs把其内核并入索引。这是为"插件化 EP"(以独立动态库形式分发的 EP)补充支持表的官方途径。
CI 的防漂移校验:build.py 第 2398~2422 行在文档生成后会git diff比对 docs/OperatorKernels.md 与docs/ContribOperators.md,若有差异即抛出BuildError("Generated documents have diffs")。也就是说,任何改动了内核注册的 PR(新增算子、增删类型约束、调整版本区间)都必须重新生成该文档,否则 CI 直接失败——这也是文档与实现永不失同步的保证。
四、注册链路:一张表背后的源码结构
表格里的每一条记录,源头都是各 EP 在编译期通过KernelDefBuilder注册的内核。以 CPU EP 的Clip为例(clip.cc 第 12~52 行):
ONNX_CPU_OPERATOR_VERSIONED_KERNEL( Clip, 6, 10, KernelDefBuilder().MayInplace(0, 0) .TypeConstraint("T", DataTypeImpl::GetTensorType<float>()), Clip_6<float>); // opset 11 允许 float;opset 12 起扩展为 double/MLFloat16/int8/... ORT_SPECIFY_OP_KERNEL_ARG_DEFAULT_TYPES( kCpuExecutionProvider, kOnnxDomain, Clip, 12, Input, 0, float, MLFloat16, double, int8_t, uint8_t, int32_t, uint32_t, int64_t, uint64_t);要点:
- 宏的第一个版本段
Clip, 6, 10恰好对应文档 CPU 表格中Clip的[6, 10]行(**T** = tensor(float),OperatorKernels.md 第 85 行); - 下方
op_kernel_type_control命名空间的ORT_SPECIFY_OP_KERNEL_ARG_DEFAULT_TYPES声明了"启用类型列表",再由BuildKernelDefConstraintsFromTypeList<...>()展开为TypeConstraint——文档第 82~85 行Clip的13+(含tensor(int32)...tensor(uint8)十种类型)、12、11(仅tensor(float))、[6, 10]四行,正是不同版本段启用类型列表的并集呈现; - EP 名称常量统一维护在 constants.h(如第 32~34 行的
kCpuExecutionProvider = "CPUExecutionProvider"),这正是gen_opkernel_doc.py --providers参数注释中"Matches provider names from constants.h"所指的来源; - 内核定义(
KernelDef,含version_range与type_constraints,即文档生成脚本直接消费的两个字段)集中在 kernel_registry.h / kernel_info.h,会话装配时由 kernel_registry_manager.h 管理查找。从源码结构看,文档表格就是"注册表字段 → Markdown 行"的直译。
运行时语义:模型中的算子节点要能在某个 EP 上执行,需同时满足三条——算子在 EP 的内核注册表中存在、模型的算子 opset 落在该内核的version_range内、实际张量类型属于该版本段的类型约束集合。三者任一不满足,算子就会"落回"其它 EP(通常是 CPU)。
五、逐 EP 解读支持矩阵
5.1 CPUExecutionProvider:类型覆盖最广的基线
CPU 章节(第 23~657 行)覆盖ai.onnx全量主干算子 + 大量扩展:
- 量化/反量化:
QuantizeLinear的25+段支持float8e4m3fn、float8e5m2、int2、int4、uint2、uint4等低位宽与 FP8 目标类型(第 329 行),DequantizeLinear对应段支持int2/int4源类型(第 121 行)——CPU EP 是低比特量化的参考实现; - LLM 基础设施:
Attention(24+,Q/K 为float, float16,第 54 行)、TensorScatter(KV cache 写入,第 510 行)、RMSNormalization、RotaryEmbedding(ai.onnx 域23+),以及com.microsoft域的GroupQueryAttention、MoE、QMoE、BeamSearch、GreedySearch、Sampling、PagedAttention之外的多种 attention 变体(第 570~643 行); - 控制流与序列:
If/Loop/Scan的V类型约束覆盖了optional(...)与seq(...)组合类型(第 210、246、418 行),Sequence*全家族齐备; - 字符串:
StringConcat、StringNormalizer、StringSplit、RegexFullMatch,以及Cast对tensor(string)的支持(第 72 行)。
5.2 CUDAExecutionProvider:fp16/bf16 优先
CUDA 章节(第 661~1189 行)的类型列表呈现明显的"半精度优先"策略:
Gemm/MatMul的13+段为bfloat16, double, float, float16(第 777、846 行);- 对比 CPU 的
Gemm(double, float两种,第 177 行)——CUDA 多出的float16/bfloat16是 Tensor Core 路径的前提; Cast的25+段额外出现tensor(float4e2m1)(第 690 行),这是 CPU EP 没有的 FP4 类型,对应 CUDA 硬件新特性;- 独有
com.ms.internal.nhwc域(第 1153 行起):Conv、ConvTranspose、BatchNormalization、MaxPool等的 NHWC 布局变体(float, float16),从源码结构看是 CUDA EP 内部用于布局优化重写(NCHW→NHWC)的中间层算子,不对外暴露; ai.onnx.ml域仅LabelEncoder一项(第 1067 行)。
5.3 DmlExecutionProvider:float/float16 限定与 DML 融合域
DML 章节(第 1194 行起):
- 类型基本限定在
tensor(float), tensor(float16),个别算子扩展到 int 系列(如Add的14+段覆盖 10 种数值类型,第 1203 行);无 bfloat16、无 FP8 主线路径(com.microsoft.dml域算子亦仅为float, float16); - 独有
com.microsoft.dml域(第 1618 行起)的 9 个DmlFused*算子(DmlFusedConv、DmlFusedGemm、DmlFusedMatMul、DmlFusedAdd、DmlFusedBatchNormalization、DmlFusedInstanceNormalization、DmlFusedMeanVarianceNormalization、DmlFusedSum等),是把"计算 + 激活/归一化"融合的 DirectML 专属算子,配合com.microsoft域的FusedMatMulActivation、NhwcConv等共同构成 DML 路径的融合算子族; - 版本记号更"粗":很多算子直接写
N+(如Reshape的21+/19+/14+/13+/5+),说明 DML 的内核注册习惯上以更宽的版本段承接后续 opset。
六、实战:查询、再生成与插件 EP 支持表
6.1 查表决策流
判断"模型算子能否走 EP X":
- 在 docs/OperatorKernels.md 对应 EP 章节找到算子(注意域:
ai.onnx之外需核对com.microsoft等); - 确认模型 opset 落在某个版本段(
N、[N, M]、N+)内; - 核对实际张量类型是否在该行
Types Supported中。例如:Clip想走 DML 用int32,其13+段包含tensor(int32)(第 1244 行)可以;若用double则不在列表内,会被重划到 CPU。
6.2 本地再生成文档
前提:已构建出包含 Python 绑定的 ONNX Runtime,并在构建目录下import onnxruntime可用。
# 全量(构建中启用的所有 EP) python tools/python/gen_opkernel_doc.py --output_path /tmp/OperatorKernels.md # 只看 CPU 与 CUDA(--providers 大小写不敏感,自动补全 "ExecutionProvider" 后缀) python tools/python/gen_opkernel_doc.py --providers cpu cuda --output_path /tmp/opk_cpu_cuda.md # 纳入以插件形式分发的 EP 动态库 python tools/python/gen_opkernel_doc.py --output_path /tmp/opk_plugin.md \ --plugin-ep "CUDAExecutionProvider:/path/to/libonnxruntime_providers_cuda.so"参数细节来自 gen_opkernel_doc.py 第 210~233 行的 argparse 定义:--providers(可选,过滤)、--plugin-ep(NAME:PATH形式,可多个)、--output_path(必填)。CI 中使用的正是"CPU CUDA DML"这一固定组合(见第三节),若本地构建启用了更多 EP,不带--providers即可得到更全的表格——但请注意此时产物与仓库 checked-in 版本不一致是预期行为。
6.3 常见"缺失"排查
- 某个 EP 的章节整体不存在:因为生成时被
--providers过滤掉了,按 6.2 自行生成; - 算子在某 EP 缺席:该 EP 未注册对应内核(如 CUDA 无
RegexFullMatch、DML 无MoE/BeamSearch),运行时该算子会落回 CPU; - 类型不在列表:需要 EP 侧补充类型约束注册(参考 clip.cc 中
ORT_SPECIFY_OP_KERNEL_ARG_DEFAULT_TYPES的写法),并同步重新生成 OperatorKernels.md 以通过 build.py 的git diff校验。
七、小结
docs/OperatorKernels.md 是 ONNX Runtime 算子内核注册表的"单一事实来源"文档:N/[N, M]/N+三种版本记号精确刻画每个内核的版本适用域;三张 EP 表格分别呈现了 CPU(全类型基线)、CUDA(fp16/bf16 + NHWC 内部域)、DML(float/fp16 + DmlFused 融合域)三条硬件路径的能力边界;而 gen_opkernel_doc.py 与 build.py 的生成 + diff 校验闭环保证了它与源码注册(KernelDefBuilder/ORT_SPECIFY_OP_KERNEL_ARG_DEFAULT_TYPES等)严格一致。对使用者,它是 EP 选型与精度决策的速查表;对贡献者,它是每次内核注册变更都必须同步更新的交付物。
【免费下载链接】onnxruntimeONNX Runtime: cross-platform, high performance ML inferencing and training accelerator项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考