[Model] Use upstream GLM-5.3 and Qwen4-Exp configs and processor (#57387)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Harry Mellor committed
Oct 2, 2026 at 03:25 UTC
e3cae8d2ac6be8dfd319f3bb8061a1ba4d19bf52
36 files changed
+365
-2384
requirements/common.txt
+1
-1
@@ -7,7 +7,7 @@ requests >= 2.33.0
7
tqdm
8
blake3
9
py-cpuinfo
10
-transformers >= 5.10.4
10
+transformers >= 5.16.1
11
huggingface_hub >= 1.31.0 # Required for the `huggingface_hub.utils.httpx` re-export
12
tokenizers >= 0.21.1 # Required for fast incremental detokenization.
13
safetensors >= 0.6.2 # MXFP4/MXFP6 dtype support (F8_E8M0, F4) added in 0.6.0: https://github.com/huggingface/safetensors/pull/611
tests/models/glm5next/test_sequence_parallel.py
+1
-1
@@ -15,11 +15,11 @@ from types import SimpleNamespace
15
import pytest
16
import torch
17
from torch import nn
18
+from transformers import Glm5NextTextConfig
19
20
from vllm.model_executor import parameter
21
from vllm.model_executor.layers import linear
22
from vllm.models.glm5next.common import model as glm_model
22
-from vllm.transformers_utils.configs.glm5_next import Glm5NextTextConfig
23
24
HIDDEN, INTERMEDIATE, TP_SIZE = 8, 16, 2
25
tests/models/multimodal/processing/test_glm5next.py
+16
-33
@@ -15,16 +15,15 @@ The checks below are arithmetic: no weights, no GPU.
15
import pytest
16
import torch
17
from PIL import Image
18
+from transformers.models.glm5_next.video_processing_glm5_next import (
19
+ Glm5NextVideoProcessor,
20
+ smart_resize,
21
+)
22
+from transformers.video_utils import VideoMetadata
23
24
from vllm.model_executor.layers.fusion.mm_input_norm import build_mm_input_norm
25
from vllm.multimodal import MULTIMODAL_REGISTRY
26
from vllm.platforms import current_platform
22
-from vllm.transformers_utils.processors.glm5next import (
23
- Glm5NextVideoProcessor,
24
- _pixel_budget,
25
- glm_sample_frame_indices,
26
- smart_resize,
27
-)
27
28
from ...utils import build_model_context
29
@@ -48,27 +47,16 @@ def _pixel_path_grid(
47
width: int,
48
) -> tuple[int, int, int]:
49
"""The ``video_grid_thw`` ``Glm5NextVideoProcessor._preprocess`` builds."""
51
- min_pixels, max_pixels = _pixel_budget(
52
- video_processor.min_image_tokens,
53
- video_processor.max_image_tokens,
54
- video_processor.patch_size,
55
- video_processor.merge_size,
56
- video_processor.temporal_patch_size,
57
- )
58
- factor = (
59
- video_processor.patch_size
60
- * video_processor.merge_size
61
- * video_processor.patch_expand_factor
62
- )
50
resized_height, resized_width = smart_resize(
64
- t=num_frames,
65
- h=height,
66
- w=width,
67
- t_factor=video_processor.temporal_patch_size,
68
- h_factor=factor,
69
- w_factor=factor,
70
- min_pixels=min_pixels,
71
- max_pixels=max_pixels,
51
+ num_frames=num_frames,
52
+ height=height,
53
+ width=width,
54
+ temporal_factor=video_processor.temporal_patch_size,
55
+ factor=video_processor.patch_size
56
+ * video_processor.merge_size
57
+ * video_processor.patch_expand_factor,
58
+ min_pixels=video_processor.min_image_tokens,
59
+ max_pixels=video_processor.max_image_tokens,
60
)
61
padded_frames = num_frames + (-num_frames % video_processor.temporal_patch_size)
62
return (
@@ -104,13 +92,8 @@ def test_video_placeholders_match_encoder_rows(
92
info = processor.info
93
video_processor = info.get_video_processor()
94
107
- frame_indices = glm_sample_frame_indices(
108
- total_num_frames,
109
- fps,
110
- duration,
111
- target_fps=video_processor.fps_interval,
112
- max_frame_count=video_processor.max_frame_count_dynamic,
113
- temporal_patch_size=video_processor.temporal_patch_size,
95
+ frame_indices = video_processor.sample_frames(
96
+ VideoMetadata(total_num_frames=total_num_frames, fps=fps, duration=duration)
97
)
98
grid_t, grid_h, grid_w = _pixel_path_grid(
99
video_processor, len(frame_indices), height, width
tests/models/qwen4_exp/test_config.py
+5
-5
@@ -7,16 +7,13 @@ from unittest.mock import patch
7
8
import pytest
9
import torch
10
+from transformers import Qwen4ExpConfig, Qwen4ExpTextConfig
11
12
from vllm.config.speculative import SpeculativeConfig
13
from vllm.model_executor.models.config import (
14
Qwen3_5ForConditionalGenerationConfig,
15
Qwen4ExpForConditionalGenerationConfig,
16
)
16
-from vllm.models.qwen4_exp.config import (
17
- Qwen4ExpConfig,
18
- Qwen4ExpTextConfig,
19
-)
17
from vllm.models.qwen4_exp.nvidia.model_state import Qwen4ExpModelState
18
from vllm.v1.worker.gpu.model_states.mamba_hybrid import MambaHybridModelState
19
@@ -37,7 +34,10 @@ def _text_config(**kwargs) -> Qwen4ExpTextConfig:
34
"linear_num_value_heads": 2,
35
"linear_key_head_dim": 8,
36
"linear_value_head_dim": 8,
40
- "num_experts": 0,
37
+ "num_experts": 4,
38
+ "num_experts_per_tok": 2,
39
+ # `Qwen4ExpTextConfig` requires an EOS token whenever PLE is enabled.
40
+ "eos_token_id": 1,
41
"hc_count": 2,
42
"hc_lowrank": 4,
43
"ple_layer_ids": [1],
tests/models/qwen4_exp/test_ple.py
+1
-1
@@ -10,6 +10,7 @@ import pytest
10
import torch
11
from torch import nn
12
from torch.nn import functional as F
13
+from transformers import Qwen4ExpTextConfig
14
15
import vllm.model_executor.layers.vocab_parallel_embedding as embedding_module
16
import vllm.model_executor.parameter as parameter_module
@@ -33,7 +34,6 @@ from vllm.models.qwen4_exp.common.ple import (
34
compute_ple_shard_overlap,
35
copy_ple_embedding_shard_,
36
)
36
-from vllm.models.qwen4_exp.config import Qwen4ExpTextConfig
37
from vllm.models.qwen4_exp.nvidia.ngram_embedding import (
38
Qwen4ExpNGramEmbedding,
39
Qwen4ExpPLEDeviceEmbedding,
tests/multimodal/test_video.py
+3
-15
@@ -1591,10 +1591,6 @@ def test_glm5next_backend_indices_match_sampler(
1591
):
1592
"""The loader must select exactly the frames the processor's sampler
1593
would, with target.fps mapping onto the raw-fps override."""
1594
- from vllm.transformers_utils.processors.glm5next import (
1595
- glm_sample_frame_indices,
1596
- )
1597
-
1594
source = VideoSourceMetadata(
1595
total_frames_num=total_frames, original_fps=original_fps, duration=duration
1596
)
@@ -1604,7 +1600,7 @@ def test_glm5next_backend_indices_match_sampler(
1600
source, target, max_frames=max_frames
1601
)
1602
1607
- assert indices == glm_sample_frame_indices(
1603
+ assert indices == Glm5NextVideoBackend._sample_frame_indices(
1604
total_frames,
1605
original_fps,
1606
duration,
@@ -1692,10 +1688,6 @@ def test_glm5next_backend_codec_parity(tmp_path, backend):
1688
if backend == "torchcodec":
1689
pytest.importorskip("torchcodec")
1690
1695
- from vllm.transformers_utils.processors.glm5next import (
1696
- glm_sample_frame_indices,
1697
- )
1698
-
1691
total_frames, fps = 120, 10
1692
path = _write_gray_video(tmp_path, total_frames, fps)
1693
# Dense default sampling (gap 5) and a sparse max_frames cap (gap 20).
@@ -1703,7 +1695,7 @@ def test_glm5next_backend_codec_parity(tmp_path, backend):
1695
kwargs: dict[str, Any] = (
1696
{} if max_frames is None else {"max_frames": max_frames}
1697
)
1706
- expected = glm_sample_frame_indices(
1698
+ expected = Glm5NextVideoBackend._sample_frame_indices(
1699
total_frames, float(fps), 12.0, max_frame_count=max_frames
1700
)
1701
@@ -1726,11 +1718,7 @@ def test_glm5next_backend_decodes_only_sampled_frames(tmp_path):
1718
total_frames, fps = 60, 10
1719
path = _write_gray_video(tmp_path, total_frames, fps)
1720
1729
- from vllm.transformers_utils.processors.glm5next import (
1730
- glm_sample_frame_indices,
1731
- )
1732
-
1733
- expected = glm_sample_frame_indices(total_frames, float(fps), 6.0)
1721
+ expected = Glm5NextVideoBackend._sample_frame_indices(total_frames, float(fps), 6.0)
1722
1723
frames, metadata = Glm5NextVideoBackend.load_bytes(path.read_bytes())
1724
tests/test_config.py
+44
@@ -4114,3 +4114,47 @@ def test_revision_resolved_for_model(mock_resolve):
4114
assert isinstance(config.revision, ResolvedRevision)
4115
assert config.revision.resolved == REVISION
4116
mock_resolve.assert_any_call(model, None, config.hf_token)
4117
+
4118
+
4119
+@pytest.mark.parametrize(
4120
+ ("layer_types", "expected_attention"),
4121
+ [
4122
+ # Qwen3-Next / Qwen3.5 spell their attention layers "full_attention".
4123
+ (["linear_attention", "full_attention"], 1),
4124
+ # GLM-5.3-Flash and Qwen4-Exp use sparse attention, which still caches
4125
+ # every token and so must count as attention.
4126
+ (["linear_attention", "deepseek_sparse_attention"], 1),
4127
+ (["linear_attention", "qwen_sparse_attention"], 1),
4128
+ (["linear_attention", "linear_attention"], 0),
4129
+ ],
4130
+)
4131
+def test_hybrid_layer_counts_sparse_attention_as_attention(
4132
+ layer_types, expected_attention
4133
+):
4134
+ """Sparse-attention layer types consume a full attention KV cache."""
4135
+ model_config = object.__new__(ModelConfig)
4136
+ model_config.hf_config = SimpleNamespace()
4137
+ model_config.hf_text_config = SimpleNamespace(layer_types=layer_types)
4138
+ model_config.model_arch_config = SimpleNamespace(text_model_type="glm5_next")
4139
+
4140
+ parallel_config = SimpleNamespace()
4141
+ with (
4142
+ patch.object(ModelConfig, "is_hybrid", True),
4143
+ patch.object(ModelConfig, "has_noops", False),
4144
+ patch.object(ModelConfig, "is_attention_free", False),
4145
+ patch.object(
4146
+ ModelConfig,
4147
+ "get_layers_start_end_indices",
4148
+ return_value=(0, len(layer_types)),
4149
+ ),
4150
+ ):
4151
+ assert (
4152
+ model_config.get_num_layers_by_block_type(parallel_config, "attention")
4153
+ == expected_attention
4154
+ )
4155
+ assert (
4156
+ model_config.get_num_layers_by_block_type(
4157
+ parallel_config, "linear_attention"
4158
+ )
4159
+ == len(layer_types) - expected_attention
4160
+ )
tests/transformers_utils/processors/test_glm5next.py
deleted
-389
@@ -1,389 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Unit tests for the vLLM-native GLM-5.3-Flash multimodal processor."""
4
-
5
-import math
6
-
7
-import pytest
8
-import torch
9
-from PIL import Image
10
-from transformers.image_utils import PILImageResampling
11
-from transformers.video_utils import VideoMetadata
12
-
13
-from vllm.transformers_utils.processors.glm5next import (
14
- Glm5NextImageProcessor,
15
- Glm5NextVideoProcessor,
16
- _get_pad_content_size,
17
- _resize_or_pad,
18
- glm_sample_frame_indices,
19
- smart_resize,
20
-)
21
-
22
-PATCH_SIZE = 14
23
-MERGE_SIZE = 2
24
-PATCH_EXPAND_FACTOR = 1 # checkpoint processor_config.json
25
-FACTOR = PATCH_SIZE * MERGE_SIZE * PATCH_EXPAND_FACTOR # 28
26
-PIXELS_PER_TOKEN = 2 * (PATCH_SIZE * MERGE_SIZE) ** 2 # 1568
27
-MIN_TOKENS = 16
28
-MAX_TOKENS = 800 # serving-cap equivalent budget, keeps test canvases small
29
-MIN_PIXELS = MIN_TOKENS * PIXELS_PER_TOKEN
30
-MAX_PIXELS = MAX_TOKENS * PIXELS_PER_TOKEN
31
-
32
-
33
-def resize(height: int, width: int, t: int = 2) -> tuple[int, int]:
34
- return smart_resize(
35
- t,
36
- height,
37
- width,
38
- t_factor=2,
39
- h_factor=FACTOR,
40
- w_factor=FACTOR,
41
- min_pixels=MIN_PIXELS,
42
- max_pixels=MAX_PIXELS,
43
- )
44
-
45
-
46
-@pytest.mark.parametrize(
47
- ("height", "width", "t", "expected"),
48
- [
49
- (300, 400, 2, (308, 420)), # ceil alignment, no budget pressure
50
- (50, 80, 2, (112, 168)), # below min budget: proportional upscale
51
- (57, 3, 2, (504, 28)), # slim side ceils to one factor
52
- (2160, 3840, 2, (588, 1064)), # shrink: max canvas under the cap
53
- (112, 11200, 2, (84, 7420)), # extreme aspect stays proportional
54
- (300, 400, 16, (252, 308)), # frame count eats into the budget
55
- (112, 11200, 16, (28, 2800)), # slim side stays positive, budget holds
56
- ],
57
-)
58
-def test_smart_resize_reference_values(height, width, t, expected):
59
- got = smart_resize(
60
- t,
61
- height,
62
- width,
63
- t_factor=2,
64
- h_factor=FACTOR,
65
- w_factor=FACTOR,
66
- min_pixels=MIN_PIXELS,
67
- max_pixels=MAX_PIXELS,
68
- )
69
- assert got == expected
70
- # The aligned canvas never exceeds the budget it was fitted against.
71
- t_bar = max(2, round(t / 2) * 2)
72
- assert t_bar * got[0] * got[1] <= MAX_PIXELS
73
-
74
-
75
-def test_smart_resize_stays_snapped_and_positive():
76
- heights = (1, 27, 57, 111, 113, 300, 720, 2160)
77
- widths = (1, 27, 57, 111, 113, 400, 1280, 11200)
78
- for h in heights:
79
- for w in widths:
80
- for t in (2, 4, 16, 64):
81
- resized_h, resized_w = resize(h, w, t)
82
- assert resized_h > 0 and resized_w > 0
83
- assert resized_h % FACTOR == 0
84
- assert resized_w % FACTOR == 0
85
- t_bar = max(2, round(t / 2) * 2)
86
- assert t_bar * resized_h * resized_w <= MAX_PIXELS
87
-
88
-
89
-@pytest.mark.parametrize(
90
- ("height", "width", "match"),
91
- [
92
- (0, 100, "must be positive"),
93
- (100, 0, "must be positive"),
94
- ],
95
-)
96
-def test_smart_resize_rejects_degenerate_inputs(height, width, match):
97
- with pytest.raises(ValueError, match=match):
98
- resize(height, width, 2)
99
-
100
-
101
-def test_smart_resize_rejects_inverted_budget():
102
- with pytest.raises(ValueError, match="min_pixels must be less than or equal"):
103
- smart_resize(
104
- 2,
105
- 100,
106
- 100,
107
- t_factor=2,
108
- h_factor=FACTOR,
109
- w_factor=FACTOR,
110
- min_pixels=MAX_PIXELS,
111
- max_pixels=MIN_PIXELS,
112
- )
113
-
114
-
115
-def test_get_pad_content_size():
116
- # Oversized content shrinks proportionally, never upscales by default.
117
- assert _get_pad_content_size(300, 400, 308, 420) == (300, 400)
118
- assert _get_pad_content_size(600, 800, 308, 420) == (308, 410)
119
- # allow_upscale enlarges small content toward the canvas.
120
- assert _get_pad_content_size(28, 28, 112, 112, allow_upscale=True) == (112, 112)
121
-
122
-
123
-def test_resize_or_pad_pads_right_and_bottom():
124
- stacked = torch.rand(1, 3, 300, 400)
125
-
126
- def identity_resize(x, size, resample=None):
127
- assert (size.height, size.width) == (300, 400)
128
- return x
129
-
130
- padded = _resize_or_pad(stacked, 308, 420, "pad", None, identity_resize)
131
- assert padded.shape == (1, 3, 308, 420)
132
- torch.testing.assert_close(padded[..., :300, :400], stacked)
133
- assert padded[..., 300:, :].eq(0).all()
134
- assert padded[..., :, 400:].eq(0).all()
135
-
136
- def force_resize(x, size, resample=None):
137
- return torch.zeros(x.shape[0], x.shape[1], size.height, size.width)
138
-
139
- assert _resize_or_pad(stacked, 308, 420, "resize", None, force_resize).shape == (
140
- 1,
141
- 3,
142
- 308,
143
- 420,
144
- )
145
- with pytest.raises(ValueError, match="resize_mode"):
146
- _resize_or_pad(stacked, 308, 420, "crop", None, identity_resize)
147
-
148
-
149
-@pytest.fixture(scope="module")
150
-def image_processor():
151
- return Glm5NextImageProcessor(
152
- min_image_tokens=MIN_TOKENS, max_image_tokens=MAX_TOKENS
153
- )
154
-
155
-
156
-@pytest.mark.parametrize(
157
- ("height", "width"),
158
- [(50, 80), (300, 400), (2160, 3840), (112, 11200)],
159
-)
160
-def test_image_grid_matches_smart_resize(image_processor, height, width):
161
- out = image_processor(Image.new("RGB", (width, height)), return_tensors="pt")
162
- resized_h, resized_w = resize(height, width)
163
- grid = out["image_grid_thw"][0].tolist()
164
- assert grid == [1, resized_h // PATCH_SIZE, resized_w // PATCH_SIZE]
165
- assert out["pixel_values"].shape == (
166
- grid[1] * grid[2],
167
- 3 * 2 * PATCH_SIZE * PATCH_SIZE,
168
- )
169
-
170
-
171
-@pytest.mark.parametrize(
172
- ("height", "width"),
173
- [(50, 80), (300, 400), (2160, 3840), (112, 11200)],
174
-)
175
-def test_image_patch_count_matches_preprocess(image_processor, height, width):
176
- out = image_processor(Image.new("RGB", (width, height)), return_tensors="pt")
177
- grid = out["image_grid_thw"][0].tolist()
178
- expected = grid[1] * grid[2]
179
- assert image_processor.get_number_of_image_patches(height, width) == expected
180
-
181
-
182
-@pytest.fixture(scope="module")
183
-def video_processor():
184
- return Glm5NextVideoProcessor(
185
- min_image_tokens=MIN_TOKENS, max_image_tokens=MAX_TOKENS
186
- )
187
-
188
-
189
-def run_video_preprocess(video_processor, frames, **overrides):
190
- kwargs = dict(
191
- resample=PILImageResampling.BICUBIC,
192
- image_mean=(0.48145466, 0.4578275, 0.40821073),
193
- image_std=(0.26862954, 0.26130258, 0.27577711),
194
- patch_size=PATCH_SIZE,
195
- temporal_patch_size=2,
196
- merge_size=MERGE_SIZE,
197
- patch_expand_factor=PATCH_EXPAND_FACTOR,
198
- return_tensors="pt",
199
- )
200
- kwargs.update(overrides)
201
- return video_processor._preprocess([frames], **kwargs)
202
-
203
-
204
-def test_video_preprocess_pads_odd_frame_count(video_processor):
205
- out = run_video_preprocess(video_processor, torch.rand(7, 3, 300, 400))
206
- grid = out["video_grid_thw"][0].tolist()
207
- # 7 frames padded to 8 -> grid_t 4; canvas ceil-aligned (308, 420).
208
- assert grid == [4, 22, 30]
209
- assert out["pixel_values_videos"].shape == (
210
- 4 * 22 * 30,
211
- 3 * 2 * PATCH_SIZE * PATCH_SIZE,
212
- )
213
-
214
-
215
-def test_video_preprocess_keeps_min_side_under_frame_budget(video_processor):
216
- out = run_video_preprocess(video_processor, torch.rand(16, 3, 112, 11200))
217
- # Budget-fitted canvas (28, 2800): 16 * 28 * 2800 == the pixel cap.
218
- assert out["video_grid_thw"][0].tolist() == [8, 2, 200] # height 28, not 0
219
-
220
-
221
-def test_video_preprocess_pads_content_not_distort(video_processor):
222
- frames = torch.zeros(4, 3, 300, 400)
223
- frames[..., 50, 60] = 1.0 # marker inside the content area
224
- # Bypass rescale/normalize so zero padding stays exactly zero.
225
- out = run_video_preprocess(
226
- video_processor, frames, do_rescale=False, do_normalize=False
227
- )
228
- # Pad mode keeps the 300x400 content aspect on the (308, 420) canvas
229
- # with zero padding on the right/bottom.
230
- grid = out["video_grid_thw"][0].tolist()
231
- assert grid == [2, 22, 30]
232
- patches = out["pixel_values_videos"].view(
233
- grid[0],
234
- grid[1] // MERGE_SIZE,
235
- grid[2] // MERGE_SIZE,
236
- MERGE_SIZE,
237
- MERGE_SIZE,
238
- 3 * 2 * PATCH_SIZE * PATCH_SIZE,
239
- )
240
- frame = patches[0].permute(0, 3, 1, 4, 2).reshape(grid[1], grid[2], -1)
241
- # Patch columns fully right of the 400px content are pure padding.
242
- assert frame[:, math.ceil(400 / PATCH_SIZE) :].abs().max() == 0
243
-
244
-
245
-@pytest.mark.parametrize(
246
- ("total_frames", "fps", "duration", "expected_len", "first", "last"),
247
- [
248
- # Dense source: the tp-scaled greedy overshoots extract_t, so the
249
- # fixup spreads picks uniformly across the whole video (linspace).
250
- (900, 30.0, 30.0, 60, 0, 899),
251
- (3000, 30.0, 100.0, 200, 0, 2999),
252
- # extract_t capped at 2048 frames.
253
- (72000, 30.0, 2400.0, 2048, 0, 71999),
254
- # Low container fps: the greedy picks every frame.
255
- (48, 2.0, 24.0, 48, 0, 47),
256
- # Duration derived from the frame count when metadata lacks it.
257
- (300, 25.0, 0, 26, 0, 299),
258
- ],
259
-)
260
-def test_glm_sample_frame_indices_behaviour(
261
- total_frames, fps, duration, expected_len, first, last
262
-):
263
- indices = glm_sample_frame_indices(total_frames, fps, duration)
264
- assert len(indices) == expected_len
265
- assert indices[0] == first
266
- assert indices[-1] == last
267
- assert len(indices) % 2 == 0
268
- assert indices == sorted(indices)
269
- assert all(0 <= idx < total_frames for idx in indices)
270
-
271
-
272
-def test_glm_sample_frame_indices_short_clip_floor_spread():
273
- # extract_t (20) > total (7): evenly spaced timestamps, deduplicated,
274
- # then pair-padded to an even count.
275
- assert glm_sample_frame_indices(7, 30.0, 10.0) == [0, 1, 2, 3, 4, 5, 6, 6]
276
-
277
-
278
-@pytest.mark.parametrize(
279
- ("kwargs", "expected_len"),
280
- [
281
- ({"target_fps": 0.5}, 16),
282
- ({"max_frame_count": 16}, 16),
283
- ({"target_fps": 8}, 240), # 30s * 8 = 240, under the 2048 cap
284
- ],
285
-)
286
-def test_glm_sample_frame_indices_request_overrides(kwargs, expected_len):
287
- indices = glm_sample_frame_indices(900, 30.0, 30.0, **kwargs)
288
- assert len(indices) == expected_len
289
- assert len(indices) % 2 == 0
290
-
291
-
292
-def test_sample_frames_fps_interval(video_processor):
293
- indices = video_processor.sample_frames(
294
- VideoMetadata(total_num_frames=900, fps=30.0, duration=30.0)
295
- )
296
- assert len(indices) == 60
297
- assert indices[0] == 0
298
- assert indices[-1] == 899 # uniform spread reaches the final frame
299
-
300
- # Request overrides reach the sampler through both kwarg spellings. The
301
- # 15-frame spread is odd, so pair-padding duplicates the last frame.
302
- assert (
303
- len(
304
- video_processor.sample_frames(
305
- VideoMetadata(total_num_frames=900, fps=30.0, duration=30.0),
306
- fps=0.5,
307
- )
308
- )
309
- == 16
310
- )
311
- assert (
312
- len(
313
- video_processor.sample_frames(
314
- VideoMetadata(total_num_frames=900, fps=30.0, duration=30.0),
315
- target_fps=0.5,
316
- )
317
- )
318
- == 16
319
- )
320
- assert (
321
- len(
322
- video_processor.sample_frames(
323
- VideoMetadata(total_num_frames=900, fps=30.0, duration=30.0),
324
- max_frames=16,
325
- )
326
- )
327
- == 16
328
- )
329
-
330
-
331
-def test_defaults_mirror_checkpoint_config():
332
- """Bare instantiation matches the checkpoint's ``processor_config.json``
333
- token-budget style defaults; ``size`` carries no budget."""
334
- image_processor = Glm5NextImageProcessor()
335
- assert image_processor.patch_expand_factor == 1
336
- assert image_processor.min_image_tokens == 16
337
- assert image_processor.max_image_tokens == 8000
338
- assert image_processor.resize_mode == "pad"
339
-
340
- video_processor = Glm5NextVideoProcessor()
341
- assert video_processor.patch_expand_factor == 1
342
- assert video_processor.min_image_tokens == 16
343
- assert video_processor.max_image_tokens == 240000
344
- assert video_processor.fps_interval == 2.0
345
- assert video_processor.max_frame_count_dynamic == 2048
346
-
347
-
348
-def test_token_budgets_drive_geometry():
349
- """The token bounds fully determine the pixel budget and the grid."""
350
- token_proc = Glm5NextImageProcessor(min_image_tokens=64, max_image_tokens=512)
351
- img = Image.new("RGB", (400, 300))
352
- grid = token_proc(img, return_tensors="pt")["image_grid_thw"][0].tolist()
353
- # Budgets 100,352..802,816 px: the 300x400 canvas ceils to (308, 420)
354
- # without hitting either bound -> 22x30 patches.
355
- assert grid == [1, 22, 30]
356
- assert token_proc.get_number_of_image_patches(300, 400) == 22 * 30
357
-
358
-
359
-def test_missing_token_budgets_rejected():
360
- proc = Glm5NextImageProcessor(min_image_tokens=None)
361
- with pytest.raises(ValueError, match="min_image_tokens"):
362
- proc(Image.new("RGB", (400, 300)), return_tensors="pt")
363
-
364
-
365
-def test_video_config_fields_land():
366
- """fps_interval / max_frame_count_dynamic from the dedicated config
367
- shape sampling without any request overrides."""
368
- proc = Glm5NextVideoProcessor(
369
- fps_interval=4,
370
- max_frame_count_dynamic=32,
371
- )
372
- indices = proc.sample_frames(
373
- VideoMetadata(total_num_frames=900, fps=30.0, duration=30.0)
374
- )
375
- # 30s * 4 = 120 candidates, capped at 32 -> uniform spread of 32.
376
- assert len(indices) == 32
377
- assert indices[0] == 0
378
- assert indices[-1] == 899
379
-
380
- # Request overrides still win over the config values.
381
- assert (
382
- len(
383
- proc.sample_frames(
384
- VideoMetadata(total_num_frames=900, fps=30.0, duration=30.0),
385
- fps=0.5,
386
- )
387
- )
388
- == 16
389
- )
tests/transformers_utils/test_config.py
-53
@@ -24,11 +24,6 @@ from vllm.transformers_utils.config import (
24
try_get_generation_config,
25
uses_mrope,
26
)
27
-from vllm.transformers_utils.configs.glm5_next import (
28
- Glm5NextConfig,
29
- Glm5NextTextConfig,
30
- Glm5NextVisionConfig,
31
-)
27
from vllm.transformers_utils.configs.mistral import adapt_config_dict
28
29
@@ -174,54 +169,6 @@ def test_mistral_yarn_apply_scale_false_disables_yarn_magnitude_scaling():
169
assert config.rope_parameters["attention_factor"] == 1.0
170
171
177
-def test_glm5_next_accepts_deepseek_sparse_attention_layers():
178
- layer_types = ["linear_attention", "deepseek_sparse_attention"]
179
-
180
- config = Glm5NextTextConfig(
181
- num_hidden_layers=len(layer_types), layer_types=layer_types
182
- )
183
-
184
- assert config.layer_types == layer_types
185
- assert config.layers_block_type == ["linear_attention", "attention"]
186
-
187
-
188
-def test_glm5_next_accepts_prebuilt_subconfigs():
189
- text_config = Glm5NextTextConfig(hidden_size=1024)
190
- vision_config = Glm5NextVisionConfig(hidden_size=768)
191
-
192
- config = Glm5NextConfig(
193
- text_config=text_config,
194
- vision_config=vision_config,
195
- )
196
-
197
- assert config.text_config is text_config
198
- assert config.vision_config is vision_config
199
-
200
-
201
-@pytest.mark.parametrize(
202
- ("kwargs", "option"),
203
- [
204
- (
205
- {"index_topk": 2048, "index_dsa_use_layernorm": False},
206
- "index_dsa_use_layernorm",
207
- ),
208
- (
209
- {"index_topk": 2048, "index_kpool_compress": False},
210
- "index_kpool_compress",
211
- ),
212
- (
213
- {"index_topk": 2048, "index_kpool_always_select_tail": False},
214
- "index_kpool_always_select_tail",
215
- ),
216
- ({"hres_vwnstyle": False}, "hres_vwnstyle"),
217
- ({"mhc_no_norm_weight": True}, "mhc_no_norm_weight"),
218
- ],
219
-)
220
-def test_glm5_next_rejects_unimplemented_config_options(kwargs, option):
221
- with pytest.raises(NotImplementedError, match=option):
222
- Glm5NextTextConfig(**kwargs)
223
-
224
-
172
def test_get_llama3_eos_token():
173
model_name = "meta-llama/Llama-3.2-1B-Instruct"
174
tests/v1/spec_decode/test_mtp.py
+1
-1
@@ -43,7 +43,7 @@ def test_glm_mtp_defers_lm_head(default_vllm_config):
43
index_kpool=4,
44
)
45
vllm_config = mock.MagicMock()
46
- vllm_config.speculative_config.draft_model_config.hf_config = config
46
+ vllm_config.speculative_config.draft_model_config.hf_text_config = config
47
vllm_config.scheduler_config.max_num_batched_tokens = 4
48
49
with (
vllm/config/model.py
+12
-1
@@ -108,6 +108,16 @@ HfOverrides = dict[str, Any] | Callable[[PreTrainedConfig], PreTrainedConfig]
108
ModelImpl = Literal["auto", "vllm", "transformers", "terratorch"]
109
LayerBlockType = Literal["attention", "linear_attention", "mamba"]
110
111
+_ATTENTION_LAYER_TYPES = frozenset(
112
+ {
113
+ "full_attention",
114
+ "deepseek_sparse_attention",
115
+ "qwen_sparse_attention",
116
+ }
117
+)
118
+"""`layer_types` spellings that consume a full attention KV cache. Sparse
119
+attention still caches every token, so it counts as attention here."""
120
+
121
_RUNNER_CONVERTS: dict[RunnerType, list[ConvertType]] = {
122
"generate": [],
123
"pooling": ["embed", "classify"],
@@ -1718,7 +1728,8 @@ class ModelConfig:
1728
if layer_types_value is not None:
1729
if block_type == "attention":
1730
return sum(
1721
- t == "full_attention" for t in layer_types_value[start:end]
1731
+ t in _ATTENTION_LAYER_TYPES
1732
+ for t in layer_types_value[start:end]
1733
)
1734
elif block_type == "linear_attention":
1735
return sum(
vllm/config/speculative.py
+1
-1
@@ -1059,7 +1059,7 @@ class SpeculativeConfig:
1059
)
1060
if hf_config.model_type == "glm5_next":
1061
hf_config.model_type = "glm5_next_mtp"
1062
- n_predict = hf_config.num_nextn_predict_layers
1062
+ n_predict = hf_config.get_text_config().num_nextn_predict_layers
1063
hf_config.update(
1064
{"n_predict": n_predict, "architectures": ["Glm5NextMTPModel"]}
1065
)
vllm/models/glm5next/common/attention.py
+18
-14
@@ -4,6 +4,7 @@
4
import torch
5
import torch.nn.functional as F
6
from torch import nn
7
+from transformers import Glm5NextTextConfig
8
9
from vllm.config import (
10
CacheConfig,
@@ -32,7 +33,6 @@ from vllm.model_executor.utils import maybe_disable_graph_partition
33
from vllm.models.glm5next.nvidia.ops.kpool_compress import fwht128_quant_fp8
34
from vllm.models.glm5next.sparse_indexer import SparseAttnIndexerKpool
35
from vllm.platforms import current_platform
35
-from vllm.transformers_utils.configs.glm5_next import Glm5NextConfig
36
from vllm.utils.deep_gemm import PAGED_MQA_PAGE_SIZES
37
from vllm.utils.math_utils import cdiv, next_power_of_2
38
from vllm.v1.kv_cache_interface import KpoolTailSpec, MLAAttentionSpec
@@ -221,7 +221,7 @@ class Indexer(nn.Module):
221
def __init__(
222
self,
223
vllm_config: VllmConfig,
224
- config: Glm5NextConfig,
224
+ config: Glm5NextTextConfig,
225
hidden_size: int,
226
q_lora_rank: int,
227
quant_config: QuantizationConfig | None,
@@ -236,7 +236,7 @@ class Indexer(nn.Module):
236
# self.indexer_cfg = config.attn_module_list_cfg[0]["attn_index"]
237
# Indexer is only constructed for v32 configs, where these sparse-indexer
238
# fields are guaranteed populated; narrow away the `int | None` declared
239
- # on Glm5NextConfig for the optional-indexer case.
239
+ # on Glm5NextTextConfig for the optional-indexer case.
240
assert config.index_topk is not None
241
assert config.index_n_heads is not None
242
assert config.index_head_dim is not None
@@ -419,7 +419,7 @@ class Glm5NextMLAAttention(nn.Module):
419
def __init__(
420
self,
421
vllm_config: VllmConfig,
422
- config: Glm5NextConfig,
422
+ config: Glm5NextTextConfig,
423
hidden_size: int,
424
num_heads: int,
425
qk_nope_head_dim: int,
@@ -506,28 +506,32 @@ class Glm5NextMLAAttention(nn.Module):
506
prefix=f"{prefix}.o_proj",
507
)
508
509
+ # `Glm5NextTextConfig` is NoPE-only and so declares no `rope_parameters`;
510
+ # a checkpoint may still ship one.
511
+ rope_parameters = getattr(config, "rope_parameters", None)
512
+
513
if not skip_rope:
510
- assert config.rope_parameters is not None
511
- if config.rope_parameters["rope_type"] != "default":
512
- config.rope_parameters["rope_type"] = (
514
+ assert rope_parameters is not None
515
+ if rope_parameters["rope_type"] != "default":
516
+ rope_parameters["rope_type"] = (
517
"deepseek_llama_scaling"
514
- if config.rope_parameters.get("attention_factor") == 1.0
518
+ if rope_parameters.get("attention_factor") == 1.0
519
else "deepseek_yarn"
520
)
521
522
self.rotary_emb: RotaryEmbedding | None = get_rope(
523
qk_rope_head_dim,
524
max_position=max_position_embeddings,
521
- rope_parameters=config.rope_parameters,
525
+ rope_parameters=rope_parameters,
526
is_neox_style=False,
527
)
528
529
if (
526
- config.rope_parameters["rope_type"] != "default"
527
- and config.rope_parameters["rope_type"] == "deepseek_yarn"
530
+ rope_parameters["rope_type"] != "default"
531
+ and rope_parameters["rope_type"] == "deepseek_yarn"
532
):
529
- mscale_all_dim = config.rope_parameters.get("mscale_all_dim", False)
530
- scaling_factor = config.rope_parameters["factor"]
533
+ mscale_all_dim = rope_parameters.get("mscale_all_dim", False)
534
+ scaling_factor = rope_parameters["factor"]
535
mscale = yarn_get_mscale(scaling_factor, float(mscale_all_dim))
536
self.scaling = self.scaling * mscale * mscale
537
else:
@@ -539,7 +543,7 @@ class Glm5NextMLAAttention(nn.Module):
543
self.indexer_rope_emb: RotaryEmbedding | None = get_rope(
544
qk_rope_head_dim,
545
max_position=max_position_embeddings,
542
- rope_parameters=config.rope_parameters,
546
+ rope_parameters=rope_parameters,
547
is_neox_style=not config.indexer_rope_interleave,
548
)
549
# The sparse indexer projects from the MLA q-lora rank, which is
vllm/models/glm5next/common/kda.py
+2
-2
@@ -6,6 +6,7 @@ from dataclasses import replace
6
7
import torch
8
from torch import nn
9
+from transformers import Glm5NextTextConfig
10
11
from vllm.compilation.breakable_cudagraph import eager_break_during_capture
12
from vllm.config import VllmConfig, get_current_vllm_config
@@ -44,7 +45,6 @@ from vllm.model_executor.utils import (
45
)
46
from vllm.platforms import current_platform
47
from vllm.third_party.flash_linear_attention.ops.kda import FusedRMSNormGated
47
-from vllm.transformers_utils.configs.glm5_next import Glm5NextConfig
48
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
49
from vllm.v1.kv_cache_interface import MambaSpec
50
from vllm.v1.worker.workspace import current_workspace_manager
@@ -201,7 +201,7 @@ class Glm5NextLinearAttention(GatedDeltaNetAttention):
201
202
def __init__(
203
self,
204
- config: Glm5NextConfig,
204
+ config: Glm5NextTextConfig,
205
vllm_config: VllmConfig,
206
prefix: str = "",
207
) -> None:
vllm/models/glm5next/common/model.py
+74
-26
@@ -6,6 +6,7 @@ from typing import ClassVar, Literal
6
7
import torch
8
from torch import nn
9
+from transformers import Glm5NextTextConfig
10
11
from vllm.config import ParallelConfig, VllmConfig
12
from vllm.distributed import (
@@ -83,7 +84,6 @@ from vllm.models.common.ops.sequence_parallel import (
84
from vllm.multimodal import MULTIMODAL_REGISTRY
85
from vllm.platforms import current_platform
86
from vllm.sequence import IntermediateTensors
86
-from vllm.transformers_utils.configs.glm5_next import Glm5NextConfig
87
88
from .attention import Glm5NextMLAAttention
89
from .kda import Glm5NextLinearAttention
@@ -95,6 +95,57 @@ from .multimodal import (
95
96
logger = init_logger(__name__)
97
98
+_MHC_TAU = 0.05
99
+"""mHC routing temperature. A GLM-5.3-Flash trained value that neither the
100
+checkpoint nor `Glm5NextTextConfig` carries."""
101
+
102
+_MHC_POST_MULT_VALUE = 2.0
103
+"""mHC post-multiplier. A GLM-5.3-Flash trained value that neither the
104
+checkpoint nor `Glm5NextTextConfig` carries."""
105
+
106
+_LOGIT_SCALE = 1.0
107
+"""Output logit scale. A GLM-5.3-Flash trained value that neither the
108
+checkpoint nor `Glm5NextTextConfig` carries."""
109
+
110
+_VISION_RMS_NORM_EPS = 1e-6
111
+"""Vision tower RMSNorm epsilon.
112
+
113
+GLM-5.3-Flash checkpoints ship `vision_config.rms_norm_eps = 1e-5`, but the
114
+vision tower was trained with 1e-6. Serving with 1e-5 drifts the RMSNorm and
115
+produces repetitive/degraded image descriptions, so force the trained value
116
+regardless of the checkpoint field.
117
+"""
118
+
119
+
120
+def _is_moe(config: Glm5NextTextConfig) -> bool:
121
+ return config.n_routed_experts is not None
122
+
123
+
124
+def _is_kda_layer(config: Glm5NextTextConfig, layer_idx: int) -> bool:
125
+ layer_types = config.layer_types
126
+ return (
127
+ layer_types is not None
128
+ and layer_idx < len(layer_types)
129
+ and layer_types[layer_idx] == "linear_attention"
130
+ )
131
+
132
+
133
+def _is_linear_attn(config: Glm5NextTextConfig) -> bool:
134
+ layer_types = config.layer_types
135
+ return layer_types is not None and "linear_attention" in layer_types
136
+
137
+
138
+def _validate_supported_config(config: Glm5NextTextConfig) -> None:
139
+ """Reject checkpoints using config options this implementation lacks.
140
+
141
+ The kpool indexer kernels always keep the incomplete trailing pool, so a
142
+ checkpoint asking otherwise would be served silently wrong.
143
+ """
144
+ if config.index_topk is not None and not config.index_kpool_always_select_tail:
145
+ raise NotImplementedError(
146
+ "GLM-5.3 sparse indexer requires index_kpool_always_select_tail=True"
147
+ )
148
+
149
150
class Glm5NextMLP(nn.Module):
151
def __init__(
@@ -152,7 +203,7 @@ class Glm5NextMLP(nn.Module):
203
class Glm5NextMoE(nn.Module):
204
def __init__(
205
self,
155
- config: Glm5NextConfig,
206
+ config: Glm5NextTextConfig,
207
parallel_config: ParallelConfig,
208
quant_config: QuantizationConfig | None = None,
209
prefix: str = "",
@@ -227,10 +278,10 @@ class Glm5NextMoE(nn.Module):
278
shared_experts=self.shared_experts,
279
gate=self.gate,
280
num_experts=config.n_routed_experts,
230
- top_k=config.num_experts_per_token,
281
+ top_k=config.num_experts_per_tok,
282
hidden_size=config.hidden_size,
283
intermediate_size=config.moe_intermediate_size,
233
- renormalize=config.moe_renormalize,
284
+ renormalize=config.norm_topk_prob,
285
quant_config=quant_config,
286
use_grouped_topk=True,
287
num_expert_group=config.n_group,
@@ -280,7 +331,7 @@ class Glm5NextDecoderLayer(nn.Module):
331
def __init__(
332
self,
333
vllm_config: VllmConfig,
283
- config: Glm5NextConfig,
334
+ config: Glm5NextTextConfig,
335
layer_idx: int,
336
prefix: str = "",
337
topk_indices_buffer: torch.Tensor | None = None,
@@ -295,13 +346,13 @@ class Glm5NextDecoderLayer(nn.Module):
346
347
self.hidden_size = config.hidden_size
348
self.layer_idx = layer_idx
298
- self.is_moe = config.is_moe
349
+ self.is_moe = _is_moe(config)
350
self.num_hidden_layers = config.num_hidden_layers
351
self.rms_norm_eps = config.rms_norm_eps
352
self.num_experts = config.n_routed_experts
353
self.is_mtp_layer = is_mtp_layer
354
self.mhc = config.mhc
304
- is_kda_layer = not is_mtp_layer and config.is_kda_layer(layer_idx)
355
+ is_kda_layer = not is_mtp_layer and _is_kda_layer(config, layer_idx)
356
self.layer_kind = "kda" if is_kda_layer else "mla"
357
self.is_sequence_parallel = parallel_config.use_sequence_parallel_moe
358
@@ -331,7 +382,7 @@ class Glm5NextDecoderLayer(nn.Module):
382
quant_config=None, # MLA projections are BF16 in checkpoint
383
prefix=f"{prefix}.self_attn",
384
topk_indices_buffer=topk_indices_buffer,
334
- skip_rope=config.mla_nope,
385
+ skip_rope=config.mla_use_nope,
386
)
387
388
# MTP layers sit past the base model's hidden layers (layer_idx >=
@@ -375,13 +426,13 @@ class Glm5NextDecoderLayer(nn.Module):
426
427
if self.mhc and not is_mtp_layer:
428
# mhc config
378
- self.mhc_num_residual_streams = config.mhc_num_residual_streams
379
- self.mhc_tau = config.mhc_tau
429
+ self.mhc_num_residual_streams = config.hc_mult
430
+ self.mhc_tau = _MHC_TAU
431
self.hc_eps = config.hc_eps
381
- self.mhc_sinkhorn_iterations = config.mhc_sinkhorn_iterations
382
- self.mhc_post_mult_value = config.mhc_post_mult_value
432
+ self.mhc_sinkhorn_iterations = config.hc_sinkhorn_iters
433
+ self.mhc_post_mult_value = _MHC_POST_MULT_VALUE
434
384
- n = config.mhc_num_residual_streams
435
+ n = config.hc_mult
436
d_model = n * self.hidden_size
437
mix_hc = (2 + n) * n
438
@@ -625,7 +676,8 @@ class Glm5NextModel(nn.Module):
676
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
677
super().__init__()
678
628
- config = vllm_config.model_config.hf_config
679
+ config = vllm_config.model_config.hf_text_config
680
+ _validate_supported_config(config)
681
self.config = config
682
683
self.vocab_size = config.vocab_size
@@ -768,7 +820,7 @@ class Glm5NextModel(nn.Module):
820
(".in_proj_qkvbfg_a", ".f_a_proj", 4),
821
(".in_proj_qkvbfg_a", ".g_a_proj", 5),
822
]
771
- if self.config.is_moe:
823
+ if _is_moe(self.config):
824
# Params for weights, fp8 weight scales, fp8 activation scales
825
# (param_name, weight_name, expert_id, shard_id)
826
# EPLB: the mapping enumerates physical experts, so it must cover
@@ -798,7 +850,7 @@ class Glm5NextModel(nn.Module):
850
# GLM-5.3-Flash NoPE checkpoints omit the RoPE rows from
851
# ``kv_a_proj_with_mqa``; pad them with zeros for the model shape.
852
kv_a_pad_size = 0
801
- if self.config.mla_nope and self.config.qk_rope_head_dim > 0:
853
+ if self.config.mla_use_nope and self.config.qk_rope_head_dim > 0:
854
kv_a_pad_size = self.config.qk_rope_head_dim
855
856
_pending_wk_fp8: dict = {}
@@ -912,7 +964,7 @@ class Glm5NextModel(nn.Module):
964
if (
965
name.endswith(".bias")
966
and name not in params_dict
915
- and not self.config.is_linear_attn
967
+ and not _is_linear_attn(self.config)
968
): # noqa: E501
969
continue
970
# Remapping the name of FP8 kv-scale.
@@ -939,7 +991,7 @@ class Glm5NextForCausalLM(
991
super().__init__()
992
self.model_config = vllm_config.model_config
993
self.vllm_config = vllm_config
942
- self.config = self.model_config.hf_config
994
+ self.config = self.model_config.hf_text_config
995
quant_config = vllm_config.quant_config
996
self.quant_config = quant_config
997
self.model = Glm5NextModel(
@@ -955,7 +1007,7 @@ class Glm5NextForCausalLM(
1007
else:
1008
self.lm_head = PPMissingLayer()
1009
self.logits_processor = LogitsProcessor(
958
- self.config.vocab_size, scale=self.config.logit_scale
1010
+ self.config.vocab_size, scale=_LOGIT_SCALE
1011
)
1012
1013
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
@@ -988,7 +1040,7 @@ class Glm5NextForCausalLM(
1040
cls, vllm_config: "VllmConfig"
1041
) -> tuple[tuple[int, int], tuple[int, int, int]]:
1042
parallel_config = vllm_config.parallel_config
991
- hf_config = vllm_config.model_config.hf_config
1043
+ hf_config = vllm_config.model_config.hf_text_config
1044
tp_size = parallel_config.tensor_parallel_size
1045
num_spec = (
1046
vllm_config.speculative_config.num_speculative_tokens
@@ -1092,11 +1144,7 @@ class Glm5NextForConditionalGeneration(
1144
self.visual = Glm5NextVisionTransformer(
1145
config.text_config,
1146
config.vision_config,
1095
- # Read eps from the VISION sub-config, not the top-level
1096
- # `config.rms_norm_eps`: Glm5NextConfig.__getattribute__ mirrors
1097
- # the latter onto text_config (1e-5), silently ignoring the
1098
- # vision tower's own (1e-6) rms_norm_eps.
1099
- norm_eps=config.vision_config.rms_norm_eps,
1147
+ norm_eps=_VISION_RMS_NORM_EPS,
1148
# Vision tower ships BF16 weights in this fp8 checkpoint (no
1149
# weight_scale_inv for visual.*), so it must NOT inherit the
1150
# global fp8 quant_config -- doing so incorrectly quantizes
@@ -1166,7 +1214,7 @@ class Glm5NextForConditionalGeneration(
1214
1215
1216
def get_spec_layer_idx_from_weight_name(
1169
- config: Glm5NextConfig, weight_name: str
1217
+ config: Glm5NextTextConfig, weight_name: str
1218
) -> int | None:
1219
if hasattr(config, "num_nextn_predict_layers") and (
1220
config.num_nextn_predict_layers > 0
vllm/models/glm5next/common/mtp.py
+4
-4
@@ -40,7 +40,7 @@ class Glm5NextMultiTokenPredictorLayer(nn.Module):
40
def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
41
super().__init__()
42
assert vllm_config.speculative_config is not None
43
- config = vllm_config.speculative_config.draft_model_config.hf_config
43
+ config = vllm_config.speculative_config.draft_model_config.hf_text_config
44
self.config = config
45
46
self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -114,7 +114,7 @@ class Glm5NextMultiTokenPredictorLayer(nn.Module):
114
class Glm5NextMultiTokenPredictor(nn.Module):
115
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
116
super().__init__()
117
- config = vllm_config.model_config.hf_config
117
+ config = vllm_config.model_config.hf_text_config
118
self.mtp_start_layer_idx = config.num_hidden_layers
119
self.num_mtp_layers = config.num_nextn_predict_layers
120
self.layers = torch.nn.ModuleDict(
@@ -211,7 +211,7 @@ class Glm5NextMultiTokenPredictor(nn.Module):
211
class Glm5NextMTP(nn.Module, DeepseekV2MixtureOfExperts):
212
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
213
super().__init__()
214
- self.config = vllm_config.model_config.hf_config
214
+ self.config = vllm_config.model_config.hf_text_config
215
self.quant_config = vllm_config.quant_config
216
self.model = Glm5NextMultiTokenPredictor(
217
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
@@ -313,7 +313,7 @@ class Glm5NextMTP(nn.Module, DeepseekV2MixtureOfExperts):
313
# GLM-5.3-Flash NoPE checkpoints omit the RoPE rows from
314
# ``kv_a_proj_with_mqa``; the FP8-to-BF16 path pads them for the model.
315
kv_a_pad_size = 0
316
- if self.config.mla_nope and self.config.qk_rope_head_dim > 0:
316
+ if self.config.mla_use_nope and self.config.qk_rope_head_dim > 0:
317
kv_a_pad_size = self.config.qk_rope_head_dim
318
for name, loaded_weight in weights:
319
if "rotary_emb.inv_freq" in name:
vllm/models/glm5next/common/multimodal.py
+68
-52
@@ -3,13 +3,14 @@
3
"""GLM-5.3-Flash vision tower and multimodal processor."""
4
5
from collections.abc import Mapping
6
-from functools import cached_property, partial
6
+from functools import partial
7
from typing import Any
8
9
import numpy as np
10
import torch
11
import torch.nn as nn
12
from einops import rearrange
13
+from transformers.video_utils import VideoMetadata
14
15
from vllm.distributed import (
16
get_tensor_model_parallel_world_size,
@@ -44,7 +45,6 @@ from vllm.model_executor.models.vision import (
45
)
46
from vllm.models.common.ops import fused_q_kv_rmsnorm
47
from vllm.multimodal.parse import ImageSize, MultiModalDataItems
47
-from vllm.transformers_utils.processors.glm5next import glm_sample_frame_indices
48
from vllm.v1.attention.backends.registry import AttentionBackendEnum
49
50
@@ -616,37 +616,48 @@ class Glm5NextVisionTransformer(nn.Module):
616
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
617
618
619
+_MAX_VIDEO_TOKENS = 30000
620
+
621
+
622
class Glm5NextProcessingInfo(Glm4vProcessingInfo):
620
- """Wires up the vLLM-native processor for the multimodal checkpoint.
621
-
622
- The checkpoint's ``processor_config.json`` declares a custom ``processor_class``
623
- and stores its image/video processor configs inline (no standalone
624
- ``preprocessor_config.json``), so ``AutoProcessor`` cannot resolve the
625
- config. We bypass it and build our own ``Glm5NextProcessor``
626
- (``vllm/transformers_utils/processors/glm5next.py``), a port of the
627
- training-side pipeline that no longer imports transformers' GLM processor
628
- classes. The port applies ``patch_expand_factor`` (checkpoint ships 1)
629
- inside ``smart_resize``'s spatial factor.
623
+ """Token-budget geometry for the multimodal checkpoint.
624
+
625
+ This checkpoint's ``processor_config.json`` ships the token-budget style
626
+ (``min_image_tokens`` / ``max_image_tokens``) with no ``size`` key, so the
627
+ inherited Glm4v ``size.longest_edge`` path does not apply.
628
"""
629
632
- @cached_property
633
- def _glm5_hf_processor(self):
634
- from vllm.transformers_utils.processors.glm5next import Glm5NextProcessor
630
+ def get_hf_processor(self, **kwargs: object):
631
+ processor = super().get_hf_processor(**kwargs)
632
+ # Cap video inputs to keep encoder profiling from starving the KV cache.
633
+ video_processor = processor.video_processor
634
+ video_processor.max_image_tokens = min(
635
+ video_processor.max_image_tokens, _MAX_VIDEO_TOKENS
636
+ )
637
+ return processor
638
636
- return Glm5NextProcessor.from_pretrained(self.ctx.model_config.model)
639
+ @staticmethod
640
+ def _alignment_factor(proc) -> int:
641
+ """Spatial factor canvases align to, as ``smart_resize`` takes it."""
642
+ return proc.patch_size * proc.merge_size * proc.patch_expand_factor
643
638
- def get_hf_processor(self, **kwargs: object):
639
- return self._glm5_hf_processor
644
+ @classmethod
645
+ def _pixels_per_token(cls, proc) -> int:
646
+ """Pixels one vision token covers, matching ``smart_resize``'s own
647
+ ``temporal_factor * factor ** 2``."""
648
+ return proc.temporal_patch_size * cls._alignment_factor(proc) ** 2
649
650
def _processor_pixel_budget(self, proc) -> tuple[int, int]:
642
- from vllm.transformers_utils.processors.glm5next import _pixel_budget
643
-
644
- return _pixel_budget(
645
- proc.min_image_tokens,
646
- proc.max_image_tokens,
647
- proc.patch_size,
648
- proc.merge_size,
649
- proc.temporal_patch_size,
651
+ """(min_pixels, max_pixels) from the processor's token bounds."""
652
+ if proc.min_image_tokens is None or proc.max_image_tokens is None:
653
+ raise ValueError(
654
+ "min_image_tokens and max_image_tokens must be provided by "
655
+ "processor_config.json (or per-call kwargs)."
656
+ )
657
+ pixels_per_token = self._pixels_per_token(proc)
658
+ return (
659
+ proc.min_image_tokens * pixels_per_token,
660
+ proc.max_image_tokens * pixels_per_token,
661
)
662
663
def _get_image_max_pixels(self) -> int:
@@ -687,19 +698,20 @@ class Glm5NextProcessingInfo(Glm4vProcessingInfo):
698
else:
699
frame_indices = [
700
int(idx)
690
- for idx in glm_sample_frame_indices(
691
- int(metadata.get("total_num_frames", total_frames)),
692
- video_fps,
693
- float(metadata.get("duration") or 0),
694
- target_fps=video_processor.fps_interval,
695
- max_frame_count=video_processor.max_frame_count_dynamic,
696
- temporal_patch_size=temporal_patch_size,
701
+ for idx in video_processor.sample_frames(
702
+ VideoMetadata(
703
+ total_num_frames=int(
704
+ metadata.get("total_num_frames", total_frames)
705
+ ),
706
+ fps=video_fps,
707
+ duration=metadata.get("duration"),
708
+ )
709
)
710
]
711
num_frames = len(frame_indices)
712
713
if not frame_indices or num_frames <= 0:
702
- # ``duration * fps_interval < 1`` selects no frames at all; the
714
+ # ``duration * fps < 1`` selects no frames at all; the
715
# pixel path raises an opaque IndexError on the same input a moment
716
# later, so say what happened while there is still a request.
717
raise ValueError(
@@ -731,12 +743,13 @@ class Glm5NextProcessingInfo(Glm4vProcessingInfo):
743
744
The inherited Glm4v path resolves the pixel budget from
745
``size.longest_edge`` and resizes with GLM-4V's ``smart_resize``. This
734
- checkpoint's ``processor_config.json`` ships the token-budget style
735
- (``min_image_tokens`` / ``max_image_tokens``) with no ``size`` key, and
736
- the alignment factor carries ``patch_expand_factor`` — resolve both
737
- from the vLLM-native processor so profiling matches runtime geometry.
746
+ checkpoint ships token bounds instead, and the alignment factor carries
747
+ ``patch_expand_factor`` — resolve both from the processor so profiling
748
+ matches runtime geometry.
749
"""
739
- from vllm.transformers_utils.processors.glm5next import smart_resize
750
+ from transformers.models.glm5_next.image_processing_glm5_next import (
751
+ smart_resize,
752
+ )
753
754
vision_config = self.get_hf_config().vision_config
755
patch_size = vision_config.patch_size
@@ -744,22 +757,25 @@ class Glm5NextProcessingInfo(Glm4vProcessingInfo):
757
temporal_patch_size = vision_config.temporal_patch_size
758
759
image_processor = self.get_hf_processor().image_processor
747
- factor = patch_size * merge_size * image_processor.patch_expand_factor
748
- # Keep the profiling search viable when the caller's budget is below
749
- # one aligned canvas of the requested duration.
750
- max_image_pixels = max(max_image_pixels, temporal_patch_size * factor * factor)
760
+ factor = self._alignment_factor(image_processor)
761
+ temporal_factor = image_processor.temporal_patch_size
762
+ # `smart_resize` denominates its bounds in vision tokens. Round down,
763
+ # but keep the profiling search viable when the caller's budget is
764
+ # below one aligned canvas of the requested duration.
765
+ max_image_tokens = max(
766
+ max_image_pixels // self._pixels_per_token(image_processor), 1
767
+ )
768
769
if do_resize:
753
- t = num_frames if num_frames > temporal_patch_size else temporal_patch_size
770
+ t = num_frames if num_frames > temporal_factor else temporal_factor
771
resized_height, resized_width = smart_resize(
755
- t=t,
756
- h=image_height,
757
- w=image_width,
758
- t_factor=temporal_patch_size,
759
- h_factor=factor,
760
- w_factor=factor,
772
+ num_frames=t,
773
+ height=image_height,
774
+ width=image_width,
775
+ temporal_factor=temporal_factor,
776
+ factor=factor,
777
min_pixels=1,
762
- max_pixels=max_image_pixels,
778
+ max_pixels=max_image_tokens,
779
)
780
preprocessed_size = ImageSize(width=resized_width, height=resized_height)
781
else:
@@ -777,7 +793,7 @@ class Glm5NextProcessingInfo(Glm4vProcessingInfo):
793
794
795
class Glm5NextMultiModalProcessor(Glm4vMultiModalProcessor):
780
- """The vLLM-native ``Glm5NextProcessor`` extracts image/video features
796
+ """``Glm5NextProcessor`` extracts image/video features
797
only and passes the prompt text through unchanged, so prompt expansion
798
(image token repeat, video frame/timestamp structure) is owned by vLLM's
799
prompt-update machinery — the inherited ``_get_prompt_updates`` builds
vllm/models/qwen4_exp/amd/indexer_qsa.py
+1
-3
@@ -8,6 +8,7 @@ from typing import cast
8
9
import torch
10
from torch import nn
11
+from transformers import Qwen4ExpTextConfig
12
13
from vllm.config import VllmConfig
14
from vllm.forward_context import get_forward_context
@@ -15,9 +16,6 @@ from vllm.model_executor.layers.layernorm import GemmaRMSNorm
16
from vllm.model_executor.layers.linear import ReplicatedLinear
17
from vllm.model_executor.layers.quantization import QuantizationConfig
18
from vllm.model_executor.layers.rotary_embedding.mrope import triton_mrope
18
-from vllm.transformers_utils.configs.qwen4_exp import (
19
- Qwen4ExpTextConfig,
20
-)
19
20
from ..common.qsa_cache import (
21
QSACompressedKeyCache,
vllm/models/qwen4_exp/amd/model.py
+10
-37
@@ -7,6 +7,7 @@ from itertools import islice
7
8
import torch
9
from torch import nn
10
+from transformers import Qwen4ExpConfig, Qwen4ExpTextConfig
11
12
from vllm.compilation.decorators import support_torch_compile
13
from vllm.config import VllmConfig
@@ -47,7 +48,6 @@ from vllm.model_executor.models.qwen3_5 import (
48
)
49
from vllm.model_executor.models.qwen3_next import (
50
Qwen3NextAttention,
50
- Qwen3NextMLP,
51
Qwen3NextSparseMoeBlock,
52
)
53
from vllm.model_executor.models.qwen3_vl import (
@@ -71,13 +71,9 @@ from vllm.multimodal import MULTIMODAL_REGISTRY
71
from vllm.multimodal.inputs import MultiModalFeatureSpec
72
from vllm.sequence import IntermediateTensors
73
from vllm.tokenizers.registry import cached_tokenizer_from_config
74
-from vllm.transformers_utils.configs.qwen4_exp import (
75
- Qwen4ExpTextConfig,
76
-)
74
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
75
from vllm.v1.kv_cache_interface import MambaSpec
76
80
-from ..config import ATTENTION_LAYER_TYPES, QSA_LAYER_TYPE, Qwen4ExpConfig
77
from .hyperconnection import GatedResidual, HyperConnectionConfig
78
from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
79
from .ple_layer import Qwen4ExpPLELayer
@@ -179,8 +175,6 @@ class Qwen4ExpDecoderLayer(nn.Module):
175
) -> None:
176
super().__init__()
177
config: Qwen4ExpTextConfig = vllm_config.model_config.hf_text_config
182
- model_config = vllm_config.model_config
183
- cache_config = vllm_config.cache_config
178
quant_config = vllm_config.quant_config
179
180
self.config = config
@@ -213,16 +207,13 @@ class Qwen4ExpDecoderLayer(nn.Module):
207
prefix=f"{prefix}.linear_attn",
208
gqa_interleaved_layout=False,
209
)
216
- elif layer_type in ATTENTION_LAYER_TYPES:
217
- use_qsa = (
218
- layer_type == QSA_LAYER_TYPE
219
- or getattr(config, "indexer_n_heads", None) is not None
220
- )
210
+ elif layer_type == "qwen_sparse_attention":
211
+ use_qsa = getattr(config, "indexer_n_heads", None) is not None
212
if not use_qsa:
213
self.self_attn = Qwen3NextAttention(
214
config,
224
- model_config=model_config,
225
- cache_config=cache_config,
215
+ model_config=vllm_config.model_config,
216
+ cache_config=vllm_config.cache_config,
217
quant_config=quant_config,
218
prefix=f"{prefix}.self_attn",
219
)
@@ -237,24 +228,9 @@ class Qwen4ExpDecoderLayer(nn.Module):
228
else:
229
raise ValueError(f"Invalid layer_type {layer_type}")
230
240
- mlp_only_layers = getattr(config, "mlp_only_layers", [])
241
- num_experts = getattr(config, "num_experts", 0) or 0
242
- absolute_layer_id = self.layer_idx + 1
243
- is_moe_layer = self.layer_idx not in mlp_only_layers and (
244
- num_experts > 0 and absolute_layer_id % config.decoder_sparse_step == 0
231
+ self.mlp = Qwen4ExpSparseMoeBlock(
232
+ vllm_config=vllm_config, prefix=f"{prefix}.mlp"
233
)
246
- if is_moe_layer:
247
- self.mlp = Qwen4ExpSparseMoeBlock(
248
- vllm_config=vllm_config, prefix=f"{prefix}.mlp"
249
- )
250
- else:
251
- self.mlp = Qwen3NextMLP(
252
- hidden_size=config.hidden_size,
253
- intermediate_size=config.intermediate_size,
254
- hidden_act=config.hidden_act,
255
- quant_config=quant_config,
256
- prefix=f"{prefix}.mlp",
257
- )
234
235
hc_config = HyperConnectionConfig(
236
hc_count=config.hc_count,
@@ -313,7 +289,7 @@ class Qwen4ExpDecoderLayer(nn.Module):
289
290
if self.layer_type == "linear_attention":
291
attn_out = self.linear_attn(hidden_states=block_input)
316
- elif self.layer_type in ATTENTION_LAYER_TYPES:
292
+ elif self.layer_type == "qwen_sparse_attention":
293
attn_out = self.self_attn(
294
hidden_states=block_input,
295
positions=positions,
@@ -404,11 +380,8 @@ class Qwen4ExpModel(nn.Module):
380
self._qsa_layer_ids = frozenset(
381
layer_idx
382
for layer_idx, layer_type in enumerate(config.layer_types)
407
- if layer_type == QSA_LAYER_TYPE
408
- or (
409
- layer_type == "full_attention"
410
- and getattr(config, "indexer_n_heads", None) is not None
411
- )
383
+ if layer_type == "qwen_sparse_attention"
384
+ and getattr(config, "indexer_n_heads", None) is not None
385
)
386
self.embed_tokens = VocabParallelEmbedding(self.vocab_size, config.hidden_size)
387
vllm/models/qwen4_exp/amd/mtp.py
+2
-4
@@ -18,6 +18,7 @@ from collections.abc import Iterable
18
import regex as re
19
import torch
20
from torch import nn
21
+from transformers import Qwen4ExpTextConfig
22
23
from vllm.compilation.decorators import support_torch_compile
24
from vllm.config import VllmConfig, replace, set_current_vllm_config
@@ -45,9 +46,6 @@ from vllm.model_executor.models.utils import (
46
maybe_prefix,
47
)
48
from vllm.sequence import IntermediateTensors
48
-from vllm.transformers_utils.configs.qwen4_exp import (
49
- Qwen4ExpTextConfig,
50
-)
49
50
from .hyperconnection import GatedResidual, HyperConnectionConfig
51
from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
@@ -222,7 +220,7 @@ class Qwen4ExpMultiTokenPredictor(nn.Module):
220
self.layers = nn.ModuleList(
221
Qwen4ExpDecoderLayer(
222
draft_vllm_config,
225
- layer_type="full_attention",
223
+ layer_type="qwen_sparse_attention",
224
prefix=f"{prefix}.layers.{self.mtp_start_layer_idx + idx}",
225
)
226
for idx in range(self.num_mtp_layers)
vllm/models/qwen4_exp/amd/ple_layer.py
+1
-3
@@ -8,6 +8,7 @@ from collections.abc import Iterable, Sequence
8
import torch
9
import torch.nn.functional as F
10
from torch import nn
11
+from transformers import Qwen4ExpTextConfig
12
13
from vllm.config import CacheConfig, ModelConfig, VllmConfig, get_current_vllm_config
14
from vllm.forward_context import get_forward_context
@@ -23,9 +24,6 @@ from vllm.model_executor.layers.quantization.base_config import (
24
QuantizationConfig,
25
)
26
from vllm.model_executor.models.utils import AutoWeightsLoader
26
-from vllm.transformers_utils.configs.qwen4_exp import (
27
- Qwen4ExpTextConfig,
28
-)
27
from vllm.utils.torch_utils import direct_register_custom_op
28
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
29
from vllm.v1.attention.backends.short_conv_attn import (
vllm/models/qwen4_exp/amd/qsa.py
+1
-3
@@ -8,6 +8,7 @@ from typing import ClassVar, cast
8
9
import torch
10
from torch import nn
11
+from transformers import Qwen4ExpTextConfig
12
13
from vllm.config import VllmConfig
14
from vllm.config.cache import CacheDType
@@ -23,9 +24,6 @@ from vllm.model_executor.layers.quantization import QuantizationConfig
24
from vllm.model_executor.layers.rotary_embedding import get_rope
25
from vllm.model_executor.models.qwen3_next import Qwen3NextAttention
26
from vllm.platforms import current_platform
26
-from vllm.transformers_utils.configs.qwen4_exp import (
27
- Qwen4ExpTextConfig,
28
-)
27
from vllm.utils.torch_utils import (
28
LayerNameType,
29
_encode_layer_name,
vllm/models/qwen4_exp/config.py
deleted
-263
@@ -1,263 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Qwen4Exp model configuration."""
4
-
5
-from typing import Any, ClassVar, cast
6
-
7
-from transformers import PreTrainedConfig
8
-from transformers.models.qwen3_vl.configuration_qwen3_vl import (
9
- Qwen3VLVisionConfig,
10
-)
11
-
12
-from vllm.transformers_utils.configs.qwen3_next import Qwen3NextConfig
13
-
14
-_QSA_CONFIG_FIELDS = (
15
- "indexer_n_heads",
16
- "indexer_kv_heads",
17
- "indexer_head_dim",
18
- "indexer_budget",
19
- "indexer_compress_ratio",
20
-)
21
-
22
-# Transformers labels Qwen4Exp's sparse-attention layers `qwen_sparse_attention`.
23
-# Older checkpoints label them `full_attention` and mark QSA by setting the
24
-# indexer fields for the whole model.
25
-QSA_LAYER_TYPE = "qwen_sparse_attention"
26
-ATTENTION_LAYER_TYPES = ("full_attention", QSA_LAYER_TYPE)
27
-
28
-
29
-class Qwen4ExpVisionConfig(Qwen3VLVisionConfig):
30
- model_type = "qwen4_exp"
31
- base_config_key = "vision_config"
32
-
33
-
34
-class Qwen4ExpTextConfig(Qwen3NextConfig):
35
- model_type = "qwen4_exp_text"
36
- base_config_key = "text_config"
37
- keys_to_ignore_at_inference = ["past_key_values"]
38
-
39
- def __init__(
40
- self,
41
- hc_count: int = 4,
42
- hc_lowrank: int = 320,
43
- ple_layer_ids: list[int] | None = None,
44
- ple_embed_dim: int | None = None,
45
- ple_conv_kernel_size: int = 4,
46
- ngram_size: int = 3,
47
- heads_per_ngram: int = 8,
48
- ngram_vocab_size_base: int = 20_000_000,
49
- make_ngram_vocab_size_divisible_by: int = 128,
50
- output_gate_type: str = "sigmoid",
51
- rope_parameters: dict[str, Any] | None = None,
52
- layer_types: list[str] | None = None,
53
- **kwargs: Any,
54
- ) -> None:
55
- if hc_count <= 1:
56
- raise ValueError(f"Qwen4Exp requires hc_count > 1, got {hc_count}.")
57
-
58
- if rope_parameters is not None:
59
- if kwargs.get("rope_scaling") is None:
60
- kwargs["rope_scaling"] = rope_parameters
61
- if kwargs.get("rope_theta") is None and "rope_theta" in rope_parameters:
62
- kwargs["rope_theta"] = rope_parameters["rope_theta"]
63
- if (
64
- kwargs.get("partial_rotary_factor") is None
65
- and "partial_rotary_factor" in rope_parameters
66
- ):
67
- kwargs["partial_rotary_factor"] = rope_parameters[
68
- "partial_rotary_factor"
69
- ]
70
-
71
- rope_scaling = kwargs.get("rope_scaling")
72
- rope_theta = kwargs.get("rope_theta", 10_000.0)
73
- super().__init__(layer_types=layer_types, **kwargs)
74
-
75
- normalized_rope_parameters = self.rope_parameters
76
- self.rope_scaling = (
77
- rope_scaling or rope_parameters or normalized_rope_parameters
78
- )
79
- self.rope_parameters = rope_parameters or normalized_rope_parameters
80
- self.rope_theta = rope_theta
81
-
82
- self.hc_count = hc_count
83
- self.hc_lowrank = hc_lowrank
84
- self.ple_layer_ids = ple_layer_ids or []
85
- self.ple_embed_dim = (
86
- self.hidden_size if ple_embed_dim is None else ple_embed_dim
87
- )
88
- self.ple_conv_kernel_size = ple_conv_kernel_size
89
- self.ngram_size = ngram_size
90
- self.heads_per_ngram = heads_per_ngram
91
- self.ngram_vocab_size_base = ngram_vocab_size_base
92
- self.make_ngram_vocab_size_divisible_by = make_ngram_vocab_size_divisible_by
93
- self.output_gate_type = output_gate_type
94
-
95
- self._validate_ple_config()
96
- self._validate_ple_layer_ids()
97
- self._validate_qsa_config()
98
-
99
- def _validate_ple_config(self) -> None:
100
- if self.hc_lowrank <= 0:
101
- raise ValueError(f"hc_lowrank must be positive, got {self.hc_lowrank}")
102
- if self.ngram_size < 2:
103
- raise ValueError(f"ngram_size must be >= 2, got {self.ngram_size}")
104
- if self.heads_per_ngram <= 0:
105
- raise ValueError(
106
- f"heads_per_ngram must be positive, got {self.heads_per_ngram}"
107
- )
108
- if self.ple_embed_dim <= 0:
109
- raise ValueError(
110
- f"ple_embed_dim must be positive, got {self.ple_embed_dim}"
111
- )
112
- ngram_heads = (self.ngram_size - 1) * self.heads_per_ngram
113
- if self.ple_embed_dim % ngram_heads:
114
- raise ValueError(
115
- "ple_embed_dim must be divisible by total ngram heads: "
116
- f"{self.ple_embed_dim} % {ngram_heads} != 0"
117
- )
118
- if self.ple_conv_kernel_size <= 0:
119
- raise ValueError(
120
- "ple_conv_kernel_size must be positive, got "
121
- f"{self.ple_conv_kernel_size}"
122
- )
123
- if self.ngram_vocab_size_base <= 0:
124
- raise ValueError("ngram_vocab_size_base must be positive")
125
- if self.make_ngram_vocab_size_divisible_by <= 0:
126
- raise ValueError("make_ngram_vocab_size_divisible_by must be positive")
127
-
128
- def _validate_ple_layer_ids(self) -> None:
129
- invalid = [
130
- layer_id
131
- for layer_id in self.ple_layer_ids
132
- if not 1 <= int(layer_id) <= self.num_hidden_layers
133
- ]
134
- if invalid:
135
- raise ValueError(
136
- "ple_layer_ids are 1-based and must refer to an existing layer; "
137
- f"got {invalid} for {self.num_hidden_layers} layers"
138
- )
139
-
140
- def _validate_qsa_config(self) -> None:
141
- configured = {name: getattr(self, name, None) for name in _QSA_CONFIG_FIELDS}
142
- if all(value is None for value in configured.values()):
143
- if QSA_LAYER_TYPE in (self.layer_types or ()):
144
- raise ValueError(
145
- f"{QSA_LAYER_TYPE} layers require the QSA config fields: "
146
- f"{list(_QSA_CONFIG_FIELDS)}"
147
- )
148
- return
149
-
150
- missing = [name for name, value in configured.items() if value is None]
151
- if missing:
152
- raise ValueError(f"QSA config is missing required fields: {missing}")
153
-
154
- values = {name: int(cast(int, value)) for name, value in configured.items()}
155
- if any(value <= 0 for value in values.values()):
156
- raise ValueError(f"QSA config values must be positive: {values}")
157
- if values["indexer_kv_heads"] != 1:
158
- raise ValueError("the QSA MQA operators require indexer_kv_heads=1")
159
- if values["indexer_budget"] % values["indexer_compress_ratio"] != 0:
160
- raise ValueError(
161
- "indexer_budget must be divisible by indexer_compress_ratio"
162
- )
163
- block_topk = values["indexer_budget"] // values["indexer_compress_ratio"]
164
- if block_topk not in (512, 2048):
165
- raise ValueError(
166
- "QSA requires indexer_budget / indexer_compress_ratio "
167
- f"to be 512 or 2048, got {block_topk}"
168
- )
169
- rotary_dim = int(self.head_dim * self.partial_rotary_factor)
170
- if rotary_dim > values["indexer_head_dim"]:
171
- raise ValueError(
172
- "QSA indexer_head_dim must cover the attention rotary "
173
- f"dimension, got {values['indexer_head_dim']} < {rotary_dim}"
174
- )
175
-
176
- @property
177
- def layers_block_type(self) -> list[str]:
178
- return [
179
- "attention" if layer_type in ATTENTION_LAYER_TYPES else layer_type
180
- for layer_type in self.layer_types
181
- ]
182
-
183
- @property
184
- def short_conv_layer_ids(self) -> list[int]:
185
- if not self.ple_layer_ids:
186
- return []
187
- return sorted({int(layer_id) - 1 for layer_id in self.ple_layer_ids})
188
-
189
- @property
190
- def short_conv_state_shape(self) -> tuple[int, int] | None:
191
- if not self.short_conv_layer_ids:
192
- return None
193
- ple_state_len = (self.ple_conv_kernel_size - 1) * self.ngram_size
194
- ple_channels = self.hidden_size * self.hc_count
195
- return ple_channels, ple_state_len
196
-
197
- @property
198
- def ngram_context_len(self) -> int:
199
- if not self.ple_layer_ids:
200
- return 0
201
- return max(int(self.ngram_size) - 1, 0)
202
-
203
-
204
-class Qwen4ExpConfig(PreTrainedConfig):
205
- model_type = "qwen4_exp"
206
- sub_configs: ClassVar[dict[str, type[PreTrainedConfig]]] = {
207
- "vision_config": Qwen4ExpVisionConfig,
208
- "text_config": Qwen4ExpTextConfig,
209
- }
210
- keys_to_ignore_at_inference = ["past_key_values"]
211
-
212
- def __init__(
213
- self,
214
- text_config: Qwen4ExpTextConfig | dict[str, Any] | None = None,
215
- vision_config: Qwen4ExpVisionConfig | dict[str, Any] | None = None,
216
- image_token_id: int = 248056,
217
- video_token_id: int = 248057,
218
- vision_start_token_id: int = 248053,
219
- vision_end_token_id: int = 248054,
220
- tie_word_embeddings: bool = False,
221
- rope_parameters: dict[str, Any] | None = None,
222
- **kwargs: Any,
223
- ) -> None:
224
- if text_config is not None:
225
- kwargs.pop("split_ngram_parts", None)
226
-
227
- text_kwargs = (
228
- dict(kwargs)
229
- if text_config is None
230
- and "hidden_size" in kwargs
231
- and "num_hidden_layers" in kwargs
232
- else {}
233
- )
234
-
235
- if isinstance(vision_config, dict):
236
- self.vision_config = self.sub_configs["vision_config"](**vision_config)
237
- elif vision_config is None:
238
- self.vision_config = self.sub_configs["vision_config"]()
239
- else:
240
- self.vision_config = vision_config
241
-
242
- if isinstance(text_config, dict):
243
- self.text_config = self.sub_configs["text_config"](**text_config)
244
- elif text_config is None:
245
- self.text_config = self.sub_configs["text_config"](**text_kwargs)
246
- else:
247
- self.text_config = text_config
248
-
249
- self.image_token_id = image_token_id
250
- self.video_token_id = video_token_id
251
- self.vision_start_token_id = vision_start_token_id
252
- self.vision_end_token_id = vision_end_token_id
253
- self.rope_parameters = rope_parameters or getattr(
254
- self.text_config, "rope_parameters", {}
255
- )
256
- super().__init__(**kwargs, tie_word_embeddings=tie_word_embeddings)
257
-
258
-
259
-__all__ = [
260
- "Qwen4ExpConfig",
261
- "Qwen4ExpTextConfig",
262
- "Qwen4ExpVisionConfig",
263
-]
vllm/models/qwen4_exp/nvidia/indexer_qsa.py
+1
-3
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, cast
6
7
import torch
8
from torch import nn
9
+from transformers import Qwen4ExpTextConfig
10
11
from vllm.config import VllmConfig
12
from vllm.forward_context import get_forward_context
@@ -13,9 +14,6 @@ from vllm.model_executor.layers.layernorm import GemmaRMSNorm
14
from vllm.model_executor.layers.linear import ReplicatedLinear
15
from vllm.model_executor.layers.quantization import QuantizationConfig
16
from vllm.model_executor.layers.rotary_embedding.mrope import triton_mrope
16
-from vllm.transformers_utils.configs.qwen4_exp import (
17
- Qwen4ExpTextConfig,
18
-)
17
18
from ..common.qsa_cache import (
19
QSACompressedKeyCache,
vllm/models/qwen4_exp/nvidia/model.py
+10
-37
@@ -7,6 +7,7 @@ from itertools import islice
7
8
import torch
9
from torch import nn
10
+from transformers import Qwen4ExpConfig, Qwen4ExpTextConfig
11
12
from vllm.config import VllmConfig
13
from vllm.distributed import get_pp_group
@@ -46,7 +47,6 @@ from vllm.model_executor.models.qwen3_5 import (
47
)
48
from vllm.model_executor.models.qwen3_next import (
49
Qwen3NextAttention,
49
- Qwen3NextMLP,
50
Qwen3NextSparseMoeBlock,
51
)
52
from vllm.model_executor.models.qwen3_vl import (
@@ -70,13 +70,9 @@ from vllm.multimodal import MULTIMODAL_REGISTRY
70
from vllm.multimodal.inputs import MultiModalFeatureSpec
71
from vllm.sequence import IntermediateTensors
72
from vllm.tokenizers.registry import cached_tokenizer_from_config
73
-from vllm.transformers_utils.configs.qwen4_exp import (
74
- Qwen4ExpTextConfig,
75
-)
73
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
74
from vllm.v1.kv_cache_interface import MambaSpec
75
79
-from ..config import ATTENTION_LAYER_TYPES, QSA_LAYER_TYPE, Qwen4ExpConfig
76
from .hyperconnection import GatedResidual, HyperConnectionConfig
77
from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
78
from .ops.cute_dsl.hc_down_silu import request_hc_down_silu_warmup
@@ -181,8 +177,6 @@ class Qwen4ExpDecoderLayer(nn.Module):
177
) -> None:
178
super().__init__()
179
config: Qwen4ExpTextConfig = vllm_config.model_config.hf_text_config
184
- model_config = vllm_config.model_config
185
- cache_config = vllm_config.cache_config
180
quant_config = vllm_config.quant_config
181
182
self.config = config
@@ -215,16 +209,13 @@ class Qwen4ExpDecoderLayer(nn.Module):
209
prefix=f"{prefix}.linear_attn",
210
gqa_interleaved_layout=False,
211
)
218
- elif layer_type in ATTENTION_LAYER_TYPES:
219
- use_qsa = (
220
- layer_type == QSA_LAYER_TYPE
221
- or getattr(config, "indexer_n_heads", None) is not None
222
- )
212
+ elif layer_type == "qwen_sparse_attention":
213
+ use_qsa = getattr(config, "indexer_n_heads", None) is not None
214
if not use_qsa:
215
self.self_attn = Qwen3NextAttention(
216
config,
226
- model_config=model_config,
227
- cache_config=cache_config,
217
+ model_config=vllm_config.model_config,
218
+ cache_config=vllm_config.cache_config,
219
quant_config=quant_config,
220
prefix=f"{prefix}.self_attn",
221
)
@@ -239,24 +230,9 @@ class Qwen4ExpDecoderLayer(nn.Module):
230
else:
231
raise ValueError(f"Invalid layer_type {layer_type}")
232
242
- mlp_only_layers = getattr(config, "mlp_only_layers", [])
243
- num_experts = getattr(config, "num_experts", 0) or 0
244
- absolute_layer_id = self.layer_idx + 1
245
- is_moe_layer = self.layer_idx not in mlp_only_layers and (
246
- num_experts > 0 and absolute_layer_id % config.decoder_sparse_step == 0
233
+ self.mlp = Qwen4ExpSparseMoeBlock(
234
+ vllm_config=vllm_config, prefix=f"{prefix}.mlp"
235
)
248
- if is_moe_layer:
249
- self.mlp = Qwen4ExpSparseMoeBlock(
250
- vllm_config=vllm_config, prefix=f"{prefix}.mlp"
251
- )
252
- else:
253
- self.mlp = Qwen3NextMLP(
254
- hidden_size=config.hidden_size,
255
- intermediate_size=config.intermediate_size,
256
- hidden_act=config.hidden_act,
257
- quant_config=quant_config,
258
- prefix=f"{prefix}.mlp",
259
- )
236
237
hc_config = HyperConnectionConfig(
238
hc_count=config.hc_count,
@@ -317,7 +293,7 @@ class Qwen4ExpDecoderLayer(nn.Module):
293
294
if self.layer_type == "linear_attention":
295
attn_out = self.linear_attn(hidden_states=block_input)
320
- elif self.layer_type in ATTENTION_LAYER_TYPES:
296
+ elif self.layer_type == "qwen_sparse_attention":
297
attn_out = self.self_attn(
298
hidden_states=block_input,
299
positions=positions,
@@ -397,11 +373,8 @@ class Qwen4ExpModel(nn.Module):
373
self._qsa_layer_ids = frozenset(
374
layer_idx
375
for layer_idx, layer_type in enumerate(config.layer_types)
400
- if layer_type == QSA_LAYER_TYPE
401
- or (
402
- layer_type == "full_attention"
403
- and getattr(config, "indexer_n_heads", None) is not None
404
- )
376
+ if layer_type == "qwen_sparse_attention"
377
+ and getattr(config, "indexer_n_heads", None) is not None
378
)
379
self.embed_tokens = VocabParallelEmbedding(self.vocab_size, config.hidden_size)
380
vllm/models/qwen4_exp/nvidia/mtp.py
+2
-4
@@ -18,6 +18,7 @@ from collections.abc import Iterable
18
import regex as re
19
import torch
20
from torch import nn
21
+from transformers import Qwen4ExpTextConfig
22
23
from vllm.config import VllmConfig, replace, set_current_vllm_config
24
from vllm.distributed import get_pp_group
@@ -44,9 +45,6 @@ from vllm.model_executor.models.utils import (
45
maybe_prefix,
46
)
47
from vllm.sequence import IntermediateTensors
47
-from vllm.transformers_utils.configs.qwen4_exp import (
48
- Qwen4ExpTextConfig,
49
-)
48
49
from .hyperconnection import GatedResidual, HyperConnectionConfig
50
from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
@@ -212,7 +210,7 @@ class Qwen4ExpMultiTokenPredictor(nn.Module):
210
self.layers = nn.ModuleList(
211
Qwen4ExpDecoderLayer(
212
draft_vllm_config,
215
- layer_type="full_attention",
213
+ layer_type="qwen_sparse_attention",
214
prefix=f"{prefix}.layers.{self.mtp_start_layer_idx + idx}",
215
)
216
for idx in range(self.num_mtp_layers)
vllm/models/qwen4_exp/nvidia/ngram_embedding.py
+1
-3
@@ -6,6 +6,7 @@ from collections.abc import Iterable
6
7
import torch
8
from torch import nn
9
+from transformers import Qwen4ExpTextConfig
10
11
from vllm.config import get_current_vllm_config
12
from vllm.logger import init_logger
@@ -13,9 +14,6 @@ from vllm.model_executor.layers.quantization.base_config import (
14
QuantizationConfig,
15
)
16
from vllm.model_executor.models.utils import AutoWeightsLoader
16
-from vllm.transformers_utils.configs.qwen4_exp import (
17
- Qwen4ExpTextConfig,
18
-)
17
18
from ..common.ngram_embedding import (
19
Qwen4ExpPLEDeviceEmbedding,
vllm/models/qwen4_exp/nvidia/ple_layer.py
+1
-3
@@ -6,6 +6,7 @@ from collections.abc import Sequence
6
7
import torch
8
from torch import nn
9
+from transformers import Qwen4ExpTextConfig
10
11
from vllm.compilation.breakable_cudagraph import eager_break_during_capture
12
from vllm.config import CacheConfig, ModelConfig, VllmConfig, get_current_vllm_config
@@ -17,9 +18,6 @@ from vllm.model_executor.layers.mamba.mamba_utils import (
18
MambaStateShapeCalculator,
19
is_conv_state_dim_first,
20
)
20
-from vllm.transformers_utils.configs.qwen4_exp import (
21
- Qwen4ExpTextConfig,
22
-)
21
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
22
from vllm.v1.attention.backends.short_conv_attn import (
23
PleShortConvAttentionBackend,
vllm/models/qwen4_exp/nvidia/qsa.py
+1
-3
@@ -8,6 +8,7 @@ from typing import ClassVar, cast
8
9
import torch
10
from torch import nn
11
+from transformers import Qwen4ExpTextConfig
12
13
from vllm.compilation.breakable_cudagraph import eager_break_during_capture
14
from vllm.config import VllmConfig
@@ -25,9 +26,6 @@ from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding, get_ro
26
from vllm.model_executor.models.qwen3_next import Qwen3NextAttention
27
from vllm.platforms import current_platform
28
from vllm.platforms.interface import DeviceCapability
28
-from vllm.transformers_utils.configs.qwen4_exp import (
29
- Qwen4ExpTextConfig,
30
-)
29
from vllm.utils.torch_utils import (
30
kv_cache_dtype_str_to_dtype,
31
)
vllm/multimodal/video.py
+83
-12
@@ -714,15 +714,92 @@ class GLM46VVideoBackend(VideoBackend):
714
class Glm5NextVideoBackend(VideoBackend):
715
"""GLM-5.3-Flash fps-interval video backend.
716
717
- Selects frames with the same ``glm_sample_frame_indices`` sampler the
718
- processor falls back to, so only the sampled frames are
719
- materialized. ``fps_interval`` semantics (default 2.0) with a
720
- temporal-patch-scaled greedy walk, frame count capped at 2048, temporal
721
- pairs kept even. Request overrides: ``fps`` -> fps interval,
717
+ Selects frames with the same sampler the processor falls back to, so only
718
+ the sampled frames are materialized. ``fps_interval`` semantics (default
719
+ 2.0) with a temporal-patch-scaled greedy walk, frame count capped at 2048,
720
+ temporal pairs kept even. Request overrides: ``fps`` -> fps interval,
721
``max_frames`` -> frame cap, ``temporal_patch_size`` (default 2).
722
"""
723
724
_SEEK_GAP_THRESHOLD: ClassVar[int] = 64
725
+ _DEFAULT_FPS: ClassVar[float] = 2.0
726
+ _DEFAULT_MAX_FRAMES: ClassVar[int] = 2048
727
+
728
+ @classmethod
729
+ def _sample_frame_indices(
730
+ cls,
731
+ total_frames: int,
732
+ fps: float,
733
+ duration: float,
734
+ *,
735
+ target_fps: float | None = None,
736
+ max_frame_count: int | None = None,
737
+ temporal_patch_size: int = 2,
738
+ ) -> list[int]:
739
+ """GLM video frame sampling (training-reference parity).
740
+
741
+ ``target_fps`` is the ``fps_interval`` request knob. The greedy walk
742
+ advances at ``1 / (temporal_patch_size * target_fps)`` seconds, so on
743
+ frame-dense sources it collects more candidates than ``extract_t`` and
744
+ the ``> extract_t`` fixup re-spreads the picks uniformly with
745
+ ``np.linspace`` -- that fallback is the intended reference behavior, not
746
+ an accident. Short clips (fewer frames than ``extract_t``) are spread at
747
+ evenly spaced timestamps (``floor`` sampling; the linspace variant
748
+ samples frames unevenly and cost 4 points on video grounding evals).
749
+ Request overrides: ``target_fps`` -> fps interval, ``max_frame_count``
750
+ -> frame cap.
751
+ """
752
+ max_frame_idx = total_frames - 1
753
+ if not duration:
754
+ duration = (round(max_frame_idx / fps) + 1) if fps else 0
755
+ if max_frame_count is None:
756
+ max_frame_count = cls._DEFAULT_MAX_FRAMES
757
+ if target_fps is None:
758
+ target_fps = cls._DEFAULT_FPS
759
+
760
+ extract_t = int(duration * target_fps)
761
+ extract_t = min(extract_t, int(max_frame_count))
762
+
763
+ duration_per_frame = 1 / fps
764
+ timestamps = [i * duration_per_frame for i in range(total_frames)]
765
+ max_second = int(duration)
766
+
767
+ if total_frames < extract_t:
768
+ frame_indices = [
769
+ math.floor(_i * total_frames / extract_t) for _i in range(extract_t)
770
+ ]
771
+ else:
772
+ frame_indices = []
773
+ current_second = 0.0
774
+ inv_fps = 1 / (temporal_patch_size * target_fps)
775
+ for frame_index in range(total_frames):
776
+ if timestamps[frame_index] >= current_second:
777
+ current_second += inv_fps
778
+ frame_indices.append(frame_index)
779
+ if current_second >= max_second:
780
+ break
781
+
782
+ if len(frame_indices) < extract_t:
783
+ if len(frame_indices) == 0:
784
+ start, end = 0, max(total_frames - 1, 0)
785
+ else:
786
+ start, end = frame_indices[0], frame_indices[-1]
787
+ frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
788
+ elif len(frame_indices) > extract_t:
789
+ frame_indices = np.linspace(
790
+ 0, total_frames - 1, extract_t, dtype=int
791
+ ).tolist()
792
+
793
+ seen, uniq = set(), []
794
+ for idx in frame_indices:
795
+ if idx not in seen:
796
+ seen.add(idx)
797
+ uniq.append(int(idx))
798
+
799
+ if len(uniq) & 1:
800
+ uniq.append(uniq[-1])
801
+
802
+ return uniq
803
804
@classmethod
805
def compute_frames_index_to_sample(
@@ -731,13 +808,7 @@ class Glm5NextVideoBackend(VideoBackend):
808
target: VideoTargetMetadata,
809
**kwargs,
810
) -> list[int]:
734
- # Lazy import: the processor module sits behind the
735
- # transformers_utils package init, which multimodal must not pull in.
736
- from vllm.transformers_utils.processors.glm5next import (
737
- glm_sample_frame_indices,
738
- )
739
-
740
- return glm_sample_frame_indices(
811
+ return cls._sample_frame_indices(
812
source.total_frames_num,
813
source.original_fps,
814
source.duration or 0,
vllm/transformers_utils/config.py
-5
@@ -107,9 +107,6 @@ _CONFIG_REGISTRY: dict[str, type[PreTrainedConfig]] = LazyConfigDict(
107
k3_dspark="K3DSparkConfig",
108
funaudiochat="FunAudioChatConfig",
109
granite4_vision="Granite4VisionConfig",
110
- glm5_next="Glm5NextConfig",
111
- glm5_next_text="Glm5NextTextConfig",
112
- glm5_next_vision="Glm5NextVisionConfig",
110
hyperclovax="HyperCLOVAXConfig",
111
hy_v3="HYV3Config",
112
hy_v4="HYV4Config",
@@ -145,8 +142,6 @@ _CONFIG_REGISTRY: dict[str, type[PreTrainedConfig]] = LazyConfigDict(
142
qianfan_ocr="QianfanOCRConfig",
143
qwen3_asr="Qwen3ASRConfig",
144
qwen3_next="Qwen3NextConfig",
148
- qwen4_exp="Qwen4ExpConfig",
149
- qwen4_exp_text="Qwen4ExpTextConfig",
145
qwen3_5="Qwen3_5Config",
146
qwen3_5_text="Qwen3_5TextConfig",
147
qwen3_5_moe="Qwen3_5MoeConfig",
vllm/transformers_utils/configs/__init__.py
-12
@@ -43,9 +43,6 @@ _CLASS_TO_MODULE: dict[str, str] = {
43
"FunAudioChatConfig": "vllm.transformers_utils.configs.funaudiochat",
44
"FunAudioChatAudioEncoderConfig": "vllm.transformers_utils.configs.funaudiochat",
45
"Granite4VisionConfig": "vllm.transformers_utils.configs.granite4_vision",
46
- "Glm5NextConfig": "vllm.transformers_utils.configs.glm5_next",
47
- "Glm5NextTextConfig": "vllm.transformers_utils.configs.glm5_next",
48
- "Glm5NextVisionConfig": "vllm.transformers_utils.configs.glm5_next",
46
"HYV3Config": "vllm.transformers_utils.configs.hy_v3",
47
"HYV4Config": "vllm.transformers_utils.configs.hy_v4",
48
"HyperCLOVAXConfig": "vllm.transformers_utils.configs.hyperclovax",
@@ -95,9 +92,6 @@ _CLASS_TO_MODULE: dict[str, str] = {
92
"QianfanOCRVisionConfig": "vllm.transformers_utils.configs.qianfan_ocr",
93
"Qwen3ASRConfig": "vllm.transformers_utils.configs.qwen3_asr",
94
"Qwen3NextConfig": "vllm.transformers_utils.configs.qwen3_next",
98
- "Qwen4ExpConfig": "vllm.transformers_utils.configs.qwen4_exp",
99
- "Qwen4ExpTextConfig": "vllm.transformers_utils.configs.qwen4_exp",
100
- "Qwen4ExpVisionConfig": "vllm.transformers_utils.configs.qwen4_exp",
95
"Qwen3_5Config": "vllm.transformers_utils.configs.qwen3_5",
96
"Qwen3_5TextConfig": "vllm.transformers_utils.configs.qwen3_5",
97
"Qwen3_5MoeConfig": "vllm.transformers_utils.configs.qwen3_5_moe",
@@ -140,9 +134,6 @@ __all__ = [
134
"FunAudioChatConfig",
135
"FunAudioChatAudioEncoderConfig",
136
"Granite4VisionConfig",
143
- "Glm5NextConfig",
144
- "Glm5NextTextConfig",
145
- "Glm5NextVisionConfig",
137
"HYV3Config",
138
"HYV4Config",
139
"HyperCLOVAXConfig",
@@ -187,9 +178,6 @@ __all__ = [
178
"QianfanOCRVisionConfig",
179
"Qwen3ASRConfig",
180
"Qwen3NextConfig",
190
- "Qwen4ExpConfig",
191
- "Qwen4ExpTextConfig",
192
- "Qwen4ExpVisionConfig",
181
"Qwen3_5Config",
182
"Qwen3_5TextConfig",
183
"Qwen3_5MoeConfig",
vllm/transformers_utils/configs/glm5_next.py
deleted
-420
@@ -1,420 +0,0 @@
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
-
6
-from vllm.logger import init_logger
7
-
8
-logger = init_logger(__name__)
9
-
10
-
11
-class Glm5NextTextConfig(PreTrainedConfig):
12
- model_type = "glm5_next_text"
13
- base_config_key = "text_config"
14
- keys_to_ignore_at_inference = ["past_key_values"]
15
-
16
- def __init__(
17
- self,
18
- model_type="glm5_next_text",
19
- vocab_size: int = 154880,
20
- hidden_size: int = 4096,
21
- head_dim: int | None = None,
22
- intermediate_size: int = 12288,
23
- num_hidden_layers: int = 45,
24
- num_attention_heads: int = 64,
25
- num_key_value_heads: int | None = None,
26
- hidden_act: str = "silu",
27
- rms_norm_eps: float = 1e-5,
28
- pad_token_id: int | None = 151329,
29
- bos_token_id: int | None = None,
30
- eos_token_id: int | list[int] | None = None,
31
- rope_parameters: dict | None = None,
32
- max_position_embeddings: int = 1048576,
33
- tie_word_embeddings: bool = False,
34
- moe_intermediate_size: int = 2048,
35
- moe_renormalize: bool = True,
36
- scoring_func: str = "sigmoid",
37
- n_routed_experts: int | None = 288,
38
- num_experts_per_token: int = 7,
39
- n_shared_experts: int = 1,
40
- routed_scaling_factor: float = 2.5,
41
- topk_method: str | None = None,
42
- first_k_dense_replace: int = 0,
43
- moe_layer_freq: int = 1,
44
- use_grouped_topk: bool = True,
45
- n_group: int = 1,
46
- topk_group: int = 1,
47
- mla: bool = True,
48
- q_lora_rank: int | None = 1536,
49
- kv_lora_rank: int | None = 512,
50
- qk_nope_head_dim: int = 256,
51
- qk_rope_head_dim: int = 0,
52
- v_head_dim: int | None = 256,
53
- mla_nope: bool | None = True,
54
- num_nextn_predict_layers: int = 1,
55
- # Per-layer layout: "linear_attention" | "deepseek_sparse_attention"
56
- layer_types: list[str] | None = None,
57
- # Per-layer MLP: "dense" | "sparse"
58
- mlp_layer_types: list[str] | None = None,
59
- # Linear-attention (KDA) head config (flattened from the old
60
- # linear_attn_config dict).
61
- linear_head_dim: int = 128,
62
- linear_num_heads: int = 64,
63
- linear_conv_kernel_dim: int = 4,
64
- linear_lower_bound: float = -5.0,
65
- index_head_dim: int | None = None,
66
- index_topk: int | None = None,
67
- index_n_heads: int | None = None,
68
- index_dsa_use_layernorm: bool = True,
69
- index_kpool_compress: bool = True,
70
- # Every ``index_kpool`` indexer K entries pool into one stored entry
71
- # (compress_ratio). The 300B checkpoint ships 4; topk runs at pool
72
- # granularity (select_k = index_topk // index_kpool).
73
- index_kpool: int | None = 4,
74
- index_kpool_always_select_tail: bool = True,
75
- indexer_rope_interleave: bool = False,
76
- mhc: bool | None = True,
77
- mhc_num_residual_streams: int = 4,
78
- hc_eps: float | None = 1e-06,
79
- mhc_tau: float = 0.05,
80
- hres_vwnstyle: bool | None = True,
81
- mhc_no_norm_weight: bool | None = False,
82
- mhc_sinkhorn_iterations: int | None = 20,
83
- mhc_post_mult_value: float | None = 2.0,
84
- swiglu_limit: float | None = None,
85
- logit_scale: float = 1.0,
86
- **kwargs,
87
- ):
88
- # Preserve checkpoint field names and local aliases because their
89
- # consumers use different spellings.
90
- num_experts_per_token = kwargs.get("num_experts_per_tok", num_experts_per_token)
91
- moe_renormalize = kwargs.get("norm_topk_prob", moe_renormalize)
92
- mhc_num_residual_streams = kwargs.get("hc_mult", mhc_num_residual_streams)
93
- mhc_sinkhorn_iterations = kwargs.get(
94
- "hc_sinkhorn_iters", mhc_sinkhorn_iterations
95
- )
96
- # Checkpoint ships ``mla_use_nope`` (not ``mla_nope``); without this
97
- # alias self.mla_nope silently stays at the param default.
98
- mla_nope = kwargs.get("mla_use_nope", mla_nope)
99
- # Checkpoints ship the KDA head config as the ``linear_attn_config``
100
- # dict (head_dim / num_heads / short_conv_kernel_size /
101
- # gate_lower_bound) rather than the flattened top-level fields; fold it
102
- # in so the trained values are read instead of the param defaults
103
- # (which only match this checkpoint by coincidence).
104
- linear_cfg = kwargs.get("linear_attn_config") or {}
105
- if linear_cfg:
106
- linear_head_dim = linear_cfg.get("head_dim", linear_head_dim)
107
- linear_num_heads = linear_cfg.get("num_heads", linear_num_heads)
108
- linear_conv_kernel_dim = linear_cfg.get(
109
- "short_conv_kernel_size", linear_conv_kernel_dim
110
- )
111
- linear_lower_bound = linear_cfg.get("gate_lower_bound", linear_lower_bound)
112
-
113
- if index_topk is not None:
114
- if index_dsa_use_layernorm is not True:
115
- raise NotImplementedError(
116
- "GLM-5.3 sparse indexer requires index_dsa_use_layernorm=True"
117
- )
118
- if index_kpool_compress is not True:
119
- raise NotImplementedError(
120
- "GLM-5.3 sparse indexer requires index_kpool_compress=True"
121
- )
122
- if index_kpool_always_select_tail is not True:
123
- raise NotImplementedError(
124
- "GLM-5.3 sparse indexer requires "
125
- "index_kpool_always_select_tail=True"
126
- )
127
-
128
- if mhc:
129
- if hres_vwnstyle is not True:
130
- raise NotImplementedError("GLM-5.3 mHC requires hres_vwnstyle=True")
131
- if mhc_no_norm_weight not in (False, None):
132
- raise NotImplementedError(
133
- "GLM-5.3 mHC requires mhc_no_norm_weight=False"
134
- )
135
-
136
- self.model_type = model_type
137
- self.vocab_size = vocab_size
138
- self.hidden_size = hidden_size
139
- self.head_dim = (
140
- head_dim if head_dim is not None else hidden_size // num_attention_heads
141
- )
142
- self.intermediate_size = intermediate_size
143
- self.num_hidden_layers = num_hidden_layers
144
- self.num_attention_heads = num_attention_heads
145
-
146
- # for backward compatibility
147
- if num_key_value_heads is None:
148
- num_key_value_heads = num_attention_heads
149
-
150
- self.num_key_value_heads = num_key_value_heads
151
- self.hidden_act = hidden_act
152
- self.rms_norm_eps = rms_norm_eps
153
- self.max_position_embeddings = max_position_embeddings
154
- self.rope_parameters = rope_parameters
155
-
156
- # mla config
157
- self.mla = mla
158
- self.q_lora_rank = q_lora_rank
159
- self.kv_lora_rank = kv_lora_rank
160
- self.qk_nope_head_dim = qk_nope_head_dim
161
- self.qk_rope_head_dim = qk_rope_head_dim
162
- self.v_head_dim = v_head_dim
163
- self.mla_nope = mla_nope
164
- # moe config
165
- self.n_routed_experts = n_routed_experts
166
- self.num_experts_per_token = num_experts_per_token
167
- self.moe_renormalize = moe_renormalize
168
- self.n_shared_experts = n_shared_experts
169
- self.routed_scaling_factor = routed_scaling_factor
170
- self.topk_method = topk_method
171
- self.scoring_func = scoring_func
172
- assert self.scoring_func in ("softmax", "sigmoid")
173
- self.moe_intermediate_size = moe_intermediate_size
174
- self.first_k_dense_replace = first_k_dense_replace
175
- self.moe_layer_freq = moe_layer_freq
176
- self.use_grouped_topk = use_grouped_topk
177
- self.n_group = n_group
178
- self.topk_group = topk_group
179
- self.num_nextn_predict_layers = num_nextn_predict_layers
180
-
181
- # Per-layer attention / MLP layout. Normalize mlp_layer_types from
182
- # first_k_dense_replace when the new-schema field is absent so layer
183
- # construction sees a consistent layout (mirrors cohere2_moe).
184
- self.layer_types = layer_types
185
- if mlp_layer_types is None:
186
- n = self.num_hidden_layers
187
- if first_k_dense_replace is not None:
188
- mlp_layer_types = ["dense"] * first_k_dense_replace + ["sparse"] * (
189
- n - first_k_dense_replace
190
- )
191
- else:
192
- mlp_layer_types = ["sparse"] * n
193
- self.mlp_layer_types = mlp_layer_types
194
-
195
- # Linear-attention (KDA) head config.
196
- self.linear_head_dim = linear_head_dim
197
- self.linear_num_heads = linear_num_heads
198
- self.linear_conv_kernel_dim = linear_conv_kernel_dim
199
- self.linear_lower_bound = linear_lower_bound
200
-
201
- # dsa index config
202
- self.index_head_dim = index_head_dim
203
- self.index_topk = index_topk
204
- self.index_n_heads = index_n_heads
205
- self.index_dsa_use_layernorm = index_dsa_use_layernorm
206
- self.index_kpool_compress = index_kpool_compress
207
- self.index_kpool = index_kpool
208
- self.index_kpool_always_select_tail = index_kpool_always_select_tail
209
- self.indexer_rope_interleave = indexer_rope_interleave
210
-
211
- # mhc config
212
- self.mhc = mhc
213
- self.mhc_num_residual_streams = mhc_num_residual_streams
214
- self.mhc_tau = mhc_tau
215
- self.hres_vwnstyle = hres_vwnstyle
216
- self.hc_eps = hc_eps
217
- self.mhc_no_norm_weight = mhc_no_norm_weight
218
- self.mhc_sinkhorn_iterations = mhc_sinkhorn_iterations
219
- self.mhc_post_mult_value = mhc_post_mult_value
220
-
221
- self.swiglu_limit = swiglu_limit
222
- self.logit_scale = logit_scale
223
-
224
- super().__init__(
225
- pad_token_id=pad_token_id,
226
- bos_token_id=bos_token_id,
227
- eos_token_id=eos_token_id,
228
- tie_word_embeddings=tie_word_embeddings,
229
- **kwargs,
230
- )
231
-
232
- @property
233
- def is_mla(self):
234
- return (
235
- self.q_lora_rank is not None
236
- or self.kv_lora_rank is not None
237
- or self.qk_nope_head_dim is not None
238
- or self.qk_rope_head_dim is not None
239
- or self.v_head_dim is not None
240
- or self.mla_nope is True
241
- )
242
-
243
- @property
244
- def is_moe(self):
245
- return self.n_routed_experts is not None
246
-
247
- @property
248
- def is_linear_attn(self) -> bool:
249
- return self.layer_types is not None and any(
250
- t == "linear_attention" for t in self.layer_types
251
- )
252
-
253
- def is_kda_layer(self, layer_idx: int):
254
- return (
255
- self.layer_types is not None
256
- and layer_idx < len(self.layer_types)
257
- and self.layer_types[layer_idx] == "linear_attention"
258
- )
259
-
260
- @property
261
- def layers_block_type(self):
262
- # Map the schema's per-layer types onto the block strings vLLM's hybrid
263
- # accounting (get_num_layers_by_block_type) recognizes: linear-attention
264
- # layers stay "linear_attention"; every other attention variant collapses
265
- # to "attention".
266
- if self.layer_types is None:
267
- return ["attention"] * self.num_hidden_layers
268
- return [
269
- "linear_attention" if t == "linear_attention" else "attention"
270
- for t in self.layer_types
271
- ]
272
-
273
-
274
-class Glm5NextVisionConfig(PreTrainedConfig):
275
- model_type = "glm5_next_vision"
276
- base_config_key = "vision_config"
277
-
278
- def __init__(
279
- self,
280
- depth: int = 24,
281
- hidden_size: int = 1024,
282
- hidden_act: str = "silu",
283
- image_size: int = 448,
284
- intermediate_size: int = 4096,
285
- num_heads: int = 16,
286
- out_hidden_size: int = 4096,
287
- projection_intermediate_size: int = 10240,
288
- in_channels: int = 3,
289
- initializer_range: float = 0.02,
290
- patch_size: int = 14,
291
- rms_norm_eps: float = 1e-5,
292
- spatial_merge_size: int = 2,
293
- temporal_patch_size: int = 2,
294
- attention_dropout: float = 0.0,
295
- attention_bias: bool = True,
296
- swiglu_limit: float | None = None,
297
- **kwargs,
298
- ):
299
- super().__init__(**kwargs)
300
-
301
- self.depth = depth
302
- self.hidden_size = hidden_size
303
- self.hidden_act = hidden_act
304
- self.image_size = image_size
305
- self.intermediate_size = intermediate_size
306
- self.num_heads = num_heads
307
- self.out_hidden_size = out_hidden_size
308
- # GLM-5.3-Flash merger bottleneck width (absent from the generic
309
- # GLM-OCR vision config); the tower uses it as the PatchMerger
310
- # context_dim instead of text_config.intermediate_size.
311
- self.projection_intermediate_size = projection_intermediate_size
312
- self.in_channels = in_channels
313
- self.initializer_range = initializer_range
314
- self.patch_size = patch_size
315
- # GLM-5.3-Flash checkpoints ship vision_config.rms_norm_eps = 1e-5,
316
- # but the vision tower was trained with 1e-6. Serving with 1e-5 drifts
317
- # the RMSNorm and produces repetitive/degraded image descriptions, so
318
- # force the trained value regardless of the checkpoint field.
319
- self.rms_norm_eps = 1e-6
320
- self.spatial_merge_size = spatial_merge_size
321
- self.temporal_patch_size = temporal_patch_size
322
- self.attention_dropout = attention_dropout
323
- self.attention_bias = attention_bias
324
- self.swiglu_limit = swiglu_limit
325
-
326
-
327
-class Glm5NextConfig(PreTrainedConfig):
328
- model_type = "glm5_next"
329
- sub_configs = {
330
- "vision_config": Glm5NextVisionConfig,
331
- "text_config": Glm5NextTextConfig,
332
- }
333
- keys_to_ignore_at_inference = ["past_key_values"]
334
-
335
- def __init__(
336
- self,
337
- text_config=None,
338
- vision_config=None,
339
- image_token_id: int = 154854,
340
- video_token_id: int = 154855,
341
- image_start_token_id: int = 154830,
342
- image_end_token_id: int = 154831,
343
- video_start_token_id: int = 154832,
344
- video_end_token_id: int = 154833,
345
- **kwargs,
346
- ):
347
- # Init super() first so base-class defaults don't clobber text-config
348
- # values set below (PreTrainedConfig has many text-related defaults
349
- # that differ from Glm5NextTextConfig).
350
- super().__init__(**kwargs)
351
-
352
- if isinstance(vision_config, dict):
353
- self.vision_config = self.sub_configs["vision_config"](**vision_config)
354
- elif vision_config is None:
355
- self.vision_config = self.sub_configs["vision_config"]()
356
- else:
357
- self.vision_config = vision_config
358
-
359
- if isinstance(text_config, dict):
360
- self.text_config = self.sub_configs["text_config"](**text_config)
361
- elif text_config is None:
362
- # Backward compatibility: a flat top-level checkpoint (no nested
363
- # text_config) folds its text fields into Glm5NextTextConfig.
364
- self.text_config = self.sub_configs["text_config"](**kwargs)
365
- else:
366
- self.text_config = text_config
367
-
368
- self.image_token_id = image_token_id
369
- self.video_token_id = video_token_id
370
- self.image_start_token_id = image_start_token_id
371
- self.image_end_token_id = image_end_token_id
372
- self.video_start_token_id = video_start_token_id
373
- self.video_end_token_id = video_end_token_id
374
-
375
- # Mirror attention implementation recursively onto sub-configs.
376
- self._attn_implementation = kwargs.pop("attn_implementation", None)
377
-
378
- # Config-metadata fields that belong to the top-level (multimodal) config
379
- # and must NOT be mirrored onto text_config: ``architectures`` /
380
- # ``torch_dtype`` differ between the top-level config and the text
381
- # sub-config, and mirroring them makes the top-level ``architectures``
382
- # silently read back as None (PreTrainedConfig initializes both to None),
383
- # which then fails model-class resolution ("No model architectures are
384
- # specified").
385
- _UNMIRRORED_KEYS = [
386
- "_name_or_path",
387
- "model_type",
388
- "dtype",
389
- "torch_dtype",
390
- "architectures",
391
- "_attn_implementation_internal",
392
- ]
393
-
394
- def __setattr__(self, key, value):
395
- unmirrored = type(self)._UNMIRRORED_KEYS
396
- if (
397
- (text_config := super().__getattribute__("__dict__").get("text_config"))
398
- is not None
399
- and key not in unmirrored
400
- and key in text_config.__dict__
401
- ):
402
- setattr(text_config, key, value)
403
- else:
404
- super().__setattr__(key, value)
405
-
406
- def __getattribute__(self, key):
407
- unmirrored = type(self)._UNMIRRORED_KEYS
408
- if (
409
- "text_config" in super().__getattribute__("__dict__")
410
- and key not in unmirrored
411
- ):
412
- text_config = super().__getattribute__("text_config")
413
- # Forward both instance attributes AND class-defined properties/
414
- # methods of the text config, so a flat text-only checkpoint
415
- # (model_type "glm5_next", no nested text_config) sees is_moe /
416
- # is_kda_layer / layers_block_type like a Glm5NextTextConfig.
417
- if key in text_config.__dict__ or key in type(text_config).__dict__:
418
- return getattr(text_config, key)
419
-
420
- return super().__getattribute__(key)
vllm/transformers_utils/configs/qwen4_exp.py
deleted
-15
@@ -1,15 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Exports for the package-owned Qwen4Exp configs."""
4
-
5
-from vllm.models.qwen4_exp.config import (
6
- Qwen4ExpConfig,
7
- Qwen4ExpTextConfig,
8
- Qwen4ExpVisionConfig,
9
-)
10
-
11
-__all__ = [
12
- "Qwen4ExpConfig",
13
- "Qwen4ExpTextConfig",
14
- "Qwen4ExpVisionConfig",
15
-]
vllm/transformers_utils/processors/__init__.py
-2
@@ -17,7 +17,6 @@ __all__ = [
17
"FireRedASR2Processor",
18
"FunASRProcessor",
19
"GLM4VProcessor",
20
- "Glm5NextProcessor",
20
"Granite4VisionProcessor",
21
"H2OVLProcessor",
22
"Moondream3Processor",
@@ -56,7 +55,6 @@ _CLASS_TO_MODULE: dict[str, str] = {
55
"FireRedASR2Processor": "vllm.transformers_utils.processors.fireredasr2",
56
"FunASRProcessor": "vllm.transformers_utils.processors.funasr",
57
"GLM4VProcessor": "vllm.transformers_utils.processors.glm4v",
59
- "Glm5NextProcessor": "vllm.transformers_utils.processors.glm5next",
58
"Granite4VisionProcessor": "vllm.transformers_utils.processors.granite4_vision",
59
"H2OVLProcessor": "vllm.transformers_utils.processors.h2ovl",
60
"InternVLProcessor": "vllm.transformers_utils.processors.internvl",
vllm/transformers_utils/processors/glm5next.py
deleted
-953
@@ -1,953 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""vLLM-native multimodal processor for GLM-5.3-Flash."""
4
-
5
-import math
6
-
7
-import numpy as np
8
-import torch
9
-from torchvision.transforms.v2 import functional as tvF
10
-from transformers.image_processing_backends import (
11
- TorchvisionBackend,
12
- group_images_by_shape,
13
- reorder_images,
14
-)
15
-from transformers.image_processing_utils import BatchFeature
16
-from transformers.image_utils import (
17
- OPENAI_CLIP_MEAN,
18
- OPENAI_CLIP_STD,
19
- ChannelDimension,
20
- ImageInput,
21
- PILImageResampling,
22
- SizeDict,
23
- get_image_size,
24
-)
25
-from transformers.models.auto.image_processing_auto import get_image_processor_config
26
-from transformers.processing_utils import (
27
- ImagesKwargs,
28
- MultiModalData,
29
- ProcessingKwargs,
30
- ProcessorMixin,
31
- Unpack,
32
- VideosKwargs,
33
-)
34
-from transformers.tokenization_utils_base import PreTokenizedInput, TextInput
35
-from transformers.utils import TensorType, logging
36
-from transformers.video_processing_utils import BaseVideoProcessor
37
-from transformers.video_utils import (
38
- VideoInput,
39
- VideoMetadata,
40
- group_videos_by_shape,
41
- reorder_videos,
42
-)
43
-
44
-from vllm.transformers_utils.repo_utils import get_hf_file_to_dict
45
-
46
-logger = logging.get_logger(__name__)
47
-
48
-# Cap video inputs at 30,000 vision tokens to keep encoder profiling from
49
-# starving the KV cache. Image inputs retain their checkpoint-defined budget.
50
-_MAX_VIDEO_TOKENS = 30000
51
-
52
-# Frame-sampler fallbacks (mirror the checkpoint's fps_interval=2 /
53
-# max_frame_count_dynamic=2048); used when neither the request nor the
54
-# processor config overrides them.
55
-GLM_VIDEO_DEFAULT_FPS = 2.0
56
-GLM_VIDEO_DEFAULT_MAX_FRAMES = 2048
57
-
58
-
59
-def glm_sample_frame_indices(
60
- total_frames: int,
61
- fps: float,
62
- duration: float,
63
- *,
64
- target_fps: float | None = None,
65
- max_frame_count: int | None = None,
66
- temporal_patch_size: int = 2,
67
-) -> list[int]:
68
- """GLM video frame sampling (training-reference parity).
69
-
70
- ``target_fps`` is the ``fps_interval`` request knob. The greedy walk
71
- advances at ``1 / (temporal_patch_size * target_fps)`` seconds, so on
72
- frame-dense sources it collects more candidates than ``extract_t`` and
73
- the ``> extract_t`` fixup re-spreads the picks uniformly with
74
- ``np.linspace`` -- that fallback is the intended reference behavior, not
75
- an accident. Short clips (fewer frames than ``extract_t``) are spread at
76
- evenly spaced timestamps (``floor`` sampling; the linspace variant
77
- samples frames unevenly and cost 4 points on video grounding evals).
78
- Request overrides: ``target_fps`` -> fps interval, ``max_frame_count``
79
- -> frame cap.
80
- """
81
- max_frame_idx = total_frames - 1
82
- if not duration:
83
- duration = (round(max_frame_idx / fps) + 1) if fps else 0
84
- if max_frame_count is None:
85
- max_frame_count = GLM_VIDEO_DEFAULT_MAX_FRAMES
86
- if target_fps is None:
87
- target_fps = GLM_VIDEO_DEFAULT_FPS
88
-
89
- extract_t = int(duration * target_fps)
90
- extract_t = min(extract_t, int(max_frame_count))
91
-
92
- duration_per_frame = 1 / fps
93
- timestamps = [i * duration_per_frame for i in range(total_frames)]
94
- max_second = int(duration)
95
-
96
- if total_frames < extract_t:
97
- frame_indices = [
98
- math.floor(_i * total_frames / extract_t) for _i in range(extract_t)
99
- ]
100
- else:
101
- frame_indices = []
102
- current_second = 0.0
103
- inv_fps = 1 / (temporal_patch_size * target_fps)
104
- for frame_index in range(total_frames):
105
- if timestamps[frame_index] >= current_second:
106
- current_second += inv_fps
107
- frame_indices.append(frame_index)
108
- if current_second >= max_second:
109
- break
110
-
111
- if len(frame_indices) < extract_t:
112
- if len(frame_indices) == 0:
113
- start, end = 0, max(total_frames - 1, 0)
114
- else:
115
- start, end = frame_indices[0], frame_indices[-1]
116
- frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
117
- elif len(frame_indices) > extract_t:
118
- frame_indices = np.linspace(0, total_frames - 1, extract_t, dtype=int).tolist()
119
-
120
- seen, uniq = set(), []
121
- for idx in frame_indices:
122
- if idx not in seen:
123
- seen.add(idx)
124
- uniq.append(int(idx))
125
-
126
- if len(uniq) & 1:
127
- uniq.append(uniq[-1])
128
-
129
- return uniq
130
-
131
-
132
-def _ceil_to_factor(value: int, factor: int) -> int:
133
- """Round a positive integer upward to the nearest multiple of factor."""
134
- return math.ceil(value / factor) * factor
135
-
136
-
137
-def _fit_aligned_size_within_budget(
138
- t: int,
139
- h: int,
140
- w: int,
141
- h_factor: int,
142
- w_factor: int,
143
- max_pixels: int,
144
-) -> tuple[int, int]:
145
- """Largest proportional size whose upward-aligned canvas fits the budget.
146
-
147
- Binary search on the unaligned content height; each candidate is rounded
148
- upward to h_factor/w_factor, so the returned canvas always satisfies
149
- ``t * aligned_h * aligned_w <= max_pixels``.
150
- """
151
- minimum_pixels = t * h_factor * w_factor
152
- if max_pixels < minimum_pixels:
153
- raise ValueError(
154
- f"max_pixels={max_pixels} is too small. At least "
155
- f"{minimum_pixels} pixels are required for one aligned patch."
156
- )
157
-
158
- low, high = 1, h
159
- best_h, best_w = h_factor, w_factor
160
- while low <= high:
161
- content_h = (low + high) // 2
162
- content_w = max(1, math.floor(w * content_h / h))
163
- aligned_h = _ceil_to_factor(content_h, h_factor)
164
- aligned_w = _ceil_to_factor(content_w, w_factor)
165
- if t * aligned_h * aligned_w <= max_pixels:
166
- best_h, best_w = aligned_h, aligned_w
167
- low = content_h + 1
168
- else:
169
- high = content_h - 1
170
- return best_h, best_w
171
-
172
-
173
-def smart_resize(
174
- t: int,
175
- h: int,
176
- w: int,
177
- t_factor: int = 1,
178
- h_factor: int = 28,
179
- w_factor: int = 28,
180
- min_pixels: int = 56 * 56,
181
- max_pixels: int = 14 * 14 * 4 * 1280,
182
-) -> tuple[int, int]:
183
- """GLM-5.3-Flash ``smart_resize``: upward-aligned canvas under a
184
- ``t_bar * h_bar * w_bar`` pixel budget.
185
-
186
- Height/width always round UP to their factors (content is then padded,
187
- never cropped or distorted); an over-budget canvas is refit by binary
188
- search instead of one-shot square-root scaling. ``h_factor`` /
189
- ``w_factor`` carry ``patch_expand_factor`` on top of
190
- ``patch_size * merge_size``; ``t_factor`` is ``temporal_patch_size``. For
191
- a still image ``t = t_factor = temporal_patch_size`` so ``t_bar =
192
- temporal_patch_size``.
193
- """
194
- if min(t, h, w, t_factor, h_factor, w_factor) <= 0:
195
- raise ValueError("Image dimensions and alignment factors must be positive.")
196
- if min_pixels <= 0 or max_pixels <= 0:
197
- raise ValueError("min_pixels and max_pixels must be positive.")
198
- if min_pixels > max_pixels:
199
- raise ValueError("min_pixels must be less than or equal to max_pixels.")
200
-
201
- t_bar = max(t_factor, round(t / t_factor) * t_factor)
202
- h_bar = _ceil_to_factor(h, h_factor)
203
- w_bar = _ceil_to_factor(w, w_factor)
204
-
205
- if t_bar * h_bar * w_bar > max_pixels:
206
- h_bar, w_bar = _fit_aligned_size_within_budget(
207
- t=t_bar,
208
- h=h,
209
- w=w,
210
- h_factor=h_factor,
211
- w_factor=w_factor,
212
- max_pixels=max_pixels,
213
- )
214
- elif t_bar * h_bar * w_bar < min_pixels:
215
- beta = math.sqrt(min_pixels / (t * h * w))
216
- h_bar = _ceil_to_factor(max(1, math.ceil(h * beta)), h_factor)
217
- w_bar = _ceil_to_factor(max(1, math.ceil(w * beta)), w_factor)
218
-
219
- # Alignment can push a candidate slightly over a tight max_pixels
220
- # budget. Refit it when that happens.
221
- if t_bar * h_bar * w_bar > max_pixels:
222
- h_bar, w_bar = _fit_aligned_size_within_budget(
223
- t=t_bar,
224
- h=h,
225
- w=w,
226
- h_factor=h_factor,
227
- w_factor=w_factor,
228
- max_pixels=max_pixels,
229
- )
230
-
231
- return h_bar, w_bar
232
-
233
-
234
-def _get_pad_content_size(
235
- image_height: int,
236
- image_width: int,
237
- canvas_height: int,
238
- canvas_width: int,
239
- allow_upscale: bool = False,
240
-) -> tuple[int, int]:
241
- """Aspect-ratio-preserving content size that fits the canvas.
242
-
243
- Oversized images are shrunk proportionally. Small images are enlarged
244
- only when ``allow_upscale``. Padding is applied after the resize.
245
- """
246
- scale = min(canvas_height / image_height, canvas_width / image_width)
247
- if not allow_upscale:
248
- scale = min(1.0, scale)
249
- content_height = max(1, min(canvas_height, math.floor(image_height * scale)))
250
- content_width = max(1, min(canvas_width, math.floor(image_width * scale)))
251
- return content_height, content_width
252
-
253
-
254
-def _resize_or_pad(
255
- stacked_images: torch.Tensor,
256
- target_height: int,
257
- target_width: int,
258
- resize_mode: str,
259
- resample: "PILImageResampling | tvF.InterpolationMode | int | None",
260
- resize,
261
- allow_upscale: bool = False,
262
-) -> torch.Tensor:
263
- """Resize onto the aligned canvas, or keep the aspect ratio and
264
- zero-pad the right/bottom sides (``resize_mode="pad"``)."""
265
- height, width = stacked_images.shape[-2:]
266
-
267
- if resize_mode == "resize":
268
- return resize(
269
- stacked_images,
270
- size=SizeDict(height=target_height, width=target_width),
271
- resample=resample,
272
- )
273
-
274
- if resize_mode != "pad":
275
- raise ValueError("resize_mode must be either 'resize' or 'pad'.")
276
-
277
- content_height, content_width = _get_pad_content_size(
278
- image_height=height,
279
- image_width=width,
280
- canvas_height=target_height,
281
- canvas_width=target_width,
282
- allow_upscale=allow_upscale,
283
- )
284
-
285
- if (content_height, content_width) != (height, width):
286
- stacked_images = resize(
287
- stacked_images,
288
- size=SizeDict(height=content_height, width=content_width),
289
- resample=resample,
290
- )
291
-
292
- # torchvision padding order: [left, top, right, bottom] -> pad only the
293
- # right and bottom sides.
294
- return tvF.pad(
295
- stacked_images,
296
- padding=[0, 0, target_width - content_width, target_height - content_height],
297
- fill=0,
298
- )
299
-
300
-
301
-def _pixel_budget(
302
- min_image_tokens: int | None,
303
- max_image_tokens: int | None,
304
- patch_size: int,
305
- merge_size: int,
306
- temporal_patch_size: int,
307
-) -> tuple[int, int]:
308
- """(min_pixels, max_pixels) from the token bounds of
309
- ``processor_config.json``; one vision token covers
310
- ``temporal_patch_size * (patch_size * merge_size) ** 2`` pixels."""
311
- if min_image_tokens is None or max_image_tokens is None:
312
- raise ValueError(
313
- "min_image_tokens and max_image_tokens must be provided by "
314
- "processor_config.json (or per-call kwargs)."
315
- )
316
- factor = temporal_patch_size * (patch_size * merge_size) ** 2
317
- return min_image_tokens * factor, max_image_tokens * factor
318
-
319
-
320
-class Glm5NextImageProcessorKwargs(ImagesKwargs, total=False): # type: ignore[call-arg]
321
- patch_size: int | None
322
- temporal_patch_size: int | None
323
- merge_size: int | None
324
- patch_expand_factor: int | None
325
- resize_mode: str | None
326
- min_image_tokens: int | None
327
- max_image_tokens: int | None
328
-
329
-
330
-class Glm5NextImageProcessor(TorchvisionBackend):
331
- """Fast torchvision image processor for GLM-5.3-Flash.
332
-
333
- ``patch_expand_factor`` multiplies into the ``smart_resize`` spatial
334
- factor ``patch_size * merge_size``. ``resize_mode`` picks the geometry:
335
- ``"pad"`` (default) preserves the aspect ratio and zero-pads the
336
- right/bottom of the upward-aligned canvas, ``"resize"`` stretches onto
337
- it. Defaults mirror the checkpoint's ``image_processor`` config.
338
- """
339
-
340
- do_resize = True
341
- resample = PILImageResampling.BICUBIC
342
- size = {"longest_edge": 1} # unused: budgets come from the token bounds
343
- do_rescale = True
344
- do_normalize = True
345
- image_mean = OPENAI_CLIP_MEAN
346
- image_std = OPENAI_CLIP_STD
347
- do_convert_rgb = True
348
- patch_size = 14
349
- temporal_patch_size = 2
350
- merge_size = 2
351
- patch_expand_factor = 1
352
- resize_mode = "pad"
353
- min_image_tokens = 16
354
- max_image_tokens = 8000
355
- valid_kwargs = Glm5NextImageProcessorKwargs
356
- model_input_names = ["pixel_values", "image_grid_thw"]
357
-
358
- def _preprocess(
359
- self,
360
- images: list[torch.Tensor],
361
- do_resize: bool,
362
- size: SizeDict,
363
- resample: "PILImageResampling | tvF.InterpolationMode | int | None",
364
- do_rescale: bool,
365
- rescale_factor: float,
366
- do_normalize: bool,
367
- image_mean: float | list[float] | None,
368
- image_std: float | list[float] | None,
369
- patch_size: int,
370
- temporal_patch_size: int,
371
- merge_size: int,
372
- patch_expand_factor: int,
373
- resize_mode: str | None,
374
- min_image_tokens: int | None,
375
- max_image_tokens: int | None,
376
- disable_grouping: bool | None,
377
- return_tensors: str | TensorType | None,
378
- **kwargs,
379
- ) -> BatchFeature:
380
- resize_mode = resize_mode if resize_mode is not None else self.resize_mode
381
- min_pixels, max_pixels = _pixel_budget(
382
- min_image_tokens if min_image_tokens is not None else self.min_image_tokens,
383
- max_image_tokens if max_image_tokens is not None else self.max_image_tokens,
384
- patch_size,
385
- merge_size,
386
- temporal_patch_size,
387
- )
388
- grouped_images, grouped_images_index = group_images_by_shape(
389
- images, disable_grouping=disable_grouping
390
- )
391
- resized_images_grouped = {}
392
- for shape, stacked_images in grouped_images.items():
393
- height, width = stacked_images.shape[-2:]
394
- if do_resize:
395
- resized_height, resized_width = smart_resize(
396
- t=temporal_patch_size,
397
- h=height,
398
- w=width,
399
- t_factor=temporal_patch_size,
400
- h_factor=patch_size * merge_size * patch_expand_factor,
401
- w_factor=patch_size * merge_size * patch_expand_factor,
402
- min_pixels=min_pixels,
403
- max_pixels=max_pixels,
404
- )
405
- stacked_images = _resize_or_pad(
406
- stacked_images,
407
- target_height=resized_height,
408
- target_width=resized_width,
409
- resize_mode=resize_mode,
410
- resample=resample,
411
- resize=self.resize,
412
- allow_upscale=(temporal_patch_size * height * width < min_pixels),
413
- )
414
- resized_images_grouped[shape] = stacked_images
415
-
416
- resized_images = reorder_images(resized_images_grouped, grouped_images_index)
417
-
418
- grouped_images, grouped_images_index = group_images_by_shape(
419
- resized_images, disable_grouping=disable_grouping
420
- )
421
- processed_images_grouped = {}
422
- processed_grids = {}
423
-
424
- for shape, stacked_images in grouped_images.items():
425
- resized_height, resized_width = stacked_images.shape[-2:]
426
-
427
- patches = self.rescale_and_normalize(
428
- stacked_images,
429
- do_rescale,
430
- rescale_factor,
431
- do_normalize,
432
- image_mean,
433
- image_std,
434
- )
435
- if patches.ndim == 4: # (B, C, H, W)
436
- patches = patches.unsqueeze(1) # (B, T=1, C, H, W)
437
-
438
- if patches.shape[1] % temporal_patch_size != 0:
439
- repeats = patches[:, -1:].repeat(
440
- 1,
441
- temporal_patch_size - (patches.shape[1] % temporal_patch_size),
442
- 1,
443
- 1,
444
- 1,
445
- )
446
- patches = torch.cat([patches, repeats], dim=1)
447
-
448
- batch_size, t_len, channel = patches.shape[:3]
449
- grid_t = t_len // temporal_patch_size
450
- grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
451
-
452
- patches = patches.view(
453
- batch_size,
454
- grid_t,
455
- temporal_patch_size,
456
- channel,
457
- grid_h // merge_size,
458
- merge_size,
459
- patch_size,
460
- grid_w // merge_size,
461
- merge_size,
462
- patch_size,
463
- )
464
- # (B, grid_t, gh, gw, mh, mw, C, tp, ph, pw)
465
- patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
466
-
467
- flatten_patches = patches.reshape(
468
- batch_size,
469
- grid_t * grid_h * grid_w,
470
- channel * temporal_patch_size * patch_size * patch_size,
471
- )
472
-
473
- processed_images_grouped[shape] = flatten_patches
474
- processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
475
-
476
- processed_images = reorder_images(
477
- processed_images_grouped, grouped_images_index
478
- )
479
- processed_grids = reorder_images(processed_grids, grouped_images_index)
480
-
481
- pixel_values = torch.cat(processed_images, dim=0)
482
- image_grid_thw = torch.tensor(processed_grids)
483
-
484
- return BatchFeature(
485
- data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw},
486
- tensor_type=return_tensors,
487
- )
488
-
489
- def preprocess(
490
- self, images: ImageInput, **kwargs: Unpack[Glm5NextImageProcessorKwargs]
491
- ) -> BatchFeature:
492
- return super().preprocess(images, **kwargs)
493
-
494
- def get_number_of_image_patches(
495
- self, height: int, width: int, images_kwargs: dict | None = None
496
- ) -> int:
497
- """Number of image patches (pre-merge) for a given (height, width)."""
498
- images_kwargs = images_kwargs or {}
499
- patch_size = images_kwargs.get("patch_size", self.patch_size)
500
- merge_size = images_kwargs.get("merge_size", self.merge_size)
501
- patch_expand_factor = images_kwargs.get(
502
- "patch_expand_factor", self.patch_expand_factor
503
- )
504
- min_pixels, max_pixels = _pixel_budget(
505
- images_kwargs.get("min_image_tokens", self.min_image_tokens),
506
- images_kwargs.get("max_image_tokens", self.max_image_tokens),
507
- patch_size,
508
- merge_size,
509
- self.temporal_patch_size,
510
- )
511
- resized_height, resized_width = smart_resize(
512
- t=self.temporal_patch_size,
513
- h=height,
514
- w=width,
515
- t_factor=self.temporal_patch_size,
516
- h_factor=patch_size * merge_size * patch_expand_factor,
517
- w_factor=patch_size * merge_size * patch_expand_factor,
518
- min_pixels=min_pixels,
519
- max_pixels=max_pixels,
520
- )
521
- grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
522
- return grid_h * grid_w
523
-
524
-
525
-class Glm5NextVideoProcessorKwargs(VideosKwargs, total=False): # type: ignore[call-arg]
526
- fps: list[float] | float
527
- patch_size: int
528
- temporal_patch_size: int
529
- merge_size: int
530
- patch_expand_factor: int
531
- resize_mode: str | None
532
- target_fps: float | None
533
- max_frames: int | None
534
- fps_interval: int | None
535
- max_frame_count_dynamic: int | None
536
- min_image_tokens: int | None
537
- max_image_tokens: int | None
538
-
539
-
540
-class Glm5NextVideoProcessor(BaseVideoProcessor):
541
- """Fast video processor for GLM-5.3-Flash.
542
-
543
- Shares ``smart_resize`` / the pad-mode geometry / the patchify with the
544
- image processor, and adds GLM-5.3-Flash frame sampling
545
- (``glm_sample_frame_indices``: ``fps_interval`` semantics with a
546
- temporal-patch-scaled greedy walk). Defaults mirror the checkpoint's
547
- ``video_processor`` config.
548
- """
549
-
550
- resample = PILImageResampling.BICUBIC
551
- size = {"longest_edge": 1} # unused: budgets come from the token bounds
552
- image_mean = OPENAI_CLIP_MEAN
553
- image_std = OPENAI_CLIP_STD
554
- do_resize = True
555
- do_rescale = True
556
- do_normalize = True
557
- do_convert_rgb = True
558
- do_sample_frames = True
559
- patch_size = 14
560
- temporal_patch_size = 2
561
- patch_expand_factor = 1
562
- merge_size = 2
563
- valid_kwargs = Glm5NextVideoProcessorKwargs
564
- num_frames = 16
565
- fps = 2
566
- fps_interval = 2.0
567
- max_frame_count_dynamic = 2048
568
- resize_mode = "pad"
569
- min_image_tokens = 16
570
- max_image_tokens = 240000
571
- model_input_names = ["pixel_values_videos", "video_grid_thw"]
572
-
573
- def sample_frames(
574
- self,
575
- metadata: VideoMetadata,
576
- fps: int | float | None = None,
577
- **kwargs,
578
- ) -> np.ndarray:
579
- """Sample frame indices with GLM's fps-interval policy.
580
-
581
- ``fps`` / ``target_fps``, ``max_frames`` and ``fps_interval`` /
582
- ``max_frame_count_dynamic`` are the overrides described in
583
- :func:`glm_sample_frame_indices`.
584
- """
585
- if metadata is None or getattr(metadata, "fps", None) is None:
586
- raise ValueError(
587
- "Asked to sample frames per second but no video metadata was "
588
- "provided which is required when sampling in GLM-5.3-Flash. Please "
589
- "pass in `VideoMetadata` object or set `do_sample_frames=False`."
590
- )
591
-
592
- target_fps = fps if fps is not None else kwargs.get("target_fps")
593
- if target_fps is None:
594
- target_fps = self.fps_interval
595
- indices = glm_sample_frame_indices(
596
- metadata.total_num_frames,
597
- metadata.fps,
598
- metadata.duration or 0,
599
- target_fps=target_fps,
600
- max_frame_count=kwargs.get("max_frames") or self.max_frame_count_dynamic,
601
- temporal_patch_size=self.temporal_patch_size,
602
- )
603
- return np.array(indices)
604
-
605
- def _preprocess(
606
- self,
607
- videos: list[torch.Tensor],
608
- do_convert_rgb: bool = True,
609
- do_resize: bool = True,
610
- size: SizeDict | None = None,
611
- resample: "PILImageResampling | int | None" = PILImageResampling.BICUBIC,
612
- do_rescale: bool = True,
613
- rescale_factor: float = 1 / 255.0,
614
- do_normalize: bool = True,
615
- image_mean: float | list[float] | None = None,
616
- image_std: float | list[float] | None = None,
617
- patch_size: int | None = None,
618
- temporal_patch_size: int | None = None,
619
- patch_expand_factor: int | None = None,
620
- merge_size: int | None = None,
621
- resize_mode: str | None = None,
622
- min_image_tokens: int | None = None,
623
- max_image_tokens: int | None = None,
624
- return_tensors: str | TensorType | None = None,
625
- **kwargs,
626
- ) -> BatchFeature:
627
- patch_expand_factor = self.patch_expand_factor
628
- patch_size = patch_size if patch_size is not None else self.patch_size
629
- temporal_patch_size = (
630
- temporal_patch_size
631
- if temporal_patch_size is not None
632
- else self.temporal_patch_size
633
- )
634
- merge_size = merge_size if merge_size is not None else self.merge_size
635
- resize_mode = resize_mode if resize_mode is not None else self.resize_mode
636
- min_pixels, max_pixels = _pixel_budget(
637
- min_image_tokens if min_image_tokens is not None else self.min_image_tokens,
638
- max_image_tokens if max_image_tokens is not None else self.max_image_tokens,
639
- patch_size,
640
- merge_size,
641
- temporal_patch_size,
642
- )
643
- grouped_videos, grouped_videos_index = group_videos_by_shape(videos)
644
- resized_videos_grouped = {}
645
- for shape, stacked_videos in grouped_videos.items():
646
- if do_convert_rgb:
647
- stacked_videos = self.convert_to_rgb(stacked_videos)
648
- b, t_len, c, h, w = stacked_videos.shape
649
- num_frames, height, width = t_len, h, w
650
- if do_resize:
651
- resized_height, resized_width = smart_resize(
652
- t=num_frames,
653
- h=height,
654
- w=width,
655
- t_factor=temporal_patch_size,
656
- h_factor=patch_size * merge_size * patch_expand_factor,
657
- w_factor=patch_size * merge_size * patch_expand_factor,
658
- min_pixels=min_pixels,
659
- max_pixels=max_pixels,
660
- )
661
- stacked_videos = stacked_videos.view(b * t_len, c, h, w)
662
- stacked_videos = _resize_or_pad(
663
- stacked_videos,
664
- target_height=resized_height,
665
- target_width=resized_width,
666
- resize_mode=resize_mode,
667
- resample=resample,
668
- resize=self.resize,
669
- allow_upscale=(num_frames * height * width < min_pixels),
670
- )
671
- stacked_videos = stacked_videos.view(
672
- b, t_len, c, resized_height, resized_width
673
- )
674
- resized_videos_grouped[shape] = stacked_videos
675
- resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index)
676
-
677
- grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos)
678
- processed_videos_grouped = {}
679
- processed_grids = {}
680
- for shape, stacked_videos in grouped_videos.items():
681
- resized_height, resized_width = get_image_size(
682
- stacked_videos[0], channel_dim=ChannelDimension.FIRST
683
- )
684
- stacked_videos = self.rescale_and_normalize(
685
- stacked_videos,
686
- do_rescale,
687
- rescale_factor,
688
- do_normalize,
689
- image_mean,
690
- image_std,
691
- )
692
- patches = stacked_videos
693
-
694
- if pad := -patches.shape[1] % temporal_patch_size:
695
- repeats = patches[:, -1:].expand(-1, pad, -1, -1, -1)
696
- patches = torch.cat((patches, repeats), dim=1)
697
- batch_size, grid_t, channel = patches.shape[:3]
698
- grid_t = grid_t // temporal_patch_size
699
- grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
700
-
701
- patches = patches.view(
702
- batch_size,
703
- grid_t,
704
- temporal_patch_size,
705
- channel,
706
- grid_h // merge_size,
707
- merge_size,
708
- patch_size,
709
- grid_w // merge_size,
710
- merge_size,
711
- patch_size,
712
- )
713
- patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
714
- flatten_patches = patches.reshape(
715
- batch_size,
716
- grid_t * grid_h * grid_w,
717
- channel * temporal_patch_size * patch_size * patch_size,
718
- )
719
-
720
- processed_videos_grouped[shape] = flatten_patches
721
- processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
722
-
723
- processed_videos = reorder_videos(
724
- processed_videos_grouped, grouped_videos_index
725
- )
726
- processed_grids = reorder_videos(processed_grids, grouped_videos_index)
727
- pixel_values_videos = torch.cat(processed_videos, dim=0)
728
- video_grid_thw = torch.tensor(processed_grids)
729
- return BatchFeature(
730
- data={
731
- "pixel_values_videos": pixel_values_videos,
732
- "video_grid_thw": video_grid_thw,
733
- },
734
- tensor_type=return_tensors,
735
- )
736
-
737
-
738
-class Glm5NextProcessorKwargs(ProcessingKwargs, total=False): # type: ignore[call-arg]
739
- images_kwargs: Glm5NextImageProcessorKwargs
740
- videos_kwargs: Glm5NextVideoProcessorKwargs
741
- _defaults = {
742
- "text_kwargs": {
743
- "padding": False,
744
- "return_token_type_ids": False,
745
- "return_mm_token_type_ids": False,
746
- },
747
- "videos_kwargs": {"return_metadata": True},
748
- }
749
-
750
-
751
-class Glm5NextProcessor(ProcessorMixin):
752
- """Wraps a GLM-5.3-Flash image processor, video processor and tokenizer.
753
-
754
- Token expansion per image = ``prod(image_grid_thw) // merge_size**2``; video
755
- frames are expanded with ``<|begin_of_image|>...<|end_of_image|>{ts} seconds``
756
- structure (mrope timestamps).
757
- """
758
-
759
- attributes = ["image_processor", "tokenizer", "video_processor"]
760
- image_processor_class = "AutoImageProcessor"
761
- video_processor_class = "AutoVideoProcessor"
762
- tokenizer_class = ("PythonBackend", "TokenizersBackend")
763
-
764
- def __init__(
765
- self,
766
- image_processor=None,
767
- tokenizer=None,
768
- video_processor=None,
769
- chat_template=None,
770
- **kwargs,
771
- ) -> None:
772
- super().__init__(
773
- image_processor, tokenizer, video_processor, chat_template=chat_template
774
- )
775
- self.image_token = (
776
- "<|image|>"
777
- if not hasattr(tokenizer, "image_token")
778
- else tokenizer.image_token
779
- )
780
- self.video_token = (
781
- "<|video|>"
782
- if not hasattr(tokenizer, "video_token")
783
- else tokenizer.video_token
784
- )
785
- self.image_token_id = (
786
- tokenizer.image_token_id
787
- if getattr(tokenizer, "image_token_id", None)
788
- else tokenizer.convert_tokens_to_ids(self.image_token)
789
- )
790
- self.video_token_id = (
791
- tokenizer.video_token_id
792
- if getattr(tokenizer, "video_token_id", None)
793
- else tokenizer.convert_tokens_to_ids(self.video_token)
794
- )
795
-
796
- @classmethod
797
- def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
798
- """Build the processor directly from the checkpoint config.
799
-
800
- GLM-5.3-Flash stores nested image/video configs in
801
- ``processor_config.json`` and declares a custom processor class. This
802
- method reads those configs directly and caps only the video token budget.
803
- """
804
- from transformers import AutoTokenizer
805
-
806
- model_path = pretrained_model_name_or_path
807
- tokenizer = AutoTokenizer.from_pretrained(model_path, **kwargs)
808
-
809
- def _cap_cfg(cfg: dict, *, is_video: bool) -> dict:
810
- # Video keeps a serving token cap (the checkpoint's 240k-token
811
- # budget would starve the KV cache at startup profiling); images
812
- # follow the checkpoint budget verbatim so preprocessing matches
813
- # the HF reference exactly.
814
- if is_video and cfg.get("max_image_tokens") is not None:
815
- cfg["max_image_tokens"] = min(
816
- cfg["max_image_tokens"], _MAX_VIDEO_TOKENS
817
- )
818
- return cfg
819
-
820
- ip_cfg = _cap_cfg(
821
- dict(get_image_processor_config(model_path, **kwargs)), is_video=False
822
- )
823
- image_processor = Glm5NextImageProcessor(
824
- **{k: v for k, v in ip_cfg.items() if k != "image_processor_type"}
825
- )
826
-
827
- processor_config = get_hf_file_to_dict(
828
- "processor_config.json",
829
- model_path,
830
- revision=kwargs.get("revision", "main"),
831
- )
832
- if processor_config is None:
833
- raise ValueError(f"Missing processor_config.json for {model_path}")
834
- vp_cfg = _cap_cfg(dict(processor_config["video_processor"]), is_video=True)
835
- video_processor = Glm5NextVideoProcessor(
836
- **{k: v for k, v in vp_cfg.items() if k != "video_processor_type"}
837
- )
838
-
839
- return cls(
840
- image_processor=image_processor,
841
- tokenizer=tokenizer,
842
- video_processor=video_processor,
843
- )
844
-
845
- def __call__(
846
- self,
847
- images: ImageInput | None = None,
848
- text: TextInput
849
- | PreTokenizedInput
850
- | list[TextInput]
851
- | list[PreTokenizedInput] = None,
852
- videos: VideoInput | None = None,
853
- **kwargs: Unpack[Glm5NextProcessorKwargs],
854
- ) -> BatchFeature:
855
- output_kwargs = self._merge_kwargs(
856
- Glm5NextProcessorKwargs,
857
- tokenizer_init_kwargs=self.tokenizer.init_kwargs,
858
- **kwargs,
859
- )
860
- if images is not None:
861
- image_inputs = self.image_processor(
862
- images=images, **output_kwargs["images_kwargs"]
863
- )
864
- else:
865
- image_inputs = {}
866
-
867
- if videos is not None:
868
- videos_inputs = self.video_processor(
869
- videos=videos, **output_kwargs["videos_kwargs"]
870
- )
871
- if "return_metadata" not in kwargs:
872
- videos_inputs.pop("video_metadata")
873
- else:
874
- videos_inputs = {}
875
-
876
- if not isinstance(text, list):
877
- text = [text]
878
-
879
- # Prompt updates expand the unchanged image/video markers after this call.
880
- return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
881
- return_mm_token_type_ids = output_kwargs["text_kwargs"].pop(
882
- "return_mm_token_type_ids", False
883
- )
884
- text_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
885
-
886
- if return_mm_token_type_ids:
887
- array_ids = np.array(text_inputs["input_ids"])
888
- mm_token_type_ids = np.zeros_like(text_inputs["input_ids"])
889
- mm_token_type_ids[array_ids == self.image_token_id] = 1
890
- text_inputs["mm_token_type_ids"] = mm_token_type_ids.tolist()
891
- return BatchFeature(
892
- data={**text_inputs, **image_inputs, **videos_inputs},
893
- tensor_type=return_tensors,
894
- )
895
-
896
- def _get_num_multimodal_tokens(self, image_sizes=None, video_sizes=None, **kwargs):
897
- vision_data = {}
898
- if image_sizes is not None:
899
- images_kwargs = Glm5NextProcessorKwargs._defaults.get("images_kwargs", {})
900
- images_kwargs.update(kwargs)
901
- merge_size = (
902
- images_kwargs.get("merge_size", None) or self.image_processor.merge_size
903
- )
904
-
905
- num_image_patches = [
906
- self.image_processor.get_number_of_image_patches(
907
- *image_size, images_kwargs
908
- )
909
- for image_size in image_sizes
910
- ]
911
- num_image_tokens = [(n // merge_size**2) for n in num_image_patches]
912
- vision_data.update(
913
- {
914
- "num_image_tokens": num_image_tokens,
915
- "num_image_patches": num_image_patches,
916
- }
917
- )
918
-
919
- if video_sizes is not None:
920
- videos_kwargs = Glm5NextProcessorKwargs._defaults.get("videos_kwargs", {})
921
- videos_kwargs.update(kwargs)
922
- num_video_patches = [
923
- self.video_processor.get_number_of_video_patches(
924
- *video_size, videos_kwargs
925
- )
926
- for video_size in video_sizes
927
- ]
928
- num_video_tokens = [(n // merge_size**2) for n in num_video_patches]
929
- vision_data["num_video_tokens"] = num_video_tokens
930
-
931
- return MultiModalData(**vision_data)
932
-
933
- def post_process_image_text_to_text(
934
- self,
935
- generated_outputs,
936
- skip_special_tokens=True,
937
- clean_up_tokenization_spaces=False,
938
- **kwargs,
939
- ):
940
- return self.tokenizer.batch_decode(
941
- generated_outputs,
942
- skip_special_tokens=skip_special_tokens,
943
- clean_up_tokenization_spaces=clean_up_tokenization_spaces,
944
- **kwargs,
945
- )
946
-
947
-
948
-__all__ = [
949
- "Glm5NextImageProcessor",
950
- "Glm5NextVideoProcessor",
951
- "Glm5NextProcessor",
952
- "smart_resize",
953
-]