feat: Native Windows + CUDA build feasibility spike and porting plan (x86_64-pc-windows-msvc)
- Dominant language
- Rust
- Stars
- 467
- Forks
- 54
- Avg merge
- 4h 25m
- Merged PRs (30d)
- 310
Description
> Related to #25 (broad Windows + Linux x86_64 release-matrix work). This issue is the narrower native-Windows + CUDA build feasibility spike that de-risks the Windows portion of #25.
## Goal
Produce a working **native Windows** build of `mlxcel` and `mlxcel-server` for **single-node CUDA inference** on NVIDIA GPUs via the existing `cuda` feature, targeting `x86_64-pc-windows-msvc`. This issue is both a **feasibility spike** (the first phase is a hard go/no-go gate) and, if the gate passes, the **porting plan** to a functional end-to-end binary.
This is not a supported configuration today. `docs/installation.md` lists Windows as "not documented here", the release workflow has no Windows job, and the CUDA build glue in `build.rs` is written for Linux. The binding constraint is upstream maturity: the MLX C++ engine we statically link now builds and CI-tests on Windows for **CPU**, and its CUDA backend ships Windows delay-load plumbing, but the Windows+**CUDA** path is not CI-validated upstream (plumbed yet unproven, see Layer 1).
## Scope premise — distributed inference is OFF on Windows initially
Native Windows starts **single-node only**. Distributed inference — both **tensor parallelism and pipeline parallelism** — is **disabled from the outset**, not a stretch goal or a "nice to have if it falls out." The deliverable for this issue is single-host CUDA inference (one box, one or more local GPUs) with the distributed path compiled out on Windows.
This is a deliberate premise, not an accident of scoping. The `src/distributed/*` transport is built on Unix networking primitives (io_uring / kqueue / `RawFd`, RDMA capability probes) that are not portable to Windows without separate, substantial work; pulling that into the first build would expand the spike well past its go/no-go purpose. Restoring distributed support on Windows is explicitly **out of scope** here and tracked as a Phase 6 follow-up. Everything below — the blocker analysis, phases, and acceptance criteria — assumes this premise.
## Context
`mlxcel` links MLX C++ statically. The MLX source is fetched via `FetchContent` and pinned in `src/lib/mlx-cpp/CMakeLists.txt:92-95` (commit `a6ec7123dac814417147e21d4aeed694924ddd4d`, 2026-06-11 upstream `main`). The same commit is asserted in `src/lib/mlxcel-core/build.rs:178` (`MLX_EXPECTED_COMMIT`). `MLX_BUILD_CUDA` defaults to `OFF` at `build.rs:231` and is set to `ON` for the `cuda` feature in `build.rs:250-275`.
The blockers split into three layers, ordered by how fundamental they are.
### Layer 1 — Upstream MLX builds on Windows; Windows+CUDA is plumbed but not CI-validated
This was originally framed as a hard gate ("upstream MLX does not build on Windows"). At the current pin that framing is outdated:
- **Windows CPU is supported and CI-tested upstream.** `build_and_test.yml` has a `windows_build_and_test` job (`windows-2025`, MSVC `cl` + Ninja, Release). [ml-explore/mlx#1513](https://github.com/ml-explore/mlx/issues/1513) "Windows support" was **closed 2024-12-28**. The CPU blockers previously listed here are resolved at this pin: CMake FetchContents a prebuilt OpenBLAS x64 zip and ships `libopenblas.dll` (no manual OpenBLAS path needed), the `dlfcn-win32` shim is fetched, and GGUF is disabled under MSVC (mlxcel uses safetensors, so this is irrelevant).
- **The CUDA backend ships explicit Windows delay-load plumbing.** `mlx/backend/cuda/CMakeLists.txt` links `delayimp.lib`, compiles `delayload.cpp`, and adds `/DELAYLOAD:` for every CUDA/cuDNN DLL. It handles the **static-library case** specifically (`PUBLIC` + `$`), which is exactly how mlxcel links MLX. `delayload.cpp` resolves the DLLs at runtime from `MLX_CUDA_BIN_DIR` / `MLX_CUDNN_BIN_DIR`.
- **But there is no Windows+CUDA CI job.** The CUDA CI (`cuda_build_and_test`) is Linux-only (`gpu-t4` runners, `cuda-12.6` / `cuda-12.9`) on both our pin and latest `main` (`51b2768`, 2026-06-21). So Windows+CUDA is **plumbed yet unproven** upstream, not a hard build gate.
The earlier upstream context that informed this issue ([PR #1983](https://github.com/ml-explore/mlx/pull/1983) CUDA DLL delay-loading, [Discussion #2422](https://github.com/ml-explore/mlx/discussions/2422)) has landed as the delay-load mechanism above.
**Implication:** Phase 1 (build vanilla MLX+CUDA on Windows standalone) is still the gate, but its likelihood of passing is much higher than the original framing. The open questions narrow to whether the `nvcc` + MSVC host build compiles end-to-end, and the static-link delay-load (Layer 2). A documented "not feasible without unacceptable local patching" remains a valid outcome.
### Layer 2 — `mlxcel` build glue is Linux-only
Even with MLX building, `src/lib/mlxcel-core/build.rs` assumes Linux for CUDA:
- `link_cuda()` (`build.rs:342-375`) searches `CUDA_HOME/lib64` or `/usr/local/cuda/lib64`, probes a `stubs/` subdir, and links `.so`-style names (`cudart`, `cublas`, `cublasLt`, `cufft`, `cuda`, `cudnn`, `nvrtc`). Windows CUDA layout is `%CUDA_PATH%\lib\x64`, import libs are `*.lib`, and there is no `stubs/`. Needs a `cfg(windows)` branch.
- The CMake CUDA compiler hint (`build.rs:255-259`) falls back to `/usr/local/cuda/bin/nvcc`. On Windows this is `nvcc.exe` under `%CUDA_PATH%\bin`.
- CPU-backend BLAS is linked inside `#[cfg(target_os = "linux")]` (`build.rs:127-133`: `stdc++`, `openblas`, `lapack`). On Windows MLX links its own fetched OpenBLAS, so this block must not run; mlxcel instead bundles the `libopenblas.dll` MLX produces.
- The cxx-bridge compile flags are GCC/Clang syntax: `-std=c++20` (`build.rs:62`), `-O3` / `-ffast-math` (`build.rs:73-75`), `-march=native` (`build.rs:84-92`), `-flto` (macOS-gated). Under MSVC, `flag_if_supported` silently drops all of these, so MLX's required C++20 standard is never set and optimizations are lost. Need MSVC equivalents (`/std:c++20`, `/O2`, `/fp:fast`).
- **Static-link delay-load must be replicated.** MLX sets `delayimp.lib`, `delayload.cpp`, and `/DELAYLOAD:*` as `PUBLIC` / `BUILD_INTERFACE` on the `mlx` CMake target (Layer 1). mlxcel links MLX via cargo, not CMake, so these do not propagate to the rustc link step. `build.rs` must replicate them (link `delayimp.lib`, compile `delayload.cpp`, pass `/DELAYLOAD` args, define `MLX_CUDA_BIN_DIR` / `MLX_CUDNN_BIN_DIR`), or build MLX as a shared lib and ship `mlx.dll`.
### Layer 3 — Rust code has Unix dependencies
`libc` is a `[target.'cfg(unix)'.dependencies]` entry (`Cargo.toml:169-173`), so it is absent on Windows. An `rg` sweep (2026-06-25, counting code references to `std::os::unix` / `std::os::fd`, `io_uring`, `kqueue`, `RawFd` / `AsRawFd`, `memmap2`, `libc::`, `nix::`) finds **~79 Unix-API usage sites**: **39 in `src/distributed/`** and **40 outside, of which ~20 are test-only** (`*_tests.rs`), leaving a non-distributed production surface of roughly **20 sites**. The split that matters is by severity, not count: every hard Windows-hostile primitive (`io_uring`, `kqueue`, RDMA, Unix sockets) is **inside `src/distributed/`**, with zero outside it. The non-distributed Unix usage is mild file-descriptor hygiene (`libc::O_NOFOLLOW`, `fcntl` `FD_CLOEXEC`, `lseek` via `AsRawFd`) plus cross-platform `memmap2` (not a blocker). Outside the distributed module:
- `src/distributed/*` (RDMA, tensor/pipeline parallel transport): the bulk, compiled out on Windows for the initial scope.
- `src/server/media.rs` (already `#[cfg(unix)]`-gated at `:324`), `src/multimodal/video.rs` (multiple `#[cfg(unix)]`).
- `src/downloader/`, `src/models/sanitize.rs`, `src/lib/mlxcel-core/src/weights.rs` (its only flagged Unix site is a test-only `std::os::unix::fs::symlink`; the production safetensors mmap uses cross-platform `memmap2`).
Only one file is Windows-aware today: `src/distributed/rdma_capabilities.rs:39` (`cfg!(target_os = "windows")`). The distributed path is deeply tied to Unix networking primitives and is **compiled out on Windows for the initial scope** (see the Scope premise), then revisited as a Phase 6 follow-up.
## Prerequisites to prepare (toolchain)
- [ ] Visual Studio 2022 with the "Desktop development with C++" workload (MSVC v143). CUDA's `nvcc` on Windows requires the MSVC `cl.exe` host compiler — MinGW/GNU is not a CUDA-supported host.
- [ ] CUDA Toolkit 12.x or newer (MLX requires >= 12.0; our Linux releases target CUDA 13). Includes `nvcc`, cudart, cublas, cufft, nvrtc.
- [ ] cuDNN matching the CUDA version.
- [ ] NVIDIA driver >= 550.
- [ ] CMake (latest), Git, and Git Bash (in case MLX's shell-based codegen scripts are still on the CUDA path).
- [ ] Rust toolchain with the `x86_64-pc-windows-msvc` target (NOT the GNU target — must link MSVC-built CUDA/MLX libs).
- [ ] OpenBLAS for Windows is fetched automatically by MLX's CMake at this pin, so no manual vcpkg setup is required for the BLAS path.
## Phased plan
### Phase 0 — Environment
- [ ] Stand up a Windows host with an NVIDIA GPU and the toolchain above.
- [ ] Confirm `nvcc --version`, `cl.exe`, `cmake --version`, `rustc --version --verbose` (host = msvc), `nvidia-smi`.
### Phase 1 — GATE: build vanilla MLX on Windows+CUDA (standalone, no mlxcel)
- [ ] Check out MLX at pinned commit `a6ec7123dac814417147e21d4aeed694924ddd4d` and attempt a CUDA build with `-DMLX_BUILD_CUDA=ON` directly (no mlxcel).
- [ ] The CPU-layer blockers (OpenBLAS detection, `dlfcn`) are resolved upstream at this pin; focus on what remains unproven: whether the `nvcc` + MSVC host build compiles end-to-end, and CUDA/cuDNN DLL delay-load.
- [ ] **Decision point:** record whether vanilla MLX+CUDA can be built (with how many local patches), or whether this is blocked pending upstream. If blocked, document evidence and stop; that is a valid close.
### Phase 2 — mlxcel build glue (`build.rs`)
- [ ] Add a `cfg(windows)` CUDA link branch: `%CUDA_PATH%\lib\x64`, `.lib` import-lib names, no `stubs/`.
- [ ] Add Windows nvcc path resolution (`%CUDA_PATH%\bin\nvcc.exe`).
- [ ] Skip the Linux CPU/BLAS link on Windows; rely on MLX's fetched OpenBLAS and bundle `libopenblas.dll`.
- [ ] Add MSVC compile flags for the cxx bridge (`/std:c++20`, `/O2`) so C++20 + optimization are actually applied.
- [ ] Replicate the static-link CUDA delay-load (`delayimp.lib` + `delayload.cpp` + `/DELAYLOAD` + `MLX_CUDA_BIN_DIR` / `MLX_CUDNN_BIN_DIR` defines), or build MLX as a shared lib and ship `mlx.dll`.
### Phase 3 — Rust platform gating
- [ ] Audit the Unix API usages (~79 sites, 39 in `src/distributed`, ~20 non-test sites outside it); `cfg`-gate or provide Windows fallbacks for the non-distributed ones, and confirm no `libc::` use leaks outside the distributed module.
- [ ] Compile out the distributed (tensor/pipeline parallel) path on Windows per the Scope premise; track the Phase 6 follow-up to restore it.
- [ ] Ensure `mlxcel-server` and CLI build with distributed gated off.
### Phase 4 — dependent C++ crates
- [ ] Verify `sentencepiece` (`Cargo.toml:77`, builds a C++ lib via CMake) compiles under MSVC. (`tokenizers`, `llguidance`, `toktrie` are pure Rust and should be fine.)
### Phase 5 — end-to-end smoke test
- [ ] `cargo build --release --target x86_64-pc-windows-msvc --features cuda` produces both binaries.
- [ ] `mlxcel --version` / `mlxcel-server --version`.
- [ ] `mlxcel download mlx-community/Qwen3-0.6B-4bit` then `mlxcel generate -m ... -p "Hello" -n 16` produces tokens on GPU.
- [ ] `mlxcel-server` serves a `/v1/chat/completions` request.
### Phase 6 — follow-ups (out of scope for first close, file separately)
- CI Windows job (CPU and, once proven, CUDA), packaging (bundle `libopenblas.dll` + CUDA/cuDNN/NVRTC DLLs per the runtime-deps doc below), optional code signing.
- Restore distributed features on Windows.
- Reconcile CUDA 12.x vs 13.x version matrix with the Linux release.
## Acceptance criteria
This issue is complete when **either**:
1. A native `x86_64-pc-windows-msvc` `--features cuda` build of `mlxcel` + `mlxcel-server` builds and passes the Phase 5 smoke tests on an NVIDIA Windows host (end-to-end functional **single-node** binary; per the Scope premise, distributed inference is compiled out on Windows and its absence does not block this issue — it is tracked as a Phase 6 follow-up); **or**
2. Phase 1 produces a documented go/no-go determination showing native MLX-on-Windows is not feasible at the pinned commit without unacceptable local patching, with the specific blockers and evidence recorded for a future re-attempt.
## Risks & fallback
- Upstream MLX Windows+CUDA is plumbed but not CI-validated; the residual risk is the `nvcc` + MSVC host build and the static-link delay-load, plus carrying any local MLX patches across the pinned-commit upgrade process.
- Distributed inference is unlikely to work natively on Windows initially.
- **Fallback already supported:** WSL2 (Ubuntu) + NVIDIA WSL CUDA driver runs the documented Linux CUDA path (`cargo build --release --features cuda`) with none of the above work. This issue exists specifically because a native `.exe` is wanted instead of the WSL2 path.
---
**Refresh log**
- 2026-06-22 — Refreshed against the current MLX pin (`a6ec712`) and code after a build-feasibility investigation.
- Pin updated: `84961223` (~v0.31.2) → `a6ec7123` (2026-06-11 `main`); `build.rs` and `CMakeLists.txt` line references updated throughout (`MLX_EXPECTED_COMMIT` build.rs:178, `link_cuda` :342-375, nvcc hint :255-259, Linux BLAS :127-133, cxx flags :62/73-75/84-92).
- Layer 1 reframed: upstream MLX now builds and CI-tests Windows CPU (`windows_build_and_test`; [mlx#1513](https://github.com/ml-explore/mlx/issues/1513) closed 2024-12-28); the CUDA backend ships Windows delay-load plumbing including the static-library case, but has no Windows+CUDA CI, so it is plumbed yet unproven rather than a hard gate.
- Layer 2: added the static-link delay-load replication item (CMake `PUBLIC` link options do not flow to the cargo/rustc link step).
- Layer 3: "~55 Unix usages" remeasured at 130 sites (124 in `src/distributed`); only `rdma_capabilities.rs:39` is Windows-aware today.
- Internal docs added this session: `lablup/mlxcel-internal` `docs_internal/platform/windows-cuda-build-considerations.md` (build feasibility / porting) and `docs/windows-cuda-runtime-deps.md` (runtime DLL packaging, ~1.0-1.3 GB bundle).
- 2026-06-25: Corrected the Layer 3 Unix-surface counts; pin and `build.rs` / `Cargo.toml` line references re-verified unchanged at HEAD `8432ada`.
- The 2026-06-22 figure of "130 sites (124 in `src/distributed`)" was not reproducible. At that same commit, and at today's HEAD, `src/distributed/` holds 39 Unix-API lines (45 occurrences), not 124. Remeasured with a precise sweep (code references only; `nix::` / `libc::` as crate paths; no `nix`-inside-`unix` substring inflation): ~79 sites total, 39 in `src/distributed/`, 40 outside of which ~20 are test-only, leaving a non-distributed production surface of ~20 sites.
- The qualitative conclusion is unchanged and in fact stronger: all hard Windows-hostile primitives (`io_uring`, `kqueue`, RDMA, Unix sockets) live in `src/distributed/`; the non-distributed surface is mild fd-hygiene (`libc::O_NOFOLLOW`, `fcntl` `FD_CLOEXEC`, `lseek` via `AsRawFd`) plus cross-platform `memmap2` (`models/sanitize.rs`, `weights.rs`), which is not a blocker.
- Re-verified unchanged at HEAD `8432ada`: pin `a6ec7123` (`build.rs:178`, `CMakeLists.txt:95`), `link_cuda` :343, nvcc hint :256-258, Linux BLAS :127-132, cxx flags :62 / :73-75 / :86-90, `Cargo.toml` libc :169-173 / sentencepiece :77 / memmap2 :82, `media.rs:324`, `rdma_capabilities.rs:39`.
Contributor guide
Assessment
This issue has not been assessed yet.