性能调优

本章节从单卡的性能调优指导入手,帮助用户快速找到单卡训练过程中的性能瓶颈点。多卡场景亦可采用类似手段进行分析。训练性能调优思路可参考下图,具体细节可参考指南中对应部分:

../_images/performance_tuning.png

注:由于首步执行可能存在设备预热/初始化等耗时,下述内容均排除首步执行,推荐观察训练趋于稳定时的现象。

通常训练过程中各个迭代的耗时可拆分为数据预处理部分耗时和网络执行更新部分耗时。首先,可以分别进行耗时统计,明确性能瓶颈发生在哪个阶段,以常见的函数式训练写法为例:

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函数或自定义数据集的__getitem__函数较为耗时,针对这类场景,推荐尝试调用Numpy接口替换torch运算接口,或调用torchvision等组件自带的transforms进行预处理。

网络执行性能调优

本章节只涉及PyNative模式下分析网络API级别耗时。Graph模式为整图下沉执行,耗时主要集中于算子执行,可直接参考基于MindInsight工具分析算子性能进行分析。动态图模式下建议开启同步配置后进行性能分析,若未开启同步,由于代码的异步执行,计时工具可能不能准确反映真实执行耗时。

ms.set_context(pynative_synchronize=True)

注意:同步可能导致网络执行耗时轻微增大,性能调试结束后请关闭同步后训练网络。

开启同步配置后,可以结合打点计时分析性能瓶颈,也可以使用下列工具进行性能分析,加速分析效率:

1)基于Runtime Profiler工具快速定界性能问题

Runtime Profiler是MindSpore提供的一种性能调优工具,基于Runtime Profiler工具可分析正向执行/反向传播/优化器更新等阶段性能瓶颈,也可分析算子性能瓶颈,快速定界性能问题。 使用Runtime Profiler分三步,设置环境变量、在代码中调用接口以及查看统计结果。

步骤 1:设置环境变量

export MS_ENABLE_RUNTIME_PROFILER=1

步骤 2:在代码中调用接口 在待分析程序运行的首尾调用Profiler工具接口_framework_profiler_step_start(),以及_framework_profiler_step_end(),即可开启Profiler功能。

from mindspore._c_expression import _framework_profiler_step_start
from 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 == 10:
        _framework_profiler_step_end()
        exit()

注意:收集近10次迭代的数据即可满足分析要求,迭代数越大则生成文件也越大,使用Profiler工具需保证程序正常退出,因此在示例中的待测程序的尾部调用exit()函数退出。

步骤 3:查看统计结果

当前有多种途径可以查看Runtime Profiler的统计结果,可在执行代码界面直接输出,或查看保存文件(保存的结果文件默认保存在当前执行目录,如果在代码中设置了ms.set_context(save_graphs_path='your path'),则该文件将会保存在 save_graphs_path 目录中)。文件路经下保存有反映Module-Event-Op三个层次统计信息RuntimeProfilerSummary的csv文件、Op详细性能数据RuntimeProfilerDetail的csv文件、反映TimeLine trace信息的josn文件。推荐可视化查看TimeLine trace信息;

Josn文件的命名格式为RuntimeProfilerJson+当前时间戳.json,通过浏览器使用chrome://tracing打开json文件,即可可视化观察timeline信息。

time_log

参考上图实例,可以通过可视化视图了解到网络训练流程中单步训练总耗时、正向执行耗时、反向传播耗时、优化器更新耗时、以及正反向耗时算子排序等信息,根据这些信息可以快速定界性能瓶颈:

如果整体算子执行时间较为耗时,可参考基于MindInsight工具分析算子性能进行进一步分析;

如果确定算子耗时,但无法确定性能瓶颈API接口,可参考基于cProfile工具分析主要耗时API接口进行进一步分析;

如果能够明确性能瓶颈API可参考替换接口实现等价功能进行性能优化;

如果遇到自动微分/优化器更新等框架流程有明显耗时时,请将您的遇到的问题场景通过ISSUE 反馈给我们。

2)基于cProfile工具分析主要耗时接口

cProfile 工具可用于分析Python代码执行耗时,以及正向执行中的主要耗时接口

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则表示依照接口耗时(排除接口内部调用其他接口的耗时)排序。

time_log

执行后您将得到如图所示的统计文件,我们主要关注mindtorch目录下具体接口的耗时,以alexnet为例,conv2d为耗时占比最高的接口。

3)基于MindInsight工具分析算子性能

MindSpore Insight是MindSpore原生框架提供的性能分析工具,从单机和集群的角度分别提供了多项指标,用于帮助用户进行性能调优。利用该工具用户可观察到硬件侧算子的执行耗时,昇腾环境可参考性能调试(Ascend),GPU环境可参考性能调试(GPU)

op_statistics.png

最终您将得到如图所示的算子性能分析看板,通过该看板可以明确算子总耗时/算子平均单次耗时/算子耗时占比等信息。 如果是进行性能对比,和PyTorch的 Profiler结果进行比较可进一步确定性能瓶颈是否源于算子执行,并明确各算子性能差距。 在昇腾硬件上可参考使用混合精度加速训练教程利用混合精度特性加速算子执行性能。

4)替换接口实现等价功能/反馈问题

如果经上述手段分析,能够确定某个API的执行性能为整体训练的性能瓶颈,则可以尝试通过替换或组合其他API进行等价替换,例如,mindtorch.torch.pow(x, 2)可尝试替换为mindtorch.torch.square(x);或通过自定义算子实现加速。

如果您遇到等价替换无法进一步优化性能或者优化器更新等框架流程有明显耗时的场景,请将您的遇到的问题通过ISSUE 反馈给我们。