Skip to content

Stop shipping free-threaded (cp314t) wheels - #155

Merged
antonio-leblanc merged 1 commit into
forefireAPI:masterfrom
HugoFara:ci/no-freethreaded-wheels
Aug 12, 2026
Merged

Stop shipping free-threaded (cp314t) wheels#155
antonio-leblanc merged 1 commit into
forefireAPI:masterfrom
HugoFara:ci/no-freethreaded-wheels

Conversation

@HugoFara

@HugoFaraHugoFara commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

v2.5.0 + CPython 3.14 shipped free-threaded wheels. It should not have, and this stops it until the architecture is changed.

forefire-2.5.0-cp314-cp314t-manylinux_2_27_x86_64...whl
forefire-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl

cibuildwheel built cp314t because nothing in skip excluded it. My oversight in #151: I assumed free-threaded builds were opt-in.

Why that is a problem

A free-threaded wheel tells users the extension is safe without the GIL. _pyforefire.cpp uses a plain PYBIND11_MODULE with no py::mod_gil_not_used(), so CPython re-enables the GIL when it is imported. The wheel advertises free-threading and then silently switches it off. Users get a slower interpreter and a promise that was never kept.

The core could not honour that promise regardless. Searching src/ for std::mutex, std::atomic, lock_guard, pthread_mutex or OpenMP critical sections returns zero matches across roughly 33,000 lines. Concretely:

Shared stateWhereProblem without the GIL
SimulationParameters::instanceSimulationParameters.cpp:28if (instance == 0) instance = new SimulationParameters; Two threads can both allocate, then diverge with different parameter objects.
ForeFireAtom::instanceNRCountForeFireAtom.h:106Every object's ID comes from instanceNRCount++, non-atomic. Concurrent construction races and can duplicate IDs.
FireDomain::propModelsTableFireDomain.cpp:58,697,859A process-wide array of 50 slots, scanned for a free index and written at runtime. Two domains compete for the same slots.
FireNode::nmlScheme, smoothing, relax, minSpeed, minFrontDepthFireNode.cpp:22-30Per-class mutable settings shared by every node in the process.

None of this is a bug today. The binding never calls gil_scoped_release, so the GIL serialises every entry point, and ForeFire is a single-simulation-per-process design. The GIL is exactly what makes it safe, which is why a wheel built to run without one is the wrong thing to publish.

Changes

  • cp314t-* added to [tool.cibuildwheel] skip, with a comment recording why, so it is not removed casually.
  • Programming Language :: Python :: 3.14 added to the classifiers. The regular cp314 wheels do build and pass CI, and the classifier list stopped at 3.13, so this closes the gap raised in Add pip-installable wheels for Linux and macOS #151.

Regular CPython 3.14 wheels are unaffected and keep shipping.

If free-threading is wanted later

It is a real piece of work, not a flag: give the singleton a thread-safe initialiser, make the ID counter atomic, move the model table and the FireNode settings off class-wide storage, then audit FireDomain and DataBroker for shared mutable state, and only then declare py::mod_gil_not_used(). Worth its own issue if anyone wants it.


This pull request, including its code changes and this description, was generated by Claude Opus 5.

v2.5.0 published cp314t wheels. Those advertise free-threading, but
_pyforefire declares no py::mod_gil_not_used(), so importing it makes the
interpreter re-enable the GIL: the wheel promises something it then turns
off.
The core could not honour the promise anyway. There is not one mutex,
atomic or lock guard in src/. SimulationParameters::GetInstance() is an
unguarded lazy singleton, ForeFireAtom::instanceNRCount hands out object
IDs through a non-atomic ++, FireDomain::propModelsTable is a process-wide
array of 50 slots mutated at runtime, and FireNode keeps its scheme and
smoothing settings in mutable statics. The binding never releases the GIL,
which is what keeps all of this safe today.
Also adds the 3.14 classifier, since non-free-threaded cp314 wheels do
build and pass CI.
@filippi

filippi commented Aug 11, 2026 via email

Copy link
Copy Markdown
Contributor

@HugoFara

Copy link
Copy Markdown
CollaboratorAuthor

Thanks for the explanation @filippi

I am digging into this rabbit hole, as I am trying to unlock free-threaded processes, but there is more changes than expected. For now the small workaround of this PR is a clean way to fix a working Python version, before I touch the core engine and risk regressions.

