Skip to content

Commit 2ad5e16

Browse files
committed
Make the CUDA docs agree with the TensorRT page
The CUDA backend page and the getting started page said several things that the TensorRT page, and the code, contradict: - The CUDA page said the Python runtime allocates host memory for an unplanned output, so a method that keeps its outputs on the GPU could not run from Python. The Python runtime allocates device memory for it and returns it without a copy. So the page now leaves both inputs and outputs unplanned, like the TensorRT page and the PropagateDeviceConfig docstring, because that copies the least. A planned output lives in the method's memory, which the next run overwrites, and the Python runtime copies it on every call unless clone_outputs=False. The C++ example now shows Module::set_output. - It said a planned input fed with GPU memory makes an undefined host memcpy and crashes, and so did the PropagateDeviceConfig docstring. The runtime refuses that copy and the call fails with NotSupported. - It asked for a CUDA 11.x or later toolkit to run a model. The prebuilt packages use CUDA 13 and bring the CUDA runtime with them on Linux. On Windows the runtime comes from a CUDA 13 toolkit. A build from source also supports CUDA 12.6. The toolkit is needed for export, where nvcc compiles the kernels. The driver line now asks for a driver that supports that CUDA version. The page also gave no minimum GPU, while the packages are built for compute capability 8.0 and newer, and it asked for Python 3.8+, while the package needs 3.11 to 3.15. - It listed Windows without saying that programs for Windows are exported on Linux and run from C++. It now links to the C++ guide section that shows how. Its link to the getting started page was also broken. - The PropagateDeviceConfig field comments said a dict on a skip flag sets per-method values. Only a dict of configs on the outer ExecutorchBackendConfig field does, as the CUDA page says. - The getting started page said to use the CPU packages on Windows, while CUDA packages are built for Windows too, and it called CUDA 13.4 nightly only. It now also says that on Windows they run from C++, and that programs for the CUDA backend are exported on Linux.
1 parent 94fb125 commit 2ad5e16

3 files changed

Lines changed: 54 additions & 45 deletions

File tree

‎docs/source/backends/cuda/cuda-overview.md‎

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,29 @@ The CUDA backend is the ExecuTorch solution for running models on NVIDIA GPUs. I
77
- **Optimized GPU Execution**: Uses AOTInductor to generate highly optimized CUDA kernels for model operators
88
- **Triton Kernel Support**: Leverages Triton for GEMM (General Matrix Multiply), convolution, and SDPA (Scaled Dot-Product Attention) kernels.
99
- **Quantization Support**: INT4 weight quantization with tile-packed format for improved performance and reduced memory footprint
10-
- **Cross-Platform**: Supports both Linux and Windows platforms
10+
- **Cross-Platform**: Runs on Linux, and on Windows from C++. Programs for Windows are exported on Linux.
1111
- **Multiple Model Support**: Works with various models including LLMs, vision-language models, and audio models
1212

1313
## Target Requirements
1414

1515
Below are the requirements for running a CUDA-delegated ExecuTorch model:
1616

