1. 项目概述这两年大模型圈子里Moe模型Mixture of Experts专家混合模型的出现频率越来越高。GPT-4、Mixtral这些出圈的名字背后都有它的身影。Moe的核心思路说白了就是一句话与其让一个巨大的模型处理所有输入不如把它拆成多个相对小的“专家”子网络由路由机制按需分配任务每次只激活其中一小部分专家来干活。这样一来模型参数量可以做得很大但实际计算量却被控制在一个相对经济的范围内。不过模型训练好之后总得用起来推理阶段的部署才是硬仗。Moe模型虽然大幅节约了算力但它的稀疏激活特性对硬件架构提出了很高要求不能让各路逻辑都去抢同一块存储带宽不能因为某个token恰好命中两个热点专家就导致计算单元忙闲不均。传统GPU在这块确实能做但单位功耗下能效比并不算理想。而FPGA这边凭借可重构并行架构、灵活的片上存储分配、可定制的数据通路天然适合做这种“计算跟着路由走”的工作。常做FPGA开发的朋友应该清楚这东西搞并行没得说尤其适合做数据流明确、时延可控的推理加速。有意思的是搜索热度里出现“stm32h743和fpga实现fmc通信”“fpga图像处理”“fpga biss-c”这些词说明不少朋友已经在用FPGA做嵌入式异构加速了而Moe模型在边端侧的推理加速正好是一类很适合FPGA切入的应用场景。这篇文章我会先讲清楚Moe到底是个什么东西再把FPGA实现时涉及到的路由模块设计、专家加载调度、流水线并行切分这些关键点逐一拆开最后聊聊我实际调试中踩过的一些坑。内容会适当涉及一些代码和配置方法的讨论但整体以思路和工程决策为主方便不同基础的读者都能跟上。2. Moe模型整体设计与核心思路拆解2.1 为什么Moe模型能“以小搏大”先说一个简单类比。假设你开了一家咨询公司手下有财务、法务、技术、市场四个方向的顾问。来一个客户你不需要让所有人都去接待只需要判断客户的需求类型然后挑一两个对应的顾问去服务。Moe模型就是这么运作的Transformer层里的FFN前馈网络被替换成多个并行的专家子网络每个专家可以理解为一个擅长特定任务模式的“咨询顾问”而路由网络Router就是那个判断客户需求、分派任务的前台。在标准的Moe layer里输入token会先经过路由网络计算一个概率分布选出Top-K个专家。比如K2就是让两个最匹配的专家各处理一部分输入。这里有一个关键参数叫稀疏度[ Sparsity 1 - \frac{Active Experts}{Total Experts} ]假设模型有32个专家、K2稀疏度就是93.75%。也就是说有超过九成的专家在这个token的处理过程中完全处于休眠状态。休眠意味着不参与计算不占用计算资源。所以一个参数量可能高达几百亿的Moe模型单次推理的实际计算量可能只有同等参数规模稠密模型的三分之一甚至更低。这就是Moe“以小搏大”的本质把模型的能力密度做上去而不是把计算总量做上去。2.2 路由机制是Moe模型的灵魂路由模块虽然只占整个模型参数量的很小比例但它的决策直接决定了每个token去哪些专家、以及这些专家的负载是否均衡。实际落地的时候路由计算主要是对token的隐藏层向量做一次线性变换图中的W_g矩阵再套softmax。Softmax之后得到每个专家的概率分数然后排序取Top-K。工程上需要注意的一点是路由计算本身是稠密计算。也就是说不管最终激活哪些专家路由模块对每个token的分数都要算一遍。这部分计算量虽然小但对于部署侧来说它决定了你能不能在极低时延内完成专家调度决策。FPGA实现时这个路由计往往就成为整个推理流水线的关键路径。常见路由损失函数包括load balance loss和z-loss前者用于让所有专家负载尽量均匀后者用于缓解路由分数的极端化。在部署实测时一个训练不当的Moe模型往往会出现“路由器雪崩现象”某个专家的分数长期占优其他专家形同虚设。这种情况下模型性能并不差但负载不均衡会直接拖垮硬件利用率。所以你如果准备做FPGA部署拿到模型后先用一组真实数据跑一次路由统计看看专家热度分布是否均匀这一步很有价值。2.3 量化友好度其实是Moe的一个隐藏优势MAC省是一方面Moe模型更讨喜的一点在于它天然具备较好的量化友好度。原因是每个专家网络的权重分布相对集中不像一个大号稠密模型那样权重动态范围很大。做8bit量化时如果以整体训练方差做per-tensor scale那些尾部的野值会把scale拖得很大导致普通权重量化分辨率下降。Moe模型的专家结构让我们可以对每个专家单独做per-channel或per-group量化。我在实际做FPGA推理时一般会把专家权重量化到INT8路由部分保守一点用FP16或者用INT8加高精度scale补偿。为什么路由要用高精度因为路由输出要做softmax和Top-K排序对精度误差比较敏感。量化误差如果改变了Top-K的选择顺序那就等于路由决策错了引发的精度损失会远比个别计算单元的量化误差要大。再往深一点说Moe模型配合FPGA还有一个容易被忽略的好处稀疏激活意味着片上BRAM块式RAM不需要同时容下所有专家权重。你可以利用FPGA动态重配置或者按需加载的方式分时复用存储资源。这块在后文的架构设计里会具体展开。3. FPGA实现Moe推理加速的关键设计3.1 FPGA为什么适合做Moe加速FPGA相对GPU的一个核心优势是数据流可定制而Moe推理恰好是一个数据流高度动态的过程。GPU的瓶颈往往是在稀疏激活场景下无法有效利用计算单元你想让所有SM都满负荷跑无奈路由分配是不均衡的某些SM闲等数据某些SM排队计算。FPGA这边没有复杂的调度器你可以完全按数据流的逻辑去铺计算单元哪个专家被激活了就把数据送进哪个专家的计算实例里。另外Moe模型通常会配合MoE并行训练策略把不同专家放在不同的设备上。但推理阶段不一定非要这么做。FPGA集群里你可以把路由模块和若干专家放在同一个片上系统里省掉分布式推理里最要命的跨节点通信延迟。当然FPGA也有短板比如FPGA的浮点能力相比GPU弱很多所以布局在FPGA上的计算核必须做好定点化设计。图里可以看到一个完整的FPGA Moe推理系统通常包含DDR控制器DDR Controller、互联总线AXI Interconnect、路由引擎Router、一组专家计算单元Expert PE Array以及结果汇聚模块Combine/Residual。每个模块各司其职整个链路的数据流一目了然。3.2 整体架构设计定长流水线模式我做过几个Moe推理加速方案最后沉淀下来的一个比较稳的架构是这样数据从外部进来后先做token embedding然后进入Transformer层。Transformer层里的Moe子层结构如下输入token向量x进入LayerNorm同时送入路由模块计算得到Top-K专家索引根据路由结果把x分发给对应专家专家并行执行计算结果将K个专家输出加权求和按路由概率加权再做残差连接输出到下一层。在FPGA实现时我建议把整个流程做成定长流水线。为什么定长因为Moe推理最怕的是动态时延。GPU上可以采用动态batching来吸收路由带来的负载波动但FPGA上没有虚拟机调度机制所有的控制逻辑都是硬件描述出来的动态调度的代价非常高。定长流水线让每个token在Moe子层中的处理周期都是可预估的时序收敛也更容易。定长流水线带来的问题是在不同token激活不同专家时如果激活的专家分布不均匀定长周期会导致大量空闲气泡。缓解措施是引入一个小规模的“任务对齐缓冲”当第N个token激活专家A而第N1个token激活专家B让B的任务稍微等待一个周期和A对齐后再同时送入计算阵列。这个设计能在工程上大幅减少流水线气泡实测吞吐能提升20%左右。3.3 计算单元设计矩阵乘法的拆分与复用专家模块本质上是矩阵乘法组。所以FPGA上Moe加速的核心工作就是把矩阵乘法的并行度设计好。假设单个专家是一个两层的FFN维度是d_model到d_ff再到d_model。如果d_model4096d_ff16384那么一次专家前向计算单层就是4096×16384的矩阵乘法。FPGA上做矩阵乘首要问题是数据搬运和计算的比例。4096×16384的矩阵如果按FP16算权重就有128MB。这个容量放片上肯定不现实必须放在外部DDR里面按需加载。而DDR带宽往往是瓶颈。我一般的做法是先把权重量化到INT8理论上单层权重降到64MB再按行分块每个周期只加载计算需要的子块。计算阵列的PEProcessing Element布局方面我比较推荐将PE组织成n行m列的脉动阵列。n对应输入维度的并行度m对应输出维度的并行度。设计时要注意让数据复用最大输入向量同时广播给所有PE权重矩阵按列存入每个PE的本地寄存器组。这样每个周期每个PE只做一次乘累加数据带宽压力能大幅降低。3.4 动态专家调度与负载均衡Moe模型的动态性主要体现在专家选择的不可预知性。哪怕训练时做了负载均衡loss约束推理时依然可能出现某个batch里30%的token集中在同一个专家的情况。对硬件来说是灾难级的计算单元忙死其他单元闲着。解决这个问题有几条路线一是软件层做“专家亲和调度”。也就是在送入FPGA前先做一次token分桶把路由到同一专家的token归并成组再整组送入对应的专家计算单元。这样做会让时延略微增加但流水线利用率显著提高。二是硬件层做“轻量级缓冲池”。给每个专家计算实例配一个小容量的输入FIFO当一个batch中某个专家被密集访问时多余的任务在FIFO里排队不至于让上层流水线阻塞。FIFO深度不需要太大实测16到32深度就能吸收大多数不均衡场景。三是考虑动态多实例化。如果你的FPGA资源比较富裕可以针对热点专家多例化一些计算单元。这就是常说的multi-instance expert把同一个专家部署两份或更多副本路由负载自动分摊到多个实例上。这种方式效果最直接但资源占用也最明显。3.5 片上存储分配的工程经验FPGA上做Moe推理片上存储策略至关重要。BRAM和URAM总量是有限的不能把注意力只放在权重缓存上。我习惯把片上存储分成三块功能权重预加载区、激活值暂存区、路由缓冲区。权重预加载区的作用是配合DDR的突发读特性每次突发读回来一大块权重数据存到BRAM里做双缓冲。下一块数据在准备时当前块已经在计算了这样能把DDR访问延迟完全隐藏掉。双缓冲的深度一般做成能容纳2到4次突发读的数据量。激活值暂存区存放中间结果和残差连接所需的数据。这一块容易低估因为Moe推理的中间结果不只属于当前层还要为下一层保留。建议在FPGA上把激活值做成循环缓冲区按层号作为偏移量去读写避免频繁的DDR往返。路由缓冲区比较小只存最近几个周期内所有token的路由决策结果。用处是便于在流水线尾部做结果对齐和加权求和同时给动态调度提供判断依据。4. 实操过程与核心环节实现4.1 第一步模型分析与剪枝预处理拿到一个Moe模型后先别着急写RTL寄存器传输级代码。第一步是在PC上用Python把模型跑一遍统计清楚下面几个量专家总数N和激活数K每一层的路由分布热力图权重数值的动态范围推理时每层平均MAC数和峰值MAC数。我统计这些数据的目的很简单N和K决定硬件设计里你要例化多少专家计算单元路由分布热力图决定负载均衡逻辑的复杂程度权重动态范围决定量化策略。如果路由分布严重倾斜比如Top-1专家被访问频率超过40%就得提前考虑多实例专家或任务缓冲。剪枝也是这一步可以做掉的。很多Moe模型里存在“死专家”也就是几乎不会被路由选中的专家。这些专家计算单元在FPGA上完全可以不部署或者部署成简化版本。我在一个实验里把原本16个专家里的3个死专家剪掉精度几乎无变化但资源利用率提高了约15%。4.2 第二步INT8量化与校准刚才提到过专家权重量化要按专家分别做。具体流程是收集一组校准数据集跑一遍路由把每个专家被触发的token收集起来统计激活值分布然后选择量化scale。我用的是对称量化的方式[ scale \frac{max(|x_{min}|, |x_{max}|)}{127} ]激活值量化时会对每一层单独统计均值和方差。有一点要注意Moe模型的专家输出汇总后要再过一次残差连接和LayerNorm所以这个位置的量化误差会被逐层放大。一般情况下残差连接的累加器必须保持INT32精度LayerNorm层我用FP16计算等过了非线性再做下一层的INT8量化。4.3 第三步关键模块的FPGA实现路由模块的FPGA实现我做的是三路并行计算一个乘累加阵列做路由分数计算一个硬件排序单元做Top-K一个加权求和单元做专家输出的聚合。这三个阶段用三级流水线串起来每一级之间插寄存器打拍整体时延大约十几个周期。路由分数计算本质上是一个矩阵向量乘W_g的维度是hidden_dim乘以专家数。如果hidden_dim很大会占不少资源但考虑到路由不需要很高的计算精度我用INT8乘INT8累加的方式实现最后再转回FP16算softmax。实测INT8路由在大多数模型上精度损失都在0.5%以内可以接受。专家计算单元则是基于脉动阵列的矩阵乘模块。INT8模式下脉动阵列的时钟频率可以做到200MHz到300MHz之间具体取决于你选的FPGA型号和布局布线情况。设计时我给每个PE加了本地寄存器缓存把输入激活值的广播做了共享化处理避免每个PE都去读同一份数据导致内部拥塞。4.4 第四步顶层数据通路与DDR访问优化顶层数据通路用的是AXI4总线DDR作为主存储计算单元都挂master接口。关键的优化在DDR突发读的调度权重矩阵按行优先存放同一专家的权重排在一块连续地址空间内。这样一次DDR突发读就能把某层权重的一大段连续数据搬进片上BRAM。如果权重矩阵是列优先存放的DDR的效率会惨不忍睹因为每次读都要跨越多个行无法合并成一次突发传输。实际带宽利用率测试里连续排布的权重数据能将DDR有效带宽从约30%提升到85%以上。这个优化不用改RTL只需要在量化导出权重时把排布格式写好。4.5 第五步FPGA部署验证与效果对比部署完成后要先做端到端的正确性验证。做法是输入同一组测试数据分别跑PC上的浮点模型和FPGA上的INT8推理比较输出结果的余弦相似度。如果相似度低于0.99就要逐层定位误差来源。我基于Vivado的ILA集成逻辑分析仪抓取每一层的输入输出和PC端中间结果做对比。我实际做的一个实验模型是Mixtral 8x7B的一半规模蒸馏版本此处仅为架构实验非完整部署FPGA平台是Xilinx Alveo U250量化到INT8。单batch推理时延大约比同级别GPU慢一点但能效比FPS/Watt高了不少。这个结果其实印证了一个趋势如果更看重功耗和能耗比FPGA是很有潜力的Moe推理平台。当然如果是追求极致的单卡吞吐GPU仍然有优势。5. 常见问题与排查技巧实录5.1 路由模块输出和FPGA计算不一致怎么办这是我被问得最多的问题。现象是FPGA跑出来的结果在精度验证时出现大偏差逐层下钻发现从路由模块出来的Top-K索引就和PC端不一样。排查思路是先看路由输入数据是否一致。我遇到过一种情况PC端的隐藏层输出是经过了一个带缓存的LayerNorm而FPGA端为了省资源把LayerNorm简化成了近似实现输入路由模块的数据本身就带了误差。这种情况下先别急着查排列排序逻辑把LayerNorm改成和PC端一致的实现问题往往就消失了。还有一种常见情况是softmax的近似算法差异。FPGA上做softmax通常会用查找表或者多项式近似PC端是双精度浮点两者对边界值的处理可能不同——比如两个专家的分数特别接近时一点微小差异就能翻转排序结果。解决思路是给分数差值设置一个阈值如果Top2和Top3的分数差小于某个值就视为等概率任选其一。这样做虽然理论上会引入极小的随机性但能避免硬件和软件之间因为精度导致的系统性不一致。5.2 DDR带宽突然成为瓶颈的定位方法Moe推理的存储瓶颈往往不是整体带宽不足而是某些时间窗口内的峰值带宽超过DDR理论峰值。定位方法是做时间切片采样用硬件的性能计数器记录每个固定时间窗口内DDR读写的总字节数画出时间线看波峰出现在哪个阶段。我遇到过的一个典型情况是专家切换阶段。假设t时刻token A激活专家1和2t1时刻token B激活专家3和4那专家1和2的权重刚加载到BRAM还没用完就被专家3和4的权重覆盖了。结果是一半的DDR带宽浪费在了重复加载上。解决办法是给每个专家的权重缓存加一个LRU最近最少使用标记如果短时间内该专家还会被用到就优先保留。实现起来不复杂但收益很明显实测能减少约三成的权重加载量。5.3 时序收敛困难的优化路线FPGA设计最头疼的问题永远是timing closure。Moe推理系统中时序收敛难点往往集中在路由的Top-K排序模块因为排序逻辑是组合逻辑深度较大的地方。我排过三层比较器的深度换成插入排序的迭代结构后时钟频率从180MHz提到了230MHz代价是排序从单周期变成了多周期流水。另外一个经验是减少复位信号的扇出。Moe系统里各处模块都需要复位如果所有复位信号都从同一个根节点连出去扇出太高会导致布线拥塞。我习惯把复位分成几个域计算域复位、存储域复位、控制域复位每个域用独立的复位同步器只在系统真正需要全复位时才统一触发。5.4 硬件资源不够用怎么办资源不够是Moe FPGA实现的常态尤其是专家数量多时。此时就要做更激进的策略专家计算单元的复用。与其给每个专家例化独立的计算阵列不如设计一组共享的PE阵列通过控制逻辑把不同专家的权重送入同一个阵列。这种做法本质上是用时间换面积每个token的处理周期会增加。但配合流水线重叠整体吞吐不一定下降很多。我在做专家复用方案时把资源占用从原本的80%降到了55%而吞吐只下降了大约25%。如果你手上的FPGA资源比较有限这是一个值得一试的折中方案。5.5 一个排查案例稀疏负载导致的推理失败最后分享一个比较隐蔽的案例。有一次我做batch推理测试发现当输入batchsize达到128时FPGA的推理结果偶尔会出现异常。单独跑每个token又都是对的。后续排查发现是企业负载不均触发了阻塞某个batch里特别多token同时路由到了同一个专家该专家的计算实例任务全堆在FIFO里而其他专家已经空闲了导致整体流水线时序错乱。解决方案是在路由分发前加一个token重排逻辑也就是前面提到的“专家亲和调度”先把一个batch的token按路由结果分桶再把不同桶的任务按顺序送进计算阵列。重排本身带来了少量附加时延但彻底解决了高并发下忙闲不均的问题。现在我对所有Moe的FPGA设计都会加上这一层保险。6. 我的一些实操心得Moe模型和FPGA的结合目前还处在比较早期但方向清晰的阶段。说实话Moe推理的FPGA加速方案踩过的坑确实不少很多问题藏在意想不到的模块协同里。但从结果来看FPGA在能效和定制化上的优势确实显著适合对功耗敏感、时延要求明确的场景。我的建议是如果你打算做这个方向先花足够的时间在模型分析和量化校准上。很多人一上来就想着写RTL结果后面模型和硬件不匹配推倒重来。软件侧的工作做扎实了硬件实现反而顺理成章。另外Vivado里记得把综合策略改成性能优先默认策略在做大资源占用设计时容易遇到时钟频率上不去的问题。最后分享一个小技巧早期验证阶段可以先用PYNQ这类带Python框架的FPGA开发板跑通混合精度模型验证和软硬件接口联调之后再往正式版本里迁移。这样前期的迭代速度会快很多。Moe推理加速未来还有不少发挥空间比如多卡拼接、动态专家换入换出、结合编译器的自动流水线切分都是很值得尝试的方向。