[Model][ROCm] Enable DeepSeek V4 Vision (#55107)

Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com> Signed-off-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: OpenAI Codex <codex@openai.com>

Andreas Karatzas committed Sep 11, 2026 at 11:17 UTC 9dd969da096e37256ee37e24f6a4689d860f39ce
17 files changed +1055 -347
.buildkite/test_areas/models_basic.yaml
+4 -2
@@ -48,8 +48,9 @@ steps:
48 - tests/models/test_terratorch.py
49 - tests/models/transformers/test_backend.py
50 - tests/models/test_registry.py
51 + - tests/models/test_deepseek_v4_vl_rocm.py
52 commands:
52 - - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py
53 + - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py
54 mirror:
55 amd:
56 label: ":amd: (MI300) Basic Models (Other)"
@@ -59,9 +60,10 @@ steps:
60 depends_on:
61 - image-build-amd
62 source_file_dependencies:
63 + - tests/models/test_deepseek_v4_vl_rocm.py
64 - tests/models/test_hyv4_rocm.py
65 commands:
64 - - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py
66 + - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py
67 - VLLM_ROCM_USE_AITER=1 pytest -v -s models/test_hyv4_rocm.py -m 'not distributed'
68
69 - label: ":nvidia: (B200) Inkling"
tests/kernels/attention/test_rocm_triton_attn_dsv4.py
+95
@@ -336,6 +336,101 @@ def test_compute_global_topk_ragged_indices_and_indptr() -> None:
336 torch.testing.assert_close(actual_lens, expected_lens)
337
338
339 +@torch.inference_mode()
340 +def test_combine_topk_swa_indices_adds_image_visibility() -> None:
341 + from vllm.models.deepseek_v4.amd.rocm import combine_topk_swa_indices
342 +
343 + device = torch.device("cuda")
344 + num_tokens = 8
345 + topk_indices = torch.full((num_tokens, 1), -1, dtype=torch.int32, device=device)
346 + query_start_loc = torch.tensor([0, num_tokens], dtype=torch.int32, device=device)
347 + seq_lens = torch.tensor([num_tokens], dtype=torch.int32, device=device)
348 + gather_lens = seq_lens.clone()
349 + left_visible = torch.tensor(
350 + [0, 0, 0, 1, 2, 3, 4, 0], dtype=torch.int32, device=device
351 + )
352 + right_visible = torch.tensor(
353 + [0, 0, 4, 3, 2, 1, 0, 0], dtype=torch.int32, device=device
354 + )
355 +
356 + indices, lens = combine_topk_swa_indices(
357 + topk_indices,
358 + query_start_loc,
359 + seq_lens,
360 + gather_lens,
361 + window_size=4,
362 + compress_ratio=1,
363 + topk=0,
364 + M=16,
365 + N=0,
366 + max_image_tokens=5,
367 + left_visible=left_visible,
368 + right_visible=right_visible,
369 + )
370 +
371 + expected_rows = [
372 + [0],
373 + [0, 1],
374 + [0, 1, 2, 3, 4, 5, 6],
375 + [0, 1, 2, 3, 4, 5, 6],
376 + [1, 2, 3, 4, 5, 6],
377 + [2, 3, 4, 5, 6],
378 + [2, 3, 4, 5, 6],
379 + [4, 5, 6, 7],
380 + ]
381 + for token_idx, expected in enumerate(expected_rows):
382 + actual = indices[token_idx, : lens[token_idx]].cpu().tolist()
383 + assert actual == expected
384 +
385 +
386 +@torch.inference_mode()
387 +def test_combine_topk_swa_indices_apc_hit_inside_image() -> None:
388 + from vllm.models.deepseek_v4.amd.rocm import combine_topk_swa_indices
389 +
390 + device = torch.device("cuda")
391 + indices, lens = combine_topk_swa_indices(
392 + torch.full((2, 1), -1, dtype=torch.int32, device=device),
393 + torch.tensor([0, 2], dtype=torch.int32, device=device),
394 + torch.tensor([10], dtype=torch.int32, device=device),
395 + # Only positions [5, 10) exist in the gathered SWA workspace.
396 + torch.tensor([5], dtype=torch.int32, device=device),
397 + window_size=4,
398 + compress_ratio=1,
399 + topk=0,
400 + M=10,
401 + N=0,
402 + max_image_tokens=10,
403 + left_visible=torch.tensor([8, 9], dtype=torch.int32, device=device),
404 + # Deliberately extends beyond seq_len to exercise the upper clamp too.
405 + right_visible=torch.tensor([5, 5], dtype=torch.int32, device=device),
406 + )
407 +
408 + assert lens.cpu().tolist() == [5, 5]
409 + assert indices[:, :5].cpu().tolist() == [list(range(5)), list(range(5))]
410 +
411 +
412 +@torch.inference_mode()
413 +def test_combine_topk_swa_indices_keeps_vision_row_width_without_images() -> None:
414 + from vllm.models.deepseek_v4.amd.rocm import combine_topk_swa_indices
415 +
416 + device = torch.device("cuda")
417 + indices, lens = combine_topk_swa_indices(
418 + torch.full((1, 1), -1, dtype=torch.int32, device=device),
419 + torch.tensor([0, 1], dtype=torch.int32, device=device),
420 + torch.tensor([1], dtype=torch.int32, device=device),
421 + torch.tensor([1], dtype=torch.int32, device=device),
422 + window_size=120,
423 + compress_ratio=1,
424 + topk=0,
425 + M=256,
426 + N=0,
427 + max_image_tokens=16,
428 + )
429 +
430 + assert indices.shape == (1, 256)
431 + assert lens.item() == 1
432 +
433 +
434 def test_extra_cache_nan_free_provenance_gate(monkeypatch) -> None:
435 from vllm.models.deepseek_v4.amd import rocm as mod
436
tests/models/multimodal/processing/test_tensor_schema.py
+4 -3
@@ -164,10 +164,11 @@ def test_model_tensor_schema(model_id: str):
164 "Kimi-K2.5's offline inference has issues about vision chunks. Fix later."
165 )
166
167 - if model_id == "deepseek-ai/DeepSeek-V4-Flash-Vision-Exp" and not (
168 - current_platform.is_cuda()
167 + if (
168 + model_id == "deepseek-ai/DeepSeek-V4-Flash-Vision-Exp"
169 + and not current_platform.is_cuda_alike()
170 ):
170 - pytest.skip("Deepseek V4 is only supported on CUDA")
171 + pytest.skip("Deepseek V4 vision is only supported on CUDA and ROCm")
172
173 if model_id == "zai-org/GLM-5.3-Flash" and (current_platform.is_xpu()):
174 pytest.skip("GLM-5.3-Flash is not supported on XPU")
tests/models/test_deepseek_v4_vl_rocm.py new
+458
@@ -0,0 +1,458 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from types import SimpleNamespace
5 +
6 +import pytest
7 +import torch
8 +from torch import nn
9 +
10 +from vllm.model_executor.models.utils import WeightsMapper
11 +from vllm.platforms import current_platform
12 +
13 +pytestmark = pytest.mark.skipif(
14 + not current_platform.is_rocm(), reason="ROCm-specific tests"
15 +)
16 +
17 +
18 +def _preshuffled_fp8_linear(
19 + holder: str = "quant_method", weight_shape: tuple[int, int] = (256, 128)
20 +) -> nn.Module:
21 + pytest.importorskip("aiter")
22 + from vllm.model_executor.kernels.linear.scaled_mm.aiter import (
23 + AiterPreshuffledFp8BlockScaledMMKernel,
24 + )
25 + from vllm.model_executor.kernels.linear.scaled_mm.ScaledMMLinearKernel import (
26 + FP8ScaledMMLinearLayerConfig,
27 + )
28 + from vllm.model_executor.layers.quantization.utils.quant_utils import (
29 + kFp8Dynamic128Sym,
30 + kFp8Static128BlockSym,
31 + )
32 +
33 + config = FP8ScaledMMLinearLayerConfig(
34 + weight_quant_key=kFp8Static128BlockSym,
35 + activation_quant_key=kFp8Dynamic128Sym,
36 + input_dtype=torch.bfloat16,
37 + out_dtype=torch.bfloat16,
38 + weight_shape=weight_shape,
39 + )
40 + layer = nn.Module()
41 + layer.weight = nn.Parameter(
42 + torch.randn(*weight_shape, device="cuda").to(current_platform.fp8_dtype()),
43 + requires_grad=False,
44 + )
45 + layer.register_parameter(
46 + "weight_scale" if holder == "scheme" else "weight_scale_inv",
47 + nn.Parameter(
48 + torch.ones(*(dim // 128 for dim in weight_shape), device="cuda"),
49 + requires_grad=False,
50 + ),
51 + )
52 + setattr(
53 + layer,
54 + holder,
55 + SimpleNamespace(fp8_linear=AiterPreshuffledFp8BlockScaledMMKernel(config)),
56 + )
57 + return layer
58 +
59 +
60 +@pytest.mark.parametrize("holder", ["quant_method", "scheme"])
61 +def test_rocm_wo_a_keeps_row_major_weights(holder: str, default_vllm_config) -> None:
62 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import _get_cached_wo_a_bf16
63 +
64 + layer = _preshuffled_fp8_linear(holder)
65 + layer.is_bmm = True
66 + original = layer.weight.detach().clone()
67 + kernel = getattr(layer, holder).fp8_linear
68 + kernel.process_weights_after_loading(layer)
69 +
70 + actual = _get_cached_wo_a_bf16(layer, 2, 128, 128)
71 + torch.testing.assert_close(actual, original.to(torch.bfloat16).view(2, 128, 128))
72 +
73 +
74 +@pytest.mark.parametrize("holder", ["quant_method", "scheme"])
75 +def test_rocm_gateup_shuffles_once_and_down_proj_keeps_preshuffled_backend(
76 + holder: str, monkeypatch: pytest.MonkeyPatch, default_vllm_config
77 +) -> None:
78 + from vllm._aiter_ops import rocm_aiter_ops
79 + from vllm.models.deepseek_v4.amd import model as rocm_model
80 +
81 + gate_up, down = _preshuffled_fp8_linear(holder), _preshuffled_fp8_linear(holder)
82 + original_gate_up = gate_up.weight.detach().clone()
83 + original_down = down.weight.detach().clone()
84 + monkeypatch.setattr(
85 + rocm_model, "MergedColumnParallelLinear", lambda *a, **k: gate_up
86 + )
87 + monkeypatch.setattr(rocm_model, "RowParallelLinear", lambda *a, **k: down)
88 + monkeypatch.setattr(rocm_aiter_ops, "is_enabled", lambda: True)
89 + model = rocm_model.DeepseekV4MLP(128, 128, "silu")
90 + for linear in (gate_up, down):
91 + getattr(linear, holder).fp8_linear.process_weights_after_loading(linear)
92 + model.prepare_gateup_preshuffle()
93 +
94 + for linear, original in ((gate_up, original_gate_up), (down, original_down)):
95 + expected = rocm_aiter_ops.shuffle_weight(original, layout=(16, 16))
96 + torch.testing.assert_close(
97 + linear.weight.view(torch.uint8), expected.view(torch.uint8)
98 + )
99 +
100 +
101 +def test_rocm_fused_qkv_quant_matches_preshuffled_gemm_scale_layout(
102 + monkeypatch: pytest.MonkeyPatch, default_vllm_config
103 +) -> None:
104 + from vllm._aiter_ops import rocm_aiter_ops
105 + from vllm.models.deepseek_v4.amd.rocm import (
106 + DeepseekV4ROCMAiterMLAAttention,
107 + apply_pre_quantized_block_scaled_mm,
108 + )
109 +
110 + if not rocm_aiter_ops.is_blockscale_bpreshuffle_tuned(1024, 256):
111 + pytest.skip("Requires a tuned AITER preshuffled FP8 GEMM")
112 + linear = _preshuffled_fp8_linear(weight_shape=(1024, 256))
113 + original = linear.weight.detach().clone()
114 + linear.quant_method.fp8_linear.process_weights_after_loading(linear)
115 + attention = DeepseekV4ROCMAiterMLAAttention.__new__(DeepseekV4ROCMAiterMLAAttention)
116 + nn.Module.__init__(attention)
117 + attention.wq_b = linear
118 + attention.indexer = None
119 + attention.q_lora_rank, attention.head_dim = 256, 128
120 + attention.eps = 1e-5
121 + attention.q_norm = SimpleNamespace(
122 + weight=torch.ones(256, dtype=torch.bfloat16, device="cuda")
123 + )
124 + attention.kv_norm = SimpleNamespace(
125 + weight=torch.ones(128, dtype=torch.bfloat16, device="cuda")
126 + )
127 + monkeypatch.setattr(rocm_aiter_ops, "is_linear_fp8_enabled", lambda: True)
128 + # Multiple rows and groups with different ranges expose byte-order errors.
129 + inputs = torch.randn(16, 384, dtype=torch.bfloat16, device="cuda")
130 + inputs[:, :128] *= 4
131 + quantized, scales, _ = attention._split_qkv_and_norm(inputs)
132 + assert scales is not None
133 + output = apply_pre_quantized_block_scaled_mm(linear, quantized, scales)
134 + q = inputs[:, :256].float()
135 + normalized = q * torch.rsqrt(q.square().mean(dim=-1, keepdim=True) + attention.eps)
136 + reference_scales = normalized.view(16, 2, 128).abs().amax(dim=-1)
137 + reference_scales /= torch.finfo(current_platform.fp8_dtype()).max
138 + dequantized = quantized.float() * reference_scales.repeat_interleave(128, dim=1)
139 + expected = dequantized @ original.float().T
140 + torch.testing.assert_close(output.float(), expected, atol=0.125, rtol=0.01)
141 +
142 +
143 +def test_rocm_packed_kv_cache_auto_uses_ds_mla_layout() -> None:
144 + from vllm.config import CacheConfig
145 + from vllm.models.deepseek_v4.attention import _resolve_dsv4_kv_cache_dtype
146 +
147 + cache_config = CacheConfig()
148 +
149 + resolved_dtype, torch_dtype = _resolve_dsv4_kv_cache_dtype(
150 + use_fp8_ds_mla_layout=True,
151 + kv_cache_dtype=cache_config.cache_dtype,
152 + cache_config=cache_config,
153 + )
154 +
155 + assert resolved_dtype == "fp8_ds_mla"
156 + assert torch_dtype is torch.uint8
157 + assert cache_config.cache_dtype == "fp8_ds_mla"
158 +
159 +
160 +def test_rocm_packed_kv_cache_rejects_unquantized_dtype() -> None:
161 + from vllm.config import CacheConfig
162 + from vllm.models.deepseek_v4.attention import _resolve_dsv4_kv_cache_dtype
163 +
164 + cache_config = CacheConfig(cache_dtype="bfloat16")
165 +
166 + with pytest.raises(ValueError, match="only supports fp8 kv-cache"):
167 + _resolve_dsv4_kv_cache_dtype(
168 + use_fp8_ds_mla_layout=True,
169 + kv_cache_dtype=cache_config.cache_dtype,
170 + cache_config=cache_config,
171 + )
172 +
173 +
174 +def test_vl_mapper_preserves_rocm_weight_mapping() -> None:
175 + from vllm.models.deepseek_v4.amd.model import _make_deepseek_v4_weights_mapper
176 + from vllm.models.deepseek_v4.common.vl_model import (
177 + _make_deepseek_v4_vl_weights_mapper,
178 + )
179 +
180 + text_mapper = _make_deepseek_v4_weights_mapper("fp4", fuse_shared_experts=True)
181 + mapper = _make_deepseek_v4_vl_weights_mapper(text_mapper, image_enabled=True)
182 +
183 + assert mapper._map_name("layers.3.attn.wq_a.input_scale") == (
184 + "language_model.model.layers.3.attn.wq_a.input_scale_2"
185 + )
186 + assert mapper._map_name("layers.3.ffn.shared_experts.w2.weight") == (
187 + "language_model.model.layers.3.ffn.shared_experts.w2.weight"
188 + )
189 + assert mapper._map_name("head.weight") == "language_model.lm_head.weight"
190 +
191 +
192 +def test_rocm_moe_wires_vision_routing_on_hash_and_regular_layers(
193 + monkeypatch: pytest.MonkeyPatch,
194 +) -> None:
195 + from vllm.models.deepseek_v4.amd import model as rocm_model
196 + from vllm.models.deepseek_v4.common.mm_preprocess import IMAGE_SENTINEL_BASE_ID
197 +
198 + captured: list[dict] = []
199 +
200 + class FakeGate(nn.Module):
201 + def __init__(self, **kwargs) -> None:
202 + super().__init__()
203 +
204 + def fake_factory(**kwargs):
205 + captured.append(kwargs)
206 + return nn.Identity()
207 +
208 + monkeypatch.setattr(rocm_model, "GateLinear", FakeGate)
209 + monkeypatch.setattr(rocm_model, "FusedMoEFactory", fake_factory)
210 + monkeypatch.setattr(rocm_model, "get_tensor_model_parallel_world_size", lambda: 1)
211 + monkeypatch.setattr(rocm_model, "get_tensor_model_parallel_rank", lambda: 0)
212 +
213 + config = SimpleNamespace(
214 + hidden_size=16,
215 + n_routed_experts=4,
216 + num_experts_per_tok=2,
217 + moe_intermediate_size=8,
218 + swiglu_limit=None,
219 + norm_topk_prob=True,
220 + scoring_func="sqrtsoftplus",
221 + num_hash_layers=1,
222 + vocab_size=32,
223 + topk_method="noaux_tc",
224 + vision_n_layers=1,
225 + n_shared_experts=None,
226 + )
227 + vllm_config = SimpleNamespace(
228 + model_config=SimpleNamespace(hf_config=config), quant_config=None
229 + )
230 +
231 + hash_moe = rocm_model.DeepseekV4MoE(vllm_config, prefix="model.layers.0.ffn")
232 + regular_moe = rocm_model.DeepseekV4MoE(vllm_config, prefix="model.layers.1.ffn")
233 +
234 + assert hash_moe.gate.tid2eid is not None
235 + assert regular_moe.gate.tid2eid is None
236 + for moe, factory_kwargs in zip((hash_moe, regular_moe), captured, strict=True):
237 + assert moe.gate.e_score_correction_bias is not None
238 + assert moe.gate.bias_vl is not None
239 + assert factory_kwargs["bias_vl"] is moe.gate.bias_vl
240 + assert factory_kwargs["image_sentinel_lo"] == IMAGE_SENTINEL_BASE_ID
241 +
242 +
243 +def test_rocm_mtp_forwards_input_ids_for_vision_routing(
244 + monkeypatch: pytest.MonkeyPatch,
245 +) -> None:
246 + from vllm.models.deepseek_v4.amd import mtp as rocm_mtp
247 +
248 + hidden_size = 4
249 + hc_mult = 2
250 +
251 + class FakeNorm(nn.Module):
252 + def __init__(self) -> None:
253 + super().__init__()
254 + self.weight = nn.Parameter(torch.ones(hidden_size))
255 + self.variance_epsilon = 1e-6
256 +
257 + class FakeMTPBlock(nn.Module):
258 + use_fused_mhc = False
259 +
260 + def __init__(self) -> None:
261 + super().__init__()
262 + self.seen_input_ids: torch.Tensor | None = None
263 +
264 + def forward(
265 + self,
266 + *,
267 + positions: torch.Tensor,
268 + x: torch.Tensor,
269 + input_ids: torch.Tensor | None,
270 + ):
271 + self.seen_input_ids = input_ids
272 + return x, None, None, None
273 +
274 + def passthrough_mtp_input(
275 + inputs_embeds: torch.Tensor,
276 + positions: torch.Tensor,
277 + previous_hidden_states: torch.Tensor,
278 + *args,
279 + ) -> tuple[torch.Tensor, torch.Tensor]:
280 + return inputs_embeds, previous_hidden_states
281 +
282 + monkeypatch.setattr(
283 + rocm_mtp, "_FUSED_MTP_INPUT_RMSNORM_KERNEL", passthrough_mtp_input
284 + )
285 +
286 + layer = object.__new__(rocm_mtp.DeepSeekV4MultiTokenPredictorLayer)
287 + nn.Module.__init__(layer)
288 + layer.config = SimpleNamespace(hidden_size=hidden_size)
289 + layer.hc_mult = hc_mult
290 + layer.enorm = FakeNorm()
291 + layer.hnorm = FakeNorm()
292 + layer.e_proj = nn.Identity()
293 + layer.h_proj = nn.Identity()
294 + layer.mtp_block = FakeMTPBlock()
295 +
296 + input_ids = torch.tensor([11, 12])
297 + positions = torch.tensor([3, 4])
298 + inputs_embeds = torch.arange(8, dtype=torch.float32).view(2, hidden_size)
299 + previous_hidden_states = torch.arange(16, dtype=torch.float32).view(2, -1)
300 +
301 + output = layer(
302 + input_ids,
303 + positions,
304 + previous_hidden_states,
305 + inputs_embeds,
306 + )
307 +
308 + assert layer.mtp_block.seen_input_ids is input_ids
309 + expected = previous_hidden_states.view(2, hc_mult, hidden_size)
310 + expected = expected + inputs_embeds.unsqueeze(-2)
311 + torch.testing.assert_close(output, expected.flatten(1))
312 +
313 +
314 +def test_rocm_compute_logits_local_skips_gather() -> None:
315 + from vllm.models.deepseek_v4.amd.model import DeepseekV4ForCausalLM
316 +
317 + calls: list[tuple[nn.Module, torch.Tensor, bool]] = []
318 +
319 + def logits_processor(
320 + lm_head: nn.Module, hidden_states: torch.Tensor, *, skip_gather: bool = False
321 + ) -> torch.Tensor:
322 + calls.append((lm_head, hidden_states, skip_gather))
323 + return hidden_states + 1
324 +
325 + model = object.__new__(DeepseekV4ForCausalLM)
326 + nn.Module.__init__(model)
327 + model.lm_head = nn.Identity()
328 + model.logits_processor = logits_processor
329 + hidden_states = torch.tensor([4.0])
330 +
331 + result = model.compute_logits_local(hidden_states)
332 +
333 + assert torch.equal(result, torch.tensor([5.0]))
334 + assert calls == [(model.lm_head, hidden_states, True)]
335 +
336 +
337 +class _FakeLanguageModel(nn.Module):
338 + finalizes_weights_during_load = False
339 +
340 + def __init__(self) -> None:
341 + super().__init__()
342 + self.tensor_a = nn.Parameter(torch.zeros(1))
343 + self.tensor_c = nn.Parameter(torch.zeros(1))
344 + self.finalized_values: list[tuple[float, float]] = []
345 +
346 + def process_weights_after_loading(self) -> None:
347 + self.finalized_values.append((self.tensor_a.item(), self.tensor_c.item()))
348 +
349 + def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
350 + return hidden_states + 1
351 +
352 +
353 +def test_vl_wrapper_streams_then_delegates_finalization() -> None:
354 + from vllm.models.deepseek_v4.common.vl_model import (
355 + DeepseekV4ForConditionalGeneration,
356 + )
357 +
358 + model = object.__new__(DeepseekV4ForConditionalGeneration)
359 + nn.Module.__init__(model)
360 + model.language_model = _FakeLanguageModel()
361 + model.vision = nn.Module()
362 + model.vision.tensor_b = nn.Parameter(torch.zeros(1))
363 + model.hf_to_vllm_mapper = WeightsMapper()
364 +
365 + def interleaved_weights():
366 + yield "language_model.tensor_a", torch.tensor([1.0])
367 + assert model.language_model.tensor_a.item() == 1.0
368 + yield "vision.tensor_b", torch.tensor([2.0])
369 + assert model.vision.tensor_b.item() == 2.0
370 + yield "language_model.tensor_c", torch.tensor([3.0])
371 +
372 + loaded = model.load_weights(interleaved_weights())
373 +
374 + assert loaded == {
375 + "language_model.tensor_a",
376 + "vision.tensor_b",
377 + "language_model.tensor_c",
378 + }
379 + assert model.language_model.finalized_values == []
380 +
381 + model.process_weights_after_loading()
382 +
383 + assert model.language_model.finalized_values == [(1.0, 3.0)]
384 + assert torch.equal(
385 + model.compute_logits_local(torch.tensor([4.0])), torch.tensor([5.0])
386 + )
387 + model.process_weights_after_loading()
388 + assert model.language_model.finalized_values == [(1.0, 3.0)]
389 +
390 +
391 +class _FakeFinalizingLanguageModel(_FakeLanguageModel):
392 + finalizes_weights_during_load = True
393 +
394 + def __init__(self) -> None:
395 + super().__init__()
396 + self.load_calls = 0
397 +
398 + def load_weights(self, weights) -> set[str]:
399 + self.load_calls += 1
400 + loaded = set()
401 + for name, value in weights:
402 + getattr(self, name).data.copy_(value)
403 + loaded.add(name)
404 + self.process_weights_after_loading()
405 + return loaded
406 +
407 +
408 +def test_vl_wrapper_groups_child_that_finalizes_during_load() -> None:
409 + from vllm.models.deepseek_v4.common.vl_model import (
410 + DeepseekV4ForConditionalGeneration,
411 + )
412 +
413 + model = object.__new__(DeepseekV4ForConditionalGeneration)
414 + nn.Module.__init__(model)
415 + model.language_model = _FakeFinalizingLanguageModel()
416 + model.vision = nn.Module()
417 + model.vision.tensor_b = nn.Parameter(torch.zeros(1))
418 + model.hf_to_vllm_mapper = WeightsMapper()
419 +
420 + loaded = model.load_weights(
421 + iter(
422 + (
423 + ("language_model.tensor_a", torch.tensor([1.0])),
424 + ("vision.tensor_b", torch.tensor([2.0])),
425 + ("language_model.tensor_c", torch.tensor([3.0])),
426 + )
427 + )
428 + )
429 +
430 + assert loaded == {
431 + "language_model.tensor_a",
432 + "vision.tensor_b",
433 + "language_model.tensor_c",
434 + }
435 + assert model.language_model.load_calls == 1
436 + assert model.language_model.finalized_values == [(1.0, 3.0)]
437 +
438 + # The framework's later model-level hook must not double-finalize a child
439 + # which already completed this work in load_weights.
440 + model.process_weights_after_loading()
441 + assert model.language_model.finalized_values == [(1.0, 3.0)]
442 +
443 +
444 +def test_vl_wrapper_dummy_load_delegates_finalization() -> None:
445 + from vllm.models.deepseek_v4.common.vl_model import (
446 + DeepseekV4ForConditionalGeneration,
447 + )
448 +
449 + model = object.__new__(DeepseekV4ForConditionalGeneration)
450 + nn.Module.__init__(model)
451 + model.language_model = _FakeFinalizingLanguageModel()
452 +
453 + # DummyModelLoader bypasses model.load_weights(), so no finalized marker
454 + # exists and the framework-level hook must still delegate to the child.
455 + model.process_weights_after_loading()
456 + assert model.language_model.finalized_values == [(0.0, 0.0)]
457 + model.process_weights_after_loading()
458 + assert model.language_model.finalized_values == [(0.0, 0.0)]
tests/models/test_initialization.py
+2 -2
@@ -135,8 +135,8 @@ def can_initialize(
135 if model_arch == "DeepseekV4ForConditionalGeneration":
136 from vllm.platforms import current_platform
137
138 - if not current_platform.is_cuda():
139 - pytest.skip("Deepseek V4 is only supported on CUDA")
138 + if not (current_platform.is_cuda() or current_platform.is_rocm()):
139 + pytest.skip("Deepseek V4 vision is only supported on CUDA and ROCm")
140
141 with (
142 patch.object(V1EngineCore, "_initialize_kv_caches", _initialize_kv_caches_v1),
tests/models/test_registry.py
+4 -3
@@ -64,10 +64,11 @@ def test_registry_imports(model_arch):
64 ):
65 pytest.skip("HY V4 is only supported on CUDA and ROCm")
66
67 - if model_arch == "DeepseekV4ForConditionalGeneration" and not (
68 - current_platform.is_cuda()
67 + if (
68 + model_arch == "DeepseekV4ForConditionalGeneration"
69 + and not current_platform.is_cuda_alike()
70 ):
70 - pytest.skip("Deepseek V4 is only supported on CUDA")
71 + pytest.skip("Deepseek V4 vision is only supported on CUDA and ROCm")
72
73 # Ensure all model classes can be imported successfully
74 model_cls = ModelRegistry._try_load_model_cls(model_arch)
tests/test_config.py
+39
@@ -57,6 +57,45 @@ def _write_json(path: Path, value: object) -> None:
57 path.write_text(json.dumps(value), encoding="utf-8")
58
59
60 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm-specific test")
61 +@pytest.mark.parametrize(
62 + ("is_mm_prefix_lm", "is_multimodal_model", "expected"),
63 + [
64 + pytest.param(True, True, True, id="multimodal-prefix-lm"),
65 + pytest.param(False, True, False, id="multimodal-causal"),
66 + pytest.param(True, False, False, id="text-prefix-lm"),
67 + pytest.param(None, True, False, id="missing-model-config"),
68 + ],
69 +)
70 +def test_rocm_mm_prefix_lm_disables_chunked_mm_input(
71 + is_mm_prefix_lm: bool | None,
72 + is_multimodal_model: bool,
73 + expected: bool,
74 +) -> None:
75 + from vllm.platforms.rocm import RocmPlatform
76 +
77 + config = SimpleNamespace(
78 + compilation_config=SimpleNamespace(cudagraph_mode=CUDAGraphMode.NONE),
79 + parallel_config=SimpleNamespace(
80 + prefill_context_parallel_size=1,
81 + worker_cls="test-worker",
82 + ),
83 + model_config=(
84 + None
85 + if is_mm_prefix_lm is None
86 + else SimpleNamespace(is_mm_prefix_lm=is_mm_prefix_lm)
87 + ),
88 + scheduler_config=SimpleNamespace(
89 + is_multimodal_model=is_multimodal_model,
90 + disable_chunked_mm_input=False,
91 + ),
92 + )
93 +
94 + RocmPlatform.check_and_update_config(config)
95 +
96 + assert config.scheduler_config.disable_chunked_mm_input is expected
97 +
98 +
99 def test_kda_recoverssm_derivation_is_revalidated():
100 config = SimpleNamespace(
101 cache_config=SimpleNamespace(
tests/v1/spec_decode/test_adaptive_verification.py
+1
@@ -42,6 +42,7 @@ def make_manager(
42 ("mode", "piecewise_capture_available", "expected"),
43 [
44 ("FULL_DECODE_ONLY", True, "FULL_DECODE_ONLY"),
45 + ("FULL_DECODE_ONLY", False, "FULL_DECODE_ONLY"),
46 ("FULL", True, "FULL_AND_PIECEWISE"),
47 ("FULL", False, "FULL_DECODE_ONLY"),
48 ("FULL_AND_PIECEWISE", True, "FULL_AND_PIECEWISE"),
vllm/models/deepseek_v4/__init__.py
+4 -4
@@ -20,7 +20,7 @@ if current_platform.is_rocm():
20 )
21 from .amd.model import DeepseekV4ForCausalLM
22 from .amd.mtp import DeepSeekV4MTP
23 - from .vl_stub import ( # type: ignore[assignment]
23 + from .common.vl_model import ( # type: ignore[assignment]
24 DeepseekV4ForConditionalGeneration,
25 )
26 elif current_platform.is_xpu():
@@ -38,14 +38,14 @@ elif current_platform.is_cpu():
38 DeepseekV4ForConditionalGeneration,
39 )
40 else:
41 + from .common.vl_model import ( # type: ignore[assignment]
42 + DeepseekV4ForConditionalGeneration,
43 + )
44 from .nvidia.dspark import ( # type: ignore[assignment]
45 DSparkDeepseekV4ForCausalLM,
46 )
47 from .nvidia.model import DeepseekV4ForCausalLM # type: ignore[assignment]
48 from .nvidia.mtp import DeepSeekV4MTP # type: ignore[assignment]
46 - from .nvidia.vl_model import ( # type: ignore[assignment]
47 - DeepseekV4ForConditionalGeneration,
48 - )
49
50 __all__ = [
51 "DSparkDeepseekV4ForCausalLM",
vllm/models/deepseek_v4/amd/model.py
+23 -1
@@ -79,6 +79,8 @@ from vllm.platforms import current_platform
79 from vllm.platforms.rocm import on_gfx950
80 from vllm.sequence import IntermediateTensors
81
82 +from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID
83 +
84 logger = init_logger(__name__)
85
86
@@ -549,6 +551,10 @@ class DeepseekV4MoE(nn.Module):
551
552 self.gate.e_score_correction_bias = None
553 self.gate.tid2eid = None
554 + self.gate.bias_vl = None
555 + self.image_sentinel_lo = (
556 + IMAGE_SENTINEL_BASE_ID if getattr(config, "vision_n_layers", 0) > 0 else 0
557 + )
558 is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
559 self.hash_indices_dtype = torch.int32
560 if is_hash_moe:
@@ -564,12 +570,20 @@ class DeepseekV4MoE(nn.Module):
570 ),
571 requires_grad=False,
572 )
567 - elif getattr(config, "topk_method", None) == "noaux_tc":
573 + if getattr(config, "topk_method", None) == "noaux_tc" and (
574 + not is_hash_moe or getattr(config, "vision_n_layers", 0) > 0
575 + ):
576 self.gate.e_score_correction_bias = nn.Parameter(
577 torch.empty(config.n_routed_experts, dtype=torch.float32),
578 requires_grad=False,
579 )
580
581 + if getattr(config, "vision_n_layers", 0) > 0:
582 + self.gate.bias_vl = nn.Parameter(
583 + torch.empty(config.n_routed_experts, dtype=torch.float32),
584 + requires_grad=False,
585 + )
586 +
587 self.n_shared_experts = config.n_shared_experts
588
589 # TODO: Historically, only `VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1`
@@ -638,6 +652,8 @@ class DeepseekV4MoE(nn.Module):
652 routed_scaling_factor=self.routed_scaling_factor,
653 e_score_correction_bias=self.gate.e_score_correction_bias,
654 hash_indices_table=self.gate.tid2eid,
655 + bias_vl=self.gate.bias_vl,
656 + image_sentinel_lo=self.image_sentinel_lo,
657 swiglu_limit=self.swiglu_limit,
658 router_logits_dtype=torch.float32,
659 routed_experts_cls=(
@@ -660,6 +676,8 @@ class DeepseekV4MoE(nn.Module):
676 ) -> torch.Tensor:
677 if self.gate.tid2eid is not None and input_ids is None:
678 raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
679 + if self.gate.bias_vl is not None and input_ids is None:
680 + raise ValueError("DeepSeek V4 vision MoE routing requires input_ids.")
681
682 org_shape = hidden_states.shape
683 final_hidden_states = self.experts(
@@ -1351,6 +1369,7 @@ def _make_deepseek_v4_weights_mapper(
1369
1370 class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
1371 model_cls = DeepseekV4Model
1372 + finalizes_weights_during_load = False
1373
1374 # Default mapper assumes the original FP4-expert checkpoint layout.
1375 # Overridden per-instance in __init__ when expert_dtype != "fp4".
@@ -1397,6 +1416,9 @@ class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
1416 logits = self.logits_processor(self.lm_head, hidden_states)
1417 return logits
1418
1419 + def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
1420 + return self.logits_processor(self.lm_head, hidden_states, skip_gather=True)
1421 +
1422 def forward(
1423 self,
1424 input_ids: torch.Tensor,
vllm/models/deepseek_v4/amd/mtp.py
+1 -1
@@ -160,7 +160,7 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
160 inputs_embeds
161 ).unsqueeze(-2)
162 hidden_states, residual, post_mix, res_mix = self.mtp_block(
163 - positions=positions, x=hidden_states, input_ids=None
163 + positions=positions, x=hidden_states, input_ids=input_ids
164 )
165 if self.mtp_block.use_fused_mhc:
166 hidden_states = self.mtp_block.hc_post(
vllm/models/deepseek_v4/amd/rocm.py
+61 -7
@@ -67,8 +67,13 @@ def weight_already_preshuffled(linear: torch.nn.Module) -> bool:
67 The hand-shuffles below (fused_wqa_wkv, wo_b, gate_up_proj) must be skipped
68 for those, since shuffle_weight is a permutation rather than an involution.
69 """
70 - kernel = getattr(getattr(linear, "quant_method", None), "fp8_linear", None)
71 - return bool(getattr(kernel, "preshuffles_weight", False))
70 + return any(
71 + getattr(getattr(method, "fp8_linear", None), "preshuffles_weight", False)
72 + for method in (
73 + getattr(linear, "quant_method", None),
74 + getattr(linear, "scheme", None),
75 + )
76 + )
77
78
79 def apply_pre_quantized_block_scaled_mm(
@@ -116,13 +121,18 @@ def _combine_topk_swa_indices_kernel(
121 query_start_loc_ptr,
122 seq_lens_ptr,
123 gather_lens_ptr,
124 + left_visible_ptr,
125 + right_visible_ptr,
126 M,
127 N,
128 TOP_K: tl.constexpr,
129 COMPRESS_RATIO: tl.constexpr,
130 WINDOW_SIZE: tl.constexpr,
131 + SWA_WIDTH: tl.constexpr,
132 TOPK_WIDTH: tl.constexpr,
133 PADDED_TOP_K: tl.constexpr,
134 + PADDED_SWA_WIDTH: tl.constexpr,
135 + HAS_IMAGE: tl.constexpr,
136 ):
137 batch_idx = tl.program_id(0)
138 worker_id = tl.program_id(1)
@@ -141,7 +151,20 @@ def _combine_topk_swa_indices_kernel(
151 token_idx_in_query = token_idx - query_start
152 pos = start_pos + token_idx_in_query
153 topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
144 - swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
154 + if HAS_IMAGE:
155 + left = tl.load(left_visible_ptr + token_idx)
156 + right = tl.load(right_visible_ptr + token_idx)
157 + else:
158 + left = 0
159 + right = 0
160 + left_add = tl.maximum(left - (WINDOW_SIZE - 1), 0)
161 + # Prefix caching can resume inside an image span. Do not generate
162 + # indices outside the SWA rows present in the gathered workspace.
163 + swa_start = tl.maximum(
164 + tl.maximum(pos - (WINDOW_SIZE - 1) - left_add, 0), gather_start
165 + )
166 + swa_end = tl.minimum(pos + right + 1, seq_len)
167 + swa_len = tl.maximum(swa_end - swa_start, 0)
168
169 topk_offset = tl.arange(0, PADDED_TOP_K)
170 topk_mask = topk_offset < topk_len
@@ -159,14 +182,14 @@ def _combine_topk_swa_indices_kernel(
182 mask=topk_mask,
183 )
184
162 - swa_offset = tl.arange(0, WINDOW_SIZE)
185 + swa_offset = tl.arange(0, PADDED_SWA_WIDTH)
186 tl.store(
187 combined_indices_ptr
188 + token_idx * combined_indices_stride
189 + topk_len
190 + swa_offset,
168 - M * batch_idx + N + swa_offset + pos - swa_len + 1 - gather_start,
169 - mask=swa_offset < swa_len,
191 + M * batch_idx + N + swa_offset + swa_start - gather_start,
192 + mask=(swa_offset < swa_len) & (swa_offset < SWA_WIDTH),
193 )
194
195 tl.store(combined_lens_ptr + token_idx, topk_len + swa_len)
@@ -182,12 +205,21 @@ def combine_topk_swa_indices(
205 topk: int,
206 M: int,
207 N: int,
208 + max_image_tokens: int = 0,
209 + left_visible: torch.Tensor | None = None,
210 + right_visible: torch.Tensor | None = None,
211 ) -> tuple[torch.Tensor, torch.Tensor]:
212 + if (left_visible is None) != (right_visible is None):
213 + raise ValueError("left_visible and right_visible must be provided together")
214 topk_indices = topk_indices.reshape(topk_indices.shape[0], -1).contiguous()
215 num_tokens = topk_indices.shape[0]
216 num_reqs = seq_lens.shape[0]
217 + has_image = left_visible is not None
218 + # Keep the row shape fixed for a vision model even when a particular batch
219 + # has no image.
220 + swa_width = window_size + max_image_tokens
221 combined_topk = (
190 - (topk + window_size + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
222 + (topk + swa_width + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
223 // _SPARSE_PREFILL_TOPK_ALIGNMENT
224 * _SPARSE_PREFILL_TOPK_ALIGNMENT
225 )
@@ -211,13 +243,18 @@ def combine_topk_swa_indices(
243 query_start_loc,
244 seq_lens,
245 gather_lens,
246 + left_visible if left_visible is not None else topk_indices,
247 + right_visible if right_visible is not None else topk_indices,
248 M,
249 N,
250 TOP_K=topk,
251 COMPRESS_RATIO=compress_ratio,
252 WINDOW_SIZE=window_size,
253 + SWA_WIDTH=swa_width,
254 TOPK_WIDTH=topk_indices.shape[-1],
255 PADDED_TOP_K=triton.next_power_of_2(topk_indices.shape[-1]),
256 + PADDED_SWA_WIDTH=triton.next_power_of_2(swa_width),
257 + HAS_IMAGE=has_image,
258 )
259 return combined_indices, combined_lens
260
@@ -948,6 +985,12 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
985 assert query_start_loc_cpu is not None
986 assert query_start_loc is not None
987 prefill_token_base = query_start_loc_cpu[num_decodes]
988 + left_visible = swa_metadata.prefill_left_visible
989 + right_visible = swa_metadata.prefill_right_visible
990 + if left_visible is not None:
991 + left_visible = left_visible[num_decode_tokens:]
992 + assert right_visible is not None
993 + right_visible = right_visible[num_decode_tokens:]
994
995 if not swa_only:
996 if self.compress_ratio == 4:
@@ -1026,6 +1069,17 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
1069 top_k,
1070 M,
1071 N,
1072 + max_image_tokens=self.max_image_tokens,
1073 + left_visible=(
1074 + left_visible[query_start:query_end]
1075 + if left_visible is not None
1076 + else None
1077 + ),
1078 + right_visible=(
1079 + right_visible[query_start:query_end]
1080 + if right_visible is not None
1081 + else None
1082 + ),
1083 )
1084 rocm_sparse_attn_prefill(
1085 q=q[query_start:query_end],
vllm/models/deepseek_v4/common/vl_model.py new
+331
@@ -0,0 +1,331 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""DeepSeek-V4 vision variant (e.g. DeepSeek-V4-Flash-Vision-Exp).
4 +
5 +Thin multimodal wrapper around the text-only ``DeepseekV4ForCausalLM``:
6 +
7 +- ``vision`` ViT + ``aligner`` produce per-image embeddings for the IMAGE
8 + sentinel positions; four learned vectors (``image_start`` / ``image_pad`` /
9 + ``image_newline`` / ``image_end``) fill the remaining sentinel positions.
10 +- Image placeholders (``<|deepseek_image|>``) are expanded by the processor
11 + in ``common/mm_preprocess.py`` into sentinel blocks borrowing reserved
12 + in-vocab tokens ``<|place_holder_mm_span_0431|>``..``_0435|>``
13 + (see ``common/vision.py`` for the tower itself).
14 +- Merged embeddings enter the text model via ``inputs_embeds``, i.e. before
15 + its hyper-connection stream expansion. Raw ``input_ids`` still flow into
16 + the model so the MoE router can apply ``bias_vl`` to image tokens
17 + (``requires_raw_input_tokens``).
18 +"""
19 +
20 +from collections.abc import Iterable
21 +
22 +import torch
23 +from torch import nn
24 +
25 +from vllm.model_executor.models.interfaces import (
26 + MultiModalEmbeddings,
27 + SupportsEagle3,
28 + SupportsMultiModal,
29 + SupportsPP,
30 +)
31 +from vllm.model_executor.models.utils import (
32 + AutoWeightsLoader,
33 + WeightsMapper,
34 + init_vllm_registered_model,
35 + maybe_prefix,
36 +)
37 +from vllm.multimodal import MULTIMODAL_REGISTRY
38 +
39 +from .mm_preprocess import (
40 + IMAGE_PLACEHOLDER,
41 + IMAGE_SENTINEL_BASE_ID,
42 + DeepseekV4VLDummyInputsBuilder,
43 + DeepseekV4VLMultiModalProcessor,
44 + DeepseekV4VLProcessingInfo,
45 + image_sentinel_mask,
46 +)
47 +from .vision import DeepseekV4Aligner, DeepseekV4ViT
48 +
49 +
50 +def _make_deepseek_v4_vl_weights_mapper(
51 + text_mapper: WeightsMapper, image_enabled: bool
52 +) -> WeightsMapper:
53 + """Text-checkpoint mapping rules re-rooted under ``language_model.``."""
54 + orig_to_new_prefix = {
55 + src: None if dst is None else f"language_model.{dst}"
56 + for src, dst in text_mapper.orig_to_new_prefix.items()
57 + }
58 + if not image_enabled:
59 + orig_to_new_prefix.update({"vision.": None, "aligner.": None, "image_": None})
60 + return WeightsMapper(
61 + orig_to_new_renaming=text_mapper.orig_to_new_renaming,
62 + orig_to_new_prefix=orig_to_new_prefix,
63 + orig_to_new_regex=text_mapper.orig_to_new_regex,
64 + orig_to_new_stacked=text_mapper.orig_to_new_stacked,
65 + orig_to_new_suffix={
66 + **text_mapper.orig_to_new_suffix,
67 + "head.weight": "language_model.lm_head.weight",
68 + },
69 + orig_to_new_substr={
70 + **text_mapper.orig_to_new_substr,
71 + # Draft models load the checkpoint's MTP weights separately from
72 + # the target model.
73 + "mtp.": None,
74 + },
75 + )
76 +
77 +
78 +@MULTIMODAL_REGISTRY.register_processor(
79 + DeepseekV4VLMultiModalProcessor,
80 + info=DeepseekV4VLProcessingInfo,
81 + dummy_inputs=DeepseekV4VLDummyInputsBuilder,
82 +)
83 +class DeepseekV4ForConditionalGeneration(
84 + nn.Module, SupportsMultiModal, SupportsPP, SupportsEagle3
85 +):
86 + """Multimodal entry point for DeepSeek-V4 checkpoints with a vision tower.
87 +
88 + ``SupportsEagle3`` (aux hidden-state plumbing for MTP/DSpark drafters)
89 + delegates through ``language_model`` via the protocol defaults.
90 + """
91 +
92 + # The MoE router needs raw token ids to detect image sentinel tokens
93 + # (borrowed reserved ids, see common/mm_preprocess.py) and apply bias_vl.
94 + requires_raw_input_tokens = True
95 +
96 + @classmethod
97 + def get_placeholder_str(cls, modality: str, i: int) -> str | None:
98 + if modality == "image":
99 + return IMAGE_PLACEHOLDER
100 + raise ValueError(f"Unsupported modality: {modality!r}")
101 +
102 + def __init__(self, *, vllm_config, prefix: str = "") -> None:
103 + super().__init__()
104 + model_config = vllm_config.model_config
105 + config = model_config.hf_config
106 + self.config = config
107 + self.multimodal_config = model_config.multimodal_config
108 + assert self.multimodal_config is not None
109 +
110 + image_enabled = (
111 + config.vision_n_layers > 0
112 + and self.multimodal_config.get_limit_per_prompt("image") > 0
113 + )
114 + with self._mark_tower_model(vllm_config, {"image"}):
115 + self.vision: DeepseekV4ViT | None = None
116 + self.aligner: DeepseekV4Aligner | None = None
117 + self.image_start: nn.Parameter | None = None
118 + self.image_end: nn.Parameter | None = None
119 + self.image_newline: nn.Parameter | None = None
120 + self.image_pad: nn.Parameter | None = None
121 + if image_enabled:
122 + self.vision = DeepseekV4ViT(config)
123 + self.aligner = DeepseekV4Aligner(config)
124 + for name in (
125 + "image_start",
126 + "image_end",
127 + "image_newline",
128 + "image_pad",
129 + ):
130 + setattr(
131 + self,
132 + name,
133 + nn.Parameter(
134 + torch.empty(config.hidden_size, dtype=torch.float32)
135 + ),
136 + )
137 + self.vision.to(dtype=model_config.dtype)
138 + self.aligner.to(dtype=model_config.dtype)
139 +
140 + with self._mark_language_model(vllm_config):
141 + # The arch convertor routes any config with a vision tower to
142 + # this wrapper class; mark the copy handed to the text backbone
143 + # so it resolves to DeepseekV4ForCausalLM instead of recursing
144 + # (with_hf_config deepcopies the config, the marker survives).
145 + config._dsv4_vl_inner = True # type: ignore[attr-defined]
146 + try:
147 + self.language_model = init_vllm_registered_model(
148 + vllm_config=vllm_config,
149 + hf_config=config,
150 + prefix=maybe_prefix(prefix, "language_model"),
151 + architectures=["DeepseekV4ForCausalLM"],
152 + )
153 + finally:
154 + del config._dsv4_vl_inner # type: ignore[attr-defined]
155 + # The outer mapper (see load_weights) fully resolves HF names into
156 + # this wrapper's namespace before AutoWeightsLoader strips the
157 + # "language_model." prefix and delegates to the child's load_weights,
158 + # so the child's own mapper must be a no-op. Its suffix rules are not
159 + # idempotent (e.g. "lm_head.weight".endswith("head.weight") would
160 + # re-fire "head.weight" -> "lm_head.weight").
161 + text_mapper = self.language_model.hf_to_vllm_mapper
162 + self.language_model.hf_to_vllm_mapper = WeightsMapper()
163 + self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
164 + self.language_model.make_empty_intermediate_tensors
165 + )
166 +
167 + self.hf_to_vllm_mapper = _make_deepseek_v4_vl_weights_mapper(
168 + text_mapper, image_enabled
169 + )
170 + self._weights_finalized = False
171 +
172 + def _parse_and_validate_image_input(self, **kwargs: object) -> dict | None:
173 + patches = kwargs.pop("patches", None)
174 + if patches is None:
175 + return None
176 + vit_grid = kwargs.pop("vit_grid", None)
177 + llm_grid = kwargs.pop("llm_grid", None)
178 + perm = kwargs.pop("perm", None)
179 + assert vit_grid is not None and llm_grid is not None and perm is not None
180 + return {
181 + "patches": patches,
182 + "vit_grid": vit_grid,
183 + "llm_grid": llm_grid,
184 + "perm": perm,
185 + }
186 +
187 + def _encode_image(
188 + self,
189 + patches: torch.Tensor,
190 + n_vit_h: int,
191 + n_vit_w: int,
192 + perm: torch.Tensor,
193 + ) -> torch.Tensor:
194 + assert self.vision is not None and self.aligner is not None
195 + image_embeds = self.aligner(
196 + self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w
197 + )
198 + # Reorder into the N-layout block order used in the prompt.
199 + return image_embeds[perm.to(image_embeds.device)]
200 +
201 + def _process_image_input(
202 + self,
203 + patches: torch.Tensor,
204 + vit_grid: torch.Tensor,
205 + llm_grid: torch.Tensor,
206 + perm: torch.Tensor,
207 + ) -> tuple[torch.Tensor, ...]:
208 + assert self.vision is not None and self.aligner is not None
209 + patches = patches.to(self.aligner.w1.weight.dtype)
210 +
211 + embeds: list[torch.Tensor] = []
212 + vit_offset = 0
213 + llm_offset = 0
214 + for (n_vit_h, n_vit_w), (n_llm_h, n_llm_w) in zip(
215 + vit_grid.tolist(), llm_grid.tolist(), strict=True
216 + ):
217 + n_vit = n_vit_h * n_vit_w
218 + n_llm = n_llm_h * n_llm_w
219 + embeds.append(
220 + self._encode_image(
221 + patches[vit_offset : vit_offset + n_vit],
222 + n_vit_h,
223 + n_vit_w,
224 + perm[llm_offset : llm_offset + n_llm],
225 + )
226 + )
227 + vit_offset += n_vit
228 + llm_offset += n_llm
229 + return tuple(embeds)
230 +
231 + def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
232 + image_input = self._parse_and_validate_image_input(**kwargs)
233 + if image_input is None or self.vision is None:
234 + return []
235 + return self._process_image_input(
236 + image_input["patches"],
237 + image_input["vit_grid"],
238 + image_input["llm_grid"],
239 + image_input["perm"],
240 + )
241 +
242 + def embed_input_ids(
243 + self,
244 + input_ids: torch.Tensor,
245 + multimodal_embeddings: MultiModalEmbeddings | None = None,
246 + *,
247 + is_multimodal: torch.Tensor | None = None,
248 + ) -> torch.Tensor:
249 + from vllm.model_executor.models.utils import _merge_multimodal_embeddings
250 +
251 + # All ids are in-vocab here: image-block sentinels are borrowed
252 + # reserved tokens (their embedding rows are always overwritten below).
253 + inputs_embeds = self.language_model.embed_input_ids(input_ids)
254 +
255 + if self.image_start is not None:
256 + # Branch-free sentinel overwrite: safe inside compiled/captured
257 + # regions (no data-dependent control flow).
258 + sentinel_mask = image_sentinel_mask(input_ids)
259 + if is_multimodal is not None:
260 + # IMAGE positions get vision embeddings via the merge below.
261 + sentinel_mask = sentinel_mask & ~is_multimodal.to(input_ids.device)
262 + table = torch.stack(
263 + [
264 + self.image_start,
265 + self.image_pad,
266 + self.image_pad,
267 + self.image_newline,
268 + self.image_end,
269 + ]
270 + ).to(inputs_embeds.dtype)
271 + idx = (input_ids - IMAGE_SENTINEL_BASE_ID).clamp(0, 4)
272 + inputs_embeds = torch.where(
273 + sentinel_mask.unsqueeze(-1), table[idx], inputs_embeds
274 + )
275 +
276 + if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
277 + return inputs_embeds
278 +
279 + assert is_multimodal is not None
280 + return _merge_multimodal_embeddings(
281 + inputs_embeds=inputs_embeds,
282 + multimodal_embeddings=multimodal_embeddings,
283 + is_multimodal=is_multimodal,
284 + )
285 +
286 + def forward(
287 + self,
288 + input_ids: torch.Tensor,
289 + positions: torch.Tensor,
290 + intermediate_tensors=None,
291 + inputs_embeds: torch.Tensor | None = None,
292 + **kwargs,
293 + ) -> torch.Tensor:
294 + return self.language_model(
295 + input_ids, positions, intermediate_tensors, inputs_embeds
296 + )
297 +
298 + def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
299 + return self.language_model.compute_logits(hidden_states)
300 +
301 + def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
302 + return self.language_model.compute_logits_local(hidden_states)
303 +
304 + def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
305 + return self.language_model.get_expert_mapping()
306 +
307 + def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
308 + """Pre-hc_head residual stream buffer for the MTP/DSpark draft model."""
309 + return self.language_model.get_mtp_target_hidden_states()
310 +
311 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
312 + child_finalizes = getattr(
313 + self.language_model, "finalizes_weights_during_load", True
314 + )
315 + mapped = self.hf_to_vllm_mapper.apply(weights)
316 + if child_finalizes:
317 + # A child which finalizes inside load_weights must see all of its
318 + # weights in one contiguous delegation from AutoWeightsLoader.
319 + mapped = iter(sorted(mapped, key=lambda x: x[0]))
320 + loader = AutoWeightsLoader(self)
321 + loaded_params = loader.load_weights(mapped)
322 + self._weights_finalized = child_finalizes
323 + return loaded_params
324 +
325 + def process_weights_after_loading(self) -> None:
326 + # Backbones such as the ROCm implementation require this to run only
327 + # after the loader's generic per-layer quantization finalization.
328 + if getattr(self, "_weights_finalized", False):
329 + return
330 + self.language_model.process_weights_after_loading()
331 + self._weights_finalized = True
vllm/models/deepseek_v4/nvidia/model.py
+1
@@ -1790,6 +1790,7 @@ class DeepseekV4ForCausalLM(
1790 SupportsLoRA,
1791 DeepseekV4MixtureOfExperts,
1792 ):
1793 + finalizes_weights_during_load = True
1794 model_cls = DeepseekV4Model
1795
1796 # Default mapper assumes the original FP4-expert checkpoint layout.
vllm/models/deepseek_v4/nvidia/vl_model.py
+10 -322
@@ -1,333 +1,21 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""DeepSeek-V4 vision variant (e.g. DeepSeek-V4-Flash-Vision-Exp).
3 +"""Compatibility imports for the platform-neutral DeepSeek-V4 vision model."""
4
5 -Thin multimodal wrapper around the text-only ``DeepseekV4ForCausalLM``:
6 -
7 -- ``vision`` ViT + ``aligner`` produce per-image embeddings for the IMAGE
8 - sentinel positions; four learned vectors (``image_start`` / ``image_pad`` /
9 - ``image_newline`` / ``image_end``) fill the remaining sentinel positions.
10 -- Image placeholders (``<|deepseek_image|>``) are expanded by the processor
11 - in ``common/mm_preprocess.py`` into sentinel blocks borrowing reserved
12 - in-vocab tokens ``<|place_holder_mm_span_0431|>``..``_0435|>``
13 - (see ``common/vision.py`` for the tower itself).
14 -- Merged embeddings enter the text model via ``inputs_embeds``, i.e. before
15 - its hyper-connection stream expansion. Raw ``input_ids`` still flow into
16 - the model so the MoE router can apply ``bias_vl`` to image tokens
17 - (``requires_raw_input_tokens``).
18 -"""
19 -
20 -from collections.abc import Iterable
21 -
22 -import torch
23 -from torch import nn
24 -
25 -from vllm.model_executor.models.interfaces import (
26 - MultiModalEmbeddings,
27 - SupportsEagle3,
28 - SupportsMultiModal,
29 - SupportsPP,
5 +from ..common.vl_model import (
6 + DeepseekV4ForConditionalGeneration,
7 )
31 -from vllm.model_executor.models.utils import (
32 - AutoWeightsLoader,
33 - WeightsMapper,
34 - init_vllm_registered_model,
35 - maybe_prefix,
8 +from ..common.vl_model import (
9 + _make_deepseek_v4_vl_weights_mapper as _make_common_vl_weights_mapper,
10 )
37 -from vllm.multimodal import MULTIMODAL_REGISTRY
38 -
39 -from ..common.mm_preprocess import (
40 - IMAGE_PLACEHOLDER,
41 - IMAGE_SENTINEL_BASE_ID,
42 - DeepseekV4VLDummyInputsBuilder,
43 - DeepseekV4VLMultiModalProcessor,
44 - DeepseekV4VLProcessingInfo,
45 - image_sentinel_mask,
46 -)
47 -from ..common.vision import DeepseekV4Aligner, DeepseekV4ViT
11 from .model import _make_deepseek_v4_weights_mapper
12
13
51 -def _make_deepseek_v4_vl_weights_mapper(
52 - expert_dtype: str, image_enabled: bool
53 -) -> WeightsMapper:
54 - """Text-checkpoint mapping rules re-rooted under ``language_model.``."""
55 - base = _make_deepseek_v4_weights_mapper(expert_dtype)
56 - orig_to_new_prefix: dict[str, str | None] = {
57 - "layers.": "language_model.model.layers.",
58 - "embed.": "language_model.model.embed.",
59 - "norm.": "language_model.model.norm.",
60 - "hc_head": "language_model.model.hc_head",
61 - "mtp.": "language_model.model.mtp.",
62 - }
63 - if not image_enabled:
64 - orig_to_new_prefix.update({"vision.": None, "aligner.": None, "image_": None})
65 - return WeightsMapper(
66 - orig_to_new_prefix=orig_to_new_prefix,
67 - orig_to_new_regex=base.orig_to_new_regex,
68 - orig_to_new_suffix={
69 - "head.weight": "language_model.lm_head.weight",
70 - "embed.weight": "embed_tokens.weight",
71 - ".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
72 - },
73 - orig_to_new_substr={
74 - ".shared_experts.w2": ".shared_experts.down_proj",
75 - # The MTP/DSpark draft heads are not supported for the vision
76 - # variant; drop their weights.
77 - "mtp.": None,
78 - },
14 +def _make_deepseek_v4_vl_weights_mapper(expert_dtype: str, image_enabled: bool):
15 + """Retain the original NVIDIA helper signature for downstream imports."""
16 + return _make_common_vl_weights_mapper(
17 + _make_deepseek_v4_weights_mapper(expert_dtype), image_enabled
18 )
19
20
82 -@MULTIMODAL_REGISTRY.register_processor(
83 - DeepseekV4VLMultiModalProcessor,
84 - info=DeepseekV4VLProcessingInfo,
85 - dummy_inputs=DeepseekV4VLDummyInputsBuilder,
86 -)
87 -class DeepseekV4ForConditionalGeneration(
88 - nn.Module, SupportsMultiModal, SupportsPP, SupportsEagle3
89 -):
90 - """Multimodal entry point for DeepSeek-V4 checkpoints with a vision tower.
91 -
92 - ``SupportsEagle3`` (aux hidden-state plumbing for MTP/DSpark drafters)
93 - delegates through ``language_model`` via the protocol defaults.
94 - """
95 -
96 - # The MoE router needs raw token ids to detect image sentinel tokens
97 - # (borrowed reserved ids, see common/mm_preprocess.py) and apply bias_vl.
98 - requires_raw_input_tokens = True
99 -
100 - @classmethod
101 - def get_placeholder_str(cls, modality: str, i: int) -> str | None:
102 - if modality == "image":
103 - return IMAGE_PLACEHOLDER
104 - raise ValueError(f"Unsupported modality: {modality!r}")
105 -
106 - def __init__(self, *, vllm_config, prefix: str = "") -> None:
107 - super().__init__()
108 - model_config = vllm_config.model_config
109 - config = model_config.hf_config
110 - self.config = config
111 - self.multimodal_config = model_config.multimodal_config
112 - assert self.multimodal_config is not None
113 -
114 - image_enabled = (
115 - config.vision_n_layers > 0
116 - and self.multimodal_config.get_limit_per_prompt("image") > 0
117 - )
118 - with self._mark_tower_model(vllm_config, {"image"}):
119 - self.vision: DeepseekV4ViT | None = None
120 - self.aligner: DeepseekV4Aligner | None = None
121 - self.image_start: nn.Parameter | None = None
122 - self.image_end: nn.Parameter | None = None
123 - self.image_newline: nn.Parameter | None = None
124 - self.image_pad: nn.Parameter | None = None
125 - if image_enabled:
126 - self.vision = DeepseekV4ViT(config)
127 - self.aligner = DeepseekV4Aligner(config)
128 - for name in (
129 - "image_start",
130 - "image_end",
131 - "image_newline",
132 - "image_pad",
133 - ):
134 - setattr(
135 - self,
136 - name,
137 - nn.Parameter(
138 - torch.empty(config.hidden_size, dtype=torch.float32)
139 - ),
140 - )
141 - self.vision.to(dtype=model_config.dtype)
142 - self.aligner.to(dtype=model_config.dtype)
143 -
144 - with self._mark_language_model(vllm_config):
145 - # The arch convertor routes any config with a vision tower to
146 - # this wrapper class; mark the copy handed to the text backbone
147 - # so it resolves to DeepseekV4ForCausalLM instead of recursing
148 - # (with_hf_config deepcopies the config, the marker survives).
149 - config._dsv4_vl_inner = True # type: ignore[attr-defined]
150 - try:
151 - self.language_model = init_vllm_registered_model(
152 - vllm_config=vllm_config,
153 - hf_config=config,
154 - prefix=maybe_prefix(prefix, "language_model"),
155 - architectures=["DeepseekV4ForCausalLM"],
156 - )
157 - finally:
158 - del config._dsv4_vl_inner # type: ignore[attr-defined]
159 - # The outer mapper (see load_weights) fully resolves HF names into
160 - # this wrapper's namespace before AutoWeightsLoader strips the
161 - # "language_model." prefix and delegates to the child's load_weights,
162 - # so the child's own mapper must be a no-op. Its suffix rules are not
163 - # idempotent (e.g. "lm_head.weight".endswith("head.weight") would
164 - # re-fire "head.weight" -> "lm_head.weight").
165 - self.language_model.hf_to_vllm_mapper = WeightsMapper()
166 - self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
167 - self.language_model.make_empty_intermediate_tensors
168 - )
169 -
170 - expert_dtype = getattr(config, "expert_dtype", "fp4")
171 - self.hf_to_vllm_mapper = _make_deepseek_v4_vl_weights_mapper(
172 - expert_dtype, image_enabled
173 - )
174 -
175 - def _parse_and_validate_image_input(self, **kwargs: object) -> dict | None:
176 - patches = kwargs.pop("patches", None)
177 - if patches is None:
178 - return None
179 - vit_grid = kwargs.pop("vit_grid", None)
180 - llm_grid = kwargs.pop("llm_grid", None)
181 - perm = kwargs.pop("perm", None)
182 - assert vit_grid is not None and llm_grid is not None and perm is not None
183 - return {
184 - "patches": patches,
185 - "vit_grid": vit_grid,
186 - "llm_grid": llm_grid,
187 - "perm": perm,
188 - }
189 -
190 - def _encode_image(
191 - self,
192 - patches: torch.Tensor,
193 - n_vit_h: int,
194 - n_vit_w: int,
195 - perm: torch.Tensor,
196 - ) -> torch.Tensor:
197 - assert self.vision is not None and self.aligner is not None
198 - image_embeds = self.aligner(
199 - self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w
200 - )
201 - # Reorder into the N-layout block order used in the prompt.
202 - return image_embeds[perm.to(image_embeds.device)]
203 -
204 - def _process_image_input(
205 - self,
206 - patches: torch.Tensor,
207 - vit_grid: torch.Tensor,
208 - llm_grid: torch.Tensor,
209 - perm: torch.Tensor,
210 - ) -> tuple[torch.Tensor, ...]:
211 - assert self.vision is not None and self.aligner is not None
212 - patches = patches.to(self.aligner.w1.weight.dtype)
213 -
214 - embeds: list[torch.Tensor] = []
215 - vit_offset = 0
216 - llm_offset = 0
217 - for (n_vit_h, n_vit_w), (n_llm_h, n_llm_w) in zip(
218 - vit_grid.tolist(), llm_grid.tolist(), strict=True
219 - ):
220 - n_vit = n_vit_h * n_vit_w
221 - n_llm = n_llm_h * n_llm_w
222 - embeds.append(
223 - self._encode_image(
224 - patches[vit_offset : vit_offset + n_vit],
225 - n_vit_h,
226 - n_vit_w,
227 - perm[llm_offset : llm_offset + n_llm],
228 - )
229 - )
230 - vit_offset += n_vit
231 - llm_offset += n_llm
232 - return tuple(embeds)
233 -
234 - def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
235 - image_input = self._parse_and_validate_image_input(**kwargs)
236 - if image_input is None or self.vision is None:
237 - return []
238 - return self._process_image_input(
239 - image_input["patches"],
240 - image_input["vit_grid"],
241 - image_input["llm_grid"],
242 - image_input["perm"],
243 - )
244 -
245 - def embed_input_ids(
246 - self,
247 - input_ids: torch.Tensor,
248 - multimodal_embeddings: MultiModalEmbeddings | None = None,
249 - *,
250 - is_multimodal: torch.Tensor | None = None,
251 - ) -> torch.Tensor:
252 - from vllm.model_executor.models.utils import _merge_multimodal_embeddings
253 -
254 - # All ids are in-vocab here: image-block sentinels are borrowed
255 - # reserved tokens (their embedding rows are always overwritten below).
256 - inputs_embeds = self.language_model.embed_input_ids(input_ids)
257 -
258 - if self.image_start is not None:
259 - # Branch-free sentinel overwrite: safe inside compiled/captured
260 - # regions (no data-dependent control flow).
261 - sentinel_mask = image_sentinel_mask(input_ids)
262 - if is_multimodal is not None:
263 - # IMAGE positions get vision embeddings via the merge below.
264 - sentinel_mask = sentinel_mask & ~is_multimodal.to(input_ids.device)
265 - table = torch.stack(
266 - [
267 - self.image_start,
268 - self.image_pad,
269 - self.image_pad,
270 - self.image_newline,
271 - self.image_end,
272 - ]
273 - ).to(inputs_embeds.dtype)
274 - idx = (input_ids - IMAGE_SENTINEL_BASE_ID).clamp(0, 4)
275 - inputs_embeds = torch.where(
276 - sentinel_mask.unsqueeze(-1), table[idx], inputs_embeds
277 - )
278 -
279 - if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
280 - return inputs_embeds
281 -
282 - assert is_multimodal is not None
283 - return _merge_multimodal_embeddings(
284 - inputs_embeds=inputs_embeds,
285 - multimodal_embeddings=multimodal_embeddings,
286 - is_multimodal=is_multimodal,
287 - )
288 -
289 - def forward(
290 - self,
291 - input_ids: torch.Tensor,
292 - positions: torch.Tensor,
293 - intermediate_tensors=None,
294 - inputs_embeds: torch.Tensor | None = None,
295 - **kwargs,
296 - ) -> torch.Tensor:
297 - return self.language_model(
298 - input_ids, positions, intermediate_tensors, inputs_embeds
299 - )
300 -
301 - def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
302 - return self.language_model.compute_logits(hidden_states)
303 -
304 - def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
305 - return self.language_model.compute_logits_local(hidden_states)
306 -
307 - def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
308 - return self.language_model.get_expert_mapping()
309 -
310 - def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
311 - """Pre-hc_head residual stream buffer for the MTP/DSpark draft model."""
312 - return self.language_model.get_mtp_target_hidden_states()
313 -
314 - def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
315 - # Map HF names into this wrapper's namespace up front and sort, so
316 - # the "language_model." group reaches the child loader as one
317 - # contiguous block (AutoWeightsLoader delegates per contiguous group,
318 - # and the child's load_weights finalizes fused expert weights, which
319 - # must not run on a partially loaded model).
320 - mapped = sorted(self.hf_to_vllm_mapper.apply(weights), key=lambda x: x[0])
321 - loader = AutoWeightsLoader(self)
322 - loaded_params = loader.load_weights(mapped)
323 - # The child's load_weights already ran its post-load finalization.
324 - self._weights_finalized = True
325 - return loaded_params
326 -
327 - def process_weights_after_loading(self) -> None:
328 - # Model-level post-load hook (called by the loader after any load
329 - # format). Under DummyModelLoader the child's load_weights — and
330 - # hence its finalize step — is bypassed, so run it here instead.
331 - if getattr(self, "_weights_finalized", False):
332 - return
333 - self.language_model.process_weights_after_loading()
21 +__all__ = ["DeepseekV4ForConditionalGeneration"]
vllm/models/deepseek_v4/vl_stub.py
+2 -2
@@ -9,6 +9,6 @@ class DeepseekV4ForConditionalGeneration(nn.Module):
9 def __init__(self, *, vllm_config, prefix: str = ""):
10 super().__init__()
11 raise NotImplementedError(
12 - "DeepSeek-V4 vision (DeepseekV4ForConditionalGeneration) is only "
13 - "supported on NVIDIA GPUs for now."
12 + "DeepSeek-V4 vision (DeepseekV4ForConditionalGeneration) is not "
13 + "supported on the current platform; use CUDA or ROCm."
14 )
vllm/platforms/rocm.py
+15
@@ -943,6 +943,21 @@ class RocmPlatform(Platform):
943 if parallel_config.worker_cls == "auto":
944 parallel_config.worker_cls = "vllm.v1.worker.gpu_worker.Worker"
945
946 + model_config = vllm_config.model_config
947 + scheduler_config = vllm_config.scheduler_config
948 + # Note: model_config may be None during testing
949 + if (
950 + model_config is not None
951 + and model_config.is_mm_prefix_lm
952 + and scheduler_config.is_multimodal_model
953 + and not scheduler_config.disable_chunked_mm_input
954 + ):
955 + logger.warning_once(
956 + "Forcing --disable_chunked_mm_input for models "
957 + "with multimodal-bidirectional attention."
958 + )
959 + scheduler_config.disable_chunked_mm_input = True
960 +
961 @classmethod
962 def verify_model_arch(cls, model_arch: str) -> None:
963 if model_arch in _ROCM_UNSUPPORTED_MODELS: