Skip to content

Model the Python GIL as a framework resource #856

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

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

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions