Skip to content

ci: make every workflow run again - #124

Draft
kszucs wants to merge 6 commits into
mainfrom
ci
Draft

ci: make every workflow run again#124
kszucs wants to merge 6 commits into
mainfrom
ci

Conversation

@kszucs

@kszucs kszucs commented Sep 7, 2026

Copy link
Copy Markdown
Owner

ci: make every workflow run again

No CI run has passed since 2026-05-11, and the five failures had five
unrelated causes -- each verified against the runs for cb296c8, not
inferred from the last green build.

Tests and Benchmarks were compiling the tree in one unit. One pytest
selection is one compilation unit, so pytest -v over the whole tree was a
single 2,187-case build: 1,802 s on macOS (the --mojo-timeout default) and
OOM-killed on Linux; the benchmark suite, at -O3, died the same way. Both are
split by directory now. Almost all of a unit is elaborating marrow rather than
the test bodies, so a directory costs about what one file costs --
marrow/tests measures 121 s and 5.5 GB peak, well inside a free runner --
and splitting finer would only pay for another elaboration. The six test
suites run concurrently; golden/ and python/ are the only two that build
libmarrow.so, which every job used to pay 13 minutes for. Benchmark results
from several selections now merge into one per-commit envelope instead of the
last one overwriting the rest.

The binary-size gate could not pass anywhere but the machine that recorded
it.
The same source builds 0.5-1.6% larger on a macOS runner than on a
developer's Mac, which trips a 0.5% threshold on its own: the job reported six
REGRESSIONs against a baseline re-recorded that same day. It now measures the
commit under test and the commit it descends from on one runner and compares
those, caching each measurement by SHA so a run normally builds once.
baseline.json keeps the gate list and threshold, and its numbers stay the
local reference.

The docs job asked a CPU-only runner to compile a GPU listing.
DeviceContext() is instantiated against the host's accelerator, so
guide/gpu.qmd's two blocks failed with function instantiation failed on
ubuntu-latest no matter how correct they were. They are {.mojo .gpu} now,
compiled where an accelerator exists and skipped where none does.

