1. MoE到底是什么:把门控路由和稀疏激活掰开揉碎
先说结论:MoE全称Mixture of Experts,混合专家模型,它不是某个具体的网络结构,而是一种**“把大模型拆成多个小网络,然后按输入动态挑几个来干活”**的架构范式。这也是我看AI Infra相关源码和论文时第一个觉得“原来如此”的地方,因为MoE的底层逻辑并不复杂,但它的工程细节比想象中多得多。
大模型时代大家追求的是“参数越多能力越强”,但参数越多,计算量也跟着涨。Transformer里最耗算力的其实是FFN(前馈网络)那一层,大概占整个模型计算量的三分之二。于是有人想了个办法:能不能把FFN复制很多份,变成一堆“专家”,每次只根据当前输入挑最合适的少数几个专家来算?这样参数量上去了,计算量却没怎么涨。这就是MoE的核心动机。
为什么说它“稀疏”?因为虽然模型总参数可能有几百B甚至上千B,但一次前向推理,每个Token实际只激活了其中一小部分参数。比如一个8x7B的MoE模型,总参数56B,但每个Token只走2个专家,实际参与计算的参数大约是14B,这就是典型的“稀疏参数、稠密计算”。
这种设计带来的直接好处有两个:一是在同样算力预算下,模型可以拥有更多参数,学习更多知识;二是理论上推理吞吐会更高,因为FLOPs(浮点运算量)并没有随参数量线性增长。MoE也因此成了当前大模型做大参数的主流方案之一,我接触到的几个千亿级模型,无一例外都带了MoE层。
写这一篇笔记的时候,我的核心目标是梳理清楚MoE从“设计动机”到“计算过程”再到“工程落地”的完整链路,重点讲清楚一个Token在一层MoE里到底经历了什么。适合正在做AI Infra、模型训练推理优化、或者刚开始研读大模型源码的同学,读完你会对“路由”“稀疏”“专家并行”这些词不再感到模糊。
1.1 从“一个人干活”到“一组专家分工”——MoE的设计动机
理解MoE,最直观的类比是公司里的分工。传统Transformer的FFN相当于一个“什么都会一点的全能员工”,无论来什么活儿都自己干。而MoE则是请了一群“领域专家”,有人擅长数学题,有人擅长代码,有人擅长文学润色,来了一个任务后,先让“调度员”判断该找谁,然后把任务分给最合适的几个专家处理。
这个“调度员”在MoE里叫Router或Gating Network(门控网络),是整个MoE架构里最关键的组件。Router本质上是一个线性层,输入是Token的隐层向量,输出是每个专家的一个分数,表示这个专家“适合处理这个Token”的程度。
这里有个容易混淆的地方:Router的知识和专家的知识是分开训练的。Router只学“该把谁分给谁”,专家只学“被分到的任务该怎么做好”。两者协同进化,最终达到整个模型的损失最小化。
从计算角度看,MoE层可以形式化地写成这样:
假设有 ( N ) 个专家 ( E_1, E_2, ..., E_N ),输入Token的隐层表示为 ( x ),Router的输出为 ( \text{softmax}(W_r x) ),即每个专家的得分。然后取得分最高的 TopK 个专家,把输入 ( x ) 交给这几个专家分别计算,最后按得分加权求和,得到输出 ( y )。
这个计算过程听起来简单,但真正的复杂度藏在工程实现里。首先是负载不均衡——如果Router学歪了,大家都抢同一个专家,那个专家就会过载,其他专家闲着,计算资源被浪费;其次是通信开销——在分布式训练里,每个专家的参数可能放在不同的GPU上,把Token送到专家所在设备、再把结果拉回来,这中间的All-to-All通信往往成为性能瓶颈;再次是推理时的显存占用——虽然计算是稀疏的,但模型参数是稠密存在的,所有专家都要加载进显存,这对推理服务器来说是个不小的压力。
这些都是在解读MoE计算原理时必须同步考虑的。光懂数学公式,不懂工程约束,做AI Infra就处处踩坑。
1.2 核心组件拆解:Router/Gating Network到底做了什么
Router的输入输出维度一般是[batch_size, seq_len, hidden_size]->[batch_size, seq_len, num_experts]。它做的就是一个线性变换加Softmax归一化:
import torch import torch.nn.functional as F def router_forward(x, router_weight): # x: [batch, seq_len, hidden_size] # router_weight: [hidden_size, num_experts] logits = torch.matmul(x, router_weight) # [batch, seq_len, num_experts] scores = F.softmax(logits, dim=-1) return scores这里的num_experts是专家总数,TopK一般取2或者1。取2比较常见,因为多个专家协作通常比单个专家效果好,但也不是越大越好,K值太大会导致每个专家接收的Token太分散,学习不充分,同时计算量也会上去。
一个非常容易被忽略的细节是:Router得分的Softmax是在所有专家上做的,但最终只有TopK个专家的梯度会回传。也就是说,没有被选中的专家,在这次前向里不会产生任何梯度。这个特性决定了MoE模型的稀疏性——不是“参数少”,而是“每次只用一小部分”。
另外,我在看一些开源实现时发现,Router加了个load balancing loss(负载均衡损失)来约束Router不要把所有Token都分配给同一个专家。常见的做法是:
[ \text{aux_loss} = N \cdot \sum_{i=1}^{N} f_i \cdot P_i ]
其中 ( f_i ) 是第 ( i ) 个专家被选中的频率(实际比例),( P_i ) 是Router给第 ( i ) 个专家的平均概率。如果Router对所有专家一视同仁,f_i 和 P_i 都会接近 1/N,乘积求和接近 1,aux_loss 接近 N;如果Router偏心,aux_loss 就会偏大。训练时把这个loss乘以一个权衡系数(比如0.01)加到总loss里,就能有效抑制负载不均衡。
你要是在源码里看到类似的计算逻辑,不要觉得它玄,它就是给Router“打分”加了个约束项,本质上是个正则化。
2. MoE计算全过程的数学视角:从Token维度走一遍
我觉得理解MoE最好的方式,就是抛弃“模型层”的视角,完全站在“一个Token的角度”,把一次MoE前向过程从输入到输出走一遍。你会发现后面很多工程难点,其实都是这个过程中某一个步骤放大之后的体现。
假设当前有一个Token,它的隐层向量是 ( x ),维度为 ( d_{\text{model}} )。这个Token进入MoE层后,会经历以下几步。
第一步,Router打分:计算 ( s = \text{softmax}(x \cdot W_r) ),得到一个长度为 ( N ) 的向量,每个元素对应该Token和每个专家的匹配度。
第二步,TopK选择:取 ( s ) 中最大的 ( K ) 个值对应的专家编号,记为 ( E_{i_1}, E_{i_2}, ..., E_{i_K} )。同时保留这K个得分 ( s_{i_1}, s_{i_2}, ..., s_{i_K} )。
第三步,专家计算:把这K个得分归一化(只在这K个得分上做Softmax),作为每个专家输出的加权系数。然后把 ( x ) 分别送入被选中的K个专家中,每个专家都是一个FFN,输出 ( h_j = E_{i_j}(x) )。
第四步,加权组合:最终输出 ( y = \sum_{j=1}^{K} w_j \cdot h_j ),其中 ( w_j ) 是归一化后的得分。
这四步看起来清清楚楚,但实际工程实现里藏着不少坑。
2.1 Token级调度:Router打分、TopK选择与归一化
工程上实现Router打分时,一般不会一个Token一个Token地算,而是把整个batch的所有Token拼在一起批量算。但这里就出现了一个很有意思的问题:每个Token选择的专家可能不一样,怎么组织计算?
一种常见做法叫dispatch(分发):先把所有Token根据它们被选中的专家重新排列(permute),把要去同一个专家的Token放到相邻位置,凑成一个group,然后对每个group分别做矩阵乘法。
这个permute操作在PyTorch里大概是这样的:
# 伪代码:以topk为例 def moe_dispatch(hidden_states, router_logits, num_experts, top_k): # hidden_states: [total_tokens, hidden_size] # router_logits: [total_tokens, num_experts] scores = F.softmax(router_logits, dim=-1) topk_scores, topk_indices = torch.topk(scores, top_k, dim=-1) # 把token按专家分组 # 得到 dispatch_mask: [total_tokens, num_experts, top_k] # 一般为one-hot形式,表示某个token被分配给了哪个expert的第几个slot ... return dispatched_input, combine_weightsdispatch_mask在实现里通常是一个稀疏的3D张量,或者在数据布局上用sort配合cumsum来实现分组索引。这个阶段的目的很纯粹:让每个专家拿到它该处理的那一批Token。
归一化这个细节也不能省。很多人以为Router输出的Softmax分数可以直接用来加权,其实应该先对TopK分数再做一次Softmax归一化。如果不重新归一化,那么当TopK之外的专家得分总和不可忽略时,加权系数整体会偏小,相当于输出被“衰减”了一截,影响模型表达。
2.2 一个容易被忽略的细节:被“淘汰”的Token去了哪
这是个很多入门同学会懵的地方:一个Token只选TopK个专家,那么其他专家对这个Token的输出是多少?答案是零。在数学上,这个Token对没被选中的专家的输出贡献为0,所以最终组合时只需把被选中专家的输出加权求和即可。
但在工程实现里,“零”不是自动发生的。如果你用矩阵乘法去算所有专家的输出,然后把没选中的专家输出置零,那计算量依然等于所有专家都被激活了,稀疏性就名存实亡。正确的做法是,压根不把这些Token送给没被选中的专家——也正因如此,才需要dispatch阶段把Token重新排列、按需分发。
这也解释了为什么MoE在推理时必须配合精细的调度:不是模型结构说稀疏就稀疏了,还得靠底层kernel和调度系统把“不计算”真正落实到位。见过一些初学MoE的开发者,模型定义看起来没问题,但一跑起来速度跟Dense模型一样慢,多半就是dispatch那一步没做好,Token被全量送进了每个专家。
2.3 输出合并的几种方式:拼接、求和与加权
拿到每个专家算完的结果后,要把它们合并回正常的[batch, seq_len, hidden_size]张量,损失修复 permute 对顺序的破坏。这一步叫combine。
有几种合并方式,工程里比较常见的是:
按加权求和:这是最标准的做法,用Router归一化后的得分对K个专家输出做加权平均。实现时也需要一个
combine_mask,把token坐标和专家坐标对应起来,然后做一次带掩码的求和。拼接再加线性变换:有些实现会把K个专家的输出在特征维度上拼接,再接一个线性层压缩回
hidden_size。这种方式不常用,但它给了模型更强的组合自由度,代价是额外参数和计算量。平均(不加权):比较粗放,一般不推荐,因为Router的得分本身就是学出来的信息,不用白不用。
上面还在说“一个Token”,但真实场景中一个batch里可能有几万甚至几十万个Token。如果用for循环逐个处理,效率完全不可接受。所以工程实现上必然依赖“批量重排 + 批量矩阵乘 + 批量加权合并”这套组合拳。
这个“批量”正是MoE计算中大量调度开销的来源,请记住这一点,后面第三、第四部分会反复提到它。
3. 训练环节的算力与均衡问题:Capacity Factor和辅助Loss
训练MoE和训练Dense模型最大的区别,就是多了“负载均衡”这个维度。Dense模型每个Token走同样的计算路径,天然负载均衡;MoE则不然,如果Router学偏了,整个集群的利用率都会崩掉。这也是MoE落到训练平台上时,我必须优先处理的问题。
3.1 Capacity Factor:限流阀为什么会截断Token
Capacity Factor(容量因子)是我在MoE训练中遇到的第一个重要概念。它的定义是:每个专家在每个批次中能处理的Token数量上限,通常是“平均应处理Token数”的倍数。
假设一个batch有 ( T ) 个Token,专家总数为 ( E ),每个Token选 ( K ) 个专家,那么平均每个专家需要处理的Token数是:
[ \text{avg_tokens_per_expert} = \frac{T \cdot K}{E} ]
如果Capacity Factor设为1.0,意味着每个专家最多只能处理这么多Token。Router如果分配不均,某些专家可能会被分配超过容量上限的Token,超出的部分会被截断(drop)。但这个“截断”不是把Token扔掉那么简单,它会让模型丢失这些Token的信息,如果被drop的Token过多,训练效果会受损。
实际工程中Capacity Factor到底怎么设?我自己的经验是:
- 初始训练阶段可以设大一点(1.25~2.0),因为此时Router还没学好,负载偏差大,给足余量避免丢弃太多Token;
- 训练稳定后可以调小,越接近1.0越好,因为此时Router已经相对均衡,设太大反而浪费计算资源;
- 推理时通常设为1.0,因为推理不允许丢弃Token,宁可让计算稍慢,也要保证每个Token都被处理。
Capacity Factor的设定直接影响模型质量和训练吞吐的权衡——调太小则丢Token严重,调太大则意味着很多专家在空转。这个平衡点需要根据自己的数据和并行配置去实验,没有万能值。
3.2 负载均衡的辅助Loss:为什么需要、几种做法、注意点
辅助Loss是控制Router行为的主要手段。前面提到的 ( \text{aux_loss} = N \cdot \sum f_i \cdot P_i ) 是其中最经典的一种,在开源大模型里很常见——它还有一种近似实现,只统计TopK被选中概率,不统计软分配概率,计算更便宜。
除了这种,还有几种其他做法值得一提:
- Expert-level balance loss:约束每个专家收到的Token数方差尽量小;
- Router z-loss:这个不是控制均衡的,而是限制Router logits的数值范围,防止Router输出极端值,让训练更稳定;
- Drop Token:直接随机丢弃多出来的Token,作为正则手段,同时配合让被选中的Token比例更高。
我一开始对z-loss不理解,后来在训练超大MoE的时候发现Router的logits动不动就跑到几十上百,Softmax之后接近one-hot,梯度也变得很陡,训练很不稳。z-loss的公式很简单:
[ \text{z_loss} = \frac{1}{T} \sum_{i=1}^{T} \left( \log \sum_{j=1}^{N} \exp(s_{ij}) \right)^2 ]
它惩罚的是Router logits的log-sum-exp的平方,说白了就是不让logits数值过大。这个loss的权重一般很小(1e-4左右),但加上之后训练稳定性提升非常明显。
我在实际项目中还踩过这样一个坑:辅助Loss的权重太小,负载均衡效果不明显;权重太大,Router会为了均衡而损失真正的专家分工能力,导致模型精度不升反降。所以这个系数也不是拍脑袋定的,得结合模型规模和训练数据量去调。我的习惯是从0.01起步,观察训练曲线和专家负载统计,如果负载CDF(累积分布)太偏就往上调,如果精度有下降趋势就往下微调。
3.3 训练实践中Memory和Batch怎么平衡的门道
MoE训练时显存占用有两大部分:一部分是模型参数、梯度和优化器状态,这部分是“稠密”的,所有专家都要占显存;另一部分是中间激活值,尤其是dispatch之后,每个专家要处理的那批Token的激活。因为Token是被复制到多个专家去算的,中间激活量会随着TopK变大而增大。
所以MoE训练在显存上的压力,往往比同参数量Dense模型更大。不是参数更多,而是激活值更分散、更碎。
如果你用的是专家并行,每个GPU上只放一部分专家,那每个GPU的显存占用主要取决于“分到多少专家”以及“每批被分到多少Token”。这里最容易踩的坑是:选K个专家的调度机制导致的“热点专家”。
举个例子,如果某些热门Token(比如特殊的标点、常见词)经常被Router分配给同一个专家,那么那个专家所在的GPU就会收到远多于平均水平的Token,内存暴涨,甚至OOM。所以做MoE训练时,监控每个专家接收Token数的分布非常重要,我一般会在训练日志里同时记录max/avg比例,一旦超过1.5,就要考虑调整辅助Loss权重或Capacity Factor。
4. 推理阶段的MoE:显存、通信与时延的三方博弈
训练时我们可以接受较大的batch、较长的等待,但推理不一样——推理讲究低时延、高吞吐、可预期。MoE推理的核心矛盾在于:参数是稀疏激活不假,但部署时所有专家都必须常驻显存。这导致了一个很反直觉的现象:一个56B的MoE模型虽然计算量可能只相当于14B的Dense模型,但显存占用却跟56B的Dense模型差不多。
4.1 稀疏参数≠低显存:MoE推理吃参数还是吃计算
很多人会误以为MoE参数稀疏,推理时就可以只加载被激活的那部分。但现实是,我们无法预知一个Token会被Router分配给哪个专家,所以所有专家都得在显存里待命。MoE的参数稀疏,稀疏的是“计算路径”,不是“存储”。
从计算量看,MoE推理的FLOPs可以粗略估算:
[ \text{FLOPs}{\text{MoE}} = \text{FLOPs}{\text{Attn}} + \text{TopK} \times \text{FLOPs}_{\text{per_expert}} ]
相比Dense模型每个Token要过完整的FFN,MoE确实省了大量计算。但省下的计算能不能转化为更快的推理速度,取决于调度开销和通信开销能不能压住。这就不得不提到Expert Parallelism。
在纯单卡推理场景下,MoE的优势反而是受限的——单卡需要加载全部参数,此时算得再快也受显存带宽限制。MoE真正发挥价值是在多卡并行推理场景,每个GPU只放一部分专家,推理请求被路由到专家所在的卡上。这时候,通信开销就成了新一轮的核心矛盾。
4.2 Expert Parallelism的All-to-All通信:最容易成为瓶颈的环节
Expert Parallelism(EP)是MoE最常用的并行策略:把专家们切到不同设备上,每个设备只负责一部分专家。数据并行处理Attention部分,专家部分各算各的。但Token被Router分给了其他设备上的专家时,就必须把Token从当前设备发送过去,再把计算结果传回来。这个操作在分布式集合通信中叫All-to-All。
All-to-All通信的代价有多大?假设有8张卡,每个Token平均要发送给2个专家,那么在实际推理中,很大一部分时间可能消耗在通信而不是计算上。我之前做过一个实验,8卡EP并行下,如果Token规模不大,通信耗时甚至能占到单次推理总耗时的一半以上。
缓解通信瓶颈的主流思路有哪些?第一,尽量增大单次通信的Token规模,批量合并再发送,减少通信次数;第二,采用分块的All-to-All,把一个大的通信拆成几个小的,边算边通信,掩盖时延;第三,调整并行策略,把Attention和MoE分别放在不同的并行维度上,比如Attention用TP,MoE用EP,让通信和计算尽量重叠。
在多机多卡的场景下,跨机通信的带宽远低于单机内部通信,所以研读这类模型源码时会发现它设计得非常在意“哪些数据需要跨机,哪些数据只在单机内部流动”。MoE的负载均衡设计、Capacity Factor、EP切分策略,其实都是在用软件手段减少通信瓶颈带来的代价。
4.3 推理侧常见的优化策略:Prefill阶段和Decode阶段要分开看
大模型推理通常分Prefill阶段和Decode阶段。这两个阶段在MoE里表现完全不同。
Prefill阶段:要处理大量Token,计算密集,适合增大batch、充分发挥算力。此时MoE的Dispatch开销相对没那么明显,因为批量大,All-to-All通信的单Token均摊成本低。
Decode阶段:一次只生成一个Token,变成了访存密集(Memory-bound),此时MoE的稀疏计算优势会被通信延迟抵消。尤其当并发请求数量不够多的时候,All-to-All通信本身的时间开销几乎和计算时间一样长,整体推理效率会很难看。
所以推理优化里经常提到的“Chunked MoE”策略,就是把Prefill的Token按照一定的chunk大小分块做MoE计算,避免把大Token块一次性塞进Experts,从而调度更平滑、显存峰值更低。这个优化我在实际项目中试过,效果非常显著——在并发128路、上下文较长时,显存峰值能降低30%以上。
另一个方向是用Multi-head Latent Attention(MLA)这类推理友好的Attention结构搭配MoE,把KV Cache压缩到极低,给专家参数腾出显存空间。主打卖点是“更长的上下文+更省的显存”,但它本身和MoE解耦——MoE管FFN稀疏化,MLA管KV Cache压缩,两者叠加时工程上需要仔细调优缓存策略。
推理还有个小细节,就是不要用Padding Token凑batch。MoE的Router会把无效的Padding Token也分配出去,白白浪费专家计算力和通信带宽。所以MoE推理框架一般都会用动态batch和token级别的调度,而不是传统固定形状的batch。
5. MoE工程落地中的常见问题与排查经验
写到这里,我发现MoE相关的坑真的不少,而且很多是“看起来代码正确但性能很差”的隐蔽问题。我把这些年见过、踩过、修复过的问题整理成了一张速查表,查起来比较方便。
5.1 MoE常见问题速查表
| 现象 | 可能原因 | 排查手段 | 解决方案 |
|---|---|---|---|
| 训练时某些GPU显存明显偏高 | 专家负载不均衡,热点专家集中在某张卡上 | 日志中统计各专家接收Token数分布 | 调大辅助Loss权重;调整Capacity Factor |
| 推理速度远低于预期 | All-to-All通信占比过高 | 用profiler区分通信和计算耗时 | 增大batch;启用Communication Overlap;分块通信 |
| Router的logits数值过大 | 缺少z-loss | 打印logits的均值/方差 | 加Router z-loss,权重1e-4起步 |
| 模型精度一直上不去 | 辅助Loss权重设置不合理或Token被丢弃过多 | 观察辅助Loss与被丢弃Token的统计 | 调低辅助Loss权重或调大Capacity Factor |
| 训练中期出现不稳定的Loss尖峰 | TopK专家的梯度更新不同步;专家退化 | 检查专家激活频次;检查Router输出 | 对冷门专家做正则化;或者加大z-loss |
| 推理时某个请求时延特别高 | 该请求的Token被集中调度到了某些专家,导致排队 | 观察每路请求在专家上的分布 | 改用“在Prefill阶段先排序再调度”的策略 |
这张表并不全面,但覆盖了我最常遇到的几类问题。
5.2 踩坑实录:三个让我印象深刻的案例
第一个案例,是某次训练MoE模型时,整体吞吐始终上不去。看GPU利用率总是60%上下徘徊,显存也没有打满。后来用profiler逐层看时间分布,发现Router那部分的耗时竟然占到了整个模型前向的15%以上。问题出在Router的矩阵乘法没有足够优化,Token数很多但hidden_size不够大,导致访存效率极低。最后通过合并路由器权重计算、用batch矩阵乘重写,直接省掉了将近一半的Router耗时,整体吞吐提升了8%。
第二个案例,是推理阶段出现了一个非常奇怪的“长尾时延”——大部分请求10毫秒内返回,偶尔几个请求要100毫秒以上。查到最后发现,那几个慢请求的Token都被Router分配到了同一个专家,而那个专家恰好在一张比较繁忙的卡上,导致局部排队。这个问题的关键不只是负载均衡,还有推理框架本身的调度策略,它没有考虑“专家当前已有多少任务在排队”。后来调整了调度器,在分配请求时综合考虑专家负载和当前队列长度,长尾直接消失。
第三个案例是个更隐蔽的数值问题:在某个专家数较多的MoE模型上,发现训练到后期loss不降反升,而且伴随一段时间的发疯式抖动。最后查出来是Token Drop导致的。因为我们把Capacity Factor设置得太紧,到后期模型正学到比较关键的样本,结果大量Token被截断丢弃,有效训练数据量骤降,模型开始“忘了”学过的知识。这个问题的解决方法很简单:训练后期把Capacity Factor放宽,同时提高辅助Loss约束来保证负载均衡优先靠Router学而不是靠丢弃Token来硬撑。
在MoE相关项目里,我发现一个始终有效的排查思路:先看负载分布,再看通信耗时,最后才怀疑数值实现。很多人一上来就怀疑自己的算子是不是写错了、精度是不是不够,但MoE这种架构大多数性能问题都出在“Token怎么分配、数据怎么流动”这个层面,而不是单个算子内部。
还有一个操作习惯值得分享:跑MoE训练或者推理之前,先跑一个小规模的冒烟测试,把专家负载、Router logits分布、通信量全部打印出来看一眼。问题出现之前就能发现80%的隐患,比在千卡集群上跑了几小时再回头查日志高效得多。
写在最后的一些体会
这一篇笔记写下来,我自己也把MoE的整个链路重新翻了一遍。写的过程中最大的体会就是:MoE的数学原理半小时就能看懂,但真正把它跑得又快又稳,要面对的挑战全在调度、通信、均衡这些“看不见的角落”里。
我个人觉得,做MoE相关工程时,心里一定要绷两根弦:一根是“负载”,另一根是“通信”。负载决定了资源利用率和显存安全,通信决定了训练和推理的下限。这两件事没有做好,再好的模型结构也只能在纸面上表现优秀。
如果是刚接触MoE的同学,我的建议是别急着看各种花哨的加速方案,先把“一个Token在MoE层里走一遍”的前向过程彻底搞明白——Router怎么分、Expert怎么算、结果怎么合,每一步的Tensor形状怎么变、数据在设备间怎么流转。等这些细节都通了,再去看各种并行策略和算子优化,你会发现自己能直接看懂那些看起来很复杂的工程代码。
下一篇笔记我计划接着写MoE和Attention的交互、以及在不同模型架构里MoE层的位置对整体性能的影响,如果时间允许,也会把一些分布式调度细节展开。希望能把这些经验持续写下去,帮到更多做AI Infra的朋友。