稀疏激活实测:24B 总参数只激活 2.2B,路由不均会丢 11.5% 的 token
10 月 13 日一份市场调研报告把国内 AI 加速器的出货账翻了新页:2026 年上半年英伟达在中国数据中心 AI 芯片市场的份额降到 34%,出货 67.8 万颗、同比少 44%,华为升到 28%。能拿到的卡在变小、变少,于是"这个模型要占多少显存、每生成一个 token 要搬多少权重"比"总参数多大"更值得先算清楚。
稀疏激活(MoE)就是把总量和激活量拆开的那条路线。10 月 13 日在 Hugging Face 上发布的 LFM2.5-24B-A2B 属于这一类:总参数 240 亿,官方说每个 token 只激活 20 亿左右。
下面两件事都是我在本机做的:先从权重文件头把参数账算出来(不下载权重本体),再用真实中文语料实测 top-k 路由的负载分布,看稀疏这条路到底会在哪里亏。环境是 WSL + 纯 numpy,没有 GPU。
不下载权重也能把参数量算准
safetensors 的文件头是个很省事的设计:前 8 字节是一个小端整数,记着头部 JSON 的长度;紧接着的 JSON 里每个张量的 dtype、shape、offset 都在。想核对一个模型的规模,根本不用拉几十 GB 权重,读开头 1 MB 就够。
importjson,subprocess url="<权重分片的下载地址>"subprocess.run(["curl","-sL","--max-time","90","-r","0-1048575",url,"-o","head.bin"])blob=open("head.bin","rb").read()n=int.from_bytes(blob[:8],"little")# 头部 JSON 的长度head=json.loads(blob[8:8+n])# 每个张量的 dtype / shape / offset把 10 个分片的头都读一遍,逐张量把维度乘起来,得到的是这个模型的真实参数总量:
分片 model-00001-of-00010.safetensors 头部 623 个张量,参数 2313605504 头部 JSON 部分 83687 字节(前 8 字节记的就是这个长度),只读 1 MB 已经够 model.layers.2.feed_forward.expert_bias F32 [64] model.layers.3.feed_forward.expert_bias F32 [64]顺带说一句,头部里出现的 expert_bias 张量,跟模型公开配置里那个 use_expert_bias 开关是同一件事,后面会用到。
| 模型 | 分片数 | 文件头累加的参数总量 | 仓库索引里的权重字节数 ÷ 2 | 差值 |
|---|---|---|---|---|
| LFM2.5-24B-A2B | 10 | 23,971,587,712 | 23,971,590,144 | 2432 字节,占比 5e-7 |
| Qwen3-30B-A3B | 16 | 30,532,122,624 | 30,532,122,624 | 0 |
两条路径能对上,说明按文件头算账是可靠的。
激活参数怎么算:固定部分 + 被选中的专家
以 LFM2.5-24B-A2B 为例,40 层里有 2 层是稠密前馈,剩下 38 层是 MoE,每层 64 个专家、每个 token 取 4 个。单个专家是 gate/up/down 三个矩阵,3 × 2048 × 1536 = 9,437,184 个参数;38 层 × 64 专家乘起来正好是头部统计里的 22,951,231,488,一分不差。
激活参数的口径我是这么定的:每一层都要跑的部分(注意力、卷积块、稠密层、router、norm)全算,MoE 层只算被选中的 top-k 专家,embedding 与 lm_head 不按整表计入。按这个口径:
| 模型 | 总参数 | 专家参数占比 | 结构 | 激活参数 | 激活占比 | 总量 ÷ 激活 |
|---|---|---|---|---|---|---|
| LFM2.5-24B-A2B | 239.7 亿 | 95.7% | 38 层 × 64 专家 top-4 | 21.9 亿 | 9.2% | 10.9 |
| Qwen3-30B-A3B | 305.3 亿 | 95.0% | 48 层 × 128 专家 top-8 | 27.3 亿 | 8.9% | 11.2 |
算出来的 21.9 亿跟模型名里的 A2B 能对上,两个模型的激活占比也都落在 9% 上下,总参数是激活参数的 11 倍左右。这个 11 倍很容易被顺手读成"快 11 倍",但它只是参数账。
显存账和带宽账是两笔,别混
专家权重平时不参与计算,可它必须整份躺在显存里等着被选中,所以常驻显存看的是总参数;解码阶段每生成一个 token 要过一遍被激活的权重,速度看的是激活参数的搬运量。同一份权重换精度,两笔账一起缩:
| 精度 | LFM 常驻权重 | LFM 每 token 搬运 | Qwen3-30B-A3B 常驻权重 | 每 token 搬运 |
|---|---|---|---|---|
| bf16 | 47.94 GB | 4.385 GB | 61.06 GB | 5.461 GB |
| int8 | 23.97 GB | 2.193 GB | 30.53 GB | 2.731 GB |
| int4 | 11.99 GB | 1.096 GB | 15.27 GB | 1.365 GB |
"24B 模型能塞进单卡"这句话只在 int4 下成立:12 GB 权重加上 KV cache 和中间激活,才有余地。发布页称这类模型在 Apple M5 Max 上能跑到 184 token/s,这个数字我没法在本机复测。按 int4 每 token 搬 1.096 GB 反推,等效要 200 GB/s 量级的内存带宽——这类模型的速度主要由机器的带宽决定,跟参数总量不是一回事。
路由实测:专家负载不均会丢 token
拿不到线上真实的路由日志,我用本机已有的 176 万字符中文报告语料,按字符二元组哈希映射成 20 万个 token 来逼近真实 token 的出现频率;隐藏状态用 64 维随机投影模拟,router 是随机初始化矩阵。这个设置能复现"负载分布不均匀"这件事,但拿不到训练过的 router 的具体偏置程度,这点后面会说。
容量因子是生产里最关键的旋钮,定义为每个专家能接收的 token 上限除以平均期望值:
logits=x @ W.T idx=np.argpartition(-logits,k-1,axis=1)[:,:k]# 每个 token 选 top-k 专家load=np.bincount(idx.reshape(-1),minlength=E)# 每个专家收到多少 tokencap=math.ceil(k*n/E)*capacity_factor# 单专家容量上限drop=sum(max(0,l-cap)forlinload)/(n*k)# 被丢弃的 token 比例四次配置跑下来的真实输出:
语料字符数 1762928,模拟 token 数 200000,去重 token 值 23665 专家 16 top-2 | 最大/平均 1.37 | 最小/平均 0.58 | CV 0.196 | 丢弃率 cf1.0 6.90% cf1.25 1.65% cf1.5 0.00% cf2.0 0.00% 专家 16 top-4 | 最大/平均 1.20 | 最小/平均 0.85 | CV 0.096 | 丢弃率 cf1.0 4.01% cf1.25 0.00% cf1.5 0.00% cf2.0 0.00% 专家 64 top-4 | 最大/平均 1.51 | 最小/平均 0.43 | CV 0.272 | 丢弃率 cf1.0 11.49% cf1.25 2.57% cf1.5 0.04% cf2.0 0.00% 专家 128 top-8 | 最大/平均 1.83 | 最小/平均 0.35 | CV 0.310 | 丢弃率 cf1.0 12.32% cf1.25 4.30% cf1.5 0.68% cf2.0 0.00%| 配置 | 最大/平均 | 最小/平均 | 变异系数 | cf=1.0 | cf=1.25 | cf=1.5 | cf=2.0 |
|---|---|---|---|---|---|---|---|
| 16 专家 top-2 | 1.37 | 0.58 | 0.196 | 6.90% | 1.65% | 0.00% | 0.00% |
| 16 专家 top-4 | 1.20 | 0.85 | 0.096 | 4.01% | 0.00% | 0.00% | 0.00% |
| 64 专家 top-4 | 1.51 | 0.43 | 0.272 | 11.49% | 2.57% | 0.04% | 0.00% |
| 128 专家 top-8 | 1.83 | 0.35 | 0.310 | 12.32% | 4.30% | 0.68% | 0.00% |
两件事值得记一下。专家数从 16 涨到 128,最大专家与平均负载的比值从 1.20 走到 1.83——专家多并不自动均衡,反而更难摆平。容量因子按 1.0 给,64 专家那一档要丢掉 11.5% 的 token,等于十分之一的计算白花;给到 1.25 就降到 2.6%,1.5 基本归零。
专家偏置能把不均衡压下去
LFM 的配置里带着 use_expert_bias,权重头里也能看到每层一个长度为专家数的偏置向量。它的思路是给 router 的 logits 加一个可修正的偏移,谁的负载低于平均就抬一点。我在同一批 token 上试了一遍:前 10 万个 token 校准偏置,后 10 万个 token 做测试。
偏置修正前后(64 专家 top-4,同一批测试 token): 修正前 最大/平均 1.82 CV 0.339 丢弃率 cf1.25 6.11% 修正后 最大/平均 1.18 CV 0.058 丢弃率 cf1.25 0.00%最大/平均从 1.82 压到 1.18,变异系数从 0.339 掉到 0.058,容量因子给 1.25 时已经不再丢 token。这一招几乎不增加推理成本,比单纯把容量因子加大划算——后者会让所有专家的矩阵乘法一起变宽,空转的算力也得算钱。
我怎么用它
挑模型时我现在先抄三行数字:总参数、激活参数、专家数与 top-k。第一行除精度字节得常驻显存,第二行乘精度字节得每 token 搬运量,第三行决定容量因子要给多少余量。这三行一列,机器能不能装、装完有多快,心里就八九不离十了。
踩过的坑留两个。第一次读文件头我用的 urllib 自己拼 Range 头,服务端不返回响应体,直接挂到超时;换成 curl 的-r 0-1048575拿到 206,同样的地址一次就成,1 MB 就够。第二个是容量因子给太紧:我先按 1.0 跑,64 专家那一档丢了 11.5% 的 token,而丢掉的那些 token 会直接进困惑度,不是"少算一点"这么轻。
要说清不确定的地方:我这是随机初始化的 router 加哈希出来的 token 分布,能复现负载不均这个现象、也能量出偏置修正的收益量级,但真实模型训练时本身就在压均衡,偏置幅度会比这个轻,具体数值不能直接搬。另外"激活参数小所以省显存"这个想法是错的——能省的只有精度,专家权重一个都省不掉。
三条可落地的动作
- 拿到新模型先读它的公开配置和权重文件头,把总参数、专家数、top-k、单专家矩阵维度抄成一张表,再决定存成几比特。
- 部署估算分两个量走:常驻显存 = 总参数 × 精度字节,每 token 搬运 = 激活参数 × 精度字节,不要用一个数字拍全部。
- 自己加 MoE 层的话,容量因子从 1.25 起步,同时打开专家偏置修正;先把负载统计跑出来,再谈加专家数。