Files
2026-07-13 13:17:40 +08:00

280 lines
8.6 KiB
Python

# There is a dead-lock issue that arises due to gevent's monkey-patching
# https://github.com/ipython/ipython/issues/11730
# Fix: We do this import first before anything else
# Thus the # noqa tags are needed below
import gevent.monkey
gevent.monkey.patch_all()
import sys # noqa: E402
import argparse # noqa: E402
import json # noqa: E402
import logging # noqa: E402
import os # noqa: E402
import subprocess # noqa: E402
import threading # noqa: E402
import time # noqa: E402
from datetime import datetime # noqa: E402
from bm import run_bm # noqa: E402
from common import write_to_s3, get_llm_config # noqa: E402
RAYLLM_RELEASE_TEST_PERF_SERVICE_NAME = "rayllm_release_test_perf_service"
THREAD_CLEANUP_TIMEOUT_S = 10
logger = logging.getLogger(__file__)
logging.basicConfig(
format="%(asctime)s %(message)s",
datefmt="%m/%d/%Y %H:%M:%S",
level=logging.INFO,
)
def get_timestamp():
return datetime.now().strftime("%H:%M:%S.%f")[:-3]
class ColoredLogger:
HEADER = "\033[95m"
SERVER = "\033[94m"
CLIENT = "\033[92m"
WARNING = "\033[93m"
ERROR = "\033[91m"
ENDC = "\033[0m"
@staticmethod
def log_server(msg):
print(
f"{ColoredLogger.SERVER}[SERVER {get_timestamp()}] {msg}{ColoredLogger.ENDC}"
)
@staticmethod
def log_client(msg):
print(
f"{ColoredLogger.CLIENT}[CLIENT {get_timestamp()}] {msg}{ColoredLogger.ENDC}"
)
@staticmethod
def log_error(msg):
print(
f"{ColoredLogger.ERROR}[ERROR {get_timestamp()}] {msg}{ColoredLogger.ENDC}"
)
def stream_process_output(process, logger_func, stop_event, is_error: bool = False):
while not stop_event.is_set():
if is_error:
output_line = process.stderr.readline()
else:
output_line = process.stdout.readline()
if output_line:
logger_func(output_line.strip())
elif process.poll() is not None:
break
def get_vllm_cli_args(llm_config):
engine_kwargs = llm_config["engine_kwargs"]
# When we define tokenizer_pool size, vllm, by default, uses Ray
# that breaks the assumption that this script should not use ray
# TODO (Kourosh): When the job issue with non driver ray
# subprocesses are resolved we can remove these constraints
engine_kwargs.pop("tokenizer_pool_extra_config", None)
engine_kwargs.pop("tokenizer_pool_size", None)
engine_kwargs.pop("tokenizer_pool_type", None)
cli_args = ["--model", llm_config["model_loading_config"]["model_id"]]
for key, value in engine_kwargs.items():
cli_args.extend(_engine_kwarg_to_cli(key, value))
return cli_args
def _engine_kwarg_to_cli(key, value):
"""Translate one engine_kwargs entry to vllm CLI args.
vLLM uses argparse.BooleanOptionalAction for bool fields, so emit the
explicit `--flag` / `--no-flag` form to honor the configured value
regardless of the engine default.
"""
if value is None:
return []
flag = "--" + key.replace("_", "-")
if isinstance(value, bool):
return [flag] if value else ["--no-" + key.replace("_", "-")]
if isinstance(value, dict):
return [flag, json.dumps(value)]
return [flag, str(value)]
def get_ray_options(llm_config):
num_gpus = llm_config["tensor_parallelism"]["degree"]
acc_type = llm_config["accelerator_type"]
resources = {f"accelerator_type:{acc_type}": 0.001}
return {"num_gpus": num_gpus, "resources": resources}
def start_vllm_process(vllm_cli_args):
server_process = subprocess.Popen(
["python", "-m", "vllm.entrypoints.openai.api_server"] + vllm_cli_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True, # Return strings from stdout/stderr
bufsize=1, # Line buffered
env=os.environ.copy(),
)
return server_process
def run_vllm_benchmark(vllm_cli_args):
stop_event = threading.Event()
results = {}
try:
# Start the server process
ColoredLogger.log_server("Starting vLLM server...")
server_process = start_vllm_process(vllm_cli_args)
# Start server output streaming thread
server_thread = threading.Thread(
target=stream_process_output,
args=(server_process, ColoredLogger.log_server, stop_event),
daemon=True, # Daemonize the thread so it stops when the main thread stops.
)
server_thread.start()
# Start server error streaming thread
server_error_thread = threading.Thread(
target=stream_process_output,
args=(server_process, ColoredLogger.log_server, stop_event),
kwargs={"is_error": True},
daemon=True, # Daemonize the thread so it stops when the main thread stops.
)
server_error_thread.start()
# Wait for server to be ready
server_ready = False
start_time = time.time()
timeout = 300 # 5 minutes timeout
while not server_ready and time.time() - start_time < timeout:
if server_process.poll() is not None:
raise Exception("Server process terminated unexpectedly")
# Check if server is responding
try:
import requests
response = requests.get("http://localhost:8000/health")
if response.status_code == 200:
server_ready = True
ColoredLogger.log_server("Server is ready!")
except Exception:
time.sleep(1)
continue
if not server_ready:
raise TimeoutError("Server failed to start within timeout period")
# Start benchmark
ColoredLogger.log_client("Starting benchmark...")
results = run_bm(
api_url="http://localhost:8000",
api_key="NONE",
concurrency=[1, 2, 4, 8, 16, 32],
run_time="1m",
prompt_tokens=256,
max_tokens=64,
stream=False,
summary_file="./results.csv",
)
print(
"Writing final result to AWS Firehose:",
json.dumps(results, indent=4, sort_keys=True),
sep="\n",
)
ColoredLogger.log_client("Benchmark completed successfully")
except Exception as e:
ColoredLogger.log_error(f"Error during benchmark: {str(e)}")
raise
finally:
# Clean up
if "server_process" in locals():
ColoredLogger.log_server("Shutting down server...")
server_process.terminate()
server_process.wait(timeout=THREAD_CLEANUP_TIMEOUT_S)
stop_event.set()
# Wait for all threads to complete
if "server_thread" in locals():
server_thread.join(timeout=THREAD_CLEANUP_TIMEOUT_S)
if "server_error_thread" in locals():
server_error_thread.join(timeout=THREAD_CLEANUP_TIMEOUT_S)
# Wait some time to make sure everyting is cleanend up.
time.sleep(5)
return results
def upload_results_to_s3(s3_path, results, service_metadata):
if any(result is None for result in results):
raise ValueError(
"Found None results during benchmarking. " "This should not have happened."
)
data_to_write = [{**result, **service_metadata} for result in results]
write_to_s3(data_to_write, s3_path)
def main(pargs):
llm_config = get_llm_config(pargs.llm_config)
vllm_cli_args = get_vllm_cli_args(llm_config)
results = run_vllm_benchmark(vllm_cli_args)
tag = f"{llm_config['accelerator_type']}-TP{llm_config['engine_kwargs']['tensor_parallel_size']}"
service_metadata = {
"cloud_name": "",
"service_name": "",
"py_version": f"py{sys.version_info.major}{sys.version_info.minor}",
"tag": tag,
"vllm_engine": "V1",
}
# Post the results to S3
if results:
print(
"Writing final result to AWS S3:",
json.dumps(results, indent=4, sort_keys=True),
sep="\n",
)
upload_results_to_s3(pargs.remote_result_path, results, service_metadata)
else:
raise ValueError(
"For some reason the benchmarking results are empty. Something is wrong."
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--llm-config",
type=str,
required=True,
default="The LLM config to start vLLM engine",
)
parser.add_argument(
"--remote-result-path",
type=str,
required=True,
help="The remote s3 path to store intermediate results on.",
)
main(parser.parse_args())