Skip to content

Commit c248dd5

Browse files
h-guo18claudekevalmorabia97
authored
[Feat]: Domino support (#1710)
### What does this PR do? Type of change: New feature Adds **Domino** speculative decoding: the parallel DFlash draft backbone plus a lightweight **GRU causal correction head**. The backbone produces *base* logits for a full draft block in one forward; a GRU over the block's teacher-forced tokens produces a causal state that is fused with the backbone hidden state and projected to a vocab-sized logit correction on the block suffix — injecting the intra-block causal dependency the parallel backbone lacks. Trained with a dual loss `(1-λ)*final + λ*base`, where `λ_base` decays linearly 1→0 (curriculum: learn the parallel backbone first, then the correction). Reuses the DFlash mode/config/recipe; selected via `dflash_architecture_config.projector_type=domino` and routed to its own registry so `HFDominoModel` does not shadow `HFDFlashModel`. Exports in the z-lab/SpecForge drafter format (`prefix_gru.*` / `embed_proj.*`). > Note: the inference side (vLLM / AR evaluation) is intentionally **not** wired up yet — the correction head is not applied in serving. To be added once the inference path lands. ### Usage ```bash # Online training (recipe: projector_type=domino) uv run launch.py --yaml examples/Qwen/Qwen3-8B/hf_online_domino.yaml --yes ``` ### Testing CPU unit tests in `tests/unit/torch/speculative/plugins/test_hf_domino.py` cover conversion routing, the training forward (dual loss + grads), the λ schedule, and the export format. Online Qwen3-8B training validated end-to-end (loss curve below). <img width="1803" height="809" alt="image" src="https://github.com/user-attachments/assets/7c9d2001-bd80-4dec-919b-443e61089cca" /> ### Before your PR is "*Ready for review*" - Is this change backward compatible?: ✅ (opt-in via `projector_type=domino`; DFlash path unchanged) - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: N/A (no new dependency) - Did you write any new necessary tests?: ✅ - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: ✅ - Did you get Claude approval on this PR?: ❌ ### Additional Information Reference: SpecForge PR #571 (z-lab); drafter format `huggingface.co/Huang2020/Qwen3-8B-Domino-b16`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added Domino speculative-decoding training with a decaying base/final dual-loss curriculum and Domino-specific lambda scheduling (training-only; inference wiring not yet included). * Added Domino draft-head export support for training checkpoints. * **Documentation & Configuration** * Added a Domino speculative-decoding training recipe and an HF Online Domino launcher configuration for Qwen3-8B. * **Refactor** * Updated speculative model conversion/export to route to Domino variants based on the configured projector type. * **Tests** * Added unit tests for Domino conversion, training loss/metrics, lambda decay behavior, and exporter output layout. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
1 parent 3003589 commit c248dd5

13 files changed

Lines changed: 1007 additions & 10 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ repos:
112112
modelopt/torch/quantization/plugins/attention.py|
113113
modelopt/torch/sparsity/attention_sparsity/methods/vsa_utils.py|
114114
modelopt/torch/speculative/eagle/utils.py|
115+
modelopt/torch/speculative/plugins/hf_domino.py|
116+
modelopt/torch/speculative/plugins/modeling_domino.py|
115117
modelopt/torch/speculative/plugins/hf_dflash.py|
116118
modelopt/torch/speculative/plugins/modeling_dflash.py|
117119
modelopt/torch/speculative/plugins/hf_medusa.py|

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Changelog
3434
- New DiffusionGemma model-specific recipe under ``modelopt_recipes/huggingface/diffusion_gemma/ptq/`` (``nvfp4_experts_only.yaml`` + its ``disabled_quantizers.yaml`` unit) adds the ``*self_conditioning*`` exclude on top of the standard default, leaving the shared ``default_disabled_quantizers`` unit clean for non-diffusion models — pattern matches the existing ``phi4mm`` / ``nemotron_vl`` model-specific recipes.
3535
- ``hf_ptq.py`` also unwraps ``ModelOutput`` dataclasses from ``.generate()`` so the preview decode works on diffusion models. Non-tied models see no behavioral change.
3636
- Add **context-parallel (CP)** and **data-parallel (DP)** support to the shared Megatron-Core inference/calibration utilities. Under CP, ``get_megatron_calibration_forward_loop`` and ``megatron_mmlu`` partition each sequence across CP ranks (zigzag load-balanced), ``megatron_prefill`` accepts a CP-partitioned ``position_ids`` and lets the CP-aware causal attention build the mask, and MMLU gathers per-rank logits back to the full sequence for last-token scoring. Under DP, calibration shards the dataset across data-parallel ranks (``DistributedSampler``; amax is max-reduced across the DP group inside ``mtq``) and ``megatron_mmlu`` shards whole batches across DP ranks and all-reduces the per-subject counts. DP is implicit (``world_size / (tp * pp * cp)``); ``examples/megatron_bridge/quantize.py`` gains a ``--cp_size`` flag.
37+
- Add **Domino** speculative-decoding training: the parallel DFlash draft backbone plus a lightweight GRU causal correction head, selected via ``dflash_architecture_config.projector_type=domino``. Trained with a base/final dual loss whose ``dflash_lambda_base_start``/``dflash_lambda_base_decay_ratio`` curriculum decays the base-loss weight 1→0. Exports in the z-lab drafter format; recipe at ``modelopt_recipes/general/speculative_decoding/domino.yaml``. Training only — the inference path is not wired up yet.
3738

3839
**Bug Fixes**
3940

examples/speculative_decoding/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
ModelOptMedusaRecipe,
5757
ModelOptSpeculativeRecipeBase,
5858
)
59+
from modelopt.torch.speculative.plugins.hf_domino import DominoLambdaCallback
5960
from modelopt.torch.speculative.plugins.hf_training_args import (
6061
TrainingArguments as SpecTrainingArgs,
6162
)
@@ -295,6 +296,13 @@ def train():
295296
and recipe.eagle.eagle_base_lora_warmup_steps > 0
296297
):
297298
callbacks.append(LoRAWarmupCallback(recipe.eagle.eagle_base_lora_warmup_steps))
299+
# Domino (dflash recipe with projector_type=domino) needs the lambda_base
300+
# curriculum schedule driven by the trainer's global step.
301+
if (
302+
isinstance(recipe, ModelOptDFlashRecipe)
303+
and recipe.dflash.dflash_architecture_config.get("projector_type") == "domino"
304+
):
305+
callbacks.append(DominoLambdaCallback())
298306
# Leave training_args.ignore_data_skip at its default (False). The dataset is
299307
# map-style, so HF Trainer's resume skips consumed indices at the batch-sampler
300308
# level (accelerate.skip_first_batches) without re-fetching them, landing at the