17-
- **Hardware**: NVIDIA GPU with CUDA compute capability
18-
- **CUDA Toolkit**: CUDA 11.x or later (CUDA 12.x recommended)
19-
- **Operating System**: Linux or Windows
20-
- **Drivers**: PyTorch-Compatible NVIDIA GPU drivers installed
17+
- **Hardware**: NVIDIA GPU with compute capability 8.0 (Ampere) or newer. This is the floor the
18+
prebuilt packages are built for.
19+
- **CUDA**: 13 for the prebuilt packages. On Linux they bring the CUDA runtime with them. On
20+
Windows it comes from a CUDA 13 toolkit install. A build from source also supports CUDA 12.6.
21+
- **Operating System**: Linux, or Windows from C++ for programs exported on Linux (see
22+
[CUDA on Windows](../../using-executorch-cpp.md#cuda-on-windows))
23+
- **Drivers**: An NVIDIA driver that supports the CUDA version you use: CUDA 13 for the prebuilt
24+
packages.
2125

2226
## Development Requirements
2327

2428
To develop and export models using the CUDA backend:
2529

26-
- **Python**: Python 3.8+
30+
- **Python**: Python 3.11 to 3.15
2731
- **PyTorch**: PyTorch with CUDA support
32+
- **CUDA Toolkit**: export compiles kernels with `nvcc`, the CUDA compiler, so it needs a CUDA toolkit
2833
- **ExecuTorch**: Install ExecuTorch with CUDA backend support
2934

3035
## Using the CUDA Backend
@@ -83,7 +88,7 @@ For a complete working example, see the [CUDA export script](https://github.com/
8388

8489
## Runtime Integration
8590

86-
To run the model on device, use the standard ExecuTorch runtime APIs. See [Running on Device](getting-started.md#running-on-device) for more information.
91+
To run the model on device, use the standard ExecuTorch runtime APIs. See [Running on Device](../../getting-started.md#running-on-device) for more information.
8792

8893
When building from source, pass `-DEXECUTORCH_BUILD_CUDA=ON` when configuring the CMake build to compile the CUDA backend.
8994

@@ -140,13 +145,10 @@ exec_program = et_program.to_executorch(
140145
skip_d2h_for_method_outputs=True,
141146
),
142147
enable_non_cpu_memory_planning=True,
143-
# Required alongside the skips. Memory planning reserves a buffer for graph inputs by
144-
# default, and the runtime fills a planned input by copying the caller's memory into that
145-
# buffer, which reintroduces the copy you just asked to skip. On device memory that copy is
146-
# a host memcpy into a device pointer, which is undefined.
147-
# Outputs stay planned, so the runtime allocates them on the device and hands back a
148-
# pointer to its own buffer. See the note below for when to unplan them instead.
149-
memory_planning_pass=MemoryPlanningPass(alloc_graph_input=False),
148+
# Keeps the method from reserving its own input and output memory.
149+
memory_planning_pass=MemoryPlanningPass(
150+
alloc_graph_input=False, alloc_graph_output=False
151+
),
150152
)
151153
)
152154
```
@@ -157,7 +159,8 @@ outputs = method([torch.randn(4, 64, device="cuda")])
157159
```
158160

159161
Outputs also stay on the device, which is what you want when the next stage of your pipeline is
160-
also on the GPU.
162+
also on the GPU. From Python, each output comes back in GPU memory that the next call does not
163+
overwrite, unless you pass `clone_outputs=False`.
161164

162165
### Things worth knowing
163166

@@ -170,24 +173,17 @@ it to `False` while asking for either skip raises a `ValueError`, because copy i
170173
during device-aware memory planning.
171174

172175
**Inputs must be unplanned**, via `MemoryPlanningPass(alloc_graph_input=False)`. Without it the
173-
program reserves its own input buffer and the runtime copies the caller's memory into it, so the
174-
copy comes back at run time. On device memory that copy is a host memcpy into a device pointer,
175-
which is undefined, so the program crashes rather than returning a wrong answer.
176-
177-
**Outputs have two valid shapes, and which one you want depends on the language.**
178-
179-
Leave outputs planned, which is the default, when the caller cannot supply device memory itself.
180-
The planned buffer is allocated on the device because `enable_non_cpu_memory_planning` is on, and
181-
`skip_d2h_for_method_outputs` means nothing copies it back, so the result stays on the device and
182-
the caller receives a pointer to the runtime's own buffer. That buffer belongs to the method and is
183-
reused by the next execution, so read or copy the result before running again. This is the shape
184-
every Python caller wants, and it is what the exporters in this repository use, for example
185-
`examples/models/gemma4_31b/export.py`.
186-
187-
Set `alloc_graph_output=False` only when the caller supplies the output memory itself, which today
188-
means a C++ caller handing the runtime a device tensor through `Module::set_output`. The Python
189-
bindings allocate host memory for an unplanned output, so a device resident method exported this
190-
way cannot run from Python.
176+
program reserves its own input buffer on the device, and the runtime refuses to copy the caller's
177+
tensor into it. The call fails with error `0x10` (`NotSupported`), and the log says
178+
`Planned-buffer copy needs host memory on both sides`.
179+
180+
**Leave outputs unplanned too**, via `alloc_graph_output=False`. Then each result lands in memory
181+
you own, with no extra copy. A C++ caller must give each unplanned output a buffer once with
182+
`Module::set_output`, before the first run: GPU memory when the output copy is skipped, host
183+
memory otherwise. Pass each output's index to `set_output`; the C++ example below sets output 0. If
184+
you keep outputs planned instead, they live in the method's own memory and the next run overwrites
185+
them. Python's `execute()` then copies each output on every call, unless you pass
186+
`clone_outputs=False`.
191187

192188
**Per-method selection is on the outer config, not on the flags.** Pass a dict of
193189
`PropagateDeviceConfig` keyed by method name:
@@ -221,6 +217,11 @@ device right after the scalar type. Nothing is copied:
221217

222218
```cpp
223219
auto input = from_blob(device_data, {rows, columns}, ScalarType::Float, DeviceType::CUDA);
220+
auto output = from_blob(device_result, {rows, columns}, ScalarType::Float, DeviceType::CUDA);
221+
// Once, before the first run. Every run then writes its result to device_result.
222+
if (module.set_output(output) != Error::Ok) {
223+
return 1;
224+
}
224225
auto result = module.forward(input);
225226
```
226227

@@ -235,6 +236,7 @@ across, which costs one transfer:
235236
```cpp
236237
auto host_input = make_tensor_ptr({rows, columns}, std::move(data));
237238
auto input = clone_tensor_ptr_to(host_input, DeviceType::CUDA);
239+
// Set the output with module.set_output first, as above.
238240
auto result = module.forward(input);
239241
```
240242

‎docs/source/getting-started.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ pip install executorch torch \
3737
| NVIDIA GPU, CUDA 13.2 | `cu132` |
3838
| NVIDIA GPU, CUDA 13.4 | `cu134` |
3939

40-
The CUDA packages are built for Linux, on x86_64 and ARM64. Use the `cpu`
41-
variant on macOS and on Windows. If your CUDA version is not in the table,
40+
The CUDA packages are built for Linux, on x86_64 and ARM64, and for Windows on
41+
x86_64. On Windows they run CUDA programs from C++, and programs for the CUDA
42+
backend are exported on Linux (see [CUDA on Windows](using-executorch-cpp.md#cuda-on-windows)).
43+
Use the `cpu` variant on macOS. If your CUDA version is not in the table,
4244
choose the closest lower one with the same major version, because CUDA works
4345
across minor versions but not across major ones. There is no package for CUDA 12,
4446
so on CUDA 12 use the `cpu` variant or build from source. On CUDA 13.0 or 13.1 no
@@ -54,8 +56,7 @@ To get a change that has landed on the `main` branch but is not in a release
5456
yet, use a nightly build. These are rebuilt every day. Put `nightly/` in front
5557
of the variant name, for example `nightly/cu132`, and add `--pre` to the
5658
command, otherwise pip skips development versions. Nightly builds cover the same
57-
variants. CUDA 13.4 is the newest, and until the next release it is in nightly
58-
builds only.
59+
variants.
5960

6061
The second index is needed because a bare `--index-url` replaces PyPI instead of
6162
adding to it, and some dependencies are published only on PyPI.

‎exir/passes/propagate_device_config.py‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
class PropagateDeviceConfig:
2828
"""Controls whether the runtime copies method inputs and outputs across the device boundary.
2929
30-
Skipping a copy also means the runtime must not reserve its own buffer for the tensor, or it
31-
would fill that buffer from the caller's memory and the copy would come back. Memory planning
32-
allocates graph inputs and outputs by default, so a program using either skip below needs::
30+
Skipping a copy also means memory planning should not reserve its own buffer for the tensor.
31+
Memory planning allocates graph inputs and outputs by default, so a program using both skips
32+
below should use::
3333
3434
ExecutorchBackendConfig(
3535
propagate_device_config=PropagateDeviceConfig(
@@ -43,7 +43,9 @@ class PropagateDeviceConfig:
4343
)
4444
4545
Without ``alloc_graph_input=False`` the runtime reserves device memory for an input the caller
46-
already owns, then copies into it with a host memcpy, which is undefined for device memory.
46+
already owns and refuses to copy into it, so every call fails with ``NotSupported``. Planned
47+
outputs work, but they live in the method's memory, which the next run overwrites, and the
48+
Python ``execute()`` copies them unless it gets ``clone_outputs=False``.
4749
"""
4850

4951
# When True, method-level input tensors that feed directly into a device
@@ -52,17 +54,21 @@ class PropagateDeviceConfig:
5254
# pre-staged on GPU.
5355
#
5456
# Pair with MemoryPlanningPass(alloc_graph_input=False), or the runtime reserves its own buffer
55-
# for the input and copies the caller's memory into it, which is what this exists to avoid.
56-
# A dict can be used to set per-method values, keyed by method name.
57+
# for the input and refuses the caller's tensor.
58+
# For per-method values, pass a dict of PropagateDeviceConfig, keyed by method name, as the
59+
# propagate_device_config of ExecutorchBackendConfig.
5760
skip_h2d_for_method_inputs: Union[bool, Dict[str, bool]] = False
5861

5962
# When True, device delegate outputs that are directly method outputs
6063
# are NOT wrapped with _d2h_copy. The method outputs stay on device.
6164
# Useful for cross-method GPU pipelines where the next method consumes
6265
# GPU tensors directly.
6366
#
64-
# Pair with MemoryPlanningPass(alloc_graph_output=False) for the same reason as the input flag.
65-
# A dict can be used to set per-method values, keyed by method name.
67+
# Planned outputs work, but they live in the method's memory, which the next run overwrites,
68+
# and the Python runtime copies them. MemoryPlanningPass(alloc_graph_output=False) avoids
69+
# that copy, and then a C++ caller must set each output with Module::set_output.
70+
# For per-method values, pass a dict of PropagateDeviceConfig, keyed by method name, as the
71+
# propagate_device_config of ExecutorchBackendConfig.
6672
skip_d2h_for_method_outputs: Union[bool, Dict[str, bool]] = False
6773

6874
def __hash__(self) -> int:

0 commit comments

Comments
 (0)