Repository navigation
Ship the CUDA runtime in the Windows CUDA wheel - #23123
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/23123
Note: Links to docs will display an error until the docs builds have been completed. ❌ 4 New Failures, 1 Cancelled Job, 1 Pending, 3 Unrelated Failures, 1 Unclassified FailureAs of commit fd104a7 with merge base aaa31ad ( NEW FAILURES - The following jobs have failed:
UNCLASSIFIED FAILURE - DrCI could not classify the following job because the workflow did not run on the merge base. The failure may be pre-existing on trunk or introduced by this PR:
CANCELLED JOB - The following job was cancelled. Please retry:
FLAKY - The following job failed but was likely due to flakiness present on trunk:
BROKEN TRUNK - The following job failed but was present on the merge base:👉 Rebase onto the `viable/strict` branch to avoid these failures
UNSTABLE - The following job is marked as unstable, possibly due to flakiness on trunk:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
162d4c7 to
b10b9e3
Compare
5afcafd to
c518096
Compare
b10b9e3 to
886edad
Compare
c518096 to
c2bace9
Compare
886edad to
f5e316b
Compare
The CUDA backend's stream-ordered allocations fail on any device that does not support
memory pools, which includes data-center GPUs in TCC mode on Windows (A10G, A100, H100 and
the like). There cudaMemPoolCreate and cudaMallocAsync return cudaErrorNotSupported, and
nothing falls back:
E cuda_allocator.cpp:99] cudaMemPoolCreate failed for device 0: operation not supported. Using the default pool.
E cuda_allocator.cpp:448] cudaMallocAsync failed: operation not supported (requested 512 bytes on device 0)
F storage.h:126] In function allocate(), assert failed (result.ok()): CudaAllocator::allocate_async failed for 512 bytes on device 0
That is from a CUDA program running on the Windows GPU CI runner (an A10G) in #23123, the
first CI job to run the delegate on Windows with a driver installed. A GeForce card in WDDM
mode supports memory pools, which is why a local run passes.
## Change
- CudaAllocator asks each device once whether it supports memory pools
(cudaDevAttrMemoryPoolsSupported). On a device that does not, allocate_async allocates
with cudaMalloc and deallocate_async frees with cudaFree, deciding by the device the
pointer lives on. cudaFree waits for the device's outstanding work, so work still using
the block on the stream finishes first. A failed attribute query keeps the stream-ordered
path, so nothing changes where the query cannot answer.
- release_cached_memory skips cudaDeviceGraphMemTrim on such a device; graph memory is
served by memory pools, so there is none to release.
- sort.cu and rand.cu called cudaMallocAsync/cudaFreeAsync directly; they now go through
CudaAllocator, so they take the same fallback.
Devices with memory pools, which is every Linux runner and every WDDM GPU, take exactly the
path they took before.
## Tests and CI
- test_cuda_allocator: AllocateAsyncRoundtrip runs on every device and fails without the
fallback on a device without pools. FallsBackWithoutMemoryPools checks that no pool is
created and no error is left behind there. The pool tests move to a fixture that skips on
a device without pools, where they cannot mean anything.
- cuda-windows.yml gains unittest-cuda-runtime-windows, the Windows counterpart of
cuda.yml's unittest-cuda-runtime: it builds llm-release-cuda with tests and runs the same
five runtime tests. The Windows runner's A10G has no memory pools, so this covers the
fallback; the Linux job covers the pool path.
- The Windows GPU runners come without an NVIDIA driver (nvcuda.dll is missing and
cudaGetDeviceCount returns cudaErrorInsufficientDriver), so the job first installs the
data-center driver that pytorch/pytorch's Windows CUDA smoke tests install
(driver_update.bat, 580.88, from the same bucket); install_nvidia_driver_windows.ps1.
- et_cxx_test copies a test's runtime DLLs ($<TARGET_RUNTIME_DLLS>) beside it on Windows,
which records no search path: the CUDA tests link extension_cuda.dll from another
directory and could not start otherwise.
## Test plan
- Windows 11, RTX 5080 (WDDM, memory pools supported), CUDA 13.0, llm-release-cuda with
tests: all five runtime tests pass (test_cuda_allocator 16 passed, the no-pool test
skipped as expected; mutable_state 15, weight_cache 11, guard 7, stream_guard 21).
- The no-pool path runs in the new Windows CI job on the A10G.
- Linux: the pool path is unchanged and covered by the existing unittest-cuda-runtime job.
Found while adding the Windows CUDA wheel (#23123), which needs this to run on the Windows
GPU runner; this PR does not depend on it.
f5d377c to
8aeecbb
Compare
The CUDA backend's stream-ordered allocations fail on any device that does not support
memory pools, which includes data-center GPUs in TCC mode on Windows (A10G, A100, H100 and
the like). There cudaMemPoolCreate and cudaMallocAsync return cudaErrorNotSupported, and
nothing falls back:
E cuda_allocator.cpp:99] cudaMemPoolCreate failed for device 0: operation not supported. Using the default pool.
E cuda_allocator.cpp:448] cudaMallocAsync failed: operation not supported (requested 512 bytes on device 0)
F storage.h:126] In function allocate(), assert failed (result.ok()): CudaAllocator::allocate_async failed for 512 bytes on device 0
That is from a CUDA program running on the Windows GPU CI runner (an A10G) in #23123, the
first CI job to run the delegate on Windows with a driver installed. A GeForce card in WDDM
mode supports memory pools, which is why a local run passes.
## Change
- CudaAllocator asks each device once whether it supports memory pools
(cudaDevAttrMemoryPoolsSupported). On a device that does not, allocate_async allocates
with cudaMalloc and deallocate_async frees with cudaFree, deciding by the device the
pointer lives on. cudaFree waits for the device's outstanding work, so work still using
the block on the stream finishes first. A failed attribute query keeps the stream-ordered
path, so nothing changes where the query cannot answer.
- release_cached_memory skips cudaDeviceGraphMemTrim on such a device; graph memory is
served by memory pools, so there is none to release.
- sort.cu and rand.cu called cudaMallocAsync/cudaFreeAsync directly; they now go through
CudaAllocator, so they take the same fallback.
Devices with memory pools, which is every Linux runner and every WDDM GPU, take exactly the
path they took before.
## Tests and CI
- test_cuda_allocator: AllocateAsyncRoundtrip runs on every device and fails without the
fallback on a device without pools. FallsBackWithoutMemoryPools checks that no pool is
created and no error is left behind there. The pool tests move to a fixture that skips on
a device without pools, where they cannot mean anything.
- cuda-windows.yml gains unittest-cuda-runtime-windows, the Windows counterpart of
cuda.yml's unittest-cuda-runtime: it builds llm-release-cuda with tests and runs the same
five runtime tests. The Windows runner's A10G has no memory pools, so this covers the
fallback; the Linux job covers the pool path.
- The Windows GPU runners come without an NVIDIA driver (nvcuda.dll is missing and
cudaGetDeviceCount returns cudaErrorInsufficientDriver), so the job first installs the
data-center driver that pytorch/pytorch's Windows CUDA smoke tests install
(driver_update.bat, 580.88, from the same bucket); install_nvidia_driver_windows.ps1.
- et_cxx_test copies a test's runtime DLLs ($<TARGET_RUNTIME_DLLS>) beside it on Windows,
which records no search path: the CUDA tests link extension_cuda.dll from another
directory and could not start otherwise.
## Test plan
- Windows 11, RTX 5080 (WDDM, memory pools supported), CUDA 13.0, llm-release-cuda with
tests: all five runtime tests pass (test_cuda_allocator 16 passed, the no-pool test
skipped as expected; mutable_state 15, weight_cache 11, guard 7, stream_guard 21).
- The no-pool path runs in the new Windows CI job on the A10G.
- Linux: the pool path is unchanged and covered by the existing unittest-cuda-runtime job.
Found while adding the Windows CUDA wheel (#23123), which needs this to run on the Windows
GPU runner; this PR does not depend on it.
The CUDA backend's stream-ordered allocations fail on any device that does not support
memory pools, which includes data-center GPUs in TCC mode on Windows (A10G, A100, H100 and
the like). There cudaMemPoolCreate and cudaMallocAsync return cudaErrorNotSupported, and
nothing falls back:
E cuda_allocator.cpp:99] cudaMemPoolCreate failed for device 0: operation not supported. Using the default pool.
E cuda_allocator.cpp:448] cudaMallocAsync failed: operation not supported (requested 512 bytes on device 0)
F storage.h:126] In function allocate(), assert failed (result.ok()): CudaAllocator::allocate_async failed for 512 bytes on device 0
That is from a CUDA program running on the Windows GPU CI runner (an A10G) in #23123, the
first CI job to run the delegate on Windows with a driver installed. A GeForce card in WDDM
mode supports memory pools, which is why a local run passes.
## Change
- CudaAllocator asks each device once whether it supports memory pools
(cudaDevAttrMemoryPoolsSupported). On a device that does not, allocate_async allocates
with cudaMalloc and deallocate_async frees with cudaFree, deciding by the device the
pointer lives on. cudaFree waits for the device's outstanding work, so work still using
the block on the stream finishes first. A failed attribute query keeps the stream-ordered
path, so nothing changes where the query cannot answer.
- release_cached_memory skips cudaDeviceGraphMemTrim on such a device; graph memory is
served by memory pools, so there is none to release.
- sort.cu and rand.cu called cudaMallocAsync/cudaFreeAsync directly; they now go through
CudaAllocator, so they take the same fallback.
Devices with memory pools, which is every Linux runner and every WDDM GPU, take exactly the
path they took before.
## Tests and CI
- test_cuda_allocator: AllocateAsyncRoundtrip runs on every device and fails without the
fallback on a device without pools. FallsBackWithoutMemoryPools checks that no pool is
created and no error is left behind there. The pool tests move to a fixture that skips on
a device without pools, where they cannot mean anything.
- cuda-windows.yml gains unittest-cuda-runtime-windows, the Windows counterpart of
cuda.yml's unittest-cuda-runtime: it builds llm-release-cuda with tests and runs the same
five runtime tests. The Windows runner's A10G has no memory pools, so this covers the
fallback; the Linux job covers the pool path.
- The Windows GPU runners come without an NVIDIA driver (nvcuda.dll is missing and
cudaGetDeviceCount returns cudaErrorInsufficientDriver), so the job first installs the
data-center driver that pytorch/pytorch's Windows CUDA smoke tests install
(driver_update.bat, 580.88, from the same bucket); install_nvidia_driver_windows.ps1.
- et_cxx_test copies a test's runtime DLLs ($<TARGET_RUNTIME_DLLS>) beside it on Windows,
which records no search path: the CUDA tests link extension_cuda.dll from another
directory and could not start otherwise.
## Test plan
- Windows 11, RTX 5080 (WDDM, memory pools supported), CUDA 13.0, llm-release-cuda with
tests: all five runtime tests pass (test_cuda_allocator 16 passed, the no-pool test
skipped as expected; mutable_state 15, weight_cache 11, guard 7, stream_guard 21).
- The no-pool path runs in the new Windows CI job on the A10G.
- Linux: the pool path is unchanged and covered by the existing unittest-cuda-runtime job.
Found while adding the Windows CUDA wheel (#23123), which needs this to run on the Windows
GPU runner; this PR does not depend on it.
c9f35f9 to
fb53b4b
Compare
This PR needs a
|
fb53b4b to
b2f3bf7
Compare
| if(EXECUTORCH_BUILD_CUDA) | ||
| # extension/cuda links CUDA::cudart before backends/cuda finds the toolkit, | ||
| # and a CPU torch does not define it. | ||
| find_package(CUDAToolkit REQUIRED) |
There was a problem hiding this comment.
I think this find_package(CUDAToolkit) is no longer needed. Since an earlier fix for exactly this case, extension/cuda finds the toolkit itself right before it links CUDA::cudart. I tried a small project with that layout and no Torch, and it configured and built without a find in the parent. The comment describes the old state. The block also runs on Linux, while the test plan says the CMake changes are behind WIN32. Could you drop these lines?
There was a problem hiding this comment.
Agreed, removed; the top-level CMakeLists.txt is back to main around that block. extension/cuda finds the toolkit itself before linking CUDA::cudart, and the rebuilt cu132 wheel configures and builds without it.
| ), f"this is run as a CUDA row but the installed version carries no +cuXYZ tag: {local!r}" | ||
| text = (Path(__file__).parent / "cuda_arch_list.sh").read_text() | ||
|
|
||
| def assigned(name: str) -> str: |
There was a problem hiding this comment.
This check and the end to end PowerShell script each parse cuda_arch_list.sh with a regex. The Linux check, and the Windows branch of the pre-build hook in this pull request, source the script and call executorch_cuda_arch_list. The regex only understands a plain string or one variable reference, so a reference plus one more architecture gives a wrong list. bash is on the Windows runners, so could both call the function the same way? Then there is one source of truth.
There was a problem hiding this comment.
Done. Both now call executorch_cuda_arch_list through bash, the way the pre-build hook does, so cuda_arch_list.sh is the single source of truth. They prefer Git's bash (System32's bash.exe is WSL's launcher, which can come first on PATH), pass the train on the command line, and strip carriage returns while sourcing, so a checkout with core.autocrlf works too. Checked locally: cu132 and cu134 resolve to the same list as before, and an unknown train fails with the script's own error.
| for library in with_device_code | ||
| if newest in _device_code(cuobjdump, library, "ptx") | ||
| ] | ||
| assert portable, ( |
There was a problem hiding this comment.
The docstring says every DLL with device code also carries the newest architecture as PTX, and the description says the same. The check only needs one DLL to have it, because assert portable passes as soon as the list is not empty. With a second library that has device code but no PTX, the test still passes and its message says the PTX is there. Could it assert that no library with device code is missing the PTX?
There was a problem hiding this comment.
Right, it only required one. It now asserts that no library with device code is missing the newest architecture as PTX, and names the ones that are.
The CUDA backend's stream-ordered allocations fail on any device that does not support
memory pools, which includes data-center GPUs in TCC mode on Windows (A10G, A100, H100 and
the like). There cudaMemPoolCreate and cudaMallocAsync return cudaErrorNotSupported, and
nothing falls back:
E cuda_allocator.cpp:99] cudaMemPoolCreate failed for device 0: operation not supported. Using the default pool.
E cuda_allocator.cpp:448] cudaMallocAsync failed: operation not supported (requested 512 bytes on device 0)
F storage.h:126] In function allocate(), assert failed (result.ok()): CudaAllocator::allocate_async failed for 512 bytes on device 0
That is from a CUDA program running on the Windows GPU CI runner (an A10G) in #23123, the
first CI job to run the delegate on Windows with a driver installed. A GeForce card in WDDM
mode supports memory pools, which is why a local run passes.
## Change
- CudaAllocator asks each device once whether it supports memory pools
(cudaDevAttrMemoryPoolsSupported) and caches the answer in per-device atomics. On a device
that does not, allocate_async allocates with cudaMalloc and deallocate_async frees with
cudaFree, deciding by the device the pointer lives on. A failed attribute query keeps the
stream-ordered path.
- On a device with memory pools the paths are the ones they were: allocate_async adds one
atomic load, and deallocate_async looks up the pointer's device only once some device
without pools has been seen, so a process whose devices all have them frees exactly as
before.
- The fallback refuses while the stream is capturing a CUDA graph. In relaxed capture mode
cudaMalloc and cudaFree succeed, so a block freed during capture would be handed out
again while every replay still writes to it; refusing keeps the loud failure the
stream-ordered call gave. On such a device the backend also does not turn CUDA graphs on
for a method that asks for them (enable_cuda_graph_for_method), and says so in the log.
- sort.cu and rand.cu called cudaMallocAsync/cudaFreeAsync directly. They now call
CudaAllocator::allocate_stream_ordered / deallocate_stream_ordered with their real device,
which is cudaMallocAsync/cudaFreeAsync on the stream as before (the device default pool,
not the backend's retaining pool) and the cudaMalloc fallback without pools.
- release_cached_memory skips cudaDeviceGraphMemTrim on a device without pools; graph
memory is served by memory pools, so there is none to release.
## Tests and CI
- test_cuda_allocator: AllocateAsyncRoundtrip and StreamOrderedScratchRoundtrip run on every
device. FallsBackWithoutMemoryPools and FallbackRefusesWhileCapturing run on a device
without pools: the first checks that no pool is created and no error is left behind, the
second that neither fallback allocates inside a capture. The pool tests move to a fixture
that skips on a device without pools, where they cannot mean anything.
- test_cuda_sort_rand (new): runs the sort shim along both dims of a 2-D tensor (the
transpose path that allocates two scratch buffers, and thrust's scratch) against a host
sort, and the rand shim, on whatever device is present.
- cuda-windows.yml gains unittest-cuda-runtime-windows, the Windows counterpart of
cuda.yml's unittest-cuda-runtime: it builds llm-release-cuda with tests and runs the same
runtime tests plus test_cuda_sort_rand. The Windows runner's A10G has no memory pools, so
this covers the fallback; the Linux job, which now runs test_cuda_sort_rand too, covers
the pool path.
- The Windows GPU runners come without an NVIDIA driver (nvcuda.dll is missing and
cudaGetDeviceCount returns cudaErrorInsufficientDriver), so the job first installs the
data-center driver that pytorch/pytorch's Windows CUDA smoke tests install
(driver_update.bat, 580.88, from the same bucket); install_nvidia_driver_windows.ps1.
- et_cxx_test copies a test's runtime DLLs ($<TARGET_RUNTIME_DLLS>) beside it on Windows,
which records no search path: the CUDA tests link extension_cuda.dll from another
directory and could not start otherwise.
## Test plan
- Windows 11, RTX 5080 (WDDM, memory pools supported), CUDA 13.0, llm-release-cuda with
tests: all six runtime tests pass (test_cuda_allocator 17 passed and the two no-pool
tests skipped as expected; mutable_state 15, weight_cache 11, sort_rand 2, guard 7,
stream_guard 21).
- The no-pool path runs in the Windows CI job on the A10G; on the previous revision of this
PR that job passed the fallback tests there.
- Linux: the pool path is covered by the existing unittest-cuda-runtime job.
Found while adding the Windows CUDA wheel (#23123), which needs this to run on the Windows
GPU runner; this PR does not depend on it.
2ab27cd to
1ce32bb
Compare
b2f3bf7 to
34284cd
Compare
| # The pybindings need GLIBCXX_3.4.30, which the image's conda libstdc++ lacks. | ||
| mv /opt/conda/lib/libstdc++.so.6 /opt/conda/lib/libstdc++.so.6.bak || true | ||
| ln -sf /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /opt/conda/lib/libstdc++.so.6 | ||
| if [ "${TRAIN}" != "13.0" ]; then |
There was a problem hiding this comment.
The matrix comes from SUPPORTED_CUDA_VERSIONS, which no longer has 13.0. So this check is always true, and the image toolkit path never runs. I would drop the condition and always use the redist archives. The comment on the table in install_cuda_redist.py has the same problem: it says one entry per published train, but 13.0 is there too.
There was a problem hiding this comment.
Agreed: dropped the condition, so every train uses the redist archives, and updated the image comment. The _RELEASES comment now says why 13.0 stays there: it is no longer published but still builds from source, and local tests of it use that entry.
shoumikhin
left a comment
There was a problem hiding this comment.
Two things from this round, plus some stale text.
The description still says the root CMake file finds the CUDA toolkit first, because extension/cuda links CUDA::cudart before backends/cuda finds it. That find was removed during review, and the root file now only adds the Windows guard, so that bullet no longer matches the code.
In setup.py, the comment on the CUDA runtime requirements says a CUDA wheel is only built on Linux. Windows builds one now, so it would help if that comment said why Windows still needs no NVIDIA package.
One process thing: without the ciflow/binaries/all label this builds and runs only the 13.2 train, for one Python version. Worth adding the label once before merge so 13.4 is checked on the final code.
| $env:TORCH_CUDA_ARCH_LIST = "$archList".Trim() | ||
| $env:CMAKE_ARGS = "-DEXECUTORCH_BUILD_CUDA=ON -DEXECUTORCH_BUILD_VULKAN=OFF" | ||
| $env:DISTUTILS_USE_SDK = "1" | ||
| Invoke-Native { python setup.py bdist_wheel } |
There was a problem hiding this comment.
This job builds its own wheel from source on the GPU runner, so the wheel we publish is never the one that runs a CUDA program.
The build job's own smoke test skips that run: it needs EXECUTORCH_CUDA_WINDOWS_ARTIFACTS, which is not set there, so it prints a skip and moves on. Between the two jobs, nothing in CI runs a program through the released artifact, and a problem that only shows up at run time in the release build would pass.
Could this job download the wheel the build job made for the same CUDA version and install that instead? Then CI tests what users install, and each train saves a full wheel build on a GPU runner.
There was a problem hiding this comment.
Done. The end-to-end job no longer builds a wheel. A small bundle-cuda-windows-wheel-e2e job (one per train) downloads the wheel the build job published for that train and the exported program, and uploads them as one artifact, because windows_job.yml downloads a single artifact and gives its script no token for a second. The Windows job installs that published wheel (with a conda env matching its cpXY tag) and runs the program through it, so CI now tests what users install, and each train saves a wheel build on a GPU runner.
Two things came with it. The trains come from the build job's own filtered matrix, so they always match the wheels that exist, including when the generator drops a train or a limited PR falls back. And the export and end-to-end jobs check out github.sha, the commit the build job's empty ref builds (on a PR, its merge commit), so the wheel, the program and the test scripts come from one commit. I ran the new script locally against my cu132 wheel and a WSL-exported program: it installs the wheel and the full smoke test passes, with the program matching eager. The bundle step itself only runs in CI.
| set(_aoti_cuda_backend_library_type STATIC) | ||
| endif() | ||
| if(_cuda_is_msvc_toolchain AND NOT _executorch_cuda_ships_libraries) | ||
| # MSVC links aoti_cuda_backend into portable_lib without relying on C++ |
There was a problem hiding this comment.
This comment says MSVC links the delegate into portable_lib. The root file now skips aoti_cuda_backend on Windows entirely, so after this change no Windows build does that, and the reason given for the allocator copy reads wrong.
The copy still looks needed, for C++ applications that link the static delegate and cannot import the allocator from the shims DLL in this layout. Could the comment say that instead?
There was a problem hiding this comment.
Right, that reason is gone. The comment now says why the copy is still there: in the static layout a C++ application links the delegate as an archive and, on MSVC, cannot import the allocator's C++ symbols from aoti_cuda_shims.dll.
|
Thanks. On the review-level notes:
|
The CUDA backend's stream-ordered allocations fail on any device that does not support
memory pools, which includes data-center GPUs in TCC mode on Windows (A10G, A100, H100 and
the like). There cudaMemPoolCreate and cudaMallocAsync return cudaErrorNotSupported, and
nothing falls back:
E cuda_allocator.cpp:99] cudaMemPoolCreate failed for device 0: operation not supported. Using the default pool.
E cuda_allocator.cpp:448] cudaMallocAsync failed: operation not supported (requested 512 bytes on device 0)
F storage.h:126] In function allocate(), assert failed (result.ok()): CudaAllocator::allocate_async failed for 512 bytes on device 0
That is from a CUDA program running on the Windows GPU CI runner (an A10G) in #23123, the
first CI job to run the delegate on Windows with a driver installed. A GeForce card in WDDM
mode supports memory pools, which is why a local run passes.
## Change
- CudaAllocator (extension/cuda/cuda_allocator, where #23447 moved it) asks each device once
whether it supports memory pools
(cudaDevAttrMemoryPoolsSupported) and caches the answer in per-device atomics. On a device
that does not, allocate_async allocates with cudaMalloc and deallocate_async frees with
cudaFree, deciding by the device the pointer lives on. A failed attribute query keeps the
stream-ordered path.
- On a device with memory pools the paths are the ones they were: allocate_async adds one
atomic load, and deallocate_async looks up the pointer's device only once some device
without pools has been seen, so a process whose devices all have them frees exactly as
before.
- The fallback refuses while the stream is capturing a CUDA graph. In relaxed capture mode
cudaMalloc and cudaFree succeed, so a block freed during capture would be handed out
again while every replay still writes to it; refusing keeps the loud failure the
stream-ordered call gave, and a free kept during a capture is reported to the caller
(deallocate_stream_ordered returns NotSupported; a failed cudaFree returns Internal). On
such a device the backend also does not turn CUDA graphs on for a method that asks for
them (enable_cuda_graph_for_method), and says so in the log.
- sort.cu and rand.cu called cudaMallocAsync/cudaFreeAsync directly. They now call
CudaAllocator::allocate_stream_ordered / deallocate_stream_ordered with their real device,
which is cudaMallocAsync/cudaFreeAsync on the stream as before (the device default pool,
not the allocator's retaining pool) and the cudaMalloc fallback without pools. rand passes
the current device, the one its stream was created on.
- rand's one-time RNG state allocation now reports a failure: the shims return an error
instead of launching their kernels with a null state, and a later call tries again.
## Tests and CI
- test_cuda_allocator: AllocateAsyncRoundtrip and StreamOrderedScratchRoundtrip run on every
device. Four tests run on a device without pools: FallsBackWithoutMemoryPools checks that
the fallback allocation is actually released by deallocate_async,
FallbackRefusesWhileCapturing that neither allocator falls back inside a capture,
FallbackKeepsBlocksFreedWhileCapturing that blocks freed during a capture, through both
free paths, stay allocated and intact through a replay and that the kept block is
reported, and CudaGraphStaysOffWithoutMemoryPools that the CUDA graph state the backend
starts from (CudaGraphState::start_warmup) stays disabled. They skip on a device with pools,
except where EXECUTORCH_CUDA_TEST_REQUIRE_NO_MEMORY_POOLS is set, as the Windows job does,
where they fail instead so the fallback cannot go untested; with it set, no visible CUDA
device fails the tests too rather than skipping them. The pool tests move to a
fixture that skips on a device without pools, alongside CudaGraphStartsWithMemoryPools.
- test_cuda_sort_rand (new): runs the sort shim along both dims of a 2-D tensor (the
transpose path that allocates two scratch buffers, and thrust's scratch) on input with
repeated values in every slice, against a host stable sort, checking values and indices,
and the rand shim. Built only where sort.cu and
rand.cu are (a CUDA build that found a CUDA compiler).
- cuda-windows.yml gains unittest-cuda-runtime-windows, the Windows counterpart of
cuda.yml's unittest-cuda-runtime: it builds llm-release-cuda with tests and runs the same
runtime tests plus test_cuda_sort_rand. The Windows runner's A10G has no memory pools, so
this covers the fallback; the Linux job, which now runs test_cuda_sort_rand too, covers
the pool path. Its pull_request paths include the scripts and tools/cmake/Test.cmake it
depends on.
- The Windows GPU runners come without an NVIDIA driver (nvcuda.dll is missing and
cudaGetDeviceCount returns cudaErrorInsufficientDriver), so the job first installs the
data-center driver that pytorch/pytorch's Windows CUDA smoke tests install
(driver_update.bat, 580.88, from the same bucket); install_nvidia_driver_windows.ps1.
- et_cxx_test copies a test's runtime DLLs ($<TARGET_RUNTIME_DLLS>) beside it on Windows,
which records no search path: the CUDA tests link extension_cuda.dll from another
directory and could not start otherwise.
- extension/cuda/test/cuda_allocator_exit.cpp (Linux only) fakes every CUDA call on its
path; it now also fakes cudaDeviceGetAttribute to report pools, since the pool query is new
on that path and would otherwise make the test depend on the machine's GPU.
## Test plan
- Windows 11, RTX 5080 (WDDM, memory pools supported), CUDA 13.0, llm-release-cuda with
tests, rebased on main with the allocator in extension/cuda: all six runtime tests pass
(test_cuda_allocator 22 passed with the four no-pool tests skipped; mutable_state 15,
weight_cache 11, sort_rand 2, guard 7, stream_guard 21). With
EXECUTORCH_CUDA_TEST_REQUIRE_NO_MEMORY_POOLS set, the four no-pool tests fail there, as
intended. test_cuda_allocator_exit is Linux only and runs in the Linux job.
- The no-pool path runs in the Windows CI job on the A10G; on the previous revision of this
PR that job passed the fallback tests there.
- Linux: the pool path is covered by the existing unittest-cuda-runtime job.
Found while adding the Windows CUDA wheel (#23123), which needs this to run on the Windows
GPU runner; this PR does not depend on it.
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.
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.
A C++ application links it exactly as on Linux:
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
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.
preprocessor, so the CUDA sources pass -Xcompiler=/Zc:preprocessor on Windows (13.0
accepts it too).
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.
strong symbols are in Ship linkable libraries in the Windows CPU wheel #23121; C++20 for the delegate and shims comes from Fix CUDA backend Windows build under C++20 #23135.)
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 (Move the CUDA allocator into extension/cuda so other delegates can share it #23447), a shared library the
delegate and the shims both import it from, so the process has one.
now builds the shared layout with CUDA too, and adds the CUDA import libraries to the
Windows packaging list.
Windows; extension/cuda finds the CUDA toolkit itself, as on main.
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
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.
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 Stop publishing CUDA 13.0 wheels #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 smalljob 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:
requirement is declared;
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;
and sees CudaBackend registered;
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);
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:
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.
CCCL's "traditional preprocessor" #error; 13.0 was unaffected.
test_cuda_windows.py --exportfrom this branch in WSL, with mingw-w64 andthe 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).
the Windows A10G runner, with a Linux-exported program matching eager (maxdiff 1.3e-7 for
13.2, 2.4e-7 for 13.4).
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 host); setup.py's new path runs only on Windows.