Repository navigation
Compare benchmarks against master's stored results, re-check only failures - #9978
Conversation
Master's own Benchmarks runs store their results as a workflow artifact, and a pull request's run compares against the latest one instead of building and running master itself. Only the algorithms that fall below the stored reference are run again, on master and on the change on the same machine, and the run fails only if the regression repeats. Without a stored reference, all algorithms are re-checked that way. Each algorithm now runs once instead of a discarded warm-up run plus the measured one: the first run's samples differ from the warm ones no more than two warm runs differ from each other, and failures are re-checked. Pushes to branches other than master and tags no longer run the workflow: pull requests run it, and master's runs store the reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
As the issue asks, I cancelled this pull request's runs of the other workflows (Build & Test, Regression, Research Regression, Syntax, Report Generator, Python Virtual Environments) so the shared self-hosted runners stay free. This change only affects the Benchmarks workflow and |
|
The six failing |
Without stored results, run master as the reference and re-check the algorithms below it, instead of failing on a single sample per side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The Benchmarks failure was caused by this change, now fixed in c5bdc06. No master run has stored a reference yet, so the run took the no-reference path. That path ran all 22 algorithms once on master and once on the change, then failed on that single comparison: Python |
There was a problem hiding this comment.
Requesting changes: the Benchmarks run on this pull request failed, and the change caused it.
The fallback path fails on noise. No master run has stored a reference yet, so run 40807448038 took the fallback path. The lookup found master run 40766358261, the download failed with Artifact not found for name: benchmark-results as expected, Compare was skipped and the re-check ran all 22 algorithms. One single-sample comparison of everything, with nothing re-checked after it, then failed: Py HistoryRequestBenchmark measured 108 against master's 145 (floor 130.5). That is noise. This change doesn't touch the engine, and the same build measured 142 for that algorithm in its first pass a few minutes earlier, which passes. This is the #9961 failure mode that #9963 fixed, and the fallback brings it back. It also does one more pass than it needs, because the change's first full pass is thrown away. The description says a missing artifact never fails the run, and this run shows it does. Any later run whose lookup or download fails, such as an API error or an expired artifact, gets the same flaky check.
Suggested fix: make the fallback produce the reference, and leave the rest as it is. When there's no stored reference, build master and run all algorithms on it, then point Compare Benchmarks at whichever reference exists (reference/ or LeanMaster/) so it writes failed_benchmarks.json. The re-check then always runs only the failed algorithms. It never re-runs all of them, and the no-reference path does the same work #9963 does today, minus the warm-up.
What's fine:
- The
steps.*.outcomechaining works on CI. The download failed withcontinue-on-error,steps.reference.outcomewasfailure, Compare was skipped and the re-check ran. - The single pass took 7m42s on CI (02:39:00–02:46:42), against 19–20 minutes for a pass with the warm-up.
- Its samples were within a few percent of the master pass on the same machine for 21 of 22 algorithms. Py
EmptySingleSecuritySecondEquity289 vs 272 and C#EmptyFutures439 vs 394 were the widest. actions/checkout's default clean (git clean -ffdx) removes a stalefailed_benchmarks.json,reference/orLeanMaster/before each run, soALGORITHMScan't pick up a leftover file.- Over the last 40 runs, every Benchmarks job ran on the same
benchmarksrunner, so the cross-machine gap in the stored reference matters less than the description's caveat suggests.
Still unproven: the reference path, which finds and downloads a stored artifact and compares against it, can't run on CI until a master run stores one. Its consistency evidence is the 6 local runs. Because every failure there falls back with continue-on-error, the fallback has to be reliable before this merges. After the fix, please re-run Benchmarks on this pull request (it still takes the fallback path) and add the outcome to the description, together with the first-compare and re-check results.
Other repositories: none affected. Only the workflow and run_benchmarks.py change, and nothing outside this repository uses them.
CI: the six cancelled build checks are the other workflows, cancelled on purpose as the issue asks (each cancelled after ~1 minute). They're unrelated to this change. The failing Benchmarks check is caused by it.
Risk: risk:high. The changed workflow's own check fails, and the reference path has never run on CI. A green Benchmarks run with the fixed fallback would support lowering it. The change itself is CI-only and easy to revert.
| if: steps.compare.outcome == 'failure' | ||
| if: github.event_name == 'pull_request' && steps.compare.outcome != 'success' | ||
| run: | | ||
| ALGORITHMS=$([ -f failed_benchmarks.json ] && echo failed_benchmarks.json || true) |
There was a problem hiding this comment.
With no failed_benchmarks.json, this re-runs all 22 algorithms on both sides and the compare on line 89 is final, so one noisy sample fails the run. That's what happened on this pull request's own run: Py HistoryRequestBenchmark measured 108 vs 145, after 142 in the first pass. When there's no stored reference, could master's full run happen before Compare Benchmarks, so it becomes the reference, compares against the first pass and writes failed_benchmarks.json? Then this step only ever re-checks the failures.
There was a problem hiding this comment.
Approving. c5bdc06 fixes the fallback the way my last review asked, and the Benchmarks run on it passes.
What changed since my last review: when the stored reference can't be fetched, Run Benchmarks Master now builds master, runs every algorithm and copies the result to reference/benchmark_results.json. Compare Benchmarks then always compares against reference/ and writes failed_benchmarks.json, so Re-check Failed Benchmarks only ever runs the failures. The re-check no longer runs every algorithm on both sides with a single final compare.
CI evidence, run 40810140894 on c5bdc06:
- The lookup found the latest master run. The download failed with
Artifact not found for name: benchmark-results, as expected before any master run stores one, andcontinue-on-errorlet the run carry on.Run Benchmarks Masterran as the reference. - The compare passed all 22 algorithms, so the re-check was skipped. Py
HistoryRequestBenchmark, the one that failed last time, measured 148 against a floor of 131.4. The closest was PyIndicatorRibbonBenchmarkat 75 against 74.7, which would have triggered a re-check of that algorithm only. - The whole job took 18m32s. Today a run takes about 45 minutes, and the change's single pass took 7m40s.
Checked in the code:
- Every way the reference path can fail (the lookup, the download, a run from before this merges that has no artifact, an expired artifact) leaves
steps.reference.outcomeother thansuccess, so master is run in full as before. A missing artifact can't fail the run on its own. - In the re-check,
dotnet buildon LeanMaster is incremental after the fallback build, and it's needed on the reference path, where master hasn't been built yet.compare_benchmarks.pyiterates the reference's keys, so the re-check's partial results compare only the failed algorithms. - Master pushes only build, run and upload. They skip the master checkout, the compare and the re-check. Before this change, a master push compared master against itself.
Still unproven on CI: the reference path, where an artifact is found, downloaded and compared against, can only run after a master run stores one. Its evidence is the 6 local consecutive runs and the local regression check in the description. Any failure there falls back to the path proven above. Please watch the first pull request run after the first master run stores a reference.
Nit (description): the second bullet says "Master is checked out and built only in that case", but Checkout Lean Master runs on every pull request. Only the build is conditional, as the workflow-changes list says correctly.
CI: the six failing build checks are the Research Regression, Report Generator, Regression, Python Virtual Environments, Syntax and Build & Test runs on c5bdc06. Each was cancelled on purpose, as the issue asks, and none of them uses the workflow or run_benchmarks.py. If they are required to merge, they need to be re-run first.
Other repositories: none affected.
Risk: I set risk:low. The change touches only CI, isn't user-facing and is easy to revert. Each new failure mode falls back to the old behavior, and the run on this pull request proves that fallback works.
Closes #9970
Description
Makes the Benchmarks workflow cheaper without changing the benchmark algorithms.
benchmark_results.jsonas thebenchmark-resultsworkflow artifact. A pull request's run finds the latest successful master run (pushevent) through the Actions API and downloads that artifact as its reference. It builds and runs only the change and no longer builds or runs master up front. Master's runs only write the reference and pull request runs only read it. Each master run stores its own immutable artifact, so concurrent runs can't interfere.failed_benchmarks.jsonfrom Re-check failed benchmarks before failing the Benchmarks workflow #9963) run again on master and on the change, on the same machine after a 30 second pause. The run fails only when the regression repeats. Master is checked out and built only in that case. If no reference is available (the lookup or download fails, for example before the first master run stores one), master is checked out, built and run in full as the reference, as before. The same compare and re-check of failures follow, so a missing artifact never fails the run on its own.run_benchmarks.pyruns each algorithm once instead of a discarded warm-up run followed by the measured one. Measured locally, the first run's samples differ from the warm ones no more than two warm runs differ from each other (table below), and failures are re-checked anyway. The results file keeps the same format.pushnow triggers only onmaster, so pushes to other branches and tags no longer run the workflow. Pull requests are still benchmarked throughpull_request. In the workflow's last 200 runs, 53 were pushes to non-master branches, mostly the push side of branches that also had a pull request run. I didn't skip documentation-only changes with a paths filter: if the check is required, a filtered-out run leaves it pending and blocks the merge.Workflow file changes (
.github/workflows/benchmarks.yml):on.pushonly coversbranches: [master](it used to cover'*'branches and tags).permissions: contents: read, actions: read, so the token can read master's runs and artifacts, including from forks.Build Lean Masterstep is removed.Store Master Benchmarksstep (push only) usingactions/upload-artifact@v4.Find Master Benchmarksstep (curl + python, both already in the image) andFetch Master Benchmarksstep (actions/download-artifact@v4withrun-id), on pull requests only, bothcontinue-on-error.Checkout Lean Masternow runs on every pull request, after the reference lookup. It is only a checkout: master is built only when it is needed.Run Benchmarks Masternow runs only when the reference couldn't be fetched: it builds master, runs every algorithm and uses the result as the reference.Compare Benchmarkscompares againstreference/benchmark_results.json.Re-check Failed Benchmarks(unchanged trigger: the compare failed) builds master (a no-op when already built), then runs the failed algorithms on both sides after the pause.compare_benchmarks.pyand the benchmark algorithms are unchanged.How it was tested
Everything ran in
quantconnect/lean:foundationwith the workflow's container limits (--cpus 12 --memory 12g). The market data the benchmarks read was copied to local disk and mounted at/Data. Each benchmark processed the same number of data points and had the same failed data request count as when reading from the full data folder.Before and after times (seconds, local):
For reference, a full pass with the warm-up currently takes 19-20 minutes on CI, and a whole run about 45 minutes.
Consistency, 6 consecutive pull request runs on unchanged code against a stored master reference:
All 6 passed, and the re-check never needed more than 3 of the 22 algorithms.
Regressions are still caught: with a temporary busy-wait in the engine's time slice loop (about 25-33% slower on the smaller benchmarks, not committed), the first compare flagged 9 algorithms. The same-machine re-check confirmed 7 of them (for example C# BasicTemplate 226 vs 343.8 expected, Py BasicTemplate 187 vs 275.4), so the run fails.
Warm-up, the same 20 benchmarks run 3 times each in a row after dropping the OS page cache (EmptyEquityAndOptions400 was left out because its data was still incomplete locally at that point). The first, cold run was between -11% and +23% of the measured second run, median -1%. A third warm run was between -12% and +19% of the second, median +1%. A full pass took 328s, 329s and 325s.
Other checks:
actionlintreports only the existing warnings (the custombenchmarkrunner label,actions/checkout@v2).failed_benchmarks.json/ run-all argument handling in the re-check step was checked in bash.The Lean build passes. The C# test suite was not run, because the change touches only the workflow file and
run_benchmarks.py, which nothing in the solution uses.Caveats:
🤖 Generated with Claude Code