DiffIR2VR / utils /sampler.py
jimmycv07's picture
first commit
1de8821
raw
history blame
19.4 kB
from typing import Optional, Tuple, Dict
import copy
import torch
from torch import nn
import numpy as np
from tqdm import tqdm
from einops import rearrange
import torch.nn.functional as F
from model.gaussian_diffusion import extract_into_tensor
from model.cldm import ControlLDM
from utils.cond_fn import Guidance
from utils.common import sliding_windows, gaussian_weights
import vidtome
from controller.controller import AttentionControl
def pad_to_multiples_of(imgs: torch.Tensor, multiple: int) -> torch.Tensor:
_, _, h, w = imgs.size()
if h % multiple == 0 and w % multiple == 0:
return imgs.clone()
# get_pad = lambda x: (x // multiple + 1) * multiple - x
get_pad = lambda x: (x // multiple + int(x % multiple != 0)) * multiple - x
ph, pw = get_pad(h), get_pad(w)
return F.pad(imgs, pad=(0, pw, 0, ph), mode="constant", value=0)
# https://github.com/openai/guided-diffusion/blob/main/guided_diffusion/respace.py
def space_timesteps(num_timesteps, section_counts):
"""
Create a list of timesteps to use from an original diffusion process,
given the number of timesteps we want to take from equally-sized portions
of the original process.
For example, if there's 300 timesteps and the section counts are [10,15,20]
then the first 100 timesteps are strided to be 10 timesteps, the second 100
are strided to be 15 timesteps, and the final 100 are strided to be 20.
If the stride is a string starting with "ddim", then the fixed striding
from the DDIM paper is used, and only one section is allowed.
:param num_timesteps: the number of diffusion steps in the original
process to divide up.
:param section_counts: either a list of numbers, or a string containing
comma-separated numbers, indicating the step count
per section. As a special case, use "ddimN" where N
is a number of steps to use the striding from the
DDIM paper.
:return: a set of diffusion steps from the original process to use.
"""
if isinstance(section_counts, str):
if section_counts.startswith("ddim"):
desired_count = int(section_counts[len("ddim") :])
for i in range(1, num_timesteps):
if len(range(0, num_timesteps, i)) == desired_count:
return set(range(0, num_timesteps, i))
raise ValueError(
f"cannot create exactly {num_timesteps} steps with an integer stride"
)
section_counts = [int(x) for x in section_counts.split(",")]
size_per = num_timesteps // len(section_counts)
extra = num_timesteps % len(section_counts)
start_idx = 0
all_steps = []
for i, section_count in enumerate(section_counts):
size = size_per + (1 if i < extra else 0)
if size < section_count:
raise ValueError(
f"cannot divide section of {size} steps into {section_count}"
)
if section_count <= 1:
frac_stride = 1
else:
frac_stride = (size - 1) / (section_count - 1)
cur_idx = 0.0
taken_steps = []
for _ in range(section_count):
taken_steps.append(start_idx + round(cur_idx))
cur_idx += frac_stride
all_steps += taken_steps
start_idx += size
return set(all_steps)
class SpacedSampler(nn.Module):
"""
Implementation for spaced sampling schedule proposed in IDDPM. This class is designed
for sampling ControlLDM.
https://arxiv.org/pdf/2102.09672.pdf
"""
def __init__(self, betas: np.ndarray) -> "SpacedSampler":
super().__init__()
self.num_timesteps = len(betas)
self.original_betas = betas
self.original_alphas_cumprod = np.cumprod(1.0 - betas, axis=0)
self.context = {}
def register(self, name: str, value: np.ndarray) -> None:
self.register_buffer(name, torch.tensor(value, dtype=torch.float32))
def make_schedule(self, num_steps: int) -> None:
# calcualte betas for spaced sampling
# https://github.com/openai/guided-diffusion/blob/main/guided_diffusion/respace.py
used_timesteps = space_timesteps(self.num_timesteps, str(num_steps))
betas = []
last_alpha_cumprod = 1.0
for i, alpha_cumprod in enumerate(self.original_alphas_cumprod):
if i in used_timesteps:
# marginal distribution is the same as q(x_{S_t}|x_0)
betas.append(1 - alpha_cumprod / last_alpha_cumprod)
last_alpha_cumprod = alpha_cumprod
assert len(betas) == num_steps
self.timesteps = np.array(sorted(list(used_timesteps)), dtype=np.int32) # e.g. [0, 10, 20, ...]
betas = np.array(betas, dtype=np.float64)
alphas = 1.0 - betas
alphas_cumprod = np.cumprod(alphas, axis=0)
# print(f"sampler sqrt_alphas_cumprod: {np.sqrt(alphas_cumprod)[-1]}")
alphas_cumprod_prev = np.append(1.0, alphas_cumprod[:-1])
sqrt_recip_alphas_cumprod = np.sqrt(1.0 / alphas_cumprod)
sqrt_recipm1_alphas_cumprod = np.sqrt(1.0 / alphas_cumprod - 1)
# calculations for posterior q(x_{t-1} | x_t, x_0)
posterior_variance = (
betas * (1.0 - alphas_cumprod_prev) / (1.0 - alphas_cumprod)
)
# log calculation clipped because the posterior variance is 0 at the
# beginning of the diffusion chain.
posterior_log_variance_clipped = np.log(
np.append(posterior_variance[1], posterior_variance[1:])
)
posterior_mean_coef1 = (
betas * np.sqrt(alphas_cumprod_prev) / (1.0 - alphas_cumprod)
)
posterior_mean_coef2 = (
(1.0 - alphas_cumprod_prev)
* np.sqrt(alphas)
/ (1.0 - alphas_cumprod)
)
self.register("sqrt_recip_alphas_cumprod", sqrt_recip_alphas_cumprod)
self.register("sqrt_recipm1_alphas_cumprod", sqrt_recipm1_alphas_cumprod)
self.register("posterior_variance", posterior_variance)
self.register("posterior_log_variance_clipped", posterior_log_variance_clipped)
self.register("posterior_mean_coef1", posterior_mean_coef1)
self.register("posterior_mean_coef2", posterior_mean_coef2)
def q_posterior_mean_variance(self, x_start: torch.Tensor, x_t: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor]:
"""
Implement the posterior distribution q(x_{t-1}|x_t, x_0).
Args:
x_start (torch.Tensor): The predicted images (NCHW) in timestep `t`.
x_t (torch.Tensor): The sampled intermediate variables (NCHW) of timestep `t`.
t (torch.Tensor): Timestep (N) of `x_t`. `t` serves as an index to get
parameters for each timestep.
Returns:
posterior_mean (torch.Tensor): Mean of the posterior distribution.
posterior_variance (torch.Tensor): Variance of the posterior distribution.
posterior_log_variance_clipped (torch.Tensor): Log variance of the posterior distribution.
"""
posterior_mean = (
extract_into_tensor(self.posterior_mean_coef1, t, x_t.shape) * x_start
+ extract_into_tensor(self.posterior_mean_coef2, t, x_t.shape) * x_t
)
posterior_variance = extract_into_tensor(self.posterior_variance, t, x_t.shape)
posterior_log_variance_clipped = extract_into_tensor(
self.posterior_log_variance_clipped, t, x_t.shape
)
return posterior_mean, posterior_variance, posterior_log_variance_clipped
def _predict_xstart_from_eps(self, x_t: torch.Tensor, t: torch.Tensor, eps: torch.Tensor) -> torch.Tensor:
return (
extract_into_tensor(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t
- extract_into_tensor(self.sqrt_recipm1_alphas_cumprod, t, x_t.shape) * eps
)
def apply_cond_fn(
self,
model: ControlLDM,
pred_x0: torch.Tensor,
t: torch.Tensor,
index: torch.Tensor,
cond_fn: Guidance
) -> torch.Tensor:
t_now = int(t[0].item()) + 1
if not (cond_fn.t_stop < t_now and t_now < cond_fn.t_start):
# stop guidance
self.context["g_apply"] = False
return pred_x0
grad_rescale = 1 / extract_into_tensor(self.posterior_mean_coef1, index, pred_x0.shape)
# apply guidance for multiple times
loss_vals = []
for _ in range(cond_fn.repeat):
# set target and pred for gradient computation
target, pred = None, None
if cond_fn.space == "latent":
target = model.vae_encode(cond_fn.target)
pred = pred_x0
elif cond_fn.space == "rgb":
# We need to backward gradient to x0 in latent space, so it's required
# to trace the computation graph while decoding the latent.
with torch.enable_grad():
target = cond_fn.target
pred_x0_rg = pred_x0.detach().clone().requires_grad_(True)
pred = model.vae_decode(pred_x0_rg)
assert pred.requires_grad
else:
raise NotImplementedError(cond_fn.space)
# compute gradient
delta_pred, loss_val = cond_fn(target, pred, t_now)
loss_vals.append(loss_val)
# update pred_x0 w.r.t gradient
if cond_fn.space == "latent":
delta_pred_x0 = delta_pred
pred_x0 = pred_x0 + delta_pred_x0 * grad_rescale
elif cond_fn.space == "rgb":
pred.backward(delta_pred)
delta_pred_x0 = pred_x0_rg.grad
pred_x0 = pred_x0 + delta_pred_x0 * grad_rescale
else:
raise NotImplementedError(cond_fn.space)
self.context["g_apply"] = True
self.context["g_loss"] = float(np.mean(loss_vals))
return pred_x0
def predict_noise(
self,
model: ControlLDM,
x: torch.Tensor,
t: torch.Tensor,
cond: Dict[str, torch.Tensor],
uncond: Optional[Dict[str, torch.Tensor]],
cfg_scale: float
) -> torch.Tensor:
if uncond is None or cfg_scale == 1.:
model_output = model(x, t, cond)
else:
# apply classifier-free guidance
model_cond = model(x, t, cond)
model_uncond = model(x, t, uncond)
model_output = model_uncond + cfg_scale * (model_cond - model_uncond)
return model_output
@torch.no_grad()
def predict_noise_tiled(
self,
model: ControlLDM,
x: torch.Tensor,
t: torch.Tensor,
cond: Dict[str, torch.Tensor],
uncond: Optional[Dict[str, torch.Tensor]],
cfg_scale: float,
tile_size: int,
tile_stride: int
):
_, _, h, w = x.shape
tiles = tqdm(sliding_windows(h, w, tile_size // 8, tile_stride // 8), unit="tile", leave=False)
eps = torch.zeros_like(x)
count = torch.zeros_like(x, dtype=torch.float32)
weights = gaussian_weights(tile_size // 8, tile_size // 8)[None, None]
weights = torch.tensor(weights, dtype=torch.float32, device=x.device)
for hi, hi_end, wi, wi_end in tiles:
tiles.set_description(f"Process tile ({hi} {hi_end}), ({wi} {wi_end})")
tile_x = x[:, :, hi:hi_end, wi:wi_end]
tile_cond = {
"c_img": cond["c_img"][:, :, hi:hi_end, wi:wi_end],
"c_txt": cond["c_txt"]
}
if uncond:
tile_uncond = {
"c_img": uncond["c_img"][:, :, hi:hi_end, wi:wi_end],
"c_txt": uncond["c_txt"]
}
tile_eps = self.predict_noise(model, tile_x, t, tile_cond, tile_uncond, cfg_scale)
# accumulate noise
eps[:, :, hi:hi_end, wi:wi_end] += tile_eps * weights
count[:, :, hi:hi_end, wi:wi_end] += weights
# average on noise (score)
eps.div_(count)
return eps
@torch.no_grad()
def p_sample(
self,
model: ControlLDM,
x: torch.Tensor,
t: torch.Tensor,
index: torch.Tensor,
cond: Dict[str, torch.Tensor],
uncond: Optional[Dict[str, torch.Tensor]],
cfg_scale: float,
cond_fn: Optional[Guidance],
tiled: bool,
tile_size: int,
tile_stride: int,
controller: Optional[AttentionControl]=None
) -> torch.Tensor:
if tiled:
eps = self.predict_noise_tiled(model, x, t, cond, uncond, cfg_scale, tile_size, tile_stride)
else:
eps = self.predict_noise(model, x, t, cond, uncond, cfg_scale)
pred_x0 = self._predict_xstart_from_eps(x, index, eps)
if cond_fn:
assert not tiled, f"tiled sampling currently doesn't support guidance"
pred_x0 = self.apply_cond_fn(model, pred_x0, t, index, cond_fn)
if controller is not None:
pred_x0 = controller.update_x0(pred_x0)
model_mean, model_variance, _ = self.q_posterior_mean_variance(pred_x0, x, index)
noise = torch.randn_like(x)
nonzero_mask = (
(index != 0).float().view(-1, *([1] * (len(x.shape) - 1)))
)
x_prev = model_mean + nonzero_mask * torch.sqrt(model_variance) * noise
return x_prev
@torch.no_grad()
def sample(
self,
model: ControlLDM,
device: str,
steps: int,
batch_size: int,
x_size: Tuple[int],
cond: Dict[str, torch.Tensor],
uncond: Dict[str, torch.Tensor],
cfg_scale: float,
cond_fn: Optional[Guidance]=None,
tiled: bool=False,
tile_size: int=-1,
tile_stride: int=-1,
x_T: Optional[torch.Tensor]=None,
progress: bool=True,
progress_leave: bool=True,
non_pad_ratio: Tuple[float]=(1, 1),
) -> torch.Tensor:
self.make_schedule(steps)
self.to(device)
if x_T is None:
# TODO: not convert to float32, may trigger an error
img = torch.randn((batch_size, *x_size), device=device)
else:
img = x_T
timesteps = np.flip(self.timesteps) # [1000, 950, 900, ...]
total_steps = len(self.timesteps)
iterator = tqdm(timesteps, total=total_steps, leave=progress_leave, disable=not progress)
if model.controller is not None:
# non_pad_flows = model.controller.step_store["flows"].copy()
# for j, flow in enumerate(model.controller.step_store["flows"]):
# if flow is not None:
# model.controller.step_store["flows"][j] = pad_to_multiples_of(model.controller.step_store["flows"][j], 8)
if not (model.controller.ToMe_period[0]):
vidtome.update_patch(model, controller=model.controller)
# flows=non_pad_flows, \
# flow_confids=model.controller.step_store["flow_confids"].copy(), )
for i, step in enumerate(iterator):
torch.cuda.empty_cache()
if model.controller is not None:
model.controller.set_step(i)
if i == int((total_steps * model.controller.ToMe_period[0])):
print(f"[INFO] activating ToMe @ step {i} ...")
model.activate_vidtome()
vidtome.update_patch(model, controller=model.controller)
# flows=non_pad_flows, \
# flow_confids=model.controller.step_store["flow_confids"].copy(),
# for j, flow in enumerate(model.controller.step_store["flows"]):
# if flow is not None:
# model.controller.step_store["flows"][j] = pad_to_multiples_of(model.controller.step_store["flows"][j], 8)
if i <= int((total_steps * model.controller.ToMe_period[1])) and i >= int((total_steps * model.controller.ToMe_period[0])):
# ratio = model.controller.merge_ratio[0] - (i / total_steps) * (model.controller.merge_ratio[0] - model.controller.merge_ratio[1])
ToMe_start_step = int((total_steps * model.controller.ToMe_period[0]))
ToMe_end_step = int((total_steps * model.controller.ToMe_period[1]))
s = (i - ToMe_start_step) / (ToMe_end_step - ToMe_start_step)
ratio = model.controller.merge_ratio[1] + (np.cos(np.pi / 2 * s)) * (model.controller.merge_ratio[0] - model.controller.merge_ratio[1])
vidtome.update_patch(model, current_step=i,
local_merge_ratio = ratio)
# flows=model.controller.step_store["flows"], occlusion_masks=model.controller.step_store["occ_masks"],
# flow_confids=model.controller.step_store["flow_confids"])
print(f"[INFO] updating merging ratio to {ratio:.3f} @ step {i} s {s:.3f} ...")
ts = torch.full((batch_size,), step, device=device, dtype=torch.long)
index = torch.full_like(ts, fill_value=total_steps - i - 1)
img = self.p_sample(
model, img, ts, index, cond, uncond, cfg_scale, cond_fn,
tiled, tile_size, tile_stride,
controller=model.controller
)
if model.controller is not None:
# model.controller.decoded_imgs.clear()
# for img_ in img:
# sample = model.vae_decode(img_[None])
# sample = (sample + 1) / 2
# # sample = wavelet_reconstruction(sample, clean)
# # sample = F.interpolate(sample, size=self.final_size, mode="bicubic", antialias=True)
# sample = rearrange(sample * 255., "n c h w -> n h w c")
# sample = sample.contiguous().clamp(0, 255).to(torch.uint8).cpu().numpy()
# model.controller.decoded_imgs.append(sample)
# img = model.controller.update_x0(img)
# img = model.controller.merge_x0(img, merge_ratio=1)
# img = model.controller.merge_x0_scores(img, merge_ratio=1)
# img = (img + model.controller.merge_x0(img, merge_ratio=1)) / 2
if i == int((total_steps * model.controller.ToMe_period[1])):
print(f"[INFO] removing ToMe patch @ step {i} ...")
vidtome.remove_patch(model)
if cond_fn and self.context["g_apply"]:
loss_val = self.context["g_loss"]
desc = f"Spaced Sampler With Guidance, Loss: {loss_val:.6f}"
else:
desc = "Spaced Sampler"
iterator.set_description(desc)
# if model.controller is not None:
# merge.visualize_correspondence(img[0][None], img[1][None], ratio=0.05)
# img = img = model.controller.merge_x0_scores(img, merge_ratio=0.5)
return img