Add Cosmos3 Edge Reasoner model (#48291)
Signed-off-by: Bartosz Stefaniak <bstefaniak@nvidia.com> Co-authored-by: Bartosz Stefaniak <bstefaniak@nvidia.com>
adsridhar committed
Jul 14, 2026 at 01:14 UTC
894ebb27f5c7d7f8758266163ffd503132e432d2
13 files changed
+1474
-27
docs/models/supported_models.md
+1
@@ -514,6 +514,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
514
| `CheersForConditionalGeneration` | Cheers | T + I | `ai9stars/Cheers` | | ✅︎ |
515
| `Cohere2VisionForConditionalGeneration` | Command A Vision, Command-A+ | T + I<sup>+</sup> | `CohereLabs/command-a-vision-07-2025`, `CohereLabs/command-a-plus-05-2026`, etc. | | ✅︎ |
516
| `Cosmos3ForConditionalGeneration` | Cosmos3 (understanding tower) | T + I<sup>E+</sup> + V<sup>E+</sup> | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | | ✅︎ |
517
+| `Cosmos3EdgeForConditionalGeneration` | Cosmos3-Edge (understanding tower) | T + I<sup>E+</sup> + V<sup>E+</sup> | `nvidia/Cosmos3-Edge` | | ✅︎ |
518
| `DeepseekVLV2ForCausalLM` | DeepSeek-VL2 | T + I<sup>+</sup> | `deepseek-ai/deepseek-vl2-tiny`, `deepseek-ai/deepseek-vl2-small`, `deepseek-ai/deepseek-vl2`, etc. | | ✅︎ |
519
| `DeepseekOCRForCausalLM` | DeepSeek-OCR | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR`, etc. | ✅︎ | ✅︎ |
520
| `DeepseekOCR2ForCausalLM` | DeepSeek-OCR-2 | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR-2`, etc. | ✅︎ | ✅︎ |
tests/models/multimodal/processing/test_cosmos3_edge.py
new
+101
@@ -0,0 +1,101 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import os
5
+
6
+import pytest
7
+
8
+from vllm.assets.video import VideoAsset
9
+from vllm.config import ModelConfig
10
+from vllm.multimodal import MULTIMODAL_REGISTRY
11
+
12
+from ....conftest import ImageTestAssets
13
+from ...utils import build_model_context
14
+
15
+MODEL_ID = "nvidia/Cosmos3-Edge"
16
+IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>"
17
+VIDEO_PLACEHOLDER = "<|vision_start|><|video_pad|><|vision_end|>"
18
+LOCAL_MODEL_PATH = os.getenv("COSMOS3_EDGE_MODEL_PATH")
19
+
20
+
21
+@pytest.fixture(scope="module")
22
+def processor():
23
+ if LOCAL_MODEL_PATH:
24
+ model_config = ModelConfig(
25
+ LOCAL_MODEL_PATH,
26
+ tokenizer=LOCAL_MODEL_PATH,
27
+ max_model_len=4096,
28
+ limit_mm_per_prompt={"image": 2, "video": 1},
29
+ )
30
+ else:
31
+ ctx = build_model_context(
32
+ MODEL_ID,
33
+ limit_mm_per_prompt={"image": 2, "video": 1},
34
+ )
35
+ model_config = ctx.model_config
36
+
37
+ return MULTIMODAL_REGISTRY.create_processor(model_config)
38
+
39
+
40
+def _assert_image_outputs(processor, processed, num_images: int) -> None:
41
+ mm_data = processed["mm_kwargs"].get_data()
42
+ grid_thw = mm_data["image_grid_thw"]
43
+ pixel_values = mm_data["pixel_values"]
44
+
45
+ assert grid_thw.shape == (num_images, 3)
46
+ assert pixel_values.shape[0] == int(grid_thw.prod(dim=-1).sum())
47
+
48
+ merge_size = processor.info.get_hf_config().vision_config.spatial_merge_size
49
+ expected_tokens = (grid_thw.prod(dim=-1) // merge_size**2).tolist()
50
+ image_placeholders = processed["mm_placeholders"]["image"]
51
+
52
+ assert len(image_placeholders) == num_images
53
+ assert [placeholder.length for placeholder in image_placeholders] == (
54
+ expected_tokens
55
+ )
56
+
57
+ image_token_id = processor.info.get_hf_processor().image_token_id
58
+ assert processed["prompt_token_ids"].count(image_token_id) == sum(expected_tokens)
59
+
60
+
61
+def _assert_video_outputs(processor, processed) -> None:
62
+ mm_data = processed["mm_kwargs"].get_data()
63
+ grid_thw = mm_data["video_grid_thw"]
64
+ pixel_values = mm_data["pixel_values_videos"]
65
+
66
+ assert grid_thw.shape == (1, 3)
67
+ assert pixel_values.shape[0] == int(grid_thw.prod())
68
+ assert len(processed["mm_placeholders"]["video"]) == 1
69
+
70
+ merge_size = processor.info.get_hf_config().vision_config.spatial_merge_size
71
+ expected_tokens = int(grid_thw.prod()) // merge_size**2
72
+ video_token_id = processor.info.get_hf_config().video_token_id
73
+ assert processed["prompt_token_ids"].count(video_token_id) == expected_tokens
74
+
75
+
76
+@pytest.mark.parametrize("num_images", [1, 2])
77
+def test_process_images(
78
+ processor,
79
+ image_assets: ImageTestAssets,
80
+ num_images: int,
81
+) -> None:
82
+ images = [asset.pil_image for asset in image_assets[:num_images]]
83
+ processed = processor(
84
+ IMAGE_PLACEHOLDER * num_images,
85
+ mm_items=processor.info.parse_mm_data({"image": images}),
86
+ hf_processor_mm_kwargs={},
87
+ )
88
+
89
+ _assert_image_outputs(processor, processed, num_images)
90
+
91
+
92
+def test_process_video(processor) -> None:
93
+ video_asset = VideoAsset(name="baby_reading", num_frames=8)
94
+ video = (video_asset.np_ndarrays, video_asset.metadata)
95
+ processed = processor(
96
+ VIDEO_PLACEHOLDER,
97
+ mm_items=processor.info.parse_mm_data({"video": [video]}),
98
+ hf_processor_mm_kwargs={},
99
+ )
100
+
101
+ _assert_video_outputs(processor, processed)
tests/models/multimodal/test_mapping.py
+71
@@ -74,6 +74,77 @@ def test_cosmos3_new_checkpoint_weights_mapper():
74
)
75
76
77
+def test_cosmos3_edge_checkpoint_weights_mapper():
78
+ from vllm.model_executor.models.cosmos3_edge import (
79
+ Cosmos3EdgeForConditionalGeneration,
80
+ )
81
+
82
+ mapper = Cosmos3EdgeForConditionalGeneration.hf_to_vllm_mapper
83
+
84
+ assert mapper.apply_list(
85
+ [
86
+ "embed_tokens.weight",
87
+ "norm.weight",
88
+ "layers.0.input_layernorm.weight",
89
+ "layers.0.self_attn.to_q.weight",
90
+ "layers.0.self_attn.to_k.weight",
91
+ "layers.0.self_attn.to_v.weight",
92
+ "layers.0.self_attn.to_out.weight",
93
+ "layers.0.post_attention_layernorm.weight",
94
+ "layers.0.mlp.up_proj.weight",
95
+ "layers.0.mlp.down_proj.weight",
96
+ "layers.27.input_layernorm.weight",
97
+ "layers.27.self_attn.to_q.weight",
98
+ "layers.27.post_attention_layernorm.weight",
99
+ "layers.27.mlp.down_proj.weight",
100
+ "model.visual.embeddings.patch_embedding.weight",
101
+ "model.visual.encoder.layers.0.self_attn.q_proj.weight",
102
+ "model.projector.linear_fc1.weight",
103
+ "lm_head.weight",
104
+ ]
105
+ ) == [
106
+ "language_model.model.embed_tokens.weight",
107
+ "language_model.model.norm_f.weight",
108
+ "language_model.model.layers.0.norm.weight",
109
+ "language_model.model.layers.0.mixer.qkv_proj.weight",
110
+ "language_model.model.layers.0.mixer.qkv_proj.weight",
111
+ "language_model.model.layers.0.mixer.qkv_proj.weight",
112
+ "language_model.model.layers.0.mixer.o_proj.weight",
113
+ "language_model.model.layers.1.norm.weight",
114
+ "language_model.model.layers.1.mixer.up_proj.weight",
115
+ "language_model.model.layers.1.mixer.down_proj.weight",
116
+ "language_model.model.layers.54.norm.weight",
117
+ "language_model.model.layers.54.mixer.qkv_proj.weight",
118
+ "language_model.model.layers.55.norm.weight",
119
+ "language_model.model.layers.55.mixer.down_proj.weight",
120
+ "visual.encoder.embeddings.patch_embedding.weight",
121
+ "visual.encoder.encoder.layers.0.self_attn.qkv_proj.weight",
122
+ "visual.projector.linear_fc1.weight",
123
+ "language_model.lm_head.weight",
124
+ ]
125
+
126
+ assert (
127
+ mapper.apply_list(
128
+ [
129
+ "layers.0.self_attn.add_q_proj.weight",
130
+ "layers.0.self_attn.add_k_proj.weight",
131
+ "layers.0.self_attn.add_v_proj.weight",
132
+ "layers.0.self_attn.to_add_out.weight",
133
+ "layers.0.self_attn.norm_added_q.weight",
134
+ "layers.0.self_attn.norm_added_k.weight",
135
+ "layers.0.self_attn.q_proj_moe_gen.weight",
136
+ "layers.0.mlp_moe_gen.up_proj.weight",
137
+ "norm_moe_gen.weight",
138
+ "proj_in.weight",
139
+ "proj_out.weight",
140
+ "audio_modality_embed",
141
+ "action_modality_embed",
142
+ ]
143
+ )
144
+ == []
145
+ )
146
+
147
+
148
def create_repo_dummy_weights(repo: str) -> Iterable[tuple[str, torch.Tensor]]:
149
"""Create weights from safetensors checkpoint metadata"""
150
metadata = try_get_safetensors_metadata(repo)
tests/models/registry.py
+5
@@ -811,6 +811,11 @@ _MULTIMODAL_EXAMPLE_MODELS = {
811
max_model_len=4096,
812
min_transformers_version="4.57",
813
),
814
+ "Cosmos3EdgeForConditionalGeneration": _HfExamplesInfo(
815
+ "nvidia/Cosmos3-Edge",
816
+ max_model_len=4096,
817
+ is_available_online=False,
818
+ ),
819
"DeepseekVLV2ForCausalLM": _HfExamplesInfo(
820
"deepseek-ai/deepseek-vl2-tiny",
821
extras={"fork": "Isotr0py/deepseek-vl2-tiny"},
vllm/model_executor/models/cosmos3.py
+4
-21
@@ -3,8 +3,6 @@
3
4
import regex
5
6
-from vllm.config import VllmConfig
7
-from vllm.model_executor.model_loader.default_loader import DefaultModelLoader
6
from vllm.model_executor.models.qwen3_vl import Qwen3VLForConditionalGeneration
7
from vllm.model_executor.models.utils import WeightsMapper
8
@@ -52,22 +50,7 @@ class Cosmos3ForConditionalGeneration(Qwen3VLForConditionalGeneration):
50
},
51
)
52
55
- allow_patterns_overrides = ["transformer/*.safetensors"]
56
-
57
- """
58
- Cosmos3 checkpoint separates transformer weights and vision_encoder weights
59
- into separate directories, as it's in diffusers checkpoint format.
60
- Using secondary_weights here to load all necessary weights for
61
- the Reasoner-only part.
62
- """
63
-
64
- def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
65
- super().__init__(vllm_config=vllm_config, prefix=prefix)
66
- self.secondary_weights = [
67
- DefaultModelLoader.Source(
68
- model_or_path=vllm_config.model_config.model,
69
- revision=vllm_config.model_config.revision,
70
- prefix="",
71
- allow_patterns_overrides=["vision_encoder/*.safetensors"],
72
- ),
73
- ]
53
+ # Cosmos3 unified Diffusers checkpoints store reasoner weights across
54
+ # transformer/ and vision_encoder/. Match both while excluding VAE and
55
+ # sound_tokenizer weights; the mapper drops generation-only tensors.
56
+ allow_patterns_overrides = ["[tv]*er/*.safetensors"]
vllm/model_executor/models/cosmos3_edge.py
new
+751
@@ -0,0 +1,751 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Iterable
5
+
6
+import torch
7
+import torch.nn as nn
8
+from transformers import ProcessorMixin
9
+
10
+from vllm.config import VllmConfig
11
+from vllm.model_executor.layers.layernorm import RMSNorm
12
+from vllm.model_executor.layers.linear import (
13
+ ColumnParallelLinear,
14
+ RowParallelLinear,
15
+)
16
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
17
+from vllm.model_executor.layers.rotary_embedding import get_rope
18
+from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
19
+from vllm.model_executor.models.module_mapping import MultiModelKeys
20
+from vllm.multimodal import MULTIMODAL_REGISTRY
21
+from vllm.multimodal.inputs import MultiModalFeatureSpec
22
+from vllm.sequence import IntermediateTensors
23
+from vllm.transformers_utils.configs.nemotron_h import NemotronHConfig
24
+
25
+from .interfaces import (
26
+ MultiModalEmbeddings,
27
+ SupportsMRoPE,
28
+ SupportsMultiModal,
29
+ SupportsPP,
30
+)
31
+from .lfm2_siglip2 import Siglip2VisionTransformer
32
+from .nemotron_h import (
33
+ NemotronHAttention,
34
+ NemotronHMLPDecoderLayer,
35
+ NemotronHModel,
36
+)
37
+from .qwen2_5_vl import (
38
+ Qwen2_5_VLImageEmbeddingInputs,
39
+ Qwen2_5_VLImageInputs,
40
+ Qwen2_5_VLImagePixelInputs,
41
+ Qwen2_5_VLVideoEmbeddingInputs,
42
+ Qwen2_5_VLVideoInputs,
43
+ Qwen2_5_VLVideoPixelInputs,
44
+)
45
+from .qwen3_vl import (
46
+ Qwen3VLDummyInputsBuilder,
47
+ Qwen3VLForConditionalGeneration,
48
+ Qwen3VLMultiModalProcessor,
49
+ Qwen3VLProcessingInfo,
50
+)
51
+from .utils import (
52
+ AutoWeightsLoader,
53
+ WeightsMapper,
54
+ maybe_prefix,
55
+)
56
+from .vision import (
57
+ is_vit_use_data_parallel,
58
+ run_dp_sharded_mrope_vision_model,
59
+)
60
+
61
+
62
+class Cosmos3EdgeVisionEncoder(Siglip2VisionTransformer):
63
+ """Adapts Cosmos (T, H, W) metadata to vLLM packed SigLIP2."""
64
+
65
+ @property
66
+ def dtype(self) -> torch.dtype:
67
+ return self.embeddings.patch_embedding.weight.dtype
68
+
69
+ def forward(
70
+ self,
71
+ pixel_values: torch.Tensor,
72
+ grid_thw: torch.Tensor,
73
+ ) -> torch.Tensor:
74
+ # SigLIP2 attends to each frame independently, so expand every THW
75
+ # entry into T separate HW attention sequences.
76
+ grid_thw_cpu = grid_thw.to(device="cpu")
77
+ spatial_shapes = torch.repeat_interleave(
78
+ grid_thw_cpu[:, 1:],
79
+ grid_thw_cpu[:, 0],
80
+ dim=0,
81
+ )
82
+ lengths_cpu = spatial_shapes.prod(dim=-1).to(torch.int32)
83
+ lengths = lengths_cpu.to(
84
+ device=pixel_values.device,
85
+ non_blocking=True,
86
+ )
87
+
88
+ cu_seqlens = torch.zeros(
89
+ lengths.numel() + 1,
90
+ dtype=torch.int32,
91
+ device=pixel_values.device,
92
+ )
93
+ cu_seqlens[1:] = lengths.cumsum(dim=0)
94
+
95
+ max_seqlen = lengths_cpu.max().reshape(1)
96
+
97
+ return super().forward(
98
+ pixel_values_packed=pixel_values,
99
+ spatial_shapes=spatial_shapes,
100
+ cu_seqlens=cu_seqlens,
101
+ max_seqlen=max_seqlen,
102
+ )[0]
103
+
104
+
105
+def patch_merging_by_param(
106
+ image_embeds: torch.Tensor,
107
+ grid_thw: torch.Tensor,
108
+ merge_size: int = 2,
109
+) -> torch.Tensor:
110
+ """Merge each spatial ``merge_size`` block into the channel dimension."""
111
+ merged_embeds: list[torch.Tensor] = []
112
+ current_idx = 0
113
+ hidden_size = image_embeds.shape[-1]
114
+
115
+ for grid in grid_thw:
116
+ t, h, w = grid.tolist()
117
+ if h % merge_size != 0 or w % merge_size != 0:
118
+ raise ValueError(
119
+ "Grid height and width must be divisible by merge_size: "
120
+ f"got grid={(t, h, w)} and merge_size={merge_size}."
121
+ )
122
+
123
+ num_patches = t * h * w
124
+ media_embeds = image_embeds[current_idx : current_idx + num_patches]
125
+ if media_embeds.shape[0] != num_patches:
126
+ raise ValueError(
127
+ "image_embeds contains fewer patches than grid_thw describes."
128
+ )
129
+ current_idx += num_patches
130
+
131
+ media_embeds = media_embeds.view(t, h, w, hidden_size)
132
+ media_embeds = media_embeds.view(
133
+ t,
134
+ h // merge_size,
135
+ merge_size,
136
+ w // merge_size,
137
+ merge_size,
138
+ hidden_size,
139
+ )
140
+ media_embeds = media_embeds.permute(0, 1, 3, 2, 4, 5).contiguous()
141
+ merged_embeds.append(
142
+ media_embeds.view(-1, merge_size * merge_size * hidden_size)
143
+ )
144
+
145
+ if current_idx != image_embeds.shape[0]:
146
+ raise ValueError(
147
+ "image_embeds contains more patches than grid_thw describes: "
148
+ f"got {image_embeds.shape[0]}, expected {current_idx}."
149
+ )
150
+ if not merged_embeds:
151
+ return image_embeds.new_empty((0, merge_size * merge_size * hidden_size))
152
+ return torch.cat(merged_embeds, dim=0)
153
+
154
+
155
+class Cosmos3EdgePatchMerger(nn.Module):
156
+ """
157
+ Projector: LayerNorm -> Linear -> GELU -> Linear
158
+
159
+ Reads config from projector_config (not vision_config).
160
+ input_hidden_size * spatial_merge_size² -> merger_intermediate_size
161
+ -> out_hidden_size
162
+ """
163
+
164
+ def __init__(
165
+ self,
166
+ input_hidden_size: int,
167
+ out_hidden_size: int,
168
+ merger_intermediate_size: int,
169
+ spatial_merge_size: int = 2,
170
+ use_postshuffle_norm: bool = False,
171
+ quant_config=None,
172
+ prefix: str = "",
173
+ ) -> None:
174
+ super().__init__()
175
+ use_data_parallel = is_vit_use_data_parallel()
176
+ self.spatial_merge_size = spatial_merge_size
177
+ self.hidden_size = input_hidden_size * (spatial_merge_size**2)
178
+ self.input_hidden_size = input_hidden_size
179
+ self.out_hidden_size = out_hidden_size
180
+ self.use_postshuffle_norm = use_postshuffle_norm
181
+
182
+ norm_dim = self.hidden_size if use_postshuffle_norm else input_hidden_size
183
+ self.norm = nn.LayerNorm(norm_dim, eps=1e-6)
184
+
185
+ self.linear_fc1 = ColumnParallelLinear(
186
+ self.hidden_size,
187
+ merger_intermediate_size,
188
+ bias=True,
189
+ quant_config=quant_config,
190
+ prefix=f"{prefix}.linear_fc1",
191
+ disable_tp=use_data_parallel,
192
+ )
193
+ self.act_fn = nn.GELU()
194
+ self.linear_fc2 = RowParallelLinear(
195
+ merger_intermediate_size,
196
+ out_hidden_size,
197
+ bias=True,
198
+ quant_config=quant_config,
199
+ prefix=f"{prefix}.linear_fc2",
200
+ disable_tp=use_data_parallel,
201
+ )
202
+
203
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
204
+ if self.use_postshuffle_norm:
205
+ x = self.norm(x.view(-1, self.hidden_size))
206
+ else:
207
+ x = self.norm(x).view(-1, self.hidden_size)
208
+
209
+ x, _ = self.linear_fc1(x)
210
+ x = self.act_fn(x)
211
+ x, _ = self.linear_fc2(x)
212
+ return x
213
+
214
+
215
+class Cosmos3EdgeVisionModel(nn.Module):
216
+ """Complete Cosmos vision tower returning language-model embeddings."""
217
+
218
+ def __init__(
219
+ self,
220
+ vision_config,
221
+ projector_config,
222
+ quant_config=None,
223
+ prefix: str = "",
224
+ ) -> None:
225
+ super().__init__()
226
+ self.spatial_merge_size = projector_config.spatial_merge_size
227
+ self.out_hidden_size = projector_config.out_hidden_size
228
+
229
+ self.encoder = Cosmos3EdgeVisionEncoder(
230
+ vision_config,
231
+ quant_config=quant_config,
232
+ prefix=maybe_prefix(prefix, "encoder"),
233
+ )
234
+ self.projector = Cosmos3EdgePatchMerger(
235
+ input_hidden_size=projector_config.input_hidden_size,
236
+ out_hidden_size=projector_config.out_hidden_size,
237
+ merger_intermediate_size=projector_config.merger_intermediate_size,
238
+ spatial_merge_size=self.spatial_merge_size,
239
+ use_postshuffle_norm=getattr(
240
+ projector_config, "use_postshuffle_norm", False
241
+ ),
242
+ quant_config=quant_config,
243
+ prefix=maybe_prefix(prefix, "projector"),
244
+ )
245
+
246
+ @property
247
+ def dtype(self) -> torch.dtype:
248
+ return self.encoder.dtype
249
+
250
+ def forward(
251
+ self,
252
+ pixel_values: torch.Tensor,
253
+ grid_thw: torch.Tensor | list[list[int]],
254
+ ) -> torch.Tensor:
255
+ grid_thw = torch.as_tensor(grid_thw, dtype=torch.int64, device="cpu")
256
+ image_embeds = self.encoder(pixel_values.type(self.dtype), grid_thw=grid_thw)
257
+ image_embeds = patch_merging_by_param(
258
+ image_embeds,
259
+ grid_thw,
260
+ merge_size=self.spatial_merge_size,
261
+ )
262
+ image_embeds = image_embeds.view(
263
+ -1,
264
+ self.spatial_merge_size**2,
265
+ self.projector.input_hidden_size,
266
+ )
267
+ return self.projector(image_embeds)
268
+
269
+
270
+class Cosmos3EdgeProcessingInfo(Qwen3VLProcessingInfo):
271
+ def get_hf_config(self):
272
+ return self.ctx.get_hf_config()
273
+
274
+ def get_hf_processor(self, **kwargs: object) -> ProcessorMixin:
275
+ return self.ctx.get_hf_processor(**kwargs)
276
+
277
+
278
+class Cosmos3EdgeMultiModalProcessor(Qwen3VLMultiModalProcessor):
279
+ pass
280
+
281
+
282
+class Cosmos3EdgeDummyInputsBuilder(Qwen3VLDummyInputsBuilder):
283
+ pass
284
+
285
+
286
+class Cosmos3EdgeAttention(NemotronHAttention):
287
+ """Nemotron-H attention with interleaved multimodal RoPE."""
288
+
289
+ def __init__(
290
+ self,
291
+ config: NemotronHConfig,
292
+ layer_idx: int,
293
+ model_config=None,
294
+ cache_config=None,
295
+ quant_config=None,
296
+ prefix: str = "",
297
+ ) -> None:
298
+ super().__init__(
299
+ config,
300
+ layer_idx,
301
+ model_config,
302
+ cache_config,
303
+ quant_config,
304
+ prefix,
305
+ )
306
+ self.rotary_emb = get_rope(
307
+ self.head_dim,
308
+ max_position=config.max_position_embeddings,
309
+ rope_parameters=config.rope_parameters,
310
+ )
311
+
312
+ def forward(
313
+ self,
314
+ positions: torch.Tensor,
315
+ hidden_states: torch.Tensor,
316
+ **kwargs,
317
+ ) -> torch.Tensor:
318
+ qkv, _ = self.qkv_proj(hidden_states)
319
+ q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
320
+ q, k = self.rotary_emb(positions, q, k)
321
+ attn_output = self.attn(q, k, v)
322
+ output, _ = self.o_proj(attn_output)
323
+ return output
324
+
325
+
326
+class Cosmos3EdgeAttentionDecoderLayer(nn.Module):
327
+ """Pre-norm attention layer for the Cosmos3 Edge dense text model."""
328
+
329
+ def __init__(
330
+ self,
331
+ config: NemotronHConfig,
332
+ layer_idx: int,
333
+ model_config=None,
334
+ cache_config=None,
335
+ quant_config=None,
336
+ parallel_config=None,
337
+ prefix: str = "",
338
+ ) -> None:
339
+ super().__init__()
340
+ get_layer_config = getattr(config, "get_nemotron_h_config_for_layer", None)
341
+ layer_config = get_layer_config(layer_idx) if get_layer_config else config
342
+ self.mixer = Cosmos3EdgeAttention(
343
+ layer_config,
344
+ layer_idx,
345
+ model_config,
346
+ cache_config,
347
+ quant_config,
348
+ prefix=f"{prefix}.mixer",
349
+ )
350
+ self.norm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
351
+
352
+ def forward(
353
+ self,
354
+ positions: torch.Tensor,
355
+ hidden_states: torch.Tensor,
356
+ residual: torch.Tensor | None,
357
+ **kwargs,
358
+ ) -> tuple[torch.Tensor, torch.Tensor]:
359
+ if residual is None:
360
+ residual = hidden_states
361
+ hidden_states = self.norm(hidden_states)
362
+ else:
363
+ hidden_states, residual = self.norm(hidden_states, residual)
364
+ hidden_states = self.mixer(positions=positions, hidden_states=hidden_states)
365
+ return hidden_states, residual
366
+
367
+
368
+class Cosmos3EdgeTextModel(NemotronHModel):
369
+ """Nemotron-H backbone with Cosmos-owned checkpoint name mapping."""
370
+
371
+ # The enclosing Cosmos model maps and packs checkpoint weights. Disabling
372
+ # Nemotron-H's checkpoint-specific hook prevents those names from being
373
+ # remapped a second time when AutoWeightsLoader descends into this module.
374
+ load_weights = None # type: ignore[assignment]
375
+
376
+
377
+class Cosmos3EdgeForCausalLM(nn.Module):
378
+ """Minimal CausalLM wrapper for the Cosmos3 Edge dense text model."""
379
+
380
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
381
+ super().__init__()
382
+ hf_config = vllm_config.model_config.hf_config
383
+ text_config = getattr(hf_config, "text_config", hf_config)
384
+ vllm_config = vllm_config.with_hf_config(text_config)
385
+
386
+ self.vllm_config = vllm_config
387
+ self.model_config = vllm_config.model_config
388
+ self.quant_config = vllm_config.quant_config
389
+ self.config = text_config
390
+ pattern = text_config.hybrid_override_pattern
391
+ unsupported = set(pattern) - {"*", "-"}
392
+ if unsupported:
393
+ raise ValueError(
394
+ "Cosmos3 Edge only supports attention (`*`) and MLP (`-`) "
395
+ f"layers, but found: {sorted(unsupported)}"
396
+ )
397
+ if len(pattern) != text_config.num_hidden_layers:
398
+ raise ValueError(
399
+ "hybrid_override_pattern must contain one entry per layer: "
400
+ f"expected {text_config.num_hidden_layers}, "
401
+ f"got {len(pattern)}"
402
+ )
403
+
404
+ self.model = Cosmos3EdgeTextModel(
405
+ vllm_config=vllm_config,
406
+ prefix=maybe_prefix(prefix, "model"),
407
+ decoder_layer_types={
408
+ "*": Cosmos3EdgeAttentionDecoderLayer,
409
+ "-": NemotronHMLPDecoderLayer,
410
+ },
411
+ )
412
+ self.lm_head = ParallelLMHead(
413
+ text_config.vocab_size,
414
+ text_config.hidden_size,
415
+ quant_config=self.quant_config,
416
+ prefix=maybe_prefix(prefix, "lm_head"),
417
+ )
418
+ self.logits_processor = LogitsProcessor(text_config.vocab_size)
419
+ self.make_empty_intermediate_tensors = (
420
+ self.model.make_empty_intermediate_tensors
421
+ )
422
+
423
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
424
+ return self.model.embed_input_ids(input_ids)
425
+
426
+ def forward(
427
+ self,
428
+ input_ids: torch.Tensor | None,
429
+ positions: torch.Tensor,
430
+ intermediate_tensors: IntermediateTensors | None = None,
431
+ inputs_embeds: torch.Tensor | None = None,
432
+ **kwargs,
433
+ ):
434
+ return self.model(
435
+ input_ids=input_ids,
436
+ positions=positions,
437
+ intermediate_tensors=intermediate_tensors,
438
+ inputs_embeds=inputs_embeds,
439
+ )
440
+
441
+ def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
442
+ return self.logits_processor(self.lm_head, hidden_states)
443
+
444
+
445
+def _cosmos3_edge_diffusers_prefix_map() -> dict[str, str]:
446
+ """Map Diffusers blocks to interleaved Nemotron-H layers.
447
+
448
+ The checkpoint stores attention and MLP in one ``layers.N`` block, while
449
+ the Nemotron-H model represents them as separate decoder layers. Therefore,
450
+ checkpoint block ``N`` maps its attention to layer ``2N`` and its MLP to
451
+ layer ``2N + 1``.
452
+ """
453
+ mappings = {
454
+ "embed_tokens.": "language_model.model.embed_tokens.",
455
+ "norm.": "language_model.model.norm_f.",
456
+ }
457
+ for physical_idx in range(28):
458
+ attention_idx = 2 * physical_idx
459
+ mlp_idx = attention_idx + 1
460
+ source_prefix = f"layers.{physical_idx}"
461
+ attention_prefix = f"language_model.model.layers.{attention_idx}"
462
+ mlp_prefix = f"language_model.model.layers.{mlp_idx}"
463
+ mappings.update(
464
+ {
465
+ f"{source_prefix}.input_layernorm.": f"{attention_prefix}.norm.",
466
+ f"{source_prefix}.self_attn.qkv_proj.": (
467
+ f"{attention_prefix}.mixer.qkv_proj."
468
+ ),
469
+ f"{source_prefix}.self_attn.o_proj.": (
470
+ f"{attention_prefix}.mixer.o_proj."
471
+ ),
472
+ f"{source_prefix}.post_attention_layernorm.": (f"{mlp_prefix}.norm."),
473
+ f"{source_prefix}.mlp.": f"{mlp_prefix}.mixer.",
474
+ }
475
+ )
476
+ return mappings
477
+
478
+
479
+@MULTIMODAL_REGISTRY.register_processor(
480
+ Cosmos3EdgeMultiModalProcessor,
481
+ info=Cosmos3EdgeProcessingInfo,
482
+ dummy_inputs=Cosmos3EdgeDummyInputsBuilder,
483
+)
484
+class Cosmos3EdgeForConditionalGeneration(
485
+ nn.Module,
486
+ SupportsMultiModal,
487
+ SupportsPP,
488
+ SupportsMRoPE,
489
+):
490
+ """
491
+ Cosmos3 Edge model with a SigLIP2 vision encoder.
492
+
493
+ Architecture:
494
+ - self.visual: SigLIP2 encoder + patch merger + projector
495
+ - self.language_model: Cosmos3EdgeForCausalLM (pure attention + RoPE)
496
+ """
497
+
498
+ hf_to_vllm_mapper = WeightsMapper(
499
+ orig_to_new_stacked={
500
+ ".self_attn.q_proj": (".self_attn.qkv_proj", "q"),
501
+ ".self_attn.k_proj": (".self_attn.qkv_proj", "k"),
502
+ ".self_attn.v_proj": (".self_attn.qkv_proj", "v"),
503
+ },
504
+ orig_to_new_prefix={
505
+ **_cosmos3_edge_diffusers_prefix_map(),
506
+ "proj_in.": None,
507
+ "proj_out.": None,
508
+ "time_embedder.": None,
509
+ "action_proj_in.": None,
510
+ "action_proj_out.": None,
511
+ "audio_modality_embed": None,
512
+ "action_modality_embed": None,
513
+ "model.visual.": "visual.encoder.",
514
+ "model.projector.": "visual.projector.",
515
+ "lm_head.": "language_model.lm_head.",
516
+ "model.language_model.": "language_model.model.",
517
+ },
518
+ orig_to_new_substr={
519
+ "_moe_gen": None,
520
+ ".add_q_proj.": None,
521
+ ".add_k_proj.": None,
522
+ ".add_v_proj.": None,
523
+ ".to_add_out.": None,
524
+ ".norm_added_q.": None,
525
+ ".norm_added_k.": None,
526
+ ".self_attn.to_q.": ".self_attn.q_proj.",
527
+ ".self_attn.to_k.": ".self_attn.k_proj.",
528
+ ".self_attn.to_v.": ".self_attn.v_proj.",
529
+ ".self_attn.to_out.": ".self_attn.o_proj.",
530
+ "language_model.embeddings": "language_model.embed_tokens",
531
+ },
532
+ )
533
+
534
+ packed_modules_mapping = {
535
+ "qkv_proj": ["q_proj", "k_proj", "v_proj"],
536
+ }
537
+
538
+ # Cosmos3 Edge unified Diffusers checkpoints store reasoner weights across
539
+ # transformer/ and vision_encoder/. Match both while excluding VAE weights.
540
+ allow_patterns_overrides = ["[tv]*er/*.safetensors"]
541
+
542
+ supports_encoder_tp_data = True
543
+
544
+ @classmethod
545
+ def get_placeholder_str(cls, modality: str, i: int) -> str | None:
546
+ if modality.startswith("image"):
547
+ return "<|vision_start|><|image_pad|><|vision_end|>"
548
+ if modality.startswith("video"):
549
+ return "<|vision_start|><|video_pad|><|vision_end|>"
550
+ raise ValueError(f"Unsupported modality: {modality}")
551
+
552
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"):
553
+ super().__init__()
554
+
555
+ config = vllm_config.model_config.hf_config
556
+ quant_config = vllm_config.quant_config
557
+ multimodal_config = vllm_config.model_config.multimodal_config
558
+
559
+ self.config = config
560
+ self.multimodal_config = multimodal_config
561
+ self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
562
+
563
+ with self._mark_tower_model(vllm_config, {"image", "video"}):
564
+ self.visual = Cosmos3EdgeVisionModel(
565
+ vision_config=config.vision_config,
566
+ projector_config=config.projector_config,
567
+ quant_config=quant_config,
568
+ prefix=maybe_prefix(prefix, "visual"),
569
+ )
570
+
571
+ with self._mark_language_model(vllm_config):
572
+ self.language_model = Cosmos3EdgeForCausalLM(
573
+ vllm_config=vllm_config,
574
+ prefix=maybe_prefix(prefix, "language_model"),
575
+ )
576
+
577
+ self.make_empty_intermediate_tensors = (
578
+ self.language_model.make_empty_intermediate_tensors
579
+ )
580
+
581
+ def _get_image_features(
582
+ self,
583
+ pixel_values: torch.Tensor,
584
+ grid_thw: torch.Tensor,
585
+ ) -> tuple[torch.Tensor, ...]:
586
+ """Run the complete vision tower and split by media item."""
587
+ image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
588
+ sizes = (grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist()
589
+ return image_embeds.split(sizes)
590
+
591
+ def _parse_and_validate_image_input(
592
+ self, **kwargs: object
593
+ ) -> Qwen2_5_VLImageInputs | None:
594
+ pixel_values = kwargs.pop("pixel_values", None)
595
+ image_embeds = kwargs.pop("image_embeds", None)
596
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
597
+
598
+ if pixel_values is None and image_embeds is None:
599
+ return None
600
+
601
+ if pixel_values is not None:
602
+ return Qwen2_5_VLImagePixelInputs(
603
+ type="pixel_values",
604
+ pixel_values=pixel_values,
605
+ image_grid_thw=image_grid_thw,
606
+ )
607
+
608
+ if image_embeds is not None:
609
+ return Qwen2_5_VLImageEmbeddingInputs(
610
+ type="image_embeds",
611
+ image_embeds=image_embeds,
612
+ image_grid_thw=image_grid_thw,
613
+ )
614
+
615
+ def _parse_and_validate_video_input(
616
+ self, **kwargs: object
617
+ ) -> Qwen2_5_VLVideoInputs | None:
618
+ pixel_values_videos = kwargs.pop("pixel_values_videos", None)
619
+ video_embeds = kwargs.pop("video_embeds", None)
620
+ video_grid_thw = kwargs.pop("video_grid_thw", None)
621
+ second_per_grid_ts = kwargs.pop("second_per_grid_ts", None)
622
+
623
+ if pixel_values_videos is None and video_embeds is None:
624
+ return None
625
+
626
+ if pixel_values_videos is not None:
627
+ return Qwen2_5_VLVideoPixelInputs(
628
+ type="pixel_values_videos",
629
+ pixel_values_videos=pixel_values_videos,
630
+ video_grid_thw=video_grid_thw,
631
+ second_per_grid_ts=second_per_grid_ts,
632
+ )
633
+
634
+ if video_embeds is not None:
635
+ return Qwen2_5_VLVideoEmbeddingInputs(
636
+ type="video_embeds",
637
+ video_embeds=video_embeds,
638
+ video_grid_thw=video_grid_thw,
639
+ )
640
+
641
+ def _process_image_input(
642
+ self, image_input: Qwen2_5_VLImageInputs
643
+ ) -> tuple[torch.Tensor, ...]:
644
+ grid_thw = image_input["image_grid_thw"]
645
+ assert grid_thw.ndim == 2
646
+
647
+ if image_input["type"] == "image_embeds":
648
+ image_embeds = image_input["image_embeds"].type(self.visual.dtype)
649
+ merge_size = self.visual.spatial_merge_size
650
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
651
+ return image_embeds.split(sizes)
652
+
653
+ pixel_values = image_input["pixel_values"].type(self.visual.dtype)
654
+ if self.use_data_parallel:
655
+ return run_dp_sharded_mrope_vision_model(
656
+ self.visual, pixel_values, grid_thw.tolist(), rope_type="rope_3d"
657
+ )
658
+ return self._get_image_features(pixel_values, grid_thw)
659
+
660
+ def _process_video_input(
661
+ self, video_input: Qwen2_5_VLVideoInputs
662
+ ) -> tuple[torch.Tensor, ...]:
663
+ grid_thw = video_input["video_grid_thw"]
664
+ assert grid_thw.ndim == 2
665
+
666
+ if video_input["type"] == "video_embeds":
667
+ video_embeds = video_input["video_embeds"].type(self.visual.dtype)
668
+ merge_size = self.visual.spatial_merge_size
669
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
670
+ return video_embeds.split(sizes)
671
+
672
+ pixel_values_videos = video_input["pixel_values_videos"].type(self.visual.dtype)
673
+ if self.use_data_parallel:
674
+ return run_dp_sharded_mrope_vision_model(
675
+ self.visual, pixel_values_videos, grid_thw.tolist(), rope_type="rope_3d"
676
+ )
677
+ return self._get_image_features(pixel_values_videos, grid_thw)
678
+
679
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
680
+ modalities = {}
681
+ image_input = self._parse_and_validate_image_input(**kwargs)
682
+ if image_input is not None:
683
+ modalities["image"] = image_input
684
+ video_input = self._parse_and_validate_video_input(**kwargs)
685
+ if video_input is not None:
686
+ modalities["video"] = video_input
687
+ return modalities
688
+
689
+ def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
690
+ mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
691
+ if not mm_input_by_modality:
692
+ return None
693
+
694
+ multimodal_embeddings: tuple[torch.Tensor, ...] = ()
695
+
696
+ for modality in mm_input_by_modality:
697
+ multimodal_input = mm_input_by_modality[modality]
698
+ if modality == "image":
699
+ image_embeddings = self._process_image_input(multimodal_input)
700
+ multimodal_embeddings += tuple(image_embeddings)
701
+ if modality == "video":
702
+ video_embeddings = self._process_video_input(multimodal_input)
703
+ multimodal_embeddings += tuple(video_embeddings)
704
+
705
+ return multimodal_embeddings
706
+
707
+ def forward(
708
+ self,
709
+ input_ids: torch.Tensor | None,
710
+ positions: torch.Tensor,
711
+ intermediate_tensors: IntermediateTensors | None = None,
712
+ inputs_embeds: torch.Tensor | None = None,
713
+ **kwargs: object,
714
+ ) -> torch.Tensor | IntermediateTensors:
715
+ if intermediate_tensors is not None:
716
+ inputs_embeds = None
717
+
718
+ return self.language_model.model(
719
+ input_ids=input_ids,
720
+ positions=positions,
721
+ intermediate_tensors=intermediate_tensors,
722
+ inputs_embeds=inputs_embeds,
723
+ )
724
+
725
+ def compute_logits(
726
+ self,
727
+ hidden_states: torch.Tensor,
728
+ ) -> torch.Tensor | None:
729
+ return self.language_model.compute_logits(hidden_states)
730
+
731
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
732
+ loader = AutoWeightsLoader(self)
733
+ return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
734
+
735
+ def get_mm_mapping(self) -> MultiModelKeys:
736
+ return MultiModelKeys.from_string_field(
737
+ language_model="language_model",
738
+ connector="visual.projector",
739
+ tower_model="visual.",
740
+ )
741
+
742
+ def get_mrope_input_positions(
743
+ self,
744
+ input_tokens: list[int],
745
+ mm_features: list[MultiModalFeatureSpec],
746
+ ) -> tuple[torch.Tensor, int]:
747
+ return Qwen3VLForConditionalGeneration._get_mrope_input_positions(
748
+ input_tokens=input_tokens,
749
+ mm_features=mm_features,
750
+ config=self.config,
751
+ )
vllm/model_executor/models/nemotron_h.py
+24
-6
@@ -19,7 +19,7 @@
19
"""Inference-only NemotronH model."""
20
21
import typing
22
-from collections.abc import Callable, Iterable
22
+from collections.abc import Callable, Iterable, Mapping
23
from itertools import islice
24
25
import torch
@@ -540,9 +540,22 @@ ALL_DECODER_LAYER_TYPES = {
540
}
541
542
543
-@support_torch_compile
543
+@support_torch_compile(
544
+ dynamic_arg_dims={
545
+ "input_ids": 0,
546
+ "positions": -1,
547
+ "intermediate_tensors": 0,
548
+ "inputs_embeds": 0,
549
+ }
550
+)
551
class NemotronHModel(nn.Module, EagleModelMixin):
545
- def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
552
+ def __init__(
553
+ self,
554
+ *,
555
+ vllm_config: VllmConfig,
556
+ prefix: str = "",
557
+ decoder_layer_types: Mapping[str, type[nn.Module]] | None = None,
558
+ ):
559
super().__init__()
560
561
config: NemotronHConfig = vllm_config.model_config.hf_config
@@ -562,11 +575,16 @@ class NemotronHModel(nn.Module, EagleModelMixin):
575
576
self.has_moe = "E" in config.hybrid_override_pattern
577
578
+ layer_types = decoder_layer_types or ALL_DECODER_LAYER_TYPES
579
+
580
def get_layer(prefix: str):
581
layer_idx = int(prefix.rsplit(".", 1)[1])
567
- layer_class = ALL_DECODER_LAYER_TYPES[
568
- config.hybrid_override_pattern[layer_idx]
569
- ]
582
+ layer_type = config.hybrid_override_pattern[layer_idx]
583
+ if layer_type not in layer_types:
584
+ raise ValueError(
585
+ f"Unsupported layer type {layer_type!r} at layer {layer_idx}"
586
+ )
587
+ layer_class = layer_types[layer_type]
588
return layer_class(
589
config=config,
590
layer_idx=layer_idx,
vllm/model_executor/models/registry.py
+4
@@ -349,6 +349,10 @@ _MULTIMODAL_MODELS = {
349
"Cohere2VisionForConditionalGeneration",
350
),
351
"Cosmos3ForConditionalGeneration": ("cosmos3", "Cosmos3ForConditionalGeneration"),
352
+ "Cosmos3EdgeForConditionalGeneration": (
353
+ "cosmos3_edge",
354
+ "Cosmos3EdgeForConditionalGeneration",
355
+ ),
356
"DeepseekVLV2ForCausalLM": ("deepseek_vl2", "DeepseekVLV2ForCausalLM"),
357
"DeepseekOCRForCausalLM": ("deepseek_ocr", "DeepseekOCRForCausalLM"),
358
"DeepseekOCR2ForCausalLM": ("deepseek_ocr2", "DeepseekOCR2ForCausalLM"),
vllm/transformers_utils/config.py
+1
@@ -81,6 +81,7 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
81
ops_colqwen3="OpsColQwen3Config",
82
qwen3_vl_nemotron_embed="Qwen3VLNemotronEmbedConfig",
83
cosmos3_omni="Cosmos3Config",
84
+ cosmos3_edge="Cosmos3EdgeConfig",
85
diffusion_gemma="DiffusionGemmaConfig",
86
deepseek_vl_v2="DeepseekVLV2Config",
87
deepseek_v32="DeepseekV3Config",
vllm/transformers_utils/configs/__init__.py
+8
@@ -27,6 +27,10 @@ _CLASS_TO_MODULE: dict[str, str] = {
27
"OpsColQwen3Config": "vllm.transformers_utils.configs.colqwen3",
28
"Qwen3VLNemotronEmbedConfig": "vllm.transformers_utils.configs.colqwen3",
29
"Cosmos3Config": "vllm.transformers_utils.configs.cosmos3",
30
+ "Cosmos3EdgeConfig": "vllm.transformers_utils.configs.cosmos3_edge",
31
+ "Cosmos3EdgeProjectorConfig": "vllm.transformers_utils.configs.cosmos3_edge",
32
+ "Cosmos3EdgeTextConfig": "vllm.transformers_utils.configs.cosmos3_edge",
33
+ "Cosmos3EdgeVisionConfig": "vllm.transformers_utils.configs.cosmos3_edge",
34
"DiffusionGemmaConfig": "vllm.transformers_utils.configs.diffusion_gemma",
35
"DiffusionGemmaTextConfig": "vllm.transformers_utils.configs.diffusion_gemma",
36
"DeepseekVLV2Config": "vllm.transformers_utils.configs.deepseek_vl2",
@@ -107,6 +111,10 @@ __all__ = [
111
"OpsColQwen3Config",
112
"Qwen3VLNemotronEmbedConfig",
113
"Cosmos3Config",
114
+ "Cosmos3EdgeConfig",
115
+ "Cosmos3EdgeProjectorConfig",
116
+ "Cosmos3EdgeTextConfig",
117
+ "Cosmos3EdgeVisionConfig",
118
"DiffusionGemmaConfig",
119
"DiffusionGemmaTextConfig",
120
"DeepseekVLV2Config",
vllm/transformers_utils/configs/cosmos3_edge.py
new
+146
@@ -0,0 +1,146 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from transformers.configuration_utils import PretrainedConfig
5
+from transformers.models.siglip2.configuration_siglip2 import Siglip2VisionConfig
6
+
7
+from .nemotron_h import NemotronHConfig
8
+
9
+
10
+def _normalize_mrope_parameters(config: NemotronHConfig) -> None:
11
+ """Normalize the checkpoint's mRoPE configuration for vLLM."""
12
+ rope_parameters = dict(getattr(config, "rope_parameters", None) or {})
13
+ mrope_section = rope_parameters.get("mrope_section")
14
+ if mrope_section is None:
15
+ return
16
+
17
+ rope_parameters["mrope_section"] = list(mrope_section)
18
+ rope_parameters.setdefault("mrope_interleaved", True)
19
+ config.rope_parameters = rope_parameters
20
+
21
+
22
+class Cosmos3EdgeTextConfig(NemotronHConfig):
23
+ """Dense Cosmos3 Edge text config backed by Nemotron-H layers."""
24
+
25
+ model_type = "cosmos3_edge_text"
26
+ has_no_defaults_at_init = True
27
+ ignore_keys_at_rope_validation = {"mrope_section", "mrope_interleaved"}
28
+
29
+ def __init__(
30
+ self,
31
+ num_hidden_layers: int = 28,
32
+ hidden_act: str = "relu2",
33
+ rms_norm_eps: float = 1e-5,
34
+ **kwargs,
35
+ ) -> None:
36
+ super().__init__(
37
+ num_hidden_layers=2 * num_hidden_layers,
38
+ hybrid_override_pattern="*-" * num_hidden_layers,
39
+ mlp_hidden_act=hidden_act,
40
+ layer_norm_epsilon=rms_norm_eps,
41
+ **kwargs,
42
+ )
43
+
44
+ def to_dict(self) -> dict:
45
+ config_dict = super().to_dict()
46
+ config_dict["num_hidden_layers"] = self.num_hidden_layers // 2
47
+ config_dict["hidden_act"] = config_dict.pop("mlp_hidden_act")
48
+ config_dict["rms_norm_eps"] = config_dict.pop("layer_norm_epsilon")
49
+ config_dict.pop("hybrid_override_pattern", None)
50
+ return config_dict
51
+
52
+
53
+class Cosmos3EdgeProjectorConfig(PretrainedConfig):
54
+ model_type = "cosmos3_edge_projector"
55
+
56
+ def __init__(
57
+ self,
58
+ input_hidden_size: int = 1024,
59
+ use_postshuffle_norm: bool = False,
60
+ spatial_merge_size: int = 2,
61
+ merger_intermediate_size: int = 11520,
62
+ out_hidden_size: int = 1024,
63
+ **kwargs,
64
+ ) -> None:
65
+ super().__init__(**kwargs)
66
+ self.input_hidden_size = input_hidden_size
67
+ self.use_postshuffle_norm = use_postshuffle_norm
68
+ self.spatial_merge_size = spatial_merge_size
69
+ self.merger_intermediate_size = merger_intermediate_size
70
+ self.out_hidden_size = out_hidden_size
71
+
72
+
73
+class Cosmos3EdgeVisionConfig(Siglip2VisionConfig):
74
+ model_type = "cosmos3_edge_vision"
75
+
76
+
77
+class Cosmos3EdgeConfig(PretrainedConfig):
78
+ model_type = "cosmos3_edge"
79
+ keys_to_ignore_at_inference = ["past_key_values"]
80
+ has_no_defaults_at_init = True
81
+ sub_configs = {
82
+ "vision_config": Cosmos3EdgeVisionConfig,
83
+ "projector_config": Cosmos3EdgeProjectorConfig,
84
+ "text_config": Cosmos3EdgeTextConfig,
85
+ }
86
+
87
+ def __init__(
88
+ self,
89
+ text_config: Cosmos3EdgeTextConfig | dict | None = None,
90
+ vision_config: Cosmos3EdgeVisionConfig | dict | None = None,
91
+ projector_config: Cosmos3EdgeProjectorConfig | dict | None = None,
92
+ image_token_id: int = 19,
93
+ video_token_id: int = 18,
94
+ vision_start_token_id: int = 20,
95
+ vision_end_token_id: int = 21,
96
+ tie_word_embeddings: bool = False,
97
+ **kwargs,
98
+ ) -> None:
99
+ if text_config is None:
100
+ text_config = {}
101
+ if vision_config is None:
102
+ vision_config = {}
103
+ if projector_config is None:
104
+ projector_config = {}
105
+
106
+ self.text_config = (
107
+ Cosmos3EdgeTextConfig(**text_config)
108
+ if isinstance(text_config, dict)
109
+ else text_config
110
+ )
111
+ _normalize_mrope_parameters(self.text_config)
112
+
113
+ self.vision_config = (
114
+ Cosmos3EdgeVisionConfig(**vision_config)
115
+ if isinstance(vision_config, dict)
116
+ else vision_config
117
+ )
118
+ self.projector_config = (
119
+ Cosmos3EdgeProjectorConfig(**projector_config)
120
+ if isinstance(projector_config, dict)
121
+ else projector_config
122
+ )
123
+
124
+ # Qwen3-VL processing and M-RoPE read these attributes from the
125
+ # vision config, while this checkpoint stores them in projector config.
126
+ self.vision_config.spatial_merge_size = self.projector_config.spatial_merge_size
127
+ self.vision_config.temporal_patch_size = 1
128
+ self.vision_config.out_hidden_size = self.projector_config.out_hidden_size
129
+
130
+ self.image_token_id = image_token_id
131
+ self.video_token_id = video_token_id
132
+ self.vision_start_token_id = vision_start_token_id
133
+ self.vision_end_token_id = vision_end_token_id
134
+
135
+ super().__init__(
136
+ tie_word_embeddings=tie_word_embeddings,
137
+ **kwargs,
138
+ )
139
+
140
+
141
+__all__ = [
142
+ "Cosmos3EdgeConfig",
143
+ "Cosmos3EdgeProjectorConfig",
144
+ "Cosmos3EdgeTextConfig",
145
+ "Cosmos3EdgeVisionConfig",
146
+]
vllm/transformers_utils/processors/__init__.py
+2
@@ -14,6 +14,7 @@ __all__ = [
14
"BagelProcessor",
15
"CheersProcessor",
16
"CohereASRProcessor",
17
+ "Cosmos3EdgeProcessor",
18
"DeepseekVLV2Processor",
19
"FireRedASR2Processor",
20
"FireRedLIDProcessor",
@@ -49,6 +50,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
50
"BagelProcessor": "vllm.transformers_utils.processors.bagel",
51
"CheersProcessor": "vllm.transformers_utils.processors.cheers",
52
"CohereASRProcessor": "vllm.transformers_utils.processors.cohere_asr",
53
+ "Cosmos3EdgeProcessor": "vllm.transformers_utils.processors.cosmos3_edge",
54
"DeepseekVLV2Processor": "vllm.transformers_utils.processors.deepseek_vl2",
55
"FireRedASR2Processor": "vllm.transformers_utils.processors.fireredasr2",
56
"FireRedLIDProcessor": "vllm.transformers_utils.processors.fireredlid",
vllm/transformers_utils/processors/cosmos3_edge.py
new
+356
@@ -0,0 +1,356 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+
5
+import math
6
+
7
+import torch
8
+from torchvision.transforms import InterpolationMode
9
+from transformers import AutoTokenizer
10
+from transformers.feature_extraction_utils import BatchFeature
11
+from transformers.image_utils import ChannelDimension, PILImageResampling, SizeDict
12
+from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
13
+from transformers.models.qwen3_vl.video_processing_qwen3_vl import (
14
+ Qwen3VLVideoProcessor,
15
+ Qwen3VLVideoProcessorInitKwargs,
16
+ get_image_size,
17
+ smart_resize,
18
+)
19
+from transformers.models.siglip2.image_processing_siglip2 import (
20
+ Siglip2ImageProcessor,
21
+ convert_image_to_patches,
22
+)
23
+from transformers.processing_utils import Unpack
24
+from transformers.utils import TensorType
25
+from transformers.video_utils import group_videos_by_shape, reorder_videos
26
+
27
+
28
+def round_by_factor(number: int, factor: int) -> int:
29
+ """Returns the closest integer to 'number' that is divisible by 'factor'."""
30
+ return round(number / factor) * factor
31
+
32
+
33
+def ceil_by_factor(number: int, factor: int) -> int:
34
+ """Return the smallest multiple of ``factor`` greater than ``number``."""
35
+ return math.ceil(number / factor) * factor
36
+
37
+
38
+def floor_by_factor(number: int, factor: int) -> int:
39
+ """Return the largest multiple of ``factor`` less than ``number``."""
40
+ return math.floor(number / factor) * factor
41
+
42
+
43
+class Cosmos3EdgeImagesKwargs( # type: ignore[call-arg]
44
+ Siglip2ImageProcessor.valid_kwargs,
45
+ total=False,
46
+):
47
+ # global setting for all images, can be overridden by per-image kwargs
48
+ max_pixels: int | None
49
+ min_pixels: int | None
50
+
51
+ per_image_kwargs: list[dict] | None
52
+
53
+
54
+class Cosmos3EdgeImageProcessor(Siglip2ImageProcessor):
55
+ resample = PILImageResampling.BICUBIC
56
+ valid_kwargs = Cosmos3EdgeImagesKwargs
57
+
58
+ def __init__(self, **kwargs: Unpack[Cosmos3EdgeImagesKwargs]):
59
+ super().__init__(**kwargs)
60
+
61
+ def _resize_image(
62
+ self,
63
+ image: torch.Tensor,
64
+ max_ratio=200,
65
+ resample: PILImageResampling | InterpolationMode | int | None = None,
66
+ **kwargs,
67
+ ) -> torch.Tensor:
68
+ """
69
+ Rescales the image so that the following conditions are met:
70
+
71
+ 1. Both dimensions (height and width) are divisible by 'factor'.
72
+ 2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
73
+ 3. The aspect ratio of the image is maintained as closely as possible.
74
+ """
75
+ image_min_pixels = kwargs.get("min_pixels") or self.size.get(
76
+ "shortest_edge", None
77
+ )
78
+ image_max_pixels = kwargs.get("max_pixels") or self.size.get(
79
+ "longest_edge", None
80
+ )
81
+ assert image_min_pixels is not None and image_max_pixels is not None, (
82
+ "When do_resize is True, min_pixels and max_pixels must be provided."
83
+ )
84
+ assert image_max_pixels >= image_min_pixels, (
85
+ "The max_pixels of image must be greater than or equal to min_pixels."
86
+ )
87
+
88
+ _, height, width = image.shape
89
+ aspect_ratio = max(height, width) / min(height, width)
90
+ if aspect_ratio > max_ratio:
91
+ raise ValueError(
92
+ f"absolute aspect ratio must be smaller than {max_ratio}, "
93
+ f"got {aspect_ratio}"
94
+ )
95
+ factor = self.merge_size * self.patch_size
96
+ h_bar = max(factor, round_by_factor(height, factor))
97
+ w_bar = max(factor, round_by_factor(width, factor))
98
+ if h_bar * w_bar > image_max_pixels:
99
+ beta = math.sqrt((height * width) / image_max_pixels)
100
+ h_bar = floor_by_factor(height / beta, factor)
101
+ w_bar = floor_by_factor(width / beta, factor)
102
+ elif h_bar * w_bar < image_min_pixels:
103
+ beta = math.sqrt(image_min_pixels / (height * width))
104
+ h_bar = ceil_by_factor(height * beta, factor)
105
+ w_bar = ceil_by_factor(width * beta, factor)
106
+
107
+ image = self.resize(
108
+ image,
109
+ size=SizeDict(height=h_bar, width=w_bar),
110
+ resample=resample,
111
+ )
112
+ return image
113
+
114
+ def _preprocess(
115
+ self,
116
+ images: list["torch.Tensor"],
117
+ do_resize: bool,
118
+ patch_size: int,
119
+ max_num_patches: int,
120
+ resample: PILImageResampling | InterpolationMode | int | None,
121
+ do_rescale: bool,
122
+ rescale_factor: float,
123
+ do_normalize: bool,
124
+ image_mean: float | list[float] | None,
125
+ image_std: float | list[float] | None,
126
+ return_tensors: str | TensorType | None,
127
+ **kwargs,
128
+ ) -> BatchFeature:
129
+ per_image_kwargs = kwargs.pop("per_image_kwargs", None)
130
+ pixel_values = []
131
+ spatial_shapes = []
132
+ for idx, image in enumerate(images):
133
+ # merge global kwargs with per-image overrides (if provided)
134
+ image_kwargs = kwargs.copy()
135
+ if (
136
+ per_image_kwargs is not None
137
+ and idx < len(per_image_kwargs)
138
+ and per_image_kwargs[idx]
139
+ ):
140
+ image_kwargs.update(per_image_kwargs[idx])
141
+ if do_resize:
142
+ image = self._resize_image(
143
+ image,
144
+ max_ratio=200,
145
+ resample=resample,
146
+ **image_kwargs,
147
+ )
148
+
149
+ image = self.rescale_and_normalize(
150
+ image, do_rescale, rescale_factor, do_normalize, image_mean, image_std
151
+ )
152
+
153
+ # (channels, height, width) -> (patches, patch_size**2 * channels)
154
+ patches = convert_image_to_patches(image, patch_size)
155
+
156
+ num_patches_height = image.shape[1] // patch_size
157
+ num_patches_width = image.shape[2] // patch_size
158
+
159
+ spatial_shapes.append((num_patches_height, num_patches_width))
160
+ pixel_values.append(patches)
161
+
162
+ pixel_values = torch.cat(pixel_values, dim=0)
163
+
164
+ spatial_shapes = torch.tensor(spatial_shapes)
165
+ t_dim = torch.ones(
166
+ (spatial_shapes.shape[0], 1),
167
+ dtype=spatial_shapes.dtype,
168
+ device=spatial_shapes.device,
169
+ )
170
+ image_grid_thw = torch.cat([t_dim, spatial_shapes], dim=1)
171
+
172
+ batch_feature = BatchFeature(
173
+ data={
174
+ "pixel_values": pixel_values,
175
+ "image_grid_thw": image_grid_thw,
176
+ },
177
+ tensor_type=return_tensors,
178
+ )
179
+ return batch_feature
180
+
181
+
182
+class Cosmos3EdgeVideoProcessor(Qwen3VLVideoProcessor):
183
+ def __init__(self, **kwargs: Unpack[Qwen3VLVideoProcessorInitKwargs]):
184
+ super().__init__(**kwargs)
185
+
186
+ def _preprocess(
187
+ self,
188
+ videos: list[torch.Tensor],
189
+ do_convert_rgb: bool = True,
190
+ do_resize: bool = True,
191
+ size: SizeDict | None = None,
192
+ resample: PILImageResampling
193
+ | InterpolationMode
194
+ | int
195
+ | None = PILImageResampling.BICUBIC,
196
+ do_rescale: bool = True,
197
+ rescale_factor: float = 1 / 255.0,
198
+ do_normalize: bool = True,
199
+ image_mean: float | list[float] | None = None,
200
+ image_std: float | list[float] | None = None,
201
+ patch_size: int | None = None,
202
+ return_tensors: str | TensorType | None = None,
203
+ **kwargs,
204
+ ):
205
+ assert patch_size is not None
206
+
207
+ merge_size = self.merge_size
208
+ grouped_videos, grouped_videos_index = group_videos_by_shape(videos)
209
+ resized_videos_grouped = {}
210
+
211
+ for shape, stacked_videos in grouped_videos.items():
212
+ B, T, C, H, W = stacked_videos.shape
213
+ num_frames, height, width = T, H, W
214
+ if do_resize:
215
+ assert size is not None
216
+ # 1. Both dimensions (height and width) are divisible by 'factor'.
217
+ # 2. Total pixels are within the configured min/max range.
218
+ resized_height, resized_width = smart_resize(
219
+ num_frames=num_frames,
220
+ height=height,
221
+ width=width,
222
+ temporal_factor=1,
223
+ factor=patch_size * merge_size,
224
+ min_pixels=size.shortest_edge,
225
+ max_pixels=size.longest_edge,
226
+ )
227
+ stacked_videos = stacked_videos.view(B * T, C, H, W)
228
+ stacked_videos = self.resize(
229
+ stacked_videos,
230
+ size=SizeDict(height=resized_height, width=resized_width),
231
+ resample=resample,
232
+ )
233
+ stacked_videos = stacked_videos.view(
234
+ B, T, C, resized_height, resized_width
235
+ )
236
+ resized_videos_grouped[shape] = stacked_videos
237
+ resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index)
238
+
239
+ # Group videos by size for further processing
240
+ # Resize may return videos with different sizes.
241
+ grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos)
242
+ processed_videos_grouped = {}
243
+ processed_grids = {}
244
+ for shape, stacked_videos in grouped_videos.items():
245
+ resized_height, resized_width = get_image_size(
246
+ stacked_videos[0], channel_dim=ChannelDimension.FIRST
247
+ )
248
+ # Fused rescale and normalize
249
+ stacked_videos = self.rescale_and_normalize(
250
+ stacked_videos,
251
+ do_rescale,
252
+ rescale_factor,
253
+ do_normalize,
254
+ image_mean,
255
+ image_std,
256
+ )
257
+ patches = stacked_videos
258
+
259
+ batch_size, grid_t, channel = patches.shape[:3]
260
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
261
+
262
+ # [1, 16, 3, 320, 608] -> [1, 16, 3, 20, 16, 38, 16]
263
+ patches = patches.view(
264
+ batch_size, # 0
265
+ grid_t, # 1
266
+ channel, # 2
267
+ grid_h, # 3
268
+ patch_size, # 4
269
+ grid_w, # 5
270
+ patch_size, # 6
271
+ )
272
+ # [1, 16, 3, 20, 16, 38, 16] -> [1, 16, 20, 38, 16, 16, 3]
273
+ # Result: [batch, grid_t, grid_h, grid_w, patch_h, patch_w, channel]
274
+ patches = patches.permute(0, 1, 3, 5, 4, 6, 2)
275
+
276
+ # [1, 16, 20, 38, 16, 16, 3] -> [1, 5120, 768]
277
+ flatten_patches = patches.reshape(
278
+ batch_size,
279
+ grid_t * grid_h * grid_w,
280
+ patch_size * patch_size * channel,
281
+ )
282
+
283
+ processed_videos_grouped[shape] = flatten_patches
284
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
285
+
286
+ processed_videos = reorder_videos(
287
+ processed_videos_grouped, grouped_videos_index
288
+ )
289
+ processed_grids = reorder_videos(processed_grids, grouped_videos_index)
290
+ pixel_values_videos = torch.cat(processed_videos, dim=0)
291
+ video_grid_thw = torch.tensor(processed_grids)
292
+ data = {
293
+ "pixel_values_videos": pixel_values_videos,
294
+ "video_grid_thw": video_grid_thw,
295
+ }
296
+
297
+ return BatchFeature(data=data, tensor_type=return_tensors)
298
+
299
+
300
+class Cosmos3EdgeProcessor(Qwen3VLProcessor):
301
+ """Construct a processor for Cosmos3 Edge multimodal inputs.
302
+
303
+ This processor combines :class:`Cosmos3EdgeImageProcessor`,
304
+ :class:`Cosmos3EdgeVideoProcessor`, and a tokenizer into a single
305
+ interface. It inherits the text, image, and video input handling and the
306
+ decoding methods from :class:`Qwen3VLProcessor`, while using the Cosmos3
307
+ Edge-specific image and video preprocessing implementations.
308
+
309
+ Args:
310
+ image_processor: A :class:`Cosmos3EdgeImageProcessor` used to resize,
311
+ normalize, and patch image inputs.
312
+ tokenizer: The tokenizer used to encode and decode text.
313
+ video_processor: A :class:`Cosmos3EdgeVideoProcessor` used to resize,
314
+ normalize, and patch video inputs.
315
+ chat_template: An optional Jinja template used to render chat messages
316
+ into a tokenizable string.
317
+ """
318
+
319
+ attributes = ["image_processor", "tokenizer", "video_processor"]
320
+
321
+ # Transformers' auto loaders cannot resolve these vLLM-native image and
322
+ # video processors without remote code or global auto-class registration.
323
+ @classmethod
324
+ def _get_arguments_from_pretrained(
325
+ cls,
326
+ pretrained_model_name_or_path,
327
+ processor_dict=None,
328
+ **kwargs,
329
+ ):
330
+ component_kwargs = dict(kwargs)
331
+ component_kwargs.pop("trust_remote_code", None)
332
+
333
+ image_processor = Cosmos3EdgeImageProcessor.from_pretrained(
334
+ pretrained_model_name_or_path,
335
+ **component_kwargs,
336
+ )
337
+ tokenizer = AutoTokenizer.from_pretrained(
338
+ pretrained_model_name_or_path,
339
+ trust_remote_code=False,
340
+ **component_kwargs,
341
+ )
342
+ video_processor = Cosmos3EdgeVideoProcessor.from_pretrained(
343
+ pretrained_model_name_or_path,
344
+ **component_kwargs,
345
+ )
346
+
347
+ return [
348
+ image_processor,
349
+ tokenizer,
350
+ video_processor,
351
+ ]
352
+
353
+
354
+__all__ = [
355
+ "Cosmos3EdgeProcessor",
356
+]