MiniCPM-Robot 这类带权重分片、还需要 trust_remote_code=True 加载自定义建模代码的模型,报错位置基本能把问题分成两类:ImportError、AttributeError 多半指向依赖版本不对,FileNotFoundError、分片相关的 KeyError 多半指向权重路径或文件不完整。所以在动手改 pip 版本或重新下载之前,先把导入、加载、前向拆成三条独立命令分别执行,看报错停在哪一条,再决定查哪一侧。
建议的判断顺序是:先跑 import,再跑 from_pretrained,最后跑一次最小前向。导入阶段报 ImportError/AttributeError,通常是包缺失或版本对不上;加载阶段报 FileNotFoundError、safetensors KeyError、找不到分片,通常是路径写错或目录不完整;前向阶段报形状或设备不一致的 RuntimeError,应先检查输入构造与 device,而不是回头动权重。下面的命令只做定位不做修复,结论需要结合你本地的目录列表与已装包列表确认。
先定下报错发生在哪一步
把三件事拆开跑,报错就不会被上一步的副作用掩盖。三条命令互不依赖,可以逐条复制,把 /abs/path/to/MiniCPM-Robot 换成你自己的绝对路径。
# 第一步:只验证依赖能不能导入,不碰权重
python -c "import torch, transformers; print(torch.__version__, transformers.__version__)" 2>&1 | tail -20
# 第二步:只验证权重能不能加载,不构造任何输入
python -c "
from transformers import AutoModel
m = AutoModel.from_pretrained('/abs/path/to/MiniCPM-Robot', trust_remote_code=True)
print(type(m).__name__)
" 2>&1 | tail -40
# 第三步:加载后跑一次最小前向
python -c "
import torch
from transformers import AutoModel, AutoTokenizer
p = '/abs/path/to/MiniCPM-Robot'
tok = AutoTokenizer.from_pretrained(p, trust_remote_code=True)
m = AutoModel.from_pretrained(p, trust_remote_code=True).eval()
print('device/dtype:', next(m.parameters()).device, next(m.parameters()).dtype)
" 2>&1 | tail -40
读输出时看三件事:哪一条命令非零退出、报错栈最底部那一行指向的是哪个文件、以及最后打印出来的 device/dtype 是否符合预期。第一条就失败说明环境层没对齐,后两条不用急着跑;第一条通过、第二条失败,问题在路径、分片或建模代码本身;前两条都通过、第三条才失败,基本可以排除权重,转去查输入和设备。
| 阶段 | 典型报错关键字 | 通常指向 | 先查什么 |
|---|---|---|---|
| 依赖导入 | ModuleNotFoundError、ImportError: cannot import name ... from 'transformers'、torch 上不存在的 attribute | 缺包或版本区间不对 | 已装包列表与建模代码里 import 的那一行 |
| 权重加载 | FileNotFoundError、does not appear to have a file named config.json、safetensors 分片 KeyError | 路径写错、目录不完整、分片缺一 | 目录结构、分片数量与 index.json 是否对齐 |
| 前向执行 | Expected all tensors to be on the same device、size mismatch、expected scalar type Half but found Float | 输入形状、dtype 或 device 不一致 | 输入张量的 shape/dtype/device 与模型参数对照 |
导入阶段:看 ImportError 指向哪个包
导入阶段的报错最容易误判,因为报错栈常常停在 cache 目录下的 modeling_xxx.py 某一行,看起来像模型代码有问题,实际是那一行 import 的包没装或版本不符。判别方法是先分清报错类型:ModuleNotFoundError: No module named 'X' 是直接缺包;ImportError: cannot import name 'Y' from 'transformers' 是 transformers 版本与建模代码要求的接口区间对不上,需要回到那个 modeling 文件看它 import 了哪些符号;AttributeError: module 'torch' has no attribute 'Z' 通常是 torch 版本偏旧。
# 看关键包是否都在,版本是否成组
pip list 2>/dev/null | grep -Ei "torch|transformers|accelerate|safetensors|sentencepiece|tokenizers|einops|timm|torchvision|pillow|protobuf"
# 单个包看装在哪、依赖谁
pip show transformers
# 确认解释器是同一个,避免装到系统 python 却用 venv 跑
python -c "import sys, transformers; print(sys.executable); print(transformers.__version__)"
常见缺失包集中在几类:tokenizer 相关的 sentencepiece、tokenizers、protobuf;加载与显存切分相关的 accelerate、safetensors;视觉侧常见的 torchvision、Pillow、timm、einops。缺哪个装哪个之前,先确认 pip list 里确实没有,而不是装到了另一个解释器里——sys.executable 与 pip -V 指向同一路径才算一致。
加载阶段:核对路径与分片文件是否齐全
加载到一半才报错,很典型的分片缺失特征:前面的分片读完了,读到缺失的那一片才抛错。所以先别改代码,先看目录。
MODEL_DIR=/abs/path/to/MiniCPM-Robot
ls -l "$MODEL_DIR"
ls "$MODEL_DIR"/*.safetensors 2>/dev/null | wc -l
du -h "$MODEL_DIR"/*.safetensors 2>/dev/null | sort -h
# 有 index.json 时,比对索引里登记的分片与磁盘上实际的分片
python -c "
import json, os
d = '$MODEL_DIR'
idx = os.path.join(d, 'model.safetensors.index.json')
if os.path.exists(idx):
j = json.load(open(idx))
need = sorted(set(j['weight_map'].values()))
have = sorted(f for f in os.listdir(d) if f.endswith('.safetensors'))
print('need:', need)
print('missing:', [f for f in need if f not in have])
else:
print('no index file')
"
判定口径很直接:索引里登记的分片应在磁盘上一个不少,文件数量对不上或某个分片体积明显小于同批其他文件,通常就是下载中断。路径是相对还是绝对,用下面的方式确认——os.path.isdir 为 False 就是路径本身写错;如果报错里出现的是 hub cache 路径而不是你写的目录,说明传入的字符串被当成了仓库 id 去解析,而不是本地目录,这种情况把相对路径换成 os.path.abspath 出来的绝对路径再试。
import os
model_dir = os.path.abspath('./MiniCPM-Robot')
print('cwd:', os.getcwd())
print('dir:', model_dir, os.path.isdir(model_dir))
if os.path.isdir(model_dir):
print(sorted(os.listdir(model_dir))[:20])
前向阶段:核对输入形状与设备是否一致
能加载成功却在前向时报错,问题基本不在权重。先把输入张量的三个属性和模型参数的 device/dtype 一起打出来,再断言,报错会从“形状不对”变成一句能直接读懂的提示。
import torch
def check(name, t):
print(f'{name}: shape={tuple(t.shape)} dtype={t.dtype} device={t.device}')
return t
pixel_values = check('pixel_values', pixel_values) # 通常为 [B, C, H, W]
input_ids = check('input_ids', input_ids) # 通常为 [B, L]
attention_mask = check('attention_mask', attention_mask)
param = next(model.parameters())
print('model:', param.device, param.dtype)
assert pixel_values.device == param.device, '输入与模型不在同一设备'
assert input_ids.device == param.device, 'input_ids 与模型不在同一设备'
assert pixel_values.dtype == param.dtype, '输入与模型 dtype 不一致'
assert input_ids.shape[0] == pixel_values.shape[0], 'batch 维不一致'
典型不匹配表现可以对着报错读:Expected all tensors to be on the same device, but found cuda:0 and cpu 说明图像张量还在 CPU 而模型已在显卡上;mat1 and mat2 shapes cannot be multiplied 一般是序列长度或投影维度和 config 对不上;expected scalar type Half but found Float 常见于模型以半精度加载、预处理产出的张量仍是 float32;batch 维不一致时往往表现为广播失败而不是直观的形状错误。使用 device_map 把模型分散到多卡时,输入张量要和实际接收输入的层同设备,稳妥做法是不手动 .cuda(),交给加载逻辑处理,或先确认模型是否只落在一张卡上。
固化成最小复现脚本
定位到阶段之后,把能稳定复现的最小片段固化成脚本,去掉业务代码和多余参数,只留版本、路径、加载、前向四段。
# repro.py —— 用环境变量传路径,避免把路径写死在代码里
import os, sys, torch
from transformers import AutoModel, AutoTokenizer
MODEL_DIR = os.environ.get('MODEL_DIR', '').strip()
assert MODEL_DIR and os.path.isdir(MODEL_DIR), f'MODEL_DIR 不存在: {MODEL_DIR}'
def log(msg):
print(f'[repro] {msg}', flush=True)
log(f'torch={torch.__version__} transformers={__import__("transformers").__version__}')
log(f'cwd={os.getcwd()} dir={MODEL_DIR} files={len(os.listdir(MODEL_DIR))}')
tok = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True)
log('tokenizer ok')
model = AutoModel.from_pretrained(MODEL_DIR, trust_remote_code=True).eval()
log(f'model ok type={type(model).__name__}')
enc = tok('test input', return_tensors='pt')
log(f'input_ids {tuple(enc["input_ids"].shape)} {enc["input_ids"].dtype}')
with torch.no_grad():
out = model(**enc)
log(f'forward ok type={type(out).__name__}')
需要保留的日志行就这几类:版本号、MODEL_DIR 与文件数量、tokenizer ok / model ok / forward ok 三个打点、以及输入的 shape 与 dtype。删减时从能复现的完整脚本往回退,一次只删一处——先把 batch 降为 1、把多模态输入只留图像或只留文本、把 device_map 去掉、把序列长度调小,每删一次重跑,报错不变说明删掉的部分无关;报错消失的那一步就是可疑点,把它单独拎出来做对照。分享给别人时附上版本号、目录文件列表和报错末 30 行,比贴整段业务代码更容易被定位。