DeepSeek-R1 是目前开源社区 size 最大的 reasoning model,是由 DeepSeek-V3 通过 Post-training 之后产出的,一共有 671B 参数(~37B 激活参数)的 MoE,如果使用 16-bit 的精度进行加载和推理,需要 1.3T memory footprint,哪怕是 H100 这样的机器,单卡显存有 94G,使用两机 16 卡也就刚刚能放下模型,几乎没有额外的 memory 来放 kv cache。 对于这样 size 巨大的模型一定需要进行量化来降低模型的显存占用,DeepSeek 官方在 paper 里面详细描述了他们的 FP8 量化方案,相较于之前的 channel-wise 的方法,其采用了粒度更小的 block-wise 的方案,具体的示意图如下,这里就不做深入介绍了。 <div align=center> <img src="block-fp8.png" width=700> </div> DeepSeek-V3/R1 相较于之前的模型来说,不仅 size 巨大,而且是稀疏的 MoE 架构,这两个特点使得 low-bit 量化成为可能,下面调研社区关于 MoE 量化的一些方案,以及最佳实践的总结。 ## MoE 的特点 MoE 和 Dense 模型在结构方面存在差异,这也导致了他们在面对量化时的不同问题。 首先我们看看 MoE 的两个显著特点: 1. Spare Activation:在 MoE 中每个 input token 只会激活一部分专家,使得整体结构是稀疏的; 2. MoE Gating:MoE router 部分非常敏感,量化的微小扰动会改变 top-k 排序,从而扰乱模型推理中使用的专家; 由于其稀疏的特性,在论文 [MoQE](https://openreview.net/pdf?id=G_D6xThdQe4) 有下面的结论: > MoE is much more robust to quantization than the dense model. 更具体来说,这是由于每个 token 在 forward 的过程中只会经过模型的一部分参数,所以量化 noise 的整体影响由于专家 ensemble 的特性而缓解。 同时也是在 MoQE 这篇论文中提到,MoE 的权重相比于 FFN 的权重,在分布上更加光滑,有更少的 outlier,可以参考下图 <div align=center> <img src="moe_weight_distribution.png"> </div> 在上面的图中,偶数层是 moe ffn,奇数层是 dense ffn,可以看出奇数层相比之下有更大的 scale。 这也使得量化 MoE expert 相比于 Dense 模型 FFN 来说更加容易,也进一步说明了 MoE 量化相比 Dense 来说,更加 robust,可以接受更激进的量化方式(3-4 bit)。 ## Challenges in MoE Quantization 前面讲了 MoE 的相比 dense 易于量化的部分,不过 MoE 也由于其本身的特点,存在下面一些量化的挑战: 1. Outlier Weights and Activations outlier 问题是 LLM 量化中的主要的挑战,是指 weight 和 activation 中有一小部分数值显著比其他大值的现象,这在 MoE 中也同样存在。最近的研究 [The Super Weight in Large Language Models](https://arxiv.org/abs/2411.07191) 表明,仅仅删掉一小部分权重(super weight) 会直接摧毁模型的性能。 2. Imbalanced Expert Usage MoE 中 expert 的使用并不是均衡和平衡的,这意味着有一些专家会有更高的概率被 route 到,而另外一些专家概率更低。 最近的研究 [QuantMoE-Bench](https://arxiv.org/abs/2406.08155) 通过对比 Mixtral-8x7B 和 DeepSeek-MoE-16B 的专家使用情况,发现 Mixtral 有更平均的使用情况,而 DeepSeek-MoE 专家使用是一个 skewed distribution。 <div align=center> <img src="moe_expert_usage.png" width=700> </div> ## Best Practice for MoE Quant 前面讲了 MoE 量化的挑战,针对这些问题,有哪些解决方案呢?下面根据最近的一些研究论文,提出了一些最佳实践。 ### Outlier-aware Quant 不同的模型有不同的 outlier weight/activation pattern,可以采用 [The Super Weight in Large Language Models](https://arxiv.org/abs/2411.07191) 论文中的方案,通过 activation spikes 的方式来识别出 super weight 的下标,简单来说就是通过 activation 中的极值来找到决定他的 weight 的下标,然后将这个值置 0 之后再重复,这样经过多次迭代就可以找到所有的 super weight。 论文中也列出了一些模型的极值坐标,可以直接通过下标访问对应权重进行查看。 <div align=center> <img src="super_weight.png"> </div> 在找到这些 SWs(super weights) 之后,可以在量化前先 clip 这些 outlier weights,以消除他们对量化的影响,后续在推理过程中,可以对反量化之后的权重恢复这些 outlier 来保留他们对模型精度的影响。 同样也可以对 super activation 做相似的处理,经过这样的方案,最终模型能够完全 outlier 的影响。 还有一种更简单的方案就是针对这些 outlier weights 采用更大的 bit budget,这样也能缓解 outlier 的问题。 ### Expert-aware Mixed Precision 上面也提到不同的 expert 有不同的 route 概率,所以一个简单的启发式方案就是通过专家的使用频率来决定那些专家更重要,可以通过校准数据来统计每个专家的使用频率,然后对使用更多(更重要)的专家采用更大的 bit 量化,以充分保留模型的量化精度。 ### Layer-wise Bit Allocation 在 [QuantMoE-Bench](https://arxiv.org/abs/2406.08155) 论文中发现: **earlier MoE blocks (layers) are more critical than later ones when it comes to quantization​.** 这表明早期的误差会对模型的精度存在更大的影响,下面是论文中的实验结果 <div align=center> <img src="moe_quant_first_layers.png"> </div> 从实验结果可以看出,保留前 4/8 层更大的 bit 位可以获得更好的精度,这在 Mixtral 和 DeepSeek-MoE 中能看到完全一致的结论。 所以在低 bit 量化的过程中,如果发现精度不满足要求,可以考虑给前几层更大的 bit 预算来减缓量化引入的误差。 ### Prioritize Attention and Shared Components 除了在 layer 层面分配不同的量化 bit 预算,还可以考虑在同一层内分配不同的 bit 预算。对于 MoE 来说,给更多的 bit 预算给那些输入都会被激活的参数更划算,因为这些 component 会影响所有的 token,而 MoE 中的 expert 只会影响到被 route 给它们的 token。 所以对 MoE 来说,如果想要更好的 precision retention,可以考虑对 attention 和 shared_experts 分配更大的 bit 预算。 基于上面的 best practice,可以看看下面 unsloth 对 R1 量化的 blog 中为了做 precision retention 的一些方法,它采用的方案都是上面 best practices 中提到的。 ## Case Study: unsloth dynamic quant unsloth 在 1.27 发布了一篇 [blog](https://unsloth.ai/blog/deepseekr1-dynamic) 介绍他们将 V3/R1 成功量化到 1.58-bit 的方案,成功将模型大小减少了 80%。 他们初步尝试通过 naive 方案是直接将所有权重量化到 1.5-bit 失败了,模型会一直不断循环,而且生成乱码。最终采用 dynamic quant 的方案获得了可以生成正常结果的模型,整体结果如下 | MoE Bits | Disk Size | Type | Quality | Down_proj | | :------- | :-------- | :------ | :------ | :----------- | | 1.58-bit | 131GB | IQ1_S | Fair | 2.06/1.56bit | | 1.73-bit | 158GB | IQ1_M | Good | 2.06bit | | 2.22-bit | 183GB | IQ2_XXS | Better | 2.5/2.06bit | | 2.51-bit | 212GB | Q2_K_XL | Best | 3.5/2.5bit | > Type 的具体含义可以通过 [llama.cpp repo](https://github.com/ggml-org/llama.cpp/blob/d7cfe1ffe0f435d0048a6058d529daf76e072d9c/examples/quantize/quantize.cpp#L18) 进行查看 > > llama.cpp 中会将 weight 量化成特定低 bit 进行存储,而 activation 是在 runtime 时 on-the-fly 量化成 int8 进行推理[^1] 不过他们的结果测试做得并不严谨,并没有采用 general benchmarks 进行测试,而是给 3 次机会让 R1 去生成 Flappy Bird 的游戏,然后让 10 个评委打分。 整体的结果对比也没什么信息量,只是将 naive 方案和 dynamic 方案的结果做了一些生成结果的比较,比如说 naive 方案会产生类似的无限循环结果 ”Colours with dark Colours with dark Colours with dark Colours with dark Colours with dark“ 等,所以整体的精度 eval 做得非常一般,结果也不具有太多可信度,不过他们的做法可以带来一些启发,下面我们具体讲一下 dynamic 量化的做法。 主要的做法如下: 1. 前三层 dense layer 占据全部权重的 0.5%,保留他们到更高的 bit-width 影响不大,所以保留他们到 4-bit 或者 6-bit。 2. MoE 使用了 shared_experts,整体占据全部权重的 1.5%,也保留为 6-bit。 3. 保留 MLA 的所有部分为 4-bit 或者 6-bit,他们占据整体权重 <5%;如果想要更小的内存占用,可以把 attention output 量化到更小的 bit 位,但是对精度存在影响; 4. `down_proj` 对于量化异常敏感,特别是前几层,这个在 super weight[^2] 论文里也得到了证实,下面是它的实验结果,它发现几乎所有的 `down_proj` 都不应该被量化。 <div align=center> <img src="attachments/super_weight.avif" width=800> </div> 5. 对于 `embedding` 和 `lm_head` 都保留成 4-bit 或者 6-bit,MoE router(gate) 以及所有的 norm 操作都是用 32-bit。 可以看到上面这 5 点方案都对应于 best practices 中提到的做法。 ## Conclusions 本篇文章探索了 MoE 量化的一些方案,主要参考了最近的一些 research papers,首先分析 MoE 量化的一些问题,然后提出了一些 best practices,最后根据 unsloth 对于 R1 量化到 1.58-bit 的 blog 验证了这些 best practice 的有效性。 MoE 量化依然是一个值得研究的问题,首先是量化的 Eval 比较有难度,目前采用的方式都是通过 benchmarks 或者跑 PPL 等方式,这些方式不能有效地衡量量化前后的精度差异。另外对于像 R1 这种参数量巨大的 MoE 模型,社区也并没有太多的探索,所以 MoE 量化依然是一个 open problems,期待社区进行更多的探索。 ## Reference - [Run DeepSeek R1Dynamic 1.58-bit](https://unsloth.ai/blog/deepseekr1-dynamic) - Zafirakis _et al._, "Mixture of Quantized Experts (MoQE): Complementary Effect of Low-Bit Quantization and Robustness" https://openreview.net/pdf?id=G_D6xThdQe4 - Mengxia Yu _et al._, "The Super Weight in Large Language Models" https://arxiv.org/pdf/2411.07191 - Pingzhi Li _et al._, "Examining Post-Training Quantization for Mixture-of-Experts: A Benchmark" https://arxiv.org/abs/2406.08155 [^1]: https://github.com/ggml-org/llama.cpp/discussions/3349#discussioncomment-7120779 [^2]: https://arxiv.org/pdf/2411.07191