chore: vendor sglang v0.5.10 snapshot
This commit is contained in:
147
third_party/sglang/docs/performance_dashboard/README.md
vendored
Normal file
147
third_party/sglang/docs/performance_dashboard/README.md
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
# SGLang Performance Dashboard
|
||||
|
||||
A web-based dashboard for visualizing SGLang nightly test performance metrics.
|
||||
|
||||
## Features
|
||||
|
||||
- **Performance Trends**: View throughput, latency, and TTFT trends over time
|
||||
- **Model Comparison**: Compare performance across different models and configurations
|
||||
- **Filtering**: Filter by GPU configuration, model, variant, and batch size
|
||||
- **Interactive Charts**: Zoom, pan, and hover for detailed metrics
|
||||
- **Run History**: View recent benchmark runs with links to GitHub Actions
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Run with Local Server (Recommended)
|
||||
|
||||
For live data from GitHub Actions artifacts:
|
||||
|
||||
```bash
|
||||
# Install requirements
|
||||
pip install requests
|
||||
|
||||
# Run the server
|
||||
python server.py --fetch-on-start
|
||||
|
||||
# Visit http://localhost:8000
|
||||
```
|
||||
|
||||
The server provides:
|
||||
- Automatic fetching of metrics from GitHub
|
||||
- Caching to reduce API calls
|
||||
- `/api/metrics` endpoint for the frontend
|
||||
|
||||
### Option 2: Fetch Data Manually
|
||||
|
||||
Use the fetch script to download metrics data:
|
||||
|
||||
```bash
|
||||
# Fetch last 30 days of metrics
|
||||
python fetch_metrics.py --output metrics_data.json
|
||||
|
||||
# Fetch a specific run
|
||||
python fetch_metrics.py --run-id 21338741812 --output single_run.json
|
||||
|
||||
# Fetch only scheduled (nightly) runs
|
||||
python fetch_metrics.py --scheduled-only --days 7
|
||||
```
|
||||
|
||||
## GitHub Token
|
||||
|
||||
To download artifacts from GitHub, you need authentication:
|
||||
|
||||
1. **Using `gh` CLI** (recommended):
|
||||
```bash
|
||||
gh auth login
|
||||
```
|
||||
|
||||
2. **Using environment variable**:
|
||||
```bash
|
||||
export GITHUB_TOKEN=your_token_here
|
||||
```
|
||||
|
||||
Without a token, the dashboard will show run metadata but not detailed benchmark results.
|
||||
|
||||
## Data Structure
|
||||
|
||||
The metrics JSON has this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "21338741812",
|
||||
"run_date": "2026-01-25T22:24:02.090218+00:00",
|
||||
"commit_sha": "5cdb391...",
|
||||
"branch": "main",
|
||||
"results": [
|
||||
{
|
||||
"gpu_config": "8-gpu-h200",
|
||||
"partition": 0,
|
||||
"model": "deepseek-ai/DeepSeek-V3.1",
|
||||
"variant": "TP8+MTP",
|
||||
"benchmarks": [
|
||||
{
|
||||
"batch_size": 1,
|
||||
"input_len": 4096,
|
||||
"output_len": 512,
|
||||
"latency_ms": 2400.72,
|
||||
"input_throughput": 21408.64,
|
||||
"output_throughput": 231.74,
|
||||
"overall_throughput": 1919.43,
|
||||
"ttft_ms": 191.32,
|
||||
"acc_length": 3.19
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### GitHub Pages
|
||||
|
||||
The dashboard can be deployed to GitHub Pages for public access:
|
||||
|
||||
1. Copy the dashboard files to `docs/performance_dashboard/`
|
||||
2. Enable GitHub Pages in repository settings
|
||||
3. Set up a GitHub Action to periodically update metrics data
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
For a self-hosted deployment with live data:
|
||||
|
||||
1. Set up a server running `server.py`
|
||||
2. Configure a cron job or systemd timer to refresh data
|
||||
3. Optionally put behind nginx/caddy for SSL
|
||||
|
||||
## Metrics Explained
|
||||
|
||||
- **Overall Throughput**: Total tokens (input + output) processed per second
|
||||
- **Input Throughput**: Input tokens processed per second (prefill speed)
|
||||
- **Output Throughput**: Output tokens generated per second (decode speed)
|
||||
- **Latency**: End-to-end time to complete the request
|
||||
- **TTFT**: Time to First Token - time until the first output token
|
||||
- **Acc Length**: Acceptance length for speculative decoding (MTP variants)
|
||||
|
||||
## Contributing
|
||||
|
||||
To add support for new metrics or visualizations:
|
||||
|
||||
1. Update `fetch_metrics.py` if data collection needs changes
|
||||
2. Modify `app.js` to add new chart types or filters
|
||||
3. Update `index.html` for UI changes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No data displayed**
|
||||
- Check browser console for errors
|
||||
- Verify GitHub API is accessible
|
||||
- Try running with `server.py --fetch-on-start`
|
||||
|
||||
**API rate limits**
|
||||
- Use a GitHub token for higher limits
|
||||
- The server caches data for 5 minutes
|
||||
|
||||
**Charts not rendering**
|
||||
- Ensure Chart.js is loading from CDN
|
||||
- Check for JavaScript errors in console
|
||||
1056
third_party/sglang/docs/performance_dashboard/app.js
vendored
Normal file
1056
third_party/sglang/docs/performance_dashboard/app.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
272
third_party/sglang/docs/performance_dashboard/fetch_metrics.py
vendored
Executable file
272
third_party/sglang/docs/performance_dashboard/fetch_metrics.py
vendored
Executable file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fetch and process SGLang nightly test metrics from GitHub Actions artifacts.
|
||||
|
||||
This script fetches consolidated metrics from GitHub Actions workflow runs
|
||||
and outputs them as JSON for the performance dashboard.
|
||||
|
||||
Usage:
|
||||
python fetch_metrics.py --output metrics_data.json
|
||||
python fetch_metrics.py --output metrics_data.json --days 30
|
||||
python fetch_metrics.py --output metrics_data.json --run-id 21338741812
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
GITHUB_REPO = "sgl-project/sglang"
|
||||
WORKFLOW_NAME = "nightly-test-nvidia.yml"
|
||||
ARTIFACT_PREFIX = "consolidated-metrics-"
|
||||
|
||||
|
||||
def get_github_token() -> Optional[str]:
|
||||
"""Get GitHub token from environment or gh CLI."""
|
||||
# Check environment variable first
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
|
||||
# Try gh CLI
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["gh", "auth", "token"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_headers(token: Optional[str]) -> dict:
|
||||
"""Get request headers with optional authentication."""
|
||||
headers = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
return headers
|
||||
|
||||
|
||||
def fetch_workflow_runs(
|
||||
token: Optional[str],
|
||||
days: int = 30,
|
||||
event: Optional[str] = None,
|
||||
) -> list:
|
||||
"""Fetch completed workflow runs from GitHub Actions."""
|
||||
url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/workflows/{WORKFLOW_NAME}/runs"
|
||||
|
||||
params = {
|
||||
"status": "completed",
|
||||
"per_page": 100,
|
||||
}
|
||||
|
||||
if event:
|
||||
params["event"] = event
|
||||
|
||||
response = requests.get(url, headers=get_headers(token), params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
runs = response.json().get("workflow_runs", [])
|
||||
|
||||
# Filter by date
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
runs = [
|
||||
run
|
||||
for run in runs
|
||||
if datetime.fromisoformat(run["created_at"].replace("Z", "+00:00")) > cutoff
|
||||
]
|
||||
|
||||
return runs
|
||||
|
||||
|
||||
def fetch_run_artifacts(token: Optional[str], run_id: int) -> list:
|
||||
"""Fetch artifacts for a specific workflow run."""
|
||||
url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/runs/{run_id}/artifacts"
|
||||
|
||||
response = requests.get(url, headers=get_headers(token), timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json().get("artifacts", [])
|
||||
|
||||
|
||||
def download_artifact(token: Optional[str], artifact_id: int) -> Optional[bytes]:
|
||||
"""Download an artifact by ID."""
|
||||
if not token:
|
||||
print(f"Warning: GitHub token required to download artifacts", file=sys.stderr)
|
||||
return None
|
||||
|
||||
url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/artifacts/{artifact_id}/zip"
|
||||
|
||||
headers = get_headers(token)
|
||||
response = requests.get(url, headers=headers, allow_redirects=True, timeout=60)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.content
|
||||
|
||||
print(
|
||||
f"Failed to download artifact {artifact_id}: {response.status_code}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def extract_metrics_from_zip(zip_content: bytes) -> Optional[dict]:
|
||||
"""Extract metrics JSON from a zip file."""
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(zip_content)) as zf:
|
||||
# Find the JSON file in the archive
|
||||
json_files = [f for f in zf.namelist() if f.endswith(".json")]
|
||||
if not json_files:
|
||||
return None
|
||||
|
||||
with zf.open(json_files[0]) as f:
|
||||
return json.load(f)
|
||||
except (zipfile.BadZipFile, json.JSONDecodeError) as e:
|
||||
print(f"Failed to extract metrics: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def fetch_metrics_for_run(token: Optional[str], run: dict) -> Optional[dict]:
|
||||
"""Fetch metrics for a single workflow run."""
|
||||
run_id = run["id"]
|
||||
print(f"Fetching metrics for run {run_id}...", file=sys.stderr)
|
||||
|
||||
artifacts = fetch_run_artifacts(token, run_id)
|
||||
|
||||
# Find consolidated metrics artifact
|
||||
metrics_artifact = None
|
||||
for artifact in artifacts:
|
||||
if artifact["name"].startswith(ARTIFACT_PREFIX):
|
||||
metrics_artifact = artifact
|
||||
break
|
||||
|
||||
if not metrics_artifact:
|
||||
print(f"No consolidated metrics found for run {run_id}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Download and extract
|
||||
zip_content = download_artifact(token, metrics_artifact["id"])
|
||||
if not zip_content:
|
||||
return None
|
||||
|
||||
metrics = extract_metrics_from_zip(zip_content)
|
||||
if not metrics:
|
||||
return None
|
||||
|
||||
# Ensure required fields are present
|
||||
if "run_id" not in metrics:
|
||||
metrics["run_id"] = str(run_id)
|
||||
if "run_date" not in metrics:
|
||||
metrics["run_date"] = run["created_at"]
|
||||
if "commit_sha" not in metrics:
|
||||
metrics["commit_sha"] = run["head_sha"]
|
||||
if "branch" not in metrics:
|
||||
metrics["branch"] = run["head_branch"]
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def fetch_single_run(token: Optional[str], run_id: int) -> Optional[dict]:
|
||||
"""Fetch metrics for a single run by ID."""
|
||||
url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/runs/{run_id}"
|
||||
|
||||
response = requests.get(url, headers=get_headers(token), timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
run = response.json()
|
||||
return fetch_metrics_for_run(token, run)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Fetch SGLang nightly test metrics from GitHub Actions"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=str,
|
||||
default="metrics_data.json",
|
||||
help="Output JSON file path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--days",
|
||||
type=int,
|
||||
default=30,
|
||||
help="Number of days to fetch (default: 30)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run-id",
|
||||
type=int,
|
||||
help="Fetch a specific run by ID",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--event",
|
||||
type=str,
|
||||
choices=["schedule", "workflow_dispatch", "push"],
|
||||
help="Filter by trigger event type",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scheduled-only",
|
||||
action="store_true",
|
||||
help="Only fetch scheduled (nightly) runs",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
token = get_github_token()
|
||||
if not token:
|
||||
print(
|
||||
"Warning: No GitHub token found. Some features may be limited.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"Set GITHUB_TOKEN env var or login with 'gh auth login'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
all_metrics = []
|
||||
|
||||
if args.run_id:
|
||||
# Fetch single run
|
||||
metrics = fetch_single_run(token, args.run_id)
|
||||
if metrics:
|
||||
all_metrics.append(metrics)
|
||||
else:
|
||||
# Fetch multiple runs
|
||||
event = "schedule" if args.scheduled_only else args.event
|
||||
runs = fetch_workflow_runs(token, days=args.days, event=event)
|
||||
print(f"Found {len(runs)} workflow runs", file=sys.stderr)
|
||||
|
||||
for run in runs:
|
||||
metrics = fetch_metrics_for_run(token, run)
|
||||
if metrics:
|
||||
all_metrics.append(metrics)
|
||||
|
||||
# Sort by date descending
|
||||
all_metrics.sort(key=lambda x: x.get("run_date", ""), reverse=True)
|
||||
|
||||
# Write output
|
||||
output_path = Path(args.output)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(all_metrics, f, indent=2)
|
||||
|
||||
print(f"Wrote {len(all_metrics)} metrics records to {output_path}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
946
third_party/sglang/docs/performance_dashboard/index.html
vendored
Normal file
946
third_party/sglang/docs/performance_dashboard/index.html
vendored
Normal file
@@ -0,0 +1,946 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SGLang Performance Dashboard</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0a0e17;
|
||||
--bg-secondary: #111827;
|
||||
--bg-tertiary: #1a2332;
|
||||
--bg-elevated: #1e293b;
|
||||
--text-primary: #e2e8f0;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--border-color: #1e293b;
|
||||
--border-subtle: rgba(148, 163, 184, 0.08);
|
||||
--accent-cyan: #22d3ee;
|
||||
--accent-cyan-dim: rgba(34, 211, 238, 0.15);
|
||||
--accent-green: #34d399;
|
||||
--accent-green-dim: rgba(52, 211, 153, 0.15);
|
||||
--accent-amber: #fbbf24;
|
||||
--accent-amber-dim: rgba(251, 191, 36, 0.15);
|
||||
--accent-red: #f87171;
|
||||
--accent-red-dim: rgba(248, 113, 113, 0.15);
|
||||
--accent-violet: #a78bfa;
|
||||
--accent-violet-dim: rgba(167, 139, 250, 0.15);
|
||||
--glass-bg: rgba(17, 24, 39, 0.7);
|
||||
--glass-border: rgba(148, 163, 184, 0.1);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
--radius-xl: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Subtle grid background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Ambient glow */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -40%;
|
||||
left: -20%;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
background: radial-gradient(ellipse, rgba(34, 211, 238, 0.04) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1480px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 32px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ---- Header ---- */
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0 24px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.logo-mark {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--accent-cyan-dim), rgba(167, 139, 250, 0.12));
|
||||
border: 1px solid rgba(34, 211, 238, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-mark svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
h1 span.title-accent {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 18px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--glass-border);
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, 0.15), rgba(34, 211, 238, 0.08));
|
||||
border-color: rgba(34, 211, 238, 0.25);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, 0.25), rgba(34, 211, 238, 0.12));
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
}
|
||||
|
||||
/* ---- Stats Row ---- */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 20px 22px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
}
|
||||
|
||||
.stat-card:nth-child(1)::before { background: linear-gradient(90deg, var(--accent-cyan), transparent); }
|
||||
.stat-card:nth-child(2)::before { background: linear-gradient(90deg, var(--accent-violet), transparent); }
|
||||
.stat-card:nth-child(3)::before { background: linear-gradient(90deg, var(--accent-green), transparent); }
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.stat-card .change {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-card .change.positive { color: var(--accent-green); }
|
||||
.stat-card .change.negative { color: var(--accent-red); }
|
||||
|
||||
/* ---- Filters ---- */
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 28px;
|
||||
padding: 18px 22px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--glass-border);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 9px 32px 9px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.08);
|
||||
}
|
||||
|
||||
/* ---- Metric Tabs ---- */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 24px;
|
||||
padding: 4px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-subtle);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 9px 18px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text-secondary);
|
||||
background: rgba(148, 163, 184, 0.05);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-cyan);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* ---- Chart Cards ---- */
|
||||
.chart-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.chart-card h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.chart-card h3::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
background: var(--accent-cyan);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.metric-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.batch-charts-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.batch-chart-wrapper {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.batch-chart-title {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* ---- Data Table ---- */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: rgba(148, 163, 184, 0.04);
|
||||
}
|
||||
|
||||
.data-table td code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
background: var(--accent-cyan-dim);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.run-link {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.run-link:hover {
|
||||
color: #67e8f9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.model-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: var(--accent-violet-dim);
|
||||
color: var(--accent-violet);
|
||||
border: 1px solid rgba(167, 139, 250, 0.15);
|
||||
}
|
||||
|
||||
/* ---- Loading ---- */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
gap: 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top-color: var(--accent-cyan);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ---- Error ---- */
|
||||
.error {
|
||||
background: var(--accent-red-dim);
|
||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 28px;
|
||||
text-align: center;
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.error h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error p {
|
||||
font-size: 13px;
|
||||
color: rgba(248, 113, 113, 0.8);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-data h3 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ---- Footer ---- */
|
||||
footer {
|
||||
margin-top: 48px;
|
||||
padding: 28px 0;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* ---- Login Overlay ---- */
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: var(--bg-primary);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-overlay::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-overlay::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(ellipse, rgba(34, 211, 238, 0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 44px 40px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: loginSlideUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes loginSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(135deg, var(--accent-cyan-dim), rgba(167, 139, 250, 0.12));
|
||||
border: 1px solid rgba(34, 211, 238, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-icon-wrapper svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.login-card h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.login-card .login-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-card .form-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-card .form-group label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 7px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-card .form-group input {
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.login-card .form-group input:focus {
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.08);
|
||||
}
|
||||
|
||||
.login-card .form-group input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.login-card .login-btn {
|
||||
width: 100%;
|
||||
padding: 11px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid rgba(34, 211, 238, 0.3);
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, 0.15), rgba(34, 211, 238, 0.08));
|
||||
color: var(--accent-cyan);
|
||||
font-size: 14px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.login-card .login-btn:hover {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, 0.25), rgba(34, 211, 238, 0.12));
|
||||
border-color: rgba(34, 211, 238, 0.5);
|
||||
}
|
||||
|
||||
.login-card .login-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: var(--accent-red);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin-top: 14px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
/* ---- Entrance Animations ---- */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeInUp 0.4s ease-out both;
|
||||
}
|
||||
|
||||
.animate-delay-1 { animation-delay: 0.05s; }
|
||||
.animate-delay-2 { animation-delay: 0.1s; }
|
||||
.animate-delay-3 { animation-delay: 0.15s; }
|
||||
.animate-delay-4 { animation-delay: 0.2s; }
|
||||
.animate-delay-5 { animation-delay: 0.25s; }
|
||||
.animate-delay-6 { animation-delay: 0.3s; }
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.batch-charts-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: 16px;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Scrollbar ---- */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Login overlay -->
|
||||
<div id="login-overlay" class="login-overlay">
|
||||
<div class="login-card">
|
||||
<div class="login-icon">
|
||||
<div class="login-icon-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="16" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h2>SGLang Performance Dashboard</h2>
|
||||
<p class="login-subtitle">Enter your credentials to access the dashboard</p>
|
||||
<form id="login-form" onsubmit="return handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="login-username">Username</label>
|
||||
<input type="text" id="login-username" name="username" autocomplete="username" placeholder="Enter username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password">Password</label>
|
||||
<input type="password" id="login-password" name="password" autocomplete="current-password" placeholder="Enter password" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn" id="login-btn">Sign In</button>
|
||||
</form>
|
||||
<div id="login-error" class="login-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" id="dashboard-container" style="display: none;">
|
||||
<header class="animate-in">
|
||||
<h1>
|
||||
<div class="logo-mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span><span class="title-accent">SGLang</span> Performance Dashboard</span>
|
||||
</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn" onclick="refreshData()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
Refresh
|
||||
</button>
|
||||
<a href="https://github.com/sgl-project/sglang/actions/workflows/nightly-test-nvidia.yml?query=event%3Aschedule" target="_blank" class="btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
Workflow
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">Loading performance data...</div>
|
||||
</div>
|
||||
|
||||
<div id="content" style="display: none;">
|
||||
<div class="stats-row animate-in animate-delay-1" id="stats-row"></div>
|
||||
|
||||
<div class="filters animate-in animate-delay-2">
|
||||
<div class="filter-group">
|
||||
<label>GPU Configuration</label>
|
||||
<select id="gpu-filter" onchange="handleGpuFilterChange()">
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Model</label>
|
||||
<select id="model-filter" onchange="handleModelFilterChange(this.value)">
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Variant</label>
|
||||
<select id="variant-filter" onchange="updateCharts()">
|
||||
<option value="all">All Variants</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Input / Output Length</label>
|
||||
<select id="io-len-filter" onchange="updateCharts()">
|
||||
<option value="all">All Lengths</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Batch Size</label>
|
||||
<select id="batch-filter" onchange="updateCharts()">
|
||||
<option value="all">All Batch Sizes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs animate-in animate-delay-3" id="metric-tabs"></div>
|
||||
|
||||
<div class="metric-section animate-in animate-delay-4">
|
||||
<div class="chart-card">
|
||||
<h3 id="metric-title">Overall Throughput (tokens/sec)</h3>
|
||||
<div class="batch-charts-container" id="charts-container">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card animate-in animate-delay-5" style="margin-top: 24px;">
|
||||
<h3>Recent Benchmark Runs</h3>
|
||||
<table class="data-table" id="runs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Run ID</th>
|
||||
<th>Commit</th>
|
||||
<th>Branch</th>
|
||||
<th>Models Tested</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="runs-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error" class="error" style="display: none;">
|
||||
<h3>Failed to load performance data</h3>
|
||||
<p id="error-message"></p>
|
||||
</div>
|
||||
|
||||
<footer class="animate-in animate-delay-6">
|
||||
<p>
|
||||
SGLang Performance Dashboard —
|
||||
<a href="https://github.com/sgl-project/sglang" target="_blank">GitHub</a> ·
|
||||
<a href="https://docs.sglang.io/" target="_blank">Documentation</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
422
third_party/sglang/docs/performance_dashboard/server.py
vendored
Executable file
422
third_party/sglang/docs/performance_dashboard/server.py
vendored
Executable file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple development server for the SGLang Performance Dashboard.
|
||||
|
||||
This server:
|
||||
1. Serves the static HTML/JS files
|
||||
2. Provides an API endpoint to fetch metrics from GitHub
|
||||
3. Caches metrics data to reduce API calls
|
||||
|
||||
Usage:
|
||||
python server.py
|
||||
python server.py --port 8080
|
||||
python server.py --host 0.0.0.0 # Allow external access
|
||||
python server.py --fetch-on-start
|
||||
python server.py --username admin --password secret # Enable authentication
|
||||
DASHBOARD_USERNAME=admin DASHBOARD_PASSWORD=secret python server.py # Via env vars
|
||||
python server.py --refresh-interval 12 # Auto-refresh data every 12 hours
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import http.server
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import socketserver
|
||||
import threading
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
GITHUB_REPO = "sgl-project/sglang"
|
||||
WORKFLOW_NAME = "nightly-test-nvidia.yml"
|
||||
ARTIFACT_PREFIX = "consolidated-metrics-"
|
||||
|
||||
# Cache for metrics data with thread-safe lock
|
||||
cache_lock = threading.Lock()
|
||||
metrics_cache = {
|
||||
"data": [],
|
||||
"last_updated": None,
|
||||
"updating": False,
|
||||
}
|
||||
|
||||
CACHE_TTL = 300 # 5 minutes
|
||||
REQUEST_TIMEOUT = 30 # seconds
|
||||
|
||||
# Authentication configuration (set via CLI flags)
|
||||
auth_config = {
|
||||
"enabled": False,
|
||||
"username": None,
|
||||
"password_hash": None, # SHA-256 hash of the password
|
||||
"active_tokens": {}, # token -> expiry timestamp
|
||||
}
|
||||
auth_lock = threading.Lock()
|
||||
AUTH_TOKEN_TTL = 3600 # 1 hour
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
"""Hash a password using SHA-256 for constant-time comparison."""
|
||||
return hashlib.sha256(password.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def create_auth_token():
|
||||
"""Create a new session token."""
|
||||
token = secrets.token_hex(32)
|
||||
with auth_lock:
|
||||
# Clean up expired tokens
|
||||
now = time.time()
|
||||
auth_config["active_tokens"] = {
|
||||
t: exp for t, exp in auth_config["active_tokens"].items() if exp > now
|
||||
}
|
||||
auth_config["active_tokens"][token] = now + AUTH_TOKEN_TTL
|
||||
return token
|
||||
|
||||
|
||||
def verify_auth_token(token):
|
||||
"""Verify a session token is valid and not expired."""
|
||||
if not token:
|
||||
return False
|
||||
with auth_lock:
|
||||
expiry = auth_config["active_tokens"].get(token)
|
||||
if expiry and expiry > time.time():
|
||||
return True
|
||||
# Remove expired token
|
||||
auth_config["active_tokens"].pop(token, None)
|
||||
return False
|
||||
|
||||
|
||||
def get_github_token():
|
||||
"""Get GitHub token from environment or gh CLI."""
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["gh", "auth", "token"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def fetch_metrics_from_github(days=30):
|
||||
"""Fetch metrics from GitHub Actions artifacts."""
|
||||
token = get_github_token()
|
||||
headers = {"Accept": "application/vnd.github.v3+json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
# Get workflow runs - only scheduled (nightly) runs, not workflow_dispatch
|
||||
url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/workflows/{WORKFLOW_NAME}/runs"
|
||||
params = {"status": "completed", "per_page": 50, "event": "schedule"}
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
url, headers=headers, params=params, timeout=REQUEST_TIMEOUT
|
||||
)
|
||||
if not response.ok:
|
||||
print(f"Failed to fetch workflow runs: {response.status_code}")
|
||||
return []
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Network error fetching workflow runs: {e}")
|
||||
return []
|
||||
|
||||
runs = response.json().get("workflow_runs", [])
|
||||
|
||||
# Filter by date
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
runs = [
|
||||
run
|
||||
for run in runs
|
||||
if datetime.fromisoformat(run["created_at"].replace("Z", "+00:00")) > cutoff
|
||||
]
|
||||
|
||||
all_metrics = []
|
||||
|
||||
for run in runs[:20]: # Limit to 20 most recent
|
||||
run_id = run["id"]
|
||||
|
||||
# Get artifacts
|
||||
artifacts_url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/runs/{run_id}/artifacts"
|
||||
try:
|
||||
artifacts_resp = requests.get(
|
||||
artifacts_url, headers=headers, timeout=REQUEST_TIMEOUT
|
||||
)
|
||||
if not artifacts_resp.ok:
|
||||
continue
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Network error fetching artifacts for run {run_id}: {e}")
|
||||
continue
|
||||
|
||||
artifacts = artifacts_resp.json().get("artifacts", [])
|
||||
|
||||
# Find consolidated metrics
|
||||
for artifact in artifacts:
|
||||
if artifact["name"].startswith(ARTIFACT_PREFIX):
|
||||
if not token:
|
||||
# Without token, we can't download - return metadata only
|
||||
all_metrics.append(
|
||||
{
|
||||
"run_id": str(run_id),
|
||||
"run_date": run["created_at"],
|
||||
"commit_sha": run["head_sha"],
|
||||
"branch": run["head_branch"],
|
||||
"results": [],
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
# Download artifact
|
||||
download_url = f"https://api.github.com/repos/{GITHUB_REPO}/actions/artifacts/{artifact['id']}/zip"
|
||||
try:
|
||||
download_resp = requests.get(
|
||||
download_url,
|
||||
headers=headers,
|
||||
allow_redirects=True,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Network error downloading artifact: {e}")
|
||||
break
|
||||
|
||||
if download_resp.ok:
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(download_resp.content)) as zf:
|
||||
json_files = [
|
||||
f for f in zf.namelist() if f.endswith(".json")
|
||||
]
|
||||
if json_files:
|
||||
with zf.open(json_files[0]) as f:
|
||||
metrics = json.load(f)
|
||||
# Ensure required fields
|
||||
metrics.setdefault("run_id", str(run_id))
|
||||
metrics.setdefault("run_date", run["created_at"])
|
||||
metrics.setdefault("commit_sha", run["head_sha"])
|
||||
metrics.setdefault("branch", run["head_branch"])
|
||||
all_metrics.append(metrics)
|
||||
except (zipfile.BadZipFile, json.JSONDecodeError) as e:
|
||||
print(f"Failed to process artifact: {e}")
|
||||
break
|
||||
|
||||
return all_metrics
|
||||
|
||||
|
||||
def update_cache_async():
|
||||
"""Update the metrics cache in background with thread safety."""
|
||||
with cache_lock:
|
||||
if metrics_cache["updating"]:
|
||||
return
|
||||
metrics_cache["updating"] = True
|
||||
|
||||
try:
|
||||
data = fetch_metrics_from_github()
|
||||
with cache_lock:
|
||||
metrics_cache["data"] = data
|
||||
metrics_cache["last_updated"] = time.time()
|
||||
print(f"Cache updated with {len(data)} metrics records")
|
||||
finally:
|
||||
with cache_lock:
|
||||
metrics_cache["updating"] = False
|
||||
|
||||
|
||||
def start_periodic_refresh(interval_hours):
|
||||
"""Start a background thread that refreshes the cache periodically."""
|
||||
interval_seconds = interval_hours * 3600
|
||||
|
||||
def refresh_loop():
|
||||
while True:
|
||||
time.sleep(interval_seconds)
|
||||
print(f"Periodic refresh triggered (every {interval_hours}h)")
|
||||
update_cache_async()
|
||||
|
||||
thread = threading.Thread(target=refresh_loop, daemon=True)
|
||||
thread.start()
|
||||
print(f"Periodic refresh enabled: every {interval_hours} hours")
|
||||
|
||||
|
||||
class DashboardHandler(http.server.SimpleHTTPRequestHandler):
|
||||
"""HTTP request handler for the dashboard."""
|
||||
|
||||
def __init__(self, *args, directory=None, **kwargs):
|
||||
super().__init__(*args, directory=directory, **kwargs)
|
||||
|
||||
def _send_json(self, data, status=200):
|
||||
"""Send a JSON response."""
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def _check_auth(self):
|
||||
"""Check if request is authenticated. Returns True if OK, sends 401 and returns False otherwise."""
|
||||
if not auth_config["enabled"]:
|
||||
return True
|
||||
auth_header = self.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
if verify_auth_token(token):
|
||||
return True
|
||||
self._send_json({"error": "Unauthorized"}, status=401)
|
||||
return False
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
|
||||
# Prevent directory traversal attacks
|
||||
if ".." in parsed.path or parsed.path.startswith("//"):
|
||||
self.send_error(400, "Invalid path")
|
||||
return
|
||||
|
||||
if parsed.path == "/api/auth-check":
|
||||
self.handle_auth_check()
|
||||
elif parsed.path == "/api/metrics":
|
||||
if self._check_auth():
|
||||
self.handle_metrics_api(parsed)
|
||||
elif parsed.path == "/api/refresh":
|
||||
if self._check_auth():
|
||||
self.handle_refresh_api()
|
||||
else:
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urlparse(self.path)
|
||||
|
||||
if parsed.path == "/api/login":
|
||||
self.handle_login()
|
||||
else:
|
||||
self.send_error(404, "Not Found")
|
||||
|
||||
def handle_auth_check(self):
|
||||
"""Tell the frontend whether authentication is required."""
|
||||
self._send_json({"auth_required": auth_config["enabled"]})
|
||||
|
||||
def handle_login(self):
|
||||
"""Validate username/password and return a session token."""
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
if content_length == 0 or content_length > 4096:
|
||||
self._send_json({"error": "Invalid request"}, status=400)
|
||||
return
|
||||
|
||||
try:
|
||||
body = json.loads(self.rfile.read(content_length))
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
self._send_json({"error": "Invalid JSON"}, status=400)
|
||||
return
|
||||
|
||||
username = body.get("username", "")
|
||||
password = body.get("password", "")
|
||||
|
||||
if hmac.compare_digest(
|
||||
username, auth_config["username"]
|
||||
) and hmac.compare_digest(
|
||||
hash_password(password), auth_config["password_hash"]
|
||||
):
|
||||
token = create_auth_token()
|
||||
self._send_json({"token": token})
|
||||
else:
|
||||
self._send_json({"error": "Invalid username or password"}, status=401)
|
||||
|
||||
def handle_metrics_api(self, parsed):
|
||||
"""Handle /api/metrics endpoint."""
|
||||
# Check cache with thread safety
|
||||
with cache_lock:
|
||||
cache_valid = (
|
||||
metrics_cache["last_updated"]
|
||||
and time.time() - metrics_cache["last_updated"] < CACHE_TTL
|
||||
)
|
||||
data = metrics_cache["data"].copy()
|
||||
|
||||
if not cache_valid:
|
||||
# Trigger background update
|
||||
threading.Thread(target=update_cache_async, daemon=True).start()
|
||||
|
||||
self._send_json(data)
|
||||
|
||||
def handle_refresh_api(self):
|
||||
"""Handle /api/refresh endpoint."""
|
||||
threading.Thread(target=update_cache_async, daemon=True).start()
|
||||
self._send_json({"status": "refreshing"})
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Custom log format."""
|
||||
print(f"[{self.log_date_time_string()}] {args[0]}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SGLang Performance Dashboard Server")
|
||||
parser.add_argument("--port", type=int, default=8000, help="Port to serve on")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="Host to bind to (use 0.0.0.0 for external access)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fetch-on-start", action="store_true", help="Fetch metrics on startup"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--refresh-interval",
|
||||
type=float,
|
||||
default=12,
|
||||
help="Auto-refresh interval in hours (default: 12, set to 0 to disable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--username",
|
||||
default=os.environ.get("DASHBOARD_USERNAME"),
|
||||
help="Username for dashboard authentication (or set DASHBOARD_USERNAME env var)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password",
|
||||
default=os.environ.get("DASHBOARD_PASSWORD"),
|
||||
help="Password for dashboard authentication (or set DASHBOARD_PASSWORD env var)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Configure authentication if both username and password are provided
|
||||
if args.username and args.password:
|
||||
auth_config["enabled"] = True
|
||||
auth_config["username"] = args.username
|
||||
auth_config["password_hash"] = hash_password(args.password)
|
||||
print(f"Authentication enabled for user: {args.username}")
|
||||
elif args.username or args.password:
|
||||
parser.error("Both --username and --password must be provided together")
|
||||
|
||||
# Change to dashboard directory
|
||||
dashboard_dir = Path(__file__).parent
|
||||
os.chdir(dashboard_dir)
|
||||
|
||||
if args.fetch_on_start:
|
||||
print("Fetching initial metrics data...")
|
||||
update_cache_async()
|
||||
|
||||
if args.refresh_interval > 0:
|
||||
start_periodic_refresh(args.refresh_interval)
|
||||
|
||||
handler = lambda *a, **kw: DashboardHandler(*a, directory=str(dashboard_dir), **kw)
|
||||
|
||||
with socketserver.TCPServer((args.host, args.port), handler) as httpd:
|
||||
print(f"Serving dashboard at http://{args.host}:{args.port}")
|
||||
print("Press Ctrl+C to stop")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user