面向高效推理的 MoE 专家压缩,合并与剪枝的协同设计与工程实践

0 阅读17分钟

​

 本文整理自 26 年 9 月 22 日 SGLang Meetup 北京站活动的同名主题演讲。

在技术站公众号回复「百度百舸 2609」,可以获得此次 Meetup 下午的 3 个演讲主题材料。


各位嘉宾、各位社区的开发者,大家好。

今天想和大家分享我们在 MoE 专家压缩方向上的一些思考和实践。

MoE 模型通过 router 为每个 token 选择少量专家参与计算,在扩大模型参数规模的同时,把单 token 的计算量控制在相对可接受的范围。但进入实际推理部署之后,我们发现一个很直接的问题:计算已经稀疏了,专家权重的存储却没有同步稀疏。

以 Qwen3-30B-A3B 为例,每个 token 只激活 8 个专家,但在 GPU-resident 的部署方式下,128 个 routed experts 仍然需要保持可访问。也就是说,top-k 可以降低每个 token 的计算量,但并不会自然减少 GPU 上需要随时可访问的 expert weights。随着模型规模继续扩大,这部分常驻权重会直接挤占 KV cache 和服务并发所需要的显存空间。

这里需要先区分三个概念:每个 token 实际激活多少个专家,决定 active compute;模型一共保留多少个 routed experts,决定主要的专家权重存储;最终的 serving 收益,则还取决于 working set、KV capacity 和具体 workload。我们这次压缩的是全局 expert count,而不是每个 token 的 active expert count。

这次分享主要围绕三个问题展开:为什么要压缩专家、PRIME 如何联合做剪枝与合并,以及压缩后的模型怎样真正落到 serving 和工程工具链中。

图片​

编辑

1.     为什么要压缩 MoE 专家

以 Qwen3-30B-A3B 为例,每层有 128 个 routed experts,而每个 token 只激活 8 个。从计算视角看,这是非常典型的稀疏 MoE:一个 token 只需要进入少量专家。

但从存储视角看,情况并不一样。下一批 token 可能被路由到任意专家,因此在 GPU-resident 的部署方式下,每层的 routed experts 仍然需要保持可访问状态。在单卡 80GB、bf16 的配置中,routed expert 权重约占 56.9 GiB,留给 KV cache 的空间约为 11.2 GiB,对应大约 12 万 token。

量化、offload、TP / EP 都可以缓解问题,但它们分别改变的是位宽、驻留方式或参数放置,并不会直接减少全局 expert count。要真正降低这一部分常驻权重,更直接的方法是把每层的 routed experts 从 N 个压缩到 K 个。

因此我们希望解决的问题是:能否在无需梯度训练的条件下,把 N 个 routed experts 压缩成 K 个有效专家,同时尽量保持模型能力?

图片​

要回答这个问题,第一步是确认模型内部是否真的存在足够的专家冗余。

我们在 Qwen3-30B-A3B 上使用 C4 calibration 数据分析专家的实际行为。按照图中所示的校准配置,我们统计了 48 层专家的路由频率和输出相似性,观察到两类比较明显的冗余。

第一类是低频冗余。从 48 层平均的激活频率来看,大约 24.5% 的专家激活频率接近 0。它们虽然长期占据参数和显存,但主要路由路径很少经过这些专家,因此具备直接剪枝的空间。这里的 24.5% 是当前 calibration 数据和统计口径下的结果,并不意味着换一份数据后比例一定完全相同。

第二类是相似冗余。从专家输出的相似度来看,可以看到比较明显的成组结构。以 Layer 0 为例,最大的相似组包含 40 个专家,不少专家之间的相似度超过 0.5。这类专家如果直接删除,可能会损失已经学习到的信息;更自然的处理方式是把相似专家合并起来。

这两类冗余分别对应了两种压缩手段:低频、离群的专家更适合剪枝,高相似专家更适合合并。

图片​

2.    PRIME:让剪枝和合并成为同一个决策问题

现有专家压缩方法大致可以分成两条路线:一种是直接删除部分专家的剪枝(pruning),另一种是将相似专家的信息聚合起来的合并(merging)。两种路线虽然都旨在减少专家数量,但各自面临不同的问题。

对于只做剪枝的 prune-only 方法,每个 expert 只有「保留」或「删除」两种选择。这种方式适合处理低频、贡献较小的专家,但如果两个专家高度相似,直接删除其中一个仍可能造成有价值的信息损失。

而对于只做合并的 merge-only 方法,每个 expert 都必须被划入某个 cluster,再在簇内进行信息聚合。它能够较好地保留相似专家的信息,但对于低频、离群的专家来说,「必须加入某个簇」未必合理,强行合并反而可能带来表征污染。

