Skip to content

ci(brain): check out before setup-zig so the Zig cache survives - #890

Open
gHashTag wants to merge 2 commits into
mainfrom
ci/zig-cache-after-checkout
Open

gHashTag wants to merge 2 commits into
mainfrom
ci/zig-cache-after-checkout

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Part of #844 and #851 (W4b).

What

Moves actions/checkout ahead of mlugg/setup-zig in the eight Zig jobs of brain-ci.yml.

Adds a gate so the old order cannot return: .github/workflows/zig-cache-order.yml runs tools/check_setup_zig_order.py. That check fails any job where a caching setup-zig step is followed by a checkout of the workspace. A checkout into a subdirectory passes, and so does use-cache: false. If no job uses setup-zig, or a workflow does not parse, it answers "cannot tell" (exit 2) rather than passing.

tools/test_check_setup_zig_order.py plants 17 cases and runs first. I wrote four mutants of the checker and the test catches each of them:

  • only an absent path: counts as the workspace;
  • every setup-zig counts as caching;
  • it never answers "cannot tell";
  • action names are matched case-sensitively.

On this branch the checker reads 123 workflows and finds 16 setup-zig jobs, none of them out of order. The commit message says 122 because I counted before adding the gate's own workflow, which has no setup-zig. Run on main's brain-ci.yml, it reports 8 of 8 jobs.

Why

setup-zig restores .zig-cache into $GITHUB_WORKSPACE. When checkout runs after it, the workspace is not a clone yet, so checkout logs Deleting the contents of '<workspace>' and removes the cache that was just restored. Read from the job logs of the last 25 brain-ci runs with speed zigcache --workflow brain-ci.yml: 238 restores, 238 hits, 0 used. Every hit was downloaded and then deleted before any step ran, and every job saved a new entry on the way out anyway.

