想象你搬家。训练好的模型就像一个塞满整个房间的家当:几十万个参数张量、优化器的状态、训练轮数,全堆在一起。要把它们搬到新家,市面上有三种打包方案。第一种是神秘纸箱,随手把所有东西塞进去——这就是 torch.save,什么都能装,但开箱要依靠一个叫 pickle 的“自动拆箱器”,它会把箱子里每一件东西原样激活,包括可能藏在里面的整蛊蛇;到了新家,还必须请同一个搬家师傅(相同的 Python 环境)才能打开。第二种是透明收纳箱,箱盖上贴一张清单,写清楚每件物品的尺寸和位置——这就是 SafeTensors,想看哪件物品直接伸手拿,不用把全屋东西倒出来找。第三种是分房间分拣发货——Megatron 把权重按 GPU 切成好几箱,几辆小车同时搬,到了新家再照图纸拼装。选择哪种箱子、怎么把旧箱子换成新箱子,就是本文要讲清的 Checkpoint 格式与兼容性问题。

岗位关联

打开、转换、合并各种 checkpoint,是 AI 工程师的日常。搞懂三种格式的来龙去脉,你既能为团队避开从天而降的安全事故,也能在跨框架迁移时少加三天班。

目录
  1. 可视化演示
  2. torch.save 机制
  3. SafeTensors 特性
  4. Megatron 切分
  5. 兼容问题剖析
  6. 格式转换工程
  7. 选型决策策略
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示连续信号的采样过程及重建效果,通过步进控制观察不同采样率对信号恢复的影响,演示奈奎斯特定理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

原始连续信号 蓝色曲线为原始模拟信号,由多个频率分量叠加而成 f(t) = sin(2πft) 点击下一步开始采样过程

torch.save 机制

搬家那天,打包师傅老王号称“什么都能装”:随手一个纸箱、一卷胶带,把衣服、碗筷、路由器一股脑塞进去,箱子上一个字都不写。这就是 torch.save 的个性——它把整个 Python 世界都当作可打包对象,不仅是模型的张量,连训练轮数、学习率,甚至你自己定义的小工具类,统统塞进同一个箱子。打包时确实爽,开箱时就有点慌。

技术上说,torch.save 的底层是 Python 的 pickle 序列化协议。pickle 把内存里的 Python 对象图(对象之间如何互相引用)翻译成一串字节,torch.load 再把字节还原成对象。听起来完美,但有一个致命点:pickle 在“还原”时会执行字节流里记录的任意函数调用。这就像那只纸箱自带一个“自动拆箱器”,它会激活箱子里的每一项内容,包括藏着的整蛊蛇。

正因如此,加载不可信的 checkpoint 等于运行别人的程序。PyTorch 2.0 起提供 torch.load(path, weights_only=True),只允许还原张量、数字、字符串这类“标准零件”,拒绝还原自定义类,把风险挡在门外。另一个隐患是环境依赖:pickle 记录的是“类所在的路径”,比如 my_module.MyModel。你换台机器、改了类名、升级大版本,开箱时就报错——因为只有旧家的搬家师傅才认识这只箱子。

所以 torch.save 是“纸箱协议”:方便、万能,但黑箱、挑师傅。知道自己打包了什么、交给谁拆,是使用它的底线。

file_size=∑i=1npi×s(ti)\mathrm{file\_size} = \sum_{i=1}^{n} p_i \times s(t_i)
一个 checkpoint 文件有多大?把每个张量的“元素个数”乘上“每个元素的字节数”,再全部累加。
  1. 累加求和把模型中所有张量逐一相加,得到整个 checkpoint 的总字节数。
    ∑i=1n\sum_{i=1}^{n}
  2. 参数个数$p_i$ 表示第 $i$ 个张量的元素个数,比如 4096×4096 的矩阵,$p=16{,}777{,}216$。
    pip_i
  3. 单元素字节$s(t_i)$ 由 dtype 决定,fp32 占 4 字节、fp16 和 bf16 占 2 字节、int8 占 1 字节。
    s(ti)s(t_i)

SafeTensors 特性

