Skip to content
Published on

在本地跑 LLM 到底需要多少 VRAM — 别查表,用公式算

分享
Authors

引言 — 别去找表,把公式背下来

「跑一个 8B 模型需要几 GB VRAM?」对这个问题,互联网上的标准答案是一张不知从哪儿复制来的表。那张表通常没有出处,不注明上下文长度,量化是哪一档也含糊不清,而最重要的是 — 对你的情况并不适用。模型、上下文、量化都是你自己挑的,而表格不过是别人某一次挑好的组合的快照。

所以先把答案给你。VRAM 大体上分两块,各有一个公式。

(1) 权重 = 参数量 × bpw / 8                [字节]
       bpw = bits per weight(量化档位的「真实」位数)

(2) KV 缓存 = 2 × 层数 × KV头数 × head_dim × 字节/元素 × token 数
       2 = K 和 V,两份
       字节/元素 = f16 为 2,q8_0 为 1

(1) 是常数。在你选定模型和量化的那一刻就固定了。(2) 则 随上下文成正比增长。而大多数人在本地 LLM 上撞墙的原因不是 (1),是 (2)。先把结论说在前面 — 把 Llama-3.1-8B 量化到 Q4_K_M 后权重是 4.58 GiB,可要是把这个模型支持的 128K 上下文用 f16 KV 缓存全部用满,光缓存就正好是 16 GiB。是权重的 3.5 倍。

本文先立起这两个公式,然后 直接对照 llama.cpp 的源代码和官方发布的表格来验证。接着处理诚实的部分 — 削减位数会损失多少质量(只用实测值来谈),以及再往下压真的会更快吗(不会)。

相关地,在浏览器里跑时的约束,我在 llama.cpp WebGPU 后端一篇里单独写过(在那边瓶颈不是 VRAM 而是标签页内存);量化技术本身的版图 — GPTQ、AWQ、FP8、MXFP4 各是什么 — 则在 LLM 量化总整理一篇里。本文只专注 内存算术 这一件事。

VRAM 被什么填满 — 三大块

先看地形。进入 GPU 内存的东西有三样。

  1. 权重。 模型文件本身。与上下文无关,固定不变。
  2. KV 缓存。 为每个已处理的 token 存下注意力的 K 和 V。与 token 数成正比。
  3. 其余。 计算用的临时缓冲区(compute buffer)、CUDA 上下文,如果桌面显示也接在同一张卡上,还得加上画面输出的那一份。

第 3 项没法写成公式。它取决于后端、批大小、有没有开 Flash Attention。我不会在这里编数字 — llama.cpp 在加载模型时会 直接打印给你。去日志里读 KV self sizecompute buffer size 这两行就行。本文集中在把 (1) 和 (2),也就是下限,算准。知道了下限,你才能判断第 3 项到底吃掉了多少。

权重计算 — 「4 位」不是 4 位

公式 (1) 看起来简单,但陷阱藏在 bpw 里。「4 位量化」的 bpw 不是 4。

原因在量化的工作方式里。要把权重变成 4 位整数,就得把「把这些整数还原成真实值的缩放系数(scale)」一起存下来。那个缩放系数不是 4 位。所以真实位数总是大于名义值。

大多少不需要估算。ggml 的块结构体里原原本本写着。看 ggml/src/ggml-common.h 里最简单的 Q8_0

#define QK8_0 32
typedef struct {
    ggml_half d;       // delta
    int8_t  qs[QK8_0]; // quants
} block_q8_0;

读法是这样的:32 个权重(QK8_0)组成一个块,每个块里放 32 个 int8(32 字节)和 1 个 ggml_half 缩放系数(2 字节)。于是,

Q8_0 = (32 × 8 + 16) 位 / 32 个权重
     = 272 / 32
     = 8.5 bits per weight

名义上是 8 位,实际是 8.5 位。每 32 个权重带一个 fp16 缩放系数,正好多出 0.5 位。

现在看 k-quant。Q4_K 把 256 个权重(QK_K)打包成一个超级块。

#define QK_K 256
#define K_SCALE_SIZE 12
typedef struct {
    ggml_half d;                  // 超级块缩放系数                  2 字节
    ggml_half dmin;               // 最小值用的超级块缩放系数        2 字节
    uint8_t scales[K_SCALE_SIZE]; // 量化到 6 位的缩放系数与最小值   12 字节
    uint8_t qs[QK_K/2];           // 4 位量化值                     128 字节
} block_q4_K;

