先把显存和精度算清楚——Muse Spark Open 的部署取舍

文章导读
一张消费级显卡能不能跑 Muse Spark Open,先别去看跑分榜。把三个数摆到纸上:这张卡实际能分配出去多少显存、所选精度的权重占多少、图像编码和前向激活还要吃掉多少。三个数加不进去,讨论并发没有意义;加得进去,剩下的余量就是能开的批量上限。顺序反了,通常会在加载完权重、第一次喂图的时候直接 OOM。
📋 目录
  1. 一 记下显卡真实可用显存与驱动版本
  2. 二 把权重体积按各精度档逐个算一遍
  3. 三 为图像编码与前向激活单独留余量
  4. 四 从 batch=1 往上压到第一次 OOM
  5. 五 输出一张机器档位对照表
A A

一张消费级显卡能不能跑 Muse Spark Open,先别去看跑分榜。把三个数摆到纸上:这张卡实际能分配出去多少显存、所选精度的权重占多少、图像编码和前向激活还要吃掉多少。三个数加不进去,讨论并发没有意义;加得进去,剩下的余量就是能开的批量上限。顺序反了,通常会在加载完权重、第一次喂图的时候直接 OOM。

判断顺序建议固定:先从 nvidia-smi 读出真实可用显存,再按配置文件里的参数量乘每参数字节数得到权重体积,最后用同一张图在不同分辨率下实测激活增量。权重加激活留出安全余量后还装得下,才谈 batch=1 能不能跑通;batch 逐级上压,第一次 OOM 的前一档就是保守并发上限。显存不足时优先降精度或降分辨率,不要指望 swap 或内存换出来救,那只会让延迟变得不可用。

记下显卡真实可用显存与驱动版本

标称显存和可用显存不是一回事。桌面环境、浏览器硬件加速、残留的 Python 进程都会占掉一块。先在干净状态下抄下这几个字段,后面所有估算都以这份记录为起点。

nvidia-smi `--query-gpu`=name,driver_version,memory.total,memory.used,memory.free `--format`=csv
nvidia-smi `--query-compute-apps`=pid,process_name,used_memory `--format`=csv

需要抄下的是 name(确认具体型号,同型号不同显存版本很常见)、driver_version、memory.total 与 memory.free。第二行列出当前占卡的进程,看到不认识的 PID 就用 ps -p <pid> -o cmd= 确认来源;桌面环境的 Xorg 占用通常在系统重启后才会释放,测试前可以先切到无图形界面或者直接重启一次。

驱动与运行时的一致性检查同样要做:nvidia-smi 右上角显示的 CUDA Version 是驱动能支持的最高运行时版本,而 python -c "import torch; print(torch.version.cuda)" 是框架编译时的版本,后者不能高于前者。两者对不上时,通常表现为加载权重阶段报 no kernel image,而不是 OOM,容易误判成显存问题。

把权重体积按各精度档逐个算一遍

权重体积是唯一能提前算准的部分。公式很直白:参数量乘每参数字节数,再除以 1024 的三次方换成 GiB。

权重 GiB ≈ 参数量 × 每参数字节数 / 1024³

fp16 / bf16 : 2.0 字节/参数
int8        : 1.0 字节/参数,另需为量化 scale 留一点系数(建议按 1.05 左近取)
int4        : 0.5 字节/参数,组 scale 与打包开销建议按 1.1 左近取

参数量必须从模型文件目录里读,不要凭模型名猜。最省事的做法是直接读分片索引里的总字节数,这是文件实际大小,比任何推算都准;config.json 里的层数、隐层宽度、词表大小则用来交叉核对。

python - <<'PY'
import json, pathlib
root = pathlib.Path('./muse-spark-open')
idx = json.loads((root / 'model.safetensors.index.json').read_text())
total = idx['metadata']['total_size']
print('权重文件合计 GiB:', round(total / 1024**3, 2))
cfg = json.loads((root / 'config.json').read_text())
for k in ('num_hidden_layers', 'hidden_size', 'intermediate_size', 'vocab_size'):
    print(k, cfg.get(k))   # 字段名以实际 config.json 为准,缺失就跳过
