Skip to content

Fix CUDA backend Windows build under C++20 - #23135

Merged
Gasoonjia merged 3 commits into
mainfrom
gasoonjia/fix-windows-cuda-cxx20
Sep 25, 2026
Merged

Gasoonjia merged 3 commits into
mainfrom
gasoonjia/fix-windows-cuda-cxx20

Conversation

@Gasoonjia

@Gasoonjia Gasoonjia commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

All `test-model-cuda-windows-e2e` jobs on main fail to build:

```
backends\cuda\runtime\platform\platform.cpp(63,20): error C2440: 'initializing': cannot convert from 'std::basic_string<char8_t,...>' to 'std::basic_string<char,...>'
```

#22793 made `executorch_core` export `cxx_std_20`, so `cuda_platform` now compiles as C++20, where `std::filesystem::path::u8string()` returns `std::u8string`, which no longer converts to `std::string`.

Fix: load the library with `LoadLibraryW(path.c_str())` (which is also correct for non-ASCII paths), and build a UTF-8 `std::string` only for the error log. The conversion compiles under both C++17 and C++20.

Second fix: once platform.cpp built, aoti_cuda_shims failed next:

c10/util/safe_numerics.h(71): error : namespace "std" has no member "countl_zero"

The .cu shims are compiled by nvcc, and backends/cuda/CMakeLists.txt pinned CMAKE_CUDA_STANDARD 17. cxx_std_20 on executorch_core only raises the CXX language, so it never reaches nvcc. The CUDA backend now uses C++20 for both CXX and CUDA on Windows (matching the top-level rule) and stays on C++17 elsewhere.

Test plan

CI: `Test CUDA Windows Export and E2E` (triggered by the `backends/cuda/**` path filter).

