华为云AI开发平台ModelArts精度对齐_云淘科技

精度问题是指模型从GPU设备迁移到昇腾NPU设备之后由于软硬件差异引入的精度问题。根据是否在单卡环境下,可分为单卡精度问题与多卡精度问题。多卡相对于单卡,会有卡与卡之间的通信,这可能也是精度偏差的一种来源。所以多卡的精度对齐问题相对于单卡会更复杂。不过针对多卡的精度问题,可以分步骤先保证单卡对齐精度,然后分析通信过程的偏差。本文针对单卡的情形给出基于ptdbg-ascend精度对比工具的精度排查过程。

loss曲线对比

训练结束后,在 output_dir 参数指定目录下会输出 trainer_state.json 文件,该文件保存了训练过程 loss 以及 learning_rate 的 log 信息。

将GPU设备训练输出的 trainer_state.json 文件重命名为trainer_state_gpu.json,并拷贝到NPU节点的容器内;将NPU设备训练输出的 trainer_state.json 文件重命名为trainer_state_npu.json;

对其进行解析就可以获取 loss 信息,这里可以使用如下脚本进行 loss 曲线的绘制。

# compare_metric.py
import json
import os
from typing import List, Dict
 
import matplotlib.pyplot as plt
import numpy as np
 
## 解析 json 文件
def load_trainer_status(file_path):
    with open(file_path, "r") as f:
        trainer_status = json.load(f)
    return trainer_status.get("log_history")
 
def plot_curve(data_source: List[Dict], tags: List[str]):
    fig, ax = plt.subplots()
    for tag in tags:
        # print(data_source[0], len(data_source[0]))
        # assert all([tag in status.keys() for status in data_source]), f"Tag {tag} is missing for data source."
        for index, source in enumerate(data_source):
            y = []
            x = []
            for log in source:
                x.append(log.get("step"))
                y.append(log.get(tag))
            ax.plot(x, y, label=f"{tag}_{index}")
 
    ax.legend()
    plt.savefig("loss.png")
 
if __name__ == "__main__":
    state_npu_path = os.path.join("trainer_state_npu.json")
    state_gpu_path = os.path.join("trainer_state_gpu.json")
    state_npu = load_trainer_status(state_npu_path)
    state_gpu = load_trainer_status(state_gpu_path)
    plot_curve([state_npu, state_gpu], ["loss"])

对比单卡模式下NPU和GPU训练曲线,发现loss曲线下降趋势不一致。这说明迁移的模型存在精度偏差。

图1 loss曲线对比

图中蓝色loss_0是NPU迭代曲线,黄色loss_1是GPU的迭代曲线。

问题定位解决

使用ptdbg_ascend工具dump全网数据,dump接口设置方法具体参考PyTorch精度工具。dump完成后compare GPU和NPU结果进行分析。

dropout算子引入了随机性偏差,如下图:

图2 随机性偏差

根据堆栈信息定位得知dropout是使用的torch.nn.Dropout(),为消除随机性需要将随机因子p改为0或者1,此处是将model_chatglm.py中随机因子改为了0,如下修改:

图3 随机因子改为0

使用ptdbg修改register_hook方式做精度溢出检查。结果显示Tensor___add___233_forward执行时有溢出,这里使用浮点数精度的是 float16,结果显示输入的最大、最小、平均值都为65504(float16的精度范围是-65504 至 65504),如下图所示:

图4 精度溢出检查

因为在NPU下对INF和NAN的支持默认是饱和模式,会将INF置为MAX,NAN置为0,此处Tensor___add___233_forward的输入输出都是fp16的,会将Inf置为65504。 但是在GPU下采用的是INF_NAN模式(保留INF及NAN的结果),所以在做精度对比时先修改 NPU支持模式为INF_NAN模式与GPU保持一致,请参考INF_NAN_MODE_ENABLE。

开启INF_NAN模式方式命令如下:

#shell
export INF_NAN_MODE_ENABLE=1

