Skip to content

Ship the CUDA delegate in a CUDA-enabled ExecuTorch wheel - #21478

Draft
shoumikhin wants to merge 12 commits into
gh/shoumikhin/76/headfrom
gh/shoumikhin/77/head
Draft

Ship the CUDA delegate in a CUDA-enabled ExecuTorch wheel#21478
shoumikhin wants to merge 12 commits into
gh/shoumikhin/76/headfrom
gh/shoumikhin/77/head

Conversation

@shoumikhin

Copy link
Copy Markdown
Contributor

Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

What is inside

Added only when the wheel is built with CUDA (Linux):

  • executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
    It wraps the existing static CUDA backend so that loading the library runs
    its "CudaBackend" registration and registers into the one process-wide
    runtime in libexecutorch.so. It is libtorch-free.
  • executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
    is a single shared library on purpose so its per-thread caller-stream state
    has exactly one instance across the whole process (needed so a TensorRT
    delegate and the CUDA backend share one stream).
  • executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
  • CMake targets executorch::cuda_backend and executorch::extension_cuda in the
    shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

How to use it

pip install executorch   # from a CUDA-enabled index/build
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build

At runtime, get_backend_class("CudaBackend") returns the registered backend.

Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

  • Built the wheel with CUDA enabled and confirmed it contains
    libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
    the CUDA headers, and the CMake config.
  • Installed the wheel into a clean virtual environment and built a standalone
    C++ program via find_package(executorch) linking executorch::cuda_backend.
    The program compiled, linked, ran, and get_backend_class("CudaBackend")
    returned non-null, both when the backend was linked directly and when it was
    dlopen'd.
  • Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
    other through an $ORIGIN rpath because they are co-located in executorch/lib/.
  • Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
  • Confirmed there is a single libextension_cuda.so that both the backend and
    the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.

[ghstack-poisoned]
@shoumikhin

shoumikhin commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@pytorch-bot

pytorch-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 62 New Failures, 2 Pending, 1 Unrelated Failure, 42 Unclassified Failures

As of commit 00d5527 with merge base b26b9ac (image):

NEW FAILURES - The following jobs have failed:

UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

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 Jul 29, 2026
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 29, 2026
## Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

## What is inside

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

## How to use it

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

## Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.


ghstack-source-id: 27c5585
ghstack-comment-id: 5124122149
Pull-Request: #21478
@shoumikhin shoumikhin added ciflow/periodic ciflow/trunk ciflow/binaries ciflow/binaries/all Release PRs with this label will build wheels for all python versions ciflow/nightly labels Jul 29, 2026
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 29, 2026
## Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

## What is inside

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

## How to use it

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

## Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.


ghstack-source-id: 2098e23
ghstack-comment-id: 5124122149
Pull-Request: #21478
shoumikhin added a commit that referenced this pull request Jul 29, 2026
## Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

## What is inside

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

## How to use it

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

## Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.


ghstack-source-id: 2098e23
ghstack-comment-id: 5124122149
Pull-Request: #21478
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
## Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

## What is inside

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

## How to use it

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

## Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.


ghstack-source-id: dc8ddc4
ghstack-comment-id: 5124122149
Pull-Request: #21478
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
## Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

## What is inside

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

## How to use it

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

## Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.


ghstack-source-id: dd0a612
ghstack-comment-id: 5124122149
Pull-Request: #21478
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
## Why this is needed

The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

## What is inside

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

## How to use it

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
# Linking the CUDA backend force-loads it so it registers on startup.
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

## Test plan

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.


ghstack-source-id: 5946067
ghstack-comment-id: 5124122149
Pull-Request: #21478
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.

ghstack-source-id: eabaea7
ghstack-comment-id: 5124122149
Pull-Request: #21478

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a loadable CUDA delegate shared library to CUDA-enabled Linux ExecuTorch wheels so that standalone C++ consumers can link/load the CUDA backend (and related CUDA stream extension) without building from source, mirroring how other delegates (e.g., QNN) are distributed.

Changes:

  • Adds an “empty” translation unit to allow building a wrapper shared library that bundles the existing static CUDA backend for load-time registration.
  • Introduces a new executorch_cuda_backend shared library target in backends/cuda/ (gated on EXECUTORCH_BUILD_SHARED and non-MSVC), with wheel-relocatable $ORIGIN-based RPATH and link options intended to prevent --as-needed from dropping the library.
  • Adds a new PR CI job to build a CUDA-enabled wheel on a GPU runner and validate that libexecutorch_cuda_backend.so ships and successfully registers CudaBackend at runtime.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
