Repository navigation
feat(cuda): run Windows CUDA builds from a relocatable runtime bundle - #2651
Conversation
A Windows --features cuda build only ran where the toolkit and cuDNN sat at their build-time paths. This mirrors the ROCm bundle design (#2606/#2607) for CUDA. The CUDA overlay's delay-load hook now picks one runtime directory per process through the pure choose_cuda_runtime_dir() in the new runtime_dir.h: MLXCEL_CUDA_RUNTIME_DIR (absolute, must hold the runtime, no fallback), else the executable directory when it holds the CUDA runtime DLL named in the binary's own delay-import table (cuBLASLt, since nvcc links the static cudart), else the unchanged build-time per-library lookup. In a bundle the DLLs the libraries open by plain name (NVRTC and its builtins, cuBLAS, cuBLASLt, nvJitLink, the cuDNN sub-libraries) must be present and are preloaded from it, and a load failure raises the delay-load exception with the expected path instead of falling back to PATH. jit_module.cpp takes NVRTC headers, CCCL and CUTLASS from the bundle's include, ranked after CUDA_HOME and ahead of CUDA_PATH. scripts/package_windows_cuda_runtime.py stages or zips the bundle (DLL closure derived statically from import tables, the JIT header set, CCCL and CUTLASS from the build's _deps, OpenBLAS, and the CUDA, cuDNN, CCCL and CUTLASS licenses) and fails naming every missing file. Validation: host test via make verify-cuda-runtime-dir, 25 unit tests for the script, an x64 CUDA 13.4.3 release build, the bundle staged from the real archives, and each JIT header compiled with NVRTC against only the bundle's include. No NVIDIA GPU was available, so runtime loading past device initialization is unverified. Refs #2640
A bundle without cublasLt64_13.dll does not qualify as a runtime directory, so the hook fell back to the build-time toolkit and, with the toolkit gone, failed with no message. When that lookup fails and the executable's directory still holds CUDA DLLs, the hook now names the marker path it expected there. The build-time path is otherwise unchanged and still falls back to the loader's own search. looks_like_runtime_bundle() is pure and covered by the host test. Verified with a harness that links this delayload.cpp: removing the marker from a staged bundle with the toolkit hidden prints the expected path. Refs #2640
…eaders
Two review findings on the Windows CUDA runtime bundle.
cuSPARSE is imported only by cuSOLVER, so a bundle without it failed with a message naming cusolver64_12.dll ("a DLL it imports may be missing") instead of the missing file. runtime_dir.h now lists import-only DLLs (kImportOnlyPrefixes, cuSPARSE) in the up-front check, so the binary names the expected cusparse64_*.dll path before it loads any CUDA DLL. They are checked, not preloaded.
The bundle's include/ was added as an NVRTC include path ahead of the CUDA header directory, so with CUDA_HOME set NVRTC still took every CUDA header from the bundle, contrary to the documented order (CUDA_HOME, bundle, CUDA_PATH, baked). With CUDA_HOME set the bundle's include/ now goes last and only supplies CUTLASS/CuTe. Linux and builds without a bundle produce the same include paths as before.
Verified with the host test (new cuSPARSE case), the packaging tests, an x64 CUDA release build, and the hook harness: a bundle without cuSPARSE now fails naming its path; bundle, override, error and developer-layout runs behave as before.
Refs #2640
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
A trailing separator in MLXCEL_CUDA_RUNTIME_DIR (C:\b\bin\) made parent_path() return the directory itself, so jit_module.cpp looked for the bundle headers in bin\include instead of the include\ beside bin\ and fell back to CUDA_PATH or the build-time toolkit, pairing the bundle's NVRTC with other headers. choose_cuda_runtime_dir() now resolves . and .. and drops a trailing separator; the host test covers both spellings and fails without the change. The baked-mode cuDNN log line converted cudnn_bin_dir() through the ANSI code page inside the delay-load hook's static initializer, where an exception must not escape; it is now guarded. docs/installation.md no longer implies that no CUDA-related load reaches the current directory: the libraries open nvcuda.dll, nvcudart_hybrid64.dll, zlibwapi.dll, cask_profile_interface.dll and nvtileiras64.dll themselves (and the optional cudnn_ext and cudnn_engines_tensor_ir when a bundle lacks them), and those follow the standard search order. Verified with the host test, the 25 packaging tests, the env-var docs check, an x64 CUDA release build and the hook harness (exe-dir bundle, trailing-separator override, wrong-folder override). Refs #2640
Security and performance reviewNo CRITICAL or HIGH findings. Fixed in d73cb01:
Checked and fine: the regular-import closure of every bundle DLL stays inside the bundle or the system; no fallback after a bad override or an incomplete bundle; the upstream return-null path is reachable only in the build-time layout; the env override and exe-dir candidates add no planting surface beyond the application directory Windows already searches first; NVRTC include order; the packaging script's destinations come from fixed names, with no traversal, and the zip is written through Remaining (follow-up, not fixed here):
Verification: host test, 25 packaging tests, env-var docs check, x64 |
Add a row to the Windows current-limits table for the CUDA runtime directory and bundle (#2640). It lists what was checked by building and host tests, and what has not run on an NVIDIA host: the statically derived DLL list, the DLLs a generate run loads from the bundle, and everything after device initialization. Validation: check_env_var_docs.py OK, cargo fmt --all --check OK, tests.test_script_executable_modes and tests.test_package_windows_cuda_runtime (26 tests) OK. Refs #2640
Summary
Windows
--features cudabinaries now load every CUDA, cuDNN and NVRTC DLL from one runtime directory and can run from a relocatable bundle, following the ROCm design of #2606/#2607. No NVIDIA GPU was available, so this PR is verified by building and by GPU-less runtime checks only (see below); runs on an NVIDIA host are still open.overlays/cuda/.../runtime_dir.h(new, header-only, host-tested):choose_cuda_runtime_dir()picksMLXCEL_CUDA_RUNTIME_DIR(absolute, must hold the runtime, no fallback), else the executable directory when it holds the CUDA runtime DLL named in the binary's own delay-import table, else the build-time layout. nvcc links the static cudart, so the marker iscublasLt64_13.dll.delayload.cpp: in a bundle, the DLLs the libraries open by plain name (nvrtc64_*,nvrtc-builtins*,cublas64_*,cublasLt64_*,nvJitLink_*, seven requiredcudnn_*sub-libraries;cusparse64_*checked only) are checked before any CUDA DLL loads and preloaded by full path; a failed load raises the delay-load exception naming the expected path instead of returning null. The build-time path keeps upstream's per-library lookup and its fallback; it only adds the[mlx-cuda] CUDA runtime:line, plus a message naming the missing marker path when a bundle-looking exe directory lacks it.jit_module.cpp: NVRTC headers, CCCL and CUTLASS come from the bundle'sinclude\(in or beside the runtime directory), ranked afterCUDA_HOMEand ahead ofCUDA_PATH.scripts/package_windows_cuda_runtime.py: stages or zipsbin\(DLLs + OpenBLAS),include\(JIT headers, CCCL from the build's_deps, CUTLASS),licenses\(CUDA EULA, cuDNN, CCCL, CUTLASS); fails naming every missing DLL, header or license. Licenses are told apart by content, because the redistributable archives all ship aLICENSEthat overwrites the others when unpacked into one directory.make verify-cuda-runtime-dir(inmake verify), docs ininstallation.mdandenvironment-variables.md, overlay README record.Decisions and open points
include\(needs no GPU). That addedcuda_runtime.h,mma.handcrt/mma.h(pp), which CUTLASS includes. Several of these headers (cooperative_groups.h,math_constants.h,cuda_runtime.h,mma.h, ...) are not on the CUDA EULA Attachment A list; the docs say so and advise confirming terms with NVIDIA before publishing a bundle. Prebuilt archives stay out of scope.delayload.cppis 595 lines, over the 500-line guideline; it is an overlay of an upstream file (likejit_module.cppat 805), so it was not split.Changes during review
CUDA_HOMEnow really outranks the bundle's headers: the bundleinclude\was placed ahead of the chosen CUDA header dir on the NVRTC include list, so NVRTC took every CUDA header from the bundle even withCUDA_HOMEset. WithCUDA_HOMEset the bundle include now goes last and only supplies CUTLASS/CuTe (implementation review, HIGH).cusparse64_*, which only cuSOLVER imports, is checked before any CUDA DLL loads, so a missing cuSPARSE is reported under its own name instead of as cuSOLVER (implementation review).MLXCEL_CUDA_RUNTIME_DIRis normalized (./.., trailing separator) before use;C:\b\bin\previously made the header lookup miss the bundle'sinclude\and fall back toCUDA_PATH(security review).nvcuda.dll,nvcudart_hybrid64.dll,zlibwapi.dll,cask_profile_interface.dll,nvtileiras64.dlland the two optional cuDNN sub-libraries by plain name; the bundle neither ships nor checks them, so the standard search order (application directory, System32, then current directory andPATH) applies, as it does in developer builds. Closing that needs a decision and is left to the DLL-planting follow-up (fix(security): Close remaining ROCm DLL and JIT-cache planting paths #2641).CUDA_HOMEnow counts as unset.Test plan
Toolkit: NVIDIA redistributables for windows-x86_64, CUDA 13.4.3 (cudart/nvcc/crt/nvrtc/nvjitlink/nvvm 13.4.92, cccl 13.3.4.3.1, cublas 13.8.0.4, cufft 12.4.0.43, cusolver 12.3.5.3, cusparse 12.8.6.72) and cuDNN 9.27.0.42 for CUDA 13, sha256-checked. Host: Windows 11 x64, Radeon 8060S, no NVIDIA GPU.
cargo build --release --features cuda(MLX_CUDA_ARCHITECTURES=120): cold build 32 min, rebuilt after the review fixes (17 min 37 s,mlxcel.exe133,756,928 bytes) with the harness re-run, no build-glue change needed;mlxcel.exe128 MiB,mlxcel-server.exe118 MiB.llvm-readobj --coff-imports: cuBLASLt, cuFFT, cuSOLVER, cuDNN and NVRTC are DelayImport, no CUDA DLL in the regular import table.cargo clippy --release --features cuda -- -D warnings(x64),cargo fmt --all --check,cargo test -p mlxcel-mlx-pin --lib(49 passed).make verify-cuda-runtime-dirhost test (clang++), checked to fail on mutated selection logic; 25 unit tests intests/test_package_windows_cuda_runtime.py; env-var docs check; license header check.LICENSEas the CUDA EULA.delayload.cppunchanged callscublasLtGetVersion,nvrtcVersion,cufftGetVersionandcudnnGetVersionthrough delay-loaded DLLs. With the toolkit renamed away andCUDA_PATHunset, every CUDA/cuDNN/NVRTC module loads from the bundle (exe directory andMLXCEL_CUDA_RUNTIME_DIR),nvcuda.dllfrom System32; relative and wrong-folder overrides, a removed delay-loaded DLL, a removedcudnn_opsand a removed marker each fail with the expected path. With the toolkit present the harness loads from the toolkit as before.mlxcel generateandmlxcel-serverfrom the bundle (toolkit hidden) and the developer build both stop with 0xC0000409 at the first device query afterDetected 1 GPU(s)., before any delay-loaded CUDA DLL is requested (this host has a stalenvcuda.dllbut no NVIDIA GPU). Everything after device initialization is unverified.Closes #2640