CI/docs: gate macOS float32 on Ooura, and say which backend certified… #217
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: CI | |
| on: | |
| push: | |
| pull_request: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-test: | |
| name: ${{ matrix.name }} | |
| runs-on: ${{ matrix.os }} | |
| # The vDSP float32 leg is a known-flaky observation post, not a gate — see | |
| # the matrix comment below and tap/MuTap#31. | |
| continue-on-error: ${{ matrix.nonblocking }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: Linux GCC | |
| os: ubuntu-latest | |
| cc: gcc | |
| cxx: g++ | |
| werror: ON | |
| capi: ON | |
| nonblocking: false | |
| - name: Linux Clang | |
| os: ubuntu-latest | |
| cc: clang | |
| cxx: clang++ | |
| werror: ON | |
| capi: ON | |
| nonblocking: false | |
| # macOS gates on the OOURA float32 backend. Apple's vDSP is the | |
| # platform default and stays exercised by the leg below, but | |
| # vDSP_fft_zrip at N=2048 returns one of two bit-exact outputs for | |
| # identical input, drawn once per process (tap/MuTap#31), and the | |
| # residual suppressor amplifies that into a ~97 dB swing on the | |
| # G.168 §7 tone row. A single run of the float32 battery on vDSP is | |
| # therefore a ~72/28 coin flip rather than a gate, and it has been | |
| # reddening unrelated PRs since 2026-07-27. | |
| # | |
| # This job deliberately keeps the name "macOS AppleClang" so branch | |
| # protection referencing that check keeps working. Restore vDSP as | |
| # the gating backend — and delete the leg below — when #31 resolves. | |
| - name: macOS AppleClang | |
| os: macos-latest | |
| werror: ON | |
| capi: ON | |
| nonblocking: false | |
| extra_cmake: -DTAP_DSP_FFT_ACCELERATE=OFF | |
| # Non-gating: keeps the vDSP float32 draw visible (and measures how | |
| # often it bites) without blocking work that has nothing to do with it. | |
| - name: macOS AppleClang (vDSP float32, non-gating) | |
| os: macos-latest | |
| werror: ON | |
| capi: ON | |
| nonblocking: true | |
| # Warnings stay non-fatal on MSVC until /W4 output has been triaged | |
| # on a Windows runner (same policy as the sibling *Tap repos). | |
| # capi stays OFF on Windows: tools/capi carries no __declspec(dllexport) | |
| # (unlike DspTap's), so an MSVC build would link a DLL exporting nothing | |
| # — it would pass without gating anything. Turn this ON in the same | |
| # change that gives the C ABI an export decoration. | |
| - name: Windows MSVC | |
| os: windows-latest | |
| werror: OFF | |
| capi: OFF | |
| nonblocking: false | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| # Chip identity matters on Apple: #31's draw was observed on an M1 VM | |
| # and does NOT reproduce on Intel, so a bare "macOS" label is not enough | |
| # to attribute a past result. | |
| - name: Record host identity (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| sysctl -n machdep.cpu.brand_string | |
| sysctl -n hw.model | |
| sw_vers | |
| cc --version | head -2 | |
| - name: Configure | |
| env: | |
| CC: ${{ matrix.cc }} | |
| CXX: ${{ matrix.cxx }} | |
| run: > | |
| cmake -B build | |
| -DCMAKE_BUILD_TYPE=Release | |
| -DMUTAP_WERROR=${{ matrix.werror }} | |
| -DMUTAP_BUILD_CAPI=${{ matrix.capi }} | |
| ${{ matrix.extra_cmake }} | |
| # Record the backend actually built, so a log can never be misread about | |
| # which float32 configuration produced its numbers. | |
| - name: Record float32 FFT backend | |
| if: runner.os == 'macOS' | |
| run: grep '^TAP_DSP_FFT_ACCELERATE' build/CMakeCache.txt || echo 'TAP_DSP_FFT_ACCELERATE not in cache (default)' | |
| - name: Build | |
| run: cmake --build build --config Release -j 4 | |
| - name: Test | |
| run: ctest --test-dir build -C Release --output-on-failure | |
| # A single pass cannot certify a per-process bimodal outcome: on vDSP the | |
| # float32 rows sample a ~72/28 draw. Repeat the implicated rows on this | |
| # host so the log records how often the draw bites, rather than one | |
| # sample of it. Non-gating by virtue of the job's continue-on-error. | |
| - name: Repeat the float32 rows (vDSP draw rate) | |
| if: matrix.nonblocking | |
| run: > | |
| ctest --test-dir build -C Release --output-on-failure | |
| --repeat until-fail:20 | |
| -R 'itu_echo\.EchoStability<float>|g168_adapted\.ToneStability<float>|Float32Parity\.ToneRowWithNarrowbandGuard' | |
| sanitizers: | |
| name: ASan + UBSan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| - name: Configure | |
| env: | |
| CC: clang | |
| CXX: clang++ | |
| run: > | |
| cmake -B build | |
| -DCMAKE_BUILD_TYPE=RelWithDebInfo | |
| -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all" | |
| - name: Build | |
| run: cmake --build build -j 4 | |
| - name: Test | |
| env: | |
| UBSAN_OPTIONS: print_stacktrace=1 | |
| run: ctest --test-dir build --output-on-failure | |
| # Cross-compile for Arm Cortex-M55 (bare metal, newlib + semihosting) and | |
| # run the emulation-sized test subset on QEMU's MPS3 AN547 board model: | |
| # the float32 embedded profile the one-core/three-targets plan exists for, | |
| # on a 32-bit MCU-class target with no OS, no threads and no | |
| # double-precision FPU, including the float-tracks-double oracle check | |
| # and the PEM canceller's float closed-loop headline scenario. Platform | |
| # rig (startup, linker script, one-shot gtest harness) ported from | |
| # SampleRateTap. | |
| cortex-m55-qemu: | |
| name: Cortex-M55 cross (QEMU) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| - name: Install toolchain and QEMU | |
| run: > | |
| sudo apt-get update -q && | |
| sudo apt-get install -y -q gcc-arm-none-eabi qemu-system-arm | |
| - name: Configure | |
| run: > | |
| cmake -B build | |
| -DCMAKE_BUILD_TYPE=MinSizeRel | |
| -DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake | |
| - name: Build | |
| run: cmake --build build -j 4 | |
| # The default leg above builds the CMSIS-DSP Helium FFT backend, which is | |
| # ON by default on the bare-metal M55 profile (docs/optimization.md) — so | |
| # ctest exercises the vendored third_party/cmsis-dsp subset and its | |
| # Ooura-contract reconciliation on the full emulated battery. | |
| - name: Test under emulation (CMSIS Helium FFT — default) | |
| run: ctest --test-dir build --output-on-failure | |
| # Second leg keeps the Ooura float32 fallback alive: same emulated battery | |
| # with the backend forced OFF, so -DMUTAP_FFT_CMSIS=OFF cannot bitrot. | |
| - name: Configure (Ooura FFT fallback) | |
| run: > | |
| cmake -B build-ooura | |
| -DCMAKE_BUILD_TYPE=MinSizeRel | |
| -DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake | |
| -DMUTAP_FFT_CMSIS=OFF | |
| - name: Build (Ooura FFT fallback) | |
| run: cmake --build build-ooura -j 4 | |
| - name: Test under emulation (Ooura FFT fallback) | |
| run: ctest --test-dir build-ooura --output-on-failure | |
| # Cross-compile for Qualcomm Hexagon (hexagon-unknown-linux-musl, HVX | |
| # auto-vectorization on) and run the FULL test suite under qemu-hexagon | |
| # user-mode emulation: the third target of the one-core/three-targets | |
| # plan. Unlike the M55 leg this is a hosted Linux target — stock gtest | |
| # main, ctest discovery and exit codes all work unchanged; binaries link | |
| # statically so the emulator needs no sysroot. The VTCM/FastRPC | |
| # data-layout work needs the proprietary Hexagon SDK and hardware; this | |
| # job pins down what is checkable without them: the LLVM Hexagon backend | |
| # compiles the core (including HVX vectorized loops) and every numeric | |
| # regression test passes on the target ISA. | |
| hexagon-qemu: | |
| name: Hexagon cross (QEMU) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| env: | |
| TOOLCHAIN: clang+llvm-19.1.5-cross-hexagon-unknown-linux-musl | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| - name: Install QEMU | |
| run: > | |
| sudo apt-get update -q && | |
| sudo apt-get install -y -q qemu-user-static | |
| # The quic/toolchain_for_hexagon GitHub releases carry no assets of | |
| # their own — the release notes link to Codelinaro's artifactory, | |
| # which is where the archives actually live. | |
| - name: Download Hexagon toolchain | |
| run: > | |
| mkdir -p "$RUNNER_TEMP/hexagon-toolchain" && | |
| curl -fsSL | |
| "https://artifacts.codelinaro.org/artifactory/codelinaro-toolchain-for-hexagon/19.1.5/${TOOLCHAIN}.tar.zst" | |
| | tar --zstd -x -C "$RUNNER_TEMP/hexagon-toolchain" | |
| # The tarball's inner directory naming has varied across releases, so | |
| # find the driver and derive the root from it instead of hardcoding. | |
| - name: Locate toolchain root | |
| run: | | |
| ls "$RUNNER_TEMP/hexagon-toolchain" | |
| cxx=$(find "$RUNNER_TEMP/hexagon-toolchain" -maxdepth 5 \( -type f -o -type l \) \ | |
| \( -name 'hexagon-unknown-linux-musl-clang++' -o -name 'clang++' \) | sort | head -1) | |
| test -n "$cxx" || { echo "no clang++ found in the toolchain archive"; exit 1; } | |
| root=$(dirname "$(dirname "$cxx")") | |
| echo "found driver: $cxx" | |
| ls "$root/bin" | head -40 | |
| echo "HEXAGON_TOOLCHAIN_ROOT=$root" >> "$GITHUB_ENV" | |
| - name: Configure | |
| run: > | |
| cmake -B build | |
| -DCMAKE_BUILD_TYPE=Release | |
| -DCMAKE_TOOLCHAIN_FILE=cmake/hexagon-linux-musl.cmake | |
| -DHEXAGON_TOOLCHAIN_ROOT="$HEXAGON_TOOLCHAIN_ROOT" | |
| - name: Build | |
| run: cmake --build build -j 4 | |
| - name: Test under emulation | |
| run: ctest --test-dir build --output-on-failure | |
| # Keeps the benchmarks compiling and runnable; never a performance gate | |
| # (shared runners are noise — see bench/README.md). | |
| bench-smoke: | |
| name: Benchmark smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| - name: Configure | |
| run: > | |
| cmake -B build | |
| -DCMAKE_BUILD_TYPE=Release | |
| -DMUTAP_BUILD_BENCHMARKS=ON | |
| -DMUTAP_BUILD_TESTS=OFF | |
| - name: Build | |
| run: cmake --build build -j 4 | |
| - name: Run (smoke) | |
| run: ./build/bench/mutap_bench --benchmark_min_time=0.01s | |
| # The suppressor's pass-1 estimator has two bit-identical forms selected by | |
| # MUTAP_SUPPRESSOR_BRANCHLESS (branch-free on Arm Helium, branchy elsewhere; | |
| # see include/mutap/postfilter.h). No single build compiles both, so this job | |
| # compiles tests/branchless_parity_check.cpp once per macro value and diffs | |
| # its output fingerprint — the guard that the two forms cannot drift apart. | |
| branchless-parity: | |
| name: Suppressor branch-free/branchy parity | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| - name: Compile both forms and diff the fingerprint | |
| run: | | |
| set -e | |
| # Ooura is C: compile with gcc so g++ does not name-mangle rdft/rdft_f. | |
| gcc -O2 -c submodules/dsptap/third_party/ooura/fftsg.c -o /tmp/fftsg.o | |
| gcc -O2 -c submodules/dsptap/third_party/ooura/fftsg_float.c -o /tmp/fftsg_float.o | |
| for bl in 0 1; do | |
| g++ -std=c++20 -O2 -DMUTAP_SUPPRESSOR_BRANCHLESS=$bl -Iinclude -Isubmodules/dsptap/include \ | |
| tests/branchless_parity_check.cpp /tmp/fftsg.o /tmp/fftsg_float.o -o /tmp/parity_$bl | |
| /tmp/parity_$bl | tee /tmp/out_$bl | |
| done | |
| a=$(awk '{print $2}' /tmp/out_0); b=$(awk '{print $2}' /tmp/out_1) | |
| if [ "$a" != "$b" ]; then | |
| echo "::error::branchy ($a) and branch-free ($b) suppressor forms diverged"; exit 1 | |
| fi | |
| echo "both forms fingerprint $a — bit-identical" | |
| # Deterministic instruction-count ratchet (bench/README.md). Unlike the | |
| # wall-clock bench these counts are noise-free under QEMU's TCG plugin, so | |
| # a hard two-sided gate (+-3%) is safe on shared runners. Baselines live | |
| # in bench/baselines.json and are seeded by running scripts/icount.py with | |
| # --update once per target (see bench/README.md); until a target is seeded | |
| # this job reports each scenario's count and fails with "NO BASELINE". | |
| icount-ratchet: | |
| name: Instruction-count ratchet | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| env: | |
| # Commit the tag pointed at when pinned (tags are movable; commit SHAs | |
| # are not); the header's digest is verified on download. This SHA is | |
| # QEMU v8.2.2, matching ubuntu-latest's qemu-system-arm (plugin API v2). | |
| QEMU_PLUGIN_HEADER_URL: https://raw.githubusercontent.com/qemu/qemu/11aa0b1ff115b86160c4d37e7c37e6a6b13b77ea/include/qemu/qemu-plugin.h | |
| QEMU_PLUGIN_HEADER_SHA256: "c53a2af163e80e3f4bc6c60dbdfc84003db329d757e37cd8a16a77e1d82606ff" | |
| # Neither Debian's nor CodeLinaro's qemu-hexagon enables TCG plugins, so | |
| # the Hexagon leg builds its own qemu-hexagon from this pinned release | |
| # (matching the plugin header version). Digest verified on download. | |
| QEMU_SRC_URL: https://download.qemu.org/qemu-8.2.2.tar.xz | |
| QEMU_SRC_SHA256: "847346c1b82c1a54b2c38f6edbd85549edeb17430b7d4d3da12620e2962bc4f3" | |
| TOOLCHAIN: clang+llvm-19.1.5-cross-hexagon-unknown-linux-musl | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| - name: Install toolchains and QEMU | |
| run: > | |
| sudo apt-get update -q && | |
| sudo apt-get install -y -q gcc-arm-none-eabi qemu-system-arm | |
| libglib2.0-dev pkg-config ninja-build meson flex bison | |
| - name: Build counting plugin | |
| run: | | |
| curl -sfLo /tmp/qemu-plugin.h "$QEMU_PLUGIN_HEADER_URL" | |
| actual=$(sha256sum /tmp/qemu-plugin.h | cut -d' ' -f1) | |
| if [ "$actual" != "$QEMU_PLUGIN_HEADER_SHA256" ]; then | |
| echo "::error::qemu-plugin.h checksum mismatch"; exit 1 | |
| fi | |
| gcc -shared -fPIC $(pkg-config --cflags glib-2.0) -I/tmp \ | |
| -o /tmp/libinsncount.so tools/qemu_insn_plugin/insn_count.c | |
| # Release (-O2) M55 workloads, matching how baselines are recorded. No | |
| # -DMUTAP_FFT_CMSIS here: the M55 profile defaults it ON, so the ratchet | |
| # gates the deployed CMSIS-DSP Helium FFT (bench/baselines.json m55). | |
| - name: Build M55 workloads | |
| run: > | |
| cmake -B build-m55 | |
| -DCMAKE_BUILD_TYPE=Release | |
| -DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake | |
| -DMUTAP_BUILD_TESTS=OFF | |
| -DMUTAP_BUILD_ICOUNT_BENCH=ON | |
| && cmake --build build-m55 -j 4 | |
| # Each target's ratchet runs even if an earlier one failed (each | |
| # target's numbers are independent evidence, and a legitimate baseline | |
| # move should surface all at once, not one CI round-trip at a time). | |
| # The job still fails overall if any target regressed or is unseeded. | |
| - name: Ratchet M55 | |
| if: ${{ !cancelled() }} | |
| run: > | |
| python3 scripts/icount.py --target m55 | |
| --build-dir build-m55 --plugin /tmp/libinsncount.so | |
| # qemu-system-arm's plugins cover M55; qemu-hexagon must be built with | |
| # --enable-plugins (linux-user only, ~4 min, cached thereafter). | |
| - name: Cache plugin-enabled qemu-hexagon | |
| if: ${{ !cancelled() }} | |
| id: qemu-hex | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: ~/qemu-hexagon-plugins | |
| key: qemu-hexagon-plugins-${{ env.QEMU_SRC_SHA256 }}-1 | |
| - name: Build plugin-enabled qemu-hexagon | |
| if: ${{ !cancelled() && steps.qemu-hex.outputs.cache-hit != 'true' }} | |
| run: | | |
| curl -sfLo /tmp/qemu-src.tar.xz "$QEMU_SRC_URL" | |
| actual=$(sha256sum /tmp/qemu-src.tar.xz | cut -d' ' -f1) | |
| if [ "$actual" != "$QEMU_SRC_SHA256" ]; then | |
| echo "::error::qemu source checksum mismatch"; exit 1 | |
| fi | |
| tar -xJf /tmp/qemu-src.tar.xz -C /tmp | |
| cd /tmp/qemu-*/ | |
| ./configure --target-list=hexagon-linux-user --enable-plugins \ | |
| --disable-docs --disable-tools --disable-system | |
| ninja -C build qemu-hexagon | |
| mkdir -p ~/qemu-hexagon-plugins | |
| cp build/qemu-hexagon ~/qemu-hexagon-plugins/ | |
| - name: Download Hexagon toolchain | |
| if: ${{ !cancelled() }} | |
| run: > | |
| mkdir -p "$RUNNER_TEMP/hexagon-toolchain" && | |
| curl -fsSL | |
| "https://artifacts.codelinaro.org/artifactory/codelinaro-toolchain-for-hexagon/19.1.5/${TOOLCHAIN}.tar.zst" | |
| | tar --zstd -x -C "$RUNNER_TEMP/hexagon-toolchain" | |
| - name: Locate toolchain root | |
| if: ${{ !cancelled() }} | |
| run: | | |
| cxx=$(find "$RUNNER_TEMP/hexagon-toolchain" -maxdepth 5 \( -type f -o -type l \) \ | |
| \( -name 'hexagon-unknown-linux-musl-clang++' -o -name 'clang++' \) | sort | head -1) | |
| test -n "$cxx" || { echo "no clang++ found in the toolchain archive"; exit 1; } | |
| echo "HEXAGON_TOOLCHAIN_ROOT=$(dirname "$(dirname "$cxx")")" >> "$GITHUB_ENV" | |
| - name: Build Hexagon workloads | |
| if: ${{ !cancelled() }} | |
| run: > | |
| cmake -B build-hex | |
| -DCMAKE_BUILD_TYPE=Release | |
| -DCMAKE_TOOLCHAIN_FILE=cmake/hexagon-linux-musl.cmake | |
| -DHEXAGON_TOOLCHAIN_ROOT="$HEXAGON_TOOLCHAIN_ROOT" | |
| -DMUTAP_BUILD_TESTS=OFF | |
| -DMUTAP_BUILD_ICOUNT_BENCH=ON | |
| && cmake --build build-hex -j 4 | |
| - name: Ratchet Hexagon | |
| if: ${{ !cancelled() }} | |
| run: | | |
| export PATH="$HOME/qemu-hexagon-plugins:$PATH" | |
| # qemu-hexagon exits 1 here with or without plugin support (no guest | |
| # binary given), so probe by the error text, not the exit code. | |
| if qemu-hexagon -plugin help 2>&1 | grep -q "unknown option"; then | |
| echo "::error::built qemu-hexagon lacks plugin support"; exit 1 | |
| fi | |
| python3 scripts/icount.py --target hexagon \ | |
| --build-dir build-hex --plugin /tmp/libinsncount.so | |
| clang-format: | |
| name: clang-format | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| submodules: recursive | |
| # Run the TapHouse pre-commit hook — the exact same pinned clang-format | |
| # developers get from `pre-commit install` (.pre-commit-config.yaml). CI | |
| # and local therefore share one clang-format version by construction, and | |
| # cover the same files (all C/C++ minus third_party and generated tables), | |
| # so a green local commit can't fail here. pipx is preinstalled on the | |
| # runner; the pinned hook is fetched and cached on first use. | |
| - name: Check formatting (pre-commit) | |
| run: | | |
| pipx install pre-commit | |
| pre-commit run --all-files --show-diff-on-failure |