Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for free-threaded Python #695

Merged
merged 17 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'windows-2022', 'macos-13']
python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-rc.1', 'pypy3.9-v7.3.16', 'pypy3.10-v7.3.17']
python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-rc.2', 'pypy3.9-v7.3.16', 'pypy3.10-v7.3.17']

name: "Python ${{ matrix.python }} / ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
python -m pip install pytest pytest-github-actions-annotate-failures typing_extensions

- name: Install NumPy
if: ${{ !startsWith(matrix.python, 'pypy') && !contains(matrix.python, 'rc.1') }}
if: ${{ !startsWith(matrix.python, 'pypy') && !contains(matrix.python, 'rc.2') }}
run: |
python -m pip install numpy scipy

Expand Down Expand Up @@ -135,4 +135,39 @@ jobs:
run: cmake --build build -j 2

- name: Run tests
run: cd build && python3 -m pytest
run: >
cd build;
python -m pytest

free-threaded:
name: "Python 3.13-dev / ubuntu.latest [free-threaded]"
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
submodules: true

- uses: deadsnakes/action@v3.1.0
with:
python-version: 3.13-dev
nogil: true

- name: Install the latest CMake
uses: lukka/get-cmake@latest

- name: Install PyTest
run: |
python -m pip install pytest pytest-github-actions-annotate-failures

- name: Configure
run: >
cmake -S . -B build -DNB_TEST_FREE_THREADED=ON

- name: Build C++
run: cmake --build build -j 2

- name: Run tests
run: >
cd build;
python -m pytest
wjakob marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
libnanobind-static.a
libnanobind-static-abi3.a
libnanobind.dylib
libnanobind-static-ft.a
libnanobind.so
libnanobind-abi3.dylib
libnanobind-abi3.so
libnanobind-ft.so
libnanobind.dylib
libnanobind-abi3.dylib
libnanobind-ft.dylib
nanobind.dll
nanobind-abi3.dll
nanobind-ft.dll

libinter_module.dylib
libinter_module.so
Expand Down
11 changes: 6 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ endif()
option(NB_CREATE_INSTALL_RULES "Create installation rules" ${NB_MASTER_PROJECT})
option(NB_USE_SUBMODULE_DEPS "Use the nanobind dependencies shipped as a git submodule of this repository" ON)

option(NB_TEST "Compile nanobind tests?" ${NB_MASTER_PROJECT})
option(NB_TEST_STABLE_ABI "Test the stable ABI interface?" OFF)
option(NB_TEST_SHARED_BUILD "Build a shared nanobind library for the test suite?" OFF)
option(NB_TEST_CUDA "Force the use of the CUDA/NVCC compiler for testing purposes" OFF)
option(NB_TEST "Compile nanobind tests?" ${NB_MASTER_PROJECT})
option(NB_TEST_STABLE_ABI "Test the stable ABI interface?" OFF)
option(NB_TEST_SHARED_BUILD "Build a shared nanobind library for the test suite?" OFF)
option(NB_TEST_CUDA "Force the use of the CUDA/NVCC compiler for testing purposes" OFF)
option(NB_TEST_FREE_THREADED "Build free-threaded extensions for the test suite?" ON)

if (NOT MSVC)
option(NB_TEST_SANITZE "Build tests with address and undefined behavior sanitizers?" OFF)
option(NB_TEST_SANITIZE "Build tests with address and undefined behavior sanitizers?" OFF)
endif()

# ---------------------------------------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions cmake/darwin-ld-cpython.sym
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,12 @@
-U __Py_INCREF_IncRefTotal
-U __PyObject_GetDictPtr
-U _PyList_GetItemRef
-U _PyDict_GetItemRef
-U _PyDict_GetItemStringRef
-U _PyDict_SetDefault
-U _PyDict_SetDefaultRef
-U _PyWeakref_GetRef
-U _PyImport_AddModuleRef
-U _PyUnstable_Module_SetGIL
-U _PyMutex_Unlock
-U _PyMutex_Lock
Expand All @@ -916,3 +922,4 @@
-U _PyCriticalSection_End
-U _PyCriticalSection2_Begin
-U _PyCriticalSection2_End
-U _PyUnicode_AsUTF8
26 changes: 24 additions & 2 deletions cmake/nanobind-config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ if(DEFINED NB_SOSABI)
endif()
endif()

# Extract Python version and extensions (e.g. free-threaded build)
string(REGEX REPLACE "[^-]*-([^-]*)-.*" "\\1" NB_ABI "${NB_SOABI}")

