ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qwen3-30B-A3B+DAPO+VeRL:NPU上强化学习微调实践

Qwen3-30B-A3B+DAPO+VeRL:NPU上强化学习微调实践

这一轮做强化学习微调,目标是把Qwen3-30B-A3B这个MoE模型的输出质量再往上拉一截。Qwen3-30B-A3B是典型的30B总参、3B激活参数的稀疏专家模型,直接上全参SFT成本划不来,而用普通PPO做强化学习,又容易在熵、KL、奖励噪声之间反复震荡。我最终选定的方案是:用VeRL当训练框架,用DAPO做策略优化算法,底层跑在NPU设备上。整个流程踩了不少坑,但最终还是把整条链路完整打通了。这篇记录专门写给准备在同一套组合上动手的工程师,环境准备、配置文件、训练命令、问题排查和调参经验我都放在后面,尽量做到可以直接参考。

1. 为什么是Qwen3-30B-A3B加DAPO加VeRL这组搭配

1.1 MoE模型在强化学习里的双面性

Qwen3-30B-A3B最吸引人的地方,是总参数30B但每个token只激活3B参数。这个特性在推理阶段非常香,部署成本和单卡推理延迟都比同尺寸稠密模型低一大截。但放到强化学习训练里,情况就没那么美好了。

强化学习训练包含两个阶段:rollout生成和策略更新。rollout阶段需要批量采样,这个阶段是纯推理的逻辑,MoE带来的推理加速能实打实体现出来,训练吞吐会比想象中好些。可一旦进入策略梯度更新阶段,需要计算所有被激活专家的梯度并做反向传播,这里就暴露了一个隐藏问题——MoE的路由是动态的,不同batch里每个token激活的专家组合不一样,导致形状不固定、内存碎片化加剧,在NPU上尤其明显。

还有一个容易被忽略的点:MoE模型在做RL训练时,专家路由策略本身也在被优化。你会发现训练前期路由分布还算均匀,训练后期容易“偏科”,几个专家被频繁激活,其他专家几乎闲置。这个问题不是DAPO直接解决的,但它带来的熵坍缩现象和DAPO要处理的熵崩溃问题有很强的关联,后面调参时也要一并考虑。

所以,选Qwen3-30B-A3B做RL训练,得先接受两个事实:推理快是真的快,训练时稀疏特性和动态shape带来的复杂度也是真的麻烦。理解了这两点,后面遇到问题就不会慌乱。

1.2 DAPO和传统PPO到底差在哪

DAPO的全称理解起来有点绕,我直接用大白话拆一遍。日常最常用的PPO在更新策略时会先计算advantage,再用它来约束新旧策略的比值,整个过程里策略网络和价值网络是耦在一起更新的。耦合带来的问题很现实:价值网络估计偏差大的时候,策略网络也会跟着遭殃,训练方差会显著变大。

DAPO的做法是把策略更新的逻辑和价值估计解耦,同时对动作空间里的概率分布做更精细的约束。实际体感上有三个变化最明显。

第一,KL失控的次数少了很多。传统PPO经常出现奖励被KL惩罚拖着走、模型还没学到东西先被罚得不敢动的情况。DAPO对KL的约束方式更温和,不直接用一个大系数压死策略,而是通过锚定机制控制更新幅度,训练曲线看起来平滑不少。第二,熵效应的处理更稳。MoE模型在RL后期非常容易提前收敛到确定性策略,DAPO在动作空间的解耦设计能稍微延缓这个趋势。第三,对奖励噪声的容忍度更好。数据标注质量和奖励模型都不完美时,DAPO的梯度估计会更稳健,这是我这次实践里体感最强的一点。

当然,DAPO不是万能药,它解决的是PPO框架下策略梯度的稳定性问题,但RL训练里大量的工程瓶颈仍然取决于框架调度和硬件适配。

1.3 VeRL在整个流程中扮演的角色

VeRL的价值一句话说:它把强化学习训练里最麻烦的调度部分接了过去。rollout生成、奖励计算、训练更新、checkpoint保存,这些环节如果全部自己写,至少要花掉一个月时间,且大概率跑起来之后还有各种并发问题。

VeRL的完整流水线逻辑大致是这样:先从训练数据池里抽prompt,交给推理后端批量生成回答,同时计算奖励,把带有奖励标签的样本组织成experience,再交给训练进程做策略更新。整套流程通过一个控制器自动编排,我只需要管理好它的配置文件。

