diff --git a/.github/scripts/cpu-separation.ps1 b/.github/scripts/cpu-separation.ps1 new file mode 100644 index 0000000..3d1ac48 --- /dev/null +++ b/.github/scripts/cpu-separation.ps1 @@ -0,0 +1,39 @@ +# Separate a short generated mix on a machine without a GPU: the server starts, +# runs on the CPU and returns stems. +# +# cpu-separation.ps1 -Server stemd-server.exe -Cli stemd-cli.exe +param( + [Parameter(Mandatory)] [string]$Server, + [Parameter(Mandatory)] [string]$Cli +) +$ErrorActionPreference = 'Stop' + +$work = Join-Path $env:RUNNER_TEMP 'cpu-separation' +New-Item -ItemType Directory -Force -Path $work | Out-Null +python (Join-Path $PSScriptRoot 'test-mix.py') "$work\mix.wav" +if ($LASTEXITCODE -ne 0) { throw 'test-mix.py failed' } + +$out = "$work\server.out" +$err = "$work\server.err" +$proc = Start-Process $Server -PassThru -NoNewWindow ` + -ArgumentList '--headless', '--no-mdns', '--bind', '127.0.0.1:7979', '--cache-dir', "$work\cache" ` + -RedirectStandardOutput $out -RedirectStandardError $err +function Get-Log { (Get-Content $out, $err -ErrorAction SilentlyContinue) -join "`n" } +function Stop-WithLog([string]$why) { + Get-Log | Write-Host + if (-not $proc.HasExited) { Stop-Process $proc -Force } + throw $why +} + +$deadline = (Get-Date).AddMinutes(10) +while ((Get-Log) -notmatch 'listening') { + if ($proc.HasExited) { Stop-WithLog "the server exited with 0x$('{0:X8}' -f $proc.ExitCode)" } + if ((Get-Date) -gt $deadline) { Stop-WithLog 'the server is not listening after 10 minutes' } + Start-Sleep -Seconds 1 +} +if ((Get-Log) -notmatch 'via mlx on cpu') { Stop-WithLog 'the server is not on the CPU' } + +& $Cli --host 127.0.0.1:7979 --out "$work\stems" "$work\mix.wav" +if ($LASTEXITCODE -ne 0) { Stop-WithLog 'the separation failed' } +Stop-Process $proc -Force +Get-ChildItem "$work\stems" diff --git a/.github/scripts/cpu-separation.sh b/.github/scripts/cpu-separation.sh new file mode 100755 index 0000000..6b50582 --- /dev/null +++ b/.github/scripts/cpu-separation.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Separate a short generated mix on a machine without a GPU: the server starts, +# runs on the CPU and returns stems. +# +# cpu-separation.sh SERVER CLI +set -euo pipefail +server="$1" +cli="$2" +work="$(mktemp -d)" +python3 "$(dirname "$0")/test-mix.py" "$work/mix.wav" + +"$server" --headless --no-mdns --bind 127.0.0.1:7979 --cache-dir "$work/cache" >"$work/server.log" 2>&1 & +pid=$! +trap 'kill "$pid" 2>/dev/null || true' EXIT +fail() { + cat "$work/server.log" + echo "ERROR: $1" >&2 + exit 1 +} +for _ in $(seq 1 600); do + grep -q listening "$work/server.log" && break + kill -0 "$pid" 2>/dev/null || fail "the server exited" + sleep 1 +done +grep -q listening "$work/server.log" || fail "the server is not listening after 10 minutes" +grep -q "via mlx on cpu" "$work/server.log" || fail "the server is not on the CPU" +"$cli" --host 127.0.0.1:7979 --out "$work/stems" "$work/mix.wav" || fail "the separation failed" +ls -l "$work/stems" diff --git a/.github/scripts/test-mix.py b/.github/scripts/test-mix.py new file mode 100644 index 0000000..a74604c --- /dev/null +++ b/.github/scripts/test-mix.py @@ -0,0 +1,30 @@ +"""Write a 10 s stereo test mix at 44.1 kHz: a chord, a kick and some noise. + +Usage: test-mix.py OUT.wav +""" + +import math +import random +import struct +import sys +import wave + +RATE = 44100 +SECONDS = 10 + +random.seed(1) +frames = bytearray() +for i in range(RATE * SECONDS): + t = i / RATE + chord = 0.2 * sum(math.sin(2 * math.pi * f * t) for f in (220.0, 277.2, 329.6)) / 3 + beat = t % 0.5 + kick = 0.5 * math.sin(2 * math.pi * 60 * beat) * math.exp(-20 * beat) + left = chord + kick + 0.02 * random.uniform(-1, 1) + right = chord + kick + 0.02 * random.uniform(-1, 1) + frames += struct.pack("/dev/null) LD_LIBRARY_PATH="$PWD/dist/stub" "$RUNNER_TEMP/squashfs-root/AppRun" --version + # The runner has no GPU, and the stub libcuda finds no device. + - name: CPU separation + timeout-minutes: 30 + run: | + root="$RUNNER_TEMP/squashfs-root" + LD_LIBRARY_PATH="$PWD/dist/stub" .github/scripts/cpu-separation.sh "$root/AppRun" "$root/opt/stemd/bin/stemd-cli" - uses: actions/upload-artifact@v4 with: name: linux-${{ matrix.arch }} @@ -268,6 +274,12 @@ jobs: $installed = Join-Path $env:LOCALAPPDATA 'Programs\stemd' Test-Version (Join-Path $installed 'stemd-server.exe') Test-Version (Join-Path $installed 'stemd-cli.exe') + # The runner has no NVIDIA driver. + - name: CPU separation + timeout-minutes: 30 + run: | + $installed = Join-Path $env:LOCALAPPDATA 'Programs\stemd' + .github/scripts/cpu-separation.ps1 -Server (Join-Path $installed 'stemd-server.exe') -Cli (Join-Path $installed 'stemd-cli.exe') - uses: actions/upload-artifact@v4 with: name: windows-x86_64 diff --git a/crates/stemd-server/src/startup.rs b/crates/stemd-server/src/startup.rs index 4cca9fb..799dcf0 100644 --- a/crates/stemd-server/src/startup.rs +++ b/crates/stemd-server/src/startup.rs @@ -50,11 +50,17 @@ pub fn prepare(args: &Args, logs: LogBuffer, addr: SocketAddr) -> Result // loaded at CPU speed. A card whose runtime is missing looks exactly like // no card in every later line, and only one of the two has a fix. if stemd_core::Accelerator::gpu_refused() { - tracing::warn!( - "a GPU is installed but its runtime is not usable from here, so this \ - runs on the CPU. Run with --install-cuda to fetch it (about 1.2 GB, \ - once), or put the libraries beside the executable yourself." - ); + if cfg!(windows) { + tracing::warn!( + "a GPU is installed but its runtime is not usable from here, so this \ + runs on the CPU. Run with --install-cuda to fetch it (about 1.2 GB, \ + once), or put the libraries beside the executable yourself." + ); + } else { + tracing::warn!( + "a GPU is installed but failed a test computation, so this runs on the CPU." + ); + } } let settings = Arc::new(open_settings(args)?); let (stems, cache_summary) = open_stem_cache(args)?; diff --git a/docs/building.md b/docs/building.md index 2686216..f206258 100644 --- a/docs/building.md +++ b/docs/building.md @@ -190,6 +190,10 @@ prebuilt copy instead. On Windows the prebuilt also carries OpenBLAS (`libopenblas.lib` and `.dll`) and `dl.lib`. +`mlx-sys` patches MLX's CUDA allocator so a CUDA build also runs on the CPU of a +machine without a CUDA device. CD checks this with a CPU separation on the Linux +and Windows runners, which have no GPU. + The manifest records the `mlx-sys` version, the MLX tag, the target, the C++ compiler (only `msvc` on Windows, whose toolsets link older ones), nvcc, cuDNN, the GPU architectures and the RMSNorm patch, and `mlx-sys` refuses a diff --git a/packaging/mlx-prebuilt.env b/packaging/mlx-prebuilt.env index baeaff0..c143c78 100644 --- a/packaging/mlx-prebuilt.env +++ b/packaging/mlx-prebuilt.env @@ -5,10 +5,10 @@ # Changing an architecture list, the toolchain or the MLX pin means a new tag # and a new build. 90a rather than 90: MLX builds its Hopper-only kernels for # 90a alone. The SHA-256 lines are filled in from that workflow's summary. -MLX_PREBUILT_TAG=mlx-cuda-2 +MLX_PREBUILT_TAG=mlx-cuda-3 CUDA_ARCHS_linux_x86_64="80;86;89;90a;120" CUDA_ARCHS_linux_aarch64="90a;100;121" CUDA_ARCHS_windows_x86_64="80;86;89;90a;120" -SHA256_linux_x86_64=2837bfdeecfa441c0c7d3bf9bd4974a3b04269e79bfbebecdb53806278d2722d -SHA256_linux_aarch64=703eacbef4b6d54532417dcb455a743a2199b901c67b3630c3deeeb249b1068e -SHA256_windows_x86_64=51cb5388c5abf5c128717a294d12fdf8b0530ac4cec4d19bbbce233642fae57f +SHA256_linux_x86_64=0970ff131deb42cf9b18c9600f6de7c8a47513fe9db37eca0f4a88edf0208eb8 +SHA256_linux_aarch64=ead8f1a706cb1b0944f87289040bfbc6b9da1d8014bf5c5326d880d702fdf375 +SHA256_windows_x86_64=d7664ce18c250ae74efeed3b6a77405884e452fa608ae271d36827bd29161f76 diff --git a/vendor/mlx-rs-stemd b/vendor/mlx-rs-stemd index 824be78..e735cad 160000 --- a/vendor/mlx-rs-stemd +++ b/vendor/mlx-rs-stemd @@ -1 +1 @@ -Subproject commit 824be781f7a7d15c5d0a15e3dbda40177847df02 +Subproject commit e735cad30080cca1b91833c3d227ef9dd3dbea2c