executorch_core now exports cxx_std_20 (#22793), so cuda_platform compiles
as C++20 where std::filesystem::path::u8string() returns std::u8string and
no longer converts to std::string. This broke every test-model-cuda-windows-e2e
job.

Load the library with LoadLibraryW(path.c_str()) (also correct for non-ASCII
paths) and only build a UTF-8 std::string for the error log.
@pytorch-bot

pytorch-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/23135

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 7 Pending

As of commit 919ab09 with merge base 42b7709 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 24, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

The .cu shims include vendored c10's safe_numerics.h, whose MSVC-only branch
uses std::countl_zero / std::bit_width. The top-level build raises CXX to C++20
on Windows (and executorch_core carries cxx_std_20), but that does not reach
nvcc, and backends/cuda pinned CMAKE_CUDA_STANDARD to 17, so aoti_cuda_shims
failed to build on Windows. Match the top-level rule for both CXX and CUDA.
@Gasoonjia
Gasoonjia merged commit f626041 into main Sep 25, 2026
342 of 344 checks passed
@Gasoonjia
Gasoonjia deleted the gasoonjia/fix-windows-cuda-cxx20 branch September 25, 2026 07:58
Gasoonjia added a commit that referenced this pull request Sep 29, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The CUDA runtime is linked into the DLLs (CMake's default on Windows), so the wheel needs
the display driver and not the CUDA toolkit. nvidia publishes no CUDA runtime package for
Windows, so the wheel declares none; the Linux-marked requirements are unchanged.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset (MSB4023 in CUDA 13.0.targets), and the MSVC toolset cannot
  compile the Python extension's sources. A Windows CUDA build therefore uses
  Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses, and nvcc with cl.exe
  as its host compiler, the combination the CUDA Windows CI job builds with. The
  multi-config generator keeps the Release/ layout packaging reads. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- flatc / flatcc byproducts are declared with the executable suffix. The Visual Studio
  generator ignores them; Ninja on Windows had no rule producing flatc.exe.
- The runtime DLL makes its platform layer strong (ET_PAL_STRONG_SYMBOLS). A DLL resolves
  its own references at link time so a weak PAL cannot be overridden through it anyway,
  and the generated export list skips weak symbols, which left the shims importing
  et_pal_init from nowhere.
- The shims resolve the runtime from executorch.dll rather than a static core. (C++20 for
  the delegate and shims on Windows now comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.
- CI: build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket
  Windows CUDA=OFF (a CPU row is still forced off by the generic row classification) and
  resolves TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, since the Windows builder has no
  env-var script slot. The Arm Cortex-M Python module is off in that build: Ninja does not
  generate its two identically named sources as distinct rules.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no CUDA DLL imports cudart64_*.dll (only the driver), and no Windows CUDA requirement
  is declared;
- the shim layer's device code covers the row's TORCH_CUDA_ARCH_LIST;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows (`test_cuda_windows.py --export DIR`, which the
  CUDA Windows CI job's export step can produce) runs through the C++ SDK and matches
  eager; without artifacts it prints SKIP, as the Linux aarch64 rows do for execution;
- then everything a CPU Windows row checks: test_shared_libraries.py (now with the three
  CUDA ownership rows) and test_cpp_sdk.py.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120), VS 2022 BuildTools, CUDA 13.0, CPython 3.12:

- `CU_VERSION=cu130 TORCH_CUDA_ARCH_LIST=12.0 python setup.py bdist_wheel` ->
  executorch-1.6.0+cu130-cp312-cp312-win_amd64.whl, installed clean.
- In WSL (Ubuntu, torch 2.14.0+cu130, mingw-w64 and the Windows CUDA runtime from
  install_cuda_windows_cross_compile.sh), from this branch:
  `python .ci/scripts/wheel/test_cuda_windows.py --export C:\...\cuda-windows-artifacts`
  -> model.pte (4.9 MB, carrying the Windows kernel DLL) + aoti_cuda_blob.ptd.
- On Windows: `EXECUTORCH_CUDA_WINDOWS_ARTIFACTS=... python test_cuda_windows.py`: every
  check passes, including the Linux-exported program running on the GPU through
  executorch::backend_cuda and matching eager to 2.4e-7.
- CPU regression: the CPU wheel rebuilt from this branch (ClangCL, CUDA off) passes the
  full test_windows.py with the same results as #23121.
- The Windows CUDA CI job (cuda-windows.yml, static llm-release-cuda build) is unaffected:
  every Windows change in backends/cuda is behind EXECUTORCH_BUILD_SHARED.
- Linux: the CMake changes are behind WIN32 / MSVC or evaluate identically there (an
  empty CMAKE_EXECUTABLE_SUFFIX); setup.py's new path runs only on Windows; relying on the
  Linux wheel rows for confirmation.
Gasoonjia added a commit that referenced this pull request Oct 2, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu130, cu132, cu134), the same list the release rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. The wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 3, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu130, cu132, cu134), the same list the release rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The wheel build's own smoke test has no GPU
  program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 3, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The wheel build's own smoke test has no GPU
  program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 3, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The wheel build's own smoke test has no GPU
  program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 3, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The wheel build's own smoke test has no GPU
  program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 3, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 5, 2026
Stacked on #23121, which ships linkable libraries in the Windows wheel.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 5, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 5, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 6, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 6, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, when ninja is on PATH. Without it, as in a
  --no-build-isolation source install, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 6, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The delegate also
  imports CudaAllocator from there rather than compiling its own copy, which the static
  layout keeps: two copies would give the delegate pools that releasing cached memory
  through it never reaches.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives).
- The new CI end-to-end jobs could not be run before upload; they follow cuda-windows.yml's
  linux_job_v3 / windows_job pairing and the steps above.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 6, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The delegate also
  imports CudaAllocator from there rather than compiling its own copy, which the static
  layout keeps: two copies would give the delegate pools that releasing cached memory
  through it never reaches.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI: the end-to-end jobs pass for both published trains on the Windows A10G runner, with a
  Linux-exported program matching eager (maxdiff 1.3e-7 for 13.2, 2.4e-7 for 13.4).
