Skip to content

Commit 81b1dc1

Browse files
[executorch][muse] Export packed DFlash drafts for CUDA batching (#24038)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #23980 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/79/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/79/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/78/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/79/orig Differential Revision: [D125705712](https://our.internmc.facebook.com/intern/diff/D125705712/) @diff-train-skip-merge Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent 58ca1a3 commit 81b1dc1

7 files changed

Lines changed: 861 additions & 22 deletions

File tree

‎.github/workflows/cuda.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ jobs:
618618
pip install gguf
619619
python -m pytest -v -o "addopts=" \
620620
examples/models/muse-glimmer/tests/test_cuda_batching_pipeline.py \
621+
examples/models/muse-glimmer/tests/test_cuda_dflash_packed.py \
621622
examples/models/muse-glimmer/tests/test_checkpoint_context.py
622623
623624
unittest-cuda-quantized-gemm:

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

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424

2525

2626
def validate_dflash_export_options(backend: str, use_offgraph_kv_cache=False) -> None:
27-
if use_offgraph_kv_cache and backend != "mlx":
28-
raise ValueError("Off-graph DFlash requires MLX")
2927
if backend not in {"cuda", "mlx"}:
3028
raise ValueError(f"Unsupported DFlash backend: {backend}")
3129

@@ -129,6 +127,11 @@ def export_dflash(
129127
mmproj: str | None = None,
130128
max_vision_patches: int = 16384,
131129
use_offgraph_kv_cache: bool = False,
130+
max_step_tokens: int = 4096,
131+
max_cells: int = 65536,
132+
draft_max_cells: int | None = None,
133+
max_draft_owners: int = 4,
134+
forward_sizes=None,
132135
) -> None:
133136
"""Export DFlash target + draft to one CUDA or MLX .pte.
134137
@@ -140,6 +143,8 @@ def export_dflash(
140143
mmproj loader and vision export contract to add ``vision_encoder``.
141144
"""
142145
validate_dflash_export_options(backend, use_offgraph_kv_cache)
146+
if use_offgraph_kv_cache and backend == "cuda" and mmproj is not None:
147+
raise ValueError("the CUDA batching DFlash export is text-only")
143148

144149
import executorch.extension.llm.export.gguf # noqa: F401
145150
import executorch.extension.llm.export.int4 # noqa: F401
@@ -197,6 +202,27 @@ def export_dflash(
197202
activation_dtype=activation_dtype,
198203
)
199204

205+
if use_offgraph_kv_cache and backend == "cuda":
206+
from executorch.examples.models.muse_glimmer.export.export_solo import (
207+
DEFAULT_FORWARD_SIZES,
208+
export_cuda_batching,
209+
)
210+
211+
export_cuda_batching(
212+
target_model,
213+
target_config,
214+
output_dir,
215+
max_step_tokens=max_step_tokens,
216+
max_cells=max_cells,
217+
forward_sizes=(
218+
DEFAULT_FORWARD_SIZES if forward_sizes is None else forward_sizes
219+
),
220+
dflash_draft=draft_model,
221+
max_draft_owners=max_draft_owners,
222+
draft_max_cells=draft_max_cells,
223+
)
224+
return
225+
200226
vision_model = None
201227
pos_embed_table = None
202228
if mmproj is not None:
@@ -229,6 +255,111 @@ def export_dflash(
229255
)
230256

231257

258+
def prepare_cuda_packed_draft(
259+
target_model,
260+
target_config,
261+
draft_model,
262+
*,
263+
max_step_tokens: int,
264+
max_cells: int,
265+
max_owners: int,
266+
):
267+
"""Trace the packed draft and publish its independent cell geometry."""
268+
from executorch.examples.models.muse_glimmer.export.export_solo import cell_manifest
269+
from executorch.examples.models.muse_glimmer.model.dflash_model import (
270+
DFlashPackedExportModel,
271+
)
272+
from executorch.examples.models.muse_glimmer.source_transformations.cuda import (
273+
add_dflash_hidden_tapping,
274+
enable_dflash_offgraph_kv_cache,
275+
offgraph_kv_cache_geometry,
276+
)
277+
from torch.export import Dim, export
278+
279+
config = draft_model.config
280+
block = config.block_size
281+
if (
282+
type(block) is not int
283+
or block < 2
284+
or type(max_owners) is not int
285+
or not 1 <= max_owners <= 4
286+
):
287+
raise ValueError("packed DFlash requires a trained block >= 2 and 1..4 owners")
288+
if type(max_step_tokens) is not int or max_step_tokens < 2:
289+
raise ValueError("packed DFlash max_step_tokens must be an integer >= 2")
290+
max_write = max_step_tokens + max_owners * block
291+
if type(max_cells) is not int or max_cells <= max_write:
292+
raise ValueError("draft max_cells must exceed context plus all owner blocks")
293+
if config.dim != target_config.dim:
294+
raise ValueError("draft and target hidden dimensions must agree")
295+
if target_model.activation_dtype != torch.bfloat16:
296+
raise ValueError("CUDA packed DFlash requires bfloat16 activations")
297+
if not 0 <= config.mask_token_id < target_config.vocab_size:
298+
raise ValueError("draft mask_token_id must be in the target vocabulary")
299+
add_dflash_hidden_tapping(target_model, list(config.target_layers))
300+
from executorch.examples.models.gemma4_31b.cuda_packers import (
301+
convert_quantized_tensors_for_cuda,
302+
)
303+
304+
convert_quantized_tensors_for_cuda(draft_model)
305+
draft_model.to(torch.bfloat16)
306+
manifest = cell_manifest(
307+
enable_dflash_offgraph_kv_cache(draft_model, max_write), max_cells
308+
)
309+
combined = DFlashPackedExportModel(target_model, draft_model)
310+
owners = (
311+
Dim.STATIC if max_owners == 1 else Dim("draft_owners", min=1, max=max_owners)
312+
)
313+
context = Dim("draft_new_context", min=0, max=max_step_tokens)
314+
kv_width = (
315+
context + block
316+
if max_owners == 1
317+
else Dim("draft_kv_width", min=block, max=max_write)
318+
)
319+
sample_owners = min(2, max_owners)
320+
sample_queries = sample_owners * block
321+
with torch.no_grad():
322+
program = export(
323+
combined,
324+
(
325+
torch.zeros((sample_owners, block), dtype=torch.long),
326+
torch.zeros(
327+
(1, 2, config.n_target_layers * config.dim), dtype=torch.bfloat16
328+
),
329+
torch.arange(2 + sample_queries, dtype=torch.long),
330+
torch.arange(2 + sample_queries, dtype=torch.long),
331+
),
332+
dynamic_shapes=(
333+
{0: owners} if max_owners > 1 else None,
334+
{1: context},
335+
{0: kv_width},
336+
{0: kv_width},
337+
),
338+
strict=True,
339+
)
340+
geometry = offgraph_kv_cache_geometry(draft_model)
341+
metadata = {
342+
key.replace("get_", "get_draft_", 1): value for key, value in geometry.items()
343+
}
344+
metadata.update(
345+
{
346+
"get_block_size": block,
347+
"get_mask_token_id": config.mask_token_id,
348+
"get_n_target_layers": config.n_target_layers,
349+
"get_dflash_target_layers": torch.tensor(
350+
config.target_layers, dtype=torch.int32
351+
),
352+
"get_draft_offgraph_kv_max_cells": max_cells,
353+
"get_draft_max_step_tokens": max_write,
354+
"get_draft_max_owners": max_owners,
355+
"get_draft_dflash_block_size": block,
356+
"get_dflash_target_cache_key": "muse_glimmer_target",
357+
"get_dflash_draft_cache_key": "muse_glimmer_draft",
358+
}
359+
)
360+
return {"draft_forward": program}, manifest, metadata
361+
362+
232363
def _dflash_constant_methods(
233364
*,
234365
target_config,
@@ -926,6 +1057,11 @@ def main() -> None:
9261057
"float16 for MLX and bfloat16 for CUDA.",
9271058
)
9281059
parser.add_argument("--use-offgraph-kv-cache", action="store_true")
1060+
parser.add_argument("--max-step-tokens", type=int, default=4096)
1061+
parser.add_argument("--max-cells", type=int, default=65536)
1062+
parser.add_argument("--draft-max-cells", type=int, default=None)
1063+
parser.add_argument("--max-draft-owners", type=int, default=4)
1064+
parser.add_argument("--forward-sizes", default="1,2,4,8,16,32,64")
9291065
args = parser.parse_args()
9301066

9311067
try:
@@ -955,6 +1091,11 @@ def main() -> None:
9551091
mmproj=args.mmproj,
9561092
max_vision_patches=args.max_vision_patches,
9571093
use_offgraph_kv_cache=args.use_offgraph_kv_cache,
1094+
max_step_tokens=args.max_step_tokens,
1095+
max_cells=args.max_cells,
1096+
draft_max_cells=args.draft_max_cells,
1097+
max_draft_owners=args.max_draft_owners,
1098+
forward_sizes=[int(n) for n in args.forward_sizes.split(",") if n],
9581099
)
9591100

