Skip to content

Commit 15fde1d

Browse files
Chekov2kSeeChange-CIhenryiiirwgkpre-commit-ci[bot]
committed
Add PYBIND11_SIMPLE_GIL_MANAGEMENT option (cmake, C++ define) (#4216)
* Add option to force the use of the PYPY GIL scoped acquire/release logic to support nested gil access, see #1276 and pytorch/pytorch#83101 * Apply suggestions from code review * Update CMakeLists.txt * docs: update upgrade guide * Update docs/upgrade.rst * All bells & whistles. * Add Reminder to common.h, so that we will not forget to purge `!WITH_THREAD` branches when dropping Python 3.6 * New sentence instead of semicolon. * Temporarily pull in snapshot of PR #4246 * Add `test_release_acquire` * Add more unit tests for nested gil locking * Add test_report_builtins_internals_keys * Very minor enhancement: sort list only after filtering. * Revert change in docs/upgrade.rst * Add test_multi_acquire_release_cross_module, while also forcing unique PYBIND11_INTERNALS_VERSION for cross_module_gil_utils.cpp * Hopefully fix apparently new ICC error. ``` 2022-10-28T07:57:54.5187728Z -- The CXX compiler identification is Intel 2021.7.0.20220726 ... 2022-10-28T07:58:53.6758994Z icpc: remark #10441: The Intel(R) C++ Compiler Classic (ICC) is deprecated and will be removed from product release in the second half of 2023. The Intel(R) oneAPI DPC++/C++ Compiler (ICX) is the recommended compiler moving forward. Please transition to use this compiler. Use '-diag-disable=10441' to disable this message. 2022-10-28T07:58:54.5801597Z In file included from /home/runner/work/pybind11/pybind11/include/pybind11/detail/../detail/type_caster_base.h(15), 2022-10-28T07:58:54.5803794Z from /home/runner/work/pybind11/pybind11/include/pybind11/detail/../cast.h(15), 2022-10-28T07:58:54.5805740Z from /home/runner/work/pybind11/pybind11/include/pybind11/detail/../attr.h(14), 2022-10-28T07:58:54.5809556Z from /home/runner/work/pybind11/pybind11/include/pybind11/detail/class.h(12), 2022-10-28T07:58:54.5812154Z from /home/runner/work/pybind11/pybind11/include/pybind11/pybind11.h(13), 2022-10-28T07:58:54.5948523Z from /home/runner/work/pybind11/pybind11/tests/cross_module_gil_utils.cpp(13): 2022-10-28T07:58:54.5949009Z /home/runner/work/pybind11/pybind11/include/pybind11/detail/../detail/internals.h(177): error #2282: unrecognized GCC pragma 2022-10-28T07:58:54.5949374Z PYBIND11_TLS_KEY_INIT(tstate) 2022-10-28T07:58:54.5949579Z ^ 2022-10-28T07:58:54.5949695Z ``` * clang-tidy fixes * Workaround for PYPY WIN exitcode None * Revert "Temporarily pull in snapshot of PR #4246" This reverts commit 23ac16e. * Another workaround for PYPY WIN exitcode None * Clean up how the tests are run "run in process" Part 1: uniformity * Clean up how the tests are run "run in process" Part 2: use `@pytest.mark.parametrize` and clean up the naming. * Skip some tests `#if defined(THREAD_SANITIZER)` (tested with TSAN using the Google-internal toolchain). * Run all tests again but ignore ThreadSanitizer exitcode 66 (this is less likely to mask unrelated ThreadSanitizer issues in the future). * bug fix: missing common.h include before using `PYBIND11_SIMPLE_GIL_MANAGEMENT` For the tests in the github CI this does not matter, because `PYBIND11_SIMPLE_GIL_MANAGEMENT` is always defined from the command line, but when monkey-patching common.h locally, it matters. * if process.exitcode is None: assert t_delta > 9.9 * More sophisiticated `_run_in_process()` implementation, clearly reporting `DEADLOCK`, additionally exercised via added `intentional_deadlock()` * Wrap m.intentional_deadlock in a Python function, for `ForkingPickler` compatibility. ``` > ForkingPickler(file, protocol).dump(obj) E TypeError: cannot pickle 'PyCapsule' object ``` Observed with all Windows builds including mingw but not PyPy, and macos-latest with Python 3.9, 3.10, 3.11 but not 3.6. * Add link to potential solution for WOULD-BE-NICE-TO-HAVE feature. * Add `SKIP_IF_DEADLOCK = True` option, to not pollute the CI results with expected `DEADLOCK` failures while we figure out what to do about them. * Add COPY-PASTE-THIS: gdb ... command (to be used for debugging the detected deadlock) * style: pre-commit fixes * Do better than automatic pre-commit fixes. * Add `PYBIND11_SIMPLE_GIL_MANAGEMENT` to `pytest_report_header()` (so that we can easily know when harvesting deadlock information from the CI logs). Co-authored-by: Arnim Balzer <[email protected]> Co-authored-by: Henry Schreiner <[email protected]> Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 5b395c9 commit 15fde1d

File tree

11 files changed

+433
-60
lines changed

11 files changed

+433
-60
lines changed

.github/workflows/ci.yml

+4
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,12 @@ jobs:
102102
run: python -m pip install pytest-github-actions-annotate-failures
103103

104104
# First build - C++11 mode and inplace
105+
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON here.
105106
- name: Configure C++11 ${{ matrix.args }}
106107
run: >
107108
cmake -S . -B .
108109
-DPYBIND11_WERROR=ON
110+
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON
109111
-DDOWNLOAD_CATCH=ON
110112
-DDOWNLOAD_EIGEN=ON
111113
-DCMAKE_CXX_STANDARD=11
@@ -129,10 +131,12 @@ jobs:
129131
run: git clean -fdx
130132

131133
# Second build - C++17 mode and in a build directory
134+
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF here.
132135
- name: Configure C++17
133136
run: >
134137
cmake -S . -B build2
135138
-DPYBIND11_WERROR=ON
139+
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF
136140
-DDOWNLOAD_CATCH=ON
137141
-DDOWNLOAD_EIGEN=ON
138142
-DCMAKE_CXX_STANDARD=17

CMakeLists.txt

+6
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,16 @@ endif()
9191
option(PYBIND11_INSTALL "Install pybind11 header files?" ${PYBIND11_MASTER_PROJECT})
9292
option(PYBIND11_TEST "Build pybind11 test suite?" ${PYBIND11_MASTER_PROJECT})
9393
option(PYBIND11_NOPYTHON "Disable search for Python" OFF)
94+
option(PYBIND11_SIMPLE_GIL_MANAGEMENT
95+
"Use simpler GIL management logic that does not support disassociation" OFF)
9496
set(PYBIND11_INTERNALS_VERSION
9597
""
9698
CACHE STRING "Override the ABI version, may be used to enable the unstable ABI.")
9799

100+
if(PYBIND11_SIMPLE_GIL_MANAGEMENT)
101+
add_compile_definitions(PYBIND11_SIMPLE_GIL_MANAGEMENT)
102+
endif()
103+
98104
cmake_dependent_option(
99105
USE_PYTHON_INCLUDE_DIR
100106
"Install pybind11 headers in Python include directory instead of default installation prefix"

include/pybind11/detail/common.h

+5
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
#endif
207207

208208
#include <Python.h>
209+
// Reminder: WITH_THREAD is always defined if PY_VERSION_HEX >= 0x03070000
209210
#if PY_VERSION_HEX < 0x03060000
210211
# error "PYTHON < 3.6 IS UNSUPPORTED. pybind11 v2.9 was the last to support Python 2 and 3.5."
211212
#endif
@@ -229,6 +230,10 @@
229230
# undef copysign
230231
#endif
231232

233+
#if defined(PYPY_VERSION) && !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
234+
# define PYBIND11_SIMPLE_GIL_MANAGEMENT
235+
#endif
236+
232237
#if defined(_MSC_VER)
233238
# if defined(PYBIND11_DEBUG_MARKER)
234239
# define _DEBUG

include/pybind11/detail/internals.h

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99

1010
#pragma once
1111

12+
#include "common.h"
13+
14+
#if defined(WITH_THREAD) && defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
15+
# include "../gil.h"
16+
#endif
17+
1218
#include "../pytypes.h"
1319

1420
#include <exception>
@@ -49,7 +55,7 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass);
4955
// `Py_LIMITED_API` anyway.
5056
# if PYBIND11_INTERNALS_VERSION > 4
5157
# define PYBIND11_TLS_KEY_REF Py_tss_t &
52-
# ifdef __GNUC__
58+
# if defined(__GNUC__) && !defined(__INTEL_COMPILER)
5359
// Clang on macOS warns due to `Py_tss_NEEDS_INIT` not specifying an initializer
5460
// for every field.
5561
# define PYBIND11_TLS_KEY_INIT(var) \
@@ -169,10 +175,12 @@ struct internals {
169175
PyTypeObject *default_metaclass;
170176
PyObject *instance_base;
171177
#if defined(WITH_THREAD)
178+
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
172179
PYBIND11_TLS_KEY_INIT(tstate)
173180
# if PYBIND11_INTERNALS_VERSION > 4
174181
PYBIND11_TLS_KEY_INIT(loader_life_support_tls_key)
175182
# endif // PYBIND11_INTERNALS_VERSION > 4
183+
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
176184
PyInterpreterState *istate = nullptr;
177185
~internals() {
178186
# if PYBIND11_INTERNALS_VERSION > 4
@@ -408,6 +416,10 @@ PYBIND11_NOINLINE internals &get_internals() {
408416
return **internals_pp;
409417
}
410418

419+
#if defined(WITH_THREAD)
420+
# if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
421+
gil_scoped_acquire gil;
422+
# else
411423
// Ensure that the GIL is held since we will need to make Python calls.
412424
// Cannot use py::gil_scoped_acquire here since that constructor calls get_internals.
413425
struct gil_scoped_acquire_local {
@@ -417,6 +429,8 @@ PYBIND11_NOINLINE internals &get_internals() {
417429
~gil_scoped_acquire_local() { PyGILState_Release(state); }
418430
const PyGILState_STATE state;
419431
} gil;
432+
# endif
433+
#endif
420434
error_scope err_scope;
421435

422436
PYBIND11_STR_TYPE id(PYBIND11_INTERNALS_ID);

include/pybind11/gil.h

+40-13
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
#pragma once
1111

1212
#include "detail/common.h"
13-
#include "detail/internals.h"
13+
14+
#if defined(WITH_THREAD) && !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
15+
# include "detail/internals.h"
16+
#endif
1417

1518
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1619

@@ -21,7 +24,9 @@ PyThreadState *get_thread_state_unchecked();
2124

2225
PYBIND11_NAMESPACE_END(detail)
2326

24-
#if defined(WITH_THREAD) && !defined(PYPY_VERSION)
27+
#if defined(WITH_THREAD)
28+
29+
# if !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
2530

2631
/* The functions below essentially reproduce the PyGILState_* API using a RAII
2732
* pattern, but there are a few important differences:
@@ -62,11 +67,11 @@ class gil_scoped_acquire {
6267

6368
if (!tstate) {
6469
tstate = PyThreadState_New(internals.istate);
65-
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
70+
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
6671
if (!tstate) {
6772
pybind11_fail("scoped_acquire: could not create thread state!");
6873
}
69-
# endif
74+
# endif
7075
tstate->gilstate_counter = 0;
7176
PYBIND11_TLS_REPLACE_VALUE(internals.tstate, tstate);
7277
} else {
@@ -87,20 +92,20 @@ class gil_scoped_acquire {
8792

8893
PYBIND11_NOINLINE void dec_ref() {
8994
--tstate->gilstate_counter;
90-
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
95+
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
9196
if (detail::get_thread_state_unchecked() != tstate) {
9297
pybind11_fail("scoped_acquire::dec_ref(): thread state must be current!");
9398
}
9499
if (tstate->gilstate_counter < 0) {
95100
pybind11_fail("scoped_acquire::dec_ref(): reference count underflow!");
96101
}
97-
# endif
102+
# endif
98103
if (tstate->gilstate_counter == 0) {
99-
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
104+
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
100105
if (!release) {
101106
pybind11_fail("scoped_acquire::dec_ref(): internal error!");
102107
}
103-
# endif
108+
# endif
104109
PyThreadState_Clear(tstate);
105110
if (active) {
106111
PyThreadState_DeleteCurrent();
@@ -178,12 +183,14 @@ class gil_scoped_release {
178183
bool disassoc;
179184
bool active = true;
180185
};
181-
#elif defined(PYPY_VERSION)
186+
187+
# else // PYBIND11_SIMPLE_GIL_MANAGEMENT
188+
182189
class gil_scoped_acquire {
183190
PyGILState_STATE state;
184191

185192
public:
186-
gil_scoped_acquire() { state = PyGILState_Ensure(); }
193+
gil_scoped_acquire() : state{PyGILState_Ensure()} {}
187194
gil_scoped_acquire(const gil_scoped_acquire &) = delete;
188195
gil_scoped_acquire &operator=(const gil_scoped_acquire &) = delete;
189196
~gil_scoped_acquire() { PyGILState_Release(state); }
@@ -194,19 +201,39 @@ class gil_scoped_release {
194201
PyThreadState *state;
195202

196203
public:
197-
gil_scoped_release() { state = PyEval_SaveThread(); }
204+
gil_scoped_release() : state{PyEval_SaveThread()} {}
198205
gil_scoped_release(const gil_scoped_release &) = delete;
199206
gil_scoped_release &operator=(const gil_scoped_acquire &) = delete;
200207
~gil_scoped_release() { PyEval_RestoreThread(state); }
201208
void disarm() {}
202209
};
203-
#else
210+
211+
# endif // PYBIND11_SIMPLE_GIL_MANAGEMENT
212+
213+
#else // WITH_THREAD
214+
204215
class gil_scoped_acquire {
216+
public:
217+
gil_scoped_acquire() {
218+
// Trick to suppress `unused variable` error messages (at call sites).
219+
(void) (this != (this + 1));
220+
}
221+
gil_scoped_acquire(const gil_scoped_acquire &) = delete;
222+
gil_scoped_acquire &operator=(const gil_scoped_acquire &) = delete;
205223
void disarm() {}
206224
};
225+
207226
class gil_scoped_release {
227+
public:
228+
gil_scoped_release() {
229+
// Trick to suppress `unused variable` error messages (at call sites).
230+
(void) (this != (this + 1));
231+
}
232+
gil_scoped_release(const gil_scoped_release &) = delete;
233+
gil_scoped_release &operator=(const gil_scoped_acquire &) = delete;
208234
void disarm() {}
209235
};
210-
#endif
236+
237+
#endif // WITH_THREAD
211238

212239
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,5 @@ def pytest_report_header(config):
210210
f" {pybind11_tests.compiler_info}"
211211
f" {pybind11_tests.cpp_std}"
212212
f" {pybind11_tests.PYBIND11_INTERNALS_ID}"
213+
f" PYBIND11_SIMPLE_GIL_MANAGEMENT={pybind11_tests.PYBIND11_SIMPLE_GIL_MANAGEMENT}"
213214
)

tests/cross_module_gil_utils.cpp

+65-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66
All rights reserved. Use of this source code is governed by a
77
BSD-style license that can be found in the LICENSE file.
88
*/
9+
#if defined(PYBIND11_INTERNALS_VERSION)
10+
# undef PYBIND11_INTERNALS_VERSION
11+
#endif
12+
#define PYBIND11_INTERNALS_VERSION 21814642 // Ensure this module has its own `internals` instance.
913
#include <pybind11/pybind11.h>
1014

1115
#include <cstdint>
16+
#include <string>
17+
#include <thread>
1218

1319
// This file mimics a DSO that makes pybind11 calls but does not define a
1420
// PYBIND11_MODULE. The purpose is to test that such a DSO can create a
@@ -21,24 +27,81 @@
2127
namespace {
2228

2329
namespace py = pybind11;
30+
2431
void gil_acquire() { py::gil_scoped_acquire gil; }
2532

33+
std::string gil_multi_acquire_release(unsigned bits) {
34+
if ((bits & 0x1u) != 0u) {
35+
py::gil_scoped_acquire gil;
36+
}
37+
if ((bits & 0x2u) != 0u) {
38+
py::gil_scoped_release gil;
39+
}
40+
if ((bits & 0x4u) != 0u) {
41+
py::gil_scoped_acquire gil;
42+
}
43+
if ((bits & 0x8u) != 0u) {
44+
py::gil_scoped_release gil;
45+
}
46+
return PYBIND11_INTERNALS_ID;
47+
}
48+
49+
struct CustomAutoGIL {
50+
CustomAutoGIL() : gstate(PyGILState_Ensure()) {}
51+
~CustomAutoGIL() { PyGILState_Release(gstate); }
52+
53+
PyGILState_STATE gstate;
54+
};
55+
struct CustomAutoNoGIL {
56+
CustomAutoNoGIL() : save(PyEval_SaveThread()) {}
57+
~CustomAutoNoGIL() { PyEval_RestoreThread(save); }
58+
59+
PyThreadState *save;
60+
};
61+
62+
template <typename Acquire, typename Release>
63+
void gil_acquire_inner() {
64+
Acquire acquire_outer;
65+
Acquire acquire_inner;
66+
Release release;
67+
}
68+
69+
template <typename Acquire, typename Release>
70+
void gil_acquire_nested() {
71+
Acquire acquire_outer;
72+
Acquire acquire_inner;
73+
Release release;
74+
auto thread = std::thread(&gil_acquire_inner<Acquire, Release>);
75+
thread.join();
76+
}
77+
2678
constexpr char kModuleName[] = "cross_module_gil_utils";
2779

2880
struct PyModuleDef moduledef = {
2981
PyModuleDef_HEAD_INIT, kModuleName, nullptr, 0, nullptr, nullptr, nullptr, nullptr, nullptr};
3082

3183
} // namespace
3284

85+
#define ADD_FUNCTION(Name, ...) \
86+
PyModule_AddObject(m, Name, PyLong_FromVoidPtr(reinterpret_cast<void *>(&__VA_ARGS__)));
87+
3388
extern "C" PYBIND11_EXPORT PyObject *PyInit_cross_module_gil_utils() {
3489

3590
PyObject *m = PyModule_Create(&moduledef);
3691

3792
if (m != nullptr) {
3893
static_assert(sizeof(&gil_acquire) == sizeof(void *),
3994
"Function pointer must have the same size as void*");
40-
PyModule_AddObject(
41-
m, "gil_acquire_funcaddr", PyLong_FromVoidPtr(reinterpret_cast<void *>(&gil_acquire)));
95+
ADD_FUNCTION("gil_acquire_funcaddr", gil_acquire)
96+
ADD_FUNCTION("gil_multi_acquire_release_funcaddr", gil_multi_acquire_release)
97+
ADD_FUNCTION("gil_acquire_inner_custom_funcaddr",
98+
gil_acquire_inner<CustomAutoGIL, CustomAutoNoGIL>)
99+
ADD_FUNCTION("gil_acquire_nested_custom_funcaddr",
100+
gil_acquire_nested<CustomAutoGIL, CustomAutoNoGIL>)
101+
ADD_FUNCTION("gil_acquire_inner_pybind11_funcaddr",
102+
gil_acquire_inner<py::gil_scoped_acquire, py::gil_scoped_release>)
103+
ADD_FUNCTION("gil_acquire_nested_pybind11_funcaddr",
104+
gil_acquire_nested<py::gil_scoped_acquire, py::gil_scoped_release>)
42105
}
43106

44107
return m;

tests/pybind11_tests.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ PYBIND11_MODULE(pybind11_tests, m) {
8989
#endif
9090
m.attr("cpp_std") = cpp_std();
9191
m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID;
92+
m.attr("PYBIND11_SIMPLE_GIL_MANAGEMENT") =
93+
#if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
94+
true;
95+
#else
96+
false;
97+
#endif
9298

9399
bind_ConstructorStats(m);
94100

tests/test_embed/test_interpreter.cpp

-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,6 @@ TEST_CASE("Threads") {
293293

294294
{
295295
py::gil_scoped_release gil_release{};
296-
REQUIRE(has_pybind11_internals_static());
297296

298297
auto threads = std::vector<std::thread>();
299298
for (auto i = 0; i < num_threads; ++i) {

0 commit comments

Comments
 (0)