8.3. 平行處理、資源管理和組態#

8.3.1. 平行處理#

一些 scikit-learn 估算器和工具使用多個 CPU 核心來平行處理耗時的操作。

根據估算器的類型,有時也根據建構子參數的值,這會透過以下方式完成:

  • 透過 joblib 進行高階平行處理。

  • 透過 OpenMP 進行低階平行處理,用於 C 或 Cython 程式碼。

  • 透過 BLAS 進行低階平行處理,NumPy 和 SciPy 使用它來處理陣列的通用操作。

估算器的 n_jobs 參數始終控制 joblib 管理的平行處理量(根據 joblib 後端,可能是行程或執行緒)。scikit-learn 自身 Cython 程式碼中 OpenMP 管理的執行緒層級平行處理,或是 NumPy 和 SciPy 運算中使用的 BLAS 和 LAPACK 程式庫,始終由環境變數或 threadpoolctl 控制,如下所述。請注意,某些估算器可以在其訓練和預測方法的不同時間點利用這三種平行處理方式。

我們將在以下小節中更詳細地描述這 3 種類型的平行處理。

8.3.1.1. 使用 joblib 進行高階平行處理#

當底層實作使用 joblib 時,可以透過 n_jobs 參數控制平行產生的工作者數量(執行緒或行程)。

注意

目前對於使用 n_jobs 在使用 joblib 的估算器中,平行化發生在何處(以及如何發生)的說明文件不足。請協助我們改進文件並處理 issue 14228

Joblib 能夠同時支援多行程和多執行緒。joblib 選擇產生執行緒還是行程取決於它正在使用的後端

scikit-learn 通常依賴 loky 後端,這是 joblib 的預設後端。Loky 是多行程後端。在執行多行程時,為了避免在每個行程中複製記憶體(對於大型資料集來說不合理),當資料大於 1MB 時,joblib 會建立一個所有行程都可以共享的 memmap

在某些特定情況下(當平行執行的程式碼釋放 GIL 時),scikit-learn 會指示 joblib 最好使用多執行緒後端。

作為使用者,您可以透過使用上下文管理器來控制 joblib 將使用的後端(無論 scikit-learn 建議什麼)

from joblib import parallel_backend

with parallel_backend('threading', n_jobs=2):
    # Your scikit-learn code here

請參閱 joblib 的文件以取得更多詳細資訊。

實際上,平行處理是否有助於改善執行時間取決於許多因素。通常最好進行實驗,而不是假設增加工作者數量始終是好事。在某些情況下,平行執行某些估算器或函數的多個副本可能對效能產生極大的損害(請參閱下面的超額訂閱)。

8.3.1.2. 使用 OpenMP 進行低階平行處理#

OpenMP 用於平行處理以 Cython 或 C 撰寫的程式碼,僅依賴多執行緒。預設情況下,使用 OpenMP 的實作將使用盡可能多的執行緒,也就是與邏輯核心一樣多的執行緒。

您可以透過以下方式控制使用的確切執行緒數量:

  • 透過 OMP_NUM_THREADS 環境變數,例如在執行 python 腳本時

    OMP_NUM_THREADS=4 python my_script.py
    
  • 或透過 這段文件說明的方式使用 threadpoolctl

8.3.1.3. 來自數值程式庫的平行 NumPy 和 SciPy 常式#

scikit-learn 非常依賴 NumPy 和 SciPy,它們在內部呼叫以程式庫(例如 MKL、OpenBLAS 或 BLIS)實作的多執行緒線性代數常式 (BLAS & LAPACK)。

您可以使用環境變數來控制每個程式庫的 BLAS 使用的確切執行緒數量,即

  • MKL_NUM_THREADS 設定 MKL 使用的執行緒數量,

  • OPENBLAS_NUM_THREADS 設定 OpenBLAS 使用的執行緒數量

  • BLIS_NUM_THREADS 設定 BLIS 使用的執行緒數量

請注意,BLAS 和 LAPACK 實作也可能受到 OMP_NUM_THREADS 的影響。若要檢查您的環境是否為這種情況,您可以檢查當在 bash 或 zsh 終端機中以不同的 OMP_NUM_THREADS 值執行下列命令時,這些程式庫實際使用的執行緒數量如何受到影響

OMP_NUM_THREADS=2 python -m threadpoolctl -i numpy scipy

注意

