原生依賴噩夢:Python 中的 BLAS、LAPACK 與 OpenMP

對於絕大多數 Python 開發者來說,安裝像 NumPy 或 scikit-learn 這樣的函式庫,只需執行 pip install 即可。然而,在這些無縫安裝的表面之下,隱藏著一個複雜且往往脆弱的原生依賴網絡。PyData 技術棧的核心是 BLAS、LAPACK 和 OpenMP——這些函式庫為線性代數和並行處理提供了原始的計算能力。

由於這些函式庫是用低階語言(C、C++、Fortran 和 assembly)編寫的,並且針對特定硬體進行了高度優化,因此它們為 Python 的打包基礎設施帶來了一套獨特的挑戰。目前管理這些依賴關係的掙扎,揭示了特定語言的套件管理器與系統級軟體現實之間的深層結構性緊張關係。

理解核心組件

要理解打包衝突,首先必須了解這些函式庫究竟在做什麼:

  • BLAS (Basic Linear Algebra Subprograms): 這些是向量和矩陣運算的基礎例程。
  • LAPACK (Linear Algebra PACKage): 建立在 BLAS 之上,LAPACK 提供更複雜的例程來求解線性方程組、特徵值問題和奇異值分解。
  • OpenMP (Open Multi-Processing): 一個能夠實現共享記憶體並行編程的 API,允許程式碼有效利用多個 CPU 核心。

雖然存在參考實現(例如 Netlib 儲存庫中的實現),但這些函式庫的實際應用依賴於高度優化的版本,例如 Intel MKLOpenBLASApple AccelerateAMD AOCL。這些優化版本與參考實現相比,性能提升可達 10 倍到 100 倍,使其在科學計算和深度學習中不可或缺。

現狀:內置 (Vendoring) 與碎片化

由於 PyPI (the Python Package Index) 並非設計來處理系統級的原生依賴,許多主要專案都採取了「內置 (vendoring)」的方式——將依賴項的編譯二進位檔直接包含在 Python wheel 中。

目前,NumPy 和 SciPy 都內置了 OpenBLAS。然而,他們經常使用不同的版本或不同的構建配置(例如,NumPy 使用 64-bit ILP64,而 SciPy 使用 32-bit)。這造成了一個碎片化的環境,使得單個 Python 安裝可能包含多個相同函式庫的副本。

其他函式庫遵循不同的模式:

  • PyTorch 在大多數平台上靜態連結 MKL,並內置了自己的 OpenMP 實現 (libiomplibgomp)。
  • TensorFlow 使用 Eigen,這是一個僅標頭檔 (header-only) 的 C++ 函式庫,這簡化了分發,但仍依賴 OpenMP 進行並行處理。
  • scikit-learn 內置了 libomp/libgomp 並依賴 SciPy 的 BLAS/LAPACK 介面。

「鑽石依賴」與執行時錯誤

這種內置 (vendoring) 的方法導致了幾個關鍵的技術問題:

1. 超額訂閱 (Oversubscription) 與執行緒衝突

當多個函式庫(例如 NumPy 和 PyTorch)各自帶入自己的執行緒運行時 (threading runtime)(例如不同的 OpenMP 實現)時,它們可能會競爭 CPU 資源。這會導致「超額訂閱」,即創建了過多的執行緒,從而導致嚴重的性能下降。

2. Fork-Safety 死鎖

在 PyTorch 中,一個反覆出現的問題是在使用 Python 的 multiprocessing 模組時發生死鎖。這通常是由於 GCC 的 OpenMP 實現 (libgomp) 不是「fork-safe」的所導致的。如果套件管理器可以明確指定對 fork-safe OpenMP 實現(例如 LLVM 的 libomp)的需求,這些錯誤就可以在安裝層級避免。

3. ABI 不相容性

不同的 BLAS/LAPACK 實現可能會使用不同的應用程式二進位介面 (ABIs)。例如,有些使用 32-bit 整數進行索引,而有些則使用 64-bit。在單個環境中混合使用這些實現,可能會導致分段錯誤 (segmentation faults) 或極其錯誤的數值結果。

與系統套件管理器的比較

系統級管理器(例如 Debian 的 apt、Fedora 的 dnf 或 Conda)使用「虛擬依賴」或「互斥元套件 (mutex metapackages)」來處理這個問題。

例如,在 Conda 中,一個互斥套件可以確保一次只安裝一個 BLAS 實現。這允許使用者在所有已安裝的套件中,在全球範圍內切換 OpenBLAS 和 MKL,而不會破壞環境。PyPI 缺乏類似的機制,使得每個 wheel 成為一個孤立的孤島。

潛在的前進路徑

解決「原生依賴噩夢」需要超越目前的現狀。目前已提出了幾種潛在的緩解措施:

  • 獨立的原生 Wheel: 為 OpenBLAS 和 OpenMP 創建獨立的 PyPI wheels。雖然這可以減少重複,但這引發了誰來維護這些 wheels 以及如何管理破壞性變更的問題。

  • Demuxing 函式庫: 利用像 FlexiBLASlibblastrampoline 這樣的封裝函式庫,提供統一的 API/ABI,從而允許在執行時切換底層實現。

  • 結構性 PyPI 變更: 實施構建農場 (build farm) 或虛擬套件支持,使 PyPI 與系統套件管理器的功能對齊。

更廣泛的視角: 「小島問題」

與 BLAS 和 OpenMP 的掙扎反映了一個更大的架構問題。正如社群觀察者所指出的,許多特定語言的套件管理器是設計為彷彿他們存在於一個「小小的孤立島嶼」上,忽略了系統函式庫和跨語言 ABIs 的複雜性。

"Phase 1- Every language try to reimplements its own package manager... Phase 2 - The language specific package lanager is designed as if they live on a small isolated island. They do not try to integrate with other languages nor care about system libraries... Phase 3- the ecosystem grows up and the entire things blow up spectacularly in production."

直到 Python 的打包生態系統能夠有效地彌合高階語言與其所依賴的低階層系統函式庫之間的差距,PyData 技術棧將繼續依賴脆弱的內置 (vendoring) 權宜措施來維持其性能和穩定性。

Sources