Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/actions/build-randlapack-stack/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: build-randlapack-stack
description: >
Provide an installed RandLAPACK (with BLAS++, LAPACK++, Random123) for the
bindings to find_package against, by running RandLAPACK's own installer at a
pinned commit. The installer owns the dependency recipe -- immutable-ref
pins, provenance-gated reuse, and a run-not-just-link BLAS verification --
so this action deliberately re-implements none of it. Linux + macOS.

inputs:
randlapack-ref:
description: >
RandLAPACK git ref to build the bindings against. An immutable commit,
not a branch: bump it deliberately, in a PR whose CI run then validates
the bindings against the new commit.
default: ab05872aec1b8695e137fb3c7f29685e5399fc49

runs:
using: composite
steps:
- name: base toolchain + BLAS
shell: bash
run: |
set -euo pipefail
if [ "$RUNNER_OS" = "Linux" ]; then
export DEBIAN_FRONTEND=noninteractive
# Intel MKL (ILP64) is the BLAS on Linux, NOT OpenBLAS: the bindings
# run inside MATLAB, whose runtime BLAS is MKL ILP64 (64-bit ints).
# blaspp must match that ABI or MATLAB's MKL services the MEX's BLAS
# calls with the wrong integer width (garbage leading dimension ->
# "Parameter N incorrect on entry to DGEMM" -> segfault).
# RandLAPACK's installer requests ILP64 for MKL by default and reads
# the built width back, so asking for --blas=mkl below yields the
# right ABI without further flags.
sudo apt-get update -qq
sudo apt-get install -qq -y git-core gcc g++ gfortran cmake m4 wget gpg
wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \
| sudo gpg --dearmor -o /usr/share/keyrings/oneapi-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" \
| sudo tee /etc/apt/sources.list.d/oneAPI.list > /dev/null
sudo apt-get update -qq
sudo apt-get install -qq -y intel-oneapi-mkl-devel
# Source Intel's own env script (robust across versions and the
# 'latest' symlink), then hand the variables to later steps and the
# calling job. oneAPI's vars.sh reads variables that may be unset,
# so nounset is relaxed around the source.
MKL_ENV="$(ls /opt/intel/oneapi/mkl/*/env/vars.sh | sort -V | tail -1)"
set +u; source "$MKL_ENV"; set -u
MKL_LIB="$(dirname "$(find "$MKLROOT" -name 'libmkl_core.so*' | head -1)")"
{
echo "MKLROOT=$MKLROOT"
echo "MKL_LIB=$MKL_LIB"
echo "CPATH=${CPATH:-}"
echo "LIBRARY_PATH=${LIBRARY_PATH:-}"
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}"
} >> "$GITHUB_ENV"
elif [ "$RUNNER_OS" = "macOS" ]; then
# OpenBLAS, NOT Apple Accelerate: legacy Accelerate has a broken
# divide-and-conquer gesdd (RandLAPACK #165) and RandLAPACK's BQRRP
# guard refuses it outright. This is also the macOS default of
# RandLAPACK's installer, for the same reason. macOS two-level
# namespaces keep the MEX bound to its own OpenBLAS at runtime (no
# MATLAB-BLAS interposition), so LP64 is fine here.
brew install openblas libomp
OB="$(brew --prefix openblas)"
{
echo "OPENBLAS_LIB=$OB/lib"
echo "LIBRARY_PATH=$OB/lib"
echo "LD_LIBRARY_PATH=$OB/lib"
echo "DYLD_LIBRARY_PATH=$OB/lib"
echo "CPATH=$OB/include"
} >> "$GITHUB_ENV"
fi

- name: clone RandLAPACK at the pinned ref
shell: bash
run: |
set -euo pipefail
cd "${{ github.workspace }}/.."
git clone https://github.com/BallisticLA/RandLAPACK.git
cd RandLAPACK
git checkout "${{ inputs.randlapack-ref }}"
git submodule update --init --recursive