这也是我们选择 PRIME 的核心原因。 我们观察到,MoE 模型中并不是只有一种专家冗余:低频、离群的专家更适合直接剪枝,高度相似的专家则更适合合并;而且不同 layer 的冗余结构也不一样。如果预先规定所有专家只能剪枝,或者都必须参与合并,就无法同时适应这些不同的情况。

因此,我们希望让每个专家根据自己的激活频率和相似关系,在保留、合并和剪枝之间选择更合适的处置方式。这正是 PRIME 的出发点:如果「怎么处置这个专家」本身也是一个决策变量,会怎么样?

PRIME 把 retain、merge、prune 放进同一个解空间,让求解器根据每个专家的频率与相似关系,为它分配更合适的处置方式。

接下来,我们具体看一下 PRIME 是怎样完成这个决策的。

图片​

PRIME 的整体流程可以概括成三个阶段:校准画像 → 统一 ILP 建模求解 → 频率加权聚合。

首先,通过 calibration forward 收集两类信息:一是每个 expert 的路由频率,二是 expert 之间的输出距离。随后把聚合中心、合并归属和剪枝对象放进同一个整数线性规划,也就是 ILP 中统一求解。

其中,合并代价可以理解「频率 × 距离」,剪枝代价则与「频率 × λ」相关。这样的设计会自然产生三类结果:

  • 高频 expert 的错误处置代价更高,因此更适合作为聚合中心;
  • 低频但与中心很接近的 expert,合并代价较小,可以通过频率加权平均保留信息;
  • 低频且离群的 expert,如果合并代价高于剪枝惩罚,就直接剪除。

最终,求解器只输出一个统一的中间表示 group_labels:非负 label 表示专家被归到哪个 group,-1 表示该专家被剪除。这样,后续 tensor rewrite 只需要消费这一份结果,不需要感知 ILP 内部是怎么求解的。

图片​

专家做完剪枝和合并之后,还有一个关键问题:router 如何一起压缩?

对于纯剪枝,处理相对直接。把被剪 expert 对应的 router row 删除,剩余权重仍然构成一个标准的 K-way linear router。

但 merge 不一样。若要精确保留原始 group routing,需要先计算原来的 N-way routing probability,再把属于同一 group 的概率相加。等价地看,合并后的 grouped logit 是多个原始 logit 经过 log-sum-exp 聚合得到的函数,它对输入 hidden state 不再是简单的线性映射。因此,直接把几行 router 权重做平均,通常无法得到严格等价的 K-way linear router。

PRIME 的处理方式是在 calibration 阶段进行一次 Router Reconstruction:使用 hidden states 和 grouped logits,通过 ridge regression 的闭式求解拟合出一个标准 K × hidden 的线性 router。

这样压缩完成以后,线上仍然是普通的 K-way router,最终输出标准 Hugging Face checkpoint。整个过程不需要 backward,也不需要 optimizer。

图片​

从实际模型的结果来看,不同 layer 的冗余结构差异很明显,因此它们需要的 prune / merge 比例也并不相同。

在 Qwen3-30B-A3B、K=96、expert count 压缩 25% 的实验中,6~11 层更偏向剪枝,而 24~35 层几乎全部以合并为主;从整个模型汇总来看,大约是 73% 合并、27% 剪枝。

这说明,如果事先规定所有层只能 prune,或者所有层只能 merge,本质上是在强制冗余结构不同的 layer 使用同一种策略。

联合 ILP 的另一个优势是,整层的决策可以一起调整。某个 expert 既可以成为聚合中心,也可以被改派到更合适的中心,还可以直接被剪除;求解器会围绕整层总目标进行统一优化,而不是在局部贪心决策以后就无法回退。

消融实验也显示,联合决策和频率权重都提供了独立贡献。

图片​

2.1.    压缩之后,模型能力和部署收益如何?

这一部分我们分别看两个问题:第一,压缩之后模型能力损失有多大;第二,专家数量减少之后,显存、KV capacity 和 Serving 性能能得到什么收益。

首先看模型能力。

我们在 Qwen3-30B-A3B 和 Qwen3.5-122B-A10B 上测试了主流数学与代码任务。在当前评测集和 calibration 配置下,当 routed expert count 减少 25%  时,代码与数学平均分的绝对下降大约在 1 个百分点;当压缩比例进一步提高到 50% 时,平均绝对下降约为 3 个百分点。

这说明在当前评测条件下,25% expert-count reduction 是一个相对温和的区间:能够显著减少 routed experts,同时基本保持代码和数学能力。当然,这里反映的是当前模型、任务和 calibration 配置下的结果,不应理解为所有模型上的固定规律。

