Beam 加载后显存就吃满 / 是量化等级选错了吗?

文章导读
显存吃满很少是单一原因:权重文件本身、KV cache 的预分配、推理框架启动时的运行时开销,三者都会各占一块。判断量化档位是否选错,正确顺序不是先去翻量化参数,而是先看加载日志——显存是在权重载入阶段跳上去的,还是在缓存预分配或框架初始化阶段跳上去的。前者才需要怀疑量化,后者通常和量化无关。
📋 目录
  1. 壹 在加载日志里定位显存被占满发生在哪一步
  2. 贰 读出模型目录里的量化标识与权重实际体积
  3. 叁 把上下文长度与批大小都降到最小后重启
  4. 肆 对比“只加载不推理”和“推理一次”的显存差
  5. 伍 确认是量化不匹配时的权重替换前检查
A A

显存吃满很少是单一原因:权重文件本身、KV cache 的预分配、推理框架启动时的运行时开销,三者都会各占一块。判断量化档位是否选错,正确顺序不是先去翻量化参数,而是先看加载日志——显存是在权重载入阶段跳上去的,还是在缓存预分配或框架初始化阶段跳上去的。前者才需要怀疑量化,后者通常和量化无关。

先分清显存被占满发生在哪一步:若在权重分片载入完成后就接近上限,且目录体积与标注量化档位明显不符,才考虑换权重;若显存放在 KV cache 预分配或运行时初始化阶段,优先降上下文长度和批大小。只加载不推理与推理一次两次采样做差,可把权重占用和缓存/激活占用拆开,避免盲目换量化档位。

在加载日志里定位显存被占满发生在哪一步

在服务启动的同时另开一个终端采样显存,把时间点和日志对齐,比事后看一个总量数字有用得多。

nvidia-smi `--query-gpu`=timestamp,memory.used,memory.total `--format`=csv -l 1 | tee mem.log

grep -nEi "load(ing)? (model )?(weight|checkpoint|shard)|kv.?cache|prealloc|profil|cuda graph|warmup|init engine|ready" beam.log

三类阶段关键字通常长这样:权重载入阶段常见 loading checkpoint shards、loading weights、load_model;缓存与显存预分配阶段常见 kv cache、cache blocks、preallocat、memory profiling、gpu_memory_utilization;框架初始化阶段常见 init engine、capturing cuda graph、compile、warmup。

读法是按时间顺序从上往下走,每读到一条阶段关键字,就看同一时刻 mem.log 里显存到了多少。若显存在分片载入的最后一条日志附近就跳满,问题偏向权重体积;若分片载入完成后显存还比较低,直到出现 kv cache 或 profiling 才跳上去,说明主要是预分配;若一直到 cuda graph capture、warmup 才跳,说明框架运行时本身占了一部分。这里不预设任何数值门槛,各版本日志措辞不同,以你机器上的实际顺序为准。

读出模型目录里的量化标识与权重实际体积

确认当前加载的到底是哪一档量化,先看目录本身,而不是看下载页面上的标注。

ls -lh /path/to/model

du -sh /path/to/model
find /path/to/model -maxdepth 1 -type f \( -name "*.safetensors" -o -name "*.gguf" -o -name "*.bin" -o -name "*.pt" \) \
  -printf "%s\t%p\n" | sort -n

cat /path/to/model/config.json | jq '{quantization_config, torch_dtype}'
grep -o "Q[0-9]_[A-Z0-9_]*\|IQ[0-9]_[A-Z0-9_]*" /path/to/model/*.gguf

GGUF 的量化档位一般写在文件名里(Q4_K_M、Q5_K_M、IQ4_XS 等),safetensors 类权重则看 config.json 里的 quantization_config(quant_method、bits 等字段)。体积和档位只是大致对应关系:可按参数量粗估,fp16 约每参数 2 字节,int8 约 1 字节,4bit 约 0.5 字节,再叠加少量额外开销,所以整目录体积会略高于这个估算值。

实用的判断是量级对比:如果标称 4bit,而目录体积却接近 fp16 量级的一半以上,或者目录里同时躺着多份不同后缀的权重,就要先怀疑加载的根本不是你选的那一份。具体数字以本机实际文件为准,不要拿别人的体积当标准。

把上下文长度与批大小都降到最小后重启

这一步是为了排除上下文预分配。常见参数位置:vLLM 类服务在启动参数或 yaml 里的 max_model_len、max_num_seqs、gpu_memory_utilization;TGI 类的 max_input_length、max_total_tokens、max_batch_total_tokens;llama.cpp 的 -c(上下文)和 -b(批大小)、-ngl(卸载层数)。

# 示例:只压上下文与并发,其他不动
`--max-model-len` 512 `--max-num-seqs` 1
# llama.cpp 对应写法
-c 512 -b 1

重启后重新看日志与显存采样,观察点有三个:一是显存在权重载入完成后是否仍接近上限,二是 KV cache 相关日志里报的预分配量是否明显变小,三是同样的模型在最小配置下能否正常起来。如果降完上下文显存立刻松下来,那之前被占掉的一大块是预分配,不是量化档位的问题;如果几乎没变化,才需要回到权重侧继续查。

对比“只加载不推理”和“推理一次”的显存差

这个对比的目的,是把权重占用和推理时的缓存、激活占用分开。两次都在同一份配置下做,只改是否发请求。

# 第一次:服务起来后不发任何请求,等日志显示加载完成
nvidia-smi `--query-gpu`=memory.used `--format`=csv,noheader,nounits
# 连续采 3 次,记下稳态值

# 第二次:发一条很短的请求,生成期间连续采样
while true; do nvidia-smi `--query-gpu`=memory.used `--format`=csv,noheader,nounits; sleep 1; done
curl -s http://127.0.0.1:8000/v1/completions -H 'Content-Type: application/json' \
  -d '{"model":"local","prompt":"hi","max_tokens":8}'

记录方式建议用一张两列小表:只加载稳态值、推理峰值与推理结束后的回落值。比较方法是看差值而不是看绝对值——差值主要来自已分配的 KV cache、临时激活和运行时缓冲。如果“只加载”这一侧就已经吃满,说明问题在权重或预分配,换量化档位才有意义;如果只加载很宽裕、推理时明显上涨,说明瓶颈在缓存和并发侧,先调上下文与批大小更划算。

确认是量化不匹配时的权重替换前检查

决定换一份权重之前,把下面三项过一遍,能避免换了权重仍然加载失败。

  • 推理框架支持的权重格式:先确认当前目录是 safetensors、GGUF、GPTQ、AWQ 还是 bitsandbytes 中的哪一种,再确认所用的推理框架及其版本是否支持这种量化方法。GGUF 与 safetensors 通常不能互相替换,GPTQ/AWQ 也要看框架是否内置对应内核。
  • 分片文件是否齐全:有索引文件时,用 cat /path/to/model/model.safetensors.index.json | jq '.weight_map | keys | length' 取索引里登记的分片数,再和 ls /path/to/model/*.safetensors | wc -l 的实际数量对照;同时确认每个文件大小非 0,缺一个分片就会在载入中途失败。
  • 加载报错关键字的含义:grep -iE "unexpected|missing key|size mismatch|not supported|out of memory" beam.log | head -n 20。unexpected key / missing key 多为模型结构与代码版本不匹配;size mismatch 常见于量化位数与解量化逻辑对不上;not supported / unsupported quantization 表示框架不支持这份量化格式,属于格式问题,不是显存不够;out of memory 才是显存侧,但要先看它是发生在载入权重还是在预分配缓存阶段。

还需要注意一点:显存吃满和显存溢出是两件事。吃满只说明用量接近上限,服务可能仍在正常跑,不要因为一个接近上限的数字就直接去换权重,先看有没有实际报错、日志停在哪一步。