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.
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.
This script runs the pipeline with different configurations described bellow while changing the number of workers.
Image decoding and resizing
Image decoding, resizing, and batching
Image decoding, resizing, batching, and normalization
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
batchis 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
batchis 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.