# If either suffix is missing, call Python to compute it
if(NOT DEFINED NB_SUFFIX OR NOT DEFINED NB_SUFFIX_S)
# Query Python directly to get the right suffix.
Expand Down Expand Up @@ -72,6 +75,7 @@ endif()
# Stash these for later use
set(NB_SUFFIX ${NB_SUFFIX} CACHE INTERNAL "")
set(NB_SUFFIX_S ${NB_SUFFIX_S} CACHE INTERNAL "")
set(NB_ABI ${NB_ABI} CACHE INTERNAL "")

get_filename_component(NB_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(NB_DIR "${NB_DIR}" PATH)
Expand Down Expand Up @@ -201,13 +205,17 @@ function (nanobind_build_library TARGET_NAME)
endif()

if (WIN32)
if (${TARGET_NAME} MATCHES "abi3")
if (${TARGET_NAME} MATCHES "-abi3")
target_link_libraries(${TARGET_NAME} PUBLIC Python::SABIModule)
else()
target_link_libraries(${TARGET_NAME} PUBLIC Python::Module)
endif()
endif()

if (TARGET_NAME MATCHES "-ft")
target_compile_definitions(${TARGET_NAME} PUBLIC NB_FREE_THREADED)
endif()

# Nanobind performs many assertion checks -- detailed error messages aren't
# included in Release/MinSizeRel modes
target_compile_definitions(${TARGET_NAME} PRIVATE
Expand Down Expand Up @@ -297,7 +305,7 @@ endfunction()

function(nanobind_add_module name)
cmake_parse_arguments(PARSE_ARGV 1 ARG
"STABLE_ABI;NB_STATIC;NB_SHARED;PROTECT_STACK;LTO;NOMINSIZE;NOSTRIP;MUSL_DYNAMIC_LIBCPP"
"STABLE_ABI;FREE_THREADED;NB_STATIC;NB_SHARED;PROTECT_STACK;LTO;NOMINSIZE;NOSTRIP;MUSL_DYNAMIC_LIBCPP"
"NB_DOMAIN" "")

add_library(${name} MODULE ${ARG_UNPARSED_ARGUMENTS})
Expand All @@ -319,6 +327,12 @@ function(nanobind_add_module name)
set(ARG_STABLE_ABI FALSE)
endif()

if (NB_ABI MATCHES "t")
set(ARG_STABLE_ABI FALSE)
else(ARG_STABLE_ABI)
set(ARG_FREE_THREADED FALSE)
endif()

set(libname "nanobind")
if (ARG_NB_STATIC)
set(libname "${libname}-static")
Expand All @@ -328,6 +342,10 @@ function(nanobind_add_module name)
set(libname "${libname}-abi3")
endif()

if (ARG_FREE_THREADED)
set(libname "${libname}-ft")
endif()

if (ARG_NB_DOMAIN AND ARG_NB_SHARED)
set(libname ${libname}-${ARG_NB_DOMAIN})
endif()
Expand All @@ -345,6 +363,10 @@ function(nanobind_add_module name)
nanobind_extension(${name})
endif()

if (ARG_FREE_THREADED)
target_compile_definitions(${name} PRIVATE NB_FREE_THREADED)
endif()

target_link_libraries(${name} PRIVATE ${libname})

if (NOT ARG_PROTECT_STACK)
Expand Down
7 changes: 7 additions & 0 deletions docs/api_cmake.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ The high-level interface consists of just one CMake command:
<https://docs.python.org/3/c-api/stable.html>`__ build, making it
possible to use a compiled extension across Python minor versions.
The flag is ignored on Python versions older than < 3.12.
* - ``FREE_THREADED``
- Compile an Python extension that opts into free-threaded (i.e.,
GIL-less) Python behavior, which requires a special free-threaded
build of Python 3.13 or newer. The flag is ignored on unsupported
Python versions.
* - ``NB_STATIC``
- Compile the core nanobind library as a static library. This
simplifies redistribution but can increase the combined binary
Expand Down Expand Up @@ -270,6 +275,8 @@ The various commands are described below:
- Perform a static library build (without this suffix, a shared build is used)
* - ``-abi3``
- Perform a stable ABI build targeting Python v3.12+.
* - ``-ft``
- Perform a build that opts into the Python 3.13+ free-threaded behavior.

.. code-block:: cmake

Expand Down
115 changes: 112 additions & 3 deletions docs/api_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,10 @@ Wrapper classes
Return an item iterator that returns ``std::pair<handle, handle>``
key-value pairs analogous to ``iter(dict.items())`` in Python.

In free-threaded Python, the :cpp:class:``detail::dict_iterator`` class
acquires a lock to the underlying dictionary to enable the use of the
efficient but thread-unsafe ``PyDict_Next()`` Python C traversal routine.

.. cpp:function:: detail::dict_iterator end() const

Return a sentinel that ends the iteration.
Expand Down Expand Up @@ -1616,7 +1620,9 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,

.. cpp:function:: template <typename T> arg_v operator=(T &&value) const

Assign a default value to the argument.
Return an argument annotation that is like this one but also assigns a
default value to the argument. The default will be converted into a Python
object immediately, so its bindings must have already been defined.

.. cpp:function:: arg &none(bool value = true)

Expand All @@ -1638,6 +1644,12 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,
explain it in docstrings and stubs (``str(value)``) does not produce
acceptable output.

.. cpp:function:: arg_locked lock()

Return an argument annotation that is like this one but also requests that
this argument be locked when dispatching a function call in free-threaded
Python extensions. It does nothing in regular GIL-protected extensions.

.. cpp:struct:: is_method

Indicate that the bound function is a method.
Expand All @@ -1656,6 +1668,12 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,

Indicate that the bound constructor can be used to perform implicit conversions.

.. cpp:struct:: lock_self

Indicate that the implicit ``self`` argument of a method should be locked
when dispatching a call in a free-threaded extension. This annotation does
nothing in regular GIL-protected extensions.

.. cpp:struct:: template <typename... Ts> call_guard

Invoke the call guard(s) `Ts` when the bound function executes. The RAII
Expand Down Expand Up @@ -1875,10 +1893,8 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,

.. cpp:struct:: template <typename T> for_setter


Analogous to :cpp:struct:`for_getter`, but for setters.


.. _class_binding_annotations:

Class binding annotations
Expand Down Expand Up @@ -2552,10 +2568,103 @@ running it in parallel from multiple Python threads.

Release the GIL (**must** be currently held)

In :ref:`free-threaded extensions <free-threaded>`, this operation also
temporarily releases all :ref:`argument locks <argument-locks>` held by
the current thread.

.. cpp:function:: ~gil_scoped_release()

Reacquire the GIL

Free-threading
--------------

Nanobind provides abstractions to implement *additional* locking that is
needed to ensure the correctness of free-threaded Python extensions.

.. cpp:struct:: ft_mutex

Object-oriented wrapper representing a `PyMutex
<https://docs.python.org/3.13/c-api/init.html#c.PyMutex>`__. It can be
slightly more efficient than OS/language-provided primitives (e.g.,
``std::thread``, ``pthread_mutex_t``) and should generally be preferred when
adding critical sections to Python bindings.

In Python builds *without* free-threading, this class does nothing. It has
no attributes and the :cpp:func:`lock` and :cpp:func:`unlock` functions
return immediately.

.. cpp:function:: ft_mutex()

Create a new (unlocked) mutex.

.. cpp:function:: void lock()

Acquire the mutex.

.. cpp:function:: void unlock()

Release the mutex.

.. cpp:struct:: ft_lock_guard

This class provides a RAII lock guard analogous to ``std::lock_guard`` and
``std::unique_lock``.

.. cpp:function:: ft_lock_guard(ft_mutex &mutex)

Call :cpp:func:`mutex.lock() <ft_mutex::lock>` (no-op in non-free-threaded builds).

.. cpp:function:: ~ft_lock_guard()

Call :cpp:func:`mutex.unlock() <ft_mutex::unlock>` (no-op in non-free-threaded builds).

.. cpp:struct:: ft_object_guard

This class provides a RAII guard that locks a single Python object within a
local scope (in contrast to :cpp:class:`ft_lock_guard`, which locks a
mutex).

It is a thin wrapper around the Python `critical section API
<https://docs.python.org/3.13/c-api/init.html#c.Py_BEGIN_CRITICAL_SECTION>`__.
Please refer to the Python documentation for details on the semantics of
this relaxed form of critical section (in particular, Python critical sections
may release previously held locks).

In Python builds *without* free-threading, this class does nothing---the
constructor and destructor return immediately.

.. cpp:function:: ft_object_guard(handle h)

Lock the object ``h`` (no-op in non-free-threaded builds)

.. cpp:function:: ~ft_object_guard()

Unlock the object ``h`` (no-op in non-free-threaded builds)

.. cpp:struct:: ft_object2_guard

This class provides a RAII guard that locks *two* Python object within a
local scope (in contrast to :cpp:class:`ft_lock_guard`, which locks a
mutex).

It is a thin wrapper around the Python `critical section API
<https://docs.python.org/3.13/c-api/init.html#c.Py_BEGIN_CRITICAL_SECTION2>`__.
Please refer to the Python documentation for details on the semantics of
this relaxed form of critical section (in particular, Python critical sections
may release previously held locks).

In Python builds *without* free-threading, this class does nothing---the
constructor and destructor return immediately.

.. cpp:function:: ft_object2_guard(handle h1, handle h2)

Lock the objects ``h1`` and ``h2`` (no-op in non-free-threaded builds)

.. cpp:function:: ~ft_object2_guard()

Unlock the objects ``h1`` and ``h2`` (no-op in non-free-threaded builds)

Low-level type and instance access
----------------------------------

Expand Down
Loading