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 with AMD ROCm | experimental | rocm |
Source build only. Validated on RDNA 3.5 (gfx1151, Strix Halo) with ROCm 10.0 / HIP 7.15; tracked in #1801. See Linux with AMD ROCm. |
| 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). |
rocm |
off | AMD GPU backend on Linux (delegates to mlxcel-core/rocm), built from the ROCm overlay in src/lib/mlx-cpp/patches-rocm/. Cannot be combined with cuda or metal. Experimental; see Linux with AMD ROCm. |
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).
On Blackwell (sm_100, sm_120, sm_121) the a suffix decides something
else, and you should not set it. MLX compiles the hardware block-float
converters in mlx/backend/cuda/quantized/nvfp4_quantize.cuh, the ones that
issue cvt.rn.satfinite.e2m1x2.f32, only when nvcc is compiling for an
architecture-specific target, which it signals with __CUDA_ARCH_SPECIFIC__.
A plain 121 build therefore compiles them out, and both NVFP4 and MXFP4
quantization fall back to a scalar CUTLASS conversion sequence with nothing in
the build output saying so.
Putting 121a in the list would fix that by charging every translation unit
for it. CUTLASS derives CUTLASS_ARCH_MMA_SM121A_ENABLED from the same macro,
so the decode kernels compile differently too, and on a device the suffix
matches, the architecture-specific image is the one the driver loads rather
than an alternative it can ignore. On this project's GB10 that is 12.7 MB of
extra archive and a different qmv on every Blackwell machine, to fix a
converter no decode kernel calls.
So the list stays plain and src/lib/mlx-cpp/CMakeLists.txt adds the
architecture-specific image to fp_quantize.cu alone, which is the only
translation unit that can reach those converters. You get the hardware path
without asking for it, and without it reaching anything else. Building with
121a is a step backwards, not a step forwards; 121f is worse still, because
it satisfies the converters' dispatcher gate but not their own gate and fails
to compile outright.
# 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. Plain: the
# hardware NVFP4/MXFP4 converter is added to one translation unit by CMake.
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 cudaTo confirm what a finished build carries, read the archive rather than the
environment variable. A correct Blackwell build has the fp4 instruction in
fp_quantize.cu.o, exactly one architecture-specific image in the whole
archive, and its forward-JIT PTX untouched:
A=$(ls -d target/release/build/mlxcel-core-*/out/build/lib/libmlx.a | head -1)
mkdir -p /tmp/mlxq && (cd /tmp/mlxq && ar x "$A" fp_quantize.cu.o qmv.cu.o)
cuobjdump --dump-sass /tmp/mlxq/fp_quantize.cu.o | grep -c F2FP.SATFINITE.E2M1 # > 0
cuobjdump --list-elf /tmp/mlxq/qmv.cu.o | grep -c 'sm_[0-9]*a' # 0: decode untouched
cuobjdump --list-elf "$A" | grep -c 'sm_[0-9]*a' # 1
cuobjdump --dump-ptx "$A" | grep -c '.target sm_121$' # > 0One checkout can hold several of those build directories, one per feature set
and architecture list, so confirm the one you are reading is the one you meant:
grep MLX_CUDA_ARCHITECTURES "$(dirname "$A")/../CMakeCache.txt" names the list
it was configured with.
Do not look for cvt.rn.satfinite.e2m1x2 in the PTX. The injection emits a
cubin and no PTX, and the PTX that is emitted comes from the plain compute_121
pass, which takes the fallback arm, so that grep returns zero on a correct
build. The SASS count above is the one that distinguishes the two builds.
If 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). Blackwell is plain in both for the
reason above, with the hardware converter added to one translation unit by
CMake. 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.
src/lib/mlxcel-core/build.rs reads MLX_ROCM_ARCHITECTURES. If it is unset, the build script asks rocminfo for the GPU agent's gfx target and fails with a named error when no agent is reported, which is what happens in a container that cannot reach /dev/kfd. Setting the variable bypasses detection entirely and is used verbatim, so a build host with no visible GPU can still produce a binary for one.
# The Radeon 8060S (Ryzen AI MAX+ 395) used for ROCm validation.
MLX_ROCM_ARCHITECTURES=gfx1151 cargo build --release --features rocm
# Multiple targets, semicolon-separated.
MLX_ROCM_ARCHITECTURES="gfx1100;gfx1151" cargo build --release --features rocmHIP coverage is not the ordering CUDA coverage is. A CUDA cubin runs on a higher minor revision and its PTX JITs forward across majors, so a list can cover a device it does not name. A HIP code object is built for one gfx target and runs on that target only, with no JIT fallback, so gfx1151 and gfx1150 are unrelated despite the adjacent numbers and a list covers exactly the targets it names. Target-feature suffixes (gfx90a:xnack+) select code-object features on one target rather than naming another, so they are ignored when the list is compared against the device.
A binary whose compiled gfx list does not contain the device it is started on refuses to start and names both, rather than failing later with an opaque HIP error at the first kernel launch (issue #1805). Set MLXCEL_TRACE_ARCH (see Environment variables) to print the running target, the compiled list and whether it is covered; the same summary appears next to the Detected N GPU(s) line at startup, alongside the device name and its memory. MLXCEL_DEVICE=cpu bypasses the refusal, so a binary built for the wrong target can still run on the CPU while a correct one is built.
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. - CUDA shared libraries come from the host toolkit too. The binary links
cudart,cublas,cublasLt,cufft,cusolver,nvrtcand cuDNN dynamically. cuSOLVER joined that list when the MLX pin moved to81ba1c6a(MLX's CUDA backend now uses it for Cholesky and initializes it at startup), so a runtime-only install that omits it fails at launch withlibcusolver.so...: cannot open shared object file. The full CUDA toolkit package ships all of them. - 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 cudaExperimental. AMD GPU support on Linux was added in #1802 and is tracked by #1801. It is a source build only: there is no release artifact and no ROCm CI job yet, and the gaps listed below are open.
The backend is not part of upstream MLX. mlxcel vendors the ROCm backend from
the rocm-support branch of
NripeshN/mlx (MIT) into
src/lib/mlx-cpp/patches-rocm/ and applies it on top of the same pinned MLX
commit that the Metal and CUDA builds use. See
mlxcelverse for how the
overlay is organized, and src/lib/mlx-cpp/patches-rocm/LOCAL_FIXES.md for the
changes mlxcel carries on top of the fork.
Tested configuration: AMD Ryzen AI MAX+ 395 with Radeon 8060S (gfx1151,
RDNA 3.5) and a 96 GiB VRAM carve-out, Debian 13, ROCm 10.0.0 packages
(HIP 7.15, AMD clang 23). Other RDNA 3, 3.5 and 4 parts are expected to build;
CDNA parts (for example MI300) compile but carry no tuning.
- Rust toolchain compatible with the Rust 2024 edition, CMake and a C++20 compiler.
pkg-configand the OpenSSL headers, which the Rust dependencies need on Linux.- BLAS and LAPACK development packages, including
lapacke.h(see Linux with CUDA). - A ROCm installation that provides the
hip,rocblas,rocthrust,rocprim,hiprand,rocwmma,hipblaslt,hipfftandhiprtcCMake packages (ls "${ROCM_PATH:-/opt/rocm}"/lib/cmakelists them), plushipccandrocminfo. - Access to the GPU device nodes: the build and run user must be in the
videoandrendergroups.
On Debian/Ubuntu:
sudo apt-get install -y \
build-essential cmake git pkg-config libssl-dev \
libopenblas-dev liblapack-dev liblapacke-dev
# ROCm itself comes from AMD's repository (repo.radeon.com).
rocminfo | grep -E '^\s+Name:\s+gfx' # should list your GPU targetcargo build --release --features rocm
# or
make release-rocmrocm cannot be combined with cuda or metal; the build fails with a message
naming the conflict. The first build compiles the MLX device code with hipcc,
which takes a few minutes on top of the Rust build.
The build compiles MLX device code for the gfx targets that rocminfo
reports on the build host. Set MLX_ROCM_ARCHITECTURES to choose them
explicitly, for example to build on a host without a GPU or for several
targets:
MLX_ROCM_ARCHITECTURES=gfx1151 cargo build --release --features rocm
MLX_ROCM_ARCHITECTURES="gfx1100;gfx1151" cargo build --release --features rocmIf neither the variable nor rocminfo yields a target, the build fails and
names the variable. The chosen list is recorded in the binary as
MLXCEL_ROCM_ARCHITECTURES. Set ROCM_PATH when ROCm is not installed under
/opt/rocm; the build reads its CMake packages, hipcc and rocminfo from
there.
The binaries carry $ROCM_PATH/lib as an rpath, so LD_LIBRARY_PATH is not
needed even when the ROCm libraries are not registered with the dynamic loader.
A GPU run prints Runtime device: GPU at startup, and the process appears in
rocm-smi --showpids:
./target/release/mlxcel generate -m models/mlx/Qwen3-0.6B-4bit -p "Hello" -n 50 --temp 0On a UMA host the GPU shares memory with the operating system and with any
other GPU process, so check rocm-smi --showpids for other tenants before
loading a large model.
To check the HTTP server rather than the CLI, start it and run the chat smoke
script against it. Both a dense and an affine MoE checkpoint pass on gfx1151;
the results are in
docs/benchmark_results/rocm-correctness-gfx1151-2026-09-12.md.
MLXCEL_FUSED_MOE=0 ./target/release/mlxcel-server -m models/mlx/Qwen3-30B-A3B-4bit --port 8080 &
./scripts/server_chat_smoke.sh --port 8080The script checks /health, /v1/models, and /v1/chat/completions both
streaming and non-streaming. It counts the reasoning channel as output, so a
thinking model that spends its whole budget inside the thinking block reports
channel=reasoning only rather than looking like an empty response.
| Area | Status on ROCm |
|---|---|
| Affine 4-bit / 8-bit checkpoints | Run natively. |
| mxfp8 and mxfp4 checkpoints | Run natively, including MoE experts through gather_qmm (for example gpt-oss-20b-MXFP4-Q4). |
| NVFP4 checkpoints | No native kernel; load-time conversion is tracked in #1806. |
| Affine MoE models (for example Qwen3-30B-A3B) | Set MLXCEL_FUSED_MOE=0; the fused MoE path aborts on ROCm until #1803. |
| mlxcel's fused kernels (sampling, fused norm, RoPE + KV append, paged attention) | Run as MLX graph fallbacks on the paths that have one (#1803); ROCm ports are #1814. |
| GPU faults | May show up as NaN output or a hang instead of an error (#1804). |
| Memory estimation on UMA hosts | Reads host RAM, not the VRAM carve-out; set MLXCEL_MEMORY_LIMIT if a model that fits is refused (#1805). |
| Diagnostics | Print a CUDA compute capability line for the AMD device (#1805). |
mlxcel-server chat completions |
Work for dense and affine MoE checkpoints, streaming and non-streaming; verified with scripts/server_chat_smoke.sh. |
| Audio (speech to text, text to speech) | Works. The FFT primitive runs on hipFFT; MLX_ROCM_FFT_CACHE_SIZE bounds how many transform plans stay alive (#1825). |
| Windows, multiple GPUs, distributed inference | Not supported. |
Decode throughput measured on the tested configuration, for orientation only
(greedy, short prompt): Qwen3-0.6B-4bit about 250 tok/s, Qwen3-30B-A3B-4bit
about 55 tok/s with MLXCEL_FUSED_MOE=0, gpt-oss-20b-MXFP4-Q4 about 3.6 tok/s
(generic gather kernel). A benchmark page is tracked in #1810.
| 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
# Linux / ROCm (experimental): no make target yet
cargo test --profile test-fast --features rocm -- --test-threads=1
# 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.
ROCm: Could NOT find hip (or rocblas, rocwmma, ...) during the MLX
configure: install the ROCm development packages that ship those CMake
configs, or point ROCM_PATH at the ROCm root that has them under lib/cmake.
ROCm: openssl-sys fails because pkg-config could not be found: install
pkg-config and libssl-dev, or set OPENSSL_LIB_DIR and
OPENSSL_INCLUDE_DIR for the build.
ROCm: rocminfo reported no GPU agent: set MLX_ROCM_ARCHITECTURES, and
check that the user is in the video and render groups so rocminfo can open
/dev/kfd.
ROCm: [cuda_kernel] No CUDA back-end. followed by an abort: a fused path
without a ROCm fallback was reached. For MoE models set MLXCEL_FUSED_MOE=0;
otherwise report the model in #1803.