深入了解 PyTorch Intel CPU 的效能原理 (Part 2)¶
建立於:2022 年 10 月 14 日 | 最後更新:2024 年 1 月 16 日 | 最後驗證:未驗證
作者:Min Jean Cho、Jing Xu、Mark Saroufim
在深入了解 PyTorch Intel CPU 的效能原理教學中,我們介紹了如何調整 CPU 執行階段配置、如何分析它們,以及如何將它們整合到 TorchServe 中以獲得最佳化的 CPU 效能。
在本教學中,我們將示範如何透過 Intel® Extension for PyTorch* Launcher 使用記憶體分配器來提升效能,以及如何透過 Intel® Extension for PyTorch* 在 CPU 上最佳化核心,並將它們應用於 TorchServe,展示 ResNet50 的 7.71 倍輸送量加速和 BERT 的 2.20 倍輸送量加速。
先決條件¶
在本教學中,我們將使用由上而下的微架構分析 (TMA) 來分析和展示後端限制 (記憶體限制、核心限制) 通常是未最佳化或未調整的深度學習工作負載的主要瓶頸,並示範透過 Intel® Extension for PyTorch* 改善後端限制的優化技術。我們將使用 toplev,這是基於 Linux perf 構建的 pmu-tools 的一部分,用於 TMA。
我們還將使用 Intel® VTune™ Profiler 的 Instrumentation and Tracing Technology (ITT) 來進行更細粒度的分析。
由上而下的微架構分析方法 (TMA)¶
在調整 CPU 以獲得最佳效能時,了解瓶頸在哪裡很有用。大多數 CPU 核心都有晶片上的效能監控單元 (PMU)。PMU 是 CPU 核心內專用的邏輯元件,用於計算系統上發生的特定硬體事件。這些事件的範例可能是快取未命中或分支預測錯誤。PMU 用於由上而下的微架構分析 (TMA) 以識別瓶頸。TMA 由層次結構組成,如下所示
頂層、第 1 層指標收集Retiring、Bad Speculation、Front End Bound、Back End Bound。CPU 的 Pipeline 在概念上可以簡化並分為兩個部分:前端和後端。前端負責獲取程式碼並將其解碼為稱為微操作 (uOps) 的低階硬體操作。然後將 uOps 饋送到後端,這個過程稱為分配。一旦分配完成,後端負責在可用的執行單元中執行 uOp。完成 uOp 的執行稱為retirement。相反,bad speculation 是指推測性獲取的 uOps 在退休之前被取消,例如在分支預測錯誤的情況下。每個指標都可以在後續層級中進一步分解,以查明瓶頸。
針對後端限制進行調整¶
大多數未調整的深度學習工作負載都會受到後端限制。解決後端限制通常是解決導致退休時間長於必要的延遲來源。如上所示,後端限制有兩個子指標 – 核心限制和記憶體限制。
記憶體限制停頓的原因與記憶體子系統有關。例如,last-level 快取 (LLC 或 L3 快取) 未命中導致存取 DRAM。縮放深度學習模型通常需要大量的運算。高運算利用率需要確保在執行單元需要執行 uOp 時資料可用。這需要預取資料並在快取中重複使用資料,而不是多次從主記憶體中獲取相同的資料,這會導致在返回資料時執行單元處於飢餓狀態。在本教學中,我們將展示更有效率的記憶體分配器、運算符融合和記憶體佈局格式最佳化如何減少記憶體限制的開銷,並提高快取局部性。
核心限制停頓表示在沒有未完成的記憶體存取時,對可用執行單元的利用率不佳。例如,一行中的幾個通用矩陣-矩陣乘法 (GEMM) 指令爭奪 fused-multiply-add (FMA) 或點積 (DP) 執行單元可能會導致核心限制停頓。包括 DP 核心在內的關鍵深度學習核心已由 oneDNN library (oneAPI Deep Neural Network Library) 進行了很好的最佳化,從而減少了核心限制的開銷。
像 GEMM、卷積、反卷積這樣的運算是計算密集型的。而像池化、批次正規化、ReLU 這樣的激活函數則受到記憶體限制。
Intel® VTune™ Profiler 的 Instrumentation and Tracing Technology (ITT)¶
Intel® VTune Profiler 的 ITT API 是一個有用的工具,可以標註您的工作負載區域,以便追蹤、分析並以更精細的粒度(OP/函數/子函數粒度)視覺化您的標註。透過以 PyTorch 模型 OP 的粒度進行標註,Intel® VTune Profiler 的 ITT 能夠進行 OP 等級的效能分析。Intel® VTune Profiler 的 ITT 已整合到 PyTorch Autograd Profiler 中。1
此功能必須透過 with torch.autograd.profiler.emit_itt() 明確啟用。
搭配 Intel® Extension for PyTorch* 的 TorchServe¶
Intel® Extension for PyTorch* 是一個 Python 套件,透過針對 Intel 硬體進行最佳化來擴展 PyTorch,以獲得額外的效能提升。
Intel® Extension for PyTorch* 已經整合到 TorchServe 中,以實現開箱即用的效能提升。2 對於自訂處理程序腳本,我們建議加入 intel_extension_for_pytorch 套件。
此功能必須透過在 config.properties 中設定 ipex_enable=true 明確啟用。
在本節中,我們將展示 Back End Bound (後端瓶頸) 通常是未經最佳化或調整不佳的深度學習工作負載的主要瓶頸,並展示透過 Intel® Extension for PyTorch* 改善 Back End Bound 的最佳化技術,Back End Bound 有兩個子指標 - Memory Bound (記憶體瓶頸) 和 Core Bound (核心瓶頸)。更有效率的記憶體配置器、運算符融合 (operator fusion) 和記憶體佈局格式最佳化可以改善 Memory Bound。理想情況下,透過最佳化的運算符和更好的快取局部性,Memory Bound 可以改善到 Core Bound。關鍵的深度學習基本運算,例如卷積、矩陣乘法、點積,已經由 Intel® Extension for PyTorch* 和 oneDNN 庫進行了良好的最佳化,從而改善了 Core Bound。
利用進階啟動器配置:記憶體配置器¶
從效能的角度來看,記憶體配置器扮演著重要的角色。更有效率的記憶體使用可以減少不必要的記憶體配置或釋放的開銷,從而加快執行速度。在實際的深度學習工作負載中,尤其是在像 TorchServe 這樣的大型多核心系統或伺服器上運行的工作負載,TCMalloc 或 JeMalloc 通常可以比預設的 PyTorch 記憶體配置器 PTMalloc 獲得更好的記憶體使用率。
TCMalloc、JeMalloc、PTMalloc¶
TCMalloc 和 JeMalloc 都使用線程本地快取來減少線程同步的開銷,並分別透過使用 spinlock 和每個線程的 arena 來減少鎖定競爭。TCMalloc 和 JeMalloc 減少了不必要的記憶體配置和釋放的開銷。這兩種配置器都按大小對記憶體配置進行分類,以減少記憶體碎片化的開銷。
透過啟動器,使用者可以輕鬆地使用不同的記憶體配置器,方法是選擇三個啟動器旋鈕中的一個:–enable_tcmalloc (TCMalloc)、–enable_jemalloc (JeMalloc)、–use_default_allocator (PTMalloc)。
練習¶
讓我們分析 PTMalloc 與 JeMalloc 的效能。
我們將使用啟動器來指定記憶體配置器,並將工作負載綁定到第一個 socket 的物理核心,以避免任何 NUMA 複雜性 - 僅分析記憶體配置器的效果。
以下範例測量 ResNet50 的平均推論時間
import torch
import torchvision.models as models
import time
model = models.resnet50(pretrained=False)
model.eval()
batch_size = 32
data = torch.rand(batch_size, 3, 224, 224)
# warm up
for _ in range(100):
model(data)
# measure
# Intel® VTune Profiler's ITT context manager
with torch.autograd.profiler.emit_itt():
start = time.time()
for i in range(100):
# Intel® VTune Profiler's ITT to annotate each step
torch.profiler.itt.range_push('step_{}'.format(i))
model(data)
torch.profiler.itt.range_pop()
end = time.time()
print('Inference took {:.2f} ms in average'.format((end-start)/100*1000))
讓我們收集 level-1 TMA 指標。
Level-1 TMA 顯示 PTMalloc 和 JeMalloc 都受到後端瓶頸的限制。超過一半的執行時間被後端延遲。讓我們深入一層。
Level-2 TMA 顯示 Back End Bound 是由 Memory Bound 引起的。讓我們深入一層。
Memory Bound 下的大多數指標都用於識別從 L1 快取到主記憶體的記憶體層次結構中的哪個層級是瓶頸。在給定層級受到限制的熱點表示大多數資料都是從該快取或記憶體層級檢索的。最佳化應側重於將資料移到更靠近核心的位置。Level-3 TMA 顯示 PTMalloc 受 DRAM Bound 的限制。另一方面,JeMalloc 受 L1 Bound 的限制 - JeMalloc 將資料移到更靠近核心的位置,從而加快了執行速度。
讓我們看看 Intel® VTune Profiler ITT 追蹤。在範例腳本中,我們標註了推論迴圈的每個 step_x。
每個步驟都在時間軸圖表中追蹤。最後一個步驟 (step_99) 的模型推論持續時間從 304.308 毫秒減少到 261.843 毫秒。
搭配 TorchServe 的練習¶
讓我們分析搭配 TorchServe 的 PTMalloc 與 JeMalloc 的效能。
我們將使用 TorchServe apache-bench 基準測試,搭配 ResNet50 FP32、批次大小 32、並發 32、請求 8960。所有其他參數與 預設參數 相同。
與之前的練習一樣,我們將使用啟動器來指定記憶體配置器,並將工作負載綁定到第一個 socket 的物理核心。為此,使用者只需在 config.properties 中添加幾行。
PTMalloc
cpu_launcher_enable=true
cpu_launcher_args=--node_id 0 --use_default_allocator
JeMalloc
cpu_launcher_enable=true
cpu_launcher_args=--node_id 0 --enable_jemalloc
讓我們收集 level-1 TMA 指標。
讓我們深入一層。
讓我們使用 Intel® VTune Profiler ITT 來標註 TorchServe 推論範圍,以便在推論層級的粒度進行分析。由於 TorchServe 架構 由多個子元件組成,包括用於處理請求/回應的 Java 前端,以及用於在模型上執行實際推論的 Python 後端,因此使用 Intel® VTune Profiler ITT 來限制在推論層級的追蹤資料收集會很有幫助。
每個推論呼叫都會在時間軸圖表中追蹤。最後一個模型推論的持續時間從 561.688 毫秒減少到 251.287 毫秒 - 速度提升 2.2 倍。
可以展開時間軸圖表以查看 OP 層級的效能分析結果。aten::conv2d 的持續時間從 16.401 毫秒減少到 6.392 毫秒 - 速度提升 2.6 倍。
在本節中,我們展示了 JeMalloc 可以提供比預設的 PyTorch 記憶體配置器 PTMalloc 更好的效能,透過有效率的線程本地快取來改善後端瓶頸。
Intel® Extension for PyTorch*¶
三個主要的 Intel® Extension for PyTorch* 最佳化技術,運算符、圖形、執行階段,如下所示
Intel® Extension for PyTorch* 最佳化技術 |
||
---|---|---|
運算符 (Operator) |
圖形 (Graph) |
執行階段 (Runtime) |
|
|
|
運算符最佳化¶
優化的運算子和核心透過 PyTorch 調度機制註冊。這些運算子和核心藉由 Intel 硬體的原生向量化特性和矩陣計算特性來加速。在執行期間,Intel® Extension for PyTorch* 會攔截 ATen 運算子的調用,並以這些優化的運算子取代原始運算子。熱門的運算子,如 Convolution 和 Linear,已在 Intel® Extension for PyTorch* 中進行了最佳化。
練習¶
讓我們使用 Intel® Extension for PyTorch* 剖析最佳化的運算子。我們將比較程式碼有變更和沒有變更的情況。
如同先前的練習,我們會將工作負載綁定到第一個 socket 的物理核心。
import torch
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv = torch.nn.Conv2d(16, 33, 3, stride=2)
self.relu = torch.nn.ReLU()
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
#################### code changes ####################
import intel_extension_for_pytorch as ipex
model = ipex.optimize(model)
######################################################
print(model)
此模型由兩個操作組成——Conv2d 和 ReLU。透過印出模型物件,我們得到以下輸出。
讓我們收集 level-1 TMA 指標。
請注意 Back End Bound 從 68.9 降低到 38.5 – 速度提升 1.8 倍。
此外,讓我們使用 PyTorch Profiler 進行剖析。
請注意 CPU 時間從 851 us 降低到 310 us – 速度提升 2.7 倍。
圖形最佳化¶
強烈建議使用者利用 Intel® Extension for PyTorch* 與 TorchScript 來進行進一步的圖形最佳化。為了使用 TorchScript 進一步最佳化效能,Intel® Extension for PyTorch* 支援 oneDNN 融合常用的 FP32/BF16 運算子模式,例如 Conv2D+ReLU、Linear+ReLU 等,以減少運算子/核心調用開銷,並獲得更好的快取局部性。某些運算子融合允許維護暫時計算、資料類型轉換、資料佈局,以獲得更好的快取局部性。對於 INT8,Intel® Extension for PyTorch* 內建量化方法,可為流行的 DL 工作負載 (包括 CNN、NLP 和推薦模型) 提供良好的統計準確性。然後使用 oneDNN 融合支援來最佳化量化模型。
練習¶
讓我們使用 TorchScript 剖析 FP32 圖形最佳化。
如同先前的練習,我們會將工作負載綁定到第一個 socket 的物理核心。
import torch
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv = torch.nn.Conv2d(16, 33, 3, stride=2)
self.relu = torch.nn.ReLU()
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
#################### code changes ####################
import intel_extension_for_pytorch as ipex
model = ipex.optimize(model)
######################################################
# torchscript
with torch.no_grad():
model = torch.jit.trace(model, data)
model = torch.jit.freeze(model)
讓我們收集 level-1 TMA 指標。
請注意 Back End Bound 從 67.1 降低到 37.5 – 速度提升 1.8 倍。
此外,讓我們使用 PyTorch Profiler 進行剖析。
請注意,使用 Intel® Extension for PyTorch* 時,Conv + ReLU 運算子會被融合,並且 CPU 時間從 803 us 降低到 248 us – 速度提升 3.2 倍。oneDNN eltwise post-op 允許將一個基本運算與一個 elementwise 基本運算融合。這是最流行的融合種類之一:一個 eltwise (通常是一個啟動函數,例如 ReLU) 與前面的卷積或內積融合。請查看下一節中顯示的 oneDNN 冗長日誌。
Channels Last 記憶體格式¶
在模型上調用 *ipex.optimize* 時,Intel® Extension for PyTorch* 會自動將模型轉換為最佳化的記憶體格式,channels last。Channels last 是一種對 Intel 架構更友好的記憶體格式。與 PyTorch 預設的 channels first NCHW (批次、通道、高度、寬度) 記憶體格式相比,channels last NHWC (批次、高度、寬度、通道) 記憶體格式通常會以更好的快取局部性加速卷積神經網路。
需要注意的是,轉換記憶體格式的成本很高。因此,最好在部署之前轉換一次記憶體格式,並在部署期間將記憶體格式轉換保持在最低限度。由於資料透過模型的各層傳播,因此 channels last 記憶體格式會透過連續的 channels last 支援層 (例如,Conv2d -> ReLU -> Conv2d) 維持,並且僅在 channels last 不支援的層之間進行轉換。有關更多詳細資訊,請參閱 記憶體格式傳播。
練習¶
讓我們示範 channels last 最佳化。
import torch
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv = torch.nn.Conv2d(16, 33, 3, stride=2)
self.relu = torch.nn.ReLU()
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
import intel_extension_for_pytorch as ipex
############################### code changes ###############################
ipex.disable_auto_channels_last() # omit this line for channels_last (default)
############################################################################
model = ipex.optimize(model)
with torch.no_grad():
model = torch.jit.trace(model, data)
model = torch.jit.freeze(model)
我們將使用 oneDNN 冗長模式,這是一種有助於收集 oneDNN 圖形層級資訊 (例如運算子融合、執行 oneDNN 基本運算的 kernel 執行時間) 的工具。有關更多資訊,請參閱 oneDNN 文件。
以上是來自 channels first 的 oneDNN 冗長輸出。我們可以驗證權重和資料都有重新排序,然後進行計算,最後將輸出重新排序回去。
以上是來自 channels last 的 oneDNN 冗長輸出。我們可以驗證 channels last 記憶體格式避免了不必要的重新排序。
使用 Intel® Extension for PyTorch* 提升效能¶
以下總結了針對 ResNet50 和 BERT-base-uncased,使用 Intel® Extension for PyTorch* 的 TorchServe 的效能提升。
使用 TorchServe 進行練習¶
讓我們使用 TorchServe 剖析 Intel® Extension for PyTorch* 最佳化。
我們將使用 TorchServe apache-bench 基準測試,使用 ResNet50 FP32 TorchScript,批次大小 32,並發 32,請求數 8960。所有其他參數與 預設參數 相同。
如同先前的練習,我們將使用啟動器將工作負載綁定到第一個 socket 的物理核心。為此,使用者只需在 config.properties 中新增幾行即可
cpu_launcher_enable=true
cpu_launcher_args=--node_id 0
讓我們收集 level-1 TMA 指標。
Level-1 TMA 顯示兩者都受到後端的限制。如前所述,大多數未調整的深度學習工作負載都將受到 Back End Bound 的限制。請注意 Back End Bound 從 70.0 降低到 54.1。讓我們更深入一層。
如前所述,Back End Bound 有兩個子指標 – Memory Bound 和 Core Bound。Memory Bound 指示工作負載未經最佳化或未充分利用,並且理想情況下,可以透過最佳化 OPs 並改善快取局部性,將受記憶體限制的操作改善為受核心限制的操作。Level-2 TMA 顯示 Back End Bound 從 Memory Bound 改善為 Core Bound。讓我們更深入一層。
在模型服務框架 (如 TorchServe) 上擴展深度學習模型以進行生產需要高計算利用率。這需要透過預取資料並在執行單元需要執行 uOps 時重複使用快取中的資料來使資料可用。Level-3 TMA 顯示 Back End Memory Bound 從 DRAM Bound 改善為 Core Bound。
如同先前使用 TorchServe 進行的練習,讓我們使用 Intel® VTune Profiler ITT 來註釋 TorchServe 推論範圍,以在推論層級粒度進行剖析。
每個推論呼叫都會在時間軸圖中追蹤。最後一個推論呼叫的持續時間從 215.731 ms 減少到 95.634 ms - 速度提升 2.3 倍。
可以展開時間軸圖以查看運算子層級的剖析結果。請注意 Conv + ReLU 已被融合,並且持續時間從 6.393 ms + 1.731 ms 減少到 3.408 ms - 速度提升 2.4 倍。
結論¶
在本教學課程中,我們使用了由上而下的微架構分析 (TMA) 和 Intel® VTune™ Profiler 的儀器和追蹤技術 (ITT) 來證明
通常,未經最佳化或未經調整的深度學習工作負載的主要瓶頸是 Back End Bound,它有兩個子指標,Memory Bound 和 Core Bound。
Intel® Extension for PyTorch* 更有效率的記憶體配置器、運算子融合、記憶體佈局格式最佳化可改善 Memory Bound。
關鍵的深度學習基本運算 (例如卷積、矩陣乘法、點積等) 已透過 Intel® Extension for PyTorch* 和 oneDNN 程式庫進行了最佳化,從而改善了 Core Bound。
Intel® Extension for PyTorch* 已整合至 TorchServe,並提供簡易使用的 API。
結合 Intel® Extension for PyTorch* 的 TorchServe 在 ResNet50 上展現了 7.71 倍的吞吐量加速,在 BERT 上則有 2.20 倍的吞吐量加速。
致謝¶
我們要感謝 Ashok Emani (Intel) 和 Jiong Gong (Intel) 在本教學的許多階段提供的巨大指導和支持,以及詳盡的回饋和審閱。我們還要感謝 Hamid Shojanazeri (Meta) 和 Li Ning (AWS) 在程式碼審查和本教學中提供的寶貴意見。