main
py 195 lines 6.78 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import ast
4 import json
5 from dataclasses import dataclass, field
6 from functools import lru_cache
7 from typing import TYPE_CHECKING
8
9 import torch
10 from transformers import PreTrainedTokenizerBase
11
12 from vllm.exceptions import VLLMValidationError
13 from vllm.sampling_params import SamplingParams
14 from vllm.utils.import_utils import LazyLoader
15 from vllm.utils.torch_utils import PIN_MEMORY
16 from vllm.v1.structured_output.backend_types import (
17 StructuredOutputBackend,
18 StructuredOutputGrammar,
19 StructuredOutputOptions,
20 )
21 from vllm.v1.structured_output.utils import compile_regex_with_timeout
22
23 if TYPE_CHECKING:
24 import lmformatenforcer
25 import lmformatenforcer.integrations.vllm as lmfe_vllm
26 else:
27 lmformatenforcer = LazyLoader("lmformatenforcer", globals(), "lmformatenforcer")
28 lmfe_vllm = LazyLoader(
29 "lmformatenforcer.integrations.vllm",
30 globals(),
31 "lmformatenforcer.integrations.vllm",
32 )
33
34
35 @lru_cache
36 def _cached_build_vllm_token_enforcer_tokenizer_data(
37 tokenizer: PreTrainedTokenizerBase, vocab_size: int
38 ) -> "lmfe_vllm.TokenEnforcerTokenizerData":
39 return lmfe_vllm.build_vllm_token_enforcer_tokenizer_data(
40 tokenizer, use_bitmask=True, vocab_size=vocab_size
41 )
42
43
44 @dataclass
45 class LMFormatEnforcerGrammar(StructuredOutputGrammar):
46 token_enforcer: lmformatenforcer.TokenEnforcer
47 current_tokens_prefix: list[int] = field(default_factory=list)
48
49 def accept_tokens(self, request_id: str, tokens: list[int]) -> bool:
50 original_len = len(self.current_tokens_prefix)
51 for token in tokens:
52 if not self.token_enforcer.get_allowed_tokens(
53 self.current_tokens_prefix
54 ).is_token_allowed(token):
55 # Rollback partial updates to ensure atomicity.
56 del self.current_tokens_prefix[original_len:]
57 return False
58 self.current_tokens_prefix.append(token)
59 return True
60
61 def validate_tokens(self, tokens: list[int]) -> list[int]:
62 for prefix_length in range(len(tokens)):
63 prefix = tokens[:prefix_length]
64 next_token = tokens[prefix_length]
65 if not self.token_enforcer.get_allowed_tokens(
66 self.current_tokens_prefix + prefix
67 ).is_token_allowed(next_token):
68 break
69 else:
70 return tokens
71
72 return tokens[:prefix_length]
73
74 def rollback(self, num_tokens: int) -> None:
75 self.current_tokens_prefix = self.current_tokens_prefix[:-num_tokens]
76
77 def fill_bitmask(self, bitmask: torch.Tensor, batch_index: int) -> None:
78 allowed_tokens = self.token_enforcer.get_allowed_tokens(
79 self.current_tokens_prefix
80 )
81 bitmask[batch_index] = allowed_tokens.allowed_tokens
82
83 def is_terminated(self) -> bool:
84 # We are considered terminated if the prefix ends with eos_token_id
85 return_value = (
86 len(self.current_tokens_prefix) > 0
87 and self.current_tokens_prefix[-1] == self.token_enforcer.eos_token_id
88 )
89 return return_value
90
91 def reset(self):
92 self.current_tokens_prefix = []
93
94
95 @dataclass
96 class LMFormatEnforcerBackend(StructuredOutputBackend):
97 def __post_init__(self):
98 self.tokenizer_data = _cached_build_vllm_token_enforcer_tokenizer_data(
99 self.tokenizer, self.vocab_size
100 )
101
102 def compile_grammar(
103 self,
104 request_type: StructuredOutputOptions,
105 grammar_spec: str,
106 stop_token_ids: set[int] | None = None,
107 ) -> StructuredOutputGrammar:
108 character_level_parser: lmformatenforcer.CharacterLevelParser
109 if request_type == StructuredOutputOptions.JSON:
110 spec_dict = json.loads(grammar_spec)
111 character_level_parser = lmformatenforcer.JsonSchemaParser(spec_dict)
112 elif request_type == StructuredOutputOptions.JSON_OBJECT:
113 character_level_parser = lmformatenforcer.JsonSchemaParser(None)
114 elif request_type == StructuredOutputOptions.REGEX:
115 character_level_parser = compile_regex_with_timeout(
116 lmformatenforcer.RegexParser,
117 grammar_spec,
118 )
119 elif request_type == StructuredOutputOptions.CHOICE:
120 choices = ast.literal_eval(grammar_spec)
121 character_level_parser = lmformatenforcer.UnionParser(
122 [lmformatenforcer.StringParser(choice) for choice in choices]
123 )
124 else:
125 raise ValueError(
126 f"Invalid request type for LM Format Enforcer backend({request_type!s})"
127 )
128 max_rollback_tokens = (
129 self.vllm_config.speculative_config.num_speculative_tokens
130 if self.vllm_config.speculative_config is not None
131 else 0
132 )
133
134 if max_rollback_tokens > 0:
135 raise ValueError(
136 "LM Format Enforcer backend does not support speculative tokens"
137 )
138
139 token_enforcer = lmformatenforcer.TokenEnforcer(
140 tokenizer_data=self.tokenizer_data,
141 parser=character_level_parser,
142 )
143 return LMFormatEnforcerGrammar(token_enforcer)
144
145 def allocate_token_bitmask(self, max_num_seqs: int) -> torch.Tensor:
146 return torch.full(
147 (max_num_seqs, (self.vocab_size + 31) // 32),
148 -1,
149 dtype=torch.int32,
150 pin_memory=PIN_MEMORY,
151 )
152
153 def destroy(self):
154 pass
155
156
157 def validate_structured_output_request_lm_format_enforcer(params: SamplingParams):
158 if params.structured_outputs is None:
159 return
160
161 so_params = params.structured_outputs
162
163 if so_params.regex:
164 try:
165 compile_regex_with_timeout(
166 lmformatenforcer.RegexParser,
167 so_params.regex,
168 )
169 except Exception as err:
170 raise VLLMValidationError(
171 f"Failed to compile regex for lm-format-enforcer: {err}"
172 ) from err
173 return
174 elif so_params.json:
175 if isinstance(so_params.json, str):
176 try:
177 # make sure schema is valid json
178 json.loads(so_params.json)
179 except json.JSONDecodeError as e:
180 raise VLLMValidationError("Invalid JSON grammar specification.") from e
181 else:
182 try:
183 json.dumps(so_params.json)
184 except Exception as e:
185 raise VLLMValidationError(
186 f"Error serializing structured outputs jsonschema: {e}"
187 ) from e
188 return
189 elif so_params.choice:
190 return
191 elif so_params.grammar:
192 raise VLLMValidationError(
193 "LM Format Enforcer structured outputs backend "
194 "does not support grammar specifications"
195 )