1. MoE模型到底是什么——用“多个小专家”拼出大模型
MoE全称是Mixture of Experts,混合专家模型。这两年它在大模型圈子里火得厉害,很多人第一次听到这个名字,是在GPT-4、Mixtral这些模型的架构说明里——只要一提到“稀疏激活”“专家路由”,十有八九就是MoE路线。但说实话,MoE并不是什么新概念,早在1991年就有学者提出了这个思路,只是当时算力和数据都不支持把它推到太大的规模。现在借着大模型的东风,它才真正从论文变成了主流工程方案。
它的核心思想其实非常好理解:一个庞大的模型,不一定非得让所有参数对每个输入都起到作用,而是可以把模型拆成若干个“专家”子网络,每个专家擅长处理不同类型的数据分布,然后由一个“路由”模块(也叫门控模块、Router、Gate)来决定某个输入应该交给哪个或者哪几个专家去计算。这样做的好处很直接——模型总参数量可以很大,但每次前向计算只用其中一小部分参数,推理成本大幅下降。
举个例子,假设你有一个包含8个专家的MoE模型,每个专家是一个独立的FFN(前馈网络)层,总参数量可能达到几百亿。但因为路由模块每次只激活Top-2个专家,所以实际参与计算的参数量可能只有全量参数的几分之一。这就相当于你养了一支几十人的专家顾问团,但每次遇到具体问题,只需要喊其中最对口的两个人过来处理,其他顾问在旁边喝茶休息。省人力,省电费,而且处理效果还不差。
GPT-4具体用了多少个专家、路由怎么设计,官方一直没有公开细节。但已经开源并且大家非常熟悉的Mixtral 8x7B,结构就很典型——8个专家,每次输入激活2个。它总参数量约47B,但推理时的计算量只相当于一个12B左右的稠密模型。这个数字对比非常直观,也是MoE最吸引人的地方。
从部署和推理的角度来看,MoE带来的挑战也很明显。最大的一个就是“显存/存储带宽压力”。虽然计算量小了,但模型总参数量并没有减少——所有专家的权重都得存在内存里。推理时,虽然只算2个专家,但想要算哪个专家,总得先把那个专家的权重从内存搬到计算单元里吧?如果搬到速度跟不上计算的消耗速度,整个系统就会卡在“等权重”这件事上。这也是为什么MoE模型在GPU上跑的时候,特别吃显存带宽,H100、A100这类带宽怪兽在这方面有明显优势。
但这恰恰也是FPGA的机会窗口。FPGA的存储层次是可以定制的,片上BRAM/URAM可以按需分配给不同专家,外部DDR的带宽分配也可以根据访问特征做针对性优化。相比固定架构的GPU,FPGA在处理“冷门专家和热门专家访问频率严重不均”这类问题时,多了很多可操作的空间。这是后文要展开讲的重点。
我的理解是,MoE的本质是用“参数稀疏化”换“计算稀疏化”。它不减少模型存储量,只减少计算量。理解了这个本质,后面再做硬件映射的时候,思路就不会跑偏。
2. 为什么有人愿意用FPGA做MoE推理——GPU之外的另一种解法
聊FPGA实现MoE之前,先要搞清楚一个问题:现在大模型推理基本是GPU的天下,FPGA在这里面到底图什么?
2.1 GPU方案的痛点在哪
GPU跑MoE推理,最大的痛点是带宽和访存模式不匹配。前面说了,MoE每次激活少数专家,但为了找到该激活哪些专家,路由模块要对输入和所有专家做个相似度计算,这个计算量相对不大,但它决定了后续的访存行为。问题是,GPU的线程模型和数据并行方式是相对固定的,SM(流式多处理器)调度以warp为单位。当路由结果导致不同输入样本选择了不同专家时,同一个warp里的32个样本可能被路由到4个不同的专家上,这时候就产生了严重的访存发散(memory divergence),SM的效率会大打折扣。
另一个痛点是动态性。MoE的专家负载天然不均衡,有的专家可能会被频繁选中(hot expert),有的可能长期“失业”。GPU上处理这种动态负载均衡,需要显式的通信和重新分配逻辑,软件栈要做的额外操作很多,稍不留神就会引入大量开销。
还有一点是部署场景。很多应用并不在数据中心,而是在边缘设备、嵌入式系统、工业控制现场。这些场景不一定有条件塞一块几百瓦的GPU,但有功耗限制、时延要求、数据安全隔离需求。FPGA在这种环境下的适用性其实比GPU更强。
2.2 FPGA能够提供的独特价值
FPGA做MoE推理,我认为核心优势有三个。
第一,存储层次可定制。GPU的缓存层次是设计死的,L1、L2、显存,程序员能做的只是尽量让数据访问贴合这个层次。FPGA的片上存储是可编程的BRAM/URAM,你可以按照专家粒度划分存储块,甚至给热门的专家分配更大的片上缓存空间,让它们的数据尽可能留在离计算单元近的地方。这种“存储跟着计算走”的能力,是GPU做不到的。
第二,访存调度可以做到极细粒度。FPGA里你可以为DDR控制器设计专门的请求调度逻辑,针对路由结果做批量的权重预取(prefetch)。比如,路由模块刚算出结果,还没有正式开始专家计算的时候,FPGA的DMA逻辑就可以根据路由结果去DDR里把对应专家的权重搬上片。这个“提前量”如果设计得好,能把DDR的带宽利用率拉得很高。
第三,数据通路可以旁路掉模型中间的无关数据流动。MoE模型除了专家网络,还有注意力层、归一化层、残差连接等。在GPU上,这些层是同一个计算阵列按指令序列执行的;在FPGA上,你可以为每个层设计专门的数据通路,让它们像流水线一样并行工作——注意力在算的时候,路由模块已经在算下一个token的专家分配了,专家计算的结果也在往回写了。整个推理过程真正变成一条流水线,而不是GPU那种“一个大kernel接一个大kernel”的模式。
当然,FPGA也有劣势,比如开发周期长、频率低、通用性差。但如果针对某个固定模型做长期部署、批量出货,FPGA的优势就会随着功耗、面积、成本摊薄而逐渐显现。
2.3 什么样的MoE场景适合FPGA
并不是所有MoE推理都适合拿到FPGA上做。根据我接触过的项目,下面几类场景比较现实:
- 模型结构固定且已知,比如公司内部自研的MoE模型,结构不会再频繁变动。
- 推理批量比较小,时延要求高,典型如自动驾驶、工业实时控制里的在线推理。
- 带宽和功耗严格受限,比如单板功耗限制在20W以内,还要跑一定规模的模型。
- 数据不出设备,对安全隔离有要求,FPGA的物理隔离特性天然合适。
反过来,如果是要做大规模并发推理、高吞吐的云侧服务,那GPU还是理性选择。FPGA在这个场景里打正面,胜算不大。
我的判断是:FPGA做MoE当前不是要取代GPU,而是在那些GPU“使不上劲”的地方把它补上——低功耗、低延迟、定制访存、安全隔离。想清楚定位,再谈实现。
3. 从算法到硬件:MoE模型映射FPGA的整体设计思路
3.1 拆解MoE推理的计算图
要把一个MoE模型搬到FPGA上,第一步不是写代码,而是把这个模型的计算图拆开,看清每一部分的数据流和控制流。一个典型MoE层的推理过程大概分成这样几步:
- 输入向量x进入MoE层。
- 路由模块计算每个专家的得分,得到logits。
- 对logits做softmax或稀疏归一化,取Top-k个专家。
- 根据路由权重,把输入x分别送入选中的k个专家网络。
- 每个专家输出结果乘以对应的路由权重。
- 所有被选中的专家结果做加权求和,得到该MoE层的输出。
这个过程里,第1、2步是纯计算密集型,第3步是控制密集型,第4到第6步是计算+访存混合。在FPGA上做设计的时候,要把这几步区分对待。
路由计算(第2步)本质是一个矩阵向量乘:输入向量和每个专家对应的中心向量/权重做内积。这个计算量并不大,但它的结果决定了后续几乎所有行为,必须做得足够快。在FPGA里,这一步可以放在一个专门的路由计算单元里,用并行乘加阵列实现。如果专家数量是8个或者16个,这种规模的矩阵乘对于FPGA来说非常轻松,几个周期就能出结果。
Top-k选择(第3步)在FPGA里实现也有讲究。8个取2个,直接比较就能做到;如果专家数量扩展到64个、128个,甚至更多,就需要用硬件友好的Top-k排序网络。注意,这里不需要精确排序所有专家的得分,只要找到前k个,所以可以用分块比较的思路,把专家分成若干组,先各取top,再合并比较,能节省大量硬件资源。
专家计算(第4步到第6步)是数据量和计算量的主体。每个专家就是一个FFN层,里面是两层线性变换+激活函数。在FPGA里实现,线性变换可以用矩阵乘加速器做,激活函数一般用查找表实现(比如SwiGLU这类激活,直接用LUT分段拟合)。关键是,多个专家之间是并行还是轮转?这要看硬件资源来定。
3.2 硬件架构的三层划分
把MoE层的计算流拆清楚之后,硬件架构可以按三个层次来组织:
第一层:路由决策层。对应前面的第1到第3步,负责快速产生路由结果。这一层对计算量要求不高,但对时延敏感,必须做得浅,尽量在几十个周期内出结果。
第二层:专家执行层。这是模型的主体,由若干个专家计算单元(Expert Unit,EU)组成。每个EU内部包含一组乘加阵列、激活函数查找表、输出累加逻辑。EU的数量通常不等于专家数量,而是远小于专家数量——原因很简单,FPGA资源有限,而模型推理每次只激活Top-k个专家,只要保证至少k个EU在同时工作就够了。
第三层:全局调度与存储层。包括路由结果缓存、输入分发网络、专家权重预取器、结果汇聚与加权求和单元。这一层起到“后勤部”的作用,负责让所有数据在对的时间到达对的地方。
这三层之间的关系是这样的:路由决策层得出结果后,触发全局调度器,调度器一边给对应EU下发计算指令,一边从外部DDR预取专家权重。EU算完之后,结果汇总到加权求和单元,结合路由权重做最终输出。这个过程如果设计成流水线,不同token就可以在不同阶段同时被处理,吞吐量会非常可观。
3.3 为什么FPGA实现MoE不能照搬GPU思路
这条我想单独强调一下。很多做过GPU CUDA开发的工程师第一次接触FPGA的时候,习惯性地想把GPU的编程模型映射到FPGA上——比如想象一堆并行的线程,每个线程负责一个数据元素。但在FPGA里,这种“单指令流多数据流”的抽象并不好用。FPGA更自然的抽象是“多个专门化的处理引擎,每个引擎负责一个小任务,引擎之间通过FIFO/BRAM互联”。
具体到MoE,GPU的做法是把专家计算当作一个大kernel执行,内部用大量线程并行。FPGA的做法则是把专家计算拆成几个并行的硬件流水线,每个流水线针对一个专家、处理一个token流。你会发现,从算法到架构的映射方式完全不同,GPU是“数据并行”,FPGA是“任务并行+流水线并行”。想清楚这一层,后续的RTL和HLS编码才不会走大弯路。
设计MoE的FPGA架构,最重要的不是把每个数学运算都想得多么复杂,而是把“路由→调度→计算→汇聚”这条控制路径想清楚。控制路径通了,数据通路就是水到渠成的事。
4. 关键模块实现细节——路由、专家计算与存储调度
4.1 路由模块的FPGA友好实现
路由模块可以用一个简单的矩阵乘实现:假设输入向量维度为d,专家数量为N,那么路由权重矩阵是N×d。输出分数的计算就是input(1×d) × weight(N×d),得到N个logits。
FPGA实现这一步,可以直接用一个乘加阵列。比如d=4096,N=8,那么这步要做8×4096次乘加运算。以FPGA的DSP资源来算,如果器件上有2000个DSP,每个DSP可以做一个乘加,2000个DSP并行计算的话,只需要约16个周期就能算完4096维的点积,这个速度非常快。
代码层面,如果用HLS(高层次综合)来写,这个路由模块非常简单,本质上就是一个矩阵向量乘。但要注意,HLS综合时一定要显式控制流水和数组分割,否则生成的电路并行度会很低。
4.2 专家计算的硬件组织方式
专家计算是MoE里计算量最大的部分。以Mixtral 8x7B为例,每个专家的FFN输入维度是4096,中间隐藏层是14336,输出维度回到4096。单看这个FFN,两层矩阵乘的计算量非常大。但FPGA资源有限,通常不会把整个FFN的权重全部放在片上,而是采用“分块计算+数据流复用”的方式。
一种常见的做法是:将FFN的第一层矩阵按输出通道分成若干block,每次加载一个block的权重到BRAM,完成一遍部分计算,再加载下一个block。这样虽然权重搬运次数变多了,但BRAM占用大幅降低。这种做法在FPGA里叫“权重分块流式处理”,非常适合大矩阵乘。
另一个关键设计是EU之间的并行与串行粒度。假设系统里有4个EU,而模型每次激活2个专家,那么可以一次把2个专家分别分配给2个EU并行计算,另外2个EU留作流水线重叠,处理下一批token。如果模型每次激活的专家数量更大(比如Top-8),那就需要更多EU。
在设计EU的过程中,我的经验是,要给每个EU配置一块独立的权重缓存,缓存大小应该能容纳单个专家全部或者大部分权重。这样做的原因很简单:专家切换时的存储重加载开销非常大,如果每个EU只能容纳专家的一小部分权重,每次切换专家都要从DDR重新灌权重,时间浪费严重。片上缓存越大,专家切换越顺滑。
4.3 存储系统的分层调度
MoE的存储调度是整个设计里最容易出问题的部分。专家的权重普遍很大,无法全部放在片上,所以只能分层管理。
第一层是片上BRAM/URAM,用来缓存当前或者下一批正在计算的专家权重。每个EU配一个权重缓存区,命中的专家权重直接从缓存区读取,不用去DDR。
第二层是片外DDR,存放所有专家的完整权重。当路由结果出来之后,预取器判断所选的专家权重是否在片上缓存中,如果不在,就发DDR读取请求。
但这里有个不好处理的问题:DDR的带宽是有限的。假设DDR带宽是25.6GB/s(4个DDR4-3200通道,64bit),专家权重大小是2GB,那么把整个专家数据从DDR搬到片上需要约80ms。如果每个数据请求都等这么久,推理速度会非常慢。所以,预取策略很关键。
一个可行的策略是“提前两个token预取”。路由模块在计算当前token的专家选择时,同时根据历史信息和输入特征的连续性强弱,预测下一两个token可能激活的专家,提前发起权重预取。对于输入变化比较平缓的任务(比如视频帧流、传感器时序数据),预测命中率会很可观。这种做法虽然增加了一点控制复杂度,但能有效掩盖DDR访问延迟。
4.4 数据格式与量化选择
MoE模型在FPGA上通常要配合量化来做,否则带宽和资源都会吃紧。一般来说:
- 路由模块对精度要求相对较高,建议保留int8或fp16,因为它要做softmax和sort,误差容易被放大。
- 专家权重的线性层可以用int8量化,少数对精度敏感的层可以保留int16。
- 激活值建议用int8,如果模型是训练后量化(PTQ),需要做一定的校准集验证。
在具体实现时,int8的乘加在FPGA上用的是DSP48E2的25×18模式,即用DSP做低精度乘法。要注意的是,DSP的输出位宽有限,累加过程中要防止溢出。通常做法是每累加一定数量就做一次截断和饱和处理,不能等到最后统一处理,否则数据位宽撑爆了。
关于量化,我的实际体会是:可以先跑一遍浮点仿真,统计中间激活值的动态范围,再决定定点位宽和缩放因子。想当然地全模型一刀切使用8bit量化,往往会在路由计算和softmax这个环节翻车。
5. 用HLS还是RTL写MoE——工程选型的实际考量
5.1 HLS的开发效率优势
说实话,纯RTL写一个完整的MoE推理系统,工作量相当大。路由部分还好,但专家计算里的矩阵乘、数据搬运、流水控制、状态机协调,如果全部用RTL手工设计,没有几个月下不来。用HLS(比如Vitis HLS)确实能显著提速。
HLS适合的模块是那种数据流清晰、计算密集、控制相对规整的地方。典型的比如:专家FFN里的矩阵乘、激活函数、量化/反量化逻辑。这些模块用HLS写,C/C++代码量和可读性都远优于RTL,综合出来的性能在优化得当的情况下也能接近RTL的八成以上。
5.2 RTL依然不可替代的部分
但MoE系统里有一块东西HLS不太好搞定,那就是路由到专家之间的动态调度逻辑。这个部分涉及条件分支、动态选择、不同数据来源的竞争仲裁,用HLS描述起来非常别扭,综合后的控制逻辑也常常不够精细。我的做法是把这部分用Verilog手写,做成一个专门的路由调度状态机。
如果你采用的是纯HLS方案,比如使用Vitis HLS的数据流(dataflow)模式,那么一定要特别关注任务之间的同步。MoE的多个专家是条件执行的——不是所有专家都被激活,如果用dataflow把所有专家都摆成并行流水线,HLS工具往往会做很大的资源冗余,因为工具提前无法确定哪些分支会被跳过。这种情况,建议把专家执行做成一个动态分发的任务,通过控制信号来启动/停止对应的计算任务。
5.3 一个可行的混合编码流程
我建议的流程是这样的:
- 用HLS编写专家FFN计算核、路由矩阵乘、激活函数这三类计算密集模块,每个模块单独工程综合,生成IP。
- 用Verilog编写顶层系统,包含路由调度状态机、crossbar分发网络、权重预取DMA、结果汇聚逻辑。
- 将HLS生成的IP以RTL的形式集成进顶层工程,通过AXI-Stream接口互联。
- 用UVM或定向测试分别验证各模块,再运行完整的端到端推理仿真,对比Python浮点模型输出,误差控制在预期范围内。
- 在FPGA板上做原型验证,实测资源占用、功耗、吞吐率和时延。
这套流程兼顾效率和可维护性,是比较推荐的工程路径。如果团队HLS经验不深,也可以做模块测试,但要留足集成和排错的时间。
我见过不少团队一上来就把所有东西都用HLS写,结果遇到动态调度就在那儿绕来绕去,最后返工。建议从第一天就想清楚哪些归HLS、哪些归RTL,分工明确才能减少返工。
6. 常见问题与调试技巧——FPGA上跑MoE的坑
6.1 路由结果与权重加载延迟不匹配
这是最常见的问题。路由模块很快就算出了结果,但权重预取需要几十上百个周期才能完成,于是专家计算单元只能在等待中空转,整个系统的吞吐率被打回原形。
排查思路:先确认片上缓存命中率,如果命中率太低,毫无疑问DDR会成为瓶颈。解决办法:要么增大EU内的权重缓存,要么优化预取策略,用“下一批预测”来提前预取。现场调试时,可以在路由结果寄存器旁边加一个计数器,记录“路由完成到权重就绪”之间的等待周期数,观察瓶颈在哪里。
6.2 专家负载不均衡导致部分EU空闲
理论上,如果路由结果分布均匀,4个EU就能撑起8选2的负载。但实际上,输入数据往往有偏置,某些专家被频繁激活,另一些几乎闲置。如果一段时间内连续激活的是同一批专家,那么未分配到的EU就只能空转,浪费资源。
处理方式有两种。一种是“动态EU分配”——哪个专家被选中,就把它分配给当前空闲的EU,而不是把EU固定绑定到专家编号。这样虽然增加了调度复杂度,但能有效提升EU利用率。另一种是“批量重排”——在数据流中插入一个重排缓冲,攒够一定数量的token后再统一做路由决策,尽量让同一批次的token分配到不同专家,提升并行度。后者实现更简单,适合批量预处理场景。
6.3 softmax与Top-k的动态范围溢出
路由模块里的softmax涉及指数运算,在定点实现时,指数很容易放大数值。如果输入logits范围比较大,softmax计算中间结果可能超过预定的定点位宽,导致结果不正确。
我踩过这个坑之后的修复方式是:把softmax变成“减最大值再算指数”的形式。先找到所有logits的最大值并减去,再做指数和归一化。虽然多了一轮比较和减法操作,但数值稳定性和位宽控制会好很多。Top-k排序时,也建议用一个“比较-筛选”的硬件友好实现,别用软件那种全排序后再取前k,硬件代价太高。
6.4 仿真可以过、上板就挂
这种问题几乎每个FPGA工程师都遇到过。最常见的两个原因:时序约束没加全、跨时钟域信号没做同步处理。
MoE系统里往往有多个时钟域,比如DDR接口的时钟、Expert计算单元的高频时钟、路由模块的较低频时钟。跨时钟域的信号如果没用同步器(两级触发器或者异步FIFO)处理,仿真里可能能过,上板就会随机出错。另一个常见问题是,综合工具默认不会自动约束BRAM的输出寄存器,导致时序收敛困难。解决办法是:在约束SDC文件里显式定义所有关键路径,并且对每个跨时钟域的接口打上set_clock_groups或者使用FIFO做同步。
6.5 资源估算不足导致布局布线失败
很多人在规划阶段就预估资源,但MoE这种系统的资源估算波动很大。比如矩阵乘的DSP使用量相对好估,但数据分发网络、跨bar、FIFO的LUT/FF开销往往会超预算。建议在架构设计阶段就预留20%~30%的资源余量,尤其是LUT和BRAM。如果估算太满,综合布线工具会在布局阶段报错,到时候修改架构的成本非常高。
给一个实际的撬门技巧:在Vivado里先做一次综合,看看LUT/FF/DSP/BRAM的初步占用率,再对照设计预期。如果某类资源占用超过了70%,就尽早考虑优化策略——要么减小并行度,要么调整量化位宽,别等到布局布线失败再回头改。
7. 未来扩展方向——从单板推理到多板互联
如果单块FPGA的资源不足以容纳更大的MoE模型,一个很自然的扩展方向是多板卡协同。FPGA之间的互联方式常见有几种:通过PCIe连接主机、通过以太网/光口互联、通过自定义的GPIO/Aurora协议组成阵列。
多板MoE的思路和GPU做模型并行类似——按专家维度切分。比如两块FPGA,各自存放一半专家的权重。路由结果如果是本板的专家,直接本地计算;如果是另一板上的专家,就要把中间结果通过互联发送过去。这里核心的问题是通信开销。每次跨板传输的数据量如果太大,通信时延就会抵消掉并行收益。
一种常见的做法是按token流水并行——两块板分别处理一批不同的token,只有当batch内token被路由到了对方板上的专家时才做通信。由于MoE路由是稀疏的,跨板通信比例通常不会太高,这种方案在批量较大的场景下可行。还有一种做法是把专家复制到多块板卡上,按数据并行方式处理,这样路由结果在本地都能找到专家,不需要跨卡通信。代价是每块板都要存完整的模型权重,对存储容量要求高。
我个人更看好前一种——按专家切分+稀疏通信的方向。它更贴合MoE模型本身的特性,而且随着FPGA开发板和SerDes带宽的不断提升,跨板通信的开销会继续下降。
8. 一些实际测试数据和个人经验总结
最后分享几个我自己实测观察到的数据点,不一定代表所有平台,但可以作为参考。
在一款中等规模的FPGA(大约25.6万个LUT,1200多个DSP,5MB左右URAM)上,我做过一个简化版的MoE层测试:专家数量8,隐藏维1024,激活数2。在int8量化、200MHz主频、单DDR4通道的情况下,单层推理的时延大约是30~40微秒,吞吐率能到每秒钟几千个token。对比同样逻辑在嵌入式GPU上跑,时延差不多,但FPGA的功耗只有GPU的大约三分之一。
如果把专家数量放大到32个,模型的参数量增加,DDR带宽压力立刻凸显。实测发现,如果专家权重全都放在DDR里,预取命中率低于50%时,系统几乎会被DDR等待卡死,吞吐率下降一个数量级。这个时候,增加片上缓存比提升DDR频率更有效。把每个专家的权重缓存从原来的25%提升到50%,命中率提升到80%以上,吞吐率立刻回到正常水平的六成以上。
这些经验说明什么?说明MoE在FPGA上能不能跑顺,瓶颈往往不在计算,而在存储。谁把权重喂给计算单元喂得又快又准,谁就能赢。
另一个经验是,如果你是这个领域的新手,想要动手实验,不要一开始就奔着大模型去。可以先做一个很小规模的博客级项目:一个2到4个专家的MoE层,跑在入门级的FPGA开发板上,先把路由、调度、汇聚这条链路打通。等你把这一条链路吃透了,再去升级专家数量、模型维度、DDR带宽,就有了底气。说实话,MoE的硬件实现并没有特别高深莫测的东西,它更考验的是对系统级的理解和耐心调优。只要一步一步踩实了,最后做出来的系统,性能不会差到哪里去。