Decrease run-to-run variance of the bare metal runner#923
Conversation
Add new host tuning measures to reduce benchmark run-to-run variance: - Disable NUMA balancing, timer migration, soft watchdog, and KSM - Hold CPUs out of deep C-states via /dev/cpu_dma_latency (PM QoS) - Steer device IRQs and unbound workqueues to housekeeping cores Fix a gap where one-shot runner run applied tuning but never detected the CPU layout, so cpuset pinning only happened under runner up. Add static preflight warnings at startup for conditions the runner cannot fix at runtime: irqbalance running, no cgroup v2, virtualized host, and missing isolcpus/nohz_full boot args. All new measures are on by default with individual CLI disable flags, skip gracefully when the underlying files are missing, and restore their original values on shutdown.
The local (no sandbox) execution path previously ran the benchmark with no CPU isolation at all: the child shared cores with the runner's own threads. Bring it in line with the Firecracker path: - Create a per-run cgroup with a cpuset restricted to the benchmark cores, reusing CgroupManager - Attach the child race-free via Command::pre_exec: the child writes itself into the pre-opened cgroup.procs fd between fork and exec, so it never runs startup work on the wrong cores - Fall back to sched_setaffinity when cgroup setup fails (non-root, no cgroup v2); isolation is best-effort and never blocks the run - Emit RunMetrics (wall clock plus cgroup CPU and memory usage) from the local path with transport "local"; it previously emitted none
Detach the benchmark cores from the root scheduling domain by turning the parent /sys/fs/cgroup/bencher cgroup into an isolated cpuset partition, the runtime equivalent of the isolcpus= boot arg. The kernel load balancer can no longer pull other tasks onto benchmark cores. Validity is verified by read-back (the kernel reports rejected partitions inline), with a fallback chain isolated -> root -> member. Per-run cgroups inherit the partition automatically. Disable with --no-cpuset-partition. Pin each Firecracker vCPU thread (fc_vcpu N) to its own dedicated benchmark core after InstanceStart, with VMM/API threads pinned to the last benchmark core. This is the required companion to the isolated partition, which does no load balancing between its cores, and also removes vCPU migration noise (cold caches, TLB refills) without it. Also disable swap for the VM cgroup in the live Firecracker path and fill RunMetrics.cgroup from the existing cgroup metrics reader, which previously always emitted None.
Extend the default guest kernel cmdline with determinism params, all stock options on the bundled Firecracker CI 6.1.174 kernel: - norandmaps: guest ASLR off (overridable via kernel_cmdline, giving opt-out parity with the host --aslr flag) - nokaslr: every run boots a fresh VM, so guest KASLR is a per-run variance source unique to the sandbox path - transparent_hugepage=never: deterministic guest memory backing Make the default cmdline arch-conditional: reboot=t is an x86 triple fault; aarch64 now uses reboot=k plus keep_bootcon instead of the previous x86-flavored cmdline. Pin the host transparent hugepage mode to 'never' by default (enabled and defrag), with a --thp <never|madvise|always|leave> override; 'leave' preserves the previous behavior of not touching THP. Note for release: memory-heavy benchmarks may see a one-time baseline step from the THP change.
🤖 Claude Code ReviewPR: #923 I have a thorough understanding of the changes. Here is my review. PR Review: Host tuning, CPU isolation, and cpuset partitioning for the runnerOverall this is a high-quality, well-structured change. The code is idiomatic, extensively unit-tested (with fake Strengths
Observations (non-blocking)
Minor nits
Testing noteI could not run Net: solid, careful work. I'd approve after confirming the CI Linux checks pass, and I'd suggest tracking the concurrent-runner mutual-clobbering case as a follow-up hardening item rather than a docs-only note. Model: claude-opus-4-8 |
Save the cpuset partition restore entry before attempting any mode write. A write can succeed at the syscall level while the kernel rejects the partition in the read-back; previously the fallback path left the partition file dirty on the persistent bencher cgroup, and a later apply could even save the dirty state as its restore value. The saved value is now normalized to the mode token, since a rejected partition reads back as '<mode> invalid (<reason>)', which is not a valid value to write back. Make enable_controllers verification-gated: write failures are now tolerated when the required controllers are already enabled, e.g. pre-configured by an admin for an unprivileged runner. This was a behavior regression from making the call unconditional. Also fix the controller check to match whole tokens; 'cpuset' alone previously satisfied the 'cpu' controller via substring matching.
|
| Branch | claude/runner-variance-optimization-293nsj |
| Testbed | intel-v1 |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result microseconds (µs) (Result Δ%) | Upper Boundary microseconds (µs) (Limit %) |
|---|---|---|---|
| Adapter::Json | 📈 view plot 🚷 view threshold | 4.60 µs(-0.54%)Baseline: 4.62 µs | 4.90 µs (93.78%) |
| Adapter::Magic (JSON) | 📈 view plot 🚷 view threshold | 4.47 µs(-0.70%)Baseline: 4.50 µs | 4.72 µs (94.59%) |
| Adapter::Magic (Rust) | 📈 view plot 🚷 view threshold | 25.87 µs(+1.31%)Baseline: 25.54 µs | 26.64 µs (97.13%) |
| Adapter::Rust | 📈 view plot 🚷 view threshold | 3.51 µs(+0.69%)Baseline: 3.49 µs | 3.60 µs (97.48%) |
| Adapter::RustBench | 📈 view plot 🚷 view threshold | 3.51 µs(+0.66%)Baseline: 3.48 µs | 3.59 µs (97.58%) |
Review follow-ups from PR #923, documentation only: - tuning module docs now state that restoration happens on clean shutdown only, and that a crash makes the pre-tuning values unrecoverable for the next run (the C-state fd hold and per-run cgroups self-heal by construction) - note that /dev/cpu_dma_latency only supports the literal zero value here; a non-zero latency would need i32::to_ne_bytes - note that vCPU threads run briefly before pinning and the cgroup cpuset is the hard confinement, with pinning as a refinement
|
Addressed the review asks in be106d0 and 3d12eea:
Confirming the compliance asks: cargo fmt, cargo clippy -Dwarnings (native and cross-compiled via ./scripts/clippy.sh), cargo nextest run (193 passing), cargo test --doc, cargo check --no-default-features, and ./scripts/test.sh --linux-only are all green locally on every commit. Not changed: guest ASLR stays cmdline-only (norandmaps via the user-overridable kernel_cmdline was a deliberate design decision), and --thp keeps FromStr parsing to match the existing SandboxLogLevel convention. |
Comment-only follow-ups from the PR #923 review: - document the CliTuning flag polarity rationale (keep-enabled flags for hardware features, --no- flags for runner-side actions) - note that at full vCPU width the highest-index vCPU shares its core with the VMM threads in assign_core - note that CPU core pinning in runner run is deliberately independent of --no-tuning, matching runner up - note why the local spawn-failure path emits no run metrics
|
Round 3 review nits addressed in 3f4020e (comment-only):
On the workqueue scope of --no-irq-steering: the flag's own help text already reads "Do not steer device IRQs and kernel workqueues to housekeeping cores", so the scope is documented at the point of use; keeping the shorter flag name. Linux cross-checks (./scripts/clippy.sh, ./scripts/test.sh --linux-only) plus nextest, doc tests, and no-default-features check remain green on this commit; both the unqualified size_of in local_isolation.rs and the unconditional CpuLayout import compile on both targets. |
Extend cargo test-runner with a non-sandboxed scenario asserting the local path emits the BENCHER_METRICS stderr marker with transport 'local' (it previously emitted no metrics at all). Note in the tuning module docs that tuning assumes a single runner process per host: the sysctls, IRQ affinities, THP mode, and cpuset partition are host-global, so a second concurrent runner's shutdown restores them out from under the first.
|
Round 4 review points, addressed in e07f278:
Cross-compile checks, nextest, doc tests, and no-default-features remain green on this commit. |
The local isolation module documents that it mirrors the Firecracker path, but only applied the cpuset. Also disable swap on the per-run cgroup so benchmark memory stays resident on the no-sandbox path; swap thrashing adds run-to-run variance.
|
Round 5 observations, addressed in 8c233dd:
Cross-compile checks (./scripts/clippy.sh, ./scripts/test.sh --linux-only), nextest, doc tests, and no-default-features are green on this commit. |
CpuLayout::detect() previously counted online CPUs and assumed the IDs were contiguous 0..N. Disabling SMT (on by default) on topologies with interleaved sibling numbering, common on AMD, leaves a non-contiguous online set like 0,2,4,6; the count-based layout would then pin cpusets and affinities to offline cores. Parse the actual ID list from /sys/devices/system/cpu/online instead, keeping the count-based path as a fallback. with_core_count() now delegates to with_cpu_ids(). Also normalize the remaining emdashes in the tuning output strings and the relocated host-tuning comments to hyphens, per the no-emdash policy in CLAUDE.md; new code already used hyphens, so output was mixed.
|
Round 6 findings, addressed in 98696e5:
|
|
Round 7 review: all observations are known, documented constraints or no-action items. Two follow-ups:
CI is green on this branch apart from the two pre-existing online lint failures (Lint / Zizmor Online, Lint / Cargo Deny Advisories), which also fail on devel. |
What
Reduces run-to-run variance of the bare metal
runneron both the Firecracker sandbox path and the no-sandbox path. All new measures are runtime sysfs/procfs/cgroup configuration shipped inside the single staticrunnerbinary (no host boot args required), on by default with individual disable flags, best-effort with graceful skips on ARM and locked-down hosts, and restored on shutdown.One commit per phase:
1. Host tuning extensions,
runner runfix, preflight warnings/dev/cpu_dma_latencyPM QoS fd held open for the runner lifetime (crash-safe, works on x86 and ARM)--no-irq-steeringto disable), applied via a new layout-scoped tuning phase afterCpuLayout::detect()runner runapplied tuning but never detected the CPU layout, so cpuset pinning only happened underrunner up; it now mirrors theuppathisolcpus=/nohz_full=boot args2. No-sandbox path isolation and metrics
Command::pre_exec(the child moves itself into the pre-openedcgroup.procsfd between fork and exec) and asched_setaffinityfallback for unprivileged hostsRunMetrics(wall clock plus cgroup CPU/memory,transport: "local"); it previously emitted none3. Scheduler-domain isolation and vCPU determinism (Firecracker)
/sys/fs/cgroup/bencherbecomes anisolatedcpuset partition, the runtime equivalent ofisolcpus=: benchmark cores leave the root scheduling domain so nothing else can be balanced onto them. Kernel acceptance is verified by read-back, with a fallback chainisolated->root->member(--no-cpuset-partitionto disable)fc_vcpu Nthread is pinned to its own dedicated benchmark core afterInstanceStart(required companion to the isolated partition, which does no load balancing); VMM/API threads pin to the last benchmark coreRunMetrics.cgroupis now filled from the existing cgroup metrics reader (previously alwaysNone)4. Guest determinism and host THP
norandmaps nokaslr transparent_hugepage=never(guest ASLR/KASLR off, deterministic guest memory backing), and is now arch-conditional: aarch64 useskeep_bootconandreboot=kinstead of the x86-onlyreboot=tnever(enabledanddefrag), with--thp <never|madvise|always|leave>;leavepreserves the old do-not-touch behaviorNote
Release notes for operators:
runner updeployments will start steering IRQs, disabling NUMA balancing/watchdogs/KSM, holding C-states off, creating an isolated cpuset partition, and pinning host THP toneveron upgrade. Each has an individual disable flag, and--no-tuningdisables everything.--thp leaveopts out.Testing
bencher_runner+bencher_runner_clicargo clippy -Dwarningsnatively and cross-compiled tox86_64-unknown-linux-gnu(./scripts/clippy.sh),cargo check --no-default-features,cargo test --doc,./scripts/test.sh --linux-onlycargo test-runner, plus before/afterbencher noiseand repeatedrunner run --iter Non x86_64 and aarch64, with and without--sandbox firecracker