基于VeRL框架在Atlas 800T A2上打通DeepSeek-671B DAPO强化学习训练流程实践
作者:昇腾实战派
知识地图:https://blog.csdn.net/Lumos_Lovegood/article/details/161601003
背景概述
在大模型强化学习训练中,DeepSeek-671B模型因其参数量巨大、结构复杂(含MLA、MoE等),对推理和训练框架的适配提出了极高要求。本文基于VeRL开源强化学习仓库,在Atlas 800T A2集群上完成了DeepSeek-671B-R1-Zero的DAPO强化学习训练流程打通,解决了推理乱码、显存溢出、精度对齐等关键问题,最终实现了端到端性能达标。本文从适配方案、流程优化、问题定位与解决等角度进行技术总结,为后续DeepSeek系列模型的强化学习训练提供前置技术路线。
关键里程碑
在Atlas 800T A2集群上基于VeRL开源仓库打通Deepseek-671B-R1-Zero DAPO强化学习训练流程,完成关键指标reward与response length曲线对齐,最终Atlas 800T A2上端到端性能达到预期指标。
功能
适配方案选择
推理引擎:目前VeRL支持的推理引擎有vLLM和SGLang两类。其中vLLM的适配工作较多,当前昇腾有基于vLLM-Ascend在Qwen2.5、Qwen3等模型上完成强化学习相关适配工作的成果和经验,因此以vLLM/vLLM-Ascend为推理引擎做新模型的适配。
训练后端:由于DeepSeek-671B模型参数较为庞大,FSDP后端无法承载该规模尺寸的模型训练,因此首选Megatron进行训练后端的适配,且目前Megatron+MindSpeed已有成熟经验。
框架开发:当前VeRL社区代码未支持DeepSeek-671B模型训练,无基线数据。
打通流程
VeRL强化学习框架由推理和训练两部分组成,当前DAPO训练策略采用训推全共卡方案,即推理引擎和训练后端以分时复用方式共享显存。训练通常需要占用大量计算资源,而推理依赖于训练模型,但其decode阶段耗时较长,导致训练需要等待推理完成,存在大量闲置等待时间,整体适配效率较低。
在传统的功能打通流程中,推理和训练是串行进行的,通常需要先打通推理再打通训练。然而,在实际打通推理功能时,并不需要实际用到训练模型,因此无需占用大量计算资源。同时,由于推理和训练的交互本质上是推理结果的传递,这种串行流程导致了大量计算资源的浪费和调试闲置时间,进一步降低了开发和适配效率。
真实训练打通过程中,我们提出可以通过打桩生成推理结果,将推理和训练的功能适配工作解耦,并行化开展推理和训练的功能打通工作。这样,无需等待实际推理完成,即可同时进行训练的功能验证和调试,避免资源闲置和等待时间,从而显著降低物料浪费,提高适配效率,其流程如下图所示。
请添加图片描述
打通实践
针对671B大规模模型,在有限的适配时间下,我们采用推理、训练阶段并行,大模型减层从单机到多机再到满层拉起的策略,具体方案如下,并为大家介绍其中关键环节。

环境准备
基础环境:
| 配置项 | 版本信息 |
|---|---|
| AI服务器 | Atlas 800T A2 (64G) |
| 驱动、固件 | 24.1.0.3 |
| Python | 3.10.12 |
| CANN | 8.2.RC2 |
| torch | 2.7.1 |
| torch_npu | 2.7.1 |
| transformer | 4.53.3 |
| vllm | 0.10.0 |
| vllm-ascend | 0.10.0rc1 |
| verl | 0.7.0.dev0 |
| Megatron-core | 0.12.1 |
| MindSpeed | 2.2.0_core_r0.12.1 |
| Mbridge | 0.13.1 |
FP8转BF16权重:DeepSeek V3模型原生采用FP8混合精度训练,需转为BF16权重。原生fp8_cast_bf16文件基于Triton算子优化,使用可能会存在一些问题,现提供NPU已打通的fp8_cast_bf16.py文件:fp8_cast_bf16.py
基于VeRL场景下的K8s任务使能
该项目场景是基于K8s裸机生产集群拉起强化学习任务,此场景下无法直接获取全部节点IP,只能通过K8s调度工作,相较常规开发环境存在差异。现提供常规的执行脚本如下,任务拉起可通过K8s命令启动对应YAML文件。
三方组件Mbridge
Mbridge是一个提供HF权重与MCore权重在线自动转换的工具,支持在线的权重导入及导出,不需要依赖离线保存的MCore权重,并且支持TP/PP/CP/VPP/EP/ETP等并行策略。GPU侧基于Mbridge进行了训推权重转换,为了保证配置对齐,需进行Mbridge在NPU上的功能打通。
Mbridge与use_dist_ckpt加载权重的区别:
Mbridge加载与导出权重的核心代码都在mbridge/core/bridge.py中,不需要离线保存的MCore权重,直接通过bridge.py中的代码进行在线权重转换。use_dist_ckpt加载权重的代码内置在VeRL的Megatron Worker(verl/workers/megatron_workers.py)中,训练过程中按切分加载提前保存好的离线MCore权重。
适配方式
- Mbridge安装:略
- NPU适配:手动在Mbridge读取safetensors入口处修改device为NPU,具体代码路径为
mbridge/models/ext/deepseek_v3/dequant_fp8_safetensor_io.py,如下图所示:

