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
371 lines
10 KiB
Python
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)
|