Skip to content

Commit 1908465

Browse files
[Feature] mm and thinking model support structred output (PaddlePaddle#2749)
* mm support structured output * update code * update code * update format * update code * update code * add enable_thinking default * update code * add structured_outputs test case * add ci install xgrammar * add ci timeout time * update test for structured_outputs * update code * add error traceback info * update error msg * update structred output code * update code * update code * update config * update torch version --------- Co-authored-by: Jiang-Jia-Jun <163579578+Jiang-Jia-Jun@users.noreply.github.com>
1 parent 0e4df5a commit 1908465

17 files changed

Lines changed: 1168 additions & 83 deletions

File tree

docs/features/structured_outputs.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,65 @@ ParsedChatCompletionMessage[Info](content='{"addr": "No.1 Century Avenue, Pudong
330330
Address: No.1 Century Avenue, Pudong New Area, Shanghai
331331
Height: 468
332332
```
333+
334+
### Offline Inference
335+
336+
Offline inference allows restricting the model's output format by pre-specified constraints. In `FastDeploy`, constraints can be specified through the `GuidedDecodingParams` class in `SamplingParams`. `GuidedDecodingParams` supports the following constraint types, with usage similar to online inference:
337+
338+
```python
339+
json: Optional[Union[str, dict]] = None
340+
regex: Optional[str] = None
341+
choice: Optional[List[str]] = None
342+
grammar: Optional[str] = None
343+
json_object: Optional[bool] = None
344+
structural_tag: Optional[str] = None
345+
```
346+
347+
The following example demonstrates how to use offline inference to generate a structured json:
348+
349+
```python
350+
from fastdeploy import LLM, SamplingParams
351+
from fastdeploy.engine.sampling_params import GuidedDecodingParams
352+
from pydantic import BaseModel
353+
from enum import Enum
354+
355+
class BookType(str, Enum):
356+
romance = "Romance"
357+
historical = "Historical"
358+
adventure = "Adventure"
359+
mystery = "Mystery"
360+
dystopian = "Dystopian"
361+
362+
class BookDescription(BaseModel):
363+
author: str
364+
title: str
365+
genre: BookType
366+
367+
# Constrained decoding parameters
368+
guided_decoding_params = GuidedDecodingParams(json=BookDescription.model_json_schema())
369+
370+
# Sampling parameters
371+
sampling_params = SamplingParams(
372+
top_p=0.95,
373+
max_tokens=6400,
374+
guided_decoding=guided_decoding_params,
375+
)
376+
377+
# Load model
378+
llm = LLM(model="ERNIE-4.5-0.3B", tensor_parallel_size=1, max_model_len=8192, guided_decoding_backend="auto")
379+
380+
outputs = llm.generate(
381+
prompts="Generate a JSON describing a literary work, including author, title and book type.",
382+
sampling_params=sampling_params,
383+
)
384+
385+
# Output results
386+
for output in outputs:
387+
print(output.outputs.text)
388+
```
389+
390+
Output:
391+
392+
```
393+
{"author": "George Orwell", "title": "1984", "genre": "Dystopian"}
394+
```

docs/zh/features/structured_outputs.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,67 @@ ParsedChatCompletionMessage[Info](content='{"addr": "上海市浦东新区世纪
330330
地址: 上海市浦东新区世纪大道1号
331331
高度: 468
332332
```
333+
334+
### 离线推理
335+
336+
离线推理允许通过预先指定约束条件,限制模型输出格式。在 `FastDeploy` 中,支持通过 `SamplingParams` 中的 `GuidedDecodingParams` 类指定相关约束条件。`GuidedDecodingParams` 支持以下几种约束条件,使用方式可以参考在线推理:
337+
338+
```python
339+
json: Optional[Union[str, dict]] = None
340+
regex: Optional[str] = None
341+
choice: Optional[List[str]] = None
342+
grammar: Optional[str] = None
343+
json_object: Optional[bool] = None
344+
structural_tag: Optional[str] = None
345+
```
346+
347+
以下示例展示了如何使用离线推理生成一个结构化的 json :
348+
349+
```python
350+
351+
from fastdeploy import LLM, SamplingParams
352+
from fastdeploy.engine.sampling_params import GuidedDecodingParams
353+
from pydantic import BaseModel
354+
from enum import Enum
355+
356+
class BookType(str, Enum):
357+
romance = "Romance"
358+
historical = "Historical"
359+
adventure = "Adventure"
360+
mystery = "Mystery"
361+
dystopian = "Dystopian"
362+
363+
class BookDescription(BaseModel):
364+
author: str
365+
title: str
366+
genre: BookType
367+
368+
# Constrained decoding parameters
369+
guided_decoding_params = GuidedDecodingParams(json=BookDescription.model_json_schema())
370+
371+
# Sampling parameters
372+
sampling_params = SamplingParams(
373+
top_p=0.95,
374+
max_tokens=6400,
375+
guided_decoding=guided_decoding_params,
376+
)
377+
378+
# Load model
379+
llm = LLM(model="ERNIE-4.5-0.3B", tensor_parallel_size=1, max_model_len=8192, guided_decoding_backend="auto")
380+
381+
outputs = llm.generate(
382+
prompts="生成一个JSON,描述一本中国的著作,要包含作者、标题和书籍类型。",
383+
sampling_params=sampling_params,
384+
)
385+
386+
# Output results
387+
for output in outputs:
388+
print(output.outputs.text)
389+
390+
```
391+
392+
输出
393+
394+
```
395+
{"author": "曹雪芹", "title": "红楼梦", "genre": "Historical"}
396+
```

fastdeploy/config.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,13 @@ def __init__(
127127
self.redundant_experts_num = 0
128128
self.seed = 0
129129
self.quantization = None
130+
self.reasoning_parser = None
130131
self.pad_token_id: int = -1
131132
self.eos_tokens_lens: int = 2
132133
self.lm_head_fp32: bool = False
133134
self.model_format = "auto"
134135
for key, value in args.items():
135-
if hasattr(self, key):
136+
if hasattr(self, key) and value != "None":
136137
setattr(self, key, value)
137138

138139
assert self.model != ""
@@ -1249,7 +1250,8 @@ def postprocess(self):
12491250
self.cache_config.max_block_num_per_seq = int(self.max_model_len // self.cache_config.block_size)
12501251

12511252
if self.guided_decoding_backend == "auto":
1252-
if self.model_config.enable_mm:
1253+
if current_platform.is_xpu() or self.speculative_config.method is not None:
1254+
logger.warning("Speculative Decoding and XPU currently do not support Guided decoding, set off.")
12531255
self.guided_decoding_backend = "off"
12541256
else:
12551257
self.guided_decoding_backend = "xgrammar"
@@ -1319,12 +1321,10 @@ def check(self):
13191321
], f"Only support xgrammar、auto guided decoding backend, but got {self.guided_decoding_backend}."
13201322

13211323
if self.guided_decoding_backend != "off":
1322-
# TODO: mm support guided_decoding
1323-
assert (
1324-
self.model_config.enable_mm is False
1325-
), "Multimodal model currently do not support guided_decoding"
1326-
13271324
# TODO: speculative decoding support guided_decoding
1325+
assert (
1326+
self.speculative_config.method is None
1327+
), "speculative decoding currently do not support guided_decoding"
13281328

13291329
# TODO: xpu support guided_decoding
13301330
assert not current_platform.is_xpu(), "XPU currently do not support guided_decoding"
@@ -1335,6 +1335,7 @@ def check(self):
13351335
raise Exception(
13361336
f"import XGrammar failed, please install XGrammar use `pip install xgrammar==0.1.19`. \n\t {e}"
13371337
)
1338+
13381339
if self.scheduler_config is not None:
13391340
self.scheduler_config.check()
13401341

fastdeploy/engine/engine.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,22 @@ def _get_generated_result(self):
178178

179179
# _insert_task_to_worker moved to CommonEngine
180180

181+
def _has_guided_input(self, request):
182+
"""
183+
Check if the request has any guided input.
184+
"""
185+
return any(
186+
x is not None
187+
for x in (
188+
request.guided_json,
189+
request.guided_regex,
190+
request.guided_choice,
191+
request.structural_tag,
192+
request.guided_grammar,
193+
request.guided_json_object,
194+
)
195+
)
196+
181197
def add_requests(self, task, sampling_params=None, **kwargs):
182198
"""
183199
Add a new request to the queue.
@@ -249,8 +265,15 @@ def add_requests(self, task, sampling_params=None, **kwargs):
249265
llm_logger.error(error_msg)
250266
raise EngineError(error_msg, error_code=400)
251267

252-
if self.engine.guided_decoding_checker is not None:
253-
request, err_msg = self.engine.guided_decoding_checker.schema_format(request)
268+
if self._has_guided_input(request):
269+
err_msg = None
270+
if self.guided_decoding_checker is None:
271+
err_msg = (
272+
"guided_backend is None, use --guided-decoding-backend to specify the backend at server startup."
273+
)
274+
else:
275+
request, err_msg = self.guided_decoding_checker.schema_format(request)
276+
254277
if err_msg is not None:
255278
llm_logger.error(err_msg)
256279
raise EngineError(err_msg, error_code=400)
@@ -469,6 +492,7 @@ def _start_worker_service(self):
469492
f" --guided_decoding_backend {self.cfg.guided_decoding_backend}"
470493
f" --load_strategy {self.cfg.load_config.load_strategy}"
471494
f" --early_stop_config '{self.cfg.early_stop_config.to_json_string()}'"
495+
f" --reasoning_parser {self.cfg.reasoning_parser}"
472496
f" --load_choices {self.cfg.load_config.load_choices}"
473497
f" --moba_attention_config '{self.cfg.moba_attention_config.to_json_string()}'"
474498
f" --ips {ips}"

fastdeploy/engine/request.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,11 @@ def set(self, key, value):
263263
setattr(self, key, value)
264264

265265
def __repr__(self) -> str:
266-
return (
267-
f"Request(request_id={self.request_id}, "
268-
f"prompt={self.prompt!r}, "
269-
f"prompt_token_ids={self.prompt_token_ids}, "
270-
f"draft_token_ids={self.draft_token_ids}, "
271-
f"sampling_params={self.sampling_params})"
272-
)
266+
non_none_fields = []
267+
for attr, value in vars(self).items():
268+
if value is not None and not attr.startswith("_"):
269+
non_none_fields.append(f"{attr}={value!r}")
270+
return f"Request({', '.join(non_none_fields)})"
273271

274272

275273
@dataclass(slots=True)

fastdeploy/engine/sampling_params.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class SamplingParams:
100100
temp_scaled_logprobs: bool = False
101101
top_p_normalized_logprobs: bool = False
102102
bad_words: Optional[List[str]] = None
103+
guided_decoding: Optional[GuidedDecodingParams] = None
103104
bad_words_token_ids: Optional[List[int]] = None
104105

105106
@classmethod
@@ -132,6 +133,7 @@ def from_optional(
132133
min_tokens=1,
133134
logprobs=None,
134135
bad_words=None,
136+
guided_decoding=None,
135137
bad_words_token_ids=None,
136138
) -> SamplingParams:
137139
"""Create instance from command line arguments"""
@@ -153,6 +155,7 @@ def from_optional(
153155
min_tokens=min_tokens,
154156
logprobs=logprobs,
155157
bad_words=bad_words,
158+
guided_decoding=guided_decoding,
156159
bad_words_token_ids=bad_words_token_ids,
157160
)
158161

@@ -217,3 +220,51 @@ class BeamSearchParams:
217220
temperature: float = 0.0
218221
length_penalty: float = 1.0
219222
include_stop_str_in_output: bool = False
223+
224+
225+
@dataclass
226+
class GuidedDecodingParams:
227+
"""Guided decoding parameters for text generation."""
228+
229+
json: Optional[Union[str, dict]] = None
230+
regex: Optional[str] = None
231+
choice: Optional[List[str]] = None
232+
grammar: Optional[str] = None
233+
json_object: Optional[bool] = None
234+
structural_tag: Optional[str] = None
235+
236+
def to_dict(self):
237+
"""convert to dict"""
238+
key_dict = {
239+
"guided_json": self.json,
240+
"guided_regex": self.regex,
241+
"guided_choice": self.choice,
242+
"guided_grammar": self.grammar,
243+
"structural_tag": self.structural_tag,
244+
"guided_json_object": self.json_object,
245+
}
246+
247+
guided_dict = {}
248+
for key, value in key_dict.items():
249+
if value is not None:
250+
guided_dict[key] = value
251+
return guided_dict
252+
253+
def __post_init__(self):
254+
"""Verify the arguments."""
255+
guided_count = sum(
256+
[
257+
self.json is not None,
258+
self.regex is not None,
259+
self.choice is not None,
260+
self.grammar is not None,
261+
self.json_object is not None,
262+
self.structural_tag is not None,
263+
]
264+
)
265+
266+
if guided_count > 1:
267+
raise ValueError(
268+
"You can only use one kind of guided decoding "
269+
"('json', 'json_object', 'regex', 'choice', 'grammar', 'structural_tag')."
270+
)

fastdeploy/entrypoints/llm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ def _add_request(
295295
current_sampling_params = sampling_params[i]
296296
else:
297297
current_sampling_params = sampling_params
298+
if current_sampling_params.guided_decoding is not None:
299+
guided_decoding_dict = current_sampling_params.guided_decoding.to_dict()
300+
tasks.update(guided_decoding_dict)
298301
self.llm_engine.add_requests(tasks, current_sampling_params, **kwargs)
299302
return req_ids
300303

fastdeploy/model_executor/guided_decoding/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
"""
1616

1717
# from fastdeploy.config import FDConfig
18+
from fastdeploy.model_executor.guided_decoding.base_guided_decoding import (
19+
BackendBase,
20+
BaseChecker,
21+
LogitsProcessorBase,
22+
)
1823

19-
__all__ = ["get_guided_backend", "schema_checker"]
24+
__all__ = ["get_guided_backend", "schema_checker", "LogitsProcessorBase", "BackendBase", "BaseChecker"]
2025

2126

2227
def get_guided_backend(

0 commit comments

Comments
 (0)