第3章:模型导出与 ONNX 转换

从 HuggingFace 导出模型,再到 ONNX,最后转成 TensorRT 引擎——这条链路我走了不下几十遍。说实话,每一步都有坑。今天我把踩过的雷、淌过的水,都给你捋一遍。

3.1 从 HuggingFace 导出模型:没那么简单

很多人觉得,从 HuggingFace 导模型不就是 model.save_pretrained() 吗?嗯,对了一半。但你要部署到生产环境,光保存权重是不够的。

我个人的习惯是:先确认模型类型,再决定导出策略。比如 LLaMA、ChatGLM、Qwen,它们的架构细节不同,导出方式也有差异。

核心原则:导出时保留完整的模型配置,包括 tokenizer、config.json、generation_config.json。缺一个,推理时就可能报错。

举个例子,我曾经部署一个 ChatGLM2 模型,导出时忘了保存 tokenizer。结果推理时 tokenizer 加载失败,整个服务起不来。排查了半天,最后发现是导出脚本少写了一行。

# 正确的导出姿势
from transformers import AutoModel, AutoTokenizer

model = AutoModel.from_pretrained("THUDM/chatglm2-6b", trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm2-6b", trust_remote_code=True)

# 保存完整
model.save_pretrained("./chatglm2_export")
tokenizer.save_pretrained("./chatglm2_export")

小技巧:导出后检查一下目录结构,确认 config.json 和 tokenizer_config.json 都在。我每次都会跑一个 ls -la 看一眼。

3.2 ONNX 转换中的算子兼容性问题

ONNX 转换,说白了就是把 PyTorch 的计算图翻译成 ONNX 的中间表示。但翻译过程中,有些算子会「水土不服」。

最常见的坑

  • 动态控制流:PyTorch 里用 if 语句控制分支,ONNX 不支持。你想想看,ONNX 是静态图,它怎么知道运行时走哪条路?
  • 自定义算子:HuggingFace 模型里经常有自定义的 Flash Attention、Fused LayerNorm 等。这些算子 ONNX 原生不认识。
  • 数据类型不匹配:PyTorch 默认 float32,ONNX 可能转成 float16,精度对不上。

警告:遇到算子不兼容时,不要硬转。我见过有人强行转 ONNX,结果推理结果全是 NaN。正确的做法是:要么替换算子,要么用 ONNX Runtime 的扩展库。

我记得有一次,转一个 LLaMA 模型时,torch.where 算子死活转不过去。后来发现是 ONNX opset 版本太低,升级到 17 就解决了。

# 指定 opset 版本,避免算子兼容问题
import torch

dummy_input = torch.randint(0, 100, (1, 128))
torch.onnx.export(
    model,
    dummy_input,
    "model.onnx",
    opset_version=17,  # 推荐 17 或更高
    input_names=["input_ids"],
    output_names=["logits"],
    dynamic_axes={
        "input_ids": {0: "batch_size", 1: "seq_len"},
        "logits": {0: "batch_size", 1: "seq_len"}
    }
)

3.3 动态 shape 处理:绕不开的坎

生产环境里,输入的长度不可能固定。今天用户问一句「你好」,明天问一段「请写一篇 5000 字的文章」。所以动态 shape 是必须支持的。

但动态 shape 在 ONNX 里是个麻烦事。ONNX 默认是静态 shape,你要显式告诉它哪些维度是可变的。

上面代码里的 dynamic_axes 参数就是干这个的。我把它拆开讲一下:

  • 0: "batch_size":batch 维度可变,比如一次推理 1 条还是 8 条数据
  • 1: "seq_len":序列长度可变,比如 128 还是 2048

注意:动态 shape 会带来性能损失。因为 TensorRT 要为每种可能的 shape 都生成优化策略。我的建议是:如果业务场景里输入长度相对固定(比如 512 以内),就设成静态 shape,性能更好。

我曾经踩过一个雷:设了动态 shape 后,TensorRT 构建引擎花了 3 个小时。后来发现是 shape 范围设得太宽(1 到 8192),优化空间太大。改成 1 到 2048 后,构建时间降到了 20 分钟。

3.4 我踩过的 ONNX 转 TensorRT 的雷

这部分是我最想分享的。每一个雷,都是真金白银换来的教训。

雷区一:精度对齐问题

ONNX 转 TensorRT 后,推理结果和 PyTorch 对不上。这是最常见的问题。原因通常是:

  • TensorRT 默认用 float16,PyTorch 是 float32
  • 某些算子(比如 LayerNorm)在 TensorRT 里有不同的实现

我的解决办法:先用 torch.allclose() 对比输出,误差超过 1e-3 就要排查。如果精度要求高,强制用 float32 推理。

# 强制使用 float32
import tensorrt as trt

builder = trt.Builder(logger)
config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.FP16)  # 默认开启 FP16
# 如果精度要求高,注释掉上面这行