- After the last review fixes (the delegate importing CudaAllocator from the shims, the
  generator check, the diagnostics guard), the cu132 wheel was rebuilt and passes all 52
  checks again, with the delegate DLL importing CudaAllocator from aoti_cuda_shims.dll.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 6, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The delegate also
  imports CudaAllocator from there rather than compiling its own copy, which the static
  layout keeps: two copies would give the delegate pools that releasing cached memory
  through it never reaches.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Trains the images do not
  carry are assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, with the
  newest also as PTX; the expected list comes from cuda_arch_list.sh keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run. The Arm Cortex-M
  Python module is not built with Ninja, so its install check is left out.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI: the end-to-end jobs pass for both published trains on the Windows A10G runner, with a
  Linux-exported program matching eager (maxdiff 1.3e-7 for 13.2, 2.4e-7 for 13.4).
- After the last review fixes (the delegate importing CudaAllocator from the shims, the
  generator check, the diagnostics guard), the cu132 wheel was rebuilt and passes all 52
  checks again, with the delegate DLL importing CudaAllocator from aoti_cuda_shims.dll.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 6, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The delegate also
  imports CudaAllocator from there rather than compiling its own copy, which the static
  layout keeps: two copies would give the delegate pools that releasing cached memory
  through it never reaches.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, so the root
  finds it first; a CPU torch does not define that target.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. A pull request without the
  ciflow/binaries/all label runs only PR_CUDA_VERSION, the same rule the build job applies, so
  the end-to-end jobs follow the one wheel built. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`), and a
  Windows GPU job builds the wheel with that train's nvcc, installs it and runs
  test_cuda_windows.py with the program (test_cuda_windows_e2e.ps1). Each train's CUDA is
  assembled from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own program, so one train
  failing to export does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, and the
  newest also as PTX; the expected list comes from executorch_cuda_arch_list in
  cuda_arch_list.sh, called the way the pre-build hook calls it and keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run, including the Arm
  Cortex-M Python module, which builds under Ninja too: CMSIS-NN's test-only shared library
  took the static library's name, so its import library collided with it on Windows, and
  backends/cortex_m now gives the import library its own name.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI: the end-to-end jobs pass for both published trains on the Windows A10G runner, with a
  Linux-exported program matching eager (maxdiff 1.3e-7 for 13.2, 2.4e-7 for 13.4).
- After the last review fixes (the delegate importing CudaAllocator from the shims, the
  generator check, the diagnostics guard), the cu132 wheel was rebuilt and passes all 52
  checks again, with the delegate DLL importing CudaAllocator from aoti_cuda_shims.dll.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 7, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   the stream helper     executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # stream helper and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The delegate also
  imports CudaAllocator from there rather than compiling its own copy, which the static
  layout keeps: two copies would give the delegate pools that releasing cached memory
  through it never reaches.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- The root CMake file no longer links the CUDA delegate into the Python extension on
  Windows; extension/cuda finds the CUDA toolkit itself, as on main.
- The package config adds the stream helper and the shim layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. The end-to-end jobs take their trains from
  the build job's own filtered matrix, so a pull request without the ciflow/binaries/all
  label tests the one train it builds, and a train the generator dropped is not tested
  against a wheel that does not exist. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`); a small
  job bundles it with the wheel the build job published for that train (windows_job.yml
  downloads one artifact and gives its script no token for a second); and a Windows GPU job
  installs that published wheel and runs test_cuda_windows.py with the program
  (test_cuda_windows_e2e.ps1), so CI runs a CUDA program through the artifact users install
  rather than a wheel rebuilt on the GPU runner. The export and end-to-end jobs check out the
  commit the build job builds (on a pull request, its merge commit), so the wheel, the
  program and the scripts that test them match. Each train's CUDA (on Linux for the export,
  and on Windows for the smoke test's cuobjdump and the model's CUDA runtime) is assembled
  from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own bundle, so one train
  failing does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, and the
  newest also as PTX; the expected list comes from executorch_cuda_arch_list in
  cuda_arch_list.sh, called the way the pre-build hook calls it and keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run, including the Arm
  Cortex-M Python module, which builds under Ninja too: CMSIS-NN's test-only shared library
  took the static library's name, so its import library collided with it on Windows, and
  backends/cortex_m now gives the import library its own name.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI: the end-to-end jobs pass for both published trains on the Windows A10G runner, with a
  Linux-exported program matching eager (maxdiff 1.3e-7 for 13.2, 2.4e-7 for 13.4).
