Skip to content

Phase 2 release prep: pin to whestbench 0.16.0, add a rounds reference - #37

Merged
spMohanty merged 12 commits into
mainfrom
chore/flopscope-main-pin
Aug 23, 2026
Merged

spMohanty merged 12 commits into
mainfrom
chore/flopscope-main-pin

Conversation

@spMohanty

@spMohanty spMohanty commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

Readies the kit for the Phase 2 public release. The enabling change is one dependency pin; most of the rest of the diff follows from it.

Why the pin moved

before after
whestbench >=0.15.0,<0.16.0 >=0.16.0,<0.17.0
flopscope >=0.11.0,<0.12.0 >=0.12.0,<0.13.0

whestbench 0.16.0 is the first release whose defaults are the Phase 2 round. Under 0.15.0 the CLI applied Phase 1 limits while the documentation described Phase 2:

whest run default 0.15.0 0.16.0
shape 256 × 32 1024 × 16
flop_budget 2.72e11 2⁴¹ = 2,199,023,255,552
wall_time_limit_s 60 120
setup_timeout_s (flag did not exist) 5.0
residual_wall_time_limit_s — 0.4
lambda_flops_per_second 1e11 0.0, so C_m = F_m
dataset-less --n-samples 2,560,000 200,000

Every one of those is now the CLI default, so a plain uv run whest run rehearses the Phase 2 parameters with no flags at all. Several pages carried explicit "this becomes true at 0.16.0" hedges, written when it was not; they are gone.

flopscope 0.12.0 reprices ~40 operations relative to 0.11.0, in both directions, but none are operations the bundled examples use (matmul, tagged einsum, stats.norm, astype, pointwise). Every documented FLOP figure is therefore unchanged, and tests/test_flopscope_cost_docs.py is the gate that keeps it so.

pyproject.toml records the ordering rule it is following: the kit should never report FLOP counts a submission is not scored on, so it tracks the graded meter rather than the newest release.

New: docs/reference/rounds.md

The kit had no page describing the round it is in, so a participant reading a Phase 1 number somewhere had no way to place it. The new page puts all three rounds side by side — dataset tag, shape, predict() return shape, forward-pass cost, budget, λ, residual cap, wall cap, residual model — plus how to reproduce a score from an earlier round and why pre-Phase-2 local numbers do not compare.

It also records the ratio of budget to forward-pass cost:

round budget ÷ one forward pass
v1-warmup 64,850
v1-phase1 64,850
v2-phase2 65,536

Each round's budget corresponds to approximately 65,000 forward passes. Phase 2's budget is 8.08× Phase 1's, and one Phase 2 forward pass costs 8× one Phase 1 forward pass, so the number of passes a submission can afford is unchanged across rounds.

tests/test_rounds_doc.py (19 tests) asserts every cell of that table against whestbench.budget.ROUNDS at runtime, so a round change upstream fails the build. The two forward-pass rows are derived rather than read, since RoundConfig has no such field, and the page states this.

Why Phase 2 drops the residual price

The kit asserted C_m = F_m in three places and explained it in none, which leaves anyone who remembers Phase 1's λ unable to tell whether the rate was removed deliberately or is simply missing. rounds.md now carries the reasoning and the other two sites link to it:

  • Pricing permits the purchase of unmetered compute. Under λ, arithmetic performed outside flopscope was billable rather than disallowed: it had a defined cost, and a submission that paid it stayed within the rules. Phase 2 prohibits computation outside flopscope, so there is no transaction for a rate to price. The remaining use of residual time is plumbing, which a cap bounds directly.
  • The priced score depended on the grading machine. R_m is wall-clock time, so under λ > 0 an identical submission produced different values of C_m on different hardware and under different load. F_m is derived analytically from tensor shapes and dtypes and does not vary by machine.
  • A combined budget did not state a FLOP limit. Under λ, B bounded a sum of FLOPs and priced seconds, so the FLOPs available to a submission depended on how much residual time it spent.

The page also states that the 0.4 s cap is a limit rather than an allowance: residual time below it does not affect the score, and exceeding it zeroes that MLP's predictions.

Fixed: whest package silently dropped shipped weights

The kit's own .gitignore contained *.npz, and whest package reads .gitignore and .whestignore as one pattern list with no ! negation. Following docs/how-to/ship-weights.md produced:

Status: success

…and a tarball containing estimator.py and manifest.json, with weights.npz absent and no warning in the output. The omission first surfaced at grading time as a setup() error, which fails the whole submission rather than one MLP.

No file in this repository is an .npz; examples/04_shipped_weights.py writes to a temporary directory. The pattern therefore excluded only participant files, including the workflow documented in ship-weights.md. Removed, with a comment recording why it should not return. Verified before and after.

Fixed: an incorrect FAQ entry on process isolation

faq.md said Stage 4 and the grader "run each MLP in a fresh process — that state is gone." Measured under both runners: one pid, one module import, one setup(), one instance, state carried across MLPs. --runner subprocess is identical to --runner local here.

The remedy it recommended, caching through SetupContext.scratch_dir, raises inside setup(): that field is None on every 0.16.0 path, and a setup() exception fails the whole submission.

The entry now describes worker-level state, which the kit did not cover: state carries between calls, but a submission is served by several worker processes (approximately 5-15 setup() runs), each handling only the MLPs assigned to it, and a replaced worker starts from an empty state. A cache built across MLPs is therefore populated locally and empty on several grader workers.

Corrections

Corrections made against the shipped 0.16.0 harness, by theme:

  • setup() does not run once per submission — once per worker process, several workers per submission, and a replaced worker re-runs it. Corrected in six places. It changes the advice for anyone amortising an expensive setup across the suite.
  • Defaults described as flags you must pass — --wall-time-limit, --residual-wall-time-limit and the new --setup-timeout are already the Phase 2 values.
  • Re-measured numbers — reductions bill N−1 (var bills 4·N), fnp.eye(1024) is 2,048 at its float64 default, stats.norm per-element costs, the einsum symmetry saving. The Phase 1 example ratio was quoted as ~160×, a float64-era figure compared against a float32 one; measured like-for-like it is 137.6×.
  • Report fields — four emitted on every run were undocumented (whestbench_version, results.per_layer_mse, per_mlp[].mlp_name, per_mlp[].per_layer_mse); traceback was listed as error-only when it is always present; error_code is not stable across runners.
  • Commands — every fenced whest … is now uv run whest …; a bare whest version can resolve to a different install entirely.
  • Links — one dead URL (/participants/me/customize → /participants/me/edit); 141 internal links checked, 0 broken.

local_engine.py now models the 120 s wall cap and the 0.4 s residual gate, calls setup() outside the budget context and teardown() after the sweep, and states in its docstring what it still does not model.

Style guide

The kit adopts the Google developer documentation style guide.

Headings and titles move to sentence case. Capitalization only: GitHub lowercases anchors, so the wording is unchanged and every existing link still resolves. The emoji prefixes are kept.

Prose moves to second person and active voice with the actor named, figurative language is replaced by the mechanism it stood for, conditions precede instructions, and link text describes its destination. Word-level changes follow the guide's word list: e.g. becomes "for example", i.e. becomes "that is", etc. is replaced by naming the remaining items, and simply, easy and filler uses of just are removed. Occurrences of just meaning "only" are kept, as in "Stage 1: just python".

No number, flag, path, field name, command or error string changed, and no fenced content was edited.

Refreshed demo

assets/demo.cast was recorded under Phase 1: width=256 depth=32, FLOP Budget 2.72e+11, memory_limit_mb=65536, and an unpinned dataset resolving to main. Re-recorded at the Phase 2 shape.

The two recorders had drifted apart — one pinned @v1-phase1, the other pinned nothing — which is how the cast went stale unnoticed. Both now pin @v2-phase2, and make demo-cast-headless SOURCE=. can record a local checkout so a pre-merge release can be captured accurately.

Verification

check result
uv run pytest tests/ -q 47 passed
uv run ruff check . clean
CI green, including the new Stage 4 and Stage 5 smokes
examples 01–04, estimator.py, --baseline all run
whest validate passes on all five bundled estimators
internal documentation links 43 files, 0 broken

Baseline scores over the full 100-MLP mini split at @v2-phase2:

estimator final_layer_mse all_layers_mse
zeros baseline 0.909506 0.763593
01_random 0.685614 0.538962
02_mean_propagation 2.21617e-04 1.61058e-04
03_covariance_propagation 4.05038e-06 2.22912e-06

These match the figures already published in concepts/scoring-model.md, so that table is unchanged. The sentence above it, describing the numbers as a Phase 1 measurement, was incorrect and is removed.

… seed baselines at float32

Re-states the starter kit for the Phase 2 round and makes every FLOP figure in
the docs a measured one.

Phase 2 changes four things a participant plans around, and the kit asserted the
Phase 1 value for each:

  shape            256 x 32          -> 1024 x 16
  per-MLP budget   2.72e11           -> 2**41 = 2,199,023,255,552
  effective cost   C_m = F_m + l*R_m -> C_m = F_m
  residual time    priced at l=1e11  -> capped at 400 ms, not priced

Covered across the concepts pages, the getting-started stages, the how-to
guides, the reference pages and troubleshooting. Placeholder tables are replaced
with measured values, and stage-3 now says plainly that the dataset is the only
route to the Phase 2 shape.

New: docs/concepts/allowed-code.md

Phase 2 replaces PRICING computation outside flopscope with PROHIBITING it,
which is a different kind of rule and needed its own page. It states the three
things a submission may use - the grader's interpreter, the flopscope client
API, the pure-Python stdlib - and the prohibition list, and is explicit that
this is a rule of the round rather than a performance tip: code outside it is
disqualifiable, not merely expensive, and enforcement can be retrospective. It
also draws the line residual time sits on - plumbing, not computation - while
keeping the permission for shipped weights and lookup tables.

Examples reseeded at float32

examples/02 and examples/03 seeded their state at float64. flopscope bills
float64 at 2x, and fnp.zeros/fnp.eye default to it, so NumPy promotion carried
that rate through the O(width^3) einsum that is 99.7% of covariance
propagation's cost. Measured at 1024x16:

  02_mean_propagation          153,913,344 ->      86,639,616
  03_covariance_propagation    103,407,495,614 ->  51,709,240,799

Final-layer MSE is unchanged to five significant figures. 03 also drops a
float64 round-trip through stats.norm whose results were re-infecting the loop
at the 2x rate.

This is independent of the weight dtype. whestbench now hands estimators fp32
weights, which retired the kit's manual weight cast, but neither remaining
source originates at the read path: fnp.zeros/fnp.eye default to float64 and the
STATE wins the promotion in fp32 x fp64, and stats.norm promotes to float64 to
match scipy whatever the weights are. Measured with fp32 weights held constant,
reverting only the state seeding costs 2.000x and dropping only the cast-back
costs 1.937x.

The replacement carries a version-stability trap worth naming: substituting a
plain 0.0 for the round-trip made the gain term's dtype depend on NEP 50 weak
promotion, billing 131,072 FLOPs under flopscope 0.11.0 and 65,536 under 0.12.0.
An explicit float32 zero pins it, and both totals now reproduce identically
under 0.11.0, 0.12.0 and main at 0812afe9.
tests/test_flopscope_cost_docs.py pins them so a future reseed cannot drift the
documented numbers silently.

Pinned to the meter the grader charges

flopscope>=0.11.0,<0.12.0 and whestbench>=0.15.0,<0.16.0, both from PyPI. 0.11.0
is deliberate, not lag: the evaluator pins flopscope[server]==0.11.0, so that is
the accounting a submission is scored under today. 0.12.0 is released, but
whestbench 0.15.0 declares flopscope<0.12.0, so the three move together -
whestbench cuts a release pinning 0.12.0, the evaluator re-pins to that pair,
then the kit follows. Pinning the kit AHEAD of the grader reports FLOP counts a
submission is not scored on, which is the same defect as lagging, pointing the
other way.

Corrections

A conformance pass against the official rules found five factual errors, and
verifying those fixes found five more in the same family. All ten are measured:

  - problem-setup described the inputs as "uniformly random"; they are standard
    normal N(0,1), which the rectified-Gaussian derivation on the same page
    already assumed.
  - code-patterns cited 103,062,421,504 for the einsums (exactly 2x the truth)
    and called as_symmetric's billing dtype float64. Measured: einsum
    51,531,210,752 over 16 calls, as_symmetric 7,340,031 per call at float32.
  - algorithm-ideas quoted 4.7% of budget two lines after quoting 2.351% for the
    same example.
  - inspect-mlp-structure said matmul is 87% of mean propagation; measured
    67,076,096 of 86,639,616 = 77.42%.
  - the CHANGELOG announced that the kit tracks flopscope main, contradicting
    the pin it ships.
  - examples/03 called the float32 saving "an exact halving". It cannot be: the
    32 stats.norm calls bill float64 in BOTH runs, so they cancel. 49.99%.
  - examples/03's Step 7b understated its own argument - the untagged
    contraction bills 4,292,870,144, not 4,225,859,552, so the symmetry tag
    saves 24.98% per layer and repays its re-validation ~146x, not ~137x.
  - a float64 counterfactual quoted as measured in three files did not
    reproduce (103,407,495,614, not 103,407,544,766).
  - a per-sample forward cost was off by one FLOP, and two provenance labels had
    gone stale against the 0.11.0 pin.

Nearly all share one cause - the float32 reseed above - so each stale figure was
almost exactly double the correct one.

Three hedges here are deliberate and collapse once the kit repins to whestbench
0.16.0: --setup-timeout and run_config.setup_timeout_s do not exist in 0.15.0;
run_config.wall_time_limit_s reports 60 under 0.15.0; and local effective_compute
is still inflated by the Phase 1 lambda term, which
troubleshooting/common-participant-errors.md states outright rather than papering
over.

Verified: 27 tests pass, ruff clean, examples 01-04 all run, whest validate
accepts 4 estimators, and the documented FLOP totals reproduce exactly under
flopscope 0.11.0, 0.12.0 and main.
@spMohanty
spMohanty force-pushed the chore/flopscope-main-pin branch from 16fb366 to ce4b84a Compare August 21, 2026 11:09
whestbench 0.16.0 is the first release whose defaults are the Phase 2 round:
1024x16, a 2**41 per-MLP budget, a 120 s predict cap, a 5 s setup() cap, a
0.4 s residual cap, and lambda = 0. Under 0.15.0 the CLI applied Phase 1 limits
while the documentation described Phase 2.

flopscope 0.12.0 reprices approximately 40 operations relative to 0.11.0, in
both directions. None are operations the bundled examples use (matmul, tagged
einsum, stats.norm, astype, pointwise), so every documented FLOP figure is
unchanged. tests/test_flopscope_cost_docs.py asserts this.

The specifier strings are character-identical to whestbench's own, as
scripts/bump_starterkit_pin.py --check-deps requires.
…ball

whest package reads .gitignore and .whestignore as a single pattern list with
no `!` negation, so the kit's own `*.npz` rule excluded weights.npz from folder
submissions. The command reported success and the archive contained only
estimator.py and manifest.json. The omission first surfaced at grading time as
a setup() error, which fails the whole submission rather than one MLP.

No file in this repository is an .npz; examples/04_shipped_weights.py writes to
a temporary directory. The pattern therefore excluded only participant files,
including the workflow documented in docs/how-to/ship-weights.md.

A folder submission now packages estimator.py, weights.npz and manifest.json.
The kit did not state which round it describes, so a Phase 1 figure encountered
elsewhere could not be placed. rounds.md lists v1-warmup, v1-phase1 and
v2-phase2 side by side: dataset tag, shape, predict() return shape,
forward-pass cost, budget, lambda, residual cap, wall cap and residual mode. It
also covers reproducing a score from an earlier round, and why pre-Phase-2
local numbers are not comparable.

Each round's budget corresponds to approximately 65,000 forward passes: 64,850
for v1-warmup and v1-phase1, 65,536 for v2-phase2.

tests/test_rounds_doc.py asserts every cell against whestbench.budget.ROUNDS at
runtime, so a round change upstream fails the build. The two forward-pass rows
are derived rather than read, since RoundConfig has no such field; the page
states this, and a test pins the metered figure it quotes to the one in
local-engine-api.md.
Stale 0.15.0 statements. score-report-fields.md carried a "Local reports still
apply lambda" section advising readers to distrust effective_compute,
mean_compute_utilization and combined_budget_exhausted. Under 0.16.0 lambda
defaults to 0.0, it is recorded in run_config, and effective_compute equals
flops_used. The section and its five referrers are removed. stage-3 stated that
a dataset-less suite is fixed at 256x32; run_config reports 1024x16.

setup() does not run once per submission. It runs once per worker process, a
submission is served by several workers, and a worker that dies mid-suite is
replaced and re-runs it, approximately 5-15 times in total. Corrected in six
places.

faq.md stated that Stage 4 and the grader run each MLP in a fresh process. Both
local runners use a single worker process: one pid, one import, one setup(),
with state carried across MLPs. The remedy the entry recommended, caching
through SetupContext.scratch_dir, raises inside setup() because that field is
None on every 0.16.0 path, and a setup() exception fails the whole submission.
The entry now describes worker-level state instead.

Also corrected: defaults described as flags that must be passed
(--wall-time-limit, --residual-wall-time-limit and --setup-timeout are already
the Phase 2 values); reduction and dtype costs; four report fields that are
emitted but were undocumented; fenced whest commands now use uv run; one dead
URL; and the Phase 1 example cost ratio, quoted as ~160x, which compared a
float64 figure against a float32 one. Like-for-like it is 137.6x.

The accuracy table in concepts/scoring-model.md is unchanged. The sentence above
it, describing those numbers as a Phase 1 measurement, was incorrect and is
removed.

Prose throughout is tightened, with no change to any number, flag, path, field
name or command.
CI ran the Stage 1-3 smokes but not the subprocess runner or the packaging
path, which is where the 0.16.0 changes apply. Both are added, measured at
approximately 33 s and 1 s against a 15-minute timeout.

The Makefile help lists SOURCE=. for the headless recorder, and demo-cast
records that it clones GitHub main and cannot capture an unmerged release.
local_engine.py did not model two limits the grader applies. It now enforces
the 120 s predict() wall cap and the 0.4 s residual gate, calls setup() outside
the budget context, and calls teardown() after the sweep, matching runner.py.
Its docstring lists what it still does not model: the 5 s setup timeout, the
8 GB memory cap, and zero-and-continue on failure. The convergence table prints
all_layers_mse and final_layer_mse, ranked metric last.

estimator.py registers the module in sys.modules before exec_module, so the
diagnostic reports the module path rather than <unknown>, and forwards --seed.

examples/04 uses fnp.zeros((), dtype=fnp.float32) in place of fnp.ones(()),
which bills 2 FLOPs at the float64 default and contradicted ship-weights.md.
examples/03 lowers its rescale guard threshold from 1e100 to 1e30, and its
docstring gives the guard's measured cost, 1,023 FLOPs per layer and 16,368
across the suite, in place of a claim that it is free. examples/01 and 02 no
longer describe the random baseline as a propagation example, nor state that
every bundled example carries the seeding scaffold; examples/04 does not.