需要说明的是,这里的 compression ratio 定义为 1 - K/N,表示每层 routed-expert count 的下降比例,并不等价于整模型参数量、显存占用或端到端延迟同比例下降。

图片​

接下来再看性能。

一个容易产生误解的地方是:expert count 下降之后,每个 token 的 active FFN compute 基本不变。

原因很简单,num_experts_per_tok 仍然是 8,一个 token 实际执行的 expert 数没有变化;KV cache 每 token 的大小也不会随着专家总数减少而变化。

因此,专家压缩的 serving 收益主要来自两条通道。

第一条是 expert working-set / 访存收益。同一个 decode step 中,并发 token 越多,需要触达的 unique experts 通常越多。expert 总数下降后,unique-expert working set 随之缩小,需要从 HBM 访问的 expert weights 也会减少。

第二条是 KV-capacity 收益。专家权重减少后,释放出来的显存可以转化为更大的 KV pool,为更长上下文和更高并发提供空间。

这两条通道可以分别测量。在当前实验的近似归因下,它们可以共同解释最终收益;但实际收益仍然取决于并发量、序列长度和部署配置。

图片​

从单卡显存账来看,这种变化是非常直接的。

在 TP1、A800-80GB、bf16、SGLang 的配置下,我们比较 e128 原模型和 e64 压缩模型。expert weights 从 56.9 GiB 降到 29.9 GiB,大约减少 27 GiB。固定 --mem-fraction-static=0.90,并保持模型副本数、TP 配置和其他运行时开销基本不变时,这部分释放出来的显存大部分可以转化为可分配的 KV pool,KV capacity 从约 12.2 万 token 增加到 41.7 万 token,约为原来的 3.42 倍。

如果按 16K context 粗略换算,可完整容纳的并发数量从约 7 条提升到 25 条。具体数值还会受到运行时开销和分页策略影响。

这里更重要的意义不是简单得到一个「显存节省比例」,而是把原来被 expert weights 占据的空间释放出来,重新变成服务可以使用的上下文与并发余量。

图片​

在长输出、高并发的实验中,两条收益通道会同时体现出来。

在相同 TP1、A800-80GB 和 SGLang 配置下,我们比较 e64 压缩模型和 e128 原模型。这里 e64 表示 routed expert count 从 128 降到 64。在 512 token 输入、4096 token 输出、并发 128 的场景下,压缩模型的输出吞吐最高实测达到原模型的 2.23×。

为了拆解这部分收益,我们使用了自由 KV、固定 KV 和 TP2 三组实验进行归因。在这组实验设置下,working-set 通道约对应 1.32×,另一条 KV-capacity 通道约对应 1.68×;两者的归因乘积约为 2.22×,与实测峰值 2.23× 基本一致。这个结果用于解释当前 workload 下的收益来源,并不意味着两个因素在所有部署配置下都能严格独立相乘。

同时也可以看到,如果 workload 主要由 prefill 主导,因为 FLOPs 本身并没有随着 expert count 下降,相应收益会明显减弱。

图片​

进一步把并发从 1 扫到 256,可以看到性能收益的变化规律更加清晰。

在当前实验配置下,当 C=1 时基本没有可见的吞吐收益。单请求每一步本来就只访问 8 个 expert,working-set 没有额外可省的权重。

当并发达到 C≥4 后,working-set 收益开始出现;随着 batch 中 token 数继续增加,unique expert 占比发生变化,压缩后需要触达的 HBM 权重减少,吞吐收益逐步显现。

对于长输出、高并发的 workload,两条通道能够进一步叠加。相比之下,短序列场景更难充分利用额外的 KV capacity,收益也会更早趋于稳定。

因此,专家压缩带来的 serving gain 本质上是 workload-dependent 的:不能脱离具体的输入输出长度、并发以及部署配置,单独用一个固定加速倍数描述所有场景。

图片​

这里还需要注意一个测量边界:容量受限时,表面上的性能对比可能失真。  如果原模型在某个并发或上下文配置下已经受到 KV capacity 限制,那么它可能无法运行相同的请求规模,或者需要借助 TP2 等方式才能完成对比。此时,观测到的差异同时包含了容量约束、并行方式和通信开销,不能简单归因于专家压缩本身。

所以在评估 serving 收益时,需要同时报告模型、硬件、TP / EP 配置、输入输出长度、并发和 KV 设置,并区分「相同可运行条件下的吞吐收益」与「释放显存后新增的容量收益」。

图片​

3.    从算法实验到可部署压缩工具链:moe-compressor

为了让专家压缩不只停留在算法实验阶段,我们进一步把整个过程做成了 moe-compressor, 把配置、calibration、压缩决策、tensor rewrite、模型保存与评测串成一条标准化 pipeline。