合计 144 字节,装 256 个权重,所以 144 × 8 / 256 = 4.5 bpw。而且这不是我算出来的,是源代码的注释亲口说的 — block_q4_K 的正上方就写着 // Effectively 4.5 bits per weight。同一个文件里有五处。

ggml-common.h 注释直接给出的「实效」bpw
  Q2_K   2.625      Q3_K   3.4375     Q4_K   4.5
  Q5_K   5.5        Q6_K   6.5625
  (Q8_0 没有注释,但从结构体直接得出 8.5)

验证公式是否正确

到这里还只是推导。得确认它对不对。llama.cpp 的官方文档(tools/quantize/README.md)发布了 Llama-3.1-8B 各量化档位的 bpw 和文件大小。把公式倒过来反推参数量 — 参数量 = 大小 × 8 / bpw — 每一行都应该得出同一个值。

quant      发布 bpw   发布大小(GiB)   反推的参数量
F16        16.0005      14.96            8.031 B
Q8_0        8.5008       7.95            8.033 B
Q6_K        6.5633       6.14            8.036 B
Q5_K_M      5.7036       5.33            8.027 B
Q4_K_M      4.8944       4.58            8.038 B
Q3_K_M      3.9960       3.74            8.040 B
Q2_K        3.1593       2.95            8.021 B
IQ1_S       2.0042       1.87            8.015 B

全部聚在 8.02~8.04B。而 Llama-3.1-8B 的真实参数量,以整数写在 Hugging Face 的 safetensors 元数据里 — 8,030,261,248 个。公式是对的。在四舍五入误差范围内,每一行都命中了真实值。

还有一个更好的检验。把从块结构体推导出的值,和 llama.cpp 实际测量并发布的值并排放在一起。

                 从结构体推导    发布的实测      差值
Q8_0                 8.5000          8.5008      +0.0008
Q6_K                 6.5625          6.5633      +0.0008
Q5_K_S               5.5000          5.5704      +0.0704
Q5_K_M               5.5000          5.7036      +0.2036
Q4_K_S               4.5000          4.6672      +0.1672
Q4_K_M               4.5000          4.8944      +0.3944
Q3_K_S               3.4375          3.6429      +0.2054
Q3_K_M               3.4375          3.9960      +0.5585
Q2_K                 2.6250          3.1593      +0.5343

Q8_0 和 Q6_K 吻合到小数点后第三位(8.5000 对 8.5008)。纯靠块算术就预测出了官方数字。可其余的档位最多超出 0.56 位。这个超出部分就是下一节的主题。

为什么 Q4_K_M 是 4.89 而不是 4.5

_S(small)和 _M(medium)不是量化类型,而是混合配方。它并不是把所有张量都压成 Q4_K,而是把某些张量留在更高的精度上。

配方明确写在 src/llama-quant.cpp 里。对 Q4_K_M 来说,挂着这样几条规则。

  • 注意力的 attn_v 张量 — 在一部分层被提升到 Q6_K(use_more_bits 为真的那些层)
  • FFN 的 ffn_down 张量 — 同样在一部分层被提升到 Q6_K
  • 注意力 QKV 张量 — 提升到 Q5_K

也就是说,Q4_K_M 是「大部分 4.5 位,但对质量敏感的一部分张量是 6.5 位」。所以平均值从 4.5 升到 4.8944。源代码的注释把这套设计哲学讲得很清楚 — 把关于 70B 模型 attn_v 的那条评论搬过来:因为 8 个头共享 attn_v 权重,那个张量比 attn_q 小 8 倍,因此「几乎不增加模型大小,就能把量化精度提高不少」。

这里有一条对实务很重要的结论。并不是说 _M 总是比 _S 好。 _M 是用更多的位去换质量的交易,这笔交易划不划算,要看后面的质量数字才知道。而 Q4_K_M 为什么固化成了本地阵营的默认值,答案也在后面。

KV 缓存 — 随上下文成比例增长的那一块

现在是真正的问题。

把公式 (2) 再写一遍。

KV 缓存 = 2 × L × n_kv_heads × head_dim × bytes × n_tokens

