Signed-off-by: knlnguyen1802 <knlnguyen1802@gmail.com> Co-authored-by: Samit <285365963@qq.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2301 lines
94 KiB
Python
2301 lines
94 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright (c) 2026 SandAI. All Rights Reserved.
|
|
# Ported from daVinci-MagiHuman inference/pipeline/video_generate.py
|
|
# Adapted for vllm-omni: single-GPU, diffusers VAE, configurable dit_subfolder.
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import math
|
|
import os
|
|
from collections.abc import Iterable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Literal
|
|
|
|
import numpy as np
|
|
import torch
|
|
import torch.nn as nn
|
|
import whisper
|
|
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
|
from diffusers.schedulers.scheduling_utils import (
|
|
KarrasDiffusionSchedulers,
|
|
SchedulerMixin,
|
|
SchedulerOutput,
|
|
)
|
|
from diffusers.utils import deprecate, load_image
|
|
from diffusers.utils.torch_utils import randn_tensor
|
|
from diffusers.video_processor import VideoProcessor
|
|
from einops import rearrange
|
|
from PIL import Image
|
|
from safetensors.torch import load_file
|
|
from torch.nn import functional as F
|
|
from torch.nn.utils import weight_norm
|
|
from transformers import AutoTokenizer
|
|
from transformers.models.t5gemma import T5GemmaEncoderModel
|
|
from vllm.distributed import (
|
|
get_tensor_model_parallel_world_size,
|
|
)
|
|
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
|
|
|
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
|
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import (
|
|
DistributedAutoencoderKLWan,
|
|
)
|
|
from vllm_omni.diffusion.model_loader.diffusers_loader import (
|
|
DiffusersPipelineLoader,
|
|
)
|
|
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
|
|
from vllm_omni.diffusion.models.t5_encoder.t5_gemma_encoder import T5GemmaEncoderModelTP
|
|
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import (
|
|
DiffusionPipelineProfilerMixin,
|
|
)
|
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
|
|
|
from .magi_human_dit import (
|
|
DiTModel,
|
|
FFAHandler,
|
|
MagiHumanDiTConfig,
|
|
Modality,
|
|
VarlenHandler,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ===========================================================================
|
|
# Scheduler (ported from daVinci-MagiHuman inference/pipeline/scheduler_unipc.py)
|
|
# ===========================================================================
|
|
class FlowUniPCMultistepScheduler(SchedulerMixin, ConfigMixin):
|
|
_compatibles = [e.name for e in KarrasDiffusionSchedulers]
|
|
order = 1
|
|
|
|
@register_to_config
|
|
def __init__(
|
|
self,
|
|
num_train_timesteps: int = 1000,
|
|
solver_order: int = 2,
|
|
prediction_type: str = "flow_prediction",
|
|
shift: float = 1.0,
|
|
use_dynamic_shifting=False,
|
|
thresholding: bool = False,
|
|
dynamic_thresholding_ratio: float = 0.995,
|
|
sample_max_value: float = 1.0,
|
|
predict_x0: bool = True,
|
|
solver_type: str = "bh2",
|
|
lower_order_final: bool = True,
|
|
disable_corrector: list[int] = [],
|
|
solver_p: SchedulerMixin = None,
|
|
timestep_spacing: str = "linspace",
|
|
steps_offset: int = 0,
|
|
final_sigmas_type: str | None = "zero",
|
|
):
|
|
if solver_type not in ["bh1", "bh2"]:
|
|
if solver_type in ["midpoint", "heun", "logrho"]:
|
|
self.register_to_config(solver_type="bh2")
|
|
else:
|
|
raise NotImplementedError(f"{solver_type} is not implemented for {self.__class__}")
|
|
|
|
self.predict_x0 = predict_x0
|
|
self.num_inference_steps = None
|
|
alphas = np.linspace(1, 1 / num_train_timesteps, num_train_timesteps)[::-1].copy()
|
|
sigmas = 1.0 - alphas
|
|
sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32)
|
|
|
|
if not use_dynamic_shifting:
|
|
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
|
|
|
|
self.sigmas = sigmas
|
|
self.timesteps = sigmas * num_train_timesteps
|
|
|
|
self.model_outputs = [None] * solver_order
|
|
self.timestep_list = [None] * solver_order
|
|
self.lower_order_nums = 0
|
|
self.disable_corrector = disable_corrector
|
|
self.solver_p = solver_p
|
|
self.last_sample = None
|
|
self._step_index: int | None = None
|
|
self._begin_index: int | None = None
|
|
|
|
self.sigmas = self.sigmas.to("cpu")
|
|
self.sigma_min = self.sigmas[-1].item()
|
|
self.sigma_max = self.sigmas[0].item()
|
|
|
|
@property
|
|
def step_index(self):
|
|
return self._step_index
|
|
|
|
@property
|
|
def begin_index(self):
|
|
return self._begin_index
|
|
|
|
def set_begin_index(self, begin_index: int = 0):
|
|
self._begin_index = begin_index
|
|
|
|
def set_timesteps(
|
|
self,
|
|
num_inference_steps: int | None = None,
|
|
device: str | torch.device = None,
|
|
sigmas: list[float] | None = None,
|
|
mu: float | None | None = None,
|
|
shift: float | None | None = None,
|
|
):
|
|
if self.config.use_dynamic_shifting and mu is None:
|
|
raise ValueError(" you have to pass a value for `mu` when `use_dynamic_shifting` is set to be `True`")
|
|
|
|
if sigmas is None:
|
|
sigmas = np.linspace(self.sigma_max, self.sigma_min, num_inference_steps + 1).copy()[:-1]
|
|
|
|
if self.config.use_dynamic_shifting:
|
|
sigmas = self.time_shift(mu, 1.0, sigmas)
|
|
else:
|
|
if shift is None:
|
|
shift = self.config.shift
|
|
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
|
|
|
|
if self.config.final_sigmas_type == "sigma_min":
|
|
sigma_last = ((1 - self.alphas_cumprod[0]) / self.alphas_cumprod[0]) ** 0.5
|
|
elif self.config.final_sigmas_type == "zero":
|
|
sigma_last = 0
|
|
else:
|
|
raise ValueError(
|
|
f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}"
|
|
)
|
|
|
|
timesteps = sigmas * self.config.num_train_timesteps
|
|
sigmas = np.concatenate([sigmas, [sigma_last]]).astype(np.float32)
|
|
|
|
self.sigmas = torch.from_numpy(sigmas)
|
|
self.timesteps = torch.from_numpy(timesteps).to(device=device, dtype=torch.int64)
|
|
|
|
self.num_inference_steps = len(timesteps)
|
|
|
|
self.model_outputs = [None] * self.config.solver_order
|
|
self.lower_order_nums = 0
|
|
self.last_sample = None
|
|
if self.solver_p:
|
|
self.solver_p.set_timesteps(self.num_inference_steps, device=device)
|
|
|
|
self._step_index = None
|
|
self._begin_index = None
|
|
self.sigmas = self.sigmas.to("cpu")
|
|
|
|
def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor:
|
|
dtype = sample.dtype
|
|
batch_size, channels, *remaining_dims = sample.shape
|
|
|
|
if dtype not in (torch.float32, torch.float64):
|
|
sample = sample.float()
|
|
|
|
sample = sample.reshape(batch_size, channels * np.prod(remaining_dims))
|
|
abs_sample = sample.abs()
|
|
s = torch.quantile(abs_sample, self.config.dynamic_thresholding_ratio, dim=1)
|
|
s = torch.clamp(s, min=1, max=self.config.sample_max_value)
|
|
s = s.unsqueeze(1)
|
|
sample = torch.clamp(sample, -s, s) / s
|
|
sample = sample.reshape(batch_size, channels, *remaining_dims)
|
|
return sample.to(dtype)
|
|
|
|
def _sigma_to_t(self, sigma):
|
|
return sigma * self.config.num_train_timesteps
|
|
|
|
def _sigma_to_alpha_sigma_t(self, sigma):
|
|
return 1 - sigma, sigma
|
|
|
|
def time_shift(self, mu: float, sigma: float, t: torch.Tensor):
|
|
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
|
|
|
|
def convert_model_output(
|
|
self, model_output: torch.Tensor, *args, sample: torch.Tensor = None, **kwargs
|
|
) -> torch.Tensor:
|
|
timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None)
|
|
if sample is None:
|
|
if len(args) > 1:
|
|
sample = args[1]
|
|
else:
|
|
raise ValueError("missing `sample` as a required keyword argument")
|
|
if timestep is not None:
|
|
deprecate(
|
|
"timesteps",
|
|
"1.0.0",
|
|
"Passing `timesteps` is deprecated and has no effect as model output "
|
|
"conversion is now handled via an internal counter `self.step_index`",
|
|
)
|
|
|
|
sigma = self.sigmas[self.step_index]
|
|
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
|
|
|
if self.predict_x0:
|
|
if self.config.prediction_type == "flow_prediction":
|
|
sigma_t = self.sigmas[self.step_index]
|
|
x0_pred = sample - sigma_t * model_output
|
|
else:
|
|
raise ValueError(
|
|
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
|
|
" `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
|
|
)
|
|
if self.config.thresholding:
|
|
x0_pred = self._threshold_sample(x0_pred)
|
|
return x0_pred
|
|
else:
|
|
if self.config.prediction_type == "flow_prediction":
|
|
sigma_t = self.sigmas[self.step_index]
|
|
epsilon = sample - (1 - sigma_t) * model_output
|
|
else:
|
|
raise ValueError(
|
|
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
|
|
" `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
|
|
)
|
|
if self.config.thresholding:
|
|
sigma_t = self.sigmas[self.step_index]
|
|
x0_pred = sample - sigma_t * model_output
|
|
x0_pred = self._threshold_sample(x0_pred)
|
|
epsilon = model_output + x0_pred
|
|
return epsilon
|
|
|
|
def multistep_uni_p_bh_update(
|
|
self,
|
|
model_output: torch.Tensor,
|
|
*args,
|
|
sample: torch.Tensor | None = None,
|
|
order: int | None = None,
|
|
**kwargs,
|
|
) -> torch.Tensor:
|
|
prev_timestep = args[0] if len(args) > 0 else kwargs.pop("prev_timestep", None)
|
|
if sample is None:
|
|
if len(args) > 1:
|
|
sample = args[1]
|
|
else:
|
|
raise ValueError(" missing `sample` as a required keyword argument")
|
|
if order is None:
|
|
if len(args) > 2:
|
|
order = args[2]
|
|
else:
|
|
raise ValueError(" missing `order` as a required keyword argument")
|
|
if prev_timestep is not None:
|
|
deprecate("prev_timestep", "1.0.0", "Passing `prev_timestep` is deprecated and has no effect.")
|
|
|
|
model_output_list = self.model_outputs
|
|
s0 = self.timestep_list[-1]
|
|
m0 = model_output_list[-1]
|
|
x = sample
|
|
|
|
if self.solver_p:
|
|
return self.solver_p.step(model_output, s0, x).prev_sample
|
|
|
|
sigma_t, sigma_s0 = self.sigmas[self.step_index + 1], self.sigmas[self.step_index]
|
|
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
|
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
|
|
|
|
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
|
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
|
|
h = lambda_t - lambda_s0
|
|
device = sample.device
|
|
|
|
rks = []
|
|
D1s: list[Any] | None = []
|
|
for i in range(1, order):
|
|
si = self.step_index - i
|
|
mi = model_output_list[-(i + 1)]
|
|
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
|
|
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
|
|
rk = (lambda_si - lambda_s0) / h
|
|
rks.append(rk)
|
|
D1s.append((mi - m0) / rk)
|
|
|
|
rks.append(1.0)
|
|
rks = torch.tensor(rks, device=device)
|
|
|
|
R = []
|
|
b = []
|
|
hh = -h if self.predict_x0 else h
|
|
h_phi_1 = torch.expm1(hh)
|
|
h_phi_k = h_phi_1 / hh - 1
|
|
factorial_i = 1
|
|
|
|
if self.config.solver_type == "bh1":
|
|
B_h = hh
|
|
elif self.config.solver_type == "bh2":
|
|
B_h = torch.expm1(hh)
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
for i in range(1, order + 1):
|
|
R.append(torch.pow(rks, i - 1))
|
|
b.append(h_phi_k * factorial_i / B_h)
|
|
factorial_i *= i + 1
|
|
h_phi_k = h_phi_k / hh - 1 / factorial_i
|
|
|
|
R = torch.stack(R)
|
|
b = torch.tensor(b, device=device)
|
|
|
|
if len(D1s) > 0:
|
|
D1s = torch.stack(D1s, dim=1)
|
|
if order == 2:
|
|
rhos_p = torch.tensor([0.5], dtype=x.dtype, device=device)
|
|
else:
|
|
rhos_p = torch.linalg.solve(R[:-1, :-1], b[:-1]).to(device).to(x.dtype)
|
|
else:
|
|
D1s = None
|
|
|
|
if self.predict_x0:
|
|
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
|
|
pred_res = torch.einsum("k,bkc...->bc...", rhos_p, D1s) if D1s is not None else 0
|
|
x_t = x_t_ - alpha_t * B_h * pred_res
|
|
else:
|
|
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
|
|
pred_res = torch.einsum("k,bkc...->bc...", rhos_p, D1s) if D1s is not None else 0
|
|
x_t = x_t_ - sigma_t * B_h * pred_res
|
|
|
|
return x_t.to(x.dtype)
|
|
|
|
def multistep_uni_c_bh_update(
|
|
self,
|
|
this_model_output: torch.Tensor,
|
|
*args,
|
|
last_sample: torch.Tensor = None,
|
|
this_sample: torch.Tensor = None,
|
|
order: int | None = None,
|
|
**kwargs,
|
|
) -> torch.Tensor:
|
|
this_timestep = args[0] if len(args) > 0 else kwargs.pop("this_timestep", None)
|
|
if last_sample is None:
|
|
if len(args) > 1:
|
|
last_sample = args[1]
|
|
else:
|
|
raise ValueError(" missing`last_sample` as a required keyword argument")
|
|
if this_sample is None:
|
|
if len(args) > 2:
|
|
this_sample = args[2]
|
|
else:
|
|
raise ValueError(" missing`this_sample` as a required keyword argument")
|
|
if order is None:
|
|
if len(args) > 3:
|
|
order = args[3]
|
|
else:
|
|
raise ValueError(" missing`order` as a required keyword argument")
|
|
if this_timestep is not None:
|
|
deprecate("this_timestep", "1.0.0", "Passing `this_timestep` is deprecated and has no effect.")
|
|
|
|
model_output_list = self.model_outputs
|
|
m0 = model_output_list[-1]
|
|
x = last_sample
|
|
x_t = this_sample
|
|
model_t = this_model_output
|
|
|
|
sigma_t, sigma_s0 = self.sigmas[self.step_index], self.sigmas[self.step_index - 1]
|
|
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
|
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
|
|
|
|
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
|
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
|
|
h = lambda_t - lambda_s0
|
|
device = this_sample.device
|
|
|
|
rks = []
|
|
D1s: list[Any] | None = []
|
|
for i in range(1, order):
|
|
si = self.step_index - (i + 1)
|
|
mi = model_output_list[-(i + 1)]
|
|
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
|
|
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
|
|
rk = (lambda_si - lambda_s0) / h
|
|
rks.append(rk)
|
|
D1s.append((mi - m0) / rk)
|
|
|
|
rks.append(1.0)
|
|
rks = torch.tensor(rks, device=device)
|
|
|
|
R = []
|
|
b = []
|
|
hh = -h if self.predict_x0 else h
|
|
h_phi_1 = torch.expm1(hh)
|
|
h_phi_k = h_phi_1 / hh - 1
|
|
factorial_i = 1
|
|
|
|
if self.config.solver_type == "bh1":
|
|
B_h = hh
|
|
elif self.config.solver_type == "bh2":
|
|
B_h = torch.expm1(hh)
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
for i in range(1, order + 1):
|
|
R.append(torch.pow(rks, i - 1))
|
|
b.append(h_phi_k * factorial_i / B_h)
|
|
factorial_i *= i + 1
|
|
h_phi_k = h_phi_k / hh - 1 / factorial_i
|
|
|
|
R = torch.stack(R)
|
|
b = torch.tensor(b, device=device)
|
|
|
|
if len(D1s) > 0:
|
|
D1s = torch.stack(D1s, dim=1)
|
|
else:
|
|
D1s = None
|
|
|
|
if order == 1:
|
|
rhos_c = torch.tensor([0.5], dtype=x.dtype, device=device)
|
|
else:
|
|
rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype)
|
|
|
|
if self.predict_x0:
|
|
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
|
|
corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s) if D1s is not None else 0
|
|
D1_t = model_t - m0
|
|
x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t)
|
|
else:
|
|
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
|
|
corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s) if D1s is not None else 0
|
|
D1_t = model_t - m0
|
|
x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t)
|
|
return x_t.to(x.dtype)
|
|
|
|
def index_for_timestep(self, timestep, schedule_timesteps=None):
|
|
if schedule_timesteps is None:
|
|
schedule_timesteps = self.timesteps
|
|
indices = (schedule_timesteps == timestep).nonzero()
|
|
pos = 1 if len(indices) > 1 else 0
|
|
return indices[pos].item()
|
|
|
|
def _init_step_index(self, timestep):
|
|
if self.begin_index is None:
|
|
if isinstance(timestep, torch.Tensor):
|
|
timestep = timestep.to(self.timesteps.device)
|
|
self._step_index = self.index_for_timestep(timestep)
|
|
else:
|
|
self._step_index = self._begin_index
|
|
|
|
def step(
|
|
self,
|
|
model_output: torch.Tensor,
|
|
timestep: int | torch.Tensor,
|
|
sample: torch.Tensor,
|
|
return_dict: bool = True,
|
|
generator=None,
|
|
) -> SchedulerOutput | tuple:
|
|
if self.num_inference_steps is None:
|
|
raise ValueError(
|
|
"Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler"
|
|
)
|
|
|
|
if self.step_index is None:
|
|
self._init_step_index(timestep)
|
|
|
|
use_corrector = (
|
|
self.step_index > 0 and self.step_index - 1 not in self.disable_corrector and self.last_sample is not None
|
|
)
|
|
|
|
model_output_convert = self.convert_model_output(model_output, sample=sample)
|
|
if use_corrector:
|
|
sample = self.multistep_uni_c_bh_update(
|
|
this_model_output=model_output_convert,
|
|
last_sample=self.last_sample,
|
|
this_sample=sample,
|
|
order=self.this_order,
|
|
)
|
|
|
|
for i in range(self.config.solver_order - 1):
|
|
self.model_outputs[i] = self.model_outputs[i + 1]
|
|
self.timestep_list[i] = self.timestep_list[i + 1]
|
|
|
|
self.model_outputs[-1] = model_output_convert
|
|
self.timestep_list[-1] = timestep
|
|
|
|
if self.config.lower_order_final:
|
|
this_order = min(self.config.solver_order, len(self.timesteps) - self.step_index)
|
|
else:
|
|
this_order = self.config.solver_order
|
|
|
|
self.this_order = min(this_order, self.lower_order_nums + 1)
|
|
assert self.this_order > 0
|
|
|
|
self.last_sample = sample
|
|
prev_sample = self.multistep_uni_p_bh_update(model_output=model_output, sample=sample, order=self.this_order)
|
|
|
|
if self.lower_order_nums < self.config.solver_order:
|
|
self.lower_order_nums += 1
|
|
|
|
self._step_index += 1
|
|
|
|
if not return_dict:
|
|
return (prev_sample,)
|
|
return SchedulerOutput(prev_sample=prev_sample)
|
|
|
|
def step_ddim(
|
|
self,
|
|
velocity: torch.FloatTensor,
|
|
t: int,
|
|
curr_state: torch.FloatTensor,
|
|
prev_state: torch.FloatTensor | None = None,
|
|
generator: torch.Generator | None = None,
|
|
):
|
|
device = curr_state.device
|
|
curr_t = self.sigmas[t]
|
|
prev_t = self.sigmas[t + 1]
|
|
variance_noise = randn_tensor(curr_state.shape, generator=generator, device=device, dtype=curr_state.dtype)
|
|
cur_clean_ = curr_state - curr_t * velocity
|
|
return prev_t * variance_noise + (1 - prev_t) * cur_clean_
|
|
|
|
def step_sde(
|
|
self,
|
|
velocity: torch.FloatTensor,
|
|
t: int,
|
|
curr_state: torch.FloatTensor,
|
|
noise_theta: float = 1.0,
|
|
prev_state: torch.FloatTensor | None = None,
|
|
generator: torch.Generator | None = None,
|
|
):
|
|
device = curr_state.device
|
|
curr_t = self.sigmas[t]
|
|
prev_t = self.sigmas[t + 1]
|
|
cos = torch.cos(torch.tensor(noise_theta) * torch.pi / 2).to(device)
|
|
sin = torch.sin(torch.tensor(noise_theta) * torch.pi / 2).to(device)
|
|
prev_sample_mean = (1 - prev_t + prev_t * cos) * (curr_state - curr_t * velocity) + prev_t * cos * velocity
|
|
std_dev_t = prev_t * sin
|
|
std_dev_t = torch.ones((1, 1)).to(curr_state) * std_dev_t
|
|
if prev_state is None:
|
|
variance_noise = randn_tensor(curr_state.shape, generator=generator, device=device, dtype=curr_state.dtype)
|
|
prev_state = prev_sample_mean + std_dev_t * variance_noise
|
|
else:
|
|
prev_state = prev_sample_mean + (prev_state - prev_sample_mean.detach())
|
|
return prev_state
|
|
|
|
def scale_model_input(self, sample: torch.Tensor, *args, **kwargs) -> torch.Tensor:
|
|
return sample
|
|
|
|
def add_noise(
|
|
self, original_samples: torch.Tensor, noise: torch.Tensor, timesteps: torch.IntTensor
|
|
) -> torch.Tensor:
|
|
sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype)
|
|
if original_samples.device.type == "mps" and torch.is_floating_point(timesteps):
|
|
schedule_timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32)
|
|
timesteps = timesteps.to(original_samples.device, dtype=torch.float32)
|
|
else:
|
|
schedule_timesteps = self.timesteps.to(original_samples.device)
|
|
timesteps = timesteps.to(original_samples.device)
|
|
|
|
if self.begin_index is None:
|
|
step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timesteps]
|
|
elif self.step_index is not None:
|
|
step_indices = [self.step_index] * timesteps.shape[0]
|
|
else:
|
|
step_indices = [self.begin_index] * timesteps.shape[0]
|
|
|
|
sigma = sigmas[step_indices].flatten()
|
|
while len(sigma.shape) < len(original_samples.shape):
|
|
sigma = sigma.unsqueeze(-1)
|
|
|
|
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
|
return alpha_t * original_samples + sigma_t * noise
|
|
|
|
def __len__(self):
|
|
return self.config.num_train_timesteps
|
|
|
|
|
|
# ===========================================================================
|
|
# Audio VAE (ported from daVinci-MagiHuman inference/model/sa_audio/)
|
|
# ===========================================================================
|
|
def _snake_beta(x, alpha, beta):
|
|
return x + (1.0 / (beta + 1e-9)) * torch.pow(torch.sin(x * alpha), 2)
|
|
|
|
|
|
class _SnakeBeta(nn.Module):
|
|
def __init__(self, in_features: int, alpha: float = 1.0, alpha_trainable: bool = True, alpha_logscale: bool = True):
|
|
super().__init__()
|
|
self.alpha_logscale = alpha_logscale
|
|
if self.alpha_logscale:
|
|
self.alpha = nn.Parameter(torch.zeros(in_features) * alpha)
|
|
self.beta = nn.Parameter(torch.zeros(in_features) * alpha)
|
|
else:
|
|
self.alpha = nn.Parameter(torch.ones(in_features) * alpha)
|
|
self.beta = nn.Parameter(torch.ones(in_features) * alpha)
|
|
self.alpha.requires_grad = alpha_trainable
|
|
self.beta.requires_grad = alpha_trainable
|
|
|
|
def forward(self, x):
|
|
alpha = self.alpha.unsqueeze(0).unsqueeze(-1)
|
|
beta = self.beta.unsqueeze(0).unsqueeze(-1)
|
|
if self.alpha_logscale:
|
|
alpha = torch.exp(alpha)
|
|
beta = torch.exp(beta)
|
|
return _snake_beta(x, alpha, beta)
|
|
|
|
|
|
def _vae_sample(mean, scale):
|
|
stdev = F.softplus(scale) + 1e-4
|
|
var = stdev * stdev
|
|
logvar = torch.log(var)
|
|
latents = torch.randn_like(mean) * stdev + mean
|
|
kl = (mean * mean + var - logvar - 1).sum(1).mean()
|
|
return latents, kl
|
|
|
|
|
|
class _VAEBottleneck(nn.Module):
|
|
def encode(self, x, return_info=False, **kwargs):
|
|
info = {}
|
|
mean, scale = x.chunk(2, dim=1)
|
|
x, kl = _vae_sample(mean, scale)
|
|
info["kl"] = kl
|
|
return (x, info) if return_info else x
|
|
|
|
def decode(self, x):
|
|
return x
|
|
|
|
|
|
def _WNConv1d(*args, **kwargs):
|
|
return weight_norm(nn.Conv1d(*args, **kwargs))
|
|
|
|
|
|
def _WNConvTranspose1d(*args, **kwargs):
|
|
return weight_norm(nn.ConvTranspose1d(*args, **kwargs))
|
|
|
|
|
|
def _checkpoint(function, *args, **kwargs):
|
|
kwargs.setdefault("use_reentrant", False)
|
|
return torch.utils.checkpoint.checkpoint(function, *args, **kwargs)
|
|
|
|
|
|
def _get_activation(activation: Literal["elu", "snake", "none"], antialias: bool = False, channels=None) -> nn.Module:
|
|
if antialias:
|
|
raise NotImplementedError("antialias activation not supported")
|
|
if activation == "elu":
|
|
return nn.ELU()
|
|
if activation == "snake":
|
|
return _SnakeBeta(channels)
|
|
if activation == "none":
|
|
return nn.Identity()
|
|
raise ValueError(f"Unknown activation {activation}")
|
|
|
|
|
|
class _ResidualUnit(nn.Module):
|
|
def __init__(self, in_channels, out_channels, dilation, use_snake=False, antialias_activation=False):
|
|
super().__init__()
|
|
padding = (dilation * (7 - 1)) // 2
|
|
self.layers = nn.Sequential(
|
|
_get_activation("snake" if use_snake else "elu", antialias=antialias_activation, channels=out_channels),
|
|
_WNConv1d(in_channels, out_channels, kernel_size=7, dilation=dilation, padding=padding),
|
|
_get_activation("snake" if use_snake else "elu", antialias=antialias_activation, channels=out_channels),
|
|
_WNConv1d(out_channels, out_channels, kernel_size=1),
|
|
)
|
|
|
|
def forward(self, x):
|
|
return (_checkpoint(self.layers, x) if self.training else self.layers(x)) + x
|
|
|
|
|
|
class _EncoderBlock(nn.Module):
|
|
def __init__(self, in_channels, out_channels, stride, use_snake=False, antialias_activation=False):
|
|
super().__init__()
|
|
self.layers = nn.Sequential(
|
|
_ResidualUnit(in_channels, in_channels, 1, use_snake=use_snake),
|
|
_ResidualUnit(in_channels, in_channels, 3, use_snake=use_snake),
|
|
_ResidualUnit(in_channels, in_channels, 9, use_snake=use_snake),
|
|
_get_activation("snake" if use_snake else "elu", antialias=antialias_activation, channels=in_channels),
|
|
_WNConv1d(in_channels, out_channels, kernel_size=2 * stride, stride=stride, padding=math.ceil(stride / 2)),
|
|
)
|
|
|
|
def forward(self, x):
|
|
return self.layers(x)
|
|
|
|
|
|
class _DecoderBlock(nn.Module):
|
|
def __init__(
|
|
self, in_channels, out_channels, stride, use_snake=False, antialias_activation=False, use_nearest_upsample=False
|
|
):
|
|
super().__init__()
|
|
if use_nearest_upsample:
|
|
upsample_layer = nn.Sequential(
|
|
nn.Upsample(scale_factor=stride, mode="nearest"),
|
|
_WNConv1d(in_channels, out_channels, kernel_size=2 * stride, stride=1, bias=False, padding="same"),
|
|
)
|
|
else:
|
|
upsample_layer = _WNConvTranspose1d(
|
|
in_channels, out_channels, kernel_size=2 * stride, stride=stride, padding=math.ceil(stride / 2)
|
|
)
|
|
self.layers = nn.Sequential(
|
|
_get_activation("snake" if use_snake else "elu", antialias=antialias_activation, channels=in_channels),
|
|
upsample_layer,
|
|
_ResidualUnit(out_channels, out_channels, 1, use_snake=use_snake),
|
|
_ResidualUnit(out_channels, out_channels, 3, use_snake=use_snake),
|
|
_ResidualUnit(out_channels, out_channels, 9, use_snake=use_snake),
|
|
)
|
|
|
|
def forward(self, x):
|
|
return self.layers(x)
|
|
|
|
|
|
class _OobleckEncoder(nn.Module):
|
|
def __init__(
|
|
self,
|
|
in_channels=2,
|
|
channels=128,
|
|
latent_dim=32,
|
|
c_mults=[1, 2, 4, 8],
|
|
strides=[2, 4, 8, 8],
|
|
use_snake=False,
|
|
antialias_activation=False,
|
|
):
|
|
super().__init__()
|
|
c_mults = [1] + c_mults
|
|
depth = len(c_mults)
|
|
layers = [_WNConv1d(in_channels, c_mults[0] * channels, kernel_size=7, padding=3)]
|
|
for i in range(depth - 1):
|
|
layers.append(
|
|
_EncoderBlock(c_mults[i] * channels, c_mults[i + 1] * channels, strides[i], use_snake=use_snake)
|
|
)
|
|
layers.extend(
|
|
[
|
|
_get_activation(
|
|
"snake" if use_snake else "elu", antialias=antialias_activation, channels=c_mults[-1] * channels
|
|
),
|
|
_WNConv1d(c_mults[-1] * channels, latent_dim, kernel_size=3, padding=1),
|
|
]
|
|
)
|
|
self.layers = nn.Sequential(*layers)
|
|
|
|
def forward(self, x):
|
|
return self.layers(x)
|
|
|
|
|
|
class _OobleckDecoder(nn.Module):
|
|
def __init__(
|
|
self,
|
|
out_channels=2,
|
|
channels=128,
|
|
latent_dim=32,
|
|
c_mults=[1, 2, 4, 8],
|
|
strides=[2, 4, 8, 8],
|
|
use_snake=False,
|
|
antialias_activation=False,
|
|
use_nearest_upsample=False,
|
|
final_tanh=True,
|
|
):
|
|
super().__init__()
|
|
c_mults = [1] + c_mults
|
|
depth = len(c_mults)
|
|
layers = [_WNConv1d(latent_dim, c_mults[-1] * channels, kernel_size=7, padding=3)]
|
|
for i in range(depth - 1, 0, -1):
|
|
layers.append(
|
|
_DecoderBlock(
|
|
c_mults[i] * channels,
|
|
c_mults[i - 1] * channels,
|
|
strides[i - 1],
|
|
use_snake=use_snake,
|
|
antialias_activation=antialias_activation,
|
|
use_nearest_upsample=use_nearest_upsample,
|
|
)
|
|
)
|
|
layers.extend(
|
|
[
|
|
_get_activation(
|
|
"snake" if use_snake else "elu", antialias=antialias_activation, channels=c_mults[0] * channels
|
|
),
|
|
_WNConv1d(c_mults[0] * channels, out_channels, kernel_size=7, padding=3, bias=False),
|
|
nn.Tanh() if final_tanh else nn.Identity(),
|
|
]
|
|
)
|
|
self.layers = nn.Sequential(*layers)
|
|
|
|
def forward(self, x):
|
|
return self.layers(x)
|
|
|
|
|
|
class _AudioAutoencoder(nn.Module):
|
|
def __init__(
|
|
self,
|
|
encoder,
|
|
decoder,
|
|
latent_dim,
|
|
downsampling_ratio,
|
|
sample_rate,
|
|
io_channels=2,
|
|
bottleneck=None,
|
|
in_channels=None,
|
|
out_channels=None,
|
|
soft_clip=False,
|
|
):
|
|
super().__init__()
|
|
self.downsampling_ratio = downsampling_ratio
|
|
self.sample_rate = sample_rate
|
|
self.latent_dim = latent_dim
|
|
self.io_channels = io_channels
|
|
self.in_channels = in_channels if in_channels is not None else io_channels
|
|
self.out_channels = out_channels if out_channels is not None else io_channels
|
|
self.bottleneck = bottleneck
|
|
self.encoder = encoder
|
|
self.decoder = decoder
|
|
self.soft_clip = soft_clip
|
|
|
|
def encode(self, audio, skip_bottleneck=False, return_info=False, **kwargs):
|
|
info = {}
|
|
latents = self.encoder(audio)
|
|
info["pre_bottleneck_latents"] = latents
|
|
if self.bottleneck is not None and not skip_bottleneck:
|
|
latents, bottleneck_info = self.bottleneck.encode(latents, return_info=True, **kwargs)
|
|
info.update(bottleneck_info)
|
|
return (latents, info) if return_info else latents
|
|
|
|
def decode(self, latents, skip_bottleneck=False, **kwargs):
|
|
if self.bottleneck is not None and not skip_bottleneck:
|
|
latents = self.bottleneck.decode(latents)
|
|
decoded = self.decoder(latents, **kwargs)
|
|
if self.soft_clip:
|
|
decoded = torch.tanh(decoded)
|
|
return decoded
|
|
|
|
|
|
def _create_encoder_from_config(cfg: dict[str, Any]):
|
|
assert cfg.get("type") == "oobleck", f"Only 'oobleck' encoder supported, got: {cfg.get('type')}"
|
|
enc = _OobleckEncoder(**cfg["config"])
|
|
if not cfg.get("requires_grad", True):
|
|
for p in enc.parameters():
|
|
p.requires_grad = False
|
|
return enc
|
|
|
|
|
|
def _create_decoder_from_config(cfg: dict[str, Any]):
|
|
assert cfg.get("type") == "oobleck", f"Only 'oobleck' decoder supported, got: {cfg.get('type')}"
|
|
dec = _OobleckDecoder(**cfg["config"])
|
|
if not cfg.get("requires_grad", True):
|
|
for p in dec.parameters():
|
|
p.requires_grad = False
|
|
return dec
|
|
|
|
|
|
def _create_bottleneck_from_config(cfg: dict[str, Any]):
|
|
assert cfg.get("type") == "vae", f"Only 'vae' bottleneck supported, got: {cfg.get('type')}"
|
|
bn = _VAEBottleneck()
|
|
if not cfg.get("requires_grad", True):
|
|
for p in bn.parameters():
|
|
p.requires_grad = False
|
|
return bn
|
|
|
|
|
|
def _create_autoencoder_from_config(config: dict[str, Any]):
|
|
ae_config = config["model"]
|
|
if ae_config.get("pretransform") is not None:
|
|
raise NotImplementedError("Nested pretransform not supported")
|
|
encoder = _create_encoder_from_config(ae_config["encoder"])
|
|
decoder = _create_decoder_from_config(ae_config["decoder"])
|
|
bottleneck_cfg = ae_config.get("bottleneck")
|
|
bottleneck = _create_bottleneck_from_config(bottleneck_cfg) if bottleneck_cfg else None
|
|
return _AudioAutoencoder(
|
|
encoder=encoder,
|
|
decoder=decoder,
|
|
latent_dim=ae_config["latent_dim"],
|
|
downsampling_ratio=ae_config["downsampling_ratio"],
|
|
sample_rate=config["sample_rate"],
|
|
io_channels=ae_config["io_channels"],
|
|
bottleneck=bottleneck,
|
|
in_channels=ae_config.get("in_channels"),
|
|
out_channels=ae_config.get("out_channels"),
|
|
soft_clip=ae_config["decoder"].get("soft_clip", False),
|
|
)
|
|
|
|
|
|
class SAAudioFeatureExtractor:
|
|
def __init__(self, device, model_path):
|
|
self.device = device
|
|
self.vae_model, self.sample_rate = self._load_vae(model_path)
|
|
self.resampler = None
|
|
|
|
def _load_vae(self, model_path):
|
|
if not (isinstance(model_path, str) and Path(model_path).is_dir()):
|
|
raise ValueError("model_path must be a local directory")
|
|
|
|
model_config_path = os.path.join(model_path, "model_config.json")
|
|
with open(model_config_path) as f:
|
|
full_config = json.load(f)
|
|
|
|
vae_config = full_config["model"]["pretransform"]["config"]
|
|
sample_rate = full_config["sample_rate"]
|
|
|
|
autoencoder_config = {
|
|
"model_type": "autoencoder",
|
|
"sample_rate": sample_rate,
|
|
"model": vae_config,
|
|
}
|
|
vae_model = _create_autoencoder_from_config(autoencoder_config)
|
|
|
|
weights_path = Path(model_path) / "model.safetensors"
|
|
if not weights_path.exists():
|
|
raise FileNotFoundError(f"Weight file does not exist: {weights_path}")
|
|
|
|
full_state_dict = load_file(weights_path, device=str(self.device))
|
|
vae_state_dict = {}
|
|
for key, value in full_state_dict.items():
|
|
if key.startswith("pretransform.model."):
|
|
vae_state_dict[key[len("pretransform.model.") :]] = value
|
|
|
|
model_keys = set(vae_model.state_dict().keys())
|
|
vae_keys = set(vae_state_dict.keys())
|
|
missing = model_keys - vae_keys
|
|
extra = vae_keys - model_keys
|
|
if missing:
|
|
logger.warning("Audio VAE missing keys (%d): %s", len(missing), list(missing)[:5])
|
|
if extra:
|
|
logger.warning("Audio VAE unexpected keys (%d): %s", len(extra), list(extra)[:5])
|
|
|
|
vae_model.load_state_dict(vae_state_dict)
|
|
vae_model.to(self.device)
|
|
return vae_model, sample_rate
|
|
|
|
def decode(self, latents):
|
|
with torch.no_grad():
|
|
return self.vae_model.decode(latents)
|
|
|
|
def encode(self, waveform):
|
|
with torch.no_grad():
|
|
return self.vae_model.encode(waveform)
|
|
|
|
|
|
# ===========================================================================
|
|
# Audio utilities (ported from daVinci-MagiHuman inference/pipeline/video_process.py)
|
|
# ===========================================================================
|
|
_SAMPLE_RATE = 51200
|
|
_AUDIO_CHUNK_DURATION = 29
|
|
_OVERLAP_RATIO = 0.5
|
|
|
|
|
|
def _merge_overlapping_vae_features(audio_feats: list[torch.Tensor], overlap_ratio: float = 0.5) -> torch.Tensor | None:
|
|
if not audio_feats:
|
|
return None
|
|
if len(audio_feats) == 1:
|
|
return audio_feats[0]
|
|
|
|
batch_size, total_frames, feature_dim = audio_feats[0].shape
|
|
overlap_frames = int(total_frames * overlap_ratio)
|
|
step_frames = total_frames - overlap_frames
|
|
final_length = (len(audio_feats) - 1) * step_frames + total_frames
|
|
output_feat = torch.zeros(
|
|
batch_size, final_length, feature_dim, device=audio_feats[0].device, dtype=audio_feats[0].dtype
|
|
)
|
|
|
|
for block_idx, current_feat in enumerate(audio_feats):
|
|
output_start = block_idx * step_frames
|
|
if block_idx == 0:
|
|
output_feat[:, output_start : output_start + total_frames, :] = current_feat
|
|
continue
|
|
|
|
non_overlap_start = output_start + overlap_frames
|
|
non_overlap_end = output_start + total_frames
|
|
output_feat[:, non_overlap_start:non_overlap_end, :] = current_feat[:, overlap_frames:, :]
|
|
|
|
for frame_idx in range(overlap_frames):
|
|
output_pos = output_start + frame_idx
|
|
prev_weight = (overlap_frames - frame_idx) / overlap_frames
|
|
curr_weight = frame_idx / overlap_frames
|
|
output_feat[:, output_pos, :] = (
|
|
prev_weight * output_feat[:, output_pos, :] + curr_weight * current_feat[:, frame_idx, :]
|
|
)
|
|
return output_feat
|
|
|
|
|
|
def load_audio_and_encode(audio_vae, audio_path: str, seconds: int | None = None) -> torch.Tensor:
|
|
"""Load audio from file and encode to latent space using the Stable Audio VAE."""
|
|
audio_full = whisper.load_audio(audio_path, sr=_SAMPLE_RATE)
|
|
if seconds is not None:
|
|
audio_full = audio_full[: min(int(seconds * _SAMPLE_RATE), audio_full.shape[0])]
|
|
total_samples = audio_full.shape[0]
|
|
|
|
window_size = int(_AUDIO_CHUNK_DURATION * _SAMPLE_RATE)
|
|
step_size = int(window_size * (1 - _OVERLAP_RATIO))
|
|
if total_samples <= window_size:
|
|
audio = torch.from_numpy(audio_full).cuda()
|
|
audio = audio.unsqueeze(0).expand(2, -1)
|
|
return audio_vae.vae_model.encode(audio)
|
|
|
|
encoded_chunks = []
|
|
latent_to_audio_ratio = None
|
|
for offset_start in range(0, total_samples, step_size):
|
|
offset_end = min(offset_start + window_size, total_samples)
|
|
chunk = whisper.pad_or_trim(audio_full[offset_start:offset_end], length=window_size)
|
|
chunk_tensor = torch.from_numpy(chunk).cuda().unsqueeze(0).expand(2, -1)
|
|
encoded_chunk = audio_vae.vae_model.encode(chunk_tensor)
|
|
|
|
if latent_to_audio_ratio is None:
|
|
latent_to_audio_ratio = encoded_chunk.shape[-1] / window_size
|
|
|
|
encoded_chunks.append(encoded_chunk.permute(0, 2, 1))
|
|
if offset_end >= total_samples:
|
|
break
|
|
|
|
final_feat = _merge_overlapping_vae_features(encoded_chunks, overlap_ratio=_OVERLAP_RATIO).permute(0, 2, 1)
|
|
final_target_len = math.ceil(total_samples * latent_to_audio_ratio)
|
|
return final_feat[:, :, :final_target_len]
|
|
|
|
|
|
# ===========================================================================
|
|
# Data proxy (ported from daVinci-MagiHuman inference/pipeline/data_proxy.py)
|
|
# ===========================================================================
|
|
def _unfold_3d(x: torch.Tensor, kernel_size: tuple[int, int, int], stride: tuple[int, int, int]) -> torch.Tensor:
|
|
"""Pure-PyTorch 3D unfold matching UnfoldAnd behavior.
|
|
|
|
After N unfold ops the shape is (batch, C, oD, oH, oW, kD, kH, kW).
|
|
UnfoldAnd permutes kernel dims next to channel before reshape so that the
|
|
col_dim axis is ordered as (C, kD, kH, kW) -- matching F.unfold semantics.
|
|
Without this permute, .view() interleaves spatial and kernel positions.
|
|
|
|
Args:
|
|
x: (N, C, D, H, W)
|
|
kernel_size: (kD, kH, kW)
|
|
stride: (sD, sH, sW)
|
|
Returns:
|
|
(N, C*kD*kH*kW, L) where L = product of output spatial dims.
|
|
"""
|
|
ndim = len(kernel_size)
|
|
for d in range(ndim):
|
|
x = x.unfold(d + 2, kernel_size[d], stride[d])
|
|
perm = [0, 1] + list(range(ndim + 2, 2 * ndim + 2)) + list(range(2, ndim + 2))
|
|
x = x.permute(*perm).contiguous()
|
|
|
|
batch_size = x.shape[0]
|
|
col_dim = 1
|
|
for i in range(1, ndim + 2):
|
|
col_dim *= x.shape[i]
|
|
spatial = 1
|
|
for i in range(ndim + 2, 2 * ndim + 2):
|
|
spatial *= x.shape[i]
|
|
return x.view(batch_size, col_dim, spatial)
|
|
|
|
|
|
def _calc_local_qk_range(num_video_tokens, num_audio_and_txt_tokens, num_frames, frame_receptive_field):
|
|
token_per_frame = num_video_tokens // num_frames
|
|
total_tokens = num_video_tokens + num_audio_and_txt_tokens
|
|
|
|
q_range_list = []
|
|
k_range_list = []
|
|
for i in range(num_frames):
|
|
q_range_list.append(torch.tensor([i * token_per_frame, (i + 1) * token_per_frame]))
|
|
k_range_list.append(
|
|
torch.tensor(
|
|
[
|
|
(i - frame_receptive_field) * token_per_frame,
|
|
(i + frame_receptive_field + 1) * token_per_frame,
|
|
]
|
|
)
|
|
)
|
|
local_q_range = torch.stack(q_range_list, dim=0)
|
|
local_k_range = torch.stack(k_range_list, dim=0)
|
|
|
|
local_k_range[local_k_range < 0] = 0
|
|
local_k_range[local_k_range > num_video_tokens] = num_video_tokens
|
|
|
|
video_q_range = torch.tensor([[0, num_video_tokens]])
|
|
video_k_range = torch.tensor([[num_video_tokens, num_video_tokens + num_audio_and_txt_tokens]])
|
|
|
|
at_q_ranges = torch.tensor([[num_video_tokens, total_tokens]])
|
|
at_k_ranges = torch.tensor([[0, total_tokens]])
|
|
|
|
q_ranges = (
|
|
torch.cat([local_q_range, video_q_range, at_q_ranges], dim=0).to(torch.int32).to("cuda", non_blocking=True)
|
|
)
|
|
k_ranges = (
|
|
torch.cat([local_k_range, video_k_range, at_k_ranges], dim=0).to(torch.int32).to("cuda", non_blocking=True)
|
|
)
|
|
return q_ranges, k_ranges
|
|
|
|
|
|
def _calc_local_attn_ffa_handler(num_video_tokens, num_audio_and_txt_tokens, num_frames, frame_receptive_field):
|
|
q_ranges, k_ranges = _calc_local_qk_range(
|
|
num_video_tokens, num_audio_and_txt_tokens, num_frames, frame_receptive_field
|
|
)
|
|
total = num_video_tokens + num_audio_and_txt_tokens
|
|
return FFAHandler(
|
|
q_ranges=q_ranges,
|
|
k_ranges=k_ranges,
|
|
max_seqlen_q=total,
|
|
max_seqlen_k=total,
|
|
attn_type_map=torch.zeros([q_ranges.shape[0]], device="cuda", dtype=torch.int32),
|
|
softmax_scale=None,
|
|
)
|
|
|
|
|
|
def _get_coords(
|
|
shape: list[int],
|
|
ref_feat_shape: list[int],
|
|
offset_thw: list[int] | None = None,
|
|
device: torch.device = torch.device("cpu"),
|
|
dtype: torch.dtype = torch.float32,
|
|
):
|
|
if offset_thw is None:
|
|
offset_thw = [0, 0, 0]
|
|
ori_t, ori_h, ori_w = shape
|
|
ref_t, ref_h, ref_w = ref_feat_shape
|
|
|
|
offset_t, offset_h, offset_w = offset_thw
|
|
time_rng = torch.arange(ori_t, device=device, dtype=dtype) + offset_t
|
|
height_rng = torch.arange(ori_h, device=device, dtype=dtype) + offset_h
|
|
width_rng = torch.arange(ori_w, device=device, dtype=dtype) + offset_w
|
|
|
|
time_grid, height_grid, width_grid = torch.meshgrid(time_rng, height_rng, width_rng, indexing="ij")
|
|
coords_flat = torch.stack([time_grid, height_grid, width_grid], dim=-1).reshape(-1, 3)
|
|
|
|
meta = torch.tensor([ori_t, ori_h, ori_w, ref_t, ref_h, ref_w], device=device, dtype=dtype)
|
|
meta_expanded = meta.expand(coords_flat.size(0), -1)
|
|
return torch.cat([coords_flat, meta_expanded], dim=-1)
|
|
|
|
|
|
@dataclass
|
|
class _SingleData:
|
|
video_x_t: torch.Tensor
|
|
audio_x_t: torch.Tensor
|
|
audio_feat_len: int
|
|
txt_feat: torch.Tensor
|
|
txt_feat_len: int
|
|
t: int
|
|
h: int
|
|
w: int
|
|
patch_size: int
|
|
t_patch_size: int
|
|
spatial_rope_interpolation: Literal["inter", "extra"]
|
|
ref_audio_offset: int
|
|
text_offset: int
|
|
coords_style: Literal["v1", "v2"] = "v1"
|
|
|
|
def __post_init__(self):
|
|
self.video_token_num = self.video_x_t.shape[0]
|
|
self.audio_x_t = self.audio_x_t[: self.audio_feat_len]
|
|
self.txt_feat = self.txt_feat[: self.txt_feat_len]
|
|
self.video_channel = self.video_x_t.shape[-1]
|
|
self.audio_channel = self.audio_x_t.shape[-1]
|
|
self.txt_channel = self.txt_feat.shape[-1]
|
|
|
|
@property
|
|
def device(self):
|
|
return self.video_x_t.device
|
|
|
|
@property
|
|
def default_dtype(self):
|
|
return self.video_x_t.dtype
|
|
|
|
@property
|
|
def total_token_num(self):
|
|
return self.video_token_num + self.audio_feat_len + self.txt_feat_len
|
|
|
|
@property
|
|
def token_sequence(self):
|
|
tensors = [self.video_x_t, self.audio_x_t, self.txt_feat]
|
|
max_channel = max(t.shape[-1] for t in tensors)
|
|
padded = [F.pad(t, (0, max_channel - t.shape[-1])) for t in tensors]
|
|
return torch.cat(padded, dim=0)
|
|
|
|
@property
|
|
def modality_mapping(self):
|
|
v_map = torch.full((self.video_token_num,), Modality.VIDEO, dtype=torch.int64, device=self.device)
|
|
a_map = torch.full((self.audio_feat_len,), Modality.AUDIO, dtype=torch.int64, device=self.device)
|
|
t_map = torch.full((self.txt_feat_len,), Modality.TEXT, dtype=torch.int64, device=self.device)
|
|
return torch.cat([v_map, a_map, t_map], dim=0)
|
|
|
|
def _default_coords(self, shape, ref_feat_shape, offset_thw=None):
|
|
if offset_thw is None:
|
|
offset_thw = [0, 0, 0]
|
|
return _get_coords(
|
|
shape=shape,
|
|
ref_feat_shape=ref_feat_shape,
|
|
offset_thw=offset_thw,
|
|
device=self.device,
|
|
dtype=self.default_dtype,
|
|
)
|
|
|
|
@property
|
|
def coords_mapping(self):
|
|
if self.spatial_rope_interpolation == "inter":
|
|
video_ref_feat_shape = (self.t // self.t_patch_size, 32, 32)
|
|
else:
|
|
video_ref_feat_shape = (self.t // self.t_patch_size, self.h // self.patch_size, self.w // self.patch_size)
|
|
|
|
video_coords = self._default_coords(
|
|
shape=(self.t // self.t_patch_size, self.h // self.patch_size, self.w // self.patch_size),
|
|
ref_feat_shape=video_ref_feat_shape,
|
|
)
|
|
|
|
if self.coords_style == "v1":
|
|
audio_coords = self._default_coords(
|
|
shape=(self.audio_feat_len, 1, 1),
|
|
ref_feat_shape=(self.t // self.t_patch_size, 1, 1),
|
|
)
|
|
text_coords = self._default_coords(
|
|
shape=(self.txt_feat_len, 1, 1),
|
|
ref_feat_shape=(2, 1, 1),
|
|
offset_thw=[self.text_offset, 0, 0],
|
|
)
|
|
elif self.coords_style == "v2":
|
|
magic_audio_ref_t = (self.audio_feat_len - 1) // 4 + 1
|
|
audio_coords = self._default_coords(
|
|
shape=(self.audio_feat_len, 1, 1),
|
|
ref_feat_shape=(magic_audio_ref_t // self.t_patch_size, 1, 1),
|
|
)
|
|
text_coords = self._default_coords(
|
|
shape=(self.txt_feat_len, 1, 1),
|
|
ref_feat_shape=(1, 1, 1),
|
|
offset_thw=[-self.txt_feat_len, 0, 0],
|
|
)
|
|
else:
|
|
raise ValueError(f"Unknown coords_style: {self.coords_style}")
|
|
|
|
return torch.cat([video_coords, audio_coords, text_coords], dim=0)
|
|
|
|
def depack_token_sequence(self, token_sequence):
|
|
video_x_t = token_sequence[: self.video_token_num, : self.video_channel]
|
|
video_x_t = rearrange(
|
|
video_x_t,
|
|
"(T H W) (pT pH pW C) -> C (T pT) (H pH) (W pW)",
|
|
H=self.h // self.patch_size,
|
|
W=self.w // self.patch_size,
|
|
pT=self.t_patch_size,
|
|
pH=self.patch_size,
|
|
pW=self.patch_size,
|
|
).contiguous()
|
|
audio_x_t = token_sequence[
|
|
self.video_token_num : self.video_token_num + self.audio_feat_len, : self.audio_channel
|
|
]
|
|
return video_x_t, audio_x_t
|
|
|
|
|
|
@dataclass
|
|
class _SimplePackedData:
|
|
items: list[_SingleData]
|
|
|
|
@property
|
|
def token_sequence(self):
|
|
return torch.cat([item.token_sequence for item in self.items], dim=0)
|
|
|
|
@property
|
|
def modality_mapping(self):
|
|
return torch.cat([item.modality_mapping for item in self.items], dim=0)
|
|
|
|
@property
|
|
def coords_mapping(self):
|
|
return torch.cat([item.coords_mapping for item in self.items], dim=0)
|
|
|
|
@property
|
|
def total_token_num(self):
|
|
return sum(item.total_token_num for item in self.items)
|
|
|
|
def __getitem__(self, index):
|
|
return self.items[index]
|
|
|
|
@property
|
|
def cu_seqlen(self):
|
|
cu = torch.cumsum(torch.tensor([item.total_token_num for item in self.items]), dim=0)
|
|
return F.pad(cu, (1, 0))
|
|
|
|
@property
|
|
def max_seqlen(self):
|
|
return torch.tensor(max(item.total_token_num for item in self.items))
|
|
|
|
def depack_token_sequence(self, token_sequence):
|
|
video_list, audio_list = [], []
|
|
parts = torch.split(token_sequence, [item.total_token_num for item in self.items], dim=0)
|
|
for item, part in zip(self.items, parts):
|
|
v, a = item.depack_token_sequence(part)
|
|
video_list.append(v)
|
|
audio_list.append(a)
|
|
return torch.stack(video_list, dim=0), torch.stack(audio_list, dim=0)
|
|
|
|
|
|
class MagiDataProxy:
|
|
def __init__(
|
|
self,
|
|
patch_size: int = 2,
|
|
t_patch_size: int = 1,
|
|
frame_receptive_field: int = 11,
|
|
spatial_rope_interpolation: str = "extra",
|
|
ref_audio_offset: int = 1000,
|
|
text_offset: int = 0,
|
|
coords_style: str = "v2",
|
|
):
|
|
self.patch_size = patch_size
|
|
self.t_patch_size = t_patch_size
|
|
self.frame_receptive_field = frame_receptive_field
|
|
self.spatial_rope_interpolation = spatial_rope_interpolation
|
|
self.ref_audio_offset = ref_audio_offset
|
|
self.text_offset = text_offset
|
|
self.coords_style = coords_style
|
|
self._kernel = (t_patch_size, patch_size, patch_size)
|
|
self._stride = (t_patch_size, patch_size, patch_size)
|
|
self._saved_data: dict[str, Any] = {}
|
|
|
|
def saved_for_output(self, **kwargs):
|
|
self._saved_data.update(kwargs)
|
|
|
|
def get_saved_data(self, key: str):
|
|
return self._saved_data[key]
|
|
|
|
def img2tokens(self, x_t: torch.Tensor):
|
|
x_t_unfolded = _unfold_3d(x_t, self._kernel, self._stride)
|
|
return rearrange(x_t_unfolded, "N col_dim num_tokens -> N num_tokens col_dim").contiguous()
|
|
|
|
def process_input(self, transported_data: EvalInput):
|
|
batch_size, _, t, h, w = transported_data.x_t.shape
|
|
x_t = self.img2tokens(transported_data.x_t)
|
|
audio_x_t = transported_data.audio_x_t.contiguous()
|
|
text_in = transported_data.txt_feat.contiguous()
|
|
|
|
simple_packed_data = _SimplePackedData(items=[])
|
|
for i in range(batch_size):
|
|
single_data = _SingleData(
|
|
video_x_t=x_t[i],
|
|
audio_x_t=audio_x_t[i],
|
|
audio_feat_len=transported_data.audio_feat_len[i],
|
|
txt_feat=text_in[i],
|
|
txt_feat_len=transported_data.txt_feat_len[i],
|
|
t=t,
|
|
h=h,
|
|
w=w,
|
|
patch_size=self.patch_size,
|
|
t_patch_size=self.t_patch_size,
|
|
spatial_rope_interpolation=self.spatial_rope_interpolation,
|
|
ref_audio_offset=self.ref_audio_offset,
|
|
text_offset=self.text_offset,
|
|
coords_style=self.coords_style,
|
|
)
|
|
simple_packed_data.items.append(single_data)
|
|
|
|
if self.frame_receptive_field != -1:
|
|
assert batch_size == 1, "local attention only supports batch size 1"
|
|
local_attn_handler = _calc_local_attn_ffa_handler(
|
|
num_video_tokens=simple_packed_data[0].video_token_num,
|
|
num_audio_and_txt_tokens=simple_packed_data[0].audio_feat_len + simple_packed_data[0].txt_feat_len,
|
|
num_frames=t,
|
|
frame_receptive_field=self.frame_receptive_field,
|
|
)
|
|
if isinstance(local_attn_handler.max_seqlen_k, torch.Tensor):
|
|
local_attn_handler.max_seqlen_k = local_attn_handler.max_seqlen_k.item()
|
|
if isinstance(local_attn_handler.max_seqlen_q, torch.Tensor):
|
|
local_attn_handler.max_seqlen_q = local_attn_handler.max_seqlen_q.item()
|
|
else:
|
|
local_attn_handler = None
|
|
|
|
varlen_handler = VarlenHandler(
|
|
cu_seqlens_q=simple_packed_data.cu_seqlen.to(torch.int32).cuda(),
|
|
cu_seqlens_k=simple_packed_data.cu_seqlen.to(torch.int32).cuda(),
|
|
max_seqlen_q=simple_packed_data.max_seqlen.to(torch.int32).cuda(),
|
|
max_seqlen_k=simple_packed_data.max_seqlen.to(torch.int32).cuda(),
|
|
)
|
|
|
|
self.saved_for_output(simple_packed_data=simple_packed_data)
|
|
|
|
x = simple_packed_data.token_sequence
|
|
coords_mapping = simple_packed_data.coords_mapping
|
|
modality_mapping = simple_packed_data.modality_mapping
|
|
return (x, coords_mapping, modality_mapping, varlen_handler, local_attn_handler)
|
|
|
|
def process_output(self, x: torch.Tensor):
|
|
simple_packed_data: _SimplePackedData = self.get_saved_data("simple_packed_data")
|
|
return simple_packed_data.depack_token_sequence(x)
|
|
|
|
|
|
# ===========================================================================
|
|
# Pipeline helpers
|
|
# ===========================================================================
|
|
@dataclass
|
|
class EvalInput:
|
|
x_t: torch.Tensor
|
|
audio_x_t: torch.Tensor
|
|
audio_feat_len: torch.Tensor | list[int]
|
|
txt_feat: torch.Tensor
|
|
txt_feat_len: torch.Tensor | list[int]
|
|
|
|
|
|
class _T5GemmaEncoder:
|
|
def __init__(self, model_path: str, device: str, weight_dtype: torch.dtype, subfolder: str | None = None):
|
|
from vllm.distributed import get_tensor_model_parallel_world_size
|
|
|
|
self.device = device
|
|
hf_kwargs: dict[str, Any] = {}
|
|
if subfolder is not None:
|
|
hf_kwargs["subfolder"] = subfolder
|
|
self.tokenizer = AutoTokenizer.from_pretrained(model_path, **hf_kwargs)
|
|
|
|
tp_size = get_tensor_model_parallel_world_size()
|
|
if tp_size > 1:
|
|
from transformers.models.t5gemma.configuration_t5gemma import T5GemmaConfig
|
|
|
|
config = T5GemmaConfig.from_pretrained(model_path, **hf_kwargs)
|
|
# The config we need is the encoder config
|
|
config_encoder = config.encoder
|
|
# Propagate some outer config values
|
|
config_encoder.vocab_size = config.vocab_size
|
|
config_encoder.rms_norm_eps = getattr(config, "rms_norm_eps", config_encoder.rms_norm_eps)
|
|
self.model = T5GemmaEncoderModelTP(config_encoder).to(device).to(weight_dtype)
|
|
self.is_tp = True
|
|
else:
|
|
self.model = T5GemmaEncoderModel.from_pretrained(
|
|
model_path, is_encoder_decoder=False, dtype=weight_dtype, **hf_kwargs
|
|
).to(device)
|
|
self.is_tp = False
|
|
|
|
@torch.inference_mode()
|
|
def encode(self, prompt: str) -> torch.Tensor:
|
|
inputs = self.tokenizer([prompt], return_tensors="pt").to(self.device)
|
|
outputs = self.model(**inputs)
|
|
|
|
if self.is_tp:
|
|
# T5GemmaEncoderModelTP just returns the hidden states tensor
|
|
return outputs.half()
|
|
else:
|
|
# HF model returns BaseModelOutput
|
|
return outputs["last_hidden_state"].half()
|
|
|
|
|
|
def _pad_or_trim(tensor: torch.Tensor, target_size: int, dim: int, pad_value: float = 0.0) -> tuple[torch.Tensor, int]:
|
|
current_size = tensor.size(dim)
|
|
if current_size < target_size:
|
|
padding_amount = target_size - current_size
|
|
padding_tuple = [0] * (2 * tensor.dim())
|
|
padding_dim_index = tensor.dim() - 1 - dim
|
|
padding_tuple[2 * padding_dim_index + 1] = padding_amount
|
|
return F.pad(tensor, tuple(padding_tuple), "constant", pad_value), current_size
|
|
slicing = [slice(None)] * tensor.dim()
|
|
slicing[dim] = slice(0, target_size)
|
|
return tensor[tuple(slicing)], target_size
|
|
|
|
|
|
def _get_padded_t5_gemma_embedding(
|
|
prompt: str,
|
|
encoder: _T5GemmaEncoder,
|
|
target_length: int,
|
|
) -> tuple[torch.Tensor, int]:
|
|
txt_feat = encoder.encode(prompt)
|
|
txt_feat, original_len = _pad_or_trim(txt_feat, target_size=target_length, dim=1)
|
|
return txt_feat.to(torch.float32), original_len
|
|
|
|
|
|
def _resizecrop(img: Image.Image, target_height: int, target_width: int) -> Image.Image:
|
|
"""Centre-crop resize keeping aspect ratio then letterbox to target."""
|
|
pil_image = img.convert("RGB")
|
|
original_width, original_height = pil_image.size
|
|
scale_x = target_width / original_width
|
|
scale_y = target_height / original_height
|
|
scale = max(scale_x, scale_y)
|
|
new_width = int(round(original_width * scale))
|
|
new_height = int(round(original_height * scale))
|
|
resized_image = pil_image.resize((new_width, new_height), Image.LANCZOS)
|
|
left = (new_width - target_width) // 2
|
|
top = (new_height - target_height) // 2
|
|
return resized_image.crop((left, top, left + target_width, top + target_height))
|
|
|
|
|
|
class ZeroSNRDDPMDiscretization:
|
|
"""ZeroSNR DDPM sigma schedule, ported from daVinci-MagiHuman.
|
|
Used to compute sigma values for SR noise injection.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
linear_start: float = 0.00085,
|
|
linear_end: float = 0.0120,
|
|
num_timesteps: int = 1000,
|
|
shift_scale: float = 1.0,
|
|
keep_start: bool = False,
|
|
post_shift: bool = False,
|
|
):
|
|
from functools import partial
|
|
|
|
if keep_start and not post_shift:
|
|
linear_start = linear_start / (shift_scale + (1 - shift_scale) * linear_start)
|
|
self.num_timesteps = num_timesteps
|
|
betas = torch.linspace(linear_start**0.5, linear_end**0.5, num_timesteps, dtype=torch.float64) ** 2
|
|
alphas = 1.0 - betas.cpu().numpy()
|
|
self.alphas_cumprod = np.cumprod(alphas, axis=0)
|
|
self.to_torch = partial(torch.tensor, dtype=torch.float32)
|
|
if not post_shift:
|
|
self.alphas_cumprod = self.alphas_cumprod / (shift_scale + (1 - shift_scale) * self.alphas_cumprod)
|
|
self.post_shift = post_shift
|
|
self.shift_scale = shift_scale
|
|
|
|
def __call__(
|
|
self,
|
|
n: int,
|
|
do_append_zero: bool = True,
|
|
device: str = "cpu",
|
|
flip: bool = False,
|
|
return_idx: bool = False,
|
|
):
|
|
from functools import partial
|
|
|
|
if n < self.num_timesteps:
|
|
timesteps = np.linspace(self.num_timesteps - 1, 0, n, endpoint=False).astype(int)[::-1]
|
|
alphas_cumprod = self.alphas_cumprod[timesteps]
|
|
elif n == self.num_timesteps:
|
|
alphas_cumprod = self.alphas_cumprod
|
|
else:
|
|
raise ValueError(f"n={n} > num_timesteps={self.num_timesteps}")
|
|
|
|
to_torch = partial(torch.tensor, dtype=torch.float32, device=device)
|
|
alphas_cumprod = to_torch(alphas_cumprod)
|
|
alphas_cumprod_sqrt = alphas_cumprod.sqrt()
|
|
alphas_cumprod_sqrt_0 = alphas_cumprod_sqrt[0].clone()
|
|
alphas_cumprod_sqrt_T = alphas_cumprod_sqrt[-1].clone()
|
|
alphas_cumprod_sqrt -= alphas_cumprod_sqrt_T
|
|
alphas_cumprod_sqrt *= alphas_cumprod_sqrt_0 / (alphas_cumprod_sqrt_0 - alphas_cumprod_sqrt_T)
|
|
|
|
if self.post_shift:
|
|
alphas_cumprod_sqrt = (
|
|
alphas_cumprod_sqrt**2 / (self.shift_scale + (1 - self.shift_scale) * alphas_cumprod_sqrt**2)
|
|
) ** 0.5
|
|
|
|
sigmas = torch.flip(alphas_cumprod_sqrt, (0,))
|
|
sigmas = torch.cat([sigmas, sigmas.new_zeros([1])]) if do_append_zero else sigmas
|
|
if return_idx:
|
|
return sigmas if not flip else torch.flip(sigmas, (0,)), timesteps
|
|
return sigmas if not flip else torch.flip(sigmas, (0,))
|
|
|
|
|
|
def _schedule_latent_step(
|
|
*,
|
|
video_scheduler: FlowUniPCMultistepScheduler,
|
|
audio_scheduler: FlowUniPCMultistepScheduler,
|
|
latent_video: torch.Tensor,
|
|
latent_audio: torch.Tensor,
|
|
t,
|
|
idx: int,
|
|
steps,
|
|
v_cfg_video: torch.Tensor,
|
|
v_cfg_audio: torch.Tensor,
|
|
is_a2v: bool,
|
|
cfg_number: int,
|
|
using_sde_flag: bool,
|
|
use_sr_model: bool = False,
|
|
):
|
|
# Fast DDIM path for cfg_number==1, only used during the BR stage
|
|
if cfg_number == 1 and not use_sr_model:
|
|
latent_video = video_scheduler.step_ddim(v_cfg_video, idx, latent_video)
|
|
latent_audio = audio_scheduler.step_ddim(v_cfg_audio, idx, latent_audio)
|
|
return latent_video, latent_audio
|
|
|
|
if using_sde_flag:
|
|
if use_sr_model:
|
|
# SR stage with SDE: only update video, keep audio unchanged
|
|
latent_video = video_scheduler.step(v_cfg_video, t, latent_video, return_dict=False)[0]
|
|
return latent_video, latent_audio
|
|
if idx < int(len(steps) * (3 / 4)):
|
|
noise_theta = 1.0 if (idx + 1) % 2 == 0 else 0.0
|
|
else:
|
|
noise_theta = 1.0 if idx % 3 == 0 else 0.0
|
|
latent_video = video_scheduler.step_sde(v_cfg_video, idx, latent_video, noise_theta=noise_theta)
|
|
if not is_a2v:
|
|
latent_audio = audio_scheduler.step_sde(v_cfg_audio, idx, latent_audio, noise_theta=noise_theta)
|
|
return latent_video, latent_audio
|
|
|
|
latent_video = video_scheduler.step(v_cfg_video, t, latent_video, return_dict=False)[0]
|
|
# Do not update audio latent during the SR stage
|
|
if not is_a2v and not use_sr_model:
|
|
latent_audio = audio_scheduler.step(v_cfg_audio, t, latent_audio, return_dict=False)[0]
|
|
return latent_video, latent_audio
|
|
|
|
|
|
_NEGATIVE_PROMPT = (
|
|
"Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, "
|
|
"overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, "
|
|
"poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, "
|
|
"still picture, messy background, three legs, many people in the background, walking backwards"
|
|
", low quality, worst quality, poor quality, noise, background noise, hiss, hum, buzz, crackle, static, "
|
|
"compression artifacts, MP3 artifacts, digital clipping, distortion, muffled, muddy, unclear, echo, "
|
|
"reverb, room echo, over-reverberated, hollow sound, distant, washed out, harsh, shrill, piercing, "
|
|
"grating, tinny, thin sound, boomy, bass-heavy, flat EQ, over-compressed, abrupt cut, jarring transition, "
|
|
"sudden silence, looping artifact, music, instrumental, sirens, alarms, crowd noise, unrelated sound "
|
|
"effects, chaotic, disorganized, messy, cheap sound"
|
|
", emotionless, flat delivery, deadpan, lifeless, apathetic, robotic, mechanical, monotone, flat "
|
|
"intonation, undynamic, boring, reading from a script, AI voice, synthetic, text-to-speech, TTS, "
|
|
"insincere, fake emotion, exaggerated, overly dramatic, melodramatic, cheesy, cringey, hesitant, "
|
|
"unconfident, tired, weak voice, stuttering, stammering, mumbling, slurred speech, mispronounced, "
|
|
"bad articulation, lisp, vocal fry, creaky voice, mouth clicks, lip smacks, wet mouth sounds, heavy "
|
|
"breathing, audible inhales, plosives, p-pops, coughing, clearing throat, sneezing, speaking too fast, "
|
|
"rushed, speaking too slow, dragged out, unnatural pauses, awkward silence, choppy, disjointed, multiple "
|
|
"speakers, two voices, background talking, out of tune, off-key, autotune artifacts"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# Pre/post process funcs (registered in registry)
|
|
# ===========================================================================
|
|
def get_magi_human_pre_process_func(*args, **kwargs):
|
|
def pre_process(request: OmniDiffusionRequest):
|
|
return request
|
|
|
|
return pre_process
|
|
|
|
|
|
def get_magi_human_post_process_func(*args, **kwargs):
|
|
def post_process(output):
|
|
if isinstance(output, tuple) and len(output) == 2:
|
|
video, audio = output
|
|
return {
|
|
"video": video,
|
|
"audio": audio,
|
|
"audio_sample_rate": 44100,
|
|
"fps": 25,
|
|
}
|
|
return output
|
|
|
|
return post_process
|
|
|
|
|
|
# ===========================================================================
|
|
# HF Hub / local path helpers
|
|
# ===========================================================================
|
|
|
|
|
|
def _load_json(model_path: str, filename: str, local_files_only: bool = True) -> dict:
|
|
"""Load a JSON config file from a local path or HuggingFace Hub repo."""
|
|
if local_files_only:
|
|
path = os.path.join(model_path, *filename.split("/"))
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
else:
|
|
from huggingface_hub import hf_hub_download
|
|
|
|
cached = hf_hub_download(repo_id=model_path, filename=filename)
|
|
with open(cached) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _resolve_subdir(
|
|
model_path: str,
|
|
subfolder: str,
|
|
local_files_only: bool = True,
|
|
required_files: list[str] | None = None,
|
|
) -> str:
|
|
"""Resolve a model subfolder to a local directory path.
|
|
|
|
For HF Hub repos, downloads all ``required_files`` (default: ``["config.json"]``)
|
|
into the HF cache and returns the parent directory.
|
|
"""
|
|
if local_files_only:
|
|
return os.path.join(model_path, subfolder)
|
|
from huggingface_hub import hf_hub_download
|
|
|
|
files = required_files or ["config.json"]
|
|
last_cached: str | None = None
|
|
for fname in files:
|
|
last_cached = hf_hub_download(repo_id=model_path, filename=f"{subfolder}/{fname}")
|
|
return os.path.dirname(last_cached)
|
|
|
|
|
|
# ===========================================================================
|
|
# Main Pipeline
|
|
# ===========================================================================
|
|
class MagiHumanPipeline(nn.Module, ProgressBarMixin, DiffusionPipelineProfilerMixin):
|
|
def __init__(self, od_config: OmniDiffusionConfig, **kwargs):
|
|
super().__init__()
|
|
model_path = od_config.model
|
|
local_files_only = os.path.exists(model_path)
|
|
device = f"cuda:{torch.accelerator.current_device_index()}"
|
|
self.device_str = device
|
|
self.dtype = od_config.dtype or torch.bfloat16
|
|
|
|
model_index = _load_json(model_path, "model_index.json", local_files_only)
|
|
eval_cfg = model_index
|
|
dp_cfg = model_index.get("data_proxy", {})
|
|
|
|
dit_subfolder = "transformer"
|
|
|
|
dit_json = _load_json(model_path, f"{dit_subfolder}/config.json", local_files_only)
|
|
dit_model_config = MagiHumanDiTConfig(**dit_json)
|
|
|
|
self.dit = DiTModel(dit_model_config)
|
|
self.dit.eval()
|
|
|
|
self.vae = DistributedAutoencoderKLWan.from_pretrained(model_path, subfolder="vae")
|
|
self.vae.to(device)
|
|
self.vae.eval()
|
|
vae_cfg = _load_json(model_path, "vae/config.json", local_files_only)
|
|
self.vae_latent_mean = torch.tensor(vae_cfg["latents_mean"], dtype=torch.float32)
|
|
self.vae_latent_std = torch.tensor(vae_cfg["latents_std"], dtype=torch.float32)
|
|
|
|
self.audio_vae = SAAudioFeatureExtractor(
|
|
device=device,
|
|
model_path=_resolve_subdir(
|
|
model_path,
|
|
"audio_vae",
|
|
local_files_only,
|
|
required_files=["config.json", "model_config.json", "model.safetensors"],
|
|
),
|
|
)
|
|
|
|
logger.info("Loading T5Gemma text encoder from %s (subfolder=text_encoder)", model_path)
|
|
if local_files_only:
|
|
txt_enc_path = os.path.join(model_path, "text_encoder")
|
|
txt_enc_subfolder = None
|
|
else:
|
|
txt_enc_path = model_path
|
|
txt_enc_subfolder = "text_encoder"
|
|
self.text_encoder = _T5GemmaEncoder(
|
|
model_path=txt_enc_path,
|
|
device=device,
|
|
weight_dtype=self.dtype,
|
|
subfolder=txt_enc_subfolder,
|
|
)
|
|
|
|
self.data_proxy = MagiDataProxy(
|
|
patch_size=dp_cfg.get("patch_size", 2),
|
|
t_patch_size=dp_cfg.get("t_patch_size", 1),
|
|
frame_receptive_field=dp_cfg.get("frame_receptive_field", 11),
|
|
spatial_rope_interpolation=dp_cfg.get("spatial_rope_interpolation", "extra"),
|
|
ref_audio_offset=dp_cfg.get("ref_audio_offset", 1000),
|
|
text_offset=dp_cfg.get("text_offset", 0),
|
|
coords_style=dp_cfg.get("coords_style", "v2"),
|
|
)
|
|
# SR DataProxy forces v1 coordinate style (consistent with the original)
|
|
self.sr_data_proxy = MagiDataProxy(
|
|
patch_size=dp_cfg.get("patch_size", 2),
|
|
t_patch_size=dp_cfg.get("t_patch_size", 1),
|
|
frame_receptive_field=dp_cfg.get("frame_receptive_field", 11),
|
|
spatial_rope_interpolation=dp_cfg.get("spatial_rope_interpolation", "extra"),
|
|
ref_audio_offset=dp_cfg.get("ref_audio_offset", 1000),
|
|
text_offset=dp_cfg.get("text_offset", 0),
|
|
coords_style="v1",
|
|
)
|
|
|
|
self.fps = eval_cfg.get("fps", 25)
|
|
self.num_inference_steps_default = eval_cfg.get("num_inference_steps", 32)
|
|
self.video_txt_guidance_scale = eval_cfg.get("video_txt_guidance_scale", 5.0)
|
|
self.audio_txt_guidance_scale = eval_cfg.get("audio_txt_guidance_scale", 5.0)
|
|
self.shift = eval_cfg.get("shift", 5.0)
|
|
self.cfg_number = eval_cfg.get("cfg_number", 2)
|
|
self.use_cfg_trick = eval_cfg.get("use_cfg_trick", True)
|
|
self.cfg_trick_start_frame = eval_cfg.get("cfg_trick_start_frame", 13)
|
|
self.cfg_trick_value = eval_cfg.get("cfg_trick_value", 2.0)
|
|
self.using_sde_flag = eval_cfg.get("using_sde_flag", False)
|
|
self.t5_gemma_target_length = eval_cfg.get("t5_gemma_target_length", 640)
|
|
self.vae_stride = eval_cfg.get("vae_stride", [4, 16, 16])
|
|
self.z_dim = eval_cfg.get("z_dim", 48)
|
|
self.patch_size = eval_cfg.get("patch_size", [1, 2, 2])
|
|
# SR-specific hyperparameters
|
|
self.sr_num_inference_steps_default = eval_cfg.get("sr_num_inference_steps", 5)
|
|
self.sr_cfg_number = eval_cfg.get("sr_cfg_number", 2)
|
|
self.sr_video_txt_guidance_scale = eval_cfg.get("sr_video_txt_guidance_scale", 3.5)
|
|
self.noise_value = eval_cfg.get("noise_value", 220)
|
|
self.sr_audio_noise_scale = eval_cfg.get("sr_audio_noise_scale", 0.7)
|
|
# ZeroSNR sigma schedule for SR noise injection (flip=True, high to low)
|
|
self.zerosnr_sigmas = ZeroSNRDDPMDiscretization()(1000, do_append_zero=False, flip=True)
|
|
|
|
self.context_null, self.original_context_null_len = _get_padded_t5_gemma_embedding(
|
|
_NEGATIVE_PROMPT,
|
|
self.text_encoder,
|
|
self.t5_gemma_target_length,
|
|
)
|
|
self.video_processor = VideoProcessor(vae_scale_factor=16)
|
|
|
|
# SR DiT model (loaded from the sr/ subdirectory)
|
|
sr_dit_subfolder = "sr"
|
|
sr_dit_json = _load_json(model_path, f"{sr_dit_subfolder}/config.json", local_files_only)
|
|
sr_dit_model_config = MagiHumanDiTConfig(**sr_dit_json)
|
|
self.sr_dit = DiTModel(sr_dit_model_config)
|
|
self.sr_dit.eval()
|
|
|
|
self.weights_sources = [
|
|
DiffusersPipelineLoader.ComponentSource(
|
|
model_or_path=model_path,
|
|
subfolder=dit_subfolder,
|
|
revision=None,
|
|
prefix="dit.",
|
|
fall_back_to_pt=True,
|
|
),
|
|
DiffusersPipelineLoader.ComponentSource(
|
|
model_or_path=model_path,
|
|
subfolder=sr_dit_subfolder,
|
|
revision=None,
|
|
prefix="sr_dit.",
|
|
fall_back_to_pt=True,
|
|
),
|
|
]
|
|
if getattr(self.text_encoder, "is_tp", False):
|
|
self.weights_sources.append(
|
|
DiffusersPipelineLoader.ComponentSource(
|
|
model_or_path=model_path,
|
|
subfolder="text_encoder",
|
|
revision=None,
|
|
prefix="text_encoder.",
|
|
fall_back_to_pt=True,
|
|
),
|
|
)
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
|
# Weight loading for MagiHuman DiT with TP support.
|
|
#
|
|
# The checkpoint stores weights with these naming patterns:
|
|
# - attention.linear_qkv.weight: fused [Q, K, V, G] for shared layers,
|
|
# or stacked per-expert [expert0_Q|K|V|G, expert1_..., expert2_...] for MoE.
|
|
# - attention.linear_proj.weight: single for shared, stacked per-expert for MoE.
|
|
# - mlp.up_gate_proj.weight / mlp.down_proj.weight: similarly stacked for MoE.
|
|
#
|
|
# The model now uses per-expert vLLM parallel layers for MoE blocks:
|
|
# attention.linear_qkv.experts.{i}.weight (QKVParallelLinear per expert)
|
|
# attention.linear_gating.experts.{i}.weight (ColumnParallelLinear per expert)
|
|
# attention.linear_proj.experts.{i}.weight (RowParallelLinear per expert)
|
|
# mlp.up_gate_proj.experts.{i}.weight (ColumnParallelLinear per expert)
|
|
# mlp.down_proj.experts.{i}.weight (RowParallelLinear per expert)
|
|
#
|
|
# Shared layers keep the same naming (no .experts.).
|
|
params_dict = dict(self.named_parameters())
|
|
modules_dict = dict(self.named_modules())
|
|
loaded_params: set[str] = set()
|
|
|
|
for name, loaded_weight in weights:
|
|
# ── Text Encoder weights ──
|
|
if name.startswith("text_encoder."):
|
|
if getattr(self.text_encoder, "is_tp", False):
|
|
# Strip "text_encoder." prefix for the T5Gemma TP model
|
|
# The T5GemmaEncoderModelTP load_weights handles the "encoder." prefix itself
|
|
sub_name = name[len("text_encoder.") :]
|
|
loaded_params.update(
|
|
f"text_encoder.{k}" for k in self.text_encoder.model.load_weights([(sub_name, loaded_weight)])
|
|
)
|
|
else:
|
|
loaded_params.add(name)
|
|
continue
|
|
|
|
# ── Shared attention QKV + Gating split ──
|
|
# Checkpoint: attention.linear_qkv.weight = [Q, K, V, G] fused.
|
|
# Model: attention.linear_qkv.weight (QKVParallelLinear) + attention.linear_gating.weight.
|
|
if "attention.linear_qkv.weight" in name:
|
|
gating_name = name.replace("attention.linear_qkv.weight", "attention.linear_gating.weight")
|
|
# Check if this is a shared layer (direct param exists, no .experts.)
|
|
if name in params_dict and gating_name in params_dict:
|
|
qkv_param = params_dict[name]
|
|
gating_param = params_dict[gating_name]
|
|
|
|
mod_path = name[: -len(".weight")]
|
|
qkv_mod = modules_dict.get(mod_path)
|
|
if qkv_mod is not None and hasattr(qkv_mod, "total_num_heads"):
|
|
total_heads_q = qkv_mod.total_num_heads
|
|
total_heads_kv = qkv_mod.total_num_kv_heads
|
|
head_dim = qkv_mod.head_size
|
|
else:
|
|
head_dim = 128
|
|
tp_size = get_tensor_model_parallel_world_size()
|
|
total_heads_q = gating_param.data.shape[0] * tp_size
|
|
total_heads_kv = (loaded_weight.shape[0] - total_heads_q * head_dim - total_heads_q) // (
|
|
2 * head_dim
|
|
)
|
|
|
|
q_size = total_heads_q * head_dim
|
|
kv_size = total_heads_kv * head_dim
|
|
|
|
q_w = loaded_weight[:q_size]
|
|
k_w = loaded_weight[q_size : q_size + kv_size]
|
|
v_w = loaded_weight[q_size + kv_size : q_size + 2 * kv_size]
|
|
g_w = loaded_weight[q_size + 2 * kv_size :]
|
|
|
|
qkv_loader = getattr(qkv_param, "weight_loader", default_weight_loader)
|
|
qkv_loader(qkv_param, q_w, "q")
|
|
qkv_loader(qkv_param, k_w, "k")
|
|
qkv_loader(qkv_param, v_w, "v")
|
|
|
|
gating_loader = getattr(gating_param, "weight_loader", default_weight_loader)
|
|
gating_loader(gating_param, g_w)
|
|
|
|
loaded_params.add(name)
|
|
loaded_params.add(gating_name)
|
|
continue
|
|
|
|
# ── MoE attention QKV + Gating split ──
|
|
# Checkpoint: attention.linear_qkv.weight = stacked [expert0_QKVG, expert1_QKVG, ...].
|
|
# Model: attention.linear_qkv.experts.{i}.weight (QKVParallelLinear per expert)
|
|
# + attention.linear_gating.experts.{i}.weight (ColumnParallelLinear per expert).
|
|
expert0_name = name.replace("attention.linear_qkv.weight", "attention.linear_qkv.experts.0.weight")
|
|
if expert0_name in params_dict:
|
|
# Determine num_experts by checking which expert indices exist.
|
|
moe_qkv_mod_path = name[: -len(".weight")]
|
|
moe_qkv_mod = modules_dict.get(moe_qkv_mod_path)
|
|
num_experts = moe_qkv_mod.num_experts if moe_qkv_mod is not None else 3
|
|
|
|
# Get head info from the first expert's QKVParallelLinear.
|
|
expert0_mod_path = name.replace("attention.linear_qkv.weight", "attention.linear_qkv.experts.0")
|
|
expert0_mod = modules_dict.get(expert0_mod_path)
|
|
if expert0_mod is not None and hasattr(expert0_mod, "total_num_heads"):
|
|
total_heads_q = expert0_mod.total_num_heads
|
|
total_heads_kv = expert0_mod.total_num_kv_heads
|
|
head_dim = expert0_mod.head_size
|
|
else:
|
|
head_dim = 128
|
|
# Infer from checkpoint weight shape.
|
|
# We'll get exact sizes from model config below.
|
|
total_heads_q = 40 # fallback for default config
|
|
total_heads_kv = 8
|
|
|
|
q_size = total_heads_q * head_dim
|
|
kv_size = total_heads_kv * head_dim
|
|
# Check if gating is present.
|
|
gating_expert0_name = name.replace(
|
|
"attention.linear_qkv.weight", "attention.linear_gating.experts.0.weight"
|
|
)
|
|
has_gating = gating_expert0_name in params_dict
|
|
|
|
# Split stacked checkpoint weight into per-expert chunks.
|
|
expert_weights = loaded_weight.chunk(num_experts, dim=0)
|
|
|
|
for i in range(num_experts):
|
|
expert_w = expert_weights[i]
|
|
# Each expert chunk: [Q, K, V, G (optional)].
|
|
q_w = expert_w[:q_size]
|
|
k_w = expert_w[q_size : q_size + kv_size]
|
|
v_w = expert_w[q_size + kv_size : q_size + 2 * kv_size]
|
|
|
|
expert_param_name = name.replace(
|
|
"attention.linear_qkv.weight",
|
|
f"attention.linear_qkv.experts.{i}.weight",
|
|
)
|
|
expert_param = params_dict[expert_param_name]
|
|
expert_loader = getattr(expert_param, "weight_loader", default_weight_loader)
|
|
expert_loader(expert_param, q_w, "q")
|
|
expert_loader(expert_param, k_w, "k")
|
|
expert_loader(expert_param, v_w, "v")
|
|
loaded_params.add(expert_param_name)
|
|
|
|
if has_gating:
|
|
g_w = expert_w[q_size + 2 * kv_size :]
|
|
gating_param_name = name.replace(
|
|
"attention.linear_qkv.weight",
|
|
f"attention.linear_gating.experts.{i}.weight",
|
|
)
|
|
gating_param = params_dict[gating_param_name]
|
|
gating_loader = getattr(gating_param, "weight_loader", default_weight_loader)
|
|
gating_loader(gating_param, g_w)
|
|
loaded_params.add(gating_param_name)
|
|
continue
|
|
|
|
# ── MoE stacked weight splitting for proj / MLP layers ──
|
|
# Checkpoint: x.y.weight (stacked [expert0, expert1, ...]).
|
|
# Model: x.y.experts.{i}.weight.
|
|
if name not in params_dict:
|
|
# Check if this is a stacked MoE weight by looking for .experts.0.
|
|
base, _, suffix = name.rpartition(".")
|
|
expert0_name = f"{base}.experts.0.{suffix}" if base else None
|
|
if expert0_name and expert0_name in params_dict:
|
|
# Determine num_experts.
|
|
moe_mod = modules_dict.get(base)
|
|
num_experts = getattr(moe_mod, "num_experts", 3) if moe_mod is not None else 3
|
|
|
|
# Split stacked weight into per-expert chunks.
|
|
expert_weights = loaded_weight.chunk(num_experts, dim=0)
|
|
for i in range(num_experts):
|
|
expert_param_name = f"{base}.experts.{i}.{suffix}"
|
|
if expert_param_name not in params_dict:
|
|
continue
|
|
expert_param = params_dict[expert_param_name]
|
|
expert_loader = getattr(expert_param, "weight_loader", default_weight_loader)
|
|
expert_loader(expert_param, expert_weights[i])
|
|
loaded_params.add(expert_param_name)
|
|
continue
|
|
# Truly unknown weight — skip.
|
|
continue
|
|
|
|
# ── Standard weight loading (shared layers + non-MoE params) ──
|
|
param = params_dict[name]
|
|
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
|
weight_loader(param, loaded_weight)
|
|
loaded_params.add(name)
|
|
|
|
if getattr(self.text_encoder, "is_tp", False):
|
|
self.context_null, self.original_context_null_len = _get_padded_t5_gemma_embedding(
|
|
_NEGATIVE_PROMPT,
|
|
self.text_encoder,
|
|
self.t5_gemma_target_length,
|
|
)
|
|
|
|
return loaded_params
|
|
|
|
def _dit_forward(self, eval_input: EvalInput) -> tuple[torch.Tensor, torch.Tensor]:
|
|
packed = self.data_proxy.process_input(eval_input)
|
|
noise_pred = self.dit(*packed)
|
|
return self.data_proxy.process_output(noise_pred)
|
|
|
|
def _sr_dit_forward(self, eval_input: EvalInput) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""SR stage uses sr_data_proxy (coords_style=v1) and sr_dit model."""
|
|
packed = self.sr_data_proxy.process_input(eval_input)
|
|
noise_pred = self.sr_dit(*packed)
|
|
return self.sr_data_proxy.process_output(noise_pred)
|
|
|
|
@torch.inference_mode()
|
|
def _evaluate_with_latent(
|
|
self,
|
|
context: torch.Tensor,
|
|
original_context_len: int,
|
|
latent_image: torch.Tensor | None,
|
|
latent_video: torch.Tensor,
|
|
latent_audio: torch.Tensor,
|
|
num_inference_steps: int,
|
|
is_a2v: bool = False,
|
|
use_sr_model: bool = False,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
# Select cfg_number and guidance_scale based on BR/SR stage
|
|
cfg_number = self.sr_cfg_number if use_sr_model else self.cfg_number
|
|
video_guidance = self.sr_video_txt_guidance_scale if use_sr_model else self.video_txt_guidance_scale
|
|
forward_fn = self._sr_dit_forward if use_sr_model else self._dit_forward
|
|
|
|
video_scheduler = FlowUniPCMultistepScheduler()
|
|
audio_scheduler = FlowUniPCMultistepScheduler()
|
|
video_scheduler.set_timesteps(num_inference_steps, device=self.device_str, shift=self.shift)
|
|
audio_scheduler.set_timesteps(num_inference_steps, device=self.device_str, shift=self.shift)
|
|
timesteps = video_scheduler.timesteps
|
|
|
|
latent_length = latent_video.shape[2]
|
|
cfg_trick_guidance = (
|
|
torch.tensor(video_guidance, device=self.device_str).expand(1, 1, latent_length, 1, 1).clone()
|
|
)
|
|
if self.use_cfg_trick:
|
|
cfg_trick_guidance[:, :, : self.cfg_trick_start_frame] = min(self.cfg_trick_value, video_guidance)
|
|
|
|
with self.progress_bar(total=len(timesteps)) as pbar:
|
|
for idx, t in enumerate(timesteps):
|
|
if latent_image is not None:
|
|
latent_video[:, :, :1] = latent_image[:, :, :1]
|
|
|
|
# Reduce guidance when t<=500 during BR stage (original behavior)
|
|
cur_video_guidance = video_guidance if (use_sr_model or t > 500) else 2.0
|
|
|
|
eval_input_cond = EvalInput(
|
|
x_t=latent_video,
|
|
audio_x_t=latent_audio,
|
|
audio_feat_len=[latent_audio.shape[1]],
|
|
txt_feat=context,
|
|
txt_feat_len=[original_context_len],
|
|
)
|
|
|
|
v_cond_video, v_cond_audio = forward_fn(eval_input_cond)
|
|
|
|
if cfg_number == 1:
|
|
v_cfg_video = v_cond_video
|
|
v_cfg_audio = v_cond_audio
|
|
elif cfg_number == 2:
|
|
eval_input_uncond = EvalInput(
|
|
x_t=latent_video,
|
|
audio_x_t=latent_audio,
|
|
audio_feat_len=[latent_audio.shape[1]],
|
|
txt_feat=self.context_null,
|
|
txt_feat_len=[self.original_context_null_len],
|
|
)
|
|
v_uncond_video, v_uncond_audio = forward_fn(eval_input_uncond)
|
|
v_cfg_video = v_uncond_video + cur_video_guidance * (v_cond_video - v_uncond_video)
|
|
v_cfg_audio = v_uncond_audio + self.audio_txt_guidance_scale * (v_cond_audio - v_uncond_audio)
|
|
else:
|
|
raise ValueError(f"Invalid cfg_number: {cfg_number}")
|
|
|
|
latent_video, latent_audio = _schedule_latent_step(
|
|
video_scheduler=video_scheduler,
|
|
audio_scheduler=audio_scheduler,
|
|
latent_video=latent_video,
|
|
latent_audio=latent_audio,
|
|
t=t,
|
|
idx=idx,
|
|
steps=timesteps,
|
|
v_cfg_video=v_cfg_video,
|
|
v_cfg_audio=v_cfg_audio,
|
|
is_a2v=is_a2v,
|
|
cfg_number=cfg_number,
|
|
using_sde_flag=self.using_sde_flag,
|
|
use_sr_model=use_sr_model,
|
|
)
|
|
|
|
pbar.update()
|
|
|
|
if latent_image is not None:
|
|
latent_video[:, :, :1] = latent_image[:, :, :1]
|
|
return latent_video, latent_audio
|
|
|
|
def _encode_image(self, image: Image.Image, height: int, width: int) -> torch.Tensor:
|
|
image = load_image(image)
|
|
image = _resizecrop(image, height, width)
|
|
image = self.video_processor.preprocess(image, height=height, width=width)
|
|
image = image.to(device=self.device_str, dtype=self.dtype).unsqueeze(2)
|
|
vae_out = self.vae.encode(image)
|
|
if hasattr(vae_out, "latent_dist"):
|
|
return vae_out.latent_dist.mode().to(torch.float32)
|
|
return vae_out.to(torch.float32)
|
|
|
|
def _decode_video(self, latent: torch.Tensor) -> list[np.ndarray]:
|
|
mean = self.vae_latent_mean.to(latent.device, dtype=latent.dtype).view(1, -1, 1, 1, 1)
|
|
std = self.vae_latent_std.to(latent.device, dtype=latent.dtype).view(1, -1, 1, 1, 1)
|
|
latent = latent * std + mean
|
|
|
|
videos = self.vae.decode(latent.to(self.dtype))
|
|
if hasattr(videos, "sample"):
|
|
videos = videos.sample
|
|
videos.mul_(0.5).add_(0.5).clamp_(0, 1)
|
|
videos = [v.float().cpu().permute(1, 2, 3, 0) * 255 for v in videos]
|
|
return [v.numpy().astype(np.uint8) for v in videos]
|
|
|
|
def _decode_audio(self, latent_audio: torch.Tensor) -> np.ndarray:
|
|
latent_audio = latent_audio.squeeze(0).to(self.dtype)
|
|
audio_output = self.audio_vae.decode(latent_audio.T)
|
|
audio_np = audio_output.squeeze(0).T.float().cpu().numpy()
|
|
target_len = int(audio_np.shape[0] * 441 / 512)
|
|
from scipy.signal import resample
|
|
|
|
return resample(audio_np, target_len)
|
|
|
|
def encode_prompt(
|
|
self,
|
|
prompt: str,
|
|
target_length: int | None = None,
|
|
) -> tuple[torch.Tensor, int]:
|
|
"""Encode *prompt* with the T5-Gemma text encoder and pad to fixed length.
|
|
|
|
This is the single text-encoder entrypoint so the runner-level
|
|
prompt-embedding cache (see
|
|
``vllm_omni/diffusion/cache/prompt_embed_cache.py``) can transparently
|
|
memoize results when the same prompt is submitted repeatedly.
|
|
|
|
Returns:
|
|
``(context, original_context_len)`` matching
|
|
:func:`_get_padded_t5_gemma_embedding`.
|
|
"""
|
|
return _get_padded_t5_gemma_embedding(
|
|
prompt,
|
|
self.text_encoder,
|
|
target_length if target_length is not None else self.t5_gemma_target_length,
|
|
)
|
|
|
|
@torch.inference_mode()
|
|
def forward(
|
|
self,
|
|
req: OmniDiffusionRequest,
|
|
prompt: str | None = None,
|
|
height: int = 256,
|
|
width: int = 448,
|
|
num_inference_steps: int | None = None,
|
|
seconds: int = 10,
|
|
seed: int | None = None,
|
|
image_path: str | None = None,
|
|
audio_path: str | None = None,
|
|
**kwargs,
|
|
) -> DiffusionOutput:
|
|
if len(req.prompts) >= 1:
|
|
p = req.prompts[0]
|
|
prompt = p if isinstance(p, str) else p.get("prompt", prompt)
|
|
if not isinstance(p, str):
|
|
image_path = p.get("image_path", image_path)
|
|
audio_path = p.get("audio_path", audio_path)
|
|
if prompt is None:
|
|
raise ValueError("prompt is required")
|
|
|
|
height = req.sampling_params.height or height
|
|
width = req.sampling_params.width or width
|
|
seed = req.sampling_params.seed if req.sampling_params.seed is not None else seed
|
|
num_steps = req.sampling_params.num_inference_steps or num_inference_steps or self.num_inference_steps_default
|
|
sr_height: int | None = None
|
|
sr_width: int | None = None
|
|
sr_num_steps: int | None = None
|
|
if hasattr(req.sampling_params, "extra_args") and req.sampling_params.extra_args:
|
|
seconds = req.sampling_params.extra_args.get("seconds", seconds)
|
|
audio_path = req.sampling_params.extra_args.get("audio_path", audio_path)
|
|
image_path = req.sampling_params.extra_args.get("image_path", image_path)
|
|
sr_height = req.sampling_params.extra_args.get("sr_height", None)
|
|
sr_width = req.sampling_params.extra_args.get("sr_width", None)
|
|
sr_num_steps = req.sampling_params.extra_args.get("sr_num_inference_steps", None)
|
|
|
|
device = self.device_str
|
|
|
|
br_latent_height = height // self.vae_stride[1] // self.patch_size[1] * self.patch_size[1]
|
|
br_latent_width = width // self.vae_stride[2] // self.patch_size[2] * self.patch_size[2]
|
|
br_height = br_latent_height * self.vae_stride[1]
|
|
br_width = br_latent_width * self.vae_stride[2]
|
|
|
|
if seed is not None:
|
|
torch.manual_seed(seed)
|
|
torch.cuda.manual_seed_all(seed)
|
|
|
|
if audio_path is not None:
|
|
latent_audio = load_audio_and_encode(self.audio_vae, audio_path, seconds)
|
|
latent_audio = latent_audio.permute(0, 2, 1)
|
|
num_frames = latent_audio.shape[1]
|
|
is_a2v = True
|
|
else:
|
|
num_frames = seconds * self.fps + 1
|
|
latent_audio = torch.randn(1, num_frames, 64, dtype=torch.float32, device=device)
|
|
is_a2v = False
|
|
|
|
latent_length = (num_frames - 1) // 4 + 1
|
|
latent_video = torch.randn(
|
|
1,
|
|
self.z_dim,
|
|
latent_length,
|
|
br_latent_height,
|
|
br_latent_width,
|
|
dtype=torch.float32,
|
|
device=device,
|
|
)
|
|
|
|
context, original_context_len = self.encode_prompt(prompt)
|
|
|
|
if image_path is not None:
|
|
br_image = self._encode_image(load_image(image_path), br_height, br_width)
|
|
else:
|
|
br_image = None
|
|
|
|
# ── BR stage ─────────────────────────────────────────────────────────
|
|
br_latent_video, br_latent_audio = self._evaluate_with_latent(
|
|
context,
|
|
original_context_len,
|
|
br_image,
|
|
latent_video.clone(),
|
|
latent_audio.clone(),
|
|
num_steps,
|
|
is_a2v,
|
|
use_sr_model=False,
|
|
)
|
|
|
|
# ── SR stage (optional, triggered when sr_height/sr_width are provided) ──
|
|
if sr_height is not None and sr_width is not None:
|
|
sr_latent_height = sr_height // self.vae_stride[1] // self.patch_size[1] * self.patch_size[1]
|
|
sr_latent_width = sr_width // self.vae_stride[2] // self.patch_size[2] * self.patch_size[2]
|
|
sr_height = sr_latent_height * self.vae_stride[1]
|
|
sr_width = sr_latent_width * self.vae_stride[2]
|
|
|
|
# Image condition (at SR resolution)
|
|
if image_path is not None:
|
|
sr_image = self._encode_image(load_image(image_path), sr_height, sr_width)
|
|
else:
|
|
sr_image = None
|
|
|
|
# Trilinear interpolation of BR latent to SR resolution
|
|
sr_latent_video = torch.nn.functional.interpolate(
|
|
br_latent_video,
|
|
size=(latent_length, sr_latent_height, sr_latent_width),
|
|
mode="trilinear",
|
|
align_corners=True,
|
|
)
|
|
|
|
# Noise injection: sigma-weighted blend (noise_value indexes the ZeroSNR sigma schedule)
|
|
if self.noise_value != 0:
|
|
noise = torch.randn_like(sr_latent_video)
|
|
sigma = self.zerosnr_sigmas.to(sr_latent_video.device)[self.noise_value]
|
|
sr_latent_video = sr_latent_video * sigma + noise * (1 - sigma**2) ** 0.5
|
|
|
|
# Audio: blend with noise (noised version used during SR inference; final audio keeps BR result)
|
|
sr_latent_audio = torch.randn_like(br_latent_audio) * self.sr_audio_noise_scale + br_latent_audio * (
|
|
1 - self.sr_audio_noise_scale
|
|
)
|
|
|
|
torch.accelerator.empty_cache()
|
|
sr_steps = sr_num_steps or self.sr_num_inference_steps_default
|
|
final_latent_video, _ = self._evaluate_with_latent(
|
|
context,
|
|
original_context_len,
|
|
sr_image,
|
|
sr_latent_video.clone(),
|
|
sr_latent_audio.clone(),
|
|
sr_steps,
|
|
is_a2v,
|
|
use_sr_model=True,
|
|
)
|
|
# SR stage does not update audio; keep the BR result
|
|
final_latent_video = final_latent_video
|
|
final_latent_audio = br_latent_audio
|
|
else:
|
|
final_latent_video = br_latent_video
|
|
final_latent_audio = br_latent_audio
|
|
|
|
torch.accelerator.empty_cache()
|
|
videos_np = self._decode_video(final_latent_video)
|
|
torch.accelerator.empty_cache()
|
|
audio_np = self._decode_audio(final_latent_audio)
|
|
|
|
return DiffusionOutput(output=(videos_np, audio_np))
|