Skip to content

模型格式与转换

本页速览 PyTorch 的 .pt 到 ONNX、TensorRT、TFLite、GGUF——模型格式决定你能否在目标硬件上高效推理。本文盘点主流格式、转换流程、算子兼容陷阱与转换后的数值校验。

模型格式与转换

一句话定义:模型格式(model format)是权重与计算图的序列化载体——模型要能在目标硬件上高效推理,就得先"翻译"成该硬件认识的语言,这个翻译过程就是模型转换(conversion)。

行业洞察:模型格式是部署链路里最容易被低估的一环。PyTorch 训练完的 .pt 文件自带的是 Python 运行时依赖;把它导出成 ONNX 时少写一个 dynamic_axes,线上动态 batch 直接报错;量化后的模型忘了重导出,INT8 提速打了水漂。业界经验:部署事故里相当比例不是模型不行,而是"格式不对、导出有误、校验没做"。理解格式生态,等于给整个部署链路打地基。

一、为什么需要中间格式

1. 三个解耦

  • 与训练框架解耦:训练用 PyTorch 方便迭代,生产不想要 Python 与框架依赖——一个 500MB 的模型不能为了跑一次推理拖一个 2GB 的 PyTorch 环境。
  • 与硬件解耦:同一个模型要跑在 NVIDIA 显卡(TensorRT)、Intel/AMD CPU(OpenVINO/ONNX Runtime)、手机(TFLite/Core ML)、浏览器(WebAssembly)上,不能让每种硬件各自对接 PyTorch。
  • 与推理运行时解耦:ONNX 是一份"标准中间表示"(IR),ONNX Runtime、Triton、TensorRT 都可以消费它,避免厂商锁定。

2. 转换的两种路线

text
路线一(标准路线):
  PyTorch .pt ──导出──► ONNX ──优化/编译──► TensorRT .engine / OpenVINO IR
  (开发友好、可移植)        (针对目标硬件特化、追求极限性能)

路线二(直通路线):
  PyTorch .pt ──torch.compile / 直接加载──► PyTorch 运行时
  (跳过中间格式,牺牲部分性能或可移植性)

工程上推荐标准路线:ONNX 作为稳定中间层,既能在开发期快速验证正确性,又能按目标硬件二次编译。LLM 时代的 GGUF 则是另一个"格式即部署"的特例,见本章第六节。

二、主流格式全景

格式所属生态适用硬件一句话特点
.pt / .pthPyTorch通用(需 PyTorch 运行时)训练产物,含权重与结构(或 state_dict)
TorchScriptPyTorch通用(C++ 也可跑)训练框架的序列化图格式,无外部依赖
ONNX开放标准跨框架跨硬件事实上的中间交换格式,生态最广
.engineNVIDIA TensorRTNVIDIA GPU编译产物,性能极限,硬件绑定
.tfliteTensorFlow Lite移动/边缘/微控制器移动端事实标准,支持量化
Core MLAppleApple 芯片苹果生态原生格式
OpenVINO IRIntelIntel CPU/GPU/NPUIntel 硬件优化,CPU 推理利器
GGUFllama.cpp 生态CPU/GPU 皆可LLM 专用,内嵌量化方案与 tokenizer
SafeTensorsHugging Face通用安全快速的权重容器,无代码执行

记住三个"分水岭"

  • ONNX:可移植性分水岭——用它,你的模型不绑框架;
  • TensorRT .engine / OpenVINO IR:性能分水岭——用它,你绑定一块具体硬件;
  • GGUF / TFLite:场景分水岭——LLM 本地跑选 GGUF,端侧跑选 TFLite。

SafeTensors:为什么 LLM 社区正在抛弃 .bin

Hugging Face 的 pytorch_model.bin 本质是 pickle,加载时会执行任意代码——下载到恶意权重即中毒。SafeTensors 是纯数据格式(JSON 头 + 二进制张量),加载快(零反序列化开销,7B 权重加载可达秒级)且安全,是目前 LLM 权重分发的推荐容器。相关供应链风险见 安全、隐私与合规

三、标准转换流程

text
训练产出 .pt
   │ ① 导出(export)

ONNX 图(含动态轴、算子集版本)
   │ ② 优化(optimize)  图优化/精度校准(INT8 需 calibration)

优化后 ONNX
   │ ③ 编译(build)     面向目标硬件

TensorRT engine / OpenVINO IR / TFLite
   │ ④ 校验(validate)  数值对比 + 性能实测

上线

每一步都可能引入问题,下面逐一展开。

四、ONNX 导出细节

1. 用 torch.onnx.export 的正确姿势

python
import torch
import torch.onnx

model = MyModel().eval().cuda()
dummy = torch.randn(1, 3, 224, 224).cuda()  # 输入 shape 必须与实际一致

torch.onnx.export(
    model,
    dummy,
    "model.onnx",
    input_names=["input"],      # 起名,方便后端绑定
    output_names=["logits"],
    dynamic_axes={              # 关键:声明动态轴
        "input": {0: "batch", 2: "height", 3: "width"},
        "logits": {0: "batch"},
    },
    opset_version=17,           # 算子集版本,见下文
    do_constant_folding=True,   # 常量折叠,默认开
)