modelopt/torch/export/plugins/hf_spec_export.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,35 @@ def export(self, export_dir: Path | str, dtype: torch.dtype | None = None):
445445
f"Exported DFlash draft model: {len(drafter_sd)} tensors, "
446446
f"config keys: {list(drafter_config.keys())[:5]}..."
447447
)
448+
449+
450+
class DominoExporter(DFlashExporter):
451+
"""Draft model exporter for Domino (DFlash backbone + causal correction head).
452+
453+
Same z-lab-compatible format as DFlash, plus the Domino head weights
454+
(``prefix_gru.*`` / ``embed_proj.*``, already captured by the inherited
455+
``dflash_module.`` stripping) and the extra config fields the loader needs to
456+
rebuild the head (``projector_type``, ``emb_dim``, ``gru_hidden_dim``,
457+
``pure_draft_prefix_len``, ``shift_label``).
458+
"""
459+
460+
def _export_config(self):
461+
"""Extend the DFlash config with the Domino head fields."""
462+
config = super()._export_config()
463+
draft_config = self.model.dflash_config
464+
465+
# Present because HFDominoModel.modify validates them at convert time.
466+
emb_dim = draft_config.emb_dim
467+
gru_hidden_dim = draft_config.gru_hidden_dim
468+
# Mirror the reference checkpoint: emb_dim also appears at the top level.
469+
config["emb_dim"] = emb_dim
470+
config["dflash_config"].update(
471+
{
472+
"projector_type": getattr(draft_config, "projector_type", "domino"),
473+
"shift_label": getattr(draft_config, "shift_label", True),
474+
"pure_draft_prefix_len": getattr(draft_config, "pure_draft_prefix_len", 1),
475+
"gru_hidden_dim": gru_hidden_dim,
476+
"emb_dim": emb_dim,
477+
}
478+
)
479+
return config

modelopt/torch/speculative/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,27 @@ class DFlashConfig(ModeloptBaseConfig):
163163
),
164164
)
165165

