Multi thread preprocessing

This example shows how to run PyTorch tarnsform in SPDL Pipeline, and compares its performance against PyTorch DataLoader.

Each pipeline reads images from the ImageNet dataset, and applies resize, batching, and pixel normalization then the data is transferred to GPU.

In the PyTorch and TorchVision native solution, the images are decoded and resized using Pillow, batched with torch.utils.data.default_collate(), pixel normalization is applied with torchvision.transforms.Normalize, and data are transferred to GPU with torch.Tensor.cuda().

Using torch.utils.data.DataLoader, the batch is created and normalized in subprocess and transferred to the main process before they are sent to GPU.

The following diagram illustrates this.

flowchart TB subgraph SP[Subprocess] subgraph TV[TorchVision/Pillow] Source --Decode--> F0[YUV] --Convert--> F1[RGB] --Resize--> F2[RGB] --To Contiguous--> T1[Tensor] T2[Tensor] end T1 --Copy--> B1[Batch] --Pixel Normalization--> B2[Batch] T2 --Copy--> B1 end Pipe subgraph MP[Main Process] B3[Batch] --GPU Transfer--> B4[Batch] end SP --Deserialize--> Pipe --Serialize--> MP

On the other hand, SPDL Pipeline executes the transforms in the main process. SPDL pipeline uses its own implementation for decode, resize and batching image data.

flowchart LR subgraph MP[Main Process] direction LR subgraph Decode[SPDL I/O] Source --Decode--> F0[YUV] --Resize--> F1[YUV] --Convert--> F2[RGB] --Copy--> B1[Batch] F3[RGB] --Copy--> B1 end B1 --Pixel Normalization--> B2[Batch] --Transfer--> GPU end

This script runs the pipeline with different configurations described bellow while changing the number of workers.

  1. Image decoding and resizing

  2. Image decoding, resizing, and batching

  3. Image decoding, resizing, batching, and normalization

  4. Image decoding, resizing, batching, normalization, and transfer to GPU

The following result was obtained.

The following observations can be made.

  • In both implementations, the throughput peaks around 16 workers, and then decreases as the number of workers.

  • The throughput increases when batching images, then decreases as additional processing is added.

  • The degree of improvement from batching in SPDL is significantly higher than in PyTorch. (more than 2x at 16 workers.)

  • The peak throughput is almost 2.7x in SPDL than in PyTorch.

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
  9"""This example shows how to run PyTorch tarnsform in SPDL Pipeline,
 10and compares its performance against PyTorch DataLoader.
 11
 12Each pipeline reads images from the ImageNet dataset, and applies
 13resize, batching, and pixel normalization then the data is transferred
 14to GPU.
 15
 16In the PyTorch and TorchVision native solution, the images are decoded
 17and resized using Pillow, batched with :py:func:`torch.utils.data.default_collate`,
 18pixel normalization is applied with :py:class:`torchvision.transforms.Normalize`,
 19and data are transferred to GPU with :py:func:`torch.Tensor.cuda`.
 20
 21Using :py:class:`torch.utils.data.DataLoader`, the batch is created and
 22normalized in subprocess and transferred to the main process before they are
 23sent to GPU.
 24
 25The following diagram illustrates this.
 26
 27.. include:: ../plots/multi_thread_preprocessing_chart_torch.txt
 28
 29On the other hand, SPDL Pipeline executes the transforms in the main process.
 30SPDL pipeline uses its own implementation for decode, resize and batching image data.
 31
 32.. include:: ../plots/multi_thread_preprocessing_chart_spdl.txt
 33
 34This script runs the pipeline with different configurations described bellow while
 35changing the number of workers.
 36
 371. Image decoding and resizing
 382. Image decoding, resizing, and batching
 393. Image decoding, resizing, batching, and normalization
 404. Image decoding, resizing, batching, normalization, and transfer to GPU
 41
 42The following result was obtained.
 43
 44.. include:: ../plots/multi_thread_preprocessing_plot.txt
 45
 46The following observations can be made.
 47
 48- In both implementations, the throughput peaks around 16 workers,
 49  and then decreases as the number of workers.
 50- The throughput increases when batching images, then decreases
 51  as additional processing is added.
 52- The degree of improvement from batching in SPDL is significantly
 53  higher than in PyTorch. (more than 2x at 16 workers.)
 54- The peak throughput is almost 2.7x in SPDL than in PyTorch.
 55"""
 56
 57from __future__ import annotations
 58
 59import logging
 60import multiprocessing
 61import time
 62from collections.abc import Iterable
 63from multiprocessing import Process, Queue
 64
 65import spdl.io
 66import torch
 67from spdl.pipeline import PipelineBuilder
 68from torchvision.datasets import ImageNet
 69from torchvision.transforms import Compose, Normalize, PILToTensor, Resize
 70
 71__all__ = [
 72    "entrypoint",
 73    "exp_torch",
 74    "exp_spdl",
 75    "run_dataloader",
 76]
 77
 78
 79logging.getLogger().setLevel(logging.ERROR)
 80
 81
 82def run_dataloader(
 83    dataloader: Iterable,
 84    max_items: int,
 85) -> tuple[int, float]:
 86    """Run the given dataloader and measure its performance.
 87
 88    Args:
 89        dataloader: The dataloader to benchmark.
 90        max_items: The maximum number of items to process.
 91
 92    Returns:
 93        The number of items processed and the elapsed time in seconds.
 94    """
 95    num_items = 0
 96    t0 = time.monotonic()
 97    try:
 98        for i, (data, _) in enumerate(dataloader, start=1):
 99            num_items += 1 if data.ndim == 3 else len(data)
100            if i >= max_items:
101                break
102    finally:
103        elapsed = time.monotonic() - t0
104    return num_items, elapsed
105
106
107def exp_torch(
108    *,
109    root_dir: str,
110    split: str,
111    num_workers: int,
112    max_items: int,
113    batch_size: int | None = None,
114    normalize: bool = False,
115    transfer: bool = False,
116) -> tuple[int, float]:
117    """Load data with PyTorch native operation using PyTorch DataLoader.
118
119    This is the baseline for comparison.
120
121    Args:
122        root_dir: The root directory of the ImageNet dataset.
123        split: The dataset split, such as "train" and "val".
124        num_workers: The number of workers to use.
125        max_items: The maximum number of items to process.
126        batch: Whether to batch the data.
127        normalize: Whether to normalize the data. Only applicable when ``batch`` is True.
128        transfer: Whether to transfer the data to GPU.
129
130    Returns:
131        The number of items processed and the elapsed time in seconds.
132    """
133    dataset = ImageNet(
134        root=root_dir,
135        split=split,
136        transform=Compose([Resize((224, 224)), PILToTensor()]),
137    )
138
139    normalize_transform = Normalize(
140        mean=[0.485, 0.456, 0.406],
141        std=[0.229, 0.224, 0.225],
142    )
143
144    def collate(item):
145        batch, cls = torch.utils.data.default_collate(item)
146        if normalize:
147            batch = batch.float() / 255
148            batch = normalize_transform(batch)
149        return batch, cls
150
151    dataloader = torch.utils.data.DataLoader(
152        dataset,
153        batch_size=batch_size,
154        num_workers=num_workers,
155        collate_fn=None if batch_size is None else collate,
156        prefetch_factor=1,
157        multiprocessing_context="fork",
158    )
159
160    if transfer:
161
162        def with_transfer(dataloader):
163            for tensor, cls in dataloader:
164                tensor = tensor.cuda()
165                yield tensor, cls
166
167        dataloader = with_transfer(dataloader)
168
169    with torch.no_grad():
170        return run_dataloader(dataloader, max_items)
171
172
173def exp_spdl(
174    *,
175    root_dir: str,
176    split: str,
177    num_workers: int,
178    max_items: int,
179    batch_size: int | None = None,
180    normalize: bool = False,
181    transfer: bool = False,
182) -> tuple[int, float]:
183    """Load data with SPDL operation using SPDL Pipeline.
184
185    Args:
186        root_dir: The root directory of the ImageNet dataset.
187        split: The dataset split, such as "train" and "val".
188        num_workers: The number of workers to use.
189        max_items: The maximum number of items to process.
190        batch: Whether to batch the data.
191        normalize: Whether to normalize the data. Only applicable when ``batch`` is True.
192        transfer: Whether to transfer the data to GPU.
193
194    Returns:
195        The number of items processed and the elapsed time in seconds.
196    """
197    filter_desc = spdl.io.get_video_filter_desc(
198        scale_width=224,
199        scale_height=224,
200    )
201
202    def decode_image(path):
203        packets = spdl.io.demux_image(path)
204        return spdl.io.decode_packets(packets, filter_desc=filter_desc)
205
206    dataset = ImageNet(
207        root=root_dir,
208        split=split,
209        loader=decode_image,
210    )
211
212    def convert(items):
213        frames, cls = list(zip(*items, strict=True))
214        buffer = spdl.io.convert_frames(frames)
215        tensor = spdl.io.to_torch(buffer).permute(0, 3, 1, 2)
216        return tensor, cls
217
218    builder = (
219        PipelineBuilder()
220        .add_source(range(len(dataset)))
221        .pipe(dataset.__getitem__, concurrency=num_workers)
222        .aggregate(batch_size or 1)
223        .pipe(convert)
224    )
225
226    if normalize:
227        transform = Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
228
229        def normalize(item):
230            tensor, cls = item
231            tensor = tensor.float() / 255
232            tensor = transform(tensor)
233            return tensor, cls
234
235        builder = builder.pipe(normalize)
236
237    if transfer:
238        builder = builder.pipe(lambda item: (item[0].cuda(), item[1]))
239
240    builder = builder.add_sink(num_workers)
241    pipeline = builder.build(num_threads=num_workers)
242
243    with torch.no_grad(), pipeline.auto_stop():
244        return run_dataloader(pipeline, max_items)
245
246
247##############################################################################
248# Execute the test function in subprocess, so as to isolate them
249##############################################################################
250def exp_torch_(queue, **kwargs):
251    queue.put(exp_torch(**kwargs))
252
253
254def exp_spdl_(queue, **kwargs):
255    queue.put(exp_spdl(**kwargs))
256
257
258def run_in_process(func, **kwargs):
259    queue = Queue()
260    Process(target=func, args=[queue], kwargs=kwargs).run()
261    return queue.get()
262
263
264def run_test(**kwargs):
265    data = {}
266    num_workers_ = [1, 2, 4, 8, 16, 32]
267    for func in [exp_torch_, exp_spdl_]:  # exp_torch_thread, exp_spdl]:
268        print(func.__name__)
269        print("\tnum_workers\tFPS")
270        y = []
271        for num_workers in num_workers_:
272            num_images, elapsed = run_in_process(
273                func, num_workers=num_workers, **kwargs
274            )
275            qps = num_images / elapsed
276            y.append(qps)
277            print(f"\t{num_workers}\t{qps:8.2f} ({num_images} / {elapsed:5.2f})")
278
279        data[func.__name__] = (num_workers_, y)
280
281    return data
282
283
284def _print(data):
285    for i, (x, y) in enumerate(data.values()):
286        if i == 0:
287            print("\t".join(str(v) for v in x))
288        print("\t".join(f"{v:.2f}" for v in y))
289
290
291def entrypoint(
292    root_dir: str,
293    split: str,
294    batch_size: int,
295    max_items: int,
296) -> None:
297    """The main entrypoint for CLI.
298
299    Args:
300        root_dir: The root directory of the ImageNet dataset.
301        split: Dataset split, such as "train" and "val".
302        batch_size: The batch size to use.
303        max_items: The maximum number of items to process.
304    """
305    multiprocessing.set_start_method("spawn")
306
307    argset = (
308        {"batch_size": None},
309        {"batch_size": batch_size},
310        {"batch_size": batch_size, "normalize": True},
311        {"batch_size": batch_size, "normalize": True, "transfer": True},
312    )
313
314    for kwargs in argset:
315        print(kwargs)
316        data = run_test(root_dir=root_dir, split=split, max_items=max_items, **kwargs)
317        _print(data)
318
319
320def _parse_args():
321    import argparse
322
323    parser = argparse.ArgumentParser()
324    parser.add_argument(
325        "--root-dir",
326        help="Directory where the ImageNet dataset is stored.",
327        default="/home/moto/local/imagenet/",
328    )
329    parser.add_argument("--batch-size", default=32, type=int)
330    parser.add_argument(
331        "--max-items",
332        type=int,
333        help="The maximum number of items (images or batches) to process.",
334        default=100,
335    )
336    parser.add_argument(
337        "--split",
338        default="val",
339    )
340    return parser.parse_args()
341
342
343if __name__ == "__main__":
344    _args = _parse_args()
345    entrypoint(
346        _args.root_dir,
347        _args.split,
348        _args.batch_size,
349        _args.max_items,
350    )

API Reference

Functions

entrypoint(root_dir: str, split: str, batch_size: int, max_items: int) None[source]

The main entrypoint for CLI.

Parameters:
  • root_dir – The root directory of the ImageNet dataset.

  • split – Dataset split, such as “train” and “val”.

  • batch_size – The batch size to use.

  • max_items – The maximum number of items to process.

exp_torch(*, root_dir: str, split: str, num_workers: int, max_items: int, batch_size: int | None = None, normalize: bool = False, transfer: bool = False) tuple[int, float][source]

Load data with PyTorch native operation using PyTorch DataLoader.

This is the baseline for comparison.

Parameters:
  • root_dir – The root directory of the ImageNet dataset.

  • split – The dataset split, such as “train” and “val”.

  • num_workers – The number of workers to use.

  • max_items – The maximum number of items to process.

  • batch – Whether to batch the data.

  • normalize – Whether to normalize the data. Only applicable when batch is True.

  • transfer – Whether to transfer the data to GPU.

Returns:

The number of items processed and the elapsed time in seconds.

exp_spdl(*, root_dir: str, split: str, num_workers: int, max_items: int, batch_size: int | None = None, normalize: bool = False, transfer: bool = False) tuple[int, float][source]

Load data with SPDL operation using SPDL Pipeline.

Parameters:
  • root_dir – The root directory of the ImageNet dataset.

  • split – The dataset split, such as “train” and “val”.

  • num_workers – The number of workers to use.

  • max_items – The maximum number of items to process.

  • batch – Whether to batch the data.

  • normalize – Whether to normalize the data. Only applicable when batch is True.

  • transfer – Whether to transfer the data to GPU.

Returns:

The number of items processed and the elapsed time in seconds.

run_dataloader(dataloader: Iterable, max_items: int) tuple[int, float][source]

Run the given dataloader and measure its performance.

Parameters:
  • dataloader – The dataloader to benchmark.

  • max_items – The maximum number of items to process.

Returns:

The number of items processed and the elapsed time in seconds.