Benchmark pinned arena¶
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
8r"""Benchmarking shared-arena memory pinning
9========================================
10
11Compare a reusable pool and FIFO ring with ``pin_memory=False`` and ``True``.
12Pinned receiving memory can avoid an extra CPU staging copy before H2D.
13See :doc:`../optimization_guide/shared_memory_arenas` for arena usage.
14
15Reference results
16-----------------
17
18Historical 8-MiB FP32 Torch batches, with three fresh-process repetitions per
19arm in one resource-matched GPU allocation and 256 fully checked batches per
20trial, gave the following results.
21
22.. image:: /_static/data/pinned_arena_8mib.png
23 :alt: Checked 8 MiB throughput, H2D helper time and CPU cost
24
25.. list-table:: Historical pageable versus pinned throughput
26 :header-rows: 1
27
28 * - Backend
29 - Pageable items/s
30 - Pinned items/s
31 - Pinned / pageable
32 * - Pool
33 - 137.09 ± 0.39
34 - 184.14 ± 5.73
35 - 1.343
36 * - Ring
37 - 104.06 ± 5.96
38 - 149.42 ± 5.83
39 - 1.436
40
41Mean ± sample SD, not confidence intervals. The checked end-to-end workflow
42includes generation, transport, synchronized H2D and full GPU verification;
43H2D helper wall time is not pure DMA.
44
45These numbers and the figure describe an earlier workflow, not a fresh run of
46the current public four-arm benchmark. The figure includes additional IPC and
47copy-out controls. Pinned ring output uses a different owning copy-out allocation,
48so its gain cannot be attributed to page locking alone.
49
50Pinning helped this workload but consumes page-locked host memory. Compare the
51end-to-end trade-off in your application; this is not a training-speed promise.
52"""
53
54from __future__ import annotations
55
56import argparse
57import gc
58import hashlib
59import inspect
60import json
61import math
62import multiprocessing as mp
63import os
64import platform
65import statistics
66import sys
67import time
68from collections.abc import Callable, Iterable, Iterator, Mapping
69from functools import partial
70from itertools import product
71from pathlib import Path
72from tempfile import TemporaryDirectory
73from typing import Any
74
75import numpy as np
76import torch
77from spdl.io import transfer_tensor_h2d
78from spdl.pipeline import (
79 iterate_in_subprocess,
80 SharedMemoryRingBuffer,
81 SharedMemorySegmentPool,
82)
83
84__all__ = [
85 "configure_process",
86 "make_payload",
87 "produce_payloads",
88 "verify_payload",
89 "restore_payload",
90 "transfer_payload",
91 "process_item",
92 "run_pass",
93 "run_benchmark",
94 "run_isolated",
95 "main",
96]
97
98_ARMS = (
99 "pool-pageable",
100 "pool-pinned",
101 "ring-pageable",
102 "ring-pinned",
103)
104_CPU_ARMS = ("pool-pageable", "ring-pageable")
105_DTYPES = {"fp32": ("float32", 4), "fp16": ("float16", 2), "uint8": ("uint8", 1)}
106_SCHEMA = "public-iterate-v1"
107_RETIRED_FLAGS = (
108 "--runtime-checks",
109 "--trace-dir",
110 "--threshold",
111 "--seam",
112 "--retain-items",
113 "--consumer-delay",
114 "--sample-interval",
115)
116
117
118def configure_process() -> None:
119 """Select one Torch CPU thread in each process before benchmark setup/timing.
120
121 Use this as the public subprocess initializer, not at module import time.
122 It does not initialize CUDA or change another process's thread count.
123
124 Example:
125 >>> configure_process()
126 >>> torch.get_num_threads()
127 1
128 """
129 torch.set_num_threads(1)
130
131
132def _pattern(count: int, sequence: int, leaf: int) -> bytes:
133 shift = (sequence * 17 + leaf * 29) % 251
134 cycle = bytes((i + shift) % 251 for i in range(251))
135 return (cycle * ((count + 250) // 251))[:count]
136
137
138def _leaf_sizes(size: int, kind: str, dtype: str) -> list[int]:
139 elements = max(1, size // _DTYPES[dtype][1])
140 if kind != "nested":
141 return [elements]
142 first = max(1, elements // 2)
143 return [first, max(1, elements - first), 3]
144
145
146def make_payload(size: int, kind: str, dtype: str, sequence: int) -> Any:
147 """Create fresh deterministic CPU data, including for independent verification.
148
149 Args:
150 size: Positive requested byte budget. Leaf lengths are dtype-rounded;
151 a nested payload also includes a three-element leaf.
152 kind: ``torch``, ``numpy`` or a ``nested`` dictionary/list/tuple tensor tree.
153 dtype: ``fp32``, ``fp16`` or ``uint8``.
154 sequence: Nonnegative item identity incorporated into every leaf.
155
156 Returns:
157 A CPU tensor, NumPy array or nested CPU tensor tree; no CUDA storage.
158
159 Example:
160 >>> make_payload(8192, 'torch', 'fp32', sequence=1).numel()
161 2048
162 """
163 if size <= 0 or sequence < 0 or kind not in ("torch", "numpy", "nested"):
164 raise ValueError("invalid payload size, sequence or kind")
165 if dtype not in _DTYPES:
166 raise ValueError("unsupported dtype")
167 arrays = [
168 np.frombuffer(_pattern(n, sequence, i), dtype=np.uint8)
169 .astype(_DTYPES[dtype][0])
170 .reshape(-1)
171 for i, n in enumerate(_leaf_sizes(size, kind, dtype))
172 ]
173 leaves = arrays if kind == "numpy" else [torch.from_numpy(a) for a in arrays]
174 if kind == "nested":
175 return {"main": leaves[0], "nested": [leaves[1], (leaves[2],)]}
176 return leaves[0]
177
178
179def _map_leaves(batch: Any, fn: Callable[[Any], Any]) -> Any:
180 if isinstance(batch, dict):
181 return {key: _map_leaves(value, fn) for key, value in batch.items()}
182 if isinstance(batch, list):
183 return [_map_leaves(value, fn) for value in batch]
184 if isinstance(batch, tuple):
185 return tuple(_map_leaves(value, fn) for value in batch)
186 return fn(batch)
187
188
189def _leaves(batch: Any) -> Iterator[Any]:
190 if isinstance(batch, Mapping):
191 for value in batch.values():
192 yield from _leaves(value)
193 elif isinstance(batch, (list, tuple)):
194 for value in batch:
195 yield from _leaves(value)
196 else:
197 yield batch
198
199
200class _Payloads:
201 def __init__(self, config: dict[str, Any], worker: int) -> None:
202 self.config = config
203 self.worker = worker
204 self.epoch = 0
205
206 def __iter__(self) -> Iterator[dict[str, Any]]:
207 config, epoch = self.config, self.epoch
208 self.epoch += 1
209 for index in range(config["items"]):
210 if torch.cuda.is_initialized():
211 raise AssertionError("producer initialized CUDA")
212 sequence = (epoch * config["workers"] + self.worker) * config[
213 "items"
214 ] + index
215 start, cpu_start = time.perf_counter(), time.process_time()
216 payload = make_payload(
217 config["size"], config["kind"], config["dtype"], sequence
218 )
219 yield {
220 "sequence": sequence,
221 "worker": self.worker,
222 "epoch": epoch,
223 "payload": payload,
224 "producer": {
225 "pid": os.getpid(),
226 "generation_s": time.perf_counter() - start,
227 "generation_cpu_s": time.process_time() - cpu_start,
228 "cuda_initialized": torch.cuda.is_initialized(),
229 "cpu_threads": torch.get_num_threads(),
230 },
231 }
232 del payload
233 if torch.cuda.is_initialized():
234 raise AssertionError("producer initialized CUDA during production")
235
236
237def produce_payloads(
238 config: dict[str, Any], worker: int = 0
239) -> Iterable[dict[str, Any]]:
240 """Return a pickle-safe, reiterable CPU source for a public subprocess boundary.
241
242 Args:
243 config: ``size``, ``kind``, ``dtype``, ``items`` per epoch and ``workers``.
244 worker: Producer index in ``[0, workers)``.
245
246 Returns:
247 A reiterable source, not a single-use generator. Each epoch produces fresh
248 values and globally distinct sequence identifiers. Rows include generation
249 wall/CPU time and the producer's CUDA-uninitialized/thread-count receipt.
250 Pass ``configure_process`` as the boundary's initializer.
251
252 Example:
253 >>> cfg = dict(size=8192, kind='torch', dtype='fp32', items=2, workers=1)
254 >>> source = produce_payloads(cfg)
255 >>> [row['sequence'] for row in source]
256 [0, 1]
257 >>> [row['sequence'] for row in source]
258 [2, 3]
259 """
260 if config["items"] <= 0 or not 0 <= worker < config["workers"]:
261 raise ValueError("invalid producer item count or index")
262 return _Payloads(config, worker)
263
264
265def restore_payload(batch: Any) -> tuple[Any, dict[str, float]]:
266 """Prepare already-restored public boundary outputs as CPU tensor leaves.
267
268 No serialized blobs, registry or reader endpoints are accepted here. Arena
269 restoration belongs entirely to ``iterate_in_subprocess``.
270
271 Args:
272 batch: Tensor/array/tree already yielded by the public iterable.
273
274 Returns:
275 ``(tensor_batch, phases)``; phases measure only NumPy-to-Torch/tree
276 preparation, not the preceding transport or arena restoration.
277
278 Example:
279 >>> batch, phases = restore_payload(make_payload(16, 'numpy', 'fp32', 0))
280 >>> batch.numel()
281 4
282 """
283 start, cpu_start = time.perf_counter(), time.process_time()
284 result = _map_leaves(
285 batch,
286 lambda leaf: torch.from_numpy(leaf) if isinstance(leaf, np.ndarray) else leaf,
287 )
288 return result, {
289 "preparation_s": time.perf_counter() - start,
290 "preparation_cpu_s": time.process_time() - cpu_start,
291 }
292
293
294def _verify_tree(actual: Any, expected: Any, device: str, sequence: int) -> None:
295 if isinstance(expected, dict):
296 if not isinstance(actual, dict) or actual.keys() != expected.keys():
297 raise AssertionError("payload structure mismatch")
298 for key in expected:
299 _verify_tree(actual[key], expected[key], device, sequence)
300 elif isinstance(expected, (list, tuple)):
301 if type(actual) is not type(expected) or len(actual) != len(expected):
302 raise AssertionError("payload structure mismatch")
303 for value, target in zip(actual, expected, strict=True):
304 _verify_tree(value, target, device, sequence)
305 else:
306 if not isinstance(actual, torch.Tensor) or actual.device.type != device:
307 raise AssertionError("payload device/type mismatch")
308 if actual.dtype != expected.dtype or actual.shape != expected.shape:
309 raise AssertionError("payload shape/dtype mismatch")
310 if not torch.equal(actual.cpu(), expected):
311 raise AssertionError(
312 f"entire payload value mismatch for sequence {sequence}"
313 )
314
315
316def verify_payload(
317 batch: Any, size: int, kind: str, dtype: str, sequence: int, *, device: str = "cpu"
318) -> None:
319 """Check structure, dtype, shape, device and every value against fresh CPU data.
320
321 Args:
322 batch: Prepared tensor/tree to check, on CPU or CUDA.
323 size: Requested payload byte budget.
324 kind: Original ``torch``, ``numpy`` or ``nested`` payload kind.
325 dtype: Original ``fp32``, ``fp16`` or ``uint8`` dtype.
326 sequence: Expected producer item identity.
327 device: Required leaf device type, ``cpu`` or ``cuda``.
328
329 Raises:
330 AssertionError: Structure, layout, device or any value differs.
331
332 Example:
333 >>> batch = make_payload(8192, 'torch', 'fp32', 1)
334 >>> verify_payload(batch, 8192, 'torch', 'fp32', 1)
335 """
336 expected, _ = restore_payload(make_payload(size, kind, dtype, sequence))
337 _verify_tree(batch, expected, device, sequence)
338
339
340def transfer_payload(batch: Any) -> tuple[Any, dict[str, Any]]:
341 """Use only the public synchronized H2D helper, with its cache disabled.
342
343 Args:
344 batch: CPU tensor or supported tensor tree.
345
346 Returns:
347 ``(gpu_batch, phases)`` with measured helper completion wall/process-CPU
348 time only. Unmeasured internal pin/copy/stream phases are omitted.
349
350 Example:
351 On a CUDA consumer, call ``gpu, phases = transfer_payload(cpu_batch)``.
352 """
353 start, cpu_start = time.perf_counter(), time.process_time()
354 result = transfer_tensor_h2d(batch, num_caches=0)
355 return result, {
356 "host_h2d_completion_s": time.perf_counter() - start,
357 "helper_cpu_s": time.process_time() - cpu_start,
358 }
359
360
361def process_item(item: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
362 """Prepare, optionally transfer, and fully verify one public output.
363
364 Args:
365 item: Producer row already restored by the public subprocess iterable.
366 config: Payload settings and ``device`` (``cpu`` or ``cuda``).
367
368 Returns:
369 Scalar/list receipts only, with no retained tensors. Every arm uses this
370 identical consumer workflow. Source pinning is reported per leaf,
371 including small inline/pageable leaves.
372
373 Raises:
374 AssertionError: Producer CUDA/thread receipt or full value check fails.
375
376 Example:
377 Use this with rows from ``iterate_in_subprocess(produce_factory)``.
378 """
379 producer = item["producer"]
380 if producer["cuda_initialized"] or producer["cpu_threads"] != 1:
381 raise AssertionError("producer must use one CPU thread without CUDA")
382 batch, phases = restore_payload(item["payload"])
383 nbytes = sum(leaf.numel() * leaf.element_size() for leaf in _leaves(batch))
384 pinned = [leaf.is_pinned() for leaf in _leaves(batch)]
385 h2d: dict[str, Any] = {
386 "host_h2d_completion_s": None,
387 "helper_cpu_s": None,
388 }
389 checked = batch
390 if config["device"] == "cuda":
391 checked, h2d = transfer_payload(batch)
392 start = time.perf_counter()
393 verify_payload(
394 checked,
395 config["size"],
396 config["kind"],
397 config["dtype"],
398 item["sequence"],
399 device=config["device"],
400 )
401 return {
402 "sequence": item["sequence"],
403 "worker": item["worker"],
404 "epoch": item["epoch"],
405 "producer": producer,
406 "payload_bytes": nbytes,
407 **phases,
408 **h2d,
409 "verify_s": time.perf_counter() - start,
410 "source_is_pinned": pinned,
411 }
412
413
414def run_pass(
415 sources: Iterable[Iterable[dict[str, Any]]], config: dict[str, Any]
416) -> dict[str, Any]:
417 """Run one whole checked epoch, consuming producers in round-robin order.
418
419 Args:
420 sources: One public subprocess iterable per producer, already initialized.
421 config: Payload, per-producer ``items``, worker count and device settings.
422
423 Returns:
424 Epoch wall/consumer CPU time and per-item receipts. Wall includes epoch
425 command dispatch, generation, public transport/restore, preparation,
426 synchronized H2D if selected, full verification and final epoch completion.
427 ``next_output_s`` combines waiting, transport and restoration; it is not
428 an internal read/copy/registration timer. No payloads are retained.
429
430 Example:
431 Call once for warmup and again on the same sources for measured work.
432 """
433 iterators = [iter(source) for source in sources]
434 if len(iterators) != config["workers"]:
435 raise ValueError("one public boundary is required per producer")
436 rows = []
437 start, cpu_start = time.perf_counter(), time.process_time()
438 for _ in range(config["items"]):
439 for worker, iterator in enumerate(iterators):
440 fetch_start = time.perf_counter()
441 item = next(iterator)
442 fetch_s = time.perf_counter() - fetch_start
443 if item["worker"] != worker:
444 raise AssertionError("producer identity mismatch")
445 row = process_item(item, config)
446 row["next_output_s"] = fetch_s
447 rows.append(row)
448 del item
449 sentinel = object()
450 for iterator in iterators:
451 if next(iterator, sentinel) is not sentinel:
452 raise AssertionError("producer yielded too many items")
453 return {
454 "wall_s": time.perf_counter() - start,
455 "consumer_cpu_s": time.process_time() - cpu_start,
456 "items": rows,
457 }
458
459
460def _memory_sample() -> dict[str, Any]:
461 result: dict[str, Any] = {"consumer_pss_bytes": None, "tmpfs_available_bytes": None}
462 try:
463 for line in Path("/proc/self/smaps_rollup").read_text().splitlines():
464 if line.startswith("Pss:"):
465 result["consumer_pss_bytes"] = int(line.split()[1]) * 1024
466 except (FileNotFoundError, PermissionError, ProcessLookupError):
467 pass
468 try:
469 fs = os.statvfs("/dev/shm")
470 result["tmpfs_available_bytes"] = fs.f_bavail * fs.f_frsize
471 except (FileNotFoundError, PermissionError):
472 pass
473 return result
474
475
476def _source_receipt() -> dict[str, Any]:
477 module = sys.modules[__name__]
478 try:
479 source = inspect.getsource(module).encode("utf-8")
480 except (OSError, TypeError):
481 source = None
482 return {
483 "benchmark_sha256": hashlib.sha256(source).hexdigest()
484 if source is not None
485 else None,
486 "hash_scope": "benchmark_source_utf8",
487 "cuda_initialized": torch.cuda.is_initialized(),
488 "python": platform.python_version(),
489 "torch": str(torch.__version__),
490 "numpy": np.__version__,
491 "schema": _SCHEMA,
492 }
493
494
495def _make_arena(
496 arm: str, config: dict[str, Any]
497) -> SharedMemorySegmentPool | SharedMemoryRingBuffer:
498 unit = (
499 sum(_leaf_sizes(config["size"], config["kind"], config["dtype"]))
500 * _DTYPES[config["dtype"]][1]
501 + 512
502 )
503 pinned = arm.endswith("-pinned")
504 if arm.startswith("pool-"):
505 return SharedMemorySegmentPool(
506 segment_size=unit,
507 count=config["slots"],
508 acquire_timeout=config["timeout"],
509 pin_memory=pinned,
510 )
511 return SharedMemoryRingBuffer(
512 capacity=unit * config["slots"],
513 acquire_timeout=config["timeout"],
514 pin_memory=pinned,
515 )
516
517
518def _validate_config(arm: str, config: dict[str, Any]) -> None:
519 if arm not in _ARMS:
520 raise ValueError(f"unsupported or retired runtime arm: {arm}")
521 if config["device"] not in ("cpu", "cuda"):
522 raise ValueError("device must be cpu or cuda")
523 if config["device"] == "cpu" and arm not in _CPU_ARMS:
524 raise ValueError("pinned runtime arms require a CUDA consumer")
525 if min(config[key] for key in ("size", "items", "slots", "workers")) <= 0:
526 raise ValueError("size, items, slots and workers must be positive")
527 if config["warmup_epochs"] < 1 or config["local_rank"] < 0:
528 raise ValueError("warmup_epochs must be positive and local_rank nonnegative")
529 if not math.isfinite(config["timeout"]) or config["timeout"] <= 0:
530 raise ValueError("timeout must be finite and positive")
531 if (
532 config["kind"] not in ("torch", "numpy", "nested")
533 or config["dtype"] not in _DTYPES
534 ):
535 raise ValueError("unsupported payload kind/dtype")
536 retired = set(config).intersection(
537 ("threshold", "seam", "trace_dir", "retain_items", "consumer_delay")
538 )
539 if retired:
540 raise ValueError(f"retired internal configuration fields: {sorted(retired)}")
541
542
543def run_benchmark(arm: str, config: dict[str, Any]) -> dict[str, Any]:
544 """Run an ordinary public CPU/GPU comparison in the current consumer process.
545
546 Args:
547 arm: ``pool-pageable``, ``pool-pinned``, ``ring-pageable`` or ``ring-pinned``.
548 config: ``size``, ``kind``, ``dtype``, ``items``, ``workers``, ``slots``,
549 ``warmup_epochs``, ``timeout``, ``local_rank`` and ``device``.
550
551 Returns:
552 ``public-iterate-v1`` receipts: setup, excluded checked warmup epochs,
553 one measured epoch and throughput using only measured public metrics.
554 The consumer creates one arena per producer and transfers ownership to
555 ``iterate_in_subprocess``. That public iterable handles transport and
556 teardown. Thread/CUDA setup, warmup and finalizer cleanup are untimed.
557
558 Example:
559 >>> cfg = dict(size=8192, kind='torch', dtype='fp32', items=2, workers=1,
560 ... slots=2, warmup_epochs=1, timeout=60., local_rank=0, device='cpu')
561 >>> result = run_benchmark('pool-pageable', cfg)
562 >>> len(result['measured']['items'])
563 2
564 """
565 _validate_config(arm, config)
566 configure_process()
567 source = _source_receipt()
568 if config["device"] == "cuda":
569 os.environ["LOCAL_RANK"] = str(config["local_rank"])
570 torch.cuda.set_device(config["local_rank"])
571 sources: list[Any] = []
572 setup_start = time.perf_counter()
573 try:
574 for worker in range(config["workers"]):
575 sources.append(
576 iterate_in_subprocess(
577 partial(produce_payloads, config, worker),
578 initializer=configure_process,
579 mp_context="spawn",
580 buffer_size=config["slots"],
581 timeout=config["timeout"],
582 arena=_make_arena(arm, config),
583 )
584 )
585 setup_s = time.perf_counter() - setup_start
586 warmup = [run_pass(sources, config) for _ in range(config["warmup_epochs"])]
587 memory_before = _memory_sample()
588 measured = run_pass(sources, config)
589 return {
590 "schema": _SCHEMA,
591 "arm": arm,
592 "config": dict(config),
593 "source": source,
594 "setup_s": setup_s,
595 "warmup": warmup,
596 "measured": measured,
597 "items_per_s": len(measured["items"]) / measured["wall_s"],
598 "payload_gib_per_s": sum(row["payload_bytes"] for row in measured["items"])
599 / (1 << 30)
600 / measured["wall_s"],
601 "memory_before": memory_before,
602 "memory_after": _memory_sample(),
603 }
604 finally:
605 sources.clear()
606 gc.collect()
607
608
609def _benchmark_to_file(arm: str, config: dict[str, Any], path: str) -> None:
610 Path(path).write_text(json.dumps(run_benchmark(arm, config)), encoding="utf-8")
611
612
613def run_isolated(arm: str, config: dict[str, Any]) -> dict[str, Any]:
614 """Run one arm in a fresh spawned consumer, with fresh public producer boundaries.
615
616 Args:
617 arm: Supported runtime arm.
618 config: Settings accepted by ``run_benchmark``; ``timeout`` bounds the
619 complete isolated invocation, including setup/warmup/teardown.
620
621 Returns:
622 The consumer's JSON-compatible benchmark receipt. A temporary result file
623 conveys only the final report, never payloads or arena endpoint messages.
624
625 Raises:
626 RuntimeError: The consumer failed or did not write its final report.
627 TimeoutError: The invocation exceeded its configured total timeout.
628
629 Example:
630 Call ``run_isolated('pool-pageable', cfg)`` for fresh-process repetitions.
631 """
632 _validate_config(arm, config)
633 with TemporaryDirectory(prefix="spdl-public-pin-") as directory:
634 path = str(Path(directory) / "result.json")
635 process = mp.get_context("spawn").Process(
636 target=_benchmark_to_file, args=(arm, config, path)
637 )
638 process.start()
639 try:
640 process.join(config["timeout"])
641 if process.is_alive():
642 raise TimeoutError("isolated benchmark consumer timed out")
643 if process.exitcode != 0 or not Path(path).is_file():
644 raise RuntimeError(
645 f"isolated benchmark consumer failed: exit {process.exitcode}"
646 )
647 return json.loads(Path(path).read_text(encoding="utf-8"))
648 finally:
649 if process.is_alive():
650 process.terminate()
651 process.join(10)
652 if process.is_alive():
653 process.kill()
654 process.join(10)
655 process.close()
656
657
658def _legend() -> dict[str, str]:
659 return {
660 "protocol": (
661 "public-iterate-v1: public iterate_in_subprocess owns transport, "
662 "restoration and teardown; no private endpoint or transfer hooks."
663 ),
664 "historical": (
665 "Seven-arm fixed-8-MiB table/figure retain original-source measurements. "
666 "ring-original is retired; the changed protocol is not remeasured."
667 ),
668 "timing": (
669 "Setup and checked warmup epochs excluded. Measured epoch includes "
670 "generation, epoch dispatch/end, public transport/restore, preparation, "
671 "synchronized public H2D if selected and entire-value verification. "
672 "next_output_s combines waiting/transport/restore. Do not sum phases."
673 ),
674 "producer_cpu": (
675 "generation_cpu_s measures generation only, not total producer CPU. "
676 "consumer_cpu_s is measured-epoch process CPU, including verification."
677 ),
678 "pinning": (
679 "Per-leaf public is_pinned status is not proof of no staging. Small "
680 "inline leaves may stay pageable. Pinning is selected only through "
681 "the public arena constructor's pin_memory option."
682 ),
683 "memory": (
684 "Before/after consumer /proc PSS and host-wide /dev/shm free space; "
685 "not peaks, producer memory or registration/reservation totals."
686 ),
687 "repeats": (
688 "Fresh consumer/producers per arm/repetition, one arena per producer "
689 "and one CPU thread per process. Arms rotate per repeat. Sample SD "
690 "is null for one repeat; these are not independent GPU allocations."
691 ),
692 }
693
694
695def _parse_args(argv: list[str] | None) -> argparse.Namespace:
696 argv = list(sys.argv[1:] if argv is None else argv)
697 parser = argparse.ArgumentParser(
698 description="Public subprocess pinned-arena comparison"
699 )
700 retired = [
701 flag
702 for flag in _RETIRED_FLAGS
703 if any(value.split("=", 1)[0] == flag for value in argv)
704 ]
705 if retired:
706 parser.error(
707 f"retired internal benchmark flags: {', '.join(retired)}; "
708 "lifecycle tests remain in the library test suite"
709 )
710 parser.add_argument(
711 "--self-test",
712 action="store_true",
713 help="bounded checked public CPU workflows, not a performance sweep",
714 )
715 parser.add_argument(
716 "--source-receipt",
717 action="store_true",
718 help="benchmark source identity only; no private library/package probes",
719 )
720 parser.add_argument("--device", choices=("cpu", "cuda"), default="cuda")
721 parser.add_argument("--arms", nargs="+", choices=_ARMS)
722 parser.add_argument(
723 "--sizes",
724 nargs="+",
725 type=int,
726 default=[8],
727 help="requested MiB; overridden by --sizes-bytes",
728 )
729 parser.add_argument("--sizes-bytes", nargs="+", type=int)
730 parser.add_argument(
731 "--kinds", nargs="+", choices=("torch", "numpy", "nested"), default=["torch"]
732 )
733 parser.add_argument("--dtypes", nargs="+", choices=tuple(_DTYPES), default=["fp32"])
734 parser.add_argument("--workers", type=int, default=1)
735 parser.add_argument("--slots", type=int, default=4)
736 parser.add_argument("--items", type=int, default=8, help="per producer per epoch")
737 parser.add_argument(
738 "--warmup-epochs",
739 type=int,
740 default=1,
741 help="complete checked epochs excluded from measurement",
742 )
743 parser.add_argument("--repeats", type=int, default=3)
744 parser.add_argument("--timeout", type=float, default=600.0)
745 parser.add_argument(
746 "--local-rank", type=int, default=int(os.environ.get("LOCAL_RANK", "0"))
747 )
748 parser.add_argument("--output", help="JSON report file, otherwise stdout")
749 args = parser.parse_args(argv)
750 if args.self_test:
751 args.device, args.arms = "cpu", list(_CPU_ARMS)
752 args.sizes_bytes, args.kinds, args.dtypes = (
753 [8192],
754 ["torch", "numpy", "nested"],
755 ["fp32"],
756 )
757 args.workers, args.slots, args.items, args.repeats, args.warmup_epochs = (
758 1,
759 2,
760 2,
761 1,
762 1,
763 )
764 if args.arms is None:
765 args.arms = list(_CPU_ARMS if args.device == "cpu" else _ARMS)
766 if min(args.workers, args.slots, args.items, args.warmup_epochs, args.repeats) <= 0:
767 parser.error(
768 "workers, slots, items, warmup-epochs and repeats must be positive"
769 )
770 if not math.isfinite(args.timeout) or args.timeout <= 0 or args.local_rank < 0:
771 parser.error("timeout must be finite/positive; local-rank must be nonnegative")
772 if any(size <= 0 for size in (args.sizes_bytes or args.sizes)):
773 parser.error("sizes must be positive")
774 if args.device == "cpu" and any(arm not in _CPU_ARMS for arm in args.arms):
775 parser.error("pinned runtime arms require --device cuda")
776 return args
777
778
779def _summaries(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
780 groups: dict[tuple[Any, ...], list[float]] = {}
781 for row in rows:
782 config = row["config"]
783 key = (
784 row["arm"],
785 config["size"],
786 config["kind"],
787 config["dtype"],
788 config["workers"],
789 config["device"],
790 )
791 groups.setdefault(key, []).append(row["items_per_s"])
792 return [
793 {
794 "arm": key[0],
795 "size": key[1],
796 "kind": key[2],
797 "dtype": key[3],
798 "workers": key[4],
799 "device": key[5],
800 "items_per_s_mean": statistics.mean(values),
801 "items_per_s_stdev": statistics.stdev(values) if len(values) > 1 else None,
802 "fresh_process_repeats": len(values),
803 }
804 for key, values in groups.items()
805 ]
806
807
808def main(argv: list[str] | None = None) -> None:
809 """Run the four-arm public comparison CLI or bounded CPU/source checks.
810
811 Args:
812 argv: CLI arguments, or ``None`` to read ``sys.argv``.
813
814 Returns:
815 None; writes structured JSON to ``--output`` or stdout. Reports include
816 raw per-item receipts, grouped throughput means/sample SD and scope legend.
817 ``--device cpu`` selects only unpinned public workflows. ``--self-test``
818 checks bounded CPU epochs without CUDA; ``--source-receipt`` hashes only
819 this benchmark, not private library modules or installed native packages.
820
821 Raises:
822 SystemExit: Unsupported/retired CLI flags or invalid configuration.
823 RuntimeError: A public consumer/producer workflow failed.
824
825 Example:
826 >>> main(['--device', 'cpu', '--arms', 'pool-pageable', '--sizes-bytes', '8192',
827 ... '--items', '2', '--repeats', '1', '--output', 'cpu-results.json'])
828 """
829 args = _parse_args(argv)
830 if args.source_receipt:
831 report = {"schema": _SCHEMA, "source_receipt": _source_receipt()}
832 else:
833 rows = []
834 sizes = args.sizes_bytes or [size << 20 for size in args.sizes]
835 for size, kind, dtype, repeat in product(
836 sizes, args.kinds, args.dtypes, range(args.repeats)
837 ):
838 config = {
839 "size": size,
840 "kind": kind,
841 "dtype": dtype,
842 "repeat": repeat,
843 "workers": args.workers,
844 "slots": args.slots,
845 "items": args.items,
846 "warmup_epochs": args.warmup_epochs,
847 "timeout": args.timeout,
848 "local_rank": args.local_rank,
849 "device": args.device,
850 }
851 order = (
852 args.arms[repeat % len(args.arms) :]
853 + args.arms[: repeat % len(args.arms)]
854 )
855 rows.extend(run_isolated(arm, config) for arm in order)
856 report = {
857 "schema": _SCHEMA,
858 "legend": _legend(),
859 "results": rows,
860 "summary": _summaries(rows),
861 }
862 text = json.dumps(report, indent=2, allow_nan=False)
863 if args.output:
864 Path(args.output).write_text(text + "\n", encoding="utf-8")
865 else:
866 print(text)
867
868
869if __name__ == "__main__":
870 main()
API Reference¶
Functions
- configure_process() None[source]¶
Select one Torch CPU thread in each process before benchmark setup/timing.
Use this as the public subprocess initializer, not at module import time. It does not initialize CUDA or change another process’s thread count.
Example
>>> configure_process() >>> torch.get_num_threads() 1
- make_payload(size: int, kind: str, dtype: str, sequence: int) Any[source]¶
Create fresh deterministic CPU data, including for independent verification.
- Parameters:
size – Positive requested byte budget. Leaf lengths are dtype-rounded; a nested payload also includes a three-element leaf.
kind –
torch,numpyor anesteddictionary/list/tuple tensor tree.dtype –
fp32,fp16oruint8.sequence – Nonnegative item identity incorporated into every leaf.
- Returns:
A CPU tensor, NumPy array or nested CPU tensor tree; no CUDA storage.
Example
>>> make_payload(8192, 'torch', 'fp32', sequence=1).numel() 2048
- produce_payloads(config: dict[str, Any], worker: int = 0) Iterable[dict[str, Any]][source]¶
Return a pickle-safe, reiterable CPU source for a public subprocess boundary.
- Parameters:
config –
size,kind,dtype,itemsper epoch andworkers.worker – Producer index in
[0, workers).
- Returns:
A reiterable source, not a single-use generator. Each epoch produces fresh values and globally distinct sequence identifiers. Rows include generation wall/CPU time and the producer’s CUDA-uninitialized/thread-count receipt. Pass
configure_processas the boundary’s initializer.
Example
>>> cfg = dict(size=8192, kind='torch', dtype='fp32', items=2, workers=1) >>> source = produce_payloads(cfg) >>> [row['sequence'] for row in source] [0, 1] >>> [row['sequence'] for row in source] [2, 3]
- verify_payload(batch: Any, size: int, kind: str, dtype: str, sequence: int, *, device: str = 'cpu') None[source]¶
Check structure, dtype, shape, device and every value against fresh CPU data.
- Parameters:
batch – Prepared tensor/tree to check, on CPU or CUDA.
size – Requested payload byte budget.
kind – Original
torch,numpyornestedpayload kind.dtype – Original
fp32,fp16oruint8dtype.sequence – Expected producer item identity.
device – Required leaf device type,
cpuorcuda.
- Raises:
AssertionError – Structure, layout, device or any value differs.
Example
>>> batch = make_payload(8192, 'torch', 'fp32', 1) >>> verify_payload(batch, 8192, 'torch', 'fp32', 1)
- restore_payload(batch: Any) tuple[Any, dict[str, float]][source]¶
Prepare already-restored public boundary outputs as CPU tensor leaves.
No serialized blobs, registry or reader endpoints are accepted here. Arena restoration belongs entirely to
iterate_in_subprocess.- Parameters:
batch – Tensor/array/tree already yielded by the public iterable.
- Returns:
(tensor_batch, phases); phases measure only NumPy-to-Torch/tree preparation, not the preceding transport or arena restoration.
Example
>>> batch, phases = restore_payload(make_payload(16, 'numpy', 'fp32', 0)) >>> batch.numel() 4
- transfer_payload(batch: Any) tuple[Any, dict[str, Any]][source]¶
Use only the public synchronized H2D helper, with its cache disabled.
- Parameters:
batch – CPU tensor or supported tensor tree.
- Returns:
(gpu_batch, phases)with measured helper completion wall/process-CPU time only. Unmeasured internal pin/copy/stream phases are omitted.
Example
On a CUDA consumer, call
gpu, phases = transfer_payload(cpu_batch).
- process_item(item: dict[str, Any], config: dict[str, Any]) dict[str, Any][source]¶
Prepare, optionally transfer, and fully verify one public output.
- Parameters:
item – Producer row already restored by the public subprocess iterable.
config – Payload settings and
device(cpuorcuda).
- Returns:
Scalar/list receipts only, with no retained tensors. Every arm uses this identical consumer workflow. Source pinning is reported per leaf, including small inline/pageable leaves.
- Raises:
AssertionError – Producer CUDA/thread receipt or full value check fails.
Example
Use this with rows from
iterate_in_subprocess(produce_factory).
- run_pass(sources: Iterable[Iterable[dict[str, Any]]], config: dict[str, Any]) dict[str, Any][source]¶
Run one whole checked epoch, consuming producers in round-robin order.
- Parameters:
sources – One public subprocess iterable per producer, already initialized.
config – Payload, per-producer
items, worker count and device settings.
- Returns:
Epoch wall/consumer CPU time and per-item receipts. Wall includes epoch command dispatch, generation, public transport/restore, preparation, synchronized H2D if selected, full verification and final epoch completion.
next_output_scombines waiting, transport and restoration; it is not an internal read/copy/registration timer. No payloads are retained.
Example
Call once for warmup and again on the same sources for measured work.
- run_benchmark(arm: str, config: dict[str, Any]) dict[str, Any][source]¶
Run an ordinary public CPU/GPU comparison in the current consumer process.
- Parameters:
arm –
pool-pageable,pool-pinned,ring-pageableorring-pinned.config –
size,kind,dtype,items,workers,slots,warmup_epochs,timeout,local_rankanddevice.
- Returns:
setup, excluded checked warmup epochs, one measured epoch and throughput using only measured public metrics. The consumer creates one arena per producer and transfers ownership to
iterate_in_subprocess. That public iterable handles transport and teardown. Thread/CUDA setup, warmup and finalizer cleanup are untimed.- Return type:
public-iterate-v1receipts
Example
>>> cfg = dict(size=8192, kind='torch', dtype='fp32', items=2, workers=1, ... slots=2, warmup_epochs=1, timeout=60., local_rank=0, device='cpu') >>> result = run_benchmark('pool-pageable', cfg) >>> len(result['measured']['items']) 2
- run_isolated(arm: str, config: dict[str, Any]) dict[str, Any][source]¶
Run one arm in a fresh spawned consumer, with fresh public producer boundaries.
- Parameters:
arm – Supported runtime arm.
config – Settings accepted by
run_benchmark;timeoutbounds the complete isolated invocation, including setup/warmup/teardown.
- Returns:
The consumer’s JSON-compatible benchmark receipt. A temporary result file conveys only the final report, never payloads or arena endpoint messages.
- Raises:
RuntimeError – The consumer failed or did not write its final report.
TimeoutError – The invocation exceeded its configured total timeout.
Example
Call
run_isolated('pool-pageable', cfg)for fresh-process repetitions.
- main(argv: list[str] | None = None) None[source]¶
Run the four-arm public comparison CLI or bounded CPU/source checks.
- Parameters:
argv – CLI arguments, or
Noneto readsys.argv.- Returns:
None; writes structured JSON to
--outputor stdout. Reports include raw per-item receipts, grouped throughput means/sample SD and scope legend.--device cpuselects only unpinned public workflows.--self-testchecks bounded CPU epochs without CUDA;--source-receipthashes only this benchmark, not private library modules or installed native packages.- Raises:
SystemExit – Unsupported/retired CLI flags or invalid configuration.
RuntimeError – A public consumer/producer workflow failed.
Example
>>> main(['--device', 'cpu', '--arms', 'pool-pageable', '--sizes-bytes', '8192', ... '--items', '2', '--repeats', '1', '--output', 'cpu-results.json'])