nightly #37
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
| name: nightly | |
| # Everything too expensive, network-dependent or non-gating for the PR path: | |
| # foundation-model tests (downloads), the full-matrix e2e training suite, | |
| # external-dep refresh (polar/les install from git and can break on their | |
| # own), CPU benchmarks, the real-tier LAMMPS integration, and the durations | |
| # refresh. GPU nightly coverage runs via ci-gpu-mpcdf.yaml's own schedule. | |
| on: | |
| schedule: | |
| - cron: '47 2 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: nightly | |
| cancel-in-progress: true | |
| jobs: | |
| foundations: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/setup-mace | |
| with: | |
| python-version: "3.12" | |
| extras: dev | |
| # graph_longrange so the polar foundation loaders run too. | |
| pip-packages: -r requirements/polar.txt | |
| - name: Cache foundation model downloads | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/mace | |
| # URLs live in foundations_models.py: new URLs -> new cache. | |
| key: mace-foundations-${{ hashFiles('mace/calculators/foundations_models.py') }} | |
| # Downloads are allowed and REQUIRED here: a broken download host | |
| # must turn this job red, never a PR. | |
| # | |
| # tests/golden joins the selection for its `network`-marked foundation | |
| # goldens: they are the only place a re-uploaded or re-pointed | |
| # published artifact is caught, and with require-caps below they fail | |
| # here rather than skipping. The unmarked goldens in that directory run | |
| # again here, which is cheap and means a nightly is a complete | |
| # statement about the golden set. | |
| - uses: ./.github/actions/run-tests | |
| with: | |
| tests: tests/foundations tests/golden | |
| markers: not gpu | |
| require-caps: network,polar | |
| allow-network: "true" | |
| timeout: "1200" | |
| workflows-full: | |
| runs-on: ubuntu-latest | |
| name: workflows-full (py${{ matrix.python-version }}, group ${{ matrix.split-group }}) | |
| timeout-minutes: 150 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: [ '3.10', '3.11', '3.12', '3.13' ] | |
| split-group: [ 1, 2 ] | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/setup-mace | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # wandb for the same reason as the PR-time workflows job: the flags | |
| # are only tested where the client is installed. | |
| extras: dev, wandb | |
| - name: Free up disk space | |
| run: | | |
| sudo rm -rf /usr/lib/jvm /usr/share/dotnet /usr/share/swift \ | |
| /usr/local/.ghcup /usr/local/lib/android \ | |
| /usr/local/share/chromium /opt/microsoft /opt/google | |
| # The e2e suite INCLUDING its network-marked tests (the PR path | |
| # runs this suite without network). | |
| - uses: ./.github/actions/run-tests | |
| with: | |
| tests: tests/workflows | |
| require-caps: wandb | |
| splits: "2" | |
| group: ${{ matrix.split-group }} | |
| allow-network: "true" | |
| timeout: "1200" | |
| extensions-refresh: | |
| # Re-runs the extension jobs even when nobody touched the repo: polar/les | |
| # install from external git and can break on their own. | |
| uses: ./.github/workflows/ci-extensions.yaml | |
| permissions: | |
| contents: read | |
| benchmarks: | |
| # The performance baseline the rewrite will be judged against, recorded | |
| # while legacy is still the live stack: "not slower" is only falsifiable | |
| # against an old number, and after legacy retires the comparison runs | |
| # against the frozen one, so these have to be reproducible cases with | |
| # recorded metadata rather than one-off timings. | |
| # | |
| # This job used to be continue-on-error and published nothing at all: the | |
| # only test in tests/benchmarks was marked `gpu` AND `network`, so all | |
| # sixteen parametrizations skipped on a CPU runner, the job went green, | |
| # and benchmark.json was reproducibly a 0-byte file. tests/benchmarks/ | |
| # test_inference_cpu.py adds the CPU cases, and since a job that cannot | |
| # fail is not a baseline, continue-on-error is gone and the artifact is | |
| # checked for contents below -- not merely for existence, which it always | |
| # had. | |
| # | |
| # numprocs 0 is load-bearing: pytest-benchmark disables itself under | |
| # xdist, so a parallel run of this job would collect timings from nothing. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/setup-mace | |
| with: | |
| python-version: "3.12" | |
| extras: dev | |
| - name: Cache foundation model downloads | |
| # The MP-small cases are network-gated; without the cache every night | |
| # re-downloads the same checkpoint. | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/mace | |
| key: mace-foundations-${{ hashFiles('mace/calculators/foundations_models.py') }} | |
| - uses: ./.github/actions/run-tests | |
| with: | |
| tests: tests/benchmarks | |
| # Deliberately no marker expression: every narrowing applied here so | |
| # far has ended with an empty artifact. The gpu-marked cases skip on | |
| # this runner and say so. | |
| allow-network: "true" | |
| # The MP-small half of the baseline is downloaded, so a lost network | |
| # must turn this job red rather than quietly halve the artifact. | |
| require-caps: network | |
| numprocs: "0" | |
| timeout: "3600" | |
| extra-args: --benchmark-json=benchmark.json | |
| # THE check that this job stopped being green-while-measuring-nothing. | |
| # | |
| # `if-no-files-found: error` on the upload below cannot catch it: | |
| # pytest-benchmark creates the --benchmark-json path regardless of what | |
| # ran, and a session in which every case skipped leaves a **0-byte | |
| # file** behind (reproduced locally: `pytest tests/benchmarks/ | |
| # test_benchmark.py --benchmark-json=...` -> 16 skipped, 0-byte json). | |
| # So the file is always present, the upload always succeeds, and the | |
| # only thing that distinguishes a baseline from an empty artifact is its | |
| # contents. Hence a content assertion rather than a presence one. | |
| # | |
| # It also checks one case per declared size, because the hole this job | |
| # had was size-shaped: skipping is per-parametrization, so a single | |
| # surviving case would satisfy "non-empty" while the size spread the | |
| # downstream comparison needs had quietly gone. | |
| - name: The artifact must actually contain a baseline | |
| run: | | |
| python - <<'PY' | |
| import json, pathlib, sys | |
| raw = pathlib.Path("benchmark.json").read_text() or "{}" | |
| runs = json.loads(raw).get("benchmarks", []) | |
| if not runs: | |
| sys.exit( | |
| "benchmark.json carries no benchmarks: every case skipped " | |
| "and this job would have published an empty baseline." | |
| ) | |
| sizes = {b.get("extra_info", {}).get("regime") for b in runs} | |
| missing = {"subdomain216", "subdomain512", "kernel1728"} - sizes | |
| if missing: | |
| sys.exit(f"no benchmark ran at size(s) {sorted(missing)}") | |
| for b in runs: | |
| info = b.get("extra_info", {}) | |
| absent = [ | |
| k for k in ("dtype", "device", "torch_version", "backend") | |
| if not info.get(k) | |
| ] | |
| if absent: | |
| sys.exit(f"{b['name']} records no {absent}: not reproducible") | |
| print(f"{len(runs)} benchmarks across sizes {sorted(sizes)}") | |
| PY | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| # always(), so the artifact is there to diagnose a failed content check | |
| # rather than only on the nights that pass it. | |
| if: always() | |
| with: | |
| name: benchmark-json | |
| path: benchmark.json | |
| retention-days: 90 | |
| if-no-files-found: error | |
| lammps-real: | |
| # Real-tier LAMMPS via conda-forge (ML-IAP unified path). Runs a | |
| # single-layer model; tests/integrations/README.md says why. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| defaults: | |
| run: | |
| shell: bash -leo pipefail {0} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up conda env with LAMMPS (ML-IAP + python) | |
| uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 | |
| with: | |
| miniforge-version: latest | |
| python-version: "3.11" | |
| activate-environment: lammps-mace | |
| - name: Install LAMMPS and mace | |
| run: | | |
| conda install -y -c conda-forge "lammps>=2024.06" python-devtools | |
| python -m pip install -U pip | |
| python -m pip install ".[dev, cueq]" | |
| python -c "import lammps; print('lammps import OK')" | |
| # The conda env activates only in a login shell, and this job's | |
| # `defaults.run.shell` does NOT reach steps inside a composite action -- | |
| # run-tests hardcodes `shell: bash`, so pytest was simply not found | |
| # (exit 127) and the real tier never ran a single test. Putting the env's | |
| # bin on PATH is what makes it visible to the action. | |
| - name: Expose the conda env to non-login shells | |
| run: | | |
| test -n "$CONDA_PREFIX" || { echo "conda env is not active"; exit 1; } | |
| test -x "$CONDA_PREFIX/bin/pytest" || { echo "no pytest in $CONDA_PREFIX"; exit 1; } | |
| echo "$CONDA_PREFIX/bin" >> "$GITHUB_PATH" | |
| - uses: ./.github/actions/run-tests | |
| with: | |
| tests: tests/integrations | |
| markers: bin_lammps | |
| require-caps: bin_lammps,cueq | |
| numprocs: "0" | |
| timeout: "1200" | |
| coverage-full: | |
| # THE project coverage number: full fixed CPU scope (network, cueq, polar, | |
| # les, torchsim, schedulefree), comparable night to night. The per-PR | |
| # coverage job measures only the deterministic PR slice by design. | |
| # | |
| # Sharded, one xdist worker per shard, because this is the ONLY job that | |
| # installs every extra at once: extensions-refresh runs polar, les, | |
| # torchsim and backends-cpu in separate jobs, and durations-refresh runs | |
| # the same paths with `dev` alone, so they all skip the heavy ones. On | |
| # 2026-08-06 the single job died at 87% with the runner itself killed | |
| # ("the runner has received a shutdown signal") while one worker was in a | |
| # cueq CPU-fallback polar case and the other was loading the omol | |
| # foundation model. Shards bound both the concurrent peak and how much a | |
| # single process accumulates; `[METRICS] mem/peak rss` in the log says | |
| # which of the two it was, should it come back. | |
| name: coverage-full (group ${{ matrix.split-group }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 150 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| split-group: [ 1, 2, 3 ] | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/setup-mace | |
| with: | |
| python-version: "3.12" | |
| extras: dev, cueq, torchsim, schedulefree, magnetic, wandb | |
| pip-packages: -r requirements/polar.txt -r requirements/les.txt | |
| - name: Cache foundation model downloads | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/mace | |
| key: mace-foundations-${{ hashFiles('mace/calculators/foundations_models.py') }} | |
| - name: Free up disk space | |
| run: | | |
| sudo rm -rf /usr/lib/jvm /usr/share/dotnet /usr/share/swift \ | |
| /usr/local/.ghcup /usr/local/lib/android \ | |
| /usr/local/share/chromium /opt/microsoft /opt/google | |
| - uses: ./.github/actions/run-tests | |
| with: | |
| tests: tests | |
| markers: not gpu and not benchmark | |
| require-caps: network,cueq,polar,les,torchsim,schedulefree,magnetic,wandb | |
| allow-network: "true" | |
| coverage: "data" | |
| splits: "3" | |
| group: ${{ matrix.split-group }} | |
| numprocs: "1" | |
| timeout: "1200" | |
| - name: Name this shard's coverage data | |
| run: mv .coverage ".coverage.${{ matrix.split-group }}" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-data-${{ matrix.split-group }} | |
| path: .coverage.${{ matrix.split-group }} | |
| retention-days: 7 | |
| # A dotfile, and upload-artifact skips hidden files by default -- | |
| # the same trap that silently emptied the durations upload. | |
| include-hidden-files: true | |
| coverage-report: | |
| # Each shard measures a third of the suite: this is where the nightly | |
| # coverage number is actually produced. Deliberately NOT `if: always()` -- | |
| # a report combined from a subset of the shards would read as the project | |
| # number while missing whatever the failed shard covered. | |
| needs: coverage-full | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # `coverage` alone, not the whole dev stack: combining and reporting | |
| # needs the data files and the sources, not an importable mace. | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.12" | |
| - run: python -m pip install "coverage[toml]" | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: coverage-data-* | |
| merge-multiple: true | |
| - name: Combine the shards | |
| run: | | |
| coverage combine | |
| coverage xml | |
| coverage html | |
| - name: Coverage summary | |
| run: | | |
| { | |
| echo '## Full-scope CPU coverage (nightly)' | |
| echo '```' | |
| coverage report --skip-covered --sort=cover | tail -40 | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload coverage to Coveralls | |
| uses: coverallsapp/github-action@v2 | |
| with: | |
| github-token: ${{ github.token }} | |
| file: coverage.xml | |
| format: cobertura | |
| fail-on-error: false | |
| # THE coverage gate. The overall percentage above stays informative in | |
| # both this job and the per-PR one; only the files listed below are | |
| # enforced. | |
| # | |
| # WHY ONLY THESE FILES. The rewrite's test-prerequisite bar is golden | |
| # characterization + end-to-end contracts + line-coverage floors on the | |
| # modules whose *behaviour* ports. There is deliberately no global | |
| # target: everything else in mace/ is pinned by the goldens in | |
| # tests/golden and the CLI/calculator contracts in tests/workflows, and | |
| # a global number would be a second, weaker statement about the same | |
| # code. These six are the pure-math and physics-glue modules that have | |
| # no golden standing in for them. | |
| # | |
| # MIGRATION INVARIANT. A floor protects the behaviour, not the legacy | |
| # file. When a capability moves to the new stack, its floor moves with | |
| # it to the corresponding mace_core/mace_torch module in the same | |
| # commit. Forgetting to is not silent: `--include` on a path that no | |
| # longer exists selects nothing and `coverage report` exits 1 with | |
| # "No data to report" (and tests/unit/test_ci_gates.py fails on the PR | |
| # that renames the file, a night earlier). | |
| # | |
| # WHY HERE AND NOWHERE ELSE. Two other places look plausible and are | |
| # both wrong. Inside coverage-full the check would run in a shard | |
| # holding one third of the suite (splits: 3), so a module exercised | |
| # entirely by tests that landed in another group would fail its own | |
| # floor. In ci-core's coverage job the denominator is different: it | |
| # measures tests/unit + tests/workflows only -- no extensions, backends, | |
| # integrations or network -- so the same percentage means a different | |
| # thing. | |
| # | |
| # That second one is not a theoretical objection, and the file that | |
| # settles it is mace/modules/utils.py. Measured on one host over the | |
| # whole selection: 76% on the PR slice, 87% on the full scope. Its floor | |
| # is 85. The same number against the same file therefore PASSES here and | |
| # FAILS in the PR job. The gap is 36 statements and it is not diffuse: | |
| # compute_forces_virials_magforces, compute_forces_magforces, the | |
| # get_outputs branches that dispatch to them, and | |
| # compute_total_charge_dipole_permuted -- reached from tests/extensions, | |
| # which the PR slice does not run. A floor is meaningless without naming | |
| # the selection, so: | |
| # | |
| # THE SELECTION THESE FLOORS ARE MEASURED UNDER is exactly | |
| # coverage-full's, combined over all three shards: | |
| # pytest tests -m "not gpu and not benchmark" | |
| # with extras dev,cueq,torchsim,schedulefree,magnetic plus | |
| # requirements/polar.txt and requirements/les.txt, and | |
| # MACE_CI_ALLOW_NETWORK=1. The magnetic extra is not optional here: the | |
| # modules/utils.py floor counts compute_forces_virials_magforces and | |
| # compute_forces_magforces, which only tests/extensions/magnetic reach, | |
| # and they are ~8 points of that file against 2 points of headroom. | |
| # | |
| # HEADROOM, as measured under that selection when the floors went in: | |
| # loss.py 100 (floor 90) data/utils.py 96 (85) | |
| # radial.py 100 (floor 90) neighborhood.py 100 (95) | |
| # modules/utils.py 87 (85) atomic_data.py 96 (85) | |
| # Two of those are ratchets rather than newly-cleared bars -- | |
| # neighborhood.py and atomic_data.py already sat above their floors | |
| # before the characterization tests landed, so they gate against | |
| # regression and nothing else. modules/utils.py is the tight one at two | |
| # points; the honest reading is that its floor is the one that will bite | |
| # first, which is the point of putting it here rather than rounding it | |
| # down to something comfortable. | |
| # | |
| # This step runs after `coverage combine`, which is also why | |
| # coverage-report is deliberately not `if: always()`: a gate must never | |
| # be computed from a subset of the shards. | |
| - name: Per-file coverage floors | |
| run: | | |
| status=0 | |
| echo '## Per-file coverage floors' >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| while read -r file floor; do | |
| case "$file" in ''|'#'*) continue ;; esac | |
| if report=$(coverage report --include="$file" --fail-under="$floor" 2>&1); then | |
| verdict='ok' | |
| else | |
| verdict='BELOW FLOOR' | |
| status=1 | |
| fi | |
| printf '%-34s >= %3s%% %s\n' "$file" "$floor" "$verdict" \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| printf '%s\n%s\n' "--- $file (floor ${floor}%): $verdict" "$report" | |
| done <<'FLOORS' | |
| mace/modules/loss.py 90 | |
| mace/modules/radial.py 90 | |
| mace/modules/utils.py 85 | |
| mace/data/utils.py 85 | |
| mace/data/neighborhood.py 95 | |
| mace/data/atomic_data.py 85 | |
| FLOORS | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| exit "$status" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| # always(), so the html report that explains a floor failure is | |
| # available on the run that failed. | |
| if: always() | |
| with: | |
| name: coverage-full | |
| path: | | |
| coverage.xml | |
| htmlcov/ | |
| retention-days: 90 | |
| durations-refresh: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 180 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/setup-mace | |
| with: | |
| python-version: "3.11" | |
| extras: dev | |
| - name: Free up disk space | |
| run: | | |
| sudo rm -rf /usr/lib/jvm /usr/share/dotnet /usr/share/swift \ | |
| /usr/local/.ghcup /usr/local/lib/android \ | |
| /usr/local/share/chromium /opt/microsoft /opt/google | |
| - uses: ./.github/actions/run-tests | |
| with: | |
| tests: tests | |
| allow-network: "true" | |
| store-durations: "true" | |
| timeout: "1200" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: test-durations | |
| path: tests/.test_durations | |
| retention-days: 30 | |
| # The path is a dotfile and upload-artifact skips hidden files by | |
| # default: without this the job stayed green while uploading nothing, | |
| # so the committed durations silently went stale. | |
| include-hidden-files: true |