Skip to content

Commit f3a9b06

Browse files
[release/1.6] Delete duplicate CUDA output handles once (#24490)
Supersedes #24448 with a clean release pick from #24420. Deletes each aliased CUDA output handle once, including error cleanup, and carries its regression test. The original three source files are the complete scope of this pick. ## Scope and landing order Land after #24489 in the verified landing order. This branch starts directly at release/1.6 commit `dfb78b9ecd933a5fee33ebc94f69ed4b71cbb1e7`. Its GitHub diff contains 3 files for this pick; it does not carry duplicate #24298 history or the excluded unmerged QNN work. Dependencies are documented here rather than embedded as other cherry-pick commits. ## Verification All changed Python files compile and pass Ruff F821 checks. All seven replacements merge cleanly in the documented sequence, including simulated squash merges. No QNN files are changed. Native CUDA, OpenVINO and portable C++ suites were not run locally on this macOS host; PR CI remains required before landing. Prepared with OpenAI Codex.
1 parent 1f49f64 commit f3a9b06

3 files changed

Lines changed: 52 additions & 5 deletions

File tree

‎backends/aoti/passes/replace_view_copy_with_view.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
ops.edge.aten.split_copy.Tensor: ops.edge.aten.split.Tensor,
3434
# Edge turns ``detach`` into ``detach_copy``, which has no c-shim either.
3535
# Without autograd it is ``alias_copy``, which Inductor can decompose,
36-
# unlike ``detach_copy``. A view would let two outputs share one handle,
37-
# and ``clone`` fails Edge verification in dim order mode.
36+
# unlike ``detach_copy``. ``clone`` fails Edge verification in dim order
37+
# mode.
3838
torch.ops.aten.detach_copy.default: torch.ops.aten.alias_copy.default,
3939
ops.edge.aten.detach_copy.default: ops.edge.aten.alias_copy.default,
4040
}

‎backends/cuda/runtime/cuda_backend.cpp‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,18 @@ class ET_EXPERIMENTAL CudaBackend final
10011001

10021002
bool run_called = false;
10031003

1004+
// AOTI puts the same handle in every slot of an output that it returns
1005+
// more than once, for example y.detach() returned twice. So only the last
1006+
// slot that holds a handle deletes it, as PyTorch's AOTI runner does.
1007+
const auto delete_output = [&](size_t i) noexcept {
1008+
for (size_t j = i + 1; j < slim_outputs.size(); j++) {
1009+
if (slim_outputs[j] == slim_outputs[i]) {
1010+
return;
1011+
}
1012+
}
1013+
delete slim_outputs[i];
1014+
};
1015+
10041016
// Scope guard: deletes any non-null slim_outputs on exit. Normal paths
10051017
// null entries as they take ownership, so the guard only fires on
10061018
// early-return error paths. Also cleans up inputs if run() was never
@@ -1011,7 +1023,7 @@ class ET_EXPERIMENTAL CudaBackend final
10111023
}
10121024
for (size_t i = 0; i < slim_outputs.size(); i++) {
10131025
if (slim_outputs[i]) {
1014-
delete slim_outputs[i];
1026+
delete_output(i);
10151027
}
10161028
}
10171029
});
@@ -1248,7 +1260,7 @@ class ET_EXPERIMENTAL CudaBackend final
12481260
if (pre_run_outputs[i] == slim_outputs[i]) {
12491261
// Case (a): AOTI wrote directly into our planned ET buffer. The result
12501262
// is already in place — nothing to copy, just drop the view wrapper.
1251-
delete slim_outputs[i];
1263+
delete_output(i);
12521264
} else {
12531265
// Case (b): AOTI returned its own buffer. D2D copy the result into the
12541266
// planned GPU ET buffer, then free AOTI's buffer.
@@ -1268,7 +1280,7 @@ class ET_EXPERIMENTAL CudaBackend final
12681280
slim_outputs[i], et_output, cuda_stream),
12691281
"Failed to D2D copy GPU output %zu into ETensor",
12701282
i);
1271-
delete slim_outputs[i];
1283+
delete_output(i);
12721284
}
12731285
slim_outputs[i] = nullptr;
12741286
}

‎backends/cuda/tests/test_cuda_export.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,41 @@ def forward(self, x, mask, column, idx, counts):
815815
for output, expected in zip(outputs, model(*inputs), strict=True):
816816
torch.testing.assert_close(output.cpu(), expected)
817817

818+
@unittest.skipIf(
819+
torch.version.hip is not None,
820+
"loads through the Python runtime, which the ROCm build does not include",
821+
)
822+
def test_same_tensor_returned_twice(self):
823+
"""AOTI gives two outputs one handle when they are the same tensor. With
824+
CUDA graphs on, three calls run eagerly, the fourth captures a graph and
825+
the fifth replays it."""
826+
from executorch.runtime import Runtime
827+
828+
class Twice(torch.nn.Module):
829+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, ...]:
830+
y = x * 2
831+
return y.detach(), x + 1, y.detach()
832+
833+
model = Twice()
834+
edge = self._export_to_cuda_with_lower(model, (torch.randn(4, 8),))
835+
runtime = Runtime.get()
836+
option = "enable_cuda_graph_for_method"
837+
with tempfile.TemporaryDirectory() as directory:
838+
path = os.path.join(directory, "model.pte")
839+
with open(path, "wb") as f:
840+
edge.to_executorch().write_to_file(f)
841+
runtime.backend_registry.set_option("CudaBackend", {option: "forward"})
842+
try:
843+
method = runtime.load_program(path).load_method("forward")
844+
finally:
845+
# The option is process wide.
846+
runtime.backend_registry.set_option("CudaBackend", {option: ""})
847+
for _ in range(5):
848+
x = torch.randn(4, 8)
849+
outputs = method.execute([x])
850+
for output, expected in zip(outputs, model(x), strict=True):
851+
torch.testing.assert_close(output.cpu(), expected)
852+
818853
def test_device_info_propagated_to_cuda_delegate_outputs(self):
819854
"""
820855
Verify that, for a CUDA-delegated graph, every memory-planned tensor's

0 commit comments

Comments
 (0)