Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions tests/multimodal/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,38 @@ def test_mm_processor_kwargs_merge_then_overlay_preserves_scoping():
assert "size" not in overlay_modality_mm_kwargs(merged, None)


@pytest.mark.skip_global_cleanup
def test_mm_processor_kwargs_deep_merges_nested_scoped_kwargs():
"""Request-level nested kwargs must not drop configured sibling keys.

A shallow ``dict`` union replaced the whole ``videos_kwargs`` mapping, so a
configured ``fps`` (or any key not repeated in the request) was silently
dropped. Nested mappings should merge key-by-key instead.
"""
from vllm.config.multimodal import MultiModalConfig

mm_config = MultiModalConfig(
mm_processor_kwargs={
"size": {"shortest_edge": 100, "longest_edge": 1000},
"videos_kwargs": {"fps": 2, "size": {"longest_edge": 1200}},
}
)
merged = mm_config.merge_mm_processor_kwargs(
{
"size": {"longest_edge": 1800},
"videos_kwargs": {"size": {"shortest_edge": 200}},
}
)
# Configured fps survives the request merge.
assert merged["videos_kwargs"]["fps"] == 2
# Nested size dicts are merged key-by-key.
assert merged["videos_kwargs"]["size"] == {
"longest_edge": 1200,
"shortest_edge": 200,
}
assert merged["size"] == {"shortest_edge": 100, "longest_edge": 1800}


def test_processor_inputs_hashes_partial_uuids():
rng = np.random.RandomState(0)
images = [random_image(rng, min_wh=8, max_wh=9) for _ in range(2)]
Expand Down
24 changes: 23 additions & 1 deletion vllm/config/multimodal.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ def __get_pydantic_core_schema__(
`"xpu"` on XPU). Validated against that set by the CLI."""



def _deep_merge_mm_kwargs(
base: Mapping[str, object], override: Mapping[str, object]
) -> dict[str, object]:
"""Deep-merge inference ``mm_processor_kwargs`` over configured ones.

The previous implementation used a shallow ``dict`` union, so supplying a
nested mapping (e.g. ``videos_kwargs``) in the request wholesale replaced the
configured one and dropped every key not repeated in the request (e.g. a
configured ``fps``). Nested mappings are now merged key-by-key, while
non-mapping values are overridden as before.
"""
result: dict[str, object] = dict(base)
for key, value in override.items():
existing = result.get(key)
if isinstance(existing, Mapping) and isinstance(value, Mapping):
result[key] = _deep_merge_mm_kwargs(existing, value)
else:
result[key] = value
return result


@config
class MultiModalConfig:
"""Controls the behavior of multimodal models."""
Expand Down Expand Up @@ -538,7 +560,7 @@ def merge_mm_processor_kwargs(
if self.mm_device_do_normalize:
kwargs["do_normalize"] = False
kwargs["do_rescale"] = False
return kwargs | dict(inference_kwargs)
return _deep_merge_mm_kwargs(kwargs, inference_kwargs)

def use_gpu_video_backend(self) -> bool:
"""Return whether the configured video loader or codec uses the GPU."""
Expand Down
Loading