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 2, 12:15 UTC
[feat] add GlmgaProcessor specific logits in `glm4_1v.py` (#43575)
Signed-off-by: JaredforReal <w13431838023@gmail.com> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Jared Wen committed
May 29, 2026 at 10:56 UTC
212deff2ec77a6154c69a5fcc5b51a702b3d6c91
3 files changed
+346
-33
tests/models/registry.py
+4
-1
@@ -935,7 +935,10 @@ _MULTIMODAL_EXAMPLE_MODELS = {
935
trust_remote_code=True,
936
hf_overrides={"architectures": ["GLM4VForCausalLM"]},
937
),
938
- "Glm4vForConditionalGeneration": _HfExamplesInfo("zai-org/GLM-4.1V-9B-Thinking"),
938
+ "Glm4vForConditionalGeneration": _HfExamplesInfo(
939
+ "zai-org/GLM-4.1V-9B-Thinking",
940
+ extras={"4.6V": "zai-org/GLM-4.6V-Flash"},
941
+ ),
942
"Glm4vMoeForConditionalGeneration": _HfExamplesInfo("zai-org/GLM-4.5V"),
943
"GlmOcrForConditionalGeneration": _HfExamplesInfo(
944
"zai-org/GLM-OCR",
vllm/model_executor/models/glm4_1v.py
+241
-32
@@ -36,7 +36,9 @@ import numpy as np
36
import torch
37
import torch.nn as nn
38
import torch.nn.functional as F
39
+import transformers
40
from einops import rearrange
41
+from packaging.version import Version
42
from transformers import BatchFeature, Glm4vProcessor
43
from transformers.models.glm4v.configuration_glm4v import (
44
Glm4vTextConfig,
@@ -122,6 +124,15 @@ logger = init_logger(__name__)
124
# For profile run
125
_MAX_FRAMES_PER_VIDEO = 600
126
127
+TRANSFORMERS_WITH_GA = Version(transformers.__version__) >= Version("5.10.0.dev0")
128
+
129
+
130
+def _to_video_metadata(metadata: Mapping[str, Any]) -> VideoMetadata:
131
+ return VideoMetadata(
132
+ **{k: metadata[k] for k in metadata if k != "do_sample_frames"}
133
+ )
134
+
135
+
136
# === Vision Inputs === #
137
138
@@ -832,6 +843,46 @@ class Glm4vProcessingInfo(BaseProcessingInfo):
843
def get_video_processor(self, **kwargs: object) -> Glm4vVideoProcessor:
844
return self.get_hf_processor(**kwargs).video_processor
845
846
+ def get_mm_max_tokens_per_item(
847
+ self,
848
+ seq_len: int,
849
+ mm_counts: Mapping[str, int],
850
+ ) -> Mapping[str, int] | None:
851
+ processor = self.get_hf_processor()
852
+ if isinstance(processor, Glm4vProcessor):
853
+ return None
854
+
855
+ result: dict[str, int] = {}
856
+
857
+ if mm_counts.get("image", 0) > 0:
858
+ result["image"] = self.get_max_image_tokens()
859
+
860
+ if mm_counts.get("video", 0) > 0:
861
+ video_processor = self.get_video_processor()
862
+ max_pixels = video_processor.size["longest_edge"]
863
+
864
+ vision_config = self.get_hf_config().vision_config
865
+ temporal_patch_size = vision_config.temporal_patch_size
866
+ patch_size = vision_config.patch_size
867
+ merge_size = vision_config.spatial_merge_size
868
+
869
+ max_vision_tokens = max_pixels // (
870
+ temporal_patch_size * patch_size**2 * merge_size**2
871
+ )
872
+
873
+ # GLMGA supports up to 640 frames (max_frames).
874
+ max_grid_t = 640 // temporal_patch_size
875
+
876
+ tokenizer = self.get_tokenizer()
877
+ max_ts_tokens = max(
878
+ len(tokenizer.encode(f"{t:.1f} seconds", add_special_tokens=False))
879
+ for t in range(min(max_grid_t, 300))
880
+ )
881
+
882
+ result["video"] = max_vision_tokens + max_grid_t * (2 + max_ts_tokens) + 2
883
+
884
+ return result
885
+
886
def get_data_parser(self):
887
return MultiModalDataParser(
888
video_needs_metadata=True,
@@ -1104,12 +1155,83 @@ class Glm4vProcessingInfo(BaseProcessingInfo):
1155
selected_timestamps.append(timestamps_list[idx])
1156
return selected_timestamps
1157
1158
+ def _is_glmga_model(self, processor: object) -> bool:
1159
+ """Detect GLMGA variant via its Glmga sub-processors."""
1160
+ for attr in ("image_processor", "video_processor"):
1161
+ sub = getattr(processor, attr, None)
1162
+ if sub and "Glmga" in type(sub).__name__:
1163
+ return True
1164
+ return False
1165
+
1166
+ def _get_video_second_idx_glmga(
1167
+ self, metadata: dict[str, Any], total_frames: int
1168
+ ) -> list[int]:
1169
+ """Fixed fps=2 frame selection matching GlmgaVideoProcessor.sample_frames."""
1170
+ video_processor = self.get_video_processor()
1171
+
1172
+ video_fps = metadata["fps"]
1173
+ meta_frames = metadata.get("total_num_frames", total_frames)
1174
+ max_frame_idx = meta_frames - 1
1175
+ duration = metadata.get("duration", round(max_frame_idx / video_fps) + 1)
1176
+
1177
+ do_sample_frames = metadata.get("do_sample_frames", True)
1178
+ if not do_sample_frames:
1179
+ frame_indices = metadata["frames_indices"]
1180
+ else:
1181
+ target_fps = 2
1182
+ max_frames = getattr(video_processor, "max_frames", 640)
1183
+ extract_t = int(duration * target_fps)
1184
+ extract_t = min(extract_t, max_frames)
1185
+
1186
+ duration_per_frame = 1 / video_fps
1187
+ timestamps = [i * duration_per_frame for i in range(meta_frames)]
1188
+
1189
+ if meta_frames < extract_t:
1190
+ frame_indices = [
1191
+ math.floor(i * meta_frames / extract_t) for i in range(extract_t)
1192
+ ]
1193
+ else:
1194
+ frame_indices = []
1195
+ current_second = 0.0
1196
+ inv_fps = 1 / target_fps
1197
+ for frame_index in range(meta_frames):
1198
+ if timestamps[frame_index] >= current_second:
1199
+ current_second += inv_fps
1200
+ frame_indices.append(frame_index)
1201
+ if current_second >= duration - inv_fps:
1202
+ break
1203
+
1204
+ if len(frame_indices) < extract_t:
1205
+ if len(frame_indices) == 0:
1206
+ start, end = 0, max(meta_frames - 1, 0)
1207
+ else:
1208
+ start, end = frame_indices[0], frame_indices[-1]
1209
+ frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
1210
+ elif len(frame_indices) > extract_t:
1211
+ frame_indices = np.linspace(
1212
+ 0, meta_frames - 1, extract_t, dtype=int
1213
+ ).tolist()
1214
+
1215
+ seen, uniq = set(), []
1216
+ for idx in frame_indices:
1217
+ if idx not in seen:
1218
+ seen.add(idx)
1219
+ uniq.append(idx)
1220
+
1221
+ if len(uniq) & 1:
1222
+ uniq.append(uniq[-1])
1223
+
1224
+ frame_indices = uniq
1225
+ full_second_idxs = [int(idx / video_fps) for idx in frame_indices]
1226
+ timestamps_list = full_second_idxs[::2]
1227
+ return list(timestamps_list)
1228
+
1229
def _construct_video_placeholder(
1230
self,
1231
video_array: np.ndarray,
1232
metadata: dict[str, Any],
1233
grid_thw: torch.Tensor,
1112
- ) -> str:
1234
+ ) -> list[int]:
1235
hf_processor = self.get_hf_processor()
1236
tokenizer = self.get_tokenizer()
1237
image_processor = hf_processor.image_processor
@@ -1122,11 +1244,13 @@ class Glm4vProcessingInfo(BaseProcessingInfo):
1244
merge_length = image_processor.merge_size**2
1245
1246
assert isinstance(grid_thw, torch.Tensor)
1125
- timestamps = (
1126
- self._get_video_second_idx_glm4v(metadata, len(video_array))
1127
- if isinstance(hf_processor, Glm4vProcessor)
1128
- else self._get_video_second_idx_glm46v(metadata, len(video_array))
1129
- )
1247
+
1248
+ if isinstance(hf_processor, Glm4vProcessor):
1249
+ timestamps = self._get_video_second_idx_glm4v(metadata, len(video_array))
1250
+ elif self._is_glmga_model(hf_processor):
1251
+ timestamps = self._get_video_second_idx_glmga(metadata, len(video_array))
1252
+ else:
1253
+ timestamps = self._get_video_second_idx_glm46v(metadata, len(video_array))
1254
1255
timestamp_format = (
1256
"{}" if isinstance(hf_processor, Glm4vProcessor) else "{:.1f} seconds"
@@ -1139,9 +1263,16 @@ class Glm4vProcessingInfo(BaseProcessingInfo):
1263
num_tokens_per_frame = int(H * W) // merge_length
1264
placeholder = []
1265
placeholder.append(bov_token_id)
1266
+ # Glm46VProcessor uses image_token_id for video frame embeddings;
1267
+ # Glm4vProcessor uses video_token_id.
1268
+ frame_embed_token_id = (
1269
+ hf_processor.video_token_id
1270
+ if isinstance(hf_processor, Glm4vProcessor) or not TRANSFORMERS_WITH_GA
1271
+ else hf_processor.image_token_id
1272
+ )
1273
for frame_idx in frames_idx_token:
1274
placeholder.append(boi_token_id)
1144
- placeholder.extend([hf_processor.video_token_id] * num_tokens_per_frame)
1275
+ placeholder.extend([frame_embed_token_id] * num_tokens_per_frame)
1276
placeholder.append(eoi_token_id)
1277
placeholder.extend(frame_idx)
1278
placeholder.append(eov_token_id)
@@ -1241,6 +1372,47 @@ class Glm4vDummyInputsBuilder(BaseDummyInputsBuilder[Glm4vProcessingInfo]):
1372
1373
1374
class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1375
+ @staticmethod
1376
+ def _get_direct_path_inputs(
1377
+ mm_data: Mapping[str, object],
1378
+ mm_kwargs: Mapping[str, object],
1379
+ ) -> tuple[Mapping[str, object], Mapping[str, object]]:
1380
+ prepared_data = dict(mm_data)
1381
+ prepared_kwargs = dict(mm_kwargs)
1382
+
1383
+ videos = prepared_data.get("videos")
1384
+ if not (isinstance(videos, list) and len(videos) > 0):
1385
+ return prepared_data, prepared_kwargs
1386
+
1387
+ hf_videos = []
1388
+ hf_video_metadata = []
1389
+ for item in videos:
1390
+ if isinstance(item, tuple) and len(item) == 2:
1391
+ video_array, metadata = item
1392
+ hf_videos.append(video_array)
1393
+ if isinstance(metadata, VideoMetadata):
1394
+ hf_video_metadata.append(metadata)
1395
+ elif isinstance(metadata, Mapping):
1396
+ hf_video_metadata.append(_to_video_metadata(metadata))
1397
+ if "do_sample_frames" in metadata:
1398
+ prepared_kwargs["do_sample_frames"] = metadata[
1399
+ "do_sample_frames"
1400
+ ]
1401
+ elif metadata is not None:
1402
+ raise TypeError(
1403
+ "Video metadata must be a mapping or VideoMetadata, "
1404
+ f"got {type(metadata)}"
1405
+ )
1406
+ else:
1407
+ hf_videos.append(item)
1408
+
1409
+ prepared_data["videos"] = hf_videos
1410
+ if hf_video_metadata:
1411
+ prepared_data["video_metadata"] = hf_video_metadata
1412
+ prepared_kwargs["return_metadata"] = True
1413
+
1414
+ return prepared_data, prepared_kwargs
1415
+
1416
def _call_hf_processor(
1417
self,
1418
prompt: str,
@@ -1249,11 +1421,32 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1421
tok_kwargs: Mapping[str, object],
1422
) -> BatchFeature:
1423
mm_data = dict(mm_data)
1424
+ if not mm_data:
1425
+ tokenizer = self.info.get_tokenizer()
1426
+ prompt_ids = tokenizer.encode(prompt, add_special_tokens=False)
1427
+ return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
1428
+
1429
processor = self.info.get_hf_processor(**mm_kwargs)
1430
1254
- # GLM-4.1V use `image_token_id` as video placeholder, we need to
1255
- # replace it with `video_token_id` for video processing. So we
1256
- # separate video processing from image processing.
1431
+ # Glm46VProcessor and GLMGA handle image/video placeholders together
1432
+ # via the direct path. Only Glm4vProcessor (GLM-4.1V) needs the
1433
+ # split-video path because it uses image_token_id as the video
1434
+ # placeholder. The direct path requires transformers >= 5.5.0
1435
+ # (Glm46VProcessor / GlmgaVideoProcessor support).
1436
+ use_direct_path = (
1437
+ not isinstance(processor, Glm4vProcessor) and TRANSFORMERS_WITH_GA
1438
+ )
1439
+ if use_direct_path:
1440
+ prepared_data, prepared_kwargs = self._get_direct_path_inputs(
1441
+ mm_data, mm_kwargs
1442
+ )
1443
+ return super()._call_hf_processor(
1444
+ prompt=prompt,
1445
+ mm_data=prepared_data,
1446
+ mm_kwargs=prepared_kwargs,
1447
+ tok_kwargs=tok_kwargs,
1448
+ )
1449
+
1450
if (
1451
"videos" in mm_data
1452
and isinstance(mm_data["videos"], list)
@@ -1272,19 +1465,7 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1465
1466
video_mm_data = dict()
1467
video_mm_data["videos"] = [[video_array]]
1275
-
1276
- unuse_metadata = ["do_sample_frames"]
1277
- video_mm_data["video_metadata"] = [
1278
- [
1279
- VideoMetadata(
1280
- **{
1281
- k: metadata[k]
1282
- for k in metadata
1283
- if k not in unuse_metadata
1284
- }
1285
- )
1286
- ]
1287
- ]
1468
+ video_mm_data["video_metadata"] = [[_to_video_metadata(metadata)]]
1469
1470
video_outputs = super()._call_hf_processor(
1471
prompt="<|begin_of_video|><|video|><|end_of_video|>",
@@ -1366,6 +1547,22 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1547
embed_token_id=hf_processor.video_token_id,
1548
)
1549
1550
+ def get_video_replacement_glm46v(item_idx: int):
1551
+ out_item = out_mm_kwargs["video"][item_idx]
1552
+ grid_thw = out_item["video_grid_thw"].data
1553
+ assert isinstance(grid_thw, torch.Tensor)
1554
+
1555
+ video, metadata = mm_items["video"][item_idx]
1556
+ placeholder = self.info._construct_video_placeholder(
1557
+ video, metadata, grid_thw
1558
+ )
1559
+ return PromptUpdateDetails.select_token_id(
1560
+ placeholder,
1561
+ embed_token_id=hf_processor.image_token_id,
1562
+ )
1563
+
1564
+ is_glm46v = not isinstance(hf_processor, Glm4vProcessor)
1565
+
1566
return [
1567
PromptReplacement(
1568
modality="image",
@@ -1375,7 +1572,11 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1572
PromptReplacement(
1573
modality="video",
1574
target="<|begin_of_video|><|video|><|end_of_video|>",
1378
- replacement=get_video_replacement_glm4v,
1575
+ replacement=(
1576
+ get_video_replacement_glm46v
1577
+ if is_glm46v and TRANSFORMERS_WITH_GA
1578
+ else get_video_replacement_glm4v
1579
+ ),
1580
),
1581
]
1582
@@ -1436,7 +1637,7 @@ class Glm4vForConditionalGeneration(
1637
prefix=maybe_prefix(prefix, "visual"),
1638
)
1639
1439
- if config.model_type in ("glm4v", "glm_ocr"):
1640
+ if config.model_type in ("glm4v", "glm_ocr", "glmga"):
1641
architectures = ["Glm4ForCausalLM"]
1642
elif config.model_type == "glm4v_moe":
1643
architectures = ["Glm4MoeForCausalLM"]
@@ -1600,19 +1801,27 @@ class Glm4vForConditionalGeneration(
1801
hf_config = self.config
1802
spatial_merge_size = hf_config.vision_config.spatial_merge_size
1803
for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
1603
- offset = mm_feature.mm_position.offset
1804
+ embed_ranges = mm_feature.mm_position.extract_embeds_range()
1805
if mm_feature.modality == "image":
1806
t, h, w = mm_feature.data["image_grid_thw"].data.tolist()
1807
assert t == 1, f"Image must have 1 frame, got {t}"
1808
+ assert len(embed_ranges) == 1
1809
+ offset, end = embed_ranges[0]
1810
+ assert end - offset + 1 == h * w // spatial_merge_size**2
1811
yield offset, t, h // spatial_merge_size, w // spatial_merge_size
1812
elif mm_feature.modality == "video":
1813
t, h, w = mm_feature.data["video_grid_thw"].data.tolist()
1610
- yield (
1611
- offset,
1612
- t,
1613
- h // spatial_merge_size,
1614
- w // spatial_merge_size,
1615
- )
1814
+ llm_grid_h = h // spatial_merge_size
1815
+ llm_grid_w = w // spatial_merge_size
1816
+ num_tokens_per_frame = llm_grid_h * llm_grid_w
1817
+
1818
+ if len(embed_ranges) == t:
1819
+ for offset, end in embed_ranges:
1820
+ assert end - offset + 1 == num_tokens_per_frame
1821
+ yield offset, 1, llm_grid_h, llm_grid_w
1822
+ else:
1823
+ offset = mm_feature.mm_position.offset
1824
+ yield offset, t, llm_grid_h, llm_grid_w
1825
else:
1826
raise ValueError(f"Unsupported modality: {mm_feature.modality}")
1827
vllm/multimodal/video.py
+101
@@ -639,6 +639,107 @@ class DynamicVideoBackend(VideoBackend):
639
)
640
641
642
+@VIDEO_LOADER_REGISTRY.register("glm4_6v")
643
+class GLM4_6VVideoBackend(VideoBackend):
644
+ @classmethod
645
+ def _prepare_source(cls, source: VideoSourceMetadata) -> VideoSourceMetadata:
646
+ # Estimate duration from frame count and fps when the container
647
+ # does not report it (common for WebM/streaming inputs).
648
+ if source.duration:
649
+ return source
650
+ if source.original_fps > 0:
651
+ max_frame_idx = source.total_frames_num - 1
652
+ duration = round(max_frame_idx / source.original_fps) + 1
653
+ else:
654
+ duration = 0
655
+ return VideoSourceMetadata(
656
+ source.total_frames_num, source.original_fps, duration
657
+ )
658
+
659
+ @classmethod
660
+ def compute_frames_index_to_sample(
661
+ cls,
662
+ source: VideoSourceMetadata,
663
+ target: VideoTargetMetadata,
664
+ **kwargs,
665
+ ) -> list[int]:
666
+ total_frames_num = source.total_frames_num
667
+ duration = source.duration
668
+ original_fps = source.original_fps
669
+ target_fps = target.fps
670
+ max_frame_idx = source.total_frames_num - 1
671
+ max_frames = kwargs.get("max_frames", 640)
672
+
673
+ duration = duration or round(max_frame_idx / original_fps) + 1
674
+
675
+ extract_t = int(duration * target_fps)
676
+ extract_t = min(extract_t, max_frames)
677
+
678
+ duration_per_frame = 1 / original_fps
679
+ timestamps = [i * duration_per_frame for i in range(total_frames_num)]
680
+
681
+ if total_frames_num < extract_t:
682
+ frame_indices = [
683
+ math.floor(i * total_frames_num / extract_t) for i in range(extract_t)
684
+ ]
685
+ else:
686
+ frame_indices = []
687
+ current_second = 0.0
688
+ inv_fps = 1 / target_fps
689
+ for frame_index in range(total_frames_num):
690
+ if timestamps[frame_index] >= current_second:
691
+ current_second += inv_fps
692
+ frame_indices.append(frame_index)
693
+ if current_second >= duration - inv_fps:
694
+ break
695
+
696
+ if len(frame_indices) < extract_t:
697
+ if len(frame_indices) == 0:
698
+ start, end = 0, max(total_frames_num - 1, 0)
699
+ else:
700
+ start, end = frame_indices[0], frame_indices[-1]
701
+ frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
702
+ elif len(frame_indices) > extract_t:
703
+ frame_indices = np.linspace(
704
+ 0, total_frames_num - 1, extract_t, dtype=int
705
+ ).tolist()
706
+
707
+ seen, uniq = set(), []
708
+ for idx in frame_indices:
709
+ if idx not in seen:
710
+ seen.add(idx)
711
+ uniq.append(idx)
712
+
713
+ return uniq
714
+
715
+ @classmethod
716
+ def load_bytes(
717
+ cls,
718
+ data: bytes,
719
+ num_frames: int = -1,
720
+ fps: int = 2,
721
+ max_duration: int = 300,
722
+ frame_recovery: bool = False,
723
+ *,
724
+ backend: Literal["opencv", "pyav"] = "opencv",
725
+ **kwargs,
726
+ ) -> tuple[npt.NDArray, dict[str, Any]]:
727
+ frames, metadata = super().load_bytes(
728
+ data,
729
+ num_frames=num_frames,
730
+ fps=fps,
731
+ max_duration=max_duration,
732
+ frame_recovery=frame_recovery,
733
+ backend=backend,
734
+ **kwargs,
735
+ )
736
+ # Ensure even frame count — matches HF's sample_frames even-padding
737
+ # and _preprocess temporal_patch_size divisibility check.
738
+ if frames.shape[0] & 1:
739
+ frames = np.concatenate([frames, frames[-1:]], axis=0)
740
+ return frames, metadata
741
+
742
+
743
@VIDEO_LOADER_REGISTRY.register("molmo2")
744
class Molmo2VideoBackend(VideoLoader, OpenCVVideoBackendMixin):
745
@classmethod