Skip to content
Merged
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
25 changes: 9 additions & 16 deletions examples/disaggregated/disaggregated_encoder/disagg_epd_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import argparse
import asyncio
import hashlib
import io
import itertools
import json
import logging
Expand All @@ -34,7 +33,6 @@
from typing import Any

import aiohttp
import pybase64 as base64
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
Expand Down Expand Up @@ -125,19 +123,6 @@ def content_uuid(item: dict) -> str:
return hashlib.sha256(payload.encode()).hexdigest()


def _b64_tensor(values: list) -> str:
import torch

buf = io.BytesIO()
flat = [v for item in values for v in (item if isinstance(item, list) else [item])]
# Floats stay float64 so timestamp strings format exactly as the
# encoder computed them.
dtype = torch.float64 if any(isinstance(v, float) for v in flat) else None
# Downstream stacks per item, so hand over a flat vector.
torch.save(torch.tensor(flat, dtype=dtype), buf)
return base64.b64encode(buf.getvalue()).decode()


def rewrite_for_decode(req_data: dict, item_meta: dict[int, dict]) -> dict:
"""Replace each media item with a metadata-only reference for the decoder.

Expand Down Expand Up @@ -172,7 +157,15 @@ def rewrite_for_decode(req_data: dict, item_meta: dict[int, dict]) -> dict:
# Whatever keys the encoder reported are the metadata its model
# declared as needed to size the placeholder range; the proxy does
# not need to know their names.
metadata = {k: _b64_tensor(v) for k, v in meta.items()}
# Downstream stacks per item; keep the existing per-item vector shape.
metadata = {
k: [
x
for item in v
for x in (item if isinstance(item, list) else [item])
]
for k, v in meta.items()
}
if not metadata or not item_uuid:
# Nothing to size the placeholder range with. A processor cache
# hit is not a cause on its own: with the default `lru` type the
Expand Down
47 changes: 46 additions & 1 deletion tests/entrypoints/unit_tests/test_chat_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
MEDIA_CONNECTOR_REGISTRY,
AsyncMultiModalItemTracker,
ConversationMessage,
_load_embeds_dict,
_parse_metadata_array,
_postprocess_messages,
parse_chat_messages,
parse_chat_messages_async,
Expand All @@ -39,6 +41,45 @@
MISTRAL_MODEL_ID = "mistralai/Mistral-Small-3.1-24B-Instruct-2503"


@pytest.mark.parametrize("values", [[1, 32, 48], [0.0, 0.12345678912345678]])
def test_json_metadata_preserves_numeric_precision(values):
tensor = _parse_metadata_array("grid", values, {"grid"})
assert tensor.tolist() == values
assert tensor.dtype == (
torch.float64 if isinstance(values[0], float) else torch.int64
)


@pytest.mark.parametrize(
"key,values",
[
("image_embeds", [1, 2]),
("grid", [True]),
("grid", [float("nan")]),
("grid", [float("inf")]),
("grid", [[1, 2]]),
("grid", [2**64]),
],
)
def test_json_arrays_are_only_valid_numeric_metadata(key, values):
with pytest.raises(VLLMValidationError):
_parse_metadata_array(key, values, {"grid"})


@pytest.mark.asyncio
async def test_json_metadata_bypasses_tensor_deserialization():
from unittest.mock import AsyncMock

tensor = torch.ones(2, 3)
fetch = AsyncMock(return_value=tensor)
result = await _load_embeds_dict(
{"image_embeds": "legacy-base64", "image_grid_thw": [1, 2, 2]}, fetch
)
fetch.assert_awaited_once_with("legacy-base64")
assert result["image_embeds"] is tensor
assert result["image_grid_thw"] == [1, 2, 2]


@pytest.fixture(scope="function")
def kimi_k2_5_model_config():
return ModelConfig(
Expand Down Expand Up @@ -1355,8 +1396,10 @@ def test_parse_chat_messages_empty_dict_image_embeds(
_assert_mm_uuids(mm_uuids, 1, expected_uuids=[None])


@pytest.mark.parametrize("json_metadata", [False, True])
def test_parse_chat_messages_multiple_dict_image_embeds(
qwen25omni_model_config_image_embeds,
json_metadata,
):
"""Test that multiple dictionaries for image_embeds is handled without errors."""
# Create two sample image embedding tensors
Expand All @@ -1374,7 +1417,9 @@ def test_parse_chat_messages_multiple_dict_image_embeds(
"type": "image_embeds",
"image_embeds": {
"image_embeds": tensor2base64(embeds),
"image_grid_thw": tensor2base64(grid_thw),
"image_grid_thw": grid_thw.tolist()
if json_metadata
else tensor2base64(grid_thw),
},
}
for embeds, grid_thw in zip(
Expand Down
26 changes: 25 additions & 1 deletion tests/utils_/test_collection_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,31 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest

from vllm.utils.collection_utils import common_prefix, swap_dict_values
from vllm.utils.collection_utils import (
common_prefix,
is_list_of_numbers,
swap_dict_values,
)


@pytest.mark.parametrize(
"value,expected",
[
([], True),
([1, -2, 2**1024], True),
([1, 0.5], True),
([1, True], False),
([1, float("nan")], False),
([1, float("inf")], False),
([1, -float("inf")], False),
([1, "2"], False),
([[1]], False),
((1, 2), False),
(None, False),
],
)
def test_is_list_of_numbers_checks_all_finite_non_boolean_items(value, expected):
assert is_list_of_numbers(value) is expected


@pytest.mark.parametrize(
Expand Down
3 changes: 3 additions & 0 deletions tests/v1/ec_connector/unit/test_epd_proxy_round_robin.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ def test_decode_rewrite_preserves_engine_reported_ec_hash(proxy):
)

assert rewritten["messages"][0]["content"][0]["uuid"] == "proxy-uuid"
assert rewritten["messages"][0]["content"][0]["image_embeds"] == {
"image_grid_thw": [1, 2, 3]
}
assert rewritten["ec_transfer_params"]["ec_items"] == [
{"mm_hash": "engine-derived-hash", "transfer_id": "transfer"}
]
Loading
Loading