Merge pull request #19 from RubixML/feature/iterator-improved #51
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: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| # Cancel in-progress runs on the same branch to save CI minutes. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ========================================================================= | |
| # Linux matrix: Ubuntu (22.04 / 24.04) × PHP (8.1–8.5) × CPU / CUDA | |
| # 20 combinations. | |
| # ========================================================================= | |
| test: | |
| name: "PHP ${{ matrix.php }} / ${{ matrix.os }} / ${{ matrix.cuda && 'CUDA' || 'CPU' }}" | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-22.04, ubuntu-24.04] | |
| php: ['8.1', '8.2', '8.3', '8.4', '8.5'] | |
| cuda: [false, true] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup PHP ${{ matrix.php }} | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php }} | |
| extensions: gd | |
| tools: none | |
| coverage: none | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y \ | |
| build-essential \ | |
| autoconf \ | |
| liblapacke-dev \ | |
| libopenblas-dev \ | |
| libjpeg-dev \ | |
| libpng-dev \ | |
| libwebp-dev \ | |
| libfreetype6-dev \ | |
| libxpm-dev | |
| # nvidia-cuda-toolkit provides libcublas + development headers on Ubuntu 22/24/26. | |
| # GPU tests are skipped at runtime (no hardware on standard runners); this job | |
| # validates that the CUDA build path compiles and links correctly. | |
| - name: Install CUDA toolkit | |
| if: matrix.cuda | |
| run: sudo apt-get install -y nvidia-cuda-toolkit | |
| - name: Build extension | |
| run: | | |
| phpize | |
| ./configure ${{ matrix.cuda && '--with-cuda' || '' }} | |
| make -j$(nproc) | |
| # When cublas is detected, the standard PHP build links ndarray.so without | |
| # the .cu objects (cuda_math.cu / cuda_dnn.cu) — only install-cuda does that, | |
| # and it targets the system extension dir. cuda-modules rebuilds everything | |
| # with nvcc and drops the result into modules/ndarray.so so that make test | |
| # picks it up without a system install. | |
| - name: Link CUDA objects into extension | |
| if: matrix.cuda | |
| run: | | |
| if grep -q "HAVE_CUBLAS 1" config.h; then | |
| echo "cublas detected — rebuilding modules/ndarray.so with CUDA objects" | |
| make cuda-modules | |
| else | |
| echo "::warning::cublas not found; extension built without GPU support" | |
| fi | |
| # make test generates tmp-php.ini from the system ini scan dir (including gd.so), | |
| # then loads the extension directly from ./modules/ — no `make install` needed. | |
| # REPORT_EXIT_STATUS=1 is required: without it run-tests.php always exits 0. | |
| - name: Run tests | |
| run: REPORT_EXIT_STATUS=1 make test TESTS="-q --show-diff" | |
| - name: Upload failure diffs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "diffs-php${{ matrix.php }}-${{ matrix.os }}-${{ matrix.cuda && 'cuda' || 'cpu' }}" | |
| path: "**/*.diff" | |
| if-no-files-found: ignore | |
| # ========================================================================= | |
| # macOS matrix: macos-14 / macos-15 (both Apple Silicon ARM64) × PHP 8.1–8.5. | |
| # 10 combinations. CPU only — CUDA is not available on macOS. | |
| # | |
| # Apple clang on ARM64 falls through the long-double fallback in | |
| # src/ndarray_types.h (no __float128, no libquadmath). The HAVE_AVX2 macro | |
| # is left undefined on non-x86 hosts by config.m4, so the AVX2 kernels are | |
| # excluded from the build. OpenBLAS + LAPACK come from Homebrew. | |
| # ========================================================================= | |
| macos-test: | |
| name: "PHP ${{ matrix.php }} / ${{ matrix.os }}" | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-14, macos-15] | |
| php: ['8.1', '8.2', '8.3', '8.4', '8.5'] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup PHP ${{ matrix.php }} | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php }} | |
| tools: none | |
| coverage: none | |
| # Homebrew ships its own openblas + lapack kegs that include LAPACKE | |
| # headers and the libopenblas.dylib symbol set we need (cblas_sdot, | |
| # LAPACKE_sgesdd). Apple's vecLib/Accelerate framework would be lighter | |
| # weight but doesn't export the LAPACKE_* C bindings. | |
| - name: Install OpenBLAS + LAPACK (Homebrew) | |
| run: | | |
| brew update | |
| brew install openblas lapack autoconf | |
| - name: Build extension | |
| run: | | |
| OPENBLAS_PREFIX=$(brew --prefix openblas) | |
| LAPACK_PREFIX=$(brew --prefix lapack) | |
| export CPPFLAGS="-I${OPENBLAS_PREFIX}/include -I${LAPACK_PREFIX}/include ${CPPFLAGS}" | |
| export LDFLAGS="-L${OPENBLAS_PREFIX}/lib -L${LAPACK_PREFIX}/lib ${LDFLAGS}" | |
| export LIBRARY_PATH="${OPENBLAS_PREFIX}/lib:${LAPACK_PREFIX}/lib:${LIBRARY_PATH}" | |
| export PKG_CONFIG_PATH="${OPENBLAS_PREFIX}/lib/pkgconfig:${LAPACK_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}" | |
| phpize | |
| ./configure | |
| make -j$(sysctl -n hw.ncpu) | |
| - name: Run tests | |
| run: REPORT_EXIT_STATUS=1 make test TESTS="-q --show-diff" | |
| - name: Upload failure diffs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "diffs-php${{ matrix.php }}-${{ matrix.os }}" | |
| path: "**/*.diff" | |
| if-no-files-found: ignore | |
| # ========================================================================= | |
| # Windows matrix: Windows Server 2022 / 2025 × PHP 8.1–8.5 × CPU / CUDA. | |
| # 20 combinations. CUDA jobs install the NVIDIA Toolkit via Jimver's action, | |
| # run build-cuda-windows.bat to pre-compile the .cu sources, then nmake | |
| # links them in. GitHub-hosted Windows runners have no GPU, so the GPU | |
| # PHPT tests SKIP at runtime — same behaviour as the Linux CUDA matrix. | |
| # ========================================================================= | |
| windows-test: | |
| name: "PHP ${{ matrix.php }} / ${{ matrix.os }} / ${{ matrix.cuda && 'CUDA' || 'CPU' }}" | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [windows-2022, windows-2025] | |
| php: ['8.1', '8.2', '8.3', '8.4', '8.5'] | |
| cuda: [false, true] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Sets %PHP_SDK_ROOT% / %PHP_PREFIX%, downloads the matching PHP devel | |
| # pack, configures the right Visual Studio toolset (vs16/vs17) and | |
| # exposes phpize.bat + nmake on PATH for subsequent steps. | |
| - name: Set up PHP build environment | |
| uses: php/setup-php-sdk@v0.12 | |
| id: php-sdk | |
| with: | |
| version: ${{ matrix.php }} | |
| arch: x64 | |
| ts: nts | |
| - name: Set up MSVC developer prompt | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| with: | |
| arch: x64 | |
| toolset: ${{ steps.php-sdk.outputs.toolset }} | |
| # OpenBLAS for Windows: download the upstream pre-built x64 release. | |
| # The ZIP contains include/, lib/libopenblas.lib + libopenblas.dll.a, | |
| # bin/libopenblas.dll — config.w32 finds these via --with-openblas-dir. | |
| - name: Cache OpenBLAS | |
| id: openblas-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: openblas | |
| key: openblas-0.3.30-windows-x64 | |
| - name: Download OpenBLAS prebuilt | |
| if: steps.openblas-cache.outputs.cache-hit != 'true' | |
| shell: pwsh | |
| run: | | |
| $url = "https://github.com/OpenMathLib/OpenBLAS/releases/download/v0.3.30/OpenBLAS-0.3.30-x64.zip" | |
| Invoke-WebRequest -Uri $url -OutFile openblas.zip | |
| New-Item -ItemType Directory -Path openblas -Force | Out-Null | |
| Expand-Archive -Path openblas.zip -DestinationPath openblas -Force | |
| # CUDA Toolkit (network installer with a minimal sub-package list so the | |
| # runner only downloads ~1.5 GB instead of the full 3+ GB). The action | |
| # exposes %CUDA_PATH% for the rest of the steps. cuDNN is not installed | |
| # by default — the GPU DNN tests will skip, matching the cudnn-less | |
| # build path on the Linux CUDA matrix runners. | |
| - name: Install CUDA Toolkit | |
| if: matrix.cuda | |
| uses: Jimver/cuda-toolkit@v0.2.19 | |
| id: cuda-toolkit | |
| with: | |
| cuda: '12.5.0' | |
| method: 'network' | |
| sub-packages: '["nvcc", "cublas", "cublas_dev", "cudart", "thrust", "curand", "curand_dev", "cusolver", "cusolver_dev", "cusparse", "nvjitlink", "visual_studio_integration"]' | |
| - name: phpize | |
| shell: cmd | |
| run: | | |
| phpize | |
| - name: configure (CPU) | |
| if: '!matrix.cuda' | |
| shell: cmd | |
| run: | | |
| configure --enable-ndarray --with-openblas-dir=%CD%\openblas --with-prefix=${{ steps.php-sdk.outputs.prefix }} | |
| - name: configure (CUDA) | |
| if: matrix.cuda | |
| shell: cmd | |
| env: | |
| PHP_PREFIX: ${{ steps.php-sdk.outputs.prefix }} | |
| run: | | |
| configure --enable-ndarray --with-openblas-dir=%CD%\openblas --with-cuda --with-cuda-dir="%CUDA_PATH%" --with-prefix=%PHP_PREFIX% | |
| # Pre-compile the .cu sources into ndarray_cuda.lib. Must run BEFORE | |
| # nmake, since config.w32 added ndarray_cuda.lib to LIBS_NDARRAY and | |
| # nmake will fail at link time if the file is missing. The .bat reads | |
| # %CUDA_PATH% (set by the cuda-toolkit action) and %PHP_PREFIX% (set | |
| # below) directly from the environment — passing them as CLI flags | |
| # would hit cmd.exe's quirk of treating `=` as an argument delimiter. | |
| - name: Build CUDA objects | |
| if: matrix.cuda | |
| shell: cmd | |
| env: | |
| PHP_PREFIX: ${{ steps.php-sdk.outputs.prefix }} | |
| run: | | |
| call build-cuda-windows.bat | |
| - name: nmake | |
| shell: cmd | |
| run: | | |
| nmake | |
| # The PHP test runner loads the freshly-built php_ndarray.dll from the | |
| # build output dir; the OpenBLAS DLL (and on CUDA builds the cublas / | |
| # cudart / cudnn DLLs) have to be co-located or on PATH for the loader | |
| # to resolve cblas_* / LAPACKE_* / cublas* / cudnn* at runtime. | |
| - name: Stage runtime DLLs next to extension | |
| shell: pwsh | |
| run: | | |
| # PHP on Windows calls SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) | |
| # early in startup, which kills PATH-based search for extension transitive | |
| # deps. After that, dependency resolution only looks in: | |
| # 1. The directory of php.exe (= %PHP_PREFIX% = php-bin\) | |
| # 2. C:\Windows\System32 | |
| # 3. Dirs added via AddDllDirectory() — PHP adds none. | |
| # So every runtime DLL must end up in php-bin\. The copies into x64\Release\ | |
| # are belt & suspenders for tools that bypass the hardened loader. | |
| $buildDir = if (Test-Path 'x64\Release') { 'x64\Release' } else { 'x64\Release_TS' } | |
| $phpBin = "${{ steps.php-sdk.outputs.prefix }}" | |
| Write-Host "=== Staging targets ===" | |
| Write-Host " buildDir = $buildDir" | |
| Write-Host " phpBin = $phpBin" | |
| Write-Host "=== Copying libopenblas.dll ===" | |
| Copy-Item openblas\bin\libopenblas.dll "$buildDir\" -Force -Verbose | |
| Copy-Item openblas\bin\libopenblas.dll "$phpBin\" -Force -Verbose | |
| if ($env:RUNNER_CUDA -eq 'true') { | |
| $cudaBin = Join-Path $env:CUDA_PATH 'bin' | |
| Write-Host "=== Source CUDA bin: $cudaBin ===" | |
| Get-ChildItem -Path $cudaBin -Filter '*.dll' | Select-Object -ExpandProperty Name | Sort-Object | Out-Host | |
| $filters = @( | |
| 'cublas64_*.dll', # direct link dep | |
| 'cudart64_*.dll', # direct link dep | |
| 'cublasLt64_*.dll', # transitive dep of cublas | |
| 'cusolver64_*.dll', # direct link dep | |
| 'curand64_*.dll', # direct link dep | |
| 'cusparse64_*.dll', # transitive dep of cusolver | |
| 'nvJitLink_*.dll', # transitive dep of cublas/cusolver on CUDA 12+ | |
| 'cudnn64_*.dll' # optional, when cuDNN sub-package was installed | |
| ) | |
| foreach ($filter in $filters) { | |
| $matched = Get-ChildItem -Path $cudaBin -Filter $filter -ErrorAction SilentlyContinue | |
| if (-not $matched) { | |
| Write-Host " $filter -> no match" | |
| continue | |
| } | |
| foreach ($f in $matched) { | |
| Write-Host " Copying $($f.Name) -> $buildDir\ and $phpBin\" | |
| Copy-Item -Path $f.FullName -Destination "$buildDir\" -Force | |
| Copy-Item -Path $f.FullName -Destination "$phpBin\" -Force | |
| } | |
| } | |
| } | |
| Write-Host "" | |
| Write-Host "=== Files in $phpBin (final) ===" | |
| Get-ChildItem -Path $phpBin -Filter '*.dll' | Select-Object -ExpandProperty Name | Sort-Object | Out-Host | |
| env: | |
| RUNNER_CUDA: ${{ matrix.cuda }} | |
| - name: Show extension's transitive DLL deps | |
| shell: pwsh | |
| run: | | |
| $buildDir = if (Test-Path 'x64\Release') { 'x64\Release' } else { 'x64\Release_TS' } | |
| $dll = Join-Path $buildDir 'php_ndarray.dll' | |
| Write-Host "=== dumpbin /dependents $dll ===" | |
| dumpbin /dependents $dll | |
| if ($env:RUNNER_CUDA -eq 'true') { | |
| # Also dump deps of the CUDA libs themselves — these are transitive | |
| # deps of php_ndarray.dll that the loader needs to resolve too. | |
| # If something here is missing from php-bin\ or System32, that's | |
| # the source of "module could not be found". | |
| $phpBin = "${{ steps.php-sdk.outputs.prefix }}" | |
| foreach ($name in @('cublas64_12.dll', 'cusolver64_11.dll', 'cudart64_12.dll', 'curand64_10.dll')) { | |
| $p = Join-Path $phpBin $name | |
| if (Test-Path $p) { | |
| Write-Host "" | |
| Write-Host "=== dumpbin /dependents $p ===" | |
| dumpbin /dependents $p | |
| } | |
| } | |
| } | |
| env: | |
| RUNNER_CUDA: ${{ matrix.cuda }} | |
| - name: Run tests | |
| shell: cmd | |
| env: | |
| REPORT_EXIT_STATUS: 1 | |
| run: | | |
| nmake test TESTS="-q --show-diff" | |
| - name: Upload failure diffs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "diffs-php${{ matrix.php }}-${{ matrix.os }}-win-x64-${{ matrix.cuda && 'cuda' || 'cpu' }}" | |
| path: "**/*.diff" | |
| if-no-files-found: ignore | |
| # ========================================================================= | |
| # Gate job — the ONLY required status check for branch protection. | |
| # ========================================================================= | |
| all-pass: | |
| name: All required tests passed | |
| needs: [test, macos-test, windows-test] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Check required matrix results | |
| run: | | |
| fail=0 | |
| for label in "linux:${{ needs.test.result }}" \ | |
| "macos:${{ needs.macos-test.result }}" \ | |
| "windows:${{ needs.windows-test.result }}"; do | |
| name="${label%%:*}" | |
| result="${label##*:}" | |
| echo "${name}: ${result}" | |
| if [[ "${result}" != "success" ]]; then | |
| fail=1 | |
| fi | |
| done | |
| if [[ $fail -ne 0 ]]; then | |
| echo "::error::One or more required matrices failed or were cancelled." | |
| exit 1 | |
| fi | |
| echo "All required Linux + macOS + Windows combinations passed." |