在本机运行 MOSS-VL-Realtime 做截图问答时,真正卡住流程的地方往往不在模型能力,而在启动之前没有对齐两件事:权重实际包含几个文件、以什么精度加载;这些权重加载到显卡后又会占用多少显存。建议按“读配置列权重 → 按精度算显存 → 核对框架与驱动 → 再谈推理”的顺序处理;顺序反过来,就会在同一条报错上反复试错。
适用场景是单机单卡或两卡的本地部署,目标是先判断“能否正常启动”,尚未开始调优吞吐。操作上,先把 config.json 与权重目录的 index.json 对齐,按参数量×每参数字节数估出权重显存,再为图像编码器和 KV cache 留出余量;启动后用 torch.cuda.max_memory_allocated 采样对照。估算只对权重部分比较可信,视觉输入分辨率越高偏差越大,最终以实测峰值为准。
读模型配置文件,列清楚参数量、精度和权重文件清单
加载时模型会读取 config.json 和权重目录里的索引文件;这两个文件决定了要加载多少参数、以什么精度加载。先定位字段,再列文件,最后判定缺失。
- architectures 与 model_type:确认仓库结构,以及该用哪个 AutoModel 类。
- hidden_size、num_hidden_layers、intermediate_size、vocab_size:用于粗算语言侧参数量。
- torch_dtype:默认加载精度,常见 bfloat16 或 float16;如果出现 quantization_config,说明仓库带了量化配置,要按量化后的字节数估算。
- vision_config 或图像塔子配置:视觉部分通常有单独的一组 hidden_size 与层数,显存要单独核算,不要和语言侧混在一起。
- max_position_embeddings 与视觉 token 上限:决定长上下文、高分辨率截图时 KV cache 的规模。
目录结构通常长这样,分片数量按实际仓库为准:
moss-vl-realtime/
├── config.json
├── preprocessor_config.json # 图像预处理参数
├── tokenizer_config.json
├── model.safetensors.index.json # 分片索引,单文件仓库可能没有
├── model-00001-of-0000X.safetensors
└── ...
判定文件缺失不要靠肉眼数。读 index.json 的 weight_map,去重得到文件列表后逐个判断是否存在,再核对文件大小:只有几 KB 的 safetensors 多半是 LFS 指针没有拉全,这种文件能通过路径检查,但会在加载时才报错。
import json, os
root = './moss-vl-realtime'
cfg = json.load(open(os.path.join(root, 'config.json')))
print('arch:', cfg.get('architectures'), 'dtype:', cfg.get('torch_dtype'))
idx = os.path.join(root, 'model.safetensors.index.json')
if os.path.exists(idx):
files = sorted(set(json.load(open(idx))['weight_map'].values()))
for f in files:
p = os.path.join(root, f)
print(f, os.path.exists(p), os.path.getsize(p) if os.path.exists(p) else 0)
else:
print('无 index 文件,改为逐个核对目录下 *.safetensors')
这一步的输出先存档。文件缺失或尺寸异常时,先补齐再调整显存参数,否则后面的报错会混在一起。
按精度估算权重显存占用,并与实际加载后的占用对比
估算式是:权重显存 ≈ 参数量 × 每参数字节数。bf16 或 fp16 取 2,fp32 取 4,int8 取 1,4bit 量化约 0.5;加载瞬时可能同时存在两份权重拷贝,建议再乘 1.1 左右的余量。这个式子只覆盖权重本身,不包含激活、KV cache 和图像编码器前向计算。
参数量可以先用配置字段粗估。语言侧每层大致是 4×hidden² + 2×hidden×intermediate 的量级,再加上词表嵌入;视觉部分按 vision_config 单独计算。这样得到的数字误差不小,但足以判断差的是一个数量级还是只差一点。
h = cfg['hidden_size']; L = cfg['num_hidden_layers']
i = cfg.get('intermediate_size', 4*h); v = cfg.get('vocab_size', 0)
approx = L * (4*h*h + 2*h*i) + 2*v*h
print('语言侧粗估参数量 B', round(approx/1e9, 2))
print('bf16 权重约 GiB', round(approx*2/1024**3, 2))
实测有两种方式:外部用 nvidia-smi 观察整卡占用(会包含其他进程),进程内用 PyTorch 统计本次分配的峰值,后者更适合和估算值对比。
nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv -l 1
import torch
torch.cuda.reset_peak_memory_stats()
model = None # 这里换成仓库示例里的加载语句
# model = AutoModel...from_pretrained(path, torch_dtype=torch.bfloat16)
torch.cuda.synchronize()
print('加载后峰值 GiB', round(torch.cuda.max_memory_allocated()/1024**3, 2))
把估算和实测填进同一张表,偏差本身就是信息:
| 项目 | 估算值 | 实测值 | 偏差原因 |
|---|---|---|---|
| 语言侧权重 | 粗估公式忽略 bias 与 norm 参数,通常偏小 | ||
| 视觉塔权重 | 未包含在语言侧公式内,需单独列 | ||
| 加载瞬时峰值 | 分片加载与拷贝可能短暂出现双份权重 | ||
| 单次截图推理峰值 | — | 激活与 KV cache,随截图分辨率与输出长度变化 |
如果实测明显高于估算,先排查是否误用了 fp32 加载,或是否加载了未量化的原始权重;如果实测低于估算很多,检查是否只加载了部分分片。
核对运行环境:框架版本、驱动与 CUDA 支持、Python 依赖
环境核对放在加载模型之前,能挡掉一批与显存无关的失败。把下面几条命令的输出和权重清单一起存到同一个目录,后续对比方便。
python -V
nvidia-smi
python -c 'import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())'
pip show transformers accelerate safetensors pillow
pip freeze > req-lock.txt
CUDA 要分层看:驱动支持的 CUDA 版本不能低于 PyTorch 编译所用版本,否则 import 能过、kernel 跑不起来。pip freeze 的产出是当前环境的快照;依赖清单也可以从仓库的 requirements 或 pyproject 里取,两边对照才能看出被谁覆盖了版本。
| 现象或报错关键字 | 通常指向 | 验证动作 |
|---|---|---|
| torch.cuda.is_available() 返回 False | 驱动与 CUDA 运行时不匹配,或装成了 CPU 版 torch | 对比 nvidia-smi 顶部的 CUDA Version 与 torch.version.cuda |
| no kernel image is available for execution on the device | PyTorch 构建的算力架构不含当前显卡 | 确认显卡计算能力,换对应构建版本 |
| undefined symbol 或 version mismatch | torch 与编译扩展、transformers 的 ABI 冲突 | 新建干净虚拟环境,固定版本重装 |
| ImportError: cannot import name ... | transformers 版本低于仓库要求 | 按仓库声明的下限升级,不要混装多个版本 |
按顺序定位首次加载失败的原因
四类问题按下面的顺序查,前面的确认过再往下走,避免把路径问题误判成显存问题。
- 权重路径。关键字:OSError、Unable to load weights、No such file or directory、no file named model.safetensors。验证动作:打印模型目录的绝对路径,ls 查看内容,重跑第一步的索引核对脚本。常见错误是从其他工作目录启动,相对路径失效。
- 精度是否被支持。关键字:Unsupported dtype、bitsandbytes 相关的 CUDA 报错、quantization 配置解析失败。验证动作:显式传 torch_dtype=torch.bfloat16 试一次;较老的卡没有原生 bf16 支持,可换 float16 再试。这一步只看能否加载,不评价效果。
- 显存不足。关键字:CUDA out of memory、Tried to allocate。验证动作:先只加载模型不做推理,看权重占用是否已接近上限;再换一张小尺寸截图。仍不够时,可考虑 device_map 分卡或多卡切分、降低精度或使用量化权重;这些是让流程跑通的保守手段,不是性能优化方案。
- 依赖冲突。关键字:ImportError、AttributeError、requires transformers 版本号。验证动作:新建 venv,按 req-lock.txt 或仓库声明的版本重装,避免在已有大环境里反复升级降级。
每次失败记录三样东西:完整报错首行、当时的命令行或加载参数、改了什么之后恢复的。截图问答这类多模态仓库,图像处理器相关的报错也常出现在第 1 步和第 4 步之间,关键字通常是 image processor 或 pixel_values,按同样方式处理。
跑通一次截图问答并记录显存峰值与耗时
跑通的标准不是“没报错”,而是回答内容确实来自截图。建议固定一张截图、固定问题、固定输出长度,形成可重复的基线。
- 准备一张截图放 ./samples/shot.png,后续基线都用它。
- 按仓库示例加载 processor 与 model,模型切 eval 并指定半精度。
- 组装提示词,图片交给 processor 处理。
- 在 generate 前后打点,采集峰值显存与耗时。
- 核对输出:问截图上的一个数字或按钮文字,答案对得上才算通了。
import time, torch
from PIL import Image
torch.cuda.reset_peak_memory_stats(); torch.cuda.synchronize()
t0 = time.perf_counter()
image = Image.open('./samples/shot.png').convert('RGB')
inputs = processor(images=image, text=prompt, return_tensors='pt') # 键名按仓库示例
inputs = {k: (v.to('cuda') if v.dtype.is_floating_point else v.to('cuda')) for k, v in inputs.items()}
with torch.inference_mode():
out = model.generate(**inputs, max_new_tokens=128)
torch.cuda.synchronize()
print('端到端耗时 s', round(time.perf_counter()-t0, 2))
print('推理峰值 GiB', round(torch.cuda.max_memory_allocated()/1024**3, 2))
print(processor.decode(out[0][inputs['input_ids'].shape[-1]:], skip_special_tokens=True))
精度转换不要图省事把整个 inputs 一次性 cast,input_ids 是整型,转成浮点通常会直接报错;按仓库示例逐键处理,拿不准就先不做精度转换跑通一次。同一份配置重复跑两三次取较稳定的读数,再填下表。
| 记录项 | 内容 |
|---|---|
| 模型版本与精度 | |
| 显卡与驱动版本 | |
| torch / transformers 版本 | |
| 截图尺寸与格式 | |
| max_new_tokens | |
| 加载后显存峰值 | |
| 单次问答显存峰值 | |
| 端到端耗时 | |
| 输出是否与截图内容一致 |
之后任何一处变动(换精度、换显卡、升级依赖、换更大分辨率的截图),都回看这张表判断是环境变了还是输入变了,不要靠记忆回忆上次是怎么跑通的。