- 使能Mbridge功能:VeRL中配置了Mbridge使能的参数,开启该配置并关闭dist加载权重配置即可开启Mbridge,具体配置如下:
推理EP使能
DeepSeek满血版模型因参数量级高带来推理阶段的资源开销过高,而现有资源有限,因此在VeRL强化学习场景启用推理阶段的专家并行(EP)机制。为使能推理EP,我们增加核心环境初始化逻辑,为每个分组配置独立的通信主节点,完成VLLM分布式通信环境变量的注入。
代码修改位置:verl/workers/rollout/vllm_rollout/vllm_rollout_spmd.py
关键代码如下:


Skip_rollout使能
Skip_rollout功能是指,将VeRL强化学习过程中的推理结果保存下来,在之后的所有训练流程中,都跳过推理部分,直接进入到训练。这在大GBS、长序列的场景中,往往可以将推理耗时2~12小时缩减到5分钟。
使能方式如下:
该功能的v1版本已经上仓VeRL,可通过添加对应参数使能。通过该功能,我们可以在指定路径上dump一次推理的数据,并在之后自动获取缓存信息,跳过推理阶段,直接进行训练阶段调试。
单推代码
单推功能是模拟VeRL场景下单独拉起vLLM离线纯模型推理的代码,贴合实际使用场景,包含Ray集群初始化、VLLM推理引擎构建、适配TP/EP切分策略,重写VeRL场景下的generate_sequences等方法,但仅完成推理阶段,并会打印部分推理信息,轻量测试强化学习推理阶段功能。
相关代码如链接所示:https://gitcode.com/Ascend/msit/pull/4766,脚本拉下来单独运行(需要VeRL作为环境依赖)。
使能方法:
单机减层测试结果(减层吐字乱码,仅做功能打通示例):

训练阶段MLA使能
MLA(Multi-head Latent Attention)是由DeepSeek团队提出的多头潜注意力优化机制。与传统KV Cache不同,MLA并不直接存储完整的Key和Value矩阵,而是通过一个压缩隐向量来表示Key和Value,借助低秩压缩技术降低KV Cache。在训练中,Query也会进行低秩压缩以降低激活值内存。
强化学习场景下,NPU基于MindSpeed后端使能MLA方法如下:
其余详细参数,可见特性说明文档:https://gitcode.com/Ascend/MindSpeed/blob/2.2.0_core_r0.12.1/docs/features/multi-head-latent-attention.md
断点续训
大模型断点续训是指在模型训练过程中(如因硬件故障、网络中断、资源调度或主动暂停等原因终止后),能够基于训练中断时保存的完整状态(包括模型权重、优化器参数、学习率调度器状态、数据加载进度、训练迭代次数等核心信息),直接恢复至中断节点继续训练的能力,无需从头重启整个训练流程,在问题定位和长稳训练中起着关键作用。VeRL框架权重保存及断点续训关键参数如下:
其中resume_mode参数包含三个可选值,其意义如下:
VeRL框架本身支持断点续训,但在实践中,我们发现基于特定场景下,断点续训涉及的权重保存和加载存在问题。
权重保存
当采用Mbridge进行训练,保存权重时,VeRL代码会保存HF Model,走入Mbridge的save_hf_weights()权重保存逻辑。
权重保存关键代码:mbridge/core/safetensor_io.py

filename_to_keys_map:作为model.safetensors.index.json文件的映射文件,以Moonlight-16B-A3B模型文件举例:

filename:safetensors文件名keys_for_file:包含所有相同safetensors文件的keys列表states:遍历生成器获取的权重keys和对应的tensor张量
判断逻辑:遍历生成器获取权重,当states字典存储足够多的权重,使得每个keys_for_file作为states.keys()的子集,即可保存对应safetensors文件。只要对应的模型权重不符合条件,存在缺失,权重就无法保存对应文件。
问题案例:
MoonLight-16B-A3B模型权重几乎与DeepSeek模型结构一致,在后期资源不足的情况下,我们将基于MoonLight-16B-A3B进行验证,断点续训也是如此。在使用Mbridge组件下,我们观测到对应HF Model并未保存。
问题根因:
遍历训后的模型权重,发现MoonLight-16B-A3B模型的model.safetensors.index.json文件存在self_attn.rotary_emb.inv_freq层,但该层并未参与训练,后续生成器获取的权重没有对应key-value值,所以导致每次判断逻辑都无法满足要求,致使权重保存失败。
通过如下修改后,即可完整保存权重:

注意:
(1)权重保存采用子集判断逻辑,所以一旦模型训后获取的权重存在些许偏差都可能导致模型权重保存失败,这里提供Mbridge权重保存经验,希望后续其他场景相关问题可以提供相应思路。
(2)DeepSeek模型本身不存在self_attn.rotary_emb.inv_freq层,所以通过Mbridge权重保存不会遇到这个问题。
权重加载
问题现象:基于Mbridge权重加载方式下,同步使能optimizer_cpu_offload显存优化特性,在断点续训阶段出现报错:KeyError: "master_param"。

问题根因:VeRL在使用Mbridge且进行续训的时候,在分布式场景下加载Checkpoint时,优化器的shared_state_dict方法未正确区分对应场景,导致优化器分片状态解析错误,Checkpoint反序列化失败。
解决方案:
(1)社区修复方案:https://github.com/volcengine/verl/pull/3850
(2)社区修复方案涉及import transformer_engine,当前版本针对import transformer_engine修复。
显存问题
时间线
我们有两个用例,用例1: GBS32,用例2: GBS128。
前期:在用例1上OOM频发,初始怀疑存在显存释放不干净的情况,于是进行显存建模估算以及采集Profiling进行确认,首先明确了引发OOM问题的不是因为VeRL框架在NPU上存在显存释放不干净,而是推理长度过长导致的。此时查看rollout结果与response length mean指标,发现推理长度基本拉满到了12k,基本能判断OOM问题是因为推理乱码长序列引发的。
中期:确认OOM是一个和精度的耦合问题,当推理长度很长时,OOM不可避免,可以明确推理长度很长也确实是模型训崩导致的。
后期:解决了模型训练的精度问题,之前的过长response导致OOM问题不再出现;用例2出现大GBS导致的推理OOM问题,通过前期的经验,快速找到了显存的优化策略。
怀疑OOM是由VeRL框架在NPU上存在显存释放不干净引发的,需要两步走,一是进行显存建模,二是采集显存Profiling加以确认框架侧是否存在显存卸载不干净的情况。
显存建模
模型参数:

训练切分策略为TP8 PP16 EP16 ETP1,first_pp=3 layers, last_pp=2 layers, middle_pp=4 layers。
以下得到总的未经过PP切分的静态显存:

接下来是每个PP内的静态显存:[7, 8 x 6, 6],首尾两个PP注意还有word_embedding与output_layer。

推理切分策略为TP8 EP64。Dense部分与训练一致,但是在MoE层上不再被ETP切分而是被TP切分,则静态显存为:

以上讨论了静态显存逻辑,在update阶段还有激活显存:




总激活(也相当于第一个PP stage micro batch激活):Dense * 3 + MoE * 58 + loss = 0.15435791 * 3 + 0.578186035 * 58 + 0.246582031 = 34.2444458(GB)
训练开启全重计算:仅保留embedding后的激活 [s/TP, b, h] 为0.006835938 GB与loss部分的激活0.246582031 GB,加和为0.253417969 GB,即只考虑0.2534GB的激活。
算上首PP静态显存,其总的显存占用为33GB左右。
VeRL显存Profiling采集
首先明确HybridEngine下卡上显存的占用逻辑:

