评价此页

剖析您的 PyTorch 模块#

创建日期:2020 年 12 月 30 日 | 最后更新:2025 年 11 月 11 日 | 最后验证:2024 年 11 月 5 日

作者: Suraj Subramanian

PyTorch 包含一个剖析器(Profiler)API,用于识别代码中各种 PyTorch 操作的时间和内存开销。Profiler 可以轻松集成到您的代码中,结果既可以表格形式打印,也可以导出为 JSON 追踪文件。

注意

Profiler 支持多线程模型。它在操作所在的同一线程中运行,但也会剖析可能在其他线程中运行的子算子。同时运行的多个 Profiler 将被限制在其各自的线程范围内,以防止结果混淆。

注意

PyTorch 1.8 引入了新的 API,该 API 将在未来的版本中取代旧的 Profiler API。请通过此页面查看新 API。

前往此指南,快速了解 Profiler API 的使用方法。


import torch
import numpy as np
from torch import nn
import torch.autograd.profiler as profiler

使用 Profiler 进行性能调试#

Profiler 对于识别模型中的性能瓶颈非常有用。在本示例中,我们构建一个执行两个子任务的自定义模块:

  • 对输入进行线性变换,以及

  • 使用变换结果获取掩码张量(mask tensor)的索引。

我们使用 profiler.record_function("label") 将每个子任务的代码包装在带有标签的独立上下文管理器中。在 Profiler 的输出中,该子任务中所有操作的聚合性能指标将显示在其对应的标签下。

请注意,使用 Profiler 会产生一定的开销,因此最适合仅在代码调查时使用。如果您正在进行运行时基准测试,请记得移除它。

class MyModule(nn.Module):
    def __init__(self, in_features: int, out_features: int, bias: bool = True):
        super(MyModule, self).__init__()
        self.linear = nn.Linear(in_features, out_features, bias)

    def forward(self, input, mask):
        with profiler.record_function("LINEAR PASS"):
            out = self.linear(input)

        with profiler.record_function("MASK INDICES"):
            threshold = out.sum(axis=1).mean().item()
            hi_idx = np.argwhere(mask.cpu().numpy() > threshold)
            hi_idx = torch.from_numpy(hi_idx).cuda()

        return out, hi_idx

剖析前向传播#

我们初始化随机输入、掩码张量以及模型。

在运行 Profiler 之前,我们对 CUDA 进行预热以确保性能基准测试的准确性。我们将模块的前向传播过程包装在 profiler.profile 上下文管理器中。with_stack=True 参数会将操作的文件名和行号附加到追踪记录中。

警告

with_stack=True 会带来额外的开销,更适合用于代码调查。如果您正在测试性能,请务必将其移除。

model = MyModule(500, 10).cuda()
input = torch.rand(128, 500).cuda()
mask = torch.rand((500, 500, 500), dtype=torch.double).cuda()

# warm-up
model(input, mask)

with profiler.profile(with_stack=True, profile_memory=True) as prof:
    out, idx = model(input, mask)

优化内存性能#

请注意,在内存和时间方面最昂贵的操作出现在 forward (10),它代表了 MASK INDICES 内部的操作。让我们先解决内存消耗问题。我们可以看到第 12 行的 .to() 操作消耗了 953.67 MB。此操作将 mask 复制到 CPU。mask 初始化时使用了 torch.double 数据类型。我们能否通过将其转换为 torch.float 来减少内存占用?

model = MyModule(500, 10).cuda()
input = torch.rand(128, 500).cuda()
mask = torch.rand((500, 500, 500), dtype=torch.float).cuda()

# warm-up
model(input, mask)

with profiler.profile(with_stack=True, profile_memory=True) as prof:
    out, idx = model(input, mask)

print(prof.key_averages(group_by_stack_n=5).table(sort_by='self_cpu_time_total', row_limit=5))