在撰寫本文時 (2022),在 pypi.org 上發佈的 NumPy 和 SciPy 套件(即透過 pip install 安裝的套件)以及在 conda-forge 頻道上發佈的套件(即透過 conda install --channel conda-forge 安裝的套件)都與 OpenBLAS 連結,而從 Anaconda.org 的 defaults conda 頻道發佈的 NumPy 和 SciPy 套件(即透過 conda install 安裝的套件)預設與 MKL 連結。

8.3.1.4. 超額訂閱:產生過多執行緒#

一般建議避免使用明顯多於機器上 CPU 數量的行程或執行緒。當程式同時執行過多執行緒時,就會發生超額訂閱。

假設您有一台配備 8 個 CPU 的機器。考慮一個情況,您正在對 GridSearchCV (使用 joblib 並行化) 執行 n_jobs=8 的操作,並在 HistGradientBoostingClassifier (使用 OpenMP 並行化) 上執行。每個 HistGradientBoostingClassifier 的實例會產生 8 個執行緒(因為您有 8 個 CPU)。這總共會產生 8 * 8 = 64 個執行緒,這會導致實體 CPU 資源的執行緒超額訂閱,進而造成排程的額外負擔。

當 MKL、OpenBLAS 或 BLIS 的平行化常式巢狀於 joblib 呼叫中時,也可能發生相同的超額訂閱情況。

joblib >= 0.14 開始,當使用 loky 後端 (這是預設值) 時,joblib 會告知其子程序限制它們可以使用的執行緒數量,以避免超額訂閱。實際上,joblib 使用的啟發式方法是告知程序使用 max_threads = n_cpus // n_jobs,透過它們對應的環境變數。回到我們上面的範例,由於 GridSearchCV 的 joblib 後端是 loky,因此每個程序只能使用 1 個執行緒,而不是 8 個,從而減輕超額訂閱的問題。

請注意:

  • 手動設定其中一個環境變數 (OMP_NUM_THREADSMKL_NUM_THREADSOPENBLAS_NUM_THREADSBLIS_NUM_THREADS) 將優先於 joblib 嘗試執行的操作。執行緒總數將為 n_jobs * <LIB>_NUM_THREADS。請注意,設定此限制也會影響主程序中的計算,主程序只會使用 <LIB>_NUM_THREADS。Joblib 公開了一個上下文管理器,以便更精細地控制其工作程序中的執行緒數量 (請參閱下方連結的 joblib 文件)。

  • 當 joblib 設定為使用 threading 後端時,在 joblib 管理的執行緒中呼叫平行原生程式庫時,沒有機制可以避免超額訂閱。

  • 所有明確依賴 Cython 程式碼中 OpenMP 的 scikit-learn 估算器,都會在內部使用 threadpoolctl 來自動調整 OpenMP 和可能巢狀的 BLAS 呼叫所使用的執行緒數量,以避免超額訂閱。

您可以在 joblib 文件中找到有關 joblib 減輕超額訂閱的其他詳細資訊。

您可以在 Thomas J. Fan 的這份文件中找到有關數值 Python 程式庫中平行化的其他詳細資訊。

8.3.2. 組態開關#

8.3.2.1. Python API#

sklearn.set_configsklearn.config_context 可用於變更控制平行化方面的組態參數。

8.3.2.2. 環境變數#

這些環境變數應在匯入 scikit-learn 之前設定。

8.3.2.2.1. SKLEARN_ASSUME_FINITE#

設定 sklearn.set_configassume_finite 引數的預設值。

8.3.2.2.2. SKLEARN_WORKING_MEMORY#

設定 sklearn.set_configworking_memory 引數的預設值。

8.3.2.2.3. SKLEARN_SEED#

為了實現可重現性,設定執行測試時全域隨機產生器的種子。

請注意,scikit-learn 測試預期以決定性的方式執行,並明確設定其自己的獨立 RNG 實例的種子,而不是依賴 numpy 或 Python 標準程式庫的 RNG 單例,以確保測試結果獨立於測試執行順序。但是,某些測試可能會忘記使用明確的種子設定,而此變數是控制上述單例初始狀態的一種方式。

8.3.2.2.4. SKLEARN_TESTS_GLOBAL_RANDOM_SEED#

