99"""SPDL pipeline construction for video classification.
1010
1111Provides ``build_pipeline`` which assembles a GPU NVDEC-accelerated SPDL
12- pipeline with a split demux/decode architecture ::
12+ pipeline using Multi-Threading in subprocess (MTP) ::
1313
14- sample → fetch → disaggregate → demux (CPU) → decode (NVDEC) → aggregate → collate
14+ Backend (subprocess): sample → fetch → disaggregate → demux (CPU)
15+ Frontend (main process): GPU decode (NVDEC) → aggregate → collate
16+
17+ Demuxed packets are serialized across the process boundary, isolating
18+ CPU-intensive demux work from CUDA kernel scheduling in the training
19+ process.
1520
1621The pipeline is dataset-agnostic: any dataset whose ``__getitem__`` returns
1722``list[{"video_bytes": bytes, "label": str}]`` can be used.
2025from __future__ import annotations
2126
2227import argparse
28+ import logging
2329import os
2430from collections .abc import Iterator
2531from concurrent .futures import ThreadPoolExecutor
32+ from functools import partial
2633from typing import Protocol
2734
2835import spdl .io
36+ import spdl .pipeline
2937import spdl .source .utils
3038import torch
3139from spdl .pipeline import PipelineBuilder
3240from spdl .source import DistributedRandomSampler
3341
42+ _LG : logging .Logger = logging .getLogger (__name__ )
43+
3444type _TBatch = dict [str , torch .Tensor ]
3545
3646
@@ -39,24 +49,36 @@ def __len__(self) -> int: ...
3949 def __getitem__ (self , index : int ) -> list [dict [str , object ]]: ...
4050
4151
42- class Demux :
43- """Demux video bytes into packets and resolve label to index."""
52+ # ---------------------------------------------------------------------------
53+ # Backend (subprocess) stage functions — must be picklable.
54+ # Using module-level functions + functools.partial.
55+ # ---------------------------------------------------------------------------
4456
45- def __init__ (
46- self , label_to_index : dict [str , int ], subclip_duration : float | None = None
47- ) -> None :
48- self .label_to_index = label_to_index
49- self .subclip_duration = subclip_duration
5057
51- def __call__ (self , sample : dict [str , object ]) -> dict [str , object ] | None :
52- video_bytes = sample ["video_bytes" ]
53- try :
54- timestamp = (0.0 , self .subclip_duration ) if self .subclip_duration else None
55- packets = spdl .io .demux_video (video_bytes , timestamp = timestamp )
56- except RuntimeError :
57- return None
58- label = self .label_to_index [sample ["label" ]] # pyre-ignore[6]
59- return {"packets" : packets , "label" : label }
58+ def _fetch_sample (index : int , * , dataset : object ) -> list [dict [str , object ]]:
59+ return dataset [index ] # pyre-ignore[16]
60+
61+
62+ def _demux_sample (
63+ sample : dict [str , object ],
64+ * ,
65+ label_to_index : dict [str , int ],
66+ subclip_duration : float | None = None ,
67+ ) -> dict [str , object ] | None :
68+ video_bytes = sample ["video_bytes" ]
69+ try :
70+ timestamp = (0.0 , subclip_duration ) if subclip_duration else None
71+ packets = spdl .io .demux_video (video_bytes , timestamp = timestamp )
72+ except RuntimeError as e :
73+ _LG .warning ("Demux failed: %s" , e )
74+ return None
75+ label = label_to_index [sample ["label" ]] # pyre-ignore[6]
76+ return {"packets" : packets , "label" : label }
77+
78+
79+ # ---------------------------------------------------------------------------
80+ # Frontend (main process) — GPU decode, runs in the training process.
81+ # ---------------------------------------------------------------------------
6082
6183
6284class NvdecDecode :
@@ -92,7 +114,8 @@ def __call__(
92114 pix_fmt = "rgb" ,
93115 )
94116 tensor = spdl .io .to_torch (buffer ) # [T, C, H, W], already on GPU
95- except RuntimeError :
117+ except RuntimeError as e :
118+ _LG .warning ("NVDEC decode failed: %s" , e )
96119 return None
97120
98121 # Frame sampling
@@ -130,20 +153,22 @@ def build_pipeline(
130153 rank : int ,
131154 world_size : int ,
132155) -> Iterator [_TBatch ]:
133- """Build a multithreaded SPDL pipeline with GPU NVDEC video decoding .
156+ """Build an MTP pipeline with subprocess demux and GPU NVDEC decode .
134157
135- Uses the GPU's dedicated NVDEC hardware decoders instead of CPU FFmpeg,
136- with a split demux/decode architecture. Each stage runs on a dedicated
137- thread executor to eliminate pool contention.
158+ Splits the pipeline into a backend subprocess (CPU-only: fetch,
159+ disaggregate, demux) and a frontend main process (GPU NVDEC decode,
160+ aggregate, collate). Demuxed packets are serialized across the
161+ process boundary, isolating CPU work from CUDA kernel scheduling.
138162
139163 Pipeline stages::
140164
141- sample → fetch → disaggregate → demux (CPU) → decode (NVDEC) → aggregate → collate
165+ Backend (subprocess): sample → fetch → disaggregate → demux
166+ Frontend (main process): NVDEC decode → aggregate → collate
142167
143168 Args:
144169 args: CLI args providing ``num_fetch_threads``, ``num_decode_threads``,
145- ``num_frames ``, ``frame_width ``, ``frame_height``, ``batch_size ``,
146- and ``subclip_duration``.
170+ ``num_demux_threads ``, ``num_frames ``, ``frame_width ``,
171+ ``frame_height``, ``batch_size``, and ``subclip_duration``.
147172 dataset: Any object implementing the ``VideoDataset`` protocol.
148173 label_to_index: Mapping from label string to class index.
149174 rank: Current distributed rank.
@@ -165,17 +190,44 @@ def build_pipeline(
165190
166191 num_fetch_threads = args .num_fetch_threads
167192 num_decode_threads = args .num_decode_threads
193+ num_demux_threads = args .num_demux_threads
168194
169195 source = spdl .source .utils .embed_shuffle (
170196 DistributedRandomSampler (
171- len (dataset ), rank = rank , world_size = world_size
172- ) # pyre-ignore[6]
197+ len (dataset ),
198+ rank = rank ,
199+ world_size = world_size , # pyre-ignore[6]
200+ )
173201 )
174202
175- fetch_executor = ThreadPoolExecutor (max_workers = num_fetch_threads )
176- demux_executor = ThreadPoolExecutor (max_workers = 8 )
177- decode_executor = ThreadPoolExecutor (max_workers = num_decode_threads )
203+ # Backend (subprocess) — CPU-only stages: fetch → disaggregate → demux
204+ backend = (
205+ PipelineBuilder ()
206+ .add_source (source , continuous = True )
207+ .pipe (
208+ partial (_fetch_sample , dataset = dataset ),
209+ concurrency = num_fetch_threads ,
210+ )
211+ .disaggregate ()
212+ .pipe (
213+ partial (
214+ _demux_sample ,
215+ label_to_index = label_to_index ,
216+ subclip_duration = args .subclip_duration ,
217+ ),
218+ concurrency = num_demux_threads ,
219+ )
220+ .add_sink (buffer_size = 3 )
221+ )
178222
223+ source2 = spdl .pipeline .run_pipeline_in_subprocess (
224+ backend .get_config (),
225+ num_threads = max (num_fetch_threads , num_demux_threads ),
226+ mp_context = "forkserver" ,
227+ )
228+
229+ # Frontend (main process) — GPU NVDEC decode → aggregate → collate
230+ decode_executor = ThreadPoolExecutor (max_workers = num_decode_threads )
179231 nvdec_decode = NvdecDecode (
180232 num_frames = args .num_frames ,
181233 cuda_cfg = cuda_cfg ,
@@ -184,29 +236,13 @@ def build_pipeline(
184236 device = device ,
185237 )
186238
187- pipeline = (
239+ frontend = (
188240 PipelineBuilder ()
189- .add_source (source , continuous = True )
190- .pipe (
191- dataset .__getitem__ ,
192- concurrency = num_fetch_threads ,
193- executor = fetch_executor ,
194- ) # pyre-ignore[6]
195- .disaggregate ()
196- .pipe (
197- Demux (label_to_index , subclip_duration = args .subclip_duration ),
198- concurrency = 8 ,
199- executor = demux_executor ,
200- )
201- .pipe (
202- nvdec_decode ,
203- concurrency = num_decode_threads ,
204- executor = decode_executor ,
205- )
241+ .add_source (source2 , continuous = True )
242+ .pipe (nvdec_decode , concurrency = num_decode_threads , executor = decode_executor )
206243 .aggregate (args .batch_size , drop_last = True )
207244 .pipe (collate )
208- .add_sink (buffer_size = 3 )
209- .build (num_threads = 2 )
245+ .add_sink (buffer_size = 5 )
210246 )
211-
247+ pipeline = frontend . build ( num_threads = 2 )
212248 return pipeline .get_iterator (timeout = 300 )
0 commit comments