Skip to content

Commit b4dd493

Browse files
alexmalyshevfacebook-github-bot
authored andcommitted
Support dynamically enabling the JIT with a new call count threshold
Summary: Adds new Python API functions for configuring the JIT to compile functions automatically. `cinderx.jit.compile_after_n_calls(N)`: As it sounds, this tells the JIT to compile functions after they have been called `N` times. This is equivalent to `PYTHONJITAUTO=N`, but it can be called multiple times as a program runs. `cinderx.jit.auto()`: This tells the JIT to automatically compile functions, but leaves all the decision-making to the JIT. It's the recommended behavior for users. Reviewed By: jbower-fb Differential Revision: D81356873 fbshipit-source-id: 94145aa1b4d00d29301ae8c5d9651c932e2e9584
1 parent 0fa0547 commit b4dd493

4 files changed

Lines changed: 184 additions & 10 deletions

File tree

cinderx/Jit/pyjit.cpp

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,18 @@ _PyJIT_Result tryCompile(BorrowedRef<PyFunctionObject> func) {
191191
return result;
192192
}
193193

194+
void incrementShadowcodeCall([[maybe_unused]] BorrowedRef<PyCodeObject> code) {
195+
#if SHADOWCODE_SUPPORTED
196+
// The interpreter will only increment up to the shadowcode threshold
197+
// PYSHADOW_INIT_THRESHOLD. After that, it will stop incrementing. If someone
198+
// sets -X jit-auto above the PYSHADOW_INIT_THRESHOLD, we still have to keep
199+
// counting.
200+
if (code->co_mutable->ncalls > PYSHADOW_INIT_THRESHOLD) {
201+
code->co_mutable->ncalls++;
202+
}
203+
#endif
204+
}
205+
194206
// Python function entry point when AutoJIT is enabled.
195207
PyObject* autoJITVectorcall(
196208
PyObject* func_obj,
@@ -207,16 +219,10 @@ PyObject* autoJITVectorcall(
207219

208220
// Interpret function as usual until it passes the call count threshold.
209221

210-
// The interpreter will only increment up to the shadowcode threshold
211-
// PYSHADOW_INIT_THRESHOLD. After that, it will stop incrementing. If someone
212-
// sets -X jit-auto above the PYSHADOW_INIT_THRESHOLD, we still have to keep
213-
// counting.
214-
#if SHADOWCODE_SUPPORTED
215-
if (code->co_mutable->ncalls > PYSHADOW_INIT_THRESHOLD) {
216-
code->co_mutable->ncalls++;
217-
}
218-
#endif
219-
if (countCalls(code) < jit::getConfig().auto_jit_threshold) {
222+
auto const calls = countCalls(code);
223+
224+
if (calls < jit::getConfig().auto_jit_threshold) {
225+
incrementShadowcodeCall(code);
220226
auto entry = getInterpretedVectorcall(func);
221227
return entry(func_obj, stack, nargsf, kwnames);
222228
}
@@ -226,6 +232,7 @@ PyObject* autoJITVectorcall(
226232
return nullptr;
227233
}
228234
if (result == PYJIT_RESULT_RETRY) {
235+
incrementShadowcodeCall(code);
229236
auto entry = getInterpretedVectorcall(func);
230237
return entry(func_obj, stack, nargsf, kwnames);
231238
}
@@ -1332,6 +1339,44 @@ PyObject* enable_jit(PyObject* /* self */, PyObject* /* arg */) {
13321339
Py_RETURN_NONE;
13331340
}
13341341

1342+
PyObject* compile_after_n_calls(PyObject* /* self */, PyObject* arg) {
1343+
Py_ssize_t calls = -1;
1344+
if (!PyArg_Parse(arg, "n:compile_after_n_calls", &calls)) {
1345+
return nullptr;
1346+
}
1347+
if (calls < 0) {
1348+
PyErr_Format(
1349+
PyExc_ValueError,
1350+
"Cannot configure JIT to compile functions after '%zd' calls",
1351+
calls);
1352+
return nullptr;
1353+
}
1354+
if (calls == 0) {
1355+
PyErr_Format(
1356+
PyExc_ValueError,
1357+
"compile_after_n_calls(0) not supported yet, use PYTHONJITALL=1");
1358+
return nullptr;
1359+
}
1360+
1361+
getMutableConfig().auto_jit_threshold = calls;
1362+
JIT_DLOG("Configuring JIT to compile functions after {} calls", calls);
1363+
1364+
Py_RETURN_NONE;
1365+
}
1366+
1367+
PyObject* auto_jit(PyObject* /* self */, PyObject* /* arg */) {
1368+
// Default value that works well for most applications.
1369+
constexpr size_t kThreshold = 1000;
1370+
1371+
getMutableConfig().auto_jit_threshold = kThreshold;
1372+
1373+
JIT_DLOG(
1374+
"Configuring JIT to compile functions automatically using default "
1375+
"behavior");
1376+
1377+
Py_RETURN_NONE;
1378+
}
1379+
13351380
PyObject* get_batch_compilation_time_ms(PyObject*, PyObject*) {
13361381
return PyLong_FromLong(g_batch_compilation_time.count());
13371382
}
@@ -2300,6 +2345,16 @@ PyMethodDef jit_methods[] = {
23002345
METH_NOARGS,
23012346
PyDoc_STR("Re-enable the JIT and re-attach compiled onto previously "
23022347
"JIT-compiled functions.")},
2348+
{"auto",
2349+
auto_jit,
2350+
METH_NOARGS,
2351+
PyDoc_STR("Configure the JIT to automatically compile functions, using "
2352+
"default settings")},
2353+
{"compile_after_n_calls",
2354+
compile_after_n_calls,
2355+
METH_O,
2356+
PyDoc_STR("Configure the JIT to automatically compile functions after "
2357+
"they are called a set number of times.")},
23032358
{"disassemble", disassemble, METH_O, "Disassemble JIT compiled functions."},
23042359
{"dump_elf",
23052360
dump_elf,

cinderx/PythonLib/cinderx/jit.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
from cinderjit import (
2121
_deopt_gen,
2222
append_jit_list,
23+
auto,
2324
auto_jit_threshold,
2425
clear_runtime_stats,
26+
compile_after_n_calls,
2527
count_interpreted_calls,
2628
disable,
2729
disable_emit_type_annotation_guards,
@@ -78,12 +80,18 @@ def _deopt_gen(
7880
def append_jit_list(entry: str) -> None:
7981
return None
8082

83+
def auto() -> None:
84+
return None
85+
8186
def auto_jit_threshold() -> int:
8287
return 0
8388

8489
def clear_runtime_stats() -> None:
8590
return None
8691

92+
def compile_after_n_calls(calls: int) -> None:
93+
return None
94+
8795
def count_interpreted_calls(func: FuncAny) -> int:
8896
return 0
8997

cinderx/PythonLib/test_cinderx/test_cinderjit.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import cinderx.test_support as cinder_support
3131
from cinderx.compiler.consts import CO_FUTURE_BARRY_AS_BDFL, CO_SUPPRESS_JIT
3232
from cinderx.jit import (
33+
compile_after_n_calls,
3334
force_compile,
3435
force_uncompile,
3536
is_jit_compiled,
@@ -2564,6 +2565,16 @@ def f(x: int) -> int:
25642565

25652566
@unittest.skipUnless(cinderx.jit.is_enabled(), "Testing the cinderjit module itself")
25662567
class BadArgumentTests(unittest.TestCase):
2568+
def test_compile_after_n_calls(self) -> None:
2569+
with self.assertRaises(TypeError):
2570+
compile_after_n_calls(None)
2571+
with self.assertRaises(TypeError):
2572+
compile_after_n_calls(is_jit_compiled)
2573+
with self.assertRaises(ValueError):
2574+
compile_after_n_calls(-1)
2575+
with self.assertRaises(ValueError):
2576+
compile_after_n_calls(0)
2577+
25672578
def test_is_compiled(self) -> None:
25682579
with self.assertRaises(TypeError):
25692580
is_jit_compiled(None)

cinderx/PythonLib/test_cinderx/test_jit_disable.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
# pyre-unsafe
44

5+
import subprocess
6+
import sys
7+
import tempfile
8+
import textwrap
59
import unittest
10+
from pathlib import Path
611

712
from cinderx.jit import (
813
disable as disable_jit,
@@ -17,6 +22,7 @@
1722
lazy_compile,
1823
pause as pause_jit,
1924
)
25+
from cinderx.test_support import CINDERX_PATH
2026

2127

2228
@unittest.skipUnless(is_jit_enabled(), "Tests functionality on the JIT")
@@ -201,6 +207,100 @@ def foo(a, b):
201207
# detection.
202208
force_uncompile(foo)
203209

210+
def test_default(self) -> None:
211+
with tempfile.TemporaryDirectory() as tmp_dir:
212+
code = textwrap.dedent("""
213+
import cinderx.jit
214+
215+
def inc(x):
216+
return x + 1
217+
218+
assert not cinderx.jit.is_jit_compiled(inc)
219+
cinderx.jit.force_compile(inc)
220+
assert cinderx.jit.is_jit_compiled(inc)
221+
""")
222+
223+
test_file = Path(tmp_dir) / "mod.py"
224+
test_file.write_text(code)
225+
226+
subprocess.run(
227+
[sys.executable, str(test_file)],
228+
check=True,
229+
env={"PYTHONPATH": CINDERX_PATH},
230+
)
231+
232+
def test_auto(self) -> None:
233+
with tempfile.TemporaryDirectory() as tmp_dir:
234+
code = textwrap.dedent("""
235+
import cinderx.jit
236+
237+
cinderx.jit.auto()
238+
239+
def inc(x):
240+
return x + 1
241+
242+
assert not cinderx.jit.is_jit_compiled(inc)
243+
for i in range(1000):
244+
inc(i)
245+
assert not cinderx.jit.is_jit_compiled(inc)
246+
247+
inc(1001)
248+
assert cinderx.jit.is_jit_compiled(inc)
249+
""")
250+
251+
test_file = Path(tmp_dir) / "mod.py"
252+
test_file.write_text(code)
253+
254+
subprocess.run(
255+
[sys.executable, str(test_file)],
256+
check=True,
257+
env={"PYTHONPATH": CINDERX_PATH},
258+
)
259+
260+
def test_compile_after_n_calls(self) -> None:
261+
with tempfile.TemporaryDirectory() as tmp_dir:
262+
code = textwrap.dedent("""
263+
import cinderx.jit
264+
265+
cinderx.jit.compile_after_n_calls(2)
266+
267+
def inc(x):
268+
return x + 1
269+
270+
assert not cinderx.jit.is_jit_compiled(inc)
271+
inc(1)
272+
inc(2)
273+
assert not cinderx.jit.is_jit_compiled(inc)
274+
275+
inc(3)
276+
assert cinderx.jit.is_jit_compiled(inc)
277+
278+
cinderx.jit.compile_after_n_calls(5)
279+
280+
def dec(x):
281+
return x - 1
282+
283+
assert not cinderx.jit.is_jit_compiled(dec)
284+
dec(1)
285+
dec(2)
286+
dec(3)
287+
dec(4)
288+
dec(5)
289+
assert not cinderx.jit.is_jit_compiled(dec)
290+
291+
dec(6)
292+
assert cinderx.jit.is_jit_compiled(dec)
293+
""")
294+
295+
test_file = Path(tmp_dir) / "mod.py"
296+
test_file.write_text(code)
297+
298+
subprocess.run(
299+
[sys.executable, str(test_file)],
300+
check=True,
301+
env={"PYTHONPATH": CINDERX_PATH},
302+
)
303+
204304

205305
if __name__ == "__main__":
206306
unittest.main()

0 commit comments

Comments
 (0)