Problem
If you export a model so the caller supplies GPU tensors, running it crashes the process.
ExecuTorch can export a CUDA model two ways. By default it inserts copies at the delegate boundary, so
you pass ordinary CPU tensors and the runtime moves the data. You can also turn those copies off, which
tags the method's inputs as device memory and makes the caller responsible for passing GPU tensors:
program = lowered.to_executorch(
ExecutorchBackendConfig(
propagate_device_config=PropagateDeviceConfig(
skip_h2d_for_method_inputs=True,
skip_d2h_for_method_outputs=True,
),
enable_non_cpu_memory_planning=True,
)
)
That is the documented way to avoid a redundant copy when your data is already on the GPU. Running such a
program segfaults.
Why
Setting an input goes through copy_tensor_data, which is an unconditional host copy
(runtime/core/exec_aten/util/tensor_util_portable.cpp):
std::memcpy(
t_dst.mutable_data_ptr(), t_src.const_data_ptr(), t_src.nbytes());
Called from Method::set_input (runtime/executor/method.cpp:1247).
In this mode the destination is device memory, which is not mapped into the host address space, so the
copy faults. Stack:
Module::execute
Method::set_input
copy_tensor_data
memcpy <- faults on a device pointer
The no-copy path appears to assume set_input will skip the copy for a device-resident input. It does
not.
Reproducing
import torch
from executorch.backends.cuda.cuda_partitioner import CudaPartitioner
from executorch.exir import ExecutorchBackendConfig, to_edge_transform_and_lower
from executorch.exir.passes.propagate_device_config import PropagateDeviceConfig
from executorch.extension.pybindings import portable_lib
class Net(torch.nn.Module):
def __init__(self):
super().__init__()
self.register_buffer("scale", torch.full((64,), 1.5))
def forward(self, x):
return torch.relu(x * self.scale) * 2.0
model = Net().eval().cuda()
example = (torch.randn(4, 64, device="cuda"),)
lowered = to_edge_transform_and_lower(
torch.export.export(model, example), partitioner=[CudaPartitioner([])]
)
program = lowered.to_executorch(
ExecutorchBackendConfig(
propagate_device_config=PropagateDeviceConfig(
skip_h2d_for_method_inputs=True,
skip_d2h_for_method_outputs=True,
),
enable_non_cpu_memory_planning=True,
)
)
open("model.pte", "wb").write(program.buffer)
module = portable_lib._load_for_executorch("model.pte")
module.forward([torch.randn(4, 64, device="cuda")]) # segfault
The same crash happens from a C++ application that builds its input over a cudaMalloc pointer, so this
is in the runtime rather than the Python bindings.
Observed on x86_64 with CUDA 13.0 and on an aarch64 GPU device with CUDA 13.0. The default export mode,
where the copies are inserted, is unaffected.
Suggested fix
copy_tensor_data should not assume both pointers are host memory. Two options:
Dispatch on where the memory lives. Ask the allocator or the tensor which device backs the pointer and
use a device copy when either side is not host memory. This keeps a single entry point for callers.
Skip the copy when the destination is already device memory. In the no-copy mode the caller has
deliberately placed the data where the delegate wants it, so the correct action at the boundary is to
adopt the pointer rather than copy it. That also removes the transfer the mode exists to avoid, so it is
probably the intended behaviour.
Either way the mode currently has no working runtime path, so a test that exports with these flags and
runs with a device pointer would keep it working once fixed.
Note
An earlier version of this report also described a constants-loading fault, based on this log line:
weights_blob '<hash>' not found or update fn is null
That was wrong and I have retracted it. The hash is sha256(""), an empty blob, so the lookup misses
because a partition has no constants of its own, and the message is expected at info level. A model that
emits the line runs correctly. My earlier failures were a delegate that had not been loaded and, in one
case, an input shape that did not match the exported program.
Problem
If you export a model so the caller supplies GPU tensors, running it crashes the process.
ExecuTorch can export a CUDA model two ways. By default it inserts copies at the delegate boundary, so
you pass ordinary CPU tensors and the runtime moves the data. You can also turn those copies off, which
tags the method's inputs as device memory and makes the caller responsible for passing GPU tensors:
That is the documented way to avoid a redundant copy when your data is already on the GPU. Running such a
program segfaults.
Why
Setting an input goes through
copy_tensor_data, which is an unconditional host copy(
runtime/core/exec_aten/util/tensor_util_portable.cpp):std::memcpy( t_dst.mutable_data_ptr(), t_src.const_data_ptr(), t_src.nbytes());Called from
Method::set_input(runtime/executor/method.cpp:1247).In this mode the destination is device memory, which is not mapped into the host address space, so the
copy faults. Stack:
The no-copy path appears to assume
set_inputwill skip the copy for a device-resident input. It doesnot.
Reproducing
The same crash happens from a C++ application that builds its input over a
cudaMallocpointer, so thisis in the runtime rather than the Python bindings.
Observed on x86_64 with CUDA 13.0 and on an aarch64 GPU device with CUDA 13.0. The default export mode,
where the copies are inserted, is unaffected.
Suggested fix
copy_tensor_datashould not assume both pointers are host memory. Two options:Dispatch on where the memory lives. Ask the allocator or the tensor which device backs the pointer and
use a device copy when either side is not host memory. This keeps a single entry point for callers.
Skip the copy when the destination is already device memory. In the no-copy mode the caller has
deliberately placed the data where the delegate wants it, so the correct action at the boundary is to
adopt the pointer rather than copy it. That also removes the transfer the mode exists to avoid, so it is
probably the intended behaviour.
Either way the mode currently has no working runtime path, so a test that exports with these flags and
runs with a device pointer would keep it working once fixed.
Note
An earlier version of this report also described a constants-loading fault, based on this log line:
That was wrong and I have retracted it. The hash is
sha256(""), an empty blob, so the lookup missesbecause a partition has no constants of its own, and the message is expected at info level. A model that
emits the line runs correctly. My earlier failures were a delegate that had not been loaded and, in one
case, an input shape that did not match the exported program.