VeRL还有一个我比较看重的特点,就是它对不同推理后端的适配能力。在NPU环境下,推理后端的选择本身就比GPU上少,VeRL能把这个环节抽象出来,降低了不少硬件绑定风险。这也是我最终决定用它而不是从零搭流程的原因。

2. NPU上的环境准备与验证,动手之前先做好四件事

2.1 确认硬件规格和拓扑

跑这个规模的任务,第一件事不是急着装软件,而是把NPU资源摸清楚。先看有多少张卡、单卡显存多大、卡间通信是否能正常初始化。可以用设备信息查看命令直接确认,比看机房登记表靠谱得多。

实践里我遇到过“登记8张卡,实际只有4张可用”的情况,原因是某张卡温度过高保护性降频,但表面的设备列表还是正常。所以建议看一眼温度和功耗数据,确认所有卡都在正常范围内。NPU环境里,单卡故障比GPU环境更容易表现为“训练直接中断”,提前排查比中途救火强。

还需要确认一下卡间的互联拓扑。不同NPU服务器的卡间网络拓扑有差异,如果恰好遇到通信效率较差的拓扑,分布式训练的同步开销会明显增大。一种快速验证方式是在所有卡上同时做一次全局同步,耗时大于预期的话,后续就要在通信调度上多花点心思。

2.2 依赖环境与框架版本的对齐

安装依赖时最忌讳的就是直接“latest”一把梭。NPU的软件栈更新节奏很快,对应的深度学习框架、并行训练库和训练框架之间,基本上是锁定版本配套的关系。

我这次的做法是先查一份官方已经验证过的版本组合,严格按组合安装。每个版本的框架都对应特定版本的算子库,一旦某个组件被升级,其他组件很可能因为算子兼容问题直接报错。这个错误往往不是“缺少依赖”这种明显的提示,而是跑起来之后掐在某个具体算子上崩溃。

建议在虚拟环境里安装,不要污染系统级Python。另外,确认好编译宏和AOE(自动优化引擎)相关的开关,这些工具会直接影响NPU上的算子生成效果,对MoE模型的动态路由场景影响尤其大。

2.3 模型权重的获取与检查

Qwen3-30B-A3B的权重文件比较大,从模型库下载时先检查文件完整性。遇到过下载到一半静默中断的情况,本地文件看起来存在,但加载到模型结构里某些层参数缺失,训练跑起来才报错。

下载完成后,先走一遍模型模板的加载流程,确认序列化格式和预设的数据类型一致。MoE模型在加载时有个特点:专家参数的量占大头,加载耗时比同体量稠密模型略长,这是正常的。但加载完成后,用一个小样例跑一次纯推理,确定输出结果符合预期再进入下一步。

这一步的目的很简单:确认模型本身是健康的。模型有问题和训练框架有问题,排错成本完全不是一个量级。

2.4 用小模型玩一次“泛冒烟测试”

正式训练前,我非常建议抽两小时做一个冒烟测试:换一个参数量小很多的同架构模型,把后续要用的训练流程完整跑一遍。不要觉得这个是浪费时间,它在后面的排错阶段能帮你省掉一大半精力。

冒烟测试可以验证三件事:训练框架是否能正常初始化分布式环境、推理后端是否能在NPU上正常出结果、整体流水线是否能在小规模数据上完整跑一轮。这三点任何一个有问题,在小模型上定位都比在30B模型上容易得多。等冒烟测试通过,再切换到Qwen3-30B-A3B,你心里就有底了:框架环境是干净可用的,剩下的问题只可能出在模型规模和参数设置上。

3. 训练配置与流水线搭建,从配置文件到启动命令

3.1 配置文件的核心字段拆解

VeRL习惯用YAML文件管理整套训练配置。我会把核心配置分成四层来看:模型层、算法层、运行时层、数据层。

模型层需要指定模型路径、是否加载预训练权重、并行切分的策略。Qwen3-30B-A3B这种30B体量的模型,模型并行是必须的,关键是选好切分维度。MoE模型在切分时要特别注意专家的放置策略。把不同专家分布到不同卡上可以降低显存峰值,但会增加通信量;把专家复制多份可以减少通信却吃显存。这个平衡没有绝对标准,取决于卡数和单卡显存容量。

