Skip to content

Support baking PythonCall into a juliacall system image (opt-in)#773

Open
ncudlenco wants to merge 1 commit into
JuliaPy:mainfrom
buchi-labortechnik-ag:feat/embed-sysimage
Open

Support baking PythonCall into a juliacall system image (opt-in)#773
ncudlenco wants to merge 1 commit into
JuliaPy:mainfrom
buchi-labortechnik-ag:feat/embed-sysimage

Conversation

@ncudlenco
Copy link
Copy Markdown

@ncudlenco ncudlenco commented May 19, 2026

Summary

Lets PythonCall itself be compiled into a juliacall system image (opt-in). With it baked in, the using PythonCall that import juliacall performs becomes a memory-map instead of a multi-second load+compile. In a containerised juliacall workload we measured, fresh-process startup drops from ~18 s to ~1.9 s. Off by default — no behaviour change unless enabled. It's meant as a low-risk interim until the automatic fix discussed in #436 (or related work) lands; it doesn't conflict with or preclude that.

Motivation

In short-lived Python processes that embed Julia via juliacall — serverless or autoscaled containers (AWS Lambda, queue workers, CI jobs) that start, handle one request, and exit — there's no long-lived process to amortise Julia start-up. Every cold start pays it again, and the dominant part is the using PythonCall that import juliacall runs to bring the bridge up.

Measured, fresh container to first call:

container init
stock juliacall (only app packages in the sysimage) ~18 s
PythonCall also baked into the sysimage (this PR) ~1.9 s

(~0.68 s of the ~1.9 s is the Julia stack; the rest is container/runtime overhead common to any container.) The ~16 s saved is essentially the repeated using PythonCall load+compile.

PYTHON_JULIACALL_SYSIMAGE already exists, but a sysimage with only the application packages still runs using PythonCall every cold start. Baking PythonCall in too is the obvious fix, but it currently fails: when PythonCall is in the sysimage its __init__ runs during jl_init_with_image, before juliacall's bootstrap has set Main.__PythonCall_libptr. init_context() decides "embedded" only from that global, so it sees "not embedded"; since Python is mid-import juliacall, init_juliacall() then errors with 'juliacall' module already exists.

Change

A second, opt-in way to detect the embedded case, using the same getpref mechanism added in 0.9.33 (preference embedded / env JULIA_PYTHONCALL_EMBEDDED, default no):

  • src/Utils/Utils.jl — add getpref_embedded(), alongside the existing getpref_exe / getpref_lib / getpref_pickle.
  • src/C/context.jl — if the Main global isn't set but embedded is, take the embedded path and get libpython from the existing lib preference / JULIA_PYTHONCALL_LIB (it's already loaded in the host process, so dlopen just hands back a handle).
  • docs/src/juliacall.md — a short "Baking PythonCall into a system image" section.
  • CHANGELOG.md — Unreleased entry.

In practice it's used together with PYTHON_JULIACALL_SYSIMAGE (PythonCall baked in) and PYTHON_JULIACALL_EXE / PROJECT.

Backward compatibility

It is backwards compatible since the option is off by default. The new path is only reached when embedded is explicitly set and Main.__PythonCall_libptr is absent. The normal juliacall path and the PythonCall-as-host path are unchanged.

Testing

The new code path only runs when explicitly enabled, so default behaviour is unchanged and the existing test suites are unaffected.

It was verified manually: with PythonCall baked into a system image and embedded enabled, import juliacall and calling Julia both work. No automated test is included because exercising it requires building a system image in CI, which is slow — happy to add one, or document the steps, if you'd prefer.

Related issues

Relates to #436 and #600 — this unblocks their use case (baking PythonCall into a custom system image) as an interim. The root-cause fix the maintainer described in #436 — resetting PythonCall's sysimage-persisted state in __init__ so it works with no opt-in — is the longer-term path; this PR doesn't attempt or preclude it.

Also relevant to #762 ("Improve juliacall startup time?", which explicitly asks about compiling a system image) and a building block for #76 ("Compile and use custom sysimages automatically").

Does not address #129 (a different failure: no environment in the LOAD_PATH depends on CondaPkg).

When PythonCall is compiled into a juliacall system image, its __init__
runs during jl_init_with_image, before juliacall's bootstrap defines
Main.__PythonCall_libptr. Embedding was therefore mis-detected as
non-embedded and failed with "'juliacall' module already exists".

Add an opt-in embedded preference / JULIA_PYTHONCALL_EMBEDDED (via the
same getpref mechanism as exe/lib) that forces the embedded path and
obtains libpython from the lib preference / JULIA_PYTHONCALL_LIB (already
loaded in the host process). Unset, behaviour is unchanged. Docs and
CHANGELOG updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant