215
tests/ut/patch/platform/test_patch_structured_output.py
Normal file
215
tests/ut/patch/platform/test_patch_structured_output.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from inspect import signature
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import vllm.v1.structured_output as structured_output
|
||||
from vllm.config.structured_outputs import StructuredOutputsConfig
|
||||
from vllm.sampling_params import SamplingParams, StructuredOutputsParams
|
||||
from vllm.v1.structured_output import StructuredOutputManager, backend_guidance, backend_xgrammar
|
||||
from vllm.v1.structured_output.backend_types import StructuredOutputOptions
|
||||
|
||||
from vllm_ascend.patch.platform import patch_structured_output # noqa: F401
|
||||
|
||||
MODEL_CONFIG = SimpleNamespace(is_diffusion=False)
|
||||
|
||||
|
||||
class FakeBackend:
|
||||
def __init__(self, vllm_config, tokenizer, vocab_size):
|
||||
self.vllm_config = vllm_config
|
||||
self.tokenizer = tokenizer
|
||||
self.vocab_size = vocab_size
|
||||
|
||||
def compile_grammar(self, request_type, grammar_spec):
|
||||
return (type(self).__name__, request_type, grammar_spec)
|
||||
|
||||
|
||||
class FakeXgrammarBackend(FakeBackend):
|
||||
pass
|
||||
|
||||
|
||||
class FakeGuidanceBackend(FakeBackend):
|
||||
pass
|
||||
|
||||
|
||||
def make_manager() -> StructuredOutputManager:
|
||||
manager = object.__new__(StructuredOutputManager)
|
||||
manager.backend = None
|
||||
manager.vllm_config = SimpleNamespace(model_config=SimpleNamespace(get_vocab_size=lambda: 128))
|
||||
manager.tokenizer = object()
|
||||
manager._use_async_grammar_compilation = False
|
||||
return manager
|
||||
|
||||
|
||||
def make_request(backend: str):
|
||||
return SimpleNamespace(
|
||||
sampling_params=SimpleNamespace(structured_outputs=SimpleNamespace(_backend=backend)),
|
||||
structured_output_request=SimpleNamespace(
|
||||
structured_output_key=(StructuredOutputOptions.JSON, "{}"),
|
||||
grammar=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def validate_structured_outputs(params, config):
|
||||
original_validate = getattr(
|
||||
SamplingParams,
|
||||
patch_structured_output._ORIGINAL_VALIDATE_ATTR,
|
||||
)
|
||||
if "model_config" in signature(original_validate).parameters:
|
||||
params._validate_structured_outputs(MODEL_CONFIG, config, tokenizer=object())
|
||||
else:
|
||||
params._validate_structured_outputs(config, tokenizer=object())
|
||||
|
||||
|
||||
def test_sampling_params_rejects_mixed_structured_output_backends(monkeypatch):
|
||||
def fake_validate_xgrammar(sampling_params):
|
||||
schema = sampling_params.structured_outputs.json
|
||||
if schema.get("force_guidance"):
|
||||
raise ValueError("xgrammar unsupported")
|
||||
|
||||
monkeypatch.setattr(
|
||||
backend_xgrammar,
|
||||
"validate_xgrammar_grammar",
|
||||
fake_validate_xgrammar,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
backend_guidance,
|
||||
"has_guidance_unsupported_json_features",
|
||||
lambda schema: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
backend_guidance,
|
||||
"validate_guidance_grammar",
|
||||
lambda sampling_params, tokenizer=None: None,
|
||||
)
|
||||
|
||||
config = StructuredOutputsConfig(backend="auto")
|
||||
xgrammar_params = SamplingParams(structured_outputs=StructuredOutputsParams(json={"type": "object"}))
|
||||
validate_structured_outputs(xgrammar_params, config)
|
||||
|
||||
assert xgrammar_params.structured_outputs._backend == "xgrammar"
|
||||
assert getattr(config, patch_structured_output._BACKEND_ATTR) == "xgrammar"
|
||||
|
||||
guidance_params = SamplingParams(structured_outputs=StructuredOutputsParams(json={"force_guidance": True}))
|
||||
with pytest.raises(ValueError, match="already using 'xgrammar'.*'guidance'"):
|
||||
validate_structured_outputs(guidance_params, config)
|
||||
|
||||
|
||||
def test_sampling_params_allows_consistent_guidance_backend(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
backend_guidance,
|
||||
"has_guidance_unsupported_json_features",
|
||||
lambda schema: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
backend_guidance,
|
||||
"validate_guidance_grammar",
|
||||
lambda sampling_params, tokenizer=None: None,
|
||||
)
|
||||
|
||||
config = StructuredOutputsConfig(backend="guidance")
|
||||
for _ in range(2):
|
||||
params = SamplingParams(structured_outputs=StructuredOutputsParams(json={"type": "array"}))
|
||||
validate_structured_outputs(params, config)
|
||||
|
||||
assert params.structured_outputs._backend == "guidance"
|
||||
assert getattr(config, patch_structured_output._BACKEND_ATTR) == "guidance"
|
||||
|
||||
|
||||
def test_failed_first_validation_does_not_lock_config(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
backend_xgrammar,
|
||||
"validate_xgrammar_grammar",
|
||||
lambda sampling_params: (_ for _ in ()).throw(ValueError("xgrammar error")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
backend_guidance,
|
||||
"has_guidance_unsupported_json_features",
|
||||
lambda schema: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
backend_guidance,
|
||||
"validate_guidance_grammar",
|
||||
lambda sampling_params, tokenizer=None: (_ for _ in ()).throw(ValueError("guidance error")),
|
||||
)
|
||||
|
||||
config = StructuredOutputsConfig(backend="auto")
|
||||
params = SamplingParams(structured_outputs=StructuredOutputsParams(json={"force_guidance": True}))
|
||||
with pytest.raises(ValueError, match="guidance error"):
|
||||
validate_structured_outputs(params, config)
|
||||
|
||||
assert not hasattr(config, patch_structured_output._BACKEND_ATTR)
|
||||
|
||||
|
||||
def test_manager_rejects_mixed_structured_output_backends(monkeypatch):
|
||||
monkeypatch.setattr(structured_output, "XgrammarBackend", FakeXgrammarBackend)
|
||||
monkeypatch.setattr(structured_output, "GuidanceBackend", FakeGuidanceBackend)
|
||||
|
||||
manager = make_manager()
|
||||
xgrammar_request = make_request("xgrammar")
|
||||
manager.grammar_init(xgrammar_request)
|
||||
|
||||
assert isinstance(manager.backend, FakeXgrammarBackend)
|
||||
assert (
|
||||
getattr(
|
||||
manager,
|
||||
patch_structured_output._BACKEND_ATTR,
|
||||
)
|
||||
== "xgrammar"
|
||||
)
|
||||
assert xgrammar_request.structured_output_request.grammar == (
|
||||
"FakeXgrammarBackend",
|
||||
StructuredOutputOptions.JSON,
|
||||
"{}",
|
||||
)
|
||||
|
||||
guidance_request = make_request("guidance")
|
||||
with pytest.raises(ValueError, match="already using 'xgrammar'.*'guidance'"):
|
||||
manager.grammar_init(guidance_request)
|
||||
|
||||
|
||||
def test_manager_rejects_mixed_backend_after_subclassed_backend_is_initialized():
|
||||
manager = make_manager()
|
||||
manager.backend = FakeXgrammarBackend(
|
||||
manager.vllm_config,
|
||||
manager.tokenizer,
|
||||
manager.vllm_config.model_config.get_vocab_size(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="already using 'xgrammar'.*'guidance'"):
|
||||
manager.grammar_init(make_request("guidance"))
|
||||
|
||||
|
||||
def test_manager_allows_consistent_guidance_backend(monkeypatch):
|
||||
monkeypatch.setattr(structured_output, "GuidanceBackend", FakeGuidanceBackend)
|
||||
|
||||
manager = make_manager()
|
||||
for _ in range(2):
|
||||
request = make_request("guidance")
|
||||
manager.grammar_init(request)
|
||||
|
||||
assert isinstance(manager.backend, FakeGuidanceBackend)
|
||||
assert getattr(manager, patch_structured_output._BACKEND_ATTR) == "guidance"
|
||||
assert request.structured_output_request.grammar == (
|
||||
"FakeGuidanceBackend",
|
||||
StructuredOutputOptions.JSON,
|
||||
"{}",
|
||||
)
|
||||
|
||||
|
||||
def test_failed_first_backend_does_not_lock_manager(monkeypatch):
|
||||
monkeypatch.setattr(structured_output, "XgrammarBackend", FakeXgrammarBackend)
|
||||
|
||||
manager = make_manager()
|
||||
with pytest.raises(ValueError, match="Unsupported structured output backend"):
|
||||
manager.grammar_init(make_request("unsupported"))
|
||||
|
||||
assert not hasattr(manager, patch_structured_output._BACKEND_ATTR)
|
||||
|
||||
request = make_request("xgrammar")
|
||||
manager.grammar_init(request)
|
||||
|
||||
assert isinstance(manager.backend, FakeXgrammarBackend)
|
||||
assert getattr(manager, patch_structured_output._BACKEND_ATTR) == "xgrammar"
|
||||
Reference in New Issue
Block a user