Skip to content

Commit e9f6214

Browse files
authored
Add Execution Models docs and reorganize parallelism/GIL (#1593)
1 parent b0f1dca commit e9f6214

15 files changed

Lines changed: 621 additions & 230 deletions

File tree

40.7 KB
Loading

docs/source/getting_started/execution_models.rst

Lines changed: 311 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.

docs/source/getting_started/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ Getting Started
77
stages
88
concurrency
99
parallelism
10+
execution_models
11+
gil
1012
logging
1113
pytorch

docs/source/getting_started/intro.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,8 @@ worth avoiding for prompt resource release.
201201
202202
def __iter__(self) -> Iterator[T]:
203203
pipeline = self.get_pipeline()
204-
with pipeline.auto_stop():
205-
for item in pipeline:
206-
yield item
204+
for item in pipeline:
205+
yield item
207206
208207
dataloader = DataLoader(...)
209208

0 commit comments

Comments
 (0)