DreamVAE / scripts /export_trt.py
ryanontheinside's picture
DreamVAE initial release
53e74f7
Raw
History Blame Contribute Delete
11.1 kB
#!/usr/bin/env python3
"""Export FastOobleckDecoder checkpoint to ONNX and build a TRT engine.
The student decoder has the exact same I/O contract as the teacher
(latents [B, 64, T] -> audio [B, 2, samples]), so we reuse the existing
VAE TRT build pipeline. The resulting engine is a drop-in replacement.
Usage:
uv run python research_program/vae_distillation/export_trt.py \
--ckpt research_program/vae_distillation/checkpoints/student_step620000.pt
# FP32 engine (if FP16 has Snake1d issues):
uv run python research_program/vae_distillation/export_trt.py \
--ckpt research_program/vae_distillation/checkpoints/student_step620000.pt \
--no-fp16
# Custom output directory:
uv run python research_program/vae_distillation/export_trt.py \
--ckpt ... --output-dir trt_engines
"""
import argparse
import logging
import math
import sys
from pathlib import Path
import torch
import torch.nn as nn
from torch.nn.utils import weight_norm
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# =========================================================================
# Student model definition (must match training script exactly)
# =========================================================================
class Snake1d(nn.Module):
def __init__(self, hidden_dim, logscale=True):
super().__init__()
self.alpha = nn.Parameter(torch.zeros(1, hidden_dim, 1))
self.beta = nn.Parameter(torch.zeros(1, hidden_dim, 1))
self.alpha.requires_grad = True
self.beta.requires_grad = True
self.logscale = logscale
def forward(self, hidden_states):
shape = hidden_states.shape
alpha = self.alpha if not self.logscale else torch.exp(self.alpha)
beta = self.beta if not self.logscale else torch.exp(self.beta)
hidden_states = hidden_states.reshape(shape[0], shape[1], -1)
hidden_states = hidden_states + (beta + 1e-9).reciprocal() * torch.sin(alpha * hidden_states).pow(2)
hidden_states = hidden_states.reshape(shape)
return hidden_states
class FastResidualUnit(nn.Module):
def __init__(self, dim: int, dilation: int = 1):
super().__init__()
pad = ((7 - 1) * dilation) // 2
self.snake1 = Snake1d(dim)
self.conv1 = weight_norm(nn.Conv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad))
self.snake2 = Snake1d(dim)
self.conv2 = weight_norm(nn.Conv1d(dim, dim, kernel_size=1))
def forward(self, x):
h = self.conv1(self.snake1(x))
h = self.conv2(self.snake2(h))
pad = (x.shape[-1] - h.shape[-1]) // 2
if pad > 0:
x = x[..., pad:-pad]
return x + h
class FastDecoderBlock(nn.Module):
def __init__(self, in_dim: int, out_dim: int, stride: int = 1):
super().__init__()
self.snake1 = Snake1d(in_dim)
self.conv_t = weight_norm(nn.ConvTranspose1d(
in_dim, out_dim, kernel_size=2 * stride, stride=stride,
padding=math.ceil(stride / 2),
))
self.res1 = FastResidualUnit(out_dim, dilation=1)
self.res2 = FastResidualUnit(out_dim, dilation=3)
def forward(self, x):
x = self.snake1(x)
x = self.conv_t(x)
x = self.res1(x)
x = self.res2(x)
return x
class FastOobleckDecoder(nn.Module):
def __init__(self, channels=128, input_channels=64, audio_channels=2,
upsampling_ratios=None, channel_multiples=None):
super().__init__()
if upsampling_ratios is None:
upsampling_ratios = [10, 6, 4, 4, 2]
if channel_multiples is None:
channel_multiples = [1, 2, 4, 8, 8]
cm = [1] + channel_multiples
self.conv1 = weight_norm(nn.Conv1d(input_channels, channels * cm[-1], kernel_size=7, padding=3))
blocks = []
for i, stride in enumerate(upsampling_ratios):
in_dim = channels * cm[len(upsampling_ratios) - i]
out_dim = channels * cm[len(upsampling_ratios) - i - 1]
blocks.append(FastDecoderBlock(in_dim, out_dim, stride=stride))
self.blocks = nn.ModuleList(blocks)
self.final_snake = Snake1d(channels)
self.conv2 = weight_norm(nn.Conv1d(channels, audio_channels, kernel_size=7, padding=3, bias=False))
def forward(self, latents: torch.Tensor) -> torch.Tensor:
x = self.conv1(latents)
for block in self.blocks:
x = block(x)
x = self.final_snake(x)
x = self.conv2(x)
return x
# =========================================================================
# Export
# =========================================================================
def export_onnx(student: nn.Module, onnx_path: Path, device: str = "cuda"):
"""Export student decoder to ONNX with dynamic latent length."""
onnx_path.parent.mkdir(parents=True, exist_ok=True)
# Trace with 30s worth of latent frames (750 = 30s * 25 fps)
example = torch.randn(1, 64, 750, device=device, dtype=torch.float32)
logger.info("Tracing student decoder for ONNX export...")
with torch.no_grad():
torch.onnx.export(
student,
(example,),
str(onnx_path),
input_names=["latents"],
output_names=["audio"],
dynamic_axes={
"latents": {0: "batch", 2: "latent_frames"},
"audio": {0: "batch", 2: "samples"},
},
opset_version=18,
do_constant_folding=True,
dynamo=False,
)
logger.info("ONNX saved to %s (%.1f MB)", onnx_path, onnx_path.stat().st_size / (1 << 20))
return onnx_path
def build_engine(onnx_path: Path, engine_path: Path, fp16: bool = True):
"""Build TRT engine using the same config as the teacher VAE decoder."""
# Import the existing build infrastructure
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from acestep.engine.trt.vae_export import VAETRTBuildConfig, build_vae_decode_engine
config = VAETRTBuildConfig(fp16=fp16)
return build_vae_decode_engine(str(onnx_path), str(engine_path), config=config)
def verify_engine(engine_path: Path, device: str = "cuda"):
"""Quick sanity check: run a dummy input through the engine."""
from polygraphy.backend.common import bytes_from_path
from polygraphy.backend.trt import engine_from_bytes
from polygraphy import cuda as pg_cuda
engine = engine_from_bytes(bytes_from_path(str(engine_path)))
ctx = engine.create_execution_context()
stream = pg_cuda.Stream()
# 10s of audio = 250 latent frames
T = 250
latents = torch.randn(1, 64, T, device=device, dtype=torch.float32).contiguous()
ctx.set_input_shape("latents", (1, 64, T))
ctx.set_tensor_address("latents", latents.data_ptr())
out_shape = tuple(ctx.get_tensor_shape("audio"))
audio_buf = torch.empty(out_shape, dtype=torch.float32, device=device)
ctx.set_tensor_address("audio", audio_buf.data_ptr())
ok = ctx.execute_async_v3(stream.ptr)
stream.synchronize()
expected_samples = T * 1920 # hop length
assert ok, "TRT execution failed"
assert out_shape == (1, 2, expected_samples), f"Shape mismatch: {out_shape} vs (1, 2, {expected_samples})"
assert not torch.isnan(audio_buf).any(), "NaN in output"
assert audio_buf.abs().max() > 1e-6, "Output is all zeros"
logger.info("Engine verification passed: input [1, 64, %d] -> output %s, range [%.4f, %.4f]",
T, list(out_shape), audio_buf.min().item(), audio_buf.max().item())
# Quick speed check
import time
torch.cuda.synchronize()
times = []
for _ in range(20):
torch.cuda.synchronize()
t0 = time.time()
ctx.execute_async_v3(stream.ptr)
stream.synchronize()
times.append(time.time() - t0)
avg_ms = sum(times) / len(times) * 1000
logger.info("TRT speed (10s audio, 20 trials): %.1f ms avg", avg_ms)
def main():
parser = argparse.ArgumentParser(description="Export FastOobleckDecoder to ONNX + TRT")
parser.add_argument("--ckpt", type=str, required=True, help="Path to student checkpoint .pt")
parser.add_argument("--output-dir", type=str, default=None,
help="Output directory (default: trt_engines/ in project root)")
parser.add_argument("--no-fp16", action="store_true", help="Build FP32 engine instead of FP16")
parser.add_argument("--skip-engine", action="store_true", help="Only export ONNX, skip TRT build")
parser.add_argument("--skip-verify", action="store_true", help="Skip engine verification")
parser.add_argument("--device", type=str, default="cuda")
args = parser.parse_args()
ckpt_path = Path(args.ckpt)
step = "unknown"
# Determine output paths
project_root = Path(__file__).resolve().parents[2]
if args.output_dir:
out_dir = Path(args.output_dir)
else:
out_dir = project_root / "trt_engines"
# Load student
logger.info("Loading checkpoint: %s", ckpt_path)
ckpt = torch.load(ckpt_path, map_location=args.device, weights_only=False)
config = ckpt.get("config", {})
student = FastOobleckDecoder(
channels=config.get("channels", 128),
input_channels=config.get("input_channels", 64),
audio_channels=config.get("audio_channels", 2),
upsampling_ratios=config.get("upsampling_ratios", [10, 6, 4, 4, 2]),
channel_multiples=config.get("channel_multiples", [1, 2, 4, 8, 8]),
).to(args.device).eval()
student.load_state_dict(ckpt["student_state_dict"])
step = ckpt.get("step", "unknown")
params_m = sum(p.numel() for p in student.parameters()) / 1e6
logger.info("Student loaded: step %s, %.1fM params", step, params_m)
# ONNX export
onnx_dir = out_dir / "_onnx" / "dreamvae_decode"
onnx_path = onnx_dir / "dreamvae_decode.onnx"
export_onnx(student, onnx_path, device=args.device)
if args.skip_engine:
logger.info("Skipping TRT engine build (--skip-engine)")
return
# TRT engine build
fp16 = not args.no_fp16
prec = "fp16" if fp16 else "fp32"
engine_name = f"dreamvae_decode_{prec}_240s"
engine_dir = out_dir / engine_name
engine_path = engine_dir / f"{engine_name}.engine"
logger.info("Building TRT engine (%s)...", prec)
build_engine(onnx_path, engine_path, fp16=fp16)
if not args.skip_verify:
logger.info("Verifying engine...")
verify_engine(engine_path, device=args.device)
logger.info("Done. Engine: %s", engine_path)
logger.info("To use as drop-in replacement, pass this engine path where vae_decode engine is expected.")
if __name__ == "__main__":
main()