Skip to content

Commit 235176b

Browse files
yoneymeta-codesync[bot]
authored andcommitted
Create JIT worker thread states on their owning threads
Summary: Create compile-worker PyThreadState objects inside their worker threads so free-threaded runtime state is initialized on the correct thread. Reviewed By: alexmalyshev Differential Revision: D116652507 fbshipit-source-id: 366b8615182d81f41b75166689e975b980d3fa40
1 parent 6107150 commit 235176b

1 file changed

Lines changed: 35 additions & 90 deletions

File tree

cinderx/Jit/pyjit.cpp

Lines changed: 35 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -77,53 +77,6 @@ TaggedPyObject tagIfDeferred(PyObject* obj);
7777

7878
namespace {
7979

80-
// RAII wrapper for PyThreadState* similar to Ref<>.
81-
// Owns a PyThreadState created via PyThreadState_New and frees it via
82-
// Clear + Delete/DeleteCurrent. Move-only, like Ref.
83-
class PyThreadStateHandle {
84-
public:
85-
explicit PyThreadStateHandle(PyThreadState* tstate) : ptr_{tstate} {}
86-
87-
~PyThreadStateHandle() {
88-
reset(nullptr);
89-
}
90-
91-
PyThreadStateHandle(PyThreadStateHandle&& other) noexcept : ptr_{other.ptr_} {
92-
other.ptr_ = nullptr;
93-
}
94-
95-
PyThreadStateHandle(const PyThreadStateHandle&) = delete;
96-
PyThreadStateHandle& operator=(const PyThreadStateHandle&) = delete;
97-
PyThreadStateHandle& operator=(PyThreadStateHandle&& other) noexcept {
98-
reset(nullptr);
99-
ptr_ = other.ptr_;
100-
other.ptr_ = nullptr;
101-
return *this;
102-
}
103-
104-
PyThreadState* release() {
105-
PyThreadState* tmp = ptr_;
106-
ptr_ = nullptr;
107-
return tmp;
108-
}
109-
110-
void reset(PyThreadState* tstate = nullptr) {
111-
if (ptr_ != nullptr) {
112-
// Caller must hold GIL when freeing a threadstate.
113-
PyThreadState_Clear(ptr_);
114-
if (ptr_ == PyThreadState_Get()) {
115-
PyThreadState_DeleteCurrent();
116-
} else {
117-
PyThreadState_Delete(ptr_);
118-
}
119-
}
120-
ptr_ = tstate;
121-
}
122-
123-
private:
124-
PyThreadState* ptr_{nullptr};
125-
};
126-
12780
// RAII equivalent of the Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS macro
12881
// pair. Saves and releases the current thread state on construction, and
12982
// restores it (reacquiring the GIL) on destruction.
@@ -993,6 +946,16 @@ std::string unitFullname(BorrowedRef<> unit) {
993946
return codeFullname(iter->second->func_module, code);
994947
}
995948

949+
PyThreadState* acquireCompileWorkerThreadState(PyInterpreterState* interp) {
950+
// Create the state on its owning worker so thread-local runtime state is
951+
// initialized for the thread that uses it. Passing the interpreter avoids
952+
// the subinterpreter-unsafe PyGILState_Ensure().
953+
PyThreadState* tstate = PyThreadState_New(interp);
954+
JIT_THROW_IF(tstate == nullptr, "Failed to allocate worker thread state");
955+
PyEval_AcquireThread(tstate);
956+
return tstate;
957+
}
958+
996959
// Load the preloader for a given function or code object. If it doesn't exist
997960
// yet, then preload the function and return the new preloader.
998961
//
@@ -1101,18 +1064,18 @@ std::pair<Result, Ref<>> tryCompilePreloaded(Ref<>&& unit) {
11011064
}
11021065

11031066
void compile_worker_thread(
1104-
PyThreadState* initial_tstate,
1067+
PyInterpreterState* interp,
11051068
std::shared_ptr<ThreadedCompileContext> context,
11061069
std::shared_ptr<hir::IsolatedPreloaders> isolated) {
11071070
JIT_DLOG("Started compile worker in thread {}", std::this_thread::get_id());
11081071

1072+
PyThreadState* tstate = acquireCompileWorkerThreadState(interp);
1073+
11091074
// Publish the context and isolated preload manager via TLS so that
11101075
// compileRunning()/preloaderManager() work on this worker.
11111076
hir::setThreadLocalPreloaderManager(isolated->manager());
11121077

1113-
// Acquire GIL with the pre-created worker threadstate.
1114-
PyEval_AcquireThread(initial_tstate);
1115-
context->beginWorker(initial_tstate);
1078+
context->beginWorker(tstate);
11161079

11171080
{
11181081
// Release the GIL for the lifetime of this scope, saving our tstate so it
@@ -1158,15 +1121,16 @@ void compile_worker_thread(
11581121
attempts,
11591122
retries);
11601123
}
1161-
// GIL reacquired here, restoring threaded_compile_tstate as current.
1124+
// Leaving the scope reattached the worker state and reacquired the GIL when
1125+
// it is enabled.
11621126
context->endWorker();
11631127

11641128
// Clear TLS for preload manager (context TLS already cleared by endWorker).
11651129
hir::setThreadLocalPreloaderManager(nullptr);
11661130

1167-
// Now we hold GIL again with initial_tstate as current.
1168-
// Clean up the worker threadstate.
1169-
PyThreadState_Clear(PyThreadState_Get());
1131+
// The worker state is current and attached, so it can be cleared and
1132+
// deleted by its owning thread.
1133+
PyThreadState_Clear(tstate);
11701134
PyThreadState_DeleteCurrent();
11711135
}
11721136

@@ -1193,34 +1157,20 @@ bool multithread_compile_units_preloaded(
11931157
// kept alive by the workers
11941158
auto compilation = std::make_shared<ThreadedCompileContext>(std::move(units));
11951159

1196-
// Pre-create a PyThreadState for each worker while holding the GIL.
1197-
// This avoids needing the GIL to be held inside the worker just to call
1198-
// PyThreadState_New, and lets workers block on GIL acquisition until we
1199-
// release it. Use RAII handle similar to Ref<> for exception safety.
12001160
PyInterpreterState* interp = ThreadedCompileContext::interpreter();
1201-
std::vector<PyThreadStateHandle> worker_tstates;
1202-
worker_tstates.reserve(worker_count);
1203-
for (size_t i = 0; i < worker_count; i++) {
1204-
auto* tstate = PyThreadState_New(interp);
1205-
JIT_CHECK(tstate != nullptr, "Failed to allocate thread state");
1206-
worker_tstates.emplace_back(tstate);
1207-
}
12081161

12091162
// Track the worker threads on the module state so the runtime can wait for
12101163
// them to finish (e.g. during finalization) if we don't get to join them
12111164
// here.
12121165
auto* mod_state = cinderx::getModuleState();
12131166
std::vector<std::thread>& worker_threads = mod_state->compile_worker_threads;
1214-
for (auto& worker_tstate : worker_tstates) {
1215-
// Transfer ownership to the worker thread via release().
1216-
PyThreadState* raw = worker_tstate.release();
1167+
for (size_t i = 0; i < worker_count; i++) {
12171168
worker_threads.emplace_back(
1218-
compile_worker_thread, raw, compilation, isolated);
1169+
compile_worker_thread, interp, compilation, isolated);
12191170
}
1220-
// Ensure that no worker threads start compiling until they are all created,
1221-
// in case something else in the process has hooked thread creation to run
1222-
// arbitrary code (the worker threads need the GIL to initialize their thread
1223-
// state).
1171+
// Keep the coordinator attached until every worker is launched in case
1172+
// thread creation has been hooked to run arbitrary code. In GIL builds,
1173+
// workers wait in PyEval_AcquireThread() until this point.
12241174
compilation->releaseGil();
12251175

12261176
mod_state->joinCompileWorkers();
@@ -3777,7 +3727,7 @@ void processBackgroundCompile(
37773727
// drained.
37783728
void backgroundCompileWorkerLoop(
37793729
CompilerContext<Compiler>* jit_ctx,
3780-
PyThreadState* initial_tstate) {
3730+
PyInterpreterState* interp) {
37813731
JIT_DLOG(
37823732
"Background compile worker thread started: {}",
37833733
std::this_thread::get_id());
@@ -3788,7 +3738,7 @@ void backgroundCompileWorkerLoop(
37883738
#endif
37893739
"cinderx_compile");
37903740
#endif
3791-
PyEval_AcquireThread(initial_tstate);
3741+
PyThreadState* tstate = acquireCompileWorkerThreadState(interp);
37923742

37933743
ThreadedCompileContext bgContext;
37943744
BackgroundCompileRegistry& reg = jit_ctx->backgroundCompileRegistry();
@@ -3819,9 +3769,9 @@ void backgroundCompileWorkerLoop(
38193769
finishBackgroundCompile(task->code);
38203770
}
38213771

3822-
// Now we hold GIL again with initial_tstate as current.
3823-
// Clean up the worker threadstate.
3824-
PyThreadState_Clear(PyThreadState_Get());
3772+
// The worker state is current and attached, so it can be cleared and
3773+
// deleted by its owning thread.
3774+
PyThreadState_Clear(tstate);
38253775
PyThreadState_DeleteCurrent();
38263776

38273777
JIT_DLOG(
@@ -3837,27 +3787,22 @@ bool startBackgroundWorkerThread(
38373787
// _PyInterpreterState_GET()) while the GIL is still held.
38383788
jit_ctx->builtins();
38393789

3840-
// Create a dedicated PyThreadState for the background worker while
3841-
// holding the GIL, so the worker can attach via PyEval_AcquireThread
3842-
// instead of the sub-interpreter-unsafe PyGILState_Ensure.
3790+
// The worker creates its own thread state, because PyThreadState_New() binds
3791+
// mimalloc and biased-reference-counting state to the calling thread. The
3792+
// interpreter is guaranteed to still be alive when it does: reg.worker is
3793+
// published under reg.mutex here, and cancelBackgroundCompiles() joins it
3794+
// before the interpreter is torn down.
38433795
PyInterpreterState* interp = PyInterpreterState_Get();
3844-
PyThreadState* tstate = PyThreadState_New(interp);
3845-
if (tstate == nullptr) {
3846-
throw CAPIError();
3847-
}
38483796

38493797
// Normally already registered by jit::initialize(), but re-check here since
38503798
// the worker is the widest source of locks held across a fork.
38513799
ensureForkHandlersRegistered();
38523800

38533801
try {
3854-
reg.worker = std::thread(backgroundCompileWorkerLoop, jit_ctx, tstate);
3802+
reg.worker = std::thread(backgroundCompileWorkerLoop, jit_ctx, interp);
38553803
reg.worker_started = true;
38563804
} catch (const std::system_error& exn) {
38573805
JIT_LOG("Failed to start background compile worker: {}", exn.what());
3858-
// Clean up the thread state we created for the worker.
3859-
PyThreadState_Clear(tstate);
3860-
PyThreadState_Delete(tstate);
38613806
return false;
38623807
}
38633808
return true;

0 commit comments

Comments
 (0)