1212#include < executorch/backends/aoti/slim/core/slim_tensor.h>
1313#include < executorch/extension/cuda/device_guard.h>
1414#include < executorch/extension/cuda/runtime_api.h>
15+ #include < algorithm>
1516#include < cstdint>
1617#include < cstdlib>
1718#include < memory>
@@ -125,21 +126,94 @@ struct CudaGraphState {
125126 std::vector<size_t > static_input_nbytes;
126127 std::vector<size_t > static_output_nbytes;
127128
129+ // Allocations the graph makes and never frees -- the outputs AOTI allocates
130+ // while being captured. AutoFreeOnLaunch frees one launch's before the next
131+ // launch of the same graph, so the last launch's outlive a graph that is
132+ // dropped unless released with it. Outputs that alias other memory are not
133+ // graph allocations and are not listed.
134+ std::vector<void *> graph_allocations;
135+
128136 CudaGraphState () = default ;
129137
130138 ~CudaGraphState () {
139+ release ();
140+ }
141+
142+ // Records the graph's outstanding allocations: every memory-allocation node
143+ // without a matching free node. Call once the graph is captured.
144+ void note_graph_allocations () {
145+ graph_allocations.clear ();
146+ #if !defined(EXECUTORCH_USE_HIP)
147+ size_t count = 0 ;
148+ if (graph == nullptr ||
149+ cudaGraphGetNodes (graph, nullptr , &count) != cudaSuccess) {
150+ return ;
151+ }
152+ std::vector<cudaGraphNode_t> nodes (count);
153+ if (cudaGraphGetNodes (graph, nodes.data (), &count) != cudaSuccess) {
154+ return ;
155+ }
156+ std::vector<void *> allocated;
157+ std::vector<void *> freed;
158+ for (cudaGraphNode_t node : nodes) {
159+ cudaGraphNodeType type;
160+ if (cudaGraphNodeGetType (node, &type) != cudaSuccess) {
161+ continue ;
162+ }
163+ if (type == cudaGraphNodeTypeMemAlloc) {
164+ cudaMemAllocNodeParams params{};
165+ if (cudaGraphMemAllocNodeGetParams (node, ¶ms) == cudaSuccess) {
166+ allocated.push_back (params.dptr );
167+ }
168+ } else if (type == cudaGraphNodeTypeMemFree) {
169+ void * ptr = nullptr ;
170+ if (cudaGraphMemFreeNodeGetParams (node, &ptr) == cudaSuccess) {
171+ freed.push_back (ptr);
172+ }
173+ }
174+ }
175+ for (void * ptr : allocated) {
176+ if (std::find (freed.begin (), freed.end (), ptr) == freed.end ()) {
177+ graph_allocations.push_back (ptr);
178+ }
179+ }
180+ #endif
181+ }
182+
183+ // Frees the captured graph, the static inputs pinned for it, and the
184+ // allocations its last launch left outstanding (see graph_allocations).
185+ void release () {
131186 if (graph_exec) {
132187 (void )cudaGraphExecDestroy (graph_exec);
188+ graph_exec = nullptr ;
133189 }
134190 if (graph) {
135191 (void )cudaGraphDestroy (graph);
192+ graph = nullptr ;
136193 }
137- // Only free input buffers — output buffers are owned by the AOTI runtime
138- // (allocated during graph capture via the caching allocator).
139194 for (auto * ptr : static_input_ptrs) {
140195 if (ptr)
141196 (void )cudaFree (ptr);
142197 }
198+ // cudaFree synchronises, so the last replay's reads of these are done.
199+ for (auto * ptr : graph_allocations) {
200+ (void )cudaFree (ptr);
201+ }
202+ graph_allocations.clear ();
203+ static_input_ptrs.clear ();
204+ static_output_ptrs.clear ();
205+ static_input_nbytes.clear ();
206+ static_output_nbytes.clear ();
207+ }
208+
209+ // Drops the captured graph so a new one is captured, for when memory the
210+ // graph baked in has moved. One eager step first: rebinding constants resets
211+ // AOTI's constant-fold state, and the fold the next run performs cannot run
212+ // inside a stream capture. The kernels are already loaded, so one is enough.
213+ void recapture () {
214+ release ();
215+ phase = CudaGraphPhase::Warmup;
216+ warmup_remaining = 1 ;
143217 }
144218
145219 // Non-copyable: prevent double-free of CUDA resources
@@ -155,22 +229,15 @@ struct CudaGraphState {
155229 static_input_ptrs(std::move(other.static_input_ptrs)),
156230 static_output_ptrs(std::move(other.static_output_ptrs)),
157231 static_input_nbytes(std::move(other.static_input_nbytes)),
158- static_output_nbytes(std::move(other.static_output_nbytes)) {
232+ static_output_nbytes(std::move(other.static_output_nbytes)),
233+ graph_allocations(std::move(other.graph_allocations)) {
159234 other.graph = nullptr ;
160235 other.graph_exec = nullptr ;
161236 }
162237
163238 CudaGraphState& operator =(CudaGraphState&& other) noexcept {
164239 if (this != &other) {
165- // Clean up existing resources
166- if (graph_exec)
167- (void )cudaGraphExecDestroy (graph_exec);
168- if (graph)
169- (void )cudaGraphDestroy (graph);
170- for (auto * ptr : static_input_ptrs) {
171- if (ptr)
172- (void )cudaFree (ptr);
173- }
240+ release ();
174241
175242 phase = other.phase ;
176243 warmup_remaining = other.warmup_remaining ;
@@ -180,6 +247,7 @@ struct CudaGraphState {
180247 static_output_ptrs = std::move (other.static_output_ptrs );
181248 static_input_nbytes = std::move (other.static_input_nbytes );
182249 static_output_nbytes = std::move (other.static_output_nbytes );
250+ graph_allocations = std::move (other.graph_allocations );
183251
184252 other.graph = nullptr ;
185253 other.graph_exec = nullptr ;
@@ -190,6 +258,14 @@ struct CudaGraphState {
190258
191259// CUDA-specific delegate handle that extends AOTIDelegateHandle.
192260struct CudaDelegateHandle : public aoti ::AOTIDelegateHandle {
261+ // AOTI's run() records a completion event on the stream and queries it at
262+ // the start of the next run. Captured into a CUDA graph, that record belongs
263+ // to the graph, so the first run() after the graph is dropped (to recapture
264+ // against moved KV storage) fails the query. The single-threaded entry point
265+ // skips the event, and a CUDA-graph method runs single-threaded by design.
266+ // Null when the .so does not export it; run() is used then.
267+ aoti::AOTInductorModelContainerRunFunc run_single_threaded{nullptr };
268+
193269 // Extra AOTI metadata used to validate per-FQN weights before binding.
194270 AOTInductorModelContainerGetConstantDtypeFunc get_constant_dtype{nullptr };
195271 // Bytes a constant's compiled shape spans; the off-graph KV cache checks its
0 commit comments