算法层需要指定强化学习算法名、clip范围、学习率、KL系数、熵奖励系数等。DAPO在这里有几个独立参数需要单独设置,比如锚定阈值的上下界。初学时直接用默认值跑一遍,再根据训练曲线调整,效果通常比一次性迷信某个“最佳参数”要好。

运行时层涉及批大小、微批大小、梯度累积步数、训练总步数、日志保存频率。数据层则管理训练提示词文件路径、奖励规则或奖励模型配置。这四个层之间有一定耦合,比如调整批大小就必须同步关注显存占用和通信效率。

下面给一个经过精简的配置示例,只保留了可以直接套用的主干字段:

# config/dapo_qwen3_30b_a3b.yaml model: path: "Qwen3-30B-A3B" model_type: "moe" tensor_parallel: 8 expert_parallel: 4 algorithm: name: "dapo" lr: 1e-6 clip_ratio: 0.2 kl_coef: 0.02 entropy_coef: 0.005 dapo: anchor_threshold_low: 0.1 anchor_threshold_high: 0.5 runtime: global_batch_size: 64 micro_batch_size: 1 grad_accum_steps: 8 max_steps: 5000 save_interval: 200 log_interval: 10 data: prompt_file: "./data/train_prompts.jsonl" reward: type: "rule" rule_file: "./rewards/rule_v1.py"

这里要特别提一下micro_batch_size,在NPU上跑MoE模型时,这个值不宜设太大。微批大小过大容易造成中间激活的显存峰值陡增,尤其在动态形状的场景里。我自己的经验是将micro_batch_size控制在1到2之间,配合梯度累积来凑全局批大小,训练稳定性会好很多。

3.2 奖励设计:规则奖励和模型奖励怎么选

奖励是强化学习的灵魂,配置得不好,算法再好也白搭。这次任务我用的是规则奖励,因为指标非常明确,可以用脚本精确计算。规则奖励的好处是稳定、可解释性强、不受奖励模型本身噪声干扰。缺点是要把评估标准完全量化成代码,对于主观性强一点的问答场景会比较难写。

如果任务指标不那么硬,比如需要衡量“回答是否更有帮助”,就得引入模型奖励。模型奖励通常是一个单独训练过的reward model,它会给每条回答打分。用模型奖励的代价是奖励模型本身的误差会传递到策略更新中,造成训练方差上升,同时多一个模型在线推理也会增加资源开销。

我的建议是:能用规则的绝不要先上模型奖励。规则奖励可以让你独立地观察策略更新是否有效,省去奖励噪声这个变量。等规则奖励训练已经进入瓶颈,再尝试引入模型奖励进行微调。

3.3 启动训练命令与监控日志

环境就绪、配置文件写完,接下来就是启动训练。启动命令通常是一个shell脚本,里面封装了分布式拉起逻辑,核心是让每张NPU卡都拿到正确的rank和训练入口。

bash scripts/run_dapo.sh \ --config config/dapo_qwen3_30b_a3b.yaml \ --model-path Qwen3-30B-A3B \ --nnodes 1 \ --nproc-per-node 8

训练起来之后,日志会大量输出,最关键的是几个指标:kl散度、policy_loss、value_loss、奖励均值、熵值和梯度范数。我强烈建议把这些指标接入可视化面板,不要只看命令行。命令行日志刷得太快,趋势根本看不清楚,等反应过来可能已经训练崩了好几轮。

判断训练是否健康有几个比较简单有效的信号。奖励均值应该整体呈上升趋势,允许有震荡但中枢要抬升。熵值在训练初期不应该断崖下跌,否则后面极可能出现熵坍缩。梯度范数要控制在稳定范围内,忽大忽小且幅度悬殊,大概率是数据batch之间差异太大或者学习率偏高。

3.4 断点续训和checkpoint管理

VeRL支持定期保存checkpoint,这一步千万别偷懒关掉。RL训练的时间和成本都不低,一旦中途异常中断,没有最近一个checkpoint,就得从头再来。我这次跑的时长比较长,中途因为节点抖动断过一次,要不是保存间隔设置得密,损失会相当可观。

保存频率也要适度,保存太频繁会占用大量磁盘和时间,建议结合训练步数来定,200步到500步之间是比较合理的区间。恢复训练后,确认加载的是最近的checkpoint,并且所有分布式进程都从相同的index恢复,这个细节出错会导致后续训练数据分布错乱。