PY

拿到总字节数后,fp16 就按这个数估;int8 大约是一半,int4 大约是四分之一,再按上表系数补上量化开销。值得注意的是,int4 权重加载后往往还需要反量化到计算精度,这部分临时显存不能漏算,通常表现为加载完成瞬间峰值远高于静态权重体积。

为图像编码与前向激活单独留余量

只算权重是新手最容易踩的坑。Muse Spark Open 这类带视觉输入的模型,图像编码器吃显存的方式和文本不同:分辨率提高,切出的 patch token 数通常按平方增长,激活跟着涨。机器不同、框架版本不同,增量也不同,所以这里只能实测,不能套别人的数字。

先把显存和精度算清楚——Muse Spark Open 的部署取舍

做法是固定同一张图片内容,只改重采样尺寸,避免图像内容差异干扰结果。每档都用 reset_peak_memory_stats 重新统计,记录的是峰值而不是结束后的占用。

import torch
from PIL import Image

def peak_gib():
    return torch.cuda.max_memory_allocated() / 1024**3

torch.cuda.reset_peak_memory_stats()
baseline = peak_gib()          # 只加载完权重时的峰值
img0 = Image.open('sample.png').convert('RGB')

for w, h in [(512, 512), (768, 768), (1024, 1024)]:
    img = img0.resize((w, h))
    torch.cuda.reset_peak_memory_stats()
    with torch.inference_mode():
        out = model.generate(image=img, prompt='描述这张图')
    print(w, h, '峰值 GiB', round(peak_gib(), 2),
          '增量 GiB(实测)', round(peak_gib() - baseline, 2))
    del out
    torch.cuda.empty_cache()

表格里就把“增量”这一列标成实测值,并注明显卡型号、精度、驱动版本,别人复现时才知道边界在哪。如果 1024 这一档直接 OOM,说明留的余量不足,先把分辨率上限压下来,再考虑换精度。

从 batch=1 往上压到第一次 OOM

batch=1 跑通只说明装得下,不说明能并发。逐级加批量的目的不是找极限性能,而是找出第一次失败的档位,取它的前一档作为保守并发值。

import torch, gc
img = Image.open('sample.png').convert('RGB')

def try_batch(n):
    torch.cuda.reset_peak_memory_stats()
    try:
        with torch.inference_mode():
            for _ in range(n):
                model.generate(image=img, prompt='描述这张图')
        return True, torch.cuda.max_memory_allocated() / 1024**3
    except torch.cuda.OutOfMemoryError:
        return False, torch.cuda.max_memory_allocated() / 1024**3

for n in (1, 2, 3, 4, 6, 8):
    ok, peak = try_batch(n)
    print(f'batch={n} ok={ok} peak={peak:.2f}GiB')
    gc.collect(); torch.cuda.empty_cache()
    if not ok:
        break

要分清两件事:上面这种串行重复调用,测到的是多请求排队时显存堆积的下限,不等于真实并发。真实并发需要把 n 份输入同时保留在显存里,或者用支持批处理的接口一次喂多张图,两者峰值通常更高。记录本上至少写三列:档位、该档稳定后的显存(连续跑几轮取稳定值)、失败时的报错原文。失败点前后差一大截,说明进入了显存碎片敏感区,取更保守的档位。

输出一张机器档位对照表

测完几轮,把结果整理成别人能直接套的表。行是精度档,列是分辨率与并发,格子里填显存峰值和是否通过,空白或未测就写未测,不要靠估。

精度 分辨率 并发 显存峰值(实测, GiB) 是否通过 备注
fp16512×5121待填待填基准行
int8768×7682待填待填含量化开销
int41024×10244待填待填首次 OOM 前一档

表的抬头要带上显卡型号、显存总量、驱动版本和精度实现方式(原生量化还是加载后反量化),否则别人照着套会得到完全不同的结论。判断规则可以写进表的注释里:可用显存减去权重体积和图像激活增量后,剩下不到峰值的一半,就不要再往上加并发;精度换来的余量优先留给分辨率,而不是先堆并发,因为分辨率一超就是直接失败,并发超了通常还能靠排队扛过去。