Skip to content

Commit fee2527

Browse files
XuehaiPanb-passpre-commit-ci[bot]rwgk
authored
Fix concurrency consistency for internals_pp_manager under multiple-interpreters (#5947)
* Add per-interpreter storage for `gil_safe_call_once_and_store` * Disable thread local cache for `internals_pp_manager` * Disable thread local cache for `internals_pp_manager` for multi-interpreter only * Use anonymous namespace to separate these type_ids from other tests with the same class names. * style: pre-commit fixes * Revert internals_pp_manager changes * This is the crux of fix for the subinterpreter_before_main failure. The pre_init needs to check if it is in a subinterpreter or not. But in 3.13+ this static initializer runs in the main interpreter. So we need to check this later, during the exec phase. * Continue to do the ensure in both places, there might be a reason it was where it was... Should not hurt anything to do it extra times here. * Change get_num_interpreters_seen to a boolean flag instead. The count was not used, it was just checked for > 1, we now accomplish this by setting the flag. * Spelling typo * Work around older python versions, only need this check for newish versions * Add more comments for test case * Add more comments for test case * Stop traceback propagation * Re-enable subinterpreter support on ubuntu 3.14 builds Was disabled in e4873e8 * As suggested, don't use an anonymous namespace. * Typo in test assert format string * Use a more appropriate function name * Fix mod_per_interpreter_gil* output directory on Windows/MSVC On Windows with MSVC (multi-configuration generators), CMake uses config-specific properties like LIBRARY_OUTPUT_DIRECTORY_DEBUG when set, otherwise falls back to LIBRARY_OUTPUT_DIRECTORY/<Config>/. The main test modules (pybind11_tests, etc.) correctly set both LIBRARY_OUTPUT_DIRECTORY and the config-specific variants (lines 517-528), so they output directly to tests/. However, the mod_per_interpreter_gil* modules only copied the base LIBRARY_OUTPUT_DIRECTORY property, causing them to be placed in tests/Debug/ instead of tests/. This mismatch caused test_import_in_subinterpreter_concurrently and related tests to fail with ModuleNotFoundError on Windows Python 3.14, because the test code sets sys.path based on pybind11_tests.__file__ (which is in tests/) but tries to import mod_per_interpreter_gil_with_singleton (which ended up in tests/Debug/). This bug was previously masked by @pytest.mark.xfail decorators on these tests. Now that the underlying "Duplicate C++ type registration" issue is fixed and the xfails are removed, this path issue surfaced. The fix mirrors the same pattern used for main test targets: also set LIBRARY_OUTPUT_DIRECTORY_<CONFIG> for each configuration type. * Remove unneeded `pytest.importorskip` * Remove comment --------- Co-authored-by: b-pass <b-pass@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
1 parent 0057e49 commit fee2527

10 files changed

Lines changed: 72 additions & 56 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
cmake-args: -DCMAKE_CXX_STANDARD=20 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON
8989
- runs-on: ubuntu-latest
9090
python-version: '3.14'
91-
cmake-args: -DCMAKE_CXX_STANDARD=14 -DCMAKE_CXX_FLAGS="-DPYBIND11_HAS_SUBINTERPRETER_SUPPORT=0"
91+
cmake-args: -DCMAKE_CXX_STANDARD=14
9292
- runs-on: ubuntu-latest
9393
python-version: 'pypy-3.10'
9494
cmake-args: -DCMAKE_CXX_STANDARD=14

include/pybind11/detail/common.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,11 @@ Note that this is run once for each (sub-)interpreter the module is imported int
441441
possibly concurrently. The PyModuleDef is allowed to be static, but the PyObject* resulting from
442442
PyModuleDef_Init should be treated like any other PyObject (so not shared across interpreters).
443443
*/
444-
#define PYBIND11_MODULE_PYINIT(name, pre_init, ...) \
444+
#define PYBIND11_MODULE_PYINIT(name, ...) \
445445
static int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject *); \
446446
PYBIND11_PLUGIN_IMPL(name) { \
447447
PYBIND11_CHECK_PYTHON_VERSION \
448-
pre_init; \
449-
PYBIND11_ENSURE_INTERNALS_READY \
448+
pybind11::detail::ensure_internals(); \
450449
static ::pybind11::detail::slots_array mod_def_slots = ::pybind11::detail::init_slots( \
451450
&PYBIND11_CONCAT(pybind11_exec_, name), ##__VA_ARGS__); \
452451
static PyModuleDef def{/* m_base */ PyModuleDef_HEAD_INIT, \
@@ -465,6 +464,7 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across
465464
static void PYBIND11_CONCAT(pybind11_init_, name)(::pybind11::module_ &); \
466465
int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject * pm) { \
467466
try { \
467+
pybind11::detail::ensure_internals(); \
468468
auto m = pybind11::reinterpret_borrow<::pybind11::module_>(pm); \
469469
if (!pybind11::detail::get_cached_module(m.attr("__spec__").attr("name"))) { \
470470
PYBIND11_CONCAT(pybind11_init_, name)(m); \
@@ -518,8 +518,7 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across
518518
519519
\endrst */
520520
#define PYBIND11_MODULE(name, variable, ...) \
521-
PYBIND11_MODULE_PYINIT( \
522-
name, (pybind11::detail::get_num_interpreters_seen() += 1), ##__VA_ARGS__) \
521+
PYBIND11_MODULE_PYINIT(name, ##__VA_ARGS__) \
523522
PYBIND11_MODULE_EXEC(name, variable)
524523

525524
// pop gnu-zero-variadic-macro-arguments

include/pybind11/detail/internals.h

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,12 @@ inline PyThreadState *get_thread_state_unchecked() {
418418
#endif
419419
}
420420

421-
/// We use this counter to figure out if there are or have been multiple subinterpreters active at
422-
/// any point. This must never decrease while any interpreter may be running in any thread!
423-
inline std::atomic<int64_t> &get_num_interpreters_seen() {
424-
static std::atomic<int64_t> counter(0);
425-
return counter;
421+
/// We use this to figure out if there are or have been multiple subinterpreters active at any
422+
/// point. This must never go from true to false while any interpreter may be running in any
423+
/// thread!
424+
inline std::atomic_bool &has_seen_non_main_interpreter() {
425+
static std::atomic_bool multi(false);
426+
return multi;
426427
}
427428

428429
template <class T,
@@ -649,7 +650,7 @@ class internals_pp_manager {
649650
/// acquire the GIL. Will never return nullptr.
650651
std::unique_ptr<InternalsType> *get_pp() {
651652
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
652-
if (get_num_interpreters_seen() > 1) {
653+
if (has_seen_non_main_interpreter()) {
653654
// Whenever the interpreter changes on the current thread we need to invalidate the
654655
// internals_pp so that it can be pulled from the interpreter's state dict. That is
655656
// slow, so we use the current PyThreadState to check if it is necessary.
@@ -675,7 +676,7 @@ class internals_pp_manager {
675676
/// Drop all the references we're currently holding.
676677
void unref() {
677678
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
678-
if (get_num_interpreters_seen() > 1) {
679+
if (has_seen_non_main_interpreter()) {
679680
last_istate_tls() = nullptr;
680681
internals_p_tls() = nullptr;
681682
return;
@@ -686,7 +687,7 @@ class internals_pp_manager {
686687

687688
void destroy() {
688689
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
689-
if (get_num_interpreters_seen() > 1) {
690+
if (has_seen_non_main_interpreter()) {
690691
auto *tstate = get_thread_state_unchecked();
691692
// this could be called without an active interpreter, just use what was cached
692693
if (!tstate || tstate->interp == last_istate_tls()) {
@@ -791,6 +792,16 @@ PYBIND11_NOINLINE internals &get_internals() {
791792
return *internals_ptr;
792793
}
793794

795+
inline void ensure_internals() {
796+
pybind11::detail::get_internals_pp_manager().unref();
797+
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
798+
if (PyInterpreterState_Get() != PyInterpreterState_Main()) {
799+
has_seen_non_main_interpreter() = true;
800+
}
801+
#endif
802+
pybind11::detail::get_internals();
803+
}
804+
794805
inline internals_pp_manager<local_internals> &get_local_internals_pp_manager() {
795806
// Use the address of this static itself as part of the key, so that the value is uniquely tied
796807
// to where the module is loaded in memory

include/pybind11/embed.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
PYBIND11_WARNING_PUSH
5959
PYBIND11_WARNING_DISABLE_CLANG("-Wgnu-zero-variadic-macro-arguments")
6060
#define PYBIND11_EMBEDDED_MODULE(name, variable, ...) \
61-
PYBIND11_MODULE_PYINIT(name, {}, ##__VA_ARGS__) \
61+
PYBIND11_MODULE_PYINIT(name, ##__VA_ARGS__) \
6262
::pybind11::detail::embedded_module PYBIND11_CONCAT(pybind11_module_, name)( \
6363
PYBIND11_TOSTRING(name), PYBIND11_CONCAT(PyInit_, name)); \
6464
PYBIND11_MODULE_EXEC(name, variable)
@@ -202,7 +202,7 @@ inline void initialize_interpreter(bool init_signal_handlers = true,
202202
#endif
203203

204204
// There is exactly one interpreter alive currently.
205-
detail::get_num_interpreters_seen() = 1;
205+
detail::has_seen_non_main_interpreter() = false;
206206
}
207207

208208
/** \rst
@@ -242,12 +242,12 @@ inline void initialize_interpreter(bool init_signal_handlers = true,
242242
\endrst */
243243
inline void finalize_interpreter() {
244244
// get rid of any thread-local interpreter cache that currently exists
245-
if (detail::get_num_interpreters_seen() > 1) {
245+
if (detail::has_seen_non_main_interpreter()) {
246246
detail::get_internals_pp_manager().unref();
247247
detail::get_local_internals_pp_manager().unref();
248248

249-
// We know there can be no other interpreter alive now, so we can lower the count
250-
detail::get_num_interpreters_seen() = 1;
249+
// We know there can be no other interpreter alive now
250+
detail::has_seen_non_main_interpreter() = false;
251251
}
252252

253253
// Re-fetch the internals pointer-to-pointer (but not the internals itself, which might not
@@ -265,8 +265,8 @@ inline void finalize_interpreter() {
265265
// avoid undefined behaviors when initializing another interpreter
266266
detail::get_local_internals_pp_manager().destroy();
267267

268-
// We know there is no interpreter alive now, so we can reset the count
269-
detail::get_num_interpreters_seen() = 0;
268+
// We know there is no interpreter alive now, so we can reset the multi-flag
269+
detail::has_seen_non_main_interpreter() = false;
270270
}
271271

272272
/** \rst

include/pybind11/gil_safe_call_once.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class gil_safe_call_once_and_store {
232232
// Indicator of fast path for single-interpreter case.
233233
bool is_last_storage_valid() const {
234234
return is_initialized_by_at_least_one_interpreter_
235-
&& detail::get_num_interpreters_seen() == 1;
235+
&& !detail::has_seen_non_main_interpreter();
236236
}
237237

238238
// Get the unique key for this storage instance in the interpreter's state dict.

include/pybind11/subinterpreter.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class subinterpreter {
109109

110110
// upon success, the new interpreter is activated in this thread
111111
result.istate_ = result.creation_tstate_->interp;
112-
detail::get_num_interpreters_seen() += 1; // there are now many interpreters
112+
detail::has_seen_non_main_interpreter() = true;
113113
detail::get_internals(); // initialize internals.tstate, amongst other things...
114114

115115
// In 3.13+ this state should be deleted right away, and the memory will be reused for

tests/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,14 @@ if(NOT PYBIND11_CUDA_TESTS)
592592
foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES)
593593
set_target_properties("${mod}" PROPERTIES LIBRARY_OUTPUT_DIRECTORY
594594
"${pybind11_tests_output_directory}")
595+
# Also set config-specific output directories for multi-configuration generators (MSVC)
596+
if(DEFINED CMAKE_CONFIGURATION_TYPES)
597+
foreach(config ${CMAKE_CONFIGURATION_TYPES})
598+
string(TOUPPER ${config} config)
599+
set_target_properties("${mod}" PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${config}
600+
"${pybind11_tests_output_directory}")
601+
endforeach()
602+
endif()
595603
endforeach()
596604

597605
if(PYBIND11_TEST_SMART_HOLDER)

tests/mod_per_interpreter_gil_with_singleton.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace py = pybind11;
99
# include <pybind11/native_enum.h>
1010
#endif
1111

12+
namespace pybind11_tests {
13+
namespace mod_per_interpreter_gil_with_singleton {
1214
// A singleton class that holds references to certain Python objects
1315
// This singleton is per-interpreter using gil_safe_call_once_and_store
1416
class MySingleton {
@@ -95,11 +97,15 @@ enum class MyEnum : int {
9597
TWO = 2,
9698
THREE = 3,
9799
};
100+
} // namespace mod_per_interpreter_gil_with_singleton
101+
} // namespace pybind11_tests
98102

99103
PYBIND11_MODULE(mod_per_interpreter_gil_with_singleton,
100104
m,
101105
py::mod_gil_not_used(),
102106
py::multiple_interpreters::per_interpreter_gil()) {
107+
using namespace pybind11_tests::mod_per_interpreter_gil_with_singleton;
108+
103109
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
104110
m.attr("defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT") = true;
105111
#else

tests/test_multiple_interpreters.py

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def test_independent_subinterpreters():
9090

9191
run_string, create = get_interpreters(modern=True)
9292

93-
m = pytest.importorskip("mod_per_interpreter_gil")
93+
import mod_per_interpreter_gil as m
9494

9595
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
9696
pytest.skip("Does not have subinterpreter support compiled in")
@@ -139,7 +139,7 @@ def test_independent_subinterpreters_modern():
139139

140140
sys.path.insert(0, os.path.dirname(pybind11_tests.__file__))
141141

142-
m = pytest.importorskip("mod_per_interpreter_gil")
142+
import mod_per_interpreter_gil as m
143143

144144
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
145145
pytest.skip("Does not have subinterpreter support compiled in")
@@ -187,7 +187,7 @@ def test_dependent_subinterpreters():
187187

188188
run_string, create = get_interpreters(modern=False)
189189

190-
m = pytest.importorskip("mod_shared_interpreter_gil")
190+
import mod_shared_interpreter_gil as m
191191

192192
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
193193
pytest.skip("Does not have subinterpreter support compiled in")
@@ -222,15 +222,27 @@ def test():
222222
223223
objects = m.get_objects_in_singleton()
224224
expected = [
225-
type(None),
226-
tuple,
227-
list,
228-
dict,
229-
collections.OrderedDict,
230-
collections.defaultdict,
231-
collections.deque,
225+
type(None), # static type: shared between interpreters
226+
tuple, # static type: shared between interpreters
227+
list, # static type: shared between interpreters
228+
dict, # static type: shared between interpreters
229+
collections.OrderedDict, # static type: shared between interpreters
230+
collections.defaultdict, # heap type: dynamically created per interpreter
231+
collections.deque, # heap type: dynamically created per interpreter
232232
]
233-
assert objects == expected, f"Expected {{expected!r}}, got {{objects!r}}."
233+
# Check that we have the expected objects. Avoid IndexError by checking lengths first.
234+
assert len(objects) == len(expected), (
235+
f"Expected {{expected!r}} ({{len(expected)}}), got {{objects!r}} ({{len(objects)}})."
236+
)
237+
# The first ones are static types shared between interpreters.
238+
assert objects[:-2] == expected[:-2], (
239+
f"Expected static objects {{expected[:-2]!r}}, got {{objects[:-2]!r}}."
240+
)
241+
# The last two are heap types created per-interpreter.
242+
# The expected objects are dynamically imported from `collections`.
243+
assert objects[-2:] == expected[-2:], (
244+
f"Expected heap objects {{expected[-2:]!r}}, got {{objects[-2:]!r}}."
245+
)
234246
235247
assert hasattr(m, 'MyClass'), "Module missing MyClass"
236248
assert hasattr(m, 'MyGlobalError'), "Module missing MyGlobalError"
@@ -240,11 +252,6 @@ def test():
240252
).lstrip()
241253

242254

243-
@pytest.mark.xfail(
244-
reason="Duplicate C++ type registration under multiple-interpreters, needs investigation.",
245-
# raises=interpreters.ExecutionFailed, # need to import the module
246-
strict=False,
247-
)
248255
@pytest.mark.skipif(
249256
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
250257
)
@@ -278,14 +285,9 @@ def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
278285
f"```\n\n"
279286
f"Output:\n"
280287
f"{ex.output}"
281-
) from ex
288+
) from None
282289

283290

284-
@pytest.mark.xfail(
285-
reason="Duplicate C++ type registration under multiple-interpreters, needs investigation.",
286-
raises=RuntimeError,
287-
strict=False,
288-
)
289291
@pytest.mark.skipif(
290292
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
291293
)
@@ -342,11 +344,6 @@ def test_import_in_subinterpreter_after_main():
342344
)
343345

344346

345-
@pytest.mark.xfail(
346-
reason="Duplicate C++ type registration under multiple-interpreters, needs investigation.",
347-
raises=RuntimeError,
348-
strict=False,
349-
)
350347
@pytest.mark.skipif(
351348
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
352349
)
@@ -427,11 +424,6 @@ def test_import_in_subinterpreter_before_main():
427424
)
428425

429426

430-
@pytest.mark.xfail(
431-
reason="Duplicate C++ type registration under multiple-interpreters, needs investigation.",
432-
raises=RuntimeError,
433-
strict=False,
434-
)
435427
@pytest.mark.skipif(
436428
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
437429
)

tests/test_with_catch/test_subinterpreter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ void unsafe_reset_internals_for_single_interpreter() {
3131
py::detail::get_local_internals_pp_manager().unref();
3232

3333
// we know there are no other interpreters, so we can lower this. SUPER DANGEROUS
34-
py::detail::get_num_interpreters_seen() = 1;
34+
py::detail::has_seen_non_main_interpreter() = false;
3535

3636
// now we unref the static global singleton internals
3737
py::detail::get_internals_pp_manager().unref();

0 commit comments

Comments
 (0)