Skip to content

Free-threaded Python 3.14t/3.15t; Python 3.10-3.15 and wheels as in BinPackage - #10

Merged
vthorsteinsson merged 14 commits into
masterfrom
free-threading
Oct 16, 2026
Merged

vthorsteinsson merged 14 commits into
masterfrom
free-threading

Conversation

@HaukurPall

@HaukurPall HaukurPall commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

What is the problem?

Icegrams didn't build on free-threaded Python. Its supported versions and wheels had also drifted from mideind/BinPackage:

  • it still supported Python 3.9;
  • CI didn't test Python 3.15;
  • it had no PyPy 7.3, PyPy 3.12 or Linux aarch64 wheels.

What is the solution?

  • Free-threaded builds: free-threaded CPython gets a regular extension, since it has no abi3. cffi 2.x keeps the GIL off.
  • Tests: a new test_threads.py has threads query one Ngrams and compares the results with serial ones: 8 threads over all queries on free-threaded Python, 2 threads with half the queries each when the GIL is on.
  • -O2 safeguard: trie.cpp is built with -O2 when the interpreter records no optimization level. Gentoo's Python records none, so trie.cpp ran at -O0, 2.6× slower.
  • Versions, the same as BinPackage:
    • Python 3.10+, with a cp310-abi3 wheel. Python 3.9 has been end of life since October 2025.
    • cffi>=2.0.0, declared in pyproject.toml only: setup.py keeps just what pyproject can't express (cffi_modules, zip_safe, the abi3 option).
    • The Free Threading classifier and the 3.15 classifier.
    • A CI matrix of 3.10–3.15, 3.14t, 3.15t, and PyPy 3.11 and 3.12. The free-threaded jobs also run every test in 8 threads at once.
  • Wheels, the same as BinPackage:
    • cibuildwheel is pinned.
    • New cp314t, cp315t, PyPy 7.3 (pp73), PyPy 3.12 and Linux aarch64 wheels.
    • Every wheel is checked to import, and to keep the GIL off on free-threaded Python.
    • A last step checks that every expected wheel was built.
  • close(): on CPython it no longer fails halfway with BufferError when a query in another thread still holds the memory map; the map is unmapped when that query is done. PyPy unmaps at close() regardless, so close() must still be called only when no other thread uses the instance.
  • CI: pushes are tested on master only (other branches through their pull requests, so jobs no longer run twice), and ruff runs once, in the 3.10 job.
  • Docs:
    • An Ngrams can be shared by threads. Call close() only when no other thread is using it.
    • With the GIL, use processes rather than threads for parallel queries. cffi releases the GIL around every C call, so 8 busy threads take 25 times as long as one.
    • The Prerequisites section lists the platforms that get wheels.
    • A 2.1.0 changelog entry, marked unreleased.

No locks were needed. The C++ trie only reads the memory map, and the one lazily computed value is computed the same way by any thread.

How was this tested?

  • CI: all jobs pass, including 3.14t and 3.15t serially and in 8 threads.
  • Wheels: a dry run of wheels.yml on the test tag 2.1.0-test1 built all 30 expected wheels (6 kinds × Linux x86_64 and aarch64, macOS x86_64 and arm64, Windows), and each passed its import and GIL test. The publish job was skipped. The Linux x86_64 build was also reproduced locally with the same cibuildwheel versions.
  • Locally: the suite passes on 3.10, 3.14, 3.14t, 3.15t and PyPy 3.12. setup.py builds the same sdist and wheel contents as before.
  • Thread test: caught a deliberately planted race in 5 out of 5 runs. A new test reproduces the close() failure on CPython without the fix.
  • Speed: trigram logprob on real text, 8 threads sharing one Ngrams on 3.14t, against 8 processes on the GIL build: 139k lookups/s each. One worker does 26k.

The pypi environment now only accepts tags of the form *.*.*, as in BinPackage.

Releasing this should be 2.1.0, since it drops Python 3.9; the version bump and the changelog date come with the release, possibly together with #11. Users on 3.9 keep 2.0.0.

🤖 Generated with Claude Code

HaukurPall and others added 6 commits October 15, 2026 20:49
The free-threaded build doesn't support the limited API, so it gets a
regular (non-abi3) extension. cffi 2.x marks the module as free-threading
safe, so the GIL stays off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eight threads query one Ngrams, including its lazily computed sentence
count, and compare the results with serial ones. A value published before
it is final, planted in that lazy computation, fails the test on 3.14t.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cibuildwheel builds cp314t wheels next to the abi3 wheel, and every wheel
is checked to import, keeping the GIL off on free-threaded Python. The
test matrix runs 3.14t, also with every test in eight threads at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gentoo's Python records no -O flag for extension builds, so a source build
outside Portage ran trie.cpp at -O0: 2.6x slower trigram lookups. Add -O2
unless the interpreter's flags or CFLAGS/CXXFLAGS already set a level, so
builds that use -O3 keep it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The abi3 wheel already covers 3.15; free-threaded 3.15 gets its own
cp315t wheel. The test matrix gains 3.15 and 3.15t.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HaukurPall and others added 2 commits October 15, 2026 21:22
Python 3.9 reached its end of life in October 2025; cffi 2.1 and
setup-python on Ubuntu 26.04 no longer support it. The abi3 wheel becomes
cp310-abi3. cffi 2.0 is the first version that runs free-threaded Python
without the GIL; on PyPy, whose cffi is built in, the requirement installs
only metadata. The test matrix is BinPackage's: CPython 3.10-3.15, 3.14t,
3.15t, PyPy 3.11 and 3.12. Adds the Free Threading classifier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cibuildwheel is pinned: 4.3.1 builds the abi3, cp314t, cp315t and PyPy 8.0
(pp311, pp312) wheels, and 4.2.1 builds pp311 for PyPy 7.3, whose users
would otherwise get source builds. ubuntu-26.04-arm builds the Linux
aarch64 wheels. A last step checks that every expected wheel was built
for every architecture, since cibuildwheel only fails when a selector
matches nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@HaukurPall HaukurPall changed the title Free-threaded Python 3.14t/3.15t, and Python 3.15 Free-threaded Python 3.14t/3.15t; Python 3.10-3.15 and wheels as in BinPackage Oct 15, 2026
Base automatically changed from ci-node24 to master October 16, 2026 14:18
vthorsteinsson and others added 6 commits October 16, 2026 14:21
pyproject.toml already declares the packages, package data and
dependencies; setup.py's install_requires was overridden ("install_requires
overwritten in pyproject.toml") and setup_requires is unused under PEP 517.
The cffi requirement now lives in pyproject.toml alone, as in
mideind/BinPackage. The sdist and wheel contents are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cffi releases the GIL around every C call, and a query makes several, so
with the GIL more threads only add hand-offs: the 8 threads took 21 s on
CPython 3.14 against 3.4 s on 3.14t, most of the suite's run time in the
GIL and PyPy jobs. It now takes about 3 s there; free-threaded Python
still runs 8 threads over all the queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On CPython, mmap.close() raises BufferError while any view of the map is
alive, and a running query holds such views: close() then failed halfway,
with the store's attributes already cleared. It now succeeds, and CPython
unmaps the file when the last view is released, so the running query
completes correctly. PyPy unmaps at close() regardless, so there a query
that is still running can crash: the docstring says so, and close() must
still be called only when no other thread is using the instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Prerequisites paragraph now lists the platforms that get wheels
(including the new Linux aarch64 ones) instead of the old ARMhf test.
The 2.1.0 entry is marked unreleased; the version bump and the date come
with the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pushes to other branches were tested twice, once for the push and once
for their pull request; they are now tested through the pull request
only. Ruff runs in the 3.10 job only, as in mideind/BinPackage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…queries

cffi releases the GIL around every C call and a query makes several, so
on Python with the GIL busy threads mostly hand the GIL to each other:
8 threads took 25 times as long as one thread for the same queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@vthorsteinsson vthorsteinsson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against mideind/BinPackage's free-threading setup; parity confirmed, wheels dry run (2.1.0-test1) green on all platforms. Thanks!

@vthorsteinsson
vthorsteinsson merged commit ed31fce into master Oct 16, 2026
10 checks passed
@HaukurPall
HaukurPall deleted the free-threading branch October 16, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants