第 19 篇 · 第四部分 · 压缩与工程落地
模型文件格式全景:pt、safetensors、onnx、gguf 该如何选择
模型训练完后需要保存,而保存的方式有多种。一旦选错,要么加载慢得难以接受,要么根本部署不到目标设备上。
一、为什么"格式"这件事值得单独写一篇
模型训练完后需要保存带走。你可以存成 PyTorch 的 .pt,也可以另存成 safetensors,或者导出成 .onnx、转成 .gguf。看似都是把权重写进文件,实际差别不小——有的加载要几十秒,有的可以随取随用;有的能在浏览器中运行,有的只能在特定框架里读取;有的文件本身还带有一些安全隐患。
选错格式的直接后果只有两种:要么加载慢得难以接受,一个几 GB 的模型读盘耗时半分钟,服务冷启动时用户体验会明显下降;要么根本部署不到目标设备上去。这一篇就把几大主流格式逐一说明,顺便说明一个模型目录里那些各类文件的用途。
更具体地说:加载一个 7B 模型的权重,safetensors 通过内存映射可能一两秒就绪,而 pickle 需要逐个对象反序列化,耗时十几秒以上也不罕见。冷启动场景里,这十几秒就决定了能否达到上线标准。
二、.pt / .pth / .bin:PyTorch 的 pickle 和它的安全麻烦
PyTorch 默认的保存方式是 torch.save,一般保存为 .pt 或 .pth;Hugging Face 早期又习惯把权重存成 .bin,本质上是同一类。它的底层是 Python 的 pickle。
pickle 是 Python 的"万物皆可序列化"利器,一个 torch.save(model) 能把整个模型对象——包括那些 Python 层级的类结构——原样序列化下来。使用起来确实方便:保存时不加挑选,读取时一行 torch.load 就能还原。
风险也正源于此。pickle 反序列化的时候,会实际执行文件里记录的指令来重建对象。这意味着在没有启用安全限制的情况下,读取一份来源不明的 .pt 文件等于运行一段陌生代码。2025 年 Hugging Face 上就处置过这类投毒的模型文件,攻击者把恶意代码藏进 pickle 里,下载者就可能受害。较新的 PyTorch(2.6 起)把 torch.load 的默认行为改成只加载张量(weights_only=True),封堵了这条路径,但更早的版本和显式关闭该选项的代码仍暴露在这类风险中。所以社区的安全建议很统一:只加载你信任来源的 .pt,或直接改用 safetensors。
它的用法却很简单,两行即可完成,风险也正集中在这两行之中:
torch.save(model.state_dict(), "model.pt")
state = torch.load("model.pt") # 旧版本默认不设防,可能执行陌生代码
三、safetensors:只存张量,加载快也更安全
safetensors 是 Hugging Face 牵头制定的一种格式,思路极其克制:它只存张量,其余一概不涉及。文件开头是一小段 JSON 头,记录每块张量的名称、类型、在文件里的偏移量与长度;后面跟着裸张量数据。
这个头部 JSON 很小,通常只有几十 KB,里面列着每块张量的名字、dtype、shape 和字节偏移。真正占体积的是后面的裸数据,解析开销几乎为零。
这个设计顺便解决了两件事。安全上,因为它没有任何可执行代码,反序列化就是纯读写内存,天然不会执行陌生逻辑。速度上,因为偏移量都写在头部,加载时可以直接 mmap 内存映射,跳过解析过程,按需读取所需的块。同样是 13GB 的模型,safetensors 的加载时间往往比 pickle 短不少——尤其模型规模一大,pickle 逐个对象反序列化的开销就凸显了。
原因就是内存映射:它把文件当作内存使用,张量在原地被访问,而不是先整块读进来再拷贝一遍。少了那次全量拷贝,内存峰值也跟着降下来。
它还有个值得称道的特性:文件自描述、边界清楚,即便加载中途失败,也不会让已读入的张量处于混乱状态。如今 Hugging Face 上绝大多数开源模型的权重默认就是 safetensors,它已经成为事实标准。
四、.onnx:跨框架的图格式
上一篇专门讲过 ONNX,这里只从"文件格式"的角度补充说明一点。它与前面几种最显著的区别在于:它存的不只是权重,还有完整的前向计算图。权重(初始化器)嵌在图里,算子和连接关系也在里面。
所以 .onnx 是自包含的:不需要任何训练框架的代码就能运行,任何支持 ONNX 的运行时即可用于推理。代价是它更"重"——导出时需要把图冻结下来,动态控制流往往无法表达;同时它也不适用于继续训练,适合部署,不适合再次修改。
五、.gguf:为 llama.cpp 和端侧而生,单文件带元数据
GGUF 全称 GPT-Generated Unified Format,是 llama.cpp 项目推出的格式。它最便利的一点是把整个模型收成一个文件:权重在里面,分词器在里面,超参、聊天模板这些元数据也在里面。
为什么这么设计?因为端侧用户往往就是拉取一个文件到本地就能运行。让用户在手机或笔记本上放置七个配置文件,还要分辨每个文件的用途,体验较差。GGUF 一个文件即可满足,配上 llama.cpp 这类纯 C/C++、无重依赖的运行时,一个可执行文件加一个模型文件,即可完成。
GGUF 天然支持多种量化等级,文件名里那串 Q4_K_M、Q5_K_S、Q8_0 就是量化方式和位宽的标记。同一份模型,你可以选择要 4bit 还是 8bit,体积和精度自己权衡。
那串后缀也不是随意命名的。Q4 表示 4bit,K 指它属于 K-quant 系列(把权重分块、不同块用不同精度处理),后面的 M、S 代表中等或较小规模。同样是 4bit,Q4_K_M 一般比更粗糙的量化在精度上更稳定。
六、一张格式对照表
把上面几种放在一起比较,差别一目了然。
| 格式 | 存了什么 | 体积(7B fp16 为例) | 加载速度 | 可移植性 | 典型用途 |
|---|---|---|---|---|---|
| .pt / .pth / .bin | 张量 + pickle 对象 | 约 13–14GB | 中等偏慢 | 限 PyTorch | 训练、存档 |
| .safetensors | 仅张量 | 约 13–14GB | 快 | 多框架可读 | 分发、继续训练 |
| .onnx | 计算图 + 权重 | 随精度,通常相近 | 依赖运行时 | 跨框架跨语言 | 异构部署 |
| .gguf | 权重 + 元数据(可量化) | 4bit 约 4GB | 快 | llama.cpp 生态 | 端侧推理、本地跑 |
表里的体积会随量化变。safetensors 里也可以放 4bit 或 int8 的权重,GGUF 也一样;体积主要取决于用什么精度存,而不只是文件后缀。
补充一点容易被忽略的:可移植性那一栏里,safetensors 虽然写着"多框架可读",但它本身不含计算图,读入之后还需要对应框架的模型定义才能运行;ONNX 才是那个无需额外依赖即可执行的。
七、转换链路怎么走
实际工作中,格式经常需要在不同形式之间转换,以下是几条常见路线:
- PyTorch → safetensors:训练完成后把 .pt 转成 safetensors 再分发,最便捷,也顺便规避了 pickle 的风险。
- PyTorch → ONNX:交给 ONNX Runtime 之类的去做部署,上一篇写了流程。
- Hugging Face → GGUF:llama.cpp 仓库中提供了转换脚本,先把权重转成 gguf 中间态,再按所需的量化等级压缩一遍。
- 任意格式互转的中转站:往往先统一回 safetensors,再进行后续转换,因为它是各方都认可的"通用语言"。
转换过程并非无损。每次转换都可能碰上算子映射失败、精度微妙漂移,转换完成后务必用一两条真实样本比对一遍输出,不要仅凭转换脚本退出码为 0 就认定成功。
八、一个模型目录里都有哪些文件
打开一个 Hugging Face 模型仓库,会看到许多文件,各有各的用途:
config.json:模型结构超参,多少层、多少头、hidden 维度多大。model.safetensors(或 .bin):权重本体,可能被切成若干分片,如model-00001-of-00004.safetensors。tokenizer.json/tokenizer_config.json:分词器的词表和规则,文本进入模型前的第一道环节。generation_config.json:生成相关的默认参数,temperature、top_p、eos_token 之类。special_tokens_map.json:特殊 token 的映射表。README.md:模型卡,写着用法、许可、训练数据说明。
权重分片的规则是"单个文件不超过某个大小(常见 5GB 上下)",方便在网络状况不稳定的环境中下载,加载时运行库会自动将分片重新合并,使用者无需操心。
这些文件之间还有依赖关系。config.json 里写着 num_hidden_layers、hidden_size 这些超参,运行时依据它查找对应形状的权重;分词器配置和词表不一致,编码出来的 token 就全错位。少一个文件,模型往往是直接加载失败,而不是勉强运行。
九、按场景给结论
回到最实用的问题:我到底该存成什么。这里给出几条明确的建议。
- 刚训练完要存档、还要继续训练:safetensors,不再使用 pickle 了。
- 要部署到多种硬件、多语言环境:ONNX,交给对应的运行时。
- 要在本地、手机、Mac 上直接运行:GGUF,配 llama.cpp,一个文件即可部署。
- 只在自己机器上、永远用 PyTorch:使用 .pt 也未尝不可,但请只读取自己产出或可信来源的文件。
格式选择没有绝对的对错,也没有哪种"永远最优"。目标环境的具体情况是什么样,就选择最适合该环境的格式。下一篇会把 ONNX 这条链路从导出一直推进到压测,真正落实到设备上。
还有一点值得提醒:格式一旦定下,团队里所有成员都需保持一致。有人用 .pt 存、有人用 safetensors 发,仓库里混用两套文件,加载脚本也需分别编写两套——这种混乱带来的消耗,甚至超过选错格式本身。
参考资料
- huggingface/safetensors 开源仓库. github.com/huggingface/safetensors(格式设计与实现)
- GGUF 格式规范文档. ggml/docs/gguf.md(文件结构与元数据定义)
- llama.cpp 开源仓库. github.com/ggml-org/llama.cpp(端侧运行时与格式转换脚本)
- ONNX 官方网站. onnx.ai(跨框架图格式规范)
- PyTorch 官方文档, Serialization Semantics. pytorch.org/docs/stable/notes/serialization.html(pickle 保存与安全说明)