这两年聊大模型落地,绕不开一个词:MoE。不管你是刷到 DeepSeek 的模型卡,还是翻 Ollama 的模型库,或者在看 Qwen、Mixtral 这类开源权重的时候,"总参数几百 B、激活只有几十 B"这种标注已经成了常态。对做本地部署的人来说,这件事的意义非常具体——以前一个几百 B 的稠密模型,推理一次要把全部权重从头到尾过一遍,显存和算力两头卡脖子;换成 MoE 之后,每次推理只激活一小部分参数,算力需求断崖式下跌,大模型才真正有了在家里那台机器上跑起来的可能性。
MoE 全称 Mixture of Experts,混合专家。它的核心思想说出来简单得有点朴素:模型里塞一大堆"专家"子网络,但每个 token 进来的时候,只挑其中几个专家参与计算,其余的原地待命。这样模型的总参数量可以做得很大,知识容量足够,但每次前向传播真正动用的算力却控制在很小的规模。DeepSeek 系列是这个路线的典型代表,Mixtral、Qwen 的 MoE 版本、Phi 系列的 MoE 变体也都走得通。这篇内容我准备从原理讲到实操,把"为什么 MoE 能省算力""为什么 MoE 反而不省显存""本地部署时专家权重怎么摆"这几个问题掰开揉碎,再给出 Ollama、llama.cpp、vLLM 三条路线的完整配置。适合已经玩过稠密模型本地部署、想把手里的机器榨出更多价值的人,也适合刚接触 MoE 想搞明白它和普通 Transformer 差异在哪的人。
1. 先把 MoE 这笔账算明白:它到底省了什么
1.1 稠密模型的成本结构卡在哪
稠密(Dense)Transformer 的成本结构特别直白:参数有多少,算力就要多少。一个 70B 的稠密模型,每个 token 前向传播都要经过全部 70B 参数的矩阵乘法,一个都跑不掉。显存里要装下全部权重,算力上也要把全部权重参与一遍计算。这就导致一个很尴尬的局面——想提升模型能力,最直接的办法是加参数,但参数量一上去,推理成本跟着线性涨,最后变成"训练得起、用不起"。我在早期折腾 70B 稠密模型的时候,两张 24G 卡跑 Q4 量化勉强能塞下,但生成速度只有个位数 token 每秒,交互体验基本等于没有。
问题出在"参数利用率"上。一个稠密模型里,处理"写诗"这件事和处理"算数学题"这件事用的是同一套参数,神经网络在训练过程中把所有能力压缩进了同一批权重里。这在统计上是高效的,但在推理时是浪费的——处理一个简单问候语,根本不需要动用整个模型的全部容量。
1.2 稀疏激活的核心直觉
MoE 的出发点就是打破这种"全员参与"的模式。它的做法是把 Transformer 里那个参数量占比最大的前馈网络(FFN)拆成很多份,每一份叫一个"专家",然后加一个小的路由网络,专门负责给每个 token 分配合适的专家。一个 token 进来,路由器看一眼它的隐藏状态,挑出最匹配的 1 到 8 个专家,只让这几个专家参与计算,其余的权重完全不动。
举个生活化的类比。传统稠密模型像一家只有一位全科大夫的诊所,什么病都找他,他得什么都懂,而且每次问诊他都要把全部知识过一遍。MoE 像一家综合医院,挂号台(路由器)先判断你该看哪个科,然后你只去对应的科室,其他科室的医生该干嘛干嘛。医院可以雇几百个专科医生,但你看一次病只占用其中一两个的时间。医院的"知识总量"上去了,但单次问诊的"资源消耗"并没有同比上升。
放到数字上看更直观。一个模型如果把每层的 FFN 复制 8 份,总参数量大致变成原来的 8 倍(注意力层和嵌入层是共享的,不复制),但每个 token 只激活其中 2 份,实际参与计算的有效参数量只有总参数的十几分之一。这就是"总参数很大、激活参数很小"这个标注的由来。
1.3 省算力但不省显存,这是最容易被误解的一点
很多人第一次接触 MoE 会想当然地认为:激活参数少,那显存占用也应该少。这是个坑。激活参数决定的是每次计算的算力开销,而显存占用取决于总参数量——因为虽然每个 token 只用到少数几个专家,但你永远不知道下一个 token 会路由到哪个专家,所以所有专家的权重都必须老老实实待在显存(或者能被快速访问的内存)里,随时待命。
拿 Mixtral 8x7B 举例,它总参数 46.7B,每 token 激活 12.9B。算力需求大致相当于一个 13B 稠密模型,但显存占用跟一个 47B 的稠密模型差不多。这意味着什么?意味着在一张 24G 卡的机器上,Mixtral 8x7B 用 Q4 量化大概刚好能塞进去,而如果只看激活参数的话,你会觉得它应该像 13B 模型一样轻松。
这个特性直接决定了本地部署 MoE 的策略。既然算力省下来了,我们完全可以把一部分权重丢到 CPU 内存里,用 CPU 去做那部分专家的计算,GPU 只负责注意力层和当前激活的专家。CPU 推理的瓶颈从来都是算力而不是内存带宽,而 MoE 恰好把算力需求压得很低,两者简直是天作之合。后面第 4 节会详细讲这个配置。
| 对比维度 | 稠密模型 | MoE 模型 |
|---|---|---|
| 显存占用 | 由总参数量决定 | 由总参数量决定 |
| 单 token 算力 | 由总参数量决定 | 由激活参数量决定 |
| 提升容量的代价 | 算力和显存同步上涨 | 显存上涨、算力基本不变 |
| 通信开销 | 低 | 专家并行时需要 all-to-all 通信 |
| 训练稳定性 | 相对稳定 | 需要负载均衡约束 |
2. MoE 架构的核心组件拆解
2.1 用专家层替换 FFN:为什么动的是 FFN 而不是注意力
Transformer 层大致分两块:多头注意力负责 token 之间的信息交换,前馈网络负责单 token 内部的特征变换。MoE 改造的通常是 FFN 这一块,很少有人去动注意力。原因有两个。第一是参数量分布,在主流配置里 FFN 的参数量通常占单层总参数的六成到七成,动它的收益最大。第二是语义分工,注意力机制处理的是"这个 token 该看哪些其他 token"这种位置和上下文相关的问题,它天然是全局的、需要一致的,拆成多个专家各看各的会让上下文建模变得混乱;而 FFN 更像是"根据当前特征做一次知识查询",不同专家可以负责不同领域的知识,职责上更容易切分。
所以一个 MoE 层长这样:输入先经过共享的注意力层,然后进入 MoE 的 FFN 部分。这里有一组并行的专家 FFN,外加一个路由器。路由器输出每个 token 对每个专家的偏好分数,取分数最高的 top-k 个专家,把这些专家的输出按照路由权重加权求和,作为这一层的输出。k 通常取 1 到 8 之间,Mixtral 用 top-2,DeepSeek-V3 用 top-8。
2.2 门控网络怎么挑专家
门控网络(Router / Gate)本身是个非常小的线性层,输入维度是模型的隐藏维度,输出维度是专家数量。它的计算量在整个模型里可以忽略不计,但它决定了整个 MoE 的行为。
最朴素的做法是softmax(W_g @ x),得到每个专家的概率,然后取 top-k。但这里有个细节:取 top-k 之后的概率分布需要重新归一化,否则几个专家的权重加起来不等于 1,输出尺度会漂移。也有做法是在 top-k 之后直接对选中项做 softmax,效果等价。
还有个变体是加噪路由(Noisy Top-K Gating),在打分时加一个可学习的高斯噪声,训练时鼓励探索,避免路由过早固化。这个技巧在大规模训练里比较常见,推理时噪声关掉。
路由器的另一个关键设计是它是可微的,但 top-k 这个选择操作本身不可微。解决办法是让梯度只通过被选中专家的路由权重回传,没被选中的专家这一轮就拿不到梯度。这个近似在实践中效果不错。
2.3 Top-K 路由与容量因子
容量因子(Capacity Factor)是训练时的概念,但它能帮我们理解 MoE 的并行特性。训练时一个 batch 里有很多 token,为了保证负载可控,通常给每个专家设一个容量上限:能接收的 token 数量 = 容量因子 × (总 token 数 / 专家数)。如果某个专家收到的 token 超过了这个上限,超出的部分就被"丢弃"(token dropping),直接跳过这一层,靠残差连接把输入原样传下去。
容量因子设小了会丢 token,影响效果;设大了会浪费显存。常见取值在 1.0 到 2.0 之间。推理时一般不做丢弃,因为单条请求的 token 数不多,而且丢弃会让输出质量不稳定。
顺带说一句,专家并行(Expert Parallelism)这套东西在训练里是刚需,因为一台机器塞不下几百个专家。这个时候 token 需要通过网络在设备之间搬运,产生 all-to-all 通信。这也是 MoE 训练比稠密模型更依赖高速互联的原因。本地推理时如果单机放得下,就不涉及这个问题。
2.4 负载均衡:专家坍缩是怎么发生的
MoE 训练里最经典的失败模式叫"专家坍缩"(Expert Collapse)。现象是路由网络慢慢倾向于只挑那几个表现好的专家,其余的专家长期拿不到 token,得不到梯度,参数逐渐退化,最后彻底没人用。等于花了大价钱雇了一堆医生,结果全都挤在一个诊室门口。
解决手段是在训练损失里加一个负载均衡损失(Load Balancing Loss),通常是让每个专家接收到的 token 比例与其路由概率之和尽量一致,用两者的乘积作为惩罚项。这个系数一般设得很小,0.01 量级,太大了会干扰主任务的学习。
从部署角度看,这件事的启示是:MoE 模型的质量高度依赖训练时的负载均衡做得好不好。如果一个 MoE 模型在训练时专家利用率严重不均,那它的有效容量其实远小于标称的总参数,表现出来就是"明明参数很多,但回答质量一般"。
2.5 DeepSeek 系的几处改动:共享专家与细粒度专家
DeepSeek 在标准 MoE 上做了几个改动,值得单独说说,因为现在很多开源模型都在借鉴。
第一是共享专家(Shared Expert)。DeepSeekMoE 的每个 MoE 层里,除了 256 个路由专家,还有一个永远被激活的共享专家。这个专家的作用是吸收那些通用的、所有 token 都需要的基础知识,让路由专家能更专注于各自领域的差异化知识。V3 里每个 token 激活 1 个共享专家加 8 个路由专家。
第二是细粒度专家切分。传统的做法是专家数量少、每个专家大(比如 8 个大专家),DeepSeek 走的是相反路线:把专家切得很细,数量做到 256 个,每个专家小得多,然后激活更多个。这样做的好处是组合空间大得多,8 个从 256 里选,比 2 个从 8 里选,能覆盖的知识组合数是天壤之别。
第三是设备限制路由,主要是为了训练时控制通信开销,把每个 token 的专家选择限制在少数几台设备上。对本地推理影响不大,但理解这个设计能帮你看懂它的参数配置表。
3. 一次 MoE 推理里到底发生了什么
3.1 从 token 到专家命中的完整链路
我们跟着一个 token 走一遍。假设模型有 32 层,每层 MoE 有 8 个专家,top-2 路由。
token 经过嵌入层变成向量,进入第一层。先过共享的注意力子层,做完上下文聚合,得到这一层的隐藏状态。这个隐藏状态被送进路由器,路由器算出一个长度为 8 的分数向量,选出最高的 2 个,比如第 3 号和第 6 号专家命中,权重分别是 0.7 和 0.3。然后这个 token 分别进入第 3 号和第 6 号专家的 FFN,得到两个输出,按 0.7 和 0.3 加权相加,再加上注意力子层的残差输出,进入下一层。
下一层重复同样的过程,但选择结果可能完全不同——同一个 token 在第 5 层可能命中 1 号和 4 号专家。整个前向传播下来,一个 token 在 32 层里会命中 64 次专家(32 层 × 2 个),命中的组合每次都可能不一样。
这个过程有个重要的工程后果:不同层、不同 token 激活的专家组合千差万别,你没法提前预知,所以所有专家权重都得准备着。这就是前面说的"不省显存"的根本原因。
3.2 激活参数量手算:以 Mixtral 8x7B 为例
光说概念不够直观,我们拿 Mixtral 8x7B 的公开配置手算一遍。它的配置是:32 层,隐藏维度 4096,注意力 32 个 head、8 个 KV head,每个 head 维度 128,FFN 中间维度 14336,词表 32000,每层 8 个专家,top-2 激活。
先算单个专家的参数量。SwiGLU 结构的 FFN 有三个矩阵:
- gate_proj:4096 × 14336 = 58.7M
- up_proj:4096 × 14336 = 58.7M
- down_proj:14336 × 4096 = 58.7M
合计每个专家约 176M 参数。
一层 8 个专家就是 1.41B。32 层下来,专家部分总共 45.1B。
再看共享部分。注意力层里:
- q_proj:4096 × 4096 = 16.8M
- k_proj:4096 × (8 × 128) = 4.2M
- v_proj:4096 × (8 × 128) = 4.2M
- o_proj:4096 × 4096 = 16.8M
一层注意力约 42M,32 层共 1.34B。加上词表嵌入 32000 × 4096 = 131M。
总参数 = 45.1B + 1.34B + 0.131B ≈ 46.6B,跟官方标称的 46.7B 完全对得上。这也顺便解释了为什么它叫 "8x7B" 却只有 46.7B 而不是 56B——注意力层和嵌入层是共享的,不复制,而且每个专家也没到 7B 那么大,命名里的 7B 是粗略的营销口径。
再看激活参数。每个 token 每层只激活 2 个专家,专家部分激活 176M × 2 × 32 = 11.27B。加上共享的注意力 1.34B 和嵌入 0.131B,总共约 12.7B,跟官方标称的 12.9B 基本一致(差异来自 RMSNorm 等小参数和舍入)。
看完这个手算你应该能直观感受到:46.7B 的总容量,每次只动用 12.9B,参数利用率大概是 27.6%。这个比例还可以调,把专家数做到 64 个、top-4 激活,利用率能压到 6% 左右,DeepSeek-V3 就是这么干的——671B 总参数,37B 激活,利用率 5.5%。
3.3 为什么 MoE 推理卡的是显存带宽
单流生成场景下(batch size 为 1),大模型推理的性能瓶颈不是算力,而是显存带宽。原因很简单:每生成一个 token,都要把参与计算的权重从显存读进计算单元,读一遍,算一次,然后这个 token 就过去了,权重不会被重复使用(激活值会被缓存,权重不会)。所以速度上限约等于"显存带宽 ÷ 需要读取的权重字节数"。
对稠密模型,这个数是固定的。对 MoE,需要读取的是当前激活的专家权重。Mixtral 激活 12.9B,FP16 下是 25.8GB,如果显存带宽是 1TB/s,理论上限约 38 token/s。换成稠密 47B 模型,需要读 94GB,理论上限就掉到 10 token/s 出头。差了将近四倍,这就是 MoE 在同硬件下生成速度更快的根本原因。
注意:这个估算只是理论上限,实际速度还会被 KV cache 读取、kernel 启动开销、路由计算等因素稀释,通常能跑到理论值的六到八成就不错了。
3.4 Batch 对 MoE 吞吐的影响
刚才说的是单流场景。一旦并发上来,MoE 的表现会变得更有意思。
batch 变大的时候,不同请求的 token 会各自路由到不同的专家。如果 batch 足够大,很可能所有专家都被激活了——这时候 MoE 就退化成了一个稠密模型,算力需求反而比同等激活参数的稠密模型高,因为你还要额外承担路由开销和可能的重复计算。
但反过来说,大 batch 下 GPU 的算力利用率会显著提升,因为矩阵乘法的形状更大了。所以 MoE 在服务端(高并发)和端侧(单流)的最优策略完全不同。端侧强调低延迟,MoE 靠低激活量取胜;服务端强调高吞吐,MoE 靠专家并行把不同专家的计算分摊到不同卡上。
这也是为什么 vLLM 这类推理引擎要专门支持"专家并行"——它把专家切到多张卡上,每张卡负责一部分专家,用 all-to-all 通信把 token 送到对应的卡上算完再拿回来。在单机多卡场景下,这套机制能显著提升吞吐。
4. 本地部署实操:把 MoE 模型跑起来
4.1 选型第一步:显存估算与量化档位
部署之前先算账。总参数量 × 每参数字节数 = 权重占用,再加上 KV cache 和运行时开销。
不同量化档位的大致字节数:
| 量化格式 | 每参数字节 | 说明 |
|---|---|---|
| FP16 / BF16 | 2.0 | 精度最高,占用最大 |
| Q8_0 | ~1.06 | 几乎无感知损失 |
| Q6_K | ~0.82 | 性价比很高 |
| Q4_K_M | ~0.60 | 最常用的档位 |
| Q3_K_M | ~0.48 | 开始有明显损失 |
| Q2_K | ~0.35 | 应急用,质量下降明显 |
几个常见 MoE 模型的估算:
| 模型 | 总参数 | 激活参数 | Q8_0 占用 | Q4_K_M 占用 |
|---|---|---|---|---|
| Qwen3-30B-A3B | 30.5B | 3.3B | ~32GB | ~18GB |
| Mixtral 8x7B | 46.7B | 12.9B | ~49GB | ~28GB |
| Phi-3.5-MoE | 42B | 6.6B | ~44GB | ~25GB |
| DeepSeek-V3 | 671B | 37B | 不可行 | ~400GB |
KV cache 的占用跟激活参数没关系,只跟层数、KV head 数、上下文长度和 batch size 有关。以 Mixtral 为例,8 个 KV head × 128 维度 × 2(K 和 V)× 32 层 × 2 字节 ≈ 131KB/token。32K 上下文就是 4.2GB 左右。这个数不算小,长上下文场景要单独规划。
提示:MoE 模型的显存规划要把"权重"和"KV cache"分开算。权重是死的,KV cache 随上下文线性涨。很多人配好模型发现跑长文本就 OOM,问题都出在 KV cache 上。
4.2 Ollama 路线:最省事的起步方式
Ollama 对 MoE 的支持已经比较成熟,模型库里能直接拉到的 MoE 模型不少。基本流程是这样:
# 拉取模型,会自动选择匹配你显存的量化档位 ollama pull mixtral:8x7b-instruct-v0.1-q4_K_M # 直接跑 ollama run mixtral:8x7b-instruct-v0.1-q4_K_M默认参数不一定适合你的机器,建议写个 Modelfile 自己调:
FROM mixtral:8x7b-instruct-v0.1-q4_K_M # 上下文长度,按需给,给大了吃 KV cache PARAMETER num_ctx 8192 # offload 到 GPU 的层数,0 表示全 CPU,999 表示全 GPU PARAMETER num_gpu 999 # 并发请求数,单人流式对话设 1 就行 PARAMETER num_parallel 1 # 采样参数 PARAMETER temperature 0.7 PARAMETER top_p 0.9 PARAMETER repeat_penalty 1.1 SYSTEM "你是一个严谨的助手,回答简洁直接。"ollama create my-moe -f Modelfile ollama run my-moe几个实操要点。num_gpu是最关键的参数,它控制有多少层放到 GPU 上。MoE 模型有个好处:即使把一部分层放到 CPU,因为激活参数少,CPU 的计算压力也不大,速度会比同规模的稠密模型好很多。我的经验是,如果显存只够放一半的层,MoE 全 CPU 混合跑出来的速度往往比同参数稠密模型快两到三倍。
开启 Flash Attention 和 KV cache 量化能省不少显存。这两个是环境变量控制的,需要重启 Ollama 服务:
export OLLAMA_FLASH_ATTENTION=1 export OLLAMA_KV_CACHE_TYPE=q8_0q8_0的 KV cache 量化几乎不掉质量,能省一半的 KV 显存;q4_0能省更多,但长上下文下会有可感知的退化,自己权衡。
4.3 llama.cpp 路线:把专家权重丢到 CPU 内存
如果你显存不够塞下全部权重,但内存管够,llama.cpp 有个专门为 MoE 设计的参数,能把 MoE 层的专家权重单独放到 CPU 内存里,其他部分留在 GPU。这个设计非常聪明——前面说过 MoE 的算力需求低,CPU 完全扛得住专家那部分计算,而 GPU 保留注意力层能保住上下文处理速度。
关键参数是--n-cpu-moe,含义是把前 N 层的 MoE 专家权重放在 CPU:
./llama-cli \ -m ./models/mixtral-8x7b-instruct-v0.1.Q4_K_M.gguf \ -ngl 99 \ --n-cpu-moe 20 \ -c 8192 \ -ctk q8_0 -ctv q8_0 \ --flash-attn \ -p "解释一下什么是稀疏激活"-ngl 99表示所有层都交给 GPU 处理(注意这跟专家权重放在哪是两回事),--n-cpu-moe 20表示前 20 层的专家权重放 CPU,后面的层专家放 GPU。剩下的层全放 CPU。这个数字需要根据你的显存和内存反复试,目标是让显存占用刚好卡在安全线下。
如果你的 llama.cpp 版本没有--n-cpu-moe,可以用更通用的张量覆盖参数:
./llama-cli \ -m ./models/mixtral-8x7b-instruct-v0.1.Q4_K_M.gguf \ -ngl 99 \ -ot "\.ffn_(up|down|gate)_exps\.=CPU" \ -c 8192-ot后面跟的是正则表达式,匹配到的张量会被放到指定设备。CPU表示 CPU 内存。这个写法更灵活,你可以精确控制哪些张量放哪。
注意:把专家放 CPU 会走 PCIe 传输,如果一次激活的专家数据量很大,PCIe 带宽可能成为新瓶颈。实践中
--n-cpu-moe的值不要超过总层数的三分之二,否则速度会掉得很难看。
我自己在 24G 显存 + 64G 内存的机器上跑 Mixtral Q4_K_M,--n-cpu-moe设到 18 左右比较舒服,生成速度能到 12-18 token/s,而同样条件下纯 CPU 跑只有 5 token/s 上下,纯 GPU 又塞不下,这个混合方案算是把机器用透了。
4.4 vLLM 路线:多卡并发场景
如果你手上是多张卡,而且要做并发服务,vLLM 是更合适的选择。它对 MoE 的支持主要靠专家并行:
vllm serve Qwen/Qwen3-30B-A3B \ --tensor-parallel-size 2 \ --enable-expert-parallel \ --gpu-memory-utilization 0.92 \ --max-model-len 16384 \ --max-num-seqs 32 \ --port 8000这里几个参数值得说。--tensor-parallel-size 2是张量并行度,按卡数设置。--enable-expert-parallel打开专家并行,让不同专家分布到不同卡上,每张卡只持有部分专家。--gpu-memory-utilization 0.92是显存利用率上限,留 8% 给运行时开销,别设成 1.0,容易 OOM。--max-num-seqs控制并发序列数,MoE 模型在大并发下专家全激活,显存和算力压力都会涨,这个值需要根据实测调。
服务起来之后用 OpenAI 兼容接口调用:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="none") resp = client.chat.completions.create( model="Qwen/Qwen3-30B-A3B", messages=[{"role": "user", "content": "用三句话说明 MoE 的原理"}], temperature=0.6, max_tokens=256, ) print(resp.choices[0].message.content)--enable-expert-parallel这个开关在单卡上没意义,只有 TP > 1 的时候才生效。另外注意,专家并行会引入 all-to-all 通信,如果卡之间是 PCIe 而不是 NVLink,通信开销可能吃掉并行带来的收益,这点在双卡 4090 这类配置上尤其明显,实测下来 TP=2 的吞吐提升可能只有 1.4 倍而不是 2 倍。
4.5 上下文与 KV Cache 的配置取舍
MoE 模型在上下文这块没有特殊性,但因为权重本身占用大,留给 KV cache 的显存余量往往更紧张,所以配置得更精细。
以 Qwen3-30B-A3B 为例,它的注意力是 GQA 结构,KV head 数远小于注意力 head 数,KV cache 本来就省。假设 4 个 KV head × 128 维度 × 2 × 48 层 × 2 字节 ≈ 98KB/token。给 32K 上下文,单序列就是 3.1GB。如果你要跑 8 路并发,就是 25GB,比模型权重(Q4 约 18GB)还大。
几个实用做法:
- 量化的 KV cache(
-ctk q8_0 -ctv q8_0)能省一半,质量损失可忽略 - 按实际需要设
-c,不要上来就 128K - 用
--n-cpu-moe把专家赶去 CPU,腾出显存给 KV cache - 多轮对话场景可以开启上下文滑动窗口,只保留最近 N 个 token
5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 加载模型时 OOM | 权重量化档位过高 | 换更低量化档,或增加--n-cpu-moe |
| 跑起来后中途 OOM | KV cache 随上下文增长 | 缩短-c,开启 KV 量化 |
| 生成速度个位数 | 层数过多落在 CPU | 调大-ngl,减小--n-cpu-moe |
| 首 token 延迟高 | 预填充阶段计算量大 | 缩短上下文,检查是否在 CPU 上预填充 |
| 输出重复、循环 | 量化损失或采样参数问题 | 提高repeat_penalty,换 Q5 以上量化 |
| 回答明显变笨 | 低比特量化损伤大 | 专家层对量化更敏感,建议 Q5 起步 |
| 多轮后速度骤降 | KV cache 增长 | 限制历史长度,开启滑动窗口 |
5.2 显存不够时的几条降级路线
按代价从低到高排一下。
第一档是降量化。Q8 到 Q4_K_M,显存占用几乎减半,质量损失在日常对话里基本感知不到。但要注意 MoE 对量化比稠密模型敏感,因为路由决策依赖隐藏状态的精确数值,量化误差可能让本该选 3 号专家的 token 选了 5 号,输出就飘了。所以不建议一步跨到 Q2_K,Q4_K_M 是比较稳妥的下限。
第二档是专家权重转 CPU。这是 MoE 专属的杀手锏,用--n-cpu-moe控制比例。代价是速度,但因为只影响专家部分,而专家部分算力需求低,实际损失比稠密模型 offload 小得多。
第三档是换更小的 MoE 模型。30B 级别的 MoE 激活只有 3B 左右,一张 16G 卡配 32G 内存就能舒服跑起来,质量对日常使用完全够。
第四档是分布式。多台机器通过局域网跑张量并行,这是最后的选项,配置复杂度高,收益也不一定有想象中大。除非你已经有多台闲置机器,否则不太值得折腾。
5.3 速度慢、首 token 延迟高的定位方法
先判断瓶颈在哪。看一眼 GPU 利用率:如果连续生成时 GPU 利用率一直在 95% 以上,那是 GPU 算力打满了,说明你可能把太多层放 GPU 了;如果利用率在 30% 上下波动,多半是等 CPU 或等内存带宽,说明 offload 太多。
再看几个指标。首 token 延迟(TTFT)主要受预填充阶段影响,这个阶段是并行计算的,对算力敏感;出词速度(TPOT)主要受解码阶段影响,对显存带宽敏感。两个指标分开测,能快速定位问题在哪个阶段。
如果 TTFT 高但 TPOT 正常,多半是上下文太长或预填充跑在 CPU 上,检查-c的设置和-ngl的值。如果 TTFT 正常但 TPOT 低,是解码阶段的带宽瓶颈,可以考虑减少 offload 到 CPU 的层数,或者换更低量化让数据量变小。
有个容易被忽略的点:--flash-attn一定要开。不开的话注意力计算会走一条慢得多的路径,长上下文下差距能到两三倍。
5.4 输出质量异常的排查
MoE 模型出现质量问题时,排查顺序跟稠密模型不太一样。
先看路由。如果你的推理框架支持输出专家命中统计(llama.cpp 有--verbose能看部分信息),观察一下各专家的激活频率是否严重偏斜。如果一个模型大量 token 都挤在少数几个专家上,说明它的路由训练有问题,这种情况下再怎么调参也救不回来,只能换模型。
再看量化。MoE 的专家层参数在低比特下损失比注意力层大,因为专家各自负责细分领域,参数分布更集中,量化误差相对影响更大。如果怀疑是量化问题,把同一个模型换成 Q5_K_M 或 Q6_K 对比一下,质量如果明显回升就是量化的锅。
最后看采样参数。MoE 模型的路由本身就带一点随机性(虽然推理时噪声关闭,但不同量化实现可能有差异),温度设得太高会让输出的稳定性变差。日常使用建议 temperature 在 0.6 到 0.8 之间,别拉满。
6. 几处容易翻车的地方
6.1 量化档位的取舍:MoE 比稠密模型更娇气
我踩过的最大一个坑就是拿稠密模型的经验去套 MoE。稠密模型 Q3_K_M 虽然掉点,但日常对话还能用;MoE 模型用 Q3 经常出现"答非所问",你问 A 它答 B,看着不像是模型变笨,更像是"理解错了"。后来想明白了,这是路由环节被量化误差带偏了——隐藏状态的细微偏差导致 token 被分给了不合适的专家,输出自然就跑偏。
所以我的建议是:MoE 模型的量化下限定在 Q4_K_M,空间允许就上 Q5_K_M 或 Q6_K。多出来的那点显存,换来的稳定性提升非常值。如果实在塞不下,优先考虑 offload 专家到 CPU,而不是继续压量化档位。
另外提一句,不同量化格式对 MoE 的影响差异也挺大。同样是 4 bit,K-quants 系列(Q4_K_M)保留了更多的重要通道精度,在 MoE 上表现明显好于旧的 Q4_0。选模型文件的时候认准 K-quants 后缀。
6.2 MoE 的微调跟稠密模型不是一回事
如果你打算在本地对 MoE 模型做微调,有几个和稠密模型不同的点要注意。
第一,LoRA 的挂载位置。稠密模型通常挂在注意力和 FFN 的线性层上,MoE 模型如果也这么干,训练信号会分散到所有专家上,而每个专家在单次前向里可能根本拿不到几个 token。更合理的做法是只对注意力层做 LoRA,或者结合路由统计,只对高频激活的专家做微调。全专家微调基本等于从头训,代价太大。
第二,路由会漂移。微调会改变隐藏状态的分布,导致原本的路由决策发生变化,出现"灾变遗忘"——微调完某个领域,其他领域的能力反而下降了。缓解办法是冻结路由器,或者加一个路由一致性约束,这个在 LLaMA-Factory 这类框架里需要通过自定义损失来实现。
第三,显存需求是非线性的。稠密模型微调和推理的显存差距大概是 3 到 4 倍,MoE 因为要维护所有专家的优化器状态,这个倍数会更大。一个 30B 的 MoE 用全参数微调,即便开了 ZeRO-3 也要好几张 80G 卡,本地基本不现实。
我个人现在的做法是:MoE 模型只做推理,不做微调;要定制能力就用 RAG 或者提示词工程。真需要微调,退回小尺寸的稠密模型,成本可控得多。这条经验不一定适合所有人,但对于只有一两张卡的个人开发者,我觉得这是最务实的选择。
再说个小细节。MoE 模型的专家分工在预训练阶段就基本成型了,你在推理时其实可以通过观察不同 prompt 触发的专家组合,反推模型的知识分布。有些推理框架会把专家命中情况打进日志,拿一批领域样本跑一遍,统计各专家的激活频率,能大致看出哪些专家负责代码、哪些负责自然语言。这个信息在做领域适配的时候挺有用,虽然有点"黑箱解剖"的意思,但比盲猜靠谱。