关于算法:Paddle模型性能分析工具Profiler定位瓶颈点优化程序提升性能

4次阅读

共计 11619 个字符,预计需要花费 30 分钟才能阅读完成。

我的项目链接,fork 一下即可应用
https://aistudio.baidu.com/aistudio/projectdetail/4482932?contributionType=1

Paddle 模型性能剖析 Profiler:定位性能瓶颈点优化程序晋升性能

Paddle Profiler 是飞桨框架自带的低开销性能分析器,能够对模型运行过程的性能数据进行收集、统计和展现。性能分析器提供的数据能够帮忙定位模型的瓶颈,辨认造成程序运行工夫过长或者 GPU 利用率低的起因,从而寻求优化计划来取得性能的晋升

1. 应用 Profiler 工具调试程序性能

在模型性能剖析中,通常采纳如下四个步骤:

  • 获取模型失常运行时的 ips(iterations per second,每秒的迭代次数),给出 baseline 数据。
  • 开启性能分析器,定位性能瓶颈点。
  • 优化程序,查看优化成果。
  • 获取优化后模型失常运行时的 ips,和 baseline 比拟,计算实在的晋升幅度。

上面是应用神经网络对 cifar10 进行分类的示例代码,外面加上了启动性能剖析的代码。通过这个比较简单的示例,来看性能剖析工具是如何通过上述四个步骤在调试程序性能中发挥作用。

1.1 应用 cifar10 数据集卷积神经网络进行图像分类

import paddle
import paddle.nn.functional as F
from paddle.vision.transforms import ToTensor
import numpy as np
import matplotlib.pyplot as plt

print(paddle.__version__)

加载数据集

cifar10 数据集由 60000 张大小为 32 * 32 的彩色图片组成,其中有 50000 张图片组成了训练集,另外 10000 张图片组成了测试集。这些图片分为 10 个类别,将训练一个模型可能把图片进行正确的分类。

transform = ToTensor()
cifar10_train = paddle.vision.datasets.Cifar10(mode='train',
                                               transform=transform)
cifar10_test = paddle.vision.datasets.Cifar10(mode='test',
                                              transform=transform)

组建网络

接下来应用飞桨定义一个应用了三个二维卷积(Conv2D ) 且每次卷积之后应用 relu 激活函数,两个二维池化层(MaxPool2D),和两个线性变换层组成的分类网络,来把一个 (32, 32, 3) 形态的图片通过卷积神经网络映射为 10 个输入,这对应着 10 个分类的类别

class MyNet(paddle.nn.Layer):
    def __init__(self, num_classes=1):
        super(MyNet, self).__init__()

        self.conv1 = paddle.nn.Conv2D(in_channels=3, out_channels=32, kernel_size=(3, 3))
        self.pool1 = paddle.nn.MaxPool2D(kernel_size=2, stride=2)

        self.conv2 = paddle.nn.Conv2D(in_channels=32, out_channels=64, kernel_size=(3,3))
        self.pool2 = paddle.nn.MaxPool2D(kernel_size=2, stride=2)

        self.conv3 = paddle.nn.Conv2D(in_channels=64, out_channels=64, kernel_size=(3,3))

        self.flatten = paddle.nn.Flatten()

        self.linear1 = paddle.nn.Linear(in_features=1024, out_features=64)
        self.linear2 = paddle.nn.Linear(in_features=64, out_features=num_classes)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.pool1(x)

        x = self.conv2(x)
        x = F.relu(x)
        x = self.pool2(x)

        x = self.conv3(x)
        x = F.relu(x)

        x = self.flatten(x)
        x = self.linear1(x)
        x = F.relu(x)
        x = self.linear2(x)
        return x

模型训练 & 预测

接下来,用一个循环来进行模型的训练,将会:

应用 paddle.optimizer.Adam 优化器来进行优化。

应用 F.cross_entropy 来计算损失值。

应用 paddle.io.DataLoader 来加载数据并组建 batch。

import paddle.profiler as profiler
#参数设置
epoch_num = 10
batch_size = 32
learning_rate = 0.001

val_acc_history = []
val_loss_history = []

def train(model):
    print('start training ...')
    # turn into training mode
    model.train()

    opt = paddle.optimizer.Adam(learning_rate=learning_rate,
                                parameters=model.parameters())

    train_loader = paddle.io.DataLoader(cifar10_train,
                                        shuffle=True,
                                        batch_size=batch_size,
                                        num_workers=4)

    valid_loader = paddle.io.DataLoader(cifar10_test, batch_size=batch_size)

    # 创立性能分析器相干的代码
    def my_on_trace_ready(prof):# 定义回调函数,性能分析器完结采集数据时会被调用
        callback = profiler.export_chrome_tracing('./profiler_demo') # 创立导出性能数据到 profiler_demo 文件夹的回调函数
        callback(prof)  # 执行该导出函数
        prof.summary(sorted_by=profiler.SortedKeys.GPUTotal) # 打印表单,按 GPUTotal 排序表单项

    p = profiler.Profiler(scheduler = [3,14], on_trace_ready=my_on_trace_ready, timer_only=True) # 初始化 Profiler 对象

    p.start() # 性能分析器进入第 0 个 step
    
    for epoch in range(epoch_num):
        for batch_id, data in enumerate(train_loader()):
            x_data = data[0]
            y_data = paddle.to_tensor(data[1])
            y_data = paddle.unsqueeze(y_data, 1)

            logits = model(x_data)
            loss = F.cross_entropy(logits, y_data)

            if batch_id % 1000 == 0:
                print("epoch: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, loss.numpy()))
            loss.backward()
            opt.step()
            opt.clear_grad()

            p.step() # 批示性能分析器进入下一个 step
            if batch_id == 19:
                p.stop() # 敞开性能分析器
                exit() # 做性能剖析时,能够将程序提前退出


        # evaluate model after one epoch
        model.eval()
        accuracies = []
        losses = []
        for batch_id, data in enumerate(valid_loader()):
            x_data = data[0]
            y_data = paddle.to_tensor(data[1])
            y_data = paddle.unsqueeze(y_data, 1)

            logits = model(x_data)
            loss = F.cross_entropy(logits, y_data)
            acc = paddle.metric.accuracy(logits, y_data)
            accuracies.append(acc.numpy())
            losses.append(loss.numpy())

        avg_acc, avg_loss = np.mean(accuracies), np.mean(losses)
        print("[validation] accuracy/loss: {}/{}".format(avg_acc, avg_loss))
        val_acc_history.append(avg_acc)
        val_loss_history.append(avg_loss)
        model.train()

model = MyNet(num_classes=10)
train(model)

** 局部后果展现:**

epoch: 6, batch_id: 0, loss is: [0.91811454]
epoch: 6, batch_id: 1000, loss is: [0.89851004]
[validation] accuracy/loss: 0.7232428193092346/0.8434960246086121
epoch: 7, batch_id: 0, loss is: [0.60690844]
epoch: 7, batch_id: 1000, loss is: [0.6912922]
[validation] accuracy/loss: 0.7049720287322998/0.887704074382782
epoch: 8, batch_id: 0, loss is: [0.6330824]
epoch: 8, batch_id: 1000, loss is: [0.5715592]
[validation] accuracy/loss: 0.7176517844200134/0.8511289954185486
epoch: 9, batch_id: 0, loss is: [0.29487646]
epoch: 9, batch_id: 1000, loss is: [0.9094696]
[validation] accuracy/loss: 0.7097643613815308/0.9166476130485535

1.1.1 获取性能调试前模型失常运行的 ips

上述程序在创立 Profiler 时候,timer_only 设置的值为 True,此时将只开启 benchmark 性能,不开启性能分析器,程序输入模型失常运行时的 benchmark 信息如下

  • Reader Ratio:示意数据读取局部占训练 batch 迭代过程的工夫占比,
  • reader_cost:代表数据读取工夫,
  • batch_cost:代表 batch 迭代的工夫,
  • ips:示意每秒能迭代多少次,即跑多少个 batch。

能够看到,此时的 ips 为 70.99,可将这个值作为优化比照的 baseline。

 ============================================Perf Summary============================================
 Reader Ratio: 35.240%
 Time Unit: s, IPS Unit: steps/s
|                 |       avg       |       max       |       min       |
 |   reader_cost   |     0.00496     |     0.00542     |     0.00469     |
|    batch_cost   |     0.01408     |     0.01325     |     0.01246     |
 |       ips       |     70.99914    |     80.24470    |     75.46403    |

1.1.2. 开启性能分析器,定位性能瓶颈点

批改程序,将Profiler 的 timer_only 参数设置为 False,此时代表不只开启 benchmark 性能,还将开启性能分析器,进行具体的性能剖析。

p = profiler.Profiler(scheduler = [3,14], on_trace_ready=my_on_trace_ready, timer_only=False)

性能分析器会收集程序在第 3 到 14 次(不包含 14)训练迭代过程中的性能数据,并在 profiler_demo 文件夹中输入一个 json 格局的文件,用于展现程序执行过程的 timeline,可通过 chrome 浏览器的 chrome://tracing 插件关上这个文件进行查看。

如图所示,把 json 文件 load 即可:

性能分析器还会间接在终端打印统计表单(倡议重定向到文件中查看),查看程序输入的 Model Summary 表单

-----------------------------------------------Model Summary-----------------------------------------------
Time unit: ms
---------------  ------  ----------------------------------------  ----------------------------------------  
Name             Calls   CPU Total / Avg / Max / Min / Ratio(%)    GPU Total / Avg / Max / Min / Ratio(%)    
---------------  ------  ----------------------------------------  ----------------------------------------  
ProfileStep      11      138.99 / 12.64 / 17.91 / 10.65 / 100.00   8.81 / 0.80 / 0.80 / 0.80 / 100.00        
  Dataloader     11      16.88 / 1.53 / 6.91 / 0.09 / 12.14        0.00 / 0.00 / 0.00 / 0.00 / 0.00          
  Forward        11      45.18 / 4.11 / 4.41 / 3.61 / 32.50        2.73 / 0.25 / 0.25 / 0.25 / 31.01         
  Backward       11      27.63 / 2.51 / 2.85 / 2.37 / 19.88        4.04 / 0.37 / 0.37 / 0.36 / 45.81         
  Optimization   11      19.75 / 1.80 / 1.89 / 1.61 / 14.21        1.05 / 0.10 / 0.10 / 0.09 / 11.56         
  Others         -       29.55 / - / - / - / 21.26                 1.05 / - / - / - / 11.63                  
---------------  ------  ----------------------------------------  ----------------------------------------  
Note:
在此表中,GPU 工夫是该阶段调用的所有设施(GPU)事件的总和。与概述摘要不同,如果两个设施(GPU)事件在不同的流上执行重叠工夫,咱们间接在这里求和。
  • 其中 ProfileStep 示意训练 batch 的迭代 step 过程,对应代码中每两次调用 p.step()的间隔时间;
  • Dataloader示意数据读取的工夫,即 for batch_id, data in enumerate(train_loader())的执行工夫;
  • Forward示意模型前向的工夫,即 logits = model(x_data)的执行工夫,
  • Backward示意反向流传的工夫,即 loss.backward()的执行工夫;
  • Optimization示意优化器的工夫,即 opt.step()的执行工夫。

通过 timeline 能够看到,Dataloader 占了执行过程的很大比重,Model Summary 显示其靠近了 12%。分析程序发现,这是因为模型自身比较简单,须要的计算量小,再加上 Dataloader 筹备数据时只用了单过程来读取,使得程序读取数据时和执行计算时没有并行操作,导致 Dataloader 占比过大。

1.1.3. 优化程序,查看优化成果

辨认到了问题产生的起因,对程序持续做如下批改,将 Dataloader 的 num_workers 设置为 4,使得能有多个过程并行读取数据。

train_loader = paddle.io.DataLoader(cifar10_train,
                                    shuffle=True,
                                    batch_size=batch_size,
                                    num_workers=4)

从新对程序进行性能剖析,新的 timeline 和 Model Summary 如下所示

-----------------------------------------------Model Summary-----------------------------------------------
Time unit: ms
---------------  ------  ----------------------------------------  ----------------------------------------  
Name             Calls   CPU Total / Avg / Max / Min / Ratio(%)    GPU Total / Avg / Max / Min / Ratio(%)    
---------------  ------  ----------------------------------------  ----------------------------------------  
ProfileStep      11      89.44 / 8.13 / 8.76 / 7.82 / 100.00       8.82 / 0.80 / 0.80 / 0.80 / 100.00        
  Dataloader     11      1.51 / 0.14 / 0.16 / 0.12 / 1.69          0.00 / 0.00 / 0.00 / 0.00 / 0.00          
  Forward        11      31.67 / 2.88 / 3.17 / 2.82 / 35.41        2.72 / 0.25 / 0.25 / 0.24 / 36.11         
  Backward       11      25.35 / 2.30 / 2.49 / 2.20 / 28.34        4.07 / 0.37 / 0.37 / 0.37 / 42.52         
  Optimization   11      11.67 / 1.06 / 1.16 / 1.01 / 13.04        1.04 / 0.09 / 0.10 / 0.09 / 10.59         
  Others         -       19.25 / - / - / - / 21.52                 1.06 / - / - / - / 10.78                  
---------------  ------  ----------------------------------------  ----------------------------------------

能够看到,从 Dataloader 中取数据的工夫大大减少,从 12% 变成了均匀只占一个 step 的 1.69%,并且均匀一个 step 所须要的工夫也相应缩小了从 1.53 到 0.14。

### 1.1.4 获取优化后模型失常运行的 ips,确定实在晋升幅度

从新将 timer_only 设置的值为 True,获取优化后模型失常运行时的 benchmark 信息




============================================Perf Summary============================================
Reader Ratio: 1.718%
Time Unit: s, IPS Unit: steps/s
|                 |       avg       |       max       |       min       |
|   reader_cost   |     0.00013     |     0.00015     |     0.00012     |
|    batch_cost   |     0.00728     |     0.00690     |     0.00633     |
|       ips       |    137.30879    |    158.01126    |    144.91796    |

<font size=3 color=”red”>此时从原来的 Reader Ratio: 35.240%—->Reader Ratio: 1.718%

ips 的值变成了 137.3,相比优化前的 baseline70.99,模型真实性能晋升了 193%。

</font>

<font size=3 color=”blue”>留神点:</font>

因为 Profiler 开启的时候,收集性能数据自身也会造成程序性能的开销,因而失常跑程序时请不要开启性能分析器,性能分析器只作为调试程序性能时应用。

  • 1. 如果想取得程序失常运行时候的 benchmark 信息(如 ips),能够像示例一样将 Profiler 的 timer_only 参数设置为 True,此时不会进行详尽的性能数据收集,简直不影响程序失常运行的性能,所取得的 benchmark 信息也趋于实在。
  • 2.benchmark 信息计算的数据范畴是从调用 Profiler 的 start 办法开始,到调用 stop 办法完结这个过程的数据。而 Timeline 和性能数据的统计表单的数据范畴是所指定的采集区间,如这个例子中的第 3 到 14 次迭代,这会导致开启性能分析器时统计表单和 benchmark 信息输入的值不同(如统计到的 Dataloader 的工夫占比)。
  • 3. 当 benchmark 统计的范畴和性能分析器统计的范畴不同时,因为 benchmark 统计的是均匀工夫,如果 benchmark 统计的范畴笼罩了性能分析器开启的范畴,也笼罩了敞开性能调试时的失常执行的范畴,此时 benchmark 的值没有意义,因而开启性能分析器时请以性能分析器输入的统计表单为参考,这也是为何下面示例里在开启性能分析器时没贴 benchmark 信息的起因。

1.1.5 后果展现

# 后果展现

plt.plot(val_acc_history, label = 'validation accuracy')

plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.ylim([0.5, 0.8])
plt.legend(loc='lower right')

2.2 统计表单展现

统计表单负责对采集到的数据 (Event) 从多个不同的角度进行解读,也能够了解为对 timeline 进行一些量化的指标计算。目前提供 Device Summary、Overview Summary、Model Summary、Distributed Summary、Operator Summary、Kernel Summary、Memory Manipulation Summary 和 UserDefined Summary 的统计表单,每个表单从不同的角度进行统计计算。每个表单的统计内容简要叙述如下:

Device Summary

-------------------Device Summary-------------------
------------------------------  --------------------  
Device                          Utilization (%)  
------------------------------  --------------------  
CPU(Process)                    77.13  
CPU(System)                     25.99  
GPU2                            55.50  
------------------------------  --------------------  
Note:
CPU(过程) 利用率 = 以后过程在所有 cpu 内核上的 CPU 工夫 / 通过的工夫,因而最大利用率能够达到 100% * cpu 内核数。CPU(零碎)利用率 = 所有过程在所有 cpu 内核上的 CPU 工夫(繁忙工夫)/(繁忙工夫 + 闲暇工夫)。GPU 利用率 = 以后过程 GPU 工夫 / 已用工夫。----------------------------------------------------

DeviceSummary 提供 CPU 和 GPU 的均匀利用率信息。其中

CPU(Process): 指的是过程的 cpu 均匀利用率,算的是从 Profiler 开始记录数据到完结这一段过程,过程所利用到的 cpu core 的总工夫与该段时间的占比。因而如果是多核的状况,对于过程来说 cpu 均匀利用率是有可能超过 100% 的,因为同时用到的多个 core 的工夫进行了累加。

CPU(System): 指的是整个零碎的 cpu 均匀利用率,算的是从 Profiler 开始记录数据到完结这一段过程,整个零碎所有过程利用到的 cpu core 总工夫与该段时间乘以 cpu core 的数量的占比。能够当成是从 cpu 的视角来算的利用率。

GPU: 指的是过程的 gpu 均匀利用率,算的是从 Profiler 开始记录数据到完结这一段过程,过程在 gpu 上所调用的 kernel 的执行工夫 与 该段时间 的占比。

Overview Summary

Overview Summary 用于展现每种类型的 Event 一共别离耗费了多少工夫,对于多线程或多 stream 下,如果同一类型的 Event 有重叠的时间段,采取取并集操作,不对重叠的工夫进行反复计算。

---------------------------------------------Overview Summary---------------------------------------------
Time unit: ms
-------------------------  -------------------------  -------------------------  -------------------------  
Event Type                 Calls                      CPU Time                   Ratio (%)  
-------------------------  -------------------------  -------------------------  -------------------------  
ProfileStep                8                          4945.15                    100.00  
  CudaRuntime              28336                      2435.63                    49.25  
  UserDefined              486                        2280.54                    46.12  
  Dataloader               8                          1819.15                    36.79  
  Forward                  8                          1282.64                    25.94  
  Operator                 8056                       1244.41                    25.16  
  OperatorInner            21880                      374.18                     7.57  
  Backward                 8                          160.43                     3.24  
  Optimization             8                          102.34                     2.07  
-------------------------  -------------------------  -------------------------  -------------------------  
                          Calls                      GPU Time                   Ratio (%)  
-------------------------  -------------------------  -------------------------  -------------------------  
  Kernel                   13688                      2744.61                    55.50  
  Memcpy                   496                        29.82                      0.60  
  Memset                   104                        0.12                       0.00  
  Communication            784                        257.23                     5.20  
-------------------------  -------------------------  -------------------------  -------------------------  
Note:
在此表中,咱们依据事件类型汇总了所有收集到的事件。在主机上收集的事件工夫显示为 CPU 工夫,如果在设施上则显示为 GPU 工夫。不同类型的事件可能会重叠或蕴含,例如 Operator 包含 OperatorInner,因而比率之和不是 100%。有重叠的同类型事件的工夫不会计算两次,合并后所有工夫相加。Example:
Thread 1:
  Operator: |___________|     |__________|
Thread 2:
  Operator:   |____________|     |___|
After merged:
  Result:   |______________|  |__________|

----------------------------------------------------------------------------------------------------------

不在持续具体阐明,参考我的项目即可:

我的项目链接,fork 一下即可应用
https://aistudio.baidu.com/aistudio/projectdetail/4482932?contributionType=1

正文完
 0