Benchmark numpy

This example benchmarks the speed of loading data in different formats.

See Case Studies / Data Format for the detail of how data format and the loading function affects the performance of the training pipeline.

Example

$ numactl --membind 0 --cpubind 0 python benchmark_numpy.py --output results.csv
# Plot results
$ python benchmark_numpy_plot.py --input results.csv --output plot.png

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"""This example benchmarks the speed of loading data in different formats.
  9
 10See `Case Studies / Data Format <../case_studies/data_format.html>`_ for
 11the detail of how data format and the loading function affects
 12the performance of the training pipeline.
 13
 14**Example**
 15
 16.. code-block:: shell
 17
 18   $ numactl --membind 0 --cpubind 0 python benchmark_numpy.py --output results.csv
 19   # Plot results
 20   $ python benchmark_numpy_plot.py --input results.csv --output plot.png
 21
 22"""
 23
 24__all__ = [
 25    "main",
 26    "get_mock_data",
 27    "load_npy",
 28    "load_npy_spdl",
 29    "load_npz",
 30    "load_npz_spdl",
 31    "load_torch",
 32    "BenchmarkConfig",
 33]
 34
 35
 36import argparse
 37import os
 38from collections.abc import Callable
 39from dataclasses import dataclass
 40from functools import partial
 41from io import BytesIO
 42
 43import numpy as np
 44import spdl.io
 45import torch
 46from numpy.typing import NDArray
 47
 48try:
 49    from examples.benchmark_utils import (  # pyre-ignore[21]
 50        BenchmarkResult,
 51        BenchmarkRunner,
 52        ExecutorType,
 53        get_default_result_path,
 54        save_results_to_csv,
 55    )
 56except ImportError:
 57    from spdl.examples.benchmark_utils import (
 58        BenchmarkResult,
 59        BenchmarkRunner,
 60        ExecutorType,
 61        get_default_result_path,
 62        save_results_to_csv,
 63    )
 64
 65
 66DEFAULT_RESULT_PATH: str = get_default_result_path(__file__)
 67
 68
 69def load_npy(items: list[bytes]) -> list[NDArray]:
 70    """Load arrays from serialized NPY binary strings using :py:func:`numpy.load`."""
 71    return [np.load(BytesIO(item), allow_pickle=False) for item in items]
 72
 73
 74def load_npy_spdl(items: list[bytes]) -> list[NDArray]:
 75    """Load arrays from serialized NPY binary strings using :py:func:`spdl.io.load_npy`."""
 76    return [spdl.io.load_npy(item) for item in items]
 77
 78
 79def load_npz(item: bytes) -> list[NDArray]:
 80    """Load arrays from a serialized NPZ binary string using :py:func:`numpy.load`."""
 81    data = np.load(BytesIO(item))
 82    return list(data.values())
 83
 84
 85def load_npz_spdl(item: bytes) -> list[NDArray]:
 86    """Load arrays from serialized NPZ binary strings using :py:func:`spdl.io.load_npz`."""
 87    data = spdl.io.load_npz(item)
 88    return list(data.values())
 89
 90
 91def load_torch(item: bytes) -> list[NDArray]:
 92    """Load arrays from a serialized PyTorch state dict."""
 93    return list(torch.load(BytesIO(item)).values())
 94
 95
 96def _get_load_fn(
 97    data_format: str, impl: str
 98) -> Callable[[list[bytes]], list[NDArray]] | Callable[[bytes], list[NDArray]]:
 99    match data_format:
