Skip to content

Commit 0bf741c

Browse files
[executorch][muse] Wire dynamic KV cache into the runtime (pytorch#23296)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: pytorch#22818 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/188/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/gasoonjia/188/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/gasoonjia/187/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/gasoonjia/188/orig Differential Revision: [D119571991](https://our.internmc.facebook.com/intern/diff/D119571991/) @diff-train-skip-merge --------- Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent 3850a74 commit 0bf741c

7 files changed

Lines changed: 213 additions & 19 deletions

File tree

‎.ci/scripts/export_model_artifact.sh‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Arguments:
4444
- vr-streaming: Voxtral Realtime streaming mode
4545
- vr-offline: Voxtral Realtime offline mode
4646
- solo-text: Muse Glimmer solo text mode
47+
- solo-text-offgraph: Muse Glimmer solo text, runtime-owned KV cache
4748
- dflash-image: Muse Glimmer DFlash vision mode
4849
4950
Examples:
@@ -95,7 +96,7 @@ if [ -n "$MODE" ]; then
9596
exit 1
9697
fi
9798
;;
98-
solo-text|dflash-image)
99+
solo-text|solo-text-offgraph|dflash-image)
99100
if [ "$HF_MODEL" != "meta-models/Muse-Glimmer-30B-GGUF" ]; then
100101
echo "Error: Mode '$MODE' can only be used with Muse Glimmer model"
101102
echo "Provided model: $HF_MODEL"
@@ -104,7 +105,7 @@ if [ -n "$MODE" ]; then
104105
;;
105106
*)
106107
echo "Error: Unsupported mode '$MODE'"
107-
echo "Supported modes: vr-streaming, vr-offline, solo-text, dflash-image"
108+
echo "Supported modes: vr-streaming, vr-offline, solo-text, solo-text-offgraph, dflash-image"
108109
exit 1
109110
;;
110111
esac
@@ -565,6 +566,16 @@ if [ "$MODEL_NAME" = "muse_glimmer" ]; then
565566
--backend cuda \
566567
--output-dir "${OUTPUT_DIR}"
567568
;;
569+
solo-text-offgraph)
570+
EXPORT_START_SECONDS=$SECONDS
571+
TMPDIR="$INDUCTOR_TMPDIR" \
572+
TORCHINDUCTOR_CACHE_DIR="$INDUCTOR_CACHE" \
573+
python -m executorch.examples.models.muse_glimmer.export.export_solo \
574+
--gguf "$TARGET_GGUF_PATH" \
575+
--backend cuda \
576+
--use-offgraph-kv-cache \
577+
--output-dir "${OUTPUT_DIR}"
578+
;;
568579
dflash-image)
569580
DRAFT_GGUF_FILE="dflash-Muse-Glimmer-30B-Q4_K_M.gguf"
570581
MMPROJ_GGUF_FILE="mmproj-Muse-Glimmer-30B-Q4_K_M.gguf"
@@ -581,7 +592,7 @@ if [ "$MODEL_NAME" = "muse_glimmer" ]; then
581592
--output-dir "${OUTPUT_DIR}"
582593
;;
583594
*)
584-
echo "Error: Muse Glimmer requires mode 'solo-text' or 'dflash-image'"
595+
echo "Error: Muse Glimmer requires mode 'solo-text', 'solo-text-offgraph' or 'dflash-image'"
585596
exit 1
586597
;;
587598
esac

