4545#include < executorch/backends/aoti/utils.h>
4646#include < executorch/backends/cuda/runtime/cuda_allocator.h>
4747#include < executorch/backends/cuda/runtime/cuda_delegate_handle.h>
48+ #include < executorch/backends/cuda/runtime/cuda_kv_cache.h>
4849#include < executorch/backends/cuda/runtime/cuda_mutable_state.h>
4950#include < executorch/backends/cuda/runtime/cuda_weight_cache.h>
5051#include < executorch/backends/cuda/runtime/platform/platform.h>
5152#include < executorch/backends/cuda/runtime/shims/memory.h>
5253#include < executorch/backends/cuda/runtime/utils.h>
54+ #include < executorch/extension/llm/cache/cache_registry.h>
5355
5456namespace executorch ::backends::cuda {
5557
@@ -87,6 +89,17 @@ constexpr int kCudaGraphWarmupSteps = 3;
8789constexpr char kWeightSharingAcrossMethods [] = " weight_sharing_across_methods" ;
8890} // anonymous namespace
8991
92+ // Advances the off-graph KV cache past a step's tokens. Every successful exit
93+ // of execute() ends here, and only those: a run that failed never wrote its
94+ // tokens, and committing them would desynchronise the cache's length from the
95+ // history the kernels actually hold.
96+ Error commit_offgraph_kv_step (CudaDelegateHandle* handle, int64_t width) {
97+ if (handle->kv_cache == nullptr ) {
98+ return Error::Ok;
99+ }
100+ return handle->kv_cache ->commit_step (width);
101+ }
102+
90103class ET_EXPERIMENTAL CudaBackend final
91104 : public ::executorch::runtime::BackendInterface {
92105 private:
@@ -306,7 +319,14 @@ class ET_EXPERIMENTAL CudaBackend final
306319 ArrayRef<CompileSpec> compile_specs // This will be my empty list
307320 ) const override {
308321 std::string method_name;
322+ std::optional<OffGraphKVStepWidth> kv_step_width;
309323 for (const CompileSpec& spec : compile_specs) {
324+ if (std::strcmp (spec.key , kOffGraphKVStepWidthSpec ) == 0 ) {
325+ auto parsed = parse_offgraph_kv_step_width (std::string_view (
326+ static_cast <const char *>(spec.value .buffer ), spec.value .nbytes ));
327+ ET_CHECK_OK_OR_RETURN_ERROR (parsed.error ());
328+ kv_step_width = parsed.get ();
329+ }
310330 if (std::strcmp (spec.key , " method_name" ) == 0 ) {
311331 method_name.assign (
312332 static_cast <const char *>(spec.value .buffer ),
@@ -443,6 +463,47 @@ class ET_EXPERIMENTAL CudaBackend final
443463
444464 handle->container_handle = container_handle;
445465
466+ // Runtime-owned off-graph buffers must capture their AOTI names before
467+ // the serialized constants update installs the ordinary weight set.
468+ //
469+ // The cache is found by the key the runner published as a runtime spec.
470+ // No key means an in-graph model, whose KV state is ordinary mutable
471+ // buffers -- that is the whole of the off-graph/in-graph detection, and it
472+ // needs nothing from the user.
473+ //
474+ // The key alone does not decide it, though: a program whose storage was
475+ // lowered off-graph carries no bytes for it, so running one without a cache
476+ // would hand its kernels unbound memory. That requirement is read from the
477+ // program's own constants and checked whether or not a key was passed.
478+ const char * cache_key = nullptr ;
479+ if (auto spec = context.get_runtime_spec <const char *>(
480+ ::executorch::extension::llm::cache::kCacheKeyOption );
481+ spec.ok () && spec.get () != nullptr && *spec.get () != ' \0 ' ) {
482+ cache_key = spec.get ();
483+ }
484+ const auto needs_offgraph_kv = requires_offgraph_kv_storage (*handle);
485+ ET_CHECK_OK_OR_RETURN_ERROR (needs_offgraph_kv.error ());
486+ if (cache_key != nullptr ) {
487+ #if defined(EXECUTORCH_BUILD_EXTENSION_LLM)
488+ ET_CHECK_OK_OR_RETURN_ERROR (
489+ attach_offgraph_kv_cache (*handle, cache_key, kv_step_width));
490+ #else
491+ ET_LOG (
492+ Error,
493+ " init: an off-graph KV cache was supplied, but this CUDA backend was "
494+ " built without EXECUTORCH_BUILD_EXTENSION_LLM" );
495+ return Error::NotSupported;
496+ #endif
497+ }
498+ ET_CHECK_OR_RETURN_ERROR (
499+ !needs_offgraph_kv.get () || handle->kv_cache != nullptr ,
500+ InvalidArgument,
501+ " init: method '%s' keeps its KV cache off-graph but no cache was "
502+ " installed under the '%s' backend option (a CUDA backend built without "
503+ " EXECUTORCH_BUILD_EXTENSION_LLM cannot run it)" ,
504+ method_name.c_str (),
505+ ::executorch::extension::llm::cache::kCacheKeyOption );
506+
446507 // Versioned artifacts load each (device, FQN) through the same process-wide
447508 // cross-method cache model used by the legacy path. The payload only adds
448509 // the tensor metadata needed to reconstruct independently named PTD blobs.
@@ -591,6 +652,23 @@ class ET_EXPERIMENTAL CudaBackend final
591652 }
592653 }
593654
655+ // The cache has to grow, and its storage be rebound, before the compiled
656+ // program reads it -- and nothing inside that program can call back out, so
657+ // the step is driven from here rather than by the runner.
658+ int64_t kv_step_width = 0 ;
659+ if (handle->kv_cache != nullptr ) {
660+ const auto step_width = read_offgraph_kv_step_width (
661+ handle->kv_step_width , Span<EValue*>(args.data (), n_inputs));
662+ ET_CHECK_OK_OR_RETURN_ERROR (step_width.error ());
663+ kv_step_width = step_width.get ();
664+ // Growth is ordered on the stream this step runs on, which was just
665+ // installed as current above.
666+ const Result<cudaStream_t> step_stream = getCurrentCUDAStream (0 );
667+ ET_CHECK_OK_OR_RETURN_ERROR (step_stream.error ());
668+ ET_CHECK_OK_OR_RETURN_ERROR (
669+ handle->kv_cache ->prepare_step (kv_step_width, step_stream.get ()));
670+ ET_CHECK_OK_OR_RETURN_ERROR (handle->kv_cache ->rebind_for_execute (handle));
671+ }
594672 ET_CHECK_OK_OR_RETURN_ERROR (mutable_state_rebind_for_execute (handle));
595673
596674 // ---------------------------------------------------------------
@@ -641,7 +719,7 @@ class ET_EXPERIMENTAL CudaBackend final
641719 }
642720 ET_CUDA_CHECK_OR_RETURN_ERROR (cudaStreamSynchronize (cs));
643721
644- return Error::Ok ;
722+ return commit_offgraph_kv_step (handle, kv_step_width) ;
645723 }
646724
647725 // ---------------------------------------------------------------
@@ -934,7 +1012,7 @@ class ET_EXPERIMENTAL CudaBackend final
9341012 // Last failure point is behind us, so the captured state is now the state
9351013 // the next call should replay from.
9361014 capture_guard.disarm ();
937- return Error::Ok ;
1015+ return commit_offgraph_kv_step (handle, kv_step_width) ;
9381016 }
9391017
9401018 // ----- Normal / WARMUP execution continues here -----
@@ -980,7 +1058,7 @@ class ET_EXPERIMENTAL CudaBackend final
9801058 slim_outputs[i] = nullptr ;
9811059 }
9821060
983- return Error::Ok ;
1061+ return commit_offgraph_kv_step (handle, kv_step_width) ;
9841062 }
9851063
9861064 void destroy (DelegateHandle* handle_) const override {
@@ -990,6 +1068,9 @@ class ET_EXPERIMENTAL CudaBackend final
9901068 cuda::CudaDelegateHandle* handle = (cuda::CudaDelegateHandle*)handle_;
9911069
9921070 mutable_state_forget_handle (handle);
1071+ if (handle->kv_cache != nullptr ) {
1072+ handle->kv_cache ->forget_handle (handle);
1073+ }
9931074
9941075 // NOTE: AOTInductorModelContainerDelete does not work correctly with
9951076 // multiple .so files. Deleting one container frees shared resources,
@@ -1465,7 +1546,7 @@ class ET_EXPERIMENTAL CudaBackend final
14651546namespace executorch ::backends {
14661547namespace {
14671548auto cls = cuda::CudaBackend();
1468- executorch::runtime::Backend backend{" CudaBackend " , &cls};
1549+ executorch::runtime::Backend backend{cuda:: kCudaBackendId , &cls};
14691550static executorch::runtime::Error success_with_compiler =
14701551 register_backend (backend);
14711552
0 commit comments