@@ -87,28 +87,6 @@ constexpr char kUseSharedCudaStream[] = "use_shared_cuda_stream";
8787constexpr char kEnableCudaGraphForMethod [] = " enable_cuda_graph_for_method" ;
8888constexpr int kCudaGraphWarmupSteps = 3 ;
8989constexpr 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
11492bool 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