Skip to content

Add a benchmark sweep script with an OpenMP baseline and hardware counters - #58

Draft
vchuravy wants to merge 9 commits into
mainfrom
vc/bench-sweep
Draft

vchuravy wants to merge 9 commits into
mainfrom
vc/bench-sweep

Conversation

@vchuravy

Copy link
Copy Markdown
Owner

A script for reproducing the measurements of #46 on any machine and comparing machines:

benchmark/sweep/run.sh        # e.g. JULIA_CHANNELS="1.12 1.13" THREADS="2 4 8" REPEAT=3
julia benchmark/sweep/summarize.jl benchmark/sweep/results/*/results.csv

What a run does

  • Placement: one hardware thread per physical core (from lscpu, within the shell's affinity), taskset, and each Julia thread pinned to its own CPU (pin.jl, loaded with julia -L); OpenMP gets the same CPUs through OMP_PLACES/OMP_PROC_BIND. Julia runs with -t N,0.
  • Benchmarks, each in a process of its own:
    • overhead: benchmark/overhead/overhead.jl, all mechanisms;
    • syncbench: a Julia port of EPCC syncbench's PARALLEL FOR and REDUCTION (team, greedy team, Polyester, @threads :static/:dynamic, OhMyThreads);
    • epcc: the C EPCC syncbench v4.0 at a fixed commit, built with gcc (libgomp) and clang (libomp), each with the default and OMP_WAIT_POLICY=active;
    • wakeup: the loop after a busy gap of 0–10 ms, and for the team with set_spin_time!(0);
    • counters: LinuxPerf.jl counters per loop, skipped if perf_event_paranoid > 2;
    • steal: benchmark/overhead/steal.jl.
  • Shared machines: a per-host lock file, and the load average recorded before every run.
  • Output: results/<host>-<date>/ with machine.txt (CPU, topology, OS, compilers, versioninfo, governor), a log per run and one results.csv. summarize.jl prints tables in the layout of Benchmark report: loop overhead, wake-up and OpenMP comparison on x86, Apple M1 and four ARM boards #46: a row per host, Julia version and thread count, a column per mechanism.
  • Several machines: remote.sh start|status|fetch|stop host… copies the checkout over ssh (including uncommitted and untracked files), runs the sweep in the background and collects the results.

Changes to the existing scripts

  • benchmark/record.jl: appends CSV rows when BENCH_CSV is set, does nothing otherwise, so the scripts run as before on their own.
  • counters.jl: also measures the greedy team and @threads :dynamic, counts task-clock, context switches and CPU migrations, and takes COUNTERS_EVENTS (LinuxPerf syntax) to choose other events. LinuxPerf only knows the generic perf list events; CPU-specific ones such as the RFO HITM counts of Benchmark report: loop overhead, wake-up and OpenMP comparison on x86, Apple M1 and four ARM boards #46 still need the perf CLI.
  • overhead.jl: leaves out KernelAbstractions if it does not load (POCL is not built for every platform); OVERHEAD_SECONDS sets the time per mechanism.

Tested

  • Full and QUICK=1 sweeps locally (Zen, Arch, gcc 16 + clang 23).
  • remote.sh against a fresh depot (fake ssh), and against a real shared Xeon E5-2603 v4 host (2×6 cores, gcc only, juliaup not on the non-interactive PATH, stale registry).

Fixes found that way:

  • Pkg.resolve neither installs nor updates the General registry, so the setup step now does both.
  • libgomp 16 does not export omp_init_lock_with_hint, so EPCC v4.0 is built without the lock hints, with v3.1 as a fallback.

Not tested: macOS (no pinning there) and non-x86 hosts.

🤖 Generated with Claude Code

vchuravy and others added 2 commits October 20, 2026 11:27
…nters

`benchmark/sweep/run.sh` measures, on one machine, the numbers of #46 for a range of
thread counts and Julia versions: the per-loop overhead (`overhead.jl`), a Julia port of
EPCC syncbench's PARALLEL FOR and REDUCTION next to the C syncbench built against libgomp
and libomp, the wake-up latency after a serial gap, hardware counters per loop with
LinuxPerf.jl (`counters.jl`) and the cost of stealing (`steal.jl`). Each run uses one
hardware thread per physical core, with every Julia thread pinned to its own CPU, under a
per-host lock, and records the load before it. The results go to one CSV per machine;
`summarize.jl` turns any number of them into tables, and `remote.sh` runs the sweep on
other machines over ssh and collects the results.

The scripts in `benchmark/overhead` append rows to that CSV when `BENCH_CSV` is set
(`benchmark/record.jl`); `counters.jl` also measures the greedy team and `@threads
:dynamic`, counts context switches and migrations, and takes its events from
`COUNTERS_EVENTS`; `overhead.jl` leaves out KernelAbstractions where POCL does not load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a Broadwell Xeon (E5-2603 v4, NMI watchdog on), with all four groups open at once the
kernel returns 0 for cycles, cache misses and L1d misses while reporting them as counted
the whole time; each group on its own counts correctly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Benchmark Results (Julia v1)

Time benchmarks
main 23cd198... main / 23cd198...
ad/forward tforeach team, 1000 4.63 ± 0.48 μs 7.28 ± 4.3 μs 0.636 ± 0.38
ad/forward tforeach team, 100000 0.148 ± 0.055 ms 0.129 ± 0.039 ms 1.15 ± 0.56
ad/reverse serial loop, 1000 17.1 ± 0.78 μs 17.1 ± 0.69 μs 1 ± 0.061
ad/reverse serial loop, 100000 1.61 ± 0.26 ms 1.62 ± 0.039 ms 0.996 ± 0.16
ad/reverse tforeach team, 1000 0.0327 ± 0.001 ms 0.0319 ± 0.0011 ms 1.02 ± 0.047
ad/reverse tforeach team, 100000 1.46 ± 0.54 ms 1.49 ± 0.37 ms 0.981 ± 0.44
ad/reverse tmapreduce team, 1000 20.2 ± 0.81 μs 22.5 ± 1.3 μs 0.894 ± 0.062
ad/reverse tmapreduce team, 100000 0.483 ± 0.081 ms 0.498 ± 0.045 ms 0.97 ± 0.18
fastbroadcast/9 arguments serial, 100x100x8 0.305 ± 0.02 ms 0.312 ± 0.022 ms 0.978 ± 0.094
fastbroadcast/9 arguments thread, 100x100x8 0.173 ± 0.016 ms 0.179 ± 0.016 ms 0.967 ± 0.13
fastbroadcast/log Base, 1000 7.2 ± 0.11 μs 7.22 ± 0.12 μs 0.998 ± 0.023
fastbroadcast/log Base, 100000 0.722 ± 0.017 ms 0.727 ± 0.017 ms 0.993 ± 0.033
fastbroadcast/log serial, 1000 7.07 ± 0.058 μs 7.07 ± 0.059 μs 1 ± 0.012
fastbroadcast/log serial, 100000 0.711 ± 0.017 ms 0.709 ± 0.016 ms 1 ± 0.033
fastbroadcast/log thread, 1000 4.54 ± 0.62 μs 4.59 ± 0.53 μs 0.989 ± 0.18
fastbroadcast/log thread, 100000 0.381 ± 0.012 ms 0.381 ± 0.0099 ms 0.999 ± 0.041
overhead/@tasks team 0.766 ± 0.065 μs 0.793 ± 0.06 μs 0.966 ± 0.11
overhead/OhMyThreads tforeach 6.23 ± 0.88 μs 6.4 ± 0.92 μs 0.974 ± 0.2
overhead/Threads.@threads :dynamic 4.53 ± 0.53 μs 4.67 ± 0.67 μs 0.97 ± 0.18
overhead/serial loop 26 ± 2 ns 27 ± 3 ns 0.963 ± 0.13
overhead/tforeach greedy team 1.03 ± 0.15 μs 1.03 ± 0.26 μs 1.01 ± 0.29
overhead/tforeach team 0.761 ± 0.066 μs 0.787 ± 0.068 μs 0.967 ± 0.12
overhead/tmapreduce team 0.793 ± 0.067 μs 0.918 ± 0.05 μs 0.864 ± 0.087
trixi/reset du Threads.@threads :static 0.119 ± 0.012 ms 0.122 ± 0.013 ms 0.976 ± 0.14
trixi/reset du serial 1.48 ± 0.11 μs 1.68 ± 0.1 μs 0.885 ± 0.085
trixi/reset du team 2.25 ± 0.13 μs 2.18 ± 0.14 μs 1.03 ± 0.089
trixi/reset du team, 10 in a row 19.5 ± 0.35 μs 19.5 ± 0.37 μs 0.999 ± 0.026
work/serial, 1000 1.31 ± 0.003 μs 1.31 ± 0.003 μs 1 ± 0.0032
work/serial, 100000 0.129 ± 0.0003 ms 0.129 ± 0.00033 ms 1 ± 0.0034
work/tforeach team, 1000 1.48 ± 0.11 μs 1.46 ± 0.088 μs 1.02 ± 0.095
work/tforeach team, 100000 0.067 ± 0.0027 ms 0.0671 ± 0.001 ms 0.999 ± 0.043
time_to_load 0.0902 ± 0.00041 s 0.0928 ± 0.0022 s 0.971 ± 0.023

vchuravy and others added 7 commits October 20, 2026 13:43
The first call of a test compiled it, which already took longer than the 1 ms target, so
`innerreps` stopped at 20 and every measurement covered about 20 loops: waking the
workers dominated, and the team's PARALLEL FOR overhead read 0.50 ± 0.47 µs on a Ryzen 9
5950X at 8 threads. With the warm-up (as the port in #46 does), the inner repetitions
are 5120 there and the confidence interval ±0.03 µs; Polyester and the reductions now
agree with that port. Also specialise `measure` on the mechanism's function, and print
the inner repetitions.

The team's PARALLEL FOR still measures 0.41-0.43 µs against the #46 port's 0.23-0.25 µs
on the same machine and commit; that difference is not explained yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… own

`run.sh` sets `JULIA_LLVM_ARGS=-align-all-nofallthru-blocks=6` (`CODE_ALIGN=0` turns it
off, `CODE_ALIGN_ARGS` changes it), so that where the hot loops land relative to the
64-byte fetch blocks does not vary between builds and processes. The cache key of
package images leaves out LLVM options, so an image compiled without the option would be
loaded as valid: the aligned runs use their own depot, `.cache/<host>/depot-aligned`,
with `JULIAUP_DEPOT_PATH` still pointing juliaup at its installations. `machine.txt`
records the setting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLuPxMba5AAEGKwEUNhQjN
On an NVIDIA GB10, `lscpu` numbers the cores per cluster (CPU 0, a Cortex-A725, and
CPU 5, a Cortex-X925, are both core 0 of socket 0), so one CPU per (socket, core) kept
only the ten small cores. The first CPU of each `thread_siblings_list` is one per core
on every machine tried. Where the kernel reports `cpu_capacity` (big.LITTLE ARM), the
cores of the highest capacity class come first, so that small thread counts run on the
big cores; elsewhere CPU order, as before.

Also expand the list of EPCC binaries safely when it is empty: bash 3.2 (macOS) rejects
an empty array under `set -u`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLuPxMba5AAEGKwEUNhQjN
On cfarm421, GitHub reset the connection while `run.sh` downloaded EPCC, and the broken
tarball stopped the sweep under `set -e`. The tarball is now kept once for all hosts in
`.cache/`, fetched with three attempts and unpacked through a temporary directory; if it
cannot be had, the sweep runs without the `epcc` benchmark. `remote.sh` copies a local
tarball to each host, so that the hosts need not reach GitHub, and extracts the checkout
without the modification times (a host's clock behind ours made tar warn per file).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLuPxMba5AAEGKwEUNhQjN
A run interrupted while fetching EPCC (cfarm421, before 7f92ac1) left an empty source
directory behind, which the next run took for the unpacked sources: both builds failed
for want of the files. Unpack unless `syncbench.c` is there, replacing what is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLuPxMba5AAEGKwEUNhQjN
`Pkg.resolve` keeps the packages of an existing manifest even where `[sources]` in
Project.toml now names another (here OhMyThreads with a fix, JuliaFolds2/OhMyThreads.jl#176):
a sweep on a host that had run one before measured the old package. `run.sh` now
deletes the per-version manifest before resolving.

Without `flock` (macOS) the sweep took no lock at all; it now uses a directory,
`$LOCK.d`, created atomically by `mkdir`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLuPxMba5AAEGKwEUNhQjN
…CK}dir)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLuPxMba5AAEGKwEUNhQjN
@maleadt

maleadt commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

AI-generated review:

I used the sweep's approach for cross-host benchmarks of #62–#69 and needed these changes:

  • a -t N mode next to -t N,0 (the interactive thread is where the Serial loops when the team is busy, a raw chunk loop, and idle workers that sleep #68 issues show);
  • pin.jl pins 1:nthreads(), which under -t N pins the interactive thread and leaves the last default thread unpinned; pin by threadpooltids(:interactive)/(:default);
  • pinning the calling thread before the pool starts makes uv_available_parallelism() return 1, so the pool comes up oversubscribed and workers sched_yield every 5 µs; run a loop first (or merge Detect oversubscription from every thread's affinity mask (thread pinning) #19 first);
  • macOS: thread counts from hw.perflevel0.physicalcpu, not hw.physicalcpu; and remote.sh stop needs setsid to stop the run, not only the shell;
  • a revision column in results.csv, so summarize.jl can compare branches.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants