Calibrate benchmarks against Library Checker problems - #39
Merged
Conversation
The benchmark suite could report a large win for a change the judge barely noticed, because its workloads were synthetic: invented query mixes, unrealistic operators, sizes below the real constraints, and a report that flagged a 1% delta as an improvement. Workloads: every primary benchmark is now a transcription of a problem from yosupo06/library-checker-problems. Inputs are generated the way each problem's gen/max_random.cpp does -- same query mix, same value ranges, and a faithful port of the judge's uniform_pair for range queries, which is not the same distribution as randrange(l, n + 1). The run functions are shaped like accepted submissions: same monoid, same operators, and they accumulate answers rather than discarding them. This matters most for segtree, which was benchmarked with the C builtin max as its op; real submissions pass a Python lambda that costs an order of magnitude more per call, so tree-internal savings that looked enormous are worth a few percent in practice. Both shapes are now measured, as staticrmq and point_set_range_composite. Sizing: ACL_BENCH_SCALE scales N/Q/M, and each workload carries a COST_FACTOR normalising for per-element cost so every workload lands in a one-to-four-second band instead of ranging from 20ms to three minutes. The effective size is capped at the judge's maximum, and ACL_BENCH_SCALE=max pins everything there. Methodology: base and PR are measured in interleaved passes rather than one branch after the other, so drift on the runner cannot masquerade as a delta. The reported statistic is the minimum, not the mean -- job interference can only make a run slower. A delta is called a win or a regression only when it exceeds both 3% and the noise measured between passes; anything smaller renders as a explicit "within noise" marker. Coverage: public methods no judge problem exercises (max_right/min_left, dsu.groups, min_cut, change_edge, mcf_graph.slope, crt, divisors) keep extra_* workloads, reported in a separate table and labelled as a regression net rather than a source of speedup claims. Tests: every workload runner is checked against a naive reference, so a benchmark cannot get faster by being wrong, plus a check that no runner mutates its inputs (pytest-benchmark reuses one build() across rounds) and tests for the report's thresholds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178GKMBABNdFEX6E1PcXR4n
📊 Performance Benchmark ResultsWorkloads ran at ⏱️ Library Checker workloads (best of N, lower is better)
🧪 API coverage workloads (not judge-calibrated)
📏 Workload sizes
💾 Peak memory (lower is better)
Only the workloads whose module the change touches are measured; a scheduled sweep covers the rest. Each workload mirrors the linked Library Checker problem: same query mix, same operators, same value ranges as that problem's |
Running all 25 workloads on both branches of every PR costs more Actions time than it earns: a change to segtree.py cannot make factorize faster, so measuring factorize only adds runtime and gives noise another chance to invent a row. select_workloads.py maps changed files to workload names, and the workflow threads them through both runners with ACL_BENCH_ONLY so time and memory measure the same subset. compare.py --only keeps the report to that set instead of padding it with n/a rows for workloads that were deliberately skipped. The mapping is exact rather than heuristic because none of the library modules imports another -- they are meant to be pasted into a submission -- so a change to scc.py reaches exactly the workloads whose module is scc. two_sat.py and fps.py carry their own inlined copies of the SCC and NTT routines, and the selection reflects that: editing scc.py or convolution.py genuinely does not affect them. Touching the workloads, either runner, the selector or the workflow runs everything, because then the comparison itself is in question; touching docs, tests or compare.py runs nothing and the PR comment says so. Full coverage moves to a scheduled sweep (benchmark-full.yml): the whole suite on master at ACL_BENCH_SCALE=1.0, weekly or on demand, compared against the previous sweep's artifact and published to the job summary. That catches drift accumulating across many small PRs, and anything the per-PR selection cannot see. Absent a previous artifact it still renders the current times, so the first run is not a special case. compare.py also grows --base-label/--pr-label/--title so the scheduled sweep reads as Previous vs Now rather than base vs PR. The selector is named select_workloads.py, not select.py: benchmarks/ is placed first on sys.path by both runners, and a module named select.py there would shadow the stdlib select used by subprocess and asyncio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178GKMBABNdFEX6E1PcXR4n
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completely redesigned the benchmark suite to predict real-world performance by using actual Library Checker problems as workloads, rather than synthetic query mixes. This addresses systematic disagreements between benchmark results and judge performance.
Key Changes
Workload redesign: Replaced synthetic benchmarks with transcriptions of actual Library Checker problems:
point_add_range_sum(fenwicktree) - Point Add Range Sum problemstaticrmq(segtree) - Static RMQ problempoint_set_range_composite(segtree) - Point Set Range Composite problemrange_affine_range_sum(lazysegtree) - Range Affine Range Sum problemunionfind(dsu) - Unionfind problemscc(scc) - Strongly Connected Components problemtwo_sat(two_sat) - 2 SAT problembipartitematching(maxflow) - Matching on Bipartite Graph problemassignment(mincostflow) - Assignment problemScaling system: Introduced
ACL_BENCH_SCALEenvironment variable and per-workloadCOST_FACTORto normalize runtime across different problem complexities while staying within CI budgets. Each workload targets 1-4 seconds at default 0.2 scale.Improved comparison logic (
compare.py):Comprehensive test coverage (
test_benchmark_workloads.py,test_benchmark_compare.py):Documentation: Added extensive docstrings explaining why each design choice matters for predictive accuracy, plus
benchmarks/README.mdwith problem mappings and historical context.CI workflow updates (
benchmark.yml):Notable Implementation Details
_uniform_pair(): Implements Library Checker'sRandom::uniform_pairdistribution for query ranges, which produces mean width of ~n/3 (more realistic than uniform random).Workload class: Unified interface with metadata (problem URL, full_size, effective_scale) while maintaining backward compatibility with existing
(name, build, run)tuple consumers.Operator realism: Benchmarks use actual Python operators (e.g., affine map composition in Point Set Range Composite) rather than C builtins, so overhead measurements match real submissions.
Answer accumulation: All
run_*functions accumulate results rather than discarding them, matching how real submissions work and preventing dead-code elimination.Noise estimation:
spread()function measures relative variation between passes to set per-benchmark significance thresholds, preventing false positives from CI runner variance.https://claude.ai/code/session_0178GKMBABNdFEX6E1PcXR4n