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.

flowchart LR subgraph MP [Main Process] subgraph BG [Background Thread] A[Source] subgraph TP1[Thread Pool] direction LR T1[Thread] T2[Thread] T3[Thread] end end subgraph FG [Main Thread] ML[Main loop] end end subgraph G[GPU] direction TB GM[Memory] T[Transform] M[Model] end A --> T1 -- Batch --> GM A --> T2 -- Batch --> GM A --> T3 -- Batch --> GM ML -.-> GM GM -.-> T -.-> M

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_func for 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.

class Preprocessing(mean: Tensor, std: Tensor)[source]

Perform pixel normalization and data type conversion.

Parameters:
  • mean – The mean value of the dataset.

  • std – The standard deviation of the dataset.

forward(x: Tensor) Tensor[source]

Normalize the given image batch.

Parameters:

x – The input image batch. Pixel values are expected to be in the range of [0, 255].

Returns:

The normalized image batch.