Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

724 lines
31 KiB
Python

# Copyright (c) ModelScope Contributors. All rights reserved.
import math
import numpy as np
import torch
import transformers
from dataclasses import dataclass, field
from functools import partial
from packaging import version
from torch import nn
from typing import Any, Dict, List, Literal, Optional
from swift.utils import get_env_args, get_packed_seq_params
from ..base import Template
from ..constant import LLMTemplateType, MLLMTemplateType
from ..register import TemplateMeta, register_template
from ..template_inputs import StdTemplateInputs
from ..utils import Context, Prompt, findall
from ..vision_utils import load_audio, load_video_minicpmv_mplug_owl3
from .llama import Llama3TemplateMeta
from .qwen import Qwen2_5TemplateMeta, Qwen3MixedTemplateMeta, QwenTemplateMeta
from .utils import ChatmlTemplateMeta
@dataclass
class MinicpmTemplateMeta(TemplateMeta):
prefix: Prompt = field(default_factory=lambda: ['<s>{{SYSTEM}}'])
prompt: Prompt = field(default_factory=lambda: ['<用户>{{QUERY}}<AI>'])
chat_sep: Optional[Prompt] = field(default_factory=list)
suffix: Prompt = field(default_factory=lambda: ['</s>'])
register_template(MinicpmTemplateMeta(LLMTemplateType.minicpm))
def _remove_idx(arr: List[int], idx_list: List[int]) -> List[int]:
res = []
idx_set = set(idx_list)
for i, x in enumerate(arr):
if i not in idx_set:
res.append(x)
return res
class MiniCPMVTemplate(Template):
is_v2_5 = False
use_model = True
skip_prompt = False
placeholder_tokens = ['<unk>']
def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
inputs: StdTemplateInputs) -> List[Context]:
if self.mode == 'vllm':
return ['(<image>./</image>)\n']
else:
return [[-100]]
async def prepare_lmdeploy_turbomind_inputs(self, inputs: Dict[str, Any]) -> None:
images = inputs.pop('images', None) or []
if len(images) == 0:
return
input_ids = inputs['input_ids']
idx_list = findall(input_ids, -100)
idx_list.insert(0, -1)
new_input_ids = []
features = []
for i in range(len(idx_list) - 1):
new_input_ids += input_ids[idx_list[i] + 1:idx_list[i + 1]]
context_list = ['<image>', [-100], '</image>']
feat = [x.squeeze() for x in images[i]['embeddings'].split(1)]
grid = images[i].get('grid')
if len(feat) > 1 and grid is not None:
context_list.append('<slice>')
for j in range(grid[1]):
if j > 0:
context_list.append('\n')
for _ in range(grid[0]):
context_list += ['<image>', [-100], '</image>']
context_list.append('</slice>\n')
new_input_ids += self._encode_context_list(context_list)[0]
features += feat
new_input_ids += input_ids[idx_list[-1] + 1:]
inputs['input_ids'] = new_input_ids
inputs['images'] = features
await super().prepare_lmdeploy_turbomind_inputs(inputs)
def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
encoded = super()._encode(inputs)
images = inputs.images
input_ids = encoded['input_ids']
labels = encoded['labels']
idx_list = findall(input_ids, -100)
idx = idx_list[0]
tgt_sizes = None
slice_mode = getattr(self.config, 'slice_mode', False)
if slice_mode:
if self.is_v2_5:
image_processor = self.processor.image_processor
image_inputs = image_processor(images, return_tensors='pt').to(self.model_info.torch_dtype)
placeholder = image_processor.get_slice_image_placeholder(image_inputs.image_sizes[0][0])
pixel_values = image_inputs['pixel_values']
tgt_sizes = image_inputs['tgt_sizes']
else:
images, placeholder = self.model.get_slice_image_placeholder(images[0], self.processor)
pixel_values = [[self.model.transform(img) for img in images]]
placeholder += '\n'
placeholder_id = self.processor.encode(placeholder, add_special_tokens=False)
input_ids = (input_ids[:idx] + placeholder_id + input_ids[idx + 1:])
if labels is not None:
labels = (labels[:idx] + [-100] * len(placeholder_id) + labels[idx + 1:])
input_tensor_ids = torch.tensor(input_ids)
image_start_idx = torch.where(input_tensor_ids == self.processor.im_start_id)[0]
image_start_idx += 1
image_end_idx = torch.where(input_tensor_ids == self.processor.im_end_id)[0]
valid_image_nums = max(len(image_start_idx), len(image_end_idx))
image_bound = [
torch.hstack(
[image_start_idx[:valid_image_nums].unsqueeze(-1), image_end_idx[:valid_image_nums].unsqueeze(-1)])
]
else:
placeholder = '<image>' + '<unk>' * self.config.query_num + '</image>\n'
placeholder_id = self.processor.encode(placeholder, add_special_tokens=False)
input_ids = (input_ids[:idx] + placeholder_id + input_ids[idx + 1:])
if labels is not None:
labels = (labels[:idx] + [-100] * len(placeholder_id) + labels[idx + 1:])
image_bound = [torch.tensor([[idx, idx + self.config.query_num]])]
pixel_values = [[self.model.transform(images[0])]]
encoded = {
'input_ids': input_ids,
'labels': labels,
'image_bound': image_bound,
'pixel_values': pixel_values,
'tgt_sizes': tgt_sizes
}
return encoded
def _post_encode(self, model: nn.Module, inputs: Dict[str, Any]) -> Dict[str, Any]:
inputs_embeds, _ = model.get_vllm_embedding(inputs)
return {'inputs_embeds': inputs_embeds}
def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
res = {}
for k in ['pixel_values', 'image_bound', 'tgt_sizes']:
res[k] = self.gather_list(batch, k)
res.update(super()._data_collator(batch, padding_to=padding_to))
return res
register_template(MinicpmTemplateMeta(MLLMTemplateType.minicpmv, template_cls=MiniCPMVTemplate))
class MiniCPMV2_5Template(MiniCPMVTemplate):
is_v2_5 = True
register_template(Llama3TemplateMeta(
MLLMTemplateType.minicpmv2_5,
template_cls=MiniCPMV2_5Template,
))
class MiniCPMV2_6Template(MiniCPMVTemplate):
def init_env_args(self):
super().init_env_args()
self.max_num_frames = get_env_args('max_num_frames', int, 64)
self.max_slice_nums = get_env_args('max_slice_nums', int, None)
self.video_max_slice_nums = get_env_args('video_max_slice_nums', int, 1) # or 2
def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index,
inputs: StdTemplateInputs) -> List[Context]:
assert media_type in {'image', 'video'}
load_video = partial(load_video_minicpmv_mplug_owl3, max_num_frames=self.max_num_frames)
image_context = super().replace_tag('image', index, inputs)
if media_type == 'image':
return image_context
elif media_type == 'video':
return self.replace_video2image(load_video, inputs, lambda i: image_context)
def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
encoded = Template._encode(self, inputs)
images = inputs.images
use_video = bool(inputs.videos)
use_image_id = True
max_slice_nums = self.max_slice_nums
if use_video:
max_slice_nums = self.video_max_slice_nums
use_image_id = False
input_ids = encoded['input_ids']
labels = encoded['labels']
loss_scale = encoded.get('loss_scale', None)
idx_list = findall(input_ids, -100)
image_processor = self.processor.image_processor
image_inputs = image_processor([images], return_tensors='pt',
max_slice_nums=max_slice_nums).to(self.model_info.torch_dtype)
def _get_new_tokens(i):
placeholder = image_processor.get_slice_image_placeholder(
image_inputs.image_sizes[0][i], image_idx=i, max_slice_nums=max_slice_nums, use_image_id=use_image_id)
placeholder += '\n'
return self.processor.encode(placeholder, add_special_tokens=False)
input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_tokens)
if inputs.images:
input_tensor_ids = torch.tensor(input_ids)
unk_token = self.processor.encode('<unk>', add_special_tokens=False)[0]
indices = (input_tensor_ids == unk_token).nonzero(as_tuple=True)[0].tolist()
ranges = []
start = indices[0]
for i in range(1, len(indices)):
if indices[i] != indices[i - 1] + 1:
ranges.append([start, indices[i - 1] + 1])
start = indices[i]
ranges.append([start, indices[-1] + 1])
image_bound = [torch.tensor(ranges)]
else:
image_bound = [[]]
encoded = {
'input_ids': input_ids,
'labels': labels,
'loss_scale': loss_scale,
'image_bound': image_bound,
'pixel_values': image_inputs['pixel_values'],
'tgt_sizes': image_inputs['tgt_sizes']
}
return encoded
register_template(QwenTemplateMeta(
MLLMTemplateType.minicpmv2_6,
template_cls=MiniCPMV2_6Template,
))
register_template(ChatmlTemplateMeta(
MLLMTemplateType.minicpmv4,
template_cls=MiniCPMV2_6Template,
))
register_template(Qwen2_5TemplateMeta(
MLLMTemplateType.minicpmo,
template_cls=MiniCPMV2_6Template,
))
class MiniCPMV4_5Template(MiniCPMV2_6Template):
def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
encoded = Template._encode(self, inputs)
images = inputs.images
use_video = bool(inputs.videos)
use_image_id = True
max_slice_nums = self.max_slice_nums
if use_video:
max_slice_nums = self.video_max_slice_nums
use_image_id = False
input_ids = encoded['input_ids']
labels = encoded['labels']
loss_scale = encoded.get('loss_scale', None)
idx_list = findall(input_ids, -100)
image_processor = self.processor.image_processor
image_inputs = image_processor([images], return_tensors='pt',
max_slice_nums=max_slice_nums).to(self.model_info.torch_dtype)
def _get_new_tokens(i):
placeholder = image_processor.get_slice_image_placeholder(
image_inputs.image_sizes[0][i], image_idx=i, max_slice_nums=max_slice_nums, use_image_id=use_image_id)
placeholder += '\n'
return self.processor.encode(placeholder, add_special_tokens=False)
input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_tokens)
if inputs.images:
input_tensor_ids = torch.tensor(input_ids)
unk_token = self.processor.encode('<unk>', add_special_tokens=False)[0]
indices = (input_tensor_ids == unk_token).nonzero(as_tuple=True)[0].tolist()
ranges = []
start = indices[0]
for i in range(1, len(indices)):
if indices[i] != indices[i - 1] + 1:
ranges.append([start, indices[i - 1] + 1])
start = indices[i]
ranges.append([start, indices[-1] + 1])
image_bound = [torch.tensor(ranges)]
else:
image_bound = [[]]
encoded = {
'input_ids': input_ids,
'labels': labels,
'loss_scale': loss_scale,
'image_bound': image_bound,
'pixel_values': image_inputs['pixel_values'],
'tgt_sizes': image_inputs['tgt_sizes'],
'temporal_ids': image_inputs['temporal_ids'],
}
return encoded
def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
res = {}
for k in ['pixel_values', 'image_bound', 'tgt_sizes', 'temporal_ids']:
res[k] = self.gather_list(batch, k)
res.update(Template._data_collator(self, batch, padding_to=padding_to))
return res
register_template(
Qwen3MixedTemplateMeta(
MLLMTemplateType.minicpmv4_5,
template_cls=MiniCPMV4_5Template,
is_thinking=True,
thinking_prefix='<think>\n',
))
class MiniCPMO4_5Template(MiniCPMV4_5Template):
SAMPLING_RATE = 16000
MAX_AUDIO_DURATION = 30 # seconds
def init_env_args(self):
super().init_env_args()
self.use_audio_in_video = get_env_args('use_audio_in_video', bool, False)
def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
inputs: StdTemplateInputs) -> List[Context]:
if media_type == 'image' or media_type == 'video' and not self.use_audio_in_video:
return super().replace_tag(media_type, index, inputs)
elif media_type == 'audio':
# Load audio from file path to numpy array at 16kHz
if isinstance(inputs.audios[index], str):
inputs.audios[index] = load_audio(inputs.audios[index], sampling_rate=self.SAMPLING_RATE)
return ['<|audio_start|><|audio_end|>']
elif media_type == 'video':
from minicpmo.utils import get_video_frame_audio_segments
video = inputs.videos[inputs.video_idx]
video_segments, audio_segments, _ = get_video_frame_audio_segments(
video, use_audio=self.use_audio_in_video, stack_frames=1)
# Insert frames into images list at current position
images = inputs.images
inputs.images = images[:inputs.image_idx] + video_segments + images[inputs.image_idx:]
# Build context list
image_context = [[-100]]
context_list = []
if self.use_audio_in_video and audio_segments:
# Insert audio segments into audios list at current position
audios = inputs.audios
inputs.audios = audios[:inputs.audio_idx] + audio_segments + audios[inputs.audio_idx:]
audio_context = ['<|audio_start|><|audio_end|>']
# Interleave: one image placeholder + one audio placeholder per second
for i in range(len(video_segments)):
context_list += image_context
if i < len(audio_segments):
context_list += audio_context
inputs.audio_idx += len(audio_segments)
else:
for _ in range(len(video_segments)):
context_list += image_context
inputs.image_idx += len(video_segments)
return context_list
def _get_audio_num_tokens(self, audio_sample_len: int) -> int:
"""Compute the number of <unk> placeholder tokens for an audio of given sample count.
This mirrors the official get_audio_placeholder logic:
1. mel frames = ceil(audio_samples / hop_length)
2. after CNN downsampling: (mel_frames - 1) // 2 + 1
3. after avg pooling: (cnn_frames - pool_step) // pool_step + 1
"""
hop_length = self.processor.audio_processor.hop_length # 160
pool_step = self.config.audio_pool_step # 5
feature_lens = math.ceil(audio_sample_len / hop_length)
feature_lens_after_cnn = (feature_lens - 1) // 2 + 1
output_lens = (feature_lens_after_cnn - pool_step) // pool_step + 1
return output_lens
def _extract_audio_features(self, audios: List[np.ndarray]):
"""Extract mel features from audio arrays using the WhisperFeatureExtractor.
Handles chunking of long audios (>30s) into segments.
Matches the official audio_feature_extract output format.
Returns:
audio_features: tensor [N, 80, max_frames] or [] if no audios
audio_feature_lens: [tensor([l1, l2, ...])] or None
"""
audio_processor = self.processor.audio_processor
max_audio_inp_len = self.MAX_AUDIO_DURATION * self.SAMPLING_RATE
all_audio_features = []
all_audio_lens = []
for audio in audios:
# Chunk long audios at 30s boundaries
if len(audio) <= max_audio_inp_len:
chunks = [audio]
else:
chunks = [audio[i:i + max_audio_inp_len] for i in range(0, len(audio), max_audio_inp_len)]
for chunk in chunks:
audio_input = audio_processor(
chunk,
sampling_rate=self.SAMPLING_RATE,
return_tensors='pt',
padding='max_length',
return_attention_mask=True,
)
feat = audio_input['input_features'] # [1, 80, frames]
actual_len = audio_input['attention_mask'].sum(dim=1) # [1]
feat = feat[:, :, :actual_len[0]]
all_audio_features.append(feat.squeeze(0)) # [80, actual_frames]
all_audio_lens.append(actual_len[0])
if all_audio_features:
# Pad and stack: [N, 80, max_frames] — same as official processor
audio_features = torch.nn.utils.rnn.pad_sequence(
[f.transpose(0, 1) for f in all_audio_features],
batch_first=True,
padding_value=0.0,
).transpose(1, 2)
audio_feature_lens = [torch.hstack(all_audio_lens)]
else:
audio_features = []
audio_feature_lens = None
return audio_features, audio_feature_lens
def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
# Step 1: Base encode — produces input_ids with -100 for images
# and audio_start_id,audio_end_id pairs for audios
encoded = Template._encode(self, inputs)
images = inputs.images
use_video = bool(inputs.videos)
use_image_id = True
max_slice_nums = self.max_slice_nums
if use_video:
max_slice_nums = self.video_max_slice_nums
use_image_id = False
input_ids = encoded['input_ids']
labels = encoded['labels']
loss_scale = encoded.get('loss_scale', None)
# Step 2: Process images — replace -100 tokens with image placeholders
idx_list = findall(input_ids, -100)
image_processor = self.processor.image_processor
image_inputs = image_processor([images], return_tensors='pt',
max_slice_nums=max_slice_nums).to(self.model_info.torch_dtype)
def _get_new_tokens(i):
placeholder = image_processor.get_slice_image_placeholder(
image_inputs.image_sizes[0][i], image_idx=i, max_slice_nums=max_slice_nums, use_image_id=use_image_id)
placeholder += '\n'
return self.processor.encode(placeholder, add_special_tokens=False)
input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_tokens)
# Step 3: Process audios — expand audio_start/audio_end pairs with <unk> tokens
tokenizer = self.processor.tokenizer
audio_start_id = tokenizer.convert_tokens_to_ids('<|audio_start|>')
audio_end_id = tokenizer.convert_tokens_to_ids('<|audio_end|>')
unk_token_id = tokenizer.convert_tokens_to_ids('<unk>')
audio_features = None
audio_feature_lens = None
if inputs.audios:
audios = inputs.audios
audio_features, audio_feature_lens = self._extract_audio_features(audios)
# Find empty audio placeholder pairs (audio_start_id immediately followed by audio_end_id)
audio_placeholder_positions = []
for i in range(len(input_ids) - 1):
if input_ids[i] == audio_start_id and input_ids[i + 1] == audio_end_id:
audio_placeholder_positions.append(i)
assert len(audio_placeholder_positions) == len(audios), \
f'Found {len(audio_placeholder_positions)} audio placeholders but have {len(audios)} audios'
# Expand each audio placeholder with <unk> tokens
offset = 0
for i, audio in enumerate(audios):
num_tokens = self._get_audio_num_tokens(len(audio))
unk_tokens = [unk_token_id] * num_tokens
pos = audio_placeholder_positions[i] + offset
# Current: [..., audio_start_id, audio_end_id, ...]
# Target: [..., audio_start_id, unk*N, audio_end_id, ...]
input_ids = input_ids[:pos + 1] + unk_tokens + input_ids[pos + 1:]
if labels is not None:
labels = labels[:pos + 1] + [-100] * num_tokens + labels[pos + 1:]
if loss_scale is not None:
scale_val = loss_scale[pos]
loss_scale = loss_scale[:pos + 1] + [scale_val] * num_tokens + loss_scale[pos + 1:]
offset += num_tokens
# Step 4: Compute image_bound using start/end token boundaries
# This is more robust than finding consecutive <unk> tokens, especially
# when both image and audio use <unk> as placeholder.
input_tensor_ids = torch.tensor(input_ids)
if images:
im_start_id = tokenizer.convert_tokens_to_ids('<image>')
im_end_id = tokenizer.convert_tokens_to_ids('</image>')
slice_start_id = tokenizer.convert_tokens_to_ids('<slice>')
slice_end_id = tokenizer.convert_tokens_to_ids('</slice>')
start_cond = (input_tensor_ids == im_start_id) | (input_tensor_ids == slice_start_id)
end_cond = (input_tensor_ids == im_end_id) | (input_tensor_ids == slice_end_id)
image_start_idx = torch.where(start_cond)[0] + 1
image_end_idx = torch.where(end_cond)[0]
valid_image_nums = min(len(image_start_idx), len(image_end_idx))
image_bound = [
torch.hstack([
image_start_idx[:valid_image_nums].unsqueeze(-1),
image_end_idx[:valid_image_nums].unsqueeze(-1),
])
]
else:
image_bound = [[]]
# Step 5: Compute audio_bounds
if inputs.audios:
audio_start_idx = torch.where(input_tensor_ids == audio_start_id)[0]
audio_end_idx = torch.where(input_tensor_ids == audio_end_id)[0]
assert len(audio_start_idx) == len(audio_end_idx)
audio_bounds = [torch.hstack([
(audio_start_idx + 1).unsqueeze(-1),
audio_end_idx.unsqueeze(-1),
])]
else:
audio_bounds = [[]]
encoded = {
'input_ids': input_ids,
'labels': labels,
'loss_scale': loss_scale,
'image_bound': image_bound,
'pixel_values': image_inputs['pixel_values'],
'tgt_sizes': image_inputs['tgt_sizes'],
'audio_features': audio_features,
'audio_feature_lens': audio_feature_lens,
'audio_bounds': audio_bounds,
}
return encoded
def _post_encode(self, model: nn.Module, inputs: Dict[str, Any]) -> Dict[str, Any]:
# Vision embeddings
inputs_embeds, _ = model.get_vllm_embedding(inputs)
# Audio embeddings — scatter audio features into the embedding
inputs_embeds = model.get_omni_embedding(
inputs,
input_embeddings=inputs_embeds,
chunk_length=getattr(self.config, 'audio_chunk_length', 1.0),
)
return {'inputs_embeds': inputs_embeds}
def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
res = {}
# Vision data
for k in ['pixel_values', 'image_bound', 'tgt_sizes']:
res[k] = self.gather_list(batch, k)
# Audio data — collect from all samples
all_audio_feats = [] # list of [N_i, 80, frames_i] tensors
audio_feature_lens_list = []
audio_bounds_list = []
for b in batch:
af = b.pop('audio_features', None)
afl = b.pop('audio_feature_lens', None)
ab = b.pop('audio_bounds', None)
if af is not None and isinstance(af, torch.Tensor):
all_audio_feats.append(af)
if afl is not None:
audio_feature_lens_list.extend(afl)
if ab is not None:
audio_bounds_list.extend(ab)
# Re-pad audio features across the batch to the same max frame length
if all_audio_feats:
# Unpack per-sample tensors into individual segments, then re-pad
segments = []
for af in all_audio_feats:
for i in range(af.shape[0]):
segments.append(af[i]) # [80, frames_i]
res['audio_features'] = torch.nn.utils.rnn.pad_sequence(
[s.transpose(0, 1) for s in segments],
batch_first=True,
padding_value=0.0,
).transpose(1, 2) # [total_segments, 80, max_frames]
else:
res['audio_features'] = []
res['audio_feature_lens'] = audio_feature_lens_list if audio_feature_lens_list else []
res['audio_bounds'] = audio_bounds_list if audio_bounds_list else []
res.update(Template._data_collator(self, batch, padding_to=padding_to))
return res
register_template(
Qwen3MixedTemplateMeta(
MLLMTemplateType.minicpmo4_5,
template_cls=MiniCPMO4_5Template,
is_thinking=True,
))
class MiniCPMV4_6Template(Template):
support_padding_free = True
placeholder_tokens = ['<|image_pad|>', '<|video_pad|>']
def init_env_args(self):
super().init_env_args()
self.downsample_mode = get_env_args('downsample_mode', str, '16x')
self.max_slice_nums = get_env_args('max_slice_nums', int, 9)
self.video_max_slice_nums = get_env_args('video_max_slice_nums', int, 1)
self.max_num_frames = get_env_args('max_num_frames', int, 128)
self.stack_frames = get_env_args('stack_frames', int, 1)
self.transformers_version = version.parse(transformers.__version__)
self.transformers_5_9 = self.transformers_version >= version.parse('5.9.0')
def _preprocess_inputs(self, inputs: StdTemplateInputs) -> None:
super()._preprocess_inputs(inputs)
# Inject downsample_mode into mm_processor_kwargs so that vLLM rollout
# receives the correct mode via _encode_truncated -> _add_request.
inputs.mm_processor_kwargs['downsample_mode'] = self.downsample_mode
def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
inputs: StdTemplateInputs) -> List[Context]:
if media_type == 'image':
return ['<|image_pad|>\n']
else:
return ['<|video_pad|>\n']
def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
encoded = super()._encode(inputs)
split_token = self._tokenize(self.tokenizer.eos_token)
input_ids = encoded['input_ids']
labels = encoded['labels']
loss_scale = encoded.get('loss_scale', None)
for media_type in ['image', 'video']:
mm_data = getattr(inputs, f'{media_type}s')
media_token = f'<|{media_type}_pad|>'
media_token_id = self._tokenize(media_token)[0]
max_slice_nums = self.max_slice_nums if media_type == 'image' else self.video_max_slice_nums
if mm_data:
media_inputs = self.processor(
text=self.tokenizer.eos_token.join([media_token] * len(mm_data)),
images=inputs.images or None,
videos=inputs.videos or None,
return_tensors='pt',
add_special_tokens=False,
downsample_mode=self.downsample_mode,
stack_frames=self.stack_frames,
max_num_frames=self.max_num_frames,
max_slice_nums=max_slice_nums,
)
splited_tokens = self._split_list(media_inputs['input_ids'][0].tolist(), split_token)
idx_list = findall(input_ids, media_token_id)
def _get_new_tokens(i):
return splited_tokens[i]
input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list,
_get_new_tokens)
encoded.update(media_inputs)
encoded['input_ids'] = input_ids
encoded['labels'] = labels
encoded['loss_scale'] = loss_scale
return encoded
def _data_collator_mm_data(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]:
res = {}
pixel_values = [b['pixel_values'] for b in batch if b.get('pixel_values') is not None]
if len(pixel_values) > 0:
res['pixel_values'] = torch.concat(pixel_values, dim=-1)
pixel_values_videos = [b['pixel_values_videos'] for b in batch if b.get('pixel_values_videos') is not None]
if len(pixel_values_videos) > 0:
res['pixel_values_videos'] = torch.concat(pixel_values_videos, dim=-1)
for key in ['target_sizes', 'target_sizes_videos']:
value = self.concat_tensor(batch, key, dim=0)
if value is not None:
res[key] = value
# Inject downsample_mode so the model forward uses the same mode
# as data preprocessing, keeping image token/feature counts aligned.
res['downsample_mode'] = self.downsample_mode
return res
def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
res = super()._data_collator(batch, padding_to=padding_to)
if self.padding_free:
res.update(get_packed_seq_params(res['position_ids']))
return res
def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]:
if self.padding_free and self.sequence_parallel_size <= 1 and not self.transformers_5_9:
raise RuntimeError('MiniCPM-V 4.6 packing/padding_free with sequence_parallel_size=1 requires '
f'transformers>=5.9.0 (current: {self.transformers_version}). ')
return super()._post_encode(model, inputs)
register_template(
ChatmlTemplateMeta(
MLLMTemplateType.minicpmv4_6,
template_cls=MiniCPMV4_6Template,
is_thinking=True,
thinking_prefix='<think>\n',
non_thinking_prefix='<think>\n\n</think>\n\n',
))
register_template(
ChatmlTemplateMeta(
LLMTemplateType.minicpm5,
is_thinking=True,
thinking_prefix='<think>\n',
non_thinking_prefix='<think>\n\n</think>\n\n',
agent_template='minicpm5',
))