Tests use measured bands in place of placeholder caps, add a parity test for
the rng/seed distinction, and use paths that work when pytest runs from inside
tests/.
… checkout

The two recorders had diverged: record-demo.sh pinned @v1-phase1 and
record-demo-headless.py pinned no revision, tracking main. The committed cast
therefore showed Phase 1's 256x32 after the kit moved to 1024x16. Both now pin
@v2-phase2, with a comment recording why an unpinned demo goes stale without
notice.

--source clones a local checkout instead of GitHub main, which is required to
record an asset for a release that has not merged. It reads committed state
only, and must be an absolute path because the clone runs from a temporary
directory. make demo-cast-headless SOURCE=. supplies one.
The committed cast was recorded under Phase 1: width=256 depth=32, a 2.72e+11
budget, memory_limit_mb=65536, and an unpinned dataset resolving to main. It is
embedded at the top of README.md.

Re-recorded against this branch at @v2-phase2. The cast shows width 1024,
depth 16, a 2.20e+12 budget and memory_limit_mb=8192.
@spMohanty spMohanty changed the title Phase 2: re-state the kit for 1024x16, fix the meter drift, and seed the baselines at float32 Phase 2 release prep: pin to whestbench 0.16.0, add a rounds reference Aug 22, 2026
The kit stated the rule, C_m = F_m with residual time capped at 400 ms rather
than charged, in three places without stating the reason. rounds.md now gives
it, and the other two sites link there.

Three properties of the priced model motivated the change:

- Pricing permits the purchase of unmetered compute. Under lambda, arithmetic
  performed outside flopscope was billable rather than disallowed. Phase 2
  prohibits it, so there is no transaction for a rate to price.
- The priced score depended on the grading machine. R_m is wall-clock time, so
  under lambda > 0 an identical submission produced different values of C_m on
  different hardware. F_m is derived from tensor shapes and dtypes and does not
  vary by machine.
- A combined budget did not state a FLOP limit. Under lambda, B bounded a sum
  of FLOPs and priced seconds, so the FLOPs available depended on how much
  residual time a submission spent.

Also records that the 0.4 s cap is a limit rather than an allowance: residual
time below it does not affect the score, and exceeding it zeroes that MLP's
predictions.
@spMohanty
spMohanty force-pushed the chore/flopscope-main-pin branch from 11a96d8 to 86674ed Compare August 22, 2026 05:46
Applies https://developers.google.com/style across the documentation, examples
and scripts.

Headings and titles move to sentence case. Capitalization only: GitHub lowercases
anchors, so the wording is unchanged and every existing link still resolves. The
emoji prefixes are kept.

Prose moves to second person and active voice, with the actor named. Figurative
language is replaced by the mechanism it stood for, so "wall time is a gate, not
a currency" becomes "wall time is a limit, not part of the score", and "you pay
the ground-truth tax on every run" becomes "you regenerate the ground truth on
every run".

Word-level changes follow the guide's word list: "e.g." becomes "for example",
"i.e." becomes "that is", "etc." is replaced by naming the remaining items, and
"simply", "easy" and filler uses of "just" are removed. Occurrences of "just"
that mean "only" are kept, as in "Stage 1: just `python`".

Conditions now precede instructions, link text describes its destination, and
rhetorical questions addressed to the reader are replaced by statements.

No number, flag, path, field name, command or error string was changed, and no
fenced content was edited.
@spMohanty
spMohanty force-pushed the chore/flopscope-main-pin branch from 86674ed to 8351087 Compare August 22, 2026 06:18
Six places claimed that an unflagged local run meters "exactly as the grader
does". The verifiable claim is narrower: since whestbench 0.16.0 the CLI
defaults are the Phase 2 round's parameters. What a particular deployment
applies at a given moment is not something the kit can assert, and stating it
as fact invites a participant to trust a local number for something it does not
cover.

Each site now says what the defaults are rather than what another system does
with them.
@spMohanty
spMohanty merged commit eb320ee into main Aug 23, 2026
1 check passed
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