Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

371 lines
10 KiB
Python

"""Common utilities for testing and benchmarking."""
import json
import logging
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
from typing import List, Optional, Tuple
import requests
from tokenspeed_kernel.platform import current_platform
from tokenspeed.runtime.utils import get_bool_env_var, get_device
from tokenspeed.runtime.utils.process import kill_process_tree
def is_in_ci():
"""Return whether it is in CI runner."""
return get_bool_env_var("CI") or get_bool_env_var("GITHUB_ACTIONS")
def is_in_amd_ci():
"""Return whether it is in CI on an AMD runner."""
return is_in_ci() and current_platform().is_amd
def is_blackwell_system():
"""Return whether it is running on a Blackwell system."""
return current_platform().is_blackwell
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH = 600
if is_in_amd_ci():
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH = 3600
if is_blackwell_system():
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH = 3000
if is_in_ci():
DEFAULT_PORT_FOR_SRT_TEST_RUNNER = (
10000 + int(os.environ.get("CUDA_VISIBLE_DEVICES", "0")[0]) * 2000
)
else:
DEFAULT_PORT_FOR_SRT_TEST_RUNNER = (
20000 + int(os.environ.get("CUDA_VISIBLE_DEVICES", "0")[0]) * 1000
)
DEFAULT_URL_FOR_TEST = f"http://127.0.0.1:{DEFAULT_PORT_FOR_SRT_TEST_RUNNER + 1000}"
def auto_config_device() -> str:
return get_device()
def _serve_process(
command: List[str],
env: dict,
return_stdout_stderr: Optional[tuple],
) -> subprocess.Popen:
if return_stdout_stderr:
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env.copy(),
text=True,
bufsize=1,
)
def _dump(src, sinks):
for line in iter(src.readline, ""):
for sink in sinks:
sink.write(line)
sink.flush()
src.close()
threading.Thread(
target=_dump,
args=(proc.stdout, [return_stdout_stderr[0], sys.stdout]),
daemon=True,
).start()
threading.Thread(
target=_dump,
args=(proc.stderr, [return_stdout_stderr[1], sys.stderr]),
daemon=True,
).start()
else:
proc = subprocess.Popen(command, stdout=None, stderr=None, env=env.copy())
return proc
def _wait_for_server_health(
proc: subprocess.Popen,
base_url: str,
api_key: Optional[str],
timeout_duration: float,
) -> Tuple[bool, Optional[str]]:
start_time = time.perf_counter()
with requests.Session() as session:
while time.perf_counter() - start_time < timeout_duration:
return_code = proc.poll()
if return_code is not None:
return False, f"Server process exited with code {return_code}"
try:
headers = {
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {api_key}",
}
response = session.get(
f"{base_url}/readiness",
headers=headers,
timeout=5,
)
if response.status_code == 200:
return True, None
except requests.RequestException:
pass
return_code = proc.poll()
if return_code is not None:
return False, f"Server unexpectedly exited (return_code={return_code})"
time.sleep(10)
return False, "Server failed to start within the timeout period"
def popen_serve_server(
model: str,
base_url: str,
timeout: float,
api_key: Optional[str] = None,
other_args: Optional[list[str]] = None,
env: Optional[dict] = None,
return_stdout_stderr: Optional[tuple] = None,
device: str = "auto",
):
other_args = other_args or []
if device == "auto":
device = auto_config_device()
other_args = list(other_args)
other_args += ["--device", str(device)]
if env is None:
env = os.environ.copy()
else:
merged = os.environ.copy()
merged.update(env)
env = merged
_, host, port = base_url.split(":")
host = host[2:]
command = [
"tokenspeed",
"serve",
"--model",
model,
*[str(x) for x in other_args],
"--host",
host,
"--port",
port,
]
if api_key:
command += ["--api-key", api_key]
print(f"command={shlex.join(command)}")
process = _serve_process(command, env, return_stdout_stderr)
success, error_msg = _wait_for_server_health(process, base_url, api_key, timeout)
if success:
return process
try:
kill_process_tree(process.pid)
except Exception as e:
print(f"Error killing process after launch failure: {e}")
if "exited" in error_msg:
raise Exception(error_msg + ". Check server logs for errors.")
raise TimeoutError(error_msg)
def lcs(x, y):
m = len(x)
n = len(y)
table = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
for j in range(n + 1):
if i == 0 or j == 0:
table[i][j] = 0
elif x[i - 1] == y[j - 1]:
table[i][j] = table[i - 1][j - 1] + 1
else:
table[i][j] = max(table[i - 1][j], table[i][j - 1])
return table[m][n]
def calculate_rouge_l(output_strs_list1, output_strs_list2):
rouge_l_scores = []
for s1, s2 in zip(output_strs_list1, output_strs_list2):
lcs_len = lcs(s1, s2)
precision = lcs_len / len(s1) if len(s1) > 0 else 0
recall = lcs_len / len(s2) if len(s2) > 0 else 0
if precision + recall > 0:
fmeasure = (2 * precision * recall) / (precision + recall)
else:
fmeasure = 0.0
rouge_l_scores.append(fmeasure)
return rouge_l_scores
def _evalscope_executable() -> str:
evalscope_bin = os.environ.get("EVALSCOPE_BIN")
if evalscope_bin:
return evalscope_bin
evalscope_bin = shutil.which("evalscope")
if evalscope_bin:
return evalscope_bin
venv_dir = "/tmp/evalscope-perf"
evalscope_bin = os.path.join(venv_dir, "bin", "evalscope")
if os.path.exists(evalscope_bin):
return evalscope_bin
subprocess.run(
["python3", "-m", "uv", "venv", "--seed", "--clear", venv_dir],
check=True,
)
subprocess.run(
[
"python3",
"-m",
"uv",
"pip",
"install",
"--python",
os.path.join(venv_dir, "bin", "python"),
"evalscope[perf]",
],
check=True,
)
return evalscope_bin
def _iter_evalscope_scores(value):
if isinstance(value, dict):
for key, item in value.items():
key_lower = str(key).lower()
if key_lower in {
"score",
"accuracy",
"acc",
"averageaccuracy",
} and isinstance(item, (int, float)):
yield float(item)
else:
yield from _iter_evalscope_scores(item)
elif isinstance(value, list):
for item in value:
yield from _iter_evalscope_scores(item)
def _parse_evalscope_stdout(output: str) -> Optional[float]:
scores = []
for line in output.splitlines():
if "|" not in line or "Score" in line or "===" in line or "---" in line:
continue
columns = [col.strip() for col in line.strip().strip("|").split("|")]
if not columns:
continue
try:
scores.append(float(columns[-1]))
except ValueError:
continue
return sum(scores) / len(scores) if scores else None
def _load_evalscope_score(work_dir: str, output: str) -> float:
scores = []
reports_dir = os.path.join(work_dir, "reports")
if os.path.isdir(reports_dir):
for path in Path(reports_dir).rglob("*.json"):
try:
with open(path) as f:
scores.extend(_iter_evalscope_scores(json.load(f)))
except (OSError, json.JSONDecodeError):
continue
if scores:
return sum(scores) / len(scores)
score = _parse_evalscope_stdout(output)
if score is not None:
return score
raise RuntimeError(f"Unable to parse evalscope score from {work_dir}")
def run_evalscope(
*,
base_url: str,
model: str,
dataset: str,
limit: Optional[int] = None,
eval_batch_size: int = 16,
generation_config: Optional[dict] = None,
dataset_args: Optional[dict] = None,
) -> dict:
work_dir = tempfile.mkdtemp(prefix=f"evalscope-{dataset}-", dir="/tmp")
api_url = base_url.rstrip("/")
if not api_url.endswith("/v1"):
api_url += "/v1"
cmd = [
_evalscope_executable(),
"eval",
"--model",
model,
"--api-url",
api_url,
"--api-key",
"EMPTY_TOKEN",
"--datasets",
dataset,
"--eval-batch-size",
str(eval_batch_size),
"--work-dir",
work_dir,
]
if limit is not None:
cmd.extend(["--limit", str(limit)])
if generation_config:
cmd.extend(["--generation-config", json.dumps(generation_config)])
if dataset_args:
cmd.extend(["--dataset-args", json.dumps(dataset_args)])
result = subprocess.run(
cmd,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
print(result.stdout)
score = _load_evalscope_score(work_dir, result.stdout)
return {"score": score, "accuracy": score, "work_dir": work_dir}
def write_github_step_summary(content):
if not os.environ.get("GITHUB_STEP_SUMMARY"):
logging.warning("GITHUB_STEP_SUMMARY environment variable not set")
return
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
f.write(content)