CUDA programs assume an NVIDIA machine. CuMetal makes a useful subset of them run on Apple Silicon.
It is a compiler, a runtime, and a set of compatibility libraries. CUDA source or PTX goes in. Metal runs on the GPU. There is no NVIDIA hardware in the loop.
This is experimental software. The covered paths execute real kernels and check real answers. Unsupported paths are expected to fail explicitly. That is a better failure mode than silently computing nonsense.
CuMetal requires macOS 14 or newer on Apple Silicon. Install the source-first compiler and runtime from the official CuMetal tap:
brew install lulzx/tap/cumetal
cumetalc vectorAdd.cu -o vectorAdd
./vectorAddVerify the complete local toolchain with:
cumetal doctorHomebrew installs CMake and LLVM as dependencies. Apple's Metal compiler still
comes from Xcode; if xcrun --find metal fails, install Xcode and its Metal
Toolchain component.
The formula deliberately installs the source-first compiler/runtime without
the optional libcuda.dylib binary shim.
You need:
- macOS 14 or newer
- an Apple M-series GPU
- CMake and the Xcode command-line tools
- Apple's Metal toolchain (
xcrun metalandxcrun metallib)
Build it:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(sysctl -n hw.ncpu)"
bash scripts/ci_report.sh build --exclude-regex '^bench_'One command that climbs from vector-add through reduction, SGEMM, and a path
tracer, refusing any stage that lacks device=apple_gpu provenance:
bash demos/apollo/run.shOptional: bash demos/apollo/run.sh --full adds the llm.c GPT-2 FP32 gate.
Details, scope limits, and artifacts: demos/apollo/README.md.
./build/cumetalc samples/vectorAdd/vectorAdd.cu -o /tmp/vectorAdd
CUMETAL_TRACE_GPU=1 /tmp/vectorAddThe program should print a numerical PASS. The trace should contain a
CUMETAL_PROVENANCE record with device=apple_gpu and
launch_success=true. A correct number without GPU provenance is not proof of
GPU execution.
Runtime-compiled MSL preserves Metal's fast-math default. Set
CUMETAL_MSL_MATH_MODE=safe to request safe Metal math for JIT-compiled source;
GPU provenance reports the selected math_mode. Precompiled metallibs retain
the policy used when they were built.
Install it without changing shell startup files:
bash install/install.sh build /opt/cumetal
/opt/cumetal/bin/cumetal doctorTo also add CuMetal to your shell's PATH, opt in explicitly:
bash install/install.sh build /opt/cumetal --shell-configRemove the installation with /opt/cumetal/uninstall.sh. The uninstaller uses
the recorded CMake install manifest, so every installed header, tool, shim, and
library is covered.
Source-built programs need no launcher:
cumetalc samples/vectorAdd/vectorAdd.cu -o vectorAdd
./vectorAddcumetal run is a convenience for launching a process with this installation's
runtime library path scoped to that child:
cumetal run ./cuda-applicationIt does not make unsupported binaries portable. Prebuilt CUDA applications
still require a compatible PTX payload and an installation configured with
CUMETAL_ENABLE_BINARY_SHIM=ON; SASS-only applications remain unsupported.
CuMetal is source-first.
CUDA C++ ---> Clang / NVVM ---> typed CuMetal IR --+
|
PTX --------> legacy or typed lowering -------------+--> MSL
|
v
Apple Metal toolchain
|
v
.metallib
|
v
libcumetal -> Metal GPU
There are three entry paths:
- Source recompilation.
cumetalccompiles a.cufile into a runnable executable or a.metallib. This is the primary path. - PTX compatibility. CuMetal parses PTX, builds CFG/SSA, and lowers it toward the same Metal backend. This path currently covers more project-scale CUDA than the typed source path.
- Binary compatibility. An opt-in
libcuda.dylibalias accepts programs already linked against the CUDA Driver API and handles supported fatbinary registration. It is useful, narrower than CUDA, and not the architecture.
The SIMD width is 32. This is fixed. CUDA warp semantics are lowered onto Metal SIMD-group operations; CuMetal does not pretend the machine has a different warp size because that would make every hard problem harder.
Metal calls stay behind the Objective-C++ boundary in
runtime/metal_backend/. CUDA-facing headers are clean-room. No private Apple
API is used.
cumetalc can emit every useful stage:
cumetalc kernel.cu --emit=cumetal-ir -o kernel.cumetal
cumetalc kernel.cu --emit=msl -o kernel.metal
cumetalc kernel.cu --emit=metallib -o kernel.metallib
cumetalc kernel.cu --emit=exe -o kernelImportant switches:
| Switch | Meaning |
|---|---|
--backend=cumetal-ir|legacy |
Select the typed shared-IR backend or the compatibility backend. There is no silent fallback. |
--cuda-device |
Ask a CUDA-capable Clang to produce PTX before CuMetal lowering. |
--entry NAME |
Compile one kernel and its reachable device-call closure. |
--ptx-strict |
Reject unsupported PTX instead of tolerating it. |
--fp64=native|emulate|warn |
Choose the FP64 policy. Default: emulate. |
--save-temps |
Keep link intermediates. |
The default backend follows the input because measured coverage says it should:
| Input corpus | legacy |
cumetal-ir |
|---|---|---|
direct .cu |
0/19 | 10/19 |
.cu --cuda-device / PTX |
17/19 | 6/19 |
Direct .cu therefore defaults to cumetal-ir; PTX and --cuda-device
default to legacy. This is engineering, not ideology. When the measurements
change, the default should change.
The complete compiler boundary is in docs/compiler-architecture.md. Unsupported instructions, calls, pointer conversions, CFGs, and ABI forms are tracked in docs/known-gaps.md.
libcumetal.dylib implements the CUDA Runtime and Driver API over Metal. It
tracks allocations, resolves CUDA pointers to Metal buffers, preserves the
per-thread error model, and maps streams and events onto command queues and
shared-event ordering.
The same library also exports compatibility surfaces for:
- cuBLAS and cublasLt
- cuRAND
- cuFFT
- cuSPARSE
- cuSOLVER Dense
- cuDNN
- CUDA Graphs
- NVML
- NCCL single-rank operations
- a small CPU-backed Thrust surface over unified memory
- async allocation, texture, and surface object lifecycle APIs
These names do not imply full NVIDIA parity. Some operations use MetalPerformanceShaders, some use Accelerate, some exploit unified memory, and some are deliberately partial. Read docs/status.md before building on one.
Large cudaMalloc allocations use MTLHeap suballocation at 4 MiB and above.
Override this for diagnosis:
CUMETAL_MTLHEAP_ALLOC=1 ./program # always
CUMETAL_MTLHEAP_ALLOC=0 ./program # neverBy default, Release builds keep source registration enabled and the
libcuda.dylib alias disabled. Enable the alias explicitly:
cmake -B build-shim \
-DCMAKE_BUILD_TYPE=Release \
-DCUMETAL_ENABLE_BINARY_SHIM=ON
cmake --build build-shimCUMETAL_ENABLE_CUDA_REGISTRATION=ON is the host registration ABI emitted by
Clang for recompiled .cu programs. It is part of the source path.
CUMETAL_ENABLE_BINARY_SHIM=ON only adds the drop-in libcuda.dylib name.
Do not confuse them.
The shim recognizes CMTL envelopes, raw PTX, and basic FatBinary/FatBinary2/FatBinary3 PTX wrappers. It does not execute SASS and does not understand every NVCC fatbinary variant.
Registered PTX is compiled on first use and cached under:
$CUMETAL_CACHE_DIR/registration-jit/
The key includes the input, kernel, lowering policy, schema versions,
toolchain-dependent inputs, and the libcumetal Mach-O UUID. Set
CUMETAL_DEBUG_REGISTRATION=1 to see format detection, compilation, cache hits,
ABI inference, and registration.
The legal boundary is documented in docs/legal-notice.md.
Vector add, SAXPY, reduction, matrix operations, atomics, shared memory, warp operations, streams, events, and selected CUDA library calls have numerical GPU tests. The suite includes negative cases because accepting a program is not the same as implementing it correctly.
The Phase 5 gate compares CuMetal with hand-written Metal for three memory-bound kernels. On an Apple M4 Pro, rebuilt and measured on 2026-07-27:
| Kernel | Elements | CuMetal / native Metal |
|---|---|---|
| vector add | 262,144 | 1.063× |
| SAXPY | 262,144 | 1.036× |
| FP32 reduction | 262,144 | 1.008× |
The gate uses the fastest of 20 synchronized wall-clock iterations. These kernels take roughly 0.2 ms, so averages mostly measure scheduler interference. The target is at most 2× native Metal, not a suspicious claim that translated code beats the baseline.
Reproduce it:
ctest --test-dir build -R bench_phase5_all_kernels --output-on-failure- Upstream
cuda-samplesvector add builds without source changes and passes a numerical plus Apple-GPU provenance gate. - llm.c GPT-2 FP32 passes logits, loss, tensor, and GPU-provenance checks on the tested path. It uses explicit workload specializations and is not proof of arbitrary PTX support.
- llama.cpp's unmodified GGML CUDA backend builds against CuMetal. SmolLM2-135M greedy decoding was coherent from one-layer offload through saturation on the verified Apple M4 Pro setup. FlashAttention is advertised as unsupported, so llama.cpp selects its ordinary attention path.
- A reduced PhysX 5.6 GRB path runs selected sphere, box, convex, and triangle mesh contacts on the GPU. It is a selected-shape conformance target, not general PhysX GPU support.
Exact commands, models, tolerances, provenance requirements, results, and scope boundaries live in docs/apple-gpu-execution.md, docs/testing.md, and docs/known-gaps.md. Compatibility claims without those conditions are meaningless.
- No dynamic parallelism.
- No multi-GPU or peer-to-peer execution.
- No OpenGL, Vulkan, or DirectX interop.
- No SASS execution.
- Multi-block cooperative launch/grid sync is rejected; single-block cooperative launch is supported.
- FP64 register emulation provides about a 44-bit mantissa, not IEEE binary64; unsupported binary64 memory/conversion boundaries fail compilation.
- Texture and surface object lifecycle exists; general device-side sampling does not.
- Device
printfuses a bounded buffer and limits format strings to 256 bytes. - CUDA, cuDNN, PhysX, llama.cpp, and PTX coverage is incomplete.
This list is intentionally short. The authoritative list is docs/known-gaps.md.
Run both the normal source-first build and the binary-shim build:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
bash scripts/ci_report.sh build --exclude-regex '^bench_'
cmake -B build-shim \
-DCMAKE_BUILD_TYPE=Debug \
-DCUMETAL_ENABLE_BINARY_SHIM=ON
cmake --build build-shim
bash scripts/ci_report.sh build-shim --exclude-regex '^bench_'For focused work:
ctest --test-dir build -R unit_ --output-on-failure
ctest --test-dir build -R functional_ --output-on-failure
ctest --test-dir build -R conformance_ --output-on-failureReport passes, skips, and failures separately. A registered test is not a passing test. A skip is not evidence of compatibility. A correct answer without GPU provenance may be a CPU fallback. The test policy exists because all three mistakes have happened before.
The hosted and Apple-GPU CI definitions are temporarily disabled and retained
as .github/workflows/*.yml.disabled for later re-enablement. The equivalent
local test selections and runner contract remain documented in
docs/testing.md.
| Tool | Job |
|---|---|
cumetalc |
Compile .cu, PTX, or NVVM IR to inspectable stages, .metallib, or an executable |
cumetal |
Check an installation with doctor or launch a child process with run |
air_inspect |
Inspect kernels, bitcode offsets, and metadata in a .metallib |
air_validate |
Validate .metallib structure and optionally check it with xcrun |
cumetal-air-emitter |
AIR research and regression container generation |
cumetal-ptx2llvm |
Legacy PTX-to-LLVM inspection |
ptx_diff |
Compare PTX-related outputs |
cumetal_bench |
Compare covered CuMetal kernels with native Metal |
- Design specification — canonical architecture and semantics
- Current status — what is implemented
- Known gaps — what is partial, wrong, or absent
- Build guide — toolchains and validation
- Testing guide — gates and conformance workflows
- Compiler architecture — lowering paths and migration boundaries
- Apple-GPU execution record — evidence and provenance
- AIR ABI notes — metallib research and limitations
- Correctness audit — failures found by testing the tests
If the README and the spec disagree, the spec wins.