Skip to content
Merged
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
118 changes: 75 additions & 43 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -872,4 +872,8 @@ FodyWeavers.xsd

# Local model files for testing
Tests/**/LocalModels/
Apps/**/LocalModels/
Apps/**/LocalModels/
# Downloaded native artifacts
libs/native-libs/
ManagedCode.MLXSharp.nupkg

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 6 additions & 10 deletions src/MLXSharp.Tests/ModelIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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}'.");
}
}
8 changes: 8 additions & 0 deletions src/MLXSharp/MLXSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
</ItemGroup>

<PropertyGroup>
<_RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../..'))</_RepoRoot>
<MLXSharpNativeLibsDir Condition="'$(MLXSharpNativeLibsDir)' == ''">$([System.IO.Path]::Combine('$(_RepoRoot)','libs','native-libs'))</MLXSharpNativeLibsDir>
<_MLXSharpMacNativeFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'osx-arm64', 'libmlxsharp.dylib'))</_MLXSharpMacNativeFromLibs>
<_MLXSharpMacMetallibFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'osx-arm64', 'mlx.metallib'))</_MLXSharpMacMetallibFromLibs>
<_MLXSharpLinuxNativeFromLibs>$([System.IO.Path]::Combine($(MLXSharpNativeLibsDir), 'linux-x64', 'libmlxsharp.so'))</_MLXSharpLinuxNativeFromLibs>
<MLXSharpMacNativeBinary Condition="'$(MLXSharpMacNativeBinary)' == '' and Exists('$(_MLXSharpMacNativeFromLibs)')">$(_MLXSharpMacNativeFromLibs)</MLXSharpMacNativeBinary>
<MLXSharpMacNativeBinary Condition="'$(MLXSharpMacNativeBinary)' == ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','libmlxsharp.dylib'))</MLXSharpMacNativeBinary>
<MLXSharpMacNativeDestination>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','osx-arm64','native','libmlxsharp.dylib'))</MLXSharpMacNativeDestination>
<MLXSharpSkipMacNativeValidation Condition="'$(MLXSharpSkipMacNativeValidation)' == ''">false</MLXSharpSkipMacNativeValidation>
<MLXSharpMacNativeDestinationDir>$([System.IO.Path]::GetDirectoryName('$(MLXSharpMacNativeDestination)'))</MLXSharpMacNativeDestinationDir>
<MLXSharpMacMetallibBinary Condition="'$(MLXSharpMacMetallibBinary)' == '' and Exists('$(_MLXSharpMacMetallibFromLibs)')">$(_MLXSharpMacMetallibFromLibs)</MLXSharpMacMetallibBinary>
<MLXSharpMacMetallibBinary Condition="'$(MLXSharpMacMetallibBinary)' == ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','macos','extern','mlx','mlx','backend','metal','kernels','mlx.metallib'))</MLXSharpMacMetallibBinary>
<MLXSharpMacMetallibDestination Condition="'$(MLXSharpMacMetallibDestination)' == ''">$([System.IO.Path]::Combine('$(MLXSharpMacNativeDestinationDir)','mlx.metallib'))</MLXSharpMacMetallibDestination>

<MLXSharpLinuxNativeBinary Condition="'$(MLXSharpLinuxNativeBinary)' == '' and Exists('$(_MLXSharpLinuxNativeFromLibs)')">$(_MLXSharpLinuxNativeFromLibs)</MLXSharpLinuxNativeBinary>
<MLXSharpLinuxNativeBinary Condition="'$(MLXSharpLinuxNativeBinary)' == ''">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','..','..','native','build','linux','libmlxsharp.so'))</MLXSharpLinuxNativeBinary>
<MLXSharpLinuxNativeDestination>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','runtimes','linux-x64','native','libmlxsharp.so'))</MLXSharpLinuxNativeDestination>
<MLXSharpSkipLinuxNativeValidation Condition="'$(MLXSharpSkipLinuxNativeValidation)' == ''">false</MLXSharpSkipLinuxNativeValidation>
Expand Down
Loading