1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""
4
+Vision tower implementation for Kimi-K2.5 model.
5
+
6
+This module provides the vision encoder components for Kimi-K2.5,
7
+including 3D patch embedding, RoPE position embedding, and
8
+temporal pooling for video chunks.
9
+"""
10
+
11
+from collections.abc import Sequence
12
+from copy import deepcopy
13
+from typing import Any
14
+
15
+import numpy as np
16
+import torch
17
+import torch.nn as nn
18
+import torch.nn.functional as F
19
+from transformers.activations import GELUActivation
20
+
21
+from vllm.distributed import divide, get_tensor_model_parallel_world_size
22
+from vllm.logger import init_logger
23
+from vllm.model_executor.layers.activation import get_act_fn
24
+from vllm.model_executor.layers.attention.mm_encoder_attention import MMEncoderAttention
25
+from vllm.model_executor.layers.linear import (
26
+ ColumnParallelLinear,
27
+ QKVParallelLinear,
28
+ ReplicatedLinear,
29
+ RowParallelLinear,
30
+)
31
+from vllm.model_executor.models.utils import maybe_prefix
32
+from vllm.model_executor.models.vision import (
33
+ is_vit_use_data_parallel,
34
+ run_dp_sharded_mrope_vision_model,
35
+)
36
+from vllm.transformers_utils.configs.kimi_k25 import KimiK25VisionConfig
37
+
38
+logger = init_logger(__name__)
39
+
40
+
41
+def _apply_rope_input_validation(x, freqs_cis):
42
+ assert x.ndim == freqs_cis.ndim + 1, (x.shape, freqs_cis.shape)
43
+ assert x.shape[:-2] == freqs_cis.shape[:-1], (x.shape, freqs_cis.shape)
44
+ assert x.shape[-1] == 2 * freqs_cis.shape[-1], (x.shape, freqs_cis.shape)
45
+ assert freqs_cis.dtype == torch.complex64, freqs_cis.dtype
46
+
47
+
48
+def get_rope_shape_decorate(func):
49
+ _get_rope_shape_first_call_flag = set()
50
+
51
+ def wrapper(org, interpolation_mode, shape):
52
+ key = (org.requires_grad, torch.is_grad_enabled(), interpolation_mode)
53
+ if key not in _get_rope_shape_first_call_flag:
54
+ _get_rope_shape_first_call_flag.add(key)
55
+ _ = func(org, interpolation_mode, shape=(64, 64))
56
+ return func(org, interpolation_mode, shape)
57
+
58
+ return wrapper
59
+
60
+
61
+@get_rope_shape_decorate
62
+@torch.compile(dynamic=True)
63
+def get_rope_shape(org, interpolation_mode, shape):
64
+ return (
65
+ F.interpolate(
66
+ org.permute((2, 0, 1)).unsqueeze(0),
67
+ size=shape,
68
+ mode=interpolation_mode,
69
+ )
70
+ .squeeze(0)
71
+ .permute((1, 2, 0))
72
+ .flatten(end_dim=1)
73
+ )
74
+
75
+
76
+def apply_rope(
77
+ xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor
78
+) -> tuple[torch.Tensor, torch.Tensor]:
79
+ """
80
+ Args: (The leading dimensions of all inputs should be the same)
81
+ xq: query, tensor of shape (..., num_heads, head_dim)
82
+ xk: key, tensor of shape (..., num_heads, head_dim)
83
+ freqs_cis: tensor of shape (..., head_dim/2), dtype=torch.complex64.
84
+ Returns:
85
+ xq_out, xk_out: tensors of shape (..., num_heads, head_dim)
86
+ """
87
+ _apply_rope_input_validation(xq, freqs_cis)
88
+ _apply_rope_input_validation(xk, freqs_cis)
89
+
90
+ freqs_cis = freqs_cis.unsqueeze(-2) # ..., 1, head_dim/2
91
+ # ..., num_heads, head_dim/2
92
+ xq_ = torch.view_as_complex(xq.float().view(*xq.shape[:-1], -1, 2))
93
+ xk_ = torch.view_as_complex(xk.float().view(*xq.shape[:-1], -1, 2))
94
+ xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(-2) # ..., num_heads, head_dim
95
+ xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(-2) # ..., num_heads, head_dim
96
+ return xq_out.type_as(xq), xk_out.type_as(xk)
97
+
98
+
99
+def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
100
+ """Generate 1D sincos positional embedding from grid positions."""
101
+ assert embed_dim % 2 == 0
102
+ omega = np.arange(embed_dim // 2, dtype=np.float32)
103
+ omega /= embed_dim / 2.0
104
+ omega = 1.0 / 10000**omega # (D/2,)
105
+
106
+ pos = pos.reshape(-1) # (M,)
107
+ out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
108
+
109
+ emb_sin = np.sin(out) # (M, D/2)
110
+ emb_cos = np.cos(out) # (M, D/2)
111
+
112
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
113
+ return emb
114
+
115
+
116
+def get_1d_sincos_pos_embed(embed_dim, t_size, cls_token=False):
117
+ """Generate 1D sincos positional embedding."""
118
+ grid_t = np.arange(t_size, dtype=np.float32)
119
+ pos_embed = get_1d_sincos_pos_embed_from_grid(embed_dim, grid_t)
120
+ if cls_token:
121
+ pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
122
+ return pos_embed
123
+
124
+
125
+class Learnable2DInterpPosEmbDivided_fixed(nn.Module):
126
+ """2D learnable position embedding with temporal extension."""
127
+
128
+ def __init__(
129
+ self,
130
+ height: int,
131
+ width: int,
132
+ num_frames: int,
133
+ dim: int,
134
+ interpolation_mode: str = "bicubic",
135
+ ) -> None:
136
+ super().__init__()
137
+ self.height = height
138
+ self.width = width
139
+ self.num_frames = num_frames
140
+ self.dim = dim
141
+ self.interpolation_mode = interpolation_mode
142
+ self.weight = nn.Parameter(torch.empty(height, width, dim))
143
+ self.register_buffer(
144
+ "time_weight",
145
+ torch.from_numpy(get_1d_sincos_pos_embed(self.dim, self.num_frames))
146
+ .float()
147
+ .unsqueeze(1),
148
+ persistent=False,
149
+ )
150
+
151
+ self.reset_parameters()
152
+
153
+ def reset_parameters(self):
154
+ nn.init.normal_(self.weight)
155
+
156
+ def forward(self, x: torch.Tensor, grid_thws: torch.Tensor) -> torch.Tensor:
157
+ pos_embs = []
158
+ for t, h, w in grid_thws.tolist():
159
+ assert t <= self.num_frames, f"t:{t} > self.num_frames:{self.num_frames}"
160
+ if (h, w) == self.weight.shape[:-1]:
161
+ pos_emb_2d = self.weight.flatten(end_dim=1)
162
+ else:
163
+ pos_emb_2d = get_rope_shape(
164
+ self.weight,
165
+ interpolation_mode=self.interpolation_mode,
166
+ shape=(h, w),
167
+ )
168
+
169
+ if t == 1:
170
+ pos_emb_3d = pos_emb_2d
171
+ else:
172
+ pos_emb_3d = (
173
+ pos_emb_2d.unsqueeze(0).repeat(t, 1, 1) + self.time_weight[0:t]
174
+ )
175
+
176
+ pos_embs.append(pos_emb_3d.reshape(-1, pos_emb_3d.shape[-1]))
177
+
178
+ out = x + torch.cat(pos_embs)
179
+ return out
180
+
181
+
182
+class MoonVision3dPatchEmbed(nn.Module):
183
+ """3D patch embedding for vision tower."""
184
+
185
+ def __init__(
186
+ self,
187
+ out_dim: int,
188
+ in_dim: int = 3,
189
+ patch_size: int | tuple[int, int] = (14, 14),
190
+ pos_emb_height: int = 14,
191
+ pos_emb_width: int = 14,
192
+ pos_emb_time: int = 4,
193
+ pos_emb_type: str = "divided_fixed",
194
+ ):
195
+ super().__init__()
196
+ assert isinstance(patch_size, int | Sequence), (
197
+ f"Invalid patch_size type: {type(patch_size)}"
198
+ )
199
+ if isinstance(patch_size, int):
200
+ patch_size = (patch_size, patch_size)
201
+ assert len(patch_size) == 2, (
202
+ f"Expected patch_size to be a tuple of 2, got {patch_size}"
203
+ )
204
+ self.patch_size = patch_size
205
+
206
+ self.proj = nn.Conv2d(
207
+ in_dim, out_dim, kernel_size=patch_size, stride=patch_size
208
+ )
209
+
210
+ if pos_emb_type == "divided_fixed":
211
+ self.pos_emb = Learnable2DInterpPosEmbDivided_fixed(
212
+ height=pos_emb_height,
213
+ width=pos_emb_width,
214
+ num_frames=pos_emb_time,
215
+ dim=out_dim,
216
+ )
217
+ else:
218
+ raise NotImplementedError(f"Not support pos_emb_type: {pos_emb_type}")
219
+
220
+ def forward(self, x: torch.Tensor, grid_thws: torch.Tensor) -> torch.Tensor:
221
+ x = self.proj(x).view(x.size(0), -1)
222
+ # apply positional embedding
223
+ x = self.pos_emb(x, grid_thws)
224
+ return x
225
+
226
+
227
+class Rope2DPosEmbRepeated(nn.Module):
228
+ """2D rotary position embedding with multi-resolution support."""
229
+
230
+ def __init__(self, dim: int, max_height: int, max_width: int, theta_base=10000):
231
+ super().__init__()
232
+ self.dim = dim
233
+ assert self.dim % 4 == 0, "dim must be divisible by 4"
234
+ self.max_height = max_height
235
+ self.max_width = max_width
236
+ self.theta_base = theta_base
237
+
238
+ def extra_repr(self):
239
+ return (
240
+ f"dim={self.dim}, max_height={self.max_height}, "
241
+ f"max_width={self.max_width}, theta_base={self.theta_base}"
242
+ )
243
+
244
+ def _precompute_freqs_cis(self, device: torch.device) -> torch.Tensor:
245
+ """Calculate the cis(freqs) for each position in the 2D grid."""
246
+ N = self.max_height * self.max_width
247
+ flat_pos = torch.arange(0, N).float().to(device)
248
+ x_pos = flat_pos % self.max_width
249
+ y_pos = flat_pos // self.max_width
250
+ dim_range = (
251
+ torch.arange(0, self.dim, 4)[: (self.dim // 4)].float().to(device)
252
+ ) # C/4
253
+ freqs = 1.0 / (self.theta_base ** (dim_range / self.dim))
254
+ x_freqs = torch.outer(x_pos, freqs).float() # N, C/4
255
+ y_freqs = torch.outer(y_pos, freqs).float() # N, C/4
256
+ x_cis = torch.polar(torch.ones_like(x_freqs), x_freqs) # N, C/4
257
+ y_cis = torch.polar(torch.ones_like(y_freqs), y_freqs) # N, C/4
258
+ # N, C/4, 2
259
+ freqs_cis = torch.cat(
260
+ [x_cis.unsqueeze(dim=-1), y_cis.unsqueeze(dim=-1)], dim=-1
261
+ )
262
+ # max_height, max_width, C/2
263
+ freqs_cis = freqs_cis.reshape(self.max_height, self.max_width, -1)
264
+ return freqs_cis
265
+
266
+ def get_freqs_cis(
267
+ self, grid_thws: torch.Tensor, device: torch.device
268
+ ) -> torch.Tensor:
269
+ """
270
+ Args:
271
+ grid_thws (torch.Tensor): grid time, height and width
272
+
273
+ Returns:
274
+ freqs_cis: tensor of shape (sum(t * height * width), dim//2)
275
+ """
276
+ if not hasattr(self, "freqs_cis"):
277
+ self.register_buffer(
278
+ "freqs_cis", self._precompute_freqs_cis(device), persistent=False
279
+ )
280
+
281
+ shapes = grid_thws.tolist()
282
+ assert all(
283
+ 1 <= h <= self.max_height and 1 <= w <= self.max_width for t, h, w in shapes
284
+ ), (
285
+ shapes,
286
+ self.max_height,
287
+ self.max_width,
288
+ )
289
+ freqs_cis = torch.cat(
290
+ [
291
+ self.freqs_cis[:h, :w].reshape(-1, self.dim // 2).repeat(t, 1)
292
+ for t, h, w in shapes
293
+ ],
294
+ dim=0,
295
+ )
296
+ return freqs_cis
297
+
298
+
299
+class MLP2(nn.Module):
300
+ """Two-layer MLP with tensor parallel support."""
301
+
302
+ def __init__(
303
+ self,
304
+ dims: list[int],
305
+ activation,
306
+ bias: bool = True,
307
+ prefix: str = "",
308
+ use_data_parallel: bool = False,
309
+ ):
310
+ super().__init__()
311
+ assert len(dims) == 3
312
+ self.use_data_parallel = use_data_parallel
313
+ self.fc0 = ColumnParallelLinear(
314
+ dims[0],
315
+ dims[1],
316
+ bias=bias,
317
+ prefix=maybe_prefix(prefix, "fc0"),
318
+ disable_tp=self.use_data_parallel,
319
+ )
320
+ self.fc1 = RowParallelLinear(
321
+ dims[1],
322
+ dims[2],
323
+ bias=bias,
324
+ prefix=maybe_prefix(prefix, "fc1"),
325
+ disable_tp=self.use_data_parallel,
326
+ )
327
+ self.activation = activation
328
+
329
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
330
+ x, _ = self.fc0(x)
331
+ x = self.activation(x)
332
+ x, _ = self.fc1(x)
333
+ return x
334
+
335
+
336
+class MoonViTEncoderLayer(nn.Module):
337
+ """Single encoder layer for MoonViT with TP/DP support."""
338
+
339
+ def __init__(
340
+ self,
341
+ num_heads: int,
342
+ hidden_dim: int,
343
+ mlp_dim: int,
344
+ prefix: str = "",
345
+ *,
346
+ activation=F.gelu,
347
+ attn_bias: bool = False,
348
+ ):
349
+ super().__init__()
350
+ self.use_data_parallel = is_vit_use_data_parallel()
351
+
352
+ self.num_heads = num_heads
353
+ self.hidden_dim = hidden_dim
354
+ self.hidden_size_per_attention_head = self.hidden_dim // self.num_heads
355
+ self.tp_size = (
356
+ 1 if self.use_data_parallel else get_tensor_model_parallel_world_size()
357
+ )
358
+ self.num_attention_heads_per_partition = divide(num_heads, self.tp_size)
359
+
360
+ self.norm0 = nn.LayerNorm(hidden_dim)
361
+ self.norm1 = nn.LayerNorm(hidden_dim)
362
+ self.mlp = MLP2(
363
+ [hidden_dim, mlp_dim, hidden_dim],
364
+ activation,
365
+ prefix=f"{prefix}.mlp",
366
+ use_data_parallel=self.use_data_parallel,
367
+ )
368
+ self.wqkv = QKVParallelLinear(
369
+ hidden_size=hidden_dim,
370
+ head_size=self.hidden_size_per_attention_head,
371
+ total_num_heads=num_heads,
372
+ total_num_kv_heads=num_heads,
373
+ bias=attn_bias,
374
+ prefix=f"{prefix}.wqkv",
375
+ disable_tp=self.use_data_parallel,
376
+ )
377
+ self.wo = RowParallelLinear(
378
+ hidden_dim,
379
+ hidden_dim,
380
+ bias=attn_bias,
381
+ prefix=f"{prefix}.wo",
382
+ disable_tp=self.use_data_parallel,
383
+ )
384
+ self.attn = MMEncoderAttention(
385
+ num_heads=self.num_attention_heads_per_partition,
386
+ head_size=self.hidden_size_per_attention_head,
387
+ scale=self.hidden_size_per_attention_head**-0.5,
388
+ prefix=f"{prefix}.attn",
389
+ )
390
+
391
+ def attention_qkvpacked(
392
+ self,
393
+ x: torch.Tensor,
394
+ cu_seqlens: torch.Tensor,
395
+ rope_freqs_cis: torch.Tensor | None = None,
396
+ ):
397
+ """Compute self-attention with packed QKV.
398
+
399
+ Args:
400
+ x (torch.Tensor): (seqlen, hidden_dim)
401
+ cu_seqlens (torch.Tensor): cumulative sequence lengths
402
+ """
403
+ seq_length = x.size(0)
404
+ xqkv, _ = self.wqkv(x)
405
+
406
+ qkv_shape = xqkv.size()[:-1] + (
407
+ 3,
408
+ self.num_attention_heads_per_partition,
409
+ self.hidden_size_per_attention_head,
410
+ )
411
+ # xqkv: (seqlen, 3, nheads, headdim)
412
+ xqkv = xqkv.view(*qkv_shape)
413
+ xq, xk, xv = torch.unbind(xqkv, dim=-3)
414
+
415
+ xq, xk = apply_rope(xq, xk, rope_freqs_cis)
416
+
417
+ max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
418
+ attn_out = self.attn(
419
+ xq.unsqueeze(0),
420
+ xk.unsqueeze(0),
421
+ xv.unsqueeze(0),
422
+ cu_seqlens=cu_seqlens,
423
+ max_seqlen=max_seqlen,
424
+ )
425
+ attn_out = attn_out.reshape(
426
+ seq_length,
427
+ self.num_attention_heads_per_partition
428
+ * self.hidden_size_per_attention_head,
429
+ )
430
+ attn_out, _ = self.wo(attn_out)
431
+ return attn_out
432
+
433
+ def forward(
434
+ self,
435
+ hidden_states: torch.Tensor,
436
+ cu_seqlens: torch.Tensor,
437
+ rope_freqs_cis: torch.Tensor | None = None,
438
+ ):
439
+ residual = hidden_states
440
+ hidden_states = self.norm0(hidden_states)
441
+
442
+ hidden_states = self.attention_qkvpacked(
443
+ hidden_states, cu_seqlens, rope_freqs_cis
444
+ )
445
+ hidden_states = residual + hidden_states
446
+
447
+ residual = hidden_states
448
+ hidden_states = self.norm1(hidden_states)
449
+ hidden_states = self.mlp(hidden_states)
450
+ hidden_states = residual + hidden_states
451
+
452
+ return hidden_states
453
+
454
+
455
+class MoonViT3dEncoder(nn.Module):
456
+ """Full encoder stack for MoonViT 3D."""
457
+
458
+ def __init__(
459
+ self,
460
+ hidden_dim: int,
461
+ num_layers: int,
462
+ block_cfg: dict,
463
+ video_attn_type: str = "spatial_temporal",
464
+ prefix: str = "",
465
+ ) -> None:
466
+ super().__init__()
467
+
468
+ assert video_attn_type == "spatial_temporal", (
469
+ f'video_attn_type must be "spatial_temporal", got {video_attn_type}'
470
+ )
471
+ self.video_attn_type = video_attn_type
472
+ self.rope_2d = Rope2DPosEmbRepeated(
473
+ block_cfg["hidden_dim"] // block_cfg["num_heads"], 512, 512
474
+ )
475
+ self.blocks = nn.ModuleList(
476
+ [
477
+ MoonViTEncoderLayer(
478
+ **block_cfg,
479
+ prefix=f"{prefix}.blocks.{layer_idx}",
480
+ )
481
+ for layer_idx in range(num_layers)
482
+ ]
483
+ )
484
+ self.final_layernorm = nn.LayerNorm(hidden_dim)
485
+
486
+ def forward(
487
+ self,
488
+ hidden_states: torch.Tensor,
489
+ grid_thws: torch.Tensor,
490
+ ) -> torch.Tensor:
491
+ rope_freqs_cis = self.rope_2d.get_freqs_cis(
492
+ grid_thws=grid_thws, device=hidden_states.device
493
+ )
494
+
495
+ lengths = torch.cat(
496
+ (
497
+ torch.zeros(1, dtype=grid_thws.dtype, device=grid_thws.device),
498
+ grid_thws[:, 0] * grid_thws[:, 1] * grid_thws[:, 2],
499
+ )
500
+ )
501
+
502
+ cu_seqlens = lengths.to(hidden_states.device).cumsum(dim=0, dtype=torch.int32)
503
+
504
+ for block in self.blocks:
505
+ hidden_states = block(
506
+ hidden_states, cu_seqlens, rope_freqs_cis=rope_freqs_cis
507
+ )
508
+
509
+ hidden_states = self.final_layernorm(hidden_states)
510
+
511
+ return hidden_states
512
+
513
+
514
+def tpool_patch_merger(
515
+ x: torch.Tensor,
516
+ grid_thws: torch.Tensor,
517
+ merge_kernel_size: tuple[int, int] = (2, 2),
518
+) -> list[torch.Tensor]:
519
+ """Temporal pooling patch merger."""
520
+ kh, kw = merge_kernel_size
521
+ lengths = (grid_thws[:, 0] * grid_thws[:, 1] * grid_thws[:, 2]).tolist()
522
+ seqs = x.split(lengths, dim=0)
523
+
524
+ outputs = []
525
+ for seq, (t, h, w) in zip(seqs, grid_thws.tolist()):
526
+ nh, nw = h // kh, w // kw
527
+ # Reshape: (t*h*w, d) -> (t, nh, kh, nw, kw, d)
528
+ v = seq.view(t, nh, kh, nw, kw, -1)
529
+ # Temporal pooling first (reduces tensor size before permute)
530
+ v = v.mean(dim=0) # (nh, kh, nw, kw, d)
531
+ # Spatial rearrangement: (nh, kh, nw, kw, d) -> (nh, nw, kh, kw, d)
532
+ out = v.permute(0, 2, 1, 3, 4).reshape(nh * nw, kh * kw, -1)
533
+ outputs.append(out)
534
+
535
+ return outputs
536
+
537
+
538
+class MoonViT3dPretrainedModel(nn.Module):
539
+ """Main vision tower model.
540
+
541
+ Uses KimiK25VisionConfig directly from transformers_utils/configs/kimi_k25.py.
542
+ """
543
+
544
+ def __init__(
545
+ self,
546
+ config: KimiK25VisionConfig,
547
+ prefix: str = "",
548
+ ):
549
+ super().__init__()
550
+ config = deepcopy(config)
551
+ self.config = config # Required for run_dp_sharded_mrope_vision_model
552
+ self.merge_kernel_size = config.merge_kernel_size
553
+ self.patch_size = config.patch_size
554
+ self.merge_type = config.merge_type
555
+
556
+ self.patch_embed = MoonVision3dPatchEmbed(
557
+ out_dim=config.hidden_size,
558
+ patch_size=config.patch_size,
559
+ pos_emb_height=config.init_pos_emb_height,
560
+ pos_emb_width=config.init_pos_emb_width,
561
+ pos_emb_time=config.init_pos_emb_time,
562
+ pos_emb_type=config.pos_emb_type,
563
+ )
564
+
565
+ self.encoder = MoonViT3dEncoder(
566
+ hidden_dim=config.hidden_size,
567
+ num_layers=config.num_hidden_layers,
568
+ block_cfg={
569
+ "num_heads": config.num_attention_heads,
570
+ "hidden_dim": config.hidden_size,
571
+ "mlp_dim": config.intermediate_size,
572
+ "activation": get_act_fn("gelu_pytorch_tanh"),
573
+ "attn_bias": True,
574
+ },
575
+ video_attn_type=config.video_attn_type,
576
+ prefix=maybe_prefix(prefix, "encoder"),
577
+ )
578
+
579
+ def forward(
580
+ self, pixel_values: torch.Tensor, grid_thws: torch.Tensor
581
+ ) -> torch.Tensor:
582
+ """
583
+ Args:
584
+ pixel_values (torch.Tensor): The input pixel values.
585
+ grid_thws (torch.Tensor): Temporal, height and width.
586
+
587
+ Returns:
588
+ torch.Tensor: The output tokens.
589
+ """
590
+ hidden_states = self.patch_embed(pixel_values, grid_thws)
591
+ hidden_states = self.encoder(hidden_states, grid_thws)
592
+ if (
593
+ self.merge_type == "sd2_tpool"
594
+ ): # spatial downsampling 2x with temporal pooling all
595
+ hidden_states = tpool_patch_merger(
596
+ hidden_states, grid_thws, merge_kernel_size=self.merge_kernel_size
597
+ )
598
+ else:
599
+ raise NotImplementedError(f"Not support {self.merge_type}")
600
+
601
+ return hidden_states
602
+
603
+
604
+@torch.inference_mode()
605
+def mm_projector_forward(mm_projector: torch.nn.Module, vt_output: list[torch.Tensor]):
606
+ """Apply MM projector to vision tower outputs."""
607
+ num_embedding_list = [x.shape[0] for x in vt_output]
608
+ batched = torch.cat(vt_output, dim=0)
609
+ proj_out = mm_projector(batched)
610
+ proj_out = proj_out.reshape(-1, proj_out.shape[-1])
611
+ proj_out = torch.split(proj_out, num_embedding_list)
612
+ return proj_out
613
+
614
+
615
+@torch.inference_mode()
616
+def vision_tower_forward(
617
+ vision_tower: Any,
618
+ pixel_values: torch.Tensor,
619
+ grid_thw: torch.Tensor,
620
+ mm_projector: Any,
621
+ use_data_parallel: bool,
622
+) -> list[torch.Tensor]:
623
+ """DP-sharded vision tower forward with mrope.
624
+
625
+ Uses vLLM's standard data parallelism utility to shard the batch
626
+ across available GPUs, enabling parallel processing of vision features.
627
+ """
628
+ if use_data_parallel:
629
+ grid_thw_list = grid_thw.tolist()
630
+ vt_outputs = run_dp_sharded_mrope_vision_model(
631
+ vision_model=vision_tower,
632
+ pixel_values=pixel_values,
633
+ grid_thw_list=grid_thw_list,
634
+ rope_type="rope_2d",
635
+ )
636
+ else:
637
+ vt_outputs = vision_tower(pixel_values, grid_thw)
638
+ tensors = mm_projector_forward(mm_projector, list(vt_outputs))
639
+ return list(tensors)
640
+
641
+
642
+class KimiK25MultiModalProjector(nn.Module):
643
+ """Multi-modal projector with patch merging for Kimi-K2.5."""
644
+
645
+ def __init__(
646
+ self,
647
+ config: KimiK25VisionConfig,
648
+ use_data_parallel: bool = False,
649
+ prefix: str = "",
650
+ ):
651
+ super().__init__()
652
+ self.use_data_parallel = use_data_parallel
653
+
654
+ # Hidden size after patch merging
655
+ merge_h, merge_w = config.merge_kernel_size
656
+ self.hidden_size = config.hidden_size * merge_h * merge_w
657
+
658
+ self.pre_norm = torch.nn.LayerNorm(config.hidden_size, eps=1e-5)
659
+ self.linear_1 = ReplicatedLinear(
660
+ self.hidden_size,
661
+ self.hidden_size,
662
+ bias=True,
663
+ prefix=maybe_prefix(prefix, "linear_1"),
664
+ )
665
+ self.linear_2 = ReplicatedLinear(
666
+ self.hidden_size,
667
+ config.mm_hidden_size,
668
+ bias=True,
669
+ prefix=maybe_prefix(prefix, "linear_2"),
670
+ )
671
+ self.act = GELUActivation()
672
+
673
+ def forward(self, image_features: torch.Tensor) -> torch.Tensor:
674
+ hidden_states = self.pre_norm(image_features).view(-1, self.hidden_size)
675
+ hidden_states, _ = self.linear_1(hidden_states)
676
+ hidden_states = self.act(hidden_states)
677
+ hidden_states, _ = self.linear_2(hidden_states)
678
+ return hidden_states