隔壁搬家公司用的是透明收纳箱,每个箱子盖上贴一张清单:第一格是“fc1.weight“,第二格是”fc1.bias”,表格里写明每件物品的尺寸和位置。这就是 SafeTensors 的发明哲学——把元数据与数据分开,先让所有人看清箱子里有什么,再决定要不要取。

SafeTensors 用一段 JSON 做清单头部,正文是排列整齐的权重原始字节,没有任何 Python 对象引用。读取时,加载器借助操作系统“内存映射”机制把文件映射进虚拟内存,再根据清单里的偏移量直接算出目标张量的地址。你只取需要的权重,系统按页懒加载,不需要把整个文件解出来——这就是“零拷贝”。冷启动一个大模型,从“把几百 GB 全部反序列化”变成“按需读几页”,加载耗时可以相差数倍到十几倍。

安全性上,因为文件里没有函数调用记录,解析器只会读取字节、构造张量视图,恶意代码没有执行入口。它只支持张量这类标准零件,装不了任意 Python 对象——这既是限制,也是安全感来源。

透明箱的代价是只能装“标准零件”,任意 Python 自定义对象需要先转成张量才能存储。但换来的是透明、安全和快,这也是它成为 Hugging Face 社区默认格式的原因。

addr(k)=base+hdr_len+ok\mathrm{addr}(k) = \mathrm{base} + \mathrm{hdr\_len} + o_k
想取第 $k$ 个张量?base 是文件映射到内存的起点,加上清单长度和该张量的偏移,地址直接算出。
  1. 映射基址$\mathrm{base}$ 是文件被内存映射后的起始地址,加载时操作系统按页懒加载、不真读盘。
    base\mathrm{base}
  2. 清单长度$\mathrm{hdr\_len}$ 由文件前 8 个字节记录,告诉读取者 JSON 说明文档有多长。
    hdr_len\mathrm{hdr\_len}
  3. 张量偏移$o_k$ 是在 JSON 清单里查到的第 $k$ 个张量的数据偏移量,由它直接定位数据。
    oko_k
  4. 零拷贝取数拿到地址后直接构造张量视图,全程不复制数据、不执行代码。
    addr(k)\mathrm{addr}(k)

Megatron 切分

如果家里有 8 个房间要同时搬,全塞进一个大纸箱显然很笨。Megatron 的做法是“分房间分拣”:按 GPU 编号,每个房间(rank)一箱,箱子外面写明“本箱装的是哪个矩阵的哪一段”。搬家公司派 8 辆小车同时拉,效率翻倍,但到了新家必须照图纸把各部分拼回去。

张量并行把一个大矩阵切成 $TP$ 片分给 $TP$ 张卡:列并行按列方向切,行并行按行方向切,每张卡只保存它负责的切片,训练时只做自己那部分矩阵乘法。流水线并行则按层切分——就像 GPT 的前几层住“主卧”,中间层住“次卧”。两种切分叠加后,模型不是一整块,而是分布在多卡上的拼图。

于是 Megatron 的 checkpoint 不是一个文件,而是一组 rank-*.pt 分片,每个分片只含该 rank 的那段权重。要恢复完整模型,必须先把分片按切分方向拼接回去,还要处理融合布局问题,例如多头注意力里 Q、K、V 三个矩阵被拼成一个整体保存。这就是为什么 Megatron 的产物不能拿到单卡直接 torch.load。

分箱搬运的好处是并行和低单机内存,代价是“图纸”复杂:谁切的、怎么切的、箱子顺序如何,都必须靠专门工具还原。想拿回家直接用?不拼图可不行。

