ONNX 模型转换中的中间特征与数值对齐 ​

一段推理代码换成 ONNX 后,输出形状完全一致,下游结果却发生了变化。问题可能出在算子计算,也可能更早:导出时取错了一层特征,验证时重新抽了一份随机噪声,或者模型外的一次缩放没有接回来。

在音频编码、图像潜变量与关键点模型的转换中,这些问题经常混在同一个误差数字里。有效的对齐需要先确定两边实际执行了什么,再用固定输入逐段比较。否则,即使误差很小,也未必验证了真正要替换的那条路径。

一、先明确要替换的是哪一段计算 ​

应用调用的对象通常比神经网络本身多做几步:读取配置、处理输入、选择中间层、缩放特征,再把结果包装成下游需要的形式。直接导出模型类的默认输出,可能遗漏调用方依赖的行为。

例如,一个编码器的最终输出是 [B,T,D],实际使用的特征却是多层堆叠后的 [B,L,T,D]。把最终输出复制 L 次,也能凑出相同形状,但各层信息已经丢失。即使保留全部中间层,取值位置也要一致:位置编码前后、注意力块前后,以及最终归一化前后的特征,都不能随意互换。

检查导出路径时,应沿着真正使用这些特征的代码往回看:

对象需要核对的内容
输入归一化、布局、有效长度和 padding 的位置
中间特征取自哪些层,是否包含输入特征,是否经过最终归一化
组合方式层顺序、时间顺序,以及 stack 和 concat 使用的轴
输出缩放、转置、截断与下游索引方式

曾有一份编码器包装实现完整加载了最终归一化层的权重,但实际 forward 在各块输出处直接收集特征,没有调用这一层。这本身不能判定为错误:如果原调用方使用的就是归一化前特征,这样导出才是正确的。它说明,参数加载检查与计算路径检查是两件事。

比较时可以暂时把关键位置都暴露为输出,按层报告误差。若第一层已经不一致,继续调后端融合往往没有意义;若前面各层一致、只有拼接后出错,应先检查轴和顺序。定位完成后再移除调试输出,重新验证最终导出图,因为增加输出可能影响图优化。

二、随机采样需要使用同一份随机量 ​

潜变量编码器可能输出一个分布,再从中采样:

z=μ(x)+σ(x)⊙ϵ,ϵ∼N(0,I)

如果原框架和 ONNX 各自生成一份 ϵ,即使两边的 μ、σ 完全相同,得到的 z 仍可能明显不同。此时计算逐元素 MAE,测到的是两次不同采样的差异,不能直接解释为转换误差。留存的一个编码器验证脚本正是分别执行两条含 sample 的路径,再比较输出;这种测试需要先改比较条件。

更容易定位的办法是将编码器拆成确定部分和采样部分。先对照 μ 与 log variance,再在图外生成一份固定噪声,分别传给两条计算路径。若使用 log variance ℓ,则 σ=exp⁡(ℓ/2);原实现若还有截断、缩放或特殊的确定性分支,也要保持一致。

下面是采样与缩放部分的验证表达,不包含编码器本身:

python
import numpy as np

def latent_from_noise(mean, logvar, noise, scale):
    # 三项使用相同布局;noise 由外部生成一次,两边复用。
    return scale * (mean + np.exp(0.5 * logvar) * noise)

这样可以分别回答:编码器算出的分布是否一致、相同噪声下重参数化是否一致,以及潜变量进入解码器后的结果是否一致。固定噪声通过后,还需要检查实际随机路径的分布和生成效果;它不能证明不同运行时会产生逐项相同的随机序列。

只设相同 seed 不足以替代共享噪声。不同设备或运行时可能使用不同的随机数实现,随机调用次数也可能变化。PyTorch 的可复现性说明同样没有保证跨平台、跨版本及 CPU/GPU 的完全一致结果。

把 sample() 改成 mode() 可以得到确定输出,但对于通常的对角高斯实现,后者返回均值,已经改变了采样行为。它可以帮助检查编码器的确定部分,不能直接当作保留原有生成行为的修复方案。

三、缩放与坐标变换也属于需要对齐的计算 ​

3.1 固定缩放值要在图内外对应 ​

图像潜变量常在编码后乘以缩放系数,解码前再除回来。导出编码器和解码器时,要先明确各自接口接收的是缩放前还是缩放后的张量。

若两边都以为对方已经完成缩放,会漏做一次;若包装类和调用方各做一次,则会重复缩放。这类错误不会改变 shape,模型也可能正常执行,最后表现为亮度、对比度或纹理异常。

固定值可以由模块的 buffer 持有,也可以在导出图中成为常量或 initializer。关键是检查实际图中的乘除操作,以及调用方是否仍在执行相同步骤。曾使用不可训练 Parameter 保存缩放系数,不意味着 Python 常量必然无法导出,也不能据此认定原图漏掉了计算。

验证时先比较缩放前的 latent,再比较缩放后、解码前和解码后的张量。最终转成 uint8 图像之前也保留一次浮点对照:取整和裁剪会掩盖部分误差,超出范围的值还可能被统一压到同一边界。

3.2 原始输出接近,最终坐标仍可能偏移 ​

