import json import logging import os import re import subprocess import threading import time from urllib.parse import urlparse from sglang.srt.utils import kill_process_tree from sglang.test.ascend.e2e.test_npu_multi_node_utils import ( SERVICE_PORT, check_role, launch_pd_mix_node, launch_pd_separation_node, launch_router, wait_server_ready, ) from sglang.test.test_utils import ( DEFAULT_URL_FOR_TEST, CustomTestCase, dump_metric, popen_launch_server, ) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler()], ) logger = logging.getLogger(__name__) EVALSCOPE = "evalscope" BENCHMARK_TOOL_DEFAULT = EVALSCOPE PYTHON_FOR_TEST_TOOL = "test_env_transformers_tool/bin/python" if not os.path.exists(PYTHON_FOR_TEST_TOOL) or not os.access( PYTHON_FOR_TEST_TOOL, os.X_OK ): PYTHON_FOR_TEST_TOOL = "python3" logger.info(f"PYTHON_FOR_TEST_TOOL: {PYTHON_FOR_TEST_TOOL}") DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH = 3600 MAX_SERVER_KEEP_ALIVE_TIME = 3600 ACCURACY_TOLERANCE = 0.99 # Dataset total question counts and allowed fluctuation (in questions) DATASET_QUESTION_COUNTS = { "aime25": 30, "aime26": 30, "gpqa_diamond": 198, } DATASET_FLUCTUATION = { "aime25": 2, "aime26": 2, "gpqa_diamond": 5, } MAX_RETRY_COUNT = 3 SERVER_INITIALIZATION_DELAY = 120 if os.environ.get("ASCEND_RT_VISIBLE_DEVICES"): DEFAULT_SERVER_PORT_FOR_TEST = ( 20000 + int(os.environ.get("ASCEND_RT_VISIBLE_DEVICES", "0")[0]) * 100 ) else: DEFAULT_SERVER_PORT_FOR_TEST = ( 20000 + int(os.environ.get("ASCEND_VISIBLE_DEVICES", "0")[0]) * 100 ) DEFAULT_URL_FOR_TEST = f"http://127.0.0.1:{DEFAULT_SERVER_PORT_FOR_TEST + 66}" def get_accuracy_threshold(datasets, baseline_accuracy): """Calculate accuracy threshold based on dataset fluctuation tolerance. For datasets with defined fluctuation (aime*, gpqa_diamond), use absolute question count tolerance. For others (e.g. mmmu), use percentage tolerance. """ dataset = datasets[0] if datasets else None if dataset in DATASET_FLUCTUATION and dataset in DATASET_QUESTION_COUNTS: fluctuation = DATASET_FLUCTUATION[dataset] / DATASET_QUESTION_COUNTS[dataset] return baseline_accuracy - fluctuation return baseline_accuracy * ACCURACY_TOLERANCE def get_max_retries(datasets): """Return max retry count for accuracy tests. gpqa and aime datasets support up to MAX_RETRY_COUNT retries. mmmu and others use 1 attempt (no retry). """ dataset = datasets[0] if datasets else None if dataset in DATASET_FLUCTUATION: return MAX_RETRY_COUNT return 1 def run_evalscope( host, port, model, datasets, dataset_args=None, eval_batch_size=16, limit=100000, generation_config=None, dataset_dir=None, timeout=60000, stream=True, eval_type="openai_api", ): metrics_path = os.getenv("METRICS_DATA_FILE") result_path = "./evalscope_result" if not metrics_path else metrics_path logger.info(f"The metrics result file: {result_path}") api_url = f"http://{host}:{port}/v1/chat/completions" if generation_config is None: generation_config = {"max_tokens": 512} config_dict = { "model": model, "api_url": api_url, "eval_type": eval_type, "datasets": datasets, "eval_batch_size": eval_batch_size, "generation_config": generation_config, "timeout": timeout, "stream": stream, "limit": limit, "work_dir": result_path, } if dataset_args: config_dict["dataset_args"] = dataset_args if dataset_dir: config_dict["dataset_dir"] = dataset_dir config_json = json.dumps(config_dict, ensure_ascii=False, indent=2) config_json_escaped = config_json.replace("\\", "\\\\").replace("'''", "\\'\\'\\'") script_content = "import json\n" script_content += "from evalscope import TaskConfig, run_task\n\n" script_content += f"config = json.loads('''{config_json_escaped}''')\n" script_content += "task_cfg = TaskConfig(**config)\n" script_content += "run_task(task_cfg=task_cfg)\n" script_path = f"/tmp/evalscope_run_{model}_{'_'.join(datasets)}.py" with open(script_path, "w") as f: f.write(script_content) logger.info(f"Generated evalscope script: {script_path}") install_cmd = ( "/bin/bash /root/sglang/python/sglang/test/ascend/e2e/run_evalscope.sh" ) subprocess.run(install_cmd, shell=True, check=True) python_bin = "test_env_evalscope/bin/python" cmd = f"{python_bin} {script_path}" logger.info(f"Command: {cmd}") process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, shell=True, ) output_lines = [] try: for line in iter(process.stdout.readline, ""): if line.strip(): print(line, end="") output_lines.append(line.strip()) process.wait() if process.returncode != 0: logger.error(f"Command failed with return code: {process.returncode}") raise subprocess.CalledProcessError(process.returncode, cmd) logger.info("Command executed successfully") metrics = {} full_output = "\n".join(output_lines) report_match = re.search(r"Dump report to:\s*(\S+)", full_output) if report_match: report_path = report_match.group(1) logger.info(f"Found evalscope report file: {report_path}") try: with open(report_path, "r") as rf: report_data = json.load(rf) for item in report_data: score = item.get("score") if score is not None: metrics["accuracy"] = float(score) logger.info(f"The Final Accuracy from report: {score}") break except Exception as e: logger.warning(f"Failed to read report file {report_path}: {e}") if "accuracy" not in metrics: accuracy_patterns = [ r"mean_acc\s*.*?│\s*\d+\s*│\s*([\d.]+)\s*│", r"│\s+([\d.]+)\s+│\s+\S+\s+│\s*$", r"accuracy\s*[:=]?\s*([\d.]+)", r"Accuracy\s*[:=]?\s*([\d.]+)", r"score\s*[:=]?\s*([\d.]+)", ] for pattern in accuracy_patterns: matches = re.findall(pattern, full_output) if matches: final_accuracy = float(matches[-1]) metrics["accuracy"] = final_accuracy logger.info(f"The Final Accuracy from output: {final_accuracy}") break if "accuracy" not in metrics: logger.info("Can Not Find The Accuracy in evalscope output") return metrics except KeyboardInterrupt: logger.info("Keyboard interrupt received, terminating process...") process.terminate() try: process.wait(timeout=5) logger.info("Process terminated") except subprocess.TimeoutExpired: logger.warning("Process did not terminate gracefully, killing it...") process.kill() logger.info("Process killed") raise except Exception as e: logger.error(f"Error executing command: {e}") process.terminate() process.wait(timeout=5) raise def assert_metrics(self, metrics): if not metrics: raise Exception("No metrics obtained from benchmark") if self.accuracy is not None: threshold = get_accuracy_threshold(self.datasets, self.accuracy) dump_metric( "accuracy", float(metrics["accuracy"]), labels={"test_case": self.__class__.__name__, "type": "accuracy"}, ) dump_metric( "accuracy_baseline", float(self.accuracy), labels={"test_case": self.__class__.__name__, "type": "accuracy"}, ) self.assertGreaterEqual( float(metrics["accuracy"]), threshold, f"Accuracy check failed. Expected >= {threshold}, Got: {metrics['accuracy']}", ) class TestNpuAccuracyTestCaseBase(CustomTestCase): model = None benchmark_tool = BENCHMARK_TOOL_DEFAULT backend = "sglang" datasets = ["gsm8k"] dataset_args = None eval_batch_size = 16 limit = 100000 generation_config = None dataset_dir = None stream = True timeout = 60000 eval_type = "openai_api" other_args = None server_timeout = DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH envs = None max_attempts = 2 n_runs = 3 accuracy = 0.1 @classmethod def setUpClass(cls): cls.base_url = DEFAULT_URL_FOR_TEST env = os.environ.copy() for key, value in env.items(): logger.info(f"ENV_VAR_SYS {key}:{value}") if cls.envs: for key, value in cls.envs.items(): logger.info(f"ENV_VAR_CASE {key}:{value}") env[key] = value other_args = list(cls.other_args) cls.process = popen_launch_server( cls.model, cls.base_url, timeout=cls.server_timeout, other_args=other_args, env=env, ) @classmethod def tearDownClass(cls): if hasattr(cls, "process") and cls.process: try: kill_process_tree(cls.process.pid) except Exception as e: logger.error(f"Error during tearDown: {e}") def run_accuracy(self): parsed_url = urlparse(self.base_url) host = parsed_url.hostname port = parsed_url.port if self.benchmark_tool == EVALSCOPE: model_name = os.path.basename(self.model) max_retries = get_max_retries(self.datasets) best_metrics = None for attempt in range(max_retries): metrics = run_evalscope( host=host, port=port, model=model_name, datasets=self.datasets, dataset_args=self.dataset_args, eval_batch_size=self.eval_batch_size, limit=self.limit, generation_config=self.generation_config, dataset_dir=self.dataset_dir, stream=self.stream, timeout=self.timeout, eval_type=self.eval_type, ) if best_metrics is None or float(metrics.get("accuracy", 0)) > float( best_metrics.get("accuracy", 0) ): best_metrics = metrics threshold = get_accuracy_threshold(self.datasets, self.accuracy) if float(best_metrics.get("accuracy", 0)) >= threshold: break if attempt < max_retries - 1: logger.info( f"Accuracy {best_metrics.get('accuracy')} below threshold " f"{threshold}, retrying ({attempt + 1}/{max_retries - 1})..." ) assert_metrics(self, best_metrics) def run_accuracy_multiple(self, n_runs=None): if n_runs is None: n_runs = self.n_runs parsed_url = urlparse(self.base_url) host = parsed_url.hostname port = parsed_url.port if self.benchmark_tool != EVALSCOPE: raise Exception( "run_accuracy_multiple only supports evalscope benchmark tool" ) model_name = os.path.basename(self.model) all_metrics = [] for i in range(n_runs): logger.info(f"=== Accuracy run {i + 1}/{n_runs} ===") metrics = run_evalscope( host=host, port=port, model=model_name, datasets=self.datasets, dataset_args=self.dataset_args, eval_batch_size=self.eval_batch_size, limit=self.limit, generation_config=self.generation_config, dataset_dir=self.dataset_dir, stream=self.stream, timeout=self.timeout, eval_type=self.eval_type, ) all_metrics.append(metrics) if metrics and "accuracy" in metrics: logger.info(f"Run {i + 1} accuracy: {metrics['accuracy']}") else: logger.warning(f"Run {i + 1} failed to get accuracy metric") valid_metrics = [m for m in all_metrics if m and "accuracy" in m] if not valid_metrics: raise Exception("No valid accuracy metrics obtained from any run") avg_accuracy = sum(float(m["accuracy"]) for m in valid_metrics) / len( valid_metrics ) logger.info("=" * 60) logger.info("Multiple Run Accuracy Results:") for i, m in enumerate(valid_metrics): logger.info(f" Run {i + 1}: {m['accuracy']}") logger.info(f" Average: {avg_accuracy}") logger.info("=" * 60) avg_metrics = {"accuracy": avg_accuracy} dump_metric( "accuracy_avg", avg_accuracy, labels={"test_case": self.__class__.__name__, "type": "accuracy"}, ) assert_metrics(self, avg_metrics) class TestNpuAccuracyMultiNodePdMixTestCaseBase(CustomTestCase): model_config = None benchmark_tool = BENCHMARK_TOOL_DEFAULT backend = "sglang" datasets = ["gsm8k"] dataset_args = None eval_batch_size = 16 limit = 100000 generation_config = None dataset_dir = None stream = True timeout = 60000 eval_type = "openai_api" other_args = None server_timeout = DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH envs = None max_attempts = 2 accuracy = 0.1 @classmethod def setUpClass(cls): cls.local_ip = "127.0.0.1" cls.host = os.getenv("POD_IP") cls.port = SERVICE_PORT cls.base_url = f"http://{cls.host}:{cls.port}" cls.hostname = os.getenv("HOSTNAME") cls.role = "master" if cls.hostname.endswith("sglang-node-0") else "worker" logger.info(f"Init {cls.host} {cls.role=}!") cls.start_pd_mix_master_node() cls.start_pd_mix_worker_node() @classmethod def tearDownClass(cls): pass @classmethod @check_role(allowed_roles=["master"]) def start_pd_mix_master_node(cls): sglang_thread = threading.Thread( target=launch_pd_mix_node, args=(cls.model_config,) ) sglang_thread.start() wait_server_ready(f"{cls.base_url}/health") logger.info( f"Wait {SERVER_INITIALIZATION_DELAY}s, starting run benchmark ......" ) time.sleep(SERVER_INITIALIZATION_DELAY) @classmethod @check_role(allowed_roles=["worker"]) def start_pd_mix_worker_node(cls): sglang_thread = threading.Thread( target=launch_pd_mix_node, args=(cls.model_config,) ) sglang_thread.start() logger.info( f"{cls.role} node started, keeping test alive for {MAX_SERVER_KEEP_ALIVE_TIME} seconds" ) time.sleep(MAX_SERVER_KEEP_ALIVE_TIME) @check_role(allowed_roles=["master", "worker"]) def run_accuracy(self): parsed_url = urlparse(self.base_url) host = parsed_url.hostname port = parsed_url.port if self.benchmark_tool == EVALSCOPE: model_name = os.path.basename(self.model_config.get("model_path")) max_retries = get_max_retries(self.datasets) best_metrics = None for attempt in range(max_retries): metrics = run_evalscope( host=self.host, port=self.port, model=model_name, datasets=self.datasets, dataset_args=self.dataset_args, eval_batch_size=self.eval_batch_size, limit=self.limit, generation_config=self.generation_config, dataset_dir=self.dataset_dir, stream=self.stream, timeout=self.timeout, eval_type=self.eval_type, ) if best_metrics is None or float(metrics.get("accuracy", 0)) > float( best_metrics.get("accuracy", 0) ): best_metrics = metrics threshold = get_accuracy_threshold(self.datasets, self.accuracy) if float(best_metrics.get("accuracy", 0)) >= threshold: break if attempt < max_retries - 1: logger.info( f"Accuracy {best_metrics.get('accuracy')} below threshold " f"{threshold}, retrying ({attempt + 1}/{max_retries - 1})..." ) assert_metrics(self, best_metrics) class TestNpuAccuracyMultiNodePdSepTestCaseBase(CustomTestCase): model_config = None benchmark_tool = BENCHMARK_TOOL_DEFAULT backend = "sglang" datasets = ["gsm8k"] dataset_args = None eval_batch_size = 16 limit = 100000 generation_config = None dataset_dir = None stream = True timeout = 60000 eval_type = "openai_api" other_args = None server_timeout = DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH max_attempts = 2 accuracy = 0.1 @classmethod def setUpClass(cls): cls.process = None cls.local_ip = "127.0.0.1" cls.host = os.getenv("POD_IP") cls.port = SERVICE_PORT cls.base_url = f"http://{cls.host}:{cls.port}" cls.hostname = os.getenv("HOSTNAME") cls.role = ( "router" if "router" in cls.hostname else "prefill" if "prefill" in cls.hostname else "decode" ) logger.info(f"Init {cls.host} {cls.role=}!") cls.start_pd_server() cls.start_router_server() @classmethod def tearDownClass(cls): if cls.process: try: kill_process_tree(cls.process.pid) except Exception as e: logger.error(f"Error during tearDown: {e}") @classmethod @check_role(allowed_roles=["router"]) def start_router_server(cls): logger.info(f"Starting router in thread...") sglang_thread = threading.Thread(target=launch_router, args=(cls.model_config,)) sglang_thread.daemon = True sglang_thread.start() health_check_url = f"{cls.base_url}/health" logger.info(f"Waiting for router to be ready at {health_check_url}") wait_server_ready(health_check_url) logger.info( f"Waiting {SERVER_INITIALIZATION_DELAY} seconds for the server to fully initialize..." ) time.sleep(SERVER_INITIALIZATION_DELAY) @classmethod @check_role(allowed_roles=["prefill", "decode"]) def start_pd_server(cls): logger.info(f"Starting pd separation node...") cls.process = launch_pd_separation_node(cls.model_config) logger.info(f"Pd separation node started with PID: {cls.process.pid}") while True: if cls.process.poll() is None: time.sleep(30) else: exit_code = cls.process.poll() raise Exception( f"Sglang process exited on node {cls.host} {cls.hostname} with exit code: {exit_code}" ) @check_role(allowed_roles=["router"]) def run_accuracy(self): parsed_url = urlparse(self.base_url) host = parsed_url.hostname port = parsed_url.port if self.benchmark_tool == EVALSCOPE: model_name = os.path.basename(self.model_config.get("model_path")) max_retries = get_max_retries(self.datasets) best_metrics = None for attempt in range(max_retries): metrics = run_evalscope( host=host, port=port, model=model_name, datasets=self.datasets, dataset_args=self.dataset_args, eval_batch_size=self.eval_batch_size, limit=self.limit, generation_config=self.generation_config, dataset_dir=self.dataset_dir, stream=self.stream, timeout=self.timeout, eval_type=self.eval_type, ) if best_metrics is None or float(metrics.get("accuracy", 0)) > float( best_metrics.get("accuracy", 0) ): best_metrics = metrics threshold = get_accuracy_threshold(self.datasets, self.accuracy) if float(best_metrics.get("accuracy", 0)) >= threshold: break if attempt < max_retries - 1: logger.info( f"Accuracy {best_metrics.get('accuracy')} below threshold " f"{threshold}, retrying ({attempt + 1}/{max_retries - 1})..." ) assert_metrics(self, best_metrics)