Transformer中的混合专家架构详解
Hugging Face深度讲解MoE架构,这是现代LLM核心优化技术,对做AI应用和模型优化的程序员价值大。
Hugging Face深度讲解MoE架构,这是现代LLM核心优化技术,对做AI应用和模型优化的程序员价值大。
过去几年里,扩展稠密语言模型一直是推动 LLM 取得大部分进展的主要动力。从早期的原始 ULMFiT(约 3000 万参数)或 GPT-2(15 亿参数,当时被认为“发布出来过于危险” 🧌),一直到今天拥有数千亿参数的系统,其方法很简单:
更多数据 + 更多参数 = 更好的性能。
扩展定律进一步强化了这一趋势,但稠密扩展存在实际限制:
训练成本越来越高。
推理延迟不断增加。
部署需要大量内存和硬件资源。
这正是混合专家模型(MoE)发挥作用的地方。
如果你已经熟悉 MoE,并希望直接了解 transformers 中完成的工程工作,可以直接跳转到「Transformers 与 MoE」。
混合专家模型保留了 Transformer 主干,但会用一组专家替换其中某些稠密前馈层。“专家”并不是针对某个主题特化的模块(例如“数学专家”“代码专家”),它只是一个可学习的子网络。对于每个 token,路由器会选择一小部分专家来处理它。
不同的 token 会根据各自的隐藏表示激活不同的专家。
模型容量取决于参数总量,而推理速度取决于活跃参数量。
这就是关键所在。
以 gpt-oss-20b 为例。它共有 210 亿参数,总计 32 个专家,但每个 token 只会使用其中 4 个活跃专家。将共享组件与活跃专家计算在内,该模型处理每个 token 时使用约 36 亿个活跃参数。在一台内存带宽约为 800 GB 的 M3 Ultra Mac 上运行该模型时,我们可以估算,在每个参数占用 2 字节的 bfloat16 格式下,其生成速度约为 800 / (3.6 * 2)。结果约为每秒 111 个 token。我们实际测得的性能约为 115 tok/s,与这一粗略估算非常接近。
如此快的速度证实,该模型运行时的表现大致相当于一个 36 亿参数的模型,但它拥有与 210 亿参数模型相同的容量(或质量)。
(注意:如果使用该模型原生采用的 mxfp4 量化格式所对应的内核,速度还会更快。)
MoE 之所以具有吸引力,原因如下:
在训练 FLOP 预算固定的情况下,MoE 的表现往往优于对应的稠密模型。
图 2:稠密模型与 MoE 的训练曲线对比(来源:OLMoE: Open Mixture-of-Experts Language Models)
这意味着更快的迭代速度和更高的扩展效率。
在训练 FLOP 预算固定的情况下,MoE 的表现往往优于对应的稠密模型。
这意味着更快的迭代速度和更高的扩展效率。
专家在计算图中提供了结构边界。由于不同 token 会调用不同的专家,我们可以跨专家进行并行化(稍后会在「专家并行」中讨论这一点)。
专家在计算图中提供了结构边界。由于不同 token 会调用不同的专家,我们可以跨专家进行并行化(稍后会在「专家并行」中讨论这一点)。
过去几周发布的主要开源 MoE 模型包括 Qwen 3.5、MiniMax M2、GLM-5 和 Kimi K2.5。
2025 年 1 月 DeepSeek R1 取得成功后,这一趋势开始加速;它建立在 DeepSeek V2 等更早期系统的基础之上。另一个较早的 MoE 是 2023 年 12 月发布的 Mixtral-8x7B。
图 3:两年间 transformers 库新增 MoE 模型的时间线。DeepSeek R1 标志着一个明显的转折点。
闭源实验室也在使用 MoE。长期以来,一直有传言称 ChatGPT 使用了稀疏架构,而开源的 gpt-oss 模型则确定采用了这种架构。
过去几周发布的主要开源 MoE 模型包括 Qwen 3.5、MiniMax M2、GLM-5 和 Kimi K2.5。
2025 年 1 月 DeepSeek R1 取得成功后,这一趋势开始加速;它建立在 DeepSeek V2 等更早期系统的基础之上。另一个较早的 MoE 是 2023 年 12 月发布的 Mixtral-8x7B。
闭源实验室也在使用 MoE。长期以来,一直有传言称 ChatGPT 使用了稀疏架构,而开源的 gpt-oss 模型则确定采用了这种架构。
如果你想进一步了解 MoE,我们强烈建议阅读这篇博客,并观看我们最近发布的路由主题 YouTube 视频。
生态系统中的大多数工具,包括模型加载、设备放置、量化和后端执行,最初都是为稠密模型设计的。MoE 对这些假设提出了挑战。
要让 MoE 成为 transformers 中的一等公民,并不只是添加新的模型类,还意味着需要重新设计加载流水线、执行模型和分布式抽象的部分机制。我们将重点介绍 transformers 库为了支持稀疏架构,在以下方面所做的演进:
权重加载重构
使用 transformers 训练 MoE
AutoModelForCausalLM.from_pretrained("model_id") 会下载模型权重,并将其加载到 PyTorch 模型中。对于稠密模型,加载过程相对简单:检查点中的每个张量都会与运行时模块中的某个参数一一对应。
对于 MoE,情况则更加复杂。在大多数 MoE 检查点中,每个专家都会被单独序列化。如果查看 DeepSeek-V3 的检查点索引,你会看到如下键:
model.layers.3.mlp.experts.0.gate_proj.weight
...
model.layers.3.mlp.experts.255.gate_proj.weight
每个专家都有自己的一组权重矩阵,本质上就是将 256 个(以 DeepSeek-V3 为例,编号从 0 到 255)小型前馈网络并排保存。然而在运行时,GPU 执行的是经过优化的内核。现代 MoE 内核,例如分组 GEMM 和融合 MoE 实现,旨在通过单次操作处理所有专家,而不是逐个循环处理。
为了高效完成这一操作,它们要求将专家权重打包到一个连续张量中。
因此,我们面临着不匹配:
检查点:256 个独立张量
运行时:1 个打包张量
系统化地弥合这一差距,正是权重加载重构所实现的能力。
引入通用的 WeightConverter 后,其思维模型从:
检查点已经与我的运行时布局匹配;加载基本上只是逐键复制。
转变为:
检查点只是一个经过序列化的张量来源。加载是一个转换流水线,负责将这些张量转换成我们所需的运行时布局。
此次重构引入的核心抽象,是通过 WeightConverter 实现动态权重加载。
WeightConverter 允许我们定义:
source key patterns → target key(s) + operations
基础操作(分块、拼接等)可以组合使用。其中有两个操作对 MoE 尤其有用:
MergeModulelist 将张量列表合并为单个张量。例如,可以将 MergeModulelist 与 Concatenate 组合起来,堆叠 MoE 中的专家,并将它们打包到一个张量中。WeightConverter( ["block_sparse_moe.experts.*.w1.weight", "block_sparse_moe.experts.*.w3.weight",], "mlp.experts.gate_up_proj", operations=[ MergeModulelist(dim=0), Concatenate(dim=1), ], )
MergeModulelist 将张量列表合并为单个张量。例如,可以将 MergeModulelist 与 Concatenate 组合起来,堆叠 MoE 中的专家,并将它们打包到一个张量中。
WeightConverter(
["block_sparse_moe.experts.*.w1.weight", "block_sparse_moe.experts.*.w3.weight",],
"mlp.experts.gate_up_proj",
operations=[
MergeModulelist(dim=0),
Concatenate(dim=1),
],
)
SplitModulelist 将一个张量拆分回张量列表。例如,可以将堆叠在一起的专家重新拆分为各个独立专家。WeightConverter( "mlp.experts.down_proj", "block_sparse_moe.experts.*.w2.weight", operations=[SplitModulelist(dim=0)], )
SplitModulelist 将一个张量拆分回张量列表。例如,可以将堆叠在一起的专家重新拆分为各个独立专家。
WeightConverter(
"mlp.experts.down_proj",
"block_sparse_moe.experts.*.w2.weight",
operations=[SplitModulelist(dim=0)],
)
此次重构不仅改进了可用的转换操作,还改进了这些操作的调度方式。
加载器只扫描一次检查点中的键,将它们与转换器模式进行匹配,并按转换器对张量进行分组。某个键一旦被确定为必需,就会被注册为 future,并通过线程池进行实例化。只有当依赖项准备就绪后,转换操作才会运行。例如,MergeModulelist 会等待某一层的所有专家全部加载完成。
这样可以避免重复扫描,并降低内存峰值。
为了评估新权重加载流水线带来的改进,我们对 transformers 的 v4 与 v5 版本进行了基准测试。测试重点是大型 MoE 模型的加载速度,因为这一过程通常是训练和推理中的瓶颈。
我们使用以下分支对 v4 与 v5 进行了基准测试:
v4 分支:https://github.com/ariG23498/transformers/tree/bench-v4
from transformers import AutoModelForCausalLM
model_id = "Qwen/Qwen1.5-110B-Chat"
model = AutoModelForCausalLM.from_pretrained(model_id)
两个相关的环境变量:
HF_ENABLE_PARALLEL_LOADING: 通过线程启用并行分片加载。
HF_ENABLE_PARALLEL_LOADING: 通过线程启用并行分片加载。
HF_DEACTIVATE_ASYNC_LOAD: 禁用新的异步管道(v5 逃生舱)。
HF_DEACTIVATE_ASYNC_LOAD: 禁用新的异步管道(v5 逃生舱)。
模型:Qwen/Qwen1.5-110B-Chat GPU: 1× A100 (80GB)
加速不仅仅是"更多线程"。
这是单次路由、异步物化和转换感知调度的组合,它们共同避免了不必要的物化和内存峰值,同时在加载时启用专家打包和投影融合。
通过这次重构,我们现在可以先创建运行时模块结构,然后将权重转换到该结构。我们现在可以选择在转换管道中附加量化,使量化成为权重加载管道本身的一部分。这至关重要,因为"按专家"量化只有在专家以可预测的打包布局存在时才有意义。
这个端到端管道之前不可能实现,现在它作为暴露的 API 提供给用户。
一旦专家被打包到单个运行时张量中,就会产生另一个问题:
你实际上如何高效地通过它们进行路由?
在混合专家模型中,每个 token 被路由到不同的专家。这意味着运行时必须将 token 分派到其选定的专家权重,高效地执行投影,应用路由权重,然后收集和重新排序结果。
这是专家后端系统(在 PR #42697 中引入)要解决的问题。专家后端引入了一个可插接的执行架构,将专家计算与模型实现解耦。与其在每个 MoE 模型内部硬编码一个分派策略,该系统允许专家层在运行时动态选择后端。
这是通过装饰器模式实现的:
@use_experts_implementation
装饰器包装专家类,并自动将计算分派到选定的后端。
目前提供了三个后端:
eager 循环遍历选定的专家并对每个专家应用投影。这用于正确性参考和调试。
eager 循环遍历选定的专家并对每个专家应用投影。这用于正确性参考和调试。
batched_mm 使用 torch.bmm API。这为每个 token 复制选定的专家权重,并执行单个批处理 GEMM。这个后端非常适合内存充足的小批次、GPU 密集型工作负载。
batched_mm 使用 torch.bmm API。这为每个 token 复制选定的专家权重,并执行单个批处理 GEMM。这个后端非常适合内存充足的小批次、GPU 密集型工作负载。
grouped_mm 使用 torch._grouped_mm API。这里我们按专家 ID 对 token 进行排序、分组,然后执行单个分组 GEMM。这个后端在大批次或内存受限的设置中表现出色。
grouped_mm 使用 torch._grouped_mm API。这里我们按专家 ID 对 token 进行排序、分组,然后执行单个分组 GEMM。这个后端在大批次或内存受限的设置中表现出色。
混合专家(MoE)模型可以拥有数百亿个参数(远远超过单个 GPU 可以容纳的参数)。专家并行(EP)通过在多个设备间分布专家来解决这个问题。每个设备仅加载其分配的专家子集,对这些专家进行计算,然后参与结果聚合。这种方法将模型扩展到更大的参数数量,而不增加计算成本,因为每个 token 仅激活少数几个专家。
专家并行通过 enable_expert_parallel 启用:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.distributed.configuration_utils import DistributedConfig
distributed_config = DistributedConfig(enable_expert_parallel=True)
model = AutoModelForCausalLM.from_pretrained(
"openai/gpt-oss-120b",
dtype="auto",
distributed_config=distributed_config,
)
torchrun --nproc-per-node N script.py
其中 N 均匀整除专家总数,并可能与节点中的 GPU 数量相匹配。
当 enable_expert_parallel=True 时,模型从标准张量并行(TP)方案切换到专家并行(EP)方案,使用专门的分片策略。
EP 的核心组件包括:
GroupedGemmParallel: 这沿专家维度(dim=0)分割专家权重。每个设备仅加载 num_experts / num_devices。
GroupedGemmParallel: 这沿专家维度(dim=0)分割专家权重。每个设备仅加载 num_experts / num_devices。
RouterParallel: 这将全局专家索引重新映射到本地索引,掩盖分配给当前 rank 的专家之外的专家,确保每个设备仅使用其本地专家进行计算,并使用全归约来合并跨设备的部分输出。
RouterParallel: 这将全局专家索引重新映射到本地索引,掩盖分配给当前 rank 的专家之外的专家,确保每个设备仅使用其本地专家进行计算,并使用全归约来合并跨设备的部分输出。
MoEs 在扩展推理方面表现出色,但训练它们要复杂得多。
MoEs 拥有庞大的参数数量,分布式专家通信很复杂,存在需要处理的路由不稳定性。为了解决这个问题,我们与 Unsloth 合作,实现了显著更快的混合专家训练:
~12× 更快的 MoE 训练
与 v4 相比总体加速 12-30×
我们利用专家后端抽象,围绕 PyTorch 的 torch._grouped_mm API 进行标准化,并使用自定义 Triton 分组 GEMM + LoRA 内核。Unsloth 建立在 Transformers(和 TRL)优化的基础之上,进一步提升性能。
有关完整详情,我们推荐阅读:Unsloth 的官方指南
随着稀疏架构的不断演进,我们希望 transformers 库也随之发展。如果你正在使用 MoEs 进行构建或实验新的稀疏想法,我们很乐意听到你的声音。让我们知道你希望在 transformers 中看到什么样的抽象、内核或工作流。
本文提及的模型 6
本文提及的论文 2
本文提及的集合 5
来自我们博客的更多文章
原生速度 vLLM transformers 建模后端
在连续批处理中解锁异步性
enable_expert_parallel 标志将 GroupedGemmParallel + RouterParallel 的复杂性隐藏在单个配置后面,这是一个极好的开发体验——将专家分布到设备上曾经需要大量自定义的管道代码。
· 登录或注册以评论
本文提及的模型 6
本文提及的论文 2
本文提及的集合 5