A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
Support for NemotronH Nano VLM (#23644)
Signed-off-by: Daniel Afrimi <danielafrimi8@gmail.com>
danielafrimi committed
Sep 10, 2025 at 16:10 UTC
72d30108a0fe06a1ba14d8645dda830a9ab01791
4 files changed
+1400
-1
tests/models/registry.py
+3
@@ -515,6 +515,9 @@ _MULTIMODAL_EXAMPLE_MODELS = {
515
trust_remote_code=True),
516
"Llama_Nemotron_Nano_VL" : _HfExamplesInfo("nvidia/Llama-3.1-Nemotron-Nano-VL-8B-V1", # noqa: E501
517
trust_remote_code=True),
518
+ "NemotronH_Nano_VL": _HfExamplesInfo("nano_vl_dummy",
519
+ is_available_online=False,
520
+ trust_remote_code=True),
521
"Ovis": _HfExamplesInfo("AIDC-AI/Ovis2-1B", trust_remote_code=True,
522
max_transformers_version="4.53",
523
transformers_version_reason="HF model is not compatible", # noqa: E501
vllm/config/__init__.py
+1
-1
@@ -1552,7 +1552,7 @@ class ModelConfig:
1552
for bc in block_configs[start:end])
1553
else:
1554
# Hybrid model Jamba
1555
- layers_block_type_value = getattr(self.hf_config,
1555
+ layers_block_type_value = getattr(self.hf_text_config,
1556
"layers_block_type", None)
1557
if layers_block_type_value is not None:
1558
if hasattr(self.hf_text_config,
vllm/model_executor/models/nano_nemotron_vl.py
new
+1395
@@ -0,0 +1,1395 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# --------------------------------------------------------
4
+# Adapted from
5
+# https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/internvl.py
6
+# under Apache-2.0 License
7
+# LICENSE is in root directory.
8
+# --------------------------------------------------------
9
+
10
+import copy
11
+import warnings
12
+from abc import ABC, abstractmethod
13
+from collections.abc import Iterable, Mapping, Sequence
14
+from typing import Annotated, Any, Literal, Optional, TypedDict, TypeVar, Union
15
+
16
+import numpy.typing as npt
17
+import torch
18
+import torch.nn as nn
19
+import torchvision.transforms as T
20
+from PIL import Image
21
+from transformers import (AutoModel, BatchEncoding, BatchFeature,
22
+ PretrainedConfig, TensorType)
23
+
24
+from vllm.config import VllmConfig
25
+from vllm.model_executor.layers.activation import ReLUSquaredActivation
26
+from vllm.model_executor.layers.layernorm import RMSNorm
27
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
28
+from vllm.model_executor.models.interfaces import (HasInnerState, IsHybrid,
29
+ MultiModalEmbeddings,
30
+ SupportsMultiModal)
31
+from vllm.model_executor.models.internvl import (calculate_internvl_targets,
32
+ get_internvl_target_ratios)
33
+from vllm.model_executor.models.module_mapping import MultiModelKeys
34
+from vllm.model_executor.models.nemotron_h import NemotronHForCausalLM
35
+from vllm.model_executor.models.utils import (flatten_bn,
36
+ init_vllm_registered_model,
37
+ maybe_prefix,
38
+ merge_multimodal_embeddings)
39
+from vllm.model_executor.sampling_metadata import SamplingMetadata
40
+from vllm.multimodal import MULTIMODAL_REGISTRY
41
+from vllm.multimodal.inputs import (MultiModalDataDict, MultiModalFieldConfig,
42
+ MultiModalKwargs, MultiModalKwargsItems,
43
+ NestedTensors)
44
+from vllm.multimodal.parse import (ImageEmbeddingItems, ImageProcessorItems,
45
+ ImageSize, MultiModalDataItems)
46
+from vllm.multimodal.processing import (BaseMultiModalProcessor,
47
+ BaseProcessingInfo, PromptReplacement,
48
+ PromptUpdate, PromptUpdateDetails)
49
+from vllm.multimodal.profiling import BaseDummyInputsBuilder
50
+from vllm.sequence import IntermediateTensors
51
+from vllm.transformers_utils.tokenizer import AnyTokenizer
52
+from vllm.utils.tensor_schema import TensorSchema, TensorShape
53
+
54
+# Configure PIL to handle large images without warnings
55
+# This prevents DecompressionBombWarning for legitimate large images
56
+Image.MAX_IMAGE_PIXELS = None # Disable the limit entirely
57
+# Alternative: Set a specific higher limit
58
+# Image.MAX_IMAGE_PIXELS = 300000000 # ~300M pixels
59
+
60
+IMG_START = "<img>"
61
+IMG_END = "</img>"
62
+IMG_CONTEXT = "<image>"
63
+
64
+# Profiling
65
+MAX_FRAMES = 16
66
+
67
+
68
+class NanoNemotronVLImagePixelInputs(TypedDict):
69
+ type: Literal["pixel_values"]
70
+ pixel_values_flat: torch.Tensor
71
+ """
72
+ Shape:
73
+ `(batch_size * num_images * (1 + num_patches), num_channels, height, width)`
74
+ """
75
+
76
+ num_patches: torch.Tensor
77
+ """Shape: `(batch_size * num_images)`"""
78
+
79
+
80
+class NanoNemotronVLImageEmbeddinInputs(TypedDict):
81
+ type: Literal["image_embeds"]
82
+ data: Union[torch.Tensor, list[torch.Tensor]]
83
+ """
84
+ A tensor of shape `(num_images, total_image_feature_size, hidden_size)`
85
+ or a list of tensors of shape `(total_image_feature_size, hidden_size)`
86
+
87
+ `hidden_size` must match the hidden size of language model backbone.
88
+ """
89
+
90
+
91
+NanoNemotronVLImageInputs = Union[NanoNemotronVLImagePixelInputs,
92
+ NanoNemotronVLImageEmbeddinInputs]
93
+
94
+
95
+class NanoNemotronVLVideoPixelInputs(TensorSchema):
96
+ """
97
+ Dimensions:
98
+ - bvf: Batch size * number of videos * num_frames
99
+ - bn: Batch size * number of images
100
+ - c: Number of channels (3)
101
+ - h: Height of each video frame
102
+ - w: Width of each video frame
103
+ """
104
+ type: Literal["pixel_values_videos"]
105
+ pixel_values_flat: Annotated[torch.Tensor, TensorShape("bvf", 3, "h", "w")]
106
+ num_patches: Annotated[torch.Tensor, TensorShape("bn")]
107
+
108
+
109
+class NanoNemotronVLVideoEmbeddingInputs(TensorSchema):
110
+ """
111
+ Dimensions:
112
+ - n: Number of videos
113
+ - f: Total video feature size
114
+ - h: Hidden size (must match the hidden size of language model backbone)
115
+ """
116
+ type: Literal["video_embeds"]
117
+ data: Annotated[Union[torch.Tensor, list[torch.Tensor]],
118
+ TensorShape("n", "f", "h")]
119
+
120
+
121
+NanoNemotronVLVideoInputs = Union[NanoNemotronVLVideoPixelInputs,
122
+ NanoNemotronVLVideoEmbeddingInputs]
123
+
124
+
125
+def input_conditioner(x, norm_mean, norm_std):
126
+ y = (x - norm_mean) / norm_std
127
+ return y
128
+
129
+
130
+def dynamic_preprocess(image,
131
+ *,
132
+ image_size=512,
133
+ max_num_tiles=12,
134
+ use_thumbnail=True,
135
+ idx=0):
136
+ orig_width, orig_height = image.size
137
+
138
+ target_ratios = get_internvl_target_ratios(1, max_num_tiles)
139
+
140
+ blocks, target_width, target_height = calculate_internvl_targets(
141
+ orig_width=orig_width,
142
+ orig_height=orig_height,
143
+ target_ratios=target_ratios,
144
+ image_size=image_size,
145
+ use_thumbnail=False)
146
+ # resize the image
147
+ resized_img = image.resize((target_width, target_height))
148
+ processed_images = []
149
+ for i in range(blocks):
150
+ box = (
151
+ (i % (target_width // image_size)) * image_size,
152
+ (i // (target_width // image_size)) * image_size,
153
+ ((i % (target_width // image_size)) + 1) * image_size,
154
+ ((i // (target_width // image_size)) + 1) * image_size,
155
+ )
156
+ # split the image
157
+ split_img = resized_img.crop(box)
158
+ processed_images.append(split_img)
159
+ assert len(processed_images) == blocks
160
+ if use_thumbnail and len(processed_images) != 1:
161
+ thumbnail_img = image.resize((image_size, image_size))
162
+ processed_images.append(thumbnail_img)
163
+
164
+ processed_images = [
165
+ img.convert("RGB") if img.mode != "RGB" else img
166
+ for img in processed_images
167
+ ]
168
+ processed_images = [
169
+ T.Resize((image_size, image_size),
170
+ interpolation=T.InterpolationMode.BICUBIC)(img)
171
+ for img in processed_images
172
+ ]
173
+ processed_images = [T.ToTensor()(img) for img in processed_images]
174
+ return processed_images
175
+
176
+
177
+def image_to_pixel_values(
178
+ image: Image.Image,
179
+ *,
180
+ input_size: int,
181
+ max_num: int,
182
+ use_thumbnail: bool,
183
+ idx: int,
184
+) -> torch.Tensor:
185
+ images = dynamic_preprocess(
186
+ image,
187
+ image_size=input_size,
188
+ max_num_tiles=max_num,
189
+ use_thumbnail=use_thumbnail,
190
+ idx=idx,
191
+ )
192
+
193
+ pixel_values = torch.stack(images)
194
+ return pixel_values
195
+
196
+
197
+def video_to_pixel_values(
198
+ video: npt.NDArray,
199
+ *,
200
+ input_size: int,
201
+ max_num_tiles: int = 1,
202
+ use_thumbnail: bool,
203
+) -> torch.Tensor:
204
+ # Convert each frame to a single resized tile tensor consistent
205
+ # with image path
206
+ frames_tensors: list[torch.Tensor] = []
207
+ for frame in video:
208
+ pil_frame = dynamic_preprocess(
209
+ Image.fromarray(frame, mode="RGB"),
210
+ image_size=input_size,
211
+ max_num_tiles=max_num_tiles,
212
+ use_thumbnail=use_thumbnail,
213
+ idx=0,
214
+ )
215
+ # dynamic_preprocess returns tensors already; take the single tile
216
+ assert len(pil_frame) >= 1
217
+ frames_tensors.append(pil_frame[0])
218
+
219
+ return torch.stack(frames_tensors)
220
+
221
+
222
+class BaseNanoNemotronVLProcessor(ABC):
223
+ """
224
+ This model doesn't define its own HF processor,
225
+ so we implement our own one here.
226
+
227
+ The code to insert image tokens is based on:
228
+ https://huggingface.co/OpenGVLab/InternVL2-1B/blob/main/modeling_internvl_chat.py#L252
229
+ """
230
+
231
+ def __init__(self, config: PretrainedConfig, tokenizer: AnyTokenizer,
232
+ *args, **kwargs) -> None:
233
+ super().__init__()
234
+
235
+ self.config = config
236
+ self.tokenizer = tokenizer
237
+
238
+ image_size: int = config.force_image_size
239
+ patch_size: int = config.patch_size
240
+
241
+ self.num_image_token = int(
242
+ (image_size // patch_size)**2 * (config.downsample_ratio**2))
243
+ self.image_size = image_size
244
+ self.use_thumbnail: bool = config.use_thumbnail
245
+ self.norm_mean = torch.Tensor(config.norm_mean).reshape(1, 3, 1, 1)
246
+ self.norm_std = torch.Tensor(config.norm_std).reshape(1, 3, 1, 1)
247
+
248
+ @property
249
+ @abstractmethod
250
+ def image_token_id(self) -> int:
251
+ raise NotImplementedError
252
+
253
+ @abstractmethod
254
+ def get_image_repl(
255
+ self,
256
+ feature_size: int,
257
+ num_patches: Optional[int],
258
+ ) -> PromptUpdateDetails[str]:
259
+ raise NotImplementedError
260
+
261
+ def get_num_image_tokens(
262
+ self,
263
+ *,
264
+ image_width: int,
265
+ image_height: int,
266
+ max_num_tiles: int,
267
+ ) -> int:
268
+ target_ratios = get_internvl_target_ratios(1, max_num_tiles)
269
+
270
+ num_patches, _, _ = calculate_internvl_targets(
271
+ orig_width=image_width,
272
+ orig_height=image_height,
273
+ target_ratios=target_ratios,
274
+ image_size=self.image_size,
275
+ use_thumbnail=self.use_thumbnail,
276
+ )
277
+
278
+ return num_patches * self.num_image_token
279
+
280
+ def _images_to_pixel_values_lst(
281
+ self,
282
+ images: list[Image.Image],
283
+ max_num_tiles: int,
284
+ ) -> list[torch.Tensor]:
285
+ return [
286
+ image_to_pixel_values(
287
+ image,
288
+ input_size=self.image_size,
289
+ max_num=max_num_tiles,
290
+ use_thumbnail=self.use_thumbnail,
291
+ idx=idx,
292
+ ) for idx, image in enumerate(images)
293
+ ]
294
+
295
+ def _preprocess_image(
296
+ self,
297
+ text: list[str],
298
+ images: list[Image.Image],
299
+ max_num_tiles: int,
300
+ ) -> tuple[list[str], dict[str, torch.Tensor]]:
301
+ if len(images) == 0:
302
+ image_inputs = {}
303
+ else:
304
+ pixel_values_lst = self._images_to_pixel_values_lst(
305
+ images, max_num_tiles)
306
+ image_inputs: dict[str, NestedTensors] = {
307
+ "pixel_values_flat":
308
+ input_conditioner(torch.cat(pixel_values_lst), self.norm_mean,
309
+ self.norm_std),
310
+ "image_num_patches":
311
+ torch.tensor([len(item) for item in pixel_values_lst]),
312
+ }
313
+
314
+ for pixel_values in pixel_values_lst:
315
+ num_patches = pixel_values.shape[0]
316
+ feature_size = num_patches * self.num_image_token
317
+ image_repl = self.get_image_repl(feature_size, num_patches)
318
+ text = [t.replace('<image>', image_repl.full, 1) for t in text]
319
+ return text, image_inputs
320
+
321
+ def _make_batch_input(self,
322
+ input_item: Optional[Union[Any, list[Any]]] = None):
323
+ if input_item is None:
324
+ input_item = []
325
+ if not isinstance(input_item, list):
326
+ input_item = [input_item]
327
+ return input_item
328
+
329
+ def __call__(
330
+ self,
331
+ text: Optional[Union[str, list[str]]] = None,
332
+ images: Optional[Union[Image.Image, list[Image.Image]]] = None,
333
+ return_tensors: Optional[Union[str, TensorType]] = None,
334
+ max_num_tiles: Optional[int] = None,
335
+ ) -> Mapping[str, NestedTensors]:
336
+ # Use default if not provided
337
+ if max_num_tiles is None:
338
+ max_num_tiles = 12
339
+
340
+ text, images = [self._make_batch_input(x) for x in (text, images)]
341
+
342
+ text, image_inputs = self._preprocess_image(
343
+ text=text,
344
+ images=images,
345
+ max_num_tiles=max_num_tiles,
346
+ )
347
+
348
+ text_inputs = self.tokenizer(text, add_special_tokens=False)
349
+
350
+ return {
351
+ **BatchEncoding(text_inputs, tensor_type=return_tensors),
352
+ **image_inputs,
353
+ }
354
+
355
+
356
+class NanoNemotronVLProcessor(BaseNanoNemotronVLProcessor):
357
+ """
358
+ HF Processor with extended video processing logic.
359
+ Code for video processing is adapted from video example:
360
+ https://huggingface.co/OpenGVLab/InternVL3-1B#inference-with-transformers
361
+ """
362
+
363
+ def __init__(
364
+ self,
365
+ config: PretrainedConfig,
366
+ tokenizer: AnyTokenizer,
367
+ *,
368
+ min_dynamic_patch: Optional[int] = None,
369
+ max_dynamic_patch: Optional[int] = None,
370
+ dynamic_image_size: Optional[bool] = None,
371
+ video_token: Optional[str] = None,
372
+ ) -> None:
373
+ super().__init__(
374
+ config=config,
375
+ tokenizer=tokenizer,
376
+ min_dynamic_patch=min_dynamic_patch,
377
+ max_dynamic_patch=max_dynamic_patch,
378
+ dynamic_image_size=dynamic_image_size,
379
+ )
380
+ # add extra video token for video processing
381
+ self.video_token = video_token
382
+
383
+ @property
384
+ def supports_video(self) -> bool:
385
+ return self.video_token_id is not None
386
+
387
+ @property
388
+ def video_token_id(self) -> Optional[int]:
389
+ if self.video_token is None:
390
+ return None
391
+ return self.tokenizer.get_vocab().get(self.video_token, None)
392
+
393
+ @property
394
+ def image_token_id(self) -> int:
395
+ return self.tokenizer.convert_tokens_to_ids(IMG_CONTEXT)
396
+
397
+ def _videos_to_pixel_values_lst(
398
+ self,
399
+ videos: list[npt.NDArray],
400
+ max_num_tiles: int,
401
+ dynamic_image_size: Optional[bool] = None,
402
+ ) -> list[torch.Tensor]:
403
+
404
+ return [
405
+ video_to_pixel_values(
406
+ video,
407
+ input_size=self.image_size,
408
+ max_num_tiles=max_num_tiles,
409
+ use_thumbnail=self.use_thumbnail,
410
+ ) for video in videos
411
+ ]
412
+
413
+ def _preprocess_video(
414
+ self,
415
+ text: list[str],
416
+ videos: list[npt.NDArray],
417
+ max_num_tiles: int,
418
+ dynamic_image_size: Optional[bool] = None,
419
+ ):
420
+ if len(videos) == 0 or not self.supports_video:
421
+ video_inputs = {}
422
+ else:
423
+ pixel_values_lst_video = self._videos_to_pixel_values_lst(
424
+ videos,
425
+ max_num_tiles=max_num_tiles,
426
+ dynamic_image_size=dynamic_image_size,
427
+ )
428
+
429
+ video_inputs: dict[str, NestedTensors] = {
430
+ "pixel_values_flat_video":
431
+ input_conditioner(torch.cat(pixel_values_lst_video),
432
+ self.norm_mean, self.norm_std),
433
+ "video_num_patches":
434
+ torch.tensor([len(item) for item in pixel_values_lst_video]),
435
+ }
436
+
437
+ for pixel_values in pixel_values_lst_video:
438
+ num_patches = pixel_values.shape[0]
439
+
440
+ video_repl = self.get_video_repl(self.num_image_token,
441
+ num_patches, self.video_token)
442
+ text = [t.replace('<video>', video_repl.full, 1) for t in text]
443
+ return text, video_inputs
444
+
445
+ def __call__(
446
+ self,
447
+ text: Optional[Union[str, list[str]]] = None,
448
+ images: Optional[Union[Image.Image, list[Image.Image]]] = None,
449
+ videos: Optional[Union[npt.NDArray, list[npt.NDArray]]] = None,
450
+ return_tensors: Optional[Union[str, TensorType]] = None,
451
+ max_num_tiles: Optional[int] = None,
452
+ dynamic_image_size: Optional[bool] = None,
453
+ ) -> Mapping[str, NestedTensors]:
454
+ # Use default if not provided
455
+ if max_num_tiles is None:
456
+ max_num_tiles = 12
457
+
458
+ text, images, videos = [
459
+ self._make_batch_input(x) for x in (text, images, videos)
460
+ ]
461
+
462
+ text, image_inputs = self._preprocess_image(
463
+ text=text,
464
+ images=images,
465
+ max_num_tiles=max_num_tiles,
466
+ )
467
+
468
+ text, video_inputs = self._preprocess_video(
469
+ text=text,
470
+ videos=videos,
471
+ max_num_tiles=max_num_tiles,
472
+ dynamic_image_size=dynamic_image_size,
473
+ )
474
+
475
+ text_inputs = self.tokenizer(text, add_special_tokens=False)
476
+
477
+ return BatchFeature({
478
+ **BatchEncoding(text_inputs, tensor_type=return_tensors),
479
+ **image_inputs,
480
+ **video_inputs,
481
+ })
482
+
483
+ def get_image_repl(
484
+ self,
485
+ feature_size: int,
486
+ num_patches: Optional[int],
487
+ ) -> PromptUpdateDetails[str]:
488
+ repl_features = IMG_CONTEXT * feature_size
489
+ repl_full = IMG_START + repl_features + IMG_END
490
+
491
+ return PromptUpdateDetails.select_text(repl_full, IMG_CONTEXT)
492
+
493
+ def get_video_repl(
494
+ self,
495
+ feature_size: int,
496
+ num_patches: Optional[int] = None,
497
+ video_context_token: str = IMG_CONTEXT,
498
+ ) -> PromptUpdateDetails[str]:
499
+ repl_features = video_context_token * self.num_image_token
500
+ repl_features_with_sep = IMG_START + repl_features + IMG_END
501
+ # num_patches is equal to num_frames
502
+ repl_full = ''.join([
503
+ f'Frame{i+1}: {repl_features_with_sep}' for i in range(num_patches)
504
+ ])
505
+
506
+ return PromptUpdateDetails.select_text(repl_full, video_context_token)
507
+
508
+
509
+class BaseNanoNemotronVLProcessingInfo(BaseProcessingInfo):
510
+ """Basic image-only ProcessingInfo for InternVL-style models."""
511
+
512
+ @abstractmethod
513
+ def get_hf_processor(
514
+ self,
515
+ **kwargs: object,
516
+ ) -> BaseNanoNemotronVLProcessor:
517
+ raise NotImplementedError
518
+
519
+ def get_supported_mm_limits(self) -> Mapping[str, Optional[int]]:
520
+ return {"image": None}
521
+
522
+ def get_num_image_tokens(
523
+ self,
524
+ *,
525
+ image_width: int,
526
+ image_height: int,
527
+ max_num_tiles: int,
528
+ processor: Optional[BaseNanoNemotronVLProcessor],
529
+ ) -> int:
530
+ if processor is None:
531
+ processor = self.get_hf_processor()
532
+
533
+ return processor.get_num_image_tokens(
534
+ image_width=image_width,
535
+ image_height=image_height,
536
+ max_num_tiles=max_num_tiles,
537
+ )
538
+
539
+ def get_image_size_with_most_features(self,
540
+ max_num_tiles: int) -> ImageSize:
541
+ processor = self.get_hf_processor()
542
+
543
+ base_size = processor.image_size
544
+ target_ratios = get_internvl_target_ratios(1, max_num_tiles)
545
+
546
+ largest_feature_size, largest_feature_pinpoint = 0, None
547
+ for wr, hr in target_ratios:
548
+ width, height = base_size * wr, base_size * hr
549
+
550
+ feat_size = self.get_num_image_tokens(
551
+ image_width=width,
552
+ image_height=height,
553
+ max_num_tiles=max_num_tiles,
554
+ processor=processor,
555
+ )
556
+ if feat_size > largest_feature_size:
557
+ largest_feature_size = feat_size
558
+ largest_feature_pinpoint = ImageSize(width=width,
559
+ height=height)
560
+
561
+ if largest_feature_size == 0 or largest_feature_pinpoint is None:
562
+ raise ValueError("Cannot have a largest feature size of 0!")
563
+
564
+ return largest_feature_pinpoint
565
+
566
+ def get_max_image_tokens(self) -> int:
567
+ processor = self.get_hf_processor()
568
+ # Use default max_num_tiles for max tokens calculation
569
+ max_num_tiles = 12
570
+ target_width, target_height = self.get_image_size_with_most_features(
571
+ max_num_tiles)
572
+
573
+ return self.get_num_image_tokens(
574
+ image_width=target_width,
575
+ image_height=target_height,
576
+ max_num_tiles=max_num_tiles,
577
+ processor=processor,
578
+ )
579
+
580
+
581
+_I = TypeVar("_I", bound=BaseNanoNemotronVLProcessingInfo)
582
+
583
+
584
+class NanoNemotronVLProcessingInfo(BaseNanoNemotronVLProcessingInfo):
585
+ """ ProcessingInfo extended for video processing"""
586
+
587
+ @property
588
+ def supports_video(self):
589
+ return self.get_hf_processor().supports_video
590
+
591
+ def get_supported_mm_limits(self):
592
+ video_limit = {"video": None} if self.supports_video else {}
593
+ return {**super().get_supported_mm_limits(), **video_limit}
594
+
595
+ def get_video_token(self) -> Optional[str]:
596
+ return IMG_CONTEXT
597
+
598
+ def get_num_frames_with_most_features(
599
+ self,
600
+ seq_len: int,
601
+ mm_counts: Mapping[str, int],
602
+ ) -> int:
603
+ max_images = mm_counts.get("image", 0)
604
+ max_videos = mm_counts.get("video", 0)
605
+
606
+ processor = self.get_hf_processor() # we get the CustomProcessor here
607
+
608
+ max_image_tokens = self.get_max_image_tokens() * max_images
609
+ max_total_frames = (seq_len -
610
+ max_image_tokens) // processor.num_image_token
611
+ max_frames_per_video = max_total_frames // max(max_videos, 1)
612
+
613
+ max_frames_per_video = min(max_frames_per_video, MAX_FRAMES)
614
+ return max(max_frames_per_video, 1)
615
+
616
+ def get_hf_processor(self, **kwargs: object) -> NanoNemotronVLProcessor:
617
+ return self.ctx.init_processor(
618
+ NanoNemotronVLProcessor,
619
+ config=self.get_hf_config(),
620
+ tokenizer=self.get_tokenizer(),
621
+ video_token=self.get_video_token(),
622
+ **kwargs,
623
+ )
624
+
625
+
626
+class NanoNemotronBaseVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
627
+ """Basic image-only MultiModalProcessor for InternVL-style models."""
628
+
629
+ def _call_hf_processor(
630
+ self,
631
+ prompt: str,
632
+ mm_data: Mapping[str, object],
633
+ mm_kwargs: Mapping[str, object],
634
+ tok_kwargs: Mapping[str, object],
635
+ ) -> Mapping[str, NestedTensors]:
636
+ processed_outputs = super()._call_hf_processor(
637
+ prompt=prompt,
638
+ mm_data=mm_data,
639
+ mm_kwargs=mm_kwargs,
640
+ tok_kwargs=tok_kwargs,
641
+ )
642
+
643
+ hf_processor = self.info.get_hf_processor(**mm_kwargs)
644
+ image_token_id = hf_processor.image_token_id
645
+
646
+ # Since there may be extra tokens in the feature placeholders,
647
+ # we need to pass the image token ID to the model to select the
648
+ # tokens to merge from the vision encoder outputs
649
+ processed_outputs["image_token_id"] = torch.tensor(image_token_id)
650
+
651
+ return processed_outputs
652
+
653
+ def _get_mm_fields_config(
654
+ self,
655
+ hf_inputs: Mapping[str, NestedTensors],
656
+ hf_processor_mm_kwargs: Mapping[str, object],
657
+ ) -> Mapping[str, MultiModalFieldConfig]:
658
+ image_num_patches = hf_inputs.get("image_num_patches", torch.empty(0))
659
+ num_images = len(image_num_patches)
660
+
661
+ return dict(
662
+ pixel_values_flat=MultiModalFieldConfig.flat_from_sizes(
663
+ "image", image_num_patches),
664
+ image_num_patches=MultiModalFieldConfig.batched("image"),
665
+ image_embeds=MultiModalFieldConfig.batched("image"),
666
+ image_token_id=MultiModalFieldConfig.shared("image", num_images),
667
+ )
668
+
669
+ def _get_prompt_updates(
670
+ self,
671
+ mm_items: MultiModalDataItems,
672
+ hf_processor_mm_kwargs: Mapping[str, object],
673
+ out_mm_kwargs: MultiModalKwargs,
674
+ ) -> Sequence[PromptUpdate]:
675
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
676
+
677
+ if "image_num_patches" in out_mm_kwargs:
678
+ image_num_patches = out_mm_kwargs["image_num_patches"]
679
+ assert isinstance(image_num_patches, torch.Tensor)
680
+ image_num_patches = image_num_patches.tolist()
681
+ elif "image_embeds" in out_mm_kwargs:
682
+ # to compute num_patches (similar to Qwen2-VL)
683
+ image_num_patches = [None] * len(out_mm_kwargs["image_embeds"])
684
+ else:
685
+ image_num_patches = []
686
+
687
+ def get_replacement_custom(item_idx: int):
688
+ images = mm_items.get_items(
689
+ "image", (ImageEmbeddingItems, ImageProcessorItems))
690
+
691
+ if isinstance(images, ImageEmbeddingItems):
692
+ feature_size = images.get_feature_size(item_idx)
693
+ else:
694
+ image_size = images.get_image_size(item_idx)
695
+ # Extract max_num_tiles from kwargs, default to 12
696
+ max_num_tiles = hf_processor_mm_kwargs.get("max_num_tiles", 12)
697
+ feature_size = self.info.get_num_image_tokens(
698
+ image_width=image_size.width,
699
+ image_height=image_size.height,
700
+ max_num_tiles=max_num_tiles,
701
+ processor=hf_processor,
702
+ )
703
+
704
+ num_patches = None
705
+ local_image_num_patches = image_num_patches
706
+ if isinstance(local_image_num_patches, torch.Tensor):
707
+ local_image_num_patches = local_image_num_patches.tolist()
708
+ if isinstance(
709
+ local_image_num_patches,
710
+ (list, tuple)) and item_idx < len(local_image_num_patches):
711
+ num_patches = int(local_image_num_patches[item_idx])
712
+
713
+ return hf_processor.get_image_repl(feature_size, num_patches)
714
+
715
+ return [
716
+ PromptReplacement(
717
+ modality="image",
718
+ target="<image>",
719
+ replacement=get_replacement_custom,
720
+ )
721
+ ]
722
+
723
+
724
+class NanoNemotronVLMultiModalProcessor(
725
+ NanoNemotronBaseVLMultiModalProcessor[NanoNemotronVLProcessingInfo]):
726
+ """MultiModalProcessor extended for video support"""
727
+
728
+ def _call_hf_processor(
729
+ self,
730
+ prompt: str,
731
+ mm_data: Mapping[str, object],
732
+ mm_kwargs: Mapping[str, object],
733
+ tok_kwargs: Mapping[str, object],
734
+ ) -> Mapping[str, NestedTensors]:
735
+ processed_outputs = super()._call_hf_processor(prompt, mm_data,
736
+ mm_kwargs, tok_kwargs)
737
+
738
+ hf_processor = self.info.get_hf_processor(**mm_kwargs)
739
+ if self.info.supports_video and (
740
+ video_token_id := hf_processor.video_token_id) is not None:
741
+ processed_outputs["video_token_id"] = torch.tensor(video_token_id)
742
+ return processed_outputs
743
+
744
+ def _get_mm_fields_config(
745
+ self,
746
+ hf_inputs: Mapping[str, NestedTensors],
747
+ hf_processor_mm_kwargs: Mapping[str, object],
748
+ ) -> Mapping[str, MultiModalFieldConfig]:
749
+ image_fields = super()._get_mm_fields_config(hf_inputs,
750
+ hf_processor_mm_kwargs)
751
+ if self.info.supports_video:
752
+ video_num_patches = hf_inputs.get("video_num_patches",
753
+ torch.empty(0))
754
+ num_videos = len(video_num_patches)
755
+ video_fields = dict(
756
+ pixel_values_flat_video=MultiModalFieldConfig.flat_from_sizes(
757
+ "video", video_num_patches),
758
+ video_num_patches=MultiModalFieldConfig.batched("video"),
759
+ video_token_id=MultiModalFieldConfig.shared(
760
+ "video", num_videos))
761
+ else:
762
+ video_fields = {}
763
+
764
+ return image_fields | video_fields
765
+
766
+ def _get_prompt_updates(
767
+ self,
768
+ mm_items: MultiModalDataItems,
769
+ hf_processor_mm_kwargs: Mapping[str, object],
770
+ out_mm_kwargs: MultiModalKwargsItems,
771
+ ) -> Sequence[PromptUpdate]:
772
+ prompt_repl = super()._get_prompt_updates(
773
+ mm_items=mm_items,
774
+ hf_processor_mm_kwargs=hf_processor_mm_kwargs,
775
+ out_mm_kwargs=out_mm_kwargs,
776
+ )
777
+
778
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
779
+
780
+ out_mm_data = out_mm_kwargs.get_data()
781
+ if "video_num_patches" in out_mm_data:
782
+ video_num_patches = out_mm_data["video_num_patches"]
783
+ assert isinstance(video_num_patches, torch.Tensor)
784
+ video_num_patches = video_num_patches.tolist()
785
+ else:
786
+ video_num_patches = []
787
+
788
+ def get_video_replacement_internvl(item_idx: int):
789
+ feature_size = hf_processor.num_image_token
790
+ num_patches = video_num_patches[item_idx]
791
+ if num_patches is not None:
792
+ assert isinstance(num_patches, int)
793
+
794
+ return hf_processor.get_video_repl(
795
+ feature_size,
796
+ num_patches,
797
+ video_context_token=hf_processor.video_token)
798
+
799
+ if self.info.supports_video:
800
+ prompt_repl = [
801
+ *prompt_repl,
802
+ PromptReplacement(
803
+ modality="video",
804
+ target="<video>",
805
+ replacement=get_video_replacement_internvl,
806
+ )
807
+ ]
808
+
809
+ return prompt_repl
810
+
811
+
812
+class NanoNemotronVLDummyInputsBuilder(BaseDummyInputsBuilder[_I]):
813
+ """Basic image-only DummyInputsBuilder for InternVL-style models."""
814
+
815
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
816
+ num_images = mm_counts.get("image", 0)
817
+
818
+ return "<image>" * num_images
819
+
820
+ def get_dummy_mm_data(
821
+ self,
822
+ seq_len: int,
823
+ mm_counts: Mapping[str, int],
824
+ ) -> MultiModalDataDict:
825
+ # Use default max_num_tiles for dummy data generation
826
+ max_num_tiles = 12
827
+ target_width, target_height = (
828
+ self.info.get_image_size_with_most_features(max_num_tiles))
829
+ num_images = mm_counts.get("image", 0)
830
+
831
+ return {
832
+ "image":
833
+ self._get_dummy_images(width=target_width,
834
+ height=target_height,
835
+ num_images=num_images)
836
+ }
837
+
838
+
839
+class NanoNemotronVLDummyInputsBuilder(
840
+ NanoNemotronVLDummyInputsBuilder[NanoNemotronVLProcessingInfo]):
841
+ """DummyInputsBuilder extended for video support"""
842
+
843
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
844
+ num_videos = mm_counts.get("video", 0)
845
+
846
+ return super().get_dummy_text(mm_counts) + "<video>" * num_videos
847
+
848
+ def get_dummy_mm_data(
849
+ self,
850
+ seq_len: int,
851
+ mm_counts: Mapping[str, int],
852
+ ) -> MultiModalDataDict:
853
+ dummy_image = super().get_dummy_mm_data(seq_len=seq_len,
854
+ mm_counts=mm_counts)
855
+ if self.info.supports_video:
856
+ config = self.info.get_hf_config()
857
+ image_size: int = config.force_image_size
858
+ target_num_frames = \
859
+ self.info.get_num_frames_with_most_features(seq_len, mm_counts)
860
+ num_videos = mm_counts.get("video", 0)
861
+ dummy_video = {
862
+ "video":
863
+ self._get_dummy_videos(width=image_size,
864
+ height=image_size,
865
+ num_frames=target_num_frames,
866
+ num_videos=num_videos)
867
+ }
868
+ else:
869
+ dummy_video = {}
870
+ return {**dummy_image, **dummy_video}
871
+
872
+
873
+@MULTIMODAL_REGISTRY.register_processor(
874
+ NanoNemotronVLMultiModalProcessor,
875
+ info=NanoNemotronVLProcessingInfo,
876
+ dummy_inputs=NanoNemotronVLDummyInputsBuilder,
877
+)
878
+class NemotronH_Nano_VL(nn.Module, HasInnerState, IsHybrid,
879
+ SupportsMultiModal):
880
+
881
+ @classmethod
882
+ def get_placeholder_str(cls, modality: str, i: int) -> Optional[str]:
883
+ if modality.startswith("image"):
884
+ return "<image>"
885
+ if modality.startswith("video"):
886
+ return "<video>"
887
+ return None
888
+
889
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
890
+ super().__init__()
891
+ config = vllm_config.model_config.hf_config
892
+
893
+ image_size = config.force_image_size
894
+ patch_size = config.patch_size
895
+ self.patch_size = patch_size
896
+ self.template = config.template
897
+ self.num_image_token = int(
898
+ (image_size // patch_size)**2 * (config.downsample_ratio**2))
899
+ self.downsample_ratio = config.downsample_ratio
900
+ self.ps_version = config.ps_version
901
+ self.image_tag_type = config.image_tag_type
902
+
903
+ self.language_model = init_vllm_registered_model(
904
+ vllm_config=vllm_config,
905
+ hf_config=config.text_config,
906
+ prefix=maybe_prefix(prefix, "language_model"),
907
+ )
908
+ self.vision_model = AutoModel.from_config(config.vision_config,
909
+ trust_remote_code=True)
910
+ self.vision_model.model._initialize_weights = (
911
+ self.vision_model.model._init_weights)
912
+ # Move input normalization to processor to mirror original HF
913
+ # implementation where normalization is done in fp32
914
+ self.vision_model.radio_model.make_preprocessor_external()
915
+ self.vision_model = self.vision_model.to(
916
+ self.language_model.config.torch_dtype)
917
+
918
+ self.drop_vision_class_token = True
919
+
920
+ # Construct the vision projection.
921
+ vit_hidden_size = config.vit_hidden_size
922
+ vision_projection_hidden_size = config.projector_hidden_size
923
+ llm_hidden_size = config.text_config.hidden_size
924
+
925
+ self.mlp1 = nn.Sequential(
926
+ RMSNorm(hidden_size=vit_hidden_size *
927
+ int(1 / self.downsample_ratio)**2,
928
+ eps=1e-5),
929
+ nn.Linear(
930
+ vit_hidden_size * int(1 / self.downsample_ratio)**2,
931
+ vision_projection_hidden_size,
932
+ bias=False,
933
+ ),
934
+ ReLUSquaredActivation(),
935
+ nn.Linear(vision_projection_hidden_size,
936
+ llm_hidden_size,
937
+ bias=False),
938
+ )
939
+ self.mlp1 = self.mlp1.to(self.language_model.config.torch_dtype)
940
+
941
+ self.img_context_token_id = None
942
+ self.video_context_token_id = None
943
+ self.config = config
944
+
945
+ def pixel_shuffle(self, x, scale_factor=0.5):
946
+ n, w, h, c = x.size()
947
+ # N, W, H, C --> N, W, H * scale, C // scale
948
+ x = x.view(
949
+ n,
950
+ w,
951
+ int(h * scale_factor),
952
+ int(c / scale_factor),
953
+ )
954
+ # N, W, H * scale, C // scale --> N, H * scale, W, C // scale
955
+ x = x.permute(0, 2, 1, 3).contiguous()
956
+ # N, H * scale, W, C // scale -->
957
+ # N, H * scale, W * scale, C // (scale ** 2)
958
+ x = x.view(
959
+ n,
960
+ int(h * scale_factor),
961
+ int(w * scale_factor),
962
+ int(c / (scale_factor * scale_factor)),
963
+ )
964
+ if self.ps_version == "v1":
965
+ warnings.warn(
966
+ "In ps_version 'v1', the height and width have not "
967
+ "been swapped back, which results in a transposed image.",
968
+ stacklevel=2,
969
+ )
970
+ else:
971
+ x = x.permute(0, 2, 1, 3).contiguous()
972
+ return x
973
+
974
+ def extract_feature(self, pixel_values):
975
+ vit_embeds = self.vision_model(pixel_values).features
976
+ vit_embeds = vit_embeds.to(dtype=torch.bfloat16)
977
+ h = w = int(vit_embeds.shape[1]**0.5)
978
+ vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], h, w, -1)
979
+ vit_embeds = self.pixel_shuffle(vit_embeds,
980
+ scale_factor=self.downsample_ratio)
981
+ vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], -1,
982
+ vit_embeds.shape[-1])
983
+ vit_embeds = self.mlp1(vit_embeds)
984
+ return vit_embeds
985
+
986
+ def _parse_and_validate_image_input(
987
+ self, **kwargs: object) -> Optional[NanoNemotronVLImageInputs]:
988
+ pixel_values_flat = kwargs.pop("pixel_values_flat", None)
989
+ image_num_patches = kwargs.pop("image_num_patches", None)
990
+ image_embeds = kwargs.pop("image_embeds", None)
991
+
992
+ if pixel_values_flat is None and image_embeds is None:
993
+ return None
994
+
995
+ if image_embeds is not None:
996
+ if not isinstance(image_embeds, (torch.Tensor, list)):
997
+ raise ValueError("Incorrect type of image embeddings. "
998
+ f"Got type: {type(image_embeds)}")
999
+
1000
+ return NanoNemotronVLImageEmbeddinInputs(
1001
+ type="image_embeds",
1002
+ data=flatten_bn(image_embeds),
1003
+ )
1004
+
1005
+ image_token_id = kwargs["image_token_id"]
1006
+ assert isinstance(image_token_id, torch.Tensor)
1007
+ self.img_context_token_id = image_token_id.flatten().unique().item()
1008
+
1009
+ if pixel_values_flat is not None:
1010
+ if not isinstance(pixel_values_flat, (torch.Tensor, list)):
1011
+ raise ValueError("Incorrect type of pixel values. "
1012
+ f"Got type: {type(pixel_values_flat)}")
1013
+
1014
+ if not isinstance(image_num_patches, (torch.Tensor, list)):
1015
+ raise ValueError("Incorrect type of image_num_patches. "
1016
+ f"Got type: {type(image_num_patches)}")
1017
+
1018
+ pixel_values_flat = flatten_bn(pixel_values_flat, concat=True)
1019
+ image_num_patches = flatten_bn(image_num_patches, concat=True)
1020
+
1021
+ return NanoNemotronVLImagePixelInputs(
1022
+ type="pixel_values",
1023
+ pixel_values_flat=pixel_values_flat,
1024
+ num_patches=image_num_patches,
1025
+ )
1026
+
1027
+ raise AssertionError("This line should be unreachable.")
1028
+
1029
+ def _process_image_input(
1030
+ self, image_input: NanoNemotronVLImageInputs) -> torch.Tensor:
1031
+ if image_input["type"] == "image_embeds":
1032
+ return image_input["data"]
1033
+
1034
+ assert self.vision_model is not None
1035
+
1036
+ image_embeds = self.extract_feature(image_input["pixel_values_flat"])
1037
+ num_patches = image_input["num_patches"]
1038
+
1039
+ # Only one image in the current batch
1040
+ if len(num_patches) == 1:
1041
+ return (image_embeds.view(-1,
1042
+ self.config.text_config.hidden_size), )
1043
+
1044
+ # NOTE: Image embeddings are split into separate tensors for each image
1045
+ # by the size of each embedding.
1046
+ feature_size = image_embeds.shape[1]
1047
+ image_embeds = image_embeds.view(-1,
1048
+ self.config.text_config.hidden_size)
1049
+ image_feature_sizes = [
1050
+ num_patches * feature_size for num_patches in num_patches
1051
+ ]
1052
+ return image_embeds.split(image_feature_sizes)
1053
+
1054
+ def _parse_and_validate_video_input(
1055
+ self,
1056
+ **kwargs: object) -> Optional[NanoNemotronVLVideoPixelInputs]:
1057
+ pixel_values_flat_video = kwargs.pop("pixel_values_flat_video", None)
1058
+ video_num_patches = kwargs.pop("video_num_patches", None)
1059
+ video_embeds = kwargs.pop("video_embeds", None)
1060
+
1061
+ if pixel_values_flat_video is None and video_embeds is None:
1062
+ return None
1063
+
1064
+ if video_embeds is not None:
1065
+ return NanoNemotronVLVideoEmbeddingInputs(
1066
+ type="video_embeds",
1067
+ data=flatten_bn(video_embeds),
1068
+ )
1069
+
1070
+ video_token_id = kwargs["video_token_id"]
1071
+ assert isinstance(video_token_id, torch.Tensor)
1072
+ self.video_context_token_id = video_token_id.flatten().unique().item()
1073
+
1074
+ if pixel_values_flat_video is not None:
1075
+ if not isinstance(pixel_values_flat_video, (torch.Tensor, list)):
1076
+ raise ValueError("Incorrect type of pixel values. "
1077
+ f"Got type: {type(pixel_values_flat_video)}")
1078
+
1079
+ if not isinstance(video_num_patches, (torch.Tensor, list)):
1080
+ raise ValueError("Incorrect type of image_num_patches. "
1081
+ f"Got type: {type(video_num_patches)}")
1082
+
1083
+ pixel_values_flat_video = flatten_bn(pixel_values_flat_video,
1084
+ concat=True)
1085
+ video_num_patches = flatten_bn(video_num_patches, concat=True)
1086
+ expected_h = expected_w = self.config.force_image_size
1087
+ resolve_bindings = {"h": expected_h, "w": expected_w}
1088
+
1089
+ return NanoNemotronVLVideoPixelInputs(
1090
+ type="pixel_values_videos",
1091
+ pixel_values_flat=pixel_values_flat_video,
1092
+ num_patches=video_num_patches,
1093
+ resolve_bindings=resolve_bindings,
1094
+ )
1095
+
1096
+ raise AssertionError("This line should be unreachable.")
1097
+
1098
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1099
+ modalities = {}
1100
+ # Preserve the order of modalities if there are multiple of them
1101
+ # from the order of kwargs.
1102
+ for input_key in kwargs:
1103
+ if input_key in ("pixel_values_flat",
1104
+ "image_embeds") and "images" not in modalities:
1105
+ modalities["images"] = self._parse_and_validate_image_input(
1106
+ **kwargs)
1107
+ if input_key in ("pixel_values_flat_video",
1108
+ ) and "videos" not in modalities:
1109
+ modalities["videos"] = self._parse_and_validate_video_input(
1110
+ **kwargs)
1111
+
1112
+ return modalities
1113
+
1114
+ def get_multimodal_embeddings(
1115
+ self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
1116
+ # Validate the multimodal input keyword arguments
1117
+ modalities = self._parse_and_validate_multimodal_inputs(**kwargs)
1118
+ if modalities is None:
1119
+ return []
1120
+
1121
+ # # The result multimodal_embeddings is tuple of tensors, with each
1122
+ # tensor correspoending to a multimodal data item (image or video).
1123
+ multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1124
+
1125
+ # NOTE: It is important to iterate over the keys in this dictionary
1126
+ # to preserve the order of the modalities.
1127
+ for modality in modalities:
1128
+ if modality == "images":
1129
+ image_input = modalities["images"]
1130
+ vision_embeddings = self._process_image_input(image_input)
1131
+ multimodal_embeddings += vision_embeddings
1132
+ if modality == "videos":
1133
+ video_input = modalities["videos"]
1134
+ video_embeddings = self._process_image_input(video_input)
1135
+ multimodal_embeddings += video_embeddings
1136
+
1137
+ return multimodal_embeddings
1138
+
1139
+ def get_input_embeddings(
1140
+ self,
1141
+ input_ids: torch.Tensor,
1142
+ multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
1143
+ ) -> torch.Tensor:
1144
+ inputs_embeds = self.language_model.get_input_embeddings(input_ids)
1145
+
1146
+ if (multimodal_embeddings is not None
1147
+ and len(multimodal_embeddings) != 0):
1148
+ context_token_ids = [
1149
+ token_id for token_id in (self.img_context_token_id,
1150
+ self.video_context_token_id)
1151
+ if token_id is not None
1152
+ ]
1153
+ assert len(context_token_ids) >= 1
1154
+ inputs_embeds = merge_multimodal_embeddings(
1155
+ input_ids,
1156
+ inputs_embeds,
1157
+ multimodal_embeddings,
1158
+ context_token_ids,
1159
+ )
1160
+
1161
+ return inputs_embeds
1162
+
1163
+ def get_language_model(self) -> torch.nn.Module:
1164
+ return self.language_model
1165
+
1166
+ def forward(
1167
+ self,
1168
+ input_ids: torch.Tensor,
1169
+ positions: torch.Tensor,
1170
+ intermediate_tensors: Optional[IntermediateTensors] = None,
1171
+ inputs_embeds: Optional[torch.Tensor] = None,
1172
+ **kwargs: object,
1173
+ ) -> Union[torch.Tensor, IntermediateTensors]:
1174
+ if intermediate_tensors is not None:
1175
+ input_ids = None
1176
+ inputs_embeds = None
1177
+
1178
+ # NOTE: In v1, inputs_embeds is always generated at model runner, this
1179
+ # condition is for v0 compatibility.
1180
+ elif inputs_embeds is None:
1181
+ vision_embeddings = self.get_multimodal_embeddings(**kwargs)
1182
+ inputs_embeds = self.get_input_embeddings(input_ids,
1183
+ vision_embeddings)
1184
+ input_ids = None
1185
+
1186
+ hidden_states = self.language_model(
1187
+ input_ids=input_ids,
1188
+ positions=positions,
1189
+ intermediate_tensors=intermediate_tensors,
1190
+ inputs_embeds=inputs_embeds,
1191
+ **kwargs,
1192
+ )
1193
+
1194
+ return hidden_states
1195
+
1196
+ def get_mm_mapping(self) -> MultiModelKeys:
1197
+ """
1198
+ Get the module prefix in multimodal models
1199
+ """
1200
+ return MultiModelKeys.from_string_field(
1201
+ language_model="language_model",
1202
+ connector="mlp1",
1203
+ tower_model="vision_model",
1204
+ )
1205
+
1206
+ def compute_logits(
1207
+ self,
1208
+ hidden_states: torch.Tensor,
1209
+ sampling_metadata: SamplingMetadata,
1210
+ ) -> Optional[torch.Tensor]:
1211
+ return self.language_model.compute_logits(hidden_states,
1212
+ sampling_metadata)
1213
+
1214
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
1215
+
1216
+ def is_vision_model_weights(weight: tuple[str, torch.Tensor]):
1217
+ return weight[0].startswith("vision_model")
1218
+
1219
+ def is_adapter_weights(weight: tuple[str, torch.Tensor]):
1220
+ return weight[0].startswith("mlp1")
1221
+
1222
+ # Get references to parameters for direct loading
1223
+ vision_model_dict = dict(self.vision_model.named_parameters())
1224
+ vision_model_buffers = dict(self.vision_model.named_buffers())
1225
+ adapter_dict = dict(self.mlp1.named_parameters())
1226
+
1227
+ def llm_weights_generator():
1228
+ # Single pass over weights
1229
+ for name, w in weights:
1230
+ if is_vision_model_weights((name, w)):
1231
+ # Load vision encoder weights directly
1232
+ trimmed_name = ".".join(name.split(".")[1:])
1233
+ if "input_conditioner" in trimmed_name:
1234
+ continue
1235
+ if trimmed_name in vision_model_buffers:
1236
+ param = vision_model_buffers[trimmed_name]
1237
+ else:
1238
+ param = vision_model_dict[trimmed_name]
1239
+ with torch.no_grad():
1240
+ default_weight_loader(param, w)
1241
+ elif is_adapter_weights((name, w)):
1242
+ # Load vision-language adapter weights directly
1243
+ trimmed_name = ".".join(name.split(".")[1:])
1244
+ param = adapter_dict[trimmed_name]
1245
+ with torch.no_grad():
1246
+ default_weight_loader(param, w)
1247
+ else:
1248
+ # LLM weights: yield them to be loaded
1249
+ # by language_model.load_weights
1250
+ assert name.startswith("language_model")
1251
+ trimmed_name = ".".join(name.split(".")[1:])
1252
+ yield (trimmed_name, w)
1253
+
1254
+ # Now we call the language model load with the generator
1255
+ self.language_model.load_weights(llm_weights_generator())
1256
+
1257
+ def print_architecture(self,
1258
+ detailed: bool = True,
1259
+ save_to_file: str = None):
1260
+ """
1261
+ Print model architecture with parameter names, shapes, and sizes.
1262
+
1263
+ Args:
1264
+ detailed: If True, show detailed parameter breakdown
1265
+ save_to_file: If provided, save output to this file path
1266
+ """
1267
+ import sys
1268
+ from io import StringIO
1269
+
1270
+ # Capture output if saving to file
1271
+ original_stdout = sys.stdout
1272
+ if save_to_file:
1273
+ sys.stdout = StringIO()
1274
+
1275
+ try:
1276
+ print("=" * 100)
1277
+ print("NemotronH_Nano_VL Model Architecture")
1278
+ print("=" * 100)
1279
+
1280
+ total_params = 0
1281
+ param_groups = {
1282
+ "language_model": [],
1283
+ "vision_model": [],
1284
+ "mlp1": [],
1285
+ "other": [],
1286
+ }
1287
+
1288
+ for name, param in self.named_parameters():
1289
+ param_size = param.numel()
1290
+ total_params += param_size
1291
+
1292
+ # Group parameters by main component
1293
+ if name.startswith("language_model"):
1294
+ param_groups["language_model"].append(
1295
+ (name, param.shape, param_size, param.dtype))
1296
+ elif name.startswith("vision_model"):
1297
+ param_groups["vision_model"].append(
1298
+ (name, param.shape, param_size, param.dtype))
1299
+ elif name.startswith("mlp1"):
1300
+ param_groups["mlp1"].append(
1301
+ (name, param.shape, param_size, param.dtype))
1302
+ else:
1303
+ param_groups["other"].append(
1304
+ (name, param.shape, param_size, param.dtype))
1305
+
1306
+ if detailed:
1307
+ print(f"{name:<70} | Shape: {str(param.shape):<25} | "
1308
+ f"Size: {param_size:>12,} | Dtype: {param.dtype}")
1309
+
1310
+ print("=" * 100)
1311
+ print("Summary by Component:")
1312
+ print("-" * 60)
1313
+
1314
+ for component, params in param_groups.items():
1315
+ if params: # Only show components that have parameters
1316
+ component_total = sum(size for _, _, size, _ in params)
1317
+ percentage = ((component_total / total_params) *
1318
+ 100 if total_params > 0 else 0)
1319
+ print(f"{component:<20} | Parameters: {len(params):>4} | "
1320
+ f"Total Size: {component_total:>15,} | "
1321
+ f"{percentage:>6.2f}%")
1322
+
1323
+ print("-" * 60)
1324
+ print(f"{'Total Parameters':<20} | {total_params:>15,}")
1325
+
1326
+ # Estimate memory usage (assuming bfloat16 = 2 bytes per parameter)
1327
+ memory_mb = total_params * 2 / (1024**2)
1328
+ memory_gb = memory_mb / 1024
1329
+ print(f"{'Est. Memory (MB)':<20} | {memory_mb:>15.2f}")
1330
+ print(f"{'Est. Memory (GB)':<20} | {memory_gb:>15.2f}")
1331
+ print("=" * 100)
1332
+
1333
+ # Save to file if requested
1334
+ if save_to_file:
1335
+ output = sys.stdout.getvalue()
1336
+ sys.stdout = original_stdout
1337
+ with open(save_to_file, "w") as f:
1338
+ f.write(output)
1339
+ print(f"Architecture saved to: {save_to_file}")
1340
+ print(output) # Also print to console
1341
+
1342
+ finally:
1343
+ if save_to_file and sys.stdout != original_stdout:
1344
+ sys.stdout = original_stdout
1345
+
1346
+ def get_model_info(self):
1347
+ """
1348
+ Get basic model information as a dictionary.
1349
+ """
1350
+ total_params = sum(p.numel() for p in self.parameters())
1351
+
1352
+ component_info = {}
1353
+ for name, param in self.named_parameters():
1354
+ component = name.split(".")[0]
1355
+ if component not in component_info:
1356
+ component_info[component] = {"params": 0, "size": 0}
1357
+ component_info[component]["params"] += 1
1358
+ component_info[component]["size"] += param.numel()
1359
+
1360
+ return {
1361
+ "model_name": "NemotronH_Nano_VL",
1362
+ "total_parameters": total_params,
1363
+ "memory_estimate_mb": total_params * 2 / (1024**2), # bfloat16
1364
+ "components": component_info,
1365
+ "config": {
1366
+ "image_size": getattr(self.config, "force_image_size", None),
1367
+ "patch_size": getattr(self.config, "patch_size", None),
1368
+ "num_image_token": self.num_image_token,
1369
+ "downsample_ratio": self.downsample_ratio,
1370
+ },
1371
+ }
1372
+
1373
+ def copy_inputs_before_cuda_graphs(self, input_buffers, **kwargs):
1374
+ return self.language_model.mamba_cache.copy_inputs_before_cuda_graphs(
1375
+ input_buffers, **kwargs)
1376
+
1377
+ def get_seqlen_agnostic_capture_inputs(self, batch_size: int):
1378
+ return (self.language_model.mamba_cache.
1379
+ get_seqlen_agnostic_capture_inputs(batch_size))
1380
+
1381
+ @classmethod
1382
+ def get_mamba_state_shape_from_config(cls, vllm_config: "VllmConfig"):
1383
+ text_config = vllm_config.model_config.hf_config.text_config
1384
+ temp_vllm_config = copy.deepcopy(vllm_config)
1385
+ temp_vllm_config.model_config.hf_config = text_config
1386
+ return NemotronHForCausalLM.get_mamba_state_shape_from_config(
1387
+ temp_vllm_config)
1388
+
1389
+ @classmethod
1390
+ def get_mamba_state_dtype_from_config(cls, vllm_config: "VllmConfig"):
1391
+ text_config = vllm_config.model_config.hf_config.text_config
1392
+ temp_vllm_config = copy.deepcopy(vllm_config)
1393
+ temp_vllm_config.model_config.hf_config = text_config
1394
+ return NemotronHForCausalLM.get_mamba_state_dtype_from_config(
1395
+ temp_vllm_config)
vllm/model_executor/models/registry.py
+1
@@ -223,6 +223,7 @@ _MULTIMODAL_MODELS = {
223
"GraniteSpeechForConditionalGeneration": ("granite_speech", "GraniteSpeechForConditionalGeneration"), # noqa: E501
224
"H2OVLChatModel": ("h2ovl", "H2OVLChatModel"),
225
"InternVLChatModel": ("internvl", "InternVLChatModel"),
226
+ "NemotronH_Nano_VL": ("nano_nemotron_vl", "NemotronH_Nano_VL"),
227
"InternS1ForConditionalGeneration": ("interns1", "InternS1ForConditionalGeneration"), # noqa: E501
228
"InternVLForConditionalGeneration": ("interns1", "InternS1ForConditionalGeneration"), # noqa: E501
229
"Idefics3ForConditionalGeneration":("idefics3","Idefics3ForConditionalGeneration"),