Skip to content

Commit 315be40

Browse files
committed
Import torch to wait for CUDA work in bindings built without CUDA support
Bindings built without CUDA support have no CUDA runtime of their own, so they wait for CUDA work through torch. Before torch was imported lazily, importing ExecuTorch imported it whenever it was installed. Now a wait found torch missing unless something else had imported it, and failed with an error that blamed the build. That broke CUDA inputs from other libraries, such as CuPy, in 3 places: set_inputs() followed by execute(), a call to a method that returns no tensor, and get_attribute() of a CUDA attribute. A wait now imports torch, unless EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT is executorch, which keeps torch out. When torch is still not there, the error says that imported torch would also work, and the note on a torch that fails to import names the wait as a reason too.
1 parent d65a186 commit 315be40

4 files changed

Lines changed: 128 additions & 12 deletions

File tree

‎extension/pybindings/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ device_input = ExecuTorchResult.from_host(
157157
Copying CUDA memory requires CUDA-enabled bindings: `from_host()`, `to_host()`
158158
of a CUDA result, and a CUDA `ExecuTorchResult` with `clone_outputs=True`.
159159
Bindings built without CUDA support synchronize through Python Torch instead,
160-
so once torch is imported, by you or by a call that returns torch tensors, they
161-
still read CUDA inputs, torch tensors or DLPack, and return borrowed CUDA
162-
results.
160+
and import torch to do so unless `EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT` is
161+
`executorch`. So with torch installed, or imported by you, they still read
162+
CUDA inputs, torch tensors or DLPack, and return borrowed CUDA results.
163163

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

‎extension/pybindings/pybindings.cpp‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -805,8 +805,9 @@ bool torch_available() {
805805
if (!error.matches(PyExc_ImportError)) {
806806
try {
807807
error.value().attr("add_note")(
808-
"ExecuTorch imports torch here to return torch.Tensor outputs. "
809-
"Set EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT=executorch before "
808+
"ExecuTorch imports torch here to return torch.Tensor outputs "
809+
"or to wait for CUDA work. Set "
810+
"EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT=executorch before "
810811
"importing ExecuTorch to skip it.");
811812
} catch (...) {
812813
// add_note is best effort; never hide the original error.
@@ -845,7 +846,8 @@ bool keep_torch_outputs() {
845846
// then, keep_torch_outputs() keeps torch.Tensor where that rule would change
846847
// it. A call with no tensor inputs returns torch.Tensor whenever torch can be
847848
// imported, now and later, so its type never depends on what imported torch
848-
// first. A call that returns no tensor never imports torch.
849+
// first. Picking the type for a call that returns no tensor never imports
850+
// torch.
849851
PortableTensorOutput portable_tensor_output(
850852
bool returns_tensor,
851853
bool torch_input,
@@ -2369,6 +2371,18 @@ std::shared_ptr<DeviceMemoryBuffer> allocate_device_output(
23692371
return std::make_shared<DeviceMemoryBuffer>(std::move(buffer.get()));
23702372
}
23712373

2374+
// Bindings built without CUDA support wait for CUDA work through torch, which
2375+
// used to be imported with ExecuTorch. So a wait imports it, unless the
2376+
// setting keeps torch out.
2377+
void synchronize_device_for_call(int index) {
2378+
#ifndef EXECUTORCH_PYBINDINGS_CUDA
2379+
if (tensor_output_override() != PortableTensorOutput::ExecuTorch) {
2380+
torch_available();
2381+
}
2382+
#endif
2383+
synchronize_device(index);
2384+
}
2385+
23722386
// The Python boundary is synchronous, and each wait is for the whole device,
23732387
// so one wait per device covers every tensor of a run. Before the run, another
23742388
// library may still be writing a CUDA input on its own streams: a torch tensor,
@@ -2397,7 +2411,7 @@ void synchronize_tensor_devices(const Method& method) {
23972411
add(method.get_output(i));
23982412
}
23992413
for (const auto index : devices) {
2400-
synchronize_device(index);
2414+
synchronize_device_for_call(index);
24012415
}
24022416
}
24032417

@@ -4879,7 +4893,7 @@ struct PyMethod final {
48794893
#else
48804894
// No execute() in this call has waited for the work that last wrote it.
48814895
if (!attr->device().is_cpu() && attr->nbytes() != 0) {
4882-
synchronize_device(attr->device().index());
4896+
synchronize_device_for_call(attr->device().index());
48834897
}
48844898
// Mutable state lives in the method's planned memory. Anything else is a
48854899
// constant, which can sit in read-only pages, so it is never returned as

‎extension/pybindings/pybindings_executorch_result.h‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,15 @@ inline void synchronize_torch_cuda_device(int index) {
135135
// once torch is imported.
136136
inline void synchronize_device(int index) {
137137
#ifndef EXECUTORCH_PYBINDINGS_CUDA
138-
if (loaded_torch_module() != nullptr) {
139-
synchronize_torch_cuda_device(index);
140-
return;
138+
if (loaded_torch_module() == nullptr) {
139+
throw std::runtime_error(
140+
"CUDA tensor interchange requires bindings built with CUDA support, "
141+
"or torch imported to wait for CUDA work");
141142
}
142-
#endif
143+
synchronize_torch_cuda_device(index);
144+
#else
143145
synchronize_cuda_device(index);
146+
#endif
144147
}
145148

146149
/** Tensor result returned by portable Python bindings. */

‎extension/pybindings/test/test_pybindings.py‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4161,6 +4161,105 @@ def forward(self, x, y):
41614161
method.execute(cuda_inputs)[0].cpu(), torch.full((4, 4), 4.0)
41624162
)
41634163

4164+
def test_cuda_input_imports_torch_to_wait_without_cuda_support(self):
4165+
if self.kernel_mode != "portable":
4166+
self.skipTest("only portable bindings wait for CUDA work themselves")
4167+
if not torch.cuda.is_available():
4168+
self.skipTest("needs a visible CUDA device")
4169+
from executorch.extension.pybindings import portable_lib
4170+
4171+
if "CudaBackend" in portable_lib._get_registered_backend_names():
4172+
self.skipTest("bindings built with CUDA support wait without torch")
4173+
4174+
from executorch.exir._serialize._program import PTEFile, serialize_pte_binary
4175+
from executorch.exir.schema import ExtraTensorInfo, Tensor
4176+
4177+
class Identity(torch.nn.Module):
4178+
def forward(self, x):
4179+
return x
4180+
4181+
program = to_edge(export(Identity(), (torch.ones(4),))).to_executorch(
4182+
ExecutorchBackendConfig(
4183+
memory_planning_pass=MemoryPlanningPass(
4184+
alloc_graph_input=False, alloc_graph_output=False
4185+
)
4186+
)
4187+
)
4188+
schema = program.executorch_program
4189+
for entry in schema.execution_plan[0].values:
4190+
if isinstance(entry.val, Tensor):
4191+
entry.val.extra_tensor_info = ExtraTensorInfo(
4192+
device_type=DeviceType.CUDA, device_index=0
4193+
)
4194+
data = bytes(serialize_pte_binary(PTEFile(schema)))
4195+
# Device memory from the CUDA driver, so nothing imports torch before
4196+
# the bindings wait for the device.
4197+
script = """
4198+
import ctypes
4199+
import sys
4200+
4201+
from executorch.extension.pybindings import portable_lib
4202+
4203+
class Device(ctypes.Structure):
4204+
_fields_ = [("device_type", ctypes.c_int32), ("device_id", ctypes.c_int32)]
4205+
4206+
class DataType(ctypes.Structure):
4207+
_fields_ = [("code", ctypes.c_uint8), ("bits", ctypes.c_uint8), ("lanes", ctypes.c_uint16)]
4208+
4209+
class DLTensor(ctypes.Structure):
4210+
_fields_ = [
4211+
("data", ctypes.c_void_p),
4212+
("device", Device),
4213+
("ndim", ctypes.c_int32),
4214+
("dtype", DataType),
4215+
("shape", ctypes.POINTER(ctypes.c_int64)),
4216+
("strides", ctypes.POINTER(ctypes.c_int64)),
4217+
("byte_offset", ctypes.c_uint64),
4218+
]
4219+
4220+
class ManagedTensor(ctypes.Structure):
4221+
_fields_ = [("dl_tensor", DLTensor), ("manager_ctx", ctypes.c_void_p), ("deleter", ctypes.c_void_p)]
4222+
4223+
driver = ctypes.CDLL("libcuda.so.1")
4224+
context, pointer = ctypes.c_void_p(), ctypes.c_uint64()
4225+
assert driver.cuInit(0) == 0
4226+
assert driver.cuDevicePrimaryCtxRetain(ctypes.byref(context), 0) == 0
4227+
assert driver.cuCtxSetCurrent(context) == 0
4228+
assert driver.cuMemAlloc_v2(ctypes.byref(pointer), 16) == 0
4229+
shape = (ctypes.c_int64 * 1)(4)
4230+
managed = ManagedTensor(
4231+
DLTensor(pointer.value, Device(2, 0), 1, DataType(2, 32, 1), shape, None, 0)
4232+
)
4233+
make_capsule = ctypes.pythonapi.PyCapsule_New
4234+
make_capsule.restype = ctypes.py_object
4235+
make_capsule.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p]
4236+
4237+
with open(sys.argv[1], "rb") as source:
4238+
method = portable_lib._load_program_from_buffer(source.read()).load_method("forward")
4239+
assert "torch" not in sys.modules
4240+
method.set_inputs([make_capsule(ctypes.addressof(managed), b"dltensor", None)])
4241+
method.execute()
4242+
assert "torch" in sys.modules
4243+
"""
4244+
with tempfile.TemporaryDirectory() as directory:
4245+
path = os.path.join(directory, "cuda_identity.pte")
4246+
with open(path, "wb") as handle:
4247+
handle.write(data)
4248+
environment = os.environ.copy()
4249+
environment.pop("EXECUTORCH_PYBINDINGS_TENSOR_OUTPUT", None)
4250+
completed = subprocess.run(
4251+
[sys.executable, "-c", script, path],
4252+
env=environment,
4253+
capture_output=True,
4254+
text=True,
4255+
timeout=60,
4256+
)
4257+
self.assertEqual(
4258+
completed.returncode,
4259+
0,
4260+
f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}",
4261+
)
4262+
41644263
def test_cuda_torch_tensor_interchange_without_aten(self):
41654264
if self.kernel_mode != "portable":
41664265
self.skipTest("only portable bindings use the Python Torch adapter")

0 commit comments

Comments
 (0)