ネイティブ依存関係の悪夢:PythonにおけるBLAS、LAPACK、およびOpenMP
Python開発者の大多数にとって、pip installを実行するだけでNumPyやscikit-learnのようなライブラリをインストールすることは非常に簡単です。しかし、これらのシームレスなインストールの裏側には、複雑でしばしば脆弱なネイティブ依存関係のネットワークが潜んでいます。PyDataスタックの中核を成すのは、線形代数と並列処理のための生の計算能力を提供するBLAS、LAPACK、およびOpenMPです。
これらのライブラリは低レベル言語(C、C++、Fortran、およびアセンブリ)で記述されており、特定のハードウェアに対して高度に最適化されているため、Pythonのパッケージング・インフラストラクチャにとって独自の課題をもたらします。これらの依存関係を管理しようとする現在の苦闘は、言語固有のパッケージマネージャーとシステムレベルのソフトウェアの現実との間の、より深い構造的な緊張関係を明らかにしています。
コアコンポーネントの理解
パッケージングの衝突を理解するには、まずこれらのライブラリが実際に何を行っているのかを理解する必要があります。
- BLAS (Basic Linear Algebra Subprograms): ベクトルおよび行列演算のための基本的なルーチンです。
- LAPACK (Linear Algebra PACKage): BLASの上に構築されており、線形方程式系の解法、固有値問題、および特異値分解のためのより複雑なルーチンを提供します。
- OpenMP (Open Multi-Processing): 共有メモリ並列プログラミングを可能にするAPIであり、コードが複数のCPUコアを効果的に利用できるようにします。
リファレンス実装(Netlibリポジトリにあるものなど)は存在しますが、これらのライブラリの現実世界での使用は、Intel MKL、OpenBLAS、Apple Accelerate、およびAMD AOCLのような高度に最適化されたバージョンに依存しています。これらの最適化されたバージョンは、リファレンス実装に対して10倍から100倍のパフォーマンス向上を提供することがあり、科学計算やディープラーニングにおいて不可欠なものとなっています。
現状:ベンディングと断片化
PyPI (Python Package Index) はシステムレベルのネイティブ依存関係を扱うように設計されていないため、多くの主要プロジェクトは「ベンディング(vendoring)」、つまりコンパイル済みの依存関係のバイナリをPython wheelの中に直接含めるという手法に頼っています。
現在、NumPyとSciPyはどちらもOpenBLASをベンディングしています。しかし、それらはしばしば異なるバージョンや異なるビルド構成(例:NumPyは64ビットILP64を使用し、SciPyは32ビットを使用する)を使用しています。これにより、単一のPythonインストール環境に同じライブラリの複数のコピーが含まれる可能性があるという、断片化された環境が作成されます。
他のライブラリは異なるパターンに従っています:
- PyTorchは(ほとんどのプラットフォームで)MKLを静的にリンクし、独自のOpenMP実装(
libiompまたはlibgomp)をベンディングしています。 - TensorFlowは、ヘッダーのみのC++ライブラリであるEigenを利用しており、これにより配布が簡素化されますが、並列処理のために依然としてOpenMPに依存しています。
- scikit-learnは
libomp/libgompをベンディングし、SciPyのBLAS/LAPACKインターフェースに依存しています。
「ダイヤモンド依存関係」とランタイムエラー
ベンディングという手法は、いくつかの重大な技術的課題を引き起こします。
1. オーバーサブスクリプションとスレッドの衝突
複数のライブラリ(例:NumPyとPyTorch)がそれぞれ独自のthreading runtime(例:異なるOpenMP実装)を持ち込む場合、それらはCPUリソースを奪い合うことになります。これは「オーバーサブスクリプション」を引き起こし、あまりにも多くのスレッドが作成されることで、深刻なパフォーマンス低下を招きます。
2. フォーク安全性(Fork-Safety)のデッドロック
PyTorchにおける繰り返される問題は、Pythonのmultiprocessingモジュールを使用する際にデッドロックが発生することです。これは多くの場合、GCCのOpenMP実装(libgomp)が「フォーク安全」ではないことが原因です。
もしパッケージマネージャーが、LLVMのlibompのようなフォーク安全なOpenMP実装を明示的に要求する仕様を指定できれば、これらのバグはインストールレベルで回避できる可能性があります。
3. ABIの不整合
異なるBLAS/LAPACK実装は、異なるApplication Binary Interface (ABI) を使用している場合があります。例えば、一部はインデックス作成に32ビット整数を使用し、他のものは64ビットを使用します。これらを単一の環境に混在させると、セグメンテーション違反(segmentation fault)や、著しく誤った数値計算結果を招くことがあります。
システムパッケージマネージャーとの比較
システムレベルのマネージャー(Debianのapt、Fedoraのdnf、またはConda)は、「仮想依存関係」または「ミューテックス・メタパッケージ」を使用することでこの問題に対取り組んでいます。
例えばCondaでは、ミューテックス・パッケージにより、一度にBLASの実現方法が一つだけインストールされることを保証します。これにより、ユーザーは環境を壊すことなく、すべてのインストール済みパッケージ全体で、OpenBLASとMKLの間でグローバルに切り替えることが可能です。PyPIは同様のメカニズムを持たず、各wheelは孤立したサイロとして存在しています。
潜在的な解決策への道
「ネイティブ依存関係の悪夢」を解決するには、現状維持を超えた動きが必要です。いくつかの潜在的な的な解決策が提案されています。
- スタンドアロン・ネイティブ・ホイール: OpenBLASやOpenMPのための個別のPyPI wheelを作成すること。これにより重複は減少しますが、誰がこれらのホイールを維持管理し、どのように破壊的変更が管理されるかという問題が生じます。
- Demuxingライブラリの利用: FlexiBLASやlibblastrampolineのようなラッパーライブラリを利用して、統一されたAPI/ABIを提供し、実行時に基礎となる実装を入れ替えられるようにすること。
- 構造的なPyPIの変更: ビルドファームや仮想パッケージのサポートを実装し、PyPIをシステムパッケージマネージャーに近い機能へと近づけること。
より広い視点: 「小島問題」
BLASとOpenMPに関する苦闘は、より大きなアーキテクチャ上の問題の兆候です。コミュニティの観察者によって指摘されているように、多くの言語固有のパッケージマネージャーは、あたかも「小さな孤立した島」に存在しているかのように設計されており、システムライブラリやクロス言語のABIの複雑さを無視しています。
「フェーズ1 - すべての言語が独自のパッケージマネージャーを再実装しようとする... フェーズ2 - 言語固有のパッケージマネージャーは、あたかも小さな孤立した島に住んでいるかのように設計される。他の言語との統合を試みず、システムライブラリにも関心を持たない... フェーズ3 - エコシステムが成長し、システム全体が本番環境で劇的に崩壊する。」
Pythonのパッケージング・エコシステムが、高レベル言語と、それが依存する低レベルシステムライブラリとの間の溝を埋めることが効果的にできなくなる限り、PyDataスタックは、そのパフォーマンスと安定性を維持するために、脆弱なベンディングによる回避策に頼り続けることになるでしょう。