Qwen3.6-35B-A3B 剪枝实战:在单卡 RTX 5090 上把 MoE 压缩到极限的七轮实验

2026-05-18 • edited 2026-05-19

本文是 Pruning Qwen3.6-35B-A3B for RTX 5090 的中文版,更新至第 7 轮实验后的完整记录。

为什么会有这篇文章

我手头有一个 35B 参数的 MoE 模型,需要跑在一张 RTX 5090 上。这个模型在 FP8 下需要 34.4 GiB —— GPU 只有 31.84 GiB。差距只有 2.6 GiB,小到让人觉得唾手可得,大到足以让所有标准部署流程全部失效。

六天后,我得到了第一个可用的剪枝模型:v3,HumanEval+ 73.2%、Toolcall 51.0%、MMLU 33.6%,FP8 下仅 26.3 GiB。它成了当时最好的输出。

二十四小时后,我发现自己之前对「标定(calibration)」的理解全都局限于某一个配方,完全没有泛化性。又过了十二个小时,v7b-fp8 诞生了——它在每一个评测包上都超越了 v3,BugFind 提升 17 分、DataExtract 提升 17 分、InstructFollow 提升 20 分。

从最初到最终的路径远非线性。那些我曾深信不疑的结论,有些被后续实验彻底推翻。这篇文章是我希望第一天就能知道的事情——经过七轮实验反复修正后的版本。

Qwen3.6-35B-A3B 是一个 256 专家的 MoE 模型,专为 agentic 编程场景优化。我的全部硬件只有一张 RTX 5090。本文涵盖剪枝、量化、评测、以及尝试性的恢复微调——全部在单张消费级 GPU 上完成。不涉及多卡训练、云端推理、或其他剪枝算法的横向对比。

为什么 2.6 GiB 的缺口吃掉了我一周

通常的看法是:把模型压缩 8% 来适配显存预算,不过是例行公事——跑个 AWQ 或 GPTQ,调一下量化配置,然后上线。2.6 GiB 对 34.4 GiB 来说是 7.5% 的压缩率,量化本身理应轻松搞定。

现实并非如此。

问题立刻显现:BitsAndBytes、GPTQ、AWQ 全都只量化 2D 的 nn.Linear 权重。MoE 模型把专家参数存储为批量的 3D 张量——[n_experts, in_dim, out_dim]——这是为了高效的分组矩阵乘法(grouped MM)。这些 3D 张量占了模型总参数的大约 90%,而每一种标准量化工具都会直接跳过它们。没有报错,没有警告。就是原样保留全精度。

所以光靠量化关不掉这个缺口。我需要专家剪枝。

REAP(Router-Weighted Expert Activation Pruning)通过以下公式给每个专家打分:

$$S_j = \mathbb{E}_{x \in X_j}[g_j(x) \cdot |f_j(x)|]$$

直觉上讲,一个既获得低门控权重又产生小激活值的专家,对模型输出的贡献有限,可以被移除而不会造成显著的重建误差。然后 REAP 移除得分最低的专家,并传播残差以保持模型的功能流形拓扑结构。

剪枝器按 block 逐个处理:把一层 750 MiB 的解码器加载到 GPU,评分所有幸存专家,剪掉得分最低的,传播残差,保存。一次实验处理 40 层解码器,大约 25 分钟。我在这张 GPU 上跑了七轮实验,持续七天。

整个过程中最具约束性的事实是:RTX 5090 的 32 GiB 同时是我的推理平台、评测框架和训练环境。剪枝、评测和微调争夺同一块显存,且任何时候最多只能运行其中一项。在 BF16 下,模型占用 30.6 GiB——GPU 容量的 97%,训练时连梯度和激活值的空间都不剩。

最终,真正的难题不是剪枝算法本身,而是这种全方位的资源竞争。

重写我认知的标定实验

刚开始时,我预期剪枝算法或压缩比会主导模型质量——文献也是这么写的:更好的重要性评分,更好的剪枝决策。

证据指向了别处。

第一次实验(v2)用了四个代码类标定数据集:evol-codealpaca、BigCodeBench、SWE-bench、xlam。纯代码数据。结果:HumanEval+ 72.0%、Toolcall 44.0%、MMLU——两个类别直接归零。

