Skip to content

Model the Python GIL as a framework resource #856

Open

Description

@knoepfel

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:

  1. 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.
  2. 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

  1. 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.
  2. 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.
  3. 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

Activity

  1. wlav commented on Sep 10, 2026

    @wlav
    Contributor

    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.)

  2. added this to the Prototype 0.5 milestone on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestexecution graphHow the graph is established and executed

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions