Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a177efcaf | ||
|
|
819da3f221 | ||
|
|
de1f573647 | ||
|
|
3224a6e406 |
@@ -1,316 +0,0 @@
|
||||
"""Fast NVFP4 linear inference for Wan2.1-T2V-1.3B with TAEHV decoding.
|
||||
|
||||
This is the FP4-linear fast path from ``fp4_linear_wan2_1_1_3b.py`` with the
|
||||
heavy Wan VAE swapped out for TAEHV -- a tiny autoencoder that decodes Wan2.1
|
||||
latents directly (no denormalization) and is dramatically faster / lighter.
|
||||
|
||||
How it works: the generator runs with ``output_type="latent"`` so the pipeline
|
||||
returns raw denoised latents instead of pixels (the Wan VAE is offloaded and
|
||||
never used). We then decode those latents with TAEHV in this script and save
|
||||
the frames ourselves. This mirrors the FastVideo-Quantization
|
||||
``quantization_example_taehv.py`` proof-of-concept, but kept clean: TAEHV is a
|
||||
pip package (no ``sys.path`` hacks), the latent->uint8 conversion is vectorized,
|
||||
and there is no dead profiler / sanitization code.
|
||||
|
||||
Requirements:
|
||||
- Blackwell GPU (B200/B300, sm100a/sm103a) for the FP4 linear path
|
||||
- flashinfer (``pip install flashinfer-python``)
|
||||
- TAEHV weights ``taew2_1.pth`` (https://github.com/madebyollin/taehv)
|
||||
|
||||
Usage:
|
||||
python fp4_linear_taehv_wan2_1_1_3b.py # FP4 + TAEHV + compile
|
||||
python fp4_linear_taehv_wan2_1_1_3b.py --no-taehv # FP4 + full Wan VAE
|
||||
python fp4_linear_taehv_wan2_1_1_3b.py --no-compile # eager
|
||||
python fp4_linear_taehv_wan2_1_1_3b.py --baseline # dense bf16 reference
|
||||
python fp4_linear_taehv_wan2_1_1_3b.py --distilled_model '' # base Wan2.1 weights
|
||||
python fp4_linear_taehv_wan2_1_1_3b.py --warmups 5 --benchmark-runs 20 # timing stats (default)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import statistics
|
||||
import time
|
||||
|
||||
import imageio
|
||||
import torch
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
|
||||
|
||||
OUTPUT_PATH = "video_samples"
|
||||
|
||||
# Distilled, quantization-aware (QAD) transformer for Wan2.1-1.3B (3 steps,
|
||||
# guidance 1.0). Loaded on top of the base Wan2.1 pipeline; pass
|
||||
# ``--distilled_model ''`` to run the base weights instead.
|
||||
DEFAULT_DISTILLED_MODEL = "FastVideo/FastWan-QAD-1.3B"
|
||||
DISTILLED_WEIGHTS_FILE = (
|
||||
"generator_inference_transformer/diffusion_pytorch_model.safetensors"
|
||||
)
|
||||
|
||||
# TAEHV checkpoint for Wan2.1. Clone https://github.com/madebyollin/taehv to get
|
||||
# ``taew2_1.pth`` (Wan 2.1 / Wan 2.2-14B / Qwen-Image all use this VAE).
|
||||
DEFAULT_TAEHV_CHECKPOINT = "/root/taehv/taew2_1.pth"
|
||||
|
||||
PROMPT = (
|
||||
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
|
||||
"wide with interest. The playful yet serene atmosphere is complemented by soft "
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
|
||||
|
||||
class TaehvDecoder:
|
||||
"""Thin wrapper around the TAEHV tiny autoencoder for Wan2.1 latents.
|
||||
|
||||
TAEHV consumes the *normalized* latents the diffusion model produces (the
|
||||
same representation FastVideo carries internally), so no denormalization is
|
||||
needed -- unlike the full Wan VAE path.
|
||||
"""
|
||||
|
||||
def __init__(self, checkpoint_path: str, device: str = "cuda",
|
||||
dtype: torch.dtype = torch.float16) -> None:
|
||||
from taehv import TAEHV # pip-installed; no sys.path manipulation
|
||||
self.device = device
|
||||
self.dtype = dtype
|
||||
print(f"Loading TAEHV from {checkpoint_path} ...")
|
||||
self.model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype).eval()
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor):
|
||||
"""Decode FastVideo latents into uint8 RGB frames.
|
||||
|
||||
Args:
|
||||
latents: ``[B, C, T, H, W]`` (NCTHW) normalized latent tensor.
|
||||
|
||||
Returns:
|
||||
A ``(T, H, W, 3)`` uint8 numpy array ready for ``imageio.mimsave``.
|
||||
"""
|
||||
# NCTHW -> NTCHW (TAEHV's expected layout), on the TAEHV device/dtype.
|
||||
latents = latents.permute(0, 2, 1, 3, 4).to(self.device, self.dtype)
|
||||
decoded = self.model.decode_video(
|
||||
latents, parallel=True, show_progress_bar=False)
|
||||
# decoded: [B, T, 3, H, W] in [0, 1]. Take batch 0, vectorize to uint8.
|
||||
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
|
||||
return frames.permute(0, 2, 3, 1).cpu().numpy()
|
||||
|
||||
|
||||
def resolve_distilled_weights(hf_id: str) -> str:
|
||||
"""Return a local path to the distilled transformer safetensors."""
|
||||
if os.path.exists(hf_id):
|
||||
return hf_id
|
||||
from huggingface_hub import hf_hub_download
|
||||
return hf_hub_download(repo_id=hf_id, filename=DISTILLED_WEIGHTS_FILE)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def silence_request_log():
|
||||
"""Quiet ``VideoGenerator.generate``'s per-request config printout.
|
||||
|
||||
Each ``generate(...)`` call logs a multi-line debug block (height/width/
|
||||
prompt/steps/...) at INFO via ``logger.info`` in
|
||||
``fastvideo.entrypoints.video_generator``. There is no built-in switch,
|
||||
so this context manager raises that logger's level to WARNING while the
|
||||
warmup calls run, then restores it for the timed run.
|
||||
"""
|
||||
vg_logger = logging.getLogger("fastvideo.entrypoints.video_generator")
|
||||
prev_level = vg_logger.level
|
||||
vg_logger.setLevel(logging.WARNING)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
vg_logger.setLevel(prev_level)
|
||||
|
||||
|
||||
def resolve_taehv_checkpoint(path: str) -> str:
|
||||
"""Validate the TAEHV checkpoint path, with a helpful error if missing."""
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
raise FileNotFoundError(
|
||||
f"TAEHV checkpoint not found at {path!r}. Clone the weights with:\n"
|
||||
" git clone https://github.com/madebyollin/taehv\n"
|
||||
"and pass --taehv_checkpoint <repo>/taew2_1.pth")
|
||||
|
||||
|
||||
def build_generator(args: argparse.Namespace) -> VideoGenerator:
|
||||
model_id = args.model
|
||||
|
||||
# Half precision everywhere; DiT linears are additionally NVFP4-quantized
|
||||
# via dit_config.quant_config below.
|
||||
pipeline_config = PipelineConfig.from_pretrained(model_id)
|
||||
pipeline_config.dit_precision = "bf16"
|
||||
pipeline_config.vae_precision = "bf16"
|
||||
pipeline_config.text_encoder_precisions = ("bf16",)
|
||||
|
||||
if not args.baseline:
|
||||
pipeline_config.dit_config.quant_config = NVFP4QATConfig()
|
||||
|
||||
compile_enabled = not args.no_compile
|
||||
|
||||
extra_kwargs = {}
|
||||
if args.distilled_model:
|
||||
weights_path = resolve_distilled_weights(args.distilled_model)
|
||||
print(f"Using distilled weights: {args.distilled_model} -> {weights_path}")
|
||||
extra_kwargs["init_weights_from_safetensors"] = weights_path
|
||||
|
||||
if args.taehv:
|
||||
# Skip the in-pipeline VAE decode entirely: the pipeline returns raw
|
||||
# latents, the Wan VAE is offloaded to CPU (and not compiled) since we
|
||||
# decode with TAEHV in this script instead.
|
||||
extra_kwargs["output_type"] = "latent"
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
model_id,
|
||||
pipeline_config=pipeline_config,
|
||||
num_gpus=args.num_gpus,
|
||||
# Keep everything resident on the GPU -- no offloading, except the
|
||||
# unused Wan VAE when TAEHV handles decoding.
|
||||
use_fsdp_inference=False,
|
||||
dit_cpu_offload=False,
|
||||
dit_layerwise_offload=False,
|
||||
vae_cpu_offload=args.taehv,
|
||||
text_encoder_cpu_offload=False,
|
||||
pin_cpu_memory=False,
|
||||
enable_torch_compile=compile_enabled,
|
||||
enable_torch_compile_text_encoder=compile_enabled,
|
||||
enable_torch_compile_vae=compile_enabled and not args.taehv,
|
||||
**extra_kwargs,
|
||||
)
|
||||
return generator
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="FP4 linear Wan2.1-1.3B with TAEHV decoding benchmark")
|
||||
parser.add_argument("--model", default="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
help="Model path or HuggingFace ID")
|
||||
parser.add_argument("--baseline", action="store_true",
|
||||
help="Run dense bf16 instead of FP4 linear")
|
||||
parser.add_argument("--no-compile", action="store_true",
|
||||
help="Disable torch.compile (eager)")
|
||||
parser.add_argument("--taehv", action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Decode with TAEHV instead of the full Wan VAE "
|
||||
"(use --no-taehv for the Wan VAE path)")
|
||||
parser.add_argument("--taehv_checkpoint", default=DEFAULT_TAEHV_CHECKPOINT,
|
||||
help="Path to the TAEHV taew2_1.pth checkpoint")
|
||||
parser.add_argument("--distilled_model", default=DEFAULT_DISTILLED_MODEL,
|
||||
help="HuggingFace ID (or local path) of a distilled "
|
||||
"transformer checkpoint to load on top of --model. "
|
||||
"Pass '' to use the base --model weights instead.")
|
||||
parser.add_argument("--num_gpus", type=int, default=1)
|
||||
parser.add_argument("--infer_steps", type=int, default=3)
|
||||
parser.add_argument("--guidance_scale", type=float, default=1.0)
|
||||
parser.add_argument("--warmups", type=int, default=5,
|
||||
help="Warmup runs before timing (default: 5).")
|
||||
parser.add_argument("--benchmark-runs", type=int, default=20,
|
||||
help="Timed runs to collect min/max/mean/std over (default: 20).")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
raise SystemExit("CUDA is required for FP4 inference.")
|
||||
|
||||
cap = torch.cuda.get_device_capability()
|
||||
print(f"GPU: {torch.cuda.get_device_name()} (capability {cap[0]}.{cap[1]})")
|
||||
if not args.baseline and cap[0] < 10:
|
||||
print("Warning: NVFP4 requires Blackwell (capability 10.0+); "
|
||||
"FP4 kernels may be unavailable on this GPU.")
|
||||
|
||||
mode = "bf16" if args.baseline else "fp4_linear"
|
||||
mode += "_taehv" if args.taehv else "_wanvae"
|
||||
if not args.no_compile:
|
||||
mode += "_compile"
|
||||
print(f"Mode: {mode.upper()}")
|
||||
|
||||
# Load TAEHV before the (slow) generator build so a bad checkpoint path
|
||||
# fails fast.
|
||||
taehv = TaehvDecoder(resolve_taehv_checkpoint(args.taehv_checkpoint)) \
|
||||
if args.taehv else None
|
||||
|
||||
generator = build_generator(args)
|
||||
|
||||
os.makedirs(OUTPUT_PATH, exist_ok=True)
|
||||
|
||||
# Warmup: pay the DiT torch.compile cost and warm TAEHV's cuDNN algo
|
||||
# selection + allocator growth (~0.2s on the first decode) so the timed
|
||||
# runs below measure steady-state latency only.
|
||||
with silence_request_log():
|
||||
for _ in range(args.warmups):
|
||||
warm = generator.generate(request={
|
||||
"prompt": PROMPT,
|
||||
"sampling": {"num_inference_steps": 2, "guidance_scale": args.guidance_scale},
|
||||
"output": {"save_video": False, "return_frames": args.taehv},
|
||||
})
|
||||
if args.taehv:
|
||||
taehv.decode(warm.samples)
|
||||
|
||||
output_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
|
||||
|
||||
# Benchmark: time each run end-to-end. ``denoise`` is the generator's own
|
||||
# generation_time; for TAEHV we add the in-script decode. The mp4 is not
|
||||
# written inside the loop (only the final run's frames are saved below) so
|
||||
# disk I/O never pollutes the timings.
|
||||
denoise_times: list[float] = []
|
||||
decode_times: list[float] = []
|
||||
totals: list[float] = []
|
||||
frames = None
|
||||
with silence_request_log():
|
||||
for i in range(args.benchmark_runs):
|
||||
result = generator.generate(request={
|
||||
"prompt": PROMPT,
|
||||
"sampling": {
|
||||
"num_inference_steps": args.infer_steps,
|
||||
"guidance_scale": args.guidance_scale,
|
||||
},
|
||||
"output": {
|
||||
"save_video": False,
|
||||
"return_frames": args.taehv,
|
||||
"output_path": output_path,
|
||||
},
|
||||
})
|
||||
denoise_elapsed = result.generation_time
|
||||
denoise_times.append(denoise_elapsed)
|
||||
|
||||
if args.taehv:
|
||||
torch.cuda.synchronize()
|
||||
decode_start = time.perf_counter()
|
||||
frames = taehv.decode(result.samples)
|
||||
torch.cuda.synchronize()
|
||||
decode_elapsed = time.perf_counter() - decode_start
|
||||
decode_times.append(decode_elapsed)
|
||||
total = denoise_elapsed + decode_elapsed
|
||||
else:
|
||||
total = denoise_elapsed
|
||||
totals.append(total)
|
||||
|
||||
line = f" run {i + 1:02d}/{args.benchmark_runs}: {total:.3f}s"
|
||||
if args.taehv:
|
||||
line += (f" (denoise {denoise_elapsed:.3f}s + "
|
||||
f"decode {decode_elapsed:.3f}s)")
|
||||
print(line)
|
||||
|
||||
if args.taehv and frames is not None:
|
||||
imageio.mimsave(output_path, frames, fps=16, format="mp4")
|
||||
print(f"Saved video to {output_path}")
|
||||
|
||||
# Report min / max / mean / std over the timed runs.
|
||||
def _stat_row(name: str, xs: list[float]) -> str:
|
||||
std = statistics.stdev(xs) if len(xs) > 1 else 0.0
|
||||
return (f" {name:<11}{min(xs):>8.3f}{max(xs):>9.3f}"
|
||||
f"{statistics.mean(xs):>9.3f}{std:>9.3f}")
|
||||
|
||||
if totals:
|
||||
print(f"\n[{mode.upper()}] {args.benchmark_runs} runs, {args.warmups} warmup, "
|
||||
f"{args.infer_steps} steps:")
|
||||
print(f" {'metric':<11}{'min':>8}{'max':>9}{'mean':>9}{'std':>9} (s)")
|
||||
print(_stat_row("total", totals))
|
||||
if args.taehv:
|
||||
print(_stat_row("denoise", denoise_times))
|
||||
print(_stat_row("decode", decode_times))
|
||||
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Shared GPU-arch helpers for the QAD 5090 example scripts.
|
||||
|
||||
The default (non ``--bf16``) path of the NVFP4 scripts runs DiT linears through
|
||||
flashinfer's cutlass FP4 gemm, which ships sm_120a cubins only (RTX 5090-class
|
||||
consumer Blackwell). These helpers pick the right FLASHINFER_CUDA_ARCH_LIST and
|
||||
fail fast with a readable error instead of an opaque flashinfer ValueError.
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
# Capabilities the flashinfer FP4 gemm path has cubins for.
|
||||
FP4_CAPABILITIES = ((12, 0), (12, 1))
|
||||
|
||||
|
||||
def flashinfer_arch_list() -> str:
|
||||
"""FLASHINFER_CUDA_ARCH_LIST derived from the local GPU, defaulting to 12.0a."""
|
||||
if torch.cuda.is_available():
|
||||
major, minor = torch.cuda.get_device_capability()
|
||||
if (major, minor) in FP4_CAPABILITIES:
|
||||
return f"{major}.{minor}a"
|
||||
return "12.0a"
|
||||
|
||||
|
||||
def require_fp4_capable_gpu() -> None:
|
||||
"""Exit with a one-line error if the GPU cannot run the NVFP4 gemm path."""
|
||||
cap = torch.cuda.get_device_capability() if torch.cuda.is_available() else None
|
||||
if cap not in FP4_CAPABILITIES:
|
||||
raise SystemExit(
|
||||
f"The default NVFP4 path requires an sm_120a GPU (RTX 5090-class Blackwell, "
|
||||
f"compute capability 12.0/12.1); found {cap}. Re-run with --bf16 on other GPUs.")
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Standalone benchmark harness for the FastWan-QAD Wan2.1-1.3B recipes.
|
||||
|
||||
Measures steady-state end-to-end generation latency (denoise + optional TAEHV
|
||||
decode) over N timed runs after a warmup phase, and prints a min/max/mean/std
|
||||
stats table. This is the multi-run timing machinery that used to live in
|
||||
``FastWan_QAD_TAEHV.py``, kept as a separate harness so the example scripts
|
||||
stay lean single-run examples.
|
||||
|
||||
Pipeline setup mirrors the corresponding example script in this directory:
|
||||
fp8 -> fp8_wan2_1_1_3b.py (FastVideo/FastWan-QAD-FP8-1.3B, SAGE_ATTN)
|
||||
nvfp4_qat -> nvfp4_qat_wan2_1_1_3b.py (FastVideo/FastWan-QAD-1.3B, ATTN_QAT_INFER)
|
||||
nvfp4_sa2 -> nvfp4_sa2_wan2_1_1_3b.py (FastVideo/FastWan-QAD-1.3B-SA2, SAGE_ATTN)
|
||||
|
||||
Hardware requirements per mode (default quantized paths; --bf16 relaxes them):
|
||||
fp8 sm89+ (H100, L40S, RTX 4090, Ada Lovelace, or newer)
|
||||
nvfp4_qat RTX 5090-class Blackwell (sm_120a): flashinfer FP4 gemm + attn_qat_infer
|
||||
nvfp4_sa2 RTX 5090-class Blackwell (sm_120a): flashinfer FP4 gemm + SageAttention2
|
||||
|
||||
Usage:
|
||||
python benchmark_qad_wan2_1_1_3b.py --mode nvfp4_qat --taehv-checkpoint /path/to/taehv/taew2_1.pth
|
||||
python benchmark_qad_wan2_1_1_3b.py --mode fp8 --warmups 5 --runs 20
|
||||
python benchmark_qad_wan2_1_1_3b.py --mode nvfp4_sa2 --bf16 # BF16 baseline
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import statistics
|
||||
import time
|
||||
|
||||
import torch
|
||||
|
||||
from _qad_common import flashinfer_arch_list, require_fp4_capable_gpu
|
||||
|
||||
OUTPUT_PATH = "video_samples"
|
||||
|
||||
# mode -> (default model, attention backend, needs the flashinfer FP4 gemm path)
|
||||
MODES = {
|
||||
"fp8": ("FastVideo/FastWan-QAD-FP8-1.3B", "SAGE_ATTN", False),
|
||||
"nvfp4_qat": ("FastVideo/FastWan-QAD-1.3B", "ATTN_QAT_INFER", True),
|
||||
"nvfp4_sa2": ("FastVideo/FastWan-QAD-1.3B-SA2", "SAGE_ATTN", True),
|
||||
}
|
||||
|
||||
PROMPT = (
|
||||
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
|
||||
"wide with interest. The playful yet serene atmosphere is complemented by soft "
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
|
||||
|
||||
class TaehvDecoder:
|
||||
def __init__(self, checkpoint_path: str, device: str = "cuda",
|
||||
dtype: torch.dtype = torch.float16) -> None:
|
||||
from taehv import TAEHV
|
||||
self.device = device
|
||||
self.dtype = dtype
|
||||
print(f"Loading TAEHV from {checkpoint_path} ...")
|
||||
self.model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype).eval()
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor):
|
||||
latents = latents.permute(0, 2, 1, 3, 4).to(self.device, self.dtype)
|
||||
decoded = self.model.decode_video(latents, parallel=True, show_progress_bar=False)
|
||||
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
|
||||
return frames.permute(0, 2, 3, 1).cpu().numpy()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def silence_request_log():
|
||||
"""Quiet ``VideoGenerator.generate``'s per-request multi-line INFO printout
|
||||
so the per-run timing lines stay readable."""
|
||||
vg_logger = logging.getLogger("fastvideo.entrypoints.video_generator")
|
||||
prev_level = vg_logger.level
|
||||
vg_logger.setLevel(logging.WARNING)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
vg_logger.setLevel(prev_level)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Multi-run QAD Wan2.1-1.3B benchmark")
|
||||
parser.add_argument("--mode", required=True, choices=sorted(MODES),
|
||||
help="Which QAD recipe to benchmark (mirrors the example script of the same name)")
|
||||
parser.add_argument("--warmups", type=int, default=5,
|
||||
help="Warmup runs before timing (default: 5)")
|
||||
parser.add_argument("--runs", type=int, default=20,
|
||||
help="Timed runs to collect min/max/mean/std over (default: 20)")
|
||||
parser.add_argument("--bf16", action="store_true", help="BF16 baseline (no quantization)")
|
||||
parser.add_argument("--granularity", choices=["tensor", "channel"], default="tensor",
|
||||
help="FP8 weight scale granularity (fp8 mode only)")
|
||||
parser.add_argument("--taehv-checkpoint", default=None, metavar="PATH",
|
||||
help="Path to taew2_1.pth; enables TAEHV tiny autoencoder decoding")
|
||||
parser.add_argument("--model", default=None,
|
||||
help="Model path or HuggingFace ID (default: per --mode)")
|
||||
parser.add_argument("--no-compile", action="store_true", help="Disable torch.compile for the DiT")
|
||||
parser.add_argument("--num_gpus", type=int, default=1)
|
||||
parser.add_argument("--infer_steps", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
|
||||
model, backend, needs_fp4 = MODES[args.mode]
|
||||
if args.model:
|
||||
model = args.model
|
||||
|
||||
if needs_fp4 and not args.bf16:
|
||||
require_fp4_capable_gpu()
|
||||
|
||||
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", backend)
|
||||
if needs_fp4:
|
||||
os.environ["FASTVIDEO_DISABLE_ATTENTION_COMPILE"] = "0"
|
||||
os.environ.setdefault("FLASHINFER_CUDA_ARCH_LIST", flashinfer_arch_list())
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
mode = "bf16" if args.bf16 else args.mode
|
||||
if not args.no_compile:
|
||||
mode += "_compile"
|
||||
use_taehv = args.taehv_checkpoint is not None
|
||||
print(f"Mode: {mode.upper()} model={model} " + ("decoder=TAEHV" if use_taehv else "decoder=VAE"))
|
||||
|
||||
taehv = TaehvDecoder(args.taehv_checkpoint) if use_taehv else None
|
||||
|
||||
pipeline_config = PipelineConfig.from_pretrained(model)
|
||||
pipeline_config.text_encoder_precisions = ("bf16",)
|
||||
if not args.bf16:
|
||||
if args.mode == "fp8":
|
||||
from fastvideo.layers.quantization import get_quantization_config
|
||||
pipeline_config.dit_config.quant_config = get_quantization_config("FP8")(
|
||||
granularity=args.granularity)
|
||||
else:
|
||||
from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
|
||||
pipeline_config.dit_config.quant_config = NVFP4QATConfig()
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
model,
|
||||
pipeline_config=pipeline_config,
|
||||
num_gpus=args.num_gpus,
|
||||
use_fsdp_inference=False,
|
||||
dit_cpu_offload=False,
|
||||
dit_layerwise_offload=False,
|
||||
vae_cpu_offload=use_taehv,
|
||||
text_encoder_cpu_offload=False,
|
||||
pin_cpu_memory=False,
|
||||
enable_torch_compile=not args.no_compile,
|
||||
enable_torch_compile_text_encoder=not args.no_compile,
|
||||
enable_torch_compile_vae=not args.no_compile and not use_taehv,
|
||||
output_type="latent" if use_taehv else "pil",
|
||||
)
|
||||
|
||||
request = {
|
||||
"prompt": PROMPT,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False},
|
||||
}
|
||||
|
||||
# Warmup: pay the torch.compile cost and warm TAEHV's cuDNN algo selection
|
||||
# so the timed runs below measure steady-state latency only.
|
||||
with silence_request_log():
|
||||
for _ in range(args.warmups):
|
||||
warm = generator.generate(request=request)
|
||||
if use_taehv:
|
||||
taehv.decode(warm.samples)
|
||||
|
||||
# Benchmark: time each run end-to-end. ``denoise`` is the generator's own
|
||||
# generation_time; for TAEHV we add the in-script decode. Nothing is written
|
||||
# to disk inside the loop so I/O never pollutes the timings.
|
||||
denoise_times: list[float] = []
|
||||
decode_times: list[float] = []
|
||||
totals: list[float] = []
|
||||
frames = None
|
||||
with silence_request_log():
|
||||
for i in range(args.runs):
|
||||
result = generator.generate(request=request)
|
||||
denoise_elapsed = result.generation_time
|
||||
denoise_times.append(denoise_elapsed)
|
||||
|
||||
if use_taehv:
|
||||
torch.cuda.synchronize()
|
||||
decode_start = time.perf_counter()
|
||||
frames = taehv.decode(result.samples)
|
||||
torch.cuda.synchronize()
|
||||
decode_elapsed = time.perf_counter() - decode_start
|
||||
decode_times.append(decode_elapsed)
|
||||
total = denoise_elapsed + decode_elapsed
|
||||
else:
|
||||
total = denoise_elapsed
|
||||
totals.append(total)
|
||||
|
||||
line = f" run {i + 1:02d}/{args.runs}: {total:.3f}s"
|
||||
if use_taehv:
|
||||
line += f" (denoise {denoise_elapsed:.3f}s + decode {decode_elapsed:.3f}s)"
|
||||
print(line)
|
||||
|
||||
if frames is not None:
|
||||
os.makedirs(OUTPUT_PATH, exist_ok=True)
|
||||
import imageio
|
||||
output_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
|
||||
imageio.mimsave(output_path, frames, fps=16, format="mp4")
|
||||
print(f"Saved video to {output_path}")
|
||||
|
||||
# Report min / max / mean / std over the timed runs.
|
||||
def _stat_row(name: str, xs: list[float]) -> str:
|
||||
std = statistics.stdev(xs) if len(xs) > 1 else 0.0
|
||||
return (f" {name:<11}{min(xs):>8.3f}{max(xs):>9.3f}"
|
||||
f"{statistics.mean(xs):>9.3f}{std:>9.3f}")
|
||||
|
||||
if totals:
|
||||
print(f"\n[{mode.upper()}] {args.runs} runs, {args.warmups} warmup, "
|
||||
f"{args.infer_steps} steps:")
|
||||
print(f" {'metric':<11}{'min':>8}{'max':>9}{'mean':>9}{'std':>9} (s)")
|
||||
print(_stat_row("total", totals))
|
||||
if use_taehv:
|
||||
print(_stat_row("denoise", denoise_times))
|
||||
print(_stat_row("decode", decode_times))
|
||||
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,25 +1,23 @@
|
||||
"""FP8 weight quantization inference example.
|
||||
|
||||
Runs Wan2.1-T2V-1.3B with FP8 e4m3 quantized DiT linear layers (attention
|
||||
projections and FFN). Weights are quantized in-place after loading; activations
|
||||
are quantized dynamically at runtime. Reduces GPU memory relative to BF16 and
|
||||
can improve throughput on sm89+ GPUs.
|
||||
Runs FastWan-QAD-FP8-1.3B (a distilled Wan2.1-T2V-1.3B-Diffusers checkpoint) with
|
||||
FP8 e4m3 quantized DiT linear layers. Uses the SAGE_ATTN attention backend.
|
||||
|
||||
Requirements:
|
||||
- GPU: sm89+ (H100, L40S, RTX 4090, Ada Lovelace, or newer)
|
||||
Falls back to a bf16 dequant path on older GPUs.
|
||||
- sageattention: pip install sageattention (the script defaults to the
|
||||
SAGE_ATTN attention backend)
|
||||
- TAEHV (optional): Follow install instructions at https://github.com/madebyollin/taehv
|
||||
|
||||
Usage:
|
||||
python fp8_wan2_1_1_3b.py # FP8 per-tensor (default)
|
||||
python fp8_wan2_1_1_3b.py --bf16 # BF16 baseline
|
||||
python fp8_wan2_1_1_3b.py --granularity channel # per-channel (higher accuracy but slower)
|
||||
python fp8_wan2_1_1_3b.py --taehv-checkpoint /path/to/taew2_1.pth
|
||||
python fp8_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth # FP8 per-tensor
|
||||
python fp8_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth --bf16 # BF16 baseline
|
||||
python fp8_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth --granularity channel # per-channel
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import torch
|
||||
@@ -27,28 +25,21 @@ import torch
|
||||
OUTPUT_PATH = "video_samples"
|
||||
|
||||
|
||||
def load_taehv(checkpoint_path, device="cuda", dtype=torch.float16):
|
||||
repo_dir = os.path.dirname(checkpoint_path)
|
||||
if repo_dir not in sys.path:
|
||||
sys.path.insert(0, repo_dir)
|
||||
from taehv import TAEHV
|
||||
print(f"Loading TAEHV from {checkpoint_path}...")
|
||||
model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype)
|
||||
print("TAEHV loaded.")
|
||||
return model
|
||||
class TaehvDecoder:
|
||||
def __init__(self, checkpoint_path: str, device: str = "cuda",
|
||||
dtype: torch.dtype = torch.float16) -> None:
|
||||
from taehv import TAEHV
|
||||
self.device = device
|
||||
self.dtype = dtype
|
||||
print(f"Loading TAEHV from {checkpoint_path} ...")
|
||||
self.model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype).eval()
|
||||
|
||||
|
||||
@torch.no_grad() # type: ignore[misc]
|
||||
def decode_with_taehv(taehv_model, latents):
|
||||
latents = latents.permute(0, 2, 1, 3, 4)
|
||||
latents = latents.to(device=next(taehv_model.parameters()).device,
|
||||
dtype=next(taehv_model.parameters()).dtype)
|
||||
decoded = taehv_model.decode_video(latents, parallel=False, show_progress_bar=False)
|
||||
frames = []
|
||||
for frame in decoded[0]:
|
||||
frame_np = (frame.clamp(0, 1) * 255).byte().cpu().permute(1, 2, 0).numpy()
|
||||
frames.append(frame_np)
|
||||
return frames
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor):
|
||||
latents = latents.permute(0, 2, 1, 3, 4).to(self.device, self.dtype)
|
||||
decoded = self.model.decode_video(latents, parallel=True, show_progress_bar=False)
|
||||
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
|
||||
return frames.permute(0, 2, 3, 1).cpu().numpy()
|
||||
|
||||
|
||||
def main():
|
||||
@@ -69,7 +60,7 @@ def main():
|
||||
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "SAGE_ATTN")
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.layers.quantization import get_quantization_config
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
mode = "bf16" if args.bf16 else f"fp8_{args.granularity}"
|
||||
if not args.no_compile:
|
||||
@@ -77,15 +68,17 @@ def main():
|
||||
use_taehv = args.taehv_checkpoint is not None
|
||||
print(f"Mode: {mode.upper()}" + (" decoder=TAEHV" if use_taehv else " decoder=VAE"))
|
||||
|
||||
taehv_model = load_taehv(args.taehv_checkpoint) if use_taehv else None
|
||||
taehv = TaehvDecoder(args.taehv_checkpoint) if use_taehv else None
|
||||
|
||||
pipeline_config = PipelineConfig.from_pretrained(args.model)
|
||||
pipeline_config.text_encoder_precisions = ("bf16",)
|
||||
if not args.bf16:
|
||||
from fastvideo.layers.quantization import get_quantization_config
|
||||
pipeline_config.dit_config.quant_config = get_quantization_config("FP8")(granularity=args.granularity)
|
||||
|
||||
# transformer_quant needs a QuantizationConfig *instance* — the bare string
|
||||
# is not resolved on the from_pretrained kwarg path.
|
||||
extra = {} if args.bf16 else {
|
||||
"transformer_quant": get_quantization_config("FP8")(granularity=args.granularity)
|
||||
}
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
args.model,
|
||||
pipeline_config=pipeline_config,
|
||||
num_gpus=args.num_gpus,
|
||||
use_fsdp_inference=False,
|
||||
dit_cpu_offload=False,
|
||||
@@ -94,9 +87,9 @@ def main():
|
||||
text_encoder_cpu_offload=False,
|
||||
pin_cpu_memory=False,
|
||||
enable_torch_compile=not args.no_compile,
|
||||
enable_torch_compile_text_encoder=not args.no_compile,
|
||||
enable_torch_compile_vae=not args.no_compile and not use_taehv,
|
||||
output_type="latent" if use_taehv else "pil",
|
||||
**extra,
|
||||
)
|
||||
|
||||
prompt = (
|
||||
@@ -105,33 +98,42 @@ def main():
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
|
||||
n_warmup = 1 if not args.no_compile else 0
|
||||
n_warmup = 2 if not args.no_compile else 0
|
||||
for _ in range(n_warmup):
|
||||
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 3, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False}})
|
||||
warmup_result = generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 3, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False}})
|
||||
if use_taehv:
|
||||
taehv.decode(warmup_result.samples)
|
||||
|
||||
os.makedirs(OUTPUT_PATH, exist_ok=True)
|
||||
start = time.time()
|
||||
video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
|
||||
if use_taehv:
|
||||
import imageio
|
||||
result = generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False},
|
||||
})
|
||||
import imageio
|
||||
frames = decode_with_taehv(taehv_model, result.samples)
|
||||
video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
|
||||
denoise_elapsed = result.generation_time
|
||||
torch.cuda.synchronize()
|
||||
t_decode = time.perf_counter()
|
||||
frames = taehv.decode(result.samples)
|
||||
torch.cuda.synchronize()
|
||||
decode_elapsed = time.perf_counter() - t_decode
|
||||
total = denoise_elapsed + decode_elapsed
|
||||
imageio.mimsave(video_path, frames, fps=16, format="mp4")
|
||||
print(f"Saved TAEHV-decoded video to: {video_path}")
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {total:.3f}s "
|
||||
f"(denoise {denoise_elapsed:.3f}s + decode {decode_elapsed:.3f}s)")
|
||||
else:
|
||||
generator.generate(request={
|
||||
result = generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")},
|
||||
"output": {"save_video": True, "output_path": video_path},
|
||||
})
|
||||
elapsed = time.time() - start
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
|
||||
f"({args.infer_steps / elapsed:.2f} it/s)")
|
||||
elapsed = result.generation_time
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.3f}s "
|
||||
f"({args.infer_steps / elapsed:.2f} it/s)")
|
||||
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
@@ -1,69 +1,100 @@
|
||||
"""NVFP4 + Attn-QAT (modified SageAttention3) inference on Blackwell.
|
||||
"""NVFP4 QAD inference example.
|
||||
|
||||
Runs Wan2.1-T2V-1.3B fully in 4-bit: NVFP4 linear layers (activations
|
||||
quantized on the fly) together with the modified SageAttention3 FP4 attention
|
||||
backend (``ATTN_QAT_INFER``). This is the inference half of the
|
||||
Quantization-Aware Distillation (QAD) recipe.
|
||||
Runs FastWan-QAD-1.3B (a distilled Wan2.1-T2V-1.3B-Diffusers checkpoint) with
|
||||
NVFP4QATConfig quantization. Uses ATTN_QAT_INFER attention backend.
|
||||
|
||||
Requirements:
|
||||
- RTX 5090 / consumer Blackwell (sm_120a). The attn_qat_infer kernel hard
|
||||
gates on sm_120; on other GPUs it falls back to Flash Attention.
|
||||
- The attn_qat_infer kernel built into fastvideo-kernel (see #1455) and
|
||||
flashinfer for the NVFP4 linear matmuls.
|
||||
- GPU (default NVFP4 mode): RTX 5090-class Blackwell (sm_120a). The FP4
|
||||
linear path uses flashinfer's sm_120a cubins, which do not run on
|
||||
sm_100 (B200/B300), and the attn_qat_infer kernel hard-gates on sm_120.
|
||||
- GPU (--bf16 baseline): any supported GPU (the ATTN_QAT_INFER backend
|
||||
falls back to Flash Attention off sm_120).
|
||||
- TAEHV (optional): Follow install instructions at https://github.com/madebyollin/taehv
|
||||
|
||||
Usage:
|
||||
python nvfp4_qat_wan2_1_1_3b.py # NVFP4 linear + Attn-QAT attn
|
||||
python nvfp4_qat_wan2_1_1_3b.py --bf16 # BF16 baseline
|
||||
python nvfp4_qat_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth # NVFP4 QAD
|
||||
python nvfp4_qat_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth --bf16 # BF16 baseline
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
|
||||
import torch
|
||||
|
||||
OUTPUT_PATH = "video_samples"
|
||||
|
||||
|
||||
class TaehvDecoder:
|
||||
def __init__(self, checkpoint_path: str, device: str = "cuda",
|
||||
dtype: torch.dtype = torch.float16) -> None:
|
||||
from taehv import TAEHV
|
||||
self.device = device
|
||||
self.dtype = dtype
|
||||
print(f"Loading TAEHV from {checkpoint_path} ...")
|
||||
self.model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype).eval()
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor):
|
||||
latents = latents.permute(0, 2, 1, 3, 4).to(self.device, self.dtype)
|
||||
decoded = self.model.decode_video(latents, parallel=True, show_progress_bar=False)
|
||||
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
|
||||
return frames.permute(0, 2, 3, 1).cpu().numpy()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="NVFP4 + Attn-QAT video generation")
|
||||
parser = argparse.ArgumentParser(description="NVFP4 QAD video generation benchmark")
|
||||
parser.add_argument("--bf16", action="store_true",
|
||||
help="BF16 baseline (no NVFP4 linear, default attention)")
|
||||
parser.add_argument("--model", default="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
help="BF16 baseline (no NVFP4 quantization)")
|
||||
parser.add_argument("--taehv-checkpoint", default=None, metavar="PATH",
|
||||
help="Path to taew2_1.pth; enables TAEHV tiny autoencoder decoding")
|
||||
parser.add_argument("--model", default="FastVideo/FastWan-QAD-1.3B",
|
||||
help="Model path or HuggingFace ID")
|
||||
parser.add_argument("--quant-method", default="nvfp4_qat", choices=["nvfp4_qat", "NVFP4"],
|
||||
help="Linear quantization config. Wan-2.1 uses nvfp4_qat (matches its "
|
||||
"to_q/k/v/out + ffn layers); NVFP4 is LTX2-specific and will NOT "
|
||||
"quantize Wan.")
|
||||
parser.add_argument("--compile", action="store_true", help="Enable torch.compile for the DiT")
|
||||
parser.add_argument("--no-compile", action="store_true", help="Disable torch.compile for the DiT")
|
||||
parser.add_argument("--num_gpus", type=int, default=1)
|
||||
parser.add_argument("--infer_steps", type=int, default=50)
|
||||
parser.add_argument("--infer_steps", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
|
||||
# The attention backend is selected via env var before the engine starts.
|
||||
# ATTN_QAT_INFER -> AttnQatInferBackend (modified SageAttention3 FP4).
|
||||
from _qad_common import flashinfer_arch_list, require_fp4_capable_gpu
|
||||
|
||||
if not args.bf16:
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "ATTN_QAT_INFER"
|
||||
require_fp4_capable_gpu()
|
||||
|
||||
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "ATTN_QAT_INFER")
|
||||
os.environ["FASTVIDEO_DISABLE_ATTENTION_COMPILE"] = "0"
|
||||
os.environ.setdefault("FLASHINFER_CUDA_ARCH_LIST", flashinfer_arch_list())
|
||||
|
||||
# Import after the env var so the platform picks up the selection.
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.layers.quantization import get_quantization_config
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
mode = "bf16" if args.bf16 else args.quant_method
|
||||
if args.compile:
|
||||
mode = "bf16" if args.bf16 else "nvfp4_qad"
|
||||
if not args.no_compile:
|
||||
mode += "_compile"
|
||||
print(f"Mode: {mode.upper()}")
|
||||
use_taehv = args.taehv_checkpoint is not None
|
||||
print(f"Mode: {mode.upper()}" + (" decoder=TAEHV" if use_taehv else " decoder=VAE"))
|
||||
|
||||
taehv = TaehvDecoder(args.taehv_checkpoint) if use_taehv else None
|
||||
|
||||
pipeline_config = PipelineConfig.from_pretrained(args.model)
|
||||
pipeline_config.text_encoder_precisions = ("bf16",)
|
||||
if not args.bf16:
|
||||
from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
|
||||
pipeline_config.dit_config.quant_config = NVFP4QATConfig()
|
||||
|
||||
# transformer_quant needs a QuantizationConfig *instance* — the bare string
|
||||
# is not resolved on the from_pretrained kwarg path.
|
||||
extra = {} if args.bf16 else {"transformer_quant": get_quantization_config(args.quant_method)()}
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
args.model,
|
||||
pipeline_config=pipeline_config,
|
||||
num_gpus=args.num_gpus,
|
||||
use_fsdp_inference=args.bf16,
|
||||
use_fsdp_inference=False,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
enable_torch_compile=args.compile,
|
||||
**extra,
|
||||
dit_layerwise_offload=False,
|
||||
vae_cpu_offload=use_taehv,
|
||||
text_encoder_cpu_offload=False,
|
||||
pin_cpu_memory=False,
|
||||
enable_torch_compile=not args.no_compile,
|
||||
enable_torch_compile_text_encoder=not args.no_compile,
|
||||
enable_torch_compile_vae=not args.no_compile and not use_taehv,
|
||||
output_type="latent" if use_taehv else "pil",
|
||||
)
|
||||
|
||||
prompt = (
|
||||
@@ -72,21 +103,42 @@ def main():
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
|
||||
n_warmup = 2 if args.compile else 1
|
||||
n_warmup = 2 if not args.no_compile else 0
|
||||
for _ in range(n_warmup):
|
||||
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 2},
|
||||
"output": {"save_video": False}})
|
||||
warmup_result = generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 3, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False}})
|
||||
if use_taehv:
|
||||
taehv.decode(warmup_result.samples)
|
||||
|
||||
os.makedirs(OUTPUT_PATH, exist_ok=True)
|
||||
start = time.time()
|
||||
generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps},
|
||||
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")},
|
||||
})
|
||||
elapsed = time.time() - start
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
|
||||
f"({args.infer_steps / elapsed:.2f} it/s)")
|
||||
video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
|
||||
if use_taehv:
|
||||
import imageio
|
||||
result = generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False},
|
||||
})
|
||||
denoise_elapsed = result.generation_time
|
||||
torch.cuda.synchronize()
|
||||
t_decode = time.perf_counter()
|
||||
frames = taehv.decode(result.samples)
|
||||
torch.cuda.synchronize()
|
||||
decode_elapsed = time.perf_counter() - t_decode
|
||||
total = denoise_elapsed + decode_elapsed
|
||||
imageio.mimsave(video_path, frames, fps=16, format="mp4")
|
||||
print(f"Saved TAEHV-decoded video to: {video_path}")
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {total:.3f}s "
|
||||
f"(denoise {denoise_elapsed:.3f}s + decode {decode_elapsed:.3f}s)")
|
||||
else:
|
||||
result = generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": True, "output_path": video_path},
|
||||
})
|
||||
elapsed = result.generation_time
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.3f}s "
|
||||
f"({args.infer_steps / elapsed:.2f} it/s)")
|
||||
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""NVFP4 QAD inference example with SageAttention 2 backend.
|
||||
|
||||
Runs FastWan-QAD-1.3B-SA2 (a distilled Wan2.1-T2V-1.3B-Diffusers checkpoint) with
|
||||
NVFP4QATConfig quantization. Uses the SAGE_ATTN attention backend.
|
||||
|
||||
Requirements:
|
||||
- GPU (default NVFP4 mode): RTX 5090-class Blackwell (sm_120a). The NVFP4
|
||||
linear path uses flashinfer's cutlass FP4 gemm, which has no fallback.
|
||||
- GPU (--bf16 baseline): sm89+ (H100, L40S, RTX 4090, Ada Lovelace, or newer)
|
||||
- sageattention: pip install sageattention
|
||||
- TAEHV (optional): Follow install instructions at https://github.com/madebyollin/taehv
|
||||
|
||||
Usage:
|
||||
python nvfp4_sa2_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth # NVFP4 + SageAttn2
|
||||
python nvfp4_sa2_wan2_1_1_3b.py --taehv-checkpoint /path/to/taehv/taew2_1.pth --bf16 # BF16 baseline
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
|
||||
import torch
|
||||
|
||||
OUTPUT_PATH = "video_samples"
|
||||
|
||||
|
||||
class TaehvDecoder:
|
||||
def __init__(self, checkpoint_path: str, device: str = "cuda",
|
||||
dtype: torch.dtype = torch.float16) -> None:
|
||||
from taehv import TAEHV
|
||||
self.device = device
|
||||
self.dtype = dtype
|
||||
print(f"Loading TAEHV from {checkpoint_path} ...")
|
||||
self.model = TAEHV(checkpoint_path=checkpoint_path).to(device, dtype).eval()
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor):
|
||||
latents = latents.permute(0, 2, 1, 3, 4).to(self.device, self.dtype)
|
||||
decoded = self.model.decode_video(latents, parallel=True, show_progress_bar=False)
|
||||
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
|
||||
return frames.permute(0, 2, 3, 1).cpu().numpy()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="NVFP4 QAD + SageAttention2 video generation benchmark")
|
||||
parser.add_argument("--bf16", action="store_true",
|
||||
help="BF16 baseline (no NVFP4 quantization)")
|
||||
parser.add_argument("--taehv-checkpoint", default=None, metavar="PATH",
|
||||
help="Path to taew2_1.pth; enables TAEHV tiny autoencoder decoding")
|
||||
parser.add_argument("--model", default="FastVideo/FastWan-QAD-1.3B-SA2",
|
||||
help="Model path or HuggingFace ID")
|
||||
parser.add_argument("--no-compile", action="store_true", help="Disable torch.compile for the DiT")
|
||||
parser.add_argument("--num_gpus", type=int, default=1)
|
||||
parser.add_argument("--infer_steps", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
|
||||
from _qad_common import flashinfer_arch_list, require_fp4_capable_gpu
|
||||
|
||||
if not args.bf16:
|
||||
require_fp4_capable_gpu()
|
||||
|
||||
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "SAGE_ATTN")
|
||||
os.environ["FASTVIDEO_DISABLE_ATTENTION_COMPILE"] = "0"
|
||||
os.environ.setdefault("FLASHINFER_CUDA_ARCH_LIST", flashinfer_arch_list())
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
mode = "bf16" if args.bf16 else "nvfp4_sa2"
|
||||
if not args.no_compile:
|
||||
mode += "_compile"
|
||||
use_taehv = args.taehv_checkpoint is not None
|
||||
print(f"Mode: {mode.upper()}" + (" decoder=TAEHV" if use_taehv else " decoder=VAE"))
|
||||
|
||||
taehv = TaehvDecoder(args.taehv_checkpoint) if use_taehv else None
|
||||
|
||||
pipeline_config = PipelineConfig.from_pretrained(args.model)
|
||||
pipeline_config.text_encoder_precisions = ("bf16",)
|
||||
if not args.bf16:
|
||||
from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
|
||||
pipeline_config.dit_config.quant_config = NVFP4QATConfig()
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
args.model,
|
||||
pipeline_config=pipeline_config,
|
||||
num_gpus=args.num_gpus,
|
||||
use_fsdp_inference=False,
|
||||
dit_cpu_offload=False,
|
||||
dit_layerwise_offload=False,
|
||||
vae_cpu_offload=use_taehv,
|
||||
text_encoder_cpu_offload=False,
|
||||
pin_cpu_memory=False,
|
||||
enable_torch_compile=not args.no_compile,
|
||||
enable_torch_compile_text_encoder=not args.no_compile,
|
||||
enable_torch_compile_vae=not args.no_compile and not use_taehv,
|
||||
output_type="latent" if use_taehv else "pil",
|
||||
)
|
||||
|
||||
prompt = (
|
||||
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
|
||||
"wide with interest. The playful yet serene atmosphere is complemented by soft "
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
|
||||
n_warmup = 2 if not args.no_compile else 0
|
||||
for _ in range(n_warmup):
|
||||
warmup_result = generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 3, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False}})
|
||||
if use_taehv:
|
||||
taehv.decode(warmup_result.samples)
|
||||
|
||||
os.makedirs(OUTPUT_PATH, exist_ok=True)
|
||||
video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
|
||||
if use_taehv:
|
||||
import imageio
|
||||
result = generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": False},
|
||||
})
|
||||
denoise_elapsed = result.generation_time
|
||||
torch.cuda.synchronize()
|
||||
t_decode = time.perf_counter()
|
||||
frames = taehv.decode(result.samples)
|
||||
torch.cuda.synchronize()
|
||||
decode_elapsed = time.perf_counter() - t_decode
|
||||
total = denoise_elapsed + decode_elapsed
|
||||
imageio.mimsave(video_path, frames, fps=16, format="mp4")
|
||||
print(f"Saved TAEHV-decoded video to: {video_path}")
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {total:.3f}s "
|
||||
f"(denoise {denoise_elapsed:.3f}s + decode {decode_elapsed:.3f}s)")
|
||||
else:
|
||||
result = generator.generate(request={
|
||||
"prompt": prompt,
|
||||
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
|
||||
"output": {"save_video": True, "output_path": video_path},
|
||||
})
|
||||
elapsed = result.generation_time
|
||||
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.3f}s "
|
||||
f"({args.infer_steps / elapsed:.2f} it/s)")
|
||||
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user