这个公式对不对,同样可以拿 llama.cpp 的源代码来确认。src/llama-kv-cache.cpp 为每一层这样创建 K 和 V 张量。

const uint32_t n_embd_k_gqa = hparams.n_embd_k_gqa(il);
ggml_tensor * k = ggml_new_tensor_3d(ctx, type_k, n_embd_k_gqa, kv_size, n_stream);
ggml_tensor * v = ggml_new_tensor_3d(ctx, type_v, n_embd_v_gqa, kv_size, n_stream);

n_embd_k_gqa 的定义在 src/llama-hparams.cpp

uint32_t llama_hparams::n_embd_k_gqa(uint32_t il) const {
    const uint32_t n_head_kv = this->n_head_kv(il);
    return n_embd_head_k(il) * n_head_kv;
}

也就是说,一个 head_dim × n_kv_heads 大小的向量,按 token(kv_size)、K 和 V 两份、逐层分配。正是公式 (2)。

这里的关键是 n_kv_heads不是注意力头数,而是 KV 头数。如今的模型几乎全都用 GQA(Grouped-Query Attention)把 KV 头砍掉了一大截。Llama-3.1-8B 有 32 个注意力头,KV 头却只有 8 个。在这里填 32,答案就错 4 倍。互联网上的表格频繁出错的地方恰恰就是这里。

用 Llama-3.1-8B 算一遍

配置值在 config.json 里 — 32 层、32 个注意力头、8 个 KV 头、hidden 4096、最大上下文 131,072。head_dim 没有单独给出,所以是 hidden / n_heads = 4096 / 32 = 128。(顺带一提,Meta 的官方仓库需要访问审批,匿名查询是被挡住的。上面这些值和后文的参数量读自镜像仓库,出处在参考资料里注明了。)

每个 token = 2 × 32层 × 8个KV头 × 128 × 2字节(f16)
           = 131,072 字节
           = 128 KiB / token

128K 上下文(131,072 个 token)全部用满:
  131,072 字节 × 131,072 个 token = 17,179,869,184 字节 = 正好 16 GiB

一个 token 就是 128 KiB。好背。而把 128K 填满就是 16 GiB,一分不差。

现在把它和刚才那个数字并排放。

Llama-3.1-8B, Q4_K_M
  权重           4.58 GiB   (固定)
  KV @  4K        0.5 GiB
  KV @ 32K        4.0 GiB   ← 在这里已经追平权重
  KV @ 128K      16.0 GiB   ← 权重的 3.5 倍

拦住你的不是模型,而是上下文。 成功把 8B 模型压到 4 位,完全不等于你就能用上 128K 上下文。

别的模型不一样 — 所以才需要公式

同样是「8B」,数值却不同。Qwen3-8B 的 config.json 是 36 层、8 个 KV 头、head_dim 128、最大上下文 40,960。

Qwen3-8B: 2 × 36 × 8 × 128 × 2 = 147,456 字节 = 144 KiB / token
          把最大上下文 40,960 个 token 全部用满 → 5.625 GiB

按每 token 算它比 Llama 多吃 12.5%(因为有 36 层),但最大上下文是 40,960,总量反而小得多。「8B」这个标签对 KV 缓存什么都没告诉你。 层数、KV 头数、head_dim,以及你实际要开的上下文 — 是这四样决定的。

KV 缓存也可以量化

llama.cpp 提供把 KV 缓存本身以更低精度存储的选项。照 tools/server/README.md 原样搬过来,

  • -ctk, --cache-type-k TYPE — 允许值:f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1(默认 f16)
  • -ctv, --cache-type-v TYPE — 允许值相同,默认值相同

换成 q8_0,每个元素从 2 字节降到 1 字节,KV 缓存正好减半。Llama-3.1-8B 的 128K 缓存从 16 GiB 降到 8 GiB。允许值列表里有一点值得留意 — k-quant(像 q4_K 这样的)不在列表里。 KV 缓存只能用传统(legacy)量化。

不过这里是有质量代价的,而那个代价没有像权重量化那样被充分测量和公开。这个方向的最新研究在 INT4 KV 缓存量化一篇里单独写过。这里只把 内存正好减半 这一条算术敲定,然后继续。

那么你的卡里到底能装什么

现在开始组装。假设把 Llama-3.1-8B 以 Q4_K_M(4.58 GiB)装上去,剩下的空间全部给 KV,能开多大的上下文。

注意 — 下面是 扣除计算缓冲区和画面输出之前的理论上限。实际会比这里更少。那一份如前所述我不会编造,你得从自己的日志里读。

VRAM     扣除权重后剩余    f16 KV 能开的上下文(上限)     换 q8_0 KV
16 GiB      11.4 GiB           约  93,000 token           约 187,000
24 GiB      19.4 GiB           约 159,000 token           约 318,000
32 GiB      27.4 GiB           约 224,000 token           约 449,000

读法很重要。

  • 16GB 卡:8B 模型装得绰绰有余(4.58 GiB)。但 128K 上下文在 f16 下上限是 93,000 token,打不开。 再扣掉计算缓冲区就更打不开了。打开 -ctk q8_0 -ctv q8_0,128K 才总算进入射程。
  • 24GB 卡(RTX 3090/4090):f16 的 128K 在上限之内(159,000),但余量不大。
  • 32GB 卡(RTX 5090):f16 128K 轻松放下。

一句话总结 — 「模型装不装得进卡」是个错误的问题。 正确的问题是「模型 + 我实际要用的上下文装不装得下」。

量化削掉多少质量 — 只谈实测值

从这里开始我不主张,只引用。

llama.cpp 在 tools/perplexity/README.md 里测量并公开了各量化档位的质量损失。这张表好就好在 把条件写明了

出处: llama.cpp tools/perplexity/README.md — "LLaMA 3 8b Scoreboard"
  Revision : f364eb6f
  Backend  : CUDA
  CPU      : AMD Epyc 7742
  GPU      : 1x NVIDIA RTX 4090
  数据集   : Wikitext-2 测试集

先说 一个大警告。用 GitHub API 查那个 revision f364eb6f,提交日期是 2024 年 4 月 30 日。也就是说,这张表虽然挂在当前 master 的 README 里,数据本身却出自一个 2 年多前的构建。这期间 llama.cpp 的量化实现一直在变。所以这些数字 要用来读量化档位之间的相对排序,不要把绝对值原样搬到今天的构建上。 不过,这是该项目亲自测量并公开、条件写明的唯一公开数据。

指标看两个。PPL(perplexity)越低越好;KLD(KL 散度)衡量概率分布偏离 fp16 有多远,0 表示完全相同。README 自己就把表格按更信任 KLD 的方向排好了序。

quant      大小(GiB)     PPL        ΔPPL       KLD        平均 Δp
f16          14.97    6.233160        —      0.000551     0.001 %
q8_0          7.96    6.234284   0.002650   0.001355    -0.019 %
q6_K          6.14    6.253382   0.021748   0.005452    -0.007 %
q5_K_M        5.33    6.288607   0.056974   0.010762    -0.114 %
q4_K_M        4.58    6.407115   0.175482   0.031273    -0.596 %
q3_K_M        3.74    6.888498   0.656864   0.101913    -1.990 %
q2_K          2.96    9.751568   3.519934   0.445132    -9.123 %
iq1_S         1.88   58.097760  51.866126   2.211278   -32.471 %
   (基准为不用 imatrix。仅 iq1_S 使用 "WT 1m" imatrix)

这张表的读法。

  • q8_0 实际上是免费的。 PPL 从 6.233 到 6.234。大小减半,KLD 只有 0.0014。几乎没有理由死守 fp16。
  • q6_K、q5_K_M 也非常便宜。 平均 Δp(正确 token 的概率变化)分别是 -0.007%、-0.114%。
  • q4_K_M 是拐点。 大小只有 fp16 的 31%,平均 Δp 是 -0.596%。这就是 Q4_K_M 成为本地阵营默认值的原因。
  • 从 q3_K_M 开始就明显疼了。 PPL 从 6.41 跳到 6.89。
  • q2_K 已经是另一个模型。 PPL 9.75,平均 Δp 是 -9.1%。
  • iq1_S 直接崩塌。 PPL 58.1。原版是 6.23,也就是 9.3 倍。

README 自己附上的警告,原样搬过来

在使用这些数字之前,文档亲自加了几条限定。按原文的意思搬过来,

  • perplexity 不能在模型之间直接比较。分词器不同时尤其如此。
  • 微调即便让人评的输出质量上升,perplexity 通常也会上升。
  • llama.cpp 的数值强烈依赖实现细节,不能与其他项目的数值直接比较。

