Acquire the GIL in lazy init_extension#3255
Conversation
Greptile SummaryThis PR fixes a segfault that occurs when a
Confidence Score: 3/5The one-line placement of The fix correctly identifies that Files Needing Attention: transformer_engine/pytorch/csrc/extensions/pybind.cpp — the Important Files Changed
Sequence DiagramsequenceDiagram
participant A as Thread A (GIL released via call_guard)
participant CO as std::call_once lock
participant GIL as Python GIL
participant B as Thread B (holds GIL, calls init_extension)
Note over A,B: Both threads reach init_extension() for the first time concurrently
A->>CO: enters call_once, acquires internal lock
B->>GIL: holds GIL (normal Python thread)
A-->>GIL: gil_scoped_acquire (BLOCKS — B holds GIL)
B-->>CO: call_once (BLOCKS — A holds call_once lock)
Note over A,B: DEADLOCK: A needs GIL (held by B), B needs call_once lock (held by A)
Note over A,B: Correct fix: acquire GIL before call_once so lock order is always GIL → call_once
Reviews (2): Last reviewed commit: "Acquire the GIL in lazy init_extension" | Re-trigger Greptile |
Bindings declared with call_guard<gil_scoped_release> (e.g. fp8_transpose) can be the first transformer_engine_torch call in a process. init_extension() then runs py::module_::import without holding the GIL and segfaults inside PyImport_ImportModule. Reacquire the GIL inside the one-time initializer. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
af6afe3 to
ce243b2
Compare
| void init_extension() { | ||
| std::call_once(extension_init_flag, []() { | ||
| pybind11::gil_scoped_acquire gil; | ||
| init_float8_extension(); |
There was a problem hiding this comment.
Placing
gil_scoped_acquire inside the call_once lambda inverts the lock order relative to callers that still hold the GIL, creating an ABBA deadlock. Consider two threads both hitting an uninitialised tex entry point simultaneously: Thread A (GIL released via call_guard<gil_scoped_release>) wins the call_once race and blocks on gil_scoped_acquire; Thread B (holds the GIL, e.g. calling quantize) reaches call_once and blocks waiting for Thread A's lambda to complete. Thread A owns the call_once internal lock and needs the GIL; Thread B owns the GIL and needs the call_once lock — neither can proceed.
The fix is to acquire the GIL before call_once so that every caller establishes the same ordering (GIL → call_once lock), eliminating the inversion. After the one-time init completes, subsequent calls to init_extension() will acquire the GIL briefly and return immediately from call_once; pybind11::gil_scoped_acquire is a no-op when the calling thread already holds the GIL, so callers that never released it are unaffected.
| void init_extension() { | |
| std::call_once(extension_init_flag, []() { | |
| pybind11::gil_scoped_acquire gil; | |
| init_float8_extension(); | |
| void init_extension() { | |
| pybind11::gil_scoped_acquire gil; | |
| std::call_once(extension_init_flag, []() { | |
| init_float8_extension(); |
Description
init_extension()imports Python modules viapy::module_::import, which requires the GIL. Several bindings that can trigger this lazy initialization are declared withcall_guard<py::gil_scoped_release>— if one of them is the firsttransformer_engine_torchcall in the process, initialization runs without the GIL and segfaults. Typical runs don't hit this because a GIL-holding call (e.g. a quantize) usually comes first.Deterministic repro on main:
Running any GIL-holding tex call first (e.g.
tex.quantize) and thenfp8_transposeworks, confirming the GIL state is the only difference.Type of change
Changes
init_extension()'sstd::call_onceinitializer before importing Python modules.Checklist: