94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
365 lines
13 KiB
Python
365 lines
13 KiB
Python
"""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", "dsa")
|
|
|
|
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,
|
|
server_args: Optional[List[str]] = None,
|
|
enable_profile: bool = True,
|
|
) -> 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
|
|
server_args: Optional server launch arguments to record in metrics
|
|
enable_profile: Whether to enable profiling (default True for NVIDIA)
|
|
|
|
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",
|
|
f"--pydantic-result-filename={json_output_file}",
|
|
"--no-append-to-github-summary",
|
|
"--trust-remote-code",
|
|
]
|
|
|
|
# Add profiling flags only if enabled (disabled for AMD tests)
|
|
if enable_profile and profile_path_prefix:
|
|
command.extend(
|
|
[
|
|
"--profile",
|
|
"--profile-by-stage",
|
|
"--profile-output-dir",
|
|
profile_path_prefix,
|
|
]
|
|
)
|
|
|
|
if extra_args:
|
|
command.extend(extra_args)
|
|
|
|
# Record server launch arguments in metrics for tracking configurations
|
|
if server_args:
|
|
command.append("--server-args-for-metrics")
|
|
command.extend(server_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}"
|
|
)
|
|
|
|
# Note: JSON files are preserved for metrics collection by CI scripts
|
|
# They will be collected by scripts/ci/utils/save_metrics.py
|
|
|
|
return benchmark_results, True
|
|
|
|
except Exception as e:
|
|
desc = model_description or "model"
|
|
print(f"Error loading benchmark results for {desc}: {e}")
|
|
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,
|
|
enable_profile: bool = True,
|
|
timeout: Optional[int] = None,
|
|
env: Optional[dict] = 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
|
|
enable_profile: Whether to enable profiling (default True for NVIDIA)
|
|
timeout: Optional timeout for server launch (defaults to DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH)
|
|
env: Environment dict for subprocess
|
|
|
|
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 "")
|
|
|
|
process = None
|
|
try:
|
|
# Launch server
|
|
process = popen_launch_server(
|
|
model=model_path,
|
|
base_url=self.base_url,
|
|
other_args=other_args or [],
|
|
timeout=(
|
|
timeout
|
|
if timeout is not None
|
|
else DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH
|
|
),
|
|
env=env,
|
|
)
|
|
|
|
# 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,
|
|
server_args=other_args,
|
|
enable_profile=enable_profile,
|
|
)
|
|
|
|
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
|
|
if process is not None:
|
|
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}/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], variant: Optional[str] = None
|
|
) -> 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, variant)
|
|
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
|