想象你是一个大模型团队的“仓库管理员”。训练结束,你要把几百 GB 的模型权重从一个机房搬到另一个机房。现在有两家搬家公司摆在面前。第一家递给你一个漆黑的“魔法快递箱”:它说,你不需要知道里面是什么,只要拆开箱子,里面的机关会自动帮你把东西摆好。听起来很酷,但问题是——箱子一旦开启,就能执行它自己携带的任何指令,可能是整理货物,也可能是打开仓库大门、运行一段恶意脚本。这就是 Python 生态里最常见的 pickle 序列化格式。
第二家递来的不是箱子,而是一个半透明的标准货运集装箱。箱顶贴着一张 JSON 清单,清楚地写着“第几号货位放着哪一层网络的权重,数据从哪里开始、到哪里结束”。箱体内部只有整齐排列的原始二进制数据,没有任何齿轮、弹簧或会自动执行的“机关”。更妙的是,你不需要把箱子搬进客厅,只需站在仓库门口,通过一面“透视窗”直接看到货位上的数据——这就是 Safetensors 的内存映射读取。
本文就用“集装箱搬家”这个比喻,带你理解:为什么 Safetensors 比 pickle 更安全?它的格式如何设计?内存映射为什么快?以及你在真实项目中应该怎么选。
对 AI 从业者而言,掌握 Safetensors 的安全原理与正确用法,是避免恶意模型文件 RCE 攻击、优化大模型加载内存、构建可信模型供应链的基本功。
可视化演示
动态演示Safetensors内存安的核心机制,通过输入/处理/存储/输出四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Safetensors内存安:Pick Pickle反序列化的代码执行 Safetensors格式设计 核心机制可视化 Safetensors内存安整体架构与关键组件
模型序列化
搬家之前,你总得先把服务器机柜里正在“运行”的神经网络权重安稳地搬到硬盘上。模型序列化做的就是这件事:把内存中张量、结构信息变成磁盘上的一个字节流文件。一个训练好的模型可能包含数百个张量、形状、数据类型和优化器状态,序列化决定这些信息能否被完整保存、快速加载,也决定了文件在传输过程中是否值得信任。
Python 世界里最常见的“搬家队”是 pickle。PyTorch 的 torch.save 和 torch.load 长期默认使用它,因为它能保存几乎任何 Python 对象,包括类实例、函数、闭包。可是“什么都能装”的魔法箱子,恰恰是安全问题的源头:一旦箱子设计成“拆箱时自动执行里面的机关”,那这个机关就能被攻击者利用。
因此,序列化不只是“把对象变成字节”这么简单。它同时定义了一条安全边界:文件里的内容到底是纯粹的“货物清单 + 货物”,还是带着“自动运行指令”的可执行包裹。Safetensors 之所以出现,就是要把这条边界画得清清楚楚:我只搬货,不跑任何代码。选哪个搬家队,决定了你的仓库是门禁森严还是大门敞开。
Pickle漏洞
有些黑心快递箱里藏了机关:拆开箱子的瞬间,箱子会自己播放一段指令,甚至操作仓库里的其他设备。pickle 就是这种“魔法箱子”。它的设计目标是把任意 Python 对象完整还原,于是允许每个对象自定义“如何被重新创建”。这个钩子就是 __reduce__ 魔术方法。当一个 pickle 文件被反序列化时,Python 会先调用对象指定的还原函数,再传入参数执行,最终得到对象。如果攻击者把还原函数改成 os.system,那 torch.load 一个恶意文件就等同于在目标机器上运行一条 shell 命令。
所以,任何一个不可信的 .pkl、.bin、.pt 文件,都不能用 torch.load 直接打开。哪怕文件来自“朋友”或“同事”,只要经过不可信渠道传输,都可能被注入恶意载荷。pickle 的信任边界是:它本质上是一段需要被完整信任的代码,而不是一份普通数据文件。
- 魔术钩子
__reduce__是 Python 留给对象自定义还原方式的钩子,pickle 在反序列化时一定会调用它。 - 还原即调用pickle 拿到函数元组后,会执行
func(*args)来重建对象,这是协议规定的自动步骤。 - 恶意组合攻击者把
func设成os.system,把args设成攻击命令,反序列化瞬间就在机器上执行。
格式设计
Safetensors 的箱子是半透明集装箱。箱顶贴着一张 JSON 标签,标签写着每个货位的位置、每个张量的名称、形状、数据类型,以及数据在箱子里的起止位置。箱体内部没有任何“自动执行”的机械装置,只有按顺序码放的原始二进制权重。加载时,程序先读标签,再根据标签直接去对应位置取货。
Safetensors 的文件布局非常固定:文件开头是 8 字节的小端无符号整数,表示 JSON 头部有多少字节;紧接着是 JSON 元数据;再往后就是原始张量数据区。JSON 中的 data_offsets 记录每个张量在数据区内的 [start, end),由于偏移量是显式给出的,加载器不需要扫描、不需要猜测,也不会触发任何 Python 对象还原。
为了让加载和内存映射更高效,实现还会对 JSON 头部做补齐,并对张量偏移做固定边界对齐。这样设计的好处是:解析器面对的是一个极其规整的二进制结构,任何不符合规则的偏移都能被立刻识别并拒绝。
- 头部长度 H文件最前面的 8 字节,按小端序写成无符号整数,告诉解析器 JSON 头部有多长。
- JSON 元数据 M一个字典,键是张量名,值包含
dtype、shape、data_offsets。 - 数据区 D只存放权重对应的原始字节,不包含 Python 对象、函数或导入语句。
内存映射
如果说 torch.load 是“把所有箱子都搬进客厅再拆开”,那么 safe_open 就是“站在仓库门口,用遥控射灯直接照亮目标货位”。Safetensors 的加载过程会把整个文件映射到进程的虚拟内存中,而不是一次性把几百 GB 读进物理内存。操作系统只在代码真正访问某个张量时,才把对应磁盘页调入内存。
这种机制叫 mmap。对 AI 开发者最直观的感受是:加载一个 10GB 的模型文件,启动时间可能不到一秒,内存占用也很低。因为文件映射只建立了“虚拟地址 → 文件偏移”的对应关系,真正读盘是后续访问张量数据时按需发生的。
这种“按需取货”的方式特别适合超大模型。比如推理服务只需要加载前几层做试运行,或者只要某几个 LoRA 权重,都能通过切片精准读取。等计算真正用到这些数据时,操作系统才会从磁盘读取对应页,这就是惰性加载带来的速度与内存红利。
mmap 拿到文件的一个“虚拟视图”,再按清单里的偏移直接切片。整个过程没有数据复制,也没有代码执行。- 映射基础地址
mmap(fd)把整个文件映射成一段连续的虚拟地址空间,返回基础地址base。 - 数据区起点
data_start = 8 + H,也就是“8 字节头部长度 + JSON 头部”之后的位置。 - 偏移切片第
i个张量从data_start + start_i开始,到data_start + end_i结束,直接切片即可。
安全防线
安全不是靠“加载时扫描病毒”实现的,而是从设计上取消“执行”这个动作。Safetensors 的解析器只做三件事:读头部、校验元数据、返回原始数据的视图。它没有任何“根据文件内容调用用户函数”的机制,因此攻击者即使精心构造文件,也找不到代码执行入口。
更重要的是,解析器会像一个严格的安检员,全面校验文件结构与偏移。JSON 头部长度不能超过文件总大小;每个 data_offsets 必须满足开始小于结束、结束不越过数据区末尾;张量名不能重复;数据偏移必须落在合法范围内。这些校验全部发生在任何数据被访问之前。
这套“免信任文件”原则很重要:模型文件不是代码,而是不可信的外部输入。SafeTensors 的加载器不依赖“这个文件是否来自官方”,而是靠严格的格式校验来保证安全。集装箱有没有夹层不重要,因为海关只按清单验货,清单之外的一律拦下。
- 下界约束每个张量的起始偏移必须不小于 0,防止出现负数偏移。
- 顺序约束起始偏移必须小于结束偏移,防止空切片或反向切片。
- 上界约束结束偏移必须不超过数据区总长度
L,防止越界读取文件之外的内存。
格式对比
现在我们来比较三家搬家公司:魔法箱子 pickle、多功能橱柜 HDF5、透明集装箱 Safetensors。
pickle 最大优势是零门槛,什么都能装,正因如此它也是风险最高的。加载 pickle 文件本质上是执行一段 Python 字节码,文件来源一旦不可信,整个机器都可能被攻陷。除了安全问题,pickle 加载大模型时还会创建大量临时 Python 对象,速度慢、内存峰值高。
HDF5 是一个成熟的科学数据格式,支持层级结构、数据集、属性,也能保存张量。它适合科研场景里的复杂数据管理,但对大模型权重分发来说偏重:C 库依赖体积大、API 复杂、历史上有不少 C 层漏洞。它并不以“处理不可信文件”作为首要设计目标,所以不能完全替代 SafeTensors 的安全定位。
Safetensors 有意做“减法”:只保存张量和基础元数据,不保存 Python 对象;格式简单到可以用几十行代码解析;配合 mmap 可以做到零拷贝、并行读取。它牺牲了通用性,换来了加载速度、内存效率和更小的攻击面。对模型权重的发布、下载和加载来说,这种取舍非常合理。
生产实践
真实仓库里的正规流程是:任何从外部收到的 .bin 文件,先过安检转换为 .safetensors,再让业务系统用 safe_open 按需挂载。Hugging Face 生态已经全面拥抱这套流程,from_pretrained 会自动优先寻找 .safetensors 文件;只有在没有 Safetensors 版本时,才会回退到 pickle 格式。
如果你想直接读取一个 Safetensors 文件,可以使用 safetensors 库的 safe_open。加载时指定 framework="pt",返回的就是 PyTorch 可以使用的张量;最妙的是,这个张量背后是内存映射视图,真正访问到某个权重时才会发生磁盘 I/O。对于 LLM 推理引擎,这种按层加载的能力意味着更快的冷启动和更低的内存占用。
日常工作中,转换脚本也很简单:先把旧 checkpoint 用 torch.load 在可信环境中读出来,再通过 safetensors.torch.save_file 写成 SafeTensors 格式。转换完成后,旧文件就可以从模型库中移除。模型部署平台上,统一使用 SafeTensors 还可以让多个进程并发读取同一个文件而不互相阻塞,这对大规模推理服务尤其重要。
边界取舍
透明集装箱解决了“安全搬运”的问题,但并不是万能货架。Safetensors 只能保存张量,不能保存 optimizer 里自定义的 Python 回调、迭代器或任意对象。如果你的 checkpoint 需要完整恢复训练状态,还是要配合其他配置文件或可信的 pickle 方案。
另外,Safetensors 默认不加密、不压缩。它本身不追求文件最小化,也不负责隐私保护。传输前后若需要防篡改或防泄露,还得借助外部工具做签名、校验和、加密或压缩。不要因为格式安全,就误以为它可以替代完整的网络安全机制。
还有一点容易被忽略:mmap 大文件时会占用虚拟地址空间。在 64 位系统上通常不是问题,但在 32 位或内存受限的嵌入式环境里,映射超大模型仍可能遇到地址空间不足。使用建议是:对外发布的权重一律用 Safetensors;自己本机快速调试时,如果确实需要保存复杂对象,才在可信环境里短暂使用 pickle;涉及多机分发时,配合哈希校验一起使用。
AI 应用场景
模型中心下载与微调
阶段:模型获取与预处理
解决的问题:Hugging Face 等模型中心的权重文件可能被恶意替换或投毒。过去直接从网上下载 .bin 再 torch.load,等于在本地“运行”一段外部代码,很容易被 RCE 攻击。改用 Safetensors 后,下载到的是纯数据文件,加载过程不存在代码执行入口。
具体案例:团队准备微调一个开源大模型,配置 from_pretrained 时自动选择仓库里的 .safetensors 权重。代码在 CI 中对文件做哈希校验后加载,整个过程不会执行任何来自文件的 Python 指令。
大模型推理服务冷启动
阶段:模型推理与部署
解决的问题:加载 70B 模型如果用 pickle 反序列化,需要先复制到内存、再重建张量,启动慢、峰值内存高。Safetensors 配合 mmap 让服务启动时只建立文件映射,按需读取被访问的层,显著降低冷启动时间和内存占用。
具体案例:一个 LLM 推理服务同时加载多个 13B 模型副本。以前启动一次要 2 分钟、占用 30GB 内存;换成 Safetensors + mmap 后,启动只需 10 秒,且未访问的权重不占用物理内存。
模型供应链安全审查
阶段:模型资产管理
解决的问题:企业内部模型仓库混有 pickle、HDF5、SafeTensors 等格式,难以统一审计。通过自动化脚本把所有对外发布的张量权重统一转成 Safetensors,并拦截 .pkl 入库,可以从源头切断恶意代码注入路径。
具体案例:安全团队编写了一个文件扫描器,凡是检出 magic bytes 为 pickle 的文件就直接拒绝合并请求,并把 torch 权重自动转为 .safetensors 再发布到内网模型仓库。
Python 演示
import jsonimport mmapimport struct def write_safetensors(path, tensors): header_map = {} offset = 0 for name, (dtype, shape, raw) in tensors.items(): header_map[name] = { "dtype": dtype, "shape": list(shape), "data_offsets": [offset, offset + len(raw)], } offset += len(raw) header_bytes = json.dumps(header_map, separators=(",", ":")).encode() pad = (8 - len(header_bytes) % 8) % 8 header_bytes += b" " * pad with open(path, "wb") as fp: fp.write(struct.pack("<Q", len(header_bytes))) fp.write(header_bytes) for _, _, raw in tensors.values(): fp.write(raw) def safe_load(path): with open(path, "rb") as fp: head_len = struct.unpack("<Q", fp.read(8))[0] header = json.loads(fp.read(head_len)) head_len_padded = head_len + ((8 - head_len % 8) % 8) data_start = 8 + head_len_padded mm = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) result = {} for name, meta in header.items(): begin, end = meta["data_offsets"] result[name] = memoryview(mm)[data_start + begin:data_start + end] return result, mm if __name__ == "__main__": demo = { "weight": ("F32", [2, 3], bytes(range(24))), "bias": ("F32", [3], bytes(range(12, 24))), } write_safetensors("/tmp/demo.safetensors", demo) loaded, mmap_view = safe_load("/tmp/demo.safetensors") print(loaded["weight"].tobytes()[:4]) mmap_view.close()
fp.write(struct.pack("<Q", len(header_bytes)))把 JSON 头部长度按 8 字节小端序写入文件,这是后续定位数据区的起点。header_map[name] = {"data_offsets": [offset, offset + len(raw)]}记录每个张量在数据区内的起止偏移,加载时不用扫描文件。pad = (8 - len(header_bytes) % 8) % 8将 JSON 头部补齐到 8 字节倍数,保证后续数据对齐。mm = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)把文件映射到虚拟内存,而不是一次性读入内存。data_start = 8 + head_len_padded数据区起点等于“头部长度字节 + 补齐后的 JSON 头部”。result[name] = memoryview(mm)[data_start + begin:data_start + end]用内存视图按偏移切片,是零拷贝读取,不产生 Python 对象还原。
常见误区
误区很多初学者以为 SafeTensors 文件是加密的,所以安全。其实我直接把文件打开,看到的还是明文二进制数据。
正解Safetensors 的安全来自“不含可执行代码”,不是来自加密。它默认既不加密也不压缩,任何能读取文件的人都能直接解析权重。真要防泄漏,需要自己在传输层或文件外再做加密。
为何常见:因为“安全”两个字容易让人联想到加密、加壳,忽略了 SafeTensors 解决的核心问题是“反序列化时不执行任意代码”。
误区既然 PyTorch 可以用 `torch.save` 保存模型,那 SafeTensors 应该也能保存模型结构、优化器状态、自定义类实例。
正解SafeTensors 只能保存张量及少量 JSON 元数据,不能保存 Python 对象。模型结构需要单独用配置类或代码重建;优化器中的自定义对象也不能直接放进去。它是“重量集装箱”,不是“魔法工具箱”。
为何常见:因为模型训练里的 checkpoint 通常又包含权重又包含状态,初学者容易把格式本身的能力和上层库的封装混为一谈。
误区SafeTensors 是安全的,所以任何恶意文件都不可能攻破我的程序。
正解SafeTensors 降低了“任意代码执行”这一最危险的攻击面,但它并不是操作系统级安全边界。解析器本身是 C++/Rust 代码,仍可能有潜在的越界读写问题;加载后如果其他代码错误地信任偏移,也可能出问题。安全是分层设计,不是靠一个文件格式就万事大吉。
为何常见:人们容易把“更安全”理解成“绝对安全”,忽视任何软件都建立在大量底层代码之上。
误区pickle 在 Python 里很成熟,SafeTensors 是新格式,加载起来应该更慢、更占内存。
正解实际恰恰相反。pickle 需要递归重建 Python 对象,加载大模型时会产生大量临时对象,慢且吃内存。SafeTensors 通过 mmap 和偏移切片实现零拷贝加载,启动快、内存占用低,尤其在超大模型场景下优势明显。
为何常见:因为“新格式”听起来像试验品,而且很多人没有实测过两种格式在大文件下的性能差距。
误区HDF5 也是成熟的模型存储格式,应该和 SafeTensors 一样能安全加载不可信文件。
正解HDF5 主要面向科学数据管理,并非以“免信任文件加载”为设计目标。它的 C 库庞大、历史漏洞较多,也支持更多复杂结构。SafeTensors 则刻意保持极简,只存张量并加严格边界校验,两者设计目标并不相同。
为何常见:因为 HDF5 和 SafeTensors 都能存张量,容易让人忽略“可扩展性”与“安全性”是两种不同的工程取舍。
面试题
Q1. 一个同学拿到可疑的 model.pkl,最安全的做法是?
- A.用 torch.load 直接加载
- B.重命名为 .bin 再加载
- C.先用 pickle 转存一次
- D.不加载,改用可信 safetensors← 正确答案
答案:D。pickle 文件本质上是一段可执行代码,重命名、转存都不能消除危险。只有不加载它,改从可信来源获取 SafeTensors 文件,才能避开反序列化执行风险。
Q2. Safetensors 文件最前面的 8 字节表示什么?
- A.文件总长度
- B.JSON 头部字节数← 正确答案
- C.张量总数量
- D.数据区起始偏移
答案:B。Safetensors 的前 8 字节是小端无符号整数,表示 JSON 头部的字节长度。解析器先读这个长度,再精确读取元数据,才能定位后续数据区。
Q3. 关于 pickle 反序列化,下列说法正确的是?
- A.只恢复数据,不执行函数
- B.只有 import 该模块才会执行
- C.可能通过 reduce 执行任意函数← 正确答案
- D.只对 zip 文件有风险
答案:C。pickle 在还原对象时会调用 __reduce__ 返回的函数和参数,攻击者可以利用这一点执行 os.system 等任意命令。它不只影响 zip 文件,任何 pickle 流都可能含恶意载荷。
Q4. SafeTensors 的 data_offsets 相对于什么位置计算?
- A.文件开头
- B.8 字节头部之后的数据区起点← 正确答案
- C.JSON 头部字符串中间
- D.上一个张量的末尾
答案:B。SafeTensors 的数据区从 JSON 头部之后开始,data_offsets 中的 [start, end) 都是相对于数据区起点的偏移。这样设计让加载器可以基于 mmap 直接切片。
Q5. SafeTensors 中不会包含以下哪一项?
- A.JSON 元数据
- B.原始权重字节
- C.Python 可执行代码← 正确答案
- D.张量形状信息
答案:C。SafeTensors 只包含 JSON 元数据和原始张量数据,刻意不保存任何 Python 对象或可执行代码。正是这种“纯数据”设计,让它避免了 pickle 的任意代码执行问题。
Q6. SafeTensors 比 pickle 更安全的原因包括?
- A.加载时不会执行任意函数← 正确答案
- B.头部偏移经过边界校验← 正确答案
- C.文件被强加密保护
- D.只保存纯张量数据← 正确答案
答案:ABD。SafeTensors 没有代码执行入口,且加载器会严格校验偏移和长度,所以 A、B 正确。它只保存纯张量数据,D 也正确。C 错误,SafeTensors 默认不加密。
Q7. 一个 SafeTensors 文件的数据区总长度为 500,头部声明某张量偏移为 start=900, end=100。加载器应该?
- A.直接切片返回
- B.返回空张量
- C.拒绝加载并报错← 正确答案
- D.自动调整为 0 到 500
答案:C。start 不小于 end,且起始偏移已超出数据区长度,这违反 SafeTensors 的边界校验规则。安全加载器不会自行修正,而是直接报错,防止越界访问。
Q8. 关于用内存映射加载 SafeTensors,下列说法正确的有?
- A.访问哪个张量才读取对应磁盘页← 正确答案
- B.加载时无需把整个文件复制进内存← 正确答案
- C.文件删除后映射数据必定立即崩溃
- D.不同张量可用偏移切片获得← 正确答案
答案:ABD。mmap 是惰性加载,访问到某页才读盘,也避免整文件复制,因此 A、B 正确。D 正确,SafeTensors 正是靠偏移切片取张量。C 错误,文件删除后已建立的映射通常仍可访问直到解除映射,不会“必定崩溃”。
Q9. 以下哪些内容适合用 SafeTensors 保存?
- A.大模型权重与 shape← 正确答案
- B.optimizer 中的自定义 Python 回调
- C.LoRA 权重← 正确答案
- D.训练日志 logger 实例
答案:AC。SafeTensors 只能保存张量和 JSON 元数据,所以模型权重、LoRA 权重适合使用。自定义回调、logger 实例是 Python 对象,pickle 才能保存,但它们不适合也不应该放进 SafeTensors。
Q10. 和 HDF5 相比,SafeTensors 的核心优势是?
- A.支持更复杂的数据层级
- B.自动加密且文件更小
- C.面向不可信文件加载更安全← 正确答案
- D.内置 GPU 调度算法
答案:C。HDF5 擅长复杂层级结构,但这不是 SafeTensors 的卖点;SafeTensors 也不负责加密压缩,更与 GPU 调度无关。它的核心优势是用极简纯数据格式加严格校验,让不可信文件难以发起代码执行攻击。