外观
模型格式与转换
一句话定义:模型格式(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 / .pth | PyTorch | 通用(需 PyTorch 运行时) | 训练产物,含权重与结构(或 state_dict) |
| TorchScript | PyTorch | 通用(C++ 也可跑) | 训练框架的序列化图格式,无外部依赖 |
| ONNX | 开放标准 | 跨框架跨硬件 | 事实上的中间交换格式,生态最广 |
.engine | NVIDIA TensorRT | NVIDIA GPU | 编译产物,性能极限,硬件绑定 |
.tflite | TensorFlow Lite | 移动/边缘/微控制器 | 移动端事实标准,支持量化 |
| Core ML | Apple | Apple 芯片 | 苹果生态原生格式 |
| OpenVINO IR | Intel | Intel CPU/GPU/NPU | Intel 硬件优化,CPU 推理利器 |
| GGUF | llama.cpp 生态 | CPU/GPU 皆可 | LLM 专用,内嵌量化方案与 tokenizer |
| SafeTensors | Hugging 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, # 常量折叠,默认开
)三个高频坑:
eval()别忘:训练模式下 BatchNorm/Dropout 行为不同,导出的图会错。dynamic_axes漏了:默认按dummy的 shape 固死,线上 batch=8 直接失败。凡是可能变化的轴都要显式声明。- 用真实输入形状导出: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_M、q8_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 |
一句话总结:模型格式是部署的"语言"——先想清楚目标硬件与场景,再决定导出的路径;导完必须校验,否则等于没导。
延伸阅读
- 量化 —— 导出后最常见的下一步优化,INT8 需要重新导出与校准
- GPU 与硬件选型 —— 格式最终要落到具体硬件的算力与带宽上
- 推理:从前向传播到推理引擎 —— 引擎如何消费 ONNX 并做图优化
- TensorRT 与边缘部署 —— 从 ONNX 到 .engine 的完整实战
- 常见陷阱与反模式 —— 导出相关的高频翻车现场