100        case "torch":
101            return load_torch
102        case "npy":
103            if impl == "spdl":
104                return load_npy_spdl
105            return load_npy
106        case "npz":
107            if impl == "spdl":
108                return load_npz_spdl
109            return load_npz
110        case _:
111            raise ValueError(f"Unexpected data format: {data_format}")
112
113
114def _dump_np(arr: NDArray | dict[str, NDArray], compressed: bool = False) -> bytes:
115    with BytesIO() as buf:
116        if isinstance(arr, dict):
117            if compressed:
118                np.savez_compressed(buf, allow_pickle=False, **arr)
119            else:
120                np.savez(buf, allow_pickle=False, **arr)
121        else:
122            np.save(buf, arr, allow_pickle=False)
123        buf.seek(0)
124        return buf.read()
125
126
127def _dump_torch(arr: dict[str, NDArray]) -> bytes:
128    with BytesIO() as buf:
129        torch.save({k: torch.from_numpy(v) for k, v in arr.items()}, buf)
130        buf.seek(0)
131        return buf.read()
132
133
134def get_mock_data(format: str, compressed: bool = False) -> tuple[bytes, bytes] | bytes:
135    """Generate a single sample in the given format.
136
137    The mock data resemboles an RGB image and its segmentation labels.
138
139    Args:
140        format: One of ``"npz"``, ``"npy"`` or ``"torch"``.
141        compressed: If ``True``, NPZ file is compressed.
142            (i.e. :py:func:`numpy.savez_compressed` is used.)
143
144    Returns:
145        Serialized mock arrays. If ``"npy"`` then arrays are serialized
146        separately. Otherwise arrays are bundled together.
147    """
148    # pyrefly: ignore [no-matching-overload]
149    img = np.random.randint(256, size=(3, 640, 480), dtype=np.uint8)
150    # pyrefly: ignore [no-matching-overload]
151    lbl = np.random.randint(256, size=(640, 480), dtype=np.uint8)
152
153    match format:
154        case "npz":
155            return _dump_np({"img": img, "lbl": lbl}, compressed=compressed)
156        case "npy":
157            return _dump_np(img), _dump_np(lbl)
158        case "torch":
159            return _dump_torch({"img": img, "lbl": lbl})
160        case _:
161            raise ValueError(f"Unexpected `format`: {format}")
162
163
164@dataclass
165class BenchmarkConfig:
166    """BenchmarkConfig()
167
168    Configuration for a single benchmark run."""
169
170    data_format: str
171    """Data format (``"npy"``, ``"npz"``, or ``"torch"``)"""
172
173    compressed: bool
174    """Whether NPZ file is compressed"""
175
176    impl: str
177    """Implementation (``"np"``, ``"spdl"``, or ``"torch"``)"""
178
179    num_workers: int
180    """Number of concurrent workers"""
181
182
183def _parse_args() -> argparse.Namespace:
184    """Parse command line arguments.
185
186    Returns:
187        Parsed arguments.
188    """
189    parser = argparse.ArgumentParser(
190        description="Benchmark data format loading performance"
191    )
192    parser.add_argument(
193        "--output",
194        type=lambda p: os.path.realpath(p),
195        default=DEFAULT_RESULT_PATH,
196        help="Output path for the results",
197    )
198    return parser.parse_args()
199
200
201def main() -> None:
202    """The entrypoint from CLI."""
203    args = _parse_args()
204
205    # Define explicit configuration lists
206    worker_counts = [32, 16, 8, 4, 2, 1]
207    executor_types = [ExecutorType.PROCESS, ExecutorType.THREAD]
208
209    # Define benchmark configurations
210    # (data_format, compressed, impl)
211    data_configs = [
212        ("torch", False, "torch"),
213        ("npy", False, "np"),
214        ("npy", False, "spdl"),
215        ("npz", False, "np"),
216        ("npz", True, "np"),
217        ("npz", False, "spdl"),
218        ("npz", True, "spdl"),
219    ]
220
221    results: list[BenchmarkResult[BenchmarkConfig]] = []
222    iterations = 1000
223    num_runs = 5
224
225    for num_workers in worker_counts:
226        for executor_type in executor_types:
227            with BenchmarkRunner(
228                executor_type=executor_type,
229                num_workers=num_workers,
230                warmup_iterations=30 * num_workers,
231            ) as runner:
232                for data_format, compressed, impl in data_configs:
233                    data = get_mock_data(data_format, compressed)
234
235                    load_fn = _get_load_fn(data_format, impl)
236
237                    result, _ = runner.run(
238                        BenchmarkConfig(
239                            data_format=data_format,
240                            compressed=compressed,
241                            impl=impl,
242                            num_workers=num_workers,
243                        ),
244                        partial(load_fn, data),
245                        iterations,
246                        num_runs=num_runs,
247                    )
248
249                    results.append(result)
250                    print(
251                        f"{data_format},{compressed},{impl},{executor_type.value},{num_workers},{result.qps:.1f}"
252                    )
253
254    save_results_to_csv(results, args.output)
255    plot_output = args.output.replace(".csv", ".png")
256    print(
257        f"\nBenchmark complete. To generate plots, run:\n"
258        f"python benchmark_numpy_plot.py --input {args.output} --output {plot_output}"
259    )
260
261
262if __name__ == "__main__":
263    main()

API Reference

Functions

main() None[source]

The entrypoint from CLI.

get_mock_data(format: str, compressed: bool = False) tuple[bytes, bytes] | bytes[source]

Generate a single sample in the given format.

The mock data resemboles an RGB image and its segmentation labels.

Parameters:
  • format – One of "npz", "npy" or "torch".

  • compressed – If True, NPZ file is compressed. (i.e. numpy.savez_compressed() is used.)

Returns:

Serialized mock arrays. If "npy" then arrays are serialized separately. Otherwise arrays are bundled together.

load_npy(items: list[bytes]) list[NDArray][source]

Load arrays from serialized NPY binary strings using numpy.load().

load_npy_spdl(items: list[bytes]) list[NDArray][source]

Load arrays from serialized NPY binary strings using spdl.io.load_npy().

load_npz(item: bytes) list[NDArray][source]

Load arrays from a serialized NPZ binary string using numpy.load().

load_npz_spdl(item: bytes) list[NDArray][source]

Load arrays from serialized NPZ binary strings using spdl.io.load_npz().

load_torch(item: bytes) list[NDArray][source]

Load arrays from a serialized PyTorch state dict.

Classes

class BenchmarkConfig[source]

Configuration for a single benchmark run.

compressed: bool

Whether NPZ file is compressed

data_format: str

Data format ("npy", "npz", or "torch")

impl: str

Implementation ("np", "spdl", or "torch")

num_workers: int

Number of concurrent workers