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
77 changes: 77 additions & 0 deletions .github/workflows/mlx_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: MLX Tests (SwiftPM metallib)

# The "Build and Test" job in pull_request.yml only runs on ml-explore/mlx-swift (self-hosted runner), and the
# integration workflows copy a full mlx.metallib from the Python wheel, so none of them runs the Metal kernels
# that SwiftPM itself bundles into default.metallib. A kernel missing from KERNEL_LIST in
# tools/fix-metal-includes.sh therefore only shows up at run time in a consumer that has no mlx.metallib next
# to its binary. This job builds MLXTests with Xcode and runs them with no mlx.metallib on disk, so the tests
# load the metallib that SwiftPM built from Source/Cmlx. The second job builds xcode/MLX.xcodeproj, whose
# Cmlx target keeps its own list of excluded kernel sources that must be extended for every new .metal file.

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]

permissions:
contents: read

jobs:
mlx-tests:
name: MLXTests (SwiftPM bundle metallib)
runs-on: macos-15
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Select Xcode 26 (Swift 6.2)
run: sudo xcode-select -s /Applications/Xcode_26.3.app

- name: Install Metal Toolchain
run: xcodebuild -downloadComponent MetalToolchain || true

- name: Build for testing (macOS)
run: |
xcodebuild build-for-testing \
-scheme mlx-swift-Package \
-destination 'platform=macOS' \
-derivedDataPath "$RUNNER_TEMP/dd" \
-skipPackagePluginValidation -skipMacroValidation

- name: Run MLXTests
run: |
# Fail early if a stray mlx.metallib could hide a kernel missing from default.metallib.
if find "$RUNNER_TEMP/dd/Build/Products" -name mlx.metallib | grep -q .; then
echo "mlx.metallib found next to the test products; this job must use the SwiftPM bundle metallib"
exit 1
fi
xcrun xctest "$RUNNER_TEMP/dd/Build/Products/Debug/MLXTests.xctest"

xcodeproj-build:
name: Build xcode/MLX.xcodeproj (macOS)
runs-on: macos-15
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Select Xcode 26 (Swift 6.2)
run: sudo xcode-select -s /Applications/Xcode_26.3.app

- name: Install Metal Toolchain
run: xcodebuild -downloadComponent MetalToolchain || true

