[ci] mex-build: build the MNN/OpenCL MATLAB MEX on Windows too #122
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: | |
| branches: [main, opencl] | |
| # Tag pushes (v*) also run the workflow so release bundles get a clean, | |
| # hash-less version: the Package steps key off GITHUB_REF_TYPE=tag and name | |
| # artifacts siamize-<version>-<label> (vs <version>-<sha> on branch pushes). | |
| # NOTE: GitHub reads this workflow from the *tagged commit*, so a tag only | |
| # triggers if the tag points at a commit that already contains this filter. | |
| tags: ['v*'] | |
| pull_request: | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| cpp-build: | |
| name: build (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-22.04 | |
| binary: build/siamize | |
| deps_check: | | |
| echo "--- ldd ---" | |
| ldd build/siamize | |
| echo "--- direct (NEEDED) deps ---" | |
| readelf -d build/siamize | grep NEEDED | |
| - os: macos-14 | |
| binary: build/siamize | |
| deps_check: | | |
| echo "--- otool -L (linked dylibs) ---" | |
| otool -L build/siamize | |
| - os: windows-2022 | |
| # MSVC multi-config generator drops the binary under Release/. | |
| binary: build/Release/siamize.exe | |
| deps_check: | | |
| ls -la build/Release/ | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install OpenMP (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| brew install libomp | |
| echo "OpenMP_ROOT=$(brew --prefix libomp)" >> "$GITHUB_ENV" | |
| - name: Fetch C++ deps | |
| run: scripts/fetch_deps.sh | |
| - name: Configure | |
| # -DSIAMIZE_STATIC_LINK=ON is the default but we pin it for clarity: | |
| # statically links libstdc++/libgcc/libgomp (Linux), libomp (macOS), | |
| # /MT static CRT (Windows). Only libonnxruntime remains dynamic. | |
| run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DSIAMIZE_STATIC_LINK=ON | |
| - name: Build | |
| run: cmake --build build --config Release --parallel | |
| - name: Inspect runtime deps | |
| run: ${{ matrix.deps_check }} | |
| - name: Smoke-run --help | |
| run: ${{ matrix.binary }} --help | |
| - name: Package | |
| shell: bash | |
| run: | | |
| BASE=$(cat VERSION 2>/dev/null || echo "0.0.0") | |
| # Tag push -> use tag verbatim (strip leading 'v' for the bundle). | |
| # Branch push -> base version + short SHA so successive main builds | |
| # are uniquely-named in the artifact list. | |
| if [ "${GITHUB_REF_TYPE}" = "tag" ]; then | |
| VER="${GITHUB_REF_NAME#v}" | |
| else | |
| VER="${BASE}-${GITHUB_SHA:0:7}" | |
| fi | |
| case "${{ runner.os }}" in | |
| Linux) PLAT=linux-x86_64 ;; | |
| macOS) PLAT=macos ;; | |
| Windows) PLAT=windows-x64 ;; | |
| esac | |
| BUNDLE="siamize-${VER}-${PLAT}-cpu" | |
| scripts/package.sh cpu "$BUNDLE" | |
| echo "BUNDLE=$BUNDLE" >> "$GITHUB_ENV" | |
| - name: Upload bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BUNDLE }} | |
| path: dist/${{ env.BUNDLE }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| cpp-build-cuda: | |
| # Builds siamize with the CUDA Execution Provider enabled, then runs | |
| # single-fold inference. GitHub runners have no NVIDIA GPU, so the | |
| # run exercises CUDA EP probe -> CPU fallback. The fp16 weight is | |
| # fetched fresh each run from the public NeuroJSON URL that | |
| # weights.cpp's default_weights_url() also uses. | |
| name: build+run cuda (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-22.04 | |
| binary: build/siamize | |
| - os: windows-2022 | |
| binary: build/Release/siamize.exe | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Fetch C++ deps (GPU ORT prebuilt) | |
| env: | |
| ORT_BUILD: gpu | |
| run: scripts/fetch_deps.sh | |
| - name: Fetch fold_0 weight | |
| # Pulls the same dynamic-shape ONNX export that weights.cpp's | |
| # default_weights_url() serves to end users, so CI exercises | |
| # the canonical inference path (variable patch sizes via | |
| # dynamic_axes={D,H,W}). ORT's CPU EP fits the default | |
| # 256x256x192 patch at ~11-12 GB on the ~14 GB GHA cgroup with | |
| # --no-arena, so the run step below keeps the default patch | |
| # (8 tiles) -- no --lowmem / -P needed (unlike the MNN-CPU job, | |
| # whose workspace allocation forces 192x192x128 / 27 tiles). | |
| run: | | |
| URL='https://neurojson.org/io/stat.cgi?action=get&db=siam_v03&doc=dynshape&size=95360591&file=fold_0_fp16.onnx.gz' | |
| mkdir -p models | |
| curl -fsSL "$URL" -o models/fold_0_fp16.onnx.gz | |
| gunzip -f models/fold_0_fp16.onnx.gz | |
| ls -la models/ | |
| - name: Configure (CUDA EP) | |
| run: | | |
| cmake -S . -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DSIAMIZE_STATIC_LINK=ON \ | |
| -DSIAMIZE_GPU=cuda | |
| - name: Build | |
| run: cmake --build build --config Release --parallel | |
| - name: Run single-fold inference (expect CPU fallback on GPU-less runner) | |
| # GitHub runners have no NVIDIA GPU, so siamize falls back to | |
| # CPU. Dynamic-shape ONNX + default 256x256x192 + --no-arena | |
| # peaks ~11-12 GB on the 14 GB GHA cgroup (the historical | |
| # OOM was a stale arena-ignore bug, fixed in 6d052ed). 8 | |
| # tiles, end-to-end ~500-550 s. | |
| timeout-minutes: 30 | |
| run: | | |
| ${{ matrix.binary }} \ | |
| --no-arena \ | |
| -i tests/sub-01_T1w.nii.gz \ | |
| -o pred_ci.nii.gz \ | |
| -M models/fold_0_fp16.onnx \ | |
| -c auto 2>&1 | tee siamize.log | |
| - name: Verify CPU fallback happened | |
| run: | | |
| if grep -Eq "\[cuda\][[:space:]]+unavailable" siamize.log; then | |
| echo "OK: CUDA EP correctly fell back to CPU." | |
| else | |
| echo "FAIL: expected '[cuda] unavailable' fallback message; got:" | |
| cat siamize.log | |
| exit 1 | |
| fi | |
| - name: Verify output produced | |
| run: | | |
| test -s pred_ci.nii.gz || { echo "FAIL: empty/missing output"; exit 1; } | |
| ls -lh pred_ci.nii.gz | |
| - name: Package | |
| shell: bash | |
| run: | | |
| BASE=$(cat VERSION 2>/dev/null || echo "0.0.0") | |
| if [ "${GITHUB_REF_TYPE}" = "tag" ]; then | |
| VER="${GITHUB_REF_NAME#v}" | |
| else | |
| VER="${BASE}-${GITHUB_SHA:0:7}" | |
| fi | |
| case "${{ runner.os }}" in | |
| Linux) PLAT=linux-x86_64 ;; | |
| Windows) PLAT=windows-x64 ;; | |
| esac | |
| BUNDLE="siamize-${VER}-${PLAT}-cuda" | |
| scripts/package.sh cuda "$BUNDLE" | |
| echo "BUNDLE=$BUNDLE" >> "$GITHUB_ENV" | |
| - name: Upload bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BUNDLE }} | |
| path: dist/${{ env.BUNDLE }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| mex-build: | |
| # Build siamize's MATLAB/Octave MEX wrapper and verify it loads. Heavy | |
| # full-inference test is left to cpp-build-cuda; this job's job is just | |
| # to catch MEX build regressions on each supported host. | |
| name: mex (${{ matrix.label }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - label: linux-octave | |
| os: ubuntu-22.04 | |
| kind: octave | |
| cmake_flag: -DSIAMIZE_BUILD_OCTAVE_MEX=ON | |
| mex_file: matlab/siamex.mex | |
| mnn_mex: true | |
| mnn_mex_target: siamize_oct | |
| - label: linux-matlab | |
| os: ubuntu-22.04 | |
| kind: matlab | |
| cmake_flag: -DSIAMIZE_BUILD_MATLAB_MEX=ON | |
| mex_file: matlab/siamex.mexa64 | |
| mnn_mex: true | |
| mnn_mex_target: siamize_mex | |
| - label: windows-matlab | |
| os: windows-2022 | |
| kind: matlab | |
| cmake_flag: -DSIAMIZE_BUILD_MATLAB_MEX=ON | |
| mex_file: matlab/siamex.mexw64 | |
| mnn_mex: true | |
| mnn_mex_target: siamize_mex | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Install Octave (Linux) | |
| if: matrix.kind == 'octave' && runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends octave liboctave-dev | |
| - name: Set up MATLAB | |
| if: matrix.kind == 'matlab' | |
| uses: matlab-actions/setup-matlab@v2 | |
| with: | |
| release: R2024a | |
| - name: Install GCC 10 (Linux MATLAB) | |
| # MATLAB R2024a on Linux bundles a libstdc++.so.6 that supports | |
| # GLIBCXX <=3.4.28 (GCC 9 / 10). The ubuntu-22.04 runner ships | |
| # GCC 11 as the default, whose libstdc++ headers reference | |
| # GLIBCXX_3.4.29 symbols and produce a MEX MATLAB can't load. | |
| # GCC 10 is MathWorks' officially supported MEX compiler for | |
| # R2024a on Linux. | |
| if: matrix.kind == 'matlab' && runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends g++-10 gcc-10 | |
| - name: Fetch C++ deps (CPU ORT) | |
| run: scripts/fetch_deps.sh | |
| - name: Configure | |
| env: | |
| # Pin to GCC 10 for the MATLAB-on-Linux leg so the MEX only | |
| # references GLIBCXX symbols MATLAB's bundled libstdc++ has. | |
| CC: ${{ (matrix.kind == 'matlab' && runner.os == 'Linux') && 'gcc-10' || '' }} | |
| CXX: ${{ (matrix.kind == 'matlab' && runner.os == 'Linux') && 'g++-10' || '' }} | |
| run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flag }} | |
| - name: Build | |
| run: cmake --build build --config Release --parallel | |
| - name: Verify MEX exists | |
| run: | | |
| ls -la "${{ matrix.mex_file }}" | |
| test -s "${{ matrix.mex_file }}" | |
| - name: Smoke-load MEX (Octave) | |
| if: matrix.kind == 'octave' && runner.os == 'Linux' | |
| run: | | |
| # Call siamex (the low-level MEX) with zero args to trigger the | |
| # C++ "Usage:" error. Proves the .mex loaded + reached mexFunction. | |
| octave-cli --no-gui --eval "addpath('matlab'); \ | |
| try; siamex(); catch err; \ | |
| if isempty(strfind(err.message, 'Usage:')); error('unexpected: %s', err.message); end; \ | |
| fprintf('MEX loaded and reached argument check\\n'); \ | |
| end" 2>&1 | tee mex.log | |
| grep -q "MEX loaded" mex.log | |
| - name: Run siamize.m unit tests (Octave) | |
| if: matrix.kind == 'octave' && runner.os == 'Linux' | |
| run: | | |
| # Pure-MATLAB dispatcher tests. Stubs siamex internally so it | |
| # doesn't depend on ORT or weight files; needs the jsonlab | |
| # submodule (pulled via submodules: recursive above). | |
| octave-cli --no-gui --eval "cd matlab/tests; run_tests('--exit')" | |
| - name: Smoke-load MEX (MATLAB) | |
| if: matrix.kind == 'matlab' | |
| uses: matlab-actions/run-command@v2 | |
| with: | |
| command: | | |
| addpath(fullfile(pwd, 'matlab')); | |
| % cd into matlab/ on Windows so the MEX picks up the | |
| % onnxruntime.dll the POST_BUILD step copied there via the | |
| % EXE search rule. On Linux the rpath handles it. | |
| if ispc; cd('matlab'); end | |
| try | |
| siamex(); | |
| error('siamex() with no args should have thrown'); | |
| catch err | |
| if isempty(strfind(err.message, 'Usage:')) | |
| error('unexpected: %s', err.message); | |
| end | |
| fprintf('MEX loaded and reached argument check\n'); | |
| end | |
| - name: Run siamize.m unit tests (MATLAB) | |
| if: matrix.kind == 'matlab' | |
| uses: matlab-actions/run-command@v2 | |
| with: | |
| command: | | |
| cd(fullfile('matlab', 'tests')); | |
| [~, nfail] = run_tests(); | |
| assert(nfail == 0, 'siamize.m unit tests failed (%d)', nfail); | |
| - name: Package | |
| shell: bash | |
| run: | | |
| BASE=$(cat VERSION 2>/dev/null || echo "0.0.0") | |
| if [ "${GITHUB_REF_TYPE}" = "tag" ]; then | |
| VER="${GITHUB_REF_NAME#v}" | |
| else | |
| VER="${BASE}-${GITHUB_SHA:0:7}" | |
| fi | |
| BUNDLE="siamex-${VER}-${{ matrix.label }}" | |
| scripts/package.sh mex "$BUNDLE" | |
| echo "BUNDLE=$BUNDLE" >> "$GITHUB_ENV" | |
| - name: Upload bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BUNDLE }} | |
| path: dist/${{ env.BUNDLE }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| # --- MNN/OpenCL MEX (vendor-neutral GPU backend) -------------------- | |
| # Build the same MEX wrapper against the MNN backend and upload it as a | |
| # separate artifact. It writes to the same matlab/siamex.mex* path as the | |
| # ORT MEX above, so this runs AFTER the ORT bundle is packaged + uploaded. | |
| # Linux legs only (matrix.mnn_mex); the OpenCL ICD is dlopen'd at runtime | |
| # so the smoke-load needs no GPU. Static libMNN.a is embedded into the | |
| # MEX (no libMNN.so to ship), matching `make openclmex` / `opencloct`. | |
| - name: Cache MNN stage (libMNN.a + headers) | |
| if: matrix.mnn_mex | |
| id: mnn-mex-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: third_party/mnn | |
| # Per-leg key: the MATLAB leg builds MNN with GCC 10 (to match | |
| # MATLAB's libstdc++), the Octave leg with the runner default, so | |
| # their staged libMNN.a are not interchangeable. | |
| key: mnn-mex-${{ matrix.label }}-${{ hashFiles('scripts/fetch_mnn.sh') }}-v3.5-opencl-conv3d-r6 | |
| - name: Fetch + build patched MNN (static) | |
| # First run: ~15-20 min compile; later runs hit the cache above. | |
| # SIAMIZE_GLIBCXX_COMPAT=1 pins condition_variable::wait@3.4.11 in | |
| # libMNN so the (dynamic-libstdc++) MEX loads in MATLAB's older | |
| # libstdc++. Safe here because the MEX never uses -static-libstdc++; | |
| # the CLI/docker MNN builds leave it unset (see fetch_mnn.sh). | |
| if: matrix.mnn_mex && steps.mnn-mex-cache.outputs.cache-hit != 'true' | |
| env: | |
| MNN_STATIC: "1" | |
| SIAMIZE_GLIBCXX_COMPAT: "1" | |
| # The MEX uses MSVC's dynamic CRT (/MD); build MNN /MD to match | |
| # (MNN_WIN_MT=0). Ignored off Windows. The CLI opencl build leaves | |
| # this default (=1 -> /MT) to match siamize.exe's static CRT. | |
| MNN_WIN_MT: "0" | |
| CC: ${{ (matrix.kind == 'matlab' && runner.os == 'Linux') && 'gcc-10' || '' }} | |
| CXX: ${{ (matrix.kind == 'matlab' && runner.os == 'Linux') && 'g++-10' || '' }} | |
| run: scripts/fetch_mnn.sh | |
| - name: Build MNN/OpenCL MEX | |
| # Build ONLY the MEX target -- NOT the `siamize` CLI. The CLI links | |
| # -static-libstdc++, which cannot resolve the @3.4.11 versioned | |
| # reference in the compat-built libMNN; the MEX uses dynamic libstdc++ | |
| # and links it fine. | |
| if: matrix.mnn_mex | |
| env: | |
| CC: ${{ (matrix.kind == 'matlab' && runner.os == 'Linux') && 'gcc-10' || '' }} | |
| CXX: ${{ (matrix.kind == 'matlab' && runner.os == 'Linux') && 'g++-10' || '' }} | |
| run: | | |
| cmake -S . -B build-mnn -DCMAKE_BUILD_TYPE=Release \ | |
| -DSIAMIZE_BACKEND=mnn ${{ matrix.cmake_flag }} | |
| cmake --build build-mnn --config Release --parallel \ | |
| --target ${{ matrix.mnn_mex_target }} | |
| - name: Verify MNN MEX exists | |
| if: matrix.mnn_mex | |
| run: | | |
| ls -la "${{ matrix.mex_file }}" | |
| test -s "${{ matrix.mex_file }}" | |
| # Diagnostic: the highest GLIBCXX symbol version the MEX requires. | |
| # With the GCC 10 toolchain pin (fetch_mnn + the Build step) this | |
| # must stay <= 3.4.28 so the MEX loads in stock MATLAB. A higher | |
| # value here means the GCC 10 pin regressed and the MATLAB smoke-load | |
| # below will fail -- fix the toolchain, not with a runtime preload. | |
| echo "--- MNN MEX max GLIBCXX requirement (want <= 3.4.28 for MATLAB) ---" | |
| objdump -T "${{ matrix.mex_file }}" 2>/dev/null \ | |
| | grep -oE "GLIBCXX_[0-9.]+" | sort -V | tail -3 || true | |
| - name: Smoke-load MNN MEX (Octave) | |
| if: matrix.mnn_mex && matrix.kind == 'octave' && runner.os == 'Linux' | |
| run: | | |
| octave-cli --no-gui --eval "addpath('matlab'); \ | |
| try; siamex(); catch err; \ | |
| if isempty(strfind(err.message, 'Usage:')); error('unexpected: %s', err.message); end; \ | |
| fprintf('MNN MEX loaded and reached argument check\\n'); \ | |
| end" 2>&1 | tee mex-mnn.log | |
| grep -q "MNN MEX loaded" mex-mnn.log | |
| - name: Smoke-load MNN MEX (MATLAB) | |
| # No LD_PRELOAD: libMNN is built with GCC 10 (see fetch_mnn's compiler | |
| # pin + the gcc-10 CC/CXX on the Build step), so the MEX stays within | |
| # MATLAB's bundled libstdc++ and must load as-is -- exactly like the | |
| # ORT MATLAB MEX. If this step regresses on a GLIBCXX symbol, the | |
| # diagnostic above shows the floor and the fix is the toolchain, not a | |
| # preload that would only mask a non-portable artifact. | |
| if: matrix.mnn_mex && matrix.kind == 'matlab' | |
| uses: matlab-actions/run-command@v2 | |
| with: | |
| command: | | |
| addpath(fullfile(pwd, 'matlab')); | |
| try | |
| siamex(); | |
| error('siamex() with no args should have thrown'); | |
| catch err | |
| if isempty(strfind(err.message, 'Usage:')) | |
| error('unexpected: %s', err.message); | |
| end | |
| fprintf('MNN MEX loaded and reached argument check\n'); | |
| end | |
| - name: Package MNN/OpenCL MEX | |
| if: matrix.mnn_mex | |
| shell: bash | |
| run: | | |
| BASE=$(cat VERSION 2>/dev/null || echo "0.0.0") | |
| if [ "${GITHUB_REF_TYPE}" = "tag" ]; then | |
| VER="${GITHUB_REF_NAME#v}" | |
| else | |
| VER="${BASE}-${GITHUB_SHA:0:7}" | |
| fi | |
| MNN_BUNDLE="siamex-opencl-${VER}-${{ matrix.label }}" | |
| scripts/package.sh openclmex "$MNN_BUNDLE" | |
| echo "MNN_BUNDLE=$MNN_BUNDLE" >> "$GITHUB_ENV" | |
| - name: Upload MNN/OpenCL MEX bundle | |
| if: matrix.mnn_mex | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.MNN_BUNDLE }} | |
| path: dist/${{ env.MNN_BUNDLE }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| cpp-build-coreml: | |
| # Builds siamize with the CoreML Execution Provider enabled and | |
| # actually runs end-to-end inference on a real Apple Silicon runner | |
| # (M-class CPU + Metal GPU + Neural Engine). Unlike cpp-build-cuda | |
| # where GHA has no NVIDIA GPU, GHA's macos-14 runner DOES expose | |
| # the full Apple Silicon stack to CoreML, so this is the "GPU | |
| # smoke" leg for macOS. | |
| name: build+run coreml (macos-14) | |
| runs-on: macos-14 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install OpenMP | |
| run: | | |
| brew install libomp | |
| echo "OpenMP_ROOT=$(brew --prefix libomp)" >> "$GITHUB_ENV" | |
| - name: Fetch C++ deps (CPU ORT -- macOS dylib already includes CoreML EP) | |
| run: scripts/fetch_deps.sh | |
| - name: Configure (CoreML EP) | |
| run: | | |
| cmake -S . -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DSIAMIZE_STATIC_LINK=ON \ | |
| -DSIAMIZE_GPU=coreml | |
| - name: Build | |
| run: cmake --build build --config Release --parallel | |
| - name: Smoke-run --help (always) | |
| # Cheap sanity check on every push: the binary launches and the | |
| # CoreML-EP-aware help text is intact. Full inference is gated | |
| # behind the tag-push branch below since CPU-only Core ML on the | |
| # 7 GB free runner takes ~7-10 min and adds no regression value | |
| # to normal CI. | |
| run: build/siamize --help | head -20 | |
| # The remaining steps (fetch weight, run, verify, package) execute | |
| # only on tag pushes -- that's where we actually want to validate | |
| # the macOS-arm64-coreml release bundle end-to-end. Pull requests | |
| # and main-branch pushes stop at "Build" above. | |
| # DISABLED end-to-end CoreML inference: on the ~7 GB macos-14 runner the | |
| # GPU/ANE path OOMs, so it falls back to MLComputeUnits=CPUOnly, which is | |
| # too slow (>30 min) and timed out the tag CI. The Build + `--help` steps | |
| # above still validate the CoreML build, and Package/Upload below still | |
| # ship the macOS-arm64-coreml bundle -- only the inference smoke test is | |
| # off. Re-enable (flip `if: false` back to `github.ref_type == 'tag'`) | |
| # when a larger macOS runner can actually exercise the GPU/ANE path. | |
| - name: Fetch fold_0 weight (CoreML variant; InstanceNorm rewritten) | |
| if: false | |
| # SIAM v0.3's encoder/decoder uses 3D InstanceNorm, which Apple's | |
| # mlcompilerd doesn't accept in MLProgram format (only rank 3-4). | |
| # We host a rewritten variant on the NeuroJSON server under | |
| # doc=coreml where rank-5 InstanceNorm has been replaced with | |
| # Reshape -> InstanceNorm-rank-3 -> Reshape (see | |
| # tools/onnx_export/rewrite_for_coreml.py). siamize's weight | |
| # resolver auto-picks this variant when -c coreml is active. | |
| run: | | |
| URL='https://neurojson.org/io/stat.cgi?action=get&db=siam_v03&doc=coreml&file=fold_0_fp16.onnx.gz' | |
| mkdir -p models | |
| curl -fsSL "$URL" -o models/fold_0_fp16.onnx.gz | |
| gunzip -f models/fold_0_fp16.onnx.gz | |
| ls -la models/ | |
| - name: Run single-fold inference on Apple Silicon | |
| if: false | |
| # macos-14 free-tier runner has ~7 GB RAM, which forces | |
| # auto-lowmem to MLComputeUnits=CPUOnly (the only target whose | |
| # mlcompilerd peak fits the budget; ALL and even cpugpu OOM at | |
| # exit 137). CPU-only Core ML completes in ~7-10 min wall. | |
| # Local benchmarks on hosts with >=14 GB free can override with | |
| # --coreml-units all to exercise the Metal GPU + ANE paths. | |
| timeout-minutes: 30 | |
| run: | | |
| build/siamize \ | |
| -i tests/sub-01_T1w.nii.gz \ | |
| -o pred_ci.nii.gz \ | |
| -M models/fold_0_fp16.onnx \ | |
| -c auto 2>&1 | tee siamize.log | |
| - name: Verify CoreML EP registered | |
| if: false | |
| run: | | |
| if ! grep -Eq "\[coreml\][[:space:]]+enabled" siamize.log; then | |
| echo "FAIL: expected '[coreml] enabled' line; got:" | |
| cat siamize.log | |
| exit 1 | |
| fi | |
| echo "OK: CoreML EP registered." | |
| - name: Verify output produced | |
| if: false | |
| run: | | |
| test -s pred_ci.nii.gz || { echo "FAIL: empty/missing output"; exit 1; } | |
| ls -lh pred_ci.nii.gz | |
| - name: Package | |
| if: github.ref_type == 'tag' | |
| shell: bash | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| BUNDLE="siamize-${VER}-macos-arm64-coreml" | |
| # package.sh's 'cuda' / 'tensorrt' modes pull GPU-specific libs; | |
| # the macOS CoreML bundle is just the CPU package (CoreML EP is | |
| # statically baked into libonnxruntime.dylib on macOS). | |
| scripts/package.sh cpu "$BUNDLE" | |
| echo "BUNDLE=$BUNDLE" >> "$GITHUB_ENV" | |
| - name: Upload bundle | |
| if: github.ref_type == 'tag' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BUNDLE }} | |
| path: dist/${{ env.BUNDLE }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| cpp-build-opencl: | |
| # Builds siamize with SIAMIZE_BACKEND=mnn (single static libMNN.a) | |
| # and runs single-fold inference end-to-end on the GHA runner. | |
| # The MNN-CPU forward type works without a GPU, so this job | |
| # genuinely verifies the inference produces a non-empty output on | |
| # every push -- not just a fallback to a different backend the | |
| # way cpp-build-cuda has to. The .mnn weight (int8-quantized, | |
| # ~35 MB gzipped) auto-downloads from doc=mnn_i8a via siamize's | |
| # built-in resolver, exercising the canonical user path. | |
| name: build+run opencl (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-22.04 | |
| binary: build/siamize | |
| - os: macos-14 | |
| binary: build/siamize | |
| - os: windows-2022 | |
| # MSVC multi-config generator drops the binary under Release/. | |
| binary: build/Release/siamize.exe | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install OpenMP (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| brew install libomp | |
| echo "OpenMP_ROOT=$(brew --prefix libomp)" >> "$GITHUB_ENV" | |
| - name: Cache MNN stage (libMNN.a + headers) | |
| id: mnn-cache | |
| uses: actions/cache@v4 | |
| with: | |
| # We only need the staged artifacts; the heavy intermediate | |
| # build tree under third_party/mnn-build/ is not cached | |
| # (caching the 1 GB CMakeFiles tree saved less time than | |
| # the cache restore/save itself in benchmarking). | |
| path: third_party/mnn | |
| # Key off the script that drives the build + the ref it | |
| # downloads. Bump either to invalidate the cache. | |
| key: mnn-${{ runner.os }}-static-${{ hashFiles('scripts/fetch_mnn.sh') }}-v3.5-opencl-conv3d-r6 | |
| - name: Fetch + build patched MNN (static) | |
| # First run: 15-20 min compile. Subsequent runs hit the cache | |
| # above and skip this step entirely. fetch_mnn.sh itself is | |
| # idempotent (FORCE=1 to bust), but we don't need it: the | |
| # actions/cache step decides whether the work is needed. | |
| if: steps.mnn-cache.outputs.cache-hit != 'true' | |
| env: | |
| MNN_STATIC: "1" | |
| run: scripts/fetch_mnn.sh | |
| - name: Configure (MNN backend) | |
| run: | | |
| cmake -S . -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DSIAMIZE_STATIC_LINK=ON \ | |
| -DSIAMIZE_BACKEND=mnn | |
| - name: Build | |
| run: cmake --build build --config Release --parallel | |
| - name: Inspect runtime deps (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| echo "--- ldd ---" | |
| ldd build/siamize | |
| echo "--- siamize binary size ---" | |
| ls -lh build/siamize | |
| stat -c "%s" build/siamize | |
| - name: Inspect runtime deps (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| echo "--- otool -L (linked dylibs) ---" | |
| otool -L build/siamize | |
| ls -lh build/siamize | |
| - name: Inspect runtime deps (Windows) | |
| if: runner.os == 'Windows' | |
| run: ls -la build/Release/ | |
| - name: Smoke-run --help | |
| run: ${{ matrix.binary }} --help | |
| # The end-to-end MNN-CPU inference + Dice check below run on Linux | |
| # only: they exercise the inference path once per push and rely on | |
| # Linux tooling (ldd/lscpu/free, the -P workspace tuning calibrated to | |
| # the Linux runner's cgroup). The macOS / Windows legs are build + | |
| # --help + package, mirroring cpp-build, so we still ship and smoke the | |
| # MNN/OpenCL binaries on those platforms. | |
| - name: Runner CPU / memory profile (diagnostic) | |
| if: runner.os == 'Linux' | |
| # Helps triage MNN crashes that depend on the runner's | |
| # /proc/cpuinfo, /sys/devices/system/cpu/cpufreq, or memory | |
| # layout -- MNN's CPU runtime probes all three at session | |
| # creation time. | |
| run: | | |
| echo "--- CPU ---" | |
| lscpu | head -25 | |
| echo "--- memory ---" | |
| free -h | |
| echo "--- /sys/devices/system/cpu/cpufreq ---" | |
| ls /sys/devices/system/cpu/cpufreq/ 2>&1 | head -10 || true | |
| echo "--- ulimit ---" | |
| ulimit -a | |
| - name: Run single-fold MNN-CPU inference | |
| # int8-quantized .mnn weight auto-downloads from doc=mnn_i8a | |
| # (~35 MB gzipped -> 143 MB on disk). MNN's CPU forward type | |
| # is the safe choice on GHA -- no OpenCL ICD to depend on. | |
| # Threads: auto (no -t) -> min(hardware_concurrency, 8); on the | |
| # 4-vCPU runner that is 4. The old -t 2 worked around a CPURaster | |
| # MT segfault that the native-Conv3D path (this -M 0 / n3d model) | |
| # no longer hits. If a runner segfaults at >2 threads, restore -t 2. | |
| # | |
| # -P 192x192x128 (instead of the default 256x256x192) so | |
| # MNN's resizeSession workspace allocation stays under the | |
| # ~14 GB GHA cgroup. The default patch pre-allocates the | |
| # full intermediate-tensor pyramid at ctor time, which on | |
| # this runner segfaults inside MNN before the engine prints | |
| # its first log line. The smaller patch is the same one | |
| # siamize's auto-lowmem heuristic picks on RAM-tight hosts; | |
| # Dice stays in spec across patch sizes. | |
| if: runner.os == 'Linux' | |
| timeout-minutes: 30 | |
| run: | | |
| ${{ matrix.binary }} \ | |
| -i tests/sub-01_T1w.nii.gz \ | |
| -o pred_ci.nii.gz \ | |
| -M 0 \ | |
| -c cpu \ | |
| -P 192x192x128 2>&1 | tee siamize.log | |
| - name: Verify MNN backend was used | |
| if: runner.os == 'Linux' | |
| run: | | |
| if grep -Eq "backend mnn" siamize.log; then | |
| echo "OK: MNN backend engaged." | |
| else | |
| echo "FAIL: expected 'backend mnn' marker; got:" | |
| cat siamize.log | |
| exit 1 | |
| fi | |
| - name: Verify output produced | |
| if: runner.os == 'Linux' | |
| run: | | |
| test -s pred_ci.nii.gz || { echo "FAIL: empty/missing output"; exit 1; } | |
| ls -lh pred_ci.nii.gz | |
| - name: Compare to bundled reference (mean foreground Dice) | |
| if: runner.os == 'Linux' | |
| # Bundled reference (tests/pred_ref_allfolds.nii.gz) is the | |
| # 5-fold ORT ensemble; we're running single-fold MNN-int8 here, | |
| # so the comparison is "do we get the same general structure" | |
| # rather than an equality check. Use a generous floor: any | |
| # mean-fg Dice < 0.90 on this T1 means something is badly | |
| # broken (silent garbage logits, wrong weight bundle, etc). | |
| run: | | |
| python3 -m pip install --quiet nibabel numpy | |
| python3 - <<'PY' | |
| import nibabel as nib, numpy as np | |
| new = nib.load('pred_ci.nii.gz').get_fdata().astype(np.int32) | |
| ref = nib.load('tests/pred_ref_allfolds.nii.gz').get_fdata().astype(np.int32) | |
| # nnUNet outputs argmax labels; compute per-class Dice over the | |
| # foreground classes, then take the unweighted mean. Single-fold | |
| # MNN-int8 vs 5-fold ORT-fp16 reference is ~0.95 Dice on this T1. | |
| dices = [] | |
| for k in range(1, int(max(new.max(), ref.max())) + 1): | |
| a = (new == k); b = (ref == k) | |
| if a.sum() + b.sum() == 0: | |
| continue | |
| dices.append((2 * (a & b).sum()) / (a.sum() + b.sum())) | |
| mean = float(np.mean(dices)) if dices else 0.0 | |
| print(f'mean foreground Dice = {mean:.4f} ({len(dices)} classes)') | |
| assert mean >= 0.90, f'Dice {mean:.4f} below sanity floor 0.90' | |
| PY | |
| # Compress the self-contained MNN/OpenCL binary with UPX (Linux + | |
| # Windows only -- UPX's macOS Mach-O support, especially arm64, is | |
| # unreliable). The binary self-decompresses at startup; we re-run | |
| # --help afterwards to prove the packed binary still launches. | |
| - name: Install UPX (Linux) | |
| if: runner.os == 'Linux' | |
| run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends upx-ucl | |
| - name: Install UPX (Windows) | |
| if: runner.os == 'Windows' | |
| run: choco install upx -y --no-progress | |
| - name: Compress binary with UPX | |
| if: runner.os == 'Linux' || runner.os == 'Windows' | |
| run: | | |
| echo "--- before ---"; ls -l "${{ matrix.binary }}" | |
| upx --best "${{ matrix.binary }}" | |
| echo "--- after ---"; ls -l "${{ matrix.binary }}" | |
| # The compressed binary must still launch (self-extracts at startup). | |
| "${{ matrix.binary }}" --help > /dev/null && echo "OK: UPX-packed binary runs" | |
| - name: Package | |
| shell: bash | |
| run: | | |
| BASE=$(cat VERSION 2>/dev/null || echo "0.0.0") | |
| if [ "${GITHUB_REF_TYPE}" = "tag" ]; then | |
| VER="${GITHUB_REF_NAME#v}" | |
| else | |
| VER="${BASE}-${GITHUB_SHA:0:7}" | |
| fi | |
| case "${{ runner.os }}" in | |
| Linux) PLAT=linux-x86_64 ;; | |
| macOS) PLAT=macos ;; | |
| Windows) PLAT=windows-x64 ;; | |
| esac | |
| BUNDLE="siamize-${VER}-${PLAT}-opencl" | |
| # `opencl` mode stages the (static or shared) MNN binary | |
| # alone; libMNN.so is only copied if a shared build was | |
| # staged. See scripts/package.sh. | |
| scripts/package.sh opencl "$BUNDLE" | |
| echo "BUNDLE=$BUNDLE" >> "$GITHUB_ENV" | |
| - name: Upload bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BUNDLE }} | |
| path: dist/${{ env.BUNDLE }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| mex-cuda-smoke: | |
| # Compile-only smoke test for `make cudaoct` / `make cudamex`. The | |
| # GHA runner has no NVIDIA GPU and no CUDA runtime libs, so we can | |
| # NOT loadlibrary the resulting MEX -- libonnxruntime_providers_cuda | |
| # .so would dlopen and fail. Instead we just verify the build | |
| # succeeds AND the produced .mex contains the CUDA-EP-probe strings | |
| # that `sliding.cpp`'s `#ifdef SIAMIZE_HAS_CUDA` block emits. That | |
| # catches the kind of regression where SIAMIZE_HAS_CUDA fails to | |
| # propagate to the MEX (which was the bug fixed in dee8b1d). | |
| name: mex-cuda-smoke (octave) | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Install Octave | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends octave liboctave-dev | |
| - name: Fetch C++ deps (GPU ORT prebuilt) | |
| env: | |
| ORT_BUILD: gpu | |
| run: scripts/fetch_deps.sh | |
| - name: Build `make cudaoct` | |
| run: make cudaoct | |
| - name: Verify MEX exists | |
| run: | | |
| ls -la matlab/siamex.mex | |
| test -s matlab/siamex.mex | |
| - name: Verify CUDA-EP code paths compiled in | |
| run: | | |
| # Strings emitted by sliding.cpp's `#ifdef SIAMIZE_HAS_CUDA` | |
| # block. Their presence proves the CUDA EP probe code reached | |
| # the linker; their absence means SIAMIZE_HAS_CUDA didn't | |
| # propagate (the dee8b1d bug). | |
| # | |
| # Dump strings to a file first, then grep the file. Piping | |
| # `strings ... | grep -q ...` is fragile under bash's | |
| # `-o pipefail` (GHA default): when grep finds the match | |
| # early it closes the pipe, strings gets SIGPIPE and exits | |
| # non-zero, and the pipeline reports failure even though the | |
| # match was found. | |
| strings -a matlab/siamex.mex > /tmp/siamex_strings.txt | |
| for needle in "probing CUDA EP" "cudnn_conv_use_max_workspace" \ | |
| "cudnn_conv_algo_search"; do | |
| if ! grep -qF "$needle" /tmp/siamex_strings.txt; then | |
| echo "FAIL: '$needle' not found in matlab/siamex.mex" >&2 | |
| exit 1 | |
| fi | |
| done | |
| echo "OK: CUDA-EP probe strings present in matlab/siamex.mex" | |
| mex-coreml-smoke: | |
| # macOS-only compile + strings-grep test for `make coremloct`. | |
| # The macos-14 runner DOES have a real CoreML stack, but we keep | |
| # this leg lightweight (compile + symbol probe only) since the | |
| # heavier end-to-end MEX-load test would require MATLAB on Apple | |
| # Silicon, which isn't part of the matlab-actions/setup-matlab | |
| # macos-14 support yet. Validates SIAMIZE_HAS_COREML propagates | |
| # to the MEX -- same kind of regression as dee8b1d but for the | |
| # CoreML path. | |
| name: mex-coreml-smoke (octave) | |
| runs-on: macos-14 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Install Octave + libomp via Homebrew | |
| run: | | |
| brew install octave libomp | |
| echo "OpenMP_ROOT=$(brew --prefix libomp)" >> "$GITHUB_ENV" | |
| - name: Fetch C++ deps (CPU ORT) | |
| run: scripts/fetch_deps.sh | |
| - name: Build `make coremloct` | |
| run: make coremloct | |
| - name: Verify MEX exists | |
| run: | | |
| ls -la matlab/siamex.mex | |
| test -s matlab/siamex.mex | |
| - name: Verify CoreML EP code paths compiled in | |
| run: | | |
| # Strings emitted by sliding.cpp's `#ifdef SIAMIZE_HAS_COREML` | |
| # block. Their presence proves the CoreML EP probe code reached | |
| # the linker; their absence means SIAMIZE_HAS_COREML didn't | |
| # propagate through the Octave MEX's CXXFLAGS path. | |
| # | |
| # Dump strings to a file first, then grep the file. Apple's | |
| # BSD strings exits non-zero on SIGPIPE when grep -q closes | |
| # the pipe early; combined with bash's `-o pipefail` (GHA | |
| # default) that fails the pipeline even when the match was | |
| # found. -a forces strings to read every Mach-O section, | |
| # not just __TEXT. | |
| strings -a matlab/siamex.mex > /tmp/siamex_strings.txt | |
| for needle in "probing CoreML EP" "MLComputeUnits" \ | |
| "ModelCacheDirectory" "RequireStaticInputShapes"; do | |
| if ! grep -qF "$needle" /tmp/siamex_strings.txt; then | |
| echo "FAIL: '$needle' not found in matlab/siamex.mex" >&2 | |
| echo "--- strings related to coreml / CoreML (for triage) ---" | |
| grep -iE "coreml|mlcompute|mlprogram|instance" \ | |
| /tmp/siamex_strings.txt | head -20 | |
| exit 1 | |
| fi | |
| done | |
| echo "OK: CoreML-EP probe strings present in matlab/siamex.mex" | |
| python-smoke: | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install Python deps | |
| run: | | |
| pip install --upgrade pip | |
| pip install numpy nibabel scipy scikit-image torch \ | |
| dynamic_network_architectures onnxruntime | |
| - name: Import smoke test | |
| run: | | |
| python -c "import sys; sys.path.insert(0, 'py'); import siam_ref; print(siam_ref.build_network.__name__)" | |
| python tools/onnx_export/compare.py --help || true |