評價此頁

分析您的 PyTorch 模組#

創建於:2020 年 12 月 30 日 | 最後更新:2024 年 1 月 19 日 | 最後驗證:2024 年 11 月 05 日

作者: Suraj Subramanian

PyTorch 包含一個Profiler API,有助於識別程式碼中各種 PyTorch 操作的時間和記憶體成本。Profiler 可以輕鬆整合到您的程式碼中,並且結果可以列印為表格或以 JSON 跟蹤檔案形式返回。

注意

Profiler 支援多執行緒模型。Profiler 在與操作相同的執行緒中執行,但它也會分析可能在另一個執行緒中執行的子操作。併發執行的 Profiler 將被限制在其自己的執行緒中,以防止結果混淆。

注意

PyTorch 1.8 引入了新的 API,將在未來的版本中取代舊的 Profiler API。請檢視 此頁面 上的新 API。

請參閱 此食譜,以更快地瞭解 Profiler API 的用法。


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

使用 Profiler 進行效能除錯#

Profiler 可用於識別模型中的效能瓶頸。在此示例中,我們構建了一個執行兩個子任務的自定義模組:

  • 對輸入進行線性變換,以及

  • 使用變換結果在掩碼張量上獲取索引。

我們使用 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 的更多資訊: