先算清显存再拉权重——书生·浦语本地部署前的取舍

文章导读
手上有卡但不确定能不能跑起书生·浦语,这个问题不该靠“先把权重拉下来再试”来回答。权重动辄十几到几十 GB,下载和加载都要时间,而真正决定成败的是加载那一刻的可用显存,不是显卡型号,也不是标称容量。可行的顺序是:先量出实际可用显存,再按量化档位估权重占用,再把上下文长度与并发预留算进去,三项加起来小于可用值,才值得开始下载。
📋 目录
  1. A 先量出显卡的实际可用显存而不是标称值
  2. B 把量化档位与权重占用对应起来做估算
  3. C 计算上下文长度与并发数对显存的挤占
  4. D 按最小可用目标写一份部署配置骨架
  5. E 用一次完整请求验证部署是否真的可用
A A

手上有卡但不确定能不能跑起书生·浦语,这个问题不该靠“先把权重拉下来再试”来回答。权重动辄十几到几十 GB,下载和加载都要时间,而真正决定成败的是加载那一刻的可用显存,不是显卡型号,也不是标称容量。可行的顺序是:先量出实际可用显存,再按量化档位估权重占用,再把上下文长度与并发预留算进去,三项加起来小于可用值,才值得开始下载。

判断顺序建议固定为“可用显存 → 量化档位 → 上下文与并发”。先用 nvidia-smi 读真实空闲显存,按 参数量×每参数字节数 粗估权重占用,再为 KV cache 留余量;任何一项算下来逼近上限,就先降档位、缩上下文或减并发,而不是拉完权重再试。单条短请求跑通只说明能加载,不代表长上下文和高并发可用,这两件事要单独验证。

先量出显卡的实际可用显存而不是标称值

判断基准要落在真实可用数值上。先看每张卡的总量、已用和空闲:

nvidia-smi `--query-gpu`=index,name,memory.total,memory.used,memory.free `--format`=csv

读法很直接:memory.total 是标称值,只能用来对照型号;memory.free 才是可以用来做估算的基准。桌面环境、显示输出、其他已在跑的推理进程、以及上一个进程退出后没释放干净的 CUDA context,都会提前吃掉一部分显存,所以标称和空闲之间经常有明显落差。

再确认是谁占着:

nvidia-smi `--query-compute-apps`=pid,process_name,used_memory `--format`=csv

把自己不需要的进程停掉后再量一次。多卡机器要逐卡看,如果打算用 device_map 把模型铺到多张卡上,最紧的那张卡决定整体上限,而不是剩余最多的那张。另外,刚启动时读到的空闲值通常偏乐观,推理框架自己也会分配工作显存,建议在实测空闲值上再留出一定宽裕量(例如一两 GB 级别),不要按顶满来算。

把量化档位与权重占用对应起来做估算

权重占用的通用估算方法是:参数量 × 每参数字节数。每参数字节数大致按量化位宽除以 8 得到,FP16/BF16 约 2 字节,8bit 约 1 字节,4bit 约 0.5 字节,再叠加嵌入层、输出层和框架自身的少量开销。按这个算法,7B 参数量在 2 字节精度下光是权重就是 14GB 量级,换到 8bit 大致减半,4bit 再减一轮。具体模型的实际参数量、是否共享嵌入权重,需要打开本地权重目录里的配置文件确认,不要凭印象套。

常见档位在效果与占用上的取舍,可以先按这样理解:

  • 不量化(FP16/BF16):兼容性和效果基准最好,占用最高,显存够时优先选它,便于后续排查问题时排除量化因素。
  • 8bit:占用明显下降,多数任务上差异不明显,适合作为折中档位先跑通。
  • 4bit(GPTQ、AWQ 等预量化权重):占用进一步下降,长上下文、复杂推理和多语言任务上更容易看出差别,需要用你自己的任务样本确认可用性。
  • 更低比特或更激进的压缩:通常只在显存严重受限时考虑,选择它意味着先接受效果上的让步。

