【LLM推理与Serving⑪】MoE推理与MLA:DeepSeek-V3架构下的显存与通信权衡
本系列博客专注于LLM推理与Serving的核心技术,从基础概念到工程实践,循序渐进。本篇是第11篇,我们深入MoE(混合专家)模型的推理系统,结合DeepSeek-V3的架构设计,探讨专家并行、MLA(Multi-head Latent Attention)等关键技术如何在显存与通信之间找平衡。
一、MoE推理为什么独特?总参数巨大但激活参数很小
在聊MoE推理之前,先回顾一下MoE是什么。
传统的Transformer是"密集模型"(Dense Model)------每个token进来,所有层的所有参数都要参与计算。而MoE(Mixture of Experts,混合专家)模型把FFN层拆成N个"专家",每个token只路由到其中Top-K个专家(通常K=1或K=2)。
一句话概括:总参数量很大,但每个token只激活很小一部分参数。
以DeepSeek-V3为例,它有671B总参数,但每个token只激活37B参数(大约是总参数的5.5%)。这带来了一个很反直觉的现象:
训练时,MoE的瓶颈是算力------因为要把所有专家的参数都"过一遍"(虽然每个token只激活少数专家,但batch内所有token激活的专家并集可能覆盖全部)。
推理时,MoE的瓶颈是显存容量------因为所有专家的权重都得放在显存里,虽然每次只用少数,但你得全部存着。
举个具体的对比:
| 模型 | 总参数量 | 激活参数量 | FP16权重显存 |
|---|---|---|---|
| Llama-3-70B(Dense) | 70B | 70B | ~140GB |
| DeepSeek-V3-671B(MoE) | 671B | ~37B | ~1342GB |
看到了吗?DeepSeek-V3的激活参数量只有37B,比70B密集模型还小,但权重显存是70B的近10倍!这就是MoE推理最核心的矛盾------算力很富余,显存装不下。
所以,MoE推理系统的设计,本质上就是在和"显存容量"和"通信开销"这两件事做斗争。
二、MoE推理的第一瓶颈:显存容量而非算力
我们以DeepSeek-V3为例,来拆解一下显存都花在哪了。
DeepSeek-V3的架构参数(公开信息整理):
- 层数:61层
- 隐藏维度:7168
- 专家总数:256个(每层)
- 每个token激活专家数:Top-8
- 注意力头数:128(GQA,共享16组KV头)
- 中间FFN维度:每个专家是标准FFN
2.1 权重要占多少显存?
权重主要分两部分:注意力层 + FFN专家层。
注意力层权重:是共享的(所有token都用同一套attention参数),所以和密集模型一样:
Wattn≈2×h2×L=2×71682×61≈6.3B 参数≈12.6GB (FP16)W_{attn} \approx 2 \times h^2 \times L = 2 \times 7168^2 \times 61 \approx 6.3\text{B 参数} \approx 12.6\text{GB (FP16)}Wattn≈2×h2×L=2×71682×61≈6.3B 参数≈12.6GB (FP16)
FFN专家层权重:256个专家,每个专家是标准的up+gate+down三层:
Wffn≈Nexpert×3×h×dff×LW_{ffn} \approx N_{expert} \times 3 \times h \times d_{ff} \times LWffn≈Nexpert×3×h×dff×L
假设每个专家的中间维度是常规比例(比如d_ff = 2.7h ≈ 19200左右),那么单层FFN专家总参数大约是:
Wffn_per_layer=256×3×7168×19200≈105B 参数W_{ffn\_per\_layer} = 256 \times 3 \times 7168 \times 19200 \approx 105\text{B 参数}Wffn_per_layer=256×3×7168×19200≈105B 参数
乘以61层,总FFN参数量极其庞大。这就是为什么MoE模型"看起来很大"。
好消息是,推理时每个token只激活8个专家,所以计算量并不大------8/256 ≈ 3%的专家被激活,和一个30B左右的密集模型计算量相当。
坏消息是,所有256个专家的权重都得存在显存里------因为你不知道下一个token会路由到哪些专家,总不能现用现从CPU加载吧?那延迟就爆炸了。
2.2 所以瓶颈是什么?
总结一下MoE推理的资源画像:
- 显存容量:紧张!权重本身就大,还要加上KV Cache
- 显存带宽:还好。激活的专家少,实际搬的权重数据不多
- 算力:很富余。37B激活参数,H100轻轻松松
- 通信:很大!这是我们下一节要讲的专家并行(EP)带来的
这和密集模型很不一样------密集模型的瓶颈通常是显存带宽(Decode阶段)或算力(Prefill阶段),而MoE推理的第一瓶颈是能不能把模型塞进显存。
三、专家并行EP原理:dispatch/combine两次all-to-all
既然单个GPU装不下所有专家,那就把专家分散到多张GPU上。这就是专家并行(Expert Parallelism, EP)。
3.1 EP的基本思路
假设我们有8张GPU,模型有256个专家。每张GPU放32个专家(256/8=32)。当一个token需要路由到Top-8专家时:
- 如果这8个专家都在同一张GPU上,那好办,直接在本地算
- 如果这8个专家分布在不同GPU上,那就需要把token的"代表"(hidden state)发送到持有对应专家的GPU上,算完再把结果收回来
这就是EP的核心:token在GPU之间"搬家",去找对应的专家。
3.2 Dispatch / Combine 两次all-to-all
EP的每一层FFN包含两次全局通信:
第一次:Dispatch(分发)
-
输入:每张GPU上有一批token的hidden state
-
操作:根据路由结果,把每个token发送到持有其目标专家的GPU上
-
通信模式:all-to-all(每个GPU都要给其他每个GPU发数据)
GPU0 的 token A → 专家3 → 在 GPU1 上 → 发给 GPU1
GPU0 的 token B → 专家7 → 在 GPU2 上 → 发给 GPU2
...
第二次:Combine(聚合)
- 输入:每张GPU上,本地专家算完了各自的输出
- 操作:把算好的结果发回token原来所在的GPU
- 通信模式:又是一次all-to-all
两次all-to-all,一去一回,构成了EP一层完整的通信。
3.3 通信量估算
我们来算一下一层EP的通信量有多大。
假设:
- 全局batch size(token数):B
- 隐藏维度:h
- Top-K激活专家数:K
- 每个token的hidden state大小:h × 2字节(FP16)
每个token要被发送K次(去K个专家),然后K个结果要回来。注意回来的也是K个hidden state,大小一样。
所以单层EP的全局通信量是:
Comm=2×B×K×h×2 字节\text{Comm} = 2 \times B \times K \times h \times 2 \text{ 字节}Comm=2×B×K×h×2 字节
系数2是因为dispatch + combine各一次。最后那个2是FP16的字节数。
代入DeepSeek-V3的数字(h=7168, K=8):
Comm_per_token=2×8×7168×2≈229KB / token / 层\text{Comm\_per\_token} = 2 \times 8 \times 7168 \times 2 \approx 229\text{KB / token / 层}Comm_per_token=2×8×7168×2≈229KB / token / 层
61层的话,每个token的通信总量约为:229KB × 61 ≈ 14MB / token
这个量其实相当大。所以EP对GPU之间的互联带宽要求非常高------这也是为什么NVLink在MoE推理中这么重要。
3.4 EP与TP/DP的组合策略
实际生产中,不会只单独用EP,而是和张量并行(TP)、流水线并行(PP)、数据并行(DP)组合使用。常见的组合策略:
策略一:EP + TP
把FFN专家按EP切,注意力层按TP切。适合单节点内(NVLink高速互联)。
DeepSeek-V3的推理推荐配置就是这种------节点内用高速NVLink做EP + TP,通信开销可以接受。
策略二:EP + DP
跨节点之间用DP(数据并行),节点内用EP。跨节点的DP通信量(all-reduce)相对较小(只传梯度/更新,推理时甚至不需要),而EP的all-to-all通信量大,放在节点内用NVLink扛。
策略三:EP + TP + PP + DP 四维并行
超大规模模型(比如万亿参数以上)可能需要四维并行一起上。工程复杂度极高,一般只有头部公司才有能力做。
对于MoE推理,一个经验法则是:EP的粒度尽量放在高速互联域内(比如同一个DGX节点的8张卡之间),跨节点尽量只做DP。
四、Decode阶段专家负载不均问题
Prefill阶段还好------batch里有很多token,路由到各个专家的数量比较均匀。但到了Decode阶段,每个请求一个token,batch size小的时候,专家负载可能严重不均。
4.1 为什么会不均?
举个极端例子:batch里只有1个token,它激活Top-8专家。如果这8个专家全在同一张GPU上,那其他GPU上的专家就全闲着,利用率 = 1/8 = 12.5%。
即使batch稍大一些,由于路由的随机性和数据的偏态分布,也可能出现某些GPU上专家被激活得多、某些GPU上少的情况。
这就是专家负载不均(Expert Load Imbalance)问题,它会导致:
- 部分GPU空闲,整体吞吐下降
- 木桶效应------最慢的那张GPU决定了整层的延迟
4.2 三种工程解法
解法一:冗余专家(Redundant Experts)
把同一个专家复制到多张GPU上。token路由到这个专家时,可以选负载较轻的那张GPU去算。
代价是:权重显存增加了(重复存储),而且需要额外的负载均衡调度逻辑。
解法二:EPLB(Expert Parallel Load Balancing)
EPLB是一种动态负载均衡策略------在dispatch阶段,不严格按照"专家在哪个GPU就发给哪个GPU",而是允许一部分token被"迁移"到其他GPU上对应的冗余专家副本,或者在允许的范围内调整路由选择,让各GPU的计算量尽量均衡。
具体实现上,可以在路由时加一个"负载感知"的调整:如果某个专家所在的GPU已经很忙,而另一个GPU上有这个专家的副本(或者有一个排名接近的备选专家),就把token导过去。
解法三:Bias调整(路由偏置)
在训练时就给路由函数加一个"负载均衡bias",让路由结果天然更均匀。这样推理时各专家的负载就比较平衡,不需要推理侧做太多调度。
典型做法是在router的logits上加一个aux loss,惩罚专家负载不均。这是训练侧的优化,但直接影响推理侧的效率。
五、DeepEP:为MoE而生的all-to-all通信库
聊到MoE推理的通信优化,就不得不提 DeepEP------DeepSeek团队开源的、专门为MoE推理优化的all-to-all通信库。
5.1 为什么需要专门的通信库?
普通的all-to-all(比如NCCL的alltoall)是通用的,假设每个进程给每个其他进程发的数据量都差不多。但MoE的all-to-all有特殊性:
- 每个GPU发送的数据量取决于路由结果,可能不均匀
- 数据是按token组织的,每个token有自己的目标专家
- 通信和计算可以重叠(一边发,一边算已经到达的token)
DeepEP就是针对这些特点做的深度优化。
5.2 DeepEP的核心优化点
1. NVLink 感知优化
DeepEP针对NVLink的拓扑做了专门的调度,充分利用多对多同时传输的能力。在DGX H100这样的全互联节点上,NVLink带宽利用率可以做得很高。
2. 双模式(Dual Mode)
DeepEP支持两种工作模式:
- Token-based模式:以token为单位调度,适合batch较小、负载不均的情况
- Batch-based模式:以批次为单位,适合batch较大、负载较均匀的情况
推理系统可以根据当前batch size和负载情况自动切换模式。
3. 通信计算重叠(Overlap)
这是最关键的优化------不需要等所有数据都收完才开始算,而是收到一部分就先算一部分,计算和通信流水线化。
具体来说:
- Dispatch阶段:边收token,边把已经收到的token喂给本地专家计算
- Combine阶段:边算边发结果,不用等所有专家都算完
这样可以把通信时间"藏"在计算时间里,端到端延迟显著降低。
4. 低延迟调度
针对Decode阶段batch小、延迟敏感的特点,DeepEP做了细粒度的调度优化,减少启动开销和等待时间。
DeepEP的开源地址:https://github.com/deepseek-ai/DeepEP,有兴趣的同学可以去看看源码,对理解MoE通信很有帮助。
六、MLA原理:从存全部head K/V到存低秩latent
聊完了MoE的FFN部分,我们再来聊聊DeepSeek-V3的另一个创新------MLA(Multi-head Latent Attention,多头潜在注意力)。这是对注意力机制本身的改造,目标是进一步减少KV Cache的显存占用。
6.1 为什么需要MLA?
普通MHA(Multi-Head Attention)中,每个注意力头都有自己独立的K和V:
Attention(Q,K,V)=softmax(QKT/dk)V\text{Attention}(Q, K, V) = \text{softmax}(QK^T/\sqrt{d_k})VAttention(Q,K,V)=softmax(QKT/dk )V
假设有H个head,每个head维度是d_k,那么KV Cache的总大小是 2 × H × d_k × seq_len × 2字节(FP16)。
后来有了GQA(Grouped Query Attention)和MQA(Multi-Query Attention)------让多个query head共享同一组KV head,减少KV Cache。DeepSeek-V2/V3用的MLA走得更远。
6.2 MLA的核心思想
MLA的直觉是:不同head的K/V之间存在冗余,可以用更低的秩来表示。
具体做法是这样的:
1. 压缩KV到低秩latent
不直接存每个head的K和V,而是存一个低维的"潜在表示"(latent)。这个latent向量比原始K/V的总维度小得多。
公式上,MLA引入了一个压缩矩阵 WkvW_{kv}Wkv,把key和value"压缩"到低维空间:
ht(kv)=Wkv⋅kt;vth_t^{(kv)} = W_{kv} \cdot k_t; v_tht(kv)=Wkv⋅kt;vt
其中 kt;vtk_t; v_tkt;vt 是原始K/V的拼接,维度很高;ht(kv)h_t^{(kv)}ht(kv) 是压缩后的latent,维度很低。
2. 用吸收矩阵技巧恢复
推理时,从latent恢复出每个head需要的K/V,靠的是吸收矩阵(Absorption Matrix)技巧。
具体来说,把Q投影矩阵和K/V的解压缩矩阵合并------在Q端做一次线性变换,相当于在K端做了逆变换。这样,虽然KV被压缩了,但Q乘以"压缩后的K的转置"效果和原来差不多。
更准确地说,MLA的注意力计算变成了:
Attn=softmax(Q′⋅(Wk_latent⋅ht(kv))T/dk)⋅(Wv_latent⋅ht(kv))\text{Attn} = \text{softmax}\left(Q' \cdot (W_{k\latent} \cdot h_t^{(kv)})^T / \sqrt{d_k}\right) \cdot (W{v\_latent} \cdot h_t^{(kv)})Attn=softmax(Q′⋅(Wk_latent⋅ht(kv))T/dk )⋅(Wv_latent⋅ht(kv))
其中 Q′=Q⋅WQ′Q' = Q \cdot W_Q'Q′=Q⋅WQ′ 是经过吸收矩阵调整后的Q。
关键洞察 :因为 Wk_latentW_{k\latent}Wk_latent 和 Wv_latentW{v\_latent}Wv_latent 是权重(和token无关),可以和Q的投影矩阵合并------于是Q侧多了一点计算,但KV侧省了大量存储空间。
6.3 MLA 与 TP 的矛盾 → DP Attention
MLA有一个"副作用":它和**张量并行(TP)**不太兼容。
为什么?因为TP通常是按head切分的------每张GPU存一部分head的KV。但MLA的latent是"全局压缩"的,各个head的信息混在一起了,没法干净地按head切。
DeepSeek的解决方案是:用DP Attention代替TP Attention------即注意力层不用张量并行,改用数据并行(或者说"序列并行"的某种变体)。具体来说,把序列维度切到多张卡上,每张卡算自己那段序列的注意力,然后通过ring attention之类的方式通信。
这是一个很有意思的架构tradeoff:用MLA省KV Cache显存 + 用DP Attention做并行,替代了传统的TP方案。两者各有优劣,但对于超长上下文场景,MLA+DP的组合往往更优。
七、MHA/GQA/MQA/MLA定量对比
说了这么多,我们来做一个定量对比,看看各种注意力方案的KV Cache到底差多少。
7.1 每token KV Cache字节数
计算公式(FP16存储):
KV_per_token=2×Hkv×dk×2 字节\text{KV\_per\token} = 2 \times H{kv} \times d_k \times 2 \text{ 字节}KV_per_token=2×Hkv×dk×2 字节
其中:
- 系数2:K和V各一份
- HkvH_{kv}Hkv:KV head的数量
- dkd_kdk:每个KV head的维度
- 最后那个2:FP16每个元素2字节
我们以DeepSeek-V3的配置为参考基准来对比:
| 注意力方案 | KV head数 | 每head维度 | 总KV维度 | 每token KV Cache(FP16) | 相对大小 |
|---|---|---|---|---|---|
| MHA | 128 | 128 | 16384 | 64 KB | 1x |
| GQA(16组) | 16 | 128 | 2048 | 8 KB | 1/8x |
| MQA | 1 | 128 | 128 | 0.5 KB | 1/128x |
| MLA(latent) | - | - | 512(latent维) | ~2 KB | ~1/32x |
(注:MLA的数字是示意,实际latent维度根据模型配置不同而不同)
可以看到:
- MQA最省,但精度损失也最大
- GQA是精度和效率的折中(目前最主流)
- MLA比GQA还省好几倍,同时精度损失比MQA小很多
7.2 128k上下文对比
我们来算一下128k上下文长度下,单个请求的KV Cache占多少显存:
| 方案 | 每token KV | 128K上下文KV总大小 | 可放的请求数(80GB显存,假设占20%给KV) |
|---|---|---|---|
| MHA | 64 KB | 8 GB | ~2个 |
| GQA(16组) | 8 KB | 1 GB | ~16个 |
| MQA | 0.5 KB | 64 MB | ~256个 |
| MLA | ~2 KB | 256 MB | ~64个 |
这个差距是非常惊人的。同样80GB的A100,用MHA只能同时服务2个128K上下文的请求;用MLA可以服务64个;用MQA甚至可以服务256个。
当然,实际系统中KV Cache不是唯一的显存开销(权重、激活、临时工作区都要占显存),而且MQA的精度损失在很多场景下是不可接受的。MLA的价值就在于------在比GQA更省显存的同时,保持接近MHA的精度。
八、vLLM/SGLang的MoE与MLA支持现状
讲了这么多理论,最后我们来看看主流推理框架的支持情况。
8.1 vLLM
vLLM是目前最流行的开源推理框架之一,对MoE的支持一直在迭代:
- MoE支持:vLLM从v0.4开始逐步完善MoE支持,目前已经支持Mixtral、Qwen2MoE、DeepSeekMoE等主流MoE模型
- 专家并行(EP):支持通过NCCL进行EP通信,节点内效果不错
- 量化:支持AWQ、GPTQ等量化方式配合MoE
- MLA支持:vLLM已经在适配DeepSeek-V2/V3的MLA注意力,社区有对应的PR
8.2 SGLang
SGLang是另一个快速发展的推理框架,在某些场景下性能优于vLLM:
- MoE支持:SGLang对MoE的支持很积极,DeepSeek-V2/V3是其重点优化对象
- RadixAttention + MoE:SGLang的RadixAttention(前缀缓存)和MoE结合,可以进一步提升长上下文场景的效率
- MLA支持:SGLang较早支持了MLA,并且做了针对性的kernel优化
- DeepEP集成:SGLang在探索和DeepEP的集成,以进一步降低MoE通信开销
8.3 给infra学习者的建议
如果你想动手实践MoE推理,建议的路径是:
- 先跑通Mixtral-8x7B:这是最经典的MoE模型,社区支持最好,各种框架都能跑
- 再试DeepSeek-V2-Lite:体量适中,包含MoE + MLA的核心设计
- 读SGLang的MoE相关代码:SGLang的代码质量不错,MoE部分的实现比较清晰
- 做性能实验:对比不同EP粒度、不同batch size下的吞吐和延迟,建立直觉
九、小结与思考
9.1 小结
MoE推理是大模型推理领域最有挑战性也最有意思的方向之一。我们这篇博客覆盖了:
- MoE推理的独特性:总参数巨大但激活参数很小,瓶颈是显存而非算力
- 专家并行EP:dispatch/combine两次all-to-all,通信量巨大,对互联带宽要求高
- 负载不均问题:Decode阶段尤其严重,解法包括冗余专家、EPLB、bias调整
- DeepEP:专门为MoE优化的通信库,NVLink感知、双模式、通信计算重叠
- MLA原理:把KV压缩成低秩latent,用吸收矩阵技巧恢复,大幅省KV Cache
- MLA与TP的矛盾:MLA不适合TP,改用DP Attention
- 定量对比:MHA/GQA/MQA/MLA四种方案的KV Cache差距可达上百倍
- 框架支持:vLLM和SGLang都在快速迭代MoE与MLA支持
整个MoE推理系统的设计哲学可以概括为:在显存容量和通信开销之间找最优平衡点。 EP用通信换显存,MLA用少量计算换显存,DeepEP用更聪明的通信减少延迟------一切都是tradeoff。