雷区二:动态 shape 导致的内存爆炸

动态 shape 时,TensorRT 会为每个可能的 shape 分配内存。如果范围太大,显存直接爆掉。

我曾经部署一个 70B 模型,设了动态 shape 范围 1-4096。结果构建引擎时,显存占用飙到 80GB,直接 OOM。后来我把范围缩小到 1-1024,才跑通。

建议:动态 shape 的范围,根据业务实际需求来定。不要贪大。比如你的业务里最长输入是 1024,就设成 1-1024,别设成 1-8192。

雷区三:ONNX 图优化不足

ONNX 模型里可能有很多冗余节点,比如多余的 reshape、transpose。这些节点在 TensorRT 里会降低性能。

我的做法:先用 ONNX Simplifier 做一次图优化,再转 TensorRT。

# 安装 ONNX Simplifier
# pip install onnx-simplifier

import onnx
from onnxsim import simplify

model = onnx.load("model.onnx")
model_simp, check = simplify(model)
onnx.save(model_simp, "model_simplified.onnx")

雷区四:序列长度对齐问题

TensorRT 要求输入序列长度是 32 的倍数(某些架构是 64)。如果你的输入长度是 127,它会自动 padding 到 128。但 padding 会影响 attention 的计算。

解决办法:在 ONNX 导出时,显式指定 attention_mask,让模型知道哪些位置是 padding。

# 导出时带上 attention_mask
torch.onnx.export(
    model,
    (dummy_input, dummy_mask),  # 传入 input_ids 和 attention_mask
    "model.onnx",
    input_names=["input_ids", "attention_mask"],
    dynamic_axes={
        "input_ids": {0: "batch_size", 1: "seq_len"},
        "attention_mask": {0: "batch_size", 1: "seq_len"}
    }
)

3.5 本章知识体系

下面这张图,是我自己总结的 ONNX 转 TensorRT 的完整流程。你照着走,能少踩 80% 的坑。

ONNX 转 TensorRT 完整流程 HuggingFace 导出 保存权重 + tokenizer ONNX 转换 opset≥17, 动态shape ONNX 图优化 onnx-simplifier TensorRT 构建 设置精度、动态范围 精度验证 torch.allclose() 对比 生产部署 TensorRT-LLM 推理 常见问题 • 算子不兼容 → 升级opset • 动态shape内存爆炸 → 缩小范围 • 精度对不上 → 检查FP16 • 序列长度不对齐 → padding • 构建时间过长 → 优化shape范围

这张图里,我特别标注了「常见问题」区域。你每次转模型时,对照着检查一遍,能省不少时间。

3.6 小结

模型导出和 ONNX 转换,是整个部署流程里最容易被忽视的一环。很多人觉得「能跑就行」,结果到了生产环境各种翻车。

我个人总结了三句话:

  • 导出要全:权重、配置、tokenizer,一个都不能少
  • 转换要稳:opset 版本、动态 shape、算子兼容,提前排查
  • 验证要狠:精度对比、性能测试、边界情况,全部跑一遍

做到这三点,ONNX 转 TensorRT 的坑,你至少能避开 90%。剩下的 10%,嗯,那就要靠实战积累了。


专注资料整理