From faa8c94f18e0ed72434cdeaacae71f15aa13bd60 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 21:46:31 -0500 Subject: [PATCH 1/6] ci: rebuild the MATLAB toolbox workflow on the proven CUDA toolchain The workflow's only run (v4.0.0 merge) failed on both platforms at the Jimver CUDA install step - the same breakage ci.yml already moved away from. Port the four ci.yml lessons: CUDA from conda-forge, conda base env, Ninja generator, CUDA 13.x (nvcc 12.x rejects VS 2026 MSVC). Other fixes while rebuilding it: - Pass Matlab_ROOT_DIR explicitly; FindMatlab cannot discover the hosted-toolcache install on its own. - Pin MATLAB R2024b (the release the .prj was authored with) so the MEX binaries match the toolbox's recorded minimum release. - Replace the inline .prj regex patch with packageToolboxCI.m, which packages programmatically via ToolboxOptions - no machine-specific absolute paths, version single-sourced from pyproject.toml (the old .prj still said 3.1.2). - Package with the freshly generated .m wrappers from the Windows build instead of the checked-in copies, and guard against Git LFS pointer files sneaking into the toolbox. - Run path-filtered on PRs (single CUDA arch for speed), all-major on main, and attach the .mltbx to published GitHub Releases. Co-Authored-By: Claude Fable 5 --- .github/workflows/matlab-multibuild.yml | 285 ++++++++++++-------- CLAUDE.md | 2 +- src/MATLAB/build-scripts/packageToolboxCI.m | 66 +++++ 3 files changed, 242 insertions(+), 111 deletions(-) create mode 100644 src/MATLAB/build-scripts/packageToolboxCI.m diff --git a/.github/workflows/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml index c2e5929..511b99f 100644 --- a/.github/workflows/matlab-multibuild.yml +++ b/.github/workflows/matlab-multibuild.yml @@ -1,105 +1,171 @@ -name: Multi-Platform Toolbox Build +name: MATLAB Toolbox + +# Build the HIP MEX extension on the platforms the toolbox supports +# (Windows + Linux) and package the MATLAB toolbox (.mltbx). +# +# Toolchain mirrors ci.yml: CUDA comes from conda-forge (compiler only — +# runners have no GPU), builds run in the conda base env with the Ninja +# generator, and CUDA 13.x is required because nvcc 12.x rejects the +# VS 2026 MSVC that windows-latest ships. MATLAB comes from +# matlab-actions/setup-matlab, which self-licenses on public-repo hosted +# runners. Runners have no GPU, so this verifies build + packaging only; +# the MATLAB accuracy suite runs locally on a GPU box (see TESTING.md). on: push: - branches: [ "main", "master" ] + branches: [main] + pull_request: + paths: + - ".github/workflows/matlab-multibuild.yml" + - "src/c/**" + - "src/MATLAB/**" + - "CMakeLists.txt" + - "pyproject.toml" + # Attach the packaged toolbox to the GitHub Release. Triggering on the + # published release (rather than the tag push) avoids racing release.yml, + # which is what creates the release. + release: + types: [published] workflow_dispatch: +concurrency: + group: matlab-${{ github.ref }} + cancel-in-progress: true + +env: + # R2024b is the release the toolbox .prj was authored with; it is also the + # floor recorded in the packaged toolbox metadata (packageToolboxCI.m). + # MEX binaries are only guaranteed to load on the release they were built + # with or newer, so keep these in sync. + MATLAB_RELEASE: R2024b + jobs: - build-mex-windows: - name: Build MEX (Windows) - runs-on: windows-latest + build-mex: + name: Build MEX (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest] + + defaults: + run: + shell: bash -el {0} + + env: + # Distribution artifacts target every major architecture; PR builds + # use a single architecture so the gate stays fast. + CUDA_ARCHS: ${{ github.event_name == 'pull_request' && '75' || 'all-major' }} + steps: - uses: actions/checkout@v4 - - - name: Install CUDA Toolkit - uses: Jimver/cuda-toolkit@v0.2.14 with: - cuda: '12.4.1' + lfs: false # the MEX binaries are rebuilt here; no LFS assets needed - - name: Setup MATLAB - uses: matlab-actions/setup-matlab@v2 + - name: Set up MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 - - name: Configure CMake - run: | - cmake -S . -B build -G "Visual Studio 17 2022" -DHYDRA_MODULE_NAME=HIP - # Matlab_ROOT is set by setup-matlab and detected by FindMatlab module usually - # If not, we might need -DMatlab_ROOT_DIR=$env:MATLAB_ROOT + - uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-version: latest + channels: conda-forge + conda-remove-defaults: "true" + # Build in base: activating a named env intermittently fails on + # the Windows runners (EnvironmentNameNotFound). + activate-environment: "" - - name: Build MEX - run: | - cmake --build build --config Release --target HydraMex + - name: Install build dependencies + run: conda install -y -c conda-forge cmake ninja cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=13.*" - - name: Verify Output - shell: powershell - run: | - # Verify the mex file exists in the source tree (copied by autoInstallMex) - if (Test-Path "src/MATLAB/+HIP/@Cuda/HIP.mexw64") { - Write-Host "MEX file found." - } else { - Write-Error "MEX file not found in src/MATLAB/+HIP/@Cuda/HIP.mexw64" - # Check build dir if autoInstall failed - Get-ChildItem -Recurse -Filter HIP.mexw64 - exit 1 - } + - name: Install host compiler for nvcc (Linux) + if: runner.os == 'Linux' + run: conda install -y -c conda-forge c-compiler cxx-compiler - - uses: actions/upload-artifact@v4 + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 with: - name: mex-windows - path: src/MATLAB/+HIP/@Cuda/HIP.mexw64 + release: ${{ env.MATLAB_RELEASE }} - build-mex-linux: - name: Build MEX (Linux) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install CUDA Toolkit - uses: Jimver/cuda-toolkit@v0.2.14 + # FindMatlab cannot discover the hosted-toolcache install location on + # its own, so ask MATLAB where it lives and pass that to CMake. + - name: Locate MATLAB root + uses: matlab-actions/run-command@v2 with: - cuda: '12.4.1' - - - name: Setup MATLAB - uses: matlab-actions/setup-matlab@v2 + command: fid = fopen(fullfile(getenv('GITHUB_WORKSPACE'), 'matlabroot.txt'), 'w'); fprintf(fid, '%s', matlabroot); fclose(fid); - - name: Configure CMake + - name: Configure run: | - cmake -S . -B build -DHYDRA_MODULE_NAME=HIP - + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DHYDRA_MODULE_NAME=HIP \ + -DHYDRA_BUILD_TESTS=OFF \ + -DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCHS}" \ + -DMatlab_ROOT_DIR="$(tr -d '\r' < matlabroot.txt)" + + # The post-build step (autoBuildMex.cmake) launches MATLAB to copy the + # MEX binary into src/MATLAB/+HIP/@Cuda and regenerate the .m wrappers. - name: Build MEX run: | - cmake --build build --config Release --target HydraMex - - - name: Verify Output + set -o pipefail + if ! cmake --build build --target HydraMex 2>&1 | tee build.log; then + echo "::error::cl=$(command -v cl 2>/dev/null || echo MISSING) nvcc=$(command -v nvcc 2>/dev/null || echo MISSING) cmake=$(cmake --version | head -1)" + grep -iE "error|fatal" build.log | head -n 8 | while IFS= read -r line; do echo "::error::${line}"; done + exit 1 + fi + + - name: Verify output run: | - if [ -f "src/MATLAB/+HIP/@Cuda/HIP.mexa64" ]; then - echo "MEX file found." - else - echo "MEX file not found in src/MATLAB/+HIP/@Cuda/HIP.mexa64" - find . -name "HIP.mexa64" - exit 1 - fi - - - uses: actions/upload-artifact@v4 + MEXT=$([ "$RUNNER_OS" = "Windows" ] && echo mexw64 || echo mexa64) + MEXFILE="src/MATLAB/+HIP/@Cuda/HIP.${MEXT}" + if [ ! -f "$MEXFILE" ]; then + echo "::error::${MEXFILE} not found after build" + find . -name "HIP.mex*" -not -path "./build/*" + exit 1 + fi + ls -l "$MEXFILE" + # Wrapper drift is informational: the generated .m files are + # tracked in git and should be re-committed when help text changes. + if ! git -c core.safecrlf=false diff --quiet -- 'src/MATLAB/+HIP/**/*.m'; then + echo "::warning::Generated .m wrappers differ from the checked-in ones; consider re-committing src/MATLAB/+HIP" + fi + + # Windows contributes the whole package directory (fresh wrappers + + # HIP.mexw64). The checked-out HIP.mexa64 is excluded because with + # lfs:false it is only a Git LFS pointer; the Linux job supplies the + # real one. + - name: Upload package directory (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: hip-package-windows + path: | + src/MATLAB/+HIP + !src/MATLAB/+HIP/@Cuda/HIP.mexa64 + if-no-files-found: error + + - name: Upload MEX (Linux) + if: runner.os == 'Linux' + uses: actions/upload-artifact@v4 with: - name: mex-linux - path: src/MATLAB/+HIP/@Cuda/HIP.mexa64 + name: mex-linux + path: src/MATLAB/+HIP/@Cuda/HIP.mexa64 + if-no-files-found: error package-toolbox: name: Package Toolbox - needs: [build-mex-windows, build-mex-linux] + needs: build-mex runs-on: windows-latest steps: - uses: actions/checkout@v4 + with: + lfs: false - - name: Setup MATLAB - uses: matlab-actions/setup-matlab@v2 - - - name: Download Windows MEX + - name: Download Windows package directory uses: actions/download-artifact@v4 with: - name: mex-windows - path: src/MATLAB/+HIP/@Cuda + name: hip-package-windows + path: src/MATLAB/+HIP - name: Download Linux MEX uses: actions/download-artifact@v4 @@ -107,49 +173,48 @@ jobs: name: mex-linux path: src/MATLAB/+HIP/@Cuda - - name: Patch PRJ File - shell: python + # Guard against packaging Git LFS pointer files if an artifact path + # ever drifts: real MEX binaries are megabytes, pointers ~130 bytes. + - name: Verify MEX binaries are real + shell: bash + run: | + for f in src/MATLAB/+HIP/@Cuda/HIP.mexw64 src/MATLAB/+HIP/@Cuda/HIP.mexa64; do + size=$(stat -c%s "$f") + if [ "$size" -lt 100000 ]; then + echo "::error::$f is only ${size} bytes - looks like a Git LFS pointer, not a built MEX binary" + exit 1 + fi + echo "$f: ${size} bytes" + done + + - name: Read toolbox version from pyproject.toml + shell: bash run: | - import os - - prj_path = r'src/MATLAB/HydraImageProcessor.prj' - with open(prj_path, 'r') as f: - content = f.read() - - # Replace absolute paths with ${PROJECT_ROOT} if they look like the user's local path - # The user's path was D:\Users\ewait\git\programming\hydra-image-processor - # We'll just genericize any absolute path pattern if possible, or just the specific one. - # Easier: Update the section to use ${PROJECT_ROOT} - - # Since we don't know the exact string that might be in the repo vs what was pasted, - # we will try to be smart. - # But actually, verify if the repo version has absolute paths. - # If it uses ${PROJECT_ROOT} mostly, we are good. - # The snippet showed absolute path in . - - import re - # Regex to find ANYTHING\Hydra Image Processor.mltbx inside - # and replace it with ${PROJECT_ROOT}\Hydra Image Processor.mltbx - - new_content = re.sub( - r'(\s*]*>).*?(\Hydra Image Processor\.mltbx)', - r'\1${PROJECT_ROOT}\2', - content, - flags=re.DOTALL - ) - - with open(prj_path, 'w') as f: - f.write(new_content) - - print("Patched .prj file.") - - - name: Package Toolbox + VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/version *= *"([^"]+)".*/\1/') + echo "TOOLBOX_VERSION=${VERSION}" >> "$GITHUB_ENV" + echo "Toolbox version: ${VERSION}" + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ env.MATLAB_RELEASE }} + + - name: Package toolbox uses: matlab-actions/run-command@v2 with: - command: | - matlab.addons.toolbox.packageToolbox('src/MATLAB/HydraImageProcessor.prj'); + command: >- + addpath(fullfile(getenv('GITHUB_WORKSPACE'), 'src', 'MATLAB', 'build-scripts')); + packageToolboxCI(getenv('TOOLBOX_VERSION')); - uses: actions/upload-artifact@v4 with: - name: HydraImageProcessor.mltbx - path: src/MATLAB/Hydra Image Processor.mltbx + name: HydraImageProcessor.mltbx + path: src/MATLAB/Hydra Image Processor.mltbx + if-no-files-found: error + + - name: Attach toolbox to the GitHub Release + if: github.event_name == 'release' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ github.event.release.tag_name }}" "src/MATLAB/Hydra Image Processor.mltbx" --clobber diff --git a/CLAUDE.md b/CLAUDE.md index 8d488fc..d0889ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -96,4 +96,4 @@ Type dispatch, Python method registration, and MATLAB wrappers are all generated ## CI -`.github/workflows/ci.yml` is the build-gate: on Windows + Linux it installs CUDA/compilers from conda-forge, builds via the same `pip install .` path the feedstock uses, and runs an import smoke test (runners have no GPU — accuracy suites run locally, see TESTING.md). `.github/workflows/release.yml` runs on `v*` tags. `.github/workflows/matlab-multibuild.yml` builds `HIP` MEX binaries on Windows + Linux and packages the MATLAB toolbox from `src/MATLAB/HydraImageProcessor.prj`. +`.github/workflows/ci.yml` is the build-gate: on Windows + Linux it installs CUDA/compilers from conda-forge, builds via the same `pip install .` path the feedstock uses, and runs an import smoke test (runners have no GPU — accuracy suites run locally, see TESTING.md). `.github/workflows/release.yml` runs on `v*` tags. `.github/workflows/matlab-multibuild.yml` builds `HIP` MEX binaries on Windows + Linux (same conda-forge CUDA toolchain as ci.yml, plus `matlab-actions/setup-matlab`), packages the MATLAB toolbox via `src/MATLAB/build-scripts/packageToolboxCI.m` (version single-sourced from `pyproject.toml`; `HydraImageProcessor.prj` is only for interactive packaging), and attaches the `.mltbx` to published GitHub Releases. diff --git a/src/MATLAB/build-scripts/packageToolboxCI.m b/src/MATLAB/build-scripts/packageToolboxCI.m new file mode 100644 index 0000000..6874744 --- /dev/null +++ b/src/MATLAB/build-scripts/packageToolboxCI.m @@ -0,0 +1,66 @@ +function packageToolboxCI(version) +% packageToolboxCI Package the Hydra Image Processor toolbox (CI entry point). +% +% packageToolboxCI(VERSION) builds "Hydra Image Processor.mltbx" in +% src/MATLAB from the +HIP package and CudaMexTester.m. The MEX binaries +% (HIP.mexw64 / HIP.mexa64) must already be present in +HIP/@Cuda. +% +% VERSION is single-sourced from pyproject.toml by the workflow; the GUID +% below matches HydraImageProcessor.prj so installs upgrade in place. The +% .prj itself is not used here because it embeds machine-specific absolute +% paths - it remains the entry point for interactive packaging in the IDE. + + if nargin < 1 || isempty(version) + error('packageToolboxCI:noVersion', 'A toolbox version string is required.'); + end + + scriptDir = fileparts(mfilename('fullpath')); + matlabDir = fileparts(scriptDir); % src/MATLAB + repoRoot = fileparts(fileparts(matlabDir)); % repository root + + guid = '8c91e1c9-d55d-4c1e-96df-875fd68e82cd'; % keep in sync with HydraImageProcessor.prj + opts = matlab.addons.toolbox.ToolboxOptions(matlabDir, guid); + + opts.ToolboxName = 'Hydra Image Processor'; + opts.ToolboxVersion = version; + opts.AuthorName = 'Eric Wait'; + opts.AuthorEmail = 'eric@waitphoto.com'; + opts.Summary = ['Hydra Image Processor handles large 1D to 5D image data by ' ... + 'optimally chunking across GPUs, ensuring energy-insulated boundaries.']; + opts.Description = sprintf(['Hydra Image Processor (Hydra) is a collection of signal filters ' ... + 'for image analysis that can handle 1-5 dimensional data (x, y, z, ' ... + 'channels, time), efficiently processing data larger than GPU memory by ' ... + 'optimally chunking it and distributing higher-dimensional chunks across ' ... + 'multiple GPUs, while ensuring energy-insulated boundary conditions to ' ... + 'prevent energy leakage at the edges.\n\nSee https://hydraimageprocessor.com/']); + opts.ToolboxImageFile = fullfile(repoRoot, 'logo.png'); + + % Only the user-facing package ships; tests, perf scripts, and the build + % scripts stay out of the toolbox (mirrors the .prj exclude list). + hipDir = fullfile(matlabDir, '+HIP'); + opts.ToolboxFiles = [string(hipDir); string(fullfile(matlabDir, 'CudaMexTester.m'))]; + opts.ToolboxMatlabPath = string(matlabDir); + + % MEX binaries are only guaranteed to load on the MATLAB release they + % were built with or newer; the workflow builds with this same release. + opts.MinimumMatlabRelease = 'R2024b'; + + opts.SupportedPlatforms.Win64 = true; + opts.SupportedPlatforms.Glnxa64 = true; + opts.SupportedPlatforms.Maci64 = false; + opts.SupportedPlatforms.MatlabOnline = false; + + opts.OutputFile = fullfile(matlabDir, 'Hydra Image Processor.mltbx'); + + % Fail loudly if the MEX binaries are missing - a toolbox without them + % would silently fall back to the CPU implementations for every call. + mexFiles = {fullfile(hipDir, '@Cuda', 'HIP.mexw64'), fullfile(hipDir, '@Cuda', 'HIP.mexa64')}; + for i = 1:numel(mexFiles) + if ~exist(mexFiles{i}, 'file') + error('packageToolboxCI:missingMex', 'Missing MEX binary: %s', mexFiles{i}); + end + end + + matlab.addons.toolbox.packageToolbox(opts); + fprintf('Packaged %s (version %s)\n', opts.OutputFile, version); +end From f9359d58c779e6d6c81f12788a2651df1cdb5d31 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 21:56:45 -0500 Subject: [PATCH 2/6] ci: run autoInstallMex through matlab-actions instead of the post-build hook Both platforms compiled and linked HydraMex, then failed with License Manager Error -1: the matlab-actions self-licensing on hosted runners only covers MATLAB launched through their actions, not a matlab -batch spawned by the CMake POST_BUILD step. Gate that step behind a new HYDRA_MEX_AUTOINSTALL option (default ON, so local builds keep the old behavior) and have the workflow run autoInstallMex via matlab-actions/run-command after the build. Co-Authored-By: Claude Fable 5 --- .github/workflows/matlab-multibuild.yml | 14 ++++++++++++-- src/c/Mex/autoBuildMex.cmake | 9 +++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml index 511b99f..f5d4fbd 100644 --- a/.github/workflows/matlab-multibuild.yml +++ b/.github/workflows/matlab-multibuild.yml @@ -100,11 +100,10 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DHYDRA_MODULE_NAME=HIP \ -DHYDRA_BUILD_TESTS=OFF \ + -DHYDRA_MEX_AUTOINSTALL=OFF \ -DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCHS}" \ -DMatlab_ROOT_DIR="$(tr -d '\r' < matlabroot.txt)" - # The post-build step (autoBuildMex.cmake) launches MATLAB to copy the - # MEX binary into src/MATLAB/+HIP/@Cuda and regenerate the .m wrappers. - name: Build MEX run: | set -o pipefail @@ -114,6 +113,17 @@ jobs: exit 1 fi + # Copy the MEX into src/MATLAB/+HIP/@Cuda and regenerate the .m + # wrappers. This must go through run-command: a MATLAB launched + # directly (e.g. by the CMake post-build step, disabled above) is not + # licensed on hosted runners. + - name: Install MEX and regenerate wrappers + uses: matlab-actions/run-command@v2 + with: + command: >- + cd(fullfile(getenv('GITHUB_WORKSPACE'), 'src', 'MATLAB', 'build-scripts')); + autoInstallMex('HIP', fullfile(getenv('GITHUB_WORKSPACE'), 'build', 'src', 'c', 'Mex', ['HIP.' mexext])); + - name: Verify output run: | MEXT=$([ "$RUNNER_OS" = "Windows" ] && echo mexw64 || echo mexa64) diff --git a/src/c/Mex/autoBuildMex.cmake b/src/c/Mex/autoBuildMex.cmake index a62bb23..ea2a414 100644 --- a/src/c/Mex/autoBuildMex.cmake +++ b/src/c/Mex/autoBuildMex.cmake @@ -1,3 +1,12 @@ +# CI runners can't license a MATLAB launched directly by the build (the +# matlab-actions self-licensing only covers MATLAB run through their actions), +# so the workflow disables this and runs autoInstallMex via run-command instead. +option(HYDRA_MEX_AUTOINSTALL "Run MATLAB post-build to install the MEX and regenerate the .m wrappers" ON) + +if (NOT HYDRA_MEX_AUTOINSTALL) + return() +endif() + add_custom_command(TARGET HydraMex POST_BUILD COMMAND ${Matlab_MAIN_PROGRAM} -nosplash $<$>:-nodisplay> -nodesktop $<$:$<$:-wait>> $,-r,-batch> "autoInstallMex('${HYDRA_MODULE_NAME}', '$')$<$:$exit>" From 2cfd746dab74bef74ed4aa50512c742d60688939 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 22:05:22 -0500 Subject: [PATCH 3/6] fix: let -DHYDRA_MODULE_NAME override the default again The plain set(HYDRA_MODULE_NAME Hydra) silently shadowed the cache variable, so -DHYDRA_MODULE_NAME=HIP (documented, and used by the MATLAB CI to build HIP.mex*) has been a no-op since the v4.0 build rework - the MEX always linked as Hydra.mex*. Declare it as a CACHE STRING so the command line wins and the default stays Hydra. Caught by the rebuilt MATLAB workflow: autoInstallMex could not find build/src/c/Mex/HIP.mexa64 because the file was Hydra.mexa64. Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a6689f..b21e4a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,9 @@ endif() project(HydraImageProcessor LANGUAGES C CXX CUDA) -set(HYDRA_MODULE_NAME "Hydra") +# CACHE so -DHYDRA_MODULE_NAME=HIP (used by the MATLAB CI) can override it; +# a plain set() would silently shadow the command-line value. +set(HYDRA_MODULE_NAME "Hydra" CACHE STRING "Name of the built extension module") # Tests build by default for local development; wheel/feedstock builds pass # -DHYDRA_BUILD_TESTS=OFF to skip the standalone C++ accuracy executable. From e2686105a061fa73140a49bce6c4e11fe096eb7b Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 22:13:53 -0500 Subject: [PATCH 4/6] ci: pin GCC 12 on Linux so the MEX loads against MATLAB's libstdc++ MATLAB resolves a MEX file's libstdc++ from its own bundled copy, and R2024b's rejects binaries built with GCC 14 (CXXABI_1.3.15 not found) - conda-forge's current default cxx-compiler. Build with GCC 12 so the MEX loads on R2024b and newer for end users as well, not just in CI. Co-Authored-By: Claude Fable 5 --- .github/workflows/matlab-multibuild.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml index f5d4fbd..269eb15 100644 --- a/.github/workflows/matlab-multibuild.yml +++ b/.github/workflows/matlab-multibuild.yml @@ -78,9 +78,13 @@ jobs: - name: Install build dependencies run: conda install -y -c conda-forge cmake ninja cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=13.*" + # GCC is pinned to 12: MATLAB loads MEX files against its own bundled + # libstdc++, and R2024b's rejects binaries built with newer GCC ABIs + # (CXXABI_1.3.15 from GCC 14 is not found). GCC 12 keeps the MEX + # loadable on R2024b+ everywhere, not just on this runner. - name: Install host compiler for nvcc (Linux) if: runner.os == 'Linux' - run: conda install -y -c conda-forge c-compiler cxx-compiler + run: conda install -y -c conda-forge "gcc_linux-64=12" "gxx_linux-64=12" - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 From 69625d375c44ba744a7a7a2d036d19914916aac4 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 22:25:27 -0500 Subject: [PATCH 5/6] ci: pull logo.png from LFS before packaging the toolbox The toolbox image is LFS-tracked, so the lfs:false checkout leaves a pointer file and ToolboxOptions rejects it as an unrecognized image format. Fetch just that file - the remaining LFS assets are large test images the packaging job does not need. Co-Authored-By: Claude Fable 5 --- .github/workflows/matlab-multibuild.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml index 269eb15..d083c80 100644 --- a/.github/workflows/matlab-multibuild.yml +++ b/.github/workflows/matlab-multibuild.yml @@ -175,6 +175,12 @@ jobs: with: lfs: false + # logo.png (the toolbox image) is LFS-tracked; fetch just that one + # file rather than every LFS asset (the test images are large). + - name: Fetch the toolbox image from LFS + shell: bash + run: git lfs pull --include=logo.png + - name: Download Windows package directory uses: actions/download-artifact@v4 with: From 149cfe24f196cfaee488ccd435f1069f14e3924e Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 22:50:17 -0500 Subject: [PATCH 6/6] test: add a no-GPU MATLAB smoke suite and drop the stale MEX binaries - Remove the LFS-tracked pre-4.0 MEX binaries (HIP.mexw64, HIP.mexa64, Hydra.mexw64) and gitignore locally built ones plus the packaged .mltbx - users get binaries from the CI-built toolbox, not git. - Add Test.SmokeTest (matlab.unittest, no GPU): the MEX must load, Info() must return the command table, DeviceCount must return 0 gracefully, and the generated wrappers must agree with the command table. matlab-multibuild.yml now runs it on both platforms after regenerating the wrappers. - Fix AccuracyTest GPU detection: it called Hydra.DeviceCount(), which does not exist (DeviceCount has no package-level wrapper), so devCount was always 0 and the whole suite silently skipped as Incomplete even on a GPU box. Use HIP.Cuda.DeviceCount(). - Document the MATLAB steps in TESTING.md (build with -DHYDRA_MODULE_NAME=HIP since the wrappers hard-code HIP.Cuda; watch for Incomplete results) and CLAUDE.md. - Simplify the Windows artifact upload: with no checked-in mexa64 pointer to dodge, upload the whole +HIP directory. Co-Authored-By: Claude Fable 5 --- .github/workflows/matlab-multibuild.yml | 18 +++++--- .gitignore | 8 ++++ CLAUDE.md | 8 +++- TESTING.md | 21 ++++++++- src/MATLAB/+HIP/@Cuda/HIP.mexa64 | 3 -- src/MATLAB/+HIP/@Cuda/HIP.mexw64 | 3 -- src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 | 3 -- src/MATLAB/+Test/AccuracyTest.m | 20 +++------ src/MATLAB/+Test/SmokeTest.m | 59 +++++++++++++++++++++++++ 9 files changed, 109 insertions(+), 34 deletions(-) delete mode 100755 src/MATLAB/+HIP/@Cuda/HIP.mexa64 delete mode 100644 src/MATLAB/+HIP/@Cuda/HIP.mexw64 delete mode 100644 src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 create mode 100644 src/MATLAB/+Test/SmokeTest.m diff --git a/.github/workflows/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml index d083c80..41e4771 100644 --- a/.github/workflows/matlab-multibuild.yml +++ b/.github/workflows/matlab-multibuild.yml @@ -128,6 +128,16 @@ jobs: cd(fullfile(getenv('GITHUB_WORKSPACE'), 'src', 'MATLAB', 'build-scripts')); autoInstallMex('HIP', fullfile(getenv('GITHUB_WORKSPACE'), 'build', 'src', 'c', 'Mex', ['HIP.' mexext])); + # Structural tests that need no GPU: the MEX must load, DeviceCount + # must return 0 gracefully, and the regenerated wrappers must agree + # with the MEX's command table. + - name: MATLAB smoke test (no GPU required) + uses: matlab-actions/run-command@v2 + with: + command: >- + addpath(fullfile(getenv('GITHUB_WORKSPACE'), 'src', 'MATLAB')); + assertSuccess(runtests('Test.SmokeTest')); + - name: Verify output run: | MEXT=$([ "$RUNNER_OS" = "Windows" ] && echo mexw64 || echo mexa64) @@ -145,17 +155,13 @@ jobs: fi # Windows contributes the whole package directory (fresh wrappers + - # HIP.mexw64). The checked-out HIP.mexa64 is excluded because with - # lfs:false it is only a Git LFS pointer; the Linux job supplies the - # real one. + # HIP.mexw64); the Linux job supplies HIP.mexa64. - name: Upload package directory (Windows) if: runner.os == 'Windows' uses: actions/upload-artifact@v4 with: name: hip-package-windows - path: | - src/MATLAB/+HIP - !src/MATLAB/+HIP/@Cuda/HIP.mexa64 + path: src/MATLAB/+HIP if-no-files-found: error - name: Upload MEX (Linux) diff --git a/.gitignore b/.gitignore index d3d3117..79820d5 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,11 @@ conda-build-dir/ /dist/ *.egg-info/ *.whl + +# MEX binaries dropped into the MATLAB packages by local HydraMex builds +# (users get them from the CI-built toolbox, not from git) +/src/MATLAB/+HIP/@Cuda/*.mex* +/src/MATLAB/+Hydra/@Cuda/*.mex* + +# Packaged toolbox (built by packageToolboxCI.m or the .prj) +/src/MATLAB/Hydra Image Processor.mltbx diff --git a/CLAUDE.md b/CLAUDE.md index d0889ef..548fa24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,8 +47,12 @@ cd src/Python && python test_package.py # Python TIFF accuracy suite (needs a GPU + tifffile; THE release gate, see TESTING.md) python src/Python/Test/test_accuracy.py -# MATLAB accuracy tests (matlab.unittest; compares against test_data/*.tif) -matlab -batch "results = runtests('src/MATLAB/+Test/AccuracyTest.m')" +# MATLAB smoke test (no GPU needed; CI runs this after building the MEX) +matlab -batch "addpath('src/MATLAB'); assertSuccess(runtests('Test.SmokeTest'))" + +# MATLAB accuracy tests (matlab.unittest; needs a GPU + MicroscopeData/ImUtils utilities; +# build HydraMex with -DHYDRA_MODULE_NAME=HIP first - the wrappers hard-code HIP.Cuda) +matlab -batch "addpath('src/MATLAB'); assertSuccess(runtests('Test.AccuracyTest'))" ``` - C++ tests use a minimal custom framework (`TEST_ASSERT`/`RUN_TEST` in `src/c/test_back/test_accuracy.cpp`) and call the generated `CudaCall_::run(...)` entry points directly. To run a single test, comment out other `RUN_TEST` lines or add a new `RUN_TEST` — there is no filter flag. diff --git a/TESTING.md b/TESTING.md index e7e9e4f..95c9c8b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -30,17 +30,34 @@ flow this feeds into. `python src/Python/Test/test_accuracy.py` → `Test Summary: PASSED`. **This is the gate for tagging.** - [ ] Spot-check one op end-to-end (e.g. `gaussian` on a small array) and `device_count() >= 1`. +- [ ] Build the MEX with the `HIP` module name (the wrappers hard-code `HIP.Cuda`, so the + default `Hydra` name would silently fall back to the CPU path): + `cmake -S . -B build -DHYDRA_MODULE_NAME=HIP` then + `cmake --build build --config Release --target HydraMex` + (the post-build MATLAB step installs into `src/MATLAB/+HIP`). +- [ ] MATLAB smoke test (also run by CI, no GPU needed): + `matlab -batch "addpath('src/MATLAB'); assertSuccess(runtests('Test.SmokeTest'))"`. +- [ ] MATLAB accuracy suite (needs a GPU, the LFS test images, and the + MicroscopeData/ImUtils utilities on the MATLAB path): + `matlab -batch "addpath('src/MATLAB'); assertSuccess(runtests('Test.AccuracyTest'))"`. + **Check the summary for `Incomplete` results** — an assumption failure (no GPU + detected, missing test data or utilities) skips tests without failing them, which + looks green but verified nothing. ## D. Working-tree hygiene - [ ] After building, `git status` is still clean — the in-source `Hydra.pyd` is ignored: `git check-ignore src/Python/hydra_image_processor/Hydra.pyd` prints the path. -- [ ] `git log --oneline` shows the build/ci/docs commits; the `Hydra.mexw64` change remains - separate (commit or discard it intentionally). +- [ ] After building `HydraMex`, the MEX binary is ignored + (`git check-ignore src/MATLAB/+HIP/@Cuda/HIP.mexw64` prints the path); regenerated + `.m` wrappers may show a diff — commit or discard that intentionally. ## E. CI workflows (GitHub) - [ ] Push the working branch → `ci.yml` runs and **both** `ubuntu-latest` and `windows-latest` jobs build and pass the import smoke test (`__version__ == 4.0.0`, `HIP is Hydra`). - [ ] Open a PR to `main` → the gate runs on the PR. +- [ ] `matlab-multibuild.yml` is green when the PR touches `src/c/**`, `src/MATLAB/**`, or + the build files: MEX builds on both platforms, the no-GPU MATLAB smoke test passes, + and the `HydraImageProcessor.mltbx` artifact is produced. - [ ] `release.yml` version guard sanity: the tag (minus `v`) must equal the `pyproject.toml` version. (An `rc` tag won't match unless `pyproject` is set to that rc.) diff --git a/src/MATLAB/+HIP/@Cuda/HIP.mexa64 b/src/MATLAB/+HIP/@Cuda/HIP.mexa64 deleted file mode 100755 index b06ddea..0000000 --- a/src/MATLAB/+HIP/@Cuda/HIP.mexa64 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa5dc2788e5c0180c4bc1448d2392728dfabd6f53fb9025e70c287b336fef981 -size 12989776 diff --git a/src/MATLAB/+HIP/@Cuda/HIP.mexw64 b/src/MATLAB/+HIP/@Cuda/HIP.mexw64 deleted file mode 100644 index 35e6b91..0000000 --- a/src/MATLAB/+HIP/@Cuda/HIP.mexw64 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61fc9a6d7245c1d012cf6c5b21dfce41db10d5f739651aa09d30582275c6a8e1 -size 6216704 diff --git a/src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 b/src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 deleted file mode 100644 index 3dc7331..0000000 --- a/src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5c81574d8a5edca96aa6555166f863d5c4007c30b552c243ba7a30ec759b0a46 -size 13505024 diff --git a/src/MATLAB/+Test/AccuracyTest.m b/src/MATLAB/+Test/AccuracyTest.m index f0a346b..810f851 100644 --- a/src/MATLAB/+Test/AccuracyTest.m +++ b/src/MATLAB/+Test/AccuracyTest.m @@ -10,25 +10,15 @@ methods(TestClassSetup) function setupData(testCase) - % Check for GPU availability + % Check for GPU availability. DeviceCount exists only on the + % class (autoInstallMex generates no package-level wrapper for + % it), and even the +Hydra wrappers delegate to HIP.Cuda. try - devCount = Hydra.DeviceCount(); - if isstruct(devCount) || iscell(devCount) - % Sometimes it returns stats or tuple, handle loosely - % In Python wrapper it returns (count, stats) or count - % Let's assume if it runs, we check count - if iscell(devCount) - devCount = devCount{1}; - end - end - - % If devCount is struct/stats, maybe count is length? - % Let's rely on documentation/header: SCR_CMD_NOPROC(DeviceCount, SCR_PARAMS(SCR_OUTPUT(SCR_SCALAR(int32_t), numCudaDevices), SCR_OUTPUT(SCR_STRUCT, memStats))) - % Mex returns [num, stats]. + devCount = HIP.Cuda.DeviceCount(); catch devCount = 0; end - + testCase.assumeTrue(isnumeric(devCount) && devCount > 0, ... 'No CUDA devices found. Skipping accuracy tests.'); diff --git a/src/MATLAB/+Test/SmokeTest.m b/src/MATLAB/+Test/SmokeTest.m new file mode 100644 index 0000000..6c2689f --- /dev/null +++ b/src/MATLAB/+Test/SmokeTest.m @@ -0,0 +1,59 @@ +classdef SmokeTest < matlab.unittest.TestCase +% Structural checks for the compiled HIP MEX interface that need no GPU. +% CI (matlab-multibuild.yml) runs this on both platforms right after +% building the MEX and regenerating the wrappers; GPU correctness is +% covered by AccuracyTest.m, which must run on a machine with a CUDA +% device (see TESTING.md). + + properties + HipDir + end + + methods (TestClassSetup) + function locateMex(testCase) + matlabDir = fileparts(fileparts(which('Test.SmokeTest'))); + testCase.HipDir = fullfile(matlabDir, '+HIP'); + mexFile = fullfile(testCase.HipDir, '@Cuda', ['HIP.' mexext]); + testCase.fatalAssertTrue(isfile(mexFile), sprintf( ... + ['Compiled MEX missing: %s. Build the HydraMex target with ' ... + '-DHYDRA_MODULE_NAME=HIP first.'], mexFile)); + end + end + + methods (Test) + function mexLoadsAndListsCommands(testCase) + info = HIP.Cuda.Info(); + testCase.verifyClass(info, 'struct'); + testCase.verifyNotEmpty(info); + testCase.verifyTrue(isfield(info, 'command'), ... + 'Info() entries should have a command field'); + end + + function deviceCountRunsWithoutGpu(testCase) + % Must not error on a machine with no CUDA device (returns 0). + n = HIP.Cuda.DeviceCount(); + testCase.verifyTrue(isnumeric(n) && isscalar(n), ... + 'DeviceCount should return a numeric scalar'); + testCase.verifyGreaterThanOrEqual(double(n), 0); + end + + function wrappersMatchCommandTable(testCase) + % Every command the MEX reports must have a generated class + % method and, unless excluded by autoInstallMex, a package-level + % wrapper (the GPU-try/CPU-fallback shims). + noWrapper = {'Cuda', 'DeviceCount', 'DeviceStats'}; + info = HIP.Cuda.Info(); + for i = 1:numel(info) + cmd = info(i).command; + classFile = fullfile(testCase.HipDir, '@Cuda', [cmd '.m']); + testCase.verifyTrue(isfile(classFile), ... + ['Missing generated class method: ' classFile]); + if ~any(strcmp(noWrapper, cmd)) + wrapFile = fullfile(testCase.HipDir, [cmd '.m']); + testCase.verifyTrue(isfile(wrapFile), ... + ['Missing package wrapper: ' wrapFile]); + end + end + end + end +end