9601101

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

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,21 @@ def step_forward(
268268
The whole step runs through the decoder so every token's K/V reaches the
269269
cache; only the rows the batch samples from go through the LM head.
270270
"""
271-
x = model._run_blocks(model.embed_text(tokens), input_pos)
271+
x = model.embed_text(tokens)
272+
taps = None
273+
if hasattr(model, "_dflash_target_layer_ids"):
274+
sliding_mask, global_mask = model._build_masks(input_pos)
275+
tapped = []
276+
for i, layer in enumerate(model.layers):
277+
if i in model._dflash_target_layer_ids:
278+
tapped.append(x)
279+
x = layer(x, input_pos, sliding_mask, global_mask)
280+
taps = torch.cat(tapped, dim=-1)
281+
else:
282+
x = model._run_blocks(x, input_pos)
272283
x = model.output_norm(x[:, logits_to_keep, :])
273-
return model._soft_cap(model.lm_head(x))[0]
284+
logits = model._soft_cap(model.lm_head(x))[0]
285+
return logits if taps is None else (logits, taps)
274286

275287

276288
def cell_manifest(sequence_manifest: str, max_cells: int) -> str:
@@ -324,14 +336,19 @@ def device_residency(forward_methods, device_sampling: bool):
324336
return device
325337

326338

327-
def export_cuda_batching(
339+
# Keep target and draft assembly under one shared-weight collector.
340+
def export_cuda_batching( # noqa: C901
328341
model: MuseGlimmerModel,
329342
config: MuseGlimmerConfig,
330343
output_dir: str,
331344
max_step_tokens: int,
332345
max_cells: int,
333346
forward_sizes=DEFAULT_FORWARD_SIZES,
334347
device_sampling: bool = True,
348+
*,
349+
dflash_draft: nn.Module | None = None,
350+
max_draft_owners: int = 4,
351+
draft_max_cells: int | None = None,
335352
):
336353
"""Exports the forward methods and writes the artifact.
337354
@@ -362,6 +379,20 @@ def export_cuda_batching(
362379
)
363380
from torch.export import Dim, export
364381

382+
if dflash_draft is not None:
383+
from executorch.examples.models.muse_glimmer.export.export_dflash import (
384+
prepare_cuda_packed_draft,
385+
)
386+
387+
draft_programs, draft_manifest, draft_metadata = prepare_cuda_packed_draft(
388+
model,
389+
config,
390+
dflash_draft,
391+
max_step_tokens=max_step_tokens,
392+
max_cells=max_cells if draft_max_cells is None else draft_max_cells,
393+
max_owners=max_draft_owners,
394+
)
395+
365396
forward_sizes = tuple(sorted({int(n) for n in forward_sizes}))
366397
widest_static = forward_sizes[-1] if forward_sizes else 0
367398
if not forward_sizes or forward_sizes[0] != 1:
@@ -392,16 +423,20 @@ def export_cuda_batching(
392423
manifest = cell_manifest(
393424
enable_offgraph_kv_cache(model, max_step_tokens), max_cells
394425
)
426+
if dflash_draft is not None:
427+
# Deferred checkpoints still need scalar/RoPE buffers, after KV removal.
428+
materialize_runtime_buffers(model, dtype=torch.bfloat16)
395429
geometry = offgraph_kv_cache_geometry(model)
396430

397431
width = Dim(
398432
"width", min=max(widest_static, _CUDA_MIN_PREFILL_CHUNK), max=max_step_tokens
399433
)
400434
rows = Dim("rows", min=_CUDA_MIN_PREFILL_CHUNK, max=max_step_tokens)
401435
programs = {}
402-
with common.BoundMethodForward(
403-
model, types.MethodType(step_forward, model)
404-
), torch.no_grad():
436+
with (
437+
common.BoundMethodForward(model, types.MethodType(step_forward, model)),
438+
torch.no_grad(),
439+
):
405440
for n in forward_sizes:
406441
print(f"Exporting forward_{n} (T={n})...")
407442
programs[f"forward_{n}"] = export(
@@ -431,23 +466,42 @@ def export_cuda_batching(
431466
gc.collect()
432467
torch.cuda.empty_cache()
433468
forward_methods = list(programs)
469+
if dflash_draft is not None:
470+
programs.update(draft_programs)
471+
del draft_programs, dflash_draft
472+
cache_methods = forward_methods + (
473+
["draft_forward"] if "draft_forward" in programs else []
474+
)
434475
if device_sampling:
435476
print(f"Exporting {SAMPLE_METHOD} and {ARGMAX_METHOD}...")
436477
programs.update(sampler_programs(config.vocab_size))
437478

438479
def partitioner(name: str) -> CudaPartitioner:
439-
if name not in forward_methods:
480+
if name not in cache_methods:
440481
return CudaPartitioner(
441482
[CudaBackend.generate_method_name_compile_spec(name)]
442483
)
443-
return CudaPartitioner(
444-
[
445-
CudaBackend.generate_method_name_compile_spec(name),
446-
CompileSpec("low_memory_mode", b"ON"),
447-
CompileSpec(OFFGRAPH_KV_COMPILE_SPEC, manifest.encode()),
448-
CompileSpec(OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC, step_width_spec(name)),
449-
]
450-
)
484+
is_draft = name == "draft_forward"
485+
specs = [
486+
CudaBackend.generate_method_name_compile_spec(name),
487+
CompileSpec("low_memory_mode", b"ON"),
488+
CompileSpec(
489+
OFFGRAPH_KV_COMPILE_SPEC,
490+
(draft_manifest if is_draft else manifest).encode(),
491+
),
492+
CompileSpec(
493+
OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC,
494+
b"2:0" if is_draft else step_width_spec(name),
495+
),
496+
]
497+
if "draft_forward" in cache_methods:
498+
specs.append(
499+
CompileSpec(
500+
"llm_cache_registry_key",
501+
b"muse_glimmer_draft" if is_draft else b"muse_glimmer_target",
502+
)
503+
)
504+
return CudaPartitioner(specs)
451505

452506
constant_methods = _solo_constant_methods(
453507
config=config,
@@ -463,6 +517,8 @@ def partitioner(name: str) -> CudaPartitioner:
463517
constant_methods["get_min_prefill_chunk"] = _CUDA_MIN_PREFILL_CHUNK
464518
constant_methods["use_sampling"] = False
465519
constant_methods[MAX_CELLS_METHOD] = max_cells
520+
if "draft_forward" in programs:
521+
constant_methods.update(draft_metadata)
466522

467523
print(f"Lowering {', '.join(programs)} to ExecuTorch (CUDA)...")
468524
edge = to_edge_transform_and_lower(
@@ -483,7 +539,7 @@ def partitioner(name: str) -> CudaPartitioner:
483539
extract_delegate_segments=True,
484540
do_quant_fusion_and_const_prop=True,
485541
memory_planning_pass=MemoryPlanningPass(alloc_graph_input=False),
486-
propagate_device_config=device_residency(forward_methods, device_sampling),
542+
propagate_device_config=device_residency(cache_methods, device_sampling),
487543
emit_mutable_buffer_names=True,
488544
),
489545
)

0 commit comments

Comments
 (0)