一个 671B 总参数的模型,为什么在成本上能和几十 B 的稠密模型打平?答案藏在两个数字之间的那条缝里:总参数量与激活参数量。理解这条缝,你就能解释模型选型里最反直觉的那些现象。
编辑部注 · 本章最容易答错的是一道看似简单的推理题:稀疏激活省的是算力还是显存?请务必读到最后一节。
模型参数有两个口径,混用会让人得出完全相反的结论。第一个是总参数量——模型文件有多大,要占多少显存去装。第二个是激活参数量——每生成一个 token,实际参与计算的有多少。稠密模型里这两个数字相等;MoE(混合专家)把它们拆开了。
一、两个数字之间的缝
设一个 MoE 模型有 256 个专家,每个专家约 2.6B 参数,每层路由激活其中 8 个。
| 口径 | 稠密模型 | MoE 模型 | 工程后果 |
|---|---|---|---|
| 总参数量 | 与激活量相同 | 所有专家之和,例如 671B | 决定显存下限:权重得装得下 |
| 激活参数量 | 同上 | 每 token 只走 top-k 个专家,例如 37B | 决定单 token 算力:接近一个 37B 稠密模型 |
| 每 token FLOPs | 与总参数成正比 | 与激活参数成正比 | 吞吐高、单价低 |
| 跨设备通信 | 少(同层并行即可) | 多(专家分布在不同设备,需 All-to-All) | 延迟与实现复杂度上升 |
| 训练稳定性 | 较稳 | 路由易坍缩,需负载均衡损失 | 训练工程难度更高 |
一句话概括 MoE 的交易:用显存和通信换算力效率。参数总量上去了(容量变大),但每个 token 的计算量没跟着上去。
MoE 不是让模型变小,而是让「不参与计算的参数」变得便宜。本刊编辑部
二、路由是怎么工作的
三、省的是算力,不是显存
这是本章最重要的一句话,也是最容易答错的地方:
错误说法:「MoE 因为只激活部分专家,所以显存需求也降低了,能在小显存上部署大模型。」
为什么错:路由是运行时根据输入动态决定的。你无法预知下一个 token 会选哪两个专家,所以所有专家的权重都必须常驻显存(或在需要时快速换入,代价极高)。MoE 压缩的是每 token 的 FLOPs,不是权重的存储体积。
正确说法:MoE 让「参数容量」和「单 token 算力」解耦——你可以拥有 671B 的知识容量,却只付 37B 的算力账单;但显存下限仍然由 671B 决定。
由此衍生出几个工程结论:
- MoE 的部署门槛是显存,不是算力。 这就是为什么 MoE 模型通常只在云端大规模集群上跑,很难本地化。
- MoE 的延迟优势不如吞吐优势明显。 单请求延迟受 All-to-All 通信影响,可能反而比同激活量的稠密模型更慢;但吞吐(单位时间处理 token 数)会好很多。
- MoE 对 batch 敏感。 batch 越大,专家利用率越高,单位成本越低。这解释了为什么小 batch 场景下 MoE 的性价比优势会缩水。
四、动手:三十行感受一次路由
import numpy as np
def softmax(x, axis=-1):
m = x.max(axis=axis, keepdims=True)
e = np.exp(x - m)
return e / e.sum(axis=axis, keepdims=True)
class MoELayer:
def __init__(self, d_model=16, n_experts=8, top_k=2, seed=0):
rng = np.random.default_rng(seed)
self.top_k = top_k
# 门控权重 [d_model, n_experts]
self.Wg = rng.normal(size=(d_model, n_experts)) / np.sqrt(d_model)
# 每个专家的权重 [n_experts, d_model, d_model]
self.experts = rng.normal(size=(n_experts, d_model, d_model)) / np.sqrt(d_model)
def forward(self, x):
"""x: [n_tokens, d_model]"""
gates = softmax(x @ self.Wg) # [n, n_experts]
idx = np.argsort(-gates, axis=1)[:, :self.top_k] # [n, top_k]
out = np.zeros_like(x)
for t in range(x.shape[0]):
acc = np.zeros(x.shape[1])
w_sum = gates[t, idx[t]].sum() # 重归一化,保证权重和为 1
for e in idx[t]:
acc += (gates[t, e] / w_sum) * (x[t] @ self.experts[e])
out[t] = acc
# 专家利用率统计 —— 这是训练稳定性的关键观察指标
used, counts = np.unique(idx, return_counts=True)
self.last_usage = dict(zip(used.tolist(), counts.tolist()))
return out
layer = MoELayer()
x = np.random.default_rng(1).normal(size=(64, 16))
y = layer.forward(x)
print('输出形状:', y.shape) # (64, 16),与输入一致
print('专家使用次数:', layer.last_usage) # 观察是否集中在少数专家身上- 把
n_experts从 8 调到 64,观察last_usage的长尾分布——你会看到少数专家被反复选中,这就是「路由坍缩」; - 加上一个负载均衡项:统计每个专家的使用次数,对过度使用的专家施加惩罚,再跑一次;
- 计算「激活参数量占比」:
top_k / n_experts,并说明它与「每 token 算力」和「显存占用」分别是什么关系。
五、自测
六、小结
| 你要能回答的问题 | 一句话答案 |
|---|---|
| MoE 省什么 | 省每 token 的计算量(FLOPs),不省显存 |
| MoE 贵在哪 | 显存下限高、跨设备 All-to-All 通信、训练需要负载均衡 |
| 什么时候 MoE 划算 | 高并发、高吞吐的云端推理场景 |
| 什么时候不划算 | 低并发、强延迟敏感、本地化部署 |
| 与 KV Cache 的关系 | 完全独立:KV Cache 管的是历史复用,MoE 管的是参数复用 |
下一章换个更贴近日常的问题:既然输出是采样出来的,那「同一个问题问两次答案不同」到底该不该修,以及怎么修。
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
为什么 MoE 的总参数量和激活参数量不是一回事? 高频
总参数量是所有专家参数之和——决定模型需要
要多少显存/内存装下它;
激活参数量是单个 token 实际参与运算的那部分参数——决定算力开销。
所以一个总参数 671B 的模型,单 token 激活可能只有 37B 量级,算力像小模型,容量像大模型,这就是它便宜的原因。
追问链
- 那显存能不能按激活参数量来估?
- 路由不均(专家负载倾斜)会带来什么工程问题?
MoE 和稠密模型分别适合什么场景?
MoE 适合:预算充足、追求单位算力下的质量、有大规模并发可以摊薄通信与调度开销、以及显存资源相对宽裕的服务端部署。
稠密模型适合:显存受限(端侧、单机小卡)、并发低且延迟敏感(MoE 的专家并行会引入额外通信延迟)、需要极简部署与稳定吞吐的场景。
换句话说:MoE 换来的是"用容量换质量",代价是显存占用、通信开销与调度复杂度。
MoE 的负载不均衡是什么问题?会怎么表现?
常规处理是加负载均衡损失(鼓励路由分布均匀)、设置专家容量上限与溢出丢弃策略、以及在工程侧做专家并行的重排布。对使用方的启示:同一个模型在不同任务分布下的实际吞吐可能差异很大,压测要用真实流量而不是随机文本。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。