PyCon JP 2024の開発Sprintにて、pytest-freethreadedの開発に参加してきました。
私はこのプロジェクトでOSSライブラリのFree Threading対応の検証に携わり、いくつかの課題を発見しました。短期間ながらとても良い体験でしたので、本記事で、開発の背景と検証ツールの使い方、実際の検証の方法について共有します。
Python 3.13におけるFree Threadingの導入
Free threadingはPEP703で提案され、Python 3.13からオプションで有効化できるようになった新機能です。Free threadingはGIL(Global Interpreter Lock)を無効にし、複数のスレッドが同時に実行されるようになります。Free Threadingに関する詳細は公式ドキュメントで確認できます。
しかし、GILに(暗黙的に)依存して動作していたプログラムは、Free threadingによってエラーをおこす可能性があり、対応状況の検証が必要です。py-free-threadingではScientific Pythonを中心にOSSライブラリのFree threadingの対応状況がトラッキングされています。
この記事では、Free threadingを有効にしたPython処理系のインストール方法、Free threadingの検証のためのpytestプラグインであるpytest-freethreadedのインストール方法と使い方、実際のOSSライブラリでの検証例を紹介します。
Free Threadingを有効にしたPython処理系のインストール手順
前述の py-free-threadingにインストール方法 で各OSごとのインストール方法が説明されています。
私はmacOSを使っているため、ここではmacOSでのインストール方法を紹介します。GUI、CUIどちらでもインストールできますので、順に説明します。
方法1:GUIインストーラ
https://www.python.org/ からPython 3.13のmacOS用のインストーラをダウンロードします。執筆時点の 2024/10/01時点では、3.13.0rc3が最新です。
ダウンロードしたインストーラ python-3.13.0rc3-macos11.pkg を実行し、下記のダイアログの「カスタマイズ」をクリックするとFree threading関係のオプションを含めたカスタムインストールのオプションが表示されます。
下記の "Free-threaded Python [experimental]" にチェックを入れ「インストール」ボタンを押すと、Free threading対応のPython処理系がインストールされます。
方法2:CUIインストーラ
GUI版と同様にインストーラをダウンロードした後、py-free-threadingのインストール方法に従います。
$ cat > ./choicechanges.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>attributeSetting</key>
<integer>1</integer>
<key>choiceAttribute</key>
<string>selected</string>
<key>choiceIdentifier</key>
<string>org.python.Python.PythonTFramework-3.13</string>
</dict>
</array>
</plist>
EOF
$ sudo installer -pkg ./python-3.13.0rc3-macos11.pkg \
-applyChoiceChangesXML ./choicechanges.plist \
-target /
オプションの設定はCPythonのGitHub Issueに書かれているもののようです。
動作確認
上記の方法でインストールした場合、 /usr/local/bin
に python
バイナリがインストールされます。 python3.13t
のようにファイル名の末尾に t
がついたものがFree threading対応のPythonインタプリタです。
下記のように -VV
オプションをつけて python3.13t
を実行すると、 experimental free-threading build
のようにFree threadingが有効化されていることが確認できます。
$ /usr/local/bin/python3.13t -VV
Python 3.13.0rc3 experimental free-threading build (v3.13.0rc3:fae84c70fbd, Oct 1 2024, 00:22:06) [Clang 15.0.0 (clang-1500.3.9.4)]
また、 sys._is_gil_enabled()
でGILが無効になっていることを確認できます。
$ /usr/local/bin/python3.13t -c "import sys; print(sys._is_gil_enabled())"
False
pytest-freethreaded
Sprintでは、リーダーの tonybaloney さんが企画したFree threading関係のプロジェクトに参加しました。
このプロジェクトは2つのタスクがあり、そのうちの一つがpytestのプラグイン pytest-freethreaded の開発と検証でした。このプラグインは、複数のスレッドで同時にテストケースを実行することで、Free Threading環境でのライブラリの動作を検証することができます。
tonybaloney さんを中心にプラグインはすごい速さでプロトタイピングされ、午後の早いうちに動作するようになっていました。
私がFree threadingのドキュメントを読んでサンプルコードを書いている間にプラグインができていた、という感じでした。tonybaloney さん、m-clare さんと相談の上、pytest-freethreadedを用いたOSSライブラリの検証を担当することになりました。
その経験をもとに、プラグインのインストール方法と使い方を説明します。
プラグインのインストール
まず、python3.13t
の仮想環境を作リます。
$ /usr/local/bin/python3.13t -m venv .venv
$ source .venv/bin/activate
このプラグインは執筆時点(2024/10/01)でまだPyPIで公開されていません。
そのため、GitHubリポジトリからインストールします。
$ pip install git+https://github.com/tonybaloney/pytest-freethreaded.git
(参考) もし、pytest-freethreaded自体の開発をする場合には、READMEにあるようにeditable installします
$ git clone git@github.com:tonybaloney/pytest-freethreaded.git
$ pip install flit
$ flit install -s
使い方
通常の pytest
と同様に pytest
コマンドを実行すると pytest-freethreaded
が各テストケースを並列に反復して実行します。
執筆時点でのデフォルトの並列数は10、反復数は200です(code)。
$ pytest -vvvv --log-level=DEBUG
反復数だけテストの実行時間も増えます。
実際の運用ではデフォルトはなにもしないようにしておいて(並列数を1、反復数も1)、気になるテストケースだけプラグインを有効にする、という使い方が現実的かもしれません (example)。
@pytest.mark.freethreaded(threads=20, iterations=200)
def test_foo(...):
...
pytest-freethreaded を用いたOSSのテストの例
ここでは、OSSのテストの例としてPillowのテストを並列に実行してみます。
こちらは m-clare さんがSprint中にSegmentation faultを発見されており、その再現を試みます。
Pillowのインストール
初めに、Pillowのリポジトリをクローンします。
$ git clone git@github.com:python-pillow/Pillow.git
$ cd Pillow
次に、Pillowをソースからビルドしてインストールします。
これは、各ライブラリでFree threading対応が日進月歩で進んでおり、最新版で検証をするためです。
執筆時点でのコミットは下記です。お手元で再現される際には問題が修正されている可能性があります。
https://github.com/python-pillow/Pillow/commit/84b167dfd53fe726a61953040ddbfa0e4e3e3359
ソースからのインストールは各ライブラリの説明に従ってください。Pillowの場合は下記です:
macOSの場合には、下記の依存ライブラリのインストールが必要でした。
$ brew install libjpeg libtiff little-cms2 openjpeg webp
$ brew install freetype harfbuzz fribidi
依存ライブラリをインストールした後は、通常通り pip でインストール可能です。
$ pip install .
pytestの実行
問題発生時に、詳細な情報が出力されることを期待して、以下のようにオプションを足してpytestを実行します。
$ pytest -vvvv --log-level=DEBUG
下記のように、Segmentation Faultが再現されました。
Tests/oss-fuzz/test_fuzzers.py::test_fuzz_fonts[Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf] PASSED [ 22%]
Tests/oss-fuzz/test_fuzzers.py::test_fuzz_fonts[Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf] Fatal Python error: Segmentation fault
Current thread 0x0000000177a8f000 (most recent call first):
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/Tests/oss-fuzz/test_fuzzers.py", line 64 in test_fuzz_fonts
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/_pytest/python.py", line 159 in pytest_pyfunc_call
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_callers.py", line 103 in _multicall
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_manager.py", line 120 in _hookexec
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_hooks.py", line 513 in __call__
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/_pytest/python.py", line 1627 in runtest
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pytest_freethreaded/plugin.py", line 63 in get_one_result
File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/concurrent/futures/thread.py", line 58 in run
File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/concurrent/futures/thread.py", line 92 in _worker
File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/threading.py", line 992 in run
File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/threading.py", line 1041 in _bootstrap_inner
File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/threading.py", line 1012 in _bootstrap
Thread 0x0000000176a83000 (most recent call first):
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/Tests/oss-fuzz/test_fuzzers.py", line 64 in test_fuzz_fonts
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/_pytest/python.py", line 159 in pytest_pyfunc_call
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_callers.py", line 103 in _multicall
File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_manager.py", line 120zsh: segmentation fault .venv/bin/pytest -vvvv --log-level=DEBUG
このプラグインは、現状すべてのテストケースを並列化して反復実行していますが、テストケースによっては並列実行を想定していない場合もありそうです。検証対象のライブラリにUpstream活動をする際には、ライブラリを修正すべきなのか、テストに問題があるのか、それとも、そもそもこのプラグインを使うべきではないテストなのか、などについて事前に考えてみるとよりよいコントリビューションができるかもしれません。
また、私はSprint中には scikit-image の検証をしていたのですが、まず scikit-image のインストールをする際のエラー解消に苦労しました。下記のIssueで報告していますので、もしご興味があれば再現に取り組んでみていただけると幸いです。
最後に
PyCon JP Sprintに参加するまでは「GILが無効化できるとマルチスレッディングが速くなってうれしいな」と能天気に考えていましたが、 pytest-freethreaded のプロジェクトに参加してみて、意外と影響の大きな変更であることを実感しました。
また、リーダーの tonybaloney さんをはじめ、メンバーの皆様がすごい勢いで開発、検証される姿に刺激を受けました。私にとって、とても良い開発体験でした。
Free Threadingの普及に備え、今後もpytest-freethreadedへの貢献やライブラリの対応状況の検証を続けていきたいと思います。
謝辞
PyCon JP 2024 Sprintの運営の皆様、リーダーの皆様、参加者の皆様に感謝します。素晴らしい機会をありがとうございました。