From df391ce5bfb4df0f004bd1d1499186828f92d587 Mon Sep 17 00:00:00 2001 From: Christian Humer Date: Wed, 20 May 2026 22:31:58 +0200 Subject: [PATCH] Add GraalPy JIT presets --- CHANGELOG.md | 1 + docs/user/Performance.md | 10 ++++ .../python/micro/jsonrpc-pipe.py | 14 ++++- .../graal/python/shell/GraalPythonMain.java | 55 ++++++++++++++++++- .../src/tests/test_cmd_line.py | 23 ++++++++ mx.graalpython/mx_graalpython_benchmark.py | 12 ++-- 6 files changed, 108 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c4c7f1293..c8c859ccb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ language runtime. The main focus is on user-observable behavior of the engine. ## Version 25.1.0 * The standalone artifacts now include the Python version name before the Graal version. The new artifacts now start with `graalpy---`. * Standalone JVM artifacts are no longer released as separate distributions. For standalone deployments, use the GraalPy native artifacts. If you require Java interoperability, use a custom embedding. +* Add `-X jit=0|1|2` presets to tune startup-heavy or throughput-oriented workloads, and make the GraalPy launcher default to the `jit=1` preset. * Treat foreign buffer objects as Python buffer-compatible binary objects, so APIs like `memoryview`, `bytes`, `bytearray`, `binascii.hexlify`, and `io.BytesIO` work naturally on them when embedding GraalPy in Java. This allows passing binary data between Python and Java's `ByteBuffer` and `ByteSequence` types with minimal (sometimes zero) copies. * Add support for [Truffle source options](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/source/Source.SourceBuilder.html#option(java.lang.String,java.lang.String)): * The `python.Optimize` option can be used to specify the optimization level, like the `-O` (level 1) and `-OO` (level 2) commandline options. diff --git a/docs/user/Performance.md b/docs/user/Performance.md index fa84d2be43..7d9c1e0dc3 100644 --- a/docs/user/Performance.md +++ b/docs/user/Performance.md @@ -17,6 +17,16 @@ Many Python packages from the machine learning or data science ecosystems contai This code benefits little from GraalPy's JIT compilation and suffers from having to emulate CPython implementation details on GraalPy. When many C extensions are involved, performance can vary a lot depending on the specific interactions of native and Python code. +### Launcher JIT Presets + +The GraalPy launcher provides `-X jit=0|1|2` presets for common startup-heavy and throughput-oriented use cases. The launcher defaults to `-X jit=1` when no `-X jit=...` preset is specified: + +- `-X jit=0` disables runtime compilation and minimizes memory usage for tiny one-shot runs. +- `-X jit=1` keeps compilation enabled, but favors small command line applications with a single compiler thread and higher compilation thresholds. +- `-X jit=2` keeps compilation enabled with throughput mode and the same higher thresholds for hotter or longer-running workloads. + +These presets are launcher conveniences that expand to engine options. They can still be combined with explicit `--engine.*` options when more detailed tuning is needed. + ## Code Loading Performance and Footprint It takes time to parse Python code so when using GraalPy to embed another language in Python, observe the general advice for embedding Graal languages related to [code caching](https://www.graalvm.org/latest/reference-manual/embed-languages/#code-caching-across-multiple-contexts). diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/micro/jsonrpc-pipe.py b/graalpython/com.oracle.graal.python.benchmarks/python/micro/jsonrpc-pipe.py index 83ee8cfd06..f3762c5466 100644 --- a/graalpython/com.oracle.graal.python.benchmarks/python/micro/jsonrpc-pipe.py +++ b/graalpython/com.oracle.graal.python.benchmarks/python/micro/jsonrpc-pipe.py @@ -305,11 +305,23 @@ def get_subprocess_launcher_args(): orig_argv = getattr(sys, "orig_argv", None) if not orig_argv: return [sys.executable] + + main_argv0 = sys.argv[0] + for i, arg in enumerate(orig_argv[1:], start=1): + if arg == main_argv0: + return [sys.executable, *orig_argv[1:i]] + launcher_args = [sys.executable] - for arg in orig_argv[1:]: + i = 1 + while i < len(orig_argv): + arg = orig_argv[i] if not arg.startswith("-"): break launcher_args.append(arg) + if arg == "-X" and i + 1 < len(orig_argv): + i += 1 + launcher_args.append(orig_argv[i]) + i += 1 return launcher_args diff --git a/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java b/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java index 28e6dbc7c1..5f2ec5e99f 100644 --- a/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java +++ b/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java @@ -242,6 +242,7 @@ protected List preprocessArguments(List givenArgs, Map argumentIterator = arguments.iterator(); argumentIterator.hasNext();) { String arg = argumentIterator.next(); origArgs.add(arg); @@ -464,6 +465,9 @@ protected List preprocessArguments(List givenArgs, Map 0) { intMaxStrDigits = validateIntMaxStrDigits(xOption.substring(eq), "-X int_max_str_digits"); } + } else if ("jit".equals(xOption) || xOption.startsWith("jit=")) { + applyJitModePreset(polyglotOptions, xOption); + jitModePresetSpecified = true; } break shortOptionLoop; default: @@ -526,6 +530,9 @@ protected List preprocessArguments(List givenArgs, Map= %d or 0 for unlimited.", name, INT_MAX_STR_DIGITS_THRESHOLD), 1); } + private void applyJitModePreset(Map polyglotOptions, String xOption) { + boolean fallbackRuntime = usesFallbackRuntime(); + switch (xOption) { + case "jit=0": + if (!fallbackRuntime) { + polyglotOptions.put("engine.Compilation", "false"); + } + break; + case "jit=1": + if (!fallbackRuntime) { + applyLatencyJitPreset(polyglotOptions); + } + break; + case "jit=2": + if (!fallbackRuntime) { + applyThroughputJitPreset(polyglotOptions); + } + break; + default: + throw abort("Invalid argument for the -X jit option: expected jit=0, jit=1, or jit=2\n" + SHORT_HELP, 2); + } + } + + private boolean usesFallbackRuntime() { + return findOptionDescriptor("engine", "engine.Compilation") == null; + } + + private static void applyLatencyJitPreset(Map polyglotOptions) { + polyglotOptions.put("engine.CompilerThreads", "1"); + applyRaisedJitThresholds(polyglotOptions); + } + + private static void applyThroughputJitPreset(Map polyglotOptions) { + polyglotOptions.put("engine.Mode", "throughput"); + applyRaisedJitThresholds(polyglotOptions); + } + + private static void applyRaisedJitThresholds(Map polyglotOptions) { + polyglotOptions.put("engine.FirstTierCompilationThreshold", "10000"); + polyglotOptions.put("engine.LastTierCompilationThreshold", "100000"); + polyglotOptions.put("engine.OSRCompilationThreshold", "200704"); + polyglotOptions.put("engine.SingleTierCompilationThreshold", "100000"); + } + private static String toAbsolutePath(String executable) { if (executable.contains(":")) { // this is either already an absolute windows path, or not a single executable @@ -1182,7 +1233,9 @@ protected void printHelp(OptionCategory maxCategory) { " can be supplied multiple times to increase verbosity\n" + "-V : print the Python version number and exit (also --version)\n" + " when given twice, print more information about the build\n" + - "-X opt : CPython implementation-specific options. warn_default_encoding and int_max_str_digits are supported on GraalPy\n" + + "-X opt : set implementation-specific option\n" + + " CPython-compatible options supported by GraalPy: warn_default_encoding, int_max_str_digits\n" + + " GraalPy implementation-specific options: jit=0|1|2 (default: jit=1)\n" + "-W arg : warning control; arg is action:message:category:module:lineno\n" + " also PYTHONWARNINGS=arg\n" + "file : program read from script file\n" + diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_cmd_line.py b/graalpython/com.oracle.graal.python.test/src/tests/test_cmd_line.py index d258ac9572..642c1153cf 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_cmd_line.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_cmd_line.py @@ -41,6 +41,8 @@ import sys import unittest +IS_GRAALPY = sys.implementation.name == "graalpy" + class CmdLineTest(unittest.TestCase): @@ -48,3 +50,24 @@ def test_stdin_script_exit_code(self): code = "import sys\nsys.exit(42)\n" result = subprocess.run([sys.executable], input=code, text=True) self.assertEqual(42, result.returncode) + + @unittest.skipUnless(IS_GRAALPY, "GraalPy-specific test") + def test_jit_mode_presets(self): + for mode in ('0', '1', '2'): + result = subprocess.run( + [sys.executable, '-X', f'jit={mode}', '-c', '1'], + capture_output=True, + text=True, + ) + self.assertEqual(0, result.returncode, result) + + @unittest.skipUnless(IS_GRAALPY, "GraalPy-specific test") + def test_jit_mode_invalid_value(self): + result = subprocess.run( + [sys.executable, '-X', 'jit=3', '-c', 'pass'], + capture_output=True, + text=True, + ) + self.assertNotEqual(0, result.returncode) + self.assertIn('expected jit=0, jit=1, or jit=2', result.stderr) + diff --git a/mx.graalpython/mx_graalpython_benchmark.py b/mx.graalpython/mx_graalpython_benchmark.py index 58a59c4934..2e33bc0118 100644 --- a/mx.graalpython/mx_graalpython_benchmark.py +++ b/mx.graalpython/mx_graalpython_benchmark.py @@ -70,6 +70,7 @@ CONFIGURATION_INTERPRETER_MULTI = "interpreter-multi" CONFIGURATION_NATIVE_INTERPRETER_MULTI = "native-interpreter-multi" CONFIGURATION_NATIVE = "native" +CONFIGURATION_NATIVE_JIT2 = "native-jit2" CONFIGURATION_UNCACHED = "interpreter-uncached" CONFIGURATION_NATIVE_MULTI = "native-multi" CONFIGURATION_SANDBOXED = "sandboxed" @@ -1077,16 +1078,17 @@ def add_graalpy_vm(name, *extra_polyglot_args): # GraalPy VMs: add_graalpy_vm(CONFIGURATION_DEFAULT) add_graalpy_vm(CONFIGURATION_CUSTOM) - add_graalpy_vm(CONFIGURATION_INTERPRETER, '--experimental-options', '--engine.Compilation=false') + add_graalpy_vm(CONFIGURATION_INTERPRETER, '-X', 'jit=0') add_graalpy_vm(CONFIGURATION_DEFAULT_MULTI, '--experimental-options', '-multi-context') - add_graalpy_vm(CONFIGURATION_INTERPRETER_MULTI, '--experimental-options', '-multi-context', '--engine.Compilation=false') + add_graalpy_vm(CONFIGURATION_INTERPRETER_MULTI, '--experimental-options', '-multi-context', '-X', 'jit=0') add_graalpy_vm(CONFIGURATION_SANDBOXED, *sandboxed_options) add_graalpy_vm(CONFIGURATION_NATIVE) - add_graalpy_vm(CONFIGURATION_UNCACHED, '--experimental-options', '--engine.Compilation=false', '--python.ForceUncachedInterpreter=true') - add_graalpy_vm(CONFIGURATION_NATIVE_INTERPRETER, '--experimental-options', '--engine.Compilation=false') + add_graalpy_vm(CONFIGURATION_NATIVE_JIT2, '-X', 'jit=2') + add_graalpy_vm(CONFIGURATION_UNCACHED, '--experimental-options', '-X', 'jit=0', '--python.ForceUncachedInterpreter=true') + add_graalpy_vm(CONFIGURATION_NATIVE_INTERPRETER, '-X', 'jit=0') add_graalpy_vm(CONFIGURATION_SANDBOXED_MULTI, '--experimental-options', '-multi-context', *sandboxed_options) add_graalpy_vm(CONFIGURATION_NATIVE_MULTI, '--experimental-options', '-multi-context') - add_graalpy_vm(CONFIGURATION_NATIVE_INTERPRETER_MULTI, '--experimental-options', '-multi-context', '--engine.Compilation=false') + add_graalpy_vm(CONFIGURATION_NATIVE_INTERPRETER_MULTI, '--experimental-options', '-multi-context', '-X', 'jit=0') # all of the graalpy vms, but with one compiler thread for name, extra_polyglot_args in graalpy_vms[:]: