.. _working-around-the-gil: Working Around the GIL ====================== In Python, the GIL (Global Interpreter Lock) practically prevents multi-threaded code from running Python bytecode in parallel: while one thread holds the lock, no other thread in the same process can execute Python. Extension modules written in low-level languages such as C, C++ and Rust can, however, release the GIL while executing operations that do not interact with the Python interpreter. This page collects GIL-specific guidance. For the execution models that build on it, see :ref:`pipeline-parallelism` (the fundamentals) and :ref:`execution-models` (the MT / MP / MTP patterns). Which operations release the GIL? --------------------------------- Many libraries used for data loading release the GIL. To name a few: - Pillow - OpenCV - Decord - tiktoken - polars Libraries such as PyTorch and NumPy also release the GIL when manipulating arrays, so they are usually fine. For loading raw byte strings into array format, SPDL offers efficient GIL-releasing functions through the :py:mod:`spdl.io` module. Typically the bottleneck in model training is loading and pre-processing media data. Even though some parts of a pipeline are constrained by the GIL, we can achieve high throughput by using pre-processing functions that release it. This is what lets the default multi-threaded pipeline scale; see :ref:`pipeline-parallelism` for how the shared thread pool dispatches these stages. Example: pandas vs polars ------------------------- DataFrame libraries make the effect concrete. `polars `_ is implemented in Rust and releases the GIL during its operations, whereas `pandas `_ is largely Python/Cython and holds the GIL for much of its work. In a multi-threaded pipeline (MT or MTP) whose per-row work is a DataFrame decode and transform, switching the backend from pandas to polars can nearly double end-to-end throughput -- we observed roughly a 1.8x speedup on such a workload -- because the polars stages run in parallel across the thread pool while the pandas stages are serialized by the GIL. .. figure:: ../_static/data/pandas_vs_polars.png :width: 100% The same pipeline with a pandas vs a polars backend (higher is better; absolute values omitted, gridlines for scale). On the GIL builds (3.12, 3.14) polars roughly doubles the threaded runners (mt, mtp), while multi-processing (mp) is largely unchanged. On free-threaded Python (3.14t; no polars wheel yet) pandas alone reaches comparable threaded throughput, since the GIL no longer serializes the threads. The gain is specific to the thread-based models. With multi-processing (MP), where each worker has its own interpreter and GIL, the backend choice makes little difference: the parallelism there does not depend on the stage releasing the GIL. This is a good illustration of why the best execution model depends on whether the hot stage releases the GIL; see :ref:`execution-models`. What if a function does not release the GIL? -------------------------------------------- If a stage relies on a function that holds the GIL, running it on the shared thread pool blocks the other stages. In that case, move the work off the shared thread pool: - Delegate the single stage to a subprocess with a :py:class:`~concurrent.futures.ProcessPoolExecutor`. See the "Multi-processing (stage)" section of :ref:`pipeline-parallelism` for the mechanics, including the picklability requirements and one-time subprocess initialization. - For a data loader with several such stages, use the multi-processing (MP) or multi-threading-in-subprocess (MTP) patterns instead of paying a process round trip per stage. See :ref:`execution-models`. On free-threaded (no-GIL) builds of Python the constraint is lifted, so threaded stages can run Python in parallel; see :ref:`execution-models` for how that affects the choice of execution model. Which functions hold the GIL? ----------------------------- The following is the list of functions that we are aware hold the GIL. It is advised to use them with a ``ProcessPoolExecutor`` (as above) or to avoid using them in SPDL. * `np.load `_: Please refer to :ref:`data-formats-case-study` for a possible workaround.