Skip to content

Commit c1d7efb

Browse files
authored
[PyBindings] Fix wrong results for inputs in another layout, refuse stale or mixed state, return CUDA outputs without copies (#23957)
## What is wrong today - An input in another dimension order, such as a transposed array, is silently read as contiguous. - After `set_inputs()` fails partway, `execute()` runs on a mix of new and old inputs (#23913), and before any `set_inputs()` it reads unset inputs. Modules have the same problems. - A module reads freed memory once its program bytes are dropped. - Every CUDA output is copied once more before it is returned. - Without torch, CUDA results cannot reach the host. ## What this change does - The portable bindings copy an input in another dimension order into the exported order, on the host for buffers, on the device for torch tensors. Other CUDA DLPack inputs in another order raise. ATen bindings still read them as they are. ```python method.set_inputs([array.T]) # was read as contiguous, now copied first ``` - `execute()` raises before the first `set_inputs()`, and after one that failed partway, until every input is passed again; a wrong count or dtype keeps the old inputs. `get_outputs()` raises after `set_inputs()` until `execute()` succeeds. Modules do the same. - Modules keep their program bytes. - With `clone_outputs=True` (the default), a CUDA output filling memory the bindings allocated is returned without a copy. While it lives, a method's next call uses a second buffer, then the two take turns. - A call waits for the device once after it runs, and before it runs only when an input may still be written on another library's stream. - `ExecuTorchResult` gains `to_host()`, `from_host()`, `dtype_name`, and a `repr`, and is exported from `executorch.runtime`. - Buffers accept complex numbers and a native byte-order prefix, CUDA DLPack inputs work on any GPU, and number inputs take NumPy numbers and must match the declared kind (an `int` works for a float). Input errors name the input and method. The state checks and input formats apply to both portable and ATen bindings. ## Testing New tests for each fixed problem fail without this change. The CUDA tests pass in both output modes on four GPUs (CI has one, so the multi-GPU test skips there). The portable suites pass, except two quantized tests that also fail without it. On a chain of 8 outputs on one GPU, a median call takes 0.37 ms, not 0.43 ms, with `Method`, and 0.31 ms, not 0.59 ms, with `Module` (5 runs of 200 calls). Fixes #23913.
1 parent 450cc5d commit c1d7efb

11 files changed

Lines changed: 2674 additions & 363 deletions

‎docs/source/runtime-python-api-reference.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ For detailed information on how APIs evolve and the deprecation process, please
1919

2020
.. autoclass:: Method
2121
:members: execute, metadata
22+
23+
.. autoclass:: ExecuTorchResult
24+
:members: shape, strides, dtype, dtype_name, nbytes, to_host, from_host
25+
26+
Without PyTorch, ``Method.execute`` returns ``ExecuTorchResult`` values: read a CPU result through the buffer protocol, NumPy, or DLPack, read a CUDA result through DLPack or ``to_host()``, and place host data on a CUDA device with ``ExecuTorchResult.from_host()``.

‎extension/pybindings/README.md‎

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,27 @@ pip install . --no-build-isolation
1111
## Portable tensor mode
1212

1313
Portable bindings do not link against ATen or PyTorch. CPU inputs may be NumPy
14-
arrays or other dense objects implementing Python's buffer protocol. CPU and
14+
arrays or other dense objects implementing Python's buffer protocol, including
15+
`ctypes` arrays, `array.array`, and complex NumPy arrays. CPU and
1516
CUDA `torch.Tensor` inputs are inspected through their public Python attributes
1617
instead of being cast to `at::Tensor`. DLPack producers, including CuPy, can
1718
provide CPU or CUDA allocations directly; their device indices are preserved.
1819
Tensor protocols can be mixed within the same call.
1920

20-
Tensor inputs must have dense storage laid out for the exported method's
21-
dimension order. Stride compatibility checks are deferred; the bindings interpret
22-
Torch and DLPack tensor storage using the exported layout without copying data.
21+
Tensor inputs must have dense storage. In the portable bindings, an input laid
22+
out in a different dimension order than the exported method, for example a
23+
channels-last tensor for a contiguous method or a transposed array, is copied
24+
into the exported layout: on the host for buffers and CPU DLPack tensors, and by
25+
torch on the tensor's own device for torch tensors. Read-only buffers are
26+
copied too, in any layout. A non-torch CUDA DLPack input must already be in the
27+
exported layout. The ATen bindings do not convert inputs, and their runtime
28+
reads every input as contiguous, so pass contiguous inputs there.
29+
30+
Pass an input again after changing it. A method exported with the default
31+
memory planning copies every input into its own memory, and a converted input
32+
is a copy too. Only a method exported with
33+
`MemoryPlanningPass(alloc_graph_input=False)` reads an unconverted, writable
34+
input in place, and only such an input sees the method's own in-place updates.
2335
CUDA inputs require a method
2436
exported with CUDA inputs and `MemoryPlanningPass(alloc_graph_input=False)` so
2537
execution can share the caller's device allocation.
@@ -28,13 +40,25 @@ When PyTorch is available, importing `portable_lib` imports it and portable
2840
bindings return `torch.Tensor` outputs. When it cannot be imported, the default
2941
output type is `ExecuTorchResult`. Output conversion uses Python APIs and
3042
DLPack; the extension does not link against PyTorch.
31-
`clone_outputs=True` returns independent storage on the output's original
32-
device. With `clone_outputs=False`, results share runtime or input storage and
33-
later execution may overwrite that storage.
43+
`clone_outputs=True` returns storage that later executions do not overwrite,
44+
on the output's original device. With `clone_outputs=False`, results share
45+
runtime or input storage and later execution may overwrite that storage.
46+
47+
A CUDA output that fills memory the bindings allocated for it is returned
48+
without a copy when `clone_outputs=True`. The result keeps that memory, so a
49+
method writes its next execution into a second buffer, and the two take turns;
50+
a module allocates new memory each call. Reading the outputs again before the
51+
next execution returns the same object, so an in-place change to one read
52+
shows in the other. In torch output mode such a result is a view of memory
53+
torch does not own, so it cannot grow with `resize_()`.
54+
55+
`dtype_name` names a result's dtype, including dtypes NumPy lacks, such as
56+
`"bfloat16"`.
3457

3558
Calls on a shared method or module are serialized. A sequence of separate
36-
`set_inputs`, `execute`, and `get_outputs` calls requires caller synchronization
37-
when each thread needs its own inputs and outputs.
59+
`set_inputs`, `execute`, and `get_outputs` calls on a shared method needs a lock
60+
held by the caller around the whole sequence: `set_inputs` hides the outputs
61+
until the next `execute`, so another thread's call can land in between.
3862

3963
To select `ExecuTorchResult` outputs even when PyTorch is installed, set the
4064
environment variable before importing the bindings. The setting is fixed when
@@ -63,12 +87,27 @@ CPU and CUDA results implement `__dlpack__` and `__dlpack_device__`. CUDA result
6387
remain on CUDA and can be passed to another DLPack consumer or back into
6488
ExecuTorch without moving the data to the CPU.
6589

90+
Without torch or another DLPack library, `to_host()` copies a result to CPU
91+
memory, and `ExecuTorchResult.from_host()` copies a CPU buffer to a CUDA device
92+
for a method whose inputs live there:
93+
94+
```python
95+
from executorch.extension.pybindings.portable_lib import ExecuTorchResult
96+
97+
device_input = ExecuTorchResult.from_host(numpy_input, device_index=0)
98+
output = method([device_input])[0]
99+
result = np.asarray(output.to_host())
100+
```
101+
66102
CUDA DLPack inputs and `ExecuTorchResult` outputs require CUDA-enabled bindings.
67103
Torch CUDA inputs and outputs synchronize through Python Torch, including in
68-
CPU builds of the bindings. The Python CUDA execution boundary waits for device
69-
work because the device allocator does not expose readiness events.
70-
Execution is synchronous at this boundary, including producer and consumer
71-
stream readiness. The synchronization has no C++ ATen dependency.
104+
CPU builds of the bindings. Execution is synchronous at the Python boundary:
105+
a call returns after its device work has finished, so its outputs are ready on
106+
any stream. Each call waits for the device once, rather than once per tensor.
107+
Before running, it also waits once if an input may still be written on another
108+
library's stream: a torch CUDA tensor, a DLPack input from another library, or
109+
an `ExecuTorchResult` whose memory was handed out through `__dlpack__`. The
110+
synchronization has no C++ ATen dependency.
72111

73112
For bfloat16 inputs, raw byte buffers use the shape declared by the exported
74113
method. Bfloat16 results expose flat bytes through the buffer protocol because
@@ -87,7 +126,10 @@ The CUDA build workflow runs `test_pybindings_cuda_dlpack.py` in both output
87126
modes on a GPU. It checks device allocations, DLPack stream ordering, channels
88127
last views, cloning, and borrowed output lifetimes using a CUDA delegate.
89128
Set `EXECUTORCH_PYBIND_TEST_REQUIRE_CUDA=1` when running this test to treat missing
90-
GPU hardware or a missing CUDA backend as a failure.
129+
GPU hardware or a missing CUDA backend as a failure. The CUDA delegate's kernels
130+
need the OpenMP runtime (`libgomp.so.1` on Linux). PyTorch ships one; without PyTorch,
131+
install it from the system, or the two tests that run without PyTorch fail
132+
under this setting.
91133

92134
# Link Backends
93135

0 commit comments

Comments
 (0)