Skip to content

Commit 00ffa2e

Browse files
committed
Import torch only when the Python runtime returns torch tensors, and warn before outputs follow inputs
Importing executorch.runtime imports PyTorch whenever it is installed, even for a program that only passes NumPy arrays. Only setting EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT=executorch avoids that, and never loading PyTorch for NumPy callers would change what they get back. So that change has to be announced first. Without the setting, importing executorch.runtime no longer imports torch. The outputs stay as they are today: torch.Tensor whenever PyTorch is installed, and ExecuTorchResult without it. So a program with only NumPy inputs still loads torch, at its first call that returns a tensor. A future release will make tensor outputs follow each call's tensor inputs, including inputs kept from an earlier call: - any torch.Tensor input: torch.Tensor outputs; - only NumPy, other buffer, DLPack or ExecuTorchResult inputs: ExecuTorchResult outputs, even when PyTorch is installed; - no tensor inputs: torch.Tensor outputs only if torch is already imported. get_attribute() and bundled program verification count as calls without tensor inputs. The first call whose outputs will change shows one FutureWarning, which says how to keep torch.Tensor outputs or get ExecuTorchResult outputs now. A call that runs the method picks the type first, so with an error filter for this FutureWarning, such a call raises before it runs the method (its inputs are already installed). A torch that is installed but fails to import, even with an ImportError, raises its own error, with a note on how to skip torch. EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT still fixes the type: "executorch" never imports torch, and "torch" imports it when ExecuTorch is imported. Tested on Linux x86_64 with Python 3.12 and a CUDA build of PyTorch: the pybindings, runtime, bundled program and CUDA DLPack suites give the same results as on main, and the standalone no-ATen job passes with and without PyTorch. Importing executorch.runtime takes 0.04 s and 18 MB of peak memory instead of 1.9 s and 506 MB. A new test runs each case of the rule in a fresh process, and each of its checks fails when its part of the change is removed.
1 parent c17afa3 commit 00ffa2e

8 files changed

Lines changed: 646 additions & 185 deletions

File tree

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,21 @@ Other buffers, such as ``memoryview``, work as inputs too. NumPy has no bfloat16
4343

4444
For data on a GPU, use DLPack, the standard way to share tensors between Python libraries. This needs ExecuTorch built with CUDA support. Pass a CuPy array to a method that takes CUDA inputs. Read a CUDA result with ``cupy.from_dlpack(out)``, or copy it to the CPU with ``out.to_host()``. ``ExecuTorchResult.from_host()`` copies CPU data to a CUDA device.
4545

46-
To get ``ExecuTorchResult`` values even when PyTorch is installed, set ``EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT=executorch`` before importing ExecuTorch.
46+
Output types
47+
~~~~~~~~~~~~
48+
49+
Importing ``executorch.runtime`` does not import PyTorch, unless the setting below asks for ``torch``. When PyTorch is installed, ``Method.execute`` returns ``torch.Tensor`` outputs, and the first call that returns one imports ``torch``. Without PyTorch, it returns ``ExecuTorchResult`` outputs. If PyTorch is installed but fails to import, every call that returns a tensor raises the import error; set the setting below to ``executorch`` to run without PyTorch.
50+
51+
A future release will make the outputs follow the tensor inputs of each call. A call with any ``torch.Tensor`` input will return ``torch.Tensor`` outputs. A call whose tensor inputs are all NumPy arrays, other buffers, DLPack objects, or ``ExecuTorchResult`` values will return ``ExecuTorchResult`` outputs, even when PyTorch is installed. A call with no tensor inputs will return ``torch.Tensor`` outputs only if ``torch`` is already imported. The first call whose outputs will change shows a ``FutureWarning``.
52+
53+
To choose the output type now, set ``EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT`` before importing ExecuTorch. With ``executorch``, every call returns ``ExecuTorchResult`` outputs and ExecuTorch never imports PyTorch. With ``torch``, every call returns ``torch.Tensor`` outputs, and importing ExecuTorch imports PyTorch.
54+
55+
.. code-block:: python
56+
57+
import os
58+
59+
os.environ["EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT"] = "executorch"
60+
from executorch.runtime import Runtime
4761
4862
CUDA streams
4963
~~~~~~~~~~~~

‎extension/pybindings/README.md‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,14 @@ execution can share the caller's device allocation.
4141
input `i`, as `"cpu"` or `"cuda:N"`, and `output_tensor_meta(i).device()` does
4242
the same for its outputs.
4343

44-
When PyTorch is available, importing `portable_lib` imports it and portable
45-
bindings return `torch.Tensor` outputs. When it cannot be imported, the default
46-
output type is `ExecuTorchResult`. Output conversion uses Python APIs and
47-
DLPack; the extension does not link against PyTorch.
44+
Unless `EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT` is `torch`, importing
45+
`portable_lib` does not import PyTorch. When PyTorch is installed, portable
46+
bindings return `torch.Tensor` outputs, and the first call that returns one
47+
imports `torch`. When PyTorch is not installed, outputs are `ExecuTorchResult`
48+
values. When it is installed but fails to import, every call that returns a
49+
tensor raises the import error; set the variable to `executorch` to run
50+
without PyTorch. Output conversion uses Python APIs and DLPack; the extension
51+
does not link against PyTorch.
4852
`clone_outputs=True` returns storage that later executions do not overwrite,
4953
on the output's original device. With `clone_outputs=False`, results share
5054
runtime or input storage and later execution may overwrite that storage.
@@ -91,9 +95,16 @@ with `clone_outputs=False` lives in the method's output memory, so passing it
9195
back in as an input means each run reads what the run before it wrote there;
9296
pass a copied result instead.
9397

