Image dataloading

Benchmark the performance of loading images from local file system to GPUs.

Given a list of image files to process, this script spawns subprocesses, each of which load images and send them to the corresponding GPUs, then collect the runtime statistics.

flowchart A[Main Process] subgraph P1[Worker Process 1] subgraph TP1[Thread Pool] t11[Thread] t12[Thread] end end G1[GPU 1] subgraph P3[Worker Process N] subgraph TP3[Thread Pool] t31[Thread] t32[Thread] end end G3[GPU N] A --> P1 A --> P3 t11 --> G1 t12 --> G1 t31 --> G3 t32 --> G3

A file list can be created, for example, by:

cd /data/users/moto/imagenet/
find train -name '*.JPEG' > ~/imagenet.train.flist

To run the benchmark, pass it to the script like the following.

python image_dataloading.py
    --input-flist ~/imagenet.train.flist
    --prefix /data/users/moto/imagenet/
    --num-workers 8 # The number of GPUs

Source

Source

Click here to see the source.
  1#!/usr/bin/env python3
  2# Copyright (c) Meta Platforms, Inc. and affiliates.
  3# All rights reserved.
  4#
  5# This source code is licensed under the BSD-style license found in the
  6# LICENSE file in the root directory of this source tree.
  7
  8"""Benchmark the performance of loading images from local file system to GPUs.
  9
 10Given a list of image files to process, this script spawns subprocesses,
 11each of which load images and send them to the corresponding GPUs, then
 12collect the runtime statistics.
 13
 14.. include:: ../plots/image_dataloading_chart.txt
 15
 16A file list can be created, for example, by:
 17
 18.. code-block:: bash
 19
 20   cd /data/users/moto/imagenet/
 21   find train -name '*.JPEG' > ~/imagenet.train.flist
 22
 23To run the benchmark,  pass it to the script like the following.
 24
 25.. code-block::
 26
 27   python image_dataloading.py
 28       --input-flist ~/imagenet.train.flist
 29       --prefix /data/users/moto/imagenet/
 30       --num-workers 8 # The number of GPUs
 31"""
 32
 33# pyre-strict
 34
 35from __future__ import annotations
 36
 37import argparse
 38import logging
 39import signal
 40import time
 41from argparse import Namespace
 42from collections.abc import Iterator
 43from dataclasses import dataclass
 44from functools import partial
 45from pathlib import Path
 46from threading import Event
 47from types import FrameType
 48
 49import spdl.io
 50import spdl.io.utils
 51import torch
 52from spdl.io import CUDAConfig
 53from spdl.pipeline import Pipeline, PipelineBuilder
 54from torch import Tensor
 55
 56_LG: logging.Logger = logging.getLogger(__name__)
 57
 58__all__ = [
 59    "entrypoint",
 60    "worker_entrypoint",
 61    "benchmark",
 62    "source",
 63    "batch_decode",
 64    "get_pipeline",
 65    "PerfResult",
 66]
 67
 68
 69def _parse_args(args: list[str]) -> Namespace:
 70    parser = argparse.ArgumentParser(
 71        description=__doc__,
 72        formatter_class=argparse.RawDescriptionHelpFormatter,
 73    )
 74    parser.add_argument("--debug", action="store_true")
 75    parser.add_argument("--input-flist", type=Path, required=True)
 76    parser.add_argument("--max-samples", type=int)
 77    parser.add_argument("--prefix", required=True)
 78    parser.add_argument("--batch-size", type=int, default=32)
 79    parser.add_argument("--trace", type=Path)
 80    parser.add_argument("--buffer-size", type=int, default=16)
 81    parser.add_argument("--num-threads", type=int, default=16)
 82    parser.add_argument("--worker-id", type=int, required=True)
 83    parser.add_argument("--num-workers", type=int, required=True)
 84    ns = parser.parse_args(args)
 85    if ns.trace:
 86        ns.max_samples = ns.batch_size * 40
 87    return ns
 88
 89
 90def source(
 91    path: Path,
 92    prefix: str = "",
 93    split_size: int = 1,
 94    split_id: int = 0,
 95) -> Iterator[str]:
 96    """Iterate a file containing a list of paths, while optionally skipping some.
 97
 98    Args:
 99        path: Path to the file containing list of file paths.
100        prefix: Prepended to the paths in the list.
101        split_size: Split the paths in to this number of subsets.
102        split_id: The index of this split.
103            Paths at ``line_number % split_size == split_id`` are returned.
104
105    Yields:
106        Path: The paths of the specified split.
107    """
108    with open(path) as f:
109        for i, line in enumerate(f):
110            if i % split_size == split_id:
111                if line := line.strip():
112                    yield prefix + line
113
114
115def batch_decode(
116    srcs: list[str],
117    device_config: spdl.io.CUDAConfig,
118    width: int = 224,
119    height: int = 224,
120) -> Tensor:
121    """Given image paths, decode, resize, batch and optionally send them to GPU.
122
123    Args:
124        srcs: List of image paths.
125        width, height: The size of the images to batch.
126        device_config: When provided, the data are sent to the specified GPU.
127
128    Returns:
129        The batch tensor.
130    """
131    buffer = spdl.io.load_image_batch(
132        srcs,
133        width=width,
134        height=height,
135        pix_fmt="rgb24",
136        device_config=device_config,
137        strict=False,
138    )
139    return spdl.io.to_torch(buffer)
140
141
142def get_pipeline(
143    src: Iterator[str],
144    batch_size: int,
145    device_config: CUDAConfig,
146    buffer_size: int,
147    num_threads: int,
148) -> Pipeline:
149    """Build image data loading pipeline.
150
151    The pipeline uses :py:func:`batch_decode` for decoding images concurrently
152    and send the resulting data to GPU.
153
154    Args:
155        src: Pipeline source. Generator that yields image paths.
156            See :py:func:`source`.
157        batch_size: The number of images in a batch.
158        device_config: The configuration of target CUDA device.
159        buffer_size: The size of buffer for the resulting batch image Tensor.
160        num_threads: The number of threads in the pipeline.
161
162    Returns:
163        The pipeline that performs batch image decoding and device transfer.
164    """
165    decode = partial(batch_decode, device_config=device_config)
166
167    pipeline = (
168        PipelineBuilder()
169        .add_source(src)
170        .aggregate(batch_size)
171        .pipe(decode, concurrency=num_threads)
172        .add_sink(buffer_size)
173        .build(num_threads=num_threads, report_stats_interval=15)
174    )
175    return pipeline
176
177
178def _get_pipeline(args: Namespace) -> Pipeline:
179    return get_pipeline(
180        source(args.input_flist, args.prefix, args.num_workers, args.worker_id),
181        args.batch_size,
182        device_config=(
183            spdl.io.cuda_config(
184                device_index=args.worker_id,
185                allocator=(
186                    torch.cuda.caching_allocator_alloc,
187                    torch.cuda.caching_allocator_delete,
188                ),
189            )
190        ),
191        buffer_size=args.buffer_size,
192        num_threads=args.num_threads,
193    )
194
195
196@dataclass
197class PerfResult:
198    """Used to report the worker performance to the main process."""
199
200    elapsed: float
201    """The time it took to process all the inputs."""
202
203    num_batches: int
204    """The number of batches processed."""
205
206    num_frames: int
207    """The number of frames processed."""
208
209
210def worker_entrypoint(args_: list[str]) -> PerfResult:
211    """Entrypoint for worker process. Load images to a GPU and measure its performance.
212
213    It builds a :py:class:`~spdl.pipeline.Pipeline` object using :py:func:`get_pipeline`
214    function and run it with :py:func:`benchmark` function.
215    """
216    args = _parse_args(args_)
217    _init(args.debug, args.worker_id)
218
219    _LG.info(args)
220
221    pipeline = _get_pipeline(args)
222    print(pipeline)
223
224    device = torch.device(f"cuda:{args.worker_id}")
225
226    ev: Event = Event()
227
228    def handler_stop_signals(_signum: int, _frame: FrameType | None) -> None:
229        ev.set()
230
231    signal.signal(signal.SIGTERM, handler_stop_signals)
232
233    # Warm up
234    torch.zeros([1, 1], device=device)
235
236    trace_path = f"{args.trace}.{args.worker_id}"
237    with (
238        pipeline.auto_stop(),
239        spdl.io.utils.tracing(trace_path, enable=args.trace is not None),
240    ):
241        return benchmark(pipeline.get_iterator(), ev)
242
243
244def benchmark(loader: Iterator[Tensor], stop_requested: Event) -> PerfResult:
245    """The main loop that measures the performance of dataloading.
246
247    Args:
248        loader: The dataloader to benchmark.
249        stop_requested: Used to interrupt the benchmark loop.
250
251    Returns:
252        The performance result.
253    """
254    t0 = time.monotonic()
255    num_frames = num_batches = 0
256    try:
257        for batch in loader:
258            num_frames += batch.shape[0]
259            num_batches += 1
260
261            if stop_requested.is_set():
262                break
263
264    finally:
265        elapsed = time.monotonic() - t0
266
267    return PerfResult(elapsed, num_batches, num_frames)
268
269
270def _init_logging(debug: bool = False, worker_id: int | None = None) -> None:
271    fmt = "%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s"
272    if worker_id is not None:
273        fmt = f"[{worker_id}:%(thread)d] {fmt}"
274    level = logging.DEBUG if debug else logging.INFO
275    logging.basicConfig(format=fmt, level=level)
276
277
278def _init(debug: bool, worker_id: int) -> None:
279    _init_logging(debug, worker_id)
280
281
282def _parse_process_args(args: list[str] | None) -> tuple[Namespace, list[str]]:
283    parser = argparse.ArgumentParser(
284        description=__doc__,
285        formatter_class=argparse.RawDescriptionHelpFormatter,
286    )
287    parser.add_argument("--num-workers", type=int, default=8)
288    return parser.parse_known_args(args)
289
290
291def entrypoint(args: list[str] | None = None) -> None:
292    """CLI entrypoint. Launch the worker processes,
293    each of which load images and send them to GPU."""
294    ns, args = _parse_process_args(args)
295
296    args_set = [
297        [*args, f"--worker-id={i}", f"--num-workers={ns.num_workers}"]
298        for i in range(ns.num_workers)
299    ]
300
301    from multiprocessing import Pool
302
303    with Pool(processes=ns.num_workers) as pool:
304        _init_logging()
305        _LG.info("Spawned: %d workers", ns.num_workers)
306
307        vals = pool.map(worker_entrypoint, args_set)
308
309    ave_time = sum(v.elapsed for v in vals) / len(vals)
310    total_frames = sum(v.num_frames for v in vals)
311    total_batches = sum(v.num_batches for v in vals)
312
313    _LG.info(f"{ave_time=:.2f}, {total_frames=}, {total_batches=}")
314
315    FPS = total_frames / ave_time
316    BPS = total_batches / ave_time
317    _LG.info(f"Aggregated {FPS=:.2f}, {BPS=:.2f}")
318
319
320if __name__ == "__main__":
321    entrypoint()

