[CI] Migrate nightly tests to test/registered/ (#15582)

This commit is contained in:
Alison Shao
2025-12-22 22:16:32 -08:00
committed by GitHub
parent bc3ca30023
commit 989d4b3012
64 changed files with 168 additions and 140 deletions

View File

@@ -0,0 +1,263 @@
from dataclasses import dataclass
from types import SimpleNamespace
from typing import List, Optional, Tuple
from sglang.srt.utils import kill_process_tree
from sglang.test.run_eval import run_eval
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST,
ModelLaunchSettings,
popen_launch_server,
write_github_step_summary,
)
@dataclass
class AccuracyTestParams:
"""Parameters for accuracy testing."""
dataset: str # e.g., "mgsm_en", "gsm8k", "mmmu", "gpqa"
baseline_accuracy: float # Required: minimum accuracy threshold
num_examples: Optional[int] = None
num_threads: Optional[int] = None
max_tokens: Optional[int] = None
return_latency: bool = False
# Extended parameters for special evaluations (e.g., GPQA with thinking mode)
thinking_mode: Optional[str] = None # e.g., "deepseek-v3"
temperature: Optional[float] = None
repeat: Optional[int] = None
@dataclass
class AccuracyTestResult:
"""Result of an accuracy test."""
model: str
dataset: str
passed: bool
score: Optional[float]
baseline_accuracy: float
error: Optional[str]
latency: Optional[float] = None
def write_accuracy_github_summary(
test_name: str,
dataset: str,
results: List[AccuracyTestResult],
) -> None:
"""Write accuracy test results to GitHub step summary.
Args:
test_name: Name of the test
dataset: Dataset name used for evaluation
results: List of AccuracyTestResult objects
"""
summary = f"## {test_name} - Accuracy ({dataset})\n"
summary += "| model | status | score | baseline | error |\n"
summary += "| ----- | ------ | ----- | -------- | ----- |\n"
for result in results:
status_emoji = "" if result.passed else ""
score_str = f"{result.score:.4f}" if result.score is not None else "N/A"
baseline_str = f"{result.baseline_accuracy:.4f}"
error_str = result.error if result.error else "-"
summary += f"| {result.model} | {status_emoji} | {score_str} | {baseline_str} | {error_str} |\n"
write_github_step_summary(summary)
def _run_simple_eval(
model: ModelLaunchSettings,
base_url: str,
dataset: str,
num_examples: Optional[int] = None,
num_threads: Optional[int] = None,
max_tokens: Optional[int] = None,
return_latency: bool = False,
thinking_mode: Optional[str] = None,
temperature: Optional[float] = None,
repeat: Optional[int] = None,
) -> Tuple[bool, Optional[str], Optional[dict]]:
"""Run evaluation using simple_eval backend (run_eval.py).
Returns:
Tuple of (success, error_message, metrics_dict)
"""
process = None
try:
process = popen_launch_server(
model.model_path,
base_url,
other_args=model.extra_args,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
args = SimpleNamespace(
base_url=base_url,
model=model.model_path,
eval_name=dataset,
num_examples=num_examples,
num_threads=num_threads or 1024,
)
if max_tokens is not None:
args.max_tokens = max_tokens
if return_latency:
args.return_latency = True
if thinking_mode is not None:
args.thinking_mode = thinking_mode
if temperature is not None:
args.temperature = temperature
if repeat is not None:
args.repeat = repeat
result = run_eval(args)
# Handle result format (run_eval can return metrics or (metrics, latency))
if return_latency and isinstance(result, tuple):
metrics, latency = result
metrics["latency"] = round(latency, 4)
else:
metrics = result
return True, None, metrics
except Exception as e:
return False, f"Accuracy test exception: {str(e)}", None
finally:
if process:
kill_process_tree(process.pid)
def _run_few_shot_eval(
model: ModelLaunchSettings,
base_url: str,
num_questions: Optional[int] = None,
num_shots: int = 8,
max_tokens: int = 512,
) -> Tuple[bool, Optional[str], Optional[dict]]:
"""Run evaluation using few_shot backend (few_shot_gsm8k.py).
Returns:
Tuple of (success, error_message, metrics_dict)
"""
from sglang.test.few_shot_gsm8k import run_eval as run_few_shot_eval
process = None
try:
process = popen_launch_server(
model.model_path,
base_url,
other_args=model.extra_args,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
args = SimpleNamespace(
num_shots=num_shots,
data_path=None,
num_questions=num_questions or 200,
max_new_tokens=max_tokens,
parallel=128,
host="http://127.0.0.1",
port=int(base_url.split(":")[-1]),
)
metrics = run_few_shot_eval(args)
# Normalize metrics format (few_shot returns "accuracy", simple_eval returns "score")
if "accuracy" in metrics and "score" not in metrics:
metrics["score"] = metrics["accuracy"]
return True, None, metrics
except Exception as e:
return False, f"Few-shot evaluation exception: {str(e)}", None
finally:
if process:
kill_process_tree(process.pid)
def run_accuracy_test(
model: ModelLaunchSettings,
params: AccuracyTestParams,
base_url: Optional[str] = None,
) -> AccuracyTestResult:
"""Run accuracy test for a single model.
Args:
model: ModelLaunchSettings with model config
params: AccuracyTestParams with dataset, baseline, and optional settings
base_url: Server base URL (default: DEFAULT_URL_FOR_TEST)
Returns:
AccuracyTestResult with test outcome
"""
base_url = base_url or DEFAULT_URL_FOR_TEST
print(f"\n{'='*60}")
print(f"Running ACCURACY test for {model.model_path}")
print(f" Dataset: {params.dataset}")
print(f" Baseline: {params.baseline_accuracy}")
print(f"{'='*60}\n")
# Run evaluation based on dataset type
if params.dataset == "gsm8k":
success, error, metrics = _run_few_shot_eval(
model=model,
base_url=base_url,
num_questions=params.num_examples,
max_tokens=params.max_tokens or 512,
)
else:
success, error, metrics = _run_simple_eval(
model=model,
base_url=base_url,
dataset=params.dataset,
num_examples=params.num_examples,
num_threads=params.num_threads,
max_tokens=params.max_tokens,
return_latency=params.return_latency,
thinking_mode=params.thinking_mode,
temperature=params.temperature,
repeat=params.repeat,
)
if not success:
print(f"✗ Accuracy test failed for {model.model_path}: {error}")
return AccuracyTestResult(
model=model.model_path,
dataset=params.dataset,
passed=False,
score=None,
baseline_accuracy=params.baseline_accuracy,
error=error,
)
# Validate against baseline
score = metrics.get("score", 0.0)
passed = score >= params.baseline_accuracy
latency = metrics.get("latency")
if passed:
print(f"✓ Accuracy {score:.3f} >= baseline {params.baseline_accuracy:.3f}")
else:
error = f"Accuracy {score:.3f} below baseline {params.baseline_accuracy:.3f}"
print(f"{error}")
return AccuracyTestResult(
model=model.model_path,
dataset=params.dataset,
passed=passed,
score=score,
baseline_accuracy=params.baseline_accuracy,
error=error if not passed else None,
latency=latency,
)

View File

View File

@@ -0,0 +1,68 @@
import os
from abc import ABC
from types import SimpleNamespace
from sglang.srt.utils import kill_process_tree
from sglang.test.few_shot_gsm8k import run_eval
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST,
popen_launch_server,
)
class GSM8KAscendMixin(ABC):
model = ""
accuracy = 0.00
other_args = [
"--trust-remote-code",
"--mem-fraction-static",
"0.8",
"--attention-backend",
"ascend",
"--disable-cuda-graph",
]
@classmethod
def setUpClass(cls):
cls.base_url = DEFAULT_URL_FOR_TEST
os.environ["PYTORCH_NPU_ALLOC_CONF"] = "expandable_segments:True"
os.environ["ASCEND_MF_STORE_URL"] = "tcp://127.0.0.1:24666"
os.environ["HCCL_BUFFSIZE"] = "200"
os.environ["SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK"] = "24"
os.environ["USE_VLLM_CUSTOM_ALLREDUCE"] = "1"
os.environ["HCCL_EXEC_TIMEOUT"] = "200"
os.environ["STREAMS_PER_DEVICE"] = "32"
os.environ["SGLANG_ENBLE_TORCH_COMILE"] = "1"
os.environ["AUTO_USE_UC_MEMORY"] = "0"
os.environ["P2P_HCCL_BUFFSIZE"] = "20"
env = os.environ.copy()
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=cls.other_args,
env=env,
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def test_gsm8k(self):
args = SimpleNamespace(
num_shots=5,
data_path=None,
num_questions=200,
max_new_tokens=512,
parallel=128,
host="http://127.0.0.1",
port=int(self.base_url.split(":")[-1]),
)
metrics = run_eval(args)
self.assertGreater(
metrics["accuracy"],
self.accuracy,
f'Accuracy of {self.model} is {str(metrics["accuracy"])}, is lower than {self.accuracy}',
)

View File

@@ -0,0 +1,217 @@
import glob
import json
import os
import subprocess
from sglang.srt.utils import kill_process_tree
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST,
CustomTestCase,
popen_launch_server,
)
class TestVLMModels(CustomTestCase):
model = ""
mmmu_accuracy = 0.00
other_args = [
"--trust-remote-code",
"--cuda-graph-max-bs",
"32",
"--enable-multimodal",
"--mem-fraction-static",
0.35,
"--log-level",
"info",
"--attention-backend",
"ascend",
"--disable-cuda-graph",
"--tp-size",
4,
]
@classmethod
def setUpClass(cls):
# Removed argument parsing from here
cls.base_url = DEFAULT_URL_FOR_TEST
cls.api_key = "sk-123456"
cls.time_out = DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH
# Set OpenAI API key and base URL environment variables. Needed for lmm-evals to work.
os.environ["OPENAI_API_KEY"] = cls.api_key
os.environ["OPENAI_API_BASE"] = f"{cls.base_url}/v1"
def run_mmmu_eval(
self,
model_version: str,
output_path: str,
limit: str,
*,
env: dict | None = None,
):
"""
Evaluate a VLM on the MMMU validation set with lmmseval.
Only `model_version` (checkpoint) and `chat_template` vary;
We are focusing only on the validation set due to resource constraints.
"""
# -------- fixed settings --------
model = "openai_compatible"
tp = 1
tasks = "mmmu_val"
batch_size = 2
log_suffix = "openai_compatible"
os.makedirs(output_path, exist_ok=True)
# -------- compose --model_args --------
model_args = f'model_version="{model_version}",' f"tp={tp}"
# -------- build command list --------
cmd = [
"python3",
"-m",
"lmms_eval",
"--model",
model,
"--model_args",
model_args,
"--tasks",
tasks,
"--batch_size",
str(batch_size),
"--log_samples",
"--log_samples_suffix",
log_suffix,
"--output_path",
str(output_path),
"--limit",
limit,
"--config",
"/__w/sglang/sglang/test/registered/ascend/vlm_models/mmmu-val.yaml",
]
subprocess.run(
cmd,
check=True,
timeout=3600,
)
def _run_vlm_mmmu_test(
self,
output_path="./logs",
test_name="",
custom_env=None,
capture_output=False,
limit="50",
):
"""
Common method to run VLM MMMU benchmark test.
Args:
model: Model to test
output_path: Path for output logs
test_name: Optional test name for logging
custom_env: Optional custom environment variables
capture_output: Whether to capture server stdout/stderr
"""
print(f"\nTesting model: {self.model}{test_name}")
process = None
server_output = ""
try:
# Prepare environment variables
process_env = os.environ.copy()
if custom_env:
process_env.update(custom_env)
# Prepare stdout/stderr redirection if needed
stdout_file = None
stderr_file = None
if capture_output:
stdout_file = open("/tmp/server_stdout.log", "w")
stderr_file = open("/tmp/server_stderr.log", "w")
process = popen_launch_server(
self.model,
base_url=self.base_url,
timeout=self.time_out,
api_key=self.api_key,
other_args=self.other_args,
env=process_env,
return_stdout_stderr=(
(stdout_file, stderr_file) if capture_output else None
),
)
# Run evaluation
self.run_mmmu_eval(self.model, output_path, limit)
# Get the result file
result_file_path = glob.glob(f"{output_path}/*.json")[0]
with open(result_file_path, "r") as f:
result = json.load(f)
print(f"Result{test_name}\n: {result}")
# Process the result
mmmu_accuracy = result["results"]["mmmu_val"]["mmmu_acc,none"]
print(
f"Model {self.model} achieved accuracy{test_name}: {mmmu_accuracy:.4f}"
)
# Capture server output if requested
if capture_output and process:
server_output = self._read_output_from_files()
# Assert performance meets expected threshold
self.assertGreaterEqual(
mmmu_accuracy,
self.mmmu_accuracy,
f"Model {self.model} accuracy ({mmmu_accuracy:.4f}) below expected threshold ({self.mmmu_accuracy:.4f}){test_name}",
)
return server_output
except Exception as e:
print(f"Error testing {self.model}{test_name}: {e}")
self.fail(f"Test failed for {self.model}{test_name}: {e}")
finally:
# Ensure process cleanup happens regardless of success/failure
if process is not None and process.poll() is None:
print(f"Cleaning up process {process.pid}")
try:
kill_process_tree(process.pid)
except Exception as e:
print(f"Error killing process: {e}")
# clean up temporary files
if capture_output:
if stdout_file:
stdout_file.close()
if stderr_file:
stderr_file.close()
for filename in ["/tmp/server_stdout.log", "/tmp/server_stderr.log"]:
try:
if os.path.exists(filename):
os.remove(filename)
except Exception as e:
print(f"Error removing {filename}: {e}")
def _read_output_from_files(self):
output_lines = []
log_files = [
("/tmp/server_stdout.log", "[STDOUT]"),
("/tmp/server_stderr.log", "[STDERR]"),
]
for filename, tag in log_files:
try:
if os.path.exists(filename):
with open(filename, "r") as f:
for line in f:
output_lines.append(f"{tag} {line.rstrip()}")
except Exception as e:
print(f"Error reading {tag.lower()} file: {e}")
return "\n".join(output_lines)

View File

@@ -0,0 +1,333 @@
"""Utilities for running nightly performance benchmarks with profiling."""
import json
import os
import subprocess
import time
from typing import List, Optional, Tuple
import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.nightly_bench_utils import BenchmarkResult, generate_markdown_report
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
is_in_ci,
popen_launch_server,
write_github_step_summary,
)
class NightlyBenchmarkRunner:
"""Helper class for running nightly performance benchmarks with profiling.
This class encapsulates common patterns used across nightly performance tests,
including profile directory management, benchmark command construction,
result parsing, and report generation.
"""
def __init__(
self,
profile_dir: str,
test_name: str,
base_url: str,
gpu_config: str = None,
):
"""Initialize the benchmark runner.
Args:
profile_dir: Directory to store performance profiles
test_name: Name of the test (used for reporting)
base_url: Base URL for the server
gpu_config: Optional GPU configuration string (e.g., "2-gpu-h100", "8-gpu-b200")
"""
self.profile_dir = profile_dir
self.test_name = test_name
self.base_url = base_url
self.gpu_config = gpu_config or os.environ.get("GPU_CONFIG", "")
# Include GPU config in report header if available
header = f"## {test_name}"
if self.gpu_config:
header += f" ({self.gpu_config})"
header += "\n"
self.full_report = header + BenchmarkResult.help_str()
def setup_profile_directory(self) -> None:
"""Create the profile directory if it doesn't exist."""
os.makedirs(self.profile_dir, exist_ok=True)
def generate_profile_filename(
self, model_path: str, variant: str = ""
) -> Tuple[str, str]:
"""Generate unique profile filename and path for the model.
Args:
model_path: Path to the model (e.g., "deepseek-ai/DeepSeek-V3.1")
variant: Optional variant suffix (e.g., "basic", "mtp", "nsa")
Returns:
Tuple of (profile_path_prefix, json_output_file)
"""
timestamp = int(time.time())
model_safe_name = model_path.replace("/", "_")
# Build filename with optional variant
if variant:
profile_filename = f"{model_safe_name}_{variant}_{timestamp}"
json_filename = f"results_{model_safe_name}_{variant}_{timestamp}.json"
else:
profile_filename = f"{model_safe_name}_{timestamp}"
json_filename = f"results_{model_safe_name}_{timestamp}.json"
profile_path_prefix = os.path.join(self.profile_dir, profile_filename)
return profile_path_prefix, json_filename
def build_benchmark_command(
self,
model_path: str,
batch_sizes: List[int],
input_lens: Tuple[int, ...],
output_lens: Tuple[int, ...],
profile_path_prefix: str,
json_output_file: str,
extra_args: Optional[List[str]] = None,
) -> List[str]:
"""Build the benchmark command with all required arguments.
Args:
model_path: Path to the model
batch_sizes: List of batch sizes to test
input_lens: Tuple of input lengths to test
output_lens: Tuple of output lengths to test
profile_path_prefix: Prefix for profile output files
json_output_file: Path to JSON output file
extra_args: Optional extra arguments to append to command
Returns:
List of command arguments ready for subprocess.run()
"""
command = [
"python3",
"-m",
"sglang.bench_one_batch_server",
"--model",
model_path,
"--base-url",
self.base_url,
"--batch-size",
*[str(x) for x in batch_sizes],
"--input-len",
*[str(x) for x in input_lens],
"--output-len",
*[str(x) for x in output_lens],
"--show-report",
"--profile",
"--profile-by-stage",
"--profile-output-dir",
profile_path_prefix,
f"--pydantic-result-filename={json_output_file}",
"--no-append-to-github-summary",
]
if extra_args:
command.extend(extra_args)
return command
def run_benchmark_command(
self, command: List[str], model_description: str = ""
) -> Tuple[subprocess.CompletedProcess, bool]:
"""Execute the benchmark command and return the result.
Args:
command: Command to execute
model_description: Description for logging (e.g., "model_name (variant)")
Returns:
Tuple of (CompletedProcess, success_bool)
"""
print(f"Running command: {' '.join(command)}")
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
desc = model_description or "benchmark"
print(f"Error running benchmark for {desc}:")
print(result.stderr)
return result, False
return result, True
def load_benchmark_results(
self, json_output_file: str, model_description: str = ""
) -> Tuple[List[BenchmarkResult], bool]:
"""Load and parse benchmark results from JSON file.
Args:
json_output_file: Path to JSON output file
model_description: Description for logging
Returns:
Tuple of (list of BenchmarkResult objects, success_bool)
"""
benchmark_results = []
if not os.path.exists(json_output_file):
desc = model_description or "model"
print(f"Warning: JSON output file {json_output_file} not found for {desc}")
return benchmark_results, False
try:
with open(json_output_file, "r") as f:
json_data = json.load(f)
# Convert JSON data to BenchmarkResult objects
for data in json_data:
benchmark_result = BenchmarkResult(**data)
benchmark_results.append(benchmark_result)
print(
f"Loaded {len(benchmark_results)} benchmark results from {json_output_file}"
)
# Clean up JSON file
os.remove(json_output_file)
return benchmark_results, True
except Exception as e:
desc = model_description or "model"
print(f"Error loading benchmark results for {desc}: {e}")
# Try to clean up the file anyway
if os.path.exists(json_output_file):
os.remove(json_output_file)
return benchmark_results, False
def run_benchmark_for_model(
self,
model_path: str,
batch_sizes: List[int],
input_lens: Tuple[int, ...],
output_lens: Tuple[int, ...],
other_args: Optional[List[str]] = None,
variant: str = "",
extra_bench_args: Optional[List[str]] = None,
) -> Tuple[List[BenchmarkResult], bool, Optional[float]]:
"""Run a complete benchmark for a single model with server management.
This method handles:
- Server launch and cleanup
- Profile filename generation
- Benchmark command construction and execution
- Result loading and parsing
- Fetching speculative decoding accept length (for MTP/EAGLE)
Args:
model_path: Path to the model
batch_sizes: List of batch sizes to test
input_lens: Tuple of input lengths
output_lens: Tuple of output lengths
other_args: Arguments to pass to server launch
variant: Optional variant suffix (e.g., "basic", "mtp")
extra_bench_args: Extra arguments for the benchmark command
Returns:
Tuple of (list of BenchmarkResult objects, success_bool, avg_spec_accept_length or None)
"""
benchmark_results = []
avg_spec_accept_length = None
model_description = f"{model_path}" + (f" ({variant})" if variant else "")
# Launch server
process = popen_launch_server(
model=model_path,
base_url=self.base_url,
other_args=other_args or [],
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
try:
# Generate filenames
profile_path_prefix, json_output_file = self.generate_profile_filename(
model_path, variant
)
# Build and run benchmark command
# Prepare extra args with run_name if variant is specified
bench_args = list(extra_bench_args) if extra_bench_args else []
if variant:
bench_args.extend(["--run-name", variant])
command = self.build_benchmark_command(
model_path,
batch_sizes,
input_lens,
output_lens,
profile_path_prefix,
json_output_file,
extra_args=bench_args,
)
result, cmd_success = self.run_benchmark_command(command, model_description)
if not cmd_success:
return benchmark_results, False, None
# Load results
benchmark_results, load_success = self.load_benchmark_results(
json_output_file, model_description
)
# Fetch speculative decoding accept length before killing server
avg_spec_accept_length = self._get_spec_accept_length()
return benchmark_results, load_success, avg_spec_accept_length
finally:
# Always clean up server process
kill_process_tree(process.pid)
def _get_spec_accept_length(self) -> Optional[float]:
"""Query the server for avg_spec_accept_length metric.
Returns:
The average speculative decoding accept length, or None if not available.
"""
try:
response = requests.get(f"{self.base_url}/get_server_info", timeout=10)
if response.status_code == 200:
server_info = response.json()
internal_states = server_info.get("internal_states", [])
if internal_states and len(internal_states) > 0:
accept_length = internal_states[0].get("avg_spec_accept_length")
if accept_length is not None:
print(f" avg_spec_accept_length={accept_length:.2f}")
return accept_length
except Exception as e:
print(f" Warning: Could not fetch spec accept length: {e}")
return None
def add_report(self, results: List[BenchmarkResult]) -> None:
"""Add benchmark results to the full report.
Args:
results: List of BenchmarkResult objects to add to report
"""
if results:
report_part = generate_markdown_report(self.profile_dir, results)
self.full_report += report_part + "\n"
def write_final_report(self) -> None:
"""Write the final report to GitHub summary if in CI."""
if is_in_ci():
write_github_step_summary(self.full_report)
print(self.full_report)
def get_full_report(self) -> str:
"""Get the accumulated full report.
Returns:
The full markdown report as a string
"""
return self.full_report

View File

@@ -0,0 +1,230 @@
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
from sglang.test.nightly_bench_utils import BenchmarkResult
from sglang.test.nightly_utils import NightlyBenchmarkRunner
from sglang.test.test_utils import DEFAULT_URL_FOR_TEST, ModelLaunchSettings
@dataclass
class PerformanceTestParams:
"""Parameters for performance testing."""
batch_sizes: List[int] = field(default_factory=lambda: [1, 8, 16, 64])
input_lens: Tuple[int, ...] = (4096,)
output_lens: Tuple[int, ...] = (512,)
profile_dir: Optional[str] = None # None = auto-generate based on is_vlm
dataset_name: str = "mmmu" # For VLM perf test
# MTP/EAGLE speculative decoding: minimum accept length threshold (None = no validation)
spec_accept_length_threshold: Optional[float] = None
@dataclass
class PerformanceTestResult:
"""Result of a performance test.
Aggregates metrics across all batch sizes tested for a single model.
"""
model: str
passed: bool
error: Optional[str]
# Aggregate metrics (from the largest batch size result, or None if failed)
latency: Optional[float] = None
input_throughput: Optional[float] = None
output_throughput: Optional[float] = None
overall_throughput: Optional[float] = None
# All individual benchmark results
benchmark_results: Optional[List[BenchmarkResult]] = None
# MTP/EAGLE speculative decoding metric
avg_spec_accept_length: Optional[float] = None
def run_performance_test(
model: ModelLaunchSettings,
perf_runner: NightlyBenchmarkRunner,
batch_sizes: List[int] = None,
input_lens: Tuple[int, ...] = (4096,),
output_lens: Tuple[int, ...] = (512,),
is_vlm: bool = False,
dataset_name: str = "mmmu",
spec_accept_length_threshold: Optional[float] = None,
) -> PerformanceTestResult:
# Set default for mutable argument
if batch_sizes is None:
batch_sizes = [1, 8, 16, 64]
print(f"\n{'='*60}")
print(f"Running PERFORMANCE test for {model.model_path}")
print(f" Batch sizes: {batch_sizes}")
print(f" Input lens: {input_lens}")
print(f" Output lens: {output_lens}")
if spec_accept_length_threshold is not None:
print(f" Spec accept length threshold: {spec_accept_length_threshold}")
print(f"{'='*60}\n")
# Build extra args for benchmarks
extra_bench_args = ["--trust-remote-code"]
if is_vlm:
extra_bench_args.append(f"--dataset-name={dataset_name}")
try:
results, success, avg_spec_accept_length = perf_runner.run_benchmark_for_model(
model_path=model.model_path,
batch_sizes=batch_sizes,
input_lens=input_lens,
output_lens=output_lens,
other_args=model.extra_args,
extra_bench_args=extra_bench_args,
)
if success and results:
perf_runner.add_report(results)
print(f"✓ Performance test succeeded for {model.model_path}")
# Validate speculative decoding accept length if threshold is set
error_msg = None
passed = True
if spec_accept_length_threshold is not None:
if avg_spec_accept_length is None:
error_msg = f"Spec accept length threshold set but no accept length reported"
passed = False
print(f"{error_msg}")
elif avg_spec_accept_length < spec_accept_length_threshold:
error_msg = (
f"Spec accept length {avg_spec_accept_length:.2f} < "
f"threshold {spec_accept_length_threshold}"
)
passed = False
print(f"{error_msg}")
else:
print(
f"✓ Spec accept length {avg_spec_accept_length:.2f} >= "
f"threshold {spec_accept_length_threshold}"
)
# Extract aggregate metrics from the largest batch size result
largest_batch_result = max(results, key=lambda r: r.batch_size)
return PerformanceTestResult(
model=model.model_path,
passed=passed,
error=error_msg,
latency=largest_batch_result.latency,
input_throughput=largest_batch_result.input_throughput,
output_throughput=largest_batch_result.output_throughput,
overall_throughput=largest_batch_result.overall_throughput,
benchmark_results=results,
avg_spec_accept_length=avg_spec_accept_length,
)
else:
error_msg = f"Performance test failed for {model.model_path}"
print(f"{error_msg}")
return PerformanceTestResult(
model=model.model_path,
passed=False,
error=error_msg,
)
except Exception as e:
error_msg = f"Performance test exception for {model.model_path}: {str(e)}"
print(f"{error_msg}")
return PerformanceTestResult(
model=model.model_path,
passed=False,
error=error_msg,
)
def run_performance_for_models(
models: List[ModelLaunchSettings],
profile_dir: str,
test_name: str,
base_url: Optional[str] = None,
batch_sizes: List[int] = None,
input_lens: Tuple[int, ...] = (4096,),
output_lens: Tuple[int, ...] = (512,),
is_vlm: bool = False,
dataset_name: str = "mmmu",
) -> dict:
"""Run performance tests for multiple models.
Args:
models: List of ModelLaunchSettings to test
profile_dir: Directory for performance profiles
test_name: Name for the test (used in reports)
base_url: Server base URL (default: DEFAULT_URL_FOR_TEST)
batch_sizes: Batch sizes for perf test
input_lens: Input lengths
output_lens: Output lengths
is_vlm: Whether these are VLM models
dataset_name: Dataset name for VLM benchmarks
Returns:
dict with results:
{
"all_passed": bool,
"results": [PerformanceTestResult, ...]
}
"""
base_url = base_url or DEFAULT_URL_FOR_TEST
# Setup performance runner
perf_runner = NightlyBenchmarkRunner(
profile_dir=profile_dir,
test_name=test_name,
base_url=base_url,
)
perf_runner.setup_profile_directory()
all_results = []
all_passed = True
for model in models:
print("\n" + "=" * 80)
print(f"PERFORMANCE TEST: {model.model_path}")
print(f" TP Size: {model.tp_size}")
print(f" Extra Args: {model.extra_args}")
print("=" * 80)
result = run_performance_test(
model=model,
perf_runner=perf_runner,
batch_sizes=batch_sizes,
input_lens=input_lens,
output_lens=output_lens,
is_vlm=is_vlm,
dataset_name=dataset_name,
)
all_results.append(result)
if not result.passed:
all_passed = False
# Write performance report
perf_runner.write_final_report()
# Print summary
print("\n" + "=" * 60)
print(f"Performance Test Summary: {test_name}")
print("=" * 60)
for result in all_results:
status = "PASS" if result.passed else "FAIL"
throughput_str = (
f", output: {result.output_throughput:.1f} tok/s"
if result.output_throughput
else ""
)
print(f" {result.model}: {status}{throughput_str}")
if result.error:
print(f" Error: {result.error}")
print("\n" + "=" * 60)
print(f"OVERALL: {'ALL PASSED' if all_passed else 'SOME FAILED'}")
print("=" * 60 + "\n")
return {
"all_passed": all_passed,
"results": all_results,
}

View File

@@ -0,0 +1,191 @@
from typing import List, Optional
from sglang.test.accuracy_test_runner import (
AccuracyTestParams,
AccuracyTestResult,
run_accuracy_test,
write_accuracy_github_summary,
)
from sglang.test.nightly_utils import NightlyBenchmarkRunner
from sglang.test.performance_test_runner import (
PerformanceTestParams,
PerformanceTestResult,
run_performance_test,
)
from sglang.test.test_utils import DEFAULT_URL_FOR_TEST, ModelLaunchSettings, is_in_ci
def run_combined_tests(
models: List[ModelLaunchSettings],
test_name: str = "NightlyTest",
base_url: Optional[str] = None,
is_vlm: bool = False,
accuracy_params: Optional[AccuracyTestParams] = None,
performance_params: Optional[PerformanceTestParams] = None,
) -> dict:
"""Run performance and/or accuracy tests for a list of models.
Args:
models: List of ModelLaunchSettings to test
test_name: Name for the test (used in reports)
base_url: Server base URL (default: DEFAULT_URL_FOR_TEST)
is_vlm: Whether these are VLM models (affects defaults)
accuracy_params: Parameters for accuracy tests (None to skip accuracy)
performance_params: Parameters for performance tests (None to skip perf)
Returns:
dict with test results:
{
"all_passed": bool,
"results": [
{
"model": str,
"perf_result": PerformanceTestResult/None,
"accuracy_result": AccuracyTestResult/None,
"errors": list,
},
...
]
}
"""
base_url = base_url or DEFAULT_URL_FOR_TEST
run_perf = performance_params is not None
run_accuracy = accuracy_params is not None
# Print test header
print("\n" + "=" * 80)
print(f"RUNNING: {test_name}")
print(f" Models: {len(models)}")
if run_accuracy:
print(f" Accuracy dataset: {accuracy_params.dataset}")
if run_perf:
print(f" Performance batches: {performance_params.batch_sizes}")
print("=" * 80)
# Set up performance parameters
if run_perf:
perf = performance_params
profile_dir = perf.profile_dir or (
"performance_profiles_vlms"
if is_vlm
else "performance_profiles_text_models"
)
perf_runner = NightlyBenchmarkRunner(
profile_dir=profile_dir,
test_name=test_name,
base_url=base_url,
)
perf_runner.setup_profile_directory()
else:
perf_runner = None
# Run tests for each model
all_results = []
all_passed = True
for model in models:
print("\n" + "=" * 80)
print(f"TESTING MODEL CONFIG: {model.model_path}")
print(f" TP Size: {model.tp_size}")
print(f" Extra Args: {model.extra_args}")
print("=" * 80)
model_result = {
"model": model.model_path,
"perf_result": None,
"accuracy_result": None,
"errors": [],
}
# Run performance test
if run_perf:
perf_result: PerformanceTestResult = run_performance_test(
model=model,
perf_runner=perf_runner,
batch_sizes=performance_params.batch_sizes,
input_lens=performance_params.input_lens,
output_lens=performance_params.output_lens,
is_vlm=is_vlm,
dataset_name=performance_params.dataset_name,
spec_accept_length_threshold=performance_params.spec_accept_length_threshold,
)
model_result["perf_result"] = perf_result
if not perf_result.passed:
all_passed = False
model_result["errors"].append(perf_result.error)
# Run accuracy test
if run_accuracy:
acc_result: AccuracyTestResult = run_accuracy_test(
model=model,
params=accuracy_params,
base_url=base_url,
)
model_result["accuracy_result"] = acc_result
if not acc_result.passed:
all_passed = False
model_result["errors"].append(acc_result.error)
all_results.append(model_result)
# Write performance report if we ran perf tests
if run_perf and perf_runner:
perf_runner.write_final_report()
# Write accuracy results to GitHub summary if in CI
if run_accuracy and is_in_ci():
accuracy_results = [
r["accuracy_result"] for r in all_results if r["accuracy_result"]
]
write_accuracy_github_summary(
test_name, accuracy_params.dataset, accuracy_results
)
# Print summary
print("\n" + "=" * 60)
print(f"{test_name} Results Summary")
if run_accuracy:
print(f"Dataset: {accuracy_params.dataset}")
print(f"Baseline: {accuracy_params.baseline_accuracy}")
print("=" * 60)
for i, model_result in enumerate(all_results):
print(f"\nModel {i + 1}: {model_result['model']}")
if run_perf and model_result["perf_result"]:
perf = model_result["perf_result"]
throughput_str = (
f", output: {perf.output_throughput:.1f} tok/s"
if perf.output_throughput
else ""
)
accept_str = (
f", accept_len: {perf.avg_spec_accept_length:.2f}"
if perf.avg_spec_accept_length
else ""
)
print(
f" Performance: {'PASS' if perf.passed else 'FAIL'}{throughput_str}{accept_str}"
)
if run_accuracy and model_result["accuracy_result"]:
acc = model_result["accuracy_result"]
print(f" Accuracy: {'PASS' if acc.passed else 'FAIL'}")
if acc.score is not None:
print(f" Score: {acc.score:.3f}")
if model_result["errors"]:
print(f" Errors: {model_result['errors']}")
print("\n" + "=" * 60)
print(f"OVERALL: {'ALL TESTS PASSED' if all_passed else 'SOME TESTS FAILED'}")
print("=" * 60 + "\n")
# Raise assertion error if any test failed
if not all_passed:
failed_models = [r["model"] for r in all_results if r["errors"]]
raise AssertionError(
f"Tests failed for models: {failed_models}. See results above for details."
)
return {
"all_passed": all_passed,
"results": all_results,
}