backends/cuda/runtime/executorch_cuda_backend_lib.cpp Adds a placeholder TU to satisfy CMake for a shared-library wrapper used to retain static registration.
backends/cuda/CMakeLists.txt Builds/installs libexecutorch_cuda_backend.so for shared builds, sets wheel-relocatable RPATH, and adds consumer link options to force retention under --as-needed.
.github/workflows/pull.yml Adds a GPU CI job that builds a CUDA wheel and asserts the shipped CUDA backend library is present and loadable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.

ghstack-source-id: fb189a3
ghstack-comment-id: 5124122149
Pull-Request: #21478
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.

ghstack-source-id: 4bb0dba
ghstack-comment-id: 5124122149
Pull-Request: #21478
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 30, 2026
The previous change ships a default Linux wheel with the core runtime as a
linkable libexecutorch.so, but no GPU delegate. To actually run a model on
an NVIDIA GPU (including a coalesced TensorRT + CUDA .pte), a C++ program
also needs the CUDA delegate as a loadable library.

This follows the way PyTorch distributes CUDA: the default wheel has the CPU
runtime, and a CUDA-specific wheel (built on a CUDA machine / installed from
a CUDA index) adds the GPU binaries. Here the CUDA-enabled ExecuTorch wheel
ships the CUDA delegate shared libraries alongside the same core runtime, so
a C++ consumer can link and load the CUDA backend with no source build.

Today the CUDA backend only exists as a static archive that is baked into
the Python module (_portable_lib.so). There is no standalone loadable
backend a C++ program can use, the way QNN already ships
libqnn_executorch_backend.so. This change adds that.

Added only when the wheel is built with CUDA (Linux):

- executorch/lib/libexecutorch_cuda_backend.so: a loadable CUDA delegate.
  It wraps the existing static CUDA backend so that loading the library runs
  its "CudaBackend" registration and registers into the one process-wide
  runtime in libexecutorch.so. It is libtorch-free.
- executorch/lib/libextension_cuda.so: the CUDA caller-stream extension. It
  is a single shared library on purpose so its per-thread caller-stream state
  has exactly one instance across the whole process (needed so a TensorRT
  delegate and the CUDA backend share one stream).
- executorch/include/executorch/extension/cuda/*.h: the caller-stream headers.
- CMake targets executorch::cuda_backend and executorch::extension_cuda in the
  shipped executorch-config.cmake, defined only when the libraries are present.

The default (CPU) wheel is unchanged: none of these are added unless the
wheel is built with CUDA. Windows and macOS wheels are unchanged.

```bash
pip install executorch   # from a CUDA-enabled index/build
```

```cmake
find_package(executorch CONFIG REQUIRED)

add_executable(my_runner main.cpp)
target_link_libraries(my_runner PRIVATE
    executorch::runtime
    executorch::cuda_backend)
```

```bash
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')"
cmake --build build
```

At runtime, get_backend_class("CudaBackend") returns the registered backend.

Verified on Linux x86_64 with an NVIDIA H100 and CUDA 12.8:

- Built the wheel with CUDA enabled and confirmed it contains
  libexecutorch_cuda_backend.so and libextension_cuda.so under executorch/lib/,
  the CUDA headers, and the CMake config.
- Installed the wheel into a clean virtual environment and built a standalone
  C++ program via find_package(executorch) linking executorch::cuda_backend.
  The program compiled, linked, ran, and get_backend_class("CudaBackend")
  returned non-null, both when the backend was linked directly and when it was
  dlopen'd.
- Confirmed it runs with no LD_LIBRARY_PATH: the shipped libraries resolve each
  other through an $ORIGIN rpath because they are co-located in executorch/lib/.
- Confirmed the program and the CUDA backend link no libtorch/libc10 (via ldd).
- Confirmed there is a single libextension_cuda.so that both the backend and
  the consumer resolve to (the one-instance caller-stream requirement).

Root-caused and fixed a load-time abort during development: the CUDA backend
static archive already carries a whole-archive link option, so also wrapping it
in an explicit whole-archive linked it twice and ran its static registration
twice, hitting a duplicate-registration check. The fix links it once.

Known follow-ups: a CUDA wheel CI job to exercise this automatically, and
running auditwheel on release so the shipped rpath is limited to $ORIGIN.

ghstack-source-id: ad3687f
ghstack-comment-id: 5124122149
Pull-Request: #21478
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/binaries/all Release PRs with this label will build wheels for all python versions ciflow/binaries ciflow/nightly ciflow/periodic ciflow/trunk 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