| 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(): |
| |
| key = key.replace('_', '-') |
| cmd.append(f'--{key}') |
| if str(value): |
| cmd.append(str(value)) |
| |
| 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' |
|
|
| |
| 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 != '') |
| |
| 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'): |
| |
| parts = server_config['proxy_url'].split(':') |
| server_ip = parts[1].lstrip('//') |
| server_port = int(parts[2]) |
| else: |
| |
| 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(): |
| |
| |
| 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_server_ready(server_ip, server_port) |
| |
| 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: |
| |
| if proc and proc.poll() is None: |
| if server_config.get('proxy_url') and server_config.get('dp'): |
| |
| |
| |
| 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) |
| |
| model_path = model_path or server_config.pop('model_path') |
| |
| server_config.pop('model_path', None) |
| benchmark(model_path, backend, server_config, data_config) |
|
|
|
|
| if __name__ == '__main__': |
| fire.Fire(main) |
|
|