Wlocal(r)=Wglobal[:,r DTP:(r+1) DTP]W^{(r)}_{\mathrm{local}} = W_{\mathrm{global}}\left[:, \frac{r\,D}{TP} : \frac{(r+1)\,D}{TP}\right]
列并行下,第 $r$ 张卡保存的是全局矩阵的第 $r$ 段列。
  1. 全局权重$W_{\mathrm{global}}$ 是原始大矩阵,形状为 $N \times D$,$D$ 是输出维度。
    WglobalW_{\mathrm{global}}
  2. 并行划分$TP$ 是张量并行数,输出维度 $D$ 被等分成 $TP$ 段,每段 $D/TP$ 列。
    DTP\frac{D}{TP}
  3. 设备序号$r$ 是当前 GPU 的编号,从 0 到 $TP-1$,决定它拿第几段。
    rr
  4. 切片取列列区间 $[rD/TP, (r+1)D/TP]$ 就是第 $r$ 张卡负责的权重块。
    [:,r DTP:(r+1) DTP]\left[:, \frac{r\,D}{TP} : \frac{(r+1)\,D}{TP}\right]

兼容问题剖析

搬家最头疼的不是箱子沉,而是新家不认旧标签。你在纸箱上写“老床单”,新家管家只认“卧室-床品-编号001”。torch.save 里记录的类路径、state_dict 的 key 命名,换一个框架就是另一套标签体系,这就是兼容性问题的根源。

兼容问题主要有三类。第一类是跨版本:PyTorch 升级后某些序列化结构变化、类路径移动,旧权重加载失败,就像旧家钥匙插不进新锁。第二类是跨框架命名:同一个模型在不同框架里的 key 完全不同,比如 transformers 里一个注意力层叫 encoder.layer.0.attention.query.weight,而 Megatron 侧可能是一个融合好的 query_key_value.weight。第三类是结构布局差异:同样是 QKV,一个框架拆成三个矩阵,另一个框架合并成一个矩阵,形状对不上,直接加载必然报错。

因此跨框架加载前,先做 key 映射:写一个 $f(\mathrm{old})=\mathrm{new}$ 的翻译函数逐键改名,再做形状和 dtype 校验。映射就是“新旧标签对照表”,是迁移时唯一可靠的翻译官。

loadable=(K^=Kt)∧⋀k∈K^(shape(w^k)=shape(wk))\mathrm{loadable} = \left(\hat K = K_t\right) \land \bigwedge_{k \in \hat K} \left( \mathrm{shape}(\hat w_k) = \mathrm{shape}(w_k) \right)
加载成功的条件是:映射后的键集合与目标框架完全一致,且每个键对应的形状都相同。
  1. 映射键集$\hat K = \{f(k) \mid k \in K\}$,把旧 key 逐一带入翻译函数得到新 key 集合。
    K^={f(k)∣k∈K}\hat K = \{f(k) \mid k \in K\}
  2. 键完全对齐映射后的集合必须与新框架要求的键集合 $K_t$ 一模一样,多一个少一个都不行。
    K^=Kt\hat K = K_t
  3. 逐键校验对每个键,检查新旧权重的形状是否一致,任一不一致整个加载失败。
    shape(w^k)=shape(wk)\mathrm{shape}(\hat w_k) = \mathrm{shape}(w_k)
  4. 逻辑与所有条件同时成立才算成功,这就是加载器底层校验的逻辑。
    ⋀\bigwedge

格式转换工程

既然三种箱子各有脾气,搬家公司提供“重新装箱”服务:把旧纸箱里的东西全部取出、清点、贴上透明标签、再装进新箱子。模型世界里,这叫格式转换管线——把 torch.save 的 pickle 流转换成 SafeTensors,或者把 Megatron 分片归并成完整权重。

转换管线本质是三步:load(读旧格式)、mapping(重命名 key)、save(写新格式)。难点在内存:朴素做法是先把整个模型从旧文件读进内存,全部改名后再写成新文件,内存里同时存在旧、新两份数据,峰值内存约等于两倍模型大小。对大模型来说,几百 GB 的显存/内存根本扛不住。

优化做法是逐张量流式处理:读一个张量、改写一个张量、写一个张量,峰值内存降到单个最大张量的大小。Hugging Face 的转换脚本、transformers-cli 等工具正是这么设计的。自己写转换脚本时,还要留意 dtype 与 8 字节对齐问题。重新装箱最忌贴错标签——转换后必须做全套校验:key 集合相等、所有 shape 相等、权重数值抽样一致,才算真正完工。

Mnaive=2∑k∣Wk∣d,Mstream=max⁡k∣Wk∣dM_{\mathrm{naive}} = 2\sum_k |W_k| d, \qquad M_{\mathrm{stream}} = \max_k |W_k| d
朴素转换要两倍模型内存,流式转换只吃最大那个张量。
  1. 朴素方案先全部读入再全部写出,新旧两份同时在内存,所以乘 2。
    2∑k∣Wk∣d2\sum_k |W_k| d
  2. 流式方案一次只处理一个张量,峰值内存退化为最大的单个张量。
    max⁡k∣Wk∣d\max_k |W_k| d
  3. 字节因子$d$ 是每个元素占的字节数,由 dtype 决定,转 fp16 能直接减半。
    dd
  4. 权衡取舍流式牺牲一点 IO 随机性,换来内存可承受,大模型转换的必要条件。
    Mnaive≫MstreamM_{\mathrm{naive}} \gg M_{\mathrm{stream}}

选型决策策略

搬家前,正常人会算笔账:请几个工人、买多少箱子、哪些东西怕摔。Checkpoint 选型也是同样的道理——没有最好的箱子,只有最适合这趟搬家的箱子。想清楚三件事:安全底线、加载速度、生态兼容。

决策可以用一个加权打分来量化:给安全、速度、兼容性各打一个 0 到 1 的分,再按场景配权重。加载第三方模型时,安全权重拉到最高,选 SafeTensors;自有训练链路全面依赖 Megatron 分布式训练,兼容性权重最高,继续用 Megatron 分片;做推理服务冷启动,速度优先,safetensors 的 mmap 优势明显。公式是简单的加权和,但权重系数来自真实业务场景。

比个人偏好更重要的是团队规范:把“对外发布一律存 safetensors、训练产物转出前先归并校验、转换脚本进 CI”写进工程约定,能省掉无数个“为什么加载失败”的深夜排查。选型不是一次性的,而是持续沉淀的团队经验。

回到搬家比喻:纸箱、透明箱、分拣箱各有主场。把安全、速度、兼容三件事想清楚,你就不再是被格式坑的新手,而是指挥搬家全局的老司机。

S=w1 Safety+w2 Speed+w3 CompatS = w_1\,\mathrm{Safety} + w_2\,\mathrm{Speed} + w_3\,\mathrm{Compat}
给三项打分,按场景配权重,总分最高的格式胜出。
  1. 三档打分$\mathrm{Safety}$、$\mathrm{Speed}$、$\mathrm{Compat}$ 分别为安全、速度、兼容性打分,取值 0 到 1。
    Safety,Speed,Compat\mathrm{Safety}, \mathrm{Speed}, \mathrm{Compat}
  2. 场景权重$w_1, w_2, w_3$ 由团队和场景决定,例如医疗合规场景 $w_1$ 极大。
    w1,w2,w3w_1, w_2, w_3
  3. 加权求和总分越高越适合当前场景,选型就变成了可解释的计算。
    S=w1 Safety+w2 Speed+w3 CompatS = w_1\,\mathrm{Safety} + w_2\,\mathrm{Speed} + w_3\,\mathrm{Compat}

AI 应用场景

开源模型的安全分发

阶段:模型发布与存储

解决的问题:传统方式下,Hugging Face 上的 checkpoint 多用 torch.save 保存为 .bin,用户下载后 torch.load 存在执行恶意代码的风险——曾出现过下载量极高的“投毒”模型事件。改用 SafeTensors 后,加载器只读 JSON 清单和原始字节,没有代码执行入口,从根上化解投毒风险。

具体案例:某开源大模型发布时同时提供 pytorch_model.bin 和 model.safetensors,后者成为官方推荐下载项;平台后台用转换管线把训练产物统一重装成透明箱,并校验 key 集合与形状后上线。

Megatron 训练到推理服务的归并

阶段:模型训练与部署衔接

解决的问题:8 卡张量并行训练完,产出 8 个 rank-*.pt 分片,推理框架需要完整权重和统一的 transformers 命名。人工拼接既慢又易错,且分片里还藏着 QKV 融合布局差异。用转换脚本按切分方向归并、映射 key、落盘 safetensors,再抽样校验数值,就能自动化衔接训练与推理。

