1 ポイント 投稿者 GN⁺ 2024-03-12 | 1件のコメント | WhatsAppで共有
  • CPython PR #116338 は、free-threaded ビルドPYTHON_GIL=0 または -X gil=0 により GIL を無効化できるようにする変更を python:main にマージした
  • 実行時に GIL を再度有効化できる可能性を残すため、GIL 関連のデータ構造は通常どおり初期化し、無効化は起動時にフラグを設定して take_gil()drop_gil() が早期リターンするように処理する
  • 初期確認では、PYTHON_GIL=0 設定でスレッドを使わない一部のテストと小さなプログラムは正常に動作し、ごく基本的なスレッドプログラムは時々動作したが、テストスイート全体は test_asyncio で早々にクラッシュした
  • レビュー過程で PYTHON_GIL のテスト、ドキュメント化、-X gil オプション、sys.flags への反映が追加され、PYTHON_GIL=1GIL 有効化を強制するように設定処理も修正された
  • 後続作業は、互換性のない拡張をロードする際に GIL を再度有効化する問題と、GIL をデフォルトで無効化する問題に分けられ、この変更は Python 3.13 の free-threaded ビルドにおける GIL 制御のインターフェースを追加する

マージされた変更

  • CPython PR #116338 は gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 の変更を扱う
  • colesbury が 2024 年 3 月 11 日に python:main へマージした
  • 変更規模は 12 ファイル163 行追加1 行削除 と表示されている
  • 対象機能は通常ビルドではなく、free-threaded ビルドで GIL を無効化する実行オプションである

GIL 無効化の方式

  • free-threaded ビルドでは次の設定で GIL を無効化できる
    • PYTHON_GIL=0
    • -X gil=0
  • 実行時に GIL を再度有効化できるよう、すべての GIL 関連データ構造は通常どおり初期化される
  • 実際の無効化は起動時にフラグを設定する方式である
    • このフラグにより take_gil()drop_gil() は早期リターンする
  • レビュー中には、PYTHON_GIL=1 のとき enable_gil を正しく設定するコミットも追加された

テストと現在の制約

  • PYTHON_GIL=0 設定で一部のテストと小さなプログラムを確認した
    • スレッドを使わないテストと小さなプログラムは正常に動作することが確認された
    • ごく基本的なスレッドプログラムは時々動作した
  • テストスイート全体はすぐにクラッシュし、場所は test_asyncio と記録された
  • !buildbot nogil コマンドにより NoGIL 関連ビルダーテストが複数回予約された
    • x86-64 MacOS Intel ASAN NoGIL PR
    • x86-64 MacOS Intel NoGIL PR
    • ARM64 MacOS M1 Refleaks NoGIL PR
    • ARM64 MacOS M1 NoGIL PR
    • AMD64 Ubuntu NoGIL Refleaks PR
    • AMD64 Ubuntu NoGIL PR
    • AMD64 Windows Server 2022 NoGIL PR

レビュー中に追加された範囲

  • corona10Lib/test/test_cmd_line.py に環境変数テストを追加する価値があると提案した
  • その後、次のコミットが追加された
    • Add test for PYTHON_GIL in test_cmd_line
    • Set enable_gil properly when PYTHON_GIL=1
    • Don't add 'enable_gil' to test_embed in normal builds
  • colesbury は、環境変数を追加する時点でドキュメント化するのがよいと考えた
    • その根拠として、すでに --disable-gil configure フラグは文書化されている点を挙げた
    • ドキュメントには、free-threaded ビルドでのみ使用可能であること、0 は GIL 無効化を強制し、1 は GIL 有効化を強制し、Python 3.13 の新機能であることを含めるべきだと整理した
  • その後 Document PYTHON_GIL environment variable コミットが追加された

-X gil オプションの追加と最終マージ

  • Discord での議論の後、環境変数とあわせて使える -X オプションも追加することになった
  • PR タイトルは PYTHON_GIL=0 のみを扱う形から、PYTHON_GIL=0 or -X gil=0 まで含む形に変更された
  • 追加コミットには次の内容が含まれる
    • Add -X gil option, add to sys.flags, modify test to cover env var… and option
    • Fix link to -X gil
    • Fix PYTHON_GIL versionchanged line
    • Clarify test_flags in normal builds
  • ericsnowcurrentlyerlend-aaslandcorona10colesbury が変更を承認した
  • マージコミットは 2731913 で、マージ後に vstinner はこの変更について「興味深く、非常に恐ろしい」と反応した