修改之后再次做溢出检查显示所有API正常,无溢出情况。

GPU dump数据缺失,从Tensor_transpose_2_forward_output之后没有与NPU对应的bench data数据。

图5 GPU dump数据

在pkl文件中找到对应缺失的位置,发现Tensor_transpose_2_forward_output之后,NPU下一个执行的算子是Tensor_squeeze_0_forward_input,而GPU下一个执行的算子是Tensor___getitem___6_forward_input。

图6 api_stack_dump.pkl

根据stack信息查找到对应源码的代码行,发现对应函数上添加了@torch.jit.script装饰器,经过调试发现,GPU也执行了这个函数,但是没有dump算子执行信息,而且pdb无法在函数中正常中断,删除此装饰器后,GPU能够正常dump数据。

图7 删除@torch.jit.script装饰器

加了@torch.jit.script装饰器,torch_npu能采到数据,而GPU上则不行的原因为:@torch.jit.script装饰器会将装饰函数作为ScriptFunction对象返回,不会产生dump数据。而目前该装饰器在torch_npu下不生效,NPU会按照普通函数执行,因此能够采集到数据。从精度对比角度考虑,先删除@torch.jit.script可以保证这块GPU和NPU dump的数据对齐。

compare表中Cosine列第一个出现偏差的位置,为einsum算子的输入。

图8 Cosine列的偏差

查看堆栈信息发现是self.inv_freq的值存在精度偏差,再追溯到self.inv_freq的定义片段。

图9 inv_freq的定义片段

通过构造该计算公式,发现在x86上:torch+CPU和torch+GPU以及aarch64 torch+NPU场景的结果都是一致的,而aarch64 torch+CPU结果不同,如下:

图10 torch+CPU

图11 torch+GPU

图12 aarch64 torch+NPU

图13 aarch64 torch+CPU

而inv_freq恰好都是在CPU上初始化的。修改NPU版代码,强制使用torch+NPU进行初始化后,可以消除einsum算子输入偏差的问题。修改如下:

inv_freq = 1. / (base ** (torch.arange(0, dim, 2).float().npu() / dim))

另外的一种修改方式是转换到dobule下进行计算。

图14 转换到dobule下进行计算

修复上述问题后,Cosine值第一次出现偏差的位置为permute算子,在backward阶段作为input引入。

图15 permute算子偏差

由于在backward阶段ptdbg-ascend没有输出执行的堆栈信息,先查找了Tensor_permute_0在forward阶段相应的堆栈信息。

图16 Tensor_permute_0在forward阶段相应的堆栈信息

可以得知此处进行了换轴操作,但是在 forward 时输入输出均无精度异常。

因此转换排查思路,全局查找Cosine、MaxAbsErr值和Tensor_permute_0_backward相同的行。发现在Tensor___getitem___490_backward_output.0处MaxAbsErr 的值和Tensor_permute_0_backward一样

图17 Tensor___getitem___490_backward_output.0

并且Bench data列的max、min、mean对应值也一致,但是Tensor___getitem___490_backward_output.0 在NPU下的 max、min、mean值都是0,代表该处是全零的向量。猜想应该是梯度计算错误。使用PyTorch的index_select函数作为getitem函数的替代,对modeling_chatglm.py做如下修改:

图18 modeling_chatglm.py修改

再次dump对比精度,发现该算子精度问题得到解决。

图19 Tensor_permute_0精度对比

图20 算子精度对比

修改上述问题之后,重新对比精度数据后发现,重新进行训练任务,通过对比NPU和GPU的loss曲线,可以发现,两者的下降趋势几乎是一致的。

图21 loss曲线

图中蓝色loss_0是NPU的loss曲线,黄色loss_1是GPU的loss曲线。

父主题: LLM训练业务迁移指导

同意关联代理商云淘科技,购买华为云产品更优惠(QQ 78315851)

内容没看懂? 不太想学习?想快速解决? 有偿解决: 联系专家