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 on PyGILState_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:213 releases the GIL from the main thread exactly once and records a TODO that 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 to framework_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:213 TODO.
- Python algorithms no longer need a blanket
concurrency::serial default 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
PyGILRAII scopes. 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.cpp touches 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
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
PyGILRAIIhelper (plugins/python/src/wrap.hpp:77), with acquisition sites inerrorwrap.cpp,pymodule.cpp, and eleven places inmodulewrap.cpp, several inside macros. Each Python algorithm invocation acquires the GIL on whichever TBB thread is running it.Two consequences:
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.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:
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:
pymodule.cpp:213TODO.concurrency::serialdefault purely to approximate GIL serialization.Open questions
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.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.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
TFileService