diff --git a/rules/benchmark/measurement.md b/rules/benchmark/measurement.md index e4fb1f7..ab3215b 100644 --- a/rules/benchmark/measurement.md +++ b/rules/benchmark/measurement.md @@ -31,12 +31,33 @@ taskset -c it landed on `4-11`, the guard saw the cores busy after the run (`pinned.sh: cpus 4-11 busy after run`), and an hour of measurement was discarded. The work was not wrong — it was unverifiable. +**The agent is a process too.** Pinning the recorder does nothing for the agent that launched it: +affinity is per process and is not inherited backwards, and the agent keeps working while the cells +are timed. Before the first measurement, pin the agent's own process and any other agent on the host +(`taskset -apc ` for each, in a command of its own — a shell that is already +running keeps its old mask), then start the recorder from a fresh command so it inherits the pin. The +toolkit's observer audit (`python3 -m campaign_toolkit.observer_audit --observer --confine`) +does this for the user's processes and refuses a recording while a busy one still overlaps the measured +domain. + ## One measurement at a time A campaign holds one lock across build, verify and timing. Do not start a second campaign, a build or a commit while it is held: the recorder checks that the measured sources did not move and stops the run rather than publishing something it cannot vouch for. +**No commit or edit in any measured repository while a run is in flight** — not the library under +measurement, not the harness, not the toolkit. The recorder compares each one's `HEAD` and clean state +with what it recorded at the start, between process sets and at the end; a documentation commit in the +library checkout moves `HEAD` and ends the run. Write notes outside the repositories (a scratch +directory) and commit them after the run. + +**Build the harness only through its own build script.** A campaign harness links the measured +library by a path patch; a plain `cargo test`, `cargo check` or `cargo build` in the harness +re-resolves `Cargo.lock` to the registry crate, and the next recording stops at the patch guard with a +dirty lock. After any manifest change, read the whole `git diff Cargo.lock` (a filtered grep hides +`+version = ...` lines) and commit only the dependency line you added. + ## A discarded run stays discarded The guard discards a run whose cores were busy before or after it. Do not re-run until it passes: diff --git a/rules/common/performance.md b/rules/common/performance.md index b59f219..0496815 100644 --- a/rules/common/performance.md +++ b/rules/common/performance.md @@ -94,6 +94,30 @@ about memory traffic. Judge by mechanism: fix a split whose cost grows with - When leaving a known tradeoff in place, document why the input size is bounded or the cost acceptable. +## Optimising Against Proprietary Software + +A vendor library (MKL, cuBLAS, cuTENSOR, a closed BLAS) is a yardstick, not a +source. When a change is measured against one: + +- **Allowed:** timing the vendor through its public API under the same + conditions as our code (the campaign's vendor arms); reading its public + documentation for what an API does and what its parameters mean; stating a + hypothesis about how it reaches a number ("it probably pads the leading + dimension") and testing that hypothesis on *our* code; reading published + papers and reference implementations whose licence is compatible (reference + LAPACK is BSD-3) and citing them in the module header. +- **Not allowed:** disassembling, decompiling or tracing the vendor binary; + sampling a profiler into its internals to learn its structure; reading its + source where one exists (leaked, under NDA, or as a licensed component); + carrying over constants, thresholds or orderings that were learned from it + rather than measured on our code; translating a reference implementation's + code without its licence notice, even when the licence would permit it. +- **Record the provenance** in the change's worklog and ledger row: which + papers the algorithm comes from, that the vendor was timed through its public + API only, and that no vendor or (if so) no LAPACK code was consulted. A design + choice should trace to a measured breakdown of *our* code, not to what the + vendor is believed to do. + ## Performance-Gated Experiment Protocol Human/process protocol; intentionally not routed to the diff-scoped review bot. diff --git a/rules/rust/performance.md b/rules/rust/performance.md index 6168221..b78ddc9 100644 --- a/rules/rust/performance.md +++ b/rules/rust/performance.md @@ -64,6 +64,16 @@ and solves. Do not reimplement kernels in downstream layers. - When integrating faer, BLAS, LAPACK, or CUDA libraries, keep packing and ABI copies at explicit boundaries and reuse scratch where possible. +- A work copy's leading dimension must not be a multiple of 1 KiB. A column + stride of 4 KiB maps every entry of a row onto one L1 set, and an algorithm + that touches rows (a bidiagonalization gathers and scatters one per step) + runs a fifth slower at n = 512 than at 500 or 520; pad the copy by 64 bytes + per column when the stride would be such a multiple (faer fork, + `svd/mod.rs` `work_rows`, 2026-10-23: 18.6 -> 15.3 ms at eight threads). + Benchmark shapes are powers of two, so the defect is invisible nowhere else. +- Rows of a column-major matrix are strided operands: vector kernels and + reductions want a contiguous one, so gather a row into a contiguous buffer + once, work there, and scatter once, rather than passing the strided view. ## Graph, Compiler, And Cache Work @@ -139,3 +149,16 @@ An audit of code that fans out on the CPU checks every rule of that section, using the check the rule names, and quotes the rule with each finding. In a repository not yet migrated to `strided_exec::Exec`, report each departure as a migration gap with its location, not as a defect. + +- A caller that hands faer a thread budget holds a team around its calls: + `faer::team::with_team(width, |par| ...)`, once per session or region, and + passes `par` to every faer call inside. The members spin while the team is + held instead of being woken for each region, so an algorithm made of many + short regions stays cheap (bidiagonalization at n = 512, eight threads: + 25.5 ms with the members woken per region against 19.2 ms with the team + held). The library does not create the team itself; the policy owner + (`CpuExecutionContext`, a benchmark harness) does. +- Inside the closure, run other parallel work on the same team rather than + through a raw rayon iterator (`par_chunks_mut`, `par_iter`, `rayon::join`): + the held members occupy their cores, and the raw iterator competes with + them.