後続作業

  • 後続の issue として 2 つの作業が分離された
    • #116322: 互換性のない拡張をロードする際に GIL を再度有効化する作業
    • #116329: GIL をデフォルトで無効化する作業
  • 現在の PR は GIL のデフォルト値変更ではなく、free-threaded ビルドでユーザーが環境変数または -X オプションにより GIL の状態を制御できるようにする変更である

1件のコメント

 
GN⁺ 2024-03-12
Hacker News のコメント
  • no-GIL の取り組みに興味がある人向けに、追加リンクを残しておく: [0], [1]
    [0] Multithreaded Python without the GIL
    https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
    [1] Github repo
    https://github.com/colesbury/nogil

  • 標準の Python をどこまで速くできるのか期待している。この問題を緩和しようとするツールが増えすぎて、Python の価値提案も揺さぶられている
    高速化ツールとしては Mojo、pytorch、triton、numba、taichi が思い浮かぶ。この問題を解こうとする試みがあまりに多く、前にどれかを使ってみようとしたときは選択肢が多すぎて圧倒された。結局 taichi を選んだが、かなり面白く使いやすかった一方で、適用範囲はやや限定的だった

  • https://peps.python.org/pep-0703/ で説明されている biased reference counting の方式が、なぜ単一スレッド親和性だけを持たせ、他のスレッドからアクセスするとアトミックなインクリメント/デクリメントを要求するのか気になる
    他の実装、たとえば biased reference counting を実装した複数の Rust クレートでは、新しいスレッドへ移すときだけアトミックに増やし、そのスレッドは再び 0 に達するまで非アトミックなインクリメント/デクリメントを行い、最後にアトミックなデクリメントを行う方式を見たことがある。既存システムに付け足す形なので単一の PyObject があり、新しいスレッドローカルなオブジェクトを指すように置き換えられないからなのか気になる

    • 将来的に CPython で所有権の移転を実装することもできるが、少し厄介だ
      Rust では所有権移転のための「move」が言語の一部だが、C や Python にはそれに対応する概念がないため、いつ所有権を移すべきか、どのスレッドが新しい所有者になるべきかを判断するのが難しい。ヒューリスティックは使える。たとえばオブジェクトを queue.SimpleQueue に入れるときに所有権を放棄または移転できるかもしれないが、その場合でもキューに入ったオブジェクトをどのスレッドが「get」するかを事前に知るのは難しい
      性能上の利得も小さいと思う。多くのオブジェクトは単一スレッドからしかアクセスされず、一部のオブジェクトは複数スレッドからアクセスされるが、あるスレッドからだけ排他的にアクセスされた後、別のスレッドからだけ排他的にアクセスされるオブジェクトはまれだ
  • 先に tranched bread のニュースを読んだのに、今度はこれまで? すごい時代だ
    Unladen Swallow プロジェクト [1] が尻すぼみになったときは少し残念だった。Python が再び中核的な最適化の道に戻ってくるのを見るのはうれしい
    [1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow

  • 5歳児に説明するように教えてほしい
    GIL が何なのかは概念的には分かる。けれど、この変更の影響は何なのか? 全体的な性能向上を期待しつつ、これからパッケージが壊れることになるのか?

    • 以前は GIL のせいで、実質的にマルチスレッドの Python はほとんど書かれていなかった。スレッドは主に、独立した入出力でブロックされ得る複数の作業を処理するために使われており、もちろん一般的で有用だが、CPU 中心の Python コードの性能には役立たなかった
      高負荷の CPU 作業でなくても、この変化は有用になり得る。最近は多くのコードが Python のネイティブな asyncio 言語機能で書かれている。これは NodeJS のように async/await で実行を譲りながら単一スレッドで動作し、単一スレッドだけでも秒間数千リクエスト程度のかなり良いスループットを出せる
      しかし大きな問題は、何らかの CPU 作業 を実行した瞬間に他のすべてのコルーチンをブロックしてしまい、さまざまな曖昧な問題が生じ、秒間リクエスト数が崩れることだ。たとえば、あるコルーチンでランダムな入出力タイムアウトが見えるが、実際の原因はまったく別のコルーチンがしばらく CPU を占有していたことかもしれない。なぜこうしたことが起きるのかを観測するのも非常に難しい。asyncio にはブロッキング作業をメインスレッド外へ逃がすのに役立つ asyncio.to_thread() 関数 [1] があるが、GIL のせいで CPU 中心の作業が他のコルーチンに干渉しないよう本当に分離することはできない
      [1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
    • あるパッケージが GIL に依存しているなら、GIL が有効になる。パッケージは壊れない
  • 興味がある人向けに言うと、GIL は Global Interpreter Lock の略だ

  • ここでのより大きな全体像をうまく整理した資料はあるだろうか?

  • ついに各種ツールの ベンチマーク が楽しみだ