Skip to content

Commit db8e41d

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Track all code objects that can potentially be compiled
Summary: For the CompiledFunction's lifetime to be tied to the lifetime of a function we need to be careful with how we deal with nested functions. If we have a nested function which just gets compiled and thrown away we need to keep the nested `CompiledFunction` alive somewhere otherwise we'll free the `CompiledFunction` and just re-compile it the next time it gets called. This updates our tracking of functions so that we track all nested functions and their outer function. This means breaking out scheduling and whether or not a function is JIT eligible as we effectively need to schedule every funtion. Reviewed By: alexmalyshev Differential Revision: D92468056 fbshipit-source-id: 369252600dfe3e3fbe4267e356c909e104e38aaa
1 parent 659606a commit db8e41d

3 files changed

Lines changed: 196 additions & 97 deletions

File tree

cinderx/Jit/pyjit.cpp

Lines changed: 195 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ UnitDeletedCallback handle_unit_deleted_during_preload = nullptr;
9797
std::atomic<int> g_compile_workers_attempted;
9898
std::atomic<int> g_compile_workers_retries;
9999

100+
// Don't care flags: CO_NOFREE, CO_FUTURE_* (the only still-relevant future is
101+
// "annotations" which doesn't impact bytecode execution.)
102+
constexpr int required_code_flags = CO_OPTIMIZED | CO_NEWLOCALS;
103+
bool hasRequiredFlags(BorrowedRef<PyCodeObject> code) {
104+
return (code->co_flags & required_code_flags) == required_code_flags;
105+
}
106+
100107
uint64_t countCalls(PyCodeObject* code) {
101108
#if SHADOWCODE_SUPPORTED
102109
return code->co_mutable->ncalls;
@@ -1087,6 +1094,89 @@ bool compile_all(size_t workers = 0) {
10871094
return true;
10881095
}
10891096

1097+
// Gets the eligibility for code or a function to be compiled. A function
1098+
// can be ineligible, eligible due to the JIT list, or if there's no
1099+
// jit list then just eligible. This is used to support handling nested
1100+
// functions in the cases of multi-threaded compile / JIT list and without.
1101+
//
1102+
// In multi-threaded compile w/ a JIT list: We need to track the nested code
1103+
// objects in jit_reg_units for when the multi-threaded compile kicks in and we
1104+
// may not have created any functions yet. But we don't need that if we're not
1105+
// doing multi-threaded compile, we'll only compile nested functions when a
1106+
// function gets called. So that's why we track this as an extra state.
1107+
//
1108+
// In both cases we always need to track the outer function so that we don't
1109+
// repeatedly re-compile nested functions - which is the big change here. That's
1110+
// the processing that we were previously only doing when we had a JIT list so
1111+
// now we're just skipping the jit_reg_units case when we're doing this for the
1112+
// non-JIT list/multi-threaded compile case.
1113+
enum class JitEligibility { Ineligible, JitListEligible, Eligible };
1114+
1115+
/*
1116+
* Check for a functions eligibility to be compiled.
1117+
*
1118+
* This is the most broad definition of eligibility - that is it will only
1119+
* return Ineligible for functions which are specifically not allowed to
1120+
* be compiled for one reason or another.
1121+
*
1122+
* This doesn't guarantee that the function can or will be compiled, it just
1123+
* checks if the JIT has been configured in such a way that compilation is
1124+
* possible.
1125+
*/
1126+
JitEligibility getCompilationEligibility(BorrowedRef<PyFunctionObject> func) {
1127+
// Can be called after the module has been finalized, due to function events.
1128+
if (jitCtx() == nullptr || isCinderModule(func->func_module)) {
1129+
return JitEligibility::Ineligible;
1130+
}
1131+
1132+
BorrowedRef<PyCodeObject> code{func->func_code};
1133+
if (!hasRequiredFlags(code)) {
1134+
return JitEligibility::Ineligible;
1135+
}
1136+
1137+
// Note: This is not the same as fetching the function's code object and
1138+
// checking its module and qualname, as functions can be renamed after they
1139+
// are created. Code objects cannot.
1140+
if (auto jit_list = cinderx::getModuleState()->jitList()) {
1141+
if (jit_list->lookupFunc(func) == 1) {
1142+
return JitEligibility::JitListEligible;
1143+
}
1144+
return JitEligibility::Ineligible;
1145+
}
1146+
1147+
return JitEligibility::Eligible;
1148+
}
1149+
1150+
/*
1151+
* Variant of getCompilationEligibility() for nested code objects.
1152+
*/
1153+
JitEligibility getCompilationEligibility(
1154+
BorrowedRef<> module_name,
1155+
BorrowedRef<PyCodeObject> code) {
1156+
// Can be called after the module has been finalized, due to function events.
1157+
if (jitCtx() == nullptr) {
1158+
return JitEligibility::Ineligible;
1159+
}
1160+
1161+
if (isCinderModule(module_name)) {
1162+
return JitEligibility::Ineligible;
1163+
}
1164+
1165+
if (!hasRequiredFlags(code)) {
1166+
return JitEligibility::Ineligible;
1167+
}
1168+
1169+
if (auto jit_list = cinderx::getModuleState()->jitList()) {
1170+
if (jit_list->lookupCode(code) == 1 ||
1171+
jit_list->lookupName(module_name, code->co_qualname) == 1) {
1172+
return JitEligibility::JitListEligible;
1173+
}
1174+
return JitEligibility::Ineligible;
1175+
}
1176+
1177+
return JitEligibility::Eligible;
1178+
}
1179+
10901180
// Recursively search the given co_consts tuple for any code objects that are
10911181
// on the current jit-list, using the given module name to form a
10921182
// fully-qualified function name.
@@ -1106,7 +1196,8 @@ std::vector<BorrowedRef<PyCodeObject>> findNestedCodes(
11061196
BorrowedRef<PyCodeObject> code = PyTuple_GET_ITEM(consts, i);
11071197
if (!PyCode_Check(code) || !visited.insert(code).second ||
11081198
code->co_qualname == nullptr ||
1109-
!shouldScheduleCompile(module, code)) {
1199+
getCompilationEligibility(module, code) ==
1200+
JitEligibility::Ineligible) {
11101201
continue;
11111202
}
11121203

@@ -1151,23 +1242,6 @@ bool registerFunction(BorrowedRef<PyFunctionObject> func) {
11511242
auto& jit_reg_units = cinderx::getModuleState()->registeredCompilationUnits();
11521243
jit_reg_units.emplace(func.getObj());
11531244

1154-
// Map this function's code object to itself.
1155-
auto& jit_code_outer_funcs = jitCtx()->codeOuterFunctions();
1156-
BorrowedRef<PyCodeObject> func_code{func->func_code};
1157-
jit_code_outer_funcs.emplace(func_code, func);
1158-
1159-
// If we have an active jit-list, scan this function's code object for any
1160-
// nested functions that might be on the jit-list, and register them as well.
1161-
if (cinderx::getModuleState()->jitList() != nullptr) {
1162-
PyObject* module = func->func_module;
1163-
BorrowedRef<PyCodeObject> top_code{func->func_code};
1164-
BorrowedRef<> top_consts{top_code->co_consts};
1165-
for (BorrowedRef<PyCodeObject> code : findNestedCodes(module, top_consts)) {
1166-
jit_reg_units.emplace(code.getObj());
1167-
jit_code_outer_funcs.emplace(code, func);
1168-
}
1169-
}
1170-
11711245
return true;
11721246
}
11731247

@@ -2952,6 +3026,39 @@ PyModuleDef jit_module = {
29523026
nullptr, /* m_free */
29533027
};
29543028

3029+
void trackEligibleCodeObjects(
3030+
BorrowedRef<PyFunctionObject> func,
3031+
BorrowedRef<PyCodeObject> func_code,
3032+
JitEligibility eligibility = JitEligibility::Eligible) {
3033+
// We need to maintain a mapping for all functions which are
3034+
// eligible for compilation at some point - we track the code
3035+
// object and their parent function.
3036+
// Map this function's code object to itself.
3037+
auto& jit_code_outer_funcs = jitCtx()->codeOuterFunctions();
3038+
if (jit_code_outer_funcs.contains(func_code)) {
3039+
// already registered this code
3040+
return;
3041+
}
3042+
3043+
auto& jit_reg_units = cinderx::getModuleState()->registeredCompilationUnits();
3044+
3045+
jit_code_outer_funcs.try_emplace(func_code, func);
3046+
3047+
// Scan this function's code object for any nested functions that
3048+
// might be compiled
3049+
PyObject* module = func->func_module;
3050+
BorrowedRef<> top_consts{func_code->co_consts};
3051+
for (BorrowedRef<PyCodeObject> code : findNestedCodes(module, top_consts)) {
3052+
if (jit_code_outer_funcs.contains(code)) {
3053+
continue;
3054+
}
3055+
jit_code_outer_funcs.emplace(code, func);
3056+
if (eligibility == JitEligibility::JitListEligible) {
3057+
jit_reg_units.emplace(code.getObj());
3058+
}
3059+
}
3060+
}
3061+
29553062
// Preload a function and its dependencies, then compile them all.
29563063
//
29573064
// Failing to compile a dependent function is a soft failure, and is ignored.
@@ -2960,10 +3067,13 @@ _PyJIT_Result compile_func(BorrowedRef<PyFunctionObject> func) {
29603067
// jitable function, resulting in a single-function compile
29613068
hir::IsolatedPreloaders ip;
29623069

2963-
// Ensure the function's code object is mapped to itself.
2964-
auto& jit_code_outer_funcs = jitCtx()->codeOuterFunctions();
2965-
BorrowedRef<PyCodeObject> func_code{func->func_code};
2966-
jit_code_outer_funcs.emplace(func_code, func);
3070+
// We generally track function objects when they are created. But we may need
3071+
// to re-track here. A function can have nested functions and those nested
3072+
// functions can out-live the function that created them. When the outer
3073+
// function is destroyed we need to remove the dangling registrations in
3074+
// codeOuterFunctions. We will treat whatever remains as new top-level
3075+
// functions.
3076+
trackEligibleCodeObjects(func, func->func_code);
29673077

29683078
// Collect a list of functions to compile. If it's empty then there must have
29693079
// been a Python error during preloading.
@@ -3128,6 +3238,46 @@ constexpr std::string_view getCpuArchName() {
31283238
#endif
31293239
}
31303240

3241+
// Unregister a function and its nested code objects from jit_reg_units and
3242+
// jit_code_outer_funcs. Called when a function is destroyed or its code object
3243+
// is being replaced.
3244+
void unregisterFunctionCodes(BorrowedRef<PyFunctionObject> func) {
3245+
if (!jitCtx()) {
3246+
return;
3247+
}
3248+
auto mod_state = cinderx::getModuleState();
3249+
if (!mod_state) {
3250+
return;
3251+
}
3252+
3253+
auto& jit_reg_units = mod_state->registeredCompilationUnits();
3254+
auto& jit_code_outer_funcs = jitCtx()->codeOuterFunctions();
3255+
3256+
BorrowedRef<PyCodeObject> top_code{func->func_code};
3257+
auto it = jit_code_outer_funcs.find(top_code);
3258+
if (it != jit_code_outer_funcs.end() && it->second == func) {
3259+
jit_code_outer_funcs.erase(it);
3260+
PyObject* module = func->func_module;
3261+
BorrowedRef<> top_consts{top_code->co_consts};
3262+
for (BorrowedRef<PyCodeObject> code : findNestedCodes(module, top_consts)) {
3263+
jit_reg_units.erase(code);
3264+
auto existing = jit_code_outer_funcs.find(code);
3265+
if (existing != jit_code_outer_funcs.end() && existing->second == func) {
3266+
jit_code_outer_funcs.erase(code);
3267+
}
3268+
if (handle_unit_deleted_during_preload != nullptr) {
3269+
handle_unit_deleted_during_preload(code.getObj());
3270+
}
3271+
}
3272+
}
3273+
3274+
jit_reg_units.erase(func);
3275+
jit_reg_units.erase(top_code);
3276+
if (handle_unit_deleted_during_preload != nullptr) {
3277+
handle_unit_deleted_during_preload(func.getObj());
3278+
}
3279+
}
3280+
31313281
} // namespace
31323282

31333283
#if PY_VERSION_HEX < 0x030C0000
@@ -3435,44 +3585,25 @@ void finalize() {
34353585
}
34363586

34373587
bool shouldScheduleCompile(BorrowedRef<PyFunctionObject> func) {
3438-
// Can be called after the module has been finalized, due to function events.
3439-
if (jitCtx() == nullptr) {
3440-
return false;
3441-
}
3442-
3443-
if (isCinderModule(func->func_module)) {
3444-
return false;
3445-
}
3446-
3447-
// Note: This is not the same as fetching the function's code object and
3448-
// checking its module and qualname, as functions can be renamed after they
3449-
// are created. Code objects cannot.
3450-
if (auto jit_list = cinderx::getModuleState()->jitList()) {
3451-
return jit_list->lookupFunc(func) == 1;
3452-
}
3453-
34543588
BorrowedRef<PyCodeObject> code{func->func_code};
34553589
return shouldAlwaysScheduleCompile(code) ||
34563590
getConfig().compile_after_n_calls.has_value();
34573591
}
34583592

3459-
bool shouldScheduleCompile(
3460-
BorrowedRef<> module_name,
3461-
BorrowedRef<PyCodeObject> code) {
3462-
if (isCinderModule(module_name)) {
3593+
bool scheduleJitCompile(BorrowedRef<PyFunctionObject> func) {
3594+
auto eligible = getCompilationEligibility(func);
3595+
if (eligible == JitEligibility::Ineligible) {
34633596
return false;
34643597
}
3598+
trackEligibleCodeObjects(func, func->func_code, eligible);
34653599

3466-
if (auto jit_list = cinderx::getModuleState()->jitList()) {
3467-
return jit_list->lookupCode(code) == 1 ||
3468-
jit_list->lookupName(module_name, code->co_qualname) == 1;
3600+
// If we're not eligible due to the JIT list check if we have config (e.g.
3601+
// auto jit, jit all, or jit all static methods) that makes compilation happen
3602+
// automatically.
3603+
if (eligible == JitEligibility::Eligible && !shouldScheduleCompile(func)) {
3604+
return false;
34693605
}
34703606

3471-
return shouldAlwaysScheduleCompile(code) ||
3472-
getConfig().compile_after_n_calls.has_value();
3473-
}
3474-
3475-
bool scheduleJitCompile(BorrowedRef<PyFunctionObject> func) {
34763607
// Could be creating an inner function with an already-compiled code object.
34773608
if (isJitCompiled(func)) {
34783609
return true;
@@ -3533,7 +3664,9 @@ std::vector<BorrowedRef<PyFunctionObject>> preloadFuncAndDeps(
35333664
worklist.push_back(func);
35343665

35353666
auto shouldPreload = [&](BorrowedRef<PyFunctionObject> f) {
3536-
return !isPreloaded(f) && (shouldScheduleCompile(f) || forcePreload);
3667+
return !isPreloaded(f) &&
3668+
(forcePreload ||
3669+
getCompilationEligibility(f) != JitEligibility::Ineligible);
35373670
};
35383671

35393672
while (worklist.size() > 0 && result.size() < limit) {
@@ -3608,32 +3741,13 @@ void codeDestroyed(BorrowedRef<PyCodeObject> code) {
36083741
}
36093742

36103743
void funcDestroyed(BorrowedRef<PyFunctionObject> func) {
3611-
if (isJitUsable()) {
3612-
auto mod_state = cinderx::getModuleState();
3613-
3614-
auto& jit_reg_units = mod_state->registeredCompilationUnits();
3615-
jit_reg_units.erase(func.getObj());
3616-
if (handle_unit_deleted_during_preload != nullptr) {
3617-
handle_unit_deleted_during_preload(func.getObj());
3618-
}
3619-
3620-
// erase any child code objects we registered too
3621-
if (mod_state->jitList() != nullptr) {
3622-
auto& jit_code_outer_funcs = jitCtx()->codeOuterFunctions();
3623-
PyObject* module = func->func_module;
3624-
BorrowedRef<PyCodeObject> top_code{func->func_code};
3625-
BorrowedRef<> top_consts{top_code->co_consts};
3626-
for (BorrowedRef<PyCodeObject> code :
3627-
findNestedCodes(module, top_consts)) {
3628-
jit_reg_units.erase(code);
3629-
jit_code_outer_funcs.erase(code);
3630-
if (handle_unit_deleted_during_preload != nullptr) {
3631-
handle_unit_deleted_during_preload(code.getObj());
3632-
}
3633-
}
3634-
}
3744+
auto mod_state = cinderx::getModuleState();
3745+
if (!mod_state) {
3746+
return;
36353747
}
36363748

3749+
unregisterFunctionCodes(func);
3750+
36373751
// Have to check if context exists as this can fire after jit::finalize().
36383752
if (jitCtx()) {
36393753
jitCtx()->funcDestroyed(func);
@@ -3646,6 +3760,11 @@ void funcDestroyed(BorrowedRef<PyFunctionObject> func) {
36463760

36473761
void funcModified(BorrowedRef<PyFunctionObject> func) {
36483762
deoptFunc(func);
3763+
// Clean up registrations for the old code object. At this point
3764+
// func->func_code still refers to the old code. The caller will update
3765+
// func->func_code and call scheduleCompile() to re-register with the new
3766+
// code.
3767+
unregisterFunctionCodes(func);
36493768
}
36503769

36513770
void typeDestroyed(BorrowedRef<PyTypeObject> type) {
@@ -3691,10 +3810,7 @@ _PyJIT_Result compilePreloaderImpl(
36913810
BorrowedRef<PyDictObject> builtins = preloader.builtins();
36923811
BorrowedRef<PyDictObject> globals = preloader.globals();
36933812

3694-
// Don't care flags: CO_NOFREE, CO_FUTURE_* (the only still-relevant future is
3695-
// "annotations" which doesn't impact bytecode execution.)
3696-
int required_flags = CO_OPTIMIZED | CO_NEWLOCALS;
3697-
if ((code->co_flags & required_flags) != required_flags) {
3813+
if (!hasRequiredFlags(code)) {
36983814
JIT_DLOG(
36993815
"Can't compile {} due to missing required code flags",
37003816
preloader.fullname());

cinderx/Jit/pyjit.h

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,6 @@ int initialize();
3535
*/
3636
void finalize();
3737

38-
/*
39-
* Check if a function should be scheduled for compilation.
40-
*
41-
* This doesn't guarantee that the function can or will be compiled, it just
42-
* checks if the JIT has been configured in such a way that compilation is
43-
* possible.
44-
*/
45-
bool shouldScheduleCompile(BorrowedRef<PyFunctionObject> func);
46-
47-
/*
48-
* Variant of shouldScheduleCompile() for nested code objects.
49-
*/
50-
bool shouldScheduleCompile(
51-
BorrowedRef<> module_name,
52-
BorrowedRef<PyCodeObject> code);
53-
5438
/*
5539
* Overwrite the entry point of a function so that it tries to JIT-compile
5640
* itself in the future.

0 commit comments

Comments
 (0)