控制依賴 global_random_seed` 夾具的測試中使用的隨機數字產生器的種子設定。

所有使用此夾具的測試都接受合約,即它們應針對 0 到 99 (包含) 的任何種子值確定性地通過。

在每晚的 CI 組建中,SKLEARN_TESTS_GLOBAL_RANDOM_SEED 環境變數會隨機抽取上述範圍中的值,並且所有固定測試都將針對該特定種子執行。這樣做的目的是確保,隨著時間的推移,我們的 CI 將使用不同的種子執行所有測試,同時保持單次執行完整測試套件的測試時間有限。這將檢查為使用此夾具編寫的測試的斷言是否不依賴於特定的種子值。

允許的種子值範圍限制為 [0, 99],因為通常不可能編寫適用於任何可能種子的測試,我們希望避免測試在 CI 上隨機失敗。

SKLEARN_TESTS_GLOBAL_RANDOM_SEED 的有效值

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="42": 使用固定的種子 42 執行測試

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="40-42": 使用 40 到 42 (包含) 之間的所有種子執行測試

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all": 使用 0 到 99 (包含) 之間的所有種子執行測試。這可能需要很長時間:僅適用於個別測試,不適用於完整的測試套件!

如果未設定變數,則會以決定性的方式使用 42 作為全域種子。這可確保預設情況下,scikit-learn 測試套件盡可能具有確定性,以避免干擾我們友好的協力廠商套件維護者。同樣地,此變數不應在提取要求的 CI 組態中設定,以確保我們友好的貢獻者不是第一個在與其自身 PR 變更無關的測試中遇到種子敏感度回歸的人。只有觀看每晚組建結果的 scikit-learn 維護者才會對此感到惱火。

當編寫使用此夾具的新測試函式時,請使用以下命令來確保它在您的本機機器上針對所有允許的種子確定性地通過

SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all" pytest -v -k test_your_test_name

8.3.2.2.5. SKLEARN_SKIP_NETWORK_TESTS#

當此環境變數設定為非零值時,會跳過需要網路存取的測試。如果未設定此環境變數,則會跳過網路測試。

8.3.2.2.6. SKLEARN_RUN_FLOAT32_TESTS#

當此環境變數設定為 '1' 時,使用 global_dtype 夾具的測試也會在 float32 資料上執行。如果未設定此環境變數,則測試只會在 float64 資料上執行。

8.3.2.2.7. SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES#

當此環境變數設定為非零值時,Cython 衍生詞 boundscheck 會設定為 True。這對於尋找區段錯誤很有用。

8.3.2.2.8. SKLEARN_BUILD_ENABLE_DEBUG_SYMBOLS#

當此環境變數設定為非零值時,偵錯符號將會包含在編譯的 C 擴充功能中。僅設定 POSIX 系統的偵錯符號。

8.3.2.2.9. SKLEARN_PAIRWISE_DIST_CHUNK_SIZE#

這會設定基礎 PairwiseDistancesReductions 實作要使用的區塊大小。預設值為 256,這已顯示在大多數機器上都足夠。

尋求最佳效能的使用者可能想要使用 2 的乘冪來調整此變數,以便針對其硬體取得最佳的平行化行為,尤其是在快取大小方面。

8.3.2.2.10. SKLEARN_WARNINGS_AS_ERRORS#

此環境變數用於將測試和文件組建中的警告轉換為錯誤。

某些 CI (持續整合) 組建會設定 SKLEARN_WARNINGS_AS_ERRORS=1,例如,以確保我們捕獲來自我們依賴項的棄用警告,並調整我們的程式碼。

若要在本機執行時使用與這些 CI 組建中相同的「將警告視為錯誤」設定,您可以設定 SKLEARN_WARNINGS_AS_ERRORS=1

預設情況下,不會將警告轉換為錯誤。如果 SKLEARN_WARNINGS_AS_ERRORS 未設定,或 SKLEARN_WARNINGS_AS_ERRORS=0,則會是這種情況。

這個環境變數使用特定的警告篩選器來忽略某些警告,因為有時警告來自第三方函式庫,我們對此無能為力。您可以在 sklearn/utils/_testing.py 中的 _get_warnings_filters_info_list 函式中查看警告篩選器。

請注意,對於文件建置,SKLEARN_WARNING_AS_ERRORS=1 會檢查文件建置,特別是執行範例時,是否產生任何警告。這與 -W sphinx-build 參數不同,後者會捕獲 rst 檔案中的語法警告。