Repository navigation
chore(core): expose CUDA compute capability to the mlxcel runtime - #1551
Conversation
Before this change `grep -rn "compute_capability" src/ --include=*.rs` returned nothing. Every architecture decision was delegated to MLX's C++ gates, so mlxcel could not report which architecture it was running on, did not record which architectures it had been compiled for, and failed with an opaque CUDA load error at the first kernel launch when the two did not agree. The published x86_64 release matrix starts at `80`, so a released archive cannot run on a V100 at all, and `build.rs` falls back to `90a` when `nvidia-smi` is missing, which yields a binary that cannot run on its own build host. Neither case said so. Plumbing and diagnostics only. No kernel or dispatch behavior changes; acting on the value is left to the consumers in epic #1536. - `hardware::cuda_compute_capability() -> Option<(u32, u32)>` (new `mlxcel-core/src/cuda_arch.rs`, re-exported from `hardware.rs`) reports the running GPU's capability, cached in a `OnceLock`. It is backed by a new `gpu_compute_capability(index)` bridge call that reads MLX's own `device_info` map rather than issuing a second CUDA call, so the symbol links unchanged on Metal and CPU-only builds and stays callable on a binary whose cubins do not cover the device. - `build.rs` resolves the architecture list once in `main`, passes that same string to CMake, and records it as `cargo:rustc-env=MLXCEL_CUDA_ARCHITECTURES`, so the list compiled for and the list reported cannot disagree. - `cuda_arch_mismatch()` applies CUDA's own compatibility rules (cubin forward across minor revisions within a major, PTX JIT forward including across majors, `a`-suffixed entries exact-only, `f`-suffixed entries within their major), and `initialize_runtime_checked()` turns a no-coverage verdict into a named error at device init for generate, chat, embed, rerank, detect and the server. An unparseable list is treated as unknown rather than as "covers nothing", and `MLXCEL_DEVICE=cpu` bypasses the refusal. - The startup device report carries the capability, the compiled list and the coverage verdict next to `Detected N GPU(s)` in both the CLI and the server. `MLXCEL_TRACE_ARCH` prints the same picture once per process to stderr, and the CUDA `QuantizedMatmul` overlay reports which quantized-matmul path the dispatcher selected on its first call. - `docs/environment-variables.md` documents the new variable; `docs/installation.md` cross-references the refusal from the CUDA architecture selection section. Validation on the sm_70 host (Tesla V100-PCIE-32GB, CUDA 12.9, driver 575.51.03): - `cargo test -p mlxcel-core --release --features cuda --lib cuda_arch` (26 passed), covering exact match, PTX-forward-compatible match, `90a`-suffixed entries, `-real` / `-virtual` qualifiers, the no-match case, and the error message - the same 26 tests on a non-CUDA build, where the recorded architecture list is empty and the capability probe reports `None`, which is the code path a Metal build takes - `cargo test --release --features cuda --lib execution::runtime` (13 passed), including that the refusal tracks `cuda_arch_mismatch()` and applies only when the GPU was requested - `MLX_CUDA_ARCHITECTURES=70 make release-cuda`, then `MLXCEL_TRACE_ARCH=1 mlxcel generate -m ./models/mlx-community/qwen3.8-27B-4bit -p "Hi." -n 8`, which prints `compute capability 7.0 (sm_70); compiled for [70]; coverage: cubin` exactly once and `quantized matmul path on first call: qmm_naive (device sm_70)` exactly once, and the same run with the variable unset, which prints neither - `cargo clippy --release --features mlxcel/cuda -p mlxcel-core -p mlxcel --lib --tests -- -D warnings` - `cargo fmt --check`, `git diff --check` The GB10 acceptance criteria could not be exercised: this host has no sm_80-or-later device, so `Some((12, 1))` on GB10, the Metal `None`, and the GB10 baseline remain unverified and are tracked in the epic's GB10 continuation section. Refs #1536
|
Follow-up datum on what the check replaces, since the acceptance criterion is phrased as "the new named error rather than a CUDA load failure".
I am not attributing the stall to a specific CUDA call, since I did not attach a debugger and MLX also JIT-compiles some kernels with NVRTC at first use, which could account for part of it. The point that stands is the one the issue makes: without the check the failure mode carries no diagnostic naming either the architectures the binary holds or the device it found. |
Summary
Makes CUDA compute capability a first-class, testable value in mlxcel. Before this change
grep -rn "compute_capability" src/ --include=*.rsreturned nothing: every architecture decision was delegated to MLX's C++ gates, so mlxcel could not report which architecture it was running on, did not record which architectures it had been compiled for, and failed with an opaque CUDA load error at the first kernel launch when the two did not agree. This is plumbing and diagnostics only. No kernel or dispatch behavior changes; acting on the value is left to the consumers in epic #1536.What changed
Runtime probe.
mlxcel_core::hardware::cuda_compute_capability() -> Option<(u32, u32)>(implemented in the newsrc/lib/mlxcel-core/src/cuda_arch.rs, re-exported fromhardware.rs) returns the running GPU's compute capability, cached in aOnceLockbecause it cannot change within a process. It is backed by a newgpu_compute_capability(index)bridge function that reads MLX's owndevice_infomap, whose CUDA backend already fillscompute_capability_major/compute_capability_minorfrom thecudaDeviceGetAttributevalues it caches per device, so no second CUDA call is issued and no CUDA header is required. The same symbol links on Metal and CPU-only builds, where those keys are absent and the probe reportsNone. Reading device properties never loads a cubin, which is what keeps the probe callable on exactly the binaries the mismatch check has to report on.Build-time record.
build.rsnow resolves the CUDA architecture list once inmainvia a newresolve_cuda_architectures(), passes that same string to CMake, and records it in the crate ascargo:rustc-env=MLXCEL_CUDA_ARCHITECTURES=<list>. The list CMake compiles for and the list the binary reports are therefore the same string by construction. Empty off CUDA.Mismatch check.
cuda_arch_mismatch()compares the running device against the compiled list using CUDA's own compatibility rules: a cubin carries forward across minor revisions within one major version, PTX JITs forward including across majors,a-suffixed (architecture-specific) entries cover their exact target only, andf-suffixed (family-specific) entries carry forward within their major.enforce_cuda_arch_compatibility()turns a no-coverage verdict into a named error, and the newinitialize_runtime_checked()calls it at device init, sogenerate,chat,embed,rerank,detectand the server refuse to start with a message naming both the compiled list and the device found, instead of continuing to a CUDA load failure that names neither. Two safety valves are deliberate: an architecture list that parses to nothing (native,all-major, or a spelling this parser has not learned) is treated as unknown rather than as "covers nothing", so it can never manufacture a startup failure; andMLXCEL_DEVICE=cpubypasses the refusal, since running on the CPU is the one workaround left to someone holding an archive built for the wrong architecture.Diagnostics. The startup device report now carries the architecture picture next to
Detected N GPU(s), in both the CLI (src/commands/generate.rs) and the server (src/server/startup.rs, which also emits it as structured fields alongside the existingHardware capabilities detectedrecord).MLXCEL_TRACE_ARCH(presence-enables, matching the other diagnostic switches) prints the resolved capability, the compiled list, and the coverage verdict once per process to stderr. The CUDAQuantizedMatmuloverlay (src/lib/mlx-cpp/patches/mlx/backend/cuda/quantized/quantized.cpp) additionally reports which quantized-matmul path the dispatcher picked on its first call, which is the most architecture-dependent choice in decode and which MLX exposes no hook for; its environment read is cached because it sits on the decode path.Docs.
docs/environment-variables.mdgains theMLXCEL_TRACE_ARCHrow;docs/installation.mdcross-references the new refusal from the CUDA architecture selection section, naming the two cases that produce it (a published x86_64 archive on a pre-Ampere card, and a source build made wherenvidia-smiwas unavailable andbuild.rsfell back to90a).Test plan
Host: Tesla V100-PCIE-32GB (compute capability 7.0, sm_70), CUDA 12.9.41, driver 575.51.03, x86_64. This is the only GPU on the machine, which bounds what could be verified; see the section below.
cargo test -p mlxcel-core --release --features cuda --lib cuda_arch(26 passed). The parser and coverage tests cover exact match, PTX-forward-compatible match,90a-suffixed entries,f-suffixed entries,-real/-virtualqualifiers, cubin-preferred-over-PTX, the no-match case, and the error message. None of them need a GPU.cudafeature (cargo test -p mlxcel-core --release --lib cuda_arch). There the recorded architecture list is empty and the capability probe reportsNone, which is the samedevice_info-without-capability-keys path a Metal build takes.cargo test --release --features mlxcel/cuda --lib execution::runtime(13 passed), including that the refusal trackscuda_arch_mismatch()exactly and that it applies only when the GPU was actually requested.cargo clippy --release --features mlxcel/cuda -p mlxcel-core -p mlxcel --lib --tests -- -D warnings,cargo fmt --check,git diff --check.MLX_CUDA_ARCHITECTURES=70 cargo build --release --features cuda, thenMLXCEL_TRACE_ARCH=1 mlxcel generate -m ./models/mlx-community/qwen3.8-27B-4bit -p "Hi." -n 8. The probe reports the real device:[mlxcel arch] compute capability 7.0 (sm_70); compiled for [70]; coverage: cubin, printed exactly once across 8 decode steps, followed once by[mlxcel arch] quantized matmul path on first call: qmm_naive (device sm_70). The startup device report showsDetected 1 GPU(s).thenCUDA compute capability 7.0 (sm_70); compiled for [70] (cubin).MLXCEL_TRACE_ARCHunset prints neither trace line and generates identically.MLX_CUDA_ARCHITECTURES="80;86;89;90a;100;120" cargo build --release --features cuda --bin mlxcel(the x86_64 release matrix; itslibmlx.ais 249 MB against 155 MB for the single-architecture sm_70 build, so the six architectures really are compiled in), then running that binary on this V100. It exits non-zero with the new named error before touching a kernel:this mlxcel binary was compiled for CUDA architectures [80;86;89;90a;100;120] and cannot run on the GPU in this host, which is compute capability 7.0 (sm_70). No compiled architecture provides a cubin for sm_70, and none is old enough for the driver to JIT its PTX forward, so every kernel launch would fail. Rebuild for this device with MLX_CUDA_ARCHITECTURES=70 ... To run this binary as it is, set MLXCEL_DEVICE=cpu.MLXCEL_TRACE_ARCH=1reportscoverage: not coveredbefore refusing, and underMLXCEL_DEVICE=cpustarts normally on the CPU with the startup line readingcompiled for [80;86;89;90a;100;120] (not covered).Not run: the full
cargo test --features cudaandcargo testsuites. This unit builds MLX, CUTLASS and cuDNN from source per feature set, and each cold configuration here took 30 to 47 minutes, so validation was scoped to the modules this change touches plus the end-to-end runs above. CI covers the rest.Deferred to GB10
This machine has exactly one GPU, a Tesla V100 at compute capability 7.0, and no Ampere-or-later device of any kind. The following acceptance criteria from #1537 therefore could not be exercised here and are left unticked rather than assumed. They belong to the
## GB10 (sm_121) continuationsection of epic #1536.cuda_compute_capability()returnsSome((12, 1))on GB10. Only theSome((7, 0))half was verified. The unpacking of the bridge'smajor * 1000 + minorencoding and the121list entry are covered by unit tests, but no sm_121 device ran this code.cuda_compute_capability()returnsNoneon a Metal build. No macOS host was available. The nearest reachable evidence is the Linux CPU-only build above, which exercises the same branch:device_inforeturns a map without the two capability keys and the probe reportsNone. That is the identical code path, but it is not a Metal run.MLXCEL_TRACE_ARCHunset is unchanged. No GB10 or Apple Silicon baseline was measured, and no throughput claim is made for either.Closes #1537