被 Naive-N0.5-Flash 的 MoE 结构吸引、想在自己显卡上跑起来的人,常见的误区是按“激活参数量”去估显存。实际情况是:权重文件要整份加载进显存,不会因为每次只激活部分专家就少占地方;KV 缓存随上下文长度线性增长,是最容易失控的一项;再叠加 CUDA context、算子 workspace 和显存碎片,才是真正要跟 nvidia-smi 上的可用显存对齐的数字。
判断顺序建议固定为:先按权重文件字节数算出常驻权重,再按 config.json 里的层数、KV 头数、精度和目标上下文算 KV 缓存,最后加上框架与驱动的固定余量,和本机可用显存对比。三项之和明显低于可用显存才谈得上本地部署;接近或超出时,优先降上下文,其次降精度,最后才降并发。
按权重文件总大小换算出常驻显存需求
常驻权重是无论怎么调上下文、怎么调并发都不会变的那部分开销。它最可靠的估算方式不是拿参数量乘位宽,而是直接量权重文件的字节数——量化方案对 embedding、lm_head、部分敏感层往往保留更高精度,张量还有对齐填充和分片元数据,参数量乘 bit 只是理论下界,和实际加载量可能差出一截。
换成 GiB 的公式可以写成:常驻权重 ≈ 所有权重分片文件字节数之和 ÷ 1024³。在模型目录下执行:
ls -l ./Naive-N0.5-Flash/*.safetensors | awk '{s+=$5} END {printf "%.2f GiB\n", s/1024/1024/1024}'
# 假设输出 12.34 GiB,这个值就是常驻权重的起点
两个需要留意的点:一是目录名和分片命名按你实际下载到的文件替换;二是如果显卡或框架不支持文件里的原始精度,加载时可能被反量化成 bf16 再放进显存,实际占用会比文件更大。这一点只能通过加载完成后看框架日志的显存报告或再跑一次 nvidia-smi 确认,估算阶段先按文件大小取个下界。
估算 KV 缓存随上下文长度增长的部分
KV 缓存是唯一会随上下文长度、并发数一起涨的项,所以要单独算。通用公式骨架是:
KV 字节数 ≈ 2 × 层数 × KV头数 × head_dim × 精度字节数 × 序列长度 × 并发数
# 2 表示 K 和 V 各一份
这些字段需要从模型配置文件里读,不要凭印象填:
num_hidden_layers——注意力层数,也就是要缓存多少层num_key_value_heads——KV 头数,GQA 模型里它通常小于num_attention_heads,这里填错会把估算放大数倍head_dim——没有直接给时用hidden_size ÷ num_attention_heads推算torch_dtype或量化配置——决定每个数值占 2 字节还是 1 字节
import json
cfg = json.load(open("./Naive-N0.5-Flash/config.json"))
layers = cfg["num_hidden_layers"]
kv_heads = cfg.get("num_key_value_heads", cfg["num_attention_heads"])
head_dim = cfg.get("head_dim", cfg["hidden_size"] // cfg["num_attention_heads"])
print(layers, kv_heads, head_dim)
# 把 seq_len、batch、每元素字节数代入上面的公式即可
MoE 的专家数量不进入这个公式,KV 只跟注意力层有关;但专家结构会影响临时激活显存,这部分后面按框架日志观察更实际。
把估算结果与本机可用显存对齐
把前面几项填进一张表,再跟机器上真正空闲的显存比。注意是“空闲”,不是“总量”:
nvidia-smi `--query-gpu`=memory.total,memory.used,memory.free `--format`=csv
| 项目 | 估算值 | 依据 |
|---|---|---|
| 常驻权重 | (示例)12.3 GiB | 权重文件字节数换算 |
| KV 缓存 @ 目标上下文 | 待填 | 层数 × KV头数 × head_dim × 精度 × 长度 |
| 激活与临时缓冲 | 待填 | 框架日志中的 peak 值更可靠 |
| 框架与驱动余量 | 通常留数 GiB | CUDA context、算子 workspace、显存碎片 |
| 合计 | 各项之和 | — |
| 本机可用显存 | memory.free | nvidia-smi |
余量具体留多少要结合框架版本、是否开启图捕获和量化内核确认,没有通用数字,但完全不留余量几乎一定会在长上下文或并发上来时触发 OOM。分档可以粗略按合计与可用显存的比例看:合计明显低于可用,属于够用,可以按目标上下文和并发直接试;两者接近,属于勉强,先按单并发、短上下文跑通再逐步加压;合计超出可用,属于不够,不要靠反复加载去试,直接进入下一节的降配顺序。
在不够用时确定降上下文、降精度、降并发的先后顺序
乱调参数会让排查失去参照,所以按影响面从窄到宽来降:
- 先降上下文长度。它只影响 KV 缓存一项,对权重和框架开销没有影响。代价是长文档、长对话会被截断,需要确认业务能不能接受。观测方式是看框架日志里声明的 max context 和实际 KV 分配量是否同步下降,以及请求是否被截断。
- 再降精度。权重侧量化影响常驻权重,KV 量化影响 KV 缓存,两项都会动。代价是输出质量可能变化,部分量化内核需要特定硬件支持,缺支持时会回退甚至报错。观测方式是固定同一段提示词对比输出,检查加载日志里有没有反量化或回退提示,再看
nvidia-smi占用是否真的下降。 - 最后降并发。并发主要影响 KV 缓存和临时缓冲里的 batch 维度。代价是吞吐下降、请求排队变长。观测方式是并发请求时是否出现 OOM,以及单请求的 token 延迟变化。
不建议把 swap 或 CPU offload 当成性能方案:它们只是让服务不崩的止血手段,权重或 KV 被换到主机内存后,每步推理都要走 PCIe,速度会明显下降,只适合验证流程是否跑通,不适合当作长期运行配置。