- name: install the RandLAPACK stack (RandLAPACK's own installer)
shell: bash
run: |
set -euo pipefail
cd "${{ github.workspace }}/.."
# The installer pins blaspp/lapackpp/Random123 itself, stamps
# provenance, and finishes by compiling, linking and RUNNING a
# gemm+gesdd check against the finished install. --blas is stated
# explicitly rather than left to auto, because the choice is
# load-bearing here (see the BLAS step above).
if [ "$RUNNER_OS" = "Linux" ]; then BLAS=mkl; else BLAS=openblas; fi
bash RandLAPACK/install.sh --yes --no-gpu --no-extras --no-benchmarks \
--blas="$BLAS" --project-dir "$PWD/RandNLA-project"

- name: export the install locations
shell: bash
run: |
set -euo pipefail
DEPS="$(cd "${{ github.workspace }}/.." && pwd)/RandNLA-project/install"
# Dependency install directories carry the backend and GPU
# configuration in their names (blaspp-mkl-cpu-install); glob rather
# than hardcode so this action does not break when that naming grows.
BLASPP_DIR="$(echo "$DEPS"/blaspp-*-install)"
LAPACKPP_DIR="$(echo "$DEPS"/lapackpp-*-install)"
{
echo "RandLAPACK_DIR=$DEPS/RandLAPACK-install/lib/cmake/RandLAPACK"
echo "blaspp_DIR=$BLASPP_DIR/lib/cmake/blaspp"
echo "lapackpp_DIR=$LAPACKPP_DIR/lib/cmake/lapackpp"
echo "Random123_DIR=$DEPS/random123/include"
echo "BLASPP_LIB=$BLASPP_DIR/lib"
echo "LAPACKPP_LIB=$LAPACKPP_DIR/lib"
} >> "$GITHUB_ENV"
159 changes: 159 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: ci

# Tests the BINDING layer (the glue), not RandLAPACK's numerics -- RandLAPACK
# has its own CI. The stack is provided by RandLAPACK's own installer at a
# pinned commit (see .github/actions/build-randlapack-stack), then the
# bindings are built and exercised against it.
#
# MATLAB: matlab-actions/setup-matlab runs license-free on PUBLIC GitHub repos
# (this repo is public). Linux + macOS via the job matrix.

on:
pull_request:
workflow_dispatch:
# Only main. A branch with an open pull request is already covered by the
# pull_request event, and firing on both runs every job twice per commit.
push:
branches:
- main

# One run per ref. Superseding is only safe for pull requests: on main every
# commit should be validated, not just the newest.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
matlab:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
# macOS is non-blocking pending issue #6: MATLAB ships its OWN
# libomp.dylib, which wins at load time over the Homebrew libomp that
# blaspp was built against, and does not export the symbols blaspp needs
# (___kmpc_dispatch_deinit) -- so the MEX builds and then fails to
# dlopen. Same class as the Linux ILP64-MKL constraint: the MEX runs
# inside MATLAB's process and MATLAB's bundled runtime wins. The fix is
# a --no-openmp option in RandLAPACK's installer (blaspp already has
# -Duse_openmp=OFF), which is a change in another repo. Linux MATLAB,
# Python and bootstrap all block normally.
continue-on-error: ${{ matrix.os == 'macos-latest' }}
steps:
- uses: actions/checkout@v4

- name: build RandLAPACK stack
uses: ./.github/actions/build-randlapack-stack

- name: set up MATLAB
uses: matlab-actions/setup-matlab@v2

- name: build the MEX bindings
shell: bash
run: |
set -euo pipefail
MATLAB_ROOT="$(dirname "$(dirname "$(command -v matlab)")")"
# macOS: blaspp's installed config does find_dependency(OpenMP),
# which stock Apple Clang cannot satisfy unaided -- every consumer
# of the stack needs the same libomp hints the installer used.
HINTS=()
if [ "$RUNNER_OS" = "macOS" ]; then
LIBOMP="$(brew --prefix libomp)"
HINTS=(
"-DOpenMP_C_LIB_NAMES=omp"
"-DOpenMP_CXX_LIB_NAMES=omp"
"-DOpenMP_omp_LIBRARY=$LIBOMP/lib/libomp.dylib"
"-DOpenMP_C_FLAGS=-Xpreprocessor;-fopenmp;-I$LIBOMP/include"
"-DOpenMP_CXX_FLAGS=-Xpreprocessor;-fopenmp;-I$LIBOMP/include"
)
fi
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DRandLAPACK_DIR="$RandLAPACK_DIR" \
-DMatlab_ROOT_DIR="$MATLAB_ROOT" \
"${HINTS[@]}"
cmake --build build -j2

- name: run MATLAB binding tests
uses: matlab-actions/run-command@v2
with:
command: "addpath('matlab'); addpath('matlab/tests'); test_bqrrp; disp('matlab binding tests passed');"
env:
# The MEX dynamically links blaspp/lapackpp; set BOTH search-path
# vars (Linux uses LD_LIBRARY_PATH, macOS uses DYLD_LIBRARY_PATH).
# The MEX also needs its vendor BLAS: Linux DT_NEEDEDs the ILP64 MKL
# (MKL_LIB), macOS loads OpenBLAS (OPENBLAS_LIB); both are exported
# by the stack action, as are BLASPP_LIB/LAPACKPP_LIB (their install
# directories carry the backend in their names, so they are not
# spelled out here). The unused vendor var is empty, collapsing to a
# leading ':' the loader ignores.
LD_LIBRARY_PATH: "${{ env.MKL_LIB }}:${{ env.BLASPP_LIB }}:${{ env.LAPACKPP_LIB }}"
DYLD_LIBRARY_PATH: "${{ env.OPENBLAS_LIB }}:${{ env.BLASPP_LIB }}:${{ env.LAPACKPP_LIB }}"

python:
# Python bindings are a BQRRP-only skeleton for now; keep CI green while
# it matures (the job still reports build/test status). Becomes required
# when the nanobind migration lands.
continue-on-error: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: build RandLAPACK stack
uses: ./.github/actions/build-randlapack-stack

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: build + test the Python bindings
shell: bash
run: |
set -euo pipefail
export LD_LIBRARY_PATH="$BLASPP_LIB:$LAPACKPP_LIB:${LD_LIBRARY_PATH:-}"
python -m pip install --upgrade pip pytest
python -m pip install ./python -v
pytest python/tests -q

# The user-facing entry point, exercised as documented: a bare clone, an
# existing RandLAPACK install, and one command. The stack action already
# installed RandLAPACK into ../RandNLA-project, so this exercises the
# discovery path (RANDNLA_PROJECT_DIR) end to end.
bootstrap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: build RandLAPACK stack
uses: ./.github/actions/build-randlapack-stack

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: run bootstrap.sh against the existing install
shell: bash
run: |
set -euo pipefail
export RANDNLA_PROJECT_DIR="$(cd "$GITHUB_WORKSPACE/.." && pwd)/RandNLA-project"
export LD_LIBRARY_PATH="$BLASPP_LIB:$LAPACKPP_LIB:${LD_LIBRARY_PATH:-}"
python -m pip install --upgrade pip pytest
# RandLAPACK_DIR is unset for this run on purpose: the stack action
# exports it, and it short-circuits the discovery path this job
# exists to test (the shared RandNLA-project layout probe).
env -u RandLAPACK_DIR bash bootstrap.sh --yes 2>&1 | tee bootstrap.out
grep -q "reused from" bootstrap.out \
|| { echo "bootstrap did not reuse the existing RandLAPACK install"; exit 1; }

# Redirected output must carry no ANSI escapes and no carriage returns,
# same contract as the library installers.
- name: piped output is free of terminal control sequences
shell: bash
run: |
if LC_ALL=C grep -qP '\x1b\[|\r' bootstrap.out; then
echo "Found terminal control sequences in non-TTY output:"
LC_ALL=C grep -nP '\x1b\[|\r' bootstrap.out | head -20
exit 1
fi
echo "OK: no escape sequences in redirected output."
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ __pycache__/
*.egg-info/
.pytest_cache/
*.so

# bootstrap
bootstrap.log
Loading
Loading