Implements the CI plan (proposed in
ROCm/aorta#300; docs/ci-plan.md lands
with that PR). This is the “how it works / how to operate it” reference for the
nightly evaluation, dashboard, alerting, baselines, and automated bumps.
| Piece | File |
|---|---|
| Eval matrix (workload x config) | config/ci/nightly_eval_matrix.yaml |
| Expected-outcome baselines | config/ci/regression_baselines.yaml |
| CI dependency lock (exact pins) | config/ci/ci-constraints.txt (generated) |
| Harvester + comparator (pure) | scripts/ci/eval_lib.py |
| Nightly harness | scripts/ci/nightly_eval.py |
| Baseline refresher | scripts/ci/refresh_baselines.py |
| Requirements locker | scripts/ci/lock_requirements.sh |
| Dashboard generator | scripts/ci/gen_dashboard.py |
| Regression alerter | scripts/ci/alert_issue.py |
| Nightly workflow | .github/workflows/nightly-eval.yml |
| Pages build + deploy (landing + dashboard) | .github/workflows/pages.yml |
| Baseline refresh workflow | .github/workflows/refresh-baselines.yml |
| Lock refresh workflow | .github/workflows/lock-requirements.yml |
| Automated bumps | .github/dependabot.yml |
Triggered by workflow_run on “Nightly wheels” success (+ workflow_dispatch):
rocm-ci-setup).amd-aorta[hw-queue] (constrained by
config/ci/ci-constraints.txt when present).nightly_eval.py runs each matrix entry via aorta sweep run --strict,
harvests matrix.json, and compares each cell to the baselines:
passed),fail — a failed/errored cell (even
with no baseline), a missing/empty matrix.json, a per-entry timeout, a
blessed-baseline breach, or a run that did zero work (all skipped). An
empty baseline file therefore protects against crashes/failures immediately;
it just doesn’t add perf/metric gates until blessed. Results go to
gpu-nightly-results.json with build/ROCm metadata.alert_issue.py): opens/updates one nightly-regression issue on
failure; comments + closes it when green.publish job on ubuntu-latest): appends
results/<date>.json to the ci-results data branch (history only).Deploy (pages.yml): a repo has a single Pages site, shared with the
project docs, so one workflow owns the deploy. On main pushes, after each
Nightly Evaluation completes, and on demand, pages.yml builds the Jekyll
site into _site/, relocates the rendered README from _site/index.html to
_site/docs/index.html, writes the self-contained dashboard
(gen_dashboard.py, from the ci-results history) to _site/index.html, and
deploys the combined site via actions/upload-pages-artifact +
actions/deploy-pages. Repo Pages source must be “GitHub Actions”
(Settings -> Pages). Nightly dashboard: https://rocm.github.io/aorta/;
project docs: https://rocm.github.io/aorta/docs/.
Sanitizer nightly dashboard: https://rocm.github.io/aorta/sanitizers/ (linked
from the root nav). The route is always published so it never 404s: before the
first successful sanitizer nightly it shows a “no sanitizer runs yet” empty
state, and if the latest nightly failed it shows the last data under a red
stale banner linking the failed run (rather than silently re-serving the
previous green page). Only sanitizer data produced by a main run is
published.
Each nightly’s raw per-case sanitizer_report.json files are co-located on the
sanitizer-results data branch under dashboard/runs/<YYYY-MM-DD>-<run_id>/
(one <case>/sanitizer_report.json per recipe plus a meta.json with commit /
date / gpu / run_url / gate), and the rendered page links to them: the latest
run table has a per-recipe Report link, the kernel-detail sections carry a
“view raw report” link, and each Run row in the history table links to its
runs/<id>/ area. A tiny runs/<id>/index.html landing page lists that run’s
three reports. Because pages.yml copies dashboard/* recursively into
_site/sanitizers/, everything under runs/ is served at
/sanitizers/runs/... with relative links and no change to pages.yml. The
publish job keeps a rolling window of the newest 30 run directories
(gen_sanitizer_dashboard.py --history-root <dir> --keep 30); older ones are
pruned. Previously these raw reports lived only in an expiring Actions
artifact and were never linked; the rolling window makes them durable and
reachable from the page.
The dashboard previously lived at /ci/, so that path is kept: /ci/
redirects to the root and /ci/data.json is published alongside
/data.json for anything already polling it. A verification step fails the
deploy if any of those routes would be missing, since a Pages deploy
replaces the whole site and a dropped route 404s immediately.
Three views, ordered so the page reads from “what should I do?” down to detail:
_MOVE_PCT (10%). A quiet night says so
in one line rather than rendering an empty list.Both of the first two withhold themselves until there are two runs to compare; with a single run they would imply a trend from one sample. The page needs no JavaScript – the only script expands and collapses the metric rows, and its controls stay hidden unless it runs.
fail.
Default baselines also bless correctness metrics (exact-equality checksums
such as logits_checksum), so a finite-but-wrong output is caught even without
perf gating. Baselines honor the expected passed outcome (an expected-failure
baseline is supported).refresh_baselines.py --perf-gate (adds step_time_ms.max plus per-metric
policy/value bounds – min for throughput, max for latency/step-time, equal
for checksums – that the comparator then enforces; a required metric that is
absent is a failure).Baselines are ROCm/stack-specific. Generate them on the runner and bless via PR:
Actions -> Refresh baselines -> Run workflow
refresh_baselines.py runs the matrix, captures each passing cell, and opens a
PR updating regression_baselines.yaml. Review the diff and merge to bless. An
empty baseline file means record-only (nightly won’t be red before blessing).
The refresh fails atomically if any entry ran but couldn’t be blessed
(timeout / missing or empty matrix.json / a cell that didn’t pass) — this
prevents silently reverting live gates to record-only. Entries the runner
can’t physically exercise (e.g. min_gpus: 8 variants on a smaller box) are
not fatal: their existing baselines are carried over unchanged, so you can
still refresh single-GPU baselines on a small runner and only re-bless the
multi-GPU entries on an 8-GPU box.
.github/dependabot.yml) opens weekly PRs for pip / docker
(ROCm base image digest) / github-actions updates.config/ci/ci-constraints.txt (partial pip
constraints, not a hash-pinned lock). Both open PRs; a human blesses.
No auto-merge.The publish step keeps only the most recent 180 results/<date>.json files on
the ci-results data branch (older ones are pruned), and the dashboard renders at
most the last 180 builds (gen_dashboard.py --max-builds). Files are tiny; adjust
the cap in nightly-eval.yml / the flag if a longer window is wanted.
The sanitizer nightly keeps a separate rolling window on the
sanitizer-results data branch: the newest 30 dashboard/runs/<id>/
directories (each holding that run’s three raw sanitizer_report.json files and a
meta.json). The publish job prunes older ones and re-renders with
gen_sanitizer_dashboard.py --history-root dashboard/runs --keep 30; adjust
keep in sanitizers-nightly.yml (and the matching --keep) to change the
window.
pages.yml, which serves
the dashboard at / and the project docs under /docs/ from one deploy.
Run the Pages (landing + nightly dashboard) workflow once to publish
immediately (the docs are served even before any nightly results, and the
root shows the dashboard’s empty state).recording rather than passing, because
nothing has been graded against a baseline yet.refresh_baselines.py --perf-gate.