# 1.10。使用`@jit` 自動并行化
> 原文: [http://numba.pydata.org/numba-doc/latest/user/parallel.html](http://numba.pydata.org/numba-doc/latest/user/parallel.html)
為 [`jit()`](../reference/jit-compilation.html#numba.jit "numba.jit") 設置[并行](jit.html#parallel-jit-option)選項可啟用 Numba 轉換過程,該過程嘗試自動并行化并對函數(部分)執行其他優化。目前,此功能僅適用于 CPU。
用戶定義的函數內的一些操作,例如,向數組添加標量值已知具有并行語義。用戶程序可以包含許多這樣的操作,并且雖然每個操作可以單獨并行化,但是這種方法通常由于較差的高速緩存行為而具有低廉的性能。相反,通過自動并行化,Numba 嘗試在用戶程序中識別此類操作,并將相鄰的操作融合在一起,以形成一個或多個并行自動運行的內核。該過程完全自動化而無需修改用戶程序,這與 Numba 的 [`vectorize()`](../reference/jit-compilation.html#numba.vectorize "numba.vectorize") 或 [`guvectorize()`](../reference/jit-compilation.html#numba.guvectorize "numba.guvectorize") 機制形成對比,其中需要手動操作來創建并行內核。
## 1.10.1。支持的操作
在本節中,我們列出了所有具有并行語義且我們嘗試并行化的數組操作。
1. [案例研究支持的所有 numba 數組操作:數組表達式](../developer/rewrites.html#case-study-array-expressions),包括 Numpy 數組之間,數組和標量之間的常用算術函數,以及 Numpy ufuncs。它們通常被稱為<cite>元素</cite>或<cite>逐點</cite>數組操作:
> * 一元算子:`+` `-` `~`
> * 二元算子:`+` `-` `*` `/` `/?` `%` `|` `>>` `^` `<<` `&` `**` `//`
> * 比較運算符:`==` `!=` `<` `<=` `>` `>=`
> * [](../reference/numpysupported.html#supported-ufuncs)[nopython 模式](../glossary.html#term-nopython-mode)支持的 Numpy ufuncs。
> * 用戶定義 [`DUFunc`](../reference/jit-compilation.html#numba.DUFunc "numba.DUFunc") 至 [`vectorize()`](../reference/jit-compilation.html#numba.vectorize "numba.vectorize") 。
2. Numpy 減少函數`sum`,`prod`,`min`,`max`,`argmin`和`argmax`。此外,數組數學函數`mean`,`var`和`std`。
3. Numpy 數組創建函數`zeros`,`ones`,`arange`,`linspace`和幾個隨機函數(rand,randn,ranf,random_sample,sample,random,standard_normal,chisquare,weibull,power,geometric,exponential,poisson ,瑞利,正常,均勻,貝塔,二項式,f,伽瑪,對數正態,拉普拉斯,蘭丁,三角形??)。
4. Numpy `dot`函數在矩陣和向量之間,或兩個向量之間。在所有其他情況下,使用 Numba 的默認實現。
5. 當操作數具有匹配的尺寸和大小時,上述操作也支持多維數組。不支持具有混合維度或大小的陣列之間的 Numpy 廣播的完整語義,也不支持所選維度上的減少。
6. 數組賦值,其中目標是使用切片或布爾數組的數組選擇,并且指定的值是標量或其他選擇,其中切片范圍或位陣列被推斷為兼容。
7. `functools`的`reduce`運算符支持指定 1D Numpy 數組的并行減少,但初始值參數是必需的。
## 1.10.2。顯式并行循環
代碼轉換傳遞的另一個特性(當`parallel=True`時)支持顯式并行循環。可以使用 Numba 的`prange`而不是`range`來指定循環可以并行化。除了支持的減少之外,用戶需要確保循環沒有交叉迭代依賴性。
如果變量由二元函數/運算符使用其在循環體中的先前值更新,則自動推斷減少。對于`+=`和`*=`運算符,自動推斷減少的初始值。對于其他函數/運算符,reduce 變量應在進入`prange`循環之前保持標識值。對于標量和任意維度的數組,支持以這種方式減少。
下面的示例演示了一個帶有縮減的并行循環(`A`是一維 Numpy 數組):
```py
from numba import njit, prange
@njit(parallel=True)
def prange_test(A):
s = 0
# Without "parallel=True" in the jit-decorator
# the prange statement is equivalent to range
for i in prange(A.shape[0]):
s += A[i]
return s
```
以下示例演示了二維數組的產品縮減:
```py
from numba import njit, prange
import numpy as np
@njit(parallel=True)
def two_d_array_reduction_prod(n):
shp = (13, 17)
result1 = 2 * np.ones(shp, np.int_)
tmp = 2 * np.ones_like(result1)
for i in prange(n):
result1 *= tmp
return result1
```
## 1.10.3。示例
在本節中,我們舉例說明此功能如何幫助并行化 Logistic 回歸:
```py
@numba.jit(nopython=True, parallel=True)
def logistic_regression(Y, X, w, iterations):
for i in range(iterations):
w -= np.dot(((1.0 / (1.0 + np.exp(-Y * np.dot(X, w))) - 1.0) * Y), X)
return w
```
我們不會討論算法的細節,而是關注該程序如何使用自動并行化:
1. 輸入`Y`是大小為`N`的向量,`X`是`N x D`矩陣,`w`是大小為`D`的向量。
2. 函數體是一個迭代循環,它更新變量`w`。循環體由一系列向量和矩陣運算組成。
3. 內部`dot`操作產生一個大小為`N`的向量,然后是標量和大小為`N`的向量之間的一系列算術運算,或兩個大小為`N`的向量。
4. 外部`dot`產生一個大小為`D`的向量,然后在變量`w`上進行就地數組減法。
5. 通過自動并行化,將生成大小為`N`的數組的所有操作融合在一起,成為單個并行內核。這包括內部`dot`操作和后面的所有逐點數組操作。
6. 外部`dot`操作產生不同維度的結果數組,并且不與上述內核融合。
這里,利用并行硬件唯一需要的是為 [`jit()`](../reference/jit-compilation.html#numba.jit "numba.jit") 設置[并行](jit.html#parallel-jit-option)選項,而不對`logistic_regression`功能本身進行修改。如果我們使用 [`guvectorize()`](../reference/jit-compilation.html#numba.guvectorize "numba.guvectorize") 給出等價并行實現,則需要進行普遍的更改,重寫代碼以提取可并行化的內核計算,這既繁瑣又具有挑戰性。
## 1.10.4。診斷
注意
目前,并非所有并行變換和功能都可以通過代碼生成過程進行跟蹤。偶爾可能會丟失有關某些循環或變換的診斷信息。
[`jit()`](../reference/jit-compilation.html#numba.jit "numba.jit") 的[并行](jit.html#parallel-jit-option)選項可以生成有關自動并行化修飾代碼的變換的診斷信息。這個信息可以通過兩種方式訪問??,第一種是通過設置環境變量 [`NUMBA_PARALLEL_DIAGNOSTICS`](../reference/envvars.html#envvar-NUMBA_PARALLEL_DIAGNOSTICS) ,第二種是通過調用 [`parallel_diagnostics()`](../reference/jit-compilation.html#Dispatcher.parallel_diagnostics "Dispatcher.parallel_diagnostics") ,兩種方法都給出相同的信息并打印至`STDOUT`。診斷信息中的詳細程度由 1 到 4 之間的整數參數控制,其中 1 表示最小,4 表示最多。例如:
```py
@njit(parallel=True)
def test(x):
n = x.shape[0]
a = np.sin(x)
b = np.cos(a * a)
acc = 0
for i in prange(n - 2):
for j in prange(n - 1):
acc += b[i] + b[j + 1]
return acc
test(np.arange(10))
test.parallel_diagnostics(level=4)
```
生產:
```py
================================================================================
======= Parallel Accelerator Optimizing: Function test, example.py (4) =======
================================================================================
Parallel loop listing for Function test, example.py (4)
--------------------------------------|loop #ID
@njit(parallel=True) |
def test(x): |
n = x.shape[0] |
a = np.sin(x)---------------------| #0
b = np.cos(a * a)-----------------| #1
acc = 0 |
for i in prange(n - 2):-----------| #3
for j in prange(n - 1):-------| #2
acc += b[i] + b[j + 1] |
return acc |
--------------------------------- Fusing loops ---------------------------------
Attempting fusion of parallel loops (combines loops with similar properties)...
Trying to fuse loops #0 and #1:
- fusion succeeded: parallel for-loop #1 is fused into for-loop #0.
Trying to fuse loops #0 and #3:
- fusion failed: loop dimension mismatched in axis 0\. slice(0, x_size0.1, 1)
!= slice(0, $40.4, 1)
----------------------------- Before Optimization ------------------------------
Parallel region 0:
+--0 (parallel)
+--1 (parallel)
Parallel region 1:
+--3 (parallel)
+--2 (parallel)
--------------------------------------------------------------------------------
------------------------------ After Optimization ------------------------------
Parallel region 0:
+--0 (parallel, fused with loop(s): 1)
Parallel region 1:
+--3 (parallel)
+--2 (serial)
Parallel region 0 (loop #0) had 1 loop(s) fused.
Parallel region 1 (loop #3) had 0 loop(s) fused and 1 loop(s) serialized as part
of the larger parallel loop (#3).
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
---------------------------Loop invariant code motion---------------------------
Instruction hoisting:
loop #0:
Failed to hoist the following:
dependency: $arg_out_var.10 = getitem(value=x, index=$parfor__index_5.99)
dependency: $0.6.11 = getattr(value=$0.5, attr=sin)
dependency: $expr_out_var.9 = call $0.6.11($arg_out_var.10, func=$0.6.11, args=[Var($arg_out_var.10, example.py (7))], kws=(), vararg=None)
dependency: $arg_out_var.17 = $expr_out_var.9 * $expr_out_var.9
dependency: $0.10.20 = getattr(value=$0.9, attr=cos)
dependency: $expr_out_var.16 = call $0.10.20($arg_out_var.17, func=$0.10.20, args=[Var($arg_out_var.17, example.py (8))], kws=(), vararg=None)
loop #3:
Has the following hoisted:
$const58.3 = const(int, 1)
$58.4 = _n_23 - $const58.3
--------------------------------------------------------------------------------
```
為了幫助用戶不熟悉使用[并行](jit.html#parallel-jit-option)選項時進行的轉換,并幫助理解后續章節,提供了以下定義:
* ```py
Loop fusion
```
[循環融合](https://en.wikipedia.org/wiki/Loop_fission_and_fusion)是一種技術,其中具有等效邊界的循環可以在某些條件下組合以產生具有較大主體的循環(旨在改善數據局部性)。
* ```py
Loop serialization
```
當在另一個`prange`驅動的回路內存在任意數量的`prange`驅動回路時,會發生回路串行化。在這種情況下,所有`prange`循環的最外層并行執行,并且任何內部`prange`循環(嵌套或其他)被視為基于標準`range`的循環。實質上,嵌套并行性不會發生。
* ```py
Loop invariant code motion
```
[循環不變代碼運動](https://en.wikipedia.org/wiki/Loop-invariant_code_motion)是一種優化技術,它分析循環以查找可以移動到循環體外的語句而不改變執行循環的結果,然后這些語句被“提升”出循環保存重復計算。
* ```py
Allocation hoisting
```
分配提升是循環不變代碼運動的一種特殊情況,由于一些常見的 NumPy 分配方法的設計,這是可能的。這個技術的解釋最好由一個例子驅動:
```py
@njit(parallel=True)
def test(n):
for i in prange(n):
temp = np.zeros((50, 50)) # <--- Allocate a temporary array with np.zeros()
for j in range(50):
temp[j, j] = i
# ...do something with temp
```
在內部,這被轉換為大致如下:
```py
@njit(parallel=True)
def test(n):
for i in prange(n):
temp = np.empty((50, 50)) # <--- np.zeros() is rewritten as np.empty()
temp[:] = 0 # <--- and then a zero initialisation
for j in range(50):
temp[j, j] = i
# ...do something with temp
```
然后吊裝后:
```py
@njit(parallel=True)
def test(n):
temp = np.empty((50, 50)) # <--- allocation is hoisted as a loop invariant as `np.empty` is considered pure
for i in prange(n):
temp[:] = 0 # <--- this remains as assignment is a side effect
for j in range(50):
temp[j, j] = i
# ...do something with temp
```
可以看出`np.zeros`分配被分成一個分配和一個賦值,然后分配從`i`中的循環中提升,這產生了更高效的代碼,因為分配只發生一次。
### 1.10.4.1。并行診斷報告部分
該報告分為以下幾個部分:
1. ```py
Code annotation
```
這是第一部分,包含帶有循環的源代碼,循環具有標識和枚舉的并行語義。源代碼右側的`loop #ID`列與已識別的并行循環對齊。從示例中,`#0`是`np.sin`,`#1`是`np.cos`,`#2`和`#3`是`prange()`:
```py
Parallel loop listing for Function test, example.py (4)
--------------------------------------|loop #ID
@njit(parallel=True) |
def test(x): |
n = x.shape[0] |
a = np.sin(x)---------------------| #0
b = np.cos(a * a)-----------------| #1
acc = 0 |
for i in prange(n - 2):-----------| #3
for j in prange(n - 1):-------| #2
acc += b[i] + b[j + 1] |
return acc |
```
值得注意的是,循環 ID 按它們被發現的順序枚舉,這不一定與源中存在的順序相同。此外,還應注意,并行變換使用靜態計數器進行循環 ID 索引。因此,由于使用相同的計數器進行對用戶不可見的內部優化/變換,循環 ID 索引可能不會從 0 開始。
2. ```py
Fusing loops
```
本節介紹在融合發現的循環時所做的嘗試,注意哪些成功哪些失敗。在未融合的情況下,給出了一個原因(例如,依賴于其他數據)。從示例:
```py
--------------------------------- Fusing loops ---------------------------------
Attempting fusion of parallel loops (combines loops with similar properties)...
Trying to fuse loops #0 and #1:
- fusion succeeded: parallel for-loop #1 is fused into for-loop #0.
Trying to fuse loops #0 and #3:
- fusion failed: loop dimension mismatched in axis 0\. slice(0, x_size0.1, 1)
!= slice(0, $40.4, 1)
```
可以看出,環`#0`和`#1`的融合被嘗試并且這成功(兩者都基于`x`的相同尺寸)。在`#0`和`#1`成功融合后,嘗試在`#0`(現在包括融合的`#1`環)和`#3`之間進行融合。這種融合失敗是因為存在環尺寸不匹配,`#0`是尺寸`x.shape`而`#3`是尺寸`x.shape[0] - 2`。
3. ```py
Before Optimization
```
本節顯示了在進行任何優化之前代碼中并行區域的結構,但是具有與其最終并行區域相關聯的循環(這是在優化輸出之前/之后直接進行比較)。如果存在不能融合的循環,則可能存在多個并行區域,在這種情況下,每個區域內的代碼將并行執行,但每個并行區域將順序運行。從示例:
```py
Parallel region 0:
+--0 (parallel)
+--1 (parallel)
Parallel region 1:
+--3 (parallel)
+--2 (parallel)
```
正如<cite>融合循環</cite>部分所提到的,代碼中必然存在兩個并行區域。第一個包含循環`#0`和`#1`,第二個包含`#3`和`#2`,所有循環都標記為`parallel`,因為尚未進行優化。
4. ```py
After Optimization
```
本節顯示優化發生后代碼中并行區域的結構。同樣,平行區域用它們相應的循環枚舉,但是記錄融合或序列化的這個時間循環并給出摘要。從示例:
```py
Parallel region 0:
+--0 (parallel, fused with loop(s): 1)
Parallel region 1:
+--3 (parallel)
+--2 (serial)
Parallel region 0 (loop #0) had 1 loop(s) fused.
Parallel region 1 (loop #3) had 0 loop(s) fused and 1 loop(s) serialized as part
of the larger parallel loop (#3).
```
可以注意到,并行區域 0 包含循環`#0`,并且如<cite>定影循環</cite>部分所示,循環`#1`融合到循環`#0`中。還可以注意到,并行區域 1 包含循環`#3`并且該循環`#2`(內部`prange()`)已被序列化以在循環體`#3`中執行。
5. ```py
Loop invariant code motion
```
此部分顯示優化發生后的每個循環:
* 未能提升的指示和失敗的原因(依賴/不純)。
* 懸掛的指示。
* 任何可能發生的分配吊裝。
從示例:
```py
Instruction hoisting:
loop #0:
Failed to hoist the following:
dependency: $arg_out_var.10 = getitem(value=x, index=$parfor__index_5.99)
dependency: $0.6.11 = getattr(value=$0.5, attr=sin)
dependency: $expr_out_var.9 = call $0.6.11($arg_out_var.10, func=$0.6.11, args=[Var($arg_out_var.10, example.py (7))], kws=(), vararg=None)
dependency: $arg_out_var.17 = $expr_out_var.9 * $expr_out_var.9
dependency: $0.10.20 = getattr(value=$0.9, attr=cos)
dependency: $expr_out_var.16 = call $0.10.20($arg_out_var.17, func=$0.10.20, args=[Var($arg_out_var.17, example.py (8))], kws=(), vararg=None)
loop #3:
Has the following hoisted:
$const58.3 = const(int, 1)
$58.4 = _n_23 - $const58.3
```
首先要注意的是,此信息適用于高級用戶,因為它指的是正在轉換的函數的 [Numba IR](../glossary.html#term-numba-ir) 。例如,示例源中的表達式`a * a`部分轉換為 IR 中的表達式`$arg_out_var.17 = $expr_out_var.9 * $expr_out_var.9`,這顯然無法從`loop #0`中提升,因為它不是循環不變的!而在`loop #3`中,表達式`$const58.3 = const(int, 1)`來自源`b[j + 1]`,數字`1`顯然是一個常數,因此可以從循環中提升。
也可以看看
[并行](jit.html#parallel-jit-option),[并行常見問題解答](faq.html#parallel-faqs)
- 1. 用戶手冊
- 1.1。 Numba 的約 5 分鐘指南
- 1.2。概述
- 1.3。安裝
- 1.4。使用@jit 編譯 Python 代碼
- 1.5。使用@generated_jit 進行靈活的專業化
- 1.6。創建 Numpy 通用函數
- 1.7。用@jitclass 編譯 python 類
- 1.8。使用@cfunc 創建 C 回調
- 1.9。提前編譯代碼
- 1.10。使用@jit 自動并行化
- 1.11。使用@stencil裝飾器
- 1.12。從 JIT 代碼 中回調到 Python 解釋器
- 1.13。性能提示
- 1.14。線程層
- 1.15。故障排除和提示
- 1.16。常見問題
- 1.17。示例
- 1.18。會談和教程
- 2. 參考手冊
- 2.1。類型和簽名
- 2.2。即時編譯
- 2.3。提前編譯
- 2.4。公用事業
- 2.5。環境變量
- 2.6。支持的 Python 功能
- 2.7。支持的 NumPy 功能
- 2.8。與 Python 語義的偏差
- 2.9。浮點陷阱
- 2.10。 Python 2.7 壽命終止計劃
- 3. 用于 CUDA GPU 的 Numba
- 3.1。概述
- 3.2。編寫 CUDA 內核
- 3.3。內存管理
- 3.4。編寫設備功能
- 3.5。 CUDA Python 中支持的 Python 功能
- 3.6。支持的原子操作
- 3.7。隨機數生成
- 3.8。設備管理
- 3.10。示例
- 3.11。使用 CUDA 模擬器 調試 CUDA Python
- 3.12。 GPU 減少
- 3.13。 CUDA Ufuncs 和廣義 Ufuncs
- 3.14。共享 CUDA 內存
- 3.15。 CUDA 陣列接口
- 3.16。 CUDA 常見問題
- 4. CUDA Python 參考
- 4.1。 CUDA 主機 API
- 4.2。 CUDA 內核 API
- 4.3。內存管理
- 5. 用于 AMD ROC GPU 的 Numba
- 5.1。概述
- 5.2。編寫 HSA 內核
- 5.3。內存管理
- 5.4。編寫設備功能
- 5.5。支持的原子操作
- 5.6。代理商
- 5.7。 ROC Ufuncs 和廣義 Ufuncs
- 5.8。示例
- 6. 擴展 Numba
- 6.1。高級擴展 API
- 6.2。低級擴展 API
- 6.3。示例:間隔類型
- 7. 開發者手冊
- 7.1。貢獻給 Numba
- 7.2。 Numba 建筑
- 7.3。多態調度
- 7.4。關于發電機的注意事項
- 7.5。關于 Numba Runtime 的注意事項
- 7.6。使用 Numba Rewrite Pass 獲得樂趣和優化
- 7.7。實時變量分析
- 7.8。上市
- 7.9。模板注釋
- 7.10。關于自定義管道的注意事項
- 7.11。環境對象
- 7.12。哈希 的注意事項
- 7.13。 Numba 項目路線圖
- 8. Numba 增強建議
- 9. 術語表