Skip to content

Commit f79349d

Browse files
[release/1.6] Fix CUDA indexing, graph execution and output buffers (#24488)
Supersedes #24320 with a clean release pick from #24299. Adds CUDA mask-index shims, corrects graph output layout and captured-shape handling, and checks bound output buffers against their upper bounds. Includes the graph retry, host-constant handling, allocation bookkeeping, input-shape checks and wheel shim-check follow-ups already carried by #24320. The Windows import library preserves the existing release dtype-element-size export while adding the three source PR exports. Buffers smaller than the maximum planned output size and replays with changed input shapes are rejected. ## Scope and landing order First in the verified landing order; #24298 is already merged and remains the baseline. This branch starts directly at release/1.6 commit `dfb78b9ecd933a5fee33ebc94f69ed4b71cbb1e7`. Its GitHub diff contains 21 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 dfb78b9 commit f79349d

21 files changed

Lines changed: 1636 additions & 94 deletions

‎.ci/scripts/wheel/test_shared_libraries.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@
104104
"aoti_torch_cuda_randint_low_out",
105105
"aoti_torch_cuda_sort_stable",
106106
"aoti_torch_cuda_guard_set_index",
107+
"aoti_torch_cuda_index_Tensor",
108+
"aoti_torch_index_put_out",
107109
)
108110

