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