想象你要搬家了。旧搬家公司把沙发、书架、钢琴……所有东西一股脑塞进一口巨型木箱。想找那把最常用的阅读椅?对不起,你得把整箱翻个底朝天。更可怕的是,某些木箱里藏着机关——开箱的一瞬间,弹簧小丑弹出来,能趁你愣神时在你家里为所欲为。这就是当年用 pickle 打包模型权重时的真实处境。幸好,市场上来了一家专业搬家公司:每件物品独立成箱、贴好标签、先交清单,你指哪箱就先搬哪箱,全程不碰任何机关。这家公司叫 Safetensors,而行业总协会 HuggingFace 已把它列为首选供应商。在如今的 HuggingFace 生态里,模型权重的“搬运”——保存、下载、加载、多卡同步——几乎全都依赖这家公司。
掌握 Safetensors,你就握住了 HuggingFace 生态中模型分发与加载提速的钥匙:既能避开 Pickle 反序列化的安全雷区,也能借助 mmap 零拷贝与分片机制,让大模型训练和推理的冷启动从半小时压缩到几分钟。这是每一位 AI 工程师的必备常识。
可视化演示
展示Safetensors二进制布局的8字节头长JSON元数据与原始张量三段结构、传统拷贝与mmap零拷贝加载的差异、以及Pickle与Safetensors在安全性与完整性校验上的对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Safetensors二进制结构 Ten原始张量段占八成 8字节头长指示JSON元数据 len(8B)+meta(json)+tensors 三段式布局简洁可mmap
格式诞生背景
你终于受够了旧搬家公司:每次入住新家,都要先翻一整天大木箱才能找到阅读椅;有一次邻居因为开箱被弹簧小丑狠狠咬了一口——箱子里藏着恶意机关。这就是 2021 年前后 HuggingFace 社区反复出现的模型攻击事件:用户下载一个“热心网友”分享的权重文件,load 的一瞬间,恶意代码就在自己机器上执行了。
旧公司的名字叫 pickle。它是 Python 的标准序列化工具,能把任意 Python 对象打包成二进制文件;但解包时为了重建对象,必须执行文件里记录的“指令”。传统模型权重文件(.bin、.pt)本质上是 Python 的 dict 和 tensor,默认就走这条大木箱路径。对小白来说可以这样记:pickle 不只是搬东西,它还会照着清单在你家表演节目——清单被坏人改过,表演的就是炸弹。
随着模型规模奔向千亿参数,“整箱拆封”的代价也难以承受:加载 30GB 权重,pickle 需要先整读文件、再重建对象、再复制成 tensor,峰值内存轻松超过两倍模型大小。HuggingFace 联合社区推出了 Safetensors——名字就是 “safe tensors“ 的缩写——只存张量裸数据,彻底取消”表演节目”的通道。它要解决的第一个问题,就是读取量的浪费:
行业协会定下了新规矩:凡是新建的楼(新发布的模型),默认用专业搬家公司。Safetensors 的诞生不是格式之争的终点,而是模型安全分发的新起点。
- Pickle 的读取量旧格式反序列化必须读完整份文件,读取字节数等于文件总大小,一块都不能少。
- Safetensors 的读取量新格式只读取目标张量集合 \(A\) 中的字节,其余权重留在磁盘上不动。
- 两者的大小关系需要哪块取哪块,因此新格式读取量不超过总量,且通常远小于总量。
文件结构解析
专业搬家公司的第一件事,是交给你一份总清单:全屋有 8 只箱子,第 4 只装的是钢琴,尺寸多少、放在客厅哪个角落,写得一清二楚。没人需要拆箱才知道里面是什么。Safetensors 文件的前半部分就是这份清单。
具体来说,Safetensors 文件由三部分组成:最开头 8 个字节,用无符号整数记录“清单占了多少字节”;紧接着是 JSON 格式的清单头,记录每个张量的名字、形状 shape、类型 dtype,以及它在数据区中的起始偏移和长度;最后是真正的张量数据区——所有权重按 8 字节对齐依次排列,中间没有任何 Python 对象、没有压缩、没有脚本。
想读某个张量?读 JSON 拿偏移,直接对文件切片即可。为什么大家总说 Safetensors 天然适合随机访问?因为结构上就是“清单 + 裸数据”的二元组。而 8 字节对齐是硬件友好的关键:CPU、GPU、内存映射引擎都喜欢对齐访问,不对齐会让读取效率大打折扣。每个张量的偏移是这样算出来的:
清单加抽屉,结构上彻底“透明”。这把偏移钥匙,让 Safetensors 能精准取件、不多搬一箱。
- 上一块结束位置第 \(i-1\) 个张量的起点加长度,就是它在文件数据区中占到的末尾位置。
- 向上取整用天花板函数把末尾位置推进到 8 的倍数,保证下一个张量从对齐地址开始。
- 对齐后的新起点这就是第 \(i\) 个张量的偏移,也是 JSON 清单里记录的值,加载时直接拿来切片。
安全特性保障
新公司的合同第一条写得明明白白:我们只接“纯物品”箱,绝不在你家开箱运行任何程序。搬家就是搬东西,不是开演唱会。这句话点破了 Safetensors 与 Pickle 的本质区别:Pickle 是“数据 + 指令”的二合一,Safetensors 是纯粹的“数据”。
Pickle 的机关藏在协议本身里:反序列化时重建对象图,会触发 __reduce__、__setstate__ 这些钩子方法。攻击者只需在权重文件里藏一个恶意对象,受害者 load 的瞬间命令就被执行。这就是“模型投毒”最经典的攻击面——你以为是下载权重,其实是下载了一个远程控制程序。
Safetensors 的加载过程只做三件事:读 8 字节长度、解析 JSON、按偏移切片复制。它不会把字节变成对象图,没有任何钩子能执行代码。用风险公式来看,安全提升一目了然:
危险箱一律拒收,风险从“加载即中招”变成“永不中招”。这个安全基座确立之后,社区才敢放心地把权重文件当作普通数据到处分发。
- 被攻击的概率模型被投毒、文件被篡改等恶意事件发生的可能性,这是风险的第一个因子。
- 损失量级一旦中招,数据泄露、机器失守带来的代价,这是风险的第二个因子。
- 风险乘积安全风险是概率与损失的乘积;Safetensors 把概率 \(P_{\text{attack}}\) 直接降为零,整个风险归零。
Transformers集成
行业协会 HuggingFace 把新公司列为首选供应商之后,新楼开盘(模型发布)就默认用它,你作为住户根本不用操心搬家细节。这就是 Safetensors 在生态中最深层的角色:默认值即标准。
具体落在代码上:Transformers 库的 save_pretrained() 默认保存 model.safetensors;from_pretrained() 加载时如果目录里同时存在 .safetensors 和 .bin,会优先选择前者。你还可以显式传 use_safetensors=True 强制启用。对于还停留在老格式的模型,HuggingFace 提供了 convert 脚本,一条命令把 .bin 转成 .safetensors,权重、配置、tokenizer 全部无缝衔接。
有人会担心:加上 JSON 清单是不是浪费空间?实际上头部开销小到可以忽略。即便是分片很多的大模型,清单也只是几 KB 对几十 GB 的差距:
你只管一键 from_pretrained(),剩下的“安全搬箱”由默认规则默默完成。成为生态默认格式,是 Safetensors 最重要的里程碑。
- 头部长度JSON 清单的字节数,记录全部张量的名字、形状、类型和偏移。
- 数据总长所有张量数据的字节总和,是文件体积的绝对主力。
- 开销占比头部在文件中的比例;模型越大,这个比例越趋近于零,几乎不占空间。
零拷贝加载
专业搬家公司还有一手杀手锏:透明抽屉货架。整架货柜可以整体拉进新家——这相当于把文件映射到内存——但你只抽第 4 只抽屉,工人绝不会先把整面墙拆下来。这套机制叫内存映射(mmap)。
传统 torch.load 的路径是:分配一块大内存 → 把文件字节全部读进来 → 重建对象图 → 再复制成 tensor。等于搬家时先把整座仓库搬空,再慢慢找你要的椅子。Safetensors 则把文件直接映射到进程的虚拟内存地址空间,访问到哪个“抽屉页”,操作系统才真正从磁盘换入那一页;之后可以直接按偏移构造 tensor 视图,省掉一次全量反序列化拷贝。
这个差异在大模型上被放大到肉眼可见:几十 GB 权重,pickle 路径的峰值内存约等于两倍模型大小,Safetensors 路径约等于一倍。时间上的对比同样悬殊:
指哪搬哪,这就是 vLLM 等推理框架敢直接 mmap 权重文件的原因——大模型的冷启动,从“搬空仓库”变成了“抽一只抽屉”。
- Safetensors 的耗时mmap 之后只在首次访问每个内存页时产生一次缺页中断,之后全是直接命中。
- Pickle 的耗时整读文件、解析对象图、再复制进 tensor,三段开销串行叠加。
- 差距来源Safetensors 砍掉了 parse 与 copy 两段,只留下最少的读取开销,所以显著更快。
分布式训练协同
搬新家往往一次要搬好几套同户型:每个房间(GPU)都要放同款家具。因为每件家具都有标准尺寸标签,搬家车队不用每辆车都拆箱清点,直接照清单分配车辆和路线。这套“标签驱动调度”的思想,让 Safetensors 在分布式训练中格外好使。
分布式训练加载 checkpoint 时,几千亿参数分布在多个 rank 上。传统 pickle 的冷启动要先整读、再反序列化、再分发到各卡,光等加载就要半小时。Safetensors 配合分片(shard)机制,每个 rank 只映射自己需要的层或切片,Deepspeed、Accelerate 拿到 JSON 清单后可以直接规划每个 rank 读哪一段,加载时间从“半小时”压缩到“几分钟”。
通信侧同样受益:模型参数总量 \(P\) 是固定的,ring all-reduce 是分布式训练最常用的参数同步算法。它的总通信量有一个清晰的公式:
标准尺寸标签让车队几乎零校准就能开跑。加载快了,每次实验迭代、每次故障恢复都省下大量等待时间——大模型训练因此越来越像“流水线搬家”。
- 参数量模型权重与梯度的总字节数,是通信的基础负载,相当于要同步的“家具总量”。
- 节点数参与 all-reduce 的 GPU 或机器数量,相当于“车队车辆数”。
- 总通信量ring 方式下每条数据要沿环传递,总通信量近似为上述乘积;节点越多,越接近 2 倍参数量的上界。
格式对比选择
货比三家,现在桌上有三家搬家公司:旧木箱(Pickle)、透明货架(Safetensors)、压缩行李袋(npz)。哪个更好?取决于你要搬什么。
Pickle 的优势是“什么都能搬”——任意 Python 对象、函数、类实例都能打包,灵活至极;代价是危险且慢。npz 的优势是压缩省磁盘,但压缩数据不能直接 mmap 成张量,随机切片要先解压,而且它不记录结构化的偏移信息,谈不上零拷贝。Safetensors 不压缩,但换来的是安全、零拷贝、随机访问三件套。
所以选型建议很直白:临时调试的小对象用 pickle 无妨;磁盘极度紧张、又不需要随机访问的归档数据可以考虑 npz;凡是发布给别人的模型权重,首选 safetensors。内存占用是最直观的判据:
选择并不复杂:要给别人用、要在大模型上用,就选专业公司。如今 Safetensors 已经是 HuggingFace 生态的“默认语言”,认识它、用它,是每一位 AI 工程师的基本功。
- Pickle 的峰值内存原权重一份、反序列化的临时对象一份、再加缓冲,接近两倍模型大小。
- Safetensors 的峰值内存mmap 直接承载体,内存里只保留一份模型本体。
- 显存规划差距在几十 GB 的大模型上,一份拷贝就是十几甚至几十 GB 的显存差,往往决定能不能跑起来。
AI 应用场景
HuggingFace Hub 模型发布与下载
阶段:模型部署与 MLOps
解决的问题:传统方式把权重存成 .bin(pickle),用户下载后 load 时面临反序列化执行任意代码的风险,且模型越大下载校验、加载耗时越长;AI 团队也难以保证分发的权重在用户机器上是“安全数据”而非“可执行程序”。
具体案例:某团队微调了一个 30B 的对话模型,原计划发布 .bin 权重。安全评审发现 pickle 反序列化风险后,改用 convert 脚本转成 model.safetensors 并分片上传到 HuggingFace Hub。结果:用户侧冷启动时间从 12 分钟降到 3 分钟,下载时 Hub 自动显示“此模型使用 safetensors”的安全标识,社区信任度明显提升。
大模型分布式训练 Checkpoint 同步
阶段:模型训练与调优
解决的问题:千亿参数模型训练中,checkpoint 动辄几十 GB,传统 pickle 加载需要整读、反序列化再广播到各卡,每次恢复训练都要等待半小时以上,还会造成 CPU 峰值内存翻倍、训练集群资源空转。
具体案例:某团队用 8 卡 A100 训练 Llama 架构模型,把 checkpoint 改为 sharded safetensors 格式后,每个 rank 只映射自己负责的分片,配合 Deepspeed 的加载流程,冷启动从 20 分钟压缩到 4 分钟。训练中途断点恢复的次数一多,累计节省的时间以天计算。
弹性推理服务冷启动加速
阶段:推理服务与性能优化
解决的问题:Serverless GPU 推理场景中,每个新实例都要从冷启动加载模型,如果每次加载都做全量反序列化,首 token 延迟高得惊人;多个实例重复加载还会造成内存页缓存无法共享。
具体案例:某推理平台用 vLLM 部署开源模型,权重以 safetensors 存储在共享存储上,所有 worker 通过 mmap 映射同一份文件。由于操作系统页缓存天然共享,第二个实例启动时大量页面已在内存中,冷启动首 token 延迟从 8 秒降到 2 秒,实例扩容速度提升 4 倍。
Python 演示
# -*- coding: utf-8 -*-"""Safetensors 核心思想的最小实现 + Pickle 危险对照"""import jsonimport osimport pickleimport struct ALIGN = 8 def align_up(n): return (n + ALIGN - 1) // ALIGN * ALIGN class MiniSafeStore: def __init__(self): self._buffer = b"" self._layout = {} def save(self, tensors): body = b"" layout = {} for name, buf in tensors.items(): offset = align_up(len(body)) body += b"\x00" * (offset - len(body)) + buf layout[name] = { "offset": offset, "length": len(buf), "dtype": "i8", "shape": [len(buf)], } header = json.dumps(layout, sort_keys=True).encode("utf-8") padded = header + b"\x00" * (align_up(len(header) + 1) - len(header)) self._buffer = struct.pack("<Q", len(padded)) + padded + body self._layout = layout def header_bytes(self): n = struct.unpack("<Q", self._buffer[:8])[0] return self._buffer[8:8 + n] def load_tensor(self, name): item = self._layout[name] data_start = 8 + len(self.header_bytes()) + item["offset"] return self._buffer[data_start:data_start + item["length"]] class EvilPickle: def __reduce__(self): return (os.system, ("echo 不要在生产环境反序列化未知pickle!",)) def main(): store = MiniSafeStore() tensors = {"w1": b"\x01" * 16, "w2": b"\x02" * 32} store.save(tensors) print("JSON 头部:", store.header_bytes().decode("utf-8")[:100]) got = store.load_tensor("w2") print("只取 w2, 实际读取字节数:", len(got)) payload = pickle.dumps({"w1": tensors["w1"], "evil": EvilPickle()}) print("恶意 pickle 载荷已生成, 未来一旦 loads 即触发系统命令") if __name__ == "__main__": main()
offset = align_up(len(body))每个张量都从头开始做 8 字节对齐,保证硬件高效读取,等价于搬家清单里的“抽屉号”。layout[name] = { ... }把每个张量的偏移和长度写进 JSON 清单,这就是 Safetensors 支持随机访问的秘密。self._buffer = struct.pack("<Q", len(padded)) + padded + body8 字节长度 + JSON 头部 + 裸数据,完整的文件结构。data_start = 8 + len(self.header_bytes()) + item["offset"]按偏移直接切片取出目标张量,不整文件反序列化。return (os.system, ("echo 不要在生产环境反序列化未知pickle!",))Pickle 的__reduce__钩子:一旦loads,这行命令就会在受害者机器上执行,这就是 Safetensors 要消灭的“机关”。
常见误区
误区我听说 safetensors 加载比 pickle 快很多,是不是因为它把模型压缩了,文件更小所以读得就快?
正解不是。Safetensors 不做任何压缩,文件里就是原始张量字节。它快的真正原因是两点:一是 mmap 零拷贝,省掉了反序列化时“重建对象再复制”的两份内存开销;二是按需加载,只读目标张量的偏移区间,而不是整份文件。压缩反而会破坏随机访问和零拷贝。
为何常见:人们习惯把“体积更小”和“速度更快”绑定在一起,看到加载快就自然联想到压缩,忽略了内存映射才是核心机制。
误区零拷贝听起来像是“完全不用内存”,那我把模型存成 safetensors 是不是就能在很小的机器上跑超大模型?
正解零拷贝省掉的只是“反序列化临时拷贝”这一份内存,模型权重本身的显存/内存一分都不会少。30B 模型依然是 60GB 左右的权重,该装不下的照样装不下。Safetensors 的贡献是把加载峰值从“两倍模型”降为“一倍模型”,而不是把一倍降为零。
为何常见:“零拷贝”这个术语太诱人,容易被过度解读成“零内存”,忽略了它针对的是反序列化路径而非模型本体。
误区既然 safetensors 这么安全,那我下载别人发布的模型就万无一失了,里面的权重肯定没问题。
正解Safetensors 只保证“加载过程不执行任意代码”,不保证“权重数据本身是干净的”。攻击者完全可以先正常训练一个模型,再把有害行为训练进权重里——这叫数据投毒/权重投毒,与文件格式无关。安全加载解决的是“开箱时不会被弹簧小丑咬”,解决不了“箱子里本来就装着坏东西”。
为何常见:人们把“不会执行代码”和“内容可信”混为一谈,其实格式安全只是信任链条中的一环,模型审计与来源验证同样重要。
误区safetensors 是 HuggingFace 出的格式,是不是只有 transformers 库能读?我自己的 PyTorch 项目用不了吧?
正解Safetensors 是独立开源格式,官方库 safetensors 提供了 save_file / load_file / load 等接口,可以直接读写 dict 形式的 tensor 集合,完全不依赖 transformers。diffusers、Deepspeed、vLLM 等大量框架都在直接使用它,只是 transformers 把它设成了默认值。
为何常见:Safetensors 在 HuggingFace 生态出现频率太高,容易被误认为“官方插件”,其实它是与框架无关的通用序列化格式。
误区模型发布者把一个 60GB 的 LLM 存成一个 safetensors 文件,那我加载的时候是不是必须把 60GB 全读进内存?
正解不必。JSON 头部记录了每个张量的偏移和长度,你可以按张量名精确读取其中任意一段。实践中大模型还常用分片(shard)方式存储,每片一个 safetensors 文件,按需加载对应分片,配合 mmap 实现真正的“用多少读多少”。单文件只是组织形式,不代表加载时必须整体入内存。
为何常见:把“文件是整体”等同于“加载是整体”,忽略了头部清单为随机访问提供的可能性——这正是 Safetensors 与 pickle 最本质的结构差异。
面试题
Q1. Safetensors 文件最开头的 8 个字节记录的是什么?
- A.模型名称的文本描述
- B.标识文件格式起始魔数
- C.头部 JSON 的字节总长度← 正确答案
- D.当前文件的张量总数量
答案:C。Safetensors 的文件结构是“8 字节长度头 + JSON 清单 + 数据区”。开头的 8 个字节用小端无符号整数记录 JSON 清单占了多少字节,读取时需要先把它解析出来才能定位 JSON 起始位置。它既不是魔数,也不是模型名或张量数量。
Q2. 相比 Pickle,Safetensors 最核心的安全性提升是什么?
- A.加载时无法执行任意代码← 正确答案
- B.整个文件被加密成密文
- C.所有张量被压缩存储
- D.文件名的内容哈希值校验
答案:A。Pickle 反序列化时会重建对象图并触发 __reduce__ 等钩子,等于给文件里的代码开了执行通道;Safetensors 只存张量裸数据,加载只有“读长度、解析 JSON、按偏移切片”三步,没有任何代码执行路径。它不是加密、不是压缩、也不做哈希校验。
Q3. Transformers 库中 save_pretrained() 默认保存的权重文件名是什么?
- A.
.bin即 Pickle 文件格式 - B.
.safetensors权重文件← 正确答案 - C.
.npz压缩权重格式 - D.
.onnx模型交换格式
答案:B。HuggingFace 把 Safetensors 设为生态默认格式,save_pretrained() 默认写出 model.safetensors;from_pretrained() 加载时也优先选择 .safetensors 文件。.bin 是旧版 pickle 格式的遗留,.onnx 是另一套推理交换格式。
Q4. Safetensors 的零拷贝加载主要依赖下列哪种系统机制?
- A.共享内存机制 shm
- B.直接内存访问技术 DMA
- C.内存映射机制 mmap← 正确答案
- D.固态硬盘的缓存加速
答案:C。Safetensors 把磁盘文件通过 mmap 映射到进程虚拟内存空间,访问某个张量时才触发对应的缺页换入,并按偏移直接构造 tensor 视图,省掉了“整读 + 反序列化 + 复制”的完整拷贝路径。DMA 是硬件外设层面的技术,与文件格式无关。
Q5. Safetensors 的 JSON 头部主要存储了哪些关键信息?
- A.模型的网络层数与结构
- B.各张量偏移长度与形状← 正确答案
- C.训练超参和优化器状态
- D.数据集原始文件名列表
答案:B。JSON 清单记录每个张量的名字、shape、dtype,以及在文件数据区中的起始偏移和长度。加载时就是靠这些偏移做精确切片。网络结构、超参、数据来源属于模型配置与训练元数据,不在 Safetensors 的张量清单范围内。
Q6. 为什么 Safetensors 要求张量数据按 8 字节对齐?
- A.让数据的布局更整齐美观
- B.方便对头部整体做加密
- C.避免和 Pickle 头的混淆
- D.让硬件高效读取每个张量← 正确答案
答案:D。CPU、GPU 和内存映射引擎对对齐地址访问效率最高,对齐保证每个张量能高效、安全地用指针切片读取。它跟美观、加密、格式区分都没有关系,纯粹是性能与可移植性的考虑。
Q7. 大模型以分片(shard)形式保存为多个 safetensors 文件的主要目的是什么?
- A.并行下载且按需加载分片← 正确答案
- B.绕过单文件体积与并发限制
- C.让仓库页面展示布局更美观
- D.妨碍逆向直接读取权重文件
答案:A。分片的核心价值是并行性与按需加载:多个分片可以并发下载,各 rank/worker 只加载自己需要的分片,加速分布式训练与推理的冷启动。绕过限制、页面美观、阻碍逆向都不是设计目标,Safetensors 本身也不提供加密或混淆。
Q8. 关于 Safetensors 与内存的关系,下列说法正确的有哪些?
- A.加载时不会额外拷贝完整模型← 正确答案
- B.大模型可以完全不占用显存
- C.多进程可共享同一份页缓存← 正确答案
- D.文件越小内存占用就越小
答案:AC。Safetensors 通过 mmap 省掉了反序列化临时拷贝,所以“不会额外拷贝完整模型”正确;多个进程映射同一文件时,操作系统页缓存天然共享,所以 C 也正确。但权重本体仍需显存,B 错误;内存占用取决于模型大小而非文件大小,文件小不等于加载后内存小,D 错误。
Q9. 下列哪些属于 Safetensors 相比 Pickle 的优势?
- A.头部记录偏移支持随机访问← 正确答案
- B.加速分布式训练的冷启动← 正确答案
- C.能检测并修复中毒的模型权重
- D.被 Transformers 与 Hub 采用← 正确答案
答案:ABD。JSON 头部记录偏移和形状,天然支持随机访问(A 对);mmap 零拷贝和分片加载让分布式训练冷启动大幅提速(B 对);作为 HuggingFace 生态默认格式,被 transformers 和 Hub 广泛采用(D 对)。但 Safetensors 只保证加载不执行代码,无法检测权重里被训练出来的有害行为,C 错。
Q10. 关于 Safetensors 的文件结构,以下描述正确的是?
- A.张量数据按字节对齐排列← 正确答案
- B.内部采用 zip 格式深度压缩
- C.每个张量前都有一段校验码
- D.头部与数据区之间留有随机填充
答案:A。Safetensors 的数据区确实按 8 字节对齐排列张量,方便 mmap 和指针切片,A 正确。它不是压缩格式,B 错;它不逐张量加校验码,C 错;对齐填充是确定性计算出来的,不是随机填充,D 错。格式的设计哲学是“简单、透明、可偏移访问”。