Video dataloading¶
This example uses SPDL to decode and batch video frames, then send them to GPU.
The structure of the pipeline is identical to that of
image_dataloading.
Basic Usage¶
Running this example requires a dataset consists of videos.
For example, to run this example with Kinetics dataset.
Download Kinetics dataset. https://github.com/cvdfoundation/kinetics-dataset provides scripts to facilitate this.
Create a list containing the downloaded videos.
cd /data/users/moto/kinetics-dataset/k400/ find train -name '*.mp4' > ~/imagenet.train.flist
Run the script.
python examples/video_dataloading.py --input-flist ~/kinetics400.train.flist --prefix /data/users/moto/kinetics-dataset/k400/ --num-threads 8
Using GPU video decoder¶
When SPDL is built with NVDEC integration enabled, and the GPUs support NVDEC,
providing --nvdec option switches the video decoder to NVDEC, using
spdl.io.decode_packets_nvdec(). When using this option, adjust the
number of threads (the number of concurrent decoding) to accommodate
the number of hardware video decoder available on GPUs.
For the details, please refer to https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
Note
This example decodes videos from the beginning to the end, so using NVDEC speeds up the whole decoding speed. But in cases where framees are sampled, CPU decoding with higher concurrency often yields higher throughput.
Source¶
Source
Click here to see the source.
1# Copyright (c) Meta Platforms, Inc. and affiliates.
2# All rights reserved.
3#
4# This source code is licensed under the BSD-style license found in the
5# LICENSE file in the root directory of this source tree.
6
7"""This example uses SPDL to decode and batch video frames, then send them to GPU.
8
9The structure of the pipeline is identical to that of
10:py:mod:`image_dataloading`.
11
12Basic Usage
13-----------
14
15Running this example requires a dataset consists of videos.
16
17For example, to run this example with Kinetics dataset.
18
191. Download Kinetics dataset.
20 https://github.com/cvdfoundation/kinetics-dataset provides scripts to facilitate this.
212. Create a list containing the downloaded videos.
22
23 .. code-block::
24
25 cd /data/users/moto/kinetics-dataset/k400/
26 find train -name '*.mp4' > ~/imagenet.train.flist
27
283. Run the script.
29
30 .. code-block:: shell
31
32 python examples/video_dataloading.py
33 --input-flist ~/kinetics400.train.flist
34 --prefix /data/users/moto/kinetics-dataset/k400/
35 --num-threads 8
36
37Using GPU video decoder
38-----------------------
39
40When SPDL is built with NVDEC integration enabled, and the GPUs support NVDEC,
41providing ``--nvdec`` option switches the video decoder to NVDEC, using
42:py:func:`spdl.io.decode_packets_nvdec`. When using this option, adjust the
43number of threads (the number of concurrent decoding) to accommodate
44the number of hardware video decoder available on GPUs.
45For the details, please refer to https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
46
47.. note::
48
49 This example decodes videos from the beginning to the end, so using NVDEC
50 speeds up the whole decoding speed. But in cases where framees are sampled,
51 CPU decoding with higher concurrency often yields higher throughput.
52"""
53
54import argparse
55import logging
56import signal
57import time
58from argparse import Namespace
59from collections.abc import Callable, Iterable
60from dataclasses import dataclass
61from pathlib import Path
62from threading import Event
63from types import FrameType
64
65import spdl.io
66import spdl.io.utils
67import torch
68from spdl.pipeline import Pipeline, PipelineBuilder
69from torch import Tensor
70
71_LG: logging.Logger = logging.getLogger(__name__)
72
73__all__ = [
74 "entrypoint",
75 "worker_entrypoint",
76 "benchmark",
77 "source",
78 "decode_video",
79 "decode_video_nvdec",
80 "get_pipeline",
81 "PerfResult",
82]
83
84
85def _parse_args(args: list[str]) -> Namespace:
86 parser = argparse.ArgumentParser(
87 description=__doc__,
88 )
89 parser.add_argument("--debug", action="store_true")
90 parser.add_argument("--input-flist", type=Path, required=True)
91 parser.add_argument("--max-samples", type=int, default=float("inf"))
92 parser.add_argument("--prefix", default="")
93 parser.add_argument("--trace", type=Path)
94 parser.add_argument("--queue-size", type=int, default=16)
95 parser.add_argument("--num-threads", type=int, required=True)
96 parser.add_argument("--worker-id", type=int, required=True)
97 parser.add_argument("--num-workers", type=int, required=True)
98 parser.add_argument("--nvdec", action="store_true")
99 ns = parser.parse_args(args)
100 if ns.trace:
101 ns.max_samples = 320
102 return ns
103
104
105def source(
106 input_flist: str,
107 prefix: str,
108 max_samples: int,
109 split_size: int = 1,
110 split_id: int = 0,
111) -> Iterable[str]:
112 """Iterate a file containing a list of paths, while optionally skipping some.
113
114 Args:
115 input_flist: A file contains list of video paths.
116 prefix: Prepended to the paths in the list.
117 max_samples: The maximum number of items to yield.
118 split_size: Split the paths in to this number of subsets.
119 split_id: The index of this split. Paths at ``line_number % split_size == split_id`` are returned.
120
121 Yields:
122 The paths of the specified split.
123 """
124 with open(input_flist, "r") as f:
125 num_yielded = 0
126 for i, line in enumerate(f):
127 if i % split_size != split_id:
128 continue
129 if line := line.strip():
130 yield prefix + line
131
132 if (num_yielded := num_yielded + 1) >= max_samples:
133 return
134
135
136def decode_video(
137 src: str | bytes,
138 width: int,
139 height: int,
140 device_index: int,
141) -> Tensor:
142 """Decode video and send decoded frames to GPU.
143
144 Args:
145 src: Data source. Passed to :py:func:`spdl.io.demux_video`.
146 width, height: The target resolution.
147 device_index: The index of the target GPU.
148
149 Returns:
150 A GPU tensor represents decoded video frames.
151 The dtype is uint8, the shape is ``[N, C, H, W]``, where ``N`` is the number
152 of frames in the video, ``C`` is RGB channels.
153 """
154 packets = spdl.io.demux_video(src)
155 frames = spdl.io.decode_packets(
156 packets,
157 filter_desc=spdl.io.get_filter_desc(
158 packets,
159 scale_width=width,
160 scale_height=height,
161 pix_fmt="rgb24",
162 ),
163 )
164 buffer = spdl.io.convert_frames(frames)
165 buffer = spdl.io.transfer_buffer(
166 buffer,
167 device_config=spdl.io.cuda_config(
168 device_index=device_index,
169 allocator=(
170 torch.cuda.caching_allocator_alloc,
171 torch.cuda.caching_allocator_delete,
172 ),
173 ),
174 )
175 return spdl.io.to_torch(buffer).permute(0, 2, 3, 1)
176
177
178def decode_video_nvdec(
179 src: str,
180 device_index: int,
181 width: int,
182 height: int,
183) -> Tensor:
184 """Decode video using NVDEC.
185
186 Args:
187 src: Data source. Passed to :py:func:`spdl.io.demux_video`.
188 device_index: The index of the target GPU.
189 width, height: The target resolution.
190
191 Returns:
192 A GPU tensor represents decoded video frames.
193 The dtype is uint8, the shape is ``[N, C, H, W]``, where ``N`` is the number
194 of frames in the video, ``C`` is RGB channels.
195 """
196 packets = spdl.io.demux_video(src)
197 buffer = spdl.io.decode_packets_nvdec(
198 packets,
199 device_config=spdl.io.cuda_config(
200 device_index=device_index,
201 allocator=(
202 torch.cuda.caching_allocator_alloc,
203 torch.cuda.caching_allocator_delete,
204 ),
205 ),
206 scale_width=width,
207 scale_height=height,
208 pix_fmt="rgb",
209 )
210 return spdl.io.to_torch(buffer)[..., :3].permute(0, 2, 3, 1)
211
212
213def _get_decode_fn(
214 device_index: int, use_nvdec: bool, width: int = 222, height: int = 222
215) -> Callable[[str], Tensor]:
216 if use_nvdec:
217
218 def _decode_func(src: str) -> Tensor:
219 return decode_video_nvdec(src, device_index, width, height)
220
221 else:
222
223 def _decode_func(src: str) -> Tensor:
224 return decode_video(src, width, height, device_index)
225
226 return _decode_func
227
228
229def get_pipeline(
230 src: Iterable[str],
231 decode_fn: Callable[[str], Tensor],
232 decode_concurrency: int,
233 num_threads: int,
234 buffer_size: int = 3,
235) -> Pipeline:
236 """Construct the video loading pipeline.
237
238 Args:
239 src: Pipeline source. Generator that yields image paths. See :py:func:`source`.
240 decode_fn: Function that decode the given image and send the decoded frames to GPU.
241 decode_concurrency: The maximum number of decoding scheduled concurrently.
242 num_threads: The number of threads in the pipeline.
243 buffer_size: The size of buffer for the resulting batch image Tensor.
244 """
245 return (
246 PipelineBuilder()
247 .add_source(src)
248 .pipe(decode_fn, concurrency=decode_concurrency)
249 .add_sink(buffer_size)
250 .build(num_threads=num_threads, report_stats_interval=15)
251 )
252
253
254def _get_pipeline(args: Namespace) -> Pipeline:
255 src = source(
256 input_flist=args.input_flist,
257 prefix=args.prefix,
258 max_samples=args.max_samples,
259 split_id=args.worker_id,
260 split_size=args.num_workers,
261 )
262
263 decode_fn = _get_decode_fn(args.worker_id, args.nvdec)
264 pipeline = get_pipeline(
265 src,
266 decode_fn,
267 decode_concurrency=args.num_threads,
268 num_threads=args.num_threads + 3,
269 buffer_size=args.queue_size,
270 )
271 print(pipeline)
272 return pipeline
273
274
275@dataclass
276class PerfResult:
277 """Used to report the worker performance to the main process."""
278
279 elapsed: float
280 """The time it took to process all the inputs."""
281
282 num_batches: int
283 """The number of batches processed."""
284
285 num_frames: int
286 """The number of frames processed."""
287
288
289def benchmark(
290 dataloader: Iterable[Tensor],
291 stop_requested: Event,
292) -> PerfResult:
293 """The main loop that measures the performance of dataloading.
294
295 Args:
296 dataloader: The dataloader to benchmark.
297 stop_requested: Used to interrupt the benchmark loop.
298
299 Returns:
300 The performance result.
301 """
302 t0 = time.monotonic()
303 num_frames = num_batches = 0
304 try:
305 for batches in dataloader:
306 for batch in batches:
307 num_frames += batch.shape[0]
308 num_batches += 1
309
310 if stop_requested.is_set():
311 break
312
313 finally:
314 elapsed = time.monotonic() - t0
315 fps = num_frames / elapsed
316 _LG.info(f"FPS={fps:.2f} ({num_frames} / {elapsed:.2f}), (Done {num_frames})")
317
318 return PerfResult(elapsed, num_batches, num_frames)
319
320
321def worker_entrypoint(args_: list[str]) -> PerfResult:
322 """Entrypoint for worker process. Load images to a GPU and measure its performance.
323
324 It builds a Pipeline object using :py:func:`get_pipeline` function and run it with
325 :py:func:`benchmark` function.
326 """
327 args = _parse_args(args_)
328 _init(args.debug, args.worker_id)
329
330 _LG.info(args)
331
332 pipeline = _get_pipeline(args)
333
334 device = torch.device(f"cuda:{args.worker_id}")
335
336 ev: Event = Event()
337
338 def handler_stop_signals(_signum: int, _frame: FrameType | None) -> None:
339 ev.set()
340
341 signal.signal(signal.SIGTERM, handler_stop_signals)
342
343 # Warm up
344 torch.zeros([1, 1], device=device)
345
346 trace_path = f"{args.trace}.{args.worker_id}"
347 with (
348 pipeline.auto_stop(),
349 spdl.io.utils.tracing(trace_path, enable=args.trace is not None),
350 ):
351 return benchmark(pipeline.get_iterator(), ev)
352
353
354def _init_logging(debug: bool = False, worker_id: int | None = None) -> None:
355 fmt = "%(asctime)s [%(levelname)s] %(message)s"
356 if worker_id is not None:
357 fmt = f"[{worker_id}:%(thread)d] {fmt}"
358 level = logging.DEBUG if debug else logging.INFO
359 logging.basicConfig(format=fmt, level=level)
360
361
362def _init(debug: bool, worker_id: int) -> None:
363 _init_logging(debug, worker_id)
364
365
366def _parse_process_args(args: list[str] | None) -> tuple[Namespace, list[str]]:
367 parser = argparse.ArgumentParser(
368 description=__doc__,
369 )
370 parser.add_argument("--num-workers", type=int, default=8)
371 return parser.parse_known_args(args)
372
373
374def entrypoint(args: list[str] | None = None) -> None:
375 """CLI entrypoint. Launch the worker processes, each of which load videos and send them to GPU."""
376 ns, args = _parse_process_args(args)
377
378 args_set = [
379 [*args, f"--worker-id={i}", f"--num-workers={ns.num_workers}"]
380 for i in range(ns.num_workers)
381 ]
382
383 from multiprocessing import Pool
384
385 with Pool(processes=ns.num_workers) as pool:
386 _init_logging()
387 _LG.info("Spawned: %d workers", ns.num_workers)
388
389 vals = pool.map(worker_entrypoint, args_set)
390
391 ave_time = sum(v.elapsed for v in vals) / len(vals)
392 total_frames = sum(v.num_frames for v in vals)
393 total_batches = sum(v.num_batches for v in vals)
394
395 _LG.info(f"{ave_time=:.2f}, {total_frames=}, {total_batches=}")
396
397 FPS = total_frames / ave_time
398 BPS = total_batches / ave_time
399 _LG.info(f"Aggregated {FPS=:.2f}, {BPS=:.2f}")
400
401
402if __name__ == "__main__":
403 entrypoint()
API Reference¶
Functions
- entrypoint(args: list[str] | None = None) None[source]¶
CLI entrypoint. Launch the worker processes, each of which load videos and send them to GPU.
- worker_entrypoint(args_: list[str]) PerfResult[source]¶
Entrypoint for worker process. Load images to a GPU and measure its performance.
It builds a Pipeline object using
get_pipeline()function and run it withbenchmark()function.
- benchmark(dataloader: Iterable[Tensor], stop_requested: Event) PerfResult[source]¶
The main loop that measures the performance of dataloading.
- Parameters:
dataloader – The dataloader to benchmark.
stop_requested – Used to interrupt the benchmark loop.
- Returns:
The performance result.
- source(input_flist: str, prefix: str, max_samples: int, split_size: int = 1, split_id: int = 0) Iterable[str][source]¶
Iterate a file containing a list of paths, while optionally skipping some.
- Parameters:
input_flist – A file contains list of video paths.
prefix – Prepended to the paths in the list.
max_samples – The maximum number of items to yield.
split_size – Split the paths in to this number of subsets.
split_id – The index of this split. Paths at
line_number % split_size == split_idare returned.
- Yields:
The paths of the specified split.
- decode_video(src: str | bytes, width: int, height: int, device_index: int) Tensor[source]¶
Decode video and send decoded frames to GPU.
- Parameters:
src – Data source. Passed to
spdl.io.demux_video().width – The target resolution.
height – The target resolution.
device_index – The index of the target GPU.
- Returns:
A GPU tensor represents decoded video frames. The dtype is uint8, the shape is
[N, C, H, W], whereNis the number of frames in the video,Cis RGB channels.
- decode_video_nvdec(src: str, device_index: int, width: int, height: int) Tensor[source]¶
Decode video using NVDEC.
- Parameters:
src – Data source. Passed to
spdl.io.demux_video().device_index – The index of the target GPU.
width – The target resolution.
height – The target resolution.
- Returns:
A GPU tensor represents decoded video frames. The dtype is uint8, the shape is
[N, C, H, W], whereNis the number of frames in the video,Cis RGB channels.
- get_pipeline(src: Iterable[str], decode_fn: Callable[[str], Tensor], decode_concurrency: int, num_threads: int, buffer_size: int = 3) Pipeline[source]¶
Construct the video loading pipeline.
- Parameters:
src – Pipeline source. Generator that yields image paths. See
source().decode_fn – Function that decode the given image and send the decoded frames to GPU.
decode_concurrency – The maximum number of decoding scheduled concurrently.
num_threads – The number of threads in the pipeline.
buffer_size – The size of buffer for the resulting batch image Tensor.
Classes