bench: concurrent-read harness against h5py threads and processes
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]>
This commit is contained in:
@@ -482,6 +482,85 @@ The rows and columns of the uncompressed layouts are within 20% (chunked
|
||||
column 0.45 -> 0.49 ms, contiguous column 2.55 -> 2.61 ms). This run does not
|
||||
explain the slower windows.
|
||||
|
||||
## Concurrent reads
|
||||
|
||||
**Not yet measured.** The harness exists; no numbers are published until it
|
||||
has been run on an idle machine. The smoke runs used while building it (tiny
|
||||
files, other jobs compiling on the box) are not results.
|
||||
|
||||
The question: libhdf5's threadsafe build serialises every API call under one
|
||||
global mutex, and h5py holds a global lock around every call too, so threads
|
||||
reading through h5py cannot decode in parallel; h5py users scale with
|
||||
processes. A clawhdf5 `File` is `Send + Sync`, and nothing on the read paths
|
||||
this harness uses (`read_f32`, `read_f32_selection`) takes a library-wide
|
||||
lock: the one mutex is the `File`'s chunk cache (keyed per dataset), taken by
|
||||
full reads of chunked datasets for each chunk's O(1) lookup and insert, never
|
||||
across a decode; hyperslab reads do not use the cache. How does
|
||||
decoded throughput scale with threads on one open file, against h5py threads
|
||||
and h5py processes on the same files?
|
||||
|
||||
Workload (`crates/clawhdf5-bench/src/bin/concurrent_read.rs`; the h5py script
|
||||
mirrors it): `<dir>/deflate.h5` and `<dir>/contiguous.h5`, each with 64 `f32`
|
||||
datasets of 64 MiB decoded (`[16384, 1024]`; the deflate file chunked
|
||||
`256 x 256`, level 4), written by clawhdf5 on first use and reused while
|
||||
`manifest.json` matches. The data is a slowly varying ramp plus 8 bits of
|
||||
noise per element, every value exact in `f32`, so both harnesses check what
|
||||
they read; it deflates about 3.1x (128 MiB -> 40.7 MiB for two 64 MiB
|
||||
datasets). For each layout and thread count
|
||||
(1, 2, 4, 8, 16; fixed total work per repetition, split among the threads):
|
||||
|
||||
- `distinct`: every dataset read in full once, thread `t` taking datasets
|
||||
`t, t + T, ...`;
|
||||
- `same`: 1024 random `256 x 256` hyperslabs of `d00` in total, from a seeded
|
||||
splitmix64 stream that both harnesses generate identically.
|
||||
|
||||
Reported per row: MB/s of decoded (selected) data from the median of the
|
||||
repetitions, and scaling efficiency `MB/s(T) / (T x MB/s(1))`. Each worker
|
||||
times itself from a start barrier; a repetition spans the earliest start to
|
||||
the latest finish. Page cache: warm by default (each file is read once before
|
||||
timing); `--cold` evicts the files with `posix_fadvise(POSIX_FADV_DONTNEED)`
|
||||
before every repetition (no root needed; best effort). clawhdf5 opens one
|
||||
`File` per repetition, shared by all threads; h5py threads share one
|
||||
`h5py.File`; h5py processes (spawned before timing) each open the file inside
|
||||
the timed region.
|
||||
|
||||
Decode inside a single clawhdf5 read is itself parallel in this binary
|
||||
(clawhdf5-format's `parallel` feature, enabled here through clawhdf5-agent;
|
||||
it is off in the facade's default features), so a 1-thread clawhdf5 full read
|
||||
of the deflate file already uses the whole rayon pool. Run both
|
||||
`--decode-threads 1` (each read decodes on its calling thread, like h5py —
|
||||
this isolates the API's own scaling) and the default pool.
|
||||
|
||||
> **Run** (from the repository root). The default files take about 5.4 GiB
|
||||
> of disk (4 GiB contiguous + about 1.3 GiB deflate). Generating them is
|
||||
> memory-hungry because `FileBuilder` holds a whole file in memory: peak RSS
|
||||
> was 676 MB for `--datasets 2 --mib 64` (2026-09-25, tank,
|
||||
> `/usr/bin/time -f %M`), about 5x one file's decoded size, so expect about
|
||||
> 21 GB at the defaults (once; later runs reuse the files). Put `--dir` on a
|
||||
> real disk, not tmpfs, if `--cold` is to mean anything.
|
||||
>
|
||||
> ```bash
|
||||
> DIR=/path/on/disk/concurrent-read
|
||||
> BENCH=crates/clawhdf5-bench/scripts
|
||||
> PY=.venv/bin/python # h5py 3.16 / HDF5 2.0 in this repo
|
||||
> cargo build --release -p clawhdf5-bench --bin concurrent_read
|
||||
> B=target/release/concurrent_read
|
||||
> $B --dir $DIR --json claw-pool.json # generates on first run
|
||||
> $B --dir $DIR --decode-threads 1 --json claw-1.json
|
||||
> $PY $BENCH/concurrent_read_h5py.py --dir $DIR --executor threads --json h5py-threads.json
|
||||
> $PY $BENCH/concurrent_read_h5py.py --dir $DIR --executor processes --json h5py-procs.json
|
||||
> $PY $BENCH/compare_concurrent_read.py claw-1.json h5py-threads.json h5py-procs.json
|
||||
> $PY $BENCH/compare_concurrent_read.py claw-pool.json h5py-threads.json h5py-procs.json
|
||||
> ```
|
||||
>
|
||||
> Cold page cache: add `--cold` to every harness command. Smoke test (seconds):
|
||||
> `$B --dir /tmp/cr --datasets 4 --mib 1 --threads 1,2,4 --slabs 16 --reps 1`
|
||||
> and the same `--threads/--slabs/--reps` to the h5py script.
|
||||
|
||||
Other flags (both harnesses): `--threads`, `--reps`, `--slab`, `--slabs`,
|
||||
`--seed`, `--modes distinct,same`, `--layouts deflate,contiguous`; sizes
|
||||
(`--datasets`, `--mib`) only on the Rust harness, which writes the files.
|
||||
|
||||
## Search harness baseline (v2.3.0)
|
||||
|
||||
Produced by `cargo run --release -p clawhdf5-bench --bin search_harness -- --full`
|
||||
|
||||
Reference in New Issue
Block a user