Skip to content

Commit 8c651b2

Browse files
authored
Add autoresearch v2 example doc for video classification MTP + concrrency tuning (#1499)
Document the second autoresearch run on the video classification pipeline, which discovered MTP subprocess isolation and demux concurrency reduction as key optimizations. The run explored 111 experiments over ~20 hours, achieving 6.6x throughput improvement (195 → 1,368 samples/s). Includes progress plot, hypothesis tree, optimization breakdown, demux contention curve, and before/after pipeline code. Changes from initial draft: - Add a note block explaining the root cause of demux contention: FFmpeg's process-wide `codec_mutex` in `libavcodec/avcodec.c` serializes `codec->init()` for decoders flagged `FF_CODEC_CAP_NOT_INIT_THREADSAFE` (external libraries with unknown thread-safety), hit during `avformat_find_stream_info` probing on every `Demuxer` construction - Fix kept-improvements count in Experiment Statistics table from 13 to 12 to match the prose (12/79 ≈ 15% hit rate)
1 parent 633a423 commit 8c651b2

4 files changed

Lines changed: 332 additions & 0 deletions

File tree

3.8 MB
Loading
343 KB
Loading
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
.. _autoresearch-example-v2:
2+
3+
Example: Video Classification (MTP + Concurrency Tuning)
4+
=========================================================
5+
6+
This page describes the second autoresearch run on the video classification
7+
example pipeline. The first run (:ref:`autoresearch-example`) discovered
8+
GPU NVDEC decode, split demux/decode, and subclip windowing — reducing
9+
step time from 426ms to 153ms (2.78×). This run starts from that
10+
optimized baseline and pushes further by introducing subprocess isolation
11+
(MTP) and concurrency tuning, achieving a **7.0× throughput improvement**
12+
over the new baseline.
13+
14+
Setup
15+
-----
16+
17+
**Starting point**: The pipeline from the first autoresearch run, which
18+
uses GPU NVDEC decode with a split demux/decode architecture, subclip
19+
2s windowing, dedicated thread executors, and bf16 autocast.
20+
21+
**Pipeline**:
22+
23+
.. code-block:: text
24+
25+
Sampling → fetch (c=8) → disaggregate → demux (c=8) → NvdecDecode (c=16) → aggregate → collate
26+
27+
**Hardware**: 1×8 H100 GPUs (grandteton).
28+
29+
**Engine configuration**:
30+
31+
.. code-block:: bash
32+
33+
spdl autoresearch engine \
34+
--workflow spdl.autoresearch.pipeline_optimization:create_workflow \
35+
--workdir /data/users/moto/autoresearch/video_classification_opt_v5 \
36+
--max-concurrency 3 --platform auto \
37+
-- \
38+
--pipeline-script video_classification.py \
39+
--source-dir spdl/examples/video_classification \
40+
--build-command '...' \
41+
--base-launch-command 'torchx run ...' \
42+
--max-iterations 20 --patience 5 --job-timeout 600
43+
44+
The engine ran with up to 3 concurrent experiments and a 10-minute timeout
45+
per job.
46+
47+
48+
Baseline
49+
--------
50+
51+
The starting pipeline produced **195 samples/s** (3,120 fps) with 14.7%
52+
steady-state GPU SM utilization. The headspace analysis (CacheDataLoader)
53+
measured a compute floor of **37.6ms/step** — 94% of step time was spent
54+
waiting for data.
55+
56+
Unlike the first run, MTP (subprocess pipeline) succeeded this time because
57+
packet serialization support was added to SPDL, enabling demuxed
58+
``VideoPackets`` to be pickled across the process boundary. The MTP seed
59+
experiment provided a modest +4% improvement (202.8 sps) by isolating CPU
60+
data loading threads from CUDA kernel scheduling.
61+
62+
63+
Results
64+
-------
65+
66+
The best configuration (``gc_disabled_buffer_5``) achieved **1,368 samples/s**
67+
(21,890 fps), a **7.0× improvement** from the baseline of 195 sps (3,120 fps).
68+
The optimized pipeline is available in the repository at `commit fa5e934
69+
<https://github.com/facebookresearch/spdl/commit/fa5e934c5c783444bffb06f3a73ac27051ed4878>`_.
70+
71+
.. list-table:: Optimization Breakdown
72+
:header-rows: 1
73+
:widths: 45 15 15 15
74+
75+
* - Optimization
76+
- Throughput
77+
- Step Time
78+
- Cumulative Improvement
79+
* - **Baseline** (single process, demux c=8, NVDEC c=16)
80+
- 195 sps
81+
- 632ms
82+
- —
83+
* - \+ MTP subprocess isolation
84+
- 203 sps
85+
- 573ms
86+
- ↑4%
87+
* - \+ Subclip 0.5s (from 2.0s)
88+
- 317 sps
89+
- 395ms
90+
- ↑63%
91+
* - \+ NVDEC concurrency 7 (from 16)
92+
- 353 sps
93+
- 360ms
94+
- ↑81%
95+
* - \+ Subclip 0.5s + NVDEC c=7 combined
96+
- 386 sps
97+
- 332ms
98+
- ↑98%
99+
* - \+ Demux concurrency 4 (from 8)
100+
- 1,126 sps
101+
- 113ms
102+
- ↑478%
103+
* - \+ Demux concurrency 3
104+
- 1,294 sps
105+
- 93ms
106+
- ↑564%
107+
* - \+ GC disabled + frontend buffer=5
108+
- **1,368 sps**
109+
- **85ms**
110+
- **↑601%**
111+
112+
113+
Key Discoveries
114+
---------------
115+
116+
What worked
117+
~~~~~~~~~~~
118+
119+
1. **Reducing demux concurrency from 8 to 3** — The single biggest lever
120+
and the most counter-intuitive finding. Demuxing was assumed to be a
121+
near-trivial operation (milliseconds per item vs hundreds of
122+
milliseconds for decoding), so its concurrency seemed unimportant.
123+
In reality, demux threads contend severely at modest concurrency
124+
levels, and per-item latency rises sharply:
125+
126+
.. note::
127+
128+
A follow-up investigation traced the bottleneck to FFmpeg's
129+
``codec_mutex`` — a process-wide lock in `libavcodec/avcodec.c
130+
<https://ffmpeg.org/doxygen/8.0/avcodec_8c_source.html>`_
131+
that serializes ``codec->init()`` for any decoder flagged
132+
``FF_CODEC_CAP_NOT_INIT_THREADSAFE`` (applied to codecs backed
133+
by external libraries whose thread-safety status is unknown to
134+
FFmpeg). Every ``Demuxer`` construction calls
135+
``avformat_find_stream_info``, which internally opens decoders
136+
to probe stream parameters, hitting this lock once per stream.
137+
138+
.. list-table:: Demux Contention Curve
139+
:header-rows: 1
140+
:widths: 20 25 25
141+
142+
* - Concurrency
143+
- Per-item Latency (p50)
144+
- Throughput
145+
* - c=2
146+
- ~0.010s
147+
- 1,261 sps
148+
* - **c=3**
149+
- **0.013s**
150+
- **1,294 sps**
151+
* - c=4
152+
- 0.023s
153+
- 1,126 sps
154+
* - c=6
155+
- 0.067s
156+
- 580 sps
157+
* - c=8 (default)
158+
- 0.150s
159+
- 386 sps
160+
161+
Going from c=8 to c=3 reduced per-item demux latency by **11.5×**
162+
(0.150s → 0.013s). The agent mapped this curve methodically and
163+
pinpointed c=3 as the sweet spot.
164+
165+
2. **Shorter subclip duration (0.5s vs 2.0s)** — The first run found
166+
2.0s optimal. With MTP subprocess isolation and the improved pipeline
167+
architecture, 0.5s became viable — the demux seeking overhead that
168+
penalized short clips in the first run was eliminated by running demux
169+
in a separate process.
170+
171+
3. **MTP subprocess isolation** — Running demux in a subprocess via
172+
``spdl.pipeline.run_pipeline_in_subprocess`` isolates CPU-intensive
173+
FFmpeg container parsing from CUDA kernel launch scheduling. This
174+
required packet serialization support (pickle for ``VideoPackets``).
175+
176+
4. **GC management** — Disabling automatic garbage collection during
177+
training steps and running ``gc.collect()`` between epochs eliminated
178+
periodic latency spikes (~30ms every 50 steps).
179+
180+
5. **Frontend buffer size** — Increasing the frontend sink buffer from
181+
3 to 5 smoothed NVDEC timing jitter, providing a marginal but
182+
consistent improvement.
183+
184+
What did not work
185+
~~~~~~~~~~~~~~~~~
186+
187+
1. **Increasing demux concurrency (c=12, c=16)** — Every experiment
188+
with higher demux concurrency was worse than baseline. The intuition
189+
"more threads = more throughput" pointed in the wrong direction for
190+
this memory-bandwidth-bound stage.
191+
192+
2. **Larger batch sizes (alone)** — Batch=32 without other optimizations
193+
only yielded +5%. However, batch=32 combined with the full MTP +
194+
concurrency tuning stack was competitive (1,301 sps).
195+
196+
3. **CPU decode** — CPU-based FFmpeg decode with demux c=4 reached
197+
~1,060 sps — competitive but ~20% below NVDEC configurations.
198+
199+
4. **torch.compile** — Added compilation warmup with no steady-state
200+
improvement. The model (R3D-18 at 112×112) is too compute-light for
201+
compilation to matter.
202+
203+
5. **Streaming demux** — Both streaming demux variants crashed or stalled.
204+
205+
6. **Priority executor** — Provided no improvement over standard
206+
ThreadPoolExecutor configurations.
207+
208+
209+
Pipeline Architecture: Before and After
210+
----------------------------------------
211+
212+
**Before** (single process, 195 sps):
213+
214+
.. code-block:: python
215+
216+
pipeline = (
217+
PipelineBuilder()
218+
.add_source(source, continuous=True)
219+
.pipe(dataset.__getitem__, concurrency=8, executor=fetch_executor)
220+
.disaggregate()
221+
.pipe(Demux(...), concurrency=8, executor=demux_executor)
222+
.pipe(nvdec_decode, concurrency=16, executor=decode_executor)
223+
.aggregate(batch_size, drop_last=True)
224+
.pipe(collate)
225+
.add_sink(buffer_size=3)
226+
.build(num_threads=2)
227+
)
228+
229+
**After** (MTP subprocess split, 1,368 sps):
230+
231+
.. code-block:: python
232+
233+
# Backend (subprocess) — CPU-only: fetch → disaggregate → demux
234+
backend = (
235+
PipelineBuilder()
236+
.add_source(source, continuous=True)
237+
.pipe(partial(_fetch_sample, dataset=dataset), concurrency=num_fetch_threads)
238+
.disaggregate()
239+
.pipe(
240+
partial(_demux_sample, label_to_index=label_to_index, ...),
241+
concurrency=3, # not 8 — memory-bandwidth contention
242+
)
243+
.add_sink(buffer_size=3)
244+
)
245+
246+
source2 = spdl.pipeline.run_pipeline_in_subprocess(
247+
backend.get_config(),
248+
num_threads=max(num_fetch_threads, num_demux_threads),
249+
mp_context="forkserver",
250+
)
251+
252+
# Frontend (main process) — GPU NVDEC decode → aggregate → collate
253+
frontend = (
254+
PipelineBuilder()
255+
.add_source(source2, continuous=True)
256+
.pipe(nvdec_decode, concurrency=7, executor=decode_executor) # match H100 HW slots
257+
.aggregate(batch_size, drop_last=True)
258+
.pipe(collate)
259+
.add_sink(buffer_size=5)
260+
)
261+
pipeline = frontend.build(num_threads=2)
262+
263+
264+
Progress
265+
--------
266+
267+
The following plot shows throughput, step time, SM utilization, job
268+
duration, and raw SM utilization across all 111 experiments. Green dots
269+
mark improvements over the running best. The dashed blue line shows the
270+
headspace ceiling (3,405 samples/s steady-state compute floor).
271+
272+
.. image:: /_static/data/autoresearch_video_classification_v2_progress.png
273+
:alt: Autoresearch progress over 111 experiments
274+
:width: 100%
275+
276+
Throughput climbed steadily through the first 30 experiments as the
277+
engine discovered subclipping and NVDEC concurrency tuning. The
278+
breakthrough at experiment ~30 (reducing demux concurrency to c=4) caused
279+
a sharp jump past 1,000 sps. Subsequent experiments refined the demux
280+
sweet spot (c=3 vs c=2 vs c=4) and stacked GC/buffer tuning for the
281+
final result.
282+
283+
284+
Hypothesis Tree
285+
---------------
286+
287+
.. image:: /_static/data/autoresearch_video_classification_v2_hypothesis_tree.png
288+
:alt: Hypothesis tree for 111 experiments
289+
:width: 100%
290+
291+
The tree shows the engine exploring three main branches from the seed
292+
experiments: subclipping (the dominant branch), decode concurrency
293+
tuning, and batch size. The breakthrough path runs through
294+
subclip 0.5s → NVDEC c=7 → demux c=4 → demux c=3 → GC+buffer tuning.
295+
Failed experiments (red nodes) include high demux concurrency, streaming
296+
demux, and torch.compile — each failure narrowed the search space and
297+
confirmed that concurrency reduction, not increase, was the right
298+
direction.
299+
300+
301+
Remaining Headspace
302+
-------------------
303+
304+
.. list-table::
305+
:widths: 40 20
306+
307+
* - Compute floor (CacheDataLoader)
308+
- 37.6ms (3,405 sps)
309+
* - Best achieved
310+
- 85ms (1,368 sps)
311+
* - Remaining headspace
312+
- ~60%
313+
* - Bottleneck
314+
- NVDEC hardware decode rate (7 slots)
315+
316+
The best result is now limited by NVDEC frontend throughput — the 7
317+
hardware decoder slots on H100 are the binding constraint. Further
318+
improvement would likely require IPC optimizations (e.g. memory arenas
319+
to reduce pickle/unpickle overhead across the subprocess boundary) or
320+
pre-decoded datasets.
321+
322+
323+
Experiment Statistics
324+
---------------------
325+
326+
The engine ran more than 100 experiments over approximately 20 hours
327+
with up to 3 concurrent jobs. About a quarter failed at runtime —
328+
mostly aggressive configurations (high demux concurrency, streaming
329+
demux, ``torch.compile``, exotic GC settings) that the engine tried
330+
knowing some would fail. Of those that completed, 12 produced kept
331+
improvements.

docs/source/autoresearch/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ and GC alignment:
3131

3232
autoresearch
3333
autoresearch_example
34+
autoresearch_example_v2
3435
autoresearch_architecture

0 commit comments

Comments
 (0)