NEP 38 — 使用 SIMD 最佳化指令提升效能#
- 作者:
Sayed Adel, Matti Picus, Ralf Gommers
- 狀態:
最終
- 類型:
標準
- 建立日期:
2019-11-25
- 決議:
摘要#
雖然編譯器在利用硬體特定的常式來最佳化程式碼方面越來越出色,但有時它們無法產生最佳結果。此外,我們希望能夠將二進位最佳化的 C 擴充模組從一部機器複製到另一部具有相同基礎架構 (x86、ARM 或 PowerPC) 但功能不同的機器,而無需重新編譯。
我們在 ufunc 機制中有一種機制可以建置由 CPU 功能名稱索引的替代迴圈。在匯入時(在 InitOperators
中),會從候選者中選擇與執行階段 CPU 資訊相符的迴圈函數。此 NEP 提出了一種機制,以在此基礎上針對更多功能和架構進行建置。提議的步驟如下:
建立一組定義完善、與架構無關的通用內建函數,以捕捉跨架構可用的功能。
將這些通用內建函數擷取到一組 C 巨集中,並使用這些巨集來建置程式碼路徑,以涵蓋從基準到該架構上可用的最大功能集的功能集。將這些作為數量有限的已編譯替代程式碼路徑提供。
在執行階段,探索哪些 CPU 功能可用,並相應地從可能的程式碼路徑中選擇。
動機與範疇#
傳統上,NumPy 依賴編譯器來產生專門針對目標架構的最佳程式碼。然而,如今很少有使用者在本機為其機器編譯 NumPy。大多數人使用二進位套件,這些套件必須為最低公分母 CPU 架構提供執行階段支援。因此,NumPy 無法利用其 CPU 處理器的更進階功能,因為這些功能可能並非在所有使用者的系統上都可用。
傳統上,CPU 功能是透過內建函數公開的,這些內建函數是編譯器特定的指令,直接對應到組合語言指令。最近,有人討論新增更多內建函數的有效性(例如,gh-11113 針對浮點數的 AVX 最佳化)。過去,NumPy 中新增了針對快速 avx512 常式的架構特定程式碼,用於各種 ufunc 中,並使用上述機制來選擇最適合該架構的迴圈。然而,該程式碼不通用,也無法推廣到其他架構。
最近,OpenCV 轉向使用硬體抽象層 (HAL) 中的通用內建函數,這為常見的共用單指令多資料 (SIMD) 建構提供了良好的抽象化。此 NEP 針對 NumPy 提出了類似的機制。使用此機制分為三個階段:
程式碼中為抽象內建函數提供基礎架構。ufunc 機制將使用這些抽象內建函數集進行擴充,以便將單個 ufunc 表示為一組迴圈,從最小到最大可能可用的內建函數集。
在編譯時,編譯器巨集和 CPU 偵測用於將抽象內建函數轉換為具體內建函數呼叫。平台上不可用的任何內建函數,無論是因為 CPU 不支援它們(因此無法測試)還是因為抽象內建函數在平台上沒有平行的具體內建函數,都不會產生錯誤,而是不會產生相應的迴圈並將其新增到可能性集中。
在執行階段,CPU 偵測程式碼將進一步限制可用的迴圈集,並為 ufunc 選擇最佳迴圈。
目前的 NEP 僅建議將執行階段功能偵測和最佳迴圈選擇機制用於 ufunc。未來的 NEP 可能會針對提議的解決方案提出其他用途。
ufunc 機制已經能夠在執行階段為特定可用的 CPU 功能選擇最佳迴圈,目前用於 avx2
、fma
和 avx512f
迴圈(在產生的 __umath_generated.c
檔案中);通用內建函數將擴充產生的程式碼,以包含更多迴圈變體。
使用方式與影響#
最終使用者將能夠取得適用於其平台和編譯器的內建函數清單。或者,使用者可以指定將使用執行階段可用的哪些迴圈,或許可以透過環境變數來啟用基準測試不同迴圈的影響。對於不熟悉的最終使用者來說,應該不會有直接的影響,所有迴圈的結果都應該在一個小數字 (1-3?) ULP 內相同。另一方面,擁有更強大機器的使用者應該會注意到效能顯著提升。
二進位發行版本 - PyPI 上的 wheel 和 conda 套件#
此流程發行的二進位檔案將會更大,因為它們包含該架構的所有可能迴圈。某些封裝者可能更喜歡限制迴圈數量,以限制二進位檔案的大小,我們希望他們仍然支援廣泛的架構系列。請注意,Intel MKL 產品中已經存在此問題,其中二進位套件包含大量適用於各種 CPU 替代方案的替代共用物件 (DLL)。
原始碼建置#
請參閱下方的「詳細描述」。在封裝者知道目標機器的詳細資訊的原始碼建置中,理論上可以透過選擇僅編譯目標所需的迴圈,透過命令列引數產生更小的二進位檔案。
如何執行基準測試以評估效能優勢#
新增更多使用內建函數的程式碼會使程式碼更難維護。因此,只有在產生顯著的效能優勢時,才應新增此類程式碼。評估此效能優勢可能並非易事。為了協助實現這一點,此 NEP 的實作將新增一種方法,可以透過環境變數在執行階段選擇可以使用哪些指令集。(名稱待定)。此功能對於 CI 程式碼驗證至關重要。
診斷#
新的字典 __cpu_features__
將可供 python 使用。鍵是可用的功能,值是布林值,指示該功能是否可用。各種新的私有 C 函數將在內部用於查詢可用功能。這些可能會透過特定的 c 擴充模組公開以進行測試。
新增新的 CPU 架構特定最佳化的工作流程#
對於任何可能是 SIMD 向量化的候選程式碼,NumPy 將始終具有基準 C 實作。如果貢獻者想要為某些架構(通常是他們最感興趣的架構)新增 SIMD 支援,則此註解是關於如何執行此操作的教學課程的開始:numpy/numpy#13516
目前,NumPy 針對許多 ufunc 具有許多 avx512f
和 avx2
和 fma
SIMD 迴圈。這些可能是第一個要移植到通用內建函數的候選對象。預期新的實作可能會導致基準測試中的效能衰退,但不會增加二進位檔案的大小。如果效能衰退並非微不足道,我們可能會選擇保留該平台的 X86 特定程式碼,並針對其他平台使用通用內建函數程式碼。
任何使用內建函數實作 ufunc 的新 PR 都應預期使用通用內建函數。如果可以證明使用通用內建函數過於笨拙或效能不足,則也可以接受平台特定程式碼。在極少數情況下,可能會接受僅限單一平台的 PR,但必須在偏好使用通用內建函數的解決方案的框架內對其進行審查。
接受新迴圈的主觀標準是
正確性:即使在演算法的邊緣點,新程式碼的準確性也不得降低超過 1-3 個 ULP。
程式碼膨脹:原始碼大小和已編譯 wheel 的二進位檔案大小。
可維護性:程式碼的可讀性
效能:基準測試必須顯示顯著的效能提升
新增新的內建函數#
如果貢獻者想要使用平台特定的 SIMD 指令,但該指令尚不支援作為通用內建函數,則
應將其新增為所有平台的通用內建函數
如果它在其他平台上沒有等效的指令(例如,
AVX512
中的_mm512_mask_i32gather_ps
),則不應新增通用內建函數,而應改為編寫平台特定的ufunc
或簡短的輔助函數。如果使用此類輔助函數,則必須使用功能巨集將其包裝起來,並預設使用合理的非內建函數後備方案。
我們預期 (2) 會是例外情況。貢獻者和維護者應考慮與使用最佳可用通用內建函數實作相比,該單一平台內建函數是否值得。
其他專案的重複使用#
如果 SciPy 或 Astropy 等也建置 ufunc 的其他程式庫可以使用通用內建函數,那就太好了,但這並非此 NEP 首次實作的明確目標。
向後相容性#
應該不會對向後相容性產生影響。
詳細描述#
CPU 特定的內建函數會對應到通用內建函數,這些通用內建函數對於所有 x86 SIMD 變體、ARM SIMD 變體等都是相似的。例如,NumPy 通用內建函數 npyv_load_u32
對應到
適用於 ARM 型 NEON 的
vld1q_u32
適用於 x86 型 AVX2 的
_mm256_loadu_si256
適用於 x86 型 AVX-512 的
_mm512_loadu_si512
任何編寫 SIMD 迴圈的人都將使用 npyv_load_u32
巨集,而不是架構特定的內建函數。程式碼還提供用於編譯和執行階段的防護巨集,以便可以選擇適當的迴圈。
runtests.py
和 setup.py
提供兩個新的建置選項:--cpu-baseline
和 --cpu-dispatch
。--cpu-baseline
定義了編譯所需的絕對最低功能。例如,在 x86_64
上,這預設為 SSE3
。如果編譯器支援,則將啟用最低功能。--cpu-dispatch
設定了可以偵測到並用作調度需求集的其他內建函數集。例如,在 x86_64
上,這預設為 [SSSE3, SSE41, POPCNT, SSE42, AVX, F16C, XOP, FMA4, FMA3, AVX2, AVX512F, AVX512CD, AVX512_KNL, AVX512_KNM, AVX512_SKX, AVX512_CLX, AVX512_CNL, AVX512_ICL]
。這些功能都對應到 c 語言層級的布林陣列 npy__cpu_have
,而 c 語言層級的便利函數 npy_cpu_have(int feature_id)
查詢此陣列,並且結果會儲存在執行階段的 __cpu_features__
中。
匯入 ufunc 時,可用的已編譯迴圈的所需功能會與偵測到的功能相符。最佳匹配的迴圈會被標記為由 ufunc 呼叫。
實作#
目前的 PR
編譯時和執行階段程式碼基礎架構由第一個 PR 提供。第二個新增了基礎架構在迴圈中使用的示範。一旦 NEP 獲得批准,就需要更多工作來使用 NEP 提供的機制編寫迴圈。
替代方案#
gh-13516 中提出的一種替代方案是手動為每個 CPU 架構單獨實作迴圈,而不嘗試抽象化 SIMD 內建函數中的常見模式(例如,具有 loops.avx512.c.src、loops.avx2.c.src、loops.sse.c.src、loops.vsx.c.src、loops.neon.c.src 等)。這更類似於 PIXMAX 的做法。儘管如此,這裡還是有很多重複,而且手動程式碼重複需要一位擁護者,他將專注於實作和維護該平台的迴圈程式碼。
討論#
大多數討論發生在接受此 NEP 的 PR gh-15228 上。郵件清單上的討論提到了 VOLK,已將其新增到相關工作章節中。可維護性問題也在郵件清單和 gh-15228 中提出,並按如下方式解決
參考文獻與註腳#
著作權#
本文檔已置於公有領域。[1]