Skip to content

build: ship one abi3 wheel for CPython 3.12+ (nanobind _ext, limited-API SWIG module) - #408

Draft
lgray wants to merge 8 commits into
scikit-hep:mainfrom
graphed-org:abi3
Draft

lgray wants to merge 8 commits into
scikit-hep:mainfrom
graphed-org:abi3

Conversation

@lgray

@lgray lgray commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

Cuts a release from 7 wheels per platform to 4 by building one CPython stable-ABI (abi3) wheel for 3.12+ instead of one per version: cp311, cp312-abi3 (tested on 3.12, 3.13, 3.14, 3.15), cp314t and cp315t. Across the 7 platform tags that is 28 wheels per release, and new CPython versions no longer need new wheels.

What changes

  • _ext moves from pybind11 to nanobind 3.1 (STABLE_ABI, its own NB_DOMAIN, dynamic libstdc++ on musllinux as before). pybind11 has no limited-API support ([BUG]: Compatibility issues between pybind11 and Py_LIMITED_API. pybind/pybind11#5905). The rewrite is mechanical; a second commit is only the pinned clang-format hook's output, since the new parameter types reflow the whole module body.
  • The SWIG module is built against the limited API from the top-level CMakeLists.txt, with no edits inside extern/: Py_LIMITED_API=0x030C0000, the .abi3 suffix (.pyd on Windows), and Python::SABIModule (python3.lib on Windows) in place of Python::Module. FastJet's pyinterface/FastJetPythonExtensions.hh calls PyUnicode_AsUTF8, which is not in the limited API; a configure-time copy of that header uses PyUnicode_AsUTF8AndSize(py_str, NULL) instead.
  • wheel.py-api = "cp312". 3.11 builds stay version-specific, as do free-threaded builds (there is no abi3t yet).
  • Limited-API SWIG output needs SWIG >= 4.3 (4.2 still calls malloc/free, which Python.h stops declaring under the 3.11+ limited API). An abi3 build with an older SWIG stops at configure with a message naming SWIG 4.3 and the -Cwheel.py-api= escape, which builds a version-specific wheel; the README says the same. The CMake floor moves to 3.18 (file(CONFIGURE), FindPython Development.Module).
  • CI: build_wheels runs [311, abi3, '314t', '315t'], with the abi3 entry building on cp312 and testing on cp313–cp315 (cibuildwheel reuses the wheel and runs abi3audit on it); test_wheels runs [abi3, '315t']; test_sdist takes the PyPI SWIG step from ci: install SWIG from PyPI in the check jobs #407.

Known differences

  • Arrays returned by _ext don't own their data: the buffer belongs to a nanobind capsule (base is a memoryview), so in-place ndarray.resize raises. Values, dtypes, shapes and writeability are unchanged.
  • _ext docstring signatures render in nanobind's style.
  • A conda-forge recipe needs nanobind >= 3.1 in place of pybind11 in its host requirements.
  • Importing on free-threaded Python re-enables the GIL, as today.

Verification (local, macOS arm64)

  • A harness calling interfacemulti and every to_numpy* method on randomized multi-event input gives identical values, dtypes, shapes and offsets to the pybind11 build: 177 calls, 0 mismatches on 3.11, the abi3 wheel on 3.12–3.15, 3.14t and 3.15t. Perturbing one value by 1e-12 relative makes it fail.
  • pytest on the same interpreters has the same failure set as the pybind11 build on each one; the only failures are 4 dask tests that also fail on main on 3.11–3.14 locally. 3.15 and 3.15t pass.
  • abi3audit --strict on the cp312-abi3 wheel: both extension modules are clean at baseline 3.12.
  • Timing on 3.12: per-call overhead (5k single-event cluster + to_numpy round trips) drops from 0.074 s to 0.034 s; a bulk workload (20k events, anti-kt R=0.4, inclusive jets plus soft-drop grooming) is about 1.5% slower.
  • SWIG 4.2.1 hits the configure error, the escape builds a cp312-cp312 wheel that clusters, and SWIG 4.3.0 builds the abi3 wheel.

This PR's CI is the first run of the Windows python3.lib link, Chocolatey's SWIG 4.4.1, universal2 and the x86_64 Linux legs.

lgray added 8 commits October 7, 2026 14:02
Array inputs go through numpy.ascontiguousarray before the ndarray cast so
lists, awkward arrays and other dtypes/byte orders are still accepted;
outputs are numpy arrays owning a new[] buffer via a capsule. A catch-all
exception translator (in a private NB_DOMAIN) maps non-std::exception throws
to RuntimeError. The cse property becomes read-only and the no-op setter is
removed. STABLE_ABI is inert until the wheel targets the limited API.
Output of the pinned clang-format pre-commit hook; no hand edits. The nb::handle lambda parameters let clang-format parse the NB_MODULE lambda chain, which reflows the whole body.
wheel.py-api = "cp312": 3.12+ builds produce one cp312-abi3 wheel; 3.11 and
free-threaded builds keep version-specific wheels. In abi3 builds the SWIG
module is compiled with Py_LIMITED_API=0x030C0000, named .abi3 and linked to
Python::SABIModule (python3.lib on Windows). Its header's PyUnicode_AsUTF8
call is swapped for the limited-API PyUnicode_AsUTF8AndSize through a
build-tree copy, leaving the submodule untouched. SWIG < 4.3 output does not
compile under the limited API, so configure stops with a message naming
SWIG >= 4.3 and the -Cwheel.py-api= escape.
The 3.12-3.15 matrix entries collapse into one abi3 entry whose CIBW_BUILD
selects cp312-cp315: cibuildwheel builds the cp312-abi3 wheel once, runs
abi3audit on it and tests it on every later selected CPython. The ci.yml
wheel tests use the same entry. test_sdist builds on Python 3.12, an abi3
build, so it takes SWIG >= 4.3 from PyPI as the ci.yml checks job does.
…elopment.Module

Both run unconditionally; scikit-build-core reads the floor from cmake_minimum_required, so a CMake 3.15-3.17 on PATH was accepted and failed at configure. Also narrows the SWIG floor comment to the 3.11+ limited API, the only one whose Python.h omits <stdlib.h>.
@henryiii

henryiii commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

FWIW, Limited ABI is the next target now that pre-compilation is in. I've got a Claude working on a draft now, actually. pybind/pybind11#6104

I think pybind11 will also be able to support abit3t, which nanobind cannot. But I'm not sure yet.

@henryiii

henryiii commented Oct 7, 2026

Copy link
Copy Markdown
Member

I'd recommend using clang-format, so the format then will be stable across a change like this, it's hard to see how much changes for the move due to indention changing.

@lgray

lgray commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

@henryiii thanks for the commentary - didn't know that pybind was heading for limited ABI. I'll just wait for that, much easier.

@henryiii

henryiii commented Oct 8, 2026

Copy link
Copy Markdown
Member

See pybind/pybind11#6198.

This branch has not been deployed

No deployments
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