具体案例:某团队训练 70B 模型后,通过“读取全部 rank 分片 → 按维度拼接 → 映射成 HF 命名 → 校验 shape → 落 safetensors”的管线,四小时内完成从 Megatron 产物到 vLLM 可直接加载权重的转换,避免了两天的返工。

跨框架迁移的标签翻译

阶段:数据准备与模型迁移

解决的问题:从 PyTorch 迁移到国产推理框架时,权重 key 命名、dtype、布局都不同,逐层手改极易出错。借助 key 映射表与格式转换工具,先把旧权重统一转成 safetensors 中间格式,再利用 mmap 边读边映射,实现低成本迁移。

具体案例:某算法团队将内部微调好的 BERT 类 checkpoint 迁移到自研推理引擎,映射了 199 个 key,统一转为 fp16 的 safetensors,迁移后首轮前向结果与原框架完全一致。

Python 演示

python81 行
# 演示三种 Checkpoint 格式与转换流程# 运行前: pip install torch numpyimport jsonimport structfrom collections import OrderedDict import torchimport torch.nn as nn  class TinyModel(nn.Module):    def __init__(self):        super().__init__()        self.fc1 = nn.Linear(4, 8)        self.fc2 = nn.Linear(8, 2)     def forward(self, x):        return self.fc2(torch.relu(self.fc1(x)))  def save_as_safetensors(state_dict, path):    # 手写最小版 SafeTensors: JSON 清单 + 纯字节数据区    entries = {}    offset = 0    for name, tensor in state_dict.items():        t = tensor.contiguous()        nbytes = t.numel() * t.element_size()        entries[name] = {            "dtype": str(t.dtype).split(".")[-1],            "shape": list(t.shape),            "data_offsets": [offset, offset + nbytes],        }        offset += nbytes    meta = json.dumps(entries).encode("utf-8")    meta += b" " * ((8 - len(meta) % 8) % 8)    with open(path, "wb") as f:        f.write(struct.pack("<Q", len(meta)))        f.write(meta)        for name, tensor in state_dict.items():            f.write(tensor.contiguous().numpy().tobytes())  def main():    model = TinyModel()     # 1) torch.save: 神秘纸箱, 一切靠 pickle    torch.save({"model": model.state_dict(), "acc": 0.91}, "demo.pt")    loaded = torch.load("demo.pt", weights_only=True)    print("torch.load 恢复 acc:", loaded["acc"])     # 2) 跨框架 key 映射: 旧家钥匙换新家锁    raw = model.state_dict()    renames = {        "fc1.weight": "encoder.layer.0.attention.query.weight",        "fc1.bias": "encoder.layer.0.attention.query.bias",        "fc2.weight": "classifier.weight",        "fc2.bias": "classifier.bias",    }    renamed = OrderedDict((renames[k], raw[k].clone()) for k in raw)     # 3) 模拟 Megatron 列并行: TP=2 切分再归并    W = model.fc2.weight.data    shard0, shard1 = W[:, :4], W[:, 4:]    merged = torch.cat([shard0, shard1], dim=1)    assert torch.equal(merged, W)    print("TP 分片归并校验通过:", list(merged.shape))     # 4) 存成 SafeTensors: 透明箱 + 清单    save_as_safetensors(renamed, "demo.safetensors")    with open("demo.safetensors", "rb") as f:        hlen = struct.unpack("<Q", f.read(8))[0]        header = json.loads(f.read(hlen))    print("safetensors keys:", list(header.keys()))     # 5) 转写校验: 清单与源完全一致    assert set(header) == set(renamed)    print("格式转换与校验通过")  if __name__ == "__main__":    main()
目的:演示 torch.save 的 pickle 装箱、安全加载、key 映射、TP 分片归并,以及手写一个最小 SafeTensors 序列化器。
  1. torch.save({"model": model.state_dict(), "acc": 0.91}, "demo.pt")把模型权重和指标随手塞进纸箱,底层是 pickle。
  2. loaded = torch.load("demo.pt", weights_only=True)只拆“标准零件”,拒绝自定义类,挡住整蛊蛇。
  3. renamed = OrderedDict((renames[k], raw[k].clone()) for k in raw)逐 key 更换新家标签,保留克隆避免引用旧张量。
  4. shard0, shard1 = W[:, :4], W[:, 4:]模拟 TP=2 的列并行切分,每个分片只有一半列。
  5. merged = torch.cat([shard0, shard1], dim=1)沿列方向把分片拼回完整矩阵,Megatron 归并的核心。
  6. meta = json.dumps(entries).encode("utf-8")箱盖上的物品清单,记录每个张量的 dtype、shape 与偏移。
  7. f.write(struct.pack("<Q", len(meta)))文件头用 8 字节记录清单长度,读取方先读它再跳转到清单。
  8. for name, tensor in state_dict.items():逐张量写入纯字节,不做任何压缩,透明且零冗余。