三个高频坑:

  1. eval() 别忘:训练模式下 BatchNorm/Dropout 行为不同,导出的图会错。
  2. dynamic_axes 漏了:默认按 dummy 的 shape 固死,线上 batch=8 直接失败。凡是可能变化的轴都要显式声明。
  3. 用真实输入形状导出:shape 是导出时的硬约束,与训练输入不一致会导致图结构错误。

2. opset 版本(算子集版本)是什么

ONNX 规范按版本演进算子语义,opset_version=17 表示使用第 17 版算子集。选择原则:用支持你所有算子、且目标运行时支持的最高版本。ONNX Runtime 官方维护"算子兼容矩阵",例如 opset 18 的某些新语义在旧版本 Runtime 上不识别。经验做法:

text
看运行时文档 → 选其支持的最高 opset → 若报"不支持算子",降版本或改图

3. 导出后用 onnx.checker 和 onnxruntime 验证

python
import onnx
import onnxruntime as ort

onnx.checker.check_model("model.onnx")          # 结构合法性
sess = ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider"])
# 拿几个真实输入,对比 onnx 输出与 pytorch 输出(见第六节数值校验)

五、算子不兼容:部署最常踩的坑

1. 问题长什么样

导出 ONNX 时报错、或在目标引擎构建时报 "Unsupported operator / op type not registered"。常见来源:

  • 动态控制流if / for 依赖张量值——ONNX 不支持,需用 torch.where、固定循环重写。
  • 训练专用算子F.dropout(换 eval 模式解决)、torch.autograd 相关。
  • 自定义算子 / 新算子:如 torch.roll 的老版本不支持、部分 attention 实现。
  • 数据类型不支持:某些引擎不支持 FP64 或 int64 输入。

2. 三种处理策略(按成本排序)

策略做法成本适用
替换算子用等价标准算子重写(如 attention 拆成 matmul+softmax)大多数场景
拆图(graph partitioning)不支持的子图用 Python fallback,其余走引擎少数顽固算子
回退 CPU该算子强制 CPU 执行冷门算子、超低频路径

拆图与回退的代价

图一旦拆开,中间张量要来回拷贝,性能可能掉 20% 以上。优先替换算子;实在不行才拆图;回退 CPU 只用于"一年跑不了几次"的路径。

3. 治本之策:算子越新,越要早验证

新模型(尤其新 Transformer 结构)在训练阶段就该同步做一次导出冒烟测试,别等上线前一周才发现某个 attention 算子引擎不支持。这属于 MLOps 部署流水线 里的"模型可部署性检查"关卡。

六、转换后的数值校验

1. 为什么必须校验

导出/编译本身不改变数学,但算子重排、低精度 kernel、不同的 accumulation 顺序都会引入数值差异。不校验就直接上线的模型,一旦业务指标异常,你分不清是模型问题还是转换问题

2. 三层校验方法

python
import numpy as np

# 1) 最大绝对误差 / 相对误差
max_err = np.abs(ort_out - pt_out).max()
# 2) 余弦相似度
cos_sim = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 3) 业务指标(最硬核):转换前后 AUC / top-1 acc 对比

经验阈值(业界常用参考):

对比对象最大绝对误差余弦相似度备注
同框架 FP32 vs FP32< 1e-4> 0.9999纯算子实现差异
PyTorch vs ONNX Runtime(FP32)< 1e-3> 0.999通常可接受
FP32 vs INT8 量化相对基线指标掉点 < 1%看任务量化需另跑校准,见 量化

关键纪律:用线上同分布数据抽样校验,不要用训练集里挑的"顺眼"样本;结果以业务指标为准,不是逐位对齐。

七、GGUF 与 LLM 量化的关系

GGUF(由 llama.cpp 社区定义)在 LLM 部署里扮演特殊角色:

  • 单文件自包含:权重 + tokenizer + 模型结构 + 超参数全打包,下载即用;
  • 内嵌量化方案q4_K_Mq8_0 等预置量化(见 量化),按块级做权重量化,无需用户自行校准;
  • 以 CPU 为第一公民:在无 GPU 环境下也能跑,是本地 LLM(如 Ollama、LM Studio)的事实标准。

但它有两个局限:量化方案与推理运行时强绑定(GGUF 里的 ggml 张量布局主要为 llama.cpp 优化)、以及对 vLLM/TensorRT-LLM 等高性能推理栈不友好。所以生产级 LLM 服务通常不走 GGUF,而是用 FP16/BF16 或专门的 AWQ/GPTQ 量化(见 大模型推理优化)。

权衡与取舍

决策点选项适用
可移植 vs 极限性能ONNX(可移植) vs TensorRT(绑定硬件)多平台要 ONNX;单机型极限性能要 TensorRT
训练框架直出 vs 中间格式直出省事;中间格式可移植跨框架/跨硬件必须中间格式
导出时机上线前突击 vs 训练期同步冒烟永远选后者,见 MLOps 关卡设计
LLM 走 GGUF 还是专用量化GGUF(llama.cpp 生态) vs AWQ/GPTQ(生产服务)本地工具选 GGUF;高并发服务选 AWQ/GPTQ

一句话总结:模型格式是部署的"语言"——先想清楚目标硬件与场景,再决定导出的路径;导完必须校验,否则等于没导

延伸阅读

参考资料