mlxcel builds two native executables from the root Rust package:
mlxcel— command-line generation, model listing, and downloads.mlxcel-server— HTTP server with OpenAI/llama-server-style endpoints.
The binaries do not require Python or Node.js at runtime. They are not fully static binaries: platform GPU/runtime libraries are still required.
| Platform | Status | Typical feature flags | Notes |
|---|---|---|---|
| macOS on Apple Silicon | primary | metal,accelerate |
Main development and validation target. |
| Linux with NVIDIA CUDA | secondary | cuda |
Release builds currently target CUDA 13-era systems; other versions depend on MLX/CUDA compatibility. |
| Linux CPU-only | not a release target | none | May compile in limited configurations, but it is not a useful or validated inference target for this project. |
| Windows | not documented here | — | The current public installation path is macOS/Linux. |
Both binaries (mlxcel and mlxcel-server) build from the same root package, so
one feature set applies to both. Pass them with cargo build --features <a,b>.
Shipping builds enable only the platform backend flags; the rest are opt-in seams
or test scaffolding.
| Feature | Default | Effect |
|---|---|---|
surgery |
on | Axis A weight-load surgery. Exposes --surgery <config.yaml> and MLXCEL_SURGERY for scale / add / prune / replace / interpolate weight-space edits at load time, and pulls in the mlxcel-surgery crate. When no surgery config is supplied the load path is byte-for-byte identical to a build without the feature. |
metal |
off | Apple Silicon Metal GPU backend (delegates to mlxcel-core/metal). Standard on macOS. |
accelerate |
off | Apple Accelerate CPU BLAS backend (delegates to mlxcel-core/accelerate). Standard on macOS. |
cuda |
off | NVIDIA CUDA GPU backend (delegates to mlxcel-core/cuda). Required on NVIDIA hosts; a plain build is CPU-only (see the footgun note below). |
experimental-backend |
off | Reserves the non-MLX compute-backend seam slot (issue #338). Ships no kernels and adds no runtime dispatch; it only compiles the plug-in boundary where a future non-MLX engine (e.g. FuriosaAI RNGD) would implement ComputeBackend. select_backend() still folds to MLX. |
xla-backend |
off | OpenXLA / StableHLO backend seam (issue #449, ADR 0004). Pulls in mlxcel-xla and compiles the Backend::Xla / Session::Xla arms and the MLXCEL_BACKEND=xla selector, but no native execution engine: the crate is pure-Rust stubs plus the StableHLO graph emitter, so CI builds it unchanged. |
xla-iree |
off | xla-backend plus real IREE execution (mlxcel-xla/iree). Compiles a C shim against a prebuilt IREE runtime and drives the bundled prefill / decode_step graphs. Needs IREE_DIST (or the source-build vars below) at build time, so it is a local / opt-in build, not a CI or release default. |
test-utils |
off | Test-only helpers. Required to build the distributed_integration, pipeline_e2e, and paged_handoff_parity integration tests (cargo test --features test-utils). Not needed for the binaries. |
default = ["surgery"], so a plain cargo build enables surgery only. A real
build always adds a platform backend on top, e.g. --features metal,accelerate on
Apple Silicon or --features cuda on NVIDIA. Build with --no-default-features
to drop the mlxcel-surgery crate entirely (CI parity tests against pre-surgery
behavior, or constrained embedded targets):
# Metal + Accelerate, no surgery crate.
cargo build --release --no-default-features --features metal,accelerateThe XLA path is a two-tier opt-in and never enters Apple-Silicon or CUDA shipping builds, so those binaries compile none of it and the seam folds to MLX:
xla-backendcompiles only the seam: theBackend::Xla/Session::Xlaarms, theMLXCEL_BACKEND=xlaselection, and the StableHLO graph emitter. It needs no native toolchain, so CI builds it unchanged.xla-ireeadds the executing runtime. Its build script compiles a C shim against a prebuilt IREE distribution, so one of these must be set at build time:IREE_DIST: the extractediree-dist-<ver>-linux-<arch>tree (CPU / Vulkan dist). The dist's ownbin/iree-compilelowers the bundled graphs.IREE_CUDA_HOME(+IREE_CUDA_COMPILE): a source-built CUDA-enabled IREE runtime and a matching cuda-capableiree-compile, for the GB10-class GPU path.scripts/iree/setup-cuda.shproduces this tree.IREE_MACOS_HOME(+IREE_MACOS_COMPILE): a source-built macOS runtime and a Metal-capableiree-compile, for the Apple Silicon dev path.scripts/iree/setup-macos.shproduces this tree and prints the matching environment.
At runtime, select the backend with MLXCEL_BACKEND=xla and tune it with the
MLXCEL_XLA_* variables (device, precision, packed quant). See
Environment variables
for the full list and ADR 0004
for the design.
Prerequisites:
- Apple Silicon Mac.
- Rust toolchain compatible with the Rust 2024 edition.
- Xcode Command Line Tools (
xcode-select --install). - Metal toolchain component.
- CMake available on
PATH. ffmpeg5.0 or newer, only if you need video input (brew install ffmpeg). It is a runtime dependency, not a build one: the build and every text, image, and audio path work without it, and--videoreports a named error when it is absent. See Video input and ffmpeg.
# One-time: install the Metal shader compiler if it is not already present.
xcodebuild -downloadComponent MetalToolchain
git clone https://github.com/lablup/mlxcel.git
cd mlxcel
cargo build --release --features metal,accelerateThe build outputs:
target/release/mlxcel
target/release/mlxcel-server
The macOS release workflow also packages a mlx.metallib artifact when needed.
If you distribute binaries manually, verify the runtime package layout against the
release workflow rather than assuming the executable alone is always sufficient.
Prerequisites vary by distribution and CUDA version. At minimum you need:
- Rust toolchain compatible with the Rust 2024 edition.
- CMake and a C++20-capable compiler.
- CUDA toolkit with
nvcc. - NVIDIA driver compatible with the selected CUDA toolkit.
- cuDNN and CUDA runtime libraries required by the pinned MLX build.
- BLAS and LAPACK development packages, including the C headers. MLX's CMake
resolves
cblas.handlapacke.h, so thelapackeheaders must be present, not only the runtime libraries. ffmpeg5.0 or newer, only if you need video input (sudo apt-get install -y ffmpeg). Runtime only, same as on macOS; see Video input and ffmpeg.
On Debian/Ubuntu (x86_64 or aarch64) the build packages are:
sudo apt-get install -y \
build-essential cmake git \
libopenblas-dev liblapack-dev liblapacke-dev
# CUDA toolkit (nvcc) and cuDNN come from NVIDIA's apt repository, e.g.
# cuda-toolkit-13-0 cudnn9-cuda-13liblapacke-dev is the package that ships lapacke.h; liblapack-dev alone
omits it and the MLX CMake configure step fails with LAPACK_INCLUDE_DIRS set
to NOTFOUND.
Example build shape:
git clone https://github.com/lablup/mlxcel.git
cd mlxcel
cargo build --release --features cudaCPU-only build footgun. A plain
cargo build --releaseon Linux uses the default features (nocuda) and produces a CPU-only binary. It still loads and generates, but silently runs MLX on the host CPU at a fraction of GPU throughput (single-digit tok/s on GB10 instead of hundreds), so the mistake is easy to miss. Always pass--features cudaon an NVIDIA host.
If CUDA is not installed under /usr/local/cuda, set CUDA_HOME:
CUDA_HOME=/opt/cuda cargo build --release --features cudaVolta (sm_70) requires a CUDA 12.x toolchain. CUDA 13 removed support for Volta, so its
nvccrejectscompute_70outright withnvcc fatal : Unsupported gpu architecture 'compute_70'before compiling anything. The published release archives are built against CUDA 13 and therefore contain no sm_70 code at all, and the project's CUDA CI runners carry CUDA 13, so thecuda-sm70-compilegate skips there rather than failing. Building for a V100 or any other Volta card means a source build on a host with CUDA 12.x installed. This was verified on CUDA 12.9.41, which compiles sm_70 without complaint.
src/lib/mlxcel-core/build.rs reads MLX_CUDA_ARCHITECTURES. If it is unset,
the build script tries to detect the compute capability with nvidia-smi and
falls back to 90a when detection fails. For SM 90 and above it appends CUDA's
architecture-specific a suffix (so 90 becomes 90a), because the dedicated
Hopper quantized kernel (qmm_sm90) is only compiled when 90a is in the arch
list. An explicitly set MLX_CUDA_ARCHITECTURES is used verbatim, so include the
suffix yourself for Hopper (90a).
# Hopper / GH200-style target. The `a` suffix is required for the Hopper
# quantized kernel; plain `90` builds without it.
MLX_CUDA_ARCHITECTURES=90a cargo build --release --features cuda
# GB10 / DGX Spark-style target used by the release workflow.
MLX_CUDA_ARCHITECTURES=121 cargo build --release --features cuda
# Multiple targets, if your MLX/CUDA toolchain supports them.
MLX_CUDA_ARCHITECTURES="90a;121" cargo build --release --features cudaIf the architecture list a binary was built with does not cover the GPU it is
started on, it refuses to start and says so, naming both the list it carries and
the compute capability it found, instead of failing later with an opaque CUDA
load error at the first kernel launch (issue #1537). Two cases produce that: a
published x86_64 archive, whose matrix starts at 80, on a pre-Ampere card such
as a V100; and a source build made on a host where nvidia-smi was unavailable,
which falls back to 90a and so cannot run on its own build machine. The fix in
both cases is the rebuild above with MLX_CUDA_ARCHITECTURES set to the target
device. Set MLXCEL_TRACE_ARCH (see
Environment variables) to print the running
capability, the compiled list, and whether the device is served by a cubin or by
JIT-compiled PTX; the same summary appears next to the Detected N GPU(s) line
at startup. MLXCEL_DEVICE=cpu bypasses the refusal, so a binary built for the
wrong architecture can still be run on the CPU while a correct one is built.
The repository release workflow builds two Linux CUDA targets on self-hosted
runners, each as one fat binary: aarch64 covering GH200 (90a), GB200 (100),
and GB10 (121) in a single build (90a;100;121), and x86_64 covering Ampere
through Blackwell (80;86;89;90a;100;120). For each target the mlxcel CLI and the
mlxcel-server are published as separate archives (mlxcel-... and
mlxcel-server-..., each roughly 347 MB) so a consumer downloads only the one
it needs. Every published release also ships a CycloneDX SBOM named
sbom-<version>.cyclonedx.json.gz for supply-chain transparency and
vulnerability scanning. Treat other GPU/OS combinations as source builds that
need local validation.
MLX's CUDA backend compiles some kernels at runtime with NVRTC the first time they run (gather and other indexing kernels, and since the 2026-07 MLX pin also the quantized matmul kernels), so a prebuilt binary needs CUDA headers available on the deployment host, not only the runtime libraries:
- CCCL (libcu++) headers are bundled inside the prebuilt Linux CUDA
archives (both aarch64 and x86_64). Each unpacks to
bin/+include/cccl/, the layout MLX's JIT looks for relative to the executable (<exe-dir>/../include/cccl). Keepmlxcel/mlxcel-serverunderbin/and theinclude/cccl/directory beside it; do not flatten them. The runtime resolves the bundled headers from the executable's canonical path (/proc/self/exe), so any launch style works, including a relative./mlxcel. SetMLXCEL_CCCL_DIRto point the JIT at the CCCL headers explicitly, e.g. when embedding mlxcel and keeping a flat binary layout. - CUTLASS/CuTe headers are bundled the same way (
include/cute/andinclude/cutlass/besidebin/). The MLX pin from 2026-07 on JIT-compiles the quantized matmul kernels (qmm,gather_gemm) with NVRTC, and those kernels include<cute/...>/<cutlass/...>. The JIT resolves them from<exe-dir>/../include; setMLXCEL_CUTLASS_DIRto a directory containingcute/andcutlass/to override, e.g. for a flat embedded layout. Source builds fall back to the build tree automatically. Without these headers the first quantized-model run fails withcannot open source file "cute/numeric/numeric_types.hpp". - CUDA toolkit headers (
cuda_runtime.hand friends) come from the host. Install the CUDA toolkit and setCUDA_HOME(orCUDA_PATH) if it is not at/usr/local/cuda. Without them the first NVRTC compile fails withcannot open source fileerrors. - An NVIDIA driver matching the CUDA toolkit must be present to run on the GPU.
Compiled kernels are cached on disk (MLX_PTX_CACHE_DIR, default under the
system temp dir), so only the first run of each kernel variant pays the NVRTC
cost. Point MLX_PTX_CACHE_DIR at a persistent path to keep the cache across
sessions.
In release builds the C++ bridge defaults to -march=native, which tunes for
(and only runs on) the build host's CPU. That is correct for builds that run
where they are built (developer machines, the per-machine GB10/GH200 release
assets). For a binary that must run on other machines, set MLXCEL_CXX_MARCH
to a portable baseline; the release workflow's x86-64 assets use x86-64-v3
(AVX2):
# Portable x86-64 build (any AVX2-capable CPU, ~2013+).
MLXCEL_CXX_MARCH=x86-64-v3 cargo build --release --features cuda
# Omit -march entirely (compiler default baseline).
MLXCEL_CXX_MARCH=none cargo build --release --features cuda| Variable | Description | Default |
|---|---|---|
CUDA_HOME |
CUDA toolkit root, build-time and for runtime NVRTC headers | /usr/local/cuda when present |
MLX_CUDA_ARCHITECTURES |
CUDA SM target list, build-time | auto-detect via nvidia-smi, then 90a fallback |
MLXCEL_CXX_MARCH |
C++ bridge -march value, build-time; none omits the flag |
native |
MLXCEL_CCCL_DIR |
Override for the bundled CCCL (libcu++) header dir used by the CUDA NVRTC JIT | bundled <exe-dir>/../include/cccl, then build-time fallback |
MLXCEL_CUTLASS_DIR |
Override for the bundled CUTLASS/CuTe header dir used by the CUDA NVRTC JIT for quantized matmul kernels | bundled <exe-dir>/../include, then build-time fallback |
MLX_PTX_CACHE_DIR |
On-disk cache for JIT-compiled CUDA kernels | system temp dir |
MLXCEL_QUIET_JIT |
Suppress the one-time "compiling CUDA kernels" notice on a cold first run | unset (notice shown) |
MLXCEL_DEVICE |
Runtime device hint (gpu, metal, or cpu) |
gpu |
MLXCEL_WIRED_LIMIT |
Apple Silicon wired-memory ceiling, e.g. 64GB; 0/none disables it |
max |
LLAMA_ARG_* |
Environment-backed server options accepted by clap | unset |
For the complete MLXCEL_* reference, see
Environment variables.
Video frame extraction shells out to the system ffmpeg and ffprobe. Both
must be on PATH, and both must come from ffmpeg 5.0 (2022) or newer.
Neither is a build-time dependency: a build without ffmpeg is complete and
every text, image, and audio path works, and --video (CLI) or a video_url
content block (server) returns a named error rather than failing obscurely.
# macOS
brew install ffmpeg
# Debian / Ubuntu
sudo apt-get install -y ffmpeg
ffmpeg -version | head -1 # must report 5.0 or newerThe floor is set by one flag. Extraction passes -fps_mode vfr, which ffmpeg
added in 5.0 at the same time it deprecated the older -vsync; ffmpeg 8
removed -vsync outright. On 4.x and older, -fps_mode is unrecognized and
video input is unsupported, so upgrade the system binary rather than trying to
work around it. There is no upper bound; releases through 9.x work unchanged.
A wrong-version ffmpeg fails at argument parsing, before any frame is decoded, so the error names the option rather than the video:
Unrecognized option 'fps_mode'.
Error splitting the argument list: Option not found
Contributors touching the video path should run make verify-test-video, which
runs the ffmpeg-backed tests for real. They are #[ignore] in the normal suite,
so a machine without ffmpeg reports them as ignored instead of silently passing
(#1172).
./target/release/mlxcel --version
./target/release/mlxcel-server --version
# `download` defaults to the global store at
# ${MLXCEL_CACHE_DIR:-$HOME/.cache/mlxcel}/models/<owner>/<name>.
./target/release/mlxcel download mlx-community/Qwen3-0.6B-4bit
./target/release/mlxcel generate \
-m ~/.cache/mlxcel/models/mlx-community/Qwen3-0.6B-4bit \
-p "Hello" -n 1On CUDA hosts, run the test suite single threaded. Since the 2026-07 MLX pin the quantized kernels are JIT-compiled and module-loaded on first use, and those first-use paths are not safe against concurrent test threads, so the default parallel run aborts. The measured signatures are in the table below. Inference binaries are unaffected; this is a test-parallelism artifact.
make verify-test-cuda
# which is:
cargo test --workspace --profile test-fast --features cuda --no-fail-fast -- --test-threads=1Three runs of cargo test --release --features cuda -p mlxcel-core --lib on an
idle GB10 (sm_121) at MLX pin 2c46b953 put numbers on that (#1048):
| Threads | MLX_USE_CUDA_GRAPHS |
Outcome |
|---|---|---|
| default (20) | on | SIGABRT, cudaStreamEndCapture ... previous error during capture |
--test-threads=1 |
on | ran to a verdict, 1410 tests in 88s |
| default (20) | 0 |
SIGABRT, cuLaunchKernelEx ... invalid argument |
The third row is why MLX_USE_CUDA_GRAPHS=0 is not the workaround it looks
like: disabling capture does not rescue the parallel run, it only changes which
CUDA call reports the failure, from a module load racing another thread's
stream capture to a kernel-configure race. Serializing addresses the cause;
capture stays fully on under the gate, so the suite keeps exercising it. The
same command under [profile.test-fast], which is what make verify-test-cuda
actually builds, behaves the same way: 1411 passed, 4 failed, 89.35s, no abort.
The abort site and the error text both move between runs, which is what makes
the raw SIGABRT expensive to read: it looks like whichever test happened to be
running is broken.
mlxcel-core carries a the_cuda_test_suite_must_run_single_threaded
guard (src/lib/mlxcel-core/src/cuda_test_serialization_tests.rs) so an
invocation that forgets the flag fails by name with the right command instead.
Being an ordinary test, the guard is filtered out of any narrowed run whose
filter does not match its name, and those runs stay parallel; scoped subsets
pass parallel and it is whole-suite runs that abort. A filter that does match
it, --lib cuda for one, trips the guard on a run that would have been safe;
set MLXCEL_ALLOW_PARALLEL_CUDA_TESTS=1 to downgrade it to a warning there.
make verify-test-cuda is the Linux/NVIDIA counterpart of make verify-test.
Before #1048 there was no CUDA target that ran mlxcel-core's tests at all:
verify-test pins --features metal,accelerate, and make test-fast-cuda
serializes but stays on the root package, so a bare cargo test under it
resolves to -p mlxcel and never builds mlxcel-core. That is the same
blindness #1007 removed on macOS, on the other backend.
cargo build --release (and a hand-run cargo test --release) use
[profile.release]: fat LTO across all ~439 locked crates plus
codegen-units = 1 for the ~390k-line main crate. That is the right tradeoff
for anything you ship, but it is expensive for the day-to-day edit-test loop:
measured at 4 to 6 minutes per incremental rebuild, so a typical issue cycle of
several edit-test iterations pays 20+ minutes of pure compile time.
For local and agent development, use [profile.test-fast] instead (no cross-crate LTO,
codegen-units = 16, incremental compilation, strip = false; still
opt-level = 3 so MLX-heavy numerics stay representative):
# CPU / Metal / Accelerate (macOS adds metal,accelerate automatically)
make test-fast
# Linux / CUDA
make test-fast-cuda
# Narrow to a subset while iterating
make test-fast-cuda FILTER=server::chat_requestor invoke cargo directly:
cargo test --profile test-fast --features cuda -- --test-threads=1Measured on the Linux/CUDA development machine (2026-07): a cold test-fast
build (all dependencies plus the MLX C++ tree) takes about 4m53s, and an
incremental rebuild after touching one main-crate source file takes about 19s,
versus 4 to 6 minutes under [profile.release], roughly a 13x to 19x
iteration speedup. A representative narrow test set (139 tests across model,
server, sampling, and cache modules) passes identically under both profiles.
Use [profile.release] (make release*, or plain cargo build --release) for
anything you ship, benchmark, or quote as representative performance:
test-fast trades link time and binary size for rebuild speed and is not tuned
for either.
Running tests is the exception. make verify-test, and therefore the
nightly workflow
that invokes it, builds the test binaries under test-fast as well. Linking
roughly 77 test binaries under fat LTO was costing that job its entire
180-minute budget before a single test ran. opt-level = 3 is unchanged, so
the optimised MLX numerics the suite depends on are the same; what is no longer
covered is a defect that reproduces only under release LTO or codegen-units = 1.
Reach for cargo test --release --features metal,accelerate by hand when you
are chasing one of those.
make verify-clippy and make verify-test pass --workspace, and dropping it
changes what they cover rather than only how fast they run. The workspace root
in this repository is itself the mlxcel package, so a bare cargo test or
cargo clippy here resolves to -p mlxcel and never builds mlxcel-core,
mlxcel-surgery or mlxcel-xla. Until #1007 the gate did exactly that, which
left 1754 tests unrun, 1354 of them in mlxcel-core, the crate holding the MLX
cxx bridge, layers.rs, the KV cache and the quantization loaders. The lint
half of the hole is easier to miss: --all-targets without --workspace does
not compile a member's test target either, so test-only lint errors and
test-only compile errors both passed the gate.
There is deliberately no default-members in Cargo.toml doing this instead.
It would re-scope every bare cargo invocation in the repository at once,
including the cargo build --release --target aarch64-apple-darwin --locked
that release.yml runs, which would start compiling the default-off
mlxcel-xla into every release build.
Each member builds at the feature set the root selects. mlxcel-core resolves
to metal and accelerate through the root package's forwarding, so there is
one build of it shared by every member. mlxcel-mlx-pin, mlxcel-surgery and
mlxcel-xla resolve to their empty defaults; in particular mlxcel-xla's
iree feature stays off, so its build script skips the native shim and the gate
needs no IREE distribution. The code behind iree, diagnostics and
micro-oracle is still outside the gate for that reason, and needs a local IREE
dist to check (see scripts/iree/setup-macos.sh). mlxcel-mlx-pin is a leaf
with no production role, holding the unit tests for the MLX-pin logic in
mlxcel-core/build_support/mlx_pin.rs; it deliberately does not depend on
mlxcel-core, so cargo test -p mlxcel-mlx-pin runs in seconds instead of
triggering an MLX C++ build.
make verify-test-cuda (#1048) says --workspace for the same reason and
resolves the same way, with cuda in place of metal,accelerate. Pulling in
mlxcel-surgery and mlxcel-xla on the CUDA path is intended and close to
free. Both depend on mlxcel-core, and one cargo test --workspace --features cuda unifies that into a single cuda-enabled build of it, so neither triggers
a second MLX compile; mlxcel-xla's iree feature stays off there too, so it
stays pure Rust and needs no IREE distribution. What their test targets contain
is backend-agnostic Rust, so excluding them would only mean the two crates are
gated on macOS and nowhere else. mlxcel-mlx-pin does not depend on
mlxcel-core at all and costs the run seconds.
make verify-test also passes --no-fail-fast, which matters only now that
the run covers five members: without it the first failing test binary ends the
run and hides the other four behind whatever failed first. Cargo still exits
non-zero, so the gate is no weaker for it. make verify-test-cuda passes it
too.
Widening the scope does not put the run into the concurrency hazard of #1008,
where two mlxcel-core suites sharing one Metal device aborted 7 of 12 runs.
Cargo builds every test binary and then runs them one at a time, so the
mlxcel-core suite never overlaps the root suite on the device. That is
measured, not assumed: on cargo 1.97.1 a three-crate probe workspace completes
its entire build before the first test binary starts, and finishes each binary
before starting the next. Anything that changes it, a parallel test runner such
as cargo nextest for instance, has to re-establish it: the
no_other_mlxcel_core_test_binary_is_sharing_the_gpu guard in
src/lib/mlxcel-core/src/gpu_exclusivity_tests.rs detects a second
mlxcel-core binary, not a root-suite binary competing for the same device.
make verify-test also passes --test-threads=1 (#1092). The sequencing
above bounds concurrency between binaries and nothing else, and the failure
that took main red on 2026-08-16 was inside one: the mlxcel-core binary
died with signal: 11, SIGSEGV, publishing no panic and no test result line,
so --no-fail-fast had nothing to collect and cargo reported a failed target
with no explanation. libtest defaults to one test thread per logical CPU, and
the macOS crash report from the local repro on an 18-core M5 Max shows what
that means here: 18 libtest workers live at the fault, all of them running
MLX-backed cache tests, two inside iokit_user_client_trap and two inside the
allocator, faulting on an address in no mapped region. It is the CUDA abort of
#1048 on the other backend. --jobs 1 does not address it, because --jobs
bounds the build and the build has already finished by the time any test runs.
Serializing is close to free, because the work serializes on the one Metal
device whether or not the host threads do. Measured on an M5 Max at 5dfcb390,
warm cache, whole workspace, 101 binaries and 8128 tests:
| test threads | wall clock |
|---|---|
| default (18) | 69.17s |
--test-threads=1 |
76.39s |
+7.2s, against a cargo test step the nightly budgets 180 minutes for and
whose time goes to the build rather than to running tests. The two large
members pull in opposite directions and nearly cancel: mlxcel-core costs
+23s serialized (10.2s to 33.2s), while the root suite gains 12s (23.5s to
11.6s), because thread contention across 5695 tests is worse than running them
in a row. Order matters when reproducing these numbers: a cold first run pays
roughly 50s of one-time Metal shader compilation, which is enough to invert the
comparison if the two arms are not both warm.
There is deliberately no macOS counterpart to the
the_cuda_test_suite_must_run_single_threaded guard. The CUDA suite aborts
every time it runs parallel, so failing by name costs nothing; the Metal suite
crashes rarely, and a hard guard would break cargo test -p mlxcel-core --lib,
which is three times faster parallel and succeeds nearly always. Narrowed
hand-runs are meant to stay parallel. It is the whole-suite gate that
serializes.
Missing Metal toolchain on macOS — run
xcodebuild -downloadComponent MetalToolchain and rebuild.
Cannot find CUDA library directory on Linux — set CUDA_HOME to the CUDA
toolkit root and rebuild.
nvidia-smi is unavailable on the build host — set MLX_CUDA_ARCHITECTURES
explicitly.
CUDA/cuDNN linker errors — confirm that the libraries expected by the pinned
MLX version are installed and discoverable by the linker. The root build script
links CUDA runtime/math libraries directly and relies on the system driver for
libcuda.
gmake: *** Error 137 (SIGKILL) while compiling qmm_*.cu — the build ran
out of memory. The CUTLASS-heavy quantized-matmul kernels peak at ~4-5 GB of
compiler memory per parallel job, so a default -j$(nproc) build needs roughly
5 GB × cores. Cap the parallelism with cargo build -j N ... (cargo forwards
N to the CMake subbuild); pick N ≈ available_RAM_GB / 5.
CMake error: LAPACK_INCLUDE_DIRS ... NOTFOUND — install liblapacke-dev
(MLX needs lapacke.h, which liblapack-dev alone does not provide) and
libopenblas-dev.