MSAdapter调试调优指南
1.简介
MSAdapter是一款将PyTorch训练脚本高效迁移至MindSpore框架执行的实用工具,旨在不改变原生PyTorch用户的编程使用习惯下,使得PyTorch风格代码能在昇腾硬件上获得高效性能。用户只需要将PyTorch源代码中torch
系列相关的包导入部分(如torch、torchvision
等),替换为导入msadapter.pytorch
系列相关的包(如msadapter.pytorch、msadapter.torchvision
等),加上少量训练代码适配即可实现模型在昇腾硬件上的训练。
本教材旨在为开发者提供一个简明扼要的精度问题与性能问题初步定位指导。如果您还未完成模型迁移转换,可参考MSAdapter用户使用指南。
2.功能调试
PyNative模式功能调试
1)当执行出现异常时,您会得到由MindSpore反馈的报错信息,MindSpore报错信息采用Python Traceback处理,包括Python堆栈信息、报错类型与报错描述等信息,对于接口级别的问题,可以根据报错堆栈信息快速定位出问题位置:
更多细节请参考MindSpore功能调试。
2)PyNative模式模式下可以通过添加Print打印信息获取问题接口当前的输入数据具体取值:
若输入数据不符合预期,则可能由于前置接口导致问题,可以在关键位置添加断点,逐步缩小范围,直至明确问题接口;
如果您在使用过程中遇到框架问题或接口无法对标请通过ISSUE 和我们反馈交流。
Graph模式功能调试
首先推荐您在PyNative模式(即默认模式)下完成功能调试后再尝试Graph模式执行。当Graph模式出现异常时,可结合报错信息和静态图语法支持文档进行手动适配。同时您将您的受限场景通过ISSUE 反馈给我们,我们会优先分析支持。
3.精度调优
您可以通过对比迁移后模型和torch原始模型的执行结果,确保迁移模型的功能正确性。
方式一:利用TroubleShooter工具进行比较
Step1:安装TroubleShooter工具
pip install troubleshooter -i https://pypi.org/simple
Step2:参考以下用例进行模型推理结果对比
import sys
import numpy as np
import troubleshooter as ts
sys.path.append("./alexnet_adapter.py") # MSAdapter模型定义文件路经
sys.path.append("./alexnet_torch.py") # PyTorch模型定义文件路经
from alexnet_adapter import AlexNet as msa_net
from alexnet_torch import AlexNet as torch_net
pt_net = torch_net()
ms_net = msa_net()
diff_finder = ts.migrator.NetDifferenceFinder(pt_net=pt_net, ms_net=ms_net, auto_conv_ckpt=2)
# auto_conv_ckpt为2时, PyTorch网络权重会自动加载到MSAdapter网络权重中;
diff_finder.compare(auto_inputs=(((128, 3, 224, 224), np.float32), )) # 提供输入的shape和type自动构造输入数据,并进行比较输出结果,默认执行model.eval()模式;
您将获得如下执行结果:
PyTorch原生模型权重与MSAdapter迁移模型权重映射情况;
PyTorch原生模型与MSAdapter迁移模型完成权重自动转换后权重值比较结果;
PyTorch原生模型与MSAdapter迁移模型推理结果比较,如图所示则表示网络推理结果完全一致。
更多使用细节可参考教程应用场景5:比较MindSpore和PyTorch网络输出是否一致。
方式二:手动加载pth进行比较
在比较之前,需要保证以下条件的一致性:
1)确保网络输入完全一致(可以使用固定的输入数据,也可调用真实数据集);
2)确保执行推理模式
model = LeNet()
model.eval()
由于框架随机策略(详情请参考MindSpore与PyTorch随机数策略的区别)以及各自内置随机数生成算法的实现存在差异,所以即使用户配置相同的随机种子,两个框架生成的随机数并不一致。同理,带有随机性的接口,如nn.dropout
,当配置概率不为0或1时,即使输入一致,由于内置随机数逻辑差异,两个框架得到的输出结果并不一致。通过配置网络为推理模式则可排除这方面随机性的影响。
3)确保网络权重的一致性
由于MindSpore随机策略与PyTorch随机策略有所不同,即使网络层初始化策略与算法完全一致,也无法保证权重值一致。此时可以先保存torch的网络权重,再加载至MSAdapter迁移模型的权重中:
Step1:在torch原始脚本中保存网络权重至本地
torch.save(net.state_dict(), 'model.pth')
Step2:将torch权重加载至MSAdapter迁移模型中
net.load_state_dict(torch.load('model.pth'), strict=True)
在MSAdapter迁移网络脚本中加载Step1保存的pth,即可将torch的权重加载到迁移模型中,从而保证网络权重的一致性;
如果输出误差过大情况,可以在PyNative模式下基于关键位置添加断点,逐步缩小范围,直至明确误差是否合理。
4.性能调优
本章节从单卡的性能调优指导入手,帮助用户快速找到单卡训练过程中的性能瓶颈点。多卡场景亦可采用类似手段进行分析。
注:由于首步执行可能存在设备预热/初始化等耗时,下述内容均排除首步执行,推荐观察训练趋于稳定时的现象。
通常训练过程中各个迭代的耗时可拆分为数据预处理部分耗时和网络执行更新部分耗时。可以分别进行耗时统计,明确性能瓶颈发生在哪个阶段,以常见的函数式训练写法为例:
import time
...
train_data = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2, drop_last=True)
...
from mindspore.common.api import _pynative_executor
# 数据迭代训练
for i in range(epochs):
train_time = time.time()
for X, y in train_data:
X, y = X.to(config_args.device), y.to(config_args.device)
_pynative_executor.sync() # 调用同步接口
date_time = time.time()
print("Data Time: ", date_time - train_time, flush=True) # 数据预处理部分耗时
res = train_step(X, y)
print("------>epoch:{}, loss:{:.6f}".format(i, res.numpy()))
_pynative_executor.sync() # 调用同步接口
train_time = time.time()
print("Train Time: ", train_time - date_time, flush=True) # 网络执行更新部分耗时
与此同时,也可以查看PyTorch的 Data Time和 Train Time。(Tips:由于算子下发时间和算子执行时间是不同的,因此在记录时间之前,调用同步接口可以保证计算操作同步执行,让计时更加准确,例如 torch是调用torch.cuda.synchronize()
,而MindSpore是调用_pynative_executor.sync()
接口),下面代码为PyTorch代码记录Train Time和Data Time的示例。
import time
...
train_data = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2, drop_last=True)
...
# 数据迭代训练
for i in range(epochs):
train_time = time.time()
for X, y in train_data:
X, y = X.to(config_args.device), y.to(config_args.device)
torch.cuda.synchronize() # 调用同步接口
date_time = time.time()
print("Data Time: ", date_time - train_time, flush=True) # 数据预处理部分耗时
res = model(X)
loss = loss_func(res, y)
optimizer.zero_grad()
loss.backward()
print("------>epoch:{}, loss:{:.6f}".format(i, res.numpy()))
train_time = time.time()
print("Train Time: ", train_time - date_time, flush=True) # 网络执行更新部分耗时
正常情况下,Data Time应基本可忽略不计,如果出现了Data Time和 Train Time在相同或相邻数量级的情况,可参考数据处理性能调优来降低数据加载耗时。
在Data Time忽略不计的情况下,如果Train Time有明显差距,则同样需要进一步利用打点计时的方式,分析前向
,后向
以及优化器
的耗时,进而定位性能问题原因。然后可参考网络执行性能调优以及算子执行性能调优中的分析工具,查看具体算子的性能参数。
数据处理性能调优
1.启用多进程数据加载
如果出现数据耗时过大的情况,请先确认是否合理配置DataLoader中的num_workers
属性。num_workers
表示采用多进程并行方式执行数据加载时的进程数,num_workers
取值越大表示并行程度越高,但由于并行进程会开辟额外存储空间,以及进程数过多可能加剧进程间通讯耗时,不推荐配置过大,按需配置即可。推荐将num_workers
配置为单次网络训练耗时与单次数据预处理耗时的差异倍数向上取整的取值,例如,网络执行单次耗时为10 s/step,数据预处理单次耗时为20 s/step,则配置num_workers=2
可使得数据处理耗时基本可被完全隐藏。
2.优化数据预处理操作
如果依照上述方法预计的num_workers
取值大于16,可以着重分析数据预处理耗时,性能瓶颈可能出现在预处理操作中。如自定义的collate_fn函数较为耗时等。
网络执行性能调优
本章节只涉及PyNative模式下分析网络API级别耗时。Graph模式为整图下沉执行,耗时主要集中于算子执行,可直接参考算子执行性能调优进行分析。
1.动态图模式下可以通过开启同步结合打点计时分析性能瓶颈
ms.set_context(pynative_synchronize=True)
注意:若未开启同步,python侧计时可能不能准确反映真实执行耗时。同步可能导致网络执行耗时轻微增大,性能调试结束后请关闭同步后训练网络。
2.结合 cProfile 工具分析主要耗时接口
import cProfile, pstats, io
from pstats import SortKey
pr = cProfile.Profile()
pr.enable()
...
训练代码
...
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
ps.print_stats()
with open('time_log.txt', 'w+') as f:
f.write(s.getvalue())
其中sort_stats
配置为cumtime
表示依照接口耗时(包含该接口内部调用其他接口的总耗时)排序,若配置为tottime
则表示依照接口耗时(排除接口内部调用其他接口的耗时)排序。
执行后您将得到如图所示的统计文件,我们主要关注msadapter目录下具体接口的耗时,以alexnet为例,conv2d为耗时占比最高的接口。
算子执行性能调优
最终您将得到如图所示的算子性能分析看板,通过该看板可以明确算子总耗时/算子平均单次耗时/算子耗时占比等信息。
Runtime Profiler是MindSpore提供的一种性能调优工具,用于显示执行过程中每个step的各个模块耗时占比,快速定界性能问题。
使用Runtime Profiler分三步,设置环境变量、在代码中调用接口以及查看统计结果。
步骤 1-设置环境变量
export MS_ENABLE_RUNTIME_PROFILER=1
步骤 2-在代码中调用接口
在待分析程序运行的首尾调用Profiler工具接口_framework_profiler_step_start()
,以及_framework_profiler_step_end()
。如果您的网络脚本使用了model.train,则设置MS_ENABLE_RUNTIME_PROFILER=1
即可开启Profiler功能,可直接查看到步骤 3。
form mindspore._c_expression import _framework_profiler_step_start
form mindspore._c_expression import _framework_profiler_step_end
for i, data in enumerate(data_loader):
if i == 0:
_framework_profiler_step_start()
"""
training
"""
if i == 20:
_framework_profiler_step_end()
exit()
注意,使用Profiler工具需保证程序正常退出,因此在示例中的待测程序的尾部调用exit()
函数退出。
步骤 3-查看统计结果
有两个途径可以查看Runtime Profiler的统计结果,第一种是执行代码界面直接输出;
第二种是查看保存的名字为RuntimeProfilerSummary+当前时间戳.csv文件,此文件默认保存在当前执行目录,如果在代码中设置了mindspore.context(save_graph_path='your path')
,则该文件将会保存在 save_graph_path
目录中。