Skip to content

Commit 7c22acc

Browse files
yoneymeta-codesync[bot]
authored andcommitted
Serialize JIT entrypoints in free-threaded builds
Summary: Add a minimal top-level guard for JIT entrypoints in free-threaded builds. The goal is to make the current FT test path reliable by serializing top-level registration, scheduling, compilation, and teardown paths that can now race without the GIL. A more integrated alternative would be to extend `ThreadedCompileSerialize` to cover these FT entrypoints too, but that is a larger change. In particular, `preload` can re-enter other JIT paths, so folding it into the existing threaded-compile locking model needs more restructuring to avoid lock-ordering problems. This change is FT-specific and should not change GIL builds. In non-FT builds the new guard is a no-op and should optimize away. Reviewed By: jbower-fb, DinoV Differential Revision: D99853055 fbshipit-source-id: 546f122605b307337f2295fc2c43527d8ee2dfd0
1 parent 1fa50ee commit 7c22acc

3 files changed

Lines changed: 57 additions & 0 deletions

File tree

cinderx/Jit/context.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ namespace jit {
2424

2525
AotContext g_aot_ctx;
2626

27+
#ifdef Py_GIL_DISABLED
28+
std::recursive_mutex& freeThreadedJITEntrypointMutex() {
29+
static std::recursive_mutex mutex;
30+
return mutex;
31+
}
32+
#endif
33+
2734
PyObject* yieldFromValue(
2835
GenDataFooter* gen_footer,
2936
const GenYieldPoint* yield_point) {
@@ -592,6 +599,9 @@ void Context::forgetCode(BorrowedRef<PyFunctionObject> func) {
592599
}
593600

594601
void Context::forgetCompiledFunction(CompiledFunction& function) {
602+
// tp_clear() can reach here from GC without going through a guarded
603+
// top-level JIT entrypoint, so this path has to take the FT guard itself.
604+
FreeThreadedJITEntrypointGuard guard;
595605
if (function.runtime() != nullptr) {
596606
for (auto pyfunc : function.functions()) {
597607
compiled_funcs_.erase(pyfunc);

cinderx/Jit/context.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,37 @@
3535

3636
namespace jit {
3737

38+
#ifdef Py_GIL_DISABLED
39+
std::recursive_mutex& freeThreadedJITEntrypointMutex();
40+
#endif
41+
42+
// Free-threaded builds can enter top-level JIT operations concurrently:
43+
// function/code registration, compilation, and destruction hooks.
44+
// Use a dedicated lock instead of ThreadedCompileSerialize, which is acquired
45+
// deeper in the threaded-compile internals.
46+
class FreeThreadedJITEntrypointGuard {
47+
public:
48+
FreeThreadedJITEntrypointGuard() {
49+
#ifdef Py_GIL_DISABLED
50+
freeThreadedJITEntrypointMutex().lock();
51+
#endif
52+
}
53+
54+
~FreeThreadedJITEntrypointGuard() {
55+
#ifdef Py_GIL_DISABLED
56+
freeThreadedJITEntrypointMutex().unlock();
57+
#endif
58+
}
59+
60+
FreeThreadedJITEntrypointGuard(const FreeThreadedJITEntrypointGuard&) =
61+
delete;
62+
FreeThreadedJITEntrypointGuard& operator=(
63+
const FreeThreadedJITEntrypointGuard&) = delete;
64+
FreeThreadedJITEntrypointGuard(FreeThreadedJITEntrypointGuard&&) = delete;
65+
FreeThreadedJITEntrypointGuard& operator=(FreeThreadedJITEntrypointGuard&&) =
66+
delete;
67+
};
68+
3869
#if PY_VERSION_HEX < 0x030C0000
3970
// Memory management functions for JIT generator data.
4071
// In 3.12+ there is no gen->gi_jit_data and this functionality is part of

cinderx/Jit/pyjit.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,7 @@ void multithread_compile_units_preloaded(
10321032
// Compile all functions registered via a JIT list that haven't been executed
10331033
// yet.
10341034
bool compile_all(size_t workers = 0) {
1035+
FreeThreadedJITEntrypointGuard guard;
10351036
JIT_CHECK(jitCtx(), "JIT not initialized");
10361037

10371038
if (workers == 0) {
@@ -1227,6 +1228,8 @@ std::vector<BorrowedRef<PyCodeObject>> findNestedCodes(
12271228
// Return true if the function is registered with JIT or is already compiled,
12281229
// and false otherwise.
12291230
bool registerFunction(BorrowedRef<PyFunctionObject> func) {
1231+
FreeThreadedJITEntrypointGuard guard;
1232+
12301233
// Attempt to attach already-compiled code even if the JIT is disabled, as
12311234
// long as it hasn't been finalized.
12321235
if (reoptFunc(func)) {
@@ -1251,6 +1254,7 @@ bool registerFunction(BorrowedRef<PyFunctionObject> func) {
12511254
}
12521255

12531256
PyObject* multithreaded_compile_test(PyObject*, PyObject*) {
1257+
FreeThreadedJITEntrypointGuard guard;
12541258
if (!getConfig().multithreaded_compile_test) {
12551259
PyErr_SetString(
12561260
PyExc_NotImplementedError, "multithreaded_compile_test not enabled");
@@ -1328,6 +1332,7 @@ bool deoptFunc(BorrowedRef<PyFunctionObject> func) {
13281332
}
13291333

13301334
void disable_jit_impl(bool deopt_all) {
1335+
FreeThreadedJITEntrypointGuard guard;
13311336
if (jitCtx() == nullptr) {
13321337
return;
13331338
}
@@ -1373,6 +1378,7 @@ PyObject* disable_jit(PyObject* /* self */, PyObject* args, PyObject* kwargs) {
13731378
}
13741379

13751380
bool enable_jit_impl() {
1381+
FreeThreadedJITEntrypointGuard guard;
13761382
if (jitCtx() == nullptr) {
13771383
PyErr_SetString(
13781384
PyExc_RuntimeError,
@@ -1451,6 +1457,7 @@ bool isInstrumentationActive() {
14511457

14521458
// Returns false only if enable_jit_impl() fails (with Python exception set).
14531459
bool toggleJitBasedOnInstrumentationState() {
1460+
FreeThreadedJITEntrypointGuard guard;
14541461
if (isInstrumentationActive()) {
14551462
disable_jit_impl(true /* deopt_all */);
14561463
return true;
@@ -1723,6 +1730,7 @@ PyObject* lazy_compile(PyObject* /* self */, PyObject* arg) {
17231730
if (func == nullptr) {
17241731
return nullptr;
17251732
}
1733+
FreeThreadedJITEntrypointGuard guard;
17261734

17271735
if (!isJitUsable() || isJitCompiled(func)) {
17281736
Py_RETURN_FALSE;
@@ -1747,6 +1755,7 @@ PyObject* force_uncompile(PyObject* /* self */, PyObject* arg) {
17471755
if (func == nullptr) {
17481756
return nullptr;
17491757
}
1758+
FreeThreadedJITEntrypointGuard guard;
17501759

17511760
if (!isJitCompiled(func)) {
17521761
Py_RETURN_FALSE;
@@ -3646,6 +3655,7 @@ int initialize() {
36463655
}
36473656

36483657
void finalize() {
3658+
FreeThreadedJITEntrypointGuard guard;
36493659
if (!isJitInitialized()) {
36503660
return;
36513661
}
@@ -3717,6 +3727,8 @@ bool shouldScheduleCompile(BorrowedRef<PyFunctionObject> func) {
37173727
}
37183728

37193729
bool scheduleJitCompile(BorrowedRef<PyFunctionObject> func) {
3730+
FreeThreadedJITEntrypointGuard guard;
3731+
37203732
auto eligible = getCompilationEligibility(func);
37213733
if (eligible == JitEligibility::Ineligible) {
37223734
return false;
@@ -3760,6 +3772,7 @@ bool scheduleJitCompile(BorrowedRef<PyFunctionObject> func) {
37603772
}
37613773

37623774
Result compileFunction(BorrowedRef<PyFunctionObject> func) {
3775+
FreeThreadedJITEntrypointGuard guard;
37633776
if (!isJitInitialized()) {
37643777
return Result::NOT_INITIALIZED;
37653778
}
@@ -3855,6 +3868,7 @@ std::vector<BorrowedRef<PyFunctionObject>> preloadFuncAndDeps(
38553868
}
38563869

38573870
void codeDestroyed(BorrowedRef<PyCodeObject> code) {
3871+
FreeThreadedJITEntrypointGuard guard;
38583872
if (isJitUsable()) {
38593873
auto mod_state = cinderx::getModuleState();
38603874
if (!mod_state) {
@@ -3874,6 +3888,7 @@ void funcDestroyed(BorrowedRef<PyFunctionObject> func) {
38743888
if (!mod_state) {
38753889
return;
38763890
}
3891+
FreeThreadedJITEntrypointGuard guard;
38773892

38783893
unregisterFunctionCodes(func);
38793894

@@ -3888,6 +3903,7 @@ void funcDestroyed(BorrowedRef<PyFunctionObject> func) {
38883903
}
38893904

38903905
void funcModified(BorrowedRef<PyFunctionObject> func) {
3906+
FreeThreadedJITEntrypointGuard guard;
38913907
deoptFunc(func);
38923908
// Clean up registrations for the old code object. At this point
38933909
// func->func_code still refers to the old code. The caller will update

0 commit comments

Comments
 (0)