[GLM-5] Add DSV3.2/GLM5 to `vllm/models/` (#46808)

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>

Woosuk Kwon committed Jun 26, 2026 at 14:09 UTC 65e655d2959111d508ad97515c85be0627a7b916
5 files changed +1170
vllm/models/deepseek_v32/__init__.py new
+22
@@ -0,0 +1,22 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""DeepSeek V3.2 (``deepseek_v32``) model — hardware-isolated entry point.
4 +
5 +DeepSeek V3.2 introduced the DeepSeek Sparse Attention (DSA) architecture:
6 +MLA + a "lightning indexer" that selects the top-k tokens for a sparse MLA
7 +attend. The same model code serves any DSA checkpoint, including GLM-5.2
8 +(``glm_moe_dsa``), which reuses this architecture.
9 +"""
10 +
11 +from vllm.platforms import current_platform
12 +
13 +if current_platform.is_rocm() or current_platform.is_xpu():
14 + raise NotImplementedError("deepseek_v32 currently supports NVIDIA SM100 only.")
15 +
16 +from .nvidia.model import DeepseekV32ForCausalLM
17 +from .nvidia.mtp import DeepseekV32MTP
18 +
19 +__all__ = [
20 + "DeepseekV32ForCausalLM",
21 + "DeepseekV32MTP",
22 +]
vllm/models/deepseek_v32/nvidia/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/models/deepseek_v32/nvidia/attention.py new
+423
@@ -0,0 +1,423 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from typing import TYPE_CHECKING
4 +
5 +import torch
6 +import torch.nn as nn
7 +from transformers import DeepseekV2Config, DeepseekV3Config
8 +
9 +from vllm.compilation.breakable_cudagraph import eager_break_during_capture
10 +from vllm.config import CacheConfig, VllmConfig
11 +from vllm.distributed import get_tensor_model_parallel_world_size
12 +from vllm.forward_context import get_forward_context
13 +from vllm.model_executor.layers.attention import MLAAttention
14 +from vllm.model_executor.layers.layernorm import LayerNorm, RMSNorm
15 +from vllm.model_executor.layers.linear import (
16 + ColumnParallelLinear,
17 + MergedColumnParallelLinear,
18 + ReplicatedLinear,
19 + RowParallelLinear,
20 +)
21 +from vllm.model_executor.layers.quantization import QuantizationConfig
22 +from vllm.model_executor.layers.quantization.utils.fp8_utils import (
23 + per_token_group_quant_fp8,
24 +)
25 +from vllm.model_executor.layers.rotary_embedding import get_rope
26 +from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
27 +from vllm.model_executor.models.deepseek_v2 import (
28 + DeepSeekV2FusedQkvAProjLinear,
29 + DeepseekV32IndexerCache,
30 + yarn_get_mscale,
31 +)
32 +from vllm.model_executor.models.utils import extract_layer_index
33 +from vllm.utils.torch_utils import is_quantized_kv_cache
34 +
35 +if TYPE_CHECKING:
36 + from vllm.model_executor.layers.attention.mla_attention import (
37 + MLACommonMetadata,
38 + )
39 +
40 +
41 +class DeepseekV32Indexer(nn.Module):
42 + def __init__(
43 + self,
44 + vllm_config: VllmConfig,
45 + config: DeepseekV2Config | DeepseekV3Config,
46 + hidden_size: int,
47 + q_lora_rank: int,
48 + quant_config: QuantizationConfig | None,
49 + cache_config: CacheConfig | None,
50 + topk_indices_buffer: torch.Tensor | None,
51 + prefix: str = "",
52 + ):
53 + super().__init__()
54 + self.topk_tokens = config.index_topk
55 + self.n_head = config.index_n_heads
56 + self.head_dim = config.index_head_dim
57 + self.rope_dim = config.qk_rope_head_dim
58 + self.q_lora_rank = q_lora_rank
59 +
60 + # No tensor parallel, just replicated.
61 + self.wq_b = ReplicatedLinear(
62 + self.q_lora_rank,
63 + self.head_dim * self.n_head,
64 + bias=False,
65 + quant_config=quant_config,
66 + prefix=f"{prefix}.wq_b",
67 + )
68 + # Fused wk + weights_proj: single GEMM producing [head_dim + n_head].
69 + # FP8 wk weights are upcasted to BF16 during loading to keep this fused.
70 + self.wk_weights_proj = MergedColumnParallelLinear(
71 + hidden_size,
72 + [self.head_dim, self.n_head],
73 + bias=False,
74 + quant_config=None,
75 + disable_tp=True,
76 + prefix=f"{prefix}.wk_weights_proj",
77 + )
78 + self.k_norm = LayerNorm(self.head_dim, eps=1e-6)
79 + self.softmax_scale = self.head_dim**-0.5
80 +
81 + self.scale_fmt = "ue8m0"
82 + self.quant_block_size = 128
83 + self.topk_indices_buffer = topk_indices_buffer
84 +
85 + # fp8 naive cache: value in fp8 + fp32 scale per quant_block_size element.
86 + assert cache_config is not None, "DeepSeek V3.2 indexer requires cache_config"
87 + self.k_cache = DeepseekV32IndexerCache(
88 + head_dim=self.head_dim + self.head_dim // self.quant_block_size * 4,
89 + dtype=torch.uint8,
90 + prefix=f"{prefix}.k_cache",
91 + cache_config=cache_config,
92 + )
93 + self.max_model_len = vllm_config.model_config.max_model_len
94 + self.prefix = prefix
95 +
96 + from vllm.v1.attention.backends.mla.indexer import (
97 + get_max_prefill_buffer_size,
98 + )
99 +
100 + self.max_total_seq_len = get_max_prefill_buffer_size(vllm_config)
101 + self.indexer_op = SparseAttnIndexer(
102 + self.k_cache,
103 + self.quant_block_size,
104 + self.scale_fmt,
105 + self.topk_tokens,
106 + self.head_dim,
107 + self.max_model_len,
108 + self.max_total_seq_len,
109 + self.topk_indices_buffer,
110 + )
111 +
112 + def forward(
113 + self,
114 + hidden_states: torch.Tensor,
115 + qr: torch.Tensor,
116 + positions: torch.Tensor,
117 + rotary_emb: nn.Module,
118 + ) -> torch.Tensor:
119 + q, _ = self.wq_b(qr)
120 + q = q.view(-1, self.n_head, self.head_dim)
121 +
122 + q_pe, q_nope = torch.split(
123 + q, [self.rope_dim, self.head_dim - self.rope_dim], dim=-1
124 + )
125 + # Fused wk + weights_proj: one GEMM, then split.
126 + kw, _ = self.wk_weights_proj(hidden_states)
127 + k = kw[:, : self.head_dim]
128 + weights = kw[:, self.head_dim :]
129 +
130 + k = self.k_norm(k)
131 + k_pe, k_nope = torch.split(
132 + k, [self.rope_dim, self.head_dim - self.rope_dim], dim=-1
133 + )
134 +
135 + q_pe, k_pe = rotary_emb(positions, q_pe, k_pe.unsqueeze(1))
136 + # RoPE (NeoX) can introduce extra leading dims; reshape back to flat.
137 + q_pe = q_pe.reshape(-1, self.n_head, self.rope_dim)
138 + k_pe = k_pe.reshape(-1, 1, self.rope_dim)
139 +
140 + q = torch.cat([q_pe, q_nope], dim=-1)
141 + k = torch.cat([k_pe.squeeze(-2), k_nope], dim=-1)
142 +
143 + # Only quant q here; k quant is fused with cache insertion.
144 + q = q.view(-1, self.head_dim)
145 + q_fp8, q_scale = per_token_group_quant_fp8(
146 + q,
147 + self.quant_block_size,
148 + column_major_scales=False,
149 + use_ue8m0=self.scale_fmt is not None,
150 + )
151 + q_fp8 = q_fp8.view(-1, self.n_head, self.head_dim)
152 + q_scale = q_scale.view(-1, self.n_head, 1)
153 +
154 + weights = (
155 + weights.unsqueeze(-1) * q_scale * self.softmax_scale * self.n_head**-0.5
156 + )
157 + weights = weights.squeeze(-1)
158 +
159 + return self.indexer_op(hidden_states, q_fp8, k, weights)
160 +
161 +
162 +class DeepseekV32Attention(MLAAttention):
163 + def __init__(
164 + self,
165 + vllm_config: VllmConfig,
166 + config: DeepseekV2Config | DeepseekV3Config,
167 + prefix: str,
168 + topk_indices_buffer: torch.Tensor | None = None,
169 + ) -> None:
170 + quant_config = vllm_config.quant_config
171 + cache_config = vllm_config.cache_config
172 +
173 + hidden_size = config.hidden_size
174 + qk_nope_head_dim = config.qk_nope_head_dim
175 + qk_rope_head_dim = config.qk_rope_head_dim
176 + v_head_dim = config.v_head_dim
177 + q_lora_rank = config.q_lora_rank
178 + kv_lora_rank = config.kv_lora_rank
179 + num_heads = config.num_attention_heads
180 +
181 + tp_size = get_tensor_model_parallel_world_size()
182 + assert num_heads % tp_size == 0
183 + num_local_heads = num_heads // tp_size
184 + qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
185 + scaling = qk_head_dim**-0.5
186 + max_position_embeddings = getattr(config, "max_position_embeddings", 8192)
187 +
188 + # DSA checkpoints may use plain ("default") or yarn-scaled RoPE.
189 + if config.rope_parameters["rope_type"] != "default":
190 + config.rope_parameters["rope_type"] = (
191 + "deepseek_yarn"
192 + if config.rope_parameters.get("apply_yarn_scaling", True)
193 + else "deepseek_llama_scaling"
194 + )
195 + if config.rope_parameters["rope_type"] == "deepseek_yarn":
196 + mscale_all_dim = config.rope_parameters.get("mscale_all_dim", False)
197 + scaling_factor = config.rope_parameters["factor"]
198 + mscale = yarn_get_mscale(scaling_factor, float(mscale_all_dim))
199 + scaling = scaling * mscale * mscale
200 +
201 + # DSA "shared indexer" pattern: only some layers carry an indexer; the
202 + # rest reuse the top-k written by the previous indexer layer into the
203 + # shared topk_indices_buffer. DeepSeek-V3.2 builds it on every layer
204 + # (index_topk_freq defaults to 1); GLM-5.2 uses index_topk_freq=4 so
205 + # only layers [0,1,2,6,10,...] (+ MTP) carry one.
206 + layer_id = extract_layer_index(prefix)
207 + index_topk_freq = getattr(config, "index_topk_freq", 1)
208 + index_topk_pattern = getattr(config, "index_topk_pattern", None)
209 + index_skip_topk_offset = getattr(config, "index_skip_topk_offset", 2)
210 + if index_topk_pattern is None:
211 + skip_topk = (
212 + max(layer_id - index_skip_topk_offset + 1, 0) % index_topk_freq != 0
213 + )
214 + elif 0 <= layer_id < len(index_topk_pattern):
215 + skip_topk = index_topk_pattern[layer_id] == "S"
216 + else:
217 + skip_topk = False
218 + # MTP/nextn layers always build a full indexer (they toggle at runtime).
219 + num_hidden_layers = getattr(config, "num_hidden_layers", None)
220 + is_mtp_layer = num_hidden_layers is not None and layer_id >= num_hidden_layers
221 +
222 + # Build kv_b_proj + indexer first; they are passed to MLAAttention.__init__
223 + # (which runs nn.Module.__init__ and registers them).
224 + kv_b_proj = ColumnParallelLinear(
225 + kv_lora_rank,
226 + num_heads * (qk_nope_head_dim + v_head_dim),
227 + bias=False,
228 + quant_config=quant_config,
229 + prefix=f"{prefix}.kv_b_proj",
230 + )
231 + indexer = None
232 + if not skip_topk or is_mtp_layer:
233 + indexer = DeepseekV32Indexer(
234 + vllm_config,
235 + config,
236 + hidden_size,
237 + q_lora_rank,
238 + quant_config,
239 + cache_config,
240 + topk_indices_buffer,
241 + prefix=f"{prefix}.indexer",
242 + )
243 +
244 + # Set up the MLA engine (impl, KV cache, scales, backend, registration,
245 + # and process_weights_after_loading) via the MLAAttention base.
246 + super().__init__(
247 + num_heads=num_local_heads,
248 + scale=scaling,
249 + qk_nope_head_dim=qk_nope_head_dim,
250 + qk_rope_head_dim=qk_rope_head_dim,
251 + v_head_dim=v_head_dim,
252 + q_lora_rank=q_lora_rank,
253 + kv_lora_rank=kv_lora_rank,
254 + kv_b_proj=kv_b_proj,
255 + cache_config=cache_config,
256 + quant_config=quant_config,
257 + prefix=f"{prefix}.attn",
258 + use_sparse=True,
259 + indexer=indexer,
260 + topk_indices_buffer=topk_indices_buffer,
261 + )
262 +
263 + self.num_local_heads = num_local_heads
264 + self.qk_head_dim = qk_head_dim
265 + self.indexer = indexer
266 + # Runtime toggle for index_share_for_mtp_iteration: MTP draft step 0
267 + # computes the top-k, steps 1+ set this True to reuse it.
268 + self.skip_topk = False
269 + # Whether the paged KV cache must be viewed as fp8 before the attention
270 + # (per-tensor fp8; the fp8_ds_mla layout is read as uint8).
271 + self._fp8_kv_needs_view = (
272 + is_quantized_kv_cache(self.kv_cache_dtype)
273 + and self.kv_cache_dtype != "fp8_ds_mla"
274 + )
275 + # Whether the backend takes an fp8-quantized query (FlashInfer sparse)
276 + # vs the (ql_nope, q_pe) tuple (FlashMLA sparse).
277 + self._use_concat_quant = (
278 + is_quantized_kv_cache(self.kv_cache_dtype)
279 + and self.impl.supports_quant_query_input
280 + )
281 +
282 + # Remaining MLA projections (registered on this module).
283 + self.fused_qkv_a_proj = DeepSeekV2FusedQkvAProjLinear(
284 + hidden_size,
285 + [q_lora_rank, kv_lora_rank + qk_rope_head_dim],
286 + quant_config=quant_config,
287 + prefix=f"{prefix}.fused_qkv_a_proj",
288 + )
289 + self.q_a_layernorm = RMSNorm(q_lora_rank, eps=config.rms_norm_eps)
290 + self.q_b_proj = ColumnParallelLinear(
291 + q_lora_rank,
292 + num_heads * qk_head_dim,
293 + bias=False,
294 + quant_config=quant_config,
295 + prefix=f"{prefix}.q_b_proj",
296 + )
297 + self.kv_a_layernorm = RMSNorm(kv_lora_rank, eps=config.rms_norm_eps)
298 + self.o_proj = RowParallelLinear(
299 + num_heads * v_head_dim,
300 + hidden_size,
301 + bias=False,
302 + quant_config=quant_config,
303 + prefix=f"{prefix}.o_proj",
304 + )
305 +
306 + self.rotary_emb = get_rope(
307 + qk_rope_head_dim,
308 + max_position=max_position_embeddings,
309 + rope_parameters=config.rope_parameters,
310 + is_neox_style=False,
311 + )
312 + # Lightning indexer uses its own RoPE; interleave maps to non-NeoX.
313 + self.indexer_rope_emb = get_rope(
314 + qk_rope_head_dim,
315 + max_position=max_position_embeddings,
316 + rope_parameters=config.rope_parameters,
317 + is_neox_style=not getattr(config, "indexer_rope_interleave", False),
318 + )
319 +
320 + def forward( # type: ignore[override]
321 + self,
322 + positions: torch.Tensor,
323 + hidden_states: torch.Tensor,
324 + ) -> torch.Tensor:
325 + qkv_lora = self.fused_qkv_a_proj(hidden_states)[0]
326 + q_c, kv_lora = qkv_lora.split(
327 + [self.q_lora_rank, self.kv_lora_rank + self.qk_rope_head_dim], dim=-1
328 + )
329 + q_c = self.q_a_layernorm(q_c)
330 + q = self.q_b_proj(q_c)[0]
331 +
332 + kv_c, k_pe = kv_lora.split([self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
333 + kv_c_normed = self.kv_a_layernorm(kv_c)
334 +
335 + q = q.view(-1, self.num_local_heads, self.qk_head_dim)
336 + k_pe = k_pe.unsqueeze(1)
337 + q[..., self.qk_nope_head_dim :], k_pe = self.rotary_emb(
338 + positions, q[..., self.qk_nope_head_dim :], k_pe
339 + )
340 +
341 + num_tokens = hidden_states.shape[0]
342 + q_nope, q_pe = q.split([self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
343 + q_nope = q_nope.transpose(0, 1) # (N, B, P)
344 + ql_nope = torch.bmm(q_nope, self.W_UK_T).transpose(0, 1) # (B, N, L)
345 +
346 + # Lightning indexer writes the top-k indices into the shared buffer.
347 + # "Shared" layers (indexer is None) reuse the top-k from the previous
348 + # indexer layer already sitting in the buffer.
349 + if self.indexer is not None and not self.skip_topk:
350 + self.indexer(hidden_states, q_c, positions, self.indexer_rope_emb) # type: ignore[operator]
351 +
352 + attn_latent = torch.empty(
353 + (num_tokens, self.num_local_heads, self.kv_lora_rank),
354 + dtype=q.dtype,
355 + device=q.device,
356 + )
357 + self._sparse_attention(kv_c_normed, k_pe, ql_nope, q_pe, attn_latent)
358 +
359 + # V up-projection + output projection are metadata-independent GEMMs and
360 + # stay captured.
361 + output = torch.empty(
362 + (num_tokens, self.num_local_heads * self.v_head_dim),
363 + dtype=q.dtype,
364 + device=q.device,
365 + )
366 + self._v_up_proj(attn_latent, out=output)
367 + return self.o_proj(output)[0]
368 +
369 + @eager_break_during_capture
370 + def _sparse_attention(
371 + self,
372 + kv_c_normed: torch.Tensor,
373 + k_pe: torch.Tensor,
374 + ql_nope: torch.Tensor,
375 + q_pe: torch.Tensor,
376 + attn_latent: torch.Tensor,
377 + ) -> None:
378 + forward_context = get_forward_context()
379 + attn_metadata_raw = forward_context.attn_metadata
380 + attn_metadata: MLACommonMetadata | None
381 + if isinstance(attn_metadata_raw, dict):
382 + attn_metadata = attn_metadata_raw[self.layer_name] # type: ignore[assignment]
383 + elif isinstance(attn_metadata_raw, list):
384 + # Speculative decoding: [0] is the base-model metadata dict.
385 + attn_metadata = attn_metadata_raw[0][self.layer_name] # type: ignore[assignment]
386 + else:
387 + attn_metadata = attn_metadata_raw
388 +
389 + slot_mapping = forward_context.slot_mapping
390 + assert isinstance(slot_mapping, dict)
391 + self.impl.do_kv_cache_update( # type: ignore[attr-defined]
392 + kv_c_normed,
393 + k_pe,
394 + self.kv_cache,
395 + slot_mapping.get(self.layer_name),
396 + self.kv_cache_dtype,
397 + self._k_scale,
398 + )
399 +
400 + if attn_metadata is None:
401 + # Profile / warmup: zero-fill for DP+EP determinism.
402 + attn_latent.zero_()
403 + return
404 +
405 + num_actual = attn_metadata.num_actual_tokens
406 + kv_cache = self.kv_cache
407 + if self._fp8_kv_needs_view:
408 + kv_cache = kv_cache.view(torch.float8_e4m3fn)
409 +
410 + ql_nope = ql_nope[:num_actual]
411 + q_pe = q_pe[:num_actual]
412 + # FlashInfer sparse takes a single fp8-quantized query; FlashMLA sparse
413 + # takes the (ql_nope, q_pe) tuple and concatenates internally.
414 + mqa_q: torch.Tensor | tuple[torch.Tensor, torch.Tensor]
415 + if self._use_concat_quant:
416 + mqa_q = self._decode_concat_quant_fp8_op(ql_nope, q_pe, self._q_scale)
417 + else:
418 + mqa_q = (ql_nope, q_pe)
419 +
420 + attn_out, _ = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self) # type: ignore[attr-defined]
421 + attn_latent[:num_actual] = attn_out.view(
422 + num_actual, self.num_local_heads, self.kv_lora_rank
423 + )
vllm/models/deepseek_v32/nvidia/model.py new
+333
@@ -0,0 +1,333 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import typing
4 +from collections.abc import Callable, Iterable
5 +from itertools import islice
6 +
7 +import torch
8 +
9 +from vllm.config import VllmConfig
10 +from vllm.distributed import get_pp_group
11 +from vllm.model_executor.layers.fused_moe import (
12 + fused_moe_make_expert_params_mapping,
13 +)
14 +from vllm.model_executor.layers.layernorm import RMSNorm
15 +from vllm.model_executor.layers.vocab_parallel_embedding import (
16 + VocabParallelEmbedding,
17 +)
18 +from vllm.model_executor.model_loader.weight_utils import (
19 + default_weight_loader,
20 + maybe_remap_kv_scale_name,
21 +)
22 +from vllm.model_executor.models.deepseek_v2 import (
23 + DeepseekV2ForCausalLM,
24 + DeepseekV2MLP,
25 + DeepseekV2MoE,
26 + _try_load_fp8_indexer_wk,
27 + get_spec_layer_idx_from_weight_name,
28 +)
29 +from vllm.model_executor.models.utils import (
30 + PPMissingLayer,
31 + get_pp_missing_layer_names,
32 + is_pp_missing_parameter,
33 + make_empty_intermediate_tensors_factory,
34 + make_layers,
35 +)
36 +from vllm.sequence import IntermediateTensors
37 +
38 +from .attention import DeepseekV32Attention
39 +
40 +
41 +class DeepseekV32DecoderLayer(torch.nn.Module):
42 + def __init__(
43 + self,
44 + vllm_config: VllmConfig,
45 + prefix: str,
46 + config=None,
47 + topk_indices_buffer: torch.Tensor | None = None,
48 + ) -> None:
49 + super().__init__()
50 +
51 + if config is None:
52 + config = vllm_config.model_config.hf_config
53 + quant_config = vllm_config.quant_config
54 + parallel_config = vllm_config.parallel_config
55 +
56 + self.hidden_size = config.hidden_size
57 + moe_layer_freq = getattr(config, "moe_layer_freq", 1)
58 + layer_idx = int(prefix.split(sep=".")[-1])
59 + self.layer_idx = layer_idx
60 + self.use_mha = False
61 +
62 + self.self_attn = DeepseekV32Attention(
63 + vllm_config=vllm_config,
64 + config=config,
65 + prefix=f"{prefix}.self_attn",
66 + topk_indices_buffer=topk_indices_buffer,
67 + )
68 +
69 + if (
70 + config.n_routed_experts is not None
71 + and layer_idx >= config.first_k_dense_replace
72 + and layer_idx % moe_layer_freq == 0
73 + ):
74 + self.mlp = DeepseekV2MoE(
75 + config=config,
76 + parallel_config=parallel_config,
77 + quant_config=quant_config,
78 + prefix=f"{prefix}.mlp",
79 + )
80 + else:
81 + self.mlp = DeepseekV2MLP(
82 + hidden_size=config.hidden_size,
83 + intermediate_size=config.intermediate_size,
84 + hidden_act=config.hidden_act,
85 + quant_config=quant_config,
86 + prefix=f"{prefix}.mlp",
87 + )
88 + self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
89 + self.post_attention_layernorm = RMSNorm(
90 + config.hidden_size, eps=config.rms_norm_eps
91 + )
92 + self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
93 +
94 + def forward(
95 + self,
96 + positions: torch.Tensor,
97 + hidden_states: torch.Tensor,
98 + residual: torch.Tensor | None,
99 + ) -> tuple[torch.Tensor, torch.Tensor]:
100 + if residual is None:
101 + residual = hidden_states
102 + hidden_states = self.input_layernorm(hidden_states)
103 + else:
104 + hidden_states, residual = self.input_layernorm(hidden_states, residual)
105 + hidden_states = self.self_attn(positions=positions, hidden_states=hidden_states)
106 + hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
107 + hidden_states = self.mlp(hidden_states)
108 + return hidden_states, residual
109 +
110 +
111 +class DeepseekV32Model(torch.nn.Module):
112 + fall_back_to_pt_during_load = False
113 +
114 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
115 + super().__init__()
116 +
117 + config = vllm_config.model_config.hf_config
118 + quant_config = vllm_config.quant_config
119 + self.config = config
120 + from vllm.platforms import current_platform
121 +
122 + self.device = current_platform.device_type
123 +
124 + self.vocab_size = config.vocab_size
125 + # DSA is always sparse (has index_topk); allocate the shared top-k
126 + # buffer the indexer writes and the sparse MLA backend reads.
127 + self.is_v32 = True
128 + topk_indices_buffer = torch.empty(
129 + vllm_config.scheduler_config.max_num_batched_tokens,
130 + config.index_topk,
131 + dtype=torch.int32,
132 + device=self.device,
133 + )
134 +
135 + if get_pp_group().is_first_rank:
136 + self.embed_tokens = VocabParallelEmbedding(
137 + config.vocab_size,
138 + config.hidden_size,
139 + quant_config=quant_config,
140 + prefix=f"{prefix}.embed_tokens",
141 + )
142 + else:
143 + self.embed_tokens = PPMissingLayer()
144 +
145 + self.start_layer, self.end_layer, self.layers = make_layers(
146 + config.num_hidden_layers,
147 + lambda prefix: DeepseekV32DecoderLayer(
148 + vllm_config=vllm_config,
149 + prefix=prefix,
150 + topk_indices_buffer=topk_indices_buffer,
151 + ),
152 + prefix=f"{prefix}.layers",
153 + )
154 +
155 + if get_pp_group().is_last_rank:
156 + self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
157 + else:
158 + self.norm = PPMissingLayer()
159 + self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
160 + ["hidden_states", "residual"], config.hidden_size
161 + )
162 +
163 + self.aux_hidden_state_layers = tuple[int, ...]()
164 + self.num_redundant_experts = (
165 + vllm_config.parallel_config.eplb_config.num_redundant_experts
166 + )
167 +
168 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
169 + return self.embed_tokens(input_ids)
170 +
171 + def forward(
172 + self,
173 + input_ids: torch.Tensor | None,
174 + positions: torch.Tensor,
175 + intermediate_tensors: IntermediateTensors | None = None,
176 + inputs_embeds: torch.Tensor | None = None,
177 + ) -> torch.Tensor | IntermediateTensors:
178 + if get_pp_group().is_first_rank:
179 + if inputs_embeds is not None:
180 + hidden_states = inputs_embeds
181 + else:
182 + assert input_ids is not None
183 + hidden_states = self.embed_input_ids(input_ids)
184 + residual = None
185 + else:
186 + assert intermediate_tensors is not None
187 + hidden_states = intermediate_tensors["hidden_states"]
188 + residual = intermediate_tensors["residual"]
189 +
190 + aux_hidden_states = []
191 + for idx, layer in enumerate(
192 + islice(self.layers, self.start_layer, self.end_layer),
193 + start=self.start_layer,
194 + ):
195 + if idx in self.aux_hidden_state_layers:
196 + aux_hidden_states.append(hidden_states + residual)
197 + hidden_states, residual = layer(positions, hidden_states, residual)
198 +
199 + if not get_pp_group().is_last_rank:
200 + return IntermediateTensors(
201 + {"hidden_states": hidden_states, "residual": residual}
202 + )
203 +
204 + hidden_states, _ = self.norm(hidden_states, residual)
205 + if len(aux_hidden_states) > 0:
206 + return hidden_states, aux_hidden_states
207 + return hidden_states
208 +
209 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
210 + # DSA-only: MLA (fused_qkv_a_proj) + the fused indexer wk/weights_proj +
211 + # routed experts. No MHA (qkv_proj) or ROCm shared-expert-fusion paths.
212 + stacked_params_mapping = [
213 + ("gate_up_proj", "gate_proj", 0),
214 + ("gate_up_proj", "up_proj", 1),
215 + ("fused_qkv_a_proj", "q_a_proj", 0),
216 + ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
217 + ("wk_weights_proj", "wk", 0),
218 + ("wk_weights_proj", "weights_proj", 1),
219 + ]
220 + expert_params_mapping = fused_moe_make_expert_params_mapping(
221 + self,
222 + ckpt_gate_proj_name="gate_proj",
223 + ckpt_down_proj_name="down_proj",
224 + ckpt_up_proj_name="up_proj",
225 + num_experts=self.config.n_routed_experts,
226 + num_redundant_experts=self.num_redundant_experts,
227 + )
228 +
229 + pp_missing_layer_names = get_pp_missing_layer_names(self)
230 + params_dict = dict(self.named_parameters())
231 + loaded_params: set[str] = set()
232 + _pending_wk_fp8: dict = {}
233 + for name, loaded_weight in weights:
234 + if "rotary_emb.inv_freq" in name:
235 + continue
236 + # MTP / nextn layers are loaded by the MTP model, not here.
237 + if get_spec_layer_idx_from_weight_name(self.config, name) is not None:
238 + continue
239 + if _try_load_fp8_indexer_wk(
240 + name,
241 + loaded_weight,
242 + _pending_wk_fp8,
243 + params_dict,
244 + loaded_params,
245 + pp_missing_layer_names,
246 + ):
247 + continue
248 +
249 + for param_name, weight_name, shard_id in stacked_params_mapping:
250 + if weight_name not in name:
251 + continue
252 + # Experts are handled below; skip here before the name rewrite.
253 + if ("mlp.experts." in name) and name not in params_dict:
254 + continue
255 + name_mapped = name.replace(weight_name, param_name)
256 + if (
257 + param_name == "fused_qkv_a_proj"
258 + ) and name_mapped not in params_dict:
259 + continue
260 + name = name_mapped
261 + if name.endswith(".bias") and name not in params_dict:
262 + continue
263 + if is_pp_missing_parameter(name, self):
264 + continue
265 + param = params_dict[name]
266 + param.weight_loader(param, loaded_weight, shard_id)
267 + break
268 + else:
269 + is_expert_weight = False
270 + for mapping in expert_params_mapping:
271 + param_name, weight_name, expert_id, shard_id = mapping # type: ignore[assignment]
272 + if weight_name not in name:
273 + continue
274 + is_expert_weight = True
275 + name_mapped = name.replace(weight_name, param_name)
276 + if is_pp_missing_parameter(name_mapped, self):
277 + continue
278 + param = params_dict[name_mapped]
279 + weight_loader = typing.cast(
280 + Callable[..., bool], param.weight_loader
281 + )
282 + success = weight_loader(
283 + param,
284 + loaded_weight,
285 + name_mapped,
286 + shard_id=shard_id,
287 + expert_id=expert_id,
288 + return_success=True,
289 + )
290 + if success:
291 + name = name_mapped
292 + break
293 + else:
294 + if is_expert_weight:
295 + continue
296 + if name.endswith(".bias") and name not in params_dict:
297 + continue
298 + name = maybe_remap_kv_scale_name(name, params_dict) # type: ignore[assignment]
299 + if name is None:
300 + continue
301 + if is_pp_missing_parameter(name, self):
302 + continue
303 + param = params_dict[name]
304 + loader = getattr(param, "weight_loader", default_weight_loader)
305 + loader(param, loaded_weight)
306 + loaded_params.add(name)
307 + return loaded_params
308 +
309 +
310 +class DeepseekV32ForCausalLM(DeepseekV2ForCausalLM):
311 + """DSA causal LM — DeepSeek V2/V3 orchestration with the DSA backbone.
312 +
313 + Serves DeepSeek V3.2 and any architecture reusing DSA (e.g. GLM-5.2).
314 + """
315 +
316 + model_cls = DeepseekV32Model
317 +
318 + def set_moe_parameters(self):
319 + # Same as the base, but keyed on the MoE block type rather than the
320 + # decoder-layer type (DeepseekV32DecoderLayer is a plain nn.Module).
321 + self.expert_weights = []
322 + self.num_expert_groups = getattr(self.config, "n_group", 1)
323 + self.moe_layers = []
324 + self.moe_mlp_layers = []
325 + example_moe = None
326 + for layer in self.model.layers:
327 + if isinstance(layer, PPMissingLayer):
328 + continue
329 + if isinstance(layer.mlp, DeepseekV2MoE):
330 + example_moe = layer.mlp
331 + self.moe_mlp_layers.append(layer.mlp)
332 + self.moe_layers.append(layer.mlp.experts)
333 + self.extract_moe_parameters(example_moe)
vllm/models/deepseek_v32/nvidia/mtp.py new
+390
@@ -0,0 +1,390 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import typing
5 +from collections.abc import Callable, Iterable
6 +
7 +import torch
8 +import torch.nn as nn
9 +
10 +from vllm._aiter_ops import rocm_aiter_ops
11 +from vllm.config import VllmConfig
12 +from vllm.model_executor.layers.fused_moe import (
13 + fused_moe_make_expert_params_mapping,
14 +)
15 +from vllm.model_executor.layers.layernorm import RMSNorm
16 +from vllm.model_executor.layers.logits_processor import LogitsProcessor
17 +from vllm.model_executor.layers.vocab_parallel_embedding import (
18 + VocabParallelEmbedding,
19 +)
20 +from vllm.model_executor.model_loader.weight_utils import (
21 + default_weight_loader,
22 + maybe_remap_kv_scale_name,
23 +)
24 +from vllm.model_executor.models.deepseek_mtp import SharedHead
25 +from vllm.model_executor.models.deepseek_v2 import (
26 + DeepseekV2MixtureOfExperts,
27 + DeepseekV2MoE,
28 + _try_load_fp8_indexer_wk,
29 + get_spec_layer_idx_from_weight_name,
30 +)
31 +from vllm.model_executor.models.utils import (
32 + get_pp_missing_layer_names,
33 + maybe_prefix,
34 +)
35 +from vllm.platforms import current_platform
36 +from vllm.sequence import IntermediateTensors
37 +
38 +from .model import DeepseekV32DecoderLayer
39 +
40 +
41 +class DeepseekV32MultiTokenPredictorLayer(nn.Module):
42 + def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
43 + super().__init__()
44 + assert vllm_config.speculative_config is not None
45 + config = vllm_config.speculative_config.draft_model_config.hf_config
46 + self.config = config
47 + quant_config = vllm_config.quant_config
48 +
49 + self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
50 + self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
51 + self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
52 +
53 + topk_indices_buffer = torch.empty(
54 + vllm_config.scheduler_config.max_num_batched_tokens,
55 + config.index_topk,
56 + dtype=torch.int32,
57 + device=current_platform.device_type,
58 + )
59 + self.shared_head = SharedHead(
60 + config=config, prefix=prefix, quant_config=quant_config
61 + )
62 + self.mtp_block = DeepseekV32DecoderLayer(
63 + vllm_config,
64 + prefix,
65 + config=config,
66 + topk_indices_buffer=topk_indices_buffer,
67 + )
68 +
69 + def forward(
70 + self,
71 + input_ids: torch.Tensor,
72 + positions: torch.Tensor,
73 + previous_hidden_states: torch.Tensor,
74 + inputs_embeds: torch.Tensor | None = None,
75 + spec_step_index: int = 0,
76 + ) -> torch.Tensor:
77 + assert inputs_embeds is not None
78 + inputs_embeds = torch.where(positions.unsqueeze(-1) == 0, 0, inputs_embeds)
79 + inputs_embeds = self.enorm(inputs_embeds)
80 + previous_hidden_states = self.hnorm(previous_hidden_states)
81 + hidden_states = self.eh_proj(
82 + torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
83 + )
84 + hidden_states, residual = self.mtp_block(
85 + positions=positions, hidden_states=hidden_states, residual=None
86 + )
87 + # Return the pre-final-norm recycle hidden (re-fed as the next spec
88 + # step's previous_hidden_states); shared_head norm is applied in
89 + # compute_logits. Matches the V2-runner / deepseek_v4 MTP contract.
90 + return residual + hidden_states
91 +
92 +
93 +class DeepseekV32MultiTokenPredictor(nn.Module):
94 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
95 + super().__init__()
96 + config = vllm_config.model_config.hf_config
97 + self.mtp_start_layer_idx = config.num_hidden_layers
98 + self.num_mtp_layers = config.num_nextn_predict_layers
99 + self.layers = torch.nn.ModuleDict(
100 + {
101 + str(idx): DeepseekV32MultiTokenPredictorLayer(
102 + vllm_config, f"{prefix}.layers.{idx}"
103 + )
104 + for idx in range(
105 + self.mtp_start_layer_idx,
106 + self.mtp_start_layer_idx + self.num_mtp_layers,
107 + )
108 + }
109 + )
110 + self.embed_tokens = VocabParallelEmbedding(
111 + config.vocab_size,
112 + config.hidden_size,
113 + prefix=maybe_prefix(prefix, "embed_tokens"),
114 + )
115 + self.logits_processor = LogitsProcessor(config.vocab_size)
116 +
117 + def set_skip_topk(self, skip: bool):
118 + # index_share_for_mtp_iteration: step 0 computes top-k, steps 1+ reuse.
119 + for layer in self.layers.values():
120 + self_attn = getattr(layer.mtp_block, "self_attn", None)
121 + if self_attn is not None and hasattr(self_attn, "skip_topk"):
122 + self_attn.skip_topk = skip
123 +
124 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
125 + return self.embed_tokens(input_ids)
126 +
127 + def forward(
128 + self,
129 + input_ids: torch.Tensor,
130 + positions: torch.Tensor,
131 + previous_hidden_states: torch.Tensor,
132 + inputs_embeds: torch.Tensor | None = None,
133 + spec_step_idx: int = 0,
134 + ) -> torch.Tensor:
135 + if inputs_embeds is None:
136 + inputs_embeds = self.embed_tokens(input_ids)
137 + current_step_idx = spec_step_idx % self.num_mtp_layers
138 + return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
139 + input_ids,
140 + positions,
141 + previous_hidden_states,
142 + inputs_embeds,
143 + current_step_idx,
144 + )
145 +
146 + def compute_logits(
147 + self,
148 + hidden_states: torch.Tensor,
149 + spec_step_idx: int = 0,
150 + ) -> torch.Tensor:
151 + current_step_idx = spec_step_idx % self.num_mtp_layers
152 + mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
153 + return self.logits_processor(
154 + mtp_layer.shared_head.head, mtp_layer.shared_head(hidden_states)
155 + )
156 +
157 +
158 +class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
159 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
160 + super().__init__()
161 + self.config = vllm_config.model_config.hf_config
162 + self.quant_config = vllm_config.quant_config
163 + self.model = DeepseekV32MultiTokenPredictor(
164 + vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
165 + )
166 + self.set_moe_parameters()
167 +
168 + def set_moe_parameters(self):
169 + self.expert_weights = []
170 + self.num_moe_layers = self.config.num_nextn_predict_layers
171 + self.num_expert_groups = self.config.n_group
172 + self.moe_layers = []
173 + self.moe_mlp_layers = []
174 + example_moe = None
175 + for layer in self.model.layers.values():
176 + mlp = layer.mtp_block.mlp
177 + if isinstance(mlp, DeepseekV2MoE):
178 + example_moe = mlp
179 + self.moe_mlp_layers.append(mlp)
180 + self.moe_layers.append(mlp.experts)
181 + self.extract_moe_parameters(example_moe)
182 +
183 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
184 + return self.model.embed_input_ids(input_ids)
185 +
186 + def forward(
187 + self,
188 + input_ids: torch.Tensor | None,
189 + positions: torch.Tensor,
190 + hidden_states: torch.Tensor,
191 + intermediate_tensors: IntermediateTensors | None = None,
192 + inputs_embeds: torch.Tensor | None = None,
193 + spec_step_idx: int = 0,
194 + ) -> torch.Tensor:
195 + return self.model(
196 + input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
197 + )
198 +
199 + def compute_logits(
200 + self,
201 + hidden_states: torch.Tensor,
202 + spec_step_idx: int = 0,
203 + ) -> torch.Tensor | None:
204 + return self.model.compute_logits(hidden_states, spec_step_idx)
205 +
206 + def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
207 + spec_layer_weight_names = [
208 + "embed_tokens",
209 + "enorm",
210 + "hnorm",
211 + "eh_proj",
212 + "shared_head",
213 + ]
214 + shared_weight_names = ["embed_tokens"]
215 + spec_layer_weight = False
216 + shared_weight = False
217 + for weight_name in spec_layer_weight_names:
218 + if weight_name in name:
219 + spec_layer_weight = True
220 + if weight_name in shared_weight_names:
221 + shared_weight = True
222 + break
223 + if not spec_layer_weight:
224 + name = name.replace(
225 + f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
226 + )
227 + elif shared_weight:
228 + name = name.replace(f"model.layers.{spec_layer}.", "model.")
229 + return name
230 +
231 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
232 + rocm_aiter_moe_shared_expert_enabled = (
233 + rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
234 + )
235 + stacked_params_mapping = [
236 + ("gate_up_proj", "gate_proj", 0),
237 + ("gate_up_proj", "up_proj", 1),
238 + ("fused_qkv_a_proj", "q_a_proj", 0),
239 + ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
240 + ("wk_weights_proj", "wk", 0),
241 + ("wk_weights_proj", "weights_proj", 1),
242 + ]
243 + expert_params_mapping = fused_moe_make_expert_params_mapping(
244 + self,
245 + ckpt_gate_proj_name="gate_proj",
246 + ckpt_down_proj_name="down_proj",
247 + ckpt_up_proj_name="up_proj",
248 + num_experts=self.config.n_routed_experts
249 + + (
250 + self.config.n_shared_experts
251 + if rocm_aiter_moe_shared_expert_enabled
252 + else 0
253 + ),
254 + )
255 +
256 + pp_missing_layer_names = get_pp_missing_layer_names(self)
257 + params_dict = dict(self.named_parameters())
258 + loaded_params: set[str] = set()
259 + _pending_wk_fp8: dict = {}
260 + for name, loaded_weight in weights:
261 + if "rotary_emb.inv_freq" in name:
262 + continue
263 + spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
264 + if spec_layer is None:
265 + continue
266 + is_fusion_moe_shared_experts_layer = (
267 + rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
268 + )
269 + name = self._rewrite_spec_layer_name(spec_layer, name)
270 +
271 + if _try_load_fp8_indexer_wk(
272 + name,
273 + loaded_weight,
274 + _pending_wk_fp8,
275 + params_dict,
276 + loaded_params,
277 + pp_missing_layer_names,
278 + ):
279 + continue
280 +
281 + for param_name, weight_name, shard_id in stacked_params_mapping:
282 + if weight_name not in name:
283 + continue
284 + if ("mlp.experts." in name) and name not in params_dict:
285 + continue
286 + if is_fusion_moe_shared_experts_layer:
287 + continue
288 + name_mapped = name.replace(weight_name, param_name)
289 + if (
290 + param_name == "fused_qkv_a_proj"
291 + ) and name_mapped not in params_dict:
292 + continue
293 + else:
294 + name = name_mapped
295 + if name.endswith(".bias") and name not in params_dict:
296 + continue
297 + param = params_dict[name]
298 + weight_loader = param.weight_loader
299 + weight_loader(param, loaded_weight, shard_id)
300 + break
301 + else:
302 + num_chunks = 1
303 + if is_fusion_moe_shared_experts_layer:
304 + num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
305 + split_dim = (
306 + 1
307 + if ("down_proj.weight" in name and loaded_weight.ndim > 1)
308 + else 0
309 + )
310 + total = loaded_weight.shape[split_dim]
311 + assert total % num_chunks == 0
312 + chunk_size = total // num_chunks
313 +
314 + for j in range(num_chunks):
315 + chunk_name = name
316 + weight_to_load = loaded_weight
317 + if is_fusion_moe_shared_experts_layer:
318 + chunk_slice = slice(j * chunk_size, (j + 1) * chunk_size)
319 + if loaded_weight.ndim == 1:
320 + weight_to_load = loaded_weight[chunk_slice]
321 + elif split_dim == 0:
322 + weight_to_load = loaded_weight[chunk_slice, :]
323 + else:
324 + weight_to_load = loaded_weight[:, chunk_slice]
325 + chunk_name = name.replace(
326 + "mlp.shared_experts",
327 + f"mlp.experts.{self.config.n_routed_experts + j}",
328 + )
329 +
330 + is_expert_weight = False
331 + for mapping in expert_params_mapping:
332 + param_name, weight_name, expert_id, shard_id = mapping # type: ignore[assignment]
333 + if weight_name not in chunk_name:
334 + continue
335 + is_expert_weight = True
336 + name_mapped = chunk_name.replace(weight_name, param_name)
337 + param = params_dict[name_mapped]
338 + weight_loader = typing.cast(
339 + Callable[..., bool], param.weight_loader
340 + )
341 + success = weight_loader(
342 + param,
343 + weight_to_load,
344 + name_mapped,
345 + shard_id=shard_id,
346 + expert_id=expert_id,
347 + return_success=True,
348 + )
349 + if success:
350 + if not is_fusion_moe_shared_experts_layer:
351 + name = name_mapped
352 + else:
353 + loaded_params.add(name_mapped)
354 + break
355 + else:
356 + if is_expert_weight:
357 + continue
358 + if name.endswith(".bias") and name not in params_dict:
359 + continue
360 + name = maybe_remap_kv_scale_name(name, params_dict) # type: ignore[assignment]
361 + if name is None:
362 + continue
363 + if (
364 + spec_layer != self.model.mtp_start_layer_idx
365 + and ".layers" not in name
366 + ):
367 + continue
368 + param = params_dict[name]
369 + weight_loader = getattr(
370 + param, "weight_loader", default_weight_loader
371 + )
372 + weight_loader(param, loaded_weight)
373 + if not is_fusion_moe_shared_experts_layer:
374 + loaded_params.add(name)
375 +
376 + loaded_layers: set[int] = set()
377 + for param_name in loaded_params:
378 + spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
379 + if spec_layer is not None:
380 + loaded_layers.add(spec_layer)
381 + for layer_idx in range(
382 + self.model.mtp_start_layer_idx,
383 + self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
384 + ):
385 + if layer_idx not in loaded_layers:
386 + raise ValueError(
387 + f"MTP speculative decoding layer {layer_idx} weights "
388 + f"missing from checkpoint."
389 + )
390 + return loaded_params