@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / a09ad90a72
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 15, 17:15 UTC

[V1] guidance backend for structured output + `auto` fallback mode (#14779)

Signed-off-by: Russell Bryant <rbryant@redhat.com> Co-authored-by: Loc Huynh <jc1da.3011@gmail.com> Co-authored-by: Michal Moskal <michal@moskal.me>

Russell Bryant committed Mar 25, 2025 at 00:02 UTC a09ad90a72e47060007aa7011969fefb7dedf8f5
9 files changed +345 -111
requirements/common.txt
+1 -1
@@ -18,7 +18,7 @@ pillow # Required for image processing
18 prometheus-fastapi-instrumentator >= 7.0.0
19 tiktoken >= 0.6.0 # Required for DBRX tokenizer
20 lm-format-enforcer >= 0.10.11, < 0.11
21 -llguidance >= 0.7.2, < 0.8.0; platform_machine == "x86_64" or platform_machine == "arm64" or platform_machine == "aarch64"
21 +llguidance >= 0.7.9, < 0.8.0; platform_machine == "x86_64" or platform_machine == "arm64" or platform_machine == "aarch64"
22 outlines == 0.1.11
23 lark == 1.2.2
24 xgrammar == 0.1.16; platform_machine == "x86_64" or platform_machine == "aarch64"
tests/v1/entrypoints/llm/test_struct_output_generate.py
+102 -67
@@ -13,7 +13,7 @@ from vllm.entrypoints.llm import LLM
13 from vllm.outputs import RequestOutput
14 from vllm.sampling_params import GuidedDecodingParams, SamplingParams
15
16 -GUIDED_DECODING_BACKENDS_V1 = ["xgrammar"]
16 +GUIDED_DECODING_BACKENDS_V1 = ["xgrammar", "guidance"]
17 MODELS_TO_TEST = [
18 "Qwen/Qwen2.5-1.5B-Instruct", "mistralai/Ministral-8B-Instruct-2410"
19 ]
@@ -30,12 +30,13 @@ def test_guided_json_completion(
30 model_name: str,
31 ):
32 monkeypatch.setenv("VLLM_USE_V1", "1")
33 - llm = LLM(model=model_name, max_model_len=1024)
34 - sampling_params = SamplingParams(temperature=1.0,
35 - max_tokens=1000,
36 - guided_decoding=GuidedDecodingParams(
37 - json=sample_json_schema,
38 - backend=guided_decoding_backend))
33 + llm = LLM(model=model_name,
34 + max_model_len=1024,
35 + guided_decoding_backend=guided_decoding_backend)
36 + sampling_params = SamplingParams(
37 + temperature=1.0,
38 + max_tokens=1000,
39 + guided_decoding=GuidedDecodingParams(json=sample_json_schema))
40 outputs = llm.generate(prompts=[
41 f"Give an example JSON for an employee profile "
42 f"that fits this schema: {sample_json_schema}"
@@ -111,13 +112,14 @@ def test_guided_json_object(
112 model_name: str,
113 ):
114 monkeypatch.setenv("VLLM_USE_V1", "1")
114 - llm = LLM(model=model_name, max_model_len=1024)
115 - sampling_params = SamplingParams(temperature=1.0,
116 - max_tokens=100,
117 - n=2,
118 - guided_decoding=GuidedDecodingParams(
119 - json_object=True,
120 - backend=guided_decoding_backend))
115 + llm = LLM(model=model_name,
116 + max_model_len=1024,
117 + guided_decoding_backend=guided_decoding_backend)
118 + sampling_params = SamplingParams(
119 + temperature=1.0,
120 + max_tokens=100,
121 + n=2,
122 + guided_decoding=GuidedDecodingParams(json_object=True))
123
124 outputs = llm.generate(
125 prompts=("Generate a JSON object with curly braces for a person with "
@@ -137,12 +139,20 @@ def test_guided_json_object(
139
140 # Parse to verify it is valid JSON
141 parsed_json = json.loads(generated_text)
140 - assert isinstance(parsed_json, dict)
142 + allowed_types: tuple[type, ...] = (dict, )
143 + if guided_decoding_backend == "xgrammar":
144 + # TODO - we are currently too permissive with xgrammar and
145 + # allow # any valid json (typically comes back as a list or
146 + # object). We can fix this by specifying a jsonschema of
147 + # {"type": "object"}, # but we need this fix in a release
148 + # first: https://github.com/mlc-ai/xgrammar/pull/264
149 + allowed_types = (dict, list)
150 + assert isinstance(parsed_json, allowed_types)
151
152
153 @pytest.mark.skip_global_cleanup
154 @pytest.mark.parametrize("guided_decoding_backend",
145 - GUIDED_DECODING_BACKENDS_V1)
155 + GUIDED_DECODING_BACKENDS_V1 + ["auto"])
156 @pytest.mark.parametrize("model_name", MODELS_TO_TEST)
157 def test_guided_json_unsupported_schema(
158 monkeypatch: pytest.MonkeyPatch,
@@ -151,21 +161,43 @@ def test_guided_json_unsupported_schema(
161 model_name: str,
162 ):
163 monkeypatch.setenv("VLLM_USE_V1", "1")
154 - llm = LLM(model=model_name, max_model_len=1024)
155 - sampling_params = SamplingParams(temperature=1.0,
156 - max_tokens=1000,
157 - guided_decoding=GuidedDecodingParams(
158 - json=unsupported_json_schema,
159 - backend=guided_decoding_backend))
160 - with pytest.raises(ValueError,
161 - match="The provided JSON schema contains features "
162 - "not supported by xgrammar."):
163 - llm.generate(prompts=[
164 - f"Give an example JSON for an employee profile "
165 - f"that fits this schema: {unsupported_json_schema}"
166 - ] * 2,
167 - sampling_params=sampling_params,
168 - use_tqdm=True)
164 + llm = LLM(model=model_name,
165 + max_model_len=1024,
166 + guided_decoding_backend=guided_decoding_backend)
167 + sampling_params = SamplingParams(
168 + temperature=1.0,
169 + max_tokens=1000,
170 + guided_decoding=GuidedDecodingParams(json=unsupported_json_schema))
171 + if guided_decoding_backend == "xgrammar":
172 + with pytest.raises(ValueError,
173 + match="The provided JSON schema contains features "
174 + "not supported by xgrammar."):
175 + llm.generate(prompts=[
176 + f"Give an example JSON for an employee profile "
177 + f"that fits this schema: {unsupported_json_schema}"
178 + ] * 2,
179 + sampling_params=sampling_params,
180 + use_tqdm=True)
181 + else:
182 + # This should work for both "guidance" and "auto".
183 +
184 + outputs = llm.generate(
185 + prompts=("Give an example JSON object for a grade "
186 + "that fits this schema: "
187 + f"{unsupported_json_schema}"),
188 + sampling_params=sampling_params,
189 + use_tqdm=True)
190 + assert outputs is not None
191 + for output in outputs:
192 + assert output is not None
193 + assert isinstance(output, RequestOutput)
194 + generated_text = output.outputs[0].text
195 + assert generated_text is not None
196 + print(generated_text)
197 +
198 + # Parse to verify it is valid JSON
199 + parsed_json = json.loads(generated_text)
200 + assert isinstance(parsed_json, dict)
201
202
203 @pytest.mark.skip_global_cleanup
@@ -179,13 +211,14 @@ def test_guided_grammar_ebnf(
211 model_name: str,
212 ):
213 monkeypatch.setenv("VLLM_USE_V1", "1")
182 - llm = LLM(model=model_name, max_model_len=1024)
183 - sampling_params = SamplingParams(temperature=0.8,
184 - top_p=0.95,
185 - max_tokens=1000,
186 - guided_decoding=GuidedDecodingParams(
187 - grammar=sample_sql_ebnf,
188 - backend=guided_decoding_backend))
214 + llm = LLM(model=model_name,
215 + max_model_len=1024,
216 + guided_decoding_backend=guided_decoding_backend)
217 + sampling_params = SamplingParams(
218 + temperature=0.8,
219 + top_p=0.95,
220 + max_tokens=1000,
221 + guided_decoding=GuidedDecodingParams(grammar=sample_sql_ebnf))
222 outputs = llm.generate(
223 prompts=("Generate a sql statement that selects col_1 from "
224 "table_1 where it is equal to 1"),
@@ -222,13 +255,14 @@ def test_guided_grammar_lark(
255 model_name: str,
256 ):
257 monkeypatch.setenv("VLLM_USE_V1", "1")
225 - llm = LLM(model=model_name, max_model_len=1024)
226 - sampling_params = SamplingParams(temperature=0.8,
227 - top_p=0.95,
228 - max_tokens=1000,
229 - guided_decoding=GuidedDecodingParams(
230 - grammar=sample_sql_lark,
231 - backend=guided_decoding_backend))
258 + llm = LLM(model=model_name,
259 + max_model_len=1024,
260 + guided_decoding_backend=guided_decoding_backend)
261 + sampling_params = SamplingParams(
262 + temperature=0.8,
263 + top_p=0.95,
264 + max_tokens=1000,
265 + guided_decoding=GuidedDecodingParams(grammar=sample_sql_lark))
266 outputs = llm.generate(
267 prompts=("Generate a sql statement that selects col_1 from "
268 "table_1 where it is equal to 1"),
@@ -269,16 +303,15 @@ def test_guided_grammar_ebnf_invalid(
303 model_name: str,
304 ):
305 monkeypatch.setenv("VLLM_USE_V1", "1")
272 - llm = LLM(model=model_name, max_model_len=1024)
273 - sampling_params = SamplingParams(temperature=0.8,
274 - top_p=0.95,
275 - max_tokens=1000,
276 - guided_decoding=GuidedDecodingParams(
277 - grammar="not a grammar",
278 - backend=guided_decoding_backend))
279 - with pytest.raises(ValueError,
280 - match="Failed to convert the grammar "
281 - "from Lark to EBNF."):
306 + llm = LLM(model=model_name,
307 + max_model_len=1024,
308 + guided_decoding_backend=guided_decoding_backend)
309 + sampling_params = SamplingParams(
310 + temperature=0.8,
311 + top_p=0.95,
312 + max_tokens=1000,
313 + guided_decoding=GuidedDecodingParams(grammar="not a grammar"))
314 + with pytest.raises(ValueError, match="Failed to convert the grammar "):
315 llm.generate(
316 prompts=("Generate a sql statement that selects col_1 from "
317 "table_1 where it is equal to 1"),
@@ -298,12 +331,13 @@ def test_guided_regex(
331 model_name: str,
332 ):
333 monkeypatch.setenv("VLLM_USE_V1", "1")
301 - llm = LLM(model=model_name, max_model_len=1024)
302 - sampling_params = SamplingParams(temperature=0.8,
303 - top_p=0.95,
304 - guided_decoding=GuidedDecodingParams(
305 - regex=sample_regex,
306 - backend=guided_decoding_backend))
334 + llm = LLM(model=model_name,
335 + max_model_len=1024,
336 + guided_decoding_backend=guided_decoding_backend)
337 + sampling_params = SamplingParams(
338 + temperature=0.8,
339 + top_p=0.95,
340 + guided_decoding=GuidedDecodingParams(regex=sample_regex))
341 outputs = llm.generate(
342 prompts=[
343 f"Give an example IPv4 address with this regex: {sample_regex}"
@@ -335,12 +369,13 @@ def test_guided_choice_completion(
369 model_name: str,
370 ):
371 monkeypatch.setenv("VLLM_USE_V1", "1")
338 - llm = LLM(model=model_name, max_model_len=1024)
339 - sampling_params = SamplingParams(temperature=0.8,
340 - top_p=0.95,
341 - guided_decoding=GuidedDecodingParams(
342 - choice=sample_guided_choice,
343 - backend=guided_decoding_backend))
372 + llm = LLM(model=model_name,
373 + max_model_len=1024,
374 + guided_decoding_backend=guided_decoding_backend)
375 + sampling_params = SamplingParams(
376 + temperature=0.8,
377 + top_p=0.95,
378 + guided_decoding=GuidedDecodingParams(choice=sample_guided_choice))
379 outputs = llm.generate(
380 prompts="The best language for type-safe systems programming is ",
381 sampling_params=sampling_params,
vllm/config.py
+7 -2
@@ -2800,12 +2800,17 @@ class DecodingConfig:
2800 return hash_str
2801
2802 def __post_init__(self):
2803 - valid_guided_backends = [
2804 - 'outlines', 'lm-format-enforcer', 'xgrammar', 'guidance'
2803 + v0_valid_guided_backends = [
2804 + 'outlines', 'lm-format-enforcer', 'xgrammar'
2805 ]
2806 + v1_valid_guided_backends = ['xgrammar', 'guidance', 'auto']
2807
2808 backend = GuidedDecodingParams(
2809 backend=self.guided_decoding_backend).backend_name
2810 + if envs.VLLM_USE_V1:
2811 + valid_guided_backends = v1_valid_guided_backends
2812 + else:
2813 + valid_guided_backends = v0_valid_guided_backends
2814 if backend not in valid_guided_backends:
2815 raise ValueError(f"Invalid guided_decoding_backend '{backend}',"
2816 f" must be one of {valid_guided_backends}")
vllm/engine/arg_utils.py
+9 -12
@@ -391,16 +391,13 @@ class EngineArgs:
391 default='xgrammar',
392 help='Which engine will be used for guided decoding'
393 ' (JSON schema / regex etc) by default. Currently support '
394 - 'https://github.com/outlines-dev/outlines, '
395 - 'https://github.com/mlc-ai/xgrammar, and '
396 - 'https://github.com/noamgat/lm-format-enforcer.'
397 - ' Can be overridden per request via guided_decoding_backend'
398 - ' parameter.\n'
399 - 'Backend-specific options can be supplied in a comma-separated '
400 - 'list following a colon after the backend name. Valid backends and '
401 - 'all available options are: [xgrammar:no-fallback, '
402 - 'xgrammar:disable-any-whitespace, '
403 - 'outlines:no-fallback, lm-format-enforcer:no-fallback]')
394 + 'https://github.com/mlc-ai/xgrammar and '
395 + 'https://github.com/guidance-ai/llguidance.'
396 + 'Valid backend values are "xgrammar", "guidance", and "auto". '
397 + 'With "auto", we will make opinionated choices based on request'
398 + 'contents and what the backend libraries currently support, so '
399 + 'the behavior is subject to change in each release. '
400 + 'The default is xgrammar.')
401 parser.add_argument(
402 '--logits-processor-pattern',
403 type=nullable_str,
@@ -1539,9 +1536,9 @@ class EngineArgs:
1536 recommend_to_remove=False)
1537 return False
1538
1542 - # Only support Xgrammar for guided decoding so far.
1539 + # Xgrammar and Guidance are supported.
1540 SUPPORTED_GUIDED_DECODING = [
1544 - "xgrammar", "xgrammar:disable-any-whitespace"
1541 + "xgrammar", "xgrammar:disable-any-whitespace", "guidance", "auto"
1542 ]
1543 if self.guided_decoding_backend not in SUPPORTED_GUIDED_DECODING:
1544 _raise_or_fallback(feature_name="--guided-decoding-backend",
vllm/v1/engine/processor.py
+32 -7
@@ -4,7 +4,6 @@ import time
4 from collections.abc import Mapping
5 from typing import Optional, Union
6
7 -import vllm.platforms
7 from vllm.config import VllmConfig
8 from vllm.inputs import (INPUT_REGISTRY, InputRegistry, ProcessorInputs,
9 PromptType, SingletonInputsAdapter)
@@ -20,7 +19,10 @@ from vllm.prompt_adapter.request import PromptAdapterRequest
19 from vllm.sampling_params import SamplingParams
20 from vllm.transformers_utils.tokenizer_group import BaseTokenizerGroup
21 from vllm.v1.engine import EngineCoreRequest
23 -from vllm.v1.structured_output.utils import validate_structured_output_request
22 +from vllm.v1.structured_output.backend_guidance import (
23 + validate_guidance_grammar)
24 +from vllm.v1.structured_output.utils import (
25 + validate_structured_output_request_xgrammar)
26
27
28 class Processor:
@@ -120,7 +122,9 @@ class Processor:
122 if not params.guided_decoding or not self.decoding_config:
123 return
124
123 - supported_backends = ["xgrammar", "xgrammar:disable-any-whitespace"]
125 + supported_backends = [
126 + "xgrammar", "xgrammar:disable-any-whitespace", "guidance", "auto"
127 + ]
128 engine_level_backend = self.decoding_config.guided_decoding_backend
129 if engine_level_backend not in supported_backends:
130 raise ValueError(f"Only {supported_backends} structured output is "
@@ -134,10 +138,31 @@ class Processor:
138 else:
139 params.guided_decoding.backend = engine_level_backend
140
137 - if vllm.platforms.current_platform.is_tpu():
138 - raise ValueError("Structured output is not supported on TPU.")
139 -
140 - validate_structured_output_request(params)
141 + # Request content validation
142 +
143 + if engine_level_backend == "xgrammar":
144 + # xgrammar with no fallback
145 + validate_structured_output_request_xgrammar(params)
146 + params.guided_decoding.backend = "xgrammar"
147 + elif engine_level_backend == "auto":
148 + # "auto" is an opt-in to opinionated behavior where we try to
149 + # choose a backend based on request contents. This is not the
150 + # default as it is less predictable and subject to change
151 + # between releases as feature support changes.
152 + try:
153 + validate_structured_output_request_xgrammar(params)
154 + params.guided_decoding.backend = "xgrammar"
155 + except ValueError:
156 + # The request includes some jsonschema feature(s) that
157 + # are not supported in xgrammar. Fall back to guidance.
158 + params.guided_decoding.backend = "guidance"
159 +
160 + if params.guided_decoding.backend == "guidance":
161 + # TODO ideally we would have the LLTokenizer here as Lark syntax
162 + # allows <|special_token|> and similar, see
163 + # https://github.com/guidance-ai/llguidance/blob/main/docs/syntax.md#special-tokens
164 + # Without tokenizer these are disallowed in grammars.
165 + validate_guidance_grammar(params, tokenizer=None)
166
167 def process_inputs(
168 self,
vllm/v1/structured_output/__init__.py
+3
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Optional
7
8 from vllm.config import VllmConfig
9 from vllm.logger import init_logger
10 +from vllm.v1.structured_output.backend_guidance import GuidanceBackend
11 from vllm.v1.structured_output.backend_types import (StructuredOutputBackend,
12 StructuredOutputGrammar)
13
@@ -50,6 +51,8 @@ class StructuredOutputManager:
51 XgrammarBackend)
52
53 self.backend = XgrammarBackend(self.vllm_config)
54 + elif backend_name == "guidance":
55 + self.backend = GuidanceBackend(self.vllm_config)
56 else:
57 raise ValueError(
58 f"Unsupported structured output backend: {backend_name}")
vllm/v1/structured_output/backend_guidance.py new
+164
@@ -0,0 +1,164 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +
3 +import os
4 +from dataclasses import dataclass
5 +from typing import TYPE_CHECKING, Optional
6 +
7 +import torch
8 +
9 +from vllm.config import VllmConfig
10 +from vllm.logger import init_logger
11 +from vllm.sampling_params import SamplingParams
12 +from vllm.transformers_utils.tokenizer_group import init_tokenizer_from_configs
13 +from vllm.utils import LazyLoader
14 +from vllm.v1.structured_output.backend_types import (StructuredOutputBackend,
15 + StructuredOutputGrammar,
16 + StructuredOutputOptions)
17 +from vllm.v1.structured_output.request import get_structured_output_key
18 +
19 +if TYPE_CHECKING:
20 + import llguidance
21 + import llguidance.hf as llguidance_hf
22 + import llguidance.torch as llguidance_torch
23 +else:
24 + llguidance = LazyLoader("llguidance", globals(), "llguidance")
25 + llguidance_hf = LazyLoader("llguidance.hf", globals(), "llguidance.hf")
26 + llguidance_torch = LazyLoader("llguidance.torch", globals(),
27 + "llguidance.torch")
28 +
29 +logger = init_logger(__name__)
30 +
31 +
32 +class GuidanceBackend(StructuredOutputBackend):
33 +
34 + def __init__(self, vllm_config: VllmConfig):
35 + self.vllm_config = vllm_config
36 + tokenizer_group = init_tokenizer_from_configs(
37 + model_config=vllm_config.model_config,
38 + scheduler_config=vllm_config.scheduler_config,
39 + parallel_config=vllm_config.parallel_config,
40 + lora_config=vllm_config.lora_config) # type: ignore[arg-type]
41 + tokenizer_group.ping()
42 + self.vllm_config = vllm_config
43 + self.vocab_size = vllm_config.model_config.get_vocab_size()
44 +
45 + tokenizer = tokenizer_group.get_lora_tokenizer(None)
46 + self.ll_tokenizer = llguidance_hf.from_tokenizer(tokenizer, None)
47 +
48 + def compile_grammar(self, request_type: StructuredOutputOptions,
49 + grammar_spec: str) -> StructuredOutputGrammar:
50 + self.serialized_grammar = serialize_guidance_grammar(
51 + request_type, grammar_spec)
52 +
53 + ll_matcher = llguidance.LLMatcher(
54 + self.ll_tokenizer,
55 + self.serialized_grammar,
56 + log_level=int(os.environ.get("LLGUIDANCE_LOG_LEVEL", "1")),
57 + )
58 +
59 + r = GuidanceGrammar(
60 + ll_matcher=ll_matcher,
61 + ll_tokenizer=self.ll_tokenizer,
62 + vocab_size=self.vocab_size,
63 + )
64 +
65 + r.check_error()
66 + return r
67 +
68 + def allocate_token_bitmask(self, max_num_seqs: int):
69 + return llguidance_torch.allocate_token_bitmask(
70 + max_num_seqs, self.ll_tokenizer.vocab_size)
71 +
72 +
73 +@dataclass
74 +class GuidanceGrammar(StructuredOutputGrammar):
75 + ll_matcher: llguidance.LLMatcher
76 + ll_tokenizer: llguidance.LLTokenizer
77 + vocab_size: int
78 + printed_error: bool = False
79 + terminated: bool = False
80 +
81 + def check_error(self):
82 + if not self.printed_error:
83 + err = self.ll_matcher.get_error()
84 + if err:
85 + self.printed_error = True
86 + logger.warning("LLMatcher error: %s", err)
87 +
88 + def accept_tokens(self, request_id: str, tokens: list[int]) -> bool:
89 + """Accepts a list of tokens and advances the parser.
90 +
91 + Returns True if the parser was advanced successfully.
92 + Returns False if the parser failed to advance.
93 + """
94 +
95 + if self.ll_tokenizer.eos_token in tokens:
96 + self.terminated = True
97 +
98 + if self.ll_matcher.is_stopped():
99 + return True
100 +
101 + # TODO - Add jump decoding support in the future:
102 + # self.ll_matcher.compute_ff_bytes() - this should always work
103 + # self.ll_matcher.compute_ff_tokens() - this only works for
104 + # "canonical" tokenizers
105 + # For conversion between the two, see
106 + # https://github.com/guidance-ai/llguidance/blob/main/docs/fast_forward.md
107 +
108 + r = self.ll_matcher.consume_tokens(tokens)
109 +
110 + self.check_error()
111 +
112 + return r
113 +
114 + def fill_bitmask(self, bitmask: torch.Tensor, idx: int) -> None:
115 + # this will automatically return [EOS] mask if the matcher is stopped
116 + # or otherwise in an error state
117 + llguidance_torch.fill_next_token_bitmask(self.ll_matcher, bitmask, idx)
118 + self.check_error()
119 +
120 + def is_terminated(self) -> bool:
121 + return self.terminated
122 +
123 + def reset(self):
124 + # This method may be not needed anymore? TODO
125 + self.ll_matcher.reset()
126 +
127 +
128 +def serialize_guidance_grammar(request_type: StructuredOutputOptions,
129 + grammar_spec: str) -> str:
130 + if request_type == StructuredOutputOptions.JSON:
131 + # TODO: make whitespace_flexible configurable
132 + return llguidance.LLMatcher.grammar_from_json_schema(
133 + grammar_spec, defaults={
134 + "whitespace_flexible": True,
135 + })
136 + elif request_type == StructuredOutputOptions.JSON_OBJECT:
137 + return llguidance.LLMatcher.grammar_from_json_schema(
138 + '{"type": "object"}', defaults={
139 + "whitespace_flexible": True,
140 + })
141 + else:
142 + if request_type == StructuredOutputOptions.REGEX:
143 + tp = "regex"
144 + elif request_type == StructuredOutputOptions.GRAMMAR:
145 + tp = "grammar"
146 + elif request_type == StructuredOutputOptions.CHOICE:
147 + tp = "choice"
148 + else:
149 + logger.error("Validation should have already occurred. "
150 + "Please file an issue.")
151 + raise ValueError("grammar is not of valid supported types. "
152 + f"({request_type!s})")
153 + return llguidance.grammar_from(tp, grammar_spec)
154 +
155 +
156 +def validate_guidance_grammar(
157 + sampling_params: SamplingParams,
158 + tokenizer: Optional[llguidance.LLTokenizer] = None) -> None:
159 + tp, grm = get_structured_output_key(sampling_params)
160 + guidance_grm = serialize_guidance_grammar(tp, grm)
161 + err = llguidance.LLMatcher.validate_grammar(guidance_grm,
162 + tokenizer=tokenizer)
163 + if err:
164 + raise ValueError(f"Grammar error: {err}")
vllm/v1/structured_output/request.py
+26 -21
@@ -53,25 +53,30 @@ class StructuredOutputRequest:
53
54 @functools.cached_property
55 def structured_output_key(self) -> StructuredOutputKey:
56 - params = self.sampling_params.guided_decoding
57 - assert params is not None, "params can't be None."
58 - if params.json is not None:
59 - if not isinstance(params.json, str):
60 - json_str = json.dumps(params.json)
61 - else:
62 - json_str = params.json
63 - return (StructuredOutputOptions.JSON, json_str)
64 - elif params.json_object:
65 - return (StructuredOutputOptions.JSON_OBJECT, "")
66 - elif params.regex is not None:
67 - return (StructuredOutputOptions.REGEX, params.regex)
68 - elif params.choice is not None:
69 - if not isinstance(params.choice, str):
70 - json_str = json.dumps(params.choice)
71 - else:
72 - json_str = params.choice
73 - return (StructuredOutputOptions.CHOICE, json_str)
74 - elif params.grammar is not None:
75 - return (StructuredOutputOptions.GRAMMAR, params.grammar)
56 + return get_structured_output_key(self.sampling_params)
57 +
58 +
59 +def get_structured_output_key(
60 + sampling_params: SamplingParams) -> StructuredOutputKey:
61 + params = sampling_params.guided_decoding
62 + assert params is not None, "params can't be None."
63 + if params.json is not None:
64 + if not isinstance(params.json, str):
65 + json_str = json.dumps(params.json)
66 + else:
67 + json_str = params.json
68 + return (StructuredOutputOptions.JSON, json_str)
69 + elif params.json_object:
70 + return (StructuredOutputOptions.JSON_OBJECT, "")
71 + elif params.regex is not None:
72 + return (StructuredOutputOptions.REGEX, params.regex)
73 + elif params.choice is not None:
74 + if not isinstance(params.choice, str):
75 + json_str = json.dumps(params.choice)
76 else:
77 - raise ValueError("No valid structured output parameter found")
77 + json_str = params.choice
78 + return (StructuredOutputOptions.CHOICE, json_str)
79 + elif params.grammar is not None:
80 + return (StructuredOutputOptions.GRAMMAR, params.grammar)
81 + else:
82 + raise ValueError("No valid structured output parameter found")
vllm/v1/structured_output/utils.py
+1 -1
@@ -239,7 +239,7 @@ def choice_as_grammar(choice: list[str]) -> str:
239 return grammar
240
241
242 -def validate_structured_output_request(
242 +def validate_structured_output_request_xgrammar(
243 sampling_params: SamplingParams) -> None:
244 """Validate that the request is supported by structured output.
245