Imagenet classification¶
Benchmark the performance of loading images from local file systems and classifying them using a GPU.
This script builds the data loader and instantiates an image classification model in a GPU. The data loader transfers the batch image data to the GPU concurrently, and the foreground thread run the model on data one by one.
To run the benchmark, pass it to the script like the following.
python imagenet_classification.py
--root-dir ~/imagenet/
--split val
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"""Benchmark the performance of loading images from local file systems and
8classifying them using a GPU.
9
10This script builds the data loader and instantiates an image
11classification model in a GPU.
12The data loader transfers the batch image data to the GPU concurrently, and
13the foreground thread run the model on data one by one.
14
15.. include:: ../plots/imagenet_classification_chart.txt
16
17To run the benchmark, pass it to the script like the following.
18
19.. code-block::
20
21 python imagenet_classification.py
22 --root-dir ~/imagenet/
23 --split val
24"""
25
26import argparse
27import contextlib
28import logging
29import time
30from argparse import Namespace
31from collections.abc import Callable, Iterator
32from pathlib import Path
33
34import spdl.io
35import spdl.io.utils
36import torch
37from spdl.dataloader import DataLoader
38from spdl.source.imagenet import ImageNet
39from torch import Tensor
40from torch.profiler import profile
41
42_LG: logging.Logger = logging.getLogger(__name__)
43
44
45__all__ = [
46 "entrypoint",
47 "benchmark",
48 "get_decode_func",
49 "get_dataloader",
50 "get_model",
51 "ModelBundle",
52 "Classification",
53 "Preprocessing",
54]
55
56
57def _parse_args(args: list[str] | None) -> Namespace:
58 parser = argparse.ArgumentParser(
59 description=__doc__,
60 formatter_class=argparse.RawDescriptionHelpFormatter,
61 )
62 parser.add_argument("--debug", action="store_true")
63 parser.add_argument("--root-dir", type=Path, required=True)
64 parser.add_argument("--max-batches", type=int, default=float("inf"))
65 parser.add_argument("--batch-size", type=int, default=32)
66 parser.add_argument("--split", default="val", choices=["train", "val"])
67 parser.add_argument("--trace", type=Path)
68 parser.add_argument("--buffer-size", type=int, default=16)
69 parser.add_argument("--num-threads", type=int, default=16)
70 parser.add_argument("--no-compile", action="store_false", dest="compile")
71 parser.add_argument("--no-bf16", action="store_false", dest="use_bf16")
72 parser.add_argument("--use-nvjpeg", action="store_true")
73 ns = parser.parse_args(args)
74 if ns.trace:
75 ns.max_batches = 60
76 return ns
77
78
79# Handroll the transforms so as to support `torch.compile`
80class Preprocessing(torch.nn.Module):
81 """Perform pixel normalization and data type conversion.
82
83 Args:
84 mean: The mean value of the dataset.
85 std: The standard deviation of the dataset.
86 """
87
88 def __init__(self, mean: Tensor, std: Tensor) -> None:
89 super().__init__()
90 self.register_buffer("mean", mean)
91 self.register_buffer("std", std)
92
93 def forward(self, x: Tensor) -> Tensor:
94 """Normalize the given image batch.
95
96 Args:
97 x: The input image batch. Pixel values are expected to be
98 in the range of ``[0, 255]``.
99 Returns:
100 The normalized image batch.
101 """
102 x = x.float() / 255.0
103 # pyrefly: ignore [unsupported-operation]
104 return (x - self.mean) / self.std
105
106
107class Classification(torch.nn.Module):
108 """Classification()"""
109
110 def forward(self, x: Tensor, labels: Tensor) -> tuple[Tensor, Tensor]:
111 """Given a batch of features and labels, compute the top1 and top5 accuracy.
112
113 Args:
114 images: A batch of images. The shape is ``(batch_size, 3, 224, 224)``.
115 labels: A batch of labels. The shape is ``(batch_size,)``.
116
117 Returns:
118 A tuple of top1 and top5 accuracy.
119 """
120
121 probs = torch.nn.functional.softmax(x, dim=-1)
122 top_prob, top_catid = torch.topk(probs, 5)
123 top1 = (top_catid[:, :1] == labels).sum()
124 top5 = (top_catid == labels).sum()
125 return top1, top5
126
127
128class ModelBundle(torch.nn.Module):
129 """ModelBundle()
130
131 Bundle the transform, model backbone, and classification head into a single module
132 for a simple handling."""
133
134 def __init__(
135 self,
136 model: torch.nn.Module,
137 preprocessing: Preprocessing,
138 classification: Classification,
139 use_bf16: bool,
140 ) -> None:
141 super().__init__()
142 self.model = model
143 self.preprocessing = preprocessing
144 self.classification = classification
145 self.use_bf16 = use_bf16
146
147 def forward(self, images: Tensor, labels: Tensor) -> tuple[Tensor, Tensor]:
148 """Given a batch of images and labels, compute the top1, top5 accuracy.
149
150 Args:
151 images: A batch of images. The shape is ``(batch_size, 3, 224, 224)``.
152 labels: A batch of labels. The shape is ``(batch_size,)``.
153
154 Returns:
155 A tuple of top1 and top5 accuracy.
156 """
157
158 x = self.preprocessing(images)
159
160 if self.use_bf16:
161 x = x.to(torch.bfloat16)
162
163 output = self.model(x)
164
165 return self.classification(output, labels)
166
167
168def _expand(vals: list[float], batch_size: int, res: int) -> Tensor:
169 return torch.tensor(vals).view(1, 3, 1, 1).expand(batch_size, 3, res, res).clone()
170
171
172def get_model(
173 batch_size: int,
174 device_index: int,
175 compile: bool,
176 use_bf16: bool,
177 model_type: str = "mobilenetv3_large_100",
178) -> ModelBundle:
179 """Build computation model, including transfor, model, and classification head.
180
181 Args:
182 batch_size: The batch size of the input.
183 device_index: The index of the target GPU device.
184 compile: Whether to compile the model.
185 use_bf16: Whether to use bfloat16 for the model.
186 model_type: The type of the model. Passed to ``timm.create_model()``.
187
188 Returns:
189 The resulting computation model.
190 """
191 import timm
192
193 device = torch.device(f"cuda:{device_index}")
194
195 model = timm.create_model(model_type, pretrained=True)
196 model = model.eval().to(device=device)
197
198 if use_bf16:
199 model = model.to(dtype=torch.bfloat16)
200
201 preprocessing = Preprocessing(
202 mean=_expand([0.4850, 0.4560, 0.4060], batch_size, 224),
203 std=_expand([0.2290, 0.2240, 0.2250], batch_size, 224),
204 ).to(device)
205
206 classification = Classification().to(device)
207
208 if compile:
209 with torch.no_grad():
210 mode = "max-autotune"
211 model = torch.compile(model, mode=mode)
212 preprocessing = torch.compile(preprocessing, mode=mode)
213
214 return ModelBundle(model, preprocessing, classification, use_bf16) # pyre-ignore[6]
215
216
217def get_decode_func(
218 device_index: int,
219 width: int = 224,
220 height: int = 224,
221) -> Callable[[list[tuple[str, int]]], tuple[Tensor, Tensor]]:
222 """Get a function to decode images from a list of paths.
223
224 Args:
225 device_index: The index of the target GPU device.
226 width: The width of the decoded image.
227 height: The height of the decoded image.
228
229 Returns:
230 Async function to decode images in to batch tensor of NCHW format
231 and labels of shape ``(batch_size, 1)``.
232 """
233 device: torch.device = torch.device(f"cuda:{device_index}")
234
235 filter_desc: str | None = spdl.io.get_video_filter_desc(
236 scale_width=256,
237 scale_height=256,
238 crop_width=width,
239 crop_height=height,
240 pix_fmt="rgb24",
241 )
242
243 def decode_images(items: list[tuple[str, int]]) -> tuple[Tensor, Tensor]:
244 paths = [item for item, _ in items]
245 labels = [[item] for _, item in items]
246 labels = torch.tensor(labels, dtype=torch.int64).to(device)
247 buffer = spdl.io.load_image_batch(
248 paths,
249 width=None,
250 height=None,
251 pix_fmt=None,
252 strict=True,
253 filter_desc=filter_desc,
254 device_config=spdl.io.cuda_config(
255 device_index=0,
256 allocator=(
257 torch.cuda.caching_allocator_alloc,
258 torch.cuda.caching_allocator_delete,
259 ),
260 ),
261 )
262 batch = spdl.io.to_torch(buffer)
263 batch = batch.permute((0, 3, 1, 2))
264 return batch, labels
265
266 return decode_images
267
268
269def _get_experimental_nvjpeg_decode_function(
270 device_index: int,
271 width: int = 224,
272 height: int = 224,
273) -> Callable[[list[tuple[str, int]]], tuple[Tensor, Tensor]]:
274 device: torch.device = torch.device(f"cuda:{device_index}")
275 device_config: spdl.io.CUDAConfig = spdl.io.cuda_config(
276 device_index=device_index,
277 allocator=(
278 torch.cuda.caching_allocator_alloc,
279 torch.cuda.caching_allocator_delete,
280 ),
281 )
282
283 def decode_images_nvjpeg(
284 items: list[tuple[str, int]],
285 ) -> tuple[Tensor, Tensor]:
286 paths = [item for item, _ in items]
287 labels = [[item] for _, item in items]
288 labels = torch.tensor(labels, dtype=torch.int64).to(device)
289 buffer = spdl.io.load_image_batch_nvjpeg(
290 paths,
291 device_config=device_config,
292 width=width,
293 height=height,
294 pix_fmt="rgb",
295 # strict=True,
296 )
297 batch = spdl.io.to_torch(buffer)
298 return batch, labels
299
300 return decode_images_nvjpeg
301
302
303def get_dataloader(
304 src: Iterator[tuple[str, int]],
305 batch_size: int,
306 decode_func: Callable[[list[tuple[str, int]]], tuple[Tensor, Tensor]],
307 buffer_size: int,
308 num_threads: int,
309) -> Iterator[tuple[Tensor, Tensor]]:
310 """Build the dataloader for the ImageNet classification task.
311
312 The dataloader uses the ``decode_func`` for decoding images concurrently and
313 send the resulting data to GPU.
314
315 Args:
316 src: The source of the data. See :py:func:`source`.
317 batch_size: The number of images in a batch.
318 decode_func: The function to decode images.
319 buffer_size: The size of the buffer for the dataloader sink
320 num_threads: The number of worker threads.
321
322 """
323 return DataLoader( # pyre-ignore[7]
324 src,
325 batch_size=batch_size,
326 drop_last=True,
327 aggregator=decode_func,
328 buffer_size=buffer_size,
329 num_threads=num_threads,
330 timeout=20,
331 )
332
333
334def benchmark(
335 dataloader: Iterator[tuple[Tensor, Tensor]],
336 model: ModelBundle,
337 max_batches: float = float("nan"),
338) -> None:
339 """The main loop that measures the performance of dataloading and model inference.
340
341 Args:
342 loader: The dataloader to benchmark.
343 model: The model to benchmark.
344 max_batches: The number of batch before stopping.
345 """
346
347 _LG.info("Running inference.")
348 num_frames, num_correct_top1, num_correct_top5 = 0, 0, 0
349 t0 = time.monotonic()
350 try:
351 for i, (batch, labels) in enumerate(dataloader):
352 if i == 20:
353 t0 = time.monotonic()
354 num_frames, num_correct_top1, num_correct_top5 = 0, 0, 0
355
356 with (
357 torch.profiler.record_function(f"iter_{i}"),
358 spdl.io.utils.trace_event(f"iter_{i}"),
359 ):
360 top1, top5 = model(batch, labels)
361
362 num_frames += batch.shape[0]
363 num_correct_top1 += top1
364 num_correct_top5 += top5
365
366 if i + 1 >= max_batches:
367 break
368 finally:
369 elapsed = time.monotonic() - t0
370 if num_frames != 0:
371 num_correct_top1 = num_correct_top1.item() # pyre-ignore[16]
372 # pyrefly: ignore [missing-attribute]
373 num_correct_top5 = num_correct_top5.item()
374 fps = num_frames / elapsed
375 _LG.info(f"FPS={fps:.2f} ({num_frames}/{elapsed:.2f})")
376 acc1 = 0 if num_frames == 0 else num_correct_top1 / num_frames
377 _LG.info(f"Accuracy (top1)={acc1:.2%} ({num_correct_top1}/{num_frames})")
378 acc5 = 0 if num_frames == 0 else num_correct_top5 / num_frames
379 _LG.info(f"Accuracy (top5)={acc5:.2%} ({num_correct_top5}/{num_frames})")
380
381
382def _get_dataloader(
383 args: Namespace, device_index: int
384) -> Iterator[tuple[Tensor, Tensor]]:
385 src = ImageNet(args.root_dir, split=args.split)
386
387 if args.use_nvjpeg:
388 decode_func = _get_experimental_nvjpeg_decode_function(device_index)
389 else:
390 decode_func = get_decode_func(device_index)
391
392 return get_dataloader(
393 src, # pyre-ignore[6]
394 args.batch_size,
395 decode_func,
396 args.buffer_size,
397 args.num_threads,
398 )
399
400
401def entrypoint(args_: list[str] | None = None) -> None:
402 """CLI entrypoint. Run pipeline, transform and model and measure its performance."""
403
404 args = _parse_args(args_)
405 _init_logging(args.debug)
406 _LG.info(args)
407
408 device_index = 0
409 model = get_model(args.batch_size, device_index, args.compile, args.use_bf16)
410 dataloader = _get_dataloader(args, device_index)
411
412 trace_path = f"{args.trace}"
413 if args.use_nvjpeg:
414 trace_path = f"{trace_path}.nvjpeg"
415
416 with (
417 torch.no_grad(),
418 profile() if args.trace else contextlib.nullcontext() as prof,
419 spdl.io.utils.tracing(f"{trace_path}.pftrace", enable=args.trace is not None),
420 ):
421 benchmark(dataloader, model, args.max_batches)
422
423 if args.trace:
424 # pyrefly: ignore [missing-attribute]
425 prof.export_chrome_trace(f"{trace_path}.json")
426
427
428def _init_logging(debug: bool = False) -> None:
429 fmt = "%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s"
430 level = logging.DEBUG if debug else logging.INFO
431 logging.basicConfig(format=fmt, level=level)
432
433
434if __name__ == "__main__":
435 entrypoint()
API Reference¶
Functions
- entrypoint(args_: list[str] | None = None) None[source]¶
CLI entrypoint. Run pipeline, transform and model and measure its performance.
- benchmark(dataloader: Iterator[tuple[Tensor, Tensor]], model: ModelBundle, max_batches: float = nan) None[source]¶
The main loop that measures the performance of dataloading and model inference.
- Parameters:
loader – The dataloader to benchmark.
model – The model to benchmark.
max_batches – The number of batch before stopping.
- get_decode_func(device_index: int, width: int = 224, height: int = 224) Callable[[list[tuple[str, int]]], tuple[Tensor, Tensor]][source]¶
Get a function to decode images from a list of paths.
- Parameters:
device_index – The index of the target GPU device.
width – The width of the decoded image.
height – The height of the decoded image.
- Returns:
Async function to decode images in to batch tensor of NCHW format and labels of shape
(batch_size, 1).
- get_dataloader(src: Iterator[tuple[str, int]], batch_size: int, decode_func: Callable[[list[tuple[str, int]]], tuple[Tensor, Tensor]], buffer_size: int, num_threads: int) Iterator[tuple[Tensor, Tensor]][source]¶
Build the dataloader for the ImageNet classification task.
The dataloader uses the
decode_funcfor decoding images concurrently and send the resulting data to GPU.- Parameters:
src – The source of the data. See
source().batch_size – The number of images in a batch.
decode_func – The function to decode images.
buffer_size – The size of the buffer for the dataloader sink
num_threads – The number of worker threads.
- get_model(batch_size: int, device_index: int, compile: bool, use_bf16: bool, model_type: str = 'mobilenetv3_large_100') ModelBundle[source]¶
Build computation model, including transfor, model, and classification head.
- Parameters:
batch_size – The batch size of the input.
device_index – The index of the target GPU device.
compile – Whether to compile the model.
use_bf16 – Whether to use bfloat16 for the model.
model_type – The type of the model. Passed to
timm.create_model().
- Returns:
The resulting computation model.
Classes
- class ModelBundle[source]¶
Bundle the transform, model backbone, and classification head into a single module for a simple handling.
- forward(images: Tensor, labels: Tensor) tuple[Tensor, Tensor][source]¶
Given a batch of images and labels, compute the top1, top5 accuracy.
- Parameters:
images – A batch of images. The shape is
(batch_size, 3, 224, 224).labels – A batch of labels. The shape is
(batch_size,).
- Returns:
A tuple of top1 and top5 accuracy.
- class Classification[source]¶
- forward(x: Tensor, labels: Tensor) tuple[Tensor, Tensor][source]¶
Given a batch of features and labels, compute the top1 and top5 accuracy.
- Parameters:
images – A batch of images. The shape is
(batch_size, 3, 224, 224).labels – A batch of labels. The shape is
(batch_size,).
- Returns:
A tuple of top1 and top5 accuracy.