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.

  1. Download Kinetics dataset. https://github.com/cvdfoundation/kinetics-dataset provides scripts to facilitate this.

  2. Create a list containing the downloaded videos.

    cd /data/users/moto/kinetics-dataset/k400/
    find train -name '*.mp4' > ~/imagenet.train.flist
    
  3. 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 with benchmark() 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_id are 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], where N is the number of frames in the video, C is 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], where N is the number of frames in the video, C is 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

class PerfResult(elapsed: float, num_batches: int, num_frames: int)[source]

Used to report the worker performance to the main process.

elapsed: float

The time it took to process all the inputs.

num_batches: int

The number of batches processed.

num_frames: int

The number of frames processed.