死的类别。模型能写代码,但回答不了一个常识问题。剪枝器在评分阶段从未见过通用知识的 token,所以它没有办法知道哪些专家对通用领域是重要的。承载通用知识的专家要么被剪掉,要么被严重削弱了。

第二次实验(v3),我换成了 70/30 的代码/通用混合。同样的 REAP 算法,同样的压缩比。只是在四个代码数据集(各 700 条样本)的基础上,加了两类通用数据集——600 条 MMLU 和 600 条 C4。

类别v2(纯代码标定)v3(70/30 标定)变化
MMLU 社会科学0.0%33.3%+33.3pp
MMLU 其他0.0%34.3%+34.3pp
HumanEval+72.0%73.2%+1.2pp
Toolcall44.0%51.0%+7.0pp

每一项指标都提升了。MMLU 从死亡状态恢复到 33%+。代码类指标也提升了。

我倾向于这样理解:标定数据是剪枝器观察模型的透镜。窄透镜(纯代码)视野锐利但狭窄——剪枝器只保留它看到的东西,模型在其他领域就失明了。宽透镜(70/30 混合)让剪枝器看到完整的功能空间,从而保留了服务于整体分布的结构,而非仅仅一个模态。

这个类比有一个局限:你不能无限扩充标定领域。更多数据意味着更长的评分阶段。但在实际预算范围内,证据是明确的——标定数据的组成比任何对剪枝器本身的算法调优都更重要。

v3 发布了,我继续前进。这时故事才真正开始变得有趣。

我的结论只是某个配方的特例

v3 发布后,我回去测试了一个看似显而易见的假设:如果用 agentic 轨迹替换代码类标定数据,剪枝后的模型在 agentic 基准上应该有更好的表现。工具调用、缺陷查找、多步推理——这本就是这个模型的设计目标。

我搭建了一套完整的 BenchLocal 评测框架——8 个评测包覆盖工具调用、agent 能力、缺陷查找、数据提取、指令跟随、数学推理、结构化输出和 CLI——并在新的 v19 对话模板下建立了 v3 基线。基线数据让人清醒:ToolCall-15 90 分、HermesAgent-20 16 分、BugFind-15 8 分。agentic 门槛本来就低。

我在更深的压缩率(0.40,保留 154/256 专家)下跑了两个候选实验,用了两种标定策略:

  • Mix-A:完全替换——用 agentic 数据(glm47-reap + hermes-agent-traces)替换代码数据
  • Mix-B:叠加——在 v3 原始标定的基础上叠加 agentic 数据

两者给出了相同的结果。ToolCall-15 从 97(旧推理解析器下的 v3 基线)掉到 90。HermesAgent-20 恰好停在 16。BugFind-15 在 3-10 之间波动。没有一项 agentic 指标被拉动。三个门控全部失败。

我停下来跑了一个对照实验。拿 Mix-A 的标定列表,在 v3 的压缩比(0.289,保留 183 专家)下重新剪枝。如果压缩深度是 toolcall 退化的原因,对照实验应该恢复 toolcall。如果标定内容才是原因,退化应该仍然存在。

对照实验的结果是决定性的:

候选压缩比标定方式ToolCall-15HermesAgent-20BugFind-15
v30.28970/30 均衡97168
Mix-A0.40agentic 替换901610
Mix-B0.40叠加式90163
v3ratio0.289Mix-A 标定90160

全部三个候选的 ToolCall-15 都是 90,包括在 v3 压缩比下的对照实验。退化来自标定内容,而非压缩深度。

进一步分析发现,退化集中在一个非常具体的子维度:参数精确度(Parameter Precision)从 100 掉到了 67。模型仍然能选对工具、保持正确的结构——只是更频繁地生成类型错误或格式错误的参数。去掉代码类语料(evol-codealpaca, bigcodebench, swe-bench)换成 agentic 轨迹,让模型丢失了严格的参数格式化能力。

另一个发现更加残酷。HermesAgent-20 在全部四个配置下都是 16 分——字面意义上的完全一致,连子类别得分都一模一样。一个 25B 的剪枝 MoE 无法处理这些多步浏览器自动化场景,不管你往剪枝标定里喂什么数据。这个门控是容量受限的。

