为什么 greedy 也可能出不同 token,MoE 为什么更严重
复习时先读第 0 节。后面每一节只支撑这一节里的一句话。
0. 三十秒回忆
结论: greedy 每次都选当前分数最高的那个 token,前向本身不是抽签。同一套权重、同一段输入,数学上应该得到同一串字。线上仍可能不同,是因为 GPU 把同一串加法换成了另一种括号,logits 在小数点后几位不一样;两个候选几乎打平时,最高分就会换人。
MoE 更严重的原因: 稠密模型里这点误差只把隐藏状态挪动一点点,后面多半还是同一个函数。MoE 的 TopK 是一次硬切换。第 k 名和第 k+1 名贴在一起时,同样的扰动会换掉一整组专家权重,输出从连续的小误差变成跳变,后面的层更容易跟着换。
三句对照:
| | 稠密 | MoE |
|---|---|---|
| 误差从哪来 | 矩阵乘、attention 的加法顺序 | 同样这些,再加路由名次 |
| 误差看起来像什么 | 连续、很小,常常衰减 | 跨过名次线就是另一套矩阵 |
| 单人、单卡、greedy 重跑 | 通常同一串字 | 也通常同一串字;专家分数打平时更容易被残余抖动放大 |
1. greedy 在算什么
模型前向算出的是词表上每个字的分数,叫 logits。greedy 就是 argmax:取得分最高的那个 token。
要不要按概率抽样,由解码策略决定,不由结构决定。do_sample=True 才走多项分布抽样;greedy 不抽样。MoE 的路由器在推理时也是确定的 TopK:同一个隐藏状态,选中的专家相同。训练时可能给路由加过噪声,推理会关掉。
所以「同输入 + 同权重 + greedy」在精确算术里是同一条路径。会分叉的前提只有一个:两次前向算出来的 logits 不完全一样,而且第一名和第二名贴得很近。
2. 为什么同一公式会算出两个 logits
浮点加法不满足结合律。括号一变,结果就变。
记 a, b, c = 1e20, -1e20, 1.0。这里的 e 是科学计数法,1e20 = 1 × 10^20,不是自然常数。-1e20 是绝对值很大的负数。
text
(a + b) + c = 0 + 1 = 1.0
a + (b + c) = 1e20 + (-1e20) = 0.0
b + c 是 -1e20 + 1。float64 在 1e20 附近,相邻两个可表示数大约隔 16384。1 小到挪不动这个大数,所以 b + c 仍是 -1e20,c 被吃掉。先把两个大数抵消,再加 c,c 才留得住。
4096 个 float32 随机数,从头加到尾和分组加,可以差出大约 1e-5。一次线性层就是这种长度的加法。很多层传完,logits 常常只差 1e-5 到 1e-3。第一名领先一截时,字不变;第一名和第二名只差 0.0001 时,字会换。换了一个字,下一步的输入就不同,后面整段分开。
3. 括号是在哪一次加法里换的
换括号发生在同一次矩阵乘或 attention 里面,不是把后面的 Transformer 层拿来先算。层与层仍然按顺序:第 0 层的输出才是第 1 层的输入。
3.1 线性层内部的那一串乘积
q_proj 的一个输出数是隐藏维上的点积。MiniMind 默认隐藏维 768,就是 768 个乘积相加:
text
y = x[0]*W[0] + x[1]*W[1] + ... + x[767]*W[767]
= p0 + p1 + ... + p767
这 768 个 p 都来自这一层、这一次 q_proj。它们彼此不依赖,所以 GPU 可以分段加完再合并。合并完,这一层才把 y 交给后面的 RMSNorm 和 attention。
别人的 token 不出现在你的公式里。y_你 = x_你 · W。别人只占同一张表的其他行,从而改变 GPU 怎么切你这串加法。
切法有两层:
-
分块累加(每次矩阵乘都有)。 权重放得进显存,放不进 SM 旁边那块大约一百多 KB 的共享内存。7B 的一个
q_proj大约 32 MB。张量核一条指令也只乘十几到几十见方的小块。所以核里是acc = 0,每次取 K 方向的一小段,acc += 这一段的点积。 -
split-K(行数很少时才有)。 输出位置不够把整颗 GPU 占满时,把 K 再劈给多个 SM,各算一个部分和,再在显存里相加。解码时每一拍往往只有 1 个新 token,最容易走到这里。谁先算完谁先
atomicAdd,同形状两次跑,y仍可能差在很后面的小数位。
行数变多时,split-K 通常切得更粗,不是更细。GPU 已经能用不同的输出行把 SM 占满,就不必再把同一条点积劈得很碎。
3.2 Continuous batching 改变的是「这一拍有多少行」
服务器每一拍把正在算的 token 排成一张表一起乘同一套权重。你的 prompt 和此刻并发的其他请求可以在同一张表里。每一拍的行数、你排在第几行,都跟着并发变。
它要解决的是吞吐,不是数值问题。解码时每个请求只新算 1 个 token,却要把整份权重从显存读出来。行越多,读一次权重摊到的 token 越多。请求有长有短,也不能凑齐一批再一起结束:短请求会空占位置,新请求要等最长的那个跑完。Continuous batching 每一拍重组这张表,谁结束谁让位。队列里只有你一个人时,表上就一行,这个机制帮不上忙。
副作用是:5 行和 30 行会选出不同的切块和 split-K,你那串 p 的括号变了。RMSNorm 不参与这件事。它只对你这一个 token 的隐藏维做均方根,别的请求不进统计量。误差在前面的点积里已经产生,归一化只是把带误差的向量往后传。
3.3 Attention 是另一串加法
一个 token 的 attention 输出是你自己前文的加权和:
text
y = w0*V0 + w1*V1 + ... + w1023*V1023
别人的 token 不进你的分数。会变的是这串和怎么切。
-
Flash-Decoding: 上下文很长、这一拍的 query 又很少时,把 KV 分给多个 SM,各算一段的最大值、指数和、部分输出,再缩放后合并。切几段由 batch 和上下文长度决定。
-
Chunked prefill: 长 prompt 按这一拍剩下的 token 额度切开。后一拍要同时看「已进缓存的前缀」和「本拍这几个字」。并发把额度占掉时,合并点从 512 变成 256 或 128。单独跑、额度够一次算完时,这些边界不存在。
所以:GEMM 的 split-K 在人少时更细;attention 的块边界是人多时才会多出来的合并点。
3.4 两种 reduce 不要混
| | 发生在哪 | 什么时候有 |
|---|---|---|
| 内核内归约 | 一张卡上,几个 SM 把同一个 y 的几段部分和相加 | 单卡就会有,解码、行数少时最常见 |
| 张量并行 AllReduce | 多张卡各拿 K 的一段,算出部分 y 后用 NCCL 相加 | 只有把同一层权重拆到多卡时才有 |
按输出维度切开的那一半(例如各卡算不同的注意力头)是把结果拼接起来,那一步是 AllGather,不是相加。单卡推理没有 AllReduce。开了张量并行时,两种会叠在一起:卡内可能先 split-K,卡间再 AllReduce。
4. 为什么 MoE 把这件事放大
4.1 路由是一次硬切换
MiniMind 的门控是 softmax 之后做 TopK。默认 4 个专家里选 1 个。得分连续,名次不连续。例如:
text
专家0 0.400 专家1 0.399 → 选 0
扰动后 0.398 0.401 → 选 1
选 1 就是换一套 gate_proj、up_proj、down_proj。那是另一组训练出来的矩阵,输出可以从头几位就不一样。这一层差大了,下一层路由器看到的向量也差大了,后面的专家可能跟着换。
不是每个 token 都会换。名次拉开时,1e-5 翻不过去。容易翻的是两个专家几乎打平的那些 token。句子长、层数多,这种边界会反复出现。
4.2 容量丢 token 是训练时的显存上限,不是推理默认行为
训练时可以给每个专家固定名额,例如 1.25 × token数 / 专家数,矩阵乘的行数才有硬顶,显存按这个上限分配。挤进来的人按路由分数截断,多出来的那个专家输出记成 0,只靠残差把当前隐藏状态送去下一层。Attention 已经加过了,下一层拿到的不是空向量,只是少了这一层的前馈。偶尔发生还能训;溢出一多,这些 token 反复绕过前馈,困惑度会变差。
让专家被均匀用到的,是另一项负载均衡损失,不是丢弃本身。MiniMind 只在 self.training 时算这项辅助损失,前向里没有容量丢弃:选中了就算。vLLM、SGLang 的推理也是按实际人数算完。
推理和这件事还剩的联系:路由器在训练时学过要分散。分散会改变每个专家这一拍的行数,从而改变那次分组矩阵乘的切法,但不会把你的前馈静默变成 0。
4.3 专家并行多了一层「谁选了这个专家」
专家并行把专家分到不同 GPU。送到某个专家上的矩阵行数,等于这一拍有多少 token 选了它。总 batch 同是 32 时,可能 4 个选了它,也可能 30 个。行数不同,分块和 split-K 就不同。
稠密模型的行数只由这一拍的 token 总数决定。MoE 还要看整批的路由直方图。直方图又是上面那个离散选择的结果。
5. 单机、单条请求、greedy,还剩什么
同一张卡、同一套库、一次只跑这一条、并且是真正的 argmax(温度关掉),重复跑应当得到同一串 token。MoE 也一样。
这时矩阵行数就是你这句的 token 数,attention 每次切在同样的位置,每个专家接到的也只是你这句话里选中它的 token。前面那些「跟着陌生人变」的量都没了。
还可能改字的,只剩同一形状下的内核归约,例如 split-K 的原子加。稠密模型里这点抖动很少越过 argmax。MoE 在某一层两个专家几乎打平时,仍可能换专家,后面整段分叉。
这不是比特级保证。要锁死,需要确定性算法和固定的 cuBLAS workspace。不锁的话,同形状也不保证每次走同一条归约。
6. 附录:对话里一并弄清的概念
主线复习可以不看这节。想回忆硬件和并行时从这里翻。
6.1 一张消费卡里有什么
SM 是 Streaming Multiprocessor,一组一起干活的电路。规格表里的 CUDA 核是 SM 里面的标量算术单元;张量核是同一组上的矩阵乘阵列;光追核做光线和三角形相交。三者共用这一组的寄存器和共享内存。
5060 Ti(Blackwell,计算能力 12.0)每个 SM:
| 部件 | 数量 |
|---|---|
| CUDA 核 | 128,4 组 × 32;都能同时做 FP32 和 INT32 |
| 第 5 代张量核 | 4 |
| 第 4 代光追核 | 1 |
| 纹理单元 | 4 |
| 调度器 | 4 |
| 寄存器 | 256 KB |
| L1 与共享内存 | 物理 128 KB,程序最多划出约 100 KB 做共享内存 |
整卡 36 个 SM:4608 CUDA 核,144 张量核,36 光追核。显存 8 GB 或 16 GB GDDR7,128-bit,448 GB/s。
张量核只有 4 个,是因为 SM 有 4 组,一组旁边放一个矩阵阵列,不是因为矩阵算力少。5060 Ti 的 FP32 峰值大约 24 TFLOPS;官方 AI 算力 759 TOPS。两者都是「每秒万亿次运算」,T = 10¹²。24 TFLOPS 只数 CUDA 核上的 FP32 乘加。759 TOPS 是张量核上带 2:4 稀疏的 FP4 理论峰值,大约是 FP32 的 32 倍。权重没有按 2:4 稀疏存储时,吃不到 759。
RTX PRO 6000 Blackwell 工作站版(600 W):188 个 SM,24064 CUDA 核,FP32 125 TFLOPS ,AI 4000 TOPS(同样是稀疏 FP4)。96 GB GDDR7 ECC,512-bit,1792 GB/s。Max-Q 是同一套 SM,功耗 300 W,频率和这两个峰值都会低一些。5090 的总线也是 512-bit、1792 GB/s;它是 170 个 SM、32 GB、96 MB 二级缓存。
显存带宽不跟 SM 个数成比例。带宽 = 总线位数 / 8 × 颗粒速率。5060 是 30 个 SM,5060 Ti 是 36 个,带宽都是 448 GB/s。
GB10(DGX Spark)是另一类:48 个 SM、6144 CUDA 核、计算能力 12.1,128 GB LPDDR5X 由 CPU 和 GPU 共用,总线 256-bit,带宽 273 GB/s。SM 数量接近 5060 Ti,带宽反而更低,因为颗粒是 LPDDR5X 不是 GDDR7。
每一代架构会改 SM。同一代、同一系列共用一套 SM,差别是启用多少个。数据中心的 B200 是计算能力 10.0,带 TMEM,两个 SM 可以合做一次矩阵乘,和 5090 的 12.0 不是同一个 SM。
SM 里放多种核,是因为指令形状不同。CUDA 核一拍做一两个数的加减乘,负责地址、RMSNorm、softmax、TopK。张量核一拍做一小块矩阵乘,线性层的大头在这里。光追核只在图形里忙,跑语言模型时空着。
6.2 NVFP4 和 Rubin
NVFP4 是 Blackwell 张量核上的 4-bit 浮点。每个数是 E2M1:1 位符号、2 位指数、1 位尾数,绝对值只有 0, 0.5, 1, 1.5, 2, 3, 4, 6。每 16 个数共用一个 FP8 缩放,整张张量再有一个 FP32 缩放,平均大约 4.5 bit 一个数。它和 AWQ/GPTQ 那种「权重 INT4、激活仍是 FP16」不是同一种用法;NVFP4 是权重和激活都进 4-bit 矩阵乘。大模型的线性层比较扛得住,嵌入、输出层、softmax、MoE 路由器通常留在更高精度。Hopper 新增的主要是 FP8;Blackwell 加上了 FP4、FP6 和按块缩放的 MXFP8。Rubin 公开的精度集合与 Blackwell 相同,提高的是 NVFP4 吞吐。
数据中心 Rubin 相对 Blackwell,NVIDIA 公布的单卡数字:NVFP4 推理约 5 倍(50 PFLOPS),NVFP4 训练约 3.5 倍(35 PFLOPS);显存为 HBM4,288 GB,带宽约 22 TB/s;NVLink 6 为每卡 3.6 TB/s,是 Blackwell 1.8 TB/s 的两倍。消费级下一代尚未官宣,泄露说法落在 2027 到 2028。
6.3 张量并行为什么能加快单个用户
张量并行把同一层的权重切开,放到多张卡上。不是把第 0 层放卡 0、第 1 层放卡 1;那是流水线并行。
小 batch 的解码卡在读权重。时间大致是 权重大小 / 显存带宽。TP=N 时每张卡只读 1/N,而且同时读。AllReduce 加的是激活:一个 token 的隐藏状态只有几 KB 到十几 KB,比几十 MB 的权重小得多。所以这一条流的延迟下降。
人一多,同一份权重已经被很多行摊薄,AllReduce 的体积也随 batch 变大,总吞吐常常不如数据并行(每张卡一份完整模型,各算各的请求)。模型放不进一张卡时,张量并行首先是为了放得下。半精度大约每 10 亿参数 2 GB:7B 约 14 GB,一张消费卡放得下;70B 约 140 GB,一张 80 GB 或 96 GB 的卡放不下。
vLLM 用 --tensor-parallel-size,SGLang 用 --tp。SemiAnalysis 的 InferenceX 把 TP、专家并行、数据并行放在同一条延迟--吞吐曲线上测过:TP 用来拉高小 batch 的单用户速度;并发高时,MoE 更常靠专家并行和数据并行来换每卡吞吐。一个具体对照:8×A100 上 vLLM 跑 QwQ-32B,batch=1 时 TP 从 2 到 8,速度大约 36 → 50 → 63 token/s;batch=128 时 TP=8 的总吞吐低于 TP=4。