[V1][Structured Output] Move xgrammar related utils to backend_xgrammar.py
(#16578)
Signed-off-by: shen-shanshan <467638484@qq.com>
This commit is contained in:
parent
44fa4d556c
commit
976711d9db
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vllm.v1.structured_output.utils import (
|
from vllm.v1.structured_output.backend_xgrammar import (
|
||||||
has_xgrammar_unsupported_json_features)
|
has_xgrammar_unsupported_json_features)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ from vllm.v1.engine import EngineCoreRequest
|
|||||||
from vllm.v1.engine.mm_input_cache import MirroredProcessingCache
|
from vllm.v1.engine.mm_input_cache import MirroredProcessingCache
|
||||||
from vllm.v1.structured_output.backend_guidance import (
|
from vllm.v1.structured_output.backend_guidance import (
|
||||||
validate_guidance_grammar)
|
validate_guidance_grammar)
|
||||||
from vllm.v1.structured_output.utils import (
|
from vllm.v1.structured_output.backend_xgrammar import (
|
||||||
validate_structured_output_request_xgrammar)
|
validate_xgrammar_grammar)
|
||||||
|
|
||||||
|
|
||||||
class Processor:
|
class Processor:
|
||||||
@ -165,7 +165,7 @@ class Processor:
|
|||||||
# Request content validation
|
# Request content validation
|
||||||
if engine_level_backend.startswith("xgrammar"):
|
if engine_level_backend.startswith("xgrammar"):
|
||||||
# xgrammar with no fallback
|
# xgrammar with no fallback
|
||||||
validate_structured_output_request_xgrammar(params)
|
validate_xgrammar_grammar(params)
|
||||||
params.guided_decoding.backend = engine_level_backend
|
params.guided_decoding.backend = engine_level_backend
|
||||||
elif engine_level_backend == "auto":
|
elif engine_level_backend == "auto":
|
||||||
# "auto" is an opt-in to opinionated behavior where we try to
|
# "auto" is an opt-in to opinionated behavior where we try to
|
||||||
@ -173,7 +173,7 @@ class Processor:
|
|||||||
# default as it is less predictable and subject to change
|
# default as it is less predictable and subject to change
|
||||||
# between releases as feature support changes.
|
# between releases as feature support changes.
|
||||||
try:
|
try:
|
||||||
validate_structured_output_request_xgrammar(params)
|
validate_xgrammar_grammar(params)
|
||||||
params.guided_decoding.backend = "xgrammar"
|
params.guided_decoding.backend = "xgrammar"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# The request includes some jsonschema feature(s) that
|
# The request includes some jsonschema feature(s) that
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import json
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
import vllm.envs
|
import vllm.envs
|
||||||
from vllm.config import VllmConfig
|
from vllm.config import VllmConfig
|
||||||
from vllm.logger import init_logger
|
from vllm.logger import init_logger
|
||||||
|
from vllm.sampling_params import SamplingParams
|
||||||
from vllm.transformers_utils.tokenizer_group import init_tokenizer_from_configs
|
from vllm.transformers_utils.tokenizer_group import init_tokenizer_from_configs
|
||||||
from vllm.transformers_utils.tokenizers.mistral import MistralTokenizer
|
from vllm.transformers_utils.tokenizers.mistral import MistralTokenizer
|
||||||
from vllm.utils import LazyLoader
|
from vllm.utils import LazyLoader
|
||||||
from vllm.v1.structured_output.backend_types import (StructuredOutputBackend,
|
from vllm.v1.structured_output.backend_types import (StructuredOutputBackend,
|
||||||
StructuredOutputGrammar,
|
StructuredOutputGrammar,
|
||||||
StructuredOutputOptions)
|
StructuredOutputOptions)
|
||||||
|
from vllm.v1.structured_output.utils import (choice_as_grammar,
|
||||||
|
convert_lark_to_ebnf,
|
||||||
|
grammar_is_likely_lark)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import xgrammar as xgr
|
import xgrammar as xgr
|
||||||
@ -156,3 +161,112 @@ class XgrammarGrammar(StructuredOutputGrammar):
|
|||||||
def reset(self):
|
def reset(self):
|
||||||
self.num_processed_tokens = 0
|
self.num_processed_tokens = 0
|
||||||
self.matcher.reset()
|
self.matcher.reset()
|
||||||
|
|
||||||
|
|
||||||
|
def has_xgrammar_unsupported_json_features(schema: dict[str, Any]) -> bool:
|
||||||
|
"""Check if JSON schema contains features unsupported by xgrammar."""
|
||||||
|
|
||||||
|
def check_object(obj: dict[str, Any]) -> bool:
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for pattern restrictions
|
||||||
|
if "pattern" in obj:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for numeric ranges
|
||||||
|
if obj.get("type") in ("integer", "number") and any(
|
||||||
|
key in obj
|
||||||
|
for key in ("minimum", "maximum", "exclusiveMinimum",
|
||||||
|
"exclusiveMaximum", "multipleOf")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for array unsupported keywords
|
||||||
|
if obj.get("type") == "array" and any(
|
||||||
|
key in obj
|
||||||
|
for key in ("uniqueItems", "contains", "minContains",
|
||||||
|
"maxContains", "minItems", "maxItems")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Unsupported keywords for strings
|
||||||
|
if obj.get("type") == "string" and "format" in obj:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Unsupported keywords for objects
|
||||||
|
if obj.get("type") == "object" and any(
|
||||||
|
key in obj for key in ("minProperties", "maxProperties",
|
||||||
|
"propertyNames", "patternProperties")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Recursively check all nested objects and arrays
|
||||||
|
for value in obj.values():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if check_object(value):
|
||||||
|
return True
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, dict) and check_object(item):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return check_object(schema)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
|
||||||
|
"""Validate that the request is supported by structured output.
|
||||||
|
|
||||||
|
Raises ValueError if the request is not supported.
|
||||||
|
"""
|
||||||
|
if sampling_params.guided_decoding is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
gd_params = sampling_params.guided_decoding
|
||||||
|
|
||||||
|
if gd_params.regex:
|
||||||
|
try:
|
||||||
|
xgr.Grammar.from_regex(gd_params.regex)
|
||||||
|
except Exception as err:
|
||||||
|
raise ValueError("Failed to transform regex into a grammar: "
|
||||||
|
f"{err}") from err
|
||||||
|
|
||||||
|
if gd_params.choice:
|
||||||
|
choice_grammar = choice_as_grammar(gd_params.choice)
|
||||||
|
try:
|
||||||
|
xgr.Grammar.from_ebnf(choice_grammar)
|
||||||
|
except Exception as err:
|
||||||
|
raise ValueError("Failed to transform choices into a grammar: "
|
||||||
|
"{err}") from err
|
||||||
|
gd_params.choice = None
|
||||||
|
gd_params.grammar = choice_grammar
|
||||||
|
return
|
||||||
|
|
||||||
|
if gd_params.json:
|
||||||
|
if isinstance(gd_params.json, str):
|
||||||
|
try:
|
||||||
|
schema = json.loads(gd_params.json)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError("Invalid JSON grammar specification.") from e
|
||||||
|
else:
|
||||||
|
schema = gd_params.json
|
||||||
|
|
||||||
|
if has_xgrammar_unsupported_json_features(schema):
|
||||||
|
raise ValueError("The provided JSON schema contains features not "
|
||||||
|
"supported by xgrammar.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if gd_params.grammar:
|
||||||
|
if grammar_is_likely_lark(gd_params.grammar):
|
||||||
|
# xgrammar supports EBNF grammars only
|
||||||
|
try:
|
||||||
|
gd_params.grammar = convert_lark_to_ebnf(gd_params.grammar)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(
|
||||||
|
"Failed to convert the grammar from Lark to EBNF. ") from e
|
||||||
|
|
||||||
|
# Test parsing EBNF grammar, possibly already converted from Lark
|
||||||
|
try:
|
||||||
|
# parse the grammar, but we aren't compiling it.
|
||||||
|
xgr.Grammar.from_ebnf(gd_params.grammar)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError("Invalid grammar specification.") from e
|
||||||
|
@ -2,67 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from vllm.sampling_params import SamplingParams
|
|
||||||
from vllm.utils import LazyLoader
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
import xgrammar as xgr
|
|
||||||
else:
|
|
||||||
xgr = LazyLoader("xgr", globals(), "xgrammar")
|
|
||||||
|
|
||||||
|
|
||||||
def has_xgrammar_unsupported_json_features(schema: dict[str, Any]) -> bool:
|
|
||||||
"""Check if JSON schema contains features unsupported by xgrammar."""
|
|
||||||
|
|
||||||
def check_object(obj: dict[str, Any]) -> bool:
|
|
||||||
if not isinstance(obj, dict):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check for pattern restrictions
|
|
||||||
if "pattern" in obj:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check for numeric ranges
|
|
||||||
if obj.get("type") in ("integer", "number") and any(
|
|
||||||
key in obj
|
|
||||||
for key in ("minimum", "maximum", "exclusiveMinimum",
|
|
||||||
"exclusiveMaximum", "multipleOf")):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check for array unsupported keywords
|
|
||||||
if obj.get("type") == "array" and any(
|
|
||||||
key in obj
|
|
||||||
for key in ("uniqueItems", "contains", "minContains",
|
|
||||||
"maxContains", "minItems", "maxItems")):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Unsupported keywords for strings
|
|
||||||
if obj.get("type") == "string" and "format" in obj:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Unsupported keywords for objects
|
|
||||||
if obj.get("type") == "object" and any(
|
|
||||||
key in obj for key in ("minProperties", "maxProperties",
|
|
||||||
"propertyNames", "patternProperties")):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Recursively check all nested objects and arrays
|
|
||||||
for value in obj.values():
|
|
||||||
if isinstance(value, dict):
|
|
||||||
if check_object(value):
|
|
||||||
return True
|
|
||||||
elif isinstance(value, list):
|
|
||||||
for item in value:
|
|
||||||
if isinstance(item, dict) and check_object(item):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
return check_object(schema)
|
|
||||||
|
|
||||||
|
|
||||||
def grammar_is_likely_lark(grammar_str: str) -> bool:
|
def grammar_is_likely_lark(grammar_str: str) -> bool:
|
||||||
@ -232,63 +172,3 @@ def choice_as_grammar(choice: list[str]) -> str:
|
|||||||
escaped_choices = (escape_ebnf_string(c) for c in choice)
|
escaped_choices = (escape_ebnf_string(c) for c in choice)
|
||||||
grammar = ('root ::= ' + ' | '.join(f'"{c}"' for c in escaped_choices))
|
grammar = ('root ::= ' + ' | '.join(f'"{c}"' for c in escaped_choices))
|
||||||
return grammar
|
return grammar
|
||||||
|
|
||||||
|
|
||||||
def validate_structured_output_request_xgrammar(
|
|
||||||
sampling_params: SamplingParams) -> None:
|
|
||||||
"""Validate that the request is supported by structured output.
|
|
||||||
|
|
||||||
Raises ValueError if the request is not supported.
|
|
||||||
"""
|
|
||||||
if sampling_params.guided_decoding is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
gd_params = sampling_params.guided_decoding
|
|
||||||
|
|
||||||
if gd_params.regex:
|
|
||||||
try:
|
|
||||||
xgr.Grammar.from_regex(gd_params.regex)
|
|
||||||
except Exception as err:
|
|
||||||
raise ValueError("Failed to transform regex into a grammar: "
|
|
||||||
f"{err}") from err
|
|
||||||
|
|
||||||
if gd_params.choice:
|
|
||||||
choice_grammar = choice_as_grammar(gd_params.choice)
|
|
||||||
try:
|
|
||||||
xgr.Grammar.from_ebnf(choice_grammar)
|
|
||||||
except Exception as err:
|
|
||||||
raise ValueError("Failed to transform choices into a grammar: "
|
|
||||||
"{err}") from err
|
|
||||||
gd_params.choice = None
|
|
||||||
gd_params.grammar = choice_grammar
|
|
||||||
return
|
|
||||||
|
|
||||||
if gd_params.json:
|
|
||||||
if isinstance(gd_params.json, str):
|
|
||||||
try:
|
|
||||||
schema = json.loads(gd_params.json)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise ValueError("Invalid JSON grammar specification.") from e
|
|
||||||
else:
|
|
||||||
schema = gd_params.json
|
|
||||||
|
|
||||||
if has_xgrammar_unsupported_json_features(schema):
|
|
||||||
raise ValueError("The provided JSON schema contains features not "
|
|
||||||
"supported by xgrammar.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if gd_params.grammar:
|
|
||||||
if grammar_is_likely_lark(gd_params.grammar):
|
|
||||||
# xgrammar supports EBNF grammars only
|
|
||||||
try:
|
|
||||||
gd_params.grammar = convert_lark_to_ebnf(gd_params.grammar)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ValueError(
|
|
||||||
"Failed to convert the grammar from Lark to EBNF. ") from e
|
|
||||||
|
|
||||||
# Test parsing EBNF grammar, possibly already converted from Lark
|
|
||||||
try:
|
|
||||||
# parse the grammar, but we aren't compiling it.
|
|
||||||
xgr.Grammar.from_ebnf(gd_params.grammar)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError("Invalid grammar specification.") from e
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user