采集显存Profiling进行分析,拿到总体显存变化趋势,符合预期占用逻辑。
第一个step拿出各阶段的显存情况:

结合Timeline侧打出的显存情况:

vLLM推理引擎侧分析sleep模式是否生效
此处能观察到显存有一段回落,结合Timeline分析,此处是完成了vLLM推理:

结合代码调用情况,vLLM在结束推理后,会调用release操作即进入sleep模式,此时会根据sleep level进行param与kv cache的释放,主要的机制就是unmap_and_release中的aclrtFreePhysical:

该操作会对指定的tag进行处理,比如"weights",然后会offload到CPU侧。看具体代码执行,这部分在vLLM-Ascend中的CaMemAllocator中进行管理。
结合显存曲线变化及时间点切入,在app reserved即进程保留一线上符合预期:

做完release操作后,为将分配allocated较reserved超出的部分回收,一般都会进行empty_cache的操作:

但是,此处发现了一个问题,app reserved曲线一般都会较op侧allocated与reserved的高,此处看到两条op相关的曲线在做完generate_sequence操作后与app reserved曲线发生偏离并较之高,基本认定是vLLM自行管理的aclrtFreePhysical在torch侧是感知不到的。
Profiling显存进一步分析
拆解npu_module_mem.csv文件,大致主要分为APP、HCCL、Runtime三个部分,其中HCCL占用了6-8GB,这个也暂时先进行记录,看后续能否通过HCCL_BUFFSIZE设置再进行一些控制。

通过挖取npu_module_mem.csv文件中显存占比大头,主要发现两部分:APP与HCCL占用,单拎出来进行分析:

如前文所述,APP侧进程主要包括sharding+rollout、old_logp、ref_logp、update四个阶段,在状态切换的时候明显可以看到显存基本释放为0了。而在Profiling曲线中呈现的不归零的情况,主要是由HCCL侧导致的显存大小(HCCL侧曲线基本在6-8GB),两部分一叠加就形成了Profiling APP reserved曲线中的不归零的情况。

上述过程从理论建模到实际采集显存Profiling,从推理引擎分析到整体曲线说明,基本能得出在2k->2k的短序列场景下,模型不会出现OOM的情况。但是注意到,在VeRL中最后计算logits时,会存在[b,s,v]的shape,我们可以简单计算下在2k推12k且因为精度乱码的情况下,在rollout后的训练阶段会有多大的显存压力:
RL训练开了dynamic batch_size,那么batch会被算法装箱,从而micro_batch的大小会做调整,默认值设置的是2,通过装箱操作后,其大小会改变,此处以5做举例:[5, 1024 x 14, 1024 x 128],1024 x 14是2k推12k全部推满的序列的长度,1024 x 128是模型词表的长度,logits按照4byte来计算,显存大小为:4 x 5 x 1024 x 14 x 1024 x 128 / 1024^3 = 35GB,长序列场景下基本就是显存爆炸的状态。
actor阶段需要的显存约为:35G + 8G(HCCL)+ 37G(模型权重)= 80G。
所以本质上要解决2k推12k的显存问题还是得从解决精度问题的角度出发,先保证模型不会每次都推满到12k,至于在模型正常推满到12k还出现OOM问题时,此处可通过开启CP进行缓解。
分析完显存问题后,接下来本文将重点分析VeRL 671B精度问题,探究reward曲线怎么一步步通过修复问题与GPU基线对齐。
大batch下Prefill阶段在推理的MLA处OOM问题
GBS128 + dynamic bsz + PP16 + 关闭overlong + 2e-6 lr
做该组配置实验时候遇到的问题:


Prefill阶段在MLA处发生了OOM的情况。
分析:加大GBS后,原本[b,1,1,s]的attn_mask显存申请加大,当与attn_weights进行加和时shape也会进行相应的shape广播,从而在此处会形成显存OOM的情况。

解决方式:问题出在MLA prefill阶段,走的是vanilla分支,实现为小算子实现;通过开启chunk_prefill_for_mla走到融合算子中可避免此情况的发生。
精度问题
问题梳理
精度问题表现为功能打通后启动的DeepSeek 671B-Base模型DAPO训练,其reward曲线未能与GPU对齐。

