权重加载报错时,最容易被误判成“框架不支持算子”的,往往是文件本身的问题:分片少一个、命名差一位、传输断一次,报错形态都很像。可行的顺序是先排文件、再排算子——索引核对、哈希校验、最小配置只加载不推理,把第一处报错读到具体某一类原因上,再决定是重取分片还是改框架配置。
建议把判断顺序固定成三步:对着索引文件核对分片清单,逐文件哈希校验,用最小配置只加载不推理并读第一处报错。文件类报错通常出现在分片加载阶段,带明确文件名或键名;算子类报错出现在权重映射完成之后,常含 unsupported op、算子名或后端名。这套顺序适用于多分片 safetensors 一类权重,换成其他打包格式需要替换对应命令。
对着索引文件核对分片数量与命名
多分片权重一般会带一个索引文件(常见名为 model.safetensors.index.json),里面的 weight_map 把张量名映射到具体分片文件名。先以这个索引为唯一事实来源,导出登记的分片集合,再和实际目录里的文件做对比,缺片、多片、名字对不上会一次性暴露。
# 1. 从索引里导出登记过的分片名,排序去重
jq -r '.weight_map | to_entries[] | .value' model.safetensors.index.json \
| sort -u > index_shards.txt
# 2. 实际目录中的分片文件
ls -1 *.safetensors | sort > disk_shards.txt
# 3. 只看差异:左边是索引有而磁盘没有,右边是磁盘多出来的
diff index_shards.txt disk_shards.txt
逐一对应时可以用一张简表把结论落下来,避免“看起来都在”这种模糊判断:
| 索引登记的分片 | 实际目录文件 | 判断 |
|---|---|---|
| model-00001-of-00008.safetensors | model-00001-of-00008.safetensors | 一致 |
| model-00005-of-00008.safetensors | model-0005-of-00008.safetensors | 位数不一致,加载器按字符串找文件会报缺失 |
| model-00007-of-00008.safetensors | (无) | 确实缺片,需要重新下载该分片 |
| — | model-00008-of-00008(1).safetensors | 下载工具重命名,属于多余文件,不参与加载 |
命名不一致的常见表现有几类:编号位数被补零或截断;大小写与索引不一致;下载或解压多套了一层子目录,加载器在相对路径下找不到;同名文件被浏览器或下载器追加序号;以及索引文件与分片来自不同版本、不同次下载后被混放在同一目录——这种情况下文件数量可能对得上,但会出现键名缺失或 shape 不匹配。核对前建议先确认整目录来自同一次下载。
用哈希校验确认下载文件完整
数量对上不代表内容完整。大分片在断点续传、跨机器拷贝、解压中断时都可能损坏,而损坏文件常常能被读进来一部分,直到某个张量解析失败才报错。逐文件计算哈希并落盘,比“重新下一遍试试”更可控。
# 逐文件计算并记录,便于之后复算与对比
find . -maxdepth 1 -name '*.safetensors' -print0 \
| sort -z | xargs -0 sha256sum > shards.sha256
# 若有上游给出的校验清单,按同样格式对齐后整体校验
sha256sum -c upstream.sha256 `--ignore-missing`
需要说明边界:本地生成的哈希只能证明“这次文件与上次一致”,不能证明与上游发布一致;要判断是否完整,得拿到上游的校验清单或分片大小清单做对比。上游没有提供清单时,退一步核对每个文件字节数是否与索引或下载页登记的大小一致,并把结果记录在同一份日志里。
校验不通过时的重取策略:只重取失败的那几个分片,不要整包重下;重取后在同一个 shards.sha256 上复算并追加一行记录,写明文件名、字节数、哈希、重取次数。多次重取同一分片仍不一致,优先怀疑下载链路或磁盘,而不是模型本身。
用最小配置只加载不推理,读第一处报错位置
文件层排除干净后,再让框架把权重加载一遍,但不做生成。这样报错只会来自加载与建图两个阶段,位置本身就是线索:卡在分片加载进度里,多半是文件、键名或映射问题;分片全部加载完、进入模型构建或首次前向才报错,才更可能是算子与后端问题。
# 只加载、不推理:去掉 generate 调用,让第一处报错落在加载阶段
from transformers import AutoConfig, AutoModelForCausalLM
path = './model-dir'
cfg = AutoConfig.from_pretrained(path, trust_remote_code=True)
print(cfg.num_hidden_layers, cfg.hidden_size, cfg.num_attention_heads)
model = AutoModelForCausalLM.from_pretrained(
path,
config=cfg,
torch_dtype='bfloat16',
device_map='cpu', # 先上 CPU,排除显存和多卡并行的干扰
low_cpu_mem_usage=True,
attn_implementation='eager',
)
print('loaded ok')
# 上面字段名以你实际使用的推理框架为准,重点是关掉生成、关掉并行
读到报错后按三类信息分流:
- 键名缺失或多余:出现 Missing key(s)、Unexpected key(s)、KeyError 一类信息,指向索引与分片不配套、或 weight_map 里登记的张量在文件里不存在。回到第一小节重新核对清单。
- shape 不匹配:出现 size mismatch 一类信息,且能看出是某个具体层的形状对不上。这时要区分是分片内容与索引错位(文件问题),还是 config 里 num_hidden_layers、hidden_size、head_dim 等字段与权重不是同一代(版本混用)。先用 config 打印的字段和权重里的形状逐层比。
- unsupported op:出现 unsupported operator、not implemented for、没有可用 kernel、后端名加算子名这类信息,说明权重已经读进去了,是计算实现缺失。这类问题改文件没有用,走下一小节。
把不支持的算子换掉或回退到可跑通路
确认是算子类报错后,处理思路是从“最保守的通用实现”往回走,而不是直接改权重或跳过层。每一步只改一个变量,改完立刻用同一段只加载不推理的脚本复跑,观察第一处报错是否前移或消失。
- 换计算精度:部分融合 kernel 只在特定精度下有实现。把 torch_dtype 在 bfloat16、float16、float32 之间逐个试,验证方式是同一脚本只改该参数重跑,看报错是否变成显存不足(这说明进到了下一步)还是保持不变。
- 换注意力实现:把 attn_implementation 显式设为 eager,这是语义上最保守的通路;确认能加载后,再逐个试 sdpa 或框架里的 flash 类实现。用循环把候选实现跑一遍并记录每个的成功与失败原因,比手工反复改配置更省事。
- 关闭融合算子:框架通常有 fuse_ 前缀、use_fused_ 前缀或独立的 kernel 开关。逐个关掉后重跑,定位到具体是哪一组融合实现触发了不支持。注意这类开关属于回退手段,代价常是速度,不是优化项。
# 逐个注意力实现试跑,记录哪一个能加载
for impl in ['eager', 'sdpa']:
try:
AutoModelForCausalLM.from_pretrained(
path, torch_dtype='bfloat16', attn_implementation=impl, device_map='cpu')
print(impl, 'ok')
except Exception as e:
print(impl, 'fail:', type(e).__name__, str(e)[:200])
如果模型带自定义算子(例如特定位置的旋转编码、专家路由等),而框架里确实没有对应实现,可行的路只有换到支持该结构的推理后端、或回退到上游提供的推理代码;不建议在加载脚本里临时 patch 掉权重中的相关张量来“让它跑起来”,那会把问题推到输出质量上,更难排查。
记录一套可复现的最小加载参数组合
跑通一次不算完成,要把那次能跑通的最小组合写下来,下次换机器、换镜像、换框架版本时才能快速复现或对比。
| 参数 | 保守取值 | 说明 |
|---|---|---|
| 精度 torch_dtype | 与权重发布精度一致,先不混用 | 混用精度容易引入新的 shape/dtype 报错 |
| 注意力实现 | eager | 作为基线,其它实现在此之上逐个替换 |
| 并行方式 | 先单卡或 CPU device_map | 排除多卡切分带来的额外报错来源 |
| 融合算子开关 | 全部关闭 | 仅在确认可跑后再逐项打开 |
| 加载内存模式 | low_cpu_mem_usage 打开 | 降低加载峰值,便于在小机器上先验证文件链路 |
显存占用一并记录,用采样命令在同一时刻取值,写清是加载完成后还是加载峰值:
nvidia-smi `--query-gpu`=index,memory.used,memory.total `--format`=csv
# 记录项建议包含:分片总字节数、加载后常驻显存、加载峰值、剩余余量
最后把结论整理成一张短记录:分片数量与哈希状态、第一处报错原文、被判定为算子问题的那一项、最终生效的参数组合与显存占用。下次遇到同类加载失败,先比对这份记录,能省掉重复排查;如果报错原文与记录里不同,说明是新的失败原因,再回到索引核对那一步重来。