Files
vllm-omni/tests/entrypoints/openai_api/test_serving_chat_sampling_params.py
T
Jared WenandHongsheng Liu 6e35bfbbae Benchmark and Bugfix for GLM-Image (#3024)
Signed-off-by: JaredforReal <w13431838023@gmail.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-04-25 20:49:19 +08:00

709 lines
26 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Unit tests for OmniOpenAIServingChat sampling params handling.
Tests that standard OpenAI API parameters (max_tokens, temperature, etc.)
are correctly applied to the comprehension stage while preserving YAML defaults.
"""
import pytest
from pytest_mock import MockerFixture
from vllm.sampling_params import SamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.fixture
def mock_comprehension_stage(mocker: MockerFixture):
"""Create a mock comprehension stage with is_comprehension=True."""
stage = mocker.MagicMock()
stage.is_comprehension = True
stage.model_stage = "comprehension"
return stage
@pytest.fixture
def mock_other_stage(mocker: MockerFixture):
"""Create a mock non-comprehension stage."""
stage = mocker.MagicMock()
stage.is_comprehension = False
stage.model_stage = "other"
return stage
@pytest.fixture
def default_comprehension_params():
"""Default sampling params for comprehension stage (from YAML)."""
return SamplingParams(
temperature=0.4,
top_p=0.9,
top_k=1,
max_tokens=4353,
seed=42,
repetition_penalty=1.05,
)
@pytest.fixture
def default_other_params():
"""Default sampling params for non-comprehension stage (from YAML)."""
return SamplingParams(
temperature=0.9,
top_k=50,
max_tokens=4096,
seed=42,
)
@pytest.fixture
def mock_engine_client(
mock_comprehension_stage,
mock_other_stage,
default_comprehension_params,
default_other_params,
mocker: MockerFixture,
):
"""Create mock engine client with stage_configs and default_sampling_params_list."""
engine_client = mocker.MagicMock()
engine_client.stage_configs = [mock_comprehension_stage, mock_other_stage]
engine_client.default_sampling_params_list = [
default_comprehension_params,
default_other_params,
]
return engine_client
@pytest.fixture
def serving_chat(mock_engine_client):
"""Create OmniOpenAIServingChat instance with mocked dependencies."""
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
# Create instance without calling __init__
instance = object.__new__(OmniOpenAIServingChat)
instance.engine_client = mock_engine_client
return instance
@pytest.fixture
def mock_request(mocker: MockerFixture):
"""Create a mock request with all OpenAI sampling params set to None."""
request = mocker.MagicMock()
# OpenAI standard sampling fields
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = None
request.stop_token_ids = None
request.frequency_penalty = None
request.presence_penalty = None
# Must be real Python objects (not MagicMock) so the code's explicit-field
# and extra_body checks work correctly.
request.model_fields_set = set()
request.extra_body = {}
return request
# =============================================================================
# Tests for _OPENAI_SAMPLING_FIELDS constant
# =============================================================================
def test_openai_sampling_fields_contains_expected_fields():
"""Test that _OPENAI_SAMPLING_FIELDS contains all expected OpenAI params."""
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
expected_fields = {
"temperature",
"top_p",
"top_k",
"max_tokens",
"min_tokens",
"seed",
"ignore_eos",
"stop",
"stop_token_ids",
"frequency_penalty",
"presence_penalty",
}
assert OmniOpenAIServingChat._OPENAI_SAMPLING_FIELDS == expected_fields
# =============================================================================
# Tests for _build_sampling_params_list_from_request
# =============================================================================
def test_preserves_yaml_defaults_when_no_request_params(serving_chat, mock_request):
"""Test that YAML defaults are preserved when request has no params."""
result = serving_chat._build_sampling_params_list_from_request(mock_request)
assert len(result) == 2
comprehension_params = result[0]
assert comprehension_params.temperature == 0.4
assert comprehension_params.top_p == 0.9
assert comprehension_params.top_k == 1 # YAML custom param preserved
assert comprehension_params.max_tokens == 4353
assert comprehension_params.seed == 42
assert comprehension_params.repetition_penalty == 1.05 # YAML custom param preserved
def test_request_temperature_overrides_yaml_default(serving_chat, mock_request):
"""Test that request temperature overrides YAML default."""
mock_request.temperature = 0.8
mock_request.model_fields_set = {"temperature"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
comprehension_params = result[0]
assert comprehension_params.temperature == 0.8 # Overridden
assert comprehension_params.seed == 42 # Preserved from YAML
assert comprehension_params.top_k == 1 # YAML custom param preserved
def test_request_top_p_overrides_yaml_default(serving_chat, mock_request):
"""Test that request top_p overrides YAML default."""
mock_request.top_p = 0.95
mock_request.model_fields_set = {"top_p"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
comprehension_params = result[0]
assert comprehension_params.top_p == 0.95 # Overridden
assert comprehension_params.temperature == 0.4 # Preserved from YAML
def test_request_max_tokens_overrides_yaml_default(serving_chat, mock_request):
"""Test that request max_tokens overrides YAML default."""
mock_request.max_tokens = 100
mock_request.model_fields_set = {"max_tokens"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
assert result[0].max_tokens == 100
def test_max_tokens_uses_yaml_default_when_not_specified(serving_chat, mock_request):
"""Test that max_tokens falls back to YAML default when not in request."""
result = serving_chat._build_sampling_params_list_from_request(mock_request)
assert result[0].max_tokens == 4353
def test_request_seed_overrides_yaml_default(serving_chat, mock_request):
"""Test that request seed overrides YAML default."""
mock_request.seed = 123
mock_request.model_fields_set = {"seed"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
comprehension_params = result[0]
assert comprehension_params.seed == 123 # Overridden
assert comprehension_params.temperature == 0.4 # Preserved from YAML
def test_request_frequency_penalty_overrides(serving_chat, mock_request):
"""Test that request frequency_penalty is applied."""
mock_request.frequency_penalty = 0.5
mock_request.model_fields_set = {"frequency_penalty"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
assert result[0].frequency_penalty == 0.5
def test_request_presence_penalty_overrides(serving_chat, mock_request):
"""Test that request presence_penalty is applied."""
mock_request.presence_penalty = 0.3
mock_request.model_fields_set = {"presence_penalty"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
assert result[0].presence_penalty == 0.3
def test_non_comprehension_stages_use_cloned_defaults(serving_chat, mock_request):
"""Test that non-comprehension stages always use cloned YAML defaults."""
mock_request.max_tokens = 50
mock_request.temperature = 0.1
result = serving_chat._build_sampling_params_list_from_request(mock_request)
other_params = result[1]
assert other_params.temperature == 0.9 # YAML default (not affected by request)
assert other_params.max_tokens == 4096 # YAML default (not affected by request)
assert other_params.top_k == 50 # YAML default
assert other_params.seed == 42 # YAML default
def test_multiple_params_override_together(serving_chat, mock_request):
"""Test that multiple request params can override together."""
mock_request.max_tokens = 200
mock_request.temperature = 0.7
mock_request.top_p = 0.85
mock_request.seed = 999
mock_request.model_fields_set = {"max_tokens", "temperature", "top_p", "seed"}
result = serving_chat._build_sampling_params_list_from_request(mock_request)
comprehension_params = result[0]
# Overridden by request
assert comprehension_params.temperature == 0.7
assert comprehension_params.top_p == 0.85
assert comprehension_params.max_tokens == 200
assert comprehension_params.seed == 999
# Preserved from YAML (not in _OPENAI_SAMPLING_FIELDS)
assert comprehension_params.top_k == 1
assert comprehension_params.repetition_penalty == 1.05
# =============================================================================
# Tests for _apply_request_overrides
# =============================================================================
def test_apply_request_overrides_clones_params(serving_chat, mock_request, default_comprehension_params):
"""Test that _apply_request_overrides returns a cloned object."""
result = serving_chat._apply_request_overrides(default_comprehension_params, mock_request)
assert result is not default_comprehension_params # Different object
def test_apply_request_overrides_preserves_defaults(serving_chat, mock_request, default_comprehension_params):
"""Test that _apply_request_overrides preserves defaults when request has None."""
result = serving_chat._apply_request_overrides(default_comprehension_params, mock_request)
assert result.temperature == 0.4
assert result.top_p == 0.9
assert result.seed == 42
assert result.top_k == 1 # YAML custom param
def test_apply_request_overrides_applies_values(serving_chat, mock_request, default_comprehension_params):
"""Test that _apply_request_overrides applies non-None request values."""
mock_request.temperature = 0.8
mock_request.seed = 123
mock_request.model_fields_set = {"temperature", "seed"}
result = serving_chat._apply_request_overrides(default_comprehension_params, mock_request)
assert result.temperature == 0.8 # Overridden
assert result.seed == 123 # Overridden
assert result.top_p == 0.9 # Preserved from default
assert result.top_k == 1 # YAML custom param preserved
# =============================================================================
# Tests for empty-list handling in _apply_request_overrides
# =============================================================================
def test_apply_overrides_empty_stop_list_preserves_default(serving_chat, mocker):
"""Test that request.stop=[] does NOT override YAML default stop words."""
default_params = SamplingParams(temperature=0.5, stop=["<|im_end|>"])
request = mocker.MagicMock()
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = [] # empty list — should be treated as "not set"
request.stop_token_ids = None
request.frequency_penalty = None
request.presence_penalty = None
request.model_fields_set = {"stop"}
request.extra_body = {}
result = serving_chat._apply_request_overrides(default_params, request)
assert result.stop == ["<|im_end|>"] # YAML default preserved
def test_apply_overrides_nonempty_stop_list_overrides_default(serving_chat, mocker):
"""Test that request.stop=["\\n"] overrides YAML default stop words."""
default_params = SamplingParams(temperature=0.5, stop=["<|im_end|>"])
request = mocker.MagicMock()
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = ["\n"] # non-empty list — should override
request.stop_token_ids = None
request.frequency_penalty = None
request.presence_penalty = None
request.model_fields_set = {"stop"}
request.extra_body = {}
result = serving_chat._apply_request_overrides(default_params, request)
assert result.stop == ["\n"] # Overridden by request
def test_apply_overrides_empty_stop_token_ids_preserves_default(serving_chat, mocker):
"""Test that request.stop_token_ids=[] does NOT override YAML default."""
default_params = SamplingParams(temperature=0.5, stop_token_ids=[2, 3])
request = mocker.MagicMock()
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = None
request.stop_token_ids = [] # empty list — should be treated as "not set"
request.frequency_penalty = None
request.presence_penalty = None
result = serving_chat._apply_request_overrides(default_params, request)
assert result.stop_token_ids == [2, 3] # YAML default preserved
def test_apply_overrides_nonempty_stop_token_ids_overrides_default(serving_chat, mocker):
"""Test that request.stop_token_ids=[100] overrides YAML default."""
default_params = SamplingParams(temperature=0.5, stop_token_ids=[2, 3])
request = mocker.MagicMock()
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = None
request.stop_token_ids = [100] # non-empty list — should override
request.frequency_penalty = None
request.presence_penalty = None
request.model_fields_set = {"stop_token_ids"}
request.extra_body = {}
result = serving_chat._apply_request_overrides(default_params, request)
assert result.stop_token_ids == [100] # Overridden by request
def test_apply_overrides_mixed_empty_and_nonempty_lists(serving_chat, mocker):
"""Test mixing empty and non-empty list fields with scalar fields."""
default_params = SamplingParams(
temperature=0.4,
stop=["<|end|>"],
stop_token_ids=[2],
)
request = mocker.MagicMock()
request.temperature = 0.9
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = [] # empty — should NOT override
request.stop_token_ids = [100, 200] # non-empty — SHOULD override
request.frequency_penalty = None
request.presence_penalty = None
request.model_fields_set = {"temperature", "stop", "stop_token_ids"}
request.extra_body = {}
result = serving_chat._apply_request_overrides(default_params, request)
assert result.temperature == 0.9 # Scalar override works
assert result.stop == ["<|end|>"] # Empty list did NOT override
assert result.stop_token_ids == [100, 200] # Non-empty list DID override
def test_apply_overrides_none_scalar_still_preserves_default(serving_chat, mocker):
"""Regression: ensure None scalar values still don't override defaults."""
default_params = SamplingParams(temperature=0.5, max_tokens=100, seed=42)
request = mocker.MagicMock()
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = None
request.stop_token_ids = None
request.frequency_penalty = None
request.presence_penalty = None
request.model_fields_set = set()
request.extra_body = {}
result = serving_chat._apply_request_overrides(default_params, request)
assert result.temperature == 0.5
assert result.max_tokens == 100
assert result.seed == 42
def test_apply_overrides_both_lists_empty_preserves_defaults(serving_chat, mocker):
"""Test that both stop=[] and stop_token_ids=[] preserve YAML defaults."""
default_params = SamplingParams(
temperature=0.5,
stop=["<|end|>", "\\n"],
stop_token_ids=[2, 32000],
)
request = mocker.MagicMock()
request.temperature = None
request.top_p = None
request.top_k = None
request.max_tokens = None
request.min_tokens = None
request.seed = None
request.ignore_eos = None
request.stop = []
request.stop_token_ids = []
request.frequency_penalty = None
request.presence_penalty = None
request.model_fields_set = {"stop", "stop_token_ids"}
request.extra_body = {}
result = serving_chat._apply_request_overrides(default_params, request)
assert result.stop == ["<|end|>", "\\n"]
assert result.stop_token_ids == [2, 32000]
def test_build_sampling_params_list_empty_stop_preserves_yaml(serving_chat, mock_request):
"""Test that empty stop list in request preserves YAML defaults via
_build_sampling_params_list_from_request."""
mock_request.stop = []
mock_request.stop_token_ids = []
result = serving_chat._build_sampling_params_list_from_request(mock_request)
comprehension_params = result[0]
# Empty lists should NOT override — YAML defaults are preserved
assert comprehension_params.stop == []
assert comprehension_params.stop_token_ids == []
# =============================================================================
# Tests for _get_comprehension_stage_index
# =============================================================================
def test_get_comprehension_stage_index_finds_first_stage(mock_engine_client):
"""Test finding comprehension stage when it's at index 0."""
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
instance = object.__new__(OmniOpenAIServingChat)
instance.engine_client = mock_engine_client
assert instance._get_comprehension_stage_index() == 0
def test_get_comprehension_stage_index_finds_second_stage(mocker: MockerFixture):
"""Test finding comprehension stage when it's at index 1."""
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
instance = object.__new__(OmniOpenAIServingChat)
other = mocker.MagicMock()
other.is_comprehension = False
comprehension = mocker.MagicMock()
comprehension.is_comprehension = True
instance.engine_client = mocker.MagicMock()
instance.engine_client.stage_configs = [other, comprehension]
assert instance._get_comprehension_stage_index() == 1
def test_get_comprehension_stage_index_raises_when_not_found(mocker: MockerFixture):
"""Test that ValueError is raised when no comprehension stage exists."""
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
instance = object.__new__(OmniOpenAIServingChat)
stage1 = mocker.MagicMock()
stage1.is_comprehension = False
stage2 = mocker.MagicMock()
stage2.is_comprehension = False
instance.engine_client = mocker.MagicMock()
instance.engine_client.stage_configs = [stage1, stage2]
with pytest.raises(ValueError, match="No comprehension stage"):
instance._get_comprehension_stage_index()
# =============================================================================
# Tests for _resolve_height_width_from_extra_body
# =============================================================================
class TestResolveHeightWidth:
def test_explicit_height_width(self):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({"height": 512, "width": 768})
assert h == 512
assert w == 768
def test_size_string(self):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({"size": "768x512"})
assert w == 768
assert h == 512
def test_size_string_uppercase(self):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({"size": "768X512"})
assert w == 768
assert h == 512
def test_size_fallback_when_height_missing(self):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({"size": "512x512", "width": 1024})
# height is None -> size fallback fires and sets BOTH width and height
assert h == 512
assert w == 512
def test_empty_extra_body(self):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({})
assert h is None
assert w is None
def test_invalid_size_format_ignored(self):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
h, w = OmniOpenAIServingChat._resolve_height_width_from_extra_body({"size": "invalid"})
assert h is None
assert w is None
# =============================================================================
# Tests for _apply_request_overrides with GLM-Image (target_h/w injection)
# =============================================================================
class TestApplyRequestOverridesGLMImage:
"""Test target_h/w injection for GLM-Image AR stage.
max_tokens is NOT computed dynamically — it comes from the deploy YAML
default (e.g. 4353). _apply_request_overrides only injects target_h/w
into extra_args so the model can build M-RoPE position grids.
"""
@pytest.fixture
def glm_serving_chat(self, mock_engine_client, mocker: MockerFixture):
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
instance = object.__new__(OmniOpenAIServingChat)
instance.engine_client = mock_engine_client
instance._extract_diffusion_prompt_and_images_from_messages = mocker.MagicMock(return_value=("a cat", []))
return instance
@pytest.fixture
def glm_request(self, mocker: MockerFixture):
req = mocker.MagicMock()
req.temperature = None
req.top_p = None
req.top_k = None
req.max_tokens = None
req.min_tokens = None
req.seed = None
req.ignore_eos = None
req.stop = None
req.stop_token_ids = None
req.frequency_penalty = None
req.presence_penalty = None
req.extra_body = {"height": 1024, "width": 1024}
req.model_fields_set = set()
return req
def test_t2i_injects_target_h_w(self, glm_serving_chat, glm_request, default_comprehension_params):
"""t2i mode: target_h/w injected into extra_args, max_tokens unchanged."""
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, glm_request)
assert result.extra_args["target_h"] == 1024
assert result.extra_args["target_w"] == 1024
# max_tokens stays at YAML default (not dynamically computed)
assert result.max_tokens == 4353
def test_i2i_injects_target_h_w(
self, glm_serving_chat, glm_request, default_comprehension_params, mocker: MockerFixture
):
"""i2i mode: target_h/w injected, max_tokens unchanged."""
glm_serving_chat._extract_diffusion_prompt_and_images_from_messages = mocker.MagicMock(
return_value=("edit this", ["fake_image"])
)
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, glm_request)
assert result.extra_args["target_h"] == 1024
assert result.extra_args["target_w"] == 1024
# max_tokens stays at YAML default regardless of t2i/i2i
assert result.max_tokens == 4353
def test_user_max_tokens_preserved(self, glm_serving_chat, glm_request, default_comprehension_params):
"""User-provided max_tokens is respected (not overridden by dynamic computation)."""
glm_request.max_tokens = 500
glm_request.model_fields_set = {"max_tokens"}
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, glm_request)
assert result.max_tokens == 500
assert result.extra_args["target_h"] == 1024
assert result.extra_args["target_w"] == 1024
def test_no_height_width_preserves_default(
self, glm_serving_chat, mocker: MockerFixture, default_comprehension_params
):
"""When no height/width in extra_body, keep YAML default max_tokens, no target_h/w."""
req = mocker.MagicMock()
req.temperature = None
req.top_p = None
req.top_k = None
req.max_tokens = None
req.min_tokens = None
req.seed = None
req.ignore_eos = None
req.stop = None
req.stop_token_ids = None
req.frequency_penalty = None
req.presence_penalty = None
req.extra_body = {}
req.model_fields_set = set()
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, req)
assert result.max_tokens == 4353 # YAML default
# No target_h/w injected when dimensions not provided
assert not result.extra_args or "target_h" not in (result.extra_args or {})
def test_size_string_parsed_for_glm_image(
self, glm_serving_chat, mocker: MockerFixture, default_comprehension_params
):
"""'size' in extra_body is parsed as fallback for height/width."""
req = mocker.MagicMock()
req.temperature = None
req.top_p = None
req.top_k = None
req.max_tokens = None
req.min_tokens = None
req.seed = None
req.ignore_eos = None
req.stop = None
req.stop_token_ids = None
req.frequency_penalty = None
req.presence_penalty = None
req.extra_body = {"size": "512x512"}
req.model_fields_set = set()
result = glm_serving_chat._apply_request_overrides(default_comprehension_params, req)
assert result.extra_args["target_h"] == 512
assert result.extra_args["target_w"] == 512
# max_tokens stays at YAML default (not dynamically computed)
assert result.max_tokens == 4353