[EPD][VLM] Support Kimi K25 EPD (#22269)
Signed-off-by: LHXuuu <xulianhao.xlh@antgroup.com>
This commit is contained in:
@@ -124,7 +124,8 @@ def _convert(data):
|
|||||||
|
|
||||||
|
|
||||||
_mm_grid_attrs = {
|
_mm_grid_attrs = {
|
||||||
Modality.IMAGE: ["image_grid_thw", "image_grid_hws"],
|
# Kimi K2.5 HF processor uses grid_thws (see base_processor.ATTR_NAME_TO_MODALITY).
|
||||||
|
Modality.IMAGE: ["image_grid_thw", "image_grid_hws", "grid_thws"],
|
||||||
Modality.VIDEO: ["video_grid_thw"],
|
Modality.VIDEO: ["video_grid_thw"],
|
||||||
Modality.AUDIO: ["audio_feature_lens_raw"],
|
Modality.AUDIO: ["audio_feature_lens_raw"],
|
||||||
}
|
}
|
||||||
@@ -136,9 +137,14 @@ _mm_feature_attrs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_mm_grid_dim(mm_inputs, modality):
|
def _get_mm_grid_dim(mm_inputs, modality, model_type: Optional[str] = None):
|
||||||
for attr in _mm_grid_attrs[modality]:
|
# Kimi K2.5 vision processor only emits `grid_thws`; prefer it over generic keys
|
||||||
if attr in mm_inputs:
|
# so we never pick a mis-typed or stale `image_grid_hws` field from kwargs.
|
||||||
|
attrs = _mm_grid_attrs[modality]
|
||||||
|
if (model_type or "").lower() == "kimi_k25" and modality == Modality.IMAGE:
|
||||||
|
attrs = ("grid_thws", "image_grid_thw", "image_grid_hws")
|
||||||
|
for attr in attrs:
|
||||||
|
if attr in mm_inputs and mm_inputs[attr] is not None:
|
||||||
return mm_inputs[attr]
|
return mm_inputs[attr]
|
||||||
raise ValueError(f"Grid dim ({_mm_grid_attrs[modality]}) not found in {mm_inputs}")
|
raise ValueError(f"Grid dim ({_mm_grid_attrs[modality]}) not found in {mm_inputs}")
|
||||||
|
|
||||||
@@ -577,6 +583,18 @@ class MMEncoder:
|
|||||||
else:
|
else:
|
||||||
return int(grid[0] * grid[1] * grid[2])
|
return int(grid[0] * grid[1] * grid[2])
|
||||||
|
|
||||||
|
def _kimi_k25_tokens_from_patch_grid(
|
||||||
|
self, grid: Union[torch.Tensor, List[int]]
|
||||||
|
) -> int:
|
||||||
|
"""MoonViT + tpool: output len is (h//mh)*(w//mw); temporal dim is pooled (not t*h*w/merge^2)."""
|
||||||
|
if isinstance(grid, torch.Tensor):
|
||||||
|
flat = grid.flatten()
|
||||||
|
_t, h, w = (int(x) for x in flat[:3].tolist())
|
||||||
|
else:
|
||||||
|
_t, h, w = int(grid[0]), int(grid[1]), int(grid[2])
|
||||||
|
merge_h, merge_w = self.model_config.hf_config.vision_config.merge_kernel_size
|
||||||
|
return (h * w) // (merge_h * merge_w)
|
||||||
|
|
||||||
def get_num_tokens(
|
def get_num_tokens(
|
||||||
self, grid: Union[torch.Tensor, List[int]], modality: Modality
|
self, grid: Union[torch.Tensor, List[int]], modality: Modality
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -585,6 +603,8 @@ class MMEncoder:
|
|||||||
input_length = self.get_num_patches(grid, modality)
|
input_length = self.get_num_patches(grid, modality)
|
||||||
return self._get_feat_extract_output_lengths(input_length)
|
return self._get_feat_extract_output_lengths(input_length)
|
||||||
else:
|
else:
|
||||||
|
if self.model_type == "kimi_k25" and modality == Modality.IMAGE:
|
||||||
|
return self._kimi_k25_tokens_from_patch_grid(grid)
|
||||||
merge_size = getattr(self.image_processor, "merge_size", 2)
|
merge_size = getattr(self.image_processor, "merge_size", 2)
|
||||||
return self.get_num_patches(grid, modality) // (merge_size**2)
|
return self.get_num_patches(grid, modality) // (merge_size**2)
|
||||||
|
|
||||||
@@ -625,7 +645,7 @@ class MMEncoder:
|
|||||||
"""
|
"""
|
||||||
GPU Task: Run ViT inference ONLY on the subset of mm items missing from the cache.
|
GPU Task: Run ViT inference ONLY on the subset of mm items missing from the cache.
|
||||||
"""
|
"""
|
||||||
grid_thw = _get_mm_grid_dim(mm_inputs, modality)
|
grid_thw = _get_mm_grid_dim(mm_inputs, modality, self.model_type)
|
||||||
|
|
||||||
# 1. Slice mm_feature to get only the patches for missing mm items
|
# 1. Slice mm_feature to get only the patches for missing mm items
|
||||||
sub_feature_list = []
|
sub_feature_list = []
|
||||||
@@ -675,7 +695,7 @@ class MMEncoder:
|
|||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
# mm_inputs: dict
|
# mm_inputs: dict
|
||||||
mm_inputs, get_feature_fn = await self._process_mm_items(mm_items, modality)
|
mm_inputs, get_feature_fn = await self._process_mm_items(mm_items, modality)
|
||||||
grid_thw = _get_mm_grid_dim(mm_inputs, modality)
|
grid_thw = _get_mm_grid_dim(mm_inputs, modality, self.model_type)
|
||||||
mm_feature = _convert(_get_mm_feature(mm_inputs, modality))
|
mm_feature = _convert(_get_mm_feature(mm_inputs, modality))
|
||||||
num_items = len(grid_thw)
|
num_items = len(grid_thw)
|
||||||
|
|
||||||
@@ -849,10 +869,30 @@ class MMEncoder:
|
|||||||
]
|
]
|
||||||
return timestamps
|
return timestamps
|
||||||
|
|
||||||
|
def _normalize_kimi_k25_encoder_images(self, images):
|
||||||
|
"""KimiK25VisionProcessor.preprocess expects MediaInput dicts, not raw PIL."""
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
|
def wrap_one(img):
|
||||||
|
if isinstance(img, dict) and img.get("type") in ("image", "video_chunk"):
|
||||||
|
return img
|
||||||
|
if isinstance(img, PILImage.Image):
|
||||||
|
return {"type": "image", "image": img}
|
||||||
|
return img
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
return images
|
||||||
|
# Disagg may supply a nested list; Kimi preprocess expects a flat list of media.
|
||||||
|
if isinstance(images[0], (list, tuple)):
|
||||||
|
images = [x for group in images for x in group]
|
||||||
|
return [wrap_one(img) for img in images]
|
||||||
|
|
||||||
async def _process_mm_items(self, mm_items, modality):
|
async def _process_mm_items(self, mm_items, modality):
|
||||||
if modality == Modality.IMAGE and self.image_processor:
|
if modality == Modality.IMAGE and self.image_processor:
|
||||||
images = await self._flatten_and_load_images(mm_items)
|
images = await self._flatten_and_load_images(mm_items)
|
||||||
image_config = self.vision_config.get("image", {})
|
image_config = self.vision_config.get("image", {})
|
||||||
|
if self.model_type == "kimi_k25":
|
||||||
|
images = self._normalize_kimi_k25_encoder_images(images)
|
||||||
processor_input = self.image_processor(images=images, **image_config)
|
processor_input = self.image_processor(images=images, **image_config)
|
||||||
if hasattr(self.model, "thinker"): # for omni models
|
if hasattr(self.model, "thinker"): # for omni models
|
||||||
get_feature_method = self.model.thinker.get_image_feature
|
get_feature_method = self.model.thinker.get_image_feature
|
||||||
@@ -985,7 +1025,11 @@ class MMEncoder:
|
|||||||
self.profiler.step()
|
self.profiler.step()
|
||||||
|
|
||||||
aux_data = _build_mm_aux_data(mm_inputs)
|
aux_data = _build_mm_aux_data(mm_inputs)
|
||||||
return _get_mm_grid_dim(mm_inputs, modality), mm_embedding, aux_data
|
return (
|
||||||
|
_get_mm_grid_dim(mm_inputs, modality, self.model_type),
|
||||||
|
mm_embedding,
|
||||||
|
aux_data,
|
||||||
|
)
|
||||||
except BadRequestError as e:
|
except BadRequestError as e:
|
||||||
raise BadRequestError(f"Bad request error: {str(e)}")
|
raise BadRequestError(f"Bad request error: {str(e)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -708,33 +708,32 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
|||||||
# Create mm projector
|
# Create mm projector
|
||||||
self.mm_projector = K2VLMultiModalProjector(config.vision_config)
|
self.mm_projector = K2VLMultiModalProjector(config.vision_config)
|
||||||
|
|
||||||
self.language_model = DeepseekV3ForCausalLM(
|
self.language_model = None
|
||||||
config.text_config,
|
if not config.encoder_only:
|
||||||
quant_config,
|
self.language_model = DeepseekV3ForCausalLM(
|
||||||
prefix=(
|
config.text_config,
|
||||||
"language_model" if isinstance(quant_config, ModelSlimConfig) else ""
|
quant_config,
|
||||||
),
|
prefix=(
|
||||||
)
|
"language_model"
|
||||||
|
if isinstance(quant_config, ModelSlimConfig)
|
||||||
self.model = self.language_model.model
|
else ""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure that the dtype of the vision_tower and mm_projector matches that of the language_model.
|
# Ensure that the dtype of the vision_tower and mm_projector matches that of the language_model.
|
||||||
# This solves the dtype mismatch issue when using device_map="auto" and torch_dtype.
|
# This solves the dtype mismatch issue when using device_map="auto" and torch_dtype.
|
||||||
if hasattr(self.language_model, "dtype"):
|
if self.language_model is not None and hasattr(self.language_model, "dtype"):
|
||||||
target_dtype = self.language_model.dtype
|
target_dtype = self.language_model.dtype
|
||||||
self.vision_tower = self.vision_tower.to(dtype=target_dtype)
|
self.vision_tower = self.vision_tower.to(dtype=target_dtype)
|
||||||
self.mm_projector = self.mm_projector.to(dtype=target_dtype)
|
self.mm_projector = self.mm_projector.to(dtype=target_dtype)
|
||||||
|
|
||||||
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
||||||
pixel_values = torch.cat([item.feature for item in items], dim=0).type(
|
device = self.vision_tower.device
|
||||||
self.vision_tower.dtype
|
|
||||||
)
|
|
||||||
grid_thws = torch.concat([item.grid_thws for item in items], dim=0).to(
|
|
||||||
self.vision_tower.device
|
|
||||||
)
|
|
||||||
|
|
||||||
target_dtype = self.vision_tower.patch_embed.proj.weight.dtype
|
target_dtype = self.vision_tower.patch_embed.proj.weight.dtype
|
||||||
pixel_values = pixel_values.to(target_dtype)
|
pixel_values = torch.cat([item.feature for item in items], dim=0).to(
|
||||||
|
device=device, dtype=target_dtype
|
||||||
|
)
|
||||||
|
grid_thws = torch.concat([item.grid_thws for item in items], dim=0).to(device)
|
||||||
|
|
||||||
if self.use_data_parallel:
|
if self.use_data_parallel:
|
||||||
image_embeds = run_dp_sharded_mrope_vision_model(
|
image_embeds = run_dp_sharded_mrope_vision_model(
|
||||||
@@ -761,15 +760,22 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def start_layer(self) -> int:
|
def start_layer(self) -> int:
|
||||||
return self.language_model.start_layer
|
return self.language_model.start_layer if self.language_model is not None else 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end_layer(self) -> int:
|
def end_layer(self) -> int:
|
||||||
return self.language_model.end_layer
|
if self.language_model is not None:
|
||||||
|
return self.language_model.end_layer
|
||||||
|
text_config = getattr(self.config, "text_config", None)
|
||||||
|
return int(getattr(text_config, "num_hidden_layers", 0))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def routed_experts_weights_of_layer(self):
|
def routed_experts_weights_of_layer(self):
|
||||||
return self.language_model._routed_experts_weights_of_layer.value
|
return (
|
||||||
|
self.language_model._routed_experts_weights_of_layer.value
|
||||||
|
if self.language_model is not None
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
@@ -814,19 +820,20 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
|||||||
# All other weights go to language model
|
# All other weights go to language model
|
||||||
language_weights.append((name, loaded_weight))
|
language_weights.append((name, loaded_weight))
|
||||||
|
|
||||||
# Load vision tower weights
|
if not self.config.language_only:
|
||||||
vision_state_dict = dict(vision_weights)
|
# Load vision tower weights
|
||||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
vision_state_dict = dict(vision_weights)
|
||||||
for name, loaded_weight in vision_state_dict.items():
|
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||||
if name not in params_dict:
|
for name, loaded_weight in vision_state_dict.items():
|
||||||
raise ValueError(f"Weight {name} not found in params_dict")
|
if name not in params_dict:
|
||||||
param = params_dict[name]
|
raise ValueError(f"Weight {name} not found in params_dict")
|
||||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
param = params_dict[name]
|
||||||
# loaded_weight = self._pad_vit_attn_dummy_heads(name, loaded_weight)
|
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||||
weight_loader(param, loaded_weight)
|
# loaded_weight = self._pad_vit_attn_dummy_heads(name, loaded_weight)
|
||||||
|
weight_loader(param, loaded_weight)
|
||||||
|
|
||||||
# Load language model weights
|
# Load language model weights
|
||||||
if language_weights:
|
if not self.config.encoder_only and language_weights:
|
||||||
self.language_model.load_weights(language_weights)
|
self.language_model.load_weights(language_weights)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -842,7 +849,9 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
|||||||
self, layer_ids: Optional[List[int]] = None
|
self, layer_ids: Optional[List[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the layers to capture for EAGLE3 speculative decoding."""
|
"""Set the layers to capture for EAGLE3 speculative decoding."""
|
||||||
if not hasattr(self.language_model, "set_eagle3_layers_to_capture"):
|
if self.language_model is None or not hasattr(
|
||||||
|
self.language_model, "set_eagle3_layers_to_capture"
|
||||||
|
):
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
"language_model does not support EAGLE3 speculative decoding."
|
"language_model does not support EAGLE3 speculative decoding."
|
||||||
)
|
)
|
||||||
@@ -875,7 +884,9 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
|||||||
|
|
||||||
def get_embed_and_head(self) -> Tuple[torch.Tensor, torch.Tensor]:
|
def get_embed_and_head(self) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
"""Get embedding and LM head weights for speculative decoding."""
|
"""Get embedding and LM head weights for speculative decoding."""
|
||||||
if not hasattr(self.language_model, "get_embed_and_head"):
|
if self.language_model is None or not hasattr(
|
||||||
|
self.language_model, "get_embed_and_head"
|
||||||
|
):
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
"language_model does not support get_embed_and_head()."
|
"language_model does not support get_embed_and_head()."
|
||||||
)
|
)
|
||||||
@@ -884,7 +895,9 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
|||||||
|
|
||||||
def set_embed_and_head(self, embed: torch.Tensor, head: torch.Tensor) -> None:
|
def set_embed_and_head(self, embed: torch.Tensor, head: torch.Tensor) -> None:
|
||||||
"""Set embedding and LM head weights for speculative decoding."""
|
"""Set embedding and LM head weights for speculative decoding."""
|
||||||
if not hasattr(self.language_model, "set_embed_and_head"):
|
if self.language_model is None or not hasattr(
|
||||||
|
self.language_model, "set_embed_and_head"
|
||||||
|
):
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
"language_model does not support set_embed_and_head()."
|
"language_model does not support set_embed_and_head()."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Dict, List, Tuple, Union
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from sglang.srt.managers.schedule_batch import (
|
from sglang.srt.managers.schedule_batch import (
|
||||||
|
Modality,
|
||||||
MultimodalDataItem,
|
MultimodalDataItem,
|
||||||
MultimodalProcessorOutput,
|
MultimodalProcessorOutput,
|
||||||
)
|
)
|
||||||
@@ -55,6 +56,70 @@ class KimiK2_5VLImageProcessor(SGLangBaseProcessor):
|
|||||||
im_token_id=self.mm_tokens.image_token_id,
|
im_token_id=self.mm_tokens.image_token_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _num_image_tokens_from_grid(self, grid_thw: torch.Tensor) -> int:
|
||||||
|
# Kimi-K2.5 applies temporal pooling and spatial 2D merge in vision tower.
|
||||||
|
# The output sequence length per image is h*w/(merge_h*merge_w).
|
||||||
|
merge_h, merge_w = self.hf_config.vision_config.merge_kernel_size
|
||||||
|
_t, h, w = grid_thw.tolist()
|
||||||
|
return (h * w) // (merge_h * merge_w)
|
||||||
|
|
||||||
|
def get_mm_data(self, prompt, embeddings, **kwargs):
|
||||||
|
img_grid_thw = kwargs.get("img_grid_thw", None)
|
||||||
|
|
||||||
|
if not isinstance(prompt, list):
|
||||||
|
prompt = self._tokenizer.encode(prompt)
|
||||||
|
|
||||||
|
image_token_id = self.mm_tokens.image_token_id
|
||||||
|
image_token_counts = [
|
||||||
|
self._num_image_tokens_from_grid(grid) for grid in img_grid_thw
|
||||||
|
]
|
||||||
|
|
||||||
|
input_ids = []
|
||||||
|
offsets = []
|
||||||
|
img_idx = 0
|
||||||
|
|
||||||
|
for token in prompt:
|
||||||
|
if token != image_token_id:
|
||||||
|
input_ids.append(token)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if img_idx >= len(image_token_counts):
|
||||||
|
raise ValueError(
|
||||||
|
"The number of image placeholders exceeds img_grid_thw entries."
|
||||||
|
)
|
||||||
|
|
||||||
|
num_tokens = image_token_counts[img_idx]
|
||||||
|
start = len(input_ids)
|
||||||
|
input_ids.extend([image_token_id] * num_tokens)
|
||||||
|
offsets.append((start, len(input_ids) - 1))
|
||||||
|
img_idx += 1
|
||||||
|
|
||||||
|
if img_idx != len(image_token_counts):
|
||||||
|
raise ValueError(
|
||||||
|
"The number of image placeholders does not match img_grid_thw entries."
|
||||||
|
)
|
||||||
|
|
||||||
|
image_embeddings = embeddings[Modality.IMAGE]
|
||||||
|
mm_items = []
|
||||||
|
consumed = 0
|
||||||
|
for start, end in offsets:
|
||||||
|
num_tokens = end - start + 1
|
||||||
|
embedding_slice = image_embeddings[consumed : consumed + num_tokens]
|
||||||
|
consumed += num_tokens
|
||||||
|
mm_items.append(
|
||||||
|
MultimodalDataItem(
|
||||||
|
modality=Modality.IMAGE,
|
||||||
|
offsets=[(start, end)],
|
||||||
|
precomputed_embeddings=embedding_slice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return MultimodalProcessorOutput(
|
||||||
|
input_ids=input_ids,
|
||||||
|
mm_items=mm_items,
|
||||||
|
im_token_id=image_token_id,
|
||||||
|
)
|
||||||
|
|
||||||
def _process_and_collect_mm_items(
|
def _process_and_collect_mm_items(
|
||||||
self, input_text: str, images=None, audios=None, videos=None, **kwargs
|
self, input_text: str, images=None, audios=None, videos=None, **kwargs
|
||||||
) -> Tuple[List[MultimodalDataItem], torch.Tensor, dict]:
|
) -> Tuple[List[MultimodalDataItem], torch.Tensor, dict]:
|
||||||
|
|||||||
@@ -3507,6 +3507,8 @@ class ServerArgs:
|
|||||||
"Qwen3OmniMoeForConditionalGeneration",
|
"Qwen3OmniMoeForConditionalGeneration",
|
||||||
"Qwen2AudioForConditionalGeneration",
|
"Qwen2AudioForConditionalGeneration",
|
||||||
"Qwen2_5OmniForConditionalGeneration",
|
"Qwen2_5OmniForConditionalGeneration",
|
||||||
|
"KimiVLForConditionalGeneration",
|
||||||
|
"KimiK25ForConditionalGeneration",
|
||||||
]:
|
]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Model type {model_arch} is not supported for encoder disaggregation, only Qwen models are supported for now."
|
f"Model type {model_arch} is not supported for encoder disaggregation, only Qwen models are supported for now."
|
||||||
|
|||||||
Reference in New Issue
Block a user