"""
(Some columns are omitted)

-----------------  ------------  ------------  ------------  --------------------------------
             Name    Self CPU %      Self CPU  Self CPU Mem   Source Location
-----------------  ------------  ------------  ------------  --------------------------------
     MASK INDICES        93.61%        5.006s    -476.84 Mb  /mnt/xarfuse/.../torch/au
                                                             <ipython-input-...>(10): forward
                                                             /mnt/xarfuse/  /torch/nn
                                                             <ipython-input-...>(9): <module>
                                                             /mnt/xarfuse/.../IPython/

      aten::copy_         6.34%     338.759ms           0 b  <ipython-input-...>(12): forward
                                                             /mnt/xarfuse/.../torch/nn
                                                             <ipython-input-...>(9): <module>
                                                             /mnt/xarfuse/.../IPython/
                                                             /mnt/xarfuse/.../IPython/

 aten::as_strided         0.01%     281.808us           0 b  <ipython-input-...>(11): forward
                                                             /mnt/xarfuse/.../torch/nn
                                                             <ipython-input-...>(9): <module>
                                                             /mnt/xarfuse/.../IPython/
                                                             /mnt/xarfuse/.../IPython/

      aten::addmm         0.01%     275.721us           0 b  /mnt/xarfuse/.../torch/nn
                                                             /mnt/xarfuse/.../torch/nn
                                                             /mnt/xarfuse/.../torch/nn
                                                             <ipython-input-...>(8): forward
                                                             /mnt/xarfuse/.../torch/nn

      aten::_local        0.01%     268.650us           0 b  <ipython-input-...>(11): forward
      _scalar_dense                                          /mnt/xarfuse/.../torch/nn
                                                             <ipython-input-...>(9): <module>
                                                             /mnt/xarfuse/.../IPython/
                                                             /mnt/xarfuse/.../IPython/

-----------------  ------------  ------------  ------------  --------------------------------
Self CPU time total: 5.347s

"""

该操作的 CPU 内存占用减半了。

优化时间性能#

虽然消耗的时间也减少了一些,但仍然太高。事实证明,将矩阵从 CUDA 复制到 CPU 的开销非常大!forward (12) 中的 aten::copy_ 算子将 mask 复制到 CPU,以便使用 NumPy 的 argwhere 函数。forward(13) 处的 aten::copy_ 将数组作为张量复制回 CUDA。如果我们在这里使用 torch 函数 nonzero(),则可以消除这两次复制。

class MyModule(nn.Module):
    def __init__(self, in_features: int, out_features: int, bias: bool = True):
        super(MyModule, self).__init__()
        self.linear = nn.Linear(in_features, out_features, bias)

    def forward(self, input, mask):
        with profiler.record_function("LINEAR PASS"):
            out = self.linear(input)

        with profiler.record_function("MASK INDICES"):
            threshold = out.sum(axis=1).mean()
            hi_idx = (mask > threshold).nonzero(as_tuple=True)

        return out, hi_idx


model = MyModule(500, 10).cuda()
input = torch.rand(128, 500).cuda()
mask = torch.rand((500, 500, 500), dtype=torch.float).cuda()

# warm-up
model(input, mask)

with profiler.profile(with_stack=True, profile_memory=True) as prof:
    out, idx = model(input, mask)

print(prof.key_averages(group_by_stack_n=5).table(sort_by='self_cpu_time_total', row_limit=5))

"""
(Some columns are omitted)

--------------  ------------  ------------  ------------  ---------------------------------
          Name    Self CPU %      Self CPU  Self CPU Mem   Source Location
--------------  ------------  ------------  ------------  ---------------------------------
      aten::gt        57.17%     129.089ms           0 b  <ipython-input-...>(12): forward
                                                          /mnt/xarfuse/.../torch/nn
                                                          <ipython-input-...>(25): <module>
                                                          /mnt/xarfuse/.../IPython/
                                                          /mnt/xarfuse/.../IPython/

 aten::nonzero        37.38%      84.402ms           0 b  <ipython-input-...>(12): forward
                                                          /mnt/xarfuse/.../torch/nn
                                                          <ipython-input-...>(25): <module>
                                                          /mnt/xarfuse/.../IPython/
                                                          /mnt/xarfuse/.../IPython/

   INDEX SCORE         3.32%       7.491ms    -119.21 Mb  /mnt/xarfuse/.../torch/au
                                                          <ipython-input-...>(10): forward
                                                          /mnt/xarfuse/.../torch/nn
                                                          <ipython-input-...>(25): <module>
                                                          /mnt/xarfuse/.../IPython/

aten::as_strided         0.20%    441.587us          0 b  <ipython-input-...>(12): forward
                                                          /mnt/xarfuse/.../torch/nn
                                                          <ipython-input-...>(25): <module>
                                                          /mnt/xarfuse/.../IPython/
                                                          /mnt/xarfuse/.../IPython/

 aten::nonzero
     _numpy             0.18%     395.602us           0 b  <ipython-input-...>(12): forward
                                                          /mnt/xarfuse/.../torch/nn
                                                          <ipython-input-...>(25): <module>
                                                          /mnt/xarfuse/.../IPython/
                                                          /mnt/xarfuse/.../IPython/
--------------  ------------  ------------  ------------  ---------------------------------
Self CPU time total: 225.801ms

"""

进一步阅读#

我们已经了解了如何使用 Profiler 来调查 PyTorch 模型中的时间和内存瓶颈。在此阅读更多关于 Profiler 的信息: