1.3 核心术语与张量契约


1.3 核心术语与张量契约

本节摘要:Execution Provider 是硬件翻译官,Kernel 是单算子实现,OrtValue 是跨 EP 张量载体。数值语义契约要求输出偏差在声明精度下可解释。

术语地图

报错与文档里反复出现的名词,先在这里钉死关系:ExecutionProvider 是 ORT 与硬件之间的插件层,负责声明能力、注册 kernel、管理设备内存;Kernel 是某个算子在某个 EP 下的具体实现,一个 ONNX 算子可以对应多个 Kernel;KernelDef 是 Kernel 的注册描述(支持的输入类型、约束);OrtValue 是承载张量数据的统一载体,跨越 EP 边界时被拷贝或共享。

术语 角色 对应代码
ExecutionProvider 注册 kernel、管设备内存 CUDAExecutionProvider
Kernel 单算子在 EP 下的实现 Conv 的 cuDNN 实现 vs MLAS 实现
KernelDef 描述 kernel 支持的类型约束 输入必须是 float32
OrtValue 跨 EP 拷贝载体 session.run 的输入输出类型
Graph Partition 把图按能力切给各 EP 分区后的子图集合

GetCapability 与分区

EP 通过 GetCapability() 提交可接管子图:输入节点集合、输出节点、内部执行的属性。ORT 的 Partition 器综合所有 EP 的能力声明,把整图切成若干连续子图,每个子图交付给最匹配的 EP。边界处如果两个 EP 的 device 不同(GPU 与 CPU),就会插入内存拷贝。

同一个 Conv 节点,CUDA EP 用 cuDNN/cuBLAS,CPU EP 用 MLAS——这就是为什么跨 EP 输出会有微小差异。MLAS(Microsoft Linear Algebra Subroutine)针对 AVX-512 做了分块优化,与 cuBLAS 的累加顺序不同,浮点结果不会逐 bit 一致。

跨 EP 数值不一致的三类原因

输出差 0.02 算不算 bug?取决于根因。归好类再决定要不要排查:

  1. 累加顺序不同:cuBLAS 与 MLAS 对矩阵乘的拆分 tile 不同,浮点加法不满足结合律,误差在 1e-7 量级,可接受
  2. 精度模式不同:FP16 推理、TensorRT 的 tf32 中间计算,误差可到 1e-3~1e-2,需按业务阈值验收
  3. 实现语义不同:某些算子在 EP 间并非数学等价(如 NaNs 的传播、Softmax 的 axis 处理),这种差异是 bug 级,必须对齐

判断方法很简单:同一模型、同一输入,分别在 CPU(FP32 基线)与目标 EP 上跑,逐算子对比中间张量的最大绝对误差,误差来源一目了然。对序列模型还要对比 logits 的顺序,避免 RNN 展开方向不同引入的错位。

数值验收基线

把"对比"沉淀成可复用的函数,而不是每次手工看输出:

import numpy as np import onnxruntime as ort def max_diff(sess_cpu, sess_ep, feeds): out_cpu = sess_cpu.run(None, feeds)[0] out_ep = sess_ep.run(None, feeds)[0] abs_diff = np.abs(out_cpu.astype(np.float64) - out_ep.astype(np.float64)) rel_diff = abs_diff / (np.abs(out_cpu).astype(np.float64) + 1e-8) return abs_diff.max(), rel_diff.max() sess_cpu = ort.InferenceSession("mlp.onnx", providers=["CPUExecutionProvider"]) sess_ep = ort.InferenceSession("mlp.onnx", providers=["CUDAExecutionProvider"]) feeds = {"input": np.random.randn(4, 256).astype(np.float32)} print(max_diff(sess_cpu, sess_ep, feeds))

阈值按场景定:分类模型看 argmax 是否一致;回归模型看相对误差 < 1e-4;医疗/金融类高风险场景必须对 FP32 基线做全量验收。2023 年 ORT-GPU Arena 目标将显存碎片率压到 5% 以下,那是第4章的主场,但验收流程从本章就应建立。

数值契约的数学表达

语义契约可以写成一个边界不等式:设 FP32 参考输出为 y_ref,目标 EP 输出为 y_ep,则验收要求对给定输入集 D 满足:

sup_{x in D} || f_ep(x) - f_ref(x) ||_2 / (|| f_ref(x) ||_2 + eps) <= tolerance

tolerance 按三类根因分级:累加顺序差异给 1e-6,FP16/TF32 模式给 1e-3,语义差异必须为 0(否则是 bug)。注意 tolerance 是"上界保证",不是"一次运行的实测值"——多跑几组输入,取最大的那个误差,才是可复现的契约。代码里 np.max(abs_diff) 计算的就是这个量。

OrtValue 与 IO Binding

session.run(None, {"input": x}) 背后会做一次张量到 OrtValue 的转换。GPU 场景下每次 run 都走 numpy 转换会把数据搬运开销叠加到延迟里,正确做法是用 IO Binding 预先分配设备内存:

import onnxruntime as ort sess = ort.InferenceSession("mlp.onnx", providers=["CUDAExecutionProvider"]) io = sess.io_binding() dev = ort.OrtDevice("cuda", 0) x = ort.OrtValue.ortvalue_from_numpy(x_np, dev) # 直接放在 GPU io.bind_ortvalue_input("input", x) io.bind_output("logits", dev) sess.run_with_iobinding(io) out = io.get_outputs()[0].numpy()

IO Binding 让输入输出都停留在设备内存,绕开 Host/Device 往返。对低延迟服务,这个改动常能省掉 10%~30% 的每请求开销,也是理解 OrtValue 契约价值的直接例证:OrtValue 不只是"张量载体",它携带 device 与内存类型信息,决定了数据能否被零拷贝复用。

跨 EP 差异的分级对照

根因 典型量级 是否接受 处置
累加顺序(同 FP32) 1e-7 可接受 无需处理
FP16 / TF32 中间计算 1e-3 ~ 1e-2 按业务定 敏感层升 FP32
算子语义差异(NaN 传播等) 可到无穷 不可接受 对齐实现 / 换 EP
量化误差累积 1e-2 以上 按业务定 第5章校准与排除

这张表是跨 EP 验收的决策入口:先量化误差落在哪个量级,再决定动模型、动配置还是动 EP。注意第三类是红线——它意味着某个算子在两个 EP 上的实现不是数学等价的,属于需要上报的语义级缺陷,不能靠阈值糊弄过去。

判断直觉与常见误区

⚠️ TensorRT EP 与 CUDA EP 不等价——TRT 会对子图额外编译与融合,输出与 CUDA EP 也可能有差,失败时需回退 CUDA 再比对。

💡 sess.get_providers() 看实际激活 EP。配置里写了 CUDA 但实际列表只有 CPU,说明 wheel 装的是 CPU 版或设备不可用,先查这里再查模型。

核心回顾

  • EP 管资源,Kernel 管单算子
  • Partition 决定 GPU/CPU 切分与拷贝边界
  • 数值对比须统一精度基线
  • 误差按"累加顺序 / 精度模式 / 语义不同"三类分级处置
  • 跨 EP 差异先归类再排查,别一上来就怀疑模型

下一章:Session 内部第一段流水线——图加载、常量折叠与算子融合。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U