后续通过对齐代码版本、对齐拉起脚本、升级VLLM版本以及PTA版本、适配差异化功能等手段,最终基于两份setting对齐了reward曲线。同时进行了消融实验,定位到三个主要影响到精度的差异点。精度问题的闭环历时一个月,其时间线如下:

精度问题主要有2个阶段的表现:
- 整网功能验证阶段(无标杆)的推理精度问题,使用Instruct模型,当推理load_format=dummy时,rollout输出为乱码,可以通过load_format=safetensors规避。
- 整网长跑比对阶段(有GPU基线)的训练精度问题,使用Base模型,rollout结果和GPU近似,但长跑reward曲线上升速度低于GPU。
精度问题1:load_format = dummy 推理乱码问题分析
在VeRL强化学习场景下,由于推理的权重来自于训练actor,因此理应无需关心初始化推理时模型的加载方式。但实验发现,如果vLLM的load_format为dummy(即随机初始化),则NPU侧的推理始终为乱码,但GPU能够正常推理。

VeRL首次推理前的权重情况图解
如图所示,VeRL先做训练模型的初始化,然后做推理模型的初始化。在训推模型初始化后,会自动抛弃掉vLLM的权重(sleep_mode2),其后VeRL会将训练模型的权重sharding到推理模型中,即推理模型的权重始终会被训练部分的权重覆盖。无论load_format是dummy还是safetensors,其推理都不应当是乱码。
在功能打通阶段时,该问题通过加载safetensors暂时规避,由于后续训练出现精度问题,该处作为疑点之一需要深入正向定位。
定位思路
由于社区上没有已知的相关Issue,且GPU功能正常,因此主要差异点应当在NPU和GPU的实现上,由于当下没有足够多的GPU资源拉起DeepSeek-671B,因此需要基于DeepSeek和其他模型的差异点,找到可以小规模问题复现的场景。
在可以复现的场景上,首先对比load_format为safetensors和dummy两者sharding后的权重内容,如果权重一致则对比两者的推理前向是否一致,找到差异点后追溯根因。
模型结构解析
DeepSeek在推理方面与其他模型的区别在于:
①总共61层的Decoder,前3层为Dense结构,后58层为MoE结构;
②Decoder的attn为MLA(Multi-Head Latent Attention)。
以小见大
由于VeRL更新较快,需要在其他模型上验证当前VeRL的版本能力。
1)在成熟模型Qwen3上尝试复现
- 尝试在Qwen3-30B(MoE)上进行复现,load_format为dummy时推理正常。
- 尝试在Qwen3-32B(Dense)上进行复现,load_format为dummy时推理正常。
Qwen3的两个成熟用例可以正常运行,这说明当前的VeRL代码的load_format=dummy能力是具备的。
2)在类DeepSeek的Moonlight-16B模型上复现
Moonshot-Moonlight 16B-A3B模型是一个类DeepSeek的模型,和DeepSeek一样存在Dense、MoE混合部分,是一个非常适合用以验证的模型。
| 参数名称 | DeepSeekV2 | Moonlight |
|---|---|---|
| architectures | DeepseekV3ForCausalLM | DeepseekV3ForCausalLM |
| 参数量 | 671B | 16B |
| 层级 | 61层 | 27层 |
| 旋转编码 | yarn(DeepseekScalingRotaryEmbedding) | base |
| 混合MoE/Dense | 前3层 | 前1层 |
| 注意力类型 | MLA | MLA |
由于当前的vllm/vllm-ascend暂未有适配moonlight的经验,因此需要在短时间内快速穿刺打通该模型。
在该打通过程中又遇到了易用性问题:Moonlight包含有MLA模块,这一模块实际上调用到的是被vllm-ascend的MLA(npu)。MLA(npu)在代码实现上默认了rope的类型是DeepseekScalingRotaryEmbedding,但由于moonlight的rope是原始的旋转编码,因此出现了相关报错。

