A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 15:00 UTC
[Model] Add MiMo-V2.5 support (#40967)
Signed-off-by: Jee Jee Li <pandaleefree@gmail.com> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com> Co-authored-by: zjy0516 <riverclouds.zhu@qq.com> Co-authored-by: zjy0516 <zhujiangyun@inferact.ai> Co-authored-by: yasong <yasong.wang@inferact.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <copilot@github.com>
Isotr0py committed
Apr 27, 2026 at 21:26 UTC
c245d35ff467bb3e9a73fcb3c4b02e6c7a3d2964
16 files changed
+4737
-5
docs/models/supported_models.md
+2
@@ -439,6 +439,7 @@ th {
439
| `Mamba2ForCausalLM` | Mamba2 | `mistralai/Mamba-Codestral-7B-v0.1`, etc. | | ✅︎ |
440
| `MiMoForCausalLM` | MiMo | `XiaomiMiMo/MiMo-7B-RL`, etc. | ✅︎ | ✅︎ |
441
| `MiMoV2FlashForCausalLM` | MiMoV2Flash | `XiaomiMiMo/MiMo-V2-Flash`, etc. | | ✅︎ |
442
+| `MiMoV2ProForCausalLM` | MiMoV2Pro | `XiaomiMiMo/MiMo-V2.5-Pro`, etc. | | ✅︎ |
443
| `MiniCPMForCausalLM` | MiniCPM | `openbmb/MiniCPM-2B-sft-bf16`, `openbmb/MiniCPM-2B-dpo-bf16`, `openbmb/MiniCPM-S-1B-sft`, etc. | ✅︎ | ✅︎ |
444
| `MiniCPM3ForCausalLM` | MiniCPM3 | `openbmb/MiniCPM3-4B`, etc. | ✅︎ | ✅︎ |
445
| `MiniMaxForCausalLM` | MiniMax-Text | `MiniMaxAI/MiniMax-Text-01-hf`, etc. | | |
@@ -590,6 +591,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
591
| `LlavaNextVideoForConditionalGeneration` | LLaVA-NeXT-Video | T + V | `llava-hf/LLaVA-NeXT-Video-7B-hf`, etc. | | ✅︎ |
592
| `LlavaOnevisionForConditionalGeneration` | LLaVA-Onevision | T + I<sup>+</sup> + V<sup>+</sup> | `llava-hf/llava-onevision-qwen2-7b-ov-hf`, `llava-hf/llava-onevision-qwen2-0.5b-ov-hf`, etc. | | ✅︎ |
593
| `MiDashengLMModel` | MiDashengLM | T + A<sup>+</sup> | `mispeech/midashenglm-7b` | | ✅︎ |
594
+| `MiMoV2OmniForCausalLM` | MiMo-V2.5-Omni | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>+</sup> | `XiaomiMiMo/MiMo-V2.5-Omni` | | ✅︎ |
595
| `MiniCPMO` | MiniCPM-O | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>E+</sup> | `openbmb/MiniCPM-o-2_6`, etc. | ✅︎ | ✅︎ |
596
| `MiniCPMV` | MiniCPM-V | T + I<sup>E+</sup> + V<sup>E+</sup> | `openbmb/MiniCPM-V-2` (see note), `openbmb/MiniCPM-Llama3-V-2_5`, `openbmb/MiniCPM-V-2_6`, `openbmb/MiniCPM-V-4`, `openbmb/MiniCPM-V-4_5`, etc. | ✅︎ | |
597
| `MiniMaxVL01ForConditionalGeneration` | MiniMax-VL | T + I<sup>E+</sup> | `MiniMaxAI/MiniMax-VL-01`, etc. | | ✅︎ |
tests/models/registry.py
+18
@@ -594,6 +594,9 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
594
"MiMoV2FlashForCausalLM": _HfExamplesInfo(
595
"XiaomiMiMo/MiMo-V2-Flash", trust_remote_code=True
596
),
597
+ "MiMoV2ProForCausalLM": _HfExamplesInfo(
598
+ "XiaomiMiMo/MiMo-V2.5-Pro", trust_remote_code=True, is_available_online=False
599
+ ),
600
"Dots1ForCausalLM": _HfExamplesInfo("rednote-hilab/dots.llm1.inst"),
601
}
602
@@ -1069,6 +1072,9 @@ _MULTIMODAL_EXAMPLE_MODELS = {
1072
"MiDashengLMModel": _HfExamplesInfo(
1073
"mispeech/midashenglm-7b", trust_remote_code=True
1074
),
1075
+ "MiMoV2OmniForCausalLM": _HfExamplesInfo(
1076
+ "XiaomiMiMo/MiMo-V2.5-Omni", trust_remote_code=True, is_available_online=False
1077
+ ),
1078
"MiniCPMO": _HfExamplesInfo(
1079
"openbmb/MiniCPM-o-2_6",
1080
trust_remote_code=True,
@@ -1552,6 +1558,18 @@ _SPECULATIVE_DECODING_EXAMPLE_MODELS = {
1558
trust_remote_code=True,
1559
speculative_model="XiaomiMiMo/MiMo-7B-RL",
1560
),
1561
+ "MiMoV2MTPModel": _HfExamplesInfo(
1562
+ "XiaomiMiMo/MiMo-V2.5-Pro",
1563
+ trust_remote_code=True,
1564
+ speculative_model="XiaomiMiMo/MiMo-V2.5-Pro",
1565
+ is_available_online=False,
1566
+ ),
1567
+ "MiMoV2OmniMTPModel": _HfExamplesInfo(
1568
+ "XiaomiMiMo/MiMo-V2.5-Omni",
1569
+ trust_remote_code=True,
1570
+ speculative_model="XiaomiMiMo/MiMo-V2.5-Omni",
1571
+ is_available_online=False,
1572
+ ),
1573
"NemotronHMTPModel": _HfExamplesInfo(
1574
"nvidia/Nemotron-Super-Placeholder",
1575
speculative_model="nvidia/Nemotron-Super-Placeholder",
vllm/config/model.py
+1
-1
@@ -521,6 +521,7 @@ class ModelConfig:
521
if dict_overrides:
522
self._apply_dict_overrides(hf_config, dict_overrides)
523
self.hf_text_config = get_hf_text_config(self.hf_config)
524
+ self.model_arch_config = self.get_model_arch_config()
525
self.attention_chunk_size = getattr(
526
self.hf_text_config, "attention_chunk_size", None
527
)
@@ -528,7 +529,6 @@ class ModelConfig:
529
self.hf_image_processor_config = get_hf_image_processor_config(
530
self.model, hf_token=self.hf_token, revision=self.revision
531
)
531
- self.model_arch_config = self.get_model_arch_config()
532
533
architectures = self.architectures
534
registry = self.registry
vllm/config/speculative.py
+43
@@ -34,6 +34,7 @@ logger = init_logger(__name__)
34
MTPModelTypes = Literal[
35
"deepseek_mtp",
36
"mimo_mtp",
37
+ "mimo_v2_mtp",
38
"glm4_moe_mtp",
39
"glm4_moe_lite_mtp",
40
"glm_ocr_mtp",
@@ -332,6 +333,48 @@ class SpeculativeConfig:
333
}
334
)
335
336
+ if (arch := hf_config.architectures[0]) in (
337
+ "MiMoV2ProForCausalLM",
338
+ "MiMoV2OmniForCausalLM",
339
+ ):
340
+ from vllm.model_executor.models.mimo_v2_mtp import (
341
+ _MIMO_V2_PRO_NUM_MTP_LAYERS,
342
+ )
343
+
344
+ mtp_arch_maps = {
345
+ "MiMoV2ProForCausalLM": "MiMoV2MTPModel",
346
+ "MiMoV2OmniForCausalLM": "MiMoV2OmniMTPModel",
347
+ }
348
+
349
+ hf_config.model_type = "mimo_v2_mtp"
350
+ # vLLM currently supports only the first MiMo-V2 MTP layer.
351
+ n_predict = _MIMO_V2_PRO_NUM_MTP_LAYERS
352
+ hf_config.update(
353
+ {
354
+ "num_hidden_layers": 0,
355
+ "n_predict": n_predict,
356
+ "num_nextn_predict_layers": n_predict,
357
+ "architectures": [mtp_arch_maps[arch]],
358
+ }
359
+ )
360
+
361
+ if hf_config.architectures[0] == "MiMoV2FlashForCausalLM":
362
+ from vllm.model_executor.models.mimo_v2_mtp import (
363
+ _MIMO_V2_FLASH_NUM_MTP_LAYERS,
364
+ )
365
+
366
+ hf_config.model_type = "mimo_v2_mtp"
367
+ # vLLM currently supports only the first MiMo-V2 MTP layer.
368
+ n_predict = _MIMO_V2_FLASH_NUM_MTP_LAYERS
369
+ hf_config.update(
370
+ {
371
+ "num_hidden_layers": 0,
372
+ "n_predict": n_predict,
373
+ "num_nextn_predict_layers": n_predict,
374
+ "architectures": ["MiMoV2MTPModel"],
375
+ }
376
+ )
377
+
378
if hf_config.architectures[0] == "Glm4MoeForCausalLM":
379
hf_config.model_type = "glm4_moe_mtp"
380
n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
vllm/model_executor/layers/linear.py
+6
-1
@@ -1120,7 +1120,12 @@ class QKVParallelLinear(ColumnParallelLinear):
1120
# Special case for Quantization.
1121
# If quantized, we need to adjust the offset and size to account
1122
# for the packing.
1123
- if (
1123
+ if isinstance(param, BlockQuantScaleParameter):
1124
+ weight_block_size = getattr(self, "weight_block_size", None)
1125
+ shard_size, shard_offset = adjust_block_scale_shard(
1126
+ weight_block_size, shard_size, shard_offset
1127
+ )
1128
+ elif (
1129
isinstance(param, (PackedColumnParameter, PackedvLLMParameter))
1130
and param.packed_dim == param.output_dim
1131
):
vllm/model_executor/models/mimo_audio.py
new
+1389
@@ -0,0 +1,1389 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""MiMo audio: tokenizer, encoding utilities, and audio encoder.
4
+
5
+Ported from SGLang's mimo_audio.py.
6
+Audio tokenizer adapted from https://github.com/XiaomiMiMo/MiMo-Audio-Tokenizer.git
7
+"""
8
+
9
+import dataclasses
10
+import json
11
+import logging
12
+import math
13
+import os
14
+import typing as tp
15
+from dataclasses import dataclass
16
+from functools import wraps
17
+
18
+import torch
19
+import torch.distributed as dist
20
+import torch.nn as nn
21
+import torch.nn.functional as F
22
+from einops import rearrange, repeat
23
+from transformers.activations import ACT2FN
24
+from transformers.configuration_utils import PretrainedConfig
25
+from transformers.modeling_utils import PreTrainedModel
26
+from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
27
+from transformers.models.qwen2.modeling_qwen2 import Qwen2Model
28
+
29
+logger = logging.getLogger(__name__)
30
+
31
+
32
+# ---------------------------------------------------------------------------
33
+# Vector quantization (from MiMo-Audio-Tokenizer)
34
+# ---------------------------------------------------------------------------
35
+
36
+
37
+def _vq_default(val: tp.Any, d: tp.Any) -> tp.Any:
38
+ return val if val is not None else d
39
+
40
+
41
+def _ema_inplace(moving_avg, new, decay: float):
42
+ if dist.is_initialized():
43
+ dist.all_reduce(new, op=dist.ReduceOp.SUM)
44
+ moving_avg.data.mul_(decay).add_(new, alpha=(1 - decay))
45
+
46
+
47
+def _laplace_smoothing(x, n_categories: int, epsilon: float = 1e-5):
48
+ return (x + epsilon) / (x.sum() + n_categories * epsilon)
49
+
50
+
51
+def _uniform_init(*shape: int):
52
+ t = torch.empty(shape)
53
+ nn.init.kaiming_uniform_(t)
54
+ return t
55
+
56
+
57
+def _sample_vectors(samples, num: int):
58
+ num_samples, device = samples.shape[0], samples.device
59
+
60
+ if num_samples >= num:
61
+ indices = torch.randperm(num_samples, device=device)[:num]
62
+ else:
63
+ indices = torch.randint(0, num_samples, (num,), device=device)
64
+
65
+ selected_samples = samples[indices]
66
+
67
+ if dist.is_initialized():
68
+ dist.broadcast(selected_samples, src=0)
69
+
70
+ return selected_samples
71
+
72
+
73
+def _kmeans(samples, num_clusters: int, num_iters: int = 10):
74
+ dim, dtype = samples.shape[-1], samples.dtype
75
+
76
+ means = _sample_vectors(samples, num_clusters)
77
+
78
+ for _ in range(num_iters):
79
+ dists = -(
80
+ samples.pow(2).sum(1, keepdim=True)
81
+ - 2 * samples @ means.t()
82
+ + means.t().pow(2).sum(0, keepdim=True)
83
+ )
84
+
85
+ buckets = dists.max(dim=-1).indices
86
+ bins = torch.bincount(buckets, minlength=num_clusters)
87
+
88
+ new_means = buckets.new_zeros(num_clusters, dim, dtype=dtype)
89
+ new_means = new_means.scatter_add_(
90
+ 0, repeat(buckets, "n -> n d", d=dim), samples
91
+ )
92
+
93
+ if dist.is_initialized():
94
+ dist.all_reduce(bins, op=dist.ReduceOp.SUM)
95
+ dist.all_reduce(new_means, op=dist.ReduceOp.SUM)
96
+
97
+ zero_mask = bins == 0
98
+ bins_min_clamped = bins.masked_fill(zero_mask, 1)
99
+
100
+ new_means = new_means / bins_min_clamped[..., None]
101
+
102
+ means = torch.where(zero_mask[..., None], means, new_means)
103
+
104
+ return means, bins
105
+
106
+
107
+def _rotate_half(x):
108
+ x1 = x[..., : x.shape[-1] // 2]
109
+ x2 = x[..., x.shape[-1] // 2 :]
110
+ return torch.cat((-x2, x1), dim=-1)
111
+
112
+
113
+def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1):
114
+ cos = cos.unsqueeze(unsqueeze_dim)
115
+ sin = sin.unsqueeze(unsqueeze_dim)
116
+ q_embed = (q * cos) + (_rotate_half(q) * sin)
117
+ k_embed = (k * cos) + (_rotate_half(k) * sin)
118
+ return q_embed, k_embed
119
+
120
+
121
+def _compute_default_rope_parameters(
122
+ config=None, device=None, seq_len=None, **rope_kwargs
123
+):
124
+ if config is not None and len(rope_kwargs) > 0:
125
+ raise ValueError(
126
+ "Unexpected arguments: `**rope_kwargs` and `config` are mutually exclusive"
127
+ )
128
+ if len(rope_kwargs) > 0:
129
+ base = rope_kwargs["base"]
130
+ dim = rope_kwargs["dim"]
131
+ elif config is not None:
132
+ base = config.rope_theta
133
+ partial_rotary_factor = (
134
+ config.partial_rotary_factor
135
+ if hasattr(config, "partial_rotary_factor")
136
+ else 1.0
137
+ )
138
+ head_dim = (
139
+ getattr(config, "head_dim", None)
140
+ or config.hidden_size // config.num_attention_heads
141
+ )
142
+ dim = int(head_dim * partial_rotary_factor)
143
+ attention_factor = 1.0
144
+ inv_freq = 1.0 / (
145
+ base
146
+ ** (
147
+ torch.arange(0, dim, 2, dtype=torch.int64).to(
148
+ device=device, dtype=torch.float
149
+ )
150
+ / dim
151
+ )
152
+ )
153
+ return inv_freq, attention_factor
154
+
155
+
156
+_ROPE_INIT_FUNCTIONS = {
157
+ "default": _compute_default_rope_parameters,
158
+}
159
+
160
+
161
+def _dynamic_rope_update(rope_forward):
162
+ def dynamic_frequency_update(self, position_ids, device):
163
+ seq_len = torch.max(position_ids) + 1
164
+ if seq_len > self.max_seq_len_cached:
165
+ inv_freq, self.attention_scaling = self.rope_init_fn(
166
+ self.config, device, seq_len=seq_len
167
+ )
168
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
169
+ self.max_seq_len_cached = seq_len
170
+
171
+ if (
172
+ seq_len < self.original_max_seq_len
173
+ and self.max_seq_len_cached > self.original_max_seq_len
174
+ ):
175
+ self.original_inv_freq = self.original_inv_freq.to(device)
176
+ self.register_buffer("inv_freq", self.original_inv_freq, persistent=False)
177
+ self.max_seq_len_cached = self.original_max_seq_len
178
+
179
+ @wraps(rope_forward)
180
+ def wrapper(self, x, position_ids):
181
+ if "dynamic" in self.rope_type:
182
+ dynamic_frequency_update(self, position_ids, device=x.device)
183
+ return rope_forward(self, x, position_ids)
184
+
185
+ return wrapper
186
+
187
+
188
+class AudioRotaryEmbedding(nn.Module):
189
+ def __init__(self, base, dim, max_seq_len, rope_type="default", device=None):
190
+ super().__init__()
191
+ self.max_seq_len = max_seq_len
192
+ self.rope_type = rope_type
193
+ self.rope_init_fn = _ROPE_INIT_FUNCTIONS[self.rope_type]
194
+ inv_freq, self.attention_scaling = self.rope_init_fn(
195
+ device=device, base=base, dim=dim
196
+ )
197
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
198
+ self.original_inv_freq = self.inv_freq
199
+
200
+ @torch.no_grad()
201
+ @_dynamic_rope_update
202
+ def forward(self, x, position_ids):
203
+ inv_freq_expanded = self.inv_freq[:, None].float().expand(-1, 1).to(x.device)
204
+ position_ids_expanded = position_ids[None, :].float()
205
+ device_type = (
206
+ x.device.type
207
+ if isinstance(x.device.type, str) and x.device.type != "mps"
208
+ else "cpu"
209
+ )
210
+ with torch.autocast(device_type=device_type, enabled=False):
211
+ freqs = (
212
+ inv_freq_expanded.float() @ position_ids_expanded.float()
213
+ ).transpose(0, 1)
214
+ emb = torch.cat((freqs, freqs), dim=-1)
215
+ cos = emb.cos() * self.attention_scaling
216
+ sin = emb.sin() * self.attention_scaling
217
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
218
+
219
+
220
+class EuclideanCodebook(nn.Module):
221
+ def __init__(
222
+ self,
223
+ dim: int,
224
+ codebook_size: int,
225
+ kmeans_init: int = False,
226
+ kmeans_iters: int = 10,
227
+ decay: float = 0.99,
228
+ epsilon: float = 1e-5,
229
+ threshold_ema_dead_code: int = 2,
230
+ ):
231
+ super().__init__()
232
+ self.decay = decay
233
+ init_fn: tp.Callable[..., torch.Tensor] | tp.Any = (
234
+ _uniform_init if not kmeans_init else torch.zeros
235
+ )
236
+ embed = init_fn(codebook_size, dim)
237
+
238
+ self.codebook_size = codebook_size
239
+ self.kmeans_iters = kmeans_iters
240
+ self.epsilon = epsilon
241
+ self.threshold_ema_dead_code = threshold_ema_dead_code
242
+
243
+ self.register_buffer("inited", torch.Tensor([not kmeans_init]))
244
+ self.register_buffer("cluster_size", torch.zeros(codebook_size))
245
+ self.register_buffer("embed", embed)
246
+ self.register_buffer("embed_avg", embed.clone())
247
+
248
+ @torch.jit.ignore
249
+ def init_embed_(self, data):
250
+ if self.inited:
251
+ return
252
+
253
+ embed, cluster_size = _kmeans(data, self.codebook_size, self.kmeans_iters)
254
+ self.embed.data.copy_(embed)
255
+ self.embed_avg.data.copy_(embed.clone())
256
+ self.cluster_size.data.copy_(cluster_size)
257
+ self.inited.data.copy_(torch.Tensor([True]))
258
+
259
+ def replace_(self, samples, mask):
260
+ replace_num = mask.sum()
261
+ modified_codebook = self.embed.clone()
262
+ modified_codebook[mask] = _sample_vectors(samples, replace_num)
263
+ self.embed.data.copy_(modified_codebook)
264
+
265
+ def expire_codes_(self, batch_samples):
266
+ if self.threshold_ema_dead_code == 0:
267
+ return
268
+
269
+ expired_codes = self.cluster_size < self.threshold_ema_dead_code
270
+ if not torch.any(expired_codes):
271
+ return
272
+
273
+ batch_samples = rearrange(batch_samples, "... d -> (...) d")
274
+ self.replace_(batch_samples, mask=expired_codes)
275
+
276
+ def preprocess(self, x):
277
+ x = rearrange(x, "... d -> (...) d")
278
+ return x
279
+
280
+ def quantize(self, x):
281
+ embed = self.embed.t()
282
+ dist_val = -(
283
+ x.pow(2).sum(1, keepdim=True)
284
+ - 2 * x @ embed
285
+ + embed.pow(2).sum(0, keepdim=True)
286
+ )
287
+ embed_ind = dist_val.max(dim=-1).indices
288
+ return embed_ind
289
+
290
+ def postprocess_emb(self, embed_ind, shape):
291
+ return embed_ind.view(*shape[:-1])
292
+
293
+ def dequantize(self, embed_ind):
294
+ quantize = F.embedding(embed_ind, self.embed)
295
+ return quantize
296
+
297
+ def encode(self, x):
298
+ shape = x.shape
299
+ x = self.preprocess(x)
300
+ embed_ind = self.quantize(x)
301
+ embed_ind = self.postprocess_emb(embed_ind, shape)
302
+ return embed_ind
303
+
304
+ def decode(self, embed_ind):
305
+ quantize = self.dequantize(embed_ind)
306
+ return quantize
307
+
308
+ def forward(self, x):
309
+ shape, dtype = x.shape, x.dtype
310
+ x = self.preprocess(x)
311
+
312
+ self.init_embed_(x)
313
+
314
+ embed_ind = self.quantize(x)
315
+ embed_onehot = F.one_hot(embed_ind, self.codebook_size).type(dtype)
316
+ embed_ind = self.postprocess_emb(embed_ind, shape)
317
+ quantize = self.dequantize(embed_ind)
318
+
319
+ if self.training:
320
+ self.expire_codes_(x)
321
+ _ema_inplace(self.cluster_size, embed_onehot.sum(0), self.decay)
322
+ embed_sum = x.t() @ embed_onehot
323
+ _ema_inplace(self.embed_avg, embed_sum.t().contiguous(), self.decay)
324
+ cluster_size = (
325
+ _laplace_smoothing(self.cluster_size, self.codebook_size, self.epsilon)
326
+ * self.cluster_size.sum()
327
+ )
328
+ embed_normalized = self.embed_avg / cluster_size.unsqueeze(1)
329
+ self.embed.data.copy_(embed_normalized)
330
+
331
+ return quantize, embed_ind
332
+
333
+
334
+class VectorQuantization(nn.Module):
335
+ def __init__(
336
+ self,
337
+ dim: int,
338
+ codebook_size: int,
339
+ codebook_dim: int | None = None,
340
+ decay: float = 0.99,
341
+ epsilon: float = 1e-5,
342
+ kmeans_init: bool = True,
343
+ kmeans_iters: int = 50,
344
+ threshold_ema_dead_code: int = 2,
345
+ commitment_weight: float = 1.0,
346
+ ):
347
+ super().__init__()
348
+ _codebook_dim: int = _vq_default(codebook_dim, dim)
349
+
350
+ requires_projection = _codebook_dim != dim
351
+ self.project_in = (
352
+ nn.Linear(dim, _codebook_dim) if requires_projection else nn.Identity()
353
+ )
354
+ self.project_out = (
355
+ nn.Linear(_codebook_dim, dim) if requires_projection else nn.Identity()
356
+ )
357
+
358
+ self.epsilon = epsilon
359
+ self.commitment_weight = commitment_weight
360
+
361
+ self._codebook = EuclideanCodebook(
362
+ dim=_codebook_dim,
363
+ codebook_size=codebook_size,
364
+ kmeans_init=kmeans_init,
365
+ kmeans_iters=kmeans_iters,
366
+ decay=decay,
367
+ epsilon=epsilon,
368
+ threshold_ema_dead_code=threshold_ema_dead_code,
369
+ )
370
+ self.codebook_size = codebook_size
371
+
372
+ @property
373
+ def codebook(self):
374
+ return self._codebook.embed
375
+
376
+ def encode(self, x):
377
+ x = self.project_in(x)
378
+ embed_in = self._codebook.encode(x)
379
+ return embed_in
380
+
381
+ def decode(self, embed_ind):
382
+ quantize = self._codebook.decode(embed_ind)
383
+ quantize = self.project_out(quantize)
384
+ return quantize
385
+
386
+ def forward(self, x):
387
+ device = x.device
388
+ x = self.project_in(x)
389
+
390
+ quantize, embed_ind = self._codebook(x)
391
+
392
+ if self.training:
393
+ quantize = x + (quantize - x).detach()
394
+
395
+ loss = torch.tensor([0.0], device=device, requires_grad=self.training)
396
+
397
+ quantize = self.project_out(quantize)
398
+ return quantize, embed_ind, loss
399
+
400
+
401
+class ResidualVectorQuantization(nn.Module):
402
+ def __init__(self, *, num_quantizers, codebook_size, **kwargs):
403
+ super().__init__()
404
+ if isinstance(codebook_size, int):
405
+ codebook_size = [codebook_size] * num_quantizers
406
+ elif len(codebook_size) < num_quantizers:
407
+ codebook_size += [codebook_size[-1]] * (num_quantizers - len(codebook_size))
408
+ self.layers = nn.ModuleList(
409
+ [
410
+ VectorQuantization(codebook_size=codebook_size[i], **kwargs)
411
+ for i in range(num_quantizers)
412
+ ]
413
+ )
414
+
415
+ def forward(self, x, n_q: int | None = None, layers: list | None = None):
416
+ quantized_out = 0.0
417
+ residual = x
418
+
419
+ all_losses = []
420
+ all_indices = []
421
+ out_quantized = []
422
+
423
+ n_q = n_q or len(self.layers)
424
+
425
+ for i, layer in enumerate(self.layers[:n_q]):
426
+ quantized, indices, loss = layer(residual)
427
+ residual = residual - quantized
428
+ quantized_out = quantized_out + quantized
429
+
430
+ all_indices.append(indices)
431
+ all_losses.append(loss)
432
+ if layers and i in layers:
433
+ out_quantized.append(quantized_out)
434
+
435
+ out_losses, out_indices = map(torch.stack, (all_losses, all_indices))
436
+ return quantized_out, out_indices, out_losses, out_quantized
437
+
438
+ def encode(
439
+ self, x: torch.Tensor, n_q: int | None = None, st: int | None = None
440
+ ) -> torch.Tensor:
441
+ residual = x
442
+ all_indices = []
443
+ n_q = len(self.layers) if n_q is None else n_q
444
+ st = 0 if st is None else st
445
+ for layer in self.layers[st:n_q]:
446
+ indices = layer.encode(residual)
447
+ quantized = layer.decode(indices)
448
+ residual = residual - quantized
449
+ all_indices.append(indices)
450
+ out_indices = torch.stack(all_indices)
451
+ return out_indices
452
+
453
+ def decode(self, q_indices: torch.Tensor, st: int = 0) -> torch.Tensor:
454
+ quantized_out = self.layers[st].decode(q_indices[0])
455
+ for i in range(1, len(q_indices)):
456
+ layer = self.layers[st + i]
457
+ quantized = layer.decode(q_indices[i])
458
+ quantized_out = quantized_out + quantized
459
+ return quantized_out
460
+
461
+
462
+class ResidualVectorQuantizer(nn.Module):
463
+ def __init__(
464
+ self,
465
+ dimension: int = 256,
466
+ n_q: int = 8,
467
+ bins: int | list = 1024,
468
+ decay: float = 0.99,
469
+ kmeans_init: bool = True,
470
+ kmeans_iters: int = 50,
471
+ threshold_ema_dead_code: int = 2,
472
+ ):
473
+ super().__init__()
474
+ self.n_q = n_q
475
+ self.dimension = dimension
476
+ self.bins = bins
477
+ self.decay = decay
478
+ self.kmeans_init = kmeans_init
479
+ self.kmeans_iters = kmeans_iters
480
+ self.threshold_ema_dead_code = threshold_ema_dead_code
481
+ self.vq = ResidualVectorQuantization(
482
+ dim=self.dimension,
483
+ codebook_size=self.bins,
484
+ num_quantizers=self.n_q,
485
+ decay=self.decay,
486
+ kmeans_init=self.kmeans_init,
487
+ kmeans_iters=self.kmeans_iters,
488
+ threshold_ema_dead_code=self.threshold_ema_dead_code,
489
+ )
490
+
491
+ def forward(
492
+ self,
493
+ x: torch.Tensor,
494
+ n_q: int | None = None,
495
+ layers: list | None = None,
496
+ ):
497
+ n_q = n_q if n_q else self.n_q
498
+ quantized, codes, commit_loss, quantized_list = self.vq(
499
+ x, n_q=n_q, layers=layers
500
+ )
501
+ return quantized, codes, torch.mean(commit_loss), quantized_list
502
+
503
+ def encode(
504
+ self, x: torch.Tensor, n_q: int | None = None, st: int | None = None
505
+ ) -> torch.Tensor:
506
+ n_q = n_q if n_q else self.n_q
507
+ st = st or 0
508
+ codes = self.vq.encode(x, n_q=n_q, st=st)
509
+ return codes
510
+
511
+ def decode(self, codes: torch.Tensor, st: int = 0) -> torch.Tensor:
512
+ quantized = self.vq.decode(codes, st=st)
513
+ return quantized
514
+
515
+
516
+# ---------------------------------------------------------------------------
517
+# Audio tokenizer
518
+# ---------------------------------------------------------------------------
519
+
520
+
521
+class MiMoAudioTokenizerConfig(PretrainedConfig):
522
+ model_type = "mimo_audio_tokenizer"
523
+
524
+ def __init__(
525
+ self,
526
+ max_audio_seconds: int = 1800,
527
+ stride_size: int = 2,
528
+ avg_pooler: int = 1,
529
+ d_model: int = 768,
530
+ scale_embedding: bool = True,
531
+ kernel_size: int = 3,
532
+ activation_function: str = "gelu",
533
+ encoder_layers: int = 8,
534
+ encoder_skip_layer_id: int = None,
535
+ encoder_attention_heads: int = 12,
536
+ encoder_ffn_dim: int = 3072,
537
+ encoder_causal: bool = False,
538
+ encoder_attn_window_size: list = None,
539
+ decoder_layers: int = 8,
540
+ decoder_attention_heads: int = 12,
541
+ decoder_ffn_dim: int = 3072,
542
+ decoder_kernel_size: int = 3,
543
+ decoder_stride_size: int = 2,
544
+ decoder_causal: bool = True,
545
+ decoder_attn_window_size: list = None,
546
+ nfft: int = 1024,
547
+ vocoder_dim: int = 512,
548
+ vocoder_intermediate_dim: int = 4096,
549
+ vocoder_num_layers: int = 30,
550
+ n_mels: int = 80,
551
+ sampling_rate: int = 24000,
552
+ hop_length: int = 240,
553
+ window_size: int = 1024,
554
+ vocoder_padding: str = "same",
555
+ fmin: int = 0,
556
+ fmax: int = None,
557
+ num_quantizers: int = 12,
558
+ codebook_size: list = None,
559
+ threshold_ema_dead_code: int = 10,
560
+ position_embedding_type: str = "rope",
561
+ rope_theta: int = 10000,
562
+ rope_type: str = "default",
563
+ ln_type: str = "LayerNorm",
564
+ vocoder_attention_heads: int = 4,
565
+ vocoder_attn_window_size: list = None,
566
+ use_istft_only: bool = False,
567
+ hybrid_attention: bool = False,
568
+ hybrid_block_size: int = 8,
569
+ swa_per_block: int = 2,
570
+ **kwargs,
571
+ ):
572
+ super().__init__(**kwargs)
573
+ self.max_audio_seconds = max_audio_seconds
574
+ self.stride_size = stride_size
575
+ self.avg_pooler = avg_pooler
576
+ self.d_model = d_model
577
+ self.scale_embedding = scale_embedding
578
+ self.kernel_size = kernel_size
579
+ self.activation_function = activation_function
580
+ self.encoder_layers = encoder_layers
581
+ self.encoder_skip_layer_id = encoder_skip_layer_id
582
+ self.encoder_attention_heads = encoder_attention_heads
583
+ self.encoder_ffn_dim = encoder_ffn_dim
584
+ self.encoder_causal = encoder_causal
585
+ self.encoder_attn_window_size = (
586
+ encoder_attn_window_size
587
+ if encoder_attn_window_size is not None
588
+ else [-1, -1]
589
+ )
590
+ self.decoder_layers = decoder_layers
591
+ self.decoder_attention_heads = decoder_attention_heads
592
+ self.decoder_ffn_dim = decoder_ffn_dim
593
+ self.decoder_kernel_size = decoder_kernel_size
594
+ self.decoder_stride_size = decoder_stride_size
595
+ self.decoder_causal = decoder_causal
596
+ self.decoder_attn_window_size = (
597
+ decoder_attn_window_size
598
+ if decoder_attn_window_size is not None
599
+ else [-1, -1]
600
+ )
601
+ self.nfft = nfft
602
+ self.vocoder_dim = vocoder_dim
603
+ self.vocoder_intermediate_dim = vocoder_intermediate_dim
604
+ self.vocoder_num_layers = vocoder_num_layers
605
+ self.n_mels = n_mels
606
+ self.sampling_rate = sampling_rate
607
+ self.hop_length = hop_length
608
+ self.window_size = window_size
609
+ self.vocoder_padding = vocoder_padding
610
+ self.fmin = fmin
611
+ self.fmax = fmax
612
+ self.num_quantizers = num_quantizers
613
+ self.codebook_size = codebook_size if codebook_size is not None else [1024]
614
+ self.threshold_ema_dead_code = threshold_ema_dead_code
615
+ self.position_embedding_type = position_embedding_type
616
+ self.rope_theta = rope_theta
617
+ self.rope_type = rope_type
618
+ self.ln_type = ln_type
619
+ self.vocoder_attention_heads = vocoder_attention_heads
620
+ self.vocoder_attn_window_size = (
621
+ vocoder_attn_window_size
622
+ if vocoder_attn_window_size is not None
623
+ else [40, 10]
624
+ )
625
+ self.use_istft_only = use_istft_only
626
+ self.hybrid_attention = hybrid_attention
627
+ self.hybrid_block_size = hybrid_block_size
628
+ self.swa_per_block = swa_per_block
629
+
630
+
631
+def get_sequence_mask(inputs, inputs_length):
632
+ if inputs.dim() == 3:
633
+ bsz, tgt_len, _ = inputs.size()
634
+ else:
635
+ bsz, tgt_len = inputs_length.shape[0], torch.max(inputs_length)
636
+ sequence_mask = torch.arange(0, tgt_len).to(inputs.device)
637
+ sequence_mask = torch.lt(sequence_mask, inputs_length.reshape(bsz, 1)).view(
638
+ bsz, tgt_len, 1
639
+ )
640
+ unpacking_index = torch.cumsum(sequence_mask.to(torch.int64).view(-1), dim=0) - 1
641
+ return sequence_mask, unpacking_index
642
+
643
+
644
+def unpack_hidden_states(
645
+ hidden_states, lengths, sequence_mask=None, unpacking_index=None
646
+):
647
+ bsz = lengths.shape[0]
648
+ if sequence_mask is None or unpacking_index is None:
649
+ sequence_mask, unpacking_index = get_sequence_mask(hidden_states, lengths)
650
+ hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(
651
+ bsz, torch.max(lengths), hidden_states.shape[-1]
652
+ )
653
+ return torch.where(sequence_mask, hidden_states, 0)
654
+
655
+
656
+def get_position_ids(lengths):
657
+ total_len = lengths.sum()
658
+ offset = torch.cat([torch.zeros(1).to(lengths), lengths[:-1].cumsum(dim=0)])
659
+ offset = torch.repeat_interleave(offset, lengths)
660
+ return torch.arange(0, total_len).to(offset) - offset
661
+
662
+
663
+LAYER_NORM = {"LayerNorm": nn.LayerNorm}
664
+
665
+
666
+class AudioEncoderAttention(nn.Module):
667
+ def __init__(
668
+ self,
669
+ embed_dim: int,
670
+ num_heads: int,
671
+ window_size: tuple[int, int] = (-1, -1),
672
+ causal: bool = False,
673
+ ):
674
+ super().__init__()
675
+ self.embed_dim = embed_dim
676
+ self.num_heads = num_heads
677
+ self.head_dim = embed_dim // num_heads
678
+ self.window_size = window_size
679
+ self.causal = causal
680
+
681
+ self.k_proj = nn.Linear(embed_dim, embed_dim, bias=False)
682
+ self.v_proj = nn.Linear(embed_dim, embed_dim, bias=True)
683
+ self.q_proj = nn.Linear(embed_dim, embed_dim, bias=True)
684
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=True)
685
+
686
+ def forward(
687
+ self,
688
+ hidden_states: torch.Tensor,
689
+ cu_seqlens: torch.Tensor,
690
+ max_seqlen: int,
691
+ rope_position_embeddings=None,
692
+ ):
693
+ from vllm.vllm_flash_attn import flash_attn_varlen_func
694
+
695
+ bsz, _ = hidden_states.size()
696
+
697
+ query_states = self.q_proj(hidden_states).view(
698
+ bsz, self.num_heads, self.head_dim
699
+ )
700
+ key_states = self.k_proj(hidden_states).view(bsz, self.num_heads, self.head_dim)
701
+ value_states = self.v_proj(hidden_states).view(
702
+ bsz, self.num_heads, self.head_dim
703
+ )
704
+
705
+ if rope_position_embeddings is not None:
706
+ cos, sin = rope_position_embeddings
707
+ query_states, key_states = apply_rotary_pos_emb(
708
+ query_states, key_states, cos, sin
709
+ )
710
+
711
+ attn_output = flash_attn_varlen_func(
712
+ query_states,
713
+ key_states,
714
+ value_states,
715
+ cu_seqlens_q=cu_seqlens,
716
+ cu_seqlens_k=cu_seqlens,
717
+ max_seqlen_q=max_seqlen,
718
+ max_seqlen_k=max_seqlen,
719
+ causal=self.causal,
720
+ window_size=list(self.window_size),
721
+ )
722
+
723
+ attn_output = attn_output.reshape(bsz, self.embed_dim)
724
+ attn_output = self.out_proj(attn_output)
725
+ return attn_output
726
+
727
+
728
+class AudioEncoderTransformerLayer(nn.Module):
729
+ def __init__(
730
+ self,
731
+ config: MiMoAudioTokenizerConfig,
732
+ causal: bool,
733
+ attn_window_size: tuple[int, int] = (-1, -1),
734
+ ):
735
+ super().__init__()
736
+ self.embed_dim = config.d_model
737
+
738
+ self.self_attn = AudioEncoderAttention(
739
+ embed_dim=self.embed_dim,
740
+ num_heads=config.encoder_attention_heads,
741
+ window_size=attn_window_size,
742
+ causal=causal,
743
+ )
744
+ self.self_attn_layer_norm = LAYER_NORM[config.ln_type](self.embed_dim)
745
+
746
+ self.activation_fn = ACT2FN[config.activation_function]
747
+ self.fc1 = nn.Linear(self.embed_dim, config.encoder_ffn_dim)
748
+ self.fc2 = nn.Linear(config.encoder_ffn_dim, self.embed_dim)
749
+ self.final_layer_norm = LAYER_NORM[config.ln_type](self.embed_dim)
750
+
751
+ def forward(
752
+ self,
753
+ hidden_states: torch.Tensor,
754
+ cu_seqlens: torch.Tensor,
755
+ max_seqlen: int,
756
+ rope_position_embeddings: tuple[torch.Tensor, torch.Tensor],
757
+ ) -> torch.Tensor:
758
+ residual = hidden_states
759
+ hidden_states = self.self_attn_layer_norm(hidden_states)
760
+ hidden_states = self.self_attn(
761
+ hidden_states,
762
+ cu_seqlens,
763
+ max_seqlen,
764
+ rope_position_embeddings=rope_position_embeddings,
765
+ )
766
+ hidden_states = residual + hidden_states
767
+
768
+ residual = hidden_states
769
+ hidden_states = self.final_layer_norm(hidden_states)
770
+ hidden_states = self.activation_fn(self.fc1(hidden_states))
771
+ hidden_states = self.fc2(hidden_states)
772
+ hidden_states = residual + hidden_states
773
+
774
+ return hidden_states
775
+
776
+
777
+class AudioEncoder(nn.Module):
778
+ def __init__(
779
+ self,
780
+ config: MiMoAudioTokenizerConfig,
781
+ ):
782
+ super().__init__()
783
+ self.config = config
784
+ self.max_source_positions = (
785
+ config.max_audio_seconds * config.sampling_rate // config.hop_length
786
+ ) // config.stride_size
787
+ self.embed_scale = math.sqrt(config.d_model) if config.scale_embedding else 1.0
788
+ self.skip_layer_idx = config.encoder_skip_layer_id
789
+
790
+ self.conv1 = nn.Conv1d(
791
+ config.n_mels,
792
+ config.d_model,
793
+ kernel_size=config.kernel_size,
794
+ padding=1,
795
+ )
796
+ self.conv2 = nn.Conv1d(
797
+ config.d_model,
798
+ config.d_model,
799
+ kernel_size=config.kernel_size,
800
+ stride=config.stride_size,
801
+ padding=1,
802
+ )
803
+
804
+ self.position_embedding = AudioRotaryEmbedding(
805
+ config.rope_theta,
806
+ config.d_model // config.encoder_attention_heads,
807
+ self.max_source_positions,
808
+ config.rope_type,
809
+ )
810
+
811
+ attn_window_sizes = []
812
+ if config.hybrid_attention:
813
+ for i in range(config.encoder_layers):
814
+ if i % config.swa_per_block < config.swa_per_block - 1:
815
+ attn_window_sizes.append(tuple(config.encoder_attn_window_size))
816
+ else:
817
+ attn_window_sizes.append((-1, -1))
818
+ else:
819
+ attn_window_sizes = [
820
+ tuple(config.encoder_attn_window_size)
821
+ ] * config.encoder_layers
822
+
823
+ self.layers = nn.ModuleList(
824
+ [
825
+ AudioEncoderTransformerLayer(
826
+ config=config,
827
+ causal=config.encoder_causal,
828
+ attn_window_size=attn_window_sizes[i],
829
+ )
830
+ for i in range(config.encoder_layers)
831
+ ]
832
+ )
833
+
834
+ self.layer_norm = LAYER_NORM[config.ln_type](config.d_model)
835
+
836
+ if config.avg_pooler != 1:
837
+ self.down_sample_layer = nn.Sequential(
838
+ nn.Conv1d(
839
+ config.d_model,
840
+ config.d_model,
841
+ config.avg_pooler,
842
+ config.avg_pooler,
843
+ bias=False,
844
+ ),
845
+ nn.GELU(),
846
+ )
847
+ self.down_sample_norm = LAYER_NORM[config.ln_type](config.d_model)
848
+ else:
849
+ self.down_sample_layer = None
850
+
851
+ if config.num_quantizers != 0:
852
+ self.quantizer = ResidualVectorQuantizer(
853
+ dimension=config.d_model,
854
+ n_q=config.num_quantizers,
855
+ bins=config.codebook_size,
856
+ threshold_ema_dead_code=config.threshold_ema_dead_code,
857
+ )
858
+ else:
859
+ self.quantizer = None
860
+
861
+ def get_features(self, input_features, output_length):
862
+ input_features = input_features.to(self.conv1.weight)
863
+ inputs_embeds = nn.functional.gelu(self.conv1(input_features))
864
+ inputs_embeds = nn.functional.gelu(self.conv2(inputs_embeds))
865
+ inputs_embeds = inputs_embeds.permute(0, 2, 1)
866
+ bsz, tgt_len, _ = inputs_embeds.size()
867
+ hidden_states = inputs_embeds
868
+
869
+ position_ids = get_position_ids(output_length).long().to(input_features.device)
870
+ rope_position_embeddings = self.position_embedding(input_features, position_ids)
871
+
872
+ attention_mask, unpacking_index = get_sequence_mask(
873
+ hidden_states, output_length
874
+ )
875
+ hidden_states = torch.masked_select(hidden_states, attention_mask).view(
876
+ torch.sum(output_length), self.config.d_model
877
+ )
878
+
879
+ cu_seqlens = F.pad(
880
+ torch.cumsum(output_length, dim=0), (1, 0), "constant", 0
881
+ ).to(device=hidden_states.device, dtype=torch.int32)
882
+ max_seqlen = torch.max(output_length).to(torch.int32).item()
883
+
884
+ skip_connect_hidden_states = 0.0
885
+ for idx, encoder_layer in enumerate(self.layers):
886
+ hidden_states = encoder_layer(
887
+ hidden_states,
888
+ cu_seqlens,
889
+ max_seqlen,
890
+ rope_position_embeddings=rope_position_embeddings,
891
+ )
892
+ if (self.skip_layer_idx is not None) and idx == self.skip_layer_idx - 1:
893
+ skip_connect_hidden_states = hidden_states.clone()
894
+
895
+ hidden_states += skip_connect_hidden_states
896
+ hidden_states = self.layer_norm(hidden_states)
897
+
898
+ if self.down_sample_layer is not None:
899
+ hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(
900
+ bsz, tgt_len, self.config.d_model
901
+ )
902
+ if hidden_states.size(1) % self.config.avg_pooler:
903
+ pad_len = (
904
+ self.config.avg_pooler
905
+ - hidden_states.size(1) % self.config.avg_pooler
906
+ )
907
+ hidden_states = torch.nn.functional.pad(
908
+ hidden_states, (0, 0, 0, pad_len), mode="constant", value=0.0
909
+ )
910
+ tgt_len += pad_len
911
+ tgt_len = tgt_len // self.config.avg_pooler
912
+ hidden_states = self.down_sample_layer(hidden_states.transpose(1, 2))
913
+ output_length = (
914
+ output_length // self.config.avg_pooler
915
+ + (output_length % self.config.avg_pooler != 0).int()
916
+ )
917
+ hidden_states = hidden_states.transpose(1, 2)
918
+ attention_mask, unpacking_index = get_sequence_mask(
919
+ hidden_states, output_length
920
+ )
921
+ hidden_states = torch.masked_select(hidden_states, attention_mask).view(
922
+ torch.sum(output_length), self.config.d_model
923
+ )
924
+ hidden_states = self.down_sample_norm(hidden_states)
925
+
926
+ return (
927
+ hidden_states,
928
+ output_length,
929
+ attention_mask,
930
+ unpacking_index,
931
+ tgt_len,
932
+ bsz,
933
+ )
934
+
935
+ def get_output_length(self, mel_len):
936
+ tgt_len = mel_len + 3 - self.config.kernel_size
937
+ return (tgt_len + 2 - self.config.kernel_size) // self.config.stride_size + 1
938
+
939
+ @torch.no_grad()
940
+ def encode(
941
+ self,
942
+ input_features,
943
+ input_lens=None,
944
+ output_length=None,
945
+ return_codes_only=False,
946
+ n_q=None,
947
+ use_quantizer=True,
948
+ ):
949
+ if output_length is None:
950
+ output_length = self.get_output_length(input_lens)
951
+ input_features = unpack_hidden_states(input_features, input_lens)
952
+ hidden_states, output_length, attention_mask, unpacking_index, tgt_len, bsz = (
953
+ self.get_features(
954
+ input_features=input_features.transpose(1, 2),
955
+ output_length=output_length,
956
+ )
957
+ )
958
+
959
+ dtype = hidden_states.dtype
960
+ if use_quantizer and self.quantizer is not None:
961
+ self.quantizer.float()
962
+ codes = self.quantizer.encode(hidden_states.float(), n_q=n_q)
963
+ if return_codes_only:
964
+ return codes, output_length
965
+ hidden_states = self.quantizer.decode(codes)
966
+ hidden_states = hidden_states.to(dtype)
967
+ else:
968
+ codes = None
969
+
970
+ hidden_states_packed = hidden_states.clone()
971
+ hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(
972
+ bsz, tgt_len, self.config.d_model
973
+ )
974
+ hidden_states = torch.where(attention_mask, hidden_states, 0)
975
+ return hidden_states, hidden_states_packed, output_length, codes
976
+
977
+ @torch.no_grad()
978
+ def decode_vq(self, codes):
979
+ self.quantizer.float()
980
+ return self.quantizer.decode(codes)
981
+
982
+
983
+class MiMoAudioTokenizer(PreTrainedModel):
984
+ config_class = MiMoAudioTokenizerConfig
985
+
986
+ def __init__(self, config: MiMoAudioTokenizerConfig):
987
+ super().__init__(config)
988
+ self.config = config
989
+ self.sampling_rate = config.sampling_rate
990
+ self.encoder = AudioEncoder(config=config)
991
+ self.downsample_rate = int(config.hop_length * 2 * config.avg_pooler)
992
+
993
+ def get_output_length(self, mel_len):
994
+ tgt_len = mel_len + 3 - self.config.kernel_size
995
+ return (tgt_len + 2 - self.config.kernel_size) // self.config.stride_size + 1
996
+
997
+ @torch.no_grad()
998
+ def encode(self, mels, input_lens, use_quantizer=True):
999
+ input_features = mels
1000
+ encoder_output_length = self.get_output_length(input_lens)
1001
+ hidden_states, hidden_states_packed, encoder_output_length, codes = (
1002
+ self.encoder.encode(
1003
+ input_features, input_lens=input_lens, use_quantizer=use_quantizer
1004
+ )
1005
+ )
1006
+ return hidden_states, hidden_states_packed, encoder_output_length, codes
1007
+
1008
+
1009
+# ---------------------------------------------------------------------------
1010
+# Audio encoding utilities
1011
+# ---------------------------------------------------------------------------
1012
+
1013
+
1014
+def group_by_length(features: torch.Tensor, lengths: torch.Tensor, max_length: int):
1015
+ if features.size(0) != lengths.sum().item():
1016
+ raise ValueError(
1017
+ f"Feature size mismatch: {features.size(0)} vs {lengths.sum().item()}"
1018
+ )
1019
+
1020
+ split_points = []
1021
+ current_sum = 0
1022
+
1023
+ for i, seq_len in enumerate(lengths):
1024
+ if current_sum + seq_len > max_length and current_sum > 0:
1025
+ split_points.append(i)
1026
+ current_sum = seq_len.item()
1027
+ else:
1028
+ current_sum += seq_len.item()
1029
+
1030
+ group_sizes = []
1031
+ prev = 0
1032
+ for point in split_points:
1033
+ group_sizes.append(point - prev)
1034
+ prev = point
1035
+ if prev < len(lengths):
1036
+ group_sizes.append(len(lengths) - prev)
1037
+
1038
+ len_groups = torch.split(lengths, group_sizes)
1039
+ feature_sizes = [group.sum().item() for group in len_groups]
1040
+ feature_groups = torch.split(features, feature_sizes)
1041
+
1042
+ return feature_groups, len_groups
1043
+
1044
+
1045
+@torch.no_grad()
1046
+def encode_batch(
1047
+ audio_tokenizer_encoder,
1048
+ input_features: torch.Tensor,
1049
+ input_lens: torch.Tensor,
1050
+ max_length: int = 256000,
1051
+):
1052
+ feature_groups, len_groups = group_by_length(input_features, input_lens, max_length)
1053
+
1054
+ encoded_parts = []
1055
+ for features, lengths in zip(feature_groups, len_groups):
1056
+ codes, _ = audio_tokenizer_encoder.encode(
1057
+ input_features=features, input_lens=lengths, return_codes_only=True
1058
+ )
1059
+ encoded_parts.append(codes)
1060
+
1061
+ return torch.cat(encoded_parts, dim=-1)
1062
+
1063
+
1064
+def _segment_lengths_for_mel(mel: torch.Tensor, segment_size: int):
1065
+ """Split mel into segments of segment_size with a possible shorter remainder."""
1066
+ input_len = mel.size(0)
1067
+ segs = [segment_size] * (input_len // segment_size)
1068
+ if input_len % segment_size > 0:
1069
+ segs.append(input_len % segment_size)
1070
+ return segs
1071
+
1072
+
1073
+@torch.no_grad()
1074
+def tokenize_audio_batch(mels, audio_tokenizer_encoder, segment_size=6000, device=None):
1075
+ """Tokenize multiple mels in one encode_batch call.
1076
+
1077
+ Returns list of code tensors, each [T_i, C] for that mel.
1078
+ """
1079
+ if not mels:
1080
+ return []
1081
+ if device is None:
1082
+ device = next(audio_tokenizer_encoder.parameters()).device
1083
+ input_len_seg_per_mel = [_segment_lengths_for_mel(m, segment_size) for m in mels]
1084
+ input_lens_flat = [s for segs in input_len_seg_per_mel for s in segs]
1085
+ input_features = torch.cat([m.to(device) for m in mels], dim=0)
1086
+ input_lens_t = torch.tensor(input_lens_flat, dtype=torch.long, device=device)
1087
+ codes_packed = encode_batch(
1088
+ audio_tokenizer_encoder,
1089
+ input_features=input_features,
1090
+ input_lens=input_lens_t,
1091
+ )
1092
+ codes = codes_packed.transpose(0, 1).detach() # [total_code_T, C]
1093
+ code_lengths = []
1094
+ for segs in input_len_seg_per_mel:
1095
+ out_len = audio_tokenizer_encoder.get_output_length(
1096
+ torch.tensor(segs, dtype=torch.long, device=device)
1097
+ )
1098
+ if getattr(audio_tokenizer_encoder, "down_sample_layer", None) is not None:
1099
+ avg = audio_tokenizer_encoder.config.avg_pooler
1100
+ out_len = out_len // avg + (out_len % avg != 0).long()
1101
+ code_lengths.append(out_len.sum().item())
1102
+ code_list = torch.split(codes, code_lengths)
1103
+ return list(code_list)
1104
+
1105
+
1106
+# ---------------------------------------------------------------------------
1107
+# MimoAudioEncoderConfig
1108
+# ---------------------------------------------------------------------------
1109
+
1110
+
1111
+@dataclass
1112
+class MimoAudioEncoderConfig:
1113
+ """Config for MimoAudioEncoder.
1114
+
1115
+ Field names match the audio_config dict in the model checkpoint.
1116
+ """
1117
+
1118
+ speech_vocab_size: str = "1025-1025-129-129-129-129-129-129"
1119
+ speech_zeroemb_idx: str = "1024-1024-128-128-128-128-128-128"
1120
+ group_size: int = 4
1121
+ audio_channels: int = 8
1122
+ input_local_layers: int = 6
1123
+ input_local_dim: int = 1024
1124
+ input_full_attention: bool = True
1125
+ input_local_attn_heads: int = 64
1126
+ input_local_head_dim: int = 16
1127
+ input_local_intermediate_size: int = 4096
1128
+ input_local_hidden_dropout: float = 0.0
1129
+ out_hidden_size: int = 4096
1130
+ rope_theta: float = 640000.0
1131
+ partial_rotary_factor: float = 0.334
1132
+ projection_layers: int = 1
1133
+ add_post_norm: bool = False
1134
+ audio_segment_size: int = 6000
1135
+
1136
+ @classmethod
1137
+ def from_dict(cls, d: dict) -> "MimoAudioEncoderConfig":
1138
+ known = {f.name for f in dataclasses.fields(cls)}
1139
+ return cls(**{k: v for k, v in d.items() if k in known})
1140
+
1141
+
1142
+# ---------------------------------------------------------------------------
1143
+# AudioProjection
1144
+# ---------------------------------------------------------------------------
1145
+
1146
+
1147
+class AudioProjection(nn.Module):
1148
+ def __init__(
1149
+ self,
1150
+ input_size: int,
1151
+ hidden_size: int,
1152
+ output_size: int,
1153
+ ) -> None:
1154
+ super().__init__()
1155
+ self.mlp = nn.Sequential(
1156
+ nn.Linear(input_size, hidden_size, bias=False),
1157
+ nn.GELU(),
1158
+ nn.Linear(hidden_size, output_size, bias=False),
1159
+ )
1160
+
1161
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1162
+ return self.mlp(x)
1163
+
1164
+
1165
+# ---------------------------------------------------------------------------
1166
+# MimoAudioEncoder
1167
+# ---------------------------------------------------------------------------
1168
+
1169
+
1170
+class MimoAudioEncoder(nn.Module):
1171
+ """Audio encoder for MiMo-V2-Omni.
1172
+
1173
+ Encodes mel spectrograms into LLM-compatible embeddings via:
1174
+ 1. Audio tokenizer (VQ codes)
1175
+ 2. Speech embeddings lookup
1176
+ 3. Local Qwen2 transformer
1177
+ 4. Linear projection
1178
+ """
1179
+
1180
+ def __init__(self, config, model_path: str = "") -> None:
1181
+ super().__init__()
1182
+ if isinstance(config, dict):
1183
+ config = MimoAudioEncoderConfig.from_dict(config)
1184
+ self.config = config
1185
+ self.audio_channels = config.audio_channels
1186
+ self.audio_group_size = config.group_size
1187
+ self.audio_segment_size = config.audio_segment_size
1188
+
1189
+ speech_vocab_sizes = self._parse_maybe_list(
1190
+ config.speech_vocab_size, config.audio_channels
1191
+ )
1192
+ speech_empty_ids = self._parse_maybe_list(
1193
+ config.speech_zeroemb_idx, config.audio_channels
1194
+ )
1195
+
1196
+ input_local_config = Qwen2Config(
1197
+ hidden_size=config.input_local_dim,
1198
+ num_hidden_layers=config.input_local_layers,
1199
+ num_attention_heads=config.input_local_attn_heads,
1200
+ num_key_value_heads=config.input_local_attn_heads,
1201
+ intermediate_size=config.input_local_intermediate_size,
1202
+ attention_dropout=config.input_local_hidden_dropout,
1203
+ rope_theta=config.rope_theta,
1204
+ partial_rotary_factor=config.partial_rotary_factor,
1205
+ )
1206
+
1207
+ self.input_local_transformer = Qwen2Model(input_local_config)
1208
+
1209
+ if not config.add_post_norm:
1210
+ self.input_local_transformer.norm = nn.Identity()
1211
+
1212
+ self.speech_embeddings = nn.ModuleList(
1213
+ [
1214
+ nn.Embedding(
1215
+ speech_vocab_sizes[i],
1216
+ config.input_local_dim,
1217
+ padding_idx=speech_empty_ids[i],
1218
+ )
1219
+ for i in range(config.audio_channels)
1220
+ ]
1221
+ )
1222
+
1223
+ if config.projection_layers == 1:
1224
+ self.projection = nn.Linear(
1225
+ config.input_local_dim * config.group_size,
1226
+ config.out_hidden_size,
1227
+ bias=False,
1228
+ )
1229
+ elif config.projection_layers == 2:
1230
+ self.projection = AudioProjection(
1231
+ config.input_local_dim * config.group_size,
1232
+ config.input_local_dim * config.group_size * 4,
1233
+ config.out_hidden_size,
1234
+ )
1235
+ else:
1236
+ raise ValueError(f"Invalid projection_layers: {config.projection_layers}")
1237
+
1238
+ self.audio_tokenizer: MiMoAudioTokenizer | None = None
1239
+ if model_path:
1240
+ audio_tokenizer_path = os.path.join(model_path, "audio_tokenizer")
1241
+ if os.path.exists(audio_tokenizer_path):
1242
+ dev = torch.get_default_device()
1243
+ self.audio_tokenizer = self._load_audio_tokenizer(
1244
+ audio_tokenizer_path, dev
1245
+ )
1246
+ else:
1247
+ logger.warning(
1248
+ "Audio tokenizer not found at %s, audio encoding disabled",
1249
+ audio_tokenizer_path,
1250
+ )
1251
+
1252
+ @staticmethod
1253
+ def _load_audio_tokenizer(path: str, device: torch.device) -> MiMoAudioTokenizer:
1254
+ """Load MiMoAudioTokenizer from directory."""
1255
+ from safetensors.torch import load_file
1256
+
1257
+ config_path = os.path.join(path, "config.json")
1258
+ with open(config_path) as f:
1259
+ config_dict = json.load(f)
1260
+ config = MiMoAudioTokenizer.config_class(**config_dict)
1261
+ model = MiMoAudioTokenizer(config)
1262
+ safetensors_path = os.path.join(path, "model.safetensors")
1263
+ bin_path = os.path.join(path, "pytorch_model.bin")
1264
+ if os.path.exists(safetensors_path):
1265
+ state_dict = load_file(safetensors_path, device="cpu")
1266
+ elif os.path.exists(bin_path):
1267
+ state_dict = torch.load(bin_path, map_location="cpu", weights_only=True)
1268
+ else:
1269
+ raise FileNotFoundError(
1270
+ f"No model weights found in {path} "
1271
+ "(expected model.safetensors or pytorch_model.bin)"
1272
+ )
1273
+ model.load_state_dict(state_dict, strict=False)
1274
+ model = model.to(device=device, dtype=torch.bfloat16)
1275
+ model.eval()
1276
+ model.requires_grad_(False)
1277
+ return model
1278
+
1279
+ def _parse_maybe_list(self, value, length: int) -> list[int]:
1280
+ if isinstance(value, str) and "-" in value:
1281
+ return [int(s) for s in value.split("-")]
1282
+ return [int(value)] * length
1283
+
1284
+ def apply_input_local_transformer(self, speech_embeddings: torch.Tensor):
1285
+ output = self.input_local_transformer(
1286
+ inputs_embeds=speech_embeddings,
1287
+ return_dict=True,
1288
+ is_causal=not self.config.input_full_attention,
1289
+ )
1290
+ return output.last_hidden_state
1291
+
1292
+ def apply_speech_embeddings(self, audio_codes: torch.Tensor) -> torch.Tensor:
1293
+ num_segments = audio_codes.shape[0]
1294
+ _audio_embeddings = torch.zeros(
1295
+ (num_segments, self.config.group_size, self.config.input_local_dim),
1296
+ dtype=next(self.speech_embeddings[0].parameters()).dtype,
1297
+ device=audio_codes.device,
1298
+ )
1299
+ for i in range(self.config.audio_channels):
1300
+ _audio_embeddings.add_(self.speech_embeddings[i](audio_codes[:, :, i]))
1301
+ return _audio_embeddings
1302
+
1303
+ def process_audio(self, audio: torch.Tensor) -> torch.Tensor:
1304
+ """Pad audio codes to group_size boundary.
1305
+
1306
+ Args:
1307
+ audio: [T, audio_channels] code tensor
1308
+
1309
+ Returns:
1310
+ [T//group_size, group_size, audio_channels]
1311
+ """
1312
+ T = audio.shape[0]
1313
+ audio = audio[:, : self.audio_channels]
1314
+ padded_T = (
1315
+ (T + self.audio_group_size - 1)
1316
+ // self.audio_group_size
1317
+ * self.audio_group_size
1318
+ )
1319
+ padded_audio = torch.cat(
1320
+ [
1321
+ audio,
1322
+ torch.zeros(
1323
+ padded_T - T,
1324
+ self.audio_channels,
1325
+ dtype=torch.int32,
1326
+ device=audio.device,
1327
+ )
1328
+ + audio[-1, :],
1329
+ ],
1330
+ dim=0,
1331
+ )
1332
+ padded_audio = padded_audio.reshape(
1333
+ padded_T // self.audio_group_size,
1334
+ self.audio_group_size,
1335
+ self.audio_channels,
1336
+ )
1337
+ return padded_audio
1338
+
1339
+ def get_audio_feature(
1340
+ self, mel_specs: list[torch.Tensor]
1341
+ ) -> tuple[torch.Tensor, list[int]]:
1342
+ """Encode mel spectrograms into LLM embedding space.
1343
+
1344
+ Args:
1345
+ mel_specs: list of mel spectrogram tensors, each [T, n_mels]
1346
+
1347
+ Returns:
1348
+ Tuple of:
1349
+ - audio_embeds: [total_tokens, out_hidden_size] concatenated embeddings
1350
+ - item_token_lens: list of int, number of tokens per input item
1351
+ """
1352
+ if self.audio_tokenizer is None:
1353
+ raise RuntimeError(
1354
+ "audio_tokenizer is not loaded. "
1355
+ "Ensure model_path points to a directory containing audio_tokenizer/."
1356
+ )
1357
+
1358
+ if not mel_specs:
1359
+ device = next(self.projection.parameters()).device
1360
+ dtype = next(self.projection.parameters()).dtype
1361
+ return (
1362
+ torch.empty(0, self.config.out_hidden_size, device=device, dtype=dtype),
1363
+ [],
1364
+ )
1365
+
1366
+ device = next(self.audio_tokenizer.encoder.parameters()).device
1367
+ code_list = tokenize_audio_batch(
1368
+ mel_specs,
1369
+ self.audio_tokenizer.encoder,
1370
+ segment_size=self.audio_segment_size,
1371
+ device=device,
1372
+ )
1373
+
1374
+ item_token_lens: list[int] = []
1375
+ codecs_to_concat = []
1376
+ for codecs in code_list:
1377
+ padded_codes = self.process_audio(codecs)
1378
+ codecs_to_concat.append(padded_codes)
1379
+ item_token_lens.append(padded_codes.shape[0])
1380
+
1381
+ audio_codes = torch.cat(
1382
+ codecs_to_concat, dim=0
1383
+ ) # [total_T//group_size, group_size, audio_channels]
1384
+
1385
+ _audio_embeddings = self.apply_speech_embeddings(audio_codes)
1386
+ audio_embeds = self.apply_input_local_transformer(_audio_embeddings)
1387
+ B = audio_embeds.shape[0]
1388
+ audio_embeds = self.projection(audio_embeds.reshape(B, -1))
1389
+ return audio_embeds, item_token_lens
vllm/model_executor/models/mimo_v2.py
renamed
+22
-2
@@ -6,6 +6,7 @@ from itertools import islice
6
import torch
7
from torch import nn
8
9
+from vllm.compilation.decorators import support_torch_compile
10
from vllm.config import (
11
CacheConfig,
12
VllmConfig,
@@ -268,7 +269,7 @@ class MiMoV2Attention(nn.Module):
269
self.total_num_heads * self.v_head_dim,
270
hidden_size,
271
bias=False,
271
- quant_config=quant_config,
272
+ quant_config=quant_config if "mtp.layers" not in prefix else None,
273
reduce_results=True,
274
prefix=f"{prefix}.o_proj",
275
)
@@ -440,6 +441,7 @@ class MiMoV2FlashDecoderLayer(nn.Module):
441
return self.config.hybrid_layer_pattern[self.layer_id] == 1
442
443
444
+@support_torch_compile
445
class MiMoV2Model(nn.Module):
446
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
447
super().__init__()
@@ -603,7 +605,13 @@ class MiMoV2Model(nn.Module):
605
606
if expert_matched:
607
continue
606
-
608
+ # Support fused qkv_proj checkpoint (Pro format)
609
+ if "qkv_proj" in name:
610
+ if name in params_dict:
611
+ param = params_dict[name]
612
+ loaded_weight = loaded_weight.chunk(tp_size, dim=0)[tp_rank]
613
+ default_weight_loader(param, loaded_weight)
614
+ continue
615
stacked_matched = False
616
for param_name, weight_name, shard_id in stacked_params_mapping:
617
if weight_name not in name:
@@ -662,6 +670,11 @@ class MiMoV2Model(nn.Module):
670
671
672
class MiMoV2FlashForCausalLM(nn.Module, SupportsPP, MixtureOfExperts):
673
+ packed_modules_mapping = {
674
+ "qkv_proj": ["q_proj", "k_proj", "v_proj"],
675
+ "gate_up_proj": ["gate_proj", "up_proj"],
676
+ }
677
+
678
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
679
super().__init__()
680
config = vllm_config.model_config.hf_config
@@ -718,3 +731,10 @@ class MiMoV2FlashForCausalLM(nn.Module, SupportsPP, MixtureOfExperts):
731
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
732
loader = AutoWeightsLoader(self)
733
return loader.load_weights(weights)
734
+
735
+
736
+class MiMoV2ProForCausalLM(MiMoV2FlashForCausalLM):
737
+ packed_modules_mapping = {
738
+ "qkv_proj": ["qkv_proj"],
739
+ "gate_up_proj": ["gate_proj", "up_proj"],
740
+ }
vllm/model_executor/models/mimo_v2_mtp.py
new
+373
@@ -0,0 +1,373 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+"""Inference-only MiMo-V2 MTP (Multi-Token Prediction) draft model.
5
+
6
+Supports both MiMo-V2-Pro and MiMo-V2-Flash checkpoints.
7
+
8
+Checkpoint weight layout (model.mtp.layers.{idx}.*):
9
+ enorm - RMSNorm for token embeddings
10
+ hnorm - RMSNorm for previous hidden states
11
+ eh_proj - ReplicatedLinear(hidden*2 -> hidden)
12
+ input_layernorm - pre-attention RMSNorm
13
+ self_attn.* - attention weights; format differs by variant:
14
+ Pro: fused qkv_proj [Q;K;V] concatenated
15
+ Flash: separate q_proj, k_proj, v_proj
16
+ pre_mlp_layernorm - post-attention / pre-MLP RMSNorm
17
+ mlp.* - dense MLP (gate_proj / up_proj / down_proj)
18
+ final_layernorm - norm applied before logit computation
19
+"""
20
+
21
+from collections.abc import Iterable
22
+
23
+import torch
24
+import torch.nn as nn
25
+from transformers import PretrainedConfig
26
+
27
+from vllm.config import VllmConfig
28
+from vllm.distributed import (
29
+ get_tensor_model_parallel_rank,
30
+ get_tensor_model_parallel_world_size,
31
+)
32
+from vllm.model_executor.layers.layernorm import RMSNorm
33
+from vllm.model_executor.layers.linear import ReplicatedLinear
34
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
35
+from vllm.model_executor.layers.quantization import QuantizationConfig
36
+from vllm.model_executor.layers.vocab_parallel_embedding import (
37
+ ParallelLMHead,
38
+ VocabParallelEmbedding,
39
+)
40
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
41
+from vllm.sequence import IntermediateTensors
42
+
43
+from .interfaces import (
44
+ MultiModalEmbeddings,
45
+ SupportsMultiModal,
46
+ _require_is_multimodal,
47
+)
48
+from .mimo_v2 import MiMoV2Attention, MiMoV2MLP
49
+from .utils import _merge_multimodal_embeddings, maybe_prefix
50
+
51
+# MiMo-V2 checkpoints contain multiple MTP layers, but vLLM currently supports
52
+# only the first layer and only one speculative token.
53
+_MIMO_V2_PRO_NUM_MTP_LAYERS = 1
54
+_MIMO_V2_FLASH_NUM_MTP_LAYERS = 1
55
+
56
+
57
+class MiMoV2MTPLayer(nn.Module):
58
+ """Single MTP predictor layer for MiMo-V2 (Pro and Flash).
59
+
60
+ Mirrors the single-layer MiMo-V2 nextn reference implementation.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ config: PretrainedConfig,
66
+ prefix: str,
67
+ quant_config: QuantizationConfig | None = None,
68
+ ) -> None:
69
+ super().__init__()
70
+
71
+ # Predictor head components
72
+ self.enorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
73
+ self.hnorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
74
+ self.eh_proj = ReplicatedLinear(
75
+ config.hidden_size * 2, config.hidden_size, bias=False
76
+ )
77
+
78
+ # MTP uses the SWA attention configuration
79
+ # implementation.
80
+ swa_rope_theta = getattr(
81
+ config,
82
+ "swa_rope_theta",
83
+ getattr(config, "rope_theta", 1000000),
84
+ )
85
+ sliding_window_size = getattr(config, "sliding_window_size", -1)
86
+
87
+ self.input_layernorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
88
+ self.self_attn = MiMoV2Attention(
89
+ hidden_size=config.hidden_size,
90
+ num_heads=config.swa_num_attention_heads,
91
+ num_kv_heads=config.swa_num_key_value_heads,
92
+ head_dim=config.swa_head_dim,
93
+ v_head_dim=getattr(config, "swa_v_head_dim", None),
94
+ v_scale=getattr(config, "attention_value_scale", None),
95
+ sliding_window_size=sliding_window_size,
96
+ attention_bias=config.attention_bias,
97
+ add_swa_attention_sink_bias=getattr(
98
+ config, "add_swa_attention_sink_bias", False
99
+ ),
100
+ layer_id=0,
101
+ rope_theta=swa_rope_theta,
102
+ max_position_embeddings=getattr(config, "max_position_embeddings", 32768),
103
+ quant_config=quant_config,
104
+ partial_rotary_factor=getattr(config, "partial_rotary_factor", 1.0),
105
+ prefix=f"{prefix}.self_attn",
106
+ )
107
+ self.pre_mlp_layernorm = RMSNorm(
108
+ config.hidden_size, eps=config.layernorm_epsilon
109
+ )
110
+ self.mlp = MiMoV2MLP(
111
+ hidden_size=config.hidden_size,
112
+ intermediate_size=config.intermediate_size,
113
+ hidden_act=config.hidden_act,
114
+ quant_config=quant_config,
115
+ prefix=f"{prefix}.mlp",
116
+ )
117
+ self.final_layernorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
118
+
119
+ def forward(
120
+ self,
121
+ inputs_embeds: torch.Tensor,
122
+ positions: torch.Tensor,
123
+ previous_hidden_states: torch.Tensor,
124
+ ) -> torch.Tensor:
125
+ # Combine token embedding and previous hidden state
126
+ h, _ = self.eh_proj(
127
+ torch.cat(
128
+ [self.enorm(inputs_embeds), self.hnorm(previous_hidden_states)], dim=-1
129
+ )
130
+ )
131
+
132
+ # Transformer block with fused residual norms
133
+ residual = h
134
+ h = self.input_layernorm(h)
135
+ h = self.self_attn(positions=positions, hidden_states=h)
136
+ h, residual = self.pre_mlp_layernorm(h, residual)
137
+ h = self.mlp(h)
138
+ h = h + residual
139
+
140
+ return self.final_layernorm(h)
141
+
142
+
143
+class _MiMoV2MTPLayers(nn.Module):
144
+ """Thin wrapper so parameter paths match checkpoint: model.mtp.layers.*"""
145
+
146
+ def __init__(
147
+ self,
148
+ config: PretrainedConfig,
149
+ num_mtp_layers: int,
150
+ quant_config: QuantizationConfig | None,
151
+ prefix: str,
152
+ ) -> None:
153
+ super().__init__()
154
+ self.layers = nn.ModuleDict(
155
+ {
156
+ str(i): MiMoV2MTPLayer(
157
+ config=config,
158
+ prefix=f"{prefix}.{i}",
159
+ quant_config=quant_config,
160
+ )
161
+ for i in range(num_mtp_layers)
162
+ }
163
+ )
164
+
165
+
166
+class MiMoV2MultiTokenPredictor(nn.Module):
167
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
168
+ super().__init__()
169
+
170
+ config = vllm_config.model_config.hf_config
171
+ spec_cfg = vllm_config.speculative_config
172
+ assert spec_cfg is not None
173
+ if spec_cfg.num_speculative_tokens != 1:
174
+ raise ValueError(
175
+ "MiMo-V2 MTP in vLLM only supports num_speculative_tokens=1."
176
+ )
177
+ num_mtp_layers = 1
178
+
179
+ self.num_mtp_layers = num_mtp_layers
180
+
181
+ self.embed_tokens = VocabParallelEmbedding(
182
+ config.vocab_size,
183
+ config.hidden_size,
184
+ )
185
+
186
+ self.mtp = _MiMoV2MTPLayers(
187
+ config=config,
188
+ num_mtp_layers=num_mtp_layers,
189
+ quant_config=vllm_config.quant_config,
190
+ prefix=maybe_prefix(prefix, "mtp.layers"),
191
+ )
192
+
193
+ self.logits_processor = LogitsProcessor(config.vocab_size)
194
+
195
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
196
+ return self.embed_tokens(input_ids)
197
+
198
+ def forward(
199
+ self,
200
+ input_ids: torch.Tensor,
201
+ positions: torch.Tensor,
202
+ previous_hidden_states: torch.Tensor,
203
+ inputs_embeds: torch.Tensor | None = None,
204
+ spec_step_idx: int = 0,
205
+ ) -> torch.Tensor:
206
+ assert spec_step_idx == 0, "MiMo-V2 MTP only supports one speculative token."
207
+ if inputs_embeds is None:
208
+ inputs_embeds = self.embed_input_ids(input_ids)
209
+ return self.mtp.layers[str(spec_step_idx)](
210
+ inputs_embeds, positions, previous_hidden_states
211
+ )
212
+
213
+ def compute_logits(
214
+ self,
215
+ hidden_states: torch.Tensor,
216
+ lm_head: ParallelLMHead,
217
+ spec_step_idx: int = 0,
218
+ ) -> torch.Tensor:
219
+ assert spec_step_idx == 0, "MiMo-V2 MTP only supports one speculative token."
220
+ return self.logits_processor(lm_head, hidden_states)
221
+
222
+
223
+class MiMoV2MTP(nn.Module):
224
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
225
+ super().__init__()
226
+ self.config = vllm_config.model_config.hf_config
227
+ self.model = MiMoV2MultiTokenPredictor(
228
+ vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
229
+ )
230
+ self.lm_head = ParallelLMHead(
231
+ self.config.vocab_size,
232
+ self.config.hidden_size,
233
+ prefix=maybe_prefix(prefix, "lm_head"),
234
+ )
235
+
236
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
237
+ return self.model.embed_input_ids(input_ids)
238
+
239
+ def forward(
240
+ self,
241
+ input_ids: torch.Tensor | None,
242
+ positions: torch.Tensor,
243
+ hidden_states: torch.Tensor,
244
+ intermediate_tensors: IntermediateTensors | None = None,
245
+ inputs_embeds: torch.Tensor | None = None,
246
+ spec_step_idx: int = 0,
247
+ ) -> torch.Tensor:
248
+ assert spec_step_idx == 0, "MiMo-V2 MTP only supports one speculative token."
249
+ return self.model(
250
+ input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
251
+ )
252
+
253
+ def compute_logits(
254
+ self,
255
+ hidden_states: torch.Tensor,
256
+ spec_step_idx: int = 0,
257
+ ) -> torch.Tensor | None:
258
+ assert spec_step_idx == 0, "MiMo-V2 MTP only supports one speculative token."
259
+ return self.model.compute_logits(hidden_states, self.lm_head, spec_step_idx)
260
+
261
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
262
+ tp_rank = get_tensor_model_parallel_rank()
263
+ tp_size = get_tensor_model_parallel_world_size()
264
+
265
+ stacked_params_mapping = [
266
+ ("gate_up_proj", "gate_proj", 0),
267
+ ("gate_up_proj", "up_proj", 1),
268
+ # Flash format: separate projections → fused qkv_proj
269
+ ("qkv_proj", "q_proj", "q"),
270
+ ("qkv_proj", "k_proj", "k"),
271
+ ("qkv_proj", "v_proj", "v"),
272
+ ]
273
+
274
+ params_dict = dict(self.named_parameters())
275
+ loaded_params: set[str] = set()
276
+
277
+ for name, loaded_weight in weights:
278
+ if "rotary_emb.inv_freq" in name:
279
+ continue
280
+
281
+ # Only load MTP-related weights, shared embeddings, and lm_head
282
+ if (
283
+ "model.mtp" not in name
284
+ and "model.embed_tokens" not in name
285
+ and not name.startswith("lm_head")
286
+ ):
287
+ continue
288
+
289
+ # Support fused qkv_proj checkpoint (Pro format).
290
+ # The checkpoint is stored pre-sharded for TP=8 as
291
+ # [Q_rank0, K_rank0, V_rank0, Q_rank1, ...], so splitting along
292
+ # dim 0 with chunk(tp_size) gives each rank its Q+K+V slice for
293
+ # both the FP8 weight and the block weight_scale_inv. This matches
294
+ # how the main model loads the same layout.
295
+ if "qkv_proj" in name:
296
+ if name in params_dict:
297
+ param = params_dict[name]
298
+ loaded_weight = loaded_weight.chunk(tp_size, dim=0)[tp_rank]
299
+ default_weight_loader(param, loaded_weight)
300
+ loaded_params.add(name)
301
+ continue
302
+
303
+ # gate_proj/up_proj → gate_up_proj stacking (both formats);
304
+ # Flash: q_proj/k_proj/v_proj → qkv_proj merging.
305
+ stacked_matched = False
306
+ for param_name, weight_name, shard_id in stacked_params_mapping:
307
+ if weight_name not in name:
308
+ continue
309
+ name_rewritten = name.replace(weight_name, param_name)
310
+ if (
311
+ name_rewritten.endswith(".bias")
312
+ and name_rewritten not in params_dict
313
+ ):
314
+ continue
315
+ if name_rewritten not in params_dict:
316
+ continue
317
+ param = params_dict[name_rewritten]
318
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
319
+ weight_loader(param, loaded_weight, shard_id)
320
+ loaded_params.add(name_rewritten)
321
+ stacked_matched = True
322
+ break
323
+
324
+ if stacked_matched:
325
+ continue
326
+
327
+ if name.endswith(".bias") and name not in params_dict:
328
+ continue
329
+ if name not in params_dict:
330
+ continue
331
+
332
+ param = params_dict[name]
333
+ # attention_sink_bias is head-parallel; slice by tp
334
+ if "attention_sink_bias" in name:
335
+ total_heads = loaded_weight.shape[0]
336
+ heads_per_rank = total_heads // tp_size
337
+ loaded_weight = loaded_weight.narrow(
338
+ 0, tp_rank * heads_per_rank, heads_per_rank
339
+ )
340
+
341
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
342
+ weight_loader(param, loaded_weight)
343
+ loaded_params.add(name)
344
+
345
+ return loaded_params
346
+
347
+
348
+class MiMoV2OmniMTP(MiMoV2MTP, SupportsMultiModal):
349
+ def embed_input_ids(
350
+ self,
351
+ input_ids: torch.Tensor,
352
+ multimodal_embeddings: MultiModalEmbeddings | None = None,
353
+ *,
354
+ is_multimodal: torch.Tensor | None = None,
355
+ ) -> torch.Tensor:
356
+ inputs_embeds = self._embed_text_input_ids(
357
+ input_ids,
358
+ self.model.embed_input_ids,
359
+ is_multimodal=is_multimodal,
360
+ )
361
+
362
+ if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
363
+ return inputs_embeds
364
+
365
+ is_multimodal = _require_is_multimodal(is_multimodal)
366
+
367
+ inputs_embeds = _merge_multimodal_embeddings(
368
+ inputs_embeds=inputs_embeds,
369
+ multimodal_embeddings=multimodal_embeddings,
370
+ is_multimodal=is_multimodal,
371
+ )
372
+
373
+ return inputs_embeds
vllm/model_executor/models/mimo_v2_omni.py
new
+1488
@@ -0,0 +1,1488 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import math
4
+from collections.abc import Callable, Iterable, Mapping, Sequence
5
+from functools import partial
6
+from typing import Any
7
+
8
+import einops
9
+import numpy as np
10
+import torch
11
+import torch.nn as nn
12
+import torch.nn.functional as F
13
+from transformers import BatchFeature, PretrainedConfig
14
+from transformers.models.qwen2_vl.image_processing_qwen2_vl import smart_resize
15
+
16
+from vllm.config import VllmConfig
17
+from vllm.config.multimodal import BaseDummyOptions
18
+from vllm.distributed import parallel_state
19
+from vllm.distributed import utils as dist_utils
20
+from vllm.inputs import MultiModalDataDict
21
+from vllm.model_executor.layers.activation import get_act_and_mul_fn
22
+from vllm.model_executor.layers.attention import MMEncoderAttention
23
+from vllm.model_executor.layers.layernorm import RMSNorm
24
+from vllm.model_executor.layers.linear import (
25
+ ColumnParallelLinear,
26
+ QKVParallelLinear,
27
+ RowParallelLinear,
28
+)
29
+from vllm.model_executor.layers.quantization import QuantizationConfig
30
+from vllm.model_executor.layers.rotary_embedding import get_rope
31
+from vllm.model_executor.layers.rotary_embedding.common import ApplyRotaryEmb
32
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
33
+from vllm.model_executor.models.vision import is_vit_use_data_parallel
34
+from vllm.multimodal import MULTIMODAL_REGISTRY
35
+from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
36
+from vllm.multimodal.parse import ImageSize, MultiModalDataItems
37
+from vllm.multimodal.processing import (
38
+ BaseDummyInputsBuilder,
39
+ BaseMultiModalProcessor,
40
+ BaseProcessingInfo,
41
+ PromptReplacement,
42
+ PromptUpdate,
43
+ PromptUpdateDetails,
44
+)
45
+from vllm.transformers_utils.configs.mimo_v2_omni import Mimo_VLVisionConfig
46
+from vllm.transformers_utils.processors.mimo_v2_omni import (
47
+ MiMoOmniProcessor,
48
+ VideoAudioInput,
49
+ _format_timestamp,
50
+)
51
+
52
+from .interfaces import (
53
+ MultiModalEmbeddings,
54
+ SupportsMultiModal,
55
+ SupportsPP,
56
+ SupportsQuant,
57
+)
58
+from .mimo_audio import MimoAudioEncoder
59
+from .mimo_v2 import MiMoV2FlashForCausalLM
60
+from .qwen2_5_vl import (
61
+ Qwen2_5_VisionMLP,
62
+ Qwen2_5_VisionPatchEmbed,
63
+ Qwen2_5_VLImageEmbeddingInputs,
64
+ Qwen2_5_VLImageInputs,
65
+ Qwen2_5_VLImagePixelInputs,
66
+ Qwen2_5_VLVideoEmbeddingInputs,
67
+ Qwen2_5_VLVideoInputs,
68
+ Qwen2_5_VLVideoPixelInputs,
69
+)
70
+from .qwen2_vl import _create_qwen2vl_field_factory
71
+from .utils import AutoWeightsLoader, IntermediateTensors, WeightsMapper, maybe_prefix
72
+
73
+
74
+class MiMoVisionMLP(Qwen2_5_VisionMLP):
75
+ pass
76
+
77
+
78
+class MiMoVisionPatchEmbed(Qwen2_5_VisionPatchEmbed):
79
+ pass
80
+
81
+
82
+class MiMoVisionPatchMerger(nn.Module):
83
+ def __init__(
84
+ self,
85
+ d_model: int,
86
+ context_dim: int,
87
+ norm_layer: Callable[[int], nn.Module] | None = None,
88
+ spatial_merge_size: int = 2,
89
+ quant_config: QuantizationConfig | None = None,
90
+ prefix: str = "",
91
+ ) -> None:
92
+ super().__init__()
93
+ use_data_parallel = is_vit_use_data_parallel()
94
+ self.hidden_size = context_dim * (spatial_merge_size**2)
95
+ if norm_layer is None:
96
+ norm_layer = partial(nn.LayerNorm, eps=1e-6)
97
+ self.ln_q = norm_layer(context_dim)
98
+
99
+ self.mlp = nn.Sequential(
100
+ ColumnParallelLinear(
101
+ self.hidden_size,
102
+ self.hidden_size,
103
+ bias=False,
104
+ quant_config=quant_config,
105
+ prefix=f"{prefix}.mlp.0",
106
+ return_bias=False,
107
+ disable_tp=use_data_parallel,
108
+ ),
109
+ nn.GELU(),
110
+ RowParallelLinear(
111
+ self.hidden_size,
112
+ d_model,
113
+ bias=False,
114
+ quant_config=quant_config,
115
+ prefix=f"{prefix}.mlp.2",
116
+ return_bias=False,
117
+ disable_tp=use_data_parallel,
118
+ ),
119
+ )
120
+
121
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
122
+ x = self.ln_q(x)
123
+ x = x.view(-1, self.hidden_size)
124
+ out = self.mlp(x)
125
+ return out
126
+
127
+
128
+class MiMoVisionAttention(nn.Module):
129
+ def __init__(
130
+ self,
131
+ embed_dim: int,
132
+ num_heads: int,
133
+ num_kv_heads: int,
134
+ qk_channels: int,
135
+ kv_channels: int,
136
+ use_sink: bool = False,
137
+ visual_token_window_size: int = 64,
138
+ quant_config: QuantizationConfig | None = None,
139
+ prefix: str = "",
140
+ ) -> None:
141
+ super().__init__()
142
+ use_data_parallel = is_vit_use_data_parallel()
143
+ self.tp_size = (
144
+ 1
145
+ if use_data_parallel
146
+ else parallel_state.get_tensor_model_parallel_world_size()
147
+ )
148
+ self.tp_rank = parallel_state.get_tensor_model_parallel_rank()
149
+
150
+ self.num_heads = num_heads
151
+ self.num_kv_heads = num_kv_heads
152
+ self.qk_channels = qk_channels
153
+ self.kv_channels = kv_channels
154
+ self.embed_dim = embed_dim
155
+
156
+ self.num_heads_per_partition = dist_utils.divide(num_heads, self.tp_size)
157
+ self.num_kv_heads_per_partition = dist_utils.divide(num_kv_heads, self.tp_size)
158
+
159
+ # Attention scale uses the Q/K head dimension (qk_channels)
160
+ self.scale = qk_channels**-0.5
161
+
162
+ # QKV: Q is (num_heads * qk_channels), KV are (num_kv_heads * kv_channels)
163
+ self.qkv = QKVParallelLinear(
164
+ hidden_size=embed_dim,
165
+ head_size=qk_channels,
166
+ total_num_heads=num_heads,
167
+ total_num_kv_heads=num_kv_heads,
168
+ v_head_size=kv_channels,
169
+ bias=True,
170
+ quant_config=quant_config,
171
+ prefix=f"{prefix}.qkv",
172
+ disable_tp=use_data_parallel,
173
+ )
174
+
175
+ # Output projection: input is (num_heads * kv_channels) after attention
176
+ self.proj = RowParallelLinear(
177
+ input_size=num_heads * kv_channels,
178
+ output_size=embed_dim,
179
+ bias=True,
180
+ quant_config=quant_config,
181
+ prefix=f"{prefix}.proj",
182
+ disable_tp=use_data_parallel,
183
+ )
184
+
185
+ # For full attention (non-window blocks)
186
+ self.attn = MMEncoderAttention(
187
+ num_heads=self.num_heads_per_partition,
188
+ head_size=kv_channels,
189
+ scale=self.scale,
190
+ num_kv_heads=self.num_kv_heads_per_partition,
191
+ prefix=f"{prefix}.attn",
192
+ )
193
+
194
+ # Rotary embeddings applied separately to Q and K
195
+ self.apply_rotary_emb = ApplyRotaryEmb(enforce_enable=True)
196
+
197
+ # Sink attention weights (loaded but not used in vLLM flash_attn)
198
+ # The checkpoint stores these only for non-full-attention blocks
199
+ self.use_sink = use_sink
200
+ if use_sink:
201
+ self.sinks = nn.Parameter(
202
+ torch.empty(num_heads),
203
+ requires_grad=False,
204
+ )
205
+ else:
206
+ self.sinks = None
207
+
208
+ self.visual_token_window_size = visual_token_window_size
209
+
210
+ def _forward_window_attn(
211
+ self,
212
+ q: torch.Tensor,
213
+ k: torch.Tensor,
214
+ v: torch.Tensor,
215
+ cu_seqlens: torch.Tensor,
216
+ max_seqlen: torch.Tensor,
217
+ ) -> torch.Tensor:
218
+ """Window attention via flash_attn_varlen_func with window_size."""
219
+ from vllm.vllm_flash_attn import flash_attn_varlen_func
220
+
221
+ w = self.visual_token_window_size
222
+ output = flash_attn_varlen_func(
223
+ q,
224
+ k,
225
+ v,
226
+ cu_seqlens_q=cu_seqlens,
227
+ cu_seqlens_k=cu_seqlens,
228
+ max_seqlen_q=max_seqlen,
229
+ max_seqlen_k=max_seqlen,
230
+ softmax_scale=self.scale,
231
+ causal=False,
232
+ window_size=[w, w],
233
+ )
234
+ return output
235
+
236
+ def forward(
237
+ self,
238
+ x: torch.Tensor,
239
+ cu_seqlens: torch.Tensor,
240
+ rotary_pos_emb_cos: torch.Tensor,
241
+ rotary_pos_emb_sin: torch.Tensor,
242
+ max_seqlen: torch.Tensor,
243
+ full_attn: bool = True,
244
+ ) -> torch.Tensor:
245
+ """
246
+ Args:
247
+ x: [seq_len, batch=1, embed_dim] (seq-first convention)
248
+ cu_seqlens: cumulative sequence lengths [num_seqs+1], int32
249
+ rotary_pos_emb_cos: [seq_len, qk_channels // 2]
250
+ rotary_pos_emb_sin: [seq_len, qk_channels // 2]
251
+ max_seqlen: maximum sequence length
252
+ full_attn: if True, full attention; if False, window attention
253
+ """
254
+ # [seq_len, 1, embed_dim] -> QKV projection
255
+ qkv, _ = self.qkv(x) # [seq_len, 1, q_size + kv_size + kv_size]
256
+ seq_len, batch_size, _ = qkv.shape
257
+
258
+ q_size = self.num_heads_per_partition * self.qk_channels
259
+ kv_size = self.num_kv_heads_per_partition * self.kv_channels
260
+ q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
261
+
262
+ # Rearrange to [batch, seq, head, head_dim] for rotary application
263
+ q = einops.rearrange(q, "s b (h d) -> b s h d", h=self.num_heads_per_partition)
264
+ k = einops.rearrange(
265
+ k, "s b (h d) -> b s h d", h=self.num_kv_heads_per_partition
266
+ )
267
+ v = einops.rearrange(
268
+ v, "s b (h d) -> b s h d", h=self.num_kv_heads_per_partition
269
+ )
270
+
271
+ # Apply rotary embeddings to Q and K independently (handles GQA)
272
+ if rotary_pos_emb_cos is not None and rotary_pos_emb_sin is not None:
273
+ q = self.apply_rotary_emb(q, rotary_pos_emb_cos, rotary_pos_emb_sin)
274
+ k = self.apply_rotary_emb(k, rotary_pos_emb_cos, rotary_pos_emb_sin)
275
+
276
+ if full_attn:
277
+ # Full attention via MMEncoderAttention
278
+ # Flatten to [batch, seq, heads * head_dim]
279
+ q_flat = q.reshape(batch_size, seq_len, -1)
280
+ k_flat = k.reshape(batch_size, seq_len, -1)
281
+ v_flat = v.reshape(batch_size, seq_len, -1)
282
+ context_layer = self.attn(
283
+ query=q_flat,
284
+ key=k_flat,
285
+ value=v_flat,
286
+ cu_seqlens=cu_seqlens,
287
+ max_seqlen=max_seqlen,
288
+ )
289
+ # context_layer: [batch, seq, num_heads, head_dim] or [batch, seq, hidden]
290
+ # Ensure shape is [seq, batch, num_heads * kv_channels]
291
+ if context_layer.dim() == 4:
292
+ context_layer = einops.rearrange(
293
+ context_layer, "b s h d -> s b (h d)"
294
+ ).contiguous()
295
+ else:
296
+ context_layer = einops.rearrange(
297
+ context_layer, "b s d -> s b d"
298
+ ).contiguous()
299
+ else:
300
+ # Window attention via flash_attn_varlen_func with window_size
301
+ # Flatten batch dimension: [seq, head, head_dim]
302
+ q_varlen = einops.rearrange(q, "b s h d -> (b s) h d")
303
+ k_varlen = einops.rearrange(k, "b s h d -> (b s) h d")
304
+ v_varlen = einops.rearrange(v, "b s h d -> (b s) h d")
305
+ output = self._forward_window_attn(
306
+ q_varlen, k_varlen, v_varlen, cu_seqlens, max_seqlen
307
+ )
308
+ # output: [total_tokens, num_heads, kv_channels]
309
+ context_layer = einops.rearrange(
310
+ output, "(b s) h d -> s b (h d)", b=batch_size
311
+ ).contiguous()
312
+
313
+ output, _ = self.proj(context_layer)
314
+ return output
315
+
316
+
317
+class MiMoVisionBlock(nn.Module):
318
+ def __init__(
319
+ self,
320
+ dim: int,
321
+ num_heads: int,
322
+ num_kv_heads: int,
323
+ qk_channels: int,
324
+ kv_channels: int,
325
+ mlp_hidden_dim: int,
326
+ act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
327
+ norm_eps: float = 1e-6,
328
+ use_sink: bool = False,
329
+ visual_token_window_size: int = 64,
330
+ quant_config: QuantizationConfig | None = None,
331
+ prefix: str = "",
332
+ ) -> None:
333
+ super().__init__()
334
+ self.norm1 = RMSNorm(dim, eps=norm_eps)
335
+ self.norm2 = RMSNorm(dim, eps=norm_eps)
336
+ self.attn = MiMoVisionAttention(
337
+ embed_dim=dim,
338
+ num_heads=num_heads,
339
+ num_kv_heads=num_kv_heads,
340
+ qk_channels=qk_channels,
341
+ kv_channels=kv_channels,
342
+ use_sink=use_sink,
343
+ visual_token_window_size=visual_token_window_size,
344
+ quant_config=quant_config,
345
+ prefix=f"{prefix}.attn",
346
+ )
347
+ self.mlp = MiMoVisionMLP(
348
+ in_features=dim,
349
+ hidden_features=mlp_hidden_dim,
350
+ act_fn=act_fn,
351
+ bias=True,
352
+ quant_config=quant_config,
353
+ prefix=f"{prefix}.mlp",
354
+ )
355
+
356
+ def forward(
357
+ self,
358
+ x: torch.Tensor,
359
+ cu_seqlens: torch.Tensor,
360
+ rotary_pos_emb_cos: torch.Tensor,
361
+ rotary_pos_emb_sin: torch.Tensor,
362
+ max_seqlen: torch.Tensor,
363
+ full_attn: bool = True,
364
+ ) -> torch.Tensor:
365
+ # x: [seq_len, batch=1, dim]
366
+ x_attn = self.attn(
367
+ self.norm1(x),
368
+ cu_seqlens=cu_seqlens,
369
+ rotary_pos_emb_cos=rotary_pos_emb_cos,
370
+ rotary_pos_emb_sin=rotary_pos_emb_sin,
371
+ max_seqlen=max_seqlen,
372
+ full_attn=full_attn,
373
+ )
374
+ # Fused residual add + norm2
375
+ x_norm, residual = self.norm2(x, residual=x_attn)
376
+ x = residual + self.mlp(x_norm)
377
+ return x
378
+
379
+
380
+class MiMoVisionTransformer(nn.Module):
381
+ def __init__(
382
+ self,
383
+ vision_cfg: PretrainedConfig,
384
+ *,
385
+ norm_eps: float = 1e-6,
386
+ quant_config: QuantizationConfig | None = None,
387
+ prefix: str = "",
388
+ ):
389
+ super().__init__()
390
+ self.spatial_merge_size = vision_cfg.spatial_merge_size
391
+ self.spatial_merge_unit = self.spatial_merge_size**2
392
+ self.fullatt_block_indexes = vision_cfg.fullatt_block_indexes
393
+ self.vit_window_attn_types = vision_cfg.vit_window_attn_types
394
+ self.visual_token_window_size = vision_cfg.visual_token_window_size
395
+ self.hidden_size = vision_cfg.hidden_size
396
+ self.num_heads = vision_cfg.num_heads
397
+ self.num_kv_heads = vision_cfg.num_key_value_heads
398
+ self.qk_channels = vision_cfg.qk_channels
399
+ self.kv_channels = vision_cfg.kv_channels
400
+
401
+ self.patch_embed = MiMoVisionPatchEmbed(
402
+ patch_size=vision_cfg.patch_size,
403
+ temporal_patch_size=vision_cfg.temporal_patch_size,
404
+ in_channels=vision_cfg.in_channels,
405
+ hidden_size=vision_cfg.hidden_size,
406
+ )
407
+
408
+ norm_layer = partial(RMSNorm, eps=norm_eps)
409
+
410
+ # Rotary embedding for 2D positions.
411
+ # With partial_rotary_factor=0.5 and head_size=qk_channels:
412
+ # rotary_dim = qk_channels // 2
413
+ # get_cos_sin returns cos, sin each of shape [pos, rotary_dim // 2]
414
+ # After indexing with 2D pos_ids and flattening:
415
+ # result shape = [tokens, rotary_dim] = [tokens, qk_channels // 2]
416
+ # which is what ApplyRotaryEmb expects as cos/sin input.
417
+ self.rotary_pos_emb = get_rope(
418
+ head_size=vision_cfg.qk_channels,
419
+ max_position=8192,
420
+ is_neox_style=True,
421
+ rope_parameters={"partial_rotary_factor": 0.5},
422
+ )
423
+
424
+ self.blocks = nn.ModuleList(
425
+ [
426
+ MiMoVisionBlock(
427
+ dim=vision_cfg.hidden_size,
428
+ num_heads=vision_cfg.num_heads,
429
+ num_kv_heads=vision_cfg.num_key_value_heads,
430
+ qk_channels=vision_cfg.qk_channels,
431
+ kv_channels=vision_cfg.kv_channels,
432
+ mlp_hidden_dim=vision_cfg.intermediate_size,
433
+ act_fn=get_act_and_mul_fn(vision_cfg.hidden_act),
434
+ norm_eps=norm_eps,
435
+ use_sink=(
436
+ vision_cfg.use_sink
437
+ and i not in vision_cfg.fullatt_block_indexes
438
+ ),
439
+ visual_token_window_size=vision_cfg.visual_token_window_size,
440
+ quant_config=quant_config,
441
+ prefix=f"{prefix}.blocks.{i}",
442
+ )
443
+ for i in range(vision_cfg.depth)
444
+ ]
445
+ )
446
+
447
+ self.merger = MiMoVisionPatchMerger(
448
+ d_model=vision_cfg.out_hidden_size,
449
+ context_dim=vision_cfg.hidden_size,
450
+ norm_layer=norm_layer,
451
+ spatial_merge_size=vision_cfg.spatial_merge_size,
452
+ quant_config=quant_config,
453
+ prefix=f"{prefix}.merger",
454
+ )
455
+
456
+ @property
457
+ def dtype(self) -> torch.dtype:
458
+ return self.patch_embed.proj.weight.dtype
459
+
460
+ @property
461
+ def device(self) -> torch.device:
462
+ return self.patch_embed.proj.weight.device
463
+
464
+ def apply_index(self, tensor: torch.Tensor, index: torch.Tensor) -> torch.Tensor:
465
+ """Reindex tensor at the spatial_merge_unit granularity."""
466
+ tensor = tensor.unflatten(0, (-1, self.spatial_merge_unit))
467
+ tensor = tensor[index]
468
+ tensor = tensor.flatten(0, 1)
469
+ return tensor
470
+
471
+ def get_window_index_1d(
472
+ self, grid_thw: torch.Tensor, col: bool = True
473
+ ) -> torch.Tensor:
474
+ """Compute 1D window indices for col-based or row-based SWA reordering."""
475
+ window_index: list[torch.Tensor] = []
476
+ window_index_id = 0
477
+ for grid_t, grid_h, grid_w in grid_thw:
478
+ llm_grid_h = grid_h // self.spatial_merge_size
479
+ llm_grid_w = grid_w // self.spatial_merge_size
480
+ index = torch.arange(grid_t * llm_grid_h * llm_grid_w).reshape(
481
+ grid_t, llm_grid_h, llm_grid_w
482
+ )
483
+ index_new = index.transpose(1, 2).reshape(-1) if col else index.reshape(-1)
484
+ window_index.append(index_new + window_index_id)
485
+ window_index_id += int((grid_t * llm_grid_h * llm_grid_w).item())
486
+ return torch.cat(window_index, dim=0)
487
+
488
+ def rot_pos_emb(self, grid_thw: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
489
+ """Compute 2D rotary position embedding cos/sin for given grid sizes.
490
+
491
+ Returns:
492
+ cos: [total_tokens, qk_channels // 2]
493
+ sin: [total_tokens, qk_channels // 2]
494
+ """
495
+ cos_list, sin_list = [], []
496
+ for i in range(grid_thw.size(0)):
497
+ t, h, w = int(grid_thw[i, 0]), int(grid_thw[i, 1]), int(grid_thw[i, 2])
498
+
499
+ # Build 2D position IDs with spatial_merge_size interleaving
500
+ hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
501
+ hpos_ids = (
502
+ hpos_ids.reshape(
503
+ h // self.spatial_merge_size,
504
+ self.spatial_merge_size,
505
+ w // self.spatial_merge_size,
506
+ self.spatial_merge_size,
507
+ )
508
+ .permute(0, 2, 1, 3)
509
+ .flatten()
510
+ )
511
+ wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
512
+ wpos_ids = (
513
+ wpos_ids.reshape(
514
+ h // self.spatial_merge_size,
515
+ self.spatial_merge_size,
516
+ w // self.spatial_merge_size,
517
+ self.spatial_merge_size,
518
+ )
519
+ .permute(0, 2, 1, 3)
520
+ .flatten()
521
+ )
522
+ pos_ids = torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1)
523
+ # pos_ids: [t*h*w, 2]
524
+
525
+ max_grid_size = max(h, w)
526
+ # get_cos_sin returns cos, sin each of shape [max_grid_size, rotary_dim//2]
527
+ # where rotary_dim = qk_channels // 2 (from partial_rotary_factor=0.5)
528
+ cos, sin = self.rotary_pos_emb.get_cos_sin(max_grid_size)
529
+
530
+ # [t*h*w, 2, rotary_dim//2] -> [t*h*w, rotary_dim] (= qk_channels // 2)
531
+ cos_img = cos[pos_ids].flatten(1)
532
+ sin_img = sin[pos_ids].flatten(1)
533
+ cos_list.append(cos_img)
534
+ sin_list.append(sin_img)
535
+
536
+ return torch.cat(cos_list, dim=0), torch.cat(sin_list, dim=0)
537
+
538
+ def forward(self, x: torch.Tensor, grid_thw: torch.Tensor) -> torch.Tensor:
539
+ """
540
+ Args:
541
+ x: [total_tokens, C] pre-flattened patches
542
+ grid_thw: [num_images, 3] tensor of (t, h, w) for each image/video
543
+ Returns:
544
+ [merged_tokens, out_hidden_size]
545
+ """
546
+ # Ensure grid_thw is a tensor
547
+ if not isinstance(grid_thw, torch.Tensor):
548
+ grid_thw = torch.tensor(grid_thw, dtype=torch.long)
549
+
550
+ # Move to visual model device/dtype
551
+ x = x.to(device=self.device, dtype=self.dtype)
552
+
553
+ # Patch embedding: [total_tokens, hidden_size]
554
+ x = self.patch_embed(x)
555
+
556
+ # Compute 2D rotary positional embeddings
557
+ # cos, sin: [total_tokens, qk_channels // 2]
558
+ rotary_cos, rotary_sin = self.rot_pos_emb(grid_thw)
559
+ rotary_cos = rotary_cos.to(device=x.device)
560
+ rotary_sin = rotary_sin.to(device=x.device)
561
+
562
+ # Compute cu_seqlens for flash_attn (per-image/video sequence lengths)
563
+ seqlens = torch.repeat_interleave(
564
+ grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
565
+ )
566
+ cu_seqlens = torch.cat(
567
+ [
568
+ torch.tensor([0], device=x.device, dtype=torch.int32),
569
+ seqlens.cumsum(dim=0).to(device=x.device, dtype=torch.int32),
570
+ ]
571
+ )
572
+ max_seqlen = seqlens.max()
573
+
574
+ # Precompute col-based window index for type=1 (col SWA) layers
575
+ window_index_1d_col = self.get_window_index_1d(grid_thw, col=True).to(
576
+ device=x.device
577
+ )
578
+ reverse_window_index_1d_col = torch.argsort(window_index_1d_col)
579
+
580
+ # Col-based rotary embeddings (reordered at spatial_merge_unit granularity).
581
+ # apply_index reorders groups of spatial_merge_unit tokens, just like x.
582
+ col_cos = self.apply_index(rotary_cos, window_index_1d_col)
583
+ col_sin = self.apply_index(rotary_sin, window_index_1d_col)
584
+
585
+ # Add batch dimension: [total_tokens, 1, hidden_size]
586
+ x = x.unsqueeze(1)
587
+
588
+ for i, blk in enumerate(self.blocks):
589
+ window_attn_type = self.vit_window_attn_types[i]
590
+
591
+ # Reorder tokens to col-based layout when entering col-SWA region
592
+ if window_attn_type == 1 and (
593
+ i == 0 or self.vit_window_attn_types[i - 1] != 1
594
+ ):
595
+ x = self.apply_index(x, window_index_1d_col)
596
+
597
+ # Restore row-based order when leaving col-SWA region
598
+ if (
599
+ i > 0
600
+ and window_attn_type != 1
601
+ and self.vit_window_attn_types[i - 1] == 1
602
+ ):
603
+ x = self.apply_index(x, reverse_window_index_1d_col)
604
+
605
+ # Use col-based embeddings for col-SWA layers
606
+ cos_now = col_cos if window_attn_type == 1 else rotary_cos
607
+ sin_now = col_sin if window_attn_type == 1 else rotary_sin
608
+
609
+ full_attn = i in self.fullatt_block_indexes
610
+ x = blk(
611
+ x,
612
+ cu_seqlens=cu_seqlens,
613
+ rotary_pos_emb_cos=cos_now,
614
+ rotary_pos_emb_sin=sin_now,
615
+ max_seqlen=max_seqlen,
616
+ full_attn=full_attn,
617
+ )
618
+
619
+ # Restore row-based order if last block was col-SWA
620
+ if self.vit_window_attn_types[-1] == 1:
621
+ x = self.apply_index(x, reverse_window_index_1d_col)
622
+
623
+ # Remove batch dim and merge spatial tokens
624
+ # x: [total_tokens, 1, hidden_size] -> [total_tokens, hidden_size]
625
+ x = x.squeeze(1)
626
+ x = self.merger(x)
627
+ return x
628
+
629
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
630
+ stacked_params_mapping = [
631
+ ("mlp.gate_up_proj", "mlp.gate_proj", 0),
632
+ ("mlp.gate_up_proj", "mlp.up_proj", 1),
633
+ ]
634
+ params_dict = dict(self.named_parameters(remove_duplicate=False))
635
+ loaded_params: set[str] = set()
636
+
637
+ for name, loaded_weight in weights:
638
+ for param_name, weight_name, shard_id in stacked_params_mapping:
639
+ if weight_name not in name:
640
+ continue
641
+ name = name.replace(weight_name, param_name)
642
+ param = params_dict[name]
643
+ weight_loader = param.weight_loader
644
+ weight_loader(param, loaded_weight, shard_id)
645
+ break
646
+ else:
647
+ param = params_dict[name]
648
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
649
+ weight_loader(param, loaded_weight)
650
+ loaded_params.add(name)
651
+ return loaded_params
652
+
653
+
654
+class MiMoV2OmniProcessingInfo(BaseProcessingInfo):
655
+ def get_supported_mm_limits(self) -> Mapping[str, int | None]:
656
+ return {"audio": None, "image": None, "video": None}
657
+
658
+ def get_hf_config(self):
659
+ config = self.ctx.get_hf_config()
660
+ if isinstance(config.vision_config, dict):
661
+ config.vision_config = Mimo_VLVisionConfig.from_dict(config.vision_config)
662
+ return config
663
+
664
+ def get_hf_processor(self, **kwargs: object) -> MiMoOmniProcessor:
665
+ hf_config = self.get_hf_config()
666
+ tokenizer = self.get_tokenizer()
667
+ return MiMoOmniProcessor.from_hf_config(tokenizer, hf_config)
668
+
669
+ def get_image_processor(self, **kwargs: object):
670
+ return self.get_hf_processor(**kwargs).image_processor
671
+
672
+ def get_data_parser(self):
673
+ from vllm.multimodal.parse import MultiModalDataParser
674
+
675
+ return MultiModalDataParser(target_sr=24000.0)
676
+
677
+ def get_mm_max_tokens_per_item(
678
+ self,
679
+ seq_len: int,
680
+ mm_counts: Mapping[str, int],
681
+ ) -> Mapping[str, int]:
682
+ return {
683
+ "image": self.get_max_image_tokens(),
684
+ "video": self.get_max_video_tokens(seq_len, mm_counts),
685
+ }
686
+
687
+ def _get_vision_info(
688
+ self,
689
+ *,
690
+ image_width: int,
691
+ image_height: int,
692
+ num_frames: int = 1,
693
+ do_resize: bool = True,
694
+ image_processor,
695
+ mm_kwargs: Mapping[str, object],
696
+ ) -> tuple[ImageSize, int]:
697
+ hf_config = self.get_hf_config()
698
+ vision_config = hf_config.vision_config
699
+ patch_size = vision_config.patch_size
700
+ merge_size = vision_config.spatial_merge_size
701
+ temporal_patch_size = vision_config.temporal_patch_size
702
+ tokens_per_second = vision_config.tokens_per_second
703
+
704
+ mm_kwargs = self.ctx.get_merged_mm_kwargs(mm_kwargs)
705
+ size = image_processor.size
706
+ if override_size := mm_kwargs.get("size"):
707
+ size = size | override_size
708
+ if (override_min_pixels := mm_kwargs.get("min_pixels")) is not None:
709
+ size = size | {"shortest_edge": override_min_pixels}
710
+ if (override_max_pixels := mm_kwargs.get("max_pixels")) is not None:
711
+ size = size | {"longest_edge": override_max_pixels}
712
+
713
+ if do_resize:
714
+ resized_height, resized_width = smart_resize(
715
+ height=image_height,
716
+ width=image_width,
717
+ factor=patch_size * merge_size,
718
+ min_pixels=size["shortest_edge"],
719
+ max_pixels=size["longest_edge"],
720
+ )
721
+ preprocessed_size = ImageSize(width=resized_width, height=resized_height)
722
+ else:
723
+ preprocessed_size = ImageSize(width=image_width, height=image_height)
724
+
725
+ # For video, MiMo resamples to tokens_per_second fps before temporal patching,
726
+ # effective tokens = num_frames * tokens_per_second / temporal_patch_size.
727
+ # For images (num_frames == 1) no resampling is applied.
728
+ if num_frames > 1:
729
+ effective_frames = num_frames * tokens_per_second
730
+ else:
731
+ effective_frames = num_frames
732
+ padded_num_frames = effective_frames + effective_frames % temporal_patch_size
733
+ grid_t = max(padded_num_frames // temporal_patch_size, 1)
734
+ grid_h = preprocessed_size.height // patch_size
735
+ grid_w = preprocessed_size.width // patch_size
736
+ num_patches = grid_t * grid_h * grid_w
737
+ num_vision_tokens = num_patches // (merge_size**2)
738
+ return preprocessed_size, num_vision_tokens
739
+
740
+ def get_num_image_tokens(
741
+ self,
742
+ *,
743
+ image_width: int,
744
+ image_height: int,
745
+ image_processor,
746
+ mm_kwargs: Mapping[str, object],
747
+ ) -> int:
748
+ _, num_image_tokens = self._get_vision_info(
749
+ image_width=image_width,
750
+ image_height=image_height,
751
+ num_frames=1,
752
+ image_processor=image_processor,
753
+ mm_kwargs=mm_kwargs,
754
+ )
755
+ return num_image_tokens
756
+
757
+ def get_num_video_tokens(
758
+ self,
759
+ *,
760
+ image_width: int,
761
+ image_height: int,
762
+ num_frames: int,
763
+ image_processor,
764
+ mm_kwargs: Mapping[str, object],
765
+ ) -> int:
766
+ _, num_video_tokens = self._get_vision_info(
767
+ image_width=image_width,
768
+ image_height=image_height,
769
+ num_frames=num_frames,
770
+ image_processor=image_processor,
771
+ mm_kwargs=mm_kwargs,
772
+ )
773
+ return num_video_tokens
774
+
775
+ def get_image_size_with_most_features(
776
+ self, max_pixels: int | None = None
777
+ ) -> ImageSize:
778
+ hf_config = self.get_hf_config()
779
+ vision_config = hf_config.vision_config
780
+ patch_size = vision_config.patch_size
781
+ merge_size = vision_config.spatial_merge_size
782
+
783
+ if max_pixels is None:
784
+ image_processor = self.get_image_processor()
785
+ mm_kwargs = self.ctx.get_merged_mm_kwargs({})
786
+ size = image_processor.size
787
+ if override_size := mm_kwargs.get("size"):
788
+ size = size | override_size
789
+ if (override_min_pixels := mm_kwargs.get("min_pixels")) is not None:
790
+ size = size | {"shortest_edge": override_min_pixels}
791
+ if (override_max_pixels := mm_kwargs.get("max_pixels")) is not None:
792
+ size = size | {"longest_edge": override_max_pixels}
793
+ max_pixels = size["longest_edge"]
794
+
795
+ unit = patch_size * merge_size
796
+ max_seq_len = max_pixels // (unit * unit)
797
+
798
+ def closest_factor_pair(n: int) -> tuple[int, int]:
799
+ for d in range(math.isqrt(n), 0, -1):
800
+ if n % d == 0:
801
+ return d, n // d
802
+ return 1, n
803
+
804
+ height_factor, width_factor = 1, max_seq_len
805
+ for seq_len in range(max_seq_len, 0, -1):
806
+ height_factor, width_factor = closest_factor_pair(seq_len)
807
+ if width_factor / height_factor <= 200:
808
+ break
809
+
810
+ return ImageSize(width=unit * width_factor, height=unit * height_factor)
811
+
812
+ def get_max_image_tokens(self) -> int:
813
+ image_processor = self.get_image_processor()
814
+ target_width, target_height = self.get_image_size_with_most_features()
815
+ return self.get_num_image_tokens(
816
+ image_width=target_width,
817
+ image_height=target_height,
818
+ image_processor=image_processor,
819
+ mm_kwargs={},
820
+ )
821
+
822
+ def _get_max_video_frames(self, max_tokens: int, start_num_frames: int = 1) -> int:
823
+ image_processor = self.get_image_processor()
824
+ target_width, target_height = self.get_image_size_with_most_features()
825
+ num_frames = start_num_frames
826
+ while True:
827
+ next_num_frames = num_frames + 1
828
+ next_max_tokens = self.get_num_video_tokens(
829
+ image_width=target_width,
830
+ image_height=target_height,
831
+ num_frames=next_num_frames,
832
+ image_processor=image_processor,
833
+ mm_kwargs={},
834
+ )
835
+ if next_max_tokens > max_tokens:
836
+ break
837
+ num_frames = next_num_frames
838
+ return num_frames
839
+
840
+ def get_num_frames_with_most_features(
841
+ self,
842
+ seq_len: int,
843
+ mm_counts: Mapping[str, int],
844
+ max_frames_per_video: int = 14,
845
+ ) -> int:
846
+ max_videos = mm_counts.get("video", 0)
847
+ max_total_frames = self._get_max_video_frames(seq_len)
848
+ max_frames_per_video = min(
849
+ max_total_frames // max(max_videos, 1), max_frames_per_video
850
+ )
851
+ return max(max_frames_per_video, 1)
852
+
853
+ def get_max_video_tokens(
854
+ self,
855
+ seq_len: int,
856
+ mm_counts: Mapping[str, int],
857
+ ) -> int:
858
+ image_processor = self.get_image_processor()
859
+ target_width, target_height = self.get_image_size_with_most_features()
860
+ return self.get_num_video_tokens(
861
+ image_width=target_width,
862
+ image_height=target_height,
863
+ num_frames=self.get_num_frames_with_most_features(seq_len, mm_counts),
864
+ image_processor=image_processor,
865
+ mm_kwargs={},
866
+ )
867
+
868
+
869
+class MiMoV2OmniMultiModalProcessor(BaseMultiModalProcessor[MiMoV2OmniProcessingInfo]):
870
+ """vLLM multimodal processor for MiMo-Omni (image + video).
871
+
872
+ Key differences from Qwen2.5-VL:
873
+ - Videos use timestamp tokens between temporal grid positions.
874
+ - The HF processor expects ``(TCHW_tensor, timestamps_T_tensor)`` video
875
+ tuples rather than plain numpy arrays.
876
+ - ``video_start_times`` is tracked so prompt-update reconstruction can
877
+ regenerate the exact same timestamp token IDs.
878
+ """
879
+
880
+ # fps assumed for vllm-decoded video (numpy T,H,W,C arrays).
881
+ # The video loader samples ~32 frames; treat each frame as 1 s apart so
882
+ # MiMoVLProcessor sees 1 fps input and resamples internally.
883
+ _INPUT_FPS: float = 1.0
884
+
885
+ def _get_mm_fields_config(
886
+ self,
887
+ hf_inputs: BatchFeature,
888
+ hf_processor_mm_kwargs: Mapping[str, object],
889
+ ) -> Mapping[str, MultiModalFieldConfig]:
890
+ merge_size = self.info.get_hf_config().vision_config.spatial_merge_size
891
+ fields: dict[str, MultiModalFieldConfig] = dict(
892
+ **_create_qwen2vl_field_factory(merge_size)(hf_inputs),
893
+ second_per_grid_ts=MultiModalFieldConfig.batched("video"),
894
+ video_start_times=MultiModalFieldConfig.batched("video"),
895
+ audio_features=MultiModalFieldConfig.batched("audio"),
896
+ audio_token_lens=MultiModalFieldConfig.batched("audio"),
897
+ )
898
+ # video_audio fields: only present when video_audio content was processed
899
+ if "video_audio_n_segs" in hf_inputs:
900
+ fields["video_audio_n_segs"] = MultiModalFieldConfig.batched("video")
901
+ # video_audio_seg_lens: list of per-video 1D tensors, batched("video")
902
+ if "video_audio_seg_lens" in hf_inputs:
903
+ fields["video_audio_seg_lens"] = MultiModalFieldConfig.batched("video")
904
+ if "va_audio_features" in hf_inputs:
905
+ fields["va_audio_features"] = MultiModalFieldConfig.batched("va_audio")
906
+ return fields
907
+
908
+ def _call_hf_processor(
909
+ self,
910
+ prompt: str,
911
+ mm_data: Mapping[str, object],
912
+ mm_kwargs: Mapping[str, object],
913
+ tok_kwargs: Mapping[str, object],
914
+ ) -> BatchFeature:
915
+ """Convert numpy video arrays to (TCHW, timestamps) tuples for MiMo.
916
+ Also remap 'audios' → 'audio' since MiMoOmniProcessor.__call__ uses
917
+ the singular form.
918
+ """
919
+ # Remap audios → audio (MiMoOmniProcessor uses singular param name)
920
+ if "audios" in mm_data:
921
+ mm_data = {**mm_data, "audio": mm_data["audios"]}
922
+ mm_data = {k: v for k, v in mm_data.items() if k != "audios"}
923
+
924
+ # Handle video_audio items: convert video part to (TCHW, timestamps) tuple
925
+ if "video_audio" in mm_data:
926
+ va_converted: list[VideoAudioInput] = []
927
+ for va_item in mm_data["video_audio"]:
928
+ if isinstance(va_item, VideoAudioInput):
929
+ vid = va_item.video
930
+ else:
931
+ # Expect (video_frames, audio_source) tuple
932
+ vid, audio_src = va_item
933
+ va_item = VideoAudioInput(video=vid, audio=audio_src)
934
+ vid = vid
935
+ # Convert video frames to (TCHW, timestamps) if needed
936
+ if (
937
+ isinstance(vid, tuple)
938
+ and len(vid) == 2
939
+ and isinstance(vid[0], torch.Tensor)
940
+ and isinstance(vid[1], torch.Tensor)
941
+ ):
942
+ va_converted.append(va_item)
943
+ else:
944
+ if isinstance(vid, np.ndarray):
945
+ frames = torch.from_numpy(vid)
946
+ elif isinstance(vid, torch.Tensor):
947
+ frames = vid
948
+ else:
949
+ frames = torch.tensor(np.array(vid))
950
+ if frames.ndim == 4 and frames.shape[-1] in (1, 3, 4):
951
+ frames = frames.permute(0, 3, 1, 2).float()
952
+ else:
953
+ frames = frames.float()
954
+ T = frames.shape[0]
955
+ timestamps = torch.arange(T, dtype=torch.float32) / self._INPUT_FPS
956
+ va_converted.append(
957
+ VideoAudioInput(
958
+ video=(frames, timestamps),
959
+ audio=va_item.audio,
960
+ )
961
+ )
962
+ mm_data = {**mm_data, "video_audio": va_converted}
963
+
964
+ if "videos" in mm_data:
965
+ converted: list[tuple[torch.Tensor, torch.Tensor]] = []
966
+ for video in mm_data["videos"]:
967
+ if (
968
+ isinstance(video, tuple)
969
+ and len(video) == 2
970
+ and isinstance(video[0], torch.Tensor)
971
+ and isinstance(video[1], torch.Tensor)
972
+ ):
973
+ # already in MiMo format
974
+ converted.append(video)
975
+ else:
976
+ # numpy (T, H, W, C) or torch (T, H, W, C) / (T, C, H, W)
977
+ if isinstance(video, np.ndarray):
978
+ frames = torch.from_numpy(video)
979
+ elif isinstance(video, torch.Tensor):
980
+ frames = video
981
+ else:
982
+ frames = torch.tensor(np.array(video))
983
+
984
+ if frames.ndim == 4 and frames.shape[-1] in (1, 3, 4):
985
+ # THWC → TCHW
986
+ frames = frames.permute(0, 3, 1, 2).float()
987
+ else:
988
+ frames = frames.float()
989
+
990
+ T = frames.shape[0]
991
+ timestamps = torch.arange(T, dtype=torch.float32) / self._INPUT_FPS
992
+ converted.append((frames, timestamps))
993
+
994
+ mm_data = {**mm_data, "videos": converted}
995
+
996
+ return super()._call_hf_processor(prompt, mm_data, mm_kwargs, tok_kwargs)
997
+
998
+ def _get_prompt_updates(
999
+ self,
1000
+ mm_items: MultiModalDataItems,
1001
+ hf_processor_mm_kwargs: Mapping[str, Any],
1002
+ out_mm_kwargs: MultiModalKwargsItems,
1003
+ ) -> Sequence[PromptUpdate]:
1004
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1005
+ hf_config = self.info.get_hf_config()
1006
+ tokenizer = self.info.get_tokenizer()
1007
+ vocab = tokenizer.get_vocab()
1008
+
1009
+ merge_size = hf_config.vision_config.spatial_merge_size
1010
+ p = hf_processor.mimo_processor
1011
+
1012
+ image_pad_id = vocab[hf_processor.image_token]
1013
+ video_pad_id = vocab[hf_processor.video_token]
1014
+ audio_pad_id = vocab.get("<|audio_pad|>")
1015
+ vision_start_id = p.vision_start_token_id
1016
+ vision_end_id = p.vision_end_token_id
1017
+ video_start_id = p.video_start_token_id
1018
+ video_end_id = p.video_end_token_id
1019
+ audio_start_id = p.audio_start_token_id
1020
+ audio_end_id = p.audio_end_token_id
1021
+
1022
+ def get_image_replacement(item_idx: int) -> PromptUpdateDetails:
1023
+ out_item = out_mm_kwargs["image"][item_idx]
1024
+ grid_thw = out_item["image_grid_thw"].data
1025
+ n_tokens = int(grid_thw.prod()) // merge_size**2
1026
+ return [image_pad_id] * n_tokens
1027
+
1028
+ def get_video_replacement(item_idx: int) -> PromptUpdateDetails:
1029
+ out_item = out_mm_kwargs["video"][item_idx]
1030
+ grid_thw = out_item["video_grid_thw"].data
1031
+ spt = float(out_item["second_per_grid_ts"].data)
1032
+ start = float(out_item["video_start_times"].data)
1033
+
1034
+ T, H, W = map(int, grid_thw)
1035
+ n_per_grid = H * W // (merge_size * merge_size)
1036
+
1037
+ # Check if this is a video_audio item
1038
+ n_segs_field = out_item.get("video_audio_n_segs")
1039
+ n_segs_val = int(n_segs_field.data) if n_segs_field is not None else 0
1040
+ va_seg_lens: list[int] | None = None
1041
+ if n_segs_val > 0:
1042
+ seg_lens_field = out_item.get("video_audio_seg_lens")
1043
+ if seg_lens_field is not None:
1044
+ va_seg_lens = seg_lens_field.data[:n_segs_val].tolist()
1045
+
1046
+ full: list[int] = [video_start_id]
1047
+ is_embed_mask: list[bool] = [False]
1048
+
1049
+ if va_seg_lens is None:
1050
+ # Regular video: timestamp + vision tokens per grid
1051
+ for j in range(T):
1052
+ ts_text = _format_timestamp(start + j * spt)
1053
+ ts_ids = tokenizer.encode(ts_text, add_special_tokens=False)
1054
+ full.extend(ts_ids)
1055
+ is_embed_mask.extend([False] * len(ts_ids))
1056
+ full.append(vision_start_id)
1057
+ is_embed_mask.append(False)
1058
+ full.extend([video_pad_id] * n_per_grid)
1059
+ is_embed_mask.extend([True] * n_per_grid)
1060
+ full.append(vision_end_id)
1061
+ is_embed_mask.append(False)
1062
+ else:
1063
+ # video_audio: interleaved vision+audio per group
1064
+ n_groups = len(va_seg_lens)
1065
+ frames_per_group = T // n_groups # 1 for il=0, T for il=-1
1066
+ for g in range(n_groups):
1067
+ # Timestamp for first frame of this group
1068
+ frame0 = g * frames_per_group
1069
+ ts_text = _format_timestamp(start + frame0 * spt)
1070
+ ts_ids = tokenizer.encode(ts_text, add_special_tokens=False)
1071
+ full.extend(ts_ids)
1072
+ is_embed_mask.extend([False] * len(ts_ids))
1073
+ # Vision tokens for all frames in this group
1074
+ for f in range(frames_per_group):
1075
+ full.append(vision_start_id)
1076
+ is_embed_mask.append(False)
1077
+ full.extend([video_pad_id] * n_per_grid)
1078
+ is_embed_mask.extend([True] * n_per_grid)
1079
+ full.append(vision_end_id)
1080
+ is_embed_mask.append(False)
1081
+ # Audio tokens for this group
1082
+ seg_len = va_seg_lens[g]
1083
+ full.append(audio_start_id)
1084
+ is_embed_mask.append(False)
1085
+ full.extend([audio_pad_id] * seg_len)
1086
+ is_embed_mask.extend([True] * seg_len)
1087
+ full.append(audio_end_id)
1088
+ is_embed_mask.append(False)
1089
+
1090
+ full.append(video_end_id)
1091
+ is_embed_mask.append(False)
1092
+
1093
+ embed_t = torch.tensor(is_embed_mask)
1094
+ return PromptUpdateDetails(
1095
+ full=full,
1096
+ is_embed=lambda _tok, _seq: embed_t,
1097
+ )
1098
+
1099
+ def get_audio_replacement(item_idx: int) -> PromptUpdateDetails:
1100
+ out_item = out_mm_kwargs["audio"][item_idx]
1101
+ tok_len = int(out_item["audio_token_lens"].data)
1102
+ return [audio_pad_id] * tok_len
1103
+
1104
+ updates: list[PromptUpdate] = [
1105
+ PromptReplacement(
1106
+ modality="image",
1107
+ target=[image_pad_id],
1108
+ replacement=get_image_replacement,
1109
+ ),
1110
+ PromptReplacement(
1111
+ modality="video",
1112
+ target=[video_pad_id],
1113
+ replacement=get_video_replacement,
1114
+ ),
1115
+ ]
1116
+ if audio_pad_id is not None and audio_start_id is not None:
1117
+ updates.append(
1118
+ PromptReplacement(
1119
+ modality="audio",
1120
+ target=[audio_pad_id],
1121
+ replacement=get_audio_replacement,
1122
+ )
1123
+ )
1124
+ return updates
1125
+
1126
+
1127
+class MiMoV2OmniDummyInputsBuilder(BaseDummyInputsBuilder[MiMoV2OmniProcessingInfo]):
1128
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
1129
+ num_images = mm_counts.get("image", 0)
1130
+ num_videos = mm_counts.get("video", 0)
1131
+ num_audios = mm_counts.get("audio", 0)
1132
+ image_ph = "<|vision_start|><|image_pad|><|vision_end|>"
1133
+ video_ph = "<|vision_start|><|video_pad|><|vision_end|>"
1134
+ audio_ph = "<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>"
1135
+ return image_ph * num_images + video_ph * num_videos + audio_ph * num_audios
1136
+
1137
+ def get_dummy_mm_data(
1138
+ self,
1139
+ seq_len: int,
1140
+ mm_counts: Mapping[str, int],
1141
+ mm_options: Mapping[str, BaseDummyOptions],
1142
+ ) -> MultiModalDataDict:
1143
+ num_images = mm_counts.get("image", 0)
1144
+ num_videos = mm_counts.get("video", 0)
1145
+
1146
+ target_width, target_height = self.info.get_image_size_with_most_features()
1147
+ target_num_frames = self.info.get_num_frames_with_most_features(
1148
+ seq_len, mm_counts
1149
+ )
1150
+
1151
+ return {
1152
+ "image": self._get_dummy_images(
1153
+ width=target_width,
1154
+ height=target_height,
1155
+ num_images=num_images,
1156
+ overrides=mm_options.get("image"),
1157
+ ),
1158
+ "video": self._get_dummy_videos(
1159
+ width=target_width,
1160
+ height=target_height,
1161
+ num_frames=target_num_frames,
1162
+ num_videos=num_videos,
1163
+ overrides=mm_options.get("video"),
1164
+ ),
1165
+ }
1166
+
1167
+
1168
+@MULTIMODAL_REGISTRY.register_processor(
1169
+ MiMoV2OmniMultiModalProcessor,
1170
+ info=MiMoV2OmniProcessingInfo,
1171
+ dummy_inputs=MiMoV2OmniDummyInputsBuilder,
1172
+)
1173
+class MiMoV2OmniForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsQuant):
1174
+ # To ensure correct weight loading and mapping.
1175
+ hf_to_vllm_mapper = WeightsMapper(
1176
+ orig_to_new_prefix={
1177
+ # audio encoder
1178
+ "speech_embeddings.": "audio_encoder.speech_embeddings.",
1179
+ # mapping for new names in checkpoint saved after transformers v4.52
1180
+ "model.language_model.": "language_model.model.",
1181
+ "model.visual.": "visual.",
1182
+ # mapping for original checkpoint
1183
+ "lm_head.": "language_model.lm_head.",
1184
+ "model.": "language_model.model.",
1185
+ }
1186
+ )
1187
+
1188
+ @classmethod
1189
+ def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1190
+ if modality.startswith("image"):
1191
+ return "<|vision_start|><|image_pad|><|vision_end|>"
1192
+ if modality.startswith("video"):
1193
+ return "<|vision_start|><|video_pad|><|vision_end|>"
1194
+ if modality.startswith("audio"):
1195
+ return "<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>"
1196
+
1197
+ raise ValueError(f"Unsupported modality: {modality}")
1198
+
1199
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1200
+ super().__init__()
1201
+ config = vllm_config.model_config.hf_config
1202
+ self.config = config
1203
+ # Omni ViT/Audio Encoder BF16
1204
+ vision_config = (
1205
+ Mimo_VLVisionConfig.from_dict(config.vision_config)
1206
+ if isinstance(config.vision_config, dict)
1207
+ else config.vision_config
1208
+ )
1209
+ with self._mark_tower_model(vllm_config, {"image", "video"}):
1210
+ self.visual = MiMoVisionTransformer(
1211
+ vision_config,
1212
+ norm_eps=getattr(vllm_config, "rms_norm_eps", 1e-6),
1213
+ quant_config=None,
1214
+ prefix=maybe_prefix(prefix, "visual"),
1215
+ )
1216
+ audio_config = getattr(config, "audio_config", None)
1217
+ model_path = vllm_config.model_config.model
1218
+ if audio_config is not None:
1219
+ with self._mark_tower_model(vllm_config, "audio"):
1220
+ self.audio_encoder = MimoAudioEncoder(
1221
+ audio_config, model_path=model_path
1222
+ )
1223
+ else:
1224
+ self.audio_encoder = None
1225
+ with self._mark_language_model(vllm_config):
1226
+ self.language_model = MiMoV2FlashForCausalLM(
1227
+ vllm_config=vllm_config,
1228
+ prefix=maybe_prefix(prefix, "language_model"),
1229
+ )
1230
+
1231
+ self.make_empty_intermediate_tensors = (
1232
+ self.language_model.make_empty_intermediate_tensors
1233
+ )
1234
+
1235
+ def _parse_and_validate_image_input(
1236
+ self, **kwargs: object
1237
+ ) -> Qwen2_5_VLImageInputs | None:
1238
+ pixel_values = kwargs.pop("pixel_values", None)
1239
+ image_embeds = kwargs.pop("image_embeds", None)
1240
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
1241
+
1242
+ if pixel_values is None and image_embeds is None:
1243
+ return None
1244
+
1245
+ if pixel_values is not None:
1246
+ return Qwen2_5_VLImagePixelInputs(
1247
+ type="pixel_values",
1248
+ pixel_values=pixel_values,
1249
+ image_grid_thw=image_grid_thw,
1250
+ )
1251
+
1252
+ if image_embeds is not None:
1253
+ return Qwen2_5_VLImageEmbeddingInputs(
1254
+ type="image_embeds",
1255
+ image_embeds=image_embeds,
1256
+ image_grid_thw=image_grid_thw,
1257
+ )
1258
+
1259
+ def _parse_and_validate_video_input(
1260
+ self, **kwargs: object
1261
+ ) -> Qwen2_5_VLVideoInputs | None:
1262
+ pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1263
+ video_embeds = kwargs.pop("video_embeds", None)
1264
+ video_grid_thw = kwargs.pop("video_grid_thw", None)
1265
+ second_per_grid_ts = kwargs.pop("second_per_grid_ts", None)
1266
+
1267
+ if pixel_values_videos is None and video_embeds is None:
1268
+ return None
1269
+
1270
+ if pixel_values_videos is not None:
1271
+ return Qwen2_5_VLVideoPixelInputs(
1272
+ type="pixel_values_videos",
1273
+ pixel_values_videos=pixel_values_videos,
1274
+ video_grid_thw=video_grid_thw,
1275
+ second_per_grid_ts=second_per_grid_ts,
1276
+ )
1277
+
1278
+ if video_embeds is not None:
1279
+ return Qwen2_5_VLVideoEmbeddingInputs(
1280
+ type="video_embeds",
1281
+ video_embeds=video_embeds,
1282
+ video_grid_thw=video_grid_thw,
1283
+ second_per_grid_ts=second_per_grid_ts,
1284
+ )
1285
+
1286
+ def _process_image_input(
1287
+ self, image_input: Qwen2_5_VLImageInputs
1288
+ ) -> tuple[torch.Tensor, ...]:
1289
+ grid_thw = image_input["image_grid_thw"]
1290
+ assert grid_thw.ndim == 2
1291
+ grid_thw_list = grid_thw.tolist()
1292
+
1293
+ if image_input["type"] == "image_embeds":
1294
+ image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1295
+ else:
1296
+ pixel_values = image_input["pixel_values"]
1297
+ image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
1298
+
1299
+ # Split concatenated embeddings for each image item.
1300
+ merge_size = self.visual.spatial_merge_size
1301
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1302
+ return image_embeds.split(sizes)
1303
+
1304
+ def _process_video_input(
1305
+ self, video_input: Qwen2_5_VLVideoInputs
1306
+ ) -> tuple[torch.Tensor, ...]:
1307
+ grid_thw = video_input["video_grid_thw"]
1308
+ assert grid_thw.ndim == 2
1309
+ grid_thw_list = grid_thw.tolist()
1310
+
1311
+ if video_input["type"] == "video_embeds":
1312
+ video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1313
+ else:
1314
+ pixel_values_videos = video_input["pixel_values_videos"]
1315
+ video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw_list)
1316
+
1317
+ # Split concatenated embeddings for each video item.
1318
+ merge_size = self.visual.spatial_merge_size
1319
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1320
+ return video_embeds.split(sizes)
1321
+
1322
+ def _parse_and_validate_audio_input(self, **kwargs: object) -> dict | None:
1323
+ audio_features = kwargs.pop("audio_features", None)
1324
+ audio_token_lens = kwargs.pop("audio_token_lens", None)
1325
+ if audio_features is None:
1326
+ return None
1327
+ return {
1328
+ "type": "audio",
1329
+ "audio_features": audio_features,
1330
+ "audio_token_lens": audio_token_lens,
1331
+ }
1332
+
1333
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1334
+ mm_input_by_modality = {}
1335
+
1336
+ # Preserve the order of modalities if there are multiple of them
1337
+ # from the order of kwargs.
1338
+ for input_key in kwargs:
1339
+ if (
1340
+ input_key in ("pixel_values", "image_embeds")
1341
+ and "image" not in mm_input_by_modality
1342
+ ):
1343
+ mm_input_by_modality["image"] = self._parse_and_validate_image_input(
1344
+ **kwargs
1345
+ )
1346
+ if (
1347
+ input_key in ("pixel_values_videos", "video_embeds")
1348
+ and "video" not in mm_input_by_modality
1349
+ ):
1350
+ mm_input_by_modality["video"] = self._parse_and_validate_video_input(
1351
+ **kwargs
1352
+ )
1353
+ if input_key == "audio_features" and "audio" not in mm_input_by_modality:
1354
+ mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(
1355
+ **kwargs
1356
+ )
1357
+ return mm_input_by_modality
1358
+
1359
+ def _process_audio_input(self, audio_input: dict) -> tuple[torch.Tensor, ...]:
1360
+ mel_specs = audio_input["audio_features"]
1361
+ if self.audio_encoder is None:
1362
+ return ()
1363
+ # Normalize to List[2D-Tensor].
1364
+ # MultiModalBatchedField._reduce_data either wraps a single [T, 128]
1365
+ # into [1, T, 128] via unsqueeze(0) or stacks N same-T items into
1366
+ # [N, T, 128]. Indexing along dim-0 extracts the per-item [T, 128].
1367
+ if isinstance(mel_specs, torch.Tensor):
1368
+ mel_specs = list(mel_specs) # [1,T,128] or [N,T,128] → [[T,128],...]
1369
+ if not mel_specs:
1370
+ return ()
1371
+ audio_embeds, item_token_lens = self.audio_encoder.get_audio_feature(mel_specs)
1372
+ return tuple(audio_embeds.split(item_token_lens))
1373
+
1374
+ def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
1375
+ # Pop video_audio-specific fields before main mm parsing
1376
+ video_audio_n_segs = kwargs.pop("video_audio_n_segs", None)
1377
+ video_audio_seg_lens = kwargs.pop("video_audio_seg_lens", None)
1378
+ va_audio_features = kwargs.pop("va_audio_features", None)
1379
+
1380
+ mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
1381
+ if not mm_input_by_modality and va_audio_features is None:
1382
+ return []
1383
+
1384
+ # The result multimodal_embeddings is tuple of tensors, with each
1385
+ # tensor corresponding to a multimodal data item (image, video, or audio).
1386
+ multimodal_embeddings: list[torch.Tensor] = []
1387
+
1388
+ # Pre-process va audio: one mel spec per va video → per-video audio embeddings
1389
+ # keyed by va video index (0-based among va videos only)
1390
+ va_audio_embs_list: list[tuple[torch.Tensor, ...]] = []
1391
+ if va_audio_features is not None and self.audio_encoder is not None:
1392
+ mel_list = (
1393
+ list(va_audio_features)
1394
+ if isinstance(va_audio_features, torch.Tensor)
1395
+ else list(va_audio_features)
1396
+ )
1397
+ for mel_spec in mel_list:
1398
+ embs, tok_lens = self.audio_encoder.get_audio_feature([mel_spec])
1399
+ # tok_lens is a list/tensor with one entry (total tokens for this mel)
1400
+ va_audio_embs_list.append(embs) # shape (total_tok, hidden)
1401
+
1402
+ va_cursor = 0 # index into va_audio_embs_list
1403
+
1404
+ # NOTE: Iterate in dict insertion order to preserve token sequence order.
1405
+ for modality in mm_input_by_modality:
1406
+ multimodal_input = mm_input_by_modality[modality]
1407
+ if modality == "image":
1408
+ multimodal_embeddings.extend(
1409
+ self._process_image_input(multimodal_input)
1410
+ )
1411
+ elif modality == "video":
1412
+ video_embs_tuple = self._process_video_input(multimodal_input)
1413
+ if video_audio_n_segs is None:
1414
+ multimodal_embeddings.extend(video_embs_tuple)
1415
+ else:
1416
+ grid_thw = multimodal_input["video_grid_thw"]
1417
+ for i, vid_embs in enumerate(video_embs_tuple):
1418
+ n_segs = int(video_audio_n_segs[i])
1419
+ if n_segs == 0 or not va_audio_embs_list:
1420
+ multimodal_embeddings.append(vid_embs)
1421
+ else:
1422
+ T = int(grid_thw[i][0])
1423
+ n_per_grid = vid_embs.shape[0] // T
1424
+ frames = list(vid_embs.split(n_per_grid, dim=0))
1425
+ frames_per_group = T // n_segs
1426
+ # Per-group audio token lengths for this va video
1427
+ # video_audio_seg_lens is (num_videos, max_T); row i
1428
+ # has valid values in [:n_segs], rest are zeros.
1429
+ seg_lens = video_audio_seg_lens[i][:n_segs].tolist()
1430
+ # Split full audio embs for this va video by group lengths
1431
+ full_va_embs = va_audio_embs_list[va_cursor]
1432
+ va_cursor += 1
1433
+ group_audio_embs = full_va_embs.split(seg_lens)
1434
+ # Interleave: all vid frames in group, then audio for group
1435
+ for g in range(n_segs):
1436
+ for f in range(frames_per_group):
1437
+ multimodal_embeddings.append(
1438
+ frames[g * frames_per_group + f]
1439
+ )
1440
+ multimodal_embeddings.append(group_audio_embs[g])
1441
+ elif modality == "audio":
1442
+ multimodal_embeddings.extend(
1443
+ self._process_audio_input(multimodal_input)
1444
+ )
1445
+ return tuple(multimodal_embeddings)
1446
+
1447
+ def forward(
1448
+ self,
1449
+ input_ids: torch.Tensor | None,
1450
+ positions: torch.Tensor,
1451
+ intermediate_tensors: IntermediateTensors | None = None,
1452
+ inputs_embeds: torch.Tensor | None = None,
1453
+ **kwargs: object,
1454
+ ) -> torch.Tensor | IntermediateTensors:
1455
+ """Run forward pass for Qwen2.5-VL.
1456
+
1457
+ Args:
1458
+ input_ids: Flattened (concatenated) input_ids corresponding to a
1459
+ batch.
1460
+ positions: Flattened (concatenated) position ids corresponding to a
1461
+ batch. **NOTE**: If mrope is enabled (default setting for
1462
+ Qwen2.5-VL opensource models), the shape will be `(3, seq_len)`,
1463
+ otherwise it will be `(seq_len,).
1464
+ """
1465
+
1466
+ if intermediate_tensors is not None:
1467
+ inputs_embeds = None
1468
+
1469
+ hidden_states = self.language_model.model(
1470
+ input_ids=input_ids,
1471
+ positions=positions,
1472
+ intermediate_tensors=intermediate_tensors,
1473
+ inputs_embeds=inputs_embeds,
1474
+ )
1475
+ return hidden_states
1476
+
1477
+ def compute_logits(
1478
+ self,
1479
+ hidden_states: torch.Tensor,
1480
+ ) -> torch.Tensor | None:
1481
+ return self.language_model.compute_logits(hidden_states)
1482
+
1483
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1484
+ audio_loaded: set[str] = set()
1485
+
1486
+ loader = AutoWeightsLoader(self, skip_prefixes=["audio_tokenizer."])
1487
+ auto_loaded = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1488
+ return audio_loaded | auto_loaded
vllm/model_executor/models/registry.py
+5
-1
@@ -171,7 +171,8 @@ _TEXT_GENERATION_MODELS = {
171
"MptForCausalLM": ("mpt", "MPTForCausalLM"),
172
"MPTForCausalLM": ("mpt", "MPTForCausalLM"),
173
"MiMoForCausalLM": ("mimo", "MiMoForCausalLM"),
174
- "MiMoV2FlashForCausalLM": ("mimo_v2_flash", "MiMoV2FlashForCausalLM"),
174
+ "MiMoV2FlashForCausalLM": ("mimo_v2", "MiMoV2FlashForCausalLM"),
175
+ "MiMoV2ProForCausalLM": ("mimo_v2", "MiMoV2ProForCausalLM"),
176
"NemotronForCausalLM": ("nemotron", "NemotronForCausalLM"),
177
"NemotronHForCausalLM": ("nemotron_h", "NemotronHForCausalLM"),
178
"NemotronHPuzzleForCausalLM": ("nemotron_h", "NemotronHForCausalLM"),
@@ -468,6 +469,7 @@ _MULTIMODAL_MODELS = {
469
),
470
"MantisForConditionalGeneration": ("llava", "MantisForConditionalGeneration"),
471
"MiDashengLMModel": ("midashenglm", "MiDashengLMModel"),
472
+ "MiMoV2OmniForCausalLM": ("mimo_v2_omni", "MiMoV2OmniForCausalLM"),
473
"MiniMaxVL01ForConditionalGeneration": (
474
"minimax_vl_01",
475
"MiniMaxVL01ForConditionalGeneration",
@@ -570,6 +572,8 @@ _MULTIMODAL_MODELS = {
572
_SPECULATIVE_DECODING_MODELS = {
573
"ExtractHiddenStatesModel": ("extract_hidden_states", "ExtractHiddenStatesModel"),
574
"MiMoMTPModel": ("mimo_mtp", "MiMoMTP"),
575
+ "MiMoV2MTPModel": ("mimo_v2_mtp", "MiMoV2MTP"),
576
+ "MiMoV2OmniMTPModel": ("mimo_v2_mtp", "MiMoV2OmniMTP"),
577
"EagleLlamaForCausalLM": ("llama_eagle", "EagleLlamaForCausalLM"),
578
"EagleLlama4ForCausalLM": ("llama4_eagle", "EagleLlama4ForCausalLM"),
579
"EagleMiniCPMForCausalLM": ("minicpm_eagle", "EagleMiniCPMForCausalLM"),
vllm/transformers_utils/configs/mimo_v2_omni.py
new
+65
@@ -0,0 +1,65 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+
5
+from transformers import PretrainedConfig
6
+
7
+
8
+class Mimo_VLVisionConfig(PretrainedConfig):
9
+ model_type = "mimovl"
10
+ base_config_key = "vision_config"
11
+
12
+ def __init__(
13
+ self,
14
+ depth=28,
15
+ hidden_size=1280,
16
+ hidden_act="silu",
17
+ intermediate_size=4608,
18
+ num_heads=32,
19
+ in_channels=3,
20
+ patch_size=16,
21
+ spatial_merge_size=2,
22
+ temporal_patch_size=2,
23
+ tokens_per_second=2,
24
+ window_size=128,
25
+ out_hidden_size=2048,
26
+ fullatt_block_indexes=None,
27
+ initializer_range=0.02,
28
+ kv_channels=64, # HACK
29
+ qk_channels=64,
30
+ num_query_groups=4,
31
+ num_key_value_heads=8,
32
+ vit_window_attn_types=None,
33
+ visual_token_window_size=64,
34
+ **kwargs,
35
+ ):
36
+ super().__init__(**kwargs)
37
+
38
+ self.depth = depth
39
+ self.hidden_size = hidden_size
40
+ self.hidden_act = hidden_act
41
+ self.intermediate_size = intermediate_size
42
+ self.num_heads = num_heads
43
+ # Support GQA: if num_key_value_heads is not provided,
44
+ # default to num_heads (MHA)
45
+ if num_key_value_heads is None:
46
+ num_key_value_heads = num_heads
47
+ self.num_key_value_heads = num_key_value_heads
48
+ self.in_channels = in_channels
49
+ self.patch_size = patch_size
50
+ self.spatial_merge_size = spatial_merge_size
51
+ self.temporal_patch_size = temporal_patch_size
52
+ self.tokens_per_second = tokens_per_second
53
+ self.window_size = window_size
54
+ self.fullatt_block_indexes = (
55
+ fullatt_block_indexes
56
+ if fullatt_block_indexes is not None
57
+ else [7, 15, 23, 31]
58
+ )
59
+ self.out_hidden_size = out_hidden_size
60
+ self.initializer_range = initializer_range
61
+ self.kv_channels = kv_channels
62
+ self.qk_channels = qk_channels
63
+ self.num_query_groups = num_query_groups
64
+ self.vit_window_attn_types = vit_window_attn_types or [-1] * depth
65
+ self.visual_token_window_size = visual_token_window_size
vllm/transformers_utils/model_arch_config_convertor.py
+35
@@ -445,6 +445,37 @@ class MimoMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
445
return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
446
447
448
+def _strip_mimo_v2_attention_chunk_size(
449
+ hf_config: PretrainedConfig, hf_text_config: PretrainedConfig
450
+) -> None:
451
+ # MiMo-V2-Flash's config.json sets `attention_chunk_size=128` but the
452
+ # architecture does not actually use chunked local attention. Leaving it
453
+ # set makes vLLM disable the hybrid KV cache manager
454
+ for cfg in (hf_text_config, hf_config):
455
+ if cfg is not None and hasattr(cfg, "attention_chunk_size"):
456
+ delattr(cfg, "attention_chunk_size")
457
+
458
+
459
+class MimoV2ModelArchConfigConvertor(ModelArchConfigConvertorBase):
460
+ def __init__(self, hf_config: PretrainedConfig, hf_text_config: PretrainedConfig):
461
+ super().__init__(hf_config, hf_text_config)
462
+ _strip_mimo_v2_attention_chunk_size(hf_config, hf_text_config)
463
+
464
+
465
+class MimoV2MTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
466
+ def __init__(self, hf_config: PretrainedConfig, hf_text_config: PretrainedConfig):
467
+ super().__init__(hf_config, hf_text_config)
468
+ _strip_mimo_v2_attention_chunk_size(hf_config, hf_text_config)
469
+
470
+ def get_num_hidden_layers(self) -> int:
471
+ n = getattr(self.hf_text_config, "num_nextn_predict_layers", None)
472
+ if n is not None:
473
+ return n
474
+ # Fall back to n_predict set by hf_config_override
475
+ n = getattr(self.hf_text_config, "n_predict", None)
476
+ return n if n is not None else 0
477
+
478
+
479
class GLM4MoeMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
480
def get_num_hidden_layers(self) -> int:
481
return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
@@ -511,6 +542,10 @@ MODEL_ARCH_CONFIG_CONVERTORS = {
542
"qwen3_next_mtp": Qwen3NextMTPModelArchConfigConvertor,
543
"qwen3_5_mtp": Qwen3_5MTPModelArchConfigConvertor,
544
"mimo_mtp": MimoMTPModelArchConfigConvertor,
545
+ "mimo_v2_pro": MimoV2ModelArchConfigConvertor,
546
+ "mimo_v2_flash": MimoV2ModelArchConfigConvertor,
547
+ "mimo_v2_mtp": MimoV2MTPModelArchConfigConvertor,
548
+ "mimo_v2_omni_mtp": MimoV2MTPModelArchConfigConvertor,
549
"glm4_moe_mtp": GLM4MoeMTPModelArchConfigConvertor,
550
"glm_ocr_mtp": GLM4MoeMTPModelArchConfigConvertor,
551
"ernie_mtp": ErnieMTPModelArchConfigConvertor,
vllm/transformers_utils/processors/__init__.py
+2
@@ -27,6 +27,7 @@ __all__ = [
27
"IsaacProcessor",
28
"KimiAudioProcessor",
29
"KimiK25Processor",
30
+ "MiMoOmniProcessor",
31
"MistralCommonPixtralProcessor",
32
"MistralCommonVoxtralProcessor",
33
"NanoNemotronVLProcessor",
@@ -57,6 +58,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
58
"IsaacProcessor": "vllm.transformers_utils.processors.isaac",
59
"KimiAudioProcessor": "vllm.transformers_utils.processors.kimi_audio",
60
"KimiK25Processor": "vllm.transformers_utils.processors.kimi_k25",
61
+ "MiMoOmniProcessor": "vllm.transformers_utils.processors.mimo_v2_omni",
62
"MistralCommonPixtralProcessor": "vllm.transformers_utils.processors.pixtral",
63
"MistralCommonVoxtralProcessor": "vllm.transformers_utils.processors.voxtral",
64
"NanoNemotronVLProcessor": "vllm.transformers_utils.processors.nano_nemotron_vl",
vllm/transformers_utils/processors/mimo_v2_omni.py
new
+1285
@@ -0,0 +1,1285 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# mypy: ignore-errors
4
+"""MiMo-Omni multimodal processor for vLLM.
5
+
6
+Ported from SGLang's MiMoV2OmniProcessor / MiMoVLProcessor implementations.
7
+"""
8
+
9
+import contextlib
10
+import copy
11
+import io
12
+import logging
13
+import math
14
+from collections import OrderedDict
15
+from concurrent.futures import ThreadPoolExecutor, as_completed
16
+from dataclasses import dataclass, field
17
+from io import BytesIO
18
+from typing import Any, Literal
19
+
20
+import numpy as np
21
+import regex as re
22
+import requests
23
+import torch
24
+import torch.nn.functional as F
25
+from PIL import Image
26
+from transformers import BatchFeature, TensorType
27
+from transformers.processing_utils import ProcessorMixin
28
+
29
+try:
30
+ from torchcodec.decoders import AudioDecoder
31
+
32
+ _HAS_TORCHCODEC = True
33
+except ImportError:
34
+ AudioDecoder = None
35
+ _HAS_TORCHCODEC = False
36
+
37
+try:
38
+ import torchaudio
39
+ from torchaudio.transforms import MelSpectrogram as _MelSpectrogram
40
+
41
+ _HAS_TORCHAUDIO = True
42
+except ImportError:
43
+ torchaudio = None # type: ignore[assignment]
44
+ _MelSpectrogram = None # type: ignore[assignment,misc]
45
+ _HAS_TORCHAUDIO = False
46
+
47
+logger = logging.getLogger(__name__)
48
+
49
+# ---------------------------------------------------------------------------
50
+# Constants
51
+# ---------------------------------------------------------------------------
52
+
53
+_PIXEL_MEAN = [123.675, 116.28, 103.53]
54
+_PIXEL_STD = [58.395, 57.12, 57.375]
55
+_mean_std_cache: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
56
+
57
+
58
+# ---------------------------------------------------------------------------
59
+# Data classes
60
+# ---------------------------------------------------------------------------
61
+
62
+
63
+@dataclass
64
+class ImageInput:
65
+ # PIL.Image | str (path/url/base64) | bytes | torch.Tensor (C,H,W)
66
+ image: Any
67
+ max_pixels: int | None = None
68
+ min_pixels: int | None = None
69
+
70
+
71
+@dataclass
72
+class VideoInput:
73
+ # tuple[frames_TCHW: torch.Tensor, timestamps_T: torch.Tensor]
74
+ video: Any
75
+ min_pixels: int | None = None
76
+ max_pixels: int | None = None
77
+ total_max_pixels: int | None = None
78
+ fps: float | None = None
79
+ num_frames: int | None = None
80
+ max_frames: int | None = None
81
+ min_frames: int | None = None
82
+ do_include_last_frame: bool | None = False
83
+ start_time: float | None = None
84
+ end_time: float | None = None
85
+ segment_type: Literal["individual", "partial"] = "individual"
86
+
87
+
88
+@dataclass
89
+class AudioInput:
90
+ # str (path/url/base64) | bytes | tuple[waveform_1D, sr]
91
+ # | np.ndarray | torch.Tensor (T,n_vq)
92
+ audio: Any
93
+
94
+
95
+@dataclass
96
+class VideoAudioInput:
97
+ video: Any # same as VideoInput.video
98
+ audio: Any # same as AudioInput.audio
99
+ min_pixels: int | None = None
100
+ max_pixels: int | None = None
101
+ total_max_pixels: int | None = None
102
+ fps: float | None = None
103
+ num_frames: int | None = None
104
+ max_frames: int | None = None
105
+ min_frames: int | None = None
106
+ do_include_last_frame: bool | None = False
107
+ start_time: float | None = None
108
+ end_time: float | None = None
109
+ segment_type: Literal["individual", "partial"] = "individual"
110
+
111
+
112
+@dataclass
113
+class Content:
114
+ type: Literal["text", "image", "video", "audio", "video_audio"]
115
+ content: Any
116
+ is_target: bool | None = None
117
+
118
+
119
+@dataclass
120
+class MiMoVLInputSample:
121
+ input_ids: torch.Tensor
122
+ labels: torch.Tensor | None
123
+ pixel_values: list[torch.Tensor]
124
+ pixel_values_videos: list[torch.Tensor]
125
+ image_thw_grids: list[torch.Tensor]
126
+ video_thw_grids: list[torch.Tensor]
127
+ audio_inputs: list[torch.Tensor]
128
+ second_per_grid_ts: list[float] = field(default_factory=list)
129
+ video_start_times: list[float] = field(default_factory=list)
130
+ audio_token_lens: list[int] = field(default_factory=list)
131
+ va_audio_inputs: list[torch.Tensor] = field(default_factory=list)
132
+ video_audio_n_segs: list[int] = field(default_factory=list)
133
+ video_audio_seg_lens: list[int] = field(default_factory=list)
134
+ position_ids: torch.Tensor | None = None
135
+ rope_deltas: torch.Tensor | None = None
136
+ extra: dict = field(default_factory=dict)
137
+
138
+
139
+# ---------------------------------------------------------------------------
140
+# Vision utilities
141
+# ---------------------------------------------------------------------------
142
+
143
+
144
+def _format_timestamp(ts: float) -> str:
145
+ return f"{int(ts // 60):02d}:{int(ts % 60):02d}"
146
+
147
+
148
+def _smart_resize(
149
+ h: int, w: int, factor: int, min_px: int, max_px: int
150
+) -> tuple[int, int]:
151
+ if min(h, w) < factor:
152
+ if h < w:
153
+ h, w = factor, int(w * factor / h)
154
+ else:
155
+ w, h = factor, int(h * factor / w)
156
+ elif max(h, w) / min(h, w) > 200:
157
+ raise ValueError(f"Aspect ratio > 200 not allowed: {h}x{w}")
158
+ h_bar = round(h / factor) * factor
159
+ w_bar = round(w / factor) * factor
160
+ if h_bar * w_bar > max_px:
161
+ beta = math.sqrt((h * w) / max_px)
162
+ h_bar = math.floor(h / beta / factor) * factor
163
+ w_bar = math.floor(w / beta / factor) * factor
164
+ elif h_bar * w_bar < min_px:
165
+ beta = math.sqrt(min_px / (h * w))
166
+ h_bar = math.ceil(h * beta / factor) * factor
167
+ w_bar = math.ceil(w * beta / factor) * factor
168
+ return int(h_bar), int(w_bar)
169
+
170
+
171
+def _to_rgb(img: Image.Image) -> Image.Image:
172
+ if img.mode == "RGBA":
173
+ bg = Image.new("RGB", img.size, (255, 255, 255))
174
+ bg.paste(img, mask=img.split()[3])
175
+ return bg
176
+ return img.convert("RGB")
177
+
178
+
179
+def _standardize(images: torch.Tensor) -> torch.Tensor:
180
+ key = str(images.device)
181
+ if key not in _mean_std_cache:
182
+ mean = torch.tensor(_PIXEL_MEAN, device=images.device).view(1, -1, 1, 1)
183
+ std = torch.tensor(_PIXEL_STD, device=images.device).view(1, -1, 1, 1)
184
+ _mean_std_cache[key] = (mean, std)
185
+ mean, std = _mean_std_cache[key]
186
+ return (images - mean) / std
187
+
188
+
189
+def _transform_batch(
190
+ frames: torch.Tensor,
191
+ factor: int,
192
+ min_px: int,
193
+ max_px: int,
194
+ device: torch.device | None = None,
195
+) -> tuple[torch.Tensor, int, int]:
196
+ if device is not None:
197
+ frames = frames.to(device)
198
+ _, _, h, w = frames.shape
199
+ h_bar, w_bar = _smart_resize(h, w, factor, min_px, max_px)
200
+ resized = F.interpolate(
201
+ frames.float(), (h_bar, w_bar), mode="bilinear", align_corners=False
202
+ )
203
+ return _standardize(resized), w_bar, h_bar
204
+
205
+
206
+def _transform_single(
207
+ img: Any,
208
+ factor: int,
209
+ min_px: int,
210
+ max_px: int,
211
+ device: torch.device | None = None,
212
+) -> tuple[torch.Tensor, int, int]:
213
+ if isinstance(img, torch.Tensor):
214
+ t = img.float()
215
+ _, h, w = t.shape
216
+ elif isinstance(img, Image.Image):
217
+ img = img.convert("RGB")
218
+ w, h = img.size
219
+ t = torch.from_numpy(np.array(img)).permute(2, 0, 1).float()
220
+ else:
221
+ raise TypeError(f"Expected Tensor or PIL.Image, got {type(img)}")
222
+ if device is not None:
223
+ t = t.to(device)
224
+ h_bar, w_bar = _smart_resize(h, w, factor, min_px, max_px)
225
+ out = F.interpolate(
226
+ t.unsqueeze(0), (h_bar, w_bar), mode="bilinear", align_corners=False
227
+ )
228
+ return _standardize(out).squeeze(0), w_bar, h_bar
229
+
230
+
231
+def _fetch_image(src: Any) -> Image.Image:
232
+ if isinstance(src, Image.Image):
233
+ return _to_rgb(src)
234
+ if isinstance(src, bytes):
235
+ return _to_rgb(copy.deepcopy(Image.open(BytesIO(src))))
236
+ if isinstance(src, str):
237
+ if src.startswith(("http://", "https://")):
238
+ r = requests.get(src, timeout=30)
239
+ r.raise_for_status()
240
+ return _to_rgb(copy.deepcopy(Image.open(BytesIO(r.content))))
241
+ if src.startswith("file://"):
242
+ return _to_rgb(Image.open(src[7:]))
243
+ if src.startswith("data:image"):
244
+ import pybase64 as _b64
245
+
246
+ _, b64 = src.split("base64,", 1)
247
+ return _to_rgb(copy.deepcopy(Image.open(BytesIO(_b64.b64decode(b64)))))
248
+ return _to_rgb(Image.open(src))
249
+ raise ValueError(f"Unrecognized image source: {type(src)}")
250
+
251
+
252
+# ---------------------------------------------------------------------------
253
+# Core processor
254
+# ---------------------------------------------------------------------------
255
+
256
+
257
+class MiMoVLProcessor:
258
+ """Core MiMo-VL multimodal processor.
259
+
260
+ Handles image/video/audio preprocessing and token sequence construction.
261
+ Ported from SGLang's MiMoVLProcessor.
262
+ """
263
+
264
+ def __init__(
265
+ self,
266
+ tokenizer: Any,
267
+ patch_size: int = 14,
268
+ merge_size: int = 2,
269
+ temporal_patch_size: int = 2,
270
+ temporal_compression_ratio: int = 1,
271
+ use_video_timestamps: bool = True,
272
+ video_audio_interleave_length: int = 0,
273
+ audio_kernel_size: int = 3,
274
+ audio_stride_size: int = 2,
275
+ audio_avg_pooler: int = 2,
276
+ audio_sampling_rate: int = 24000,
277
+ audio_nfft: int = 960,
278
+ audio_hop_length: int = 240,
279
+ audio_window_size: int = 960,
280
+ audio_fmin: float = 0.0,
281
+ audio_fmax: float | None = None,
282
+ audio_n_mels: int = 128,
283
+ audio_segment_size: int = 6000,
284
+ audio_channels: int = 8,
285
+ audio_group_size: int = 4,
286
+ audio_input_id_per_second: float = 25.0,
287
+ audio_zeroemb_idx: int = 4096,
288
+ image_min_pixels: int | None = None,
289
+ image_max_pixels: int | None = None,
290
+ video_min_pixels: int | None = None,
291
+ video_max_pixels: int | None = None,
292
+ video_total_max_pixels: int | None = None,
293
+ fps: float | None = None,
294
+ num_frames: int | None = None,
295
+ max_frames: int | None = None,
296
+ min_frames: int | None = None,
297
+ image_token_id: int | None = None,
298
+ video_token_id: int | None = None,
299
+ audio_token_id: int | None = None,
300
+ vision_start_token_id: int | None = None,
301
+ vision_end_token_id: int | None = None,
302
+ audio_start_token_id: int | None = None,
303
+ audio_end_token_id: int | None = None,
304
+ video_start_token_id: int | None = None,
305
+ video_end_token_id: int | None = None,
306
+ pad_token_id: int | None = None,
307
+ rope_type: str = "rope",
308
+ video_process_num_threads: int = 16,
309
+ device: Any | None = None,
310
+ **kwargs: Any,
311
+ ) -> None:
312
+ self.tokenizer = tokenizer
313
+ self.video_process_num_threads = video_process_num_threads
314
+ self.device = torch.device(device) if isinstance(device, str) else device
315
+
316
+ self.rope_type = "rope" if rope_type == "1d" else rope_type
317
+ assert self.rope_type in ("rope", "mrope"), (
318
+ f"Unknown rope_type: {self.rope_type}"
319
+ )
320
+
321
+ # video timestamps require 1-D rope
322
+ assert use_video_timestamps, "use_video_timestamps must be True"
323
+ assert self.rope_type == "rope", (
324
+ "use_video_timestamps requires rope_type='rope'"
325
+ )
326
+ self.use_video_timestamps = use_video_timestamps
327
+ self.video_audio_interleave_length = video_audio_interleave_length
328
+
329
+ self.image_token_id = image_token_id
330
+ self.video_token_id = video_token_id
331
+ self.audio_token_id = audio_token_id
332
+ self.vision_start_token_id = vision_start_token_id
333
+ self.vision_end_token_id = vision_end_token_id
334
+ self.audio_start_token_id = audio_start_token_id
335
+ self.audio_end_token_id = audio_end_token_id
336
+ self.video_start_token_id = video_start_token_id
337
+ self.video_end_token_id = video_end_token_id
338
+ self.pad_token_id = pad_token_id
339
+
340
+ self.patch_size = patch_size
341
+ self.merge_size = merge_size
342
+ self.temporal_patch_size = temporal_patch_size
343
+ self.temporal_compression_ratio = temporal_compression_ratio
344
+
345
+ self.audio_sampling_rate = audio_sampling_rate
346
+ self.audio_nfft = audio_nfft
347
+ self.audio_hop_length = audio_hop_length
348
+ self.audio_window_size = audio_window_size
349
+ self.audio_fmin = audio_fmin
350
+ self.audio_fmax = audio_fmax
351
+ self.audio_n_mels = audio_n_mels
352
+ self.audio_segment_size = audio_segment_size
353
+ self.audio_kernel_size = audio_kernel_size
354
+ self.audio_stride_size = audio_stride_size
355
+ self.audio_avg_pooler = audio_avg_pooler
356
+ self.audio_channels = audio_channels
357
+ self.audio_group_size = audio_group_size
358
+ self.audio_input_id_per_second = audio_input_id_per_second
359
+
360
+ self._mel_spec_kwargs = dict(
361
+ sample_rate=audio_sampling_rate,
362
+ n_fft=audio_nfft,
363
+ hop_length=audio_hop_length,
364
+ win_length=audio_window_size,
365
+ f_min=audio_fmin,
366
+ f_max=audio_fmax,
367
+ n_mels=audio_n_mels,
368
+ power=1.0,
369
+ center=True,
370
+ )
371
+ self._mel_spectrogram: Any | None = None
372
+ self._resamplers: OrderedDict = OrderedDict()
373
+ self._resamplers_max = 16
374
+
375
+ if isinstance(audio_zeroemb_idx, int):
376
+ self.audio_zeroemb_idxs = torch.tensor(
377
+ [audio_zeroemb_idx] * audio_channels, dtype=torch.int32
378
+ )
379
+ else:
380
+ self.audio_zeroemb_idxs = torch.tensor(audio_zeroemb_idx, dtype=torch.int32)
381
+
382
+ assert image_min_pixels is not None, "image_min_pixels must be set"
383
+ assert image_max_pixels is not None, "image_max_pixels must be set"
384
+ assert video_min_pixels is not None, "video_min_pixels must be set"
385
+ assert video_max_pixels is not None, "video_max_pixels must be set"
386
+ assert video_total_max_pixels is not None, "video_total_max_pixels must be set"
387
+ assert fps is not None or num_frames is not None, (
388
+ "fps or num_frames must be set"
389
+ )
390
+
391
+ self._img_kw = {"min_pixels": image_min_pixels, "max_pixels": image_max_pixels}
392
+ self._vid_kw = {
393
+ "min_pixels": video_min_pixels,
394
+ "max_pixels": video_max_pixels,
395
+ "total_max_pixels": video_total_max_pixels,
396
+ "fps": fps,
397
+ "num_frames": num_frames,
398
+ "max_frames": max_frames,
399
+ "min_frames": min_frames,
400
+ }
401
+
402
+ @property
403
+ def mel_spectrogram(self) -> Any:
404
+ if self._mel_spectrogram is None:
405
+ if _MelSpectrogram is None:
406
+ raise RuntimeError(
407
+ "torchaudio is required for audio. "
408
+ "Install with: pip install torchaudio"
409
+ )
410
+ self._mel_spectrogram = _MelSpectrogram(**self._mel_spec_kwargs)
411
+ return self._mel_spectrogram
412
+
413
+ def _resolve_img_kw(self, img: ImageInput) -> dict:
414
+ return {
415
+ "min_px": (
416
+ img.min_pixels
417
+ if img.min_pixels is not None
418
+ else self._img_kw["min_pixels"]
419
+ ),
420
+ "max_px": (
421
+ img.max_pixels
422
+ if img.max_pixels is not None
423
+ else self._img_kw["max_pixels"]
424
+ ),
425
+ }
426
+
427
+ def _resolve_vid_kw(self, vid: VideoInput) -> dict:
428
+ kw: dict = {}
429
+ for k in ("min_pixels", "max_pixels", "total_max_pixels"):
430
+ kw[k] = getattr(vid, k) or self._vid_kw[k]
431
+ if vid.num_frames is not None:
432
+ kw["num_frames"] = vid.num_frames
433
+ elif vid.fps is not None:
434
+ kw["fps"] = vid.fps
435
+ if vid.max_frames is not None:
436
+ kw["max_frames"] = vid.max_frames
437
+ if vid.min_frames is not None:
438
+ kw["min_frames"] = vid.min_frames
439
+ elif self._vid_kw["num_frames"] is not None:
440
+ kw["num_frames"] = self._vid_kw["num_frames"]
441
+ elif self._vid_kw["fps"] is not None:
442
+ kw["fps"] = self._vid_kw["fps"]
443
+ if self._vid_kw["max_frames"] is not None:
444
+ kw["max_frames"] = self._vid_kw["max_frames"]
445
+ if self._vid_kw["min_frames"] is not None:
446
+ kw["min_frames"] = self._vid_kw["min_frames"]
447
+ else:
448
+ raise ValueError(
449
+ "No video sampling strategy specified (fps or num_frames)."
450
+ )
451
+ return kw
452
+
453
+ def preprocess_audio(self, audio: Any) -> tuple[torch.Tensor, int]:
454
+ """Decode audio bytes/path/tuple → (mel_spec (T, n_mels), token_len)."""
455
+ if isinstance(audio, tuple):
456
+ waveform, original_sr = audio
457
+ else:
458
+ if AudioDecoder is None:
459
+ raise RuntimeError(
460
+ "torchcodec is required for audio. "
461
+ "Install with: pip install torchcodec"
462
+ )
463
+ if isinstance(audio, bytes):
464
+ file_obj: Any = io.BytesIO(audio)
465
+ elif isinstance(audio, str):
466
+ if audio.startswith("data:"):
467
+ import pybase64 as _b64
468
+
469
+ file_obj = io.BytesIO(_b64.b64decode(audio.split(",")[1]))
470
+ elif audio.startswith(("http://", "https://")):
471
+ r = requests.get(audio, timeout=30)
472
+ r.raise_for_status()
473
+ file_obj = io.BytesIO(r.content)
474
+ else:
475
+ file_obj = audio
476
+ else:
477
+ raise ValueError(f"Unsupported audio source type: {type(audio)}")
478
+ samples = AudioDecoder(file_obj).get_all_samples()
479
+ waveform = samples.data
480
+ original_sr = samples.sample_rate
481
+
482
+ if original_sr != self.audio_sampling_rate:
483
+ if original_sr not in self._resamplers:
484
+ if len(self._resamplers) >= self._resamplers_max:
485
+ self._resamplers.popitem(last=False)
486
+ self._resamplers[original_sr] = torchaudio.transforms.Resample(
487
+ orig_freq=original_sr, new_freq=self.audio_sampling_rate
488
+ )
489
+ self._resamplers.move_to_end(original_sr)
490
+ waveform = self._resamplers[original_sr](waveform)
491
+
492
+ if waveform.ndim == 2:
493
+ waveform = waveform.mean(dim=0)
494
+ spec = self.mel_spectrogram(waveform[None, :])
495
+ spec = torch.log(torch.clip(spec, min=1e-7)).squeeze().transpose(0, 1)
496
+
497
+ n = spec.shape[0]
498
+ n = n + 3 - self.audio_kernel_size
499
+ n = (n + 2 - self.audio_kernel_size) // self.audio_stride_size + 1
500
+ n = n // self.audio_avg_pooler + int(n % self.audio_avg_pooler != 0)
501
+ token_len = math.ceil(n / self.audio_group_size)
502
+ return spec, token_len
503
+
504
+ def process_image(self, image: ImageInput) -> torch.Tensor:
505
+ kw = self._resolve_img_kw(image)
506
+ src = image.image
507
+ if isinstance(src, (str, bytes)):
508
+ src = _fetch_image(src)
509
+ tensor, _, _ = _transform_single(
510
+ src,
511
+ factor=self.patch_size * self.merge_size,
512
+ device=self.device,
513
+ **kw,
514
+ )
515
+ return tensor
516
+
517
+ def process_video(
518
+ self, video_input: VideoInput
519
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]:
520
+ kw = self._resolve_vid_kw(video_input)
521
+ video = video_input.video
522
+ if not isinstance(video, tuple):
523
+ raise ValueError(
524
+ f"video must be a (frames_TCHW, timestamps_T) tuple, "
525
+ f"got {type(video)}. "
526
+ "Decode the video before calling the processor."
527
+ )
528
+ frames, timestamps = video
529
+
530
+ fps = (
531
+ 1.0
532
+ if len(timestamps) < 2
533
+ else float(1.0 / (float(timestamps[1]) - float(timestamps[0])))
534
+ )
535
+ start = (
536
+ video_input.start_time
537
+ if video_input.start_time is not None
538
+ else float(timestamps[0])
539
+ )
540
+ end = (
541
+ video_input.end_time
542
+ if video_input.end_time is not None
543
+ else float(timestamps[-1]) + 1.0 / fps
544
+ )
545
+
546
+ if video_input.segment_type != "individual":
547
+ mask = (timestamps >= start) & (timestamps < end)
548
+ idxs = torch.where(mask)[0]
549
+ if len(idxs) == 0:
550
+ idxs = torch.where(timestamps <= start)[0][-1:]
551
+ frames, timestamps = frames[idxs], timestamps[idxs]
552
+
553
+ tp = self.temporal_patch_size * self.temporal_compression_ratio
554
+ n = frames.shape[0]
555
+ total_px = kw["total_max_pixels"]
556
+ max_px = max(
557
+ kw["min_pixels"], min(total_px * tp // max(n, 1), kw["max_pixels"])
558
+ )
559
+
560
+ if n % tp != 0:
561
+ pad = tp - n % tp
562
+ frames = torch.cat(
563
+ [frames, frames[-1:].repeat(pad, *([1] * (frames.ndim - 1)))],
564
+ dim=0,
565
+ )
566
+ timestamps = torch.cat([timestamps, timestamps[-1:].repeat(pad)], dim=0)
567
+
568
+ transformed, _, _ = _transform_batch(
569
+ frames,
570
+ factor=self.patch_size * self.merge_size,
571
+ min_px=kw["min_pixels"],
572
+ max_px=max_px,
573
+ device=self.device,
574
+ )
575
+ patches, thw = self._flatten_visual(transformed, "video")
576
+ meta = {
577
+ "fps_sampled": fps,
578
+ "segment_start_time": start,
579
+ "segment_end_time": end,
580
+ }
581
+ return patches, thw, timestamps, meta
582
+
583
+ def process_audio(self, audio: AudioInput) -> Any:
584
+ src = audio.audio
585
+ if isinstance(src, np.ndarray):
586
+ src = (torch.from_numpy(src).float(), self.audio_sampling_rate)
587
+ if isinstance(src, (str, bytes, tuple)):
588
+ return self.preprocess_audio(src)
589
+ # Pre-tokenized tensor (T, n_vq)
590
+ assert isinstance(src, torch.Tensor) and src.ndim == 2
591
+ T = src.shape[0]
592
+ src = src[:, : self.audio_channels].to(torch.long)
593
+ pad_T = (
594
+ (T + self.audio_group_size - 1)
595
+ // self.audio_group_size
596
+ * self.audio_group_size
597
+ )
598
+ padding = (
599
+ torch.zeros(pad_T - T, self.audio_channels, dtype=torch.long) + src[-1]
600
+ )
601
+ src = torch.cat([src, padding], dim=0)
602
+ return src.reshape(
603
+ pad_T // self.audio_group_size, self.audio_group_size, self.audio_channels
604
+ )
605
+
606
+ def _flatten_visual(
607
+ self, visual: torch.Tensor, kind: str
608
+ ) -> tuple[torch.Tensor, torch.Tensor]:
609
+ if kind == "image":
610
+ h, w = visual.shape[-2:]
611
+ patches = visual.unsqueeze(0).repeat(self.temporal_patch_size, 1, 1, 1)
612
+ else: # video / video_audio
613
+ temporal_stride = self.temporal_compression_ratio * self.temporal_patch_size
614
+ assert visual.shape[0] % temporal_stride == 0
615
+ patches = visual
616
+ h, w = patches.shape[-2:]
617
+
618
+ C = patches.shape[1]
619
+ grid_t = patches.shape[0] // self.temporal_patch_size
620
+ grid_h, grid_w = h // self.patch_size, w // self.patch_size
621
+
622
+ patches = (
623
+ patches.contiguous()
624
+ .view(
625
+ grid_t,
626
+ self.temporal_patch_size,
627
+ C,
628
+ grid_h // self.merge_size,
629
+ self.merge_size,
630
+ self.patch_size,
631
+ grid_w // self.merge_size,
632
+ self.merge_size,
633
+ self.patch_size,
634
+ )
635
+ .permute(0, 3, 6, 4, 7, 2, 1, 5, 8)
636
+ .contiguous()
637
+ .view(
638
+ grid_t * grid_h * grid_w,
639
+ C * self.temporal_patch_size * self.patch_size * self.patch_size,
640
+ )
641
+ )
642
+ thw = torch.tensor([grid_t, grid_h, grid_w], dtype=torch.int32)
643
+ return patches, thw
644
+
645
+ def process(
646
+ self, contents: list[Content], verbose: bool = False
647
+ ) -> MiMoVLInputSample:
648
+ input_ids: list[int] = []
649
+ labels: list[int] = []
650
+ img_pv: list[torch.Tensor] = []
651
+ img_grids: list[torch.Tensor] = []
652
+ vid_pv: list[torch.Tensor] = []
653
+ vid_grids: list[torch.Tensor] = []
654
+ audio_inputs: list[torch.Tensor] = []
655
+ is_audio_tokenized: list[bool] = []
656
+ audio_token_lens: list[int] = []
657
+ second_per_grid_ts: list[float] = []
658
+ video_start_times: list[float] = []
659
+ va_audio_inputs: list[torch.Tensor] = []
660
+ video_audio_n_segs: list[int] = []
661
+ video_audio_seg_lens: list[int] = []
662
+
663
+ # Pre-decode videos in parallel
664
+ vid_info = [
665
+ (i, c.content, c.type == "video_audio")
666
+ for i, c in enumerate(contents)
667
+ if c.type in ("video", "video_audio")
668
+ ]
669
+ vid_results: dict[int, tuple] = {}
670
+ if vid_info:
671
+ n_t = min(self.video_process_num_threads, len(vid_info))
672
+ if n_t > 1 and len(vid_info) > 1:
673
+ with ThreadPoolExecutor(max_workers=n_t) as ex:
674
+ fut_map = {
675
+ ex.submit(self.process_video, vi): idx
676
+ for idx, vi, _ in vid_info
677
+ }
678
+ for fut in as_completed(fut_map):
679
+ vid_results[fut_map[fut]] = fut.result()
680
+ else:
681
+ for idx, vi, _ in vid_info:
682
+ vid_results[idx] = self.process_video(vi)
683
+
684
+ for ci, content in enumerate(contents):
685
+ _ids: list[int] = []
686
+ _lbls: list[int] | None = None
687
+
688
+ if content.type == "text":
689
+ _ids = (
690
+ self.tokenizer.encode(content.content)
691
+ if isinstance(content.content, str)
692
+ else list(content.content)
693
+ )
694
+ if content.is_target:
695
+ _lbls = _ids
696
+
697
+ elif content.type == "image":
698
+ tensor = self.process_image(content.content)
699
+ patches, thw = self._flatten_visual(tensor, "image")
700
+ t, h, w = thw.tolist()
701
+ n_tok = (t * h * w) // (self.merge_size**2)
702
+ img_pv.append(patches)
703
+ img_grids.append(thw)
704
+ _ids = (
705
+ [self.vision_start_token_id]
706
+ + [self.image_token_id] * n_tok
707
+ + [self.vision_end_token_id]
708
+ )
709
+
710
+ elif content.type == "video":
711
+ patches, thw, ts, meta = vid_results[ci]
712
+ t, h, w = thw.tolist()
713
+ n_per_grid = h * w // (self.merge_size**2)
714
+ vid_pv.append(patches)
715
+ vid_grids.append(thw)
716
+ second_per_grid_ts.append(
717
+ self.temporal_patch_size / meta["fps_sampled"]
718
+ )
719
+ video_start_times.append(float(ts[0]))
720
+ video_audio_n_segs.append(0)
721
+
722
+ stride = self.temporal_patch_size * self.temporal_compression_ratio
723
+ ts_texts = [_format_timestamp(float(x)) for x in ts[::stride]]
724
+ ts_ids_list = [self.tokenizer.encode(s) for s in ts_texts]
725
+
726
+ _ids = [self.video_start_token_id]
727
+ for ts_ids in ts_ids_list:
728
+ _ids += (
729
+ ts_ids
730
+ + [self.vision_start_token_id]
731
+ + [self.video_token_id] * n_per_grid
732
+ + [self.vision_end_token_id]
733
+ )
734
+ _ids += [self.video_end_token_id]
735
+
736
+ elif content.type == "audio":
737
+ processed = self.process_audio(content.content)
738
+ if isinstance(processed, tuple):
739
+ is_audio_tokenized.append(False)
740
+ spec, tok_len = processed
741
+ audio_inputs.append(spec)
742
+ else:
743
+ is_audio_tokenized.append(True)
744
+ tok_len = processed.shape[0]
745
+ audio_inputs.append(processed)
746
+ audio_token_lens.append(tok_len)
747
+ _ids = (
748
+ [self.audio_start_token_id]
749
+ + [self.audio_token_id] * tok_len
750
+ + [self.audio_end_token_id]
751
+ )
752
+
753
+ elif content.type == "video_audio":
754
+ patches, thw, ts, meta = vid_results[ci]
755
+ second_per_grid_ts.append(
756
+ self.temporal_patch_size / meta["fps_sampled"]
757
+ )
758
+ video_start_times.append(float(ts[0]))
759
+ processed_audio = self.process_audio(content.content)
760
+ tok_per_sec = self.audio_input_id_per_second / self.audio_group_size
761
+
762
+ t, h, w = thw.tolist()
763
+ vid_pv.append(patches)
764
+ vid_grids.append(thw)
765
+
766
+ if isinstance(processed_audio, tuple):
767
+ # Mel spec (not pre-tokenized): store in va_audio_inputs separately
768
+ spec, total_atok = processed_audio
769
+ va_audio_inputs.append(spec)
770
+ _va_is_tokenized = False
771
+ else:
772
+ # Pre-tokenized: not expected in vLLM, but handle defensively
773
+ total_atok = processed_audio.shape[0]
774
+ _va_is_tokenized = True
775
+
776
+ n_per_grid = h * w // (self.merge_size**2)
777
+ stride = self.temporal_patch_size * self.temporal_compression_ratio
778
+ grid_ts = ts[::stride]
779
+ ts_texts = [_format_timestamp(float(x)) for x in grid_ts]
780
+ ts_ids_list = [self.tokenizer.encode(s) for s in ts_texts]
781
+
782
+ units: list[tuple] = []
783
+ for i in range(len(grid_ts)):
784
+ a_start = int(float(grid_ts[i]) * tok_per_sec)
785
+ a_end = (
786
+ int(float(grid_ts[i + 1]) * tok_per_sec)
787
+ if i < len(grid_ts) - 1
788
+ else int(meta["segment_end_time"] * tok_per_sec)
789
+ )
790
+ seg_len = min(a_end, total_atok) - a_start
791
+ assert seg_len > 0, f"Zero-length audio segment at grid index {i}"
792
+ seg = (
793
+ processed_audio[a_start : a_start + seg_len]
794
+ if _va_is_tokenized
795
+ else None
796
+ )
797
+ units.append(
798
+ (
799
+ float(grid_ts[i]),
800
+ ts_texts[i],
801
+ ts_ids_list[i],
802
+ n_per_grid,
803
+ seg_len,
804
+ seg,
805
+ )
806
+ )
807
+
808
+ il = self.video_audio_interleave_length
809
+ if il == -1:
810
+ groups: list[list] = [list(enumerate(units))]
811
+ elif il == 0:
812
+ groups = [[(i, u)] for i, u in enumerate(units)]
813
+ else:
814
+ groups, cur, t_ptr = [], [], 0.0
815
+ for i, u in enumerate(units):
816
+ while u[0] >= t_ptr + il:
817
+ if cur:
818
+ groups.append(cur)
819
+ cur = []
820
+ t_ptr += il
821
+ cur.append((i, u))
822
+ if cur:
823
+ groups.append(cur)
824
+
825
+ # Track n_segs (= num groups) and per-group audio token counts
826
+ video_audio_n_segs.append(len(groups))
827
+ for group in groups:
828
+ group_seg_len = sum(u[4] for _, u in group)
829
+ video_audio_seg_lens.append(group_seg_len)
830
+
831
+ _ids = [self.video_start_token_id]
832
+ for group in groups:
833
+ _ids += group[0][1][2] # first-unit timestamp token ids
834
+ _vid_tok: list[int] = []
835
+ _aud_tok: list[int] = []
836
+ for _, u in group:
837
+ _, _, _, vid_n, seg_n, seg_audio = u
838
+ _vid_tok += (
839
+ [self.vision_start_token_id]
840
+ + [self.video_token_id] * vid_n
841
+ + [self.vision_end_token_id]
842
+ )
843
+ _aud_tok += [self.audio_token_id] * seg_n
844
+ if seg_audio is not None:
845
+ # Pre-tokenized per-frame segments (rare in vLLM)
846
+ audio_inputs.append(seg_audio)
847
+ _ids += (
848
+ _vid_tok
849
+ + [self.audio_start_token_id]
850
+ + _aud_tok
851
+ + [self.audio_end_token_id]
852
+ )
853
+ _ids += [self.video_end_token_id]
854
+
855
+ input_ids.extend(_ids)
856
+ labels.extend(
857
+ _lbls if _lbls is not None else [self.pad_token_id] * len(_ids)
858
+ )
859
+
860
+ ids_t = torch.tensor(input_ids)
861
+ lbl_arr = np.roll(labels, shift=-1)
862
+ lbl_arr[-1] = self.pad_token_id
863
+ lbl_t = torch.tensor(lbl_arr)
864
+
865
+ extra: dict = {}
866
+ if is_audio_tokenized:
867
+ assert all(is_audio_tokenized) or not any(is_audio_tokenized)
868
+ extra["is_audio_tokenized"] = is_audio_tokenized[0]
869
+
870
+ position_ids = torch.arange(ids_t.shape[0]).expand(3, -1)
871
+ rope_deltas = torch.zeros((1, 1), dtype=torch.int32)
872
+
873
+ return MiMoVLInputSample(
874
+ input_ids=ids_t,
875
+ labels=lbl_t,
876
+ pixel_values=img_pv,
877
+ pixel_values_videos=vid_pv,
878
+ image_thw_grids=img_grids,
879
+ video_thw_grids=vid_grids,
880
+ audio_inputs=audio_inputs,
881
+ second_per_grid_ts=second_per_grid_ts,
882
+ video_start_times=video_start_times,
883
+ audio_token_lens=audio_token_lens,
884
+ va_audio_inputs=va_audio_inputs,
885
+ video_audio_n_segs=video_audio_n_segs,
886
+ video_audio_seg_lens=video_audio_seg_lens,
887
+ position_ids=position_ids,
888
+ rope_deltas=rope_deltas,
889
+ extra=extra,
890
+ )
891
+
892
+
893
+# ---------------------------------------------------------------------------
894
+# vLLM ProcessorMixin wrapper
895
+# ---------------------------------------------------------------------------
896
+
897
+
898
+class MiMoOmniProcessor(ProcessorMixin):
899
+ """HuggingFace-compatible ProcessorMixin wrapper for MiMo-Omni.
900
+
901
+ Accepts PIL images, pre-decoded video tuples (frames_TCHW, timestamps_T),
902
+ and audio (file path / bytes / (waveform, sr) tuple / numpy array).
903
+ """
904
+
905
+ attributes = ["tokenizer"]
906
+ tokenizer_class = "AutoTokenizer"
907
+
908
+ # Single or multi-pad placeholders produced by the chat template / prior expansion
909
+ _IMG_RE = re.compile(r"<\|vision_start\|>(?:<\|image_pad\|>)+<\|vision_end\|>")
910
+ _VID_RE = re.compile(r"<\|vision_start\|>(?:<\|video_pad\|>)+<\|vision_end\|>")
911
+ _AUD_RE = re.compile(
912
+ r"<\|mimo_audio_start\|>(?:<\|audio_pad\|>)+<\|mimo_audio_end\|>"
913
+ )
914
+
915
+ _MM_RE = re.compile(
916
+ r"(<\|vision_start\|>(?:<\|image_pad\|>)+<\|vision_end\|>"
917
+ r"|<\|vision_start\|>(?:<\|video_pad\|>)+<\|vision_end\|>"
918
+ r"|<\|mimo_audio_start\|>(?:<\|audio_pad\|>)+<\|mimo_audio_end\|>)"
919
+ )
920
+
921
+ def __init__(
922
+ self,
923
+ tokenizer: Any,
924
+ *,
925
+ patch_size: int = 14,
926
+ merge_size: int = 2,
927
+ temporal_patch_size: int = 2,
928
+ temporal_compression_ratio: int = 1,
929
+ image_min_pixels: int | None = None,
930
+ image_max_pixels: int | None = None,
931
+ video_min_pixels: int | None = None,
932
+ video_max_pixels: int | None = None,
933
+ video_total_max_pixels: int | None = None,
934
+ fps: float = 2.0,
935
+ num_frames: int | None = None,
936
+ max_frames: int = 256,
937
+ min_frames: int = 8,
938
+ video_audio_interleave_length: int = 0,
939
+ audio_sampling_rate: int = 24000,
940
+ audio_nfft: int = 960,
941
+ audio_hop_length: int = 240,
942
+ audio_window_size: int = 960,
943
+ audio_fmin: float = 0.0,
944
+ audio_fmax: float | None = None,
945
+ audio_n_mels: int = 128,
946
+ audio_segment_size: int = 6000,
947
+ audio_kernel_size: int = 3,
948
+ audio_stride_size: int = 2,
949
+ audio_avg_pooler: int = 2,
950
+ audio_channels: int = 8,
951
+ audio_group_size: int = 4,
952
+ audio_input_id_per_second: float = 25.0,
953
+ audio_zeroemb_idx: int = 4096,
954
+ image_token_id: int | None = None,
955
+ video_token_id: int | None = None,
956
+ audio_token_id: int | None = None,
957
+ vision_start_token_id: int | None = None,
958
+ vision_end_token_id: int | None = None,
959
+ audio_start_token_id: int | None = None,
960
+ audio_end_token_id: int | None = None,
961
+ video_start_token_id: int | None = None,
962
+ video_end_token_id: int | None = None,
963
+ rope_type: str = "rope",
964
+ ) -> None:
965
+ self.tokenizer = tokenizer
966
+
967
+ unit = patch_size * merge_size
968
+ self.mimo_processor = MiMoVLProcessor(
969
+ tokenizer=tokenizer,
970
+ patch_size=patch_size,
971
+ merge_size=merge_size,
972
+ temporal_patch_size=temporal_patch_size,
973
+ temporal_compression_ratio=temporal_compression_ratio,
974
+ use_video_timestamps=True,
975
+ video_audio_interleave_length=video_audio_interleave_length,
976
+ audio_sampling_rate=audio_sampling_rate,
977
+ audio_nfft=audio_nfft,
978
+ audio_hop_length=audio_hop_length,
979
+ audio_window_size=audio_window_size,
980
+ audio_fmin=audio_fmin,
981
+ audio_fmax=audio_fmax,
982
+ audio_n_mels=audio_n_mels,
983
+ audio_segment_size=audio_segment_size,
984
+ audio_kernel_size=audio_kernel_size,
985
+ audio_stride_size=audio_stride_size,
986
+ audio_avg_pooler=audio_avg_pooler,
987
+ audio_channels=audio_channels,
988
+ audio_group_size=audio_group_size,
989
+ audio_input_id_per_second=audio_input_id_per_second,
990
+ audio_zeroemb_idx=audio_zeroemb_idx,
991
+ image_min_pixels=image_min_pixels or (4 * unit * unit),
992
+ image_max_pixels=image_max_pixels or (4096 * unit * unit),
993
+ video_min_pixels=video_min_pixels or (4 * unit * unit),
994
+ video_max_pixels=video_max_pixels or (4096 * unit * unit),
995
+ video_total_max_pixels=video_total_max_pixels or (16384 * unit * unit),
996
+ fps=fps,
997
+ num_frames=num_frames,
998
+ max_frames=max_frames,
999
+ min_frames=min_frames,
1000
+ image_token_id=image_token_id,
1001
+ video_token_id=video_token_id,
1002
+ audio_token_id=audio_token_id,
1003
+ vision_start_token_id=vision_start_token_id,
1004
+ vision_end_token_id=vision_end_token_id,
1005
+ audio_start_token_id=audio_start_token_id,
1006
+ audio_end_token_id=audio_end_token_id,
1007
+ video_start_token_id=video_start_token_id,
1008
+ video_end_token_id=video_end_token_id,
1009
+ pad_token_id=tokenizer.pad_token_id,
1010
+ rope_type=rope_type,
1011
+ )
1012
+
1013
+ @classmethod
1014
+ def from_hf_config(cls, tokenizer: Any, hf_config: Any) -> "MiMoOmniProcessor":
1015
+ """Convenience factory: instantiate directly from an HF model config object."""
1016
+ vc = hf_config.vision_config
1017
+ if isinstance(vc, dict):
1018
+ patch_size = vc.get("patch_size", 14)
1019
+ merge_size = vc.get("spatial_merge_size", 2)
1020
+ temporal_patch_size = vc.get("temporal_patch_size", 2)
1021
+ else:
1022
+ patch_size = getattr(vc, "patch_size", 14)
1023
+ merge_size = getattr(vc, "spatial_merge_size", 2)
1024
+ temporal_patch_size = getattr(vc, "temporal_patch_size", 2)
1025
+
1026
+ pc: dict = getattr(hf_config, "processor_config", {}) or {}
1027
+ ac = getattr(hf_config, "audio_config", None)
1028
+ audio_sr: int | None = pc.get("audio_sampling_rate")
1029
+ if audio_sr is None and ac is not None:
1030
+ if isinstance(ac, dict):
1031
+ audio_sr = ac.get("sampling_rate") or ac.get("sample_rate")
1032
+ else:
1033
+ audio_sr = getattr(ac, "sampling_rate", None) or getattr(
1034
+ ac, "sample_rate", None
1035
+ )
1036
+
1037
+ rope_type = "rope"
1038
+ rs = getattr(hf_config, "rope_scaling", None)
1039
+ if rs and rs.get("type") == "default" and rs.get("mrope_section") is not None:
1040
+ rope_type = "mrope"
1041
+
1042
+ unit = patch_size * merge_size
1043
+ return cls(
1044
+ tokenizer,
1045
+ patch_size=patch_size,
1046
+ merge_size=merge_size,
1047
+ temporal_patch_size=temporal_patch_size,
1048
+ image_min_pixels=pc.get("image_min_pixels") or (4 * unit * unit),
1049
+ image_max_pixels=pc.get("image_max_pixels") or (4096 * unit * unit),
1050
+ video_min_pixels=pc.get("video_min_pixels") or (4 * unit * unit),
1051
+ video_max_pixels=pc.get("video_max_pixels") or (4096 * unit * unit),
1052
+ video_total_max_pixels=(
1053
+ pc.get("video_total_max_pixels") or (16384 * unit * unit)
1054
+ ),
1055
+ fps=pc.get("fps") or 2.0,
1056
+ num_frames=pc.get("num_frames"),
1057
+ max_frames=pc.get("max_frames") or 256,
1058
+ min_frames=pc.get("min_frames") or 8,
1059
+ video_audio_interleave_length=pc.get("video_audio_interleave_length", 0),
1060
+ audio_sampling_rate=audio_sr or 24000,
1061
+ image_token_id=pc.get("image_token_id"),
1062
+ video_token_id=pc.get("video_token_id"),
1063
+ audio_token_id=pc.get("audio_token_id"),
1064
+ vision_start_token_id=pc.get("vision_start_token_id"),
1065
+ vision_end_token_id=pc.get("vision_end_token_id"),
1066
+ audio_start_token_id=pc.get("audio_start_token_id"),
1067
+ audio_end_token_id=pc.get("audio_end_token_id"),
1068
+ video_start_token_id=pc.get("video_start_token_id"),
1069
+ video_end_token_id=pc.get("video_end_token_id"),
1070
+ rope_type=rope_type,
1071
+ )
1072
+
1073
+ @property
1074
+ def image_token(self) -> str:
1075
+ """Token string used as image placeholder (for vLLM integration)."""
1076
+ return "<|image_pad|>"
1077
+
1078
+ @property
1079
+ def video_token(self) -> str:
1080
+ """Token string used as video placeholder (for vLLM integration)."""
1081
+ return "<|video_pad|>"
1082
+
1083
+ @property
1084
+ def image_processor(self) -> Any:
1085
+ """Minimal image-processor-like object for vLLM processing-info compat."""
1086
+ p = self.mimo_processor
1087
+
1088
+ class _ImageProcessor:
1089
+ merge_size = p.merge_size
1090
+ size = {
1091
+ "shortest_edge": p._img_kw["min_pixels"],
1092
+ "longest_edge": p._img_kw["max_pixels"],
1093
+ }
1094
+
1095
+ return _ImageProcessor()
1096
+
1097
+ def _modality(self, token: str) -> str:
1098
+ if self._IMG_RE.fullmatch(token):
1099
+ return "image"
1100
+ if self._VID_RE.fullmatch(token):
1101
+ return "video"
1102
+ if self._AUD_RE.fullmatch(token):
1103
+ return "audio"
1104
+ return "unknown"
1105
+
1106
+ def __call__(
1107
+ self,
1108
+ text: str | list[str] | None = None,
1109
+ images: Any = None,
1110
+ videos: Any = None,
1111
+ audio: Any = None,
1112
+ video_audio: Any = None,
1113
+ return_tensors: str | TensorType | None = None,
1114
+ **kwargs: Any,
1115
+ ) -> BatchFeature:
1116
+ """Process multimodal inputs into model-ready tensors.
1117
+
1118
+ Args:
1119
+ text: Prompt string(s) containing multimodal placeholders
1120
+ ``<|vision_start|><|image_pad|><|vision_end|>``,
1121
+ ``<|vision_start|><|video_pad|><|vision_end|>``, or
1122
+ ``<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>``.
1123
+ images: PIL.Image or list[PIL.Image].
1124
+ videos: list of ``(frames_TCHW: torch.Tensor, timestamps_T: torch.Tensor)``
1125
+ tuples (pre-decoded).
1126
+ audio: list of ``str`` (path/url/base64), ``bytes``,
1127
+ ``(waveform_1D, sample_rate)`` tuples, or ``np.ndarray``.
1128
+ return_tensors: Passed to :class:`BatchFeature`.
1129
+
1130
+ Returns:
1131
+ :class:`BatchFeature` with keys:
1132
+ - ``input_ids``
1133
+ - ``pixel_values`` + ``image_grid_thw``
1134
+ - ``pixel_values_videos`` + ``video_grid_thw`` + ``second_per_grid_ts``
1135
+ - ``audio_features``
1136
+ """
1137
+ if isinstance(text, list):
1138
+ text = text[0] if len(text) == 1 else "\n".join(text)
1139
+
1140
+ imgs: list = (
1141
+ ([images] if isinstance(images, Image.Image) else list(images))
1142
+ if images is not None
1143
+ else []
1144
+ )
1145
+ vids: list = list(videos) if videos is not None else []
1146
+ auds: list = list(audio) if audio is not None else []
1147
+ va_items: list = list(video_audio) if video_audio is not None else []
1148
+
1149
+ # If audio exists but text has no audio placeholder, prepend one
1150
+ _aud_placeholder = "<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>"
1151
+ if auds and text is not None and not self._AUD_RE.search(text):
1152
+ text = _aud_placeholder + text
1153
+
1154
+ # Build Content list
1155
+ contents: list[Content] = []
1156
+
1157
+ if text and (imgs or vids or auds or va_items):
1158
+ parts = self._MM_RE.split(text)
1159
+ img_it = iter(imgs)
1160
+ vid_it = iter(vids)
1161
+ aud_it = iter(auds)
1162
+ va_it = iter(va_items)
1163
+ for part in parts:
1164
+ if self._MM_RE.fullmatch(part):
1165
+ mod = self._modality(part)
1166
+ if mod == "image":
1167
+ with contextlib.suppress(StopIteration):
1168
+ contents.append(
1169
+ Content(
1170
+ type="image",
1171
+ content=ImageInput(image=next(img_it)),
1172
+ )
1173
+ )
1174
+ elif mod == "video":
1175
+ # Try regular video first, fall back to video_audio
1176
+ vid_item = None
1177
+ vid_type = "video"
1178
+ with contextlib.suppress(StopIteration):
1179
+ vid_item = next(vid_it)
1180
+ if vid_item is None:
1181
+ with contextlib.suppress(StopIteration):
1182
+ vid_item = next(va_it)
1183
+ vid_type = "video_audio"
1184
+ if vid_item is not None:
1185
+ if vid_type == "video":
1186
+ contents.append(
1187
+ Content(
1188
+ type="video",
1189
+ content=VideoInput(video=vid_item),
1190
+ )
1191
+ )
1192
+ else:
1193
+ contents.append(
1194
+ Content(
1195
+ type="video_audio",
1196
+ content=vid_item,
1197
+ )
1198
+ )
1199
+ elif mod == "audio":
1200
+ with contextlib.suppress(StopIteration):
1201
+ contents.append(
1202
+ Content(
1203
+ type="audio",
1204
+ content=AudioInput(audio=next(aud_it)),
1205
+ )
1206
+ )
1207
+ elif part:
1208
+ contents.append(Content(type="text", content=part))
1209
+ elif text:
1210
+ contents.append(Content(type="text", content=text))
1211
+ else:
1212
+ for img in imgs:
1213
+ contents.append(Content(type="image", content=ImageInput(image=img)))
1214
+ for vid in vids:
1215
+ contents.append(Content(type="video", content=VideoInput(video=vid)))
1216
+ for aud in auds:
1217
+ contents.append(Content(type="audio", content=AudioInput(audio=aud)))
1218
+ for va_item in va_items:
1219
+ contents.append(Content(type="video_audio", content=va_item))
1220
+
1221
+ if not contents:
1222
+ ids = self.tokenizer(text or "", return_tensors=return_tensors)["input_ids"]
1223
+ return BatchFeature(data={"input_ids": ids}, tensor_type=return_tensors)
1224
+
1225
+ sample = self.mimo_processor.process(contents, verbose=False)
1226
+
1227
+ # vLLM expects input_ids to have a batch dimension [1, seq_len].
1228
+ data: dict = {"input_ids": sample.input_ids.unsqueeze(0)}
1229
+
1230
+ if sample.pixel_values:
1231
+ data["pixel_values"] = torch.cat(sample.pixel_values, dim=0)
1232
+ data["image_grid_thw"] = torch.stack(sample.image_thw_grids)
1233
+
1234
+ if sample.pixel_values_videos:
1235
+ data["pixel_values_videos"] = torch.cat(sample.pixel_values_videos, dim=0)
1236
+ data["video_grid_thw"] = torch.stack(sample.video_thw_grids)
1237
+ if sample.second_per_grid_ts:
1238
+ data["second_per_grid_ts"] = torch.tensor(
1239
+ sample.second_per_grid_ts, dtype=torch.float32
1240
+ )
1241
+ if sample.video_start_times:
1242
+ data["video_start_times"] = torch.tensor(
1243
+ sample.video_start_times, dtype=torch.float32
1244
+ )
1245
+ if sample.video_audio_n_segs:
1246
+ data["video_audio_n_segs"] = torch.tensor(
1247
+ sample.video_audio_n_segs, dtype=torch.long
1248
+ )
1249
+ # video_audio_seg_lens: 2D padded tensor (num_videos, max_T).
1250
+ # Row i has the per-group audio token lengths for video i
1251
+ # (zeros for regular videos; valid values for video_audio videos).
1252
+ n_segs_list = sample.video_audio_n_segs
1253
+ max_segs = max(n_segs_list) if n_segs_list else 0
1254
+ if max_segs > 0:
1255
+ seg_lens_2d = torch.zeros(len(n_segs_list), max_segs, dtype=torch.long)
1256
+ flat_cursor = 0
1257
+ for vi, n in enumerate(n_segs_list):
1258
+ if n > 0:
1259
+ seg_lens_2d[vi, :n] = torch.tensor(
1260
+ sample.video_audio_seg_lens[flat_cursor : flat_cursor + n],
1261
+ dtype=torch.long,
1262
+ )
1263
+ flat_cursor += n
1264
+ data["video_audio_seg_lens"] = seg_lens_2d
1265
+
1266
+ # audio_features is a list of variable-length mel-spec tensors; pop it
1267
+ # before BatchFeature conversion to avoid "batched tensors of the same
1268
+ # length" errors, then re-attach it after.
1269
+ audio_features = None
1270
+ if sample.audio_inputs:
1271
+ audio_features = sample.audio_inputs
1272
+ if "is_audio_tokenized" in sample.extra:
1273
+ data["is_audio_tokenized"] = sample.extra["is_audio_tokenized"]
1274
+ if sample.audio_token_lens:
1275
+ data["audio_token_lens"] = torch.tensor(
1276
+ sample.audio_token_lens, dtype=torch.long
1277
+ )
1278
+
1279
+ bf = BatchFeature(data=data, tensor_type=return_tensors)
1280
+ if audio_features is not None:
1281
+ bf["audio_features"] = audio_features
1282
+ # va_audio_features: list of mel-spec tensors (one per video_audio item)
1283
+ if sample.va_audio_inputs:
1284
+ bf["va_audio_features"] = sample.va_audio_inputs
1285
+ return bf
vllm/v1/core/kv_cache_utils.py
+2
@@ -1381,7 +1381,9 @@ def unify_hybrid_kv_cache_specs(kv_cache_spec: dict[str, KVCacheSpec]):
1381
block_size=spec.block_size,
1382
num_kv_heads=spec.num_kv_heads,
1383
head_size=spec.head_size,
1384
+ head_size_v=spec.head_size_v,
1385
dtype=spec.dtype,
1386
+ kv_quant_mode=spec.kv_quant_mode,
1387
sliding_window=spec.sliding_window,
1388
page_size_padded=spec.page_size_padded,
1389
)
vllm/v1/spec_decode/llm_base_proposer.py
+1
@@ -1344,6 +1344,7 @@ class SpecDecodeBaseProposer:
1344
"Exaone4_5_ForConditionalGeneration",
1345
"GlmOcrForConditionalGeneration",
1346
"HunYuanVLForConditionalGeneration",
1347
+ "MiMoV2OmniForCausalLM",
1348
"Qwen2_5_VLForConditionalGeneration",
1349
"Qwen3_5ForConditionalGeneration",
1350
"Qwen3_5MoeForConditionalGeneration",