diff --git a/.github/workflows/install-script.yaml b/.github/workflows/install-script.yaml index e69d7c2e..9347fde8 100644 --- a/.github/workflows/install-script.yaml +++ b/.github/workflows/install-script.yaml @@ -56,26 +56,47 @@ jobs: path: deps-install key: installer-deps-${{ runner.os }}-v1 + # Globbed rather than spelled out: dependency install directories carry + # the backend and GPU configuration in their names (blaspp-openblas-cpu-install), + # so hardcoding them here would couple CI to that naming and break the + # moment it changes. - name: seed discovery variables from cache if: steps.deps-cache.outputs.cache-hit == 'true' run: | - echo "BLASPP_INSTALL_DIR=$GITHUB_WORKSPACE/deps-install/blaspp-install" >> "$GITHUB_ENV" - echo "LAPACKPP_INSTALL_DIR=$GITHUB_WORKSPACE/deps-install/lapackpp-install" >> "$GITHUB_ENV" + set -euo pipefail + echo "BLASPP_INSTALL_DIR=$(echo "$GITHUB_WORKSPACE"/deps-install/blaspp-*-install)" >> "$GITHUB_ENV" + echo "LAPACKPP_INSTALL_DIR=$(echo "$GITHUB_WORKSPACE"/deps-install/lapackpp-*-install)" >> "$GITHUB_ENV" echo "RANDOM123_INSTALL_DIR=$GITHUB_WORKSPACE/deps-install/random123" >> "$GITHUB_ENV" - name: keep a pristine clone for the discovery test run: cp -a RandLAPACK RandLAPACK-discovery - name: run the installer (non-interactive) - run: bash RandLAPACK/install.sh --yes --no-gpu + run: | + set -euo pipefail + bash RandLAPACK/install.sh --yes --no-gpu 2>&1 | tee installer.out + + # Redirected output must carry no ANSI escapes and no carriage returns. + # The installer draws a progress bar on a terminal; without this check, + # that bar could start filling install.log and every CI transcript with + # control characters and nobody would notice until a log was unreadable. + - name: piped output is free of terminal control sequences + run: | + if LC_ALL=C grep -qP '\x1b\[|\r' installer.out; then + echo "Found terminal control sequences in non-TTY output:" + LC_ALL=C grep -nP '\x1b\[|\r' installer.out | head -20 + exit 1 + fi + echo "OK: no escape sequences in redirected output." - name: populate the dependency cache if: steps.deps-cache.outputs.cache-hit != 'true' run: | + set -euo pipefail mkdir -p deps-install - cp -a RandNLA-project/install/blaspp-install deps-install/ - cp -a RandNLA-project/install/lapackpp-install deps-install/ - cp -a RandNLA-project/install/random123 deps-install/ + cp -a RandNLA-project/install/blaspp-*-install deps-install/ + cp -a RandNLA-project/install/lapackpp-*-install deps-install/ + cp -a RandNLA-project/install/random123 deps-install/ - name: test the installed library run: | @@ -87,13 +108,14 @@ jobs: - name: install a second project via dependency discovery run: | - BLASPP_INSTALL_DIR="$GITHUB_WORKSPACE/deps-install/blaspp-install" \ - LAPACKPP_INSTALL_DIR="$GITHUB_WORKSPACE/deps-install/lapackpp-install" \ + set -euo pipefail + BLASPP_INSTALL_DIR="$(echo "$GITHUB_WORKSPACE"/deps-install/blaspp-*-install)" \ + LAPACKPP_INSTALL_DIR="$(echo "$GITHUB_WORKSPACE"/deps-install/lapackpp-*-install)" \ RANDOM123_INSTALL_DIR="$GITHUB_WORKSPACE/deps-install/random123" \ bash RandLAPACK-discovery/install.sh --yes --no-gpu \ --project-dir "$GITHUB_WORKSPACE/RandNLA-project-discovery" \ | tee discovery.out - grep -q "reused external install" discovery.out + grep -q "external install" discovery.out test -d RandNLA-project-discovery/build/RandLAPACK-build install-macos: diff --git a/CMakeLists.txt b/CMakeLists.txt index d6dccafe..97858a63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,7 +73,7 @@ option(RandLAPACK_EXTERNAL_RandBLAS # The submodule pin, recorded as a variable so configure-time checks also work # in tarball builds (no .git directory). Update alongside every submodule # bump; with a git checkout present, the cross-check below enforces that. -set(RandLAPACK_RandBLAS_PIN "04f2018afdb29a9478ae70cb1a52b36a9156146f") +set(RandLAPACK_RandBLAS_PIN "952251cf9dd386452ea9a88e553c9966513e85d1") find_package(Git QUIET) if (Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") diff --git a/INSTALL_SCRIPT.md b/INSTALL_SCRIPT.md index 0d348333..0e5b84f3 100644 --- a/INSTALL_SCRIPT.md +++ b/INSTALL_SCRIPT.md @@ -90,16 +90,27 @@ The install script expects a specific directory structure: ``` ~/RandNLA/ -├── RandLAPACK/ # Clone RandLAPACK here (script will move it) -└── RandNLA-project/ # Created automatically by script +├── RandLAPACK/ # Your clone. The script does NOT move it. +└── RandNLA-project/ # Created automatically ├── lib/ - │ ├── blaspp/ # Built by script - │ ├── lapackpp/ # Built by script - │ ├── random123/ # Built by script - │ └── RandLAPACK/ # Moved here by script - └── build/ # Build artifacts + │ ├── blaspp/ # Source, fetched at a pinned commit + │ ├── lapackpp/ # Source, fetched at a pinned commit + │ └── RandLAPACK -> ../../RandLAPACK # Symlink to your clone + ├── install/ + │ ├── blaspp---install + │ ├── lapackpp---install + │ ├── random123/ + │ └── RandLAPACK-install + └── build/ # One build directory per project above ``` +Two things worth noting. **Your clone stays where you put it** — earlier +versions of this script relocated it into `lib/`, which broke git worktrees; +`lib/RandLAPACK` is now a symlink. And the dependency install directories carry +the backend and GPU configuration in their names, so an ILP64 MKL build and an +LP64 OpenBLAS build, or a CUDA and a CPU build, cannot be mistaken for each +other or silently reused for one another. + ### Initial Setup 1. Create the base directory: @@ -144,17 +155,58 @@ 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 +--blas=BACKEND auto | openblas | mkl | accelerate | custom + (default: auto -- OpenBLAS on macOS, MKL on Linux when + MKLROOT is set, otherwise OpenBLAS) +--blas-int=WIDTH ilp64 | lp64 (default: ilp64 where the backend can + provide it; see section 6) +--blas-libraries=L link line for --blas=custom, used for BLAS and LAPACK --gpu / --no-gpu decide GPU support without asking --j, --jobs parallel build jobs (default: number of cores) +--project-dir=DIR place/locate RandNLA-project at DIR (default: + $RANDNLA_PROJECT_DIR if set, else ../RandNLA-project) +--prefix=DIR install RandLAPACK itself here instead of + /install/RandLAPACK-install +-j, --jobs N parallel build jobs (default: number of cores) --fresh clear build directories first (default: reuse them, so re-running is an incremental rebuild) + --no-extras skip the extras project + --no-benchmarks skip the benchmark project + --no-openmp configure without OpenMP +-y, --yes assume "yes" for every prompt --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 + --no-progress plain one-line-per-step output, no redrawing ``` +Extras and benchmarks are built **by default**; `--no-extras` and +`--no-benchmarks` opt out. They need nothing the script has not already built, +so leaving them on costs only time. (RandBLAS's installer makes its `examples/` +opt-in instead, because those pull in dependencies RandBLAS itself does not.) + +### Sharing one dependency tree with RandBLAS + +Both installers use the same `RandNLA-project` layout and both honour +`RANDNLA_PROJECT_DIR`, so setting it once keeps everything in one place: + +```shell +export RANDNLA_PROJECT_DIR=$HOME/RandNLA-project +``` + +That shares the *location*, not the artifacts. Each project builds its own +BLAS++ into a separately named directory — `blaspp-mkl-cpu-install` here versus +`blaspp-mkl-install` for RandBLAS — deliberately, so neither can overwrite the +other's dependency while its provenance stamp still describes the original. + +To genuinely reuse one BLAS++ across both, name it: + +```shell +BLASPP_INSTALL_DIR=$RANDNLA_PROJECT_DIR/install/blaspp-mkl-install bash install.sh +``` + +The install then verifies that choice by compiling, linking and running against +it rather than trusting it. + ### Automated Installation (Non-Interactive) Prompts appear only when stdin is a terminal. Piped and CI runs are already @@ -175,32 +227,39 @@ the last lines of the log. There is no need to tee the output yourself. 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/` +1. **Creates the project structure** shown in section 1, and initialises the + RandBLAS submodule. RandBLAS stays a pinned submodule and is not installed + separately; its own installer exists for people who want RandBLAS alone. + +2. **Checks the toolchain** — compiler, CMake 3.21+, Git — reporting everything + missing at once rather than failing on the first item. + +3. **Builds BLAS++** at a pinned commit, with the selected backend and integer + width, then **reads back the width it actually built** (section 6). + +4. **Builds LAPACK++** at a pinned commit against that BLAS++. + +5. **Installs Random123** (header-only) at a pinned tag. + +6. **Builds and installs RandLAPACK**, then **verifies the result by running + it**: a small program is compiled, linked and executed against the finished + install, checking a BLAS++ `gemm` and a LAPACK++ `gesdd` numerically. + + This step is not ceremony. A configuration that merely *configures* can + still be broken in ways nothing catches by inspection — most importantly, if + BLAS++'s headers were built for one integer width while the library actually + loaded uses another, the guards inside BLAS++ compile out and the symptom is + an absurd workspace size and a run that never finishes rather than an error. + Only executing something finds that. + +7. **Builds the extras and benchmark projects**, unless `--no-extras` or + `--no-benchmarks`. + +Every dependency is fetched at an immutable ref and stamped with its +provenance, so it is reused only when it came from the same source *in the same +configuration* — backend, integer width and GPU setting all count. Switching +`--no-gpu` to `--gpu` therefore rebuilds BLAS++ rather than silently reusing a +CPU-only one. ## 4. Verifying the Installation @@ -272,6 +331,95 @@ CMake projects. You'll need to specify: --- +## 6. Integer width, backends, and tested configurations + +### 6.1 What we test + +Every row below corresponds to a CI lane, so this is a statement about what is +exercised on each commit rather than what ought to work. Anything absent may +well work; it is simply untested. + +| OS | Compiler | BLAS backend | Integer width | GPU | OpenMP | +|---|---|---|---|---|---| +| Ubuntu (latest) | gcc | OpenBLAS | LP64 | none | yes | +| Ubuntu (latest) | gcc | oneMKL | ILP64 | none | yes | +| Ubuntu (latest) | clang | OpenBLAS | LP64 | none | yes | +| macOS 14/15 | Apple Clang | Accelerate | LP64 | none | no (see below) | +| Windows | MSVC | oneMKL | ILP64 | none | yes (`/openmp:llvm`) | + +The installer lanes additionally cover a fresh install, an idempotent re-run, +and dependency discovery through `BLASPP_INSTALL_DIR` and friends. + +Compiler floor: **gcc >= 13**, because RandBLAS (vendored as a submodule) uses +C++20 concepts. CMake 3.21 or later on every platform. Apple Clang ships no +OpenMP runtime, so a macOS build is single-threaded unless you install +Homebrew's `libomp`, which the installer will use when present. + +CUDA builds are not covered by CI — there is no GPU runner — so `--gpu` is +exercised locally only. CUDA 12.9 with gcc 13.3 is the reference combination. + +### 6.2 Which integer width you get, and why + +A BLAS comes in one of two flavours: **LP64** uses 32-bit integers for matrix +dimensions, **ILP64** uses 64-bit. The installer prefers ILP64 wherever the +backend can genuinely provide it: + +| `--blas=` | Width you get | Why | +|---|---|---| +| `mkl` | ILP64 | `mkl_intel_ilp64` is a separate library, so requesting it actually selects it | +| `openblas` | LP64, with a warning | there is only `-lopenblas`, so the request selects nothing | +| `accelerate` | LP64 | BLAS++ implements only Apple's legacy interface (upstream `icl-utk-edu/lapackpp#43`) | +| `custom` | whatever you pass | you named the library, so its width is yours to state | + +The OpenBLAS row deserves explaining, because it is counter-intuitive. BLAS++ +probes `int32` before `int64`, and `blas_int` only filters which *library names* +to consider. With one candidate name, a plain LP64 OpenBLAS passes the `int32` +probe and is accepted — so a successful `blas_int=int64` configure proves +nothing. The installer therefore reads the width back out of BLAS++'s generated +`blas/defines.h` after building, and reports what was actually produced. + +ILP64 OpenBLAS **does** exist (`libopenblas64`, from Debian/Ubuntu's +`libopenblas64-dev` or Fedora's `openblas64`); BLAS++ just never looks for it. +Until that is fixed upstream, name it explicitly: + +```shell +bash install.sh --blas=custom --blas-int=ilp64 \ + --blas-libraries=/usr/lib/x86_64-linux-gnu/libopenblas64.so +``` + +### 6.3 Does LP64 limit RandLAPACK? + +Rarely, and it fails loudly rather than silently. RandLAPACK is `int64_t` +throughout, and BLAS++/LAPACK++ **throw** rather than truncate when a value +exceeds the BLAS's range (`to_blas_int`, `to_lapack_int`), naming the offending +argument. The guard is on individual dimensions and leading dimensions, not +element counts — the BLAS never receives `m*n` — so a 100,000 x 100,000 matrix +is fine under LP64. The limit bites only past roughly 2.1 billion in a *single* +dimension, and for sparse work with `nnz > 2^31`. + +The case that *is* silent is different, and it is why this installer runs a +program rather than only linking one: if BLAS++'s headers were built for one +width while the library actually loaded uses the other, the guard compiles out +entirely and 64-bit values reach routines reading 32 bits. The symptom is not +wrong numbers but nonsense control values — a misread workspace query becomes an +absurd `lwork`, and the run dies in allocation or never finishes. The +verification step exists to catch that at install time. + +### 6.4 macOS: why OpenBLAS rather than Accelerate + +The macOS default is Homebrew OpenBLAS, and that is a correctness decision, not +an oversight. Apple's legacy Accelerate has a broken divide-and-conquer `gesdd`, +and RandLAPACK calls `gesdd` in `rl_rsvd.hh`, `rl_abrik.hh`, `rl_revd2.hh`, +`rl_preconditioners.hh` and `rl_util.hh` — so on Accelerate most of the +SVD-based drivers can return wrong results. This is why `core-macos` quarantines +`TestQB.Polynomial_Decay_general1` (issue #159). + +`--blas=accelerate` is allowed and warns. The default will move to Accelerate +once BLAS++ adopts Apple's new interface, which carries both the `gesdd` fix and +ILP64. + +--- + ## Building and Running GPU Benchmarks GPU benchmarks are in the `benchmark/` directory and must be built separately from the main RandLAPACK project. diff --git a/RandBLAS b/RandBLAS index 04f2018a..952251cf 160000 --- a/RandBLAS +++ b/RandBLAS @@ -1 +1 @@ -Subproject commit 04f2018afdb29a9478ae70cb1a52b36a9156146f +Subproject commit 952251cf9dd386452ea9a88e553c9966513e85d1 diff --git a/install/install.ps1 b/install/install.ps1 index 85aee87b..5a677b04 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -10,8 +10,13 @@ # Windows yet. # # Options: -# -ProjectDir Where dependencies/builds/installs go -# (default: ..\RandNLA-project next to the clone). +# -ProjectDir Where dependencies/builds/installs go. Defaults to +# $env:RANDNLA_PROJECT_DIR when set, otherwise +# ..\RandNLA-project next to the clone. +# -Prefix Install RandLAPACK itself here instead of +# \install\RandLAPACK-install. +# -ModifyEnvironment Persist RANDNLA_PROJECT_DIR for your user account. The +# default touches nothing and prints the setx command. # -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 @@ -46,6 +51,13 @@ param( # Where the dependency stack lives (default: \install). CI # points this at its shared, cached dependency directory. [string]$DependencyRoot = "", + # Install RandLAPACK itself here instead of \install\RandLAPACK-install. + # Dependencies still go in the project directory. For an HPC module tree or + # any other prefix a site wants to own. + [string]$Prefix = "", + # Persist RANDNLA_PROJECT_DIR for this user. Opt-in, mirroring install.sh's + # --modify-rc: the default touches nothing and prints the setx line instead. + [switch]$ModifyEnvironment, [switch]$Fresh, [switch]$SkipTests ) @@ -107,26 +119,41 @@ 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") } +# Precedence matches install.sh exactly: the flag, then RANDNLA_PROJECT_DIR, +# then a sibling of this clone. Honouring the environment variable is what lets +# this installer and RandBLAS's use the same project directory, so a machine +# that has already installed one does not scatter a second tree elsewhere. if ($ProjectDir -eq "") { - $ProjectDir = Join-Path (Split-Path $sourceRoot -Parent) "RandNLA-project" + if ($env:RANDNLA_PROJECT_DIR) { + $ProjectDir = $env:RANDNLA_PROJECT_DIR + } else { + $ProjectDir = Join-Path (Split-Path $sourceRoot -Parent) "RandNLA-project" + } } $ProjectDir = [System.IO.Path]::GetFullPath($ProjectDir) if ($DependencyRoot -eq "") { $DependencyRoot = Join-Path $ProjectDir "install" } +# Checked after resolution rather than before: previously this only fired for an +# explicitly-passed -ProjectDir, so a long *default* path -- the common case, +# since it is derived from wherever the clone happens to sit -- went unwarned. +if ($ProjectDir.Length -gt 150) { + Write-Warning ("The project directory path is $($ProjectDir.Length) characters long; deep " + + "dependency build paths may exceed Windows' 260-character limit. Prefer a shorter " + + "location, such as C:\RandNLA, via -ProjectDir.") +} $dependencyRoot = [System.IO.Path]::GetFullPath($DependencyRoot) $buildDir = Join-Path $ProjectDir "build\RandLAPACK-build" -$installDir = Join-Path $ProjectDir "install\RandLAPACK-install" +$installDir = if ($Prefix) { + [System.IO.Path]::GetFullPath($Prefix) +} else { + Join-Path $ProjectDir "install\RandLAPACK-install" +} Write-Host "" Write-Host "RandLAPACK Windows install" @@ -172,6 +199,17 @@ if (-not $SkipTests) { "--output-on-failure") } +# Opt-in, mirroring install.sh's --modify-rc. SetEnvironmentVariable at User +# scope is the Windows equivalent of appending to a shell profile, and the only +# mechanism that survives opening a new shell -- setting $env: alone would last +# only for this process. +if ($ModifyEnvironment) { + [Environment]::SetEnvironmentVariable("RANDNLA_PROJECT_DIR", $ProjectDir, "User") + Write-Host "" + Write-Host "Set RANDNLA_PROJECT_DIR=$ProjectDir for your user account." + Write-Host "Open a new shell to pick it up." +} + Write-Host "" Write-Host "RandLAPACK is installed." Write-Host " RandLAPACK_DIR: $installDir\lib\cmake\RandLAPACK" @@ -179,6 +217,12 @@ Write-Host " blaspp_DIR: $env:blaspp_DIR" Write-Host " lapackpp_DIR: $env:lapackpp_DIR" Write-Host " Random123_DIR: $env:Random123_DIR" Write-Host "" +if (-not $ModifyEnvironment) { + Write-Host "To have RandNLA installers reuse this project directory by default, set:" + Write-Host " setx RANDNLA_PROJECT_DIR `"$ProjectDir`"" + Write-Host "(or re-run with -ModifyEnvironment)" + Write-Host "" +} 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." diff --git a/install/install.sh b/install/install.sh index de471235..e5dd988e 100755 --- a/install/install.sh +++ b/install/install.sh @@ -1,77 +1,176 @@ #!/bin/bash -# RandLAPACK autoinstaller. +# RandLAPACK autoinstaller for Linux and macOS. # -# Installs RandLAPACK with all of its dependencies and builds the extras and -# benchmark projects. The directory that contains the RandLAPACK clone ends up -# with a top-level "RandNLA-project" directory: +# Builds RandLAPACK, its dependencies, the extras and the benchmark projects +# into a self-contained "RandNLA-project" directory laid out as: # lib: RandLAPACK, blaspp, lapackpp sources -# install: RandLAPACK-install, blaspp-install, lapackpp-install, random123 +# install: RandLAPACK-install, blaspp-*-install, lapackpp-*-install, random123 # build: one build directory per project above # -# Usage: bash install.sh [options] +# Nothing is installed system-wide, and your shell configuration is untouched +# unless you pass --modify-rc. # -# -y, --yes Assume "yes" for every prompt (also the behavior when -# stdin is not a terminal, e.g. curl | bash or CI). -# --gpu Build with CUDA support without asking. -# --no-gpu Build without GPU support without asking. -# -j, --jobs Parallel build jobs (default: number of cores). -# --fresh Clear all build directories first. The default reuses -# them, so re-running after a failure or a source -# update is an incremental rebuild. -# --modify-rc Append RANDNLA_PROJECT_DIR / RANDNLA_PROJECT_GPU_AVAIL -# exports to your shell config. The default never -# touches your shell config; the final summary prints -# the export lines to add yourself if you want them. -# --project-dir Place/locate RandNLA-project at D instead of next to -# this clone. -# -h, --help Show this help and exit. +# You bring a C++20 compiler, CMake 3.21+, Git and a BLAS/LAPACK. This script +# does not install compilers or package managers; when something is missing it +# says so and tells you the usual way to get it. # -# Every option has an environment-variable equivalent (flags win): -# RANDLAPACK_INSTALL_YES=1, RANDLAPACK_INSTALL_GPU=on|off, -# RANDLAPACK_INSTALL_JOBS=N, RANDLAPACK_INSTALL_FRESH=1, -# RANDLAPACK_INSTALL_MODIFY_RC=1, RANDLAPACK_INSTALL_PROJECT_DIR=D +# RandBLAS is intentionally not covered here: RandLAPACK vendors it as a git +# submodule pinned to an exact commit, and that pinned copy stays authoritative. +# RandBLAS's own installer is for people who want RandBLAS on its own. # -# Already-installed dependencies are discovered through: -# BLASPP_INSTALL_DIR, LAPACKPP_INSTALL_DIR, RANDOM123_INSTALL_DIR -# (RandBLAS is intentionally not covered: it stays a git submodule.) -# -# All compiler output goes to /install.log; the console shows one -# line per step. On failure the log path is printed. -# -# Prerequisites are listed in INSTALL.md. +# Prerequisites and tested configurations are listed in INSTALL_SCRIPT.md. + set -euo pipefail +usage() { + # A heredoc rather than a line-range sed over this file's own comment block: + # the latter starts printing unrelated code the moment anyone adds a line + # above it. + cat <<'USAGE' +Usage: bash install.sh [options] + +Backend selection: + --blas=BACKEND auto | openblas | mkl | accelerate | custom + (default: auto -- OpenBLAS on macOS, MKL on Linux when + MKLROOT is set, otherwise OpenBLAS) + --blas-int=WIDTH ilp64 | lp64. Defaults to ilp64 wherever the backend + can actually provide it, falling back to lp64 with a + warning. Accelerate is lp64-only and rejects ilp64. + --blas-libraries=L Link line for --blas=custom, used for both BLAS and + LAPACK, e.g. "/opt/aocl/lib/libflame.so;/opt/aocl/lib/libblis.so" + +GPU: + --gpu Build with CUDA support without asking + --no-gpu Build without GPU support without asking + +Locations: + --project-dir=DIR Where dependencies, builds and installs go. + Default: $RANDNLA_PROJECT_DIR if set, otherwise + ../RandNLA-project next to this clone. + --prefix=DIR Install RandLAPACK itself here instead of + /install/RandLAPACK-install. Dependencies + still go in the project directory. + +Build: + -j, --jobs N Parallel build jobs (default: number of cores) + --fresh Clear build directories and rebuild dependencies + --no-extras Skip the extras project + --no-benchmarks Skip the benchmark project + --no-openmp Configure without OpenMP + +Output: + -y, --yes Assume "yes" at every prompt. Also the behavior when + stdin is not a terminal (CI, pipes). + --modify-rc Append RANDNLA_PROJECT_DIR / RANDNLA_PROJECT_GPU_AVAIL + exports to your shell config. The default touches + nothing and prints the lines to add yourself. + --no-progress Plain one-line-per-step output, no redrawing + -h, --help Show this help and exit + +Every option has an environment-variable equivalent (flags win): + RANDLAPACK_INSTALL_BLAS, RANDLAPACK_INSTALL_BLAS_INT, + RANDLAPACK_INSTALL_BLAS_LIBRARIES, RANDLAPACK_INSTALL_GPU, + RANDLAPACK_INSTALL_PROJECT_DIR, RANDLAPACK_INSTALL_PREFIX, + RANDLAPACK_INSTALL_JOBS, RANDLAPACK_INSTALL_FRESH, + RANDLAPACK_INSTALL_EXTRAS, RANDLAPACK_INSTALL_BENCHMARKS, + RANDLAPACK_INSTALL_OPENMP, RANDLAPACK_INSTALL_YES, + RANDLAPACK_INSTALL_MODIFY_RC, RANDLAPACK_INSTALL_PROGRESS + +Already-installed dependencies are reused when pointed at by: + BLASPP_INSTALL_DIR, LAPACKPP_INSTALL_DIR, RANDOM123_INSTALL_DIR + +All compiler output goes to /install.log; the console shows one +line per step. On failure the log path is printed. +USAGE +} + #============================================================================== # Option parsing. Environment variables provide defaults; flags override. #============================================================================== -ASSUME_YES="${RANDLAPACK_INSTALL_YES:-0}" -GPU_CHOICE="${RANDLAPACK_INSTALL_GPU:-ask}" # ask | on | off +BLAS_BACKEND="${RANDLAPACK_INSTALL_BLAS:-auto}" +BLAS_INT_CHOICE="${RANDLAPACK_INSTALL_BLAS_INT:-auto}" # auto | ilp64 | lp64 +BLAS_LIBRARIES_ARG="${RANDLAPACK_INSTALL_BLAS_LIBRARIES:-}" +GPU_CHOICE="${RANDLAPACK_INSTALL_GPU:-ask}" # ask | on | off +PROJECT_DIR_OVERRIDE="${RANDLAPACK_INSTALL_PROJECT_DIR:-}" +PREFIX_OVERRIDE="${RANDLAPACK_INSTALL_PREFIX:-}" JOBS="${RANDLAPACK_INSTALL_JOBS:-}" FRESH="${RANDLAPACK_INSTALL_FRESH:-0}" +WANT_EXTRAS="${RANDLAPACK_INSTALL_EXTRAS:-1}" +WANT_BENCHMARKS="${RANDLAPACK_INSTALL_BENCHMARKS:-1}" +WANT_OPENMP="${RANDLAPACK_INSTALL_OPENMP:-1}" +ASSUME_YES="${RANDLAPACK_INSTALL_YES:-0}" MODIFY_RC="${RANDLAPACK_INSTALL_MODIFY_RC:-0}" -PROJECT_DIR_OVERRIDE="${RANDLAPACK_INSTALL_PROJECT_DIR:-}" +WANT_PROGRESS="${RANDLAPACK_INSTALL_PROGRESS:-1}" -usage() { sed -n '2,45p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; } +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } while [[ $# -gt 0 ]]; do case "$1" in - -y|--yes) ASSUME_YES=1 ;; - --gpu) GPU_CHOICE="on" ;; - --no-gpu) GPU_CHOICE="off" ;; - -j|--jobs) JOBS="${2:?--jobs requires a number}"; shift ;; - --jobs=*) JOBS="${1#*=}" ;; - --fresh) FRESH=1 ;; - --modify-rc) MODIFY_RC=1 ;; - --project-dir) PROJECT_DIR_OVERRIDE="${2:?--project-dir requires a path}"; shift ;; - --project-dir=*) PROJECT_DIR_OVERRIDE="${1#*=}" ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1 (see --help)" >&2; exit 2 ;; + --blas) BLAS_BACKEND="${2:?--blas requires a backend}"; shift ;; + --blas=*) BLAS_BACKEND="${1#*=}" ;; + --blas-int) BLAS_INT_CHOICE="${2:?--blas-int requires a width}"; shift ;; + --blas-int=*) BLAS_INT_CHOICE="${1#*=}" ;; + --blas-libraries) BLAS_LIBRARIES_ARG="${2:?--blas-libraries requires a value}"; shift ;; + --blas-libraries=*) BLAS_LIBRARIES_ARG="${1#*=}" ;; + --gpu) GPU_CHOICE="on" ;; + --no-gpu) GPU_CHOICE="off" ;; + --project-dir) PROJECT_DIR_OVERRIDE="${2:?--project-dir requires a path}"; shift ;; + --project-dir=*) PROJECT_DIR_OVERRIDE="${1#*=}" ;; + --prefix) PREFIX_OVERRIDE="${2:?--prefix requires a path}"; shift ;; + --prefix=*) PREFIX_OVERRIDE="${1#*=}" ;; + -j|--jobs) JOBS="${2:?--jobs requires a number}"; shift ;; + --jobs=*) JOBS="${1#*=}" ;; + -j*) JOBS="${1#-j}" ;; # attached form, as in -j8 + --fresh) FRESH=1 ;; + --no-extras) WANT_EXTRAS=0 ;; + --no-benchmarks) WANT_BENCHMARKS=0 ;; + --no-openmp) WANT_OPENMP=0 ;; + -y|--yes) ASSUME_YES=1 ;; + --modify-rc) MODIFY_RC=1 ;; + --no-progress) WANT_PROGRESS=0 ;; + -h|--help) usage; exit 0 ;; + *) printf 'Unknown option: %s (see --help)\n' "$1" >&2; exit 2 ;; esac shift done -# Prompts happen only on a terminal and only without --yes. When stdin is not -# a terminal (piped/CI), every prompt silently takes its default. +case "$BLAS_BACKEND" in + auto|openblas|mkl|accelerate|custom) ;; + *) die "--blas must be auto, openblas, mkl, accelerate or custom (got '$BLAS_BACKEND')" ;; +esac +case "$BLAS_INT_CHOICE" in + auto|ilp64|lp64) ;; + *) die "--blas-int must be ilp64 or lp64 (got '$BLAS_INT_CHOICE')" ;; +esac +case "$GPU_CHOICE" in + ask|on|off) ;; + *) die "RANDLAPACK_INSTALL_GPU must be 'on' or 'off' (got '$GPU_CHOICE')" ;; +esac +if [[ "$BLAS_BACKEND" == "custom" && -z "$BLAS_LIBRARIES_ARG" ]]; then + die "--blas=custom needs --blas-libraries=" +fi +if [[ -n "$BLAS_LIBRARIES_ARG" && "$BLAS_BACKEND" != "custom" ]]; then + die "--blas-libraries only applies to --blas=custom (backend is '$BLAS_BACKEND')" +fi +# Checked here rather than during backend resolution, which happens after GPU +# detection: a contradiction between two flags should be reported before the +# user is asked anything. "auto" never resolves to accelerate, so testing the +# literal value is sufficient. +if [[ "$BLAS_BACKEND" == "accelerate" && "$BLAS_INT_CHOICE" == "ilp64" ]]; then + die "--blas-int=ilp64 is not available with Accelerate: BLAS++ implements only Apple's legacy LP64 interface (upstream icl-utk-edu/lapackpp#43). Use --blas=openblas or --blas=mkl for ILP64." +fi + +if [[ -z "$JOBS" ]]; then + JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 8) +fi + +#============================================================================== +# Interactivity and output style. +# +# Prompts happen only on a terminal and only without --yes. When stdin is not a +# terminal (piped, CI) every prompt silently takes its default, so this script +# can never hang waiting for input nobody is there to give. +#============================================================================== INTERACTIVE=0 if [[ -t 0 && "$ASSUME_YES" != "1" ]]; then INTERACTIVE=1 @@ -89,35 +188,91 @@ ask() { [[ "$reply" == "y" || "$reply" == "Y" || "$reply" == "yes" ]] } -if [[ -z "$JOBS" ]]; then - JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 8) +if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" && "$WANT_PROGRESS" == "1" ]]; then + C_OK=$'\033[32m'; C_ERR=$'\033[31m'; C_WARN=$'\033[33m'; C_BOLD=$'\033[1m'; C_OFF=$'\033[0m' +else + C_OK=""; C_ERR=""; C_WARN=""; C_BOLD=""; C_OFF="" fi -# Plain output when not on a terminal or when NO_COLOR/TERM=dumb ask for it. -if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then - C_OK=$'\033[32m'; C_ERR=$'\033[31m'; C_BOLD=$'\033[1m'; C_OFF=$'\033[0m' +# Progress rendering tier. +# 2 a terminal that can draw: redraw a bar in place, with block characters +# 1 a terminal without colour or UTF-8: same bar, ASCII, still redrawn +# 0 not a terminal: one line per step, no escapes, no carriage returns +# +# Tier 0 is a requirement, not a fallback. Redirected output ends up in +# install.log, in CI transcripts and in bug reports, and control characters make +# all three unreadable. +PROGRESS_TIER=0 +if [[ -t 1 && "$WANT_PROGRESS" == "1" && "${TERM:-}" != "dumb" ]]; then + if [[ -z "${NO_COLOR:-}" && "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" == *[Uu][Tt][Ff]* ]]; then + PROGRESS_TIER=2 + else + PROGRESS_TIER=1 + fi +fi +if (( PROGRESS_TIER >= 2 )); then + BAR_FULL="━"; BAR_EMPTY="─" else - C_OK=""; C_ERR=""; C_BOLD=""; C_OFF="" + BAR_FULL="#"; BAR_EMPTY="-" fi +note() { printf '%s\n' "$*"; } +warn() { printf '%swarning:%s %s\n' "$C_WARN" "$C_OFF" "$*" >&2; } + +# Collected and reprinted in the final summary. A warning emitted twenty minutes +# and several thousand log lines before the summary is a warning nobody reads. +WARNINGS=() +record_warning() { WARNINGS+=("$1"); warn "$1"; } + #============================================================================== -# Toolchain checks. Warn always; abort only if the user says so at a prompt. +# Toolchain preflight. Report everything missing at once rather than failing on +# the first one, so a bare machine takes one round trip instead of three. #============================================================================== +UNAME_S="$(uname -s)" +MISSING=() +command -v cmake >/dev/null 2>&1 || MISSING+=("cmake") +command -v git >/dev/null 2>&1 || MISSING+=("git") +if ! command -v c++ >/dev/null 2>&1 && ! command -v g++ >/dev/null 2>&1 && \ + ! command -v clang++ >/dev/null 2>&1; then + MISSING+=("a C++ compiler") +fi +if (( ${#MISSING[@]} )); then + printf 'ERROR: missing prerequisites: %s\n\n' "${MISSING[*]}" >&2 + if [[ "$UNAME_S" == "Darwin" ]]; then + printf ' xcode-select --install # Apple Clang and git\n' >&2 + printf ' brew install cmake\n\n' >&2 + else + printf ' sudo apt install g++ gfortran cmake git # Debian, Ubuntu\n' >&2 + printf ' sudo dnf install gcc-c++ gcc-gfortran cmake git # Fedora, RHEL\n\n' >&2 + fi + printf 'See INSTALL_SCRIPT.md for the full prerequisite list.\n' >&2 + exit 1 +fi + +CMAKE_VERSION="$(cmake --version | head -n1 | awk '{print $3}')" +if [[ "$(printf '%s\n3.21\n' "$CMAKE_VERSION" | sort -V | head -n1)" != "3.21" ]]; then + die "CMake 3.21 or later is required (found $CMAKE_VERSION). See INSTALL_SCRIPT.md." +fi + +# GCC 13.3.0 is the reference version. Warn rather than block: newer usually +# works, and the C++20 concepts RandBLAS uses need at least 13. PREFERRED_GCC_VERSION="13.3.0" CURRENT_GCC_VERSION=$(gcc --version 2>/dev/null | head -n 1 | awk '{print $NF}') -if [[ "$CURRENT_GCC_VERSION" != "$PREFERRED_GCC_VERSION" ]]; then - echo "Note: GCC $PREFERRED_GCC_VERSION is the reference version; found ${CURRENT_GCC_VERSION:-none}." - if ! ask "Continue with the current GCC?" y; then - echo "Stopping at your request. Install GCC $PREFERRED_GCC_VERSION and re-run." - exit 1 +if [[ -n "$CURRENT_GCC_VERSION" && "$CURRENT_GCC_VERSION" != "$PREFERRED_GCC_VERSION" ]]; then + GCC_MAJOR="${CURRENT_GCC_VERSION%%.*}" + if [[ "$GCC_MAJOR" =~ ^[0-9]+$ ]] && (( GCC_MAJOR < 13 )); then + record_warning "gcc $CURRENT_GCC_VERSION is older than 13; RandBLAS uses C++20 concepts and may not compile." + else + note "Note: gcc $PREFERRED_GCC_VERSION is the reference version; found $CURRENT_GCC_VERSION." fi fi #============================================================================== # GPU decision. --gpu/--no-gpu (or RANDLAPACK_INSTALL_GPU) decide outright; -# otherwise detection + prompt. Non-interactive defaults: NVIDIA detected -> -# GPU on; AMD or nothing detected -> GPU off (the CUDA-only build cannot -# succeed on AMD, so saying yes for the user would guarantee a failure). +# otherwise detection plus a prompt. Non-interactive defaults: NVIDIA detected +# means GPU on; AMD or nothing detected means GPU off, because the CUDA-only +# build cannot succeed on AMD and saying yes for the user would guarantee a +# failure. #============================================================================== RANDLAPACK_CUDA="OFF" RANDNLA_PROJECT_GPU_AVAIL="none" @@ -130,302 +285,772 @@ case "$GPU_CHOICE" in RANDLAPACK_CUDA="ON"; RANDNLA_PROJECT_GPU_AVAIL="auto" fi elif { command -v lspci &>/dev/null && lspci | grep -i "VGA" | grep -qi "AMD"; } || \ - { [[ "$(uname)" == "Darwin" ]] && system_profiler SPDisplaysDataType 2>/dev/null | grep -qi "AMD"; }; then + { [[ "$UNAME_S" == "Darwin" ]] && system_profiler SPDisplaysDataType 2>/dev/null | grep -qi "AMD"; }; then if ask "AMD GPU detected, but only a CUDA build is available for now. Attempt a CUDA build anyway?" n; then RANDLAPACK_CUDA="ON"; RANDNLA_PROJECT_GPU_AVAIL="auto" fi else - echo "No GPU detected; building without GPU support." + note "No GPU detected; building without GPU support." fi ;; - *) echo "RANDLAPACK_INSTALL_GPU must be 'on' or 'off' (got '$GPU_CHOICE')" >&2; exit 2 ;; esac if [[ "$RANDNLA_PROJECT_GPU_AVAIL" == "auto" ]]; then PREFERRED_NVCC_VERSION="12.9" CURRENT_NVCC_VERSION=$(nvcc --version 2>/dev/null | grep "release" | awk '{print $5}' | cut -d',' -f1) if [[ "$CURRENT_NVCC_VERSION" != "$PREFERRED_NVCC_VERSION" ]]; then - echo "Note: NVCC $PREFERRED_NVCC_VERSION is the reference version; found ${CURRENT_NVCC_VERSION:-none}." + note "Note: NVCC $PREFERRED_NVCC_VERSION is the reference version; found ${CURRENT_NVCC_VERSION:-none}." if ! ask "Continue with the current NVCC?" y; then - echo "Stopping at your request. Install NVCC $PREFERRED_NVCC_VERSION and re-run." - exit 1 + die "Stopping at your request. Install NVCC $PREFERRED_NVCC_VERSION and re-run." fi fi fi #============================================================================== -# macOS preflight: Homebrew OpenBLAS + libomp, SDK C++ headers, OpenMP hints. +# Project layout. +# +# Precedence: --project-dir, then RANDNLA_PROJECT_DIR, then a sibling of this +# clone. Honouring the environment variable is what lets this installer and +# RandBLAS's share one dependency tree -- whichever runs second finds the +# first one's BLAS++ and reuses it. +# +# This script no longer moves your clone. The previous version relocated the +# repository into /lib/RandLAPACK on first run, which breaks git +# worktrees and surprises anyone who cloned deliberately. The layout below is +# created regardless of where the clone lives, and lib/RandLAPACK is a symlink +# so the tree still reads as complete. #============================================================================== -BLAS_INT="int64" -MACOS_BLAS_FLAGS="" -MACOS_LAPACK_FLAGS="" -MACOS_OPENMP_FLAGS="" -if [[ "$(uname)" == "Darwin" ]]; then - if [[ ! -f /opt/homebrew/opt/openblas/lib/libopenblas.dylib ]]; then - echo "ERROR: OpenBLAS not found. Install it first: brew install openblas" >&2 - exit 1 - fi - if [[ ! -f /opt/homebrew/opt/libomp/lib/libomp.dylib ]]; then - echo "ERROR: libomp not found. Install it first: brew install libomp" >&2 - exit 1 - fi - BLAS_INT="int32" - MACOS_SDK_PATH=$(xcrun --show-sdk-path) - # SDK C++ headers + Apple Clang OpenMP flags (no native OpenMP; Homebrew - # libomp). Appending to CXXFLAGS/CFLAGS lets cmake pick them up via - # CMAKE__FLAGS_INIT for all try_compile tests, including FindOpenMP. - export CXXFLAGS="-isystem ${MACOS_SDK_PATH}/usr/include/c++/v1 -Xpreprocessor -fopenmp -I/opt/homebrew/opt/libomp/include" - export CFLAGS="-Xpreprocessor -fopenmp -I/opt/homebrew/opt/libomp/include" - export LDFLAGS="-L/opt/homebrew/opt/libomp/lib" - MACOS_BLAS_FLAGS="-DBLAS_LIBRARIES=/opt/homebrew/opt/openblas/lib/libopenblas.dylib -Dblas_fortran=add" - MACOS_LAPACK_FLAGS="-DLAPACK_LIBRARIES=/opt/homebrew/opt/openblas/lib/libopenblas.dylib" - MACOS_OPENMP_FLAGS="-DOpenMP_C_LIB_NAMES=omp -DOpenMP_CXX_LIB_NAMES=omp -DOpenMP_omp_LIBRARY=/opt/homebrew/opt/libomp/lib/libomp.dylib -DOpenMP_C_FLAGS=-Xpreprocessor;-fopenmp -DOpenMP_CXX_FLAGS=-Xpreprocessor;-fopenmp" -fi - -#============================================================================== -# Project layout. The clone moves itself into /RandNLA-project/lib/ -# on first run; on re-runs (script already under lib/) the layout is detected. -#============================================================================== -# This script lives in /install/; REPO_DIR is the RandLAPACK clone. -SCRIPT_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")") -REPO_DIR=$(dirname "$SCRIPT_DIR") -PARENT_DIR=$(dirname "$REPO_DIR") -PARENT_BASE=$(basename "$PARENT_DIR") +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" + if [[ -n "$PROJECT_DIR_OVERRIDE" ]]; then RANDNLA_PROJECT_DIR="$PROJECT_DIR_OVERRIDE" -elif [[ "$PARENT_BASE" == "lib" ]]; then - RANDNLA_PROJECT_DIR=$(dirname "$PARENT_DIR") +elif [[ -n "${RANDNLA_PROJECT_DIR:-}" ]]; then + : # already set in the environment; use it as-is else - RANDNLA_PROJECT_DIR="$PARENT_DIR/RandNLA-project" + RANDNLA_PROJECT_DIR="$(dirname "$REPO_DIR")/RandNLA-project" fi - +mkdir -p "$RANDNLA_PROJECT_DIR" +RANDNLA_PROJECT_DIR="$(cd "$RANDNLA_PROJECT_DIR" && pwd)" mkdir -p "$RANDNLA_PROJECT_DIR"/{install,lib,build} -for d in blaspp-build lapackpp-build RandLAPACK-build extras-build benchmark-build; do - if [[ "$FRESH" == "1" ]]; then - rm -rf "$RANDNLA_PROJECT_DIR/build/$d" - fi - mkdir -p "$RANDNLA_PROJECT_DIR/build/$d" -done + +# A symlink, not a move: the clone stays where the user put it. +if [[ ! -e "$RANDNLA_PROJECT_DIR/lib/RandLAPACK" ]]; then + ln -s "$REPO_DIR" "$RANDNLA_PROJECT_DIR/lib/RandLAPACK" +fi +RL_SRC="$REPO_DIR" + +RANDLAPACK_INSTALL_DIR="${PREFIX_OVERRIDE:-$RANDNLA_PROJECT_DIR/install/RandLAPACK-install}" LOG="$RANDNLA_PROJECT_DIR/install.log" -: > "$LOG" -echo "RandLAPACK install started $(date)" >> "$LOG" +# Appended, not truncated: the previous run's output is exactly what you want +# when the current run fails the same way. +{ + printf '\n===============================================================\n' + printf 'RandLAPACK install started %s\n' "$(date)" + printf '===============================================================\n' +} >> "$LOG" -# run_step