权重能加载、一喂图片就爆显存,通常是三个变量缠在一起:机器上实际空着的显存比 nvidia-smi 报的总量小、权重精度档选得比剩余余量高、图像侧的分辨率把激活值又抬了一截。处理顺序建议反过来:先用命令把可用显存上限量出来,再按参数量算清 fp16 / 8bit / 4bit 本体各要多少,之后单独压图片分辨率看峰值,最后用最小脚本跑通一次完整图文推理,把通过的组合记成表。以下命令、换算式子和脚本骨架都是通用写法,Muse Spark Open 的 processor 接口、量化实现、图像尺寸参数需要按仓库里的配置和示例确认后再替换。
本地跑多模态推理的定量化档顺序应当是:先跑nvidia-smi扣掉其他进程占用得到真实可用显存,再用参数量×每参数字节数估出权重本体,然后固定权重档、在同一张图上分三档分辨率各测一次峰值显存,最后用最小脚本确认能完整返回文本。凡是峰值显存接近或超过可用余量的组合一律记为不通过,不要靠调小max_new_tokens硬撑。不同显卡驱动、CUDA 上下文和量化实现都会影响余量,结论只对当台机器当次环境成立。
用 nvidia-smi 划出实际可用显存上限
别直接拿显卡型号的标称显存当作可用上限。桌面环境、浏览器、残留的推理进程都会占掉一部分,CUDA 上下文本身也要吃掉固定开销。先抄下三个字段:总量、已用量、占用进程的 PID 与各自占用。
# 总量 / 已用 / 剩余,按显卡逐行列出
nvidia-smi `--query-gpu`=index,name,memory.total,memory.used,memory.free `--format`=csv
# 谁在占显存
nvidia-smi `--query-compute-apps`=pid,process_name,used_memory `--format`=csv
# 看完整视图,确认没有僵尸进程
nvidia-smi
判断余量的写法是:可用余量 ≈ 总量 − 已用 − 预留。预留给 CUDA 上下文、框架缓存和图像解码,具体数值需要结合环境确认,通常留出几个百 MB 到 1 GB 级别的空间更稳。如果 memory.free 和“总量减已用”对不上,说明有进程刚分配过缓存还没释放,先把无关进程停掉再测,否则后面所有的峰值数字都建立在一个假上限上。测试期间建议关掉占显存的桌面程序,并在每次测完清理缓存。
按权重精度算一遍模型本体占用
换算式子很短:权重显存 ≈ 参数量 × 每参数字节数。fp16 每参数 2 字节,8bit 每参数 1 字节,4bit 每参数约 0.5 字节。参数量不是常数,需要从模型配置读取,不同版本不一样,所以下面的数字只能自己算,不能照抄别人的结论。
import json, os
cfg = json.load(open("config.json")) # 字段名按实际配置替换
hidden = cfg.get("hidden_size") or cfg.get("d_model")
layers = cfg.get("num_hidden_layers") or cfg.get("num_layers")
inter = cfg.get("intermediate_size") or cfg.get("ffn_dim")
vocab = cfg.get("vocab_size")
# 只算主干量级,注意力 + MLP 近似项
per_layer = 4 * hidden * hidden + 2 * hidden * inter
total = layers * per_layer + vocab * hidden
print("主干参数量约", round(total / 1e9, 2), "B")
for name, bpp in [("fp16", 2), ("8bit", 1), ("4bit", 0.5)]:
print(name, round(total * bpp / 1e9, 2), "GB(十进制,未含视觉塔与激活)")
# 用真实权重文件大小反推,和上面结果对照
size = sum(os.path.getsize(f) for f in os.listdir(".") if f.endswith((".safetensors", ".bin")))
print("权重文件合计", round(size / 1e9, 2), "GB")
两点必须补上:视觉编码器的参数量通常单列在自己的配置里,要额外加进去;4bit 实际占用会略高于 0.5 字节/参数,因为量化常数、缩放因子和部分保留高精度的层仍然按更大字节存,具体取决于所用量化实现。算出来的权重占用要和第一步的可用余量比,差值留给 KV cache、图像 token 和框架缓存;差值太小就别急着升精度档。
把图片输入分辨率与显存峰值挂钩测试
图文推理爆显存的常见诱因是图像被切成几百上千个视觉 token,注意力激活跟着涨。做法是固定权重档和提示词,只改输入分辨率,同一张图跑三档,每档记录峰值显存。
import torch, json, os, traceback
from PIL import Image
LOG = "logs"; os.makedirs(LOG, exist_ok=True)
def one_round(model, proc, image_path, side, tag):
rec = {"side": side, "tag": tag}
try:
img = Image.open(image_path).convert("RGB")
img = img.resize((side, side)) # 也可换成 processor 的 size / max_pixels 参数
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
# 构造输入与 generate 调用放在这里,接口按模型实现替换
rec["ok"] = True
except RuntimeError as e:
rec["ok"] = False
rec["err"] = str(e)[:200]
rec["peak_alloc_gb"] = round(torch.cuda.max_memory_allocated() / 1024**3, 2)
rec["peak_reserved_gb"] = round(torch.cuda.max_memory_reserved() / 1024**3, 2)
with open(f"{LOG}/peak_{tag}.json", "w") as f:
json.dump(rec, f, ensure_ascii=False, indent=2)
print(rec)
return rec
三档怎么取:按模型图像处理器允许的边长范围,从偏小、中等偏大取到明显偏大各一档,比如 336 / 448 / 896 这类量级,实际以图像处理器的尺寸参数为准。每档之间必须重启进程或至少 empty_cache(),否则上一轮缓存会污染下一轮的 max_memory_reserved。当某一档开始接近第一步算出的可用余量,那一档就是这台机器的图像输入上限,不用再往上试。
在最小脚本里跑通一次图文输入
峰值测得再漂亮,也要有一次端到端返回文本的记录才算跑通。骨架如下,加载、预处理、生成三步,接口名按仓库示例替换。
import torch
from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM # 类名按实际实现替换
model_id = "./muse-spark-open" # 本地权重目录
proc = AutoProcessor.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id, torch_dtype=torch.float16, device_map={"": 0}
).eval()
img = Image.open("test.jpg").convert("RGB")
msgs = [{"role": "user", "content": [
{"type": "image"},
{"type": "text", "text": "用一句话描述这张图片。"}
]}]
prompt = proc.apply_chat_template(msgs, add_generation_prompt=True)
inputs = proc(text=prompt, images=[img], return_tensors="pt").to(model.device)
with torch.inference_mode():
out = model.generate(**inputs, max_new_tokens=64)
text = proc.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]
print(repr(text))
判据分三条:进程没有抛 CUDA out of memory;返回的 text 非空且不是重复字符或空白;max_memory_allocated 明显低于第一步的可用余量。任何一条不满足都算这一档没跑通,先降到更低精度或更小分辨率再试,不要只把 max_new_tokens 调小来掩盖问题——那只减少解码侧开销,解决不了加载和视觉编码阶段的占用。
把测试结果写进一张档位对照表
最后把通过和失败的组合都记下来,写成“这台机器能跑哪一档”的结论,而不是凭感觉。
| 显卡与可用余量 | 精度档 | 输入分辨率 | 峰值显存 | 是否通过 | 报错摘要 |
|---|---|---|---|---|---|
| 示例:24 GB,余量填入实测值 | fp16 / 8bit / 4bit | 边长三档 | alloc / reserved | 是 / 否 | OOM 或 token 超限 |
复测原则是每次只改一个变量:改精度档时固定分辨率,改分辨率时固定精度档,否则失败原因分不清是权重还是图像侧造成的。换驱动、换量化实现、换别的模型版本后,之前的对照表要重测,不能直接沿用。把 logs/ 下的 json 和这张表一起留在仓库里,下次再有人问“这台机器能不能跑”,直接看表就能回答。