[ROCm]Migrating Deepseek V3.2 to vllm/models/deepseek_v32/ (#47207)
Signed-off-by: sroberts-amd <sroberts@amd.com> Signed-off-by: Stacy Roberts <sroberts@amd.com> Co-authored-by: Elvir Crncevic <elvircrn@gmail.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com>
sroberts-amd committed
Jul 30, 2026 at 08:37 UTC
e2efe79695e0f92395c029a6a7907c02a0374678
12 files changed
+1031
-70
tests/kernels/test_fused_deepseek_v32_norm_rope.py
+1
-1
@@ -27,7 +27,7 @@ rtol/atol=1e-2 (the tolerance the sibling deepseek_v4 fused-kernel test uses).
27
import pytest
28
import torch
29
30
-from vllm.models.deepseek_v32.nvidia import kernels as K
30
+from vllm.models.deepseek_v32.common import kernels as K
31
from vllm.platforms import current_platform
32
33
FP8 = torch.float8_e4m3fn
vllm/models/deepseek_v32/__init__.py
+9
-5
@@ -10,11 +10,15 @@ attend. The same model code serves any DSA checkpoint, including GLM-5.2
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
13
+if current_platform.is_rocm():
14
+ from .amd.model import DeepseekV32ForCausalLM
15
+ from .amd.mtp import DeepseekV32MTP
16
+elif current_platform.is_xpu():
17
+ raise NotImplementedError("deepseek_v32 does not yet support XPU.")
18
+else:
19
+ # Covers Blackwell (sm100) and all other CUDA devices.
20
+ from .nvidia.model import DeepseekV32ForCausalLM
21
+ from .nvidia.mtp import DeepseekV32MTP
22
23
__all__ = [
24
"DeepseekV32ForCausalLM",
vllm/models/deepseek_v32/amd/__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/amd/model.py
new
+327
@@ -0,0 +1,327 @@
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
+from itertools import islice
7
+
8
+import torch
9
+
10
+from vllm.config import VllmConfig
11
+from vllm.distributed import get_pp_group
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.vocab_parallel_embedding import (
17
+ VocabParallelEmbedding,
18
+)
19
+from vllm.model_executor.model_loader.weight_utils import (
20
+ default_weight_loader,
21
+ maybe_remap_kv_scale_name,
22
+)
23
+from vllm.model_executor.models.deepseek_v2 import (
24
+ DeepseekV2ForCausalLM,
25
+ DeepseekV2MLP,
26
+ DeepseekV2MoE,
27
+ _try_load_fp8_indexer_wk,
28
+ get_spec_layer_idx_from_weight_name,
29
+)
30
+from vllm.model_executor.models.utils import (
31
+ PPMissingLayer,
32
+ get_pp_missing_layer_names,
33
+ is_pp_missing_parameter,
34
+ make_empty_intermediate_tensors_factory,
35
+ make_layers,
36
+)
37
+from vllm.models.deepseek_v32.common.fused_ops import fused_allreduce_rms_norm
38
+from vllm.sequence import IntermediateTensors
39
+
40
+from .rocm import DeepseekV32MLAAttention
41
+
42
+
43
+class DeepseekV32DecoderLayer(torch.nn.Module):
44
+ def __init__(
45
+ self,
46
+ vllm_config: VllmConfig,
47
+ prefix: str,
48
+ config=None,
49
+ topk_indices_buffer: torch.Tensor | None = None,
50
+ ) -> None:
51
+ super().__init__()
52
+
53
+ if config is None:
54
+ config = vllm_config.model_config.hf_config
55
+ quant_config = vllm_config.quant_config
56
+ parallel_config = vllm_config.parallel_config
57
+
58
+ self.hidden_size = config.hidden_size
59
+ moe_layer_freq = getattr(config, "moe_layer_freq", 1)
60
+ layer_idx = int(prefix.split(sep=".")[-1])
61
+ self.layer_idx = layer_idx
62
+ self.use_mha = False
63
+
64
+ self.self_attn = DeepseekV32MLAAttention(
65
+ vllm_config=vllm_config,
66
+ config=config,
67
+ prefix=f"{prefix}.self_attn",
68
+ topk_indices_buffer=topk_indices_buffer,
69
+ )
70
+
71
+ if (
72
+ config.n_routed_experts is not None
73
+ and layer_idx >= config.first_k_dense_replace
74
+ and layer_idx % moe_layer_freq == 0
75
+ ):
76
+ self.mlp = DeepseekV2MoE(
77
+ config=config,
78
+ parallel_config=parallel_config,
79
+ quant_config=quant_config,
80
+ prefix=f"{prefix}.mlp",
81
+ )
82
+ self.mlp.experts.moe_config.skip_final_all_reduce = True
83
+ else:
84
+ self.mlp = DeepseekV2MLP(
85
+ hidden_size=config.hidden_size,
86
+ intermediate_size=config.intermediate_size,
87
+ hidden_act=config.hidden_act,
88
+ quant_config=quant_config,
89
+ prefix=f"{prefix}.mlp",
90
+ reduce_results=False,
91
+ )
92
+ self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
93
+ self.post_attention_layernorm = RMSNorm(
94
+ config.hidden_size, eps=config.rms_norm_eps
95
+ )
96
+ self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
97
+
98
+ def forward(
99
+ self,
100
+ positions: torch.Tensor,
101
+ hidden_states: torch.Tensor,
102
+ residual: torch.Tensor | None,
103
+ ) -> tuple[torch.Tensor, torch.Tensor]:
104
+ if residual is None:
105
+ residual = hidden_states
106
+ hidden_states = self.input_layernorm(hidden_states)
107
+ else:
108
+ hidden_states, residual = fused_allreduce_rms_norm(
109
+ hidden_states, residual, self.input_layernorm
110
+ )
111
+ hidden_states = self.self_attn(positions=positions, hidden_states=hidden_states)
112
+ hidden_states, residual = fused_allreduce_rms_norm(
113
+ hidden_states, residual, self.post_attention_layernorm
114
+ )
115
+ hidden_states = self.mlp(hidden_states)
116
+ return hidden_states, residual
117
+
118
+
119
+class DeepseekV32Model(torch.nn.Module):
120
+ fall_back_to_pt_during_load = False
121
+
122
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
123
+ super().__init__()
124
+
125
+ config = vllm_config.model_config.hf_config
126
+ quant_config = vllm_config.quant_config
127
+ self.config = config
128
+ from vllm.platforms import current_platform
129
+
130
+ self.device = current_platform.device_type
131
+
132
+ self.vocab_size = config.vocab_size
133
+ self.is_v32 = True
134
+ topk_indices_buffer = torch.empty(
135
+ vllm_config.scheduler_config.max_num_batched_tokens,
136
+ config.index_topk,
137
+ dtype=torch.int32,
138
+ device=self.device,
139
+ )
140
+
141
+ if get_pp_group().is_first_rank:
142
+ self.embed_tokens = VocabParallelEmbedding(
143
+ config.vocab_size,
144
+ config.hidden_size,
145
+ quant_config=quant_config,
146
+ prefix=f"{prefix}.embed_tokens",
147
+ )
148
+ else:
149
+ self.embed_tokens = PPMissingLayer()
150
+
151
+ self.start_layer, self.end_layer, self.layers = make_layers(
152
+ config.num_hidden_layers,
153
+ lambda prefix: DeepseekV32DecoderLayer(
154
+ vllm_config=vllm_config,
155
+ prefix=prefix,
156
+ topk_indices_buffer=topk_indices_buffer,
157
+ ),
158
+ prefix=f"{prefix}.layers",
159
+ )
160
+
161
+ if get_pp_group().is_last_rank:
162
+ self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
163
+ else:
164
+ self.norm = PPMissingLayer()
165
+ self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
166
+ ["hidden_states", "residual"], config.hidden_size
167
+ )
168
+
169
+ self.aux_hidden_state_layers = tuple[int, ...]()
170
+ self.num_redundant_experts = (
171
+ vllm_config.parallel_config.eplb_config.num_redundant_experts
172
+ )
173
+
174
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
175
+ return self.embed_tokens(input_ids)
176
+
177
+ def forward(
178
+ self,
179
+ input_ids: torch.Tensor | None,
180
+ positions: torch.Tensor,
181
+ intermediate_tensors: IntermediateTensors | None = None,
182
+ inputs_embeds: torch.Tensor | None = None,
183
+ ) -> torch.Tensor | IntermediateTensors:
184
+ if get_pp_group().is_first_rank:
185
+ if inputs_embeds is not None:
186
+ hidden_states = inputs_embeds
187
+ else:
188
+ assert input_ids is not None
189
+ hidden_states = self.embed_input_ids(input_ids)
190
+ residual = None
191
+ else:
192
+ assert intermediate_tensors is not None
193
+ hidden_states = intermediate_tensors["hidden_states"]
194
+ residual = intermediate_tensors["residual"]
195
+
196
+ aux_hidden_states = []
197
+ for idx, layer in enumerate(
198
+ islice(self.layers, self.start_layer, self.end_layer),
199
+ start=self.start_layer,
200
+ ):
201
+ if idx in self.aux_hidden_state_layers:
202
+ aux_hidden_states.append(hidden_states + residual)
203
+ hidden_states, residual = layer(positions, hidden_states, residual)
204
+
205
+ if not get_pp_group().is_last_rank:
206
+ return IntermediateTensors(
207
+ {"hidden_states": hidden_states, "residual": residual}
208
+ )
209
+
210
+ hidden_states, _ = fused_allreduce_rms_norm(hidden_states, residual, self.norm)
211
+ if len(aux_hidden_states) > 0:
212
+ return hidden_states, aux_hidden_states
213
+ return hidden_states
214
+
215
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
216
+ stacked_params_mapping = [
217
+ ("gate_up_proj", "gate_proj", 0),
218
+ ("gate_up_proj", "up_proj", 1),
219
+ ("fused_qkv_a_proj", "q_a_proj", 0),
220
+ ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
221
+ ("wk_weights_proj", "wk", 0),
222
+ ("wk_weights_proj", "weights_proj", 1),
223
+ ]
224
+ expert_params_mapping = fused_moe_make_expert_params_mapping(
225
+ self,
226
+ ckpt_gate_proj_name="gate_proj",
227
+ ckpt_down_proj_name="down_proj",
228
+ ckpt_up_proj_name="up_proj",
229
+ num_experts=self.config.n_routed_experts,
230
+ num_redundant_experts=self.num_redundant_experts,
231
+ )
232
+
233
+ pp_missing_layer_names = get_pp_missing_layer_names(self)
234
+ params_dict = dict(self.named_parameters())
235
+ loaded_params: set[str] = set()
236
+ _pending_wk_fp8: dict = {}
237
+ for name, loaded_weight in weights:
238
+ if "rotary_emb.inv_freq" in name:
239
+ continue
240
+ if get_spec_layer_idx_from_weight_name(self.config, name) is not None:
241
+ continue
242
+ if _try_load_fp8_indexer_wk(
243
+ name,
244
+ loaded_weight,
245
+ _pending_wk_fp8,
246
+ params_dict,
247
+ loaded_params,
248
+ pp_missing_layer_names,
249
+ ):
250
+ continue
251
+
252
+ for param_name, weight_name, shard_id in stacked_params_mapping:
253
+ if weight_name not in name:
254
+ continue
255
+ if ("mlp.experts." in name) and name not in params_dict:
256
+ continue
257
+ name_mapped = name.replace(weight_name, param_name)
258
+ if (
259
+ param_name == "fused_qkv_a_proj"
260
+ ) and name_mapped not in params_dict:
261
+ continue
262
+ name = name_mapped
263
+ if name.endswith(".bias") and name not in params_dict:
264
+ continue
265
+ if is_pp_missing_parameter(name, self):
266
+ continue
267
+ param = params_dict[name]
268
+ param.weight_loader(param, loaded_weight, shard_id)
269
+ break
270
+ else:
271
+ is_expert_weight = False
272
+ for mapping in expert_params_mapping:
273
+ param_name, weight_name, expert_id, shard_id = mapping # type: ignore[assignment]
274
+ if weight_name not in name:
275
+ continue
276
+ is_expert_weight = True
277
+ name_mapped = name.replace(weight_name, param_name)
278
+ if is_pp_missing_parameter(name_mapped, self):
279
+ continue
280
+ param = params_dict[name_mapped]
281
+ weight_loader = typing.cast(
282
+ Callable[..., bool], param.weight_loader
283
+ )
284
+ success = weight_loader(
285
+ param,
286
+ loaded_weight,
287
+ name_mapped,
288
+ shard_id=shard_id,
289
+ expert_id=expert_id,
290
+ return_success=True,
291
+ )
292
+ if success:
293
+ name = name_mapped
294
+ break
295
+ else:
296
+ if is_expert_weight:
297
+ continue
298
+ if name.endswith(".bias") and name not in params_dict:
299
+ continue
300
+ name = maybe_remap_kv_scale_name(name, params_dict) # type: ignore[assignment]
301
+ if name is None:
302
+ continue
303
+ if is_pp_missing_parameter(name, self):
304
+ continue
305
+ param = params_dict[name]
306
+ loader = getattr(param, "weight_loader", default_weight_loader)
307
+ loader(param, loaded_weight)
308
+ loaded_params.add(name)
309
+ return loaded_params
310
+
311
+
312
+class DeepseekV32ForCausalLM(DeepseekV2ForCausalLM):
313
+ model_cls = DeepseekV32Model
314
+
315
+ def set_moe_parameters(self):
316
+ self.num_expert_groups = getattr(self.config, "n_group", 1)
317
+ self.moe_layers = []
318
+ self.moe_mlp_layers = []
319
+ example_moe = None
320
+ for layer in self.model.layers:
321
+ if isinstance(layer, PPMissingLayer):
322
+ continue
323
+ if isinstance(layer.mlp, DeepseekV2MoE):
324
+ example_moe = layer.mlp
325
+ self.moe_mlp_layers.append(layer.mlp)
326
+ self.moe_layers.append(layer.mlp.experts)
327
+ self.extract_moe_parameters(example_moe)
vllm/models/deepseek_v32/amd/mtp.py
new
+392
@@ -0,0 +1,392 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+
5
+import typing
6
+from collections.abc import Callable, Iterable
7
+
8
+import torch
9
+import torch.nn as nn
10
+
11
+from vllm._aiter_ops import rocm_aiter_ops
12
+from vllm.config import VllmConfig
13
+from vllm.distributed import tensor_model_parallel_all_reduce
14
+from vllm.model_executor.layers.fused_moe import (
15
+ fused_moe_make_expert_params_mapping,
16
+)
17
+from vllm.model_executor.layers.layernorm import RMSNorm
18
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
19
+from vllm.model_executor.layers.vocab_parallel_embedding import (
20
+ VocabParallelEmbedding,
21
+)
22
+from vllm.model_executor.model_loader.weight_utils import (
23
+ default_weight_loader,
24
+ maybe_remap_kv_scale_name,
25
+)
26
+from vllm.model_executor.models.deepseek_mtp import SharedHead
27
+from vllm.model_executor.models.deepseek_v2 import (
28
+ DeepseekV2MixtureOfExperts,
29
+ DeepseekV2MoE,
30
+ _try_load_fp8_indexer_wk,
31
+ get_spec_layer_idx_from_weight_name,
32
+)
33
+from vllm.model_executor.models.utils import (
34
+ get_pp_missing_layer_names,
35
+ maybe_prefix,
36
+)
37
+from vllm.models.deepseek_v32.common.kernels import fused_eh_norm
38
+from vllm.platforms import current_platform
39
+from vllm.sequence import IntermediateTensors
40
+
41
+from .model import DeepseekV32DecoderLayer
42
+
43
+
44
+class DeepseekV32MultiTokenPredictorLayer(nn.Module):
45
+ def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
46
+ super().__init__()
47
+ assert vllm_config.speculative_config is not None
48
+ config = vllm_config.speculative_config.draft_model_config.hf_config
49
+ self.config = config
50
+ quant_config = vllm_config.quant_config
51
+
52
+ self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
53
+ self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
54
+ self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
55
+
56
+ topk_indices_buffer = torch.empty(
57
+ vllm_config.scheduler_config.max_num_batched_tokens,
58
+ config.index_topk,
59
+ dtype=torch.int32,
60
+ device=current_platform.device_type,
61
+ )
62
+ self.shared_head = SharedHead(
63
+ config=config, prefix=prefix, quant_config=quant_config
64
+ )
65
+ self.mtp_block = DeepseekV32DecoderLayer(
66
+ vllm_config,
67
+ prefix,
68
+ config=config,
69
+ topk_indices_buffer=topk_indices_buffer,
70
+ )
71
+
72
+ def forward(
73
+ self,
74
+ input_ids: torch.Tensor,
75
+ positions: torch.Tensor,
76
+ previous_hidden_states: torch.Tensor,
77
+ inputs_embeds: torch.Tensor | None = None,
78
+ spec_step_index: int = 0,
79
+ ) -> torch.Tensor:
80
+ assert inputs_embeds is not None
81
+ eh_input = fused_eh_norm(
82
+ positions,
83
+ inputs_embeds,
84
+ previous_hidden_states,
85
+ self.enorm.weight,
86
+ self.hnorm.weight,
87
+ self.enorm.variance_epsilon,
88
+ )
89
+ hidden_states = self.eh_proj(eh_input)
90
+ hidden_states, residual = self.mtp_block(
91
+ positions=positions, hidden_states=hidden_states, residual=None
92
+ )
93
+ hidden_states = tensor_model_parallel_all_reduce(hidden_states)
94
+ hidden_states = residual + hidden_states
95
+ hidden_states = self.shared_head.norm(hidden_states)
96
+ return hidden_states, hidden_states
97
+
98
+
99
+class DeepseekV32MultiTokenPredictor(nn.Module):
100
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
101
+ super().__init__()
102
+ config = vllm_config.model_config.hf_config
103
+ self.mtp_start_layer_idx = config.num_hidden_layers
104
+ self.num_mtp_layers = config.num_nextn_predict_layers
105
+ self.layers = torch.nn.ModuleDict(
106
+ {
107
+ str(idx): DeepseekV32MultiTokenPredictorLayer(
108
+ vllm_config, f"{prefix}.layers.{idx}"
109
+ )
110
+ for idx in range(
111
+ self.mtp_start_layer_idx,
112
+ self.mtp_start_layer_idx + self.num_mtp_layers,
113
+ )
114
+ }
115
+ )
116
+ self.embed_tokens = VocabParallelEmbedding(
117
+ config.vocab_size,
118
+ config.hidden_size,
119
+ prefix=maybe_prefix(prefix, "embed_tokens"),
120
+ )
121
+ self.logits_processor = LogitsProcessor(config.vocab_size)
122
+
123
+ def set_skip_topk(self, skip: bool):
124
+ for layer in self.layers.values():
125
+ self_attn = getattr(layer.mtp_block, "self_attn", None)
126
+ if self_attn is not None and hasattr(self_attn, "skip_topk"):
127
+ self_attn.skip_topk = skip
128
+
129
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
130
+ return self.embed_tokens(input_ids)
131
+
132
+ def forward(
133
+ self,
134
+ input_ids: torch.Tensor,
135
+ positions: torch.Tensor,
136
+ previous_hidden_states: torch.Tensor,
137
+ inputs_embeds: torch.Tensor | None = None,
138
+ spec_step_idx: int = 0,
139
+ ) -> torch.Tensor:
140
+ if inputs_embeds is None:
141
+ inputs_embeds = self.embed_tokens(input_ids)
142
+ current_step_idx = spec_step_idx % self.num_mtp_layers
143
+ return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
144
+ input_ids,
145
+ positions,
146
+ previous_hidden_states,
147
+ inputs_embeds,
148
+ current_step_idx,
149
+ )
150
+
151
+ def compute_logits(
152
+ self,
153
+ hidden_states: torch.Tensor,
154
+ spec_step_idx: int = 0,
155
+ ) -> torch.Tensor:
156
+ current_step_idx = spec_step_idx % self.num_mtp_layers
157
+ mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
158
+ return self.logits_processor(mtp_layer.shared_head.head, hidden_states)
159
+
160
+
161
+class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
162
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
163
+ super().__init__()
164
+ self.config = vllm_config.model_config.hf_config
165
+ self.quant_config = vllm_config.quant_config
166
+ self.model = DeepseekV32MultiTokenPredictor(
167
+ vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
168
+ )
169
+ self.set_moe_parameters()
170
+
171
+ def set_moe_parameters(self):
172
+ self.num_moe_layers = self.config.num_nextn_predict_layers
173
+ self.num_expert_groups = self.config.n_group
174
+ self.moe_layers = []
175
+ self.moe_mlp_layers = []
176
+ example_moe = None
177
+ for layer in self.model.layers.values():
178
+ mlp = layer.mtp_block.mlp
179
+ if isinstance(mlp, DeepseekV2MoE):
180
+ example_moe = mlp
181
+ self.moe_mlp_layers.append(mlp)
182
+ self.moe_layers.append(mlp.experts)
183
+ self.extract_moe_parameters(example_moe)
184
+
185
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
186
+ return self.model.embed_input_ids(input_ids)
187
+
188
+ def forward(
189
+ self,
190
+ input_ids: torch.Tensor | None,
191
+ positions: torch.Tensor,
192
+ hidden_states: torch.Tensor,
193
+ intermediate_tensors: IntermediateTensors | None = None,
194
+ inputs_embeds: torch.Tensor | None = None,
195
+ spec_step_idx: int = 0,
196
+ ) -> torch.Tensor:
197
+ return self.model(
198
+ input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
199
+ )
200
+
201
+ def compute_logits(
202
+ self,
203
+ hidden_states: torch.Tensor,
204
+ spec_step_idx: int = 0,
205
+ ) -> torch.Tensor | None:
206
+ return self.model.compute_logits(hidden_states, spec_step_idx)
207
+
208
+ def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
209
+ spec_layer_weight_names = [
210
+ "embed_tokens",
211
+ "enorm",
212
+ "hnorm",
213
+ "eh_proj",
214
+ "shared_head",
215
+ ]
216
+ shared_weight_names = ["embed_tokens"]
217
+ spec_layer_weight = False
218
+ shared_weight = False
219
+ for weight_name in spec_layer_weight_names:
220
+ if weight_name in name:
221
+ spec_layer_weight = True
222
+ if weight_name in shared_weight_names:
223
+ shared_weight = True
224
+ break
225
+ if not spec_layer_weight:
226
+ name = name.replace(
227
+ f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
228
+ )
229
+ elif shared_weight:
230
+ name = name.replace(f"model.layers.{spec_layer}.", "model.")
231
+ return name
232
+
233
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
234
+ rocm_aiter_moe_shared_expert_enabled = (
235
+ rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
236
+ )
237
+ stacked_params_mapping = [
238
+ ("gate_up_proj", "gate_proj", 0),
239
+ ("gate_up_proj", "up_proj", 1),
240
+ ("fused_qkv_a_proj", "q_a_proj", 0),
241
+ ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
242
+ ("wk_weights_proj", "wk", 0),
243
+ ("wk_weights_proj", "weights_proj", 1),
244
+ ]
245
+ expert_params_mapping = fused_moe_make_expert_params_mapping(
246
+ self,
247
+ ckpt_gate_proj_name="gate_proj",
248
+ ckpt_down_proj_name="down_proj",
249
+ ckpt_up_proj_name="up_proj",
250
+ num_experts=self.config.n_routed_experts
251
+ + (
252
+ self.config.n_shared_experts
253
+ if rocm_aiter_moe_shared_expert_enabled
254
+ else 0
255
+ ),
256
+ )
257
+
258
+ pp_missing_layer_names = get_pp_missing_layer_names(self)
259
+ params_dict = dict(self.named_parameters())
260
+ loaded_params: set[str] = set()
261
+ _pending_wk_fp8: dict = {}
262
+ for name, loaded_weight in weights:
263
+ if "rotary_emb.inv_freq" in name:
264
+ continue
265
+ spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
266
+ if spec_layer is None:
267
+ continue
268
+ is_fusion_moe_shared_experts_layer = (
269
+ rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
270
+ )
271
+ name = self._rewrite_spec_layer_name(spec_layer, name)
272
+
273
+ if _try_load_fp8_indexer_wk(
274
+ name,
275
+ loaded_weight,
276
+ _pending_wk_fp8,
277
+ params_dict,
278
+ loaded_params,
279
+ pp_missing_layer_names,
280
+ ):
281
+ continue
282
+
283
+ for param_name, weight_name, shard_id in stacked_params_mapping:
284
+ if weight_name not in name:
285
+ continue
286
+ if ("mlp.experts." in name) and name not in params_dict:
287
+ continue
288
+ if is_fusion_moe_shared_experts_layer:
289
+ continue
290
+ name_mapped = name.replace(weight_name, param_name)
291
+ if (
292
+ param_name == "fused_qkv_a_proj"
293
+ ) and name_mapped not in params_dict:
294
+ continue
295
+ else:
296
+ name = name_mapped
297
+ if name.endswith(".bias") and name not in params_dict:
298
+ continue
299
+ param = params_dict[name]
300
+ weight_loader = param.weight_loader
301
+ weight_loader(param, loaded_weight, shard_id)
302
+ break
303
+ else:
304
+ num_chunks = 1
305
+ if is_fusion_moe_shared_experts_layer:
306
+ num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
307
+ split_dim = (
308
+ 1
309
+ if ("down_proj.weight" in name and loaded_weight.ndim > 1)
310
+ else 0
311
+ )
312
+ total = loaded_weight.shape[split_dim]
313
+ assert total % num_chunks == 0
314
+ chunk_size = total // num_chunks
315
+
316
+ for j in range(num_chunks):
317
+ chunk_name = name
318
+ weight_to_load = loaded_weight
319
+ if is_fusion_moe_shared_experts_layer:
320
+ chunk_slice = slice(j * chunk_size, (j + 1) * chunk_size)
321
+ if loaded_weight.ndim == 1:
322
+ weight_to_load = loaded_weight[chunk_slice]
323
+ elif split_dim == 0:
324
+ weight_to_load = loaded_weight[chunk_slice, :]
325
+ else:
326
+ weight_to_load = loaded_weight[:, chunk_slice]
327
+ chunk_name = name.replace(
328
+ "mlp.shared_experts",
329
+ f"mlp.experts.{self.config.n_routed_experts + j}",
330
+ )
331
+
332
+ is_expert_weight = False
333
+ for mapping in expert_params_mapping:
334
+ param_name, weight_name, expert_id, shard_id = mapping # type: ignore[assignment]
335
+ if weight_name not in chunk_name:
336
+ continue
337
+ is_expert_weight = True
338
+ name_mapped = chunk_name.replace(weight_name, param_name)
339
+ param = params_dict[name_mapped]
340
+ weight_loader = typing.cast(
341
+ Callable[..., bool], param.weight_loader
342
+ )
343
+ success = weight_loader(
344
+ param,
345
+ weight_to_load,
346
+ name_mapped,
347
+ shard_id=shard_id,
348
+ expert_id=expert_id,
349
+ return_success=True,
350
+ )
351
+ if success:
352
+ if not is_fusion_moe_shared_experts_layer:
353
+ name = name_mapped
354
+ else:
355
+ loaded_params.add(name_mapped)
356
+ break
357
+ else:
358
+ if is_expert_weight:
359
+ continue
360
+ if name.endswith(".bias") and name not in params_dict:
361
+ continue
362
+ name = maybe_remap_kv_scale_name(name, params_dict) # type: ignore[assignment]
363
+ if name is None:
364
+ continue
365
+ if (
366
+ spec_layer != self.model.mtp_start_layer_idx
367
+ and ".layers" not in name
368
+ ):
369
+ continue
370
+ param = params_dict[name]
371
+ weight_loader = getattr(
372
+ param, "weight_loader", default_weight_loader
373
+ )
374
+ weight_loader(param, loaded_weight)
375
+ if not is_fusion_moe_shared_experts_layer:
376
+ loaded_params.add(name)
377
+
378
+ loaded_layers: set[int] = set()
379
+ for param_name in loaded_params:
380
+ spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
381
+ if spec_layer is not None:
382
+ loaded_layers.add(spec_layer)
383
+ for layer_idx in range(
384
+ self.model.mtp_start_layer_idx,
385
+ self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
386
+ ):
387
+ if layer_idx not in loaded_layers:
388
+ raise ValueError(
389
+ f"MTP speculative decoding layer {layer_idx} weights "
390
+ f"missing from checkpoint."
391
+ )
392
+ return loaded_params
vllm/models/deepseek_v32/amd/rocm.py
new
+263
@@ -0,0 +1,263 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import torch
5
+
6
+from vllm.compilation.breakable_cudagraph import eager_break_during_capture
7
+from vllm.forward_context import get_forward_context
8
+from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
9
+from vllm.model_executor.models.deepseek_v2 import DeepseekV32IndexerCache
10
+from vllm.models.deepseek_v32.attention import DeepseekV32Attention, DeepseekV32Indexer
11
+from vllm.models.deepseek_v32.common.kernels import fused_norm_rope, fused_q
12
+from vllm.utils.torch_utils import is_quantized_kv_cache
13
+from vllm.v1.attention.backends.mla.indexer import DeepseekV32IndexerBackend
14
+from vllm.v1.attention.backends.mla.rocm_aiter_mla_sparse import (
15
+ ROCMAiterMLASparseBackend,
16
+)
17
+
18
+
19
+class DeepseekV32MLASparseBackend(ROCMAiterMLASparseBackend):
20
+ @staticmethod
21
+ def get_supported_kernel_block_sizes() -> list:
22
+ return [16, 32]
23
+
24
+
25
+class DeepseekV32ROCmIndexerBackend(DeepseekV32IndexerBackend):
26
+ @staticmethod
27
+ def get_supported_kernel_block_sizes() -> list:
28
+ return [16, 32]
29
+
30
+
31
+class DeepseekV32ROCmIndexerCache(DeepseekV32IndexerCache):
32
+ def get_attn_backend(self):
33
+ return DeepseekV32ROCmIndexerBackend
34
+
35
+
36
+class DeepseekV32ROCmIndexer(DeepseekV32Indexer):
37
+ indexer_cache_cls = DeepseekV32ROCmIndexerCache
38
+
39
+
40
+class DeepseekV32MLAAttention(DeepseekV32Attention):
41
+ require_fp8_kv_cache: bool = False
42
+ indexer_cls = DeepseekV32ROCmIndexer
43
+
44
+ def __init__(self, vllm_config, config, prefix, topk_indices_buffer=None):
45
+ super().__init__(
46
+ vllm_config,
47
+ config,
48
+ prefix,
49
+ topk_indices_buffer,
50
+ attn_backend=DeepseekV32MLASparseBackend,
51
+ )
52
+
53
+ self.indexer_op: SparseAttnIndexer | None = None
54
+ if self.indexer is not None:
55
+ self.indexer_op = SparseAttnIndexer(
56
+ self.indexer.k_cache,
57
+ self.indexer.quant_block_size,
58
+ self.indexer.scale_fmt,
59
+ self.indexer.topk_tokens,
60
+ self.indexer.head_dim,
61
+ self.indexer.max_model_len,
62
+ self.indexer.max_total_seq_len,
63
+ topk_indices_buffer,
64
+ skip_k_cache_insert=True,
65
+ )
66
+ self._fp8_kv = is_quantized_kv_cache(self.kv_cache_dtype)
67
+ self._fp8_kv_needs_view = self._fp8_kv and self.kv_cache_dtype != "fp8_ds_mla"
68
+
69
+ def _compute_ql_nope(self, q_c: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
70
+ q = self.q_b_proj(q_c)[0].view(-1, self.num_local_heads, self.qk_head_dim)
71
+ q_nope, q_pe = q.split([self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
72
+ q_nope = q_nope.transpose(0, 1) # (N, tokens, P)
73
+
74
+ if self.is_aiter_triton_fp4_bmm_enabled:
75
+ from aiter.ops.triton.batched_gemm_a16wfp4 import batched_gemm_a16wfp4
76
+
77
+ ql_nope = batched_gemm_a16wfp4(
78
+ q_nope, self.W_K, self.W_K_scale, transpose_bm=True, prequant=True
79
+ )
80
+ elif self.is_aiter_triton_fp8_bmm_enabled:
81
+ from vllm._aiter_ops import rocm_aiter_ops
82
+
83
+ ql_nope = rocm_aiter_ops.triton_fp8_bmm(
84
+ q_nope, self.W_K, self.W_K_scale, group_size=128, transpose_bm=True
85
+ )
86
+ else:
87
+ ql_nope = torch.bmm(q_nope, self.W_UK_T).transpose(0, 1)
88
+
89
+ return ql_nope, q_pe
90
+
91
+ def _run_indexer(
92
+ self,
93
+ q_c: torch.Tensor,
94
+ index_q_fp8: torch.Tensor | None,
95
+ index_weights_out: torch.Tensor | None,
96
+ ) -> None:
97
+ """Run the ROCm sparse indexer (forward_hip) if this layer has an indexer."""
98
+ if self.indexer_op is not None:
99
+ self.indexer_op.forward_hip(q_c, index_q_fp8, None, index_weights_out)
100
+
101
+ def _build_q_for_attn(
102
+ self,
103
+ ql_nope: torch.Tensor,
104
+ mqa_q: torch.Tensor,
105
+ num_actual: int,
106
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
107
+ if self._fp8_kv:
108
+ # fp8 KV: mqa_q is the full [ql_nope; q_pe] packed as fp8.
109
+ return mqa_q[:num_actual]
110
+
111
+ return (ql_nope[:num_actual], mqa_q[:num_actual])
112
+
113
+ def _compute_uv_out(
114
+ self,
115
+ attn_out: torch.Tensor,
116
+ output: torch.Tensor,
117
+ num_actual: int,
118
+ ) -> None:
119
+ x = attn_out.view(
120
+ num_actual, self.num_local_heads, self.kv_lora_rank
121
+ ).transpose(0, 1) # (N, tokens, L)
122
+ out_view = output[:num_actual].view(
123
+ num_actual, self.num_local_heads, self.v_head_dim
124
+ )
125
+
126
+ if self.is_aiter_triton_fp4_bmm_enabled:
127
+ from aiter.ops.triton.batched_gemm_a16wfp4 import batched_gemm_a16wfp4
128
+
129
+ batched_gemm_a16wfp4(
130
+ x, self.W_V, self.W_V_scale, out_view, transpose_bm=True, prequant=True
131
+ )
132
+ elif self.is_aiter_triton_fp8_bmm_enabled:
133
+ from vllm._aiter_ops import rocm_aiter_ops
134
+
135
+ rocm_aiter_ops.triton_fp8_bmm(
136
+ x,
137
+ self.W_V,
138
+ self.W_V_scale,
139
+ group_size=128,
140
+ transpose_bm=True,
141
+ YQ=out_view,
142
+ )
143
+ else:
144
+ torch.bmm(x, self.W_UV, out=out_view.transpose(0, 1))
145
+
146
+ @eager_break_during_capture
147
+ def _fused_attention(
148
+ self,
149
+ positions: torch.Tensor,
150
+ q_c: torch.Tensor,
151
+ kv_c: torch.Tensor,
152
+ k_pe: torch.Tensor,
153
+ index_k: torch.Tensor | None,
154
+ index_weights: torch.Tensor | None,
155
+ output: torch.Tensor,
156
+ ) -> None:
157
+ forward_context = get_forward_context()
158
+ attn_metadata_raw = forward_context.attn_metadata
159
+ if isinstance(attn_metadata_raw, dict):
160
+ attn_metadata = attn_metadata_raw.get(self.layer_name)
161
+ elif isinstance(attn_metadata_raw, list):
162
+ attn_metadata = attn_metadata_raw[0].get(self.layer_name)
163
+ else:
164
+ attn_metadata = attn_metadata_raw
165
+
166
+ slot_mapping = forward_context.slot_mapping
167
+ assert isinstance(slot_mapping, dict)
168
+ mla_slot = slot_mapping.get(self.layer_name)
169
+
170
+ if self.indexer is not None:
171
+ has_indexer = True
172
+ indexer_k_norm_w = self.indexer.k_norm.weight
173
+ indexer_k_norm_bias = self.indexer.k_norm.bias
174
+ indexer_k_norm_eps = self.indexer.k_norm.eps
175
+ indexer_k_rope_cos_sin_cache = self.indexer_rope_emb.cos_sin_cache
176
+ indexer_k_cache = self.indexer.k_cache.kv_cache
177
+ indexer_softmax_scale = self.indexer.softmax_scale
178
+ indexer_n_head_scale = self.indexer.n_head**-0.5
179
+ else:
180
+ has_indexer = False
181
+ indexer_k_norm_w = None
182
+ indexer_k_norm_bias = None
183
+ indexer_k_norm_eps = 1e-6
184
+ indexer_k_rope_cos_sin_cache = None
185
+ indexer_k_cache = None
186
+ indexer_softmax_scale = 0.0
187
+ indexer_n_head_scale = 0.0
188
+
189
+ if attn_metadata is None:
190
+ mla_kv_cache = None
191
+ mla_k_scale = None
192
+ indexer_k_cache = None
193
+ mla_slot = None
194
+ else:
195
+ mla_kv_cache = self.kv_cache
196
+ mla_k_scale = self._k_scale
197
+
198
+ q_c = fused_norm_rope(
199
+ positions,
200
+ q_c,
201
+ self.q_a_layernorm.weight,
202
+ self.q_a_layernorm.variance_epsilon,
203
+ kv_c,
204
+ self.kv_a_layernorm.weight,
205
+ self.kv_a_layernorm.variance_epsilon,
206
+ k_pe,
207
+ self.rotary_emb.cos_sin_cache,
208
+ index_k,
209
+ indexer_k_norm_w,
210
+ indexer_k_norm_bias,
211
+ indexer_k_norm_eps,
212
+ indexer_k_rope_cos_sin_cache,
213
+ self.topk_indices_buffer,
214
+ slot_mapping=mla_slot,
215
+ indexer_k_cache=indexer_k_cache,
216
+ mla_kv_cache=mla_kv_cache,
217
+ mla_kv_cache_dtype=self.kv_cache_dtype,
218
+ mla_k_scale=mla_k_scale,
219
+ has_indexer=has_indexer,
220
+ index_rope_interleave=self._index_rope_interleave,
221
+ )
222
+
223
+ ql_nope, q_pe = self._compute_ql_nope(q_c)
224
+
225
+ if self.indexer is not None:
226
+ index_q = self.indexer.wq_b(q_c)[0]
227
+ index_q = index_q.view(-1, self.indexer.n_head, self.indexer.head_dim)
228
+ else:
229
+ index_q = None
230
+
231
+ index_q_fp8, index_weights_out, mqa_q = fused_q(
232
+ positions,
233
+ q_pe,
234
+ self.rotary_emb.cos_sin_cache,
235
+ index_q,
236
+ self.indexer_rope_emb.cos_sin_cache if has_indexer else None,
237
+ ql_nope,
238
+ self._q_scale,
239
+ index_weights,
240
+ indexer_softmax_scale,
241
+ indexer_n_head_scale,
242
+ has_indexer=has_indexer,
243
+ index_rope_interleave=self._index_rope_interleave,
244
+ quantize_mqa=self._fp8_kv,
245
+ )
246
+
247
+ self._run_indexer(q_c, index_q_fp8, index_weights_out)
248
+
249
+ if attn_metadata is None:
250
+ output.zero_()
251
+ return
252
+
253
+ num_actual = attn_metadata.num_actual_tokens # type: ignore[attr-defined]
254
+ kv_cache = self.kv_cache
255
+ if self._fp8_kv_needs_view:
256
+ kv_cache = kv_cache.view(torch.float8_e4m3fn)
257
+
258
+ q_for_attn = self._build_q_for_attn(ql_nope, mqa_q, num_actual)
259
+ attn_out, _ = self.impl.forward_mqa( # type: ignore[attr-defined]
260
+ q_for_attn, kv_cache, attn_metadata, self
261
+ )
262
+
263
+ self._compute_uv_out(attn_out, output, num_actual)
vllm/models/deepseek_v32/attention.py
renamed
+32
-60
@@ -31,12 +31,13 @@ from vllm.model_executor.models.deepseek_v2 import (
31
yarn_get_mscale,
32
)
33
from vllm.model_executor.models.utils import extract_layer_index
34
+from vllm.models.deepseek_v32.common.kernels import fused_norm_rope, fused_q
35
from vllm.utils.torch_utils import is_quantized_kv_cache
36
36
-from .kernels import fused_norm_rope, fused_q
37
-
37
38
class DeepseekV32Indexer(nn.Module):
39
+ indexer_cache_cls = DeepseekV32IndexerCache
40
+
41
def __init__(
42
self,
43
vllm_config: VllmConfig,
@@ -55,7 +56,6 @@ class DeepseekV32Indexer(nn.Module):
56
self.rope_dim = config.qk_rope_head_dim
57
self.q_lora_rank = q_lora_rank
58
58
- # No tensor parallel, just replicated.
59
self.wq_b = ReplicatedLinear(
60
self.q_lora_rank,
61
self.head_dim * self.n_head,
@@ -63,8 +63,7 @@ class DeepseekV32Indexer(nn.Module):
63
quant_config=quant_config,
64
prefix=f"{prefix}.wq_b",
65
)
66
- # Fused wk + weights_proj: single GEMM producing [head_dim + n_head].
67
- # FP8 wk weights are upcasted to BF16 during loading to keep this fused.
66
+
67
self.wk_weights_proj = MergedColumnParallelLinear(
68
hidden_size,
69
[self.head_dim, self.n_head],
@@ -80,9 +79,8 @@ class DeepseekV32Indexer(nn.Module):
79
self.quant_block_size = 128
80
self.topk_indices_buffer = topk_indices_buffer
81
83
- # fp8 naive cache: value in fp8 + fp32 scale per quant_block_size element.
82
assert cache_config is not None, "DeepSeek V3.2 indexer requires cache_config"
85
- self.k_cache = DeepseekV32IndexerCache(
83
+ self.k_cache = type(self).indexer_cache_cls(
84
head_dim=self.head_dim + self.head_dim // self.quant_block_size * 4,
85
dtype=torch.uint8,
86
prefix=f"{prefix}.k_cache",
@@ -120,7 +118,7 @@ class DeepseekV32Indexer(nn.Module):
118
q_pe, q_nope = torch.split(
119
q, [self.rope_dim, self.head_dim - self.rope_dim], dim=-1
120
)
123
- # Fused wk + weights_proj: one GEMM, then split.
121
+
122
kw, _ = self.wk_weights_proj(hidden_states)
123
k = kw[:, : self.head_dim]
124
weights = kw[:, self.head_dim :]
@@ -131,14 +129,13 @@ class DeepseekV32Indexer(nn.Module):
129
)
130
131
q_pe, k_pe = rotary_emb(positions, q_pe, k_pe.unsqueeze(1))
134
- # RoPE (NeoX) can introduce extra leading dims; reshape back to flat.
132
+
133
q_pe = q_pe.reshape(-1, self.n_head, self.rope_dim)
134
k_pe = k_pe.reshape(-1, 1, self.rope_dim)
135
136
q = torch.cat([q_pe, q_nope], dim=-1)
137
k = torch.cat([k_pe.squeeze(-2), k_nope], dim=-1)
138
141
- # Only quant q here; k quant is fused with cache insertion.
139
q = q.view(-1, self.head_dim)
140
q_fp8, q_scale = per_token_group_quant_fp8(
141
q,
@@ -158,9 +155,10 @@ class DeepseekV32Indexer(nn.Module):
155
156
157
class DeepseekV32Attention(MLAAttention):
161
- # Narrow the base's broadly-typed `indexer` to the concrete type so the
162
- # `if self.indexer is not None` guards below type-check its attributes.
158
indexer: "DeepseekV32Indexer | None"
159
+ indexer_cls: "type[DeepseekV32Indexer]" = DeepseekV32Indexer
160
+
161
+ require_fp8_kv_cache: bool = True
162
163
def __init__(
164
self,
@@ -168,6 +166,7 @@ class DeepseekV32Attention(MLAAttention):
166
config: DeepseekV2Config | DeepseekV3Config,
167
prefix: str,
168
topk_indices_buffer: torch.Tensor | None = None,
169
+ attn_backend: "type | None" = None,
170
) -> None:
171
quant_config = vllm_config.quant_config
172
cache_config = vllm_config.cache_config
@@ -200,11 +199,6 @@ class DeepseekV32Attention(MLAAttention):
199
mscale = yarn_get_mscale(scaling_factor, float(mscale_all_dim))
200
scaling = scaling * mscale * mscale
201
203
- # DSA "shared indexer" pattern: only some layers carry an indexer; the
204
- # rest reuse the top-k written by the previous indexer layer into the
205
- # shared topk_indices_buffer. DeepSeek-V3.2 builds it on every layer
206
- # (index_topk_freq defaults to 1); GLM-5.2 uses index_topk_freq=4 so
207
- # only layers [0,1,2,6,10,...] (+ MTP) carry one.
202
layer_id = extract_layer_index(prefix)
203
index_topk_freq = getattr(config, "index_topk_freq", 1)
204
index_topk_pattern = getattr(config, "index_topk_pattern", None)
@@ -217,12 +211,10 @@ class DeepseekV32Attention(MLAAttention):
211
skip_topk = index_topk_pattern[layer_id] == "S"
212
else:
213
skip_topk = False
220
- # MTP/nextn layers always build a full indexer (they toggle at runtime).
214
+
215
num_hidden_layers = getattr(config, "num_hidden_layers", None)
216
is_mtp_layer = num_hidden_layers is not None and layer_id >= num_hidden_layers
217
224
- # Build kv_b_proj + indexer first; they are passed to MLAAttention.__init__
225
- # (which runs nn.Module.__init__ and registers them).
218
kv_b_proj = ColumnParallelLinear(
219
kv_lora_rank,
220
num_heads * (qk_nope_head_dim + v_head_dim),
@@ -232,7 +224,7 @@ class DeepseekV32Attention(MLAAttention):
224
)
225
indexer = None
226
if not skip_topk or is_mtp_layer:
235
- indexer = DeepseekV32Indexer(
227
+ indexer = type(self).indexer_cls(
228
vllm_config,
229
config,
230
hidden_size,
@@ -243,8 +235,6 @@ class DeepseekV32Attention(MLAAttention):
235
prefix=f"{prefix}.indexer",
236
)
237
246
- # Set up the MLA engine (impl, KV cache, scales, backend, registration,
247
- # and process_weights_after_loading) via the MLAAttention base.
238
super().__init__(
239
num_heads=num_local_heads,
240
scale=scaling,
@@ -260,41 +250,32 @@ class DeepseekV32Attention(MLAAttention):
250
use_sparse=True,
251
indexer=indexer,
252
topk_indices_buffer=topk_indices_buffer,
253
+ attn_backend=attn_backend,
254
)
255
256
self.num_local_heads = num_local_heads
257
self.qk_head_dim = qk_head_dim
258
self.indexer = indexer
259
self.topk_indices_buffer = topk_indices_buffer
269
- # Runtime toggle for index_share_for_mtp_iteration: MTP draft step 0
270
- # computes the top-k, steps 1+ set this True to reuse it.
260
+
261
self.skip_topk = False
272
- # Fused fp8 paths: Triton fused norm/rope/cache + fused-q. Two layouts,
273
- # picked by the sparse MLA backend's query support:
274
- # * supports_quant_query_input (FlashInfer sparse, SM100): per-tensor
275
- # fp8 cache + a single packed fp8 MQA query.
276
- # * not supported (FlashMLA sparse, SM90/SM100): fp8_ds_mla cache
277
- # (per-128 block-scaled fp8 NoPE + unquantized bf16 RoPE) + a bf16
278
- # (ql_nope, q_pe) query tuple. FA3 cannot mix a bf16 query with an
279
- # fp8 KV cache, so FlashMLA (which dequantizes internally) is used.
280
- # FlashMLA sparse runs on both Hopper and Blackwell, so this is the
281
- # only DSA path on SM90 and an opt-in alternative on SM100.
282
- assert is_quantized_kv_cache(self.kv_cache_dtype), (
283
- "deepseek_v32 (nvidia) requires an fp8 KV cache served by a sparse "
284
- "MLA backend. Launch with --kv-cache-dtype fp8 (FlashInfer sparse) "
285
- "or --kv-cache-dtype fp8_ds_mla (FlashMLA sparse)."
286
- )
287
- self._fp8_query = self.impl.supports_quant_query_input
288
- if not self._fp8_query:
289
- assert self.kv_cache_dtype == "fp8_ds_mla", (
290
- "deepseek_v32 (nvidia) on a bf16-query sparse MLA backend "
291
- "(FlashMLA sparse) requires the fp8_ds_mla KV cache layout. "
292
- "Launch with --kv-cache-dtype fp8_ds_mla."
262
+
263
+ if self.require_fp8_kv_cache:
264
+ assert is_quantized_kv_cache(self.kv_cache_dtype), (
265
+ "deepseek_v32 (nvidia) requires an fp8 KV cache served by a sparse "
266
+ "MLA backend. Launch with --kv-cache-dtype fp8 (FlashInfer sparse) "
267
+ "or --kv-cache-dtype fp8_ds_mla (FlashMLA sparse)."
268
)
294
- # The paged KV cache is stored as uint8 and viewed as fp8 for the decode
295
- # (per-tensor fp8). The fp8_ds_mla layout is consumed as raw bytes.
296
- self._fp8_kv_needs_view = self.kv_cache_dtype != "fp8_ds_mla"
297
- # GLM-5.2 uses interleaved indexer RoPE; DeepSeek-V3.2 uses NeoX.
269
+ self._fp8_query = self.impl.supports_quant_query_input
270
+ if not self._fp8_query:
271
+ assert self.kv_cache_dtype == "fp8_ds_mla", (
272
+ "deepseek_v32 (nvidia) on a bf16-query sparse MLA backend "
273
+ "(FlashMLA sparse) requires the fp8_ds_mla KV cache layout. "
274
+ "Launch with --kv-cache-dtype fp8_ds_mla."
275
+ )
276
+
277
+ self._fp8_kv_needs_view = self.kv_cache_dtype != "fp8_ds_mla"
278
+
279
self._index_rope_interleave = getattr(config, "indexer_rope_interleave", False)
280
281
# Remaining MLA projections (registered on this module).
@@ -313,9 +294,7 @@ class DeepseekV32Attention(MLAAttention):
294
prefix=f"{prefix}.q_b_proj",
295
)
296
self.kv_a_layernorm = RMSNorm(kv_lora_rank, eps=config.rms_norm_eps)
316
- # reduce_results=False: the attention all-reduce is fused with the
317
- # following post_attention_layernorm in the decoder layer via
318
- # fused_allreduce_rms_norm.
297
+
298
self.o_proj = RowParallelLinear(
299
num_heads * v_head_dim,
300
hidden_size,
@@ -380,11 +359,6 @@ class DeepseekV32Attention(MLAAttention):
359
index_weights: torch.Tensor | None,
360
output: torch.Tensor,
361
) -> None:
383
- # One eager break for the whole attention. In FULL cudagraph mode (pure
384
- # decode) this decorator is a no-op, so everything here is captured; in
385
- # PIECEWISE (prefill) it runs eagerly. The cache writes, sparse indexer,
386
- # and forward_mqa all depend on per-step metadata and must not be split
387
- # out (PIECEWISE capture would otherwise miss them).
362
forward_context = get_forward_context()
363
attn_metadata_raw = forward_context.attn_metadata
364
if isinstance(attn_metadata_raw, dict):
@@ -516,8 +490,6 @@ class DeepseekV32Attention(MLAAttention):
490
:num_actual
491
]
492
else:
519
- # FlashMLA sparse: bf16 (ql_nope, q_pe) tuple. mqa_q is the RoPE'd
520
- # q_pe; ql_nope is consumed directly.
493
mqa_q_arg = (ql_nope[:num_actual], mqa_q[:num_actual])
494
attn_out, _ = self.impl.forward_mqa( # type: ignore[attr-defined]
495
mqa_q_arg, kv_cache, attn_metadata, self
vllm/models/deepseek_v32/common/__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/common/fused_ops.py
renamed
vllm/models/deepseek_v32/common/kernels.py
renamed
vllm/models/deepseek_v32/nvidia/model.py
+2
-3
@@ -38,11 +38,10 @@ from vllm.model_executor.models.utils import (
38
make_layers,
39
sequence_parallel_chunk,
40
)
41
+from vllm.models.deepseek_v32.attention import DeepseekV32Attention
42
+from vllm.models.deepseek_v32.common.fused_ops import fused_allreduce_rms_norm
43
from vllm.sequence import IntermediateTensors
44
43
-from .attention import DeepseekV32Attention
44
-from .fused_ops import fused_allreduce_rms_norm
45
-
45
46
def _all_gather_sp_states(
47
hidden_states: torch.Tensor,
vllm/models/deepseek_v32/nvidia/mtp.py
+1
-1
@@ -38,10 +38,10 @@ from vllm.model_executor.models.utils import (
38
get_pp_missing_layer_names,
39
maybe_prefix,
40
)
41
+from vllm.models.deepseek_v32.common.kernels import fused_eh_norm
42
from vllm.platforms import current_platform
43
from vllm.sequence import IntermediateTensors
44
44
-from .kernels import fused_eh_norm
45
from .model import DeepseekV32DecoderLayer
46
47