Files
2026-10-06 12:17:11 -07:00

150 lines
6.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Time a resident FastH3 eight-forward Spark recipe with the release prompts.
One process loads the model, runs one excluded ceramics warmup, then times
each prompt at least twice. Every call writes a video. This script does not alter
the V2 schedule, VSA sparsity, or video resolution.
"""
from __future__ import annotations
import argparse
import json
import statistics
import time
from copy import deepcopy
from pathlib import Path
from fastvideo import VideoGenerator
from fastvideo.api.parser import load_raw_config, parse_config
from fastvideo.api.schema import RunConfig
PROMPT_IDS = ("latency-ceramics-005", "latency-harbor-005")
def _stage_metrics(result: object) -> dict[str, dict]:
logging_info = getattr(result, "logging_info", None)
stages = getattr(logging_info, "stages", None)
if isinstance(logging_info, dict):
stages = logging_info.get("stages", stages)
if not isinstance(stages, dict):
return {}
return {name: metrics for name, metrics in stages.items() if isinstance(metrics, dict)}
def _stage_seconds(metrics: dict[str, dict]) -> dict[str, float]:
return {name: float(stage["execution_time"]) for name, stage in metrics.items()
if stage.get("execution_time") is not None}
def _peak_mb(metrics: dict[str, dict], key: str) -> float | None:
values = [float(stage[key]) for stage in metrics.values() if stage.get(key) is not None]
return max(values) if values else None
def _stage_total(stages: dict[str, float], fragment: str, exclude: str | None = None) -> float | None:
matches = [
seconds for name, seconds in stages.items()
if fragment in name.lower() and (exclude is None or exclude not in name.lower())
]
return sum(matches) if matches else None
def _request(base: RunConfig, prompt: str, frames: int, width: int, height: int,
output: Path):
request = deepcopy(base.request)
request.prompt = prompt
request.inputs.prompt_path = None
request.sampling.num_frames = frames
request.sampling.width = width
request.sampling.height = height
request.output.output_path = str(output)
request.output.save_video = True
request.output.return_frames = False
return request
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--prompts", type=Path, required=True)
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--model-path", type=Path)
parser.add_argument("--frames", type=int, default=124)
parser.add_argument("--width", type=int, default=832)
parser.add_argument("--height", type=int, default=480)
parser.add_argument("--repeats", type=int, default=2)
args = parser.parse_args()
if args.frames not in (124, 243):
parser.error("use 124 frames for roughly five seconds or 243 for roughly ten seconds")
if args.repeats < 2:
parser.error("the release protocol requires at least two timed calls")
config = parse_config(RunConfig, load_raw_config(args.config))
if args.model_path:
config.generator.model_path = str(args.model_path)
if config.request.sampling.num_inference_steps != 9:
parser.error("the V2 contract requires nine sigma points for eight DiT forwards")
if config.generator.engine.offload.lazy_module_load is not False:
parser.error("the resident recipe requires lazy_module_load: false")
contract = Path(config.generator.model_path) / "fastvideo_inference.json"
if not contract.is_file():
parser.error(f"missing trained V2 schedule: {contract}")
inference = json.loads(contract.read_text())
if inference.get("num_inference_steps") != 9 or inference.get("transformer_forwards") != 8:
parser.error("the checkpoint is not the trained V2 eight-forward schedule")
prompts = json.loads(args.prompts.read_text())
if any(prompt_id not in prompts for prompt_id in PROMPT_IDS):
parser.error(f"prompt JSON must contain {', '.join(PROMPT_IDS)}")
args.output_dir.mkdir(parents=True, exist_ok=True)
generator = VideoGenerator.from_config(config.generator)
try:
for prompt_index, prompt_id in enumerate(PROMPT_IDS):
times = []
first_index = 0 if prompt_index == 0 else 1
for index in range(first_index, args.repeats + 1):
warmup = index == 0
label = "warmup" if warmup else f"run-{index:02d}"
requested_path = args.output_dir / f"{prompt_id}-{args.width}x{args.height}-{args.frames}-{label}.mp4"
request = _request(config, prompts[prompt_id], args.frames, args.width, args.height,
requested_path)
started = time.perf_counter()
result = generator.generate(request)
wall = time.perf_counter() - started
output = Path(result.video_path) if result.video_path else requested_path
if not output.is_file():
raise RuntimeError(f"generation returned without an MP4: {output}")
metrics = _stage_metrics(result)
stages = _stage_seconds(metrics)
row = {
"prompt_id": prompt_id,
"warmup": warmup,
"frames": args.frames,
"width": args.width,
"height": args.height,
"e2e_seconds": round(wall, 3),
"denoise_seconds": _stage_total(stages, "denois"),
"decode_seconds": _stage_total(stages, "decod", exclude="postdecode"),
"postprocess_seconds": _stage_total(stages, "postdecode"),
"peak_memory_mb": _peak_mb(metrics, "peak_allocated_mb"),
"peak_reserved_mb": _peak_mb(metrics, "peak_reserved_mb"),
"result_peak_memory_mb": result.peak_memory_mb,
"stages": stages,
"mp4": str(output),
}
print(json.dumps(row, sort_keys=True), flush=True)
if not warmup:
times.append(wall)
print(json.dumps({"prompt_id": prompt_id, "timed_runs": len(times),
"median_e2e_seconds": round(statistics.median(times), 3)},
sort_keys=True), flush=True)
finally:
generator.shutdown()
if __name__ == "__main__":
main()