That is not free storage. Live list at 01:05Z: brain-ci's entries are 161 of 178 entries in this repo's Actions cache, 8.43 GB of 10.83 GB (78%). The chipdb entries the FPGA jobs need compete for the same 10 GB limit (#844).

Measured

One workflow_dispatch run of this branch, 37248624658, all green, against the median of 25 earlier runs. work = restore done until the post step's size check, i.e. everything the job did after setup-zig:

job before, median of cold (s) this branch, warm (s)
Health 47.0 17.7
CLI Smoke 56.5 5.4
Metrics 53.6 30.8
Integration 34.3 0.6
Build Check 43.0 29.5
Unit basal-ganglia 41.2 21.0
Unit hslm 31.6 8.3
Unit intraparietal 30.7 6.1
Unit locus-coeruleus 34.4 11.2
Unit reticular-formation 41.0 9.6
Stress 64.4 30.3
sum 477.7 170.5

This is close to a best case: the branch differs from main only in this workflow, so the restored entries were built from the same sources.

Behaviour change: an unchanged test binary is not run again

Integration's 0.6 s is not faster tests. The tests did not run. In Zig 0.16 std/Build/Step/Run.zig, hasSideEffects() is false for .zig_test, and line 926 skips the run on a cache hit (if (!has_side_effects and try step.cacheHitAndWatch(&man))). With a warm cache, a test binary whose inputs did not change keeps its earlier pass and is not executed.

Two consequences:

  • A test that reads the clock or tri_rand (integration, stress) no longer gets a fresh run, and a fresh --seed, on every CI run when its code is unchanged.
  • A flaky test stops being re-sampled until its inputs change.

If that matters for a job, the opt-outs are per step has_side_effects = true (Run.zig line 89) or a file input (addFileInput, line 701), or use-cache: false for the whole job. This PR does not choose for you.

A warm hit only helps on the same CPU model

build.zig uses standardTargetOptions, so the target is the native CPU, and the CPU model and features are part of every artifact's hash. GitHub's ubuntu-latest runners are not one machine: a single run of this workflow spread its jobs over AMD EPYC 7763, 9V45, 9V74 and Intel Xeon Platinum 8573C.

Measured on the probe branch exp/zig-cpu-probe (this PR's change plus a step that prints /proc/cpuinfo's model name), two dispatches, 37249467137 then 37249915131, each job restoring the entry the first run saved. Saver and restorer matched by setup-zig key, not by job name:

restorer vs saver jobs .zig-cache directory work after setup-zig
same CPU model Health, Metrics, Build Check unchanged in 3 of 3 (0.70–0.71 GB) 0.6–2.9 s
different CPU model CLI Smoke (9V74 → 7763), Integration (9V45 → 9V74), Unit locus-coeruleus (9V45 → 9V74), Stress (9V45 → 7763) grew in 3 of 4: CLI Smoke 0.39 → 0.70 GB, Integration 0.23 → 0.32, locus-coeruleus 0.17 → 0.23. Stress did not (below) 7.6–30.3 s

The other four Unit legs also grew (0.17 → 0.23 GB), but they are not counted: they restored the locus-coeruleus leg's entry (shared key, below), so each built a test binary for its own region that was never in it.

Negative control: every one of those pairs ran on the same runner image version (20260927.320.1), and so did all 9 saver/restorer pairs found in 40 ci.yml runs, 7 of which rebuilt. The image is not what differs.

So the Health growth in the run above (0.39 → 0.71 GB on identical sources) fits a runner with a different CPU from the saver's. That one is an inference: the saving run on main did not print its CPU.

What this PR does not fix: a warm restore onto a different CPU model still recompiles. Pinning the target CPU (-Dcpu=… on each zig build) should make entries portable across runners. That is a separate experiment and an owner's call, because vsa_simd benefits from the native CPU's vector width.

Not measured

  • Stress did not grow. Stress restored across CPU models (9V45 → 7763) and its directory stayed at 228.8 MB, with the same 30.3 s of work as the run before. Not explained. One guess, not tested: its time is spent running the stress test, not compiling, and its artifacts for this CPU were already in the entry.
  • How many runs until the 2 GiB limit. setup-zig empties a directory over 2 GiB and saves a ~190 B entry. With growth of 0.06–0.32 GB per CPU change, some jobs will reach it; how many runs that takes is not measured.
  • Matrix legs share one key. All five Unit Tests legs get the same setup-zig key, so the first one saves and the rest are refused (4 of 5 in the run above). In the probe, all five legs restored what the locus-coeruleus leg saved, so four of them started without their own region's test binary. A cache-key: ${{ matrix.region }} would fix it. That is a follow-up, not part of this PR.
  • Not a controlled A/B. One run of this branch against a 25-run median; runner and load differ.

🤖 Generated with Claude Code

mlugg/setup-zig@v2 restores its cache into $GITHUB_WORKSPACE/.zig-cache.
In all eight Zig jobs of brain-ci.yml it ran before actions/checkout,
which finds no clone in the workspace and empties it ("Deleting the
contents of '/home/runner/work/trinity-fpga/trinity-fpga'"). Every
restored cache was deleted before a build step used it.

Measured with `speed zigcache` over the last 25 completed brain-ci runs:
238 restores, 0 used. 237 were hits that checkout then deleted; 1 was a
miss. Each job still paid about 1-1.8 s to restore and 1-2.4 s to save
26-71 MB. On 2026-10-05 those saves were 157 of the 179 Actions cache
entries, 7.95 GB of 11.04 GB, where they evict the chipdb (#844).

Check out first, in every Zig job. No step, condition or input changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

X Brain Health Check

Score: 100.0/100
Status: 🟢 HEALTHY
Threshold: 80/100

X Brain is above merge threshold

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🧪 Brain Stress Test Results

Score: not measured (no stress test prints a Score: line)
Status: 🟢 PASS
Threshold: 270/300

X Stress test passed

@gHashTag

gHashTag commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

The CPU-pinning experiment mentioned under "A warm hit only helps on the same CPU model" has now been measured once. Results are in #844 (comment 5986656054).

On branch exp/zig-dcpu, which is this PR plus -Dcpu=x86_64_v3, entries were reused across CPU models. Integration went from 9V74 to 7763 in 0.7 s and its cache did not grow. Each of the three other-CPU tri-compile jobs added about 45 MB in 4.9–8.3 s, against 23–27 s for a rebuild. That 45 MB is not explained.

This PR does not pin the CPU. Pinning changes what vsa_simd is compiled for, so that is a separate change and the owner's call.

🤖 Generated with Claude Code

… cache

mlugg/setup-zig restores into $GITHUB_WORKSPACE/.zig-cache, and an
actions/checkout of the workspace after it deletes the restored cache.
This is how brain-ci lost every hit (238 of 238 in 25 runs), and nothing
stops the old order from coming back.

tools/check_setup_zig_order.py reads every workflow with PyYAML and exits 1
for a caching setup-zig step followed by a checkout of the workspace. A
checkout into a subdirectory and `use-cache: false` both pass. It exits 2,
not 0, when no job uses setup-zig or a workflow does not parse.

tools/test_check_setup_zig_order.py plants 17 cases. Four mutants of the
checker are each caught. On this branch: 122 workflows, 16 setup-zig jobs,
0 findings. On main's brain-ci.yml: 8 of 8 jobs reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gHashTag added a commit that referenced this pull request Oct 5, 2026
The five brain-unit matrix legs got one setup-zig key, so the first leg
to finish saved and the other four were refused ("Unable to reserve
cache"): 88 of 110 saves in 25 runs. Every leg then restored that one
region's build and compiled its own test binary from nothing. setup-zig
documents `cache-key` for exactly this: include the matrix variables.

Stacked on #890, which makes the restored cache survive checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gHashTag

gHashTag commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Three checks on this PR are red. None of them comes from this change.

  • doc-refs and orphan-artefacts fail on files under research/arxiv_gft/ and research/GF*_2026-08-08.md, which this PR does not touch. The "Document references" and "Orphaned artefacts" workflows on main at 57de4a1 (after gft_mul: the product wire is 32 bits and GF-T32 needs 52 #510) fail the same way.
  • gates-can-fail fails because those two gates are already red on a clean tree, so it can test only 9 gates against a floor of 11.

#872 fixes the main side of this.

Follow-up: #891 gives each Unit Tests matrix leg its own setup-zig cache-key. It is stacked on this PR, so please merge this one first.

🤖 Generated with Claude Code

@dmitrii-f-t27
dmitrii-f-t27 force-pushed the ci/zig-cache-after-checkout branch from f75bbb9 to 966c838 Compare October 10, 2026 00:42
dmitrii-f-t27 pushed a commit that referenced this pull request Oct 10, 2026
The five brain-unit matrix legs got one setup-zig key, so the first leg
to finish saved and the other four were refused ("Unable to reserve
cache"): 88 of 110 saves in 25 runs. Every leg then restored that one
region's build and compiled its own test binary from nothing. setup-zig
documents `cache-key` for exactly this: include the matrix variables.

Stacked on #890, which makes the restored cache survive checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tri-claw

tri-claw Bot commented Oct 10, 2026

Copy link
Copy Markdown

Summary by t27-bees

This PR fixes a cache-thrashing bug in brain-ci.yml by moving actions/checkout before mlugg/setup-zig in all 8 Zig jobs, so the restored .zig-cache isn't deleted by checkout's workspace wipe. It also adds a gate workflow (zig-cache-order.yml) and a checker (tools/check_setup_zig_order.py + tests) that fails any job where a caching setup-zig step precedes a workspace checkout.

Worth a look

  • tools/check_setup_zig_order.py:100 — If any workflow under .github/workflows/ fails to parse (YAML error), the checker returns "cannot tell" (exit 2) even when all other workflows are clean and use setup-zig correctly. A syntax error in an unrelated workflow will fail the gate.

  • tools/check_setup_zig_order.py:43 — Any use-cache value other than the literal string "false" (including expressions like ${{ matrix.c }}) is treated as caching. A job that conditionally disables caching via an expression will be flagged as out-of-order even if the expression evaluates to false at runtime.

  • tools/check_setup_zig_order.py:56 — Only the last setup-zig step that restores cache is tracked (restored_at = i). If a job had multiple setup-zig steps (unlikely), a checkout between them would not be flagged for wiping the first restore.

  • .github/workflows/zig-cache-order.yml:13–18 — The gate only triggers on changes to workflow files or the checker itself. A bad order introduced via a reusable workflow or composite action referenced by a workflow would not trigger the gate until the referencing workflow file changes.

  • brain-ci.yml (all 8 jobs) — With the cache now working, Zig 0.16 skips re-running unchanged test binaries (hasSideEffects() is false for .zig_test). Integration and Stress tests that read the clock or use tri_rand will not get fresh runs/seeds on every CI execution until their inputs change. Opt-out requires per-step has_side_effects = true, a file input, or use-cache: false for the job.


Read: 4 files, +304 -40, the whole diff. Head 966c838. Model: openai-compatible/nvidia/nemotron-3-ultra-550b-a55b. Commands: @t27-bees review | summary | help | pause | resume. Free for every repository.

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.

1 participant