4. 实战中拦路最多的六个问题,全链路排障过程记录

4.1 MoE动态路由引发的显存峰值飙升

训练跑到大约几百步的时候,第一次显存溢出来得毫无预兆。拆开统计才发现,MoE的每个token在路由时可能选择不同的专家,当某个batch恰好大量选择同一组专家时,这组专家的中间激活会瞬间堆得很高。

排查思路分三步。第一步,把global_batch_size拆小,改成微批加梯度累积来降低瞬时激活。第二步,检查专家并行策略,让路由热点分散到更多设备上。第三步,开启更细粒度的显存管理策略,把不常使用的临时张量及时释放。

这个问题的核心是如何接受“MoE环境下的显存是波动而非恒定的”。要给显存留出一定冗余,不要把设备显存用到极限,否则一个极端batch就能让整个训练中断。

4.2 动态shape导致的算子拒绝执行

这只是和显存问题同步出现的:跑着跑着突然报一个算子不支持的错误,定位后发现是输入序列长度不一致导致的动态shape。rollout过程中,每条prompt生成的长度不可能完全一致,模型对不同长度做并行计算时,某些算子在NPU上的实现要求固定shape。

一种很实用的解法是做padding处理,把同一个batch内的序列补齐到相同长度。padding的代价是会浪费少量计算,但稳定性提升非常明显。另一种解法是在推理生成阶段控制输出长度上限,把动态范围压缩到一个较小的区间,减轻padding压力。如果框架支持bucket机制,能自动聚合相似长度的请求,那效果最好。

这个坑在MoE模型上特别频繁,因为专家路由本身就是动态的,再叠加序列长度动态,双重不确定性会让底层算子非常“暴躁”。尽早做padding策略,别等模型跑崩了再补。

4.3 rollout和训练更新之间的步调不同步

训练进行到中段,我发现奖励均值突然停增长,但rollout的生成质量明明在提升。排查后发现是推理生成和训练更新之间出现了严重的步调不同步。简单说就是:训练更新的策略已经比rollout生成用到的策略新了很多,整个训练里经验数据相对策略是“过时”的。

导致这个问题的直接原因,是我把推理后端的并发数调得太大,生成速度超过了训练更新的消耗速度。经验池里堆积了大量旧策略的数据,模型学到的始终是旧数据带来的反馈。

解决方式是控制rollout和训练更新的比例,让推理生成速率略低于训练消耗速率,保证策略更新能够及时作用到新一批rollout数据上。这个比例在不同模型上差异很大,Qwen3-30B-A3B这种推理速度快的MoE模型尤其需要细心调。

4.4 KL惩罚失控会带来什么连锁反应

有段时间训练曲线非常诡异:奖励在涨,但生成结果的文本质量却明显在下降。查看kl散度时发现它出现了急剧的峰值,说明策略更新幅度已经突破了合理范围。

传统PPO里,KL系数设太大会让模型不敢改变;设太小又会让策略彻底放飞。DAPO里的锚定机制本身对KL有抑制,但不能完全依靠它,还是要配合一个合理的kl_coef。我在实践里把kl_coef调到0.02附近,又设置了一个动态调整逻辑——当KL超过目标区间时,自动提高系数,把它压回去。

这个调法不能只在配置里写死,最好能写一个简单的观测循环,持续监控KL值并反馈调整。

4.5 多卡通信超时和死锁

第一次跑多卡分布式训练,就遇到了卡间通信超时。观感很直观:所有NPU的利用率整体偏低,个别卡甚至在某个点一直等待,等到超时后训练崩溃。

排查时先用最小化通信命令验证卡间连接,确认不是硬件链路的问题。然后一个配置一个配置地排查,最终定位在通信库的buffer尺寸设置上。分布式训练时,梯度同步和专家路由的all-to-all通信都会占用大量通信buffer,尺寸配置过小会频繁触发等待。

这类问题的排查,最有效的工具是看看通信耗时在整个训练step里占比有多大。如果超过30%,就要优先优化通信,而不是去调学习率。

4.6 训练后期的熵坍缩和路由偏科

训练接近尾声,熵值已经掉到很低,模型输出变得非常单一。这个现象在MoE模型上还有一层特殊表现:专家路由越来越集中,组成了一种隐藏的“捷径”,导致模型的泛化能力在悄悄变差。

