プログラミング
Lucen: コメントプラグマでforループを並列化するPythonコンパイラ
Show HN: Lucen a Python compiler that parallelizes for-loops via comment pragmas (github.com)
要約
Lucenは、コメントプラグマによってPythonのforループを自動的に並列化するソース・トゥ・ソース・コンパイラです。既存のフレームワークと異なり、並列化の許可箇所を指示するだけで、安全かつ効果的と判断されたループのみを並列化します。誤った結果を生成しないことを保証し、並列化できないループは元のまま実行されるため、導入のリスクがありません。
全文翻訳
Lucen
Lucenは、コメントプラグマによって駆動される、通常のPythonのためのソース・トゥ・ソース・コンパイラおよび自動ループ並列化ツールです。既存のPython並列フレームワークとは異なり、実装方法ではなく並列化が許可される場所を記述するように求め、安全かつ価値があると証明できるループのみを並列化します。その唯一の保証は、ティアがなく、オプトアウトもないことです。Lucenは決して誤った結果を生成しません。
以前の通常のPython:
for i in range(len(records)):
scores[i] = score(records[i])
後の、依然として通常のPython:
# LUCEN START
for i in range(len(records)):
scores[i] = score(records[i])
# LUCEN END
12コアで3.8倍高速(CPython 3.14、CPUバウンドマップ、測定値)。フロートを含め、ビット単位で同一の出力。マルチプロセッシングコードなし。デバッグすべきピクル化エラー、プール、ロックもなし。
導入リスクなし:Lucenが安全だと証明できないループは、記述したシーケンシャルPythonと全く同じように実行され、その理由が構造化されたレポートで通知されます。プログラム開始時の1回の呼び出しで有効化できます。
import lucen
lucen.activate()
プラグマは通常のコメントです。Lucenが削除、アンインストール、非アクティブ化、または存在しないファイルは、それが存在しなかった場合と同じように実行されます。これをコメント不変性(Comment Invariant)と呼び、これは負荷を支えるものです。Lucenを採用する最悪のケースは、すでに持っているプログラムです。これは他のケースもカバーします。誰もリファクタリングしたくないコードベースに座っているループです。ワーカー関数なし、プールライフサイクルなし、シリアライゼーション配管なし、フレームワークの形状への書き換えなし。2つのコメントが既存のコードに問題なくスライドし、削除することで導入は元に戻せます。
目次:保証 | インストール | チュートリアル | 基本を超えて | エキスパートガイド | 仕組み | パフォーマンス | 制限事項 | 正直さの契約 | ドキュメント | 貢献 | ライセンス
3つの保証
誤った結果を生成しない。
チャンクはプライベートスラブに書き込み、結合時に非重複が監査され、チャンク順にコミットされます。辞書の挿入順序、フロート還元ビット、およびエラー中のコンテナ状態は、シーケンシャル実行とビット単位で同一です。書き込み競合が発生した場合、並列試行は破棄され、透過的にシーケンシャルに再実行されます。
破壊的でない。
Lucenが安全だと証明できないものはすべて、記述したシーケンシャルPythonとして実行され、その理由はstderrではなく、フォールバックレポートに記載されます。例外はそのタイプ、メッセージ、およびコンテナの正確なシーケンシャルプレフィックス状態を保持します。
サイレントに無意味でない。
収益性ゲート(静的事前スクリーニングと、測定中に実際の作業を行うランタイムプローブ)は、ディスパッチオーバーヘッドに負けるループの並列化を拒否し、それも報告します。観測できない並列化は、ここではバグであり、肩すかしではありません。
これらは願望ではありません。これらは構造によって強制され、クロスバージョンテストマトリックスによって検証されています:7つのインタープリタ x 8つのワークロード x 4つの実行パスウェイ、各セルはプレーンPythonとビット単位で同一です。BENCHMARK.mdを参照してください。
インストール
pip install lucen
Python 3.9以降。3.11以降では必須の依存関係はありません。3.9および3.10では、TOMLパーサーの依存関係(tomli)が自動的にインストールされます。
ソースから(オプションのRustアクセラレーションコア、Rustツールチェーンが必要):
git clone https://github.com/fcmv/lucen
cd lucen
pip install -e ".[dev]"
pytest
GILビルド(3.9から3.14)では、pipはネイティブコア(Rust、abi3、プラットフォームごとに1つのバイナリ)をインストールし、オーケストレーションのホットループ(書き込みセット監査および参照による還元フォールド)を実行します。フリー・スレッド・ビルドでは、abi3コアをロードできないため、pipは代わりにピュアPythonホイールをインストールします。これにより、インストールは常に成功します。Lucenはその後、完全にサポートされており、同一のテストスイートに合格するピュアPythonフォールバックを実行します。
サポートされているインタープリタ
インタープリタ | ステータス | ネイティブコア
-----------------|----------|------------
CPython 3.9から3.14 (GIL) | サポート済み、リリースごとにテスト済み | はい
CPython 3.13t / 3.14t (フリー・スレッド) | サポート済み、ピュアPythonフォールバックでテスト済み |
PyPy 3.11 | サポート済み、ピュアPythonフォールバックでテスト済み |
GraalPy | ベストエフォート、ピュアPythonフォールバックでテスト済み |
チュートリアル:ゼロから最初のスピードアップへ
このウォークスルーは、基本的なPython以上の知識は不要です。各ステップは実際のコマンドと実際の出力形状を示します。
ステップ1:インストール
pip install lucen
ステップ2:遅すぎるプログラムから始める
これをwork.pyとして保存します。CPU負荷の高い関数、プレーンPython、まだLucenはどこにもありません。20,000件のレコードをスコアリングします。
import math
def score(x):
acc = 0.0
for k in range(400):
acc += math.sin(x * 0.001 + k) * math.cos(k * 0.5)
return acc
def main():
records = list(range(20_000))
scores = [0.0] * len(records)
for i in range(len(records)):
scores[i] = score(records[i])
print(f"checksum: {sum(scores):.6f}")
if __name__ == "__main__":
main()
python work.py
# 純粋な計算で約1秒かかる
ステップ3:ループをマークする
ループの周りに2行のコメントを追加します。他は何も変更しません。
# LUCEN START
for i in range(len(records)):
scores[i] = score(records[i])
# LUCEN END
再度実行します。何も起こりません。それがポイントです。プラグマはコメントであり、何もアクティブ化していません。プログラムは以前と同じくらい安全です。
ステップ4:実行する
lucen runでファイルを実行します。これは、指定したスクリプト内のマークされたループを書き換え、その後実行します。したがって、マークしたばかりのループが並列で実行されます。
lucen run work.py
これは、直接起動するスクリプトのすべてのストーリーです。Lucenが代わりに大きなアプリケーションに埋め込まれている場合は、起動時にimport hookを一度アクティブ化します。アクティベーションはフックをインストールするため、マークされたループを含むモジュールがインポートされる前に実行される必要があります。そのため、このループはここでインポートされたモジュールにあります。
# app.py
import lucen
lucen.activate()
import work
work.main()
python app.py
マルチコアマシンでは、ループは現在約3〜4倍高速に実行され、チェックサムは桁まで同一です。近似的に同一ではありません。同一です。
どちらの場合も知っておくべき2つのこと:
work.pyのif __name__ == "__main__":ガードはWindowsおよびmacOSで重要です。Lucenはそこでプロセスワーカーを使用し、Pythonは各ワーカー内でエントリーモジュールを再インポートします。Lucenはガードの欠落を検出し、シーケンシャルにフォールバックし、追加するように促すメッセージを表示します。したがって、失敗モードは遅延ではなく、破損です。
activate()は冪等であり、プログラム開始時に一度呼び出しても安全です。
ステップ5:実行せずにLucenが決定したことを確認する
lucen explain work.py
work.py: マークされたブロックが1つあります
[GILインタープリタを想定]
ブロック1(行12)
+ 並列化バックエンド:PROCESS(THREADはフリー・スレッド・インタープリタが必要です)
(GILインタープリタを想定)
ランタイム依存(静的に報告されることはない):引数のピクル化可能性、カスタム呼び出し可能性の正当性、プール可用性 -- `lucen profile`を参照してください。
explainは静的で正直です。事実は事実として報告され、呼び出し時にのみ知ることができるものは決してはいかいいえとして報告されません。
ステップ6:拒否を理解する
ループ本体を前の要素に依存するように変更します。
# LUCEN START
for i in range(1, len(scores)):
scores[i] = scores[i - 1] + score(records[i])
# LUCEN END
lucen explain work.py
ブロック1(行12)
- シーケンシャル理由:クロス・イテレーション依存性 'scores[i - 1]'(単調チェーン);...
プログラムは引き続き実行され、正しい答えを生成します。Lucenは証明できないものを並列化することを拒否し、そのことを伝えます。これは設計通りに機能する保証番号2です。
ステップ7:「価値がない」ことを理解する
些細なループをマークします。
# LUCEN START
for i in range(len(xs)):
ys[i] = xs[i] * 2 + 1
# LUCEN END
ランタイム時にLucenは最初のチャンクをプローブし、イテレーションあたり約50ナノ秒を測定し、プロセスディスパッチのコストが節約されるよりも大きいと計算し、すべてをフルスピードでシーケンシャルに実行します。フォールバックレポートは次のように表示されます。
lucen fallback: PARALLEL_UNPROFITABLE (work.py:12): 約50 ns/iterationを測定し、ディスパッチオーバーヘッドに負けました;SEQUENTIALを実行しました (calibrate=falseは上書き、spec 5.17)
ゲートがあなたのケースにとって間違っていると信じる場合は、ブロックごとに上書きできます。
# LUCEN START
calibrate=false
ステップ8:フォールバックレポートをプログラムで読み取る
import lucen
lucen.activate()
import work
work.main()
for record in lucen.get_fallback_report():
print(record.error, record.file, record.line