Skip to content

Commit 4a177ef

Browse files
[feat]: standalone QAD 5090 benchmark harness (multi-run stats)
1 parent 819da3f commit 4a177ef

1 file changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""Standalone benchmark harness for the FastWan-QAD Wan2.1-1.3B recipes.
2+
3+
Measures steady-state end-to-end generation latency (denoise + optional TAEHV
4+
decode) over N timed runs after a warmup phase, and prints a min/max/mean/std
5+
stats table. This is the multi-run timing machinery that used to live in
6+
``FastWan_QAD_TAEHV.py``, kept as a separate harness so the example scripts
7+
stay lean single-run examples.
8+
9+
Pipeline setup mirrors the corresponding example script in this directory:
10+
fp8 -> fp8_wan2_1_1_3b.py (FastVideo/FastWan-QAD-FP8-1.3B, SAGE_ATTN)
11+
nvfp4_qat -> nvfp4_qat_wan2_1_1_3b.py (FastVideo/FastWan-QAD-1.3B, ATTN_QAT_INFER)
12+
nvfp4_sa2 -> nvfp4_sa2_wan2_1_1_3b.py (FastVideo/FastWan-QAD-1.3B-SA2, SAGE_ATTN)
13+
14+
Hardware requirements per mode (default quantized paths; --bf16 relaxes them):
15+
fp8 sm89+ (H100, L40S, RTX 4090, Ada Lovelace, or newer)
16+
nvfp4_qat RTX 5090-class Blackwell (sm_120a): flashinfer FP4 gemm + attn_qat_infer
17+
nvfp4_sa2 RTX 5090-class Blackwell (sm_120a): flashinfer FP4 gemm + SageAttention2
18+
19+
Usage:
20+
python benchmark_qad_wan2_1_1_3b.py --mode nvfp4_qat --taehv-checkpoint /path/to/taehv/taew2_1.pth
21+
python benchmark_qad_wan2_1_1_3b.py --mode fp8 --warmups 5 --runs 20
22+
python benchmark_qad_wan2_1_1_3b.py --mode nvfp4_sa2 --bf16 # BF16 baseline
23+
"""
24+
25+
import argparse
26+
import contextlib
27+
import logging
28+
import os
29+
import statistics
30+
import time
31+
32+
import torch
33+
34+
from _qad_common import flashinfer_arch_list, require_fp4_capable_gpu
35+
36+
OUTPUT_PATH = "video_samples"
37+
38+
# mode -> (default model, attention backend, needs the flashinfer FP4 gemm path)
39+
MODES = {
40+
"fp8": ("FastVideo/FastWan-QAD-FP8-1.3B", "SAGE_ATTN", False),
41+
"nvfp4_qat": ("FastVideo/FastWan-QAD-1.3B", "ATTN_QAT_INFER", True),
42+
"nvfp4_sa2": ("FastVideo/FastWan-QAD-1.3B-SA2", "SAGE_ATTN", True),
43+
}
44+
45+
PROMPT = (
46+
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
47+
"wide with interest. The playful yet serene atmosphere is complemented by soft "
48+
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
49+
)
50+
51+
52+
class TaehvDecoder:
53+
def __init__(self, checkpoint_path: str, device: str = "cuda",
54+
dtype: torch.dtype = torch.float16) -> None:
55+
from taehv import TAEHV
56+
self.device = device
57+
self.dtype = dtype
58+
print(f"Loading TAEHV from {checkpoint_path} ...")
59+
self.model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype).eval()
60+
61+
@torch.no_grad()
62+
def decode(self, latents: torch.Tensor):
63+
latents = latents.permute(0, 2, 1, 3, 4).to(self.device, self.dtype)
64+
decoded = self.model.decode_video(latents, parallel=True, show_progress_bar=False)
65+
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
66+
return frames.permute(0, 2, 3, 1).cpu().numpy()
67+
68+
69+
@contextlib.contextmanager
70+
def silence_request_log():
71+
"""Quiet ``VideoGenerator.generate``'s per-request multi-line INFO printout
72+
so the per-run timing lines stay readable."""
73+
vg_logger = logging.getLogger("fastvideo.entrypoints.video_generator")
74+
prev_level = vg_logger.level
75+
vg_logger.setLevel(logging.WARNING)
76+
try:
77+
yield
78+
finally:
79+
vg_logger.setLevel(prev_level)
80+
81+
82+
def main() -> None:
83+
parser = argparse.ArgumentParser(description="Multi-run QAD Wan2.1-1.3B benchmark")
84+
parser.add_argument("--mode", required=True, choices=sorted(MODES),
85+
help="Which QAD recipe to benchmark (mirrors the example script of the same name)")
86+
parser.add_argument("--warmups", type=int, default=5,
87+
help="Warmup runs before timing (default: 5)")
88+
parser.add_argument("--runs", type=int, default=20,
89+
help="Timed runs to collect min/max/mean/std over (default: 20)")
90+
parser.add_argument("--bf16", action="store_true", help="BF16 baseline (no quantization)")
91+
parser.add_argument("--granularity", choices=["tensor", "channel"], default="tensor",
92+
help="FP8 weight scale granularity (fp8 mode only)")
93+
parser.add_argument("--taehv-checkpoint", default=None, metavar="PATH",
94+
help="Path to taew2_1.pth; enables TAEHV tiny autoencoder decoding")
95+
parser.add_argument("--model", default=None,
96+
help="Model path or HuggingFace ID (default: per --mode)")
97+
parser.add_argument("--no-compile", action="store_true", help="Disable torch.compile for the DiT")
98+
parser.add_argument("--num_gpus", type=int, default=1)
99+
parser.add_argument("--infer_steps", type=int, default=3)
100+
args = parser.parse_args()
101+
102+
model, backend, needs_fp4 = MODES[args.mode]
103+
if args.model:
104+
model = args.model
105+
106+
if needs_fp4 and not args.bf16:
107+
require_fp4_capable_gpu()
108+
109+
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", backend)
110+
if needs_fp4:
111+
os.environ["FASTVIDEO_DISABLE_ATTENTION_COMPILE"] = "0"
112+
os.environ.setdefault("FLASHINFER_CUDA_ARCH_LIST", flashinfer_arch_list())
113+
114+
from fastvideo import VideoGenerator
115+
from fastvideo.configs.pipelines.base import PipelineConfig
116+
117+
mode = "bf16" if args.bf16 else args.mode
118+
if not args.no_compile:
119+
mode += "_compile"
120+
use_taehv = args.taehv_checkpoint is not None
121+
print(f"Mode: {mode.upper()} model={model} " + ("decoder=TAEHV" if use_taehv else "decoder=VAE"))
122+
123+
taehv = TaehvDecoder(args.taehv_checkpoint) if use_taehv else None
124+
125+
pipeline_config = PipelineConfig.from_pretrained(model)
126+
pipeline_config.text_encoder_precisions = ("bf16",)
127+
if not args.bf16:
128+
if args.mode == "fp8":
129+
from fastvideo.layers.quantization import get_quantization_config
130+
pipeline_config.dit_config.quant_config = get_quantization_config("FP8")(
131+
granularity=args.granularity)
132+
else:
133+
from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
134+
pipeline_config.dit_config.quant_config = NVFP4QATConfig()
135+
136+
generator = VideoGenerator.from_pretrained(
137+
model,
138+
pipeline_config=pipeline_config,
139+
num_gpus=args.num_gpus,
140+
use_fsdp_inference=False,
141+
dit_cpu_offload=False,
142+
dit_layerwise_offload=False,
143+
vae_cpu_offload=use_taehv,
144+
text_encoder_cpu_offload=False,
145+
pin_cpu_memory=False,
146+
enable_torch_compile=not args.no_compile,
147+
enable_torch_compile_text_encoder=not args.no_compile,
148+
enable_torch_compile_vae=not args.no_compile and not use_taehv,
149+
output_type="latent" if use_taehv else "pil",
150+
)
151+
152+
request = {
153+
"prompt": PROMPT,
154+
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
155+
"output": {"save_video": False},
156+
}
157+
158+
# Warmup: pay the torch.compile cost and warm TAEHV's cuDNN algo selection
159+
# so the timed runs below measure steady-state latency only.
160+
with silence_request_log():
161+
for _ in range(args.warmups):
162+
warm = generator.generate(request=request)
163+
if use_taehv:
164+
taehv.decode(warm.samples)
165+
166+
# Benchmark: time each run end-to-end. ``denoise`` is the generator's own
167+
# generation_time; for TAEHV we add the in-script decode. Nothing is written
168+
# to disk inside the loop so I/O never pollutes the timings.
169+
denoise_times: list[float] = []
170+
decode_times: list[float] = []
171+
totals: list[float] = []
172+
frames = None
173+
with silence_request_log():
174+
for i in range(args.runs):
175+
result = generator.generate(request=request)
176+
denoise_elapsed = result.generation_time
177+
denoise_times.append(denoise_elapsed)
178+
179+
if use_taehv:
180+
torch.cuda.synchronize()
181+
decode_start = time.perf_counter()
182+
frames = taehv.decode(result.samples)
183+
torch.cuda.synchronize()
184+
decode_elapsed = time.perf_counter() - decode_start
185+
decode_times.append(decode_elapsed)
186+
total = denoise_elapsed + decode_elapsed
187+
else:
188+
total = denoise_elapsed
189+
totals.append(total)
190+
191+
line = f" run {i + 1:02d}/{args.runs}: {total:.3f}s"
192+
if use_taehv:
193+
line += f" (denoise {denoise_elapsed:.3f}s + decode {decode_elapsed:.3f}s)"
194+
print(line)
195+
196+
if frames is not None:
197+
os.makedirs(OUTPUT_PATH, exist_ok=True)
198+
import imageio
199+
output_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
200+
imageio.mimsave(output_path, frames, fps=16, format="mp4")
201+
print(f"Saved video to {output_path}")
202+
203+
# Report min / max / mean / std over the timed runs.
204+
def _stat_row(name: str, xs: list[float]) -> str:
205+
std = statistics.stdev(xs) if len(xs) > 1 else 0.0
206+
return (f" {name:<11}{min(xs):>8.3f}{max(xs):>9.3f}"
207+
f"{statistics.mean(xs):>9.3f}{std:>9.3f}")
208+
209+
if totals:
210+
print(f"\n[{mode.upper()}] {args.runs} runs, {args.warmups} warmup, "
211+
f"{args.infer_steps} steps:")
212+
print(f" {'metric':<11}{'min':>8}{'max':>9}{'mean':>9}{'std':>9} (s)")
213+
print(_stat_row("total", totals))
214+
if use_taehv:
215+
print(_stat_row("denoise", denoise_times))
216+
print(_stat_row("decode", decode_times))
217+
218+
generator.shutdown()
219+
220+
221+
if __name__ == "__main__":
222+
main()

0 commit comments

Comments
 (0)