Compare commits

..

1 Commits

Author SHA1 Message Date
Angelos Katharopoulos 5ae36f2c08 Tentative JACCL examples 2026-04-22 01:29:40 -07:00
44 changed files with 633 additions and 863 deletions
@@ -20,7 +20,7 @@ runs:
run: |
pip install auditwheel "build<=1.4.2" patchelf setuptools
python setup.py clean --all
MLX_BUILD_STAGE=2 python -m build -w
MLX_DISABLE_SM90A_KERNELS=1 MLX_BUILD_STAGE=2 python -m build -w
auditwheel repair dist/mlx_cuda*.whl \
--plat manylinux_2_35_${{ inputs.arch }} \
@@ -21,7 +21,7 @@ runs:
DEVELOPER_DIR: /Applications/Xcode-latest.app
MACOSX_DEPLOYMENT_TARGET: ${{ inputs.macos-target }}
run: |
uv pip install build
pip install build
python setup.py clean --all
MLX_BUILD_STAGE=1 python -m build -w
+20 -34
View File
@@ -4,72 +4,61 @@ description: 'Build and test MLX on macOS'
runs:
using: "composite"
steps:
- name: Install Python package
- name: Install dependencies
env:
DEBUG: 1
CMAKE_ARGS: "-DCMAKE_COMPILE_WARNING_AS_ERROR=ON"
shell: bash
shell: bash -l {0}
run: |
echo "::group::Install Python package"
uv pip install -e ".[dev]" -v
echo "::endgroup::"
pip install --upgrade pip
pip install cmake setuptools typing_extensions
pip install -e ".[dev]" -v
- name: Install tests dependencies
shell: bash
shell: bash -l {0}
run: |
echo "::group::Install tests dependencies"
uv pip install tensorflow
echo "::endgroup::"
pip install tensorflow
- name: Run Python tests
shell: bash
shell: bash -l {0}
env:
LOW_MEMORY: 1
run: |
echo "::group::Run Python tests"
DEVICE=cpu python -m unittest discover -v python/tests
DEVICE=gpu METAL_DEVICE_WRAPPER_TYPE=1 METAL_DEBUG_ERROR_MODE=0 python -m unittest discover -v python/tests
mpirun --bind-to none -host localhost:8 -np 8 -x DYLD_LIBRARY_PATH=/opt/homebrew/lib/ python python/tests/mpi_test_distributed.py
mlx.launch --verbose -n 8 python/tests/ring_test_distributed.py -v 2> >(tee -a stderr.log >&2)
if $(grep "\[WARN\]" stderr.log); then echo "Distributed ring test failed"; exit 1; fi
echo "::endgroup::"
- name: Build example extension
shell: bash
shell: bash -l {0}
run: |
echo "::group::Build example extension"
cd examples/extensions
uv pip install -r requirements.txt
uv run --no-project setup.py build_ext --inplace
uv run --no-project test.py
echo "::endgroup::"
pip install -r requirements.txt
python setup.py build_ext --inplace
python test.py
- name: Build CPP only
shell: bash
shell: bash -l {0}
run: |
echo "::group::Build CPP only"
mkdir -p build
cd build
cmake ..
make -j $(sysctl -n hw.ncpu)
echo "::endgroup::"
- name: Run CPP tests
shell: bash
shell: bash -l {0}
env:
DEVICE: gpu
METAL_DEVICE_WRAPPER_TYPE: 1
METAL_DEBUG_ERROR_MODE: 0
run: |
echo "::group::Run CPP tests"
./build/tests/tests
./build/tests/test_teardown
echo "::endgroup::"
- name: Build small binary with JIT
shell: bash
shell: bash -l {0}
run: |
echo "::group::Build small binary with JIT"
mkdir -p build
cd build
cmake .. -DCMAKE_BUILD_TYPE=MinSizeRel \
@@ -79,18 +68,15 @@ runs:
-DMLX_BUILD_GGUF=OFF \
-DMLX_METAL_JIT=ON
make -j $(sysctl -n hw.ncpu)
echo "::endgroup::"
- name: Run Python tests with JIT
shell: bash
shell: bash -l {0}
env:
LOW_MEMORY: 1
DEVICE: gpu
METAL_DEVICE_WRAPPER_TYPE: 1
METAL_DEBUG_ERROR_MODE: 0
run: |
echo "::group::Run Python tests with JIT"
CMAKE_ARGS="-DMLX_METAL_JIT=ON" \
uv pip install -e . -v
pip install -e . -v
python -m unittest discover -v python/tests
echo "::endgroup::"
+1 -4
View File
@@ -14,9 +14,6 @@ inputs:
description: 'Whether to enable ccache'
required: false
default: 'true'
ccache-key:
required: false
default: 'ccache'
runs:
using: "composite"
@@ -36,7 +33,7 @@ runs:
if: ${{ inputs.use-ccache == 'true' }}
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ inputs.ccache-key }}-${{ runner.os }}-${{ runner.arch }}-${{ inputs.toolkit }}
key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.toolkit }}
max-size: 1GB
# ccache-action bug: running "apt-get update" fails on large arm runner.
update-package-index: false
+5 -13
View File
@@ -13,20 +13,12 @@ runs:
- name: Install Homebrew packages
shell: sh
run: /opt/homebrew/bin/brew install openmpi
- name: Verify MetalToolchain installed
shell: bash
run: xcodebuild -showComponent MetalToolchain
- uses: astral-sh/setup-uv@v7
- name: Setup Python venv
shell: bash
run: |
echo "::group::Setup Python venv"
uv venv --python ${{ inputs.python-version }}
source .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
# Search python packages in .venv
echo PYTHONPATH=`python -c 'import sys; print(sys.path[-1])'` >> $GITHUB_ENV
echo "::endgroup::"
- uses: conda-incubator/setup-miniconda@v3
with:
miniconda-version: "latest"
python-version: ${{ inputs.python-version }}
+5 -9
View File
@@ -85,24 +85,20 @@ jobs:
build_cuda_release:
if: github.repository == 'ml-explore/mlx'
strategy:
matrix:
arch: ['x86_64', 'aarch64']
toolkit: ['cuda-12.9', 'cuda-13.0']
runs-on: ${{ matrix.arch == 'x86_64' && 'ubuntu-22-large' || 'ubuntu-22-large-arm' }}
runs-on: ubuntu-22-large
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-linux
with:
toolkit: ${{ matrix.toolkit }}
ccache-key: 'ccache-release'
toolkit: 'cuda-12.9'
- name: Build Python package
uses: ./.github/actions/build-cuda-release
with:
arch: ${{ matrix.arch }}
toolkit: 'cuda-12.9'
arch: 'x86_64'
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: mlx-${{ matrix.toolkit }}-${{ matrix.arch }}
name: mlx-cuda
path: wheelhouse/mlx_cuda_*.whl
retention-days: 7
+8 -3
View File
@@ -93,8 +93,13 @@ jobs:
- uses: ./.github/actions/setup-macos
with:
python-version: ${{ matrix.python-version }}
- name: Install Python package
run: uv pip install -e . -v
- name: Install dependencies
shell: bash -l {0}
run: |
pip install --upgrade pip
pip install cmake setuptools typing_extensions
pip install -e . -v
- name: Build macOS 14 package
uses: ./.github/actions/build-macos-release
with:
@@ -141,7 +146,7 @@ jobs:
- uses: ./.github/actions/setup-linux
with:
toolkit: ${{ matrix.toolkit }}
ccache-key: 'ccache-release'
use-ccache: false
- name: Build Python package
uses: ./.github/actions/build-cuda-release
with:
-2
View File
@@ -14,7 +14,6 @@ Linear Algebra
cholesky
cholesky_inv
cross
det
qr
svd
eigvals
@@ -24,6 +23,5 @@ Linear Algebra
lu
lu_factor
pinv
slogdet
solve
solve_triangular
+1 -1
View File
@@ -1,4 +1,4 @@
setuptools>=42
cmake>=3.25
mlx>=0.31.2
mlx>=0.21.0
nanobind==2.12.0
+3 -2
View File
@@ -67,10 +67,11 @@ void luf_impl(
/* ipiv */ reinterpret_cast<int*>(pivots_ptr),
/* info */ &info);
if (info < 0) {
if (info != 0) {
std::stringstream ss;
ss << "[LUF::eval_cpu] sgetrf_ failed with code " << info
<< " because argument had an illegal value";
<< ((info > 0) ? " because matrix is singular"
: " because argument had an illegal value");
throw std::runtime_error(ss.str());
}
+3 -2
View File
@@ -168,8 +168,9 @@ set_target_properties(mlx PROPERTIES CUDA_ARCHITECTURES
"${MLX_CUDA_ARCHITECTURES}")
# Skip Hopper-only kernels when not building for sm90a.
if(("90a" IN_LIST MLX_CUDA_ARCHITECTURES) OR ("90a-real" IN_LIST
MLX_CUDA_ARCHITECTURES))
if(NOT DEFINED ENV{MLX_DISABLE_SM90A_KERNELS}
AND (("90a" IN_LIST MLX_CUDA_ARCHITECTURES) OR ("90a-real" IN_LIST
MLX_CUDA_ARCHITECTURES)))
target_compile_definitions(mlx PRIVATE MLX_CUDA_SM90A_ENABLED)
endif()
+5 -10
View File
@@ -43,12 +43,6 @@ class GatherGemm {
using ElementD = typename CollectiveEpilogue::ElementD;
using StrideD = typename CollectiveEpilogue::StrideD;
static_assert(
cute::is_same_v<
ElementAccumulator,
typename CollectiveEpilogue::ElementAccumulator>,
"Mainloop and epilogue do not agree on accumulator value type.");
static constexpr int SharedStorageSize = static_cast<int>(cute::max(
sizeof(typename CollectiveMainloop::SharedStorage),
sizeof(typename CollectiveEpilogue::SharedStorage)));
@@ -104,9 +98,7 @@ class GatherGemm {
CUTLASS_DEVICE void operator()(const Params& params, char* smem_buf) {
int thread_idx = int(threadIdx.x);
int m_coord = int(blockIdx.x);
int n_coord = int(blockIdx.y);
int l_coord = int(blockIdx.z);
auto [m_coord, n_coord, l_coord] = uint3(blockIdx);
auto shape_MNKL = append<4>(params.problem_shape, Int<1>{});
auto cta_tile = TileShape{};
@@ -228,7 +220,7 @@ void gather_mm(
using TileShape = Shape<_128, _128, _8>;
using DispatchPolicy = cutlass::gemm::MainloopSm70TwoStage;
using TiledMma = TiledMMA<
MMA_Atom<UniversalFMA<Accumulator, Element, Element, Accumulator>>,
MMA_Atom<UniversalFMA<Accumulator, Element, Element, Element>>,
Layout<Shape<_16, _16, _1>>>;
using CopyTraitsA = SimtCopyTraits<Element, k_major_a.value>;
@@ -304,6 +296,9 @@ void cutlass_gather_mm(
int n = out.shape(-1);
int k = a.shape(-1);
int l = out.size() / (m * n);
if (m < 16 || n < 16) {
throw std::invalid_argument("[gather_mm] M/N is too small.");
}
encoder.set_input_array(a);
encoder.set_input_array(b);
@@ -245,7 +245,7 @@ void grouped_gemm_v2(
LayoutB,
cutlass::ComplexTransform::kNone,
GemmConfiguration::kAlignmentAB,
typename GemmConfiguration::Accumulator,
typename GemmConfiguration::Element,
cutlass::layout::RowMajor,
typename GemmConfiguration::Accumulator,
typename GemmConfiguration::OpClass,
+6 -7
View File
@@ -52,7 +52,7 @@ bool supports_qmm_sm90(
if (!biases) {
return false;
}
if (!is_last_2_dims_row_contiguous(w) ||
if (!x.flags().row_contiguous || !is_last_2_dims_row_contiguous(w) ||
!is_last_2_dims_row_contiguous(scales) ||
!is_last_2_dims_row_contiguous(*biases)) {
return false;
@@ -139,7 +139,7 @@ bool supports_qmm_sm80(
if ((n % 128 != 0) || (k % std::max(64, group_size) != 0)) {
return false;
}
if (!is_last_2_dims_row_contiguous(w) ||
if (!x.flags().row_contiguous || !is_last_2_dims_row_contiguous(w) ||
!is_last_2_dims_row_contiguous(scales)) {
return false;
}
@@ -224,7 +224,7 @@ bool supports_qmm_naive(
if (transpose && (k % std::max(64, group_size) != 0)) {
return false;
}
if (!is_last_2_dims_row_contiguous(w) ||
if (!x.flags().row_contiguous || !is_last_2_dims_row_contiguous(w) ||
!is_last_2_dims_row_contiguous(scales)) {
return false;
}
@@ -265,7 +265,7 @@ void qmm_naive(
if constexpr (k_major.value) {
if (has_k_residue) {
throw std::invalid_argument(
"[quantized_matmul] K must be multiples of max(64, group_size).");
"[quantized_matmul] K must be multiples of group_size.");
}
f.template operator()<false>();
} else {
@@ -276,8 +276,7 @@ void qmm_naive(
};
int m = out.ndim() > 1 ? out.shape(-2) : 1;
int k = x.shape(-1);
int tile_k = std::max(64, group_size);
bool has_k_residue = k % tile_k != 0;
bool has_k_residue = k % group_size != 0;
bool sm80 = encoder.device().compute_capability_major() >= 8;
dispatch_bool(transpose, [&](auto k_major) {
dispatch_k(k_major, has_k_residue, [&]<bool HasKResidue>() {
@@ -343,7 +342,7 @@ bool supports_qmv(
if (k % 8 != 0) {
return false;
}
if (!is_last_2_dims_row_contiguous(w) ||
if (!x.flags().row_contiguous || !is_last_2_dims_row_contiguous(w) ||
!is_last_2_dims_row_contiguous(scales)) {
return false;
}
+1 -3
View File
@@ -60,9 +60,7 @@ __global__ void qmm_naive_kernel(
CUTE_STATIC_ASSERT_V(congruent(select<0,1,3>(shape_MNKL), dC));
int thread_idx = int(threadIdx.x);
int m_coord = int(blockIdx.x);
int n_coord = int(blockIdx.y);
int l_coord = int(blockIdx.z);
auto [m_coord, n_coord, l_coord] = static_cast<uint3>(blockIdx);
auto m_max_coord = size<0>(shape_MNKL) - size<0>(cta_tiler) * m_coord; // M - BLK_M * m_coord
auto n_max_coord = size<1>(shape_MNKL) - size<1>(cta_tiler) * n_coord; // N - BLK_N * n_coord
+1 -3
View File
@@ -48,9 +48,7 @@ __global__ void qmm_sm80_kernel(
CUTE_STATIC_ASSERT_V(congruent(select<0,1,3>(shape_MNKL), dC));
int thread_idx = int(threadIdx.x);
int m_coord = int(blockIdx.x);
int n_coord = int(blockIdx.y);
int l_coord = int(blockIdx.z);
auto [m_coord, n_coord, l_coord] = static_cast<uint3>(blockIdx);
// For gather, use index lookup for input batch slicing.
uint32_t a_batch = lhs_indices ? lhs_indices[l_coord] : l_coord;
+4 -6
View File
@@ -17,7 +17,7 @@ void QuantizedMatmul::eval_gpu(const std::vector<array>& inputs, array& out) {
auto& s = stream();
auto& encoder = cu::get_command_encoder(s);
array x = ensure_row_contiguous(inputs[0], encoder, s);
const array& x = inputs[0];
const array& w = inputs[1];
const array& scales = inputs[2];
std::optional<array> biases;
@@ -146,17 +146,15 @@ void GatherQMM::eval_gpu(const std::vector<array>& inputs, array& out) {
auto& s = stream();
auto& encoder = cu::get_command_encoder(s);
array x = ensure_row_contiguous(inputs[0], encoder, s);
const array& x = inputs[0];
const array& w = inputs[1];
const array& scales = inputs[2];
std::optional<array> biases;
if (inputs.size() == 6) {
biases = inputs[3];
}
array lhs_indices =
ensure_row_contiguous(inputs[inputs.size() - 2], encoder, s);
array rhs_indices =
ensure_row_contiguous(inputs[inputs.size() - 1], encoder, s);
array lhs_indices = ensure_contiguous(inputs[inputs.size() - 2], encoder, s);
array rhs_indices = ensure_contiguous(inputs[inputs.size() - 1], encoder, s);
int M = out.ndim() > 1 ? out.shape(-2) : 1;
int N = out.shape(-1);
-5
View File
@@ -29,8 +29,6 @@ in macOS 26.2.
- **Point-to-Point Operations**:
- `send`: Send data to a specific node
- `recv`: Receive data from a specific node
- **Synchronization**:
- `barrier`: Block until all nodes in the group reach this point
- **Type Support**: Bool, Int8-64, UInt8-64, Float16, BFloat16, Float32,
Float64, Complex64
@@ -288,9 +286,6 @@ class Group {
// Simple send/recv primitives.
virtual void send(const void* input, size_t n_bytes, int dst) = 0;
virtual void recv(void* output, size_t n_bytes, int src) = 0;
// Block until every rank reaches this point.
virtual void barrier() = 0;
};
```
@@ -35,7 +35,8 @@ endfunction()
# Examples
build_example(minimal_env.cpp)
build_example(minimal_cfg.cpp)
build_example(minimal_barrier.cpp)
build_example(monte_carlo_pi.cpp)
build_example(file_broadcast.cpp)
# Benchmarks
build_example(allreduce_bench.cpp)
@@ -0,0 +1,360 @@
// Copyright © 2025 Apple Inc.
//
// File Broadcast with JACCL
//
// This example demonstrates distributed file transfer using JACCL's all_sum
// operation to broadcast a file from any rank to all other machines.
//
// The algorithm:
// 1. The sender rank reads the file into memory
// 2. All other ranks allocate zero-filled buffers of the same size
// 3. Use all_sum to broadcast: sender has data, others have zeros
// 4. After all_sum, all ranks have the file data
// 5. All ranks write the file to disk
//
// For large files, the transfer is chunked to manage memory efficiently.
//
// Usage:
// Set environment variables (see README.md), then run:
//
// ./jaccl_file_broadcast -f <file> [-s <sender_rank>] [-o <output_dir>]
//
// Or with mlx.launch:
//
// mlx.launch --hostfile hosts.json ./jaccl_file_broadcast -f myfile.bin
//
// Example output (4 ranks, sender rank 2):
// Rank 0 of 4: Received 10485760 bytes from rank 2 (982.5 MB/s)
// Rank 1 of 4: Received 10485760 bytes from rank 2 (985.2 MB/s)
// Rank 2 of 4: Sent 10485760 bytes (980.1 MB/s)
// Rank 3 of 4: Received 10485760 bytes from rank 2 (978.9 MB/s)
#include <jaccl/jaccl.h>
#include <jaccl/types.h>
#include <sys/stat.h>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
static void usage(const char* prog) {
std::cerr
<< "Usage: " << prog << " [options]\n"
<< " -f <file> File to broadcast (required)\n"
<< " -s <rank> Sender rank (default: 0)\n"
<< " -o <dir> Output directory (default: current dir)\n"
<< " -c <bytes> Chunk size in bytes (default: 67108864 = 64MB)\n"
<< " -v Verbose output\n"
<< " -h Show this help\n";
}
static bool file_exists(const std::string& path) {
struct stat buffer;
return (stat(path.c_str(), &buffer) == 0);
}
static std::int64_t file_size(const std::string& path) {
struct stat buffer;
if (stat(path.c_str(), &buffer) != 0) {
return -1;
}
return static_cast<std::int64_t>(buffer.st_size);
}
static bool create_directory(const std::string& path) {
if (path.empty() || path == ".") {
return true;
}
return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST;
}
static std::string basename(const std::string& path) {
size_t pos = path.find_last_of("/\\");
return (pos == std::string::npos) ? path : path.substr(pos + 1);
}
struct BroadcastStats {
std::int64_t total_bytes;
std::int64_t chunks_sent;
std::int64_t chunks_received;
double total_time_ms;
int sender_rank;
};
int main(int argc, char** argv) {
std::string input_file;
std::string output_dir = ".";
int sender_rank = 0;
std::int64_t chunk_size = 67108864;
bool verbose = false;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
usage(argv[0]);
return 0;
} else if (arg == "-f" && i + 1 < argc) {
input_file = argv[++i];
} else if (arg == "-s" && i + 1 < argc) {
sender_rank = std::atoi(argv[++i]);
} else if (arg == "-o" && i + 1 < argc) {
output_dir = argv[++i];
} else if (arg == "-c" && i + 1 < argc) {
chunk_size = std::atoll(argv[++i]);
} else if (arg == "-v" || arg == "--verbose") {
verbose = true;
} else {
std::cerr << "Unknown option: " << arg << "\n";
usage(argv[0]);
return 1;
}
}
if (input_file.empty()) {
std::cerr << "Error: Input file is required (-f <file>)\n";
usage(argv[0]);
return 1;
}
auto group = jaccl::init();
if (!group) {
std::cerr << "Failed to initialize JACCL" << std::endl;
return 1;
}
int rank = group->rank();
int nranks = group->size();
if (sender_rank < 0 || sender_rank >= nranks) {
std::cerr << "Error: Sender rank " << sender_rank << " is out of range [0, "
<< nranks << ")\n";
return 1;
}
std::int64_t total_file_size = 0;
if (rank == sender_rank) {
if (!file_exists(input_file)) {
std::cerr << "Error: File not found: " << input_file << "\n";
return 1;
}
total_file_size = file_size(input_file);
if (total_file_size < 0) {
std::cerr << "Error: Cannot read file size: " << input_file << "\n";
return 1;
}
}
group->all_sum(
&total_file_size, &total_file_size, sizeof(int64_t), jaccl::Int64);
if (!create_directory(output_dir)) {
std::cerr << "Error: Cannot create output directory: " << output_dir
<< "\n";
return 1;
}
std::string output_file = output_dir == "."
? basename(input_file)
: output_dir + "/" + basename(input_file);
if (verbose) {
std::printf(
"Rank %d of %d: Broadcasting '%s' (%ld bytes) from rank %d\n",
rank,
nranks,
input_file.c_str(),
static_cast<long>(total_file_size),
sender_rank);
}
auto t_start = std::chrono::high_resolution_clock::now();
std::int64_t num_chunks = (total_file_size + chunk_size - 1) / chunk_size;
if (num_chunks == 0) {
num_chunks = 1;
}
const int num_buffers = 4;
std::vector<std::vector<std::uint8_t>> buffers(
num_buffers, std::vector<std::uint8_t>(chunk_size, 0));
std::ifstream infile;
std::ofstream outfile;
if (rank == sender_rank) {
infile.open(input_file, std::ios::binary);
if (!infile.good()) {
std::cerr << "Error: Cannot open input file: " << input_file << "\n";
return 1;
}
}
outfile.open(output_file, std::ios::binary);
if (!outfile.good()) {
std::cerr << "Error: Cannot open output file: " << output_file << "\n";
return 1;
}
std::atomic<std::int64_t> next_read_chunk{0};
std::atomic<std::int64_t> next_comm_chunk{0};
std::atomic<std::int64_t> next_write_chunk{0};
std::atomic<bool> read_done{false};
std::atomic<bool> comm_done{false};
std::vector<std::atomic<bool>> buffer_ready(num_buffers);
std::vector<std::atomic<bool>> buffer_written(num_buffers);
for (int i = 0; i < num_buffers; i++) {
buffer_ready[i] = false;
buffer_written[i] = false;
}
std::vector<std::int64_t> chunk_sizes(num_chunks);
for (std::int64_t i = 0; i < num_chunks; i++) {
chunk_sizes[i] = std::min(chunk_size, total_file_size - i * chunk_size);
}
std::thread reader_thread;
if (rank == sender_rank) {
reader_thread = std::thread([&]() {
while (true) {
std::int64_t chunk_idx = next_read_chunk.fetch_add(1);
if (chunk_idx >= num_chunks) {
break;
}
std::int64_t offset = chunk_idx * chunk_size;
std::int64_t this_chunk_size = chunk_sizes[chunk_idx];
int buffer_idx = chunk_idx % num_buffers;
infile.seekg(offset, std::ios::beg);
infile.read(
reinterpret_cast<char*>(buffers[buffer_idx].data()),
this_chunk_size);
std::fill(
buffers[buffer_idx].begin() + this_chunk_size,
buffers[buffer_idx].end(),
0);
buffer_ready[buffer_idx] = true;
}
read_done = true;
});
} else {
read_done = true;
}
std::thread writer_thread([&]() {
while (true) {
std::int64_t chunk_idx = next_write_chunk.load();
if (chunk_idx >= num_chunks && comm_done) {
break;
}
if (chunk_idx >= num_chunks) {
std::this_thread::yield();
continue;
}
int buffer_idx = chunk_idx % num_buffers;
if (!buffer_written[buffer_idx]) {
std::this_thread::yield();
continue;
}
std::int64_t this_chunk_size = chunk_sizes[chunk_idx];
outfile.write(
reinterpret_cast<const char*>(buffers[buffer_idx].data()),
this_chunk_size);
buffer_written[buffer_idx] = false;
next_write_chunk.fetch_add(1);
}
});
for (std::int64_t chunk_idx = 0; chunk_idx < num_chunks; chunk_idx++) {
std::int64_t this_chunk_size = chunk_sizes[chunk_idx];
int buffer_idx = chunk_idx % num_buffers;
if (rank == sender_rank) {
while (!buffer_ready[buffer_idx] && !read_done) {
std::this_thread::yield();
}
}
std::fill(
buffers[buffer_idx].begin() + this_chunk_size,
buffers[buffer_idx].end(),
0);
group->all_sum(
buffers[buffer_idx].data(),
buffers[buffer_idx].data(),
this_chunk_size,
jaccl::UInt8);
buffer_written[buffer_idx] = true;
next_comm_chunk.fetch_add(1);
if (verbose) {
double progress = 100.0 * (chunk_idx + 1) / num_chunks;
std::printf(
"Rank %d: Progress %.1f%% (%ld/%ld chunks)\n",
rank,
progress,
static_cast<long>(chunk_idx + 1),
static_cast<long>(num_chunks));
}
}
comm_done = true;
if (reader_thread.joinable()) {
reader_thread.join();
}
writer_thread.join();
infile.close();
outfile.close();
auto t_end = std::chrono::high_resolution_clock::now();
double elapsed_ms =
std::chrono::duration<double, std::milli>(t_end - t_start).count();
double elapsed_sec = elapsed_ms / 1000.0;
double bandwidth_mbps = (total_file_size / (1024.0 * 1024.0)) / elapsed_sec;
if (rank == sender_rank) {
std::printf(
"Rank %d of %d: Sent %ld bytes from '%s' (%.1f MB/s)\n",
rank,
nranks,
static_cast<long>(total_file_size),
input_file.c_str(),
bandwidth_mbps);
} else {
std::printf(
"Rank %d of %d: Received %ld bytes from rank %d to '%s' (%.1f MB/s)\n",
rank,
nranks,
static_cast<long>(total_file_size),
sender_rank,
output_file.c_str(),
bandwidth_mbps);
}
if (verbose) {
std::printf(
"Rank %d: Total time: %.2f ms, Chunks: %ld, Chunk size: %ld bytes\n",
rank,
elapsed_ms,
static_cast<long>(num_chunks),
static_cast<long>(chunk_size));
}
return 0;
}
@@ -1,42 +0,0 @@
// Copyright © 2026 Apple Inc.
//
// Exercises Group::barrier(). Ranks arrive at the barrier at staggered times;
// after the barrier returns we do a small all_sum to confirm the group is
// healthy and that barrier() carried the correct fence semantics.
#include <chrono>
#include <iostream>
#include <thread>
#include <jaccl/jaccl.h>
int main() {
auto group = jaccl::init();
if (!group) {
std::cerr << "Failed to initialize JACCL" << std::endl;
return 1;
}
int rank = group->rank();
int size = group->size();
std::this_thread::sleep_for(std::chrono::milliseconds(100 * rank));
std::cout << "rank " << rank << " entering barrier" << std::endl;
group->barrier();
std::cout << "rank " << rank << " exited barrier" << std::endl;
int in = rank + 1;
int out = 0;
group->all_sum(&in, &out, sizeof(in), jaccl::Int32);
int expected = size * (size + 1) / 2;
if (out != expected) {
std::cerr << "rank " << rank << ": post-barrier all_sum mismatch (got "
<< out << ", expected " << expected << ")" << std::endl;
return 1;
}
std::cout << "rank " << rank << ": post-barrier all_sum OK (" << out << ")"
<< std::endl;
return 0;
}
@@ -0,0 +1,152 @@
// Copyright © 2025 Apple Inc.
//
// Monte Carlo Pi Estimation with JACCL
//
// This example demonstrates distributed Monte Carlo simulation using JACCL
// to estimate the value of π. Each rank generates random points independently
// and uses all-reduce to aggregate the results across all machines.
//
// The algorithm:
// 1. Each rank generates N random points in the unit square [0,1] x [0,1]
// 2. Count how many fall inside the quarter circle (x² + y² ≤ 1)
// 3. Use all_sum to aggregate hits and total points across all ranks
// 4. π ≈ 4 × (hits / total)
//
// Usage:
// Set environment variables (see README.md), then run:
//
// ./jaccl_monte_carlo_pi [-n <points_per_rank>]
//
// Or with mlx.launch:
//
// mlx.launch --hostfile hosts.json ./jaccl_monte_carlo_pi -n 10000000
//
// Example output (4 ranks, 10M points each):
// Rank 2 of 4
// Local: 7854321 hits out of 10000000 points
// Global: 31416789 hits out of 40000000 points
// Estimated π = 3.141679 (error: 0.000086)
#include <jaccl/jaccl.h>
#include <jaccl/types.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <random>
#include <string>
#include <vector>
static void usage(const char* prog) {
std::cerr << "Usage: " << prog << " [options]\n"
<< " -n <points> Points per rank (default: 1000000)\n"
<< " -s <seed> Random seed base (default: 42)\n"
<< " -h Show this help\n";
}
struct MonteCarloResult {
int64_t hits;
int64_t total;
};
MonteCarloResult estimate_pi_local(int64_t num_points, unsigned int seed) {
std::mt19937_64 rng(seed);
std::uniform_real_distribution<double> dist(0.0, 1.0);
int64_t hits = 0;
for (int64_t i = 0; i < num_points; i++) {
double x = dist(rng);
double y = dist(rng);
if (x * x + y * y <= 1.0) {
hits++;
}
}
return {hits, num_points};
}
int main(int argc, char** argv) {
int64_t points_per_rank = 1000000;
unsigned int seed_base = 42;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
usage(argv[0]);
return 0;
} else if (arg == "-n" && i + 1 < argc) {
points_per_rank = std::atoll(argv[++i]);
} else if (arg == "-s" && i + 1 < argc) {
seed_base = static_cast<unsigned int>(std::atoi(argv[++i]));
} else {
std::cerr << "Unknown option: " << arg << "\n";
usage(argv[0]);
return 1;
}
}
auto group = jaccl::init();
if (!group) {
std::cerr << "Failed to initialize JACCL" << std::endl;
return 1;
}
int rank = group->rank();
int nranks = group->size();
std::printf("Rank %d of %d\n", rank, nranks);
std::printf(
"Generating %ld random points (seed: %u)...\n",
static_cast<long>(points_per_rank),
seed_base + static_cast<unsigned int>(rank));
auto t0 = std::chrono::high_resolution_clock::now();
MonteCarloResult local = estimate_pi_local(
points_per_rank, seed_base + static_cast<unsigned int>(rank));
auto t1 = std::chrono::high_resolution_clock::now();
double local_time =
std::chrono::duration<double, std::milli>(t1 - t0).count();
std::printf(
"Rank %d: %ld hits out of %ld points (%.2f ms)\n",
rank,
static_cast<long>(local.hits),
static_cast<long>(local.total),
local_time);
MonteCarloResult global = {0, 0};
group->all_sum(&local.hits, &global.hits, sizeof(int64_t), jaccl::Int64);
group->all_sum(&local.total, &global.total, sizeof(int64_t), jaccl::Int64);
if (rank == 0) {
double pi_estimate = 4.0 * static_cast<double>(global.hits) /
static_cast<double>(global.total);
double error = std::abs(pi_estimate - M_PI);
std::printf("\n=== Results ===\n");
std::printf(
"Global: %ld hits out of %ld points\n",
static_cast<long>(global.hits),
static_cast<long>(global.total));
std::printf("Estimated π = %.10f\n", pi_estimate);
std::printf("True π = %.10f\n", M_PI);
std::printf("Error = %.10f (%.6f%%)\n", error, 100.0 * error / M_PI);
double total_time =
std::chrono::duration<double, std::milli>(t1 - t0).count();
std::printf("\nPerformance:\n");
std::printf("Total points: %ld\n", static_cast<long>(global.total));
std::printf("Time: %.2f ms\n", total_time);
std::printf(
"Points/sec: %.0f\n",
static_cast<double>(global.total) / (total_time / 1000.0));
}
return 0;
}
-1
View File
@@ -30,7 +30,6 @@ class Group {
virtual void send(const void* input, size_t n_bytes, int dst) = 0;
virtual void recv(void* output, size_t n_bytes, int src) = 0;
virtual void barrier() = 0;
};
/**
-5
View File
@@ -184,11 +184,6 @@ void MeshGroup::recv(void* output, size_t n_bytes, int src) {
mesh_.recv(static_cast<char*>(output), n_bytes, src);
}
void MeshGroup::barrier() {
uint8_t b = 0;
all_sum(&b, &b, sizeof(b), Dtype::UInt8);
}
template <typename T, typename ReduceOp>
void MeshGroup::all_reduce(
const void* input,
-2
View File
@@ -47,8 +47,6 @@ class MeshGroup : public Group {
void send(const void* input, size_t n_bytes, int dst) override;
void recv(void* output, size_t n_bytes, int src) override;
void barrier() override;
private:
template <typename T, typename ReduceOp>
void all_reduce(
-5
View File
@@ -190,11 +190,6 @@ void RingGroup::recv(void* output, size_t n_bytes, int src) {
ring_.recv(static_cast<char*>(output), n_bytes, src, n_conns_);
}
void RingGroup::barrier() {
uint8_t b = 0;
all_sum(&b, &b, sizeof(b), Dtype::UInt8);
}
template <typename T, typename ReduceOp>
void RingGroup::all_reduce(
const void* input,
-2
View File
@@ -48,8 +48,6 @@ class RingGroup : public Group {
void send(const void* input, size_t n_bytes, int dst) override;
void recv(void* output, size_t n_bytes, int src) override;
void barrier() override;
private:
template <typename T, typename ReduceOp>
void all_reduce(
-5
View File
@@ -17,11 +17,6 @@ if(MLX_BUILD_GGUF)
PRIVATE $<BUILD_INTERFACE:${gguflib_SOURCE_DIR}>)
add_library(gguflib STATIC ${gguflib_SOURCE_DIR}/fp16.c
${gguflib_SOURCE_DIR}/gguflib.c)
# gguflib uses assert() to reject malformed tensor headers (e.g. ndim > 8).
# Those checks are otherwise compiled out by -DNDEBUG in release builds, which
# leaves out-of-bounds reads/writes unguarded when loading untrusted GGUF
# files. Force NDEBUG off for this target so the asserts stay live.
target_compile_options(gguflib PRIVATE -UNDEBUG)
target_link_libraries(mlx PRIVATE $<BUILD_INTERFACE:gguflib>)
target_sources(mlx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/gguf.cpp
${CMAKE_CURRENT_SOURCE_DIR}/gguf_quants.cpp)
-3
View File
@@ -28,7 +28,6 @@ using json = nlohmann::json;
#define ST_U32 "U32"
#define ST_U64 "U64"
#define ST_F8_E4M3 "F8_E4M3"
#define ST_F8_E8M0 "F8_E8M0"
// Note: Complex numbers aren't in the spec yet so this could change -
// https://github.com/huggingface/safetensors/issues/389
@@ -98,8 +97,6 @@ Dtype dtype_from_safetensor_str(std::string_view str) {
return complex64;
} else if (str == ST_F8_E4M3) {
return uint8;
} else if (str == ST_F8_E8M0) {
return uint8;
} else {
std::ostringstream msg;
msg << "[safetensor] unsupported dtype" << str;
+1 -167
View File
@@ -705,170 +705,4 @@ array solve_triangular(
return matmul(a_inv, b, s);
}
void validate_det(
const array& a,
const StreamOrDevice& stream,
const std::string& fname) {
check_cpu_stream(stream, fname);
if (issubdtype(a.dtype(), complexfloating)) {
throw std::invalid_argument(fname + " Complex inputs are not supported.");
}
if (a.ndim() < 2) {
std::ostringstream msg;
msg << fname
<< " Arrays must have >= 2 dimensions. Received array "
"with "
<< a.ndim() << " dimensions.";
throw std::invalid_argument(msg.str());
}
if (a.shape(-1) != a.shape(-2)) {
throw std::invalid_argument(fname + " Only defined for square matrices.");
}
}
array det_raw_small(const array& a, StreamOrDevice s) {
int n = a.shape(-1);
// Empty 0x0 matrix: determinant is the empty product = 1
if (n == 0) {
Shape out_shape(a.shape().begin(), a.shape().end() - 2);
return broadcast_to(array(1.0f, a.dtype()), std::move(out_shape), s);
}
// Helper to extract a[..., i, j] from the last two dims
auto elem = [&](int i, int j) {
auto starts = Shape(a.ndim(), 0);
auto stops = a.shape();
starts[a.ndim() - 2] = i;
stops[a.ndim() - 2] = i + 1;
starts[a.ndim() - 1] = j;
stops[a.ndim() - 1] = j + 1;
return squeeze(squeeze(slice(a, starts, stops, s), -1, s), -1, s);
};
if (n == 1) {
return elem(0, 0);
} else if (n == 2) {
return subtract(
multiply(elem(0, 0), elem(1, 1), s),
multiply(elem(0, 1), elem(1, 0), s),
s);
} else {
// 3x3: a00*(a11*a22 - a12*a21) - a01*(a10*a22 - a12*a20) + a02*(a10*a21 -
// a11*a20)
auto a00 = elem(0, 0), a01 = elem(0, 1), a02 = elem(0, 2);
auto a10 = elem(1, 0), a11 = elem(1, 1), a12 = elem(1, 2);
auto a20 = elem(2, 0), a21 = elem(2, 1), a22 = elem(2, 2);
return add(
subtract(
multiply(
a00,
subtract(multiply(a11, a22, s), multiply(a12, a21, s), s),
s),
multiply(
a01,
subtract(multiply(a10, a22, s), multiply(a12, a20, s), s),
s),
s),
multiply(
a02, subtract(multiply(a10, a21, s), multiply(a11, a20, s), s), s),
s);
}
}
std::pair<array, array> slogdet_impl(const array& input, StreamOrDevice s) {
int n = input.shape(-1);
auto dtype = input.dtype();
// Small-matrix fast path
if (n <= 3) {
auto raw = det_raw_small(input, s);
auto abs_raw = abs(raw, s);
auto sgn = sign(raw, s);
auto logabs = log(abs_raw, s);
return std::make_pair(sgn, logabs);
}
// General LU-based path
auto [LU, pivots] = lu_factor(input, s);
// Extract diagonal of U
auto diag = diagonal(LU, 0, -2, -1, s);
// Permutation parity: count positions where pivot[i] != i
int k = std::min(input.shape(-2), input.shape(-1));
auto iota = arange(0, k, uint32, s);
auto parity = astype(
sum(not_equal(pivots, iota, s),
/* axis = */ -1,
/* keepdims = */ false,
s),
int32,
s);
// Count negative diagonal elements
auto num_neg = astype(
sum(less(diag, array(0.0f, dtype), s),
/* axis = */ -1,
/* keepdims = */ false,
s),
int32,
s);
// sign = (-1)^(parity + num_neg)
auto total = add(parity, num_neg, s);
auto sign_val = astype(
subtract(
array(1, int32),
multiply(array(2, int32), remainder(total, array(2, int32), s), s),
s),
dtype,
s);
// logabsdet = sum(log(abs(diag)))
auto logabsdet =
sum(log(abs(diag, s), s), /* axis = */ -1, /* keepdims = */ false, s);
// Handle singular matrices: any zero on diagonal
auto is_zero =
any(equal(diag, array(0.0f, dtype), s),
/* axis = */ -1,
/* keepdims = */ false,
s);
sign_val = where(is_zero, array(0.0f, dtype), sign_val, s);
logabsdet = where(
is_zero,
array(-std::numeric_limits<float>::infinity(), dtype),
logabsdet,
s);
return std::make_pair(sign_val, logabsdet);
}
std::pair<array, array> slogdet(const array& a, StreamOrDevice s /* = {} */) {
validate_det(a, s, "[linalg::slogdet]");
auto dtype = at_least_float(a.dtype());
auto input = astype(a, dtype, s);
return slogdet_impl(input, s);
}
array det(const array& a, StreamOrDevice s /* = {} */) {
validate_det(a, s, "[linalg::det]");
auto dtype = at_least_float(a.dtype());
auto input = astype(a, dtype, s);
int n = input.shape(-1);
// Small-matrix fast path: compute directly, skip log/exp round-trip
if (n <= 3) {
return det_raw_small(input, s);
}
// General case: det = sign * exp(logabsdet)
auto [sign_val, logabsdet] = slogdet_impl(input, s);
return multiply(sign_val, exp(logabsdet, s), s);
}
} // namespace mlx::core::linalg
} // namespace mlx::core::linalg
-4
View File
@@ -112,8 +112,4 @@ eigvalsh(const array& a, std::string UPLO = "L", StreamOrDevice s = {});
MLX_API std::pair<array, array>
eigh(const array& a, std::string UPLO = "L", StreamOrDevice s = {});
MLX_API array det(const array& a, StreamOrDevice s = {});
MLX_API std::pair<array, array> slogdet(const array& a, StreamOrDevice s = {});
} // namespace mlx::core::linalg
+2 -2
View File
@@ -5,8 +5,8 @@
#include "mlx/api.h"
#define MLX_VERSION_MAJOR 0
#define MLX_VERSION_MINOR 32
#define MLX_VERSION_PATCH 0
#define MLX_VERSION_MINOR 31
#define MLX_VERSION_PATCH 2
#define MLX_VERSION_NUMERIC \
(100000 * MLX_VERSION_MAJOR + 1000 * MLX_VERSION_MINOR + MLX_VERSION_PATCH)
+3 -12
View File
@@ -1,8 +1,5 @@
// Copyright © 2024 Apple Inc.
#include <limits>
#include <sstream>
#include <nanobind/stl/complex.h>
#include "python/src/convert.h"
@@ -18,15 +15,9 @@ enum PyScalarT {
};
int check_shape_dim(int64_t dim) {
if (dim > std::numeric_limits<int>::max() ||
dim < std::numeric_limits<int>::min()) {
std::ostringstream msg;
msg << "Shape dimension " << dim << " is outside the supported range ["
<< std::numeric_limits<int>::min() << ", "
<< std::numeric_limits<int>::max()
<< "]. MLX currently uses 32-bit integers for shape dimensions.";
PyErr_SetString(PyExc_OverflowError, msg.str().c_str());
nb::detail::raise_python_error();
if (dim > std::numeric_limits<int>::max()) {
throw std::invalid_argument(
"Shape dimension falls outside supported `int` range.");
}
return static_cast<int>(dim);
}
-4
View File
@@ -76,7 +76,3 @@ nb::object tolist(mx::array& a);
mx::array create_array(nb::object v, std::optional<mx::Dtype> t);
mx::array array_from_list(nb::list pl, std::optional<mx::Dtype> dtype);
mx::array array_from_list(nb::tuple pl, std::optional<mx::Dtype> dtype);
// Narrow a Python-side shape dimension (int64) to a C++ mx::ShapeElem (int32),
// raising a clear error if the value would overflow.
int check_shape_dim(int64_t dim);
-73
View File
@@ -660,77 +660,4 @@ void init_linalg(nb::module_& parent_module) {
Returns:
array: The unique solution to the system ``AX = B``.
)pbdoc");
m.def(
"det",
&mx::linalg::det,
"a"_a,
nb::kw_only(),
"stream"_a = nb::none(),
nb::sig(
"def det(a: array, *, stream: Union[None, Stream, Device] = None) -> array"),
R"pbdoc(
Compute the determinant of a square matrix.
This function supports arrays with at least 2 dimensions. When the
input has more than two dimensions, the determinant is computed for
each matrix in the last two dimensions.
Args:
a (array): Input array.
stream (Stream, optional): Stream or device. Defaults to ``None``
in which case the default stream of the default device is used.
Returns:
array: The determinant(s) of the input matrix (matrices).
Example:
>>> A = mx.array([[1., 2.], [3., 4.]])
>>> mx.linalg.det(A, stream=mx.cpu)
array(-2, dtype=float32)
)pbdoc");
m.def(
"slogdet",
[](const mx::array& a, mx::StreamOrDevice s) {
auto result = mx::linalg::slogdet(a, s);
return nb::make_tuple(result.first, result.second);
},
"a"_a,
nb::kw_only(),
"stream"_a = nb::none(),
nb::sig(
"def slogdet(a: array, *, stream: Union[None, Stream, Device] = None) -> Tuple[array, array]"),
R"pbdoc(
Compute the sign and natural log of the absolute value of the
determinant of a square matrix.
This function supports arrays with at least 2 dimensions. When the
input has more than two dimensions, the sign and log-absolute-determinant
are computed for each matrix in the last two dimensions.
For a singular matrix, ``sign`` is 0 and ``logabsdet`` is ``-inf``.
The determinant can be reconstructed as ``det = sign * exp(logabsdet)``.
This is more numerically stable than computing the determinant directly
for matrices with large or small determinants.
Args:
a (array): Input array.
stream (Stream, optional): Stream or device. Defaults to ``None``
in which case the default stream of the default device is used.
Returns:
tuple(array, array): The ``sign`` and ``logabsdet`` of the
determinant. ``sign`` is -1, 0, or +1. ``logabsdet`` is the
natural log of the absolute value of the determinant.
Example:
>>> A = mx.array([[1., 2.], [3., 4.]])
>>> sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
>>> sign
array(-1, dtype=float32)
>>> logabsdet
array(0.693147, dtype=float32)
)pbdoc");
}
+18 -14
View File
@@ -15,7 +15,6 @@
#include "mlx/einsum.h"
#include "mlx/ops.h"
#include "mlx/utils.h"
#include "python/src/convert.h"
#include "python/src/load.h"
#include "python/src/small_vector.h"
#include "python/src/utils.h"
@@ -46,13 +45,6 @@ double scalar_to_double(Scalar s) {
}
}
mx::Shape to_shape(const nb::object& shape) {
if (nb::isinstance<nb::int_>(shape)) {
return {check_shape_dim(nb::cast<int64_t>(shape))};
}
return nb::cast<mx::Shape>(shape);
}
void init_ops(nb::module_& m) {
m.def(
"reshape",
@@ -1710,11 +1702,15 @@ void init_ops(nb::module_& m) {
)pbdoc");
m.def(
"full",
[](const nb::object& shape,
[](const std::variant<int, mx::Shape>& shape,
const ScalarOrArray& vals,
std::optional<mx::Dtype> dtype,
mx::StreamOrDevice s) {
return mx::full(to_shape(shape), to_array(vals, dtype), s);
if (auto pv = std::get_if<int>(&shape); pv) {
return mx::full({*pv}, to_array(vals, dtype), s);
} else {
return mx::full(std::get<mx::Shape>(shape), to_array(vals, dtype), s);
}
},
"shape"_a,
"vals"_a,
@@ -1740,11 +1736,15 @@ void init_ops(nb::module_& m) {
)pbdoc");
m.def(
"zeros",
[](const nb::object& shape,
[](const std::variant<int, mx::Shape>& shape,
std::optional<mx::Dtype> dtype,
mx::StreamOrDevice s) {
auto t = dtype.value_or(mx::float32);
return mx::zeros(to_shape(shape), t, s);
if (auto pv = std::get_if<int>(&shape); pv) {
return mx::zeros({*pv}, t, s);
} else {
return mx::zeros(std::get<mx::Shape>(shape), t, s);
}
},
"shape"_a,
"dtype"_a.none() = mx::float32,
@@ -1802,11 +1802,15 @@ void init_ops(nb::module_& m) {
)pbdoc");
m.def(
"ones",
[](const nb::object& shape,
[](const std::variant<int, mx::Shape>& shape,
std::optional<mx::Dtype> dtype,
mx::StreamOrDevice s) {
auto t = dtype.value_or(mx::float32);
return mx::ones(to_shape(shape), t, s);
if (auto pv = std::get_if<int>(&shape); pv) {
return mx::ones({*pv}, t, s);
} else {
return mx::ones(std::get<mx::Shape>(shape), t, s);
}
},
"shape"_a,
"dtype"_a.none() = mx::float32,
+8 -41
View File
@@ -2,11 +2,6 @@
#pragma once
#include <cstdint>
#include <limits>
#include <sstream>
#include <type_traits>
#include "mlx/small_vector.h"
#include <nanobind/stl/detail/nb_list.h>
@@ -19,19 +14,11 @@ struct type_caster<mlx::core::SmallVector<Type, Size, Alloc>> {
using List = mlx::core::SmallVector<Type, Size, Alloc>;
using Caster = make_caster<Type>;
// For narrow integer element types we fetch each element through a wider
// integer caster so we can emit a clean OverflowError on overflow instead of
// nanobind's generic "incompatible function arguments" TypeError.
static constexpr bool kNarrowInt = std::is_integral_v<Type> &&
!std::is_same_v<Type, bool> && (sizeof(Type) < sizeof(int64_t));
NB_TYPE_CASTER(
List,
const_name("tuple[") + make_caster<Type>::Name + const_name(", ...]"))
// Not noexcept: on overflow of a narrow integer element we raise
// OverflowError so nanobind surfaces a clean error to the user.
bool from_python(handle src, uint8_t flags, cleanup_list* cleanup) {
bool from_python(handle src, uint8_t flags, cleanup_list* cleanup) noexcept {
size_t size;
PyObject* temp;
@@ -42,39 +29,19 @@ struct type_caster<mlx::core::SmallVector<Type, Size, Alloc>> {
value.clear();
value.reserve(size);
Caster caster;
bool success = o != nullptr;
flags = flags_for_local_caster<Type>(flags);
for (size_t i = 0; i < size; ++i) {
if constexpr (kNarrowInt) {
make_caster<int64_t> wide;
if (!wide.from_python(o[i], flags, cleanup) ||
!wide.template can_cast<int64_t>()) {
success = false;
break;
}
int64_t v = wide.operator cast_t<int64_t>();
if (v > std::numeric_limits<Type>::max() ||
v < std::numeric_limits<Type>::min()) {
std::ostringstream msg;
msg << "Integer value " << v << " is outside the supported range ["
<< static_cast<int64_t>(std::numeric_limits<Type>::min()) << ", "
<< static_cast<int64_t>(std::numeric_limits<Type>::max()) << "].";
Py_XDECREF(temp);
PyErr_SetString(PyExc_OverflowError, msg.str().c_str());
raise_python_error();
}
value.push_back(static_cast<Type>(v));
} else {
Caster caster;
if (!caster.from_python(o[i], flags, cleanup) ||
!caster.template can_cast<Type>()) {
success = false;
break;
}
value.push_back(caster.operator cast_t<Type>());
if (!caster.from_python(o[i], flags, cleanup) ||
!caster.template can_cast<Type>()) {
success = false;
break;
}
value.push_back(caster.operator cast_t<Type>());
}
Py_XDECREF(temp);
+17
View File
@@ -1,5 +1,22 @@
cuda_skip = {
# Lapack ops NYI
"TestLinalg.test_cholesky",
"TestLinalg.test_cholesky_inv",
"TestLinalg.test_eig",
"TestLinalg.test_eigh",
"TestLinalg.test_inverse",
"TestVmap.test_vmap_inverse",
"TestLinalg.test_lu",
"TestLinalg.test_lu_factor",
"TestLinalg.test_pseudo_inverse",
"TestLinalg.test_qr_factorization",
"TestInit.test_orthogonal",
"TestLinalg.test_svd_decomposition",
"TestVmap.test_vmap_svd",
"TestLinalg.test_tri_inverse",
# Quantization NYI
"TestQuantized.test_gather_matmul_grad",
"TestQuantized.test_gather_qmm",
"TestQuantized.test_gather_qmm_sorted",
"TestQuantized.test_gather_qmm_grad",
}
+2 -5
View File
@@ -767,13 +767,10 @@ class TestArray(mlx_tests.MLXTestCase):
def test_array_np_shape_dim_check(self):
a_npy = np.empty(2**31, dtype=np.bool_)
with self.assertRaises(OverflowError) as e:
with self.assertRaises(ValueError) as e:
mx.array(a_npy)
self.assertEqual(
str(e.exception),
"Shape dimension 2147483648 is outside the supported range "
"[-2147483648, 2147483647]. MLX currently uses 32-bit integers "
"for shape dimensions.",
str(e.exception), "Shape dimension falls outside supported `int` range."
)
def test_dtype_promotion(self):
-255
View File
@@ -520,19 +520,6 @@ class TestLinalg(mlx_tests.MLXTestCase):
P, L, U = mx.linalg.lu(a, stream=mx.cpu)
self.assertTrue(mx.allclose(L[P, :] @ U, a))
# Test singular matrix (should not throw)
a = mx.array(
[
[1.0, 2.0, 3.0, 4.0],
[2.0, 4.0, 6.0, 8.0],
[0.0, 1.0, 1.0, 0.0],
[1.0, 0.0, 0.0, 1.0],
]
)
P, L, U = mx.linalg.lu(a, stream=mx.cpu)
L_permuted = mx.take_along_axis(L, P[..., None], axis=-2)
self.assertTrue(mx.allclose(L_permuted @ U, a))
def test_lu_factor(self):
mx.random.seed(7)
@@ -629,248 +616,6 @@ class TestLinalg(mlx_tests.MLXTestCase):
expected = np.linalg.solve(a, b)
self.assertTrue(np.allclose(result, expected))
def test_det(self):
# 1x1 fast path
A = mx.array([[5.0]])
self.assertTrue(np.allclose(mx.linalg.det(A, stream=mx.cpu), 5.0))
# 2x2 fast path
A = mx.array([[1.0, 2.0], [3.0, 4.0]])
d = mx.linalg.det(A, stream=mx.cpu)
self.assertTrue(np.allclose(d, -2.0))
# 3x3 fast path
A = mx.array([[1.0, 2.0, 3.0], [0.0, 1.0, 4.0], [5.0, 6.0, 0.0]])
d = mx.linalg.det(A, stream=mx.cpu)
expected = np.linalg.det(np.array(A))
self.assertTrue(np.allclose(d, expected, atol=1e-5))
# 4x4 LU path: compare with numpy
np.random.seed(42)
A_np = np.random.randn(4, 4).astype(np.float32)
A_mx = mx.array(A_np)
d_mx = mx.linalg.det(A_mx, stream=mx.cpu)
d_np = np.linalg.det(A_np)
self.assertTrue(np.allclose(d_mx, d_np, atol=1e-4))
# 5x5 LU path
A_np = np.random.randn(5, 5).astype(np.float32)
A_mx = mx.array(A_np)
d_mx = mx.linalg.det(A_mx, stream=mx.cpu)
d_np = np.linalg.det(A_np)
self.assertTrue(np.allclose(d_mx, d_np, atol=1e-4))
# Identity matrix
A = mx.eye(5)
self.assertTrue(np.allclose(mx.linalg.det(A, stream=mx.cpu), 1.0))
# Batched: (3, 4, 4)
A_np = np.random.randn(3, 4, 4).astype(np.float32)
A_mx = mx.array(A_np)
d_mx = mx.linalg.det(A_mx, stream=mx.cpu)
d_np = np.linalg.det(A_np)
self.assertTrue(np.allclose(d_mx, d_np, atol=1e-4))
# Multi-batch: (2, 3, 3, 3)
A_np = np.random.randn(2, 3, 3, 3).astype(np.float32)
A_mx = mx.array(A_np)
d_mx = mx.linalg.det(A_mx, stream=mx.cpu)
d_np = np.linalg.det(A_np)
self.assertTrue(np.allclose(d_mx, d_np, atol=1e-4))
# Integer input auto-promotes to float
A = mx.array([[1, 2], [3, 4]])
d = mx.linalg.det(A, stream=mx.cpu)
self.assertTrue(np.allclose(d, -2.0))
# float64
A_np = np.random.randn(4, 4).astype(np.float64)
A_mx = mx.array(A_np)
d_mx = mx.linalg.det(A_mx, stream=mx.cpu)
d_np = np.linalg.det(A_np)
self.assertTrue(np.allclose(d_mx, d_np, atol=1e-10))
# Singular 4x4 matrix (LU path): det should be 0
A = mx.array(
[
[1.0, 2.0, 3.0, 4.0],
[2.0, 4.0, 6.0, 8.0],
[0.0, 1.0, 1.0, 0.0],
[1.0, 0.0, 0.0, 1.0],
]
)
d = mx.linalg.det(A, stream=mx.cpu)
self.assertTrue(np.allclose(d, 0.0, atol=1e-5))
# Singular 5x5 matrix (LU path)
A_np = np.ones((5, 5), dtype=np.float32)
A_mx = mx.array(A_np)
d = mx.linalg.det(A_mx, stream=mx.cpu)
self.assertTrue(np.allclose(d, 0.0, atol=1e-5))
# Batched singular matrices (LU path)
A_np = np.array([np.diag([1.0, 2.0, 0.0, 3.0]), np.eye(4, dtype=np.float32)])
A_mx = mx.array(A_np)
d_mx = mx.linalg.det(A_mx, stream=mx.cpu)
d_np = np.linalg.det(A_np)
self.assertTrue(np.allclose(d_mx, d_np, atol=1e-5))
# Empty 0x0 matrix: det is the empty product = 1
d = mx.linalg.det(mx.zeros((0, 0)), stream=mx.cpu)
self.assertEqual(d.shape, ())
self.assertEqual(float(d), 1.0)
# Batched empty matrices: shape preserves batch dims
d = mx.linalg.det(mx.zeros((3, 0, 0)), stream=mx.cpu)
self.assertTrue(np.allclose(d, np.linalg.det(np.zeros((3, 0, 0)))))
# Error: non-square
with self.assertRaises(ValueError):
mx.linalg.det(mx.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), stream=mx.cpu)
# Error: 1D
with self.assertRaises(ValueError):
mx.linalg.det(mx.array([1.0, 2.0]), stream=mx.cpu)
# Error: complex unsupported (small-matrix path)
with self.assertRaises(ValueError):
mx.linalg.det(mx.array([[1.0 + 1j, 2.0], [3.0, 4.0]]), stream=mx.cpu)
# Error: complex unsupported (LU path)
with self.assertRaises(ValueError):
mx.linalg.det(mx.eye(4).astype(mx.complex64), stream=mx.cpu)
def test_slogdet(self):
# 2x2: det = -2 => sign = -1, logabsdet = log(2)
A = mx.array([[1.0, 2.0], [3.0, 4.0]])
sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
self.assertTrue(np.allclose(sign, -1.0))
self.assertTrue(np.allclose(logabsdet, np.log(2.0), atol=1e-5))
# Identity: sign = 1, logabsdet = 0
A = mx.eye(4)
sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
self.assertTrue(np.allclose(sign, 1.0))
self.assertTrue(np.allclose(logabsdet, 0.0, atol=1e-6))
# Compare with numpy for random matrices
np.random.seed(42)
for n in [1, 2, 3, 4, 5]:
A_np = np.random.randn(n, n).astype(np.float32)
A_mx = mx.array(A_np)
sign_mx, logabs_mx = mx.linalg.slogdet(A_mx, stream=mx.cpu)
sign_np, logabs_np = np.linalg.slogdet(A_np)
with self.subTest(n=n):
self.assertTrue(np.allclose(sign_mx, sign_np, atol=1e-5))
self.assertTrue(np.allclose(logabs_mx, logabs_np, atol=1e-4))
# Singular matrix 2x2 (fast path): sign = 0, logabsdet = -inf
A = mx.array([[1.0, 2.0], [2.0, 4.0]])
sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
self.assertEqual(float(sign), 0.0)
self.assertEqual(float(logabsdet), float("-inf"))
# Singular 4x4 matrix (LU path): sign = 0, logabsdet = -inf
A = mx.array(
[
[1.0, 2.0, 3.0, 4.0],
[2.0, 4.0, 6.0, 8.0],
[0.0, 1.0, 1.0, 0.0],
[1.0, 0.0, 0.0, 1.0],
]
)
sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
self.assertEqual(float(sign), 0.0)
self.assertEqual(float(logabsdet), float("-inf"))
# Singular 5x5 matrix (LU path): all-ones matrix
A = mx.array(np.ones((5, 5), dtype=np.float32))
sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
self.assertEqual(float(sign), 0.0)
self.assertEqual(float(logabsdet), float("-inf"))
# Batched with mix of singular and non-singular (LU path)
A_np = np.array([np.diag([1.0, 2.0, 0.0, 3.0]), np.eye(4, dtype=np.float32)])
A_mx = mx.array(A_np)
sign_mx, logabs_mx = mx.linalg.slogdet(A_mx, stream=mx.cpu)
sign_np, logabs_np = np.linalg.slogdet(A_np)
self.assertTrue(np.allclose(sign_mx, sign_np, atol=1e-5))
# Check -inf for singular, 0.0 for identity
self.assertEqual(float(logabs_mx[0]), float("-inf"))
self.assertTrue(np.allclose(logabs_mx[1], 0.0, atol=1e-6))
# Batched
A_np = np.random.randn(3, 4, 4).astype(np.float32)
A_mx = mx.array(A_np)
sign_mx, logabs_mx = mx.linalg.slogdet(A_mx, stream=mx.cpu)
sign_np, logabs_np = np.linalg.slogdet(A_np)
self.assertTrue(np.allclose(sign_mx, sign_np, atol=1e-5))
self.assertTrue(np.allclose(logabs_mx, logabs_np, atol=1e-4))
# Multi-batch
A_np = np.random.randn(2, 3, 3, 3).astype(np.float32)
A_mx = mx.array(A_np)
sign_mx, logabs_mx = mx.linalg.slogdet(A_mx, stream=mx.cpu)
sign_np, logabs_np = np.linalg.slogdet(A_np)
self.assertTrue(np.allclose(sign_mx, sign_np, atol=1e-5))
self.assertTrue(np.allclose(logabs_mx, logabs_np, atol=1e-4))
# Numerical stability: large matrix where det overflows
# 0.1 * I_100 has det = 0.1^100 which underflows in float32
# but slogdet should give sign=1, logabsdet = 100*log(0.1)
n = 100
A = mx.array(0.1) * mx.eye(n)
sign, logabsdet = mx.linalg.slogdet(A, stream=mx.cpu)
self.assertTrue(np.allclose(sign, 1.0))
self.assertTrue(np.allclose(logabsdet, n * np.log(0.1), atol=1e-3))
# Verify det = sign * exp(logabsdet) for non-singular cases
A_np = np.random.randn(5, 5).astype(np.float32)
A_mx = mx.array(A_np)
sign_mx, logabs_mx = mx.linalg.slogdet(A_mx, stream=mx.cpu)
det_mx = mx.linalg.det(A_mx, stream=mx.cpu)
reconstructed = float(sign_mx) * np.exp(float(logabs_mx))
self.assertTrue(np.allclose(float(det_mx), reconstructed, rtol=1e-4))
# float64
A_np = np.random.randn(4, 4).astype(np.float64)
A_mx = mx.array(A_np)
sign_mx, logabs_mx = mx.linalg.slogdet(A_mx, stream=mx.cpu)
sign_np, logabs_np = np.linalg.slogdet(A_np)
self.assertTrue(np.allclose(sign_mx, sign_np))
self.assertTrue(np.allclose(logabs_mx, logabs_np, atol=1e-10))
# Empty 0x0 matrix: sign = 1, logabsdet = 0 (empty product)
sign, logabsdet = mx.linalg.slogdet(mx.zeros((0, 0)), stream=mx.cpu)
self.assertEqual(sign.shape, ())
self.assertEqual(logabsdet.shape, ())
self.assertEqual(float(sign), 1.0)
self.assertEqual(float(logabsdet), 0.0)
# Batched empty matrices
sign, logabsdet = mx.linalg.slogdet(mx.zeros((3, 0, 0)), stream=mx.cpu)
sign_np, logabs_np = np.linalg.slogdet(np.zeros((3, 0, 0)))
self.assertTrue(np.allclose(sign, sign_np))
self.assertTrue(np.allclose(logabsdet, logabs_np))
# Error: non-square
with self.assertRaises(ValueError):
mx.linalg.slogdet(
mx.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), stream=mx.cpu
)
# Error: 1D
with self.assertRaises(ValueError):
mx.linalg.slogdet(mx.array([1.0, 2.0]), stream=mx.cpu)
# Error: complex unsupported (small-matrix path)
with self.assertRaises(ValueError):
mx.linalg.slogdet(mx.array([[1.0 + 1j, 2.0], [3.0, 4.0]]), stream=mx.cpu)
# Error: complex unsupported (LU path)
with self.assertRaises(ValueError):
mx.linalg.slogdet(mx.eye(4).astype(mx.complex64), stream=mx.cpu)
if __name__ == "__main__":
mlx_tests.MLXTestRunner()
-46
View File
@@ -92,52 +92,6 @@ class TestOps(mlx_tests.MLXTestCase):
self.assertEqual(y.dtype, t)
self.assertTrue(mx.array_equal(y, x))
def test_shape_overflow_error(self):
# Shape dimensions that don't fit in int32 should raise a clear
# OverflowError that names the offending value, rather than a generic
# "incompatible function arguments" TypeError. The overflow check
# lives in the mx::Shape type caster, so it applies to every op that
# takes a shape. See issue #2681.
too_big = 2**31
# Array creation ops — also exercise the scalar shape path.
for ctor in (mx.zeros, mx.ones):
with self.assertRaises(OverflowError) as cm:
ctor(too_big)
self.assertIn(str(too_big), str(cm.exception))
with self.assertRaises(OverflowError) as cm:
ctor([too_big])
self.assertIn(str(too_big), str(cm.exception))
with self.assertRaises(OverflowError) as cm:
mx.full(too_big, 0.0)
self.assertIn(str(too_big), str(cm.exception))
with self.assertRaises(OverflowError) as cm:
mx.full([too_big], 0.0)
self.assertIn(str(too_big), str(cm.exception))
# Other shape-taking ops should surface the same clean error.
a = mx.zeros(4)
with self.assertRaises(OverflowError) as cm:
mx.reshape(a, [too_big])
self.assertIn(str(too_big), str(cm.exception))
with self.assertRaises(OverflowError) as cm:
mx.broadcast_to(a, [too_big, 1])
self.assertIn(str(too_big), str(cm.exception))
# Negative overflow (< int32 min) is caught too.
too_negative = -(2**31) - 1
with self.assertRaises(OverflowError) as cm:
mx.zeros([too_negative])
self.assertIn(str(too_negative), str(cm.exception))
# Shapes that fit in int32 still go through unchanged.
self.assertEqual(mx.zeros(4).shape, (4,))
self.assertEqual(mx.zeros((2, 3)).shape, (2, 3))
self.assertEqual(mx.ones([2, 3]).shape, (2, 3))
self.assertEqual(mx.full((2, 3), 1.5).tolist(), [[1.5] * 3] * 2)
def test_scalar_inputs(self):
# Check combinations of python types
a = mx.add(False, True)
+1 -1
View File
@@ -1046,7 +1046,7 @@ class TestQuantized(mlx_tests.MLXTestCase):
y3 = scatter_unsort(y3, inv_order, indices.shape)
y4 = scatter_unsort(y4, inv_order, indices.shape)
tol = 1.5e-5 if (dtype == mx.float32) else 1e-3
tol = 1.5e-5 if (dtype == mx.float32) else 2.5e-4
self.assertLess((y1 - y2).abs().max(), tol)
self.assertLess((y1 - y3).abs().max(), tol)
+1 -1
View File
@@ -234,7 +234,7 @@ if __name__ == "__main__":
"ml_dtypes",
"numpy>=2",
"pre-commit",
"psutil>=7.2",
"psutil",
"torch>=2.9",
"typing_extensions",
],
-65
View File
@@ -637,68 +637,3 @@ TEST_CASE("test solve_triangluar") {
expected = array({-3., 2., 3.});
CHECK(allclose(expected, result).item<bool>());
}
TEST_CASE("test det") {
// 1x1 fast path
{
array a = array({5.0f}, {1, 1});
auto d = det(a, Device::cpu);
CHECK_EQ(d.item<float>(), doctest::Approx(5.0f));
}
// 2x2 fast path: det([[1,2],[3,4]]) = -2
{
array a = array({1.0f, 2.0f, 3.0f, 4.0f}, {2, 2});
auto d = det(a, Device::cpu);
CHECK_EQ(d.item<float>(), doctest::Approx(-2.0f));
}
// 3x3 fast path: det([[1,2,3],[0,1,4],[5,6,0]]) = 1
{
array a =
array({1.0f, 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, 6.0f, 0.0f}, {3, 3});
auto d = det(a, Device::cpu);
CHECK_EQ(d.item<float>(), doctest::Approx(1.0f));
}
// 4x4 LU path: identity matrix det = 1
{
array a = eye(4);
auto d = det(a, Device::cpu);
CHECK_EQ(d.item<float>(), doctest::Approx(1.0f));
}
// Non-square should throw
CHECK_THROWS(
det(array({1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}, {2, 3}), Device::cpu));
// 1D should throw
CHECK_THROWS(det(array({1.0f, 2.0f}), Device::cpu));
}
TEST_CASE("test slogdet") {
// 2x2: det = -2, so sign = -1, logabsdet = log(2)
{
array a = array({1.0f, 2.0f, 3.0f, 4.0f}, {2, 2});
auto [s, logabs] = slogdet(a, Device::cpu);
CHECK_EQ(s.item<float>(), doctest::Approx(-1.0f));
CHECK_EQ(logabs.item<float>(), doctest::Approx(std::log(2.0f)));
}
// Identity: sign = 1, logabsdet = 0
{
array a = eye(4);
auto [s, logabs] = slogdet(a, Device::cpu);
CHECK_EQ(s.item<float>(), doctest::Approx(1.0f));
CHECK_EQ(logabs.item<float>(), doctest::Approx(0.0f));
}
// Singular: sign = 0, logabsdet = -inf
{
array a = array({1.0f, 2.0f, 2.0f, 4.0f}, {2, 2});
auto [s, logabs] = slogdet(a, Device::cpu);
CHECK_EQ(s.item<float>(), 0.0f);
CHECK(std::isinf(logabs.item<float>()));
CHECK(logabs.item<float>() < 0);
}
}