‎.ci/scripts/test_model_e2e.sh‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Arguments:
4646
- vr-streaming: Voxtral Realtime streaming mode
4747
- vr-offline: Voxtral Realtime offline mode
4848
- solo-text: Muse Glimmer solo text mode
49+
- solo-text-offgraph: Muse Glimmer solo text, runtime-owned KV cache
4950
- dflash-image: Muse Glimmer DFlash vision mode
5051
5152
Environment:
@@ -111,7 +112,7 @@ if [ -n "$MODE" ]; then
111112
exit 1
112113
fi
113114
;;
114-
solo-text|dflash-image)
115+
solo-text|solo-text-offgraph|dflash-image)
115116
if [ "$HF_MODEL" != "meta-models/Muse-Glimmer-30B-GGUF" ]; then
116117
echo "Error: Mode '$MODE' can only be used with Muse Glimmer model"
117118
echo "Provided model: $HF_MODEL"
@@ -120,7 +121,7 @@ if [ -n "$MODE" ]; then
120121
;;
121122
*)
122123
echo "Error: Unsupported mode '$MODE'"
123-
echo "Supported modes: vr-streaming, vr-offline, solo-text, dflash-image"
124+
echo "Supported modes: vr-streaming, vr-offline, solo-text, solo-text-offgraph, dflash-image"
124125
exit 1
125126
;;
126127
esac
@@ -285,7 +286,7 @@ case "$HF_MODEL" in
285286
AUDIO_FILE=""
286287
IMAGE_PATH=""
287288
case "$MODE" in
288-
solo-text)
289+
solo-text|solo-text-offgraph)
289290
RUNNER_TARGET="solo_runner"
290291
EXPECTED_OUTPUT="Paris"
291292
IMAGE_URL=""
@@ -296,7 +297,7 @@ case "$HF_MODEL" in
296297
IMAGE_URL="https://github.com/pytorch/hub/raw/master/images/dog.jpg"
297298
;;
298299
*)
299-
echo "Error: Muse Glimmer requires mode 'solo-text' or 'dflash-image'"
300+
echo "Error: Muse Glimmer requires mode 'solo-text', 'solo-text-offgraph' or 'dflash-image'"
300301
exit 1
301302
;;
302303
esac
@@ -446,12 +447,18 @@ EOF
446447
;;
447448
muse_glimmer)
448449
PROMPT_FILE="${MODEL_DIR}/muse_glimmer_prompt.txt"
449-
if [ "$MODE" = "solo-text" ]; then
450+
if [ "$MODE" = "solo-text" ] || [ "$MODE" = "solo-text-offgraph" ]; then
450451
printf '%s' '<|start|>user<|message|>What is the capital of France?<|eot|><|start|>assistant' > "$PROMPT_FILE"
451452
else
452453
printf '%s' '<|start|>user<|message|>What animal is in this image? <img><|eot|><|start|>assistant' > "$PROMPT_FILE"
453454
fi
454-
RUNNER_ARGS="$RUNNER_ARGS --tokenizer_path ${MODEL_DIR}/$TOKENIZER_FILE --prompt_file \"$PROMPT_FILE\" --max_new_tokens 512 --cuda_graph"
455+
RUNNER_ARGS="$RUNNER_ARGS --tokenizer_path ${MODEL_DIR}/$TOKENIZER_FILE --prompt_file \"$PROMPT_FILE\" --max_new_tokens 512"
456+
if [ "$MODE" = "solo-text-offgraph" ]; then
457+
# The off-graph KV cache does not support CUDA graph yet.
458+
RUNNER_ARGS="$RUNNER_ARGS --cuda_graph=false"
459+
else
460+
RUNNER_ARGS="$RUNNER_ARGS --cuda_graph"
461+
fi
455462
if [ "$MODE" = "dflash-image" ]; then
456463
RUNNER_ARGS="$RUNNER_ARGS --image_path ${MODEL_DIR}/test_image.jpg"
457464
fi

‎.github/workflows/cuda.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,8 @@ jobs:
759759
include:
760760
- variant: kquant-17gb
761761
mode: solo-text
762+
- variant: kquant-17gb
763+
mode: solo-text-offgraph
762764
- variant: kquant-17gb
763765
mode: dflash-image
764766
- variant: kquant-dynamic

‎examples/models/muse-glimmer/runtime/engine/muse_glimmer_engine.cpp‎

Lines changed: 161 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
#ifdef EXECUTORCH_BUILD_CUDA
3030
#include <cuda_runtime.h>
31+
#include <executorch/backends/cuda/runtime/backend_options.h>
32+
#include <executorch/extension/llm/runner/constants.h>
33+
#include <executorch/extension/llm/runner/model_metadata.h>
3134
#include <nlohmann/json.hpp>
3235
#else
3336
#include <executorch/extension/llm/sampler/util.h>
@@ -276,11 +279,23 @@ Result<uint64_t> read_sampled_token(
276279
#endif
277280
}
278281

