Skip to content

Commit e907282

Browse files
[executorch][muse] Serve Muse Glimmer CUDA batching on a batched-sequence cache (#24882)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #24075 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/93/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/93/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/92/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/93/orig Differential Revision: [D125987299](https://our.internmc.facebook.com/intern/diff/D125987299/) @diff-train-skip-merge Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent cc05bff commit e907282

7 files changed

Lines changed: 326 additions & 36 deletions

File tree

‎backends/cuda/batching/cuda_executor.cpp‎

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -363,21 +363,46 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
363363
NotSupported,
364364
"CudaExecutor: the program must select its logits rows");
365365
ET_ASSIGN_OR_RETURN(geometry, metadata::read_cache_geometry(*module));
366+
// The program fixes its cache layout: a private history per sequence when it
367+
// publishes its sequence count, else one shared pool of cells.
366368
ET_ASSIGN_OR_RETURN(
367-
max_cells,
368-
metadata::detail::read_required_positive_int(*module, kMaxCellsMethod));
369-
370-
ET_CHECK_OR_RETURN_ERROR(
371-
max_cells > 1 && max_cells <= std::numeric_limits<int>::max(),
372-
InvalidProgram,
373-
"CudaExecutor: cell count must fit an int and leave a padding row");
374-
if (max_session_tokens == 0) {
375-
max_session_tokens = static_cast<int>(
376-
std::min(max_context_length, (max_cells - 1) / max_sessions));
369+
declared_seqs,
370+
metadata::detail::read_int_method(*module, kMaxSeqsMethod));
371+
const bool sequence_layout = declared_seqs.has_value();
372+
std::int64_t max_cells = 0;
373+
if (sequence_layout) {
377374
ET_CHECK_OR_RETURN_ERROR(
378-
max_session_tokens > 0,
375+
*declared_seqs > 0 &&
376+
*declared_seqs <= std::numeric_limits<int>::max() &&
377+
max_context_length <= std::numeric_limits<int>::max(),
378+
InvalidProgram,
379+
"CudaExecutor: invalid sequence count or context length");
380+
if (max_session_tokens == 0) {
381+
max_session_tokens = static_cast<int>(max_context_length);
382+
}
383+
ET_CHECK_OR_RETURN_ERROR(
384+
max_session_tokens <= max_context_length,
379385
InvalidArgument,
380-
"CudaExecutor: no cell budget remains for each resident session");
386+
"CudaExecutor: %d session tokens exceed the sequence capacity %" PRId64,
387+
max_session_tokens,
388+
max_context_length);
389+
} else {
390+
ET_ASSIGN_OR_RETURN(
391+
cells,
392+
metadata::detail::read_required_positive_int(*module, kMaxCellsMethod));
393+
max_cells = cells;
394+
ET_CHECK_OR_RETURN_ERROR(
395+
max_cells > 1 && max_cells <= std::numeric_limits<int>::max(),
396+
InvalidProgram,
397+
"CudaExecutor: cell count must fit an int and leave a padding row");
398+
if (max_session_tokens == 0) {
399+
max_session_tokens = static_cast<int>(
400+
std::min(max_context_length, (max_cells - 1) / max_sessions));
401+
ET_CHECK_OR_RETURN_ERROR(
402+
max_session_tokens > 0,
403+
InvalidArgument,
404+
"CudaExecutor: no cell budget remains for each resident session");
405+
}
381406
}
382407

