Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cd3b3bb
first pass for JSON and regex
felixzhu555 Feb 8, 2024
e589bd0
tiny refactor
felixzhu555 Feb 8, 2024
5f55e6a
Added support for guided decoding in `api_server` by integrating _out…
br3no Feb 8, 2024
3a051cf
refactor/combine breno's PR with mine
felixzhu555 Feb 9, 2024
54217ba
fix type check
felixzhu555 Feb 9, 2024
c9c6f4f
fix try-except
felixzhu555 Feb 9, 2024
b82dedb
fix import bug
felixzhu555 Feb 9, 2024
ba92cb2
add outlines v0.0.27 requirement
felixzhu555 Feb 9, 2024
da2f5b8
fix dummy_llm
felixzhu555 Feb 10, 2024
b090c18
start adding tests
felixzhu555 Feb 11, 2024
9093c5e
add more tests
felixzhu555 Feb 11, 2024
1efd64d
fix pytest fixtures scope
felixzhu555 Feb 13, 2024
736ca31
remove guided decoding from vllm api server
felixzhu555 Feb 13, 2024
4d1b049
refactor + add guided_choice
felixzhu555 Feb 14, 2024
a46684e
add caching for logit processors
felixzhu555 Feb 14, 2024
058fce6
use re.escape
felixzhu555 Feb 14, 2024
dc601c7
revert logits processor 2 vs 3 arg fix
felixzhu555 Feb 14, 2024
09d2a9c
copy on cache hit
felixzhu555 Feb 14, 2024
d774cf6
add separate thread for creating logits processor
felixzhu555 Feb 15, 2024
782b1da
add simple cache test
felixzhu555 Feb 15, 2024
df3c774
use asyncio
felixzhu555 Feb 16, 2024
c74f6bb
add grammar support
felixzhu555 Feb 18, 2024
cf8494d
remove grammar
felixzhu555 Feb 21, 2024
c30fed0
copy outlines' logits processors code
felixzhu555 Feb 21, 2024
33dc082
breno PR comments
felixzhu555 Feb 22, 2024
8699039
resolve PR comments
felixzhu555 Feb 22, 2024
6bf277e
add unit test for logits processors
felixzhu555 Feb 24, 2024
b45942e
PR comments
felixzhu555 Feb 27, 2024
3169b63
Merge branch 'main' of github.com:vllm-project/vllm into add_structur…
felixzhu555 Feb 27, 2024
c176dab
fix
felixzhu555 Feb 27, 2024
bfbbce3
format with yapf and ruff
felixzhu555 Feb 28, 2024
9655b6e
fix global pool
simon-mo Feb 28, 2024
0c3d475
Apply suggestions from code review
simon-mo Feb 28, 2024
0793fc7
Merge branch 'main' of github.com:vllm-project/vllm into add_structur…
simon-mo Feb 29, 2024
ce9c07a
some minor fix
simon-mo Feb 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ pydantic >= 2.0 # Required for OpenAI server.
aioprometheus[starlette]
pynvml == 11.5.0
triton >= 2.1.0
outlines >= 0.0.27
258 changes: 258 additions & 0 deletions tests/entrypoints/test_openai_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,69 @@
import ray # using Ray for overall ease of process management, parallel requests, and debugging.
import openai # use the official client for correctness check

# imports for guided decoding tests
import json
import jsonschema
import re

MAX_SERVER_START_WAIT_S = 600 # wait for server to start for 60 seconds
MODEL_NAME = "HuggingFaceH4/zephyr-7b-beta" # any model with a chat template should work here

TEST_SCHEMA = {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
},
"skills": {
"type": "array",
"items": {
"type": "string",
"maxLength": 10
},
"minItems": 3
},
"work history": {
"type": "array",
"items": {
"type": "object",
"properties": {
"company": {
"type": "string"
},
"duration": {
"type": "string"
},
"position": {
"type": "string"
}
},
"required": [
"company",
"position"
]
}
}
},
"required": [
"name",
"age",
"skills",
"work history"
]
}

# NOTE: outlines' underlying regex library (interegular) doesn't support
# ^ or $ or \b, kinda annoying
Comment thread
felixzhu555 marked this conversation as resolved.
Outdated
TEST_REGEX = r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.){3}" + \
r"(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)"

TEST_CHOICE = ["Python", "Java", "JavaScript", "C++", "C#",
"PHP", "TypeScript", "Ruby", "Swift", "Kotlin"]

pytestmark = pytest.mark.asyncio


Expand Down Expand Up @@ -250,5 +310,203 @@ async def test_batch_completions(server, client: openai.AsyncOpenAI):
assert texts[0] == texts[1]