109111
_QUANTIZED_KERNEL_SYMBOLS = (

‎backends/aoti/common_shims_slim.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ AOTITorchError aoti_torch_get_sizes(Tensor* tensor, int64_t** ret_sizes) {
3636
return Error::Ok;
3737
}
3838

39+
AOTITorchError
40+
aoti_torch_get_size(Tensor* tensor, int64_t d, int64_t* ret_size) {
41+
if (tensor == nullptr || ret_size == nullptr) {
42+
return Error::InvalidArgument;
43+
}
44+
const auto ndim = static_cast<int64_t>(tensor->dim());
45+
if (d < -ndim || d >= ndim) {
46+
return Error::InvalidArgument;
47+
}
48+
*ret_size = tensor->size(d);
49+
return Error::Ok;
50+
}
51+
3952
AOTITorchError aoti_torch_get_strides(Tensor* tensor, int64_t** ret_strides) {
4053
if (tensor == nullptr || ret_strides == nullptr) {
4154
return Error::InvalidArgument;

‎backends/aoti/common_shims_slim.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ aoti_torch_get_data_ptr(Tensor* tensor, void** ret_data_ptr);
3939
AOTI_SHIM_EXPORT AOTITorchError
4040
aoti_torch_get_sizes(Tensor* tensor, int64_t** ret_sizes);
4141

42+
// Generated code reads the size of a result whose shape depends on the data
43+
// through this, one dim at a time.
44+
AOTI_SHIM_EXPORT AOTITorchError
45+
aoti_torch_get_size(Tensor* tensor, int64_t d, int64_t* ret_size);
46+
4247
AOTI_SHIM_EXPORT AOTITorchError
4348
aoti_torch_get_strides(Tensor* tensor, int64_t** ret_strides);
4449

‎backends/cuda/CMakeLists.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ set(_aoti_cuda_shim_sources runtime/shims/memory.cpp
255255

256256
# Only build CUDA-only operator shims when the CUDA toolchain is available.
257257
if(NOT EXECUTORCH_BUILD_ROCM AND CMAKE_CUDA_COMPILER)
258-
list(APPEND _aoti_cuda_shim_sources runtime/shims/int4mm.cu
259-
runtime/shims/sort.cu runtime/shims/rand.cu
258+
list(APPEND _aoti_cuda_shim_sources runtime/shims/index.cu
259+
runtime/shims/int4mm.cu runtime/shims/sort.cu runtime/shims/rand.cu
260260
)
261261
endif()
262262

‎backends/cuda/cuda_backend.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,8 @@ def get_supported_fallback_kernels(cls) -> Dict[str, Any]:
839839
"at::_ops::sort_stable::call": None,
840840
"aoti_torch_cuda_sort_stable": None,
841841
"aoti_torch_cuda_randint_low_out": None,
842+
"aoti_torch_cuda_index_Tensor": None,
843+
"aoti_torch_index_put_out": None,
842844
}
843845

844846
@classmethod

‎backends/cuda/passes/move_cond_predicate_to_cpu.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ def forward(self, hidden_states, encoder_hidden_states):
5555
In this example if we keep `self.initialized` on GPU, we will need to copy it to CPU for every forward pass.
5656
We move the predicate to CPU to avoid device to host copies.
5757
This pass is only applicable to models that use torch.cond and its predicate is a constant buffer.
58+
59+
A method this pass changes keeps a constant in host memory, so the CUDA backend runs it
60+
without a CUDA graph even when one is requested: a graph would keep the branch the capture
61+
call took.
5862
"""
5963

6064
requires_exported_program = True
2.39 KB
Binary file not shown.

‎backends/cuda/runtime/cuda_backend.cpp‎

Lines changed: 135 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -87,28 +87,6 @@ constexpr char kUseSharedCudaStream[] = "use_shared_cuda_stream";
8787
constexpr char kEnableCudaGraphForMethod[] = "enable_cuda_graph_for_method";
8888
constexpr int kCudaGraphWarmupSteps = 3;
8989
constexpr char kWeightSharingAcrossMethods[] = "weight_sharing_across_methods";
90-
91-
// A CUDA graph replay copies an output's raw bytes into its ETensor, which is
92-
// right only when every element sits at the same offset in both. Size-1 dims
93-
// never move an element, so their strides may differ.
94-
bool output_is_raw_copyable(const SlimTensor* output, const Tensor* et_output) {
95-
if (!output->is_contiguous() ||
96-
output->dim() != static_cast<size_t>(et_output->dim())) {
97-
return false;
98-
}
99-
// The ETensor still has its previous shape here, so its strides can be stale
100-
// for dynamic shapes; the default dim order is enough on its own.
101-
if (executorch::runtime::is_contiguous_dim_order(
102-
et_output->dim_order().data(), et_output->dim())) {
103-
return true;
104-
}
105-
for (size_t d = 0; d < output->dim(); d++) {
106-
if (output->size(d) != 1 && output->stride(d) != et_output->strides()[d]) {
107-
return false;
108-
}
109-
}
110-
return true;
111-
}
11290
} // anonymous namespace
11391

11492
bool CudaGraphState::start_warmup() {
@@ -636,7 +614,18 @@ class ET_EXPERIMENTAL CudaBackend final
636614
// Initialize CUDA graph state if enabled for this method. Not on a device
637615
// without memory pools, where the method runs without a graph.
638616
if (should_use_cuda_graph_for_method(method_name)) {
639-
if (handle->cuda_graph_state.start_warmup()) {
617+
const std::string host_constants = describe_host_constants(handle);
618+
if (!host_constants.empty()) {
619+
// A graph records only the device work of the capture call. Host code,
620+
// such as the choice of a torch.cond branch on a flag in host memory,
621+
// would be fixed at its value from that call.
622+
ET_LOG(
623+
Info,
624+
"CUDA graph requested for method '%s', but %s; running without a "
625+
"CUDA graph",
626+
method_name.c_str(),
627+
host_constants.c_str());
628+
} else if (handle->cuda_graph_state.start_warmup()) {
640629
ET_LOG(
641630
Info,
642631
"CUDA graph enabled for method '%s' (warmup=%d)",
@@ -790,14 +779,26 @@ class ET_EXPERIMENTAL CudaBackend final
790779
// Copy new input data (GPU-resident) into static input buffers (D2D)
791780
for (size_t i = 0; i < n_inputs; i++) {
792781
auto* et_input = &(args[i]->toTensor());
782+
const auto sizes = et_input->sizes();
783+
const auto& captured = handle->cuda_graph_state.static_input_sizes[i];
793784
ET_CHECK_OR_RETURN_ERROR(
794-
et_input->nbytes() ==
795-
handle->cuda_graph_state.static_input_nbytes[i],
785+
sizes.size() == captured.size(),
796786
InvalidArgument,
797-
"CUDA graph replay: input %zu size mismatch (expected %zu, got %zu)",
787+
"CUDA graph replay: input %zu has %zu dims, captured with %zu",
798788
i,
799-
handle->cuda_graph_state.static_input_nbytes[i],
800-
et_input->nbytes());
789+
sizes.size(),
790+
captured.size());
791+
for (size_t d = 0; d < sizes.size(); d++) {
792+
ET_CHECK_OR_RETURN_ERROR(
793+
sizes[d] == captured[d],
794+
InvalidArgument,
795+
"CUDA graph replay: input %zu has size %lld in dim %zu, "
796+
"captured with %lld",
797+
i,
798+
static_cast<long long>(sizes[d]),
799+
d,
800+
static_cast<long long>(captured[d]));
801+
}
801802
ET_CUDA_CHECK_OR_RETURN_ERROR(cudaMemcpyAsync(
802803
handle->cuda_graph_state.static_input_ptrs[i],
803804
et_input->const_data_ptr(),
@@ -830,13 +831,70 @@ class ET_EXPERIMENTAL CudaBackend final
830831
return commit_offgraph_kv_step(handle, kv_step_width);
831832
}
832833

833-
// ---------------------------------------------------------------
834-
// Normal path (also used for WARMUP and CAPTURE phases)
835-
// ---------------------------------------------------------------
836-
bool is_capture_step =
834+
const bool is_capture_step =
837835
(handle->cuda_graph_state.phase == CudaGraphPhase::Warmup &&
838836
handle->cuda_graph_state.warmup_remaining == 0);
837+
bool graph_ran = false;
838+
Error error =
839+
run_once(handle, args, n_inputs, n_outputs, is_capture_step, graph_ran);
840+
if (error != Error::Ok && is_capture_step && !graph_ran) {
841+
// A capture records kernels without running them, so until the new graph
842+
// runs, the inputs and any state the program updates are untouched. A
843+
// program that keeps state in host memory, where host code does run
844+
// during a capture, never gets here: init leaves its graph off.
845+
ET_LOG(
846+
Info,
847+
"CUDA graph: capture failed for '%s'; running this call and later "
848+
"ones without a graph",
849+
handle->method_name.c_str());
850+
// The capture guard turned graphs off, so later calls run eagerly too.
851+
ET_DCHECK(handle->cuda_graph_state.phase == CudaGraphPhase::Disabled);
852+
error = run_once(
853+
handle,
854+
args,
855+
n_inputs,
856+
n_outputs,
857+
/*is_capture_step=*/false,
858+
graph_ran);
859+
}
860+
ET_CHECK_OK_OR_RETURN_ERROR(error);
861+
return commit_offgraph_kv_step(handle, kv_step_width);
862+
}
863+
864+
private:
865+
// Names a constant of the program in host memory, or says that the
866+
// constants could not be listed, since unknown counts as host memory.
867+
// Empty when every constant is on the device.
868+
static std::string describe_host_constants(cuda::CudaDelegateHandle* handle) {
869+
const char* unlisted = "its constants could not be listed";
870+
if (handle->extract_constants_map == nullptr) {
871+
return unlisted;
872+
}
873+
std::unordered_map<std::string, AtenTensorHandle> constants;
874+
if (handle->extract_constants_map(
875+
handle->container_handle,
876+
reinterpret_cast<AOTInductorConstantMapHandle>(&constants),
877+
/*use_inactive=*/false) != Error::Ok) {
878+
return unlisted;
879+
}
880+
for (const auto& [fqn, tensor] : constants) {
881+
if (reinterpret_cast<SlimTensor*>(tensor)->is_cpu()) {
882+
return "constant '" + fqn + "' is in host memory";
883+
}
884+
}
885+
return "";
886+
}
839887

888+
// Runs the program once. On the capture step it captures the CUDA graph
889+
// instead, then launches it once, and sets graph_ran once that launch is
890+
// queued.
891+
Error run_once(
892+
cuda::CudaDelegateHandle* handle,
893+
Span<EValue*> args,
894+
size_t n_inputs,
895+
size_t n_outputs,
896+
bool is_capture_step,
897+
bool& graph_ran) const {
840898
// Wrrap them as SlimTensors for AOTI execution without any host<->device
841899
// copies.
842900
std::vector<SlimTensor*> slim_inputs(n_inputs);
@@ -863,25 +921,10 @@ class ET_EXPERIMENTAL CudaBackend final
863921
return;
864922
}
865923
// Free what this attempt pinned and disable graphs for this method, so
866-
// a capture that cannot succeed costs one error rather than one on
867-
// every fourth call for the life of the process. Eager execution is
868-
// correct, just slower.
869-
for (void* ptr : state_->static_input_ptrs) {
870-
(void)cudaFree(ptr);
871-
}
872-
state_->static_input_ptrs.clear();
873-
state_->static_output_ptrs.clear();
874-
state_->static_input_nbytes.clear();
875-
state_->static_output_nbytes.clear();
876-
// Same order as ~CudaGraphState: the exec depends on the graph.
877-
if (state_->graph_exec != nullptr) {
878-
(void)cudaGraphExecDestroy(state_->graph_exec);
879-
state_->graph_exec = nullptr;
880-
}
881-
if (state_->graph != nullptr) {
882-
(void)cudaGraphDestroy(state_->graph);
883-
state_->graph = nullptr;
884-
}
924+
// a capture that cannot succeed is tried once rather than on every
925+
// later call for the life of the process. Eager execution is correct,
926+
// just slower.
927+
state_->release();
885928
state_->phase = CudaGraphPhase::Disabled;
886929
state_->warmup_remaining = 0;
887930
(void)cudaGetLastError();
@@ -925,6 +968,8 @@ class ET_EXPERIMENTAL CudaBackend final
925968
// through the guard instead of leaking this allocation.
926969
handle->cuda_graph_state.static_input_ptrs.push_back(static_ptr);
927970
handle->cuda_graph_state.static_input_nbytes.push_back(nbytes);
971+
handle->cuda_graph_state.static_input_sizes.emplace_back(
972+
et_input->sizes().begin(), et_input->sizes().end());
928973

929974
ET_CUDA_CHECK_OR_RETURN_ERROR(cudaMemcpy(
930975
static_ptr,
@@ -1056,11 +1101,23 @@ class ET_EXPERIMENTAL CudaBackend final
10561101
}
10571102
}
10581103

1059-
ET_CHECK_OR_RETURN_ERROR(
1060-
error == Error::Ok,
1061-
Internal,
1062-
"AOTInductorModelContainerRun failed with error code %d",
1063-
error);
1104+
if (error != Error::Ok) {
1105+
// A failed capture is retried without a graph, which reports a real
1106+
// failure at Error.
1107+
if (is_capture_step) {
1108+
ET_LOG(
1109+
Info,
1110+
"AOTInductorModelContainerRun failed during CUDA graph capture "
1111+
"with error code %d",
1112+
error);
1113+
} else {
1114+
ET_LOG(
1115+
Error,
1116+
"AOTInductorModelContainerRun failed with error code %d",
1117+
error);
1118+
}
1119+
return Error::Internal;
1120+
}
10641121

10651122
if (is_capture_step) {
10661123
// End capture → instantiate graph
@@ -1083,13 +1140,28 @@ class ET_EXPERIMENTAL CudaBackend final
10831140
slim_outputs[i], &(args[i + n_inputs]->toTensor()))) {
10841141
std::fill(slim_outputs.begin(), slim_outputs.end(), nullptr);
10851142
ET_LOG(
1086-
Error,
1143+
Info,
10871144
"CUDA graph: output %zu of '%s' is not laid out like its "
1088-
"ETensor; this call fails and later calls run without a graph",
1145+
"ETensor",
10891146
i,
10901147
handle->method_name.c_str());
10911148
return Error::NotSupported;
10921149
}
1150+
// Replay copies raw bytes and never resizes, so the ETensor takes the
1151+
// captured shape here and keeps it for every replay.
1152+
const Error resize_err =
1153+
executorch::ET_RUNTIME_NAMESPACE::resize_tensor(
1154+
args[i + n_inputs]->toTensor(), slim_outputs[i]->sizes());
1155+
if (resize_err != Error::Ok) {
1156+
std::fill(slim_outputs.begin(), slim_outputs.end(), nullptr);
1157+
ET_LOG(
1158+
Info,
1159+
"CUDA graph: cannot resize output %zu of '%s' to its captured "
1160+
"shape",
1161+
i,
1162+
handle->method_name.c_str());
1163+
return resize_err;
1164+
}
10931165
}
10941166

10951167
gerr = cudaGraphInstantiate(
@@ -1101,7 +1173,6 @@ class ET_EXPERIMENTAL CudaBackend final
11011173
Internal,
11021174
"cudaGraphInstantiate failed: %s",
11031175
cudaGetErrorString(gerr));
1104-
handle->cuda_graph_state.note_graph_allocations();
11051176

11061177
// Record static output pointers (stable under graph replay). Releasing
11071178
// them from slim_outputs here, before the copies below, keeps the cleanup
@@ -1127,6 +1198,8 @@ class ET_EXPERIMENTAL CudaBackend final
11271198
Internal,
11281199
"cudaGraphLaunch (first replay) failed: %s",
11291200
cudaGetErrorString(gerr));
1201+
graph_ran = true;
1202+
handle->cuda_graph_state.note_graph_allocations();
11301203

11311204
// Copy capture-step outputs into the planned GPU ET buffers (D2D).
11321205
for (size_t i = 0; i < n_outputs; i++) {
@@ -1143,7 +1216,7 @@ class ET_EXPERIMENTAL CudaBackend final
11431216
// Last failure point is behind us, so the captured state is now the state
11441217
// the next call should replay from.
11451218
capture_guard.disarm();
1146-
return commit_offgraph_kv_step(handle, kv_step_width);
1219+
return Error::Ok;
11471220
}
11481221

11491222
// ----- Normal / WARMUP execution continues here -----
@@ -1200,9 +1273,10 @@ class ET_EXPERIMENTAL CudaBackend final
12001273
slim_outputs[i] = nullptr;
12011274
}
12021275

1203-
return commit_offgraph_kv_step(handle, kv_step_width);
1276+
return Error::Ok;
12041277
}
12051278

1279+
public:
12061280
void destroy(DelegateHandle* handle_) const override {
12071281
if (handle_ == nullptr) {
12081282
return;

0 commit comments

Comments
 (0)