main
py 947 lines 32.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Vision tower implementation for Kimi-K2.5 model.
4
5 This module provides the vision encoder components for Kimi-K2.5,
6 including 3D patch embedding, RoPE position embedding, and
7 temporal pooling for video chunks.
8 """
9
10 from collections.abc import Sequence
11 from copy import deepcopy
12 from typing import Any
13
14 import numpy as np
15 import torch
16 import torch.nn as nn
17 import torch.nn.functional as F
18 from transformers.activations import GELUActivation
19
20 from vllm.distributed import divide, get_tensor_model_parallel_world_size
21 from vllm.logger import init_logger
22 from vllm.model_executor.layers.activation import get_act_fn
23 from vllm.model_executor.layers.attention.mm_encoder_attention import MMEncoderAttention
24 from vllm.model_executor.layers.conv import Conv2dLayer
25 from vllm.model_executor.layers.linear import (
26 ColumnParallelLinear,
27 QKVParallelLinear,
28 ReplicatedLinear,
29 RowParallelLinear,
30 )
31 from vllm.model_executor.layers.quantization import QuantizationConfig
32 from vllm.model_executor.layers.rotary_embedding.common import ApplyRotaryEmb
33 from vllm.model_executor.layers.rotary_embedding.packed_qk_rope import (
34 packed_qk_rope_,
35 )
36 from vllm.model_executor.models.utils import maybe_prefix
37 from vllm.model_executor.models.vision import (
38 is_vit_use_data_parallel,
39 run_dp_sharded_mrope_vision_model,
40 )
41 from vllm.platforms import current_platform
42 from vllm.transformers_utils.configs.kimi_k25 import KimiK25VisionConfig
43 from vllm.triton_utils import HAS_TRITON
44 from vllm.utils.torch_utils import async_tensor_h2d
45
46 logger = init_logger(__name__)
47
48
49 def _apply_rope_input_validation(x, freqs_cis):
50 assert x.ndim == freqs_cis.ndim + 1, (x.shape, freqs_cis.shape)
51 assert x.shape[:-2] == freqs_cis.shape[:-1], (x.shape, freqs_cis.shape)
52 assert x.shape[-1] == 2 * freqs_cis.shape[-1], (x.shape, freqs_cis.shape)
53 assert freqs_cis.dtype == torch.complex64, freqs_cis.dtype
54
55
56 def get_rope_shape_decorate(func):
57 _get_rope_shape_first_call_flag = set()
58
59 def wrapper(org, interpolation_mode, shape):
60 key = (org.requires_grad, torch.is_grad_enabled(), interpolation_mode)
61 if key not in _get_rope_shape_first_call_flag:
62 _get_rope_shape_first_call_flag.add(key)
63 _ = func(org, interpolation_mode, shape=(64, 64))
64 return func(org, interpolation_mode, shape)
65
66 return wrapper
67
68
69 @get_rope_shape_decorate
70 @torch.compile(
71 dynamic=True,
72 backend=current_platform.simple_compile_backend,
73 disable=current_platform.simple_compile_backend == "tpu",
74 )
75 def get_rope_shape(org, interpolation_mode, shape):
76 return (
77 F.interpolate(
78 org.permute((2, 0, 1)).unsqueeze(0),
79 size=shape,
80 mode=interpolation_mode,
81 )
82 .squeeze(0)
83 .permute((1, 2, 0))
84 .flatten(end_dim=1)
85 )
86
87
88 def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
89 """Generate 1D sincos positional embedding from grid positions."""
90 assert embed_dim % 2 == 0
91 omega = np.arange(embed_dim // 2, dtype=np.float32)
92 omega /= embed_dim / 2.0
93 omega = 1.0 / 10000**omega # (D/2,)
94
95 pos = pos.reshape(-1) # (M,)
96 out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
97
98 emb_sin = np.sin(out) # (M, D/2)
99 emb_cos = np.cos(out) # (M, D/2)
100
101 emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
102 return emb
103
104
105 def get_1d_sincos_pos_embed(embed_dim, t_size, cls_token=False):
106 """Generate 1D sincos positional embedding."""
107 grid_t = np.arange(t_size, dtype=np.float32)
108 pos_embed = get_1d_sincos_pos_embed_from_grid(embed_dim, grid_t)
109 if cls_token:
110 pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
111 return pos_embed
112
113
114 class Learnable2DInterpPosEmbDivided_fixed(nn.Module):
115 """2D learnable position embedding with temporal extension."""
116
117 def __init__(
118 self,
119 height: int,
120 width: int,
121 num_frames: int,
122 dim: int,
123 interpolation_mode: str = "bicubic",
124 ) -> None:
125 super().__init__()
126 self.height = height
127 self.width = width
128 self.num_frames = num_frames
129 self.dim = dim
130 self.interpolation_mode = interpolation_mode
131 self.weight = nn.Parameter(torch.empty(height, width, dim))
132 self.register_buffer(
133 "time_weight",
134 torch.from_numpy(get_1d_sincos_pos_embed(self.dim, self.num_frames))
135 .float()
136 .unsqueeze(1),
137 persistent=False,
138 )
139
140 self.reset_parameters()
141
142 def reset_parameters(self):
143 nn.init.normal_(self.weight)
144
145 def get_pos_embeds(self, grid_thws: torch.Tensor | list[list[int]]) -> torch.Tensor:
146 pos_embs = []
147 grid_thw_list = grid_thws if isinstance(grid_thws, list) else grid_thws.tolist()
148 for t, h, w in grid_thw_list:
149 assert t <= self.num_frames, f"t:{t} > self.num_frames:{self.num_frames}"
150 if (h, w) == self.weight.shape[:-1]:
151 pos_emb_2d = self.weight.flatten(end_dim=1)
152 else:
153 pos_emb_2d = get_rope_shape(
154 self.weight,
155 interpolation_mode=self.interpolation_mode,
156 shape=(h, w),
157 )
158
159 if t == 1:
160 pos_emb_3d = pos_emb_2d
161 else:
162 pos_emb_3d = (
163 pos_emb_2d.unsqueeze(0).repeat(t, 1, 1) + self.time_weight[0:t]
164 )
165
166 pos_embs.append(pos_emb_3d.reshape(-1, pos_emb_3d.shape[-1]))
167
168 return torch.cat(pos_embs)
169
170 def forward(
171 self, x: torch.Tensor, grid_thws: torch.Tensor | list[list[int]]
172 ) -> torch.Tensor:
173 return x + self.get_pos_embeds(grid_thws)
174
175
176 class MoonVision3dPatchEmbed(nn.Module):
177 """3D patch embedding for vision tower."""
178
179 def __init__(
180 self,
181 out_dim: int,
182 in_dim: int = 3,
183 patch_size: int | tuple[int, int] = (14, 14),
184 pos_emb_height: int = 14,
185 pos_emb_width: int = 14,
186 pos_emb_time: int = 4,
187 pos_emb_type: str = "divided_fixed",
188 patch_embed_proj_bias: bool = True,
189 pos_emb_interpolation_mode: str = "bicubic",
190 ):
191 super().__init__()
192 assert isinstance(patch_size, int | Sequence), (
193 f"Invalid patch_size type: {type(patch_size)}"
194 )
195 if isinstance(patch_size, int):
196 patch_size = (patch_size, patch_size)
197 assert len(patch_size) == 2, (
198 f"Expected patch_size to be a tuple of 2, got {patch_size}"
199 )
200 self.patch_size = patch_size
201
202 self.proj = Conv2dLayer(
203 in_dim,
204 out_dim,
205 kernel_size=patch_size,
206 stride=patch_size,
207 bias=patch_embed_proj_bias,
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 interpolation_mode=pos_emb_interpolation_mode,
217 )
218 else:
219 raise NotImplementedError(f"Not support pos_emb_type: {pos_emb_type}")
220
221 def forward(
222 self,
223 x: torch.Tensor,
224 grid_thws: torch.Tensor | list[list[int]] | None,
225 *,
226 pos_embeds: torch.Tensor | None = None,
227 ) -> torch.Tensor:
228 # forward_native dispatches this non-overlapping patch projection to GEMM.
229 x = self.proj.forward_native(x).view(x.size(0), self.proj.out_channels)
230 if pos_embeds is not None:
231 return x + pos_embeds
232 assert grid_thws is not None
233 return self.pos_emb(x, grid_thws)
234
235
236 class Rope2DPosEmbRepeated(nn.Module):
237 """2D rotary position embedding with multi-resolution support."""
238
239 def __init__(self, dim: int, max_height: int, max_width: int, theta_base=10000):
240 super().__init__()
241 self.dim = dim
242 assert self.dim % 4 == 0, "dim must be divisible by 4"
243 self.max_height = max_height
244 self.max_width = max_width
245 self.theta_base = theta_base
246
247 def extra_repr(self):
248 return (
249 f"dim={self.dim}, max_height={self.max_height}, "
250 f"max_width={self.max_width}, theta_base={self.theta_base}"
251 )
252
253 def _precompute_freqs_cis(self, device: torch.device) -> torch.Tensor:
254 """Calculate the cis(freqs) for each position in the 2D grid."""
255 N = self.max_height * self.max_width
256 flat_pos = torch.arange(0, N).float().to(device)
257 x_pos = flat_pos % self.max_width
258 y_pos = flat_pos // self.max_width
259 dim_range = (
260 torch.arange(0, self.dim, 4)[: (self.dim // 4)].float().to(device)
261 ) # C/4
262 freqs = 1.0 / (self.theta_base ** (dim_range / self.dim))
263 x_freqs = torch.outer(x_pos, freqs).float() # N, C/4
264 y_freqs = torch.outer(y_pos, freqs).float() # N, C/4
265 x_cis = torch.polar(torch.ones_like(x_freqs), x_freqs) # N, C/4
266 y_cis = torch.polar(torch.ones_like(y_freqs), y_freqs) # N, C/4
267 # N, C/4, 2
268 freqs_cis = torch.cat(
269 [x_cis.unsqueeze(dim=-1), y_cis.unsqueeze(dim=-1)], dim=-1
270 )
271 # max_height, max_width, C/2
272 freqs_cis = freqs_cis.reshape(self.max_height, self.max_width, -1)
273 return freqs_cis
274
275 def get_freqs_cis(
276 self, grid_thws: torch.Tensor | list[list[int]], device: torch.device
277 ) -> torch.Tensor:
278 """Args:
279 grid_thws (torch.Tensor): grid time, height and width
280
281 Returns:
282 freqs_cis: tensor of shape (sum(t * height * width), dim//2)
283
284 """
285 if not hasattr(self, "freqs_cis"):
286 self.register_buffer(
287 "freqs_cis", self._precompute_freqs_cis(device), persistent=False
288 )
289
290 shapes = grid_thws if isinstance(grid_thws, list) else grid_thws.tolist()
291 assert all(
292 1 <= h <= self.max_height and 1 <= w <= self.max_width for t, h, w in shapes
293 ), (
294 shapes,
295 self.max_height,
296 self.max_width,
297 )
298 freqs_cis = torch.cat(
299 [
300 self.freqs_cis[:h, :w].reshape(-1, self.dim // 2).repeat(t, 1)
301 for t, h, w in shapes
302 ],
303 dim=0,
304 )
305 return freqs_cis
306
307
308 class MLP2(nn.Module):
309 """Two-layer MLP with tensor parallel support."""
310
311 def __init__(
312 self,
313 dims: list[int],
314 activation,
315 bias: bool = True,
316 quant_config: QuantizationConfig | None = None,
317 prefix: str = "",
318 use_data_parallel: bool = False,
319 ):
320 super().__init__()
321 assert len(dims) == 3
322 self.use_data_parallel = use_data_parallel
323 self.fc0 = ColumnParallelLinear(
324 dims[0],
325 dims[1],
326 bias=bias,
327 quant_config=quant_config,
328 prefix=maybe_prefix(prefix, "fc0"),
329 disable_tp=self.use_data_parallel,
330 )
331 self.fc1 = RowParallelLinear(
332 dims[1],
333 dims[2],
334 bias=bias,
335 quant_config=quant_config,
336 prefix=maybe_prefix(prefix, "fc1"),
337 disable_tp=self.use_data_parallel,
338 )
339 self.activation = activation
340
341 def forward(self, x: torch.Tensor) -> torch.Tensor:
342 x, _ = self.fc0(x)
343 x = self.activation(x)
344 x, _ = self.fc1(x)
345 return x
346
347
348 def _make_vision_norm(norm_type: str, hidden_dim: int) -> nn.Module:
349 if norm_type == "layernorm":
350 return nn.LayerNorm(hidden_dim)
351 if norm_type == "rmsnorm":
352 return nn.RMSNorm(hidden_dim)
353 raise NotImplementedError(f"Not support norm_type: {norm_type}")
354
355
356 class MoonViTEncoderLayer(nn.Module):
357 """Single encoder layer for MoonViT with TP/DP support."""
358
359 def __init__(
360 self,
361 num_heads: int,
362 hidden_dim: int,
363 mlp_dim: int,
364 quant_config: QuantizationConfig | None = None,
365 prefix: str = "",
366 *,
367 activation=F.gelu,
368 attn_bias: bool = False,
369 qkv_hidden_size: int | None = None,
370 norm_type: str = "layernorm",
371 mlp_type: str = "mlp2",
372 linear_bias: bool = True,
373 ):
374 super().__init__()
375 self.use_data_parallel = is_vit_use_data_parallel(num_heads)
376
377 self.num_heads = num_heads
378 self.hidden_dim = hidden_dim
379 self.qkv_hidden_size = (
380 hidden_dim if qkv_hidden_size is None else qkv_hidden_size
381 )
382 self.hidden_size_per_attention_head = self.qkv_hidden_size // self.num_heads
383 self.tp_size = (
384 1 if self.use_data_parallel else get_tensor_model_parallel_world_size()
385 )
386 self.num_attention_heads_per_partition = divide(num_heads, self.tp_size)
387
388 self.norm0 = _make_vision_norm(norm_type, hidden_dim)
389 self.norm1 = _make_vision_norm(norm_type, hidden_dim)
390 if mlp_type != "mlp2":
391 raise NotImplementedError(f"Not support mlp_type: {mlp_type}")
392 self.mlp = MLP2(
393 [hidden_dim, mlp_dim, hidden_dim],
394 activation,
395 bias=linear_bias,
396 quant_config=quant_config,
397 prefix=f"{prefix}.mlp",
398 use_data_parallel=self.use_data_parallel,
399 )
400 self.wqkv = QKVParallelLinear(
401 hidden_size=hidden_dim,
402 head_size=self.hidden_size_per_attention_head,
403 total_num_heads=num_heads,
404 total_num_kv_heads=num_heads,
405 bias=attn_bias,
406 quant_config=quant_config,
407 prefix=f"{prefix}.wqkv",
408 disable_tp=self.use_data_parallel,
409 )
410 self.wo = RowParallelLinear(
411 self.qkv_hidden_size,
412 hidden_dim,
413 bias=attn_bias,
414 quant_config=quant_config,
415 prefix=f"{prefix}.wo",
416 disable_tp=self.use_data_parallel,
417 )
418 self.attn = MMEncoderAttention(
419 num_heads=self.num_attention_heads_per_partition,
420 head_size=self.hidden_size_per_attention_head,
421 scale=self.hidden_size_per_attention_head**-0.5,
422 prefix=f"{prefix}.attn",
423 )
424 self.apply_rotary_emb = ApplyRotaryEmb(
425 enforce_enable=True,
426 is_neox_style=False,
427 enable_fp32_compute=True,
428 )
429
430 def attention_qkvpacked(
431 self,
432 x: torch.Tensor,
433 cu_seqlens: torch.Tensor,
434 rope_freqs_cis: torch.Tensor,
435 max_seqlen: torch.Tensor | None = None,
436 sequence_lengths: torch.Tensor | None = None,
437 ):
438 """Compute self-attention with packed QKV.
439
440 Args:
441 x (torch.Tensor): (seqlen, hidden_dim)
442 cu_seqlens (torch.Tensor): cumulative sequence lengths
443 rope_freqs_cis (torch.Tensor): rotary embedding frequencies
444 max_seqlen (torch.Tensor | None): longest sequence in the batch
445 sequence_lengths (torch.Tensor | None): per-sequence lengths
446
447 """
448 seq_length = x.size(0)
449 xqkv, _ = self.wqkv(x)
450
451 qkv_shape = xqkv.size()[:-1] + (
452 3,
453 self.num_attention_heads_per_partition,
454 self.hidden_size_per_attention_head,
455 )
456 # xqkv: (seqlen, 3, nheads, headdim)
457 xqkv = xqkv.view(*qkv_shape)
458
459 # xq/xk alias xqkv, so the in-place rotation below is visible through
460 # them; only the fallback needs to rebind.
461 xq, xk, xv = torch.unbind(xqkv, dim=-3)
462 if HAS_TRITON:
463 packed_qk_rope_(xqkv, rope_freqs_cis)
464 else:
465 _apply_rope_input_validation(xq, rope_freqs_cis)
466 _apply_rope_input_validation(xk, rope_freqs_cis)
467 rope_cos = rope_freqs_cis.real.contiguous()
468 rope_sin = rope_freqs_cis.imag.contiguous()
469 xq = self.apply_rotary_emb(xq, rope_cos, rope_sin)
470 xk = self.apply_rotary_emb(xk, rope_cos, rope_sin)
471
472 if max_seqlen is None:
473 max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
474 attn_out = self.attn(
475 xq.unsqueeze(0),
476 xk.unsqueeze(0),
477 xv.unsqueeze(0),
478 cu_seqlens=cu_seqlens,
479 max_seqlen=max_seqlen,
480 sequence_lengths=sequence_lengths,
481 )
482 attn_out = attn_out.reshape(
483 seq_length,
484 self.num_attention_heads_per_partition
485 * self.hidden_size_per_attention_head,
486 )
487 attn_out, _ = self.wo(attn_out)
488 return attn_out
489
490 def forward(
491 self,
492 hidden_states: torch.Tensor,
493 cu_seqlens: torch.Tensor,
494 rope_freqs_cis: torch.Tensor,
495 max_seqlen: torch.Tensor | None = None,
496 sequence_lengths: torch.Tensor | None = None,
497 ):
498 residual = hidden_states
499 hidden_states = self.norm0(hidden_states)
500
501 hidden_states = self.attention_qkvpacked(
502 hidden_states,
503 cu_seqlens,
504 rope_freqs_cis,
505 max_seqlen=max_seqlen,
506 sequence_lengths=sequence_lengths,
507 )
508 hidden_states = residual + hidden_states
509
510 residual = hidden_states
511 hidden_states = self.norm1(hidden_states)
512 hidden_states = self.mlp(hidden_states)
513 hidden_states = residual + hidden_states
514
515 return hidden_states
516
517
518 class MoonViT3dEncoder(nn.Module):
519 """Full encoder stack for MoonViT 3D."""
520
521 def __init__(
522 self,
523 hidden_dim: int,
524 num_layers: int,
525 block_cfg: dict,
526 video_attn_type: str = "spatial_temporal",
527 quant_config: QuantizationConfig | None = None,
528 prefix: str = "",
529 ) -> None:
530 super().__init__()
531
532 assert video_attn_type == "spatial_temporal", (
533 f'video_attn_type must be "spatial_temporal", got {video_attn_type}'
534 )
535 self.video_attn_type = video_attn_type
536 qkv_hidden_size = block_cfg.get("qkv_hidden_size") or block_cfg["hidden_dim"]
537 self.rope_2d = Rope2DPosEmbRepeated(
538 qkv_hidden_size // block_cfg["num_heads"], 512, 512
539 )
540 self.blocks = nn.ModuleList(
541 [
542 MoonViTEncoderLayer(
543 **block_cfg,
544 quant_config=quant_config,
545 prefix=f"{prefix}.blocks.{layer_idx}",
546 )
547 for layer_idx in range(num_layers)
548 ]
549 )
550 self.final_layernorm = _make_vision_norm(
551 block_cfg.get("norm_type", "layernorm"), hidden_dim
552 )
553
554 def prepare_encoder_metadata(
555 self,
556 grid_thw_list: list[list[int]],
557 *,
558 device: torch.device,
559 max_batch_size: int | None = None,
560 max_seqlen_override: int | None = None,
561 ) -> dict[str, torch.Tensor | None]:
562 metadata: dict[str, torch.Tensor | None] = {}
563 metadata["rope_freqs_cis"] = self.rope_2d.get_freqs_cis(
564 grid_thw_list, device=device
565 )
566
567 grid_thw_np = np.array(grid_thw_list, dtype=np.int32)
568 lengths = grid_thw_np[:, 0] * grid_thw_np[:, 1] * grid_thw_np[:, 2]
569 cu_seqlens = np.concatenate(
570 [np.zeros(1, dtype=np.int32), lengths.cumsum(dtype=np.int32)]
571 )
572 if max_batch_size is not None:
573 num_seqs = len(cu_seqlens) - 1
574 if num_seqs < max_batch_size:
575 cu_seqlens = np.concatenate(
576 [
577 cu_seqlens,
578 np.full(
579 max_batch_size - num_seqs,
580 cu_seqlens[-1],
581 dtype=np.int32,
582 ),
583 ]
584 )
585
586 attn_backend = self.blocks[0].attn.attn_backend
587 metadata["sequence_lengths"] = MMEncoderAttention.maybe_compute_seq_lens(
588 attn_backend, cu_seqlens, device
589 )
590 max_seqlen = (
591 max_seqlen_override
592 if max_seqlen_override is not None
593 else MMEncoderAttention.compute_max_seqlen(attn_backend, cu_seqlens)
594 )
595 metadata["max_seqlen"] = torch.tensor(max_seqlen, dtype=torch.int32)
596 metadata["cu_seqlens"] = MMEncoderAttention.maybe_recompute_cu_seqlens(
597 attn_backend,
598 cu_seqlens,
599 self.blocks[0].hidden_dim,
600 self.blocks[0].tp_size,
601 device,
602 )
603 return metadata
604
605 def forward(
606 self,
607 hidden_states: torch.Tensor,
608 grid_thws: torch.Tensor | list[list[int]] | None,
609 *,
610 encoder_metadata: dict[str, torch.Tensor | None] | None = None,
611 ) -> torch.Tensor:
612 if encoder_metadata is None:
613 assert grid_thws is not None
614 grid_thw_list = (
615 grid_thws if isinstance(grid_thws, list) else grid_thws.tolist()
616 )
617 encoder_metadata = self.prepare_encoder_metadata(
618 grid_thw_list, device=hidden_states.device
619 )
620
621 rope_freqs_cis = encoder_metadata["rope_freqs_cis"]
622 cu_seqlens = encoder_metadata["cu_seqlens"]
623 max_seqlen = encoder_metadata["max_seqlen"]
624 sequence_lengths = encoder_metadata.get("sequence_lengths")
625 assert rope_freqs_cis is not None
626 assert cu_seqlens is not None
627 assert max_seqlen is not None
628
629 for block in self.blocks:
630 hidden_states = block(
631 hidden_states,
632 cu_seqlens,
633 rope_freqs_cis=rope_freqs_cis,
634 max_seqlen=max_seqlen,
635 sequence_lengths=sequence_lengths,
636 )
637
638 hidden_states = self.final_layernorm(hidden_states)
639
640 return hidden_states
641
642
643 def tpool_patch_merger(
644 x: torch.Tensor,
645 grid_thws: torch.Tensor | list[list[int]],
646 merge_kernel_size: tuple[int, int] = (2, 2),
647 ) -> list[torch.Tensor]:
648 """Temporal pooling patch merger."""
649 kh, kw = merge_kernel_size
650 grid_thw_list = grid_thws if isinstance(grid_thws, list) else grid_thws.tolist()
651 lengths = [t * h * w for t, h, w in grid_thw_list]
652 seqs = x.split(lengths, dim=0)
653
654 outputs = []
655 for seq, (t, h, w) in zip(seqs, grid_thw_list):
656 nh, nw = h // kh, w // kw
657 # Reshape: (t*h*w, d) -> (t, nh, kh, nw, kw, d)
658 v = seq.view(t, nh, kh, nw, kw, -1)
659 # Temporal pooling first (reduces tensor size before permute)
660 v = v.mean(dim=0) # (nh, kh, nw, kw, d)
661 # Spatial rearrangement: (nh, kh, nw, kw, d) -> (nh, nw, kh, kw, d)
662 out = v.permute(0, 2, 1, 3, 4).reshape(nh * nw, kh * kw, -1)
663 outputs.append(out)
664
665 return outputs
666
667
668 def build_image_merge_gather_idx(
669 grid_thws: list[list[int]] | list[tuple[int, int, int]],
670 merge_kernel_size: tuple[int, int],
671 ) -> np.ndarray:
672 """Build packed spatial-merge indices for image-only CUDA graphs."""
673 kh, kw = merge_kernel_size
674 parts: list[np.ndarray] = []
675 offset = 0
676 for t, h, w in grid_thws:
677 if t != 1:
678 raise ValueError("Image encoder CUDA graphs require grid T == 1")
679 idx = np.arange(h * w, dtype=np.int64).reshape(h, w)
680 idx = idx.reshape(h // kh, kh, w // kw, kw)
681 parts.append(idx.transpose(0, 2, 1, 3).reshape(-1, kh * kw) + offset)
682 offset += h * w
683 if not parts:
684 return np.empty((0, kh * kw), dtype=np.int64)
685 return np.concatenate(parts)
686
687
688 def tpool_patch_merger_packed(
689 x: torch.Tensor,
690 merge_gather_idx: torch.Tensor,
691 ) -> torch.Tensor:
692 """Apply the image-only spatial merge using precomputed tensor indices."""
693 return x[merge_gather_idx]
694
695
696 class MoonViT3dPretrainedModel(nn.Module):
697 """Main vision tower model.
698
699 Uses KimiK25VisionConfig directly from transformers_utils/configs/kimi_k25.py.
700 """
701
702 def __init__(
703 self,
704 config: KimiK25VisionConfig,
705 quant_config: QuantizationConfig | None = None,
706 prefix: str = "",
707 ):
708 super().__init__()
709 config = deepcopy(config)
710 self.config = config # Required for run_dp_sharded_mrope_vision_model
711 self.merge_kernel_size = config.merge_kernel_size
712 self.patch_size = config.patch_size
713 self.merge_type = config.merge_type
714
715 self.patch_embed = MoonVision3dPatchEmbed(
716 out_dim=config.hidden_size,
717 patch_size=config.patch_size,
718 pos_emb_height=config.init_pos_emb_height,
719 pos_emb_width=config.init_pos_emb_width,
720 pos_emb_time=config.init_pos_emb_time,
721 pos_emb_type=config.pos_emb_type,
722 patch_embed_proj_bias=getattr(config, "patch_embed_proj_bias", True),
723 pos_emb_interpolation_mode=getattr(
724 config, "pos_emb_interpolation_mode", "bicubic"
725 ),
726 )
727
728 self.encoder = MoonViT3dEncoder(
729 hidden_dim=config.hidden_size,
730 num_layers=config.num_hidden_layers,
731 block_cfg={
732 "num_heads": config.num_attention_heads,
733 "hidden_dim": config.hidden_size,
734 "qkv_hidden_size": getattr(config, "qkv_hidden_size", None),
735 "mlp_dim": config.intermediate_size,
736 "activation": get_act_fn(
737 getattr(config, "activation_func", "gelu_pytorch_tanh")
738 ),
739 "attn_bias": getattr(config, "attn_bias", True),
740 "norm_type": getattr(config, "norm_type", "layernorm"),
741 "mlp_type": getattr(config, "mlp_type", "mlp2"),
742 "linear_bias": getattr(config, "linear_bias", True),
743 },
744 video_attn_type=config.video_attn_type,
745 quant_config=quant_config,
746 prefix=maybe_prefix(prefix, "encoder"),
747 )
748
749 def forward(
750 self,
751 pixel_values: torch.Tensor,
752 grid_thws: torch.Tensor | list[list[int]] | None,
753 *,
754 encoder_metadata: dict[str, torch.Tensor | None] | None = None,
755 ) -> torch.Tensor:
756 """Args:
757 pixel_values (torch.Tensor): The input pixel values.
758 grid_thws (torch.Tensor): Temporal, height and width.
759
760 Returns:
761 torch.Tensor: The output tokens.
762
763 """
764 if encoder_metadata is not None and "pos_embeds" in encoder_metadata:
765 hidden_states = self.patch_embed(
766 pixel_values,
767 None,
768 pos_embeds=encoder_metadata["pos_embeds"],
769 )
770 hidden_states = self.encoder(
771 hidden_states,
772 None,
773 encoder_metadata=encoder_metadata,
774 )
775 merge_gather_idx = encoder_metadata["merge_gather_idx"]
776 assert merge_gather_idx is not None
777 return tpool_patch_merger_packed(hidden_states, merge_gather_idx)
778
779 assert grid_thws is not None
780 grid_thw_list = grid_thws if isinstance(grid_thws, list) else grid_thws.tolist()
781 if encoder_metadata is None:
782 encoder_metadata = self.encoder.prepare_encoder_metadata(
783 grid_thw_list, device=pixel_values.device
784 )
785
786 hidden_states = self.patch_embed(pixel_values, grid_thw_list)
787 hidden_states = self.encoder(
788 hidden_states,
789 grid_thw_list,
790 encoder_metadata=encoder_metadata,
791 )
792 if (
793 self.merge_type == "sd2_tpool"
794 ): # spatial downsampling 2x with temporal pooling all
795 hidden_states = tpool_patch_merger(
796 hidden_states, grid_thw_list, merge_kernel_size=self.merge_kernel_size
797 )
798 else:
799 raise NotImplementedError(f"Not support {self.merge_type}")
800
801 return hidden_states
802
803 def prepare_encoder_cudagraph_metadata(
804 self,
805 grid_thw_list: list[list[int]],
806 *,
807 max_batch_size: int | None,
808 max_seqlen_override: int | None = None,
809 device: torch.device,
810 ) -> dict[str, torch.Tensor | None]:
811 """Precompute fixed-buffer metadata for image encoder CUDA graphs."""
812 grid_thw_list = [list(map(int, grid)) for grid in grid_thw_list]
813 metadata = self.encoder.prepare_encoder_metadata(
814 grid_thw_list,
815 device=device,
816 max_batch_size=max_batch_size,
817 max_seqlen_override=max_seqlen_override,
818 )
819 metadata["pos_embeds"] = self.patch_embed.pos_emb.get_pos_embeds(
820 grid_thw_list
821 ).to(device=device)
822 merge_gather_idx = build_image_merge_gather_idx(
823 grid_thw_list, self.merge_kernel_size
824 )
825 metadata["merge_gather_idx"] = async_tensor_h2d(merge_gather_idx, device=device)
826 return metadata
827
828
829 @torch.inference_mode()
830 def mm_projector_forward(mm_projector: torch.nn.Module, vt_output: list[torch.Tensor]):
831 """Apply MM projector to vision tower outputs."""
832 num_embedding_list = [x.shape[0] for x in vt_output]
833 batched = torch.cat(vt_output, dim=0)
834 projector_dtype = next(mm_projector.parameters()).dtype
835 if batched.dtype != projector_dtype:
836 batched = batched.to(projector_dtype)
837 proj_out = mm_projector(batched)
838 proj_out = proj_out.reshape(-1, proj_out.shape[-1])
839 proj_out = torch.split(proj_out, num_embedding_list)
840 return proj_out
841
842
843 @torch.inference_mode()
844 def vision_tower_forward(
845 vision_tower: Any,
846 pixel_values: torch.Tensor,
847 grid_thw: torch.Tensor,
848 mm_projector: Any,
849 use_data_parallel: bool,
850 ) -> list[torch.Tensor]:
851 """DP-sharded vision tower forward with mrope.
852
853 Uses vLLM's standard data parallelism utility to shard the batch
854 across available GPUs, enabling parallel processing of vision features.
855 """
856 if use_data_parallel:
857 grid_thw_list = grid_thw.tolist()
858 vt_outputs = run_dp_sharded_mrope_vision_model(
859 vision_model=vision_tower,
860 pixel_values=pixel_values,
861 grid_thw_list=grid_thw_list,
862 rope_type="rope_2d",
863 )
864 else:
865 grid_thw_list = grid_thw.tolist()
866 encoder_metadata = vision_tower.encoder.prepare_encoder_metadata(
867 grid_thw_list, device=pixel_values.device
868 )
869 vt_outputs = vision_tower(
870 pixel_values,
871 grid_thw_list,
872 encoder_metadata=encoder_metadata,
873 )
874 tensors = mm_projector_forward(mm_projector, list(vt_outputs))
875 return list(tensors)
876
877
878 class KimiK25MultiModalProjector(nn.Module):
879 """Multi-modal projector with patch merging for Kimi-K2.5."""
880
881 def __init__(
882 self,
883 config: KimiK25VisionConfig,
884 use_data_parallel: bool = False,
885 quant_config: QuantizationConfig | None = None,
886 prefix: str = "",
887 ):
888 super().__init__()
889 self.use_data_parallel = use_data_parallel
890 self.mm_projector_type = getattr(config, "mm_projector_type", "patchmerger")
891
892 # Hidden size after patch merging
893 merge_h, merge_w = config.merge_kernel_size
894 self.hidden_size = config.hidden_size * merge_h * merge_w
895
896 if self.mm_projector_type == "patchmergerv2":
897 self.linear_1 = ReplicatedLinear(
898 self.hidden_size,
899 self.hidden_size,
900 bias=False,
901 quant_config=quant_config,
902 prefix=f"{prefix}.linear_1",
903 )
904 self.linear_2 = ReplicatedLinear(
905 self.hidden_size,
906 getattr(config, "text_hidden_size", config.mm_hidden_size),
907 bias=False,
908 quant_config=quant_config,
909 prefix=f"{prefix}.linear_2",
910 )
911 self.post_norm = torch.nn.RMSNorm(
912 getattr(config, "text_hidden_size", config.mm_hidden_size),
913 eps=config.projector_ln_eps,
914 )
915 self.act = GELUActivation()
916 return
917
918 self.pre_norm = torch.nn.LayerNorm(config.hidden_size, eps=1e-5)
919 self.linear_1 = ReplicatedLinear(
920 self.hidden_size,
921 self.hidden_size,
922 bias=True,
923 quant_config=quant_config,
924 prefix=f"{prefix}.linear_1",
925 )
926 self.linear_2 = ReplicatedLinear(
927 self.hidden_size,
928 config.mm_hidden_size,
929 bias=True,
930 quant_config=quant_config,
931 prefix=f"{prefix}.linear_2",
932 )
933 self.act = GELUActivation()
934
935 def forward(self, image_features: torch.Tensor) -> torch.Tensor:
936 if self.mm_projector_type == "patchmergerv2":
937 hidden_states = image_features.view(image_features.shape[0], -1)
938 hidden_states, _ = self.linear_1(hidden_states)
939 hidden_states = self.act(hidden_states)
940 hidden_states, _ = self.linear_2(hidden_states)
941 return self.post_norm(hidden_states)
942
943 hidden_states = self.pre_norm(image_features).view(-1, self.hidden_size)
944 hidden_states, _ = self.linear_1(hidden_states)
945 hidden_states = self.act(hidden_states)
946 hidden_states, _ = self.linear_2(hidden_states)
947 return hidden_states