From fd27829efa6dcce0bcf4158d9b4fdf0657713505 Mon Sep 17 00:00:00 2001 From: Cheng Date: Sat, 24 Jan 2026 07:22:36 +0900 Subject: [PATCH] Build and test python package on Windows CI (#3049) --- .github/actions/build-windows/action.yml | 12 ++++++++++-- .github/actions/setup-windows/action.yml | 13 +++++++++---- .github/actions/test-windows/action.yml | 7 +++++++ .github/workflows/build_and_test.yml | 2 -- python/src/convert.cpp | 2 +- python/src/utils.cpp | 2 +- python/tests/test_array.py | 5 +++++ python/tests/test_load.py | 8 ++++++++ python/tests/test_memory.py | 4 ++-- setup.py | 1 + 10 files changed, 44 insertions(+), 12 deletions(-) diff --git a/.github/actions/build-windows/action.yml b/.github/actions/build-windows/action.yml index 5de29583..372f8481 100644 --- a/.github/actions/build-windows/action.yml +++ b/.github/actions/build-windows/action.yml @@ -3,7 +3,8 @@ name: 'Build on Windows' runs: using: 'composite' steps: - - name: Build CPP only + - name: Install Python package + id: python-build shell: cmd env: # For MSVC, Ninja/Release is the only config supported by ccache. @@ -14,5 +15,12 @@ runs: -DCMAKE_CXX_COMPILER=cl -DCMAKE_RC_COMPILER=rc run: | - cmake . -B build %CMAKE_ARGS% + uv pip install ".[dev]" -v + :: Pass the CMAKE_ARGS to following steps. + >>%GITHUB_OUTPUT% ECHO CMAKE_ARGS=%CMAKE_ARGS% + + - name: Build CPP only + shell: cmd + run: | + cmake . -B build ${{ steps.python-build.outputs.CMAKE_ARGS }} cmake --build build -j %NUMBER_OF_PROCESSORS% diff --git a/.github/actions/setup-windows/action.yml b/.github/actions/setup-windows/action.yml index a7ea8c4f..83afd4de 100644 --- a/.github/actions/setup-windows/action.yml +++ b/.github/actions/setup-windows/action.yml @@ -20,10 +20,6 @@ runs: key: ccache-${{ runner.os }}-${{ runner.arch }}-cpu max-size: 1GB - - uses: actions/setup-python@v6 - with: - python-version: ${{ inputs.python-version }} - - name: Setup Visual Studio cmd shell: cmd run: | @@ -35,3 +31,12 @@ runs: call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" x64 :: Export to all steps. >>%GITHUB_ENV% set + + - uses: astral-sh/setup-uv@v7 + + - name: Setup Python venv + shell: cmd + run: | + uv venv --python ${{ inputs.python-version }} + call ".venv/Scripts/activate.bat" + >>%GITHUB_ENV% set diff --git a/.github/actions/test-windows/action.yml b/.github/actions/test-windows/action.yml index dab83ee9..ebb886e7 100644 --- a/.github/actions/test-windows/action.yml +++ b/.github/actions/test-windows/action.yml @@ -3,6 +3,13 @@ name: 'Run tests on Windows' runs: using: 'composite' steps: + - name: Run Python tests - CPU + shell: bash + run: | + echo "::group::Python tests - CPU" + python -m unittest discover python/tests -v + echo "::endgroup::" + - name: Run CPP tests - CPU shell: bash env: diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 0fc09794..ba0ccfa6 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -83,8 +83,6 @@ jobs: steps: - uses: actions/checkout@v6 - uses: ./.github/actions/setup-windows - with: - use-ccache: false - uses: ./.github/actions/build-windows - uses: ./.github/actions/test-windows diff --git a/python/src/convert.cpp b/python/src/convert.cpp index 81adcaf1..e004ac9c 100644 --- a/python/src/convert.cpp +++ b/python/src/convert.cpp @@ -471,7 +471,7 @@ mx::array create_array(ArrayInitType v, std::optional t) { if (auto pv = std::get_if(&v); pv) { return mx::array(nb::cast(*pv), t.value_or(mx::bool_)); } else if (auto pv = std::get_if(&v); pv) { - auto val = nb::cast(*pv); + auto val = nb::cast(*pv); auto default_type = (val > std::numeric_limits::max() || val < std::numeric_limits::min()) ? mx::int64 diff --git a/python/src/utils.cpp b/python/src/utils.cpp index 5366e501..32d8a016 100644 --- a/python/src/utils.cpp +++ b/python/src/utils.cpp @@ -11,7 +11,7 @@ mx::array to_array( if (auto pv = std::get_if(&v); pv) { return mx::array(nb::cast(*pv), dtype.value_or(mx::bool_)); } else if (auto pv = std::get_if(&v); pv) { - auto val = nb::cast(*pv); + auto val = nb::cast(*pv); auto default_type = (val > std::numeric_limits::max() || val < std::numeric_limits::min()) ? mx::int64 diff --git a/python/tests/test_array.py b/python/tests/test_array.py index 3e0e9167..89ca5ffd 100644 --- a/python/tests/test_array.py +++ b/python/tests/test_array.py @@ -1618,6 +1618,11 @@ class TestArray(mlx_tests.MLXTestCase): self.assertEqual(mv_mx.format, "Q", f"{mlx_dtype}{np_dtype}") elif np_dtype == np.int64: self.assertEqual(mv_mx.format, "q", f"{mlx_dtype}{np_dtype}") + # for windows long is 32bit and numpy returns L/l. + elif np_dtype == np.uint32 and platform.system() == "Windows": + self.assertEqual(mv_mx.format, "I", f"{mlx_dtype}{np_dtype}") + elif np_dtype == np.int32 and platform.system() == "Windows": + self.assertEqual(mv_mx.format, "i", f"{mlx_dtype}{np_dtype}") else: self.assertEqual( mv_mx.format, mv_np.format, f"{mlx_dtype}{np_dtype}" diff --git a/python/tests/test_load.py b/python/tests/test_load.py index f9e62b36..97fbf6c4 100644 --- a/python/tests/test_load.py +++ b/python/tests/test_load.py @@ -1,6 +1,7 @@ # Copyright © 2023 Apple Inc. import os +import platform import tempfile import unittest from pathlib import Path @@ -126,6 +127,7 @@ class TestLoad(mlx_tests.MLXTestCase): mx.array_equal(load_dict["test"], save_dict["test"]) ) + @unittest.skipIf(platform.system() == "Windows", "GGUF is disabled on Windows") def test_save_and_load_gguf(self): if not os.path.isdir(self.test_dir): os.mkdir(self.test_dir) @@ -186,6 +188,7 @@ class TestLoad(mlx_tests.MLXTestCase): out = mx.load(f)["tensor"] self.assertTrue(mx.allclose(mx.from_fp8(out), expected)) + @unittest.skipIf(platform.system() == "Windows", "GGUF is disabled on Windows") def test_save_and_load_gguf_metadata_basic(self): if not os.path.isdir(self.test_dir): os.mkdir(self.test_dir) @@ -218,6 +221,7 @@ class TestLoad(mlx_tests.MLXTestCase): self.assertTrue("meta" in meta_load_dict) self.assertEqual(meta_load_dict["meta"], "data") + @unittest.skipIf(platform.system() == "Windows", "GGUF is disabled on Windows") def test_save_and_load_gguf_metadata_arrays(self): if not os.path.isdir(self.test_dir): os.mkdir(self.test_dir) @@ -253,6 +257,7 @@ class TestLoad(mlx_tests.MLXTestCase): metadata = {"meta": arr} mx.save_gguf(save_file_mlx, save_dict, metadata) + @unittest.skipIf(platform.system() == "Windows", "GGUF is disabled on Windows") def test_save_and_load_gguf_metadata_mixed(self): if not os.path.isdir(self.test_dir): os.mkdir(self.test_dir) @@ -396,6 +401,9 @@ class TestLoad(mlx_tests.MLXTestCase): aload = mx.load(save_file)["a"] self.assertTrue(mx.array_equal(a, aload)) + if platform.system() == "Windows": + return + save_file = os.path.join(self.test_dir, "a.gguf") mx.save_gguf(save_file, {"a": a}) aload = mx.load(save_file)["a"] diff --git a/python/tests/test_memory.py b/python/tests/test_memory.py index a61e4d87..da4a238d 100644 --- a/python/tests/test_memory.py +++ b/python/tests/test_memory.py @@ -18,8 +18,8 @@ class TestMemory(mlx_tests.MLXTestCase): self.assertEqual(mx.set_cache_limit(old_limit), old_limit) old_limit = mx.set_memory_limit(10) - self.assertTrue(mx.set_memory_limit(old_limit), 10) - self.assertTrue(mx.set_memory_limit(old_limit), old_limit) + self.assertEqual(mx.set_memory_limit(old_limit), 10) + self.assertEqual(mx.set_memory_limit(old_limit), old_limit) # Query active and peak memory a = mx.zeros((4096,)) diff --git a/setup.py b/setup.py index 0176605a..6816817d 100644 --- a/setup.py +++ b/setup.py @@ -229,6 +229,7 @@ if __name__ == "__main__": "dev": [ "numpy>=2", "pre-commit", + "psutil", "torch>=2.9", "typing_extensions", ],