|
| 1 | +.. _working-around-the-gil: |
| 2 | + |
| 3 | +Working Around the GIL |
| 4 | +====================== |
| 5 | + |
| 6 | +In Python, the GIL (Global Interpreter Lock) practically prevents multi-threaded |
| 7 | +code from running Python bytecode in parallel: while one thread holds the lock, |
| 8 | +no other thread in the same process can execute Python. Extension modules written |
| 9 | +in low-level languages such as C, C++ and Rust can, however, release the GIL |
| 10 | +while executing operations that do not interact with the Python interpreter. |
| 11 | + |
| 12 | +This page collects GIL-specific guidance. For the execution models that build on |
| 13 | +it, see :ref:`pipeline-parallelism` (the fundamentals) and :ref:`execution-models` |
| 14 | +(the MT / MP / MTP patterns). |
| 15 | + |
| 16 | +Which operations release the GIL? |
| 17 | +--------------------------------- |
| 18 | + |
| 19 | +Many libraries used for data loading release the GIL. To name a few: |
| 20 | + |
| 21 | +- Pillow |
| 22 | +- OpenCV |
| 23 | +- Decord |
| 24 | +- tiktoken |
| 25 | +- polars |
| 26 | + |
| 27 | +Libraries such as PyTorch and NumPy also release the GIL when manipulating |
| 28 | +arrays, so they are usually fine. For loading raw byte strings into array format, |
| 29 | +SPDL offers efficient GIL-releasing functions through the :py:mod:`spdl.io` |
| 30 | +module. |
| 31 | + |
| 32 | +Typically the bottleneck in model training is loading and pre-processing media |
| 33 | +data. Even though some parts of a pipeline are constrained by the GIL, we can |
| 34 | +achieve high throughput by using pre-processing functions that release it. This |
| 35 | +is what lets the default multi-threaded pipeline scale; see |
| 36 | +:ref:`pipeline-parallelism` for how the shared thread pool dispatches these |
| 37 | +stages. |
| 38 | + |
| 39 | +Example: pandas vs polars |
| 40 | +------------------------- |
| 41 | + |
| 42 | +DataFrame libraries make the effect concrete. `polars <https://pola.rs/>`_ is |
| 43 | +implemented in Rust and releases the GIL during its operations, whereas |
| 44 | +`pandas <https://pandas.pydata.org/>`_ is largely Python/Cython and holds the GIL |
| 45 | +for much of its work. |
| 46 | + |
| 47 | +In a multi-threaded pipeline (MT or MTP) whose per-row work is a DataFrame decode |
| 48 | +and transform, switching the backend from pandas to polars can nearly double |
| 49 | +end-to-end throughput -- we observed roughly a 1.8x speedup on such a workload -- |
| 50 | +because the polars stages run in parallel across the thread pool while the pandas |
| 51 | +stages are serialized by the GIL. |
| 52 | + |
| 53 | +.. figure:: ../_static/data/pandas_vs_polars.png |
| 54 | + :width: 100% |
| 55 | + |
| 56 | + The same pipeline with a pandas vs a polars backend (higher is better; |
| 57 | + absolute values omitted, gridlines for scale). On the GIL builds (3.12, 3.14) |
| 58 | + polars roughly doubles the threaded runners (mt, mtp), while multi-processing |
| 59 | + (mp) is largely unchanged. On free-threaded Python (3.14t; no polars wheel yet) |
| 60 | + pandas alone reaches comparable threaded throughput, since the GIL no longer |
| 61 | + serializes the threads. |
| 62 | + |
| 63 | +The gain is specific to the thread-based models. With multi-processing (MP), |
| 64 | +where each worker has its own interpreter and GIL, the backend choice makes |
| 65 | +little difference: the parallelism there does not depend on the stage releasing |
| 66 | +the GIL. This is a good illustration of why the best execution model depends on |
| 67 | +whether the hot stage releases the GIL; see :ref:`execution-models`. |
| 68 | + |
| 69 | +What if a function does not release the GIL? |
| 70 | +-------------------------------------------- |
| 71 | + |
| 72 | +If a stage relies on a function that holds the GIL, running it on the shared |
| 73 | +thread pool blocks the other stages. In that case, move the work off the shared |
| 74 | +thread pool: |
| 75 | + |
| 76 | +- Delegate the single stage to a subprocess with a |
| 77 | + :py:class:`~concurrent.futures.ProcessPoolExecutor`. See the "Multi-processing |
| 78 | + (stage)" section of :ref:`pipeline-parallelism` for the mechanics, including the |
| 79 | + picklability requirements and one-time subprocess initialization. |
| 80 | +- For a data loader with several such stages, use the multi-processing (MP) or |
| 81 | + multi-threading-in-subprocess (MTP) patterns instead of paying a process |
| 82 | + round trip per stage. See :ref:`execution-models`. |
| 83 | + |
| 84 | +On free-threaded (no-GIL) builds of Python the constraint is lifted, so threaded |
| 85 | +stages can run Python in parallel; see :ref:`execution-models` for how that |
| 86 | +affects the choice of execution model. |
| 87 | + |
| 88 | +Which functions hold the GIL? |
| 89 | +----------------------------- |
| 90 | + |
| 91 | +The following is the list of functions that we are aware hold the GIL. It is |
| 92 | +advised to use them with a ``ProcessPoolExecutor`` (as above) or to avoid using |
| 93 | +them in SPDL. |
| 94 | + |
| 95 | +* `np.load <https://github.com/numpy/numpy/blob/maintenance/2.1.x/numpy/lib/_npyio_impl.py#L312-L500>`_: Please refer to :ref:`data-formats-case-study` for a possible workaround. |
0 commit comments