0%
27B 不是把 2.4T 压小压出来的

拆开 Qwen3.8:一台大模型的机器手册

不是让你看懂,是让你能自己算
两份 config.json、两张官方模型卡,拆成 20 章 22 万字、236 道题、13 个在浏览器里真算的实验室和 3 个 3D 场景。跟着 18 级建造台阶,从矩阵乘法一路造到一个能跑的 Qwen3.8-27B 最小实现。

第0章 开工之前:你需要的全部数学

这一章回答一个问题:要看懂一台叫 Qwen3.8 的机器,最少需要会哪几样数学?答案是六样,全部在这一章讲完,之后十九章不再补课。

学完这一章你应该能做到

  • 用自己的话解释「一串数字为什么能表示一个词的意思」,并手算两个向量的点积与余弦相似度
  • 判断两个矩阵能不能相乘,并手算一个 2×3 与 3×2 的乘法
  • 看到「一个 5120×17408 的权重矩阵」,立刻说出它有多少个参数、在 BF16 下占多少字节
  • 说清参数与超参数的差别,并指出「27B」这个名字数的到底是什么
  • 读懂 FP32 / BF16 / FP16 / FP8 / INT4 的位宽划分,解释为什么训练偏爱 BF16
  • 把 27.36B、2.42T、54.66 GB、16 GiB 这些数放到同一把数量级尺子上比较
  • 手算三个数的 softmax,并说清为什么必须先取指数
前置:没有。这一章假设你只会加减乘除、知道 10 的乘方是什么意思。如果你已经会矩阵乘法和浮点数,可以只读 0.4 与 0.6 两节,其余跳过。

0.1 向量:一串数字为什么能表示「意思」

不用它会怎样

计算机只会算数,不会读字。最偷懒的办法是给每个词编个号:猫是 1,狗是 2,汽车是 3。但这样一来,1 和 2 之间的差是 1,2 和 3 之间的差也是 1,机器会认为「猫离狗的距离」等于「狗离汽车的距离」。编号里没有装进任何关于意思的信息,它只是一个门牌号。要让距离变得有意义,一个数不够,得用一串。

先从一个不涉及语言的例子开始。要描述一个人,你可以用三个数:身高 175、体重 68、年龄 17。这三个数排成一行写作 (175, 68, 17),就是一个三维向量(vector),它有三个维度(dimension),每一维记录一件事。向量有意义的地方在于:两个人像不像,现在可以算了。(175, 68, 17) 和 (176, 70, 17) 三个数都接近,(160, 45, 42) 就明显是另一个人。相似这件从前只能靠感觉的事,变成了一道算术题。

把维度从 3 加到 5120,把「身高体重年龄」换成 5120 件说不清是什么的事,就是 Qwen3.8-27B 表示一个词的方式——它的 hidden_size 就是 5120。每个词在机器里都是一串 5120 个小数,打印出来每行放 10 个要占 512 行。

那怎么算「像不像」?最常用的两把尺子是点积和余弦相似度。点积(dot product) 的算法是:两个向量对位相乘,再全部加起来。

ab = a1b1 + a2b2 + ⋯ + anbn
式 0-1
符号是什么直觉
ab两个长度都是 n 的向量(黑体表示向量)两串等长的数字
aia 的第 i 个分量,是一个普通实数(斜体表示标量)i 项的取值
n维度,两个向量必须一样长一共记录了几件事
求和号,把 i 从 1 走到 n 的所有项加起来「全部加起来」的缩写

手算一遍。假设有三个玩具向量(真实模型是 5120 维,这里缩到 3 维只为了能手算):

表 0-1:三个玩具向量(示意数据,非模型真实取值)
第 1 维第 2 维第 3 维
0.90.10.2
0.80.20.1
汽车0.10.90.7

猫 ⋅ 狗 = 0.9×0.8 + 0.1×0.2 + 0.2×0.1 = 0.72 + 0.02 + 0.02 = 0.76
猫 ⋅ 汽车 = 0.9×0.1 + 0.1×0.9 + 0.2×0.7 = 0.09 + 0.09 + 0.14 = 0.32
点积大的那一对更像。

但点积有个毛病:把猫的每一维都乘以 10,它和任何向量的点积都会跟着变成 10 倍,可它还是那个猫。余弦相似度(cosine similarity) 就是来修这个毛病的——先把两个向量各自的长度除掉,只留方向。

cos θ = aba∥ ∥b, ∥a∥ = √(a12 + ⋯ + an2)
式 0-2
符号是什么直觉
aa 的长度(模长),等于各分量平方和再开方这串数字整体有多大
θ两个向量之间的夹角方向差多少
cos θ取值在 −1 到 1 之间:1 表示方向完全一致,0 表示垂直(无关),−1 表示完全相反一把归一化过的相似度尺子

接着上面的数:∥猫∥ = √0.86 ≈ 0.927,∥狗∥ = √0.69 ≈ 0.831,∥汽车∥ = √1.31 ≈ 1.145。于是 cos(猫, 狗) ≈ 0.76 ÷ 0.770 ≈ 0.99,cos(猫, 汽车) ≈ 0.32 ÷ 1.062 ≈ 0.30。0.99 对 0.30 比 0.76 对 0.32 更好读,因为它有固定的上下界。

打个比方:颜色的三个数

屏幕上每一种颜色都是三个数:红、绿、蓝各 0 到 255。(255, 0, 0) 是纯红,(250, 20, 10) 是稍微脏一点的红,(0, 0, 255) 是蓝。你从来不觉得「用三个数表示一种颜色」有什么神秘,因为你天天在用。词向量是同一件事,只是把三个轴换成 5120 个轴。

类比失效处:RGB 三个轴是人指定的,每个轴叫什么名字、量的是什么,事先就定好了。词向量的 5120 个轴没有名字,是训练过程自己长出来的——没有人能指着第 137 维说它量的是「褒义程度」。所以你可以拿两个词向量比相似度,但不能像读 RGB 那样逐维解读一个词向量。

我能不看材料说清:为什么给词编号(猫=1,狗=2)不行,而给词一串数字就行。

已知 u = (2, 0, 1),v = (1, 3, 2)。求 uv,并求 ∥u∥。

先想:点积是「对位相乘再相加」,模长是「平方和再开方」。两个都不需要角度,只需要这两串数字本身。
关键在于对位:第 1 维只和第 1 维相乘,不要交叉。模长只用到 u 自己,和 v 无关。
第一步这样走:写出三项乘积 2×1、0×3、1×2,再相加。模长先写平方和 22 + 02 + 12
完整答案:uv = 2×1 + 0×3 + 1×2 = 2 + 0 + 2 = 4。∥u∥ = √(4 + 0 + 1) = √5 ≈ 2.236。注意第 2 维虽然 v 有 3,但 u 那一维是 0,乘出来就是 0——点积里任何一方为 0 的维度都不贡献。这正是「维度多但大部分维度不起作用」在数学上的样子。

变式:把 u 整体乘以 100,变成 (200, 0, 100)。点积会变成多少?余弦相似度会变吗?(两个问题的答案不一样,这就是 0.1 节最后要修的那个毛病。)

0.2 矩阵乘法:这台机器唯一的动作

不用它会怎样

模型每往前走一步,做的事都是同一件:拿进来一串 5120 个数,吐出去另一串数。这中间要做的是「新的每一个数,都由旧的全部 5120 个数按某种配比混出来」。如果一条一条写,那是 5120 × 5120 = 2600 多万条乘法算式。矩阵乘法就是把这一大堆算式打包成一个动作、一个符号、一次调用。整台机器从头到尾几乎只做这一个动作——所以看懂它,就看懂了这台机器的运动方式。

矩阵(matrix) 就是把数排成长方形的表格。一个 n×k 的矩阵有 nk 列。矩阵乘法(matrix multiplication) 记作 @,规则只有一句:

一句话记住:(n×k) @ (k×m) → (n×m)。中间那个 k 必须两边相等,算完之后它就消失了。

为什么中间那个 k 必须相等?因为结果里的每一个数,都是「左边取一整行、右边取一整列,两者做点积」。左边一行有 k 个数,右边一列也有 k 个数,两串数必须一一配对才能相乘相加——一边 3 个一边 5 个,配不上,这个乘法就不存在。写成公式:

Cij = ∑t=1k Ait Btj
式 0-3
符号是什么直觉
Ait左矩阵第 i 行、第 t 列的那个数从左边取第 i
Btj右矩阵第 t 行、第 j 列的那个数从右边取第 j
Cij结果第 i 行第 ji 行与第 j 列的点积
t被求和消掉的那个下标,范围 1 到 k就是「中间那个必须相等的维度」

手算一个 2×3 @ 3×2。设 A = [[1, 2, 3], [4, 5, 6]](2 行 3 列),B = [[7, 8], [9, 10], [11, 12]](3 行 2 列)。中间的 3 对上了,所以结果是 2×2,一共四个数,一个一个算:

C11 = A 第 1 行 ⋅ B 第 1 列 = 1×7 + 2×9 + 3×11 = 7 + 18 + 33 = 58
C12 = A 第 1 行 ⋅ B 第 2 列 = 1×8 + 2×10 + 3×12 = 8 + 20 + 36 = 64
C21 = A 第 2 行 ⋅ B 第 1 列 = 4×7 + 5×9 + 6×11 = 28 + 45 + 66 = 139
C22 = A 第 2 行 ⋅ B 第 2 列 = 4×8 + 5×10 + 6×12 = 32 + 50 + 72 = 154

结果是 [[58, 64], [139, 154]]。注意 B @ A 也算得出来(3×2 @ 2×3 → 3×3),但结果完全不是这个——矩阵乘法不满足交换律,左右顺序是有意义的。

打个比方:一张换算表

把右边那个 k×m 矩阵看成一张换算表:它有 k 个进项、m 个出项,表里第 t 行第 j 列写的是「第 t 个进项对第 j 个出项贡献多少」。左边每来一行数据(k 个进项的取值),过一遍这张表,就出来 m 个结果。一行数据过一次表,n 行数据过 n 次,这就是矩阵乘法。

类比失效处:真实的换算表是人手填的、每一格都能解释(比如「1 美元换 7.2 元」)。权重矩阵里那 k×m 个数是训练自动填出来的,单看某一格没有任何可解释的含义,只有整张表一起用才有意义。别指望能读懂其中一格。

现在到了这一章最重要的一句话,后面十九章的参数点钞全部建立在它上面:

一句话记住:一个 a×b 的权重矩阵,里面就有 a×b 个参数。数参数量,就是把模型里所有权重矩阵的行乘列加起来。

拿真数据验证一下。Qwen3.8-27B 的 hidden_size 是 5120,稠密 FFN 的中间维是 17408。FFN 里的 gate_proj 就是一个把 5120 维变成 17408 维的矩阵,所以它是 5120×17408:

5120 × 17408 = 89,128,960 个参数,约 8913 万。

这个数不是估的。本站的参数点钞脚本对同一张矩阵算出来的就是 89,128,960(见 paramcount.out.txtgate_proj 那一行)。一张矩阵、一次乘法,你已经能自己复算模型的一部分了。

再来一个更大的:嵌入表要把 248,320 个词各自映射成一串 5120 个数,所以它是 248320×5120 = 1,271,398,400,也就是 1.27B。光是一张查询表,就吃掉了 12.7 亿个参数。

我能不看材料说清:为什么 (5×3) @ (3×7) 合法而 (5×3) @ (5×3) 不合法,以及结果的形状是什么。

下面四个乘法,哪些合法?合法的写出结果形状。
(a) (5×3) @ (3×7) (b) (5×3) @ (5×3) (c) (1×5120) @ (5120×17408) (d) (17408×5120) @ (5120×1)

先想:只需要看两个形状「贴在一起」的那两个数字是不是一样。左边的列数要等于右边的行数。
关键在于把形状写成 (左行 × 左列) @ (右行 × 右列),画线的那两个必须相等;相等就消掉,剩下的两个数拼成结果形状。
第一步这样走:先做 (a)。左列是 3,右行是 3,相等,合法;消掉 3 之后剩 5 和 7,结果是 5×7。其余三个照此办理。
完整答案:(a) 合法,5×3 与 3×7 中间的 3 对上,结果 5×7。(b) 不合法,左列是 3、右行是 5,配不上。(c) 合法,中间是 5120,结果 1×17408——这正是一个 token 的 5120 维向量过一遍 gate_proj 之后的形状。(d) 合法,中间是 5120,结果 17408×1。留意 (c) 和 (d) 用的是同一张权重表的两种摆法,得到的结果形状互为转置,数值内容一样,所以工程上写成哪种都行,但同一份代码里必须从头到尾只用一种,否则某一层会突然对不上。

变式:把 (b) 里右边那个矩阵转置成 (3×5),它就合法了。结果形状是多少?转置这个动作有没有改变里面参数的个数?

Qwen3.8-27B 的稠密 FFN 有三张矩阵:gate_proj(5120→17408)、up_proj(5120→17408)、down_proj(17408→5120)。这一层 FFN 一共有多少参数?全模型 64 层都有一份,总共多少?

先想:每张矩阵的参数个数 = 行 × 列。三张加起来是一层,一层乘层数是全部。
关键在于注意到三张矩阵的行列虽然摆法不同(两张是 5120×17408,一张是 17408×5120),但行乘列的乘积是同一个数。
第一步这样走:先算 5120 × 17408 = 89,128,960,然后想清楚这三张是不是都等于这个数。
完整答案:三张都是 89,128,960,一层 FFN = 3 × 89,128,960 = 267,386,880,约 2.67 亿,与点钞脚本里「每个 FFN(稠密): 267.39 M」一致。64 层全都有 FFN(不管这一层的前半截是全注意力还是线性注意力),所以 64 × 267,386,880 = 17,112,760,320,约 171 亿。这一项就占了全模型 27.36B 的 62.6%——FFN 才是这台机器里最贵的东西,注意力反而便宜。这一条会在第 8 章展开。

变式:如果把中间维从 17408 改成 8704(砍一半),一层 FFN 的参数量变成多少?全模型少掉的参数占原来 27.36B 的百分之几?(这是一次真正意义上的「改尺寸」,注意它和量化不是一回事。)

0.3 「参数」到底是什么

不用它会怎样

不把这个词定下来,你没办法回答一个最基本的问题:「27B 是什么的 27B?」是 27 亿字的训练资料?是 27 GB 的文件?还是 27 亿个词?全都不是。它数的是一类很具体的东西——那些在训练时被反复微调、训练结束后就冻住的数。

参数parameter,也叫权重 weight):模型里那些训练时会被改动、推理时只读不改的数字。它们全部住在权重矩阵里。模型文件里存的就是它们,别无他物。

训练是这么回事:一开始所有矩阵里的数都是随机的,模型说什么都是胡话;喂给它一段文本让它猜下一个字,猜错了就算出「每一个参数应该往哪个方向挪一点点」,然后全体挪一小步。这一步叫梯度下降(gradient descent)。重复到天量次数之后,那些数就变成了能说人话的一组取值。

打个比方:一台有 274 亿个旋钮的调音台

录音棚的调音台上有一排排旋钮,每个控制一点点音色,转到合适的位置声音就对了。模型的参数就是旋钮,训练就是转旋钮的过程,模型文件就是把所有旋钮当前位置记下来的那张纸。

类比失效处有三处。其一,调音台的旋钮是人转的,参数是梯度下降按数学规则自动转的,没有人知道每个旋钮该在哪。其二,调音台的旋钮有标签(低音、混响),每个对应一个说得出口的功能;参数没有标签,也几乎不存在「这一个参数负责礼貌」这种对应,一个能力是几亿个参数一起摊出来的。其三是数量:调音台几十个旋钮,Qwen3.8-27B 有 273.6 亿 个。

273.6 亿有多大?如果你一秒念一个数,不吃不喝不睡,念完 27,355,639,808 个参数需要约 867 年。而这还只是两个模型里小的那个:Qwen3.8-2.4T-A95B 的总参数是 2.420 T,也就是 24,200 亿,念完要七万多年。

还有一个必须现在就分清的区别,否则第 1 章那张四轴表看不懂:

表 0-3:参数与超参数(数值取自 Qwen3.8-27B 的 config.json 与点钞脚本输出)
 超参数(hyperparameter)参数(parameter)
谁定的人,在训练开始之前写进 config.json梯度下降,在训练过程中
例子层数 64、hidden 5120、FFN 中间维 17408、词表 248320、专家数 512那 5120×17408 张表格里的每一个小数
数量级几十个,一个 JSON 文件装得下273.6 亿个,要 54.66 GB 才装得下
改动它会怎样房子的图纸变了,必须重新训练训练本来就在改;训练完之后改,就是微调或量化

这张表就是全站核心论点的第一块砖:27B 和 2.4T 的差别写在超参数里(层数 64 对 92、hidden 5120 对 8192、稠密 FFN 对 512 个专家),不在参数的存储格式里。第 1 章会把这句话摊开讲。

我能不看材料说清:为什么 hidden_size=5120 是超参数,而权重矩阵里的 5120×17408 个小数是参数。

下面六项,哪些是超参数,哪些是参数,哪些两者都不是?
(a) num_hidden_layers = 64 (b) gate_proj 里第 100 行第 200 列的那个数 (c) 训练用了多少万亿个 token (d) num_experts = 512 (e) 嵌入表里「猫」那一行的 5120 个数 (f) 模型文件用 BF16 还是 Q4_K_M 保存

先想:三个判据——是人事先写在配置里的吗?是梯度下降在训练中改出来的吗?还是训练结束之后才发生的事?
关键在于 (f):保存格式既不是训练前定的结构,也不是训练中学出来的数值,它发生在训练全部结束之后,属于第三类。
第一步这样走:先把明显住在矩阵里的挑出来(b、e),它们一定是参数;再把写在 config.json 里的挑出来(a、d)。
完整答案:超参数是 (a) 和 (d)——人在训练前写死的结构选择。参数是 (b) 和 (e)——权重矩阵和嵌入表里的具体数值,由梯度下降学出来。(c) 两者都不是,它是训练配方的一部分(训练预算),既不进模型文件,也不决定矩阵形状。(f) 两者都不是,它是训练之后对同一批参数换一种记法,参数一个没少——这正是全站要澄清的那件事,第 15 章会正面讲。

变式:有人把 partial_rotary_factor = 0.25 说成「模型学出来的」。他错在哪?(提示:去看它写在哪个文件里。)

0.4 浮点数:一个数字在电脑里占几个字节

不用它会怎样

同一个 Qwen3.8-27B,有的文件 54.66 GB,有的 17.11 GB,有的 9.01 GB,参数个数一模一样都是 273.6 亿。不搞清楚「一个数字占几个字节」,这三个数字就永远只能背下来,不能理解——而这正是全网关于量化的科普讲不清楚的地方。这一节是后面第 15、16、17 三章的全部地基。

先说整数。计算机里一切都是 0 和 1,一个 0 或 1 叫一(bit),8 位叫一字节(byte)n 位能表示 2n 种取值:8 位 256 种,4 位只有 16 种。INT8 就是用 8 位存一个整数(−128 到 127),INT4 用 4 位(−8 到 7,只有十六个台阶)。

但模型里的参数是小数,而且大小差得很远——有的是 0.0001,有的是 300。要用固定位数同时表示这两种数,办法是把科学计数法搬进二进制。浮点数(floating point number) 把可用的位切成三段:

x = (−1)s × (1 + f) × 2(eb)
式 0-4
符号是什么直觉
s符号位(sign),永远占 1 位,0 表示正、1 表示负正还是负
e指数位(exponent)存的整数,位数越多能表示的数量级跨度越大小数点往左还是往右挪几位——决定动态范围
b指数偏置,一个固定常数(FP32 与 BF16 是 127,FP16 是 15),用来让 e 也能表示负指数把指数的零点搬到中间
f尾数位(mantissa / significand)表示的小数部分,取值在 0 到 1 之间有效数字有几位——决定精度

把这一句话记牢,后面全部推得动:指数位管范围,尾数位管精度,两者在固定的总位数里抢地盘。

表 0-4:常见数值格式对照(位宽划分见共同口径表;上限与精度按 IEEE 754 与 OCP OFP8 规范)
格式符号指数尾数共几位每个数占大致最大值典型用途
FP321823324 字节约 3.4×1038老式训练、对精度最敏感的中间计算
BF16187162 字节约 3.4×1038现代训练与权重发布的默认格式
FP161510162 字节约 6.55×104推理、KV cache
FP8-E4M314381 字节约 448权重与激活的 8 位量化
FP8-E5M215281 字节约 5.7×104梯度这类范围更宽的量
INT8整数,没有指数尾数之分81 字节127需要配缩放因子的定点量化
INT4整数,只有 16 个台阶40.5 字节74 位量化的基础单元
读的时候要小心

表里的位宽划分来自本站锁定的口径表;「大致最大值」那一列不在口径表里,是 IEEE 754 与 OCP OFP8 v1.0 规范中的公开数值,写在这里只为给你量级概念,要做工程判断请回查规范原文。另外,INT8 与 INT4 单独一个数没法表示小数,实用的量化方案总要额外存一个缩放因子——这正是「4 位量化的文件为什么不是理论值的一半」的直接原因,第 15 章会算清楚。

为什么训练偏爱 BF16 而不是 FP16?两者都是 2 字节,区别只在地盘怎么分。FP16 把 10 位给尾数、只留 5 位给指数,能表示的最大数只有 65504 上下;训练时梯度偶尔会窜到很大,一旦越界就变成无穷大,那一步的更新全废,整个训练可能就崩了。BF16 反过来,指数位保持 8 位不变(和 FP32 一模一样),砍的全砍在尾数上(23 位砍到 7 位)。于是它的动态范围与 FP32 完全相同、不会溢出,代价是有效数字变少、单次读写有零点几个百分点的误差;而训练本来就是天量次小步累加,对这点误差不敏感,对「这一步直接变成无穷大」却极其敏感。

一句话:BF16 是宁可丢精度也不丢范围。这也解释了 FP32 转 BF16 在硬件上为什么特别便宜——把 FP32 的后 16 位截掉就是 BF16,指数段原封不动。

打个比方:两把尺子

FP32 是一把 4 米长、带毫米刻度的卷尺,什么都能量。BF16 是同样 4 米长、但只有厘米刻度的尺子——长的东西照样量得了,只是读数糙一点。FP16 是一把只有 1 米长、却有毫米刻度的直尺——量小东西比 BF16 准,但碰上 2 米的东西直接量不了,这就是溢出。

类比失效处:真尺子的刻度是均匀的,每一厘米都一样宽。浮点数不是——它的刻度靠近 0 的地方密、远离 0 的地方疏,因为尾数位表示的是相对精度。所以 0.001 和 0.001001 之间浮点数能分得清,而 1000000 和 1000001 之间可能就分不清了。这个性质是后面所有量化误差分析的起点。

现在把这一节和 0.2 节接起来,得到本章最实用的一条换算:

27.36B 个参数 × 每个 2 字节(BF16)= 54.7 GB。这就是为什么 Qwen3.8-27B 的 BF16 权重文件是 54.66 GB——不是别人随口标的一个数,是参数个数乘位宽算出来的。换成 4 位量化,同样这 273.6 亿个参数只需要约四分之一的空间,实测的 Q4_K_M 文件是 17.11 GB

一句话记住:量化改的是「每个参数占几位」,不是「有多少个参数」。54.66 GB 和 17.11 GB 这两个文件里,参数个数一样多,都是 273.6 亿个。

Qwen3.8-2.4T-A95B 的总参数是 2.420 T。如果全部用 BF16 保存,权重大约要占多少 TiB?(提示:1 TiB = 10244 字节 ≈ 1.0995×1012 字节。)

先想:字节数 = 参数个数 × 每个参数的字节数。BF16 每个参数几个字节,表 0-4 里有。
关键在于最后一步的单位换算:算出来的是字节,题目要的是 TiB,而 TiB 是 1024 的四次方而不是 10 的十二次方,两者差约 10%。
第一步这样走:2.420×1012 × 2 = 4.84×1012 字节。再除以 1.0995×1012
完整答案:2.420×1012 个参数 × 2 字节 = 4.84×1012 字节;4.84×1012 ÷ 1.0995×10124.40 TiB,与本站口径表锁定的「BF16 权重 4.40 TiB」一致。顺带感受一下量级:单张 141 GB 的加速卡要 32 张才装得下这一堆权重,而这还没算 KV cache 和框架开销。如果你算成了 4.84 TB 就说明忘了 TiB 与 TB 的区别,这个 10% 在这种规模上就是三张卡。

变式:官方还发布了 FP8 版本。同样这 2.420 T 参数用 FP8 存,理论上占多少 TiB?为什么真实文件通常会比你算的略大一点?(想一想缩放因子存在哪里。)

有人说:「BF16 和 FP16 都是 16 位,能表示的数一样多,随便换。」请构造一个具体的数,把这句话驳倒;再构造一个反方向的例子,说明 FP16 也不是全面吃亏。

先想:两者的位数确实一样,但地盘分法不同。查表 0-4,看它们的指数位分别是几位、最大值分别是多少。
关键在于「能表示的数一样多」这句话其实是对的(16 位就是 65536 种编码),但「能表示的数覆盖的范围一样」是错的。要驳倒它,只需要找一个落在一方范围内、另一方范围外的数。
第一步这样走:FP16 的最大值约 65504。随便挑一个比它大的数,比如 100000,看 BF16 能不能存。反方向则要找两个非常接近的小数,看谁能分得开。
完整答案:正方向的反例——取 x = 100000。BF16 的指数位有 8 位,上限约 3.4×1038,存得下;FP16 上限约 65504,存不下,会变成无穷大。训练时的梯度或注意力分数的中间量偶尔就会窜到这个量级,这就是训练选 BF16 的直接原因。反方向的例子——取 x = 1.001。FP16 有 10 位尾数,能分辨约千分之一的相对差异,1.001 和 1.000 存进去仍是两个不同的数;BF16 只有 7 位尾数,相对分辨力约为百分之一,1.001 存进去很可能就退化成 1.0。所以两句话都要说全:范围上 BF16 完胜,精度上 FP16 更细,16 位就这么多,只能二选一。

变式:FP8 有 E4M3 和 E5M2 两种切法。按同样的推理,哪一种更适合存权重(数值集中、范围窄),哪一种更适合存梯度(偶尔出现极大值)?说出你的判据。

0.5 对数与数量级:为什么讲模型总在说 B 和 T

不用它会怎样

27B 和 2.4T 摆在一起,直觉会说「差一点」,因为两个数字看起来都是两位数。实际上后者是前者的 88 倍。不建立数量级的感觉,你对这台机器的一切判断都会系统性地偏小。

英文的计量前缀是三位一跳:K 是 103(千),M 是 106(百万),B 是 109(十亿),T 是 1012(万亿)。中文是四位一跳:万、亿、万亿。两套系统错开一位,是中文读者读这类文章最常出错的地方。

表 0-5:数量级对照与本站会反复用到的几个数(数值取自本站口径表与点钞脚本)
写法10 的几次方中文读法本站里的实例
1 K103一千上下文 32K = 32768 个 token
1 M106一百万一层 FFN 里单张矩阵约 89 M 个参数
1 B109十亿嵌入表 1.27 B;全模型 27.36 B = 273.6 亿
1 T1012一万亿2.4T-A95B 总参 2.420 T = 24200 亿

拿两个锁定值直接相除,就能看清差距:2.420 T ÷ 27.36 B ≈ 88 倍;而它每个 token 真正用上的激活参数 95.29 B ÷ 27.36 B ≈ 3.5 倍。同一对模型,看显存差 88 倍,看单 token 计算量只差三倍半——这个反差就是第 10 章的全部内容。

对数(logarithm) 是这样一个问题的答案:10 的几次方等于这个数?log10(1000) = 3,log10(27.36×109) ≈ 10.44,log10(2.420×1012) ≈ 12.38。两者相差 1.94,也就是差了将近两个数量级,正好对应 101.94 ≈ 88 倍。在对数尺度上,「相差多少倍」变成了「相差多少」——这就是它的全部用处。

这也解释了为什么讲缩放规律的论文全都画对数坐标图。经验规律大多长成 y = a·xk 这种幂律(power law)形式;两边取对数就变成 log y = log a + k log x,是一条直线,斜率就是 k。于是跨五个数量级的实验点能画在一张图里,而且能顺着直线往外推。第 13 章讲 Scaling Law 时,你看到的每一张图都是这么来的。

补充:GB 和 GiB 不是一回事

GB 是 109 字节(十进制),GiB 是 10243 = 1,073,741,824 字节(二进制),两者差约 7.4%。本站的文件体积一律用 GB(模型仓库标的就是 GB:54.66 GB、17.11 GB),KV cache 一律用 GiB(显存与框架报的是 GiB:16.00 GiB、8.00 GiB)。两者进同一个减法之前必须先换算——在 24 GB 这种紧巴巴的预算上,7.4% 就是一两个 GB,够不够放 KV cache 全看它。第 17 章会再提醒你一次。

一篇报道写道:「Qwen3.8 最大的开源版本有 2.4 万亿参数,比 27 亿参数的小版本大了一千倍。」这句话里有两处数字问题,指出来并改对。

先想:27B 用中文该读作多少亿?还有,2.4 万亿除以那个正确的数,到底是多少倍?
关键在于 B = 十亿,不是亿。中英前缀错开一位,是这类报道最常见的翻车点。
第一步这样走:先把 27B 写成 27×109,再翻成中文的「亿」——除以 108
完整答案:第一处,27B = 27×109 = 270 亿,不是 27 亿;按本站复算的精确值是 27.36B,即 273.6 亿。第二处,2.420×1012 ÷ 27.36×10988 倍,不是一千倍——写成一千倍等于把倍数又放大了十倍以上。正确的说法是:2.42 万亿参数,约为 273.6 亿参数版本的 88 倍。顺带一提,报道里还漏了一件更重要的事:2.4T 是参数,它每个 token 只激活 95.29B,这一点第 10 章会讲。

变式:另一篇写「27B 模型量化到 4 位后只剩 68 亿参数」。这句话错在哪一层?(提示:它把哪两个量混成了一个?)

0.6 softmax:把任意一堆数变成一组概率

不用它会怎样

模型最后吐出来的是 248320 个实数,一个词一个,有正有负,加起来也不是 1。这些数叫 logits,它们只表达「相对偏好」,不是概率。要回答「下一个字是『的』的可能性有多大」,必须先把这堆数变成一组非负、加起来等于 1 的数。注意力层要决定「看哪个词看多重」、MoE 路由要决定「把这个 token 发给哪几个专家」,用的是同一个动作。这个动作叫 softmax,全站会遇到它三次。

最朴素的办法是每个数除以总和。行不通:有负数时会出现负的「概率」;如果正负正好抵消,分母还会是 0。softmax 的办法是先给每个数取一次指数 ez,再除以总和:

pi = softmax(z)i = ezij=1n ezj
式 0-5
符号是什么直觉
z输入的一串实数(logits),可正可负,个数是 n模型给每个候选打的原始分
zii 个候选的原始分这一项的原始偏好
ez自然指数,e ≈ 2.71828。它把任意实数变成正数,且保持大小顺序先拉成正数,顺便把差距放大
分母所有 ezj 之和归一化,保证结果加起来等于 1
pi输出的概率,每个都在 0 和 1 之间,全部相加等于 1可以拿去抽签的一组权重

取指数这一步干了三件事,缺一不可:一是把负数变正(e 的任何次方都大于 0,负概率的问题消失);二是保序(原来大的仍然大,排名不会被打乱);三是放大差距(原始分差 1,指数之后就差 e ≈ 2.72 倍,模型的选择因此变得果断)。

手算一个三项的例子。取 z = (2, 1, 0):

e2 ≈ 7.389,e1 ≈ 2.718,e0 = 1,三者之和 ≈ 11.107。
p1 = 7.389 ÷ 11.107 ≈ 0.665p2 = 2.718 ÷ 11.107 ≈ 0.245p3 = 1 ÷ 11.107 ≈ 0.090
三者相加 = 1.000。原始分只差 1 和 2,概率却拉开到 0.665 对 0.090,七倍多。

还有一条工程上极其重要的性质:softmax 对整体平移不变——每一个 z 都减去同一个常数,结果一模一样。验证:取 z = (0, −1, −2)(每项减 2),e0 = 1、e−1 ≈ 0.368、e−2 ≈ 0.135,和 ≈ 1.503,得到 0.665、0.245、0.090,与刚才一字不差。真实实现都靠这条性质先减去最大值再取指数:不减的话,z 稍大一点(比如 800),e800 直接溢出成无穷大,整个分布报废。

打个比方:把评委的原始分变成得票占比

三位选手拿到的原始分是 2、1、0,你要把它变成「如果现在抽签决定冠军,各自的中签率」。直接按分数比例分不行(分数可以是负的),于是先让每个人的分数走一遍指数放大器,再按放大后的数值分配比例。分高的人拿走的比例远超过分差本身。

类比失效处:真实比赛里冠军只有一个,选完就定了;softmax 出来的是一组权重,在注意力层里它不是用来选一个,而是用来把所有候选按比例混合——每个词都会被看到一点,只是看的程度不同。到了 MoE 路由那一章,它才真的被用来做「只选前 10 个」的挑选动作。同一个 softmax,两种用法,别混。

我能不看材料说清:为什么不能直接把 logits 除以它们的总和,以及取指数解决了哪几个问题。

判断这句话对不对,并给出理由或反例:「softmax 之后,最大的那个概率一定超过 0.5。」

先想:要推翻一个「一定」,只需要举一个反例。什么样的一组输入会让 softmax 出来的概率特别平均?
关键在于候选的个数。当所有 z 相等时,softmax 会把概率均分——那么均分的结果是多少?和 0.5 比呢?
第一步这样走:取 z = (0, 0, 0),三项相等。分子都是 e0 = 1,分母是 3,算出每一项的概率。
完整答案:这句话。反例:z = (0, 0, 0),三项都得到 1 ÷ 3 ≈ 0.333,最大的那个只有 0.333,远不到 0.5。更极端的反例:模型输出层有 248320 个候选,如果它们的 logits 都差不多,最大的那个概率只有约 1/248320 ≈ 0.000004。真正成立的性质有两条:一是所有概率相加恒等于 1,二是最大的那个概率不小于 1/nn 是候选个数)。「超过 0.5」需要额外条件——最大的 logit 比第二大的领先足够多,而且候选总数不能太多。这一点在第 3 章读注意力权重时会用到:一个 token 的注意力分布很平,不代表模型出错了,可能只是可参考的位置本来就多。

变式:把 z = (2, 1, 0) 里的每一项都乘以 10,变成 (20, 10, 0),再做 softmax。最大概率会更接近 1 还是更接近 0.333?这个「先乘一个系数再 softmax」的操作,在采样时叫什么?

下面这段话看起来很专业,但把本章的三件事全混了。逐条指出来:
「Qwen3.8-27B 的 BF16 版本 54.66 GB,Q4_K_M 版本 17.11 GB,说明量化把参数量从 273.6 亿压到了大约 85 亿。既然 54.66 除以 4 应该是 13.7,那 17.11 GB 说明这个 4 位量化其实是缩水的。」

先想:三个可疑点。第一句里「文件小了所以参数少了」这个推理成不成立?第二句里 13.7 这个理论值是怎么算的、它假设了什么?
关键在于把「参数个数」和「每个参数占几位」彻底拆开。文件体积 = 参数个数 × 每参数位宽 ÷ 8,两个因子里只有一个变了。至于 13.7 那个理论值,它默认了「4 位量化 = 每一个参数都恰好 4 位」。
第一步这样走:用 0.4 节那条换算反算一遍。273.6 亿 × 0.5 字节 = 多少 GB?把这个数和 17.11 摆在一起,再问:多出来的部分可能是什么东西占的?
完整答案:三处问题。其一,参数量没有变。两个文件里都是 27,355,639,808 个参数,量化只改了每个参数的记法(2 字节变成约 0.5 字节),这是全站的核心区分,第 15 章会正面讲。其二,「85 亿」这个数是把体积比当成了参数比,等于用尺子的刻度粗细去推测房子的大小,两个量根本不在一个维度上。其三,13.7 GB 这个理论下界算得没错(27.36×109 × 0.5 字节 ≈ 13.7 GB),但它假设每个参数都恰好 4 位。真实的 Q4_K_M 不是这样:一部分张量用了更高位宽保留精度,而且每一小组数还要额外存一个缩放因子,这两项加起来就是 13.7 到 17.11 之间那 3.4 GB 的来源。所以 17.11 GB 不是「缩水」,恰恰是「没敢一刀切」的证据。第 16 章会把这几 GB 拆开。

变式:按同样的推理,UD-IQ2_XXS 版本只有 9.01 GB。请算出它平均每个参数占多少位,并说明这个平均值为什么不可能等于 2。

自己推一遍:一个 a×b 的矩阵,到底占多少字节

这是全站被引用次数最多的一条推导。它把 0.2 节的形状、0.3 节的参数、0.4 节的位宽接成一根链子。每一步先自己想,再展开。

  1. 已知一个 a×b 的权重矩阵。第一个要问的问题是什么?

    想好了再看

    先问「它里面有多少个数」,而不是先问字节。这两件事必须分开问,因为它们由完全不同的东西决定:个数由形状决定,字节由存储格式决定。全网关于量化的糊涂话,十有八九就是把这两个问题合成了一个。答案是 a × b 个数——每一行每一列的交叉点上都有且只有一个数。

  2. 第二个问题:每个数占几个字节?这个答案写在哪里?

    想好了再看

    写在存储格式里,不在模型结构里。查表 0-4:FP32 是 4 字节,BF16 和 FP16 是 2 字节,FP8 和 INT8 是 1 字节,INT4 是 0.5 字节。注意这个数完全独立于 ab——同一张矩阵可以用任何一种格式存,形状一个都不变。这就是「量化发生在训练之后」这句话在算术上的样子。

  3. 把两步合起来,写出公式。

    想好了再看
    字节数 = a × b × 位宽8
    式 0-6
    符号是什么直觉
    ab矩阵的行数与列数,由 config.json 里的超参数决定房子有多大——训练之前就定了
    位宽每个数用几个二进制位存,由存储格式决定用多细的尺子记坐标——训练之后才定
    ÷ 8位换算成字节8 位等于 1 字节
  4. 代进一张真矩阵:Gated Attention 的 q_proj,形状是 5120 ×(24 头 × 每头 256 维 × 2)= 5120 × 12288。它有多少参数?BF16 下占多少?

    想好了再看

    5120 × 12288 = 62,914,560 个参数,约 6291 万,与点钞脚本里 q_proj(含门) 那一行完全一致。BF16 下 62,914,560 × 2 = 125,829,120 字节 ≈ 125.8 MB。为什么列数是 24×256 再乘 2,而不是 24×256?因为 Qwen3.8 的 attn_output_gate 是 true,这张矩阵的输出有一半不是查询向量而是一道门——这一点第 4 章会讲,现在只需要知道:形状是从 config.json 读出来的,不是猜的。

  5. 推到整台机器:27.36B 参数在 BF16 下占多少?在 INT4 下呢?

    想好了再看

    BF16:27.36×109 × 2 字节 ≈ 54.7 GB,与实测的 BF16 权重文件 54.66 GB 对得上。INT4:27.36×109 × 0.5 字节 ≈ 13.7 GB

  6. 最后一问,也是这条推导真正的价值所在:实测的 Q4_K_M 文件是 17.11 GB,比你算的 13.7 GB 大了 3.4 GB。是公式错了吗?

    想好了再看

    公式没错,是「位宽」那一项被当成了常数。真实的 4 位量化方案不会把每一个参数都压到 4 位:一部分对精度敏感的张量会用更高的位宽保留,而且量化必须按小组存一个缩放因子(否则 16 个台阶根本覆盖不了权重的取值范围),这个缩放因子本身也要占位。于是「平均每参数位宽」就不是 4,而是 4 点几到 5 之间。用 17.11 GB 反推:17.11×109 × 8 ÷ 27.36×1095.0 位——这就是这个文件真实的平均位宽。本站推导,非官方数字。第 15、16 章会把这 3.4 GB 逐项拆开。到那时你会发现,这条你已经推过一遍的公式,就是理解全部量化格式的唯一入口。

答辩:如果我是审稿人(一)

你说「向量之间的余弦相似度可以衡量两个词意思有多接近」。可这 5120 个维度是训练自己长出来的,没有任何一维带语义标签,也没有人证明过这个空间里的角度就等于语义距离。那么「相似度高等于意思接近」这句话,究竟是被证明的,还是被假设的?

参考防守(先自己组织语言再看)

经验成立,不是被证明。诚实的表述应该是:训练目标(预测下一个词)会把在相似语境里出现的词推到相似方向上,这是训练过程的副产品,不是设计时的保证。它成立到什么程度是可测的(取一批人工标注的近义词对,看余弦相似度排序是否吻合),也确实有已知的失效模式:高频词和低频词的向量长度分布明显不同、整个空间常常挤在一个锥形区域里而不是均匀铺开,导致任意两个词的余弦相似度普遍偏高。所以这一章给的是工具——一把能算的尺子——不是保证。审稿人的追问站得住,正确的做法是在用它下结论时把这层不确定性说出来,而不是把「向量近似于意思」当成公理。本站在第 2 章讲嵌入时会回到这个问题。

答辩:如果我是审稿人(二)

你把参数量定义成「所有权重矩阵的行乘列之和」,还拿它复算出 27.36B 去对官方的 27B。可这个定义漏掉了东西:归一化层的缩放向量、因果卷积核之外的各种小张量、还有 config 里明写着的 mtp_num_hidden_layers = 1 那个多 token 预测头。漏掉这些还敢说「复现了官方数字」,是不是太宽松了?

参考防守(先自己组织语言再看)

这个追问一半站得住,必须分两段回答。第一段,量级可忽略的部分:归一化层每层大约是 hidden 那么多个数,5120 个;64 层加起来不到一百万个,相对 273.6 亿是 10-5 的量级,改不了任何一个结论,也改不了小数点后两位。这类项省略是合理的,但省略这件事本身应该说出来——本站在这里说了。第二段,真正的缺口:MTP 头确实没数,它至少是一整层的量级(上亿个参数),这是一个真实的、有量级的缺口,不能拿「可忽略」搪塞。这也正好解释了为什么各家标的数字不一样:本站复算 27.36B,Ollama 页面标 27.3B,官方名叫 27B——差值来自各家算不算视觉塔、算不算 MTP 头。所以更准确的说法不是「复现了官方数字」,而是「用一条公开可复算的路径落在了同一量级,并且能说清剩下的差从哪来」。第 12 章会把这三种口径并排摆出来,那一章的价值恰恰在于承认口径差异,而不是抹平它。

对你而言未知同样的模型换一种数值格式,到底掉多少能力?

本章只讲清了「换格式会改变体积」这一半。另一半——换格式会不会、以及会掉多少能力——这一章一个字都没给,因为它需要实证数据,不是靠推导能得出来的。这个问题在学界有答案,只是不在这份材料里:有一支专门的文献在同一显存预算下把「大模型低位宽」和「小模型高位宽」正面对撞,得到的结论并不像直觉那样简单。

先做这一步:去读 arXiv:2212.09720(k-bit Inference Scaling Laws)的摘要,找到那句「一个 30B 的 8 位模型和一个 60B 的 4 位模型占用相同的比特数,但零样本准确率可能相差很远」。然后用本章式 0-6 亲手验证前半句——算一算 30×109×1 字节 与 60×109×0.5 字节 是不是真的相等。验证完再问自己一个问题:既然比特数相同,那让准确率产生差别的到底是什么?把你的猜测写下来,第 15 章开头会回来对答案。

本章小结

六件事,一条链:

  • 向量让「相似」变成可计算的量——点积对位相乘再相加,余弦相似度再除掉长度只留方向。Qwen3.8-27B 用 5120 个数表示一个词。
  • 矩阵乘法是这台机器唯一的动作,规则是 (n×k) @ (k×m) → (n×m),中间那个 k 必须相等。一个 a×b 的矩阵有 a×b 个参数——这是后面所有点钞的地基。
  • 参数是梯度下降调出来、训练完就冻住的数,共 273.6 亿个;超参数是人写在 config.json 里的结构选择。27B 和 2.4T 的差别写在超参数里。
  • 浮点数把位切成符号、指数、尾数三段:指数管范围,尾数管精度。BF16 与 FP32 指数位同宽,宁可丢精度也不丢范围,所以训练用它。
  • 数量级:B 是十亿、T 是万亿,中英前缀错开一位;2.42T 是 27.36B 的 88 倍。GB 与 GiB 差 7.4%,减法之前必须换算。
  • softmax 先取指数再归一化,把任意实数变成一组和为 1 的概率;它对整体平移不变,所以实现时先减最大值。

核心公式只有一条,请把它抄在手边:字节数 = 行 × 列 × 位宽 ÷ 8。行和列由训练前的图纸决定,位宽由训练后的存储格式决定。这两件事不在同一条时间轴上——下一章就从这句话开始。

第1章 全景:把 Qwen3.8 当成一台机器

这一章回答三件事:这次到底开源了什么、不同尺寸是不是量化出来的、以及在往下走之前,你该用什么标准去信这份材料里的每一个数字。

学完这一章你应该能做到

  • 说出这次官方一共放了几个仓库,并分别说清两个模型的许可证差别
  • 用四轴表把「参数规模 / 稠密还是稀疏 / 层布局 / 数值精度」拆开,指出哪三条发生在训练之前、哪一条在训练之后
  • 识破「小模型是大模型量化出来的」这句话混淆了哪两件事
  • 画出一个 token 从文字变成下一个 token 的完整路径,说出它经过哪几类零件
  • 把一句关于 Qwen3.8 的陈述归到四档证据里的某一档,并说出你的判据
  • 写出一个只会原样吐回去的空壳模型,并用形状自检它有没有搭对
前置:第0章的「参数与超参数的区别」「字节数 = 行 × 列 × 位宽 ÷ 8」。如果你还说不清「量化改的是哪一项」,先回去把 0.3 和 0.4 两节读完。

1.1 这次到底开源了什么

为什么先花一节讲这个

因为网上关于这次发布的说法互相矛盾,而矛盾的根源大多不是技术理解,是没搞清「官方到底放了哪几个东西」。仓库清单是这一整个站唯一的事实起点:它可以点开、可以核对、随时可以推翻本站。先把它摆平,后面十八章才有地基。

表 1-1:官方 Qwen3.8 仓库清单(本站核对的 Hugging Face 仓库,共 4 个)
仓库模态许可证建仓日
Qwen3.8-27B图文输入、文本输出(原生多模态)Apache 2.02026-08-05
Qwen3.8-27B-FP8同上Apache 2.02026-08-13
Qwen3.8-2.4T-A95B纯文本qwen3.8-max 自定义协议2026-08-08
Qwen3.8-2.4T-A95B-FP8纯文本同上2026-08-08

四个仓库,其实是两个模型各带一份官方 FP8 版本。权重上线的时间是 2.4T-A95B 在 8 月 12 日、27B 在 8 月 14 日——注意它和上表的建仓日不是一回事,仓库可以先建好占位、过几天再推权重。这个细微差别本身就值得记住:同一件事有多个时间戳时,先问清你看的是哪一个

三条必须主动说出来的事实,因为它们全都是网上高频的误传:

  • 没有官方小尺寸。4B、8B、32B 这些名字在官方仓库里一个都不存在。社区里流传的小尺寸版本是第三方做的蒸馏或再训练,不是官方发布。
  • 没有 Instruct / Thinking / Base 三件套。Qwen3.8 是单一统一权重,思考模式默认开启,深度靠 reasoning_effort 这个开关调(可选 xhigh / medium / low)。这是相对上一代把 Instruct 和 Thinking 分开发布的明确路线变化。
  • 没有官方 GitHub 仓库,也没有 arXiv 技术报告。模型卡就是唯一的权威文档。这一点对本站影响巨大——它意味着这个站不能只靠「读论文」,而必须靠「模型卡的 config.json(一手事实)+ 上游论文(原理)+ 推理框架文档(工程数字)」三条腿站着。第 18 章会正面谈这件事。
常见误解:Qwen3.8 全部 Apache 2.0 开源

这句话是事实错误,而且会误导读者的商用合规判断。正确的说法必须分开:Qwen3.8-27B 是 Apache 2.0,真开源,免费商用;Qwen3.8-2.4T-A95B 用的是名为 qwen3.8-max 的自定义协议(它是 Qwen3.8-Max 的开放权重版本),协议里至少有两条限制:其一,如果你经营模型即服务或 AI 助手类业务、且连续 12 个月收入超过 5000 万美元,商用前须另行获得授权;其二,用在月活超过 1 亿的产品上时,须在界面显著位置展示模型名称。

所以本站的用词纪律是:27B 可以说「开源」,2.4T-A95B 一律说「开放权重」。这不是咬文嚼字——一篇被广泛转载的英文报道就是把两个模型混为一谈、写成都是 Apache 2.0 的,而后续的中文转述又照抄了它。这条误传的传播路径,本身就是 1.5 节的最佳教材。

下面五句话里,哪几句是事实错误?
(a) 官方这次一共放了 4 个仓库。 (b) Qwen3.8-27B 是 Apache 2.0。 (c) Qwen3.8-2.4T-A95B 也是 Apache 2.0。 (d) 官方同时发布了 Instruct 版和 Thinking 版。 (e) 想读技术细节可以去看官方的 arXiv 技术报告。

先想:这五句里有三句可以直接用表 1-1 核对,另外两句要靠 1.1 节列的那三条澄清。
关键在于把「这次发布有什么」和「上一代发布有什么」分开——(d) 描述的其实是上一代的做法。
第一步这样走:先在表 1-1 里数仓库数量、逐行看许可证那一列,(a)(b)(c) 立刻有答案。
完整答案:错的是 (c)(d)(e) 三句。(c) 错在 2.4T-A95B 用的是 qwen3.8-max 自定义协议,不是 Apache 2.0,本站一律称它为「开放权重」而非「开源」。(d) 错在这一代是单一统一权重,思考深度靠 reasoning_effort 调,没有分开的 Instruct / Thinking / Base 变体。(e) 错在官方没有 arXiv 技术报告,也没有官方 GitHub 仓库,模型卡是唯一权威文档。(a)(b) 正确。

变式:如果你在一篇报道里读到「Qwen3.8 开源了 0.6B 到 2.4T 的全系列」,你会用表 1-1 里的哪一条去反驳?这个反驳是可复核的还是只是你的判断?

1.2 全站的核心问题:不同尺寸是量化出来的吗

为什么这一节是全站的锚

因为这是绝大多数人第一次接触大模型时冒出来的那个问题:既然有 27B 也有 2.4T,那小的是不是把大的压缩了一下?如果这个问题没被彻底讲清,后面每一章的数字都会被读成另一个意思——你会以为量化在改架构,会以为参数量是可以事后调的,会算错显存。所以本站把它放在最前面,并且后面每一章都会回指这一节

常见误解:小模型是大模型量化压缩出来的

很多人以为 27B 是把 2.4T 压小得到的,就像把一张大图导出成小图。不是。这两个模型是各自独立预训练出来的,从第一步随机初始化开始就是两套完全不同的参数,谁也不是谁的副本。它们之间的差异,写在训练开始之前的图纸上。

把差异拆开看,一共有四条互相独立的轴。这张表是全站最重要的一张:

表 1-2:两个模型差在哪里的四条轴(数值取自本站口径表与 config.json 复算)
Qwen3.8-27BQwen3.8-2.4T-A95B何时发生
① 参数规模27B(复算 27.36B)2.4T 总参 / 95B 激活训练之前(决定要建多大的房子)
② 稠密还是稀疏稠密 FFN,中间维 17408MoE,512 个专家每 token 选 10 个 + 1 个共享训练之前(图纸就不一样)
③ 注意力布局64 层 = 16 × (3 线性 + 1 全)92 层 = 23 × (3 线性 + 1 全)训练之前(同一张图纸,重复次数不同)
④ 数值精度(量化)BF16 / FP8 / NVFP4 / Q4_K_M 等同左训练之后(决定用多细的尺子记坐标)
一句话记住:尺寸差异发生在训练之前,量化发生在训练之后——这是两件事,甚至不在同一条时间轴上。

请特别留意第 ③ 行:两个模型的层布局是同一个模板,都是「3 层线性注意力配 1 层全注意力」,区别只在这个模板重复了 16 次还是 23 次。这不是巧合,而是这一代架构的设计方式——同一张图纸按需要重复不同的次数。第 7 章会把这个 3:1 布局拆开讲。

打个比方:同一套标准层,盖 16 层还是 23 层

把这两个模型想成两栋楼。标准层的图纸是同一份(三个开间加一个特殊开间),一栋重复 16 遍,另一栋重复 23 遍,而且后者每一层更宽(hidden 8192 对 5120)、把大开间隔成了 512 个小隔间(MoE 对稠密 FFN)。至于量化,它是楼盖完之后换一份精度更低的施工图去记录这栋楼——楼一点没动,只是记录它的那张纸变薄了。

类比失效处:真实的楼可以先盖 16 层、以后再加盖 7 层;模型不行。层数是训练开始前就写死的超参数,中途加层等于换了图纸,之前学到的参数无法直接沿用,必须重训。所以「先出小的、再往上加成大的」这种想象在这里不成立——两栋楼是同时开工、各盖各的。

再看第 ④ 行:它和上面三行的差别,不在于「改得多还是少」,而在于发生在哪个时刻。用第 0 章那条公式说最清楚:字节数 = 行 × 列 × 位宽 ÷ 8。前三轴动的是「行」和「列」,动完必须重新训练;第四轴只动「位宽」,参数个数一个不变,形状一个不变。

训练之前 · 写图纸 层数 · hidden · 稠密还是 MoE · 词表 训练之中 · 各训各的 两个模型独立预训练 训练之后 · 权重已经固定 剪枝 · 蒸馏 · 量化 训练完成 时间 轴 ① ② ③ 在这里被定死 轴 ④ 量化在这里发生
图 1-1:四条轴在时间轴上的位置。示意图,非官方原始图示。注意最右侧那一格里挤着三件不同的事,下面一段会把它们分开。

「训练之后」这一格里其实挤着三件完全不同的事,绝不能混:

  • 剪枝(pruning):真的把已训练模型的一部分权重砍掉,参数量真的变少,之后通常还要再训一小段来补。这是一条真实存在的技术路线(代表工作是 Minitron,arXiv:2407.14679),但它不是 Qwen3.8 这两个模型之间的关系。
  • 蒸馏(distillation):让一个大模型当老师,把它的输出分布教给一个学生模型。学生是另一套独立的参数,不是从老师身上切下来的。上一代 Qwen3 的强到弱蒸馏(arXiv:2505.09388)发生在后训练阶段,教的是教师的输出与 logits,不是把大模型的权重砍成小模型。
  • 量化(quantization):只改每个参数用几位来记,参数一个没少、形状一个没变。所以它是唯一一件「改完还能一一对应回原来那张矩阵」的事。

加上训练之前的独立预训练,一共四件事。全站要澄清的核心误解,本质上就是把这四件事压成了一件。

表 1-2 的四条轴里,哪一条发生在训练之后?改动这一条,需不需要重新训练模型?

先想:哪一条轴动的是「有多少个参数、矩阵多大」,哪一条动的是「每个参数怎么记」。
关键在于第 0 章那条公式:字节数 = 行 × 列 × 位宽 ÷ 8。四条轴里只有一条不碰行和列。
第一步这样走:把四条轴逐一代进那条公式,看它改的是哪个因子。
完整答案:只有第 条(数值精度 / 量化)发生在训练之后。它改的是公式里的「位宽」这一项,行和列都不动,因此不需要重新训练——量化是拿一份已经训好的权重,换一种记法重新写一遍文件。相对地,前三条轴改的都是矩阵的行数列数(或者矩阵的个数与连接方式),一改就是一份新图纸,必须从头训。这正是「尺寸差异发生在训练之前,量化发生在训练之后」这句话的算术含义。

变式:把第 ② 条从「稠密 FFN」改成「512 个专家的 MoE」,参数量会变吗?需不需要重新训练?如果需要,那它和量化的根本差别在哪一句话上?

有人说:「27B 就是 2.4T 的 4 位量化版,所以它才只有 17 GB。」请用表 1-2 指出他把哪两条轴混成了一条,并给出两个能直接推翻他的数字。

先想:他这句话里同时提到了「尺寸不同」和「量化」。这两件事在表 1-2 里分别是第几条轴、分别发生在什么时候?
关键在于:如果 27B 真是 2.4T 量化出来的,那它俩的参数个数应该完全相同(量化不改参数量),只是文件大小不同。那么去查两者的参数量,看是否相同。
第一步这样走:先写下 2.4T-A95B 的总参数 2.420 T 和 27B 复算出的 27.36 B,问自己:量化能不能让参数量从 2.42 万亿变成 273.6 亿?
完整答案:他把第 条(参数规模,训练之前)和第 条(数值精度,训练之后)混成了一条。两个推翻他的数字:其一,参数量——2.4T-A95B 是 2,419,802,390,528 个参数,27B 是 27,355,639,808 个,相差约 88 倍;量化不改变参数个数,无论压到几位,2.42 万亿个参数不会变成 273.6 亿个。其二,结构——27B 是 64 层、hidden 5120、稠密 FFN,2.4T-A95B 是 92 层、hidden 8192、512 专家 MoE;量化不会把 92 层变成 64 层,也不会把 512 个专家合并掉。补一句:17 GB 那个数确实是量化来的,但它是 27B 自己的 Q4_K_M 版本(17.11 GB),跟 2.4T 没有关系。

变式:如果真的把 2.4T-A95B 量化到 4 位,文件大约多大?(用第 0 章那条公式算,别查表。)算完再看:这个数离 24GB 单卡还差多远?

1.3 一台机器的爆炸图

现在把这台机器立起来看。下面这个拆机台里是 Qwen3.8-27B 的全部 64 层,按它们在 config 里的真实顺序排列:每 4 层一组,前 3 层是门控增量网络(Gated DeltaNet),第 4 层是门控注意力(Gated Attention),这一组重复 16 次。

怎么玩:把鼠标悬停在任意一层上,会显示这一层是哪一类零件、有多少参数;按住拖动可以转动整台机器,看清内部的层与层之间怎么串;点拆解会把 64 层沿轴向拉开,这时候 3:1 的节奏一眼就能看出来——每三层浅色之后必有一层深色。你不需要现在就懂每个零件在算什么,只要先记住这台机器的形状:它非常高(64 层),每一层的进出口一样宽(都是 5120),而且四层一个循环。

那么一个 token 从进到出,具体走过哪些零件?

图像分支(27B 独有) 图片 切成 16×16 小块 patch_size 16 视觉塔 27 层 hidden 1152 空间合并 · 投影 出口 5120 文本分支 文字 分词器 切成 token token 编号 0 到 248319 嵌入表查表 248320 × 5120 并成同一条序列 形状 [T, 5120] 主干 64 层 = 16 × (3 × Gated DeltaNet→FFN + 1 × Gated Attention→FFN) Gated DeltaNet 固定大小的记忆 Gated DeltaNet 固定大小的记忆 Gated DeltaNet 固定大小的记忆 Gated Attention(有 KV cache) 全站显存的来源就是这一类层 最终归一化 输出头 5120 → 248320 得到 248320 个 logits softmax 采样出下一个 token 再接回最前面 吐一个词 就整条重走一遍
图 1-2:一个 token 从输入走到输出的完整路径。零件名称与形状取自 config.json,连接顺序为示意图,非官方原始图示。虚线是自回归的回路:每吐一个词,整条链就重走一遍——这就是第 5 章 KV cache 存在的理由。

三个值得现在就记住的点。第一,中间那条主干里,四层一组、其中只有一层是全注意力;只有这一类层会产生随上下文长度不断增长的 KV cache,另外三层的记忆是固定大小的。全网的显存计算器几乎都在这里算错,第 5 章会算给你看。第二,最右边那条虚线是自回归(autoregressive)回路——模型一次只吐一个词,吐完把它接到输入末尾,整条链重走一遍。第三,图像分支和文本分支最后汇进的是同一条序列,不是两个模型拼在一起,这就是「原生多模态」这个词的实际含义,第 11 章会展开。

config.json 里有两个字段:num_hidden_layers = 64full_attention_interval = 4。请算出这台机器有几层全注意力、几层线性注意力。再算一次:如果把 full_attention_interval 改成 8,答案变成多少?

先想:interval 的意思是「每隔多少层出现一次全注意力」。那么在 64 层里,它会出现多少次?
关键在于这是一个除法:总层数除以间隔,得到全注意力层的个数;剩下的全是线性注意力层。
第一步这样走:64 ÷ 4 = ?,这就是全注意力层数;再用 64 减去它。
完整答案:64 ÷ 4 = 16 层全注意力,64 − 16 = 48 层线性注意力,与模型卡写的 16 × (3 线性 + 1 全) 完全一致——两条独立路径给出同一个数,这就是「可复算」的意思。若把 interval 改成 8:64 ÷ 8 = 8 层全注意力56 层线性。要留意后半问的分量:全注意力层数直接决定 KV cache 大小,从 16 层砍到 8 层,KV cache 会直接减半,但代价是能做精确长程回看的层也少了一半。这个取舍就是第 7 章的全部内容。

变式:2.4T-A95B 是 92 层、同样 full_attention_interval = 4。它有几层全注意力?把你的答案和第三方推理框架博客里写的「其余 69 层跑线性注意力」对一下,对得上吗?对得上说明了什么?

1.4 知识依赖图:这二十章该按什么顺序读

这个站是一条主线:每一章往手上那个最小实现里加一层,最后你手上是一台能跑的机器。所以章与章之间是有依赖的——有些章不先学,后面那章会字面意义上看不懂。下面这张图把依赖关系画出来了。

ch0 数学地基 ch1 全景 ch2 分词嵌入 ch3 注意力 ch8 FFN ch11 视觉塔 ch15 量化一 ch16 量化二 ch4 全注意力层 ch6 DeltaNet ch9 MoE ch5 KV cache ch7 3:1 布局 ch10 总参与激活 ch12 亲手点钞 ch13 尺寸从哪来 ch17 上手 24GB ch14 误解澄清 ch18 评测与缺口 ch19 终极闯关 高亮的五章是全站的骨头:读完它们,剩下十五章都只是把细节填进去
图 1-3:二十章的依赖关系。示意图,只画主依赖:第 17 章除了图上这两条,还要用到第 5 章的 KV cache 公式;第 12 章的点钞需要第 2、4、6、8、9、11 章的每一类零件,图上只画了汇合处的三条。虚线是第 0 章的浮点数直接支撑第 15 章。

五个核心章值得单独点名:第 5 章 KV cache(推理为什么越来越慢、显存到底被谁吃掉)、第 6 章 Gated DeltaNet(把记忆压成固定大小的那一类层)、第 9 章 MoE(把 FFN 切成 512 份)、第 12 章 亲手点钞(逐张矩阵数出 27B 和 2.4T,全站的高潮)、第 14 章 小模型不是大模型切出来的(本章 1.2 节那个论点的正式了结)。如果你时间有限,这五章一章都不要跳。

1.5 证据分级:这份材料里哪些能信到什么程度

为什么要在第一章就教这个

因为这次没有技术报告。没有论文意味着没有一份被同行审过的、把所有主张和证据绑在一起的文档,剩下的全是散落各处的碎片:配置文件、模型卡、第三方实测、媒体报道。这些碎片的可信程度差得非常远,但它们在网页上长得一模一样,都是一句话加一个数字。学会分级,比记住任何一个数字都重要。

表 1-3:本站使用的四档证据(外加一档本站自己的推导)
是什么本站里的例子你该怎么对待
① 可复算公开的配置文件里的数字,以及由它们算出来的量层数 64、hidden 5120、词表 248320;参数量 27.36B最高等级。你可以自己跑一遍脚本推翻本站,第 12 章会带你数一遍
② 官方声明模型卡、官方博客里的说法与评测分数模型卡上的 benchmark 成绩、上下文可扩展到 1,010,000属于厂商自述。事实性描述(架构、许可证)可信度高;自评成绩要当成一方主张读
③ 第三方实测别人真的跑出来、能重复的数字GGUF 各量化档的真实文件大小(Q4_K_M 17.11 GB)、推理框架 recipe 里的显存与卡数可信度高,但要注意它测的是哪个具体版本、哪套软硬件
④ 二手转述媒体报道、聚合站、社区帖子的转述各种「Qwen3.8 全部 Apache 2.0」式的说法只当线索,不当证据。本次已知存在转述失真,见下方
本站推导用 ① 的公式加 ③ 的实测数据算出来的数字24GB 单卡在 Q4_K_M 下大约能吃多长上下文介于 ① 和 ③ 之间。凡是这一类,本站都会明写「本站推导」,你要连推导过程一起复核
读的时候要小心:这一次真的出现了转述失真

一篇被广泛转载的英文报道把两个模型的许可证混为一谈,写成都是 Apache 2.0;后续的中文转述又照抄了它。这条错误的传播路径值得拆开看:源头写错 → 转述者没有回查许可证文件 → 再转述者把「据报道」也省掉了 → 最后它读起来像一条事实。整条链上没有人撒谎,只是没有人回到一手材料

本站的做法是:凡是能回到 config.json 或许可证原文的,一律回去看;回不去的,明写「本站未能核实」。你在读本站时也应该用同一把尺子——本站也可能写错,而 ① 档的东西是可以被你亲手推翻的。第 18 章会专门讲怎么读评测、以及这次开源没有告诉你的那些事。

还有一类特别容易被误读的东西:旧闻。一条 2025 年的消息被排在 2026 年 8 月的发布通稿里,读起来就像是因为新模型才发生的新采用。判断方法很简单:看到任何一条「某某公司改用了它」,先找日期,找不到日期就当它没有日期。

把下面四条陈述归到表 1-3 的某一档,并说出你的判据。有一条没法归档,把它挑出来并解释为什么。
(a)「27B 的 hidden_size 是 5120。」 (b)「Q4_K_M 的 GGUF 文件是 17.11 GB。」 (c)「模型卡上写它在某项评测拿了 XX 分。」 (d)「这个模型比同尺寸的其他模型更强。」

先想:每一条要问同一个问题——如果我想推翻它,我该去看什么?看得到、看得懂、能重复,就是高档;只能选择信或不信,就是低档。
关键在于第四条。它和前三条有个结构性差别:前三条都指向一个具体的、可以被核对的对象,第四条指向的是一个比较,而比较依赖于「用哪个评测、什么时候测、怎么算同尺寸」这一堆没说出来的前提。
第一步这样走:先把 (a)(b)(c) 各自的核对方式写出来——(a) 去哪个文件看?(b) 去哪个页面看?(c) 谁说的?
完整答案:(a) 是 ① 可复算——打开 config.json 就能看到,而且它还能被交叉验证(参数量算出来对得上)。(b) 是 ③ 第三方实测——第三方量化仓库页面上标的真实文件大小,能下载来核对,但要注明是哪一家的哪一版。(c) 是 ② 官方声明——事实上它是「厂商自述的自评成绩」,不是第三方复现的结果,读的时候要带着这个前提。(d) 没法归档:它不是一条可核对的陈述,而是一个结论,且省略了全部前提(哪个评测、哪个时间点、哪些模型算同尺寸)。本站的纪律是这类比较性排名一律不写,要写就必须带上评测名、日期与出处快照。这一点会在第 18 章展开。

变式:给「24GB 卡上 Q4_K_M 大约能吃 8–10 万 token 上下文(fp16 KV)」这句话归档。它属于哪一档?如果你想推翻它,你要复核的是数据还是推导步骤?(提示:本站早先给的是一个偏低的单点值,被推翻靠的正是复核推导步骤——把 24 GiB 当成 24 GB 算,少了 7.4%。第 5 章有完整推导。)

假设有人坚持说 27B 就是从 2.4T 剪出来的。撇开官方怎么说,请你设计一条可观察的证据,只要它成立就能把这个说法推翻。你的证据必须是别人也能重复的。

先想:剪枝是把一个已经训好的模型的一部分砍掉。那么被剪出来的模型,在结构上会保留原模型的什么特征?
关键在于剪枝只能,不能凭空造出原模型没有的东西,也很难改变那些和层数、专家数无关的全局设计。去比较两个模型的 config.json,找一个「剪枝做不到」的差异。
第一步这样走:把两份 config.json 并排放,逐字段比。先看有没有一方有、另一方根本没有的字段。
完整答案:可用的判据不止一条,最干净的一条是多模态相关字段:27B 的 config 里有完整的 vision_config(27 层视觉塔、patch 16、out_hidden 5120),还有 2.4T 完全没有的 mRoPE 多模态位置编码设置(mrope_interleavedmrope_section);而 2.4T-A95B 是纯文本模型,根本没有视觉塔。剪枝只会让东西变少,不可能从一个没有视觉塔的模型里剪出一个有视觉塔的模型——所以这条差异一出现,剪枝假说就死了。第二条判据是 MoE:2.4T 每层有 512 个专家,27B 是稠密 FFN;从 512 个专家剪到 1 个稠密 FFN 不叫剪枝,那是换图纸。第三条是 hidden_size:8192 与 5120,剪枝一般不动这个全局宽度,动了就等于重训。三条都可以由任何人下载两份 config.json 亲自复核——这就是 ① 档证据的价值。

变式:把问题倒过来。如果真有一个模型是从另一个剪出来的,你会期望在 config.json 里看到什么样的相似性?(提示:想想哪些字段是剪枝改不动的。)

下面这段话读起来很顺,但错得相当密集。逐条找出问题,并说明每一条该怎么改:
「Qwen3.8 这次开源了从 4B 到 2.4T 的全系列模型,全部采用 Apache 2.0 协议。其中 27B 是把 2.4T 版本量化压缩得到的轻量版,24GB 显卡可以流畅运行满血 262K 上下文。更多细节见官方技术报告。」

先想:这段话触到了本章全部三节。用表 1-1 核第一句,用表 1-2 核第二句,用表 1-3 判断最后一句该不该信。
关键在于「量化压缩得到的轻量版」这半句——它同时错了两层:一是事实(两个模型各自独立预训练),二是概念(量化不改变参数量,不可能把 2.42 万亿变成 273.6 亿)。
第一步这样走:把这段话拆成五个独立断言(全系列、许可证、来源、显存与上下文、技术报告),一个一个判,不要整段判。
完整答案:五处问题。其一,没有 4B 等小尺寸,官方只有 4 个仓库、两个模型(27B 与 2.4T-A95B,各带一份 FP8)。其二,许可证写错了:27B 是 Apache 2.0,2.4T-A95B 是 qwen3.8-max 自定义协议,本站称后者为「开放权重」而非「开源」;这一条会影响读者的商用合规判断,是最严重的一处。其三,27B 不是 2.4T 量化压缩来的,两者是独立预训练的两套参数,四条轴里前三条都不同(27.36B 对 2.42T、稠密对 512 专家 MoE、64 层对 92 层);而且量化根本不改变参数个数。其四,「24GB 流畅运行满血 262K」不成立:24GB 的卡实际是 24 GiB,Q4_K_M 权重 17.11 GB(= 15.93 GiB)加上视觉塔 0.93 GB(= 0.87 GiB),再减去 DeltaNet 固定态 0.14 GiB 和 1.0–2.0 GiB 的框架与激活开销,只剩 5.1–6.1 GiB 给 KV cache,对应 fp16 KV 约 8–10 万、fp8 KV 约 17–20 万 token(本站推导,框架开销是经验区间,真实值更靠近下沿;完整推导见第 5 章),原生 262K 在 24GB 单卡上跑不满。其五,没有官方技术报告,也没有官方 GitHub 仓库,模型卡是唯一权威文档。按表 1-3,这段话整体属于 ④ 档二手转述,而且是已知失真类型的典型样本。

变式:把这段话改写成一个你愿意签名的版本——每一句都要能对应到表 1-3 的某一档,并在需要的地方标出档位。写完数一数,你的版本比原文长了多少?这个长度差就是「说准确」的成本。

自己推一遍:从三个配置字段推出这台机器的记忆结构

这条推导只用 config.json 里的四个数,却直接决定了后面十几章的所有显存结论。每一步先自己想。

  1. 已知 num_hidden_layers = 64。光看这一个数,你能说出这台机器有多少层是全注意力吗?

    想好了再看

    不能。绝大多数显存计算器就是在这里出错的——它们默认「层数 = 全注意力层数」,因为传统架构里每一层都是注意力。这台机器不是。要回答这个问题,必须再找一个字段。

  2. config 里还有 full_attention_interval = 4。这个字段在说什么?

    想好了再看

    它说的是「每隔 4 层出现一次全注意力层」,也就是四层一组、每组里只有一层是全注意力,另外三层是线性注意力(Gated DeltaNet)。于是全注意力层数 = 64 ÷ 4 = 16,线性注意力层数 = 64 − 16 = 48。和模型卡上写的 16 × (3 线性 + 1 全) 完全一致——这是第一次交叉验证。

  3. 两类层的记忆行为不一样。哪一类的记忆会随着对话变长而不断变大?

    想好了再看

    只有全注意力层。它必须把之前每个 token 的键和值都存下来备查,存的量正比于已经读过多少个 token,这堆东西就是 KV cache。Gated DeltaNet 那一类层把历史压进一个固定大小的状态里,读多少个 token 它都那么大——代价是压缩会丢信息,第 6、7 章会讲这个取舍。

  4. 现在算每个 token 的 KV cache 有多少个元素。你还需要哪两个字段?

    想好了再看

    需要 num_key_value_heads = 4head_dim = 256。每个 token 在每个全注意力层里要存一份键和一份值,于是:

    NKV = 2 × Lfull × Hkv × dhead = 2 × 16 × 4 × 256 = 32,768
    式 1-1
    符号是什么直觉
    NKV每个 token 要缓存的元素个数每读一个字要记多少个数
    2键和值各存一份一份索引、一份内容
    Lfull全注意力层数,这里是 16——不是总层数 64全站最容易算错的一项
    HkvKV 头数,num_key_value_heads = 4几路并行的记忆
    dhead每个头的维度,head_dim = 256每路记忆有多宽

    按 fp16 每元素 2 字节算是 64 KiB/token,fp8 则是 32 KiB/token。这个数会在第 5 章被反复使用。

  5. 最后一步,也是这条推导的全部价值:如果有人按「64 层全是注意力」来算,会得出什么?差多少?

    想好了再看

    他会算成 2 × 64 × 4 × 256 = 131,072 个元素,即 256 KiB/token,整整高估 4 倍。落到满上下文 262,144 个 token 上,正确值是 16.00 GiB,错算值是 64.00 GiB——一个能不能在单卡上跑的判断,就这样被一个字段的疏忽翻转了。这是本站最有价值的一个纠错点。同样的算法套到 2.4T-A95B:92 ÷ 4 = 23 层全注意力、69 层线性,每 token 是 2 × 23 × 4 × 256 = 47,104 个元素,fp16 下 92 KiB/token。而第三方推理框架的博客独立写了「其余 69 层跑线性注意力」——两条互不相干的路径给出同一个数,这就是第 1.5 节说的交叉验证。

答辩:如果我是审稿人(一)

你花整整一节强调 27B 是 Apache 2.0、2.4T 不是。可现实里绝大多数读者既不经营模型即服务业务、也做不出月活一亿的产品,那两条限制对他们等于不存在。坚持这个区分,是不是学究气?

参考防守(先自己组织语言再看)

三层回答。第一层,门槛的对象不是个人。读者个人确实碰不到那两条线,但读者所在的公司可能碰得到,而且合规判断通常不是由写代码的人做的——一句「反正都是 Apache 2.0」传到法务那里,会变成一个错误的前提。第二层,「开源」是一个有定义的词。它不是「能免费下载」的同义词。一份带业务范围限制和展示义务的协议,无论限制多宽松,都不满足开源定义,把它叫开源是在稀释这个词——而这个词恰恰是很多人做技术选型时唯一依赖的信号。第三层,这正是失真的生成机制。转述失真从来不是有人故意撒谎,而是每一环都觉得「差别不大,省掉吧」:第一个人觉得读者用不到,第二个人照抄,第三个人连「据报道」都省了。本站在这里较真,不是为了那两条款,是为了示范一次「回一手材料」的动作。当然,这个区分也有它的边界:本站只复述了协议里的两条限制,没有做法律解读,真要商用请去读许可证全文并咨询专业意见。

答辩:如果我是审稿人(二)

你把「量化」和另外三条轴并列,还反复说它「不改变尺寸」。可谁都知道量化到 2 位模型明显变笨了。既然行为变了,说「什么都没变,只是记法不同」,是不是在玩文字游戏?

参考防守(先自己组织语言再看)

不是文字游戏,是两个不同的量被分开了。第一,本站从来没说量化不改变行为。本站说的是量化不改变参数个数与矩阵形状,这一条有可操作的判据:把一份 4 位量化的权重反量化回 BF16,每一张张量的形状会和原始 BF16 版一模一样,张量的名字和数量也一样;而如果 27B 真是从 2.4T 剪或量化来的,这个检验立刻会失败。第二,能力下降的归因不同。量化后变笨的原因是每个参数的记录精度变粗、误差在几十层里累积,不是因为参数变少了——这两种解释会导出完全不同的补救方式:前者要改量化方案(换分组粒度、给敏感张量留高位宽),后者只能重训。把原因搞反,你会去做完全无效的事。第三,审稿人的追问有一半是对的:本章只讲了体积这一半,掉多少能力那一半本章一个字都没给,它需要实证数据。这一点本站在第 15、16 章补,而且会明确区分「实测数据」和「本站推断」。

对你而言未知那 64 层的排布,能不能不靠模型卡、只靠权重文件自己数出来?

本章的推导用了 full_attention_interval = 4 这个字段,再和模型卡上的文字描述交叉验证。但这两条其实同源——都是官方说的。有没有一条完全独立的路径,能让你自己确认「第几层是哪一类」?答案是有的,只是不在本站抄录的这份配置节选里:官方 config.json 里还有一个 layer_types 数组,逐层写明每一层是 full_attention 还是 linear_attention。本站 mat/cfg27b.json 是人工节选的版本,为了聚焦没有抄这个数组进来——这本身就是一次需要你去核对的口径缩减。

先做这一步:去官方仓库打开 27B 的 config.json 原文,把 layer_types 数组复制出来,数三件事——数组长度是不是 64;full_attention 出现了几次(应该是 16);它们出现的下标是不是 3、7、11 一路到 63(也就是每组的第 4 层)。三条都对上,你就用一条独立于模型卡文字的路径确认了 3:1 布局。数完再顺手对 2.4T-A95B 做一遍,看是不是 92 长度、23 次。把你数出来的下标记下来,第 7 章会用到。

这一层要加什么:先搭一个只会原样吐回去的空壳

为什么第一步不是写注意力:因为在写任何一个真零件之前,你得先有一根能把形状串起来的管子。整台机器从头到尾只做一件事——把一个 [T, 5120] 的张量交给下一层,再接住它交回来的 [T, 5120]。这条约定一旦破了,后面每加一层都会在同一个地方报形状不匹配,而你会以为是新写的那一层错了。所以第一层加的不是功能,是一条能被检查的形状契约,外加一个把每一步形状记下来的钩子。

HIDDEN = 5120 LAYERS = 64 shapes = [] def probe(name, x): # 钩子:只记录,不改数据 shapes.append((name, shape(x))) return x def forward(tokens): # tokens 是长度为 T 的整数列表 x = probe('input', tokens) # [T] x = probe('embed', zeros(len(tokens), HIDDEN)) # [T, 5120],暂时全是 0 for i in range(LAYERS): x = probe('layer' + str(i), x) # 恒等层:进什么,出什么 x = probe('norm', x) # [T, 5120] return x, shapes

难点:这一层看起来什么都没做,最容易被跳过。但它藏着一个真问题——HIDDEN 到底该由谁说了算。新手会在每一层里各写一次 5120,代码看起来一样能跑,形状也一直对;直到你想把它改成 4096,才发现有六十几个地方写着同一个数字,改漏一个整条链就断了,而报错会出现在下游某一层,跟真正的病因隔着几十层。形状必须从一个源头流下去,不能在每一层各自声明。这条纪律在第 2 章接嵌入表时会立刻兑现:嵌入表的列数、每一层的进出口、输出头的行数,全是同一个 5120,它们必须是同一个变量。

自己验:喂 5 个 token 进去,shapes 里应该正好有 67 条记录(input + embed + 64 层 + norm),其中从 embed 那条开始的 66 条必须全部[5, 5120]。然后把 HIDDEN 从 5120 改成 4096 重跑:这 66 条应该一起变成 [5, 4096]。如果只有 embed 那一条变了、后面还是 5120,说明你在某一层里把 5120 写死了,形状没打通——回去找那个硬编码的数字,别往下走。

本章小结

  • 官方只放了 4 个仓库,其实是两个模型各带一份 FP8。没有小尺寸,没有 Instruct / Thinking / Base 变体,没有官方 GitHub,没有 arXiv 技术报告。
  • 许可证必须分开说:27B 是 Apache 2.0(开源)2.4T-A95B 是 qwen3.8-max 自定义协议(开放权重)。写成「全部 Apache 2.0」是事实错误。
  • 两个模型差在四条轴上:参数规模、稠密还是稀疏、层布局、数值精度。前三条在训练之前定死,第四条在训练之后发生。尺寸差异发生在训练之前,量化发生在训练之后。
  • 训练之后有三件不同的事:剪枝(真砍权重)、蒸馏(学生是另一套参数)、量化(只改位宽)。加上独立预训练,一共四件,绝不能混成一件。
  • 一个 token 的路径是:分词 → 查嵌入表 → 64 层主干(四层一组,只有第 4 层是全注意力)→ 归一化 → 输出头 → softmax → 采样 → 接回输入重走一遍。
  • 证据分四档:可复算 / 官方声明 / 第三方实测 / 二手转述,外加一档明确标注的「本站推导」。这次已知存在转述失真,遇到任何数字先问它是哪一档。

下一章开始动手:第一个真零件是嵌入表——248320 × 5120,1.27B 个参数,全部只为了做一件事,查表。

第2章 分词与嵌入:文字怎么变成数字

这一章回答一个最朴素的问题:你在输入框里敲下的一行汉字,是怎么变成一堆浮点数的。答案里藏着一个反直觉的事实——负责这件事的两个部件加起来吃掉了 27B 里的 2.54B,比全部 16 层注意力还贵,而它们几乎不做任何计算。

学完这一章你应该能做到

  • 用自己的话解释为什么模型不是「一个字一个编号」,而要先做分词
  • vocab_sizehidden_size 两个数直接算出嵌入表有多少参数,并说出它在 27B 里占多大比重
  • 在 config 里指出 tie_word_embeddings 这一行,并说清它是 false 意味着多花了多少参数
  • 指出「注意力天生看不见顺序」这个问题,说明位置信息为什么必须单独补进去
前置:第0章的「向量」「矩阵乘法」「参数」「浮点数」,第1章的整机爆炸图。

2.1 分词器:为什么不是一个字一个数

计算机里没有「字」,只有数。所以任何语言模型的第一步都是把文字换成整数编号。最容易想到的办法是一个字一个编号:常用汉字三千多个,编到 8000 号也够了。

不这么做会怎样

换成英文这套就崩了。按字母编号的话,internationalization 是 20 个编号,模型要跨 20 步才能拼出一个词的意思;按整词编号的话,英文单词加上各种变形、缩写、拼写错误、代码里的变量名(getUserById)、化学式、URL——词表会无限膨胀,而且只要遇到一个训练时没见过的词,模型就彻底哑火。一个字一个数,看着简单,实际是在「序列太长」和「词表爆炸且会遇到不认识的东西」之间二选一,两边都是死路。

字节对编码Byte Pair Encoding, BPE):一种自下而上的合并算法。先把所有文本拆成最小单位(字节),然后统计整个语料里哪两个相邻片段最常挨在一起,把这一对合并成一个新片段,记进词表;重复这个动作几万次,就得到一张词表。

BPE 的好处是它自己会分层。常见的词在几万次合并里被完整合成一个 词元token),罕见的词自动退化成若干个常见片段的拼接,最坏的情况一路退回单个字节。因为最底层是字节,所以只要是能存进电脑的东西,分词器就一定能表示——这叫 字节回退byte fallback)。模型永远不会遇到「这个字不认识」。

Qwen3.8 的词表大小是 248,320。这个数直接写在两份 config 里:cfg27b.jsontext_config.vocab_sizecfg24t.jsonvocab_size,两个模型一字不差,完全相同。这本身就是本站主线的一个小证据:27B 和 2.4T 共用同一套分词器,它们的差异不在「文字怎么进来」这一层。

24.8 万个格子有多大

《通用规范汉字表》收字 8105 个,日常读写用得上的大约 3500 个;一个受过完整教育的成年人,英语词汇量通常在两三万这个量级。24.8 万意味着:全世界一百多种语言的常用词各分一批,还要留出位置给代码(各种缩进组合、==)]}self. 这种在代码里高频、在自然语言里根本不出现的片段)、给数学符号、给各种 Unicode 表情的字节序列,最后还要留一整套单字节兜底。

词表越大,同一段文字切出来的 token 越少,模型跑得越省;但词表大小会直接乘进下一节那张表里。记住 248,320 这个数,它马上要乘一个 5120。

打开 cfg24t.json,找出 2.4T 模型的词表大小;再打开 cfg27b.json 找出 27B 的。两个数相同还是不同?这说明了什么?

先想:分词器是「文字怎么变成编号」这一步的东西,它和模型有多少层、每层多宽有关系吗?
关键在于:词表大小这个字段在两份 config 里的名字不完全在同一层级——27B 的架构参数被包在 text_config 里面,因为它还有一个 vision_config
第一步这样走:在 cfg27b.json 里搜 vocab_size,你会在 text_config 那一段的末尾找到它;在 cfg24t.json 里搜同一个词,它在顶层。
完整答案:两个都是 248320,完全相同。这说明两个模型共用同一套分词器与同一份词表。含义有两层:其一,同一句话在两个模型里切出来的 token 数完全一样,你可以直接拿一个模型的 token 计数去估另一个的开销;其二,也是更重要的一层——「27B 和 2.4T 差在哪」这个问题的答案里,不包括「文字怎么进来」。它们的差异在更深的地方(层数、稠密还是稀疏),而不在入口。

变式:如果哪天官方发一个词表只有 3.2 万的 Qwen3.8 小尺寸版,同一段中文切出来的 token 数会变多还是变少?这对「模型跑得快不快」是好消息还是坏消息?(提示:token 变多意味着要跑更多步)

2.2 嵌入表:一张 248320 × 5120 的大表

分词之后,「猫」这个 token 变成了一个整数,比如 27331。现在的问题是:能不能直接把 27331 这个数喂给神经网络?

不这么做会怎样

不能。因为一旦你把 id 当成数值送进去,网络就会认为 27331 比 27330 大 1、比 100 大很多——它会把 id 的大小关系当成语义关系去用。可 id 的顺序完全是 BPE 合并顺序的副产品,27330 号和 27331 号很可能一个是某个法语词尾、一个是某段代码缩进,毫无关系。用一个一维的数去表示一个词,等于强迫所有词排在一条直线上,而词与词之间的关系显然不止一个维度。

嵌入表Embedding Table):一张 Vd 列的矩阵,V 是词表大小,d 是模型主干的宽度。第 i 行就是第 i 个 token 的向量表示。把 token 换成向量这一步,就是按行号取出对应的那一行。

xt = E[idt], E ∈ ℝV×d
式 2-1
符号是什么直觉
E嵌入表,形状 V×d = 248320×5120 的矩阵一本查了就走的字典,一个词一行
idtt 个位置上那个 token 的整数编号页码
xt取出来的第 t 个位置的向量,长度 5120这个词的「坐标」
V词表大小,Qwen3.8 是 248320字典有多少页
d隐藏维度 hidden_size,27B 是 5120每个坐标有多少个分量

于是参数量就是一个乘法。cfg27b.jsonhidden_size5120vocab_size248320

Nemb = V × d = 248320 × 5120 = 1,271,398,400 ≈ 1.27 B
式 2-2
符号是什么直觉
Nemb嵌入表的参数个数这张表里一共有多少个要存的数
1,271,398,400复算值,见 mat/paramcount.out.txt 第一段12.7 亿个数,只为了查表
一句话记住:一个不做任何计算、只负责「按行号取一行」的部件,在 27.36B 里占掉了 1.27B。

这个数值得停下来感受一下。1.27B 个参数,用 BF16 存每个占 2 字节,光这一张表就是 2.54 GB。而整个 27B 模型量化成 Q4_K_M 之后的完整权重文件也才 17.11 GB。换个角度:每个 token 的向量是 5120 个 BF16 数,也就是 10,240 字节——每个词元都有一张 10 KiB 的身份证,24.8 万张身份证摞在一起,就是这 2.54 GB。

常见误解

很多人以为参数多的地方就是「模型在思考的地方」。嵌入表是最直接的反例:它的 1.27B 参数里,处理一个 token 时被读到的只有 5120 个(也就是一行),其余 1,271,393,280 个数原封不动躺着。参数量大和计算量大是两件事——这是本站会反复回到的一个区分,第10章会把它推到极致(2.4T 总参数、95B 激活参数)。

假设有人做了一个 Qwen3.8-27B 的变体,把 hidden_size 从 5120 改成 4096,其他都不动。嵌入表的参数量变成多少?比原来省下多少 GB 的 BF16 权重?

先想:式 2-2 里只有两个因子,你只动了其中一个。
关键在于:参数量和 d 是严格的正比关系,所以省下来的比例就是 d 减小的比例。BF16 每个参数 2 字节。
第一步这样走:先算 248320 × 4096,再拿它和 1,271,398,400 相减,最后乘 2 字节。
完整答案:248320 × 4096 = 1,017,118,720(约 1.02 B)。比原来少 254,279,680 个参数,乘 2 字节 = 508,559,360 字节,约 0.51 GB。注意这里还只算了嵌入表一张;因为输出头是同样大小的另一份(见 2.3),实际省下的是这个数的两倍,约 1.02 GB。顺带提醒:这道题只是算术练习,真把 hidden 改小会牵动整个模型的每一层,不是「只改一个数」那么简单。

变式:反过来,如果保持 d=5120 不变,把词表从 248320 砍到 32000(一个很多早期模型用过的规模),嵌入表省下多少参数?这个数和 16 层 Gated Attention 的总参数 1.68B 比,谁大?

自己推一遍:从两个 config 字段,推到「这台机器有多少参数只是为了查表」

  1. 你要给每个 token 一个向量表示。第一个要定的数是:这个向量有多长?到哪儿去找这个数?

    想好了再看

    cfg27b.jsontext_config.hidden_size,是 5120。之所以是这个字段,是因为 token 向量一进模型就要在主干里流动,它必须和主干等宽——不然每一层都要额外做一次尺寸转换。这是 Transformer 里一个默认到几乎没人提的约定:残差流的宽度是全局统一的

  2. 第二个数:一共要给多少个 token 准备向量?

    想好了再看

    vocab_size = 248320。注意不是「常用词有多少」,而是「词表里有多少格」——哪怕某个格子在整个训练语料里只出现过一次,它也占满一整行 5120 个参数。

  3. 现在算这张表有多少个数。写成矩阵形状。

    想好了再看

    248320 行 × 5120 列 = 1,271,398,400。当初为什么会想到「参数量就是矩阵元素个数」?因为矩阵里每一个格子都是一个要被训练、要被存进文件的独立数字,没有任何一个是算出来的。这条规则对整个模型都成立,第12章会靠它把 27B 一张一张点出来。

  4. 出口那边呢?模型最后要吐出「下一个词是词表里哪一个」的分数,这需要一个什么形状的矩阵?它和嵌入表是什么关系?

    想好了再看

    需要把 5120 维映射到 248320 个分数,也就是一个 5120×248320 的矩阵——形状恰好是嵌入表的转置。所以有一个很自然的念头:能不能就用同一份权重?这正是 2.3 节的主题。

  5. 去 config 里找一个字段,它决定了这个念头在 Qwen3.8 里成不成立。

    想好了再看

    tie_word_embeddingscfg27b.json 里它是 falsecfg24t.json 里也是 false。所以出口那份是独立的另一套参数,总账要乘 2:2 × 1,271,398,400 = 2,542,796,800。

  6. 最后一步:这 2.54B 占 27.36B 的百分之多少?

    想好了再看

    2,542,796,800 ÷ 27,355,639,808 = 9.295%,即 9.3%——和 mat/paramcount.out.txt 里那张占比图完全对上。你刚刚只用了 config 里的三个字段,就复现了一个官方从未公布的拆解。

2.3 输出头:为什么它是独立的另一份 1.27B

模型跑完全部 64 层之后,手上是一个 5120 维的向量。它得变成「词表里 248320 个 token 各自的可能性」。做这件事的部件叫 输出头LM head / output head),本质是一个 5120 × 248320 的矩阵乘法,输出 248320 个原始分数,再过一次 softmax 变成概率。

权重绑定weight tying):让输出头直接复用嵌入表的权重(用它的转置),而不是另存一份。因为两者的形状本来就互为转置,这在技术上完全可行,而且能省掉整整一份 V×d。很多模型,尤其是参数预算紧张的小模型,都这么做。

Qwen3.8 没有这么做。证据就在 config 里,一行,不用猜:

cfg27b.json → text_config → "tie_word_embeddings": false cfg24t.json → "tie_word_embeddings": false

所以出口那张表是独立训练、独立存储的另一份 1,271,398,400 个参数。进门一份、出门一份,加起来:

Nemb + Nhead = 2 × 1,271,398,400 = 2,542,796,800 ≈ 2.54 B (占 27.36 B 的 9.3%)
式 2-3
符号是什么直觉
Nhead输出头参数量,因为不绑定,等于 Nemb出门那本字典
9.3%本站按 mat/paramcount.out.txt 的复算结果,分母是全模型 27.36 B(含视觉塔)近十分之一的参数在做查表

拿它和别的部件比一下就更刺眼了:全部 16 层 Gated Attention 加起来是 16 × 104.86 M = 1.68 B,占 6.1%。也就是说,这台机器里「查字典」花的参数,比「全部的注意力」还多出一半还不止。

读的时候要小心:这里只有事实,没有理由

tie_word_embeddings: false 是本站从两份 config.json 里直接读出来的事实,可以自己去 mat/cfg27b.json 核对。但官方没有解释为什么不绑——Qwen3.8 没有 arXiv 技术报告,没有官方 GitHub 仓库,模型卡里也没有这一段。社区里流传的几种说法(词表很大时绑定会让「输入端的词表示」和「输出端的预测方向」互相牵制、不绑能让两端各自专精之类)都属于经验判断,不是这次发布的官方结论。本站的立场是:只报告 config 里写了什么,不替作者编理由。

另外要说清一处不对称,免得「进门查表、出门查表」这个顺口的说法把你带偏:两端的参数量一样,计算量完全不一样。进门是取一行,几乎不花时间;出门是一次真正的 5120×248320 矩阵乘法,而且每生成一个 token 都要重新做一遍。第3章之后你会越来越熟悉这个区分。

如果 Qwen3.8-27B 把 tie_word_embeddings 改成 true(其他都不动),下面三件事各自会怎么变?(a) 模型文件的大小;(b) 生成一个 token 需要的乘加次数;(c) KV cache 的大小。

先想:绑定改变的是「存几份权重」,还是「要做几次乘法」?这两件事不一定同步。
关键在于:绑定之后,输出头那次矩阵乘法照做不误,只是它用的那张矩阵和嵌入表是同一块内存。至于 KV cache,它跟嵌入和输出头根本不在一个部件上。
第一步这样走:先算 (a):少存 1,271,398,400 个参数,BF16 下就是少 2.54 GB。再问自己 (b):矩阵还在不在?在。乘法次数变了吗?
完整答案:(a) 文件变小约 2.54 GB(BF16),27.36 B 降到约 26.08 B。(b) 一点不变——输出头那次 5120×248320 的矩阵乘法照做,只是读的权重换成了嵌入表那块内存。省的是存储,不是算力。(c) 完全不变。KV cache 是注意力层缓存的 K、V,由全注意力层数、KV 头数、head_dim 决定(第5章),跟词表和嵌入表没有任何关系。这道题的要点是:「省参数」「省算力」「省显存里的缓存」是三条独立的账,改一个字段通常只动其中一条。

变式:反过来问,有没有哪种改动能同时减小 (a)(b)(c) 三项?(提示:想想把 num_key_value_heads 调小,或者把某些层换成不带 KV cache 的层——后者正是 Qwen3.8 真实在做的事,见第6、7章)

有人这样论证:「嵌入表和输出头占了 9.3% 的参数,而全部注意力才 6.1%,所以在 Qwen3.8 里,查字典比注意力更重要。」请构造一个具体的理由说明这个论证为什么站不住——最好能给出一个可以观察到的现象。

先想:「占参数多」和「重要」之间,需要一个什么前提才能划等号?这个前提在这里成立吗?
关键在于:嵌入表的 1.27B 参数中,处理一个 token 时真正被读到的只有一行 5120 个数;而注意力层的每一个参数,每个 token 都要用到。两者的「参数利用率」差了五个数量级。
第一步这样走:算一下嵌入表处理一个 token 时被触及的参数比例:5120 ÷ 1,271,398,400 ≈ 0.0000040,即百万分之四。再算注意力层:100%。
完整答案:这个论证默认了「参数量 ∝ 重要性」,而这个前提在稀疏访问的部件上直接失效。嵌入表是一张查找表,一个 token 只碰它的百万分之四;注意力层的每个参数每步都参与运算。可观察的现象有两个:其一,如果你只把嵌入表量化到很低的位宽,模型质量的下降通常远小于把同样比例的注意力权重量化——这也是 GGUF 的 K-quant 方案会给不同张量分配不同位宽的原因之一(第15、16章)。其二,更直接的:把嵌入表的某一行整个清零,只有那一个 token 会坏掉,别的输入照常;把某一层注意力的 o_proj 清零,整个模型立刻崩。参数量回答的是「要多少存储」,不是「有多重要」。

变式:把这个反例反过来用——第9章的 MoE 里,512 个专家每个 token 只激活 11 个。按同样的逻辑,你会怎么描述「2.4T 总参数」这个数的含义?它更像「重要性」还是更像「仓库大小」?

答辩:如果我是审稿人

248,320 的词表看着就像浪费。把它砍到 32,000,能省下 2 × (248320−32000) × 5120 ≈ 2.22 B 参数——差不多是全部注意力层的 1.3 倍。用这些参数多加几层 FFN,难道不是更划算?你凭什么说这个大词表是必要的?

参考防守(先自己组织语言再看)

先承认:省下来的参数确实可观,这个批评的算术完全正确。但它漏掉了成本的另一侧。词表变小之后,同一段文字会被切成更多 token——多语言和代码尤其严重,一个在大词表里是 1 个 token 的中文词组,在小词表里可能变成 3 到 4 个。而每多一个 token,就要多走一遍 64 层的完整前向,并且 KV cache 要多存一份(第5章会给出精确的每 token 64 KiB)。也就是说,大词表把成本从「随上下文长度线性增长的运行时开销」搬到了「一次性的静态参数」上,而后者只占显存、几乎不占算力。
但更诚实的回答是第二句:本站没有 Qwen3.8 分词器在中文与代码上的实测压缩率数据,所以「到底划不划算」这个问题,光看 config 是答不出来的。它只能靠实测。本章末尾的研究课题给了动手的第一步。

答辩:如果我是审稿人

你说这 9.3% 的参数「不做任何计算,只是查表」。可输出头那 1.27B 每生成一个 token 就要做一次完整的矩阵乘法,怎么能和输入端的取行相提并论?你这个说法是不是为了修辞效果偷换了概念?

参考防守(先自己组织语言再看)

这个批评成立,而且必须接受。准确的表述是:两端的参数量对称,计算代价严重不对称。输入端是 gather(按行号取一行),代价近似为零;输出端是一次 5120×248320 的稠密矩阵乘法,是整个前向里单张最大的矩阵之一,而且在自回归生成时每步都要重做。
本站在正文里已经把这一句写出来了,正是为了不让「进门查表、出门查表」这个顺口的说法把读者带偏。要补充的是一个有意思的推论:正因为这次乘法在解码阶段每步都做,很多推理框架会对它做特殊处理(比如只算候选 token 的分数)。这也说明为什么「一个部件贵不贵」必须分开问两次——问存储,问算力。全站会反复用到这个二分,第10章的「总参数 vs 激活参数」就是它的终极形态。

2.4 位置信息从哪来

到这里,一句话已经变成了一串 5120 维的向量。但有个东西还没进来:顺序

不管顺序会怎样

下一章会看到,注意力对输入做的核心操作是「按权重把别的位置的内容加起来」。加法是可交换的——把「狗咬人」的三个 token 打乱成「人咬狗」,如果每个 token 的向量里不带任何位置信息,那么每个位置算出来的结果只是跟着换了个位置,集合完全一样。也就是说,纯注意力看到的是一个词袋,不是一句话。它能知道这句话里有「狗」「咬」「人」,但分不清谁咬了谁。

补位置信息大致有三条路。第一条是 绝对位置嵌入absolute positional embedding):再建一张表,第 i 个位置有一个向量,加到该位置的 token 向量上。简单直接,但有两个麻烦:这张表要占参数;而且表有多少行,模型就只认识多少个位置,训练时没见过第 300000 个位置,推理时就抓瞎。

第二条是给注意力分数加一个只依赖距离的偏置。第三条,也是 Qwen3.8 用的,叫 旋转位置编码Rotary Position Embedding, RoPE):不往向量上加东西,而是它——把位置编码成一个旋转角度,作用在注意力的 Q 和 K 上。它的妙处是两个转过的向量做点积时角度相减,结果只依赖两者的相对位置。

这一章只需要你记住三件事:位置信息是单独补进去的;Qwen3.8 用 RoPE;RoPE 一个参数都不带——它是一个纯函数,所以本章的参数账里没有「位置」这一项。至于 Qwen3.8 那个相当特别的用法(只给 256 维里的 64 维加旋转,底数用 10,000,000 而不是常见的 10,000,27B 还多一套多模态版本),全部留给第4章

常见误解:max_position_embeddings 不是一张表的行数

cfg27b.json 里有 "max_position_embeddings": 262144。这个字段名是历史包袱——它听起来像「有一张 262144 行的位置嵌入表」,但 RoPE 根本不需要表。它实际上只是一个声明:这个模型的原生上下文长度是 262,144。正因为 RoPE 没有表,把这个数字改大在技术上是可能的(模型卡说可扩展到 1,010,000),只是效果要另说。字段名和它真正的含义对不上,这类地方在 config 里不止一处,读一手材料时要留意。

用一句话说明:为什么不加位置信息的注意力分不清「狗咬人」和「人咬狗」?

先想:注意力最后一步做的是什么运算?这个运算对参与项的先后顺序敏感吗?
关键在于:加权求和里,权重是由「两个向量像不像」算出来的,而「像不像」只看两个向量本身的数值,不看它们排在第几位。
第一步这样走:设三个 token 向量 a、b、c。写出「c 位置从 a、b、c 收集内容」的表达式,然后把 a 和 c 的位置对调,看表达式变没变。
完整答案:因为注意力对序列做的是加权求和,而权重只由 token 向量之间的相似度决定,与它们排在第几位无关。把输入序列做任意置换,输出也只是跟着做同一个置换,内容一个不差——数学上这叫置换等变。所以「狗咬人」和「人咬狗」在纯注意力眼里是同一个多重集合 {狗, 咬, 人},无法区分。位置信息必须从外面单独注入,注入的方式就是 RoPE(第4章)。

变式:那 FFN 层(第8章)能不能帮忙分清顺序?想一想:FFN 是对每个位置独立做同一套变换的,它有机会看到别的位置吗?

你手上有一张 24 GB 的显卡。现在有两个改造方案,都能省下大约 2.5 GB 的显存:(A) 把嵌入表和输出头从 BF16 换成 4bit 存储(其余不动);(B) 保持全 BF16,但把可用的上下文长度从 262K 砍到某个更短的值,用省下的 KV cache 空间抵掉 2.5 GB。已知 27B 的 KV cache 是每 token 64 KiB(fp16)。请分别说明这两个方案省的是什么、代价是什么,并指出一个「它们不可互换」的具体场景。

先想:权重占的显存和 KV cache 占的显存,一个是常数,另一个随什么变?
关键在于:方案 A 省的是一笔固定开销,跟你要处理多长的文本无关;方案 B 省的是一笔按 token 计费的开销。两者在「短文本」和「长文本」两种工况下的价值完全不同。
第一步这样走:先把 B 量化出来。2.5 GB ≈ 2.33 GiB,除以 64 KiB/token,看能换回多少 token 的上下文;再想 A 的代价落在什么上(提示:量化误差直接作用在每个 token 的初始表示和最终 logits 上)。
完整答案:
(A) 省的是常数项。2.54 GB 的 BF16 表压到 4bit 大约剩四分之一,省下约 1.9 GB;连同别的张量一起压才够 2.5 GB。代价是量化误差直接落在两个最敏感的位置:输入端的 token 表示和输出端的 logits。好处是不管你跑 1K 还是 262K 上下文,这 2.5 GB 永远省着。
(B) 省的是线性项。2.5 GB ≈ 2.33 GiB = 2,441,406 KiB,除以 64 KiB/token ≈ 38,000 个 token 左右的上下文。代价是能处理的文本直接变短,模型质量一点不掉。
不可互换的场景:如果你要做的是「读一本 20 万字的书然后回答问题」,方案 A 帮不上任何忙——它省下的 2.5 GB 只够多换约 3.8 万 token,而你缺的是十几万;这时必须走 B(或者按第5章的做法把 KV 换成 fp8,一步砍一半)。反过来,如果你只做短对话,B 等于白省——你本来就用不到那么长的上下文,而 A 省下的 2.5 GB 是实打实腾出来的。
这道题的要点:显存不是一个数,是「常数项 + 系数 × 上下文长度」两项。不问工况就谈「省显存」,等于没说。第17章会把这条式子完整写出来。

变式:把显卡换成 80 GB,同样两个方案,结论会不会反过来?再想一步:如果模型的 KV cache 不是 64 KiB/token 而是网上常见的那个错算值 256 KiB/token(第5章会讲这个 4 倍高估是怎么来的),方案 B 换回的上下文会变成多少?

对你而言未知248,320 个格子,到底为中文和代码换来了多少

本章反复用到的 248,320 是一个静态事实,但「它值不值那 2.54 B 参数」不是——那取决于这张词表把真实文本压得有多短,而这个数 config 里没有,模型卡里也没有,本站没有实测。答案是存在的,只是要自己跑出来:分词器文件就在公开仓库里,跑一次就有。

先做这一步:从 huggingface.co/Qwen/Qwen3.8-27B 的 Files 页把分词器相关文件下下来,用 tokenizerstransformers 加载,对三段材料各跑一次 encode——(1) 一千字的中文散文,(2) 一千字的英文散文,(3) 两百行 Python 代码。记下每段的「token 数 ÷ 字符数」。然后换一个词表约 3.2 万的开源分词器跑同样三段做对照。你会得到三个压缩比差值;把这个差值乘上第5章的 64 KiB/token,就能换算成「大词表在长上下文里替你省了多少显存」,再拿它和 2.54 GB 的静态代价比。这条链一旦跑通,本章那条答辩就有了数据,而不只是道理。

这一层要加什么:给空壳装上嵌入层和输出头

为什么现在才加它:第1层只是一个空壳,输入输出都还是抽象的「一段文本」。这一层之后,你的模型第一次能吃进真正的整数 id、吐出真正的 248320 维分数向量——虽然中间什么都还没有,输出全是噪声,但两端的形状已经和真的 Qwen3.8 一模一样了。

V, D = 248320, 5120 # 直接从 cfg27b.json 读,别写死 class Embedding: def __init__(self, V, D): self.W = randn(V, D) * 0.02 # 只在这里建一次,建完就存住 def __call__(self, ids): return self.W[ids] # 取行(gather),不是矩阵乘 class LMHead: def __init__(self, V, D): self.W = randn(V, D) * 0.02 # tie=false → 另起一份,不共享 def __call__(self, h): return h @ self.W.T # (T,D) @ (D,V) -> (T,V) model.embed = Embedding(V, D) model.head = LMHead(V, D) # 两份权重,互不相干 logits = model.head(model.embed(ids)) # 现在能一路跑通了

难点一:gather 和矩阵乘其实是同一件事,但绝不能真的当同一件事写。数学上 one_hot(id) @ W 等于 W[id]——把 id 变成一个只有一位是 1 的 248320 维向量,再和嵌入表相乘,结果就是那一行。理解这个等价关系,你才明白为什么输入端「不算」而输出端「要算」:前者可以偷懒直接取行,后者的输入是一个稠密的 5120 维向量,没法偷懒。想不明白这一点的人,往往会以为嵌入层也很费算力。

难点二,也是最容易翻车的地方:self.W 必须在 __init__ 里建好并保存。很多人第一次写会顺手在 __call__randn——代码不报错,跑得飞快,甚至 loss 也在动(因为别的层还在学),但同一个 token 每次拿到的向量都不一样,模型永远学不到任何词的含义。这种 bug 不会崩,只会让你训一整天然后困惑。

自己验,三条都要对上:(1)V=100D=8embed.W 的元素个数必须正好 800W.size == 800),不多不少。(2) 连着调两次 embed([7]),两个 8 维向量必须逐位完全相等——用 np.array_equal(a, b) is True 判,不是「差不多」,不是 allclose。如果不等,说明你在 __call__ 里重建了随机表,回去看难点二。(3) 把两份权重加起来数:embed.W.size + head.W.size 应该是 1600;再手动执行绑定 head.W = embed.W,独立参数总数掉回 800——这个从 1600 到 800 的差,就是 tie_word_embeddings: false 在真模型上多花的那 1.27 B。

本章小结

文字进模型只有两步:先用 BPE 切成 token 换成整数编号,再用嵌入表把编号换成 5120 维向量。这两步分别对应 config 里的两个数——vocab_size 248320 和 hidden_size 5120,它们一相乘就是 1,271,398,400 个参数。

出口那边,因为 tie_word_embeddingsfalse,输出头是独立的另一份同样大小的表。两份加起来 2.54 B,占 27.36 B 的 9.3%,比全部 16 层 Gated Attention 的 6.1% 还高——而它们几乎不参与「思考」。这是全站第一次撞上「参数量 ≠ 计算量」这堵墙,后面第10章会把它撞穿。

还差一样东西:顺序。注意力天生是置换等变的,看到的是词袋而不是句子,位置信息必须单独注入。Qwen3.8 用的 RoPE 不带任何参数,它把位置变成旋转角度作用在 Q 和 K 上——而 Q、K 是什么,正是下一章的全部内容。

第3章 注意力:为什么要「看别的词」

这一章回答:一个词要理解自己的意思,为什么必须回头看别的词;以及模型是怎么把「看谁、看多重」变成一串可以求导的乘法的。学完你会亲手算出一个注意力矩阵,并且知道那个除以根号 d 的操作一旦拿掉,会发生什么。

学完这一章你应该能做到

  • 说清固定窗口和循环网络各自卡在哪里,注意力绕开了哪一个
  • 指出 Q、K、V 是同一段输入经过三个不同矩阵投影出来的,而不是三批不同的数据
  • 逐个符号读懂 softmax(QK/√dk)V,并解释为什么必须除以 √dk
  • 说出因果掩码加在哪一步、为什么不能放在 softmax 之后
  • 解释多头不是「算力更强」,而是「同时维持好几种关注模式」
前置:第0章的「向量」「矩阵乘法」「softmax」,第2章的嵌入表与「注意力天生看不见顺序」。

3.1 不用注意力会怎样

先看一句话:

小明把书放在桌子上,然后他把它拿走了。

「它」指的是书还是桌子?你几乎不用想就知道是书——因为桌子拿不走。但注意你刚才做了什么:你回头扫了一遍前面出现过哪些名词,然后逐个判断哪个更可能跟「拿走」搭配。这两个动作,第2章那台机器一个都做不了。到目前为止我们造出来的东西只会把每个 token 单独查成一个向量,「它」查出来的向量和这句话里有没有出现过「书」毫无关系。

不用注意力,前人是怎么做的,各自卡在哪

固定窗口fixed window,比如 n-gram 和卷积):规定每个位置只能看前面 k 个词,k 写死在结构里。麻烦有两层:k 设小了,「它」和「书」隔了 8 个词就够不着;设大了,参数和计算量跟着线性涨,而绝大多数情况下这个大窗口是浪费的。更根本的问题是,需要的距离本来就不固定——同一个「它」可能隔 3 个词,也可能隔 300 个词。

循环网络Recurrent Neural Network, RNN):一个词一个词往前读,把读过的东西压进一个固定大小的向量 h 里带着走。这解决了距离不固定的问题,但换来两个新问题:其一是信息瓶颈——不管前面是 5 个词还是 5000 个词,都要挤进同一个 h,早期的内容会被后来的一点点冲掉;其二是必须串行——第 t 步要等第 t−1 步算完,训练时没法把一整句话铺开并行算,而并行正是 GPU 唯一擅长的事。

注意力的想法很直接:既不压缩,也不设窗口。让每个位置自己去看前面所有位置,并且自己决定看谁、看多重。不设窗口,所以远近都够得着;不压缩,所以早期的内容原样留在那里,需要的时候再去取。代价是每个位置都要和所有位置比一遍,计算量随序列长度平方增长——这笔账会在第5章变成一个非常具体的显存数字,第6、7章讲 Qwen3.8 为什么最终只让四分之一的层付这笔钱。

先埋一个伏笔

RNN 那个「固定大小的记忆」并没有被淘汰,它换了个更聪明的形式回来了——Qwen3.8-27B 的 64 层里有 48 层走的正是这条路(Gated DeltaNet,第6章)。它保留了「固定大小状态」这个取舍,但用分块算法解决了「必须串行」的问题。所以本章讲的注意力,在这台机器里只出现在 16 层上。

有人说:「RNN 的问题是看不了远处的东西。」这句话准确吗?如果不准确,正确的说法是什么?

先想:RNN 的状态 h 在每一步都会被更新并传下去,那么第 1 个词的信息在第 500 步时,究竟是「没传到」还是「传到了但变形了」?
关键在于:RNN 在结构上是够得着任意远的——信息有一条完整的通路。真正的限制来自这条通路的宽度,不是长度。
第一步这样走:把 h 的维度想成一个固定容量的水杯,每读一个词就往里倒一点、同时溢出一点。问自己:容量和读过的词数,哪个是固定的?
完整答案:不准确。RNN 在结构上能够到任意远的位置,信息通路是通的;限制在于这条通路的容量是固定的——不论前面有多少内容,都要压进同一个固定大小的 h,早期信息会被逐步覆盖。所以准确的说法是「信息瓶颈」,不是「看不到」。这个区分很要紧,因为它决定了解法方向:如果是「看不到」,办法是加长窗口;既然是「装不下」,办法要么是把记忆做大(注意力:把所有历史原样留着),要么是把「装什么、丢什么」学得更聪明(Gated DeltaNet:用门控主动决定擦掉什么,第6章)。Qwen3.8 两条路都留了。

变式:固定窗口的失败模式和 RNN 一样吗?如果把窗口设成 262144(跟 Qwen3.8 的原生上下文一样长),它还有什么问题?(提示:窗口里每个位置的权重是学出来的固定值,还是随输入变化的?)

3.2 Q、K、V:三个投影,一段输入

要让「它」去看前面的词,得先把这件事拆成可计算的步骤。注意力用了三个角色。

打个比方:查资料

你在图书馆找资料,这件事分三部分。你的问题:我想找一个前面提到过的、能被拿走的东西——这是 查询Query, Q)。每本书的标题:书脊上那行字,让你一眼判断这本值不值得翻——这是 Key, K)。书里的内容:真正被你抄回笔记本的东西——这是 Value, V)。流程是:拿问题去和每个标题比一比,比出一组「有多相关」的分数,然后按分数把内容加权抄回来。

类比失效处,两处,都很要命:第一,图书馆里问题、标题、正文是三批不同的东西——你的问题不在书架上。注意力里不是。Q、K、V 全部由同一段输入 X 乘上三个不同的矩阵变出来。同一个 token 在同一层里既是提问者又是被查的资料:它出一份 Q 去问别人,同时出一份 K 和一份 V 等着被别人问。三个矩阵是三副不同的眼镜,看的是同一张脸。第二,图书馆检索命中一本书就结束了;注意力从来不「选一本」,它把所有候选按相关度加权平均,一次性糅成一个向量——哪怕某本书只有 0.3% 的权重,它也确实混进来了。

写成公式就是三次矩阵乘法。设输入 X 是一个 T×d 的矩阵(T 个位置,每个位置一个 d 维向量,第2章刚做出来的那个):

Q = XWQ, K = XWK, V = XWV
式 3-1
符号是什么直觉
X这一层的输入,形状 T×dT 是序列长度,dhidden_size整句话摊平成一张表,一行一个位置
WQ查询投影矩阵,形状 d×dk,是要训练的参数第一副眼镜:把「我是什么」翻译成「我要找什么」
WK键投影矩阵,形状 d×dk第二副眼镜:把「我是什么」翻译成「我能被怎么找到」
WV值投影矩阵,形状 d×dv第三副眼镜:把「我是什么」翻译成「我能提供什么」
Q, K, V三个投影结果,T×dkT×dkT×dv同一批人的三种名片

为什么非得分成三个,不能直接拿 X 自己跟自己比?有两个理由。一是自恋问题:任何向量和自己的点积都是最大的,不做变换的话每个位置永远最关注自己,学不到别的。二是职责冲突:「我要找什么」和「我是什么」被迫用同一组数字表示,「我能提供什么」也挤在同一组里——三件不同的事共用一套坐标,模型没有余地把它们分开。拆成三个投影,等于给模型三个自由度,让它自己去学怎么分工。

如果把 WKWV 强行设成同一个矩阵(也就是让 K = V),注意力还能工作吗?会失去什么能力?举一个具体的语言现象说明。

先想:K 决定「我因为什么被找到」,V 决定「找到我之后你抄走什么」。这两件事在语言里一定一样吗?
关键在于:形状上没问题(只要 dk=dv),代码照跑,loss 照降。丢的是「被检索的特征」和「被搬运的内容」之间的解耦。
第一步这样走:造一个例子,让「用什么去匹配」和「要拿走什么」明显不是同一件事。比如代词消解:「它」是靠语法角色和语义类别去匹配先行词的,但真正要搬回来的是那个先行词的具体所指
完整答案:能工作,但表达力受限。K=V 意味着「一个词凭什么被注意到」和「它被注意到之后贡献什么内容」必须用同一组数字编码。具体例子:句子「小明把书放在桌子上,然后他把它拿走了」。「它」这个查询需要匹配的是「可搬动的物体」这个类别特征,但它需要搬回来的是「书」这个具体所指(书名、这本书在上下文里的角色等等)。如果 K=V,模型要么把类别特征做强、丢掉具体所指,要么反过来,没法两头兼顾。换个角度看:K 是索引,V 是内容,数据库里也从来不把索引和数据存成同一份。
顺带一提,真实模型里 K 和 V 不但不共享,连头数都可以不一样——Qwen3.8 让 24 个 Q 头共用 4 组 K/V(第4章的 GQA),但 K 和 V 各自仍然是独立的矩阵。

变式:那如果让 WQ = WK 呢?(提示:这会让注意力矩阵变成对称的——「A 关注 B」的程度必须等于「B 关注 A」。找一个语言现象说明这为什么是个损失,比如形容词修饰名词时,两个方向的依赖强度一样吗?)

3.3 注意力公式:逐个符号拆开

三个投影准备好之后,剩下的就是一行式子:

Attention(Q, K, V) = softmaxQKdk V
式 3-2
符号是什么直觉
KK 的转置,形状 dk×T把「一行一个位置」翻成「一列一个位置」,好和 Q 相乘
QK形状 T×T 的方阵,第 (i,j) 个元素是 qi·kj相关度记分板:第 i 个位置对第 j 个位置的原始分数
dk头维度的平方根,Qwen3.8 里 dk=256,所以是 16把分数的量级拉回可控范围,见下文
softmax对矩阵的每一行分别做:先取指数,再除以这一行的和把一行分数变成一组非负、加起来正好是 1 的权重
V值矩阵,形状 T×dv要被搬运的内容
整体结果形状 T×dv,第 i 行 = Σj aij vj每个位置拿到一份「按相关度调配的混合内容」

读这行式子的正确顺序是从里往外:先 QK 得到一张 T×T 的记分板,再除以 √dk 调量级,再逐行 softmax 变成权重,最后用这些权重去加权平均 V 的各行。整个过程里只有三个矩阵是参数,其余全是算出来的。

那个 √d 到底在干什么

这是全式最容易被跳过、也最值得停下来的一步。假设 qk 的每个分量都是独立的、均值 0、方差 1 的随机数,那么它们的点积是 dk 项乘积之和:

Var(q·k) = Σi=1..dk Var(qiki) = dk ⇒ σ = √dk
式 3-3
符号是什么直觉
Var方差。独立随机变量相加时,方差可以直接相加波动的平方
qiki两个独立标准正态数的乘积,均值 0、方差 1一项的贡献
σ = √dk点积的标准差,dk=256 时是 16分数的典型大小是 ±16,不是 ±1

±16 这个量级听起来无害,但 softmax 吃的是指数。两个位置的分数差 16,意味着它们的权重比是 e16 ≈ 890 万倍——第一名会拿走几乎全部的权重,剩下所有位置加起来分不到百万分之一。softmax 就从一个「柔和的排序」塌成了一个「硬性的选择」。

这不只是不好看,后果是梯度消失。softmax 塌成接近 one-hot 时,它对输入的导数几乎处处为 0(softmax 的雅可比矩阵是 diag(p) − pp,当 p 接近某个坐标轴上的单位向量时整个矩阵趋近于零矩阵)。反向传播到这里,梯度就断了,这一层学不动。除以 √dk 把方差拉回 1,分数的典型大小回到 ±1,权重分布就重新变得「有区分但不极端」。

本站数值实验(可自己复现)

dk=256、序列长 8,Q 和 K 的每个分量独立取自标准正态分布,重复两万次:除以 √256 时,一行注意力权重的最大值中位数约 0.34(多数落在 0.2 到 0.5 之间);不除时,最大值中位数约 0.999,约三分之二的试验超过 0.99。这是纯数学结论,与具体模型无关,本章末尾的建造台阶会让你亲手跑出这两个数。

Qwen3.8 的 head_dim 是 256。如果有人把它改成 1024(其他不动),而忘了同步改缩放因子(仍然除以 √256 = 16),点积分数的典型大小会变成多少?softmax 的行为会朝哪个方向偏?

先想:式 3-3 说标准差是 √dkdk 翻了 4 倍,标准差翻几倍?
关键在于:缩放因子是按旧的 256 定的,而分数是按新的 1024 产生的,两者不匹配的倍数就是「过大」的倍数。
第一步这样走:新的点积标准差 = √1024 = 32。除以旧因子 16 之后,剩下多少?
完整答案:√1024 = 32,除以 16 之后典型大小是 ±2,而正确缩放应该给出 ±1。也就是分数被放大了 2 倍。softmax 会朝更尖锐的方向偏——权重更集中在少数几个位置上。2 倍听起来不多,但因为 softmax 是指数的,分数差从 2 变成 4 时权重比从 7.4 倍变成 55 倍。
要点在于:√dk 不是一个可有可无的常数,它的作用是让缩放行为与 dk 解耦——你换 head_dim 时不需要重新调初始化、重新调学习率。这也解释了为什么这个因子写成 √dk 而不是某个手调的超参数。

变式:反过来,如果有人「保险起见」除以 dk 而不是 √dk(也就是除以 256 而不是 16),会发生什么?(提示:分数典型大小变成 1/16,softmax 的输出会趋近于什么分布?这时候梯度还在不在?)

自己推一遍:从「我想要一个加权平均」推到 softmax(QK/√d)V

  1. 目标很朴素:让第 i 个位置从别的位置那儿搬点内容回来。你会先写出一个什么形式的式子?

    想好了再看

    加权求和:oi = Σj aijvj。之所以是加权和而不是别的,是因为它同时满足三个要求:可导(能训练)、和输入长度无关(T 变了式子不用改)、能表达「看谁多一点」这个意图。

  2. 权重 aij 需要满足什么条件?为什么?

    想好了再看

    非负,且对 j 求和等于 1。非负是因为负权重意味着「反着抄」,语义上说不通且会让输出量级失控;和为 1 是为了让输出保持在和 v 差不多的量级——否则位置一多,加起来的东西就爆了。这两个条件正好是 softmax 的输出性质。

  3. softmax 要吃一组实数分数。分数从哪儿来?「两个向量像不像」最便宜的度量是什么?

    想好了再看

    点积。便宜到什么程度:T×T 个分数可以用一次矩阵乘法一口气全算出来,GPU 上这正是最快的操作。余弦相似度要额外做归一化,欧氏距离要开方,都更贵,而且点积保留了模长信息——「这个位置本身有多强的信号」也是有用的。

  4. 拿谁和谁做点积?如果直接用 xi·xj 会怎样?

    想好了再看

    会出两个问题:自己和自己的点积永远最大,于是每个位置永远最关注自己,白算一场;而且「我在找什么」和「我是什么」被迫共用一组数。所以拆出 WQWK 两副眼镜,让分数变成 (xiWQ)·(xjWK)。这一步是整个设计里最关键的一次拆分。

  5. 搬回来的内容用 xj 本身吗?

    想好了再看

    再拆一次,用 vj = xjWV。理由和上一步同源:「凭什么被找到」和「被找到之后给什么」是两件事,上一题的 K=V 那道自评题就是在验这个。

  6. 现在把三步合并写成矩阵形式。逐行的 softmax 怎么表示?

    想好了再看

    A = softmax(QK),约定 softmax 按作用;输出 = AV。注意这里有一个容易写错的地方:softmax 必须逐行做,如果你按整个矩阵做全局归一化,不同位置就会互相抢权重,语义全乱。

  7. 最后一问:dk 变大时,QK 里的数会怎么变?这对 softmax 意味着什么?该怎么补救?

    想好了再看

    方差随 dk 线性增长(式 3-3),标准差是 √dk。softmax 吃指数,所以分数量级一大就塌成 one-hot、梯度消失。补救就是除以 √dk——注意是根号,不是 dk 本身,因为要抵消的是标准差不是方差。整条推导到这里闭合,你手上就是式 3-2。

3.4 亲手算一遍

公式看懂和公式算过是两回事。下面这个实验台把式 3-2 拆成可以逐步观察的几块,建议至少玩三件事。

一,把缩放因子从 1/√d 改成 1,盯着权重分布看它怎么塌。你会看到一条明显的分界:缩放正常时,权重像一座缓坡;去掉缩放,它变成一根针。二,看 QK 的热力图,哪一行亮在哪一列,就是哪个位置在看哪个位置。三,打开和关掉因果掩码,看矩阵的右上角是怎么被抹黑的。

因果掩码causal mask):语言模型干的是「预测下一个词」。训练时整句话是一次性铺开算的,如果第 i 个位置能看到第 i+1 个位置,它就是在抄答案——训练时分数漂亮,真正生成时后面还不存在,立刻崩。所以要把记分板上 j > i 的格子在 softmax 之前设成 −∞,取指数后变成 0,归一化时自动不参与。热力图上就是一个上三角被抹掉、只剩下三角的矩阵。

常见误解:先 softmax 再置零,然后重新归一化,不也一样吗

结果的数值确实一样,但这是个坏习惯,有两个理由。第一,softmax 里那一步 exp 是在所有位置上算的,包括那些本来就不该看的——序列一长,白算的部分是平方级的浪费,而真实实现(FlashAttention 这类)正是靠「不该看的根本不算」来省显存和时间的。第二,先算再抹容易写错顺序,而写错了不会报错:模型照常训练,loss 甚至更低(因为它在抄答案),只有真正生成时才暴露。掩码永远加在 softmax 之前,这条要背下来。

某人实现注意力时,把因果掩码写成了「ji 的位置屏蔽」(本该是 j > i),也就是连自己都看不见。第 0 个位置会发生什么?整个模型还能训练吗?请说明你会观察到的具体现象

先想:第 0 个位置前面没有任何位置。如果连它自己也被屏蔽,那一行还剩下几个可见的格子?
关键在于:softmax 的分母是这一行所有 exp 的和。如果整行都是 −∞,分母是多少?
第一步这样走:手算 softmax([−inf, −inf])。分子是 exp(−inf)=0,分母是 0+0=0。0 除以 0 等于什么?
完整答案:第 0 行全部被屏蔽,softmax 的分子分母都是 0,结果是 NaN(非数)。而 NaN 有传染性:它会顺着后面所有的矩阵乘法、残差连接一路扩散,几步之内整个前向输出全是 NaN,loss 打印出来也是 NaN,梯度全废,模型一步都训不动。
这是一个「好 bug」——它会立刻炸给你看。真正难查的是另外两种写错:(a) 掩码方向反了(屏蔽了 j < i),模型能训、loss 会降得异常快,因为它在光明正大地抄后面的答案,只有生成时才发现胡言乱语;(b) 掩码根本没加,同样能训、loss 更低、生成时崩。判据是:如果你的验证 loss 好得不真实、而自由生成的质量对不上,第一件事就是去查掩码。

变式:如果只把第 0 行的掩码特殊处理成「允许看自己」,别的行仍然屏蔽自己,模型能训吗?这时候每个位置都看不到自己,会丢掉什么能力?(提示:残差连接还在,「自己的信息」有没有别的通路传下去?)

3.5 多头:为什么要分成好几组

到这里,一层注意力给每个位置产生了一组权重。问题是,一句话里同时存在好几种需要追踪的关系。

只有一组权重会怎样

还是那句「小明把书放在桌子上,然后他把它拿走了」。「它」这个位置至少要同时办四件事:找到指代的先行词(书)、知道自己的句法角色(宾语)、看紧邻的动词(拿走)、以及记住整句话的话题。这是四种不同的关注模式,需要四组不同的权重分布。但一次 softmax 只产生一组——你无法用一个概率分布同时表达「主要看书」和「主要看拿走」。硬要一组的话,模型只能把它们混在一起取个折中,四件事都做得半吊子。

多头注意力Multi-Head Attention, MHA):把注意力做 h 遍。每一遍叫一个head),有自己独立的 WQWKWV,各自算出自己的一组权重和自己的输出;最后把 h 个输出拼接起来,再过一个输出投影 WO 混合回主干宽度。

MultiHead(X) = Concat(o1, …, oh)WO, oi = Attention(XWQi, XWKi, XWVi)
式 3-4
符号是什么直觉
h头数。Qwen3.8-27B 的 Q 头数是 24(num_attention_heads同时维持多少种关注模式
WQii 个头自己的查询投影,形状 d×dheadi 个头自己的那副眼镜
ConcathT×dhead 的输出沿列方向拼成 T×(h·dhead)把各头的笔记并排放好
WO输出投影,形状 (h·dheadd把并排的笔记混合成一份,宽度回到主干

有一个关键的会计事实:在传统做法里,多头几乎不额外花参数。因为通常取 dhead = d/hh 个头的投影矩阵拼起来正好还是一个 d×d 的方阵,和单头时一样大。换句话说,多头买到的不是「更多算力」,而是把同样的参数切成几份,让它们能各自形成独立的关注模式。这是一笔非常划算的买卖,也是 Transformer 里最优雅的设计之一。

说「传统做法」是因为 Qwen3.8 恰恰在这里做了两处不常规的偏离:dhead 不等于 d/h,而且 K/V 的头数比 Q 少得多。这两件事,连同一个额外的输出门,就是下一章的全部内容。

一个 d=512 的模型,用 8 个头、每头 dhead=64。请分别算出:(a) 单头(h=1、dhead=512)时 WQ 的参数量;(b) 8 头时全部 8 个 WQi 加起来的参数量。它们相等吗?多头「多」在哪里?

先想:一个 a×b 的矩阵有多少个参数?(第0章)
关键在于:每个头的 WQi 形状是 d×dhead = 512×64,一共有 8 个。
第一步这样走:(a) 512×512 = ? (b) 512×64×8 = ?
完整答案:(a) 512×512 = 262,144。(b) 512×64×8 = 262,144完全相等。多头没有多花一个参数,它只是把同一块 512×512 的权重在列方向上切成 8 段,每段单独做一次注意力。「多」的是注意力分布的数量:从 1 组 T×T 权重变成 8 组,模型可以让第 1 个头盯语法、第 5 个头盯指代,互不干扰。计算量方面,8 次小矩阵乘法的总乘加数和 1 次大的也一样(都是 T×512×512 量级),真正多出来的只有 8 张 T×T 的注意力矩阵——这部分随序列长度平方增长,是长上下文的主要开销之一。

变式:Qwen3.8 的 d=5120、Q 头数 24,但 head_dim 是 256 而不是 5120÷24≈213。按上面的算法,24 个 WQi 拼起来的形状是多少?它比 5120×5120 大还是小?(这道题的答案就是第4章的开场)

把本章的三件事串起来算一笔账:注意力要产生一张 T×T 的记分板;多头意味着有 h 张这样的板;而生成第 T+1 个 token 时,前 T 个位置的 K 和 V 是可以复用的(不用重算)。请回答:(a) 记分板的显存开销随 T 怎么增长?(b) 如果把 K、V 缓存下来,缓存的开销随 T 怎么增长?(c) 为什么工业界宁愿花力气去优化 (a) 而不是干脆不缓存?

先想:T×T 是几次方?T×dk 又是几次方?两者随 T 增长的速度差多少。
关键在于:(c) 问的是「不缓存会怎样」。不缓存的话,每生成一个新 token 都要把前面所有位置的 K、V 重算一遍——那是把一次线性的读取换成了一次完整的重算。
第一步这样走:设 T=100,000。分别写出 T2T×256 这两个数,比一比差几个数量级。
完整答案:
(a) 记分板是 平方级T=100,000 时一张板就有 1010 个数,一个头一张、还要乘头数——单纯把它存下来在任何显卡上都不可能。所以现代实现(FlashAttention 一类)根本不把完整记分板落到显存里,而是分块算、算完即弃,只保留最终的 T×dv 输出。这就是为什么「优化 (a)」值得花力气:它不是省一点,是把不可能变成可能。
(b) K、V 缓存是 线性级:正比于 T × KV 头数 × head_dim × 2。这是第5章的主题,Qwen3.8-27B 的锁定值是每 token 64 KiB(fp16)。线性听着温和,但 262,144 上下文下就是 16 GiB,照样能吃掉一张卡的大半。
(c) 因为不缓存的代价是把线性的存储换成平方级的重复计算:生成第 t 个 token 要重算前 t−1 个位置的 K、V,整段生成下来总计算量从 O(T) 变成 O(T2)。缓存是拿显存换时间,而在自回归生成这个场景里,这笔交易几乎总是划算的——直到上下文长到显存装不下为止,那时才需要第6、7章那种结构性的解法。

变式:如果一个模型的某些层根本不产生 T×T 记分板,而是维持一个固定大小的状态矩阵(不随 T 增长),(a) 和 (b) 两笔账各自会怎么变?这样的层如果占了全部层数的四分之三,整机的 KV cache 会变成原来的几分之几?(这正是 Qwen3.8 的真实结构,答案在第7章)

答辩:如果我是审稿人

softmax 强制每一行权重加起来等于 1,也就是说每个位置必须把注意力全部分出去。可有时候一个词根本不需要看任何别人——比如一句话的第一个词,或者一个纯粹的标点。这不是设计缺陷吗?模型被迫去关注一些它并不想关注的东西。

参考防守(先自己组织语言再看)

这个批评是对的,而且它指出的是一个被反复讨论的真实问题:softmax 没有「弃权」这个选项。实践中模型会自己发明一个变通办法——学出一个几乎不携带语义的位置当垃圾桶,把用不掉的注意力全倒进去,通常是序列开头的那个 token 或某个高频标点。这个现象在开源模型上被广泛观察到,但要诚实:本站没有 Qwen3.8 的注意力可视化数据,无法证明它也这样,只能说这是这一类结构的通用现象。
更值得说的是第二点:Qwen3.8 给了另一条出路。它的注意力层带一个输出门attn_output_gate: true,第4章),门可以把整个注意力输出按通道乘到接近 0——等于说「这一层这一步我不看了」。softmax 内部做不到的弃权,被移到了 softmax 外面来做。所以这个批评不但成立,而且正好指向了下一章那个设计的动机之一。当然,本站同样没有官方说明证实这就是设计动机,这一句是本站的解读。

答辩:如果我是审稿人

你论证「必须除以 √d」的整个推导,前提是 Q 和 K 的分量是独立的标准正态随机数。可真实模型里 WQ 的初始化标准差通常是 0.02 这个量级,投影出来的 q 根本没那么大,点积也到不了 ±16。你这个论证是不是建立在一个不成立的假设上?

参考防守(先自己组织语言再看)

这个批评很尖锐,而且部分成立,必须分三层回答。
第一,承认:在训练刚开始、权重还很小的时候,点积的实际量级确实远小于 √dk,此时缩不缩放差别不大。所以「不除就立刻梯度消失」这个说法在初始化那一刻是夸张的。
第二,但权重会长大。训练过程中 WQWK 的尺度会变化,而且各层各头不一致。1/√dk 的作用不是在某一个时刻救场,而是让「分数量级」这件事在整个训练过程中、以及在不同 dk 之间保持可比。这是一个保险,不是一个补丁。
第三,也是最实在的一层:它让超参数与 dk 解耦。Qwen3.8 把 head_dim 定成 256(而不是常见的 128),如果没有这个因子,你换 head_dim 就得连初始化和学习率一起重调。
最后要诚实划界:本站没有 Qwen3.8 训练时的实际 logits 分布数据,无法量化这个因子在这个具体模型上有多关键。能被验证的只有数学那一半——给定同分布的 Q/K,dk 越大点积方差越大。这一半在本章的实验台里把 d 从 16 拉到 1024 就能亲眼看到,建造台阶里也给了可复现的数字。

真未解注意力权重能不能被当成「模型为什么这么答」的解释

热力图很有说服力:「它」那一行在「书」那一列最亮,看起来模型就是靠这个做的判断。但这个推论有一个跨不过去的缺口——权重大只能说明那条通路被加权得多,不能说明结论依赖它。已有的几种尝试各自都撞了墙:把某个位置的权重强行抹掉看输出变不变(问题是抹掉会同时改变归一化,其余权重全跟着动,改变可能来自扰动本身);找一组不同的权重使输出几乎不变(说明权重不唯一,那么原来那组凭什么算解释);用梯度归因去对照(问题是梯度和权重经常给出不一致的排序,谁对谁错没有判据)。多头和残差连接让情况更糟:同一个信息可以从别的头、或者干脆从残差通路绕过去。到今天为止,这个问题没有公认答案。

先做这一步:随便找一个能导出注意力矩阵的小模型(transformers 里前向传播时传 output_attentions=True 就能拿到全部层、全部头的权重),构造一对最小差异句——「小明把书放在桌子上,然后他把拿走了」和把「书」换成「花瓶」的版本。先做描述性观察:哪些层、哪些头的「它」那一行在先行词那一列最亮。然后做关键的第二步——把先行词换成一个语义完全不同但句法角色相同的词,看那些头的权重跟着变没变。如果换了词权重几乎不动,你就亲手拿到了一个「权重不足以当解释」的具体反例;如果动了,你还需要设计第三个对照来排除「它只是在追句法位置」。把这三步的结果记下来,你会比大多数只看热力图的人更清楚这个问题难在哪。

这一层要加什么:给模型装上第一个单头注意力

为什么现在才加它:第2层之后,你的模型能把 token 变成向量、再变回分数,但每个位置都是孤岛。这一层装上之后,位置之间第一次连通——你会亲眼看到一张 T×T 的权重矩阵被算出来。这一层还只有一个头、没有 GQA、没有 RoPE、没有门,那些都是第4层的事。

def softmax_rows(S): S = S - S.max(axis=-1, keepdims=True) # 减最大值,防 exp 溢出 E = exp(S) return E / E.sum(axis=-1, keepdims=True) def attention(X, Wq, Wk, Wv, scale, causal=True): Q = X @ Wq # (T,d) @ (d,dk) -> (T,dk) K = X @ Wk V = X @ Wv S = (Q @ K.T) * scale # (T,T) 记分板 if causal: S = S + causal_mask(T) # mask[i,j] = 0 若 j<=i,否则 -inf A = softmax_rows(S) # 掩码在 softmax 之前 return A @ V, A # 把 A 也返回,下面要验它 scale = 1.0 / sqrt(dk) # 根号,不是 dk 本身

难点一:scale 是 1/√dk,不是 1/dk。这是本章最高频的错——要抵消的是标准差(√dk),不是方差(dk)。写成 1/dk 不会报错,只会让分数被压扁 16 倍,softmax 输出接近均匀分布,模型退化成「对所有位置一视同仁地取平均」,训得动但学不到东西。

难点二:掩码必须在 softmax 之前用加 −∞ 的方式加进去,不能 softmax 之后置零再归一化。数值上等价,工程上不等价,理由见 3.4 那个提示框。

难点三:softmax_rows 里那句减最大值不是可选的。exp(800) 在 float32 里直接是 inf,然后 inf/inf = NaN。而你马上要做的第一个实验恰恰是「把缩放拿掉」,那正是最容易撞上溢出的场景——不减最大值的话,你看到的不是塌陷,是一屏幕 NaN。

自己验,三条:(1) 缩放的作用。dk=256T=8、关掉因果掩码,Q 和 K 的每个元素独立取自标准正态。scale=1/16 时,跑 200 次,A[-1].max()中位数应落在 0.3 到 0.4 之间(本站两万次实验得 0.34);把 scale 改成 1.0,同样 200 次,中位数应跳到 0.99 以上(本站得 0.999,约三分之二的单次试验超过 0.99)。看到这两个数分开,你就亲手复现了 softmax 塌陷。(2) 长度为 1 的退化情形。T=1,不论 scale 取什么值、掩码开不开,A 必须恒等于 [[1.0]]——只有一个候选,softmax 别无选择。这条不成立说明你的归一化维度选错了(很可能按列而不是按行做了 softmax)。(3) 归一化不变量。任意 T、任意 scale、掩码开启时,A.sum(axis=1) 的每一项都必须等于 1,误差在 1e-12 以内。任何一行不等于 1,说明掩码加错了位置(比如加在了 softmax 之后)。

本章小结

注意力要解决的是一个具体问题:一个词的含义依赖于句子里别的词,而这个依赖的距离不固定。固定窗口够不着,循环网络的固定状态装不下且必须串行。注意力的答案是「不压缩、不设窗口,每个位置自己去看所有位置」。

实现方式是把同一段输入 X 用三个不同的矩阵投影成 Q、K、V——记住它们同源,这是最容易被那个图书馆类比带偏的地方。然后 QK 得到记分板,除以 √dk 把量级拉回来(不除会塌成 one-hot、梯度消失,本站实测最大权重中位数从 0.34 跳到 0.999),逐行 softmax 变成权重,最后加权平均 V。因果掩码在 softmax 之前用 −∞ 加进去。

一组权重只能表达一种关系,所以要多头——而多头的妙处是它几乎不额外花参数,只是把同一块权重切成几份,各自形成独立的关注模式。

下一章,这套通用结构会被换成 Qwen3.8 真正在用的那一版:24 个 Q 头配 4 个 KV 头、head_dim 是反常识的 256、多一个输出门、以及只给四分之一维度加旋转的部分 RoPE。所有这些都能在 cfg27b.json 里一行一行读出来。

第4章 Qwen3.8 的全注意力层长什么样

这一章是全站「读一手材料」的示范章。上一章讲的是教科书上的注意力,这一章我们把 cfg27b.json 摊开,一个字段一个字段读下去,看看真机上那一层到底长什么样——它和教科书至少差了四处,每一处都有明确的工程理由,也有一处官方从未解释。

学完这一章你应该能做到

  • 说清 MHA → MQA → GQA 三者的取舍,并从 num_attention_headsnum_key_value_heads 两个数算出分组比
  • 指出 head_dim: 256 不等于 5120÷24,并说明这意味着注意力工作在比主干更宽的空间里
  • 解释 attn_output_gate: true 为什么让 q_proj 的参数翻倍,并独立复算出这一层的 104.86 M
  • 说清 RoPE 的核心性质,以及 partial_rotary_factor: 0.25 让哪 64 维参与旋转、剩下 192 维在干什么
  • 指出 mrope_section: [11,11,10] 是 27B 独有的,并解释 11+11+10=32 这个数从哪来
前置:第3章的 Q/K/V 投影softmax(QK/√d)V多头,第2章的「注意力天生看不见顺序」。

4.1 GQA:24 个 Q 头配 4 个 KV 头

先把两行 config 摆出来。mat/cfg27b.jsontext_config 里:

"num_attention_heads": 24, ← Q 有 24 个头 "num_key_value_heads": 4, ← K 和 V 只有 4 个头

上一章说多头是「把注意力做 h 遍,每遍有自己的 WQ、WK、WV」。那是 多头注意力Multi-Head Attention, MHA):Q、K、V 头数一样多。Qwen3.8 不是。它的 Q 有 24 个头,K 和 V 只有 4 个——每 6 个 Q 头共用同一组 K 和 V。

不这么做会怎样:为什么要动 K 和 V 的头数

关键在于生成阶段。模型每吐一个字,都要回头看前面所有位置的 K 和 V;为了不重算,这些 K、V 会被缓存下来(KV cache,第5章的主题)。这份缓存的大小正比于 KV 头数,而且随上下文长度线性增长。24 个 KV 头意味着这份缓存要大 6 倍。
更麻烦的是,生成阶段是带宽受限的:每生成一个 token,显卡都要把整份 KV cache 从显存里读一遍,而这份缓存动辄几十 GB。读得越多,等得越久。缩小 KV 头数,等于同时省了显存和带宽——这是唯一一处「改一个数字,两笔账一起降」的地方。

多查询注意力Multi-Query Attention, MQA):走到极端,让全部 Q 头共用一组 K 和 V。缓存直接除以头数。代价是所有头被迫看同一份索引、搬同一份内容,只有提问方式不同,表达能力明显受损。

分组查询注意力Grouped-Query Attention, GQA):折中方案。把 Q 头分成若干组,组内共用一套 K、V,组间各自独立。Qwen3.8-27B 是 24 个 Q 头分 4 组,每组 6 个。

MHA(24 组 KV) MQA(1 组 KV) GQA:Qwen3.8(4 组 KV) 24 个 Q 头 24 组 KV:缓存最大 24 个 Q 头 1 组 KV:最省,最挤 24 个 Q 头 4 组 KV,每组喂 6 个 Q 头 每 token 每层要缓存的元素数 = 2 × KV头数 × head_dim MHA: 2×24×256 = 12,288  MQA: 2×1×256 = 512  Qwen3.8: 2×4×256 = 2,048 Q 头数完全不影响这笔账——缓存里根本不存 Q。
图 4-1:三种注意力在 KV 头数上的差别。示意图,非官方原始数据;三个元素数按 head_dim=256 由本站计算。

注意图里最后那一行:缓存里不存 Q。Q 是当前这个 token 现算的,用完就扔;K、V 才要留给后面的 token 用。所以把 Q 头数做大不花缓存,把 KV 头数做大才花——GQA 正是抓住了这个不对称。

这个 4 直接决定了全站最重要的那个数。27B 有 16 层全注意力(4.6 节会说为什么是 16),每 token 要缓存的元素数是 2 × 16 × 4 × 256 = 32,768 个,fp16 下正好 64 KiB。要是 KV 头也做成 24,这个数会变成 2 × 16 × 24 × 256 = 196,608,也就是 384 KiB——在 262,144 的满上下文下,KV cache 从 16 GiB 涨到 96 GiB。这一个字段的差别,就是「24GB 卡上跑得动」和「跑不动」的差别。

从 config 里的一个 4,到你第 3 天遇到的那个报错

  1. 因为它把 K、V 的头数设成 4 而不是 24(cfg27b.json → text_config → num_key_value_heads),每 token 每层的缓存从 12,288 个元素降到 2,048 个。
  2. 所以在一张 24 GiB 卡上「一边装得下 Q4_K_M 权重、一边留出几 GiB 给上下文」成为可能——按本站的推导,扣掉 Q4_K_M 权重(17.11 GB = 15.93 GiB)、视觉塔(0.93 GB = 0.87 GiB)、DeltaNet 固定态(0.14 GiB)和框架开销(1.0–2.0 GiB)之后,还剩 5.1–6.1 GiB 能给 KV。
  3. 第 3 天你把一份长文档粘进去,前面几万 token 都好好的,粘到某个长度时框架抛出显存不足,或者悄悄把上下文截断、模型开始「忘掉」开头的内容。换算一下:5.1–6.1 GiB ÷ 64 KiB/token ≈ 8–10 万 token,正好卡在那个位置。
  4. 这条链最后落在一个具体的选择上:把 KV 换成 fp8,每 token 从 64 KiB 降到 32 KiB,同一张卡的上下文翻倍到约 17–20 万——代价是缓存精度下降。完整推导见第 5 章(表 5-2),第 17 章会把它铺到别的硬件上。这里给的是区间不是单点值,因为框架开销那一项是本站的经验区间,真实值更靠近下沿。
代价:模型质量付出了一点——4 组 KV 意味着每 6 个 Q 头被迫共用同一份索引和内容,表达力不如 24 组。本站没有 Qwen3.8 的 GQA 消融数据,无法量化这一点掉了多少。

打开 cfg24t.json,找出 2.4T 模型的 num_attention_headsnum_key_value_heads,算出它的分组比(每组几个 Q 头)。和 27B 的 6 比,谁更激进?

先想:分组比 = Q 头数 ÷ KV 头数。
关键在于:两个模型的 KV 头数是不是一样?如果一样,那么分组比的差别完全来自 Q 头数。
第一步这样走:在 cfg24t.json 里搜 num_attention_heads(顶层,不在 text_config 里,因为 2.4T 是纯文本模型没有视觉塔)。
完整答案:2.4T 是 num_attention_heads: 64num_key_value_heads: 4,分组比 64 ÷ 4 = 16,每 16 个 Q 头共用一组 KV。27B 是 24 ÷ 4 = 6。2.4T 更激进
更值得注意的是:两个模型的 KV 头数都是 4,head_dim 都是 256。也就是说每层每 token 的 KV 缓存元素数完全相同(2×4×256 = 2048),两个模型 KV cache 的差别只来自全注意力层数——27B 是 16 层,2.4T 是 23 层。这正是那两个锁定值(27B 每 token 64 KiB、2.4T 每 token 92 KiB)的由来:92 ÷ 64 = 23 ÷ 16。一个 2.4 万亿参数的模型,每 token 的 KV 只比 27B 大 44%。

变式:如果哪天出一个 num_key_value_heads: 2 的版本,27B 每 token 的 KV 会变成多少 KiB?262K 满上下文下是多少 GiB?(提示:这笔账对 4 是线性的)

4.2 head_dim 256:一个反常识的选择

上一章说过一个几乎是默认的约定:dhead = d / h。这样 h 个头拼起来正好等于主干宽度,Q/K/V/O 四个投影都是方阵,多头一分钱不多花。

Qwen3.8 打破了这个约定。hidden_size 是 5120,Q 头数是 24,5120 ÷ 24 = 213.33…——根本除不尽。而 config 里 head_dim 是一个独立写死的字段

"hidden_size": 5120, "num_attention_heads": 24, "head_dim": 256, ← 独立指定,不是 5120/24 算出来的

后果是:24 × 256 = 6144,比 hidden 的 5120 。Q 投影把 5120 维升维到 6144 维(这还没算下一节的门),做完注意力之后 o_proj 再从 6144 降回 5120。也就是说,注意力内部工作在一个比主干更宽的空间里

这在 Transformer 里不常见:通常要么等宽,要么为省参数而更窄。宽 20% 是实打实的额外开销,Q、K、V、O 四张矩阵都跟着变大。

读的时候要小心:这里的「为什么」,官方没有给

本站能确认的只有事实:head_dim: 256 写在 cfg27b.jsoncfg24t.json 里,可以自己核对。官方没有解释为什么选 256——Qwen3.8 没有 arXiv 技术报告(arXiv 上以 Qwen3.8 检索为零结果),没有官方 GitHub 仓库,模型卡里也没有这一段。
本站能给的只有三条旁证,其中第一条是推断而非事实:(1) 256 是 2 的幂,对 GPU 上的分块矩阵乘法友好——这是常识性推断,不是官方说法;(2) 2.4T 用了同一个 256,而它的 hidden 是 8192、Q 头数 64,比例更极端(64×256 = 16384,是 hidden 的整整 2 倍),说明 256 是跨模型复用的模板值,不是 27B 上的一次性选择;(3) 256 × 0.25 = 64,正好是一个整齐的旋转维度(4.4 节)。至于为什么不是 128 或 192,本站没有任何一手答案,也不去替作者编一个。

假设把 27B 的 head_dim 从 256 改成 208(一个接近 5120÷24≈213 且能被 16 整除的数),其他字段不动。o_proj 的参数量会从多少变成多少?变化的比例是多少?

先想:o_proj 的形状是 (Q 头数 × head_dim) × hidden。这三个数里你只动了一个。
关键在于:参数量对 head_dim 是线性的,所以比例就是 208÷256。
第一步这样走:先算原来的:24 × 256 = 6144,6144 × 5120 = ? 再算新的:24 × 208 = 4992,4992 × 5120 = ?
完整答案:原来 6144 × 5120 = 31,457,280(31.46 M);改成 208 之后 4992 × 5120 = 25,559,040(25.56 M)。比例 208 ÷ 256 = 0.8125,也就是省了 18.75%。
再往下想一步:q、k、v 三张矩阵也全都正比于 head_dim,所以整层都会按同一个 0.8125 缩小,104.86 M 变成约 85.20 M。16 层加起来省下约 0.31 B 参数。这个数不算大(约占 27.36 B 的 1.1%),说明 head_dim 256 的代价主要不在参数量上——它更多是一个「注意力内部空间有多宽」的表达力选择。这也是为什么本章末尾的研究课题值得做:代价小,收益不明,正好是一个能被实验测出来的问题。

变式:head_dim 还会影响 KV cache 吗?把 256 改成 208,每 token 的 KV 从 64 KiB 变成多少?(提示:回去看图 4-1 最后那个公式,head_dim 在不在里面)

4.3 输出门:Gated Attention 的「Gated」在哪

config 里还有一行,这一行是「Gated Attention」这个名字的来历:

"attn_output_gate": true, "output_gate_type": "swish",
不加门会怎样

标准注意力算完 softmax(QK/√d)V 之后就直接过 o_proj 送回主干。问题是:它没有「这一步我不看了」这个选项。softmax 强制每一行权重加起来等于 1,所以哪怕这个位置根本不需要从别处取任何东西,它也必须把注意力全部分出去,然后把分回来的东西加进残差流。上一章的第一条答辩就是在追这件事。
门就是给这条输出通路装一个可学的水龙头:另算一路信号,压到 0 到 1 之间,逐元素乘到注意力输出上。接近 0 就是关掉这个通道,接近 1 就是全量放行。softmax 内部办不到的弃权,被移到 softmax 外面办了。

输出门output gate):一路和 Q 同样宽的信号 g,经过激活函数后逐元素乘到注意力输出上。output_gate_type: "swish" 说明用的激活是 swish。它的参数不是另开一张矩阵来的,而是q_proj 的输出宽度直接翻倍——前一半当 Q,后一半当门。

所以 q_proj 的形状是 5120 × (24 × 256 × 2) = 5120 × 12288。整层的账现在可以完整算出来了:

Nattn = d·2nqdh + 2dnkvdh + nqdh·d (依次是 q_proj、k_proj+v_proj、o_proj)
式 4-1
符号是什么直觉
dhidden_size = 5120主干有多宽
nqnum_attention_heads = 24提问的头有几个
nkvnum_key_value_heads = 4被查的资料架有几组
dhhead_dim = 256每个头有多宽
q_proj 里的那个 2来自 attn_output_gate: true一半是 Q,一半是门
k_proj+v_proj 前的 2是 K 和 V 两张矩阵,不是门别和上面那个 2 混了
表 4-1:Qwen3.8-27B 一层 Gated Attention 的完整参数账(数值取自 mat/paramcount.out.txt
矩阵形状参数量占本层
q_proj(含门)5120 × 1228862,914,560(62.91 M)60.0%
k_proj5120 × 10245,242,880(5.24 M)5.0%
v_proj5120 × 10245,242,880(5.24 M)5.0%
o_proj6144 × 512031,457,280(31.46 M)30.0%
合计104,857,600(104.86 M)100%
一句话记住:一层 Gated Attention 是 104,857,600 个参数——正好是 100 × 220,一个刚好整齐的巧合,拿它当记忆锚点。其中 60% 花在 q_proj 上,而这 60% 里有一半是门。
常见误解:Qwen3.8 里的两种「门」不是一回事

这台机器里有两个都叫 gate 的东西,很容易混。这一章的门是 Gated Attention 的输出门,作用在注意力输出上,来源是 attn_output_gate: true,只出现在那 16 层全注意力层里。另一个门在 Gated DeltaNet 里(第6章),作用在循环状态的写入与遗忘上,出现在另外 48 层。两者名字像、位置不同、作用完全不同。第8章的 FFN 里还有第三种门(SwiGLU 的门)。看到 gate 这个词,先问一句它作用在什么东西上。

自己推一遍:只看 config,数出一层 Gated Attention 有多少参数

  1. q_proj 的输入维度是多少?去哪个字段找?

    想好了再看

    text_config.hidden_size = 5120。因为这一层的输入来自残差流,而残差流全局等宽(第2章的推导线第 1 步)。

  2. 先不管门,q_proj 的输出维度是多少?它为什么不是 5120?

    想好了再看

    24 个头 × 每头 256 维 = 6144。不是 5120,是因为 head_dim 在 config 里是一个独立字段,而不是 hidden÷头数 的计算结果——这正是 4.2 节那个反常识的地方。当初为什么会想到去查这个字段?因为 5120÷24 除不尽,一旦你算了这个除法,就必须回去看 config 到底写了什么。

  3. 现在把门算进来。attn_output_gate: true 让输出宽度变成多少?q_proj 有多少参数?

    想好了再看

    翻倍到 12288。参数 = 5120 × 12288 = 62,914,560。注意这里的「翻倍」是加宽输出,不是新建一张矩阵——两种记法参数量一样,但只有加宽这一种能解释 config 里为什么没有单独的 gate 权重字段。

  4. k_proj 呢?它的头数和 Q 一样吗?

    想好了再看

    不一样。num_key_value_heads = 4,所以输出是 4 × 256 = 1024,参数 = 5120 × 1024 = 5,242,880v_proj 同理,再一份 5,242,880。K 和 V 不带门——门只加在输出通路上。

  5. o_proj 的输入维度是 6144 还是 12288?为什么?

    想好了再看

    6144。这是整条推导里最容易错的一步。门是逐元素乘上去的,它不改变张量的宽度:24 个头的注意力输出拼起来是 6144 维,乘上一个同样 6144 维的门之后,还是 6144 维。那个 12288 只存在于 q_proj 的输出端,一劈两半之后就消失了。所以 o_proj = 6144 × 5120 = 31,457,280

  6. 加起来。和 mat/paramcount.out.txt 对一下。

    想好了再看

    62,914,560 + 5,242,880 + 5,242,880 + 31,457,280 = 104,857,600,即 104.86 M,和复算输出里那一行完全一致。你刚才只用了 config 里的四个字段(hidden_size、num_attention_heads、num_key_value_heads、head_dim)加一个布尔开关,就独立复现了一个官方没有公布的数。

  7. 最后一步:这一层在 27B 里出现几次?总共多少参数?占全模型多少?

    想好了再看

    full_attention_interval: 4num_hidden_layers: 64,所以 64 ÷ 4 = 16 层。16 × 104,857,600 = 1,677,721,600,即 1.68 B,占 27.36 B 的 6.1%——和 mat/paramcount.out.txt 那张占比图对上。整个模型里,「注意力」这个大家以为是灵魂的东西,只占 6.1% 的参数,比第2章那两张查找表(9.3%)还少。

有人把输出门实现成「先过 o_proj,再乘门」。请说明这个实现为什么跑不起来,并进一步说明:就算把形状凑对(比如把门也投影成 5120 维),它在语义上和正确实现有什么本质区别?

先想:o_proj 的输出是多少维?门是多少维?逐元素相乘要求两者相等。
关键在于:门的宽度是从 q_proj 输出的后一半来的,也就是 24×256 = 6144;而 o_proj 的输出是 hidden = 5120。
第一步这样走:把两个数写出来对比——6144 和 5120。逐元素乘法能不能做?
完整答案:跑不起来的原因是形状不匹配:门是 6144 维(q_proj 输出 12288 劈一半),o_proj 的输出是 5120 维,逐元素相乘会直接报 shape 错误。这个报错其实是好事——它逼你去想门到底该在哪一步。
就算凑对形状,语义也变了:正确实现里,门作用在 拼接后、混合前 的 6144 维上,而这 6144 维是按头切好的(每 256 维属于一个头)。所以门可以做到「第 3 个头这一步关掉、第 7 个头全开」这种按头、按通道的精细开关。一旦放到 o_proj 之后,各头的信息已经被 o_proj 混成 5120 维的残差流表示了,门只能对混合后的结果整体加权,再也分不开是哪个头贡献的。
要点:config 只告诉你门存在,不告诉你它在哪一步——这一步是靠形状推出来的。读一手材料常常是这样:字段给的是约束,不是答案。

变式:如果把门改成只有 24 维(每个头一个标量,而不是每个通道一个数),参数量会变成多少?表达能力损失在哪里?(提示:先算 5120 × (6144+24) 和 5120 × 12288 的差)

4.4 部分 RoPE:只给四分之一的维度加旋转

第2章留了个尾巴:注意力天生看不见顺序,位置信息必须单独注入。Qwen3.8 用的是 RoPE,而且用法很特别。先把原理讲清楚。

旋转位置编码Rotary Position Embedding, RoPE):不往向量上加东西,而是转它。把 q 向量相邻的两个维度看成平面上的一个点 (x, y),位置 m 就把这个点绕原点旋转 mθ 弧度;k 也照做。整个 drot 维就是 drot/2 对这样的平面点,每对用不同的转速。

它的核心性质是一句几何常识:两个转过的向量做点积,转的角度会相减。

Rmq, Rnk⟩ = ⟨Rm−nq, k⟩  θi = base−2i/drot
式 4-2
符号是什么直觉
Rm把每一对维度旋转 mθi 的旋转矩阵,不含任何参数一把按位置刻好角度的扳手
mn两个 token 的相对距离点积只关心「隔多远」,不关心「各自在第几位」
θii 对维度的转速,i 从 0 数到 drot/2−1第 0 对转得最快,越往后越慢
baserope_theta,27B 是 10,000,000决定最慢那一对有多慢
drot参与旋转的维度数,Qwen3.8 是 64(见下)256 维里只有 64 维在转

这一句「角度相减」就是 RoPE 全部的价值:注意力分数自动只依赖相对位置,而不需要模型去记住绝对位置。而且 R 是一个纯函数——RoPE 一个参数都不带,这就是为什么表 4-1 里没有它的份。

多个转速:为什么不能只用一个角度

如果所有维度都用同一个 θ,位置每增加 2π/θ 向量就转回原处,模型分不清位置 5 和位置 5+2π/θ。所以 RoPE 用一整套从快到慢的转速:快的那几对对近距离敏感,慢的那几对要几万个位置才转小半圈。rope_theta 就是控制这个速度谱有多宽的底数。

Qwen3.8 的 rope_theta10,000,000(1e7),比常见的 10,000(1e4)大三个数量级。按式 4-2,在 drot=64 时,最慢那一对的转速是 base−62/64:base=1e4 时约 1.33×10−4 弧度每位置,转一整圈要约 4.7 万个位置;base=1e7 时约 1.66×10−7,转一圈要约 3800 万个位置(本站按式 4-2 计算)。原生上下文 262,144、外推目标 1,010,000,都远在一圈之内——大 base 就是为长上下文准备的。

只转四分之一

现在是这一节真正特别的地方。config 里:

"partial_rotary_factor": 0.25, ← head_dim 256 × 0.25 = 64 维参与旋转 "rope_theta": 10000000, (在 text_config.rope_parameters 里)

head_dim 是 256,但只有 64 维(32 对)参与旋转,剩下 192 维原封不动地穿过去,不带任何位置信息。

为什么要留一部分不转

转过的维度是有代价的:两个 token 距离越远,这些维度上的贡献衰减得越厉害(快速旋转的那几对早就转乱了,点积平均下来趋近于 0)。也就是说,转过的维度天然偏向近处。而语言里有大量与距离无关的需求——「全篇有没有提到过苹果」「这段话的主题是什么」,这些不该因为隔得远就被削弱。
把 256 维劈成 64 + 192,等于在同一个头里同时开了两条通道:64 维的位置敏感通道负责「看它前面第 3 个词」这类局部句法,192 维的位置无关通道负责纯内容检索,不管隔多远都不衰减。模型自己去学哪件事走哪条通道。

本站数值实验:那 192 维确实是一条不衰减的通道

取 head_dim 256、base 1e7,用同一个随机向量分别当 q 和 k(模拟「同一个内容出现在两个位置」),看它们隔 1000 个位置时的点积还剩多少:只转 64 维时保留约 91%全部 256 维都转时只剩约 59%。隔 10000 个位置时分别是约 83% 和约 43%。所以部分 RoPE 不是省事,是真的改变了远距离上的行为。这个实验在本章末尾的建造台阶里可以自己跑。

partial_rotary_factor 从 0.25 改成 0.5,(a) 这一层的参数量变成多少?(b) 参与旋转的维度变成多少?(c) 两个相距很远的 token 之间的注意力分数,会更容易还是更不容易保持住?

先想:表 4-1 里有 RoPE 这一行吗?R 矩阵里的角度是训练出来的还是算出来的?
关键在于:RoPE 是纯函数,改它的比例不新增也不减少任何一个要存储的数。变的只有「多少维带位置、多少维不带」。
第一步这样走:(b) 256 × 0.5 = ? 然后想:不带位置信息的维度从 192 降到多少?这条不衰减的通道是变宽了还是变窄了?
完整答案:(a) 一个参数都不变,还是 104,857,600。RoPE 不带参数,旋转角度全部由位置和 base 算出来。(b) 256 × 0.5 = 128 维参与旋转,位置无关的通道从 192 维缩到 128 维。(c) 更不容易保持住。因为不衰减的那条通道变窄了(192→128),而衰减的那条变宽了(64→128),远距离上被保留的分数比例会下降。本站的数值实验给了两端的参照:只转 64 维时距离 1000 处保留约 91%,全转(256 维)时只剩约 59%,0.5 会落在两者之间。
这道题的要点是把两笔账分开:改这个字段动的是行为,不是参数量。config 里不是每个数字都对应存储开销。

变式:那 rope_theta 呢?把它从 10,000,000 改回 10,000,(a)(b)(c) 三问的答案分别怎么变?(提示:(b) 的答案不变,(c) 要回去看最慢那一对转一圈需要多少个位置,以及 262,144 是不是还在一圈之内)

4.5 mRoPE:27B 独有的那一段

把两份 config 并排放,会看到一处只在 27B 出现的东西:

表 4-2:两个模型在 RoPE 相关字段上的差异(逐字取自 mat/cfg27b.jsonmat/cfg24t.json
字段Qwen3.8-27BQwen3.8-2.4T-A95B
head_dim256256
num_key_value_heads44
partial_rotary_factor0.250.25
rope_theta10000000本站手上的 config 摘录里未列出
mrope_interleavedtrue没有这一项
mrope_section[11, 11, 10]没有这一项
为什么 27B 需要多一套位置编码

因为 27B 是原生多模态:输入里除了文字,还有图像和视频切出来的图块。一段文字的位置可以用一个整数表示(第 1 个字、第 2 个字……),但一张图上的图块不是排成一条线的——它有;视频还多一个时间。用一个整数 m 你没办法表达「这个图块在第 3 帧、第 5 行、第 7 列」。硬要拉成一条线的话,本来上下相邻的两个图块可能在一维坐标上隔了一整行的距离,模型学到的空间关系就是错的。

多模态旋转位置编码multimodal RoPE, mRoPE):把参与旋转的那些维度对分成几段,每一段吃一个坐标轴。27B 的 mrope_section[11, 11, 10]——三段,长度分别是 11、11、10 对。

这三个数加起来是 32。这不是巧合:head_dim 256 × partial_rotary_factor 0.25 = 64 维参与旋转,而 RoPE 是成对使用的(每两维凑成一个平面点),64 ÷ 2 = 32 对mrope_section 把这 32 对切成三份,正好分完。你可以把这条当成一个自检:如果哪天看到一份 config 里三段和不等于 head_dim × partial_rotary_factor ÷ 2,那多半是抄错了。

读的时候要小心:这里本站只能读出一半

能确认的是事实层:mrope_section: [11,11,10]mrope_interleaved: true 写在 cfg27b.jsonrope_parameters 里;11+11+10=32 与 64÷2 的对应关系是算出来的。
本站不能确认的有两处。第一,哪一段对应哪个轴。config 只给了三个数字和它们的顺序,没有写字段名。按这一类模型的常见约定推测是「时间、高、宽」,但这是本站的推断,材料里没有依据。第二,mrope_interleaved: true 具体怎么交错。这个开关说明三段不是简单地按顺序切成连续三块,而是交错排布,但具体的排布规则要看实现代码,模型卡没有说明,本站没有核实。
另外,纯文本 token 在 mRoPE 下怎么处理(常见做法是三个轴取同一个值,退化成普通 RoPE),本站同样未能从材料核实 Qwen3.8 的具体做法。

不管细节如何,有一件事是确定的:这是「稠密 vs MoE」之外,两个模型的又一处真实架构差异,而且它的来源不是「谁大谁小」,是「谁要处理图像」。全站的主线到这里又多了一个证据——27B 和 2.4T 不是同一个模型的两个尺寸档,它们连位置编码都不一样。

你在网上看到一份声称是「Qwen3.8-27B 简化版」的 config,里面写着 head_dim: 128partial_rotary_factor: 0.25mrope_section: [11, 11, 10]。不查任何资料,你能立刻判断它有问题吗?说明你的判据。

先想:mrope_section 那三个数加起来,应该等于什么?
关键在于:参与旋转的维度数 = head_dim × partial_rotary_factor,而 RoPE 成对使用,所以段数之和应该是这个数的一半。
第一步这样走:算 128 × 0.25 = ? 再除以 2 = ? 和 11+11+10 比一比。
完整答案:有问题。128 × 0.25 = 32 维参与旋转,成对使用就是 16 对;而 mrope_section 三段之和是 11+11+10 = 32 对,是可用对数的两倍,分不完。这份 config 自相矛盾。
更可能的情况是:写这份 config 的人把 head_dim 改小了,却直接把原版的 mrope_section 抄了过来——这是二手资料里最常见的一类错误。
这道题真正想教的是一个习惯:config 里的字段之间有算术约束,可以互相校验。本站在这一章至少给了你三条可用的校验式:(1) mrope_section 之和 = head_dim × partial_rotary_factor ÷ 2;(2) 全注意力层数 = num_hidden_layers ÷ full_attention_interval;(3) 每层参数量 = 式 4-1。拿到任何一份声称是某模型的 config,先跑这三条,能筛掉相当一部分假货和抄错。

变式:再看一份,写着 num_hidden_layers: 64full_attention_interval: 3,同时又说「16 层全注意力 + 48 层线性注意力」。哪里对不上?正确的层数拆分应该是多少?

4.6 这一层在 27B 里只出现 16 次

最后一个字段,也是把这一章接到后面几章的那一个:

"num_hidden_layers": 64, "full_attention_interval": 4, ← 每 4 层才有 1 层是全注意力

64 ÷ 4 = 16。这一整章讲的东西,在 Qwen3.8-27B 里只出现 16 次。另外 48 层走的是完全不同的路——Gated DeltaNet,一种维持固定大小状态的线性注意力(第6章)。整机的布局是 16 次重复的「3 层 DeltaNet + 1 层全注意力」,每层后面各跟一个 FFN。

把账摆出来看比例:

表 4-3:27B 的参数去了哪里(占比取自 mat/paramcount.out.txt
部件参数量占 27.36 B在哪一章
FFN(全部 64 层)62.6%第8章
Gated DeltaNet(48 层)20.3%第6章
嵌入 + 输出头2.54 B9.3%第2章
Gated Attention(16 层)1.68 B6.1%本章
视觉塔460.32 M1.7%第11章

注意力占 6.1%,比查表的 9.3% 还少。这不是说它不重要——16 层全注意力承担了整台机器唯一的「精确回看任意位置」的能力,第7章会讲为什么把它砍到 0 层不行。但从参数账上看,它确实是个小部件。而它真正昂贵的地方不在参数,在推理时的显存:那 16 这个数会原样出现在 KV cache 的公式里,也就是下一章的主题。

只给你四个数字:num_hidden_layers: 64full_attention_interval: 4num_key_value_heads: 4head_dim: 256。请完整推出:(a) 27B 每 token 的 KV cache 是多少 KiB(fp16);(b) 262,144 满上下文下总共多少 GiB;(c) 网上很多显存计算器把这个数算成 4 倍,它们错在哪一个字段上;(d) 如果把 num_key_value_heads 从 4 改成 24,(a)(b) 分别变成多少。

先想:KV cache 里存的是 K 和 V 两样东西,每一层每一个 KV 头都要存一份 head_dim 长的向量。哪些层需要存?
关键在于 (c):不是所有层都有 KV cache。只有全注意力层才缓存 K、V;线性注意力层维持的是固定大小的循环状态,不随序列长度增长。所以公式里该用的是 16,不是 64。
第一步这样走:写出元素数 = 2 × 全注意力层数 × KV头数 × head_dim。先算全注意力层数 = 64 ÷ 4 = 16,代进去:2 × 16 × 4 × 256 = ? 然后 fp16 每个元素 2 字节。
完整答案:
(a) 2 × 16 × 4 × 256 = 32,768 个元素,fp16 每个 2 字节 = 65,536 字节 = 64 KiB/token
(b) 64 KiB × 262,144 = 16,777,216 KiB = 16.00 GiB
(c) 错在层数上:它们用 num_hidden_layers = 64 代替了全注意力层数 16,得到 256 KiB/token、262K 下 64 GiB,整整高估 4 倍。根源是把 Qwen3.8 当成了普通的稠密 Transformer——而它 64 层里有 48 层根本不产生 KV cache。这是全站最有价值的一个纠错点,第5、7章会把它讲透。
(d) 元素数变成 2 × 16 × 24 × 256 = 196,608,即 384 KiB/token;262K 满上下文 = 96.00 GiB。也就是说,光是 num_key_value_heads 这一个字段,就决定了这个模型在 262K 上下文下需要 16 GiB 还是 96 GiB 的缓存——单卡能不能跑,全看这一个数。
这道题把本章的四个字段串成了一条完整的因果链:架构参数 → 缓存公式 → 显存 → 你的卡跑不跑得动。

变式:拿同样四个数去算 2.4T(num_hidden_layers: 92full_attention_interval: 4、KV 头 4、head_dim 256),每 token 多少 KiB?和 27B 的比值是多少?这个比值和「2.4T 的总参数是 27B 的 88 倍」这件事对得上吗?说明这意味着什么。

答辩:如果我是审稿人

你说 GQA 是为了省显存。可从参数账上看,把 KV 头从 24 砍到 4,每层只省了 (31.46−5.24)×2 ≈ 52.4 M,16 层加起来 0.84 B,占 27.36 B 的 3.1%。为了这 3% 就牺牲表达能力,这笔账划算吗?

参考防守(先自己组织语言再看)

这个批评的算术完全正确,但它算错了 GQA 在省什么。GQA 省的不是参数,是推理时的显存和带宽。这两笔账的性质完全不同:
参数是常数项,算一次就固定在那儿,0.84 B 在 BF16 下是 1.68 GB,确实不多。
KV cache 是线性项,随上下文长度增长。同样这个改动,在 262,144 满上下文下是 16 GiB 对 96 GiB——差 80 GiB,不是 3%,是 6 倍。一张 80GB 的卡,一个装得下、一个装不下。
还有第三笔账,比显存更少被提到:带宽。生成阶段是访存受限的,每吐一个 token 都要把整份 KV cache 读一遍。6 倍的缓存意味着 6 倍的读取量,也就是大致 6 倍的解码延迟——这一条在长上下文下比显存更早撞墙。
所以正确的表述是:GQA 用一点表达能力,换的是「线性项的系数除以 6」,而参数上省的那 0.84 B 只是顺带。至于牺牲了多少表达能力,本站没有 Qwen3.8 的 GQA 消融数据,无法量化——只能指出这个取舍存在,不能说它一定划算。

答辩:如果我是审稿人

你花了一整节讲 head_dim: 256 有多「反常识」,可你自始至终没给出任何证据说明这样更好。它完全可能只是一个没调好的超参数,或者某次实验遗留下来的值。你凭什么把它当成一个「设计选择」来讲?

参考防守(先自己组织语言再看)

这个批评完全成立,必须承认,而且必须修正措辞。本站能拿出的全部证据只有三条,一条都不能证明「更好」:
(1) head_dim 在 config 里是一个显式独立的字段,不是 hidden÷头数 的计算结果——这只能说明它是被特意写下来的,不能说明写得对。
(2) 2.4T 用了同一个 256,而它的 hidden 是 8192、Q 头 64,比例更极端(16384÷8192 = 2.0,27B 是 6144÷5120 = 1.2)。同一个值出现在两个规模差 88 倍的模型上,说明它是跨模型复用的模板值,不是一次手滑。
(3) 这套架构的谱系可以往上追(设计源头是 Qwen3-Next,代码谱系是 Qwen3.5,config 里 model_type 就写着 qwen3_5),说明它至少经过了几代复用。
至于「更好」——本站没有任何消融实验数据,官方也没有发技术报告,这个判断做不了。所以正确的表述应该是:「这是一个反常识的、被两个模型共同采用的选择」,而不是「这是一个更好的选择」。审稿人这一刀切在了本站的措辞上,接受,正文里的 caution 框就是为这一刀留的。
顺带说,这个空缺正好是一个可做的实验,见本章的研究课题。

真未解把 head_dim 和 hidden_size 解耦,到底买到了什么

教科书里 dhead = d/h 几乎是一条不成文的规矩,但它并没有理论依据——它只是让参数量刚好不变而已。Qwen3.8 两个模型都把这两个数解耦了,而且方向一致:注意力内部比主干更宽(27B 是 1.2 倍,2.4T 是 2.0 倍)。问题是,这条「更宽」的收益曲线长什么样?在什么规模上开始有收益?收益来自更强的表达力,还是仅仅来自「256 是 2 的幂,kernel 跑得快」这种工程原因?
这个问题目前没有公认答案:厂商侧没有公开消融(Qwen3.8 没有技术报告),学界侧的注意力结构研究大多把 dhead = d/h 当默认设置,很少把它当成一个独立的可扫参数。所以它既不在材料的边界之外,也不在某个已知答案的抽屉里——它是真的没被系统回答过。

先做这一步:在本章末尾建造台阶 4 的代码里,把 head_dim 依次设成 128、192、256、320,其余字段一律不动。第一步只做算术:把四种设置下这一层的参数量列成一张表(用式 4-1),你会发现它对 head_dim 是严格线性的——这一步就把「参数量」这个混淆因素排除了,因为你可以按参数量对齐再比。第二步做实验:在同一份玩具语料(几 MB 文本就够)上各训 2000 步,记录验证 loss,画一条「loss 对 head_dim」的曲线。如果 256 那个点明显不在拐点上,你就复现了这条答辩里的疑问;如果它恰好在拐点附近,你就摸到了一条别人没写下来的经验规律。两种结果都值得记下来——现在这条曲线在公开材料里根本不存在。

这一层要加什么:把单头注意力升级成 Qwen3.8 的 Gated Attention

为什么现在才加它:第3层那个单头注意力是教科书版本,跑得通但和真机不一样。这一层把四处差异一次补齐——GQA、head_dim 256、输出门、部分 RoPE。补完之后,你手上这一层的参数量应该正好等于 104,857,600,和 mat/paramcount.out.txt 一个数不差。这是全站第一次「造出来的东西和真机对上号」。

n_q, n_kv, d_h, d = 24, 4, 256, 5120 # 全部从 cfg27b.json 读 rot = int(d_h * 0.25) # partial_rotary_factor -> 64 Wq = param(d, n_q * d_h * 2) # 5120 x 12288(一半 Q,一半门) Wk = param(d, n_kv * d_h) # 5120 x 1024 Wv = param(d, n_kv * d_h) # 5120 x 1024 Wo = param(n_q * d_h, d) # 6144 x 5120 qg = X @ Wq q, g = split_last(qg, 2) # 各 (T, 24, 256) k = reshape(X @ Wk, T, n_kv, d_h) # (T, 4, 256) v = reshape(X @ Wv, T, n_kv, d_h) q[..., :rot] = rope(q[..., :rot], pos, base=1e7) # 只转前 64 维 k[..., :rot] = rope(k[..., :rot], pos, base=1e7) # 后 192 维原样穿过 k = repeat(k, n_q // n_kv, axis=1) # 4 -> 24,注意是 repeat v = repeat(v, n_q // n_kv, axis=1) o = attention(q, k, v, scale=1/sqrt(d_h), causal=True) # 复用第 3 层 o = o * swish(g) # 门在这里,o_proj 之前 return reshape(o, T, n_q * d_h) @ Wo # (T, 6144) @ (6144, 5120)

难点一,门乘在哪一步:必须乘在注意力输出 o 上、o_proj 之前。判据是形状——门是 6144 维,o_proj 的输出是 5120 维,乘在后面根本对不上。config 只告诉你门存在,不告诉你它在哪一步,这一步是靠形状推出来的。

难点二,也是最阴险的一个:把 4 组 KV 扩成 24 组时必须用 repeat,不是 tilerepeat([a,b,c,d], 6) 给的是 aaaaaabbbbbbcccccc dddddd,第 0 到 5 号 Q 头共用第 0 组 KV,这才是 GQA 的分组语义;tile 给的是 abcdabcd…,形状一模一样、代码照跑、loss 照降,但分组关系全错了。这个 bug 不会报错、不会崩、也不会在小规模实验里明显表现出来,只能靠断言抓。

难点三,RoPE 只转前 64 维:q[..., :rot] 那个切片写成 q[..., :d_h] 同样不报错,只是把部分 RoPE 悄悄变成了全 RoPE。参数量一模一样,远距离行为却变了——这正是下面第 2 条判据要抓的。

自己验,三条:(1) GQA 改 KV 头数,看参数量。num_key_value_heads 从 4 改成 24,重新统计这一层的参数总数:应该从 104,857,600(104.86 M)涨到 157,286,400(157.29 M)——k_projv_proj 各从 5,242,880(5.24 M)变成 31,457,280(31.46 M),而 q_proj(62,914,560)和 o_proj(31,457,280)一个数都不动。如果 q 或 o 也变了,说明你把 head_dim 或 Q 头数一起改了。(2) 部分 RoPE:参数不变,行为要变。partial_rotary_factor 从 0.25 改成 1.0,参数量必须还是 104,857,600,一个都不变(RoPE 不带参数)。但行为必须变:取一个随机的 256 维向量同时当 q 和 k,分别放在位置 0 和位置 1000 上算注意力分数,再和两个都放在位置 0 时的分数相比——0.25 时应保留约 91%1.0 时掉到约 59%(本站实测值,base=1e7)。参数没变而这个比例变了,就说明部分 RoPE 真的接上了。(3) repeat 还是 tile。扩展完 KV 之后检查:k[:, 0]k[:, 5] 必须逐位完全相等(同一组内的 6 个 Q 头共用),而 k[:, 0]k[:, 6] 必须不相等(跨组)。两条同时成立才是 repeat;如果 k[:, 0]k[:, 4] 相等,你写成了 tile

本章小结

这一章把 cfg27b.json 逐字段读了一遍,教科书上的注意力和 Qwen3.8 真机上的那一层,差了四处:

GQA:24 个 Q 头配 4 个 KV 头(num_attention_heads / num_key_value_heads),每 6 个 Q 头共用一组 KV。省的不是参数(只有 3.1%),是 KV cache 和访存带宽——262K 上下文下是 16 GiB 与 96 GiB 的差别。head_dim 256:不等于 5120÷24,24×256 = 6144 比 hidden 还宽,注意力工作在一个更宽的空间里;官方没解释为什么是 256,本站只报告事实。输出门attn_output_gate: trueq_proj 输出翻倍到 12288,一半是 Q 一半是门,这就是「Gated Attention」的来历;整层 q 62.91 M + k 5.24 M + v 5.24 M + o 31.46 M = 104.86 M部分 RoPEpartial_rotary_factor: 0.25,256 维里只有 64 维参与旋转,剩下 192 维是一条不带位置、不随距离衰减的通道;rope_theta 是 1e7,比常见的 1e4 大三个数量级,为长上下文准备。

再加上一处 27B 独有的:mRoPEmrope_section: [11,11,10],三段和正好是 64÷2=32),因为它要给图像和视频的行、列、时间各留一个位置轴——2.4T 是纯文本,没有这一项。

最后,full_attention_interval: 4 意味着 64 层里只有 16 层是这一章讲的东西,占全模型参数的 6.1%。那另外 48 层是什么?为什么 16 这个数会原样出现在显存公式里?下一章开始回答。

第5章 KV cache:推理为什么越来越慢

这一章回答三件事:为什么每生成一个词都要把前面所有词重算一遍(以及怎么不重算)、这份「不重算」的代价具体是多少字节、以及为什么全网几乎所有 Qwen3.8 显存计算器在这一项上都高估了整整 4 倍。

学完这一章你应该能做到

  • 用自己的话说清:不缓存的总计算量为什么是 n2 量级,缓存之后为什么变成 n 量级
  • 不看资料写出每 token KV 显存的公式,并说清公式里四个因子各自从哪来
  • 算出 Qwen3.8-27B 在 262K 上下文下的 KV 显存,并解释为什么它不是 64 GiB 而是 16 GiB
  • 看到任何一篇写「24GB 显卡流畅运行 Qwen3.8-27B」的文章,能立刻问出正确的追问:上下文开到多少?KV 用什么精度?
前置:第0章的「浮点数怎么占字节」,第3章的「注意力:Q 去查 K,取回 V」,第4章的 GQA 24 个 Q 头对 4 个 KV 头、head_dim 256。

5.1 不缓存会怎样

先问:不用它会怎样

大模型生成文字是一个字一个字往外蹦的。写第 1 个词,看输入;写第 2 个词,要看输入加第 1 个词;写第 3 个词,要看输入加前 2 个词……每多写一个词,要「回头看」的东西就多一份。

问题在于:第 3 步回头看第 1 个词的时候,第 1 个词的 KV 在第 2 步就已经算过一次了。如果每一步都从头把所有词的 K、V 重算一遍,就是在把同一份工作反复做。

把这笔账摊开算。第 3 章讲过,注意力要用三样东西:查询 Q、钥匙 K、内容 V。它们都是把每个 token 的向量乘上一张权重矩阵得来的。生成第 t 个 token 的时候,你需要的是这一个新 token 的 Q,和前面全部 t 个 token 的 K、V

不缓存的话,第 1 步算 1 个 token 的 K、V,第 2 步算 2 个,第 3 步算 3 个……第 n 步算 n 个。总计算量是 1+2+3+…+n,也就是高中数学里那个等差数列求和:n(n+1)/2

代具体数字:生成 1000 个 token,不缓存要做 1000×1001÷2 = 500,500 次 token 级的 K、V 投影;缓存的话只要 1000 次。差 500 倍。而且这个倍数会随长度继续拉大——如果一口气生成到 262,144 个 token(Qwen3.8-27B 的原生上下文长度),不缓存是缓存的 131,072 倍。一个本来跑 1 分钟的任务,会变成跑三个月。

一句话记住:KV cache 干的事情只有一件——把算过的 K、V 存下来,别再算第二遍。它把生成过程的投影计算从 n2 量级压回 n 量级。
常见误解:有了 KV cache 生成就变成匀速的了

不是。KV cache 只消掉了「重算 K、V」这一块。注意力打分那一步还是躲不掉:第 t 步,新 token 的那一个 Q 必须和缓存里全部 t 个 K 各做一次点积。所以每一步的打分量仍然随长度线性增长,写到第 10000 个字时,单步比第 1 个字慢,是正常的、无法靠缓存消除的。

这一章标题里的「越来越慢」有两个来源:打分量线性增长(算力),和缓存越占越大(显存与显存带宽)。后者往往先撞墙。

一个模型要生成 200 个 token。不使用 KV cache 时,总共要做多少次「单个 token 的 K、V 投影」?使用 KV cache 呢?两者相差几倍?

先想:第 1 步要算几个 token 的 K、V?第 2 步呢?把每一步的数量列出来看看有什么规律。
关键在于每一步的数量是 1、2、3、…、200,你要求的是它们的和,而不是最后一项。
第一步这样走:写出等差数列求和公式 n(n+1)/2,把 n=200 代进去。
完整答案:不缓存 = 200×201÷2 = 20,100 次;缓存 = 每步只算新来的那 1 个,共 200 次。相差 20100÷200 = 100.5 倍。注意这个倍数正好约等于 (n+1)/2,也就是说序列越长,缓存省下的比例越大——这也是为什么长上下文时代 KV cache 是必需品而不是优化项。

变式:如果输入的提示词本身就有 5000 个 token,然后生成 200 个 token,这两个数字分别变成多少?(提示:第一步就得处理 5000 个,之后每步加 1。)

自己推一遍:为什么省下来的是「一半」而不是「全部」

  1. 不缓存时,生成 n 个 token 的总投影量是 n(n+1)/2;缓存后是 n。有人由此说「KV cache 省掉了一半的计算」。这句话错在哪?

    想好了再看

    「一半」这个说法来自把 n2/2 里的那个 1/2 当成了节省比例。其实节省比例是 n ÷ [n(n+1)/2] = 2/(n+1)——n=1000 时只剩 0.2%,也就是省掉了 99.8%。当初会想到这一步,是因为要比较的是两条曲线的(一条是二次的,一条是一次的),而不是两个具体数字。

  2. 既然缓存这么划算,为什么训练的时候反而不用 KV cache?

    想好了再看

    训练时整句话是一次性喂进去的,所有位置的 K、V 在同一个矩阵乘法里一起算完,本来就没有「重算」这回事。KV cache 解决的是逐 token 生成特有的重复,训练根本不处在那个模式里。这也解释了为什么「训练显存」和「推理显存」是两笔完全不同的账。

  3. 既然缓存把投影降到了 n 量级,那生成 262K 个 token 的总成本是不是就跟长度成正比了?

    想好了再看

    不是。投影这一项是线性了,但注意力打分那一项每步是 t 次点积,累计仍是 n2/2。KV cache 把两个 n2 项消掉了一个,剩下的那个是注意力机制本身的定义带来的,只能靠换机制来解决——这正是第 6 章 Gated DeltaNet 要动手的地方。

5.2 缓存的代价是显存

时间省下来了,代价换到了空间上。缓存要存在显卡里,存多少字节是可以精确算出来的——而且这个公式只有四个因子,每个因子都能直接从 config.json 里读到。

NKV = 2 × Lfull × Hkv × dhead
式 5-1
符号是什么它从哪来
NKV每一个 token 要在缓存里占掉的数字个数(还没换算成字节)这是我们要求的量
2常数 2K 要存一份,V 也要存一份,两份都不能省。Q 不用存——Q 只在它自己那一步用一次,用完就扔
Lfull全注意力层的层数,不是总层数每一层都有自己独立的一套 K、V 投影矩阵,所以每层都要存自己的一份缓存。这个因子是本章的关键,下一节专门讲
HkvKV 头数第 4 章讲的 GQAGrouped-Query Attention):24 个 Q 头共用 4 组 K、V。缓存只需要存那 4 组,这正是 GQA 省下来的那笔钱
dhead每个头的宽度 head_dim一个头的 K 向量有多少个数字。Qwen3.8 这个数是 256,比常见的 128 宽一倍

拿到数字个数,换成字节就是再乘一个「每个数占几个字节」:

KV 显存 = NKV × bdtype × T
式 5-2
符号是什么直觉
bdtype一个数占几个字节fp16 / bf16 = 2,fp8 = 1,fp32 = 4。这是第 0 章「浮点数怎么存」那一节的直接应用
T上下文里的 token 总数包括你输入的提示词和模型已经生成的部分。它是唯一一个会不断变大的因子,其余三个在模型出厂时就钉死了

式 5-2 里最值得盯住的是 T 那一项:KV 显存和上下文长度严格成正比。不是「差不多成正比」,是一条从原点出发的直线。这就是为什么长上下文永远是显存问题,而不是算法问题。

式 5-1 里为什么用的是 KV 头数(4),而不是 Q 头数(24)?如果一个模型没有用 GQA、24 个 Q 头各配一套自己的 K、V,缓存会变成几倍?

先想:缓存里到底存的是什么?Q 有没有被存进去?
关键在于 GQA 让多个 Q 头共用同一组 K、V。共用的东西只需要存一份。
第一步这样走:把式 5-1 里的 Hkv 从 4 换成 24,其余不动,比一比。
完整答案:缓存里只有 K 和 V,Q 用完即弃,所以只有 KV 头数进公式。Qwen3.8-27B 是 24 个 Q 头共用 4 组 KV,24÷4 = 6,即每 6 个 Q 头共享一组。若改成每个 Q 头各一套(即 MHA),Hkv 从 4 变 24,缓存变成 6 倍。换句话说,GQA 在这个模型上把 KV cache 直接砍到了 1/6——这是它存在的主要理由,比「省参数」重要得多。

变式:如果反过来把 KV 头数压到 1(即 MQA,所有 Q 头共用一组 KV),缓存又会变成现在的几分之一?这样做会付出什么代价?

读的时候要小心:式 5-2 是下界,不是实测值

式 5-2 算的是 K、V 数据本身的字节数,不含推理框架的开销。真实框架(vLLM 的 PagedAttention、llama.cpp 的 slot 分配)会按固定大小的块预留显存,最后一块通常填不满,还有页表本身的开销。所以正确的用法是:把式 5-2 当成「至少要这么多」的硬地板,实测值一定不低于它,通常还要高出一截。5.4 节做 24 GiB 卡的推导时专门留了 1.0–2.0 GiB 给「框架与激活开销」,就是为了不让这个下界被当成上界用;也正因为这个下界的方向是偏乐观的,那一节给出的上下文区间必须补一句「真实值更靠近下沿」。

5.3 代入 Qwen3.8-27B:一个反直觉的结果

现在把 Qwen3.8-27B 的数字填进式 5-1。mat/cfg27b.json 里能直接读到:num_hidden_layers: 64num_key_value_heads: 4head_dim: 256

看到这三个数,绝大多数人会写下这一行:

2 × 64 × 4 × 256 = 131,072 (
式 5-3 · 错误版
这里的数他写的是什么问题在哪
64config.json 里的 num_hidden_layers,总层数式 5-1 那一格要的是全注意力层数。在过去的模型里这两个数相等,在 Qwen3.8 上不相等

错了。问题出在 Lfull 上——式 5-1 里那个因子写的是全注意力层的层数,不是总层数。Qwen3.8-27B 的 64 层里,只有 16 层是全注意力。

Qwen3.8-27B 的 64 层,从第 1 层到第 64 层 48 层 linear_attention:Gated DeltaNet,没有 KV cache 16 层 full_attention:有 KV cache,公式里只算这些
图 5-1:64 层里每 4 层出现一次全注意力(示意图,层序按 full_attention_interval: 4 的 3+1 循环绘制,非官方原始图)。

所以正确的算式是:

NKV = 2 × 16 × 4 × 256 = 32,768
式 5-3 · 正确版
这里的数是什么出处(mat/cfg27b.json
2K 一份、V 一份常数,与配置无关
16全注意力层数num_hidden_layers: 64 ÷ full_attention_interval: 4 = 16;完整 config 的 layer_types 数组里也正好是 16 个 full_attention
4KV 头数num_key_value_heads: 4
256每个头的宽度head_dim: 256

32,768 个数字。fp16 每个数 2 字节 → 65,536 字节 = 64 KiB/token;fp8 每个数 1 字节 → 32 KiB/token。而按 64 层算出来的是 131,072 个数 → fp16 256 KiB/token,正好是真实值的 4 倍

常见误解:这不是一个小数点问题,是全网通行的错算

网上几乎所有 Qwen3.8 显存计算器都算错了这一项。原因不难理解:过去十年绝大多数模型每一层都是全注意力,「层数」和「有 KV cache 的层数」是同一个数,公式里写 num_hidden_layers 从来没错过。到了 3:1 混合布局,这两个数第一次分开了——而通用计算器里那一行代码没人去改。

自己去数一遍。Hugging Face 上 Qwen3.8-27B 的完整 config.json 里有一个逐层写死的 layer_types 数组,数一数:48 个 linear_attention + 16 个 full_attention,严格按 3+1 循环排列。本站 mat/cfg27b.json 是精简副本,保留的是等价的那一行 full_attention_interval: 4——64 ÷ 4 = 16,结论一样。两个字段互相印证:16,不是 64。

表 5-1:Qwen3.8-27B 各上下文长度下的 KV cache 显存(本站按式 5-2 复算,见 mat/paramcount.out.txt
上下文长度fp16 KV(正确)fp8 KV(正确)按 64 层错算(fp16)
32K(32,768)2.00 GiB1.00 GiB8.00 GiB
128K(131,072)8.00 GiB4.00 GiB32.00 GiB
262K(262,144,原生满上下文)16.00 GiB8.00 GiB64.00 GiB
1M(1,010,000)61.65 GiB30.82 GiB246.58 GiB

这张表里最刺眼的是最后一行的对比:真实值 61.65 GiB 已经很吓人了,错算值 246.58 GiB 会让人直接得出「这个模型根本没法在单机上开 1M 上下文」的结论——而真相是,在一张 141GB 的卡上,fp8 KV 的 1M 上下文(30.82 GiB)是有余地的。一个算错了的公式,会让人放弃一个本来做得到的方案。

上面这个计算器把两条算法并排放着:左边按 Lfull=16 算,右边按 64 算。拖上下文长度的滑块,看两条柱子怎么分叉;再切量化位宽,看权重那一块怎么变。能不能塞进一张卡,从来不是权重一个人的事——这一点在下一节会被算成具体数字。

假设有人做了一个 Qwen3.8-27B 的改版:层数还是 64,但把布局从 3:1 改成 1:1(32 层全注意力 + 32 层线性)。其余参数不变。这个改版在 128K 上下文、fp16 KV 下要占多少显存?

先想:式 5-1 的四个因子里,这次只有一个变了,是哪一个?
关键在于 Lfull 从 16 变成 32,恰好翻倍,而显存和它是正比关系。
第一步这样走:先查表 5-1 拿到原版 128K/fp16 的 8.00 GiB,再想这个数要乘几。
完整答案:NKV = 2×32×4×256 = 65,536 个数 → fp16 = 128 KiB/token。128K 上下文 = 131,072 token × 128 KiB = 16.00 GiB,正好是原版 8.00 GiB 的两倍。可以直接由正比关系得到,不必重算。这也说明:3:1 这个比例本身就是一个显存旋钮,第 7 章会讲为什么最后选的是 3:1 而不是 1:1 或 7:1。

变式:如果反过来保持 16 层全注意力不变,但把 head_dim 从 256 减到 128,显存变成多少?和上面那个改版比,哪种改法对模型能力的影响更难预测?

你在网上看到一个 Qwen3.8-27B 显存计算器,输入 262K 上下文、fp16 KV,它给出 64 GiB。你不看它的源码,只凭这一个输出,就能断定它错在哪一行。怎么断定?如果它给的是 32 GiB 呢?

先想:64 ÷ 16 = 4,这个 4 是从哪来的?式 5-1 里哪个因子刚好差 4 倍?
关键在于错算的倍数是固定的:64÷16 = 4。任何整数倍偏差都能被反推回某一个因子。
第一步这样走:把它给的数除以正确值 16 GiB,得到倍数,再去式 5-1 的四个因子里找哪个能凑出这个倍数。
完整答案:64÷16 = 4,正是 64 层 ÷ 16 层,所以它把 Lfull 写成了 num_hidden_layers。若给的是 32 GiB,倍数是 2——这就不是层数问题了,最可能是把 fp16 当成了 fp32(bdtype 从 2 变 4),也可能是它同时算错了两处而恰好抵消掉一半(比如层数错 4 倍、KV 头数误用了 2 而不是 4)。倍数是整数并不能唯一确定病因,只能缩小范围——这是这道题真正想让你记住的:靠一个输出反推实现,要老实承认哪些解释还没被排除。

变式:如果那个计算器给出的是 20 GiB(不是 16 的整数倍),你还能用同样的办法反推吗?想想什么样的实现会产生非整数倍的偏差。

答辩:如果我是审稿人

你说另外 48 层「根本没有 KV cache」,所以不进公式。可它们总不是免费的吧?Gated DeltaNet 的循环状态也要占显存。你把它从公式里划掉,是不是为了让结论好看?

参考防守(先自己组织语言再看)

它们确实占显存,把它写成零是不诚实的。但它们和 KV cache 有一个决定性的区别:循环状态的大小与序列长度完全无关。48 层的状态加起来是 144 MiB(48 层 × 3 MiB,第 6、7 章会把这个数算出来),无论上下文是 1K 还是 1M 都是同一个数。放在表 5-1 的 262K 那一行(16.00 GiB)旁边,它连 1% 都占不到——所以 5.4 节的预算表把它单列成 0.14 GiB 的一行,而不是并进框架开销里含糊掉。

但有一个场景不能这么糊弄:并发。循环状态是每条请求各一份的,同时服务 32 个用户就是 32 份。这一项不随长度增长,却随并发数线性增长——它不该被写成零,只是它出现在另一张账单上。这条追问是对的,本章的公式只覆盖了「单条长序列」这一种账。

5.4 24GB 显卡到底能开多长上下文

「24GB 显卡能跑 Qwen3.8-27B」这句话,网上到处都是。它不算错,但它省略了一个决定使用体验的变量。把 5.2 的公式和量化后的权重体积放在同一张纸上,就能把这句话补完整。

这一节是全站这条推导的正本。第 1、4、6、7、8 章引用的那个上下文区间都从这里出来,第 17 章会把同一笔账再算一遍,并把它铺到 8GB / 16GB / 服务器几档硬件上。

补充:先把单位说清——GB 与 GiB 差 7.4%

显卡厂商标的 24GB(RTX 4090、3090 那一档)指的是 24 GiB,即二进制的 24 × 10243 字节,换算成十进制是 25.77 GB。而 Hugging Face、Unsloth、Ollama 页面上显示的模型文件大小是十进制 GB(109 字节)。两者差 7.37%——在 24 这个量级上就是 1.77 GB,足够多装两万多个 token 的 fp16 KV。

本站的约定(全站统一):权重与模型文件体积用十进制 GB(与各家页面显示的一致),显存预算、KV cache、显卡容量用 GiB。要放进同一个加减法里时,先把 GB 除以 1.0737 换成 GiB:17.11 GB = 15.93 GiB,0.93 GB = 0.87 GiB

这不是学究气。本站早先的推导正是栽在这一步:把 24 GiB 的卡当成 24 GB 来算,凭空少了 1.77 GiB,结论比正确值低了近三成。单位混用不会报错,只会让一个偏低的数字看起来很合理。

读的时候要小心:下面这段是本站推导,不是官方数字

官方没有发布 Qwen3.8 的技术报告,也没有公布 24GB 单卡的上下文上限。下面这几行是本站用「Unsloth 实测的 GGUF 文件体积」加「式 5-2 的 KV 公式」推出来的估算,不是实测跑出来的结果。三条限定必须跟着这个数一起走: 它是本站推导,非官方数字; 五项里的「框架与激活开销」是本站取的经验区间,不是实测,整式的不确定性全在这一项,所以结论只能是区间,不能是单点值 实际部署还有分页与碎片开销(式 5-2 只算数据本身的字节数),真实可用值会更靠近区间下沿

Q4_K_M 权重 17.11 + 视觉塔 mmproj 0.93 = 18.04 GB = 16.80 GiB
式 5-4
是什么为什么不能漏
17.11 GB
= 15.93 GiB
Q4_K_M 量化后的文本塔权重文件真实体积来自 Unsloth 的 GGUF 逐文件体积,是实测文件大小,不是估算
0.93 GB
= 0.87 GiB
视觉塔 mmproj 文件Qwen3.8-27B 是原生多模态,视觉塔是单独一个文件,要看图就必须额外加载。很多显存估算漏掉了它(第 11 章细讲)

接下来把 24 GiB 这块预算逐项摊完。一张卡上同时住着五样东西,权重只是最大的那一样;漏掉任何一项,上下文估计都会偏高,而偏高的后果是跑到一半突然显存不足。

24 − 15.93 − 0.87 − 0.14 − (1.0~2.0)= 5.1 – 6.1 GiB 留给 KV cache
式 5-5

五项逐个的来源与可靠性:

表 5-2:24 GiB 单卡跑 Qwen3.8-27B Q4_K_M 的显存预算(本站推导,非官方数字;单位统一到 GiB)
数值它从哪来,硬不硬
显卡容量24 GiB硬。厂商标称的 24GB 就是 24 GiB(= 25.77 GB 十进制)
Q4_K_M 权重17.11 GB = 15.93 GiB硬。Unsloth 实测 GGUF 文件体积 ÷ 1.0737
mmproj 视觉塔0.93 GB = 0.87 GiB硬。实测文件。只做纯文本时这一项为 0(第 11 章)
DeltaNet 固定态0.14 GiB48 层 × 3 MiB = 144 MiB。本站按论文定义推导,官方与框架均未公布不随序列长度变(第 6、7 章)
框架与激活开销1.0–2.0 GiB软。本站取的经验区间,非实测——整式的不确定性全在这一行
剩给 KV cache5.1–6.1 GiB24 − 15.93 − 0.87 − 0.14 − (1.0~2.0)
→ fp16 KV(64 KiB/token)约 83K – 99K token精确 82,875 – 99,259,按式 5-2 的每 token 2×16×4×256 个元素
→ fp8 KV(32 KiB/token)约 166K – 199K token精确 165,751 – 198,519。位宽减半,token 数翻倍

写成一句话就是:

  • fp16 KV(64 KiB/token)→ 上下文约 8–10 万 token
  • fp8 KV(32 KiB/token)→ 上下文约 17–20 万 token

注意这里为什么给的是区间而不是一个数:五项输入里有一项本身就是区间(框架开销 1.0–2.0 GiB)。一个输入不确定,输出就不该假装确定——把它写成「约 9 万」这样的单点值,会让读者以为这个数是测出来的。区间的宽度也是有信息的:1.0 到 2.0 GiB 这一整格的不确定性,只让结论在 8.3 万和 9.9 万之间摆动(19%),而 262K 需要 16 GiB 的 KV,是上沿的 2.6 倍——结论对那个最软的输入并不敏感

一句话记住:24 GiB 单卡跑 Q4_K_M 的 Qwen3.8-27B 是跑得起来的,上下文实际到约 8–10 万 token(fp16 KV);开 fp8 KV cache 大约翻倍到约 17–20 万原生 262K 上下文在 24GB 上跑不满。凡是看到「24GB 流畅运行」的说法,都要补上这个限定。(本站推导,框架开销为经验区间,真实值更靠近下沿。)

从一行 config 到你第 30 天遇到的那个报错

  1. 因为它把 64 层里的 48 层设计成了没有 KV cache 的线性注意力(mat/cfg27b.jsonfull_attention_interval: 4)。
  2. 所以在 24 GiB 这块预算里,塞进 8–10 万(fp16 KV)到 17–20 万(fp8 KV)的上下文成为可能;如果 64 层全是注意力,每 token 要 256 KiB,同样这 5.1–6.1 GiB 只够约 2–2.5 万
  3. 第 30 天你把一份 12 万字的项目文档整个丢进去做问答,fp16 KV 下模型在读到大约七八成的位置时框架报显存不足(8–10 万 token 对 12 万字,具体落在哪要看分词器把几个汉字算成一个 token);你把启动参数换成 --kv-cache-dtype fp8,同一份文档一次读完了。
  4. 这一条链最后落在一个判断上:决定你能读多长文档的,不是模型有多少参数,是有多少层带 KV cache,以及你用几个字节去存它。
代价:要为 fp8 KV 付出一点点精度——KV 是会被反复读取、和每一个新 query 做点积的量,量化误差会沿着长序列累积,具体掉多少本站没有 Qwen3.8 上的实测数据。

换成一张 48GB 的卡,其他条件不变(Q4_K_M 权重 15.93 GiB + 视觉塔 0.87 GiB + DeltaNet 固定态 0.14 GiB,框架开销仍取 1.0–2.0 GiB)。fp16 KV 下能开多长上下文?能开满 262K 吗?

先想:这次变的只有总预算这一个数,其余四项照抄。注意 48GB 的卡也是 48 GiB。
关键在于留给 KV 的空间从 5.1–6.1 GiB 变成了多少,而 KV 与上下文是严格正比。
第一步这样走:48 − 15.93 − 0.87 − 0.14 − (1.0~2.0) = ?,再和 5.1–6.1 比一比是几倍。
完整答案:留给 KV 约 29.1–30.1 GiB,是原来 5.1–6.1 的约 5 到 5.7 倍,所以上下文约 48–49 万 token(精确 476,091 – 492,475),能开满 262K,还有一倍富余。也可以直接用表 5-1 核对:262K/fp16 要 16.00 GiB,29 GiB 的预算装得下。顺带注意一个反直觉之处——从 24 GiB 换到 48 GiB,卡的容量只翻了一倍,能开的上下文却变成了约 5 倍,因为前四项是固定支出,翻倍出来的 24 GiB 几乎全落到了 KV 头上。还要注意区间在这里变窄了:框架开销那 1 GiB 的不确定性,在 5.1–6.1 上是 19%,在 29.1–30.1 上只有 3.4%——预算越宽,那个最软的输入越不重要。

变式:如果不是换卡,而是把量化从 Q4_K_M 换成 UD-IQ2_XXS(9.01 GB),24GB 卡上 fp16 KV 能开多长?这样做换来的上下文,值不值得那一档精度损失?

5.5 KV cache 也能量化

式 5-2 里有四个因子,其中三个在模型出厂时就定死了,你改不了。但 bdtype——每个数占几个字节——是推理时你自己能选的

KV cache 量化KV cache quantization):把缓存里的 K、V 从 fp16(每个数 2 字节)改成 fp8(1 字节)来存,读的时候再还原成计算需要的精度。权重不动,只动缓存。在 vLLM 里就是启动时加一个参数 --kv-cache-dtype fp8;llama.cpp 那边对应的是 --cache-type-k--cache-type-v

效果是直给的:每 token 从 64 KiB 降到 32 KiB,同样的显存预算能开一倍长的上下文。表 5-1 的第三列就是这么来的,5.4 节那个「约 8–10 万 → 约 17–20 万」也是——注意翻倍的是区间的两端,结论仍然是区间。

补充:为什么 KV 比权重更适合被压

注意这是两件独立的事:权重量化(Q4_K_M 那一类,第 15、16 章的主题)压的是模型文件;KV 量化压的是运行时的缓存。它们可以任意组合——Q4_K_M 权重 + fp16 KV、Q4_K_M 权重 + fp8 KV,都是合法配置。

KV 相对好压的原因在于它的寿命:一份权重要被这个模型此后每一次推理反复使用,误差会作用在所有输入上;而一份 KV 只服务当前这一次对话,用完就丢。不过这不等于免费——见下面的追问。

答辩:如果我是审稿人

既然 fp8 KV 白拿一倍上下文,那为什么不干脆上 fp4、甚至 int2?照你的逻辑,位宽减半上下文就翻倍,一路减下去不是更好?

参考防守(先自己组织语言再看)

因为 KV 不是「存起来看看」的数据,它是被反复参与运算的数据:缓存里每一个 K 向量,都要和之后每一个新 token 的 Q 做点积,序列越长,同一个 K 被读的次数越多,量化误差被放大的机会也越多。这和权重量化的误差模式不一样。

业界确实有做到 2–4 bit 的工作:KIVI(arXiv:2402.02750)和 KVQuant(arXiv:2401.18079)都在这个方向上,但两者都不是「直接把位宽调小」那么简单——需要非对称量化、按通道/按 token 分组、以及对离群值单独处理。而且这些工作是在纯全注意力模型上验证的;Qwen3.8 只有 1/4 的层带 KV cache,这些层承担了全部的精确回忆职责(第 6.7 节会讲为什么),KV 量化误差在这种架构上是被稀释了还是被放大了,本站没有找到公开实验。所以本章只敢推荐 fp8,再往下需要你自己测。

有人说:「我已经用 Q4_K_M 把模型量化到 4 bit 了,KV cache 应该也自动变成 4 bit 了吧?」这句话对吗?如果不对,问题出在哪里?

先想:Q4_K_M 压的是文件里的哪些数?KV cache 里的数是运行时才产生的,它们是同一批数吗?
关键在于权重是静态的(存在文件里),KV 是动态的(每次对话临时算出来)。量化设置是分开配的。
第一步这样走:回到式 5-2,看看 bdtype 这个因子描述的是谁的位宽。
完整答案:不对。Q4_K_M 只决定权重文件怎么存,KV cache 的精度由推理框架的另一个开关决定(vLLM 的 --kv-cache-dtype、llama.cpp 的 --cache-type-k/-v),默认通常是 fp16 而不是跟随权重。所以一个常见的现象是:模型文件明明只有 17 GB,一开长上下文显存就爆——因为那 5.1–6.1 GiB 的 KV 是按 fp16 在算的。两个旋钮,要分别拧。

变式:反过来,如果只把 KV 设成 fp8 而权重保持 BF16(54.66 GB),在一张 80GB 的卡上能开多长上下文?这种配置在什么场景下是合理的?

5.6 2.4T 的账

同一个公式,换一组数。Qwen3.8-2.4T-A95B 是 92 层,其中 23 层全注意力 + 69 层线性(同样是 3+1 循环,重复 23 次),KV 头数 4,head_dim 256——注意后两个数和 27B一模一样,这也是「同一张图纸,不同的重复次数」这个说法的一处硬证据。

NKV = 2 × 23 × 4 × 256 = 47,104
式 5-6
这里的数是什么和 27B 比
2K 一份、V 一份相同
23全注意力层数27B 是 16。92 层 ÷ 4 = 23,同样的 3+1 循环,只是重复了 23 次
4KV 头数完全相同(虽然 Q 头从 24 涨到了 64)
256每个头的宽度完全相同

fp16 → 92 KiB/token,fp8 → 46 KiB/token。误按 92 层全算是 368 KiB/token,同样是整整 4 倍的高估——因为 3:1 这个比例在两个模型上是一样的,错算的倍数也就一样。

把 27B 和 2.4T 放在一起看会发现一个值得记住的对比:2.4T 的总参数是 27B 的将近 90 倍,但它每 token 的 KV cache 只是 27B 的 1.44 倍(92 ÷ 64 = 1.4375)。为什么?因为 KV cache 只跟全注意力层数、KV 头数、head_dim 有关,跟 FFN 有多宽、专家有多少个一点关系都没有——而 2.4T 那多出来的两万多亿参数,几乎全在 MoE 的专家里(第 9、10 章)。

一句话记住:权重体积看总参数,KV cache 只看注意力那一小块。这是两笔完全不同的账,不能互相推算。

有人给你一个陌生模型,只告诉你它的总参数是 2.4T,问你「它每 token 的 KV cache 是多少」。你应该回答什么?请构造一个具体的反例,说明为什么这个问题无法回答。

先想:式 5-1 需要哪四个输入?总参数量是其中之一吗?
关键在于构造两个总参数都是 2.4T、但 KV cache 差很多的模型,证明这个映射不是唯一的。
第一步这样走:拿 Qwen3.8-2.4T 当第一个例子(92 KiB/token),再想想怎么在保持 2.4T 总参数的前提下把 KV 改大——比如把 MoE 专家的钱挪一部分给注意力。
完整答案:应该回答「信息不足」。反例:Qwen3.8-2.4T-A95B 是 92 层、23 层全注意力、4 个 KV 头、head_dim 256,得 92 KiB/token;而一个假想的稠密 2.4T 模型可以是 120 层全部是全注意力、40 个 KV 头、head_dim 128,每 token KV = 2×120×40×128 = 1,228,800 个数 → fp16 2.34 MiB/token,是前者的 26 倍,而两者总参数可以都是 2.4T。总参数决定的是「要买几张卡装权重」,KV cache 决定的是「装完权重之后还能读多长的文档」,这两个量之间没有函数关系。要回答这个问题,必须拿到全注意力层数、KV 头数、head_dim 三个数。

变式:反过来问——只告诉你「每 token KV 是 64 KiB」,你能推出模型的总参数吗?如果不能,你最多能推出什么?

你手上有一张 24GB 卡,要做一个「读长论文 + 看论文里的插图」的助手,希望上下文至少 128K。请给出一套完整配置(权重量化档位、是否加载视觉塔、KV 精度),并说明每一步是被哪个约束逼出来的。如果 128K 实在放不下,你会先砍哪一项?

先想:需求里有「看插图」,这一条直接决定了式 5-4 里的某一项能不能省。
关键在于先用表 5-1 查出 128K 需要多少 KV,再倒推权重最多能占多少,最后去 GGUF 体积表里挑一档。
第一步这样走:128K + fp8 KV = 4.00 GiB。24 − 4.00 − 1.2(框架)− 0.93(视觉塔)≈ 17.9,看看哪一档权重能塞进 17.9。
完整答案:① 要看图 → 视觉塔 0.93 GB 是硬性支出,不能省。② 128K 上下文若用 fp16 KV 要 8.00 GiB,加上 0.93 + 1.2 已经 10.1,只剩不到 14 给权重,Q4_K_M(17.11)放不下,得掉到 Q3_K_M(13.82)——精度代价明显。③ 改用 fp8 KV,128K 只要 4.00 GiB,预算剩约 17.9,Q4_K_M(17.11)刚好塞得下,这是更优的一组。④ 若还是放不下(比如框架实际开销超过 1.2),砍的顺序应该是:先把上下文从 128K 降到 96K(KV 是唯一与需求线性挂钩、且可以逐步让步的项),再考虑 UD-Q4_K_XL→IQ4_XS 这类同档微调,最后才动视觉塔——因为砍掉视觉塔等于砍掉一半的需求,那是换了个产品,不是省了显存。注意这一整套推导都建立在 5.4 节那个标注为「本站推导」的估算上,实际部署必须实测。

变式:把需求改成「不看图,但要同时服务 8 个用户,每人 32K 上下文」,配置该怎么变?(提示:8 个用户的 KV 是各自独立的,但权重只加载一份。)

答辩:如果我是审稿人

你把 64 KiB/token 说成一个确定的数。可这个模型是原生多模态的,图片会变成一堆视觉 token 进同一个序列;而且 config 里还有 mtp_num_hidden_layers: 1,那个多 token 预测的草稿头难道不占 KV 吗?你这个数是不是低估了?

参考防守(先自己组织语言再看)

两个质疑要分开答。

视觉 token 这一条不成立:图片经过视觉塔之后变成的就是普通 token,进的是同一个序列,按 token 数记账,每个视觉 token 同样占 64 KiB。所以视觉输入不改公式,只改 T——一张图折算成几十到几百个 token,这笔账落在 T 里,公式本身没错。

MTP 这一条本站答不上来。mat/cfg27b.json 只写了 mtp_num_hidden_layers: 1,没有说明这一层是不是全注意力、推理时是否为它单独维护 KV、以及框架默认开不开 MTP。本站在 mat/ 的全部材料里都没有找到这个信息,官方也没有技术报告。老实说:如果这一层带 KV,64 KiB/token 就是一个略微的低估(多一层就是多 1/16,约 +6%)。这个不确定性应该被写出来,而不是藏起来——它不影响「4 倍高估」这个主结论,因为 6% 和 300% 不在一个量级上。

真未解在 3:1 混合布局上,KV cache 压到 4 bit 以下,误差会集中在哪些层?

已有的低比特 KV 量化工作(KIVI arXiv:2402.02750、KVQuant arXiv:2401.18079)都是在每一层都有 KV cache 的纯注意力模型上做的实验,它们的结论是「靠分组量化和离群值处理,2–4 bit 可用」。但在 Qwen3.8 这种只有 1/4 层带 KV 的架构上,这 16 层承担了全部的精确回忆职责(第 6.7 节会讲清楚为什么),直觉上有两种完全相反的预测:一种认为剩下 48 层的线性注意力提供了冗余,误差会被稀释;另一种认为正因为只剩这 16 层能做精确回忆,它们的误差反而无处可退。这两种预测本站都没能找到公开实验来判定,而 Qwen3.8 发布至今只有两天,也不可能有。

先做这一步:用本章那个显存计算器里的公式,先算出「fp8 → fp4 能多换多少上下文」这个收益的确切数字(提示:还是翻倍);然后拿 llama.cpp 的 --cache-type-k q4_0 --cache-type-v q4_0 跑一个「大海捞针」测试——把一句话藏在 60K 上下文的不同深度,分别用 fp16 / fp8 / q4_0 三档 KV 各测 20 个位置,看命中率曲线在哪一档开始塌。这是一个下午就能跑完的实验,而且它的结论目前确实没有公开答案

这一层要加什么:给注意力加 KV cache

为什么现在才加它:第 3、4 层造出来的注意力是「一次喂进整句话」的版本——训练就长这样,它压根没有「上一步」这个概念。要让它一个字一个字往外生成,就得在两次调用之间把 K、V 留住。这是整个实现里第一次出现「跨调用的状态」,也是第一次出现「显存会随着用户说得越多而涨」这件事。

cache = [ {k: [], v: []} for _ in full_attn_layers ] # 每个全注意力层一份 def step(token, pos): # 一次只喂一个新 token x = embed(token) for L in layers: if L.kind == 'full_attention': q, k, v = L.proj(x) # 只对这 1 个新 token 做投影 k = rope(k, pos) # 注意:用它自己的真实位置,不是 0 cache[L.id].k.append(k) # 追加,不重算 cache[L.id].v.append(v) x = attend(q, cache[L.id].k, cache[L.id].v) # q 是 1 个,K/V 是 t 个 else: x = L.forward(x) # 线性注意力层,第 6 层再改 return head(x)

难点:位置编码必须在写进缓存之前、按这个 token 的真实位置转好。第 4 层做的部分 RoPE 是按绝对位置旋转 K 的(head_dim 256 里只有前 64 维参与旋转,partial_rotary_factor: 0.25)。缓存里存的是已经转过的 K——那 64 维转过的、加上 192 维没转的,拼成完整的一块存进去。这里有两个经典的写错方式:一是每次调用都把 pos 传成 0(因为「这一步只有一个 token 嘛」),二是只把转过的那 64 维存进缓存、剩下 192 维每步重新拼。前者的症状特别阴险:短对话完全正常,写到一两千字才开始语无伦次,因为所有 K 都以为自己在第 0 位,注意力分不清先后。后者会直接输出乱码,反而好查。

自己验:连续生成 10 个 token。第 10 步的 QKV 投影乘加量必须和第 1 步一样(每步都只处理 1 个新 token),而缓存张量的序列那一维要从 1 长到 10(形状 [1, 4, t, 256] 里的 t);如果第 10 步的投影量是第 1 步的 10 倍,说明缓存根本没生效,你还在重算历史。注意力打分那一步则相反,它应该从 1×1 长到 1×10——这一项涨才是对的。然后把缓存整个关掉,用「每步重喂全序列」的老路径跑同样 10 步,两条路径的 logits 最大绝对差应该在 1e-5 以内(fp16);差得更多说明缓存写错了位置——按上面那个难点,先去查 pos 是不是传成了 0。

本章小结

  • 不缓存 K、V,生成 n 个 token 要做 n(n+1)/2 次投影;缓存之后只要 n 次。生成 1000 个 token 就是 500,500 次对 1000 次。
  • 代价是显存:每 token KV 元素数 = 2 × 全注意力层数 × KV 头数 × head_dim。四个因子分别对应「K 和 V 各一份」「每层各有一套」「GQA 省下来的那个数」「每个头的宽度」。
  • Qwen3.8-27B:2 × 16 × 4 × 256 = 32,768 → fp16 64 KiB/token,fp8 32 KiB/token。按 64 层算得到的 256 KiB/token 是整整 4 倍的高估,而这正是网上绝大多数显存计算器的写法。
  • 262K 满上下文的 KV 是 16.00 GiB(fp16)或 8.00 GiB(fp8),不是 64 GiB。
  • 24 GiB 单卡(本站推导,表 5-2):24 − 15.93(Q4_K_M)− 0.87(视觉塔)− 0.14(DeltaNet 固定态)− 1.0~2.0(框架与激活开销)= 剩 5.1–6.1 GiB 给 KV → fp16 约 8–10 万、fp8 约 17–20 万 token。原生 262K 在 24GB 上跑不满。框架开销是经验区间,真实值更靠近下沿。
  • 2.4T-A95B:2 × 23 × 4 × 256 = 47,104 → fp16 92 KiB/token。总参数差 90 倍,KV 只差 1.44 倍——这两笔账互不相干。
  • 下一章的问题:那 48 层线性注意力凭什么可以没有 KV cache?它把历史存到哪里去了?
我能不看材料说清:为什么 Qwen3.8-27B 的 KV 公式里那个层数是 16 而不是 64,以及这个差别在 262K 上下文下具体是多少 GiB。

第6章 Gated DeltaNet:把记忆压成固定大小

这一章回答:Qwen3.8-27B 的 64 层里,那 48 层「没有 KV cache」的层,到底把历史存到哪里去了?答案是一块永远不变大的记事本——以及为了让这块记事本不糊掉,需要做哪两件事。

学完这一章你应该能做到

  • 说清为什么去掉 softmax 之后,整段历史可以被压进一个固定大小的矩阵
  • 逐符号读懂 delta rule 的更新式,并解释 (Iβkk) 为什么叫「擦除算子」
  • 说清门控 α 和 delta rule 各自负责哪一种遗忘,为什么缺一不可
  • config.json 的几行里算出 Qwen3.8 一个 DeltaNet 层的参数量,并解释它为什么全注意力层还大
  • 指出固定大小状态的适用边界:什么任务它做不了,以及为什么这个限制是数学上必然的
前置:第3章的注意力(Q 查 K 取 V)、第4章的 head_dim 与多头、第5章的 KV cache 显存公式(这一章的全部动机都在那里),第0章的矩阵乘法与「向量的外积」。

6.1 问题:KV cache 随长度线性增长,这条路走不通

第 5 章算出来的数字,摆在一起看是这样的:

表 6-1:Qwen3.8-27B 的 KV cache 与权重体积对比(数据来自第 5 章表 5-1 与 mat/ 的 GGUF 实测体积)
大小随上下文变吗
Q4_K_M 量化权重(含视觉塔)18.04 GB不变
BF16 全精度权重54.66 GB(≈ 50.9 GiB)不变
KV cache @ 262K(原生满上下文,fp16)16.00 GiB正比增长
KV cache @ 1M(fp16)61.65 GiB正比增长

最后一行是这一章存在的理由:1M 上下文的 KV cache(61.65 GiB)比这个模型的全精度权重(约 50.9 GiB)还大。而这已经是只算 16 层之后的数字——如果 64 层全是全注意力,同样的 1M 上下文要 246.58 GiB,一台八卡机器装完权重之后也塞不下。

问题的性质要看清楚:权重是常数项,KV cache 是随长度线性增长的项。两者之间没有任何一种量化能改变的力量对比——fp8 KV 把每 token 从 64 KiB 砍到 32 KiB,是把那条直线的斜率减半,撞墙的位置往后挪一倍,仅此而已。要把 100 万上下文变成日常可用,需要的不是更小的斜率,是把这条线掰平

为什么会有这个东西

注意力之所以必须留住全部历史,根子在 softmax。第 3 章讲过,第 t 个 token 对第 i 个 token 的注意力权重是 exp(qt·ki) ÷ Σ。这个式子里,每一个历史 ki 都必须以原样出现——它先要单独和 qt 做点积,然后进指数,然后才被归一化。你没法提前把它们合并成一个东西,因为 exp 挡在中间:exp(a) + exp(b)exp(a+b) 不是一回事。

所以「必须存全部历史」不是工程上偷懒,而是 softmax 这个函数的形状逼出来的。要绕开它,就得先把它拿掉。

有人提议:把 KV cache 从 fp16 换成 fp8,「就能把 1M 上下文的问题解决」。用表 6-1 的数字说明这个提议解决了什么、没解决什么。

先想:fp8 让 61.65 GiB 变成多少?这个新数字和权重比是大是小?
关键在于区分「把一条斜线的斜率减半」和「把它变成水平线」这两件事。
第一步这样走:算出 fp8 下 1M 的 KV(30.82 GiB),再问一句:那 2M 上下文呢?
完整答案:fp8 把 1M 的 KV 从 61.65 降到 30.82 GiB,确实解决了「1M 上下文在一张 141GB 卡上放不下」这个具体问题。但它没有改变增长的性质:2M 上下文要 61.65 GiB,4M 要 123.3 GiB,撞墙只是往后挪了一格。量化能给你的是一个常数因子(这里是 2 倍),而问题是线性增长——常数因子对付不了增长阶。要改变阶,必须改机制,这就是本章的主题。

变式:如果有人做出了 2-bit 的 KV 量化(每 token 8 KiB),在 24 GiB 卡的 5.1–6.1 GiB KV 预算下(第 5 章表 5-2 的完整推导)能开多长上下文?这个数字离 262K 还差多少?

6.2 换个思路:只存一个固定大小的记事本

线性注意力Linear Attention):把注意力里的 softmax 拿掉(或者换成别的、可以拆开的函数)之后得到的一族机制。拿掉 softmax 会损失一些表达能力,但换来一个决定性的好处——历史可以被累加进一个固定大小的矩阵,不必逐条保留。

看看拿掉 softmax 之后会发生什么。第 t 步的输出本来是「对所有历史 vi 按权重求和」。去掉 exp 和归一化,权重就是裸的点积 qt·ki

也就是 ot = Σit (qt·ki) vi:把每个历史位置的内容 vi,按它的键和当前查询的点积大小加权,全部加起来。

这一步是全章的转折点:点积是可以结合律重排的(q·ki)vi 可以写成 (kivi)q,于是求和号可以整个挪到 q 的另一侧,先把所有 kivi 加起来,再让 q 去乘这个和。而那个和,就是我们要的记事本:

St = St−1 + ktvt   ot = Stqt
式 6-1(最朴素的线性注意力)
符号是什么直觉
Stt 步的状态矩阵,形状 dk × dv(Qwen3.8 里是 128×128)模型的「记事本」。它的形状和 t 无关——这是整章唯一真正要记住的一件事
ktvt两个向量的外积:一个 dk 维列向量乘一个 dv 维行向量,得到一张 dk×dv 的表「把 v 这条内容,登记在 k 这个地址上」。外积就是「地址 × 内容」的登记动作
qt本步的查询向量,dk拿着一个地址去记事本上查
ot本步的输出,dv查出来的内容。Sq 一次矩阵-向量乘就完事,不需要回看任何历史 token

把式 6-1 和第 5 章的 KV cache 并排放,差别一目了然:KV cache 要存 TkTv;式 6-1 只存一张 dk×dv 的表,T 完全不出现在它的尺寸里。

全注意力:KV cache(每 token 64 KiB) Gated DeltaNet:循环状态 1K token 3K token 10K token 1K token 3K token 10K token 形状 128×128,永远不变
图 6-1:两种记忆随序列长度的增长方式(示意图,非官方原始数据;左侧条形按 token 数严格等比例绘制,右侧三个方块尺寸完全相同)。
打个比方

全注意力是录音:开会时把每一句话都录下来。占地方随时长线性增长,但任何一句都能精确回放,一个字不差。

线性注意力是记一页笔记:无论会开三小时还是三天,笔记永远是那一页。要用的时候看笔记,不回放录音。

类比失效处(三处,都很重要):① 人记笔记会重点,而式 6-1 是无差别地把每一条 kv 都叠上去,谁也不挑;② 纸写满了会「没地方写」,而 S 永远不会写满,它只会越叠越——旧内容不会消失,而是和新内容混在同一批数字里,查出来是一个被污染的平均值,这比「写不下」更难察觉;③ 笔记可以回头翻页,S 只能用 q 去查,查不准就是查不准,没有「翻回去看一眼」这个动作。第 6.3 节要解决的正是第 ① 和第 ② 条。

一个 DeltaNet 层的状态是 128×128 的矩阵,用 fp32 存。请分别算出:处理 1,000 个 token 时它占多少字节,处理 1,000,000 个 token 时占多少字节。再算出同样两个长度下、一个全注意力层的 KV cache 各占多少(4 个 KV 头 × head_dim 256,fp16)。

先想:这两个量里,哪一个的公式里出现了 token 数?
关键在于状态那一项的答案两次是同一个数,而 KV 那一项差 1000 倍。
第一步这样走:128×128 = 16,384 个数,fp32 每个 4 字节;KV 每层每 token = 2×4×256 = 2048 个数,fp16 每个 2 字节。
完整答案:状态 = 16,384 × 4 = 65,536 字节 = 64 KiB,两种长度下完全一样(这是单个头的账,一层有多个头,见 6.5 节)。KV cache 每层每 token = 2048 × 2 = 4 KiB;1,000 token → 4,000 KiB(约 3.9 MiB);1,000,000 token → 4,000,000 KiB(约 3.8 GiB),差整整 1000 倍。这道题要留下的印象是:两个量根本不在同一个函数族里,一个是常数,一个是 O(T)

变式:如果同时服务 64 个用户,上面四个数字分别变成多少?(提示:状态和 KV 都是每条请求各一份,但权重只加载一份。)这时候「状态与长度无关」还算不算一个决定性优势?

6.3 delta rule:怎么往记事本上写才不糊

式 6-1 有一个致命毛病,一句话就能说清:它只会往上叠,从来不擦。

设想同一个地址被写了两次:第 5 个 token 写下 k, v1,第 900 个 token 又用几乎同一个 k 写下 v2。式 6-1 的结果是 S 里那个方向上同时躺着 v1v2,用 qk 去查,取回来的是 v1+v2——两条信息糊成了一条,而且没有任何办法把它们分开。序列越长,糊得越厉害,几万个 token 之后记事本变成一团噪声。

为什么需要 delta rule

正确的做法是人人都懂的常识:要改一条记录,得先把旧的划掉,再写新的,而不是在旁边又写一行。名字里的 Delta(增量)就是这么来的——每一步写进去的不是 v 本身,而是「v 和现在存着的那个之间的」。

门控增量网络Gated DeltaNet):Yang、Kautz、Hatamizadeh 等人在 arXiv:2412.06464Gated Delta Networks: Improving Mamba2 with Delta Rule,NVIDIA,ICLR 2025)提出的线性注意力层,也是 Qwen3.8 那 48 层(2.4T 是 69 层)线性注意力的原型。它同时装了两个遗忘机制:一个整体的门控,一个定点的 delta rule。

St = (Iβtktkt) Diag(αt) St−1 + βtktvt
式 6-2(Gated DeltaNet 的状态更新,arXiv:2412.06464)
符号是什么直觉 / 它管什么
St状态矩阵,dk×dv记事本本身。整条式子描述的就是「这一步之后记事本变成什么样」
kt本步的键向量(实现里通常先做 L2 归一化,让 ‖k‖=1)要写/要擦的那个地址
vt本步的值向量要写进去的内容
βt写入强度,标量,取值在 0 到 1 之间这一笔写得多用力。β=1 是「旧的全擦掉,换成新的」,β=0 是「这一步什么都不写」,中间值是按比例混合
αt门控gating),取值在 0 到 1 之间,随输入变化整块记事本统一衰减多少。α 接近 1 = 全都留着;α 明显小于 1 = 这一步之后把过去整体调淡,为新内容腾地方
Diag(αt)αt 摆到对角线上的矩阵写成对角阵是为了兼容「每个维度各有一个衰减率」的一般形式;α 退化成一个标量时,它就是「整块乘以同一个数」
(Iβtktkt)擦除算子本章最该看懂的一项。kk 是「投影到 k 这个方向」的算子,I 减去它就是「把 k 方向上的东西拿掉、其余原样保留」。乘上 β 就是「按 β 的比例拿掉」。它只动 k 这一个方向,别的地址上写着的东西一点不碰
+ βtktvt写入项擦完之后,把新内容按同样的力度登记上去

论文自己的总结是一句很短的话:「gating enables rapid memory erasure while the delta rule facilitates targeted updates」(arXiv:2412.06464)。翻成大白话就是分工明确的两个旋钮——门控 α 管「快速整体遗忘」(话题变了,整页调淡),delta rule 管「定点精确更新」(这一条记录改了,只改这一条)。少了门控,换话题时旧内容会一直拖着;少了 delta rule,同一个地址被反复写就会糊。两者互补,缺一个都不行——这也是 6.4 节那个实验室里两个「故意弄坏」开关分别在演示的事。

读的时候要小心:不同文献的写法不完全一样

线性注意力这一族的更新式,在不同论文和不同实现里有几处约定差异:状态是写成 dk×dv 还是 dv×dk(差一个转置)、擦除算子乘在左边还是右边、门控是一个标量还是一个逐维向量。式 6-2 采用的是「状态 dk×dv、算子左乘、门控写成 Diag 的一般形式」这一套。去读 arXiv:2412.06464 原文时,先花两分钟把它的形状约定和这里对齐,否则每一项都会看着眼熟但对不上。本站没有 Qwen3.8 的官方实现源码可核对,式 6-2 的具体转置约定与 Qwen3.8 代码里是否一致,本站未能验证。

自己推一遍:擦除算子为什么长成 (Iβkk) 这个样子

  1. 先不管公式。你想做的事是「把 k 这个地址上现在存着的东西读出来」。用式 6-1 的读取方式,怎么读?

    想好了再看

    读取就是 o = Sq,把 q 换成 k 即可:vold = Sk。这一步之所以是起点,是因为「先擦后写」这句话里的「旧的」必须先能被表达出来,才谈得上减掉。很多人卡在这儿,是因为下意识觉得「记事本里没有一条叫做 k 的记录」——确实没有,但用 k 去查得到的那个东西,就是它。

  2. 现在把 vold 从记事本里减掉。要减的那一项写成矩阵,长什么样?

    想好了再看

    写入一条记录用的是外积 kv,那么抹掉它就是减去 kvold。代进上一步:Sk(Sk) = SkkS = (Ikk)S。擦除算子的骨架就出来了,而且它有个漂亮的几何意义:当 k‖=1 时,(Ikk) 正是「投影到与 k 垂直的那个子空间」。

  3. 为什么还要在前面挂一个 β?直接全擦不好吗?

    想好了再看

    全擦(β=1)意味着模型每见到一个相似的 k 就把之前那条彻底作废。但很多时候新证据只是对旧内容的补充或微调,不该整条推翻。β 让模型自己决定这一笔有多确定——它是从输入算出来的,不是超参数。config 里那个 in_proj_ba 就是专门为它准备的投影(下一节会数出它的形状)。

  4. 把「擦」和「写」合起来,你会得到什么?和式 6-2 对得上吗?

    想好了再看

    St = (Iβkk)St−1 + βkv——正是式 6-2 去掉门控之后的样子。还有一个等价的读法值得自己验一遍:把上式整理成 St = St−1βk(St−1kv),括号里就是「查出来的」减「本该是的」,也就是误差。所以 delta rule 等价于:对「用 kS 应该查出 v」这件事做一步梯度下降,而 β 就是学习率。想到这一层,很多东西会突然变得自然:为什么 β 要限制在 0 到 1(学习率过大会震荡)、为什么 k 要归一化(否则同一个 β 在不同长度的 k 上意味着完全不同的步长)。

  5. 最后:既然 delta rule 已经能改写任意一条记录了,门控 α 为什么还有必要?

    想好了再看

    因为 delta rule 一次只动一个方向。如果一段对话彻底换了话题,此前几千个 token 写下的几千个方向全都该淡出,靠 delta rule 一个一个擦要几千步。门控是一个乘法:S 整体乘以 α一步就把整块调淡。这正是论文那句话里 rapid(快速)和 targeted(定点)的分工——一个负责面,一个负责点。

在式 6-2 里,如果对所有 t 都取 βt = 0,模型会变成什么样?如果对所有 t 都取 αt = 1 且 βt = 1,又会变成什么样?

先想:把这两个数代进式 6-2,哪几项直接消失了?
关键在于 β=0 会让擦除项和写入项同时归零,剩下的只有门控那一项。
第一步这样走:β=0 时式 6-2 变成 St = Diag(α)St−1,看看这个递推展开是什么。
完整答案:① β=0 时 St = Diag(α)St−1,记事本只会一路衰减、再也写不进任何东西,最后趋近于全零矩阵,输出与输入无关——这一层等于被关掉了。② α=1 且 β=1 时式子变成 St = (Ikk)St−1 + kv:从不整体遗忘,但每一步都把当前 k 方向彻底覆写。这是「纯 delta rule、无门控」的版本,也正是 6.4 节实验室里那个关掉门控的开关——它在长序列上的表现值得亲眼看一次。

变式:如果去掉擦除项、只保留 St = Diag(α)St−1 + βkv(这其实就是不带 delta rule 的门控线性注意力),同一个地址被写两次时会发生什么?和 6.3 节开头那个「糊」的例子比,有区别吗?

6.4 亲手跑一遍:递推形式 vs 分块并行形式

式 6-2 有一个工程上的麻烦:它是递推的。St 要等 St−1 算完才能开始,一个 token 接一个 token,没法并行。推理时这不成问题(本来就是一个字一个字往外蹦),但训练时是灾难——训练时整段几万 token 的序列是一次性喂进去的,如果被迫串行走几万步,GPU 上几万个计算单元会一直闲着等。

分块并行形式chunkwise parallel form):把序列切成固定长度的块(比如每 64 或 128 个 token 一块),块内用矩阵乘法一次算完,块与块之间只传递一个状态矩阵。于是串行的步数从「token 数」降到「块数」,而块内是 GPU 最喜欢的稠密矩阵乘。

这里有一条铁律:两种形式必须在数学上严格等价。训练用分块并行、推理用递推,如果两条路径算出来的东西不一样,那就是「训练的模型」和「部署的模型」不是同一个模型——症状是训练指标好看、一上线就胡说,而且极难定位。所以任何一份 DeltaNet 实现,第一件事都是拿这两条路径互相对拍。

上面这个实验台就在做这件事:同一组随机的 q, k, v, α, β,一路按式 6-2 逐 token 递推,另一路按分块并行算,然后逐元素比最大绝对差。如果实现是对的,这个差应该只剩浮点舍入的量级——双精度下大约 1e-16,单精度下大约 1e-6。它不会是 0(加法的顺序不同,舍入就不同),但也绝不该是 1e-2。

实验台上还有两个「故意弄坏」的开关,建议都拨一遍:

  • 关掉门控(强制 α=1):把序列拉长,看状态矩阵的数值范围怎么一路涨上去,以及读出来的结果怎么逐渐变成一团分不开的平均。这是 6.3 节说的「糊」,眼见为实。
  • 关掉 delta rule 的擦除项(只留 Diag(α)S + βkv):这时两条路径仍然各自自洽,但它们算的已经不是同一个递推了;把这个残缺版的递推结果和完整版比,误差会从 1e-16 直接跳到 1e-1 量级——差好几个数量级。这个跳变正好说明擦除项不是可有可无的修饰。

你实现了分块并行版本,和递推版本对拍。发现:块长设成 1 时误差是 1e-16,块长设成 64 时误差是 3e-2。请判断 bug 最可能在哪一段代码,并说明你的推理链条。

先想:块长为 1 意味着什么?这时分块并行退化成了什么?
关键在于块长 1 时每个块只有一个 token,块间传递就是逐 token 传递——这条路正好把「块内计算」这段代码绕过去了。
第一步这样走:把块长依次设成 1、2、4、8,看误差是从哪一个块长开始变大的。
完整答案:块长 1 正确说明块间状态传递和整体框架是对的;一加块长就错,说明 bug 在块内那段矩阵化的推导里。最常见的两处:一是块内的因果掩码写漏了或写反了(块内第 i 个 token 只能看到第 ji 个,写成 j<i 或者干脆没掩码,都会在块长>1 时才暴露);二是块内门控的累积乘积算错了——块内第 i 个位置看到的旧状态应该被 α 从块首连乘到 i,少乘一项或多乘一项,块长 1 时那个连乘只有一项,恰好看不出来。用块长 2 复现是最省事的定位办法:只有两个 token,可以手算出正确值逐个对。

变式:如果反过来——块长 1 时误差就有 1e-2,而块长 64 时误差还是 1e-2 且数值几乎不变,你的判断会怎么改?

6.5 Qwen3.8 的 DeltaNet 层长什么样

mat/cfg27b.json 里跟线性注意力有关的几行挑出来,逐条读:

表 6-2:Qwen3.8-27B 的 Gated DeltaNet 配置(mat/cfg27b.json 原文字段)
字段含义
linear_num_value_heads48值(V)头数。注意它比 QK 头多,这是线性注意力版的分组共享
linear_num_key_heads16键(K)头数。48 ÷ 16 = 3,每个 K 头被 3 个 V 头共用
linear_key_head_dim128每个 K 头的宽度 dk
linear_value_head_dim128每个 V 头的宽度 dv
linear_conv_kernel_dim4因果卷积的核长(6.6 节专讲)
mamba_ssm_dtypefloat32循环状态用 fp32 存,不跟随权重精度

最后一行值得多说一句。mamba_ssm_dtype 这个字段名里的 Mamba 直接说明了血缘:Gated DeltaNet 属于 Mamba 这一系的状态空间 / 线性循环模型,它的论文副标题就是「Improving Mamba2 with Delta Rule」。而把状态钉死在 fp32 是有原因的:状态是一路乘下去的(门控 α 逐步连乘),几万步之后 bf16 的舍入误差会累积到不可忽略。这一行配置本身就是「这个部件对数值精度敏感」的官方声明。

参数账

mat/paramcount.out.txt 里逐张矩阵数出来的结果:

表 6-3:一个 Gated DeltaNet 层的参数(Qwen3.8-27B,hidden_size 5120)
矩阵形状参数量干什么的
in_proj_qkvz5120 × 1638483,886,080(83.89 M)一次投出四样东西:q 2048 + k 2048 + v 6144 + z 6144。z 是输出门(第 4 章那个门控的同款思路)
in_proj_ba5120 × 96491,520(0.49 M)投出 βα:48 + 48 = 96,每个 V 头各一个标量
conv1d10240 × 440,960作用在 q、k、v 上的短因果卷积
out_proj6144 × 512031,457,280(31.46 M)把 48×128 = 6144 维的输出投回 5120 维主干
合计115,875,840(115.88 M)

in_proj_ba 那一行是本节最有信息量的一处:96 = 48 + 48,说明 βα 各是「每个值头一个标量」,而不是每个维度一个数。式 6-2 里写成 Diag(α) 的一般形式,在 Qwen3.8 这里退化成了「整块乘同一个数」。这不是从文档上抄来的,是从矩阵形状上出来的——config 里没有任何一行字直接说这件事。

常见误解:线性注意力是「更轻的注意力」,所以参数更少

对照一下第 4 章数过的全注意力层:Gated Attention 层 104.86 M,Gated DeltaNet 层 115.88 M。线性注意力层比全注意力层还大 10.5%

差在哪?拆开看:全注意力层的输入投影一共 12288(q 含门)+ 1024(k)+ 1024(v)= 14336 维,DeltaNet 是 16384 维,多出的 2048 维乘 5120 就是 10.49 M;再加上 in_proj_ba 的 0.49 M 和卷积的 0.04 M,正好 11.02 M,与 115.88 − 104.86 = 11.02 分毫不差。根子在于 DeltaNet 有 48 个值头(6144 维)而全注意力只有 4 个 KV 头(1024 维)——GQA 省下来的那笔钱,线性注意力没省,因为它的值头不共享。

所以要把话说准:线性注意力省的不是参数,省的是 KV cache 和长序列上的计算量。这两件事完全是两笔账。整个模型层面:48 个 DeltaNet 层合计 5.56 B,占 27.36 B 的 20.3%;16 个全注意力层合计 1.68 B,占 6.1%——线性注意力层在这个模型里花的钱是全注意力的三倍多。

记事本到底有多大

状态矩阵是 dk×dv = 128×128 = 16,384 个数,每个头一份。一层有多少份,取决于状态是按 K 头分还是按 V 头分。按 Gated DeltaNet 原论文的定义,每个值头各持有一个 dk×dv 的状态矩阵,键/查询头通过重复对齐到值头——所以份数看的是 linear_num_value_heads

  • 48 个 V 头各存一份(本站口径):48 × 128 × 128 = 786,432 个数/层 → fp32 3 MiB/层,48 层共 144 MiB
  • (如果误按 16 个 QK 头算:16 × 128 × 128 = 262,144 个数/层 → 1 MiB/层,48 层共 48 MiB——这是本站早先的算法,已作废
读的时候要小心:这个数是本站按论文定义推的,官方与框架都没公布过

config.json 只给了头数和头宽,没有任何一个字段直接说明状态张量的形状,而本站手上没有 Qwen3.8 的实现源码可以核对。定这个口径靠的是两条:其一,Gated DeltaNet 原论文里状态是每个值头一个 dk×dv 矩阵;其二,表 6-3 那一行 in_proj_ba = 5120×96 说明 βα按 48 个值头各出一个的——既然每个值头有自己独立的写入强度和遗忘率,它们的状态就没法共用同一块。所以全站统一取 3 MiB/层、全模型 144 MiB。但这是推导,不是核实;不同框架实现时还可能多留一份卷积状态或做双缓冲,实际占用会略高(第 7 章 7.5 节)。

好在这个数字上的不确定性不影响本章的结论:无论 1 MiB 还是 3 MiB,它都是一个与序列长度完全无关的常数。放在第 5 章表 5-1 的 262K 那一行(16.00 GiB)旁边,两个数都不到 1%——第 5 章表 5-2 把它按 144 MiB = 0.14 GiB 单列成一行。真正需要小心的是另一件事——这份状态是每条请求各一份的,同时服务 64 个用户就是 64 份(144 MiB × 64 ≈ 9 GiB)。它不随长度涨,但随并发涨,写显存预算时不能当成零。

用表 6-2 的字段验算 in_proj_qkvz 的输出维度 16384 是怎么来的(提示:q 和 k 用 key 头,v 和 z 用 value 头)。再算出 out_proj 的输入维度为什么是 6144。

先想:q 有几个头、每个头多宽?把这四样(q、k、v、z)各自的总维度分别算出来再相加。
关键在于 q、k 用的是 16 个 key 头,v、z 用的是 48 个 value 头,两组的头维都是 128。
第一步这样走:16 × 128 = 2048(这是 q 的总维,k 同理);48 × 128 = ?(这是 v 的总维,z 同理)。
完整答案:q = 16×128 = 2048,k = 2048,v = 48×128 = 6144,z = 6144。相加 2048+2048+6144+6144 = 16384,对上了。参数量 5120 × 16384 = 83,886,080,与表 6-3 一致。out_proj 的输入是 6144,因为注意力的输出维度由决定(每个值头输出 128 维,48 个头拼起来 6144 维),再投回主干的 5120 维:6144 × 5120 = 31,457,280。

变式:Qwen3.8-2.4T-A95B 的 linear_num_value_heads 是 128、linear_num_key_heads 是 16、hidden_size 是 8192,头维同样 128。请算出它一个 DeltaNet 层的 in_proj_qkvz 参数量,再去 mat/paramcount.out.txt 里核对。

答辩:如果我是审稿人

你自己算出来 DeltaNet 层 115.88 M 比全注意力层 104.86 M 还大,48 层加起来占了模型 20.3% 的参数,是全注意力的三倍多。那这个「线性注意力」到底省了什么?说好的更高效呢?

参考防守(先自己组织语言再看)

问得对,「高效」这个词必须指明是哪一项的高效。三笔账要分开:

参数:不省,还多花了。115.88 M vs 104.86 M,多 10.5%。这是事实,本章用 callout pitfall 明确标了出来。

推理时的显存:省得极多。一个全注意力层在 262K 上下文下的 KV 是 1 GiB(16.00 GiB ÷ 16 层),而一个 DeltaNet 层的状态是 3 MiB 且与长度无关——在长上下文这个场景下,差三个数量级。

长序列上的计算量:省得更多。全注意力第 t 步要做 t 次点积,累计 O(T2);DeltaNet 每步是固定大小的矩阵更新,累计 O(T)。T 小的时候前者更快(常数小、算子成熟),T 一大就完全反过来。

所以准确的说法是:它用更多的参数(一次性成本,训练前就付了),换掉了随长度增长的两项运行时成本。参数是买断,KV cache 是每次对话都要重新付的租金。这也解释了为什么它不能全用——见 6.7 节。

6.6 那个 4 长的因果卷积在干嘛

linear_conv_kernel_dim: 4。这个部件小到几乎会被忽略:40,960 个参数,是 27B 里最小的部件之一。对比一下——FFN 里随便一张矩阵是 89,128,960 个参数,是它的 2176 倍;全部 48 层的卷积加起来 1,966,080 个参数,占 27.36 B 的 0.007%

因果卷积causal convolution):沿着序列方向、只往回看不往前看的滑动窗口。核长 4 意味着每个位置的输出由「它自己 + 前 3 个 token」这四个位置线性组合而来。「因果」就是「不许看未来」的意思,和注意力里的因果掩码是同一个约束。

它在补什么?纯线性注意力的短程能力偏弱。回想式 6-1 的读取方式:o = Sq——这是一次「按内容查询」,它对「谁挨着谁」是天然不敏感的。「上一个 token 是什么」这种最基本的局部信息,理论上可以由状态里的某个方向携带,但要靠训练把它挤出来,代价不低。而一个核长 4 的卷积用四万个参数就把这件事办了:每个位置直接拿到最近 4 个 token 的线性组合。用最便宜的部件解决最局部的问题,把状态矩阵腾出来干远距离的活。

顺带算一笔常被忽略的账:卷积也需要一个小缓存——要算下一个位置,得记住最近 3 个 token 在那 10240 个通道上的值。fp32 下约 120 KiB/层。它同样与序列长度无关,同样是每条请求各一份。

全部 48 层的因果卷积加起来是 1,966,080 个参数。请问:它相当于 FFN 里几张矩阵?相当于嵌入层(1,271,398,400 参数)的几分之一?如果把这些卷积全部删掉,模型体积会变化多少个百分点?

先想:一张 FFN 矩阵是 89,128,960,拿 1,966,080 去比一比。
关键在于这道题要的是尺度感,不是精确小数——量级对了就行。
第一步这样走:1,966,080 ÷ 89,128,960 ≈ 0.022,也就是不到一张 FFN 矩阵的 1/45。
完整答案:约等于一张 FFN 矩阵的 1/45(0.022 张);是嵌入层的 1/647;占全模型 27.36 B 的 0.0072%,删掉它体积基本不变(连 0.01% 都不到)。但这道题真正的意思在反面:参数占比和重要性完全不是一回事。这四万个参数管的是「看清最近 4 个 token」,删掉它模型体积几乎不变,能力却会实实在在地掉一块。任何按「参数占比」给部件排重要性的做法,在这里直接失效。

变式:把核长从 4 改成 16,参数量变成多少?占比变成多少?你能想到什么理由让设计者把它加大?(提示:想想核长 16 的卷积和「只有 16 层全注意力」之间是什么关系。)

6.7 代价:它记不住细节

前面六节讲的都是好处。现在讲代价,而且这个代价不是工程上没做好,是数学上必然的

一个数数就能说清的论证:状态矩阵的键那一侧只有 dk = 128 维。任何 128 维空间里,最多只能有 128 个线性无关的向量。序列走到第 129 个 token 时,它的 k129 必然可以写成之前某些 k 的线性组合——也就是说,往这个「新地址」上写东西,不可能不动到旧地址上的内容。到了 262,144 个 token,平均每一个键方向上挤着两千多个 token。

相比之下,全注意力在 262K 上下文下老老实实存了 262,144 份 K 和 V,每一份都在原地,一个字节都没混。这就是那 16 GiB 买到的东西:精确回忆。

一句话记住:固定大小的状态一定会丢信息,这是鸽笼原理,不是实现问题。DeltaNet 的 delta rule 和门控做的是「把该丢的丢掉、把该留的留住」这件事,而不是「不丢」。

具体会在什么任务上翻车?典型的是「大海捞针」(needle in a haystack)这一类:在十几万字里藏一句无关紧要的话,然后问模型那句话说了什么。这种任务要的正是逐字精确回忆一个没有任何语义重要性的细节——恰好是固定大小状态最不擅长的:它在压缩的时候必须有所取舍,而一句「不重要」的话在写入的那一刻看不出后面会被问到。

读的时候要小心:这一节讲的是一般性局限,不是对 Qwen3.8 的实测断言

本站没有 Qwen3.8 的大海捞针实测数据。官方没有发布技术报告,模型卡上也没有这类曲线。上面这一段说的是线性注意力这一族的共性局限,依据是「固定维度的状态无法无损保存任意长的历史」这个数学事实,以及混合架构论文里的消融实验——Jamba(arXiv:2403.19887)和 Samba(arXiv:2406.07522)都用实验论证过为什么必须保留少量全注意力层。把这些结论套到 Qwen3.8 上是一个合理的推断,但它是推断,不是实测。

那么,一个自然的问题:既然线性注意力有这个硬伤,为什么不干脆全用全注意力?答案在第 5 章已经算过——262K 要 16 GiB,1M 要 61.65 GiB,而且这还是打了四折之后的数字。两条路各有一个致命缺点:全注意力记得住但装不下,线性注意力装得下但记不住。Qwen3.8 的选择是两个都用,按 3:1 混着排。下一章专门讲这个比例:为什么是 3:1,为什么那 16 层全注意力必须留着,以及它们各自在这个模型里承担了什么。

你要设计一个测试,用来判断某个模型的某一层是「全注意力」还是「固定大小状态的线性注意力」,但你只能从外部观察(不能看源码、不能看 config),可以自由构造输入并测量显存和输出。请给出至少两种互相独立的判据。

先想:这两种机制在「输入变长时,什么东西会变」这件事上表现完全不同。
关键在于找出两条互相独立的证据链:一条从资源占用入手,一条从能力表现入手。
第一步这样走:先做最省事的那条——把输入从 1K 加到 10K,盯着显存占用曲线的形状。
完整答案:判据一(资源侧):把上下文从 1K 逐步加到 100K,记录显存占用。全注意力会画出一条明显的上升直线(斜率 = 每 token 的 KV 字节数);纯线性注意力那部分是一条水平线。斜率还能反推出全注意力层数——这正是第 5 章式 5-1 的逆用。判据二(能力侧):做大海捞针,把同一句话藏在长度递增的上下文里,画「命中率 vs 上下文长度」。全注意力的曲线在训练长度内应该基本平坦;固定状态的曲线会随长度下滑,而且下滑发生在特定长度之后。判据三(更硬的一条):测单 token 生成延迟随已有上下文长度的变化——全注意力是线性增长(每步要和全部历史做点积),固定状态是常数。这三条分别测显存、能力、时延,任何一条单独都可能被别的实现细节干扰(比如框架的显存预分配会毁掉判据一),三条一起对上才可信。

变式:如果被测的是一个 3:1 混合模型(像 Qwen3.8 这样),上面三条判据分别会给出什么样的曲线?你能只凭这些外部观测反推出「16 层全注意力」这个数吗?

答辩:如果我是审稿人

你说递推形式和分块并行形式「数学上严格等价」,但你自己又说浮点下的误差是 1e-6(fp32)。那到底等不等价?一个 1e-6 的误差,在几万步的递推里会不会被放大成一个真实的行为差异?

参考防守(先自己组织语言再看)

「数学上等价」和「浮点下相等」是两件事,这个质疑戳的正是它们的缝。

等价性是在实数上成立的:两条路径推导出的表达式逐项相同。浮点下不相等,是因为加法的结合顺序不同——递推是一步一步加,分块是块内先并行加再合并,浮点加法不满足结合律,所以必然有舍入差。1e-6(fp32)就是这个差的量级,它不是 bug

会不会被放大?这才是要害,而答案是「取决于门控」。α 小于 1 时,历史误差每一步都被乘以 α,是指数衰减的——这种递推对扰动是稳定的,早期的舍入误差会被自然遗忘掉。但如果 α 长期贴近 1(模型学到「什么都别忘」),衰减就几乎不发生,误差会一路累加。这正是 mamba_ssm_dtype 设成 float32 这一行配置存在的理由:把状态钉在 fp32,就是在给这个累积留足余量。

所以诚实的说法是:等价是数学上的,稳定是靠门控和 fp32 一起保住的,而不是天上掉下来的。这也解释了 6.4 节那个对拍为什么必须做——它同时在验两件事:推导对不对,以及数值上稳不稳。

答辩:如果我是审稿人

6.7 节那个「128 维最多装 128 个线性无关向量」的鸽笼论证听起来很有力,但它是不是证明得太多了?按同样的逻辑,任何有限维的神经网络都装不下任意长的历史,那全注意力的 KV cache 不也是有限的吗?这个论证凭什么单挑线性注意力?

参考防守(先自己组织语言再看)

这个反驳有力,需要把论证的边界说清楚。

区别不在「有限」,在有限的东西随不随长度增长。KV cache 在 262K 上下文下确实也是有限的(16 GiB),但它是 O(T) 的:每来一个 token 就新增一块专属空间,历史 token 之间不共享存储,所以不存在「必须覆写」这回事。DeltaNet 的状态是 O(1) 的:所有 token 共用同一块 128×128,第 129 个 token 的键必然落在前面那些键张成的空间里,写它就必然动到别人。

所以论证的准确形式不是「有限所以会丢」,而是「T 无关的容量,配上无限增长的 T,必然出现覆写」。它单挑的不是线性注意力,是一切固定大小状态的循环模型——包括 Mamba、RWKV、以及老式 RNN。反过来,这个论证也不能推出「线性注意力没用」:会覆写不等于会覆写掉重要的东西,delta rule 和门控要做的正是让覆写发生在该发生的地方。至于它做得好不好,那是实证问题,需要 6.7 节那个 callout caution 里承认的、本站没有的实测数据。

一个团队想把 Qwen3.8-27B 部署成一个「每人 200K 上下文、同时服务 32 人」的服务,手上是一张 80GB 的卡。请估算总显存需求(Q4_K_M 权重、不加载视觉塔、fp8 KV),并指出这套配置里哪一项是主要瓶颈。如果要把并发从 32 提到 128,你会先改哪一项,为什么?

先想:这个场景里有三笔账——权重、KV cache、DeltaNet 状态。哪些随并发涨,哪些不涨?
关键在于权重只加载一份,而 KV 和状态都是每人一份。先分别算出「每人一份」是多少。
第一步这样走:fp8 KV 是 32 KiB/token,200K token 每人 = 200,000 × 32 KiB ≈ 6.10 GiB。乘 32 人。
完整答案:权重 Q4_K_M 17.11 GB(不加视觉塔),只算一份。KV:每人 200,000 × 32 KiB ≈ 6.10 GiB,32 人 ≈ 195 GiBDeltaNet 状态:每人 144 MiB(48 层 × 3 MiB,见 6.5 节),32 人 ≈ 4.5 GiB。合计约 216 GiB(权重 17.11 GB ≈ 15.93 GiB),一张 80GB 的卡差了将近三倍,这套配置跑不起来。瓶颈毫无疑问是 KV cache,它占了 90% 以上,而且是唯一同时随长度并发两个方向增长的项(另外两项各只随一个方向)。要提并发到 128,先动的一定是每人的上下文长度——KV 与「长度 × 并发」成正比,砍长度和砍并发在显存上是等效的,但砍长度通常更容易被产品接受(200K 降到 50K,KV 降到 1/4,正好补上并发的 4 倍)。动权重量化没用(权重只占 17 GB 里的一份,压到 IQ2 也只省 8 GB),动 DeltaNet 状态更没用(它总共才几 GiB)。这道题真正要练的是:先分清哪些项随哪个维度增长,再决定拧哪个旋钮。

变式:如果这个团队改用 Qwen3.8-2.4T-A95B(92 KiB/token fp16、46 KiB/token fp8),同样的「200K × 32 人」需要多少 KV?光是 KV 就要几张 141GB 的卡?

真未解一块 128×128 的状态,到底能可靠地记住多少条互不干扰的记录?

6.7 节那个鸽笼论证只给出了一个上界方向的定性结论:超过 128 个线性无关的键就一定会互相干扰。但真正想知道的是定量的那一半——在 delta rule + 门控的更新规则下,给定「查询命中率不低于 p」这个要求,状态能同时维持多少条记录?这个容量随 dk 怎么变(线性?还是像 Hopfield 网络那样有个别的阶)?和门控的取值分布是什么关系?据本站所知,Gated DeltaNet 论文(arXiv:2412.06464)没有给出这样一条容量曲线,本站也没有在公开文献里找到。这个问题重要,是因为它直接决定「3:1 里那个 3 能不能变成 7」——而现在这个比例是靠实验试出来的,不是算出来的。

先做这一步:在 6.4 节那个实验台里,构造 N 对互相正交的 (k, v),按顺序写进一个 dk=128 的状态,然后逐条用 ki 查回来,测 ‖查出来的 − vi。把 N 从 8 一路加到 512,画误差曲线,看它在哪里开始拐;再把 dk 换成 32、64、256 各跑一遍,看拐点是不是就落在 Ndk 上。最后把「正交的键」换成「随机的键」(真实模型里的键不会正交),看拐点提前了多少——这一步的结果会比前面几步有意思得多。

这一层要加什么:加一个 Gated DeltaNet 层

为什么现在才加它:第 5 层给注意力装上了 KV cache,你手上第一次有了一个「能一个字一个字往外生成」的东西——也第一次能亲眼看到显存随着生成长度一路往上爬。这一层要造的,是同一个位置上的另一种选择:同样接收一串 token、同样输出一串向量,但它的状态从头到尾是同一块内存。造完这一层,第 7 层才有得挑(3 个这种 + 1 个那种)。

state = zeros(H, d_k, d_v) # 一层一份,形状与序列长度无关 conv_buf = zeros(conv_dim, 3) # 最近 3 个 token,供 kernel=4 的因果卷积用 def deltanet_step(x): q, k, v, z = in_proj_qkvz(x) # 5120 -> 2048 + 2048 + 6144 + 6144 b, a = in_proj_ba(x) # 每个 value 头各一个 β、一个 α q, k, v = causal_conv(q, k, v, conv_buf) k = l2_normalize(k) # 让 kkᵀ 成为真正的投影算子 beta = sigmoid(b) # 写入强度 ∈ (0,1) alpha = exp(-softplus(a)) # 遗忘门 ∈ (0,1) state = alpha * state # ① 先整体遗忘 old = state.T @ k # ② 再读:衰减之后这个地址存的是什么 state = state + outer(k, beta * (v - old)) # ③ 擦旧 + 写新,一步完成 return out_proj(rmsnorm(state.T @ q) * silu(z)) # z 是输出门

难点:三处顺序与归一化,错一个就静默地变成另一个模型。遗忘必须在读取之前。把式 6-2 展开就知道,old 读的是衰减后的状态;如果写成「先读 old 再乘 alpha」,你得到的是一个不同的递推——它自己也自洽、也能训练,但和分块并行版本对不上,6.4 节那个对拍会给你 1e-1 量级的误差,而不是 1e-6。② k 必须 L2 归一化(Iβkk) 只有在 k‖=1 时才是「按 β 比例擦掉 k 方向」;不归一化的话 kk 会带上 k2 这个缩放,β 的含义就随输入漂移,k‖>1 时甚至会擦过头让状态发散。③ 状态用 fp32——这不是保守,是 config 里 mamba_ssm_dtype 设成 float32 明写的,理由见上面那条答辩。

自己验:第一条(这一层的定义性质)——把序列从 100 个 token 加到 10,000 个,打印 state.nbytes,两次必须一个字节都不变(按 16 个 QK 头存是 16×128×128×4 = 1 MiB/层,按 48 个 V 头存是 3 MiB/层,取决于你怎么分头;是哪个不重要,不变才重要);同一段代码里第 5 层的 KV cache 会从 x 涨到 100x。这两个数放在一起打出来,就是这一整章的结论。第二条(实现对不对)——把同一段输入分别用递推形式和分块并行形式(块长 64)各跑一遍,取输出的最大绝对差:fp32 下应该在 1e-6 量级以内;如果到了 1e-2,说明分块边界上的状态传递写错了,先把块长设成 1 复现一次(块长 1 还错说明框架就错了,块长 1 对、块长 2 就错说明块内那段推导错了,见 6.4 节那道题)。

本章小结

  • KV cache 是 O(T) 的,量化只能改常数因子改不了增长阶。1M 上下文的 KV(61.65 GiB)比 BF16 全精度权重(约 50.9 GiB)还大。
  • 去掉 softmax 之后,点积可以重排结合,整段历史被压进一个 dk×dv 的矩阵:St = St−1 + ktvtot = Stqt状态的形状里没有 T
  • 朴素累加会糊。delta rule 的办法是先擦后写(Iβtktkt) 是擦除算子,只动 k 这一个方向;β 是写入强度。它等价于对「用 kS 应查出 v」做一步梯度下降。
  • 门控 α 管整体快速遗忘,delta rule 管定点精确更新——论文原话是「gating enables rapid memory erasure while the delta rule facilitates targeted updates」(arXiv:2412.06464)。
  • 训练用分块并行、推理用递推,两条路径必须数学等价;浮点下的差应该只剩舍入量级(fp32 约 1e-6)。
  • Qwen3.8-27B 的 DeltaNet 层:48 V 头 / 16 QK 头 / 头维 128 / 卷积核 4 / 状态用 fp32。参数 115.88 M比全注意力层的 104.86 M 还大 10.5%——它省的不是参数,是 KV cache 和长序列上的计算量。
  • 那个 4 长的因果卷积只有 40,960 个参数(全模型的 0.007%),补的是纯线性注意力的短程弱点。参数占比和重要性完全是两回事。
  • 代价是数学上必然的:128 维的键空间最多容纳 128 个线性无关的方向,第 129 个 token 起必然覆写。大海捞针这类需要精确回忆的任务,固定状态打不过全注意力。
  • 下一章:既然一个记得住装不下、一个装得下记不住,那就两个都用。为什么是 3:1?
我能不看材料说清:擦除算子 (Iβkk) 每一部分在干什么,以及门控 α 为什么不能被 delta rule 替代。

第7章 3:1 混合布局:两种记忆的分工

这一章回答一个工程问题:既然全注意力太贵、线性注意力又记不准,那到底该放多少层全注意力?Qwen3.8 的答案写在 config.json 的一个数字里——full_attention_interval: 4

学完这一章你应该能做到

  • 用自己的话说清:为什么「全是注意力」和「全是线性注意力」这两个极端都不能用
  • 看着 config.json 数出 Qwen3.8-27B 的 64 层里哪 16 层是全注意力,并说出它们的层号
  • 算出 262K 上下文下混合布局省了多少显存,并说清这笔钱是拿什么换的
  • 把 27B、2.4T、Qwen3-Next 三个模型的层布局写成同一个模板,指出它们只差在重复次数
  • 指出这套设计给推理框架带来的真实工程麻烦
前置:第4章的门控注意力层(24 Q 头 / 4 KV 头 / head_dim 256)、第5章的 KV cache 与那条每 token 显存公式、第6章的 Gated DeltaNet 固定大小记事本。这三样凑齐了,这一章才有意义——它讲的正是「这两种层按什么比例混在一起」。

7.1 两个极端都不行

不做混合会怎样

假设你是 Qwen3.8 的架构师,桌上只有两种零件:第 4 章那种全注意力层full attention),和第 6 章那种 Gated DeltaNet 线性层。最省事的做法是只用一种,全部堆满 64 层。这一节就是把这两条捷径分别走到头,看它们各自撞在什么墙上。

捷径一:64 层全是注意力

第 5 章的公式还记得吧——每个 token 要在每一个全注意力层里留下一份 K 和一份 V。把 64 层全做成注意力层,这笔账是这样的:

BKV = 2 × Lfull × Hkv × dhead × b
式 7-1
符号是什么Qwen3.8-27B 里是多少
BKV每一个 token 要占的 KV cache 字节数就是要算的那个数
2K 和 V 各存一份,所以乘 22(这个永远是 2)
Lfull有 KV 的层数——注意不是总层数,是全注意力层的数量16(这一章的主角)
Hkv每层的 KV 头数(第 4 章的 GQA:24 个 Q 头共用 4 个 KV 头)4
dhead每个头的维度256
b一个数值占几个字节fp16 是 2,fp8 是 1

Lfull 填成 64(假装 64 层全是注意力),fp16:2 × 64 × 4 × 256 × 2 = 262,144 字节 = 256 KiB,每个 token 都要这么多。Qwen3.8 的原生上下文是 262,144 个 token,乘起来是 64 GiB

64 GiB 是什么概念?一张 RTX 4090 是 24 GB 显存,你需要三张 4090 只用来放 KV cache——模型权重还一个字节都没放呢。这条路直接死掉。

捷径二:64 层全是 Gated DeltaNet

那反过来,全用第 6 章那种固定大小的记事本?KV cache 直接归零,显存问题一夜消失。代价出现在另一个地方:精确回忆

大海捞针Needle in a Haystack):一类专门用来测长上下文的任务。做法是把一句无关的话(针)藏进一篇几十万字的长文(草堆)里,然后问模型那句话是什么。它考的不是理解,是逐字复述某个具体位置的内容

全注意力层为什么能做对这种题?因为它把每个 token 的 K、V 原封不动地存着,要找第 137,492 个 token 时,直接去那一格取就行——它是一本翻得到任意一页的书。

Gated DeltaNet 不是书,是一块固定大小的白板。第 6 章讲过,它的状态矩阵形状是固定的,新信息进来就要往上写,写多了就得擦掉一些旧的(那个门控就是在决定擦多少)。读了 20 万字之后,白板上留下的是一份被反复覆写过的摘要。摘要能答「这篇文章讲什么」,答不了「第 137,492 个字是什么」。

打个比方

全注意力像录像:占硬盘,但任意一帧都能倒回去逐帧看。Gated DeltaNet 像你自己记的会议纪要:一页纸就够,但别人问「第 3 个人说的那个订单号是多少」,你只能干瞪眼——除非你当时正好觉得那个号重要,把它记下来了。

类比失效处:会议纪要是人挑重点记的,而 DeltaNet 的「挑」是训练出来的一组权重,它不知道你等会儿要问什么。所以它不是「记了重点」,而是「记了训练时统计上有用的东西」。

这里要格外小心一件事:并不是说线性注意力「不行」。它在语言建模的整体困惑度上可以做得很接近,甚至在长文本吞吐上远远胜出。它塌下去的是一小类但极其常用的能力:精确定位、逐字复述、多跳查表。而这类能力恰好是你把一份 20 万 token 的代码库丢给模型时最需要的那种。

一句话记住:全注意力买的是「任意位置精确可取」,Gated DeltaNet 买的是「显存不随长度增长」。这两样东西必须同时要,所以只能混着用。

有人拿 Qwen3.8-27B 的参数按式 7-1 算 KV cache,把 Lfull 填成了 64,得到每 token 256 KiB。他错在哪一步?正确的数应该是多少?

先想:式 7-1 里那个 L 的下标写的是 full,不是 total。这个下标是随便写的吗?
关键在于:KV cache 只由「会产生 K 和 V 的层」贡献。Gated DeltaNet 层根本不产生 K、V 缓存,它只有一个固定大小的状态。
第一步:先数清楚 64 层里有几层是全注意力。config.json 里 full_attention_interval = 4,64 ÷ 4 = 16。把 16 代进式 7-1。
完整答案:他把「总层数」当成了「有 KV 的层数」。正确算法是 2 × 16 × 4 × 256 × 2 = 65,536 字节 = 64 KiB/token,不是 256 KiB。他整整高估了 4 倍——因为 64 层里有 48 层是 Gated DeltaNet,它们的状态是固定大小的,不随序列长度增长,一个字节的 KV cache 都不占。262K 满上下文下,真实值 16 GiB,他算出来的是 64 GiB。

变式:Qwen3.8-2.4T-A95B 有 92 层,同样是每 4 层一次全注意力,KV 头数 4、head_dim 256。它每 token 的 fp16 KV 是多少 KiB?(答案:Lfull = 92 ÷ 4 = 23,2 × 23 × 4 × 256 × 2 = 94,208 字节 = 92 KiB。)

7.2 Qwen3.8 的答案:每 4 层放 1 层全注意力

两个极端都走不通,那中间路线在哪儿?Qwen3.8 的答案朴素得有点让人失望:每 4 层里放 1 层全注意力,其余 3 层用 Gated DeltaNet。这就是本章标题里的 3:1。

这不是我推断的,是 config.json 里明写的一个数:

"full_attention_interval": 4

「interval 4」的意思是:每隔 4 层出现一次全注意力层。从第 1 层开始数,第 1、2、3 层是线性层,第 4 层是全注意力;第 5、6、7 层是线性,第 8 层是全注意力……一直到第 64 层。所以全注意力层的层号就是 4 的倍数:4、8、12、16、20、24、28、32、36、40、44、48、52、56、60、64——正好 16 个。

Lfull = NI = 644 = 16, Llinear = NLfull = 48
式 7-2
符号是什么直觉
N总层数,config 里的 num_hidden_layers64(27B)/92(2.4T)
I间隔,config 里的 full_attention_interval4——本章唯一的主角
Lfull全注意力层数有 KV cache 的层,会随上下文长胖
LlinearGated DeltaNet 层数状态固定大小,长度翻倍它也纹丝不动
config.json 里有两处互相印证,都去看一眼

一处是开关:full_attention_interval: 4,一句话定下整张图纸。另一处是清单:layer_types,一个长度 64 的数组,逐层写着 linear_attention 还是 full_attention。你去模型卡的 config.json 里把这个数组翻出来,一个一个数下去,会看到 linear、linear、linear、full、linear、linear、linear、full……严格的 3+1 循环,一共 48 个 linear、16 个 full。

两处必须对得上——如果哪天你遇到一个模型,它的 layer_typesfull_attention_interval 对不上,那就是这份 config 有问题,别信任何一个。(本站 mat/cfg27b.json 是抓下来的精简副本,只保留了 full_attention_interval 这一项;完整的 64 项数组请直接看模型卡原文。)

Qwen3.8-27B 64 层 = 16 组 ×(3 × Gated DeltaNet + 1 × Gated Attention) 全注意力(有 KV,16 层) Gated DeltaNet(固定态,48 层) 组内第 4 层 组内第 3 层 组内第 2 层 组内第 1 层 第 1 组 第 8 组 第 16 组 从左往右 16 组,每组自下而上 4 层。第 4、8、12、…、64 层是全注意力,其余 48 层是 Gated DeltaNet。
图 7-1:Qwen3.8-27B 的 64 层布局。示意图——方块的位置与颜色对应 config.json 的 layer_types,方块本身的大小不代表参数量或计算量。

三代同构:同一张图纸,只是重复次数不同

现在把这个模板拿去套另外两个模型,会看到全站最值得记住的一张表:

表 7-1:三个模型的层布局(层数与专家数来自各自模型卡 / config.json)
模型层数布局FFN
Qwen3-Next-80B-A3B4812 × (3×DeltaNet + 1×Attention)MoE 512 选 10+1
Qwen3.8-27B6416 × (3×DeltaNet + 1×Attention)稠密 17408
Qwen3.8-2.4T-A95B9223 × (3×DeltaNet + 1×Attention)MoE 512 选 10+1

盯着中间那一列看:(3×DeltaNet + 1×Attention) 这个括号里的东西,三个模型一模一样。48 = 12 × 4,64 = 16 × 4,92 = 23 × 4。三个模型的差别,在这一列上只剩下括号前面那个数字:12、16、23。

一句话记住:这三个模型是同一张图纸按重复次数缩放出来的。「不同尺寸」差在模板重复了多少次、FFN 换没换成 MoE——不是差在有没有量化

这正是第 1 章那张四轴表里的第 ③ 轴(注意力布局)。第 ③ 轴上,27B 和 2.4T 的差别是 16 组 vs 23 组;第 ② 轴上,差别是稠密 FFN vs MoE。这两件事都在训练之前就定死了——图纸画出来就是那样,不可能训练完再改。而第 ④ 轴的量化发生在训练之后,它一层都不动,只是把每个数字记得粗一点。这一章看到的东西,就是「尺寸差异发生在训练之前」这句话最直观的证据。

一个模型 config 里写着 num_hidden_layers: 92full_attention_interval: 4。它有几层全注意力、几层线性注意力?第 46 层是哪一种?

先想:interval 是「每隔几层来一次」,全注意力层的层号有什么规律?
关键在于:全注意力层的层号都是 interval 的整数倍。判断第 k 层是哪一种,只要看 k 能不能被 interval 整除。
第一步:92 ÷ 4 = 23,这就是全注意力层数;剩下 92 − 23 = 69 层是线性。再看 46 ÷ 4 = 11 余 2。
完整答案:23 层全注意力,69 层线性。第 46 层除以 4 有余数(46 = 4×11 + 2),不是 4 的倍数,所以它是线性注意力层——它在第 12 组里排第 2。这就是 Qwen3.8-2.4T-A95B,vLLM 的博客独立写过「其余 69 层跑线性注意力」,23 + 69 = 92,和模型卡的 23 × (3+1) 完全对得上。

变式:如果这个模型的层数是 90(不是 92),interval 仍是 4,最后一组会发生什么?(提示:90 ÷ 4 = 22 余 2,最后两层没凑够一组,第 90 层不是 4 的倍数,所以模型最顶上会是两层线性层——整个模型的最后一层不再是全注意力。这就是为什么层数几乎总是 interval 的整数倍。)

我能不看材料说清:64 层里哪 16 层是全注意力,以及这 16 个层号是怎么来的。

答辩:如果我是审稿人

你说这三个模型「同构」,可它们的 FFN 一个是稠密、两个是 MoE,隐藏维一个 5120 一个 8192,注意力头数一个 24 一个 64。这也叫同构?你是不是只挑了对你论点有利的那一列来看?

参考防守(先自己组织语言再看)

这个质疑是对的,必须限定范围:我说的「同构」只在层布局这一条轴上成立,不是说三个模型处处相同。它们在隐藏维、头数、FFN 形态上确实都不一样——正因为如此,把它们放在一起才有意义。

更准确的表述是:层布局这一条轴被固定成了一个常数。隐藏维随规模变(5120 → 8192),头数随规模变(24 → 64),FFN 随部署目标变(稠密 → MoE),唯独「每 4 层一次全注意力」这个比例,从 2025-09 的 Qwen3-Next 到 2026-08 的 Qwen3.8,跨三个模型、跨两倍层数、跨稠密与稀疏两条路线,一次都没变。

这本身就是个值得注意的信号:它说明设计者把这个比例当成了一个已经调好、不再动的东西。但要老实承认——我拿不出官方的消融数据来解释他们为什么锁定 4 而不是 3 或 5。这只是一个观察到的稳定性,不是一个被证明的最优性。

7.3 谱系:这个设计从哪来

问「这个设计从哪来」的时候,很容易得到一个含糊的答案。这里要把它拆成两层,因为这两层确实指向两个不同的东西。

设计源头:Qwen3-Next(2025-09)

3:1 混合布局第一次在 Qwen 系列里出现,是 2025 年 9 月的 Qwen3-Next-80B-A3B。表 7-1 的第一行就是它:48 层 = 12 组。Qwen3.8 用的是同一个比例,只是把组数从 12 加到了 16(27B)和 23(2.4T)。

Alibaba Cloud 的官方博客在介绍 Qwen3-Next 时把这个比例写得很直白:3:1 ratio (75% layers use Gated DeltaNet, 25% keep standard attention),并给出 32K 以上上下文吞吐提升约 10 倍的说法。这是目前能找到的、关于「为什么这么设计」最接近一手的公开表述。

读的时候要小心

上面那个「10×」是厂商博客对 Qwen3-Next 的说法,不是对 Qwen3.8 的说法,也没有附上完整的测试条件(哪张卡、哪个框架、batch 多大、输入输出各多长)。吞吐这种数字对测试条件极其敏感。本站把它作为「设计意图的表述」引用,不作为可复现的性能承诺

代码谱系:Qwen3.5

另一条线索藏在 config.json 的最上面两行。Qwen3.8-27B 的 model_type 写的不是 qwen3_8,而是 qwen3_5architectures 写的是 Qwen3_5ForConditionalGeneration。模型卡的原文也说它是 Built on the architectural foundation of Qwen3.5

这说明:3:1 的设计思路来自 Qwen3-Next,但落到代码上、固化成主线架构的是 Qwen3.5,Qwen3.8 直接复用了那一套实现。所以你在 Hugging Face 上加载 Qwen3.8 时,transformers 里跑的其实是名字叫 Qwen3_5 的那份类定义——这不是故障,是架构没变。

常见误解

很多人看到 model_type: qwen3_5 会以为「Qwen3.8 就是 Qwen3.5 改个名」或者「这个仓库传错了文件」。都不是。model_type 标的是模型结构的实现代码属于哪一族,不是模型的版本号。结构没变、权重是新训的,这在业界很常见:结构复用、权重重训,两件事互不相干。

为什么必须保留少量全注意力层:论文级的论证

Qwen3.8 没有技术报告,所以「为什么不能全用线性层」这个问题,官方没有正面回答过。但这个问题在公开文献里被反复论证过,两篇最直接的是:

JambaarXiv:2403.19887)把状态空间层和注意力层混在一起,专门讨论了为什么少量注意力层不可省。SambaarXiv:2406.07522)做了一组更简洁的混合架构消融,把「加多少注意力」这件事的取舍摆出来看。这两篇给出的是同一个方向的结论:把注意力层砍到零,一类依赖精确检索的能力会明显退化;而只要保留一小部分,退化就能被很大程度地补回来。

换句话说,3:1 这个具体的比例是 Qwen 团队的工程选择,但「必须留一点全注意力」这件事本身,是有公开论文支撑的。

你的同学看到 Qwen3.8-27B 的 config 里 model_typeqwen3_5,下了三个结论:(a) Qwen3.8 是把 Qwen3.5 量化出来的;(b) Qwen3.8 的权重就是 Qwen3.5 的权重;(c) Qwen3.8 用的是 Qwen3.5 那套结构代码。哪个对?另外两个分别错在哪?

先想:model_type 这个字段是给谁看的?是给人看的版本号,还是给加载代码看的?
关键在于:结构、权重、数值精度是三件独立的事。model_type 只锁定了第一件。
第一步:先排除 (a)。量化只改每个数字用几个比特存,不会改 model_type,也不会改层数、头数、隐藏维——config 里那些结构字段一个都不会动。
完整答案:只有 (c) 对model_type 决定 transformers 去实例化哪一份结构代码,说明 Qwen3.8 复用了 Qwen3.5 的架构实现。(a) 错在混淆了轴:量化是训练之后改位宽,改不了 model_type,Qwen3.8 的 BF16 权重也照样写着 qwen3_5。(b) 错在把「结构相同」当成「权重相同」——同一个模具可以浇出无数个不同的零件,Qwen3.8 是重新训练的一套独立参数。补充一条:这也解释了为什么谱系要分两层说——设计源头是 Qwen3-Next,代码谱系是 Qwen3.5

变式:假如某天你看到一个仓库叫 Qwen3.8-27B-AWQ,它的 config 里 model_type 应该是什么?num_hidden_layers 会不会变?(答案:还是 qwen3_5,层数还是 64。AWQ 是训练后量化,只会多出一段 quantization_config,结构字段一个不动。)

7.4 这个比例换成别的会怎样

光看结论没意思。既然 full_attention_interval 就是一个数,那就把它当成旋钮拧一拧:从 1(每层都是全注意力,退化成传统 Transformer)拧到 8(更稀疏,全注意力层只剩 8 层)。左边是显存,右边是能力,中间是你要做的取舍。

拖动 interval,两条曲线会朝相反方向走:全注意力层数越多,KV cache 越大,但那个「精确回忆」小任务的成功率越高;层数越少,显存越省,成功率越往下掉。你要找的不是某个「最优点」——这条曲线上没有最优点,只有取舍点。你在意什么,最优点就在哪儿。

这个实验室里的「回忆能力」是本站模拟的,不是官方数据

说清楚这一点很重要:上面那个实验室里的「回忆成功率」,是本站用一个玩具任务模拟出来的——它演示的是「注意力层越少、精确定位越难」这个机制方向,数值本身没有任何权威性。

Qwen 官方没有公开过 3:1 这个比例的消融实验数据:没有技术报告、没有 arXiv 论文、模型卡里也没有对照表。所以本站拿不出「interval=4 时回忆率是 X%,interval=8 时是 Y%」这种真实数字,也不会去编一个。你从这个实验室里应该带走的是趋势的方向和背后的道理,不是任何一个具体的百分比。

另一件值得亲眼看一遍的事:随着对话越来越长,那 16 层的 KV 是怎么一根一根长上去的,而另外 48 层始终是同一个高度。

把 Qwen3.8-27B 的 full_attention_interval 从 4 改成 8(假设重新训练一个这样的模型),层数仍是 64。全注意力层变成几层?每 token 的 fp16 KV cache 变成多少 KiB?Gated DeltaNet 层数怎么变?

先想:interval 变大,意味着全注意力层变稀疏还是变密?两类层的数量是此消彼长的关系。
关键在于:总层数是固定的 64,全注意力层数 = 64 ÷ interval,剩下的全部是线性层。KV cache 只跟前者成正比。
第一步:64 ÷ 8 = 8 层全注意力。把 8 代进式 7-1:2 × 8 × 4 × 256 × 2 = ?
完整答案:全注意力层从 16 层降到 8 层;每 token fp16 KV = 2 × 8 × 4 × 256 × 2 = 32,768 字节 = 32 KiB,正好是原来 64 KiB 的一半;Gated DeltaNet 层从 48 层升到 56 层。262K 满上下文下,KV 从 16 GiB 降到 8 GiB。代价是:能做精确逐字回忆的层只剩下 8 层,模型每 8 层才「校准」一次,中间那 7 层全靠有损的固定态往下传。省一半显存,赌的是那类精确检索任务掉得不多——而这正是官方没给数据的那一块。

变式:反过来把 interval 改成 1 会发生什么?(答案:64 层全是全注意力,KV 回到 256 KiB/token,262K 下 64 GiB,Gated DeltaNet 层数为 0——这个模型就退化成一个传统的稠密 Transformer 了,7.1 节那条死路。)

这个比例最后会落在你手上的哪一天

  1. 因为它把 64 层里的 48 层设计成固定大小的循环状态(证据:config.json → text_config.full_attention_interval = 4,配合 layer_types 逐层清单)。
  2. 所以「在一张 24GB 卡上塞进十万级别的上下文」成为可能,而「让模型逐字复述第 137,492 个 token」变得不那么可靠。
  3. 第 3 天你把一整个项目的代码丢进去问架构,它答得又快又稳,你会觉得这个上下文长度是真的能用的。
  4. 第 30 天你开始拿它做「把这份 20 万 token 的日志里所有出现过的错误码原样列出来」这类活儿,偶尔会发现漏了几个、或者某个编号的某一位对不上——而同一个问题只要把输入截短到几千 token,它就再也不出错了。
  5. 这就是那 48 层有损记忆露头的地方:出错的概率随输入长度上升,且集中在「逐字精确」这一类任务上。
代价:使用者要自己判断手上这个任务吃不吃精确检索——省下的显存不是白给的,账单在你不知不觉时结清。(第 4 步描述的现象是本站按架构机制作出的推断,不是官方或第三方公布的实测结论。)

7.5 省下来的到底是什么

现在把账彻底算清楚。同样是 Qwen3.8-27B 那些参数(4 个 KV 头、head_dim 256、fp16),只改层布局这一件事:

表 7-2:262,144 token 满上下文下的 KV cache(fp16,按式 7-1 复算)
方案有 KV 的层数每 token KV262K 满上下文
假想:64 层全是注意力64256 KiB64.00 GiB
真实:3:1 混合1664 KiB16.00 GiB
差额(这个设计买到的)−48−192 KiB−48.00 GiB

48 GiB。这就是「每 4 层放 1 层全注意力」这个决定,在 262K 上下文下换来的东西。48 GiB 相当于两张 4090 的全部显存,或者说:它把「必须用多卡才能碰的长上下文」变成了「单卡有机会摸到的东西」。第 17 章会看到,24GB 卡上到底能跑多长,答案完全建立在这 48 GiB 上。

自己推一遍:那 48 层到底占多少显存?

KV cache 的账算完了,但有个问题被跳过了:那 48 层 Gated DeltaNet 的状态,难道是不占显存的吗?当然占。只是它占的方式完全不同。跟着下面五步自己推一遍——用到的每个数都在 config.json 里。

  1. 第 6 章说 Gated DeltaNet 的状态是一个「固定大小的矩阵」。要算它多大,你需要从 config 里找哪几个数?

    想好了再看

    三个:linear_num_value_heads: 48(有多少个头,每个头一块自己的白板)、linear_key_head_dim: 128linear_value_head_dim: 128(每块白板的两个边长)。为什么是「键维 × 值维」?因为第 6 章那个状态矩阵干的事是「给一个查询键,还我一个值向量」——它本质上是一张从 128 维键映射到 128 维值的表,所以形状必须是 128×128。

  2. 一层的状态有多少个数?

    想好了再看

    48 × 128 × 128 = 786,432 个数。注意这里乘的是值头数 48,不是键头数 16——16 个键头被 48 个值头分组共用(第 6 章那个 3:1 的分组),但每个值头维护自己的一块状态。

  3. 一个数占几个字节?这个不能拍脑袋,config 里有答案。

    想好了再看

    mamba_ssm_dtype: "float32"——循环状态用 fp32 存,4 字节。为什么不像 KV 那样用 fp16?因为这个状态是被反复迭代更新的:每来一个 token 就要在原状态上乘一次、加一次。误差在几十万步迭代里会累积,用 fp16 存会漂。KV cache 写一次就再也不改,所以敢用低精度。

  4. 48 层加起来是多少?

    想好了再看

    786,432 × 4 字节 = 3,145,728 字节 = 3 MiB 每层。48 层 × 3 MiB = 144 MiB

  5. 现在把它和 KV cache 放一起看。你推出的这个数,最关键的性质是什么?

    想好了再看

    这 144 MiB 和上下文长度没有任何关系。输入 100 个 token 是 144 MiB,输入 26 万个 token 还是 144 MiB,输入 100 万个 token 依然是 144 MiB。而 KV cache 在 262K 时是 16 GiB = 16,384 MiB——是它的 约 114 倍

    所以真正的对比是这样的:16 层的 KV 在长,48 层的状态是一条水平线。你在 7.4 那个 3D 场景里看到的就是这件事。这也解释了为什么这个架构在短上下文下反而没什么优势——短的时候 KV 本来就不大,144 MiB 的固定开销还显得挺贵;长度一上去,两者的斜率差异才把差距拉开。

上面那 144 MiB 是本站推的,不是官方给的

推导用的三个数(linear_num_value_heads=48linear_key_head_dim=128linear_value_head_dim=128)和 mamba_ssm_dtype=float32 全部来自 config.json,是硬事实;但「状态形状 = 值头数 × 键维 × 值维」这一步是按 Gated DeltaNet 原论文(arXiv:2412.06464)的定义推的,官方没有直接公布过这个显存数字。不同推理框架实现时可能还会多留一份卷积状态(linear_conv_kernel_dim=4 那一小块)或者做双缓冲,实际占用会比 144 MiB 略高。本站给的是数量级,不是逐字节的账。

但这不是免费午餐

省下 48 GiB 的代价,前面已经说过一遍,这里必须再说一遍,因为它太容易被「省了 48 GiB」的兴奋盖过去:那 48 层的记忆是有损的

更准确地说:全注意力层的记忆是「无损但线性增长」,Gated DeltaNet 的记忆是「有损但恒定」。3:1 混合的意思是——你手上有一条 16 层宽的无损通道,和一条 48 层宽的有损通道,模型必须学会把「以后可能要逐字调用的东西」尽量塞进前者。这件事它做得好不好,官方没给数据。这也是为什么 7.4 那个实验室必须写清楚「本站模拟」。

有人在论坛上写:「既然 Gated DeltaNet 不占 KV cache,那把那 16 层全注意力也换成 DeltaNet,显存开销就归零了,长上下文就彻底免费了。」这句话有一半是对的,一半是错的。分别指出来。

先想:他说的「显存开销归零」,指的是哪一种显存?模型跑起来要占的显存只有 KV cache 一种吗?
关键在于两处:第一,DeltaNet 层自己的固定状态也要占显存,只是不随长度长;第二,7.1 节讲的那个能力代价他一个字没提。
第一步:先算全换成 DeltaNet 后的固定状态。64 层 × 3 MiB = 192 MiB,不是 0。再想第二件事:这个模型还能做大海捞针吗?
完整答案:
对的一半:KV cache 确实会归零,显存不再随上下文长度增长——这是线性注意力的真实性质,262K 和 1M 的开销一模一样。
错的两处:(1) 「归零」是假的。64 层 DeltaNet 的固定状态按 7.5 的推导是 64 × 3 MiB = 192 MiB,比 48 层的 144 MiB 还多。它只是不增长,不是不存在。(2) 「免费」是假的,而且这才是真正的错。7.1 节那条捷径二正是这个方案:所有精确逐字回忆的能力都被牺牲掉了。他把「显存账」算成了全部的账,把「能力账」当成了零。
一句话概括:他描述的不是一个更好的 Qwen3.8,而是 7.1 节里那条已经被排除的死路。

变式:那把 16 层全注意力增加到 32 层呢(interval=2)?请分别说出 KV cache、DeltaNet 固定状态、精确回忆能力三者各朝哪个方向变。(答案:KV 每 token 64 → 128 KiB,262K 下 16 → 32 GiB;DeltaNet 层 48 → 32 层,固定状态 144 → 96 MiB;精确回忆能力应当上升——但上升多少,官方无数据。)

7.6 vLLM 是怎么伺候这种模型的

到这里为止,混合架构看起来只有好处:省显存、保能力。但工程上从来没有白拿的东西——它把复杂度转嫁给了推理框架

问题出在显存管理上。传统 Transformer 的推理框架(比如 vLLM 的 PagedAttention)把 KV cache 切成一个个大小相同的block),像操作系统管内存页那样分配和回收。这套机制的前提是:所有层长得一样,每层每 token 占的空间相同,一块就是一块。

混合架构把这个前提打破了。现在框架要同时管两种东西:

表 7-3:推理框架要同时伺候的两种状态
全注意力层(16 层)Gated DeltaNet 层(48 层)
存的是什么每个 token 的 K 和 V一个循环状态矩阵
随长度怎么变线性增长完全不变
分配时机边生成边追加开头一次分配到位
能不能分块能,天然按 token 切不能,它是一个整体
数值精度可以 fp16 甚至 fp8float32(config 指定)

vLLM 在支持 Qwen3-Next 时给出的做法是:自动调整全注意力层的「逻辑块大小」,让两类层占用相同的物理显存。翻译成人话——既然 DeltaNet 层每个序列固定要吃掉一块(比如 3 MiB),那就把注意力层的块也凑成同样大小,这样显存池里全是等大的格子,分配器又可以按老办法工作了。这是个很朴素但很聪明的对齐技巧:不去改分配器,而是把两种不同形状的东西都塞进同一种盒子

顺带一个数字,来自 vLLM 给 Qwen3.8-27B 的部署 recipe:单卡跑 1M 上下文的配置下,可容纳约 6.6M 个 KV token。能报出这么大的数,前提正是这一章讲的——真正吃 KV 的只有 16 层。

为什么这一节值得讲

因为它示范了架构设计的一条通则:任何一个「省了资源」的设计,都会在别处生出复杂度。3:1 混合布局省的是显存,生出的是推理框架的实现难度——两种状态、两种生命周期、两种精度,都要在同一个显存池里共处。你在模型卡上看到的是「省 4 倍 KV」,vLLM 那边看到的是一个需要专门写 PR 才能支持的新架构。这两件事是同一件事的两面。

一个推理框架原本只支持纯注意力模型,它的显存管理逻辑是「按需分配 KV 块,请求结束就回收」。现在要它支持 Qwen3.8。请指出至少两处必须改动的地方,并说清不改会出什么故障。

先想:对着表 7-3 逐行读。那五行里,哪几行是老框架的假设直接不成立的?
关键在于「按需分配」和「固定大小」的冲突:DeltaNet 的状态在第一个 token 到来时就必须整块就位,而且它不按 token 切。
第一步:先想分配时机。老框架的逻辑是「来一个 token,申请一点空间」。如果 DeltaNet 层也走这条路,第一个 token 只申请到 1/262144 块状态,会发生什么?
完整答案(任答两条即可):
(1) 分配时机:DeltaNet 状态必须在序列开始时一次性分配完整的一块。若沿用按需增长,第一个 token 就会写到未分配的内存上,轻则崩溃,重则读到别的请求的残留数据。
(2) 块的划分方式:注意力的 KV 能按 token 切成等大块,DeltaNet 状态是一个不可分割的整体。若强行按 token 切,状态矩阵会被拆到不连续的显存里,每一步递推都要做一次拼接,性能垮掉。vLLM 的解法是反过来:调整注意力层的逻辑块大小去凑 DeltaNet 那一块的物理尺寸。
(3) 数值精度:KV 池可能整体开成 fp16 或 fp8,但 DeltaNet 状态 config 指定 float32。若把它塞进 fp16 的池子,几十万步递推下来状态会漂,输出逐渐失真——而且不报错,只是答得越来越不对。
(4) 显存预估公式:老框架用「层数 × 长度」估容量,会把 Qwen3.8 的可用上下文低估 4 倍,导致明明放得下却拒绝请求。

变式:如果反过来,有个框架把 Qwen3.8 的显存需求高估了 4 倍(把 64 层都当成有 KV 的),用户会看到什么现象?(答案:明明显存充足,框架却报「上下文超限」或自动把 max_model_len 砍到实际能力的 1/4——这正是第 5 章说的那个全网普遍存在的错算,只不过这次它体现成了一条恼人的报错。)

答辩:如果我是审稿人

你整章都在讲 3:1 有多合理,但你自己也承认官方没给过任何消融数据。那凭什么说 4 是对的?会不会 3 更好、5 也够用,团队只是随手挑了个能被层数整除的数?你这一章讲的到底是「设计原理」,还是「事后给一个既定数字编理由」?

参考防守(先自己组织语言再看)

这个质疑基本成立,我只能守住一部分。

守不住的:我确实无法论证 4 优于 3 或 5。Qwen3.8 没有技术报告、没有 arXiv 论文、模型卡里没有消融表,官方博客也是纯 SPA 抓不到正文。任何声称「4 是最优的」的说法——包括本站——都拿不出一手证据。7.4 那个实验室里的曲线是本站模拟的,我已经用红框标了出来,就是为了不让读者误以为那是官方数据。

能守住的有三条:第一,「必须留一些全注意力层」有公开论文支撑(Jamba、Samba),这条不依赖 Qwen 的数据。第二,4 这个值在三个模型上稳定复现:Qwen3-Next 48 层、27B 64 层、2.4T 92 层,跨一年、跨两倍层数、跨稠密与 MoE 两条路线都没变。如果这是随手挑的,很难解释这种一致性。第三,这一章的可验证部分不依赖「4 是不是最优」:KV 从 64 GiB 降到 16 GiB 是纯算术,48 GiB 的差额是硬的,只要 interval 是 4 就成立。

所以更诚实的表述是:这一章讲的是这个选择带来的确定后果(显存账、能力代价、工程复杂度),而不是这个选择的最优性证明。后者需要官方公布消融数据,或者有人自己重训一批不同 interval 的模型来做——这正好是本章 research 块里那个开放课题。

真未解混合架构里,全注意力层到底该占多少?

这个问题今天没有公认答案。已知的是两端:一端有 Jamba、Samba 这类论文证明「注意力层不能砍到零」;另一端有 Qwen3-Next / Qwen3.5 / Qwen3.8 三个工业模型不约而同地锁定 3:1。但中间那一大段是空的——没有人公开发表过「在同一训练配方、同一数据、同一算力下,interval 从 2 扫到 8,各项能力分别怎么变」的完整曲线。

难在哪儿:这种消融必须重新预训练才有意义(换了布局,权重就不能复用),而每扫一个点就是一次完整的预训练,成本高到只有少数几家做得起;而做得起的那几家,通常不发表这一类实验。更麻烦的是,「回忆能力」本身缺一个公认的度量——大海捞针任务对针的位置、草堆的内容、提问的措辞都极其敏感,换一套模板结论就可能翻转。

先做这一步:打开 7.4 的混合比例台,把 interval 从 1 扫到 8,把每一档的 KV/token 和玩具任务成功率记成一张表,找出成功率下降最陡的那一档(注意本站的曲线是模拟的,你要找的是拐点的位置而不是数值)。然后去读 Samba(arXiv:2406.07522)的消融部分,看它在真实训练里报出的拐点落在哪儿,两边对照。如果位置差得很远,去想为什么——是任务不同,还是本站的玩具模型把这件事简化过头了。这个「为什么差」就是你自己的第一个研究问题。

已知第 5 章表 5-2 的预算:一张 24 GiB 卡扣掉 Q4_K_M 权重 15.93 GiB、视觉塔 0.87 GiB、DeltaNet 固定态 0.14 GiB(48 层 × 3 MiB)和 1.0–2.0 GiB 框架与激活开销后,剩 5.1–6.1 GiB 给 KV,据此 fp16 KV 约能撑 8–10 万 token、fp8 约 17–20 万
现在假设 Qwen 重新训练了一个各方面完全一样、只把 full_attention_interval 从 4 改成 2 的版本。请回答三问:(1) 这个新版本在同一张卡上的最大上下文变成多少?(2) 权重体积会不会变?(3) 如果有人说「那用量化把它压回去不就行了」,这句话哪里对哪里错?

先想:interval 从 4 变 2,只影响三个量里的哪一个——权重体积、KV cache、还是两个都影响?
关键在于把两条轴分开:布局这条轴决定有 KV 的层数(在训练之前定死),量化那条轴决定每个数用几个比特(在训练之后才发生)。它们乘在同一个公式里,但不是同一件事。
第一步:interval=2 → 全注意力层 64÷2 = 32 层 → 每 token fp16 KV 翻倍到 128 KiB。第二步别偷懒:KV 预算也动了一点点——线性层从 48 降到 32,DeltaNet 固定态跟着从 144 MiB 降到 96 MiB,省出来的这 0.05 GiB 回到 KV 那一格。所以答案不是把旧数字除以二,要重算一遍。
完整答案:
(1) 要动的有两格,不是一格。
 第一格(KV 单价,涨):全注意力层 16 → 32,按式 7-1 每 token fp16 KV = 2 × 32 × 4 × 256 × 2 字节 = 131,072 字节 = 128 KiB,正好翻倍。
 第二格(KV 预算,也涨了一点):线性层 48 → 32,DeltaNet 固定态从 48 × 3 = 144 MiB 降到 32 × 3 = 96 MiB,省出 0.05 GiB。于是 KV 可用 = 24 − 15.93 − 0.87 − 0.09 − (1.0~2.0) = 5.15–6.15 GiB(原版是 5.06–6.06)。
 合起来算:5.15 GiB ÷ 128 KiB = 41,822;6.15 GiB ÷ 128 KiB = 50,014。也就是 fp16 约 8–10 万 → 约 4–5 万 token;fp8 单价 128 → 64 KiB,翻倍回去,约 17–20 万 → 约 8–10 万
 注意:结果不是精确腰斩。直接把旧数字除以二会得到 41,438–49,630,比正确值少约 0.9%——差的正是 DeltaNet 那 48 MiB。数量级上无所谓,但这道题要练的就是「凡是布局变了,整张预算表都要重走一遍」,而不是拿结论去除。
(2) 权重体积几乎不变,但方向可能出乎意料:注意力层从 16 增到 32(每层 104.86M),DeltaNet 层从 48 减到 32(每层 115.88M)。总参数 = 16×104.86M + 48×115.88M ≈ 7.24B 变成 32×104.86M + 32×115.88M ≈ 7.06B,反而略微减少——因为 DeltaNet 层的参数比注意力层还多一点。所以「多放注意力层 = 模型变大」是错觉,17.11 GB 那个数几乎不动。
(3) 对的部分:量化确实能把 KV 也压——fp16 换 fp8,每 token 从 128 KiB 降回 64 KiB,上下文就补回到约 8–10 万
错的部分:它补不回原来的水平。这里有一个很干净的巧合可以帮你记住——新版开 fp8 得到的约 8–10 万,恰好就是原版开 fp16 的水平;而原版开 fp8 能到约 17–20 万,仍然是新版的一倍。也就是说,量化只把你换回了「原版不开量化」的起跑线,那一整倍的差距还在。原因是量化改的是每个数用几个比特,布局改的是要存多少个数;压缩率再高也压不掉「多出来的 16 层每 token 都要各存一份 K 和 V」这件事。这正是全站那句话在这里的具体形态:布局是训练之前的事,量化是训练之后的事,后者补不了前者的账。

变式:反过来,如果新版本把 interval 改成 8(全注意力层降到 8 层),同一张卡上 fp8 KV 大约能撑多长?这个数字看起来很诱人,你会建议直接这么做吗?(答案:同样要走两格。全注意力 8 层 → 每 token fp16 KV 32 KiB、fp8 16 KiB;线性层升到 56 层 → DeltaNet 固定态涨到 56 × 3 = 168 MiB = 0.16 GiB,KV 可用到 5.03–6.03 GiB。算出来是 约 33–40 万 token(精确 329,966 – 395,502),能吃下完整的 262K 原生上下文还有富余。注意这次固定态是往反方向动的——层数此消彼长,两格永远要一起看。但不建议无条件推荐——能不能这么做,取决于你的任务吃不吃精确检索,而这一点官方没有数据,只能自己在自己的任务上测。)

这一层要加什么:把 3 个 DeltaNet 层 + 1 个 Attention 层拼成一个「块」,重复 16 次

为什么现在才加它:第 4 层造了全注意力层,第 6 层造了 Gated DeltaNet 层,两种零件都在手上了,但它们还是散着的。这一层不新增任何计算,只干一件事——按 config 的 full_attention_interval 把它们排成一个 64 层的队列。这也是整个模型第一次有了「深度」这个概念。

INTERVAL = cfg.full_attention_interval # 4 N = cfg.num_hidden_layers # 64 def layer_kind(i): # i 从 1 数到 N return "full" if i % INTERVAL == 0 else "linear" layers = [] for i in range(1, N + 1): if layer_kind(i) == "full": layers.append(GatedAttentionLayer(cfg)) # 第 4 章那个,带 KV cache else: layers.append(GatedDeltaNetLayer(cfg)) # 第 6 章那个,带固定状态 layers.append(FFN(cfg)) # 两种层后面都跟 FFN(第 8 章) assert len(layers) == 2 * N assert sum(1 for i in range(1, N+1) if layer_kind(i)=="full") == N // INTERVAL

难点:这里有一个很容易搞反的地方——组内的顺序是「3 线性在前、1 全注意力在后」,不是反过来。写成 i % INTERVAL == 0 而不是 i % INTERVAL == 1,效果是全注意力层落在每组的末尾(第 4、8、12…层),于是整个模型的最后一层是全注意力层。这不是巧合:最后一层的输出直接送去预测下一个 token,让这一步走在无损通道上,比走在有损通道上更稳。你把判断改成 == 1,模型照样能跑、参数量一个不差,但第 64 层会变成 DeltaNet 层——这种错误不会报错,只会让你的最小实现和真权重对不上,而且要跑很久才发现。

另一个坑:FFN 是两种层都有的,不要以为只有注意力层后面才跟 FFN。64 层全都有,这是第 8 章那 62.6% 参数的来源。

自己验:
① 打印每一层的类型。[layer_kind(i) for i in range(1,65)],输出应该是严格的 linear, linear, linear, full 循环;第 4、8、12、…、60、64 这 16 个位置是 full,其余 48 个是 linearfull 的总数正好 16,最后一个元素必须是 full
② 把 INTERVAL 从 4 改成 2,三个数应该同时动:full 层数 16 → 32;按式 7-1 算的每 token fp16 KV 从 64 KiB → 128 KiB(正好翻倍);linear 层数 48 → 32,DeltaNet 固定状态总量从 48×3 = 144 MiB → 32×3 = 96 MiB(降到原来的 2/3,注意不是减半——总层数固定在 64,两类层此消彼长,比例是 48:32 = 3:2)。
③ 把 INTERVAL 改成 64,应该只剩最后一层是 full,KV 变成 4 KiB/token。
三条都对上,这一层就成了。

本章小结

  • 两个极端都不行:64 层全注意力,262K 上下文要 64 GiB KV,等于三张 4090 只放缓存;64 层全线性,KV 归零但精确逐字回忆垮掉。
  • Qwen3.8 的答案是 3:1full_attention_interval = 4,64 层 = 16 组 ×(3 线性 + 1 全注意力)。全注意力层的层号是 4 的倍数,共 16 层,最后一层(第 64 层)是全注意力。
  • 三代同构:Qwen3-Next 48 层 = 12 组、Qwen3.8-27B 64 层 = 16 组、Qwen3.8-2.4T 92 层 = 23 组。括号里的模板一模一样,只差重复次数和 FFN 换没换成 MoE——不是差在量化
  • 谱系两层:设计源头是 Qwen3-Next(2025-09 首次引入 3:1),代码谱系是 Qwen3.5(model_type: qwen3_5)。「必须保留少量全注意力层」有 Jamba、Samba 等公开论文支撑;但 3:1 这个具体比例,官方没有公开消融数据
  • 省下的是 48 GiB:262K 下 64 GiB → 16 GiB。而那 48 层 DeltaNet 的固定状态按本站推导约 144 MiB,且不随长度变化。代价是这 48 层的记忆有损,不是免费午餐。
  • 复杂度转嫁给了推理框架:两种状态、两种生命周期、两种精度共处一个显存池。vLLM 的解法是调整全注意力层的逻辑块大小,让两类层占用相同的物理显存。

下一章换一个视角。这一章一直在数「哪些层有 KV」,但从参数量的角度看,注意力层其实只占 6.1%,Gated DeltaNet 占 20.3%。真正的大头是一个到现在为止只被顺带提过一句的东西——每一层后面都挂着的那三张矩阵。

第8章 FFN:62.6% 的参数都在这里

这一章回答一个会让人愣一下的问题:你花了五章学注意力,可注意力只占 Qwen3.8-27B 参数的 6.1%。那 62.6% 的参数在哪儿?在一个到现在为止只被顺带提过一句的东西里——每一层后面挂着的三张矩阵。

学完这一章你应该能做到

  • 用一句话说清注意力和 FFN 的分工,以及去掉 FFN 会怎样
  • 写出 SwiGLU 的式子,并解释为什么是三张矩阵而不是两张
  • 手算一层 FFN 的参数量,并算出它占全模型的比例
  • 指出 17408 这个数是怎么来的,以及官方没有解释哪一部分
  • 回答「如果想让参数量涨 100 倍但每次只用一小部分,该怎么改 FFN」
前置:第0章的矩阵乘法与「什么叫一个参数」、第3章的注意力在干什么、第7章的 64 层布局(这一章会用到「64 层全都有 FFN」这个事实)。

8.1 注意力负责「看谁」,FFN 负责「想什么」

不用 FFN 会怎样

做个思想实验:把 Qwen3.8 里所有的 FFN 全部删掉,只留 64 层注意力和 DeltaNet。这个模型还能跑——张量形状都对得上,不会报错。但它会变成一个几乎什么都学不会的东西。为什么?因为注意力做的全部事情,是把已有的向量按权重加起来。加权求和是线性的:一堆向量线性组合出来的,还在原来那些向量张成的空间里。你可以把它叠 64 层,本质上还是在做「换一组权重再加一遍」。没有 FFN,这个模型就没有任何一处能做非线性变换——它能重新组合信息,但没法产生新的信息。

所以一个 Transformer 层其实是两个动作交替:

注意力(或 DeltaNet)负责「看谁」——当前这个位置该从上下文的哪些位置取信息,取多少。它是个搬运工兼调度员,本身不加工内容。第 3 到第 7 章讲的全是这件事。

FFN 负责「想什么」——拿到搬过来的那一堆信息之后,在这个位置上单独做一次加工。注意「单独」这两个字:FFN 是逐位置position-wise)作用的,第 5 个 token 的 FFN 计算和第 500 个 token 的 FFN 计算互不干扰,用的还是同一套权重。它完全不看别的位置。

前馈网络Feed-Forward Network, FFN):Transformer 每一层里那个「只看自己、不看别人」的加工模块。输入一个向量,输出一个同样长度的向量,中间先把维度撑宽、做一次非线性、再压回来。

打个比方

一场会议:注意力是谁该听谁发言的调度规则,FFN 是每个人听完之后自己在脑子里过一遍。调度规则再精妙,如果所有人听完都不动脑子、只是把听到的话原样复述,这个会开不出任何新东西。

类比失效处:真实的人各有各的脑子,而模型里所有位置共用同一套 FFN 权重——更像是所有人拿着同一本操作手册各自查一遍,查出来的结果不同只是因为输入不同。

还有一个常被提到的说法:FFN 那个被撑宽的中间层,很像一个键值查找表——宽出来的每一维可以看成一条「如果输入长这样,就往输出里加这个东西」的规则。17408 维就是 17408 条规则。这个视角能解释为什么 FFN 要占掉这么多参数:模型学到的大部分具体知识,是存在这些规则里的,不是存在注意力里的。

这个「查找表」说法要打折扣

「FFN 是键值存储」是一个流行且有启发性的解释视角,学界有相关研究支持,但它是一种解释,不是 Qwen3.8 的设计文档里写的东西。Qwen3.8 没有技术报告,官方从未说明它的 FFN 里存了什么、怎么存的。本站用这个比喻是为了帮你建立直觉,不要把它当成已被证实的机制

下面两件事,分别该由注意力还是 FFN 来做?(a) 判断「它」这个字指的是前文哪个名词;(b) 知道「巴黎」和「法国首都」是一回事。

先想:哪一件事必须看别的位置才能做?哪一件事光看当前这一个位置就能做?
关键在于 FFN 是逐位置的:它计算第 k 个位置时,完全接触不到第 k−1 个位置的信息。凡是需要跨位置的事,它做不了。
第一步:(a) 里的「它」和它指代的名词处在不同位置,要建立这两个位置的联系。这个动作的名字就叫……
完整答案:(a) 是注意力的活——指代消解本质上是「当前位置该去看哪个位置」,这正是注意力的定义。(b) 是 FFN 的活——这是一条与上下文无关的知识,输入向量里带着「巴黎」的语义,FFN 就该往输出里加上「法国首都」相关的成分,不需要看别的位置。
补一句更准确的话:真实模型里两者是纠缠的,没有哪一条知识是纯粹存在某一处的。但这个划分能解释一个数字上的事实——存知识很吃参数,所以 FFN 占 62.6%;调度不太吃参数,所以注意力只占 6.1%。

变式:如果把 FFN 全删掉但层数加到 640 层,模型能补回来吗?(答案:不能。堆再多层线性变换,复合起来还是线性变换。缺的不是「层数」,是「非线性」——而非线性只在 FFN 里那个 silu 上。)

8.2 SwiGLU:三张矩阵而不是两张

最早的 Transformer 里,FFN 是两张矩阵:先用一张把 5120 维撑到宽的中间维,做一次非线性,再用另一张压回 5120。Qwen3.8 用的是三张。config.json 里的线索有两条:

"hidden_size": 5120 "intermediate_size": 17408 "hidden_act": "silu"

门控前馈网络Gated Linear Unit, GLU 系列,Qwen3.8 用的这一种叫 SwiGLU):把撑宽这一步拆成两条并行的路——一条叫 gate_proj(门),一条叫 up_proj(内容)。门这条路先过一次非线性,再和内容那条路逐元素相乘,相乘的结果才送去第三张矩阵 down_proj 压回原维度。

FFN(x) = down( silu( gate(x) ) ⊙ up(x) )
式 8-1
符号是什么形状 / 直觉
x这个位置的向量,从上一步(注意力或 DeltaNet)出来的5120 维
gate(x)用矩阵 gate_projx 撑宽,这条路算的是「开多大」5120 → 17408
up(x)用矩阵 up_projx 撑宽,这条路算的是「放什么过去」5120 → 17408
silu(·)非线性函数,config 里的 hidden_act: silu。见式 8-2逐元素,不改形状
逐元素乘element-wise product):两个 17408 维向量,第 1 位乘第 1 位、第 2 位乘第 2 位……不是矩阵乘法17408 × 17408 → 17408
down(·)用矩阵 down_proj 压回原来的宽度17408 → 5120

那个 silu 长这样:

silu(z) = z · σ(z) = z1 + ez
式 8-2
符号是什么直觉
z一个标量——silu 是逐个数字作用的,17408 个数就各算各的gate 那条路撑宽后的某一维
σ(z)sigmoid 函数,把任意实数压到 0 和 1 之间一个开关的「开度」,0 是全关,1 是全开
z · σ(z)z 自己乘上这个开度z 很负时结果接近 0(关上),z 很正时结果接近 z(放行)
e自然常数,约 2.718指数函数的底,让开关是平滑的而不是硬跳变
一层 FFN 的内部:三张矩阵,两条路 输入 x 5120 维 gate_proj 5120 → 17408 up_proj 5120 → 17408 silu 逐元素乘 down_proj 17408 → 5120 输出 5120 维 这条路算「开多大」 这条路算「放什么过去」
图 8-1:SwiGLU 的数据流。示意图——方框大小不代表参数量,三张矩阵的参数量其实完全相等(各 89.13 M)。

为什么是三张不是两张

两张矩阵的老式 FFN 里,中间那 17408 维的每一维都是「算出来多少就往下传多少」,非线性只负责把负数压掉。三张矩阵的版本多了一个能力:每一维放行多少,是由输入自己另算出来的一个数说了算的

具体看第 i 维:up_proj 算出一个值(内容),gate_proj 算出另一个值再过 silu(开度),两个一乘。如果开度接近 0,这一维的内容就被掐灭了,不管它本身多大;如果开度接近 1,内容原样放行。也就是说,网络可以学会「只在满足某种条件时才启用某条规则」。

回到 8.1 那个查找表的视角:两张矩阵的版本,17408 条规则总是全体参与,只是权重不同;三张矩阵的版本,网络可以为每条规则单独学一个「什么时候该醒过来」的条件。门控买到的是选择性。

常见误解

很多人以为 gate_projup_proj 是同一张矩阵算两遍,或者以为门是一个 0/1 的开关。都不是。它们是两张完全独立、各自训练的矩阵,参数各 89.13 M,加起来是这一层参数的三分之二。而门的取值是连续的实数(silu 的输出可以是 0.03、0.7、也可以大于 1),不是二值开关——所以它是「开多大」,不是「开不开」。

假设某一层的 FFN 中,第 700 维上 up_proj 算出的值是 12.0,而 gate_proj 算出的值是 −8.0。这一维最终送进 down_proj 的数大约是多少?换成 gate_proj 算出 +8.0 呢?

先想:门那条路要先过 silu,再和内容那条路相乘。所以你要先算 silu(−8.0)。
关键在于 silu(z) = z·σ(z)。z 很负时 σ(z) 接近 0,所以 silu(z) 也接近 0;z 很正时 σ(z) 接近 1,所以 silu(z) 接近 z 本身。
第一步:σ(−8) ≈ 0.000335,所以 silu(−8) ≈ −8 × 0.000335 ≈ −0.0027。再乘上 12.0。
完整答案:
门 = −8.0 时:silu(−8) ≈ −0.0027,乘上 12.0 得到约 −0.032——原本 12.0 那么大的内容被压成了几乎为零。这一维等于被关掉了。
门 = +8.0 时:σ(8) ≈ 0.99966,silu(8) ≈ 7.997,乘上 12.0 得到约 96——不但放行,还被放大了。
这就是门控的实际效果:同样的内容值 12.0,仅仅因为另一条路算出的数不同,结果可以差出四个数量级。注意最后这一点:silu 的输出不封顶,所以门不只能关,还能放大——这也是它和「乘一个 0 到 1 之间的开关」的区别。

变式:如果把 silu 换成 sigmoid(输出严格落在 0 到 1 之间),上面两个答案会变成多少?这个改动会让 FFN 失去什么能力?(答案:门 = −8 时 σ ≈ 0.000335,结果约 0.004;门 = +8 时 σ ≈ 0.99966,结果约 12.0。失去的是「放大」——门最多只能原样放行,不能把内容放得更大,整个 FFN 的输出幅度被门死死压住。)

有人提议:「gate_projup_proj 形状一模一样,太浪费了,让它们共用同一张矩阵不就省下 89.13 M 参数?」也就是把式 8-1 改成 FFN(x) = down( silu(Wx) ⊙ Wx )。请说清这么改之后 FFN 失去了什么,并构造一个具体例子说明它为什么不再是「门控」。

先想:门控的意义是「开度」和「内容」可以各说各的。共用一张矩阵之后,这两个数还能各说各的吗?
关键在于:共用之后,第 i 维的开度和内容变成了同一个数 z 的两个函数,输出永远是 z·silu(z)。这条曲线由 z 一个数完全决定,网络再也无法为同一个内容值配不同的开度。
第一步:写出改动后第 i 维的输出表达式:silu(z) × z = z²·σ(z)。然后问自己:这还是一个「两输入」的运算吗?
完整答案:
改动后第 i 维的输出恒等于 z·silu(z) = z²σ(z),是一个只有一个自变量的固定曲线。原来的 SwiGLU 是二元的:内容 u 和开度 g 相互独立,输出 = u·silu(g)。
具体反例:原版可以做到「内容 = 12.0、开度 = −8.0,输出约 −0.03(掐灭)」和「内容 = 12.0、开度 = +8.0,输出约 96(放大)」——同一个内容,两种结局,靠的是 gate 那张独立矩阵从输入里读出了不同的条件。共用一张矩阵后,只要那一维的值是 12.0,输出就永远是 12·silu(12) ≈ 144,再也不可能被掐灭
所以省下的 89.13 M 换掉的正是这一章说的那件事:选择性。它退化成了一个特殊的逐元素非线性(相当于把 silu 换成 z²σ(z) 的两矩阵 MLP),门这个概念直接消失了。
顺带算笔账:这么改一层省 89.13 M,64 层省 5,704,253,440 ≈ 5.70 B,全模型从 27.36 B 降到约 21.65 B——省得不少,但省掉的是功能,不是冗余。

变式:那反过来,如果把 up_proj 去掉、直接让内容那条路等于输入 x 本身(形状对不上,先假装能对上),还算门控吗?(答案:算,但严重受限。开度仍由独立的 gate 算出,可以掐灭或放大;可失去的是「内容也要被重新投影一次」这一步——模型无法在撑宽的同时重组内容,17408 条规则只剩下 17408 个开关,没有各自的内容了。)

8.3 算账:一层 FFN 是 267.39M

三张矩阵,每张的形状都是 5120 × 17408(down_proj 是 17408 × 5120,参数个数一样)。矩阵的参数个数就是它的行数乘列数——这是第 0 章的内容。

PFFN = 3 × dmodel × dff = 3 × 5120 × 17408 = 267,386,880
式 8-3
符号是什么
PFFN一层 FFN 的参数个数267,386,880 ≈ 267.39 M
3三张矩阵:gate、up、down换成老式两矩阵 MLP 这里就是 2
dmodel模型的隐藏维,config 里的 hidden_size5120
dff撑宽后的中间维,config 里的 intermediate_size17408

先感受一下 267.39 M 这个数:一层 FFN 的参数,就比整个 Qwen3-0.6B 那个量级的小模型还多出一大截。而 Qwen3.8-27B 有 64 层。

关键的一步:64 层全都有 FFN

这里是最容易算错的地方。第 7 章刚说过 64 层里只有 16 层是全注意力,很容易顺手以为 FFN 也只有 16 层。不是。每一层——不管它的前半截是 Gated Attention 还是 Gated DeltaNet——后半截都挂着一个 FFN。第 7 章建造台阶里那句 layers.append(FFN(cfg)) 写在 if/else 外面,就是这个意思。

所以:64 × 267,386,880 = 17,112,760,320 ≈ 17.11 B

17.11 B ÷ 27.36 B = 62.6%。这个模型三分之二的参数,在这三张平平无奇的矩阵里。

Qwen3.8-27B 的 27.36 B 参数花在哪儿(按 mat/paramcount.out.txt 复算) FFN 62.6% 20.3% FFN(64 层 × 267.39 M) 17.11 B 62.6% Gated DeltaNet(48 层 × 115.88 M) 5.56 B 20.3% Gated Attention(16 层 × 104.86 M) 1.68 B 6.1% 嵌入 + 输出头 2.54 B 9.3% 视觉塔(27 层) 0.46 B 1.7%
图 8-2:参数去向。条形按真实比例绘制,数字来自本站 mat/paramcount.out.txt 的逐矩阵复算,不是估算。
表 8-1:Qwen3.8-27B 参数分布(合计 27.36 B = 文本塔 26.90 B + 视觉塔 0.46 B)
部件层数每层参数小计占比
FFN(稠密 SwiGLU)64267.39 M17.11 B62.6%
Gated DeltaNet48115.88 M5.56 B20.3%
嵌入 + 输出头各 1.27 B2.54 B9.3%
Gated Attention16104.86 M1.68 B6.1%
视觉塔270.46 B1.7%
一句话记住:你花五章学的注意力,只占 6.1%。参数的大头在 FFN——62.6%。注意力决定信息怎么流动,FFN 决定模型知道多少东西,而「知道多少」是要拿参数换的。

再对照第 7 章看一眼,会发现一个有意思的错位:参数占比显存增长是两件完全不同的事。FFN 占了 62.6% 的参数,但它一个字节的 KV cache 都不占——因为它逐位置计算,算完就扔,什么都不用缓存。反过来,Gated Attention 只占 6.1% 的参数,却贡献了 262K 上下文下那全部 16 GiB 的 KV。权重是一次性买断的固定成本,KV 是按使用量计费的变动成本。

两个 17.11 长得一样,但完全不是一回事

FFN 总参数是 17.11 B(个数,十亿),Q4_K_M 量化后的模型文件是 17.11 GB(体积,十亿字节)。这两个 17.11 纯属数字巧合,没有任何因果关系——前者是 64 层 FFN 的参数个数,后者是全部 27.36 B 参数压到约 4 比特之后的文件大小。看到相同的数字就以为「原来那个文件装的就是 FFN」,是这一章最容易掉进去的坑。

自己推一遍:从一张矩阵到 62.6%

这一节的所有数字都能从 config.json 的两个数推出来。别看答案,跟着算一遍——第 12 章要你把整个 27.36 B 从零数出来,这是那次的预演。

  1. 一张 5120 × 17408 的矩阵有多少个参数?先估个数量级再动手算。

    想好了再看

    5120 × 17408 = 89,128,960,约 8900 万。估算的话:5000 × 17000 = 8500 万,量级对得上。这一张矩阵就快赶上一个 0.1B 小模型的全部参数了。

  2. 一层 FFN 有三张这样的矩阵,为什么 down_proj 是 17408 × 5120 却还算同样多的参数?

    想好了再看

    因为矩阵的参数个数是行数 × 列数,和乘法的方向无关。5120 × 17408 和 17408 × 5120 都是 89,128,960 个数,只是排布方式不同。所以一层 = 3 × 89,128,960 = 267,386,880

  3. 现在最关键的一步:乘几层?在下笔之前,先回答——第 7 章那 48 层 Gated DeltaNet 后面有没有 FFN?

    想好了再看

    有。第 7 章的模型卡布局写的是 16 × (3 × Gated DeltaNet → FFN, 1 × Gated Attention → FFN)——箭头右边那个 FFN,在两种层后面都出现了。所以是乘 64,不是 16 也不是 48。这一步选错,后面全错,而且错得很像样(16 × 267.39 M = 4.28 B,看起来也是个合理的数)。

  4. 算出 FFN 总量,再算它占 27.36 B 的比例。

    想好了再看

    64 × 267,386,880 = 17,112,760,320 ≈ 17.11 B。17,112,760,320 ÷ 27,355,639,808 = 0.6256 = 62.6%

  5. 最后一步,也是最该被记住的一步:把注意力也算一遍,然后看看两个数摆在一起意味着什么。

    想好了再看

    16 层 × 104.86 M = 1.68 B = 6.1%
    FFN 是它的十倍还多。这意味着:如果你想把这个模型做小,动 FFN 的收益远大于动注意力;如果你想让它「知道得更多」,加宽 FFN 比加注意力头更直接。这个 62.6 : 6.1 的比例,正是下一章 MoE 之所以要动 FFN、而不是动注意力的全部理由——钱在哪儿,就改哪儿。

Qwen3.8-2.4T-A95B 的 hidden_size 是 8192,它的每个 MoE 专家中间维是 2048。按式 8-3 的思路,一个专家有多少参数?

先想:一个 MoE 专家本身就是一个小号的 SwiGLU FFN,三张矩阵一张不少。
关键在于直接套式 8-3,把 dmodel 换成 8192、dff 换成 2048。
第一步:8192 × 2048 = 16,777,216。再乘 3。
完整答案:3 × 8192 × 2048 = 50,331,648 ≈ 50.33 M。这和本站 mat/paramcount.out.txt 里点出来的「单个专家 50,331,648 = 50.33 M」完全一致。顺带留意一件事:这个专家的中间维只有 2048,比 hidden_size 8192 还——扩张比是 0.25,不是大于 1。这正是下一章要讲的「细粒度专家切分」,把一张大 FFN 切成很多很窄的小 FFN。

变式:512 个这样的专家一共多少参数?(答案:512 × 50,331,648 = 25,769,803,776 ≈ 25.77 B——一层 MoE 就快赶上整个 27B 模型了。这就是 2.4 万亿参数是怎么堆出来的。)

答辩:如果我是审稿人

你说「FFN 占 62.6%,所以它才是主角」。可参数占比高就等于重要吗?按这个逻辑,嵌入层占 9.3%,比注意力的 6.1% 还高,那嵌入层是不是也比注意力重要?你这是在用一个会计指标冒充重要性指标。

参考防守(先自己组织语言再看)

这个批评击中了要害,我必须缩小主张的范围。

我不主张的:参数多 = 重要。按这个逻辑确实会推出「嵌入层比注意力重要」这种荒谬结论。事实上把注意力全删掉,模型立刻退化成一个不看上下文的词频机器;而把 FFN 的中间维砍掉一半,模型只是变笨一点,还是能用。删得掉的和删不掉的,不由参数量决定。

我主张的是三件具体、可验证的事:(1) 显存账上,权重体积由参数量决定,所以谈「这个模型多大、要几张卡」时,FFN 是主要项——这不是价值判断,是算术。(2) 改造的杠杆在这里:想让参数量涨 100 倍,动 FFN 一次就够,动注意力得动上百次还未必够——下一章的 MoE 之所以只改 FFN,正是这个原因。(3) 读者的直觉需要被纠正:一路学到第 7 章,绝大多数人会以为注意力占了模型的大半,实际是 6.1%。指出这个错位本身就有价值。

所以更准确的说法应该是:FFN 是参数账上的主角,不是功能上的主角。本章标题写的是「62.6% 的参数都在这里」,不是「FFN 最重要」——这个区分我应该在正文里说得更明确,这条批评是成立的。

8.4 17408 这个数从哪来

17408 看起来像个随便挑的数。把它和 5120 一除,规律就出来了:

17408 ÷ 5120 = 3.4,正好是个一位小数。

扩张比expansion ratio):FFN 中间维和模型隐藏维的比值。它决定 FFN 把向量「撑多宽」再压回来。这个比值几乎是每个 Transformer 都要选的一个超参数。

为什么正好是 3.4 这么干净?因为这两个数都是 1024 的整数倍:5120 = 5 × 102417408 = 17 × 1024。所以扩张比其实是 17 ÷ 5,一个整数比。1024 的倍数对硬件友好——GPU 做矩阵乘法时按固定大小的块切分,维度能被 128、256 这类数整除时,最后一块不会只填一半,算力不浪费。

常见的扩张比有两个传统档位:

4 倍——最早的 Transformer 和 GPT 系列用的就是这个,那时候 FFN 是两张矩阵,参数量 = 2 × 4 d² = 8 d²。

8/3 ≈ 2.67 倍——LLaMA(arXiv:2302.13971)那一路的做法。逻辑很清楚:换成三张矩阵的 SwiGLU 后,为了让参数量和老式 4 倍两矩阵版本持平,就得把中间维乘上 2/3,于是 3 × (8/3) d² = 8 d²,正好对上。

Qwen3.8 的 3.4 落在这两档之间偏上:3 × 3.4 = 10.2 d²,比老式 4 倍 MLP 的 8 d² 多出 27.5%。翻译过来就是:它没有为了省参数而缩窄 FFN,反而在 FFN 上比传统配置多花了钱。这和 8.3 那个 62.6% 是同一件事的两种说法。

官方没有解释为什么是 3.4

上面那句「3.4 = 17 ÷ 5,对硬件友好」是本站从 config.json 读出比值后作的观察与推测,不是官方说明。Qwen3.8 没有技术报告、没有 arXiv 论文,模型卡也没有解释任何一个超参数的选取理由。我能确证的只有一件事:config 里 hidden_size = 5120intermediate_size = 17408,两者相除等于 3.4。为什么选 3.4 而不是 3.0 或 4.0,本站未能核实,也没有找到任何一手出处。凡是给你讲得头头是道的文章,去问它出处。

某人想把 Qwen3.8-27B 的 FFN 改成 LLaMA 那种 8/3 扩张比,隐藏维 5120 不变。(a) 新的中间维大约是多少?(b) 一层 FFN 参数变成多少?(c) 全模型参数从 27.36 B 变成多少?

先想:扩张比乘上隐藏维就是中间维。8/3 × 5120 是多少?
关键在于第三问:只有 FFN 那 17.11 B 会变,其余 10.24 B(DeltaNet + 注意力 + 嵌入 + 视觉塔)纹丝不动。
第一步:8/3 × 5120 = 13,653.3,实际实现会向上取整到 128 的倍数,取 13,696(= 107 × 128)。为方便估算,下面直接用 13,653 这个理论值算。
完整答案:
(a) 8/3 × 5120 ≈ 13,653(真实模型会对齐到 128 的倍数,如 13,696)。
(b) 3 × 5120 × 13,653 ≈ 209.7 M,比原来的 267.39 M 少约 21.6%。
(c) FFN 总量 64 × 209.7 M ≈ 13.42 B,比原来的 17.11 B 少 3.69 B。全模型 27.36 − 3.69 ≈ 23.67 B
注意结论怎么说才准确:模型总参数降了约 13.5%,不是降了 21.6%——因为 FFN 只占 62.6%,砍 FFN 的效果要按这个比例打折。这是本章最实用的一个计算习惯:改一个部件之前,先看它占多大比重。

变式:反过来,如果保持总参数 27.36 B 不变,把省下的 3.69 B 全部加到注意力层上(每层 104.86 M),能加出多少层全注意力?这样做的显存后果是什么?(答案:3.69 B ÷ 104.86 M ≈ 35 层。但每加一层全注意力,262K 上下文下的 KV cache 就多 1 GiB——加 35 层就是多 35 GiB,一张 24GB 卡直接爆掉。参数账和显存账不是同一本账。)

有人说:「FFN 占 62.6%,那我把 intermediate_size 从 17408 减半到 8704,模型体积就能减半,24GB 卡就轻松了。」这句话错在哪?减半之后真实的参数量是多少?

先想:减半的是 FFN 那一部分,不是整个模型。剩下那 37.4% 会跟着变吗?
关键在于分开算:变的部分(FFN 17.11 B)减半,不变的部分(27.36 − 17.11 = 10.24 B)原样保留,然后相加。
第一步:新的单层 FFN = 3 × 5120 × 8704 = 133,693,440。乘 64 得到新的 FFN 总量,再加上 10.24 B。
完整答案:
新单层 FFN = 3 × 5120 × 8704 = 133,693,440(正好是原来 267,386,880 的一半)。
FFN 总量 = 64 × 133,693,440 = 8,556,380,160 ≈ 8.56 B。
全模型 = 27,355,639,808 − 17,112,760,320 + 8,556,380,160 = 18,799,259,648 ≈ 18.80 B
所以模型只小了 31.3%,不是 50%。他错在把「FFN 减半」直接读成了「模型减半」,忽略了另外 37.4% 的参数一个都没动。
还有一处他没提的代价:中间维减半意味着 8.1 节那 17408 条「规则」只剩 8704 条,模型能记住的具体知识必然变少——省下来的显存是拿能力换的,不是白捡的。
另外顺带一提:这么改必须重新预训练。改结构不是量化,不能在训练好的权重上改——你不能把一张 5120×17408 的矩阵「压」成 5120×8704 还指望它照常工作。

变式:如果他改的不是 intermediate_size 而是 hidden_size(5120 减半到 2560),全模型参数会降多少?(提示:这次几乎所有部件都会跟着变——FFN、注意力、DeltaNet、嵌入层、输出头,全都含有 hidden_size 这个因子。所以降幅远大于 31.3%,而且这已经不是「改一个部件」,是换了一个模型。)

答辩:如果我是审稿人

你为 3.4 这个扩张比找的理由是「17408 = 17 × 1024,对硬件友好」。可 3.0(15360 = 15 × 1024)和 4.0(20480 = 20 × 1024)同样是 1024 的整数倍,同样对硬件友好。你这个解释根本没有区分度——它解释了「为什么是 1024 的倍数」,但一个字也没解释「为什么是 17 倍而不是 15 或 20」。

参考防守(先自己组织语言再看)

这条批评完全成立,我接受,而且这正是我在 caution 框里已经写明的:官方没有解释为什么是 3.4,本站也未能核实。

把两件事分开会更清楚:「17408 是 1024 的整数倍」是一个事实,它排除了一大批候选值(比如 17000、17500 这种不对齐的数),这个约束是真的;「为什么在对齐的候选里选了 17 而不是 15 或 20」是一个我答不上来的问题。我给的解释只覆盖了前者,审稿人说它没有区分度,对。

能补上的一点旁证是:Kaplan 等人 2020 年的 scaling law 工作(arXiv:2001.08361)有一个被反复引用的结论——语言模型的 loss 强烈依赖规模,而对模型「形状」(深宽比、扩张比这类)依赖很弱。如果这个结论在 27B 这个规模上仍然成立,那么 3.0、3.4、4.0 之间的差别可能本来就很小,团队完全可能是在若干个差不多的候选里,按训练时的吞吐实测挑了一个跑得最顺的。

但要说清楚:这是一个推测,不是解释。我拿不出 Qwen 团队的任何说明来支持它。诚实的结论是——这个数字的来源,本站未能核实。

8.5 铺垫:如果把这一张大矩阵切成很多小的

把这一章的账再看一眼:一层 FFN 267.39 M,64 层 17.11 B,占全模型的 62.6%。现在换个角度提问——

Qwen3.8-2.4T-A95B 是怎么做到 2.4 万亿参数的?

先用这一章的方法试着推一下。它的隐藏维是 8192。如果它也是稠密 FFN,用同样的 3.4 扩张比,中间维大约 27,853,一层 FFN 就是 3 × 8192 × 27853 ≈ 684 M。它有 92 层,FFN 总量约 63 B。离 2.4 万亿差着三十多倍

那把中间维再撑大三十几倍呢?中间维就得做到一百多万(27,853 × 38 ≈ 106 万)。这条路会死在两个地方:一是显存装不下——权重体积是按总参数算的,2.4 万亿参数光 BF16 权重就是 4.40 TiB;二是速度垮掉——稠密 FFN 的每一个参数,每来一个 token 都要参与一次乘加,参数涨三十倍,每个 token 的计算量就涨三十倍。

矛盾就摆在这儿:我们想要更多的参数(装更多知识),但不想要更多的计算(每个 token 还是要快)。稠密 FFN 把这两件事焊死了——参数量和计算量严格成正比。

留给你的思考题(下一章揭晓)

如果我想让参数量涨 100 倍,但每来一个 token 只用其中一小部分,你会怎么改这个 FFN?

提示:现在的做法是一张 17408 维的大 FFN,每个 token 都把这 17408 条规则全过一遍。如果把它切成很多张窄 FFN(比如 512 张,每张只有 2048 维),然后只挑其中几张来算——参数总量涨上去了,但每个 token 的计算量只由「挑了几张」决定,和「一共有几张」无关。

剩下的问题就变成了:谁来挑?按什么挑?挑错了会怎样?如果所有 token 都挑同一张呢?这四个问题就是第 9MoE 的全部内容。而 q8-3 那道题里那个「中间维只有 2048、扩张比 0.25」的奇怪专家,就是切出来的其中一张。

一位工程师要在 24GB 卡上跑 Qwen3.8-27B,262K 上下文。他列了三条优化方案:
(A) 把 intermediate_size 从 17408 砍到 8704;
(B) 把 KV cache 从 fp16 换成 fp8;
(C) 把 full_attention_interval 从 4 改成 8。
请分别说明:每条各省多少、省的是哪一本账、以及哪几条今天就能做、哪几条必须重新训练。

先想:这三条里,哪些改的是 config 里的结构字段,哪些只是改推理时的运行选项?这个区分决定了能不能今天就做。
关键在于分清两本账:权重账(由参数量决定,一次性)和 KV 账(由全注意力层数 × 上下文长度决定,随用量增长)。A 只动第一本,B 只动第二本,C 动第二本但方式不同。
第一步:先把两本账的基线写出来,注意统一到 GiB(第 5 章的单位约定)。权重:Q4_K_M 17.11 GB + 视觉塔 0.93 GB = 18.04 GB = 16.80 GiB。KV:262K × 64 KiB = 16 GiB(fp16)。两者相加已经 32.8 GiB,一张 24 GiB 的卡根本放不下——这就是为什么必须优化。
完整答案:
(A) 省权重账,必须重新训练。FFN 从 17.11 B 降到 8.56 B,全模型 27.36 → 18.80 B(降 31.3%,见 q8-5)。Q4_K_M 文件按比例约从 17.11 GB 降到约 11.8 GB。但这是改结构——已经训好的权重上做不到,必须从头预训练一个新模型。今天做不了。
(B) 省 KV 账,今天就能做。每 token 从 64 KiB 降到 32 KiB,262K 下 KV 从 16 GiB 降到 8 GiB,省 8 GiB。这是纯推理选项(llama.cpp 的 --cache-type-k/-v、vLLM 的 KV 量化开关),改个参数即可,权重一个字节不动。唯一今天就能做的一条。
(C) 省 KV 账,必须重新训练。全注意力层 16 → 8,每 token KV 64 → 32 KiB,262K 下 16 → 8 GiB。省的量和 (B) 一样,但代价完全不同:它改的是 layer_types,是结构,同样要重训;而且第 7 章说过,这会牺牲精确回忆能力,官方没有数据说牺牲多少。
合起来的结论:三条里只有 (B) 是「今天就能做」的,因为只有它不动结构。而 (A) 和 (C) 落在第 1 章那张四轴表的第 ①②③ 轴上——那三条轴上的一切都发生在训练之前。这道题真正在考的是:你能不能一眼看出一个优化落在哪条轴上,从而知道它是「改个参数」还是「重训一次模型」。
补一句:即便做了 (B),权重 16.80 GiB 加 262K 的 8 GiB fp8 KV 已经 24.8 GiB,仍然超过一张 24 GiB 的卡。本站在第 5、17 章给出的结论是——fp8 KV 下这张卡实际能撑到约 17–20 万 token(本站推导,框架开销为经验区间,真实值更靠近下沿;完整推导见第 5 章表 5-2),原生 262K 在 24GB 上跑不满

变式:如果他把三条全做了(假设 A、C 那个新模型已经训好了),24GB 卡能跑满 262K 吗?(答案:权重约 11.8 + 0.93 ≈ 12.7 GB,KV 在 8 层全注意力 + fp8 下是每 token 16 KiB,262K 约 4 GiB,合计约 16.7 GB,加上框架开销确实放得下。但这已经不是 Qwen3.8-27B 了——它是一个参数更少、精确回忆能力未知的另一个模型。这正是全站要反复说的那件事:换尺寸和换精度是两件事,前者要重新训练。

对你而言未知扩张比该定在多少,有没有一条能算的规律?

本章诚实地承认了:3.4 这个数从哪来,本站不知道,官方也没说。但这个问题不是「学界无解」——它属于第二类:答案(或至少大量证据)是存在的,只是不在这份材料里,需要你自己去拼。已知的线索有三条:Kaplan 等人(arXiv:2001.08361)报告 loss 对模型形状依赖很弱;LLaMA(arXiv:2302.13971)选 8/3 是为了让 SwiGLU 的参数量对齐老式 4 倍 MLP;而 Qwen3.8 选了更宽的 3.4。三条线索指向不同方向,说明这件事没有一个被普遍接受的公式。

先做这一步:打开五到十个开源模型的 config.json(各家都在 Hugging Face 上公开),把每个模型的 hidden_sizeintermediate_size、二者比值、FFN 是两矩阵还是三矩阵(看有没有 gate_proj)、以及模型总参数量,做成一张表。做完先别急着下结论,先回答一个问题:比值是随模型变大而升高、降低,还是根本没有趋势?然后带着这张表回去读 Kaplan 论文里关于「模型形状」的那一节,看你的观察和它对不对得上。这张表本身就是一个能拿得出手的小结果——因为公开材料里很少有人真的把它列出来过。

这一层要加什么:给每一层装上 SwiGLU FFN

为什么现在才加它:第 7 层刚把 64 层的骨架排好,但每一层的后半截还是空的——数据流过注意力或 DeltaNet 之后就直接进下一层了,中间没有任何非线性加工。这一层补上那三张矩阵。补完之后,你手上第一次有了一个参数量对得上真实模型的骨架:加上这一层,模型的参数量会从约 7.24 B 一跃到约 24.35 B(还差嵌入、输出头和视觉塔,第 11、12 章补齐)。

D_MODEL = cfg.hidden_size # 5120 D_FF = cfg.intermediate_size # 17408 class FFN: def __init__(self, cfg): self.gate = Linear(D_MODEL, D_FF, bias=False) # 89,128,960 self.up = Linear(D_MODEL, D_FF, bias=False) # 89,128,960 self.down = Linear(D_FF, D_MODEL, bias=False) # 89,128,960 def forward(self, x): # x: [..., 5120] return self.down(silu(self.gate(x)) * self.up(x)) # * 是逐元素乘 def silu(z): return z / (1.0 + exp(-z)) # 注意:第 7 层的循环里,两个分支后面都要挂 FFN —— 64 层全有 def n_params(d_model, d_ff, n_layers, gated=True): return (3 if gated else 2) * d_model * d_ff * n_layers

难点一:* 是逐元素乘,不是矩阵乘。这是这一层最容易写错、而且不会报错的地方——两个张量形状都是 [..., 17408],用矩阵乘法在数学上根本接不上,框架会直接抛形状错误,这算好事。真正阴险的是反过来:如果你的框架里 * 被重载成了矩阵乘,或者你手滑写成了 matmul,在某些形状下它能悄悄跑通,只是结果全错。写完先用一组小张量手验一遍。

难点二:三张矩阵都没有偏置bias=False)。config 里 attention_bias: false,现代大模型的线性层普遍不带偏置——省参数、也不影响效果。如果你手滑加上偏置,每层会多出 2×17408 + 5120 = 39,936 个参数,64 层就是 2.56 M。这个数小到你不会在总参数上一眼看出来,但会让第 12 章的逐矩阵点钞对不上官方数字。

难点三:顺序不能反。是 silu 作用在 gate 上,不是作用在 up 上,更不是作用在乘完之后。写成 silu(self.gate(x) * self.up(x)) 参数量一个不差、形状也全对、照样能训——只是它不再是 SwiGLU,和真权重对不上。

自己验:三个数必须分毫不差。
n_params(5120, 17408, 1) 应该正好输出 267,386,880n_params(5120, 17408, 64) 应该正好是 17,112,760,320
intermediate_size 从 17408 改成 8704(减半),单层应该正好变成 133,693,440——正好是原来的一半,一个不多一个不少。
gated 传成 False(去掉 gate,退化成老式两矩阵 MLP,intermediate_size 仍是 17408),单层应该正好变成 178,257,920——正好是 267,386,880 的 2/3。
三个数都对上,这一层就成了。若 ① 对但 ③ 差了 89,128,960,说明你的 gated=False 分支忘了真的把 gate 那张矩阵去掉,只是没用它。

本章小结

  • 分工:注意力(和 DeltaNet)负责「看谁」——跨位置搬运信息;FFN 负责「想什么」——逐位置加工。没有 FFN,整个模型就没有非线性,堆多少层都还是线性变换。
  • SwiGLU 是三张矩阵gate_proj(开多大)、up_proj(放什么过去)并行撑宽到 17408,逐元素相乘后由 down_proj 压回 5120。多出的第三张矩阵买到的是选择性——每条规则可以学到自己的启用条件。
  • 算账:一层 = 3 × 5120 × 17408 = 267,386,88064 层全都有 FFN(不管那层是 Attention 还是 DeltaNet),所以 64 × 267.39 M = 17.11 B,占 27.36 B 的 62.6%
  • 震撼点:Gated Attention 只占 6.1%(16 层 × 104.86 M = 1.68 B),Gated DeltaNet 占 20.3%,嵌入 + 输出头 9.3%,视觉塔 1.7%。你学了五章的注意力,在参数账上是个配角。
  • 参数账 ≠ 显存增长账:FFN 占 62.6% 参数但不占一字节 KV;注意力占 6.1% 参数却贡献 262K 下全部 16 GiB 的 KV。权重是买断的固定成本,KV 是按量计费的变动成本。
  • 17408 = 17 × 1024,除以 5120(= 5 × 1024)得扩张比 3.4,比 LLaMA 那一路的 8/3 更宽、比经典 4 倍两矩阵 MLP 多花 27.5% 的参数。为什么是 3.4,官方从未解释,本站未能核实。

下一章解决这一章最后抛出的那个矛盾:怎么让参数量涨上去,而每个 token 的计算量不跟着涨。答案是把这张 17408 维的大 FFN 切成 512 张 2048 维的小 FFN,每次只用 10 张——2.4 万亿参数就是这么堆出来的。

第9章 MoE:把 FFN 切成 512 份

上一章留下一个矛盾:知识存在 FFN 里,想多存就得加参数;可稠密 FFN 的每一个参数,每个 token 都要用一遍——加参数就是加计算。这一章讲那个把这两件事拆开的办法:把一张 17408 维的大矩阵,换成 512 张 2048 维的小矩阵,每次只用其中 11 张。Qwen3.8-2.4T 的两万四千亿参数,就是这么堆出来的。

学完这一章你应该能做到

  • 用一句话说清「条件计算」到底把哪两个数解耦了
  • 写出路由那张矩阵的形状,并算出它占一层 MoE 的百分之几
  • 手算 Qwen3.8-2.4T 一层 MoE 的总参数与激活参数,并说清两者差 46 倍是怎么来的
  • 解释那个永远被激活的第 11 个专家在解决什么问题
  • 说清 router_aux_loss_coef: 0.001 这个小系数在防什么灾难
  • 回答「27B 为什么不用 MoE」,而且不能答成「因为它小」
前置:第0章的矩阵乘法softmax、第8章的 SwiGLU(一个专家就是一个窄版 SwiGLU FFN,三张矩阵一张不少)、第7章的层布局(2.4T 的 92 层每一层后面都挂一个 MoE,和 27B 的 64 层都挂 FFN 是同一个道理)。

9.1 上一章留下的那个死结

先把第 8 章的账重抄一遍,因为整章都是从这三个数出发的。Qwen3.8-27B 的一层稠密 FFN 是 267,386,880 个参数(3 × 5120 × 17408),64 层加起来 17.11 B,占全模型的 62.6%。知识主要存在这里。

那么问题来了:如果想让这个模型多知道一百倍的东西,该怎么办?

最直接的想法是把 FFN 加宽——中间维从 17408 拉到 1,740,800。参数量确实涨了 100 倍。代价呢?这里需要第 0 章那条最朴素的事实:矩阵乘法里,每一个参数在每一次前向传播中都要被用上一次,做一次乘法、一次加法。所以对稠密模型来说:

一句话记住:稠密模型里,「有多少参数」和「每个 token 要算多少次乘加」是同一个数。参数涨 100 倍,每个 token 的账单就涨 100 倍。

这就是死结。你想要的是知识多,被迫付出的是每个字都变慢一百倍。而且这两件事在稠密结构里是绑死的,你没有任何旋钮可以只拧其中一个。

为什么会有 MoE 这个东西

因为有人问了一个看起来很傻的问题:凭什么每个参数都要被每个 token 用到?你写一份关于宋词的句子,为什么要惊动模型里那些关于 C++ 内存管理的知识?如果能让每个 token 只唤醒它真正用得上的那一小部分参数,那「参数总量」和「每个 token 的计算量」就不再是同一个数了——它们会变成两个可以分别拧的旋钮。这个想法叫条件计算conditional computation),MoE 是它到目前为止最成功的一种实现。

有人说:「把 27B 的 FFN 中间维从 17408 加宽到 34816,参数会翻倍,但因为矩阵还是那三张、只是变大了,所以计算量增加得没那么多。」这句话对吗?

先想:一次矩阵乘法要做多少次乘法?它和矩阵里有多少个数是什么关系?
关键在于:矩阵乘法里每一个矩阵元素恰好被用一次。所以「有几个参数」和「要做几次乘法」是一一对应的,跟矩阵被切成几张、每张多大都没关系。
第一步这样走:把 gate_proj 单独拿出来。它是 5120 × 17408,一个 5120 维的输入向量过它一遍,要做多少次乘法?答案就是 5120 × 17408 = 89,128,960 次,正好等于它的参数个数。加宽一倍以后呢?
完整答案:不对。中间维翻倍,三张矩阵的参数各翻倍,总参数从 267,386,880 变成 534,773,760;而每个 token 通过这一层要做的乘加次数,也从约 2.67 亿次变成约 5.35 亿次,同样是整整翻倍。在稠密结构里,参数量和每 token 计算量之间不存在任何折扣,它们是同一个数的两种叫法。这道题真正想让你记住的是:想让这两个数分家,不能靠「把矩阵切开」这种账面操作,必须真的让一部分参数在这一次前向里完全不参与——这正是下一节要做的事。

变式:把一张 5120 × 17408 的矩阵拆成两张 5120 × 8704 的矩阵,输出拼起来。参数量变吗?计算量变吗?(答案:都不变,分毫不差。拆分本身不省任何东西——省的必须是「不算」,不是「分开算」。)

9.2 条件计算:参数量和计算量第一次分家

混合专家Mixture of Experts, MoE):把一层里那一张大 FFN 换成很多张小 FFN(每一张叫一个专家expert),再配一个小小的路由器router,也叫 gate)来决定当前这个 token 该交给哪几个专家处理。没被选中的专家,这一次前向传播里一个乘法都不做。

这个结构最著名的一句广告词来自 Switch TransformerarXiv:2101.03961)的摘要,原文是:一个稀疏激活的模型,拥有「outrageous numbers of parameters」(大得离谱的参数量),「but a constant computational cost」(但计算成本恒定)。这句话里最重要的是那个 but:它宣告了参数量和计算量从此可以分别定价。

打个比方:一个只准同时借 11 本书的图书馆

把一层 MoE 想成一间小图书馆。书架上摆着 512 本专业书,每一本讲一个方向。你每处理一个 token,管理员先扫一眼你手上的问题,然后从架上抽 11 本递给你——你只读这 11 本。藏书量由架子上有几本决定(那是参数总量),你的阅读速度由每次拿几本决定(那是每 token 的计算量)。往架子上再加 500 本书,你的阅读速度一点也不会变慢。这就是参数量和计算量分家的全部含义。

类比失效处(这一条极其重要,第 10 章整章都建立在它上面):真实的图书馆可以把冷门书搬去仓库,用到时再取;MoE 不行——512 本书全部都得摆在架子上,也就是全部都得在显存里。原因有两个。第一,「取哪 11 本」是每个 token 现算出来的,下一个 token 可能换一批,你没有时间去仓库跑一趟:从内存或硬盘搬权重进显存的带宽,比显存自己的带宽低一到两个数量级。第二,真实推理是成批处理的,一批里几十上百个 token 各选各的,很快就把架子上大半的书都点亮了(9.6 节会给出具体数字)。所以 MoE 省的是算力,一个字节的显存都不省。

再补一句类比失效处:图书馆里是人在挑书,挑的依据是书名和目录;MoE 里是一个小神经网络在挑,它挑的依据既不是书名也不是内容,而是当前那个 5120 维(在 2.4T 里是 8192 维)向量指向哪个方向。这个「小神经网络」朴素得会让你吃惊——下一节专门讲它。

下面四句话,哪几句是 MoE 能做到的?(a) 让模型装下更多知识;(b) 让每个 token 的计算量不跟着涨;(c) 让模型占用的显存变小;(d) 让模型文件变小。

先想:显存里装的是什么?是「这一次用到的参数」还是「全部参数」?
关键在于类比失效处那一段:哪 11 个专家被选中,是运行时才知道的,而且逐 token 变化。所以你不能只把用得上的那部分放进显存。
第一步这样走:先判断 (c) 和 (d)。模型文件里存的是全部 512 个专家的权重,还是只存被选中的那些?
完整答案:只有 (a) 和 (b)。(c) 错——512 个专家全部要在显存里,MoE 反而让显存需求暴涨。(d) 错——模型文件里一个专家都不能少,2.4T 的 BF16 权重是 4.40 TiB,比 27B 的约 54.7 GB 大了将近 90 倍。MoE 是一笔交易:用显存去买算力。它让你在「每 token 的计算量不变」的前提下把知识量堆上去,代价是你得先买得起装下全部参数的那些卡。第 10 章会把这笔交易的账单完整列出来。

变式:如果有一天显存变得极其便宜而算力极其昂贵,MoE 的价值是变高还是变低?(答案:变高。MoE 交易的方向是「多花显存、少花算力」,所以显存越便宜、算力越贵,这笔交易越划算。反过来,在一张 24 GiB 的消费卡上——显存是一堵挪不动的墙——这笔交易根本做不成,这就是 9.7 节的答案。)

9.3 路由:谁来决定去哪几个专家

你可能会想象路由器是个复杂的东西——毕竟它要在 512 个专家里做选择,还要选得有道理。实际上它是这样的:

p = softmax(Wrx),其中 Wr 的形状是 512 × 8192
式 9-1
符号是什么直觉
x当前这个 token 走到这一层时的向量,8192 维「这个位置现在的状态」
Wr路由矩阵,512 行 × 8192 列,共 4,194,304 个参数512 个专家各自的「招牌」,每个招牌是一个 8192 维向量
Wrx512 个数,第 i 个是第 i 个专家的招牌与 x 的点积「这个 token 和第 i 个专家有多对味」
softmax第 0 章讲过:把任意一堆数压成一组和为 1 的正数把「对味程度」变成「概率」
p512 个概率,加起来等于 1一张给 512 个专家打的分数表

然后取分数最高的 10 个(num_experts_per_tok: 10),把这 10 个分数重新归一化成权重,让对应的 10 个专家各处理一遍 x,按权重加起来。再加上一个永远参与的共享专家(9.5 节讲它):

y = E共享(x) + Σi ∈ Top10(p) gi · Ei(x),其中 gi = piΣj ∈ Top10 pj
式 9-2
符号是什么直觉
Eii 个专家,就是第 8 章那个 SwiGLU FFN,只是中间维从 17408 换成 2048一本专业书
Top10(p)分数最高的 10 个专家的编号管理员抽出来的那 10 本
gi把那 10 个分数重新归一化后的权重,10 个数加起来等于 1「这 10 本书各占多大分量」
E共享共享专家,不参与选择,永远跑那本谁都要翻的工具书
y这一层的输出,还是 8192 维形状和进来时一模一样,下一层察觉不到里面换了结构
归一化那一步有两种常见写法,config 没说是哪一种

式 9-2 写的是「先对全部 512 个算 softmax,取出 top-10,再把这 10 个重新归一化」。另一种常见实现是「先取 top-10 的原始分数,再只对这 10 个算 softmax」。两者算出来的权重数值不同,训练好的权重不能互换。本站手上的 cfg24t.json 摘录里没有 norm_topk_prob 这类字段,官方也没有技术报告,所以本站无法确定 Qwen3.8 用的是哪一种,上面按较常见的一种写。这不影响本章的任何一个参数量结论——两种写法参数量完全一样。

输入 x 8192 维 路由 W 512 × 8192 只占这层的 0.016% softmax 512 个路由专家(每个 3 × 8192 × 2048 = 50.33 M) ⋯⋯ 深色 = 被选中的 10 个,其余 502 个这一次一个乘法都不做 共享专家(第 11 个) 不参与选择,永远跑 + 输出 y 8192 维
图 9-1:一层 MoE 的数据流。示意图,非官方原始图示;专家格子只画了 12 个代表 512 个。注意进出口的维度都是 8192——从外面看,这一层和一个普通 FFN 没有任何区别。

现在把这张路由矩阵放回它的尺寸感里:它有 4,194,304 个参数,而它所在的这一层一共有 25,824,329,728 个参数。0.016%。也就是说,决定这一层 258 亿参数里哪 5.58 亿会被唤醒的,是一张只占这一层万分之一点六的小矩阵。

常见误解:路由器一定是个很聪明的模块

很多人以为,既然它承担着「理解这个 token 属于哪个领域」的重任,它至少得是个几层的小网络。不是。它就是一张矩阵乘一个向量,再过一次 softmax——第 0 章讲完你就已经能完整读懂它的全部数学,没有隐藏层,没有激活函数,甚至不看上下文(每个 token 独立路由,前后 token 选了谁它一概不知)。整个 MoE 的稀疏性、专家分工、以及 9.6 节那个会要命的负载塌缩问题,全部由这一张朴素的矩阵引发。

这里有一个训练上的真问题,值得你知道

「取分数最高的 10 个」这个动作是不可导的——选中和没选中之间是一个跳变,梯度传不过去。实际训练时,梯度只能通过那 10 个被选中专家的权重 gi 回流到路由矩阵;没被选中的 502 个专家,这一步一点梯度都收不到。你现在先记住这句话,9.6 节会告诉你它会导致什么后果。(这是 MoE 这一类方法的公认难点,不是 Qwen3.8 特有的;Qwen3.8 具体怎么处理,官方没有公布。)

如果把 num_experts 从 512 改成 2048(其他都不动),路由矩阵会变成多大?它占这一层的比例会变高还是变低?

先想:路由矩阵的形状是 num_experts × hidden_size,两个因子里哪个在变?
关键在于:路由矩阵和专家总数是同比例增长的(都乘 4),所以比例这个问题的答案取决于分母的其他项怎么变。分母里除了专家,还有一个共享专家。
第一步这样走:先算新的路由矩阵 8192 × 2048 = ?再算新的一层总参数 = 2048 × 50,331,648 + 50,331,648 + 新路由。
完整答案:路由矩阵变成 8192 × 2048 = 16,777,216,是原来的 4 倍。一层总参数变成 2048 × 50,331,648 + 50,331,648 + 16,777,216 = 103,146,323,968(约 103.1 B)。占比 16,777,216 ÷ 103,146,323,968 = 0.0163%,和原来的 0.0162% 几乎一模一样,只是极其微弱地变高了一点点。原因:分子严格乘 4,分母里那 512→2048 的专家部分也乘 4,但共享专家那一项(50,331,648)没变,所以分母涨的倍数略小于 4,比值就略微上升。结论:无论专家有多少个,路由永远是这一层里可以忽略不计的一项。这道题想让你形成的直觉是——MoE 层的参数账,几乎就是「专家个数 × 单个专家」这一项说了算,另外两项是零头。

变式:如果反过来,把 hidden_size 从 8192 翻倍到 16384(专家数不变),路由矩阵占这一层的比例会怎么变?(答案:几乎不变。路由是 N × d,单个专家是 3 × d × d_e,两者都正比于 dd 一约就没了。真正能改变这个比例的只有两个数:专家的中间维 d_e,和每个专家用了几张矩阵。)

9.4 Qwen3.8-2.4T 的具体配置:逐条读那五个字段

现在打开 mat/cfg24t.json,和 MoE 相关的一共五个字段,一条条读:

表 9-1:Qwen3.8-2.4T-A95B 的 MoE 配置(逐字取自 mat/cfg24t.json)
字段它在决定什么
num_experts512书架上摆几本书。它单独决定了这一层有多少参数
num_experts_per_tok10每个 token 抽几本读。它单独决定了这一层每 token 算多少
moe_intermediate_size2048每个路由专家有多「胖」。对照:27B 的稠密 FFN 是 17408
shared_expert_intermediate_size2048共享专家的宽度,和路由专家一样胖
router_aux_loss_coef0.001训练时那条防塌缩的辅助损失的权重(9.6 节)

细粒度专家:2048 这个数有多窄

先感受一下 2048 有多窄。第 8 章讲过,27B 的稠密 FFN 是 hidden 5120 撑到 17408,扩张比 3.4——中间层比进出口宽三倍多。而 2.4T 的一个专家是 hidden 8192 撑到 2048,扩张比 0.25:中间层比进出口窄了四倍。这不是「一个小一点的 FFN」,这是一个瘦到几乎不像 FFN 的东西。

为什么要切这么细?DeepSeekMoEarXiv:2401.06066)的论文里把这个策略叫细粒度专家切分fine-grained expert segmentation),原文的说法是把专家「finely segmenting the experts into mN ones and activating mK from them」——把 N 个专家各切成 m 份变成 mN 个,同时把激活个数从 K 提到 mK。切完之后总参数量和总计算量都不变,变的只有一件事:组合数

这个「组合数」是整个细粒度策略的全部理由,值得把数字摆出来。假如是 64 个专家选 1 个,一共只有 64 种搭配方式;而 512 个选 10 个,搭配方式有

C(512, 10) = 312,268,282,598,377,321,216 ≈ 3.12 × 1020
式 9-3
符号是什么直觉
C(512, 10)从 512 个里不计顺序地取 10 个,有多少种取法这一层能摆出多少种不同的「知识组合」
3.12 × 1020三千一百二十亿亿如果每秒试一种组合,试完要约 9.9 万亿年,是宇宙年龄的七百多倍

同样的参数量、同样的计算量,切得更细就能表达出多得多的组合。这是细粒度专家唯一的、也是全部的论证。

算账:一层 MoE 到底多少参数

一个专家就是第 8 章那个 SwiGLU,三张矩阵一张不少,只是中间维换成 2048:

表 9-2:Qwen3.8-2.4T 一层 MoE 的逐项参数(算式与结果均来自本站 mat/paramcount.out.txt)
算式参数个数每 token 用不用
单个路由专家3 × 8192 × 204850,331,648(50.33 M)512 个里用 10 个
512 个路由专家512 × 50,331,64825,769,803,776(25.77 B)只有 503,316,480 被用到
共享专家3 × 8192 × 204850,331,648(50.33 M)全用
路由门8192 × 5124,194,304(4.19 M)全用
一层合计(总)25,769,803,776 + 50,331,648 + 4,194,30425,824,329,728(25.82 B)
一层合计(激活)11 × 50,331,648 + 4,194,304557,842,432(557.84 M)

两者的比值是 25,824,329,728 ÷ 557,842,432 = 46.29。也就是说,这一层里每 46 个参数只有 1 个会为当前这个 token 干活

一句话记住:27B 的一整层稠密 FFN 是 267.39 M;2.4T 的一层 MoE 是 25.82 B——是前者的 96.6 倍,也是整个 27B 模型(27.36 B)的 94.4%。2.4T 有 92 层这样的层。

这个对比值得多停留一秒。你花了八章造出来的那个 27B 模型,从嵌入表到输出头、64 层全部零件加起来 27.36 B——还装不满 2.4T 的一层。而 2.4T 有 92 层。

但如果只看每 token 的计算量,画风完全变了:2.4T 一层 MoE 激活 557.84 M,27B 一层 FFN 是 267.39 M,只差 2.09 倍。这 2.09 倍还能拆开验算:2.4T 每 token 实际过的中间维总和是 11 × 2048 = 22,528,27B 是 17,408,比值 1.29;hidden 是 8192 对 5120,比值 1.6;1.29 × 1.6 = 2.07,和 2.09 对得上(零头来自路由门)。

我能不看材料说清:一层 MoE 的总参数 25.82 B 和激活参数 557.84 M 各是怎么加出来的,以及 46 倍这个比值从哪来。

自己推一遍:从五个 config 字段到 25,824,329,728

  1. 第 8 章说 SwiGLU 是三张矩阵,形状都是 hidden × intermediateintermediate × hidden。现在 hidden = 8192、intermediate = 2048,一个专家有多少参数?

    想好了再看

    3 × 8192 × 2048 = 50,331,648。注意「专家」这个词听起来很唬人,但它就是一个窄版的 SwiGLU FFN——gate_projup_projdown_proj 一张不少,只是三张都从 17408 那一维换成了 2048。这一步是整章最容易被跳过、也最不该跳过的一步:MoE 没有发明任何新零件,它只是把老零件复制了 512 份。

  2. 512 个是多少?直觉上这个数会有多大?

    想好了再看

    512 × 50,331,648 = 25,769,803,776,约 25.77 B。停一下感受这个数:它已经差不多是整个 27B 模型的大小了,而这只是 92 层里的一层的路由专家部分。

  3. 还差什么?config 里那五个字段,你用掉了几个?

    想好了再看

    用掉了 num_expertsmoe_intermediate_size。还有一个 shared_expert_intermediate_size: 2048 说明有一个额外的共享专家,也是 3 × 8192 × 2048 = 50,331,648。还有路由矩阵,形状是 hidden × num_experts = 8192 × 512 = 4,194,304。路由门这一项最容易漏——它太小了,漏了你在小数点后第三位都看不出来,但第 12 章逐矩阵点钞时会对不上。

  4. 加起来。这是「总参数」。现在换一个问题:处理一个 token 时,上面这些里哪些真的动了?

    想好了再看

    总参数 = 25,769,803,776 + 50,331,648 + 4,194,304 = 25,824,329,728
    激活的是:10 个被选中的路由专家 + 1 个共享专家 = 11 × 50,331,648 = 553,648,128,再加上整个路由门——注意路由门是 100% 激活的,因为要给全部 512 个专家打分,那张矩阵得整个乘一遍。所以激活参数 = 553,648,128 + 4,194,304 = 557,842,432

  5. 比值是多少?它等于 512 ÷ 11 吗?

    想好了再看

    25,824,329,728 ÷ 557,842,432 = 46.29。而 512 ÷ 11 = 46.55,不相等。差在哪?差在共享专家和路由门这两项在两张账单上的地位不同:共享专家在分母里,路由门在分子分母里都是全额。这个小差别在这一章无关紧要,但它是第 10 章那个建造台阶的题眼——激活参数必须独立算一遍,绝不能拿总参数去乘一个比例。

  6. 最后一问,反过来推:如果把 num_experts_per_tok 从 10 改成 20,总参数变吗?激活参数变多少?

    想好了再看

    总参数一个字节都不变(还是 25,824,329,728)——书架上的书没多也没少。激活参数从 557,842,432 涨到 21 × 50,331,648 + 4,194,304 = 1,061,158,912,几乎翻倍。这一问就是整章的中心思想:num_experts 单独控制显存那本账,num_experts_per_tok 单独控制算力那本账,两个旋钮互不干扰。第 10 章会把这句话变成一整章。

某人想让 2.4T 的推理变快一倍,同时不增加显存需求。他有两个方案:(A) 把 num_experts 从 512 减到 256;(B) 把 num_experts_per_tok 从 10 减到 5。哪个能达到目的?另一个会发生什么?

先想:这两个字段各自出现在「总参数」和「激活参数」的哪个公式里?
关键在于:num_experts 只出现在总参数里(还有路由门那一小项),num_experts_per_tok 只出现在激活参数里。速度看激活,显存看总。
第一步这样走:分别算 (A) 和 (B) 之后的一层总参数与一层激活参数,四个数摆出来对比。
完整答案:(B) 达到目的。方案 B 下总参数不变(还是 25,824,329,728,显存需求分毫不动),激活参数从 557,842,432 降到 6 × 50,331,648 + 4,194,304 = 306,184,192,约降到 55%——算力接近减半。方案 A 恰恰相反:总参数掉到 256 × 50,331,648 + 50,331,648 + 8192×256 = 12,939,428,864(显存需求几乎减半,正是他不想要的那一项),而激活参数是 11 × 50,331,648 + 2,097,152 = 555,745,280,只掉了 0.38%——速度基本没变。他把两个旋钮认反了。
但必须补一句诚实的话:两个方案都是在改结构,都必须重新预训练。已经训好的 2.4T 权重上,你既不能删专家也不能改激活数——删专家等于扔掉学到的知识,改激活数等于换了一个和训练时不一致的前向过程。这两个旋钮是给「设计模型的人」用的,不是给「部署模型的人」用的。这正好落在第 1 章那张四轴表的第 ② 轴上:训练之前。

变式:有没有一个旋钮,是部署的人今天就能拧、并且真的能省显存的?(答案:有,但不在这一章——是第 ④ 轴的量化,把每个参数从 16 bit 记成 4 bit。它不动专家个数、不动激活数,只动记录精度,所以已经训好的权重上就能做。第 15、16 章讲它。)

9.5 共享专家:那个永远被激活的第 11 个

回到式 9-2 里那个 E共享。它和另外 512 个专家形状完全一样(3 × 8192 × 2048 = 50,331,648),唯一的区别是:它不参与选择,每个 token 都要过它一遍

不设共享专家会怎样

设想没有它,512 个路由专家各管一摊。现在来了一个 token,它需要的知识里有一部分是所有 token 都需要的——比如基本的语法结构、常见词的形态变化、「这是一个句子的结尾」这类判断。这些知识不属于任何一个专门领域,可是每个 token 都要用。结果就是:512 个专家里的每一个,都必须自己学一份。同一份通用知识被复制了 512 遍,占掉每个专家本来可以拿去学专门东西的容量。这就是 DeepSeekMoE 论文里说的冗余redundancy)。

解决办法朴素得可爱:把这份通用知识单独抽出来,放进一个永远激活的专家里。DeepSeekMoE(arXiv:2401.06066)把这个策略叫共享专家隔离shared expert isolation),原文写的是「isolating Ks experts as shared ones, aiming at capturing common knowledge and mitigating redundancy in routed experts」——隔离出若干个共享专家,目标是承接通用知识、减轻路由专家里的冗余。

这两条策略——细粒度切分共享专家隔离——正是 DeepSeekMoE 论文的全部两大主张,而 Qwen3.8-2.4T 的配置把它们同时用上了:512 个又细又窄的路由专家(细粒度),外加 1 个永远在岗的共享专家(隔离)。

再看一眼共享专家的性价比:它占这一层总参数的 50,331,648 ÷ 25,824,329,728 = 0.195%(五百分之一),却占这一层激活参数的 50,331,648 ÷ 557,842,432 = 9.0%(十一分之一)。用总量千分之二的参数,接住了每个 token 都要用的那部分知识。

这里有一句话本站不能替 Qwen3.8 说

「共享专家真的学到了通用知识」这件事,本站没有任何针对 Qwen3.8 的证据。能确认的只有事实层:cfg24t.json 里有 shared_expert_intermediate_size: 2048 这个字段,说明结构上确实存在一个共享专家。至于它学到了什么、有没有真的减少冗余,Qwen3.8 没有技术报告,官方也从未公布任何专家分析。上面那套「通用知识 vs 专门知识」的说法,来自 DeepSeekMoE 论文在它自己的模型上做的消融实验和论证。你可以说「Qwen3.8 采用了这个结构」,但不能说「Qwen3.8 的共享专家学会了语法」——后一句没人验证过。

有人提出一个「改进」:既然共享专家这么划算,那就把共享专家的数量从 1 个加到 11 个,同时把 num_experts_per_tok 从 10 降到 0(完全不路由)。他说这样激活参数不变,还省掉了路由的麻烦。这个模型会退化成什么?为什么这是个坏主意?

先想:11 个「永远都跑」的专家,它们的输出是被加起来的。若干个并联的线性-非线性模块加起来,和一个更宽的单模块比,差别在哪?
关键在于:如果一组模块的选择方式永远相同(全都选),那么「分成几个」这件事就不再携带任何信息了。此时组合数是多少?
第一步这样走:算一下这个新配置的总参数和激活参数,看它们是否相等。相等意味着什么?
完整答案:它退化成一个稠密 FFN,而且是一个中间维只有 11 × 2048 = 22,528 的普通稠密 FFN。总参数 = 11 × 50,331,648 = 553,648,128(路由门没用了可以删掉),激活参数也是 553,648,128,两者相等——稀疏度变成 100%,MoE 的全部意义消失。
为什么这是坏主意:11 个「永远一起跑、输出直接相加」的窄 SwiGLU,和一个中间维 22,528 的宽 SwiGLU 在数学上几乎等价(把 11 份 gate/up 沿中间维拼起来、down 沿中间维拼起来,就是同一个东西)。你得到的是一个 5.5 亿参数的稠密 FFN,而不是一个 258 亿参数的 MoE 层。那 512 个专家、3.12 × 1020 种组合,全没了。
这道题的题眼是:MoE 的价值不在「有很多个模块」,而在「每次只选一部分,且选谁是随输入变的」。去掉选择,剩下的只是一个被拆开写的稠密层。共享专家之所以划算,恰恰是因为它只有一个——它是那个「不需要选择的例外」,而不是规则本身。

变式:反过来,把共享专家删掉、num_experts_per_tok 从 10 改成 11,激活参数一样吗?这两个配置有什么实质区别?(答案:激活参数完全一样,都是 11 × 50,331,648 + 4,194,304 = 557,842,432。实质区别在于:前一种保证有一个专家一定在场,后一种下 11 个全靠竞争产生,那份通用知识只能靠某几个专家在竞争中自发承担——DeepSeekMoE 的主张正是「与其指望它自发形成,不如直接指定一个」。这个区别在参数账上完全看不出来,只体现在训练动力学上。)

9.6 负载均衡:不管它,路由会塌

现在讲那个最小的字段:router_aux_loss_coef: 0.001。这个小到不起眼的系数,防的是 MoE 最经典的一场灾难。

不管它会怎样:富者愈富

训练刚开始时,512 个专家是随机初始化的,路由矩阵也是随机的。纯靠运气,某几个专家会稍微多被选中一点点。接下来发生的事情是一条正反馈:被选中得多 → 收到的梯度多 → 学得更好 → 分数更高 → 被选中得更多。而 9.3 节那个「没被选中就收不到梯度」的性质,让这条正反馈没有任何刹车——落后的专家不是学得慢,是完全不学

放任下去的终点是:路由退化成「永远选那固定的十几个」,其余四百多个专家从头到尾没被训练过,是一堆随机数。这时候你手上的 2.4T 模型,实际有效的部分只相当于一个小得多的稠密模型——而你还得为那 4.40 TiB 的显存买单。这就是路由塌缩routing collapse)。

对策是在训练的损失函数里加一项辅助损失auxiliary loss),专门惩罚不均衡。Switch Transformer(arXiv:2101.03961)给出的形式是这样的:

Laux = α · N · Σi=1N fi · Pi
式 9-4
符号是什么直觉
α系数,Qwen3.8 的 config 里就是 router_aux_loss_coef = 0.001「这条纪律有多重要」的定价
N专家个数,512用来做归一化,让式子的取值范围和 N 无关
fi这一批 token 里,被派给第 i 个专家的比例「第 i 个专家实际接了多少活」
Pi这一批 token 里,路由给第 i 个专家的平均概率「路由器有多想派给它」——这一项是可导的,梯度从这里走

这个式子的取值范围可以手算,非常直观。完全均衡时每个 fi = Pi = 1/512,求和得到 512 × (1/512)² = 1/512,再乘 N = 512,结果是 1,所以 Laux = α = 0.001。完全塌缩(全部派给同一个专家)时 fP 都变成 1,求和得 1,乘 512 得 512,Laux = 512α = 0.512

所以这一项在损失函数里的贡献,在 0.001 到 0.512 之间浮动。它的设计很巧妙:fi(实际派单数)是数出来的,不可导;Pi(路由概率)是算出来的,可导。两者相乘,梯度就能顺着 Pi 流回路由矩阵,把它往「别老盯着那几个」的方向推。

式 9-4 是 Switch Transformer 的形式,不一定是 Qwen3.8 的

本站能确认的只有一件事:cfg24t.json 里有 router_aux_loss_coef: 0.001 这个字段名,说明训练时确实用了某种带系数的路由辅助损失。但这个系数乘在哪个式子上,config 没有说,Qwen3.8 也没有技术报告。式 9-4 是 Switch Transformer 论文给出的形式,也是这个字段名在开源框架里最常对应的那一个,本站按它讲解。
因此有一个诱人的对比必须打住:Switch Transformer 论文用的 α 是 0.01,Qwen3.8 是 0.001,看起来「小了十倍」。只有在两者用同一个式子、同一种归一化时,这个对比才有意义——本站无法核实这一点,所以不下这个结论。另外,这个方向近年也出现了完全不用辅助损失、改成动态调整每个专家偏置的做法;Qwen3.8 有没有额外用这类手段,同样未知。

还有一件事必须说清:辅助损失只在训练时存在,推理时完全不起作用。推理时路由器就是一张训好的固定矩阵,谁分数高选谁。所以即使训练时均衡得很好,如果部署时遇到的输入分布和训练时差很远(比如全是某个特定领域的文本),照样可能出现严重倾斜——只不过那时它不再是「模型学不好」的问题,而是「专家并行下某几张卡忙死、其余闲死」的工程问题。

在一个 N = 8、α = 0.01 的小 MoE 上,你观察到 Laux = 0.02。路由是均衡的还是塌了?如果同一个模型上 Laux 涨到 0.08,说明什么?

先想:完全均衡时 Laux 等于多少?完全塌缩时呢?把这两个端点先算出来,才有尺子。
关键在于:完全均衡 → Laux = α;完全塌缩到一个专家 → Laux = 。这两个端点之间是连续的。
第一步这样走:α = 0.01,N = 8,所以下界是 0.01,上界是 0.08。观察值 0.02 落在哪儿?0.08 又落在哪儿?
完整答案:下界(完全均衡)是 α = 0.01,上界(全部挤到一个专家)是 = 8 × 0.01 = 0.08
观察值 0.02 是下界的 2 倍,处在区间的低端——有一定倾斜但远谈不上塌缩,属于正常训练中的波动。
观察值 0.08 正好等于上界,说明 ΣfiPi = 1,这只有在所有 token 都被派给同一个专家、且路由概率也全部集中在它身上时才成立——完全塌缩了。此时这个 8 专家的 MoE 层实际上就是一个单专家的稠密层,另外 7 个是死重。
这道题真正想教的是一个可操作的习惯:辅助损失不只是一个用来优化的项,它同时是一个免费的监控指标。训练时把 Laux / α 这个比值打出来,它就是一个从 1(完美均衡)到 N(完全塌缩)的「不均衡度」读数,一眼就能看出路由健不健康。放到 Qwen3.8-2.4T 上,这个读数的范围是 1 到 512。

变式:如果你把 α 设成 0(完全不管均衡),Laux 的读数会怎么变?你还能用它监控吗?(答案:Laux 恒等于 0,读数废了。但 ΣfiPi 这一项本身还是可以照常计算并打印出来的——只是不把它加进损失、不让它产生梯度。「监控它」和「优化它」是两件可以分开的事,这也是评估任何正则项的通用手法。)

9.7 那 27B 为什么不用 MoE

读到这里你可能会问:MoE 这么好,同样的推理成本能装下几十倍的知识,那 27B 为什么不用?

答案不是「因为它小」。答案在 9.2 节那个类比失效处里:MoE 省的是算力,一个字节的显存都不省。而 27B 的定位是「一张消费级显卡在本地跑」,在这个场景里,显存是一堵挪不动的墙。

把这个取舍量化一下。假设我们真的给 27B 换上 MoE——照搬 2.4T 的配方,每层 512 个专家、每个中间维 2048、每 token 选 10 + 1,只是 hidden 保持 5120:

表 9-3:一个假想实验——如果 27B 换成 MoE(本站构造的配置,不是任何真实存在的模型)
真实的 27B(稠密)假想的 27B-MoE倍数
单个专家 / 单层 FFN3 × 5120 × 17408 = 267,386,8803 × 5120 × 2048 = 31,457,280
每层 FFN 总参数267,386,88016,140,206,08060.4×
64 层 FFN 合计17.11 B1,032,973,189,120(约 1.03 T)60.4×
全模型总参数27.36 B约 1.043 T38.1×
全模型激活参数27.36 B约 32.56 B1.19×
BF16 权重体积约 54.7 GB约 2.09 TB38.1×

看最后三行:每个 token 的计算量只多了 19%(因为 11 × 2048 = 22,528 比 17,408 略宽),可是权重体积涨了 38 倍。对一个要跑在 24 GiB 卡上的模型来说,这不是「代价大一点」,这是方案直接不成立——Q4_K_M 量化之后大约还要 587 GB,比原来的 17.11 GB 多了 34 倍,什么消费卡都装不下。

表 9-3 是本站构造的假想配置

Qwen3.8-27B 没有 MoE 版本,官方也从未解释过为什么 27B 选稠密。上面这张表是本站把 2.4T 的 MoE 配方套到 27B 的 hidden 上算出来的思想实验,用来给取舍一个数量级上的尺度感,不是任何真实模型的数据。真实的 MoE 设计里,专家宽度、专家数、激活数都会跟着 hidden 和目标场景一起重新选,不会这样硬套。

一句话记住:同一个技术,在两个场景里的价值完全相反。在数据中心,显存可以靠加卡解决(2.4T 就是这么解决的,24 张卡),算力才是每个 token 都要付的账,所以 MoE 划算。在单卡本地,卡加不了,24 GiB 就是 24 GiB,MoE 用来交易的那样东西你根本拿不出来。

这就是为什么这两个模型在第 ② 轴(稠密 vs 稀疏)上分道扬镳:它们不是「一大一小」,是为两种完全不同的部署场景各自设计的两台机器。下一章会把这句话变成一张完整的账单。

一家公司要给内部一万名员工提供代码助手,两个候选:(甲) 在自建机房部署 2.4T-A95B;(乙) 给每个工位配一张 24 GiB 显卡跑本地的 27B。假设两者效果他们都能接受。从这一章学到的东西出发,指出三处甲、乙两案在「参数账」上的本质差异,并说明为什么「哪个更省」这个问题在本章的知识范围内无法回答

先想:这一章反复区分的两本账是哪两本?把甲、乙分别代进去。
关键在于:显存这本账是一次性的、集中的(甲案一套机器服务一万人),算力那本账是按次计费的、随请求量线性增长的。乙案则把两本账都乘以一万,但每一份都很小。
第一步这样走:先把四个数摆出来——甲案的总参数 2.420 T / 激活 95.29 B,乙案的总参数 27.36 B / 激活 27.36 B。然后问:这四个数里,哪几个要乘以「一万」?
完整答案:
差异一(显存账的形状):甲案的 4.40 TiB 权重是一份,一万人共用;乙案是 18.04 GB 乘以一万份,合计约 180 TB。按总量算乙案反而多得多,但它是分散的、且用的是便宜得多的卡。
差异二(算力账的斜率):甲案每个 token 的乘加量对应 95.29 B 参数,乙案对应 27.36 B,比值 3.48。也就是说甲案每一次请求都比乙案贵 3.5 倍的算力;请求量越大,这个差距的绝对值越大。乙案的算力天然随人头分摊,斜率为零(每个人自己的卡算自己的)。
差异三(稀疏度的意义):甲案的 3.94% 稀疏度说明它的显存利用率极低——4.40 TiB 里每一次前向只有约 173 GiB 在干活;但这正是它能用「95 B 的算力成本」提供「2.42 T 的知识量」的原因。乙案稀疏度 100%,显存利用率满格,但知识量就是 27.36 B 封顶。甲案买的是知识量,付的是显存;乙案买的是分摊,付的是知识量上限。
为什么本章回答不了「哪个更省」:因为这一章只教了怎么数参数,而「省不省」至少还需要四类本章完全没有的信息——① 硬件价格(本站没有任何价格来源,也不引用);② 实际并发与请求分布(甲案的成本高度依赖批处理效率,一万人不会同时敲回车);③ 显存带宽与并行方式(第 10 章会说明,生成阶段的速度更多受带宽而非纯算力约束);④ 两个模型在这家公司真实任务上的效果差距(题目里是「假设都能接受」,现实中这个假设最经不起推敲,而且第 18 章会讲为什么榜单分数替代不了它)。
这道题的题眼是:参数账是成本估算的必要输入,但绝不是充分输入。凡是只拿参数量就断言「A 比 B 便宜」的说法,都跳过了上面至少四步。

变式:如果把甲案的服务对象从一万人改成十个人,三处差异里哪一处的结论会翻转?(答案:差异一。4.40 TiB 的显存是固定成本,与用户数无关;十个人分摊和一万个人分摊,人均显存成本差一千倍。这正是 MoE 这类「高固定成本、低边际成本」结构的典型特征——它只有在请求量足够大时才摊得开。第 13 章讲推理预算时会把这条曲线画出来。)

答辩:如果我是审稿人(一)

你说稀疏度 46 倍,每个 token 只用 1/46 的参数。那我干脆只把那 11 个专家加载进显存不就行了?其余 501 个放在内存里,需要时再换进来。你凭什么说 MoE 不省显存?

参考防守(先自己组织语言再看)

这个方案在单个 token、单条请求的理想情况下确实成立,但真实推理里它有两处会崩。

第一处是时间尺度。「哪 11 个」是这一层的路由器在这一刻现算出来的,算完立刻就要用。而从主机内存搬权重进显存要走 PCIe,带宽比显存自身低一到两个数量级。每一层都要搬一次,92 层搬 92 次,而且下一个 token 的选择可能完全不同、又要重搬。生成一个 token 需要搬进来的量是 11 × 50.33 M × 92 × 2 字节 ≈ 101 GB——你每吐一个字就要过一遍百 GB 级的数据。这不是「慢一点」,这是把一个按毫秒计的操作变成按秒计。

第二处更致命:批处理。真实推理服务从来不是一次一个 token,而是一批几十上百个 token 一起算。做个估算:一批 64 个 token,每个选 10 个专家,共 640 次派单;在均匀路由的假设下,被点亮的专家数期望是 512 × [1 − (1 − 1/512)640] ≈ 365 个——一批 64 个 token 就点亮了 512 个专家里的 71%。批再大一点就基本全亮。所谓的「只用 1/46」是单个 token 视角的性质,一旦进入批处理,它几乎立刻消失。

所以正确的表述是:MoE 省的是「每个 token 的乘加次数」,不是「需要常驻显存的字节数」。这两句话在单 token 视角下容易被混为一谈,在批处理视角下泾渭分明。这也是第 10 章那句核心结论——显存看总参数,速度看激活参数——真正的成因。

答辩:如果我是审稿人(二)

路由器只是一张矩阵加一个 softmax,连隐藏层都没有。凭什么相信这么朴素的东西能学会把 512 个专家分好工?你有 Qwen3.8 上的证据吗?

参考防守(先自己组织语言再看)

先把最诚实的话说在前面:本站没有任何 Qwen3.8 上的证据。Qwen3.8 没有技术报告,官方没有公布任何专家使用率统计、路由熵曲线或专家分工分析。「512 个专家各自学到了什么」这个问题,就本站掌握的材料而言,无人公开回答过

能替这个设计辩护的只有两条,而且都要打折扣。

第一条,路由器不需要「理解」。它要做的事在数学上只是「把 8192 维空间切成 512 块」——每个专家的那一行就是一个方向向量,点积取最大相当于找最近的那几个方向。这是一个可学习的空间划分,不是一个需要推理能力的任务。用线性层做划分、用 softmax 做软化,这个组合本身并不比 K 近邻更神秘。

第二条,它是被主任务反向推着学的。路由矩阵的梯度来自最终的语言建模损失:如果把某个 token 派给某个专家之后预测得更准,那条路径上的权重就会被加强。也就是说,分工不是被设计出来的,是被「预测下一个词」这个目标压出来的。

但必须补一句反向的诚实:「专家学会了人类可理解的分工」这件事,在这一领域是有争议的。不同工作报告的专家专门化程度差异很大,有的观察到明显的模式对应,有的发现专家分配与人类眼中的「领域」相关性很弱。本站不引用具体结论(没有逐字核实过),只陈述这个争议存在。所以正确的态度是:相信这个结构能训出好模型(有大量模型作为存在性证明),但不要相信「第 137 号专家负责数学」这类说法——那不在任何一份本站核实过的材料里。这个问题本站已经把它写进了本章的研究课题。

答辩:如果我是审稿人(三)

辅助损失是在损害主任务。你为了让 512 个专家雨露均沾,逼着路由器做出它本来不想做的选择——这不是本末倒置吗?0.001 这个数你们凭什么说它是对的?

参考防守(先自己组织语言再看)

这个批评是对的,而且它指出的是一个真实的、无法消除的取舍,不是一个可以辩护掉的缺陷。

取舍的两端很清楚。α 太大,路由器为了压低辅助损失会倾向于近乎随机地派单,专家之间学不出任何差异,512 个专家退化成 512 份大同小异的权重——你付了 46 倍的显存,买到的是一个平均意义上的稠密层。α 太小,回到 9.6 节那条正反馈,路由塌缩,四百多个专家变成随机数。0.001 就是这条取舍曲线上的一个报价,它同时承认了两件事:不均衡是有害的,以及为了纠正它可以牺牲一点点主任务。

但对「0.001 是不是对的」这个问题,本站的答案是:不知道,而且没人能从公开材料里知道。能确认的只有这个字段的值。官方没有公布调参依据、没有公布不同 α 下的对照实验、没有公布训练过程中的专家使用率曲线。任何声称「0.001 是最优值」或者「0.001 比 0.01 更先进」的说法,都超出了公开材料能支撑的范围——包括 9.6 节里那个诱人的「小十倍」对比,本站已经明确拒绝下这个结论,因为连两者是不是同一个式子都不确定。

还有一层更根本的反驳值得留给读者:「均衡」本身是不是一个好目标,其实也是可疑的。如果输入数据本来就分布不均(现实中的语料一定不均),强行要求每个专家接同样多的活,等于要求模型去拟合一个和数据分布不符的约束。这个方向近年确实催生了不再用辅助损失、改用其他机制维持均衡的做法。所以更准确的说法是:辅助损失是一个已知有副作用的权宜之计,它被广泛使用是因为它简单有效,不是因为它没有问题。

真未解专家数、激活数、专家宽度这三个数,有没有一条能算的规律?

Qwen3.8-2.4T 选了 512 / 10 / 2048。为什么不是 256 / 8 / 4096?不是 1024 / 16 / 1024?这三个数的乘积关系决定了总参数与激活参数,但在固定这两个总量的前提下,怎么分配仍然有无穷多种选法,而不同的选法会训出不同的模型。

本站把这个问题标成「真未解」,判据是:如果存在公认的公式,各家不会给出差这么远的配置。公开可查的几个数据点分散得触目惊心——Mixtral(arXiv:2401.04088)是 8 个专家选 2;Qwen3 技术报告(arXiv:2505.09388)里的 MoE 是 128 个选 8;Qwen3-Next 与 Qwen3.8-2.4T 是 512 个选 10 加 1 个共享。三年之内,专家数跨了两个数量级。DeepSeekMoE(arXiv:2401.06066)论证了「切得更细更好」的方向,但它给的是方向,不是一个能算出「切到 512 就够了」的判据——也没有回答切到 4096 会不会更好、以及在哪一点开始不划算。

先做这一步:打开本章那个 MoE 路由台,把激活参数固定住(保持 (k+1) × 3 × d × d_e 不变),然后沿着「专家更多但更窄」的方向连续调——比如 (N=64, k=1, d_e=16384)、(128, 2, 8192)、(256, 5, 3277)、(512, 10, 2048)、(1024, 21, 977)——每一步记下三个数:总参数、组合数 C(N, k)、以及单个专家的扩张比 d_e/d。做完先别急着找「最优」,先回答一个更基础的问题:沿着这条线走,组合数是单调增的吗?扩张比跌破多少之后,一个「专家」还能算是一个有意义的非线性单元?(提示:当 d_e 小到接近个位数时,一个专家就退化成了几乎没有表达能力的东西——这个下界的存在本身,就是细粒度切分不能无限进行的第一个可算的理由。)算完这张表,再回去读 DeepSeekMoE 的第 3 节,看它给的论证是不是「组合数」这一条,以及它有没有讨论下界。这张表本身就是一个拿得出手的小结果,因为公开材料里很少有人把「固定激活参数、只改切分粒度」这条线真的画出来过。

这一层要加什么:把稠密 FFN 换成 512 个专家 + 1 个路由器

为什么现在才加它:第 8 层给每一层装上了 SwiGLU FFN,那是 27B 的形态。这一层做的事是把同一个位置的零件换掉——层布局(第 7 层)、注意力(第 4 层)、DeltaNet(第 6 层)全都不动,只把「FFN」这个槽位里的东西换成一组专家加一个路由器。换完之后,你手上的代码第一次能同时表达两台机器:给它 cfg27b 就是稠密的 27B,给它 cfg24t 就是稀疏的 2.4T。

N_EXP = cfg.num_experts # 512 TOP_K = cfg.num_experts_per_tok # 10 D = cfg.hidden_size # 8192 D_E = cfg.moe_intermediate_size # 2048 class MoE: def __init__(s, cfg): s.experts = [FFN(D, D_E) for _ in range(N_EXP)] # 每个都是第8层那个 SwiGLU s.shared = FFN(D, cfg.shared_expert_intermediate_size) s.router = Linear(D, N_EXP, bias=False) # 8192 x 512 = 4,194,304 def forward(s, x): # x: [..., 8192] p = softmax(s.router(x)) # 512 个分数,和为 1 idx = topk_indices(p, TOP_K) # 这个 token 自己的 10 个编号 g = p[idx] / p[idx].sum() # 重新归一化成 10 个权重 y = s.shared(x) # 第 11 个,不参与选择 for j in range(TOP_K): y = y + g[j] * s.experts[idx[j]](x) return y # 还是 [..., 8192] def moe_params(N, k, d, d_e): one = 3 * d * d_e # 一个专家 total = N * one + one + d * N # 路由专家 + 共享 + 路由门 act = (k + 1) * one + d * N # k 个路由 + 共享 + 整个路由门 return total, act

难点一:上面那个 for 循环是「一个 token」的写法,真实实现不能这么写。一批里每个 token 选的 10 个专家各不相同,所以你没法把它写成一次整齐的矩阵乘法。真实框架的做法是按专家分组:先把整批 token 按它们选中的专家编号重新排序(散射),让每个专家一次性拿到分给它的全部 token 做一次大矩阵乘,算完再排回原位(聚集)。这段散射-聚集是 MoE 实现里最难写对、也最影响速度的一段——它同时也是「专家并行」(把不同专家放在不同卡上)的接缝所在。你在学习实现里可以先用 for 循环,但要知道它慢得离谱,而且慢的原因不是算得多,是每个专家只处理了一个 token,矩阵乘法完全没吃满

难点二:归一化那一步是个选择,不是定理。p[idx] / p[idx].sum() 是「先对 512 个 softmax,再取 top-10,再归一」;另一种常见实现是「先取 top-10 的原始分数,再对这 10 个 softmax」。两者数值不同,训练出来的权重不能互换。config 没写是哪一种,参数量也完全一样——所以这是一个不会被参数点钞发现的分歧点,只有真的加载官方权重跑起来对不上输出时才会暴露。

难点三:路由门那 4,194,304 个参数极易被漏掉。它只占一层的 0.016%,漏了它,92 层一共少 92 × 4,194,304 = 385,875,968 个参数——在 2.42 T 面前是 0.016%,你在「2.420 T」这个写法里根本看不出来。但第 12 章要求逐矩阵点钞对上官方数字,那时它就会现形。写参数统计函数时,凡是「小到可以忽略」的项,都要写进去再说。

自己验:三条,前两条是精确数,第三条是现象。
moe_params(512, 10, 8192, 2048) 应该正好返回 (25,824,329,728, 557,842,432),两者相除是 46.29。如果你的第二个数是 553,648,128,说明忘了把路由门算进激活(它是全额激活的,因为要给 512 个专家都打分)。
k 从 10 改成 512(全部激活):激活参数应该正好等于总参数 25,824,329,728,一个字节不差——此时 MoE 退化成一个中间维 513 × 2048 = 1,050,624 的巨型稠密 FFN。这一条是在验你的 act独立算出来的,而不是拿 total 乘了个比例;用比例凑的代码在这里会给出一个略微不等的数。
打开本章的 MoE 路由台,先在 router_aux_loss_coef = 0.001 下跑满 1000 步,记下「最忙专家的接单数 ÷ 平均接单数」这个读数——它应该稳定在 3 以内。然后把系数改成 0 重跑同样 1000 步:这个读数应该窜到 10 以上并且同时出现一批「一次都没被选中」的专家(此时最大 ÷ 最小 直接是无穷大)。两个现象必须同时出现才算复现了路由塌缩;如果只是最大值略高但没有 0,那只是正常的随机波动。另外注意:塌缩是训练时的现象,如果你的路由台把路由矩阵冻住不训练,无论系数设成多少都只会看到均匀分布——那说明你验的不是这件事。

本章小结

  • 条件计算把「有多少参数」和「每个 token 算多少」变成了两个可以分别拧的旋钮。Switch Transformer 的原话:参数量大得离谱,但计算成本恒定
  • 路由器朴素得惊人:一张 hidden × num_experts 的矩阵(2.4T 里是 8192 × 512 = 4,194,304 个参数)加一次 softmax,取 top-10。它只占一层 MoE 的 0.016%,却决定了这一层 258 亿参数里哪 5.58 亿会被唤醒。
  • Qwen3.8-2.4T 同时用了 DeepSeekMoE 的两大策略:细粒度切分(512 个专家,中间维只有 2048,扩张比 0.25,而 27B 的稠密 FFN 是 3.4)和共享专家隔离(第 11 个专家永远激活,用总量 0.195% 的参数接住所有 token 都要用的知识)。
  • 一层 MoE 的账:单个专家 3 × 8192 × 2048 = 50,331,648;512 个 = 25.77 B;加共享专家 50.33 M 和路由门 4.19 M,一层总 25,824,329,728(25.82 B)/ 激活 557,842,432(557.84 M),比值 46.29
  • 尺度感:27B 的一整层稠密 FFN 才 267.39 M,2.4T 的一层 MoE 就是 25.82 B(96.6 倍,是整个 27B 模型的 94.4%)——而 2.4T 有 92 层。但每 token 的计算量只差 2.09 倍
  • 路由会塌:没被选中的专家收不到梯度,形成「被选中得多 → 学得更好 → 被选中得更多」的正反馈。router_aux_loss_coef: 0.001 就是防它的。辅助损失的读数(Laux/α)本身是一个从 1 到 512 的免费不均衡度监控。
  • 27B 不用 MoE 不是因为它小:MoE 省算力、不省显存,而 27B 的定位是单卡本地跑,显存是挪不动的墙。同一个技术在数据中心是划算的交易,在消费卡上根本做不成。
  • 最要紧的一条:MoE 里「哪 11 个专家被选中」是运行时逐 token 变的,所以 512 个专家全部都得在显存里。这句话是下一章的全部基础。

下一章把这一层的账扩展成全模型的账:2,419,802,390,528 和 95,285,559,296 这两个数分别是怎么加出来的,以及为什么其中一个决定你要买几张卡,另一个决定你等多久出第一个 token。

第10章 总参数 vs 激活参数:2.4T 和 95B 各自决定什么

这一章很短,但它是全站被引用最多的一句话的出处。上一章证明了「参数总量」和「每个 token 用到的参数量」是两个可以分别拧的旋钮;这一章把这两个旋钮拧到 Qwen3.8-2.4T-A95B 的实际刻度上,逐项对账,然后回答一个非常具体的问题:这两个数各自决定了你的什么。

学完这一章你应该能做到

  • Qwen3.8-2.4T-A95B 这个名字里的每一段拆开解释,包括那个 A
  • 不看材料,把 2,419,802,390,528 这个数按五项加出来,并说清哪一项占了 98%
  • 说清为什么两张账单里有 43.96 B 是完全一样的,以及这为激活参数设了一个下限
  • 用一句话说清总参数和激活参数各自决定什么,并解释这句话背后的两套机制
  • 当场拆掉三个最常见的误解,每一个都能给出具体数字
  • 说清 27B 和 2.4T 除了大小之外,还在哪五处不一样,以及哪些字段是一模一样
前置:第9章的 MoE 一层账(总 25.82 B / 激活 557.84 M),第7章的 92 层布局(23 全注意力 + 69 线性),第2章的嵌入表与独立输出头,第1章那张四轴表(这一章会回指它两次)。

10.1 一个名字里的两个数

先把这个模型的全名拆开:

Qwen3.8 2.4T A95B - - 模型代号 总参数 2,419,802,390,528 决定你要买几张卡 A = Activated 激活参数 95,285,559,296 决定你等多久出第一个 token Qwen3.8-27B 名字里只有一个数 因为它是稠密的: 总参数 = 激活参数 27,355,639,808
图 10-1:模型全名的分段。示意图,非官方图示。A 是 Activated 的首字母;稠密模型的名字里之所以只有一个数,是因为它的两个数本来就相等。

总参数total parameters):这个模型一共有多少个可训练的数。它决定权重文件有多大、要多少显存才装得下。

激活参数activated parameters,也叫 active parameters):处理一个 token 时,实际参与了乘加运算的那部分参数有多少个。它决定每生成一个字要做多少次运算。

稠密模型里这两个数相等,所以从来没人分开说过。是 MoE 逼着整个行业发明了第二个数——从第 9 章那五个 config 字段出发,你已经知道它们是被两组互不相干的字段决定的:num_experts 管前者,num_experts_per_tok 管后者。

这套命名法在这一代模型里已经通用了。本站材料里的几个对照:Qwen3-Next-80B-A3B(80 B 总 / 3 B 激活)、Qwen3-30B-A3BQwen3-235B-A22B。看到名字里有 A,就知道它是 MoE,而且那个 A 后面的数才是你估算速度时该用的那个。

你在一个模型列表里看到三个名字:Qwen3-32BQwen3-30B-A3BQwen3-235B-A22B。不查任何资料,你能说出哪些是 MoE、哪些是稠密吗?以及,如果只关心「生成速度大概是什么量级」,这三个名字里你该看哪几个数?

先想:为什么有的名字里有两个数,有的只有一个?什么情况下第二个数是多余的?
关键在于:稠密模型的总参数和激活参数相等,所以写第二个数没有意义。名字里出现 A 开头的第二段,本身就是「这是 MoE」的标志。
第一步这样走:把三个名字按「有没有 A 段」分成两堆。再问:速度看的是总参数还是激活参数?
完整答案:Qwen3-32B稠密(只有一个数);Qwen3-30B-A3BQwen3-235B-A22BMoE(有 A 段,A = Activated)。
只关心生成速度的话,该看的是 32B、3B、22B——稠密模型看它唯一那个数,MoE 看 A 后面那个数。一个很反直觉的推论Qwen3-30B-A3B 的生成速度量级更接近一个 3 B 的小模型,比 Qwen3-32B 快得多,尽管它俩的总参数几乎一样大。而在显存上,它俩差不多——都是三十几 B 要装进去。
这道题的题眼是:模型名字是一份压缩得极狠的规格表,学会读它能省掉大量查资料的时间。看到 A,就知道要分两本账看。

变式:如果有一天你看到一个叫 SomeModel-100B-A100B 的名字,这说明什么?(答案:说明它虽然写成了 MoE 的格式,但激活参数等于总参数——要么它其实是稠密的(那第二段就是冗余的),要么它是一个「每个 token 激活全部专家」的退化 MoE,而后者在数学上几乎等价于一个更宽的稠密 FFN,见第 9 章那道 q9-5。无论哪种情况,这个名字都在告诉你:别指望它有 MoE 的速度优势。

10.2 逐项对账:这两个数是怎么加出来的

下面这张表是本站用 mat/paramcount.py 逐张矩阵数出来的,你可以拿计算器复算每一行。2.4T 有五种零件:嵌入表、输出头、23 层全注意力、69 层线性注意力(Gated DeltaNet)、92 层 MoE。

表 10-1:Qwen3.8-2.4T-A95B 的完整参数对账(数值来自本站 mat/paramcount.out.txt)
零件每个多少几个小计(总参数)小计(激活参数)
嵌入表 248,320 × 81922,034,237,44012,034,237,4402,034,237,440
输出头 248,320 × 81922,034,237,44012,034,237,4402,034,237,440
Gated Attention 层419,430,400239,646,899,2009,646,899,200
Gated DeltaNet 层438,386,6886930,248,681,47230,248,681,472
MoE 层总 25,824,329,728
激活 557,842,432
922,375,838,334,97651,321,503,744
合计2,419,802,390,528
= 2.420 T
95,285,559,296
= 95.29 B

把这张表写成两条式子,整章的结构就一目了然了:

N = N底座 + L · F ; N激活 = N底座 + L · F激活 ; 稀疏度 = N激活N
式 10-1
符号是什么Qwen3.8-2.4T 的值
N底座嵌入 + 输出头 + 全部注意力层 + 全部 DeltaNet 层43,964,055,552(两条式子里同一个数
L层数,也就是 MoE 出现了几次92
F一层 MoE 的全部参数(第 9 章算过)25,824,329,728
F激活一层 MoE 每 token 用到的参数557,842,432
稀疏度激活占总的比例3.94%,约 1/25.4

两条式子只有最后一项不同,前面那个 N底座 一模一样。每处理一个 token,这台机器里只有二十五分之一的参数在干活。

一句话记住:两张账单里,前四行一模一样,加起来是 43,964,055,552(约 43.96 B)。全部的稀疏性只发生在第五行——MoE 那一行从 2,375.84 B 塌到 51.32 B。MoE 占总参数的 98.18%,却只占激活参数的 53.9%。

这个观察有一个立刻可用的推论:激活参数有下限。那 43.96 B 是不管路由怎么稀疏都要全额付出的固定开销。就算把 num_experts_per_tok 一路砍到 1(只选 1 个路由专家 + 1 个共享专家),激活参数也只能降到 43,964,055,552 + 92 × (2 × 50,331,648 + 4,194,304) = 53,610,954,752,约 53.61 B——只比 95.29 B 少了 44%,远不是「砍到十分之一」。稀疏化能省的部分,本来就只有那 53.9%。

嵌入表凭什么算「激活」?一个 token 只查一行啊

你可能已经注意到一处不对劲:嵌入表是 248,320 行 × 8192 列,而处理一个 token 只需要取出一行(8192 个数)。严格按「参与了乘加运算的参数个数」来算,嵌入这一项应该只有 8192,不是 20 亿。

但表 10-1 把整张表都算进了激活参数。这不是本站偷懒,而是有一个很硬的理由:只有这么算,结果才正好落在官方名字给的 A95B 上。如果按「只查一行」算,激活参数会掉到 95,285,559,296 − 2,034,237,440 ≈ 93.25 B(只去掉嵌入表;输出头是真的要和全部 248,320 行做一遍矩阵乘,不能去);如果连输出头也按某种口径去掉,会掉到约 91.22 B——那这个模型就该叫 A93B 或者 A91B 了。本站由此反推:官方的口径是把嵌入表整个算进激活参数。

必须标明:这是本站的反推,官方从未公布过激活参数的计算方法(Qwen3.8 没有技术报告)。反推的证据只有一条——按这个口径算出来的 95,285,559,296 和名字里的 A95B 吻合。这条证据不弱,但它是间接的。

作为对照,同一套方法算 27B:

表 10-2:Qwen3.8-27B 的对账(稠密模型,两列必然相等)
零件每个多少几个小计
嵌入表 248,320 × 51201,271,398,40011,271,398,400
输出头 248,320 × 51201,271,398,40011,271,398,400
Gated Attention 层104,857,600161,677,721,600
Gated DeltaNet 层115,875,840485,562,040,320
稠密 FFN 层267,386,8806417,112,760,320
文本塔合计26,895,319,040(26.90 B)
视觉塔(第 11 章拆开)460,320,768(460.32 M)
全模型(总 = 激活)27,355,639,808 = 27.36 B

稠密模型的稀疏度是 100%——每个参数每次都干活。两张表并排看,MoE 到底改了什么就一目了然:它没有发明新零件,只是把第五行那一项从「一个 267 M 的 FFN」换成了「512 个 50 M 的专家挑 10 个用」。

自己数一遍:从 config 的六个字段到 2,419,802,390,528

  1. 先别急着算。第一个问题是:这台机器一共有几种不同的零件?(提示:第 7 章告诉你 92 层不是同一种层。)

    想好了再看

    五种:嵌入表、输出头、Gated Attention 层、Gated DeltaNet 层、MoE 层。前两种各一个,后三种要乘个数。为什么必须先问这个问题:参数点钞最常见的错误不是算错乘法,是漏掉一种零件或者把两种当成一种。先把零件清单列全,再动手算。

  2. 嵌入表多大?为什么输出头要单独再算一份,不能只算一次?

    想好了再看

    248,320 × 8192 = 2,034,237,440。cfg24t.json 里写着 tie_word_embeddings: false——第 2 章讲过,这个字段为 false 意味着输入端的嵌入表和输出端的那张表不共享权重,是两份独立的参数。所以要算两遍,一共 4,068,474,880。如果它是 true,这里就只算一份。这是一个只用一个布尔值就能改变 20 亿参数的字段。

  3. 92 层里,全注意力有几层?怎么从 config 算出来,而不是靠记忆?

    想好了再看

    num_hidden_layers: 92 ÷ full_attention_interval: 4 = 23 层全注意力,剩下 92 − 23 = 69 层是 Gated DeltaNet。第 7 章那个 3:1 布局在这里第一次派上真正的用场——注意 27B 是 64 ÷ 4 = 16,同一条式子,只是层数不同。

  4. 92 层里,有几层挂着 MoE?

    想好了再看

    92 层,一层不少。这是最容易出错的一步。「注意力层」和「MoE 层」不是两种互斥的层——每一层的结构都是「先做一次跨位置的信息搬运(注意力 DeltaNet),再做一次逐位置的加工(MoE)」。所以 MoE 的个数等于总层数 92,不是 23,也不是 69。这和 27B 那边「64 层全都有 FFN」是同一个道理(第 8 章)。如果你在这里写了 23 或 69,最终结果会差一个数量级。

  5. 把五项加起来。哪一项大得让其他四项几乎可以忽略?

    想好了再看

    2,034,237,440 + 2,034,237,440 + 23 × 419,430,400 + 69 × 438,386,688 + 92 × 25,824,329,728
    = 2,034,237,440 + 2,034,237,440 + 9,646,899,200 + 30,248,681,472 + 2,375,838,334,976
    = 2,419,802,390,528
    MoE 那一项占 98.18%。其余四项加起来 43,964,055,552,只占 1.82%——比一个 27B 模型还多一点点,但在 2.42 T 面前几乎看不见。

  6. 现在再做一遍,只把 MoE 那一项换成激活值 557,842,432。前四项要改吗?

    想好了再看

    前四项一个字都不改。嵌入、输出头、注意力、DeltaNet 都是稠密的,没有任何稀疏机制。
    2,034,237,440 + 2,034,237,440 + 9,646,899,200 + 30,248,681,472 + 92 × 557,842,432
    = 43,964,055,552 + 51,321,503,744 = 95,285,559,296
    这正是官方名字里的 A95B。

  7. 最后一问,也是这一节的题眼:两张账单里有一大块是完全相同的。它有多大?它意味着什么?

    想好了再看

    43,964,055,552,约 43.96 B。它意味着:激活参数有一个下限,而且这个下限高得惊人。无论你把路由稀疏化到什么程度——哪怕每 token 只选 1 个专家——激活参数也降不到 43.96 B 以下。实际上选 1 个时是 53.61 B。
    这个下限还解释了一件事:为什么 2.4T 的稀疏度是 3.94% 而不是更低。要想让稀疏度继续下降,只靠加专家(把分母做大)是有效的,但靠减激活数(把分子做小)很快就会撞上这堵 43.96 B 的墙。这是本章最值得记住的结构性事实,第 12 章的完整点钞会再用到它一次。

有人复算 2.4T 的总参数,算出来是 6.38 × 1011(约 0.638 T),大约只有正确值的四分之一。不看他的代码,你猜他最可能在哪一步出了错?

先想:0.638 T 大约是 2.420 T 的 1/4。这个模型里有什么数,被误当成另一个数时正好差 4 倍?
关键在于:MoE 占了 98.18%,所以总数差 4 倍,几乎肯定是 MoE 的层数算错了。92 层里,什么数除以 4 等于 23?
第一步这样走:用 23 × 25,824,329,728 加上另外四项,看结果是不是他那个数。
完整答案:他只给 23 层全注意力层配了 MoE,忘了另外 69 层 Gated DeltaNet 后面也各挂一个。
验算:23 × 25,824,329,728 = 593,959,583,744,加上 43,964,055,552 = 637,923,639,296——和他那个数一字不差
这个错误的根源是一个很自然的误解:以为「注意力层」和「MoE 层」是两种不同的层,交替出现。其实每一层都同时有两截——前半截是跨位置的搬运(注意力或 DeltaNet),后半截是逐位置的加工(MoE)。第 7 章那张层布局图画的是前半截怎么排,后半截 92 层全都一样。
顺带一提,这个错误在 27B 上会犯得更隐蔽:64 层里只给 16 层配 FFN,总参数会从 27.36 B 掉到 14.52 B,看起来像是「一个 15B 的模型」,量级上不那么离谱,反而更难被发现。

变式:如果他反过来,给 92 层都配了 MoE 但把注意力层和 DeltaNet 层各算了一遍(即 92 层注意力 + 92 层 DeltaNet),总参数会偏高多少?(答案:多算 69 × 419,430,400 + 23 × 438,386,688 = 28,940,697,600 + 10,082,893,824 = 39,023,591,424,约 39.02 B。总数变成约 2.459 T,只高了 1.6%——在 2.4 T 这个量级上,你几乎看不出来。这就是为什么参数点钞必须逐项对账,不能靠「结果看起来差不多」来验收。)

10.3 核心句:这两个数各自决定什么

一句话记住:2.4T 决定你要买几张卡,95B 决定你等多久出第一个 token。

这句话有两个独立的出处。HuggingFace 的 MoE 科普文(MoE Explained)在讲显存时写的是:全部参数都需要被加载进内存(all parameters need to be loaded in RAM);在讲速度时用的是另一套说法——它拿一个总参数四十多 B、激活十几 B 的 MoE 举例,说它的推理速度(FLOPs)就像在用一个十几 B 的模型(inference speed (FLOPs) is like using a 12B model)。同一个模型,同一段文字里,显存用总参数描述,速度用激活参数描述。

NVIDIA 那篇专门讲 Qwen3.8-2.4T 部署的博客把同一件事说得更直接:服务成本跟随的是激活参数,不是那全部 2.4T 参数(serving costs track active parameters, not the full 2.4T parameters)。

为什么会是这样:两套机制,恰好指向同一个结论

显存那一边的道理已经在第 9 章讲透了:哪 11 个专家被选中,是运行时逐 token 算出来的,下一个 token 可能换一批;而且批处理时一批 64 个 token 就能点亮 512 个专家里的约 71%。所以 512 个专家全部必须常驻显存。显存需求 = 总参数 × 每个参数几个字节,和激活参数没有任何关系。

速度那一边其实要分成两段,这一点值得说清楚,因为它是这句 slogan 唯一容易被挑刺的地方:

「出第一个 token」的时间(业界叫 prefill,预填充)主要受算力约束——要把你输入的整段提示词过一遍全部 92 层,做的乘加次数正比于激活参数。激活 95.29 B 就是激活 95.29 B,那 2.32 T 没被选中的参数一次乘法都没做。

而「后面每一个 token」的间隔(decode,解码)主要受显存带宽约束——每生成一个 token,要把这一次用得上的权重从显存读进计算单元一遍。读多少?还是激活参数那一份(11 个专家 × 92 层加上稠密部分),没被选中的专家不需要读。

所以这句 slogan 在两段上都成立,只是成立的机制不同:一段是「算得少」,一段是「读得少」。两段省的都是同一个数——激活参数。

把这个结论代成数字,就是本章最有说服力的一组对比:

表 10-3:同一个家族的两台机器,两本账分别差多少倍
看哪本账Qwen3.8-27BQwen3.8-2.4T-A95B倍数
总参数(决定显存)27,355,639,8082,419,802,390,52888.5 ×
激活参数(决定速度)27,355,639,80895,285,559,2963.48 ×
稀疏度100%3.94%

88.5 和 3.48——这两个数之间的距离,就是 MoE 全部的价值所在。你付的是 88.5 倍的显存,买到的是「每个 token 只贵 3.48 倍」的计算。

一位工程师说:「你们说速度看激活参数,我不服。生成阶段是显存带宽受限的,而权重全都在显存里,读的时候难道不是全部都要读一遍吗?那速度不就该看总参数?」请指出他的推理在哪一步断了,并说明如果他说的那件事真的成立,2.4T 的生成速度会退化到什么水平。

先想:「在显存里」和「被读进计算单元」是同一件事吗?一本书放在你桌上,和你把它从头翻到尾,是同一件事吗?
关键在于:生成一个 token 时,只有被选中的那 11 个专家的权重需要被搬进计算单元做乘加。没被选中的 501 个专家躺在显存里,一个字节都不用读。
第一步这样走:分别算「每 token 要读的字节数」——按激活参数算是 95,285,559,296 × 2,按总参数算是 2,419,802,390,528 × 2。两者比值是多少?
完整答案:他把「常驻显存」和「被读取」混为一谈了。这是两件独立的事:常驻显存是因为你不知道下一个 token 会选谁(这是 slogan 前半句的理由);而实际读取只发生在被选中的那部分(这是后半句的理由)。同一批参数,占着位置但不被翻动——这正是 MoE 的全部诀窍。
如果他说的成立(每 token 真要把全部权重读一遍),那么每生成一个 token 就要从显存搬 2,419,802,390,528 × 2 = 4.84 TB 的数据;而实际只需搬约 95,285,559,296 × 2 = 190.6 GB差 25.4 倍——也就是说,如果他是对的,2.4T 的生成速度会比现在慢 25 倍左右,MoE 这个结构的价值将完全归零,没人会去做它。
补一句让这个答案更完整的话:他的直觉在一种情况下是对的——如果批很大,一批里几乎所有专家都被点亮(第 9 章那个 64 个 token 点亮 71% 的估算),那么这一批确实要把大部分权重都读一遍。但注意这时读的是一批而不是一个 token,摊到每个 token 上还是划算的。MoE 在小批时省带宽,在大批时省的则是算力。两头都省,只是省的东西不同——这也是为什么这句 slogan 的后半句在两个阶段都成立。

变式:按这个思路,MoE 模型在「批大小 = 1」和「批大小 = 512」这两种极端下,哪一种更能体现它相对稠密模型的优势?(答案:批大小 = 1 时优势最大——只读 11 个专家,带宽占用是稠密同等总参数模型的 1/25。批大小很大时,专家几乎全被点亮,带宽优势基本消失,剩下的优势只有算力那一半(每个 token 仍然只做 11 个专家的乘加)。这解释了一个工程现象:MoE 对低并发、交互式场景特别友好,而在极高吞吐的批处理场景下,它相对稠密模型的优势会缩水。第 17 章讲部署时会再碰到这个权衡。)

10.4 落到硬件上:从一张消费卡到一个机柜

把总参数换算成字节。BF16 是每个参数 2 字节(第 0 章讲过):

2,419,802,390,528 × 2 字节 = 4,839,604,781,056 字节 = 4.84 TB(十进制)= 4.40 TiB
式 10-2
符号是什么直觉
× 2 字节BF16 每个参数占 16 位 = 2 字节这是「用多细的尺子记坐标」,第 ④ 轴的事
TB十进制,1 TB = 1012 字节模型文件体积按这个单位(和 HuggingFace 页面一致)
TiB二进制,1 TiB = 240 = 1,099,511,627,776 字节显存容量按这个单位;两者差 10%(在 TB/TiB 这一档)
为什么这里同时出现两种单位

本站的口径是:权重和模型文件体积用十进制 GB / TB(和 HuggingFace、Unsloth、Ollama 页面显示的一致),显存预算和 KV cache 用 GiB / TiB(显卡厂商标的 24GB 实际是 24 GiB,显存分配也按 2 的幂)。第 5 章已经提醒过一次:GB 和 GiB 在这一档差约 7.4%,TB 和 TiB 差约 10%。看到两个数字对不上的时候,先检查单位,八成是这个原因。

本站算出 4.40 TiB,vLLM 的 recipe 页写 4.45 TiB——差的那 0.05 TiB 是什么

两个数差 1.1%。本站有一个推断,而且它算得非常准:cfg24t.json 里有 mtp_num_hidden_layers: 1——这个模型自带一层多 token 预测Multi-Token Prediction, MTP)草稿头,本质上是额外的一整层。把这一层补进去(一层的注意力或 DeltaNet 约 4.2–4.4 亿参数,再加一层 MoE 的 25.82 B),总参数变成约 2,446 B,乘 2 字节除以 2404.449 TiB——四舍五入正好是 4.45。
本站算的是「模型主体」,vLLM 算的可能是「实际加载进显存的全部张量,含 MTP 头」。这是本站的推断,没有任何官方文件确认过这个差值的来源;本站只能说这个解释在数值上吻合得很好,且不需要引入任何其他假设。第 12 章会把 MTP 头这一项单独拿出来讨论。

然后是真实部署。vLLM 官方的 recipe 页给出了三档实测配置:

表 10-4:Qwen3.8-2.4T-A95B 的部署配置(vLLM recipe 页给出的数字)与 27B 的对照
模型 / 精度权重体积需要的卡
2.4T · BF16约 4.4 TiB24 张 B300
2.4T · FP8约 2.3 TiB16 张 B300
2.4T · NVFP4 W4A4约 1.3 TiB8 张 B300
27B · BF16约 54.7 GB = 50.96 GiB3 张 24 GiB 卡,或 1 张大显存的数据中心卡
27B · Q4_K_M + 视觉塔17.11 + 0.93 = 18.04 GB1 张 24 GiB 消费卡

把最后两行和前三行放在一起看:同一个家族、同一张 3:1 布局的图纸、同一个 248,320 的词表,一边是一个人在自己书桌上的一张显卡,另一边是 24 张数据中心加速卡。光权重体积就差 268 倍(4,839.6 GB 对 18.04 GB)。

「一张 24 GiB 卡跑得起 27B」这句话要带一个限定

表 10-4 最后一行只说了权重装得下,没说上下文能开多长。权重占掉 18.04 GB(= 16.80 GiB)之后,剩下的空间还要分给 KV cache、DeltaNet 的固定状态和框架开销。按本站在第 5、17 章的推导,24 GiB 单卡跑 Q4_K_M 的 27B,上下文实际到约 8–10 万 token(fp16 KV),开 fp8 KV 大约翻倍到 17–20 万原生的 262K 在 24 GiB 上跑不满。这是本站推导,不是官方数字,框架开销取的是经验区间,真实可用值会更靠近区间下沿。完整的账在第 17 章。

本站不写价格

把这个门槛差异换算成钱,是很多文章喜欢做的事。本站不做,因为本站的材料里没有任何可引用的硬件价格来源,而硬件价格随时间、地区、采购方式波动极大,写进来就是给读者埋雷。上面那两行数字(268 倍、24 张对 1 张)都是可以自己复算的事实,够用了。

如果有人做出一个 Qwen3.8-2.4T 的 4 bit 量化版本(每个参数半个字节),权重大约多大?它能装进 8 张卡吗?以及——量化之后,「等多久出第一个 token」会变快吗?

先想:量化改的是「每个参数占几个字节」,它改不改「有多少个参数」?改不改「每个 token 用到几个参数」?
关键在于第 1 章那张四轴表:量化在第 ④ 轴,它只动位宽。总参数、激活参数、层数、专家数,一个都不变。
第一步这样走:2,419,802,390,528 × 0.5 字节 = ? 换成 TiB 是多少?和表 10-4 里 NVFP4 那一行对一对。
完整答案:2,419,802,390,528 × 0.5 = 1,209,901,195,264 字节 = 1.21 TB = 1.10 TiB。表 10-4 里 NVFP4 W4A4 那一行写的是约 1.3 TiB / 8 张卡——本站算的是纯权重,实际部署还要算 KV cache、激活缓冲、MTP 头和框架开销,所以官方数字略高是正常的。能装进 8 张卡。
第二问的答案是:会变快,但原因和你可能以为的不一样。量化不改变激活参数(还是 95,285,559,296 个),所以要做的乘加次数一次都没少。变快的原因有两条:① 权重从显存读进计算单元的字节数减少了 4 倍,而解码阶段是带宽受限的,读得少就快;② 现代加速卡上低位宽的乘加本身吞吐更高(这一条依赖硬件是否原生支持该格式)。
但请注意这里的因果:量化让同样的计算跑得更快,它没有让计算变少。真正让计算变少的是 MoE 那个结构,而那件事发生在训练之前。这道题是全站两条主线的第一次正面相遇——第 15、16 章会把量化这一侧完整展开。

变式:反过来,如果把 num_experts_per_tok 从 10 改成 5,权重体积会变小吗?(答案:一个字节都不小。512 个专家全都还在文件里、全都还要进显存。变小的是每 token 的计算量和带宽读取量。这正是本章那句核心结论的反向验证:改激活数只动速度那本账,改位宽只动体积那本账,两者互不相干。)

10.5 常见误解清算:三个都要拆掉

误解一:2.4T 比 27B 大 88 倍,所以慢 88 倍

88.5 倍是总参数的比值,它决定的是显存那本账。速度看激活参数:95,285,559,296 ÷ 27,355,639,808 = 3.48 倍。每生成一个 token 要做的乘加次数,2.4T 只是 27B 的约 3.5 倍,不是 88 倍。

但这里必须补两句诚实的话,否则就成了另一种误导。第一,3.48 倍也不等于「端到端延迟正好慢 3.48 倍」——真实延迟还受显存带宽、卡间通信(24 张卡之间要互相传数据,这一项在 27B 的单卡上根本不存在)、批处理策略、以及专家并行下的负载倾斜影响。第二,本站没有这两个模型的实测吞吐对比数据,上面这个 3.48 只是参数账上的比值,是一个下界式的参考,不是性能预测。凡是把参数比值直接当成速度比值的说法(包括「慢 88 倍」和「只慢 3.5 倍」),都跳过了工程这一层。

误解二:激活 95B,所以只要装得下 95B 就行

这是本章最要命的一个误解,因为它会让人做出灾难性的采购决定:按 95 B × 2 字节 ≈ 190 GB 去配机器,实际要的是 4.40 TiB,差了 25 倍。

错在哪:哪 11 个专家被选中,是运行时才知道的,而且逐 token 变化。你没法提前把「要用的那 95 B」挑出来放进显存——下一个 token 就换一批了。第 9 章那道答辩题算过:只保留 11 个专家、其余从主机内存换入,每生成一个 token 要搬约 101 GB 的权重过 PCIe,把毫秒级的操作变成秒级。

而且这还是单 token视角。真实服务是成批处理的:一批 64 个 token 各选各的 10 个,在均匀路由假设下期望点亮 512 × [1 − (1 − 1/512)640] ≈ 365 个专家(71%)。批再大一点就基本全亮。所谓「只用 3.94%」这个性质,一进批处理就几乎消失了

误解三:MoE 是一种压缩

「2.4 T 的模型只激活 95 B,那不就是压缩了 25 倍吗?」——不是。这句话把第 1 章那张四轴表的第 ② 轴和第 ④ 轴混成了一件事。

MoE 是一种结构,写在训练开始之前的图纸上:它规定了这个模型有 512 个专家、每次选 10 个。这个决定一旦下了就改不了——你不能在已经训好的权重上「把它变成 MoE」,也不能把 MoE 变回稠密。它落在第 ② 轴,和层数、宽度一样,属于「训练之前」。

量化才是压缩,它落在第 ④ 轴:训练完之后,把每个参数从 16 位改成 4 位。参数个数一个不少,形状一个不变,只是记录得粗糙了些。

两者的区别有一个一句话的判据:压缩之后参数个数不变(2,419,802,390,528 还是 2,419,802,390,528),MoE 改变的正是这个数本身。再补一条时间轴上的判据:量化可以在你的笔记本上花几个小时做完,MoE 必须重新预训练一次。这是两件事,甚至不在同一条时间轴上。

下面这段话来自一篇科普文,里面藏着两处错误:「Qwen3.8-2.4T 采用了 MoE 稀疏化技术,把 2.4 万亿参数压缩到每次只用 95 B,因此它的显存需求和一个 95 B 的稠密模型相当,速度也差不多。这种技术可以在训练完成后应用到任何模型上。」请把两处错误各自指出来,并说明正确的表述。

先想:这段话里出现了几个动词?「压缩」「应用到已训练的模型上」——这两个动作分别对应四轴表的哪一轴?
关键在于:显存看总参数这一条被违反了一次;「训练之后才发生」这个性质被错误地安到了 MoE 头上一次。
第一步这样走:把「显存需求和一个 95 B 的稠密模型相当」这句代进数字——95 B 的稠密模型 BF16 是多少 GB?2.4T 是多少?
完整答案:
错误一:「显存需求和一个 95 B 的稠密模型相当」。一个 95 B 的稠密模型 BF16 权重约 190 GB;2.4T 是 4.40 TiB,差 25 倍。正确表述是:速度和一个 95 B 的稠密模型相当(这一条对),显存和一个 2.42 T 的稠密模型相当(因为 512 个专家全部要常驻)。这段话把「速度那一半」的结论错误地套到了显存上。
错误二:「可以在训练完成后应用到任何模型上」。MoE 是训练之前就定死的结构(四轴表第 ② 轴)。已经训好的稠密权重上没有 512 份专家权重可用,凭空变不出来;反过来也一样。能在训练完成后应用到任何模型上的是量化(第 ④ 轴)——这句话是把量化的性质安到了 MoE 头上。
还有一处措辞值得警惕但不算硬错:「压缩到每次只用 95 B」里的「压缩」二字。参数一个没少,只是每次不全用;准确的词是稀疏激活,不是压缩。这类措辞在传播中最容易演变成误解三。
这道题真正想训练的是一个习惯:读到任何关于模型大小的说法,先问它讲的是哪一轴、发生在训练之前还是之后。四轴表不是用来背的,是用来当筛子的。

变式:把那段话改成「因此它的推理算力需求和一个 95 B 的稠密模型相当,但显存需求和一个 2.4 T 的稠密模型相当,而且这个结构在训练前就定死了」。这样还有问题吗?(答案:事实上都对了。唯一还可以再苛刻一点的地方是「算力需求相当」——严格说是「每 token 的乘加次数相当」,端到端的延迟还受带宽、卡间通信和批处理影响,见误解一的第二段。但作为科普表述,改写后的版本已经站得住。)

「显存看总参数,速度看激活参数」这句话有没有失效的时候?请构造一个具体场景,在这个场景里显存需求由激活参数主导,并说明它为什么不推翻这句 slogan。

先想:显存里除了权重,还装着什么?第 5 章讲过一样东西,它和参数量完全无关。
关键在于:这句 slogan 讲的是权重那一部分显存。显存账单上还有别的项目,其中有的和总参数无关,有的甚至和参数量都无关。
第一步这样走:想一个「权重体积很小、但别的显存占用很大」的场景。比如一个专家数不多的小 MoE,配上极长的上下文和很高的并发。
完整答案:一个可构造的场景是小模型 + 超长上下文 + 高并发
举例:拿 27B(稠密,权重 Q4_K_M 后 17.11 GB)跑 262K 满上下文,KV cache 就是 16.00 GiB(第 5 章算过),已经接近权重本身;如果同时服务 4 个这样的长请求,KV 就是 64 GiB,权重那 17 GB 反而成了小头。此时决定你买几张卡的不再是参数量,是 KV cache,而 KV cache 的大小由「全注意力层数 × KV 头数 × head_dim × 上下文长度 × 并发数」决定,和总参数、激活参数没关系。
再加一项:MoE 模型在推理时还有中间激活缓冲,它正比于「批大小 × 激活的专家中间维」,这一项确实和激活参数相关而不是总参数。
为什么这不推翻 slogan:因为这句话讲的是权重那一项显存,而权重是显存账单上唯一一项「不管你怎么用都固定存在」的开销。KV cache 和激活缓冲是变动成本(随上下文和并发变),权重是固定成本(开机就在那儿)。这句 slogan 说的是固定成本那一栏。
更精确的完整表述应该是:显存 = 总参数决定的权重(固定)+ 上下文与并发决定的 KV cache(变动)+ 激活参数与批大小决定的中间缓冲(变动);速度 = 激活参数决定的计算与带宽读取。slogan 是这个完整式子的一句压缩,压缩掉的正是第 5 章那一整章和第 17 章那一整章的内容。知道一句口号压缩掉了什么,比记住这句口号更重要。

变式:对 2.4T 来说,262K 满上下文的 KV cache 是 23.00 GiB(第 5 章表)。它占 4.40 TiB 权重的百分之多少?这说明什么?(答案:23.00 ÷ 4505.9 ≈ 0.51%。说明在 2.4T 这个量级上,KV cache 几乎可以忽略,显存账单彻底由权重主导——slogan 在这里是极其准确的。而在 27B 上,262K 的 16 GiB KV 对 15.93 GiB 的 Q4_K_M 权重,两者几乎一样大同一句 slogan 的适用程度,随模型规模而变。

10.6 回到主线:这不是同一个模型的两个尺寸

全站的主线论断是:27B 和 2.4T 不是「同一个模型的大小档」,是两台各自独立预训练出来的不同机器。到这一章为止,你已经有足够的证据把这句话逐条兑现了。

表 10-5:两个模型逐字段对照(全部逐字取自 mat/cfg27b.json 与 mat/cfg24t.json)
字段Qwen3.8-27BQwen3.8-2.4T-A95B在哪一章讲过
num_hidden_layers6492第 7 章
hidden_size51208192第 2 章
FFN 形态稠密,intermediate_size 17408MoE,512 专家选 10 + 1第 8、9 章
num_attention_heads2464第 4 章
linear_num_value_heads48128第 6 章
模态有完整 vision_config(图文)没有 vision_config(纯文本)第 11 章
位置编码mrope_section: [11,11,10]没有 mrope 相关字段第 4、11 章
许可证Apache 2.0(开源)qwen3.8-max 自定义协议(开放权重)第 18 章
architecturesQwen3_5ForConditionalGenerationQwen3_5MoeForCausalLM第 1 章

然后是更有意思的那一半——两份 config 里一模一样的字段

表 10-6:两份 config 里逐字相同的字段(同样逐字取自本站的两份 config 副本)
字段共同的值它意味着什么
full_attention_interval4同一张 3:1 混合布局图纸
head_dim256注意力头的规格完全一样
num_key_value_heads4GQA 的 KV 头数一样——这正是两个模型 KV cache 都算得出来的原因
attn_output_gatetrue都用输出门
partial_rotary_factor0.25都只给 1/4 的维度加旋转
linear_num_key_heads / linear_key_head_dim / linear_value_head_dim16 / 128 / 128DeltaNet 的头规格一样
linear_conv_kernel_dim4连那个因果卷积核的长度都一样
max_position_embeddings262144原生上下文一样
vocab_size248320同一个分词器,同一张词表
tie_word_embeddingsfalse都用独立输出头
mtp_num_hidden_layers1都自带一层 MTP 草稿头
一句话记住:相同的是零件的规格——头有多大、KV 头几个、旋转加在几分之一的维度上、每 4 层来一次全注意力、词表多少个字。不同的是用了多少个零件、排成多少层、每层多宽、FFN 那个槽位里装的是什么。这就是「同一张图纸、不同的重复次数」这句话的准确含义。
表 10-6 有一个必须说明的限制

本站手上的 mat/cfg27b.jsonmat/cfg24t.json 都是人工节选的 config,不是完整文件。所以表 10-6 能证明的是「这些字段在两边都出现且值相同」,不能证明「两份完整 config 里没有别的差异」。反过来,表 10-5 里那些「27B 有、2.4T 没有」的字段,需要分两类看:vision_config 和 mRoPE 这两项,本站有独立证据(HuggingFace 上 27B 的 pipeline 标的是 image-text-to-text、2.4T 是 text-generation),可以确认是真实差异;而像 rope_theta 这类只在 27B 那份摘录里出现的字段,本站不能断言 2.4T 没有——很可能只是节选时没抄进来。第 4 章已经就 rope_theta 做过同样的声明。

最后说清一件事,它是这一章接回第 1 章的地方:上面所有的不同,全部发生在训练开始之前。层数、宽度、专家数、有没有视觉塔——这些都是写在图纸上的超参数,一旦开训就改不了。而这两个模型是各自独立从随机初始化开始训练的,谁也不是谁的副本,谁也不是谁压缩、剪枝或蒸馏出来的。第 14 章会把「独立训练 / 剪枝 / 蒸馏 / 量化」这四条路摆在同一条时间轴上,正面回答全站开头那个问题。

有人向你推销一个方案:「我们拿到了 Qwen3.8-2.4T 的权重,打算把它成一个 27B 的版本给客户在本地跑——具体做法是每层只保留使用率最高的 5 个专家,再做 4 bit 量化。这样既有 2.4T 的知识,又只要一张消费卡。」
请用这一章和前面几章学到的东西,指出这个方案里四处各自独立的问题,并说明其中哪一处是致命的(做不成),哪几处只是「代价没算清」。

先想:这个方案同时动了四轴表里的哪几条轴?每一条轴分别发生在训练之前还是之后?
关键在于:「每层只保留 5 个专家」这个动作,改的是 num_experts,它在第 ② 轴。而这一章 10.2 节算过一个东西,它证明了「减专家」几乎不省算力、但确实省显存——那这个方案想要的到底是哪一个?
第一步这样走:先算账。每层保留 5 个专家,总参数会掉到多少?(用 5 × 50,331,648 + 50,331,648 + 8192×5,乘 92,再加 base 那 43.96 B。)算完就知道它离 27B 还有多远。
完整答案:
问题一(算术上就不成立):每层保留 5 个路由专家,每层 MoE 变成 5 × 50,331,648 + 50,331,648 + 8192×5 = 302,030,848;92 层是 27,786,838,016,加上 base 那 43,964,055,552,总参数是 71,750,893,568,约 71.75 B,不是 27 B。而且注意 base 那 43.96 B 是砍不掉的——光是非 MoE 那部分就已经超过 27 B 了。要真做成 27 B,必须连层数和宽度一起改,那就不是「裁专家」了。
问题二(致命的那一处)「保留使用率最高的 5 个专家」等于扔掉这个模型学到的绝大部分知识。被扔掉的 507 个专家不是冗余备份,它们各自承载着不同 token 上用得到的东西(见第 9 章那道答辩:稀疏的是单个 token 的这一次前向,不是模型的一生)。而且路由矩阵是对着 512 个专家训出来的,删掉 507 个之后它的分数分布完全失效,剩下 5 个专家收到的输入分布和它们训练时见过的完全不同。这不是「能力下降」,是模型直接坏掉——而要修好它,你得重新训练,那就回到了「独立训练一个新模型」,方案的前提(拿现成权重)也就没了。这一处是致命的,第 14 章会把这类「真的在权重上动刀」的做法(剪枝)和它必须付出的再训练代价完整讲清。
问题三(想要的东西和做的事对不上):这个方案的目标是「在一张消费卡上跑」,也就是要省显存。删专家确实省显存(这一点它做对了),但 10.2 节算过:删专家几乎不省算力。而且这里还有一个更尴尬的后果——只剩 5 个专家的话,每 token 只能全选,于是激活参数等于总参数,都是 71,750,893,568,稀疏度回到 100%。也就是说改造完之后它是一个 71.75 B 的稠密模型:显存比 27B 大 2.6 倍,速度比 27B 慢 2.6 倍,MoE 的好处一点没剩下。所以就算它奇迹般地能跑,速度也不会是 27B 的水平。它把「显存小」等同于「跑得像小模型」,这正是本章误解一和误解二的组合体。
问题四(许可证):Qwen3.8-2.4T-A95B 用的是 qwen3.8-max 自定义协议,不是 Apache 2.0。把它改造后交付给客户商用,需要先读清楚协议里关于收入规模和显著展示模型名称的条款。这一处不是技术问题,但它是唯一一个可能让整件事在法务上停摆的问题(第 18 章展开)。

哪一处致命问题二。问题一是算错了目标值(改改数字还能谈),问题三是代价没算清(可以重新评估),问题四是合规问题(可以去谈授权),只有问题二是技术上做不成——你无法从一个训好的 MoE 里「裁」出一个能用的小模型而不重新训练。
这道题的题眼是:四轴表不只是一张知识表,它是一个决策清单。任何声称要「把大模型变小」的方案,都要先问它动的是哪一轴:动第 ④ 轴(量化)今天就能做;动第 ①②③ 轴,一律意味着重新训练。

变式:把方案改成「拿 2.4T 当教师,重新训练一个 27B 规模的学生模型」,上面四处问题还剩几处?(答案:技术上的三处基本都解决了——那是蒸馏,学生是另一套独立参数,不存在「裁掉专家导致模型坏掉」的问题,参数量和速度也可以自由设计。但它的代价换成了「需要一次完整的训练」,而不是「拿现成权重改一改」。问题四(许可证)依然在,而且可能更复杂——用一个模型的输出去训练另一个模型,属于协议里需要单独确认的场景。第 14 章会把蒸馏、剪枝、独立训练三条路的代价并排摆出来。)

我能不看材料说清:2.4T 的总参数和激活参数各是怎么加出来的,两张账单里哪一部分完全相同、有多大,以及这为激活参数设了什么下限。

答辩:如果我是审稿人(一)

A95B 这个数到底是官方给的,还是你们算出来凑上去的?如果官方的口径把嵌入表按「只查一行」算,这个数就该小 4 B,你们这套对账不就是在事后凑答案吗?

参考防守(先自己组织语言再看)

这个质疑是正当的,本站也确实无法完全消除它。先把能确认和不能确认的分清楚。

能确认的:模型的官方名字里有 A95B;本站按「嵌入表与输出头全额计入」的口径逐张矩阵加出来是 95,285,559,296,即 95.29 B。这两个数吻合。

不能确认的:官方从未公布过激活参数的计算方法。Qwen3.8 没有技术报告,模型卡也没有列出逐张量清单。

所以本站的逻辑是反推,不是证明。反推的强度可以这样评估:按另外两种可能的口径算,结果分别是约 93.25 B(去掉嵌入表,保留输出头)和约 91.22 B(两者都去掉)。这三个候选值分别对应 A95B、A93B、A91B 三个名字,官方选了第一个。一个模型的名字通常不会取错自己的核心指标,所以本站认为「全额计入」这个口径的可能性最高。

但审稿人的质疑指出了一个真实的隐患:如果哪天官方公布的口径和本站不同,表 10-1 的激活参数那一列就要改。本站愿意把这一条明确列为存疑项,而不是含糊过去。需要强调的是,这个不确定性只影响激活参数那一列(±4%),完全不影响总参数那一列——2,419,802,390,528 这个数是把每张矩阵的行乘列加起来得到的,不依赖任何口径约定。而本章的核心结论(显存看总参数、速度看激活参数、稀疏度约 4%)在 91 B 到 95 B 的任何一个取值下都成立。

答辩:如果我是审稿人(二)

你说「显存看总参数」,可 vLLM 明明支持专家并行,把 512 个专家分散到不同卡上,每张卡只装一小部分。那每张卡不就只要装一部分参数吗?你这句话是不是把「单卡」和「整个集群」混为一谈了?

参考防守(先自己组织语言再看)

这个技术细节是对的,但它不改变结论,只是让结论的表述需要更精确一点。

先承认对的部分:专家并行确实把 512 个专家切给多张卡,每张卡上只有其中一部分(比如 24 张卡的话,平均每张 21 个专家)。所以「单张卡上的权重占用」确实远小于 4.40 TiB。

但这不省任何东西,它只是把同一笔账拆开记。512 个专家一个都不能少,总量还是 4.40 TiB,只是从「一张 4.40 TiB 的卡」变成了「24 张各装约 0.18 TiB 的卡」。表 10-4 里那个「24 张 B300」正是这么来的——它本身就是专家并行之后的数字。所以更精确的表述应该是:总参数决定的是「整个部署单元加起来要有多少显存」,不是「一张卡要有多少显存」。

而且专家并行还带来一笔新账,值得写进来:卡间通信。一个 token 选中的 10 个专家可能分散在好几张卡上,每一层都要把这个 token 的向量发过去、把结果收回来,92 层就是 92 轮。这一项在单卡的 27B 上完全不存在。所以「2.4T 每 token 只比 27B 贵 3.48 倍」这个说法在纯参数账上成立,但一旦落到真实集群,通信开销是一项 27B 完全没有的额外成本——这也是误解一那段里本站坚持「3.48 不是延迟比值」的原因之一。

顺带说一句:这一段的存在,恰恰说明了为什么本站在表 10-4 里直接引 vLLM 给的卡数,而不是自己拿 4.40 TiB 去除以某个单卡容量。真实部署的卡数从来不是「权重体积 ÷ 单卡显存」这么算出来的,它还要留 KV cache、激活缓冲、通信缓冲和碎片的余量。

答辩:如果我是审稿人(三)

稀疏度 3.94% 听起来像是「96% 的参数在闲置」。你花钱买了 4.4 TiB 的显存,任何时刻只有 4% 在干活——这难道不是一种巨大的浪费?为什么不直接训一个 95 B 的稠密模型?

参考防守(先自己组织语言再看)

「浪费」这个词用得不准确,但它指向的那个成本是真实的。分三层说。

第一层,那 96% 不是闲置,是在别的 token 上被用。稀疏的是「单个 token 的这一次前向」,不是「模型的一生」。跑完一整段文本、跑完一整个数据集,512 个专家几乎都会被大量使用(这正是第 9 章那条辅助损失要保证的事)。用一个比喻:一本词典里 99.99% 的词条在你查某一个词时都没被用到,但你不会说这本词典浪费了 99.99%。

第二层,成本确实是真实的,而且本站不打算淡化它。显存的占用率是 100%,每 token 的利用率是 3.94%。这两个数并存,就是 MoE 这笔交易的完整价目表:你用显存的低利用率,换算力的高效率。如果显存是你的瓶颈(比如单卡本地部署),这笔交易就是纯亏——这正是第 9 章最后那一节解释 27B 为什么不用 MoE 的全部理由。

第三层,「为什么不直接训一个 95 B 的稠密模型」这个问题问得好,但本站给不出有证据的答案。能说的是:MoE 这一路之所以被广泛采用,公开的论证是「同样的推理成本下能装更多知识」(第 9 章引的 Switch Transformer 那句「参数量大得离谱,但计算成本恒定」)。但「2.42 T 的 MoE 是不是真的比 95 B 的稠密更强」这个问题,需要在同一套数据、同一套训练配方下做对照实验才能回答,而 Qwen3.8 没有技术报告,没有任何这类对照数据。本站不会因为「大家都这么做」就断言它一定更好。这个问题本站已经把它写进了本章的研究课题。

真未解稀疏度能推到多低?这条曲线有没有底?

把本站材料里能查到的几个 MoE 模型排一排,会看到一条很明显的趋势线:

Mixtral(arXiv:2401.04088)总参 47 B / 激活 13 B,稀疏度约 28%;DeepSeek-V3(arXiv:2412.19437)671 B / 37 B,约 5.5%;Qwen3-Next-80B-A3B 是 80 B / 3 B,约 3.75%;Qwen3.8-2.4T-A95B 是 2,419.8 B / 95.29 B,约 3.94%。三年之内稀疏度掉了将近一个数量级。

那么问题是:这条曲线会一直往下走吗?还是存在一个下界?本站把它标成「真未解」,判据有两条。第一,各家给出的数值分散在 3.75% 到 28% 之间,如果存在公认的最优点,不会这样分散。第二,本章 10.2 节已经算出了一个结构性的下界:非 MoE 部分那 43.96 B 是稀疏化碰不到的,所以在 2.4T 这个架构上,稀疏度的理论下限是 43,964,055,552 ÷ 2,419,802,390,528 = 1.82%——就算每 token 一个路由专家都不选,也不可能更低。这说明下界确实存在,但它由架构决定,不是一条普适规律;而在这个下界之前的哪一点上模型开始变差,没有公开答案。

先做这一步:打开本章的稀疏度台,固定 num_experts = 512,把 num_experts_per_tok 从 1 一路拉到 512,把每一步的三个数记下来——总参数(应该纹丝不动)、激活参数、稀疏度。你会得到一条从 2.21% 到 100% 的曲线。然后做一件更有意思的事:把 x 轴换成「非 MoE 部分占激活参数的比例」,你会看到在 k 很小的那一端,这个比例迅速逼近 100%——也就是说,模型的算力全花在了注意力和 DeltaNet 上,MoE 那部分几乎不干活了。这一端是不是就是「稀疏过头」的物理含义?带着这个问题,去查一查有没有论文报告过「专家数固定、激活数从多到少」的完整消融曲线(搜索词:MoE top-k ablationexpert sparsity scaling),特别注意它们的评测里有没有区分「知识型任务」和「推理型任务」——这两类任务对稀疏度的敏感程度很可能不同,而这正是公开材料里最缺的一块。

这一层要加什么:给模型装一个参数计价器

为什么现在才加它:前九层你已经把所有零件都造齐了——嵌入表、输出头、注意力、DeltaNet、稠密 FFN、MoE。这一层不加任何新零件,它加的是一把尺子:一个函数,喂进任意一份 config,同时报出总参数和激活参数。造完这把尺子,你就能亲手验证官方标在名字里的那两个数,而不是相信别人抄来的数字。第 12 章那场「逐矩阵点钞」的高潮,用的就是这把尺子。

def count(cfg): d, V, L = cfg.hidden_size, cfg.vocab_size, cfg.num_hidden_layers n_full = L // cfg.full_attention_interval # 64//4=16, 92//4=23 n_lin = L - n_full # 48, 69 emb = V * d head = 0 if cfg.tie_word_embeddings else V * d att = attn_params(cfg) # 第4层写好的 dn = delta_params(cfg) # 第6层写好的 if cfg.num_experts: # MoE 分支 one = 3 * d * cfg.moe_intermediate_size ffn_tot = cfg.num_experts * one + one + d * cfg.num_experts ffn_act = (cfg.num_experts_per_tok + 1) * one + d * cfg.num_experts else: # 稠密分支 ffn_tot = ffn_act = 3 * d * cfg.intermediate_size base = emb + head + n_full * att + n_lin * dn # 两条账单共用的那一块 return base + L * ffn_tot, base + L * ffn_act # 总, 激活 total, act = count(cfg) print(total, act, act / total, total / act)

难点一,也是这一层唯一真正的难点两个返回值必须走两条独立的路,绝不能用「总参数 × 某个比例」去凑激活参数。写成 act = total * (k+1) / num_experts 看起来又短又聪明,而且在 2.4T 上算出来是 51.98 B 量级——错得不多,一眼看不出来。但它错在三处:① 忽略了 base 那 43.96 B 在两条账上是全额相同的,不该被乘以比例;② 忽略了路由门在两条账上都是全额;③ 忽略了共享专家在分子分母上的地位不同。这三处错误在稠密模型上会全部现形(比例变成 1,反而歪打正着),在 MoE 上则会给出一个「差不多但不对」的数——这种错误最难被发现,因为它不会崩、也不会离谱。

难点二:稠密分支必须让两个返回值指向同一个数,不是「近似相等」。如果你的稠密分支也偷偷走了一遍激活公式并且算出个略有不同的数,那说明你在某处用了比例。判据很简单:total == act 必须是精确的相等判断,不是 abs(total - act) < 1e6

难点三:base 那一项容易被写进循环里重复算。嵌入和输出头是整个模型一份,不是每层一份。这个错误在 27B 上会让总参数从 27.36 B 涨到 108.7 B(64 × 2.54 B 的差),量级上立刻暴露;但如果你只是把它写进了「激活」那一条而没写进「总」那一条,两个数就会诡异地一大一小,很难定位。

自己验:三条,前两条是精确数,第三条是这一层的题眼。
cfg27btext_confignum_experts 为空走稠密分支):两个返回值必须精确相等,都是 26,895,319,040(文本塔;视觉塔第 11 层再补 460,320,768,全模型 27,355,639,808)。差一个字节都说明稠密分支有问题。
cfg24t:应该正好得到总 2,419,802,390,528 / 激活 95,285,559,296,比值 25.395(约 25.4),稀疏度 3.94%
③ 这一条能证明你真的把两个数算成了两件事:把 num_experts 从 512 改成 64num_experts_per_tok 保持 10 不变。总参数应该暴跌344,995,545,088(约 0.345 T,只剩原来的 14.3%);而激活参数应该几乎纹丝不动——从 95,285,559,296 变成 94,947,917,824,只掉了 337,641,472(0.35%)。
再往下追一层,那 337,641,472 的来历必须能对上:它全部来自路由门变小(每层从 8192×512 = 4,194,304 缩到 8192×64 = 524,288,少 3,670,016,乘 92 层正好是 337,641,472)。而 11 个专家那部分 92 × 11 × 50,331,648 = 50,935,627,776一个字节都没变
如果你看到激活参数跟着总参数一起大跌(比如掉到 12 B 左右),说明你在用比例凑数——回去看难点一。

本章小结

  • 名字里的两个数Qwen3.8-2.4T-A95B 的 2.4T 是总参数(2,419,802,390,528),A95B 的 A 是 Activated,95.29 B 是激活参数(95,285,559,296)。稠密模型名字里只有一个数,因为它的两个数本来就相等。
  • 五项加法:嵌入 2,034,237,440 + 输出头 2,034,237,440 + 23 × 419,430,400 + 69 × 438,386,688 + 92 × 25,824,329,728 = 2,419,802,390,528。把最后一项换成 92 × 557,842,432,就是 95,285,559,296。稀疏度 3.94%,约 1/25.4
  • 两张账单里 43,964,055,552 是完全相同的(占总参数 1.82%、占激活参数 46.1%)。全部稀疏性只发生在 MoE 那一行。推论:激活参数有下限,就算每 token 只选 1 个专家也还有 53.61 B。
  • 核心句:2.4T 决定你要买几张卡,95B 决定你等多久出第一个 token。前半句因为专家全部要常驻显存(谁被选中是运行时才知道的);后半句在预填充阶段是「算得少」,在解码阶段是「读得少」,两段省的都是激活参数那一份。
  • 硬件:BF16 权重 4.84 TB = 4.40 TiB。vLLM recipe 给的是 BF16 24 张 B300 / FP8 16 张 / NVFP4 W4A4 8 张;27B 的 Q4_K_M 加视觉塔是 18.04 GB,一张 24 GiB 消费卡。权重体积差 268 倍
  • 三个误解:① 大 88 倍不等于慢 88 倍(激活比只有 3.48,且这也不是延迟比);② 激活 95 B 不等于只要 95 B 显存(一批 64 个 token 就点亮 71% 的专家);③ MoE 不是压缩,是结构——它在第 ② 轴、训练之前;量化才在第 ④ 轴、训练之后。
  • 两台机器:层数 64 对 92、宽度 5120 对 8192、FFN 稠密对 MoE、有无视觉塔、许可证 Apache 2.0 对自定义协议。而相同的是零件规格——head_dim 256、KV 头 4 个、部分 RoPE 0.25、每 4 层一次全注意力、词表 248,320、独立输出头、1 层 MTP 头。同一张图纸,不同的重复次数和不同的宽度。

下一章去拆 27B 独有的那个零件:那个只占全模型 1.7% 参数、却让它能看懂图片的视觉塔。它也是表 10-5 里最后一处还没展开的差异。

第11章 视觉塔:27B 为什么是「原生多模态」

第 10 章那张对照表里还剩最后一处没展开的差异:27B 的 config 里有一整块 vision_config,2.4T 没有。这一章拆开那块 config,回答三个问题:一张图片是怎么变成 token 的、这个能力花了多少参数(答案会让你意外),以及为什么在本地部署时它是最容易被漏算的那 0.93 GB。

学完这一章你应该能做到

  • 说清「原生多模态」和「外挂一个识图模型」在结构上的区别
  • 给一张任意尺寸的图片,算出它会变成多少个 token,并说清哪些尺寸算不了
  • 手算视觉塔的参数量,并说出它占全模型的百分之几
  • 解释 out_hidden_size: 5120 这个字段为什么必须存在
  • 在算 24 GiB 显存预算时,不再漏掉那 0.93 GB
  • mrope_section: [11,11,10] 和「图片是二维的」这件事连起来
前置:第2章的嵌入(token 怎么变成向量)、第3章的注意力代价随序列长度增长、第4章 4.5 节的 mRoPE、第5章的 24 GiB 显存预算、第8章的一层 FFN 是 267.39 M(这一章会拿它当尺子)。

11.1 「原生多模态」和「外挂一个识图模型」的区别

先看证据。在 HuggingFace 上,这两个模型的 pipeline 标签是不一样的:

表 11-1:两个模型在「能不能看图」这件事上的差异(来自 HuggingFace 仓库页与本站的 config 副本)
Qwen3.8-27BQwen3.8-2.4T-A95B
HuggingFace pipeline 标签image-text-to-texttext-generation
architecturesQwen3_5ForConditionalGenerationQwen3_5MoeForCausalLM
config 结构顶层分成 text_configvision_config 两块只有一层扁平字段
vision_config有,完整的 10 个字段没有
image_token_id248056没有
language_model_onlyfalse没有这一项

这是第 10 章表 10-5 里列的第四处真实差异(前三处是层数、宽度、FFN 形态)。而且它和前三处不同:前三处是「同一个东西的不同规格」,这一处是有和没有

不做原生多模态会怎样:另一条路是什么样的

让一个语言模型「能看图」,历史上有一条更省事的路:外挂。先用一个独立的图像模型(比如一个看图说话模型或者一个 OCR)把图片变成一段文字描述,再把这段文字接在用户的问题前面,喂给语言模型。这条路的好处是两边完全解耦,语言模型一行代码都不用改。

代价是:语言模型从头到尾没有看见过那张图,它看见的是别人写的一份说明书。凡是没被写进说明书的信息,就永远丢了。你可以问「图里有什么」,因为那正是说明书要写的东西;但你没法问「左下角那串小字和表格第三行对得上吗」——写说明书的那个模型不知道你会问这个,它不会把左下角那串小字抄下来。这个信息瓶颈是结构性的,不是把描述写长一点就能解决的。

原生多模态natively multimodal)走的是另一条路:把图片切成小块,每一块直接变成序列里的一个 token,和文字 token 排在同一条序列里,一起进那 64 层。模型的注意力层是直接面对图块本身的,中间没有任何一层「文字描述」把信息压扁。

必须说明:上面这段对比是本站为了讲清结构差异给出的解释,不是 Qwen3.8 官方的论证——这个模型没有技术报告,官方从未解释过为什么这样设计。能确认的只有 config 里的结构事实。

你拿到一份陌生模型的 config.json,想判断它是不是原生多模态。你会去看哪几个字段?如果它只有一层扁平字段、没有嵌套的子结构,能不能立刻下结论?

先想:视觉塔是一整套独立的层(27 层、hidden 1152),它的超参数总得写在什么地方。
关键在于:一个原生多模态模型里其实住着两套不同规格的层(视觉那套 hidden 1152,文本那套 hidden 5120),它们的超参数无法共用同一组字段名,所以 config 必须分块。
第一步这样走:搜 visionimagepatch 这三个词。再看顶层结构是分块的还是扁平的。
完整答案:三处线索,按可靠性排序。vision_config(或同类名字的子结构)——有它基本就实锤了,因为视觉塔的 hidden_size 和文本塔的不可能共用一个字段名。image_token_id 这类特殊 token 编号——它说明词表里预留了「这里有一张图」的占位符。architectures 的名字——27B 是 Qwen3_5ForConditionalGeneration,2.4T 是 Qwen3_5MoeForCausalLM,前者的命名暗示它接受某种「条件」输入。
如果它只有一层扁平字段、完全没有嵌套:基本可以判断是纯文本模型,但不要说「一定」。原因是本站在第 4、10 章反复强调过的那件事——你手上的 config 可能是节选。正确的做法是补一条独立证据,比如去看仓库的 pipeline 标签(image-text-to-text vs text-generation),或者看仓库里有没有 preprocessor_config.json 这类图像预处理配置。两条独立证据指向同一个结论,才算判断成立。

变式:如果一个 config 里有 vision_config,但 language_model_only 写着 true,说明什么?(答案:说明这份权重虽然带着视觉塔的结构定义,但被配置成只跑语言部分——视觉那条支路被关掉了。27B 的这个字段是 false,也就是视觉支路是开着的。这个字段的存在本身就说明了一件事:视觉塔在结构上是可以被整个旁路掉的一条支路,而不是和文本塔缠在一起的东西——这正是下面 11.5 节讲的 mmproj 能被单独打包的原因。)

11.2 一张图片怎么变成 token

核心动作只有一个字:patch_size: 16 的意思是把图片切成 16 × 16 像素的小方块,每一块叫一个图块

图块patch):图像被切成的最小单位。它在视觉塔里的地位,完全对应文字在文本塔里的 token——每个图块过一个线性层变成一个向量,然后就和 token 一样进注意力层了。

那一个图块进线性层之前是多少个数?直觉上是 16 × 16 × 3(RGB 三个通道)= 768。但 config 里还有一个 temporal_patch_size: 2,它说的是时间方向上也要切——两帧合成一个图块。所以那张矩阵的输入维度是:

3(RGB) × 2(时间) × 16 × 16(空间) = 1536  ⟶  1152 维
式 11-1
符号是什么直觉
3in_channels,红绿蓝三个通道每个像素其实是三个数
2temporal_patch_size,时间方向上两帧一组为视频准备的;静态图片会把同一帧凑成两份
16 × 16patch_size,一个图块的边长一小方块像素
1536 → 1152图块嵌入矩阵,1536 × 1152 = 1,769,472 个参数和第 2 章那张 248320 × 5120 的嵌入表是同一个角色:把原始输入变成向量
静态图片那个「时间维」是本站的推断

temporal_patch_size: 2 这个字段确实在 cfg27b.jsonvision_config 里,值也确实是 2。但 config 没有说明静态图片(只有一帧)怎么填满这个为 2 准备的时间维。本站按这一类模型的常见做法推断是把同一帧复制成两份,这样 3 × 2 × 16 × 16 = 1536 这个输入维度才对得上。这是推断,不是官方说明——不过它有一个间接支持:只有按 1536 这个输入维度算,视觉塔的参数总数才正好落在 460,320,768 上(11.3 节会验这一条)。

切完之后,图块数量就是「宽高各除以 16 再相乘」。但还没完——config 里还有 spatial_merge_size: 2相邻的 2 × 2 个图块,最后会被合并成一个 token,token 数因此降到四分之一。

token 数 = (H/16) × (W/16)2 × 2 = H × W1024
式 11-2
符号是什么直觉
H, W图片的高和宽,单位是像素必须都是 32 的倍数,理由见下面
/16除以 patch_size,得到每个方向上的图块数
/(2×2)除以 spatial_merge_size 的平方合并,四合一
1024162 × 22,一个 token 最终代表 32 × 32 个像素一个视觉 token ≈ 一个 32 × 32 的小方块

代进去算一遍,这是本章最该被记住的一组数:

表 11-2:图片尺寸与 token 数(按式 11-2 算,边长须为 32 的倍数)
图片尺寸图块数(÷16 后相乘)合并后的视觉 token 数相当于多少字
256 × 25616 × 16 = 25664一段话
512 × 51232 × 32 = 1,024256一小节
768 × 76848 × 48 = 2,304576一页纸
1024 × 102464 × 64 = 4,0961,024约一千个字
1920 × 1088120 × 68 = 8,1602,040四五页纸
一句话记住:一张 1024 × 1024 的图片 = 4096 个图块 = 合并后 1024 个 token一张图约等于一千个字。反过来说,27B 的 262,144 上下文塞满图片的话,大约能放 256 张这样的图。
为什么要合并:不合并会怎样

如果不做 spatial_merge,一张 1024 × 1024 的图就是 4096 个 token,而不是 1024 个。多出来的 3072 个 token 要为此付两笔钱:一笔是 KV cache(第 5 章:27B 每 token 64 KiB,多 3072 个 token 就是多 192 MiB),另一笔更贵——第 3 章讲过,注意力的计算量随序列长度的平方增长。token 数变成 1/4,注意力那一项的代价变成 1/16

合并当然有代价:合并之后,模型能分辨的最小单位从 16 × 16 像素变成了 32 × 32 像素。对于「图里有一只猫」这种问题无所谓,对于「读出图里那行小字」就可能不够。这是一个真实的取舍,而官方没有公布任何关于这个取舍的评测数据——本章的研究课题就是它。

为什么边长必须是 32 的倍数

因为合并要凑成整齐的 2 × 2 组,所以两个方向上的图块数都必须是偶数;而图块数 = 边长 ÷ 16,所以边长必须是 16 × 2 = 32 的倍数。1920 × 1080 这个常见的视频分辨率就不行:1080 ÷ 16 = 67.5,第一步就切不整齐。真实实现会先把图缩放或填充到 32 的倍数(比如 1920 × 1088),这一步通常在图像预处理器里完成,不在模型里。

另一个观察:num_position_embeddings: 2304,而 2304 = 48 × 48。也就是说位置嵌入表是按 48 × 48 的图块网格准备的,对应 768 × 768 像素的图。比这更大的图怎么办?本站不知道——常见做法是把这张表插值放大,但 config 没有说明,模型卡也没有提。这一条列为存疑。

一张 640 × 480 的照片能直接喂进去吗?如果能,是多少个 token?如果不能,最小要调整到多少?

先想:两个方向的边长都要能被什么数整除?
关键在于:边长必须是 32 的倍数(16 × 2)。640 和 480 分别是不是?
第一步这样走:640 ÷ 32 = ? 480 ÷ 32 = ? 两个都是整数吗?
完整答案:能直接喂。640 ÷ 32 = 20,480 ÷ 32 = 15,两个都是整数,所以边长合法。
token 数 = (640/16) × (480/16) ÷ 4 = 40 × 30 ÷ 4 = 300 个。也可以用式 11-2 的简写直接算:640 × 480 ÷ 1024 = 307,200 ÷ 1024 = 300。
顺带练一个反例:640 × 400 就不行——400 ÷ 32 = 12.5。最近的合法尺寸是 640 × 384(12 × 32)或 640 × 416(13 × 32)。
这道题想让你形成的习惯是:先验尺寸合法性,再算 token 数。顺序反了的话,你会算出一个 250 这样的漂亮数字,然后在真跑的时候撞上一个形状错误。

变式:把一张 1024 × 1024 的图缩小一半到 512 × 512,token 数会减少多少?(答案:从 1024 降到 256,减到四分之一,不是一半。因为 token 数正比于面积,边长减半意味着面积变成 1/4。这个平方关系是所有「图片要不要缩小」决策的核心:缩小一点点边长,就能省下很多 token。)

11.3 视觉塔的账:让它能看图,只多花了 1.7%

视觉塔本身就是一个小 Transformer:27 层,hidden 1152,中间维 4304,16 个注意力头(每头 1152 ÷ 16 = 72 维)。下面是逐项点钞:

表 11-3:Qwen3.8-27B 视觉塔的逐项参数(本站 mat/paramcount.py 的复算结果)
算式参数个数
每层注意力(q/k/v/o 四张方阵)4 × 1152 × 11525,308,416(5.31 M)
每层 MLP(两张矩阵)2 × 1152 × 43049,916,416(9.92 M)
每层小计5,308,416 + 9,916,41615,224,832
27 层合计27 × 15,224,832411,070,464
图块嵌入(3 × 2 × 16 × 16) × 1152 = 1536 × 11521,769,472
位置嵌入2304 × 11522,654,208
merger 第一张(1152 × 4) × (1152 × 4) = 4608 × 460821,233,664
merger 第二张4608 × 512023,592,960
视觉塔合计460,320,768(460.32 M)

然后把它放回全模型里:

460,320,768 ÷ 27,355,639,808 = 1.68% ≈ 1.7%
式 11-3
符号是什么直觉
460,320,768视觉塔全部参数让这台机器长出眼睛的全部成本
27,355,639,808全模型参数(文本塔 26,895,319,040 + 视觉塔)整台机器
1.68%视觉塔占比比一层半的稠密 FFN 还少
一句话记住:让这个模型能看懂图片,只多花了 1.7% 的参数。换个尺子更直观:第 8 章算过,27B 的一层稠密 FFN 是 267.39 M;整个视觉塔 460.32 M ≈ 1.72 层 FFN。64 层里的不到两层。

这个数值得停下来想一想。你花了整整一章(第 8 章)讲那 62.6% 的参数怎么被 FFN 吃掉,而「看图」这个听起来完全是另一种能力的东西,只要 1.7%。

表 11-3 里有两个假设,它们是被总数反过来验证的

本站手上没有 Qwen3.8-27B 的逐张量清单(没有技术报告,模型卡也不列),所以表 11-3 里有两处是按常见结构假设的:

假设一:视觉塔每层的 MLP 是两张矩阵,不是三张。如果它其实是 SwiGLU(像文本塔那样三张),每层会多 1152 × 4304 = 4,958,208,27 层多 133,871,616,视觉塔总数会变成 594,192,384——那就对不上 460,320,768 了

假设二:视觉塔的注意力是标准多头,四张同样大小的方阵(1152 × 1152),没有 GQA、没有输出门、没有部分 RoPE。也就是说它和文本塔那套 Gated Attention 完全不是一回事。

这两个假设加上图块嵌入、位置嵌入、merger 两张矩阵,六项加起来正好是 460,320,768,一个字节不差。六个独立假设拼出来的和能正好命中,这本身是相当强的证据,但它不是证明。真正独立的验证在 11.5 节:Unsloth 实测的 mmproj 文件是 0.93 GB,除以每参数 2 字节得约 4.65 亿参数,和本站算的 4.603 亿差 1%——这个验证是独立的,因为文件体积是别人测的,参数量是本站算的。

自己推一遍:一张 1024 × 1024 的图,从像素到进入第 1 层

  1. 原始图片一共是多少个数?

    想好了再看

    1024 × 1024 × 3 = 3,145,728 个数(三百多万)。先记住这个数,最后回来对比一下它被压成了多少。

  2. patch_size: 16 切,有多少个图块?

    想好了再看

    (1024 ÷ 16)2 = 64 × 64 = 4096 个。此时序列长度是 4096——注意这已经比很多人的整段提问都长了。

  3. 每一个图块,在过图块嵌入矩阵之前,是多少个数?(小心那个容易被忘掉的 2)

    想好了再看

    3 × 2 × 16 × 16 = 1536。那个 2 是 temporal_patch_size——静态图片按本站推断是把同一帧凑两份。忘掉它的话,你算出来的图块嵌入矩阵会是 768 × 1152 = 884,736,视觉塔总数会少 884,736,就对不上 460,320,768 了。

  4. 过完嵌入矩阵,每个图块变成多少维?这时整个序列的形状是什么?

    想好了再看

    1152 维(vision_config.hidden_size)。序列形状是 [4096, 1152]。加上位置嵌入之后形状不变。

  5. 过完 27 层之后,形状变成什么?

    想好了再看

    还是 [4096, 1152],一点没变。这是 Transformer 层的基本性质(第 3 章):它改变每个向量的内容,不改变序列长度和向量维度。如果你觉得「过了 27 层总该变点什么形状吧」,那说明这个基本性质还没内化——所有形状上的变化都发生在层之外,也就是下一步。

  6. merger 做了两件事,分别是什么?形状怎么变?

    想好了再看

    第一件:把相邻 2 × 2 个图块的向量起来。4 个 1152 维拼成一个 4608 维,序列长度从 4096 降到 1024。形状变成 [1024, 4608]。
    第二件:过两张矩阵,4608 → 4608(中间带一次非线性)→ 5120。形状变成 [1024, 5120]
    注意 spatial_merge_size 在这里同时干了两件事:它既是降 token 数的手段,又提供了拼接的原料——正因为要合并 4 个,merger 的输入维度才是 1152 × 4 = 4608。

  7. 最后:这张图在文本序列里占几个位置?每个位置的向量是多少维?和文字 token 比呢?

    想好了再看

    1024 个位置,每个 5120 维。而一个文字 token 经过第 2 章那张 248320 × 5120 的嵌入表查出来,也是 5120 维两者形状完全一样,所以能排在同一条序列里,一起进第 1 层。
    回头看第 1 步:3,145,728 个数的原始像素,最终变成了 1024 × 5120 = 5,242,880 个数。数字总量反而变多了——所以这不是「压缩」,是「翻译」。压缩发生在 token 数上(4096 → 1024),不在信息量上。

  8. 反问一步:如果 spatial_merge_size 是 1(完全不合并),token 数是多少?merger 那两张矩阵会变成多大?

    想好了再看

    token 数是 4096(4 倍)。merger 的输入维度从 4608 掉到 1152,第一张矩阵从 4608 × 4608 = 21,233,664 缩到 1152 × 1152 = 1,327,104(少 94%),第二张从 4608 × 5120 = 23,592,960 缩到 1152 × 5120 = 5,898,240。视觉塔总参数会从 460,320,768 掉到 420,924,288
    这一问的意义:它说明 merger 那两张矩阵之所以是视觉塔里第二大的一项(44,826,624,占 9.7%),完全是因为 spatial_merge_size: 2 把输入维度撑到了 4608。降 token 数这件事不是免费的,它把成本转移到了 merger 的参数量上。

有人说:「27B 的视觉塔只占 1.7% 参数,说明 Qwen3.8 的多模态是敷衍了事,那 1.7% 根本干不了什么。」请用这一章和前几章的数据,给出三条反驳。

先想:参数占比小,和「计算量小」「作用小」是同一件事吗?一张图变成 1024 个 token,这 1024 个 token 后面还要走过什么?
关键在于:视觉塔的职责可能不是「理解图片」,而是「把像素翻译成文本塔能读的向量」。真正的理解发生在哪 64 层里?
第一步这样走:算一算视觉塔处理一张 1024 × 1024 图片时的计算量——4096 个 token 过 27 层,和文本塔处理 1024 个 token 过 64 层比,哪个更大?
完整答案:三条。
① 参数占比小不等于计算量小。视觉塔虽然只有 460.32 M 参数,但它处理的序列长得多——一张 1024 × 1024 的图在视觉塔里是 4096 个 token(合并发生在 27 层之后),而它进文本塔时只剩 1024 个。也就是说视觉塔要在 4 倍长的序列上跑完 27 层。参数少但序列长,计算量不能只看参数。
② 视觉塔的职责是翻译,不是理解。它把像素变成文本塔能读的 5120 维向量,之后「这张图和这句问话有什么关系」「左下角那串字是什么意思」这些真正的理解工作,全都发生在文本塔那 64 层里——那里有 26.90 B 参数。说「多模态能力只值 1.7%」,就像说「一个人的视觉能力只值视网膜那点重量」。
③ 这 1.7% 是不是从零训的,本站不知道。如果视觉塔是拿一个已经预训练好的图像编码器初始化再接上来的(这是很常见的做法),那么这 460.32 M 参数背后还站着一次本站看不见的训练,1.7% 这个数就更不能被读成「投入只有 1.7%」。Qwen3.8 没有技术报告,视觉塔是不是独立预训练过、用了多少图像数据,官方从未说明
这道题的题眼:参数占比是一个关于「存储成本」的指标,不是关于「重要性」或「计算成本」的指标。把它当成后两者用,是读参数表时最常见的一类误读。

变式:按同样的逻辑,Gated Attention 只占 27B 参数的 6.1%(16 层 × 104.86 M = 1.68 B),能说注意力不重要吗?(答案:不能,而且理由更强。第 8 章的小结里已经说过这件事:注意力占 6.1% 参数,却贡献了 262K 上下文下全部 16 GiB 的 KV cache——它在另一本账上是绝对的主角。同一个模块在不同的账本上占比可以差一个数量级,这是全站反复出现的模式。)

11.4 out_hidden_size 5120:那个转接头

视觉塔内部的 hidden 是 1152,文本塔是 5120。这两个数对不上,图像 token 就进不了同一条序列——形状不匹配,矩阵根本乘不起来。

所以 vision_config 里有一个字段专门解决这件事:out_hidden_size: 5120。它是视觉塔对外的接口宽度,和它内部那个 1152 是两回事。负责做这个转换的模块叫 merger(合并器),它就是那个转接头:

图片 1024×1024 切成 4096 个图块 视觉塔 27 层 hidden 1152 [4096, 1152] merger 2×2 合并 → 4608 4608 → 4608 → 5120 [1024, 5120] 44.83 M 参数 文字 这张图里… 嵌入表 248320 × 5120 同一条序列,每个位置都是 5120 维 深色 = 图像 token 浅色 = 文字 token 文本塔 64 层 从这里往后,不区分图和字
图 11-1:图像与文字如何汇入同一条序列。示意图,非官方图示;序列格子只画了 8 个代表实际的上千个。merger 是唯一的接缝——过了它,两种 token 在形状上完全一样。

这就是「原生」两个字的技术含义:不是两个模型串起来,是一个模型的输入端多了一条支路。从第 1 层开始,那 64 层里的每一次注意力、每一个 FFN,处理图像 token 和文字 token 用的是同一套权重,它们根本不知道自己在处理的是图还是字。区别只写在两个地方:向量的内容里,和 mRoPE 给的位置坐标里(11.6 节)。

image_token_id: 248056 是干什么的?词表一共 248,320 个编号,这个编号被留出来当占位符。你的输入序列在被处理之前是一串纯 token 编号,图片的位置先放上 248056 这个编号;真正送进第 1 层的时候,这些位置上的向量会被换成视觉塔输出的那些 5120 维向量。这一段是本站按常见实现推断的——config 只给了这个 id,没有说明替换机制,本站未能从材料核实 Qwen3.8 的具体做法。

如果把 out_hidden_size 从 5120 错改成 1152(也就是让 merger 只做合并、不做维度转换),模型会在哪一步崩?如果反过来,有人把 27B 的视觉塔原封不动接到 2.4T 上,又会在哪一步崩?

先想:图像 token 和文字 token 要排在同一条序列里,它们必须满足什么条件?
关键在于:同一条序列里的所有向量必须是同一个维度,因为它们要一起过同一批矩阵。文本塔的第一层期待的输入维度是多少?
第一步这样走:写出文本塔第 1 层里 q_proj 的形状(第 4 章)。它的输入维度是 5120 还是 1152?喂一个 1152 维的向量进去会怎样?
完整答案:
第一问:在拼接序列的那一步就崩,或者最迟在文本塔第 1 层的第一个矩阵乘法崩。文字 token 是 5120 维,图像 token 变成 1152 维,两者拼不成一个整齐的矩阵;就算强行拼上,文本塔第 1 层的 q_proj(第 4 章:输入维度 5120)也乘不动一个 1152 维的向量。这是好事——它会立刻抛一个形状错误,而不是悄悄跑出一堆垃圾。
第二问:崩在同一个地方,但原因反过来。27B 的视觉塔输出 5120 维(因为它的 out_hidden_size 是 5120,是对着 27B 的文本塔配的),而 2.4T 的 hidden 是 8192。5120 ≠ 8192,还是接不上。要接的话必须重新训练一个 merger,把 4608 映射到 8192——而且新的 merger 是随机初始化的,得配着 2.4T 一起训,不是接上就能用。
这道题真正的题眼是:out_hidden_size 这个字段的存在,本身就证明了视觉塔和文本塔是「配套」的,不是通用的。视觉塔不是一个可以随便插到任何模型上的通用组件;merger 那 44.83 M 参数就是这个「配套」的物理形态。这也顺带回答了第 10 章那道答辩题里提到的一件事——2.4T 不做多模态,绝不是「把 27B 的视觉塔搬过去」那么简单。

变式:如果有人想给 27B 换一个分辨率更高的视觉塔(比如 hidden 从 1152 提到 2048),哪些字段必须跟着改,哪些不用?(答案:必须改的是 vision_config 内部的一整套——hidden_sizeintermediate_sizenum_heads,以及 merger 的输入维度(从 1152 × 4 = 4608 变成 2048 × 4 = 8192)。不用改的是 out_hidden_size,它必须还是 5120,因为文本塔那边一个字段都没动。这正好说明 out_hidden_size 是一个接口约定,不是视觉塔的内部规格。当然,改完之后整个视觉塔和 merger 都得重训——又是第 ② 轴的事。)

11.5 mmproj 那 0.93 GB:24 GiB 预算里最容易被漏掉的一项

到了本地部署,视觉塔会以一个非常具体的形式出现在你的硬盘上。llama.cpp 那条工具链(llama.cpp、Ollama、LM Studio 都属于它)把模型打包成 GGUF 格式时,把视觉部分单独打包成一个 mmproj-*.gguf 文件,主权重文件里不含视觉塔。

常见误解:Q4_K_M 是 17.11 GB,所以 24 GiB 卡还剩 7 GB

不对。Unsloth 那张体积表里的 17.11 GB(Q4_K_M)是文本塔的体积,视觉塔要另外加载 0.93 GB。Ollama 页面上写的 18 GB 就是这么来的:17 GB 主权重 + 931 MB 的 projector。

漏掉这 0.93 GB 会怎样?在 24 GiB 的预算里,它相当于 0.87 GiB,而按第 5 章的公式(27B 每 token fp16 KV 是 64 KiB),0.87 GiB 大约等于 1.4 万个 token 的上下文。也就是说,忘记它的人会以为自己能开 10 万上下文,实际到 8.6 万就爆显存。

更隐蔽的一点:这个文件是必须加载的,只要你想让模型看图。有些人以为「我只用它处理文字,那就不加载视觉塔」——那当然可以,但那样你手上的就是一个纯文本模型了,和表 11-1 里那个 language_model_only 字段被置成 true 是同一回事。

本站把 ADDENDUM 里那张 24 GiB 预算表原样搬过来,让你看清这 0.87 GiB 在里面是哪一行:

表 11-4:24 GiB 单卡跑 Q4_K_M 的 27B,显存预算(本站推导,非官方数字
大小说明
显卡容量24 GiB厂商标的 24GB 实际是 24 GiB
Q4_K_M 文本塔权重17.11 GB = 15.93 GiBUnsloth 实测文件体积
mmproj 视觉塔0.93 GB = 0.87 GiB就是这一行,最容易被漏掉
DeltaNet 固定状态0.14 GiB48 层 × 3 MiB,不随序列长度变(第 6、7 章)
框架与激活开销1.0–2.0 GiB本站取的经验区间,非实测
剩给 KV cache5.1–6.1 GiBfp16 约 8.3–9.9 万 token;fp8 约 16.6–19.9 万
表 11-4 的三条限定,一条都不能省

这是本站推导,不是官方数字——官方从未发布 24 GiB 单卡的上下文上限。② 框架开销是经验区间,不是实测值,所以结论必须写成区间而不是单点。③ 实际部署还有分页与碎片开销,真实可用值会更靠近区间下沿。
结论应该这样说:24 GiB 单卡跑 Q4_K_M 的 Qwen3.8-27B,上下文实际到约 8–10 万 token(fp16 KV),开 fp8 KV 大约翻倍到 17–20 万原生的 262K 在 24 GiB 上跑不满。完整推导在第 17 章。

一个有意思的观察:视觉塔基本没被量化

拿 0.93 GB 除以本站算出的 460,320,768 个参数:

0.93 × 109 字节 ÷ 460,320,768 ≈ 2.02 字节/参数
式 11-4
符号是什么直觉
0.93 × 109Unsloth 实测的 mmproj 文件体积(十进制 GB)别人测的,不是本站算的
460,320,768本站复算的视觉塔参数量本站算的,不是别人测的
2.02 字节/参数= 16.2 bit/参数这就是 fp16,也就是没有量化

作为对照,主权重那边:17.11 × 109 字节 ÷ 26,895,319,040 参数 ≈ 5.09 bit/参数。也就是说 视觉塔的每个参数占的空间是文本塔的约 3.2 倍——文本塔被压到了 4 bit 出头,视觉塔还是原样的 16 bit。

这个式子还顺带完成了 11.3 节欠下的那个独立验证:如果本站的视觉塔参数量算错了,2.02 这个数就不会这么整齐地落在 fp16 上。反过来,本站也可以从文件体积倒推参数量:0.93 × 109 ÷ 2 = 4.65 亿,和本站算的 4.603 亿差 1%——差值来自 GGUF 的容器开销和可能有个别张量存成了 fp32。两个来源互不相干,结果对上了。

「为什么不量化视觉塔」本站未能核实

上面那个 2.02 字节/参数是可以复算的事实,但「为什么」没有任何官方或工具链的说明。本站能想到的工程理由有两条——视觉编码器对量化误差可能更敏感;视觉塔才 0.46 B,量化它省下来的绝对量太小(0.93 GB 压到 4 bit 也就省 0.7 GB),不值得冒精度的风险。但这两条都是本站的推测,没有出处。

构造一个具体场景,在这个场景里那 0.93 GB 是「能跑」和「跑不了」的分界线。然后回答:如果 mmproj 也被量化到 4 bit,这个场景会不会得救?

先想:表 11-4 里,把 0.87 GiB 这一行去掉,剩给 KV 的空间会变成多少?换算成上下文是多少?
关键在于:要让 0.93 GB 成为分界线,需要构造一个「刚好差这么多」的需求。比如某个应用要求固定的上下文长度,而预算刚好卡在中间。
第一步这样走:先算「不加 mmproj 时能开多长」和「加了之后能开多长」,两个数之间就是所有可以当分界线的需求。
完整答案:
场景构造:某个应用要求固定 10 万 token 上下文、fp16 KV。10 万 token × 64 KiB = 6,400,000 KiB = 6.10 GiB
不加载 mmproj 时,可用 = 24 − 15.93 − 0.14 − (1.0~2.0) = 5.93~6.93 GiB——取区间上沿刚好够
加载 mmproj 之后,可用 = 5.06~6.06 GiB(表 11-4 里四舍五入写成 5.1–6.1)——取区间上沿也差一点点
所以在「框架开销走运地落在 1.0 GiB」这个前提下,那 0.87 GiB 正好是分界线:不看图能跑 10 万,看图就跑不到。
第二问:量化 mmproj 能不能得救?把它压到 4 bit,体积从 0.93 GB(0.87 GiB)掉到约 0.23 GB(0.22 GiB),省下约 0.65 GiB,换算成 fp16 KV 是约 1.06 万 token。所以是的,这个场景会得救——但只是勉强得救,而且付出了视觉塔精度未知的代价(11.5 节最后那条 caution 说了,本站不知道视觉塔对量化有多敏感,也没见过任何评测)。
更划算的做法在另一条路上:把 KV cache 从 fp16 换成 fp8,10 万 token 只要 3.05 GiB,一下子省 3.05 GiB——是量化 mmproj 的4.7 倍,而且不动权重一个字节,改个启动参数就行(第 5 章讲过 --cache-type-k/-v)。
这道题的题眼是:显存优化要先看哪一项大。0.93 GB 值得被记住,是因为它容易被漏算导致预估失准,而不是因为它是最值得优化的那一项。第 17 章会把所有可选项按「省多少 / 代价是什么 / 今天能不能做」排成一张表。

变式:如果这个应用根本不需要看图,最优做法是什么?(答案:干脆不加载 mmproj。这不需要任何量化,直接省下完整的 0.87 GiB,而且零精度损失——因为你根本不用那部分能力。这也是本章最实用的一条:视觉塔是一条可以整个关掉的支路,这正是 language_model_only 这个字段和 mmproj 被单独打包这件事的意义所在。)

11.6 mRoPE:位置编码也要管图片

第 4 章 4.5 节已经把 mrope_section: [11, 11, 10] 这个字段拆过一遍了,这一节要说的是:它真正起作用的地方就在这里。

不用 mRoPE 会怎样:一个可以手算的具体后果

一段文字的位置可以用一个整数表示:第 1 个字、第 2 个字……但一张图切出来的图块不是排成一条线的,它有行和列。如果硬按「从左到右、从上到下」把它拉成一条线,会发生什么?

拿一张 1024 × 1024 的图,64 × 64 个图块。第 1 行第 5 列的图块,一维编号是 5;它正下方那个(第 2 行第 5 列)编号是 64 + 5 = 69。两个在图上紧挨着的块,在一维坐标上隔了 64 个位置;而它右边那个(第 1 行第 6 列)编号是 6,只隔 1 个。也就是说,一维化之后模型学到的「谁离谁近」是错的——它会以为上下相邻的两块离得比一行还远。

多模态旋转位置编码multimodal RoPE, mRoPE)解决的就是这件事:把参与旋转的那些维度分成几段,每段吃一个独立的坐标轴。这样「高」和「宽」各有一根自己的轴,上下相邻在「高」这根轴上就老老实实是差 1。

回顾第 4 章那条可以自己验的算术:head_dim 256 × partial_rotary_factor 0.25 = 64 维参与旋转,RoPE 成对使用,64 ÷ 2 = 32 对;而 11 + 11 + 10 = 32,正好分完。三段分别对应三根轴。

表 11-5:三种输入各需要几根坐标轴
输入类型需要几根轴一个位置怎么表示
文字1 根「第 137 个 token」
图片2 根(高、宽)「第 12 行第 40 列的图块」
视频3 根(时间、高、宽)「第 3 组帧、第 12 行、第 40 列」

视频那根时间轴,正是 11.2 节那个 temporal_patch_size: 2 的用武之地——每两帧压成一组,时间轴上的坐标就是「第几组」。

哪一段对应哪根轴,本站是推断的

这一条第 4 章已经声明过一次,因为它在这一章才真正落地,所以再确认一遍:mrope_section: [11, 11, 10] 这三个数在 config 里只有顺序,没有字段名。本站按这一类模型的常见约定推断顺序是「时间、高、宽」,但材料里没有任何依据。同样未能核实的还有两件事:① mrope_interleaved: true 具体怎么交错——这个开关说明三段不是简单地按顺序切成连续三块,但排布规则要看实现代码,模型卡没写;② 纯文本 token 在 mRoPE 下怎么处理(常见做法是三根轴取同一个值,退化成普通 RoPE),本站同样未能核实 Qwen3.8 的具体做法。
能确认的只有事实层:这两个字段确实在 cfg27b.jsonrope_parameters 里,而且 11 + 11 + 10 = 32 与 64 ÷ 2 的对应关系是算出来的、可以自己验的。

最后把这一章接回主线:vision_config 和 mRoPE 是 27B 和 2.4T 之间的第四处、第五处真实差异。而这两处差异的来源,都不是「谁大谁小」——是「谁要处理图像」。27B 和 2.4T 连位置编码都不一样,它们不可能是同一个模型的两个尺寸档。

我能不看材料说清:一张 1024 × 1024 的图会变成多少个 token(中间要经过哪两次除法)、视觉塔占全模型的百分之几、以及为什么算 24 GiB 显存时不能漏掉那 0.93 GB。

你要在一张 24 GiB 的卡上跑 Q4_K_M 的 Qwen3.8-27B,做一个「上传合同扫描件然后问问题」的工具。每份合同 12 页,每页扫成 1024 × 1024 的图。用户还会问几百字的问题、模型要回答上千字。
请回答:(a) 一份合同占多少个 token?(b) 按表 11-4 的预算,fp16 KV 下最多能一次塞几份合同?(c) 如果客户要求「一次塞 6 份」,你有哪三条路可走,每条各要付什么代价、分别落在四轴表的哪一轴?

先想:一张 1024 × 1024 的图是多少 token(式 11-2)?12 页是多少?再想:每个 token 的 fp16 KV 是多少(第 5 章)?
关键在于:视觉 token 和文字 token 在 KV cache 上没有任何区别——过了 merger 之后它们形状一样、走同样的 64 层、占同样的 64 KiB。所以你只需要算总 token 数。
第一步这样走:(a) 12 × 1024 = ? (b) 表 11-4 给的 KV 空间是 5.1–6.1 GiB,除以 64 KiB/token 得到 token 上限,再除以 (a)。(c) 三条路分别是:换 KV 精度、缩小图片、换模型——各自动了哪个字段?
完整答案:
(a) 每页 1024 × 1024 ÷ 1024 = 1024 个 token,12 页 = 12,288 个 token。加上问题和回答,一份合同的一轮对话大致按 1.4 万 token 估。
(b) 表 11-4 给 KV 的是 5.1–6.1 GiB。按每 token 64 KiB 算:5.1 GiB = 5.1 × 1,048,576 KiB ÷ 64 = 83,558 token;6.1 GiB ÷ 64 KiB = 99,942 token。除以 1.4 万,得 6.0 到 7.1 份——也就是说 5 份稳妥,6 份大致可行,7 份只在框架开销落到区间下沿时才勉强放得下
(c) 三条路:
路一:KV cache 换 fp8。每 token 从 64 KiB 降到 32 KiB,容量直接翻倍到 12–14 份。代价是 KV 的数值精度下降(第 5 章讲过,长上下文下的影响本站没有实测数据)。落在第 ④ 轴——训练之后,改个启动参数就行,今天就能做。这是三条里唯一不需要动模型的。
路二:把扫描件缩小到 512 × 512。每页从 1024 个 token 降到 256,一份合同从 12,288 降到 3,072,加上问答约 4,200,容量涨到 20–23 份。代价是分辨率减半,合同上的小字号条款可能读不出来——对这个具体应用来说,这可能是最不能接受的代价(合同的关键信息往往就在小字里)。落在哪一轴?哪一轴都不在——它连模型都没碰,只改了预处理。这是这道题最重要的一个发现:四轴表管的是模型,而你能拧的旋钮不止在模型上。
路三:换一个上下文更省的模型。比如一个全注意力层更少的模型(第 7 章:KV 正比于全注意力层数)。代价是必须重新预训练——落在第 ③ 轴,训练之前,今天做不了
三条路排序:路一今天就能做且省得最多,先做它;路二能省更多但要评估小字风险,做之前先拿几份真实合同验一下;路三在这个场景下不是一个选项。
这道题的题眼:一次真实的容量规划,要同时用到第 3、5、7、11 章的知识和第 1 章那张四轴表,而且最后你会发现最优解常常不在模型里。凡是只在「换模型 / 量化」两个选项里打转的方案,多半漏掉了路二这类成本最低的做法。

变式:如果客户改口说「不要图片了,我先把合同 OCR 成文字再传给你」,容量会怎么变?这算不算一种「压缩」?(答案:12 页合同 OCR 出来大概几千到一万字,按分词大致几千个 token,比 12,288 个视觉 token 少一半以上,容量能到 10 份以上。但这正是 11.1 节讲的「外挂」路线——OCR 结果里没有的东西就永远丢了:印章的位置、手写批注、表格的视觉结构、哪一段被划掉了。省 token 和保信息在这里是直接冲突的,而这个取舍该怎么选,取决于业务上到底在乎什么,不是一个技术问题。至于算不算压缩:它是信息层面的有损压缩,和第 15 章要讲的量化那种数值层面的压缩完全是两回事,别混。)

有人提议:「把 spatial_merge_size 从 2 改成 4,一张 1024 × 1024 的图就只占 256 个 token 了,上下文能塞四倍的图,多好。」这个改动在推理时能做吗?如果不能,是卡在哪一步?(提示:不只是「效果会变差」这个层面的回答。)

先想:spatial_merge_size 除了决定 token 数,还决定了什么?回去看 11.4 节 merger 的输入维度是怎么来的。
关键在于:merger 第一张矩阵的形状是 (1152 × merge²) × (1152 × merge²)。merge 从 2 变成 4,这个形状会变成多少?现有权重还对得上吗?
第一步这样走:算 merge=2 时的 4608 × 4608,和 merge=4 时的形状。两者的参数个数各是多少?
完整答案:推理时做不了,而且卡在一个比「效果变差」硬得多的地方——权重形状对不上。
merge = 2 时,merger 的输入维度是 1152 × 22 = 4608,第一张矩阵是 4608 × 4608 = 21,233,664,第二张是 4608 × 5120 = 23,592,960。
merge = 4 时,输入维度变成 1152 × 42 = 18432,第一张矩阵要变成 18432 × 18432 = 339,738,624,第二张 18432 × 5120 = 94,371,840两张矩阵合计从 44.83 M 暴涨到 434.11 M——单是 merger 就快赶上原来整个视觉塔了。
而这些新形状的权重根本不存在。你手上那份 4608 × 4608 的矩阵没法变成 18432 × 18432,凭空多出来的参数只能随机初始化,然后重训。所以这不是一个可以在推理时拨的开关,它是一个训练之前的结构决定——第 ② 轴。
值得补充的一点:这个提议的直觉方向其实是对的(少一些视觉 token 确实省 KV 和注意力代价),错的是以为它免费。真正想在推理时减少视觉 token,可行的路子是把图片本身缩小——把 1024 × 1024 缩到 512 × 512,token 数同样降到 256,而且一个权重都不用动(式 11-2 里 token 数正比于面积)。同一个目的,一条路要重训,一条路改个预处理参数就行。这道题真正要训练的判断是:拿到任何一个「调参数就能优化」的提议,先问它调的那个参数有没有权重形状挂在上面。

变式:patch_size 从 16 改成 32 呢?也是同样的问题吗?(答案:是,而且更严重。patch_size 决定图块嵌入矩阵的输入维度:3 × 2 × 16 × 16 = 1536 会变成 3 × 2 × 32 × 32 = 6144,那张矩阵从 1536 × 1152 变成 6144 × 1152,同样是形状不匹配、必须重训。而且 patch_size 还会连带影响位置嵌入表的网格大小。凡是出现在某张权重矩阵形状里的 config 字段,全都是训练之前的决定。这是一条可以推广到任何模型的判据。)

答辩:如果我是审稿人(一)

表 11-3 那些数字,全是你们按假设算出来的。你既没有官方的逐张量清单,也没有加载过真实权重。凭什么让读者相信视觉塔是 460,320,768 而不是别的数?

参考防守(先自己组织语言再看)

这个质疑说得对,而且本站在 11.3 节已经主动把两个假设点了名。这里把证据链完整摆一遍,让读者自己判断它有多强。

第一,本站承认的部分:Qwen3.8 没有技术报告,模型卡不列逐张量清单,本站也没有加载过 safetensors 的索引文件。表 11-3 里「视觉塔的 MLP 是两张矩阵」和「注意力是四张 1152 × 1152 的方阵」这两条,都是按常见结构假设的。

第二,内部自洽性:六项(27 层注意力、27 层 MLP、图块嵌入、位置嵌入、merger 两张)加起来正好是 460,320,768,一个字节不差。而且这六项里每一项的形状都由 vision_config 里的字段直接决定——1152、4304、16、2304、1536、4608、5120,没有一个是凑出来的自由参数。六个由 config 唯一确定的数相加,正好命中一个整数,这不是巧合能解释的。反证也做过:如果 MLP 是三张矩阵,总数会变成 594,192,384,差 29%,立刻就对不上。

第三,也是最强的一条——外部独立验证:Unsloth 打包的 mmproj 文件实测 0.93 GB。这个数不是本站算的,是别人测的一个文件的字节数。0.93 × 109 ÷ 460,320,768 = 2.02 字节/参数,正好落在 fp16 上;反过来从文件体积倒推参数量得 4.65 亿,和本站算的 4.603 亿差 1%(差值可由 GGUF 容器开销解释)。两条完全独立的路径——一条是数矩阵形状,一条是量文件字节数——在 1% 以内对上了。

第四,还剩什么没被排除:1% 的余量里理论上还能藏别的结构(比如若干个 LayerNorm 的参数,本站按惯例没算——它们通常是每层几千个参数,量级上正好落在这 1% 里)。所以更准确的表述应该是:460,320,768 是视觉塔主要权重矩阵的精确和,误差在 1% 以内,但它不含 LayerNorm 这类零星参数。本站愿意把这一条写成明确的存疑项,而不是号称精确到个位。

答辩:如果我是审稿人(二)

既然多模态只多花 1.7% 的参数,为什么 2.4T 不做?一个 2.42 T 的模型,加个视觉塔连零头都算不上,这个成本几乎为零。你们的解释是什么?

参考防守(先自己组织语言再看)

先给结论:本站不知道,官方从未解释过。下面把能确认的事实和不能确认的推测严格分开。

能确认的事实:① 2.4T 的 HuggingFace pipeline 标签是 text-generation,config 里没有 vision_config;② 参数成本确实几乎为零——如果给 2.4T 配一个与它 8192 维匹配的视觉塔,绝对参数量会比 460 M 大一些(主要是 merger 的输出维度从 5120 变 8192),但相对 2,419.8 B 的占比会小到 0.02% 量级,比 27B 那 1.7% 还低两个数量级。所以「参数成本」这个解释可以直接排除。

只能推测的部分(以下全部没有出处,读者请当成假设看):训练一个多模态模型需要大规模的图文配对数据和额外的训练阶段,成本主要在数据和训练而不在参数;一个需要 24 张卡才能部署的模型,其典型应用场景(大规模服务、复杂推理)和「传一张照片问它是什么」这类交互式场景本来就不重合;以及产品分工上,把多模态放在能本地部署的那一档、把纯文本的极限能力放在最大那一档,本身也是一种可能的划分方式。

为什么本站坚持不把这些推测写成结论:因为它们全都属于「听起来很有道理」的那一类解释,而这类解释的危险在于它们无法被证伪。第 18 章会专门讲怎么读这一类没有官方说明的空白——正确的态度不是找一个自洽的故事填进去,是把空白标出来。

顺带说一件相关的、有出处的事实:Pinterest 的购物助手 Navigator 1 建立在 Qwen3-VL 之上,并通过替换视觉编码器把成本进一步压低约 90%。也就是说,有人真的把一个开放权重多模态模型的视觉塔整个拆下来换成了自己的部件。这从侧面印证了 11.4 节那个结论:视觉塔在结构上是一条可替换、可旁路的支路out_hidden_size 那个接口约定正是这种可替换性的物理形态。它同时也说明,「有没有视觉塔」在工程上比大多数人以为的更像一个可插拔的选择——这也让「2.4T 为什么不做多模态」这个问题更值得追问,而不是更容易回答。

答辩:如果我是审稿人(三)

你把 spatial_merge_size: 2 讲成一个纯粹的优化(省 token、省注意力代价),但它明明是一次信息损失。合并之后模型能分辨的最小单位从 16 像素变成 32 像素,这对读小字、看图表是致命的。你有任何评测数据支持「2 是个好选择」吗?

参考防守(先自己组织语言再看)

没有。这个质疑本站接受,而且认为它指出的是这一章最大的一块空白。

本站在 11.2 节那个 callout 里已经写明了这是一个取舍,但审稿人说得对——本站给出的只有「省下了什么」这一半的数字(token 数降到 1/4、注意力代价降到 1/16),完全没有给「丢了什么」这一半。而这一半本站给不出来,原因是:Qwen3.8 没有技术报告,官方没有发布任何关于 spatial_merge_size 取值的消融实验,也没有发布细粒度视觉任务(OCR、图表问答、小目标识别)上的对照数据。

能补充的只有两条间接的东西。第一,这个取舍是被意识到了的——合并发生在 27 层之后而不是之前(11.4 节的图 11-1),这个顺序本身就是在保护细节:塔内的 27 层注意力接触到的仍然是 16 × 16 的原始粒度,只有最后输出给文本塔时才降采样。如果设计者不在乎细节,把合并挪到进塔之前会省掉整整 4 倍的塔内计算量——他们没有这么做。但请注意,这是本站从结构顺序读出来的推断,不是官方论证。

第二,读者有一条自己验证的路:同一张含小字的图,一次按原尺寸喂,一次缩小到一半再喂(token 数同样降到 1/4,效果上近似于把 merge 从 2 提到 4),比较模型能不能读出那行小字。这不是严格的对照实验(缩小图片和加大合并粒度在机制上不完全等价),但它至少能让你对「粒度到底重不重要」有第一手的感觉。本站已经把这条路写进了本章的研究课题。

最后表个态:本站宁可把这一处写成「有取舍、但缺数据」,也不愿意用「工程上的合理平衡」这种话把它糊过去。缺数据就是缺数据。

对你而言未知一张图到底值多少个 token?合并粒度和能力之间的那条曲线长什么样

spatial_merge_size: 2 把 token 数降到 1/4。如果降到 1/9(merge = 3)或 1/16(merge = 4)呢?省下的算力是可算的,丢掉的能力是未知的。对 Qwen3.8 而言,这条曲线上一个数据点都没有——官方没有发布任何消融实验。

本站把它标成「对你而言未知」而不是「真未解」,判据很明确:「视觉 token 压缩」是一个有大量已发表工作的活跃方向,答案(准确说是很多组针对不同模型的答案)是存在的,只是不在这份材料里,也没有一条是针对 Qwen3.8 的。把它标成「真未解」会让你误以为自己站在学科前沿,其实只是站在这份材料的边界上。

先做这一步,分两步走,第一步今天就能做完。
第一步(算清「省了什么」):打开本章的切块台,把 spatial_merge_size 从 2 调到 4,记下同一张 1024 × 1024 的图从 1024 个 token 变成 256 个;然后回到第 3 章的注意力代价公式,算出 token 数减到 1/4 时注意力那一项的计算量变成 1/16;再回到第 5 章的 KV 公式,算出 262K 上下文下能多塞多少张图(1024 → 256 意味着从 256 张变成 1024 张)。三个数算完,你就有了这条曲线的横轴。
第二步(去找「丢了什么」):这一半必须去查文献,搜索词用 visual token compressiontoken mergingvisual token pruning。查的时候盯住一件事:它的评测里有没有区分「粗粒度任务」和「细粒度任务」。图像分类、看图说话这类粗粒度任务对 token 数往往非常不敏感(压掉一大半分数几乎不掉),而 OCR、图表问答、小目标定位这类细粒度任务会掉得很快。只报粗粒度任务分数的论文,等于只报了这条曲线上最平的那一段。这个甄别习惯本身,比你查到的任何一个具体数字都值钱。

这一层要加什么:给模型加一个视觉入口

为什么现在才加它:前十层造的全是文本塔那条主干。这一层加的是一条并联的支路——它有自己的 27 层、自己的宽度(1152)、自己的位置编码,唯一和主干的接触点是 merger 那个把 4608 映射到 5120 的转接头。加完这一层,你手上的模型第一次能吃图片;而且更重要的是,加上这 460,320,768 个参数之后,你的参数计价器(第 10 层)报出来的总数会第一次达到 27,355,639,808——那正是官方称的 27B。第 12 章那场点钞的最后一块拼图就位了。

v = cfg.vision_config P, M = v.patch_size, v.spatial_merge_size # 16, 2 Dv, Dt = v.hidden_size, v.out_hidden_size # 1152, 5120 def n_vision_tokens(H, W): assert H % (P*M) == 0 and W % (P*M) == 0 # 边长必须是 32 的倍数 return (H // P) * (W // P) // (M * M) class VisionTower: def __init__(s, v): s.patch = Linear(3 * v.temporal_patch_size * P * P, Dv) # 1536 -> 1152 s.pos = Embedding(v.num_position_embeddings, Dv) # 2304 x 1152 s.blocks = [Block(Dv, v.intermediate_size, v.num_heads) # 标准多头 + 两矩阵 MLP for _ in range(v.depth)] # 27 层 s.merge1 = Linear(Dv * M * M, Dv * M * M) # 4608 -> 4608 s.merge2 = Linear(Dv * M * M, Dt) # 4608 -> 5120 def forward(s, img): h = s.patch(to_patches(img)) + s.pos(grid_index(img)) # [n_patch, 1152] for b in s.blocks: h = b(h) # 形状不变 h = group_2x2(h) # [n_patch/4, 4608] return s.merge2(gelu(s.merge1(h))) # [n_patch/4, 5120] # 拼进文本序列:把 image_token_id 占位符换成上面这些向量 seq = splice(text_embeds, at=positions_of(IMAGE_TOKEN_ID), with_=vision_out)

难点一:merger 不是可有可无的适配层,它是这一层参数量的大头之一。两张矩阵加起来 44,826,624,占整个视觉塔的 9.7%——比 27 层里任何一层(15,224,832)都大将近两倍。很多人把它想成「一个简单的线性映射」,然后在数参数时把它漏掉;漏了它,视觉塔从 460,320,768 掉到 415,494,144,全模型总参数会从 27,355,639,808 掉到 27,310,813,184——你在「27.36 B」这个写法上根本看不出差别,但第 12 章逐矩阵点钞时会对不上。

难点二:合并发生在 27 层之后,不是之前。如果你在进塔之前就把 2 × 2 合并了,塔内的序列长度直接降到 1/4,计算量省一大截——听起来是白捡的优化。但那样每个 token 看到的就是一个 32 × 32 像素的粗块,塔里那 27 层注意力再也接触不到 16 × 16 的细节顺序反了不会报错,形状全对,训练也能收敛,只是模型再也读不清小字。这是这一层最阴险的一个错误,因为它没有任何症状,只有能力上的损失。

难点三:边长约束必须写成断言,不能靠注释。合并要凑成 2 × 2 组,所以两个方向的图块数都得是偶数,也就是边长必须是 P × M = 32 的倍数。你如果直接喂一张 1920 × 1080 的图,1080 ÷ 16 = 67.5,第一步就切不整齐。真实实现会在预处理里缩放或填充,但你的学习实现最好让它大声崩掉——一个安静地把 67.5 向下取整成 67 的实现,会让你的 token 数悄悄少一行,然后在某个完全不相干的地方报一个莫名其妙的形状错误。

自己验:五条,前三条验形状,后两条验参数。
n_vision_tokens(1024, 1024) 应该正好1024;顺手打印中间量,图块数应该是 (1024/16)2 = 4096
换成 512 × 512,应该正好256(图块 1024)。边长减半,token 数变 1/4,不是 1/2——如果你得到 512,说明某处少除了一次。
打印视觉塔输出的形状,应该是 [1024, 5120]如果第二维是 1152,说明 merger 没接上;这时候把它拼进文本序列会直接触发形状不匹配的报错——这是好事,比悄悄跑通要好得多。
数一遍视觉塔的参数:应该正好460,320,768。把 merger 那两张矩阵删掉,应该掉到 415,494,144(少 9.74%)。把第 10 层的参数计价器接上视觉塔,全模型总参数应该正好是 27,355,639,808,且总参数 等于 激活参数(27B 是稠密的)。
喂 1920 × 1080 应该在第一行断言就报错(1080 不是 32 的倍数);改成 1920 × 1088 应该给出 (1920/16) × (1088/16) ÷ 4 = 120 × 68 ÷ 4 = 2040 个 token。

本章小结

  • 原生多模态是结构,不是功能开关:27B 的 config 分成 text_configvision_config 两块,HuggingFace 标签是 image-text-to-text;2.4T 只有一层扁平字段,标签是 text-generation没有 vision_config。这是两个模型的第四处真实差异。
  • 图片变 token 的两次除法:先按 patch_size: 16 切成图块,再按 spatial_merge_size: 2 四合一。合起来就是「一个 token 代表 32 × 32 个像素」。1024 × 1024 → 4096 个图块 → 1024 个 token,一张图约等于一千个字。边长必须是 32 的倍数。
  • 视觉塔只占 1.7%:27 层 × (4 × 1152² + 2 × 1152 × 4304) + 图块嵌入 + 位置嵌入 + merger 两张 = 460,320,768,占 27,355,639,808 的 1.68%,约等于 1.72 层稠密 FFN。但参数占比小不等于作用小——它处理的序列比进文本塔时长 4 倍,而且真正的理解发生在文本塔那 64 层里。
  • out_hidden_size: 5120 是接口约定:视觉塔内部 1152 维,输出必须是 5120 维才能和文字 token 排在同一条序列里。merger(4608 → 4608 → 5120)就是那个转接头,它一个人占了视觉塔的 9.7%。过了它之后,那 64 层不区分图和字
  • mmproj 那 0.93 GB:GGUF 把视觉塔单独打包,17.11 GB 的 Q4_K_M 里不含它。漏算它相当于漏掉 0.87 GiB,约 1.4 万 token 的上下文。而且 0.93 GB ÷ 460,320,768 ≈ 2.02 字节/参数——视觉塔基本没被量化,还是 fp16,每个参数占的空间是文本塔(约 5.09 bit)的 3.2 倍。
  • 24 GiB 的完整预算(本站推导):24 − 15.93 − 0.87 − 0.14 − (1.0~2.0) = 5.1–6.1 GiB 给 KV,对应 fp16 约 8–10 万 token、fp8 约 17–20 万。原生 262K 跑不满。
  • mRoPE 在这一章才真正落地:文字一维、图片二维、视频三维,mrope_section: [11, 11, 10] 三段共 32 对 = 256 × 0.25 ÷ 2。哪一段对应哪根轴,config 没写字段名,是本站按常见约定的推断。
  • 接回主线:模态和位置编码是 27B 与 2.4T 的第四、第五处真实差异,来源都不是「谁大谁小」,是「谁要处理图像」。连位置编码都不一样的两个模型,不可能是同一个模型的两个尺寸档。

到这里,27B 的全部零件都造齐了:嵌入表、64 层的注意力与 DeltaNet、FFN、视觉塔。下一章是全站的高潮——把它们逐张矩阵加起来,看看能不能把官方那两个数字一个不差地复现出来。

第12章 把它拼起来:亲手数出 27B 和 2.4T

前面十一章,每一章数清了一个零件。这一章只做一件事:把它们加起来。加完你会看到两个数从纸上冒出来——27.36B 和 2.420T——而它们正是官方在模型名字里写的那两个数。这一章的重点不是加法,是加法之后你手里多出来的那个东西:你不再需要相信厂商的数字,你可以自己算。

学完这一章你应该能做到

  • 说清「点钞」的三条规则,并解释为什么偏置和归一化层可以整个忽略
  • 只拿 config.json 里的七八个数字,逐行算出 26,895,319,040 这个整数
  • 指出参数占比里最反直觉的两处,并解释「线性注意力更省」省的到底是什么
  • 把同一套规则套到 MoE 上,同时算出总参数与激活参数两个口径
  • 说清「可复算的证据」和「厂商声明」为什么该有不同的信任度
前置:第0章的矩阵乘法与「什么叫一个参数」、第2章的嵌入层与输出头、第4章的 Gated Attention 层、第6章的 Gated DeltaNet 层、第8章的 FFN、第9章的 MoE、第10章的总参数与激活参数之分、第11章的视觉塔。这一章不引入任何新概念,它只是收账。

12.1 点钞的规则:只有三条

不会点钞会怎样

你现在打开任何一篇讲 Qwen3.8 的文章,都会看到「27B 参数」这四个字。这个数从哪来?绝大多数文章的答案是:官方这么说的。于是整条信息链是「厂商说 → 通稿转 → 你信」,中间没有一个环节可以被检验。只要你会点钞,这条链就能被你自己接管:config.json 是公开的,矩阵形状是公开的,乘法和加法是你自己会的。厂商在这个数字上说没说实话,你可以当场判定,不需要任何人授权。

点钞的全部规则只有三条,而且第 0 章已经把最难的那条讲完了。

规则一:一张 a×b 的矩阵,就是 a×b 个参数。不多不少。一张把 5120 维向量变成 17408 维向量的矩阵,它有 5120 行 17408 列(或者反过来写,怎么摆看框架的约定,乘出来的数一样),一共 89,128,960 个参数。每一个都是训练时被单独学出来的一个数,每一个都要在硬盘上占位置。

规则二:同一种部件重复 N 次,就把它的参数量乘 NQwen3.8-27B 有 64 层,每层都有一个 FFN,那就是 64 份 FFN 的参数。注意「重复」在这里是结构上重复而不是数值上共享——64 个 FFN 的形状一模一样,但里面的数各不相同,所以要乘 64 而不是只算一份。这是初学者最常犯的错:以为「同一个模块」意味着「同一份权重」。

规则三:只出现一次的东西,就只算一次。嵌入层和输出头各只有一份,视觉塔也只有一座。

为什么偏置和归一化层可以整个扔掉

一层里除了那些大矩阵,还有一些「细长条」:归一化层(RMSNorm)的缩放向量、线性层的偏置。它们也是参数,为什么点钞时可以不算?

因为它们的量级完全不在一个数量级上。一张矩阵的参数量是两个维度相乘,写成阶就是 O(d2);一条归一化向量的长度就是 hidden 本身,是 O(d)。代进 Qwen3.8-27B 的数:up_proj 那张矩阵 89,128,960 个参数,同一层里的一条 RMSNorm 向量 5,120 个参数——差 17,408 倍。整个模型里所有归一化向量加起来是百万量级,而总数是二百七十三亿,占比万分之零点几。你把它们全删掉,27.36B 这个数的前三位不会动一下。

另外 Qwen3.8 的线性层根本没有偏置——config.json 里 attention_bias: false,现代大模型的线性层普遍不带偏置。所以这一项连「忽略」都谈不上,它压根不存在。

这里有一处本站没能核实

本站副本 cfg27b.json 是人工节选的 config,它没有逐条列出归一化层出现在哪些位置(每层两条?还是加上 QK 归一化共四条?)。上面那个「百万量级」是本站按每层两条估的(64 层 × 2 × 5120 ≈ 65.5 万)。真实值可能是这个数的两三倍,但数量级不变,对三位有效数字的结论没有影响。要拿到精确值,得去看权重文件的张量清单,不是看 config。

(a) 一张把 5120 维映射到 17408 维的矩阵有多少个参数?(b) 如果有人坚持说「归一化层也是参数,不算就是耍赖」,请用一个具体的比值回应他。

先想:(a) 用规则一,两个维度直接相乘。(b) 你要比较的是「一张矩阵」和「一条向量」的参数量,它们分别怎么随 hidden 变化?
关键在于阶:矩阵是两个维度相乘(O(d2)),归一化向量的长度就是 hidden 本身(O(d))。比值就是另一个维度。
第一步这样走:把 up_proj(5120×17408)的参数量除以同一层里一条 RMSNorm 向量的长度(5120),看得到什么。
完整答案:(a) 5120 × 17408 = 89,128,960,约 8913 万。(b) 89,128,960 ÷ 5,120 = 17,408——一张矩阵抵得上一万七千多条归一化向量。全模型所有归一化向量加起来是百万量级,占 273.6 亿的万分之几,落在三位有效数字之外。这不是耍赖,这是「保留有效数字」这件事的标准做法:我们要复现的是官方那个精确到三位的数(27.4 / 27.3 / 2.42),而被扔掉的项影响不到第四位以前的任何一位。反过来说,如果哪天你发现忽略某一项之后差了 1%,那就不能忽略——判据永远是「它会不会动到你要报的那几位」,不是「它是不是参数」。

变式:如果把 hidden 从 5120 改成 512(缩小 10 倍),矩阵参数量缩小几倍?归一化向量缩小几倍?这个比值会怎么变?(答案:矩阵缩 100 倍,向量缩 10 倍,比值从 17408 降到 1740.8——模型越小,「细长条」越不能忽略。这也是为什么点钞时的近似必须随规模重新检查。)

12.2 逐层清点 27B:一张表,每一行都有出处

现在把前面各章的结果排成一张表。请注意最右边那一列——每一行都能对上前面的某一章,没有一个数是这一章新造出来的。

表 12-1:Qwen3.8-27B 逐部件点钞(全部由 mat/cfg27b.json 复算,脚本见 mat/paramcount.py)
部件单份参数量份数小计出自
嵌入层248,320 × 5,120 = 1,271,398,40011,271,398,400 = 1.27 B第 2 章
输出头248,320 × 5,120 = 1,271,398,40011,271,398,400 = 1.27 B第 2 章
Gated Attention 层104,857,600161,677,721,600 = 1.68 B第 4 章
Gated DeltaNet 层115,875,840485,562,040,320 = 5.56 B第 6 章
FFN267,386,8806417,112,760,320 = 17.11 B第 8 章
文本塔合计26,895,319,040 = 26.90 B
视觉塔460,320,7681460,320,768 = 0.46 B第 11 章
全模型27,355,639,808 = 27.36 B

把这张表写成一条式子,就是这一章的主公式:

P文本塔 = 2VH + LfullPattn + LlinPlin + L·Pffn
式 12-1
符号是什么27B 里等于多少
V词表大小,即嵌入表有多少行248,320
Hhidden_size,模型内部那根「主干」有多宽5,120
2VH嵌入表 + 输出头。乘 2 是因为 tie_word_embeddings=false,两者不共享权重2,542,796,800
Lfull全注意力层的层数16
Llin线性注意力(Gated DeltaNet)层的层数48
L总层数,L = Lfull + Llin。FFN 每层都有,所以它乘的是 L 不是别的64
Pattn一个 Gated Attention 层的参数量(含输出门那一半)104,857,600
Plin一个 Gated DeltaNet 层的参数量115,875,840
Pffn一个稠密 FFN 的参数量,3H·17408267,386,880

代进去,一步一步加:

1,271,398,400 + 1,271,398,400 + 1,677,721,600 + 5,562,040,320 + 17,112,760,320 = 26,895,319,040
式 12-2
这一项是什么怎么来的
1,271,398,400嵌入层248,320 × 5,120(词表 × hidden)
1,271,398,400输出头同上。出现两次不是笔误——tie_word_embeddings=false,两张表不共享
1,677,721,60016 层 Gated Attention16 × 104,857,600
5,562,040,32048 层 Gated DeltaNet48 × 115,875,840
17,112,760,32064 个 FFN64 × 267,386,880。乘的是 64 不是 16 或 48——每层都有 FFN
26,895,319,040文本塔合计以上五项之和

要是你想自己核对进位,把中间量也摆出来:两个 1,271,398,400 加起来是 2,542,796,800;加上 1,677,721,600 得 4,220,518,400;加上 5,562,040,320 得 9,782,558,720;再加上 17,112,760,320 得 26,895,319,040。最后加视觉塔的 460,320,768,得 27,355,639,808

一句话记住:官方叫它 27B。Ollama 的模型页标 27.3B。我们只拿一份公开的 config.json,逐张矩阵数出来 27.36B这个数对上了——而且它不是「差不多」,是我们能把每一分钱指到具体是哪张矩阵。
三个数为什么不完全一样:本站的解释与它的可检验性

「27B」是产品名,不是测量值——它是把真实值四舍五入到整数的结果,跟「27.36」不冲突。真正需要解释的是 Ollama 那个 27.3B 和我们的 27.36B 之间差的约 0.06 B。

本站的推断(不是官方说法):Ollama 分发的 GGUF 把视觉塔单独拆成了 mmproj 文件(931 MB),所以它报的很可能是文本塔那一份;而文本塔我们算的是 26.90 B,比 27.3 B 少了约 0.42 B。config 里还有一个我们没有计入的东西:mtp_num_hidden_layers: 1——内置的多 token 预测草稿头。如果这个草稿头的结构是「一个解码层 + 一张把两倍 hidden 压回 hidden 的投影」,它大致是 104,857,600 + 267,386,880 + 5120×2×5120 ≈ 4.25 亿,加上去正好是 27.32 B,四舍五入就是 27.3B。

这段推断本站无法证实:MTP 头的确切结构没有出现在公开的 config 字段里,本站也没有去读权重文件的张量清单。写在这里是因为它可以被检验——见本章末的研究课题。请把它当成一条待验证的猜测,不要当成事实转述。

自己推一遍:只给 config 的八个数,逼出 26,895,319,040

  1. 你手上只有这八个数:vocab_size=248320hidden_size=5120num_hidden_layers=64full_attention_interval=4intermediate_size=17408tie_word_embeddings=false,以及各章算好的三个单层参数量。第一件要决定的事是:哪些部件是「每层都有」的,哪些不是?为什么这个判断必须排在所有乘法之前?

    想好了再看

    因为「份数」这一列全靠它。FFN 是每层都有——不管这层的前半截是 Attention 还是 DeltaNet,后半截都挂一个 FFN,所以它乘的是 64。而 Attention 和 DeltaNet 是互斥的:一层里只可能有其中一种,所以它们的份数加起来必须正好等于 64。当初会先问这个,是因为点错份数是唯一一种「每个乘法都对、总数却差十几亿」的错法——它不会报错,只会让你对不上官方数字而找不到原因。

  2. full_attention_interval=4 怎么变成「16 和 48」这两个数?

    想好了再看

    「每 4 层出现一次全注意力」,所以全注意力层数 = 64 ÷ 4 = 16,剩下的 64 − 16 = 48 层是线性注意力。这一步藏着一个陷阱:如果层数不能被 interval 整除,这个除法就要讨论向上还是向下取整。Qwen3.8 的 64 和 92 都恰好能被 4 整除(92 ÷ 4 = 23),所以两个模型都不需要讨论——但你写代码时得决定,否则换个 config 就崩。

  3. 嵌入层 248,320 × 5,120 只有一张表,为什么式 12-1 里写的是 2VH

    想好了再看

    因为 tie_word_embeddings=false。这个字段的意思是「输入端的查表矩阵」和「输出端把 5120 维打回 248,320 个词的打分矩阵」不是同一份权重,各训各的。两张同样大的表,所以乘 2。如果这个字段是 true(很多小模型是),这两张表共用一份,那就只能算一次——同一份 config 只改这一个布尔值,总参数会少 12.7 亿。当初会去看这个字段,是因为「输入和输出都要跟词表打交道」这件事本身不能告诉你它们是不是同一张表。

  4. 现在把四项加起来。加完之后,用什么办法判断你没算错?

    想好了再看

    三个自查: 份数列相加,16 + 48 必须等于 64,FFN 那一行也必须是 64。 各项占比排序应该是 FFN > DeltaNet > 嵌入+输出头 > Attention——如果 Attention 排到了前面,说明你多半忘了 FFN 是每层都有。 最终数的量级:官方名字里写着 27B,你算出来的东西必须落在 26–28 B 之间,差一个数量级说明某处乘错了份数。这三条都不是「验算一遍」,而是用不同的方式再看一次同一个数——这才是有效的自查。

如果把 tie_word_embeddingsfalse 改成 true(其他字段全不动),Qwen3.8-27B 的文本塔参数量会变成多少?这个改动需不需要重新训练?

先想:这个字段控制的是「输入嵌入表」和「输出打分矩阵」是不是同一份权重。改成 true 之后,两张表变成了几张?
关键在于式 12-1 里的 2VH 这一项会变成 VH。其余三项一个都不动。
第一步这样走:算出 VH = 248,320 × 5,120 = 1,271,398,400,然后从 26,895,319,040 里减掉这个数。
完整答案:26,895,319,040 − 1,271,398,400 = 25,623,920,640,约 25.62 B。必须重新训练——这不是换个记法,是把两张独立的矩阵合并成一张,模型的可学参数真的少了 12.7 亿个,原来那两张表里学到的东西没有办法「合并」成一张。这一题的意义在于:它演示了第 1 章那四条轴里的第 ① 条长什么样。同样是让文件变小 2.5 GB(按 BF16 算),改这个字段是换了一张图纸,而量化是换了一支笔——前者要重训,后者不用。

变式:假设有人给你两个 config,唯一的差别就是这个布尔值,他说「这两个是同一个模型的两个版本」。他说得对吗?(答案:不对。参数个数不同、可学的东西不同,是两个模型。判据很硬:把两边的张量清单列出来,一边有 lm_head.weight,另一边没有。)

12.3 一个反直觉的发现:你学了五章的注意力,只占 6.1%

把表 12-1 的小计除以 27,355,639,808,得到的占比是这样的:

占全模型 27.36 B 的比例 FFN(64 层) 62.6% 17.11 B Gated DeltaNet(48 层) 20.3% 5.56 B 嵌入层 + 输出头 9.3% 2.54 B Gated Attention(16 层) 6.1% 1.68 B ← 就这么点 视觉塔(27 层) 1.7% 0.46 B 0% 20% 40% 60%
图 12-1:Qwen3.8-27B 的参数花在了哪里。数据来自本站对 cfg27b.json 的复算(mat/paramcount.out.txt),不是官方公布的图表。

你从第 3 章一路学到第 7 章,五章讲的都是注意力:QKV 是什么、softmax 在干嘛、GQA 怎么共享、KV cache 怎么涨、混合布局怎么排。学完之后你大概率会以为注意力是这台机器的主角。在参数账上,它是个配角——16 层全注意力加起来 1.68 B,占 6.1%。而那个一直被顺带提一句的 FFN,占 62.6%。

常见误解:占比小 = 不重要

6.1% 不代表注意力可有可无。把 16 层全注意力全删掉,模型会立刻失去逐字精确回忆的能力(第 7 章讲过为什么),而这件事 48 层 DeltaNet 补不回来。参数占比衡量的是存了多少东西,不是有多要紧。注意力是调度机制,机制本身不需要很多参数就能定义清楚;知识需要地方存,所以吃参数。
更要紧的是这两笔账走向相反:FFN 占 62.6% 的参数却不占一个字节的 KV cache;Attention 只占 6.1% 的参数,却贡献了 262K 上下文下全部 16 GiB 的 KV。权重是买断的固定成本,KV 是按量计费的变动成本——占比这张图只画了前者。

第二个反直觉:DeltaNet 层比 Attention 层参数还多

看表 12-1 的单份参数量那一列:Gated DeltaNet 层 115,875,840,Gated Attention 层 104,857,600线性注意力那一层比全注意力那一层多出 1100 万个参数,多了 10.5%。

这跟很多人的印象是反的。「线性注意力更省」这句话在网上到处都是,读到这里你可能会怀疑是不是哪里算错了。没算错。把两层的内部拆开对照一下就清楚了:

表 12-2:两种层的内部构成对照(Qwen3.8-27B,单层)
Gated Attention参数Gated DeltaNet参数
q_proj(含输出门,输出翻倍)62,914,560in_proj_qkvz(q,k,v,z 四路一起投影)83,886,080
k_proj(只有 4 个 KV 头)5,242,880in_proj_ba(β 与 α 两个标量门)491,520
v_proj(只有 4 个 KV 头)5,242,880conv1d(核长 4 的因果卷积)40,960
o_proj31,457,280out_proj31,457,280
合计104,857,600合计115,875,840

差距全部来自第一行。Attention 那边靠 GQA 省了大钱:24 个 Q 头,但 K 和 V 只有 4 个头,两张投影矩阵各只有 5120×1024,加起来才 1048 万。DeltaNet 那边没有这种共享——它有 48 个 value 头,每个头 128 维,光是 v 和 z 两路就各要 48×128 = 6144 维的输出,加上 q、k 各 16×128 = 2048 维,一共要从 5120 投影到 2048×2 + 6144×2 = 16,384 维。5120 × 16384 = 83,886,080。

一句话记住:线性注意力省的不是参数。它省的是两样别的东西——KV cache(262K 上下文下,16 层全注意力要 16 GiB,48 层 DeltaNet 的全部记忆只有 144 MiB,而且不随长度增长)和长序列上的算力(注意力的打分是 O(n2),DeltaNet 是 O(n))。你多付了 10.5% 的参数,买回来的是这两样。
那 144 MiB 是怎么来的

每个 value 头持有一个 128×128 的状态矩阵,48 个头就是 48×128×128 个 float32(config 里 mamba_ssm_dtype: "float32"),一层 3 MiB,48 层共 144 MiB。这是本站按 Gated DeltaNet 论文(arXiv:2412.06464)的定义推导的,官方与推理框架都没有公布过这个数。它和 KV cache 的关键差别是:1 个 token 和 100 万个 token,这 144 MiB 一样大。

num_hidden_layers 从 64 改成 32(保持 3:1 布局,其余字段不动),文本塔参数量变成多少?它是不是 26.90 B 的一半?差在哪里?

先想:式 12-1 的四项里,哪几项跟层数成正比,哪一项跟层数完全无关?
关键在于 2VH 那一项——嵌入表和输出头有多大,只跟词表和 hidden 有关,跟你堆几层一点关系都没有
第一步这样走:32 层意味着全注意力 32÷4 = 8 层、线性 24 层、FFN 32 份。把这三项各自算出来,再加上原封不动的 2,542,796,800。
完整答案:8 × 104,857,600 = 838,860,800;24 × 115,875,840 = 2,781,020,160;32 × 267,386,880 = 8,556,380,160;再加 2,542,796,800,总计 14,719,057,920,约 14.72 B
而 26,895,319,040 的一半是 13,447,659,520 = 13.45 B。差了 1,271,398,400——正好是嵌入表加输出头的一半(2,542,796,800 ÷ 2)。原因就是它们不随层数缩水:层数砍一半,那两张表一个格子都没少。
这道题的用处是给你一个可以随身携带的直觉:模型越小,嵌入层占比越高。27B 里它占 9.3%,砍到 32 层就涨到 17.3%(2,542,796,800 ÷ 14,719,057,920)。这也是为什么真正的小模型(1B 以下)常常要靠共享输入输出表(tie_word_embeddings=true)来省参数——大模型不在乎那点,小模型在乎得要命。

变式:反过来,把层数从 64 加到 128,嵌入层占比会降到多少?(提示:先算新的总数 51,247,841,280。答案约 5.0%——所以「词表太大浪费参数」这个抱怨,在大模型上基本站不住脚。)

请构造一处 config 改动,只动两个字段,使 Qwen3.8-27B 的总参数量一个都不差,而每 token 的 KV cache 正好翻 4 倍。(提示:第 4 章讲过 attn_output_gateq_proj 的输出翻倍。)

先想:KV cache 的大小由哪几个字段决定?它们当中,哪一个改动之后只影响 k_proj 和 v_proj 的参数量,而不影响别的?
关键在于找一个「加钱」的改动和一个「减钱」的改动,让它们的数额正好相等。KV 那边要涨 4 倍,就得让 num_key_value_heads 从 4 变成 16。现在算算这一改会多花多少参数,再去别处找一个正好省这么多的地方。
第一步这样走:算 num_key_value_heads 从 4 改到 16 时,k_proj 从 5120×(4×256) 变成 5120×(16×256),多了多少;v_proj 同理。然后算 attn_output_gatetrue 改成 falseq_proj 少了多少。
完整答案:num_key_value_heads: 4 → 16,同时 attn_output_gate: true → false。
加的一边k_proj 从 5,242,880 涨到 5120×4096 = 20,971,520,多 15,728,640;v_proj 一样多 15,728,640。每层多 31,457,280
减的一边attn_output_gate=trueq_proj 输出翻倍(一半是门),是 5120×(24×256×2) = 62,914,560;关掉门之后是 5120×(24×256) = 31,457,280,每层少 31,457,280
两边逐位相等,16 层全注意力上各自 ±503,316,480,总参数分毫不变,仍是 26,895,319,040。
KV 那边:每 token KV 元素数 = 2 × 16 × Hkv × 256,Hkv 从 4 变 16,从 32,768 变成 131,072,正好 4 倍:fp16 下每 token 从 64 KiB 涨到 256 KiB,262K 满上下文的 KV 从 16 GiB 涨到 64 GiB。
这道题是本章最该带走的一个认识:参数量和显存占用是两笔账,同一个总参数可以对应完全不同的部署成本。凡是只报「多少 B」的模型介绍,都还没有告诉你它跑起来要多少显存。

变式:能不能只改一个字段,做到「参数不变而 KV 减半」?(答案:不能。KV 只由 Lfull·Hkv·dhead 四项决定,其中任何一项变小都会同时让 k/v/o/q 里的某几张矩阵变小——想让参数不变,就必须在别处补回来,也就至少要动两个字段。)

我能不看材料说清:为什么 Gated DeltaNet 层的参数比 Gated Attention 层还多,以及「线性注意力更省」省的到底是哪两样东西。

12.4 逐层清点 2.4T:同一套规则,多一列

2.4T-A95B 的点钞完全用同一套规则,只是多了一列。因为它的 FFN 换成了 MoE(第 9 章),每一层都有两个数:这一层一共存了多少参数,和这一层处理一个 token 时真正用到了多少参数。

表 12-3:Qwen3.8-2.4T-A95B 逐部件点钞(由 mat/cfg24t.json 复算)
部件单份(总 / 激活)份数总参数小计激活参数小计
嵌入层248,320 × 8,192 = 2,034,237,44012.03 B2.03 B
输出头同上12.03 B2.03 B
Gated Attention 层419,430,400239,646,899,200 = 9.65 B9.65 B
Gated DeltaNet 层438,386,6886930,248,681,472 = 30.25 B30.25 B
MoE 层25,824,329,728 / 557,842,432922,375,838,334,976 = 2.376 T51,321,503,744 = 51.32 B
合计2,419,802,390,528 = 2.420 T95,285,559,296 = 95.29 B

MoE 那一层的 25,824,329,728 是这么来的:单个专家 3 × 8192 × 2048 = 50,331,648,512 个路由专家就是 25,769,803,776;加一个同样大小的共享专家 50,331,648;再加路由门 8192 × 512 = 4,194,304。而激活那一列,路由专家只算被选中的 10 个(503,316,480),共享专家和路由门每次都要走,所以两边都算。

P = 2VH + LfullPattn + LlinPlin + L(E Pe + Pe + HE)
P激活 = 2VH + LfullPattn + LlinPlin + L(k Pe + Pe + HE)
式 12-3
符号是什么2.4T 里等于多少
E路由专家的总个数(num_experts512
k每个 token 选中几个路由专家(num_experts_per_tok10
Pe一个专家的参数量,3H·2048(SwiGLU 三张矩阵)50,331,648
E Pe vs k Pe两条式子唯一的差别:存的时候 512 个都要存,用的时候只走 10 个25.77 B vs 0.50 B
+ Pe共享专家。它每个 token 都要走,所以在两条式子里都是原样出现50,331,648
+ HE路由门(给 512 个专家各打一个分),也是每个 token 都要走4,194,304
一句话记住:官方称 2.4T-A95B。我们从 config.json 算出 2.420 T 总参数、95.29 B 激活参数。三位有效数字全中。

这张表里还藏着两个值得停一下的对比。第一个:MoE 层占了总参数的 98.2%(2.376 T ÷ 2.420 T),但在激活参数里只占 53.9%(51.32 B ÷ 95.29 B)。也就是说,从「买硬盘」的角度看这台机器几乎全是 MoE;从「每个 token 要做多少乘加」的角度看,注意力和 DeltaNet 那 39.9 B 反而占到了四成。第二个:嵌入层加输出头在 27B 里占 9.3%,在 2.4T 里只占 0.17%——词表一样大(两个模型都是 248,320),但身体大了 88 倍。

MoE 层占 2.4T 总参数的 98.2%,却只占激活参数的 53.9%。请解释这两个数为什么差这么多,并说出哪一个决定「要买几张卡」、哪一个决定「等多久出第一个 token」。

先想:MoE 层里 512 个专家,一个 token 走几个?而注意力层和 DeltaNet 层,一个 token 走它们的百分之几?
关键在于:注意力和 DeltaNet 是稠密的——每个 token 都要把它们的每一个参数用一遍,所以它们在两个口径里的数字完全一样。只有 MoE 那一项会缩水。
第一步这样走:算 MoE 层的缩水倍数。25,824,329,728 ÷ 557,842,432 ≈ 46.3。再看别的项缩水多少倍(答案是 1 倍,不缩)。分母整体缩小了,不缩水的那些项占比自然就涨上来了。
完整答案:MoE 层从 25.82 B 缩到 557.84 M,缩了约 46.3 倍;而 Attention(9.65 B)、DeltaNet(30.25 B)、嵌入+输出头(4.07 B)一点都不缩——它们是稠密结构,每个 token 都要全部用一遍。于是分母从 2.420 T 掉到 95.29 B(缩 25.4 倍),MoE 缩得比整体还快,占比就从 98.2% 掉到 53.9%;而完全不缩水的那 43.96 B(9.65 + 30.25 + 4.07),占比从 1.8% 一路升到 46.1%——53.9% + 46.1% 正好把 100% 分完。
总参数决定要买几张卡:权重必须整个装进显存,2.420 T 按 BF16 是 4.40 TiB。激活参数决定等多久:每个 token 的乘加量只相当于一个 95.29 B 的稠密模型。这正是第 10 章那句话的算术版本。
还有一个推论值得记住:MoE 只稀疏化了 FFN 这一部分。想让激活参数进一步下降,光加专家数没用(那只涨总参数),得去动注意力和 DeltaNet——而那两块是没法稀疏化的,因为每个 token 都必须经过它们。

变式:如果把 num_experts 从 512 加到 1024(其余不变),总参数和激活参数各变成多少?(答案:总参数几乎翻倍到约 4.79 T,激活参数只多了路由门那 92 × 4,194,304 ≈ 3.86 亿,涨到约 95.67 B——成本翻倍,速度几乎不变。这就是 MoE 那条曲线的形状。)

有人说:「2.4T 是 27B 的 88 倍大,所以生成同样长的回答,它要慢 88 倍、贵 88 倍。」请用本章的两张表指出这句话的问题,并给出两个更接近事实的倍数。

先想:「大 88 倍」用的是哪个口径的参数量?「慢多少」应该看哪个口径?
关键在于把「装得下」和「跑得动」这两件事分开:前者看总参数(权重要全部驻留显存),后者看激活参数(每个 token 的乘加量)。
第一步这样走:算两个比值。总参数比:2,419,802,390,528 ÷ 27,355,639,808。激活参数比:95,285,559,296 ÷ 27,355,639,808。
完整答案:总参数比 ≈ 88.5 倍(这个数他没说错),激活参数比 ≈ 3.48 倍
他的错误是拿总参数去推速度。正确的说法是两句话: 显存上确实差约 88 倍——27B 的 BF16 权重约 54.7 GB,2.4T 是 4.40 TiB,一个塞得进一张卡,一个要几十张。 单 token 的计算量只差约 3.5 倍,因为 2.4T 每次只激活 95.29 B。
但「贵 88 倍」这个说法还有第三层问题:成本不等于计算量。就算计算量只差 3.5 倍,你也得先把 4.40 TiB 的权重放进几十张卡里,那些卡是按小时计费的,不管它们此刻算的是哪 10 个专家。所以实际的服务成本更靠近「几十张卡的租金 ÷ 吞吐」,既不是 88 也不是 3.5。本章只能给你前两个数,第三个数需要实际部署数据,本站没有。
这道题的价值在于:同一个「大多少倍」的问题,至少有三个不同的答案,取决于你问的是显存、算力还是钱。看到只给一个倍数的说法,先问它用的是哪个口径。

变式:如果有人反过来说「反正激活参数才 95B,那 2.4T 和一个 95B 的稠密模型没区别」,他又错在哪?(答案:错在显存。95B 稠密模型的 BF16 权重约 190 GB,2.4T 是 4.40 TiB(约 4840 GB),差 25.4 倍——正好就是稀疏度的倒数。它们在「算得多快」上像,在「装得下装不下」上完全不像——而后者往往才是能不能跑的决定因素。)

12.5 亲手跑一遍:参数点钞机

前面四节你已经把两个模型都数完了。但读一遍表和自己动手改一个数,是两种完全不同的理解。下面这个实验室,就是把本章那两条式子做成了可以拖动的东西。

它能玩什么:上方可以在 27B2.4T 两套真实 config 之间切换——这不是示例数据,就是 cfg27b.jsoncfg24t.json 里的字段。下面每一个字段都能改:hidden_sizenum_hidden_layersfull_attention_intervalvocab_sizeintermediate_size、专家总数、每 token 激活几个专家、KV 头数、attn_output_gate 的开关。每改一个,右边四个数会同时动:总参数激活参数BF16 权重体积每 token 的 KV cache

怎么玩才有收获——建议你带着问题去拖,而不是乱拖:

  • 先复现:什么都不改,看总参数是不是 26,895,319,040(27B 的文本塔)和 2,419,802,390,528(2.4T)。对上了,说明这个实验室和本章的表是同一套算法。
  • 推翻一个你以为对的规律:把 hidden_size 从 5120 翻到 10240,其余不动。如果你以为总参数会变 4 倍(因为「矩阵参数是 O(H2)」),实验室会给你一个正好 2 倍的数。去展开看是哪几张矩阵——你会发现 head_dimintermediate_size 这些「另一边」的维度在这份 config 里是写死的,不跟着 hidden 走。(这就是 q12-7 那道题。)
  • 验证 12.4 那道题:把 num_experts 从 512 拉到 1024,看总参数几乎翻倍而激活参数几乎不动。
  • 验证 12.3 那道题:把 num_key_value_heads 从 4 拉到 16,同时关掉 attn_output_gate,看总参数回到一模一样的值,而 KV cache 那一栏翻了 4 倍。
  • 展开看细节:每一项都可以点开,看这个小计是由哪几张矩阵、什么形状加出来的。对不上的时候,从这里往下找。
这个实验室算的是什么、不算什么

它实现的就是 mat/paramcount.py 那套逻辑:逐张权重矩阵累加,不含归一化层、不含 MTP 头、不含推理时的激活值和框架开销。所以它给出的 KV cache 是纯 KV 张量的大小,不是「这个模型要多少显存」——后者还要加权重、加 DeltaNet 的固定态、加框架开销,那是第 17 章的账。

在实验室里把 hidden_size 从 5120 改成 10240(翻倍),其余字段一个都不动。总参数会变成原来的几倍?很多人的第一反应是 4 倍(因为「矩阵参数是 O(H2)」)。先自己算,再去实验室对。

先想:「矩阵参数是 O(H2)」这句话成立的前提是什么?——是矩阵的另一边也要跟着 H 一起变。去 config 里看看,另一边的那些维度是算出来的还是写死的
关键在于三个字段:head_dim: 256num_attention_heads: 24intermediate_size: 17408——它们都是独立指定的常数,不会跟着 hidden 变。所以 q_proj 是 5120 × (24×256),改 hidden 只动了左边那一个数
第一步这样走:挑一张具体的矩阵试试。up_proj 是 5120 × 17408;把 5120 换成 10240 之后是 10240 × 17408。它涨了几倍?再对 q_proj、嵌入表各做一次同样的检查。
完整答案:正好 2 倍,不是 4 倍。实验室会给你 53,788,672,000(原来是 26,895,319,040,比值 1.9999)。
为什么:这份 config 里,所有矩阵的「另一边」都是独立写死的常数——head_dim 256、num_attention_heads 24、intermediate_size 17408、linear_*_head_dim 128、词表 248,320。它们没有一个是从 hidden 算出来的。于是每一张矩阵都长成「H × 某个常数」的样子,H 翻倍就是整张矩阵翻倍,仅此而已。
那 1.9999 而不是精确的 2.0000 是哪来的?因果卷积 conv1d 那 40,960 个参数——它是 (2dk + dv) × 4整个式子里没有 H,所以它是全模型唯一一项完全不随 hidden 变化的参数。48 层加起来不到 200 万,正好造成那个 0.005% 的零头。
这道题真正要教的东西:「参数量是 O(H2)」是一句有前提的话——它假设中间维和 hidden 成比例(很多模型确实这么设计)。Qwen3.8 不是。把一条在别处成立的规律直接套过来,是这一章最容易犯的错,而它的解药就是打开 config 看一眼。

变式:那要怎么改才能真的得到 4 倍?(答案:把所有维度一起翻倍——hidden 5120→10240、intermediate_size 17408→34816、head_dim 256→512、linear_key_head_dimlinear_value_head_dim 128→256。这样算出来是 102,444,564,480,比值 3.81——仍然到不了 4,因为嵌入表是 V×H,词表没跟着翻倍,那一项永远只有 2 倍。要真正的 4 倍,连词表都得翻倍——而那会换一个分词器,等于换了个模型。

12.6 为什么这件事重要:可复算的证据,和厂商声明

现在退一步看看刚才发生了什么。

Qwen3.8 没有 arXiv 技术报告——用 all:"Qwen3.8" 去 arXiv 检索,零条结果。它也没有官方 GitHub 仓库github.com/QwenLM/Qwen3.8 是 404)。你能拿到的一手材料只有两样:Hugging Face 上的模型卡,和仓库里的 config.json。剩下的全是二手转述,其中相当一部分互相矛盾。

但就凭 config.json 这一个文件,我们复现了官方宣传的每一个数字:27B 对上了,2.4T 对上了,A95B 对上了,三位有效数字全中。这件事的意义不在于「原来官方没骗人」,而在于它示范了一类说法应该怎么被对待

第 1 章讲过证据分级。现在可以把那张表往前推一步:同样是「官方说的」,可信度也不一样,取决于它能不能被你独立复算。

表 12-4:两类说法的可核查性对照(本站的判断框架)
「27B / 2.4T-A95B」这类「在某某评测上得了多少分」这类
要复现它,你需要什么一份公开的 config.json + 乘法和加法模型权重 + 几十张卡 + 完整的评测脚本与数据集 + 一模一样的采样参数
普通人做得到吗做得到,本章刚做完做不到。绝大多数读者复现不了任何一条 benchmark 分数
如果厂商在这上面说谎会被当场抓住,因为任何人都能算很难被抓住,因为复现成本高到几乎没人会去做
你应该给多少信任高。它是可复算的事实低到中。它是厂商声明,除非有独立第三方在公开的相同设置下复现过
一句话记住:参数量我们能自己算,所以它是证据;benchmark 分数我们算不了,所以它是声明。这两类说法出现在同一份模型卡上,但你不该给它们同样的信任度。

这也解释了为什么这个站要花十二章的篇幅去数参数。不是因为参数量本身多重要——它其实只是一个数——而是因为它是这台机器上少数几件你能亲手验证的事之一。在一个信息链普遍是「厂商说 → 通稿转 → 你信」的领域里,能亲手验证的东西每一件都值得学会。

别把这个结论用过头

「参数量复现成功」说明厂商在参数量这一个数字上是诚实的。它不能推出「所以这个模型的其他说法也可信」,更不能推出「所以它比别的模型好」。它证明的事情非常有限,而恰恰是因为有限,它才可靠。把一个小而硬的结论说成一个大而软的结论,是这类分析最常见的失败方式。

下面这段介绍里有五个数字:「Qwen3.8-27B 有 270 亿参数、64 层、上下文 262,144,在某评测上得 85.2 分,在 24GB 显卡上每秒能出 38 个 token。」请把这五个数分成「你今天就能独立验证」和「你验证不了」两组,然后真的去验一个

先想:验证每一个数分别需要什么。有的只需要一份公开文件加算术,有的需要权重、显卡、评测脚本,有的还需要一模一样的软件版本。
关键在于表 12-4 那条判据:复现它的门槛是不是普通人跨得过去的。config.json 是公开文本文件,谁都能下载;而跑一次评测要几十张卡加完整的数据集。
第一步这样走:先把「只需要 config.json + 算术」的挑出来,那一组就是可验证的。然后随便挑其中一个,现在就用本章的规则算一遍。
完整答案:能验的三个——270 亿参数(本章刚算完,26,895,319,040 + 460,320,768 = 27,355,639,808,四舍五入到「约 270 亿」成立,更精确是 273.6 亿,所以这个说法偏低了一点但没错);64 层(config 里 num_hidden_layers: 64,一眼可查);262,144 上下文(config 里 max_position_embeddings: 262144,也是一眼可查)。
验不了的两个——85.2 分:需要权重、几十张卡、完整评测脚本、以及和对方一模一样的采样参数(温度、top-p、是否开思考、思考预算),任何一项不同分数就不一样。每秒 38 个 token:更糟,它还额外依赖具体显卡型号、驱动版本、推理框架与版本、量化格式、batch 大小、上下文长度——这个数几乎无法脱离环境成立,看到它没有附带完整环境说明时,它基本等于没说。
这道题的要点不是给这段话打分,而是让你养成一个动作:读到任何一个数字,先问「我要怎么验它」。这一问会自动把说法分成两类,而这两类值得两种信任度。顺便注意一个细节:「270 亿」这个说法虽然可验,但它把 273.6 亿说成 270 亿——可验证的数字也可能被说得不精确,可验证只是意味着你能发现这件事。

变式:再加一个数字——「模型文件 17.11 GB」。它属于哪一组?(答案:介于两者之间,属于可验证但需要下载那一类。它是一个具体文件的大小,去 Hugging Face 的仓库页面上就能看到,不需要跑模型;但它依赖于「哪一个量化版本」这个前提——同一个模型有十几个不同体积的文件,脱离量化格式谈体积没有意义。所以正确的问法不是「它多大」,而是「哪一个文件多大」。)

答辩:如果我是审稿人

你算出 27.36B,官方叫 27B,Ollama 标 27.3B——三个数没有一个完全一样。你却用「复现了」这三个字。这不是复现,这是「量级差不多」,然后你反过来拿它给全站的可信度背书,是不是循环论证?

参考防守(先自己组织语言再看)

这个质疑成立的部分要先认下来:如果我们只说「差不多」,那确实什么都没证明。区别在于我们做的比「差不多」多两件事。第一,我们给出的不是一个近似值,而是一个精确到个位的整数 26,895,319,040,并且能说出它由哪五项加成、每一项来自哪张矩阵、每张矩阵的形状来自 config 的哪个字段。任何人只要发现某一项错了,整条链就断——这是可证伪的,不是「量级对上了」。第二,我们对差值给了有方向的解释:27B 是四舍五入的产品名;27.3B 与 26.90B 之间那 0.4B 我们指认为 MTP 头,并且给了检验办法(去读 GGUF 的张量清单)。
但审稿人有一点提醒得对:这个解释目前还是猜测,本站没有验证过。所以正文里它被放在 caution 框里,而不是当成结论。至于「循环论证」——我们并没有用参数量的成功去推别的结论,恰恰相反,12.6 明确写了它证明参数量这一件事。真正的循环论证长这样:「因为参数量对上了,所以模型卡可信,所以 benchmark 分数也可信」——那句话我们没写,而且专门用一个 caution 框把它拦住了。

答辩:如果我是审稿人

你把归一化层和偏置「因为小所以忽略」。这是把不利于你的项扫到地毯底下。如果全算上,会不会正好把你和 Ollama 的差距填平,从而说明你的 MTP 解释是错的?

参考防守(先自己组织语言再看)

这个反问的思路是对的——被忽略的项确实可能改变结论,所以必须算一下量级再决定,而不是凭「它看起来小」。算:全模型归一化向量按每层两条估是 64 × 2 × 5120 ≈ 65.5 万;就算低估了三倍,也是两百万量级。而我们和 Ollama 的差距是 4 亿多——差了两百倍。归一化层就算全算上、再乘上一个安全系数,也填不平那个坑。所以 MTP 那个解释不会因此被推翻(它当然可能因为别的原因被推翻,比如 MTP 头的真实结构和我们假设的不一样)。
还要补一句:偏置这一项压根不存在,config 里 attention_bias: false。这不是忽略,是这个模型确实没有。
最后给一条通用判据:一个被忽略的项什么时候必须捡回来?当它的量级达到你要报的有效数字那一位的时候。我们要报三位有效数字(27.4 B),第三位对应的是 0.1 B = 1 亿。两百万远在 1 亿之下,可以扔;四亿在 1 亿之上,必须解释——这正是我们对 MTP 的处理和对归一化层的处理不一样的原因。

答辩:如果我是审稿人

既然一份 config.json 就能把这个模型的尺寸算得一清二楚,那所谓「开放权重」还剩下什么秘密?你这一章是不是顺便证明了这些模型没什么技术含量?

参考防守(先自己组织语言再看)

正好相反,这一章证明的是公开的信息量有多小。config.json 给出的全是形状:多少层、多宽、几个头、几个专家。形状之外的一切都没有公开——每一个参数的数值(那才是模型学到的东西)、训练数据是什么、有多少 token、训练过程怎么排、超参数怎么调、后训练用了哪些方法。这些才是决定模型好坏的部分,而它们一个字都没有。
打个比方:我们相当于拿到了一栋楼的尺寸图——楼高多少、每层几个房间、房间多大。凭这个我们能算出它用了多少钢筋水泥,能判断开发商报的建筑面积对不对。但我们不知道这栋楼是怎么盖起来的,也不知道住在里面是什么体验。
所以准确的说法是:可复算的部分我们复算了,不可复算的部分我们承认不知道。后者远大于前者。这一章的价值不是「揭穿了什么」,是划出了一条线——线的这一边你可以自己判断,线的那一边你只能选择信或不信,而且应该知道自己是在信。

对你而言未知Ollama 页面上那个 27.3B 到底是怎么数出来的?

本章给了一个可以对上的猜测(文本塔 26.90 B + 一个约 4.25 亿参数的 MTP 草稿头 ≈ 27.32 B),但本站没有验证过。这个问题的答案是存在的,只是不在我们手上的材料里——它在权重文件本身。GGUF 文件的头部带着完整的张量清单:每一张权重矩阵的名字和形状都在里面,而分发方报出来的参数量通常就是把这份清单里所有张量的元素数加起来。所以这件事完全可以查实,只是需要动手。

先做这一步:下载一个 Qwen3.8-27B 的 GGUF(unsloth/Qwen3.8-27B-GGUF 里最小的 UD-IQ2_XXS 只有 9.01 GB,做这件事够用了),用 gguf-dump(llama.cpp 自带)或者任意一个能读 GGUF 头部的脚本把张量清单打出来,然后做三件事: 把所有张量的元素数加起来,看是不是 27.3 B 那个量级; 搜清单里有没有名字带 mtpnextn 前缀的张量,如果有,把它们单独加一遍,看是不是约 4.25 亿; 确认视觉塔的张量确实不在这个文件里(它在单独的 mmproj 文件里)。三步做完,本章那个 caution 框里的猜测要么被证实,要么被推翻——两种结果都是真结果。顺便你会得到一份完整的张量清单,那是比 config.json 更细的一手材料,第 14 章的血统检查器正好要用它。

这一层要加什么:一个 count_params(config) 函数

为什么现在才加它:前十一层你每写完一个模块,都只能验证「这一个模块的形状对不对」。到这一层,第一次有了一个能验证整台机器的判据——把你这十一层的模块参数量加起来,必须等于一个公开可查的整数。这是整个项目里第一次,你的实现能和外部世界对账。

def count_params(cfg): H, L = cfg.hidden_size, cfg.num_hidden_layers n_full = L // cfg.full_attention_interval n_lin = L - n_full emb = cfg.vocab_size * H head = 0 if cfg.tie_word_embeddings else emb # ← 别默认它们共享 q = H * cfg.num_attention_heads * cfg.head_dim * (2 if cfg.attn_output_gate else 1) kv = 2 * H * cfg.num_key_value_heads * cfg.head_dim o = cfg.num_attention_heads * cfg.head_dim * H P_attn = q + kv + o kd = cfg.linear_num_key_heads * cfg.linear_key_head_dim vd = cfg.linear_num_value_heads * cfg.linear_value_head_dim P_lin = H*(2*kd + 2*vd) + H*2*cfg.linear_num_value_heads \ + (2*kd + vd)*cfg.linear_conv_kernel_dim + vd*H if cfg.num_experts: # MoE:两个口径一起返回 Pe = 3 * H * cfg.moe_intermediate_size ffn_tot = cfg.num_experts*Pe + Pe + H*cfg.num_experts ffn_act = cfg.num_experts_per_tok*Pe + Pe + H*cfg.num_experts else: ffn_tot = ffn_act = 3 * H * cfg.intermediate_size base = emb + head + n_full*P_attn + n_lin*P_lin return base + L*ffn_tot, base + L*ffn_act # (总参数, 激活参数)

难点一:attn_output_gate 那个乘 2 极容易漏。它不在任何教科书的 attention 公式里,只写在 config 的一个布尔字段上,而且它只让 q_proj 翻倍、不碰 k/v/o。漏了它,27B 的文本塔会正好少 503,316,480——这个数不大不小,总数从 26.90 B 变成 26.39 B,看起来「还挺接近」,于是你会以为自己算对了。凡是「错了也看起来对」的地方,都必须有专门的判据去卡。

难点二:MoE 必须一次返回两个数,而不是返回一个再让调用方去乘系数。因为共享专家和路由门在两个口径里都要原样出现,只有路由专家那一项是 E 换成 k。如果你偷懒写成「激活 = 总数 × k/E」,共享专家和路由门就被一起打了折,激活参数会算少约 50 亿。这是 MoE 点钞里最常见的一个错。

难点三:tie_word_embeddings 的默认值不能想当然。很多参考实现默认 true(小模型的习惯),Qwen3.8 两个模型都是 false。默认错了,27B 差 12.7 亿,2.4T 差 20.3 亿——后者在 2.42 T 面前只有万分之八,三位有效数字看不出来,会一路潜伏到你去查小模型时才爆。所以这个字段要显式读,不要给默认值。

自己验:三个数逐位对上才算过。
cfg27b.json,总参数应该正好是 26,895,319,040(文本塔,不含视觉塔)。
cfg24t.json,应该正好返回 (2,419,802,390,528, 95,285,559,296) 这一对。
cfg27b.json 里的 attn_output_gatetrue 改成 false,总数应该正好减少 503,316,480(= 16 × 31,457,280,也就是 16 层里 q_proj 那一半门),变成 26,392,002,560
如果 ① 对而 ③ 少的不是这个数:少了 31,457,280 说明你只减了一层(忘了乘 16);少了 1,006,632,960 说明你把 k/v 也跟着减了(门只在 q 上)。如果 ② 的第一个数对而第二个数少了约 46 亿,去检查是不是把共享专家和路由门也按 k/E 打了折。

本章小结

  • 点钞三条规则:一张 a×b 矩阵 = a×b 个参数;重复 N 次就乘 N;只出现一次的只算一次。偏置和归一化是 O(d) 项,相对 O(d2) 的矩阵差四五个数量级,扔掉不影响三位有效数字。
  • 27B 对上了:1.27 + 1.27 + 1.68 + 5.56 + 17.11 = 26,895,319,040(文本塔),加视觉塔 460,320,768 得 27,355,639,808 = 27.36 B。官方称 27B,Ollama 标 27.3B。
  • 2.4T 也对上了2,419,802,390,528 = 2.420 T 总参数、95,285,559,296 = 95.29 B 激活参数,官方称 2.4T-A95B,三位有效数字全中
  • 两个反直觉:注意力只占 6.1%(FFN 占 62.6%);而单层来看,Gated DeltaNet(115.88 M)比 Gated Attention(104.86 M)参数还多 10.5%——线性注意力省的是 KV cache 和长序列算力,不是参数
  • MoE 两个口径:MoE 层占总参数 98.2%,占激活参数只有 53.9%。总参数决定要买几张卡,激活参数决定等多久出第一个 token。
  • 参数量和显存是两笔账:存在一处只改两个字段的改动(KV 头 4→16、关掉输出门),能让总参数分毫不变而 KV cache 正好翻 4 倍。只报「多少 B」的模型介绍,还没告诉你它跑起来要多少显存。
  • 这一章真正的产出:Qwen3.8 没有技术报告、没有官方 GitHub,但仅凭一份公开的 config.json,官方宣传的每个数字都被独立复算出来了。参数量是可复算的证据,benchmark 分数是厂商声明——同一份模型卡上的两类说法,值得两种不同的信任度。

数完之后,一个自然的问题冒出来了:27B 和 2.4T 这两个数,当初是怎么定下来的?为什么不是 20B 和 3T?下一章去看这个问题背后那条从 2020 年吵到今天的线索——Scaling Law,以及它在 2023 年被推理成本改写的那一次转折。

第13章 尺寸从哪来:Scaling Law 与推理预算

上一章你亲手数出了 27.36B 和 2.420T。这一章问的是它们的前一步:当初为什么定成这两个数?答案要从 2020 年的一条幂律讲起,经过 2022 年的一次修正,再到 2023 年被一句话掀翻——那句话说的是:所有这些计算,都忘了算模型上线之后的账

学完这一章你应该能做到

  • 用「幂律」解释为什么尺寸是一条连续曲线上的取样点,而不是几个孤立档位
  • 说出 Chinchilla 的结论(约 20 tokens/param),并解释它当年修正了什么
  • 指出 Chinchilla 的目标函数少算了哪一项,以及补上这一项之后最优点往哪边移
  • 诚实地区分「Qwen3.8 的尺寸选择」里哪些是事实、哪些是本站的推断
  • 拆开「黄金尺寸」这个说法,指出黄金的到底是哪个常数
前置:第0章的对数与幂(这一章全程用它)、第5章的显存账、第10章的总参数与激活参数、第12章的点钞规则(本章要拿它来倒推「多大的模型能塞进多大的卡」)。

13.1 尺寸不是拍脑袋的:一条画在双对数纸上的直线

不知道这条规律会怎样

假设你是 2019 年的研究员,手里有一笔算力预算,要决定训一个多大的模型。没有任何规律可循的话,你只能试:训一个 1B 的看看,再训一个 10B 的看看。可训一次要几周和几百万美元,你试不了几次。Scaling law 的全部价值就在这里——它让你用几个小模型的结果,去预测一个还没训的大模型会到什么水平。没有它,每一次决定尺寸都是一次昂贵的赌博。

缩放律Scaling Law):描述模型的损失(loss,衡量预测得有多差,越小越好)随参数量、数据量、算力这三个量变化的经验规律。Kaplan 等人在 2020 年那篇(arXiv:2001.08361)里给出的核心发现是:这个关系是一条幂律

L(N) ≈ (Nc / N)αN
式 13-1
符号是什么直觉
L损失(loss)。模型对下一个词猜得有多差,越小越好「错误率」的一种数学化写法。它不是准确率,也不是分数,是一个连续的、越小越好的数
N模型的参数量。就是第 12 章你数出来的那个数房子有多大
Nc一个拟合出来的常数,量纲和 N 一样相当于给横轴定了个「刻度原点」,具体值取决于数据和架构
αN幂次,论文报告它是一个不大的正数(远小于 1)这一章最重要的一个符号。它决定了「模型翻倍,loss 降多少」——而因为它远小于 1,答案是「降一点点,但确实在降」

幂律有一个非常好用的性质:两边取对数之后,它变成一条直线。把式 13-1 取对数得 log LαN(log Nc − log N)——横轴放 log N,纵轴放 log L,得到的是一条斜率为 αN 的直线。这就是为什么所有讲 scaling law 的图都画在双对数坐标纸上:只有在那张纸上,规律才显示成一条直线;而直线是可以外推的。

打个比方:磨刀

幂律描述的是一种「回报递减但永不为零」的关系。像磨一把刀:磨第一分钟,钝口明显变利;磨到第十分钟,还在变利,但你已经很难感觉出来;磨到第一百分钟,仍然在变利。每一次「翻倍投入」换来的改善是同样大小的一小步——从 1B 到 2B 降的 loss,和从 100B 到 200B 降的一样多。

类比失效处:刀有一个物理上的锋利极限,磨到某一步就再也磨不动了;而幂律在数学上没有下界,N 趋于无穷时 L 趋于 0。真实模型当然有下界(自然语言本身有不可预测的部分),所以后来的工作在式子里补了一个常数项——13.2 节会见到它。这个「补一个常数项」的动作本身就说明:幂律是经验拟合,不是物理定律。

一句话记住:幂律意味着尺寸是一条连续曲线上的取样点,不是几个孤立的档位。世界上没有一条规律说「模型就该做成 7B、13B、70B」——那些数字是人挑的,而挑的依据在曲线之外。

这一点值得多停一秒,因为它和很多人的直觉相反。看到 4B / 8B / 32B / 70B 这一串数字,人很容易以为它们像鞋码一样是某种自然分档。不是。曲线上每一个点都是合法的,你完全可以训一个 19.3B 的模型。真正让某些数字反复出现的,是曲线之外的约束——显卡有多大、集群有几张卡、张量并行要能整除。这一章的最后一节就是拆这件事。

(a) 幂律画在双对数坐标纸上是什么形状?(b) 如果从 1B 涨到 2B 让 loss 降了 0.05,那么从 64B 涨到 128B 大约会降多少?(c) 从这个答案能推出「参数越多越好,没有上限」吗?

先想:幂律两边取对数会变成什么?「1B 到 2B」和「64B 到 128B」这两步,在对数横轴上分别跨了多长?
关键在于:对数把「乘法」变成「加法」。翻倍在对数轴上永远是同样长的一步,不管你从哪里开始翻。而直线上等长的一步,对应等量的纵向下降。
第一步这样走:把式 13-1 两边取对数,写成 log L = 常数 − αN log N,然后看 log N 增加同样的量时 log L 会怎么变。
完整答案:(a) 一条直线,斜率是 αN。(b) 也是大约 0.05(严格说是 log 尺度上等量下降,在 loss 已经很小时换算成绝对值会略小,但同一个量级)。因为「翻倍」在对数轴上是固定长度的一步,直线上固定长度的一步对应固定的下降。(c) 推不出。这里有两个坑:其一,幂律说的是「还在降」,没说「降得值」——从 64B 到 128B 你付出了 64B 的参数(对应的钱、卡、时间全都翻倍)只换来和当初 1B→2B 一样的改善。其二,式 13-1 是在某个数据量、某个训练设置下拟合出来的,把 N 一路外推到远离拟合区间的地方,规律本身可能就不成立了——事实上下一节讲的 Chinchilla,纠正的正是这类外推带来的偏差。

变式:有人画了一张图,横轴是参数量(普通刻度,从 0 到 1000B),纵轴是 loss,然后指着曲线尾部说「你看,已经趋平了,再大也没用」。他的图有什么问题?(答案:普通刻度会让幂律看起来「趋平」,那是坐标的错觉,不是规律的性质。同一组数据画在双对数纸上是一条没有拐点的直线。看到「趋平」的结论,先问一句坐标轴是什么刻度。

13.2 Chinchilla:给定训练算力,参数和数据该怎么分

Kaplan 那篇解决了「更大更好」,但没有回答一个更实际的问题:预算是固定的,钱该花在哪一边?训练一个模型,你可以选择「参数多、看的数据少」,也可以选择「参数少、看的数据多」——同样的算力,两种花法。

要回答它,先得知道算力怎么算。这里有一条极其好用的估算式:

C ≈ 6N D
式 13-2
符号是什么直觉
C训练总算力,单位是浮点运算次数(FLOPs)整场训练一共要做多少次乘法和加法。它正比于你要花的钱和时间
N参数量第 12 章数出来的那个数
D训练数据量,单位是 token(模型看过多少个词片段)模型这辈子读了多少字
6不是魔数:前向每个参数约 2 次运算(一乘一加),反向约是前向的 2 倍,合计约 6下面的推导线会把它推一遍。记住它是估算,真实系数受注意力项和实现细节影响,量级正确即可

有了这条式子,「预算固定」就成了一条约束:ND 的乘积被钉死了,一个大另一个就得小。Hoffmann 等人在 arXiv:2203.15556(后来大家用它训出的模型名字叫它 Chinchilla)里做的事,就是在这条约束下找 loss 最低的那一组 (N, D)。他们用的损失模型长这样:

L(N, D) = E + A/Nα + B/Dβ
式 13-3
符号是什么直觉
E常数项,是这个式子的下界就算模型无限大、数据无限多,loss 也降不到它以下。它代表自然语言里本来就不可预测的那部分——下一个词有时候真的可以是好几个不同的词
A/Nα因为模型不够大而多出来的损失房子太小,装不下该学的东西。加参数能压它
B/Dβ因为数据不够多而多出来的损失书读得太少。加数据能压它
α, β两个幂次,论文从实验里拟合出来它们数值上很接近——这是整个结论的来源:两项衰减得差不多快,所以该等比例地喂它们

C = 6ND 固定的约束下最小化式 13-3,得到的结论就是那句被引用了几万次的话:参数量和数据量应该等比例放大。换算成好记的形式是——

一句话记住:Chinchilla 的经验法则是每 1 个参数配约 20 个训练 token。一个 27B 的模型「计算最优」的数据量约是 5470 亿 token;一个 2.42T 的模型是 48.4 万亿 token
为什么这在当年是一次「修正」

Kaplan 那篇的分析倾向于「把预算更多地花在参数上」——按它的建议,模型该做得更大、数据可以少些。Chinchilla 用一组新的实验说:之前那一代模型普遍训得太少了。论文自述的证据是,在相同的训练算力下,一个参数少得多、但 token 多得多的模型,表现优于当时那些参数量大得多、token 却只有几千亿的对照模型。

为什么两篇会得出不同结论?主要分歧在学习率调度等训练细节上——Kaplan 的一些扫描里,小模型没有按各自的最优设置训到位,于是显得「小模型不行」,从而把最优点推向了更大的模型。这件事有一个通用教训:scaling law 是拟合出来的,拟合的质量取决于每一个数据点是不是都被公平地训练过。

本站在这里刻意不给具体系数

式 13-3 里的 E, A, B, α, β 五个系数,Chinchilla 论文给了一组拟合值。本站正文不逐一列出它们,因为本站没有逐位复核过原文的表格,而抄错一个系数会让后面所有推论跟着错。要用具体数值请回原文(arXiv:2203.15556)查。
本章后面那个实验室会用一组公开发表的系数,界面上会标明出处——它们不是 Qwen 的数据,这一点下一节还会再强调一次。

自己推一遍:6 是怎么来的,以及推理那一项该配几

  1. 前向传播时,一个权重参数参与几次浮点运算?(提示:矩阵乘法里,每个权重被用来做什么?)

    想好了再看

    约 2 次:一次乘法(权重 × 输入)、一次加法(把结果累加进输出)。所以一个 token 走完整个模型的前向,浮点运算约是 2N。当初会从「一个权重」而不是「一层」开始数,是因为这样数不依赖具体架构——不管你是 attention 还是 FFN,只要是矩阵乘法,每个权重就是一乘一加。

  2. 反向传播呢?为什么它不是和前向一样多?

    想好了再看

    反向要算两组梯度:一组是对输入的梯度(要往前一层传),一组是对权重的梯度(要拿来更新)。每一组各自的运算量都和一次前向相当,所以反向约是 4N。前向 2 + 反向 4 = 6,这就是式 13-2 里那个 6。

  3. 现在换个场景:模型已经训好,上线服务。生成一个 token 要多少次运算?

    想好了再看

    约 2N——只有前向,没有反向,因为不需要更新任何参数。这一步是本章的枢纽:训练时每个 token 要 6N,推理时每个 token 只要 2N,两者相差 3 倍。正因为推理便宜,它才容易被忽略;也正因为它便宜但会重复无数次,忽略它才会出大错。

  4. 假设这个模型上线之后一共要处理 Dinf 个 token。把训练和推理的账加起来,总成本是什么?

    想好了再看

    C = 6N Dtrain + 2N Dinf注意这两项里 N 都在。训练那一项里,NDtrain 可以互相换(这正是 Chinchilla 在做的取舍);但推理那一项里,N单独乘上去的,而且乘的是一个可能极大的数。这就意味着:Dinf 越大,N 的「单价」越高——最优解会被推向更小的 N

  5. 最后一问:如果一个模型永远不上线(比如只是一次科研实验),Dinf = 0,这条式子会退化成什么?

    想好了再看

    退化成 C = 6N Dtrain,也就是式 13-2,也就是 Chinchilla 的那个约束。Chinchilla 不是错的,它是 Dinf = 0 这个特例下的正确答案。这是理解下一节最重要的一句话:新结论没有推翻旧结论,它把旧结论包成了自己的一个边界情形。

按 Chinchilla 的 20 tokens/param:(a) Qwen3.8-27B(27.36 B 参数)计算最优的训练数据量是多少 token?(b) 2.4T-A95B 呢(按总参数 2.420 T 算)?(c) 第二个数换算成书大概是多少本?(按一本 30 万字、一个汉字约 1 个 token 粗估)

先想:这就是一个乘 20 的动作。难的不是算,是算完之后感受一下那个数有多大
关键在于单位换算:B = 十亿,T = 万亿。27.36 B × 20 得到的还是 B 量级;2.420 T × 20 得到的是几十 T。
第一步这样走:27.36 × 20 = 547.2,单位跟着 B 走。2.420 × 20 = 48.4,单位跟着 T 走。第三问用 48.4e12 ÷ 3e5。
完整答案:(a) 约 5471 亿 token(精确算 27,355,639,808 × 20 = 547,112,796,160)。(b) 约 48.4 万亿 token。(c) 48.4e12 ÷ 3e5 ≈ 1.6 亿本书
第三问才是重点:48.4 万亿 token 这个数,很可能超过了人类产出的全部高质量文本。公开讨论里常见的估计是整个可用互联网文本在几十万亿 token 的量级,而其中「高质量」的部分要小得多。所以对 2.4T 这种规模的模型,「按 Chinchilla 训到计算最优」这件事在数据侧可能根本做不到——这是 2022 年那条结论在 2026 年遇到的真实约束,也是合成数据这个方向存在的原因之一。
另外一个陷阱:对 MoE 模型,这里的 N 该取总参数(2.420 T)还是激活参数(95.29 B)?取后者算出来只有 1.9 万亿 token,差 25 倍。这个问题本章后面会专门讨论,而且没有干净的答案。

变式:反过来,如果一个团队公开说自己的 27B 模型训了 15 万亿 token,那它是 Chinchilla 最优的多少倍?(答案:15e12 ÷ 547e9 ≈ 27 倍。远超计算最优——这正是 13.3 节要讲的「明知不是最优也要这么训」的做法。)

用式 13-2 估算:训练一个 27.36 B 参数、5471 亿 token 的模型,需要多少次浮点运算?如果有人给你一个「总算力 1×1024 FLOPs」的预算,按 Chinchilla 该训多大的模型?

先想:第一问直接代 C = 6ND。第二问要联立两条式子——一条是预算约束,一条是 Chinchilla 的比例关系。
关键在于第二问:你有两个未知数 ND,也有两个方程——C = 6NDD = 20N。代进去消掉一个。
第一步这样走:把 D = 20N 代入 C = 6ND,得 C = 120N2,于是 N = √(C/120)
完整答案:第一问 C = 6 × 2.736×1010 × 5.471×1011 ≈ 8.98×1022 FLOPs。
第二问:N = √(1024/120) = √(8.33×1021) ≈ 9.13×1010,也就是约 91 B 参数,配 D = 20N ≈ 1.83×1012,约 1.8 万亿 token。验算:6 × 9.13×1010 × 1.83×1012 ≈ 1.00×1024,与预算对得上。
顺带记住这个形状:预算翻 4 倍,最优参数量只翻 2 倍(因为 N ∝ √C)。这解释了为什么模型尺寸的增长看起来总比算力投入慢。
边界提醒:这条式子有两个前提。① 它忽略了注意力那一项(注意力的运算量还依赖序列长度,不只依赖参数量),序列很长时会低估。② 它假设每个参数每个 token 都被用到——对 MoE 不成立,MoE 每个 token 只走一小部分专家,此处的 N 更接近激活参数而不是总参数。

变式:如果一个模型是 MoE,总参数 2.42 T、激活 95.29 B,用哪个数代进 C = 6ND 才对?(答案:算训练算力时该用激活参数,因为反向传播也只经过被激活的那些专家。但这样一来「参数量」和「算力」就解耦了,Chinchilla 那条 20:1 的比例还成不成立,就成了一个开放问题——见本章的研究课题。)

13.3 转折点:Chinchilla 算的是训练,不是推理

2023 年 2 月,LLaMA 那篇论文(arXiv:2302.13971)里有一段话,把这件事整个翻了个面。原文写的是:

LLaMA 论文原文(arXiv:2302.13971)

this objective disregards the inference budget」——这个目标函数无视了推理预算

although it may be cheaper to train a large model to reach a certain level of performance, a smaller one trained longer will ultimately be cheaper at inference」——虽然训一个大模型达到某个性能水平可能更便宜,但一个训得更久的小模型,在推理上最终会更便宜

we find that the performance of a 7B model continues to improve even after 1T tokens」——他们发现,一个 7B 模型在吃过 1 万亿 token 之后仍在继续变好

把这三句话连起来读,逻辑链是完整的:Chinchilla 优化的是「训到某个水平最省」,而现实中你要付的是「训 + 用」的总账;训练只发生一次,推理会发生上亿次;所以当推理量大到一定程度,把预算从「模型更大」挪到「训得更久」是划算的。而第三句是这个策略成立的前提——如果小模型在 Chinchilla 点之后就不再变好,那多训也没用。他们观察到它还在变好。

这不是一家的做法。Llama 3 那篇(arXiv:2407.21783)把这件事写得更直白:「we also train our smaller models for much longer than is compute-optimal」——我们把小模型训得远远超过计算最优。注意「much longer than is compute-optimal」这个措辞:他们明确知道自己偏离了 Chinchilla,而且是故意的。

过度训练over-training):明知超过了计算最优点,仍然继续用更多 token 训练一个较小的模型。Gadre 等人(arXiv:2403.08540)研究的正是这个区间——他们的结论是,在过度训练的区间里 loss 仍然可以被可靠地预测。这一条很关键:它意味着「小模型灌更多数据」不是碰运气,是可以事先算的。

把推理写进目标函数

Sardana 等人在 arXiv:2401.00448Beyond Chinchilla-Optimal)里做的事,就是把上面那段道理写成数学。目标函数从「训练算力」换成「训练 + 推理的总算力」:

C = 6N Dtrain + 2N Dinf
式 13-4
符号是什么直觉
6N Dtrain训练成本。前向 2 + 反向 4一次性支出。训完就不再花了
2N Dinf推理成本。只有前向,所以是 2 不是 6按次计费。模型活多久,这一项就累加多久
Dinf模型一生要处理的 token 总数(预期请求量 × 每次请求的长度)这一章新引入的唯一一个变量。它不是模型的属性,是业务的属性——同一个模型,放在不同场景里,这个数能差好几个数量级
N参数量注意它在两项里都出现。这就是「模型大小」这个决定同时影响两笔账的原因

看清楚这条式子的结构,结论几乎是自明的。在 Chinchilla 的世界里(Dinf = 0),N 变大的代价可以靠 Dtrain 变小来抵消,两者是可以互换的。但第二项里 N 没有搭档可以换——Dinf 是业务给的、你改不了,所以第二项完全由 N 决定。Dinf 越大,这一项的权重越高,最优解就越往「小 N 、大 Dtrain」那一边挪。

一句话记住:预期请求量越大,最优点越偏向「更小的模型 + 更多的训练 token」。Chinchilla 没有错,它只是 Dinf = 0 那个特例的答案;而真实世界里 Dinf 从来不是 0。

怎么玩:固定一个总算力预算,然后把「预期推理请求量」这根滑块从 0 往右拖。左边的曲线是「在这个预算下,不同 N 对应的最终 loss」,那条曲线的最低点就是最优尺寸。你会看到最低点随着请求量增大而持续左移——同样的预算,你被推着去选一个更小、训得更久的模型。请特别注意两件事: 曲线在最低点附近是很平的,也就是说选在最优点旁边一点点,代价几乎为零——这解释了为什么现实中大家可以按显卡容量而不是按理论最优去挑尺寸。 请求量要涨到相当大(几万亿 token 量级)才会让最优点明显移动,所以这条修正对个人用户没意义,对要服务上亿次请求的平台才是决定性的。

这个实验室用的不是 Qwen 的数据

必须说清楚:这个实验室里的曲线,用的是公开论文里的拟合形式和公开系数(式 13-3 那个三项式,以及 Beyond-Chinchilla 的目标函数),不是 Qwen 的数据Qwen 从未公开过自己的 scaling law 系数,也没有发布过 Qwen3.8 的技术报告。所以:

  • 你在这个实验室里看到的趋势(请求量增大 → 最优尺寸左移)是可信的,它是式 13-4 的结构性质,不依赖具体系数。
  • 你看到的具体数值(最优点正好落在多少 B)不可迁移到 Qwen3.8。别拿它去「验证」27B 是不是选对了——那需要 Qwen 自己的数据,而那份数据不存在于任何公开材料里。

Llama 3 的作者明确写了「我们把小模型训得远超计算最优」。既然他们知道这不是计算最优,为什么还要这么做?请用「训练算力」和「推理算力」这两个词,给出一个两句话的解释。

先想:「计算最优」这四个字里的「计算」,指的是哪一笔计算?发生几次?
关键在于:Chinchilla 的最优是只针对训练算力的最优。训练发生一次,推理会发生上亿次——一个只优化了前者的方案,在总账上可能很糟。
第一步这样走:写下式 13-4,问自己「如果我把 N 砍小一半,两项分别怎么变」。第一项可以靠加 Dtrain 补回性能,第二项直接减半。
完整答案:第一句——「计算最优」只优化了训练算力这一笔一次性支出,而模型上线后每处理一个 token 都要按参数量付一次推理算力,这笔钱会重复上亿次。第二句——所以把参数量压小、用更多训练 token 把性能补回来,虽然训练那一笔变贵了,但推理那一笔按比例永久变便宜,总账更划算。
补充两点让答案更完整: 这个策略成立的前提是「小模型多训还能继续变好」,LLaMA 论文报告 7B 吃过 1T token 后仍在改善,Gadre 等人(arXiv:2403.08540)进一步证明过度训练区间的 loss 仍然可预测。如果小模型早早饱和,这条路就走不通。 受益方不只是厂商——参数少的模型也意味着用户能在更小的显卡上跑它。推理侧的修正,本质上是把成本从「用户的显卡」这一侧也一起考虑了进来。

变式:如果一个模型只训来做一次性的科研实验,永远不上线服务,那该按哪套标准选尺寸?(答案:Dinf ≈ 0,式 13-4 退化成式 13-2,按 Chinchilla 选就是对的。推理侧修正的适用条件是「会被大量使用」,不是普适真理。

有人说:「既然小模型多训 token 更划算,那干脆训一个 1B 的模型灌 100 万亿 token,就能顶替 27B 了,还省 27 倍推理成本。」请找出这句话里的两处问题。

先想:式 13-3 里有三项,加数据能压掉哪一项?压不掉哪一项?
关键在于 A/Nα 这一项——它只跟参数量有关,数据加到无穷也压不动它一分。另外,「更划算」这个结论有一个前提条件,他悄悄丢了。
第一步这样走:把 D → ∞ 代进式 13-3,看 loss 会趋向什么。那个极限值和一个 27B 模型能达到的 loss 比,谁高?
完整答案:问题一:参数量项压不掉。D → ∞ 代进式 13-3,B/Dβ → 0,但式子还剩 E + A/Nα一个 1B 模型有一个由它自己的参数量决定的 loss 下限,多少数据都到不了 27B 的水平。「小模型训得久更划算」这句话完整的说法是「在达到同一性能水平的前提下更划算」——一旦目标性能高过小模型的天花板,这个前提就不成立,比较也就不成立了。
问题二:他只算了推理,没算训练。1B × 100T token 的训练算力是 6 × 109 × 1014 = 6×1023 FLOPs,比 27B 按 Chinchilla 训(约 9×1022)还贵近 7 倍。式 13-4 是两项相加,他把第一项当成了免费的。
顺带还有第三个问题:数据可能不存在。100 万亿 token 很可能超过了人类产出的全部高质量文本,这一点在 q13-2 已经算过。
这道题的通用教训:一个「在某区间内成立」的结论被推到区间之外,几乎总会变成谬误。读到任何「所以应该走极端」的推论,先回去找原结论的适用条件。

变式:把上面那句话改成「训一个 20B 的模型灌 15 万亿 token,可能在很多任务上接近 27B」——这句话还有问题吗?(答案:这句话合理得多,因为 20B 和 27B 的参数量项差得不远,多喂数据确实有机会补上,而且 15T token 是现实中做得到的量。同一个论证形式,换一组数字就从谬误变成了合理猜测——差别不在逻辑,在有没有落在适用区间里。)

13.4 回到 Qwen3.8:为什么是 27B 和 2.4T 这两个点

现在可以回答这一章开头那个问题了。但要先说一句让人不太满意的实话:

这一节里哪些是事实,哪些是本站的推断

事实:Qwen3.8 没有技术报告(arXiv 检索零结果),没有官方 GitHub 仓库官方从未解释过为什么选 27B 和 2.4T 这两个尺寸,也从未公布过训练用了多少 token、用的是哪条 scaling law。这些不是本站没找到,是公开材料里确实没有。

本站推断:下面关于「这两个点各自对应一个部署场景」的分析,是本站从可观察的部署数字反推出来的,不是官方说法。它是一种解释,不是唯一的解释,也不能排除别的可能(比如尺寸受限于内部集群的并行配置、或者沿用了上一代的某个模板)。

把可观察的事实摆出来:

表 13-1:两个尺寸各自的部署画像(数字来自本站口径表与公开工具链页面)
Qwen3.8-27BQwen3.8-2.4T-A95B
BF16 权重约 54.7 GB4.40 TiB
4 bit 量化后Q4_K_M 17.11 GB + 视觉塔 0.93 GB = 18.04 GBNVFP4 仍需 8 张数据中心卡(vLLM recipe)
最小可跑硬件一张 24 GiB 消费级显卡BF16 需 24 张 B300,FP8 需 16 张(vLLM recipe)
许可证Apache 2.0(真开源,免费商用)qwen3.8-max 自定义协议(本站称「开放权重」)
模态原生多模态(含视觉塔)纯文本

这张表读下来,两个尺寸对应的不是曲线上随便挑的两个点,而是两个不同的部署场景

27B 这个点,正好卡在单张 24 GiB 消费卡的边界内。Q4_K_M 的权重 17.11 GB(= 15.93 GiB)加上视觉塔 0.93 GB(= 0.87 GiB),一共 16.80 GiB,装进 24 GiB 之后还剩 7 GiB 出头。再扣掉 48 层 DeltaNet 的固定状态(约 0.14 GiB)和框架与激活开销(本站取 1.0–2.0 GiB 的经验区间),留给 KV cache 的是 5.1–6.1 GiB——对应 fp16 KV 约 8–10 万 token 上下文,开 fp8 KV 大约翻倍到 17–20 万。(这段推导见第 5 章,完整的实测边界在第 17 章。)跑得起来,但原生 262K 在 24 GiB 上跑不满。

2.4T 这个点,目标显然是数据中心。4.40 TiB 的 BF16 权重,就算量化到 4 bit 也要 8 张数据中心卡。任何个人用户都碰不到它,它服务的是「一次部署、服务上亿次请求」的场景——而按 13.3 节那条修正,正是这种场景下,MoE 那套「总参数很大但激活参数很小」的结构才划算:权重的钱付一次(买卡),激活参数决定每个请求的边际成本。

一句话记住(本站推断):27B 和 2.4T 不是「大号和小号」,是两个部署场景各自的答案——一个的约束是「一张消费卡的显存」,另一个的约束是「一个机柜的吞吐」。它们在 13.3 节那条曲线上不是相邻的两点,而是两条不同曲线(不同 Dinf、不同硬件预算)各自的最优附近。
常见误解:既然 27B 能塞进 24GB,那尺寸就是「按显存倒推」出来的

这个说法把因果说反了一半。倒推的是量化后的体积,不是参数量本身。参数量在训练之前就定死了(第 1 章那四条轴的第 ①条),而「量化到 4 bit 之后是 17 GB」这件事,要等训练完、量化完才知道。设计者当然可以预先估算「27B 在 4 bit 下大约 17 GB」——这个估算用的正是第 0 章那条「字节数 = 参数个数 × 位宽 ÷ 8」——但这仍然是一个训练前的预测,不是训练后的调整。
更要紧的是:本站无法证实设计者真的做过这个考虑。「27B 恰好落在 24 GiB 内」是一个可观察的巧合,把它说成设计意图,需要官方的说明——而那份说明不存在。

要把 Chinchilla 的 20 tokens/param 用到 Qwen3.8-2.4T-A95B 上,N 该取总参数(2.420 T)还是激活参数(95.29 B)?请分别算出两种取法得到的训练 token 量,说明各自的理由,然后回答:本章为什么不给一个答案?

先想:Chinchilla 那条式子里,N 是通过哪两条路径起作用的?一条是「模型有多大容量」,另一条是「每个 token 要花多少算力」。对稠密模型这两件事是同一个数,对 MoE 呢?
关键在于:MoE 把「容量」和「每 token 算力」解耦了。式 13-3 里的 A/Nα 描述的是容量,式 13-2 里的 N 描述的是算力——在稠密模型上它们碰巧是同一个 N,Chinchilla 从没区分过它们。
第一步这样走:分别算 2.420e12 × 20 和 95.29e9 × 20,看两个数差多少倍,再问自己「这两个数分别在回答什么问题」。
完整答案:取总参数得 48.4 万亿 token;取激活参数得 1.9 万亿 token。差 25.4 倍——这不是估算误差,是两个完全不同的结论。
取总参数的理由:式 13-3 的 A/Nα 项描述的是「模型能装下多少东西」,而 MoE 的 512 个专家全都在学东西、全都要被数据喂饱,所以容量该按总参数算。
取激活参数的理由:式 13-2 的 C = 6ND 里,N 代表的是「每个 token 要做多少运算」,MoE 每个 token 只走 11 个专家,所以算力该按激活参数算。
两个理由都对,因为它们回答的本来就是不同的问题。Chinchilla 是在稠密模型上拟合的,那里「容量」和「每 token 算力」是同一个数,所以论文里根本没有必要区分——一个从未被区分过的概念,遇到 MoE 就分叉了
本章不给答案,是因为公开材料里没有一个被普遍接受的答案,而编一个出来比承认不知道更糟。这正是本章研究课题要做的事。

变式:如果有一个折中的取法,比如取「总参数和激活参数的几何平均」(√(2.42×1012 × 9.53×1010) ≈ 4.8×1011,约 480 B),你会接受它吗?(答案:不该在没有证据的情况下接受。几何平均看起来「居中因而合理」,但它没有任何理论依据——它只是把一个「不知道」包装成了一个数。凭空造一个居中的数字,是这类问题里最有迷惑性的错误做法。

官方从没说过 27B 这个尺寸是怎么定的。请列出三条你在本站材料里能观察到的事实,写出每一条分别支持什么推断,然后指出哪一条最弱、为什么。

先想:什么算「可观察的事实」?——能在公开文件(config.json、模型卡、量化仓库的文件大小、推理框架的 recipe)里直接读到的东西。什么算推断?——你从那些事实往前多走的一步。
关键在于给每条事实单独配一个推断,然后逐条问:还有没有别的解释也能产生同一个观察?能产生别的解释越多的那条,就越弱。
第一步这样走:先只写事实,一条也不许带形容词。比如「Q4_K_M 文件 17.11 GB,mmproj 0.93 GB」是事实,「所以它是为消费卡设计的」是推断。分栏写完再连线。
完整答案(一种写法):
事实 1:Q4_K_M 权重 17.11 GB + 视觉塔 0.93 GB = 18.04 GB,落在单张 24 GiB 卡内,且还剩 5.1–6.1 GiB 能给 KV。→ 推断:目标部署环境包含单张消费级显卡。
事实 2:27B 是 Apache 2.0,2.4T 是自定义协议且对大体量商用另设条件。→ 推断:两个模型面向的使用者不同,前者鼓励自由分发与二次开发。
事实 3:27B 带视觉塔(原生多模态),2.4T 是纯文本。→ 推断:27B 面向的是端侧/单机的通用场景,2.4T 面向的是文本吞吐。
最弱的是事实 1 支撑的那条推断,因为「恰好装得下」这个观察有多种成因:可能是设计意图,也可能是先定了尺寸、量化技术恰好把它压进了这个区间,还可能是尺寸受限于内部训练集群的并行配置而与显卡无关。同一个观察能被多个原因产生,就是「弱」的定义。相比之下事实 2 和 3 是官方主动做出的选择(协议是写下来的,视觉塔是训进去的),推断链更短。
这道题真正在训练的东西:把「我看到什么」「我因此认为什么」「我不知道什么」写成三行,而不是揉成一句。第 18 章会把这个动作变成一整套读评测的方法。

变式:如果明天官方发了一篇技术报告,说「27B 的尺寸是按内部集群的 8 路张量并行倒推的,跟消费卡无关」,你上面哪几条要改?(答案:事实一条都不用改——事实还是事实。要改的是推断:第 1 条的推断被推翻,第 2、3 条不受影响。把事实和推断分开写的全部好处,就在被推翻的这一刻显现出来——你只需要划掉一行,而不是重写整段。)

13.5 「黄金尺寸」这个说法要小心

社区里流传着一个说法:27B / 32B 是「黄金尺寸」——大到够用,小到能在一张消费卡上跑。这个说法有它的道理,但值得拆开看一句:它黄金在哪?

把 13.4 节那笔账倒过来算就清楚了。先量出一个关键的经验值:Q4_K_M 的文件 17.11 GB 对应文本塔的 26.90 B 参数,也就是每个参数平均约 5.1 个比特(17.11×109 × 8 ÷ 26.90×109 ≈ 5.09)。这个数比「4 bit 量化」听起来的 4 大,因为 Q4_K_M 是混合位宽——一部分张量用了 6 bit(第 15、16 章会讲)。有了它,就可以从显卡容量倒推出「这张卡的黄金尺寸」:

N黄金 ≈ (MM留给 KV 与开销) × 8 / b有效位宽
式 13-5(本站推导)
符号是什么本站取值
M显卡显存容量。注意厂商标的「24GB」实际是 24 GiB按卡型而定
M留给 KV 与开销KV cache + DeltaNet 固定态 + 框架与激活开销24 GiB 卡上取 6 GiB(本站经验区间的中值)
× 8字节换算成比特——
b有效位宽量化后平均每个参数占几个比特约 5.0–5.1(由 Q4_K_M 实测文件体积反推)
表 13-2:不同显卡容量对应的「黄金尺寸」(本站推导,按 5.0 bit/参数、留 6–7 GiB 给 KV 与开销)
显卡权重预算推出的参数量市面上对应的常见档位
16 GiB约 11 GiB19 B14B / 20B 这一档
24 GiB约 18 GiB31 B27B / 32B —— 就是那个「黄金尺寸」
32 GiB约 26 GiB45 B目前这一档的模型还不多
48 GiB约 41 GiB70 B70B 这一档
一句话记住:黄金的不是「27B」这个模型常数,是「24 GiB」这个硬件常数。27B 之所以看起来黄金,是因为它是 24 GiB 这个数经过「量化位宽」和「KV 预算」两次换算之后的像。换一张卡,这个像就跟着动。

这件事有一个直接的推论:「黄金尺寸」是一个会移动的靶子。如果哪一天 32 GiB 成为消费卡的主流,表 13-2 告诉你新的黄金尺寸会挪到 45B 附近,那时候「27B 是黄金尺寸」这句话就会像今天听「8B 是黄金尺寸」一样过时。而这个移动的时间表,取决于显存芯片的价格,跟模型研究一点关系都没有。

表 13-2 的三个假设,缺一条都会让数字变

有效位宽 5.0 bit——如果你用的是 Q3_K_M(约 3.4 bit)或者 Q6_K(约 6.6 bit),同一张卡的黄金尺寸会差出接近 2 倍。② 留 6 GiB 给 KV 与开销——这是本站按 27B 的实际情况取的经验值;上下文要求更长的话这一项要大得多,黄金尺寸就得往下压。③ 模型是稠密的——MoE 的权重全部要驻留显存但每次只用一小部分,这条式子对它给出的是「装得下」的上限,不是「跑得划算」的最优。这三条里任何一条被换掉,表里的数字都得重算。这也是为什么本站把它标成推导而不是结论。

假设三年后消费级显卡主流变成 48 GiB。(a) 用式 13-5 预测新的「黄金尺寸」。(b) 一个 70 B 的稠密模型,按第 12 章的规则,它的 BF16 权重大概多大?(c) 你这个预测依赖三个假设,把它们写出来,并指出哪一个最不牢靠。

先想:(a) 直接代式 13-5,注意留给 KV 的那一项也该跟着涨一点(卡大了,人们会想跑更长的上下文)。(b) 用第 0 章那条「字节数 = 参数个数 × 位宽 ÷ 8」。
关键在于 (c):一个外推的可信度,取决于它假设了什么「保持不变」。这里至少有三样东西被默认不变了——量化技术、上下文需求、以及模型是稠密的。
第一步这样走:(a) 48 − 7 = 41 GiB 权重预算 = 44.0×109 字节,乘 8 除以 5.0。(b) 70×109 × 2 字节。
完整答案:(a) 41 GiB × 1.0737×109 ≈ 4.40×1010 字节,×8 ÷ 5.0 ≈ 7.0×1010,也就是约 70 B。(b) 70×109 × 2 = 140 GB——注意这说明 70B 模型的 BF16 版本在 48 GiB 卡上装不下,必须量化才能跑,这和今天 27B 的处境完全一样。
(c) 三个假设: 量化的有效位宽还是 5 bit 左右。 留给 KV 与开销的还是 6–7 GiB。 模型是稠密的。
最不牢靠的是第 ①条,理由有两个方向:量化技术一直在进步(第 16 章会看到 NVFP4 这类新格式),如果三年后 3 bit 的质量损失变得可接受,同一张卡能装的参数量会涨 60% 以上;反过来,如果发现低位宽在长思维链任务上损失更大(这是一个已有研究在讨论的方向),主流可能反而回到 6 bit 附近,黄金尺寸会缩水。第 ②条次之——它随「人们想要多长的上下文」变,而那是需求侧的事,比技术更难预测。第 ③条最稳,但它也意味着这个预测对 MoE 模型完全不适用
这道题真正的训练点是最后这一问:做外推时,把假设写出来比把数字算准更重要。数字错了可以重算,假设没写出来,别人连怎么反驳你都不知道。

变式:如果三年后主流不是「一张 48 GiB 的卡」,而是「两张 24 GiB 的卡」,黄金尺寸会一样吗?(答案:不一样,而且会更小。两张卡之间要通过 PCIe 传数据,张量并行会带来通信开销,还要各留一份框架开销;总容量虽然相同,可用于权重的部分更少,而且有些层的切分方式会要求维度能被 2 整除。「总显存」和「单卡显存」是两个不同的约束——这也是为什么消费级场景一直特别在意「单卡能不能跑」。)

答辩:如果我是审稿人

你说 27B「正好卡在 24 GiB 卡的边界内」,暗示这是设计意图。可这是典型的事后合理化——任何一个尺寸,我都能找到某张卡去配它。20B 配 16 GiB、40B 配 32 GiB,一样成立。你凭什么说这个巧合有意义?

参考防守(先自己组织语言再看)

这个质疑必须先认下来:本站确实拿不出证据证明这是设计意图,正文里那个 caution 框就是为此而写的。官方没有技术报告,没有解释过尺寸选择,本站说的是「可观察的事实」加「一种解释」,不是「事实」。
但「任何尺寸都能找到某张卡」这个反驳也不完全成立,理由是约束不是单边的。27B 这个点同时满足了四件事:① 4 bit 后落在单卡 24 GiB 内;② 落进去之后还剩得下一个能用的上下文(8–10 万 token,不是剩 0.5 GiB 那种勉强);③ 它带着一座 0.93 GB 的视觉塔,也算进去了还装得下;④ 官方在发布日就提供了 FP8 官方权重,而 FP8 恰好是数据中心卡上另一条部署路径。一个尺寸要同时满足这几条,可选区间没有那么宽。
不过审稿人有一点提醒得非常对:「区间不宽」不等于「就是为它设计的」。还有别的可能没被排除——比如尺寸受限于内部集群的并行度、或者沿用了上一代的某个模板。所以正确的表述是:27B 落在消费卡边界内是可核查的事实;它是不是被这个约束选出来的,本站不知道,也不打算假装知道。愿意接受这个结论有多弱,正是它值得被相信的原因。

答辩:如果我是审稿人

Chinchilla 是 2022 年的论文,LLaMA 是 2023 年的。到 2026 年,数据质量、架构、训练方法全都变了。你拿四年前的曲线来解释今天的模型,是不是刻舟求剑?

参考防守(先自己组织语言再看)

要分两层回答,因为这两层的答案不一样。
结论的形式是稳的,具体的常数不稳。「loss 随规模呈幂律下降」「参数和数据要一起放大」这类结构性结论,到今天仍然是所有人做规划时的出发点。而 E, A, B, α, β 那几个系数、以及那个 20:1 的比值,确实会随数据质量和架构改变——数据质量提高,同样的 token 数值更多;架构变了(比如 MoE、混合注意力),拟合出来的系数也不一样。所以今天没有人会把 20:1 当成必须遵守的配方,它更像一把尺子:用来判断「这个模型是训得偏少还是偏多」,而不是用来下命令。
更要紧的一点:Chinchilla 的目标函数本身在 2024 年就被改写了(式 13-4)。所以我们引用它,不是把它当成今天的最优答案,而是当成一条线索的起点——这一章的叙事恰恰是「它被修正了两次」,而不是「它现在还对」。
最后,审稿人的质疑其实指向了本章最诚实的那一段:我们无法用任何 scaling law 去验证 Qwen3.8 的尺寸选择,因为 Qwen 的系数、token 数、训练配方一样都没公开。这一章能做的是解释「行业里的人在用什么框架思考尺寸」,不是「Qwen 当初是怎么想的」。

答辩:如果我是审稿人

按你的说法,请求量越大越该用小模型。那 2.4T 这种庞然大物为什么还要存在?按式 13-4,它应该是最不划算的那个才对。

参考防守(先自己组织语言再看)

这个质疑戳中了式 13-4 的一个真实缺口:它优化的是「达到给定性能水平的总算力」,前提是那个性能水平两种方案都够得着一旦目标性能超过了小模型的天花板(q13-5 里算过:A/Nα 那一项是加数据压不掉的),比较就不成立了——你不是在两个方案里选便宜的,你是只剩一个方案。「更划算」的前提是「都能做到」。
第二点,2.4T 是 MoE,它在式 13-4 里的位置和稠密模型不同:推理那一项 2N Dinf 里的 N 该取激活参数 95.29 B,不是总参数 2.42 T。也就是说,从「每个 token 的算力」看,它的推理成本只相当于一个 95B 稠密模型,而不是一个 2.42 T 的。MoE 存在的全部意义,就是在式 13-4 里让容量和推理成本分开定价。
第三点,式 13-4 是算力模型,不是的模型。真实成本里还有一大项它没算:权重要全部驻留显存,2.42 T 意味着几十张卡,那些卡按小时计费,不管此刻算的是哪 10 个专家。所以在低负载下 MoE 反而更贵,在高负载下才摊薄。式 13-4 里那个「请求量越大越划算」的方向,对 MoE 来说是加倍成立的——但方式和稠密模型不一样。

对你而言未知MoE 的 scaling law 里,N 到底该怎么定义?

q13-6 已经把问题摊开了:Chinchilla 在稠密模型上拟合,那里「模型容量」和「每 token 算力」是同一个数,所以论文从来没有必要区分它们;到了 MoE,这两件事差了 25 倍,而 Chinchilla 的式子里只有一个 N 的位置。这个问题有人研究过,答案不在本站的材料里——它属于「答案存在但要自己去找」那一类,而且要小心:不同论文给出的「有效参数量」定义并不一致,你会读到互相冲突的说法,这本身就是这个领域还没收敛的证据。

先做这一步:用关键词 scaling laws for routed language modelsMoE scaling laws effective parameter count 去检索,至少找到两篇给出了「有效参数量」定义的论文,然后做一件很具体的事——把它们各自的定义代到 Qwen3.8-2.4T 上,各算出一个「计算最优 token 数」(你需要的三个数第 12 章都给了:总参数 2,419,802,390,528、激活参数 95,285,559,296、专家数 512 选 10+1)。两个答案大概率不一样。接着问一个更狠的问题:这两个定义在稠密模型上是不是退化成同一个?如果是,说明它们只是在 MoE 上分叉,那分叉点在哪个假设上;如果不是,说明其中至少有一个连稠密情形都对不上。这一步做完,你对「这个领域还没有共识」这句话的理解,会比读十篇综述更实在。

这一层要加什么:一个尺寸规划器

为什么现在才加它:第 12 层的 count_params 是「给定 config,算出多少参数」;这一层反过来——给定预算,算出该用多大的 config。两个方向合起来,你的项目第一次能回答「我该造一台多大的机器」,而不只是「这台机器有多大」。

def plan(C_train, D_inf=0, tok_per_param=20): # 1) Chinchilla 基准解:C = 6ND 且 D = 20N => N = sqrt(C/120) N = (C_train / (6 * tok_per_param)) ** 0.5 D = tok_per_param * N # 2) 把推理成本并进目标函数,重新找最优 N # L(N, D) = E + A/N^a + B/D^b,约束 6*N*D + 2*N*D_inf = C_total # D 由约束反解:D = (C_total - 2*N*D_inf) / (6*N) best = None for N_try in logspace(N / 30, N * 30): # 扫一遍,别解析求解 D_try = (C_train - 2 * N_try * D_inf) / (6 * N_try) if D_try <= 0: continue # 预算全被推理吃掉了 loss = E + A / N_try**a + B / D_try**b if best is None or loss < best[2]: best = (N_try, D_try, loss) return best # (N*, D*, 预测 loss)

难点一:推理项的符号极容易写反。直觉上「推理也要花算力,所以总算力更多」,于是有人写成 D_try = (C_train + 2*N*D_inf)/(6*N)。这一写,请求量越大反而预算越多,最优 N 会跟着变大——结论正好反过来。正确的理解是:C总预算,推理要花的那部分是从这个总数里扣掉的,剩下的才能用来训练。请求量越大,能用于训练的越少,而且这个扣除量正比于 N——所以大模型被罚得更重。

难点二:不要试图解析求解。把式 13-3 代进约束求导,会得到一个没有初等闭式解的方程。数值扫一遍(在对数刻度上取几百个点)又快又稳,而且顺手能画出那条曲线——13.3 节那个实验室的图就是这么来的。这是一个通用经验:能扫就别推,尤其是当你还要把中间过程画给人看的时候。

难点三:D_inf 的单位是 token,不是「请求数」。一次请求可能是 500 个 token,也可能是 50,000 个(长文档)。如果你的接口收的是请求数,必须乘上平均长度再进来。这个错一犯,最优点的移动会差两个数量级——而你看不出来,因为曲线形状是对的,只是标错了横轴。

自己验:三条判据,全部要满足。
D_inf = 0,输入 C_train = 1e24,输出应满足 D/N ≈ 20(误差 5% 以内)且 6*N*D ≈ 1e24(误差 1% 以内)。参考值:N ≈ 9.1e10D ≈ 1.8e12
保持 C_train 不变,把 D_inf 从 0 依次加到 1e9, 1e10, …, 1e12,把每一步的 N* 打出来:这串数必须单调下降,对应的 D* 必须单调上升,而且预测 loss 不上升(它衡量的是「同样总预算下能达到的最好水平」,把推理成本考虑进来只会让分配更合理)。
反向判据(最重要的一条):如果你看到 D_inf 增大时 N* 反而变大,不要去调系数——那说明你把推理项的符号写反了,回去看难点一。这是这一层唯一会「跑得通、图也好看、结论完全相反」的错法。

本章小结

  • 幂律(Kaplan, arXiv:2001.08361):loss 随参数量呈幂律下降,画在双对数纸上是一条直线。这意味着尺寸是连续曲线上的取样点,不是几个自然分档——那些反复出现的数字,是被曲线之外的约束选出来的。
  • ChinchillaarXiv:2203.15556):在训练算力 C ≈ 6ND 固定的约束下,参数和数据应等比例放大,约 20 tokens/param。它修正的是上一代「模型偏大、数据偏少」的做法。
  • 转折点:Chinchilla 优化的只是训练。LLaMA(arXiv:2302.13971)原文点破——「this objective disregards the inference budget」,并指出「a smaller one trained longer will ultimately be cheaper at inference」。Llama 3(arXiv:2407.21783)直言把小模型训得「much longer than is compute-optimal」。
  • 把推理写进目标函数arXiv:2401.00448):C = 6N Dtrain + 2N Dinf预期请求量越大,最优点越偏向「更小的模型 + 更多 token」。Chinchilla 是 Dinf = 0 的特例,没有被推翻,只是被包了进去。
  • 回到 Qwen3.8官方从未解释过尺寸选择(没有技术报告、没有官方 GitHub)。可观察的事实是:27B 量化后 18.04 GB,落在单张 24 GiB 消费卡内(上下文约 8–10 万 token);2.4T 的 BF16 是 4.40 TiB,量化到 4 bit 仍需 8 张数据中心卡。两个点对应两个部署场景——这是本站的推断,不是官方说法。
  • 「黄金尺寸」:黄金的是 24 GiB 这个硬件常数,不是 27B 这个模型常数。按 5.0 bit/参数的有效位宽反推,16/24/32/48 GiB 对应约 19/31/45/70 B——这是一个会随显卡移动的靶子。

这一章解释了尺寸怎么被选出来,但一直绕开了一个更常被问到的问题:既然 27B 和 2.4T 是两个不同的点,那小的那个是不是从大的那个「弄」出来的?剪掉一部分?让大的教小的?还是把大的压缩一下?下一章正面回答它——那是这个站从第 1 章开始就在铺垫的那个核心论点。

第14章 小模型不是大模型切出来的

这一章回答本站从第 1 章起就在铺垫的那个问题:27B 是不是把 2.4T 压小了?答案是不是。而把「不是」讲清楚,需要摆开四件经常被混为一谈的事——独立预训练、剪枝、蒸馏、量化。它们发生在不同的时刻,对参数做的事完全不同,得到的东西也不同。全网大部分关于「模型尺寸」的误解,本质上都是把这四件事压成了一件。

学完这一章你应该能做到

  • 把四件事各自放到时间轴的正确位置上,并说出每一件对参数个数做了什么
  • 解释剪枝为什么必须配蒸馏来「恢复」,以及它和蒸馏为什么不是一回事
  • 说清 Qwen3 的强到弱蒸馏发生在哪个阶段、教的是什么、学生的参数从哪来
  • 用一句论文原话说明「量化一个大模型」和「用一个小模型」是两个不同的选择
  • 拿到一个陌生的 Hugging Face 仓库,用三步判断它和官方模型是什么关系
前置:第0章的浮点数怎么存(14.5 节全靠它)、第1章的四轴表与证据分级、第12章的点钞规则(这一章要拿它当判据用)、第13章的尺寸是怎么被选出来的。

14.1 那个问题:27B 是不是把 2.4T 压小了

不是。

先把答案放这儿,然后再解释为什么这个问题会被问出来。它之所以自然,是因为在别的领域里「大的压成小的」确实是常规操作:一张 4000 万像素的照片可以导出成 100 万像素的缩略图,一段 4K 视频可以压成 720p。人很容易把模型也想成这样——先有一个「原图」,再导出几个不同尺寸的版本。

模型不是这样。27B 和 2.4T 是两套从第一步随机初始化就各自独立的参数。它们之间不存在「谁是谁的导出版本」这种关系,就像两栋同时开工、图纸不同的楼之间不存在谁是谁的缩略图。第 12 章那两张点钞表已经把这件事量化了:它们没有一张矩阵的形状是相同的。

但「不是压小的」只回答了一半。另一半是:业界确实有把大模型变小的技术,而且不止一种。把它们和「独立预训练」摆在同一条时间轴上,误解才真正被拆开。

训练之前 · 定图纸 训练之中 · 从零学 训练之后 · 权重已经固定 预训练完成 ① 独立预训练 两套参数,各自从随机初始化开始 参数个数:各是各的 ② 剪枝 pruning 在权重上动刀 参数真的变少 得到:原模型的子集 ③ 蒸馏 distillation 学生自己的独立预训练 后训练:学教师的输出 学生是另一套参数 ④ 量化 quantization 改位宽 参数一个都没少 得到:同一套参数换记法 注意第 ③ 条:蒸馏那一行在虚线左边也有一段——学生的骨架是它自己训出来的。
图 14-1:四件事在时间轴上的位置。示意图,非官方原始图示。最容易被忽略的是第 ③ 行左边那一段:蒸馏之前,学生已经有了一套自己独立预训练出来的参数。
表 14-1:四条路的骨架对照
做法何时发生参数个数变了吗得到的是
独立预训练训练前定好尺寸,从随机初始化开始训——(各是各的,无从比较)另一套完全独立的权重
剪枝pruning训练之后,在已有权重上砍真的变少原模型的一个子集
蒸馏distillation学生的后训练 / 对齐阶段学生是另一套独立参数学到教师「行为」的另一个模型
量化quantization训练之后,改位宽一个都没少同一套参数,换了记录方式
一句话记住:这四件事里,只有剪枝会让参数个数真的变少。独立预训练根本不在「变小」这个框架里(两套参数从来没有过关系),蒸馏改的是学生的行为而不是它的规模,量化改的是记录方式而不是内容。

用第 12 章的两张点钞表,给出两个具体数字,说明 27B 不可能是 2.4T「压小」出来的。

先想:如果 A 真的是 B 「压小」来的,A 的每一张矩阵应该和 B 的某一张有对应关系。去看两个模型最基本的两个形状参数。
关键在于层数和 hidden。这两个数一旦不同,两个模型的矩阵形状就没有一张能对上——而任何「从大的弄出小的」的做法,都必须能说清每一张矩阵是从哪张来的。
第一步这样走:从 config.json 里读四个数——27B 的 num_hidden_layershidden_size,2.4T 的同两个字段。
完整答案:数字一:层数 64 对 92。27B 有 64 层,2.4T 有 92 层。「压小」没有任何一种做法能从 92 层生成 64 层还保持每层的对应关系——92 不是 64 的整数倍,也没有哪种剪枝会砍掉 28 层还宣称自己是同一个模型的缩小版。数字二:hidden 5120 对 8192。27B 每一层的主干宽 5120,2.4T 是 8192。8192 的 62.5% 是 5120,不是一个自然的剪枝比例,而且如果真是按通道剪出来的,剪完的那一份权重必须是原权重的子矩阵——那样两个模型在词表这一侧的嵌入矩阵会共享数值,可事实是它们的嵌入矩阵一个是 248,320×5,120,一个是 248,320×8,192,没有一张矩阵的完整形状是相同的
还可以补第三个:27B 是稠密 FFN(中间维 17408),2.4T 是 512 个专家的 MoE。这两种结构之间连「对应」这个词都无从谈起。
这道题的方法论:判断「A 是不是 B 变来的」,最硬的判据是形状能不能对上,因为任何变换都必须能说清每一张矩阵的去向。这个判据本章末的建造台阶会做成一个工具。

变式:如果有人说「那 27B 是不是 2.4T 的一个『分支』,比如从某个中间检查点分出来继续训的」?(答案:这个说法比「压小」高明,但同样被形状挡住——分支意味着共享历史,共享历史意味着形状必须相同。层数 64 对 92 就已经排除了它。能共享检查点的,必须是同一张图纸。

14.2 剪枝是什么样的:真的在权重上动刀

为什么会有剪枝这件事

假设你已经花了几百万美元训好一个 15B 的模型,现在需要一个 8B 版本给显存更小的场景用。从零训一个 8B 要再花一次大钱。有没有办法利用已经训好的那 15B?剪枝就是这个问题的答案:把已训好的模型里「不太重要」的部分砍掉,剩下的骨架已经带着大量学好的信息,只要再训一小段就能恢复大部分能力。

剪枝pruning):在一个已经训练完成的模型上删掉一部分权重,让参数量真的变少。它是四条路里唯一一条「直接在原模型的权重上动刀」的。

代表工作是 MinitronarXiv:2407.14679,NeurIPS 2024)。它做的事非常直白:从一个已经训好的 15B 模型剪出 8B 和 4B。论文自述这套做法相比每个模型都从零训练,「requires up to 40× fewer training tokens per model compared to training from scratch」——每个模型需要的训练 token 最多可以少到 1/40。工程实践版(arXiv:2408.11796)把同一套方法用在了别的模型上,比如把一个 8B 剪成 4B、把一个 12B 剪成 8B。

结构化剪枝:砍整个头、整个通道

剪枝有两种砍法,差别很要紧。

非结构化剪枝是把矩阵里单个的、绝对值很小的权重置零。听起来更精细,但有一个致命的工程问题:矩阵的形状没有变,只是里面多了很多 0。GPU 做矩阵乘法时不会因为某个位置是 0 就跳过它——除非有专门的稀疏算子支持。所以非结构化剪枝往往省了参数却不省时间

结构化剪枝structured pruning)砍的是整块:整个注意力头、整个 FFN 的中间通道、甚至整层。砍完之后矩阵的形状真的变小了——比如 FFN 的中间维从 17408 变成 12288,那三张矩阵就都从 5120×17408 变成了 5120×12288。这样得到的是一个更小的、形状规整的模型,普通 GPU 直接就能跑得更快。Minitron 走的是这条路。

打个比方:拆房子

非结构化剪枝像把墙上的砖一块块抠掉——墙还在原来的位置,占地一点没少,只是变得千疮百孔。结构化剪枝像整个拆掉几个房间——楼变窄了,占地真的少了,而且剩下的部分还是方方正正的。

类比失效处:拆房子拆完就是拆完了,剩下的部分性能不受影响;而剪枝砍掉的那些通道,原本是和留下来的通道协同工作的,砍完之后剩下的部分会「失衡」,必须再训一段才能恢复。这就是下一段要讲的事。

为什么剪枝必须配蒸馏

剪完之后模型的表现会明显下降——这几乎是必然的。原因不难理解:模型学到的东西是分布在所有权重上的,你砍掉一部分,剩下的那些权重仍然按照「有同伴在」的假设工作,结果就是各处都对不齐。所以剪枝之后必须有一个恢复阶段。

Minitron 用来恢复的手段,正是蒸馏:让剪枝之前那个完整的模型当教师,教剪完的小模型。这一步很关键,也是混淆的来源之一——在 Minitron 这套流程里,剪枝和蒸馏是同一条流水线上的两道工序,先剪后教。但这不代表它们是同一件事:剪枝改变了参数个数,蒸馏一个参数都没动,它只改变参数的

一句话记住:剪枝是四条路里唯一真的在原模型权重上动刀的。它的产物是原模型的一个子集——矩阵形状按比例缩小,结构保持同构。而这一点,正是本章末那个血统检查器能认出它的原因。

Minitron 证明了「从一个已训好的 15B 剪出 8B」是可行的,而且省下多达 40 倍的训练 token。既然这条路走得通,为什么它仍然不能解释 27B 和 2.4T 的关系?请给出两条理由,其中一条必须是形状上的。

先想:剪枝的产物长什么样?它和原模型在结构上是什么关系?然后去对照 27B 和 2.4T 的结构。
关键在于「同构」这个词:剪枝只是把每一维按比例缩小,层的种类和排列方式不会变。而 27B 和 2.4T 在这一点上就已经对不上了。
第一步这样走:列出两个模型的三样东西——层数、hidden、FFN 是稠密还是 MoE。逐条问:剪枝能不能造成这个差异?
完整答案:理由一(形状):剪枝的产物必须与原模型结构同构——层数可以减、每层可以变窄,但层的种类不会变。2.4T 的每一层挂的是 512 专家的 MoE,27B 挂的是稠密 FFN;没有任何剪枝操作能把一个 MoE 层变成稠密 FFN 层(要么你剪到只剩 1 个专家,那专家的中间维是 2048,而 27B 是 17408,还是对不上)。另外 92 层剪到 64 层、8192 剪到 5120,这两个比例(0.696 和 0.625)也不一致,而结构化剪枝通常会保持一个统一的缩放。
理由二(时间线与事实):剪枝发生在训练之后,需要先有那个 2.4T。而两个模型的权重上线时间只差两天(2.4T 是 8 月 12 日,27B 是 8 月 14 日),27B 还多带一座训进去的视觉塔——从 2.4T(纯文本)剪不出一座视觉塔来。更根本的是:官方从未声称过这两者之间有任何派生关系,而剪枝是一件必须由训练方主动去做、也一定会在模型卡里说明的事。
方法论提示:这道题的两条理由性质不同——理由一是你自己能验证的(打开两份 config 就行),理由二依赖于公开信息。优先用第一类理由,它不需要你信任任何人。

变式:如果某天真的有人从 2.4T 剪出一个 27B 参数的模型,你怎么一眼看出它和官方的 Qwen3.8-27B 不是同一个模型?(答案:看 config。剪枝产物会继承 2.4T 的结构基因——它多半仍是 MoE、hidden 是 8192 的某个约数、层数是 92 的某个子集;而官方 27B 是稠密的、hidden 5120、64 层、还带视觉塔。参数量相同的两个模型,可以在形状上毫无共同之处——这也是为什么「多少 B」这个标签信息量很低。)

14.3 蒸馏是什么样的:以 Qwen3 为例

这一节最重要,因为它最容易被误解——而且误解它的人往往是读过一点资料的人,正因为读过,才会把「Qwen 用了蒸馏」直接接到「所以小模型是从大模型切出来的」上面去。

知识蒸馏Knowledge Distillation, KD):Hinton 等人在 arXiv:1503.02531 提出的做法——让一个已经训好的大模型(教师)产生输出,用这些输出去训练一个小模型(学生)。核心洞见是:教师输出的完整概率分布比标准答案携带的信息多得多。标准答案只说「正确答案是 A」,而教师会说「A 的概率 0.7、B 是 0.2、C 是 0.09」——那个 0.2 告诉学生「B 也挺像的」,这是标准答案里没有的信息。

打个比方:跟一个好老师上课

标准答案像一本只有答案的习题册:这题选 A。教师的完整分布像一位老师在讲解:「答案是 A,但 B 也很有迷惑性,C 完全不沾边。」学生从后者学到的是题目之间的相似结构,不只是答案本身。

类比失效处:真实的师生之间,学生的大脑不是老师给的;而这一点在模型上同样成立,却反而最常被忘掉——学生模型的参数是它自己预训练出来的,教师只提供训练信号,不提供任何一个参数。

Qwen3 的强到弱蒸馏,具体做了什么

上一代的 Qwen3 技术报告(arXiv:2505.09388)里有一节讲 strong-to-weak distillation(强到弱蒸馏),细节相当完整,正好拿来当标本:

  • 对象:论文原文写的是「5 dense models (Qwen3-0.6B, 1.7B, 4B, 8B, and 14B) and one MoE model (Qwen3-30B-A3B)」——五个稠密模型加一个 MoE 模型。
  • 教师:Qwen3-32B 或 Qwen3-235B-A22B。
  • 第一阶段(off-policy):把教师在两种模式(/think/no_think)下的输出合并起来,让学生去学这些回答。
  • 第二阶段(on-policy):学生自己生成序列,然后通过对齐 logits(模型在 softmax 之前输出的那一排原始分数)来微调。
  • 效果:论文自述这套做法「requiring only 1/10 of the GPU hours compared to the four-stage training method」——只需要四阶段训练法 1/10 的 GPU 小时
常见误解:蒸馏就是把 235B 的权重砍成 0.6B

把上面那段读完,最容易产生的误读是:「原来 Qwen3-0.6B 是从 235B 蒸出来的,所以它就是 235B 的一个小号版本。」这是错的,而且错在两个地方。

错处一:时机。这个蒸馏发生在后训练 / 对齐阶段,也就是学生模型已经完成了自己的预训练之后。看图 14-1 第 ③ 行左边那一段——那一段就是学生自己的预训练,它在蒸馏开始之前就完成了。蒸馏省下的那 90% GPU 小时,省的是「教它怎么回答」那一部分(对齐、思维链、指令跟随),不是「建这栋楼」那一部分。

错处二:内容。教师给学生的是输出分布(第一阶段是回答文本,第二阶段是 logits),不是权重。教师的参数一个都没有流进学生。Qwen3-0.6B 的每一个参数,都是它自己在自己的预训练里学出来的,然后在对齐阶段被教师的输出信号调整过。调整参数的值 ≠ 参数来自教师。

一个能帮你记住的说法:蒸馏教的是「怎么回答」,不是「你是谁」。骨架是学生自己的,说话方式是跟老师学的。

引用这篇报告时的两条边界

其一arXiv:2505.09388Qwen3 的技术报告,不是 Qwen3.8 的。Qwen3.8 没有技术报告。上面这段描述的是上一代用过的方法,本站拿它当「蒸馏在工业界具体长什么样」的标本,不能据此推断 Qwen3.8 用了同样的流程。

其二:那篇报告里的 MoE 配置(专家数、每 token 激活几个)与 Qwen3.8-2.4T 的 512 选 10+1 并不相同,两代之间架构有变化。引用这篇报告时不要拿它去支撑 Qwen3.8 的任何架构数字——Qwen3.8 的架构数字只有一个来源:它自己的 config.json。

(a) Qwen3 的强到弱蒸馏发生在哪个阶段?(b) 学生模型的参数是从哪来的?(c) 那句「只需要 1/10 的 GPU 小时」,省下的是哪一部分的算力?

先想:一个模型从无到有要经过几个阶段?「预训练」和「后训练/对齐」分别在做什么?蒸馏被放在了哪一个里面?
关键在于:如果蒸馏发生在后训练阶段,那说明在它开始之前,学生已经是一个能用的模型了——它的参数必须早就存在。
第一步这样走:把「1/10 的 GPU 小时」这句话的对照组找出来——论文比的是「四阶段训练法」,那是后训练的方法,不是预训练的方法。对照组在哪个阶段,省下的就是哪个阶段的钱。
完整答案:(a) 后训练 / 对齐阶段,在学生的预训练之后。(b) 学生的参数来自它自己的独立预训练,教师一个参数都没给它——教师提供的是训练信号(第一阶段是教师的回答文本,第二阶段是 logits)。(c) 省下的是对齐那部分的算力,也就是「教它怎么回答」的那一段;预训练那一大笔钱一分没省,因为学生的骨架仍然要自己从零训出来。
为什么这三问要连着答:只答 (a) 容易变成背时间点,只答 (b) 容易变成背结论。三问连起来会逼出那个关键的因果:正因为蒸馏发生在预训练之后,它省的才只能是预训练之后的那部分开销。如果有人告诉你「蒸馏让小模型的训练成本降到十分之一」,你现在应该立刻反问一句:是总成本的十分之一,还是对齐那一段的十分之一?

变式:假设有人把教师的 logits 蒸给了一个随机初始化的学生(跳过学生自己的预训练),这算不算「把大模型压小」?(答案:仍然不算,但这个问法很好。这时学生确实是从零开始的,只是训练信号来自教师而不是原始文本——它得到的仍然是另一套独立的参数,只是学习材料换了。参数的来源是「训练过程」,不是「教师的权重」,这一点在任何设置下都成立。另外这种做法通常效果不如先预训练再蒸馏,因为教师的输出覆盖不了原始语料的全部多样性。)

14.4 还有第三条路:靠数据配方

前面两条路(剪枝、蒸馏)都需要先有一个大模型。但小模型变强还有第三条路,它谁都不需要:把训练数据本身做好。

Phi-3arXiv:2404.14219)是这条路最有名的例子。它的做法不是蒸馏、不是剪枝,而是在数据配方上下功夫——用大量经过合成与筛选的高质量数据,让一个 3.8B 的模型从头训出明显超出它尺寸的表现。这条路的逻辑回到第 13 章的式 13-3:那个式子里 D 只是 token 的数量,从不区分质量。而现实中同样数量的 token,质量差异可以巨大。数据配方这条路押的就是这一点。

Gemma 2arXiv:2408.00118)提供了一个官方明说的对照。论文原文写的是:「train the 2B and 9B models with knowledge distillation instead of next token prediction」——用知识蒸馏代替下一个 token 预测来训练 2B 和 9B 模型。注意这里的 instead of:它说的是训练目标被替换了,把「预测语料里的下一个词」换成了「匹配教师给出的分布」。

Gemma 2 这个例子恰好演示了「阶段」这件事有多要紧

把 Gemma 2 和 Qwen3 放一起对照,能看到蒸馏的两种不同用法:Qwen3 的强到弱蒸馏用在后训练(教已经预训练好的学生怎么回答),Gemma 2 说的是把蒸馏当成预训练的目标函数(用教师分布代替原始语料的下一词预测)。后者更接近「从一开始就跟着老师学」。

但即使在 Gemma 2 这种更「深」的用法里,结论仍然不变:学生的参数还是学生自己的。换掉的是训练信号的来源(教师分布 vs 语料),不是参数的归属。这也说明「蒸馏」这个词底下其实盖着好几种不同做法,看到这个词时应该追问一句「用在哪个阶段」

三条路在业界是并行存在的,而且经常混用:Minitron 剪完之后用蒸馏恢复;Gemma 2 用蒸馏做预训练目标;Phi-3 两条都不用,只靠数据。它们要解决的是同一个问题——怎么让小模型更强——但走的是完全不同的路。

Gemma 2 官方说用知识蒸馏而不是下一个 token 预测来训 2B 和 9B。有人据此说:「所以 Gemma 2 的 2B 就是从 27B 切出来的。」请指出这个推论的问题,并说明要证实「A 是从 B 切出来的」需要什么样的证据(提示:想想第 12 章那套点钞规则能提供什么)。

先想:「用蒸馏训练」这句话说的是训练信号从哪来,还是参数从哪来?这两件事是一回事吗?
关键在于:教师提供的是「该输出什么分布」,学生的每一个参数仍然是通过梯度下降在自己的矩阵上更新出来的。没有任何一个数值从教师的矩阵里被复制过来。
第一步这样走:想清楚要证实「切出来」需要什么——需要能指出 A 的每一张矩阵是 B 的哪一张的子块。然后问:蒸馏这套流程里,有这样的对应关系吗?
完整答案:问题所在:这个推论把「训练信号的来源」当成了「参数的来源」。蒸馏改变的是损失函数怎么算(从「匹配语料的下一词」变成「匹配教师的分布」),学生的参数仍然是它自己那一套矩阵,靠梯度下降更新出来的。整个流程里没有一次权重复制。
要证实「切出来」需要什么证据:需要一个形状上的对应关系——A 的每一张矩阵,能指认出它是 B 的哪一张矩阵的子块(哪些行、哪些列被保留了)。这正是第 12 章那套点钞规则能提供的东西:把两个模型的张量清单列出来,逐张比形状。剪枝会留下这种痕迹(结构同构、按比例缩小),蒸馏不会——蒸馏出来的学生和教师之间,形状可以毫无关系。
再进一步:光有形状对应也只是必要条件,不是充分条件——一个独立训练的小模型,如果碰巧选了成比例的尺寸,形状看起来也会「像子集」。要真正确定,还需要发布方的说明或者数值上的证据。本章末的建造台阶就卡在这条界线上:它只报「形状是子集」,不报「一定是剪枝」。

变式:Phi-3 靠数据配方让 3.8B 表现超尺寸。有人说「所以数据比参数重要」。这句话有问题吗?(答案:说得太满。Phi-3 证明的是「同样参数量下,数据质量能带来很大差异」,不是「数据可以替代参数」——第 13 章那个 A/Nα 项仍然在,3.8B 有它自己的天花板。「X 比 Y 重要」这类说法,几乎总是把一个「在给定 Y 下 X 有影响」的结论说成了「X 能替代 Y」。

14.5 那量化到底是什么

一句话:训练完之后,把每个数字用更少的位来记。参数一个都没少。

回到第 0 章那一节讲的浮点数。一个参数在 BF16 下占 16 个比特,在 4 bit 量化下占大约 4 个比特。参数的个数完全不变,变的只是每个参数用多长的一串 0 和 1 来记录。用第 0 章那条式子说最清楚:

字节数 = 参数个数 × 位宽 ÷ 8
式 14-1
因子剪枝改的是量化改的是
参数个数这一项(真的变少)不动,一个都不少
位宽不动(剪完还是 BF16)这一项(16 → 8 → 4 → …)
结果文件变小,config 里的形状字段也变小文件变小,config 里的形状字段一个不变

这张表最后一行就是全章最实用的一条判据:两件事都会让文件变小,但它们在 config.json 上留下的痕迹完全不同。

还有一个更简单的证据藏在论文标题里。GPTQ 那篇(arXiv:2210.17323)的标题里明明白白写着 Post-Training Quantization——训练后量化。Post-Training 这个词组本身就点破了时序:这件事发生在训练结束之后。AWQ(arXiv:2306.00978)虽然标题里用的是别的措辞,但它同属这一类方法——业界把这一整族的缩写就叫 PTQPost-Training Quantization),缩写里的第一个词就是「训练后」

自己推一遍:用文件大小验证「量化没让参数变少」

  1. Qwen3.8-27B 一共 27,355,639,808 个参数。按 BF16(每个参数 16 比特)存,文件应该有多大?和 Unsloth 实测的 BF16 文件 54.66 GB 对得上吗?

    想好了再看

    27,355,639,808 × 2 字节 = 54,711,279,616 字节 ≈ 54.7 GB(本站复算)。Unsloth 实测的 BF16 文件是 54.66 GB差 0.1%。这个差来自 GGUF 容器本身的开销、以及各家算不算 MTP 头之类的边角。能对到 0.1%,说明式 14-1 是对的,也说明我们数的参数个数是对的。这一步的意义是:先用一个已知无争议的情形校准工具,再拿它去查有争议的情形。

  2. Q4_K_M 的文件是 17.11 GB。假设参数个数没变,反推出来的平均位宽是多少?这个数合理吗?

    想好了再看

    文本塔 26,895,319,040 个参数(GGUF 不含视觉塔,那是单独的 mmproj 文件),17.11×109 × 8 ÷ 26.895×1095.09 比特/参数合理——Q4_K_M 是混合位宽的方案,一部分张量用了更高的位宽(第 15、16 章会讲清楚),所以平均下来比 4 高一些。得到一个「比 4 大一点、但远小于 16」的数,正是我们该期待的。

  3. 现在做反向假设:如果位宽真的是 4,那 17.11 GB 对应多少个参数?这个数说明了什么?

    想好了再看

    17.11×109 × 8 ÷ 4 = 34.2×109,也就是 34.2 B——比原模型的 27.36 B 还多。这显然荒谬(量化不可能凭空造出参数),所以「位宽正好是 4」这个假设被推翻了。这一步演示的是反证法怎么用在这类问题上:假设一个说法为真,推出一个荒谬的结论,那个说法就是错的。顺带你也知道了为什么不能把「4 bit 量化」当成字面意义上的 4 比特。

  4. 最后一个反向假设:如果 Q4_K_M 其实是「把模型剪小了」,位宽仍然是 16,那它剩下多少参数?怎么去验证这个假设?

    想好了再看

    17.11×109 ÷ 2 = 8.56×109,也就是 8.56 B。要验证只需一个动作:打开这个仓库的 config.json,看 hidden_sizenum_hidden_layers。如果真的剪到了 8.56 B,这两个字段必须变小(第 12 章那条式子决定了这一点)。而事实是——它们一个字都没变,还是 5120 和 64。假设被推翻。

  5. 把上面四步合起来,你得到了一条什么判据?

    想好了再看

    文件变小有两种可能的成因(位宽降低 / 参数减少),而它们留下的痕迹完全不同,因此是可以分辨的:量化会让文件变小而 config 里的形状字段一个不变;剪枝会让文件变小而 形状字段必须跟着变小。你不需要任何人告诉你答案,打开两个 config 对一遍就行。
    当初为什么会想到走这条路?因为「参数个数」和「位宽」是式 14-1 里仅有的两个因子——文件变小必然是其中至少一个变小了,而它们各自在别处留下的可观测痕迹不同。把一个现象归因到一个只有两项的式子上,然后逐项排除,这是这类判定问题的通用打法。

最有说服力的一击

到这里,「量化不改变参数量」已经是一个可以自己验证的事实了。但还有一个更深的问题没回答:就算参数个数没变,「量化一个大模型」和「用一个小模型」在效果上是不是等价的?如果等价,那这两件事虽然机制不同,实践中也就没必要分了。

Dettmers 与 Zettlemoyer 那篇(The case for 4-bit precision: k-bit Inference Scaling LawsarXiv:2212.09720,ICML 2023)正面回答了这个问题。他们把两种方案放在同一个比特预算下对撞,原文写的是:

论文原文(arXiv:2212.09720)

a 30B 8-bit model and a 60B 4-bit model have the same number of bits but may have very different zero-shot accuracies

——一个 30B 的 8 比特模型和一个 60B 的 4 比特模型占用同样多的比特,但它们的零样本准确率可能相差很大

把这句话拆开看:30 × 8 = 240,60 × 4 = 240。两个方案要的显存一模一样。如果「量化」和「换尺寸」是同一件事的两种说法,那这两个方案就该给出同样的结果。而论文说它们不一样。

一句话记住:同样的显存预算下,「把一个大模型量化」和「用一个小模型」是两个不同的选择,而且结果不一样。它们甚至不能互相替代——这正是从反面证明了这两件事不是一回事。如果它们是一回事,那句话就该写成「有同样多的比特,所以有同样的准确率」。

Qwen3.8-27B 的 BF16 权重约 54.7 GB,Q4_K_M 是 17.11 GB,小了 3.2 倍。请回答:(a) 参数个数变了吗?(b) 变小的到底是什么?(c) 用哪一个可以在 30 秒内查证的东西来支持你的答案?

先想:式 14-1 里有两个因子,文件变小意味着至少一个变小了。哪一个?
关键在于「量化」这个操作的定义:它作用在已经训好的权重上,把每个数字换一种记法。它有没有删掉任何一个数字?
第一步这样走:想想哪个文件能证明参数个数没变。参数个数由矩阵形状决定,矩阵形状写在哪儿?
完整答案:(a) 没变,还是 27,355,639,808 个。(b) 变小的是每个参数占多少比特——从 16 比特降到平均约 5 比特。(c) 打开量化版仓库的 config.json,看 hidden_size(还是 5120)、num_hidden_layers(还是 64)、vocab_size(还是 248320)、intermediate_size(还是 17408)。这四个字段决定了参数个数,它们一个都没变,参数个数就一个都没变。
为什么第 (c) 问最重要:(a) 和 (b) 是结论,(c) 是你自己能做的检验。第 12 章那一整章的价值,就在于让你有能力做这种检验——你知道参数个数是由哪几个字段算出来的,所以你知道该去看哪几个字段。没有第 12 章,你只能选择相信别人告诉你的 (a) 和 (b)。

变式:BF16 是 16 比特,Q4_K_M 平均约 5 比特,16 ÷ 5 = 3.2,正好是文件缩小的倍数。这个吻合说明了什么?(答案:说明式 14-1 里只有位宽这一个因子在变——如果参数个数也变了,这两个倍数就对不上了。这是一个不需要看 config 就能做的一致性检查,虽然它比看 config 弱一些,因为多个因子同时变也可能碰巧凑出同一个倍数。)

Dettmers 那句话是:「一个 30B 8-bit 模型和一个 60B 4-bit 模型有同样多的比特,但零样本准确率可能相差很大。」(a) 请解释这句话为什么能反过来证明「量化」和「换尺寸」不是一回事。(b) 如果它们真是一回事,这句话会变成什么样?

先想:如果两件事是同一件事的两种说法,那么「用同样的量做这两件事」应该得到什么?
关键在于这句话的结构:它先说「一样」(比特数),再说「不一样」(准确率)。如果比特数是唯一的决定因素,第二句就不可能成立。
第一步这样走:假设「量化」和「换尺寸」是同一件事——那么模型的能力就应该只由总比特数决定。然后看这个推论和论文原话是不是矛盾。
完整答案:(a) 用反证法:假设「把模型量化」和「换一个更小的模型」是同一件事,那么一个模型的能力就应该只由「总共占多少比特」决定——因为无论你从哪条路走到 240 比特,走到的都是同一个地方。可论文说的恰恰是:同样 240 比特,结果可能差很多。矛盾,所以假设错误。这两件事在效果上不可互换,因此不是同一件事。
更进一步:它们不可互换意味着你面对的是一个真实的选择——同样的显存,你可以选「大模型 + 低位宽」或者「小模型 + 高位宽」,而这个选择有对错、有权衡。如果它们是一回事,就没有什么可选的了。
(b) 如果真是一回事,这句话会变成:「一个 30B 8-bit 模型和一个 60B 4-bit 模型有同样多的比特,因此有同样的零样本准确率。」——而这样一句话根本不值得写进论文,因为它不包含任何信息。一篇论文之所以要写某句话,往往正是因为它反直觉。这句话之所以值得写,就是因为很多人默认了「比特数一样就一样」。
方法论提示:把一个论断的否命题写出来,看它是不是一句废话——如果是,说明原论断在澄清一个真实的混淆。这是判断「这句话有没有信息量」的一个好办法。

变式:这句话里有一个词很关键——「may have very different」(可能相差很大),而不是「一定」。为什么作者用「可能」?(答案:因为哪一个更好取决于具体情形——模型系列、任务、量化方法都会影响结论。论文的主张是「这两条路会分叉」,不是「其中一条永远更好」。一个诚实的论断会把不确定性写在句子里,而转述时最先被丢掉的往往就是那个词。

14.6 回到 Qwen3.8:那 100 多个仓库到底是什么

把四条路都讲完了,现在回来看 Qwen3.8 的实际情况。

事实一:官方这一次没有发布任何小尺寸。官方仓库一共只有 4 个——27B、27B-FP8、2.4T-A95B、2.4T-A95B-FP8。4B、8B、32B 全部不存在,不是「还没发」,是搜出来零结果。这和上一代 Qwen3 的做法不同(那一代确实发布了 0.6B 到 235B 的一整个系列,而且用了强到弱蒸馏),所以「按惯例应该有小尺寸」这个预期本身是合理的——但它这次没有兑现。

常见误解:`Ma7ee7/Qwen3.8_4B_Distilled` 是官方的小尺寸版本

社区里流传着一个叫 Ma7ee7/Qwen3.8_4B_Distilled 的仓库,名字里同时带着「Qwen3.8」「4B」「Distilled」三个词,非常容易被读成「官方发的 4B 蒸馏版」。它是第三方蒸馏,不是官方模型。

怎么一眼看出来?看仓库的命名空间。官方模型全部在 Qwen/ 这个组织下(Qwen/Qwen3.8-27B),任何斜杠前面不是 Qwen 的仓库,都不是官方发布的——不管模型名起得多像。这是 Hugging Face 上最简单也最可靠的一条判据。

说清楚这一点不是在贬低第三方的工作。第三方蒸馏可能做得很好、可能很有用,这是另一个问题。这里说的只是归属:它不是官方发布,因此它的训练细节、数据、质量都没有官方背书,你在引用它时必须说清这一点。把「谁做的」和「做得好不好」分开,是这一整章的方法论在具体场景里的应用。

事实二:发布后 48 小时内,Hugging Face 上出现了 100 多个派生仓库。这个数字经常被用来说明生态有多活跃——它确实活跃。但如果你据此以为「有 100 多个不同的 Qwen3.8 模型」,那就误读了。这 100 多个仓库里,绝大多数是量化版:GGUF(Unsloth、ggml-org、bartowski、lmstudio-community 各一份)、NVFP4(RadixArk、unsloth、Inferact)、AWQ(cyankiwi、AMD 官方 Quark)、GPTQ、EXL3、MLX 的 4bit/8bit/bf16、SmoothQuant W8A8……

一句话记住:那 100 多个仓库里的绝大部分,全都是 27B 或 2.4T 这两套参数,只是换了记录位宽——它们动的是第 条轴,不是前三条。「这么多版本」听起来像有很多个模型,实际上是两个模型的很多种记法

这里有一个特别好用的推论:你可以用第 12 章的点钞规则,自己去验证任何一个仓库属于哪一类。打开它的 config.json,看四个字段——hidden_sizenum_hidden_layersvocab_sizeintermediate_size

表 14-2:拿到一个陌生仓库,怎么判断它是什么(本站的判据表)
你看到的它最可能是为什么
形状字段和官方 27B 完全一致,但多了 quantization_config 之类的字段,或者文件是 .gguf量化版参数个数由形状字段决定,形状没变就说明参数一个没少
形状字段成比例地变小(比如层数 64→32、hidden 5120→4096),结构种类不变剪枝,或者一个独立训练的小模型——光看形状分不出这两者剪枝会保持结构同构、按比例缩小;但一个独立训练的小模型完全可以碰巧长成同样的形状
形状字段变了,而且结构种类也变了(稠密变 MoE、层的排列方式不同)另一个模型没有任何一种「从大变小」的操作会改变层的种类
命名空间不是 Qwen/第三方仓库(可能是上面任意一种)这一条和上面三条正交:第三方也可以只是做了量化
第二行那个「分不出」很重要,不要跳过

表 14-2 第二行给的不是一个结论,而是一个诚实的边界只看形状,你分不出「剪枝出来的小模型」和「独立训练的小模型」。因为剪枝的产物和独立训练的产物,在 config.json 上可以长得一模一样。要真正区分,你需要形状之外的东西——发布方的说明、训练日志,或者去比对权重的数值(剪枝产物的权重会和原模型的对应子块高度相关,独立训练的不会)。

本章末的建造台阶会把这条边界写进程序里:它只报「形状是子集」,绝不报「一定是剪枝」。一个工具最有价值的部分,往往是它明确拒绝回答的那些问题。

你在 Hugging Face 上看到一个叫 SomeOrg/Qwen3.8-9B 的仓库。请给出三步检查,判断它是剪枝、蒸馏、量化,还是别人自己训的。每一步要写清「看什么」和「看到什么说明什么」。

先想:四种可能里,哪一种能立刻排除掉?(提示:官方 27B 是 27.36 B 参数,量化不改变参数个数。)
关键在于按「排除成本」排序:先做最便宜、最能排除选项的检查。看命名空间是 0 秒,看 config 是 30 秒,比对权重数值要下载几十 GB。
第一步这样走:9B 这个数本身就已经排除了一个选项——因为量化不改变参数个数,一个量化版的 27B 仍然应该叫 27B。
完整答案:
第一步:看名字和命名空间。命名空间是 SomeOrg 不是 Qwen,所以肯定不是官方发布。而且名字里的「9B」已经排除了纯量化——量化不改变参数个数,27B 量化完还是 27B(只是文件小了)。剩下三种可能:剪枝、蒸馏、独立训练。
第二步:打开 config.json,对四个形状字段。如果 hidden_sizenum_hidden_layers 和官方 27B 完全一致,那 9B 这个数就对不上,说明标错了或者另有玄机;如果它们成比例缩小且层的种类不变(还是 3:1 的混合布局、还是稠密 FFN),那它形状上是 27B 的一个子集——剪枝和独立训练都能造成这个观察,这一步分不出;如果结构种类都变了(比如变成全注意力),那基本可以判定是另一个模型
第三步:读模型卡,找它自己的声明。是剪枝还是从零训,只有发布方知道,而这件事发布方通常会写——因为它决定了这个模型该被怎么引用。如果模型卡什么都没说,那你就该把这个模型当成来历不明的东西对待,而不是替它猜一个来历。
还想更硬的话(第四步,成本高):下载权重,把它的某一张矩阵和官方 27B 对应位置的子块比一比数值相关性。剪枝产物会高度相关,独立训练的不会。这一步能给出决定性证据,但要几十 GB 的下载。
整套流程的设计原则:按成本从低到高排,每一步都尽量多排除几个选项;并且在分不出的地方明确说「分不出」,而不是猜一个。

变式:如果这个仓库叫 SomeOrg/Qwen3.8-27B-AWQ-INT4,三步检查会变成什么样?(答案:第一步就基本结束了——名字里保留了「27B」说明参数个数没变,「INT4」说明位宽变了,这是量化版的标准命名。第二步开 config 确认四个形状字段和官方一致、并且多出一个 quantization_config,就可以收工。量化版是最容易判定的一类,因为它必须保留原模型的全部形状。

14.7 一张总表:把第 1 章那四条轴补全

第 1 章给过一张四轴表,那时候它只是一个提纲。走完这十三章之后,可以把它补全成一张带判据的表——每一行不只说「是什么」,还说「你怎么验证」。

表 14-3:五条路的完整对照(本站汇总;arXiv 号来自各条路的代表工作)
做法何时发生参数个数矩阵形状得到的是代表工作
独立预训练训练之前定尺寸,从随机初始化开始——(各是各的)——(两套图纸)另一套完全独立的权重Qwen3.8-27B 与 2.4T-A95B 的关系
剪枝训练之后,在权重上动刀真的变少按比例缩小,结构同构原模型的一个子集,通常要再训一段恢复Minitron arXiv:2407.14679 / 2408.11796
蒸馏学生的后训练/对齐阶段(也可用作预训练目标)学生是另一套独立参数与教师无关学到教师「行为」的另一个模型Hinton arXiv:1503.02531;Qwen3 arXiv:2505.09388;Gemma 2 arXiv:2408.00118
数据配方训练之前与之中(决定喂什么)——(不涉及比较)——尺寸不大但表现超尺寸的模型Phi-3 arXiv:2404.14219
量化训练之后,只改位宽一个都没少一张都没变同一套参数,换了记录方式GPTQ arXiv:2210.17323;AWQ arXiv:2306.00978;对撞实验 arXiv:2212.09720
全站的那句话,现在有了完整的支撑:尺寸差异发生在训练之前,量化发生在训练之后——这是两件事,甚至不在同一条时间轴上。中间还夹着剪枝和蒸馏两条真实存在、但各自完全不同的路。把这五行分开,全网关于「模型尺寸」的绝大部分混乱就散了。

有人说:「Qwen3.8 发布 48 小时就有 100 多个版本,这个生态太恐怖了。」请用本章的五条路和第 12 章的点钞规则,把这句话翻译成更精确的说法,并回答:这 100 多个仓库里,最多可能有几个是「不同的模型」?

先想:「版本」这个词很模糊。这 100 多个东西,各自动的是四条轴里的哪一条?
关键在于:量化版动的是第 ④ 条轴,参数一个不变。所以从「有几套不同的参数」这个角度数,它们全都不算新的。
第一步这样走:先数官方发了几套参数(不是几个仓库)。然后问:一个第三方要造出「第三套参数」,需要做什么、成本是多少、48 小时够不够?
完整答案:更精确的说法是——「Qwen3.8 发布 48 小时内,社区就为这两套参数做出了 100 多种不同的记录方式(量化格式与位宽组合)。」这句话仍然很了不起,但了不起的地方变了:它说明的是量化工具链的适配速度(第 16 章会讲,NVFP4 这类新格式第一次在开源发布日就成了主流选项),而不是「有 100 多个模型」。
最多可能有几个是不同的模型:官方只有两套参数(27B 和 2.4T-A95B;FP8 版本是这两套的量化版,不是新参数)。第三方要造出第三套参数,只有三条路:从零预训练(要几百万美元和几周时间,48 小时根本不够)、剪枝(要先把 27B 或 2.4T 下载下来剪,然后还要再训一段恢复,48 小时勉强能做出一个粗糙的)、蒸馏一个新学生(学生还得先有自己的预训练骨架)。所以现实的答案是:绝大多数是量化版,少数可能是微调版(微调改参数的值不改个数,属于第五种情况,不在表 14-3 里但同样不产生新的形状),真正「不同的模型」接近于零。
这道题真正在训练的东西:「多少个版本」这类数字,必须先问「按什么口径数」。同一批仓库,按「有几套参数」数是 2,按「有几种记录方式」数是 100+,按「有几个仓库」数还要更多。三个数都对,但它们支撑的结论完全不同——而通稿往往会挑最大的那个数,配上最强的那个结论。

变式:微调(fine-tuning)该放进表 14-3 的哪一行?(答案:它谁的行都不属于,该单开一行。微调改的是参数的,不改个数、不改形状,发生在训练之后——在「形状判据」上它和量化看起来一样(config 完全一致),要区分只能看权重数值或者模型卡。这说明形状判据有它的盲区:它能分辨「参数变没变多少个」,分辨不了「参数的值变没变」。

下面这段话把好几条路搅在了一起:「Qwen3.8 这次很聪明,先训一个 2.4T 的大模型,然后剪枝蒸馏出 27B,再量化成 4bit,这样一套流程下来,一个模型就覆盖了从数据中心到消费卡的全部场景。」请逐句指出问题,并给出正确的版本。

先想:这段话里出现了四条路里的三条。逐一问:这一条在 Qwen3.8 上成立吗?有什么证据?
关键在于把「业界存在这种技术」和「这个模型用了这种技术」分开。前者是真的,后者需要证据——而证据在 config.json 和模型卡里。
第一步这样走:先用 q14-1 的两个数字(层数 64 对 92、hidden 5120 对 8192)处理掉「剪枝蒸馏出 27B」这半句。剩下的再逐条看。
完整答案:问题一:「剪枝蒸馏出 27B」不成立。两个模型层数 64 对 92、hidden 5120 对 8192、稠密对 512 专家 MoE,没有一张矩阵形状相同;而且 27B 带着一座训进去的视觉塔,纯文本的 2.4T 里根本没有这部分。它们是两套独立预训练的参数。问题二:「剪枝蒸馏」被当成了一个词。它们是两件事——剪枝改参数个数,蒸馏改参数的值;Minitron 那条流水线上确实是先剪后蒸,但那是一种组合,不是一个操作。问题三:「再量化成 4bit」这一句本身没错(社区确实做了大量 4 bit 量化版),但它被接在前面那条错误的因果链上,读起来像是同一条流水线的第三道工序。量化是另一条轴上的事,而且它不改变参数个数——27B 量化完还是 27B。问题四:「一个模型覆盖全部场景」不成立。官方发的是两个独立的模型,各自对应一类部署场景(第 13 章);不是一个模型的几个版本。
正确的版本:「Qwen3.8 发布了两个独立预训练的模型:27B(稠密、多模态、Apache 2.0)面向单卡到小集群,2.4T-A95B(MoE、纯文本、自定义协议)面向数据中心。社区在发布后很快为这两套参数做了各种量化版本——量化只改记录位宽,参数一个不少,所以那些量化版仍然是这两个模型,不是新的模型。」
注意改写后失去了什么:原文有一个很吸引人的叙事(「一套流程覆盖全场景」),改写后没有了。这几乎是所有事实核查的代价——正确的版本往往比错误的版本无趣。但错误的那个版本会让你在第一次真的要选模型时做出错误的判断。

变式:如果把那段话里的「Qwen3.8」换成「Minitron」,有几句会变成正确的?(答案:「先训一个大的,然后剪枝出小的,再用蒸馏恢复」这部分对 Minitron 是成立的——那正是它的方法(arXiv:2407.14679:从已训好的 15B 剪出 8B 和 4B)。同一段描述,换一个主语就从错变成对——这说明问题不在于描述本身有没有道理,而在于它有没有被安到正确的对象上。这是这一整章最该带走的一条:技术路线是真的,把它安在谁身上要有证据。

答辩:如果我是审稿人

蒸馏出来的学生,学的就是老师的输出分布,目标函数里写着「尽量像老师」。那它本质上不就是老师的一个压缩版吗?你说它「独立」,是不是在玩文字游戏——反正它的行为是老师给的。

参考防守(先自己组织语言再看)

这个质疑值得认真对待,因为它指出了一件真事:蒸馏确实让学生的行为向老师靠拢,这正是它的目的。争的不是这一点,争的是「压缩版」这个词想表达什么。

如果「压缩版」的意思是行为相似,那这个说法有道理——蒸馏本来就是为了让小模型的行为接近大模型。但如果它的意思是参数派生(像 zip 压缩那样,压缩包里装的还是原文件的信息,解开能还原),那就错了:教师的权重矩阵和学生的权重矩阵之间没有任何映射关系,你没法从学生的参数里还原出教师的任何一个参数,也没法说明学生的第 3 层第 17 个通道对应教师的哪一部分。这不是文字游戏,这是一个可以检验的差别——第 12 章那套形状判据、以及本章的血统检查器,检验的正是它。

还有一个更有力的角度:学生能学到教师没有的东西,也能在教师擅长的地方失败。如果它真是教师的压缩版,这两件事都不该发生。而在实践中,蒸馏出来的学生在某些任务上超过教师、在另一些任务上远远落后,都是常见现象。一个「压缩包」不会有自己的性格。

最后补一句边界:本站说「独立」,指的是参数的来源独立,不是「和教师无关」。它当然和教师有关——它的行为被教师塑造过。把「行为受影响」和「参数被派生」分开,是这一整章反复在做的同一个动作。

答辩:如果我是审稿人

你反复说量化「什么都没少」。可 54.66 GB 压到 17.11 GB,压掉了 68%,而且量化到 2 位的模型明显变笨了。既然文件小了三倍、行为也变了,说「参数一个没少」是不是在用一个技术上正确但实际上误导的说法?

参考防守(先自己组织语言再看)

这个质疑必须先承认它对的那一半:量化确实会让模型变差,位宽越低越明显。本站从没说过量化是免费的——第 15、16 章整整两章都在讲误差从哪来、掉多少。

但「参数一个没少」和「行为变了」同时成立,而且不矛盾。用第 0 章那条式子说最清楚:量化改的是每个数被记录成什么,不是有没有这个数。打个比方:把「3.14159265」记成「3.14」,数还是那一个数,只是记得粗了;再拿它去算圆的面积,结果当然会偏。丢的是精度,不是数量。而这两件事的区别是可以被检验的:位宽变了,config 里的形状字段一个不变,张量的形状一张不变,量化版还能被反量化回近似的原始权重。剪枝就不行——被砍掉的通道是真的没了,不存在任何还原方式。

为什么要坚持这个区分?因为它决定了两件实际的事: 一个 4 bit 的 27B 仍然要按 27.36 B 参数去理解它的能力上限和 KV cache(第 5 章那笔账和量化无关); 你不能用「量化到 4 bit」去代替「换一个更小的模型」——Dettmers 那句话(arXiv:2212.09720)说的正是这两条路会分叉。如果「压小了」和「记粗了」被当成一回事,这两个推论都会出错。

所以准确的表述是三句话:参数个数一个没少;每个参数的精度降低了;因此行为会有损失,损失多大取决于量化方法和位宽。三句话缺一不可——只说第一句是误导,只说第三句则丢掉了本章要澄清的那件事。

答辩:如果我是审稿人

你专门点名 Ma7ee7/Qwen3.8_4B_Distilled 说它「容易被误传」,还建议读者看命名空间。可官方没做小尺寸,不代表社区做的就低人一等。你这种写法,是不是在用「官方 / 非官方」这个二分给社区工作打上劣等标签?

参考防守(先自己组织语言再看)

这个质疑值得回答,因为它提醒的是一个真实的风险:「非官方」很容易被读成「不好」。本站的立场是明确的——这两件事必须分开。

正文点名它,说的是归属问题,不是质量问题。具体的风险是:这个名字同时含有「Qwen3.8」「4B」「Distilled」三个词,读者很容易以为官方发布了一个 4B 蒸馏版;而一旦这么以为,后续的一串判断都会跟着错——比如以为它的训练细节有官方文档、以为它的许可证和官方 27B 一样(27B 是 Apache 2.0,第三方衍生模型的授权条件要看它自己的声明)、以为它在评测上的表现能代表 Qwen3.8。这些错误的代价是实际的,尤其是许可证那一条。

更重要的是,本站给的判据是中性且可执行的:看命名空间。这条判据不涉及任何质量评价,它只回答「谁发的」。而「谁发的」这个信息本身有价值——它决定了出问题时你该去问谁、引用时该怎么标注、以及哪些文档对它有效。

至于质量,本站的态度是不评价:本站没有跑过那个模型,也没有能力评价它。写「本站未评估其质量」比写「它可能不如官方」诚实得多。把一个自己没做过的判断说成结论,比误传归属更糟。

真未解同一个显存预算,「大模型 + 低位宽」和「小模型 + 高位宽」,能不能事先算出哪个更好?

Dettmers 与 Zettlemoyer(arXiv:2212.09720)证明了这两条路会分叉,并在他们的实验设置下给出了一个具体建议。但那个建议依赖于当时的模型系列、任务集合和量化方法。到今天,问题仍然没有一个通用答案:换一个架构(比如 Qwen3.8 这种混合注意力 + 部分层是 MoE 的结构)、换一类任务(比如需要很长思维链的推理任务)、换一种量化格式(比如 NVFP4 这类带两级 scale 的新格式),结论都可能变。这确实是一个开放问题——不是「答案在别的论文里」,而是学界仍在为不同设置各自做实证,没有一条能一般化的规律。卡住的地方在于:位宽降低造成的能力损失不是均匀的,它在不同任务、不同语种、不同上下文长度上的表现差异很大,而目前没有一个能同时预测这些差异的模型。

先做这一步(不需要显卡):打开第 12 章那个参数点钞机,把「总比特预算」固定成 18 GB × 8 = 1.44×1011 比特(也就是一张 24 GiB 卡留给权重的量)。然后穷举:位宽取 2、3、4、5、6、8 这六档,每一档反算出预算内能装下的最大参数量 N = 预算 ÷ 位宽,列成一张六行的表。接着做最关键的一步:拿着这张表去 Hugging Face 上查,这六个 (N, 位宽) 组合里,有几个是真实存在、可以下载的?你会发现表格里有一半的格子在现实中是空的——比如「8 bit 的 18B 模型」这一档几乎没人做。那些空格子本身就是一个值得追问的发现:是因为效果不好,还是因为没人做过?带着这个问题回去读 arXiv:2212.09720 的实验设置,看它扫过的区间覆盖了你表里的哪几行。

这一层要加什么:一个血统检查器

为什么现在才加它:前十三层你造的都是「这个模型是什么样」的工具。这一层第一次问「这两个模型是什么关系」——而这正是本站从第 1 章立起来的那个问题。有了它,你不再需要相信任何人对某个仓库的描述,你可以自己判。

def lineage(A, B): """A, B: {张量名: (维度元组)},从 config 或权重文件读出来的形状清单""" same = {n for n in A.keys() & B.keys() if A[n] == B[n]} # ① 每一张同名张量的形状都逐位相同 → 参数个数与排布完全一致 if A.keys() == B.keys() and len(same) == len(A): return "同一套参数,不同位宽" # 位宽不在形状里,要另看 dtype # ② 名字集合同构,且每一维等比缩小 → 形状是子集 if A.keys() == B.keys(): ratios = {tuple(a/b for a, b in zip(A[n], B[n])) for n in A} if all(all(r <= 1 for r in t) for t in ratios) and len(ratios) <= 3: return "形状是子集" # ← 注意:不写「一定是剪枝」 # ③ 没有一张张量的完整形状相同 → 两套不相干的图纸 if not same: return "不同的模型" return "无法判定:形状部分重合,需要看权重数值或发布方声明"

难点一:位宽不在形状里。张量形状是 [5120, 17408] 这样的维度元组,它不带 dtype。所以分支 ① 严格说只能证明「参数个数与排布完全一致」,要断定这是量化关系,还得看两边的 dtype 不同,并且注意量化格式通常会多出一些辅助张量(GPTQ 会有 qzerosscales,GGUF 把 scale 打包进块里)。所以真实实现里 A.keys() == B.keys() 这个条件要放宽成「主干张量集合相同,允许 B 多出量化辅助张量」。写死成严格相等,第一条 bs-check 就会挂在辅助张量上。

难点二:「有某一维相同」不等于「形状相同」。27B 和 2.4T 的嵌入矩阵分别是 [248320, 5120][248320, 8192]——第一维完全一样(两个模型的词表都是 248,320)。如果你的比较写成「有维度对得上就算相关」,这两个毫无关系的模型会被判成有血缘。判据必须是完整形状元组相等,不是「有交集」。

难点三(最重要的一条):分支 ② 绝不能返回「这是剪枝」。剪枝的产物和一个独立训练的小模型,在形状清单上可以长得一模一样——因为剪枝保持结构同构、按比例缩小,而任何人都可以直接照着这个比例设计一个新模型从零训。形状只能给出必要条件,给不出充分条件。要真正区分,得比对权重数值(剪枝产物与原模型对应子块高度相关)或者读发布方的声明。所以这个函数只报「形状是子集」,把最后一步留给人。一个工具明确拒绝回答它答不了的问题,比它多答对几个案例更有价值。

自己验:三种关系要能被分开。
喂两份 27B 的形状清单(一份来自 BF16 权重,一份来自 INT4 量化版),应返回 「同一套参数,不同位宽」——判据是逐张形状相同embed 都是 [248320, 5120]q_proj 都是 [12288, 5120],层数都是 64。如果它返回「不同的模型」,去看你是不是把量化版多出来的 scales / qzeros 也算进了主干张量集合。
27B2.4T,应返回 「不同的模型」——层数 64 对 92、hidden 5120 对 8192没有一张矩阵的完整形状相同。如果它返回了别的,八成是被嵌入矩阵那个共同的 248320 骗了(见难点二)。
喂一个 15B 和它剪枝出来的 8B(比如层数 32→24、hidden 4096→3072、其余结构不变),应返回 「形状是子集」——每一维的缩放比例落在同一小组值里,结构同构。它必须返回「形状是子集」,不是「剪枝」;如果你的实现输出了「剪枝」,回去看难点三。
三种关系都能被区分开,而且第 ③ 种的措辞是保守的,这一层就成了。

本章小结

  • 直接回答:27B 不是把 2.4T 压小的。两个数字就够了——层数 64 对 92、hidden 5120 对 8192没有一张矩阵的完整形状相同;再加上 27B 带着一座训进去的视觉塔,而 2.4T 是纯文本。
  • 剪枝(Minitron, arXiv:2407.14679):训练之后在权重上动刀,参数真的变少,产物是原模型的子集。结构化剪枝砍整个头/整个通道,砍完必须配蒸馏来恢复——但「先剪后蒸」是一条流水线上的两道工序,不是同一件事。论文自述可省下多达 40 倍的训练 token。
  • 蒸馏(Qwen3, arXiv:2505.09388):发生在后训练/对齐阶段,教的是教师的输出分布(先是回答文本,再是 logits)。学生的骨架是它自己独立预训练出来的另一套参数。省下的那 90% GPU 小时,省的是「教它怎么回答」,不是「建这栋楼」。
  • 第三条路:Phi-3(arXiv:2404.14219)不靠蒸馏、只靠数据配方;Gemma 2(arXiv:2408.00118)官方承认用知识蒸馏代替下一词预测来训 2B 和 9B。三条路并行存在,也经常混用。
  • 量化:训练之后把每个数字用更少的位来记,参数一个都没少、形状一张都没变。GPTQ 的标题里就写着 Post-Training,PTQ 这个缩写的第一个词就是「训练后」。最有说服力的一击来自 arXiv:2212.09720:「a 30B 8-bit model and a 60B 4-bit model have the same number of bits but may have very different zero-shot accuracies」——同样的比特预算,两条路会分叉,因此它们不是一回事。
  • 回到 Qwen3.8:官方没有发布任何小尺寸(4B/8B/32B 全部不存在);社区流传的 Ma7ee7/Qwen3.8_4B_Distilled第三方蒸馏,不是官方模型(判据:命名空间不是 Qwen/)。48 小时内出现的 100+ 个派生仓库绝大多数是量化版——它们全都是那两套参数,只是换了记录位宽。「这么多版本」的绝大部分,是第 ④ 条轴,不是前三条。
  • 一条你自己能用的判据:打开 config.json 看四个形状字段。形状不变 = 量化;形状按比例缩小且结构同构 = 形状是子集(剪枝和独立训练分不出);结构种类也变了 = 另一个模型。这个判据的边界要说清楚,说不清的地方就说分不出。

这一章把「量化」从那三条路里彻底摘了出来,但一直没有正面讲它:位宽降低到底是怎么做到的?为什么 4 比特还能表示那么大范围的数?误差从哪来、有多大?为什么 Q4_K_M 的平均位宽是 5.09 而不是 4?接下来两章正面拆开它——第 15 章讲原理,第 16 章讲今天真正在用的那几种格式。

第15章 量化①:位宽、误差与显存

这一章回答一件很具体的事:同一个 Qwen3.8-27B,为什么有的文件 54.66 GB、有的 17.11 GB、有的只有 9.01 GB,而里面的参数个数一个不差都是 273.6 亿。讲完你应该能自己动手算——给定参数量和位宽,算出体积;给定一小块数字和目标位宽,算出缩放因子、量化值、还原值和误差。

学完这一章你应该能做到

  • 用一行公式估出任意模型在任意位宽下的权重体积,并说清为什么估出来的数总是偏小
  • 手算一个 4 元素块的对称量化与非对称量化:缩放因子、零点、量化值、还原值、误差,四样都能写出来
  • 解释离群值为什么是量化的头号杀手,并能构造一个把误差推到 100% 的具体例子
  • 算出「INT4 + 分组 128 + FP16 缩放因子」的实际位宽是 4.125 而不是 4,并写出通用公式
  • 说清 INT4 和 FP4 的格子分布差在哪,以及为什么神经网络的权重更适合非等距的格子
前置:第0章的「浮点数怎么占字节」(符号位 / 指数位 / 尾数位,以及 FP32、BF16、FP16、FP8-E4M3、E5M2、INT8、INT4 的位划分),第12章的参数点钞(27.36B 这个数怎么来的),第14章的「量化是第四条轴」。

15.1 承接第14章:量化到底改了什么

先问:不搞清楚这件事会怎样

第14章把话撂在那儿了:尺寸差异发生在训练之前,量化发生在训练之后。但那一章只给了结论,没给账本。不把账本摊开,你就没法回答任何一个真实问题——这张 24GB 的卡到底能不能跑?跑起来会掉多少能力?为什么标着 4 位的文件比理论值大 25%?这三个问题的答案全在同一套算术里。

量化(quantization) 干的事只有一件:把每个参数占用的位数变少。Hugging Face 的量化概念文档给的定义就是这么写的——用更低精度的数据类型来表示模型的权重和/或激活值。全文只谈位宽,从不谈参数个数,也不谈层数、头数、专家数。

所以第一条公式简单到几乎不像公式:

V权重N × b8
式 15-1
符号是什么直觉
V权重权重占的字节数就是你在 Hugging Face 页面上看到的那个文件大小
N参数个数第12章逐张矩阵数出来的 27.36 B。量化不改这个数
b每个参数占几量化唯一改动的量。除以 8 是因为 8 位等于 1 字节

把 27.36 B 代进去,三个数一秒钟就出来了:BF16(b = 16)是 54.72 GB;INT8(b = 8)是 27.36 GB;INT4(b = 4)是 13.68 GB。第一个数和 Unsloth 实测的 BF16 文件 54.66 GB 只差 0.1%,可以认为公式是对的。

但第二个数和第三个数就开始对不上了。实测的 Q8_0 文件是 29.05 GB,比 27.36 大 6.2%;实测的 Q4_K_M 文件是 17.11 GB,比 13.68 大整整 25%

把这个差反过来读,会得到一个更刺眼的数字。用式 15-1 倒着解,把实测体积除以参数量再乘 8,得到实际平均位宽

b实际 = 17.11 × 109 × 827.36 × 109 = 5.00 bit
式 15-2
符号是什么它从哪来
b实际实际平均位宽,单位是「位/权重」把式 15-1 反过来解出来的。它是本站用来横向比较所有量化档位的统一尺子
17.11 × 109Q4_K_M 文件的实测字节数Unsloth 仓库逐文件真实大小,不是估算
× 8字节换算成位1 字节 = 8 位
27.36 × 109参数个数第12章逐张矩阵点出来的 27.36 B

一个名字里带着 4 的量化格式,实际每个权重花了整整 5.00 位。多出来的那 1 位,是本章和下一章要一点一点还原出来的账。

一句话记住:54.66 GB 和 17.11 GB 这两个文件里,参数个数完全一样,都是 273.6 亿个。变的只有「每个数字用几位来写」。Q4_K_M 的真实平均位宽是 5.00,不是 4——差出来的 25% 会在第16章被逐字节还原。

(a) Qwen3.8-27B 的 IQ4_XS 文件实测 15.71 GB,Q3_K_M 实测 13.82 GB。分别反算它们的实际平均位宽。(b) 如果真有一个纯 INT4、不带任何额外开销的版本,它应该是多大?

先想:式 15-1 有三个量,题目给了两个,你要求第三个。反算的时候除法和乘法的顺序别弄反。
关键在于统一单位:体积是 GB(10 的 9 次方字节),参数量是 B(10 的 9 次方个),两个 109 正好约掉,只剩「体积数字 × 8 ÷ 参数数字」。
第一步这样走:15.71 × 8 ÷ 27.36 = ? 把这个算式抄下来算完,再换 13.82 做一遍。
完整答案:(a) IQ4_XS:15.71 × 8 ÷ 27.36 = 4.59 bit;Q3_K_M:13.82 × 8 ÷ 27.36 = 4.04 bit。注意这个结果很反直觉——名字里写着 3 的 Q3_K_M,实际平均位宽是 4.04,比 4 还多;名字里写着 4 的 IQ4_XS 反而是 4.59。量化格式的名字指的是主体张量的位宽,不是全模型的平均位宽。(b) 纯 INT4:27.36 × 4 ÷ 8 = 13.68 GB。现存的任何一个 4 位档位都比它大,没有例外。

变式:Unsloth 的 UD-IQ2_XXS 只有 9.01 GB。它的实际平均位宽是多少?这个数比 2 大还是小,说明了什么?(答案 2.63——比 2 大 31%,说明连最激进的 2 位档位也要为「怎么记住缩放因子」付出三分之一的额外开销。)

15.2 最朴素的量化:线性映射

先问:不用缩放因子会怎样

第0章讲过,INT4 只有 16 个台阶,能表示的整数是 −8 到 7。而模型权重是一堆小数,绝大多数落在 −0.5 到 0.5 之间。如果直接把权重四舍五入成整数,全部会变成 0——整个模型瞬间变成一张白纸。所以必须先放大,量化成整数存起来,用的时候再缩回去。那个放大倍数就是缩放因子。

缩放因子scale,记作 s):一个真实的小数(通常用 FP16 存),表示「量化后每一格代表原始世界里多大一步」。零点zero point,记作 z):一个整数偏移量,表示「原始世界里的 0 落在量化后的第几格」。

有了这两样,量化和反量化就是一对互逆的直线映射:

q = round(xs) + z    = s × (qz)
式 15-3
符号是什么直觉
x原始权重,一个真实小数训练完就定死了,是我们要保存的东西
q量化后的整数,真正被写进文件的东西INT4 时它只能是 −8 到 7 里的一个
s缩放因子,一个 FP16 小数格子有多宽。它本身也要存进文件,这一点是 15.4 节的全部内容
z零点,一个整数把格子整体平移,让原始的 0 落在某一格上
反量化还原出来的值推理时真正参与计算的数。它不等于 x,差多少就是量化误差

缩放因子怎么定?最常用的办法叫 absmax:取这一批数里绝对值最大的那个,让它正好顶到量化范围的边界。INT4 对称量化时正边界是 7,所以 s = max|x| ÷ 7。

对称还是非对称?区别只在有没有那个 z

表 15-1:对称量化与非对称量化(本站整理)
对称量化 (symmetric)非对称量化 (asymmetric)
零点 z恒为 0,不用存要算、要存
缩放因子s = max|x| ÷ (2b−1 − 1)s = (max − min) ÷ (2b − 1)
格子怎么摆关于 0 左右对称,0 一定能被精确表示紧贴数据的真实上下界,一格不浪费
额外存储每组只存 1 个 s每组存 sz 两个
适合谁权重——分布大致以 0 为中心,左右差不多宽激活值——过了 ReLU 之类的函数后全是非负数,用对称量化会白白浪费一半格子

拿一个具体的四元素块走一遍。设这一块是 [−0.8, 0.1, 0.35, 2.4],量化到 INT4。

对称s = 2.4 ÷ 7 = 0.342857。量化值 q = [−2, 0, 1, 7]。还原值 = [−0.6857, 0, 0.3429, 2.4]。误差 = [0.114, 0.100, 0.007, 0]。

非对称:min = −0.8,max = 2.4,s = 3.2 ÷ 15 = 0.213333,z = round(0.8 ÷ 0.213333) = 4。量化值 q = [0, 4, 6, 15]。还原值 = [−0.8533, 0, 0.4267, 2.3467]。误差 = [0.053, 0.100, −0.077, 0.053]。

两种做法的总误差差不多(对称 0.152,非对称 0.147),但请盯住加粗的那一列:0.1 这个数在两种做法下都被压成了 0,误差 100%。原因是 2.4 那个数把格子撑得太宽了——一格 0.34 或 0.21,而 0.1 连半格都不到。这就是下一节的主角。

[0.02, −0.15, 0.6, −0.05]INT4 对称量化。写出 sq、每个元素的绝对误差,并指出哪个元素的相对误差最大。

先想:对称量化只需要一个数就能定下缩放因子——这批数里绝对值最大的那个是谁?
关键在于 INT4 对称的正边界是 7(不是 8,也不是 15)。s = 绝对值最大的数 ÷ 7。
第一步这样走:max|x| = 0.6,所以 s = 0.6 ÷ 7 = 0.085714。然后每个数除以 s、四舍五入。
完整答案:s = 0.085714x/s = [0.233, −1.75, 7.0, −0.583],四舍五入得 q = [0, −2, 7, −1]。还原 = s×q = [0, −0.171, 0.6, −0.0857]。绝对误差 = [0.02, 0.021, 0, 0.036]。相对误差 = [100%, 14%, 0%, 71%]。0.02 那个元素被彻底抹平了,因为它比半格(0.043)还小。这道题要建立的直觉是:在一个块里,小的数比大的数吃亏得多——绝对误差人人平摊,但相对误差和数值大小成反比。而神经网络的权重里,小数远比大数多。

变式:把同一批数改用 INT8 对称量化,s 变成多少?0.02 这次还会被抹平吗?(提示:INT8 的正边界是 127。)

15.3 误差从哪来:舍入是小头,离群值是大头

量化误差有两个来源,量级完全不同。

第一个是舍入误差,躲不掉但很温和。round 最多把一个数挪半格,也就是 s/2。如果这批数在格子里分布得比较均匀,误差就均匀落在 −s/2 到 +s/2 之间,均方根是 s/√12。这是一个可以精确预测的量,本章末的推导会用它算出一条能验证的公式。

第二个是离群值(outlier),才是真正的杀手。回头看 15.2 那个例子:整块数据里只有 2.4 一个大数,它一个人就把 s 顶到 0.34,剩下三个数被挤进很少的几个格子里,其中 0.1 直接归零。缩放因子被最大的那一个数绑架了。

打个比方:一把被姚明撑坏的尺子

你要用一把只有 15 个刻度的尺子量一屋子人的身高。屋里都是 1.6 到 1.8 米的人,本来每格 1.3 厘米,量得很准。这时进来一个 2.3 米的人,为了让他也在尺子上,你必须把量程拉到 2.3 米——每格变成 15 厘米。于是 1.65 米和 1.72 米在这把尺子上读出来是同一个数。一个人毁掉了全屋的分辨率。

类比失效处:真实的离群值不是「偶尔来一个」,而是系统性地长在某几个通道上。LLM.int8()(arXiv:2208.07339)的观察是:在足够大的模型里,激活值中会出现幅度极大的特征维度,它们不是噪声,而是承载重要信息的少数维度——所以既不能删也不能截断。这也是为什么后面 AWQ 那一路会说「有些权重不能亏」。

这件事有多严重?用一个可复现的实验:拿一个 4096×4096 的标准正态随机矩阵(1677 万个数),INT4 对称量化,整个矩阵共用一个缩放因子,相对误差约 22%。现在只往里塞一个值为 100 的离群值(比正常值大 100 倍),其余 1677 万个数一个不动,再量化一次——

相对误差从 22% 跳到 99.97%。还原出来的矩阵里只剩下 两个不同的值:0 和 100。1677 万个权重被一个数按死在了同一格里。

原因用式 15-3 一眼就能看穿:s = 100 ÷ 7 = 14.29,任何绝对值小于 7.14 的数除以 s 之后都不到 0.5,四舍五入统统变成 0。而标准正态分布的数几乎全都在 ±6 以内。整个矩阵被清零了,只有那个离群值活了下来。

换成 INT8(正边界 127)也只是稍好:同样的实验,误差从 1.22% 涨到 22.73%,涨了 18 倍。位宽再多几位也救不回来——因为问题不在位宽,在于谁来定这个缩放因子

上面这个实验台就是让你把刚才那几个数字亲手跑一遍。它能玩的东西有四类: 选位宽——INT8、INT4、NF4、FP8-E4M3、NVFP4、MXFP4 六种格式并排,看同样一批数在不同格子里被摆成什么样; 选分组大小——从「全矩阵一个缩放因子」一路调到 16,看误差曲线怎么下降、存储开销怎么上升; 开关离群值——一键往矩阵里塞一个 100 倍的大数,看误差是不是真的会爆到接近 100%,再打开分组看它怎么被关进一个小格子里; 看误差分布——不只看一个总数,还要看误差的直方图和最大相对误差落在哪个元素上,你会发现挨打最狠的永远是那些接近 0 的小权重。

有人说:「INT8 有 256 个格子,够细了,离群值最多让误差涨一点点。」请构造一个具体的、只有 5 个元素的向量,使得 INT8 对称量化后至少有 3 个元素被还原成 0。然后说明:把它拆成两组分别量化,能不能救回来。

先想:一个数被还原成 0 的条件是什么?把式 15-3 里的 round 那一步写出来看。
关键在于「归零门槛」:|x| < s/2 就会变成 0。而 s = max|x| ÷ 127。所以门槛是 max|x| ÷ 254。你要做的是让某几个数小于这个门槛。
第一步这样走:先定一个大数,比如 1000。那么门槛是 1000 ÷ 254 = 3.94。现在随便挑三个绝对值小于 3.94 的数塞进去。
完整答案:取 [1000, 3, −2, 1, 500]s = 1000 ÷ 127 = 7.874,归零门槛 3.937。3、−2、1 三个都低于门槛,全部还原成 0;1000 还原成 1000(q=127),500 还原成 q=round(63.5)=64 → 503.9。
拆成两组能救:把 [1000, 500] 和 [3, −2, 1] 分开,后一组的 s = 3 ÷ 127 = 0.0236,三个小数分别还原成 3、−2.005、0.992,误差都在 0.5% 以内。结论是:位宽解决不了的问题,分组能解决。因为离群值的伤害范围被限制在它自己那一组里,别的组根本不知道它存在。这正是下一节要讲的机制,也是为什么现代量化方案没有一个是「整张矩阵一个缩放因子」的。

变式:如果那个大数不是 1000 而是 30,同一批小数还会被归零吗?临界的大数是多少?(提示:解不等式 1 ≥ max ÷ 254。)

15.4 分组量化:不用一个缩放因子管全部

解法上一节已经说破了:别让一个缩放因子管所有数。把权重按顺序切成一段一段,每段 g 个数,每段自己算自己的缩放因子。这叫分组量化(group-wise / block-wise quantization)g分组大小(group size)

效果立竿见影。还是那个 4096×4096 的矩阵,INT4:

表 15-2:分组大小对误差的影响(本站实算,标准正态 4096×4096,INT4 对称 absmax)
分组大小 g无离群值时的相对误差塞进一个 100 倍离群值后实际位宽(FP16 缩放因子)
全矩阵(16.7 M)约 22%约 100%4.000
4096约 15.8%——4.004
128约 11.7%约 11.7%(几乎不动)4.125
32约 9.7%——4.500

看第三行和第一行的对比:分组 128 时,那个离群值只污染了它所在的那一组(128 个数里的一组),另外 13 万多组毫发无损,总误差几乎不动。离群值的伤害被关进了笼子。

但天下没有免费的午餐——缩放因子本身也要存进文件。这就是「为什么 4 位量化的实际位宽大于 4」的第一个来源,而且它可以精确算:

b实际 = b + bs + bzg
式 15-4
符号是什么直觉
b量化值本身的位宽INT4 就是 4
bs缩放因子占几位常见是 FP16,16 位
bz零点占几位对称量化时是 0(不存);非对称时通常也是 16
g一组多少个数它在分母上——组越大,摊到每个权重头上的开销越小

代最经典的一组数:128 个 INT4 值 = 128 × 4 ÷ 8 = 64 字节,加一个 FP16 缩放因子 = 2 字节,合计 66 字节。66 × 8 ÷ 128 = 4.125 位/权重。比标称的 4 多出 3.1%。

g 换几个值,规律一眼就出来了:g=256 → 4.0625;g=128 → 4.125;g=64 → 4.25;g=32 → 4.5g=16 → 5.0。分组小到 16 时,光缩放因子就把位宽推高了 25%。

常见误解:分组越小越好

不是。分组从 4096 缩到 128(缩小 32 倍),误差只从 15.8% 降到 11.7%(降 26%);再从 128 缩到 32(缩小 4 倍),误差从 11.7% 降到 9.7%(降 17%),而实际位宽从 4.125 涨到 4.5(涨 9%)。收益在递减,成本在线性上涨,所以主流方案几乎都停在 32 到 128 之间。为什么收益递减?因为误差取决于每组里的最大值,而随机数的最大值只按组大小的对数的平方根缓慢增长——组缩小 32 倍,最大值只降不到一半。本章末的推导会把这条算清楚。

某个 4 位量化方案用非对称量化,分组大小 64,缩放因子和零点都用 FP16 存。(a) 它的实际位宽是多少?(b) 用它压 Qwen3.8-27B,文件会是多大?(c) 如果改成对称量化,能省下多少 GB?

先想:非对称量化每组要存几个额外的数?式 15-4 的分子里应该填什么。
关键在于非对称要存 sz 两个,各 16 位,所以分子是 32 而不是 16。
第一步这样走:b实际 = 4 + 32 ÷ 64 = ? 算出来再用式 15-1 乘 27.36 B 除 8。
完整答案:(a) 4 + 32÷64 = 4.5 位。(b) 27.36 × 4.5 ÷ 8 = 15.39 GB。(c) 对称时 bz=0,实际位宽 = 4 + 16÷64 = 4.25 位,体积 27.36 × 4.25 ÷ 8 = 14.53 GB,省 0.86 GB
顺带记住 (b) 那个 15.39 GB——它正好是「纯 4.5 位」的体积,第16章会告诉你 GGUF 的 Q4_K 标称就是 4.5 bpw。但实测的 Q4_K_M 是 17.11 GB,还差 1.72 GB 没交代。那 1.72 GB 不来自分组开销,来自另一件事。

变式:如果缩放因子改用 FP8(8 位)而不是 FP16 来存,分组 16 的实际位宽是多少?这正是 NVFP4 的做法,第16章会用到。(答案:4 + 8÷16 = 4.5。)

15.5 定点与浮点:INT4 和 FP4 不是一回事

先问:格子等距有什么不好

INT4 的 16 个台阶是等距的——每一格一样宽,从 −8 到 7 匀速排开。这在数据均匀分布时是最优的。但神经网络的权重不是均匀分布,它接近正态分布:绝大多数值挤在 0 附近,越往外越稀疏。等距的格子把一半的分辨率浪费在了几乎没有数据的两端。

解法是让格子不等距:靠近 0 的地方密一点,远离 0 的地方疏一点。有两条路能做到这件事。

第一条是用浮点数当格子。第0章讲过,浮点数的刻度天生就是不均匀的——指数位每加 1,格子宽度翻一倍。4 位浮点数 FP4-E2M1(1 符号位 + 2 指数位 + 1 尾数位)能表示的绝对值是 0、0.5、1、1.5、2、3、4、6 这八个,加上正负号共 15 个格子。注意 0.5 到 1 之间的间隔是 0.5,而 4 到 6 之间的间隔是 2:靠近 0 的格子密了 4 倍。

第二条是直接照着正态分布造格子。这就是 NF4NormalFloat 4-bit),出自 QLoRA(arXiv:2305.14314)。思路很直接:既然权重服从正态分布,那就把正态分布切成 16 份等概率的区间,每份取中点当格子。这样每个格子分到的数据量一样多——没有一个格子是闲着的。

权重的实际分布:绝大多数挤在 0 附近 INT4 等距 FP4 E2M1 NF4 等概率 −max 0 +max
图 15-1:三种 4 位格式的格子落点对比。INT4 的 15 条竖线均匀铺开;FP4-E2M1 的格子在两端拉得很开;NF4 的格子按正态分布的等概率分位点摆放,中间密、两端疏,正好贴着上方那条曲线的形状。示意图:竖线位置按各格式定义的取值缩放到 ±max 画出,曲线是标准正态密度的示意,非实测数据。

值得不值得?同一个标准正态矩阵、同样分组 128,本站实算:INT4 相对误差约 11.7%,FP4-E2M1 约 10.9%,NF4 约 9.6%。NF4 比 INT4 好约 18%——不是量级差别,但在同样的位宽下白捡的。这就是 QLoRA 那篇论文选它的理由。

读的时候要小心:11.7% 这个数吓人,但它不等于模型掉 11.7% 的能力

上面所有误差数字都是权重矩阵本身的相对误差(本站用标准正态随机矩阵实算),不是模型能力的损失。真实模型的权重不是随机数,各层对误差的敏感度差得很远,而且误差是零均值的、在几千项的点积里会互相抵消一部分。参照物在第16章:llama.cpp 的 PR #1684 给出 LLaMA-7B 的困惑度 F16 = 5.9066、Q4_K_S = 6.0215——权重层面 10% 量级的相对误差,最后落在困惑度上只有 1.9% 的上升。这两个百分比不是同一个东西,别混着用。

NF4 的格子是按正态分布的等概率分位点摆的。请构造一个具体的数据分布,使得INT4 反而比 NF4 准,并说明理由。再回答:这个反例对「用 NF4 存模型权重」这件事构成威胁吗?

先想:NF4 的优势建立在什么假设上?这个假设什么时候不成立?
关键在于 NF4 把格子密集地放在 0 附近,是因为它假定数据集中在 0 附近。如果数据根本不集中在 0 附近呢?
第一步这样走:构造一个均匀分布——比如 −1 到 1 之间等概率取值。想想在这种数据上,NF4 两端那几个稀疏的大格子会发生什么。
完整答案:取 [−1, 1] 上的均匀分布。这时每个数值区间的数据量一样多,最优的格子摆法就是等距——INT4 正好是等距的,NF4 却把格子挤在中间,导致 0.7 到 1.0 这一段(占了 15% 的数据)只有两三个格子可用,误差反而更大。更极端的反例是双峰分布(数据集中在 ±0.8 两处),NF4 在那两个峰上几乎没有格子。
威胁不大,理由有两层:① 神经网络权重经验上确实接近正态,这个假设在这个具体场景下站得住;② 更重要的是分组——每 64 或 128 个权重单独算缩放因子后,组内分布会比全局分布更接近正态。但这个反例点出了一件真事:NF4 的优势是有前提的,它不是一个「普遍更好的 4 位格式」,而是一个「对正态数据更好的 4 位格式」。任何一篇说 NF4 优于 INT4 的材料,你都该追问一句:在什么分布上。

变式:如果一层权重经过训练后明显偏向正数(比如均值 0.3),对称量化 + NF4 会吃什么亏?非对称量化能不能补回来?

15.6 官方 FP8 版是怎么做的

前面讲的都是社区做法。官方自己也发了量化版:Qwen3.8-27B-FP8,2026 年 8 月 13 日建仓,同样是 Apache 2.0。

模型卡对做法只有一句话:fine-grained fp8 quantization with block size of 128——细粒度 FP8 量化,块大小 128。把这句话拆开,你已经全懂了:FP8 是第0章表 0-4 里的 E4M3(1 符号 + 4 指数 + 3 尾数,最大值约 448);block size 128 就是 15.4 节的分组大小 g = 128;fine-grained(细粒度)是相对于「整张张量一个缩放因子」而言的。

用式 15-4 估一下它的实际位宽:8 + 16 ÷ 128 = 8.125 位,对应体积约 27.36 × 8.125 ÷ 8 = 27.8 GB。作为对照,社区的 Q8_0 实测 29.05 GB(反算 8.50 位),BF16 是 54.66 GB。FP8 版本大约是 BF16 的一半。

读的时候要小心:「几乎相同」是厂商自述

模型卡同时称这个 FP8 版本的表现与原始模型nearly identical(几乎相同)。这句话本站无法验证,理由有三:① 它是模型发布方对自家产品的自评,不是第三方评测;② 模型卡没有给出任何逐项对照表——没有列出在哪些基准上测的、各测了多少分、原始模型的对照分数是多少;③ Qwen3.8 没有 arXiv 技术报告(本站检索 arXiv 全库为 0 条),也就没有同行评审过的实验细节可查。
正确的读法是:把它当成一条厂商声明记下来,而不是当成一个已验证的结论。第16章会给出一批第三方的实证研究,那些研究的结论是「量化在某些任务上确实掉得更明显」——两边并不矛盾,因为厂商没说是在哪些任务上测的。

把官方 FP8 模型卡那句 fine-grained fp8 quantization with block size of 128 拆成式 15-3 和式 15-4 里的具体量:位宽 b 是多少?分组大小 g 是多少?如果缩放因子用 FP16 存,实际位宽是多少?

先想:这句话里有三个信息,分别对应本章讲过的哪三个概念?
关键在于 block size 就是本章说的分组大小,两个词说的是同一件事。
第一步这样走:b = 8(FP8),g = 128。代进式 15-4:8 + 16 ÷ 128 = ?
完整答案:b = 8g = 128,实际位宽 = 8 + 16÷128 = 8.125 位,体积约 27.8 GBfine-grained 这个词本身不是一个数,它是在强调「不是整张张量共用一个缩放因子」——也就是 15.4 节讲的那件事。官方模型卡用一句话交代的做法,你现在可以完全展开成公式。

变式:官方为什么选 128 而不是 32?(提示:从式 15-4 和表 15-2 两边看——32 能多降多少误差,又要多付多少位。此外 128 也和 GPU 上矩阵乘法的分块尺寸对得上,但这一条本站没有官方出处,属于合理推测。)

一位工程师手上有一张 24 GiB 的卡,想跑 Qwen3.8-27B。他打算「把精度整体降一半」,于是同时做两件事:把权重从 BF16 换成 Q4_K_M,把 KV cache 从 fp16 换成 fp8。请回答四问:
(a) 这两件事省下来的分别是哪一笔账?
(b) 各省多少(KV 按 32K 上下文算)?
(c) 其中哪一件今天改个启动参数就能做,哪一件必须换一个文件?
(d) 如果他把「权重换成 Q4_K_M」理解成「模型变小了、参数变少了」,他接下来最可能算错哪一个数?

先想:第5章讲的 KV cache 显存公式里有没有出现「权重位宽」这个量?式 15-1 里有没有出现「上下文长度」?两个公式共用了哪个变量?
关键在于两笔账完全不共享任何变量:权重账 = 参数个数 × 权重位宽;KV 账 = 每 token 元素数 × KV 位宽 × token 数。前者是买断的固定成本,后者是按量计费。降低其中一个的位宽,对另一个一分钱影响都没有。
第一步这样走:先分别写出两个基线。权重:BF16 = 27.36 × 16 ÷ 8 = 54.72 GB。KV:第5章的每 token 64 KiB(fp16),32K 上下文 = 32768 × 64 KiB = 2.00 GiB。然后各自换位宽再算一遍。
完整答案:
(a) 换 Q4_K_M 省的是权重账;换 fp8 KV 省的是 KV 账。两者互不影响。
(b) 权重:54.66 GB(实测 BF16)→ 17.11 GB(实测 Q4_K_M),省 37.55 GB,是大头。KV:32K 上下文下 2.00 GiB → 1.00 GiB,省 1.00 GiB。注意两个数差了三十多倍——在 32K 这个长度上,压权重的收益远大于压 KV;但到 262K 时 KV 是 16 GiB,压 KV 才变成能救命的那一刀。哪一笔更值得压,取决于你打算开多长的上下文,这是第17章要做的完整预算。
(c) KV 换 fp8 是纯推理选项(llama.cpp 的 --cache-type-k/-v、vLLM 的 KV 量化开关),改个启动参数即可,权重文件一个字节不动。权重换 Q4_K_M 必须下载另一个文件——因为量化是在文件里已经做完的事,运行时改不了。
(d) 他最可能算错的是 KV cache。如果他以为「Q4_K_M 让模型变小了 3 倍」,就会顺手以为 KV cache 也小了 3 倍。但 KV cache 的大小由式 5-1 决定——2 × 全注意力层数 × KV 头数 × head_dim,四个因子里没有一个和权重位宽有关。262K 上下文下的 16 GiB 一分不少。这个错会让他以为 24 GiB 卡能开满 262K 上下文,而第17章会算出真实答案是 8–10 万 token。
这道题真正在考的是第14章那句话的可操作版本:量化改的是「每个数字占几位」,而 KV cache 的大小是由结构(层数、头数、头维)和用量(token 数)决定的。改位宽不会改结构。

变式:他后来发现 24 GiB 还是不够,于是想「那我把 KV 压到 4 位」。这一步能省多少?为什么厂商和框架很少提供 4 位 KV 而 4 位权重满地都是?(提示:想想 KV cache 里每个数被用到几次、以及第5章说的注意力打分那一步对数值误差有多敏感。KV 量化确实有专门的研究,如 KIVI(arXiv:2402.02750)和 KVQuant(arXiv:2401.18079),但它们没有像权重量化那样成为默认选项。)

自己推一遍:预测量化误差,然后去验证它

  1. 对称 absmax 量化里,round 最多把一个数挪半格。如果一组数在格子里分布得比较散,误差大致均匀落在 −s/2 到 +s/2 之间。这种均匀分布的均方根是多少?

    想好了再看

    宽度为 s 的均匀分布,方差是 s²/12,均方根就是 s/√12 ≈ 0.2887s。当初会想到这一步,是因为我们要的不是「最坏能差多少」(那是 s/2,一个太悲观的数),而是「平均差多少」——而衡量矩阵整体误差用的是 Frobenius 范数,本质就是均方根。

  2. 现在把 s 换掉。对称 absmax 量化里 s = Ag / qmax,其中 Ag 是这一组里的最大绝对值,qmax = 2b−1 − 1。把误差表示成相对误差(除以数据本身的标准差 σ),你得到什么?

    想好了再看

    相对误差 ε ≈ (1/√12) × (Ag/σ) / qmax。这个式子非常好用,因为它把三件事分开了:√12 是舍入这个动作贡献的常数Ag/σ 是数据分布和分组大小贡献的qmax 是位宽贡献的。三者互不干扰。

  3. 用这个式子做一个可以立刻验证的预测:标准正态数据、分组 128 时,一组 128 个数的最大绝对值平均约 2.83(这个数你可以自己抽样验证)。那么 INT8 和 INT4 的相对误差各是多少?两者的比值应该是多少?

    想好了再看

    INT8:qmax = 127,ε ≈ 2.83 / (127 × 3.464) = 0.643%。INT4:qmax = 7,ε ≈ 2.83 / (7 × 3.464) = 11.66%。比值 = 127/7 = 18.14——注意 Ag 和 √12 全约掉了,比值只取决于位宽。本站实测的结果是 0.647% 和 11.734%,比值 18.14。三个数全部对上,误差在千分之几以内。这是本章最值得亲手验一遍的地方:一个三行的推导,精确预言了一个 1677 万元素矩阵的实验结果。

  4. 最后一问:为什么分组从 4096 缩到 128(缩 32 倍),误差只降 26%?

    想好了再看

    因为式子里只有 Ag 随分组大小变,而正态随机数的最大绝对值随组大小只按 √(2 ln g) 增长——对数的平方根,慢得可怕。g 从 4096 降到 128,√(2 ln g) 从 4.08 降到 3.11,只降 24%,和实测的 26% 吻合。这就是「分组越小收益越递减」的数学原因,也解释了为什么主流方案都停在 32–128 而不是继续往下切。

答辩:如果我是审稿人

你用「标准正态随机矩阵」测出 INT4 误差 11.7%,然后拿它来讨论真实模型的量化。可真实的权重矩阵不是随机数——它们经过训练,行与行之间高度相关,某些通道的幅度比别的通道大一个数量级。你这个实验的结论凭什么能迁移过去?

参考防守(先自己组织语言再看)

不能直接迁移,这个批评是对的,本站在正文里已经用 callout caution 标了这一点。随机矩阵实验能支撑的只有三条机制性结论:① 误差与 1/qmax 成正比(这一条只依赖 round 的性质,与分布无关);② 缩放因子由组内最大值决定,所以离群值会绑架整组(这一条只依赖 absmax 的定义);③ 分组能把离群值的伤害限制在一组之内(这一条是前一条的直接推论)。这三条在任何分布上都成立。
不能支撑的是数值:真实权重的 11.7% 会是多少、掉多少能力,随机矩阵一个字都没说。真实答案要看真实测量——第16章给出的 llama.cpp 困惑度对照表(F16 5.9066 vs Q4_K_S 6.0215)就是那种测量,而且它显示权重层面 10% 量级的误差只对应 1.9% 的困惑度上升。把这两个数摆在一起,恰恰说明「权重误差」和「模型能力」之间隔着一层很厚的东西——那一层是什么,正是 GPTQ 和 AWQ 两条路线各自的答案。

答辩:如果我是审稿人

你说量化「参数一个没少」,可 15.3 节自己给的例子里,1677 万个权重被还原成了 0。一个恒为 0 的参数和「没有这个参数」有什么区别?你这不是自相矛盾吗?

参考防守(先自己组织语言再看)

这个追问很尖锐,答案要分两层。形式上不矛盾:文件里那个位置仍然存着一个 4 位的编码 0000,矩阵形状没变,第12章数出来的 27.36 B 一个不少,模型结构、层数、头数全都原样。这是「量化不改架构」这句话的准确含义。
但功能上确实等于把那个权重砍掉了——所以这个反例真正证明的是:一个做得足够糟的量化,其效果可以退化成一次粗暴的剪枝。这恰恰强化而不是削弱第14章的论点:量化和剪枝是两条不同的路,但一条做坏了的量化会掉进剪枝的坑里,而且是那种没有任何微调补偿的、最糟糕的剪枝。
还要补一句:那个例子是故意构造的极端情况(全矩阵一个缩放因子 + 一个 100 倍离群值),现实中没有任何一个量化方案会这么做,正因为这个坑早就被踩过了。分组量化的存在本身就是这个坑的墓碑。

答辩:如果我是审稿人

既然分组量化这么有效,为什么不干脆把分组大小设成 1——每个权重配一个自己的缩放因子,误差直接归零?

参考防守(先自己组织语言再看)

因为那样就不是量化了。把 g = 1 代进式 15-4:b实际 = 4 + 16 ÷ 1 = 20 位,比原来的 BF16(16 位)还大 25%。你为了省空间而量化,结果文件反而变大了。
这个极端情形值得记住,因为它把式 15-4 的本质点破了:量化的全部收益来自「让很多个权重共享一份元数据」。共享的人越多越省,但共享的人越多,那份元数据就越难同时照顾所有人——这是一个纯粹的取舍,没有免费解。表 15-2 那四行就是这条取舍曲线上的四个取样点,而 32 到 128 这一段是工程上找到的甜区。

对你而言未知Qwen3.8-27B 的权重分布到底长什么样,哪些通道有离群值?

本章所有误差数字都建立在「标准正态随机矩阵」上,这是一个替身。真正该问的是:Qwen3.8-27B 自己的权重分布长什么样?64 层里哪些层的权重幅度分布最宽?ffn_down(第8章那张把 17408 压回 5120 的矩阵)是不是真的比 gate_proj 更难量化——这是 GGUF 的 Q4_K_M 给它更高位宽的理由,但那个理由是在 LLaMA 上发现的,没人公开验证过它在 Qwen3.8 上是否成立。
这个问题不属于「学界无解」,它属于第二类:答案是可以测出来的,只是没人把这份测量公开发布。你需要的东西全都是公开的——Apache 2.0 的权重、Hugging Face 的 safetensors 读取接口、以及本章的公式。

先做这一步:从 Hugging Face 下载 Qwen3.8-27B 的一个分片(不用下全部 54.66 GB,随便一个 safetensors 分片就够),用 safetensors 库只读出一层ffn_downgate_proj 两张矩阵。对每张矩阵,按行切成 128 个数一组,算出每组的 max|x| / std(x),画成直方图。你要看的是:这个比值的分布有没有长尾?两张矩阵的长尾一样长吗?然后把你的直方图和本章那个「随机正态时约 2.83」的基准比一比——超出多少,就是真实权重比随机数难量化多少。做完这一步,你就有了一份公开材料里查不到的一手数据。

这一层要加什么:写一个 quantize(W, bits, group_size)

为什么现在才加它:前十四层造的都是结构——层、头、矩阵、参数。这一层第一次不动结构,只动每个数字怎么被写下来。它是全站唯一一个可以作用在「已经造好的模型」上的操作,这也正是第14章那张四轴表里第 ④ 轴的全部含义。写完这个函数,你手上第一次有了一个能把自己造的模型压小的工具。

def quantize(W, bits, group_size, sym=True): x = flatten(W) g = group_size if group_size > 0 else len(x) X = pad_and_reshape(x, g) # 形状 [组数, g] if sym: qmax = 2**(bits-1) - 1 s = rowwise_absmax(X) / qmax # 每组一个 s q = clip(round(X / s), -qmax-1, qmax) Xhat = q * s else: qmax = 2**bits - 1 s = (rowwise_max(X) - rowwise_min(X)) / qmax z = round(-rowwise_min(X) / s) q = clip(round(X / s) + z, 0, qmax) Xhat = (q - z) * s return unpad_and_reshape(Xhat, shape_of(W)) def rel_err(W, What): # Frobenius 相对误差 return norm(W - What) / norm(W) def eff_bits(bits, group_size, sym=True): # 式 15-4 return bits + (16 if sym else 32) / group_size

难点一:分组要在展平之后做,不能按行做。真实的权重矩阵形状五花八门(5120×17408、17408×5120……),如果你按行切分组,行长不是 128 的整数倍时最后一组会短一截,误差统计就被污染了。正确做法是先展平成一维、补齐、再 reshape。这一步很容易糊弄过去,而且不会报错——只会让你的误差数字比别人的高一点点,你还找不到原因。

难点二:clip 那一步不是可有可无的。对称量化时 round(X/s) 理论上不会超出 [−qmax, qmax],因为 s 就是按最大值定的。但浮点除法有舍入误差,绝对值最大的那个元素偶尔会算出 qmax+1,写进 4 位的格子里会溢出成一个负数——一个巨大的、位置随机的错误。这类 bug 在小矩阵上测不出来,一上 1677 万个元素必现。

难点三:缩放因子为 0 要单独处理。如果某一组权重全是 0(真实模型里存在这样的组),rowwise_absmax 返回 0,除法直接得到 NaN,然后 NaN 会顺着矩阵乘法污染整个模型的输出。加一行 s[s == 0] = 1e-12 就行,但你得先知道会有这一天。

自己验:生成一个 4096×4096 的标准正态随机矩阵,跑三组对照,三个现象都要复现出来。
① 位宽的效果。rel_err(W, quantize(W, 8, 128)) 应在 0.6%–0.7%(本站实测 0.647%);rel_err(W, quantize(W, 4, 128)) 应在 11%–12%(本站实测 11.734%)。两者的比值必须落在 18.0–18.3——它应该正好约等于 127÷7 = 18.14,这条比数值本身更硬,因为它与随机种子无关。
② 分组的效果。group_size 从 128 改成 4096,INT4 误差应涨到 15%–16%;再改成「全矩阵一个缩放因子」(group_size = W.size),应涨到 20%–25%(本站四个随机种子分别得到 22.07%、23.43%、24.99%、23.16%——这一项会随种子波动,因为它取决于全矩阵那一个最大值,你的数落在区间里就算对)。
③ 离群值的效果。W[0][0] 改成 100.0,其余一个不动。全矩阵一个缩放因子时,INT4 误差应爆炸到 99% 以上;同时打印 len(unique(Xhat)),应该正好是 2(只剩 0 和 100)。而 group_size=128 时误差应几乎不动(本站实测 11.734% → 11.734%,变化在万分之一以内)。
三个现象都对上,这一层就成了。若 ① 的比值不是 18.14 而是接近 16,说明你的 qmax 写成了 2b−1(8 和 128)而不是 2b−1−1(7 和 127);若 ③ 里 unique 的个数是 3 而不是 2,说明你的 round 用的是「四舍六入五成双」以外的规则,不影响结论,可以放过。

本章小结

  • 量化只改一件事:每个参数占几位。式 15-1:VN × b ÷ 8。参数个数 N 从头到尾没动过,27.36 B 就是 27.36 B。
  • 实测总比理论大:反算实际平均位宽(式 15-2),Q4_K_M 是 5.00 位而不是 4,Q8_0 是 8.50 位而不是 8,连 UD-IQ2_XXS 都是 2.63 位而不是 2。没有一个例外。
  • 线性量化两件套:缩放因子 s 管格子多宽,零点 z 管格子摆在哪。对称量化省掉 z,适合权重;非对称保留 z,适合单边分布的激活值。
  • 误差的大头不是舍入,是离群值:一个 100 倍的大数就能让全矩阵共用缩放因子的 INT4 量化误差冲到 99.97%,还原矩阵里只剩 0 和 100 两个值。这是 LLM.int8()(arXiv:2208.07339)指出的核心问题。
  • 分组是第一道解药,也是第一笔额外开销:式 15-4,b实际 = b + (bs+bz)/g。128 个 INT4 + 一个 FP16 缩放因子 = 4.125 位/权重。分组小到 16 就是 5.0 位。
  • 格子可以不等距:FP4-E2M1 靠浮点的天然刻度、NF4(arXiv:2305.14314)靠正态分布的等概率分位点,在同样 4 位下都比 INT4 准(本站实算 10.9% / 9.6% vs 11.7%)。前提是数据真的集中在 0 附近。
  • 官方 FP8 版:模型卡原话是block size of 128 的细粒度 FP8 量化,实际位宽约 8.125,体积约 27.8 GB。称与原模型「几乎相同」,但没有公开逐项对照表,属厂商自述。

本章留了一笔账没算完:Q4_K_M 的 5.00 位里,分组开销只解释了 0.125 位,还有 0.875 位不知去向。下一章会告诉你那 0.875 位花在哪儿了——答案不是「所有权重都多用了一点」,而是「有一部分权重被特别关照,用了 6 位以上」。顺带把 GPTQ、AWQ、SmoothQuant、NVFP4 这一堆名字各自在解决什么问题,摆到同一张地图上。

第16章 量化②:GPTQ / AWQ / GGUF / FP8 / NVFP4 各自在干什么

上一章留了一笔账:Q4_K_M 的实际平均位宽是 5.00 位,分组开销只解释了 0.125 位,还有 0.875 位不知去向。这一章把那 0.875 位逐字节还原出来,顺带把 GPTQ、AWQ、SmoothQuant、K-quant、NVFP4 这一堆吓人的名字摆到同一张地图上——你会发现它们解决的根本不是同一个问题。

学完这一章你应该能做到

  • 看到一个量化方法的名字,能说出它在解决「舍入」「离群值」「哪些权重更重要」「硬件支持」四个问题里的哪一个
  • 读懂 W8A8W4A16 这种记号,并说清「只量化权重」和「权重激活都量化」为什么是两件事
  • 推出 Q4_K 的 4.5 bpw 是怎么由 144 个字节拼出来的,并解释连缩放因子本身都被量化了这件事省了多少
  • 用一个混合位宽的分配模型,把实测的 17.11 GB 夹在自己算出来的两个数中间
  • 说清 NVFP4 和 MXFP4 的两处差异(block 16 vs 32、scale 用 E4M3 vs E8M0),并用式 15-4 算出各自的实际位宽
  • 对任何一条「量化后性能几乎不变」的说法,问出正确的三个追问
前置:第15章的式 15-1(体积 = 参数量 × 位宽 ÷ 8)、式 15-3(缩放因子与零点)、式 15-4(实际位宽 = 位宽 + 元数据位 ÷ 分组大小),以及第0章的 FP8-E4M3 / E5M2 的位划分。第14章的四轴表会在最后一节被回指。

16.1 一张地图:这些名字分别在解决什么

先问:不先画这张地图会怎样

关于量化的科普文最常见的写法,是把七八个名字排成一列然后说「各有优劣」。读完你会以为它们是七八个互相竞争的方案,从中选一个就行。不是这样。它们里有的在解决舍入怎么舍,有的在解决离群值往哪放,有的在解决哪些权重不能亏,有的干脆只是一份硬件规范。有些可以叠加使用,有些根本不在同一个层面上。先分清层面,后面每一节才有地方挂。

先约定一个记号,后面到处要用。W8A8 的意思是权重Weight)用 8 位、激活值Activation)也用 8 位;W4A16 就是权重 4 位、激活值保持 16 位。这个区分极其重要:

只量化权重weight-only quantization,即 WxA16):权重压小了存进文件,推理时先解压回 16 位再做矩阵乘法。省的是显存和显存带宽,不省算力。好处是激活值不用动,误差来源只有一个。权重和激活都量化W8A8 这一类):矩阵乘法本身用低位宽的硬件指令来做,算力也一起省了,但激活值是运行时才产生的、随输入变化,量化难度比权重大得多。

表 16-1:主要量化方法各自在解决什么(本站整理,arXiv 号见每行)
名字它解决的问题动权重还是动激活典型位宽
LLM.int8()
arXiv:2208.07339
离群值。发现激活值里有少数幅度极大的维度,一刀切量化会毁掉整个模型混合精度分解:离群的那几维留在 16 位,其余走 8 位W8A8
GPTQ
arXiv:2210.17323
怎么舍入才最不亏。用近似二阶信息逐列量化,每量化一列就用还没量化的列去补偿它造成的误差只量化权重4 bit
AWQ
arXiv:2306.00978
哪些权重不能亏。权重不是同等重要,按激活的幅度找出约 1% 的显著通道加以保护只量化权重4 bit
SmoothQuant
arXiv:2211.10438
激活太难量化。把激活的量化难度数学等价地迁移一部分到权重上,让两边都变得好量化权重 + 激活W8A8
NF4 / QLoRA
arXiv:2305.14314
格子该摆在哪。等距格子浪费在没数据的地方,改用匹配正态分布的非等距格子只量化权重4 bit
GGUF K-quant
llama.cpp PR #1684
位宽该怎么分配。不同张量对误差的敏感度不同,按重要性给不同的张量分不同的位宽只量化权重2–8 bit 混合
FP8 / NVFP4 / MXFP4
OCP 规范 / NVIDIA
硬件认不认。前面那些是算法,这三个是数据格式规范——芯片里有专门的电路直接算这种数权重 + 激活8 / 4 bit

盯着最后一行看:它和前面六行不是一个层面的东西。GPTQ 是一套「怎么把数字压下去」的算法,NVFP4 是一份「压下去之后长什么样」的格式约定。你完全可以用 GPTQ 的算法去产出一个 NVFP4 格式的文件。把它们并排放进「量化方法对比表」是很多科普文的通病。

某个部署方案标着 W4A16。(a) 它的权重文件大约是 BF16 版本的几分之一?(b) 它的矩阵乘法用几位的硬件指令做?(c) 它省的是显存、算力,还是两者都省?

先想:W 后面那个数管什么,A 后面那个数管什么。文件里存的是权重还是激活值?
关键在于激活值是运行时才产生的中间结果,根本不存在文件里。所以文件大小只由 W 决定。而矩阵乘法要两个操作数都是同一种精度才能用低位宽指令。
第一步这样走:(a) 用式 15-1,位宽从 16 变 4。(b) 想一想 A16 意味着计算时激活值是 16 位,那么权重必须先变回什么。
完整答案:(a) 约 1/4(不算分组开销;算上的话是 1/3.9 左右)。(b) 16 位——因为激活值是 16 位的,权重必须先解压回 16 位才能和它相乘。(c) 只省显存(和显存带宽),不省算力。这一条是很多人的盲区:W4A16 的模型在显存上瘦了四倍,但每秒能算多少次乘加和 BF16 版本一模一样。它之所以在消费级显卡上还是更快,是因为大模型生成 token 时的瓶颈通常是把权重从显存搬到计算单元,而不是乘加本身——搬的东西少了四倍,自然快。

变式:那 W8A8 呢?它比 W4A16 的文件大一倍,为什么在服务器上反而常被优先选择?(提示:想想 A8 意味着乘法可以用什么指令做,以及服务器上一次要同时处理多少个请求。)

16.2 GPTQ 与 AWQ 的分野

这两个名字最常被并排提起,也最常被读者当成「同一件事的两个版本」。它们确实都只量化权重、都做到 4 位、都发生在训练之后(Post-Training 这个词就写在 GPTQ 的标题里)。但出发点是两个方向。

GPTQ 问的是:既然一定要舍入,怎么舍才最不亏?

朴素做法是每个权重各自四舍五入,互不相干。GPTQ 说这样太浪费:一列权重被舍入之后,这一层的输出就偏了一点;既然后面还有很多列没量化,为什么不让还没量化的那些列稍微改一改,把刚才这一点偏差抵消掉?它逐列往下走,每量化完一列就更新剩下的列。决定「该改多少」用的是这一层重建误差的近似二阶信息——也就是误差曲面的弯曲程度,弯得厉害的方向要少动,平坦的方向可以多动。

注意这一步的性质:GPTQ 改动了权重的数值。量化后的权重不再是原权重的四舍五入,而是「为了让这一层的输出尽量不变,而故意挑出来的一组值」。

AWQ 问的是:这些权重真的同等重要吗?

答案是不。AWQ(Activation-aware Weight Quantization,激活感知的权重量化)的核心观察是:有约 1% 的权重通道特别重要,把它们保护好,其余的可以放心压。关键在于「重要」的判据不是权重自己有多大,而是流经它的激活值有多大——一个数值不大的权重,如果总是乘上一个很大的输入,它的误差就会被放大很多倍。

找到这些显著通道之后,AWQ 没有把它们留在 16 位(那样会变成混合精度,硬件不好办),而是先把这些通道逐通道放大,再统一量化。放大之后它们在格子里占据的位置更靠上、分辨率相对更细,误差就小了;推理时再把对应的输入按相同倍数缩小,数学上完全等价。

打个比方:同一份预算的两种花法

你要把一屋子家具搬进一个小房间,注定放不下所有东西。GPTQ 的做法是:每挪走一件,就把剩下的家具重新摆一摆,让整个房间看起来尽量还像原来那样。AWQ 的做法是:先判断哪几件是主人每天都要用的(不是最贵的,是用得最多的),给它们单独留出好位置,其余的按常规塞。

类比失效处:真实的家具是离散的、可以整件保留;权重量化里没有「整件保留」这个选项——AWQ 的保护手段是缩放,不是不量化。而 GPTQ 的「重新摆一摆」是有严格数学目标的(最小化该层输出的重建误差),不是凭感觉。两者也不是互斥的,工程上完全可以叠加。

一句话记住:GPTQ 在回答「怎么舍」,AWQ 在回答「谁不能亏」。两条完全不同的思路,都到达了 4 位。它们的共同前提是同一个——上一章那条结论:4 位的格子太少,必须有人在旁边帮忙。

AWQ 判断一个权重通道是否「显著」,看的是流经它的激活值的幅度,而不是权重本身的大小。请说明:如果改用「权重绝对值最大的 1%」当判据,会在什么情况下选错?举一个具体的场景。

先想:一层的输出是权重乘激活再求和。一个权重对输出的影响,只由它自己的大小决定吗?
关键在于乘法:某一项对输出的贡献是「权重 × 激活」。这两个因子里任何一个大,乘积都可能大;任何一个小,乘积都可能小。只看其中一个等于漏掉一半信息。
第一步这样走:构造两个通道。通道 A 权重 0.9、平均激活 0.001;通道 B 权重 0.01、平均激活 300。分别算一下「量化误差 × 激活」这个乘积。
完整答案:设量化的绝对误差都是 ε。通道 A 对输出的误差贡献是 ε × 0.001;通道 B 是 ε × 300,差 30 万倍。但按「权重最大」的判据会去保护 A(0.9 > 0.01),把真正要命的 B 扔了。
这个场景在真实模型里不是假想:LLM.int8()(arXiv:2208.07339)报告的正是激活值里存在幅度极大的少数维度。这些维度对应的权重通道数值可能平平无奇,但它们承载着模型的关键信息通路。AWQ 的全部创新就在这个判据上——不是发明了新格式,也不是发明了新算法框架,而是把「谁重要」的定义从权重侧换到了激活侧。这也是为什么这个方法叫「激活感知的权重量化」:判据来自激活,动手的对象是权重。

变式:既然激活幅度这么关键,为什么 AWQ 还需要一批「校准数据」(calibration data)才能跑?如果校准数据和实际用途差很远(比如用英文语料校准、拿去处理代码),会有什么风险?

16.3 GGUF 的 K-quant:为什么 Q4_K_M 是 4.5 bpw 不是 4

现在还上一章那笔账。

GGUF 是 llama.cpp 用的模型文件格式,也是消费级显卡上事实上的通用格式——第17章列的那些工具(llama.cpp、Ollama、LM Studio)全都吃它。它里面的量化方案叫 K-quant,一手出处是 llama.cpp 的 PR #1684。那个 PR 给出的每种档位的位宽是这样的:

表 16-2:K-quant 各档位的标称位宽(llama.cpp PR #1684 原始定义)
档位bpw(位/权重)每 256 个权重占多少字节LLaMA-7B 困惑度(同一 PR)
F16(基准)165125.9066
Q2_K2.5625826.7764(+14.7%)
Q3_K3.4375110——
Q4_K4.5144Q4_K_S:6.0215(+1.9%)
Q5_K5.5176——
Q6_K6.56252105.9110(+0.07%

那个 PR 对 Q6_K 的评价原话是:within 0.1% or better from the original fp16 model——与原始 fp16 模型的差距在 0.1% 以内或更好。用表里的数字验一下:5.9110 ÷ 5.9066 = 1.00074,确实是 0.074%。

第一个 0.5 位:连缩放因子本身都被量化了。

Q4_K 的 4.5 bpw 是这么来的。它以 256 个权重为一个超块,超块内再切成 8 个小块、每小块 32 个权重。每个小块有自己的缩放因子和最小值(非对称量化,见表 15-1)——但这两个数不用 FP16 存,而是各用 6 位存;这 6 位的值再由整个超块共享的两个 FP16 数字还原成真实小数。数一数字节:

128 + 12 + 4 = 144 字节 ⇒ 144 × 8 ÷ 256 = 4.5 bpw
式 16-1
这一项是什么为什么是这个数
128 字节256 个 4 位量化值256 × 4 ÷ 8 = 128。这是唯一「有用负载」的部分
12 字节8 个小块各自的缩放因子和最小值8 × (6 + 6) 位 = 96 位 = 12 字节。缩放因子只用 6 位,是被量化过的
4 字节超块级的两个 FP16一个用来把 6 位缩放因子还原成真实值,一个还原最小值。这是第二级缩放因子
4.5 bpw实际平均位宽与 PR #1684 给的 4.5 分毫不差

对照一下朴素做法:如果 8 个小块各存一个 FP16 缩放因子和一个 FP16 最小值,那就是 128 + 8×4 = 160 字节,5.0 bpw。K-quant 靠「把缩放因子本身也量化到 6 位」省下了 0.5 bpw——整整 10% 的文件体积。记住这个「两级缩放」的结构,16.4 节的 NVFP4 用的是同一个套路。

第二个来源:M 是 Medium,指的是混合。

Q4_K_M 里的 _M 不是一个新格式,而是一份分配方案。llama.cpp 对它的定义是:注意力的 wv(也就是 v_proj)和前馈网络的 w2(也就是第8章那张把 17408 压回 5120 的 down_proj)里,各有一半的张量用 Q6_K,其余全部用 Q4_K。

这就解释了为什么名字里写着 4,实际却不止 4.5:模型里有一部分张量根本没走 4 位这条路。

现在把整个账算给你看。Qwen3.8-27B 的 27.36 B 参数按第12章的点钞结果分组,套上面的规则:

表 16-3:三种分配方案下的体积(本站按第12章参数分组 + PR #1684 的 bpw 定义计算)
分配方案平均位宽算出的体积和实测 17.11 GB 比
全部 4 位(无任何开销的理想值)4.00013.68 GB低 20%
全部走 Q4_K(纯 4.5 bpw)4.50015.39 GB低 10%
PR 原文规则:一半 wv + 一半 w2 走 Q6_K4.71816.13 GB低 5.7%
再把嵌入层、输出头、视觉塔也提到 Q6_K4.94516.91 GB低 1.2%
实测文件5.00417.11 GB——
全部 6 位6.00020.52 GB高 20%

一步一步逼近的过程本身就是答案:纯 4 位差 3.43 GB,加上分组开销补回 1.71 GB,加上一半 wv/w2 升到 6 位再补 0.74 GB,加上嵌入层与输出头升到 6 位再补 0.78 GB——剩下 0.20 GB 落在 GGUF 的容器开销和本站没有精确建模的那些张量上。上一章那 0.875 位的去向,到这里就交代完了。

这个拆解台让你亲手拨那几个开关:把哪些张量提到 Q6_K、超块和小块各多大、缩放因子用几位存,右边实时显示平均位宽和总体积,并把实测的 17.11 GB 画成一条线让你去够。你会很快发现一件事——能显著改变总体积的只有 FFN 那一组(占 62.6% 的参数),把注意力层全提到 8 位也只动几百 MB。这正是第8章那张参数占比图在量化上的直接后果。

最后给一个交叉验证,这是本站能给出的最硬的一条。用式 15-2 把 Unsloth 每一个档位的实测文件反算成平均位宽:

表 16-4:实测文件反算的平均位宽(体积取自 Unsloth 逐文件真实大小,分母统一用 27.36 B)
档位实测体积反算平均位宽标称/理论位宽差多少
BF1654.66 GB15.9816−0.1%
Q8_029.05 GB8.508.5(= 8 + 16÷32)0.0%
Q6_K22.88 GB6.696.5625+2.0%
Q5_K_M19.83 GB5.805.5+5.5%
Q4_K_M17.11 GB5.004.5+11.2%
IQ4_XS15.71 GB4.59————
Q3_K_M13.82 GB4.043.4375+17.5%
UD-IQ2_XXS9.01 GB2.632.5625(Q2_K)+2.7%

头两行是这张表的价值所在:BF16 反算出 15.98,应该是 16,误差 0.1%;Q8_0 反算出 8.50,而 Q8_0 的理论位宽正是 8 + 16÷32 = 8.5,一分不差。两个已知答案都对上了,说明这把尺子是准的。用同一把准尺子去量 Q4_K_M,得到 5.00 —— 比标称的 4.5 高 11.2%,而这 11.2% 正是上面那张分配表算出来的混合位宽。两条独立的路径给出了同一个结论。

读的时候要小心:分母里到底装了什么,材料没说清

表 16-4 的分母统一用了 27.36 B(文本塔 26.90 B + 视觉塔 0.46 B)。但严格说,GGUF 的主文件里可能不含视觉塔——视觉塔在单独的 mmproj 文件里(0.93 GB,第17章会用到)。同时 config 里还有一个 mtp_num_hidden_layers: 1 的多 token 预测草稿头,第12章的 27.36 B 没有把它算进去,而 GGUF 文件可能算了。巧的是这两者的参数量接近,所以两种口径下分母差不多,结论的量级不受影响。
顺带一个可以自己验的观察:mmproj 那 0.93 GB 除以视觉塔的 0.46 B 参数,得到 约 16 位/参数——也就是说视觉塔基本没被量化,是按 F16 原样打包的。这是本站按体积倒推的结论,Unsloth 页面没有明说。想证伪很容易:用 gguf 库读文件头,逐张量打印它的 ggml_type,一分钟就见分晓。

Q6_K 的一个超块(256 个权重)占 210 字节,其中 128 字节存每个权重的低 4 位、64 字节存高 2 位、16 字节存 16 个 int8 缩放因子(每 16 个权重一个)、2 字节存超块的 FP16 缩放因子。(a) 验证它的 bpw 是 6.5625。(b) 如果那 16 个缩放因子改用 FP16 存,bpw 会变成多少?(c) 由此说明「两级缩放」在 Q6_K 上省了多少。

先想:bpw 的定义是「这一块占的总位数 ÷ 这一块装了几个权重」。字节要先乘 8 变成位。
关键在于 (b) 只有一项要改:16 个 int8 变成 16 个 FP16,即 16 字节变成 32 字节,其余三项不动。
第一步这样走:(a) (128+64+16+2) × 8 ÷ 256 = ? (b) 把 16 换成 32 再算一遍。
完整答案:(a) 210 × 8 ÷ 256 = 6.5625,与 PR #1684 的定义分毫不差。(b) (128+64+32+2) × 8 ÷ 256 = 226 × 8 ÷ 256 = 7.0625。(c) 省了 0.5 bpw,约占文件体积的 7.1%
把这个数和正文里 Q4_K 省的 0.5 bpw 摆在一起看,会发现一个规律:K-quant 在每个档位上都省下大约 0.5 位,办法都是同一个——不让缩放因子用满 16 位。而缩放因子之所以可以被压到 6 位甚至 8 位整数,是因为它们本身也扎堆分布,同一个超块内的 8 个缩放因子彼此差不了太多。这是把第15章的分组量化思想又套了一层,用在了元数据自己身上。

变式:Q5_K 的超块是 176 字节。照 Q4_K 的结构猜一猜这 176 是怎么拼出来的(提示:比 Q4_K 多的那 32 字节是干什么的),并验算 bpw 是不是 5.5。

16.4 2026 年的新东西:NVFP4 与 MXFP4

先问:已经有 INT4、NF4 了,为什么还要新格式

因为前面讲的全是软件做法——数据在显存里是 4 位的,进了计算单元还得先解压回 16 位再乘。真正的加速要靠芯片里有直接算 4 位浮点的电路。而芯片电路只认死规格:几位指数、几位尾数、多少个数共享一个缩放因子、缩放因子什么格式,全都得钉死。这就是这一节两份规范存在的理由——它们不是算法,是让硬件能认的约定

两份规范都把元素格式定成了 E2M1(1 符号 + 2 指数 + 1 尾数,就是第15章图 15-1 中间那一行)。分歧在别处:

表 16-5:MXFP4 与 NVFP4 的差异(OCP Microscaling Formats v1.0 / NVIDIA 官方文档)
MXFP4(OCP MX 规范 v1.0)NVFP4(NVIDIA)
元素格式E2M1(4 位)E2M1(4 位)
block size3216
缩放因子格式E8M0(8 位,只能表示 2 的幂FP8 E4M3(8 位,能表示小数
第二级缩放每张张量一个 FP32 缩放因子
实际位宽(式 15-4)4 + 8÷32 = 4.254 + 8÷16 = 4.5
定位通用微缩放标准,多家联署更细的组、更准的缩放因子

E8M0 那一格是最值得琢磨的。8 位全给指数、一位尾数都不留,意味着这个缩放因子只能取 …、0.25、0.5、1、2、4、8、… 这些 2 的整数次幂,中间的值一个都取不到。假设某一块 32 个数的绝对值最大是 1.3:用 MXFP4 你只能选 2(于是量程白白浪费 35%,相当于损失了大半个有效位)或者选 1(于是那个 1.3 被截断)。NVFP4 的 E4M3 能表示 1.25 或 1.5,贴得紧得多。

代价当然是有的:NVFP4 的组只有 16 个数,按式 15-4 每个权重要摊 0.5 位的缩放因子开销,比 MXFP4 的 0.25 位贵一倍。两者不是谁对谁错,是同一条取舍曲线上的两个取样点——正是第15章表 15-2 那条曲线。

顺带注意 NVFP4 的第二级 FP32 缩放因子:这和 16.3 节 Q4_K 的两级缩放是同一个思路——用一个高精度的全局数,去还原一堆低精度的局部数。两个完全独立的团队、两个完全不同的场景,撞出了同一个结构。

NVIDIA 官方博客给出的一组对照数据(在 DeepSeek-R1-0528 上):MMLU-PRO 是 85%(FP8)对 84%(NVFP4),AIME 2024 是 89% 对 91%,结论写作 1% or less accuracy degradation。显存方面称相对 FP16 降 3.5 倍、相对 FP8 降 1.8 倍

这两个倍数可以用式 15-4 当场验:16 ÷ 4.5 = 3.56,8.125 ÷ 4.5 = 1.81厂商给的两个压缩比,用本站上一章那个三项公式就能复现出来。

读的时候要小心:这是 NVIDIA 自己的评测,而且不是在 Qwen3.8 上做的

三条限定必须同时记住。① 评测方是格式的提出方,属于厂商自述,不是第三方复现。② 测试对象是 DeepSeek-R1-0528,不是 Qwen3.8——两个模型的架构、训练数据、权重分布都不同,结论不能直接搬。③ 那两个数字本身在互相打架:MMLU-PRO 上 NVFP4 低 1 个点,AIME 上 NVFP4 反而高 2 个点。量化不可能凭空提升能力,所以后一条只能是评测噪声——AIME 这类基准的题目数量只有几十道,一两道题的对错就是几个百分点。
这组数据能支持的结论只有一条:在这个模型、这两个基准上,两种格式的差距落在评测噪声的量级里。它不支持「NVFP4 比 FP8 更准」,也不支持「在 Qwen3.8 上也是这样」。

最后说一个值得记下来的现象。Qwen3.8 发布之后的 48 小时内,Hugging Face 上出现了 100 多个派生仓库,其中相当比例是 NVFP4。这是 Blackwell 一代的量化格式第一次在开源模型发布当天就成为主流选项,而不是几周之后才补上——一个新数据格式从规范发布到生态默认支持的时间窗,被压到了以小时计。

E8M0 缩放因子只能取 2 的整数次幂。请构造一个具体的 32 元素块,使得 MXFP4 的量程浪费接近最大;算出浪费了百分之多少,并说明这相当于损失了几位有效精度。然后回答:什么样的块会让这个缺点几乎消失?

先想:缩放因子要保证块内最大值不溢出,所以它只能往大了取。什么时候「往大取」的代价最大?
关键在于两个相邻的 2 的幂之间差一倍。如果块内最大值刚好比某个 2 的幂大一点点,缩放因子就得跳到下一个幂,量程几乎浪费一半。
第一步这样走:取块内最大值 = 1.01(刚过 1)。缩放因子只能取 2。算一算实际用到的量程占多少。
完整答案:最大值取 1.01 时缩放因子必须取 2,实际用到的量程只有 1.01÷2 = 50.5%浪费接近一半。半个量程等于少了一位有效精度——4 位的 E2M1 在这一块上退化成了大约 3 位的效果。最坏情况的极限是无限接近 50%(最大值无限接近某个 2 的幂再多一点点)。
反过来,如果块内最大值恰好就是 2 的整数次幂(比如正好 1.0、2.0、4.0),缩放因子一点不浪费,MXFP4 和 NVFP4 在这一块上表现相同。
这道题真正要说明的是:MXFP4 的这个缺点是「平均意义上损失约半位」,不是「永远差一半」。而 NVFP4 用 E4M3 换来的正是这半位——代价是 block 从 32 缩到 16,按式 15-4 每个权重多付 0.25 位。付 0.25 位,买回大约 0.5 位,这笔买卖看起来划算,这就是 NVIDIA 的论证。但它成立的前提是「块内最大值均匀落在两个幂之间」,而真实权重是不是这样,没有公开数据。

变式:如果把 MXFP4 的 block size 从 32 改成 16(其余不变),实际位宽变成多少?它能不能靠这一招追平 NVFP4 的精度?(提示:E8M0 那半位的损失和 block size 有没有关系?)

16.5 量化到底掉多少能力

到这里为止,本章讨论的全是体积数值误差。但读者真正关心的是第三件事:模型会变笨吗?

诚实的回答是:会掉一些,掉多少取决于任务,而且关于 Qwen3.8 的实测数据目前一份都没有。能给的只有方向性的实证研究,全都是在别的模型上做的:

表 16-6:量化对能力影响的几项实证研究(均在 Qwen3.8 上进行)
研究它发现的方向为什么对 Qwen3.8 格外相关
arXiv:2504.04823推理模型(长思维链)掉得更明显Qwen3.8 默认开启 thinking,用 reasoning_effort 调深度,正是这类模型
arXiv:2407.03211非英语语种掉得比英语明显中文使用者是这个模型的主要人群之一,这条直接关系到你的实际体验
arXiv:2505.20276长上下文任务上的影响与短任务不同Qwen3.8-27B 原生 262K 上下文,长上下文正是它的卖点
arXiv:2404.140471–8 位 × 多种方法在 LLaMA-3 上的经典对照表提供了一个可比的量级参照
arXiv:2601.14277统一评测 llama.cpp 的 3–8 位,同时给困惑度、下游任务分、吞吐、体积最贴近本站读者的实际选择场景
读的时候要小心:这五项研究没有一项是在 Qwen3.8 上做的

它们提供的是方向,不是这个模型的实测结论。三条限制必须一起记:① 模型不同——架构、训练数据、后训练方式全不一样,尤其 Qwen3.8-27B 有 48 层 Gated DeltaNet,其循环状态在 config 里是 float32,量化敏感度和纯注意力模型未必相同;② 量化方案不同——各研究用的方法、位宽、分组大小各不相同,不能横向拼接;③ 任务不同——你的实际用途未必落在它们测的那几个基准上。
正确的用法是把它们当成「该往哪儿多测两下」的清单:如果你主要用中文、开长上下文、依赖长思维链推理,那么表里三条都撞上了,你就该在自己的实际任务上做一次 A/B,而不是相信任何一个通用结论——包括本站的。

另一头有一个可以定量的参照物,就是表 16-2 那一列困惑度。权重层面 10% 量级的相对误差(第15章实算),落在困惑度上只有 1.9% 的上升(Q4_K_S);6 位时更是只有 0.07%。这两个百分比之间隔着一层很厚的东西——那层东西就是 16.2 节讲的 GPTQ、AWQ,以及 16.3 节讲的混合位宽,它们全都在做同一件事:把误差从要紧的地方挪到不要紧的地方去。

表 16-2 给出 LLaMA-7B 的困惑度:F16 = 5.9066、Q4_K_S = 6.0215、Q2_K = 6.7764。(a) 分别算出两个量化档位相对 F16 的上升百分比。(b) 有人说「Q2_K 只涨了 0.87,才不到 1,可以忽略」,这句话错在哪?

先想:困惑度是一个绝对数还是相对数?比较两个模型的好坏,该看差值还是看比值?
关键在于困惑度的量级本身就取决于模型和数据集。同样涨 0.87,在困惑度 5.9 的模型上和在困惑度 60 的模型上完全不是一回事。
第一步这样走:6.0215 ÷ 5.9066 = ? 6.7764 ÷ 5.9066 = ? 把两个比值都算出来再减 1。
完整答案:(a) Q4_K_S:6.0215÷5.9066 = 1.0195,+1.9%;Q2_K:6.7764÷5.9066 = 1.1473,+14.7%。(b) 那句话有两处错。第一,用差值而不是比值:困惑度没有绝对的尺度,只有相对变化才可比。第二,也更关键——困惑度和「用起来好不好」不是线性关系。困惑度衡量的是模型对下一个词的平均不确定度,14.7% 的上升意味着模型在每一步都变得更犹豫,而生成一段几百词的回答要走几百步,这些犹豫会累积、会在需要精确推理的地方集中爆发。表 16-6 里那几项研究说的正是这件事:平均指标掉一点,某些具体任务可能掉很多。
补一个尺度感:Q6_K 只涨 0.07%,PR 原话是 within 0.1% or better from the original fp16 model;而 Q2_K 涨了 14.7%,是 Q6_K 的 200 倍。从 6 位降到 2 位,体积省了 60%,困惑度代价涨了两个数量级。

变式:这些困惑度是在 LLaMA-7B 上测的。如果换成一个 27B 的模型,同样的量化档位,困惑度上升幅度你猜会更大还是更小?(提示:想想更大的模型有没有更多的冗余。这个问题正是下一节 Dettmers 那篇论文要正面回答的。)

16.6 Dettmers 的那一击

把整个量化话题收束到一句话的,是 Dettmers 与 Zettlemoyer 的 k-bit Inference Scaling LawsarXiv:2212.09720)。它把「量化一个大模型」和「换一个小模型」放在同一个比特预算下正面对撞,原文是这么写的:

a 30B 8-bit model and a 60B 4-bit model have the same number of bits but may have very different zero-shot accuracies
——一个 30B 的 8 位模型和一个 60B 的 4 位模型占用同样多的比特,但零样本准确率可能差得很远。

把这句话拆开看,它同时说了两件事,方向相反:

第一,比特预算是可以互换的。把式 15-1 反过来读,就得到「等比特预算」这条线:

N1 × b1 = N2 × b2 = 8 × V预算
式 16-2
符号是什么直觉
N1, N2两个方案各自的参数个数由训练之前的决定钉死(第14章第 ① 轴)
b1, b2两个方案各自的位宽由训练之后的决定钉死(第 ④ 轴)
V预算你的显存预算(字节)这条式子上的每一个点,都恰好用满同一张卡
等号两个方案占同样多的比特但等号只保证体积相等,不保证能力相等——这正是 Dettmers 那句话的全部内容

30B × 8 位和 60B × 4 位代进去都是 30 GB。你确实面对一个真实的选择:同样一张卡,是塞一个更大但更糙的模型,还是塞一个更小但更精细的模型。

第二,互换之后的结果不是自动的。「可能差得很远」这五个字是重点——论文没有说哪一边一定赢,它说的是这需要测,不能推

回指第14章那张四轴表:这正是第 ① 轴(参数规模)和第 ④ 轴(数值精度)在显存预算上的交汇点。两条轴各自独立——一个决定于训练之前,一个决定于训练之后——但它们的后果会在同一张显卡上相遇。

不过对 Qwen3.8 的用户来说,这个选择在这个家族内部并不存在:官方一共只发了两个模型,27B 和 2.4T-A95B,中间没有任何尺寸。没有 4B、没有 8B、没有 32B(社区流传的小尺寸版本全是第三方产物)。所以在 24 GB 这个预算上,你没有「换个小一点的 Qwen3.8」这个选项,只有「把 27B 压到几位」这一条轴可以拨。

常见误解:把 Dettmers 这句话读成「4 位总是更划算」

那篇论文的标题是 The case for 4-bit precision,很多二手转述就停在标题上,得出「4 位是最优解」的结论。但论文自己的措辞是 may have very different——可能差很远。结论是「要测」,不是「4 位赢」。而且它的实验对象是 2022 年的稠密 Transformer,Qwen3.8-27B 有 48 层 Gated DeltaNet 和 16 层全注意力的混合结构,2.4T 那个还是 512 专家的 MoE,这些架构上的差别有没有影响,那篇论文一个字都没说,因为当时还不存在这样的模型。

你有一张 24 GiB 的卡。方案 A:Qwen3.8-27B 的 Q4_K_M(17.11 GB)。方案 B:假想有一个 Qwen3.8-13B(官方并不存在),用 Q8_0 大约 13.8 GB。请回答:
(a) 两个方案的权重占用差多少?
(b) 用 Dettmers 那句话,能不能断定哪个方案的能力更强?
(c) 除了「能力」,这两个方案还有哪三项可以精确算出来的差别?
(d) 这道题的题干里藏着一个第14章反复强调的陷阱,指出来。

先想:Dettmers 那句话的结论是「哪边赢」还是「要测」?另外,题干里那个 13B 是从哪儿来的?
关键在于 (c):能精确算的东西有三类——权重体积(式 15-1)、KV cache(式 5-1,取决于全注意力层数)、以及每个 token 的计算量(取决于激活参数)。这三样都不需要跑模型就能算。
第一步这样走:先把 (a) 算了:17.11 − 13.8 = ? 然后逐条问「这一项我能不能用公式算出来」,能算的归 (c),不能算的归 (b)。
完整答案:
(a) 17.11 − 13.8 = 3.31 GB,方案 A 多占。
(b) 不能。Dettmers 的原话是 may have very different zero-shot accuracies——他明确说的是「可能差很远」,也就是必须实测。任何断言哪边一定强的说法都超出了那篇论文能支撑的范围。
(c) 三项可精确算的差别:① 权重体积,见 (a);② KV cache——由式 5-1 决定,取决于全注意力层数、KV 头数、head_dim,和权重位宽完全无关。一个 13B 模型的层数更少,KV 也更小,这一项对长上下文影响巨大;③ 每 token 的计算量——由激活参数决定(第10章),27B 是稠密模型,全部 27.36 B 参数每个 token 都要过一遍。
(d) 陷阱是:Qwen3.8-13B 不存在。官方仓库只有 4 个(27B、27B-FP8、2.4T-A95B、2.4T-A95B-FP8),没有任何官方小尺寸。更要命的是,就算有人放出一个叫这名字的东西,它也不可能是把 27B 量化或裁剪出来的——那需要一次独立的预训练(第14章)。这道题真正的考点是:Dettmers 描述的那个选择,前提是两个尺寸都真的存在。在 Qwen3.8 上,这个前提不成立。

变式:把方案 B 换成一个真实存在的东西——同一个 27B 模型的 Q6_K(22.88 GB)。这时两个方案是同一套参数、不同位宽。哪些量变了,哪些量一个比特都没变?(提示:KV cache 那一项会不会变?)

自己推一遍:K-quant 的 4.5 bpw 是怎么被省出来的

  1. 先做最朴素的方案。256 个权重,切成 8 个小块每块 32 个,每个小块做非对称 4 位量化(要存缩放因子和最小值各一个 FP16)。算出这一超块占多少字节、bpw 是多少。

    想好了再看

    量化值:256 × 4 ÷ 8 = 128 字节。元数据:8 个小块 × (2 + 2) 字节 = 32 字节。合计 160 字节,bpw = 160 × 8 ÷ 256 = 5.0。用式 15-4 也能直接得到:4 + 32÷32 = 5.0。当初为什么从 32 起步?因为第15章表 15-2 说过,分组越小误差越低,而 32 是精度和开销的一个常见折中点。

  2. 现在盯住那 32 字节的元数据。它占了总量的 20%。想一想:这 16 个数(8 个缩放因子 + 8 个最小值)之间有什么关系?它们真的需要各自 16 位的全精度吗?

    想好了再看

    同一个超块里的 8 个小块,来自权重矩阵里连续的 256 个数,它们的幅度不会差太远——所以这 8 个缩放因子本身也是扎堆的。而「一堆扎堆的数」正是第15章整章在讲的东西:可以量化。当初能想到这一步,是因为把「元数据」和「数据」放在同一个眼光下看了——两者都只是一堆数字,凭什么其中一堆有豁免权?

  3. 把那 16 个数各压到 6 位,再用整个超块共享的两个 FP16 数把它们还原。重算字节数和 bpw。

    想好了再看

    量化值 128 字节不变。元数据:16 × 6 位 = 96 位 = 12 字节。超块级还原用的两个 FP16 = 4 字节。合计 128 + 12 + 4 = 144 字节,bpw = 144 × 8 ÷ 256 = 4.5——正是 PR #1684 给的那个数。用式 15-4 的推广形式写就是:4 + 12÷32 + 32÷256 = 4 + 0.375 + 0.125 = 4.5。三项分别是:量化值、第一级缩放因子、第二级缩放因子。

  4. 最后一问:省下的 0.5 bpw 是免费的吗?代价在哪儿?

    想好了再看

    不免费。缩放因子从 16 位压到 6 位,它自己也有量化误差了——而缩放因子的误差会到它管辖的全部 32 个权重上,是一种放大效应。K-quant 敢这么做,靠的是第二级那个 FP16:它把 8 个 6 位缩放因子的共同量级提出来精确保存,6 位只负责表示彼此之间的相对差异——而相对差异的动态范围小得多,6 位够用。
    这是整个 K-quant 设计里最漂亮的一步,而且它和 NVFP4 的「FP8 局部 scale + FP32 张量级 scale」是同一个结构。两个互不相干的团队,在四位量化这个问题上收敛到了同一个答案。

答辩:如果我是审稿人

你的表 16-3 说「再把嵌入层、输出头、视觉塔也提到 Q6_K」就能算到 16.91 GB,很接近实测的 17.11。可 llama.cpp 的文档只说了一半的 wvw2 用 Q6_K,没说嵌入层。你这一行是不是为了凑近实测值倒推出来的?

参考防守(先自己组织语言再看)

坦白说:是推断,不是读出来的,正文表 16-3 的表头已经写了「本站按……计算」。但这个推断是可证伪的,而且推断链条是先验的而不是倒推的:
① 只按 PR 原文规则算,得 16.13 GB,离实测差 0.98 GB。这个差不是噪声——GGUF 的容器开销是几十 MB 量级,差不了 1 GB。所以必然还有别的张量用了更高的位宽,这一步不需要任何猜测。
② 那么是哪些?嵌入层 + 输出头合计 2.54 B 参数,占全模型 9.3%,是除 FFN 外最大的一块,也是最常被单独提高位宽的对象(词表 248,320 极大,量化误差会直接反映在词的分辨上)。把它们提到 Q6_K 恰好补上 0.78 GB,剩 0.20 GB 给容器开销和本站没建模的部分。
但我没有直接证据。本站没有打开过那个 GGUF 文件的张量类型表。要证伪它非常容易:用 gguf 库读文件头,逐张量打印 ggml_type,如果 token_embd 显示的是 Q4_K 而不是 Q6_K,我这一行就错了——那时真正的解释就得去别处找(比如更多的 ffn_down 走了 Q6_K)。
本站给的是一个带区间的模型,而不是一个精确断言:15.39(纯 Q4_K)到 17.64(把全部 ffn_down 都提到 Q6_K)这个区间必然包含真值,实测的 17.11 确实落在里面。区间是硬的,区间里哪一行对,是软的。

答辩:如果我是审稿人

你的 K-quant 位宽全部来自 2023 年的一个 GitHub PR。llama.cpp 三年来改过无数次,这些数字凭什么还能用?

参考防守(先自己组织语言再看)

这是个真问题,本站的处理办法是不信任单一来源,做交叉验证
PR #1684 给的是格式定义(Q4_K 的超块结构),本站把它当成定义而不是当前实现。然后用一条完全独立的证据去验:表 16-4 那一列「实测文件反算的平均位宽」。这条证据的来源是 Unsloth 仓库的真实文件大小,和那个 PR 毫无关系。
验证结果是两个已知答案都对上了:BF16 反算 15.98(应为 16,差 0.1%);Q8_0 反算 8.4956(Q8_0 的理论位宽正是 8 + 16÷32 = 8.5,差 0.05%)。如果 PR 里的定义已经过时到不能用,Q8_0 这个最简单、最不容易改的格式没有理由还对得上。
剩下的风险要如实承认:本站没有核对 llama.cpp 当前代码里 block_q4_K 的定义,也没有确认 Unsloth 用的是哪个版本的 llama.cpp 打包的。这一条属于「本站未能核实」。要消除它,只需要跑一次 ggmlsizeof(block_q4_K),应该输出 144。

答辩:如果我是审稿人

你说 NVFP4 用 0.25 位的额外开销换回大约 0.5 位的精度,「这笔买卖看起来划算」。可你自己也承认没有公开数据支持前提。那你凭什么把它写进正文?

参考防守(先自己组织语言再看)

写进正文的是论证结构,不是结论。可以分成三层来看哪些站得住:
0.25 位的开销是硬的——它由式 15-4 直接给出,block 从 32 缩到 16,8÷16 − 8÷32 = 0.25,这是算术,不可争议。
「E8M0 最坏浪费半个量程」也是硬的——2 的幂之间差一倍,这是 E8M0 定义的直接后果,正文那道 3 级题就在推它。
「平均意义上换回约 0.5 位」是软的——它需要假设块内最大值在两个幂之间大致均匀分布。这个假设对随机数成立,对真实权重成不成立,本站不知道,NVIDIA 的博客也没有给这方面的数据。
所以正文的写法是「看起来划算,这就是 NVIDIA 的论证,但它成立的前提没有公开数据」——把论证和结论明确分开。这也是本站对所有厂商数据的统一处理方式:能自己复算的部分复算(那两个压缩比 3.5× 和 1.8× 就被复算对上了),不能复算的部分标明证据等级。

真未解混合线性注意力架构里,Gated DeltaNet 那 20.3% 的参数该用几位?

Dettmers 的 k-bit scaling law(arXiv:2212.09720)建立在 2022 年的稠密 Transformer 上,那时每一层都是同一种东西。Qwen3.8-27B 不是:它的 48 层 Gated DeltaNet 占了 20.3% 的参数,工作机制(固定大小的循环状态、增量更新规则)与注意力层完全不同,而且 config 里 mamba_ssm_dtype 明确写着 float32——连官方都认为它的状态需要比别处更高的精度
那么它的权重呢?GGUF 的 Q4_K_M 规则里只区分了 wvw2,那是在纯注意力的 LLaMA 上定下来的,它压根不知道 Gated DeltaNet 的存在——所以 DeltaNet 那 5.56 B 参数现在是按「其余张量」一刀切走 Q4_K 的。这合适吗?没有人知道。
这个问题属于真未解,判据是:Dettmers 那篇的实验对象里没有这类架构;Gated DeltaNet 的原论文(arXiv:2412.06464)通篇不讨论量化;llama.cpp 的分配规则早于这类架构存在。三份材料之间有一个空洞,没有人搭过桥。

先做这一步:不需要跑完整模型。用第15章造好的 quantize(),从 Qwen3.8-27B 的权重里各取一层的四张矩阵——DeltaNet 的 in_proj_qkvz、DeltaNet 的 out_proj、FFN 的 down_proj、注意力的 v_proj,对每一张分别做 INT4 分组 128 的量化,算相对误差。你要看的是四个数排出来的顺序:如果 DeltaNet 那两张的误差明显高于 FFN,那说明现行的一刀切分配确实亏待了它。再往前一步:把量化前后的矩阵各喂同一组随机输入,比较输出的相对误差——权重误差和输出误差的比例,才是「该给它几位」的真正判据。这一步的全部输入都是公开的:Apache 2.0 的权重、safetensors 读取接口、本站的公式。做出来的四个数,公开材料里查不到。

这一层要加什么:给量化器加混合位宽策略

为什么现在才加它:第 15 层的 quantize() 对整个模型一视同仁——一个位宽通吃。但这一章证明了,真实的量化方案没有一个是这么干的:ffn_down 用 6 位、其余用 4 位、嵌入层可能还要更高。这一层要做的就是把「位宽」从一个全局参数,变成一份逐张量的分配表,然后用它把实测的 17.11 GB 夹住。

BPW = {"Q2_K":2.5625, "Q3_K":3.4375, "Q4_K":4.5, "Q5_K":5.5, "Q6_K":6.5625, "Q8_0":8.5} GROUPS = [ # 参数量单位 B,来自第12章的点钞结果 ("token_embd", 1.2714), ("output", 1.2714), ("attn_qko", 1.5938), ("attn_v", 0.0839), ("deltanet", 5.5620), ("ffn_gate_up",11.4085),("ffn_down",5.7043), ("vision", 0.4603), ] def assign(recipe): # recipe: 组名 -> 档位名,缺省走 base return {g: recipe.get(g, recipe["_base"]) for g, _ in GROUPS} def avg_bpw(a): tot = sum(n for _, n in GROUPS) return sum(n * BPW[a[g]] for g, n in GROUPS) / tot def size_gb(a): return sum(n * BPW[a[g]] for g, n in GROUPS) * 1e9 / 8 / 1e9 # Q4_K_M:PR 原文规则 —— wv 和 w2 各一半走 Q6_K # 「一半」用参数量折半来近似:把该组拆成两半分别赋档位

难点一:「一半的张量」不等于「一半的参数」。PR 说的是一半的 wvw2 张量走 Q6_K,而 llama.cpp 实际是按层号挑的(靠前和靠后的层优先)。由于 Qwen3.8-27B 的 64 层 FFN 每层大小完全相同,按张量数折半和按参数量折半在这里恰好等价——但换一个各层不等宽的模型就不等价了。这是一个「在这个模型上碰巧成立」的简化,要在代码注释里写清楚,否则下次换模型会静默出错。

难点二:DeltaNet 那 5.56 B 参数没有归宿。PR 的规则里只有 wvw2 两个特例,因为它是在纯注意力的 LLaMA 上写的,那时没有 Gated DeltaNet 这种东西。你的代码必须替它做一个决定——本站选择让它走 base 档位(Q4_K),但这是本站的选择,不是 llama.cpp 的规则。凡是材料没说而你替它做了决定的地方,都要在代码里留下痕迹。

难点三:别把 bpw 当成整数。6.5625 那个 .5625 看着像噪声,但它在 27.36 B 上值 0.2 GB。如果你图省事写成 6,canonical 那一行会从 16.13 GB 掉到 15.93 GB——一个足以让你误判分配方案的偏差。

自己验:三个数把实测的 17.11 GB 夹在中间。
按 Q4_K_M 规则(base = Q4_K,attn_vffn_down 各一半走 Q6_K)跑 avg_bpw,应得 4.70–4.75(本站算得 4.718),size_gb 应得 16.1–16.2 GB(本站 16.13)。再把 token_embdoutputvision 也改成 Q6_K,应得 4.90–5.0016.8–17.0 GB(本站 4.945 / 16.91)。两次结果都必须落在 4.5–5.0 bpw 与 15.4–17.1 GB 这个窗口内,且第二次要比第一次更接近 17.11。
把全部组都设成 4 位(把 BPW 里的档位换成一个自定义的 4.0),size_gb正好13.68 GB——它必须等于 27.36 × 4 ÷ 8,一分不差。
全部设成 6 位,应正好20.52 GB(= 27.36 × 6 ÷ 8)。全部设成 Q4_K(4.5),应正好15.39 GB
三个数都对上,说明你的分配模型是合理的:13.68 < 15.39 < 16.13 < 16.91 < 17.11(实测) < 20.52,实测值被稳稳夹在中间,而且随着你把越来越多的张量提到 6 位,算出来的数单调逼近它。
若 ② 算出来不是 13.68 而是 13.45,说明你的 GROUPS 漏了 vision 那 0.4603 B(8 个组的参数量之和必须是 27.356,先验这个总和再往下走)。若 ① 算出 4.718 但 ② 对不上,说明你的 size_gb 里单位换算写错了——检查是不是多除或少除了一次 109

本章小结

  • 先分层面再比方法:LLM.int8() 治离群值、GPTQ 治舍入、AWQ 治「谁更重要」、SmoothQuant 把激活的难度搬到权重上、NF4 治格子摆位、K-quant 治位宽分配;而 FP8 / NVFP4 / MXFP4 不是算法,是硬件规范。把它们排成一列比优劣是常见错误。
  • W4A16 只省显存不省算力,W8A8 两样都省但激活难量化。文件大小只由 W 决定,因为激活值根本不存在文件里。
  • 那 0.875 位的去向:Q4_K 的 4.5 bpw 来自 144 字节的超块结构(128 量化值 + 12 一级缩放 + 4 二级缩放);_M 是 Medium,指一半的 wvw2 走 Q6_K;再加上嵌入层与输出头的高位宽,本站算到 16.91 GB,实测 17.11 GB。
  • K-quant 最漂亮的一步是把缩放因子本身也量化了:8 个 FP16 缩放因子(32 字节)压成 8 个 6 位值加两个超块级 FP16(16 字节),省下 0.5 bpw,也就是 10% 的文件。NVFP4 的两级缩放是同一个结构。
  • 反算位宽这把尺子是准的:BF16 反算 15.98(应 16)、Q8_0 反算 8.50(理论 8.5),两个已知答案都对上;用同一把尺子量 Q4_K_M 得 5.00。
  • NVFP4 与 MXFP4 差在两处:block 16 对 32、缩放因子 E4M3 对 E8M0。E8M0 只能表示 2 的幂,最坏浪费半个量程;NVFP4 多付 0.25 位买回约 0.5 位。NVIDIA 给的 3.5× 与 1.8× 压缩比可以用式 15-4 直接复现。
  • 量化掉多少能力,Qwen3.8 上一份实测都没有。已知方向(均在别的模型上):长思维链掉得更明显、非英语掉得比英语明显、长上下文的表现与短任务不同。而 llama.cpp 的困惑度对照给了一个量级参照:Q6_K +0.07%、Q4_K_S +1.9%、Q2_K +14.7%。
  • Dettmers 的那一击:同样的比特预算下,30B-8bit 和 60B-4bit 的能力可能差很远——结论是「要测」,不是「4 位赢」。而在 Qwen3.8 家族里这个选择根本不存在,因为官方没有中间尺寸。

两章的量化算完了,账本齐了。下一章把它落到地面上:一张 24 GB 的显卡,插上电,到底能跑到什么程度——权重多少、视觉塔多少、KV 多少、框架吃掉多少,最后剩下的上下文长度是几万。那一章的每个数字你现在都已经有能力自己复算。

第17章 上手:24GB 显卡的真实边界

这是全站唯一一章,你今晚就能照着做。前面十六章造的所有零件——参数量、KV cache 公式、DeltaNet 的固定态、视觉塔那 0.93 GB、量化的实际位宽——会在这一章合成同一张预算表,最后落成一个数:一张 24GB 的卡,跑 Qwen3.8-27B,上下文实际能开到多长。

学完这一章你应该能做到

  • 说清 24GB 到底是 24 GiB 还是 24 GB,以及混用会让你的结论差多少
  • 把一张显卡的显存逐项拆成:权重 + 视觉塔 + DeltaNet 固定态 + 框架开销 + KV,五项都能自己算
  • 算出 24 GiB 卡上 Q4_K_M 的真实上下文上限,并说清为什么它是一个区间而不是一个数
  • 看懂 Unsloth、vLLM、Ollama、LM Studio 四家给的数字为什么不一样,各自在数什么
  • 对 8GB / 16GB / 24GB / 服务器四种硬件,各说出一条能走通的路和它的代价
前置:第5章的 KV cache 公式(每 token 64 KiB / fp8 32 KiB)与「网上普遍高估 4 倍」那件事,第7章的 DeltaNet 固定态,第11章的视觉塔与 mmproj,第15、16章的实际位宽与量化档位体积。

17.1 先把单位说清

补充:24GB 的显卡其实是 24 GiB,比 24 GB 大 7.4%

显卡厂商标的 24GB(RTX 4090、3090 那一档)指的是 24 GiB——二进制的 24 × 10243 字节。换算成十进制是 25.77 GB
而 Hugging Face、Unsloth、Ollama 页面上显示的模型文件大小是十进制 GB(109 字节)。
两者差 7.37%。在 24 这个量级上就是 1.77 GB——足够多装 2.7 万个 token 的 fp16 KV cache。混用会让你的结论错得很难查。
本站的约定(全站统一):权重与模型文件用十进制 GB;显存预算、KV cache、显卡容量用 GiB。需要放在一起加减时,先把 GB 换成 GiB(除以 1.0737)。

这不是学究气。上一轮本站自己就栽在这上面:早先的推导把 24 GiB 的卡当成 24 GB 来算,凭空少了 1.77 GiB,结论从「约 8–10 万 token」变成了「约 7.8 万」——差了近三成的余量,而这个余量正好是读者最关心的那部分。

一个随手可用的换算:GB 除以 1.0737 得 GiB。17.11 GB = 15.93 GiB;0.93 GB = 0.87 GiB;54.66 GB = 50.91 GiB

某篇教程写道:「Q4_K_M 是 17.11 GB,24GB 显卡还剩 6.89 GB,够开 11 万 token 的 fp16 上下文。」这段话里有两处单位错误,各指出来并算出正确的数。

先想:那个 24 是 GB 还是 GiB?那个 17.11 呢?两个数能直接相减吗?
关键在于第二处更隐蔽:KV cache 的「每 token 64 KiB」是二进制单位,用它去除一个十进制的 GB 数,会得到一个偏大的 token 数。
第一步这样走:先把两个数统一到 GiB。24 GiB 就是 24 GiB;17.11 GB ÷ 1.0737 = 15.93 GiB。相减得多少?
完整答案:错误一:24 是 GiB,17.11 是 GB,直接相减是拿两把不同的尺子做减法。统一到 GiB:24 − 15.93 = 8.07 GiB(不是 6.89)。错误二:算 token 数时要用 GiB。8.07 GiB × 1024 × 1024 ÷ 64 KiB = 约 13.2 万 token
有意思的是这两个错误方向相反:第一个让结果偏小,第二个让结果偏大,最后凑出的 11 万碰巧落在正确答案附近。这是单位混用最危险的形态——错误互相掩盖,数字看着合理。
但即使改对了这两处,13.2 万仍然是错的,因为它还漏了三项:视觉塔 0.87 GiB、DeltaNet 固定态 0.14 GiB、框架与激活开销 1–2 GiB。17.3 节会把五项一次算全,正确答案是 约 8.3–9.9 万

变式:一块标称 16GB 的显卡,实际是多少 GB(十进制)?它比 24GB 卡少了多少 GiB?(答案:17.18 GB;少 8 GiB。)

17.2 全部量化档位的真实体积

下面这张表不是估算,是 Unsloth 仓库里逐个文件的真实大小(该仓库是全网下载量最高的 Qwen3.8-27B GGUF 来源)。第16章反算的实际平均位宽也一并列出,方便你对照第15、16章的公式。

表 17-1:Qwen3.8-27B 各量化档位的实测体积(Unsloth GGUF 逐文件真实大小;GiB 与位宽两列为本站换算)
档位体积(GB)换算(GiB)实际平均位宽能否装进 24 GiB 卡(仅权重)
BF1654.6650.9115.98不能
Q8_029.0527.058.50不能
UD-Q6_K_XL25.9224.147.58不能(差 0.14 GiB)
Q6_K22.8821.316.69装得下,但几乎没有 KV 空间
Q5_K_M19.8318.475.80可以,上下文很紧
UD-Q4_K_XL17.9216.695.24可以
Q4_K_M17.1115.935.00可以,本章的主角
IQ4_XS15.7114.634.59可以
Q3_K_M13.8212.874.04可以
UD-Q2_K_XL10.689.953.12可以
UD-IQ2_XXS9.018.392.63可以
mmproj 视觉塔+0.93+0.87约 16(基本未量化)必须额外加载

最后一行是最容易被漏掉的一项。Qwen3.8-27B 是原生多模态模型(第11章),它的视觉塔在 GGUF 生态里被单独打包成一个叫 mmproj 的文件。它不在上面任何一个档位的体积里,要另外加 0.93 GB。不加载它,模型仍然能跑,但看不懂图片——你手上就只剩一个纯文本模型了。

另外三个非 GGUF 的数字,第16、17章都会用到:NVFP4 是 21.92–23.42 GB(不同发布方的打包方式不同),MLX-4bit 是 16.05 GB(苹果生态),官方 FP8 版按第15章估算约 27.8 GB。

一位读者说:「我这张卡 24GB,Q6_K 才 22.88 GB,装得下,我就用 Q6_K 了。」请指出他忽略了什么,并算出他实际会遇到什么情况(提示:他要用这个模型看图片)。

先想:22.88 和 24 这两个数,单位一样吗?另外,表 17-1 最后一行说了什么?
关键有两处:① 22.88 GB 要先换成 GiB 才能和 24 GiB 比;② 要看图就必须再加 mmproj。
第一步这样走:22.88 ÷ 1.0737 = 21.31 GiB。再加 mmproj 的 0.87 GiB。剩下多少?
完整答案:权重 21.31 GiB + mmproj 0.87 GiB = 22.18 GiB,24 GiB 卡还剩 1.82 GiB。再扣掉 DeltaNet 固定态 0.14 GiB 和框架开销 1–2 GiB,留给 KV 的是 −0.32 到 0.68 GiB——最好的情况下只够约 1.1 万 token 的 fp16 上下文,最坏的情况直接装不下、报显存不足。
他会遇到的现象很具体:模型能加载(或者刚好加载不了),但输入稍长一点就崩,或者被框架自动降级到极短的上下文窗口。「装得下」和「能用」之间隔着 KV cache 这一整笔账,而 KV 的大小和权重位宽毫无关系(第15章那道 4 级题的考点)。他真正该选的是 Q4_K_M,多出来的 5 GiB 全部变成上下文长度。

变式:如果他只用文本、完全不看图,能不能省下 mmproj 那 0.87 GiB?这样 Q6_K 的上下文能到多少?(可以省,但省下的量换算成 fp16 上下文只有约 1.4 万 token——仍然改变不了结论。)

17.3 完整预算推导

先问:为什么不能直接「显存减权重」

因为一张卡上同时住着五样东西,权重只是最大的那一样。漏掉任何一项,你的上下文估计都会偏高——而偏高的后果是跑到一半突然显存不足,前面的输出全丢。

五项逐个来。前四项是固定开销,和上下文长度无关;第五项 KV cache 与长度严格成正比(第5章式 5-2)。写成一条式子就是:

Tmax = CW权重MmmprojSdeltaOktoken
式 17-1
符号是什么它从哪来,硬不硬
Tmax能开到的最大上下文长度(token 数)这是我们要求的量。因为分子里有一个区间,它也必然是区间
C显卡容量,单位 GiB硬。厂商标称,24GB 就是 24 GiB
W权重量化后的权重体积,换算成 GiB硬。表 17-1 的实测文件大小 ÷ 1.0737
Mmmproj视觉塔,0.87 GiB硬。实测文件。只做文本时这一项为 0
SdeltaDeltaNet 循环状态,0.14 GiB本站按论文定义推导(48 层 × 3 MiB),官方与框架均未公布与上下文长度无关
O框架与激活开销,1.0–2.0 GiB软。本站经验区间,非实测——整式的不确定性全在这一项
ktoken每 token 的 KV 单价:fp16 是 64 KiB,fp8 是 32 KiB硬。第5章由 config 复算:2 × 16 × 4 × 256 个元素
表 17-2:24 GiB 单卡跑 Qwen3.8-27B Q4_K_M 的显存预算(本站推导,非官方数字)
数值它从哪来
显卡容量24 GiB厂商标称(= 25.77 GB 十进制)
Q4_K_M 权重17.11 GB = 15.93 GiB表 17-1,Unsloth 实测文件
mmproj 视觉塔0.93 GB = 0.87 GiB表 17-1,必须额外加载(第11章)
DeltaNet 固定态0.14 GiB48 层 × 3 MiB = 144 MiB。不随序列长度变(第7章)
框架与激活开销1.0–2.0 GiB本站取的经验区间,非实测
剩给 KV cache5.1–6.1 GiB24 − 15.93 − 0.87 − 0.14 − (1.0~2.0)
→ fp16 KV(64 KiB/token)约 83K – 99K token第5章:每 token 2×16×4×256 个元素
→ fp8 KV(32 KiB/token)约 166K – 199K token位宽减半,token 数翻倍

中间那两项最容易被忽略,值得各说一句。

DeltaNet 固定态那 0.14 GiB,是这整本手册里最反直觉的一个数。48 层线性注意力,每层持有一个循环状态矩阵,全模型合计 144 MiB——而且 1 个 token 和 100 万个 token 一样大。对照着看:262K 上下文时,16 层全注意力的 KV cache 是 16 GiB,是它的 114 倍。整个模型 3/4 的层,在长上下文这本账上几乎不花钱。

框架与激活开销那 1.0–2.0 GiB,是这张表里唯一一个没有硬来源的数。它包含推理框架自己的运行时、CUDA 上下文、中间激活值的临时缓冲、以及分页分配器的碎片。本站给的是一个经验区间,这也是为什么最终结论必须写成区间而不是一个数

一句话记住:24GB 单卡跑 Q4_K_M 的 Qwen3.8-27B,上下文实际到约 8–10 万 token(fp16 KV);开 fp8 KV cache 大约翻倍到 17–20 万原生 262K 在 24GB 上跑不满。
读的时候要小心:这是本站推导,不是官方数字,而且真实值更靠近下沿

三条限定缺一不可。① 这是本站按公开数据做的推导,官方没有发布过任何一张 24GB 卡的上下文上限表。② 框架与激活开销那 1.0–2.0 GiB 是经验区间,不是实测——换一个框架、换一个批大小,这个数就会变。③ 实际部署还有分页与碎片开销:vLLM 的 PagedAttention、llama.cpp 的 slot 分配都按固定大小的块预留显存,最后一块通常填不满,还有页表本身的开销。所以真实可用值会更靠近区间的下沿,也就是 8 万那一端而不是 10 万那一端。
正确的用法:把它当成规划的起点,然后在你自己的机器上实测一次——把上下文从 32K 起步往上加,直到框架报显存不足,那个数才是你的真值。

这个规划器把表 17-2 变成可拨的:填进你的显存容量、选一个量化档位、拉上下文滑块、切换 fp16 / fp8 KV,它逐项显示五笔开销的堆叠,超出容量时直接标红。它同时保留了第5章那个对照——按 64 层全算的错误结果会并排显示,让你看清网上那些计算器把 KV 高估了 4 倍之后,结论会离谱到什么程度。

同一张 24 GiB 卡,改用 IQ4_XS(15.71 GB),并且只做文本、不加载 mmproj,KV 用 fp8。(a) 重算这套预算,给出上下文区间。(b) 五项开销里,哪几项因为这两个改动而变了,哪几项一个字节没变?(c) 这个配置能不能开满原生 262K?

先想:换量化档位改的是哪一项?不加载 mmproj 又改的是哪一项?DeltaNet 那 0.14 GiB 会不会跟着变?
关键在于 DeltaNet 固定态和框架开销这两项与量化档位、与是否多模态都无关——前者由层数和状态维度决定(第7章),后者由框架决定。
第一步这样走:15.71 ÷ 1.0737 = 14.63 GiB。然后 24 − 14.63 − 0(无 mmproj)− 0.14 − (1.0~2.0) = ? 再除以 fp8 的 32 KiB/token。
完整答案:(a) 权重 14.63 GiB,无 mmproj,DeltaNet 0.14,框架 1.0–2.0 → KV 可用 7.23–8.23 GiB。fp8 下:7.23 × 1024 × 1024 ÷ 32 = 约 237K;8.23 × 1024 × 1024 ÷ 32 = 约 269K。区间是 约 23.7 万 – 26.9 万 token
(b) 变了的:权重(15.93 → 14.63 GiB)、mmproj(0.87 → 0)、KV 单价(64 → 32 KiB/token)。没变的:DeltaNet 固定态 0.14 GiB,框架开销 1.0–2.0 GiB。前者由 48 层 × 3 MiB 决定,和量化位宽、和多模态都无关;后者由框架决定。
(c) 处在边界上,不保险。262K 需要 262144 × 32 KiB = 8.00 GiB。区间上沿 8.23 GiB 刚好够,下沿 7.23 GiB 不够。而正文的 caution 说过真实值更靠近下沿,再加上分页碎片,所以现实中大概率跑不满 262K,除非你把框架开销压到 1 GiB 以下。
这道题真正的考点是 (b):五项里只有两项会随你的选择变,另外两项是这个模型的结构常数。知道哪些数不会变,比记住哪些数是多少更有用。

变式:如果他既要 262K 又要看图(必须加载 mmproj),在 24 GiB 上还有出路吗?(提示:算一算 UD-Q2_K_XL 的 9.95 GiB + 0.87 + 0.14 + 8.00 是多少,再想想第16章说的 Q2_K 困惑度涨了 14.7%。)

17.4 官方与权威来源怎么说

本站的推导只是一家之言。下面是四个可以交叉验证的第三方数字,每一个都能公开查到。

表 17-3:四家工具链公布的显存数字(各自原文口径)
来源它给的数字它在数什么
Unsloth 文档2bit 11–13GB / 3bit 13–16 / 4bit 17–19 / 6bit 24 / 8bit 31 / BF16 56;原文点名 RTX 5080、4090、24GB Mac各档位的推荐显存,已经含了一点余量,不是纯文件大小
vLLM recipe(27B)原文 NVFP4 in 24.6 GiB;单卡 1M 上下文下 6.6M KV token服务时的总显存占用,含框架开销
Ollamaqwen3.8:27b18GB(模型 17GB + projector 931MB)下载体积,把 mmproj 一起算进去了
LM Studio最低系统内存 17GB系统内存门槛,不是显存

把 Unsloth 那一列和本站表 17-2 对一下:4bit 档位 17–19 GB,本站的权重 + mmproj 是 17.11 + 0.93 = 18.04 GB,正好落在中间。Ollama 的 18GB 也是同一个数——它把 mmproj 一起算了。三个独立来源对上了同一个数,这一项可以放心。

常见误解:vLLM 说 NVFP4 只要 24.6 GiB,那 4090 岂不是刚好能跑

24.6 GiB 已经超过 24 GiB 了。这个数不是「刚好够」,是「差 0.6 GiB」。
更要紧的是它说的不是这一档卡。NVFP4 是 Blackwell 一代硬件才原生支持的格式,vLLM recipe 面向的是 32GB 起步的服务器卡;RTX 4090 属于上一代,即使把文件塞进去也没有对应的原生计算指令。这份 recipe 里同一页还写着 1M 上下文下 6.6M KV token 的规模——那更不是一张消费卡的场景。
这是一处极容易被误读的地方,而且误读的方向是让人以为消费卡能跑。判断方法很简单:看到任何一个显存数字,先问它是在哪张卡上、算的是文件还是运行时占用。表 17-3 四行的最后一列就是在回答这个问题——四家数的根本不是同一样东西。

表 17-3 里,LM Studio 说「最低系统内存 17GB」,Ollama 说「共 18GB」,本站说「权重 15.93 GiB」。三个数看起来都在说同一件事,其实各不相同。请分别指出:这三个数各自包含什么、不包含什么。

先想:「系统内存」和「显存」是同一样东西吗?「下载体积」和「运行时占用」呢?
关键在于三个词:系统内存(主板上的内存条)、下载体积(硬盘上的文件)、显存(显卡上的)。它们是三种不同的资源。
第一步这样走:先判断每个数的单位是 GB 还是 GiB,再判断它数的是硬盘、内存还是显存。
完整答案:
LM Studio 的 17GB系统内存门槛。它面向的场景包含 CPU 推理和部分卸载,所以数的是内存条而不是显存。不含 mmproj 明细,也不含你要开多长上下文。
Ollama 的 18GB下载体积,明确拆成 17GB 模型 + 931MB projector。它 mmproj,不含任何 KV cache 和框架开销——照着它准备 18GB 显存是不够跑的。
本站的 15.93 GiB:只是权重那一项,单位是 GiB。它不含 mmproj(另算 0.87)、不含 KV、不含框架开销——它是表 17-2 五项里的第一项,单独拿出来毫无用处。
三个数没有一个能直接回答「我这张卡能不能跑」,因为那个问题的答案取决于第五项 KV cache,而 KV 取决于你要开多长的上下文——四家里没有一家在数这个。这就是表 17-2 存在的理由。

变式:Unsloth 给 6bit 档位标 24GB。按本站表 17-1,Q6_K 文件是 22.88 GB = 21.31 GiB。这两个数差 1.1 GB 左右,这个差是什么?(提示:Unsloth 标的是推荐显存,不是文件大小。)

17.5 工具链:四条路

四条路各自面向不同的硬件和场景。本节不做优劣排名——它们服务的对象根本不同,比较没有意义。

表 17-4:四条工具链路线(入口链接见素材,均为官方文档)
路线格式一句话适用场景要注意的
llama.cpp / Ollama / LM StudioGGUF消费级显卡与 Mac 的首选。一条命令拉起来,支持 CPU + GPU 混合,对显存不足最宽容要单独加载 mmproj 才能看图;KV 精度用 --cache-type-k/-v
vLLM / SGLangFP8 / NVFP4(也支持 BF16)服务器与多卡。为高并发设计,PagedAttention 之类的机制让多个请求共享显存官方模型卡里唯一的 GitHub 链接就是这两家的支持 PR;NVFP4 需要 Blackwell 一代硬件
MLXMLX 专用(4bit 实测 16.05 GB)Apple 芯片的统一内存。内存和显存是同一块,容量上限由整机内存决定统一内存意味着模型和系统抢同一块地,实际可用量比标称小
UnslothGGUF / 微调既想跑又想微调。它同时提供量化权重和微调工具链表 17-1 那些实测体积就出自它的仓库

选路的判据其实只有两条:你的硬件是什么(消费卡 / 服务器卡 / Apple 芯片),以及你是一个人用还是要扛并发。一个人在自己电脑上跑,GGUF 那条路的门槛最低;要给一群人提供服务,vLLM / SGLang 那套为并发做的工作才用得上。

三个人各要跑 Qwen3.8-27B:甲有一台 RTX 4090 的台式机,自己用;乙要给公司三十号人提供内部服务,手上是服务器卡;丙用的是一台 64GB 内存的 Mac。分别给出一条能走通的路,并各说一句它的限制。

先想:表 17-4 那四行,第三列写的就是各自的适用场景。先按硬件对号入座。
关键在于「自己用」和「扛并发」是两种完全不同的需求——前者要的是能跑起来,后者要的是同时服务多个请求时显存不浪费。
第一步这样走:甲是消费卡 → 哪一行?乙是服务器 + 并发 → 哪一行?丙是 Apple 芯片 → 哪一行?
完整答案::走 GGUF(llama.cpp / Ollama / LM Studio),选 Q4_K_M,别忘了 mmproj。限制是上下文只能到约 8–10 万(表 17-2),要更长就得开 fp8 KV。:走 vLLM 或 SGLang,用 FP8 或 NVFP4。限制是 NVFP4 需要 Blackwell 一代硬件,而且 vLLM recipe 那个 24.6 GiB 说的不是消费卡。:走 MLX,4bit 版本 16.05 GB。限制是统一内存要和系统抢地——64GB 里能给模型的没有 64GB,而且加上 KV 之后可用上下文同样有限。
三条路没有优劣,只有匹配。如果甲非要上 vLLM 也能跑(它支持 BF16 和多种量化),只是为并发做的那些工作在一个人用的场景里派不上用场;如果乙用 llama.cpp 也能服务,只是并发上去之后显存利用率不如专门为此设计的方案。

变式:甲后来想在自己的数据上微调这个模型,四条路里哪一条同时覆盖「跑」和「调」?(表 17-4 最后一行。)

17.6 对照:2.4T 的另一个世界

把同一套算术搬到 Qwen3.8-2.4T-A95B 上,会得到一组量级完全不同的数字。这不是「大一点」,是差三个数量级

表 17-5:2.4T-A95B 的硬件门槛(vLLM recipe 数字;本站复算列见备注)
精度权重占用需要几张 B300对照 27B
BF164.45 TiB(vLLM recipe)24 张27B 的 BF16 是 50.91 GiB,差 89 倍
FP82.27 TiB16 张——
NVFP4 W4A41.32 TiB8 张——
GGUF 最低档(UD-Q1_0)397 GB,需 450GB 内存——27B 最低档是 9.01 GB,差 44 倍

一句话对照:27B 的 Q4_K_M 是 17.11 GB,一张两千块钱的二手显卡就能装;2.4T 就算压到 GGUF 的最低档也要 397 GB,需要 450GB 内存的机器——而那已经是能找到的最激进的量化了。

这正是第10章那个结论在硬件上的样子:显存看总参数,速度看激活参数。2.4T 的 95B 激活参数意味着它每个 token 的计算量只相当于一个 95B 的稠密模型——跑起来不慢;但那 2.42 万亿个参数一个都不能少地待在显存里,因为你事先不知道下一个 token 会路由到哪 10 个专家。2.4T 决定你要买几张卡,95B 决定你等多久出第一个 token。

顺带提醒一条第3章之外的纪律:27B 是 Apache 2.0,可以直接说「开源」;2.4T-A95B 用的是 qwen3.8-max 自定义协议,本站一律称「开放权重」。协议里有两条限制:经营 MaaS 或 AI 助手业务且连续 12 个月收入超 5000 万美元的,商用前须另行授权;用于月活超 1 亿的产品时,须在界面显著位置展示模型名称。这一条和你今晚在自己电脑上跑 27B 没关系,但如果你打算拿 2.4T 做产品,它比显存更早成为门槛。

按 vLLM recipe,2.4T-A95B 的 NVFP4 权重是 1.32 TiB(= 1352 GiB),用 8 张 B300 装得下。本题设定这 8 张卡合计约 2300 GiB 可用显存(各代卡的具体容量本站未核实,这里只作为题目条件)。请回答:
(a) 权重之外还剩多少显存?
(b) 2.4T 的 KV cache 每 token 多大(fp16)?这些余量够开多长上下文?
(c) 同样的余量,如果换成 27B 那个模型,能开多长?
(d) 由 (b)(c) 的对比,说出一条关于「模型规模和上下文长度」的结论。

先想:2.4T 的 KV cache 公式和 27B 一样吗?式 5-1 里四个因子,哪几个变了?
关键在于 2.4T 有 23 层全注意力(92 层里的 1/4),KV 头数和 head_dim 与 27B 相同(4 和 256)。所以每 token = 2 × 23 × 4 × 256 = 47,104 个元素,fp16 下 92 KiB
第一步这样走:(a) 2300 − 1352 = ? 留一点给框架,剩下的除以 92 KiB。(c) 同样的余量除以 64 KiB。
完整答案:
(a) 2300 − 1352 = 948 GiB。八张卡的框架开销按每张 2 GiB 算掉 16 GiB,还剩约 932 GiB
(b) 每 token 92 KiB。932 GiB × 1024 × 1024 ÷ 92 = 约 1062 万 token。远超它 262K 的原生上下文,也超过 1.01M 的扩展上限——在这种规模的机器上,KV cache 根本不是瓶颈,权重才是。
(c) 27B 每 token 64 KiB,同样 932 GiB 能开 约 1527 万 token——同样远超上限。
(d) 结论有两层。第一层:KV cache 是不是瓶颈,取决于「卡的总容量减去权重」还剩多少,而不是模型有多大。27B 在 24 GiB 卡上被 KV 卡死(8–10 万),2.4T 在 8 张 B300 上却富余到用不完——因为后者的余量绝对值大了三个数量级。第二层,也更重要:两个模型的 KV 单价只差 1.44 倍(92 vs 64 KiB),而权重差了 89 倍。原因是 KV 只跟全注意力层数有关(16 vs 23),而 2.4T 多出来的参数几乎全在 MoE 的专家里,那些参数一个字节 KV 都不产生
这道题真正的考点是:权重账和 KV 账是两条独立缩放的曲线,而且它们的斜率差得很远。把模型放大 89 倍,KV 只涨 1.44 倍。

变式:如果按网上那种「所有层都有 KV」的错误算法,2.4T 的每 token KV 会被算成多少?(92 层 → 368 KiB,高估 4 倍,和 27B 那个错误完全同源。)

17.7 如果你只有 8GB 或 16GB

这一节给的是诚实答案,不是安慰话。

8 GiB:跑不了 27B。表 17-1 里最小的档位 UD-IQ2_XXS 是 9.01 GB = 8.39 GiB,光权重就已经超了,还没算 mmproj、DeltaNet 固定态、框架开销和 KV。

唯一的出路是 CPU + GPU 混合卸载:把装不下的那部分层放在系统内存里,由 CPU 算。llama.cpp 那条路支持这么做,代价是慢很多。Unsloth 文档对这件事的原话是:RAM+VRAM ≈ the quant size; otherwise it'll still work, just much slower due to disk offloading——内存加显存的总和大致要够上量化后的体积;不够的话它照样能跑,只是因为要从硬盘上换页而慢得多

这句话里的判据很实用:看的是「内存 + 显存」的总和,不是显存单项。一台 8GB 显存 + 32GB 内存的机器,总和 40GB,跑 Q4_K_M(18.04 GB 含 mmproj)在「总和够」这一条上是过关的——它会跑起来,但速度取决于有多少层被赶到了 CPU 上。

16 GiB:能跑,但上下文很紧。把表 17-2 那套算术照搬过来(显卡换成 16 GiB):

表 17-6:16 GiB 卡的预算(本站推导,方法同表 17-2,含 mmproj)
档位权重剩给 KVfp16 上下文fp8 上下文
Q4_K_M(17.11 GB)15.93 GiB装不下(加上 mmproj 已经 16.80 GiB)
Q3_K_M(13.82 GB)12.87 GiB0.1–1.1 GiB约 2K – 18K约 4K – 37K
UD-Q2_K_XL(10.68 GB)9.95 GiB3.0–4.0 GiB约 50K – 66K约 100K – 132K

看第二行和第三行的取舍:Q3_K_M 保住了更高的位宽(实际 4.04 位),但上下文被压到几千到一万多,很多实际任务不够用;UD-Q2_K_XL 位宽掉到 2.63,换回五万到六万多的上下文。这是一个必须自己做的权衡,没有普适答案——而且别忘了第16章表 16-2 那个参照:Q2_K 档位在 LLaMA-7B 上的困惑度涨了 14.7%,是 Q6_K 的 200 倍。

一位读者有 16 GiB 显存 + 32 GB 系统内存,他的任务是「读一份 5 万字的中文文档然后回答问题」,不需要看图。请给出你的建议,并说明你的每一步判断依据。(提示:5 万汉字大约对应多少 token?中文的分词比例通常在 1 到 1.5 字/token 之间,取 1.2 估算。)

先想:他的任务对上下文长度的要求是多少 token?先把这个数算出来,再回表 17-6 找哪一行够。
关键在于「不需要看图」这一条——它省下 0.87 GiB 的 mmproj,而表 17-6 是含 mmproj 算的。省下来的这一项要加回去。
第一步这样走:5 万字 ÷ 1.2 ≈ 4.2 万 token,还要留出回答的空间,按 5 万 token 规划。然后看表 17-6 哪一行的上下文能到 5 万。
完整答案:
① 需求:约 4.2 万 token 输入 + 回答,按 5 万 token 规划。
② 不加载 mmproj,每一行的 KV 预算加回 0.87 GiB。Q3_K_M:KV 变成 0.99–1.99 GiB → fp16 约 16K–33K,fp8 约 33K–65K。UD-Q2_K_XL:KV 变成 3.9–4.9 GiB → fp16 约 64K–80K,fp8 约 128K–161K。
③ 建议:先试 Q3_K_M + fp8 KV。理由是位宽能保住 4.04(第16章的困惑度对照说明 2 位档位代价陡增),而 33K–65K 这个区间的上沿覆盖得住 5 万。但它落在边界上——如果他那台机器的框架开销靠近 2 GiB 那一端,就只有 33K,不够用。所以这一步之后必须实测。
④ 实测不够就退到 UD-Q2_K_XL + fp16 KV(64K–80K,整个区间都够)。这是拿模型能力换上下文,方向相反,但对「读长文档」这个任务而言,装不下全文比答得糙一点更致命。
⑤ 还有一条别忘了:他有 32 GB 系统内存,按 Unsloth 那句 RAM+VRAM 的判据,总和 48 GB 远超任何一个档位,所以他其实也可以跑 Q4_K_M 走 CPU 混合卸载——代价是慢很多。
这道题的核心判断是:他的瓶颈不是「能不能装下」,而是「上下文够不够」,所以正确的优化方向是省 KV(关 mmproj、开 fp8),而不是一味压权重。而当省 KV 也不够时,才轮到牺牲位宽。

变式:如果他的文档是 20 万字(约 17 万 token),16 GiB 上还有出路吗?如果换成本章主角那张 24 GiB 卡呢?(提示:查表 17-2 的 fp8 那一行。)

自己推一遍:从一张卡的容量,一路推到能开多长的上下文

  1. 你有一张标称 24GB 的卡和一个 17.11 GB 的模型文件。第一步该做什么?(这一步做错,后面全错。)

    想好了再看

    统一单位。24GB 是 24 GiB(二进制),17.11 GB 是十进制。17.11 ÷ 1.0737 = 15.93 GiB。当初为什么会想到先做这一步——因为这两个数来自两个完全不同的行业惯例:显卡厂商按 2 的幂标容量,文件系统按 10 的幂报大小。凡是两个数来自不同来源,第一件事就是问它们的单位是不是同一个。

  2. 现在列固定开销。除了权重,还有哪几项不随上下文长度变化

    想好了再看

    三项:mmproj 视觉塔 0.87 GiB(第11章,多模态模型必须另加)、DeltaNet 固定态 0.14 GiB(第7章,48 层 × 3 MiB,1 个 token 和 100 万个 token 一样大)、框架与激活开销 1.0–2.0 GiB(经验区间)。加上权重共 16.94 + (1.0~2.0) GiB
    第二项是最容易漏的——因为大多数模型没有它。它之所以能被单独列成一个固定项,正是因为 Gated DeltaNet 的状态大小与序列长度无关,这是第6、7章那整条设计的直接后果。

  3. 余下的显存全部给 KV。用第5章的每 token 单价,算出上下文长度。为什么结果必须写成区间?

    想好了再看

    KV 可用 = 24 − 16.94 − (1.0~2.0) = 5.06 ~ 6.06 GiB。fp16 单价 64 KiB:5.06 × 1048576 ÷ 64 = 82,903;6.06 × 1048576 ÷ 64 = 99,287。即 约 83K – 99K token。fp8 单价减半,翻倍到 166K – 199K
    必须写成区间,是因为五项里有一项(框架开销)本身就是区间——一个输入不确定,输出就不该假装确定。把它写成单点值 90K 是一种在数据上撒谎的方式:它让读者以为这个数是测出来的。

  4. 最后一问:这个区间是偏乐观还是偏悲观?

    想好了再看

    偏乐观。第5章讲过,KV 公式算的是数据本身的字节数,是一个下界——真实框架按固定大小的块预留显存(vLLM 的 PagedAttention、llama.cpp 的 slot),最后一块通常填不满,还有页表本身的开销。所以真实可用值会更靠近区间下沿的 83K,而不是上沿的 99K。所有基于公式的显存推导都有这个方向性偏差,写结论时必须标出来。

答辩:如果我是审稿人

你整章最关键的那个数——框架与激活开销 1.0–2.0 GiB——是「本站取的经验区间」。换句话说,你把整章结论的不确定性全都塞进了一个你自己拍的数里。那这个 83K–99K 的区间到底有什么信息量?

参考防守(先自己组织语言再看)

批评成立,但结论的信息量比它看起来的多,理由有三。
区间的宽度本身是有界的。1.0–2.0 GiB 这个跨度对应的上下文差异是 83K 到 99K,只有 19%。也就是说,就算这个经验值拍错了一倍,结论仍然是「八九万」,而不会变成「二十万」或「三万」。整章真正要推翻的那个说法是「24GB 能跑满 262K」——而 262K 需要 16 GiB 的 KV,是本站上沿的 2.6 倍,这个差距远远超出框架开销的不确定性能解释的范围。结论对那个参数不敏感。
其余四项都是硬的。权重 15.93 GiB 来自实测文件;mmproj 0.87 GiB 来自实测文件;DeltaNet 0.14 GiB 来自 config 里的层数和状态维度;KV 单价 64 KiB 来自 config 里的 16 层全注意力 × 4 KV 头 × 256 head_dim。五项里四项可查、可复算。
本站标注了偏差方向。正文明说了真实值更靠近下沿——这比给一个假装精确的单点值诚实。
要消除这个不确定性只有一条路:实测。而实测该怎么做,正文的 caution 里也写了——从 32K 起步往上加到报错。本站给的是一个能让你少试几轮的起点,不是一个可以代替实测的答案。

答辩:如果我是审稿人

Unsloth、Ollama、LM Studio 三家的数字都对上了 18GB 左右,你却说「四家数的根本不是同一样东西」。既然口径不同,它们对上不是巧合吗?你这算什么交叉验证?

参考防守(先自己组织语言再看)

不是巧合,而且这恰恰是交叉验证该有的样子。
三家对上的那一项是权重 + mmproj = 18.04 GB,这一项在三家的口径里都被包含了:Unsloth 的「推荐显存 17–19GB」含它、Ollama 的「下载 18GB」就是它、LM Studio 的「系统内存 17GB」大致是它(略低,因为 LM Studio 不假定你加载 mmproj)。三条不同的路径经过同一个中间量,所以在这一项上必然一致——一致才说明这个中间量是对的。
而它们不一致的部分同样有信息量:Unsloth 比 Ollama 多出的那 1 GB 左右,就是它替你预留的运行余量;LM Studio 说的是内存不是显存,因为它面向 CPU 卸载场景。把「哪部分一致、哪部分不一致」分开看,比笼统说「三家都说 18GB」有用得多。
真正的问题是四家都没有回答的那件事:上下文能开多长。四家给的全是固定开销,没有一家给 KV 那一项——而 KV 才是决定「这张卡能不能干你的活」的那一项。表 17-2 存在的理由就是补上这个缺口,它不是在重复三家的数字,而是在接着往下算。

答辩:如果我是审稿人

你说 24GB 跑不满 262K,可官方明明标着原生 262,144 上下文。是不是官方在虚标?

参考防守(先自己组织语言再看)

不是。262,144 是模型的能力上限,不是任何一张卡的部署上限——这是两个不同层面的量。
模型能处理多长的序列,由训练时的位置编码范围和架构决定(第4章的部分 RoPE、RoPE 底数 1e7)。这是模型自己的属性,和你用什么硬件跑无关。
而你实际能开多长,由式 5-2 和你的显存共同决定。同一个模型,放在 24 GiB 卡上是 8–10 万,放在 80 GiB 卡上就能跑满 262K,放在 8 张 B300 上(本章那道 4 级题)连一千万 token 都装得下。能力上限是模型的,实际长度是你的机器的。
需要被批评的不是官方,而是那些把「原生 262K」和「24GB 流畅运行」两句话并排写、让读者以为可以同时成立的科普稿。本站的处理是:凡是写「24GB 能跑」的地方,必须同时给出上下文限定——这也是第 5、8、17 三章反复回到同一个数的原因。

对你而言未知那 1.0–2.0 GiB 的框架开销,在你的机器上究竟是多少?

整章唯一一个没有硬来源的数就是它,而它是可以被测出来的——答案存在,只是没人把它公开发布成一张表。已知的线索有三条:不同框架的实现差别很大(vLLM 的 PagedAttention 按块预留、llama.cpp 按 slot 分配);开销随批大小和最大序列长度变化;CUDA 上下文本身就要占几百 MB。三条线索指向的是同一件事——这个数不是常数,它是你的配置的函数,而本站给的区间只是一个跨配置的粗略包络。

先做这一步:不用装模型也能测出一半。① 先记下空载显存:不加载任何模型,只初始化推理框架,看 nvidia-smi 报多少——这就是 CUDA 上下文 + 框架自身的底噪。② 加载 Q4_K_M + mmproj,把上下文设成 1024(几乎不占 KV),再记一次。两次的差减去 15.93 + 0.87 + 0.14 GiB,剩下的就是你这台机器上的「框架与激活开销」真值。③ 然后把上下文依次设成 8K、16K、32K、64K 各记一次,把「实测占用」和「表 17-2 算出来的占用」画成两条线。你要看的是这两条线之间的差随上下文怎么变——如果差是常数,说明本站的模型结构对了;如果差随长度增长,说明分页碎片才是主导项,那本站的算法就该改成按块向上取整。这五个数在公开材料里查不到,测出来对任何一个用同款卡的人都有用。

这一层要加什么:写一个 fits(gpu_gib, quant, ctx, kv_dtype) 判定器

为什么现在才加它:前十六层造的是模型,这一层造的是决定要不要跑它的那个函数。它把全站十六章的数字缝成一条判断链——权重体积(第15、16章)、视觉塔(第11章)、DeltaNet 固定态(第7章)、KV 单价(第5章)、框架开销(本章)。写完它,你就有了一个能替你回答「我这张卡行不行」的东西,而且每一项你都知道它从哪来。

GIB = 1024**3 WEIGHT_GB = {"Q4_K_M":17.11, "IQ4_XS":15.71, "Q3_K_M":13.82, "UD_Q2_K_XL":10.68, "Q5_K_M":19.83, "Q6_K":22.88} MMPROJ_GB = 0.93 # 视觉塔,看图就必须加载 DELTA_GIB = 144 / 1024 # 48 层 × 3 MiB,与上下文长度无关 KV_KIB = {"fp16": 64, "fp8": 32} # 每 token;= 2×16×4×256 个元素 OVERHEAD = (1.0, 2.0) # 经验区间,非实测 —— 唯一的软参数 def budget(quant, ctx, kv_dtype, vision=True): w = WEIGHT_GB[quant] * 1e9 / GIB mm = MMPROJ_GB * 1e9 / GIB if vision else 0.0 kv = ctx * KV_KIB[kv_dtype] * 1024 / GIB fixed = w + mm + DELTA_GIB + kv return fixed + OVERHEAD[0], fixed + OVERHEAD[1] # (乐观, 悲观) def fits(gpu_gib, quant, ctx, kv_dtype, vision=True): lo, hi = budget(quant, ctx, kv_dtype, vision) return hi <= gpu_gib, (gpu_gib - hi, gpu_gib - lo) # 判定按悲观端

难点一:三种单位在同一个函数里打架。权重是十进制 GB,KV 单价是 KiB,显卡容量是 GiB。上面的写法是全部先化成 GiB 再相加——如果你图省事把 17.11 直接当 GiB 用,第一个用例的余量会凭空多出 1.18 GiB,而这个错误不会报任何错,只会让你的结论偏乐观。这就是 17.1 节非要单开一节讲单位的原因。

难点二:fits 应该按悲观端判定,但余量要报区间。只返回一个布尔值的判定器是没用的——读者需要知道「差多少」或「富余多少」才能决定下一步。而判定用哪一端也是有讲究的:用乐观端判定会让边界情况说「能跑」然后在实际部署时崩掉,正文的 caution 说过真实值更靠近下沿。

难点三:DELTA_GIB 那一项和 ctx 无关,这不是笔误。写这个函数时你会本能地想给它乘上 ctx——因为其他所有「每层都有的东西」都随长度增长。但 Gated DeltaNet 的循环状态是固定大小的(第6、7章),48 层合计 144 MiB,1 个 token 和 100 万个 token 一样大。如果你手滑让它乘了 ctx,262K 下这一项会变成 3.6 万 GiB,函数会告诉你全世界没有任何机器能跑这个模型。

自己验:四个用例,每一个的答案都是确定的。
fits(24, 'Q4_K_M', 32768, 'fp16') 必须返回 True,余量区间约 (3.06, 4.06) GiB——乐观端约 4 GiB。(总占用:15.93 + 0.87 + 0.14 + 2.00 + 开销 = 19.94~20.94 GiB。)
fits(24, 'Q4_K_M', 262144, 'fp16') 必须返回 False,而且要差得很远:KV 一项就要 16.00 GiB,加上权重与视觉塔的 16.80 GiB 已经 32.80 GiB,超出卡容量 约 10 GiB
fits(24, 'Q4_K_M', 131072, 'fp8') 必须返回 True(KV 只要 4.00 GiB),余量约 (1.06, 2.06) GiB——刚好够,但很紧。
④ 这一条专门验你没漏掉视觉塔:把 vision=False 再跑一次用例 ①,余量必须正好多出 0.87 GiB,变成约 (3.93, 4.93)不多不少就是 0.87——多了说明你把 mmproj 算成 GiB 了(0.93 而不是 0.87),少了说明你哪里又扣了一次。
四条都对上,这一层就成了。再做一个反向自检:把 ctx 从 32768 开始每次翻倍,找出 fits 第一次返回 False 的那个值——fp16 下应该落在 131072(因为 65536 需要 4 GiB KV,能过;131072 需要 8 GiB,过不了)。再用二分法找真正的临界点,应该落在 82,900 – 99,300 之间,正是表 17-2 那个 83K–99K 区间。如果你的二分结果不在这个区间里,回去逐项对表 17-2 的五行。

本章小结

  • 单位是第一位的:显卡标的 24GB 是 24 GiB = 25.77 GB,模型文件是十进制 GB,两者差 7.4%。本站约定:权重用 GB,显存预算用 GiB。
  • 五项预算,缺一不可:权重 15.93 GiB + mmproj 0.87 GiB + DeltaNet 固定态 0.14 GiB + 框架开销 1.0–2.0 GiB + KV。前四项固定,只有 KV 随上下文长度变。
  • 本章的结论句:24 GiB 单卡跑 Q4_K_M 的 Qwen3.8-27B,上下文实际到 约 8–10 万 token(fp16 KV),开 fp8 KV 大约翻倍到 17–20 万原生 262K 在 24GB 上跑不满。这是本站推导,框架开销是经验区间,真实值更靠近下沿。
  • mmproj 那 0.93 GB 必须额外加载,任何一个量化档位的体积里都不含它。不加载模型也能跑,但就看不懂图片了。
  • 四家数字口径各不相同:Unsloth 数推荐显存、vLLM 数服务时总占用、Ollama 数下载体积、LM Studio 数系统内存。三家在「权重 + mmproj ≈ 18 GB」这一项上对得上,但没有一家回答上下文能开多长
  • vLLM 那个 24.6 GiB 的 NVFP4 已经超过 24 GiB,而且说的是 Blackwell 一代的服务器卡,不是 4090。这是全章最容易被误读的一个数字。
  • 四条工具链没有优劣,只有匹配:GGUF 面向消费卡与 Mac,vLLM / SGLang 面向服务器与并发,MLX 面向 Apple 统一内存,Unsloth 兼顾微调。
  • 2.4T 是另一个世界:BF16 要 24 张 B300、NVFP4 要 8 张,GGUF 最低档也要 397 GB。同一个家族,硬件门槛差三个数量级——而这个差距来自第 ① 轴(参数规模),不是第 ④ 轴(量化)。
  • 8 GiB 跑不了 27B(最小档位 8.39 GiB 已超),只能走 CPU 混合卸载,慢很多;16 GiB 装得下 Q3_K_M 或 UD-Q2_K_XL,但上下文被压到几万,是一个必须自己做的取舍。

到这里,一台 Qwen3.8-27B 从图纸到插电的全部账目都算完了。下一章换一个方向:这些数字该怎么读、哪些说法经不起追问、这次开源里有哪些事没有被写进任何一份模型卡——包括那份至今不存在的技术报告。

第18章 评测怎么读,以及这次开源没告诉你的事

前面十七章,你都在跟一台机器打交道:每一个数字都能从 config.json 里查到,对不上就是错,没有商量余地。这一章不一样。这一章讲的是当你走出 config.json,面对一张评测表、一条新闻、一句「某公司在用它」的时候,你手上还剩下什么工具。

学完这一章你应该能做到

  • 拿到任何一张 benchmark 表,能问出七个决定这张表含义的问题
  • 给定一条关于某个开源模型的说法,说出该去哪个一手来源核它、以及那个来源能不能被核到底
  • 准确区分「开源」与「开放权重」,并说清 Qwen3.8 两个模型各自的商用边界
  • 列出这次发布没有提供的五样东西,并解释它们的缺席怎样影响你读到的每一句解释
  • 说出本站自己最可能出错的五个地方,并知道怎么去推翻它们
前置:第1章的证据分级(表 1-3 那四档),第5章的 KV cache 公式,第12章的逐矩阵点钞,第15章的「量化只改位宽」,第17章那条 24GB 推导。这一章不加新概念,它是前面全部内容的兑现。

18.1 拿到一张 benchmark 表,先问七个问题

不问会怎样

一张评测表长得像一张物理常数表:左边一列模型名,右边几列数字,小数点后一位。它给人的印象是「这就是事实,测出来的」。但同一个模型,在同一个评测上,换一档推理配置、换一个精度的权重、换一天的榜单,分数就不是同一个数——而这些前提都不在表里。所以问题不是「这张表可不可信」,而是「这张表说的是哪一次测量」。不问清楚,你比较的可能根本不是同一件东西。

下面七个问题,是本站建议你养成的条件反射。它们对任何一张 benchmark 表都适用,不限于这一次、不限于这个模型。第三列是本站按这七问去核 Qwen3.8 这一次发布的结果——它同时也是一份诚实的清点:有几问是能回答的,有几问是回答不了的。

表 18-1:读任何一张评测表都要问的七个问题
问什么不问会漏掉什么这一次能不能回答
① 谁做的评测——厂商自述、第三方复现,还是社区自测?三者的可信基础完全不同。自述的问题不是撒谎,是它由被评价方选择了全部前提:模型卡上的那张表是厂商自述,按第 1 章的分级属于② 档,不是第三方复现
② 对照组是谁——这几个对手是怎么挑出来的?只要允许挑对照组,几乎任何模型都能在某张表上排第一部分:对照组是模型卡自己选的。你能做的是问一句「我关心的那个模型在不在表里」,不在就要问为什么
③ 哪一天的快照——这个数字有保质期吗?榜单是活的。今天的名次和上周的名次是两个数,而转述时日期通常第一个被丢掉要自己记:模型卡的表没有版本日期概念;凡是榜单类的排名,本站的纪律是不写具体名次,要提就写「以榜单官方页面的当日快照为准」并注明日期
④ 哪个精度的权重——BF16、FP8,还是某一档量化?同一个模型的 Q4 版和 BF16 版是两个测量对象。第 15、16 章讲过误差从哪来要自己问:官方有 BF16 与 FP8 两个仓库,社区量化档还有十几种。表上不写精度时,通行的默认理解是 BF16——但那是默认,不是声明
⑤ 推理配置是什么——温度、最大生成长度、思考深度?同一套权重换一档配置就是另一次实验关键:Qwen3.8 没有 Instruct / Thinking 分版,是统一权重,用 reasoning_effortxhigh / medium / low 三档调推理深度。三档的分数不是一回事,不写档位的分数没法比
⑥ 这个 benchmark 在测什么——它和我的场景像吗?「数学竞赛题正确率」和「帮我改一份两千行的代码」之间没有换算关系要自己去查:这一问的答案不在模型卡里,在那个评测自己的说明文档里
⑦ 有没有可能已经进了训练数据contamination,污染)?如果题目在训练语料里出现过,分数测的是记忆不是能力不能:这一次没有技术报告、没有训练数据说明,任何人(包括本站)都无法从公开材料排除这一条

污染contamination):评测用的题目(或它的答案)出现在了模型的训练数据里。这时模型答对可能只是背下来了。它不需要有人作弊——互联网上什么都有,训练语料是从互联网上爬的,题目自然可能混进去。

七问里最值得单独拎出来的是第 ⑤ 问,因为它是这一代模型的新情况。以前你会看到同一个模型发两个版本:一个偏对话(Instruct),一个偏推理(Thinking),分数各报各的。Qwen3.8 这一次只有一套权重,深度靠调用时传的 reasoning_effort 决定。好处是模型管理简单了,副作用是:「Qwen3.8-27B 得了多少分」这句话本身是不完整的——不说档位,就像说「这辆车油耗多少」却不说是市区还是高速。

一句话记住:一个分数至少要挂三个标签才算完整——谁测的、哪个精度的权重、什么推理配置。少一个,它就只是一个数字,不是一次测量。

有人告诉你:「Qwen3.8-27B 在某个代码评测上得了 XX 分。」按表 18-1,这句话至少缺了三个必要限定。是哪三个?

先想:如果你想自己重跑一遍来验证这个数,你会发现你缺什么信息才动不了手?缺的那些就是答案。
关键在于「重跑」这个动作。你要下载一份权重(哪一份?)、要设定调用参数(设成什么?)、要知道结果该跟谁比(跟谁?)。三个问号就是三个缺失。
第一步这样走:把表 18-1 的七问逐条套在这句话上,划掉那些「这句话里已经有了」的,剩下的就是缺口。你会发现只有模型名和评测名是给了的。
完整答案:其一,谁测的(第 ① 问)——是官方模型卡上的自述,还是第三方复现?两者的证据档位不同。其二,哪个精度的权重(第 ④ 问)——BF16、FP8,还是某个社区量化档?不同精度是不同的测量对象。其三,推理配置(第 ⑤ 问)——reasoning_effort 是 xhigh、medium 还是 low?这三档在同一套权重上跑出来的结果不是一回事。
还可以加上第四个:什么时候测的(第 ③ 问)。如果这个数来自某个会变动的榜单,没有日期它就没有保质期标签。
注意这道题的答案不包含「这个数是真是假」。质疑真假是最没有效率的怀疑方式;有效的怀疑是问清前提,因为绝大多数误导不是来自假数字,是来自被省略的前提

变式:把这句话补全成一个你愿意签名的版本。补完之后数一数它长了多少字——这个长度差,就是「说准确」要付的成本。为什么大多数人不愿意付?

甲拿了一张表:模型 A 在某评测上 82 分,Qwen3.8-27B 79 分,于是他说「A 更强」。你追问后得知:A 那一行跑的是 A 的推理模式,Qwen3.8 那一行跑的是 reasoning_effort=low。请说清这个比较错在哪儿,以及要怎么改才能比。

先想:这两行数字,测的是不是同一件事?「同一个模型」和「同一次实验」是两个概念。
关键在于 Qwen3.8 是统一权重:low 和 xhigh 用的是同一堆参数,差别在调用时让它想多深。所以那 79 分不是「这个模型的能力」,是「这个模型在最省算力的一档下的表现」。
第一步这样走:先不要下结论,先把这张表补成两列——「模型」和「推理配置」。补完你会看到左右两行的第二列不一样,这时候问题就自己浮出来了。
完整答案:错在控制变量没做。分数是「模型 + 配置」这一对组合的函数,两行的配置不同,差值就没法归因给模型。要改,有两条路:路一,把 Qwen3.8 的档位提到与 A 的推理模式大致对等(例如 xhigh),重跑;路二,反过来,把两边都压到最省的配置,比「便宜时能做到什么」。两条路测的是两个不同的问题,都合法,但必须说清楚测的是哪一个
更要紧的是第三条路:把成本也报出来。xhigh 档要生成的思考过程更长,意味着更多 token、更长等待、更多钱。只报分数不报成本的比较,等于比两辆车谁跑得快却不说谁烧了多少油。
最后提醒一句:即使控制好了配置,这张表仍然是「某一天、某个评测、某个精度」上的一个点,它不支持「A 更强」这种去掉了全部限定的结论。

变式:如果 A 也是统一权重加档位调节,而两边都没写档位,这张表还剩下多少信息量?(提示:想想「两个未知数一个方程」。)

18.2 一个可以自己动手的核对练习

上一节教的是「该怀疑什么」。光有怀疑没用,怀疑必须能落成一个动作:去哪儿、打开什么、看哪一行。这一节就是那份动作清单。

用法:下次你在任何地方读到一条关于某个开源模型的说法,先判断它属于下面哪一类,然后按第二列去核。核不到的,就按核不到处理——「我没核到」是一个合法的结论,「大概是真的吧」不是。

表 18-2:六类常见说法与它们的一手核对入口
你读到的说法去哪儿核核到之后能有多确定
参数量、层数、专家数、上下文长度config.json(仓库根目录,可直接在网页上看原文)最高。这些字段能互相验算:层数 ÷ full_attention_interval 要等于全注意力层数,逐矩阵点钞的总和要能对上官方名字。第 12 章带你亲手数过一遍
许可证、能不能商用仓库里的 LICENSE 文件——不是新闻标题,不是模型卡摘要,也不是页面上那个许可证标签最高,但必须读到文件。页面上的标签是仓库的一个元数据字段,真正有效力的是文件正文。见 18.3
文件体积、有哪些量化档仓库的文件列表(每个文件后面就标着真实字节数)。这是硬事实,但要注意是哪一家做的量化——不同人做同一档,体积会差一点
「XX 显存能跑」推理框架自己的 recipe / 文档(vLLM、Unsloth、Ollama、LM Studio、SGLang 都有),并看清它说的是哪一代卡、哪个量化档、多长上下文中高。数字是实测的,但「能跑」这个词的含义各家不同:有的指权重装得下,有的指带上一定长度的上下文也装得下
排名、榜单名次榜单官方页面,并把日期记下来。数字本身是真的,但它有保质期。转述里丢掉日期,等于把一个会变的量说成了常数
「某公司在用某模型」原始出处:财报电话会实录、公司官方博客、当事人本人的公开发言。看清是谁在什么时候说的,以及原话里到底出现了哪些词差别极大。这一类在转述中最容易变形:品牌名被替换、时间被抹掉、「开源模型」这样的泛指被换成某个具体名字。核到原文之前,一律只当线索
这份清单背后只有一条原则

一手来源的层级越高,你需要的信任就越少。config.json 你几乎不需要信任任何人——那是一个可以下载、可以逐字段验算的文件。读一篇转述,你要同时信任作者读对了、抄对了、没有省掉限定,而这条链上每多一环,出错的机会就多一次。这不是说转述都是错的,而是说:能少信一层就少信一层,这是免费的。

常见误解:以为「查证」等于「多搜几篇看看有没有说法一致」

不是。十篇文章说同一件事,只能说明它们抄了同一个源头,不能说明那个源头是对的。第 1 章讲过一条真实的传播链:源头写错了一个许可证 → 转述者没有回查许可证文件 → 再转述者把「据报道」也省掉了 → 最后它读起来像一条事实。整条链上没有人撒谎,只是没有人回到一手材料
所以正确的动作不是「多看几篇」,是「顺着链条往回走一步」:这篇文章的说法是从哪儿来的?那个来源又是从哪儿来的?一直走到一个不再引用别人的东西为止。走到头的那个东西,才是你真正要读的。

给下面四条说法各配一个具体的核对入口(要说清「打开什么、看哪一项」),并按你核完之后的确定程度从高到低排序:
(a)「它的上下文可以到一百万。」 (b)「4bit 量化后大概 17 个 GB。」 (c)「它是 Apache 2.0,可以随便商用。」 (d)「好几家大公司都在用它。」

先想:这四条里,哪些指向一个具体的、能打开的文件?哪些指向的是一个说法?前者可以核到底,后者只能核到某个人说过这句话。
关键在于分清「核实这条陈述本身」和「核实有人说过这条陈述」。(d) 只能做到后者——就算你找到了原始发言,你核到的也只是「某人在某天说了某句话」,不是「这件事现在仍然成立」。
第一步这样走:先对每条问一句「如果我要证明它是错的,我要拿出什么?」(a) 要拿出 config 里的字段;(b) 要拿出文件列表的字节数;(c) 要拿出 LICENSE 原文;(d) 你拿不出任何能证伪的东西——这本身就是答案的一部分。
完整答案:
(a) 打开 config.jsonmax_position_embeddings(27B 是 262144),再看模型卡关于扩展的说法(可扩展至 1,010,000)。注意这是两个数:一个是原生训练长度,一个是靠外推方法拉长的上限,混为一谈就会得到一个很好听但没有限定的说法。确定度,但要写清是哪一个。
(b) 打开量化仓库的文件列表,看那一档 GGUF 的字节数(Unsloth 的 Q4_K_M 是 17.11 GB)。并且别忘了视觉塔的 mmproj 还要另外 0.93 GB——这一条几乎总是被漏掉。确定度
(c) 打开那个仓库LICENSE 文件读正文。确定度,但这句话本身有陷阱:它没说是哪个模型。27B 是 Apache 2.0,2.4T-A95B 不是(见 18.3)。
(d) 只能去找原始出处(财报实录、官方博客、当事人发言),核到的是「某人在某个日期说过某句话」。确定度最低,而且必须带日期——一句 2025 年的话被放进 2026 年的发布语境里,读起来会像是因为新东西才发生的新采用。
排序:(a) ≈ (b) ≈ (c) > (d),其中 (c) 的风险不在核不核得到,在于问句本身漏了主语

变式:给 (d) 补一个你能接受的写法。提示:至少要包含「谁说的、什么时候说的、原话里出现了哪些词」三样。补完之后,你会发现它已经变成了一条弱得多、但你敢签名的陈述。

18.3 许可证:这次最容易被搞错的一件事

为什么这一节值得写这么长

因为它是这一章唯一一条会让你赔钱的知识。前面的东西写错了,代价是理解上的偏差;许可证读错了,代价是你已经把产品做完上线了,才发现用的那份权重不允许你现在这个用法。而这一次恰好是最容易搞错的情形:同一个系列、前后两天上线、名字只差几个字符的两个模型,用的是两份不同的协议。

表 18-3:Qwen3.8 两个模型的协议(以仓库 LICENSE 文件为准)
模型协议你能做什么
Qwen3.8-27B(含 FP8 版)Apache 2.0真开源。免费使用、修改、再分发、商用;改了也不用把你的代码开源;只要保留版权与许可声明、注明你改过哪些地方
Qwen3.8-2.4T-A95B(含 FP8 版)qwen3.8-max 自定义协议开放权重。可以下载、可以用、可以改,但有两条门槛,见下

Apache 2.0 到底给了你什么

Apache 2.0 是软件世界用了二十多年的一份标准许可证,条款是固定的、公开的、被无数次解释过的。它给你四样东西:再分发商用,都不用付钱、不用申请、不用告知。它还有两个对读者特别重要的性质:

第一,它不是「传染性」的(不是 copyleft)。你把它用进你的产品里,你自己写的那部分代码不需要因此开源。这一点和另一类许可证(用了就必须把整个作品也按同样条款开放)是根本区别。

第二,它带一份明示的专利授权。贡献者把与这份作品相关的专利许可给了你;但如果你反过来去告别人这份作品侵犯了你的专利,你自己的授权就终止了。这条设计的意思是:大家一起把武器放下。

你要付出的义务只有几条常规的:保留原有的版权声明和许可证副本、在你改动过的文件里注明你改过、如果原作附带 NOTICE 文件就一并保留。以及标准的免责——出了问题作者不负责。

那份自定义协议的两条门槛

2.4T-A95B 那份 LICENSE 文件的内容不是 Apache 2.0,而是一份名为 qwen3.8-max 的自定义协议。对绝大多数读者(个人学习、研究、小规模商用)它没有影响,但它写明了两条门槛:

表 18-4:qwen3.8-max 协议里两条会被触发的门槛
门槛触发条件触发之后要做什么
① 收入门槛你经营的是 MaaSModel as a Service,把模型本身当服务卖)或 AI 助手类业务,且连续 12 个月合计收入超过 5000 万美元商用前须另行获取授权,不能直接按这份协议用
② 规模门槛把它用在月活跃用户超过 1 亿的商业产品里须在用户界面显著位置展示模型名称

把这两条翻译成人话:你自己玩、你的小公司拿它挣钱,都不受影响;但如果你已经是一家很大的公司,或者你卖的正好就是「模型服务」这门生意,那就得回去读原文、走一趟授权流程。两条门槛的定位是清楚的——它不是在拦住学习者,它是在给最大的那一小撮商业用途设一道闸。

这一段是本站按 LICENSE 文件复述的要点,不是法律意见

本站只摘出了对读者最可能触发的这两条门槛;那份文件里还有免责、合规使用之类的常规条款,本站没有逐条转述。真要拿它做商业产品,唯一正确的动作是打开那个仓库里的 LICENSE 文件读原文,必要时找懂行的人看。任何二手复述(包括这一段)都不能替代原文。

用词纪律:「开源」和「开放权重」不是一个词

开放权重open weights):模型的参数文件可以自由下载和使用,但附带的许可证不满足「开源」的通行标准——比如限制了使用者的规模、限制了某些使用领域、或者某些用途需要单独授权。

在软件世界里,「开源」(open source)是一个有公认判据的词,其中两条最关键:不歧视任何个人或团体不限制任何使用领域。Apache 2.0 完全符合,所以 27B 说「开源」没问题。而一份写着「收入超过某个数就要另谈」的协议,恰恰是按使用者的身份做了区分——它是一份很慷慨的协议,但它不是开源协议。这时候准确的说法是「开放权重」。

这个区分在英文社区是分得比较清的(open sourceopen weights 常被并列使用),中文报道里则经常混着用,两个词都译成「开源」。把这条用词纪律记住,本身就是一个知识点:以后你看到一篇文章通篇只说「开源」而从不区分,你就知道要自己去查 LICENSE 了。

常见误解:看到一个 Apache 2.0,就以为一家人都是

同一次发布里的两个模型可以是两种协议,这一次就是。而且它们的名字长得极像(Qwen3.8-27BQwen3.8-2.4T-A95B),发布只差两天,出现在同一篇公告里。
这就是为什么表 18-2 里的核对入口写的是「那个仓库的 LICENSE 文件」而不是「官方的许可证」——许可证是按仓库挂的,不是按系列挂的。核的时候,你要先确定自己下的到底是哪一个仓库。

下面五个场景,分别判断:需不需要另行获取授权?需不需要在界面上显著展示模型名称?说出你的依据。
(a) 高中生用 2.4T-A95B 做一个课程作业,不对外发布。
(b) 一家年收入 200 万美元的创业公司,用 2.4T-A95B 做企业客服机器人卖给客户。
(c) 一家云平台,过去 12 个月靠卖模型 API 收入 8000 万美元,想把 2.4T-A95B 上架成一个可调用的模型。
(d) 一个月活 1.5 亿的社交产品,接入 2.4T-A95B 做内容摘要。
(e) 同样是那个月活 1.5 亿的产品,但接入的是 27B

先想:第一件要确定的不是条款,是哪个模型。两个模型两份协议,先分清用的是哪一份,再谈条款。
关键在于两条门槛是各自独立触发的:① 看的是「业务类型 + 收入」,② 看的是「产品月活」。一个场景可能一条都不触发、触发一条、或者两条都触发。别把两条混成一条。
第一步这样走:给每个场景填三个格子——「用的哪个模型」「是不是 MaaS / AI 助手类业务,连续 12 个月收入多少」「产品月活多少」。填完之后答案是自动的。
完整答案:
(a) 都不触发。个人学习使用,既不是商业收入场景,也谈不上月活。
(b) 都不触发。它虽然是商用、也沾 AI 助手类业务,但收入 200 万美元离 5000 万美元的门槛还很远;月活也不到 1 亿。这个场景值得强调:绝大多数真实的商业使用都落在这里,两条门槛都够不着。
(c) 触发门槛 ①。它正是「把模型本身当服务卖」的 MaaS 业务,且连续 12 个月收入 8000 万美元已超过 5000 万美元,商用前须另行获取授权
(d) 触发门槛 ②。月活 1.5 亿超过 1 亿,须在用户界面显著位置展示模型名称。注意它是不是 MaaS 业务另说——门槛 ② 只看月活,不看业务类型。
(e) 都不触发。27B 是 Apache 2.0,这两条门槛根本不属于它。月活一百亿也一样。这一问是整道题的题眼:换一个模型,同一个场景的答案就完全不同——这正是为什么必须按仓库分别核许可证。
最后加一句必要的限定:以上是按表 18-4 那两条门槛做的推演,真实合规判断必须回到 LICENSE 原文。

变式:场景 (c) 那家云平台如果只上架 27B,还需要授权吗?如果它同时上架两个模型,它需要做的事有什么不同?(提示:答案会解释为什么很多平台上这两个模型的可用范围不一样。)

我能不看材料说清:这一次哪个模型可以叫「开源」、哪个只能叫「开放权重」,以及后者那两条门槛分别看的是什么(不是看什么模型,是看使用者的什么)。

答辩:如果我是审稿人

你花这么大篇幅讲许可证,是不是小题大做?权重都已经放出来了,我下载下来自己用,谁会来管我?

参考防守(先自己组织语言再看)

三点。

第一,这一节的读者不是「自己用」的人。对个人学习、研究、做小项目的人,两条门槛确实都够不着——18.3 那道题里的 (a)(b) 就是这个意思,而且本站特意把 (b) 写得很显眼。这一节真正要保护的是另一类读者:某天他所在的公司要基于它做产品,而他是那个说「没事,这个是开源的」的人。那一句话如果说错了,成本不由他一个人承担。

第二,「谁会来管我」这个问题的答案随时间变化。没人管的时候确实没人管,但商业世界里真正会触发核查的时刻是可预期的:融资尽调、被收购、上市、和大客户签合同。这些时刻不看你当初的想法,只看你用的东西附了什么条款。合规问题的特点是它不在你使用的时候爆,在你成功的时候爆。

第三,也是本站更在意的一点:这是一个训练读者「回到一手材料」的最佳例题。它的一手来源是一个具体的、几分钟就能读完的文件;它的正确答案是唯一的;而它在二手转述里的失真是真实发生过的。很难再找到一个比它更好的例子来说明「顺着链条往回走一步」值多少钱。
这一击里有一半我们接受:如果读者只是想学怎么跑起来,这一节对他确实是可以跳过的。本站没打算把它写成必读,只是把它放在了跳过之前能被看见的位置。

18.4 这次开源「没有」的东西

一份诚实的材料,除了讲清有什么,还得讲清没有什么。下面五条是本站逐条核过的缺席清单。它们不是缺点——把权重完整放出来这件事本身已经给了读者极大的空间——但它们直接决定了这个站里的每一句解释该被信到什么程度。

表 18-5:这一次发布没有提供的五样东西
没有什么本站是怎么确认的它的缺席意味着什么
① 没有 arXiv 技术报告Qwen3.8 检索 arXiv 无结果;Qwen3.5、Qwen3.6、Qwen3-Next 同样没有。官方最后一篇停在 Qwen3(arXiv:2505.09388)没有一份把「主张」和「证据」绑在一起、并被同行审过的文档。所有的「为什么这么设计」都失去了权威答案
② 没有官方 GitHub 仓库两个模型卡里唯一的 GitHub 链接是两个推理框架的支持 PR(vLLM 与 SGLang),不是官方代码库没有官方实现可读。你要看「这个字段到底怎么被用」,只能去读第三方框架的实现
③ 没有小尺寸官方只有 4 个仓库:27B、27B-FP8、2.4T-A95B、2.4T-A95B-FP8。4B / 8B / 32B 全部不存在(社区流传的小尺寸是第三方蒸馏,很容易被误传成官方)没有一条从 0.6B 到 2.4T 的完整尺寸阶梯可供横向对照。第 13、14 章讲尺寸时只能借上一代和别家的公开数据
④ 没有 Instruct / Thinking / Base 变体统一权重,thinking 默认开,深度靠 reasoning_effort(xhigh/medium/low)调这是路线变化,不是遗漏。但它让「这个模型得几分」这句话必须带档位(见 18.1 第 ⑤ 问)
⑤ 没有消融实验3:1 的层布局比例、head_dim 为什么是 256、FFN 中间维为什么是 17408、专家为什么是 512 选 10+1——每一个都没有官方解释这是最要命的一条。见下

消融实验ablation study):把设计里的某一项换掉或去掉,其余全部固定,重跑一遍看结果差多少。它是回答「为什么选这个而不是那个」的唯一严肃方式——因为它给出的是对照,不是理由。

本站必须郑重声明的一件事

正因为没有技术报告、没有消融实验,这个站里所有「为什么这么设计」的解释,都是本站基于公开论文和 config.json 做出的推断,不是官方说法。

具体说:3:1 这个比例为什么不是 2:1 或 4:1,head_dim 为什么从常见的 128 加倍到 256,FFN 中间维为什么恰好是 17408(= 17 × 1024,扩张比 3.4),专家为什么是 512 个选 10 个再加 1 个共享——这四个问题本站在对应章节都给了看起来合理的解释,那些解释建立在同类模型的公开论文、以及本站自己的算术上,但没有任何一条得到过官方确认

你应该怎么读它们:当成一个可能的解释,而不是那个解释。凡是本站写「可能是为了」「这样做的好处是」的地方,你都可以问一句「有没有别的解释也说得通」——通常是有的。而真正能分出胜负的那个实验,没有人公开做过。

但缺席也给了一样东西

把这件事只当坏消息就太亏了。没有论文的直接后果是:config.json 反而成了最可靠的一手材料。

为什么?因为一篇论文是出来的——作者选择讲什么、不讲什么,用什么口径报数字,哪一组实验放进去哪一组不放。而 config.json 不是写给你看的,它是加载器要用的:里面每一个字段都必须是真的,否则模型根本加载不起来。它没有修辞空间。

更要紧的是它可复算。第 12 章你亲手做过:拿着那十几个字段,逐张矩阵数下去,最后数出 27.36 B2.420 T / 95.29 B,和官方的名字「27B」「2.4T-A95B」对上了。这个对上不是巧合——它意味着这些字段确实描述了那堆权重。一份能被你自己算出官方数字的配置文件,比一份你只能选择相信的技术报告,在证据等级上更高。

一句话记住:没有论文,你失去的是「为什么」;但 config.json 还在,你没有失去「是什么」。这一章教的全部东西,就是别把前者当后者用

答辩:如果我是审稿人

你把「厂商自述」当成一个分档写进表里,是不是过度怀疑?厂商在一个能被第三方复现的数字上撒谎,风险远大于收益,他们没有动机。

参考防守(先自己组织语言再看)

你说得对,而且本站同意那个前提。但你把「分档」读成了「指控」,这是两回事。

② 档「官方声明」的含义不是「可能是假的」,是「它的成立依赖于一组没有写出来的前提」。用哪个精度的权重、哪一档 reasoning_effort、哪个版本的评测脚本、怎么解析模型输出算对错——这些每一项都会改变分数,而每一项都由发布方选定且通常不公布。真正的风险从来不是造假,是不可比:你拿这张表里的分数去和另一篇文章里的分数比,两边的前提大概率不一样,比出来的差值没有意义。

还有一条更细的区分,本站在表 1-3 里写过但值得重申:官方声明里的「事实性描述」和「自评成绩」不是一档东西。「64 层、其中 16 层全注意力」这种描述,你可以拿 config.json 去核,核得上就是硬事实;「在某评测上得了 XX 分」你核不了,除非你自己有卡、有那份评测、还愿意花时间。前者本站当 ① 档用,后者才当 ② 档。

这一击里我们要认下一半:如果读者读完之后得到的印象是「厂商说的都不能信」,那是本站的表述失败。正确的印象应该是「厂商说的要问清是哪一次测量」。这两句话的差别,就是「怀疑」和「谨慎」的差别——前者让人什么都不信,后者让人知道该去查什么。

下面四条陈述,哪些是能从公开材料核实的,哪些是推断?对每一条推断,写出「如果要把它变成能核实的,还缺什么材料」。
(a)「27B 有 64 层,其中 16 层是全注意力。」 (b)「用 3:1 而不是 1:1,是因为全注意力层太贵。」 (c)「Q4_K_M 的 GGUF 文件是 17.11 GB。」 (d)「head_dim 设成 256 是为了配合部分 RoPE。」

先想:这四条里,哪些的主语是一个可以被打开、被数出来的对象?哪些的主语其实是设计者当初的想法?后者你永远核不到,除非设计者自己说。
关键在于句子里有没有「因为 / 为了」这一类因果词。因果词一出现,这句话就从「描述」跳到了「解释」,而这一次的材料里没有任何一份文档提供解释——见表 18-5 第 ⑤ 条。
第一步这样走:把每一条改写成「我要去 XX 打开 YY 看 ZZ」的形式。改得出来的就是可核实,改不出来的就是推断。你会发现 (b)(d) 改到一半就卡住了——你不知道要打开什么。
完整答案:
(a) 可核实,① 档。打开 config.jsonnum_hidden_layers = 64full_attention_interval = 4,64 ÷ 4 = 16。而且它还能被交叉验证:模型卡的文字写的是 16 × (3 线性 + 1 全),独立的第三方推理框架博客在讲 2.4T 时也给出 23 + 69 = 92 的同构说法。三处互相印证。
(c) 可核实,③ 档。打开那个量化仓库的文件列表看字节数。要注明是哪一家做的哪一版。
(b) 推断。「全注意力层贵」这半句是可核实的(KV cache 随长度线性增长,第 5 章算过),但「所以才选 3:1」这个因果是补上去的。要把它变成可核实的,缺的是一组消融:同样的训练配方、同样的数据、同样的算力,把比例从 1:1 扫到 7:1,把每一档的能力曲线报出来。没有这个,你只能说「3:1 与这个理由不矛盾」,不能说「因为这个理由所以 3:1」。
(d) 推断,而且比 (b) 更弱。head_dim = 256partial_rotary_factor = 0.25 这两个字段都是可核实的事实,但「前者是为了后者」纯属把两个同时出现的事实接成了一条因果。缺的材料同样是消融,外加设计者的说明。注意一个陷阱:这类解释听起来越顺,越危险——顺是因为它自洽,不是因为它被证实了。
一句总结:凡是句子里有「因为」「为了」的,这一次都要默认它是推断,除非它指向的是一条可以自己算出来的数学关系。

变式:本站在讲 FFN 时说「17408 = 17 × 1024,扩张比 3.4」。这句话是可核实还是推断?如果本站接着说「选 3.4 是为了在参数量和能力之间取平衡」呢?两句话的证据等级差在哪一个词上?

18.5 时效性:一条信息的保质期

前面四节讲的是「这句话对不对」。这一节讲一件更容易被忽略的事:就算它当初是对的,它现在还对吗?

技术信息有两类,保质期差得极远。一类是结构性的:某个模型有 64 层、某个文件 17.11 GB、某份协议写了什么。这类东西一旦发布就基本不动了,你今天核和明年核结果一样。另一类是状态性的:谁在用什么、什么东西排第几、哪个框架支持到什么程度。这一类每天都在变,而它们在文章里长得和第一类一模一样——都是一句话加一个数字。

两个中性的例子

例一:一句话被搬了家。假设某公司的负责人在 2025 年某次公开访谈里说「我们大量依赖某个模型,它很好,又快又便宜」。这句话在当时是准确的。一年后,一个新模型发布,这句话被引用进了发布相关的报道里。读者读到时的自然理解是:这家公司因为新模型才开始用的。但原话说的是一年前的另一个模型,甚至可能是另一代。没有人撒谎,是时间标签在转述中掉了。

这类错位的识别方法极其简单:看到任何一条「某某公司在用它」,先找日期。找不到日期,就当它没有日期——也就是说,把它降级成「某个时间点上曾经成立的事」,而不是「现在的事实」。

例二:一个名次会动。公开榜单是活的:新模型不断加入,评测集会更新,投票在持续累积。假设某个模型今天排在某个位置,一周后它可能往上走也可能往下掉,而这几个数没有一个是错的——它们只是几个不同时刻的快照。所以本站的纪律是:不写具体名次。要提就写「以榜单官方页面的当日快照为准」,并注明你是哪天看的。

连日期本身也可能有两个

这一次就有一个很好的例子:建仓日和权重上线日不是一回事。两个模型的仓库在 8 月 5 日和 8 月 8 日就已经建好了(这时候页面存在,但没有权重),而权重真正上线是 2.4T 在 8 月 12 日、27B 在 8 月 14 日
于是同一个模型会有两个都为真的「发布日期」。两个数并排出现时读起来像在打架,其实只是在说两件不同的事。引用时必须写清你说的是哪一个——这是「带上日期」之外的第二层功课:带上日期的定义。

本站自己也受这条规矩约束:全站数字的检索日期是 2026 年 8 月 16 日,写在页脚。这意味着结构性的那一类(层数、参数量、文件体积、协议条款)你以后读到时大概率仍然成立;状态性的那一类(生态规模、有哪些框架支持到什么程度)你要按自己的日期重新核一遍。一个站敢把检索日期写出来,你才有办法判断它过期没有。

把下面五条分成「结构性」和「状态性」两类,并给状态性的那几条各补一句必要的限定,让它变成你敢签名的写法。
(a)「27B 的词表是 248,320。」 (b)「主流推理框架都已经支持它了。」 (c)「Q4_K_M 是 17.11 GB。」 (d)「它在某个榜单上排名很靠前。」 (e)「2.4T-A95B 用的是自定义协议。」

先想:哪几条描述的是一个已经固定下来的东西,哪几条描述的是世界当前的状态?后者会随时间变,前者不会。
关键在于问一句:「如果我明年再核一遍,答案有没有可能不同?」有可能变的,就要带时间标签。注意 (e) 有个隐含前提——协议理论上也可能被修订,但它是写进文件里的,比榜单稳定得多。
第一步这样走:先把明显不会变的挑出来(打开一个文件就能读到、且那个文件是随权重一起冻结发布的),剩下的默认都是状态性的。
完整答案:
结构性:(a)(c)(e)。(a) 来自 config.jsonvocab_size,随权重一起冻结;(c) 是某个具体量化仓库里某个具体文件的字节数,那个文件不会自己变(但换一家做的量化就是另一个数,所以写的时候要带上是谁做的);(e) 来自仓库的 LICENSE 文件。
状态性:(b)(d)。
(b) 要补两样:哪些框架什么时候。可签名的写法:「截至 2026 年 8 月,vLLM 与 SGLang 已合入支持(两个 PR 的链接在模型卡里);其他框架的支持情况请按你读到的日期自行核对。」注意「都已经」这种全称词在状态性陈述里几乎总是站不住。
(d) 要补三样:哪个榜单、哪一天、什么名次。而本站的纪律更严一步——具体名次干脆不写,因为写下去就等于埋了一个会过期的雷,写成「以榜单官方页面的当日快照为准」既准确又不会烂掉。
顺带一提 (c) 那个陷阱:它是结构性的,但不是唯一的。不同的人做同一档 Q4_K_M,体积会差一点点;而且 27B 还要额外加载 0.93 GB 的视觉塔文件。所以完整写法是「某家做的 Q4_K_M 是 17.11 GB,另需 0.93 GB 的视觉塔」。

变式:本站页脚写着检索日期 2026-08-16。假设你在两年后读到这个站,表 18-5 那五条「没有的东西」里,哪几条你必须重新核?哪几条大概率还成立?说出你的判据。

我能不看材料说清:为什么「这个模型有 64 层」和「这个模型排第几」不能用同一种方式引用,以及后者少了哪一样东西就不该写。

18.6 这个站可能哪里是错的

教了一整章怀疑,最后一节该把这把尺子对准自己。下面是本站已知的五处不确定性——不是「可能存在的问题」这种客套话,是具体到哪一条数字、哪一步推导。

本站已知的五处不确定

① 24GB 那条推导里的框架开销是经验区间,不是实测。整条推导的结构是可核的:权重 + 视觉塔 + DeltaNet 固定态 + 框架开销,剩下的给 KV。但其中「框架与激活开销 1.0–2.0 GiB」这一项本站没有实测,是按常见推理框架的经验取的区间。这就是为什么结论写成区间(约 8–10 万 token)而不是一个数。真实部署还有显存分页与碎片的开销,实际值会更靠近区间的下沿。

② DeltaNet 的固定状态 144 MiB 是按论文定义推的。每个值头持有一个 128×128 的状态矩阵、按 float32 存、48 层——算出每层 3 MiB、全模型 144 MiB。这个算法本站认为是对的,但没有见到官方或任何推理框架公布过这个数字去印证它。如果哪个框架公布了实测占用,请以那个为准。

③ 各个「为什么这么设计」的解释都是推断。见 18.4 那一节的声明。3:1、head_dim 256、17408、512 选 10——本站给的理由都合理,都没被证实。

④ 混合比例实验室里的「回忆能力」是玩具任务模拟,不是官方消融。它的用途是让你看到趋势的形状拐点大概在哪,不是给你一组可以引用的数值。把里面任何一个数字拿出去当结论都是错用。

⑤ 量化掉多少能力的实证研究,都不是在 Qwen3.8 上做的。本站引的那几篇(长上下文下的量化影响、量化对推理链的影响等)测的是别的模型。它们的结论可以作为预期,不能作为本模型的实测。特别是「fp8 KV 让上下文翻倍」这句话,字节账是硬的,但「翻倍之后能力不掉」这一层在这个 3:1 混合布局上没有公开验证。

除了这五条,还有一类问题是口径——本站自己踩过的坑。GB 和 GiB 差 7.4%:显卡标称的 24GB 实际是 24 GiB(= 25.77 十进制 GB),而模型仓库页面上标的文件体积是十进制 GB。同一条推导用错单位,结论能差出好几万 token。本站最终统一到:权重与文件体积用十进制 GB(与各仓库页面一致),显存预算与 KV cache 用 GiB。你以后在别处看到显存数字对不上时,第一个该怀疑的就是这个。

怎么推翻本站

本站的每一条推导都把中间步骤写出来了,这不是为了显得严谨,是为了让你能只推翻其中一步。比如上面第 ① 条:你不需要否定整条 24GB 推导,你只需要拿一张 24GB 卡实测一次框架开销,把那个区间收窄——推导的其余部分照样成立,结论跟着变窄。这比「这个站不靠谱」有用一万倍。

如果你发现了确凿的错误,正确的处理方式和你核别人时是一样的:写清你在哪儿找的、找的是什么、看到了什么。一条写成「你们第 17 章那个数不对」的反馈没法用;一条写成「我在 X 框架的 Y 文档里看到 Z,和你们第 17 章的 W 冲突」的反馈,本站可以立刻去核。这一章教你的全部方法,反过来用在本站身上同样成立。

一个朋友来问你:他要做一个面向企业的 AI 写作助手,公司去年收入约 300 万美元,他打算先用手上那张 24GB 卡做原型,做成了再上云。他在网上看了几篇文章,兴冲冲地说「Qwen3.8 是开源的,量化一下 24G 就能跑,评测分还很高」。
请你写出你会告诉他的五件事,每件事标注它的证据等级,并指出其中哪一件如果搞错,代价最大,以及为什么。

先想:他那句话里其实塞了三个独立的断言(开源、24G 能跑、分高),每一个都对应本章的一节。先把它们拆开,再各自去核。另外还有两件他没说但会撞上的事——想想他将来上云、以及他会去哪里找「别人在用」的证据。
关键在于「面向企业的 AI 写作助手」这句业务描述——它正好落在自定义协议第一条门槛的业务类型里。所以第一件要问的不是技术,是他打算用哪个模型:两个模型两份协议,答案完全不同。
第一步这样走:给他列一张四列的表——「他的说法」「该去哪儿核」「核完的结论」「证据等级」。填满之后,「哪件代价最大」这一问就有答案了:看哪一件搞错之后没法事后补救。
完整答案,五件事:
一、先分清用哪个模型,再谈开不开源。如果用 27B:Apache 2.0,免费商用、改了也不用开源,他可以放心。如果用 2.4T-A95B:那是 qwen3.8-max 自定义协议,属开放权重;他现在收入 300 万美元,两条门槛(连续 12 个月收入 5000 万美元、月活 1 亿)都够不着,现在可以用——但他做的正是「AI 助手类业务」,所以要在项目文档里写一笔:收入接近门槛时必须回去读原文、走授权流程。① 一手 LICENSE 文件
二、24GB 上能跑,但要说清跑到哪儿。Q4_K_M 权重 17.11 GB,要处理图片还得再加 0.93 GB 的视觉塔;上下文实际到约 8–10 万 token(fp16 KV),开 fp8 KV 大约到 17–20 万;原生 262K 在 24GB 上跑不满。做原型完全够,但别拿这个上下文上限去承诺产品能力。【本站推导,含 1.0–2.0 GiB 的经验区间】
三、他看到的那些「尺寸」不是尺寸。BF16 / Q4_K_M / IQ4_XS 是同一套 27.36 B 参数的不同存法,参数一个没少,变的是每个参数占多少比特。选档是在「体积和速度」与「精度损失」之间取舍,不是在挑一个更小的模型。【定义 + ③ 第三方实测的文件体积
四、那个「分很高」,先问三句。谁测的(模型卡上的是厂商自述)、哪个精度的权重、哪一档 reasoning_effort。尤其最后一项:统一权重加三档调节,不写档位的分数没法比,也没法预算成本——档位越高,生成的思考过程越长,钱和延迟都跟着涨。【② 官方声明
五、他看到的「某某公司在用」,找原始出处和日期。这类说法在转述里最容易丢时间标签,一句一年前的话被排进新发布的语境会读成新采用。核不到就只当线索。【④ 二手转述
代价最大的是第一件——许可证。理由不是它最容易错,恰恰相反,它是五件里最容易核的(打开 LICENSE 文件,几分钟)。它代价最大是因为其他四件搞错了都能事后调:上下文不够就换 fp8 或加卡,量化档选错了就换一档,分数看走眼了就重测,引用没日期就补上。只有许可证是「等你成功了才爆」的那一类——融资尽调、被收购、和大客户签合同的时候才被核,而那时候产品已经上线,改起来的成本不由他一个人承担。一件几分钟能核清、错了却要在最不方便的时刻付账的事,就是优先级最高的那件。

变式:把他的条件改成「公司去年收入 8000 万美元,主营业务就是把模型打包成 API 卖给别人」,你的五件事里哪几件要重写?再改成「他只是想在自己电脑上试试」,又有几件可以直接删掉?(这两个变式合起来说明一件事:同一份材料,对不同的人有效的部分完全不同。

自己推一遍:不看任何评测,光用 config 和文件列表推翻一句话

这条推导演示的是这一章的完整方法:把一句听起来合理的话,拆成几个各自可核的数,然后逐个核。被推翻的那句话是——「27B 量化之后,24GB 单卡可以流畅运行满血 262K 上下文」。每一步先自己想。

  1. 这句话里有几个可以独立核对的量?先别算,先数。

    想好了再看

    四个:权重占多少(量化档决定)、视觉塔占多少(多模态模型的额外文件)、262K 上下文的 KV 要多少(第 5 章的公式)、剩下多少给框架自己用。前三个都有一手来源,第四个没有——这个「第四个没有」是整条推导里唯一的软肋,记住它,最后要用。

  2. 先算权重。Q4_K_M 是 17.11 GB,但显卡的 24GB 是 24 GiB。这两个单位能直接相减吗?

    想好了再看

    不能。1 GiB = 230 字节 ≈ 1.074 GB,两者差 7.4%。必须先统一。17.11 GB ÷ 1.074 ≈ 15.93 GiB;视觉塔 0.93 GB ≈ 0.87 GiB。两者合计 16.80 GiB
    这一步是最容易被跳过的一步,而跳过它的代价不小:直接拿 24 减 18.04,你会以为还剩 5.96,比真实值多出将近 1 GiB——足够多骗出一万多 token 的上下文。

  3. 还有一样东西也要占显存,前面章节讲过,但几乎所有显存计算器都不算它。是什么?

    想好了再看

    Gated DeltaNet 的固定状态。48 个线性层,每层一份,每层约 3 MiB,全模型约 144 MiB ≈ 0.14 GiB。它小到几乎不影响结论,但它有一个极重要的性质:它不随上下文长度变。1 个 token 和 100 万个 token,它都是这么大。
    顺便记住这个对照,它比任何解释都有说服力:262K 上下文下,16 层全注意力的 KV 是 16 GiB;48 层 DeltaNet 的全部记忆合起来是 144 MiB。

  4. 现在把预算列出来。剩多少给 KV?

    想好了再看

    把上面的量代进去:

    BKV = CcardWq4WvisSdeltaOfw
    式 18-1
    符号是什么这一次的值
    BKV能留给 KV cache 的显存待求
    Ccard显卡容量24 GiB(标称 24GB 就是 24 GiB)
    Wq4Q4_K_M 权重17.11 GB = 15.93 GiB(③ 第三方实测)
    Wvis视觉塔 mmproj 文件0.93 GB = 0.87 GiB(③ 第三方实测)
    Sdelta48 层 DeltaNet 的固定状态0.14 GiB本站推导,按论文定义算)
    Ofw框架与激活开销1.0–2.0 GiB经验区间,本站未实测

    代进去:24 − 15.93 − 0.87 − 0.14 − (1.0 ~ 2.0) = 5.1 ~ 6.1 GiB
    注意结果必须是区间,因为输入里有一项是区间。把区间的输入算成单点的输出,是这一类估算里最常见的自欺。

  5. 262K 上下文需要多少 KV?和上一步的结果比一比。

    想好了再看

    第 5 章的公式:每 token KV = 2 × 16 × 4 × 256 = 32,768 个元素,fp16 下 64 KiB/token。262,144 个 token × 64 KiB = 16.00 GiB
    而你手上只有 5.1–6.1 GiB差了将近三倍。那句话的后半截当场倒掉。

  6. 那么真实能吃多长?顺便说清你这个答案的证据等级。

    想好了再看

    fp16 KV:5.1–6.1 GiB ÷ 64 KiB = 约 8.3 万 – 9.9 万 token。开 fp8 KV(每 token 32 KiB)大约翻倍到 约 16.6 万 – 19.9 万
    所以那句话的正确版本是:「24GB 单卡跑 Q4_K_M 的 27B 是可行的,上下文实际到约 8–10 万 token;开 fp8 KV 大约到 17–20 万。原生 262K 在 24GB 上跑不满。」
    证据等级:这条结论属于本站推导。它的输入里,两个权重体积是 ③ 档第三方实测,KV 公式是 ① 档可复算,DeltaNet 固定态和框架开销是本站推导(其中后者还是经验区间)。所以你要推翻它,最省力的地方是去实测框架开销——那一项一确定,整个区间就收窄了,其余步骤不用动。这就是把中间步骤全部写出来的价值。

真未解怎么判断一个评测有没有被训练数据污染?

这是表 18-1 里唯一一个「不能回答」的问题,而它不能回答不是因为这一次材料少——它在整个领域里都没有公认答案。最直接的办法是把评测题在训练语料里搜一遍,但几乎所有大模型的训练语料都不公开,这条路对外部人直接堵死。退而求其次的黑盒办法有几种:比较模型对原题和改写题的困惑度差、看它能不能把只给了前半截的题目续写出后半截、在评测集里预埋标记串(canary)事后检查模型认不认得。这些方法都能给出信号,但都有同一个致命的失败模式:它们分不开「这道题在训练集里」和「这道题在互联网上被讨论过很多次」——而后者对任何一道流行的评测题都成立。于是你得到的永远是一个说不清强度的统计信号,不是一个判决。

更麻烦的是激励结构:真正有能力做彻底检查的是模型的发布方自己,而检查出污染对发布方没有好处;而外部人做出来的检查,发布方可以合理地说「你的方法有失败模式」——这句话还确实是对的。所以这件事卡住的地方一半在技术,一半在没有人有动机去做那个吃力不讨好的活。

先做这一步:挑一道你自己能改写的评测题,做一组四行的对照——原题 / 换数字但保结构 / 换叙述但保数字 / 数字和叙述都换。四种各跑 20 次,其余全部固定(同一档 reasoning_effort、同一个温度、同一份权重),把正确率记成四行一张表。如果只有「原题」那一行明显偏高,你就拿到了一个(弱的)污染信号;如果四行差不多,你拿到的是一个(同样弱的)反面信号。
做完最要紧的一步是给你的结论定性:把它写成「一个值得继续查的方向」,而不是「这道题被污染了」。能忍住不把信号写成结论,这一章就算学到手了。

这一层要加什么:给这台机器配一份出厂检验报告——证据台账

为什么放在最后一层:前面十七层加的都是零件——嵌入、注意力、KV cache、DeltaNet、FFN、MoE、视觉塔、量化。第 18 层不加零件,它加的是每个零件的合格证:这台机器上写着的每一个数字,各自是从哪儿来的、属于哪一档证据。一台没有合格证的机器也能转,但你没法向任何人(包括半年后的自己)解释它为什么该被信。

KINDS = ("可复算", "官方声明", "第三方实测", "本站推导") LEDGER = {} # 台账:名字 -> (值, 档位, 出处) def cite(name, value, kind, where): assert kind in KINDS, "档位只有这四种:" + str(KINDS) assert where and where.strip(), "没有出处的数字不许进台账:" + name LEDGER[name] = (value, kind, where) return value HIDDEN = cite("hidden_size", 5120, "可复算", "cfg27b.json: text_config.hidden_size") L_FULL = cite("全注意力层数", 64 // 4, "可复算", "cfg27b.json: num_hidden_layers / full_attention_interval") KV_TOK = cite("KV每token字节", 2*16*4*256*2, "可复算", "由上面两个字段 + num_key_value_heads + head_dim 推,第5章") W_Q4 = cite("Q4_K_M体积", 17.11e9, "第三方实测", "该量化仓库文件列表的字节数") CTX_24G = cite("24GiB上下文", (83000, 99000),"本站推导", "第17章:式18-1,含1.0-2.0GiB经验区间") def audit(sample=10): from collections import Counter c = Counter(k for _, k, _ in LEDGER.values()) for k in KINDS: print(k, c[k], "%.0f%%" % (100.0 * c[k] / len(LEDGER))) import random for name in random.sample(list(LEDGER), min(sample, len(LEDGER))): v, k, w = LEDGER[name] print("抽查", name, "=", v, "|", k, "|", w)

难点一:你会舍不得删。真动手做这件事,一定会撞上几个数字——它们看起来很有说服力,但你追到底只能追到「某篇文章说」。这时候唯一诚实的动作是删掉它,或者把它降级成「未核实」单独放一栏,而不是给它编一个体面的出处。这一层的难,全部难在这里:写台账是十分钟的事,删掉自己喜欢的那个数字是十分钟之后的事。

难点二:「本站推导」最容易被误登记成「可复算」。两者的判据很清楚——可复算的意思是任何人拿同一份文件,都能算出完全相同的数;只要推导过程里插进了一个经验值、一个区间、或者一句「一般来说」,它就不再可复算,必须单独一档。用这把尺子回头量一量:KV每token字节 是可复算的(全部输入都在 config 里),24GiB上下文 不是(里面有 1.0–2.0 GiB 那一项)。把后者混进前者,就等于把一个区间伪装成了一个常数。

难点三:出处要指到「文件里的哪个字段」,不能指到「本站第几章」。"第5章说的" 是循环论证——第 5 章也是本站。正确的写法是指到 cfg27b.json 的具体字段名,第几章只能作为「推导过程在哪里」的补充说明写在后面。

自己验:四条,每一条都有具体的可观察结果。
从你写过的正文里随机抽 10 个数字登记进台账,跑 audit():它应该打印出四档的条数与占比,且不抛异常。抽到的 10 个里只要有一个你只能追到「某篇文章说」,这一层就不合格——回去把它补到一手来源,或者从正文里删掉。
故意登记一条没有出处的:cite("某某排名", 3, "第三方实测", "")。它必须立刻抛 AssertionError 并把 某某排名 打出来。如果它安静地通过了,说明你的检查形同虚设,回去看 cite 的第二个 assert
故意登记一条档位写错的:cite("显存开销", 1.5, "实测", "凭印象")。第一个 assert 应该先炸,报「档位只有这四种」。两个 assert 的触发顺序也要对——先查档位再查出处。
数一数你台账里「本站推导」那一档的占比。如果超过三成,多半不是你推导得多,是有本该可复算的东西被你偷懒估了——按难点二那把尺子逐条重量一遍,能算的都去算。

本章小结

  • 七问:谁测的、对照组是谁、哪天的快照、哪个精度、什么推理配置、这个评测在测什么、有没有污染。这一次能回答的是第 ①⑤ 问(厂商自述;reasoning_effort 三档),完全回答不了的是第 ⑦ 问。
  • 核对入口:参数量看 config.json,许可证看仓库里的 LICENSE 文件,文件体积看仓库文件列表,显存需求看推理框架 recipe(并看清是哪代卡),排名看榜单官方页面并记日期,「某公司在用」找原始出处并看是什么时候说的。原则只有一条:一手来源的层级越高,你需要的信任就越少。
  • 许可证(本章最有实用价值的一节)27B 是 Apache 2.0,真开源、免费商用、改了也不用开源;2.4T-A95B 是 qwen3.8-max 自定义协议,两条门槛——① MaaS/AI 助手类业务且连续 12 个月收入超 5000 万美元须另行授权,② 用于月活超 1 亿的产品须在界面显著位置展示模型名称。用词上,前者可以说「开源」,后者严格说是「开放权重」。同一次发布里的两个模型可以是两种协议。
  • 五样没有的东西:没有 arXiv 技术报告、没有官方 GitHub 仓库、没有小尺寸、没有 Instruct/Thinking/Base 变体、没有消融实验。最后一条意味着:本站所有「为什么这么设计」的解释都是推断
  • 但 config.json 还在:它不是写给你看的,是加载器要用的,所以没有修辞空间;而且可复算——第 12 章数出的 27.36 B 与 2.420 T / 95.29 B 对上了官方名字。没有论文,你失去的是「为什么」,不是「是什么」。
  • 时效性:结构性信息(层数、体积、协议)保质期长,状态性信息(谁在用、排第几、支持到哪)随时会变。引用任何「某某在用」或「排第几」都要带日期;连「建仓日」和「权重上线日」都能差好几天,引用时要写清是哪一个。本站的检索日期是 2026-08-16,写在页脚。
  • 本站已知的五处不确定:框架开销是经验区间不是实测;DeltaNet 144 MiB 是按论文定义推的、未见官方印证;各种设计理由都是推断;混合比例实验室是玩具模拟;量化掉多少能力的实证都不是在这个模型上做的。加上一个口径坑:GB 与 GiB 差 7.4%。

下一章不再加零件。十八级台阶已经把机器造完了,最后一章做三件事:把十八级逐一验收、用六道跨五章以上的综合题闯一次关、然后把那些连本站也答不出来的问题诚实地交给你

第19章 终极闯关:跨章综合 + 总答辩 + 开放课题

这一章不加零件了。机器已经造完——十八级台阶,从一个只会把输入原样吐回去的空壳,到一台参数量能和官方名字对上、显存账能算到 token、每个数字都有出处的机器。这一章做四件事:验收、闯关、答辩、送你出门。

学完这一章你应该能做到

  • 对照十八级台阶,逐级说出自己手上那台机器每一层能不能自验
  • 拿到一份陌生的 config.json,独立算出总参数、激活参数、KV cache 与量化后体积,并判断它能不能塞进给定的卡
  • 反过来做:给定显存和上下文要求,倒推出一组可行的架构参数,并说清这组参数欠了什么债
  • 给出全站核心论断的完整答案,并逐句标出它的证据等级
  • 诚实区分「这件事领域里还没有答案」和「这件事只是我还没查」
前置:第 0 到第 18 章的全部内容。这一章的每一道题都横跨三章以上,没有新知识点,只有旧知识点的组合。做不出来时,往回翻是正确动作,不是失败。

19.1 十八级台阶回顾:你到底造了什么

先看一眼你走过的路。下面这张表把十八级台阶列在一起——第二列是那一层往机器上加了什么,第三列是那一层验的是什么。第三列才是重点:一台机器和一堆零件的区别,就在于每一层都有一条你自己能跑、能看到结果的判据。

表 19-1:十八级建造台阶(每一级的完整判据在对应章节的建造台阶里,这里只给一句话提要)
台阶这一层加了什么它验的是什么
1一个只会原样吐回去的空壳管线通了:给一段输入,能原样走完全程出来
2嵌入层与输出头两张 248,320 × 5,120 的表,各 1.27 B 参数,数得出来
3第一个单头注意力Q、K、V 三张矩阵接得上;softmax 每一行加起来是 1
4升级成 Gated Attention(24 : 4、head_dim 256、输出门、部分 RoPE)q_proj 的输出维因为输出门而翻倍;256 维里只有 64 维参与旋转
5KV cache加缓存之后逐 token 生成的结果,和一次性前向的结果一致——只是快得多
6一个 Gated DeltaNet 层状态大小不随序列长度变:读 1 个 token 和读 10 万个,它一样大
73 : 1 混合布局,(3 线性 + 1 全) × 1664 层里正好 16 层有 KV cache,48 层没有
8每层挂上 SwiGLU FFN单层 267,386,880;64 层 17,112,760,320,一个不差
9把 FFN 切成 512 份专家,每 token 选 10 + 1总参数和激活参数是两本账,分开记,都能算对
10总参数与激活参数分离换一个专家数或激活数,显存那一栏和速度那一栏往不同方向动
11视觉塔与多模态位置编码多出来的 0.46 B 参数,以及部署时那个必须额外加载的 0.93 GB 文件
12逐矩阵点钞,把整台机器数一遍27.36 B2.420 T / 95.29 B——和官方名字「27B」「2.4T-A95B」对上
13把尺寸选择接到训练与推理预算上改一次请求量,最优点往「更小更久训」还是「更大更短训」偏
14独立预训练 / 剪枝 / 蒸馏三条路线分开能只用 config 字段把「27B 是从 2.4T 剪出来的」这个假说证伪
15位宽、缩放因子、分组量化量化前后参数个数一个不变,变的只有每个参数占多少比特
16各家量化格式(GPTQ / AWQ / GGUF / FP8 / NVFP4)同一套权重在不同格式下的文件体积,各自对得上
1724GB 卡的真实边界给定量化档与 KV 精度,算出上下文上限,并说清哪一项是估的
18证据台账随机抽 10 个数字,每一个都能追到具体出处;追不到就删

把这张表从上往下读一遍,你会发现一件事:前十二级在造「是什么」,后六级在造「为什么信」。很多人学到第十二级就停了——他们知道这台机器长什么样,但没法回答「你怎么知道」。而这一次恰恰没有技术报告,第十三到十八级那部分能力才是真正值钱的东西。

我能不看材料说清:这十八级里,哪一级的判据是「两个数必须完全相等」,哪一级的判据是「某个量必须不变」,哪一级的判据只能是「区间」。

你把台阶 5(KV cache)加完了。为了自验,你把缓存整个关掉重跑同一段输入。结果发现:前 3 个 token 的输出完全一致,从第 4 个开始对不上,而且越往后差得越远。请说出最可能的两个错因,并说明为什么错误偏偏从第 4 个 token 开始暴露。

先想:开缓存和不开缓存,唯一该有的差别是速度,数值上必须一致。既然前 3 个一致,说明缓存的写入本身没错——错的是读的时候拿到的东西不对。
关键在于「第 4 个」这个位置。回想台阶 7:这台机器不是每一层都一样的,它是四层一组、组里第 4 层才是全注意力。而位置编码也和位置序号绑着。这两条线索各指向一个不同的错因。
第一步这样走:先把「层的编号」和「token 的序号」这两个 4 分开——它们是两件事,你要判断暴露点跟的是哪一个。打印出每一层写进缓存的张量形状,看第 4 层和前 3 层是不是一样的。
完整答案:两个最可能的错因。
其一,位置编码的偏移算错了。用缓存生成时,新 token 的位置序号必须接着已缓存的长度往后数;如果你每次都从 0 开始算旋转角度,那么第 1 个 token 恰好对(0 就是 0),后面全错,且越往后偏得越多——这解释了「越往后差得越远」
其二,把 KV cache 挂到了不该挂的层上。这台机器 64 层里只有 16 层是全注意力,另外 48 层是 Gated DeltaNet——后者的记忆是一个固定大小的循环状态,不是 KV cache,两者的更新方式完全不同。如果你按传统架构的写法给每一层都挂了 KV cache,那么在四层一组的布局里,问题会在走到组内那一层全注意力时集中暴露。
为什么「第 4 个」是个好线索:它同时命中了两条线。要分辨是哪一条,做一个小实验——把输入换成只有 2 个 token 反复喂,如果仍然在第二次出现偏差,那是位置编码;如果只有跨过全注意力层时才出错,那是层类型判断写错了。
顺带记住台阶 5 那条判据的形式:它要求的是「两条路径的结果一致」,不是「结果看起来合理」。后者你永远验不出来。

变式:如果关掉缓存之后结果完全一致,但显存占用也几乎没变,这说明什么?(提示:想想缓存到底为什么能省时间——它省的是计算,不是显存;显存反而是它多花的。占用没变,多半是你的缓存根本没被写进去。)

自己推一遍:把这台机器在一张卡上的显存账从零列出来

这是全站最实用的一条推导,也是对十八级台阶的一次总检。目标:不查任何现成结论,只用 config.json 里的字段 + 一份文件列表,写出一张完整的显存预算表。每一步先自己想。

  1. 一个模型跑起来,显存被哪几类东西吃掉?先分类,别急着填数。

    想好了再看

    三类,而把它们分开是整条推导的关键
    第一类,权重——买断的固定成本。装进去多少就是多少,跟你聊多长没关系。
    第二类,随上下文长度增长的——按量计费。这里只有一样东西:全注意力层的 KV cache。
    第三类,不随长度增长的运行时占用——包括 Gated DeltaNet 的循环状态,以及框架自己的临时张量与激活。
    网上的显存计算器大多只算第一类和第二类,而且第二类还算错——这是整个第 5、17 章的由来。

  2. 第一类要几个数?其中哪一个 config.json 里查不到?

    想好了再看

    两个:参数个数每个参数占多少字节。前者可以从 config 逐矩阵数出来(台阶 12),后者查不到——它由你选的量化档决定,属于训练之后的事。
    这正是全站那条主线的一次现身:参数个数写在 config 里(训练之前定的),每参数字节数写在文件名里(训练之后选的)。两个数相乘才是权重体积,而它们来自时间轴上完全不同的两端。
    顺带记一个可以反过来用的实测比值:27.36 B 参数的 Q4_K_M 文件是 17.11 GB,除下来约 0.625 字节/参数(等效约 5 比特,因为 K-quant 会把一部分张量存得更精细)。以后估别的模型的 Q4 体积,用这个比值比用「4 比特 = 0.5 字节」准。

  3. 第二类要四个字段。哪四个?其中最容易填错的是哪一个?

    想好了再看

    num_hidden_layersfull_attention_intervalnum_key_value_headshead_dim
    最容易填错的是第一个——公式里要的是全注意力层数,不是总层数。必须先用前两个字段相除得到 64 ÷ 4 = 16,再往公式里填。填成 64 的后果是 KV cache 高估整整 4 倍,而这个错在结果里长得完全合理,不会报错、不会崩,只会让你以为需要买四倍的显卡。

  4. 第三类里那个「不随长度增长」的状态有多大?为什么它在这条推导里几乎可以忽略,却又必须写出来?

    想好了再看

    48 个 DeltaNet 层,每层一份状态,每层约 3 MiB,合计约 144 MiB ≈ 0.14 GiB。在 24 GiB 的预算里它确实小到几乎不影响结论。
    但它必须写出来,有两个理由。一是诚实:一张预算表漏项就是漏项,哪怕漏的那项小。二是它承载了整章最有说服力的一句对照:262K 上下文下,16 层全注意力的 KV cache 是 16 GiB;48 层 DeltaNet 的全部记忆合起来是 144 MiB,而且 1 个 token 和 100 万个 token 一样大。这一句话就是 3 : 1 混合布局存在的全部理由。

  5. 现在把式子写出来,并给每一项标上证据等级。

    想好了再看

    能装下的上下文长度,等于「留给 KV 的显存」除以「每 token 的 KV 字节数」:

    Lmax = CcardN·bSdeltaOfw2 · Lfull · Hkv · dhead · bkv
    式 19-1
    符号是什么证据等级
    Ccard显卡容量(注意标称 24GB 就是 24 GiB硬事实
    N · b参数个数 × 每参数字节数 = 权重体积N① 可复算b 由量化档定,取实测文件大小则是 ③ 第三方实测
    SdeltaDeltaNet 循环状态总量(不随长度变)本站推导(按论文定义算,未见官方印证)
    Ofw框架与激活开销经验区间 1.0–2.0 GiB,未实测——整条推导唯一的软肋
    2 · Lfull · Hkv · dhead每 token 的 KV 元素数(27B:2×16×4×256 = 32,768)① 可复算
    bkvKV 每个元素的字节数(fp16 是 2,fp8 是 1)由你的启动参数决定

    这张表的价值不在结论,在于它把不确定性锁在了一格里。整个式子里只有 Ofw 一项是估的,所以结论必然是区间;而任何人想推翻这条推导,只需要实测那一项,其余五项不用碰。把不确定性关进一个格子,是这十九章教给你的最后一个动作。

19.2 跨章综合题:六道闯关

下面六道题,每一道都横跨三章以上,有几道横跨五章。它们没有新知识——所有需要的东西你都学过了,难的是把它们按正确的顺序接起来。建议不看提示先做,做不出来再一级一级往下翻。

第一关:读一份陌生的 config。有人给你下面这份配置(是虚构的,别去搜):
num_hidden_layers: 42 hidden_size: 6144 full_attention_interval: 3
num_attention_heads: 32 num_key_value_heads: 4 head_dim: 128
num_experts: 256 num_experts_per_tok: 8(另有 1 个共享专家) moe_intermediate_size: 1536
vocab_size: 200000 tie_word_embeddings: false
已替你算好:非 MoE 部分(注意力 + 线性注意力 + 嵌入 + 输出头)合计 5.73 B。
请回答五问:① 全注意力层有几层?② 总参数是多少?③ 激活参数是多少?④ fp16 KV 每 token 多少字节,128K 上下文要多少 GiB?⑤ 按 Q4 量化,它能塞进一张 24GB 卡吗?差多少?

先想:五问的依赖关系。① 是后面全部的前提;② 和 ③ 用的是同一批数字但两本账;④ 只用 ① 加三个字段;⑤ 靠 ② 而不是 ③。把这个依赖图先画出来,再动笔。
关键在于 MoE 那一层要算两次:一次把 256 个专家全算上(那是总参数,决定显存),一次只算被激活的 8 + 1 个(那是激活参数,决定速度)。共享专家每次都参与,路由门那张小矩阵两本账都要算。
第一步这样走:先算一个专家。SwiGLU 是三张矩阵,所以单个专家 = 3 × 6144 × 1536 = 28,311,552。这个数是后面一切的基石,先把它算对再往下走。然后算一层:256 个路由专家 + 1 个共享专家 + 一张 6144 × 256 的路由门。
完整答案:
42 ÷ 3 = 14 层全注意力,其余 28 层是线性注意力。
单个专家 3 × 6144 × 1536 = 28,311,552。每个 MoE 层 = 256 × 28,311,552(路由专家)+ 28,311,552(共享)+ 6144 × 256(路由门)= 7,277,641,728。42 层 = 305.66 B。加上给定的非 MoE 部分 5.73 B,总参数 ≈ 311.39 B
每 MoE 层激活 = (8 + 1) × 28,311,552 + 1,572,864 = 256,376,832,42 层 = 10.77 B。加 5.73 B,激活参数 ≈ 16.50 B。稀疏度:16.50 / 311.39 ≈ 5.3%,每 token 只用了约十九分之一的参数。
每 token KV = 2 × 14 × 4 × 128 = 14,336 个元素,fp16 下 = 28,672 字节 = 28 KiB/token。128K(131,072 token)= 3.5 GiB
⑤ 塞不进,差得非常远。按 4 比特的理论下限算,权重 = 311.39 B × 0.5 字节 ≈ 155.7 GB;按 Q4_K_M 实测的 0.625 字节/参数算 ≈ 194.7 GB ≈ 181 GiB。一张 24 GiB 的卡装不下,要 8 张左右才够放权重,还没算框架开销。
这一关真正要你看到的是最后一句:这个模型的 KV cache 只有 3.5 GiB,激活参数只有 16.5 B(算起来跟一个 16 B 的稠密模型差不多快),但它依然需要八张卡——因为显存看的是总参数。「2.4T 决定你要买几张卡,95B 决定你等多久出第一个 token」这句话在这里被完整复现了一遍。

变式:把 num_experts 从 256 改成 64,其余不动。② ③ ④ ⑤ 四问的答案分别怎么变?其中有一问完全不变、还有一问几乎不变——是哪两问,为什么?(提示:一个量只跟注意力那一侧的字段有关,另一个量里只有那张小小的路由门跟着缩了。而总参数会从 311 B 掉到约 83 B——四问里动得最厉害的和几乎不动的,正好就是 MoE 这个设计的全部意义。

第二关:给一张 32GB 卡配方案。你要在一张 32 GiB 的卡上跑 Qwen3.8-27B,做多模态任务(要处理图片),上下文要到 128K。请给出完整方案:选哪个量化档、KV 用什么精度、总显存怎么分配、还剩多少余量。每一步都要说出你用的数字来自哪儿。

先想:这是式 19-1 的反向用法——上下文长度已知,要求的是「哪个量化档放得下」。先把已知的三项(KV、DeltaNet 状态、框架开销)算出来,剩下的才是权重预算。
关键在于「要处理图片」这五个字。多模态意味着视觉塔那 0.93 GB 必须额外加载,它不在主权重文件里。这一项几乎是所有人算漏的一项,而漏了它你的方案会正好卡在边界上。
第一步这样走:先算 128K 的 KV。每 token 64 KiB(fp16),131,072 × 64 KiB = 8.00 GiB。再把 DeltaNet 状态 0.14 GiB 和框架开销 1.0–2.0 GiB 放进预算。三项加起来大约 9.1–10.1 GiB,那么权重加视觉塔最多能占 约 22 GiB。剩下的就是挑量化档了。
完整答案:
预算:32 GiB − 8.00(fp16 KV @128K,① 可复算)− 0.14(DeltaNet 固定态,本站推导)− 1.0~2.0(框架开销,经验区间)= 21.9–22.9 GiB 留给权重和视觉塔。
视觉塔先扣掉:0.93 GB = 0.87 GiB(③ 第三方实测)。剩 21.0–22.0 GiB 给主权重。
挑档(体积都是 ③ 第三方实测,注意单位换算 1 GiB ≈ 1.074 GB):
· Q4_K_M 17.11 GB = 15.93 GiB → 占用合计约 25.9–26.9 GiB,余量 5–6 GiB,非常宽松。
· Q5_K_M 19.83 GB = 18.47 GiB → 合计约 28.5–29.5 GiB,余量 2.5–3.5 GiB,这是本站推荐的档:装得下,还留了缓冲。
· Q6_K 22.88 GB = 21.31 GiB → 合计约 31.3–32.3 GiB压线甚至超,不建议。
方案Q5_K_M 主权重 + 0.93 GB 视觉塔 + fp16 KV,128K 上下文。如果你想更保险或想把上下文再往上推,把 KV 换成 fp8:KV 从 8.00 GiB 降到 4.00 GiB,同样的档位下余量变成 6.5–7.5 GiB,或者上下文可以推到约 256K。
三个必须一起说出来的限定:其一,框架开销 1.0–2.0 GiB 是经验区间不是实测,真实部署还有分页与碎片,实际值更靠近上沿;其二,图片会吃上下文——每张图会被切成若干个 token 占进那 128K 里,你的 128K 不全是文字;其三,fp8 KV 让上下文翻倍这件事的字节账是硬的,但「翻倍之后能力不掉」在这个 3 : 1 混合布局上没有公开验证过,属于第 18 章清点过的已知不确定。

变式:换成一张 16 GiB 的卡,其余要求不变,还有解吗?如果没有,你会先牺牲哪一项——量化档、上下文长度,还是多模态能力?说出你的取舍理由,以及每一种牺牲对应损失了什么。

第三关:三份权重清单,判断血统。有人给你三个模型的权重形状清单(只给形状,不给名字):
:64 层;hidden 5120;q_proj [5120 → 12288]k_proj [5120 → 1024]o_proj [6144 → 5120];每层三张 FFN 矩阵 [5120 → 17408]×2 与 [17408 → 5120];另有一个 27 层、hidden 1152 的视觉塔;全部张量 dtype 为 bfloat16;文件合计约 54.7 GB。
:层数、hidden、每一个张量的形状都和甲完全相同;但权重张量的 dtype 是 4 比特整数,且每个权重张量旁边多出一个形状为「原形状最后一维 ÷ 32」的缩放因子张量;文件合计约 17.1 GB。
:92 层;hidden 8192;q_proj [8192 → 32768]k_proj [8192 → 1024];每层有 512 组 [8192 → 2048] 的专家矩阵;没有视觉塔;dtype 为 bfloat16。
问:这三者两两之间是什么关系?给出你的判据,并说明哪一条判据是决定性的。

先想:第 14、15 章讲过四件不同的事——独立预训练、剪枝、蒸馏、量化。这四件事里,哪一件不改变任何张量的形状?找到它,甲乙关系就定了。
关键在于乙那个「多出来的缩放因子张量」。第 15 章讲过:低比特存不下原来的动态范围,所以要按组共享一个缩放因子。「每 32 个数共享一个 scale」是分组量化的签名,见到它基本可以确定这是一份量化权重。
第一步这样走:先做甲和丙的比对,逐项列差异——层数、hidden、有没有视觉塔、稠密还是 MoE。列完之后问自己:这些差异里,有哪一条是「把甲剪一剪就能得到丙」或者反过来能做到的?
完整答案:
甲与乙:同一套权重的两种存法,乙是甲的量化版。判据有三条,其中第一条是决定性的——每一个张量的形状完全相同。量化只改「每个数用多少比特存」,不改「有多少个数」,所以形状必然一字不差;第二条是那些缩放因子张量(分组量化的签名,这里是每 32 个数一组);第三条是体积比 54.7 : 17.1 ≈ 3.2 : 1,与 16 比特降到约 5 比特等效位宽吻合。
甲与丙:两个独立预训练的模型,谁也不是从谁那儿来的。判据也是三条,而决定性的那条是视觉塔:甲有一个完整的 27 层视觉塔,丙一个都没有。剪枝只能让东西变少,不可能从一个没有视觉塔的模型里剪出一个有视觉塔的模型——所以「甲是丙剪出来的」当场死掉。反方向也不成立:从 512 个专家「剪」成一张稠密 FFN,那不叫剪枝,那是换图纸;而且 hidden 从 8192 变成 5120 会动到全模型每一张矩阵,等于重训。
乙与丙:同理,乙就是甲,所以乙和丙也是两个独立的模型,只不过乙还是量化过的。
顺便验一下你有没有真读懂形状:甲的 q_proj 输出维 12288 = 24 个头 × 256 head_dim × 2,那个 2 来自输出门(attn_output_gate,第 4 章);k_proj 输出 1024 = 4 个 KV 头 × 256,24 : 4 就是分组查询注意力的比例。丙的 32768 = 64 × 256 × 2,同一个模板换了个头数。甲和丙不是父子,是同一张图纸的两次施工。

变式:再给你一个「丁」——层数 48、hidden 5120、张量形状和甲的前 48 层一模一样、没有视觉塔、dtype bfloat16。丁最可能是怎么来的?这一次剪枝假说能被排除吗?如果不能,你还需要什么额外证据才能判断?

第四关:找出五处问题。下面这段话读起来很像一篇正经的技术报道,但埋了五处问题,性质各不相同。逐条找出来、说清是哪一类问题、并给出改法:
「这次开源的两个模型均以 Apache 2.0 协议发布,开发者可自由商用。第三方评测显示其在多项任务上位居前列。27B 提供 BF16 与 Q4_K_M 两种尺寸,后者参数量约为前者的四分之一,因此更适合消费级显卡。按 64 层估算,262K 满上下文的 KV cache 约需 64 GiB,故长上下文场景需多卡部署。多家知名企业已表示正在使用该系列模型。」

先想:这段话跨了第 5、15、18 三章。第一句用第 18 章的许可证一节核,第二句用七问核,第三句用第 15 章核,第四句用第 5 章的公式核,最后一句用时效性一节核。一句一句判,不要整段判。
关键在于这五处的性质不同:一处是事实错误、一处是证据档位错配、一处是概念混淆、一处是算术错误、一处是缺限定。先给每处贴类型标签,再改——因为不同类型的改法不一样:事实错误要换掉,档位错配要补来源,概念混淆要拆开,算术错误要重算,缺限定要补日期和出处。
第一步这样走:把「四分之一」和「64 GiB」两个数字圈出来先算。第一个问自己「量化改的是参数个数还是每参数位宽」,第二个问自己「公式里那个层数该填 64 还是 16」。这两处一算完,整段的可信度就定性了。
完整答案:
其一(事实错误,最严重):「两个模型均以 Apache 2.0 发布」不成立。27B 是 Apache 2.02.4T-A95B 用的是 qwen3.8-max 自定义协议,带两条门槛——① 经营 MaaS / AI 助手类业务且连续 12 个月收入超 5000 万美元须另行获取授权,② 用于月活超 1 亿的产品须在界面显著位置展示模型名称。改法:两个模型分开写,后者称「开放权重」而不是「开源」。这一处直接影响读者的商用合规判断。
其二(证据档位错配):模型卡上的评测表是厂商自述(② 档),不是第三方复现(③ 档)。而且「多项任务上位居前列」还漏了精度与推理档位——这个模型是统一权重reasoning_effort 的 xhigh / medium / low 三档分数不是一回事。改法:写明来源是官方模型卡自述,补上精度与档位;核不到就不写。
其三(概念混淆):量化档不是尺寸。BF16 和 Q4_K_M 是同一套权重的两种存法,参数个数一个没少,都是 27.36 B;变的是每个参数占多少比特,所以缩小的是文件体积(54.66 GB → 17.11 GB,约三分之一)不是参数量。改法:把「尺寸」改成「量化档」,把「参数量四分之一」改成「文件体积约三分之一」。
其四(算术错误):公式里那个层数要填全注意力层数。64 层里只有 16 层是全注意力,另外 48 层是 Gated DeltaNet,其状态固定大小、不随长度增长。每 token KV = 2 × 16 × 4 × 256 = 32,768 个元素 → fp16 下 64 KiB/token → 262K 是 16.00 GiB,不是 64 GiB。高估整整 4 倍,并且这个高估把结论带成了错的(16 GiB 的 KV 加上量化权重,单卡是有解的)。
其五(缺限定):「多家知名企业已表示正在使用」没有出处、没有日期、没说是哪一代模型。这类陈述里混着更早年份的旧闻是常态,被排进新发布的语境里就会被读成「因为新模型才发生的新采用」。改法:给出「谁、什么时候、在哪儿说的、原话是什么」,否则删掉。
总评:五处问题没有一处是恶意的,它们来自五种不同的疏忽。这就是为什么第 18 章教的是七个问题和一份核对清单,而不是一份「哪些说法是假的」的名单——名单会过期,方法不会。

变式:把这段话改写成你愿意签名的版本,并给每一句标上证据档位(① 可复算 / ② 官方声明 / ③ 第三方实测 / 本站推导)。你会发现有一两句无论怎么改都标不上档位——那几句就该删。数一数你的版本比原文长了多少,那就是「说准确」的价格。

第五关:反过来做。现在你是设计者。目标:一个模型要能在单张 16 GiB 消费级显卡上跑满 256K(262,144 token)上下文,走 Q4 量化。请给出一组可行的架构参数(至少要定:总层数、full_attention_intervalnum_key_value_headshead_dim、KV 精度、以及这个模型大概能有多少参数),并说清这组选择欠下了什么债

先想:这是式 19-1 的第三种用法——把等号左边固定成 262,144,反解右边。先决定给 KV 留多少,再由它反推分母里那几个字段该多大,最后剩下的钱才是权重的。
关键在于分母:每 token KV 字节数 = 2 × Lfull × Hkv × dhead × bkv。你有四个旋钮可以拧,而其中最有杠杆的是 Lfull——因为它由 full_attention_interval 直接决定,把 interval 从 4 调到 8,KV 立刻减半,而总层数不用动。
第一步这样走:先给 KV 定一个预算,比如 4 GiB。4 GiB ÷ 262,144 token = 16 KiB/token。若用 fp8(每元素 1 字节),那就要求 2 × Lfull × Hkv × dhead = 16,384,也就是 Lfull × Hkv × dhead = 8,192。现在去找一组好看的解。
完整答案(这是一组解,不是唯一解):
KV 侧:取 Lfull = 8Hkv = 4dhead = 256,乘积正好 8,192。配 64 层总层数,则 full_attention_interval = 8(8 层全注意力 + 56 层 Gated DeltaNet)。KV 用 fp8 → 每 token 16 KiB → 262,144 token = 4.0 GiB
剩下的预算:56 层 DeltaNet 的固定状态约 56 × 3 MiB ≈ 0.16 GiB;框架开销取 1.5 GiB(经验区间中值)。于是权重可用 = 16 − 4.0 − 0.16 − 1.5 ≈ 10.3 GiB ≈ 11.1 GB
参数量:用 27B 那份实测比值反推——Q4_K_M 约 0.625 字节/参数,11.1e9 ÷ 0.625 ≈ 17.8 B。所以这是一个约 17–19 B 的稠密模型
欠的债(这一问才是重点)
· 债一,精确回忆能力。只有 8 层全注意力,比 3 : 1 布局的 16 层少了一半。第 6、7 章讲过:DeltaNet 把历史压进固定大小的状态,压缩必然丢信息;能在任意位置精确取回一个具体细节的,只有全注意力层。砍到 8 层,长文档里「找那一句只出现过一次的话」这类任务大概率会变弱。而且这笔债你事先算不出来——没有公开的规模化消融告诉你 8 层够不够,见 19.4 的第一个开放课题。
· 债二,fp8 KV 的能力损失未知。字节账是硬的,但低比特 KV 在 3 : 1 混合布局上的表现没有公开数据,见 19.4 第二个课题。
· 债三,17–19 B 是个不大的模型。为了塞进 16 GiB,你已经把参数预算压到这个量级;再想加能力只能靠训得更久或数据配方,那是第 13 章的账。
· 债四,这条推导本身带着一个经验区间(框架开销 1.5 GiB)。真实部署更靠近上沿,所以上面这组数应该当成上限而不是承诺

变式:把 KV 精度改回 fp16,其余目标不变,还有解吗?如果要保住 262K,你必须从哪儿再挤出 4 GiB?(提示:四个旋钮都试一遍,看哪个的代价最小、哪个的代价最不可控。)

第六关:一道你应该答不出来的题。有人问你:「Qwen3.8-27B 的 FFN 中间维为什么正好是 17408?为什么不是 16384,也不是 20480?」请给出一个诚实的回答。(提示:诚实的回答不一定是「我不知道」这四个字,但它一定包含这个意思。)

先想:这个问题里的「为什么」指的是什么?是「这个数在数学上有什么性质」,还是「设计者当初怎么想的」?这两个问题的可回答性完全不同。
关键在于第 18 章表 18-5 的第 ⑤ 条:这一次没有消融实验,也没有技术报告。凡是问「为什么选 A 不选 B」的问题,唯一严肃的答案形式是一组对照实验,而这组实验不存在、也没有被任何人公开做过。
第一步这样走:把你说的和不能说的分成两堆。能说的一堆里放描述性事实:17408 是多少乘多少、除以 5120 是多少、和别家比是宽还是窄。不能说的一堆里放全部带「因为」「为了」的句子。分完你就发现,第二堆是空的——你一句都填不进去。
完整答案,三段式:
第一段,我知道的事实(① 可复算)intermediate_size = 17408,写在 config.json 里。17408 = 17 × 1024,是 1024 的整数倍。除以 hidden_size 5120 得扩张比 3.4。作为对照:经典 Transformer 的两矩阵 MLP 用 4 倍扩张,LLaMA 那一路的 SwiGLU 常用 8/3 ≈ 2.67 倍。所以 3.4 落在两者之间、偏宽的一侧。单层 FFN 因此是 3 × 5120 × 17408 = 267,386,880 个参数,64 层合计 17.11 B,占全模型 62.6%
第二段,我不知道原因,以及为什么不知道:这一次没有 arXiv 技术报告、没有官方 GitHub 仓库、没有任何消融实验。没有任何一份公开材料说明过 17408 是怎么选出来的。所以任何「因为 X 所以 17408」的说法,包括本站在第 8 章给的那些,都是推断而不是官方说法。
第三段,要回答它需要什么:需要两样东西之一。一是一组消融——固定其余全部配置与数据,只把 intermediate_size 在 12288 到 24576 之间扫若干个点,各训到同样的步数,把困惑度、下游任务分、训练吞吐、推理显存四条曲线一起报出来;二是设计者自己的说明。两样都没有的情况下,能站得住的最强句式是「3.4 这个扩张比与某某考虑不矛盾」,而不是「3.4 是为了某某」。
还要提醒一个陷阱:这类问题在网上很容易找到听起来很顺的答案(比如「为了对齐硬件的某个块大小」「为了让总参数凑成整数」)。它们可能是对的,但一条都不能被现有材料验证顺不等于真——一个解释让人舒服,通常只说明它自洽,不说明它被证实了。能在这里停住,是这十九章最难的一课。

变式:同一个问题换成「head_dim 为什么是 256 而不是常见的 128?」和「专家为什么是 512 个选 10 个?」——你的三段式回答里,第一段(事实)会变,第二段和第三段会变吗?如果不变,说明什么?

19.3 总答辩:向这个站开炮

下面四条是本站论点里最薄弱的四个环节。它们不是稻草人——每一条本站都认为是真问题,而且其中至少有一条,本站守不住。答辩之前先自己组织语言,再看参考防守。

答辩一:蒸馏这条路

你从第 1 章讲到第 14 章,核心论点是「小模型不是大模型切出来的」。可是 Qwen3 的技术报告自己就承认,几个小模型用了 strong-to-weak 蒸馏,教师是更大的模型。那小模型的能力不就是从大模型来的吗?你这个论点是不是在玩文字游戏?

参考防守(先自己组织语言再看)

这一击有道理,而且本站要先把话说清楚:如果命题是「小模型的能力有一部分来自大模型」,本站没法反驳,也不打算反驳——那句话是对的。

本站反对的是更强的那一句:「小模型是把大模型压缩、切割、剪裁出来的」。区别在于两个词:训练信号权重来源

蒸馏改变的是训练信号:学生不再只跟着「下一个词是什么」的硬标签学,而是跟着教师给出的完整概率分布学,这样每一步能学到的信息更多,所以更省算力(Qwen3 报告说只需要四阶段训练法约十分之一的 GPU 时)。但学生的骨架是独立预训练的另一套参数——层数、hidden_size、参数个数,全都是在训练开始之前定好的,和教师没有对应关系。教师的权重一个字节也没有被搬进学生里。

真正在权重上动刀的是另一条路线:Minitron(arXiv:2407.14679)从一个训好的 15B 里剪出 8B 和 4B,那是真的把权重砍掉再补训。这条路和蒸馏是两件事,而它们又都和量化是两件事——量化连一个参数都不删,只是换个位宽存。把这四件事摆在一条时间轴上,就是这个站的骨架。

但还有一句必须诚实说出来的话:以上全部来自 Qwen3 的技术报告(arXiv:2505.09388),那是对上一代的描述。Qwen3.8 这一次没有技术报告,本站不知道它有没有用蒸馏、用在哪一步、教师是谁。所以严格讲,本站在这一点上能给出的是「上一代是这么做的」,不是「这一代是这么做的」。你这一炮,打在了本站材料的边界上。

答辩二:三个数字对不上

你在第 12 章说「复现了官方数字」,可你自己算出来是 27.36B,官方叫 27B,某个部署工具的页面标 27.3B。三个数没有一个一样。凭什么说你复现了?

参考防守(先自己组织语言再看)

先把「复现」这个词的含义说清,因为分歧就在这里。本站说的复现,不是「三个数字字节级相同」,而是「本站的点钞过程能把官方那个名字解释出来,且解释可以被你复核」。

具体地:本站逐张矩阵数出文本塔 26.90 B、视觉塔 0.46 B,合计 27.36 B,四舍五入到整数就是官方名 27B。同一套方法用在另一个模型上,数出总参 2.420 T、激活 95.29 B,对上官方名 2.4T-A95B一套方法在两个规模差近百倍的模型上都对上了,这件事本身就是最强的验证——如果方法有系统性错误,不可能两次都恰好落在官方名字上。

至于 27.3 和 27.36 差的那 0.06 B(约 0.2%):不同的计数口径会造成这个量级的差异,比如算不算视觉塔、算不算那个内置的多 token 预测草稿头、输出头与嵌入层是否共享。但本站必须承认:本站没有那个页面的口径说明,所以「差异来自口径」这句话本身也是本站的推断,不是已经证实的事。

这一击里有一半我们要认下:如果三个数字的差异解释不出来,那本站就该说「没复现」。本站现在能给的解释是「量级一致、差异在合理的口径范围内」,这比「完全一致」弱。你要做的检验很简单也很有力:把视觉塔那 0.46 B 加进去或减出来、把 MTP 头加进去或减出来,看哪一种组合能凑出 27.3——凑得出来,本站的解释就被证实了一步;凑不出来,你就发现了一个本站没发现的问题。这道题本站把它公开留着。

答辩三:你的估计凭什么更可信

你说网上的显存计算器都算错了,可你自己那条 24GB 推导里也有一个「框架开销 1.0–2.0 GiB」的经验值,你自己都承认没实测。都是估的,凭什么你的估计更可信?

参考防守(先自己组织语言再看)

因为这两种「估」不是同一件事。

那些计算器错的不是精度,是结构:它们把 64 层全部当成有 KV cache 的注意力层,而实际只有 16 层是。这不是「估偏了一点」,是把公式里的一个变量填错了,结果高估整整 4 倍。而这个填错是可以被证伪的——打开 config.json,full_attention_interval = 4,64 ÷ 4 = 16,模型卡的文字描述和第三方推理框架的独立表述都印证同一件事。三处互相印证的东西,和一个填错的变量,不在同一个层次上。

本站这条推导的结构是对的,不确定的只有一项:框架与激活开销。所以本站做了三件事:把它单独列成一项给区间而不是给单点让结论也跟着是区间(约 8–10 万 token,而不是「78K」这种精确到个位的假精度)。一个诚实的估计,标志不是它更准,是它把不确定性放在哪一格、有多大都写出来了,让你可以只推翻那一格。

但这一击有一半我们守不住,必须认:1.0–2.0 GiB 这个区间本站没有实测支撑,它是按常见推理框架的经验取的;而真实部署还有显存分页与碎片的开销,实际值会更靠近区间的下沿——也就是说,本站给的上下文上限偏乐观的可能性大于偏悲观。如果你手上有一张 24GB 卡,请你用一次实测把这个区间收窄。那一次实测比本站整条推导有价值得多,因为它把唯一那一格不确定性变成了确定的。

答辩四:config 未必是实现

这整个站是建立在 config.json 上的。可 config 只是一个配置文件,它是给加载器看的参数表,未必反映真实实现。你怎么知道 full_attention_interval: 4 在代码里被解释成了你以为的那个意思?

参考防守(先自己组织语言再看)

这一击是对的,而且是本站方法论上最大的软肋。本站不打算把它说小。

config.json 是加载器的输入,不是实现本身。同一个字段在不同框架里完全可能被解释成不同的东西——就拿 full_attention_interval: 4 说,它只给了一个间隔数:从第几层开始数、组里那一层全注意力排在最前还是最后,config 一个字都没说。这个差别对参数总量没有影响(所以第 12 章的点钞不受影响),但对每一层具体怎么接、KV 在哪几层上分配,是有影响的。

本站能做的是交叉验证,也确实做了:模型卡的文字描述写的是 16 × (3 线性 + 1 全)——这句话恰好补上了 config 没说的组内顺序;config 的字段算出来是 16 层全注意力,和它一致;一份第三方推理框架的博客在讲另一个模型时独立给出「每 4 层一次全注意力,其余 69 层跑线性注意力」,23 + 69 = 92 与该模型的 23 × (3 + 1) 完全吻合。三个来源、两个模型、互相印证——三处都对上时,这个读法是错的可能性很低。但这只是提高了置信度,不是证明。

更要认的一条:凡是只有 config 单一来源、没有第二处印证的解读,本站的置信度确实不高。举一个具体的:partial_rotary_factor = 0.25 本站读成「head_dim 256 里只有前 64 维加旋转」,这个读法来自实现惯例,模型卡里没有一句话直接确认。要彻底解决只有一条路:下载权重、加载、把每一个张量的名字和形状打印出来,逐条对。这件事本站没做——BF16 权重 54.66 GB,做这件事需要带宽、硬盘和显卡。

所以这是本站最该被推翻的地方。如果你做了那件事,你就比本站多知道一件事,而且那一件事本站没法反驳。这是本站能给出的最诚实的回答。

19.4 开放课题:还没有人知道的三件事

下面三个课题,前两个是真未解——领域里确实没有公认答案;第三个是对你而言未知——答案很可能存在,只是不在本站的材料里,本站会告诉你去哪儿找。这个区分很重要:把第二类当成第一类,会让你以为自己站在学科前沿,其实只是站在一份材料的边界上。

真未解混合线性注意力的最优比例,随模型规模怎么变?

第 7 章问过「全注意力层该占多少」,这里问的是下一个问题:这个比例随规模变吗?已知的事实很奇怪——三个层数差了将近一倍的模型(48 层、64 层、92 层)用的是同一个 3 : 1 比例。这件事本身就值得追问:如果最优比例随规模变化,那么三个规模用同一个比例,至少有两个不是最优;如果它随规模变化,那需要一个解释——凭什么一个关于「多少层需要精确回忆」的量,会和模型有多大无关?这两条路上都没有公开答案。

卡在哪儿:这种消融必须重新预训练才有意义(换了布局,权重就不能复用),每扫一个点就是一次完整的预训练;而要看「随规模怎么变」,你还得在至少两三个规模上各扫一遍。成本高到只有极少数机构做得起,而做得起的那几家通常不公开这一类实验。此外还有一个测量学上的麻烦:「回忆能力」缺一个公认的度量——大海捞针类任务对针的位置、草堆的内容、提问的措辞都极其敏感,换一套模板结论可能翻转。

先做这一步:用一个你跑得动的小规模(比如一亿参数以内)做一组对照。固定总层数与总参数,只改 full_attention_interval(取 2、3、4、6、8 五档),在同一份数据上训同样多步,每档测两件事:语言建模困惑度,以及一个你自己造的长距离回忆任务(把一句只出现一次的关键句埋在序列的不同深度)。把「困惑度几乎不变、但回忆正确率开始掉」的那一档记下来——那就是拐点。
然后把总层数翻倍,整组重跑一遍,看拐点位置有没有移动。移动了,你就有了规模依赖的第一个证据;没移动,你得到的是一个更强的结论。做完再去读 Samba(arXiv:2406.07522)的消融部分和 Jamba(arXiv:2403.19887)关于「为什么必须保留全注意力层」的论证,两边对照。位置差很远的话,去想为什么——是任务不同,还是你的小模型把这件事简化过头了。那个「为什么差」就是你自己的第一个研究问题。

真未解低比特 KV 量化,在 3 : 1 混合布局上表现如何?

KV cache 量化已经有一批研究(KIVI,arXiv:2402.02750;KVQuant,arXiv:2401.18079),结论大体是可以把 KV 压到很低的比特而能力损失有限。问题在于:这些研究几乎都是在纯注意力模型上做的,而 3 : 1 混合布局有一个纯注意力模型没有的结构特征——只有四分之一的层有 KV cache,而这四分之一承担了全部的「任意位置精确取回」的职责

这会让它对 KV 量化误差更敏感还是更不敏感?两个方向都讲得通。更敏感的理由:纯注意力模型里有几十层注意力,某一层的量化误差可以被其他层部分兜住;混合布局只有 16 层,每一层都更关键,误差没地方分摊。更不敏感的理由:另外 48 层 DeltaNet 提供了一条完全独立的信息通路,注意力层受损时它可能补上一部分。没有公开数据能在这两个猜想之间做出裁决。

而这件事有直接的工程后果:本站第 17 章那条推导里,「开 fp8 KV 让上下文大约翻倍」的字节账是硬的,但它默认了 fp8 KV 不掉能力——这个默认在混合布局上从来没有被验证过。你每次在启动参数里打开 KV 量化,都是在依赖一个没人验证过的假设。

先做这一步,你在一张 24GB 卡上今天就能开始:用 llama.cpp 的 --cache-type-k / --cache-type-v,在同一台机器上把 KV 精度从 f16 依次降到 q8_0、q4_0,其余参数全部固定(同一个量化档的权重、同一档 reasoning_effort、同一个温度、同一批输入)。用一个你自己造的长文档回忆任务:把一句只出现一次的关键句分别埋在 8K、32K、64K 的深度,每种精度 × 每个深度各跑 30 次,把正确率记成一张表。
先看正确率在哪一档开始掉,再看掉的幅度和针埋得多深有没有关系。如果深处掉得比浅处厉害,那说明误差是随注意力跨度累积的,这本身就是一个可发表的观察。据本站所知,这件事没有公开结果。

对你而言未知reasoning_effort 三档,在实现层面到底改了什么?

模型卡说 Qwen3.8 是统一权重,thinking 默认开,用 reasoning_effort 的 xhigh / medium / low 三档调推理深度。但本站没有找到公开说明它在实现层面改的是什么。至少有三种可能,而它们对你的影响完全不同:可能一,它改的是聊天模板里注入的一段提示词(那你自己就能拼出等效的效果,也能自己造第四档);可能二,它改的是采样与生成参数,比如思考段的最大长度上限(那你能通过调参数复现,但复现得没那么精确);可能三,它在解码时对某些特殊 token 施加了偏置(那你基本没法在框架之外复现)。

这个答案几乎肯定存在——它就写在推理框架的实现里,只是不在本站的材料里,所以本站把它标成「对你而言未知」而不是「真未解」。这个区分本身就是第 18 章那一章的实践:拿不准就标第二种,并写清去哪儿查。

先做这一步:去读实现,不要去读文档。两个模型卡里唯一的 GitHub 链接就是两个推理框架的支持 PR(vLLM 那个和 SGLang 那个),打开它们(vLLM 的那个 PR 编号是 34330,SGLang 的是 18467,模型卡里都给了链接),在改动里搜 reasoning_effort,看这个参数最终落在了哪儿——如果它进了聊天模板,那就是提示词层面的事,你可以照着自己拼;如果它进了采样参数或 logits 处理,那就是解码层面的事,性质完全不同。顺手再看一眼三档之间的具体差值是硬编码的还是可配置的。
查完做两件事:一是拿你的结论去解释为什么三档的分数不能直接比(回到第 18 章七问的第 ⑤ 问);二是如果你找到了明确答案,本站这一条就该改——这正是第 18 章最后一节说的那种反馈:写清你在哪儿找的、找的是什么、看到了什么。

19.5 接下来读什么

下面这份清单只列本站真正引用过、并核对过编号的材料,按四个方向分层。每一条后面那句话是「为什么读它」——没有理由的推荐等于没推荐。

入门补强(前面章节里被一笔带过、值得回头补的)

  • RoFormer / RoPE(arXiv:2104.09864)——位置编码为什么要用旋转。第 4 章那个「256 维里只转 64 维」的部分 RoPE,前置知识全在这里。
  • Distilling the Knowledge in a Neural Network(arXiv:1503.02531)——「蒸馏」这个词的源头,一篇很短的老论文。读它是为了看清蒸馏改的是训练信号而不是权重来源。
  • YaRN(arXiv:2309.00071)——上下文外推的主流方法。读它才知道「原生 262K,可扩展到 1M」这句话里的「扩展」是怎么做到的,以及代价在哪。

架构深入(这台机器每一层的原论文)

  • Gated Delta Networks(arXiv:2412.06464)——那 48 层的原论文。核心是两件事配合:门控负责快速擦除旧记忆,delta 规则负责定点更新。第 6 章讲的全是它。
  • Jamba(arXiv:2403.19887)——论文级地论证「为什么必须保留少量全注意力层」。3 : 1 布局里那个「1」的存在理由。
  • Samba(arXiv:2406.07522)——最简洁的混合架构消融。做 19.4 第一个课题时的对照基准。
  • DeepSeekMoE(arXiv:2401.06066)——细粒度专家切分 + 共享专家隔离,正是 512 选 10 + 1 这条路线。第 9 章的直接上游。
  • Switch Transformers(arXiv:2101.03961)——MoE 工程化的起点,一句「参数多得离谱,计算量却是常数」把总参数与激活参数的分离讲透了。
  • Mixtral of Experts(arXiv:2401.04088)——47B 总参 / 13B 激活的经典对照点,用来校准你对稀疏度的直觉。

量化深入(第 15、16 章的全部弹药)

  • LLM.int8()(arXiv:2208.07339)——「离群值」这件事第一次被讲清楚。理解为什么低比特量化不能一刀切,从这篇开始。
  • SmoothQuant(arXiv:2211.10438)——把难量化的激活「挪」一部分到权重上。一个很漂亮的问题转移思路。
  • GPTQ(arXiv:2210.17323)与 AWQ(arXiv:2306.00978)——两条主流的训练后量化路线,标题里的 Post-Training 一词就点破了时序:量化发生在训练之后
  • QLoRA(arXiv:2305.14314)——NF4 数据类型,以及「量化之后还能不能微调」这个问题的答案。
  • k-bit Inference Scaling Laws(arXiv:2212.09720)——全站最该读的一篇。它把「量化一个大模型」和「直接换用一个小模型」放在同一个比特预算下正面对撞,而这正是第 1 章那个误解的正面回答。
  • Quantization Hurts Reasoning?(arXiv:2504.04823)——专门测推理模型(长思考链)在量化后的表现。对一个默认开 thinking 的模型格外相关。
  • Does quantization affect long-context tasks?(arXiv:2505.20276)——直接回答「长上下文下量化掉多少」。
  • KIVI(arXiv:2402.02750)与 KVQuant(arXiv:2401.18079)——KV cache 量化的两篇代表作,做 19.4 第二个课题的必读。

训练与尺寸(第 13、14 章那条线)

  • Kaplan et al., Scaling Laws(arXiv:2001.08361)——损失随参数、数据、算力呈幂律。「尺寸」从此是一条连续曲线上的取样点。
  • Chinchilla(arXiv:2203.15556)——给定训练算力,参数和 token 该等比例放大。
  • LLaMA(arXiv:2302.13971)——对上一篇的核心反驳:那个目标忽略了推理预算;小模型训得更久,最终在推理上更便宜。
  • Llama 3 Herd of Models(arXiv:2407.21783)——官方白纸黑字承认「我们把小模型训得远超算力最优点」。
  • Beyond Chinchilla-Optimal(arXiv:2401.00448)——把推理成本写进 scaling law 的目标函数,数学上证明请求量越大,最优点越偏向「小模型 + 更多 token」。
  • Minitron(arXiv:2407.14679)——真剪枝:从训好的 15B 剪出 8B / 4B。读它是为了把剪枝和蒸馏彻底分开。
  • Qwen3 技术报告(arXiv:2505.09388)——上一代的一手材料,strong-to-weak 蒸馏的原文在这里。注意:里面的 MoE 是 128 专家激活 8,和这一次的 512 选 10 + 1 不是一回事,别拿它去支撑这一代的数字。
  • Gemma 2(arXiv:2408.00118)——另一家官方承认小尺寸用蒸馏训练,横向佐证。
  • Phi-3(arXiv:2404.14219)——第三条路:不靠蒸馏,靠数据配方让小模型打出超尺寸表现。
这份清单里少了一样东西

没有 Qwen3.8 自己的论文——因为它不存在。所以真正的一手材料是另外几样,它们不在 arXiv 上,但等级更高:两个模型的 config.json(可复算)、两个仓库里的 LICENSE 文件(法律事实)、量化仓库的文件列表(真实字节数)、以及推理框架的 recipe 与支持 PR(工程数字与实现细节)。这四样东西你今天就能全部打开,而且不需要相信任何人。

下面四个问题,各该去读上面清单里的哪一条?
(a)「同样是 12 GB 的显存预算,我该跑一个 24B 的 4 比特模型,还是一个 12B 的 8 比特模型?」
(b)「为什么不能把全注意力层全部换成 DeltaNet?」
(c)「为什么现在的小模型都要训到远超算力最优点?」
(d)「512 个专家里只激活 10 个,为什么还要额外留一个共享专家?」

先想:每一问的关键词是什么。(a) 里是「同样的比特预算」,(b) 里是「全部换掉会怎样」,(c) 里是「超过最优点」,(d) 里是「共享专家」。四个关键词各自对应清单里的一个分层。
关键在于分清「这是哪一类问题」:(a) 是量化与尺寸的交叉,(b) 是架构里的存在性论证,(c) 是训练预算,(d) 是 MoE 的具体设计。四类各在清单的一个板块里。
第一步这样走:先排除法。(a) 提到比特预算 → 量化板块;(b) 提到「全部换掉」→ 那是一篇专门论证「不能全换」的论文;(c) 提到「超过最优点」→ 那是一篇官方承认这么做的报告;(d) 提到共享专家 → 那是引入这个设计的那篇。
完整答案:
(a) → k-bit Inference Scaling Laws(arXiv:2212.09720)。它正是把这两个选项放在同一比特预算下对撞的那篇,原文给的例子就是同样比特数下两种配置的准确率可能差很远。
(b) → Jamba(arXiv:2403.19887)。它论证的就是「为什么必须保留少量全注意力层」。想看更简洁的消融就配 Samba(arXiv:2406.07522)。
(c) → Llama 3 Herd of Models(arXiv:2407.21783),里面直接承认把小模型训得远超算力最优;想看数学上的理由,读 Beyond Chinchilla-Optimal(arXiv:2401.00448);想看这条思路的起点,读 LLaMA(arXiv:2302.13971)。
(d) → DeepSeekMoE(arXiv:2401.06066)。共享专家隔离就是它提出的,理由是让共享专家去承接各类输入都用得上的通用知识,从而减少路由专家之间的冗余。
这道题真正在练的是一个习惯:遇到一个问题,先判断它属于哪一类,再去找那一类的一手论文——而不是搜一句话答案。前者你会记住十年,后者你下周就忘了。

变式:如果问题是「Qwen3.8 的 3 : 1 布局是怎么调出来的」,上面清单里没有任何一条能回答它。为什么?(这一问的答案在第 18 章。)

19.6 回到最初那个问题

第 1 章开头问过一句话:「这么多模型尺寸,是不是把大的量化压缩出来的?」那时候你可能没有把握。现在把完整答案写出来,并且给每一句标上证据等级——后面那半才是这十九章真正给你的东西。

表 19-2:全站核心论断的完整答案,逐句标注证据等级
结论证据等级与依据
不是。27B 与 2.4T-A95B 是两套独立预训练的参数,谁也不是谁切出来的① 可复算:hidden_size 5120 对 8192、稠密 FFN 对 512 专家 MoE、有视觉塔对没有视觉塔——三条差异里任何一条都不是剪枝能产生的
它们共用同一张架构图纸,差别在这张图纸重复了多少次:16 × (3 线性 + 1 全) 与 23 × (3 线性 + 1 全)① 可复算(config 字段相除)+ ② 官方声明(模型卡文字)+ ③ 第三方(推理框架独立给出 23 + 69 = 92)。三处印证
量化发生在训练之后,只改每个参数占多少比特,参数个数一个不变定义(量化的定义只谈位宽)+ ③ 第三方实测:同一个 27.36 B 模型有从 9.01 GB 到 54.66 GB 十几个文件,参数量全部相同
混合布局让 3/4 的层没有 KV cache,262K 下 16 层全注意力是 16 GiB,48 层 DeltaNet 合起来只有 144 MiB16 GiB 是 ① 可复算;144 MiB 是 本站推导(按论文定义算,未见官方或框架印证)
27B 是 Apache 2.0(开源);2.4T-A95B 是 qwen3.8-max 自定义协议(开放权重),带收入与月活两条门槛① 一手文件:两个仓库各自的 LICENSE这一条不看新闻标题,只看文件
24 GiB 单卡跑 Q4_K_M 的 27B,上下文约 8–10 万 token(fp16 KV),fp8 KV 约 17–20 万本站推导,输入里含一项 1.0–2.0 GiB 的经验区间,所以结论是区间。真实值更靠近下沿
为什么是 3 : 1?为什么 head_dim 是 256?为什么 17408?为什么 512 选 10 + 1?不知道。没有技术报告、没有消融实验。本站给的解释全是推断,不是官方说法

最后一行才是这个站最想留给你的东西。一份材料的价值,不只在于它告诉了你多少,还在于它有没有诚实地圈出自己不知道的地方。前六行你可以拿去用、拿去跟人争、拿去做决策;第七行你要记住的是那句「不知道」本身——因为在这个领域里,写得斩钉截铁的地方,往往正是没有证据的地方。

用表 19-2 的方式,给下面这句话做一次「逐前提拆解」:「买一张 24GB 的显卡,就能在本地跑一个能读十万字文档的多模态大模型。」请列出这句话成立所依赖的全部前提,并指出其中最脆弱的那一个——脆弱的意思是:它一旦不成立,整句话就塌了,而且它恰好是最没有证据支撑的那个。

先想:这句话看起来是一个结论,其实是好几个结论叠起来的。把它拆成「必须同时成立的若干件事」——量化档、视觉塔、KV 精度、上下文长度、框架开销,一件都不能少。
关键在于「最脆弱」这个词的判据不是「最可能错」,而是「证据最弱且不可替代」。把每个前提标上证据等级,等级最低的那个就是答案;如果有并列,看哪一个塌了之后没有备选方案。
第一步这样走:先把式 19-1 写出来,把这句话里的每个词映射到式子里的一项——「24GB」是 Ccard,「多模态」意味着要加 Wvis,「十万字」决定 Lmax。映射完,没被映射到的那一项就是这句话没说但必须成立的东西。
完整答案。这句话至少依赖六个前提:
① 显卡是 24 GiB 不是 24 GB。标称 24GB 的卡确实是 24 GiB,这一条成立,但如果按十进制 GB 算会少 7.4%,足以改变结论。证据:硬事实。
② 用的是 Q4 级别的量化档。17.11 GB = 15.93 GiB。换成 Q6_K(22.88 GB)就装不下了。证据:③ 第三方实测。
③ 记得加载视觉塔的 0.93 GB。「多模态」这三个字直接要求这一项,而它是独立文件,最容易被漏。证据:③ 第三方实测。
④ KV cache 只按 16 层全注意力算。若按 64 层算,十万字的 KV 就是 4 倍,直接爆掉。证据:① 可复算。
⑤「十万字」换算成 token 大约在十万这个量级。中文里字数和 token 数是同一个量级(一个 token 常覆盖一到两个汉字,具体随分词器而异),所以这句话要的大致是 5 万到 10 万 token——而本站推导给的上限是 8.3–9.9 万(fp16 KV)。注意:如果按最坏情况(一字一 token)算,十万字已经压在区间的上沿甚至超出。
⑥ 框架与激活开销在 1.0–2.0 GiB 之间。证据:本站推导的经验区间,未实测。
最脆弱的是 ⑥,理由有三:它是六个里唯一没有一手证据的;它是唯一一个本站明说「真实值会更靠近区间下沿」的;而一旦它取到 2.0 GiB,上限就落到约 8.3 万 token——按最坏情况(一字一 token)算,「十万字」这个目标就正好达不到。也就是说,整句话成不成立,恰好压在唯一那个最没把握的量上。
正确的说法应该是:「24GB 单卡跑 Q4_K_M 的 27B 是可行的,上下文实际到约 8–10 万 token(本站推导,含经验区间);十万字文档正好压在这个区间的上沿,开 fp8 KV 会宽裕得多。」把一个压在区间上沿的目标说成『就能』,是这一类说法最典型的失真方式——不是编造,是把区间说成了单点。

变式:把这句话改成「买一张 32GB 的显卡」,六个前提里哪几个的紧张程度会缓解?最脆弱的那一个还是同一个吗?(提示:余量变大之后,那个经验区间的宽度就不再决定成败了——这说明「脆弱」是相对于余量说的,不是绝对属性。)

一句话记住:尺寸差异发生在训练之前(决定要建多大的房子),量化发生在训练之后(决定用多细的尺子记录房子的坐标)。这是两件事,甚至不在同一条时间轴上。

这句话你现在不只是记住了它,你还能算给别人看:打开 config.json 数出参数、用四个字段算出 KV、拿文件列表核对体积、翻 LICENSE 确认能不能商用,最后在该说「不知道」的地方停下来。这一整套动作,就是这十九章真正在教的东西。至于 Qwen3.8 本身——它只是那份被拆开的教材。

本章小结

  • 十八级台阶:前十二级在造「是什么」(从空壳到点钞对上官方数字),后六级在造「为什么信」(尺寸的由来、三条路线的区分、量化、真实边界、证据台账)。这一次没有技术报告,后六级那部分能力才是最值钱的。
  • 一个式子贯穿全部Lmax = (显卡容量 − 权重 − DeltaNet 固定态 − 框架开销) ÷ 每 token KV 字节数。它可以正着用(能跑多长)、反着用(该选哪个量化档)、也可以倒着用(该怎么设计一个模型)。整个式子里只有框架开销一项是估的,所以结论必须是区间。
  • 六道闯关的共同结构:先分清「这是哪本账」(总参数还是激活参数、结构还是状态、事实还是解释),再套公式。大多数错误发生在分账阶段,不是计算阶段。
  • 四条答辩里,本站认下了两条半:蒸馏那一条本站的材料只覆盖上一代;27.3 与 27.36 的差异解释本身也是推断;框架开销偏乐观;而 config 未必等于实现这一条,是本站最该被推翻的地方——要彻底解决只能下载权重逐张量核对,本站没做。
  • 三个开放课题:混合比例随规模怎么变(真未解)、低比特 KV 在 3 : 1 布局上表现如何(真未解,且你在一张 24GB 卡上今天就能开始)、reasoning_effort 三档改了什么(对你而言未知,去读那两个推理框架的支持 PR)。诚实区分这两类,比多知道一个答案更重要。
  • 最后一行:为什么是 3 : 1、为什么 head_dim 256、为什么 17408、为什么 512 选 10 + 1——不知道。本站给的解释全是推断。记住这一行,比记住前面六行都重要。

造完了。这台机器现在在你手上:它的每一层你都亲手加过,每一个数字你都能追到出处,每一处不确定你都知道在哪儿。接下来它会不会过时?会。但你验它的那套方法不会。下一个模型发布的时候,你要做的第一件事已经很清楚了——打开它的 config.json,然后开始数。

复制一份提问

问什么

给它看多少上下文

下面就是要复制的内容,可以直接改