还要区分两条路径:使用已经量化好的权重文件,和加载原始权重后再在线量化。前者需要在配置里指向对应的量化权重目录,后者需要额外依赖和更多的加载时间,显存峰值也不一样。

先算清显存再拉权重——书生·浦语本地部署前的取舍

计算上下文长度与并发数对显存的挤占

KV cache 的量级可以用这个结构估算:2(K 和 V)× 层数 × KV 头数 × 每个头的维度 × 序列长度 × 并发数 × 每元素字节数。层数和头数从模型配置里读,不要猜;序列长度按“输入长度 + 最大输出长度”预留,因为不少推理实现会按上限预分配或按增长分配。

  • 输入长度:抬高 prefill 阶段的中间开销,也让 KV cache 一开始就占掉一截,同时影响首 token 的等待时间。
  • 输出长度:决定 KV cache 最终涨到哪里,max_new_tokens 设得越大,等于预留越多。
  • 并发数:近似成倍地乘在 KV cache 上,这是“单条能跑、并发就崩”最常见的原因。

顺序建议是逐步加,而不是一次打满:单条短输入跑通 → 单条接近真实场景的长输入 → 单条长输出 → 2 并发 → 4 并发,每一步用 nvidia-smi 记录峰值。批处理框架通常有自己的显存池,观察到的峰值可能比按公式算出来的更高,以日志和显存读数为准。

按最小可用目标写一份部署配置骨架

先把目标压到最小:单条请求、上下文长度取真实需要的下限、量化档位取显存装得下的那一档。下面是一个通用骨架,字段名按你实际使用的推理库文档替换:

from transformers import AutoModelForCausalLM, AutoTokenizer

model_path = "/path/to/your-internlm-weights"   # 替换为本地权重目录

tok = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    torch_dtype="auto",          # 跟随权重自带精度
    device_map="auto",           # 多卡时按可用显存切分
    max_memory={0: "18GiB"},     # 依实测空闲值再留余量,不要顶满
    low_cpu_mem_usage=True,
)
model.eval()

几个先取保守值的项和理由:

  • max_memory 不顶满:框架、KV cache 和推理过程中的临时张量都要在同一块显存里分配。
  • 上下文先设小:max_new_tokens 可先取一两百个 token 试通,确认链路没问题再往上加。
  • 量化档位先求能加载:显存允许时优先不量化或 8bit,便于区分输出变差是量化导致还是模型本身。
  • device_map 要和显存上限一起设:只写 auto 而不限制每卡上限,可能出现把层放到不合适的位置。
  • 如果换成带服务端的推理库,参数名会不一样,但“限制显存、限制上下文长度、限制并发”这三类开关都要能找到对应项再启动。

用一次完整请求验证部署是否真的可用

服务起来不等于能用,发一次完整请求确认输出和显存行为:

prompt = "用三句话说明显存估算的基本顺序。"
inputs = tok(prompt, return_tensors="pt").to(model.device)
out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
print(tok.decode(out[0], skip_special_tokens=True))

检查点分四类看:

  1. 输出是否完整:句子自然收尾,而不是被 max_new_tokens 截断的半句话;关闭采样重复几次,结果应基本稳定。
  2. 显存是否回落:请求结束后再跑一次 nvidia-smi,空闲值应大致回到请求前的水平;如果一直不回落,通常说明缓存或张量没释放,长时间运行会累积。
  3. 日志有无告警:重点看 CUDA out of memory、量化 kernel 回退到慢速实现、权重存在 missing 或 unexpected keys、上下文被静默截断这几类提示。缺 key 有时仍能出结果,但输出质量会退化。
  4. 长上下文与并发单独再压一轮:短请求的结果不能代表长上下文和并发场景可用。

把这一轮得到的三个数字记下来——实测空闲显存、权重占用、KV cache 预留——下次换卡、换档位或改上下文长度时,直接按同一套方法重算,就不必再靠“先拉权重试一把”来试错。