282+
// The off-graph KV cache is CUDA-only, and so are its headers; elsewhere the
283+
// module is built without one and the guard is always null.
284+
#ifdef EXECUTORCH_BUILD_CUDA
285+
using OffGraphInstallGuard = ::executorch::extension::llm::cache::InstallGuard;
286+
#else
287+
struct OffGraphInstallGuard;
288+
#endif
289+
279290
Result<std::unique_ptr<Module>> build_muse_glimmer_module(
280291
const MuseGlimmerConfig& config,
281292
MuseGlimmerArtifactMode artifact_mode,
282293
bool has_vision,
283-
bool multi_session) {
294+
bool multi_session,
295+
const OffGraphInstallGuard* offgraph_guard) {
296+
#ifndef EXECUTORCH_BUILD_CUDA
297+
(void)offgraph_guard;
298+
#endif
284299
std::vector<std::string> data_files;
285300
if (!config.data_path.empty()) {
286301
data_files.push_back(config.data_path);
@@ -315,6 +330,20 @@ Result<std::unique_ptr<Module>> build_muse_glimmer_module(
315330
#endif
316331

317332
const executorch::runtime::LoadBackendOptionsMap* load_options = nullptr;
333+
#ifdef EXECUTORCH_BUILD_CUDA
334+
// Name the installed off-graph cache so CudaBackend::init() can resolve it
335+
// from the registry. Must outlive the load_method calls below, which read it
336+
// during init. Absent for an in-graph model, and its absence is exactly how
337+
// the delegate tells the two apart.
338+
executorch::runtime::BackendOptions<1> cuda_cache_opts;
339+
executorch::runtime::LoadBackendOptionsMap cuda_options_map;
340+
if (offgraph_guard != nullptr) {
341+
ET_CHECK_OK_OR_RETURN_ERROR(offgraph_guard->set_option(cuda_cache_opts));
342+
ET_CHECK_OK_OR_RETURN_ERROR(cuda_options_map.set_options(
343+
::executorch::backends::cuda::kCudaBackendId, cuda_cache_opts.view()));
344+
load_options = &cuda_options_map;
345+
}
346+
#endif
318347
#ifdef EXECUTORCH_BUILD_MLX
319348
// Per-model MLX runtime specs, delivered to MLXBackend::init(). Must outlive
320349
// the load_method calls below (they read it during init).
@@ -456,6 +485,65 @@ Error register_mutable_fqns(
456485
return Error::Ok;
457486
}
458487

488+
struct OffGraphKVPlan {
489+
cache::CacheGeometry geometry;
490+
cache::CacheConfig config;
491+
};
492+
493+
Result<OffGraphKVPlan> read_offgraph_kv_plan(
494+
Module& module,
495+
const std::unordered_map<std::string, int64_t>& metadata,
496+
int64_t initial_capacity) {
497+
// Geometry comes from the neutral constant methods the MLX off-graph path
498+
// also reads. The two sizes are already in the engine's metadata:
499+
// kMaxContextLen is the cache ceiling (get_llm_metadata derives it from
500+
// get_max_seq_len when the model does not publish it), and
501+
// kMaxPrefillChunk is the largest single step, which is what a ring layer
502+
// sizes its slots from.
503+
ET_ASSIGN_OR_RETURN(
504+
geometry, ::executorch::extension::llm::read_cache_geometry(module));
505+
506+
const auto capacity_it = metadata.find(kMaxContextLen);
507+
const auto chunk_it = metadata.find(kMaxPrefillChunk);
508+
ET_CHECK_OR_RETURN_ERROR(
509+
capacity_it != metadata.end() && chunk_it != metadata.end(),
510+
InvalidProgram,
511+
"off-graph KV cache needs %s and %s metadata",
512+
kMaxContextLen,
513+
kMaxPrefillChunk);
514+
515+
// CacheConfig counts in int; a value that does not survive the narrowing
516+
// would otherwise be validated after truncation.
517+
constexpr int64_t kMaxCacheInt = std::numeric_limits<int>::max();
518+
for (const auto& [name, value] :
519+
{std::pair{kMaxContextLen, capacity_it->second},
520+
std::pair{kMaxPrefillChunk, chunk_it->second},
521+
std::pair{"offgraph_initial_capacity", initial_capacity}}) {
522+
ET_CHECK_OR_RETURN_ERROR(
523+
value > 0 && value <= kMaxCacheInt,
524+
InvalidProgram,
525+
"off-graph KV cache: %s is %" PRId64 ", outside [1, %" PRId64 "]",
526+
name,
527+
value,
528+
kMaxCacheInt);
529+
}
530+
531+
OffGraphKVPlan plan;
532+
plan.geometry = std::move(geometry);
533+
plan.config.capacity = static_cast<int>(capacity_it->second);
534+
plan.config.kv_dtype =
535+
static_cast<int>(executorch::aten::ScalarType::BFloat16);
536+
plan.config.initial_capacity = static_cast<int>(initial_capacity);
537+
plan.config.max_write = static_cast<int>(chunk_it->second);
538+
ET_CHECK_OR_RETURN_ERROR(
539+
cache::valid(plan.geometry, plan.config) &&
540+
plan.config.initial_capacity > 0 &&
541+
plan.config.initial_capacity <= plan.config.capacity,
542+
InvalidProgram,
543+
"invalid off-graph KV cache configuration");
544+
return plan;
545+
}
546+
459547
TensorPtr build_decode_pos_table(
460548
const std::unordered_map<std::string, int64_t>& metadata) {
461549
auto ctx_it = metadata.find(kMaxContextLen);
@@ -489,6 +577,9 @@ class MuseGlimmerSession : public LLMSession,
489577
int64_t min_prefill_chunk,
490578
TensorPtr decode_pos_table_dev,
491579
MuseGlimmerMutableStateContextOwner* mutable_state,
580+
#ifdef EXECUTORCH_BUILD_CUDA
581+
::executorch::extension::llm::cache::SequenceControl* offgraph_control,
582+
#endif
492583
int session_token)
493584
: module_(module),
494585
exec_mutex_(exec_mutex),
@@ -502,6 +593,9 @@ class MuseGlimmerSession : public LLMSession,
502593
decode_pos_table_dev_(std::move(decode_pos_table_dev)),
503594
#endif
504595
mutable_state_(mutable_state),
596+
#ifdef EXECUTORCH_BUILD_CUDA
597+
offgraph_control_(offgraph_control),
598+
#endif
505599
session_token_(session_token) {
506600
if (auto it = metadata_.find(kUseSampling); it != metadata_.end()) {
507601
use_sampling_ = it->second != 0;
@@ -752,6 +846,11 @@ class MuseGlimmerSession : public LLMSession,
752846

753847
Error reset() override {
754848
pos_ = 0;
849+
#ifdef EXECUTORCH_BUILD_CUDA
850+
if (offgraph_control_ != nullptr) {
851+
offgraph_control_->clear();
852+
}
853+
#endif
755854
pending_.reset();
756855
prev_decode_token_.reset();
757856
if (preserve_staged_image_on_next_reset_) {
@@ -1114,6 +1213,10 @@ class MuseGlimmerSession : public LLMSession,
11141213
TensorPtr decode_pos_table_dev_;
11151214
#endif
11161215
MuseGlimmerMutableStateContextOwner* mutable_state_ = nullptr;
1216+
#ifdef EXECUTORCH_BUILD_CUDA
1217+
::executorch::extension::llm::cache::SequenceControl* offgraph_control_ =
1218+
nullptr;
1219+
#endif
11171220
int session_token_ = kMuseGlimmerNoMutableSession;
11181221
#ifdef EXECUTORCH_BUILD_CUDA
11191222
float temp_val_ = 1e-6f;
@@ -1399,8 +1502,47 @@ Result<std::unique_ptr<MuseGlimmerEngine>> MuseGlimmerEngine::create(
13991502
}
14001503

14011504
std::unique_ptr<MuseGlimmerMutableStateContextOwner> mutable_state;
1505+
const OffGraphInstallGuard* offgraph_install = nullptr;
14021506
#ifdef EXECUTORCH_BUILD_CUDA
1403-
if (config.enable_cuda_graph) {
1507+
std::shared_ptr<::executorch::extension::llm::cache::Cache> offgraph_cache;
1508+
std::unique_ptr<::executorch::extension::llm::cache::InstallGuard>
1509+
offgraph_guard;
1510+
::executorch::extension::llm::cache::SequenceControl* offgraph_control =
1511+
nullptr;
1512+
if (method_names.count(kNumCaches) != 0) {
1513+
ET_CHECK_OR_RETURN_ERROR(
1514+
!config.enable_cuda_graph,
1515+
NotSupported,
1516+
"off-graph KV cache does not support CUDA graph");
1517+
ET_CHECK_OR_RETURN_ERROR(
1518+
artifact_mode == MuseGlimmerArtifactMode::Autoregressive,
1519+
NotSupported,
1520+
"off-graph KV cache currently supports autoregressive artifacts only");
1521+
ET_CHECK_OR_RETURN_ERROR(
1522+
config.max_sessions == 1,
1523+
NotSupported,
1524+
"off-graph KV cache currently supports one session");
1525+
auto plan = read_offgraph_kv_plan(
1526+
*meta_module, metadata, config.offgraph_initial_capacity);
1527+
ET_CHECK_OK_OR_RETURN_ERROR(plan.error());
1528+
// The factory hands back a neutral cache; the guard publishes it under a
1529+
// registry key that build_muse_glimmer_module passes to the delegate.
1530+
auto built = cache::CacheFactory::global().build(
1531+
::executorch::backends::cuda::kCudaBackendId,
1532+
cache::kind::kSingle,
1533+
plan.get().geometry,
1534+
plan.get().config);
1535+
ET_CHECK_OK_OR_RETURN_ERROR(built.error());
1536+
offgraph_cache = built.get();
1537+
offgraph_guard = std::make_unique<cache::InstallGuard>(offgraph_cache);
1538+
offgraph_install = offgraph_guard.get();
1539+
offgraph_control = offgraph_cache->as<cache::SequenceControl>();
1540+
ET_CHECK_OR_RETURN_ERROR(
1541+
offgraph_control != nullptr,
1542+
Internal,
1543+
"off-graph KV cache is missing a face the engine needs");
1544+
ET_LOG(Info, "MuseGlimmerEngine: dynamic off-graph KV cache enabled");
1545+
} else if (config.enable_cuda_graph) {
14041546
ET_LOG(
14051547
Info,
14061548
"MuseGlimmerEngine: CUDA graph requested; per-session rebinding "
@@ -1433,18 +1575,18 @@ Result<std::unique_ptr<MuseGlimmerEngine>> MuseGlimmerEngine::create(
14331575
// skip_mutable_buffer_init, so the skip flag can never diverge from the
14341576
// owner.
14351577
const bool multi_session = mutable_state != nullptr;
1436-
auto module_res = multi_session
1437-
? mutable_state->with_load_scope([&]() {
1438-
return build_muse_glimmer_module(
1439-
config, artifact_mode, has_vision, multi_session);
1440-
})
1441-
: build_muse_glimmer_module(
1442-
config, artifact_mode, has_vision, multi_session);
1578+
auto build_module = [&]() {
1579+
return build_muse_glimmer_module(
1580+
config, artifact_mode, has_vision, multi_session, offgraph_install);
1581+
};
1582+
// The in-graph path still scopes its load, because mutable-buffer rebinding
1583+
// has no registry key to travel on.
1584+
auto module_res = multi_session ? mutable_state->with_load_scope(build_module)
1585+
: build_module();
14431586
if (module_res.error() != Error::Ok) {
14441587
return module_res.error();
14451588
}
14461589
std::unique_ptr<Module> shared_module = std::move(module_res.get());
1447-
14481590
bool rebind_available = false;
14491591
rebind_available = mutable_state != nullptr && mutable_state->available();
14501592
if (rebind_available && mutable_state->validate_coverage() != Error::Ok) {
@@ -1493,6 +1635,12 @@ Result<std::unique_ptr<MuseGlimmerEngine>> MuseGlimmerEngine::create(
14931635
/*vision_runtime=*/nullptr,
14941636
rebind_available,
14951637
std::move(mutable_state)));
1638+
#ifdef EXECUTORCH_BUILD_CUDA
1639+
// Handed over after construction: the cache has to be installed before the
1640+
// module loads, which happens above.
1641+
engine->offgraph_guard_ = std::move(offgraph_guard);
1642+
engine->offgraph_control_ = offgraph_control;
1643+
#endif
14961644
if (has_vision) {
14971645
MuseGlimmerVisionRuntimeConfig vision_config;
14981646
vision_config.module = engine->shared_module_.get();
@@ -1623,6 +1771,9 @@ Result<std::unique_ptr<LLMSession>> MuseGlimmerEngine::create_session() {
16231771
min_prefill_chunk_,
16241772
decode_pos_table_dev_,
16251773
rebind_available_ ? mutable_state_.get() : nullptr,
1774+
#ifdef EXECUTORCH_BUILD_CUDA
1775+
offgraph_control_,
1776+
#endif
16261777
token));
16271778
}
16281779

0 commit comments

Comments
 (0)