整体架构里,决策逻辑与 tensor rewrite 被明确解耦。跨模块只保留三个固定接口:

  • CompressionRequest:把 CLI、recipe、Python API 三种入口统一成同一个冻结请求;
  • BackendPlan:modifier 编译得到的执行计划,算法只描述决策,不直接操作模型 tensor;
  • group_labels:唯一的压缩中间表示,非负 label 表示 group,-1 表示剪除。

整套工具链可以分成两个平面。控制 / 决策平面负责配置解析、组件校验和 ILP 求解,不需要 GPU;执行平面负责 calibration forward、统计收集、张量改写与保存。算法只需要产出 group_labels,不同模型族只负责实现自己的 model_ops 与 grouper。

这样做的价值在于:算法、模型结构和执行逻辑可以独立演进,不需要把每一种压缩方法和每一种模型结构写成一套彼此耦合的实现。

图片​

用户侧的使用方式也尽量保持简单:一个 recipe 加一条命令,就能生成标准 Hugging Face checkpoint。

例如,一个压缩任务可以由如下 recipe 描述:

{
  "model": { "name": "Qwen3-30B-A3B", "path": "…" },
  "output_dir": "…/qwen3-30b-a3b-prime-r025",
  "modifier": {
    "name": "prime",
    "compress_ratio": 0.25,
    "prune_penalty": 0.25
  },
  "calibration": { "name": "C4", "n_samples": 32 }
}

然后执行:


moe-compressor compress \
  --recipe examples/recipes/qwen3-30b-a3b-prime.json \
  --compressed-model-path ./out/qwen3-prime-r025

这里不需要逐项理解工具内部的实现。用户只需要在 recipe 中指定模型、压缩比例、剪枝惩罚和 calibration 配置,再执行一条压缩命令即可。

输出仍然是标准 HF checkpoint:config.json 中的 num_experts 已经变成压缩后的 K,同时包含正常的 safetensors 分片与 tokenizer 文件。后续可以直接 from_pretrained,也可以交给 SGLang 启动服务。

配置既可以通过 recipe 给出,也可以直接使用 CLI 参数;两者混用时,显式 CLI 参数优先覆盖 recipe 中的同名字段。无论入口来自 CLI、recipe 还是 Python API,后续流程最终都会归一成同一个 CompressionRequest。

图片​

在扩展性方面,moe-compressor 用 compress、eval、list 三个子命令覆盖从压缩到评测的主要流程,同时把算法、模型族、校准集和评测集统一从 registry 接入。

新增算法时,主要实现 modifier,并为支持的模型族注册 compatibility entry,物理 tensor rewrite 仍然复用现有 model_ops;新增模型族时,实现 ModelAdapter + model_ops + grouper,CLI、recipe、session、保存与评测都可以复用。

新的 calibration 数据集只需要描述 name / alias、文件 schema 和文本转换;新的 eval set 则在 EvalSuite 中登记 runner 能识别的任务名即可。

整个设计仍然围绕同一个原则:决策逻辑和 tensor rewrite 解耦。  这样才能让新的压缩算法、新模型结构和新的评测任务以相对独立的方式持续扩展。

图片​

回到最开始的问题,我们可以把这次分享归纳成三个结论。

第一,Sparse compute ≠ sparse memory。 GPU-resident MoE 中,top-k 可以降低单 token 的计算量,但不会同步减少 expert-weight residency。要真正减少常驻权重,需要直接降低 routed-expert count。

第二,PRIME jointly decides retain / merge / prune。 模型内部同时存在低频冗余和相似冗余,不同 layer 的冗余结构也不相同。把 retain、merge、prune 放进统一的优化空间,可以让每层根据自身特征自适应选择处置方式。在当前实验中,25% expert-count reduction 基本保持了代码和数学能力。

第三,Serving gain is workload-dependent。 expert count 下降并不会直接减少每 token 的 active FFN FLOPs;收益主要来自 working-set 与可用显存空间的变化,因此会随着输入输出长度、并发和部署配置而变化。

从专家冗余分析,到 PRIME 的联合决策,再到标准 Hugging Face checkpoint 的工程交付,我们希望解决的是同一个问题:让 MoE 的存储效率更好地匹配它已经具备的计算稀疏性。

如果要把这套方法用于一个新的 MoE 模型,比较稳妥的路径是:先在目标 workload 上分析专家的激活频率和相似性,再从相对温和的压缩比例开始验证能力,最后分别测量 working set、KV capacity 和端到端吞吐,而不是只看一个固定的加速数字。

图片​

今天我的分享就到这里,谢谢大家。

​