3、环境准备:搭建可复现的调试环境(Python + PyTorch + ONNX Runtime)

做算子融合的精度排查,最怕什么?

我最怕的是——环境不一致。你在本地跑得好好的,放到服务器上就崩了。或者昨天还能复现的bug,今天莫名其妙消失了。说白了,精度问题本身就像个幽灵,如果连环境都不固定,你根本没法跟它斗。

所以这一章,我带你搭一个「铁板一块」的调试环境。我自己的习惯是:用Python 3.8 + PyTorch 1.13 + ONNX Runtime 1.14,这个组合我踩过无数坑,相对稳定。

3.1 为什么必须固定版本?

你想想看,ONNX Runtime每个版本对算子的实现都有微调。PyTorch导出onnx时,不同版本对某些算子的拆解方式也不一样。我曾经遇到过一个问题:同一个模型,PyTorch 1.12导出的onnx里,Gemm被拆成了MatMul+Add;而PyTorch 1.13里,它保留了Gemm。这一拆一合,融合规则就全变了。

所以,版本锁定是第一要务。

核心原则: 调试环境 = 生产环境。不要图方便用最新版,除非你愿意当小白鼠。

3.2 创建干净的虚拟环境

我个人习惯用conda,因为它对CUDA版本的管理比较省心。当然,venv也行,看你喜好。

# 创建环境
conda create -n fusion_debug python=3.8 -y
conda activate fusion_debug

# 安装PyTorch(以CUDA 11.7为例)
pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117

# 安装ONNX Runtime
pip install onnxruntime-gpu==1.14.1

# 辅助工具
pip install onnx onnxsim netron

嗯,这里要注意:onnxruntime-gpu 和 onnxruntime 是两回事。如果你只是做CPU推理,用后者就行。但做算子融合调试,我建议直接用GPU版本,因为很多融合优化在GPU后端才生效。

3.3 验证环境是否正常

装完了别急着跑模型。先写个最小验证脚本,确保每个组件都能正常工作。

import torch
import onnxruntime as ort
import onnx

print("PyTorch version:", torch.__version__)
print("ONNX Runtime version:", ort.__version__)
print("ONNX version:", onnx.__version__)

# 验证GPU可用
print("CUDA available:", torch.cuda.is_available())

# 验证ONNX Runtime GPU可用
providers = ort.get_available_providers()
print("Available providers:", providers)
assert 'CUDAExecutionProvider' in providers, "GPU provider not found!"

如果输出里没有报错,并且能看到 CUDAExecutionProvider,那环境基本就稳了。

小技巧: 我习惯把这个验证脚本保存为 check_env.py,每次换机器或重装环境后,先跑一遍它。省得后面排查半天,结果发现是环境没装对。

3.4 准备一个「最小复现模板」

做精度排查,最忌讳的是拿整个大模型直接跑。你想想看,一个几百层的模型,融合了十几个算子,你根本不知道是哪一步出了问题。

我的做法是:先构造一个极简的融合场景。比如,一个Conv+BN+ReLU的序列,这是最常见的融合模式。

import torch
import torch.nn as nn

class SimpleFusionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 16, 3, padding=1)
        self.bn = nn.BatchNorm2d(16)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        x = self.relu(x)
        return x

model = SimpleFusionModel().eval()

# 导出onnx
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
    model, 
    dummy_input,
    "fusion_test.onnx",
    opset_version=17,
    input_names=['input'],
    output_names=['output']
)

print("ONNX model exported successfully.")

这个模板的好处是:你知道它一定会触发Conv+BN+ReLU的融合。如果这个简单case都出问题,那就不用查大模型了,直接定位到融合逻辑本身。

3.5 对比工具:PyTorch vs ONNX Runtime

精度排查的核心,就是对比。对比融合前和融合后的输出差异。我一般准备两个推理函数:

def run_pytorch(model, input_tensor):
    """用PyTorch做推理,作为基准"""
    with torch.no_grad():
        output = model(input_tensor)
    return output.numpy()

def run_onnx(onnx_path, input_tensor):
    """用ONNX Runtime做推理,看融合后的结果"""
    session = ort.InferenceSession(onnx_path, providers=['CUDAExecutionProvider'])
    input_name = session.get_inputs()[0].name
    output = session.run(None, {input_name: input_tensor.numpy()})
    return output[0]

# 对比
pytorch_out = run_pytorch(model, dummy_input)
onnx_out = run_onnx("fusion_test.onnx", dummy_input)

diff = np.abs(pytorch_out - onnx_out).max()
print(f"Max difference: {diff:.6f}")

如果diff在1e-5以内,说明融合没引入精度问题。如果差了好几个数量级,那就要深入查了。

避坑指南: 我曾经遇到过一个问题——PyTorch默认用的是float32,但ONNX Runtime在某些算子优化时,会偷偷转成float16。结果diff直接飙到0.1以上。排查了半天才发现是精度模式没对齐。所以,对比前一定要确认两边的数据类型一致。

3.6 环境快照与复现

环境搭好了,怎么保证别人也能复现?我建议你生成一个 requirements.txt,并且把关键包的版本写死。

pip freeze | grep -E "torch|onnx|numpy" > requirements_fusion.txt

然后把这个文件连同你的复现代码一起提交到git仓库。这样,不管是谁,只要执行 pip install -r requirements_fusion.txt,就能得到一模一样的环境。

3.7 本章小结

环境准备这事儿,看着简单,但往往是精度排查的第一道坎。我见过太多人花了两天查bug,最后发现是ONNX Runtime版本不对。所以,别嫌麻烦,把环境锁死,把模板写好,后面才能专心对付真正的精度问题。

一句话总结: 环境固定 + 最小模板 + 对比工具 = 可复现的调试环境。这三样齐了,你就有底气跟精度问题硬碰硬了。

可复现调试环境搭建流程 步骤1:版本锁定 Python 3.8 + PyTorch 1.13 + ONNX Runtime 1.14 步骤2:虚拟环境 conda create -n fusion_debug pip install 指定版本 步骤3:验证环境 check_env.py 确认GPU Provider 步骤4:最小模板 Conv+BN+ReLU 导出ONNX验证 步骤5:对比工具 PyTorch vs ONNX Runtime 计算Max diff 步骤6:环境快照 pip freeze > requirements 提交git仓库 环境固定 + 最小模板 + 对比工具 = 可复现调试环境

想要更多资料,联系可可