常见误区

误区一直用 torch.save 存 checkpoint,自己存自己取,觉得 pickle 风险与自己无关;从网上下载的 .pt 文件也敢直接 torch.load。

正解pickle 的反序列化等于让字节流里的代码在你机器上执行。自己保存的文件也可能被替换、被污染;下载的第三方模型更可能是精心仿冒的整蛊箱。对策有三:只信来源、加载时用 weights_only=True、对外分发优先转 safetensors。

为何常见:初学者大多只在自己电脑上存取,没有把“别人的文件”当成“可执行程序”来警惕。

误区.safetensors 秒开,于是认为它压缩了权重,文件更小、更省磁盘。

正解两种格式存的都是原始字节,都不压缩;safetensors 只多一个 JSON 清单头,体积几乎一样。快是因为 mmap 零拷贝按需读取,不是文件变小。torch.save 的 pickle 流反而包含大量 Python 对象描述开销,文件常常更臃肿。

为何常见:人们把“加载快”和“体积小”直觉捆绑,被加载速度误导。

误区把 demo.pt 直接改名为 demo.safetensors,觉得扩展名对了就能被新工具加载。

正解后缀只是名字,内部字节结构完全不同。safetensors 解析器读 JSON 清单、按偏移取数据;pickle 流是 Python 对象图。改名不改字节,解析必然失败。真正的转换必须走“读出旧格式 → 重写新格式”的流程。

为何常见:日常改后缀(.txt 改成 .md)的经验给人惯性,误以为格式由扩展名决定。

误区训练完把 rank-*.pt 文件拷回本地,torch.load 一把梭,认为权重本来就是完整的一块。

正解Megatron 每个 rank 只保存自己负责的切片:列并行按列切、行并行按行切、流水线按层切。不按图纸归并,拿不到完整 state_dict;同时还要处理 QKV 融合布局。必须用官方或社区转换脚本先合并,再转为目标框架格式。

为何常见:分布式训练对多数人是个黑盒,以为模型始终是一整块,看不见背后的分拣过程。

面试题

Q1. torch.save 默认序列化底层依赖哪种协议?

单选题 · easy

  • A.JSON 文本序列化
  • B.pickle 序列化协议← 正确答案
  • C.YAML 配置序列化
  • D.SQL 数据库存储

答案:B。torch.save 底层是 Python 的 pickle 协议,它把对象图翻译成字节流。就像搬家纸箱能塞进任何东西,pickle 也能打包任意 Python 对象,因此它才存在执行代码的安全隐患。

Q2. 从网上下载的 .pt 文件直接 torch.load,最大的风险是什么?

单选题 · easy

  • A.磁盘空间耗尽
  • B.执行任意代码← 正确答案
  • C.模型精度下降
  • D.显卡温度升高

答案:B。pickle 反序列化时会执行字节流里的函数调用,相当于运行对方的程序。这就像打开陌生人寄来的纸箱,里面可能藏着整蛊蛇。磁盘、精度、温度都是次要问题,代码执行才是致命风险。

Q3. 把 demo.pt 直接改名为 demo.safetensors,会发生什么?

