Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ae36f2c08 |
@@ -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 }} \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -146,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,12 +13,7 @@ bool is_available() {
|
||||
}
|
||||
|
||||
int device_count() {
|
||||
try {
|
||||
metal::device(Device::gpu);
|
||||
return 1;
|
||||
} catch (...) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, std::variant<std::string, size_t>>&
|
||||
|
||||
@@ -533,7 +533,6 @@ METAL_FUNC void fp_qvm_impl(
|
||||
device T* y,
|
||||
const int in_vec_size,
|
||||
const int out_vec_size,
|
||||
const int in_vec_stride,
|
||||
uint3 tid [[threadgroup_position_in_grid]],
|
||||
uint simd_gid [[simdgroup_index_in_threadgroup]],
|
||||
uint simd_lid [[thread_index_in_simdgroup]]) {
|
||||
@@ -564,7 +563,7 @@ METAL_FUNC void fp_qvm_impl(
|
||||
int out_col = pack_factor * tn * (tid.y * num_simdgroups + simd_gid);
|
||||
ws += out_col * bytes_per_pack / pack_factor + simd_lid * out_vec_size_w;
|
||||
scales += out_col / group_size + simd_lid * out_vec_size_g;
|
||||
x += tid.x * in_vec_stride + simd_lid;
|
||||
x += tid.x * in_vec_size + simd_lid;
|
||||
y += tid.x * out_vec_size + out_col;
|
||||
|
||||
if (out_col >= out_vec_size) {
|
||||
@@ -1123,16 +1122,7 @@ template <typename T, const int group_size, int bits, bool batched>
|
||||
tid);
|
||||
}
|
||||
fp_qvm_impl<T, group_size, bits>(
|
||||
w,
|
||||
scales,
|
||||
x,
|
||||
y,
|
||||
in_vec_size,
|
||||
out_vec_size,
|
||||
in_vec_size,
|
||||
tid,
|
||||
simd_gid,
|
||||
simd_lid);
|
||||
w, scales, x, y, in_vec_size, out_vec_size, tid, simd_gid, simd_lid);
|
||||
}
|
||||
|
||||
template <typename T, const int group_size, int bits, int split_k = 32>
|
||||
@@ -1174,20 +1164,8 @@ template <typename T, const int group_size, int bits, int split_k = 32>
|
||||
int in_vec_size_adj =
|
||||
tid.z % split_k == split_k - 1 ? final_block_size : in_vec_size;
|
||||
|
||||
// The in_vec_stride is the full K dimension, not the partition size
|
||||
int in_vec_stride = (split_k - 1) * in_vec_size + final_block_size;
|
||||
|
||||
fp_qvm_impl<T, group_size, bits>(
|
||||
w,
|
||||
scales,
|
||||
x,
|
||||
y,
|
||||
in_vec_size_adj,
|
||||
out_vec_size,
|
||||
in_vec_stride,
|
||||
tid,
|
||||
simd_gid,
|
||||
simd_lid);
|
||||
w, scales, x, y, in_vec_size_adj, out_vec_size, tid, simd_gid, simd_lid);
|
||||
}
|
||||
|
||||
template <
|
||||
@@ -1445,16 +1423,7 @@ template <typename T, int group_size, int bits>
|
||||
s_strides,
|
||||
tid);
|
||||
fp_qvm_impl<T, group_size, bits>(
|
||||
w,
|
||||
scales,
|
||||
x,
|
||||
y,
|
||||
in_vec_size,
|
||||
out_vec_size,
|
||||
in_vec_size,
|
||||
tid,
|
||||
simd_gid,
|
||||
simd_lid);
|
||||
w, scales, x, y, in_vec_size, out_vec_size, tid, simd_gid, simd_lid);
|
||||
}
|
||||
|
||||
template <
|
||||
|
||||
@@ -80,9 +80,7 @@ template <
|
||||
uint3 gsize [[threads_per_grid]]) {
|
||||
Op op;
|
||||
|
||||
IdxT idx =
|
||||
(IdxT(gid.z) * IdxT(gsize.y) + IdxT(gid.y)) * IdxT(gsize.x) * NWORK +
|
||||
IdxT(gid.x) * NWORK;
|
||||
IdxT idx = IdxT(gid.z) * gsize.y + gid.y * gsize.x + gid.x * NWORK;
|
||||
IdxT out_idx;
|
||||
IdxT update_idx;
|
||||
|
||||
|
||||
@@ -983,7 +983,6 @@ METAL_FUNC void qvm_impl(
|
||||
device T* y,
|
||||
const int in_vec_size,
|
||||
const int out_vec_size,
|
||||
const int in_vec_stride,
|
||||
uint3 tid [[threadgroup_position_in_grid]],
|
||||
uint simd_gid [[simdgroup_index_in_threadgroup]],
|
||||
uint simd_lid [[thread_index_in_simdgroup]]) {
|
||||
@@ -1017,7 +1016,7 @@ METAL_FUNC void qvm_impl(
|
||||
ws += out_col * bytes_per_pack / pack_factor + simd_lid * out_vec_size_w;
|
||||
scales += out_col / group_size + simd_lid * out_vec_size_g;
|
||||
biases += out_col / group_size + simd_lid * out_vec_size_g;
|
||||
x += tid.x * in_vec_stride + simd_lid;
|
||||
x += tid.x * in_vec_size + simd_lid;
|
||||
y += tid.x * out_vec_size + out_col;
|
||||
|
||||
if (out_col >= out_vec_size) {
|
||||
@@ -1644,7 +1643,6 @@ template <typename T, const int group_size, const int bits, bool batched>
|
||||
y,
|
||||
in_vec_size,
|
||||
out_vec_size,
|
||||
in_vec_size,
|
||||
tid,
|
||||
simd_gid,
|
||||
simd_lid);
|
||||
@@ -1693,9 +1691,6 @@ template <typename T, const int group_size, const int bits, int split_k = 32>
|
||||
int in_vec_size_adj =
|
||||
tid.z % split_k == split_k - 1 ? final_block_size : in_vec_size;
|
||||
|
||||
// The in_vec_stride is the full K dimension, not the partition size
|
||||
int in_vec_stride = (split_k - 1) * in_vec_size + final_block_size;
|
||||
|
||||
qvm_impl<T, group_size, bits>(
|
||||
w,
|
||||
scales,
|
||||
@@ -1704,7 +1699,6 @@ template <typename T, const int group_size, const int bits, int split_k = 32>
|
||||
y,
|
||||
in_vec_size_adj,
|
||||
out_vec_size,
|
||||
in_vec_stride,
|
||||
tid,
|
||||
simd_gid,
|
||||
simd_lid);
|
||||
@@ -2083,7 +2077,6 @@ template <typename T, int group_size, int bits>
|
||||
y,
|
||||
in_vec_size,
|
||||
out_vec_size,
|
||||
in_vec_size,
|
||||
tid,
|
||||
simd_gid,
|
||||
simd_lid);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
@@ -1550,30 +1547,6 @@ class TestArray(mlx_tests.MLXTestCase):
|
||||
a = a.at[:, :, :].add(update)
|
||||
self.assertEqualArray(a, update)
|
||||
|
||||
def test_slice_update_contiguous_2d(self):
|
||||
for shape in [(32, 32), (64, 64), (17, 33), (128, 128)]:
|
||||
for upd_shape in [(16, 16), (8, 8), (4, 4)]:
|
||||
if upd_shape[0] > shape[0] or upd_shape[1] > shape[1]:
|
||||
continue
|
||||
y = mx.zeros(shape)
|
||||
x = mx.random.normal(upd_shape)
|
||||
z = y.at[: upd_shape[0], : upd_shape[1]].add(x)
|
||||
diff = z[: upd_shape[0], : upd_shape[1]] - x
|
||||
self.assertTrue(mx.allclose(diff, mx.zeros_like(diff)))
|
||||
|
||||
# Test non-zero offset slice update
|
||||
y = mx.zeros((32, 32))
|
||||
x = mx.random.normal((16, 16))
|
||||
z = y.at[16:, 16:].add(x)
|
||||
self.assertTrue(mx.allclose(z[16:, 16:] - x, mx.zeros_like(x)))
|
||||
|
||||
# Test with size divisible by 4, 2, and odd
|
||||
for cols in [32, 18, 15]:
|
||||
y = mx.zeros((32, cols))
|
||||
x = mx.random.normal((16, cols))
|
||||
z = y.at[:16, :].add(x)
|
||||
self.assertTrue(mx.allclose(z[:16, :] - x, mx.zeros_like(x)))
|
||||
|
||||
def test_slice_negative_step(self):
|
||||
a_np = np.arange(20)
|
||||
a_mx = mx.array(a_np)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -470,30 +470,6 @@ class TestQuantized(mlx_tests.MLXTestCase):
|
||||
self.assertEqual(y_q.shape, y_hat.shape)
|
||||
self.assertLess((y_q - y_hat).abs().max(), 2e-3)
|
||||
|
||||
def test_qvm_splitk_multi_row(self):
|
||||
# Test qvm split_k with M > 1 to ensure the x row stride is correct
|
||||
key = mx.random.key(0)
|
||||
k1, k2 = mx.random.split(key)
|
||||
tests = product(
|
||||
[64, 32], # group_size
|
||||
[4, 8], # bits
|
||||
[128], # out dim (N)
|
||||
[2048, 4096], # in dim (K) >= 1024 to trigger split_k
|
||||
[2, 3], # M (multiple rows)
|
||||
)
|
||||
for group_size, bits, N, K, M in tests:
|
||||
with self.subTest(M=M, K=K, N=N, group_size=group_size, bits=bits):
|
||||
x = 1e-1 * mx.random.normal(shape=(M, K), key=k1)
|
||||
w = 1e-1 * mx.random.normal(shape=(K, N), key=k2)
|
||||
w_q, scales, biases = mx.quantize(w, group_size, bits)
|
||||
w_hat = mx.dequantize(w_q, scales, biases, group_size, bits)
|
||||
y_q = mx.quantized_matmul(
|
||||
x, w_q, scales, biases, False, group_size, bits
|
||||
)
|
||||
y_hat = x @ w_hat
|
||||
self.assertEqual(y_q.shape, y_hat.shape)
|
||||
self.assertLess((y_q - y_hat).abs().max(), 2e-3)
|
||||
|
||||
def test_fp_qvm(self):
|
||||
key = mx.random.key(0)
|
||||
k1, k2 = mx.random.split(key)
|
||||
@@ -1070,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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user