- name: Build xcodeproj
working-directory: xcode
run: |
xcodebuild build-for-testing \
-scheme MLX \
-destination 'platform=macOS' \
-derivedDataPath "$RUNNER_TEMP/dd-xcodeproj" \
-skipPackagePluginValidation -skipMacroValidation
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ let noCudaCmlxExcludes = [
// Exclude CUDA backend files, but keep no_cuda.cpp for stubs
// mlx/mlx/backend/cuda/no_cuda.cpp
"mlx/mlx/backend/cuda/allocator.cpp",
"mlx/mlx/backend/cuda/cholesky.cu",
"mlx/mlx/backend/cuda/compiled.cpp",
"mlx/mlx/backend/cuda/conv.cpp",
"mlx/mlx/backend/cuda/cublas_utils.cpp",
"mlx/mlx/backend/cuda/cudnn_utils.cpp",
"mlx/mlx/backend/cuda/cusolver_utils.cpp",
"mlx/mlx/backend/cuda/custom_kernel.cpp",
"mlx/mlx/backend/cuda/delayload.cpp",
"mlx/mlx/backend/cuda/device_info.cpp",
Expand All @@ -77,6 +79,7 @@ let noCudaCmlxExcludes = [
"mlx/mlx/backend/cuda/scaled_dot_product_attention.cpp",
"mlx/mlx/backend/cuda/slicing.cpp",
"mlx/mlx/backend/cuda/utils.cpp",
"mlx/mlx/backend/cuda/wddm.cpp",
"mlx/mlx/backend/cuda/worker.cpp",
"mlx/mlx/backend/cuda/binary",
"mlx/mlx/backend/cuda/conv",
Expand Down
2 changes: 1 addition & 1 deletion Source/Cmlx/include-framework/Metal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Metal.hpp.in
//
// Autogenerated from commit 8f784840de21cacd0c5c237ea860179e47dcec91.
// Autogenerated on October 05, 2026.
//
// Copyright 2020-2024 Apple Inc.
//
Expand Down
28 changes: 23 additions & 5 deletions Source/Cmlx/include-framework/mlx-array.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cstdint>
#include <functional>
#include <memory>
#include <utility>
#include <vector>

#include <Cmlx/mlx-allocator.h>
Expand Down Expand Up @@ -81,13 +82,22 @@ class MLX_API array {
array& operator=(array&& other) && = delete;

/** Default copy and move constructors otherwise. */
array& operator=(array&& other) & = default;
array(const array& other) = default;
array(array&& other) = default;

/** Assignment goes through release() to break sibling reference cycle. */
array& operator=(const array& other) & {
if (this->id() != other.id()) {
this->array_desc_ = other.array_desc_;
if (array_desc_ != other.array_desc_) {
// Hold the old descriptor while assigning, and then release it. Otherwise
// when |this| and |other| are siblings they would be both destroyed.
release(std::exchange(array_desc_, other.array_desc_));
}
return *this;
}

array& operator=(array&& other) & noexcept {
if (array_desc_ != other.array_desc_) {
release(std::exchange(array_desc_, std::move(other.array_desc_)));
}
return *this;
}
Expand Down Expand Up @@ -467,10 +477,12 @@ class MLX_API array {
void copy_shared_buffer(const array& other);

void overwrite_descriptor(const array& other) {
array_desc_ = other.array_desc_;
release(std::exchange(array_desc_, other.array_desc_));
}

~array();
~array() {
release(std::move(array_desc_));
}

private:
// Initialize the arrays data
Expand Down Expand Up @@ -533,6 +545,12 @@ class MLX_API array {
// the primitive which knows how to compute the array's data from its inputs
// and the list of array's inputs for the primitive.
std::shared_ptr<ArrayDesc> array_desc_;

/**
* Lets go of an ArrayDesc, breaking the sibling reference cycle of a
* multi-output primitive.
*/
static void release(std::shared_ptr<ArrayDesc> desc);
};

template <typename T>
Expand Down
10 changes: 10 additions & 0 deletions Source/Cmlx/include-framework/mlx-fast.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ MLX_API array scaled_dot_product_attention(
bool force_fused = false,
StreamOrDevice s = {});

MLX_API std::vector<array> gated_delta_update(
const array& queries,
const array& keys,
const array& values,
const array& gates,
const array& beta_,
const std::optional<array>& initial_state = std::nullopt,
const std::optional<array>& mask = std::nullopt,
StreamOrDevice s = {});

using TemplateArg = std::variant<int, bool, Dtype>;
using ScalarArg = std::variant<bool, int, float>;

Expand Down
97 changes: 72 additions & 25 deletions Source/Cmlx/include-framework/mlx-io-load.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
#pragma once

#include <memory>
#include <mutex>
#include <sstream>
#include <climits> // SIZE_MAX
#include <climits> // SIZE_MAX
#include <utility>

#include <fcntl.h>
#ifdef _MSC_VER
#ifdef _WIN32
#include <io.h>
#else
#include <sys/stat.h>
Expand Down Expand Up @@ -67,6 +69,8 @@ class Writer {
std::ios_base::seekdir way = std::ios_base::beg) = 0;
virtual void write(const char* data, size_t n) = 0;
virtual std::string label() const = 0;
virtual void open() {}

virtual ~Writer() = default;
};

Expand All @@ -89,20 +93,30 @@ class ParallelFileReader : public Reader {
}

size_t tell() override {
#ifdef _WIN32
return _lseeki64(fd_, 0, SEEK_CUR);
#else
return lseek(fd_, 0, SEEK_CUR);
#endif
}

// Warning: do not use this function from multiple threads as
// it advances the file descriptor
void seek(int64_t off, std::ios_base::seekdir way = std::ios_base::beg)
override {
int origin;
if (way == std::ios_base::beg) {
lseek(fd_, off, SEEK_SET);
origin = SEEK_SET;
} else if (way == std::ios_base::end) {
lseek(fd_, off, SEEK_END);
origin = SEEK_END;
} else {
lseek(fd_, off, SEEK_CUR);
origin = SEEK_CUR;
}
#ifdef _WIN32
_lseeki64(fd_, off, origin);
#else
lseek(fd_, off, origin);
#endif
}

// Warning: do not use this function from multiple threads as
Expand All @@ -116,42 +130,56 @@ class ParallelFileReader : public Reader {
}

private:
// On iOS, always use sequential pread() — no std::async thread spawning.
// std::async wakeup bursts during model loading exceed iOS's per-process
// wakeup rate limit → EXC_RESOURCE (RESOURCE_TYPE_WAKEUPS) crash.
// On macOS, 32MB parallel batches maximize NVMe throughput.
// Reads larger than this are split in batches and read in parallel.
// Fork: on iOS, always use sequential pread() (no thread spawning). Wakeup
// bursts during model loading exceed iOS's per-process wakeup rate limit
// (EXC_RESOURCE, RESOURCE_TYPE_WAKEUPS).
#ifdef MLX_IOS_SEQUENTIAL_IO
static constexpr size_t batch_size_ = SIZE_MAX;
#else
static constexpr size_t batch_size_ = 1 << 25; // 32MB
static constexpr size_t batch_size_ = 1 << 25;
#endif
static ThreadPool& thread_pool();

// The pool that reads the batches, held from the first batched read until
// the reader is destroyed. It cannot be io::thread_pool(), the tasks that
// run there wait for these batches and would deadlock in the same pool.
ThreadPool& thread_pool();

int fd_;
std::string label_;
std::once_flag pool_once_;
std::shared_ptr<ThreadPool> pool_;
};

class FileWriter : public Writer {
public:
explicit FileWriter() {}
explicit FileWriter(std::string file_path)
: fd_(open(
file_path.c_str(),
O_CREAT | O_WRONLY | O_TRUNC | O_BINARY,
0644)),
label_(std::move(file_path)) {}
: file_path_(std::move(file_path)) {}

FileWriter(const FileWriter&) = delete;
FileWriter& operator=(const FileWriter&) = delete;
FileWriter(FileWriter&& other) {
std::swap(fd_, other.fd_);
FileWriter(FileWriter&& other)
: fd_(std::exchange(other.fd_, -1)),
file_path_(std::move(other.file_path_)) {
other.file_path_.clear();
}

~FileWriter() override {
if (fd_ != 0) {
if (fd_ >= 0) {
close(fd_);
}
}

// Kept separate from construction so lazy inputs can be evaluated first,
// they may still read from the file.
void open() override {
if (fd_ < 0 && !file_path_.empty()) {
fd_ = ::open(
file_path_.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_BINARY, 0644);
}
}

bool is_open() const override {
return fd_ >= 0;
}
Expand All @@ -161,21 +189,34 @@ class FileWriter : public Writer {
}

size_t tell() override {
check_open();
#ifdef _WIN32
return _lseeki64(fd_, 0, SEEK_CUR);
#else
return lseek(fd_, 0, SEEK_CUR);
#endif
}

void seek(int64_t off, std::ios_base::seekdir way = std::ios_base::beg)
override {
check_open();
int origin;
if (way == std::ios_base::beg) {
lseek(fd_, off, SEEK_SET);
origin = SEEK_SET;
} else if (way == std::ios_base::end) {
lseek(fd_, off, SEEK_END);
origin = SEEK_END;
} else {
lseek(fd_, off, SEEK_CUR);
origin = SEEK_CUR;
}
#ifdef _WIN32
_lseeki64(fd_, off, origin);
#else
lseek(fd_, off, origin);
#endif
}

void write(const char* data, size_t n) override {
check_open();
while (n != 0) {
auto m = ::write(fd_, data, std::min(n, static_cast<size_t>(INT32_MAX)));
if (m <= 0) {
Expand All @@ -189,12 +230,18 @@ class FileWriter : public Writer {
}

std::string label() const override {
return "file " + label_;
return "file " + file_path_;
}

private:
int fd_{0};
std::string label_;
void check_open() const {
if (!is_open()) {
throw std::runtime_error("[write] File " + file_path_ + " is not open.");
}
}

int fd_{-1};
std::string file_path_;
};

} // namespace io
Expand Down
Loading
Loading