async def test_guided_json_completion(server, client: openai.AsyncOpenAI):
completion = await client.completions.create(
model=MODEL_NAME,
prompt=f"Give an example JSON for an employee profile that fits this schema: {TEST_SCHEMA}",
n=3,
temperature=1.0,
max_tokens=500,
extra_body=dict(
guided_json=TEST_SCHEMA
)
)

assert completion.id is not None
assert completion.choices is not None and len(completion.choices) == 3
for i in range(3):
assert completion.choices[i].text is not None
output_json = json.loads(completion.choices[i].text)
jsonschema.validate(instance=output_json, schema=TEST_SCHEMA)


async def test_guided_json_chat(server, client: openai.AsyncOpenAI):
messages = [{
"role": "system",
"content": "you are a helpful assistant"
}, {
"role": "user",
"content": "Give an example JSON for an employee profile that " + \
f"fits this schema: {TEST_SCHEMA}"
}]
chat_completion = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
max_tokens=500,
extra_body=dict(
guided_json=TEST_SCHEMA
)
)
message = chat_completion.choices[0].message
assert message.content is not None
json1 = json.loads(message.content)
jsonschema.validate(instance=json1, schema=TEST_SCHEMA)

messages.append({"role": "assistant", "content": message.content})
messages.append({
"role": "user",
"content": "Give me another one with a different name and age"
})
chat_completion = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
max_tokens=500,
extra_body=dict(
guided_json=TEST_SCHEMA
)
)
message = chat_completion.choices[0].message
assert message.content is not None
json2 = json.loads(message.content)
jsonschema.validate(instance=json2, schema=TEST_SCHEMA)
assert json1["name"] != json2["name"]
assert json1["age"] != json2["age"]


async def test_guided_regex_completion(server, client: openai.AsyncOpenAI):
completion = await client.completions.create(
model=MODEL_NAME,
prompt=f"Give an example IPv4 address with this regex: {TEST_REGEX}",
n=3,
temperature=1.0,
max_tokens=20,
extra_body=dict(
guided_regex=TEST_REGEX
)
)

assert completion.id is not None
assert completion.choices is not None and len(completion.choices) == 3
for i in range(3):
assert completion.choices[i].text is not None
assert re.fullmatch(TEST_REGEX, completion.choices[i].text) is not None


async def test_guided_regex_chat(server, client: openai.AsyncOpenAI):
messages = [{
"role": "system",
"content": "you are a helpful assistant"
}, {
"role": "user",
"content": f"Give an example IP address with this regex: {TEST_REGEX}"
}]
chat_completion = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
max_tokens=20,
extra_body=dict(
guided_regex=TEST_REGEX
)
)
ip1 = chat_completion.choices[0].message.content
assert ip1 is not None
assert re.fullmatch(TEST_REGEX, ip1) is not None

messages.append({"role": "assistant", "content": ip1})
messages.append({
"role": "user",
"content": "Give me a different one"
})
chat_completion = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
max_tokens=20,
extra_body=dict(
guided_regex=TEST_REGEX
)
)
ip2 = chat_completion.choices[0].message.content
assert ip2 is not None
assert re.fullmatch(TEST_REGEX, ip2) is not None
assert ip1 != ip2


async def test_guided_choice_completion(server, client: openai.AsyncOpenAI):
completion = await client.completions.create(
model=MODEL_NAME,
prompt="The best language for type-safe systems programming is ",
n=2,
temperature=1.0,
max_tokens=10,
extra_body=dict(
guided_choice=TEST_CHOICE
)
)

assert completion.id is not None
assert completion.choices is not None and len(completion.choices) == 2
for i in range(2):
assert completion.choices[i].text in TEST_CHOICE


async def test_guided_choice_chat(server, client: openai.AsyncOpenAI):
messages = [{
"role": "system",
"content": "you are a helpful assistant"
}, {
"role": "user",
"content": "The best language for type-safe systems programming is "
}]
chat_completion = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
max_tokens=10,
extra_body=dict(
guided_choice=TEST_CHOICE
)
)
choice1 = chat_completion.choices[0].message.content
assert choice1 in TEST_CHOICE

messages.append({"role": "assistant", "content": choice1})
messages.append({
"role": "user",
"content": "I disagree, pick another one"
})
chat_completion = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
max_tokens=10,
extra_body=dict(
guided_choice=TEST_CHOICE
)
)
choice2 = chat_completion.choices[0].message.content
assert choice2 in TEST_CHOICE
assert choice1 != choice2