一类关键点模型分别输出横轴和纵轴上的响应分布。采用基础 argmax 解码时,若坐标分类的细分比例为 r,输入图坐标为:

xinput=arg⁡maxpxr,yinput=arg⁡maxpyr

遗漏除以 r 的步骤,会把分类位置直接当成图像像素位置。两边的分布张量即使一致,最终关键点仍然会错。启用更精细的解码时,还应保留参考实现中的坐标细化与置信度计算,不能只复制一个 argmax。

随后还要将坐标从模型输入映射回原图。对于没有旋转、按中心 c 和宽高 s 定义裁剪范围,并采用对应连续坐标约定的简化情况,可写成:

psource=pinput[W,H]⊙s+c−s2

这里的 s 应包含实际的边界扩展和宽高比调整。存在旋转、不同像素中心约定或其他仿射操作时,应使用前处理对应的逆变换,不能直接套用这个简式。

因此,应先比较横纵轴分布,再比较解码后的输入坐标,最后比较原图坐标。若只看最终像素误差,很难分清模型计算和坐标映射各自造成了多少偏差。裁剪到图像范围内的操作也应放在定位之后检查,避免把错误坐标压回边缘,制造结果合理的假象。

四、把导出误差与后端精度变化分开 ​

原框架在加速设备运行、ONNX 在 CPU 运行时,比较中同时包含图表达、设备实现和精度路径差异。把两边输出都转成 FP32,只是统一了误差计算的类型,不会恢复此前丢失的精度。

可以先采用以下对照顺序,再逐步接入真实执行路径:

对照固定条件主要用于定位
原框架与 ONNX 参考执行相同输入、随机量,优先使用同设备 FP32导出是否改变计算与输出定义
同一后端的不同精度相同输入和输出位置低精度的影响
ONNX 与目标编译模型明确各自精度、相同前后处理编译和目标后端引入的变化
单段与完整链路固定各段交接张量接线、数据转换和误差传播

相同输入是指已经完成前处理的同一组数值,而不只是同一个文件名。两边独立读取图片、构造 padding 或增加位置编码,仍可能给模型送入不同数据。

统计误差前先检查输出数量、shape、dtype 和 NaN/Inf,避免 NumPy 广播把形状错误变成一个看似有效的 MAE。随后按输出分支、层或时间段报告平均误差、较高分位误差和最大误差,并回查异常位置。

相对误差在参考值接近零时会变得很大,因此需要结合绝对误差与数值尺度看;余弦相似度则可能掩盖整体幅值缩放。对于关键点、分割或生成模型,还应继续检查坐标、掩码或图像效果。一个全局阈值不适合替代这些不同层次的判断。

目标后端有明显偏差时,可以继续采用 TensorRT FP16 数值异常排查中的 QKV 布局对照方法,缩小首次发生偏差的图范围。

五、运行时必须按模型接口解释内存 ​

在一次昇腾推理适配中,输入字节数是预期缓冲区大小的两倍,问题最终落在 FP32 数组与 FP16 接口的对应关系上。对连续的普通稠密张量,样本数相同而元素字节数翻倍,就会产生这种现象;但遇到两倍差异时仍应同时核对 batch 和 shape,不能仅凭倍率确定原因。

正确顺序是先读取模型的输入描述,再核对数组类型与布局,最后检查需要传输的字节数。扩大缓冲区只能改变容量,不能让模型把四字节浮点自动解释成两字节浮点。

输出同样如此。np.frombuffer 的 dtype 决定怎样解释原始字节,它不是数值转换。先用错误类型解释,再 astype,不会得到正确数据。输出尺寸和有效 batch 也应来自实际执行的形状,而不是根据数组长度猜测。

动态 batch 则要分别检查导出图、编译档位与运行时设置。ONNX 声明 batch 轴为动态,不代表编译模型接受任意 batch;为最大档位分配了足够内存,也不代表本次已经选择正确档位。在采用动态 batch 控制接口的 OM 路径中,还需按对应版本完成执行前的档位设置。切出前几项输出不能替代这一步。

模型较大时,结构文件与外部权重也要一起保留。启用外部数据保存时,all_tensors_to_one_file=True 可以把外部张量集中保存,并不等于权重已经嵌入单个 ONNX 文件。ONNX 官方文档对大于 2 GB 的模型要求以文件路径调用 checker,并保留正确的外部数据关系。加载和结构校验通过后,仍要继续做数值验证。

六、完整链路应逐段使用已对齐的输入 ​

当多个模型串联时,后一段输入不同,最终输出误差就无法直接归因。较清楚的做法是先保存参考路径的中间张量,把它分别送入两种实现的下一段;每段独立通过后,再接回转换路径自己的输出,检查误差如何累积。

如果各段单独对齐,连接后却出现明显偏差,应优先检查缩放是否重复、层与通道顺序是否改变,以及缓存是否仍对应同一条输入。若偏差确实来自前段的微小数值变化,再判断后段是否对这些变化敏感,而不是反复放宽最终容差。

定位到首次出现偏差的位置后,应保留对应输入与最小计算图,再检查具体算子或后端实现。中间特征、随机量和前后处理都已对应时,后面的数值差异才有清楚的比较基线。

完整链路中的等待与播放问题,见 实时音视频生成中的同步与批处理延迟。

七、参考资料 ​