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. # One run per branch at a time. A push supersedes the run before it. # # There is one `macos` runner and it is the dev Mac, so every macOS job in this # repo queues on it. With no concurrency group, a session pushing four versions # of a branch queued four complete sets of runs and every one of them had to # drain: on 2026-10-05 a PR's 21 contexts settled none in 70 minutes behind a # backlog of 25 runs, and the queue grew faster than it cleared. Only the newest # commit's verdict is wanted, so the older runs are cancelled. # # Verified on this forge (Forgejo 16.0.5) in `ci-test` before being added here: # the key parses, and a newer push moves an older run to `cancelled` even while # it is still queued, which is the case that matters. # # `cancel-in-progress: true` also applies to `main`. That is deliberate and # safe here: main advances by merge commits whose tree contains the commits # before them, so the newer run covers the older one's changes. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: pull_request: paths: - 'wai/wai-rs/src/neural/**' - 'wai/wai-rs/tests/neural_native_decode.rs' - 'wai/tools/wai_validate_synthetic.py' - 'wai/tools/requirements-neural-e2e.txt' - 'wai/tools/torch-cpu-overrides.txt' - '.github/workflows/wai-neural-e2e.yml' schedule: - cron: '12 4 * * *' jobs: # Does this pull request's own diff touch the paths below? This forge matches a # workflow's `paths:` against the push, so a merge of main into a branch fires the # workflow whatever the pull request changes; every job here waits on this answer. # A push to main, a schedule, or anything the script cannot determine answers # `true` (scripts/pr-touches-paths.py). relevant: name: does this change touch it runs-on: linux-arm64 outputs: run: ${{ steps.gate.outputs.run }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - id: gate run: | set -euo pipefail r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-neural-e2e.yml \ "${{ github.event.pull_request.base.sha }}" \ "${{ github.event.pull_request.head.sha }}") echo "this change touches wai-neural-e2e: $r" echo "run=$r" >> "$GITHUB_OUTPUT" neural-e2e: needs: relevant if: needs.relevant.outputs.run == 'true' 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::_M_replace_cold basic_string::_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) # Every package pinned by SHA-256 (wai/tools/requirements-neural-e2e.txt, generated # from requirements-neural-e2e.in and torch-cpu-overrides.txt by its header's # command). torch AND torchvision are the CPU builds, so their compiled ops match # (compressai imports torchvision; a mismatched build fails at # `torchvision::nms does not exist`); the file names the CPU wheel index. run: python -m pip install --require-hashes -r wai/tools/requirements-neural-e2e.txt - 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