我还发现 vLLM 的 --reasoning-parser qwen3 标志对正确评测至关重要。如果没有它,模型的 <think> 推理块会泄漏到所有纯文本响应中,破坏所有非工具调用的评分器。加上这个标志后,ToolCall-15 从 90 提升到 97,instruct-follow 和 data-extract 从零分恢复到正常水平。教训是:在你信任任何数字之前,先验证你的评测基础设施。

第 6 轮实验以一份干净的否定结果收场,我得出的结论是:标定内容无法拉动 agentic 基准。这个结论大约维持了十二个小时。

真正突破的配方

第 7 轮实验采用了一个完全不同的标定配方:REAP-26B 六数据集混合。六个数据集——SWE-bench/SWE-smith-trajectories(tool 分片)、xlam-function-calling-60k、evol-codealpaca、以及 Mixture-of-Thoughts(code/math/science)——在更高的 token 数下(1024 样本 × 16384 序列长度,总计 1680 万 token)。同时按照 REAP-26B 的 README 建议禁用了路由器重归一化。

我跑了三个方案加一个补充实验:

方案 A(压缩比 0.40,从头剪枝)。ToolCall-15 崩到 63——灾难性的 -27 退化。但 BugFind 跳到 +15,InstructFollow 跳到 +33。这个配方显然很强——在这个深度下强过头了。

方案 C(在上游 REAP-26B-VL 基础上叠层剪枝)。toolcall 恢复到 90,但配方的其他增益损失了约 90%。叠层剪枝无法继承上游的标定信号。

方案 B(压缩比 0.289,v3 的深度——我最初跳过的实验)。这是在方案 A 和方案 C 都失败后的补充实验。

候选压缩比ToolCall-15BugFind-15DataExtract-15InstructFollow-15判定
v3+v190.289908520基线
v7a0.4063232453FailToolcall
v7c叠层900416FailAgentic
v7b0.28993252240通过

v7b-fp8 在所有 7 个已测评测包上都等于或优于 v3。没有任何退化。BugFind +17、DataExtract +17、InstructFollow +20、ToolCall +3。触发函数判定:通过。

这个结果我一直反复回味:同一个配方在 0.40 下让 toolcall 崩到 63,在 0.289 下却把 toolcall 提升到 93。配方驱动 agentic 增益,压缩深度调节 toolcall 的权衡。第 6 轮实验的结论——“标定内容无法拉动 agentic 基准”——只是 Mix-A 内容的特例,而非剪枝标定的普适性质。

实现这一切所需的管线升级也值得一提。16K 序列长度的标定需要一个分块 REAP 评分累加器——单次通过的方式会在 32 GiB 的 GPU 上产生 67 GiB 的张量。自定义 FP8 量化器(scripts/quantize_fp8.py)用 274 行代码绕过 llmcompressor 对 Qwen3.6 的兼容性问题,直接从 BF16 转为 torch.float8_e4m3fn。模式适配器配合 60 秒的预检脚本,在 GPU 分配之前就能捕获数据集结构漂移。

3D 张量如何瓦解了所有量化框架

标定实验让我得到了 v3——一个差一点就能通过质量门控的模型。HumanEval+ 73.2% 接近 75% 的阈值。MMLU 33.6% 离 40% 的目标还有距离。下一步自然是恢复微调——在剪枝模型上做 SFT 来拉高剩余的基准。

这是第二个假设崩塌的地方。

我原以为标准量化工具可以处理模型压缩以便训练。加载 4-bit 模型,挂 LoRA 适配器,训练。这是 Hugging Face 上每个 QLoRA 教程的默认流程。它能在 LLaMA 上工作,能在 Mistral 上工作——也应该能在 Qwen 上工作。

不行。因为模型的 3D 专家张量对 BitsAndBytes 来说是不可见的。

我花了第二天整个晚上系统性地排除每一种标准训练方法。七次尝试,全部失败:

尝试方法结果
1BnB 4-bit QLoRA无法量化 3D 专家张量 [183, 1024, 2048]
2BF16 model.to(‘cuda’)30.6 GiB——激活值需要 0 字节空间
3accelerate device_map=‘auto’反向传播时把所有层留在 GPU 上
4DeepSpeed ZeRO-3(单卡)Trainer 在分区前把完整模型移到 GPU
5DeepSpeed zero.Init + from_pretrained权重加载与 meta-device 张量冲突
6FP8 冻结权重 + monkey-patch 算子grouped_mm 反量化产生每层 768 MiB BF16 临时空间
7FP8 + dispatch_model 配合 10 GiB 预算卸载的层在反向传播时全部回到 GPU

