Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions rules/benchmark/measurement.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,33 @@ taskset -c <observer cores> <command>
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 <observer cores> <pid>` 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 <cores> --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:
Expand Down
24 changes: 24 additions & 0 deletions rules/common/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions rules/rust/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Loading