All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1m38s
standards workspaces build + test / aac-rs (pull_request) Successful in 32s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 34s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 32s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m48s
standards workspaces build + test / map-rs (pull_request) Successful in 30s
standards workspaces build + test / mesh-rs (pull_request) Successful in 26s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m17s
standards workspaces build + test / openpay (pull_request) Successful in 1m19s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m11s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m8s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m24s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m37s
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Successful in 1m5s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 2m44s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m15s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 3m17s
standards workspaces build + test / sense-rs (pull_request) Successful in 3m29s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 2m29s
This job has failed at ~1 minute on every run, so its reference comparisons have
never executed in CI. Two root causes, both now established from evidence — the
first from the job's actual log.
1. The Rust test could not link on the runner.
The job reaches its last step and fails there. From the job log (forge 16.0.5
exposes `GET /repos/{o}/{r}/actions/jobs/{id}/logs`):
rust-lld: error: undefined symbol: __isoc23_strtoull
>>> referenced by parser.cc ... in libort_sys-....rlib
rust-lld: error: undefined symbol:
std::__cxx11::basic_string<wchar_t>::_M_replace_cold(...)
`ort`'s `download-binaries` feature links a prebuilt static onnxruntime (1.28.0
for ort 2.0.0-rc.13) built against glibc >= 2.38 and GCC 13's libstdc++. The
runner's default job image, catthehacker/ubuntu:act-22.04, has glibc 2.35 and
GCC 11. All five undefined symbols were checked in both images: act-22.04 lacks
all five, act-24.04 (glibc 2.39, g++ 13) provides all five:
__isoc23_strtol __isoc23_strtoll __isoc23_strtoull glibc 2.38+
basic_string<char|wchar_t>::_M_replace_cold libstdc++ GCC 13+
The job now runs in catthehacker/ubuntu:act-24.04. No Rust or Cargo change, so
nothing here touches the files the landing chain edits. It ran in ~1 minute
because the runner's Python caches are warm; the fast failure was never early.
A correction: an earlier version of this commit replaced actions/setup-python
with uv, on the theory — reached by elimination, without the log — that
setup-python was the fault. The log refutes that: setup-python worked in every
run ("Create Python 3.11.16 folder"), and the old runs fail at the identical
link error. The workflow is back on setup-python exactly as main has it. My
elimination had verified the Python side in Linux containers but built the Rust
side natively on macOS, where the prebuilt is a Mac binary and glibc never
enters — so it cleared everything except the one step it never reproduced.
2. Encode the fixtures the way the decoder evaluates them.
With the link fixed, the comparisons would have exposed a real mismatch. The
Rust decoder opens every ONNX model via `ort_session::load_deterministic` — one
intra-op thread, one inter-op thread, full optimisation, on purpose. The fixture
generator used onnxruntime's defaults, which use every core; a convolution's
reduction is split across threads and its float output shifts.
For MLIC++ that is fatal. Traced symbol by symbol, encoder and decoder agree on
the first 416 of 12,288 symbols — bypass-coded ones included — then diverge
inside the first pass, before anything is fed back:
element (ch 3, row 4, col 0) scale CDF
encoder, all cores 0.78787208 16
decoder, single thread 0.78788924 17
scale_table[16] 0.78788900
One scale straddles one threshold and the arithmetic-coded stream desyncs:
11,841 symbols wrong, max|delta|=255. On main the garbage overflows the bypass
loop and panics at rans.rs:77; the landing chain bounds that loop, so there it
decodes silently to a wrong image.
Ruled out, each by experiment: the ML stack and numpy 2 -> 1.26 downgrade
(reproduced on linux/amd64); onnxruntime version drift (encoder pinned to the
decoder's 1.28.0 — still broken); the bypass decode (first 64 symbols match);
the hyper-latent path (decoded z bit-identical, FNV over 768 values);
`build_index` (scale_table[0] is exactly 0.11).
All 12 session sites now go through `_session`, mirroring load_deterministic.
MLIC++ max|delta| 255 -> 0; DCVC 1 -> 0. ELIC's residual max|delta|=1 does NOT
change, so it has another source (likely a final 8-bit rounding tie); it is
inside the +/-4 tolerance and is not claimed as fixed.
3. A correction to this PR's own guard.
The filter `_matches_reference` selects six tests, but
`snac_real_model_matches_reference` needs real pretrained SNAC weights this
synthetic job never builds, so it would skip on every run and keep the job red
with every decoder correct. I had verified the guard fires with no fixtures, but
never ran it with fixtures present. It is now excluded by name, with the reason,
so the guard stays strict for the five in scope.
Also corrects main()'s comment, which still described the old
"exit non-zero only if ALL failed" rule.
Verified on main's decoder with the job's exact commands: 5/5 fixtures (exit 0),
5 passed / 0 failed, guard clean. Falsified: removing only the threading pin
brings the MLIC++ panic back; deleting one in-scope fixture makes the guard fail
the step. The container change is verified by this PR's CI run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
105 lines
5.6 KiB
YAML
105 lines
5.6 KiB
YAML
name: WAI neural decoders end-to-end
|
|
|
|
# The float neural decoders (elic / mlicpp / glc / dcvc / snac) cannot make
|
|
# WAI's cross-architecture byte-equality promise — their ONNX synthesis is
|
|
# float, so the low bits differ by hardware (this is exactly why the
|
|
# integer-exact int_synth path exists, and why only it is in the byte-equality
|
|
# matrix). What they CAN be is *correct end-to-end* on a real machine: entropy
|
|
# decode → ONNX synthesis → image assembly, reproducing a reference within ORT
|
|
# float rounding (±4). This job proves that, with no committed model blobs:
|
|
# the committed tool builds minimal synthetic CompressAI-compatible models +
|
|
# bitstreams, and the Rust decoders reproduce each reference. It runs only when
|
|
# the neural decode code or the generator changes — not on every cargo class.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'wai/wai-rs/src/neural/**'
|
|
- 'wai/wai-rs/tests/neural_native_decode.rs'
|
|
- 'wai/tools/wai_validate_synthetic.py'
|
|
- '.github/workflows/wai-neural-e2e.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'wai/wai-rs/src/neural/**'
|
|
- 'wai/wai-rs/tests/neural_native_decode.rs'
|
|
- 'wai/tools/wai_validate_synthetic.py'
|
|
- '.github/workflows/wai-neural-e2e.yml'
|
|
|
|
jobs:
|
|
neural-e2e:
|
|
name: synthetic fixtures → Rust decode → reference (±4)
|
|
runs-on: linux
|
|
# The Rust decoders link `ort`, whose `download-binaries` feature pulls a
|
|
# prebuilt static onnxruntime (1.28.0 for ort 2.0.0-rc.13). That library was
|
|
# built against glibc >= 2.38 and GCC 13's libstdc++. The runner's default
|
|
# job image, catthehacker/ubuntu:act-22.04, has glibc 2.35 and GCC 11, so the
|
|
# test binary fails to LINK with five undefined symbols:
|
|
# __isoc23_strtol __isoc23_strtoll __isoc23_strtoull (glibc 2.38+)
|
|
# basic_string<char>::_M_replace_cold basic_string<wchar_t>::_M_replace_cold
|
|
# (libstdc++, GCC 13+)
|
|
# That link error is why this job failed at ~1 minute on every run: the
|
|
# Python steps pass on a warm cache and the job dies at its last step. Checked
|
|
# in both images: act-22.04 lacks all five, act-24.04 (glibc 2.39, g++ 13)
|
|
# provides all five.
|
|
container:
|
|
image: catthehacker/ubuntu:act-24.04
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
- name: ML stack (CPU torch + torchvision + compressai + onnxruntime)
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
# torch AND torchvision from the CPU index so their compiled ops match
|
|
# (compressai imports torchvision; a mismatched build fails at
|
|
# `torchvision::nms does not exist`).
|
|
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
|
# `onnx` (the exporter) is separate from `onnxruntime` (the decoder).
|
|
pip install compressai onnx onnxruntime numpy pillow
|
|
- name: Build the synthetic fixtures (elic / mlicpp / snac / dcvc / glc)
|
|
working-directory: wai
|
|
run: python tools/wai_validate_synthetic.py --size 256
|
|
# Bootstrap rustup: the docker runner image ships no Rust.
|
|
- name: Install Rust 1.98.0
|
|
run: |
|
|
export PATH="$HOME/.cargo/bin:$PATH"
|
|
command -v rustup >/dev/null 2>&1 || \
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
|
|
rustup toolchain install 1.98.0 --profile minimal
|
|
rustup default 1.98.0
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
- name: Each Rust decoder reproduces its reference end-to-end
|
|
working-directory: wai/wai-rs
|
|
# --no-default-features: the neural decoders need only `ort`, NOT the C
|
|
# codec chain (AV1/JXL/opus), so the job needs no system codec deps.
|
|
#
|
|
# The grep is the assertion, not decoration. Every `*_matches_reference`
|
|
# test opens with `if !wai.exists() || !ref.exists() || !model.exists() {
|
|
# eprintln!("skipping ..."); return; }` — so a missing fixture makes the
|
|
# test SKIP, and a skip is reported as a PASS. Verified by running this
|
|
# exact command on a machine with no /tmp fixtures: it prints
|
|
# "6 passed; 0 failed" while comparing nothing.
|
|
#
|
|
# That means a green `cargo test` here does NOT mean the decoders were
|
|
# checked against their references. If the Python fixture step ever
|
|
# succeeds while writing nothing, this job goes green having verified
|
|
# nothing at all — the failure mode is silent and looks like success.
|
|
# Failing on "skipping" is what makes the job's name true.
|
|
#
|
|
# One test is excluded by name, not tolerated as a skip:
|
|
# `snac_real_model_matches_reference` needs real pretrained SNAC weights,
|
|
# which this synthetic-fixture job never builds, so it would skip on every
|
|
# run and the guard would fail a job whose five in-scope decoders all
|
|
# match. Excluding it keeps the guard strict for everything in scope.
|
|
run: |
|
|
set -o pipefail
|
|
cargo test --no-default-features --features neural --test neural_native_decode \
|
|
-- _matches_reference --skip snac_real_model_matches_reference --nocapture \
|
|
2>&1 | tee /tmp/neural_e2e.log
|
|
if grep -q "skipping" /tmp/neural_e2e.log; then
|
|
echo "::error::a reference comparison was SKIPPED — its fixtures are missing, so this job verified nothing"
|
|
grep "skipping" /tmp/neural_e2e.log
|
|
exit 1
|
|
fi
|