我不太理解为什么每一个框架在单卡反向传播时最终都会调用 model.to(device)。文档承诺了 CPU 卸载,实际上 DeepSpeed ZeRO-3、accelerate 的 dispatch_model 和 FSDP 都收敛到同一个行为:梯度需要流动时,把完整模型放到 GPU 上。

解决方案来自一个我从未考虑过的变通方案:把 3D 专家张量解批量为独立的 bnb.nn.Linear4bit。BnB 可以量化标准的 2D 线性层。一个 [183, 1024, 2048] 的 3D 张量变成 183 个独立的 Linear(2048, 1024) 对象,每个都可以用 4-bit 量化。

结果:模型在 GPU 上从 30.6 GiB 降到 16.8 GiB,留下 16.9 GiB 给激活值和梯度。SFT 跑起来了——311 步、9934 条样本、11.5 小时、loss 从 1.058 降到 0.975、token 准确率从 85% 升到 96%。从所有训练指标来看,它成功了。

但它没有成功。

SFT 陷阱:当训练让一切变得更糟

我预期在量化冻结权重上的 SFT 会改善模型。训练曲线很健康。Loss 在下降。Token 准确率在攀升。所有信号都在说"继续训练,它在收敛"。

微调后的评测结果说了另一个故事:

基准微调前微调后变化
HumanEval+73.2%67.7%-5.5pp
Toolcall51.0%50.5%-0.5pp
MMLU33.6%9.4%-24.2pp

每一项都退化了。MMLU 崩回 v2 的水平。HumanEval+ 掉了 5.5 分。

这个机制非常具体且具有启发性——值得停下来仔细理解,因为它解释了一整类管线失败的原因:

4-bit 量化在每个冻结专家层的前向传播中注入了噪声。这个噪声是确定性的——同样的输入、同样的 4-bit 权重、同样的量化误差——但它改变了可训练的路由器和共享专家层所看到的激活分布。在 SFT 过程中,可训练参数适应了这个偏移后的分布。它们学会了配合 4-bit 专家的噪声特征来工作。

当你在推理时移除 4-bit 量化(把微调后的权重合并回原始的 BF16 模型),噪声特征消失了。可训练参数现在运行在干净的激活上,但它们已经过拟合到了一个不再存在的分布。

这就是为什么训练指标看起来很好而基准却崩了。模型并没有学会生成更好的代码或回答知识性问题。它学会了补偿冻结路径中的量化噪声。当噪声消失时,补偿变成了输出失真。

一个我反复回味的结论:微调前的 v3 模型是当时项目的最佳输出。标定策略才是杠杆。微调是一个陷阱。

如果重来一次我会怎么做

如果这个项目从头开始,我会改变很多事情。后续的实验让我意识到早期的一些结论是不完整的——所以下面这些建议是基于七轮实验后的完整认知。

我做了什么应该怎么做为什么
直接在生产规模上做 SFT先在 2 层玩具模型上验证七种失败方案、约 4 小时调试,在玩具模型上几分钟就能发现
评测和训练共用同一个 venv从一开始就隔离 venvhuggingface_hub 1.5 升级到 1.14 破坏了 vLLM 的权重加载
在穷尽标定实验之前就跑 SFT先跑完所有标定实验50/50 实验从未尝试,而标定才是主要杠杆
浅层剪枝(183/256)+ 4-bit SFT深层剪枝(154 专家)+ 干净的 BF16 SFT避免噪声过拟合陷阱
假设 agentic 标定能拉动 agentic 门控先在原始深度下测试 REAP-26B 配方第 6 轮实验的干净否定结果是 Mix-A 特有的;Plan-B 在 v3 深度下全面通过
一次只测一个变量默认"新标定 + 相同压缩比"的对照隔离v3ratio 对照实验翻转了全部解释——永远要做变量隔离
把"容量受限的基准"当作普适结论在声明天花板之前用多个配方测量合适的配方下 BugFind 在不改变参数量时提升了 17 分

最痛苦的教训也最有迁移价值:验证循环能捕获管线 bug,但捕获不了策略 bug。SFT 管线运行正确——没有崩溃、没有 OOM、训练曲线健康——却产出了一个更差的模型。

