diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c65e7..6fa5317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,71 +25,103 @@ jobs: - name: Build managed projects run: dotnet build --configuration Release --no-restore - native-linux: + native-assets: needs: dotnet-build runs-on: ubuntu-latest + env: + NATIVE_RELEASE_REPO: ${{ vars.MLXSHARP_NATIVE_REPO || 'ManagedCode/MLXSharp' }} + NATIVE_RELEASE_TAG: ${{ vars.MLXSHARP_NATIVE_TAG || '' }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install build dependencies + - name: Install tooling run: | sudo apt-get update - sudo apt-get install -y build-essential cmake libopenblas-dev liblapack-dev liblapacke-dev + sudo apt-get install -y jq unzip - - name: Configure native build - run: cmake -S native -B native/build/linux -DCMAKE_BUILD_TYPE=Release + - name: Download official native binaries + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail - - name: Build native library - run: cmake --build native/build/linux --target mlxsharp --config Release + repo="${NATIVE_RELEASE_REPO}" + if [ -z "$repo" ]; then + echo "::error::NATIVE_RELEASE_REPO must be provided" >&2 + exit 1 + fi - - name: Package native artifact - run: | - mkdir -p artifacts/native/linux-x64 - cp native/build/linux/libmlxsharp.so artifacts/native/linux-x64/ + if [ -n "${NATIVE_RELEASE_TAG}" ]; then + release_api="https://api.github.com/repos/${repo}/releases/tags/${NATIVE_RELEASE_TAG}" + else + release_api="https://api.github.com/repos/${repo}/releases/latest" + fi - - name: Upload native artifact - uses: actions/upload-artifact@v4 - with: - name: native-linux-x64 - path: artifacts/native/linux-x64/libmlxsharp.so + echo "Fetching release metadata from ${release_api}" + response=$(curl -fsSL -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${GITHUB_TOKEN}" "$release_api") + tag=$(echo "$response" | jq -r '.tag_name // ""') - native-macos: - needs: dotnet-build - runs-on: macos-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive + if [ -z "$tag" ]; then + echo "::error::Unable to resolve release tag from ${release_api}" >&2 + exit 1 + fi - - name: Install CMake - run: brew install cmake + echo "Using native release ${repo}@${tag}" - - name: Configure native build - run: cmake -S native -B native/build/macos -DCMAKE_BUILD_TYPE=Release + nupkg_asset=$(echo "$response" | jq -r '.assets[] | select(.name | startswith("ManagedCode.MLXSharp.")) | select(.name | endswith(".nupkg")) | .name' | head -n1) + if [ -z "$nupkg_asset" ] || [ "$nupkg_asset" = "null" ]; then + echo "::error::No ManagedCode.MLXSharp.*.nupkg asset found in release ${tag}" >&2 + exit 1 + fi - - name: Build native library - run: cmake --build native/build/macos --target mlxsharp --config Release + asset_url=$(echo "$response" | jq -r --arg name "$nupkg_asset" '.assets[] | select(.name == $name) | .url') + if [ -z "$asset_url" ] || [ "$asset_url" = "null" ]; then + echo "::error::Failed to resolve download URL for ${nupkg_asset}" >&2 + exit 1 + fi - - name: Package native artifact - run: | - mkdir -p artifacts/native/osx-arm64 - cp native/build/macos/libmlxsharp.dylib artifacts/native/osx-arm64/ - cp native/build/macos/extern/mlx/mlx/backend/metal/kernels/mlx.metallib artifacts/native/osx-arm64/ + mkdir -p work artifacts/native/osx-arm64 artifacts/native/linux-x64 - - name: Upload native artifact + echo "Downloading ${nupkg_asset}" + curl -fsSL -H "Accept: application/octet-stream" -H "Authorization: Bearer ${GITHUB_TOKEN}" "$asset_url" -o work/native.nupkg + + echo "Extracting native runtimes" + unzip -q work/native.nupkg 'runtimes/osx-arm64/native/*' -d work/extract + unzip -q work/native.nupkg 'runtimes/linux-x64/native/*' -d work/extract + + shopt -s nullglob + mac_files=(work/extract/runtimes/osx-arm64/native/*) + linux_files=(work/extract/runtimes/linux-x64/native/*) + + if [ ${#mac_files[@]} -eq 0 ]; then + echo "::error::macOS native assets are missing from ${nupkg_asset}" >&2 + exit 1 + fi + + if [ ${#linux_files[@]} -eq 0 ]; then + echo "::error::Linux native assets are missing from ${nupkg_asset}" >&2 + exit 1 + fi + + cp work/extract/runtimes/osx-arm64/native/* artifacts/native/osx-arm64/ + cp work/extract/runtimes/linux-x64/native/* artifacts/native/linux-x64/ + + echo "Staged native artifacts:" + ls -R artifacts/native + + - name: Upload macOS native artifact uses: actions/upload-artifact@v4 with: name: native-osx-arm64 path: artifacts/native/osx-arm64 + - name: Upload Linux native artifact + uses: actions/upload-artifact@v4 + with: + name: native-linux-x64 + path: artifacts/native/linux-x64 + package-test: needs: - - native-linux - - native-macos + - native-assets runs-on: macos-latest steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index eafd8ab..31ecd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -872,4 +872,8 @@ FodyWeavers.xsd # Local model files for testing Tests/**/LocalModels/ -Apps/**/LocalModels/ \ No newline at end of file +Apps/**/LocalModels/ +# Downloaded native artifacts +libs/native-libs/ +ManagedCode.MLXSharp.nupkg + diff --git a/README.md b/README.md index 996a79f..e5f4103 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ The CMake project vendored from MLX builds MLX and the shim in one go. macOS bui ## CI overview 1. `dotnet-build` (Ubuntu): restores the solution and compiles managed projects. -2. `native-linux` / `native-macos`: compile `libmlxsharp.so` and `libmlxsharp.dylib` in parallel. -3. `package-test` (macOS): downloads both native artifacts, stages them into `src/MLXSharp/runtimes/{rid}/native`, rebuilds, runs the integration tests, and produces NuGet packages. +2. `native-assets` (Ubuntu): downloads the signed native binaries published with the latest MLXSharp release and uploads them as workflow artifacts. +3. `package-test` (macOS): pulls down the staged native artifacts, copies them into `src/MLXSharp/runtimes/{rid}/native`, rebuilds, runs the integration tests, and produces NuGet packages. ## Testing The managed integration tests still piggy-back on `mlx_lm` until the native runner is feature-complete. Bring your own HuggingFace bundle (any MLX-compatible repo) and point `MLXSHARP_MODEL_PATH` to it before running: diff --git a/src/MLXSharp.Tests/ModelIntegrationTests.cs b/src/MLXSharp.Tests/ModelIntegrationTests.cs index 7cc4696..98c8f8f 100644 --- a/src/MLXSharp.Tests/ModelIntegrationTests.cs +++ b/src/MLXSharp.Tests/ModelIntegrationTests.cs @@ -14,7 +14,7 @@ public sealed class ModelIntegrationTests public async Task NativeBackendAnswersSimpleMathAsync() { TestEnvironment.EnsureInitialized(); - EnsureAssetsOrSkip(); + EnsureAssets(); var options = CreateOptions(); using var backend = MlxNativeBackend.Create(options); @@ -59,18 +59,14 @@ private static MlxClientOptions CreateOptions() return options; } - private static void EnsureAssetsOrSkip() + private static void EnsureAssets() { var modelPath = Environment.GetEnvironmentVariable("MLXSHARP_MODEL_PATH"); - if (string.IsNullOrWhiteSpace(modelPath) || !System.IO.Directory.Exists(modelPath)) - { - Skip.If(true, "Native model bundle not found."); - } + Assert.False(string.IsNullOrWhiteSpace(modelPath), "Native model bundle path is not configured. Set MLXSHARP_MODEL_PATH to a valid directory."); + Assert.True(System.IO.Directory.Exists(modelPath), $"Native model bundle not found at '{modelPath}'."); var library = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY"); - if (string.IsNullOrWhiteSpace(library) || !System.IO.File.Exists(library)) - { - Skip.If(true, "Native libmlxsharp library not configured."); - } + Assert.False(string.IsNullOrWhiteSpace(library), "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the staged native library that ships with the official MLXSharp release."); + Assert.True(System.IO.File.Exists(library), $"Native libmlxsharp library not found at '{library}'."); } } diff --git a/src/MLXSharp/MLXSharp.csproj b/src/MLXSharp/MLXSharp.csproj index d258dff..8a2a84e 100644 --- a/src/MLXSharp/MLXSharp.csproj +++ b/src/MLXSharp/MLXSharp.csproj @@ -16,13 +16,21 @@ + <_RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../..')) + $([System.IO.Path]::Combine('$(_RepoRoot)','libs','native-libs')) + <_MLXSharpMacNativeFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'osx-arm64', 'libmlxsharp.dylib')) + <_MLXSharpMacMetallibFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'osx-arm64', 'mlx.metallib')) + <_MLXSharpLinuxNativeFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'linux-x64', 'libmlxsharp.so')) + $(_MLXSharpMacNativeFromLibs) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','libmlxsharp.dylib')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','osx-arm64','native','libmlxsharp.dylib')) false $([System.IO.Path]::GetDirectoryName('$(MLXSharpMacNativeDestination)')) + $(_MLXSharpMacMetallibFromLibs) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','macos','extern','mlx','mlx','backend','metal','kernels','mlx.metallib')) $([System.IO.Path]::Combine('$(MLXSharpMacNativeDestinationDir)','mlx.metallib')) + $(_MLXSharpLinuxNativeFromLibs) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','linux','libmlxsharp.so')) $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','linux-x64','native','libmlxsharp.so')) false