Integration hit Boost.Process. Arrow C++ needs explicit per-release
support for it (apache/arrow#43746, #45121) and apache-arrow-24.0.0 predates
conda-forge's 1.91, so libarrow_testing.so failed to link. Boost is pinned
below 1.88; the lock change is confined to the integration environment.

Also: wheels.yml has not run since 2026-05-11 and is manual-only until a leg
passes on a real run, with what is known to be wrong recorded in it; the mojo
pin in python/pyproject.toml was four months stale; actions and setup-pixi
are on current majors; MODULAR_HOME was pointing at a directory nothing
reads; and every workflow now has a concurrency group and a timeout.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com


🤖 Generated with Claude Code

kszucs and others added 6 commits September 7, 2026 22:58
No CI run has passed since 2026-05-11, and the five failures had five
unrelated causes -- each verified against the runs for cb296c8, not
inferred from the last green build.

**Tests and Benchmarks were compiling the tree in one unit.** One pytest
selection is one compilation unit, so `pytest -v` over the whole tree was a
single 2,187-case build: 1,802 s on macOS (the `--mojo-timeout` default) and
OOM-killed on Linux; the benchmark suite, at -O3, died the same way. Both are
split by directory now. Almost all of a unit is elaborating marrow rather than
the test bodies, so a directory costs about what one file costs --
`marrow/tests` measures 121 s and 5.5 GB peak, well inside a free runner --
and splitting finer would only pay for another elaboration. The six test
suites run concurrently; `golden/` and `python/` are the only two that build
`libmarrow.so`, which every job used to pay 13 minutes for. Benchmark results
from several selections now merge into one per-commit envelope instead of the
last one overwriting the rest.

**The binary-size gate could not pass anywhere but the machine that recorded
it.** The same source builds 0.5-1.6% larger on a macOS runner than on a
developer's Mac, which trips a 0.5% threshold on its own: the job reported six
REGRESSIONs against a baseline re-recorded that same day. It now measures the
commit under test and the commit it descends from on one runner and compares
those, caching each measurement by SHA so a run normally builds once.
`baseline.json` keeps the gate list and threshold, and its numbers stay the
local reference.

**The docs job asked a CPU-only runner to compile a GPU listing.**
`DeviceContext()` is instantiated against the host's accelerator, so
`guide/gpu.qmd`'s two blocks failed with `function instantiation failed` on
ubuntu-latest no matter how correct they were. They are `{.mojo .gpu}` now,
compiled where an accelerator exists and skipped where none does.

**Integration hit Boost.Process.** Arrow C++ needs explicit per-release
support for it (apache/arrow#43746, #45121) and apache-arrow-24.0.0 predates
conda-forge's 1.91, so `libarrow_testing.so` failed to link. Boost is pinned
below 1.88; the lock change is confined to the integration environment.

Also: `wheels.yml` has not run since 2026-05-11 and is manual-only until a leg
passes on a real run, with what is known to be wrong recorded in it; the mojo
pin in `python/pyproject.toml` was four months stale; actions and setup-pixi
are on current majors; `MODULAR_HOME` was pointing at a directory nothing
reads; and every workflow now has a concurrency group and a timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splitting the suites let CI reach code it had not compiled since 2026-05-11,
and four real faults were waiting there. None of them is a CI-config problem.

**`marrow/parquet/tests/test_writer.mojo` aborted the whole test binary.**
Four columns were built as `pa.array(numpy.arange(n), ...)`; PyArrow wraps a
numpy buffer zero-copy at 16-byte alignment and `Buffer.from_foreign` asserts
64, so `_to_marrow` hit `Assert Error: Buffer pointer must be 64-byte aligned`
and took the shared driver with it -- reporting all 54 cases as failed with no
indication which one did it, which is why the selection's own output could not
name it and a per-file sweep had to. This file already used the list-backed
idiom elsewhere and documents the hazard in a comment; now it uses it
throughout. macOS survived on alignment luck. The underlying gap is marrow's --
the C Data Interface calls alignment "recommended, but not required" -- and is
recorded in the backlog rather than fixed here.

**`_ldd_deps` ran `ldd` with `check=True`.** One `test_compile.py` case builds
a 4-byte stub library; `otool` reports "not an object file" and exits 0, so
macOS never noticed, while `ldd` exits 1 and raised. An inspector that cannot
read a file now warns and reports no dependencies, which is the judgement
`stage_codec_libs` already makes about a codec that is not installed.

**`marrow/expr/tests` does not fit a free runner**, and measurement says why:
17.5 GB peak, of which `test_optimizer.mojo` alone is 15.4 GB. Peak follows the
heaviest instantiations, not the file count -- 6 of the 13 files peak the same
17.3 GB -- so the directory is split by lane, `test_optimizer.mojo` is its own
job, and the Linux runners get a 24 GB swapfile on /mnt, because one file is
the floor and that floor is above 16 GB.

**The Benchmarks job was benchmarking by naming directories**, and `--benchmark`
adds the bench files rather than replacing the tests -- so it also compiled and
ran every test unit, including `marrow/expr`. It names `bench_*` files now, and
gets the same swap and a 3,600 s compile budget; its own -O3 unit had been
timing out at the 1,800 s default.

Also: the Linux asan job is gone rather than silently `if: false` -- the same
selection is 5m15s on macOS and had not finished after 60 minutes on Linux,
measured twice, and the cases are identical on both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`DictionaryArray.__eq__` compared decoded values as `self[i].value() !=
other[i].value()` -- two `DynScalar`s. Comparing *elements* of an erased
container is exactly what `ArrayData.__eq__`'s docstring warns about two
thousand lines up: `DynArray.__eq__` and the nested arrays' `__eq__` become
mutually recursive at instantiation and the elaborator never resolves it,
parking at 0% CPU with no diagnostic.

It did not fire from `marrow/tests/test_arrays.mojo`, which is how it survived.
Reached from `test_ipc.mojo` it deadlocked `mojo build`: 6m40s of wall clock
for 11.9s of user CPU, on Linux on both architectures, while macOS compiled it
-- so CI could not see it either, because no Linux suite has run since
2026-05-11. `test_ipc.mojo` was killed at the 1,800s timeout with all 37 cases
reported failed and nothing to say which two were responsible; a per-file then
per-case sweep named them as the two dictionary round-trips, and deleting just
the `got == expected` assertion made the case compile in 22s.

Resolving the values dtype to a concrete array type first -- bool directly,
binary-like and primitive through their `dispatch_*` -- means no typed `__eq__`
is ever named through an erased one. Decoded-value semantics are unchanged, so
`eq_permuted_dictionary` (logical equality, arrow-rs) and `eq_respects_offset`
still hold. A dictionary over a *nested* value type has no typed leaf to
compare and answers on the representation instead, which reads a permuted one
as unequal rather than hanging; Arrow allows that shape and nothing in marrow
builds it.

`test_ipc.mojo` now compiles and passes in 46s on Linux, and `marrow/tests` is
575 passed there. macOS: 734 passed across `marrow/tests` and the kernel suites
that use dictionaries.

Found with a native arm64 Linux container, added here alongside: `Dockerfile.x86`
becomes `Dockerfile.linux` with the hardcoded platform dropped -- compose.yaml
sets it per service -- and `pixi.toml` declares `linux-aarch64`, for which the
pinned mojo and MAX both publish builds. On an Apple Silicon machine `arm64` is
native and `x86` needs Rosetta: Docker's default qemu CPU stops at SSE4.2, and
the linux-64 mojo binary dies with `Illegal instruction` before printing its
version. The two are not interchangeable -- this hang reproduces on arm64,
`test_scalars.mojo`'s crash does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**Grouping bench files is worse, not better.** Five kernel bench files as one
-O3 unit did not finish compiling in 3,600 s; the same five as five units
compile in 165 s in total (20 + 10 + 86 + 12 + 37, measured on Linux). The
combined unit blows up superlinearly, so paying for one elaboration per file is
the cheap option -- the opposite of the tests, where one directory per unit is
what fits. Benchmarks now run one file at a time.

**`pytest-benchmark` was pinned `>=5.2.3,<6` and the harness cannot take 5.3.**
`conftest.py` constructs a `BenchmarkFixture` directly with pytest-benchmark's
own keyword API, and 5.3.0 added required arguments to that constructor: every
benchmark fails with `BenchmarkFixture.__init__() missing 2 required positional
arguments` before anything is compiled. Both versions satisfied `<6`, so which
one you got depended on the solve -- adding `linux-aarch64` picked 5.3.0 there
and 5.2.3 on the two platforms CI runs, which is the only reason CI never saw
it. Pinned `<5.3`; all sixteen entries in the lock now agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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