我最遗憾没有做的一个实验是 50/50 标定混合。如果 30% 的通用数据把 MMLU 从 0% 拉到 33%,50% 可能会让它超过 40%。那个实验只需要 25 分钟。替换它的 SFT 花了 11.5 小时,还把一切都搞得更糟了。

边界条件

  • 标定组成的重要性发现是在 REAP 算法 + Qwen3.6-35B-A3B 模型上得出的。它很可能迁移到其他 MoE 模型和剪枝算法上,但我没有验证过。
  • REAP-26B 配方的发现(v7b-fp8 击败 v3)局限于这一特定的标定混合和压缩深度。是否能泛化到其他 MoE 规模是开放问题。
  • 第 6 轮实验中"标定内容驱动 toolcall 退化"的发现是 Mix-A 内容特有的(glm47-reap + hermes-agent-traces)。REAP-26B 配方在相同深度下展示了正向的 toolcall 变化。这个发现是配方特异的,不是普适的。
  • HermesAgent-20 在所有七轮实验和所有测试配置下都停在 16/20。在这个模型规模下,它确实是容量受限的。
  • 4-bit 专家解批量技术以推理速度为代价——每个专家独立的 Linear4bit 前向传播比原生分组矩阵乘法慢。
  • SFT 退化结果特指在此模型架构上使用 4-bit 冻结专家的训练。FP8 冻结专家或全 BF16 的 SFT 可能行为不同。
  • 单卡约束影响了所有结论。在多卡硬件上,权衡关系会显著变化。
  • v19 对话模板相比 v18 大约损失了 7 个 toolcall 分点(同一模型上 90 对 97)。第 7 轮实验的所有对比都在同一模板内,但直接与第 6 轮实验的数据做比较需要考虑模板变化。

开放问题

  • 在 v7b-fp8 上做 SFT 能否拉动 HermesAgent-20? 它是 v7b 唯一没有改善的评测包,停在 16/20。第 6 轮实验的"容量受限"结论对 BugFind 是错的(正确的配方在不改变参数量时把它拉动了 17 分)。对 HermesAgent 来说也可能如此——只是还没找到正确的配方。但在真实的 agent 轨迹上做 SFT 是一个性质不同的路径,相关基础设施已经存在但还没有在 v7b 上验证过。

  • 50/50 标定能否把 MMLU 推到 40% 以上? 这个实验只需要 25 分钟,从未被排进日程。

  • Transformer Engine FP8 训练能否实现有质量的 SFT 而又不落入噪声过拟合陷阱? 相关工具已经在 sm_120 上安装完毕。尚未测试。

  • REAP-26B 配方能否在其他的 MoE 模型族上复现? 它在 Qwen3.6 上驱动了多个基准 +17 的提升。在 DeepSeek、Mixtral 或 OLMoE 上是否类似?

  • 叠层剪枝到底该不该用? 第 7 轮实验表明它会破坏上游的标定信号。但如果上游标定本身很昂贵(24K 样本在 96 GB GPU 上),在其基础上叠一层廉价的重剪枝在理论上似乎应该可行。实证结果是负面的。我不太理解为什么。

[[Q]] 半年后的我:先跑 50/50 标定实验。如果它把 MMLU 推到 40% 以上,整个 SFT 的努力都是浪费。另外,把 v7b 的符号链接建好——这是你有过的最好的模型。

参考文献

  1. Fang et al., “REAP the Experts: Why Pruning Prevails for One-Shot MoE Compression”, arXiv:2510.13999, 2025.
  2. Dery et al., “Finding Fantastic Experts in MoE Models”, arXiv:2504.15447, 2025.
  3. Zhang et al., “Efficient Expert Pruning in MoE LLMs”, arXiv:2505.12345, 2025.
  4. BitsAndBytes, Hugging Face quantization library, https://github.com/bitsandbytes-foundation/bitsandbytes.
  5. TRL: Transformer Reinforcement Learning, Hugging Face, https://github.com/huggingface/trl.
expert-pruningREAPRTX5090Qwen标定数据4-bit-unbatchingagentic-benchmarksbenchlocal中文

Pruning Qwen3.6-35B-A3B for RTX 5090: what I learned pushing MoE compression to its limit on a single GPU

MoE Expert Pruning: What Works, What Doesn't, and What We Still Don't Know