第三条尤其重要。不要把这张表的 PPL 和 GPTQ 或 AWQ 论文里的 PPL 摆在一起比。 它们是不同实现各自的自测值,不是共同基准。

而第二条警告的含义很深 — perplexity 只是质量的代理指标,不是质量本身。「q4_K_M 的平均 Δp 是 -0.596%」这句话,并不意味着「你的代码生成任务会变差 0.596%」。那个没有人替你测过。得用你自己的任务来测。

同一个量化,在不同模型上疼法不同

这是这张表里最不为人知、也最重要的事实。看同一份文档里的 "LLaMA 2 vs. LLaMA 3 Quantization comparison" 表(同一 revision、同一块 4090),

Mean PPL ratio (1.0 = 无损失)
                    L2 7b        L3 8b
  q8_0            1.000689     1.000425
  q6_K            1.002406     1.003490
  q4_K_M          1.014242     1.028160      ← 1.4% vs 2.8%,两倍
  q2_K            1.107955     1.564849      ← 10.8% vs 56.5%,五倍

Mean KLD
  q2_K            0.108903     0.445132      ← 4.1 倍

同一个 q2_K,在 Llama-2 7B 上把 PPL 拉高 10.8%,在 Llama-3 8B 上却拉高 56.5%q4_K_M 也是 1.4% 对 2.8%,正好两倍。

这件事的含义很大。「Q4_K_M 的质量损失微乎其微」这句话,不加模型限定就不成立。 用更多 token 训练得越狠的模型,权重里的余量越少,同样的位预算下丢得就越多。2026 年的模型比 Llama-3 训练得还要多。所以上表的数字对今天的最新模型来说,很可能是往乐观的方向偏的。 除了对你自己的模型重新测一遍,没有别的办法。

顺带一提,关于 imatrix(importance matrix),README 也留下了诚实的观察 — 用更多 Wikitext token 去构建 imatrix,并没有带来一致的改进。imatrix 本身是有帮助的(q4_K_M 的 KLD 从 0.0313 降到 0.0282),但「更多 token = 更好的 imatrix」在这份数据里得不到支持。

tok/s 数字不带硬件就扔掉

接下来是速度。而这里我最想说的其实是方法论。

一个 tok/s 数字要有意义,至少得同时带上四样东西 — 精确的硬件(GPU 型号)、模型、量化,以及上下文长度(或批长度)。四样缺了任何一样,那个数字就无法向别人传递任何信息。

让我证明这不是唠叨。llama.cpp 的 官方 README(tools/quantize/README.md)里挂着这样一张表。

Measure                       Q4_K_M          Q8_0            F16
bits/weight                   4.8944          8.5008        16.0005
size (GiB)                      4.58            7.95          14.96
prompt processing t/s @ 512   821.81 ±21.44   865.09 ±8.30   923.49 ±0.53
text generation t/s @ 128      71.93 ±1.52     50.93 ±0.08    29.17 ±0.04