单选题 · easy

  • A.加载速度变快
  • B.文件内容仍旧← 正确答案
  • C.自动完成压缩
  • D.数据丢失一半

答案:B。修改后缀只改了名字,内部字节还是 pickle 流。safetensors 解析器按 JSON 清单取数据,对 pickle 流完全无效。格式是字节结构,不是扩展名。转换必须走“读出 → 重写”的流程。

Q4. SafeTensors 实现零拷贝加载主要依赖哪种系统机制?

单选题 · mid

  • A.虚拟内存映射← 正确答案
  • B.磁盘压缩缓存
  • C.网络协议加速
  • D.显存自动回收

答案:A。safetensors 借助操作系统 mmap 内存映射,把文件映射进虚拟内存,再按偏移直接构造张量视图,不复制整个文件。这就像透明箱看清单伸手取物,不用把全屋东西倒出来。压缩、网络、显存回收都跟零拷贝无关。

Q5. 权重 key 映射主要解决跨框架迁移时的什么问题?

单选题 · mid

  • A.参数命名差异← 正确答案
  • B.算法结构缺失
  • C.显卡算力不足
  • D.数据标注混乱

答案:A。不同框架对同一个注意力层命名不同,一个叫 fc1.weight,另一个叫 attention.query.weight。key 映射就是新旧标签对照表,把旧家钥匙翻译成新家锁型号,解决的是命名差异问题。

Q6. 哪些场景推荐优先使用 SafeTensors 格式?

多选题 · mid

  • A.加载陌生作者模型← 正确答案
  • B.频繁冷启动加载← 正确答案
  • C.保存任意对象
  • D.分布式并发读取← 正确答案

答案:ABD。陌生模型用 safetensors 可避免 pickle 投毒;频繁冷启动受益于 mmap 快速加载;分布式并发读多个分片也方便。选 C 是错的——safetensors 只序列化张量,装不了任意 Python 对象,那是 pickle 的专属能力。

Q7. torch.load(……, weights_only=True) 无法恢复下列哪种对象?

单选题 · mid

  • A.普通浮点数字
  • B.张量对象副本
  • C.自定义模型类← 正确答案
  • D.嵌套字典结构

答案:C。weights_only=True 只允许还原张量、数字、字符串、字典等“标准零件”,拒绝实例化自定义类,从而阻断恶意代码。自定义模型类是旧家师傅才认识的对象,安全模式下开不了箱,也正是这个限制保护了你。

Q8. 将 Megatron 分片 checkpoint 归并为完整权重,需要哪些操作?

多选题 · hard

  • A.按切分方向拼接← 正确答案
  • B.汇总各分片权重← 正确答案
  • C.修改文件拓展名
  • D.校验合并后形状← 正确答案

答案:ABD。列并行沿列拼、行并行沿行拼、流水线按层顺序连接,这就是“按图纸拼装”;随后要校验合并后的形状与期望一致。只改后缀不改变字节内容,分片依然是分片,所以 C 错误。

Q9. 关于 torch.save 与 safetensors 的区别,下列说法正确的是?

多选题 · hard

  • A.pickle 会执行任意代码← 正确答案
  • B.safetensors 可内存映射← 正确答案
  • C.safetensors 会压缩权重
  • D.torch.save 依赖 Python 环境← 正确答案

答案:ABD。pickle 反序列化执行任意代码,safetensors 用 mmap 零拷贝读取,torch.save 需要相同类路径和版本才能解开。C 错误——safetensors 不做压缩,存的也是原始字节,快是来自零拷贝而非压缩。

Q10. 跨框架加载 checkpoint 时,即使 key 完全匹配,仍需检查什么?

多选题 · hard

  • A.张量形状分布← 正确答案
  • B.数据类型一致← 正确答案
  • C.QKV 融合布局← 正确答案
  • D.训练轮次数值

答案:ABC。key 对齐只是第一步,形状必须逐一对上,dtype 不一致要转精度,融合布局(QKV 合成一个矩阵还是拆成三个)也要还原。训练轮次只是日志信息,不影响加载正确性,所以 D 不选。