166+
dflash_lambda_base_start: float = ModeloptField(
167+
default=1.0,
168+
ge=0.0,
169+
le=1.0,
170+
description=(
171+
"Domino only: initial weight of the base (backbone-only) loss in the "
172+
"loss = (1 - lambda)*final + lambda*base mixture; linearly decayed to 0. "
173+
"Ignored unless dflash_architecture_config.projector_type == 'domino'."
174+
),
175+
)
176+
177+
dflash_lambda_base_decay_ratio: float = ModeloptField(
178+
default=1.0,
179+
gt=0.0,
180+
le=1.0,
181+
description=(
182+
"Domino only: fraction of total training steps over which lambda_base "
183+
"decays from dflash_lambda_base_start to 0."
184+
),
185+
)
186+
166187
@model_validator(mode="after")
167188
def _check_dpace_alpha(self) -> "DFlashConfig":
168189
# Validate at construction regardless of the active objective, so a bad alpha

modelopt/torch/speculative/dflash/conversion.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,43 @@
2424
from ..config import DFlashConfig
2525

2626
DFlashDMRegistry = _DMRegistryCls(prefix="DFlash") # global instance for the registry
27+
# Domino reuses the dflash mode/config/recipe but converts the base model to a
28+
# DFlash module augmented with a causal correction head. It is selected via
29+
# ``dflash_architecture_config.projector_type == "domino"`` and lives in its own
30+
# registry so its wrapper (HFDominoModel) does not overwrite HFDFlashModel.
31+
DominoDMRegistry = _DMRegistryCls(prefix="Domino")
2732

2833

2934
def convert_to_dflash_model(model: nn.Module, config: DFlashConfig) -> ConvertReturnType:
30-
"""Convert the model to a DFlash model as per `config`."""
35+
"""Convert the model to a DFlash (or Domino) model as per `config`."""
3136
model = model.init_modellike() if isinstance(model, ModelLikeModule) else model
3237

33-
original_cls = type(model)
34-
if original_cls not in DFlashDMRegistry:
35-
for cls in DFlashDMRegistry._registry:
36-
if issubclass(original_cls, cls):
37-
DFlashDMRegistry.register({original_cls: "base_model_class"})(DFlashDMRegistry[cls])
38-
break
39-
4038
# merge custom config with default config (lazy import to avoid circular)
4139
from .default_config import default_dflash_config
4240

4341
custom_config = config.dflash_architecture_config
4442
config.dflash_architecture_config = {**default_dflash_config, **custom_config}
4543

46-
dflash_model = DFlashDMRegistry.convert(model)
44+
# Route to the Domino registry when the architecture asks for the Domino head.
45+
projector_type = config.dflash_architecture_config.get("projector_type")
46+
if projector_type == "domino":
47+
registry = DominoDMRegistry
48+
elif projector_type in (None, "dflash"):
49+
registry = DFlashDMRegistry
50+
else:
51+
raise ValueError(
52+
f"Unsupported dflash_architecture_config.projector_type: {projector_type!r}. "
53+
"Expected 'dflash' (default) or 'domino'."
54+
)
55+
56+
original_cls = type(model)
57+
if original_cls not in registry:
58+
for cls in registry._registry:
59+
if issubclass(original_cls, cls):
60+
registry.register({original_cls: "base_model_class"})(registry[cls])
61+
break
62+
63+
dflash_model = registry.convert(model)
4764
dflash_model.modify(config)
4865

4966
metadata = {}

modelopt/torch/speculative/plugins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131

3232
with import_plugin("transformers"):
3333
from .hf_dflash import *
34+
from .hf_domino import *
3435
from .hf_eagle import *
3536
from .hf_medusa import *

modelopt/torch/speculative/plugins/hf_dflash.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,9 @@ def modify(self, config):
269269

270270
self._find_base_model_parts()
271271

272-
self.dflash_module = DFlashModule(self.dflash_config)
272+
# Factory hook: subclasses (e.g. Domino) override to build an augmented
273+
# draft module while reusing all of DFlash's modify() setup.
274+
self.dflash_module = self._build_draft_module(self.dflash_config)
273275
# Match base model dtype/device. Skip if base is on meta (during from_pretrained
274276
# restore — the model will be moved to the correct device after weight loading).
275277
if self.dflash_offline:
@@ -286,6 +288,10 @@ def modify(self, config):
286288
self.is_quantized = False
287289
self._num_anchors = self.dflash_num_anchors
288290

291+
def _build_draft_module(self, dflash_config):
292+
"""Build the draft module. Subclasses override to use an augmented module."""
293+
return DFlashModule(dflash_config)
294+
289295
def get_exporter(self):
290296
"""Get the exporter for the DFlash draft model."""
291297
from modelopt.torch.export.plugins.hf_spec_export import DFlashExporter

0 commit comments

Comments
 (0)