模型写明了(Llama-3.1-8B)。量化写明了。上下文长度也写明了(@ 512、@ 128)。连误差范围都附上了。可是 哪里都没有硬件。 翻遍整份文档,GPU、CPU、后端、构建 revision,一个都没有。最后一次修改这张表的 PR(#24133,2026 年 6 月 5 日合并)的正文里也没有。

所以「Q4_K_M 是 71.93 tok/s」这句话 单独拿出来毫无用处。是在 3090 上、5090 上,还是 M4 Max 上? 3090 和 5090 的内存带宽差 1.9 倍。想想同一个项目在 perplexity 的 README 里把 CPU、GPU、后端、revision 全都写明了,这更像是文档的一处失误。

但是 — 有趣的部分来了 — 这张表里仍然有能读出来的东西。绝对值读不了,但所有行都出自同一(未知的)硬件,所以 行与行之间的比率 是有效的。下一节就讲这个。

位数再压就更快吗 — 只到某条线为止

先说量化为什么能提速。以批大小 1 生成 token 时,模型 每生成一个 token 都要把全部权重从内存里读一遍。计算量不大,要读的东西却很多。所以解码被内存带宽卡住(memory-bandwidth-bound)。天花板公式由此而来。

tok/s 上限 = 内存带宽 / 模型大小          (dense 模型,批大小 1)

用 NVIDIA 的 RTX Blackwell 架构白皮书里的带宽规格来算一算。照白皮书正文 — RTX 5090 搭载 28 Gbps GDDR7,给出 1.792 TB/sec 的峰值内存带宽。同一份白皮书的对照表写着 3090 是 936 GB/sec、4090 是 1008 GB/sec。

Llama-3.1-8B Q4_K_M (4.58 GiB) 的理论天花板
  RTX 3090 (936 GB/s)  →  190 tok/s
  RTX 4090 (1008 GB/s) →  205 tok/s
  RTX 5090 (1792 GB/s) →  364 tok/s

这是 天花板。实测永远低于它。但这个公式告诉你,对你的卡来说「不可能比这更快」,还能让你判定某个基准数字在物理上说不说得通。要是有文章说在 4090 上把 8B Q4_K_M 跑到了 300 tok/s,那要么不是批大小 1 的解码,要么量的是别的东西。

可是公式在低端碎掉了

现在用上刚才说的「比率是有效的」。如果解码纯粹被带宽卡住,那么 大小 × tok/s 在所有量化档位上都应该得出 一个恒定的值(= 有效带宽)。拿 llama.cpp 官方表格里的数字直接乘一遍。(下面的乘法是我用他们发布的数值算的。)

quant       大小(GiB)   生成 tok/s    大小 × tok/s
F16           14.96       29.17          436
Q8_0           7.95       50.93          405
Q6_K           6.14       58.67          360
Q5_K_M         5.33       67.23          358
Q4_K_M         4.58       71.93          329
Q3_K_M         3.74       71.68          268
Q2_K           2.95       79.85          236
IQ2_XXS        2.23       79.86          178
IQ1_S          1.87       79.73          149

不恒定。从 436 一路跌到 149。 意味着位数压得越低,带宽的利用率就越差 — 要读的数据是变少了,可把它解回来(dequantize)的计算变多了,瓶颈从带宽挪到了计算上。

把结果换成比率看,清楚得扎眼。

F16    → Q4_K_M :  大小缩小 3.27 倍,速度加快 2.47 倍   (交易成立)
Q4_K_M → Q2_K   :  大小缩小 1.55 倍,速度加快 1.11 倍   (交易很差)
Q4_K_M → IQ1_S  :  大小缩小 2.45 倍,速度加快 1.11 倍   (交易最差)

从 Q4_K_M 降到 IQ1_S,模型缩小 2.45 倍,速度却只快 11%。 与此同时质量 — 按前一节的表 — PPL 从 6.41 到 58.1(9.1 倍),KLD 从 0.031 到 2.21(71 倍)。这是一笔换来 9 倍差的模型、收下 11% 速度的交易。

限定条件诚实地写上。这张速度表(Llama-3.1-8B,硬件不明)和前面的质量表(Llama-3 8B、RTX 4090、2024 年 4 月的 revision)是 互不相同的两次运行。模型有微妙差异,构建也不同。所以横跨两张表去读,不是严格的比较,是看方向。即便如此,两张表各自内部的排序都很稳固,而且指向同一个地方。

所以降到极低位数的唯一正当理由不是速度,而是「除此之外装不下」。 为了速度选 IQ1_S,这笔算术算不平。

计算会变样的情况 — MoE 与混合注意力

两个公式都带着假设。如今的模型正在打破那些假设。

MoE(Mixture of Experts)。 照搬 OpenAI 的 gpt-oss-20b 模型卡的原话,这个模型是「21B 参数、活跃参数 3.6B」。在这里两个公式分道扬镳。

  • 内存需要全部。 事先不知道哪个专家会被选中,21B 得全部装进去。
  • 带宽只读活跃的部分。 每个 token 只读 3.6B 那么多。

也就是说,前面的天花板公式 带宽 / 模型大小 里,分母从 整体大小换成了活跃大小。MoE「重而快」的原因就在这里。内存付 21B 的价钱,速度拿 3.6B 的回报。同一份模型卡写明,gpt-oss-20b 用 MXFP4 量化 MoE 权重,能在 16GB 内存以内运行

混合注意力。 看 gpt-oss-20b 的 config.jsonlayer_types 交替排列着 sliding_attentionfull_attentionsliding_window 是 128。滑动窗口层只需要持有最近 128 个 token 的 KV,所以 那些层的 KV 不随上下文增长。公式 (2) 的 n_tokens 变成了每层各不相同。

重点在于这不是例外。llama.cpp 的源代码已经把这一点考虑进去了 — 前面看到的 n_embd_k_gqa(il) 之所以接受层索引 il 作为参数,正是因为每一层的 KV 头数可以不同。没有任何表格能替代打开模型的 config.json

那么,你要放弃的是什么

来整理。在本地 LLM 上你放弃的东西有三样。

第一,质量。 用实测值来说 — q8_0 实际上免费(KLD 0.0014),到 q5_K_M 都很便宜(KLD 0.011),q4_K_M 是拐点(KLD 0.031),从 q3_K_M 开始疼(KLD 0.102),q2_K 以下是另一个模型(KLD 0.445+)。但这个排序是 2024 年 4 月构建的 Llama-3 8B 基准,而且训练得越充分的模型,在同样的位数下丢得越多。

第二,上下文。 这是被低估得最厉害的一项。把 8B 模型装进 16GB 的卡很容易。给那个模型配 f16 的 128K 上下文是不可能的。大多数人卡在「模型明明装进去了,为什么长文档塞不进去」。

第三,速度的可预测性。 别人的 tok/s 不是你的 tok/s。没写明硬件的数字直接扔掉,写明了的数字也要用你的卡的带宽重新算一遍天花板。

决策规则

我不会用「视情况而定」收尾。请这样做。

  1. 量化从 Q4_K_M 开始。 它是质量/大小的拐点,再往下压速度也几乎不涨。卡上有余量时,升到 Q5_K_M 或 Q6_K,总是好过降到 Q4 以下再把省出的空间空着。
  2. 定下权重之后,把剩余 VRAM 当作 KV 预算,反推上下文。 别反着来。如果上下文是先定死的,那它决定量化。
  3. KV 装不下时,先开 -ctk q8_0 -ctv q8_0,再考虑进一步削权重。 缓存正好减半。这通常比把权重从 Q4 降到 Q3 更划算。
  4. 每次换模型都打开 config.json 层数、KV 头数、head_dim。30 秒的事。
  5. 用你的卡、你的模型、你的上下文测一次。 而且测完了,就把硬件写明再分享。

什么时候不该用本地

诚实地说 — 很多情况下本地不是答案。

  • 质量是第一优先时。 4 位的 8B 不是前沿模型。那道差距量化填不平。
  • 长上下文是本质需求时。 KV 算术会赢你。如果 128K 是常态,本地单卡就是选错了工具。
  • 有并发用户时。 上面的公式全部以批大小 1 为准。同时接多个请求,KV 缓存就按用户数成倍增加。
  • 偶尔才用时。 一张卡的钱等于非常多的 API token。

反过来,本地赢的情况也很明确 — 数据不能离开设备时、必须离线时、调用量大且可预测从而能摊销固定成本时、以及延迟比网络往返更重要时。 而且如 用一张 RTX 5090 亲手盘小模型的记录所示,小模型就够用的任务比想象中多。

结语

本地 LLM 的 VRAM 问题,正确答案不是一个数字,而是两个公式。

权重     = 参数量 × bpw / 8
KV 缓存  = 2 × 层数 × KV头数 × head_dim × 字节 × token 数

第一个是固定的,第二个会长。而且如本文所确认的,第二个轻轻松松就越过了第一个 — Llama-3.1-8B Q4_K_M 的权重是 4.58 GiB,128K 上下文的 f16 KV 是 16 GiB。

这些公式可以信,不是因为我说可以。是因为从 ggml 块结构体推导出的 Q8_0 的 8.5 bpw 与 llama.cpp 发布的 8.5008 吻合,把公式倒过来反推的参数量 8.02~8.04B 与真实值 8,030,261,248 吻合。公式能复现厂商自己发布的数字,它就也能在你的情况下工作。 表格做不到这一点。

最后再补一条方法论。写这篇文章时我纠缠得最久的问题,是 llama.cpp 官方 README 的速度表里没有硬件。那张表写明了模型、量化、上下文长度,甚至连误差范围都给了,却没有 GPU 的名字。于是「71.93 tok/s」成了一个无法引用的数字。当你分享基准的时候 — GPU、模型、量化、上下文长度。四样都写上。缺了这四样的数字,谁都用不了。

参考资料