- After the last review fixes (the delegate importing CudaAllocator from the shims, the
  generator check, the diagnostics guard), the cu132 wheel was rebuilt and passes all 52
  checks again, with the delegate DLL importing CudaAllocator from aoti_cuda_shims.dll.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 7, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   allocator + streams   executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # CUDA extension and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The allocator is not
  in either DLL: main moved it into extension_cuda (#23447), a shared library the
  delegate and the shims both import it from, so the process has one.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- The root CMake file no longer links the CUDA delegate into the Python extension on
  Windows; extension/cuda finds the CUDA toolkit itself, as on main.
- The package config adds the CUDA extension (allocator and stream helpers) and the shim
  layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. The end-to-end jobs take their trains from
  the build job's own filtered matrix, so a pull request without the ciflow/binaries/all
  label tests the one train it builds, and a train the generator dropped is not tested
  against a wheel that does not exist. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`); a small
  job bundles it with the wheel the build job published for that train (windows_job.yml
  downloads one artifact and gives its script no token for a second); and a Windows GPU job
  installs that published wheel and runs test_cuda_windows.py with the program
  (test_cuda_windows_e2e.ps1), so CI runs a CUDA program through the artifact users install
  rather than a wheel rebuilt on the GPU runner. The export and end-to-end jobs check out the
  commit the build job builds (on a pull request, its merge commit), so the wheel, the
  program and the scripts that test them match. Each train's CUDA (on Linux for the export,
  and on Windows for the smoke test's cuobjdump and the model's CUDA runtime) is assembled
  from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own bundle, so one train
  failing does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, and the
  newest also as PTX; the expected list comes from executorch_cuda_arch_list in
  cuda_arch_list.sh, called the way the pre-build hook calls it and keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run, including the Arm
  Cortex-M Python module, which builds under Ninja too: CMSIS-NN's test-only shared library
  took the static library's name, so its import library collided with it on Windows, and
  backends/cortex_m now gives the import library its own name.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI, before the rebase onto main: the end-to-end jobs passed for both published trains on
  the Windows A10G runner, with a Linux-exported program matching eager (maxdiff 1.3e-7 for
  13.2, 2.4e-7 for 13.4).
- After rebasing onto main with the allocator moved into extension_cuda (#23447), the cu132
  wheel was rebuilt and passes all 55 checks, including the new ownership row: a single CUDA
  allocator and stream helpers owned by executorch_extension_cuda.dll across the 15 shipped
  libraries. The rows above were measured before that rebase.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 7, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job already exports on
Linux and runs on Windows for exactly this reason. What was missing is a wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships (#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only the runtime links CUDA.

    lib/executorch_backend_cuda.dll    + .lib   the delegate          executorch::backend_cuda
    lib/executorch_extension_cuda.dll  + .lib   allocator + streams   executorch::extension_cuda
    backends/cuda/aoti_cuda_shims.dll            the AOTI shim layer   (runtime DLL of the delegate)
    data/lib/aoti_cuda_shims.lib                 already shipped as the lowering stub; also the
                                                 shim layer's import library here
    extension/pybindings/_C.pyd                  unchanged: no CUDA dependency, no CudaBackend

A C++ application links it exactly as on Linux:

    find_package(executorch REQUIRED COMPONENTS kernels_optimized backend_cuda)
    target_link_libraries(app PRIVATE executorch::runtime executorch::kernels_optimized
                                      executorch::backend_cuda)
    # plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now also brings the
    # CUDA extension and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler identification
  under the ClangCL toolset on some toolkit and Visual Studio pairs (MSB4023 in its targets
  file), and the MSVC toolset cannot compile the Python extension's sources. A Windows CUDA
  build therefore uses Ninja Multi-Config with clang-cl, the compiler the CPU wheel uses,
  and nvcc with cl.exe as its host compiler, in a Visual Studio developer environment that
  has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel build runs).
  Elsewhere, such as a plain shell, the build keeps the Visual Studio generator it always
  used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so options a user put in
  CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit is the one
  install_utils already reports, so what compiles is the train packaging declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's traditional
  preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host executable suffix chosen
  by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr toolchain sets
  .elf, and naming the byproduct after it left Ninja with no rule for the host tool.
- The shims resolve the runtime from executorch.dll rather than a static core. (The PAL's
  strong symbols are in #23121; C++20 for the delegate and shims comes from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as upstream sets it,
  and turn it ON in the shared layout: the delegate DLL imports the stream guard functions
  (get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry no
  AOTI_SHIM_EXPORT, so the annotations alone would not export them. The allocator is not
  in either DLL: main moved it into extension_cuda (#23447), a shared library the
  delegate and the shims both import it from, so the process has one.
- #23121 keeps a Windows CUDA build static; this drops that guard, so the pybind preset
  now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
  Windows packaging list.
- The root CMake file no longer links the CUDA delegate into the Python extension on
  Windows; extension/cuda finds the CUDA toolkit itself, as on main.
- The package config adds the CUDA extension (allocator and stream helpers) and the shim
  layer to the delegate's runtime
  DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28 route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the Windows wheel
  builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the blanket Windows
  CUDA=OFF (a CPU row is still forced off by the generic row classification), resolves
  TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to BUILD_ENV_FILE as well as
  GITHUB_ENV: the reusable workflow sources that file after the hook, and its own list had
  replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the generator.
- End-to-end, per published CUDA train, the route a Windows user has, modeled on
  cuda-windows.yml. The trains are read from filter_cuda_matrix.py's SUPPORTED_CUDA_VERSIONS
  (today cu132 and cu134; main stopped publishing cu130 in #23380), the same list the release
  rows are built from, so adding or
  dropping a train there changes what is tested. The end-to-end jobs take their trains from
  the build job's own filtered matrix, so a pull request without the ciflow/binaries/all
  label tests the one train it builds, and a train the generator dropped is not tested
  against a wheel that does not exist. (install_utils.py's list also has 12.6 for
  source builds, but 12.6 is no longer a published wheel train.) A Linux GPU job exports a
  Windows-target program with that train's torch (`test_cuda_windows.py --export`); a small
  job bundles it with the wheel the build job published for that train (windows_job.yml
  downloads one artifact and gives its script no token for a second); and a Windows GPU job
  installs that published wheel and runs test_cuda_windows.py with the program
  (test_cuda_windows_e2e.ps1), so CI runs a CUDA program through the artifact users install
  rather than a wheel rebuilt on the GPU runner. The export and end-to-end jobs check out the
  commit the build job builds (on a pull request, its merge commit), so the wheel, the
  program and the scripts that test them match. Each train's CUDA (on Linux for the export,
  and on Windows for the smoke test's cuobjdump and the model's CUDA runtime) is assembled
  from NVIDIA's checksummed redistributable archives by
  install_cuda_redist.py, which needs no installer or admin rights and fails on a train it
  has no release for. Each train's Windows job waits only for its own bundle, so one train
  failing does not skip the others, and like the export it does not run for a
  pull request from a fork. The Windows GPU runners come without an NVIDIA
  driver (nvcuda.dll is missing and cudaGetDeviceCount returns cudaErrorInsufficientDriver),
  so the job first installs the same data-center driver, from the same place, that
  pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat, 580.88); cu132 and
  cu134 run on it through CUDA's minor-version compatibility, as PyTorch's own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both directions, and the
  newest also as PTX; the expected list comes from executorch_cuda_arch_list in
  cuda_arch_list.sh, called the way the pre-build hook calls it and keyed by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and matches eager, after
  checking it was exported with the same CUDA train as the wheel (every CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the three CUDA
  ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run, including the Arm
  Cortex-M Python module, which builds under Ninja too: CMSIS-NN's test-only shared library
  took the static library's name, so its import library collided with it on Windows, and
  backends/cortex_m now gives the import library its own name.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022 BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py | Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 | executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff 2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh: device code for exactly
  sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails the train check.
- The export used `test_cuda_windows.py --export` from this branch in WSL, with mingw-w64 and
  the Windows CUDA runtime of the same train (13.0 from install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI, before the rebase onto main: the end-to-end jobs passed for both published trains on
  the Windows A10G runner, with a Linux-exported program matching eager (maxdiff 1.3e-7 for
  13.2, 2.4e-7 for 13.4).
- After rebasing onto main with the allocator moved into extension_cuda (#23447), the cu132
  wheel was rebuilt and passes all 55 checks, including the new ownership row: a single CUDA
  allocator and stream helpers owned by executorch_extension_cuda.dll across the 15 shipped
  libraries. The rows above were measured before that rebase.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.
Gasoonjia added a commit that referenced this pull request Oct 7, 2026
Builds on #23121 (merged), which ships linkable libraries in the Windows
wheel. Stacked on
#23405, which makes the CUDA backend fall back to cudaMalloc on devices
without memory
pools: the Windows GPU CI runner's A10G is one, so this PR's end-to-end
runs need it.

A Windows user can run a CUDA program but cannot lower one: lowering
compiles the model
with a toolchain only the Linux side has, and the CUDA Windows CI job
already exports on
Linux and runs on Windows for exactly this reason. What was missing is a
wheel that gives
that Windows user the runtime. This adds a Windows CUDA wheel carrying
the CUDA delegate
as C++ libraries, the same components the Linux CUDA wheel ships
(#21645, #21668), and
nothing CUDA in the Python module, which on Windows has no CUDA program
to lower. The
export (AOT) side is unchanged and identical to the CPU wheel's; only
the runtime links CUDA.

lib/executorch_backend_cuda.dll + .lib the delegate
executorch::backend_cuda
lib/executorch_extension_cuda.dll + .lib allocator + streams
executorch::extension_cuda
backends/cuda/aoti_cuda_shims.dll the AOTI shim layer (runtime DLL of
the delegate)
data/lib/aoti_cuda_shims.lib already shipped as the lowering stub; also
the
shim layer's import library here
extension/pybindings/_C.pyd unchanged: no CUDA dependency, no
CudaBackend

A C++ application links it exactly as on Linux:

find_package(executorch REQUIRED COMPONENTS kernels_optimized
backend_cuda)
target_link_libraries(app PRIVATE executorch::runtime
executorch::kernels_optimized
                                      executorch::backend_cuda)
# plus the $<TARGET_RUNTIME_DLLS:app> copy from the CPU PR, which now
also brings the
    # CUDA extension and the shim layer

The wheel's DLLs import no CUDA runtime DLL: CMake links cudart.lib into
them on Windows,
and that reaches the driver (nvcuda.dll). The compiled model is
different: the library
AOTInductor builds into model.pte imports cudart64_13.dll, which comes
from the CUDA
Toolkit's bin directory on PATH, as the guide says. PyPI's
nvidia-cuda-runtime has win_amd64
builds that carry that DLL, but a C++ program does not search
site-packages, so declaring it
would not make it loadable; the wheel declares no NVIDIA package for
Windows.

## How

- Generator. The CUDA toolkit's Visual Studio integration fails compiler
identification
under the ClangCL toolset on some toolkit and Visual Studio pairs
(MSB4023 in its targets
file), and the MSVC toolset cannot compile the Python extension's
sources. A Windows CUDA
build therefore uses Ninja Multi-Config with clang-cl, the compiler the
CPU wheel uses,
and nvcc with cl.exe as its host compiler, in a Visual Studio developer
environment that
has ninja (ninja, clang-cl and cl all on PATH, which is how the wheel
build runs).
Elsewhere, such as a plain shell, the build keeps the Visual Studio
generator it always
used. CMAKE_CUDA_COMPILER is passed only when CUDACXX is unset, so
options a user put in
CUDACXX (such as -allow-unsupported-compiler) still apply. The toolkit
is the one
install_utils already reports, so what compiles is the train packaging
declares.
- CUDA 13.2 and newer. Their CCCL stops with an #error under cl.exe's
traditional
preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on
Windows (13.0
  accepts it too).
- flatc / flatcc byproducts and imported locations share one host
executable suffix chosen
by CMAKE_HOST_WIN32. CMAKE_EXECUTABLE_SUFFIX is the target's: a Zephyr
toolchain sets
.elf, and naming the byproduct after it left Ninja with no rule for the
host tool.
- The shims resolve the runtime from executorch.dll rather than a static
core. (The PAL's
strong symbols are in #23121; C++20 for the delegate and shims comes
from #23135.)
- The shims keep WINDOWS_EXPORT_ALL_SYMBOLS OFF in a static build, as
upstream sets it,
and turn it ON in the shared layout: the delegate DLL imports the stream
guard functions
(get/set/peek/clearCurrentCUDAStream) from the shim DLL, and those carry
no
AOTI_SHIM_EXPORT, so the annotations alone would not export them. The
allocator is not
in either DLL: main moved it into extension_cuda (#23447), a shared
library the
  delegate and the shims both import it from, so the process has one.
- #23121 keeps a Windows CUDA build static; this drops that guard, so
the pybind preset
now builds the shared layout with CUDA too, and adds the CUDA import
libraries to the
  Windows packaging list.
- The root CMake file no longer links the CUDA delegate into the Python
extension on
  Windows; extension/cuda finds the CUDA toolkit itself, as on main.
- The package config adds the CUDA extension (allocator and stream
helpers) and the shim
  layer to the delegate's runtime
DLL set, and reports EXECUTORCH_RUNTIME_DLLS_EXTRA for the pre-3.28
route, since the
  shim layer ships outside lib/.

## CI

- build-wheels-cuda-windows.yml mirrors the Linux CUDA workflow on the
Windows wheel
builder and reuses filter_cuda_matrix.py. pre_build_script.sh drops the
blanket Windows
CUDA=OFF (a CPU row is still forced off by the generic row
classification), resolves
TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh, and writes it to
BUILD_ENV_FILE as well as
GITHUB_ENV: the reusable workflow sources that file after the hook, and
its own list had
replaced the row's (sm_75 in, sm_89 out). It also installs ninja for the
generator.
- End-to-end, per published CUDA train, the route a Windows user has,
modeled on
cuda-windows.yml. The trains are read from filter_cuda_matrix.py's
SUPPORTED_CUDA_VERSIONS
(today cu132 and cu134; main stopped publishing cu130 in #23380), the
same list the release
  rows are built from, so adding or
dropping a train there changes what is tested. The end-to-end jobs take
their trains from
the build job's own filtered matrix, so a pull request without the
ciflow/binaries/all
label tests the one train it builds, and a train the generator dropped
is not tested
against a wheel that does not exist. (install_utils.py's list also has
12.6 for
source builds, but 12.6 is no longer a published wheel train.) A Linux
GPU job exports a
Windows-target program with that train's torch (`test_cuda_windows.py
--export`); a small
job bundles it with the wheel the build job published for that train
(windows_job.yml
downloads one artifact and gives its script no token for a second); and
a Windows GPU job
installs that published wheel and runs test_cuda_windows.py with the
program
(test_cuda_windows_e2e.ps1), so CI runs a CUDA program through the
artifact users install
rather than a wheel rebuilt on the GPU runner. The export and end-to-end
jobs check out the
commit the build job builds (on a pull request, its merge commit), so
the wheel, the
program and the scripts that test them match. Each train's CUDA (on
Linux for the export,
and on Windows for the smoke test's cuobjdump and the model's CUDA
runtime) is assembled
  from NVIDIA's checksummed redistributable archives by
install_cuda_redist.py, which needs no installer or admin rights and
fails on a train it
has no release for. Each train's Windows job waits only for its own
bundle, so one train
failing does not skip the others, and like the export it does not run
for a
pull request from a fork. The Windows GPU runners come without an NVIDIA
driver (nvcuda.dll is missing and cudaGetDeviceCount returns
cudaErrorInsufficientDriver),
so the job first installs the same data-center driver, from the same
place, that
pytorch/pytorch's Windows CUDA smoke tests install (driver_update.bat,
580.88); cu132 and
cu134 run on it through CUDA's minor-version compatibility, as PyTorch's
own wheels do. The
  wheel build's own smoke test has no GPU program and says so.

## Tests

test_cuda_windows.py, the Windows counterpart of test_cuda_linux.py:

- the CUDA DLLs and import libraries ship;
- _C depends on no CUDA DLL and registers no CUDA backend;
- no shipped CUDA DLL imports cudart64_*.dll (only the driver), and no
Windows NVIDIA
  requirement is declared;
- every DLL with device code carries exactly the row's GPUs, both
directions, and the
newest also as PTX; the expected list comes from
executorch_cuda_arch_list in
cuda_arch_list.sh, called the way the pre-build hook calls it and keyed
by the wheel's
  +cuXYZ tag, not from the TORCH_CUDA_ARCH_LIST the build consumed;
- a C++ application linking executorch::backend_cuda gets the three DLLs
copied beside it
  and sees CudaBackend registered;
- a program lowered on Linux for Windows runs through the C++ SDK and
matches eager, after
checking it was exported with the same CUDA train as the wheel (every
CUDA 13 train loads
  cudart64_13.dll, so a mismatch would not fail on its own);
- then what a CPU Windows row checks: test_shared_libraries.py (with the
three CUDA
ownership rows), test_cpp_sdk.py and the MobileNetV3/XNNPACK model run,
including the Arm
Cortex-M Python module, which builds under Ninja too: CMSIS-NN's
test-only shared library
took the static library's name, so its import library collided with it
on Windows, and
  backends/cortex_m now gives the import library its own name.

## Test plan

On Windows 11 x64 with an RTX 5080 (sm_120, driver 595.95), VS 2022
BuildTools, CPython 3.12,
and WSL Ubuntu for the export, once per CUDA train. 13.2 and 13.4 are
the published trains
and the ones CI covers; 13.0 is no longer published (#23380) but still
builds from source, so
it was run too:

| train | Windows nvcc | WSL export torch | wheel | test_cuda_windows.py
| Linux-exported program |
|---|---|---|---|---|---|
| 13.0 | 13.0.88 (installer) | 2.14.1+cu130 | executorch-1.6.0+cu130 |
52 checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.2 | 13.2.86 (redist) | 2.14.1+cu132 | executorch-1.6.0+cu132 | 52
checks pass | runs on the GPU, maxdiff 2.4e-7 |
| 13.4 | 13.4.59 (redist) | 2.14.0.dev20260810+cu134 |
executorch-1.6.0+cu134 | 52 checks pass | runs on the GPU, maxdiff
2.4e-7 |

- Each wheel built with TORCH_CUDA_ARCH_LIST from cuda_arch_list.sh:
device code for exactly
sm_80/86/89/90/100/120, plus sm_120 PTX. 13.4 runs on a driver that
reports 13.2, through
  CUDA's minor-version compatibility.
- Before the /Zc:preprocessor change, 13.2 and 13.4 failed to compile
the .cu shims with
  CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
- Negative: the cu134 wheel pointed at the 13.0-exported program fails
the train check.
- The export used `test_cuda_windows.py --export` from this branch in
WSL, with mingw-w64 and
the Windows CUDA runtime of the same train (13.0 from
install_cuda_windows_cross_compile.sh,
  13.2/13.4 from the redist archives, the steps the guide now gives).
- CI, before the rebase onto main: the end-to-end jobs passed for both
published trains on
the Windows A10G runner, with a Linux-exported program matching eager
(maxdiff 1.3e-7 for
  13.2, 2.4e-7 for 13.4).
- After rebasing onto main with the allocator moved into extension_cuda
(#23447), the cu132
wheel was rebuilt and passes all 55 checks, including the new ownership
row: a single CUDA
allocator and stream helpers owned by executorch_extension_cuda.dll
across the 15 shipped
  libraries. The rows above were measured before that rebase.
- Linux: the CMake changes are behind WIN32 / MSVC, or pick the same
suffix there (empty on a
  Linux host); setup.py's new path runs only on Windows.

This branch was successfully deployed

1 active deployment
cadence — 919ab096 Deployed Sep 25, 2026 by Gasoonjia via hifi-op-test / hifi4 #29794
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cuda CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants