[Bugfix][MiniCPM-o] Take the duplex vision tile from the image-processor config (#8462)

Signed-off-by: Tianyao Wu <rayroy31@gmail.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
Tianyao Wuandamy-why-3459 authored and GitHub committed 2026-10-06 09:04:21 +08:00
1 parent aad3090877
commit cc444b8875
2 files changed
+167 -50

No files matched your search

@@ -186,16 +186,21 @@ def duplex_first_append_unit_count(payload: object) -> int | None:
return max(1, sample_count // _DUPLEX_CHUNK_SAMPLES - 1)
def duplex_scheduler_token_budget(payload: object, *, default: int = 64, tile_pixels: int | None = None) -> int:
vision_tokens = _duplex_vision_tokens(payload, tile_pixels=tile_pixels)
def _duplex_audio_token_budget(payload: object, *, default: int = 64) -> int:
sample_count = _duplex_pcm_sample_count(payload)
if sample_count is None:
return max(1, int(default)) + vision_tokens
return max(1, int(default))
sample_count = max(1, sample_count)
if sample_count % _DUPLEX_CHUNK_SAMPLES == 0:
units = sample_count // _DUPLEX_CHUNK_SAMPLES
return units * (2 + _DUPLEX_CHUNK_SAMPLES // _DUPLEX_SAMPLES_PER_AUDIO_TOKEN) + vision_tokens
return max(16, min(768, sample_count // _DUPLEX_SAMPLES_PER_AUDIO_TOKEN + 8)) + vision_tokens
return units * (2 + _DUPLEX_CHUNK_SAMPLES // _DUPLEX_SAMPLES_PER_AUDIO_TOKEN)
return max(16, min(768, sample_count // _DUPLEX_SAMPLES_PER_AUDIO_TOKEN + 8))
def duplex_scheduler_token_budget(payload: object, *, default: int = 64, tile_pixels: int | None = None) -> int:
return _duplex_audio_token_budget(payload, default=default) + _duplex_vision_tokens(
payload, tile_pixels=tile_pixels
)
def duplex_first_append_context_reserve(runtime_config: object) -> int:
@@ -235,14 +240,14 @@ def build_duplex_data_plane_prompt(
payload: object,
final: bool,
) -> dict[str, object]:
tile_pixels = _duplex_vision_tile_pixels(runtime_config)
token_budget = duplex_scheduler_token_budget(payload, tile_pixels=tile_pixels)
# Computed once: with a known tile this decodes the stacked pair's first frame.
vision_tokens = _duplex_vision_tokens(payload, tile_pixels=_duplex_vision_tile_pixels(runtime_config))
token_budget = _duplex_audio_token_budget(payload) + vision_tokens
if seq <= 1:
context_reserve = duplex_first_append_context_reserve(runtime_config)
token_budget += context_reserve
first_units = duplex_first_append_unit_count(payload)
if first_units is not None:
vision_tokens = _duplex_vision_tokens(payload, tile_pixels=tile_pixels)
token_budget = context_reserve + first_units * 12 - 1 + vision_tokens
if seq > 1 and duplex_payload_is_exact_chunks(payload):
token_budget += 1
@@ -479,23 +484,31 @@ def _apply_first_append_context_tokens(
def _model_vision_tile_pixels(model_config: ModelConfig | None) -> int | None:
"""Area of one normalization tile, from the checkpoint that will do the slicing.
"""Area of one normalization tile, as the processor that does the slicing sees it.
``MiniCPMVImageProcessor`` is built with ``scale_resolution=config.image_size``
and Stage0 loads the checkpoint's own processor, so this is per-checkpoint
configuration. ``None`` when it cannot be read, which keeps the reservation
at the sliced count.
Stage0 slices with the checkpoint's own processor
(``MiniCPMO45Stage0DuplexRuntime._load_processor_from_path``), whose
``MiniCPMVImageProcessor`` takes ``scale_resolution`` from the checkpoint's
image-processor config: a nested ``image_processor`` entry in
``processor_config.json`` if there is one, else ``preprocessor_config.json``.
``get_image_processor_dict`` is that lookup. ``hf_config`` has its own
``slice_config.scale_resolution`` and ``image_size``; they agree on the
released checkpoint, but reading them here would under-reserve as soon as
one side changes alone (``--hf-overrides`` reaches only ``hf_config``).
Local files only, like ``_load_tokenizer``. ``None`` when it cannot be
read, which keeps the reservation at the sliced count.
"""
hf_config = getattr(model_config, "hf_config", None)
if hf_config is None:
model_path = getattr(model_config, "model", None)
if not isinstance(model_path, str) or not model_path:
return None
slice_config = getattr(hf_config, "slice_config", None)
side = getattr(slice_config, "scale_resolution", None)
if not isinstance(side, int):
side = slice_config.get("scale_resolution") if isinstance(slice_config, dict) else None
if not isinstance(side, int):
side = getattr(hf_config, "image_size", None)
if not isinstance(side, int) or side <= 0:
try:
from transformers.image_processing_base import ImageProcessingMixin
processor_dict, _ = ImageProcessingMixin.get_image_processor_dict(model_path, local_files_only=True)
side = processor_dict.get("scale_resolution")
except Exception:
return None
if not isinstance(side, int) or isinstance(side, bool) or side <= 0:
return None
return side * side