acdir-llada-math500 / lmdeploy /benchmark /benchmark_serving.py
NYCU-MLLab's picture
Upload folder using huggingface_hub
4a28d4d verified
Raw
History Blame Contribute Delete
8.86 kB
import os
import subprocess
import time
import fire
import yaml
def get_launching_server_cmd(model_path, backend, server_config):
if backend in ['turbomind', 'pytorch']:
cmd = ['lmdeploy', 'serve', 'api_server', model_path, '--backend', backend]
elif backend == 'sglang':
cmd = ['python3', '-m', 'sglang.launch_server', '--model-path', model_path]
elif backend == 'vllm':
cmd = ['vllm', 'serve', model_path]
else:
raise ValueError(f'unknown backend: {backend}')
for key, value in server_config.items():
# Convert snake_case to kebab-case for command line args
key = key.replace('_', '-')
cmd.append(f'--{key}')
if str(value):
cmd.append(str(value))
# Special handling for proxy server case
if server_config.get('proxy_url') and server_config.get('dp'):
cmd.append('--allow-terminate-by-client')
return cmd
def get_output_file(model_path, backend, server_config):
"""Generate the benchmark output filename."""
model_name = server_config.get('model_name', None) or os.path.basename(model_path)
if backend not in ['turbomind', 'pytorch', 'sglang', 'vllm']:
raise ValueError(f'Unknown backend: {backend}')
if backend in ['sglang', 'vllm']:
return f'benchmark_{model_name}_{backend}.csv'
# For turbomind/pytorch backends
params = [
('bs', server_config['max_batch_size']),
('tp', server_config.get('tp', 1)),
('dp', server_config.get('dp', '')),
('ep', server_config.get('ep', '')),
('cache', server_config.get('cache_max_entry_count', 0.8)),
('mptk', server_config.get('max_prefill_token_num', '')),
]
params_str = '_'.join(f'{k}{v}' for k, v in params if v != '')
# Turbomind-specific additions
if backend == 'turbomind' and (comm := server_config.get('communicator')):
params_str += f'_{comm}'
return f'benchmark_{model_name}_{backend}_{params_str}.csv'
def get_server_ip_port(backend: str, server_config: dict) -> tuple[str, int]:
if backend in ['turbomind', 'pytorch']:
if server_config.get('proxy_url'):
# If proxy_url is set, we use the proxy server's IP and port
parts = server_config['proxy_url'].split(':')
server_ip = parts[1].lstrip('//')
server_port = int(parts[2])
else:
# Default to the server IP and port specified in the config
server_ip = server_config.get('server_ip', '0.0.0.0')
server_port = server_config.get('server_port', 23333)
elif backend == 'sglang':
return (server_config.get('server_ip', '0.0.0.0'), server_config.get('port', 30000))
elif backend == 'vllm':
return (server_config.get('server_ip', '0.0.0.0'), server_config.get('port', 8000))
else:
raise ValueError(f'unknown backend: {backend}')
return server_ip, server_port
def wait_server_ready(server_ip: str, server_port: int) -> bool:
"""Wait for the API server to become ready."""
from openai import OpenAI
while True:
try:
client = OpenAI(api_key='DUMMPY', base_url=f'http://{server_ip}:{server_port}/v1')
model_name = client.models.list().data[0].id
if model_name:
print('Server is ready.')
return True
except Exception as e:
print(f'connect to server http://{server_ip}:{server_port} failed {e}')
time.sleep(5)
def get_client_cmd(backend: str, server_ip: str, server_port: int, client_config: dict) -> list[str]:
"""Generate the client benchmark command."""
current_dir = os.path.dirname(os.path.abspath(__file__))
if backend in ['turbomind', 'pytorch']:
backend = 'lmdeploy'
cmd = [
'python3', f'{current_dir}/profile_restful_api.py', '--backend', backend, '--host', server_ip, '--port',
str(server_port)
]
for key, value in client_config.items():
# change the key like 'dataset_path' to 'dataset-path' to suit the optional when performing
# "python3 benchmark/profile_restful_api.py"
key = key.replace('_', '-')
if key == 'disable-warmup':
if str(value).lower() == 'true':
cmd.append(f'--{key}')
continue
cmd.append(f'--{key}')
cmd.append(str(value))
return cmd
def benchmark(model_path: str, backend: str, server_config: dict, data_config: dict | list[dict]):
"""Benchmark the server with the given configuration.
Args:
model_path: Path to the model.
backend: Backend to use.
server_config: Configuration for the server and the inference engine.
data_config: Configuration for the data.
"""
if isinstance(data_config, dict):
data_config = [data_config]
if not (isinstance(data_config, list) and all(isinstance(d, dict) for d in data_config)):
raise ValueError('data_config must be a dict or list of dicts')
server_cmd = get_launching_server_cmd(model_path, backend, server_config)
server_ip, server_port = get_server_ip_port(backend, server_config)
proc = None
try:
print(f"Starting api_server: {' '.join(server_cmd)}", flush=True)
proc = subprocess.Popen(server_cmd)
# Wait for the server to be ready
wait_server_ready(server_ip, server_port)
# Run benchmarks
output_file = get_output_file(model_path, backend, server_config)
for data in data_config:
data = data.copy()
data['output_file'] = output_file
client_cmd = get_client_cmd(backend, server_ip, server_port, data)
print(f"Running benchmark: {' '.join(client_cmd)}")
subprocess.run(client_cmd, check=True)
except Exception as e:
print(f'Unexpected error: {e}')
raise
finally:
# Clean up server process
if proc and proc.poll() is None:
if server_config.get('proxy_url') and server_config.get('dp'):
# Sending termination request to proxy_server. The request will be broadcasted to
# api_server on each dp_rank by proxy server
# Note that api_server is supposed to be launched with --allow-terminate-by-client
print('Sending termination request to proxy server')
subprocess.run(['curl', '-X', 'POST', f'{server_config["proxy_url"]}/nodes/terminate_all'],
check=True,
timeout=10)
proc.terminate()
try:
proc.wait(timeout=30)
except subprocess.TimeoutExpired:
print('Server did not terminate gracefully - killing')
proc.kill()
def validate_config(config: dict) -> None:
"""Validate the configuration structure.
Args:
config: Loaded configuration dictionary
Raises:
BenchmarkConfigError: If configuration is invalid
"""
required_sections = ['api_server', 'engine', 'data']
for section in required_sections:
if section not in config:
raise ValueError(f'Missing required config section: {section}')
if not isinstance(config['engine'], (dict, list)):
raise ValueError('engine config must be a dict or list of dicts')
if not isinstance(config['data'], (dict, list)):
raise ValueError('data config must be a dict or list of dicts')
def main(backend: str, config_path: str, model_path: str | None = None):
"""Main entry point for the benchmark script.
Args:
backend: Backend to use
config_path: Path to config file
model_path: Optional override for model path
Raises:
BenchmarkConfigError: If required parameters are missing or config is invalid
"""
with open(config_path) as f:
config = yaml.safe_load(f)
server_config = config['server']
engine_configs = config['engine']
data_config = config['data']
if isinstance(engine_configs, dict):
engine_configs = [engine_configs]
assert isinstance(engine_configs, list) and all(isinstance(s, dict) for s in engine_configs)
for engine_config in engine_configs:
server_config = server_config.copy()
server_config.update(engine_config) # Merge engine config with server config
# The model_path provided by the user will override the model_path in the config file.
model_path = model_path or server_config.pop('model_path')
# Remove model_path from server_config to avoid passing it to the server command
server_config.pop('model_path', None)
benchmark(model_path, backend, server_config, data_config)
if __name__ == '__main__':
fire.Fire(main)