Acquire the GIL when releasing NumPy-backed Core ML inputs - #2829
Open
cdeil wants to merge 1 commit into
Open
Conversation
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PybindCompatibleArrayMLMultiArrayinput path and its lifetime guaranteeProblem
PybindCompatibleArraycreates anMLMultiArrayoverarray.mutable_data()withdeallocator:niland retains the NumPy storage owner in the C++ memberpy::array m_array.Core ML may retain that feature after synchronous
predict()returns and release it later on a private runtime queue. On macOS 26.6.2 and macOS 27 beta, the observed queue was:When Objective-C destroys
PybindCompatibleArrayon that queue, the implicit C++ member destructor decrements thepy::arrayreference without an attached Python thread state or the GIL. A debug pybind11 build reports the violation directly:With a release wheel and Python 3.12 through 3.14, a one-operation
output = input + 1ML Program predicts correctly and then segfaults asynchronously. The native stack is:Conversion is not involved in the failure: loading a model package generated by a separate process has the same result.
Why Python 3.12+ exposes it
This is a binding lifetime bug rather than a CPython allocator bug. Python's C API requires an attached thread state and the GIL before accessing Python objects or changing their reference counts.
Python 3.11's
_PyObject_Freehappened to proceed directly topymalloc_free. Python 3.12+ first obtains per-interpreter allocator state through_PyInterpreterState_GET(). The Core ML reset queue has no Python thread state, so the invalid foreign-thread decrement now reliably faults. The contract violation exists regardless of whether a particular Python/runtime combination happens to crash.Related documentation:
Fix
The explicit
deallocmethod:m_array.release()to detach the raw Python handle without decrementing it;py::gil_scoped_acquire;After
deallocreturns, the implicit C++ destruction sees an emptym_array, so there is no second Python reference-count operation.This is deliberately scoped to ownership release. It preserves the existing zero-copy input behavior and keeps the NumPy allocation alive for as long as Core ML uses its data pointer; it neither copies tensor data nor assumes that Core ML will destroy features on the prediction thread.
Regression test
The test creates a one-operation FP32 ML Program and predicts from a NumPy array in a child process. A weakref callback records
PyGILState_Check()when Core ML releases the last owner. It verifies both that:The subprocess is important because the unfixed behavior can abort or segfault the interpreter. Before this patch, the parent test reports the child process's signal and native pybind11 diagnostic instead of terminating the full pytest worker.
The test lives under
coremltools.test, so it is included by the existing macOS package-test job. Its explicit GIL check also exercises Python 3.10 CI without depending on the Python 3.12 allocator crash.Validation
Host: Apple Silicon M3, macOS 26.6.2 build 25G83, Xcode 26.6.
-6with pybind11's off-GILdec_ref()assertion