@HugoFara

Copy link
Copy Markdown
CollaboratorAuthor

Update: my free-threading branch fixes the issue but its a big change. I had to move Command, SimulationParameters, FireDomain, etc...

It works end-to-end, but I had to change 35 files. Let me know if you prefer to merge this PR, or if you prefer to close this one and I open a new PR for the new feature.

@filippi

filippi commented Aug 12, 2026 via email

Copy link
Copy Markdown
Contributor

@HugoFara

Copy link
Copy Markdown
CollaboratorAuthor

Hi @filippi

To clarify, this PR is the 1-line change that prevents building a wheel on Python 3.14. It is a temporary solution to avoid falsely advertising that CPython 3.14 does not work. It is safe to merge, as far as I can tell.

The refactor I am referring to is the proper adaptation of the codebase, with risk of regressions. I am currently setting up Méso-NH and testing the binding, and I would submit a PR only if I can make sure everything works. I only want to count how much change the codebase would need for adaptation.

If you are okay with the idea, I also want to set up unit tests, so that we make sure the interface of ForeFire stays the same, before core changes. That would be an independent PR.

@filippi

filippi commented Aug 12, 2026 via email

Copy link
Copy Markdown
Contributor

@HugoFara

Copy link
Copy Markdown
CollaboratorAuthor

Thanks for the quick answer, especially during your holidays. @antonio-leblanc available to do the merge?

@antonio-leblanc
antonio-leblanc merged commit 788f546 into forefireAPI:masterAug 12, 2026
8 checks passed
antonio-leblanc pushed a commit that referenced this pull request Aug 13, 2026
The repository has seven tags and six GitHub releases, and no file that
tells a user what changed between them. `pip install forefire` makes that
worse: someone pinning a version has the release notes on GitHub and
nothing in the tree.
CHANGELOG.md reconstructs every release from the release notes and the
commit history. Claims that could be checked were checked against the
tags: listenHTTP and RothermelAndrews2018 are absent at v1.2 and present
at v2.0, the geojson dump mode is absent at v1.1.10.
[Unreleased] lists only what is on this branch, #154 and #155. The dev
branch carries a longer list in its own copy, which supersedes this one
when dev is merged.
CONTRIBUTING.md now asks for an entry, so the file stays current instead
of being reconstructed again in a year. Its link to a testing page
pointed at a Read the Docs URL that has never existed; it points at
TESTING.md, which nothing in the repository linked to.
antonio-leblanc pushed a commit that referenced this pull request Aug 13, 2026
The repository has seven tags and six GitHub releases, and no file that
tells a user what changed between them. `pip install forefire` makes that
worse: someone pinning a version has the release notes on GitHub and
nothing in the tree.
CHANGELOG.md reconstructs every release from the release notes and the
commit history. Claims that could be checked were checked against the
tags: listenHTTP and RothermelAndrews2018 are absent at v1.2 and present
at v2.0, the geojson dump mode is absent at v1.1.10.
[Unreleased] lists only what is on this branch, #154 and #155. The dev
branch carries a longer list in its own copy, which supersedes this one
when dev is merged.
CONTRIBUTING.md now asks for an entry, so the file stays current instead
of being reconstructed again in a year. Its link to a testing page
pointed at a Read the Docs URL that has never existed; it points at
TESTING.md, which nothing in the repository linked to.
(cherry picked from commit cc7e1789c66432aec56673993c3b6e198e0525a0)
antonio-leblanc pushed a commit that referenced this pull request Aug 13, 2026
The CHANGELOG landed on master carries only #154 and #155 under
[Unreleased], because that is all master has. This adds the ten pull
requests merged into dev since: the unit suite, the moisture invariants,
the sanitizer job, the HTTP characterisation tests, the threading
reproduction, and the shared-state and double-free fixes.
Two things went stale on dev while that work landed, both of them ours.
The README's CMake option table stopped at six options; #156 and #180
added FOREFIRE_BUILD_TESTS, FOREFIRE_ENABLE_WARNINGS,
FOREFIRE_WARNINGS_AS_ERRORS and FOREFIRE_SANITIZE. And CONTRIBUTING told
contributors to run `cd tests && bash run.bash`, which does not reach the
C++ unit tests at all; ctest does.
Sign up for freeto 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.

3 participants

@HugoFara@filippi@antonio-leblanc