feat: sync npu nightly test improvements from Ascend testcases (#29403)

This commit is contained in:
hhhh1252023
2026-07-06 22:41:15 +08:00
committed by GitHub
parent 80decc78ec
commit 1b481deade
37 changed files with 708 additions and 235 deletions
@@ -45,6 +45,21 @@ 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"):
@@ -58,6 +73,31 @@ else:
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,
@@ -212,6 +252,7 @@ def assert_metrics(self, 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"]),
@@ -224,14 +265,11 @@ def assert_metrics(self, metrics):
)
self.assertGreaterEqual(
float(metrics["accuracy"]),
self.accuracy * ACCURACY_TOLERANCE,
f"Accuracy check failed. Expected >= {self.accuracy * ACCURACY_TOLERANCE}, Got: {metrics['accuracy']}",
threshold,
f"Accuracy check failed. Expected >= {threshold}, Got: {metrics['accuracy']}",
)
MMMU_LOCAL_PATH = "/root/.cache/modelscope/hub/datasets/AI-ModelScope___mmmu"
class TestNpuAccuracyTestCaseBase(CustomTestCase):
model = None
benchmark_tool = BENCHMARK_TOOL_DEFAULT
@@ -249,6 +287,7 @@ class TestNpuAccuracyTestCaseBase(CustomTestCase):
server_timeout = DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH
envs = None
max_attempts = 2
n_runs = 3
accuracy = 0.1
@classmethod
@@ -280,29 +319,67 @@ class TestNpuAccuracyTestCaseBase(CustomTestCase):
except Exception as e:
logger.error(f"Error during tearDown: {e}")
def _get_dataset_args(self):
if "mmmu" in self.datasets:
base_args = {"mmmu": {"dataset_id": MMMU_LOCAL_PATH}}
if self.dataset_args:
if isinstance(self.dataset_args, dict):
base_args.update(self.dataset_args)
elif isinstance(self.dataset_args, str):
base_args.update(json.loads(self.dataset_args))
return base_args
return self.dataset_args
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._get_dataset_args(),
dataset_args=self.dataset_args,
eval_batch_size=self.eval_batch_size,
limit=self.limit,
generation_config=self.generation_config,
@@ -311,7 +388,34 @@ class TestNpuAccuracyTestCaseBase(CustomTestCase):
timeout=self.timeout,
eval_type=self.eval_type,
)
assert_metrics(self, metrics)
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):
@@ -378,17 +482,6 @@ class TestNpuAccuracyMultiNodePdMixTestCaseBase(CustomTestCase):
)
time.sleep(MAX_SERVER_KEEP_ALIVE_TIME)
def _get_dataset_args(self):
if "mmmu" in self.datasets:
base_args = {"mmmu": {"dataset_id": MMMU_LOCAL_PATH}}
if self.dataset_args:
if isinstance(self.dataset_args, dict):
base_args.update(self.dataset_args)
elif isinstance(self.dataset_args, str):
base_args.update(json.loads(self.dataset_args))
return base_args
return self.dataset_args
@check_role(allowed_roles=["master", "worker"])
def run_accuracy(self):
parsed_url = urlparse(self.base_url)
@@ -396,21 +489,36 @@ class TestNpuAccuracyMultiNodePdMixTestCaseBase(CustomTestCase):
port = parsed_url.port
if self.benchmark_tool == EVALSCOPE:
model_name = os.path.basename(self.model_config.get("model_path"))
metrics = run_evalscope(
host=self.host,
port=self.port,
model=model_name,
datasets=self.datasets,
dataset_args=self._get_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,
)
assert_metrics(self, metrics)
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):
@@ -490,17 +598,6 @@ class TestNpuAccuracyMultiNodePdSepTestCaseBase(CustomTestCase):
f"Sglang process exited on node {cls.host} {cls.hostname} with exit code: {exit_code}"
)
def _get_dataset_args(self):
if "mmmu" in self.datasets:
base_args = {"mmmu": {"dataset_id": MMMU_LOCAL_PATH}}
if self.dataset_args:
if isinstance(self.dataset_args, dict):
base_args.update(self.dataset_args)
elif isinstance(self.dataset_args, str):
base_args.update(json.loads(self.dataset_args))
return base_args
return self.dataset_args
@check_role(allowed_roles=["router"])
def run_accuracy(self):
parsed_url = urlparse(self.base_url)
@@ -508,18 +605,33 @@ class TestNpuAccuracyMultiNodePdSepTestCaseBase(CustomTestCase):
port = parsed_url.port
if self.benchmark_tool == EVALSCOPE:
model_name = os.path.basename(self.model_config.get("model_path"))
metrics = run_evalscope(
host=host,
port=port,
model=model_name,
datasets=self.datasets,
dataset_args=self._get_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,
)
assert_metrics(self, metrics)
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)
@@ -24,6 +24,7 @@ logger = logging.getLogger(__name__)
NAMESPACE = os.environ.get("NAMESPACE")
CONFIGMAP_NAME = os.environ.get("KUBE_CONFIG_MAP")
ACTIVE_TEST_CLASS = "active-test-class"
LOCAL_TIMEOUT = 3600
ALL_ROLE_SET = {"prefill", "decode", "router", "master", "worker"}
@@ -41,6 +42,7 @@ BOOTSTRAP_INIT_PORT = 8995
# Timeouts and delays
ROUTER_CONFIGMAP_TIMEOUT = 300
SERVER_INITIALIZATION_DELAY = 30
SERVICE_EXIT_WAIT_SECONDS = 120
def get_nic_name():
@@ -190,6 +192,65 @@ def query_configmap(name, namespace):
return None
def upsert_configmap_field_strict(
name: str,
namespace: str,
key: str,
value: str,
):
"""
Add or update a field in ConfigMap using patch.
Strict mode: fail if ConfigMap does not exist.
"""
from kubernetes.client.rest import ApiException
k8s_api = get_k8s_api()
patch = {"data": {key: value}}
try:
k8s_api.patch_namespaced_config_map(name=name, namespace=namespace, body=patch)
logger.info(f"Upserted ConfigMap {name}: {key}={value}")
except ApiException as e:
if e.status == 404:
raise RuntimeError(
f"ConfigMap {name} does not exist in namespace {namespace}"
)
logger.error(f"Failed to upsert ConfigMap {name}: {e}")
raise
def wait_for_prefill_decode_exit(
key: str,
value: str,
timeout: int = ROUTER_CONFIGMAP_TIMEOUT,
poll_interval: int = 15,
):
start_time = time.time()
while time.time() - start_time < timeout:
configmap = query_configmap(CONFIGMAP_NAME, NAMESPACE)
if not configmap or not configmap.data:
logger.info(f"ConfigMap data is not available yet, waiting for 15s...")
time.sleep(poll_interval)
continue
existing_value = configmap.data.get(key)
upsert_configmap_field_strict(CONFIGMAP_NAME, NAMESPACE, key, value)
if existing_value is not None:
logger.info(
"%s already set (%s), waiting 120s for prefill/decode to exit ...",
key,
existing_value,
)
time.sleep(SERVICE_EXIT_WAIT_SECONDS)
else:
logger.info("%s set for the first time (%s)", key, value)
return
# Get node count from Kubernetes
def discover_worker_nodes():
"""Discover worker nodes from Kubernetes.
@@ -15,11 +15,16 @@ from sglang.test.ascend.e2e.gen_dataset_fixed_len import (
save_jsonl,
)
from sglang.test.ascend.e2e.test_npu_multi_node_utils import (
ACTIVE_TEST_CLASS,
CONFIGMAP_NAME,
NAMESPACE,
SERVICE_PORT,
check_role,
launch_pd_mix_node,
launch_pd_separation_node,
launch_router,
query_configmap,
wait_for_prefill_decode_exit,
wait_server_ready,
)
from sglang.test.test_utils import (
@@ -338,6 +343,7 @@ def run_bench_serving(
repeat_rate=None,
temperature=None,
top_p=None,
env=None,
):
metrics_path = os.getenv("METRICS_DATA_FILE")
result_file = (
@@ -449,7 +455,12 @@ def run_bench_serving(
metrics = {"mean_ttft": None, "mean_tpot": None, "total_tps": None}
process = subprocess.Popen(
cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
cmd_args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
env=env,
)
try:
# Read output line by line
@@ -883,6 +894,7 @@ class TestNpuPerformanceTestCaseBase(CustomTestCase):
dp = None
generation_kwargs = None
pop_sglang_is_in_ci_for_gsp = False
@classmethod
def setUpClass(cls):
@@ -961,7 +973,15 @@ class TestNpuPerformanceTestCaseBase(CustomTestCase):
"top_p": self.top_p,
}
logger.info(f"Starting benchmark with parameters: {bench_params}")
metrics = run_bench_serving(**bench_params)
if (
self.dataset_name == "generated-shared-prefix"
and self.pop_sglang_is_in_ci_for_gsp
):
bench_env = os.environ.copy()
bench_env.pop("SGLANG_IS_IN_CI", None)
else:
bench_env = None
metrics = run_bench_serving(**bench_params, env=bench_env)
assert_metrics(self, metrics)
@@ -993,6 +1013,7 @@ class TestNpuPerfMultiNodePdMixTestCaseBase(CustomTestCase):
dp = None
generation_kwargs = None
pop_sglang_is_in_ci_for_gsp = False
@classmethod
def setUpClass(cls):
@@ -1085,7 +1106,15 @@ class TestNpuPerfMultiNodePdMixTestCaseBase(CustomTestCase):
"top_p": self.top_p,
}
logger.info(f"Starting benchmark with parameters: {bench_params}")
metrics = run_bench_serving(**bench_params)
if (
self.dataset_name == "generated-shared-prefix"
and self.pop_sglang_is_in_ci_for_gsp
):
bench_env = os.environ.copy()
bench_env.pop("SGLANG_IS_IN_CI", None)
else:
bench_env = None
metrics = run_bench_serving(**bench_params, env=bench_env)
assert_metrics(self, metrics)
@@ -1117,6 +1146,7 @@ class TestNpuPerfMultiNodePdSepTestCaseBase(CustomTestCase):
dp = None
generation_kwargs = None
pop_sglang_is_in_ci_for_gsp = False
@classmethod
def setUpClass(cls):
@@ -1138,15 +1168,25 @@ class TestNpuPerfMultiNodePdSepTestCaseBase(CustomTestCase):
@classmethod
def tearDownClass(cls):
logger.info("Start exec tearDownClass")
if cls.process:
try:
kill_process_tree(cls.process.pid)
for _ in range(60):
if cls.process.poll() is not None:
logger.info("Process fully exited")
break
time.sleep(1)
else:
logger.warning("Process did NOT exit in time")
except Exception as e:
logger.error(f"Error during tearDown: {e}")
logger.info("tearDownClass finished")
@classmethod
@check_role(allowed_roles=["router"])
def start_router_server(cls):
wait_for_prefill_decode_exit(key=ACTIVE_TEST_CLASS, value=cls.__name__)
logger.info(f"Starting router in thread...")
sglang_thread = threading.Thread(target=launch_router, args=(cls.model_config,))
sglang_thread.daemon = True
@@ -1170,6 +1210,13 @@ class TestNpuPerfMultiNodePdSepTestCaseBase(CustomTestCase):
# Loop to check if the process is still running
while True:
configmap = query_configmap(CONFIGMAP_NAME, NAMESPACE)
if configmap and configmap.data:
executing_class = configmap.data.get(ACTIVE_TEST_CLASS)
if executing_class and executing_class != cls.__name__:
logger.info(f"Retrieved ConfigMap data: {configmap.data}")
logger.info(f"[{cls.__name__}] exec completed, exiting waiter.")
return
if cls.process.poll() is None:
# Process is still running
time.sleep(30)
@@ -1226,5 +1273,13 @@ class TestNpuPerfMultiNodePdSepTestCaseBase(CustomTestCase):
"top_p": self.top_p,
}
logger.info(f"Starting benchmark with parameters: {bench_params}")
metrics = run_bench_serving(**bench_params)
if (
self.dataset_name == "generated-shared-prefix"
and self.pop_sglang_is_in_ci_for_gsp
):
bench_env = os.environ.copy()
bench_env.pop("SGLANG_IS_IN_CI", None)
else:
bench_env = None
metrics = run_bench_serving(**bench_params, env=bench_env)
assert_metrics(self, metrics)