fix: adding performance logging for nightly diffusion (#18023)
This commit is contained in:
44
.github/workflows/nightly-test-nvidia.yml
vendored
44
.github/workflows/nightly-test-nvidia.yml
vendored
@@ -389,6 +389,7 @@ jobs:
|
||||
env:
|
||||
SGLANG_DIFFUSION_SLACK_TOKEN: ${{ secrets.SGLANG_DIFFUSION_SLACK_TOKEN }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GPU_CONFIG: "1-gpu-runner"
|
||||
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
@@ -398,6 +399,24 @@ jobs:
|
||||
--partition-id ${{ matrix.part }} \
|
||||
--total-partitions 2
|
||||
|
||||
- name: Collect diffusion performance metrics
|
||||
if: always()
|
||||
run: |
|
||||
python3 scripts/ci/save_diffusion_metrics.py \
|
||||
--gpu-config 1-gpu-runner \
|
||||
--run-id ${{ github.run_id }} \
|
||||
--output python/diffusion-metrics-1gpu-partition-${{ matrix.part }}.json \
|
||||
--results-json python/diffusion-results.json
|
||||
|
||||
- name: Upload diffusion metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: diffusion-metrics-1gpu-partition-${{ matrix.part }}
|
||||
path: python/diffusion-metrics-1gpu-partition-${{ matrix.part }}.json
|
||||
retention-days: 90
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
||||
nightly-test-multimodal-server-2-gpu:
|
||||
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-multimodal-server-2-gpu')
|
||||
@@ -422,6 +441,7 @@ jobs:
|
||||
env:
|
||||
SGLANG_DIFFUSION_SLACK_TOKEN: ${{ secrets.SGLANG_DIFFUSION_SLACK_TOKEN }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GPU_CONFIG: "2-gpu-runner"
|
||||
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
@@ -431,6 +451,24 @@ jobs:
|
||||
--partition-id ${{ matrix.part }} \
|
||||
--total-partitions 2
|
||||
|
||||
- name: Collect diffusion performance metrics
|
||||
if: always()
|
||||
run: |
|
||||
python3 scripts/ci/save_diffusion_metrics.py \
|
||||
--gpu-config 2-gpu-runner \
|
||||
--run-id ${{ github.run_id }} \
|
||||
--output python/diffusion-metrics-2gpu-partition-${{ matrix.part }}.json \
|
||||
--results-json python/diffusion-results.json
|
||||
|
||||
- name: Upload diffusion metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: diffusion-metrics-2gpu-partition-${{ matrix.part }}
|
||||
path: python/diffusion-metrics-2gpu-partition-${{ matrix.part }}.json
|
||||
retention-days: 90
|
||||
if-no-files-found: ignore
|
||||
|
||||
# B200 Performance tests - 4 GPU
|
||||
nightly-test-perf-4-gpu-b200:
|
||||
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-4-gpu-b200')
|
||||
@@ -475,12 +513,14 @@ jobs:
|
||||
cd test
|
||||
python3 run_suite.py --hw cuda --suite nightly-8-gpu-b200 --nightly --continue-on-error --timeout-per-file 2400
|
||||
|
||||
# Consolidate performance metrics from all 8-GPU jobs
|
||||
# Consolidate performance metrics from all jobs
|
||||
consolidate-metrics:
|
||||
if: github.repository == 'sgl-project/sglang' && always()
|
||||
needs:
|
||||
- nightly-test-general-8-gpu-h200
|
||||
- nightly-test-general-8-gpu-b200
|
||||
- nightly-test-multimodal-server-1-gpu
|
||||
- nightly-test-multimodal-server-2-gpu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -491,7 +531,7 @@ jobs:
|
||||
- name: Download all partition metrics
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: metrics-*
|
||||
pattern: "*metrics-*"
|
||||
path: metrics/
|
||||
merge-multiple: true
|
||||
|
||||
|
||||
@@ -14,12 +14,17 @@ let currentMetricType = 'throughput'; // throughput, latency, ttft, inputThrough
|
||||
|
||||
// Metric type definitions
|
||||
const metricTypes = {
|
||||
throughput: { label: 'Overall Throughput', unit: 'tokens/sec', field: 'throughput' },
|
||||
outputThroughput: { label: 'Output Throughput', unit: 'tokens/sec', field: 'outputThroughput' },
|
||||
inputThroughput: { label: 'Input Throughput', unit: 'tokens/sec', field: 'inputThroughput' },
|
||||
latency: { label: 'Latency', unit: 'ms', field: 'latency' },
|
||||
ttft: { label: 'Time to First Token', unit: 'ms', field: 'ttft' },
|
||||
accLength: { label: 'Accept Length', unit: 'tokens', field: 'accLength', filterInvalid: true }
|
||||
// Text/VLM metrics
|
||||
throughput: { label: 'Overall Throughput', unit: 'tokens/sec', field: 'throughput', type: 'text' },
|
||||
outputThroughput: { label: 'Output Throughput', unit: 'tokens/sec', field: 'outputThroughput', type: 'text' },
|
||||
inputThroughput: { label: 'Input Throughput', unit: 'tokens/sec', field: 'inputThroughput', type: 'text' },
|
||||
latency: { label: 'Latency', unit: 'ms', field: 'latency', type: 'text' },
|
||||
ttft: { label: 'Time to First Token', unit: 'ms', field: 'ttft', type: 'text' },
|
||||
accLength: { label: 'Accept Length', unit: 'tokens', field: 'accLength', filterInvalid: true, type: 'text' },
|
||||
// Diffusion metrics
|
||||
e2eMs: { label: 'End-to-End Time', unit: 'ms', field: 'e2e_ms', type: 'diffusion' },
|
||||
avgDenoiseMs: { label: 'Avg Denoise Time', unit: 'ms', field: 'avg_denoise_ms', type: 'diffusion' },
|
||||
medianDenoiseMs: { label: 'Median Denoise Time', unit: 'ms', field: 'median_denoise_ms', type: 'diffusion' }
|
||||
};
|
||||
|
||||
// Chart.js default configuration for dark theme
|
||||
@@ -142,32 +147,51 @@ async function fetchMetricsForRun(run) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to detect if result is diffusion type
|
||||
function isDiffusionResult(result) {
|
||||
return result.test_type === 'diffusion' || (result.tests && !result.benchmarks);
|
||||
}
|
||||
|
||||
// Populate filter dropdowns
|
||||
function populateFilters() {
|
||||
const gpuConfigs = new Set();
|
||||
const models = new Set();
|
||||
const testNames = new Set(); // For diffusion tests
|
||||
const batchSizes = new Set();
|
||||
const ioLengths = new Set();
|
||||
|
||||
allMetricsData.forEach(run => {
|
||||
run.results.forEach(result => {
|
||||
gpuConfigs.add(result.gpu_config);
|
||||
models.add(result.model);
|
||||
// Try new structure first (benchmarks_by_io_len), fall back to flat benchmarks
|
||||
if (result.benchmarks_by_io_len) {
|
||||
Object.entries(result.benchmarks_by_io_len).forEach(([ioKey, ioData]) => {
|
||||
ioLengths.add(ioKey);
|
||||
ioData.benchmarks.forEach(bench => {
|
||||
batchSizes.add(bench.batch_size);
|
||||
|
||||
// Handle diffusion results
|
||||
if (isDiffusionResult(result)) {
|
||||
models.add(result.test_suite || 'diffusion');
|
||||
if (result.tests) {
|
||||
result.tests.forEach(test => {
|
||||
testNames.add(test.test_name);
|
||||
});
|
||||
});
|
||||
} else if (result.benchmarks) {
|
||||
result.benchmarks.forEach(bench => {
|
||||
batchSizes.add(bench.batch_size);
|
||||
if (bench.input_len && bench.output_len) {
|
||||
ioLengths.add(`${bench.input_len}_${bench.output_len}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Handle text/VLM results
|
||||
else {
|
||||
models.add(result.model);
|
||||
// Try new structure first (benchmarks_by_io_len), fall back to flat benchmarks
|
||||
if (result.benchmarks_by_io_len) {
|
||||
Object.entries(result.benchmarks_by_io_len).forEach(([ioKey, ioData]) => {
|
||||
ioLengths.add(ioKey);
|
||||
ioData.benchmarks.forEach(bench => {
|
||||
batchSizes.add(bench.batch_size);
|
||||
});
|
||||
});
|
||||
} else if (result.benchmarks) {
|
||||
result.benchmarks.forEach(bench => {
|
||||
batchSizes.add(bench.batch_size);
|
||||
if (bench.input_len && bench.output_len) {
|
||||
ioLengths.add(`${bench.input_len}_${bench.output_len}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -345,7 +369,16 @@ function createMetricTabs() {
|
||||
const tabsContainer = document.getElementById('metric-tabs');
|
||||
tabsContainer.innerHTML = '';
|
||||
|
||||
Object.entries(metricTypes).forEach(([key, metric], index) => {
|
||||
// Detect if current data is diffusion or text
|
||||
const isDiffusion = detectCurrentDataType() === 'diffusion';
|
||||
const dataType = isDiffusion ? 'diffusion' : 'text';
|
||||
|
||||
// Filter metrics based on data type
|
||||
const relevantMetrics = Object.entries(metricTypes).filter(([key, metric]) =>
|
||||
metric.type === dataType
|
||||
);
|
||||
|
||||
relevantMetrics.forEach(([key, metric], index) => {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = index === 0 ? 'tab active' : 'tab';
|
||||
tab.textContent = metric.label;
|
||||
@@ -353,6 +386,31 @@ function createMetricTabs() {
|
||||
tab.onclick = () => selectMetricTab(key, tab);
|
||||
tabsContainer.appendChild(tab);
|
||||
});
|
||||
|
||||
// Set initial metric type
|
||||
if (relevantMetrics.length > 0) {
|
||||
currentMetricType = relevantMetrics[0][0];
|
||||
}
|
||||
}
|
||||
|
||||
function detectCurrentDataType() {
|
||||
// Check if currently selected model/GPU config has diffusion data
|
||||
const gpuFilter = document.getElementById('gpu-filter')?.value;
|
||||
const modelFilter = currentModel;
|
||||
|
||||
if (!gpuFilter || !modelFilter) return 'text';
|
||||
|
||||
for (const run of allMetricsData) {
|
||||
for (const result of run.results) {
|
||||
if (result.gpu_config === gpuFilter) {
|
||||
const resultModel = result.test_suite || result.model;
|
||||
if (resultModel === modelFilter && isDiffusionResult(result)) {
|
||||
return 'diffusion';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'text';
|
||||
}
|
||||
|
||||
function selectMetricTab(metricKey, tabElement) {
|
||||
@@ -374,6 +432,8 @@ function handleModelFilterChange(model) {
|
||||
updateVariantFilter();
|
||||
// Update IO length filter based on new model selection
|
||||
updateIoLenFilter();
|
||||
// Recreate metric tabs in case data type changed (text vs diffusion)
|
||||
createMetricTabs();
|
||||
updateCharts();
|
||||
}
|
||||
|
||||
@@ -383,6 +443,8 @@ function handleGpuFilterChange() {
|
||||
updateVariantFilter();
|
||||
// Update IO length filter based on new GPU selection
|
||||
updateIoLenFilter();
|
||||
// Recreate metric tabs in case data type changed (text vs diffusion)
|
||||
createMetricTabs();
|
||||
updateCharts();
|
||||
}
|
||||
|
||||
@@ -518,6 +580,7 @@ function prepareChartData(gpuFilter, modelFilter, variantFilter, ioLenFilter, ba
|
||||
// Prepare chart data grouped by batch size - each batch size is a separate series
|
||||
function prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFilter, batchFilter) {
|
||||
const batchDataMap = new Map(); // batch_size -> Map of variant -> data
|
||||
const testDataMap = new Map(); // For diffusion: test_name -> data
|
||||
|
||||
allMetricsData.forEach(run => {
|
||||
const runDate = new Date(run.run_date);
|
||||
@@ -525,6 +588,37 @@ function prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFil
|
||||
run.results.forEach(result => {
|
||||
// Apply filters - GPU and Model are required (no "all" option)
|
||||
if (result.gpu_config !== gpuFilter) return;
|
||||
|
||||
// Handle diffusion results
|
||||
if (isDiffusionResult(result)) {
|
||||
const resultModel = result.test_suite || 'diffusion';
|
||||
if (resultModel !== modelFilter) return;
|
||||
|
||||
if (result.tests) {
|
||||
result.tests.forEach(test => {
|
||||
const testName = test.test_name;
|
||||
if (!testDataMap.has(testName)) {
|
||||
testDataMap.set(testName, {
|
||||
label: testName,
|
||||
data: [],
|
||||
model: resultModel,
|
||||
testName: testName
|
||||
});
|
||||
}
|
||||
|
||||
testDataMap.get(testName).data.push({
|
||||
x: runDate,
|
||||
e2e_ms: test.e2e_ms,
|
||||
avg_denoise_ms: test.avg_denoise_ms,
|
||||
median_denoise_ms: test.median_denoise_ms,
|
||||
runId: run.run_id
|
||||
});
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle text/VLM results
|
||||
if (result.model !== modelFilter) return;
|
||||
if (variantFilter !== 'all' && result.variant !== variantFilter) return;
|
||||
|
||||
@@ -622,6 +716,17 @@ function prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFil
|
||||
|
||||
// Sort data points by date and convert to array format
|
||||
const result = {};
|
||||
|
||||
// For diffusion data, use test names as "batch sizes"
|
||||
if (testDataMap.size > 0) {
|
||||
testDataMap.forEach((series, testName) => {
|
||||
series.data.sort((a, b) => a.x - b.x);
|
||||
result[testName] = [series]; // Each test is its own series
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// For text/VLM data, use batch sizes
|
||||
batchDataMap.forEach((variantMap, batchSize) => {
|
||||
variantMap.forEach(series => {
|
||||
series.data.sort((a, b) => a.x - b.x);
|
||||
@@ -642,7 +747,16 @@ function updateMetricChart(chartDataByBatch, metricType) {
|
||||
activeCharts = [];
|
||||
|
||||
const metric = metricTypes[metricType];
|
||||
const batchSizes = Object.keys(chartDataByBatch).sort((a, b) => parseInt(a) - parseInt(b));
|
||||
const isDiffusion = metric.type === 'diffusion';
|
||||
|
||||
// For diffusion, keys are test names; for text, keys are batch sizes
|
||||
const keys = Object.keys(chartDataByBatch);
|
||||
if (!isDiffusion) {
|
||||
keys.sort((a, b) => parseInt(a) - parseInt(b));
|
||||
} else {
|
||||
keys.sort(); // Alphabetical sort for test names
|
||||
}
|
||||
const batchSizes = keys; // Keep variable name for compatibility
|
||||
|
||||
if (batchSizes.length === 0) {
|
||||
container.innerHTML = '<div class="no-data">No data available for the selected filters</div>';
|
||||
@@ -682,7 +796,8 @@ function updateMetricChart(chartDataByBatch, metricType) {
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'batch-chart-title';
|
||||
title.textContent = `Batch Size: ${batchSize}`;
|
||||
// For diffusion, show test name; for text, show batch size
|
||||
title.textContent = isDiffusion ? `Test: ${batchSize}` : `Batch Size: ${batchSize}`;
|
||||
chartWrapper.appendChild(title);
|
||||
|
||||
const chartContainer = document.createElement('div');
|
||||
|
||||
@@ -1,4 +1,97 @@
|
||||
_GLOBAL_PERF_RESULTS = []
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
print("[CONFTEST] Loading conftest.py at import time")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
Create the perf results StashKey once and store it in config.
|
||||
This hook runs once per test session, before module double-import issues.
|
||||
"""
|
||||
if not hasattr(config, "_diffusion_perf_key"):
|
||||
config._diffusion_perf_key = pytest.StashKey[list]()
|
||||
print(f"[CONFTEST] Created perf_results_key: {config._diffusion_perf_key}")
|
||||
|
||||
|
||||
def add_perf_results(config, results: list):
|
||||
"""Add performance results to the shared stash."""
|
||||
# Get the shared key from config (created once in pytest_configure)
|
||||
key = config._diffusion_perf_key
|
||||
existing = config.stash.get(key, [])
|
||||
existing.extend(results)
|
||||
config.stash[key] = existing
|
||||
print(f"[CONFTEST] Added {len(results)} results, total now: {len(existing)}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def perf_config(request):
|
||||
"""Provide access to pytest config for storing perf results."""
|
||||
return request.config
|
||||
|
||||
|
||||
def _write_github_step_summary(content: str):
|
||||
"""Write content to GitHub Step Summary if available."""
|
||||
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if summary_file:
|
||||
with open(summary_file, "a") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _write_results_json(results: list, output_path: str = "diffusion-results.json"):
|
||||
"""Write performance results to JSON file for CI artifact collection."""
|
||||
import json
|
||||
|
||||
try:
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"[CONFTEST] Wrote results to {output_path}")
|
||||
except Exception as e:
|
||||
print(f"[CONFTEST] Failed to write results JSON: {e}")
|
||||
|
||||
|
||||
def _generate_diffusion_markdown_report(results: list) -> str:
|
||||
"""Generate a markdown report for diffusion performance results."""
|
||||
if not results:
|
||||
return ""
|
||||
|
||||
gpu_config = os.environ.get("GPU_CONFIG", "")
|
||||
header = "## Diffusion Performance Summary"
|
||||
if gpu_config:
|
||||
header += f" [{gpu_config}]"
|
||||
header += "\n\n"
|
||||
|
||||
# Main performance table
|
||||
markdown = header
|
||||
markdown += "| Test Suite | Test Name | Modality | E2E (ms) | Avg Denoise (ms) | Median Denoise (ms) |\n"
|
||||
markdown += "| ---------- | --------- | -------- | -------- | ---------------- | ------------------- |\n"
|
||||
|
||||
for entry in sorted(results, key=lambda x: (x["class_name"], x["test_name"])):
|
||||
modality = entry.get("modality", "image")
|
||||
markdown += (
|
||||
f"| {entry['class_name']} | {entry['test_name']} | {modality} | "
|
||||
f"{entry['e2e_ms']:.2f} | {entry['avg_denoise_ms']:.2f} | "
|
||||
f"{entry['median_denoise_ms']:.2f} |\n"
|
||||
)
|
||||
|
||||
# Video-specific metrics table (if any video tests)
|
||||
video_results = [r for r in results if r.get("modality") == "video"]
|
||||
if video_results:
|
||||
markdown += "\n### Video Generation Metrics\n\n"
|
||||
markdown += "| Test Name | FPS | Total Frames | Avg Frame Time (ms) |\n"
|
||||
markdown += "| --------- | --- | ------------ | ------------------- |\n"
|
||||
for entry in video_results:
|
||||
fps = entry.get("frames_per_second", "N/A")
|
||||
frames = entry.get("total_frames", "N/A")
|
||||
avg_frame = entry.get("avg_frame_time_ms", "N/A")
|
||||
if isinstance(fps, float):
|
||||
fps = f"{fps:.2f}"
|
||||
if isinstance(avg_frame, float):
|
||||
avg_frame = f"{avg_frame:.2f}"
|
||||
markdown += f"| {entry['test_name']} | {fps} | {frames} | {avg_frame} |\n"
|
||||
|
||||
return markdown
|
||||
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
@@ -6,9 +99,15 @@ def pytest_sessionfinish(session):
|
||||
This hook is called by pytest at the end of the entire test session.
|
||||
It prints a consolidated summary of all performance results.
|
||||
"""
|
||||
if not _GLOBAL_PERF_RESULTS:
|
||||
# Get results from stash using the shared key from config
|
||||
key = session.config._diffusion_perf_key
|
||||
results = session.config.stash.get(key, [])
|
||||
print(f"\n[DEBUG] pytest_sessionfinish called, has {len(results)} entries")
|
||||
if not results:
|
||||
print("[DEBUG] No results collected, skipping summary output")
|
||||
return
|
||||
|
||||
# Print to stdout (existing behavior)
|
||||
print("\n\n" + "=" * 35 + " Performance Summary " + "=" * 35)
|
||||
print(
|
||||
f"{'Test Suite':<30} | {'Test Name':<20} | {'E2E (ms)':>12} | {'Avg Denoise (ms)':>18} | {'Median Denoise (ms)':>20}"
|
||||
@@ -25,7 +124,7 @@ def pytest_sessionfinish(session):
|
||||
+ "-" * 20
|
||||
)
|
||||
|
||||
for entry in sorted(_GLOBAL_PERF_RESULTS, key=lambda x: x["class_name"]):
|
||||
for entry in sorted(results, key=lambda x: x["class_name"]):
|
||||
print(
|
||||
f"{entry['class_name']:<30} | {entry['test_name']:<20} | {entry['e2e_ms']:>12.2f} | "
|
||||
f"{entry['avg_denoise_ms']:>18.2f} | {entry['median_denoise_ms']:>20.2f}"
|
||||
@@ -34,7 +133,7 @@ def pytest_sessionfinish(session):
|
||||
print("=" * 91)
|
||||
|
||||
print("\n\n" + "=" * 36 + " Detailed Reports " + "=" * 37)
|
||||
for entry in sorted(_GLOBAL_PERF_RESULTS, key=lambda x: x["class_name"]):
|
||||
for entry in sorted(results, key=lambda x: x["class_name"]):
|
||||
print(f"\n--- Details for {entry['class_name']} / {entry['test_name']} ---")
|
||||
stage_report = ", ".join(
|
||||
f"{name}:{duration:.2f}ms"
|
||||
@@ -51,3 +150,11 @@ def pytest_sessionfinish(session):
|
||||
)
|
||||
print(f" Sampled Steps: {step_report}")
|
||||
print("=" * 91)
|
||||
|
||||
# Write to GitHub Step Summary (new behavior for CI monitoring)
|
||||
markdown_report = _generate_diffusion_markdown_report(results)
|
||||
if markdown_report:
|
||||
_write_github_step_summary(markdown_report)
|
||||
|
||||
# Write results to JSON file for CI artifact collection
|
||||
_write_results_json(results)
|
||||
|
||||
@@ -18,7 +18,7 @@ from openai import OpenAI
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.perf_logger import RequestPerfRecord
|
||||
from sglang.multimodal_gen.test.server.conftest import _GLOBAL_PERF_RESULTS
|
||||
from sglang.multimodal_gen.test.server import conftest
|
||||
from sglang.multimodal_gen.test.server.test_server_utils import (
|
||||
VALIDATOR_REGISTRY,
|
||||
PerformanceValidator,
|
||||
@@ -134,6 +134,7 @@ class DiffusionServerBase:
|
||||
|
||||
_perf_results: list[dict[str, Any]] = []
|
||||
_improved_baselines: list[dict[str, Any]] = []
|
||||
_pytest_config = None # Store pytest config for stash access
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
@@ -142,9 +143,21 @@ class DiffusionServerBase:
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
for result in cls._perf_results:
|
||||
result["class_name"] = cls.__name__
|
||||
_GLOBAL_PERF_RESULTS.append(result)
|
||||
print(
|
||||
f"\n[DEBUG teardown_class] Called for {cls.__name__}, _perf_results has {len(cls._perf_results)} entries"
|
||||
)
|
||||
if cls._pytest_config:
|
||||
# Add results to pytest stash (shared across all import contexts)
|
||||
for result in cls._perf_results:
|
||||
result["class_name"] = cls.__name__
|
||||
conftest.add_perf_results(cls._pytest_config, cls._perf_results)
|
||||
print(
|
||||
f"[DEBUG teardown_class] Added {len(cls._perf_results)} results to stash"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"[DEBUG teardown_class] No pytest_config available, skipping stash update"
|
||||
)
|
||||
|
||||
if cls._improved_baselines:
|
||||
import json
|
||||
@@ -160,6 +173,11 @@ Consider updating perf_baselines.json with the snippets below:
|
||||
)
|
||||
print(output)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _capture_pytest_config(self, request):
|
||||
"""Capture pytest config for use in teardown_class."""
|
||||
self.__class__._pytest_config = request.config
|
||||
|
||||
def _client(self, ctx: ServerContext) -> OpenAI:
|
||||
"""Get OpenAI client for the server."""
|
||||
return OpenAI(
|
||||
@@ -261,6 +279,9 @@ Consider updating perf_baselines.json with the snippets below:
|
||||
)
|
||||
|
||||
self.__class__._perf_results.append(result)
|
||||
print(
|
||||
f"[DEBUG _validate_and_record] Appended result for {case.id}, class {self.__class__.__name__} now has {len(self.__class__._perf_results)} results"
|
||||
)
|
||||
|
||||
def _check_for_improvement(
|
||||
self,
|
||||
|
||||
@@ -22,8 +22,14 @@ from datetime import datetime, timezone
|
||||
|
||||
def find_partition_files(input_dir: str) -> list[str]:
|
||||
"""Find all partition metric files in the input directory."""
|
||||
pattern = os.path.join(input_dir, "**/metrics-*.json")
|
||||
return glob.glob(pattern, recursive=True)
|
||||
patterns = [
|
||||
os.path.join(input_dir, "**/metrics-*.json"),
|
||||
os.path.join(input_dir, "**/diffusion-metrics-*.json"),
|
||||
]
|
||||
files = set()
|
||||
for pattern in patterns:
|
||||
files.update(glob.glob(pattern, recursive=True))
|
||||
return list(files)
|
||||
|
||||
|
||||
def load_partition_metrics(filepath: str) -> dict | None:
|
||||
|
||||
163
scripts/ci/save_diffusion_metrics.py
Executable file
163
scripts/ci/save_diffusion_metrics.py
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collect and save diffusion performance metrics for artifact collection in CI.
|
||||
|
||||
This script reads diffusion test results from the pytest stash and saves them
|
||||
with metadata for the performance dashboard.
|
||||
|
||||
Usage:
|
||||
python3 scripts/ci/save_diffusion_metrics.py \
|
||||
--gpu-config 1-gpu-runner \
|
||||
--run-id 12345678 \
|
||||
--output test/diffusion-metrics-1gpu.json \
|
||||
--results-json test/diffusion-results.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def load_diffusion_results(results_file: str) -> list[dict]:
|
||||
"""Load diffusion performance results from JSON file."""
|
||||
if not os.path.exists(results_file):
|
||||
print(f"Warning: Results file not found: {results_file}")
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(results_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data if isinstance(data, list) else [data]
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"Warning: Failed to parse {results_file}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def transform_diffusion_result(result: dict, gpu_config: str) -> dict:
|
||||
"""Transform a diffusion result to match dashboard expectations.
|
||||
|
||||
Dashboard expects:
|
||||
- Separate test_name, class_name
|
||||
- Numeric metrics in consistent units
|
||||
- Optional modality field
|
||||
"""
|
||||
return {
|
||||
"test_name": result.get("test_name"),
|
||||
"class_name": result.get("class_name"),
|
||||
"modality": result.get("modality", "image"),
|
||||
"e2e_ms": result.get("e2e_ms"),
|
||||
"avg_denoise_ms": result.get("avg_denoise_ms"),
|
||||
"median_denoise_ms": result.get("median_denoise_ms"),
|
||||
"stage_metrics": result.get("stage_metrics", {}),
|
||||
"sampled_steps": result.get("sampled_steps", {}),
|
||||
# Video-specific metrics (if present)
|
||||
"frames_per_second": result.get("frames_per_second"),
|
||||
"total_frames": result.get("total_frames"),
|
||||
"avg_frame_time_ms": result.get("avg_frame_time_ms"),
|
||||
}
|
||||
|
||||
|
||||
def group_results_by_class(results: list[dict], gpu_config: str) -> list[dict]:
|
||||
"""Group diffusion results by test class (suite).
|
||||
|
||||
Returns list with one entry per test class, containing all tests in that class.
|
||||
"""
|
||||
groups = {}
|
||||
|
||||
for result in results:
|
||||
class_name = result.get("class_name", "unknown")
|
||||
|
||||
if class_name not in groups:
|
||||
groups[class_name] = {
|
||||
"gpu_config": gpu_config,
|
||||
"test_suite": class_name,
|
||||
"tests": [],
|
||||
}
|
||||
|
||||
transformed = transform_diffusion_result(result, gpu_config)
|
||||
groups[class_name]["tests"].append(transformed)
|
||||
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def save_metrics(
|
||||
gpu_config: str,
|
||||
run_id: str,
|
||||
output_file: str,
|
||||
results_file: str,
|
||||
) -> bool:
|
||||
"""Collect diffusion metrics and save to output file."""
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Load diffusion results
|
||||
raw_results = load_diffusion_results(results_file)
|
||||
print(f"Loaded {len(raw_results)} diffusion test result(s)")
|
||||
|
||||
# Group by test class
|
||||
grouped = group_results_by_class(raw_results, gpu_config)
|
||||
|
||||
# Create metrics structure
|
||||
metrics = {
|
||||
"run_id": run_id,
|
||||
"timestamp": timestamp,
|
||||
"gpu_config": gpu_config,
|
||||
"test_type": "diffusion",
|
||||
"results": grouped,
|
||||
}
|
||||
|
||||
# Ensure output directory exists and write output
|
||||
try:
|
||||
os.makedirs(os.path.dirname(output_file) or ".", exist_ok=True)
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(metrics, f, indent=2)
|
||||
|
||||
if not raw_results:
|
||||
print(f"Created empty metrics file: {output_file}")
|
||||
else:
|
||||
print(f"Saved diffusion metrics to: {output_file}")
|
||||
return True
|
||||
except OSError as e:
|
||||
print(f"Error writing metrics file: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Collect diffusion performance metrics from test results"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gpu-config",
|
||||
required=True,
|
||||
help="GPU configuration (e.g., 1-gpu-runner, 2-gpu-runner)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run-id",
|
||||
required=True,
|
||||
help="GitHub Actions run ID",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
required=True,
|
||||
help="Output file path for metrics JSON",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--results-json",
|
||||
required=True,
|
||||
help="Path to diffusion results JSON file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
success = save_metrics(
|
||||
gpu_config=args.gpu_config,
|
||||
run_id=args.run_id,
|
||||
output_file=args.output,
|
||||
results_file=args.results_json,
|
||||
)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user