383408
// Every forward_{N} and forward_others the program exports, narrowest first.
@@ -444,11 +469,11 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
444469
"CudaExecutor: more than one dynamic method");
445470
const int max_step_tokens = methods.back().max_tokens;
446471
ET_CHECK_OR_RETURN_ERROR(
447-
max_step_tokens < max_cells,
472+
sequence_layout ? max_step_tokens <= max_context_length
473+
: max_step_tokens < max_cells,
448474
InvalidProgram,
449-
"CudaExecutor: a %d-token step does not fit %" PRId64 " cells",
450-
max_step_tokens,
451-
max_cells);
475+
"CudaExecutor: a %d-token step does not fit the cache",
476+
max_step_tokens);
452477
// A dynamic method whose kernels switch at a small width is exported from
453478
// more selected rows than one; its selector is padded up to that.
454479
int min_selected_rows = 1;
@@ -483,7 +508,9 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
483508
// them all, or a step could find no free cell mid-generation. One of the
484509
// program's cells is the padding scratch row, never handed out.
485510
ET_CHECK_OR_RETURN_ERROR(
486-
static_cast<std::int64_t>(max_sessions) * max_session_tokens < max_cells,
511+
sequence_layout ||
512+
static_cast<std::int64_t>(max_sessions) * max_session_tokens <
513+
max_cells,
487514
InvalidArgument,
488515
"CudaExecutor: %d sessions of %d tokens exceed the program's %" PRId64
489516
" cells, one of which is reserved",
@@ -493,10 +520,16 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
493520

494521
// The pool is the program's: its size and widest step fix the shapes the
495522
// program declared for the step buffers.
523+
// A sequence's columns hold the program's context; every sequence has its
524+
// own.
496525
llm_cache::CacheConfig cfg{};
497-
cfg.capacity = static_cast<int>(max_cells);
526+
cfg.capacity =
527+
static_cast<int>(sequence_layout ? max_context_length : max_cells);
498528
cfg.kv_dtype = kv_dtype;
499529
cfg.max_write = max_step_tokens;
530+
if (sequence_layout) {
531+
cfg.max_seqs = static_cast<int>(*declared_seqs);
532+
}
500533
if (initial_capacity >= 0) {
501534
cfg.initial_capacity = std::min(initial_capacity, cfg.capacity);
502535
}
@@ -505,7 +538,11 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
505538
InvalidProgram,
506539
"CudaExecutor: the program's layout is unusable");
507540
auto built = llm_cache::CacheFactory::global().build(
508-
backend_id, llm_cache::kind::kBatchedCell, geometry, cfg);
541+
backend_id,
542+
sequence_layout ? llm_cache::kind::kBatchedSequence
543+
: llm_cache::kind::kBatchedCell,
544+
geometry,
545+
cfg);
509546
ET_CHECK_OK_OR_RETURN_ERROR(built.error());
510547
std::shared_ptr<llm_cache::Cache> cache = built.get();
511548
auto* const ctl = cache->as<llm_cache::BatchControl>();

‎backends/cuda/batching/cuda_executor.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ inline constexpr char kStaticMethodPrefix[] = "forward_";
4848
inline constexpr char kDynamicMethod[] = "forward_others";
4949
// The cell layout's pool size, which fixes the shape of its step buffers.
5050
inline constexpr char kMaxCellsMethod[] = "get_offgraph_kv_max_cells";
51+
// Published instead of the cell count by a program whose KV cache keeps a
52+
// private history per sequence.
53+
inline constexpr char kMaxSeqsMethod[] = "get_offgraph_kv_max_seqs";
5154
// Optional: the fewest selected rows forward_others was exported for. Its
5255
// selector is padded up to it. Defaults to 1.
5356
inline constexpr char kMinSelectedRowsMethod[] = "get_min_prefill_chunk";

‎examples/models/muse-glimmer/export/export_dflash.py‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import argparse
1616
import gc
17+
import json
1718
from typing import TYPE_CHECKING
1819

1920
import torch
@@ -177,6 +178,8 @@ def export_dflash(
177178
draft_max_cells: int | None = None,
178179
max_draft_owners: int = 4,
179180
forward_sizes=None,
181+
kv_layout: str = "cell",
182+
max_kv_seqs: int = 4,
180183
) -> None:
181184
"""Export DFlash target + draft to one CUDA or MLX .pte.
182185
@@ -282,6 +285,8 @@ def export_dflash(
282285
vision_model=vision_model,
283286
pos_embed_table=pos_embed_table,
284287
max_vision_patches=max_vision_patches,
288+
kv_layout=kv_layout,
289+
max_kv_seqs=max_kv_seqs,
285290
)
286291
return
287292

@@ -309,9 +314,19 @@ def prepare_cuda_packed_draft(
309314
max_step_tokens: int,
310315
max_cells: int,
311316
max_owners: int,
317+
kv_layout: str = "cell",
318+
max_kv_seqs: int = 4,
312319
):
313-
"""Trace the packed draft and publish its independent cell geometry."""
314-
from executorch.examples.models.muse_glimmer.export.export_solo import cell_manifest
320+
"""Trace the packed draft and publish its independent cache geometry.
321+
322+
On the sequence layout the draft keeps one private history per resident
323+
session, and each owner's block attends all of it and the whole block.
324+
"""
325+
from executorch.examples.models.muse_glimmer.export.export_solo import (
326+
batched_sequence_manifest,
327+
cell_manifest,
328+
DRAFT_MAX_KV_SEQS_METHOD,
329+
)
315330
from executorch.examples.models.muse_glimmer.model.dflash_model import (
316331
DFlashPackedExportModel,
317332
)
@@ -334,7 +349,12 @@ def prepare_cuda_packed_draft(
334349
if type(max_step_tokens) is not int or max_step_tokens < 2:
335350
raise ValueError("packed DFlash max_step_tokens must be an integer >= 2")
336351
max_write = max_step_tokens + max_owners * block
337-
if type(max_cells) is not int or max_cells <= max_write:
352+
if kv_layout not in ("cell", "sequence"):
353+
raise ValueError(f"kv_layout must be 'cell' or 'sequence', got {kv_layout!r}")
354+
sequence_layout = kv_layout == "sequence"
355+
if sequence_layout and (type(max_kv_seqs) is not int or max_kv_seqs <= 0):
356+
raise ValueError("the sequence layout needs max_kv_seqs > 0")
357+
if not sequence_layout and (type(max_cells) is not int or max_cells <= max_write):
338358
raise ValueError("draft max_cells must exceed context plus all owner blocks")
339359
if config.dim != target_config.dim:
340360
raise ValueError("draft and target hidden dimensions must agree")
@@ -349,8 +369,13 @@ def prepare_cuda_packed_draft(
349369

350370
convert_quantized_tensors_for_cuda(draft_model)
351371
draft_model.to(torch.bfloat16)
352-
manifest = cell_manifest(
353-
enable_dflash_offgraph_kv_cache(draft_model, max_write), max_cells
372+
single_manifest = enable_dflash_offgraph_kv_cache(draft_model, max_write)
373+
manifest = (
374+
batched_sequence_manifest(
375+
single_manifest, max_kv_seqs, causal=False, context_window=True
376+
)
377+
if sequence_layout
378+
else cell_manifest(single_manifest, max_cells)
354379
)
355380
combined = DFlashPackedExportModel(target_model, draft_model)
356381
owners = (
@@ -418,14 +443,21 @@ def prepare_cuda_packed_draft(
418443
"get_dflash_target_layers": torch.tensor(
419444
config.target_layers, dtype=torch.int32
420445
),
421-
"get_draft_offgraph_kv_max_cells": max_cells,
422446
"get_draft_max_step_tokens": max_write,
423447
"get_draft_max_owners": max_owners,
424448
"get_draft_dflash_block_size": block,
425449
"get_dflash_target_cache_key": "muse_glimmer_target",
426450
"get_dflash_draft_cache_key": "muse_glimmer_draft",
427451
}
428452
)
453+
if sequence_layout:
454+
metadata[DRAFT_MAX_KV_SEQS_METHOD] = max_kv_seqs
455+
# Fixes each sequence column's rows, which the runtime must match.
456+
metadata["get_draft_offgraph_kv_seq_capacity"] = json.loads(single_manifest)[
457+
"maximum_capacity"
458+
]
459+
else:
460+
metadata["get_draft_offgraph_kv_max_cells"] = max_cells
429461
return programs, manifest, metadata
430462

431463

@@ -1130,6 +1162,11 @@ def main() -> None:
11301162
parser.add_argument("--max-cells", type=int, default=65536)
11311163
parser.add_argument("--draft-max-cells", type=int, default=None)
11321164
parser.add_argument("--max-draft-owners", type=int, default=4)
1165+
from executorch.examples.models.muse_glimmer.export.export_solo import (
1166+
add_kv_layout_arguments,
1167+
)
1168+
1169+
add_kv_layout_arguments(parser)
11331170
parser.add_argument("--forward-sizes", default="1,2,4,8,16,32,64")
11341171
args = parser.parse_args()
11351172

@@ -1165,6 +1202,8 @@ def main() -> None:
11651202
draft_max_cells=args.draft_max_cells,
11661203
max_draft_owners=args.max_draft_owners,
11671204
forward_sizes=[int(n) for n in args.forward_sizes.split(",") if n],
1205+
kv_layout=args.kv_layout,
1206+
max_kv_seqs=args.max_kv_seqs,
11681207
)
11691208

11701209

0 commit comments

Comments
 (0)