Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<PYTHON_VERSION>-<GRAAL_VERSION>-<OPERATING_SYSTEM>-<ARCHITECTURE>`.
* 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.
Expand Down
10 changes: 10 additions & 0 deletions docs/user/Performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ protected List<String> preprocessArguments(List<String> givenArgs, Map<String, S
boolean pyExpatBackendSpecified = false;
boolean installSignalHandlersSpecified = false;
boolean isolateNativeModulesSpecified = false;
boolean jitModePresetSpecified = false;
for (Iterator<String> argumentIterator = arguments.iterator(); argumentIterator.hasNext();) {
String arg = argumentIterator.next();
origArgs.add(arg);
Expand Down Expand Up @@ -464,6 +465,9 @@ protected List<String> preprocessArguments(List<String> givenArgs, Map<String, S
if (eq > 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:
Expand Down Expand Up @@ -526,6 +530,9 @@ protected List<String> preprocessArguments(List<String> givenArgs, Map<String, S
if (!isolateNativeModulesSpecified) {
polyglotOptions.put("python.IsolateNativeModules", "false");
}
if (!jitModePresetSpecified) {
applyJitModePreset(polyglotOptions, "jit=1");
}
// Never emit warnings that mess up the output
unrecognized.add("--engine.WarnInterpreterOnly=false");
return unrecognized;
Expand Down Expand Up @@ -976,6 +983,50 @@ private int validateIntMaxStrDigits(String input, String name) {
throw abort(String.format("%s: invalid limit; must be >= %d or 0 for unlimited.", name, INT_MAX_STR_DIGITS_THRESHOLD), 1);
}

private void applyJitModePreset(Map<String, String> 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<String, String> polyglotOptions) {
polyglotOptions.put("engine.CompilerThreads", "1");
applyRaisedJitThresholds(polyglotOptions);
}

private static void applyThroughputJitPreset(Map<String, String> polyglotOptions) {
polyglotOptions.put("engine.Mode", "throughput");
applyRaisedJitThresholds(polyglotOptions);
}

private static void applyRaisedJitThresholds(Map<String, String> 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
Expand Down Expand Up @@ -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" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,33 @@
import sys
import unittest

IS_GRAALPY = sys.implementation.name == "graalpy"


class CmdLineTest(unittest.TestCase):

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)

12 changes: 7 additions & 5 deletions mx.graalpython/mx_graalpython_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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[:]:
Expand Down
Loading