Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ef682227 | ||
|
|
ee34595f53 | ||
|
|
90bdd90b38 | ||
|
|
527cf68742 | ||
|
|
df8106ddbd | ||
|
|
a7e2a75caf | ||
|
|
cf5d2209c9 | ||
|
|
663dfb2f37 |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"config_schema_version": 2,
|
||||
"workload_id": "wan-t2v-1.3b",
|
||||
"variant_id": "canonical",
|
||||
"benchmark_version": 1,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"description": "Wan2.1 T2V 1.3B inference performance",
|
||||
"model": {
|
||||
"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { fetchSummary, fetchTrends, refreshData, RunSource, SummaryResponse, TrendGroup, TrendPoint } from "./api";
|
||||
import { fetchSummary, fetchTrends, refreshData } from "./api";
|
||||
import type { CohortValue, RunSource, SummaryResponse, TrendGroup, TrendPoint } from "./api";
|
||||
|
||||
const METRIC_KEYS = ["latency", "throughput", "memory", "text_encoder_time_s", "dit_time_s", "vae_decode_time_s"];
|
||||
const RUN_SOURCES: Array<{ value: "" | RunSource; label: string }> = [
|
||||
@@ -109,6 +110,61 @@ function metricLabel(metricKey: string) {
|
||||
return METRIC_DEFINITIONS[metricKey]?.label ?? metricKey;
|
||||
}
|
||||
|
||||
type CohortFields = {
|
||||
model_id: string;
|
||||
gpu_type: string;
|
||||
workload_id: CohortValue;
|
||||
variant_id: CohortValue;
|
||||
benchmark_version: CohortValue;
|
||||
recipe_fingerprint: CohortValue;
|
||||
hardware_profile_id: CohortValue;
|
||||
software_profile_id: CohortValue;
|
||||
};
|
||||
|
||||
function cohortValue(value: CohortValue) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "legacy";
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function shortCohortValue(value: CohortValue) {
|
||||
const text = cohortValue(value);
|
||||
if (text === "legacy" || text.length <= 14) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, 12);
|
||||
}
|
||||
|
||||
function cohortKey(cohort: CohortFields) {
|
||||
return [
|
||||
cohort.model_id,
|
||||
cohort.gpu_type,
|
||||
cohortValue(cohort.workload_id),
|
||||
cohortValue(cohort.variant_id),
|
||||
cohortValue(cohort.benchmark_version),
|
||||
cohortValue(cohort.recipe_fingerprint),
|
||||
cohortValue(cohort.hardware_profile_id),
|
||||
cohortValue(cohort.software_profile_id)
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function cohortTitle(cohort: CohortFields) {
|
||||
const workload = cohortValue(cohort.workload_id);
|
||||
const variant = cohortValue(cohort.variant_id);
|
||||
const version = cohortValue(cohort.benchmark_version);
|
||||
const versionLabel = version === "legacy" ? version : `v${version}`;
|
||||
return `${workload} / ${variant} / ${versionLabel}`;
|
||||
}
|
||||
|
||||
function cohortDetail(cohort: CohortFields) {
|
||||
return [
|
||||
`recipe ${shortCohortValue(cohort.recipe_fingerprint)}`,
|
||||
shortCohortValue(cohort.hardware_profile_id),
|
||||
shortCohortValue(cohort.software_profile_id)
|
||||
].join(" | ");
|
||||
}
|
||||
|
||||
function formatMetricValue(metricKey: string, value: number | null | undefined, tooltip = false) {
|
||||
const definition = METRIC_DEFINITIONS[metricKey];
|
||||
if (!definition) {
|
||||
@@ -171,7 +227,9 @@ function TrendChart({ group, metricKey }: { group: TrendGroup; metricKey: string
|
||||
top: `${(activePoint.y / height) * 100}%`
|
||||
}
|
||||
: undefined;
|
||||
const ariaLabel = `${metricLabel(metricKey)} trend for ${group.model_id} on ${group.gpu_type}`;
|
||||
const ariaLabel = `${metricLabel(metricKey)} trend for ${group.model_id} on ${group.gpu_type}, ${cohortTitle(
|
||||
group
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<div className="chart-shell">
|
||||
@@ -419,7 +477,7 @@ export default function App() {
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2>Latest Status</h2>
|
||||
<span>{latestRows.length} model/GPU groups</span>
|
||||
<span>{latestRows.length} comparison cohorts</span>
|
||||
</div>
|
||||
{latestRows.length === 0 ? (
|
||||
<div className="empty">No records match the selected filters.</div>
|
||||
@@ -432,6 +490,7 @@ export default function App() {
|
||||
<th>Recomputed</th>
|
||||
<th>Model</th>
|
||||
<th>GPU</th>
|
||||
<th>Cohort</th>
|
||||
<th>Commit</th>
|
||||
<th>Source</th>
|
||||
<th>Baseline</th>
|
||||
@@ -446,7 +505,7 @@ export default function App() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{latestRows.map((row) => (
|
||||
<tr key={`${row.model_id}-${row.gpu_type}`}>
|
||||
<tr key={cohortKey(row)}>
|
||||
<td>
|
||||
<span className={`badge ${row.status}`}>{row.status}</span>
|
||||
</td>
|
||||
@@ -457,6 +516,12 @@ export default function App() {
|
||||
</td>
|
||||
<td>{row.model_id}</td>
|
||||
<td>{row.gpu_type}</td>
|
||||
<td>
|
||||
<div className="cohort-cell">
|
||||
<strong>{cohortTitle(row)}</strong>
|
||||
<span>{cohortDetail(row)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{shortSha(row.commit_sha)}</td>
|
||||
<td>
|
||||
<span className={`source-badge source-${row.run_source}`}>{runSourceLabel(row.run_source)}</span>
|
||||
@@ -495,11 +560,13 @@ export default function App() {
|
||||
) : (
|
||||
trends.map((group) =>
|
||||
METRIC_KEYS.map((metricKey) => (
|
||||
<article className="trend-card" key={`${group.model_id}-${group.gpu_type}-${metricKey}`}>
|
||||
<article className="trend-card" key={`${cohortKey(group)}-${metricKey}`}>
|
||||
<div>
|
||||
<h3>{metricLabel(metricKey)}</h3>
|
||||
<p>
|
||||
{group.model_id} | {group.gpu_type}
|
||||
<span>{cohortTitle(group)}</span>
|
||||
<span>{cohortDetail(group)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<TrendChart group={group} metricKey={metricKey} />
|
||||
|
||||
@@ -13,6 +13,17 @@ export type MetricValue = {
|
||||
precision: number;
|
||||
};
|
||||
|
||||
export type CohortValue = string | number | null;
|
||||
|
||||
export type ComparisonCohort = {
|
||||
workload_id: CohortValue;
|
||||
variant_id: CohortValue;
|
||||
benchmark_version: CohortValue;
|
||||
recipe_fingerprint: CohortValue;
|
||||
hardware_profile_id: CohortValue;
|
||||
software_profile_id: CohortValue;
|
||||
};
|
||||
|
||||
export type SummaryRow = {
|
||||
model_id: string;
|
||||
gpu_type: string;
|
||||
@@ -34,7 +45,7 @@ export type SummaryRow = {
|
||||
build_id: string;
|
||||
job_id: string;
|
||||
metrics: Record<string, MetricValue>;
|
||||
};
|
||||
} & ComparisonCohort;
|
||||
|
||||
export type RunSource = "pr" | "local" | "scheduled_main" | "unknown";
|
||||
|
||||
@@ -68,13 +79,13 @@ export type TrendPoint = {
|
||||
build_id: string;
|
||||
job_id: string;
|
||||
metrics: Record<string, number | null>;
|
||||
};
|
||||
} & ComparisonCohort;
|
||||
|
||||
export type TrendGroup = {
|
||||
model_id: string;
|
||||
gpu_type: string;
|
||||
points: TrendPoint[];
|
||||
};
|
||||
} & ComparisonCohort;
|
||||
|
||||
export type TrendsResponse = {
|
||||
groups: TrendGroup[];
|
||||
|
||||
@@ -149,6 +149,11 @@ h3 {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.trend-card p {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
@@ -186,7 +191,7 @@ h3 {
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 1120px;
|
||||
min-width: 1260px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@@ -209,6 +214,25 @@ td {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cohort-cell {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.cohort-cell strong,
|
||||
.trend-card p span {
|
||||
color: #1b2836;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cohort-cell span,
|
||||
.trend-card p span + span {
|
||||
color: #607080;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -200,9 +200,9 @@ configs and remain loadable. New or migrated configs should use
|
||||
{
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"config_schema_version": 2,
|
||||
"workload_id": "wan-t2v-1.3b",
|
||||
"variant_id": "canonical",
|
||||
"benchmark_version": 1
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2
|
||||
}
|
||||
```
|
||||
|
||||
@@ -213,21 +213,25 @@ metadata that make the measured workload explicit:
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `workload_id` | Stable benchmark family, such as `wan-t2v-1.3b`. |
|
||||
| `variant_id` | Intentional recipe family, such as `canonical`. |
|
||||
| `workload_id` | Stable benchmark family, such as `wan-t2v`. |
|
||||
| `variant_id` | Intentional recipe family, including model size and parallelism config, such as `1.3b-sp2`. |
|
||||
| `benchmark_version` | Version of the measurement protocol and comparison policy. |
|
||||
|
||||
If a config declares `config_schema_version: 2`, loading fails clearly when any
|
||||
required v2 identity field is missing. If v2 identity or metadata fields are
|
||||
added without `config_schema_version: 2`, loading also fails so partial
|
||||
migrations do not silently run as v1 configs. Optional v2 metadata fields
|
||||
reserved for follow-up work, such as `recipe`, `metric_threshold_policy`, and
|
||||
`quality_metadata`, must be JSON objects when present.
|
||||
reserved for follow-up work, such as `metric_threshold_policy` and
|
||||
`quality_metadata`, must be JSON objects when present. (`recipe` is emitted
|
||||
by the harness and is not config-declarable.)
|
||||
|
||||
Recipe fingerprinting, hardware/software profile IDs, exact-identity
|
||||
comparison, metric-specific threshold policy behavior, promoted baselines, and
|
||||
dashboard regrouping are separate follow-up changes. Until those land, rolling
|
||||
baseline comparison remains keyed by `(model_id, gpu_type)`.
|
||||
comparison, and dashboard cohort grouping land with this change: v2 records
|
||||
compare only within their identity cohort, and a record that opens a NEW
|
||||
cohort is marked `baseline_status: "initialized_new_cohort"` (regression
|
||||
gating starts once that cohort accumulates history). Legacy v1 configs keep
|
||||
the `(model_id, gpu_type)` rolling-baseline comparison. Metric-specific
|
||||
threshold policies and promoted baselines remain separate follow-ups.
|
||||
|
||||
### Raw record (`results/perf_*.json`)
|
||||
|
||||
@@ -237,9 +241,9 @@ Written by `test_inference_performance.py`. One file per benchmark run.
|
||||
{
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"config_schema_version": 2,
|
||||
"workload_id": "wan-t2v-1.3b",
|
||||
"variant_id": "canonical",
|
||||
"benchmark_version": 1,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"model_short_name": "Wan2.1-T2V-1.3B-Diffusers",
|
||||
"device": "NVIDIA L40S",
|
||||
"num_gpus": 2,
|
||||
@@ -269,7 +273,43 @@ Written by `test_inference_performance.py`. One file per benchmark run.
|
||||
"timestamp": "2026-05-08T22:00:00+00:00",
|
||||
"text_encoder_time_s": 2.141,
|
||||
"dit_time_s": 8.437,
|
||||
"vae_decode_time_s": 3.208
|
||||
"vae_decode_time_s": 3.208,
|
||||
"recipe": {
|
||||
"recipe_schema_version": 1,
|
||||
"benchmark": {
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2
|
||||
},
|
||||
"model": { "model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" },
|
||||
"init_kwargs": { "num_gpus": 2, "sp_size": 2, "tp_size": 1 },
|
||||
"generation_kwargs": { "height": 480, "width": 832, "num_frames": 45 },
|
||||
"inputs": { "prompt_count": 1, "prompt_sha256": ["<measured-prompt-sha256>"] },
|
||||
"attention": { "requested_backend": "FLASH_ATTN", "resolved_backend": "FLASH_ATTN" }
|
||||
},
|
||||
"recipe_fingerprint": "<sha256>",
|
||||
"hardware_profile": {
|
||||
"device_type": "cuda",
|
||||
"gpu_count": 2,
|
||||
"gpus": [{ "name": "NVIDIA L40S", "memory_gb": 48, "compute_capability": "8.9" }],
|
||||
"interconnect": "none_or_partial"
|
||||
},
|
||||
"hardware_profile_id": "hw-<sha256-prefix>",
|
||||
"software_profile": {
|
||||
"python": "3.12",
|
||||
"pytorch": "2.12",
|
||||
"cuda": "13.0",
|
||||
"packages": {
|
||||
"fastvideo_kernel": "0.3.2",
|
||||
"flashinfer": "0.2.11",
|
||||
"nvidia_cutlass_dsl": "4.5.0",
|
||||
"triton": "3.4.1"
|
||||
}
|
||||
},
|
||||
"software_profile_id": "sw-<sha256-prefix>",
|
||||
"environment_metadata": { "env": { "IMAGE_VERSION": "py3.12-cuda13.0.0" } },
|
||||
"environment_fingerprint": "env-<sha256-prefix>"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -281,6 +321,9 @@ result, used as the rolling-baseline source of truth.
|
||||
```jsonc
|
||||
{
|
||||
"model_id": "wan-t2v-1.3b-2gpu",
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"timestamp": "2026-05-08T22:00:00+00:00",
|
||||
"commit_sha": "<full sha>",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
@@ -297,6 +340,10 @@ result, used as the rolling-baseline source of truth.
|
||||
"gated": true
|
||||
}
|
||||
},
|
||||
"recipe_fingerprint": "<sha256>",
|
||||
"hardware_profile_id": "hw-<sha256-prefix>",
|
||||
"software_profile_id": "sw-<sha256-prefix>",
|
||||
"environment_fingerprint": "env-<sha256-prefix>",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -309,6 +356,18 @@ dashboard lists skipped plots for metric series that have no non-null values.
|
||||
Records missing both `run_source` and `baseline_eligible` are treated as legacy
|
||||
successful main/full-suite uploads and remain eligible for rolling baselines.
|
||||
|
||||
New records compare only against the same `model_id`, `gpu_type`,
|
||||
`workload_id`, `variant_id`, `benchmark_version`, `recipe_fingerprint`,
|
||||
`hardware_profile_id`, and `software_profile_id` cohort.
|
||||
`environment_metadata` and `environment_fingerprint` are audit data and are not
|
||||
part of the comparison key.
|
||||
The recipe prompt digests describe the prompts actually measured by the
|
||||
benchmark run; extra configured prompts are ignored unless the benchmark runner
|
||||
executes them.
|
||||
Software profile package cohorts keep exact versions for relevant
|
||||
attention/kernel packages, including FastVideo kernels, FlashAttention,
|
||||
FlashInfer, Cutlass DSL, SageAttention, Triton, and xFormers when installed.
|
||||
|
||||
## Environment variable reference
|
||||
|
||||
| Variable | Default | Used by | Purpose |
|
||||
@@ -363,7 +422,7 @@ When the rolling-baseline phase runs, it emits:
|
||||
"benchmark_id": "<unique-id>",
|
||||
"config_schema_version": 2,
|
||||
"workload_id": "<stable-workload-id>",
|
||||
"variant_id": "canonical",
|
||||
"variant_id": "<variant, e.g. 1.3b-sp2>",
|
||||
"benchmark_version": 1,
|
||||
"model": { "model_path": "...", "model_short_name": "..." },
|
||||
"init_kwargs": { "num_gpus": 1, ... },
|
||||
@@ -390,7 +449,9 @@ When the rolling-baseline phase runs, it emits:
|
||||
|
||||
Legacy v1 configs without `config_schema_version` still load, but should not
|
||||
gain v2 identity or metadata fields until they are migrated to
|
||||
`config_schema_version: 2`.
|
||||
`config_schema_version: 2`. For v2 configs, `workload_id`, `variant_id`,
|
||||
and `benchmark_version` are part of the comparison key; benchmark runs
|
||||
fail if any of these identity fields are missing.
|
||||
|
||||
2. The pytest test auto-discovers all configs — no test code needed. CI
|
||||
picks it up on the next `/test performance` run.
|
||||
|
||||
@@ -268,16 +268,31 @@ def load_records_for_model(
|
||||
model_id: str,
|
||||
gpu_type: str | None = None,
|
||||
*,
|
||||
workload_id: str | None = None,
|
||||
variant_id: str | None = None,
|
||||
benchmark_version: str | None = None,
|
||||
recipe_fingerprint: str | None = None,
|
||||
hardware_profile_id: str | None = None,
|
||||
software_profile_id: str | None = None,
|
||||
last_n: int | None = None,
|
||||
successful_only: bool = True,
|
||||
baseline_eligible_only: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return records for a specific *model_id*, optionally filtered by GPU.
|
||||
"""Return records for a specific *model_id*, optionally filtered by cohort.
|
||||
|
||||
Args:
|
||||
local_dir: Root directory previously populated by :func:`sync_from_hf`.
|
||||
model_id: Matches the ``model_id`` field inside each JSON record.
|
||||
gpu_type: When set, only records whose ``gpu_type`` matches are returned.
|
||||
workload_id: When set, only records from the same workload are returned.
|
||||
variant_id: When set, only records from the same workload variant are returned.
|
||||
benchmark_version: When set, only records from the same benchmark version are returned.
|
||||
recipe_fingerprint: When set, only records from the same benchmark
|
||||
recipe are returned.
|
||||
hardware_profile_id: When set, only records from the same hardware
|
||||
cohort are returned.
|
||||
software_profile_id: When set, only records from the same software
|
||||
cohort are returned.
|
||||
last_n: When set, return only the most recent *n* records (after all
|
||||
other filters). Useful for sliding-window baseline calculations.
|
||||
successful_only: Passed through to :func:`load_records`.
|
||||
@@ -299,6 +314,18 @@ def load_records_for_model(
|
||||
if gpu_type is not None:
|
||||
records = [r for r in records if r.get("gpu_type") == gpu_type]
|
||||
|
||||
identity_filters = {
|
||||
"workload_id": workload_id,
|
||||
"variant_id": variant_id,
|
||||
"benchmark_version": benchmark_version,
|
||||
"recipe_fingerprint": recipe_fingerprint,
|
||||
"hardware_profile_id": hardware_profile_id,
|
||||
"software_profile_id": software_profile_id,
|
||||
}
|
||||
for key, expected in identity_filters.items():
|
||||
if expected is not None:
|
||||
records = [r for r in records if str(r.get(key)) == str(expected)]
|
||||
|
||||
if last_n is not None:
|
||||
records = records[-last_n:]
|
||||
|
||||
|
||||
@@ -17,6 +17,15 @@ from fastvideo.performance.hf_store import is_baseline_eligible_record, safe_flo
|
||||
from fastvideo.performance.metric_policy import regression_delta, resolve_metric_policies
|
||||
|
||||
Record = dict[str, Any]
|
||||
CohortKey = tuple[str, str, str, str, str, str, str, str]
|
||||
COMPARISON_COHORT_KEYS = (
|
||||
"workload_id",
|
||||
"variant_id",
|
||||
"benchmark_version",
|
||||
"recipe_fingerprint",
|
||||
"hardware_profile_id",
|
||||
"software_profile_id",
|
||||
)
|
||||
|
||||
|
||||
def parse_timestamp(value: Any) -> datetime | None:
|
||||
@@ -77,15 +86,44 @@ def record_metadata(record: Record) -> Record:
|
||||
}
|
||||
|
||||
|
||||
def group_by_model_gpu(records: list[Record]) -> dict[tuple[str, str], list[Record]]:
|
||||
groups: dict[tuple[str, str], list[Record]] = defaultdict(list)
|
||||
def record_comparison_metadata(record: Record) -> Record:
|
||||
return {key: record.get(key) or "" for key in COMPARISON_COHORT_KEYS}
|
||||
|
||||
|
||||
def comparison_cohort_key(record: Record) -> CohortKey:
|
||||
return (
|
||||
str(record.get("model_id") or "unknown"),
|
||||
str(record.get("gpu_type") or "unknown"),
|
||||
str(record.get("workload_id") or ""),
|
||||
str(record.get("variant_id") or ""),
|
||||
str(record.get("benchmark_version") or ""),
|
||||
str(record.get("recipe_fingerprint") or ""),
|
||||
str(record.get("hardware_profile_id") or ""),
|
||||
str(record.get("software_profile_id") or ""),
|
||||
)
|
||||
|
||||
|
||||
def group_by_comparison_cohort(records: list[Record]) -> dict[CohortKey, list[Record]]:
|
||||
groups: dict[CohortKey, list[Record]] = defaultdict(list)
|
||||
for record in records:
|
||||
model_id = str(record.get("model_id") or "unknown")
|
||||
gpu_type = str(record.get("gpu_type") or "unknown")
|
||||
groups[(model_id, gpu_type)].append(record)
|
||||
groups[comparison_cohort_key(record)].append(record)
|
||||
return {key: sorted(value, key=record_sort_key) for key, value in groups.items()}
|
||||
|
||||
|
||||
def comparison_sort_key(record: Record) -> CohortKey:
|
||||
return comparison_cohort_key(record)
|
||||
|
||||
|
||||
def latest_row_sort_key(row: Record) -> tuple[Any, ...]:
|
||||
return (row["status"] != "fail", *comparison_sort_key(row))
|
||||
|
||||
|
||||
def group_identity(key: CohortKey) -> tuple[str, str]:
|
||||
model_id = key[0]
|
||||
gpu_type = key[1]
|
||||
return model_id, gpu_type
|
||||
|
||||
|
||||
def baseline_value(records: list[Record], metric_key: str) -> float | None:
|
||||
values = [safe_float(record.get(metric_key)) for record in records]
|
||||
values = [value for value in values if value is not None]
|
||||
@@ -99,7 +137,8 @@ def build_latest_summary(records: list[Record],
|
||||
baseline_window: int = 5,
|
||||
run_source: str | None = None) -> list[Record]:
|
||||
rows: list[Record] = []
|
||||
for (model_id, gpu_type), group in group_by_model_gpu(records).items():
|
||||
for key, group in group_by_comparison_cohort(records).items():
|
||||
model_id, gpu_type = group_identity(key)
|
||||
latest_candidates = group
|
||||
if run_source:
|
||||
latest_candidates = [record for record in group if record_run_source(record) == run_source]
|
||||
@@ -156,6 +195,7 @@ def build_latest_summary(records: list[Record],
|
||||
"timestamp": latest.get("timestamp"),
|
||||
"commit_sha": latest.get("commit_sha"),
|
||||
**record_metadata(latest),
|
||||
**record_comparison_metadata(latest),
|
||||
"success": success,
|
||||
"baseline_n": len(baseline_records),
|
||||
"worst_regression_pct": worst_regression,
|
||||
@@ -166,12 +206,13 @@ def build_latest_summary(records: list[Record],
|
||||
"metrics": metrics,
|
||||
})
|
||||
|
||||
return sorted(rows, key=lambda row: (row["status"] != "fail", row["model_id"], row["gpu_type"]))
|
||||
return sorted(rows, key=latest_row_sort_key)
|
||||
|
||||
|
||||
def build_trends(records: list[Record]) -> list[Record]:
|
||||
trends: list[Record] = []
|
||||
for (model_id, gpu_type), group in group_by_model_gpu(records).items():
|
||||
for key, group in group_by_comparison_cohort(records).items():
|
||||
model_id, gpu_type = group_identity(key)
|
||||
points = []
|
||||
for record in group:
|
||||
metric_policies = resolve_metric_policies(record.get("regression_thresholds"))
|
||||
@@ -179,6 +220,7 @@ def build_trends(records: list[Record]) -> list[Record]:
|
||||
"timestamp": record.get("timestamp"),
|
||||
"commit_sha": record.get("commit_sha"),
|
||||
**record_metadata(record),
|
||||
**record_comparison_metadata(record),
|
||||
"success": bool(record.get("success", True)),
|
||||
"metrics": {
|
||||
policy.key: safe_float(record.get(policy.key))
|
||||
@@ -189,6 +231,7 @@ def build_trends(records: list[Record]) -> list[Record]:
|
||||
trends.append({
|
||||
"model_id": model_id,
|
||||
"gpu_type": gpu_type,
|
||||
**record_comparison_metadata(group[-1]),
|
||||
"points": points,
|
||||
})
|
||||
return sorted(trends, key=lambda trend: (trend["model_id"], trend["gpu_type"]))
|
||||
return sorted(trends, key=comparison_sort_key)
|
||||
|
||||
@@ -5,7 +5,8 @@ This script:
|
||||
1) reads current benchmark results from fastvideo/tests/performance/results,
|
||||
2) syncs the canonical baseline from the configured HF dataset repo,
|
||||
3) compares each current record against the median of up to 5 prior
|
||||
baseline-eligible successful records (filtered by gpu_type),
|
||||
baseline-eligible successful records in the same workload/variant/version,
|
||||
GPU, recipe, hardware, and software cohort,
|
||||
4) writes normalized records back to the HF dataset repo according to
|
||||
PERF_UPLOAD_POLICY,
|
||||
5) exits non-zero if any gated metric exceeds both its percent and absolute
|
||||
@@ -64,6 +65,27 @@ PERF_REPORTS_DIR = os.environ.get("PERF_REPORTS_DIR", "/root/data/perf_reports")
|
||||
UPLOAD_POLICY = os.environ.get("PERF_UPLOAD_POLICY", "never").strip().lower()
|
||||
VALID_UPLOAD_POLICIES = {"never", "pass", "always"}
|
||||
VALID_RUN_SOURCES = {"pr", "local", "scheduled_main", "unknown"}
|
||||
IDENTITY_KEYS = (
|
||||
"workload_id",
|
||||
"variant_id",
|
||||
"benchmark_version",
|
||||
"recipe",
|
||||
"recipe_fingerprint",
|
||||
"hardware_profile",
|
||||
"hardware_profile_id",
|
||||
"software_profile",
|
||||
"software_profile_id",
|
||||
"environment_metadata",
|
||||
"environment_fingerprint",
|
||||
)
|
||||
COMPARISON_IDENTITY_KEYS = (
|
||||
"workload_id",
|
||||
"variant_id",
|
||||
"benchmark_version",
|
||||
"recipe_fingerprint",
|
||||
"hardware_profile_id",
|
||||
"software_profile_id",
|
||||
)
|
||||
|
||||
|
||||
def _should_persist_tracking() -> bool:
|
||||
@@ -136,6 +158,46 @@ def _record_metadata(run_source: str, result: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _identity_metadata(result: dict[str, Any]) -> dict[str, Any]:
|
||||
metadata = {
|
||||
key: result[key]
|
||||
for key in IDENTITY_KEYS
|
||||
if key in result and result[key] is not None
|
||||
}
|
||||
recipe = result.get("recipe")
|
||||
benchmark = recipe.get("benchmark") if isinstance(recipe, dict) else None
|
||||
if isinstance(benchmark, dict):
|
||||
for key in ("workload_id", "variant_id", "benchmark_version"):
|
||||
if key not in metadata and benchmark.get(key) is not None:
|
||||
metadata[key] = benchmark[key]
|
||||
return metadata
|
||||
|
||||
|
||||
def _comparison_identity_filters(record: dict[str, Any]) -> dict[str, str]:
|
||||
missing = [
|
||||
key for key in COMPARISON_IDENTITY_KEYS
|
||||
if key not in record or record[key] is None or (isinstance(record[key], str) and not record[key].strip())
|
||||
]
|
||||
if len(missing) == len(COMPARISON_IDENTITY_KEYS):
|
||||
# Legacy v1 record: no identity fields at all. Keep the pre-v2
|
||||
# (model_id, gpu_type) rolling-baseline comparison instead of
|
||||
# crashing — v1 configs are documented as loadable.
|
||||
return {}
|
||||
if missing:
|
||||
raise ValueError("Performance record missing required comparison identity fields: " + ", ".join(missing))
|
||||
return {
|
||||
key: str(record[key])
|
||||
for key in COMPARISON_IDENTITY_KEYS
|
||||
}
|
||||
|
||||
|
||||
def _format_identity_filters(filters: dict[str, str]) -> str:
|
||||
if not filters:
|
||||
return ""
|
||||
compact = ", ".join(f"{key}={value}" for key, value in filters.items())
|
||||
return f" ({compact})"
|
||||
|
||||
|
||||
def _load_current_results() -> list[dict[str, Any]]:
|
||||
pattern = os.path.join(RESULTS_DIR, "perf_*.json")
|
||||
records: list[dict[str, Any]] = []
|
||||
@@ -169,7 +231,7 @@ def normalize_performance_result(result: dict[str, Any]) -> dict[str, Any]:
|
||||
vae_decode_time = safe_float(result.get("vae_decode_time_s"))
|
||||
metric_policies = resolve_metric_policies(result.get("regression_thresholds"))
|
||||
|
||||
return {
|
||||
record = {
|
||||
"model_id": model_id,
|
||||
"timestamp": timestamp,
|
||||
"commit_sha": commit_sha,
|
||||
@@ -184,6 +246,8 @@ def normalize_performance_result(result: dict[str, Any]) -> dict[str, Any]:
|
||||
"success": True,
|
||||
**_record_metadata(_detect_run_source(), result),
|
||||
}
|
||||
record.update(_identity_metadata(result))
|
||||
return record
|
||||
|
||||
|
||||
def _normalize_record(result: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -422,17 +486,31 @@ def main() -> int:
|
||||
TRACKING_ROOT,
|
||||
record["model_id"],
|
||||
record["gpu_type"],
|
||||
**_comparison_identity_filters(record),
|
||||
last_n=5,
|
||||
successful_only=True,
|
||||
baseline_eligible_only=True,
|
||||
)
|
||||
|
||||
if not baseline_records:
|
||||
print(f"No baseline for {record['model_id']} on "
|
||||
f"{record['gpu_type']}. Initializing...")
|
||||
# A brand-new cohort has NO regression gating until history
|
||||
# accumulates — make that loud and machine-readable instead of
|
||||
# an indistinguishable pass, so a cohort shift (intended or
|
||||
# accidental, e.g. an identity-field change) never silently
|
||||
# blinds the comparison.
|
||||
record["baseline_status"] = "initialized_new_cohort"
|
||||
print("=" * 72)
|
||||
print(f"WARNING: NO BASELINE — initializing a NEW cohort for "
|
||||
f"{record['model_id']} on {record['gpu_type']}"
|
||||
f"{_format_identity_filters(_comparison_identity_filters(record))}")
|
||||
print("Regression gating is INACTIVE for this cohort until "
|
||||
"baseline history accumulates. If this cohort shift is "
|
||||
"unexpected, check the identity fields above.")
|
||||
print("=" * 72)
|
||||
failures: list[str] = []
|
||||
record["success"] = True
|
||||
else:
|
||||
record["baseline_status"] = "compared"
|
||||
failures = _check_regressions(record, baseline_records, metric_policies)
|
||||
if static_threshold_failed:
|
||||
failures.append(f"{record['model_id']} fixed-threshold phase failed "
|
||||
|
||||
@@ -29,15 +29,76 @@ METRICS = (
|
||||
"dit_time_s",
|
||||
"vae_decode_time_s",
|
||||
)
|
||||
COMPARISON_COHORT_KEYS = (
|
||||
"workload_id",
|
||||
"variant_id",
|
||||
"benchmark_version",
|
||||
"recipe_fingerprint",
|
||||
"hardware_profile_id",
|
||||
"software_profile_id",
|
||||
)
|
||||
GROUP_KEYS = ("model_id", "gpu_type", *COMPARISON_COHORT_KEYS)
|
||||
|
||||
|
||||
def _cohort_value(value: object) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
try:
|
||||
if pd.isna(value):
|
||||
return ""
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return str(value)
|
||||
|
||||
|
||||
def _display_value(value: object) -> str:
|
||||
return _cohort_value(value) or "legacy"
|
||||
|
||||
|
||||
def _short_value(value: object) -> str:
|
||||
text = _display_value(value)
|
||||
if text == "legacy" or len(text) <= 14:
|
||||
return text
|
||||
return text[:12]
|
||||
|
||||
|
||||
def _cohort_title(group_key: tuple[object, ...]) -> str:
|
||||
workload = _display_value(group_key[2])
|
||||
variant = _display_value(group_key[3])
|
||||
version = _display_value(group_key[4])
|
||||
version_label = version if version == "legacy" else f"v{version}"
|
||||
return f"{workload} / {variant} / {version_label}"
|
||||
|
||||
|
||||
def _cohort_detail(group_key: tuple[object, ...]) -> str:
|
||||
return " | ".join((
|
||||
f"recipe {_short_value(group_key[5])}",
|
||||
_short_value(group_key[6]),
|
||||
_short_value(group_key[7]),
|
||||
))
|
||||
|
||||
|
||||
def _group_record(group_key: tuple[object, ...]) -> dict[str, str]:
|
||||
return {
|
||||
key: _cohort_value(value)
|
||||
for key, value in zip(GROUP_KEYS, group_key)
|
||||
}
|
||||
|
||||
|
||||
def _dashboard_frame(df: pd.DataFrame) -> pd.DataFrame:
|
||||
dashboard_df = df.copy()
|
||||
for key in GROUP_KEYS:
|
||||
if key not in dashboard_df.columns:
|
||||
dashboard_df[key] = ""
|
||||
dashboard_df[key] = dashboard_df[key].map(_cohort_value)
|
||||
return dashboard_df
|
||||
|
||||
# -----------------------------
|
||||
# 1. Grouping
|
||||
# -----------------------------
|
||||
def group_data(df: pd.DataFrame):
|
||||
# Group only by model+GPU so each group produces a time-series line.
|
||||
# config_id (commit SHA) is carried as a column for hover/color use.
|
||||
keys = ["model_id", "gpu_type"]
|
||||
return df.groupby(keys, dropna=False)
|
||||
# Group by the same comparison cohort used by baseline gating.
|
||||
return _dashboard_frame(df).groupby(list(GROUP_KEYS), dropna=False)
|
||||
|
||||
# -----------------------------
|
||||
# 2. Plot builder
|
||||
@@ -46,15 +107,20 @@ def build_plots(df: pd.DataFrame) -> tuple[list, list[dict[str, object]]]:
|
||||
figs = []
|
||||
skipped_metrics: list[dict[str, object]] = []
|
||||
|
||||
for (model_id, gpu_type), g in group_data(df):
|
||||
for group_key, g in group_data(df):
|
||||
model_id, gpu_type = group_key[:2]
|
||||
group_record = _group_record(group_key)
|
||||
cohort_title = _cohort_title(group_key)
|
||||
cohort_detail = _cohort_detail(group_key)
|
||||
g = g.sort_values("timestamp")
|
||||
|
||||
# One chart per metric so the y-axes aren't on wildly different scales
|
||||
for metric in METRICS:
|
||||
if metric not in g.columns:
|
||||
skipped_metrics.append({
|
||||
"model_id": model_id,
|
||||
"gpu_type": gpu_type,
|
||||
**group_record,
|
||||
"cohort": cohort_title,
|
||||
"cohort_detail": cohort_detail,
|
||||
"metric": metric,
|
||||
"reason": "column missing from loaded records",
|
||||
"records": len(g),
|
||||
@@ -65,8 +131,9 @@ def build_plots(df: pd.DataFrame) -> tuple[list, list[dict[str, object]]]:
|
||||
non_null = int(g[metric].notna().sum())
|
||||
if non_null == 0:
|
||||
skipped_metrics.append({
|
||||
"model_id": model_id,
|
||||
"gpu_type": gpu_type,
|
||||
**group_record,
|
||||
"cohort": cohort_title,
|
||||
"cohort_detail": cohort_detail,
|
||||
"metric": metric,
|
||||
"reason": "no non-null values in loaded records",
|
||||
"records": len(g),
|
||||
@@ -79,8 +146,8 @@ def build_plots(df: pd.DataFrame) -> tuple[list, list[dict[str, object]]]:
|
||||
x="timestamp",
|
||||
y=metric,
|
||||
markers=True,
|
||||
hover_data=["config_id", "commit_sha"],
|
||||
title=f"{model_id} | {gpu_type} | {metric}",
|
||||
hover_data=["config_id", "commit_sha", *COMPARISON_COHORT_KEYS],
|
||||
title=f"{model_id} | {gpu_type} | {cohort_title} | {cohort_detail} | {metric}",
|
||||
labels={"timestamp": "Time", metric: metric},
|
||||
)
|
||||
figs.append(fig)
|
||||
@@ -95,7 +162,7 @@ def render_skipped_metrics(skipped_metrics: list[dict[str, object]]) -> str:
|
||||
rows = [
|
||||
"<h3>Skipped Metric Plots</h3>",
|
||||
"<table>",
|
||||
("<thead><tr><th>Model</th><th>GPU</th><th>Metric</th>"
|
||||
("<thead><tr><th>Model</th><th>GPU</th><th>Cohort</th><th>Metric</th>"
|
||||
"<th>Records</th><th>Non-null</th><th>Reason</th></tr></thead>"),
|
||||
"<tbody>",
|
||||
]
|
||||
@@ -104,6 +171,7 @@ def render_skipped_metrics(skipped_metrics: list[dict[str, object]]) -> str:
|
||||
"<tr>"
|
||||
f"<td>{escape(str(item['model_id']))}</td>"
|
||||
f"<td>{escape(str(item['gpu_type']))}</td>"
|
||||
f"<td>{escape(str(item['cohort']))}<br><code>{escape(str(item['cohort_detail']))}</code></td>"
|
||||
f"<td>{escape(str(item['metric']))}</td>"
|
||||
f"<td>{item['records']}</td>"
|
||||
f"<td>{item['non_null']}</td>"
|
||||
@@ -165,6 +233,7 @@ def main() -> None:
|
||||
print("Skipped metric plots:")
|
||||
for item in skipped_metrics:
|
||||
print(f" - {item['model_id']} | {item['gpu_type']} | "
|
||||
f"{item['cohort']} | {item['cohort_detail']} | "
|
||||
f"{item['metric']}: {item['reason']} "
|
||||
f"({item['non_null']}/{item['records']} non-null)")
|
||||
print(f"Generated {len(figs)} metric plot(s)")
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Comparable identity helpers for performance benchmark records."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
RECIPE_SCHEMA_VERSION = 1
|
||||
PROFILE_ID_LENGTH = 16
|
||||
_PATH_EXCLUDED_GENERATION_KEYS = {"output_path", "output_video_name"}
|
||||
_PROMPT_KEYS = {"prompt", "negative_prompt", "neg_prompt"}
|
||||
REQUIRED_BENCHMARK_IDENTITY_KEYS = (
|
||||
"benchmark_id",
|
||||
"workload_id",
|
||||
"variant_id",
|
||||
"benchmark_version",
|
||||
)
|
||||
_PROFILE_ENV_VARS = (
|
||||
"CUDA_VISIBLE_DEVICES",
|
||||
"FASTVIDEO_ATTENTION_BACKEND",
|
||||
"IMAGE_VERSION",
|
||||
"UV_TORCH_BACKEND",
|
||||
)
|
||||
_PACKAGE_DISTRIBUTIONS = {
|
||||
"fastvideo_kernel": ("fastvideo-kernel", "fastvideo_kernel"),
|
||||
"flash_attn": ("flash-attn", "flash_attn"),
|
||||
"flash_attn_4": ("flash-attn-4",),
|
||||
"flash_attention_fp4": ("flash-attention-fp4",),
|
||||
"flashinfer": ("flashinfer-python", "flashinfer"),
|
||||
"nvidia_cutlass_dsl": ("nvidia-cutlass-dsl", "cutlass-dsl"),
|
||||
"sageattention": ("sageattention",),
|
||||
"sageattn3": ("sageattn3",),
|
||||
"triton": ("triton",),
|
||||
"xformers": ("xformers",),
|
||||
}
|
||||
|
||||
|
||||
def canonical_json(payload: Any) -> str:
|
||||
"""Return deterministic, compact JSON for a recipe/profile payload."""
|
||||
|
||||
return json.dumps(
|
||||
_canonicalize(payload),
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
)
|
||||
|
||||
|
||||
def sha256_hexdigest(payload: str | bytes) -> str:
|
||||
if isinstance(payload, str):
|
||||
payload = payload.encode("utf-8")
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def recipe_fingerprint(recipe: Mapping[str, Any]) -> str:
|
||||
# Runtime-resolved values are audit metadata, not identity: hashing the
|
||||
# resolved HF snapshot revision would churn the cohort (and via the
|
||||
# no-baseline init path, silently reset the rolling baseline) on ANY
|
||||
# upstream repo commit, including model-card edits with unchanged
|
||||
# weights. The fingerprint is pinned to declared inputs only; the
|
||||
# resolved values stay in the stored recipe for auditability.
|
||||
pruned = {
|
||||
key: (dict(value) if isinstance(value, Mapping) else value)
|
||||
for key, value in recipe.items()
|
||||
}
|
||||
model = pruned.get("model")
|
||||
if isinstance(model, dict):
|
||||
model.pop("resolved_revision", None)
|
||||
attention = pruned.get("attention")
|
||||
if isinstance(attention, dict):
|
||||
# Same reasoning as resolved_revision: with requested_backend="auto",
|
||||
# a silent runtime fallback (e.g. a broken flash-attn wheel) must NOT
|
||||
# open a fresh ungated cohort and seed degraded numbers as the new
|
||||
# baseline — it should fail the regression gate in the old cohort.
|
||||
# Intentional backend changes are declared via requested_backend,
|
||||
# which stays in the hash.
|
||||
attention.pop("resolved_backend", None)
|
||||
return sha256_hexdigest(canonical_json(pruned))
|
||||
|
||||
|
||||
def profile_id(prefix: str, profile: Mapping[str, Any]) -> str:
|
||||
return f"{prefix}-{sha256_hexdigest(canonical_json(profile))[:PROFILE_ID_LENGTH]}"
|
||||
|
||||
|
||||
def hardware_profile_id(profile: Mapping[str, Any]) -> str:
|
||||
return profile_id("hw", profile)
|
||||
|
||||
|
||||
def software_profile_id(profile: Mapping[str, Any]) -> str:
|
||||
return profile_id("sw", profile)
|
||||
|
||||
|
||||
def environment_fingerprint(metadata: Mapping[str, Any]) -> str:
|
||||
return profile_id("env", metadata)
|
||||
|
||||
|
||||
def benchmark_identity_from_config(cfg: Mapping[str, Any]) -> dict[str, Any]:
|
||||
missing = [
|
||||
key for key in REQUIRED_BENCHMARK_IDENTITY_KEYS
|
||||
if _none_if_empty(cfg.get(key)) is None
|
||||
]
|
||||
if missing:
|
||||
raise ValueError("Benchmark config missing required identity fields: " + ", ".join(missing))
|
||||
return {
|
||||
key: cfg[key]
|
||||
for key in REQUIRED_BENCHMARK_IDENTITY_KEYS
|
||||
}
|
||||
|
||||
|
||||
def build_recipe_from_benchmark_config(
|
||||
cfg: Mapping[str, Any],
|
||||
*,
|
||||
attention_backend: str | None = None,
|
||||
resolved_attention_backend: Any | None = None,
|
||||
resolved_model_revision: Any | None = None,
|
||||
measured_prompts: Sequence[Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a deterministic recipe document from a benchmark config.
|
||||
|
||||
The recipe captures fields that describe what benchmark workload was run. It
|
||||
intentionally excludes timings, timestamps, commit metadata, and output paths.
|
||||
"""
|
||||
|
||||
model = dict(cfg.get("model") or {})
|
||||
init_kwargs = dict(cfg.get("init_kwargs") or {})
|
||||
generation_kwargs = dict(cfg.get("generation_kwargs") or {})
|
||||
benchmark_identity = benchmark_identity_from_config(cfg)
|
||||
prompts = list(measured_prompts) if measured_prompts is not None else list(
|
||||
cfg.get("test_prompts") or ["A cinematic video."])
|
||||
if attention_backend is None:
|
||||
attention_backend = os.environ.get("FASTVIDEO_ATTENTION_BACKEND")
|
||||
|
||||
negative_prompt = generation_kwargs.pop("negative_prompt", generation_kwargs.pop("neg_prompt", None))
|
||||
generation_recipe = {
|
||||
key: value
|
||||
for key, value in generation_kwargs.items()
|
||||
if key not in _PATH_EXCLUDED_GENERATION_KEYS and key not in _PROMPT_KEYS
|
||||
}
|
||||
|
||||
return {
|
||||
"recipe_schema_version": RECIPE_SCHEMA_VERSION,
|
||||
"benchmark": benchmark_identity,
|
||||
"model": {
|
||||
"model_path": normalize_model_path(model.get("model_path")),
|
||||
"revision": _none_if_empty(model.get("revision") or cfg.get("revision")),
|
||||
"resolved_revision": _none_if_empty(resolved_model_revision),
|
||||
},
|
||||
"init_kwargs": init_kwargs,
|
||||
"generation_kwargs": generation_recipe,
|
||||
"inputs": {
|
||||
"prompt_count": len(prompts),
|
||||
"prompt_sha256": [_optional_digest(prompt) for prompt in prompts],
|
||||
"negative_prompt_sha256": _optional_digest(negative_prompt),
|
||||
},
|
||||
"attention": {
|
||||
"requested_backend": _none_if_empty(attention_backend) or "auto",
|
||||
"resolved_backend": _none_if_empty(resolved_attention_backend),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def normalize_model_path(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = os.fspath(value) if isinstance(value, os.PathLike) else str(value)
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return None
|
||||
if _looks_like_local_path(text):
|
||||
return Path(text).expanduser().as_posix()
|
||||
return re.sub(r"/+", "/", text)
|
||||
|
||||
|
||||
def resolved_revision_from_model_path(value: Any) -> str | None:
|
||||
normalized = normalize_model_path(value)
|
||||
if normalized is None:
|
||||
return None
|
||||
parts = Path(normalized).parts
|
||||
for index, part in enumerate(parts[:-1]):
|
||||
if part == "snapshots":
|
||||
revision = parts[index + 1]
|
||||
if re.fullmatch(r"[0-9a-f]{40}", revision):
|
||||
return revision
|
||||
return None
|
||||
|
||||
|
||||
def hardware_profile(
|
||||
*,
|
||||
num_gpus: int | None = None,
|
||||
gpu_devices: Sequence[Mapping[str, Any]] | None = None,
|
||||
device_type: str | None = None,
|
||||
interconnect: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return normalized hardware cohort metadata.
|
||||
|
||||
``gpu_devices`` is an injection point for tests and non-CUDA callers.
|
||||
"""
|
||||
|
||||
if gpu_devices is not None:
|
||||
devices = [_normalize_gpu_device(device) for device in gpu_devices]
|
||||
return {
|
||||
"device_type": device_type or "cuda",
|
||||
"gpu_count": num_gpus if num_gpus is not None else len(devices),
|
||||
"gpus": devices,
|
||||
"interconnect": interconnect or "unknown",
|
||||
}
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
return {
|
||||
"device_type": device_type or "cpu",
|
||||
"gpu_count": 0,
|
||||
"gpus": [],
|
||||
"interconnect": interconnect or "none",
|
||||
}
|
||||
|
||||
gpu_count = int(num_gpus if num_gpus is not None else torch.cuda.device_count())
|
||||
devices = []
|
||||
cuda_device_count = torch.cuda.device_count()
|
||||
for device_id in range(gpu_count):
|
||||
if device_id >= cuda_device_count:
|
||||
devices.append(_normalize_gpu_device({}))
|
||||
continue
|
||||
props = torch.cuda.get_device_properties(device_id)
|
||||
capability = torch.cuda.get_device_capability(device_id)
|
||||
devices.append(
|
||||
_normalize_gpu_device({
|
||||
"name": props.name,
|
||||
"memory_bytes": props.total_memory,
|
||||
"compute_capability": f"{capability[0]}.{capability[1]}",
|
||||
}))
|
||||
|
||||
return {
|
||||
"device_type": device_type or "cuda",
|
||||
"gpu_count": gpu_count,
|
||||
"gpus": devices,
|
||||
"interconnect": interconnect or _detect_cuda_interconnect(gpu_count),
|
||||
}
|
||||
|
||||
|
||||
def software_profile(
|
||||
*,
|
||||
python_version: str | None = None,
|
||||
torch_version: str | None = None,
|
||||
cuda_version: str | None = None,
|
||||
package_versions: Mapping[str, str | None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return normalized software cohort metadata.
|
||||
|
||||
Python, PyTorch, and CUDA use major/minor cohorts. Attention and kernel
|
||||
packages keep exact versions because patch releases can change performance.
|
||||
"""
|
||||
|
||||
versions = dict(package_versions) if package_versions is not None else _installed_package_versions()
|
||||
return {
|
||||
"python": _major_minor(python_version or platform.python_version()),
|
||||
"pytorch": _major_minor(torch_version or torch.__version__),
|
||||
"cuda": _major_minor(cuda_version or torch.version.cuda),
|
||||
"packages": {
|
||||
name: str(version)
|
||||
for name, version in sorted(versions.items())
|
||||
if version
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def environment_metadata(
|
||||
*,
|
||||
env: Mapping[str, str] | None = None,
|
||||
package_versions: Mapping[str, str | None] | None = None,
|
||||
hardware: Mapping[str, Any] | None = None,
|
||||
software: Mapping[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return audit metadata kept separate from comparable identity keys."""
|
||||
|
||||
source_env = env if env is not None else os.environ
|
||||
full_package_versions = dict(package_versions) if package_versions is not None else _installed_package_versions()
|
||||
return {
|
||||
"python": {
|
||||
"version": platform.python_version(),
|
||||
"implementation": platform.python_implementation(),
|
||||
},
|
||||
"platform": {
|
||||
"system": platform.system(),
|
||||
"machine": platform.machine(),
|
||||
"release": platform.release(),
|
||||
},
|
||||
"torch": {
|
||||
"version": torch.__version__,
|
||||
"cuda": torch.version.cuda,
|
||||
"cudnn": torch.backends.cudnn.version(),
|
||||
},
|
||||
"env": {
|
||||
key: source_env.get(key)
|
||||
for key in _PROFILE_ENV_VARS
|
||||
if source_env.get(key) is not None
|
||||
},
|
||||
"packages": {
|
||||
key: value
|
||||
for key, value in sorted(full_package_versions.items())
|
||||
if value
|
||||
},
|
||||
"hardware_profile": dict(hardware) if hardware is not None else hardware_profile(),
|
||||
"software_profile": dict(software) if software is not None else software_profile(
|
||||
package_versions=full_package_versions),
|
||||
}
|
||||
|
||||
|
||||
def _canonicalize(value: Any) -> Any:
|
||||
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
||||
return _canonicalize(dataclasses.asdict(value))
|
||||
if isinstance(value, Mapping):
|
||||
return {
|
||||
str(key): _canonicalize(value[key])
|
||||
for key in sorted(value, key=lambda item: str(item))
|
||||
}
|
||||
if isinstance(value, (set, frozenset)):
|
||||
return [_canonicalize(item) for item in sorted(value, key=lambda item: str(item))]
|
||||
if isinstance(value, tuple):
|
||||
return [_canonicalize(item) for item in value]
|
||||
if isinstance(value, list):
|
||||
return [_canonicalize(item) for item in value]
|
||||
if isinstance(value, enum.Enum):
|
||||
return value.name
|
||||
if isinstance(value, Path):
|
||||
return value.expanduser().as_posix()
|
||||
if isinstance(value, os.PathLike):
|
||||
return os.fspath(value)
|
||||
if isinstance(value, type):
|
||||
return f"{value.__module__}.{value.__qualname__}"
|
||||
if isinstance(value, torch.dtype):
|
||||
return str(value).removeprefix("torch.")
|
||||
return value
|
||||
|
||||
|
||||
def _none_if_empty(value: Any) -> Any | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str) and not value.strip():
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _optional_digest(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return sha256_hexdigest(str(value))
|
||||
|
||||
|
||||
def _looks_like_local_path(value: str) -> bool:
|
||||
if value.startswith(("/", "./", "../", "~")):
|
||||
return True
|
||||
looks_like_hf_repo = re.match(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$", value)
|
||||
has_separator = "/" in value or "\\" in value or os.sep in value
|
||||
return has_separator and looks_like_hf_repo is None
|
||||
|
||||
|
||||
def _normalize_gpu_device(device: Mapping[str, Any]) -> dict[str, Any]:
|
||||
memory_gb = device.get("memory_gb")
|
||||
if memory_gb is None and device.get("memory_bytes") is not None:
|
||||
memory_gb = round(float(device["memory_bytes"]) / (1024**3))
|
||||
return {
|
||||
"name": _none_if_empty(device.get("name")) or "unknown",
|
||||
"memory_gb": memory_gb,
|
||||
"compute_capability": _none_if_empty(device.get("compute_capability")),
|
||||
}
|
||||
|
||||
|
||||
def _detect_cuda_interconnect(gpu_count: int) -> str:
|
||||
if gpu_count <= 1:
|
||||
return "single_gpu"
|
||||
try:
|
||||
from fastvideo.platforms import current_platform
|
||||
|
||||
if current_platform.is_cuda() and current_platform.is_full_nvlink(list(range(gpu_count))):
|
||||
return "full_nvlink"
|
||||
return "none_or_partial"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _installed_package_versions() -> dict[str, str | None]:
|
||||
return {
|
||||
name: _distribution_version(*distribution_names)
|
||||
for name, distribution_names in _PACKAGE_DISTRIBUTIONS.items()
|
||||
}
|
||||
|
||||
|
||||
def _distribution_version(*names: str) -> str | None:
|
||||
for name in names:
|
||||
try:
|
||||
return importlib.metadata.version(name)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _major_minor(version: str | None) -> str | None:
|
||||
if not version:
|
||||
return None
|
||||
match = re.search(r"(\d+)\.(\d+)", version)
|
||||
if match is None:
|
||||
return version
|
||||
return f"{match.group(1)}.{match.group(2)}"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RECIPE_SCHEMA_VERSION",
|
||||
"REQUIRED_BENCHMARK_IDENTITY_KEYS",
|
||||
"benchmark_identity_from_config",
|
||||
"canonical_json",
|
||||
"environment_fingerprint",
|
||||
"environment_metadata",
|
||||
"hardware_profile",
|
||||
"hardware_profile_id",
|
||||
"normalize_model_path",
|
||||
"profile_id",
|
||||
"recipe_fingerprint",
|
||||
"resolved_revision_from_model_path",
|
||||
"build_recipe_from_benchmark_config",
|
||||
"sha256_hexdigest",
|
||||
"software_profile",
|
||||
"software_profile_id",
|
||||
]
|
||||
@@ -1,5 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
|
||||
from fastvideo.tests.performance import compare_baseline
|
||||
from fastvideo.performance.metric_policy import resolve_metric_policies
|
||||
|
||||
@@ -119,6 +121,110 @@ def test_boolean_regression_threshold_values_are_ignored():
|
||||
assert latency.threshold_percent == 0.08
|
||||
assert latency.threshold_absolute == 0.5
|
||||
assert latency.gated is False
|
||||
def test_normalized_record_preserves_identity_metadata(monkeypatch):
|
||||
monkeypatch.setenv("PERF_RUN_SOURCE", "pr")
|
||||
raw = _raw_result()
|
||||
raw.update({
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"recipe": {
|
||||
"recipe_schema_version": 1,
|
||||
},
|
||||
"recipe_fingerprint": "recipe-1",
|
||||
"hardware_profile": {
|
||||
"gpu_count": 1,
|
||||
},
|
||||
"hardware_profile_id": "hw-1",
|
||||
"software_profile": {
|
||||
"python": "3.12",
|
||||
},
|
||||
"software_profile_id": "sw-1",
|
||||
"environment_metadata": {
|
||||
"env": {
|
||||
"IMAGE_VERSION": "latest",
|
||||
},
|
||||
},
|
||||
"environment_fingerprint": "env-1",
|
||||
})
|
||||
|
||||
record = compare_baseline.normalize_performance_result(raw)
|
||||
|
||||
assert record["workload_id"] == "wan-t2v"
|
||||
assert record["variant_id"] == "1.3b-sp2"
|
||||
assert record["benchmark_version"] == 2
|
||||
assert record["recipe"] == {"recipe_schema_version": 1}
|
||||
assert record["recipe_fingerprint"] == "recipe-1"
|
||||
assert record["hardware_profile"] == {"gpu_count": 1}
|
||||
assert record["hardware_profile_id"] == "hw-1"
|
||||
assert record["software_profile"] == {"python": "3.12"}
|
||||
assert record["software_profile_id"] == "sw-1"
|
||||
assert record["environment_metadata"] == {"env": {"IMAGE_VERSION": "latest"}}
|
||||
assert record["environment_fingerprint"] == "env-1"
|
||||
|
||||
|
||||
def test_normalized_record_reads_identity_labels_from_recipe(monkeypatch):
|
||||
monkeypatch.setenv("PERF_RUN_SOURCE", "pr")
|
||||
raw = _raw_result()
|
||||
raw["recipe"] = {
|
||||
"benchmark": {
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
},
|
||||
}
|
||||
|
||||
record = compare_baseline.normalize_performance_result(raw)
|
||||
|
||||
assert record["workload_id"] == "wan-t2v"
|
||||
assert record["variant_id"] == "1.3b-sp2"
|
||||
assert record["benchmark_version"] == 2
|
||||
|
||||
|
||||
def test_comparison_identity_filters_use_full_issue_key():
|
||||
record = {
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"recipe_fingerprint": "recipe-1",
|
||||
"hardware_profile_id": "hw-1",
|
||||
"software_profile_id": "sw-1",
|
||||
}
|
||||
|
||||
assert compare_baseline._comparison_identity_filters(record) == {
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": "2",
|
||||
"recipe_fingerprint": "recipe-1",
|
||||
"hardware_profile_id": "hw-1",
|
||||
"software_profile_id": "sw-1",
|
||||
}
|
||||
|
||||
|
||||
def test_comparison_identity_filters_require_full_issue_key():
|
||||
record = {
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 0,
|
||||
"recipe_fingerprint": "recipe-1",
|
||||
"hardware_profile_id": "hw-1",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="software_profile_id"):
|
||||
compare_baseline._comparison_identity_filters(record)
|
||||
|
||||
|
||||
def test_comparison_identity_filters_keep_zero_version():
|
||||
record = {
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 0,
|
||||
"recipe_fingerprint": "recipe-1",
|
||||
"hardware_profile_id": "hw-1",
|
||||
"software_profile_id": "sw-1",
|
||||
}
|
||||
|
||||
assert compare_baseline._comparison_identity_filters(record)["benchmark_version"] == "0"
|
||||
|
||||
|
||||
def test_baseline_eligibility_only_for_successful_scheduled_main():
|
||||
|
||||
@@ -56,8 +56,34 @@ def _record(model_id, gpu_type, ts, commit, latency, throughput, success=True, *
|
||||
|
||||
def test_summary_endpoint_returns_latest_group_status():
|
||||
app = create_app(FakeStore([
|
||||
_record("wan", "NVIDIA L40S", "2026-01-01T00:00:00+00:00", "a" * 40, 10.0, 10.0),
|
||||
_record("wan", "NVIDIA L40S", "2026-01-02T00:00:00+00:00", "b" * 40, 11.0, 9.0),
|
||||
_record(
|
||||
"wan",
|
||||
"NVIDIA L40S",
|
||||
"2026-01-01T00:00:00+00:00",
|
||||
"a" * 40,
|
||||
10.0,
|
||||
10.0,
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
recipe_fingerprint="recipe-a",
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
),
|
||||
_record(
|
||||
"wan",
|
||||
"NVIDIA L40S",
|
||||
"2026-01-02T00:00:00+00:00",
|
||||
"b" * 40,
|
||||
11.0,
|
||||
9.0,
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
recipe_fingerprint="recipe-a",
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
),
|
||||
]))
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -71,6 +97,10 @@ def test_summary_endpoint_returns_latest_group_status():
|
||||
assert body["rows"][0]["metrics"]["latency"]["threshold_exceeded"] is True
|
||||
assert body["rows"][0]["threshold_exceeded_metrics"] == ["latency", "throughput"]
|
||||
assert body["rows"][0]["computed_regression_status"] == "fail"
|
||||
assert body["rows"][0]["workload_id"] == "wan-t2v"
|
||||
assert body["rows"][0]["variant_id"] == "1.3b-sp2"
|
||||
assert body["rows"][0]["benchmark_version"] == 2
|
||||
assert body["rows"][0]["recipe_fingerprint"] == "recipe-a"
|
||||
|
||||
|
||||
def test_summary_status_is_independent_of_days_window():
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from fastvideo.tests.performance import dashboard
|
||||
|
||||
|
||||
def _record(recipe_fingerprint="recipe-a", software_profile_id="sw-a", **overrides):
|
||||
record = {
|
||||
"model_id": "wan-t2v-1.3b-2gpu",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"recipe_fingerprint": recipe_fingerprint,
|
||||
"hardware_profile_id": "hw-l40s",
|
||||
"software_profile_id": software_profile_id,
|
||||
"timestamp": "2026-01-01T00:00:00+00:00",
|
||||
"commit_sha": "a" * 40,
|
||||
"config_id": "aaaaaaa",
|
||||
"latency": 10.0,
|
||||
"throughput": 4.5,
|
||||
"memory": 10000.0,
|
||||
"text_encoder_time_s": 2.0,
|
||||
"dit_time_s": 8.0,
|
||||
"vae_decode_time_s": 3.0,
|
||||
}
|
||||
record.update(overrides)
|
||||
return record
|
||||
|
||||
|
||||
def test_group_data_uses_full_comparison_cohort():
|
||||
df = pd.DataFrame([
|
||||
_record(recipe_fingerprint="recipe-a", software_profile_id="sw-a"),
|
||||
_record(recipe_fingerprint="recipe-b", software_profile_id="sw-b"),
|
||||
])
|
||||
|
||||
groups = list(dashboard.group_data(df))
|
||||
|
||||
assert len(groups) == 2
|
||||
assert {key[5] for key, _group in groups} == {"recipe-a", "recipe-b"}
|
||||
assert {key[7] for key, _group in groups} == {"sw-a", "sw-b"}
|
||||
|
||||
|
||||
def test_group_data_fills_legacy_cohort_columns():
|
||||
df = pd.DataFrame([{
|
||||
"model_id": "legacy-wan",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
"timestamp": "2026-01-01T00:00:00+00:00",
|
||||
"commit_sha": "a" * 40,
|
||||
"config_id": "aaaaaaa",
|
||||
"latency": 10.0,
|
||||
}])
|
||||
|
||||
groups = list(dashboard.group_data(df))
|
||||
|
||||
assert len(groups) == 1
|
||||
assert groups[0][0][2:] == ("", "", "", "", "", "")
|
||||
|
||||
|
||||
def test_build_plots_labels_distinct_cohorts():
|
||||
df = pd.DataFrame([
|
||||
_record(recipe_fingerprint="recipe-a", software_profile_id="sw-a"),
|
||||
_record(recipe_fingerprint="recipe-b", software_profile_id="sw-b"),
|
||||
])
|
||||
|
||||
figs, skipped_metrics = dashboard.build_plots(df)
|
||||
titles = [fig.layout.title.text for fig in figs]
|
||||
|
||||
assert len(figs) == len(dashboard.METRICS) * 2
|
||||
assert skipped_metrics == []
|
||||
assert any("recipe recipe-a | hw-l40s | sw-a" in title for title in titles)
|
||||
assert any("recipe recipe-b | hw-l40s | sw-b" in title for title in titles)
|
||||
|
||||
|
||||
def test_skipped_metric_table_includes_cohort_identity():
|
||||
df = pd.DataFrame([{
|
||||
"model_id": "wan-t2v-1.3b-2gpu",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"recipe_fingerprint": "recipe-a",
|
||||
"hardware_profile_id": "hw-l40s",
|
||||
"software_profile_id": "sw-a",
|
||||
"timestamp": "2026-01-01T00:00:00+00:00",
|
||||
"commit_sha": "a" * 40,
|
||||
"config_id": "aaaaaaa",
|
||||
"latency": 10.0,
|
||||
}])
|
||||
|
||||
_figs, skipped_metrics = dashboard.build_plots(df)
|
||||
html = dashboard.render_skipped_metrics(skipped_metrics[:1])
|
||||
|
||||
assert skipped_metrics[0]["cohort"] == "wan-t2v / 1.3b-sp2 / v2"
|
||||
assert skipped_metrics[0]["cohort_detail"] == "recipe recipe-a | hw-l40s | sw-a"
|
||||
assert "wan-t2v / 1.3b-sp2 / v2" in html
|
||||
assert "recipe recipe-a | hw-l40s | sw-a" in html
|
||||
@@ -142,6 +142,111 @@ def test_build_latest_summary_separates_informational_threshold_crossing():
|
||||
assert rows[0]["threshold_exceeded_metrics"] == ["latency"]
|
||||
assert rows[0]["failing_metrics"] == []
|
||||
assert rows[0]["computed_regression_status"] == "pass"
|
||||
def test_build_latest_summary_keeps_identity_cohorts_separate():
|
||||
records = [
|
||||
_record(
|
||||
"2026-01-01T00:00:00+00:00",
|
||||
"a" * 40,
|
||||
10.0,
|
||||
10.0,
|
||||
recipe_fingerprint="recipe-a",
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
run_source="scheduled_main",
|
||||
baseline_eligible=True,
|
||||
),
|
||||
_record(
|
||||
"2026-01-02T00:00:00+00:00",
|
||||
"b" * 40,
|
||||
20.0,
|
||||
5.0,
|
||||
recipe_fingerprint="recipe-b",
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
run_source="scheduled_main",
|
||||
baseline_eligible=True,
|
||||
),
|
||||
_record(
|
||||
"2026-01-03T00:00:00+00:00",
|
||||
"c" * 40,
|
||||
22.0,
|
||||
4.5,
|
||||
recipe_fingerprint="recipe-b",
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
),
|
||||
]
|
||||
|
||||
rows = build_latest_summary(records, max_regression=0.05)
|
||||
recipe_b_row = next(row for row in rows if row["recipe_fingerprint"] == "recipe-b")
|
||||
|
||||
assert len(rows) == 2
|
||||
assert recipe_b_row["baseline_n"] == 1
|
||||
assert recipe_b_row["metrics"]["latency"]["baseline"] == 20.0
|
||||
assert recipe_b_row["workload_id"] == "wan-t2v"
|
||||
assert recipe_b_row["variant_id"] == "1.3b-sp2"
|
||||
assert recipe_b_row["benchmark_version"] == 2
|
||||
|
||||
|
||||
def test_build_latest_summary_keeps_variant_versions_separate():
|
||||
records = [
|
||||
_record(
|
||||
"2026-01-01T00:00:00+00:00",
|
||||
"a" * 40,
|
||||
10.0,
|
||||
10.0,
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=1,
|
||||
recipe_fingerprint="recipe-a",
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
run_source="scheduled_main",
|
||||
baseline_eligible=True,
|
||||
),
|
||||
_record(
|
||||
"2026-01-02T00:00:00+00:00",
|
||||
"b" * 40,
|
||||
20.0,
|
||||
5.0,
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
recipe_fingerprint="recipe-a",
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
run_source="scheduled_main",
|
||||
baseline_eligible=True,
|
||||
),
|
||||
_record(
|
||||
"2026-01-03T00:00:00+00:00",
|
||||
"c" * 40,
|
||||
22.0,
|
||||
4.5,
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version=2,
|
||||
recipe_fingerprint="recipe-a",
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
),
|
||||
]
|
||||
|
||||
rows = build_latest_summary(records, max_regression=0.05)
|
||||
version_2_row = next(row for row in rows if row["benchmark_version"] == 2)
|
||||
|
||||
assert len(rows) == 2
|
||||
assert version_2_row["baseline_n"] == 1
|
||||
assert version_2_row["metrics"]["latency"]["baseline"] == 20.0
|
||||
|
||||
|
||||
def test_filter_records_and_trends_preserve_metric_points():
|
||||
@@ -218,3 +323,78 @@ def test_load_records_can_filter_baseline_eligible_records(tmp_path):
|
||||
"2026-01-02T00:00:00+00:00",
|
||||
"2026-01-03T00:00:00+00:00",
|
||||
}
|
||||
|
||||
|
||||
def test_load_records_for_model_filters_identity_cohort(tmp_path):
|
||||
model_dir = tmp_path / "wan"
|
||||
model_dir.mkdir()
|
||||
(model_dir / "matching.json").write_text(
|
||||
"""
|
||||
{
|
||||
"model_id": "wan",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
"timestamp": "2026-01-01T00:00:00+00:00",
|
||||
"success": true,
|
||||
"baseline_eligible": true,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"recipe_fingerprint": "recipe-a",
|
||||
"hardware_profile_id": "hw-l40s",
|
||||
"software_profile_id": "sw-cu130"
|
||||
}
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(model_dir / "other_recipe.json").write_text(
|
||||
"""
|
||||
{
|
||||
"model_id": "wan",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
"timestamp": "2026-01-02T00:00:00+00:00",
|
||||
"success": true,
|
||||
"baseline_eligible": true,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"recipe_fingerprint": "recipe-b",
|
||||
"hardware_profile_id": "hw-l40s",
|
||||
"software_profile_id": "sw-cu130"
|
||||
}
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(model_dir / "other_version.json").write_text(
|
||||
"""
|
||||
{
|
||||
"model_id": "wan",
|
||||
"gpu_type": "NVIDIA L40S",
|
||||
"timestamp": "2026-01-03T00:00:00+00:00",
|
||||
"success": true,
|
||||
"baseline_eligible": true,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 3,
|
||||
"recipe_fingerprint": "recipe-a",
|
||||
"hardware_profile_id": "hw-l40s",
|
||||
"software_profile_id": "sw-cu130"
|
||||
}
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
records = hf_store.load_records_for_model(
|
||||
str(tmp_path),
|
||||
"wan",
|
||||
"NVIDIA L40S",
|
||||
workload_id="wan-t2v",
|
||||
variant_id="1.3b-sp2",
|
||||
benchmark_version="2",
|
||||
recipe_fingerprint="recipe-a",
|
||||
hardware_profile_id="hw-l40s",
|
||||
software_profile_id="sw-cu130",
|
||||
baseline_eligible_only=True,
|
||||
)
|
||||
|
||||
assert len(records) == 1
|
||||
assert records[0]["recipe_fingerprint"] == "recipe-a"
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from fastvideo.tests.performance import identity as identity_module
|
||||
from fastvideo.tests.performance.identity import (
|
||||
build_recipe_from_benchmark_config,
|
||||
canonical_json,
|
||||
environment_fingerprint,
|
||||
environment_metadata,
|
||||
hardware_profile,
|
||||
hardware_profile_id,
|
||||
recipe_fingerprint,
|
||||
resolved_revision_from_model_path,
|
||||
software_profile,
|
||||
software_profile_id,
|
||||
)
|
||||
|
||||
|
||||
def _benchmark_config():
|
||||
return {
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
"model": {
|
||||
"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
"model_short_name": "Wan2.1-T2V-1.3B",
|
||||
},
|
||||
"init_kwargs": {
|
||||
"num_gpus": 2,
|
||||
"sp_size": 2,
|
||||
"tp_size": 1,
|
||||
"vae_sp": True,
|
||||
"vae_tiling": True,
|
||||
"text_encoder_precisions": ["fp32"],
|
||||
},
|
||||
"generation_kwargs": {
|
||||
"height": 480,
|
||||
"width": 832,
|
||||
"num_frames": 45,
|
||||
"num_inference_steps": 4,
|
||||
"guidance_scale": 3,
|
||||
"embedded_cfg_scale": 6,
|
||||
"seed": 1024,
|
||||
"fps": 24,
|
||||
"neg_prompt": "low quality",
|
||||
},
|
||||
"test_prompts": ["A cinematic video."],
|
||||
}
|
||||
|
||||
|
||||
def _fingerprint(cfg, attention_backend="FLASH_ATTN"):
|
||||
recipe = build_recipe_from_benchmark_config(cfg, attention_backend=attention_backend)
|
||||
return recipe_fingerprint(recipe)
|
||||
|
||||
|
||||
def test_canonical_json_is_stable_for_mapping_order():
|
||||
left = {"b": [2, 1], "a": {"z": "last", "m": "middle"}}
|
||||
right = {"a": {"m": "middle", "z": "last"}, "b": [2, 1]}
|
||||
|
||||
assert canonical_json(left) == canonical_json(right)
|
||||
|
||||
|
||||
def test_canonical_json_handles_sets_deterministically():
|
||||
assert canonical_json({"values": {"b", "a"}}) == '{"values":["a","b"]}'
|
||||
assert canonical_json({"values": frozenset(("b", "a"))}) == '{"values":["a","b"]}'
|
||||
|
||||
|
||||
def test_same_config_produces_same_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
|
||||
assert _fingerprint(cfg) == _fingerprint(deepcopy(cfg))
|
||||
|
||||
|
||||
def test_recipe_includes_first_class_benchmark_identity():
|
||||
recipe = build_recipe_from_benchmark_config(_benchmark_config())
|
||||
|
||||
assert recipe["benchmark"] == {
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 2,
|
||||
}
|
||||
|
||||
|
||||
def test_recipe_requires_first_class_benchmark_identity():
|
||||
cfg = _benchmark_config()
|
||||
del cfg["workload_id"]
|
||||
|
||||
with pytest.raises(ValueError, match="workload_id"):
|
||||
build_recipe_from_benchmark_config(cfg)
|
||||
|
||||
|
||||
def test_semantically_equivalent_sequence_forms_match():
|
||||
cfg = _benchmark_config()
|
||||
equivalent = deepcopy(cfg)
|
||||
equivalent["init_kwargs"]["text_encoder_precisions"] = tuple(
|
||||
equivalent["init_kwargs"]["text_encoder_precisions"])
|
||||
|
||||
assert _fingerprint(cfg) == _fingerprint(equivalent)
|
||||
|
||||
|
||||
def test_output_path_does_not_change_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
with_output_path = deepcopy(cfg)
|
||||
with_output_path["generation_kwargs"]["output_path"] = "/tmp/generated"
|
||||
|
||||
assert _fingerprint(cfg) == _fingerprint(with_output_path)
|
||||
|
||||
|
||||
def test_runtime_resolved_values_do_not_change_recipe_fingerprint():
|
||||
# Runtime-resolved values are audit metadata: an upstream repo commit
|
||||
# (even a README-only edit) or a silent attention-backend fallback must
|
||||
# not churn the cohort — that would open a fresh ungated cohort and let
|
||||
# degraded numbers seed a new baseline. Declared inputs (including
|
||||
# requested_backend) stay in the hash.
|
||||
cfg = _benchmark_config()
|
||||
recipe = build_recipe_from_benchmark_config(
|
||||
cfg, attention_backend="FLASH_ATTN",
|
||||
resolved_attention_backend="FLASH_ATTN",
|
||||
resolved_model_revision="aaaa1111")
|
||||
changed = build_recipe_from_benchmark_config(
|
||||
cfg, attention_backend="FLASH_ATTN",
|
||||
resolved_attention_backend="TORCH_SDPA",
|
||||
resolved_model_revision="bbbb2222")
|
||||
|
||||
assert recipe["model"]["resolved_revision"] == "aaaa1111" # kept for audit
|
||||
assert recipe["attention"]["resolved_backend"] == "FLASH_ATTN" # kept for audit
|
||||
assert recipe_fingerprint(recipe) == recipe_fingerprint(changed)
|
||||
|
||||
|
||||
def test_measured_prompt_override_ignores_unused_configured_prompts():
|
||||
cfg = _benchmark_config()
|
||||
changed = deepcopy(cfg)
|
||||
changed["test_prompts"] = [
|
||||
"Unused prompt that should not affect this measured workload.",
|
||||
cfg["test_prompts"][0],
|
||||
]
|
||||
|
||||
recipe = build_recipe_from_benchmark_config(cfg, measured_prompts=[cfg["test_prompts"][0]])
|
||||
changed_recipe = build_recipe_from_benchmark_config(changed, measured_prompts=[cfg["test_prompts"][0]])
|
||||
|
||||
assert recipe_fingerprint(recipe) == recipe_fingerprint(changed_recipe)
|
||||
|
||||
|
||||
def test_num_inference_steps_changes_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
changed = deepcopy(cfg)
|
||||
changed["generation_kwargs"]["num_inference_steps"] = 8
|
||||
|
||||
assert _fingerprint(cfg) != _fingerprint(changed)
|
||||
|
||||
|
||||
def test_benchmark_version_changes_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
changed = deepcopy(cfg)
|
||||
changed["benchmark_version"] = 3
|
||||
|
||||
assert _fingerprint(cfg) != _fingerprint(changed)
|
||||
|
||||
|
||||
def test_attention_backend_changes_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
|
||||
assert _fingerprint(cfg, attention_backend="FLASH_ATTN") != _fingerprint(
|
||||
cfg, attention_backend="TORCH_SDPA")
|
||||
|
||||
|
||||
def test_precision_changes_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
changed = deepcopy(cfg)
|
||||
changed["init_kwargs"]["text_encoder_precisions"] = ["bf16"]
|
||||
|
||||
assert _fingerprint(cfg) != _fingerprint(changed)
|
||||
|
||||
|
||||
def test_dimensions_and_seed_change_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
different_dimensions = deepcopy(cfg)
|
||||
different_dimensions["generation_kwargs"]["width"] = 1024
|
||||
different_seed = deepcopy(cfg)
|
||||
different_seed["generation_kwargs"]["seed"] = 2048
|
||||
|
||||
assert _fingerprint(cfg) != _fingerprint(different_dimensions)
|
||||
assert _fingerprint(cfg) != _fingerprint(different_seed)
|
||||
|
||||
|
||||
def test_distributed_layout_changes_recipe_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
changed = deepcopy(cfg)
|
||||
changed["init_kwargs"]["sp_size"] = 1
|
||||
changed["init_kwargs"]["tp_size"] = 2
|
||||
|
||||
assert _fingerprint(cfg) != _fingerprint(changed)
|
||||
|
||||
|
||||
def test_software_profile_uses_exact_attention_kernel_package_versions_for_id():
|
||||
profile_a = software_profile(
|
||||
python_version="3.12.4",
|
||||
torch_version="2.12.0+cu130",
|
||||
cuda_version="13.0",
|
||||
package_versions={
|
||||
"triton": "3.4.1",
|
||||
"fastvideo_kernel": "0.3.2",
|
||||
"flash_attn_4": "4.0.0.dev0",
|
||||
"flashinfer": "0.2.11",
|
||||
"nvidia_cutlass_dsl": "4.5.0",
|
||||
},
|
||||
)
|
||||
profile_b = software_profile(
|
||||
python_version="3.12.5",
|
||||
torch_version="2.12.1+cu130",
|
||||
cuda_version="13.0",
|
||||
package_versions={
|
||||
"triton": "3.4.9",
|
||||
"fastvideo_kernel": "0.3.7",
|
||||
"flash_attn_4": "4.0.0.dev1",
|
||||
"flashinfer": "0.2.12",
|
||||
"nvidia_cutlass_dsl": "4.5.1",
|
||||
},
|
||||
)
|
||||
|
||||
assert profile_a == {
|
||||
"python": "3.12",
|
||||
"pytorch": "2.12",
|
||||
"cuda": "13.0",
|
||||
"packages": {
|
||||
"fastvideo_kernel": "0.3.2",
|
||||
"flash_attn_4": "4.0.0.dev0",
|
||||
"flashinfer": "0.2.11",
|
||||
"nvidia_cutlass_dsl": "4.5.0",
|
||||
"triton": "3.4.1",
|
||||
},
|
||||
}
|
||||
assert profile_b["python"] == "3.12"
|
||||
assert profile_b["pytorch"] == "2.12"
|
||||
assert profile_b["cuda"] == "13.0"
|
||||
assert software_profile_id(profile_a) != software_profile_id(profile_b)
|
||||
|
||||
|
||||
def test_installed_package_versions_checks_attention_kernel_distributions(monkeypatch):
|
||||
versions = {
|
||||
"fastvideo-kernel": "0.3.2",
|
||||
"flash-attn": "2.8.1",
|
||||
"flash-attn-4": "4.0.0.dev0",
|
||||
"flash-attention-fp4": "0.1.0",
|
||||
"flashinfer-python": "0.2.11",
|
||||
"nvidia-cutlass-dsl": "4.5.0",
|
||||
}
|
||||
|
||||
def fake_version(name):
|
||||
if name not in versions:
|
||||
raise identity_module.importlib.metadata.PackageNotFoundError(name)
|
||||
return versions[name]
|
||||
|
||||
monkeypatch.setattr(identity_module.importlib.metadata, "version", fake_version)
|
||||
|
||||
installed = identity_module._installed_package_versions()
|
||||
|
||||
assert installed["fastvideo_kernel"] == "0.3.2"
|
||||
assert installed["flash_attn"] == "2.8.1"
|
||||
assert installed["flash_attn_4"] == "4.0.0.dev0"
|
||||
assert installed["flash_attention_fp4"] == "0.1.0"
|
||||
assert installed["flashinfer"] == "0.2.11"
|
||||
assert installed["nvidia_cutlass_dsl"] == "4.5.0"
|
||||
|
||||
|
||||
def test_hardware_profile_id_uses_normalized_gpu_cohort():
|
||||
profile = hardware_profile(
|
||||
num_gpus=2,
|
||||
gpu_devices=[
|
||||
{
|
||||
"name": "NVIDIA L40S",
|
||||
"memory_bytes": 48 * 1024**3,
|
||||
"compute_capability": "8.9",
|
||||
},
|
||||
{
|
||||
"name": "NVIDIA L40S",
|
||||
"memory_gb": 48,
|
||||
"compute_capability": "8.9",
|
||||
},
|
||||
],
|
||||
interconnect="none_or_partial",
|
||||
)
|
||||
|
||||
assert profile == {
|
||||
"device_type": "cuda",
|
||||
"gpu_count": 2,
|
||||
"gpus": [
|
||||
{
|
||||
"name": "NVIDIA L40S",
|
||||
"memory_gb": 48,
|
||||
"compute_capability": "8.9",
|
||||
},
|
||||
{
|
||||
"name": "NVIDIA L40S",
|
||||
"memory_gb": 48,
|
||||
"compute_capability": "8.9",
|
||||
},
|
||||
],
|
||||
"interconnect": "none_or_partial",
|
||||
}
|
||||
assert hardware_profile_id(profile).startswith("hw-")
|
||||
|
||||
|
||||
def test_hardware_profile_pads_missing_requested_cuda_devices(monkeypatch):
|
||||
class Props:
|
||||
name = "NVIDIA L40S"
|
||||
total_memory = 48 * 1024**3
|
||||
|
||||
monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
|
||||
monkeypatch.setattr(torch.cuda, "device_count", lambda: 1)
|
||||
monkeypatch.setattr(torch.cuda, "get_device_properties", lambda device_id: Props())
|
||||
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda device_id: (8, 9))
|
||||
|
||||
profile = hardware_profile(num_gpus=2, interconnect="unknown")
|
||||
|
||||
assert profile["gpu_count"] == 2
|
||||
assert profile["gpus"] == [
|
||||
{
|
||||
"name": "NVIDIA L40S",
|
||||
"memory_gb": 48,
|
||||
"compute_capability": "8.9",
|
||||
},
|
||||
{
|
||||
"name": "unknown",
|
||||
"memory_gb": None,
|
||||
"compute_capability": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_local_path_detection_checks_both_path_separators(monkeypatch):
|
||||
monkeypatch.setattr(identity_module.os, "sep", "\\")
|
||||
|
||||
assert identity_module._looks_like_local_path("models/local/checkpoint") is True
|
||||
assert identity_module._looks_like_local_path(r"C:\models\checkpoint") is True
|
||||
assert identity_module._looks_like_local_path("Wan-AI/Wan2.1-T2V-1.3B-Diffusers") is False
|
||||
|
||||
|
||||
def test_environment_metadata_is_separate_audit_fingerprint():
|
||||
cfg = _benchmark_config()
|
||||
recipe_hash = _fingerprint(cfg)
|
||||
audit_a = environment_metadata(
|
||||
env={"IMAGE_VERSION": "py3.12-cuda13.0.0"},
|
||||
package_versions={"triton": "3.4.1"},
|
||||
hardware={"device_type": "cuda", "gpu_count": 2},
|
||||
software={"python": "3.12", "pytorch": "2.12", "cuda": "13.0"},
|
||||
)
|
||||
audit_b = environment_metadata(
|
||||
env={"IMAGE_VERSION": "py3.12-cuda13.0.1"},
|
||||
package_versions={"triton": "3.4.1"},
|
||||
hardware={"device_type": "cuda", "gpu_count": 2},
|
||||
software={"python": "3.12", "pytorch": "2.12", "cuda": "13.0"},
|
||||
)
|
||||
|
||||
assert recipe_hash == _fingerprint(cfg)
|
||||
assert environment_fingerprint(audit_a) != environment_fingerprint(audit_b)
|
||||
|
||||
|
||||
def test_resolved_revision_from_hf_snapshot_path():
|
||||
revision = "a" * 40
|
||||
|
||||
assert resolved_revision_from_model_path(
|
||||
f"/root/.cache/huggingface/hub/models--org--repo/snapshots/{revision}") == revision
|
||||
assert resolved_revision_from_model_path("/models/local-repo") is None
|
||||
|
||||
|
||||
def test_runtime_identity_from_generator_summarizes_worker_records():
|
||||
from fastvideo.tests.performance.test_inference_performance import _runtime_identity_from_generator
|
||||
|
||||
revision = "b" * 40
|
||||
|
||||
class FakeExecutor:
|
||||
def collective_rpc(self, _method):
|
||||
return [
|
||||
{
|
||||
"resolved_attention_backends": ["FLASH_ATTN"],
|
||||
"resolved_model_path": f"/root/.cache/huggingface/hub/models--org--repo/snapshots/{revision}",
|
||||
},
|
||||
{
|
||||
"resolved_attention_backends": ["FLASH_ATTN"],
|
||||
"resolved_model_path": f"/root/.cache/huggingface/hub/models--org--repo/snapshots/{revision}",
|
||||
},
|
||||
]
|
||||
|
||||
class FakeGenerator:
|
||||
executor = FakeExecutor()
|
||||
|
||||
assert _runtime_identity_from_generator(FakeGenerator()) == {
|
||||
"resolved_attention_backend": "FLASH_ATTN",
|
||||
"resolved_model_revision": revision,
|
||||
}
|
||||
|
||||
|
||||
def test_collect_worker_identity_handles_torch_module_pipeline():
|
||||
from fastvideo.tests.performance.test_inference_performance import _collect_worker_identity
|
||||
|
||||
class Backend:
|
||||
name = "FLASH_ATTN"
|
||||
|
||||
class Leaf(torch.nn.Module):
|
||||
backend = Backend()
|
||||
|
||||
class Pipeline(torch.nn.Module):
|
||||
model_path = "/models/local"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.leaf = Leaf()
|
||||
|
||||
class Worker:
|
||||
pipeline = Pipeline()
|
||||
|
||||
assert _collect_worker_identity(Worker()) == {
|
||||
"resolved_attention_backends": ["FLASH_ATTN"],
|
||||
"resolved_model_path": "/models/local",
|
||||
}
|
||||
@@ -12,12 +12,25 @@ import os
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import pytest
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.tests.performance.identity import (
|
||||
benchmark_identity_from_config,
|
||||
build_recipe_from_benchmark_config,
|
||||
environment_fingerprint,
|
||||
environment_metadata,
|
||||
hardware_profile,
|
||||
hardware_profile_id,
|
||||
recipe_fingerprint,
|
||||
resolved_revision_from_model_path,
|
||||
software_profile,
|
||||
software_profile_id,
|
||||
)
|
||||
from fastvideo.worker.multiproc_executor import MultiprocExecutor
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -34,8 +47,11 @@ V2_REQUIRED_IDENTITY_FIELDS = (
|
||||
"variant_id",
|
||||
"benchmark_version",
|
||||
)
|
||||
# "recipe" is no longer config-declarable: the fingerprint follow-up landed,
|
||||
# and the generated recipe document owns that key in emitted records. A config
|
||||
# declaring it now fails validation loudly instead of being silently
|
||||
# overwritten by the generated one.
|
||||
V2_OPTIONAL_METADATA_FIELDS = (
|
||||
"recipe",
|
||||
"metric_threshold_policy",
|
||||
"quality_metadata",
|
||||
)
|
||||
@@ -227,6 +243,110 @@ def _write_results(results):
|
||||
logger.info("Performance results written to %s", filepath)
|
||||
|
||||
|
||||
def _backend_name(value) -> str:
|
||||
if hasattr(value, "name"):
|
||||
return str(value.name)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _collect_worker_identity(worker) -> dict[str, Any]:
|
||||
pipeline = getattr(worker, "pipeline", None)
|
||||
model_path = getattr(pipeline, "model_path", None)
|
||||
backends: set[str] = set()
|
||||
|
||||
modules = getattr(pipeline, "modules", None)
|
||||
if isinstance(modules, Mapping):
|
||||
module_iter = modules.values()
|
||||
elif isinstance(pipeline, torch.nn.Module):
|
||||
module_iter = (pipeline,)
|
||||
else:
|
||||
module_iter = ()
|
||||
|
||||
for module in module_iter:
|
||||
if isinstance(module, torch.nn.Module):
|
||||
for submodule in module.modules():
|
||||
backend = getattr(submodule, "backend", None)
|
||||
if backend is not None:
|
||||
backends.add(_backend_name(backend))
|
||||
else:
|
||||
backend = getattr(module, "backend", None)
|
||||
if backend is not None:
|
||||
backends.add(_backend_name(backend))
|
||||
|
||||
return {
|
||||
"resolved_attention_backends": sorted(backends),
|
||||
"resolved_model_path": model_path,
|
||||
}
|
||||
|
||||
|
||||
def _single_or_list(values: set[str]) -> str | list[str] | None:
|
||||
ordered = sorted(values)
|
||||
if not ordered:
|
||||
return None
|
||||
if len(ordered) == 1:
|
||||
return ordered[0]
|
||||
return ordered
|
||||
|
||||
|
||||
def _runtime_identity_from_generator(generator) -> dict[str, Any]:
|
||||
worker_records = generator.executor.collective_rpc(_collect_worker_identity)
|
||||
resolved_backends: set[str] = set()
|
||||
resolved_revisions: set[str] = set()
|
||||
|
||||
for record in worker_records:
|
||||
resolved_backends.update(record.get("resolved_attention_backends") or [])
|
||||
revision = resolved_revision_from_model_path(record.get("resolved_model_path"))
|
||||
if revision is not None:
|
||||
resolved_revisions.add(revision)
|
||||
|
||||
return {
|
||||
"resolved_attention_backend": _single_or_list(resolved_backends),
|
||||
"resolved_model_revision": _single_or_list(resolved_revisions),
|
||||
}
|
||||
|
||||
|
||||
def _benchmark_identity_fields(cfg):
|
||||
identity = benchmark_identity_from_config(cfg)
|
||||
return {
|
||||
key: identity[key]
|
||||
for key in ("workload_id", "variant_id", "benchmark_version")
|
||||
}
|
||||
|
||||
|
||||
def _build_identity_fields(cfg, init_kwargs, prompt, runtime_identity):
|
||||
# Legacy v1 configs (no config_schema_version) stay on the legacy
|
||||
# (model_id, gpu_type) cohort: they lack the required identity fields,
|
||||
# and building a recipe for them would raise AFTER the GPU measurement
|
||||
# completed. The validator already enforces that v2 configs carry the
|
||||
# identity fields, so v2 records always get the full identity block.
|
||||
if cfg.get("config_schema_version") is None:
|
||||
return {}
|
||||
recipe = build_recipe_from_benchmark_config(
|
||||
cfg,
|
||||
resolved_attention_backend=runtime_identity.get("resolved_attention_backend"),
|
||||
resolved_model_revision=runtime_identity.get("resolved_model_revision"),
|
||||
measured_prompts=[prompt],
|
||||
)
|
||||
num_gpus = init_kwargs.get("num_gpus", cfg.get("run_config", {}).get("required_gpus", 1))
|
||||
hw_profile = hardware_profile(num_gpus=num_gpus)
|
||||
sw_profile = software_profile()
|
||||
env_metadata = environment_metadata(
|
||||
hardware=hw_profile,
|
||||
software=sw_profile,
|
||||
)
|
||||
return {
|
||||
"recipe": recipe,
|
||||
"recipe_fingerprint": recipe_fingerprint(recipe),
|
||||
"hardware_profile": hw_profile,
|
||||
"hardware_profile_id": hardware_profile_id(hw_profile),
|
||||
"software_profile": sw_profile,
|
||||
"software_profile_id": software_profile_id(sw_profile),
|
||||
"environment_metadata": env_metadata,
|
||||
"environment_fingerprint": environment_fingerprint(env_metadata),
|
||||
**_benchmark_identity_fields(cfg),
|
||||
}
|
||||
|
||||
|
||||
# -- Test -------------------------------------------------------------------
|
||||
|
||||
def _run_benchmark(cfg):
|
||||
@@ -264,6 +384,7 @@ def _run_benchmark(cfg):
|
||||
model_path=model_info["model_path"],
|
||||
**init_kwargs,
|
||||
)
|
||||
runtime_identity = _runtime_identity_from_generator(generator)
|
||||
|
||||
for i in range(num_warmup):
|
||||
logger.info("Warmup run %d/%d", i + 1, num_warmup)
|
||||
@@ -318,6 +439,7 @@ def _run_benchmark(cfg):
|
||||
"dit_time_s": _avg_component(all_component_times, "dit_time_s"),
|
||||
"vae_decode_time_s": _avg_component(all_component_times,
|
||||
"vae_decode_time_s"),
|
||||
**_build_identity_fields(cfg, init_kwargs, prompt, runtime_identity),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
||||
Reference in New Issue
Block a user