diff --git a/.github/actions/setup-randlapack-deps-windows/action.yml b/.github/actions/setup-randlapack-deps-windows/action.yml index 0353a588..71f3fb8b 100644 --- a/.github/actions/setup-randlapack-deps-windows/action.yml +++ b/.github/actions/setup-randlapack-deps-windows/action.yml @@ -1,11 +1,17 @@ name: setup-randlapack-deps-windows description: > - Builds/restores RandLAPACK's native Windows dependencies (oneMKL via vcpkg, - GoogleTest, Random123, BLAS++, LAPACK++) and exports their locations as - environment variables (MKLROOT, googletest_PREFIX, Random123_DIR, blaspp_DIR, - lapackpp_DIR). Requires an initialized MSVC environment (ilammy/msvc-dev-cmd). + Builds/restores RandLAPACK's native Windows dependencies (a BLAS/LAPACK + backend -- oneMKL from Intel's NuGet packages or OpenBLAS release binaries -- + plus GoogleTest, Random123, BLAS++, LAPACK++) and exports their locations as + environment variables (RANDNLA_BLAS_BACKEND, RANDNLA_BLAS_BIN, MKLROOT for + mkl, googletest_PREFIX, Random123_DIR, blaspp_DIR, lapackpp_DIR). Requires an + initialized MSVC environment (ilammy/msvc-dev-cmd). inputs: + blas-backend: + description: "BLAS/LAPACK backend for the dependency stack: mkl or openblas." + required: false + default: "mkl" sanitize-address: description: Build an AddressSanitizer-instrumented GoogleTest. required: false @@ -14,50 +20,67 @@ inputs: runs: using: composite steps: - # Caches live beside the workspace, keyed on the setup script so any recipe - # change invalidates them. - - name: cache oneMKL (vcpkg) + # Cache keys use MANUAL revision literals (-r), bumped here whenever + # the corresponding recipe in setup.ps1 changes. Do NOT key on + # hashFiles(): it resolves paths relative to GITHUB_WORKSPACE, and the + # install-script workflow checks this repo out under RandLAPACK\, where + # the glob matches nothing and hashFiles() silently returns "" -- the two + # workflows then read/write DIFFERENT caches while appearing to share. + - name: cache oneMKL (NuGet) + if: inputs.blas-backend == 'mkl' + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}\..\windows-deps\onemkl-2025.2.0.627 + key: windows-nuget-intel-mkl-2026.1.0.226-r1 + + - name: cache OpenBLAS (release binaries) + if: inputs.blas-backend == 'openblas' uses: actions/cache@v4 with: - path: ${{ github.workspace }}\..\windows-deps\vcpkg-installed - key: windows-vcpkg-intel-mkl-1-${{ hashFiles('.github/actions/setup-randlapack-deps-windows/setup.ps1') }} + path: ${{ github.workspace }}\..\windows-deps\openblas-0.3.34 + key: windows-openblas-0.3.34-r1 - name: cache GoogleTest if: inputs.sanitize-address != 'true' uses: actions/cache@v4 with: path: ${{ github.workspace }}\..\windows-deps\googletest-install - key: windows-msvc-googletest-1.17.0-1-${{ hashFiles('.github/actions/setup-randlapack-deps-windows/setup.ps1') }} + key: windows-msvc-googletest-1.18.0-ninja-r1 - name: cache GoogleTest (ASan) if: inputs.sanitize-address == 'true' uses: actions/cache@v4 with: path: ${{ github.workspace }}\..\windows-deps\googletest-asan-install - key: windows-msvc-googletest-asan-1.17.0-1-${{ hashFiles('.github/actions/setup-randlapack-deps-windows/setup.ps1') }} + key: windows-msvc-googletest-asan-1.18.0-ninja-r1 - name: cache Random123 uses: actions/cache@v4 with: path: ${{ github.workspace }}\..\windows-deps\Random123-install - key: windows-random123-1-${{ hashFiles('.github/actions/setup-randlapack-deps-windows/setup.ps1') }} + key: windows-random123-1.14.0-r1 - name: cache BLAS++ uses: actions/cache@v4 with: - path: ${{ github.workspace }}\..\windows-deps\blaspp-install - key: windows-msvc-blaspp-remove-symv-debug-print-ilp64-sequential-1-${{ hashFiles('.github/actions/setup-randlapack-deps-windows/setup.ps1') }} + path: ${{ github.workspace }}\..\windows-deps\blaspp-${{ inputs.blas-backend }}-install + key: windows-msvc-blaspp-upstream-3057185-${{ inputs.blas-backend }}-ninja-r2 - name: cache LAPACK++ uses: actions/cache@v4 with: - path: ${{ github.workspace }}\..\windows-deps\lapackpp-install - key: windows-msvc-lapackpp-msvc-direct-includes-ilp64-sequential-1-${{ hashFiles('.github/actions/setup-randlapack-deps-windows/setup.ps1') }} + path: ${{ github.workspace }}\..\windows-deps\lapackpp-${{ inputs.blas-backend }}-install + key: windows-msvc-lapackpp-upstream-40b9d0d-${{ inputs.blas-backend }}-ninja-r2 - name: build missing dependencies shell: pwsh run: | $sanitizeAddress = '${{ inputs.sanitize-address }}' -eq 'true' + # -Yes: never prompt. Prompts are already skipped when stdin is not + # a terminal, but stating it makes the intent explicit and survives + # any future change to that detection. & "$env:GITHUB_ACTION_PATH/setup.ps1" ` -DependencyRoot "${env:GITHUB_WORKSPACE}\..\windows-deps" ` + -Backend '${{ inputs.blas-backend }}' ` + -Yes ` -SanitizeAddress:$sanitizeAddress diff --git a/.github/actions/setup-randlapack-deps-windows/setup.ps1 b/.github/actions/setup-randlapack-deps-windows/setup.ps1 index 174b0d1e..d0cf2579 100644 --- a/.github/actions/setup-randlapack-deps-windows/setup.ps1 +++ b/.github/actions/setup-randlapack-deps-windows/setup.ps1 @@ -1,13 +1,18 @@ # Builds and installs RandLAPACK's native Windows dependencies: -# - oneMKL (via vcpkg, ILP64 + sequential DLL set) -# - GoogleTest v1.17.0 +# - a BLAS/LAPACK backend (-Backend): oneMKL (default; discovered from an +# installed oneAPI or fetched from Intel's NuGet packages, ILP64 + +# sequential), OpenBLAS (official release binaries, LP64), or +# custom/bring-your-own libraries +# - GoogleTest v1.18.0 # - Random123 (headers only) -# - BLAS++ from BallisticLA/blaspp, branch remove-symv-debug-print -# - LAPACK++ from BallisticLA/lapackpp, branch msvc-direct-includes +# - BLAS++ from icl-utk-edu/blaspp (upstream), pinned by commit +# - LAPACK++ from icl-utk-edu/lapackpp (upstream), pinned by commit # -# The BallisticLA branches carry the two one-line MSVC fixes that upstream -# icl-utk-edu has not merged yet (blaspp PR #132, lapackpp PR #87). Once those -# merge, both clones below can move back to upstream master. +# Everything is fetched from its canonical upstream and pinned to an immutable +# ref. The two one-line MSVC fixes this build needs merged upstream on +# 2026-08-06 (icl-utk-edu/blaspp#132, icl-utk-edu/lapackpp#87), so the forks these +# once pointed at are no longer needed. The pins are commits rather than tags +# only because the latest release of each, v2025.05.28, predates those merges. # # Every step is idempotent: work already present under -DependencyRoot (for # example, restored from a CI cache) is left alone. Run from an MSVC developer @@ -18,12 +23,50 @@ param( [Parameter(Mandatory = $true)] [string]$DependencyRoot, - [string]$VcpkgExecutable = "", + # BLAS/LAPACK backend. "mkl" (default): oneMKL, ILP64 + sequential, + # discovered from an installed oneAPI, else downloaded (see -NoDownload). + # "openblas": official OpenBLAS release binaries, LP64. + # "custom": bring your own libraries via -BlasLibraries (anything + # BLAS++/LAPACK++ can link, e.g. AMD AOCL). + [ValidateSet("mkl", "openblas", "custom")] + [string]$Backend = "mkl", + + # Refuse to download a backend that was not found locally, and fail + # instead. The default is to download, which is the ordinary Windows + # practice (no system prefix exists for third-party libraries, so + # per-project acquisition via vcpkg/NuGet/release archives is the norm). + # This switch is for users who want the stricter Linux/macOS behaviour, + # where install.sh expects a system BLAS and errors without one. + # Whatever is downloaded lands under : project-local, + # nothing installed system-wide, removed when that directory is deleted. + # With -Backend openblas this always fails: OpenBLAS has no canonical + # Windows location to discover, so there is nothing to fall back to and + # the honest answer is to direct the user at -Backend custom. + [switch]$NoDownload, + + # Skip interactive questions and take the documented default for each, + # mirroring install.sh's -y/--yes. Prompts are already skipped whenever + # stdin is not a terminal (CI, piped input), so this is only needed to + # silence them in an interactive session. + [switch]$Yes, # Use an existing oneMKL install (e.g. from the oneAPI installer) instead - # of fetching MKL through vcpkg. Must contain the ILP64 DLL import libs. + # of auto-discovery/download. Must contain the ILP64 DLL import libs. + # Only meaningful with -Backend mkl. [string]$MklRoot = "", + # -Backend custom only: semicolon-separated .lib paths handed verbatim + # to BLAS++ (-BlasLibraries, required), optionally LAPACK++ + # (-LapackLibraries), the DLL directory for runtime staging + # (-BackendBinDir), the BLAS integer size, and blaspp's blas_fortran + # name-mangling hint (e.g. "add"). + [string]$BlasLibraries = "", + [string]$LapackLibraries = "", + [string]$BackendBinDir = "", + [ValidateSet("lp64", "ilp64")] + [string]$BlasInt = "lp64", + [string]$BlasFortran = "", + [switch]$SanitizeAddress ) @@ -38,11 +81,50 @@ function Invoke-Checked { } } +# Architecture detection is shared with install/install.ps1 rather than +# duplicated; see that file's note. $PSScriptRoot is +# \.github\actions\setup-randlapack-deps-windows. +$archHelper = Join-Path $PSScriptRoot "..\..\scripts\windows\toolchain-arch.ps1" +if (-not (Test-Path $archHelper)) { + throw "Missing $archHelper. This clone looks incomplete." +} +. $archHelper + function Convert-ToCMakePath { param([string]$Path) return $Path.Replace('\', '/') } +# Prompts happen only on a terminal and only without -Yes, mirroring +# install.sh's INTERACTIVE flag. Without this gate, a question asked under +# GitHub Actions (or any piped stdin) blocks until the job times out. +$script:Interactive = -not $Yes -and -not [Console]::IsInputRedirected ` + -and [Environment]::UserInteractive + +function Read-YesNo { + # Returns $true/$false. Non-interactive callers get $Default without a + # prompt, so every question must have a defensible unattended answer. + param([string]$Question, [bool]$Default) + if (-not $script:Interactive) { return $Default } + $suffix = if ($Default) { "[Y/n]" } else { "[y/N]" } + while ($true) { + $reply = (Read-Host "$Question $suffix").Trim().ToLowerInvariant() + if ($reply -eq "") { return $Default } + if ($reply -in @("y", "yes")) { return $true } + if ($reply -in @("n", "no")) { return $false } + Write-Host "Please answer y or n." + } +} + +function Assert-SupportedToolchain { + # Left unchecked, a non-x64 toolchain surfaces much later as BLAS++ + # reporting "BLAS library not found", which points at the wrong thing + # entirely: the libraries are present and correct, the linker simply + # cannot use them at that architecture. + $problem = Get-ToolchainArchitectureProblem (Get-ClTargetArchitecture) + if ($problem -ne "") { throw $problem } +} + function Find-PackageConfigDirectory { # Install layouts of blaspp/lapackpp vary between revisions; search for the # package config file instead of hardcoding lib/cmake/. @@ -55,16 +137,49 @@ function Find-PackageConfigDirectory { return $config.DirectoryName } -function Clone-Head { - param([string]$Url, [string]$Destination, [string]$Branch = "") +function Clone-Pinned { + # -Ref is a tag or a commit SHA. Everything here is pinned to an immutable + # ref on purpose: a branch tip moves, and these clones live inside a cache + # keyed on this script, so a moving tip would silently change what a + # "cache hit" restores. + param([string]$Url, [string]$Destination, [string]$Ref) if (Test-Path $Destination) { - Write-Host "Reusing existing clone at $Destination" - return + # Reuse only if it is actually the pinned source. A clone left by an + # earlier revision of this script may sit at a different remote or + # commit, and silently rebuilding that is how a "fixed" dependency + # stays broken. + $head = (& git -C $Destination rev-parse HEAD 2>$null) + $remote = (& git -C $Destination remote get-url origin 2>$null) + if ($LASTEXITCODE -eq 0 -and $remote -eq $Url -and ($head -eq $Ref -or $head -like "$Ref*")) { + Write-Host "Reusing existing clone at $Destination" + return + } + Write-Host "Re-cloning $Destination : it is not at the pinned $Url@$Ref." + Remove-Item -Recurse -Force $Destination } - $cloneArgs = @("clone", "--depth", "1") - if ($Branch -ne "") { $cloneArgs += @("--branch", $Branch) } - $cloneArgs += @($Url, $Destination) - Invoke-Checked "git" $cloneArgs + # A tag can be cloned shallowly by name; a SHA cannot, so fetch it + # directly (GitHub allows fetching a reachable commit by SHA). + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + Invoke-Checked "git" @("-C", $Destination, "init", "--quiet") + Invoke-Checked "git" @("-C", $Destination, "remote", "add", "origin", $Url) + Invoke-Checked "git" @("-C", $Destination, "fetch", "--quiet", "--depth", "1", "origin", $Ref) + Invoke-Checked "git" @("-C", $Destination, "checkout", "--quiet", "FETCH_HEAD") +} + +function Test-Provenance { + # A dependency install is reusable only if it was built from the source we + # would build from now. Without this, changing a pin (or a repository) has + # no effect on anyone who already has an install: the reuse check only + # asks "does a config file exist?". That is the same failure mode as + # reusing a dependency tree configured by the wrong compiler. + param([string]$InstallRoot, [string]$Expected) + $stamp = Join-Path $InstallRoot ".randlapack-source" + return (Test-Path $stamp) -and ((Get-Content -Raw $stamp).Trim() -eq $Expected) +} + +function Write-Provenance { + param([string]$InstallRoot, [string]$Expected) + Set-Content -Path (Join-Path $InstallRoot ".randlapack-source") -Value $Expected -Encoding ascii } function Export-GitHubValue { @@ -80,6 +195,70 @@ function Export-GitHubValue { Write-Host "$Name = $Value" } +function Find-OneMklLayout { + # Probes a oneMKL root for the ILP64 import libs (oneAPI layout: lib\ on + # current releases, lib\intel64 on older ones) and a DLL directory + # (bin\ since oneAPI 2024, redist\intel64 before). + # Returns @{Root; LibDir; BinDir} or $null. + param([string]$Root) + if (-not $Root) { return $null } + $resolved = [System.IO.Path]::GetFullPath($Root) + foreach ($libDir in @((Join-Path $resolved "lib"), (Join-Path $resolved "lib\intel64"))) { + if (-not (Test-Path (Join-Path $libDir "mkl_intel_ilp64_dll.lib"))) { continue } + $binDir = @((Join-Path $resolved "bin"), (Join-Path $resolved "redist\intel64")) | + Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($binDir) { return @{ Root = $resolved; LibDir = $libDir; BinDir = $binDir } } + } + return $null +} + +function Test-BlasLinkage { + # Compiles and runs a minimal dgemm_/dgesv_ caller against the given + # import libraries: one clear pass/fail up front instead of a BLAS++ + # probe cascade later. Int64 selects the integer width (ILP64 vs LP64). + param([string[]]$Libraries, [string]$DllDir, [bool]$Int64, [string]$ScratchDir) + New-Item -ItemType Directory -Force -Path $ScratchDir | Out-Null + $src = Join-Path $ScratchDir "blas_conftest.c" + $intType = if ($Int64) { "long long" } else { "int" } + @( + '#include ', + "typedef $intType blas_int;", + 'extern void dgemm_(const char*, const char*, const blas_int*, const blas_int*,', + ' const blas_int*, const double*, const double*, const blas_int*,', + ' const double*, const blas_int*, const double*, double*,', + ' const blas_int*);', + 'extern void dgesv_(const blas_int*, const blas_int*, double*, const blas_int*,', + ' blas_int*, double*, const blas_int*, blas_int*);', + 'int main(void) {', + ' blas_int n = 2, one = 1, info = -1, ipiv[2];', + ' double A[4] = {3, 1, 1, 2}, b[2] = {9, 8}, C[4], alpha = 1.0, beta = 0.0;', + ' dgemm_("N", "N", &n, &n, &n, &alpha, A, &n, A, &n, &beta, C, &n);', + ' dgesv_(&n, &one, A, &n, ipiv, b, &n, &info);', + ' if (info != 0) { printf("dgesv_ info=%lld\n", (long long)info); return 1; }', + ' if (b[0] < 1.9 || b[0] > 2.1 || b[1] < 2.9 || b[1] > 3.1) {', + ' printf("wrong dgesv_ solution: %f %f\n", b[0], b[1]); return 2;', + ' }', + ' printf("BLAS/LAPACK link check OK\n");', + ' return 0;', + '}') | Set-Content -Path $src -Encoding ascii + $exe = Join-Path $ScratchDir "blas_conftest.exe" + Push-Location $ScratchDir + try { + & cl.exe /nologo $src "/Fe:$exe" /link @($Libraries) | Out-Host + if ($LASTEXITCODE -ne 0) { return $false } + $savedPath = $env:PATH + if ($DllDir) { $env:PATH = "$DllDir;$env:PATH" } + try { + & $exe | Out-Host + return ($LASTEXITCODE -eq 0) + } finally { + $env:PATH = $savedPath + } + } finally { + Pop-Location + } +} + # ---------------------------------------------------------------- guards ---- $resolvedRoot = [System.IO.Path]::GetFullPath($DependencyRoot) @@ -91,65 +270,345 @@ New-Item -ItemType Directory -Force -Path $resolvedRoot | Out-Null if (-not (Get-Command "cl.exe" -ErrorAction SilentlyContinue)) { throw "cl.exe is not on PATH. Run from an MSVC developer environment (or ilammy/msvc-dev-cmd in CI)." } +Assert-SupportedToolchain -# ---------------------------------------------------------------- oneMKL ---- +# ---------------------------------------------------- argument checks ---- -if ($MklRoot -ne "") { - # Bring-your-own MKL (oneAPI installer layout: import libs in lib\ on - # current releases, lib\intel64 on older ones). - $mklRoot = [System.IO.Path]::GetFullPath($MklRoot) - $mklLibDir = "" - foreach ($candidate in @((Join-Path $mklRoot "lib"), (Join-Path $mklRoot "lib\intel64"))) { - if (Test-Path (Join-Path $candidate "mkl_intel_ilp64_dll.lib")) { - $mklLibDir = $candidate - break +if ($Backend -ne "mkl" -and $MklRoot -ne "") { + throw "-MklRoot is only meaningful with -Backend mkl." +} +if ($Backend -eq "custom" -and $BlasLibraries -eq "") { + throw "-Backend custom requires -BlasLibraries (semicolon-separated full paths to .lib files)." +} +if ($Backend -ne "custom") { + foreach ($customOnly in @( + @{ Name = "-BlasLibraries"; Value = $BlasLibraries }, + @{ Name = "-LapackLibraries"; Value = $LapackLibraries }, + @{ Name = "-BackendBinDir"; Value = $BackendBinDir }, + @{ Name = "-BlasFortran"; Value = $BlasFortran })) { + if ($customOnly.Value -ne "") { + throw "$($customOnly.Name) is only meaningful with -Backend custom." } } - if ($mklLibDir -eq "") { - throw "-MklRoot $MklRoot does not contain mkl_intel_ilp64_dll.lib under lib\ or lib\intel64\." - } - $mklBin = @((Join-Path $mklRoot "bin"), (Join-Path $mklRoot "redist\intel64")) | - Where-Object { Test-Path $_ } | Select-Object -First 1 - if (-not $mklBin) { throw "-MklRoot $MklRoot has no bin\ (or redist\intel64\) DLL directory." } - Write-Host "Using existing oneMKL at $mklRoot" -} else { - if ($VcpkgExecutable -eq "") { - foreach ($candidateRoot in @($env:VCPKG_INSTALLATION_ROOT, $env:VCPKG_ROOT)) { - if ($candidateRoot -and (Test-Path (Join-Path $candidateRoot "vcpkg.exe"))) { - $VcpkgExecutable = Join-Path $candidateRoot "vcpkg.exe" +} + +# ----------------------------------------------- BLAS/LAPACK backend ---- +# Every branch below must define: $backendLibraries (array of cmake-style +# .lib paths for BLAS++), $backendLapackLibraries ("" = let LAPACK++ resolve +# from the BLAS libs), $backendBlasInt, $backendBlasFortran ("" = unset), +# $backendBlasThreaded ("" = unset), and $backendBin (DLL directory; "" only +# for -Backend custom without -BackendBinDir). + +if ($Backend -eq "mkl") { + $mklLayout = $null + if ($MklRoot -ne "") { + $mklLayout = Find-OneMklLayout $MklRoot + if (-not $mklLayout) { + throw ("-MklRoot $MklRoot does not contain mkl_intel_ilp64_dll.lib under lib\ or " + + "lib\intel64\ alongside a bin\ (or redist\intel64\) DLL directory.") + } + Write-Host "Using existing oneMKL at $($mklLayout.Root)" + } else { + # Discovery before download: a setvars.bat session exports MKLROOT; + # some oneAPI installers set ONEAPI_ROOT persistently; and the + # installer's default location is stable even when neither survives + # into a fresh shell (modern installers set no persistent env vars). + foreach ($candidate in @( + $env:MKLROOT, + $(if ($env:ONEAPI_ROOT) { Join-Path $env:ONEAPI_ROOT "mkl\latest" }), + "C:\Program Files (x86)\Intel\oneAPI\mkl\latest")) { + $mklLayout = Find-OneMklLayout $candidate + if ($mklLayout) { + Write-Host "Discovered existing oneMKL at $($mklLayout.Root)" break } } } - if ($VcpkgExecutable -eq "") { - $found = Get-Command "vcpkg.exe" -ErrorAction SilentlyContinue - if ($found) { $VcpkgExecutable = $found.Source } + # Not found: offer to provision it. Auto-provisioning is the default + # answer because Windows has no system prefix for third-party libraries, + # so per-project acquisition is normal practice rather than a workaround, + # and it is what lets a bare machine install in one command. Asking first + # keeps a 155 MB download from being a surprise, and states plainly where + # it goes and that nothing touches the system. + $provisionMkl = $false + if (-not $mklLayout -and -not $NoDownload) { + Write-Host "" + Write-Host "No existing oneMKL found (checked -MklRoot, `$env:MKLROOT, `$env:ONEAPI_ROOT," + Write-Host "and C:\Program Files (x86)\Intel\oneAPI\mkl\latest)." + Write-Host "" + Write-Host "A pinned, checksum-verified copy (~155 MB) can be downloaded into" + Write-Host " $resolvedRoot" + Write-Host "It is used only by this project: nothing is installed system-wide, no PATH" + Write-Host "or registry changes, and deleting that directory removes it completely." + Write-Host "" + # Default yes: the unattended answer must keep CI and a one-command + # install on a bare machine working, and this is the same choice + # install.sh's ask() makes for its own prompts. + $provisionMkl = Read-YesNo "Download oneMKL now?" $true + if (-not $provisionMkl) { Write-Host "" } } - if ($VcpkgExecutable -eq "") { - throw ("Could not locate vcpkg.exe (checked -VcpkgExecutable, VCPKG_INSTALLATION_ROOT, " + - "VCPKG_ROOT, PATH). Alternatively pass -MklRoot pointing at an existing oneMKL install.") + if (-not $mklLayout -and -not $provisionMkl) { + # Reached two ways: -NoDownload, or the question above answered no. + # Both mean no backend is available, so both get the same options. + # Details go to the console and the throw stays short: PowerShell + # echoes a long throw message twice and buries it in a stack trace. + $mklRootShown = if ($env:MKLROOT) { $env:MKLROOT } else { "(not set)" } + $oneApiShown = if ($env:ONEAPI_ROOT) { Join-Path $env:ONEAPI_ROOT "mkl\latest" } else { "(not set)" } + Write-Host "" + $why = if ($NoDownload) { "-NoDownload was given" } else { "the download was declined" } + Write-Host "No oneMKL found, and $why." + Write-Host "" + Write-Host " Searched: -MklRoot (not given)" + Write-Host " `$env:MKLROOT $mklRootShown" + Write-Host " `$env:ONEAPI_ROOT $oneApiShown" + Write-Host " C:\Program Files (x86)\Intel\oneAPI\mkl\latest" + Write-Host "" + Write-Host " Any of these works:" + Write-Host " winget install --id Intel.oneMKL --exact" + Write-Host " -MklRoot `"`" use a copy you already have" + Write-Host " -Backend openblas use OpenBLAS instead" + Write-Host " re-run and answer yes (or pass -Yes) download a pinned oneMKL," + Write-Host " ~155 MB, into this project only" + Write-Host "" + throw "No BLAS/LAPACK backend available; see the options above." } - - $vcpkgInstallRoot = Join-Path $resolvedRoot "vcpkg-installed" - $mklRoot = Join-Path $vcpkgInstallRoot "x64-windows" - $mklLibDir = Join-Path $mklRoot "lib" - if (Test-Path (Join-Path $mklLibDir "mkl_intel_ilp64_dll.lib")) { - Write-Host "Reusing oneMKL at $mklRoot" + if (-not $mklLayout) { + # oneMKL comes straight from Intel's official NuGet packages -- plain + # zip archives on nuget.org, pinned by version and SHA256. The devel + # package carries the ILP64/sequential import libs and headers, the + # redist package the runtime DLLs. This deliberately avoids vcpkg: its + # Visual Studio-bundled distribution is manifest-only (no classic-mode + # instance), and nothing here needed vcpkg beyond this one download. + # The OpenMP/TBB packages the devel nuspec references are skipped on + # purpose -- RandLAPACK links the sequential MKL DLL set. + $mklVersion = "2026.1.0.226" + $mklPackages = @( + @{ Id = "intelmkl.devel.win-x64" + Sha256 = "d4456ce3c767b235d9c212c093a40cdf073589102dbf70bc0fd2d59140be30d2" }, + @{ Id = "intelmkl.redist.win-x64" + Sha256 = "ac2d4a14a70b021557170f53460b57038af5ab1977e82a05aca8e4a5af7bcb61" }) + $mklRoot = Join-Path $resolvedRoot "onemkl-$mklVersion" + $mklLibDir = Join-Path $mklRoot "lib" + $mklBin = Join-Path $mklRoot "bin" + if (Test-Path (Join-Path $mklLibDir "mkl_intel_ilp64_dll.lib")) { + Write-Host "Reusing oneMKL at $mklRoot" + } else { + if (Test-Path $mklRoot) { Remove-Item -Recurse -Force $mklRoot } + $extractRoot = Join-Path $mklRoot "extract" + foreach ($package in $mklPackages) { + $archive = Join-Path $resolvedRoot "$($package.Id).$mklVersion.zip" + Invoke-Checked "curl.exe" @("-fsSL", "--retry", "5", "--retry-all-errors", + "--retry-delay", "3", "-o", $archive, + "https://api.nuget.org/v3-flatcontainer/$($package.Id)/$mklVersion/$($package.Id).$mklVersion.nupkg") + $actual = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() + if ($actual -ne $package.Sha256) { + throw "$($package.Id) $mklVersion hash mismatch: expected $($package.Sha256), got $actual." + } + Expand-Archive -Path $archive -DestinationPath (Join-Path $extractRoot $package.Id) -Force + Remove-Item $archive + } + # Arrange the pieces into the oneAPI directory shape (lib\, + # include\, bin\) that Find-OneMklLayout, the checks below, and + # RandBLAS's MKL_sparse.cmake (MKLROOT/include) already expect. + Move-Item (Join-Path $extractRoot "intelmkl.devel.win-x64\build\native\win-x64") $mklLibDir + Move-Item (Join-Path $extractRoot "intelmkl.devel.win-x64\build\native\include") (Join-Path $mklRoot "include") + Move-Item (Join-Path $extractRoot "intelmkl.redist.win-x64\runtimes\win-x64\native") $mklBin + Remove-Item -Recurse -Force $extractRoot + } + $mklLayout = @{ Root = $mklRoot; LibDir = $mklLibDir; BinDir = $mklBin } + } + $mklRoot = $mklLayout.Root + $mklLibDir = $mklLayout.LibDir + $mklBin = $mklLayout.BinDir + foreach ($required in @( + (Join-Path $mklLibDir "mkl_intel_ilp64_dll.lib"), + (Join-Path $mklLibDir "mkl_sequential_dll.lib"), + (Join-Path $mklLibDir "mkl_core_dll.lib"))) { + if (-not (Test-Path $required)) { throw "oneMKL install is missing $required." } + } + # Same link-and-run check the other two backends get: one clear pass/fail + # here beats a BLAS++ probe cascade three layers down. It catches an + # incomplete or mismatched oneMKL (and, belt-and-braces after + # Assert-SupportedToolchain, any remaining bitness mismatch) before anything is + # built. + $mklLibs = @( + (Join-Path $mklLibDir "mkl_intel_ilp64_dll.lib"), + (Join-Path $mklLibDir "mkl_sequential_dll.lib"), + (Join-Path $mklLibDir "mkl_core_dll.lib")) + if (-not (Test-BlasLinkage -Libraries $mklLibs -DllDir $mklBin ` + -Int64 $true -ScratchDir (Join-Path $resolvedRoot "conftest-mkl"))) { + throw ("oneMKL at $mklRoot failed a minimal ILP64 dgemm_/dgesv_ link-and-run check. " + + "Verify that the install is complete and that its DLL directory ($mklBin) holds " + + "the matching runtime DLLs.") + } + $backendLibraries = @($mklLibs | ForEach-Object { Convert-ToCMakePath $_ }) + $backendLapackLibraries = "" + # ILP64 because RandBLAS's MKL sparse backend static-asserts that its + # int64_t sparse indices match sizeof(MKL_INT). + $backendBlasInt = "ilp64" + $backendBlasFortran = "" + $backendBlasThreaded = "false" + $backendBin = $mklBin +} elseif ($Backend -eq "openblas") { + # Official OpenBLAS release binaries: MinGW-built but self-contained + # (the DLL imports only kernel32/msvcrt -- the MinGW runtimes are linked + # in statically), MSVC-linkable through the shipped import library, and + # full LAPACK is included. LP64: no ILP64 OpenBLAS binaries are + # published for Windows. Without MKL, RandBLAS's MKL sparse + # acceleration stays off and its portable fallbacks take over. + # + # Unlike oneMKL there is nothing to auto-discover: OpenBLAS has no + # canonical Windows install location (GitHub release zips, vcpkg, conda + # and MSYS2 all differ, and the release zips even ship CMake config + # files with wrong hardcoded paths). So rather than probe and guess, ask + # -- and only when someone is there to answer. + if ($NoDownload) { + # -NoDownload must mean what it says for every backend. There is no + # OpenBLAS to discover, so the only honest outcome is to stop and + # point at the backend that takes user-supplied libraries. + Write-Host "" + Write-Host "-Backend openblas needs to download OpenBLAS, and -NoDownload forbids that." + Write-Host "" + Write-Host " OpenBLAS has no canonical install location on Windows, so unlike oneMKL" + Write-Host " there is nothing to discover. To use a copy you already have:" + Write-Host "" + Write-Host " -Backend custom -BlasLibraries `"\libopenblas.lib`" ``" + Write-Host " -BackendBinDir `"`" ``" + Write-Host " -BlasInt lp64 -BlasFortran add" + Write-Host "" + throw "-Backend openblas requires a download; use -Backend custom or drop -NoDownload." + } + if ($script:Interactive) { + if (Read-YesNo "Do you already have OpenBLAS installed?" $false) { + Write-Host "" + Write-Host "OpenBLAS has no standard layout on Windows, so point at the pieces" + Write-Host "directly rather than at a root directory. Re-run with:" + Write-Host "" + Write-Host " -Backend custom ``" + Write-Host " -BlasLibraries `"\libopenblas.lib`" ``" + Write-Host " -BackendBinDir `"`" ``" + Write-Host " -BlasInt lp64 -BlasFortran add" + Write-Host "" + Write-Host "Your libraries are checked with a real dgemm_/dgesv_ link-and-run" + Write-Host "test before anything is built, so a wrong path fails immediately." + Write-Host "" + throw "Re-run with -Backend custom to use your own OpenBLAS (see above)." + } + } + $openblasVersion = "0.3.34" + $openblasSha256 = "e9cb6134541f36c27346d5fc5995652f060fba227cebbbabcbda5a5a44d7c76b" + $openblasRoot = Join-Path $resolvedRoot "openblas-$openblasVersion" + $openblasLib = Join-Path $openblasRoot "lib\libopenblas.lib" + $openblasBin = Join-Path $openblasRoot "bin" + if (Test-Path $openblasLib) { + Write-Host "Reusing OpenBLAS at $openblasRoot" } else { - Invoke-Checked $VcpkgExecutable @( - "install", "intel-mkl:x64-windows", "--x-install-root=$vcpkgInstallRoot") + if (Test-Path $openblasRoot) { Remove-Item -Recurse -Force $openblasRoot } + $archive = Join-Path $resolvedRoot "OpenBLAS-$openblasVersion-x64.zip" + Invoke-Checked "curl.exe" @("-fsSL", "--retry", "5", "--retry-all-errors", + "--retry-delay", "3", "-o", $archive, + "https://github.com/OpenMathLib/OpenBLAS/releases/download/v$openblasVersion/OpenBLAS-$openblasVersion-x64.zip") + $actual = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() + if ($actual -ne $openblasSha256) { + throw "OpenBLAS $openblasVersion hash mismatch: expected $openblasSha256, got $actual." + } + Expand-Archive -Path $archive -DestinationPath $openblasRoot -Force + Remove-Item $archive + } + $openblasConftest = Join-Path $resolvedRoot "conftest-openblas" + if (-not (Test-BlasLinkage -Libraries @($openblasLib) -DllDir $openblasBin ` + -Int64 $false -ScratchDir $openblasConftest)) { + # The shipped import library is occasionally unusable from MSVC; + # regenerate it from the .def and retry once (self-healing when the + # version pin moves). + Write-Host "Shipped libopenblas.lib failed the link check; regenerating from libopenblas.def..." + Invoke-Checked "lib.exe" @("/nologo", "/machine:x64", + "/def:$(Join-Path $openblasRoot 'lib\libopenblas.def')", "/out:$openblasLib") + if (-not (Test-BlasLinkage -Libraries @($openblasLib) -DllDir $openblasBin ` + -Int64 $false -ScratchDir $openblasConftest)) { + throw "OpenBLAS link check failed even after import-library regeneration." + } + } + $backendLibraries = @(Convert-ToCMakePath $openblasLib) + # OpenBLAS bundles LAPACK in the same library. + $backendLapackLibraries = Convert-ToCMakePath $openblasLib + $backendBlasInt = "int32" + $backendBlasFortran = "add" + $backendBlasThreaded = "" + $backendBin = $openblasBin +} else { + # Bring-your-own backend (e.g. AMD AOCL, whose downloads are + # click-through-gated and cannot be fetched here): the libraries are + # handed to BLAS++/LAPACK++ verbatim after one clear preflight check. + # The check calls dgemm_/dgesv_, i.e. it assumes the common + # lowercase-underscore Fortran mangling (OpenBLAS, AOCL, MKL all + # export it). + $customLibs = @($BlasLibraries -split ";" | Where-Object { $_ -ne "" }) + foreach ($lib in $customLibs) { + if (-not (Test-Path $lib)) { throw "-BlasLibraries entry not found: $lib" } + } + if ($BackendBinDir -ne "" -and -not (Test-Path $BackendBinDir)) { + throw "-BackendBinDir not found: $BackendBinDir" + } + if (-not (Test-BlasLinkage -Libraries $customLibs -DllDir $BackendBinDir ` + -Int64 ($BlasInt -eq "ilp64") -ScratchDir (Join-Path $resolvedRoot "conftest-custom"))) { + throw ("The libraries in -BlasLibraries failed a minimal dgemm_/dgesv_ link-and-run " + + "check. Verify the paths, the integer size (-BlasInt), and that their runtime " + + "DLLs are in -BackendBinDir.") + } + $backendLibraries = @($customLibs | ForEach-Object { Convert-ToCMakePath $_ }) + $backendLapackLibraries = (@($LapackLibraries -split ";" | Where-Object { $_ -ne "" } | + ForEach-Object { Convert-ToCMakePath $_ })) -join ";" + $backendBlasInt = if ($BlasInt -eq "ilp64") { "ilp64" } else { "int32" } + $backendBlasFortran = $BlasFortran + $backendBlasThreaded = "" + $backendBin = $BackendBinDir + if ($backendBin -eq "") { + Write-Warning ("No -BackendBinDir given: the custom backend's runtime DLLs will not be " + + "staged next to executables; making them findable at run time is up to you.") } - $mklBin = Join-Path $mklRoot "bin" } -foreach ($required in @( - $mklBin, - (Join-Path $mklLibDir "mkl_intel_ilp64_dll.lib"), - (Join-Path $mklLibDir "mkl_sequential_dll.lib"), - (Join-Path $mklLibDir "mkl_core_dll.lib"))) { - if (-not (Test-Path $required)) { throw "oneMKL install is missing $required." } +# Distinct BLAS++/LAPACK++ installs per backend; a custom backend is keyed +# by a hash of its library list so switching -BlasLibraries rebuilds. +$backendId = $Backend +if ($Backend -eq "custom") { + $sha = [System.Security.Cryptography.SHA256]::Create() + $hashHex = ($sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($BlasLibraries)) | + ForEach-Object { $_.ToString("x2") }) -join "" + $backendId = "custom-$($hashHex.Substring(0, 8))" +} + +function Copy-LibrariesToSpaceFreePath { + # BLAS++ splits library paths on spaces before probing them, so any path + # containing one fails as "BLAS library not found". Intel's default oneMKL + # location has a space, making this the common case rather than a corner + # one. Fixed upstream in icl-utk-edu/blaspp#137; removing this workaround + # is tracked in #158. Import libraries only name their DLL, still loaded + # at run time from $backendBin, so relocating them is safe. + param([string[]]$Libraries, [string]$Destination, [string]$Label) + if (-not ($Libraries | Where-Object { $_ -match ' ' })) { return $Libraries } + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + Write-Host ("Staging $Label import libraries into a space-free path " + + "($Destination): BLAS++ cannot probe libraries whose path contains a space.") + return @($Libraries | ForEach-Object { + $leaf = Split-Path $_ -Leaf + $target = Join-Path $Destination $leaf + Copy-Item -LiteralPath $_.Replace('/', '\') -Destination $target -Force + Convert-ToCMakePath $target + }) } -$env:PATH = "$mklBin;$env:PATH" + +$spaceFreeLibDir = Join-Path $resolvedRoot "backend-libs" +$backendLibraries = Copy-LibrariesToSpaceFreePath -Libraries $backendLibraries ` + -Destination $spaceFreeLibDir -Label "BLAS" +if ($backendLapackLibraries -ne "") { + $backendLapackLibraries = (Copy-LibrariesToSpaceFreePath ` + -Libraries @($backendLapackLibraries -split ';') ` + -Destination $spaceFreeLibDir -Label "LAPACK") -join ';' +} + +if ($backendBin -ne "") { $env:PATH = "$backendBin;$env:PATH" } # ------------------------------------------------------------- GoogleTest ---- @@ -159,10 +618,10 @@ if (Test-Path (Join-Path $gtestInstall "include\gtest\gtest.h")) { Write-Host "Reusing GoogleTest at $gtestInstall" } else { $gtestSrc = Join-Path $resolvedRoot "$gtestVariant-src" - Clone-Head "https://github.com/google/googletest.git" $gtestSrc "v1.17.0" + Clone-Pinned "https://github.com/google/googletest.git" $gtestSrc "v1.18.0" $gtestBuild = Join-Path $resolvedRoot "$gtestVariant-build" $gtestArgs = @( - "-S", $gtestSrc, "-B", $gtestBuild, "-G", "NMake Makefiles", + "-S", $gtestSrc, "-B", $gtestBuild, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=$(Convert-ToCMakePath $gtestInstall)", "-DBUILD_GMOCK=OFF", "-DINSTALL_GTEST=ON") @@ -182,7 +641,7 @@ if (Test-Path (Join-Path $random123Install "include\Random123\philox.h")) { Write-Host "Reusing Random123 at $random123Install" } else { $random123Src = Join-Path $resolvedRoot "Random123-src" - Clone-Head "https://github.com/DEShawResearch/Random123.git" $random123Src + Clone-Pinned "https://github.com/DEShawResearch/Random123.git" $random123Src "v1.14.0" New-Item -ItemType Directory -Force -Path (Join-Path $random123Install "include") | Out-Null Copy-Item -Recurse -Force (Join-Path $random123Src "include\Random123") ` (Join-Path $random123Install "include\Random123") @@ -190,64 +649,98 @@ if (Test-Path (Join-Path $random123Install "include\Random123\philox.h")) { # ----------------------------------------------------------------- BLAS++ ---- -$blasppInstall = Join-Path $resolvedRoot "blaspp-install" -if (Test-Path $blasppInstall) { +$blasppInstall = Join-Path $resolvedRoot "blaspp-$backendId-install" +# Reuse only on an intact install: a partially restored cache directory must +# trigger a rebuild, not silently skip it. +# Upstream, pinned to the commit that merged the MSVC fix (PR #132, +# 2026-08-06). Not in a release yet: the latest tag, v2025.05.28, predates it. +# Move to a tag once one includes it. Declared once and used for both the +# clone and the reuse stamp, so the two cannot drift. +$blasppUrl = "https://github.com/icl-utk-edu/blaspp.git" +$blasppRef = "30571853f980d3a2a1737124ea4789e025a5e045" +$blasppSource = "$blasppUrl@$blasppRef" +$blasppReusable = (Test-Path $blasppInstall) -and (Get-ChildItem -Path $blasppInstall -Recurse ` + -Filter "blasppConfig.cmake" -ErrorAction SilentlyContinue | Select-Object -First 1) ` + -and (Test-Provenance $blasppInstall $blasppSource) +if ($blasppReusable) { Write-Host "Reusing BLAS++ at $blasppInstall" } else { $blasppSrc = Join-Path $resolvedRoot "blaspp-src" - Clone-Head "https://github.com/BallisticLA/blaspp.git" $blasppSrc "remove-symv-debug-print" - $blasppBuild = Join-Path $resolvedRoot "blaspp-build" - # ILP64 + sequential is required: RandBLAS's MKL sparse backend static-asserts - # that its int64_t sparse indices match sizeof(MKL_INT). - $mklLibs = @( - (Convert-ToCMakePath (Join-Path $mklLibDir "mkl_intel_ilp64_dll.lib")), - (Convert-ToCMakePath (Join-Path $mklLibDir "mkl_sequential_dll.lib")), - (Convert-ToCMakePath (Join-Path $mklLibDir "mkl_core_dll.lib"))) -join ';' - Invoke-Checked "cmake" @( - "-S", $blasppSrc, "-B", $blasppBuild, "-G", "NMake Makefiles", + Clone-Pinned $blasppUrl $blasppSrc $blasppRef + # Never re-configure an existing blaspp build in place: that regenerates + # blas/defines.h without the backend defines. Fresh build tree per run. + $blasppBuild = Join-Path $resolvedRoot "blaspp-$backendId-build" + if (Test-Path $blasppBuild) { Remove-Item -Recurse -Force $blasppBuild } + $blasppArgs = @( + "-S", $blasppSrc, "-B", $blasppBuild, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=$(Convert-ToCMakePath $blasppInstall)", "-DBUILD_SHARED_LIBS=ON", "-Duse_cmake_find_blas=false", - "-DBLAS_LIBRARIES=$mklLibs", - "-Dblas_int=ilp64", - "-Dblas_threaded=false", + "-DBLAS_LIBRARIES=$($backendLibraries -join ';')", + "-Dblas_int=$backendBlasInt", "-Duse_openmp=false", "-Dgpu_backend=none", "-Dbuild_tests=OFF") + if ($backendBlasThreaded -ne "") { $blasppArgs += "-Dblas_threaded=$backendBlasThreaded" } + if ($backendBlasFortran -ne "") { $blasppArgs += "-Dblas_fortran=$backendBlasFortran" } + Invoke-Checked "cmake" $blasppArgs Invoke-Checked "cmake" @("--build", $blasppBuild, "--target", "install") + Write-Provenance $blasppInstall $blasppSource } $blasppDir = Find-PackageConfigDirectory $blasppInstall "blaspp" # --------------------------------------------------------------- LAPACK++ ---- -$lapackppInstall = Join-Path $resolvedRoot "lapackpp-install" -if (Test-Path $lapackppInstall) { +$lapackppInstall = Join-Path $resolvedRoot "lapackpp-$backendId-install" +# Upstream, pinned to the commit that merged the MSVC fix (PR #87, +# 2026-08-06); likewise not yet in a release. Declared once, as above. +$lapackppUrl = "https://github.com/icl-utk-edu/lapackpp.git" +$lapackppRef = "40b9d0daf29b6f1f3fa58bc3f22bd6cfb2c67fe4" +$lapackppSource = "$lapackppUrl@$lapackppRef" +$lapackppReusable = (Test-Path $lapackppInstall) -and (Get-ChildItem -Path $lapackppInstall -Recurse ` + -Filter "lapackppConfig.cmake" -ErrorAction SilentlyContinue | Select-Object -First 1) ` + -and (Test-Provenance $lapackppInstall $lapackppSource) +if ($lapackppReusable) { Write-Host "Reusing LAPACK++ at $lapackppInstall" } else { $lapackppSrc = Join-Path $resolvedRoot "lapackpp-src" - Clone-Head "https://github.com/BallisticLA/lapackpp.git" $lapackppSrc "msvc-direct-includes" - $lapackppBuild = Join-Path $resolvedRoot "lapackpp-build" - Invoke-Checked "cmake" @( - "-S", $lapackppSrc, "-B", $lapackppBuild, "-G", "NMake Makefiles", + Clone-Pinned $lapackppUrl $lapackppSrc $lapackppRef + $lapackppBuild = Join-Path $resolvedRoot "lapackpp-$backendId-build" + if (Test-Path $lapackppBuild) { Remove-Item -Recurse -Force $lapackppBuild } + $lapackppArgs = @( + "-S", $lapackppSrc, "-B", $lapackppBuild, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=$(Convert-ToCMakePath $lapackppInstall)", "-Dblaspp_DIR=$(Convert-ToCMakePath $blasppDir)", "-DBUILD_SHARED_LIBS=ON", "-Dgpu_backend=none", "-Dbuild_tests=OFF") + if ($backendLapackLibraries -ne "") { + $lapackppArgs += "-DLAPACK_LIBRARIES=$backendLapackLibraries" + } + Invoke-Checked "cmake" $lapackppArgs Invoke-Checked "cmake" @("--build", $lapackppBuild, "--target", "install") + Write-Provenance $lapackppInstall $lapackppSource } $lapackppDir = Find-PackageConfigDirectory $lapackppInstall "lapackpp" # ----------------------------------------------------------------- export ---- -Export-GitHubValue "MKLROOT" (Convert-ToCMakePath $mklRoot) -Export-GitHubValue "MKL_BIN" $mklBin +Export-GitHubValue "RANDNLA_BLAS_BACKEND" $Backend +if ($backendBin -ne "") { Export-GitHubValue "RANDNLA_BLAS_BIN" $backendBin } +if ($Backend -eq "mkl") { + # MKLROOT is what RandBLAS's MKL_sparse.cmake probes for mkl_spblas.h; + # its absence on other backends is what turns the MKL sparse path off. + Export-GitHubValue "MKLROOT" (Convert-ToCMakePath $mklRoot) + Export-GitHubValue "MKL_BIN" $mklBin +} Export-GitHubValue "googletest_PREFIX" (Convert-ToCMakePath $gtestInstall) Export-GitHubValue "Random123_DIR" (Convert-ToCMakePath (Join-Path $random123Install "include")) Export-GitHubValue "blaspp_DIR" (Convert-ToCMakePath $blasppDir) Export-GitHubValue "lapackpp_DIR" (Convert-ToCMakePath $lapackppDir) -if ($env:GITHUB_PATH) { Add-Content -Path $env:GITHUB_PATH -Value $mklBin } +if ($env:GITHUB_PATH -and $backendBin -ne "") { + Add-Content -Path $env:GITHUB_PATH -Value $backendBin +} Write-Host "All native Windows dependencies are ready under $resolvedRoot" diff --git a/.github/scripts/windows/assert-toolchain-refused.ps1 b/.github/scripts/windows/assert-toolchain-refused.ps1 new file mode 100644 index 00000000..6fd8384b --- /dev/null +++ b/.github/scripts/windows/assert-toolchain-refused.ps1 @@ -0,0 +1,34 @@ +# Asserts that install.ps1 REFUSES the toolchain currently on PATH, with a +# message containing -Expect. Used by core-windows.yaml's guard job to prove +# the architecture check is wired end to end, not just correct in isolation. +# +# The installer is launched exactly as INSTALL_WINDOWS.md prescribes -- via +# cmd with -ExecutionPolicy Bypass -- so the documented invocation stays under +# test alongside the guard itself. + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Expect +) + +$ErrorActionPreference = "Stop" + +$output = & cmd /c "powershell -ExecutionPolicy Bypass -File .\install\install.ps1 2>&1" +$installerExit = $LASTEXITCODE +$text = $output -join "`n" +Write-Host $text + +if ($installerExit -eq 0) { + throw "install.ps1 succeeded; the architecture guard did not fire." +} +if ($text -notmatch [regex]::Escape($Expect)) { + throw "The guard fired, but its output contained no '$Expect': the wrong check failed." +} +Write-Host "OK: refused at preflight with the expected explanation ('$Expect')." + +# This script succeeds when the installer FAILS, so the non-zero exit code +# that failure left behind has to be cleared: `shell: powershell` exits the +# step with whatever $LASTEXITCODE holds, which would report a pass as a +# failure. +exit 0 diff --git a/.github/scripts/windows/run-ci.ps1 b/.github/scripts/windows/run-ci.ps1 index e192cd9b..1e8dbab8 100644 --- a/.github/scripts/windows/run-ci.ps1 +++ b/.github/scripts/windows/run-ci.ps1 @@ -1,5 +1,6 @@ # Configures, builds, installs, and tests RandLAPACK natively on Windows with -# MSVC + oneMKL (ILP64, sequential). Shared between GitHub CI and local runs. +# MSVC and the selected BLAS/LAPACK backend (-Backend: mkl default, openblas, +# custom). Shared between GitHub CI and local runs. # # Local use, from an MSVC developer prompt in the repository root: # .github\scripts\windows\run-ci.ps1 -Task Core -SetupDependencies @@ -25,6 +26,9 @@ param( [string]$DependencyRoot = "", + [ValidateSet("mkl", "openblas", "custom")] + [string]$Backend = "mkl", + [switch]$SetupDependencies, [switch]$OpenMP, @@ -64,22 +68,38 @@ if ($DependencyRoot -eq "") { } if ($SetupDependencies) { + # -Yes: this path is CI (and local reproduction of a CI leg), so it must + # never stop on a question. & (Join-Path $SourceRoot ".github\actions\setup-randlapack-deps-windows\setup.ps1") ` - -DependencyRoot $DependencyRoot -SanitizeAddress:$SanitizeAddress + -DependencyRoot $DependencyRoot -Backend $Backend -Yes ` + -SanitizeAddress:$SanitizeAddress } $blasppDir = Require-EnvironmentVariable "blaspp_DIR" $lapackppDir = Require-EnvironmentVariable "lapackpp_DIR" $random123Dir = Require-EnvironmentVariable "Random123_DIR" $gtestPrefix = Require-EnvironmentVariable "googletest_PREFIX" -$mklRoot = Require-EnvironmentVariable "MKLROOT" -$mklBin = [Environment]::GetEnvironmentVariable("MKL_BIN") -if (-not $mklBin) { $mklBin = "$($mklRoot.Replace('/', '\'))\bin" } +$backendBin = [Environment]::GetEnvironmentVariable("RANDNLA_BLAS_BIN") +if (-not $backendBin) { + # Legacy fallback for environments set up by an older setup.ps1: derive + # the DLL directory from MKLROOT, accepting both oneAPI layouts. + $mklRoot = [Environment]::GetEnvironmentVariable("MKLROOT") + if ($mklRoot) { + $backendBin = @("$($mklRoot.Replace('/', '\'))\bin", + "$($mklRoot.Replace('/', '\'))\redist\intel64") | + Where-Object { Test-Path $_ } | Select-Object -First 1 + } +} +if (-not $backendBin) { + throw "RANDNLA_BLAS_BIN is not set. Run setup.ps1 (or pass -SetupDependencies)." +} -# oneMKL enters through raw library paths recorded by BLAS++, so its DLLs are -# not covered by the TARGET_RUNTIME_DLLS staging and must be on PATH for the -# build-time gtest discovery and for ctest. -$env:PATH = "$mklBin;$env:PATH" +# The BLAS backend enters through raw library paths recorded by BLAS++, so its +# DLLs are not covered by the TARGET_RUNTIME_DLLS staging. RANDLAPACK_RUNTIME_DLL_DIRS +# stages them beside RandLAPACK's executables; the process-PATH prepend below +# additionally covers the RandBLAS submodule's own executables until the +# RandBLAS-side staging lands (planned follow-up). +$env:PATH = "$backendBin;$env:PATH" $buildDir = Join-Path $WorkRoot "RandLAPACK-build" $installDir = Join-Path $WorkRoot "RandLAPACK-install" @@ -93,6 +113,8 @@ $configureArgs = @( "-Dlapackpp_DIR=$lapackppDir", "-DRandom123_DIR=$random123Dir", "-DCMAKE_PREFIX_PATH=$gtestPrefix", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + "-DRANDLAPACK_RUNTIME_DLL_DIRS=$($backendBin.Replace('\', '/'))", # The RandBLAS submodule's own tests are covered by RandBLAS's CI; # building and running them here roughly doubled the job time. "-DBUILD_TESTS=OFF" @@ -103,10 +125,8 @@ if ($SanitizeAddress) { $configureArgs += "-DSANITIZE_ADDRESS=ON" } Invoke-Checked "cmake" $configureArgs Invoke-Checked "cmake" @("--build", $buildDir, "--target", "install") -# Same exclusion as the Linux and macOS core jobs. Invoke-Checked "ctest" @( "--test-dir", $buildDir, - "--exclude-regex", "^TestABRIK\.ABRIK_catch_instability", "--output-on-failure") Write-Host "RandLAPACK Windows $Task validation succeeded." diff --git a/.github/scripts/windows/test-toolchain-guard.ps1 b/.github/scripts/windows/test-toolchain-guard.ps1 new file mode 100644 index 00000000..db6271e0 --- /dev/null +++ b/.github/scripts/windows/test-toolchain-guard.ps1 @@ -0,0 +1,87 @@ +# Table test for the toolchain architecture guard. +# +# Why this exists as a separate test rather than relying on the build matrix: +# the guard's whole job is to reject architectures we cannot build for, so +# there is no runner on which "it built fine" demonstrates it works. Driving +# the decision directly covers arm64 and arm on ordinary x64 hardware, and +# covers them in seconds. +# +# +# The integration counterpart lives in core-windows.yaml, which runs the real +# installer under a real x86 and a real cross-compiled arm64 toolchain and +# asserts it refuses. That proves the wiring; this proves the decision. + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +# Single shared implementation, dot-sourced by both install.ps1 and setup.ps1. +$source = Join-Path $PSScriptRoot "toolchain-arch.ps1" + +# Arch, whether it must be accepted, and a phrase the refusal must contain. +# x86 and arm64 must not merely both fail -- they must fail with *different* +# advice, since one is a wrong-shell mistake and the other is an unsupported +# platform. The Expect phrases pin that distinction. +$cases = @( + @{ Arch = "x64"; ShouldPass = $true; Expect = "" }, + @{ Arch = "amd64"; ShouldPass = $true; Expect = "" }, + @{ Arch = "x86"; ShouldPass = $false; Expect = "32-bit developer shell" }, + @{ Arch = "arm64"; ShouldPass = $false; Expect = "x64-only" }, + @{ Arch = "arm"; ShouldPass = $false; Expect = "x64-only" } +) + +function Get-GuardVerdicts { + # Loads the two functions out of $SourceFile without executing the rest + # of it, then drives them over the case table. + param([string]$SourceFile, [object[]]$Cases) + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + (Get-Content -Raw $SourceFile), [ref]$null, [ref]$null) + $definitions = foreach ($name in @("Get-ClTargetArchitecture", "Get-ToolchainArchitectureProblem")) { + $found = $ast.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name + }, $true) + if (-not $found) { throw "$SourceFile does not define $name." } + $found.Extent.Text + } + $body = @' +param($Cases) +foreach ($case in $Cases) { + $env:VSCMD_ARG_TGT_ARCH = $case.Arch + $detected = Get-ClTargetArchitecture + [pscustomobject]@{ + Arch = $case.Arch + Detected = $detected + Problem = (Get-ToolchainArchitectureProblem $detected) + } +} +'@ + $script = "param(`$Cases)`n" + ($definitions -join "`n") + "`n" + ($body -replace '^param\(\$Cases\)\r?\n', '') + return & ([scriptblock]::Create($script)) $Cases +} + +$savedArch = $env:VSCMD_ARG_TGT_ARCH +$failures = 0 +try { + $verdicts = @(Get-GuardVerdicts -SourceFile $source -Cases $cases) + for ($i = 0; $i -lt $cases.Count; $i++) { + $case = $cases[$i] + $verdict = $verdicts[$i] + $accepted = ($verdict.Problem -eq "") + $ok = ($accepted -eq $case.ShouldPass) + if ($ok -and $case.Expect -ne "" -and $verdict.Problem -notmatch [regex]::Escape($case.Expect)) { + $ok = $false + Write-Host " (refused, but the message lacked '$($case.Expect)')" + } + if (-not $ok) { $failures++ } + Write-Host ("{0} {1,-6} detected={2,-6} {3}" -f + $(if ($ok) { "OK " } else { "FAIL" }), + $case.Arch, $verdict.Detected, + $(if ($accepted) { "accepted" } else { "refused" })) + } +} finally { + $env:VSCMD_ARG_TGT_ARCH = $savedArch +} + +Write-Host "" +if ($failures -gt 0) { throw "$failures toolchain-guard assertion(s) failed." } +Write-Host "All toolchain-guard assertions passed." diff --git a/.github/scripts/windows/toolchain-arch.ps1 b/.github/scripts/windows/toolchain-arch.ps1 new file mode 100644 index 00000000..42cb8d0d --- /dev/null +++ b/.github/scripts/windows/toolchain-arch.ps1 @@ -0,0 +1,75 @@ +# Toolchain architecture detection, shared by install/install.ps1 (user-facing +# preflight) and .github/actions/setup-randlapack-deps-windows/setup.ps1 (which +# also runs standalone in CI). Dot-source it; it defines functions only. +# +# Why this check exists: RandLAPACK and every BLAS backend the installer +# provisions are 64-bit, but the "Developer PowerShell for VS" and "Developer +# Command Prompt for VS" Start-menu entries both default to an *x86* toolchain. +# An x86 linker cannot use an x64 import library, and the failure surfaces +# three layers down as BLAS++ reporting "BLAS library not found" -- which +# blames the libraries when the compiler is at fault. Note that the shell's own +# bitness is not a usable signal: the Developer Command Prompt is a 64-bit +# process that still selects x86 tools. + +function Get-ClTargetArchitecture { + # Returns the compiler's TARGET architecture, lowercased ("x64", "x86", + # "arm64", "arm"), or "" if it genuinely cannot be determined. + # + # Three independent signals, most reliable first -- the same + # probe-several-things approach Find-OneMklLayout uses, and for the same + # reason: a missed detection here fails *open*, which defeats the check. + # 1. VSCMD_ARG_TGT_ARCH, exported by vcvarsall.bat / VsDevCmd (and so + # by ilammy/msvc-dev-cmd in CI). Never localized. + # 2. The toolset path: MSVC lays cl.exe out as + # ...\bin\Host\\cl.exe, a stable convention. + # 3. The banner, last, for anything matching neither of the above. + # On its own this would be wrong on a localized Visual Studio, where + # the words around the architecture are translated. + if ($env:VSCMD_ARG_TGT_ARCH) { return $env:VSCMD_ARG_TGT_ARCH.ToLowerInvariant() } + $cl = Get-Command "cl.exe" -ErrorAction SilentlyContinue + if (-not $cl) { return "" } + if ($cl.Source -match '\\bin\\Host[^\\]+\\([^\\]+)\\cl\.exe$') { + return $Matches[1].ToLowerInvariant() + } + # Native stderr merged via 2>&1 becomes ErrorRecords, which would throw + # under $ErrorActionPreference = "Stop"; relax it for this one call. + $previous = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $banner = (& $cl.Source 2>&1 | Out-String) + } finally { + $ErrorActionPreference = $previous + } + if ($banner -match '\bfor\s+(x64|x86|ARM64|ARM)\b') { return $Matches[1].ToLowerInvariant() } + return "" +} + +function Get-ToolchainArchitectureProblem { + # Returns a description of why $Arch is unusable, or "" if it is fine. + # x86 and ARM64 fail for completely different reasons and deserve + # different advice: x86 means the wrong shell was opened and is a + # one-command fix, ARM64 means the platform is genuinely unsupported. + param([string]$Arch) + if ($Arch -eq "" -or $Arch -eq "x64" -or $Arch -eq "amd64") { return "" } + if ($Arch -eq "x86") { + # Single-quoted: the cmd one-liner contains both double quotes and + # backticks, which are literal here but would need escaping in a + # double-quoted PowerShell string. + $vcvarsHint = 'for /f "usebackq delims=" %i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath`) do call "%i\VC\Auxiliary\Build\vcvars64.bat"' + return ("cl.exe targets x86, but RandLAPACK and its BLAS/LAPACK backends are 64-bit " + + "(x64).`n" + + " You are in a 32-bit developer shell. 'Developer PowerShell for VS 2022' and " + + "'Developer Command Prompt for VS 2022' both default to x86.`n" + + " Fix: open 'x64 Native Tools Command Prompt for VS 2022' from the Start menu, " + + "or run this in any Command Prompt (any edition or version):`n" + + " $vcvarsHint`n" + + " Then delete the RandNLA-project directory before retrying: dependencies already " + + "configured by the x86 compiler are reused as-is and would keep failing.") + } + return ("cl.exe targets $Arch, which this installer does not support: the Windows build " + + "is x64-only.`n" + + " Intel oneMKL publishes no $Arch build, and the OpenBLAS binaries pinned here are " + + "x64. Supplying an $Arch BLAS/LAPACK through -Backend custom is the only route, and " + + "it is untested.`n" + + " If you meant to build x64, open 'x64 Native Tools Command Prompt for VS 2022'.") +} diff --git a/.github/workflows/core-linux.yaml b/.github/workflows/core-linux.yaml index c060fdff..881db980 100644 --- a/.github/workflows/core-linux.yaml +++ b/.github/workflows/core-linux.yaml @@ -4,9 +4,19 @@ on: branches-ignore: - cqrrp-gpu-benchmarking workflow_dispatch: + # Only main. A branch with an open pull request is already covered by the + # pull_request event above, and firing on both ran every job twice per + # commit -- same SHA, same result. push: - branches-ignore: - - cqrrp-gpu-benchmarking + branches: + - main + +# One run per ref at a time. Pushing a fix used to leave the superseded run +# going to completion. Superseding is only safe for pull requests: on main we +# want every commit validated, not just the newest. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} # BLAS++/LAPACK++/Random123 track their upstream default branches but change # rarely; their installs are cached below. Bump the -v suffix in the cache key diff --git a/.github/workflows/core-macos.yaml b/.github/workflows/core-macos.yaml index 6ec0f90f..2a86325e 100644 --- a/.github/workflows/core-macos.yaml +++ b/.github/workflows/core-macos.yaml @@ -4,9 +4,19 @@ on: branches-ignore: - cqrrp-gpu-benchmarking workflow_dispatch: + # Only main. A branch with an open pull request is already covered by the + # pull_request event above, and firing on both ran every job twice per + # commit -- same SHA, same result. push: - branches-ignore: - - cqrrp-gpu-benchmarking + branches: + - main + +# One run per ref at a time. Pushing a fix used to leave the superseded run +# going to completion. Superseding is only safe for pull requests: on main we +# want every commit validated, not just the newest. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} # Dependency installs are cached and the RandBLAS submodule's own test suite # is disabled; see core-linux.yaml for the reasoning. diff --git a/.github/workflows/core-windows.yaml b/.github/workflows/core-windows.yaml index 60d270db..6349301a 100644 --- a/.github/workflows/core-windows.yaml +++ b/.github/workflows/core-windows.yaml @@ -4,22 +4,89 @@ on: branches-ignore: - cqrrp-gpu-benchmarking workflow_dispatch: + # Only main. A branch with an open pull request is already covered by the + # pull_request event above, and firing on both ran every job twice per + # commit -- same SHA, same result. push: - branches-ignore: - - cqrrp-gpu-benchmarking + branches: + - main + +# One run per ref at a time. Pushing a fix used to leave the superseded run +# going to completion. Superseding is only safe for pull requests: on main we +# want every commit validated, not just the newest. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # Structural gate for the documented *user* path, which the build matrix + # below cannot cover: it initializes MSVC with an explicit arch: x64 under + # pwsh, so it has never exercised the shell the install docs tell users to + # open. That gap is exactly how a 32-bit toolchain reached a collaborator and + # failed three layers down as "BLAS library not found". + # + # One job, not three: every check here asserts a *refusal* or runs pure + # logic, so each takes seconds and runner start-up dominates. Splitting them + # would triple the Windows runner count for no extra coverage. + windows-toolchain-guards: + name: windows-toolchain-guards + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + # Pure logic, no MSVC needed. This is the only way to cover arm64 and + # arm: we cannot build for them, so no build leg could ever test them. + - name: architecture guard decision table + shell: powershell + run: .\.github\scripts\windows\test-toolchain-guard.ps1 + + # Integration counterparts. x86 is the real-world case; amd64_arm64 + # cross-compiles, yielding an arm64-targeting cl.exe on an ordinary x64 + # runner, so the arm branch is exercised without ARM hardware. + - name: initialize MSVC (x86) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x86 + + # Windows PowerShell 5.1, not pwsh: this is what a user actually gets, + # and the launch line is verbatim the one INSTALL_WINDOWS.md prescribes, + # so the documented invocation itself stays under test. + - name: install.ps1 must refuse an x86 toolchain + shell: powershell + run: .\.github\scripts\windows\assert-toolchain-refused.ps1 -Expect "targets x86" + + - name: initialize MSVC (amd64_arm64) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64_arm64 + + - name: install.ps1 must refuse an arm64 toolchain + shell: powershell + run: .\.github\scripts\windows\assert-toolchain-refused.ps1 -Expect "targets arm64" + build-windows: - name: windows-msvc-mkl-ilp64-${{ matrix.label }} + name: windows-msvc-${{ matrix.backend }}-${{ matrix.int }}-${{ matrix.label }} runs-on: windows-2022 strategy: fail-fast: false + # Deliberately small matrix (see docs/CI.md): the mkl legs cover the + # default backend serial + OpenMP; ONE openblas leg covers the non-MKL + # provisioning and LP64 build. No openblas-openmp (OpenMP x MSVC is + # backend-orthogonal and already covered) and no Windows ASan lane. matrix: include: - label: serial + backend: mkl + int: ilp64 openmp: false - label: openmp + backend: mkl + int: ilp64 openmp: true + - label: serial + backend: openblas + int: lp64 + openmp: false steps: - uses: actions/checkout@v4 with: @@ -32,6 +99,8 @@ jobs: - name: setup native Windows dependencies uses: ./.github/actions/setup-randlapack-deps-windows + with: + blas-backend: ${{ matrix.backend }} - name: build, install, and test RandLAPACK shell: pwsh @@ -40,4 +109,27 @@ jobs: & "$env:GITHUB_WORKSPACE/.github/scripts/windows/run-ci.ps1" ` -Task Core ` -WorkRoot "$env:GITHUB_WORKSPACE/../RandLAPACK-windows-ci" ` + -Backend '${{ matrix.backend }}' ` -OpenMP:$openmp + + # Staged executables must run with NO PATH preparation: this is the + # regression gate for app-local DLL staging (Windows searches the exe's + # directory first, so a staged exe needs nothing else). + - name: run a staged test executable with a stripped PATH + shell: pwsh + run: | + $exe = Get-ChildItem "$env:GITHUB_WORKSPACE/../RandLAPACK-windows-ci/RandLAPACK-build" ` + -Recurse -Filter "RandLAPACK_tests.exe" | Select-Object -First 1 + if (-not $exe) { throw "RandLAPACK_tests.exe not found in the build tree." } + $env:PATH = "C:\Windows\System32;C:\Windows" + # Starting the process at all forces the loader to resolve every + # statically imported DLL (BLAS++, LAPACK++, the BLAS backend) from + # the staged copies; listing tests proves it end to end. Capture + # the full output first -- an early-terminating pipeline (e.g. + # Select-Object -First) would kill the process before it exits and + # leave $LASTEXITCODE unset. + $output = & $exe.FullName --gtest_list_tests 2>&1 + $exit = $LASTEXITCODE + $output | Select-Object -First 5 | Out-Host + Write-Host "... ($($output.Count) lines total, exit code $exit)" + if ($exit -ne 0) { throw "Staged executable failed with a stripped PATH (exit $exit)." } diff --git a/.github/workflows/install-script.yaml b/.github/workflows/install-script.yaml index 50276d1d..d78fffd8 100644 --- a/.github/workflows/install-script.yaml +++ b/.github/workflows/install-script.yaml @@ -16,9 +16,19 @@ on: branches-ignore: - cqrrp-gpu-benchmarking workflow_dispatch: + # Only main. A branch with an open pull request is already covered by the + # pull_request event above, and firing on both ran every job twice per + # commit -- same SHA, same result. push: - branches-ignore: - - cqrrp-gpu-benchmarking + branches: + - main + +# One run per ref at a time. Pushing a fix used to leave the superseded run +# going to completion. Superseding is only safe for pull requests: on main we +# want every commit validated, not just the newest. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: install-linux: diff --git a/CMake/RandLAPACKConfig.cmake.in b/CMake/RandLAPACKConfig.cmake.in index 96e773ba..8b3436c7 100644 --- a/CMake/RandLAPACKConfig.cmake.in +++ b/CMake/RandLAPACKConfig.cmake.in @@ -25,4 +25,12 @@ if (NOT lapackpp_DIR) endif() find_dependency(lapackpp) +# Runtime-DLL staging for downstream Windows executables: include the helper +# and seed the DLL directories recorded when RandLAPACK was configured, so a +# consumer can simply call randlapack_stage_runtime_dlls(). +if (NOT DEFINED RANDLAPACK_RUNTIME_DLL_DIRS OR RANDLAPACK_RUNTIME_DLL_DIRS STREQUAL "") + set(RANDLAPACK_RUNTIME_DLL_DIRS "@RandLAPACK_configured_runtime_dll_dirs@") +endif() +include("${CMAKE_CURRENT_LIST_DIR}/rl_runtime_dlls.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/RandLAPACKTargets.cmake") diff --git a/CMake/rl_build_options.cmake b/CMake/rl_build_options.cmake index 65734973..4f80eae6 100644 --- a/CMake/rl_build_options.cmake +++ b/CMake/rl_build_options.cmake @@ -12,8 +12,32 @@ endif() set(SANITIZE_ADDRESS OFF CACHE BOOL "Add address sanitizer flags to the library") message(STATUS "Checking for OpenMP ... ") + +# This find_package runs BEFORE add_subdirectory(RandBLAS), so the guard in +# RandBLAS/CMake/OpenMP.cmake ("if NOT DEFINED OpenMP_CXX_FLAGS") never +# fires: whatever flavor is cached here is what every target gets. MSVC's +# classic /openmp implements OpenMP 2.0 only and silently *ignores* the +# collapse clause (warning C4849) that rl_rpchol relies on; /openmp:llvm +# supports 64-bit loop indices and collapse. Mirror RandBLAS's guard here. +# Callers can still override with -DOpenMP_CXX_FLAGS=... at configure time. +if (MSVC AND NOT DEFINED OpenMP_CXX_FLAGS) + set(OpenMP_CXX_FLAGS "/openmp:llvm" CACHE STRING + "OpenMP compiler flags for C++") +endif() +if (MSVC) + set(RandLAPACK_OpenMP_MSVC_FLAGS "${OpenMP_CXX_FLAGS}") +endif() + find_package(OpenMP COMPONENTS CXX) +# FindOpenMP may replace OpenMP_CXX_FLAGS while probing the compiler. Ensure +# the imported target carries the requested MSVC mode. +if (MSVC AND OpenMP_CXX_FOUND AND TARGET OpenMP::OpenMP_CXX) + set_property(TARGET OpenMP::OpenMP_CXX PROPERTY + INTERFACE_COMPILE_OPTIONS "${RandLAPACK_OpenMP_MSVC_FLAGS}") + set(OpenMP_CXX_FLAGS "${RandLAPACK_OpenMP_MSVC_FLAGS}") +endif() + set(tmp FALSE) if (OpenMP_CXX_FOUND) set(tmp TRUE) diff --git a/CMake/rl_config.cmake b/CMake/rl_config.cmake index 7e43e0d6..1765f392 100644 --- a/CMake/rl_config.cmake +++ b/CMake/rl_config.cmake @@ -3,6 +3,7 @@ # syntax. Native Windows backslashes would otherwise be parsed as escape # sequences when a downstream project loads RandLAPACKConfig.cmake. file(TO_CMAKE_PATH "${RandLAPACK_lapackpp_DIR}" RandLAPACK_lapackpp_DIR) +file(TO_CMAKE_PATH "${RANDLAPACK_RUNTIME_DLL_DIRS}" RandLAPACK_configured_runtime_dll_dirs) configure_file(CMake/RandLAPACKConfig.cmake.in ${CMAKE_INSTALL_LIBDIR}/cmake/RandLAPACK/RandLAPACKConfig.cmake @ONLY) @@ -13,4 +14,5 @@ configure_file(CMake/RandLAPACKConfigVersion.cmake.in install(FILES ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/RandLAPACK/RandLAPACKConfig.cmake ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/RandLAPACK/RandLAPACKConfigVersion.cmake + ${CMAKE_SOURCE_DIR}/CMake/rl_runtime_dlls.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/RandLAPACK) diff --git a/CMake/rl_runtime_dlls.cmake b/CMake/rl_runtime_dlls.cmake index c5d12311..eb687e6f 100644 --- a/CMake/rl_runtime_dlls.cmake +++ b/CMake/rl_runtime_dlls.cmake @@ -1,9 +1,20 @@ -# Native Windows builds use TARGET_RUNTIME_DLLS to stage imported shared-library -# dependencies (BLAS++/LAPACK++ DLLs) beside executables, so that test discovery -# and test runs find them without PATH edits. The generator expression requires -# CMake 3.21, which is already this project's minimum. Note this only covers -# CMake imported targets: oneMKL enters through raw library paths recorded by -# BLAS++, so the MKL bin directory must be on PATH at build and test time. +# Native Windows builds stage every runtime DLL an executable needs beside +# that executable (app-local deployment, the idiomatic Windows layout: the +# exe's own directory is the first place the loader searches). Two sources: +# +# 1. TARGET_RUNTIME_DLLS (CMake >= 3.21, this project's minimum) covers +# imported SHARED targets -- BLAS++/LAPACK++ DLLs. +# 2. RANDLAPACK_RUNTIME_DLL_DIRS covers what the generator expression +# cannot see: the BLAS backend (oneMKL, OpenBLAS, ...) enters BLAS++ as +# raw library paths, i.e. UNKNOWN imported targets, which +# TARGET_RUNTIME_DLLS documentedly ignores. The installer and CI set +# this to the backend's DLL directory; its *.dll contents are staged +# alongside each executable. +# +# With both in place, staged executables run without any PATH preparation. +set(RANDLAPACK_RUNTIME_DLL_DIRS "" CACHE STRING + "Semicolon-separated directories whose DLLs are staged beside RandLAPACK executables on Windows.") + function(randlapack_stage_runtime_dlls target) if (WIN32) add_custom_command( @@ -15,5 +26,18 @@ function(randlapack_stage_runtime_dlls target) COMMAND_EXPAND_LISTS VERBATIM ) + foreach(dll_dir IN LISTS RANDLAPACK_RUNTIME_DLL_DIRS) + file(GLOB dlls "${dll_dir}/*.dll") + if (dlls) + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${dlls} + $ + VERBATIM + ) + endif() + endforeach() endif() endfunction() diff --git a/INSTALL.md b/INSTALL.md index 9ec1ee66..942df8ce 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -20,6 +20,9 @@ of the corresponding instructions in Section 1. - GCC 11 or higher - Clang 14 or higher (not extensively tested) - Intel ICPX (has known issues, see GitHub issue #91) + - MSVC (Visual Studio 2022) on native Windows, with Ninja as the build + tool (both bundled with the "Desktop development with C++" workload) -- + see [INSTALL_WINDOWS.md](INSTALL_WINDOWS.md) ### GPU Support (Optional) For GPU/CUDA support (enabled with `-DRequireCUDA=ON`), you need: @@ -301,36 +304,17 @@ We do that so that it's easier to infer a valid choice of directory structure fo ## 6. Native Windows (MSVC) -RandLAPACK builds natively on Windows with MSVC (Visual Studio 2022) and -Intel oneMKL using ILP64, sequential linking. The easy path, from a -"Developer PowerShell for VS 2022" prompt in the repository root: +RandLAPACK builds natively on Windows with MSVC (Visual Studio 2022). The +full guide -- prerequisites, the one-command installer, BLAS/LAPACK backend +choice (oneMKL default, OpenBLAS, bring-your-own), runtime-DLL handling, and +how the Windows install differs from Linux/macOS -- lives in +[INSTALL_WINDOWS.md](INSTALL_WINDOWS.md). The short version, from an +"x64 Native Tools Command Prompt for VS 2022" (the plain "Developer +PowerShell/Command Prompt for VS 2022" entries default to a 32-bit toolchain +and will not link the x64 BLAS libraries): -```powershell -.\install\install.ps1 +```bat +git clone --recursive https://github.com/BallisticLA/RandLAPACK.git +cd RandLAPACK +powershell -ExecutionPolicy Bypass -File .\install\install.ps1 ``` - -This builds all dependencies (oneMKL through vcpkg, or pass -`-MklRoot ` to use an existing oneAPI install), then builds, installs, -and tests RandLAPACK, mirroring `install.sh`'s directory layout. The header -comment in `install.ps1` documents the options. - -Points worth knowing: - -- **ILP64 is required.** RandBLAS's MKL sparse backend checks at compile time - that its 64-bit indices match `MKL_INT`, so BLAS++ must be configured with - `-Dblas_int=ilp64` against `mkl_intel_ilp64_dll.lib` (the installer does - this for you). -- **BLAS++/LAPACK++ come from BallisticLA branches** (`BallisticLA/blaspp@remove-symv-debug-print`, - `BallisticLA/lapackpp@msvc-direct-includes`) until two one-line MSVC fixes - merge upstream (blaspp PR #132, lapackpp PR #87). -- **OpenMP on MSVC uses the `/openmp:llvm` runtime**, selected automatically - by RandBLAS's build system — the only MSVC mode that accepts RandLAPACK's - 64-bit loop indices and `collapse` clauses. Serial builds (OpenMP off) are - also fully functional. -- **GPU support is not available on native Windows.** -- Executables that link MKL need the MKL `bin` directory on `PATH` at run - time; the installer prints the path to keep. - -CI runs this configuration on every pull request (`core-windows` workflow); -`.github/scripts/windows/run-ci.ps1 -Task Core -SetupDependencies` reproduces -that job locally. diff --git a/INSTALL_SCRIPT.md b/INSTALL_SCRIPT.md index 866a602b..0d348333 100644 --- a/INSTALL_SCRIPT.md +++ b/INSTALL_SCRIPT.md @@ -1,349 +1,355 @@ -# Using RandLAPACK's Automated Install Script - -The installer scripts live in the `install/` directory (`install/install.sh` -for Linux/macOS, `install/install.ps1` for native Windows); a small wrapper is -kept at the repository root so `bash install.sh` keeps working. - -This guide explains how to use the `install.sh` script to automatically install -RandLAPACK and all of its dependencies (BLAS++, LAPACK++, Random123) with a -single command. - -**When to use this guide:** Use this automated installation method if you want -a quick, streamlined setup process. If you need fine-grained control over -dependency configurations, refer to RandLAPACK's `INSTALL.md` instead. - -## 0. Software Requirements - -Before running the install script, ensure you have the following software -available on your system: - -### Essential Requirements -* **C++ Compiler:** GNU GCC 13.3.0 or higher (required for C++20 features) -* **CMake:** Version 3.27 or higher -* **BLAS/LAPACK Library:** Intel MKL 2022 or higher recommended -* **GoogleTest:** (Optional but recommended) For running RandLAPACK tests - -### GPU Support Requirements (Optional) -* **CUDA Toolkit:** Version 12.4.1 or higher - - **Recommended:** CUDA 12.9.0 + GCC 13.3.0 (verified working as of 2025-11-26) - - **IMPORTANT:** CUDA versions have strict GCC compatibility requirements: - - CUDA 12.9.0: Compatible with GCC 13.x ✓ - - CUDA 12.4.1: Compatible with GCC 13.x ✓ - - CUDA 12.2.1: Requires GCC ≤ 12.x (GCC 13.x will fail with "unsupported GNU version") - - See `INSTALL.md` Section 0 for full compatibility matrix - - Ensure compatible NVIDIA driver (v580+ recommended for CUDA 12.9) -* **CUDA Libraries:** cuBLAS and cuSOLVER (included with CUDA Toolkit) - -### Installing Requirements with Spack - -We strongly recommend using [Spack](https://github.com/spack/spack) to manage -these dependencies. A typical Spack installation would look like: - -```shell -# Step 1: Install the compiler FIRST -spack install gcc@13.3.0 - -# Step 2: Register the new compiler with Spack -spack compiler find - -# Step 3: Load the compiler -spack load gcc@13.3.0 - -# Step 4: Install all other dependencies using the new compiler -spack install cmake@3.27 -spack install intel-oneapi-mkl -spack install googletest - -# For GPU support -spack install cuda@12.9.0 -``` - -**IMPORTANT:** The compiler must be installed, registered with `spack compiler find`, -and loaded *before* installing other dependencies. This ensures all packages are -built with the correct compiler version. Spack will automatically use the loaded -compiler for subsequent package installations. - -After installation, load the environment: -```shell -spack load gcc@13.3.0 -spack load cmake -spack load intel-oneapi-mkl -spack load googletest -spack load cuda@12.9.0 # If GPU support needed -``` - -**Pro tip:** Add the spack load commands to your `~/.bashrc` to automatically -load the environment in every shell session. Make sure to load the compiler first -in your `.bashrc`. - -## 1. Preparing for Installation - -### Directory Structure - -The install script expects a specific directory structure: - -``` -~/RandNLA/ -├── RandLAPACK/ # Clone RandLAPACK here (script will move it) -└── RandNLA-project/ # Created automatically by script - ├── lib/ - │ ├── blaspp/ # Built by script - │ ├── lapackpp/ # Built by script - │ ├── random123/ # Built by script - │ └── RandLAPACK/ # Moved here by script - └── build/ # Build artifacts -``` - -### Initial Setup - -1. Create the base directory: - ```shell - mkdir -p ~/RandNLA - cd ~/RandNLA - ``` - -2. Clone RandLAPACK repository: - ```shell - git clone --recursive https://github.com/BallisticLA/RandLAPACK.git - cd RandLAPACK - ``` - -3. **(Important)** Switch to the correct development branch if needed: - ```shell - git checkout - ``` - - **Note:** Always verify with the development team which branch to use for - the latest GPU support and stability improvements. - -## 2. Running the Install Script - -### Basic Usage - -From inside the `RandLAPACK` directory: - -```shell -bash install.sh -``` - -The script will: -1. Detect if GPU hardware is available on your system and, on a terminal, - ask whether to build with CUDA support -2. Automatically clone and build all dependencies (or reuse preinstalled - ones, see the discovery variables below) -3. Build RandLAPACK with appropriate configuration -4. Build test and benchmark executables - -Run `bash install.sh --help` for the full option list. The main flags, each -with an environment-variable equivalent: - -``` --y, --yes assume "yes" for every prompt - --gpu / --no-gpu decide GPU support without asking --j, --jobs parallel build jobs (default: number of cores) - --fresh clear build directories first (default: reuse them, - so re-running is an incremental rebuild) - --modify-rc append RANDNLA_PROJECT_DIR/RANDNLA_PROJECT_GPU_AVAIL - exports to your shell config (default: never touch it; - the summary prints the lines to add yourself) - --project-dir place/locate RandNLA-project at D -``` - -### Automated Installation (Non-Interactive) - -Prompts appear only when stdin is a terminal. Piped and CI runs are already -non-interactive with safe defaults (NVIDIA detected: GPU build; AMD or no -GPU: CPU build), so no `yes |` piping is needed: - -```shell -bash install.sh < /dev/null # or simply: bash install.sh --yes -``` - -### Installation Logging - -All compiler output goes to `/install.log` automatically; the -console shows one line per step, and any failure prints the log path plus -the last lines of the log. There is no need to tee the output yourself. - -## 3. What the Script Does - -The `install.sh` script performs the following steps automatically: - -1. **Creates Project Structure** - - Creates `~/RandNLA/RandNLA-project/` directory tree - - Sets up subdirectories for libraries and build artifacts - -2. **Builds BLAS++** - - Clones BLAS++ from official repository - - Configures with appropriate BLAS backend (MKL if available) - - Builds with GPU support if requested - - Installs to `~/RandNLA/RandNLA-project/lib/blaspp/` - -3. **Builds LAPACK++** - - Clones LAPACK++ from official repository - - Configures to use previously built BLAS++ - - Builds with GPU support if requested - - Installs to `~/RandNLA/RandNLA-project/lib/lapackpp/` - -4. **Installs Random123** - - Clones Random123 header-only library - - Installs headers to `~/RandNLA/RandNLA-project/lib/random123/` - -5. **Moves and Builds RandLAPACK** - - Moves `RandLAPACK` directory to `~/RandNLA/RandNLA-project/lib/` - - Configures CMake with all dependency paths - - Builds RandLAPACK library - - Builds test suite and benchmarks - - Creates executables in `~/RandNLA/RandNLA-project/build/RandLAPACK-build/bin/` - -## 4. Verifying the Installation - -### Running Tests - -After installation completes, verify everything works correctly: - -```shell -cd ~/RandNLA/RandNLA-project/build/RandLAPACK-build -ctest -``` - -This runs the complete test suite (456 tests). Expected output: -``` -99% tests passed, 1 tests failed out of 456 -Total Test time (real) = 124.62 sec -``` - -**Note:** Some test failures are known and acceptable in development branches. -Consult the development team if you see unexpected failures. - -### Running GPU Tests Only - -If you enabled GPU support, test GPU functionality specifically: - -```shell -./bin/RandLAPACK_tests_gpu -``` - -Expected output: 13-14 GPU tests should pass within 15-20 seconds. - -## 5. Working with the Installed Project - -### Key File Locations - -After installation: - -* **RandLAPACK library:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/libRandLAPACK.a` -* **Headers:** `~/RandNLA/RandNLA-project/lib/RandLAPACK/RandLAPACK/` -* **Tests:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/bin/RandLAPACK_tests*` -* **Benchmarks:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/bin/RandLAPACK_bench*` -* **CMake config:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/RandLAPACKConfig.cmake` - -### Recompiling After Code Changes - -If you modify RandLAPACK source code: - -```shell -cd ~/RandNLA/RandNLA-project/build/RandLAPACK-build -source ~/.bashrc # Ensures environment is loaded -make -j -``` - -**Important:** Always source your `.bashrc` (or equivalent environment setup) -before running `make` to ensure CUDA libraries and other dependencies are in -your `LD_LIBRARY_PATH`. - -### Using RandLAPACK in Your Own Projects - -See Section 4 of `INSTALL.md` for details on linking RandLAPACK to external -CMake projects. You'll need to specify: - -```cmake --Dblaspp_DIR=~/RandNLA/RandNLA-project/lib/blaspp/lib/cmake/blaspp --Dlapackpp_DIR=~/RandNLA/RandNLA-project/lib/lapackpp/lib/cmake/lapackpp --DRandBLAS_DIR=~/RandNLA/RandNLA-project/build/RandLAPACK-build/RandBLAS --DRandLAPACK_DIR=~/RandNLA/RandNLA-project/build/RandLAPACK-build -``` - ---- - -## Building and Running GPU Benchmarks - -GPU benchmarks are in the `benchmark/` directory and must be built separately from the main RandLAPACK project. - -### Prerequisites - -- RandLAPACK must already be built and installed with CUDA support (`-DRequireCUDA=ON`) -- CUDA Toolkit must be available on your system -- GPU hardware must be available - -### Building GPU Benchmarks - -Navigate to the benchmark directory and build as a standalone project: - -```shell -cd ~/RandNLA/RandNLA-project/lib/RandLAPACK/benchmark -mkdir -p build -cd build -cmake \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_COMPILER=g++ \ - -DRandLAPACK_DIR=~/RandNLA/RandNLA-project/install/RandLAPACK-install/lib/cmake/RandLAPACK \ - .. -make -j -``` - -**Note:** Adjust the `RandLAPACK_DIR` path to match your installation location. - -### Running GPU Benchmarks - -#### BQRRP GPU Benchmark - -The BQRRP GPU benchmark supports two modes: - -**Block size sweep** (default): -```shell -./BQRRP_GPU_benchmark block_size [matrix_size] [profile_runtime] [run_qrf] -``` - -Examples: -```shell -# Run with default settings (16384x16384 matrix) -./BQRRP_GPU_benchmark block_size - -# Run with 32768x32768 matrix -./BQRRP_GPU_benchmark block_size 32768 - -# Run with profiling enabled and QRF comparison -./BQRRP_GPU_benchmark block_size 16384 1 1 -``` - -**Matrix size sweep**: -```shell -./BQRRP_GPU_benchmark mat_size [profile_runtime] [run_qrf] -``` - -Examples: -```shell -# Run with default settings -./BQRRP_GPU_benchmark mat_size - -# Run with profiling disabled but QRF comparison enabled -./BQRRP_GPU_benchmark mat_size 0 1 -``` - -### Output Files - -The benchmarks generate text files with timing results in the current directory: - -- `_BQRRP_GPU_speed_comparisons_block_size_*.txt` - Speed comparison results for block size sweep -- `BQRRP_GPU_speed_comparisons_mat_size_*.txt` - Speed comparison results for matrix size sweep -- `_BQRRP_GPU_runtime_breakdown_qrf_*.txt` - Detailed profiling with QRF (if profiling enabled) -- `_BQRRP_GPU_runtime_breakdown_cholqr_*.txt` - Detailed profiling with CholQR (if profiling enabled) - -**Last Updated:** 2025-11-26 -**Tested With:** -- GCC 13.3.0 -- CMake 3.31.9 -- CUDA 12.9.0 -- Intel MKL 2025.0.3 -- Ubuntu 22.04 / WSL2 +# Using RandLAPACK's Automated Install Script + +The installer scripts live in the `install/` directory (`install/install.sh` +for Linux/macOS, `install/install.ps1` for native Windows); a small wrapper is +kept at the repository root so `bash install.sh` keeps working. + +This guide explains how to use the `install.sh` script to automatically install +RandLAPACK and all of its dependencies (BLAS++, LAPACK++, Random123) with a +single command. + +**When to use this guide:** Use this automated installation method if you want +a quick, streamlined setup process. If you need fine-grained control over +dependency configurations, refer to RandLAPACK's `INSTALL.md` instead. + +> **Windows users:** this document describes the Linux/macOS installer +> (`install.sh`). The native Windows companion is `install\install.ps1`, +> documented in [INSTALL_WINDOWS.md](INSTALL_WINDOWS.md) -- prerequisites, +> every option, backend selection, and troubleshooting. + +## 0. Software Requirements + +Before running the install script, ensure you have the following software +available on your system: + +### Essential Requirements +* **C++ Compiler:** GNU GCC 13.3.0 or higher (required for C++20 features) +* **CMake:** Version 3.21 or higher (the project's CMake floor; recent + releases recommended) +* **BLAS/LAPACK Library:** Intel MKL 2022 or higher recommended +* **GoogleTest:** (Optional but recommended) For running RandLAPACK tests + +### GPU Support Requirements (Optional) +* **CUDA Toolkit:** Version 12.4.1 or higher + - **Recommended:** CUDA 12.9.0 + GCC 13.3.0 (verified working as of 2025-11-26) + - **IMPORTANT:** CUDA versions have strict GCC compatibility requirements: + - CUDA 12.9.0: Compatible with GCC 13.x ✓ + - CUDA 12.4.1: Compatible with GCC 13.x ✓ + - CUDA 12.2.1: Requires GCC ≤ 12.x (GCC 13.x will fail with "unsupported GNU version") + - See `INSTALL.md` Section 0 for full compatibility matrix + - Ensure compatible NVIDIA driver (v580+ recommended for CUDA 12.9) +* **CUDA Libraries:** cuBLAS and cuSOLVER (included with CUDA Toolkit) + +### Installing Requirements with Spack + +We strongly recommend using [Spack](https://github.com/spack/spack) to manage +these dependencies. A typical Spack installation would look like: + +```shell +# Step 1: Install the compiler FIRST +spack install gcc@13.3.0 + +# Step 2: Register the new compiler with Spack +spack compiler find + +# Step 3: Load the compiler +spack load gcc@13.3.0 + +# Step 4: Install all other dependencies using the new compiler +spack install cmake@3.31.9 +spack install intel-oneapi-mkl +spack install googletest + +# For GPU support +spack install cuda@12.9.0 +``` + +**IMPORTANT:** The compiler must be installed, registered with `spack compiler find`, +and loaded *before* installing other dependencies. This ensures all packages are +built with the correct compiler version. Spack will automatically use the loaded +compiler for subsequent package installations. + +After installation, load the environment: +```shell +spack load gcc@13.3.0 +spack load cmake +spack load intel-oneapi-mkl +spack load googletest +spack load cuda@12.9.0 # If GPU support needed +``` + +**Pro tip:** Add the spack load commands to your `~/.bashrc` to automatically +load the environment in every shell session. Make sure to load the compiler first +in your `.bashrc`. + +## 1. Preparing for Installation + +### Directory Structure + +The install script expects a specific directory structure: + +``` +~/RandNLA/ +├── RandLAPACK/ # Clone RandLAPACK here (script will move it) +└── RandNLA-project/ # Created automatically by script + ├── lib/ + │ ├── blaspp/ # Built by script + │ ├── lapackpp/ # Built by script + │ ├── random123/ # Built by script + │ └── RandLAPACK/ # Moved here by script + └── build/ # Build artifacts +``` + +### Initial Setup + +1. Create the base directory: + ```shell + mkdir -p ~/RandNLA + cd ~/RandNLA + ``` + +2. Clone RandLAPACK repository: + ```shell + git clone --recursive https://github.com/BallisticLA/RandLAPACK.git + cd RandLAPACK + ``` + +3. **(Important)** Switch to the correct development branch if needed: + ```shell + git checkout + ``` + + **Note:** Always verify with the development team which branch to use for + the latest GPU support and stability improvements. + +## 2. Running the Install Script + +### Basic Usage + +From inside the `RandLAPACK` directory: + +```shell +bash install.sh +``` + +The script will: +1. Detect if GPU hardware is available on your system and, on a terminal, + ask whether to build with CUDA support +2. Automatically clone and build all dependencies (or reuse preinstalled + ones, see the discovery variables below) +3. Build RandLAPACK with appropriate configuration +4. Build test and benchmark executables + +Run `bash install.sh --help` for the full option list. The main flags, each +with an environment-variable equivalent: + +``` +-y, --yes assume "yes" for every prompt + --gpu / --no-gpu decide GPU support without asking +-j, --jobs parallel build jobs (default: number of cores) + --fresh clear build directories first (default: reuse them, + so re-running is an incremental rebuild) + --modify-rc append RANDNLA_PROJECT_DIR/RANDNLA_PROJECT_GPU_AVAIL + exports to your shell config (default: never touch it; + the summary prints the lines to add yourself) + --project-dir place/locate RandNLA-project at D +``` + +### Automated Installation (Non-Interactive) + +Prompts appear only when stdin is a terminal. Piped and CI runs are already +non-interactive with safe defaults (NVIDIA detected: GPU build; AMD or no +GPU: CPU build), so no `yes |` piping is needed: + +```shell +bash install.sh < /dev/null # or simply: bash install.sh --yes +``` + +### Installation Logging + +All compiler output goes to `/install.log` automatically; the +console shows one line per step, and any failure prints the log path plus +the last lines of the log. There is no need to tee the output yourself. + +## 3. What the Script Does + +The `install.sh` script performs the following steps automatically: + +1. **Creates Project Structure** + - Creates `~/RandNLA/RandNLA-project/` directory tree + - Sets up subdirectories for libraries and build artifacts + +2. **Builds BLAS++** + - Clones BLAS++ from official repository + - Configures with appropriate BLAS backend (MKL if available) + - Builds with GPU support if requested + - Installs to `~/RandNLA/RandNLA-project/lib/blaspp/` + +3. **Builds LAPACK++** + - Clones LAPACK++ from official repository + - Configures to use previously built BLAS++ + - Builds with GPU support if requested + - Installs to `~/RandNLA/RandNLA-project/lib/lapackpp/` + +4. **Installs Random123** + - Clones Random123 header-only library + - Installs headers to `~/RandNLA/RandNLA-project/lib/random123/` + +5. **Moves and Builds RandLAPACK** + - Moves `RandLAPACK` directory to `~/RandNLA/RandNLA-project/lib/` + - Configures CMake with all dependency paths + - Builds RandLAPACK library + - Builds test suite and benchmarks + - Creates executables in `~/RandNLA/RandNLA-project/build/RandLAPACK-build/bin/` + +## 4. Verifying the Installation + +### Running Tests + +After installation completes, verify everything works correctly: + +```shell +cd ~/RandNLA/RandNLA-project/build/RandLAPACK-build +ctest +``` + +This runs the complete test suite (456 tests). Expected output: +``` +99% tests passed, 1 tests failed out of 456 +Total Test time (real) = 124.62 sec +``` + +**Note:** Some test failures are known and acceptable in development branches. +Consult the development team if you see unexpected failures. + +### Running GPU Tests Only + +If you enabled GPU support, test GPU functionality specifically: + +```shell +./bin/RandLAPACK_tests_gpu +``` + +Expected output: 13-14 GPU tests should pass within 15-20 seconds. + +## 5. Working with the Installed Project + +### Key File Locations + +After installation: + +* **RandLAPACK library:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/libRandLAPACK.a` +* **Headers:** `~/RandNLA/RandNLA-project/lib/RandLAPACK/RandLAPACK/` +* **Tests:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/bin/RandLAPACK_tests*` +* **Benchmarks:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/bin/RandLAPACK_bench*` +* **CMake config:** `~/RandNLA/RandNLA-project/build/RandLAPACK-build/RandLAPACKConfig.cmake` + +### Recompiling After Code Changes + +If you modify RandLAPACK source code: + +```shell +cd ~/RandNLA/RandNLA-project/build/RandLAPACK-build +source ~/.bashrc # Ensures environment is loaded +make -j +``` + +**Important:** Always source your `.bashrc` (or equivalent environment setup) +before running `make` to ensure CUDA libraries and other dependencies are in +your `LD_LIBRARY_PATH`. + +### Using RandLAPACK in Your Own Projects + +See Section 4 of `INSTALL.md` for details on linking RandLAPACK to external +CMake projects. You'll need to specify: + +```cmake +-Dblaspp_DIR=~/RandNLA/RandNLA-project/lib/blaspp/lib/cmake/blaspp +-Dlapackpp_DIR=~/RandNLA/RandNLA-project/lib/lapackpp/lib/cmake/lapackpp +-DRandBLAS_DIR=~/RandNLA/RandNLA-project/build/RandLAPACK-build/RandBLAS +-DRandLAPACK_DIR=~/RandNLA/RandNLA-project/build/RandLAPACK-build +``` + +--- + +## Building and Running GPU Benchmarks + +GPU benchmarks are in the `benchmark/` directory and must be built separately from the main RandLAPACK project. + +### Prerequisites + +- RandLAPACK must already be built and installed with CUDA support (`-DRequireCUDA=ON`) +- CUDA Toolkit must be available on your system +- GPU hardware must be available + +### Building GPU Benchmarks + +Navigate to the benchmark directory and build as a standalone project: + +```shell +cd ~/RandNLA/RandNLA-project/lib/RandLAPACK/benchmark +mkdir -p build +cd build +cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER=g++ \ + -DRandLAPACK_DIR=~/RandNLA/RandNLA-project/install/RandLAPACK-install/lib/cmake/RandLAPACK \ + .. +make -j +``` + +**Note:** Adjust the `RandLAPACK_DIR` path to match your installation location. + +### Running GPU Benchmarks + +#### BQRRP GPU Benchmark + +The BQRRP GPU benchmark supports two modes: + +**Block size sweep** (default): +```shell +./BQRRP_GPU_benchmark block_size [matrix_size] [profile_runtime] [run_qrf] +``` + +Examples: +```shell +# Run with default settings (16384x16384 matrix) +./BQRRP_GPU_benchmark block_size + +# Run with 32768x32768 matrix +./BQRRP_GPU_benchmark block_size 32768 + +# Run with profiling enabled and QRF comparison +./BQRRP_GPU_benchmark block_size 16384 1 1 +``` + +**Matrix size sweep**: +```shell +./BQRRP_GPU_benchmark mat_size [profile_runtime] [run_qrf] +``` + +Examples: +```shell +# Run with default settings +./BQRRP_GPU_benchmark mat_size + +# Run with profiling disabled but QRF comparison enabled +./BQRRP_GPU_benchmark mat_size 0 1 +``` + +### Output Files + +The benchmarks generate text files with timing results in the current directory: + +- `_BQRRP_GPU_speed_comparisons_block_size_*.txt` - Speed comparison results for block size sweep +- `BQRRP_GPU_speed_comparisons_mat_size_*.txt` - Speed comparison results for matrix size sweep +- `_BQRRP_GPU_runtime_breakdown_qrf_*.txt` - Detailed profiling with QRF (if profiling enabled) +- `_BQRRP_GPU_runtime_breakdown_cholqr_*.txt` - Detailed profiling with CholQR (if profiling enabled) + +**Last Updated:** 2025-11-26 +**Tested With:** +- GCC 13.3.0 +- CMake 3.31.9 +- CUDA 12.9.0 +- Intel MKL 2025.0.3 +- Ubuntu 22.04 / WSL2 diff --git a/INSTALL_WINDOWS.md b/INSTALL_WINDOWS.md new file mode 100644 index 00000000..3286051b --- /dev/null +++ b/INSTALL_WINDOWS.md @@ -0,0 +1,367 @@ +# Installing RandLAPACK on native Windows + +This guide covers building RandLAPACK with Microsoft's Visual C++ compiler +(MSVC) on Windows -- no WSL, no Cygwin. It assumes no prior Windows +development experience. If you are on Linux or macOS, use +[INSTALL.md](INSTALL.md) and `install.sh` instead. + +## 1. Quick start + +RandLAPACK expects the same things on Windows as on Linux and macOS: a **C++ +compiler, CMake, Ninja and Git**, available in whatever terminal you choose to +work in. The installer does not supply them and will not install them for you; +it checks for them and stops with a fix-it command if any are missing. + +If you already have those, skip to step 3. + +**1. Get a toolchain** (one way; use your own if you prefer). Visual Studio's +C++ workload provides MSVC, the Windows SDK, CMake and Ninja together: + +```powershell +winget install --id Git.Git --exact + +winget install --id Microsoft.VisualStudio.2022.BuildTools --exact ` + --override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" +``` + +Build Tools is the compiler without the IDE; `--id Microsoft.VisualStudio.2022.Community` +with `--add Microsoft.VisualStudio.Workload.NativeDesktop` works equally well. +`--includeRecommended` is what pulls in "C++ CMake tools for Windows", which +supplies CMake and Ninja. `--wait` matters too: without it winget returns while +the Visual Studio installer is still running, which looks like it finished. + +**2. Make the toolchain visible to your terminal.** This is the one real +difference from Unix: MSVC is never on `PATH` globally, only inside a +"developer environment" that Visual Studio sets up per session. Any of these +gets you one, and they are equivalent — pick whichever fits how you work: + +- Open **"x64 Native Tools Command Prompt for VS 2022"** from the Start menu. +- Or, in any Command Prompt, ask Visual Studio where it is and load it (works + for any edition or version): + + ```bat + for /f "usebackq delims=" %i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath`) do call "%i\VC\Auxiliary\Build\vcvars64.bat" + ``` + +- Or use any shell your editor or build tool already sets up, as long as it + gives you an **x64** toolchain (see section 3 — this is worth checking, the + obvious Start-menu entries give you a 32-bit one). + +Whatever you pick, this must show a path containing `Hostx64\x64`: + +``` +where cl +``` + +**3. Build:** + +```bat +git clone --recursive https://github.com/BallisticLA/RandLAPACK.git +cd RandLAPACK +powershell -ExecutionPolicy Bypass -File .\install\install.ps1 +``` + +Two details in that last line, both deliberate. It is a `cmd` prompt, so the +PowerShell script is launched rather than typed directly. And Windows blocks +PowerShell scripts by default (`Restricted` policy on a fresh machine), so +`-ExecutionPolicy Bypass` lets this one script run without permanently +loosening a machine-wide security setting. + +That is the whole install. The script uses an Intel oneMKL you already have, +and otherwise downloads a pinned copy for you, then builds the remaining +dependencies, builds RandLAPACK, and runs its test suite. Everything lands in +a sibling `RandNLA-project\` directory; re-running the script reuses what is +already built. + +You are not expected to install a BLAS library first, unlike on Linux and +macOS. If the installer cannot find one it says what it searched and asks +before downloading a pinned copy into the project directory -- nothing is +installed system-wide. Section 4 covers the choices. + +## 2. How this differs from Linux and macOS + +If you are used to Unix systems, four things explain nearly everything this +installer does differently: + +| | Linux / macOS | Windows | +|---|---|---| +| Getting BLAS/LAPACK | you install one first (`apt install libopenblas-dev`, `brew install openblas`); the installer errors without it | no system location for libraries exists, so the installer discovers an existing oneMKL and otherwise fetches a pinned copy into the project directory. Pass `-NoDownload` for the Linux/macOS behavior | +| Where the compiler lives | `gcc`/`clang` always on PATH | MSVC (`cl.exe`) exists only inside a "Developer" shell that Visual Studio sets up per session | +| Finding shared libraries at run time | the binary itself remembers where its libraries are (RPATH), plus system-wide loader paths | executables have no such memory; Windows searches the executable's **own directory first** and PATH **last**, so the installer copies ("stages") every needed DLL next to each executable | +| Default BLAS backend | OpenBLAS (Linux CI), Accelerate (macOS CI) | Intel oneMKL, ILP64 sequential (fastest on typical Windows x64 machines, and enables RandBLAS's MKL-accelerated sparse routines) | + +Two smaller differences: OpenMP with MSVC needs the `/openmp:llvm` runtime +(the only MSVC mode that accepts RandLAPACK's 64-bit loop indices), which the +build selects automatically; and GPU support is not available on native +Windows. The build tool is Ninja everywhere, which Visual Studio bundles. + +The practical consequence of the third row is worth internalizing: **on +Windows you never need to edit PATH for RandLAPACK**. If an executable is +staged, it runs; if you write your own program against RandLAPACK, either +call the provided staging helper (section 7) or copy the DLLs next to your +`.exe`. This "app-local deployment" is the idiomatic Windows layout -- it is +what Visual Studio's own package manager does by default. + +## 3. Prerequisites, in detail + +You provide these; the installer does not: + +- **A C++ compiler.** MSVC, from Visual Studio 2022 or its Build Tools, with + the **"Desktop development with C++"** workload. +- **CMake and Ninja.** Both ship with that workload's "C++ CMake tools for + Windows" component, so a separate install is usually unnecessary. Your own + copies are fine if they are on `PATH`. +- **Git** (any recent version). +- A network connection for the first run, to fetch the BLAS/LAPACK backend and + build dependencies (see section 5). Roughly 200 MB for the default backend. + +The installer checks all of this up front and prints a fix-it command for +anything missing. + +Two mistakes account for nearly every failed Windows install, and both are +about *which shell you start from*: + +1. **A regular PowerShell.** `cl.exe`, `cmake` and `ninja` are on `PATH` only + inside a Visual Studio developer environment, which is set up per session. +2. **A developer shell of the wrong architecture.** This is easy to hit + because the obvious Start-menu entries are the wrong ones: **"Developer + PowerShell for VS 2022"** and **"Developer Command Prompt for VS 2022"** + both default to a **32-bit (x86)** toolchain. RandLAPACK and every BLAS + backend here are 64-bit, and a 32-bit linker cannot use an x64 import + library. Note that shell bitness is not a usable signal: "Developer Command + Prompt" is itself a 64-bit process and still selects x86 tools. + +Confirm you are in the right place with: + +``` +where cl +``` + +The path it prints must contain `Hostx64\x64`. If it prints +`INFO: Could not find files for the given pattern(s)`, you are in a plain +shell with no compiler; if it contains `Hostx86\x86`, you are in a 32-bit +developer shell. Use `where cl` rather than running `cl` alone: in the failing +cases `cl` either is not found or prints a usage banner, and neither reads as +"wrong shell". The installer's preflight check catches this too, so you cannot +get far down the wrong path. + +**If the Start-menu entry is missing or named differently** (Build Tools, a +different edition, or a Visual Studio version other than 2022), this works +from any Command Prompt regardless of edition or version -- it asks Visual +Studio's own locator where it is installed: + +```bat +for /f "usebackq delims=" %i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath`) do call "%i\VC\Auxiliary\Build\vcvars64.bat" +``` + +The `-products *` matters: without it `vswhere` reports only full Visual +Studio editions and silently finds nothing on a Build Tools install. + +### Script execution policy + +Windows refuses to run PowerShell scripts at all under its default +`Restricted` policy, with "running scripts is disabled on this system". The +quick start sidesteps this per-invocation with `-ExecutionPolicy Bypass`, +which is the smallest hammer: it applies to that one process and changes +nothing about the machine. If you would rather allow local scripts +permanently, this is the conventional setting, and it affects only your own +account: + +```powershell +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +``` + +## 4. Choosing a BLAS/LAPACK backend + +RandLAPACK does all its heavy arithmetic through BLAS++/LAPACK++, which sit +on a BLAS/LAPACK library of your choice. On Windows the installer supports: + +| Backend | Flag | What you get | How it is obtained | +|---|---|---|---| +| **oneMKL** (default) | none needed | fastest option on most x64 CPUs; 64-bit integers (ILP64); MKL-accelerated sparse routines in RandBLAS | discovered from an existing install via `-MklRoot`, `MKLROOT`, `ONEAPI_ROOT`, or the default oneAPI location; otherwise Intel's official NuGet packages are downloaded, pinned by version and SHA256. `-NoDownload` turns "not found" into an error | +| **OpenBLAS** | `-Backend openblas` | solid free backend; 32-bit integers (LP64); RandBLAS's portable sparse fallbacks replace the MKL-only accelerations | official OpenBLAS release binaries, pinned and checksum-verified; the archive is self-contained and includes full LAPACK | +| **Custom / bring-your-own** | `-Backend custom -BlasLibraries ` | anything BLAS++/LAPACK++ can link -- e.g. AMD AOCL, a local ILP64 OpenBLAS build | you provide the import libraries (and their DLL directory via `-BackendBinDir`); the installer verifies them with a small link-and-run check before building anything | + +**ILP64 and LP64** describe the integer width a BLAS library uses for matrix +dimensions -- 64-bit and 32-bit respectively. It matters only if you mix +libraries built for different widths; the installer keeps it consistent for +you, and the practical difference is that ILP64 lets RandBLAS use oneMKL's +accelerated sparse routines. + +### When no oneMKL is found + +The installer says what it searched and asks before downloading anything: + +``` +No oneMKL found, and the download was declined. + + Searched: -MklRoot (not given) + $env:MKLROOT (not set) + $env:ONEAPI_ROOT (not set) + C:\Program Files (x86)\Intel\oneAPI\mkl\latest + + Any of these works: + winget install --id Intel.oneMKL --exact + -MklRoot "" use a copy you already have + -Backend openblas use OpenBLAS instead + re-run and answer yes (or pass -Yes) download a pinned oneMKL, + ~155 MB, into this project only +``` + +Answering no stops with those options. Questions are skipped entirely when the +installer is not attached to a terminal -- a script, a pipeline, CI -- taking +the default shown in brackets; `-Yes` skips them in an interactive session too. + +Notes for the custom path: AMD AOCL downloads sit behind a click-through +license page, so the installer cannot fetch them -- download AOCL yourself, +then point `-BlasLibraries` at its `.lib` files. The custom path is expected +to work with any well-formed backend but is not exercised by our CI; oneMKL +and OpenBLAS are. + +## 5. What the installer downloads, and at which versions + +The installer never installs a compiler, CMake or Ninja -- those are yours to provide, exactly +as on Linux and macOS (see §3). What it *can* fetch is the BLAS/LAPACK backend and RandLAPACK's +own build-time dependencies. Everything lands under ``, is pinned to an exact +version, and is verified by checksum where the source publishes archives: + +| Component | Version | Source | Verified by | +|---|---|---|---| +| Intel oneMKL | **2026.1.0.226** | `intelmkl.devel/redist.win-x64` on nuget.org | SHA256 | +| OpenBLAS | **0.3.34** | official GitHub release binaries | SHA256 | +| GoogleTest | **v1.18.0** | release tag | git tag | +| Random123 | **v1.14.0** | release tag | git tag | +| BLAS++ | commit `3057185` | icl-utk-edu/blaspp | git commit | +| LAPACK++ | commit `40b9d0d` | icl-utk-edu/lapackpp | git commit | + +All are fetched from the project's canonical upstream and pinned to an immutable reference, so +a given RandLAPACK revision always builds the same dependency versions. + +**oneMKL is only downloaded if you do not already have one.** If an existing oneAPI is +discovered (§4), the installer uses *your* version, whatever that is, and downloads nothing. +The version above therefore applies only to the no-oneMKL case. + +**Two deliberate exceptions to "pin a stable release".** BLAS++ and LAPACK++ are pinned to +commits rather than to their latest release, `v2025.05.28`, because that release predates the +two one-line MSVC fixes this build needs (icl-utk-edu/blaspp#132 and +icl-utk-edu/lapackpp#87, both merged upstream on +2026-08-06). They move to a release tag as soon as one includes those fixes. Everything else is +a stable, released version. + +## 6. `install.ps1` reference + +``` +-ProjectDir Where dependencies/builds/installs go + (default: ..\RandNLA-project next to the clone). +-Backend mkl (default) | openblas | custom. +-MklRoot Use this specific oneMKL install (oneAPI layout); + skips discovery. Backend mkl only. Invalid paths are + an error, never a silent fallback to something else. +-NoDownload Fail instead of downloading a backend that was not + found locally. The default fetches one into + ; nothing is installed system-wide, and + deleting removes it. With -Backend + openblas this always fails, because OpenBLAS has no + canonical Windows location to discover -- use + -Backend custom to supply your own. +-NoOpenMP Build serially. The default enables OpenMP through + MSVC's /openmp:llvm runtime, the only mode that + accepts RandLAPACK's 64-bit loop indices; a serial + build is fully functional too. +-Yes Skip interactive questions, taking each documented + default. Questions are already skipped when stdin is + not a terminal, so CI never needs this. +-BlasLibraries Backend custom: semicolon-separated .lib paths. +-LapackLibraries

Backend custom: LAPACK .lib paths, if separate from BLAS. +-BackendBinDir Backend custom: directory holding the backend's DLLs. +-BlasInt lp64|ilp64 Backend custom: the library's integer width (default lp64). +-BlasFortran Backend custom: BLAS++ name-mangling hint (e.g. "add"). +-DependencyRoot

Where the dependency stack lives (default: \install). +-Fresh Reconfigure RandLAPACK from scratch (dependencies are + still reused; delete \install subdirectories + to force dependency rebuilds). +-SkipTests Skip the test suite. +``` + +Worked examples: + +```powershell +# Default: oneMKL, discovered or downloaded. +.\install\install.ps1 + +# OpenBLAS instead of MKL. +.\install\install.ps1 -Backend openblas + +# You already installed the oneAPI toolkit somewhere non-standard. +.\install\install.ps1 -MklRoot "D:\intel\oneAPI\mkl\latest" + +# AMD AOCL, downloaded manually beforehand. +.\install\install.ps1 -Backend custom ` + -BlasLibraries "C:\AOCL\lib\AOCL-LibBlis-Win-MT-dll.lib;C:\AOCL\lib\AOCL-LibFlame-Win-MT-dll.lib" ` + -BackendBinDir "C:\AOCL\bin" +``` + +## 7. Runtime DLLs: what "staging" means + +A Windows executable that links a DLL-based library must be able to find +those DLLs when it starts. Windows looks in the executable's own directory +first and PATH last; there is no RPATH. RandLAPACK's build therefore copies +every DLL an executable needs (BLAS++, LAPACK++, and the BLAS backend's +runtime) into the directory of that executable -- tests and benchmarks work +out of the box, from any shell, with no environment preparation. + +For your own project, the installed CMake package carries the same helper: + +```cmake +find_package(RandLAPACK REQUIRED) +add_executable(myprog main.cc) +target_link_libraries(myprog RandLAPACK) +randlapack_stage_runtime_dlls(myprog) # no-op on non-Windows platforms +``` + +If you prefer not to use the helper, copy the DLLs from the backend's `bin` +directory (the installer prints it at the end) next to your `.exe`. + +## 8. Troubleshooting + +- **"running scripts is disabled on this system"**: Windows' default + PowerShell execution policy. Launch it as the quick start does + (`powershell -ExecutionPolicy Bypass -File .\install\install.ps1`), or see + "Script execution policy" in section 3. +- **"cl.exe is not on PATH"**: you are in a regular shell. Open "x64 Native + Tools Command Prompt for VS 2022" and re-run. If Visual Studio is missing + entirely, the preflight message includes the winget install command. +- **"cl.exe targets x86"**: you are in a developer shell of the wrong + architecture (see section 3). Open "x64 Native Tools Command Prompt for VS + 2022" instead. If an earlier run already got as far as building + dependencies, delete the `RandNLA-project` directory before retrying: + dependencies are reused when present, and the ones configured by the 32-bit + compiler will keep failing no matter which shell you re-run from. +- **"BLAS library not found" from BLAS++, with oneMKL clearly installed**: + almost always the 32-bit shell above, on a version of the installer that + predates the preflight check. The x86 linker rejects the x64 import + library, and BLAS++ can only report that its probe did not link. +- **A download fails with a hash mismatch**: the pinned artifact changed + upstream or the download was corrupted. Re-run once; if it persists, open + an issue -- do not bypass the check. +- **Corporate proxy**: the installer downloads with `curl.exe`, which honors + the standard `HTTPS_PROXY` environment variable. +- **First build is slow**: Windows Defender real-time scanning inspects + every new object file. The build is CPU-bound regardless; subsequent + reruns reuse everything. +- **Wrong results or crashes only inside an activated conda environment**: + conda ships MKL/OpenBLAS DLLs under the *same filenames* as ours and puts + its `Library\bin` on PATH when an environment is active. RandLAPACK's own + staged executables are immune (the exe's folder outranks PATH), but a + program of yours that relies on PATH can silently pick up conda's copies. + Stage your executable (section 7) and the problem disappears. To + deliberately build *against* a conda-provided MKL instead, pass + `-MklRoot "\Library"` (requires conda's `mkl-devel` package). +- **Path-length errors deep in dependency builds**: keep the project + directory short (e.g. `C:\s\RandLAPACK`), or enable Windows long paths. +- **Full reset**: delete `\build` and the relevant + `\install\*` subdirectories, then re-run the installer. + +CI runs the exact flows above on every pull request; `docs/CI.md` describes +the lanes, and `.github\scripts\windows\run-ci.ps1 -Task Core +-SetupDependencies [-Backend openblas]` reproduces a CI lane locally. diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 454a3d5c..760f276d 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -10,7 +10,22 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) include(GNUInstallDirs) message(STATUS "Checking for OpenMP ... ") +# Same MSVC OpenMP-flavor guard as CMake/rl_build_options.cmake: classic +# /openmp (OpenMP 2.0) ignores the collapse clause; /openmp:llvm is the +# mode RandLAPACK's loops need. +if (MSVC AND NOT DEFINED OpenMP_CXX_FLAGS) + set(OpenMP_CXX_FLAGS "/openmp:llvm" CACHE STRING + "OpenMP compiler flags for C++") +endif() +if (MSVC) + set(benchmark_OpenMP_MSVC_FLAGS "${OpenMP_CXX_FLAGS}") +endif() find_package(OpenMP COMPONENTS CXX) +if (MSVC AND OpenMP_CXX_FOUND AND TARGET OpenMP::OpenMP_CXX) + set_property(TARGET OpenMP::OpenMP_CXX PROPERTY + INTERFACE_COMPILE_OPTIONS "${benchmark_OpenMP_MSVC_FLAGS}") + set(OpenMP_CXX_FLAGS "${benchmark_OpenMP_MSVC_FLAGS}") +endif() set(tmp FALSE) if (OpenMP_CXX_FOUND) set(tmp TRUE) @@ -55,6 +70,11 @@ function(add_benchmark) target_compile_options(${TGT_NAME} PRIVATE $<$:-g>) # Include directories come from target_link_libraries, no need to set manually target_link_libraries(${TGT_NAME} ${TGT_LINK_LIBS}) + # On Windows the RandLAPACK package config provides DLL staging so the + # benchmark runs without PATH preparation. + if (COMMAND randlapack_stage_runtime_dlls) + randlapack_stage_runtime_dlls(${TGT_NAME}) + endif() message(STATUS "RandLAPACK: added ${TGT_NAME} benchmark") endfunction() diff --git a/docs/CI.md b/docs/CI.md index cd3bd0ab..8509830a 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -12,7 +12,8 @@ the thing we tell users to type is itself under test). | core-linux | `build-asan` | ubuntu-latest | Debug + AddressSanitizer build and test run | no | | core-macos | `build` | macos-latest | same as Linux `build`, on Apple's toolchain | **yes** (`build`) | | core-macos | `build-asan` | macos-latest | Debug + AddressSanitizer build and test run | no | -| core-windows | `build-windows` | windows-2022 | MSVC + oneMKL (ILP64, sequential) build + tests, serial (no OpenMP) | no (new) | +| core-windows | `windows-toolchain-guards` | windows-2022 | Architecture-guard decision table, plus assertions that `install.ps1` *refuses* a real x86 and a cross-compiled arm64 toolchain, launched exactly as the docs prescribe | no (new) | +| core-windows | `build-windows` | windows-2022 | MSVC build + tests: oneMKL ILP64 serial, oneMKL ILP64 OpenMP (`/openmp:llvm`), OpenBLAS LP64 serial; each leg ends with a stripped-PATH run of a staged test executable | no (new) | | install-script | `install-linux` | ubuntu-latest | `install.sh`: fresh install, idempotent re-run, dependency-discovery path | **yes** | | install-script | `install-macos` | macos-latest | `install.sh`: fresh install, idempotent re-run | **yes** | | install-script | `install-windows` | windows-2022 | `install/install.ps1`: fresh install, idempotent re-run | no (new) | @@ -24,6 +25,18 @@ candidates once they have a green track record. ## Things that are deliberate (do not "fix" without reading this) +- **Workflows fire on `pull_request`, and on `push` only for `main`.** They + used to fire on `push` for every branch as well, so each commit on a pull + request ran every job twice -- same SHA, same result, double the minutes. + A branch with an open pull request is covered by the `pull_request` event; + `main` still gets a run of its own. The one thing this drops is CI for a + branch with no pull request open, which `workflow_dispatch` covers on + demand. +- **`concurrency` supersedes in-flight runs, for pull requests only.** Pushing + a fix used to leave the previous run going to completion. On `main`, + `cancel-in-progress` is deliberately false: every commit there should be + validated, not just the newest. + - **`TestQB.Polynomial_Decay_general1` is QUARANTINED on macOS -- THIS IS TEMPORARY AND MUST BE REVERTED.** The test fails on Apple Silicon because Apple's default (old) Accelerate LAPACK has a broken divide-and-conquer @@ -32,8 +45,8 @@ candidates once they have a green track record. is always red reports nothing. **Revert once Apple's new Accelerate interface is in use**, i.e. when both - of these land (order matters -- lapackpp#88 needs the `defines.h` from - blaspp#134): + of these land (order matters -- icl-utk-edu/lapackpp#88 needs the + `defines.h` from icl-utk-edu/blaspp#134): - -- New Apple Accelerate support, a rebased and completed continuation of the stalled @@ -57,16 +70,94 @@ candidates once they have a green track record. tested by RandBLAS's CI; rebuilding its ~450 tests in every RandLAPACK job roughly doubled job times. The install-script lanes keep them, preserving exactly what a user's install builds. -- **Windows builds are serial (OpenMP off) for now.** RandLAPACK's OpenMP - loops need MSVC's `/openmp:llvm` runtime (64-bit indices, `collapse`), - which RandBLAS's build system does not select yet; see BallisticLA/RandBLAS#184. - When that lands, core-windows grows an OpenMP leg. -- **BLAS++ and LAPACK++ on Windows come from BallisticLA fork branches** - (`BallisticLA/blaspp@remove-symv-debug-print`, - `BallisticLA/lapackpp@msvc-direct-includes`) carrying two one-line MSVC - fixes. Upstream PRs are open (blaspp #132, lapackpp #87); once merged, the - clones in `.github/actions/setup-randlapack-deps-windows/setup.ps1` move - back to upstream master. +- **The Windows matrix is deliberately small** (CI-cost budget): the two + mkl legs cover the default backend serial + OpenMP, and ONE openblas leg + covers non-MKL provisioning and the LP64 build. There is no + openblas-openmp leg (OpenMP x MSVC is backend-orthogonal and covered by + mkl-openmp), no second install-script backend leg (the installer's + `-Backend` forwarding is thin; provisioning is covered by the openblas + core leg), and no Windows ASan lane. +- **The Windows guard job tests the documented *user* path, which the + build matrix structurally cannot.** Every build leg initializes MSVC with + an explicit `arch: x64` under `pwsh`, so none of them exercises the shell + the install docs actually tell users to open. That gap is precisely how a + 32-bit toolchain reached a collaborator in 2026-08 and failed three layers + down as BLAS++ reporting "BLAS library not found" (the libraries were fine; + an x86 linker simply cannot use an x64 import library). `windows-toolchain-guards` closes it + from two directions in **one** job: every check asserts a *refusal* or runs + pure logic, so each takes seconds and runner start-up dominates -- splitting + them across jobs would multiply the Windows runner count for no coverage. + - The decision table (`test-toolchain-guard.ps1`) drives the guard over + every architecture. This is the only way to cover **arm64** and **arm**: + we cannot build for them, so no build leg can ever test them. + - The integration steps (`assert-toolchain-refused.ps1`) run the real + installer under a real x86 toolchain and an `amd64_arm64` cross-compiling + one, which yields an arm64-targeting `cl.exe` on ordinary x64 hardware, + so no ARM runner is needed. They launch via `cmd` + + `-ExecutionPolicy Bypass` under Windows PowerShell 5.1, so the documented + invocation stays under test too, not just the guard. + - A step asserting a *failure* must reset `$LASTEXITCODE` before returning: + `shell: powershell` exits the step with whatever it holds, so the + intentional non-zero code reports a passing assertion as red. This bit us + on the guards' first run. + x86 and arm64 must fail with *different* messages: x86 is a wrong-shell + mistake with a one-command fix, arm64 is an unsupported platform (no + oneMKL build exists for it). The tests pin that distinction. +- **CI passes `-Yes`, and prompts are TTY-gated regardless.** `setup.ps1` can + ask a question (currently: whether you already have OpenBLAS, since unlike + oneMKL it has no canonical Windows location to probe). Any prompt reaching a + runner would block until the job times out, so `$script:Interactive` is false + whenever stdin is redirected or the session is non-interactive, and every + question must have a defensible unattended default. `action.yml` and + `run-ci.ps1` additionally pass `-Yes` so the intent survives any future + change to that detection. This mirrors `install.sh`, which computes + `INTERACTIVE` from `[[ -t 0 ]]` plus `--yes` for the same reason. +- **Backends are auto-provisioned by default; `-NoDownload` opts out.** + Windows has no system prefix for third-party libraries, so per-project + acquisition (what vcpkg, Conan, and NuGet exist for) is the ordinary + practice rather than a workaround, and it is what makes a one-command + install possible on a bare machine. oneMKL is still *discovered* first + (`-MklRoot`, `MKLROOT`, `ONEAPI_ROOT`, default oneAPI path) and the download + is announced rather than silent. `-NoDownload` gives the stricter + Linux/macOS behavior, where `install.sh` expects a system BLAS and errors + without one. Everything downloaded lands under the dependency root, so a + runner's cache and a user's project directory stay self-contained. +- **The architecture check lives in one file, dot-sourced by both callers.** + `.github/scripts/windows/toolchain-arch.ps1` is shared by `install.ps1` and + `setup.ps1`, which run independently (CI calls `setup.ps1` alone, users call + `install.ps1`). It was briefly duplicated, with a test asserting the copies + agreed; sharing the file removes both the duplication and the need for that + test. +- **The architecture check reads several signals, not just `cl.exe`'s + banner.** It prefers `VSCMD_ARG_TGT_ARCH`, then the + `bin\Host\\cl.exe` path convention, and only then the banner. + The banner alone would be wrong on a localized Visual Studio, where the + words around the architecture are translated -- and a missed detection here + fails *open*, silently allowing the exact configuration the check exists to + reject. +- **The MSVC OpenMP flavor is forced to `/openmp:llvm` in RandLAPACK's own + CMake** (`CMake/rl_build_options.cmake` and the benchmark project), not + just in RandBLAS. RandLAPACK's `find_package(OpenMP)` runs before the + submodule's guard, and a 2026-08 CI diagnostic proved classic `-openmp` + was being cached -- under which MSVC silently ignores the `collapse` + clause (warning C4849) that `rl_rpchol` relies on, i.e. the "openmp" leg + was not testing what its name claimed. +- **Windows executables are staged, not PATH-dependent.** The BLAS backend + enters BLAS++ as raw library paths, which `TARGET_RUNTIME_DLLS` cannot + see, so `RANDLAPACK_RUNTIME_DLL_DIRS` stages the backend DLLs beside every + test/benchmark executable (app-local deployment). The stripped-PATH CI + step is the regression gate; do not remove it. An internal process-PATH + prepend remains in run-ci.ps1/install.ps1 only for the RandBLAS + submodule's own test executables, until RandBLAS gains the same staging + (planned follow-up). +- **Every Windows dependency is pinned to an immutable ref**, and comes from + its canonical upstream. BLAS++ and LAPACK++ are pinned to commits rather + than tags only because the latest release of each, `v2025.05.28`, predates + the MSVC fixes this build needs (icl-utk-edu/blaspp#132, + icl-utk-edu/lapackpp#87, merged upstream + 2026-08-06); they move to a tag once one carries them. A branch name would + be a moving target inside a cache keyed on the setup script, so a cache hit + could restore a different revision than a miss builds. ## Caches @@ -78,11 +169,16 @@ slower than a warm one. |------------------|---------|--------------| | `core-deps--v` | core-linux, core-macos (both jobs) | the workspace (`../*-install`) | | `installer-deps--v` | install-linux | `deps-install/` in the workspace | -| `windows-*-v` (five keys) | core-windows, install-windows | `..\windows-deps` | +| `windows-*-r` (per backend + shared) | core-windows, install-windows | `..\windows-deps` | -To force a rebuild against fresh upstream clones, bump the `-v` suffix in -the relevant key. The Windows keys are additionally salted with a hash of -`setup.ps1`, so editing that script invalidates them automatically. +To force a rebuild against fresh upstream clones, bump the `-v`/`-r` suffix +in the relevant key. The Windows keys use MANUAL revision literals only -- +never `hashFiles()`: that helper resolves paths relative to the workspace +root, and the install-script workflow checks the repo out under +`RandLAPACK\`, where the glob matches nothing and `hashFiles()` silently +returns an empty string. The result was two workflows reading and writing +*different* caches while appearing to share one. When a recipe in +`setup.ps1` changes, bump the `-r` on the affected keys in `action.yml`. BLAS++/LAPACK++ track upstream default branches, so a stale cache also means frozen upstream — bump the suffix when upstream matters. @@ -92,6 +188,6 @@ means frozen upstream — bump the suffix when upstream matters. ordinary `cmake` + `make` invocations. - Installer lanes: `bash install.sh --yes --no-gpu` from a fresh clone. - Windows (from an MSVC developer prompt in the repository root): - `.github\scripts\windows\run-ci.ps1 -Task Core -SetupDependencies` - reproduces core-windows; `.\install\install.ps1` reproduces - install-windows. + `.github\scripts\windows\run-ci.ps1 -Task Core -SetupDependencies + [-Backend openblas]` reproduces a core-windows leg; `.\install\install.ps1` + reproduces install-windows. diff --git a/install/install.ps1 b/install/install.ps1 index 0921a894..85aee87b 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -1,34 +1,48 @@ # RandLAPACK native Windows installer -- the companion to install.sh. +# Full guide, including how to get an x64 toolchain: INSTALL_WINDOWS.md # -# Run from an MSVC developer prompt (or "Developer PowerShell for VS") in the -# repository root: +# powershell -ExecutionPolicy Bypass -File .\install\install.ps1 # -# .\install\install.ps1 -# -# What it does, mirroring install.sh's layout in a sibling RandNLA-project -# directory: -# 1. Builds/reuses the dependencies (oneMKL via vcpkg or -MklRoot, -# GoogleTest, Random123, BLAS++, LAPACK++) under \install. -# 2. Configures, builds, installs, and tests RandLAPACK. +# Builds or reuses the dependencies (a BLAS/LAPACK backend, GoogleTest, +# Random123, BLAS++, LAPACK++) under \install, then configures, +# builds, installs and tests RandLAPACK. Mirrors install.sh's layout in a +# sibling RandNLA-project directory. GPU support is not available on native +# Windows yet. # # Options: # -ProjectDir Where dependencies/builds/installs go -# (default: ..\RandNLA-project relative to this script). -# -MklRoot Use an existing oneMKL (oneAPI installer layout) -# instead of fetching MKL through vcpkg. -# -Fresh Reconfigure RandLAPACK from scratch (dependencies are -# always reused when present; delete \install -# subdirectories to force dependency rebuilds). +# (default: ..\RandNLA-project next to the clone). +# -Backend mkl (default) | openblas | custom. See setup.ps1. +# -MklRoot Use this oneMKL install instead of discovery. +# -NoDownload Fail rather than download a backend that was not +# found locally. The default fetches one into +# ; nothing is installed system-wide. +# -Yes Skip interactive questions, taking each default. +# Already skipped when stdin is not a terminal. +# -NoOpenMP Build serially. The default enables OpenMP through +# MSVC's /openmp:llvm runtime. +# -Fresh Reconfigure RandLAPACK from scratch. Dependencies are +# always reused; delete \install to rebuild +# them. # -SkipTests Do not run the test suite after building. -# -# GPU support is not available on native Windows yet. OpenMP is disabled on -# MSVC for now (see .github/scripts/windows/run-ci.ps1 for why); RandLAPACK -# is fully functional serially. +# -BlasLibraries / -LapackLibraries / -BackendBinDir / -BlasInt / -BlasFortran +# Backend custom only; see setup.ps1's header. [CmdletBinding()] param( [string]$ProjectDir = "", + [ValidateSet("mkl", "openblas", "custom")] + [string]$Backend = "mkl", [string]$MklRoot = "", + [switch]$NoDownload, + [switch]$Yes, + [switch]$NoOpenMP, + [string]$BlasLibraries = "", + [string]$LapackLibraries = "", + [string]$BackendBinDir = "", + [ValidateSet("lp64", "ilp64")] + [string]$BlasInt = "lp64", + [string]$BlasFortran = "", # Where the dependency stack lives (default: \install). CI # points this at its shared, cached dependency directory. [string]$DependencyRoot = "", @@ -52,9 +66,52 @@ $sourceRoot = Split-Path $PSScriptRoot -Parent if (-not (Test-Path (Join-Path $sourceRoot "RandLAPACK.hh"))) { throw "install.ps1 must sit in the install\ directory of a RandLAPACK clone." } + +# Architecture detection is shared with setup.ps1 rather than duplicated: the +# two scripts run independently (CI calls setup.ps1 alone, users call this), +# and two copies of a safety check drift. +$archHelper = Join-Path $sourceRoot ".github\scripts\windows\toolchain-arch.ps1" +if (-not (Test-Path $archHelper)) { + throw "Missing $archHelper. This clone looks incomplete." +} +. $archHelper + +# ------------------------------------------------------ preflight checks ---- +# Catch every missing prerequisite up front, each with its fix, instead of +# failing later with a tool-specific error. +$preflightProblems = @() if (-not (Get-Command "cl.exe" -ErrorAction SilentlyContinue)) { - throw "cl.exe is not on PATH. Run from an MSVC developer prompt (Developer PowerShell for VS 2022)." + $preflightProblems += ("cl.exe (the MSVC compiler) is not on PATH. Open 'x64 Native Tools " + + "Command Prompt for VS 2022' from the Start menu and run this script there. If Visual " + + "Studio is not installed:`n winget install Microsoft.VisualStudio.2022.Community " + + '--override "--add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended"') +} else { + # RandLAPACK and every BLAS backend the installer provisions are 64-bit. + # The plain 'Developer PowerShell/Command Prompt for VS 2022' entries + # default to an x86 toolchain, whose linker silently rejects the x64 + # import libraries; the failure then surfaces much later as BLAS++ + # reporting "BLAS library not found", which points at the wrong thing. + $archProblem = Get-ToolchainArchitectureProblem (Get-ClTargetArchitecture) + if ($archProblem -ne "") { $preflightProblems += $archProblem } } +foreach ($tool in @( + @{ Name = "cmake.exe"; Hint = "CMake ships with the Visual Studio C++ workload; an x64 Native Tools Command Prompt puts it on PATH." }, + @{ Name = "ninja.exe"; Hint = "Ninja ships with the Visual Studio C++ workload; an x64 Native Tools Command Prompt puts it on PATH." }, + @{ Name = "git.exe"; Hint = "Install Git:`n winget install Git.Git" }, + @{ Name = "curl.exe"; Hint = "curl.exe ships with Windows 10 (1803+) in System32; check your PATH includes it." })) { + if (-not (Get-Command $tool.Name -ErrorAction SilentlyContinue)) { + $preflightProblems += "$($tool.Name) is not on PATH. $($tool.Hint)" + } +} +if ($preflightProblems.Count -gt 0) { + $preflightProblems | ForEach-Object { Write-Host "PREREQUISITE MISSING: $_`n" } + throw "Missing prerequisites ($($preflightProblems.Count)); see the messages above." +} +if ($ProjectDir -ne "" -and $ProjectDir.Length -gt 150) { + Write-Warning ("-ProjectDir is $($ProjectDir.Length) characters long; deep dependency build " + + "paths may exceed Windows' 260-character limit. Prefer a shorter location.") +} + if (-not (Test-Path (Join-Path $sourceRoot "RandBLAS\CMakeLists.txt"))) { Write-Host "Initializing the RandBLAS submodule..." Invoke-Checked "git" @("-C", $sourceRoot, "submodule", "update", "--init", "--recursive") @@ -79,7 +136,10 @@ Write-Host "" # Step 1: dependencies (idempotent; reused when already present). & (Join-Path $sourceRoot ".github\actions\setup-randlapack-deps-windows\setup.ps1") ` - -DependencyRoot $dependencyRoot -MklRoot $MklRoot + -DependencyRoot $dependencyRoot -Backend $Backend -MklRoot $MklRoot ` + -NoDownload:$NoDownload -Yes:$Yes ` + -BlasLibraries $BlasLibraries -LapackLibraries $LapackLibraries ` + -BackendBinDir $BackendBinDir -BlasInt $BlasInt -BlasFortran $BlasFortran # Step 2: RandLAPACK itself. if ($Fresh -and (Test-Path $buildDir)) { @@ -87,7 +147,8 @@ if ($Fresh -and (Test-Path $buildDir)) { Remove-Item -Recurse -Force $buildDir } -Invoke-Checked "cmake" @( +$stageDllDirs = if ($env:RANDNLA_BLAS_BIN) { $env:RANDNLA_BLAS_BIN.Replace('\', '/') } else { "" } +$configureArgs = @( "-S", $sourceRoot, "-B", $buildDir, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", @@ -96,13 +157,18 @@ Invoke-Checked "cmake" @( "-Dlapackpp_DIR=$env:lapackpp_DIR", "-DRandom123_DIR=$env:Random123_DIR", "-DCMAKE_PREFIX_PATH=$env:googletest_PREFIX", - "-DCMAKE_DISABLE_FIND_PACKAGE_OpenMP=TRUE") + "-DRANDLAPACK_RUNTIME_DLL_DIRS=$stageDllDirs") +# OpenMP is ON by default. RandLAPACK's CMake selects MSVC's /openmp:llvm +# runtime, the only mode that accepts its 64-bit loop indices and collapse +# clauses; core-windows exercises that configuration on every run. -NoOpenMP +# builds serially, which is also fully functional. +if ($NoOpenMP) { $configureArgs += "-DCMAKE_DISABLE_FIND_PACKAGE_OpenMP=TRUE" } +Invoke-Checked "cmake" $configureArgs Invoke-Checked "cmake" @("--build", $buildDir, "--target", "install") if (-not $SkipTests) { Invoke-Checked "ctest" @( "--test-dir", $buildDir, - "--exclude-regex", "^TestABRIK\.ABRIK_catch_instability", "--output-on-failure") } @@ -113,4 +179,10 @@ Write-Host " blaspp_DIR: $env:blaspp_DIR" Write-Host " lapackpp_DIR: $env:lapackpp_DIR" Write-Host " Random123_DIR: $env:Random123_DIR" Write-Host "" -Write-Host "Keep $env:MKL_BIN on PATH when running executables that link MKL." +if ($env:RANDNLA_BLAS_BIN) { + Write-Host "Runtime DLLs from $env:RANDNLA_BLAS_BIN are staged next to RandLAPACK's" + Write-Host "test and benchmark executables automatically -- no PATH changes needed." + Write-Host "For your own executables: find_package(RandLAPACK), then call" + Write-Host "randlapack_stage_runtime_dlls() in your CMakeLists, or copy" + Write-Host "the DLLs from that directory beside your .exe." +}