Skip to content

Fix CI: restore paravirtual skip for test_01, add numpy to Ty env #8

Fix CI: restore paravirtual skip for test_01, add numpy to Ty env

Fix CI: restore paravirtual skip for test_01, add numpy to Ty env #8

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
name: Build & test (Apple Silicon, Metal)
# macos-14 = M1 runner — actual Apple Silicon hardware, Metal compute works.
# macos-15 also works but currently 10x more expensive on GitHub's billing.
runs-on: macos-14
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# libomp enables OpenMP fallback for CPU benchmarks.
# cmake from Homebrew ensures a recent enough version (3.21+).
- name: Install dependencies
run: brew install cmake libomp
env:
HOMEBREW_NO_AUTO_UPDATE: 1
# Ensure the Metal shader compiler is accessible via xcrun.
# This is a no-op on runners that already have Xcode configured.
- name: Bootstrap Xcode
run: sudo xcodebuild -runFirstLaunch
- name: Configure CMake
run: cmake -B build
- name: Build
run: cmake --build build --parallel
- name: Test (GPU)
run: cmake --build build --target test -- ARGS="--output-on-failure"
python-quality:
name: Python quality (Ruff + Ty)
# Runs on a plain Linux runner — no Metal device needed.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Ruff lint
run: uvx ruff check python/ demo.py wave_demo.py
- name: Ruff format check
run: uvx ruff format --check python/ demo.py wave_demo.py
- name: Ty type check
run: uvx --with numpy ty check python/
python-bindings:
name: Python bindings smoke test (Apple Silicon, Metal)
runs-on: macos-14
needs: build-and-test # skip if the core build fails
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: brew install cmake libomp
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- name: Bootstrap Xcode
run: sudo xcodebuild -runFirstLaunch
- name: Set up Python venv
run: |
python3 -m venv .venv
.venv/bin/pip install --quiet numpy pybind11
- name: Configure & build with pybind11
run: |
PYBIND11_DIR=$(.venv/bin/python3 -c "import pybind11; print(pybind11.get_cmake_dir())")
cmake -B build -Dpybind11_DIR="$PYBIND11_DIR"
cmake --build build --parallel
- name: Smoke test Python bindings
run: |
.venv/bin/python3 - <<'EOF'
import sys
sys.path.insert(0, "build")
import numpy as np
import m1_gpu_ops as metal
ctx = metal.MetalContext()
ctx.load_library("build/02-GeneralArrayOperations/ops.metallib")
ctx.load_library("build/03-2DKernels/ops.metallib")
ctx.load_library("build/04-Compute/ops.metallib")
ctx.load_library("build/05-WavePropagation/ops.metallib")
# 1D ops — verify values, not just shapes
x = np.ones(1024, dtype=np.float32)
y = np.ones(1024, dtype=np.float32) * 2
assert np.allclose(metal.add_arrays(ctx, x, y), 3.0), "add_arrays wrong values"
assert np.allclose(metal.saxpy(ctx, 2.0, x, y), 4.0), "saxpy wrong values"
# 2D Laplacian — shape + numerical correctness (flat field → zero interior)
u = np.random.rand(64, 64).astype(np.float32)
lap = metal.laplacian2d(ctx, u)
assert lap.shape == (64, 64), "laplacian2d shape wrong"
flat = np.ones((64, 64), dtype=np.float32)
lap_flat = metal.laplacian2d(ctx, flat)
assert np.allclose(lap_flat[1:-1, 1:-1], 0.0, atol=1e-5), "laplacian2d nonzero on flat field"
# Mandelbrot — signature: (ctx, width, height, x_min, x_max, y_min, y_max, max_iter)
img = metal.mandelbrot(ctx, 128, 128, -2.0, 1.0, -1.5, 1.5, 256)
assert img.shape == (128, 128), "mandelbrot shape wrong"
# N-body (one step) — pos_mass: (N,4) [x,y,z,mass], velocities: (N,4)
pos_mass = np.random.rand(64, 4).astype(np.float32)
pos_mass[:, 3] = 1.0 # mass column
vel = np.zeros((64, 4), dtype=np.float32)
pos2, vel2 = metal.nbody_step(ctx, pos_mass, vel, dt=0.01, softening=0.1)
assert pos2.shape == (64, 4), f"nbody_step shape wrong: {pos2.shape}"
# Elastic wave (small grid, few steps)
nx, nz = 50, 50
vp = np.full((nx, nz), 3000.0, dtype=np.float32)
vs = np.full((nx, nz), 1800.0, dtype=np.float32)
rho = np.full((nx, nz), 2700.0, dtype=np.float32)
wvl = np.zeros(20, dtype=np.float32); wvl[5] = 1.0
rx = np.array([30, 35, 40], dtype=np.int32)
rz = np.array([25, 25, 25], dtype=np.int32)
sv, sz, _, _ = metal.elastic_wave_propagate(
ctx, vp, vs, rho, 25, 10, wvl, rx, rz,
dx=10.0, dz=10.0, dt=1e-3, n_boundary=5
)
assert sv.shape == (3, 20), f"seismogram shape wrong: {sv.shape}"
print("All smoke tests passed.")
EOF