Skip to content

Commit 39c2e0a

Browse files
committed
ci: isolate MLX native ABI contract
1 parent ef3f3ed commit 39c2e0a

6 files changed

Lines changed: 95 additions & 9 deletions

File tree

.github/workflows/e2e-matrix.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,21 @@ jobs:
5050
base_python=/Volumes/external/sources/cppmega.mlx/.venv/bin/python
5151
test -x "$base_python"
5252
job_venv="$(mktemp -d "$RUNNER_TEMP/cppmega-mlx-e2e-${GITHUB_RUN_ID}-${GITHUB_JOB}.XXXXXX")"
53-
"$base_python" -m venv --system-site-packages "$job_venv"
53+
"$base_python" -m venv "$job_venv"
5454
echo "VBGUI_E2E_PYTHON=$job_venv/bin/python" >> "$GITHUB_ENV"
5555
echo "VBGUI_E2E_VENV=$job_venv" >> "$GITHUB_ENV"
56+
echo "PYTHONPATH=" >> "$GITHUB_ENV"
57+
echo "PYTHONNOUSERSITE=1" >> "$GITHUB_ENV"
5658
- name: Python deps
5759
run: |
5860
"$VBGUI_E2E_PYTHON" -m pip install --disable-pip-version-check \
5961
-e ".[gui,parquet,widget]"
62+
"$VBGUI_E2E_PYTHON" - <<'PY'
63+
from cppmega_mlx.training.native_optim import status
64+
native_status = status()
65+
if not native_status.get("available", False):
66+
raise RuntimeError(native_status.get("reason", "native optimizer extension unavailable"))
67+
PY
6068
- name: Generate fixtures
6169
run: |
6270
"$VBGUI_E2E_PYTHON" tests/fixtures/build_e2e_matrix.py
@@ -109,12 +117,20 @@ jobs:
109117
base_python=/Volumes/external/sources/cppmega.mlx/.venv/bin/python
110118
test -x "$base_python"
111119
job_venv="$(mktemp -d "$RUNNER_TEMP/cppmega-mlx-e2e-${GITHUB_RUN_ID}-${GITHUB_JOB}.XXXXXX")"
112-
"$base_python" -m venv --system-site-packages "$job_venv"
120+
"$base_python" -m venv "$job_venv"
113121
echo "VBGUI_E2E_PYTHON=$job_venv/bin/python" >> "$GITHUB_ENV"
114122
echo "VBGUI_E2E_VENV=$job_venv" >> "$GITHUB_ENV"
123+
echo "PYTHONPATH=" >> "$GITHUB_ENV"
124+
echo "PYTHONNOUSERSITE=1" >> "$GITHUB_ENV"
115125
- run: |
116126
"$VBGUI_E2E_PYTHON" -m pip install --disable-pip-version-check \
117127
-e ".[gui,parquet,widget]"
128+
"$VBGUI_E2E_PYTHON" - <<'PY'
129+
from cppmega_mlx.training.native_optim import status
130+
native_status = status()
131+
if not native_status.get("available", False):
132+
raise RuntimeError(native_status.get("reason", "native optimizer extension unavailable"))
133+
PY
118134
- run: |
119135
"$VBGUI_E2E_PYTHON" tests/fixtures/build_e2e_matrix.py
120136
- working-directory: vbgui
@@ -162,12 +178,20 @@ jobs:
162178
base_python=/Volumes/external/sources/cppmega.mlx/.venv/bin/python
163179
test -x "$base_python"
164180
job_venv="$(mktemp -d "$RUNNER_TEMP/cppmega-mlx-e2e-${GITHUB_RUN_ID}-${GITHUB_JOB}.XXXXXX")"
165-
"$base_python" -m venv --system-site-packages "$job_venv"
181+
"$base_python" -m venv "$job_venv"
166182
echo "VBGUI_E2E_PYTHON=$job_venv/bin/python" >> "$GITHUB_ENV"
167183
echo "VBGUI_E2E_VENV=$job_venv" >> "$GITHUB_ENV"
184+
echo "PYTHONPATH=" >> "$GITHUB_ENV"
185+
echo "PYTHONNOUSERSITE=1" >> "$GITHUB_ENV"
168186
- run: |
169187
"$VBGUI_E2E_PYTHON" -m pip install --disable-pip-version-check \
170188
-e ".[gui,parquet,widget]"
189+
"$VBGUI_E2E_PYTHON" - <<'PY'
190+
from cppmega_mlx.training.native_optim import status
191+
native_status = status()
192+
if not native_status.get("available", False):
193+
raise RuntimeError(native_status.get("reason", "native optimizer extension unavailable"))
194+
PY
171195
- run: |
172196
"$VBGUI_E2E_PYTHON" tests/fixtures/build_e2e_matrix.py
173197
- working-directory: vbgui

cppmega_mlx/training/native_optim/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import mlx.core as _mlx_core # noqa: F401 - loads the matching MLX dylib first.
6+
57
try:
68
from cppmega_mlx.training.native_optim._ext import ( # type: ignore[attr-defined]
79
fused_adam8bit_step,

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["setuptools>=68", "mlx>=0.31", "nanobind>=2.4"]
2+
requires = ["setuptools>=68", "mlx==0.32.0", "nanobind>=2.4"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
@@ -8,7 +8,7 @@ version = "0.1.0"
88
description = "MLX-native local training port of cppmega model semantics"
99
requires-python = ">=3.11"
1010
dependencies = [
11-
"mlx>=0.31",
11+
"mlx==0.32.0",
1212
"mlx-lm>=0.31",
1313
"numpy>=1.26",
1414
"safetensors>=0.7",

setup.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import subprocess
23
import sys
34
from pathlib import Path
45

@@ -8,6 +9,34 @@
89
from mlx import extension
910

1011

12+
def _target_mlx_cmake_root() -> Path:
13+
probe = subprocess.run(
14+
[sys.executable, "-m", "mlx", "--cmake-dir"],
15+
check=False,
16+
capture_output=True,
17+
text=True,
18+
)
19+
if probe.returncode == 0 and probe.stdout.strip():
20+
runtime_root = Path(probe.stdout.strip()).resolve()
21+
runtime_config = runtime_root / "share" / "cmake" / "MLX" / "MLXConfig.cmake"
22+
if runtime_config.is_file():
23+
return runtime_root
24+
25+
build_root = Path(extension.__file__).resolve().parent
26+
build_config = build_root / "share" / "cmake" / "MLX" / "MLXConfig.cmake"
27+
if not build_config.is_file():
28+
raise RuntimeError(
29+
"neither the target interpreter nor the isolated build dependency "
30+
"provides MLXConfig.cmake"
31+
)
32+
print(
33+
"cppmega build: target interpreter has no MLX CMake package; "
34+
f"using isolated build dependency at {build_root}",
35+
file=sys.stderr,
36+
)
37+
return build_root
38+
39+
1140
class CppmegaCMakeBuild(extension.CMakeBuild):
1241
"""Build native extensions with the interpreter that invoked setuptools."""
1342

@@ -16,7 +45,7 @@ def build_extension(self, ext: extension.CMakeExtension) -> None:
1645
build_contract_args = (
1746
f"-DPython_EXECUTABLE={sys.executable}",
1847
f"-Dnanobind_DIR={nanobind.cmake_dir()}",
19-
f"-DMLX_ROOT={Path(extension.__file__).resolve().parent}",
48+
f"-DMLX_ROOT={_target_mlx_cmake_root()}",
2049
)
2150
os.environ["CMAKE_ARGS"] = " ".join(
2251
part for part in (previous_args, *build_contract_args) if part

tests/test_setup_native_build.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import tomllib
66
from pathlib import Path
7+
from types import SimpleNamespace
78

89
from setuptools import Distribution
910

@@ -14,8 +15,9 @@ def test_native_build_dependencies_are_declared() -> None:
1415
)
1516
requirements = pyproject["build-system"]["requires"]
1617

17-
assert any(requirement.startswith("mlx>=") for requirement in requirements)
18+
assert "mlx==0.32.0" in requirements
1819
assert any(requirement.startswith("nanobind>=") for requirement in requirements)
20+
assert "mlx==0.32.0" in pyproject["project"]["dependencies"]
1921

2022

2123
def test_native_build_pins_cmake_to_invoking_python(monkeypatch) -> None:
@@ -47,3 +49,27 @@ def capture_build_extension(_command, _extension) -> None:
4749
assert "-Dnanobind_DIR=" in cmake_args
4850
assert "-DMLX_ROOT=" in cmake_args
4951
assert os.environ["CMAKE_ARGS"] == "-DEXISTING_OPTION=ON"
52+
53+
54+
def test_native_build_prefers_target_interpreter_mlx(tmp_path, monkeypatch) -> None:
55+
repo_root = Path(__file__).resolve().parents[1]
56+
monkeypatch.syspath_prepend(str(repo_root))
57+
58+
import setup as cppmega_setup
59+
60+
runtime_root = tmp_path / "runtime-mlx"
61+
config = runtime_root / "share" / "cmake" / "MLX" / "MLXConfig.cmake"
62+
config.parent.mkdir(parents=True)
63+
config.write_text("# fixture\n")
64+
65+
monkeypatch.setattr(
66+
cppmega_setup.subprocess,
67+
"run",
68+
lambda *args, **kwargs: SimpleNamespace(
69+
returncode=0,
70+
stdout=f"{runtime_root}\n",
71+
stderr="",
72+
),
73+
)
74+
75+
assert cppmega_setup._target_mlx_cmake_root() == runtime_root.resolve()

tests/test_workflow_runner_policy.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,19 @@ def test_macos_e2e_uses_an_isolated_job_venv() -> None:
8383
assert "pip install --upgrade pip" not in workflow
8484
assert 'job_venv="$RUNNER_TEMP/cppmega-mlx-e2e-venv"' not in workflow
8585
assert workflow.count('mktemp -d "$RUNNER_TEMP/cppmega-mlx-e2e-') == 3
86-
assert workflow.count("-m venv --system-site-packages") == 3
86+
assert "--system-site-packages" not in workflow
87+
assert workflow.count('"$base_python" -m venv "$job_venv"') == 3
88+
assert workflow.count('echo "PYTHONPATH=" >> "$GITHUB_ENV"') == 3
89+
assert workflow.count('echo "PYTHONNOUSERSITE=1" >> "$GITHUB_ENV"') == 3
8790
assert "--no-build-isolation" not in workflow
8891
assert workflow.count('-e ".[gui,parquet,widget]"') == 3
8992
assert workflow.count('echo "VBGUI_E2E_PYTHON=$job_venv/bin/python"') == 3
9093
assert workflow.count('rm -rf "$VBGUI_E2E_VENV"') == 3
94+
assert workflow.count("native optimizer extension unavailable") == 3
9195

9296

9397
def test_build_backend_declares_mlx_imported_by_setup_py() -> None:
9498
config = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
9599

96-
assert "mlx>=0.31" in config["build-system"]["requires"]
100+
assert "mlx==0.32.0" in config["build-system"]["requires"]
101+
assert "mlx==0.32.0" in config["project"]["dependencies"]

0 commit comments

Comments
 (0)