feature: adding openai compatible API request to bench_serving (#17219)
This commit is contained in:
@@ -228,17 +228,26 @@ async def async_request_openai_completions(
|
|||||||
prompt = request_func_input.prompt
|
prompt = request_func_input.prompt
|
||||||
|
|
||||||
async with _create_bench_client_session() as session:
|
async with _create_bench_client_session() as session:
|
||||||
|
# Build payload with defaults that can be overridden by extra_request_body
|
||||||
payload = {
|
payload = {
|
||||||
"model": request_func_input.model,
|
"model": request_func_input.model,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"temperature": 0.0,
|
|
||||||
"best_of": 1,
|
"best_of": 1,
|
||||||
"max_tokens": request_func_input.output_len,
|
"max_tokens": request_func_input.output_len,
|
||||||
"stream": not args.disable_stream,
|
"stream": not args.disable_stream,
|
||||||
"ignore_eos": not args.disable_ignore_eos,
|
|
||||||
**request_func_input.extra_request_body,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add temperature default only if not specified in extra_request_body
|
||||||
|
if "temperature" not in request_func_input.extra_request_body:
|
||||||
|
payload["temperature"] = 0.0
|
||||||
|
|
||||||
|
# Add ignore_eos default only if not specified in extra_request_body
|
||||||
|
if "ignore_eos" not in request_func_input.extra_request_body:
|
||||||
|
payload["ignore_eos"] = not args.disable_ignore_eos
|
||||||
|
|
||||||
|
# Merge in extra parameters - these will override defaults if present
|
||||||
|
payload.update(request_func_input.extra_request_body)
|
||||||
|
|
||||||
# hack to accommodate different LoRA conventions between SGLang and vLLM.
|
# hack to accommodate different LoRA conventions between SGLang and vLLM.
|
||||||
if request_func_input.lora_name:
|
if request_func_input.lora_name:
|
||||||
payload["model"] = request_func_input.lora_name
|
payload["model"] = request_func_input.lora_name
|
||||||
@@ -373,16 +382,27 @@ async def async_request_openai_chat_completions(
|
|||||||
messages = [{"role": "user", "content": request_func_input.prompt}]
|
messages = [{"role": "user", "content": request_func_input.prompt}]
|
||||||
|
|
||||||
async with _create_bench_client_session() as session:
|
async with _create_bench_client_session() as session:
|
||||||
|
# Build payload with defaults that can be overridden by extra_request_body
|
||||||
payload = {
|
payload = {
|
||||||
"model": request_func_input.model,
|
"model": request_func_input.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"temperature": 0.0,
|
|
||||||
"max_completion_tokens": request_func_input.output_len,
|
"max_completion_tokens": request_func_input.output_len,
|
||||||
"stream": not args.disable_stream,
|
"stream": not args.disable_stream,
|
||||||
"ignore_eos": not args.disable_ignore_eos,
|
|
||||||
**request_func_input.extra_request_body,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add temperature default only if not specified in extra_request_body
|
||||||
|
if "temperature" not in request_func_input.extra_request_body:
|
||||||
|
payload["temperature"] = 0.0
|
||||||
|
|
||||||
|
# Add ignore_eos default only if not specified in extra_request_body
|
||||||
|
# Default to False for more realistic behavior (respect EOS tokens)
|
||||||
|
if "ignore_eos" not in request_func_input.extra_request_body:
|
||||||
|
payload["ignore_eos"] = not args.disable_ignore_eos
|
||||||
|
|
||||||
|
# Merge in extra parameters (tools, temperature, top_p, etc.)
|
||||||
|
# These will override defaults if present
|
||||||
|
payload.update(request_func_input.extra_request_body)
|
||||||
|
|
||||||
# hack to accommodate different LoRA conventions between SGLang and vLLM.
|
# hack to accommodate different LoRA conventions between SGLang and vLLM.
|
||||||
if request_func_input.lora_name:
|
if request_func_input.lora_name:
|
||||||
payload["model"] = request_func_input.lora_name
|
payload["model"] = request_func_input.lora_name
|
||||||
@@ -895,6 +915,13 @@ def get_dataset(args, tokenizer, model_id=None):
|
|||||||
prompt_suffix=args.prompt_suffix,
|
prompt_suffix=args.prompt_suffix,
|
||||||
apply_chat_template=args.apply_chat_template,
|
apply_chat_template=args.apply_chat_template,
|
||||||
)
|
)
|
||||||
|
elif args.dataset_name == "openai":
|
||||||
|
input_requests = sample_openai_requests(
|
||||||
|
dataset_path=args.dataset_path,
|
||||||
|
num_requests=args.num_prompts,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
fixed_output_len=args.sharegpt_output_len,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown dataset: {args.dataset_name}")
|
raise ValueError(f"Unknown dataset: {args.dataset_name}")
|
||||||
return input_requests
|
return input_requests
|
||||||
@@ -1034,12 +1061,15 @@ class DatasetRow:
|
|||||||
image_data: Optional[List[str]] = None
|
image_data: Optional[List[str]] = None
|
||||||
timestamp: Optional[float] = None
|
timestamp: Optional[float] = None
|
||||||
routing_key: Optional[str] = None
|
routing_key: Optional[str] = None
|
||||||
|
extra_request_body: Optional[Dict[str, Any]] = None # Per-request API parameters
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.text_prompt_len is None:
|
if self.text_prompt_len is None:
|
||||||
self.text_prompt_len = self.prompt_len
|
self.text_prompt_len = self.prompt_len
|
||||||
if self.vision_prompt_len is None:
|
if self.vision_prompt_len is None:
|
||||||
self.vision_prompt_len = 0
|
self.vision_prompt_len = 0
|
||||||
|
if self.extra_request_body is None:
|
||||||
|
self.extra_request_body = {}
|
||||||
|
|
||||||
|
|
||||||
async def get_mooncake_request_over_time(
|
async def get_mooncake_request_over_time(
|
||||||
@@ -1300,6 +1330,85 @@ def sample_sharegpt_requests(
|
|||||||
return filtered_dataset
|
return filtered_dataset
|
||||||
|
|
||||||
|
|
||||||
|
def sample_openai_requests(
|
||||||
|
dataset_path: str,
|
||||||
|
num_requests: int,
|
||||||
|
tokenizer: PreTrainedTokenizerBase,
|
||||||
|
fixed_output_len: Optional[int] = None,
|
||||||
|
) -> List[DatasetRow]:
|
||||||
|
"""
|
||||||
|
Load OpenAI-compatible chat completion requests from a JSONL file.
|
||||||
|
|
||||||
|
Each line should be a JSON object with:
|
||||||
|
- "messages": list of {"role": str, "content": str}
|
||||||
|
- "max_tokens": int (used as output_len if fixed_output_len not set)
|
||||||
|
- "tools": optional list of tool definitions
|
||||||
|
- "temperature": optional temperature value
|
||||||
|
- "top_p": optional top_p value
|
||||||
|
- Other OpenAI API parameters are also extracted and passed through
|
||||||
|
"""
|
||||||
|
dataset = []
|
||||||
|
with open(dataset_path, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if num_requests > 0 and len(dataset) >= num_requests:
|
||||||
|
break
|
||||||
|
if line.strip():
|
||||||
|
try:
|
||||||
|
dataset.append(json.loads(line))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Skip invalid JSON lines
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fields that should NOT be passed through extra_request_body
|
||||||
|
# These are either handled separately or are metadata
|
||||||
|
# max_tokens is excluded because it's handled via output_len -> max_completion_tokens
|
||||||
|
# max_completion_tokens is also excluded to avoid conflicts
|
||||||
|
EXCLUDED_FIELDS = {"messages", "max_tokens", "max_completion_tokens", "model"}
|
||||||
|
|
||||||
|
filtered_dataset: List[DatasetRow] = []
|
||||||
|
for data in dataset:
|
||||||
|
messages = data.get("messages", [])
|
||||||
|
if not messages:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Use max_tokens from the request, or fall back to fixed_output_len
|
||||||
|
output_len = fixed_output_len or data.get("max_tokens", 256)
|
||||||
|
|
||||||
|
# Extract extra request body parameters (tools, temperature, top_p, etc.)
|
||||||
|
extra_body = {k: v for k, v in data.items() if k not in EXCLUDED_FIELDS}
|
||||||
|
|
||||||
|
# Calculate prompt length by applying chat template
|
||||||
|
# This includes the messages but not the tools
|
||||||
|
prompt_len = len(
|
||||||
|
tokenizer.apply_chat_template(
|
||||||
|
messages, tokenize=True, add_generation_prompt=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# If tools are present, we need to add their token count
|
||||||
|
# Tools are sent as part of the request and count toward input tokens
|
||||||
|
if "tools" in extra_body:
|
||||||
|
# Encode tools as JSON string to estimate token count
|
||||||
|
tools_str = json.dumps(extra_body["tools"])
|
||||||
|
tools_tokens = len(tokenizer.encode(tools_str))
|
||||||
|
prompt_len += tools_tokens
|
||||||
|
|
||||||
|
# Pass messages list directly - bench_serving handles List[Dict] prompts
|
||||||
|
filtered_dataset.append(
|
||||||
|
DatasetRow(
|
||||||
|
prompt=messages,
|
||||||
|
prompt_len=prompt_len,
|
||||||
|
output_len=output_len,
|
||||||
|
extra_request_body=extra_body, # Store per-request parameters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Loaded {len(filtered_dataset)} OpenAI-format requests")
|
||||||
|
print(f"#Input tokens: {np.sum([x.prompt_len for x in filtered_dataset])}")
|
||||||
|
print(f"#Output tokens: {np.sum([x.output_len for x in filtered_dataset])}")
|
||||||
|
return filtered_dataset
|
||||||
|
|
||||||
|
|
||||||
def sample_custom_requests(
|
def sample_custom_requests(
|
||||||
dataset_path: str,
|
dataset_path: str,
|
||||||
num_requests: int,
|
num_requests: int,
|
||||||
@@ -2183,7 +2292,15 @@ async def benchmark(
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown backend: {backend}")
|
raise ValueError(f"Unknown backend: {backend}")
|
||||||
|
|
||||||
is_multi_turn = isinstance(input_requests[0].prompt, list)
|
# Check for multi-turn: prompt is a list of strings (not OpenAI messages dicts)
|
||||||
|
# Multi-turn format: ["turn1", "turn2", ...] - list of strings
|
||||||
|
# OpenAI format: [{"role": "user", "content": "..."}, ...] - list of dicts
|
||||||
|
first_prompt = input_requests[0].prompt
|
||||||
|
is_multi_turn = (
|
||||||
|
isinstance(first_prompt, list)
|
||||||
|
and len(first_prompt) > 0
|
||||||
|
and isinstance(first_prompt[0], str)
|
||||||
|
)
|
||||||
if is_multi_turn:
|
if is_multi_turn:
|
||||||
request_func = wrap_multi_turn_request_func(request_func, backend=backend)
|
request_func = wrap_multi_turn_request_func(request_func, backend=backend)
|
||||||
|
|
||||||
@@ -2340,6 +2457,10 @@ async def benchmark(
|
|||||||
else:
|
else:
|
||||||
lora_name = None
|
lora_name = None
|
||||||
|
|
||||||
|
# Merge global extra_request_body with per-request extras
|
||||||
|
# Per-request parameters take precedence over global ones
|
||||||
|
merged_extra_body = {**extra_request_body, **request.extra_request_body}
|
||||||
|
|
||||||
request_func_input = RequestFuncInput(
|
request_func_input = RequestFuncInput(
|
||||||
model=model_id,
|
model=model_id,
|
||||||
prompt=request.prompt,
|
prompt=request.prompt,
|
||||||
@@ -2348,7 +2469,7 @@ async def benchmark(
|
|||||||
output_len=request.output_len,
|
output_len=request.output_len,
|
||||||
lora_name=lora_name,
|
lora_name=lora_name,
|
||||||
image_data=request.image_data,
|
image_data=request.image_data,
|
||||||
extra_request_body=extra_request_body,
|
extra_request_body=merged_extra_body,
|
||||||
timestamp=request.timestamp,
|
timestamp=request.timestamp,
|
||||||
routing_key=request.routing_key,
|
routing_key=request.routing_key,
|
||||||
)
|
)
|
||||||
@@ -2877,6 +2998,7 @@ if __name__ == "__main__":
|
|||||||
choices=[
|
choices=[
|
||||||
"sharegpt",
|
"sharegpt",
|
||||||
"custom",
|
"custom",
|
||||||
|
"openai",
|
||||||
"random",
|
"random",
|
||||||
"random-ids",
|
"random-ids",
|
||||||
"generated-shared-prefix",
|
"generated-shared-prefix",
|
||||||
|
|||||||
Reference in New Issue
Block a user