Repository navigation
Description
Summary
Phlex currently manages the CPython Global Interpreter Lock ad hoc inside the Python plugin, and bounds Python concurrency by defaulting Python algorithms to concurrency::serial. Now that #21 has introduced a resource API with declared access policies, the interpreter is a natural candidate to be modeled as a framework-owned resource with a single-token access policy, rather than as an implicit lock acquired deep in the call stack.
This issue proposes evaluating that change and, if accepted, implementing it.
Current state
GIL acquisition is scattered through the Python plugin via the PyGILRAII helper (plugins/python/src/wrap.hpp:77), with acquisition sites in errorwrap.cpp, pymodule.cpp, and eleven places in modulewrap.cpp, several inside macros. Each Python algorithm invocation acquires the GIL on whichever TBB thread is running it.
Two consequences:
- Concurrency is bounded twice, in two different ways. Python algorithms default to
concurrency::serial(plugins/python/src/modulewrap.cpp:812), and independently every entry point blocks onPyGILState_Ensure(). The scheduler has no visibility into the second bound, so it can schedule work that immediately blocks. - Interpreter lifetime is not managed.
pymodule.cpp:213releases the GIL from the main thread exactly once and records aTODOthat there is "no equivalent place to re-acquire it after the TBB runs are done, so normal shutdown of the Python interpreter will not happen atm." A framework-owned resource has a well-defined lifetime tied toframework_graph, which is the missing hook.
Proposal
Register the interpreter as a single-token resource so that GIL contention becomes a declared, scheduler-visible dependency:
class python_interpreter {
public:
using token_type = python_interpreter const*;
// Owns interpreter initialization and finalization.
};A Python-backed node then declares the dependency the same way as any other resource, and the framework acquires the token for the duration of the body:
m.observe("py_algorithm", algorithm, concurrency::unlimited)
.input_family(selector, resource<python_interpreter>{});Expected benefits:
- The GIL bound becomes visible to the scheduler instead of manifesting as a blocked worker thread.
- Interpreter setup and teardown acquire an owner with a defined lifetime, addressing the
pymodule.cpp:213TODO. - Python algorithms no longer need a blanket
concurrency::serialdefault purely to approximate GIL serialization. - C/C++ extension work that releases the GIL can be expressed by scoping the resource dependency, rather than by manual save/restore.
Open questions
- Granularity. Whole-body token acquisition is coarser than the current per-entry-point
PyGILRAIIscopes. An algorithm that spends most of its body inside a GIL-released C extension would lose concurrency it has today. Needs measurement before committing. - Boundary crossings. Type conversion in
modulewrap.cpptouches Python objects on paths that are not obviously inside an algorithm body; each needs classification as inside or outside the declared token scope. - Registration path. Users do not construct the interpreter, so it cannot be an ordinary
add_resource<T>(...)call in user code. This likely depends on Phase 2 (dynamically loaded resource registration), where the Python plugin registers the interpreter resource on load.
Related work
- Add resource-aware algorithm registration and execution #21 — Integrate resources into the registration API; resource API spec in this comment
- feat: add resource-limited execution to graph nodes #850 — Phase 1 implementation
- Discussion #515 — Token-based approach to replacing "art services with global state"; same argument applied to
TFileService - phlex-design discussion #2 — GIL in the Numba entry-point context
- phlex-design#48 — Explore Python subinterpreters for increased concurrency (closed; background on Python global state in Phlex)
- Explore an interface description language (IDL) #71 — Explore an interface description language (IDL)
Activity
wlav commentedon Sep 10, 2026
That "current state" is bit misleading: a lot of places where the GIL is acquired is in functions that are called during registration, which runs serially (in fact, runs typically from Python for Python algorithms). As such, it's not really bounded twice. The main problematic uses in the graph execution are the converters (b/c extremely fine-grained, so high overhead).
Second is that non-serial concurrency is supported for the JITed case (Numba), but note here that Numba can call into Python as well as hold the GIL on entry then release (eg. when the jit-decorated function is installed). This actually a common case (eg. for GPU offloading, or use of pytorch etc.) akin to the 4th bullet above on "C/C++ extension work". It's not immediately clear here how the use of a resource would be useful. As-is, handing off the GIL causes bottlenecks, so there's a need to communicate this to the scheduler.
Third, "Users do not construct the interpreter". I have some hope that the earlier problems I had with sub-interpreters can be explicitly solved by indeed allowing users to construct them. (It'd be a promise on their end that a piece of code is self-contained.)