Compare commits

..

4 Commits

Author SHA1 Message Date
Angelos Katharopoulos 2ecf184f91 Add a test 2026-05-07 23:54:35 -07:00
Angelos Katharopoulos 1ea24e11f0 Fix qvm split k with batch dim 2026-05-07 23:54:35 -07:00
Cheng c9aa560577 Make device_count() return 0 when there is no GPU (#3486) 2026-05-08 08:33:55 +09:00
Angelos Katharopoulos ff57d875ea Fix indexing bug in slice update with op (#3483) 2026-05-06 17:04:50 -07:00
15 changed files with 140 additions and 79 deletions
@@ -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::"
+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 }}
+1 -15
View File
@@ -9,21 +9,7 @@ inputs:
runs:
using: "composite"
steps:
# FIXME: The distributed tests fail with free-threading Python.
- name: Check free-threading Python
id: is-free-threading
shell: bash
run: |
echo "::group::Check free-threading Python"
if python -VV 2>&1 | grep "free-threading"; then
echo "result=true" >> $GITHUB_OUTPUT
else
echo "result=false" >> $GITHUB_OUTPUT
fi
echo "::endgroup::"
- name: Run MPI tests
if: ${{ steps.is-free-threading.outputs.result == 'false' }}
shell: bash
run: |
echo "::group::MPI tests"
@@ -31,7 +17,7 @@ runs:
echo "::endgroup::"
- name: Run distributed tests
if: ${{ steps.is-free-threading.outputs.result == 'false' && inputs.has-gpu == 'false' }}
if: ${{ inputs.has-gpu == 'false' }}
shell: bash
run: |
echo "::group::Distributed tests"
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python_version: ["3.11", "3.12", "3.13", "3.14", "3.14t"]
python_version: ["3.11", "3.12", "3.13", "3.14"]
runner:
- ubuntu-22.04
- ubuntu-22.04-arm
@@ -59,7 +59,7 @@ jobs:
if: github.repository == 'ml-explore/mlx'
strategy:
matrix:
python-version: ["3.10", "3.13", "3.14t"]
python-version: ["3.10", "3.13"]
runs-on: [self-hosted, macos]
steps:
- uses: actions/checkout@v6
+9 -4
View File
@@ -47,7 +47,7 @@ jobs:
if: github.repository == 'ml-explore/mlx'
strategy:
matrix:
python_version: ["3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
arch: ['x86_64', 'aarch64']
runs-on: ${{ matrix.arch == 'x86_64' && 'ubuntu-22.04' || 'ubuntu-22.04-arm' }}
env:
@@ -83,7 +83,7 @@ jobs:
if: github.repository == 'ml-explore/mlx'
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: [self-hosted, macos]
env:
PYPI_RELEASE: 1
@@ -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:
+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
+6 -1
View File
@@ -13,7 +13,12 @@ bool is_available() {
}
int device_count() {
return 1;
try {
metal::device(Device::gpu);
return 1;
} catch (...) {
return 0;
}
}
const std::unordered_map<std::string, std::variant<std::string, size_t>>&
+35 -4
View File
@@ -533,6 +533,7 @@ 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]]) {
@@ -563,7 +564,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_size + simd_lid;
x += tid.x * in_vec_stride + simd_lid;
y += tid.x * out_vec_size + out_col;
if (out_col >= out_vec_size) {
@@ -1122,7 +1123,16 @@ 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, tid, simd_gid, simd_lid);
w,
scales,
x,
y,
in_vec_size,
out_vec_size,
in_vec_size,
tid,
simd_gid,
simd_lid);
}
template <typename T, const int group_size, int bits, int split_k = 32>
@@ -1164,8 +1174,20 @@ 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, tid, simd_gid, simd_lid);
w,
scales,
x,
y,
in_vec_size_adj,
out_vec_size,
in_vec_stride,
tid,
simd_gid,
simd_lid);
}
template <
@@ -1423,7 +1445,16 @@ 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, tid, simd_gid, simd_lid);
w,
scales,
x,
y,
in_vec_size,
out_vec_size,
in_vec_size,
tid,
simd_gid,
simd_lid);
}
template <
+3 -1
View File
@@ -80,7 +80,9 @@ template <
uint3 gsize [[threads_per_grid]]) {
Op op;
IdxT idx = IdxT(gid.z) * gsize.y + gid.y * gsize.x + gid.x * NWORK;
IdxT idx =
(IdxT(gid.z) * IdxT(gsize.y) + IdxT(gid.y)) * IdxT(gsize.x) * NWORK +
IdxT(gid.x) * NWORK;
IdxT out_idx;
IdxT update_idx;
+8 -1
View File
@@ -983,6 +983,7 @@ 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]]) {
@@ -1016,7 +1017,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_size + simd_lid;
x += tid.x * in_vec_stride + simd_lid;
y += tid.x * out_vec_size + out_col;
if (out_col >= out_vec_size) {
@@ -1643,6 +1644,7 @@ 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);
@@ -1691,6 +1693,9 @@ 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,
@@ -1699,6 +1704,7 @@ 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);
@@ -2077,6 +2083,7 @@ template <typename T, int group_size, int bits>
y,
in_vec_size,
out_vec_size,
in_vec_size,
tid,
simd_gid,
simd_lid);
-1
View File
@@ -2,7 +2,6 @@ nanobind_add_module(
core
NB_STATIC
STABLE_ABI
FREE_THREADED
LTO
NOMINSIZE
NB_DOMAIN
+24
View File
@@ -1550,6 +1550,30 @@ 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)
+24
View File
@@ -470,6 +470,30 @@ 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)
+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",
],