concurrent_read reads one shared File from 1-16 threads: every dataset in full (distinct datasets per thread) and random hyperslabs of one dataset, over a deflate and a contiguous file it generates (or reuses while manifest.json matches). It reports decoded MB/s and scaling efficiency, warm or --cold (posix_fadvise) page cache, sizes the decode pool with --decode-threads, and writes JSON. scripts/concurrent_read_h5py.py runs the same workload on the same files with h5py threads or spawned processes (same splitmix64 data and slab stream, checked at spot elements), and compare_concurrent_read.py prints one table and refuses runs with different workloads. A smoke test runs all three end to end on tiny files (h5py half honours CLAWHDF5_PYTHON / CLAWHDF5_REQUIRE_INTEROP). BENCHMARKS.md gets a "Concurrent reads" section with the commands, marked not yet measured. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Tabulate concurrent_read JSON results (clawhdf5, h5py threads/processes).
|
|
|
|
python compare_concurrent_read.py clawhdf5.json h5py-threads.json h5py-procs.json
|
|
|
|
Prints one Markdown table: for each layout, mode and thread count, every
|
|
tool's MB/s and scaling efficiency, and the first file's MB/s relative to each
|
|
of the others. Refuses to compare runs whose workload parameters differ.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
|
|
COMPARED = ("datasets", "rows", "cols", "chunk", "deflate_level", "slab", "slabs", "seed")
|
|
|
|
|
|
def main(paths):
|
|
if len(paths) < 2:
|
|
sys.exit(__doc__)
|
|
docs = []
|
|
for p in paths:
|
|
with open(p) as fh:
|
|
docs.append(json.load(fh))
|
|
ref = docs[0]
|
|
for d, p in zip(docs[1:], paths[1:]):
|
|
diff = [k for k in COMPARED if d["params"].get(k) != ref["params"].get(k)]
|
|
if diff:
|
|
sys.exit(f"{p}: workload differs from {paths[0]} in {', '.join(diff)}")
|
|
if d["cache"] != ref["cache"]:
|
|
print(f"warning: {p} ran {d['cache']!r}, {paths[0]} ran {ref['cache']!r}",
|
|
file=sys.stderr)
|
|
if d.get("host") != ref.get("host"):
|
|
print(f"warning: {p} ran on {d.get('host')}, {paths[0]} on {ref.get('host')}",
|
|
file=sys.stderr)
|
|
|
|
names = [d["tool"] for d in docs]
|
|
for d in docs:
|
|
extra = f", HDF5 {d['hdf5_version']}" if "hdf5_version" in d else ""
|
|
print(f"- {d['tool']} {d['version']}{extra}: host {d.get('host')}, "
|
|
f"{d.get('cpus')} CPUs, cache {d['cache']}, decode threads per read "
|
|
f"{d.get('decode_threads')}")
|
|
p = ref["params"]
|
|
print(f"\n{p['datasets']} datasets of {p['rows']} x {p['cols']} f32, chunks "
|
|
f"{p['chunk'][0]} x {p['chunk'][1]} (deflate {p['deflate_level']}); "
|
|
f"`same`: {p['slabs']} slabs of {p['slab']} x {p['slab']}\n")
|
|
|
|
index = [{(r["layout"], r["mode"], r["threads"]): r for r in d["results"]} for d in docs]
|
|
keys = [(r["layout"], r["mode"], r["threads"]) for r in ref["results"]]
|
|
|
|
head = ["layout", "mode", "threads"]
|
|
head += [f"{n} MB/s (eff)" for n in names]
|
|
head += [f"{names[0]} / {n}" for n in names[1:]]
|
|
print("| " + " | ".join(head) + " |")
|
|
print("|---|---|" + "---:|" * (len(head) - 2))
|
|
for key in keys:
|
|
cells = [key[0], key[1], str(key[2])]
|
|
rs = [ix.get(key) for ix in index]
|
|
for r in rs:
|
|
if r is None:
|
|
cells.append("-")
|
|
else:
|
|
eff = "-" if r["efficiency"] is None else f"{r['efficiency']:.2f}"
|
|
cells.append(f"{r['mb_s']:.0f} ({eff})")
|
|
for r in rs[1:]:
|
|
cells.append("-" if r is None else f"{rs[0]['mb_s'] / r['mb_s']:.2f}x")
|
|
print("| " + " | ".join(cells) + " |")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|