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