Skip to content

Commit 7dbdd80

Browse files
[executorch][muse] Serve images on CUDA batching (#24655)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #24065 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/88/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/88/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/87/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/88/orig Differential Revision: [D125987293](https://our.internmc.facebook.com/intern/diff/D125987293/) @diff-train-skip-merge Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent 187fac4 commit 7dbdd80

6 files changed

Lines changed: 113 additions & 33 deletions

File tree

‎examples/models/muse-glimmer/BATCHING_SERVING.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Muse Glimmer Batching Serving
22

3-
The batching runner and multiplexed worker support MLX autoregressive generation
4-
with text only or text plus one image across the submitted conversation, and
5-
CUDA autoregressive generation with text only. They are opt-in entry points; the
3+
The batching runner and multiplexed worker support MLX and CUDA autoregressive
4+
generation with text only or text plus one image across the submitted
5+
conversation. They are opt-in entry points; the
66
legacy runners and export defaults remain unchanged. DFlash batching runs on MLX
77
and CUDA (text only).
88
CUDA specifics are in [CUDA](#cuda) below.
@@ -205,8 +205,10 @@ python -m executorch.examples.models.muse_glimmer.export.export_solo \
205205

206206
`--forward-sizes` (default `1,2,4,8,16,32,64`), `--max-step-tokens` (default
207207
4096) and `--max-cells` (default 65536) size the methods and the shared KV
208-
pool; `--logits` samples on the host instead. The artifact is text only, so
209-
`--mmproj` is rejected.
208+
pool; `--logits` samples on the host instead. Add `--mmproj` for images: the
209+
token forwards are unchanged, and the artifact gains `vision_encoder`,
210+
`embed_text` and a dynamic `forward_from_embeddings` over the same cache, which
211+
runs only the prefill steps that carry an image.
210212

211213
Build with `make muse-glimmer-batching-cuda` on Linux, then serve:
212214

@@ -221,8 +223,8 @@ python -m executorch.examples.models.muse_glimmer.serving.serve_batching \
221223
--max-context 8192
222224
```
223225

224-
`--data-path` is required and `--pos-embed-path` is rejected; image requests
225-
fail preparation. Every resident session reserves `--max-context` cells of the
226+
`--data-path` is required. Image artifacts read `pos_embed.bin` beside
227+
`model.pte`, or `--pos-embed-path`. Every resident session reserves `--max-context` cells of the
226228
pool: requests, `--prefix-cache-entries` snapshots, and the resolved number of
227229
capture slots when snapshots are enabled. `--prefix-capture-slots 0` (the default)
228230
matches request concurrency, capped at two when checkpoints are enabled; a

‎examples/models/muse-glimmer/runtime/backend_factory.cpp‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,8 @@ runtime::Result<MuseGlimmerBackend> create_muse_glimmer_backend(
5858
session_tokens > 1,
5959
InvalidArgument,
6060
"Muse Glimmer CUDA: sessions need room for a prompt and prediction");
61-
auto spec = std::make_shared<const MuseGlimmerPreparationSpec>(
62-
MuseGlimmerPreparationSpec{
63-
aten::ScalarType::BFloat16,
64-
/*hidden_dim=*/0,
65-
session_tokens,
66-
static_cast<int32_t>(executor->preferred_batch_tokens()),
67-
vocab,
68-
/*has_vision=*/false,
69-
/*max_soft_tokens=*/0,
70-
config.image_limits,
71-
config.bos_id});
61+
ET_ASSIGN_OR_RETURN(
62+
spec, make_cuda_preparation(*executor, config, session_tokens));
7263
return MuseGlimmerBackend{std::move(executor), std::move(spec)};
7364
}
7465
#endif

‎examples/models/muse-glimmer/runtime/cuda_dflash_executor.cpp‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88
#include <executorch/examples/models/muse-glimmer/runtime/cuda_dflash_executor.h>
99

10+
#include <executorch/examples/models/muse-glimmer/runtime/model_embeddings.h>
11+
1012
#include <cuda_runtime.h>
1113
#include <executorch/backends/cuda/runtime/backend_options.h>
1214
#include <executorch/extension/cuda/caller_stream.h>
@@ -462,6 +464,77 @@ MuseGlimmerCudaDFlashExecutor::~MuseGlimmerCudaDFlashExecutor() {
462464
}
463465
}
464466

467+
Result<std::shared_ptr<const MuseGlimmerPreparationSpec>> make_cuda_preparation(
468+
cb::CudaExecutor& target,
469+
const MuseGlimmerBackendConfig& config,
470+
std::int32_t max_context,
471+
std::shared_ptr<MuseGlimmerModelEmbeddings>* embeddings) {
472+
ET_ASSIGN_OR_RETURN(names, target.module().method_names());
473+
const bool has_vision = names.count("vision_encoder") != 0;
474+
const auto hidden =
475+
static_cast<std::int32_t>(target.embeddings_hidden_size());
476+
std::int64_t max_soft_tokens = 0;
477+
if (has_vision) {
478+
ET_CHECK_OR_RETURN_ERROR(
479+
hidden > 0,
480+
InvalidProgram,
481+
"Muse Glimmer CUDA: vision needs %s",
482+
cb::kEmbeddingsMethod);
483+
ET_ASSIGN_OR_RETURN(
484+
soft_tokens,
485+
muse_glimmer_abi::check_vision_abi(
486+
target.module(),
487+
hidden,
488+
aten::ScalarType::BFloat16,
489+
config.max_vision_patches,
490+
::executorch::backends::cuda::kCudaBackendId));
491+
max_soft_tokens = soft_tokens;
492+
}
493+
auto spec = std::make_shared<const MuseGlimmerPreparationSpec>(
494+
MuseGlimmerPreparationSpec{
495+
aten::ScalarType::BFloat16,
496+
has_vision ? hidden : 0,
497+
max_context,
498+
static_cast<std::int32_t>(target.preferred_batch_tokens()),
499+
target.vocab_size(),
500+
has_vision,
501+
max_soft_tokens,
502+
config.image_limits,
503+
config.bos_id});
504+
if (!has_vision) {
505+
ET_CHECK_OR_RETURN_ERROR(
506+
config.pos_embed_path.empty(),
507+
InvalidArgument,
508+
"Muse Glimmer CUDA: the program has no vision_encoder, but --pos-embed-path was given (%s)",
509+
config.pos_embed_path.c_str());
510+
return spec;
511+
}
512+
auto path = config.pos_embed_path;
513+
if (path.empty()) {
514+
const auto slash = config.model_path.find_last_of('/');
515+
path = (slash == std::string::npos ? "."
516+
: config.model_path.substr(0, slash)) +
517+
"/pos_embed.bin";
518+
}
519+
auto model_embeddings =
520+
std::make_shared<MuseGlimmerModelEmbeddings>(spec, std::move(path));
521+
Module* module = &target.module();
522+
ET_CHECK_OK_OR_RETURN_ERROR(target.set_input_embedder(
523+
{[model_embeddings, module] {
524+
return model_embeddings->initialize(*module);
525+
},
526+
[model_embeddings](const batching::PreparedInput& input) {
527+
return model_embeddings->accepts(input);
528+
},
529+
[model_embeddings, module](const std::vector<batching::Input>& inputs) {
530+
return model_embeddings->materialize(*module, inputs);
531+
}}));
532+
if (embeddings) {
533+
*embeddings = std::move(model_embeddings);
534+
}
535+
return spec;
536+
}
537+
465538
Result<MuseGlimmerBackend> MuseGlimmerCudaDFlashExecutor::create(
466539
const MuseGlimmerBackendConfig& config) {
467540
ET_CHECK_OR_RETURN_ERROR(

‎examples/models/muse-glimmer/runtime/cuda_dflash_executor.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515

1616
namespace executorch::extension::llm {
1717

18+
class MuseGlimmerModelEmbeddings;
19+
20+
// The preparation spec for prompts to `target`. When the program has
21+
// vision_encoder, also installs an image embedder on it; `embeddings` then
22+
// receives it, for adapters that build their own target steps.
23+
runtime::Result<std::shared_ptr<const MuseGlimmerPreparationSpec>>
24+
make_cuda_preparation(
25+
::executorch::backends::cuda::batching::CudaExecutor& target,
26+
const MuseGlimmerBackendConfig& config,
27+
std::int32_t max_context,
28+
std::shared_ptr<MuseGlimmerModelEmbeddings>* embeddings = nullptr);
29+
1830
// Exact packed-export metadata contract. Geometry tensors use the ordinary
1931
// get_n_caches/get_kv_heads/get_head_dims/get_windows names with get_draft_.
2032
namespace cuda_dflash_detail {

‎examples/models/muse-glimmer/serving/serve_batching.py‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
"""Serve Muse Glimmer text/images with the batching multiplexed worker.
88
99
Requires an off-graph batching export: export_solo.py --use-offgraph-kv-cache
10-
for MLX (text and images); add --backend cuda for CUDA (text only). DFlash is
11-
opt-in, MLX only, and requires a matching export; the legacy launcher and
12-
runners remain separate.
10+
for MLX; add --backend cuda for CUDA. Both serve text and images when exported
11+
with --mmproj. DFlash is opt-in and requires a matching export; the legacy
12+
launcher and runners remain separate.
1313
"""
1414

1515
import argparse
@@ -122,8 +122,6 @@ def _validate_backend(parser, args):
122122
return
123123
if args.data_path is None:
124124
parser.error("--backend cuda requires --data-path (aoti_cuda_blob.ptd)")
125-
if args.pos_embed_path is not None:
126-
parser.error("--backend cuda serves text only; omit --pos-embed-path")
127125

128126

129127
def _validate_paths(parser, args):

‎examples/models/muse-glimmer/tests/test_serve_batching.py‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,20 +150,24 @@ def test_cuda_launcher_forwards_dflash(arguments, tmp_path):
150150
)
151151

152152

153-
@pytest.mark.parametrize("case", ["no_weights", "pos_embed"])
154-
def test_cuda_launcher_rejects_missing_weights_or_vision(arguments, tmp_path, case):
153+
def test_cuda_launcher_requires_the_weights_file(arguments):
154+
with pytest.raises(SystemExit) as error:
155+
serve_batching._parse_args(arguments + ["--backend", "cuda"])
156+
assert error.value.code == 2
157+
158+
159+
def test_cuda_worker_takes_the_vision_table(arguments, tmp_path):
155160
data = tmp_path / "aoti_cuda_blob.ptd"
156161
table = tmp_path / "pos_embed.bin"
157162
data.touch()
158163
table.touch()
159-
argv = arguments + ["--backend", "cuda"]
160-
if case != "no_weights":
161-
argv += ["--data-path", str(data)]
162-
if case == "pos_embed":
163-
argv += ["--pos-embed-path", str(table)]
164-
with pytest.raises(SystemExit) as error:
165-
serve_batching._parse_args(argv)
166-
assert error.value.code == 2
164+
args = serve_batching._parse_args(
165+
arguments
166+
+ ["--backend", "cuda", "--data-path", str(data)]
167+
+ ["--pos-embed-path", str(table)]
168+
)
169+
flags = dict(arg.split("=", 1) for arg in serve_batching._worker_command(args)[1:])
170+
assert flags["--pos_embed_path"] == str(table)
167171

168172

169173
@pytest.mark.parametrize("shortfall", [0, 1])

0 commit comments

Comments
 (0)