Skip to content

Commit 605b561

Browse files
[executorch][cuda] Let the CUDA delegate drive the off-graph KV step (#23294)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #22989 by @Gasoonjia ^ Please use this as the source of truth for the PR details, comments, and reviews ghstack PR base: https://github.com/pytorch/executorch/tree/gh/gasoonjia/191/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/gasoonjia/191/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/gasoonjia/186/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/gasoonjia/191/orig Differential Revision: [D121131873](https://our.internmc.facebook.com/intern/diff/D121131873/) @diff-train-skip-merge --------- Co-authored-by: gasoonjia <gasoonjia@icloud.com> Co-authored-by: Gasoonjia <gasoonjia@meta.com>
1 parent 7cfcde7 commit 605b561

10 files changed

Lines changed: 497 additions & 5 deletions

File tree

‎backends/cuda/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ target_link_libraries(
407407
)
408408
if(EXECUTORCH_BUILD_EXTENSION_LLM)
409409
target_link_libraries(aoti_cuda_backend PUBLIC extension_llm_cache)
410+
target_compile_definitions(
411+
aoti_cuda_backend PRIVATE EXECUTORCH_BUILD_EXTENSION_LLM
412+
)
410413
endif()
411414

412415
if(_cuda_is_msvc_toolchain)

‎backends/cuda/cuda_backend.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
trim_host_memory,
2626
)
2727
from executorch.backends.cuda.passes.lower_offgraph_kv import (
28+
CheckOffGraphKVStepWidthPass,
2829
LowerOffGraphKVPass,
2930
OFFGRAPH_KV_COMPILE_SPEC,
31+
OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC,
3032
parse_offgraph_kv_manifest,
3133
)
3234
from executorch.backends.cuda.passes.move_cond_predicate_to_cpu import (
@@ -782,6 +784,9 @@ def get_custom_passes(cls, compile_specs: List[CompileSpec]) -> List[typing.Any]
782784
)
783785
triton_kernel_mode = mode
784786
passes = [MoveCondPredicateToCpuPass(), ReplaceInt64FloorDivWithFloatPass()]
787+
for spec in compile_specs:
788+
if spec.key == OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC:
789+
passes.append(CheckOffGraphKVStepWidthPass(spec.value))
785790
for spec in compile_specs:
786791
if spec.key == OFFGRAPH_KV_COMPILE_SPEC:
787792
passes.append(

‎backends/cuda/passes/lower_offgraph_kv.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,84 @@ def fn(q, k, v, position, k_storage, v_storage):
219219
return fn
220220

221221

222+
# Ops that pass a tensor's values through unchanged, which the export of a
223+
# position input may place between the method input and the cache op.
224+
_VALUE_PRESERVING_OPS = (
225+
"view",
226+
"reshape",
227+
"unsqueeze",
228+
"squeeze",
229+
"expand",
230+
"clone",
231+
"alias",
232+
"_to_copy",
233+
)
234+
235+
236+
def _source_input(node):
237+
while node.op == "call_function" and any(
238+
name in str(node.target) for name in _VALUE_PRESERVING_OPS
239+
):
240+
node = node.args[0]
241+
return node
242+
243+
244+
class CheckOffGraphKVStepWidthPass:
245+
"""Check the declared step width against the delegated program.
246+
247+
The runtime reads each step's token count from input ``input`` at ``dim``
248+
of the delegate. That index is the delegate's own input order, which
249+
partitioning decides, so it is checked here, after partitioning: every
250+
cache op's position must come from that input, whose ``dim`` must be the
251+
position's length.
252+
"""
253+
254+
requires_exported_program = True
255+
256+
def __init__(self, value: bytes) -> None:
257+
text = value.decode("utf-8")
258+
fields = text.split(":")
259+
if len(fields) != 2 or not all(f.isdigit() for f in fields):
260+
raise ValueError(
261+
f"{OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC} must be "
262+
f'"input_index:dim", got {text!r}'
263+
)
264+
self._input, self._dim = (int(f) for f in fields)
265+
266+
def __call__(self, exported_program: ExportedProgram) -> ExportedProgram:
267+
graph = exported_program.graph_module.graph
268+
user_inputs = set(exported_program.graph_signature.user_inputs)
269+
inputs = [
270+
n for n in graph.nodes if n.op == "placeholder" and n.name in user_inputs
271+
]
272+
target = exir_ops.edge.kvcache.update_and_attend.default
273+
for node in graph.nodes:
274+
if node.op != "call_function" or node.target != target:
275+
continue
276+
if self._input >= len(inputs):
277+
raise ValueError(
278+
f"off-graph KV step width names input {self._input}, but "
279+
f"the delegate takes {len(inputs)}"
280+
)
281+
declared = inputs[self._input]
282+
position = node.args[3]
283+
if _source_input(position) is not declared:
284+
raise ValueError(
285+
f"off-graph KV step width names input {self._input} "
286+
f"({declared.name}), but the cache op's position comes "
287+
f"from {_source_input(position).name}"
288+
)
289+
shape = declared.meta["val"].shape
290+
width = position.meta["val"].shape[0]
291+
if self._dim >= len(shape) or shape[self._dim] != width:
292+
raise ValueError(
293+
f"off-graph KV step width names dim {self._dim} of "
294+
f"{declared.name} {tuple(shape)}, which is not the step's "
295+
f"{width} positions"
296+
)
297+
return exported_program
298+
299+
222300
class LowerOffGraphKVPass:
223301
requires_exported_program = True
224302

‎backends/cuda/runtime/cuda_backend.cpp‎

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@
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

5456
namespace executorch::backends::cuda {
5557

@@ -87,6 +89,17 @@ constexpr int kCudaGraphWarmupSteps = 3;
8789
constexpr 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+
90103
class 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
14651546
namespace executorch::backends {
14661547
namespace {
14671548
auto cls = cuda::CudaBackend();
1468-
executorch::runtime::Backend backend{"CudaBackend", &cls};
1549+
executorch::runtime::Backend backend{cuda::kCudaBackendId, &cls};
14691550
static executorch::runtime::Error success_with_compiler =
14701551
register_backend(backend);
14711552

‎backends/cuda/runtime/cuda_delegate_handle.h‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,26 @@
1818
#include <vector>
1919

2020
namespace executorch {
21+
22+
// Forward declarations for the off-graph KV cache the delegate may be given.
23+
// The concrete types live in cuda_kv_cache.h, which includes this header.
24+
namespace extension::llm::cache {
25+
class Cache;
26+
} // namespace extension::llm::cache
27+
2128
namespace backends {
2229
namespace cuda {
2330

31+
class CudaKVCache;
32+
33+
// Where a method's inputs carry the number of tokens a step writes: an index
34+
// into execute()'s inputs and a dimension of that tensor. Declared by the
35+
// export side as the "offgraph_kv_step_width" compile spec, "input_index:dim".
36+
struct OffGraphKVStepWidth {
37+
int input{0};
38+
int dim{0};
39+
};
40+
2441
using AOTInductorModelContainerGetConstantDtypeFunc =
2542
aoti::AOTIRuntimeError (*)(
2643
aoti::AOTInductorModelContainerHandle container_handle,
@@ -199,6 +216,22 @@ struct CudaDelegateHandle : public aoti::AOTIDelegateHandle {
199216
// SlimTensor handles alive for as long as AOTI may reference their views.
200217
std::vector<std::shared_ptr<CudaWeightStorage>> fqn_weight_storages;
201218
std::vector<std::unique_ptr<aoti::slim::SlimTensor>> fqn_weight_tensors;
219+
220+
// The off-graph KV cache the runner installed for this model, resolved from
221+
// the registry at init. Null for an in-graph model, which is the signal that
222+
// this program owns its KV state as ordinary (mutable) buffers.
223+
//
224+
// The shared_ptr is the handle's own claim on the cache, so the cache
225+
// outlives the delegate even if the runner drops its guard first; the raw
226+
// pointer is the backend face of that same object. Forward-declared rather
227+
// than included: cuda_kv_cache.h includes this header.
228+
std::shared_ptr<::executorch::extension::llm::cache::Cache> kv_cache_shared;
229+
CudaKVCache* kv_cache{nullptr};
230+
231+
// Where this method's inputs carry the step width. The compiled program
232+
// cannot report it, because lowering replaced the cache op with kernels over
233+
// pre-bound memory.
234+
OffGraphKVStepWidth kv_step_width;
202235
};
203236

204237
} // namespace cuda

0 commit comments

Comments
 (0)