MiniCPM-Robot 加载到一半就报错——是权重路径还是依赖版本没对上

文章导读
MiniCPM-Robot 这类带权重分片、还需要 trust_remote_code=True 加载自定义建模代码的模型,报错位置基本能把问题分成两类:ImportError、AttributeError 多半指向依赖版本不对,FileNotFoundError、分片相关的 KeyError 多半指向权重路径或文件不完整。所以在动手改 pip 版本或重新下载之前,先把导入、加载、前向拆成三条独立
📋 目录
  1. A 先定下报错发生在哪一步
  2. B 导入阶段:看 ImportError 指向哪个包
  3. C 加载阶段:核对路径与分片文件是否齐全
  4. D 前向阶段:核对输入形状与设备是否一致
  5. E 固化成最小复现脚本
A A

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 指向同一路径才算一致。

加载阶段:核对路径与分片文件是否齐全

加载到一半才报错,很典型的分片缺失特征:前面的分片读完了,读到缺失的那一片才抛错。所以先别改代码,先看目录。

MiniCPM-Robot 加载到一半就报错——是权重路径还是依赖版本没对上
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 行,比贴整段业务代码更容易被定位。