熵坍缩是强化学习里出了名的难缠问题。DAPO对熵的约束有所改善,但我也额外做了一件事:给熵奖励设置一个下限保护,当熵值低于阈值时,给奖励函数增加补偿项。这个不起眼的机制能让模型在训练后期保留一定的探索能力,不至于过早锁死策略。

路由偏科的处理思路则是定期统计各专家的被激活频率,发现某一组专家占比过高时,适当调整路由分布。这个操作在MoE架构里有几种常见手段,核心是让路由保持健康的多样性。

5. 调参经验与训练提速的几条实际建议

5.1 一组可以少走弯路的超参起点

我把自己最终跑稳的一组关键超参数整理在下面,它不一定适合所有任务,但作为起点会比盲目尝试舒服得多。

超参数建议起点调整原则备注
learning_rate1e-6到5e-6观察梯度范数,范数过大就降MoE模型比稠密模型要保守
kl_coef0.02KL峰谷震荡就升,长期平缓就降结合锚定阈值一起调
clip_ratio0.2更新幅度过小可放宽到0.3不建议超过0.4
entropy_coef0.005熵下降过快要升高在训练后半段尤其重要
micro_batch_size1到2显存有余量可尝试增大注意动态shape风险
top_p0.8生成多样性不足时可降影响rollout的数据分布

学习率是这里最敏感的一个参数。MoE模型因为路由的加入,参数空间更大,学习率一旦偏高,策略波动非常剧烈。我先用1e-6跑稳,之后逐步缓慢上调,直到梯度范数开始出现不规律的跳动,再退回上一个安全区间。

5.2 并行策略的一点点实操理解

Qwen3-30B-A3B在NPU上的并行切分,我建议优先考虑模型并行加专家并行,数据并行可以相对弱化。因为MoE模型本身参数量较大,单卡放不下完整模型,数据并行在无冗余模型副本时意义有限。

专家并行是把不同专家切到不同卡上,关键在于通信优化。每次前向传播时,token要跨卡访问专家,这会产生大量all-to-all通信。如果卡间通信速度快,专家并行收益非常明显,整体吞吐会有显著提升。

实操中可以统计每个step里通信耗时占比来判断并行切分是否合理。如果通信占比太高,考虑增大并行组的大小,减少跨卡访问频率,虽然这会增加显存消耗,但整体吞吐往往反而更好。

5.3 训练加速的几件小事

想让整个训练快起来,我总结了几条不用动算法就能见效的做法。

第一,推理和训练阶段使用独立的工作进程,避免互相抢占NPU资源。第二,把数据预处理和在线采样拆开,prompt在训练开始前就完成tokenize。第三,动态padding要开启,而不是始终padding到最大长度。第四,检查训练框架的异步保存能力,让checkpoint写入不阻塞训练主流程。第五,定期清理log目录和临时文件,磁盘空间不足会引起存储底层报错。

这些优化单看每一条提升可能只有几个百分点,但叠加起来,训练总时长可以缩短三分之一左右。在这个体量下,这部分时间是非常可观的成本。

6. 复盘后我在实际操作里的三条切身感受

第一,MoE加NPU这个组合的问题,八成发生在工程层而非算法层。从头到尾最折磨人的是显存波动、动态shape、通信超时,这些问题需要的是工程手段,而不是研究思路。所以动手之前,把环境验证功课做扎实,比多读几篇算法文章更有用。

第二,DAPO确实比传统PPO稳,但不能因此就把调参的责任丢给算法。锚定阈值、KL系数、熵系数之间是强耦合关系,任何一个参数动一下,其他参数都要跟着观察。准备一个记录完整的调参实验表,比凭感觉改参数高效得多。

第三,每一个“异常参数”背后往往都有更底层的原因。比如熵值下降过快,根因可能是reward设计里探索激励不足;路由偏科,根因可能是专家初始化或数据分布不均衡。只盯着表面对症下药,问题多半会换个方式再次出现。

Qwen3-30B-A3B配合DAPO在NPU上做强化学习,这条路已经走通了。后续如果再让我搭一次类似环境,我会把冒烟测试周期拉得更长,把基础设施的每一项都验证到“枯燥”的程度再上大模型。回头来看,这次实践最大的收获不是跑通能用的训练曲线,而是对整个链路的理解变得清晰。希望这份记录能让你少踩几个我踩过的坑,把时间花在真正重要的调优工作上。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进