API Reference

Functions

entrypoint(args: list[str] | None = None) None[source]

CLI entrypoint. Launch the worker processes, each of which load images 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(loader: Iterator[Tensor], stop_requested: Event) PerfResult[source]

The main loop that measures the performance of dataloading.

Parameters:
  • loader – The dataloader to benchmark.

  • stop_requested – Used to interrupt the benchmark loop.

Returns:

The performance result.

source(path: Path, prefix: str = '', split_size: int = 1, split_id: int = 0) Iterator[str][source]

Iterate a file containing a list of paths, while optionally skipping some.

Parameters:
  • path – Path to the file containing list of file paths.

  • prefix – Prepended to the paths in the list.

  • 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:

Path – The paths of the specified split.

batch_decode(srcs: list[str], device_config: CUDAConfig, width: int = 224, height: int = 224) Tensor[source]

Given image paths, decode, resize, batch and optionally send them to GPU.

Parameters:
  • srcs – List of image paths.

  • width – The size of the images to batch.

  • height – The size of the images to batch.

  • device_config – When provided, the data are sent to the specified GPU.

Returns:

The batch tensor.

get_pipeline(src: Iterator[str], batch_size: int, device_config: CUDAConfig, buffer_size: int, num_threads: int) Pipeline[source]

Build image data loading pipeline.

The pipeline uses batch_decode() for decoding images concurrently and send the resulting data to GPU.

Parameters:
  • src – Pipeline source. Generator that yields image paths. See source().

  • batch_size – The number of images in a batch.

  • device_config – The configuration of target CUDA device.

  • buffer_size – The size of buffer for the resulting batch image Tensor.

  • num_threads – The number of threads in the pipeline.

Returns:

The pipeline that performs batch image decoding and device transfer.

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.