Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tensorrt_llm/_torch/auto_deploy/llm_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ def validate_supported_speculative_config(self):
if spec_config is None:
return self

if spec_config.moe_backend is not None:
raise ValueError(
"AutoDeploy does not support speculative_config.moe_backend. "
"This draft-model override is available only with the PyTorch backend."
)

if isinstance(spec_config, MTPDecodingConfig):
if not spec_config.mtp_eagle_one_model or spec_config.use_mtp_vanilla:
raise ValueError(
Expand Down
3 changes: 3 additions & 0 deletions tensorrt_llm/_torch/models/modeling_dflash.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ def __init__(self, draft_config, *, dflash_attention_backend: str = "VANILLA"):

# Remove spec_config to prevent recursive spec-dec initialization
draft_config_no_spec = replace(draft_config, spec_config=None, lm_head_gather_output=False)
# ModelConfig.extra_attrs is init=False, so dataclasses.replace() does
# not preserve the shared custom-op registries.
draft_config_no_spec.extra_attrs = draft_config.extra_attrs

# Weights will be loaded later by ModelLoader.load_draft_weights()
self.draft_model_full = DraftModelClass(draft_config_no_spec)
Expand Down
56 changes: 40 additions & 16 deletions tensorrt_llm/_torch/models/modeling_dspark.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class or the edge would become a cycle.
from ..._utils import is_sm_100f
from ..cute_dsl_utils import IS_CUTLASS_DSL_AVAILABLE
from ..distributed import AllReduceParams
from ..model_config import ModelConfig
from ..modules.linear import Linear
from ..modules.mhc.hyper_connection import HCHead
from ..modules.rms_norm import RMSNorm
Expand Down Expand Up @@ -1018,6 +1019,7 @@ def __init__(
aux_stream_dict: Dict[AuxStreamType, torch.cuda.Stream],
num_stages: Optional[int] = None,
block_size: Optional[int] = None,
draft_moe_backend: Optional[str] = None,
):
super().__init__()
config = model_config.pretrained_config
Expand Down Expand Up @@ -1078,7 +1080,9 @@ def __init__(
# buffers. The draft experts are physically MXFP4 (same as the main
# MoE layers), so copy a main MoE layer's experts quant onto the
# draft layer keys.
draft_model_config = self._derive_draft_model_config(model_config, base, self.num_stages)
draft_model_config = self._derive_draft_model_config(
model_config, base, self.num_stages, draft_moe_backend=draft_moe_backend
)
self.mtp_layers = nn.ModuleList(
[
DSv4DSparkBlock(
Expand Down Expand Up @@ -1238,31 +1242,40 @@ def _dspark_freqs_table(self, device: torch.device) -> torch.Tensor:
return cached

@classmethod
def _derive_draft_model_config(cls, model_config, base: int, num_stages: int):
def _derive_draft_model_config(
cls, model_config, base: int, num_stages: int, draft_moe_backend: Optional[str] = None
):
"""Return a draft-only ``model_config`` copy with draft-specific fixes.

Applies (1) the ``compress_ratios`` draft slice and (2) the
``quant_config_dict`` MXFP4 extension for the draft layers' routed
experts. A single shallow copy is made (and only when something needs to
change) so the shared ``model_config`` and the target model are untouched.

The draft MoE backend is **inherited** from the target's
``model_config.moe_backend`` (carried by the shallow copy) — not pinned —
matching every other drafter (the MTP module reuses the V4 decoder layer,
whose MoE is built with ``moe_backend=model_config.moe_backend``; separate
Eagle3/DFlash drafts resolve it from their own config the same way). The
draft ``mtp.*`` stages are full V4 blocks, so they share the target's
MXFP4 ``n_routed_experts=384`` / ``n_group=8`` (= 48 experts/group) layout
and therefore the same backend constraints: pick a backend that supports
it (CUTLASS today, DeepGEMM megaMoE once available) on the target and the
draft follows. Note the TRTLLM-Gen ``blockScaleMoe`` routing kernel asserts
``experts/group <= 32`` (warp size), so it is incompatible with this layout
for both the target and the draft.
The draft MoE backend inherits ``model_config.moe_backend`` unless
``draft_moe_backend`` is set. AUTO is resolved after the draft-specific
quantization normalization below, so backend selection uses the draft
weights rather than the target's resolved backend. The draft ``mtp.*``
stages are full V4 blocks, so they share the target's MXFP4
``n_routed_experts=384`` / ``n_group=8`` (= 48 experts/group) layout
and therefore the same backend constraints: select a backend that
supports it (CUTLASS today, DeepGEMM megaMoE once available). The
TRTLLM-Gen ``blockScaleMoe`` routing kernel asserts
``experts/group <= 32`` (warp size), so it is incompatible with this
layout for both the target and the draft.
"""
new_sa = cls._draft_sparse_config(model_config, base, num_stages)
new_qcd = cls._draft_quant_config_dict(model_config, base, num_stages)
new_qc = cls._draft_normalized_quant_config(model_config)
if new_sa is None and new_qcd is None and new_qc is None:
resolved_moe_backend = None
if draft_moe_backend is not None:
architectures = getattr(model_config.pretrained_config, "architectures", None) or []
architecture = architectures[0] if architectures else ""
draft_quant_config = new_qc if new_qc is not None else model_config.quant_config
resolved_moe_backend = ModelConfig.resolve_moe_backend(
draft_moe_backend, architecture, quant_config=draft_quant_config
)
if new_sa is None and new_qcd is None and new_qc is None and resolved_moe_backend is None:
return model_config
draft_cfg = copy.copy(model_config)
# ModelConfig is a frozen dataclass; bypass the guard for these fields.
Expand All @@ -1272,6 +1285,8 @@ def _derive_draft_model_config(cls, model_config, base: int, num_stages: int):
object.__setattr__(draft_cfg, "quant_config_dict", new_qcd)
if new_qc is not None:
object.__setattr__(draft_cfg, "quant_config", new_qc)
if resolved_moe_backend is not None:
object.__setattr__(draft_cfg, "moe_backend", resolved_moe_backend)
return draft_cfg

@staticmethod
Expand Down Expand Up @@ -1833,13 +1848,21 @@ class DSv4DSparkForCausalLM(nn.Module):
attention weights from the in-memory state dict.
"""

def __init__(self, draft_config, aux_stream_dict=None, num_stages=None, block_size=None):
def __init__(
self,
draft_config,
aux_stream_dict=None,
num_stages=None,
block_size=None,
draft_moe_backend: Optional[str] = None,
):
super().__init__()
self.dspark_model = DSv4DSparkDraftModel(
draft_config,
aux_stream_dict,
num_stages=num_stages,
block_size=block_size,
draft_moe_backend=draft_moe_backend,
)
# Generic handles expected by the loader / weight mappers.
self.model = self.dspark_model
Expand Down Expand Up @@ -2132,6 +2155,7 @@ def _build_dspark_draft(model_config, draft_config, lm_head, model):
getattr(model, "aux_stream_dict", None),
num_stages=num_stages,
block_size=model_config.spec_config.block_size,
draft_moe_backend=getattr(model_config.spec_config, "moe_backend", None),
)

# No per-model_type table here. ``DFlashForCausalLM.__init__`` already
Expand Down
3 changes: 3 additions & 0 deletions tensorrt_llm/_torch/models/modeling_gemma4.py
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,9 @@ def __init__(self, model_config: ModelConfig):
pretrained_config=assistant_text_config,
spec_config=None,
)
# extra_attrs is init=False and would otherwise be reset by replace(),
# disconnecting the assistant's custom-op registries from the engine.
text_model_config.extra_attrs = model_config.extra_attrs
super().__init__(
Gemma4TextModel(text_model_config),
config=model_config,
Expand Down
4 changes: 4 additions & 0 deletions tensorrt_llm/_torch/models/modeling_gemma4mm.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,9 @@ def get_sub_model_config(
attn_backend=attn_backend,
quant_config=quant_config,
)
# extra_attrs is init=False and would otherwise be reset by replace().
# All submodels execute under the top-level engine registry.
sub_config.extra_attrs = model_config.extra_attrs
if (
hasattr(sub_config.pretrained_config, "torch_dtype")
and sub_config.pretrained_config.torch_dtype is None
Expand Down Expand Up @@ -1072,6 +1075,7 @@ def __init__(self, model_config: ModelConfig[Gemma4Config]):
self._mm_token_ids = torch.tensor(_mm_ids, dtype=torch.int32)

model_config_cp = copy.deepcopy(model_config)
model_config_cp.extra_attrs = model_config.extra_attrs
self.model_config = model_config_cp

# --- Language model ---
Expand Down
50 changes: 44 additions & 6 deletions tensorrt_llm/_torch/models/modeling_speculative.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
import copy
import inspect
from dataclasses import replace
from typing import Dict, Generic, List, Optional, Tuple
Expand Down Expand Up @@ -1192,6 +1193,9 @@ def __init__(self, draft_config):
draft_config_no_spec = replace(draft_config,
spec_config=None,
lm_head_gather_output=False)
# ModelConfig.extra_attrs is init=False, so dataclasses.replace() does
# not preserve the shared custom-op registries.
draft_config_no_spec.extra_attrs = draft_config.extra_attrs

# Weights will be loaded later by ModelLoader.load_draft_weights()
self.draft_model_full = DraftModelClass(draft_config_no_spec)
Expand Down Expand Up @@ -1442,6 +1446,29 @@ def forward(self,
)


def _get_requested_draft_moe_backend(model_config: ModelConfig,
spec_config: object) -> str:
"""Return the draft MoE backend request, preserving target inheritance."""
requested_backend = getattr(spec_config, "moe_backend", None)
return (model_config.moe_backend
if requested_backend is None else requested_backend)


def _copy_model_config_with_moe_backend(
model_config: ModelConfig, requested_moe_backend: str) -> ModelConfig:
"""Copy a ModelConfig and resolve its MoE backend against its own weights."""
architectures = getattr(model_config.pretrained_config, "architectures",
None) or []
architecture = architectures[0] if architectures else ""
resolved_moe_backend = ModelConfig.resolve_moe_backend(
requested_moe_backend,
architecture,
quant_config=model_config.quant_config)
draft_config = copy.copy(model_config)
object.__setattr__(draft_config, "moe_backend", resolved_moe_backend)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why __setattr__ instead of just draft_config.moe_backend = ...?

return draft_config


def external_drafter_config_kwargs(model_config, spec_config) -> dict:
"""`ModelConfig.from_pretrained` kwargs for a one-model external drafter.

Expand All @@ -1461,7 +1488,7 @@ def external_drafter_config_kwargs(model_config, spec_config) -> dict:
kwargs = dict(
trust_remote_code=True,
attn_backend=model_config.attn_backend,
moe_backend=model_config.moe_backend,
moe_backend=_get_requested_draft_moe_backend(model_config, spec_config),
mapping=model_config.mapping,
spec_config=None, # Avoid recursive spec-dec
max_num_tokens=model_config.max_num_tokens,
Expand Down Expand Up @@ -1601,25 +1628,37 @@ def __init__(self,
if spec_config and spec_config.spec_dec_mode.use_one_engine():
# Only create draft_model for modes MTP, Eagle3 (not SA)
if not spec_config.spec_dec_mode.is_sa():
requested_draft_moe_backend = _get_requested_draft_moe_backend(
model_config, spec_config)
if spec_config.spec_dec_mode.is_eagle3_one_model():
if spec_config.eagle3_model_arch == "mistral_large3":
from tensorrt_llm._torch.models.checkpoints.mistral.config_loader import \
MistralConfigLoader
self.draft_config = MistralConfigLoader().load(
spec_config.speculative_model,
mapping=model_config.mapping,
moe_backend=model_config.moe_backend,
moe_backend=requested_draft_moe_backend,
moe_max_num_tokens=model_config.moe_max_num_tokens,
max_num_tokens=model_config.max_num_tokens,
moe_load_balancer=model_config.moe_load_balancer,
skip_create_weights_in_init=True,
)
if getattr(spec_config, "moe_backend",
None) is not None:
# Unlike ModelConfig.from_pretrained, the Mistral
# loader does not resolve AUTO after loading quant
# metadata. Resolve it against the draft config now,
# before constructing any draft modules.
self.draft_config = \
_copy_model_config_with_moe_backend(
self.draft_config,
requested_draft_moe_backend)
elif spec_config.eagle3_model_arch == "llama3":
self.draft_config = ModelConfig.from_pretrained(
model_config.spec_config.speculative_model,
trust_remote_code=True,
attn_backend=model_config.attn_backend,
moe_backend=model_config.moe_backend,
moe_backend=requested_draft_moe_backend,
mapping=model_config.mapping,
spec_config=model_config.spec_config,
max_num_tokens=model_config.max_num_tokens,
Expand All @@ -1637,15 +1676,14 @@ def __init__(self,
spec_config.speculative_model,
trust_remote_code=True,
attn_backend=model_config.attn_backend,
moe_backend=model_config.moe_backend,
moe_backend=requested_draft_moe_backend,
mapping=model_config.mapping,
spec_config=None,
max_num_tokens=model_config.max_num_tokens,
moe_max_num_tokens=model_config.moe_max_num_tokens)
self.draft_config.quant_config.kv_cache_quant_algo = \
model_config.quant_config.kv_cache_quant_algo
self.draft_config.extra_attrs = dict(
model_config.extra_attrs)
self.draft_config.extra_attrs = model_config.extra_attrs
self.draft_config.extra_attrs[
_SPECULATIVE_POSITION_HEADROOM] = (
2 * spec_config.tokens_per_gen_step)
Expand Down
9 changes: 7 additions & 2 deletions tensorrt_llm/_torch/moe/fused_moe/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,13 @@ def _register_layer(self, model_config: ModelConfig):
if model_config is not None and self.layer_idx_str is not None:
if "moe_layers" not in model_config.extra_attrs:
model_config.extra_attrs["moe_layers"] = {}
assert self.layer_idx_str not in model_config.extra_attrs["moe_layers"], \
f"Duplicate MoE layer for layer_idx={self.layer_idx_str}"
suffix = 0
# ``layer_idx`` is local to a model stack, while one-model
# speculative decoding shares this registry across target and
# draft modules. Preserve every module under a stable unique key.
while self.layer_idx_str in model_config.extra_attrs["moe_layers"]:
self.layer_idx_str = str(self.layer_idx) + f"_{suffix}"
suffix += 1
model_config.extra_attrs["moe_layers"][
self.layer_idx_str] = weakref.ref(self)
self.register_to_config = True
Expand Down
1 change: 1 addition & 0 deletions tensorrt_llm/_torch/speculative/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ def update_spec_config_from_model_config(spec_config,
if num_nextn_predict_layers is None:
num_nextn_predict_layers = 1
spec_config.num_nextn_predict_layers = num_nextn_predict_layers
spec_config._validate_moe_backend_compatibility(model_config_resolved=True)
is_vanilla = spec_config.spec_dec_mode.is_mtp_vanilla()

# Resolve max_draft_len when the user didn't set it:
Expand Down
Loading
Loading