94-
To select `ExecuTorchResult` outputs even when PyTorch is installed, set the
95-
environment variable before importing the bindings. The setting is fixed when
96-
the extension loads, so importing PyTorch later does not change the output type.
98+
A future release will make tensor outputs follow the tensor inputs of each
99+
call, including inputs kept from an earlier call. A call with any torch tensor
100+
input will return torch tensors. A call whose tensor inputs are all NumPy
101+
arrays, other buffers, DLPack objects, or `ExecuTorchResult` values will return
102+
`ExecuTorchResult` values, even when PyTorch is installed. A call with no tensor
103+
inputs will return torch tensors only if `torch` is already imported. The first
104+
call whose outputs will change shows a `FutureWarning`. To choose the output
105+
type now, set `EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT` to `executorch` or `torch`
106+
before importing the bindings. The setting is fixed when the extension loads,
107+
and `executorch` never imports PyTorch.
97108

98109
```python
99110
import os
@@ -105,11 +116,9 @@ from executorch.extension.pybindings import portable_lib
105116
`ExecuTorchResult` preserves shape, strides, dtype, and device. CPU results expose
106117
the Python buffer protocol so NumPy can read them without another copy. A result
107118
is not a number or a truth value: `bool()`, `int()`, and `float()` raise
108-
`TypeError`, so convert it first, for example with `np.asarray(output)`. Without
109-
the environment setting, the bindings fall back to `ExecuTorchResult` if
110-
PyTorch cannot be imported. Explicitly selecting `torch` requires it to import
111-
successfully. ATen-mode bindings continue to return native PyTorch tensors
112-
directly.
119+
`TypeError`, so convert it first, for example with `np.asarray(output)`.
120+
Explicitly selecting `torch` requires it to import successfully. ATen-mode
121+
bindings continue to return native PyTorch tensors directly.
113122

114123
```python
115124
output = module.forward([numpy_input])[0]
@@ -138,8 +147,9 @@ result = np.asarray(output.to_host())
138147
Copying CUDA memory requires CUDA-enabled bindings: `from_host()`, `to_host()`
139148
of a CUDA result, and a CUDA `ExecuTorchResult` with `clone_outputs=True`.
140149
Bindings built without CUDA support synchronize through Python Torch instead,
141-
so while torch is loaded they still read CUDA inputs, torch tensors or DLPack,
142-
and return borrowed CUDA results.
150+
so once torch is imported, by you or by a call that returns torch tensors, they
151+
still read CUDA inputs, torch tensors or DLPack, and return borrowed CUDA
152+
results.
143153

144154
Execution is synchronous at the Python boundary: a call returns after its device
145155
work has finished, so its outputs are ready on any stream. How long a call with

‎extension/pybindings/portable_lib.py‎

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,19 @@ class _LightweightExperimentalWarning(DeprecationWarning):
2828

2929
_TENSOR_OUTPUT_ENV = "EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT"
3030
_tensor_output = os.environ.get(_TENSOR_OUTPUT_ENV)
31-
if _tensor_output in (None, "torch"):
31+
# Without the setting, the first call that returns torch.Tensor outputs imports
32+
# torch, so importing ExecuTorch does not load it.
33+
if _tensor_output == "torch":
3234
try:
33-
# Preserve the existing API: importing portable_lib also imports Torch,
34-
# and tensor outputs are torch.Tensor objects.
35-
import torch as _torch
35+
_import_module("torch")
3636
except ImportError as error:
37-
if _tensor_output == "torch":
38-
raise ImportError(
39-
"ExecuTorch portable pybindings were configured to return "
40-
"torch.Tensor outputs, but PyTorch could not be imported. "
41-
f"Install PyTorch or set {_TENSOR_OUTPUT_ENV}=executorch "
42-
"before importing portable_lib."
43-
) from error
44-
_torch = None
45-
elif _tensor_output == "executorch":
46-
_torch = None
47-
else:
37+
raise ImportError(
38+
"ExecuTorch portable pybindings were configured to return "
39+
"torch.Tensor outputs, but PyTorch could not be imported. "
40+
f"Install PyTorch or set {_TENSOR_OUTPUT_ENV}=executorch "
41+
"before importing portable_lib."
42+
) from error
43+
elif _tensor_output not in (None, "executorch"):
4844
raise ValueError(
4945
f"{_TENSOR_OUTPUT_ENV} must be either 'torch' or 'executorch', "
5046
f"but was {_tensor_output!r}"
@@ -152,7 +148,6 @@ class _LightweightExperimentalWarning(DeprecationWarning):
152148
del _bindings
153149
del _tensor_output
154150
del _TENSOR_OUTPUT_ENV
155-
del _torch
156151
del _warning_category
157152
del _import_module
158153
del _exir_warnings

0 commit comments

Comments
 (0)