如图所示为临时修复方案,当rope方式不为DeepseekScalingRotaryEmbedding,则按照原始的rope方式获取对应的cos_cache和sin_cache。
实验证明,该模型复现了DeepSeek相同的现象,即推理load_format=dummy时会存在乱码。
3) 在Moonlight模型上对比权重
首先判断权重是否正常sharding,通过在对应位置添加代码进行落盘对比。
具体落盘位置:
采用的落盘方法
通过落盘后对比权重,统计如下表所示的结果。两边的所有权重都相同标记为✅,否则标记为❌。
| dummy | safetensors | ||
|---|---|---|---|
| 是否一致 | init | ①sharding before | |
| dummy | init | ||
| ①sharding before | ✅ | ||
| ②sharding after | ❌ | ❌ | |
| safetensors | init | ❌ | ❌ |
| ①sharding before | ❌ | ❌ | ✅ |
| ②sharding after | ❌ | ❌ | ✅ |
权重的落盘对比结果符合预期,对于dummy和safetensors,模型初始化(init)和模型同步前(sharding before)的内容都相同;dummy格式的init权重和safetensors的init权重不一致;dummy格式的sharding after权重和safetensors格式的sharding after权重相同。
确认完权重部分正常,需要进一步做前向推理比对来继续定位。
4)在Moonlight模型上对比推理前向
通过对mooonlight推理中所有的forward部分进行打桩,记录所有的输入输出,并在safetensors和dummy进行对比。
对比结果:

mla-attn模块 layer 0层 输入一致,输出不一致。进而导致后续的所有层都对不齐,因此定位到mla-attn层的差异。
进一步定位发现,出现前向不一致的位置在于vllm-ascend中MLA模块的_v_up_proj方法。debug到对应的位置进行逐行对比,可以看到两边的self.W_UV和self.W_UK_T无法对齐。

根因定位
1)找到W_UV、W_UK_T的赋值位置
既然这两个矩阵在NPU上的值和GPU对不上,那顺藤摸瓜得看看这两个变量是怎么赋值的。
具体位置在:vllm_ascend\attention\mla_v1.py: 650

在这里打了断点,确定整网导致在什么时候调用到了这里:

首次初始化模型的时候,调用过process_weights_after_loading()函数
模型在verl中初始化 对vllm的 LLM实例化的时候,首次进入 process_weights_after_loading (之后不再进入),MLA 层会初始化这两个变量用于前向计算(首次被赋值)。如果load_format=dummy,GPU和NPU这里都是随机权重,但之后再也没有调用过这里,为什么GPU的推理时正常的,但NPU的推理是乱码?
已经找到了W_UV和 W_UK_T的赋值位置,接下来在前向计算的时候继续定位GPU和NPU的差异。
2)vLLM MLA-attn 权重更新差异分析
再次通过落盘对比,在下图做前向的位置打断点:

GPU:第 1 步的 kv_b_proj和第2步的 kv_b_proj 是同一片地址空间且落盘对比一致;第1步的W_UV和 W_UK_T与第2步的W_UV和 W_UK_T是同一片地址空间,落盘数值存在差异。
NPU:第 1 步的 kv_b_proj和第2步的 kv_b_proj 是同一片地址空间且落盘对比一致;第1步的W_UV和 W_UK_T与第2步的W_UV和 W_UK_T是同一片空间,但落盘数值相同,没有被更新。
这说明GPU的推理引擎初始化之后,走前向之前,一定有什么操作导致了GPU的 W_UK_T、W_UV更新;而NPU没有更新的现象与GPU不一致,显然存在精度问题。
分析:

在 NPU 上进行 veRL 训练时候所有推理引擎内的 MLA 层的W_UV和W_UK_T都没更新(相当于冻结了),一直都是初始状态的权重。
GPU 和 NPU 的 veRL 代码是同一套,断点调试发现在 veRL 训练中只有在 vLLM 初始化 LLM 模型的时候才调用 1 次process_weights_after_loading(),后续训练过程中推理引擎进行 update_weights 时不再调用该方法,但是 GPU 的 W_UV和W_UK_T都是会自动更新的,只有 NPU 并没有更新该变量。
3)根因1:NPU contiguous 操作打断数据地址
通过在process_weights_after_loading()函数中加入下面的测试代码,怀疑 NPU 代码逻辑中的.contiguous()操作打断了 kv_b 矩阵的张量视图,导致W_UV和W_UK_T无法自动更新。
这里通过手动原地赋值的方式具体的展示了地址不一致导致内容更新不同步的现象。
测试代码 1,检查contiguous逻辑
AssertionError: 0.0008 != -100
测试代码 2,检查不带contiguous逻辑
校验通过
4)根因2:GPU 与 NPU view 行为不一致导致数据地址打断
当按照前文消除掉 contiguous 的影响后,在整网下发现 W_UK_T, W_UV 矩阵的权重还是没有更新,说明在局部问题以外还叠加了其他问题的影响,继续在 process_weights_after_loading 中进行断点调试。
从头重新理清 kv_b 矩阵产生的逻辑,发现在进行完
这部分操作拿到初始的 kv_b_proj_weight 后,观察其 data_ptr(),往下执行 view 操作,发现地址空间发生了改变,且经过了 view 操作后原始不连续的 tensor 也变得连续了

发现 GPU 上的 view 操作是不会产生新地址的 tensor 的,那么问题就全部说明清楚了:

同时为了严谨,再补上了一组不用 view 而是用 reshape 的实验,发现GPU能够保证是同样一片地址空间。

Dummy 权重初始时候是随机初始化权重,初始根据模型结构获取到的 self.kv_b_proj 的权重在赋值给 kv_b_proj_weight 是正确的,但后续经过了 view 操作再 split 生成前向计算所需要的 W_UV、W_UK_T 时拿到的权重与原始权重的数据地址已经不一致了。这部分导致在 vLLM-ascend 在 dummy 下精度出现错误,并且在后续所有的训推转换流程中都会带来影响。

如图所示,左侧的GPU实现,所有的张量从头到尾地址都是一致的;右边的NPU实现,在view位置和contiguous位置分别发生了两次地址变更。
修复方案
- 方案一(采用):在加载完权重后直接调用
process_weights_after_loading,确保权重始终更新,适用于veRL中update_weights后的行为。 - 方案二:修复
view操作,使其行为与GPU一致。
当前采用方案一修复,已合入VeRL主仓(PR #4130)。
精度问题 2 mindspeed的mla部分实现问题
由于打通了Moonlight小模型,我们有了充足的资源做其他验证。因此在定位推理问题的同时,还在开展训练侧的精度对比。
定位思路
训练侧的定位思路就是将强化学习的训推解耦,简化为预训练/微调。
首先针对第一步进行校验,对比前反向;如果第一步没问题,需要针对训推一致性继续深挖根因。
本节问题定位过程中不涉及到推理的乱码问题,不与【精度问题1】耦合。
根因定位
打桩固定rollout对比第一步前向计算loss
通过打桩比对GPU和NPU前向loss计算过程时,很快发现了npu和gpu存在差异,差异位置是softmax_scale。

定位代码差异
发现mindspeed的mla模块走到FA的forward,但在计算scale时由于mindspeed的mla的传参差异导致npu和gpu代码实现上存在差异。


修复方案
mindspeed 的实现方式是照搬了MHA、GQA实现的分母系数,但是MLA上需要做单独的处理因为 q 的头的宽度是不一样的;
如果按照原有的流程处理scale,得到的hidden_size_per_attention_head是128,而DeepSeek里头q_head_dim是192;
修复方案:https://gitcode.com/Ascend/MindSpeed/pull/2977

修复后对齐NPU和GPU的前向(下半部分):

整网验证
以小见大阶段性收尾后,在整网上进行验证。
验证1: MLA问题修复
实验目的:修复2个Bug后,验证精度是否有提升;
实验方法:只修改Bug,其他实验环境,软件栈版本、脚本、模型等保持相同情况下对比修改Bug后的实验结果
实验结果:256卡cp4+ep128 + 确定性计算 vs 256卡cp4+ep64+确定性计算 + Mbridge + MLA + use_cpu

实验结论:修复发现的2个Bug以后,671B Reward 10步可以从-0.9上升到-0.8,GPU是上升到-0.6,仍存在其它问题。
验证2:关闭临时开发的CP功能并启用Mbridge
最开始和GPU对比精度的时候没有开启CP功能,但由于未来有长序列训练需求,有临时的MLA的CP实现,同时因为本身精度存在问题,推理长度不断上涨,需要cp才能避免OOM,既往的实验中开cp和不开cp存在混杂。
由于这部分特性是训练特性,没有充分验证,因此在整网验证阶段关闭了精度可能存在问题的训练mla的CP功能。
mbridge是权重转换的另一种方式,由于GPU一致开启mbridge进行长跑,因此完成适配后统一开启,和GPU保持一致。

蓝色曲线:NPU,开mbridge、带上两个修复、开CP
红色曲线:NPU,开mbridge、带上两个修复、关CP,趋势接近GPU(专家认可)
绿色曲线:GPU,开mbridge、关CP
整网验证通过
最终通过以下手段对齐了GPU的两组配置
(第一组gbs32/mbs32,第二组gbs128/mbs32)
- 修复MLA的scale问题
- 修复loadformat=dummy推理精度问题
- 使能mbridge
- 关闭未验证的MLA-CP功能
第一组gbs32/mbs32:开关确定性都能较好的对齐曲线

第二组gbs128/mbs32:
红色为NPU,蓝色为GPU

消融mbridge
为了实验的严谨性,需要区分mbridge和dist权重加载方式的差异,对此开展了消融实验。由于GPU只提供了使用mbridge的长跑曲线,此处没有gpu加载dist权重的基线。

GBS128,2k-8k用例
可以看到使用dist的reward曲线上升速度慢于使用mbridge。同时verl社区后续将放弃dist权重加载方式转而使用megatron版的bridge权重加载方式,我们暂时没有可用的资源继续定位两者的差异。
精度问题解决思路和建议总结
精度第一板斧:超参对齐:数据集和模型结构不同,则超参很可能不同,因此凭借经验或论文(每个论文的前提条件都不同,不能一概而论)给出的超参都不一定解决问题,因此与GPU标杆对齐超参是排除超参问题的关键,作为调整超参后训练效果的基线;
精度第二板斧:数据集排查:如果数据无法对齐,则精度和模型效果无法保证对齐,因此通过比对喂入模型的数据(比如前10步,中10步,后10步 )来确认数据是否对齐,目的是排除硬盘数据,以及从硬盘到模型之间的数据处理的差异;
精度第三板斧:硬件压测:排除精度异常硬件,单机单卡任务定期(1个月)压测找到与其他大部分卡或机器精度不一致的机器隔离即可。
精度第四板斧:软件栈排查:
方法论1:精度对齐的基本思想是:相同输入+相同计算 = 相同输出;NPU和GPU是等价计算的两种软硬件实现,因此在相同输入和等价计算前提下,输出误差应该在合理范围内才对(目前2000步 loss平均相对误差≤1%),误差大的部分则通过工具或二分查找方式具体定位。
方法论2:理论上除了硬件,以及与硬件强绑定的算子,PTA(PytorchAdapter)外,其他软件栈,比如分布式训练框架等,都是可以通过迁移达到NPU与GPU完全一致的,因此,可以在其他软件栈全部对齐下,重点排查硬件和与硬件绑定的算子/PTA部分的精度误差;
方法论3:任何精度误差排查,都应该在确定性计算下进行(相同权重,相同数据集,相同超参和环境配置,开启分布式训练框架的确定性(随机种子等),PyTorch的确定性,HCCL的确定性),加载GPU的初始化权重,避免随机性和配置不同导致的干扰;
方法论4:计算算子精度误差的排查方法:可以将模型减层改为单机单卡任务,覆盖所有计算算子,利用精度工具,NPU/GPU训练对齐下从Logits(代表前向计算结果)和GradNorm(代表前向加反向计算结果)误差开始排查,二分方式找出误差最大的算子,当前算子精度随算子变化;
方法论5:通信算子(包括计算通信掩盖算子)精度误差的排查方法:通过单机梯度累积方式将单机任务与多机任务超参对齐(除梯度累积外,其他超参如GlobalBatchSize等都对齐),对比单机与多机任务的Loss,如果数据误差在1%以上,则说明通信算子有问题,再针对性Dump数据比对查找具体算子;
精度第五板斧:排查模型结构,权重转换等是否对齐:
GPU的模型结构和NPU的模型结构往往由于不同实现,其是否完全等价计算,需要验证排查;
权重转换是否完全等价,也需要排查
精度第六板斧:排查评测打分,few-shot prompt和后处理流程:
评分过程通常会对推理后处理部分进行修改,需注意NPU和GPU打分程序保持一致。
性能优化
通过在VeRL上适配vllm_ascend图模式功能优化推理阶段的性能达成端到端的性能目标:
图模式功能适配
在verl/workers/rollout/vllm_rollout/vllm_rollout_spmd.py 添加适配代码:

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)