async def test_guided_decoding_type_error(server, client: openai.AsyncOpenAI):
with pytest.raises(Exception):
_ = await client.completions.create(
model=MODEL_NAME,
prompt="Give an example JSON that fits this schema: 42",
temperature=0.0,
extra_body=dict(
guided_json=42
)
)

with pytest.raises(Exception):
_ = await client.completions.create(
model=MODEL_NAME,
prompt="Give an example string that fits this regex: True",
temperature=0.0,
extra_body=dict(
guided_regex=True
)
)
Comment thread
felixzhu555 marked this conversation as resolved.
Outdated


if __name__ == "__main__":
pytest.main([__file__])
1 change: 1 addition & 0 deletions tests/samplers/test_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def test_sampler_logits_processors(seed: int, device: str):
# This sample logits processor gives infinite score to the i-th token,
# where i is the length of the input sequence.
# We therefore expect the output token sequence to be [0, 1, 2, ...]
# Since this processor is stateless, the seq_id is not used
Comment thread
felixzhu555 marked this conversation as resolved.
Outdated
def pick_ith(token_ids, logits):
logits[len(token_ids)] = float("inf")
return logits
Expand Down
3 changes: 3 additions & 0 deletions vllm/engine/async_llm_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ def is_running(self) -> bool:
return (self.background_loop is not None
and not self.background_loop.done())

def get_tokenizer(self):
return self.engine.tokenizer.tokenizer

def start_background_loop(self) -> None:
"""Start the background loop."""
if self.is_running:
Expand Down
2 changes: 2 additions & 0 deletions vllm/entrypoints/api_server.py
Comment thread
felixzhu555 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async def generate(request: Request) -> Response:
prompt = request_dict.pop("prompt")
prefix_pos = request_dict.pop("prefix_pos", None)
stream = request_dict.pop("stream", False)

sampling_params = SamplingParams(**request_dict)
request_id = random_uuid()

Expand Down Expand Up @@ -83,6 +84,7 @@ async def stream_results() -> AsyncGenerator[bytes, None]:
type=str,
default=None,
help="FastAPI root_path when app is behind a path based routing proxy")

parser = AsyncEngineArgs.add_cli_args(parser)
args = parser.parse_args()

Expand Down
6 changes: 6 additions & 0 deletions vllm/entrypoints/openai/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ class ChatCompletionRequest(BaseModel):
min_p: Optional[float] = 0.0
include_stop_str_in_output: Optional[bool] = False
length_penalty: Optional[float] = 1.0
guided_json: Optional[Union[str, dict, BaseModel]] = None
guided_regex: Optional[str] = None
guided_choice: Optional[List[Union[str, int, float, bool]]] = None

def to_sampling_params(self) -> SamplingParams:
return SamplingParams(
Expand Down Expand Up @@ -133,6 +136,9 @@ class CompletionRequest(BaseModel):
min_p: Optional[float] = 0.0
include_stop_str_in_output: Optional[bool] = False
length_penalty: Optional[float] = 1.0
guided_json: Optional[Union[str, dict, BaseModel]] = None
guided_regex: Optional[str] = None
guided_choice: Optional[List[Union[str, int, float, bool]]] = None

def to_sampling_params(self):
echo_without_generation = self.echo and self.max_tokens == 0
Expand Down
4 changes: 4 additions & 0 deletions vllm/entrypoints/openai/serving_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
UsageInfo)
from vllm.outputs import RequestOutput
from vllm.entrypoints.openai.serving_engine import OpenAIServing
from vllm.model_executor.guided_decoding import get_guided_decoding_logits_processor

logger = init_logger(__name__)

Expand Down Expand Up @@ -64,6 +65,9 @@ async def create_chat_completion(
token_ids = self._validate_prompt_and_tokenize(request,
prompt=prompt)
sampling_params = request.to_sampling_params()
sampling_params.logits_processors = \
get_guided_decoding_logits_processor(
request, self.engine.get_tokenizer())
except ValueError as e:
return self.create_error_response(str(e))

Expand Down
4 changes: 4 additions & 0 deletions vllm/entrypoints/openai/serving_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from vllm.outputs import RequestOutput
from vllm.entrypoints.openai.serving_engine import OpenAIServing
from vllm.model_executor.guided_decoding import get_guided_decoding_logits_processor

logger = init_logger(__name__)

Expand Down Expand Up @@ -284,6 +285,9 @@ async def create_completion(self, request: CompletionRequest,
generators = []
try:
sampling_params = request.to_sampling_params()
sampling_params.logits_processors = \
get_guided_decoding_logits_processor(
request, self.engine.get_tokenizer())
prompt_is_tokens, prompts = parse_prompt_format(request.prompt)

for i, prompt in enumerate(prompts):
Expand Down
Loading