Add Metal extension build support - #4004
Conversation
|
One note on the Python Stable ABI: the existing CMake extension example passes Enabling The Python Stable ABI would still not provide a stable MLX C++ ABI across MLX releases. For now, this PR therefore keeps |
97e918e to
a824503
Compare
33b2a4d to
bbc907a
Compare
|
Hi, I wanted to check whether there are any updates on this PR or any remaining concerns I can address. The main motivation is to reduce the amount of project-specific build machinery required for MLX Metal extensions. Some projects currently maintain their own compiler, linker, nanobind, and metallib build logic, for example: For example, I successfully built and tested SGLang’s setup(
name="sglang-kernel",
version=_get_version(),
packages=find_packages(where="python"),
package_dir={"": "python"},
package_data={"sgl_kernel": ["*.metallib", "*.pyi"]},
ext_modules=[
extension.MetalExtension(
"sgl_kernel._metal",
sources=[
"csrc/metal/rope_pool_fused.cpp",
"csrc/metal/rope_pool_fused.metal",
],
include_dirs=["csrc", "csrc/metal"],
extra_compile_args={
"cxx": ["-O3", "-fvisibility=hidden"],
"metal": [f"-std={metal_std}", "-O3"],
},
)
],
cmdclass={"build_ext": extension.BuildExtension},
install_requires=[f"mlx=={package_version('mlx')}"],
)This allows other projects to describe their sources and project-specific flags while MLX handles the common extension build plumbing. |
|
@jundot @WindChimeRan @yeahdongcn I see you have been heavily using custom Metal extensions, would you mind checking whether this feature would be helpful to you? |
No problem. I can take a look. Thanks! |
|
Thanks for the ping. @zcbenz @XXXXRT666 The direction looks useful, but the current API does not fully cover vllm-metal: we have one Python extension with multiple metallibs, including a separate NAX library targeting macOS 26.2 while the others target 15.0. |
yeahdongcn
left a comment
There was a problem hiding this comment.
Thanks for putting this together. My main question is whether MetalExtension should belong to MLX or PyTorch.
For CUDA/ROCm/MUSA, we use from torch.utils.cpp_extension import BuildExtension, CUDAExtension, so I'd prefer to follow a similar approach here. The current implementation always discovers and links against MLX, which makes it tightly coupled to the MLX framework.
I'd prefer a framework-neutral MetalExtension that provides the common build functionality, while allowing the specific framework dependencies to be added separately when needed.
| return f"build-{generator or 'default'}" | ||
|
|
||
|
|
||
| class MetalExtension(Extension): |
There was a problem hiding this comment.
SGLang's target architecture has a Torch-owned ModelRunner with MLX as an optional operator provider, so the downstream build contract needs an explicit mlx, torch, both, or metallib choice rather than an MLX-only build graph.
| if not suffixes.intersection(_HOST_SOURCE_SUFFIXES): | ||
| raise ValueError("MetalExtension requires at least one C++ source file.") | ||
| if _METAL_SOURCE_SUFFIX not in suffixes: | ||
| raise ValueError("MetalExtension requires at least one Metal source file.") |
There was a problem hiding this comment.
Requiring both a C++ source and a Metal source rules out a framework-neutral metallib and Torch's public torch.mps.load_metallib path, which needs no host extension. Could the API model a MetalLibrary as a first-class artifact and attach zero, one, or two backend-specific adapters? metallib would build only the library; mlx and torch would add their own adapters; both would build two separately named adapters over one validated-compatible metallib, or backend-specific library variants when the shader contracts differ.
This needs an artifact split rather than only making the C++ list optional:
setuptools.Extension, get_ext_fullpath, stub generation and sidecar copy
currently all assume an importable host module. A single host binary should
also not be expected to accept both mlx.core.array and torch.Tensor.
| "if(NOT MLX_ROOT)", | ||
| f" set(MLX_ROOT {_cmake_quote(_MLX_PACKAGE_PATH)})", | ||
| "endif()", | ||
| "find_package(MLX CONFIG REQUIRED)", | ||
| "if(NOT MLX_BUILD_METAL)", | ||
| ' message(FATAL_ERROR "MetalExtension requires an MLX build with Metal support.")', | ||
| "endif()", |
There was a problem hiding this comment.
Could dependency discovery be conditional so Torch/metallib modes do not discover or link MLX, MLX mode does not require Torch, and both builds and tests two isolated adapters? If the facade remains in mlx.extension, could we also clarify whether requiring the MLX Python package solely to access a Torch/metallib builder is intentional? Native Torch support also needs an explicit composition contract with torch.utils.cpp_extension.BuildExtension: setup() has only one cmdclass["build_ext"], while this command currently delegates non-MetalExtension objects only to setuptools.
It’s supported now. I’ve put together a minimal reproduction here that demonstrates how vLLM-Metal kernels can be compiled. I think |
MLX and PyTorch can coexist within the same extension here |
6d82bbf to
35b567b
Compare
Proposed changes
Add a setuptools-based workflow for building MLX extensions from C++ and Metal source files without requiring a project
CMakeLists.txt.MetalExtensionandBuildExtensionwith Ninja-by-default CMake builds, automatic fallback, generator-specific build directories, and optional nanobind stub generation.mlx_build_metallibto accept custom Metal compiler options.cmake_extensionso both workflows remain separate.Checklist
pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes