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.
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
Pipelineobject usingget_pipeline()function and run it withbenchmark()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_idare 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