diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5168e6155cbfa0b26771e9a1df4d9b76f83441d2 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ + +
+

+

WORLDMEM: Long-term Consistent World Generation with Memory

+

+ Zeqi Xiao1 + Yushi Lan1 + Yifan Zhou1 + Wenqi Ouyang1 + Shuai Yang2 + Yanhong Zeng3 + Xingang Pan1
+ 1S-Lab, Nanyang Technological University,
2Wangxuan Institute of Computer Technology, Peking University,
3Shanghai AI Laboratry +

+

+ +

+ + + + +

+ + +## Installation + +``` +conda create python=3.10 -n worldmem +conda activate worldmem +pip install -r requirements.txt +``` + + +## Quick start + + + +## TODO + +- [x] Release inference models and weight; +- [] Release training pipeline on MineCraft; +- [] Release training data on MineCraft; + + + +## 🔗 Citation + +If you find our work helpful, please cite: + +```bibtex +@inproceedings{ +xiao2025trajectory, +title={Trajectory attention for fine-grained video motion control}, +author={Zeqi Xiao and Wenqi Ouyang and Yifan Zhou and Shuai Yang and Lei Yang and Jianlou Si and Xingang Pan}, +booktitle={The Thirteenth International Conference on Learning Representations}, +year={2025}, +url={https://openreview.net/forum?id=2z1HT5lw5M} +} +``` + +## 👏 Acknowledgements +- [Diffusion Forcing](https://github.com/buoyancy99/diffusion-forcing): Diffusion Forcing provides flexible training and inference strategies for our methods. +- [Minedojo](https://github.com/MineDojo/MineDojo): We collect our minecraft dataset from Minedojo. +- [Open-oasis](https://github.com/etched-ai/open-oasis): Our model architecture is based on Open-oasis. We also use pretrained VAE and DiT weight from it. \ No newline at end of file diff --git a/algorithms/README.md b/algorithms/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dad42ab7d3ee7065f3e2d4d89ff3c1287dd122d3 --- /dev/null +++ b/algorithms/README.md @@ -0,0 +1,21 @@ +# algorithms + +`algorithms` folder is designed to contain implementation of algorithms or models. +Content in `algorithms` can be loosely grouped components (e.g. models) or an algorithm has already has all +components chained together (e.g. Lightning Module, RL algo). +You should create a folder name after your own algorithm or baselines in it. + +Two example can be found in `examples` subfolder. + +The `common` subfolder is designed to contain general purpose classes that's useful for many projects, e.g MLP. + +You should not run any `.py` file from algorithms folder. +Instead, you write unit tests / debug python files in `debug` and launch script in `experiments`. + +You are discouraged from putting visualization utilities in algorithms, as those should go to `utils` in project root. + +Each algorithm class takes in a DictConfig file `cfg` in its `__init__`, which allows you to pass in arguments via configuration file in `configurations/algorithm` or [command line override](https://hydra.cc/docs/tutorials/basic/your_first_app/simple_cli/). + +--- + +This repo is forked from [Boyuan Chen](https://boyuan.space/)'s research template [repo](https://github.com/buoyancy99/research-template). By its MIT license, you must keep the above sentence in `README.md` and the `LICENSE` file to credit the author. diff --git a/algorithms/__init__.py b/algorithms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/algorithms/common/README.md b/algorithms/common/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b689b4ae4c42569b9eaca4540a413ca500ce63ad --- /dev/null +++ b/algorithms/common/README.md @@ -0,0 +1,5 @@ +THis folder contains models / algorithms that are considered general for many algorithms. + +--- + +This repo is forked from [Boyuan Chen](https://boyuan.space/)'s research template [repo](https://github.com/buoyancy99/research-template). By its MIT license, you must keep the above sentence in `README.md` and the `LICENSE` file to credit the author. diff --git a/algorithms/common/__init__.py b/algorithms/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/algorithms/common/base_algo.py b/algorithms/common/base_algo.py new file mode 100644 index 0000000000000000000000000000000000000000..cd46eb877ff794907061a34b934df2f0f4e526be --- /dev/null +++ b/algorithms/common/base_algo.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple, Union + +from omegaconf import DictConfig + + +class BaseAlgo(ABC): + """ + A base class for generic algorithms. + """ + + def __init__(self, cfg: DictConfig): + super().__init__() + self.cfg = cfg + + @abstractmethod + def run(*args: Any, **kwargs: Any) -> Any: + """ + Run the algorithm. + """ + raise NotImplementedError diff --git a/algorithms/common/base_pytorch_algo.py b/algorithms/common/base_pytorch_algo.py new file mode 100644 index 0000000000000000000000000000000000000000..d1a92160e5d00f06ae4818365c03be309b8cbecb --- /dev/null +++ b/algorithms/common/base_pytorch_algo.py @@ -0,0 +1,252 @@ +from abc import ABC, abstractmethod +import warnings +from typing import Any, Union, Sequence, Optional + +from lightning.pytorch.utilities.types import STEP_OUTPUT +from omegaconf import DictConfig +import lightning.pytorch as pl +import torch +import numpy as np +from PIL import Image +import wandb +import einops + + +class BasePytorchAlgo(pl.LightningModule, ABC): + """ + A base class for Pytorch algorithms using Pytorch Lightning. + See https://lightning.ai/docs/pytorch/stable/starter/introduction.html for more details. + """ + + def __init__(self, cfg: DictConfig): + super().__init__() + self.cfg = cfg + self._build_model() + + @abstractmethod + def _build_model(self): + """ + Create all pytorch nn.Modules here. + """ + raise NotImplementedError + + @abstractmethod + def training_step(self, *args: Any, **kwargs: Any) -> STEP_OUTPUT: + r"""Here you compute and return the training loss and some additional metrics for e.g. the progress bar or + logger. + + Args: + batch: The output of your data iterable, normally a :class:`~torch.utils.data.DataLoader`. + batch_idx: The index of this batch. + dataloader_idx: (only if multiple dataloaders used) The index of the dataloader that produced this batch. + + Return: + Any of these options: + - :class:`~torch.Tensor` - The loss tensor + - ``dict`` - A dictionary. Can include any keys, but must include the key ``'loss'``. + - ``None`` - Skip to the next batch. This is only supported for automatic optimization. + This is not supported for multi-GPU, TPU, IPU, or DeepSpeed. + + In this step you'd normally do the forward pass and calculate the loss for a batch. + You can also do fancier things like multiple forward passes or something model specific. + + Example:: + + def training_step(self, batch, batch_idx): + x, y, z = batch + out = self.encoder(x) + loss = self.loss(out, x) + return loss + + To use multiple optimizers, you can switch to 'manual optimization' and control their stepping: + + .. code-block:: python + + def __init__(self): + super().__init__() + self.automatic_optimization = False + + + # Multiple optimizers (e.g.: GANs) + def training_step(self, batch, batch_idx): + opt1, opt2 = self.optimizers() + + # do training_step with encoder + ... + opt1.step() + # do training_step with decoder + ... + opt2.step() + + Note: + When ``accumulate_grad_batches`` > 1, the loss returned here will be automatically + normalized by ``accumulate_grad_batches`` internally. + + """ + return super().training_step(*args, **kwargs) + + def configure_optimizers(self): + """ + Return an optimizer. If you need to use more than one optimizer, refer to pytorch lightning documentation: + https://lightning.ai/docs/pytorch/stable/common/optimization.html + """ + parameters = self.parameters() + return torch.optim.Adam(parameters, lr=self.cfg.lr) + + def log_video( + self, + key: str, + video: Union[np.ndarray, torch.Tensor], + mean: Union[np.ndarray, torch.Tensor, Sequence, float] = None, + std: Union[np.ndarray, torch.Tensor, Sequence, float] = None, + fps: int = 5, + format: str = "mp4", + ): + """ + Log video to wandb. WandbLogger in pytorch lightning does not support video logging yet, so we call wandb directly. + + Args: + video: a numpy array or tensor, either in form (time, channel, height, width) or in the form + (batch, time, channel, height, width). The content must be be in 0-255 if under dtype uint8 + or [0, 1] otherwise. + mean: optional, the mean to unnormalize video tensor, assuming unnormalized data is in [0, 1]. + std: optional, the std to unnormalize video tensor, assuming unnormalized data is in [0, 1]. + key: the name of the video. + fps: the frame rate of the video. + format: the format of the video. Can be either "mp4" or "gif". + """ + + if isinstance(video, torch.Tensor): + video = video.detach().cpu().numpy() + + expand_shape = [1] * (len(video.shape) - 2) + [3, 1, 1] + if std is not None: + if isinstance(std, (float, int)): + std = [std] * 3 + if isinstance(std, torch.Tensor): + std = std.detach().cpu().numpy() + std = np.array(std).reshape(*expand_shape) + video = video * std + if mean is not None: + if isinstance(mean, (float, int)): + mean = [mean] * 3 + if isinstance(mean, torch.Tensor): + mean = mean.detach().cpu().numpy() + mean = np.array(mean).reshape(*expand_shape) + video = video + mean + + if video.dtype != np.uint8: + video = np.clip(video, a_min=0, a_max=1) * 255 + video = video.astype(np.uint8) + + self.logger.experiment.log( + { + key: wandb.Video(video, fps=fps, format=format), + }, + step=self.global_step, + ) + + def log_image( + self, + key: str, + image: Union[np.ndarray, torch.Tensor, Image.Image, Sequence[Image.Image]], + mean: Union[np.ndarray, torch.Tensor, Sequence, float] = None, + std: Union[np.ndarray, torch.Tensor, Sequence, float] = None, + **kwargs: Any, + ): + """ + Log image(s) using WandbLogger. + Args: + key: the name of the video. + image: a single image or a batch of images. If a batch of images, the shape should be (batch, channel, height, width). + mean: optional, the mean to unnormalize image tensor, assuming unnormalized data is in [0, 1]. + std: optional, the std to unnormalize tensor, assuming unnormalized data is in [0, 1]. + kwargs: optional, WandbLogger log_image kwargs, such as captions=xxx. + """ + if isinstance(image, Image.Image): + image = [image] + elif len(image) and not isinstance(image[0], Image.Image): + if isinstance(image, torch.Tensor): + image = image.detach().cpu().numpy() + + if len(image.shape) == 3: + image = image[None] + + if image.shape[1] == 3: + if image.shape[-1] == 3: + warnings.warn(f"Two channels in shape {image.shape} have size 3, assuming channel first.") + image = einops.rearrange(image, "b c h w -> b h w c") + + if std is not None: + if isinstance(std, (float, int)): + std = [std] * 3 + if isinstance(std, torch.Tensor): + std = std.detach().cpu().numpy() + std = np.array(std)[None, None, None] + image = image * std + if mean is not None: + if isinstance(mean, (float, int)): + mean = [mean] * 3 + if isinstance(mean, torch.Tensor): + mean = mean.detach().cpu().numpy() + mean = np.array(mean)[None, None, None] + image = image + mean + + if image.dtype != np.uint8: + image = np.clip(image, a_min=0.0, a_max=1.0) * 255 + image = image.astype(np.uint8) + image = [img for img in image] + + self.logger.log_image(key=key, images=image, **kwargs) + + def log_gradient_stats(self): + """Log gradient statistics such as the mean or std of norm.""" + + with torch.no_grad(): + grad_norms = [] + gpr = [] # gradient-to-parameter ratio + for param in self.parameters(): + if param.grad is not None: + grad_norms.append(torch.norm(param.grad).item()) + gpr.append(torch.norm(param.grad) / torch.norm(param)) + if len(grad_norms) == 0: + return + grad_norms = torch.tensor(grad_norms) + gpr = torch.tensor(gpr) + self.log_dict( + { + "train/grad_norm/min": grad_norms.min(), + "train/grad_norm/max": grad_norms.max(), + "train/grad_norm/std": grad_norms.std(), + "train/grad_norm/mean": grad_norms.mean(), + "train/grad_norm/median": torch.median(grad_norms), + "train/gpr/min": gpr.min(), + "train/gpr/max": gpr.max(), + "train/gpr/std": gpr.std(), + "train/gpr/mean": gpr.mean(), + "train/gpr/median": torch.median(gpr), + } + ) + + def register_data_mean_std( + self, mean: Union[str, float, Sequence], std: Union[str, float, Sequence], namespace: str = "data" + ): + """ + Register mean and std of data as tensor buffer. + + Args: + mean: the mean of data. + std: the std of data. + namespace: the namespace of the registered buffer. + """ + for k, v in [("mean", mean), ("std", std)]: + if isinstance(v, str): + if v.endswith(".npy"): + v = torch.from_numpy(np.load(v)) + elif v.endswith(".pt"): + v = torch.load(v) + else: + raise ValueError(f"Unsupported file type {v.split('.')[-1]}.") + else: + v = torch.tensor(v) + self.register_buffer(f"{namespace}_{k}", v.float().to(self.device)) diff --git a/algorithms/common/metrics/__init__.py b/algorithms/common/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..61c0d28943dd29d7aeb4b1121939b573f7989e9b --- /dev/null +++ b/algorithms/common/metrics/__init__.py @@ -0,0 +1,3 @@ +from .fid import FrechetInceptionDistance +from .lpips import LearnedPerceptualImagePatchSimilarity +from .fvd import FrechetVideoDistance diff --git a/algorithms/common/metrics/fid.py b/algorithms/common/metrics/fid.py new file mode 100644 index 0000000000000000000000000000000000000000..428a621a58807767650101026576335090d10fc0 --- /dev/null +++ b/algorithms/common/metrics/fid.py @@ -0,0 +1 @@ +from torchmetrics.image.fid import FrechetInceptionDistance diff --git a/algorithms/common/metrics/fvd.py b/algorithms/common/metrics/fvd.py new file mode 100644 index 0000000000000000000000000000000000000000..a502055eff0b19ab8724d1d5cbee38ab85a8ee7c --- /dev/null +++ b/algorithms/common/metrics/fvd.py @@ -0,0 +1,158 @@ +""" +Adopted from https://github.com/cvpr2022-stylegan-v/stylegan-v +Verified to be the same as tf version by https://github.com/universome/fvd-comparison +""" + +import io +import re +import requests +import html +import hashlib +import urllib +import urllib.request +from typing import Any, List, Tuple, Union, Dict +import scipy + +import torch +import torch.nn as nn +import numpy as np + + +def open_url( + url: str, + num_attempts: int = 10, + verbose: bool = True, + return_filename: bool = False, +) -> Any: + """Download the given URL and return a binary-mode file object to access the data.""" + assert num_attempts >= 1 + + # Doesn't look like an URL scheme so interpret it as a local filename. + if not re.match("^[a-z]+://", url): + return url if return_filename else open(url, "rb") + + # Handle file URLs. This code handles unusual file:// patterns that + # arise on Windows: + # + # file:///c:/foo.txt + # + # which would translate to a local '/c:/foo.txt' filename that's + # invalid. Drop the forward slash for such pathnames. + # + # If you touch this code path, you should test it on both Linux and + # Windows. + # + # Some internet resources suggest using urllib.request.url2pathname() but + # but that converts forward slashes to backslashes and this causes + # its own set of problems. + if url.startswith("file://"): + filename = urllib.parse.urlparse(url).path + if re.match(r"^/[a-zA-Z]:", filename): + filename = filename[1:] + return filename if return_filename else open(filename, "rb") + + url_md5 = hashlib.md5(url.encode("utf-8")).hexdigest() + + # Download. + url_name = None + url_data = None + with requests.Session() as session: + if verbose: + print("Downloading %s ..." % url, end="", flush=True) + for attempts_left in reversed(range(num_attempts)): + try: + with session.get(url) as res: + res.raise_for_status() + if len(res.content) == 0: + raise IOError("No data received") + + if len(res.content) < 8192: + content_str = res.content.decode("utf-8") + if "download_warning" in res.headers.get("Set-Cookie", ""): + links = [ + html.unescape(link) + for link in content_str.split('"') + if "export=download" in link + ] + if len(links) == 1: + url = requests.compat.urljoin(url, links[0]) + raise IOError("Google Drive virus checker nag") + if "Google Drive - Quota exceeded" in content_str: + raise IOError( + "Google Drive download quota exceeded -- please try again later" + ) + + match = re.search( + r'filename="([^"]*)"', + res.headers.get("Content-Disposition", ""), + ) + url_name = match[1] if match else url + url_data = res.content + if verbose: + print(" done") + break + except KeyboardInterrupt: + raise + except: + if not attempts_left: + if verbose: + print(" failed") + raise + if verbose: + print(".", end="", flush=True) + + # Return data as file object. + assert not return_filename + return io.BytesIO(url_data) + + +def compute_fvd(feats_fake: np.ndarray, feats_real: np.ndarray) -> float: + mu_gen, sigma_gen = compute_stats(feats_fake) + mu_real, sigma_real = compute_stats(feats_real) + + m = np.square(mu_gen - mu_real).sum() + s, _ = scipy.linalg.sqrtm( + np.dot(sigma_gen, sigma_real), disp=False + ) # pylint: disable=no-member + fid = np.real(m + np.trace(sigma_gen + sigma_real - s * 2)) + + return float(fid) + + +def compute_stats(feats: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + mu = feats.mean(axis=0) # [d] + sigma = np.cov(feats, rowvar=False) # [d, d] + + return mu, sigma + + +class FrechetVideoDistance(nn.Module): + def __init__(self): + super().__init__() + detector_url = ( + "https://www.dropbox.com/s/ge9e5ujwgetktms/i3d_torchscript.pt?dl=1" + ) + # Return raw features before the softmax layer. + self.detector_kwargs = dict(rescale=False, resize=True, return_features=True) + with open_url(detector_url, verbose=False) as f: + self.detector = torch.jit.load(f).eval() + + @torch.no_grad() + def compute(self, videos_fake: torch.Tensor, videos_real: torch.Tensor): + """ + :param videos_fake: predicted video tensor of shape (frame, batch, channel, height, width) + :param videos_real: ground-truth observation tensor of shape (frame, batch, channel, height, width) + :return: + """ + n_frames, batch_size, c, h, w = videos_fake.shape + if n_frames < 2: + raise ValueError("Video must have more than 1 frame for FVD") + + videos_fake = videos_fake.permute(1, 2, 0, 3, 4).contiguous() + videos_real = videos_real.permute(1, 2, 0, 3, 4).contiguous() + + # detector takes in tensors of shape [batch_size, c, video_len, h, w] with range -1 to 1 + feats_fake = self.detector(videos_fake, **self.detector_kwargs).cpu().numpy() + feats_real = self.detector(videos_real, **self.detector_kwargs).cpu().numpy() + + return compute_fvd(feats_fake, feats_real) diff --git a/algorithms/common/metrics/lpips.py b/algorithms/common/metrics/lpips.py new file mode 100644 index 0000000000000000000000000000000000000000..34fc01b7c3375c3efd2b4b3929866104471022eb --- /dev/null +++ b/algorithms/common/metrics/lpips.py @@ -0,0 +1 @@ +from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity diff --git a/algorithms/common/models/__init__.py b/algorithms/common/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/algorithms/common/models/cnn.py b/algorithms/common/models/cnn.py new file mode 100644 index 0000000000000000000000000000000000000000..a1720814f03c70bab96faf4f2382ebb39b16bf83 --- /dev/null +++ b/algorithms/common/models/cnn.py @@ -0,0 +1,141 @@ +import math +import torch.nn as nn +from torch.nn import functional as F + + +def is_square_of_two(num): + if num <= 0: + return False + return num & (num - 1) == 0 + +class CnnEncoder(nn.Module): + """ + Simple cnn encoder that encodes a 64x64 image to embeddings + """ + def __init__(self, embedding_size, activation_function='relu'): + super().__init__() + self.act_fn = getattr(F, activation_function) + self.embedding_size = embedding_size + self.fc = nn.Linear(1024, self.embedding_size) + self.conv1 = nn.Conv2d(3, 32, 4, stride=2) + self.conv2 = nn.Conv2d(32, 64, 4, stride=2) + self.conv3 = nn.Conv2d(64, 128, 4, stride=2) + self.conv4 = nn.Conv2d(128, 256, 4, stride=2) + self.modules = [self.conv1, self.conv2, self.conv3, self.conv4] + + def forward(self, observation): + batch_size = observation.shape[0] + hidden = self.act_fn(self.conv1(observation)) + hidden = self.act_fn(self.conv2(hidden)) + hidden = self.act_fn(self.conv3(hidden)) + hidden = self.act_fn(self.conv4(hidden)) + hidden = self.fc(hidden.view(batch_size, 1024)) + return hidden + + +class CnnDecoder(nn.Module): + """ + Simple Cnn decoder that decodes an embedding to 64x64 images + """ + def __init__(self, embedding_size, activation_function='relu'): + super().__init__() + self.act_fn = getattr(F, activation_function) + self.embedding_size = embedding_size + self.fc = nn.Linear(embedding_size, 128) + self.conv1 = nn.ConvTranspose2d(128, 128, 5, stride=2) + self.conv2 = nn.ConvTranspose2d(128, 64, 5, stride=2) + self.conv3 = nn.ConvTranspose2d(64, 32, 6, stride=2) + self.conv4 = nn.ConvTranspose2d(32, 3, 6, stride=2) + self.modules = [self.conv1, self.conv2, self.conv3, self.conv4] + + def forward(self, embedding): + batch_size = embedding.shape[0] + hidden = self.fc(embedding) + hidden = hidden.view(batch_size, 128, 1, 1) + hidden = self.act_fn(self.conv1(hidden)) + hidden = self.act_fn(self.conv2(hidden)) + hidden = self.act_fn(self.conv3(hidden)) + observation = self.conv4(hidden) + return observation + + +class FullyConvEncoder(nn.Module): + """ + Simple fully convolutional encoder, with 2D input and 2D output + """ + def __init__(self, + input_shape=(3, 64, 64), + embedding_shape=(8, 16, 16), + activation_function='relu', + init_channels=16, + ): + super().__init__() + + assert len(input_shape) == 3, "input_shape must be a tuple of length 3" + assert len(embedding_shape) == 3, "embedding_shape must be a tuple of length 3" + assert input_shape[1] == input_shape[2] and is_square_of_two(input_shape[1]), "input_shape must be square" + assert embedding_shape[1] == embedding_shape[2], "embedding_shape must be square" + assert input_shape[1] % embedding_shape[1] == 0, "input_shape must be divisible by embedding_shape" + assert is_square_of_two(init_channels), "init_channels must be a square of 2" + + depth = int(math.sqrt(input_shape[1] / embedding_shape[1])) + 1 + channels_per_layer = [init_channels * (2 ** i) for i in range(depth)] + self.act_fn = getattr(F, activation_function) + + self.downs = nn.ModuleList([]) + self.downs.append(nn.Conv2d(input_shape[0], channels_per_layer[0], kernel_size=3, stride=1, padding=1)) + + for i in range(1, depth): + self.downs.append(nn.Conv2d(channels_per_layer[i-1], channels_per_layer[i], + kernel_size=3, stride=2, padding=1)) + + # Bottleneck layer + self.downs.append(nn.Conv2d(channels_per_layer[-1], embedding_shape[0], kernel_size=1, stride=1, padding=0)) + + def forward(self, observation): + hidden = observation + for layer in self.downs: + hidden = self.act_fn(layer(hidden)) + return hidden + + +class FullyConvDecoder(nn.Module): + """ + Simple fully convolutional decoder, with 2D input and 2D output + """ + def __init__(self, + embedding_shape=(8, 16, 16), + output_shape=(3, 64, 64), + activation_function='relu', + init_channels=16, + ): + super().__init__() + + assert len(embedding_shape) == 3, "embedding_shape must be a tuple of length 3" + assert len(output_shape) == 3, "output_shape must be a tuple of length 3" + assert output_shape[1] == output_shape[2] and is_square_of_two(output_shape[1]), "output_shape must be square" + assert embedding_shape[1] == embedding_shape[2], "input_shape must be square" + assert output_shape[1] % embedding_shape[1] == 0, "output_shape must be divisible by input_shape" + assert is_square_of_two(init_channels), "init_channels must be a square of 2" + + depth = int(math.sqrt(output_shape[1] / embedding_shape[1])) + 1 + channels_per_layer = [init_channels * (2 ** i) for i in range(depth)] + self.act_fn = getattr(F, activation_function) + + self.ups = nn.ModuleList([]) + self.ups.append(nn.ConvTranspose2d(embedding_shape[0], channels_per_layer[-1], + kernel_size=1, stride=1, padding=0)) + + for i in range(1, depth): + self.ups.append(nn.ConvTranspose2d(channels_per_layer[-i], channels_per_layer[-i-1], + kernel_size=3, stride=2, padding=1, output_padding=1)) + + self.output_layer = nn.ConvTranspose2d(channels_per_layer[0], output_shape[0], + kernel_size=3, stride=1, padding=1) + + def forward(self, embedding): + hidden = embedding + for layer in self.ups: + hidden = self.act_fn(layer(hidden)) + + return self.output_layer(hidden) diff --git a/algorithms/common/models/mlp.py b/algorithms/common/models/mlp.py new file mode 100644 index 0000000000000000000000000000000000000000..e3273eacbe8a153e10e3cf0ede6ba3145e6f81c4 --- /dev/null +++ b/algorithms/common/models/mlp.py @@ -0,0 +1,22 @@ +from typing import Type, Optional + +import torch +from torch import nn as nn + + +class SimpleMlp(nn.Module): + """ + A class for very simple multi layer perceptron + """ + def __init__(self, in_dim=2, out_dim=1, hidden_dim=64, n_layers=2, + activation: Type[nn.Module] = nn.ReLU, output_activation: Optional[Type[nn.Module]] = None): + super(SimpleMlp, self).__init__() + layers = [nn.Linear(in_dim, hidden_dim), activation()] + layers.extend([nn.Linear(hidden_dim, hidden_dim), activation()] * (n_layers - 2)) + layers.append(nn.Linear(hidden_dim, out_dim)) + if output_activation: + layers.append(output_activation()) + self.net = nn.Sequential(*layers) + + def forward(self, x): + return self.net(x) diff --git a/algorithms/worldmem/__init__.py b/algorithms/worldmem/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0e9dfe8b751f5c2aa77b5b91fbfb909f0c35faf5 --- /dev/null +++ b/algorithms/worldmem/__init__.py @@ -0,0 +1,2 @@ +from .df_video import WorldMemMinecraft +from .pose_prediction import PosePrediction \ No newline at end of file diff --git a/algorithms/worldmem/df_base.py b/algorithms/worldmem/df_base.py new file mode 100644 index 0000000000000000000000000000000000000000..685a964854ff17a206c4e0c5d11fad02886b2c3b --- /dev/null +++ b/algorithms/worldmem/df_base.py @@ -0,0 +1,307 @@ +""" +This repo is forked from [Boyuan Chen](https://boyuan.space/)'s research +template [repo](https://github.com/buoyancy99/research-template). +By its MIT license, you must keep the above sentence in `README.md` +and the `LICENSE` file to credit the author. +""" + +from typing import Optional +from tqdm import tqdm +from omegaconf import DictConfig +import numpy as np +import torch +import torch.nn.functional as F +from typing import Any +from einops import rearrange + +from lightning.pytorch.utilities.types import STEP_OUTPUT + +from algorithms.common.base_pytorch_algo import BasePytorchAlgo +from .models.diffusion import Diffusion + + +class DiffusionForcingBase(BasePytorchAlgo): + def __init__(self, cfg: DictConfig): + self.cfg = cfg + self.x_shape = cfg.x_shape + self.frame_stack = cfg.frame_stack + self.x_stacked_shape = list(self.x_shape) + self.x_stacked_shape[0] *= cfg.frame_stack + self.guidance_scale = cfg.guidance_scale + self.context_frames = cfg.context_frames + self.chunk_size = cfg.chunk_size + self.action_cond_dim = cfg.action_cond_dim + self.causal = cfg.causal + + self.uncertainty_scale = cfg.uncertainty_scale + self.timesteps = cfg.diffusion.timesteps + self.sampling_timesteps = cfg.diffusion.sampling_timesteps + self.clip_noise = cfg.diffusion.clip_noise + + self.cfg.diffusion.cum_snr_decay = self.cfg.diffusion.cum_snr_decay ** (self.frame_stack * cfg.frame_skip) + + self.validation_step_outputs = [] + super().__init__(cfg) + + def _build_model(self): + self.diffusion_model = Diffusion( + x_shape=self.x_stacked_shape, + action_cond_dim=self.action_cond_dim, + is_causal=self.causal, + cfg=self.cfg.diffusion, + ) + self.register_data_mean_std(self.cfg.data_mean, self.cfg.data_std) + + def configure_optimizers(self): + params = tuple(self.diffusion_model.parameters()) + optimizer_dynamics = torch.optim.AdamW( + params, lr=self.cfg.lr, weight_decay=self.cfg.weight_decay, betas=self.cfg.optimizer_beta + ) + return optimizer_dynamics + + def optimizer_step(self, epoch, batch_idx, optimizer, optimizer_closure): + # update params + optimizer.step(closure=optimizer_closure) + + # manually warm up lr without a scheduler + if self.trainer.global_step < self.cfg.warmup_steps: + lr_scale = min(1.0, float(self.trainer.global_step + 1) / self.cfg.warmup_steps) + for pg in optimizer.param_groups: + pg["lr"] = lr_scale * self.cfg.lr + + def training_step(self, batch, batch_idx) -> STEP_OUTPUT: + xs, conditions, masks = self._preprocess_batch(batch) + + rand_length = torch.randint(3,xs.shape[0]-2, (1,))[0].item() + xs = torch.cat([xs[:rand_length], xs[rand_length-3:rand_length-1]]) + conditions = torch.cat([conditions[:rand_length], conditions[rand_length-3:rand_length-1]]) + masks = torch.cat([masks[:rand_length], masks[rand_length-3:rand_length-1]]) + noise_levels=self._generate_noise_levels(xs) + noise_levels[:rand_length] = 15 # stable_noise_levels + noise_levels[rand_length+1:] = 15 # stable_noise_levels + + xs_pred, loss = self.diffusion_model(xs, conditions, noise_levels=noise_levels) + loss = self.reweight_loss(loss, masks) + + # log the loss + if batch_idx % 20 == 0: + self.log("training/loss", loss) + + xs = self._unstack_and_unnormalize(xs) + xs_pred = self._unstack_and_unnormalize(xs_pred) + + output_dict = { + "loss": loss, + "xs_pred": xs_pred, + "xs": xs, + } + + return output_dict + + @torch.no_grad() + def validation_step(self, batch, batch_idx, namespace="validation") -> STEP_OUTPUT: + xs, conditions, masks = self._preprocess_batch(batch) + n_frames, batch_size, *_ = xs.shape + xs_pred = [] + curr_frame = 0 + + # context + n_context_frames = self.context_frames // self.frame_stack + xs_pred = xs[:n_context_frames].clone() + curr_frame += n_context_frames + + if self.condtion_similar_length: + n_frames -= self.condtion_similar_length + + pbar = tqdm(total=n_frames, initial=curr_frame, desc="Sampling") + while curr_frame < n_frames: + if self.chunk_size > 0: + horizon = min(n_frames - curr_frame, self.chunk_size) + else: + horizon = n_frames - curr_frame + assert horizon <= self.n_tokens, "horizon exceeds the number of tokens." + scheduling_matrix = self._generate_scheduling_matrix(horizon) + + chunk = torch.randn((horizon, batch_size, *self.x_stacked_shape), device=self.device) + chunk = torch.clamp(chunk, -self.clip_noise, self.clip_noise) + xs_pred = torch.cat([xs_pred, chunk], 0) + + # sliding window: only input the last n_tokens frames + start_frame = max(0, curr_frame + horizon - self.n_tokens) + + pbar.set_postfix( + { + "start": start_frame, + "end": curr_frame + horizon, + } + ) + + if self.condtion_similar_length: + xs_pred = torch.cat([xs_pred, xs[curr_frame-self.condtion_similar_length:curr_frame].clone()], 0) + + for m in range(scheduling_matrix.shape[0] - 1): + + from_noise_levels = np.concatenate((np.zeros((curr_frame,), dtype=np.int64), scheduling_matrix[m]))[ + :, None + ].repeat(batch_size, axis=1) + to_noise_levels = np.concatenate( + ( + np.zeros((curr_frame,), dtype=np.int64), + scheduling_matrix[m + 1], + ) + )[ + :, None + ].repeat(batch_size, axis=1) + + if self.condtion_similar_length: + from_noise_levels = np.concatenate([from_noise_levels, np.array([[0,0,0,0]*self.condtion_similar_length])], axis=0) + to_noise_levels = np.concatenate([to_noise_levels, np.array([[0,0,0,0]*self.condtion_similar_length])], axis=0) + + from_noise_levels = torch.from_numpy(from_noise_levels).to(self.device) + to_noise_levels = torch.from_numpy(to_noise_levels).to(self.device) + + # update xs_pred by DDIM or DDPM sampling + # input frames within the sliding window + + try: + input_condition = conditions[start_frame : curr_frame + horizon].clone() + except: + import pdb;pdb.set_trace() + if self.condtion_similar_length: + input_condition = torch.cat([conditions[start_frame : curr_frame + horizon], conditions[-self.condtion_similar_length:]], dim=0) + xs_pred[start_frame:] = self.diffusion_model.sample_step( + xs_pred[start_frame:], + input_condition, + from_noise_levels[start_frame:], + to_noise_levels[start_frame:], + ) + + if self.condtion_similar_length: + xs_pred = xs_pred[:-self.condtion_similar_length] + + curr_frame += horizon + pbar.update(horizon) + + if self.condtion_similar_length: + xs = xs[:-self.condtion_similar_length] + # FIXME: loss + loss = F.mse_loss(xs_pred, xs, reduction="none") + loss = self.reweight_loss(loss, masks) + self.validation_step_outputs.append((xs_pred.detach().cpu(), xs.detach().cpu())) + + return loss + + def test_step(self, *args: Any, **kwargs: Any) -> STEP_OUTPUT: + return self.validation_step(*args, **kwargs, namespace="test") + + def test_epoch_end(self) -> None: + self.on_validation_epoch_end(namespace="test") + + def _generate_noise_levels(self, xs: torch.Tensor, masks: Optional[torch.Tensor] = None) -> torch.Tensor: + """ + Generate noise levels for training. + """ + num_frames, batch_size, *_ = xs.shape + match self.cfg.noise_level: + case "random_all": # entirely random noise levels + noise_levels = torch.randint(0, self.timesteps, (num_frames, batch_size), device=xs.device) + case "same": + noise_levels = torch.randint(0, self.timesteps, (num_frames, batch_size), device=xs.device) + noise_levels[1:] = noise_levels[0] + + if masks is not None: + # for frames that are not available, treat as full noise + discard = torch.all(~rearrange(masks.bool(), "(t fs) b -> t b fs", fs=self.frame_stack), -1) + noise_levels = torch.where(discard, torch.full_like(noise_levels, self.timesteps - 1), noise_levels) + + return noise_levels + + def _generate_scheduling_matrix(self, horizon: int): + match self.cfg.scheduling_matrix: + case "pyramid": + return self._generate_pyramid_scheduling_matrix(horizon, self.uncertainty_scale) + case "full_sequence": + return np.arange(self.sampling_timesteps, -1, -1)[:, None].repeat(horizon, axis=1) + case "autoregressive": + return self._generate_pyramid_scheduling_matrix(horizon, self.sampling_timesteps) + case "trapezoid": + return self._generate_trapezoid_scheduling_matrix(horizon, self.uncertainty_scale) + + def _generate_pyramid_scheduling_matrix(self, horizon: int, uncertainty_scale: float): + height = self.sampling_timesteps + int((horizon - 1) * uncertainty_scale) + 1 + scheduling_matrix = np.zeros((height, horizon), dtype=np.int64) + for m in range(height): + for t in range(horizon): + scheduling_matrix[m, t] = self.sampling_timesteps + int(t * uncertainty_scale) - m + + return np.clip(scheduling_matrix, 0, self.sampling_timesteps) + + def _generate_trapezoid_scheduling_matrix(self, horizon: int, uncertainty_scale: float): + height = self.sampling_timesteps + int((horizon + 1) // 2 * uncertainty_scale) + scheduling_matrix = np.zeros((height, horizon), dtype=np.int64) + for m in range(height): + for t in range((horizon + 1) // 2): + scheduling_matrix[m, t] = self.sampling_timesteps + int(t * uncertainty_scale) - m + scheduling_matrix[m, -t] = self.sampling_timesteps + int(t * uncertainty_scale) - m + + return np.clip(scheduling_matrix, 0, self.sampling_timesteps) + + def reweight_loss(self, loss, weight=None): + # Note there is another part of loss reweighting (fused_snr) inside the Diffusion class! + loss = rearrange(loss, "t b (fs c) ... -> t b fs c ...", fs=self.frame_stack) + if weight is not None: + expand_dim = len(loss.shape) - len(weight.shape) - 1 + weight = rearrange( + weight, + "(t fs) b ... -> t b fs ..." + " 1" * expand_dim, + fs=self.frame_stack, + ) + loss = loss * weight + + return loss.mean() + + def _preprocess_batch(self, batch): + xs = batch[0] + batch_size, n_frames = xs.shape[:2] + + if n_frames % self.frame_stack != 0: + raise ValueError("Number of frames must be divisible by frame stack size") + if self.context_frames % self.frame_stack != 0: + raise ValueError("Number of context frames must be divisible by frame stack size") + + masks = torch.ones(n_frames, batch_size).to(xs.device) + n_frames = n_frames // self.frame_stack + + if self.action_cond_dim: + conditions = batch[1] + conditions = torch.cat([torch.zeros_like(conditions[:, :1]), conditions[:, 1:]], 1) + conditions = rearrange(conditions, "b (t fs) d -> t b (fs d)", fs=self.frame_stack).contiguous() + + # f, _, _ = conditions.shape + # predefined_1 = torch.tensor([0,0,0,1]).to(conditions.device) + # predefined_2 = torch.tensor([0,0,1,0]).to(conditions.device) + # conditions[:f//2] = predefined_1 + # conditions[f//2:] = predefined_2 + else: + conditions = [None for _ in range(n_frames)] + + xs = self._normalize_x(xs) + xs = rearrange(xs, "b (t fs) c ... -> t b (fs c) ...", fs=self.frame_stack).contiguous() + + return xs, conditions, masks + + def _normalize_x(self, xs): + shape = [1] * (xs.ndim - self.data_mean.ndim) + list(self.data_mean.shape) + mean = self.data_mean.reshape(shape) + std = self.data_std.reshape(shape) + return (xs - mean) / std + + def _unnormalize_x(self, xs): + shape = [1] * (xs.ndim - self.data_mean.ndim) + list(self.data_mean.shape) + mean = self.data_mean.reshape(shape) + std = self.data_std.reshape(shape) + return xs * std + mean + + def _unstack_and_unnormalize(self, xs): + xs = rearrange(xs, "t b (fs c) ... -> (t fs) b c ...", fs=self.frame_stack) + return self._unnormalize_x(xs) diff --git a/algorithms/worldmem/df_video.py b/algorithms/worldmem/df_video.py new file mode 100644 index 0000000000000000000000000000000000000000..c73e77e5ea17762777fc04adc24cce768bbbd4ea --- /dev/null +++ b/algorithms/worldmem/df_video.py @@ -0,0 +1,920 @@ +import random +import math +import numpy as np +import torch +import torch.nn.functional as F +import torchvision.transforms.functional as TF +from torchvision.transforms import InterpolationMode +from PIL import Image +from packaging import version as pver +from einops import rearrange +from tqdm import tqdm +from omegaconf import DictConfig +from lightning.pytorch.utilities.types import STEP_OUTPUT +from algorithms.common.metrics import ( + LearnedPerceptualImagePatchSimilarity, +) +from utils.logging_utils import log_video, get_validation_metrics_for_videos +from .df_base import DiffusionForcingBase +from .models.vae import VAE_models +from .models.diffusion import Diffusion +from .models.pose_prediction import PosePredictionNet + + +# Utility Functions +def euler_to_rotation_matrix(pitch, yaw): + """ + Convert pitch and yaw angles (in radians) to a 3x3 rotation matrix. + Supports batch input. + + Args: + pitch (torch.Tensor): Pitch angles in radians. + yaw (torch.Tensor): Yaw angles in radians. + + Returns: + torch.Tensor: Rotation matrix of shape (batch_size, 3, 3). + """ + cos_pitch, sin_pitch = torch.cos(pitch), torch.sin(pitch) + cos_yaw, sin_yaw = torch.cos(yaw), torch.sin(yaw) + + R_pitch = torch.stack([ + torch.ones_like(pitch), torch.zeros_like(pitch), torch.zeros_like(pitch), + torch.zeros_like(pitch), cos_pitch, -sin_pitch, + torch.zeros_like(pitch), sin_pitch, cos_pitch + ], dim=-1).reshape(-1, 3, 3) + + R_yaw = torch.stack([ + cos_yaw, torch.zeros_like(yaw), sin_yaw, + torch.zeros_like(yaw), torch.ones_like(yaw), torch.zeros_like(yaw), + -sin_yaw, torch.zeros_like(yaw), cos_yaw + ], dim=-1).reshape(-1, 3, 3) + + return torch.matmul(R_yaw, R_pitch) + + +def euler_to_camera_to_world_matrix(pose): + """ + Convert (x, y, z, pitch, yaw) to a 4x4 camera-to-world transformation matrix using torch. + Supports both (5,) and (f, b, 5) shaped inputs. + + Args: + pose (torch.Tensor): Pose tensor of shape (5,) or (f, b, 5). + + Returns: + torch.Tensor: Camera-to-world transformation matrix of shape (4, 4). + """ + + origin_dim = pose.ndim + if origin_dim == 1: + pose = pose.unsqueeze(0).unsqueeze(0) # Convert (5,) -> (1, 1, 5) + elif origin_dim == 2: + pose = pose.unsqueeze(0) + + x, y, z, pitch, yaw = pose[..., 0], pose[..., 1], pose[..., 2], pose[..., 3], pose[..., 4] + pitch, yaw = torch.deg2rad(pitch), torch.deg2rad(yaw) + + # Compute rotation matrix (batch mode) + R = euler_to_rotation_matrix(pitch, yaw) # Shape (f*b, 3, 3) + + # Create the 4x4 transformation matrix + eye = torch.eye(4, dtype=torch.float32, device=pose.device) + camera_to_world = eye.repeat(R.shape[0], 1, 1) # Shape (f*b, 4, 4) + + # Assign rotation + camera_to_world[:, :3, :3] = R + + # Assign translation + camera_to_world[:, :3, 3] = torch.stack([x.reshape(-1), y.reshape(-1), z.reshape(-1)], dim=-1) + + # Reshape back to (f, b, 4, 4) if needed + if origin_dim == 3: + return camera_to_world.view(pose.shape[0], pose.shape[1], 4, 4) + elif origin_dim == 2: + return camera_to_world.view(pose.shape[0], 4, 4) + else: + return camera_to_world.squeeze(0).squeeze(0) # Convert (1,1,4,4) -> (4,4) + +def is_inside_fov_3d_hv(points, center, center_pitch, center_yaw, fov_half_h, fov_half_v): + """ + Check whether points are within a given 3D field of view (FOV) + with separately defined horizontal and vertical ranges. + + The center view direction is specified by pitch and yaw (in degrees). + + :param points: (N, B, 3) Sample point coordinates + :param center: (3,) Center coordinates of the FOV + :param center_pitch: Pitch angle of the center view (in degrees) + :param center_yaw: Yaw angle of the center view (in degrees) + :param fov_half_h: Horizontal half-FOV angle (in degrees) + :param fov_half_v: Vertical half-FOV angle (in degrees) + :return: Boolean tensor (N, B), indicating whether each point is inside the FOV + """ + # Compute vectors relative to the center + vectors = points - center # shape (N, B, 3) + x = vectors[..., 0] + y = vectors[..., 1] + z = vectors[..., 2] + + # Compute horizontal angle (yaw): measured with respect to the z-axis as the forward direction, + # and the x-axis as left-right, resulting in a range of -180 to 180 degrees. + azimuth = torch.atan2(x, z) * (180 / math.pi) + + # Compute vertical angle (pitch): measured with respect to the horizontal plane, + # resulting in a range of -90 to 90 degrees. + elevation = torch.atan2(y, torch.sqrt(x**2 + z**2)) * (180 / math.pi) + + # Compute the angular difference from the center view (handling circular angle wrap-around) + diff_azimuth = (azimuth - center_yaw).abs() % 360 + diff_elevation = (elevation - center_pitch).abs() % 360 + + # Adjust values greater than 180 degrees to the shorter angular difference + diff_azimuth = torch.where(diff_azimuth > 180, 360 - diff_azimuth, diff_azimuth) + diff_elevation = torch.where(diff_elevation > 180, 360 - diff_elevation, diff_elevation) + + # Check if both horizontal and vertical angles are within their respective FOV limits + return (diff_azimuth < fov_half_h) & (diff_elevation < fov_half_v) + +def generate_points_in_sphere(n_points, radius): + # Sample three independent uniform distributions + samples_r = torch.rand(n_points) # For radius distribution + samples_phi = torch.rand(n_points) # For azimuthal angle phi + samples_u = torch.rand(n_points) # For polar angle theta + + # Apply cube root to ensure uniform volumetric distribution + r = radius * torch.pow(samples_r, 1/3) + # Azimuthal angle phi uniformly distributed in [0, 2π] + phi = 2 * math.pi * samples_phi + # Convert u to theta to ensure cos(theta) is uniformly distributed + theta = torch.acos(1 - 2 * samples_u) + + # Convert spherical coordinates to Cartesian coordinates + x = r * torch.sin(theta) * torch.cos(phi) + y = r * torch.sin(theta) * torch.sin(phi) + z = r * torch.cos(theta) + + points = torch.stack((x, y, z), dim=1) + return points + +def tensor_max_with_number(tensor, number): + number_tensor = torch.tensor(number, dtype=tensor.dtype, device=tensor.device) + result = torch.max(tensor, number_tensor) + return result + +def custom_meshgrid(*args): + # ref: https://pytorch.org/docs/stable/generated/torch.meshgrid.html?highlight=meshgrid#torch.meshgrid + if pver.parse(torch.__version__) < pver.parse('1.10'): + return torch.meshgrid(*args) + else: + return torch.meshgrid(*args, indexing='ij') + +def camera_to_world_to_world_to_camera(camera_to_world: torch.Tensor) -> torch.Tensor: + """ + Convert Camera-to-World matrices to World-to-Camera matrices for a tensor with shape (f, b, 4, 4). + + Args: + camera_to_world (torch.Tensor): A tensor of shape (f, b, 4, 4), where: + f = number of frames, + b = batch size. + + Returns: + torch.Tensor: A tensor of shape (f, b, 4, 4) representing the World-to-Camera matrices. + """ + # Ensure input is a 4D tensor + assert camera_to_world.ndim == 4 and camera_to_world.shape[2:] == (4, 4), \ + "Input must be of shape (f, b, 4, 4)" + + # Extract the rotation (R) and translation (T) parts + R = camera_to_world[:, :, :3, :3] # Shape: (f, b, 3, 3) + T = camera_to_world[:, :, :3, 3] # Shape: (f, b, 3) + + # Initialize an identity matrix for the output + world_to_camera = torch.eye(4, device=camera_to_world.device).unsqueeze(0).unsqueeze(0) + world_to_camera = world_to_camera.repeat(camera_to_world.size(0), camera_to_world.size(1), 1, 1) # Shape: (f, b, 4, 4) + + # Compute the rotation (transpose of R) + world_to_camera[:, :, :3, :3] = R.transpose(2, 3) + + # Compute the translation (-R^T * T) + world_to_camera[:, :, :3, 3] = -torch.matmul(R.transpose(2, 3), T.unsqueeze(-1)).squeeze(-1) + + return world_to_camera.to(camera_to_world.dtype) + +def convert_to_plucker(poses, curr_frame, focal_length, image_width, image_height): + + intrinsic = np.asarray([focal_length * image_width, + focal_length * image_height, + 0.5 * image_width, + 0.5 * image_height], dtype=np.float32) + + c2ws = get_relative_pose(poses, zero_first_frame_scale=curr_frame) + c2ws = rearrange(c2ws, "t b m n -> b t m n") + + K = torch.as_tensor(intrinsic, device=poses.device, dtype=poses.dtype).repeat(c2ws.shape[0],c2ws.shape[1],1) # [B, F, 4] + plucker_embedding = ray_condition(K, c2ws, image_height, image_width, device=c2ws.device) + plucker_embedding = rearrange(plucker_embedding, "b t h w d -> t b h w d").contiguous() + + return plucker_embedding + + +def get_relative_pose(abs_c2ws, zero_first_frame_scale): + abs_w2cs = camera_to_world_to_world_to_camera(abs_c2ws) + target_cam_c2w = torch.tensor([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]).to(abs_c2ws.device).to(abs_c2ws.dtype) + abs2rel = target_cam_c2w @ abs_w2cs[zero_first_frame_scale] + ret_poses = [abs2rel @ abs_c2w for abs_c2w in abs_c2ws] + ret_poses = torch.stack(ret_poses) + return ret_poses + +def ray_condition(K, c2w, H, W, device): + # c2w: B, V, 4, 4 + # K: B, V, 4 + + B = K.shape[0] + + j, i = custom_meshgrid( + torch.linspace(0, H - 1, H, device=device, dtype=c2w.dtype), + torch.linspace(0, W - 1, W, device=device, dtype=c2w.dtype), + ) + i = i.reshape([1, 1, H * W]).expand([B, 1, H * W]) + 0.5 # [B, HxW] + j = j.reshape([1, 1, H * W]).expand([B, 1, H * W]) + 0.5 # [B, HxW] + + fx, fy, cx, cy = K.chunk(4, dim=-1) # B,V, 1 + + zs = torch.ones_like(i, device=device, dtype=c2w.dtype) # [B, HxW] + xs = -(i - cx) / fx * zs + ys = -(j - cy) / fy * zs + + zs = zs.expand_as(ys) + + directions = torch.stack((xs, ys, zs), dim=-1) # B, V, HW, 3 + directions = directions / directions.norm(dim=-1, keepdim=True) # B, V, HW, 3 + + rays_d = directions @ c2w[..., :3, :3].transpose(-1, -2) # B, V, 3, HW + rays_o = c2w[..., :3, 3] # B, V, 3 + rays_o = rays_o[:, :, None].expand_as(rays_d) # B, V, 3, HW + # c2w @ dirctions + rays_dxo = torch.linalg.cross(rays_o, rays_d) + plucker = torch.cat([rays_dxo, rays_d], dim=-1) + plucker = plucker.reshape(B, c2w.shape[1], H, W, 6) # B, V, H, W, 6 + + return plucker + +def random_transform(tensor): + """ + Apply the same random translation, rotation, and scaling to all frames in the batch. + + Args: + tensor (torch.Tensor): Input tensor of shape (F, B, 3, H, W). + + Returns: + torch.Tensor: Transformed tensor of shape (F, B, 3, H, W). + """ + if tensor.ndim != 5: + raise ValueError("Input tensor must have shape (F, B, 3, H, W)") + + F, B, C, H, W = tensor.shape + + # Generate random transformation parameters + max_translate = 0.2 # Translate up to 20% of width/height + max_rotate = 30 # Rotate up to 30 degrees + max_scale = 0.2 # Scale change by up to +/- 20% + + translate_x = random.uniform(-max_translate, max_translate) * W + translate_y = random.uniform(-max_translate, max_translate) * H + rotate_angle = random.uniform(-max_rotate, max_rotate) + scale_factor = 1 + random.uniform(-max_scale, max_scale) + + # Apply the same transformation to all frames and batches + + tensor = tensor.reshape(F*B, C, H, W) + transformed_tensor = TF.affine( + tensor, + angle=rotate_angle, + translate=(translate_x, translate_y), + scale=scale_factor, + shear=(0, 0), + interpolation=InterpolationMode.BILINEAR, + fill=0 + ) + + transformed_tensor = transformed_tensor.reshape(F, B, C, H, W) + return transformed_tensor + +def save_tensor_as_png(tensor, file_path): + """ + Save a 3*H*W tensor as a PNG image. + + Args: + tensor (torch.Tensor): Input tensor of shape (3, H, W). + file_path (str): Path to save the PNG file. + """ + if tensor.ndim != 3 or tensor.shape[0] != 3: + raise ValueError("Input tensor must have shape (3, H, W)") + + # Convert tensor to PIL Image + image = TF.to_pil_image(tensor) + + # Save image + image.save(file_path) + +class WorldMemMinecraft(DiffusionForcingBase): + """ + Video generation for MineCraft with memory. + """ + + def __init__(self, cfg: DictConfig): + """ + Initialize the WorldMemMinecraft class with the given configuration. + + Args: + cfg (DictConfig): Configuration object. + """ + # self.metrics = cfg.metrics + self.n_tokens = cfg.n_frames // cfg.frame_stack # number of max tokens for the model + self.n_frames = cfg.n_frames + if hasattr(cfg, "n_tokens"): + self.n_tokens = cfg.n_tokens // cfg.frame_stack + self.condition_similar_length = cfg.condition_similar_length + self.pose_cond_dim = cfg.pose_cond_dim + + self.use_plucker = cfg.use_plucker + self.relative_embedding = cfg.relative_embedding + self.cond_only_on_qk = cfg.cond_only_on_qk + self.use_reference_attention = cfg.use_reference_attention + self.add_frame_timestep_embedder = cfg.add_frame_timestep_embedder + self.ref_mode = getattr(cfg, "ref_mode", 'sequential') + self.log_curve = getattr(cfg, "log_curve", False) + self.focal_length = cfg.focal_length + self.log_video = cfg.log_video + self.self_consistency_eval = getattr(cfg, "self_consistency_eval", False) + + self.is_interactive = cfg.get("is_interactive", False) + if self.is_interactive: + self_frames = None + self_poses = None + self_memory_c2w = None + self_frame_idx = None + + super().__init__(cfg) + + def _build_model(self): + + self.diffusion_model = Diffusion( + reference_length=self.condition_similar_length, + x_shape=self.x_stacked_shape, + action_cond_dim=self.action_cond_dim, + pose_cond_dim=self.pose_cond_dim, + is_causal=self.causal, + cfg=self.cfg.diffusion, + is_dit=True, + use_plucker=self.use_plucker, + relative_embedding=self.relative_embedding, + cond_only_on_qk=self.cond_only_on_qk, + use_reference_attention=self.use_reference_attention, + add_frame_timestep_embedder=self.add_frame_timestep_embedder, + ref_mode=self.ref_mode + ) + + # self.register_data_mean_std(self.cfg.data_mean, self.cfg.data_std) + self.validation_lpips_model = LearnedPerceptualImagePatchSimilarity() + + vae = VAE_models["vit-l-20-shallow-encoder"]() + self.vae = vae.eval() + + self.pose_prediction_model = PosePredictionNet() + + def _generate_noise_levels(self, xs: torch.Tensor, masks = None) -> torch.Tensor: + """ + Generate noise levels for training. + """ + num_frames, batch_size, *_ = xs.shape + match self.cfg.noise_level: + case "random_all": # entirely random noise levels + noise_levels = torch.randint(0, self.timesteps, (num_frames, batch_size), device=xs.device) + case "same": + noise_levels = torch.randint(0, self.timesteps, (num_frames, batch_size), device=xs.device) + noise_levels[1:] = noise_levels[0] + + if masks is not None: + # for frames that are not available, treat as full noise + discard = torch.all(~rearrange(masks.bool(), "(t fs) b -> t b fs", fs=self.frame_stack), -1) + noise_levels = torch.where(discard, torch.full_like(noise_levels, self.timesteps - 1), noise_levels) + + return noise_levels + + def training_step(self, batch, batch_idx) -> STEP_OUTPUT: + """ + Perform a single training step. + + This function processes the input batch, + encodes the input frames, generates noise levels, and computes the loss using the diffusion model. + + Args: + batch: Input batch of data containing frames, conditions, poses, etc. + batch_idx: Index of the current batch. + + Returns: + dict: A dictionary containing the training loss. + """ + xs, conditions, pose_conditions, c2w_mat, frame_idx = self._preprocess_batch(batch) + + if self.use_plucker: + if self.relative_embedding: + input_pose_condition = [] + frame_idx_list = [] + for i in range(self.n_frames): + input_pose_condition.append( + convert_to_plucker( + torch.cat([c2w_mat[i:i + 1], c2w_mat[-self.condition_similar_length:]]).clone(), + 0, + focal_length=self.focal_length, + image_height=xs.shape[-2],image_width=xs.shape[-1] + ).to(xs.dtype) + ) + frame_idx_list.append( + torch.cat([ + frame_idx[i:i + 1] - frame_idx[i:i + 1], + frame_idx[-self.condition_similar_length:] - frame_idx[i:i + 1] + ]).clone() + ) + input_pose_condition = torch.cat(input_pose_condition) + frame_idx_list = torch.cat(frame_idx_list) + else: + input_pose_condition = convert_to_plucker( + c2w_mat, 0, focal_length=self.focal_length + ).to(xs.dtype) + frame_idx_list = frame_idx + else: + input_pose_condition = pose_conditions.to(xs.dtype) + frame_idx_list = None + + xs = self.encode(xs) + + noise_levels = self._generate_noise_levels(xs) + + if self.condition_similar_length: + noise_levels[-self.condition_similar_length:] = self.diffusion_model.stabilization_level + conditions[-self.condition_similar_length:] *= 0 + + _, loss = self.diffusion_model( + xs, + conditions, + input_pose_condition, + noise_levels=noise_levels, + reference_length=self.condition_similar_length, + frame_idx=frame_idx_list + ) + + if self.condition_similar_length: + loss = loss[:-self.condition_similar_length] + + loss = self.reweight_loss(loss, None) + + if batch_idx % 20 == 0: + self.log("training/loss", loss.cpu()) + + return {"loss": loss} + + + def on_validation_epoch_end(self, namespace="validation") -> None: + if not self.validation_step_outputs: + return + + xs_pred = [] + xs = [] + for pred, gt in self.validation_step_outputs: + xs_pred.append(pred) + xs.append(gt) + + xs_pred = torch.cat(xs_pred, 1) + if gt is not None: + xs = torch.cat(xs, 1) + else: + xs = None + + if self.logger and self.log_video: + log_video( + xs_pred, + xs, + step=None if namespace == "test" else self.global_step, + namespace=namespace + "_vis", + context_frames=self.context_frames, + logger=self.logger.experiment, + ) + + if xs is not None: + metric_dict = get_validation_metrics_for_videos( + xs_pred, xs, + lpips_model=self.validation_lpips_model) + + self.log_dict( + {"mse": metric_dict['mse'], + "psnr": metric_dict['psnr'], + "lpips": metric_dict['lpips']}, + sync_dist=True + ) + + if self.log_curve: + psnr_values = metric_dict['frame_wise_psnr'].cpu().tolist() + frames = list(range(len(psnr_values))) + line_plot = wandb.plot.line_series( + xs = frames, + ys = [psnr_values], + keys = ["PSNR"], + title = "Frame-wise PSNR", + xname = "Frame index" + ) + + self.logger.experiment.log({"frame_wise_psnr_plot": line_plot}) + + elif self.self_consistency_eval: + metric_dict = get_validation_metrics_for_videos( + xs_pred[:1], + xs_pred[-1:], + lpips_model=self.validation_lpips_model, + ) + self.log_dict( + {"lpips": metric_dict['lpips'], + "mse": metric_dict['mse'], + "psnr": metric_dict['psnr']}, + sync_dist=True + ) + + self.validation_step_outputs.clear() + + def _preprocess_batch(self, batch): + + xs, conditions, pose_conditions, frame_index = batch + + if self.action_cond_dim: + conditions = torch.cat([torch.zeros_like(conditions[:, :1]), conditions[:, 1:]], 1) + conditions = rearrange(conditions, "b t d -> t b d").contiguous() + else: + raise NotImplementedError("Only support external cond.") + + pose_conditions = rearrange(pose_conditions, "b t d -> t b d").contiguous() + c2w_mat = euler_to_camera_to_world_matrix(pose_conditions) + xs = rearrange(xs, "b t c ... -> t b c ...").contiguous() + frame_index = rearrange(frame_index, "b t -> t b").contiguous() + + return xs, conditions, pose_conditions, c2w_mat, frame_index + + def encode(self, x): + # vae encoding + T = x.shape[0] + H, W = x.shape[-2:] + scaling_factor = 0.07843137255 + + x = rearrange(x, "t b c h w -> (t b) c h w") + with torch.no_grad(): + x = self.vae.encode(x * 2 - 1).mean * scaling_factor + x = rearrange(x, "(t b) (h w) c -> t b c h w", t=T, h=H // self.vae.patch_size, w=W // self.vae.patch_size) + return x + + def decode(self, x): + total_frames = x.shape[0] + scaling_factor = 0.07843137255 + x = rearrange(x, "t b c h w -> (t b) (h w) c") + with torch.no_grad(): + x = (self.vae.decode(x / scaling_factor) + 1) / 2 + x = rearrange(x, "(t b) c h w-> t b c h w", t=total_frames) + return x + + def _generate_condition_indices(self, curr_frame, condition_similar_length, xs_pred, pose_conditions, frame_idx): + """ + Generate indices for condition similarity based on the current frame and pose conditions. + """ + if curr_frame < condition_similar_length: + random_idx = [i for i in range(curr_frame)] + [0] * (condition_similar_length - curr_frame) + random_idx = np.repeat(np.array(random_idx)[:, None], xs_pred.shape[1], -1) + else: + # Generate points in a sphere and filter based on field of view + num_samples = 10000 + radius = 30 + points = generate_points_in_sphere(num_samples, radius).to(pose_conditions.device) + points = points[:, None].repeat(1, pose_conditions.shape[1], 1) + points += pose_conditions[curr_frame, :, :3][None] + fov_half_h = torch.tensor(105 / 2, device=pose_conditions.device) + fov_half_v = torch.tensor(75 / 2, device=pose_conditions.device) + in_fov1 = is_inside_fov_3d_hv( + points, pose_conditions[curr_frame, :, :3], + pose_conditions[curr_frame, :, -2], pose_conditions[curr_frame, :, -1], + fov_half_h, fov_half_v + ) + + # Compute overlap ratios and select indices + in_fov_list = torch.stack([ + is_inside_fov_3d_hv(points, pc[:, :3], pc[:, -2], pc[:, -1], fov_half_h, fov_half_v) + for pc in pose_conditions[:curr_frame] + ]) + random_idx = [] + for _ in range(condition_similar_length): + overlap_ratio = ((in_fov1.bool() & in_fov_list).sum(1)) / in_fov1.sum() + + confidence = overlap_ratio + (curr_frame - frame_idx[:curr_frame]) / curr_frame * (-0.2) + + if len(random_idx) > 0: + confidence[torch.cat(random_idx)] = -1e10 + _, r_idx = torch.topk(confidence, k=1, dim=0) + random_idx.append(r_idx[0]) + + # choice 1: directly remove overlapping region + occupied_mask = in_fov_list[r_idx[0, range(in_fov1.shape[-1])], :, range(in_fov1.shape[-1])].permute(1,0) + in_fov1 = in_fov1 & ~occupied_mask + + # choice 2: apply similarity filter + # cos_sim = F.cosine_similarity(xs_pred.to(r_idx.device)[r_idx[:, range(in_fov1.shape[1])], + # range(in_fov1.shape[1])], xs_pred.to(r_idx.device)[:curr_frame], dim=2) + # cos_sim = cos_sim.mean((-2,-1)) + + # mask_sim = cos_sim>0.9 + # in_fov_list = in_fov_list & ~mask_sim[:,None].to(in_fov_list.device) + + random_idx = torch.stack(random_idx).cpu() + + return random_idx + + def _prepare_conditions(self, + start_frame, curr_frame, horizon, conditions, + pose_conditions, c2w_mat, frame_idx, random_idx, + image_width, image_height): + """ + Prepare input conditions and pose conditions for sampling. + """ + + padding = torch.zeros((len(random_idx),) + conditions.shape[1:], device=conditions.device, dtype=conditions.dtype) + input_condition = torch.cat([conditions[start_frame:curr_frame + horizon], padding], dim=0) + + batch_size = conditions.shape[1] + + if self.use_plucker: + if self.relative_embedding: + frame_idx_list = [] + input_pose_condition = [] + for i in range(start_frame, curr_frame + horizon): + input_pose_condition.append(convert_to_plucker(torch.cat([c2w_mat[i:i+1],c2w_mat[random_idx[:,range(batch_size)], range(batch_size)]]).clone(), 0, focal_length=self.focal_length, + image_width=image_width, image_height=image_height).to(conditions.dtype)) + frame_idx_list.append(torch.cat([frame_idx[i:i+1]-frame_idx[i:i+1], frame_idx[random_idx[:,range(batch_size)], range(batch_size)]-frame_idx[i:i+1]])) + input_pose_condition = torch.cat(input_pose_condition) + frame_idx_list = torch.cat(frame_idx_list) + + else: + input_pose_condition = torch.cat([c2w_mat[start_frame : curr_frame + horizon], c2w_mat[random_idx[:,range(batch_size)], range(batch_size)]], dim=0).clone() + input_pose_condition = convert_to_plucker(input_pose_condition, 0, focal_length=self.focal_length) + frame_idx_list = None + else: + input_pose_condition = torch.cat([pose_conditions[start_frame : curr_frame + horizon], pose_conditions[random_idx[:,range(batch_size)], range(batch_size)]], dim=0).clone() + frame_idx_list = None + + return input_condition, input_pose_condition, frame_idx_list + + def _prepare_noise_levels(self, scheduling_matrix, m, curr_frame, batch_size, condition_similar_length): + """ + Prepare noise levels for the current sampling step. + """ + from_noise_levels = np.concatenate((np.zeros((curr_frame,), dtype=np.int64), scheduling_matrix[m]))[:, None].repeat(batch_size, axis=1) + to_noise_levels = np.concatenate((np.zeros((curr_frame,), dtype=np.int64), scheduling_matrix[m + 1]))[:, None].repeat(batch_size, axis=1) + if condition_similar_length: + from_noise_levels = np.concatenate([from_noise_levels, np.zeros((condition_similar_length, from_noise_levels.shape[-1]), dtype=np.int32)], axis=0) + to_noise_levels = np.concatenate([to_noise_levels, np.zeros((condition_similar_length, from_noise_levels.shape[-1]), dtype=np.int32)], axis=0) + from_noise_levels = torch.from_numpy(from_noise_levels).to(self.device) + to_noise_levels = torch.from_numpy(to_noise_levels).to(self.device) + return from_noise_levels, to_noise_levels + + def validation_step(self, batch, batch_idx, namespace="validation") -> STEP_OUTPUT: + """ + Perform a single validation step. + + This function processes the input batch, encodes frames, generates predictions using a sliding window approach, + and handles condition similarity logic for sampling. The results are decoded and stored for evaluation. + + Args: + batch: Input batch of data containing frames, conditions, poses, etc. + batch_idx: Index of the current batch. + namespace: Namespace for logging (default: "validation"). + + Returns: + None: Appends the predicted and ground truth frames to `self.validation_step_outputs`. + """ + # Preprocess the input batch + condition_similar_length = self.condition_similar_length + xs_raw, conditions, pose_conditions, c2w_mat, frame_idx = self._preprocess_batch(batch) + + # Encode frames in chunks if necessary + total_frame = xs_raw.shape[0] + if total_frame > 10: + xs = torch.cat([ + self.encode(xs_raw[int(total_frame * i / 10):int(total_frame * (i + 1) / 10)]).cpu() + for i in range(10) + ]) + else: + xs = self.encode(xs_raw).cpu() + + n_frames, batch_size, *_ = xs.shape + curr_frame = 0 + + # Initialize context frames + n_context_frames = self.context_frames // self.frame_stack + xs_pred = xs[:n_context_frames].clone() + curr_frame += n_context_frames + + # Progress bar for sampling + pbar = tqdm(total=n_frames, initial=curr_frame, desc="Sampling") + + while curr_frame < n_frames: + # Determine the horizon for the current chunk + horizon = min(n_frames - curr_frame, self.chunk_size) if self.chunk_size > 0 else n_frames - curr_frame + assert horizon <= self.n_tokens, "Horizon exceeds the number of tokens." + + # Generate scheduling matrix and initialize noise + scheduling_matrix = self._generate_scheduling_matrix(horizon) + chunk = torch.randn((horizon, batch_size, *xs_pred.shape[2:])) + chunk = torch.clamp(chunk, -self.clip_noise, self.clip_noise).to(xs_pred.device) + xs_pred = torch.cat([xs_pred, chunk], 0) + + # Sliding window: only input the last `n_tokens` frames + start_frame = max(0, curr_frame + horizon - self.n_tokens) + pbar.set_postfix({"start": start_frame, "end": curr_frame + horizon}) + + # Handle condition similarity logic + if condition_similar_length: + random_idx = self._generate_condition_indices( + curr_frame, condition_similar_length, xs_pred, pose_conditions, frame_idx + ) + + xs_pred = torch.cat([xs_pred, xs_pred[random_idx[:, range(xs_pred.shape[1])], range(xs_pred.shape[1])].clone()], 0) + + # Prepare input conditions and pose conditions + input_condition, input_pose_condition, frame_idx_list = self._prepare_conditions( + start_frame, curr_frame, horizon, conditions, pose_conditions, c2w_mat, frame_idx, random_idx, + image_width=xs_raw.shape[-1], image_height=xs_raw.shape[-2] + ) + + # Perform sampling for each step in the scheduling matrix + for m in range(scheduling_matrix.shape[0] - 1): + from_noise_levels, to_noise_levels = self._prepare_noise_levels( + scheduling_matrix, m, curr_frame, batch_size, condition_similar_length + ) + + xs_pred[start_frame:] = self.diffusion_model.sample_step( + xs_pred[start_frame:].to(input_condition.device), + input_condition, + input_pose_condition, + from_noise_levels[start_frame:], + to_noise_levels[start_frame:], + current_frame=curr_frame, + mode="validation", + reference_length=condition_similar_length, + frame_idx=frame_idx_list + ).cpu() + + # Remove condition similarity frames if applicable + if condition_similar_length: + xs_pred = xs_pred[:-condition_similar_length] + + curr_frame += horizon + pbar.update(horizon) + + # Decode predictions and ground truth + xs_pred = self.decode(xs_pred[n_context_frames:].to(conditions.device)) + xs_decode = self.decode(xs[n_context_frames:].to(conditions.device)) + + # Store results for evaluation + self.validation_step_outputs.append((xs_pred, xs_decode)) + return + + @torch.no_grad() + def interactive(self, first_frame, new_actions, first_pose, device, + self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx): + + condition_similar_length = self.condition_similar_length + + if self_frames is None: + first_frame = torch.from_numpy(first_frame) + new_actions = torch.from_numpy(new_actions) + first_pose = torch.from_numpy(first_pose) + first_frame_encode = self.encode(first_frame[None, None].to(device)) + self_frames = first_frame_encode.cpu() + self_actions = new_actions[None, None].to(device) + self_poses = first_pose[None, None].to(device) + new_c2w_mat = euler_to_camera_to_world_matrix(first_pose) + self_memory_c2w = new_c2w_mat[None, None].to(device) + self_frame_idx = torch.tensor([[0]]).to(device) + return first_frame.cpu().numpy(), self_frames.cpu().numpy(), self_actions.cpu().numpy(), self_poses.cpu().numpy(), self_memory_c2w.cpu().numpy(), self_frame_idx.cpu().numpy() + else: + self_frames = torch.from_numpy(self_frames) + self_actions = torch.from_numpy(self_actions).to(device) + self_poses = torch.from_numpy(self_poses).to(device) + self_memory_c2w = torch.from_numpy(self_memory_c2w).to(device) + self_frame_idx = torch.from_numpy(self_frame_idx).to(device) + new_actions = new_actions.to(device) + + curr_frame = 0 + horizon = 1 + batch_size = 1 + n_frames = curr_frame + horizon + # context + n_context_frames = len(self_frames) + xs_pred = self_frames[:n_context_frames].clone() + curr_frame += n_context_frames + + pbar = tqdm(total=n_frames, initial=curr_frame, desc="Sampling") + + + for ai in range(len(new_actions)): + + last_frame = xs_pred[-1].clone() + curr_actions = new_actions[ai] + last_pose_condition = self_poses[-1].clone() + last_pose_condition[:,3:] = last_pose_condition[:,3:] // 15 + new_pose_condition_offset = self.pose_prediction_model(last_frame.to(device), curr_actions[None], last_pose_condition) + + new_pose_condition_offset[:,3:] = torch.round(new_pose_condition_offset[:,3:]) + new_pose_condition = last_pose_condition + new_pose_condition_offset + new_pose_condition[:,3:] = new_pose_condition[:,3:] * 15 + new_pose_condition[:,3:] %= 360 + self_actions = torch.cat([self_actions, curr_actions[None, None]]) + self_poses = torch.cat([self_poses, new_pose_condition[None]]) + new_c2w_mat = euler_to_camera_to_world_matrix(new_pose_condition) + self_memory_c2w = torch.cat([self_memory_c2w, new_c2w_mat[None]]) + self_frame_idx = torch.cat([self_frame_idx, torch.tensor([[self_frame_idx[-1,0]+1]]).to(device)]) + + conditions = self_actions.clone() + pose_conditions = self_poses.clone() + c2w_mat = self_memory_c2w .clone() + frame_idx = self_frame_idx.clone() + + # generation on frame + scheduling_matrix = self._generate_scheduling_matrix(horizon) + chunk = torch.randn((horizon, batch_size, *xs_pred.shape[2:])).to(xs_pred.device) + chunk = torch.clamp(chunk, -self.clip_noise, self.clip_noise) + + xs_pred = torch.cat([xs_pred, chunk], 0) + + # sliding window: only input the last n_tokens frames + start_frame = max(0, curr_frame + horizon - self.n_tokens) + + pbar.set_postfix( + { + "start": start_frame, + "end": curr_frame + horizon, + } + ) + + # Handle condition similarity logic + if condition_similar_length: + random_idx = self._generate_condition_indices( + curr_frame, condition_similar_length, xs_pred, pose_conditions, frame_idx + ) + + # random_idx = np.unique(random_idx)[:, None] + # condition_similar_length = len(random_idx) + xs_pred = torch.cat([xs_pred, xs_pred[random_idx[:, range(xs_pred.shape[1])], range(xs_pred.shape[1])].clone()], 0) + + # Prepare input conditions and pose conditions + input_condition, input_pose_condition, frame_idx_list = self._prepare_conditions( + start_frame, curr_frame, horizon, conditions, pose_conditions, c2w_mat, frame_idx, random_idx, + image_width=first_frame.shape[-1], image_height=first_frame.shape[-2] + ) + + # Perform sampling for each step in the scheduling matrix + for m in range(scheduling_matrix.shape[0] - 1): + from_noise_levels, to_noise_levels = self._prepare_noise_levels( + scheduling_matrix, m, curr_frame, batch_size, condition_similar_length + ) + + xs_pred[start_frame:] = self.diffusion_model.sample_step( + xs_pred[start_frame:].to(input_condition.device), + input_condition, + input_pose_condition, + from_noise_levels[start_frame:], + to_noise_levels[start_frame:], + current_frame=curr_frame, + mode="validation", + reference_length=condition_similar_length, + frame_idx=frame_idx_list + ).cpu() + + + if condition_similar_length: + xs_pred = xs_pred[:-condition_similar_length] + + curr_frame += horizon + pbar.update(horizon) + + self_frames = torch.cat([self_frames, xs_pred[n_context_frames:]]) + xs_pred = self.decode(xs_pred[n_context_frames:].to(device)).cpu() + + return xs_pred.cpu().numpy(), self_frames.cpu().numpy(), self_actions.cpu().numpy(), \ + self_poses.cpu().numpy(), self_memory_c2w.cpu().numpy(), self_frame_idx.cpu().numpy() + + + def reset(self): + self_frames = None + self_poses = None + self_memory_c2w = None + self_frame_idx = None \ No newline at end of file diff --git a/algorithms/worldmem/models/attention.py b/algorithms/worldmem/models/attention.py new file mode 100644 index 0000000000000000000000000000000000000000..975eb190707a26757b89f3958f9150a707aa7430 --- /dev/null +++ b/algorithms/worldmem/models/attention.py @@ -0,0 +1,351 @@ +""" +Based on https://github.com/buoyancy99/diffusion-forcing/blob/main/algorithms/diffusion_forcing/models/attention.py +""" + +from typing import Optional +from collections import namedtuple +import torch +from torch import nn +from torch.nn import functional as F +from einops import rearrange +from .rotary_embedding_torch import RotaryEmbedding, apply_rotary_emb +import numpy as np + +def create_attention_bias(f1, f2, device=None, dtype=torch.float32): + f = f1 + f2 + mask = torch.zeros((f, f), dtype=dtype, device=device) + if f1 > 0: + mask[:f1, :f1] = float('-inf') + if f2 > 0: + mask[f1:, f1:] = float('-inf') + return mask + +class TemporalAxialAttention(nn.Module): + def __init__( + self, + dim: int, + heads: int, + dim_head: int, + reference_length: int, + rotary_emb: RotaryEmbedding, + is_causal: bool = True, + is_temporal_independent: bool = False, + use_domain_adapter = False + ): + super().__init__() + self.inner_dim = dim_head * heads + self.heads = heads + self.head_dim = dim_head + self.inner_dim = dim_head * heads + self.to_qkv = nn.Linear(dim, self.inner_dim * 3, bias=False) + + self.use_domain_adapter = use_domain_adapter + if self.use_domain_adapter: + lora_rank = 8 + self.lora_A = nn.Linear(dim, lora_rank, bias=False) + self.lora_B = nn.Linear(lora_rank, self.inner_dim * 3, bias=False) + + self.to_out = nn.Linear(self.inner_dim, dim) + + self.rotary_emb = rotary_emb + self.is_causal = is_causal + self.is_temporal_independent = is_temporal_independent + + self.reference_length = reference_length + + def forward(self, x: torch.Tensor): + B, T, H, W, D = x.shape + + # if T>=9: + # try: + # # x = torch.cat([x[:,:-1],x[:,16-T:17-T],x[:,-1:]], dim=1) + # x = torch.cat([x[:,16-T:17-T],x], dim=1) + # except: + # import pdb;pdb.set_trace() + # print("="*50) + # print(x.shape) + + B, T, H, W, D = x.shape + + q, k, v = self.to_qkv(x).chunk(3, dim=-1) + + if self.use_domain_adapter: + q_lora, k_lora, v_lora = self.lora_B(self.lora_A(x)).chunk(3, dim=-1) + q = q+q_lora + k = k+k_lora + v = v+v_lora + + q = rearrange(q, "B T H W (h d) -> (B H W) h T d", h=self.heads) + k = rearrange(k, "B T H W (h d) -> (B H W) h T d", h=self.heads) + v = rearrange(v, "B T H W (h d) -> (B H W) h T d", h=self.heads) + + q = self.rotary_emb.rotate_queries_or_keys(q, self.rotary_emb.freqs) + k = self.rotary_emb.rotate_queries_or_keys(k, self.rotary_emb.freqs) + + q, k, v = map(lambda t: t.contiguous(), (q, k, v)) + + if self.is_temporal_independent: + attn_bias = torch.ones((T, T), dtype=q.dtype, device=q.device) + attn_bias = attn_bias.masked_fill(attn_bias == 1, float('-inf')) + attn_bias[range(T), range(T)] = 0 + elif self.is_causal: + attn_bias = torch.triu(torch.ones((T, T), dtype=q.dtype, device=q.device), diagonal=1) + attn_bias = attn_bias.masked_fill(attn_bias == 1, float('-inf')) + attn_bias[(T-self.reference_length):] = float('-inf') + attn_bias[range(T), range(T)] = 0 + else: + attn_bias = None + + try: + x = F.scaled_dot_product_attention(query=q, key=k, value=v, attn_mask=attn_bias) + except: + import pdb;pdb.set_trace() + + x = rearrange(x, "(B H W) h T d -> B T H W (h d)", B=B, H=H, W=W) + x = x.to(q.dtype) + + # linear proj + x = self.to_out(x) + + # if T>=10: + # try: + # # x = torch.cat([x[:,:-2],x[:,-1:]], dim=1) + # x = x[:,1:] + # except: + # import pdb;pdb.set_trace() + # print(x.shape) + return x + +class SpatialAxialAttention(nn.Module): + def __init__( + self, + dim: int, + heads: int, + dim_head: int, + rotary_emb: RotaryEmbedding, + use_domain_adapter = False + ): + super().__init__() + self.inner_dim = dim_head * heads + self.heads = heads + self.head_dim = dim_head + self.inner_dim = dim_head * heads + self.to_qkv = nn.Linear(dim, self.inner_dim * 3, bias=False) + self.use_domain_adapter = use_domain_adapter + if self.use_domain_adapter: + lora_rank = 8 + self.lora_A = nn.Linear(dim, lora_rank, bias=False) + self.lora_B = nn.Linear(lora_rank, self.inner_dim * 3, bias=False) + + self.to_out = nn.Linear(self.inner_dim, dim) + + self.rotary_emb = rotary_emb + + def forward(self, x: torch.Tensor): + B, T, H, W, D = x.shape + + q, k, v = self.to_qkv(x).chunk(3, dim=-1) + + if self.use_domain_adapter: + q_lora, k_lora, v_lora = self.lora_B(self.lora_A(x)).chunk(3, dim=-1) + q = q+q_lora + k = k+k_lora + v = v+v_lora + + q = rearrange(q, "B T H W (h d) -> (B T) h H W d", h=self.heads) + k = rearrange(k, "B T H W (h d) -> (B T) h H W d", h=self.heads) + v = rearrange(v, "B T H W (h d) -> (B T) h H W d", h=self.heads) + + freqs = self.rotary_emb.get_axial_freqs(H, W) + q = apply_rotary_emb(freqs, q) + k = apply_rotary_emb(freqs, k) + + # prepare for attn + q = rearrange(q, "(B T) h H W d -> (B T) h (H W) d", B=B, T=T, h=self.heads) + k = rearrange(k, "(B T) h H W d -> (B T) h (H W) d", B=B, T=T, h=self.heads) + v = rearrange(v, "(B T) h H W d -> (B T) h (H W) d", B=B, T=T, h=self.heads) + + x = F.scaled_dot_product_attention(query=q, key=k, value=v, is_causal=False) + + x = rearrange(x, "(B T) h (H W) d -> B T H W (h d)", B=B, H=H, W=W) + x = x.to(q.dtype) + + # linear proj + x = self.to_out(x) + return x + +class MemTemporalAxialAttention(nn.Module): + def __init__( + self, + dim: int, + heads: int, + dim_head: int, + rotary_emb: RotaryEmbedding, + is_causal: bool = True, + ): + super().__init__() + self.inner_dim = dim_head * heads + self.heads = heads + self.head_dim = dim_head + self.inner_dim = dim_head * heads + self.to_qkv = nn.Linear(dim, self.inner_dim * 3, bias=False) + self.to_out = nn.Linear(self.inner_dim, dim) + + self.rotary_emb = rotary_emb + self.is_causal = is_causal + + self.reference_length = 3 + + def forward(self, x: torch.Tensor): + B, T, H, W, D = x.shape + + q, k, v = self.to_qkv(x).chunk(3, dim=-1) + + + q = rearrange(q, "B T H W (h d) -> (B H W) h T d", h=self.heads) + k = rearrange(k, "B T H W (h d) -> (B H W) h T d", h=self.heads) + v = rearrange(v, "B T H W (h d) -> (B H W) h T d", h=self.heads) + + + + # q = self.rotary_emb.rotate_queries_or_keys(q, self.rotary_emb.freqs) + # k = self.rotary_emb.rotate_queries_or_keys(k, self.rotary_emb.freqs) + + q, k, v = map(lambda t: t.contiguous(), (q, k, v)) + + # if T == 21000: + # # 手动计算缩放点积分数 + # _, _, _, d_k = q.shape + # scores = torch.einsum("b h n d, b h m d -> b h n m", q, k) / (d_k ** 0.5) # Shape: (B, T_q, T_k) + + # # 计算注意力图 (Attention Map) + # attention_map = F.softmax(scores, dim=-1) # Shape: (B, T_q, T_k) + # b_, h_, n_, m_ = attention_map.shape + # attention_map = attention_map.reshape(1, int(np.sqrt(b_/1)), int(np.sqrt(b_/1)), h_, n_, m_) + # attention_map = attention_map.mean(3) + + # attn_bias = torch.zeros((T, T), dtype=q.dtype, device=q.device) + # T_origin = T - self.reference_length + # attn_bias[:T_origin, T_origin:] = 1 + # attn_bias[range(T), range(T)] = 1 + + # attention_map = attention_map * attn_bias + + # # print 注意力图 + # import matplotlib.pyplot as plt + # fig, axes = plt.subplots(21000, 21000, figsize=(9, 9)) # 调整figsize以适配图像大小 + + # # 遍历3*3维度 + # for i in range(21000): + # for j in range(21000): + # # 取出第(i, j)个子图像 + # img = attention_map[0, :, :, i, j].cpu().numpy() + # axes[i, j].imshow(img, cmap='viridis') # 可以自定义cmap + # axes[i, j].axis('off') # 隐藏坐标轴 + + # # 调整子图间距 + # plt.tight_layout() + # plt.savefig('attention_map.png') + # import pdb; pdb.set_trace() + # plt.close() + + attn_bias = torch.zeros((T, T), dtype=q.dtype, device=q.device) + attn_bias = attn_bias.masked_fill(attn_bias == 0, float('-inf')) + T_origin = T - self.reference_length + attn_bias[:T_origin, T_origin:] = 0 + attn_bias[range(T), range(T)] = 0 + + # if T==121000: + # import pdb;pdb.set_trace() + + try: + x = F.scaled_dot_product_attention(query=q, key=k, value=v, attn_mask=attn_bias) + except: + import pdb;pdb.set_trace() + + x = rearrange(x, "(B H W) h T d -> B T H W (h d)", B=B, H=H, W=W) + x = x.to(q.dtype) + + # linear proj + x = self.to_out(x) + return x + +class MemFullAttention(nn.Module): + def __init__( + self, + dim: int, + heads: int, + dim_head: int, + reference_length: int, + rotary_emb: RotaryEmbedding, + is_causal: bool = True + ): + super().__init__() + self.inner_dim = dim_head * heads + self.heads = heads + self.head_dim = dim_head + self.inner_dim = dim_head * heads + self.to_qkv = nn.Linear(dim, self.inner_dim * 3, bias=False) + self.to_out = nn.Linear(self.inner_dim, dim) + + self.rotary_emb = rotary_emb + self.is_causal = is_causal + + self.reference_length = reference_length + + self.store = None + + def forward(self, x: torch.Tensor, relative_embedding=False, + extra_condition=None, + cond_only_on_qk=False, + reference_length=None): + + B, T, H, W, D = x.shape + + if cond_only_on_qk: + q, k, _ = self.to_qkv(x+extra_condition).chunk(3, dim=-1) + _, _, v = self.to_qkv(x).chunk(3, dim=-1) + else: + q, k, v = self.to_qkv(x).chunk(3, dim=-1) + + if relative_embedding: + length = reference_length+1 + n_frames = T // length + x = x.reshape(B, n_frames, length, H, W, D) + + x_list = [] + + for i in range(n_frames): + if i == n_frames-1: + q_i = rearrange(q[:, i*length:], "B T H W (h d) -> B h (T H W) d", h=self.heads) + k_i = rearrange(k[:, i*length+1:(i+1)*length], "B T H W (h d) -> B h (T H W) d", h=self.heads) + v_i = rearrange(v[:, i*length+1:(i+1)*length], "B T H W (h d) -> B h (T H W) d", h=self.heads) + else: + q_i = rearrange(q[:, i*length:i*length+1], "B T H W (h d) -> B h (T H W) d", h=self.heads) + k_i = rearrange(k[:, i*length+1:(i+1)*length], "B T H W (h d) -> B h (T H W) d", h=self.heads) + v_i = rearrange(v[:, i*length+1:(i+1)*length], "B T H W (h d) -> B h (T H W) d", h=self.heads) + + q_i, k_i, v_i = map(lambda t: t.contiguous(), (q_i, k_i, v_i)) + x_i = F.scaled_dot_product_attention(query=q_i, key=k_i, value=v_i) + x_i = rearrange(x_i, "B h (T H W) d -> B T H W (h d)", B=B, H=H, W=W) + x_i = x_i.to(q.dtype) + x_list.append(x_i) + + x = torch.cat(x_list, dim=1) + + + else: + T_ = T - reference_length + q = rearrange(q, "B T H W (h d) -> B h (T H W) d", h=self.heads) + k = rearrange(k[:, T_:], "B T H W (h d) -> B h (T H W) d", h=self.heads) + v = rearrange(v[:, T_:], "B T H W (h d) -> B h (T H W) d", h=self.heads) + + q, k, v = map(lambda t: t.contiguous(), (q, k, v)) + x = F.scaled_dot_product_attention(query=q, key=k, value=v) + x = rearrange(x, "B h (T H W) d -> B T H W (h d)", B=B, H=H, W=W) + x = x.to(q.dtype) + + # linear proj + x = self.to_out(x) + + return x diff --git a/algorithms/worldmem/models/cameractrl_module.py b/algorithms/worldmem/models/cameractrl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..2bac51c45c6bcc11d46e453c7d2a8c4141337a01 --- /dev/null +++ b/algorithms/worldmem/models/cameractrl_module.py @@ -0,0 +1,12 @@ +import torch.nn as nn +class SimpleCameraPoseEncoder(nn.Module): + def __init__(self, c_in, c_out, hidden_dim=128): + super(SimpleCameraPoseEncoder, self).__init__() + self.model = nn.Sequential( + nn.Linear(c_in, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, c_out) + ) + def forward(self, x): + return self.model(x) + diff --git a/algorithms/worldmem/models/diffusion.py b/algorithms/worldmem/models/diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..ccaa251491c70d54f634985349c73cf4230d9ecc --- /dev/null +++ b/algorithms/worldmem/models/diffusion.py @@ -0,0 +1,520 @@ +from typing import Optional, Callable +from collections import namedtuple +from omegaconf import DictConfig +import torch +from torch import nn +from torch.nn import functional as F +from einops import rearrange +from .utils import linear_beta_schedule, cosine_beta_schedule, sigmoid_beta_schedule, extract +from .dit import DiT_models + +ModelPrediction = namedtuple("ModelPrediction", ["pred_noise", "pred_x_start", "model_out"]) + + +class Diffusion(nn.Module): + # Special thanks to lucidrains for the implementation of the base Diffusion model + # https://github.com/lucidrains/denoising-diffusion-pytorch + + def __init__( + self, + x_shape: torch.Size, + reference_length: int, + action_cond_dim: int, + pose_cond_dim, + is_causal: bool, + cfg: DictConfig, + is_dit: bool=False, + use_plucker=False, + relative_embedding=False, + cond_only_on_qk=False, + use_reference_attention=False, + add_frame_timestep_embedder=False, + ref_mode='sequential' + ): + super().__init__() + self.cfg = cfg + + self.x_shape = x_shape + self.action_cond_dim = action_cond_dim + self.timesteps = cfg.timesteps + self.sampling_timesteps = cfg.sampling_timesteps + self.beta_schedule = cfg.beta_schedule + self.schedule_fn_kwargs = cfg.schedule_fn_kwargs + self.objective = cfg.objective + self.use_fused_snr = cfg.use_fused_snr + self.snr_clip = cfg.snr_clip + self.cum_snr_decay = cfg.cum_snr_decay + self.ddim_sampling_eta = cfg.ddim_sampling_eta + self.clip_noise = cfg.clip_noise + self.arch = cfg.architecture + self.stabilization_level = cfg.stabilization_level + self.is_causal = is_causal + self.is_dit = is_dit + self.reference_length = reference_length + self.pose_cond_dim = pose_cond_dim + self.use_plucker = use_plucker + self.relative_embedding = relative_embedding + self.cond_only_on_qk = cond_only_on_qk + self.use_reference_attention = use_reference_attention + self.add_frame_timestep_embedder = add_frame_timestep_embedder + self.ref_mode = ref_mode + + self._build_model() + self._build_buffer() + + def _build_model(self): + x_channel = self.x_shape[0] + if self.is_dit: + self.model = DiT_models["DiT-S/2"](action_cond_dim=self.action_cond_dim, + pose_cond_dim=self.pose_cond_dim, reference_length=self.reference_length, + use_plucker=self.use_plucker, + relative_embedding=self.relative_embedding, + cond_only_on_qk=self.cond_only_on_qk, + use_reference_attention=self.use_reference_attention, + add_frame_timestep_embedder=self.add_frame_timestep_embedder, + ref_mode=self.ref_mode) + else: + raise NotImplementedError + + def _build_buffer(self): + if self.beta_schedule == "linear": + beta_schedule_fn = linear_beta_schedule + elif self.beta_schedule == "cosine": + beta_schedule_fn = cosine_beta_schedule + elif self.beta_schedule == "sigmoid": + beta_schedule_fn = sigmoid_beta_schedule + else: + raise ValueError(f"unknown beta schedule {self.beta_schedule}") + + betas = beta_schedule_fn(self.timesteps, **self.schedule_fn_kwargs) + + alphas = 1.0 - betas + alphas_cumprod = torch.cumprod(alphas, dim=0) + alphas_cumprod_prev = F.pad(alphas_cumprod[:-1], (1, 0), value=1.0) + + # sampling related parameters + assert self.sampling_timesteps <= self.timesteps + self.is_ddim_sampling = self.sampling_timesteps < self.timesteps + + # helper function to register buffer from float64 to float32 + register_buffer = lambda name, val: self.register_buffer(name, val.to(torch.float32)) + + register_buffer("betas", betas) + register_buffer("alphas_cumprod", alphas_cumprod) + register_buffer("alphas_cumprod_prev", alphas_cumprod_prev) + + # calculations for diffusion q(x_t | x_{t-1}) and others + + register_buffer("sqrt_alphas_cumprod", torch.sqrt(alphas_cumprod)) + register_buffer("sqrt_one_minus_alphas_cumprod", torch.sqrt(1.0 - alphas_cumprod)) + register_buffer("log_one_minus_alphas_cumprod", torch.log(1.0 - alphas_cumprod)) + register_buffer("sqrt_recip_alphas_cumprod", torch.sqrt(1.0 / alphas_cumprod)) + register_buffer("sqrt_recipm1_alphas_cumprod", torch.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) + + # above: equal to 1. / (1. / (1. - alpha_cumprod_tm1) + alpha_t / beta_t) + + register_buffer("posterior_variance", posterior_variance) + + # below: log calculation clipped because the posterior variance is 0 at the beginning of the diffusion chain + + register_buffer( + "posterior_log_variance_clipped", + torch.log(posterior_variance.clamp(min=1e-20)), + ) + register_buffer( + "posterior_mean_coef1", + betas * torch.sqrt(alphas_cumprod_prev) / (1.0 - alphas_cumprod), + ) + register_buffer( + "posterior_mean_coef2", + (1.0 - alphas_cumprod_prev) * torch.sqrt(alphas) / (1.0 - alphas_cumprod), + ) + + # calculate p2 reweighting + + # register_buffer( + # "p2_loss_weight", + # (self.p2_loss_weight_k + alphas_cumprod / (1 - alphas_cumprod)) + # ** -self.p2_loss_weight_gamma, + # ) + + # derive loss weight + # https://arxiv.org/abs/2303.09556 + # snr: signal noise ratio + snr = alphas_cumprod / (1 - alphas_cumprod) + clipped_snr = snr.clone() + clipped_snr.clamp_(max=self.snr_clip) + + register_buffer("clipped_snr", clipped_snr) + register_buffer("snr", snr) + + def add_shape_channels(self, x): + return rearrange(x, f"... -> ...{' 1' * len(self.x_shape)}") + + def model_predictions(self, x, t, action_cond=None, current_frame=None, + pose_cond=None, mode="training", reference_length=None, frame_idx=None): + x = x.permute(1,0,2,3,4) + action_cond = action_cond.permute(1,0,2) + if pose_cond is not None and pose_cond[0] is not None: + try: + pose_cond = pose_cond.permute(1,0,2) + except: + pass + t = t.permute(1,0) + model_output = self.model(x, t, action_cond, current_frame=current_frame, pose_cond=pose_cond, + mode=mode, reference_length=reference_length, frame_idx=frame_idx) + model_output = model_output.permute(1,0,2,3,4) + x = x.permute(1,0,2,3,4) + t = t.permute(1,0) + + if self.objective == "pred_noise": + pred_noise = torch.clamp(model_output, -self.clip_noise, self.clip_noise) + x_start = self.predict_start_from_noise(x, t, pred_noise) + + elif self.objective == "pred_x0": + x_start = model_output + pred_noise = self.predict_noise_from_start(x, t, x_start) + + elif self.objective == "pred_v": + v = model_output + x_start = self.predict_start_from_v(x, t, v) + pred_noise = self.predict_noise_from_start(x, t, x_start) + + + return ModelPrediction(pred_noise, x_start, model_output) + + def predict_start_from_noise(self, x_t, t, noise): + return ( + extract(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t + - extract(self.sqrt_recipm1_alphas_cumprod, t, x_t.shape) * noise + ) + + def predict_noise_from_start(self, x_t, t, x0): + return (extract(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t - x0) / extract( + self.sqrt_recipm1_alphas_cumprod, t, x_t.shape + ) + + def predict_v(self, x_start, t, noise): + return ( + extract(self.sqrt_alphas_cumprod, t, x_start.shape) * noise + - extract(self.sqrt_one_minus_alphas_cumprod, t, x_start.shape) * x_start + ) + + def predict_start_from_v(self, x_t, t, v): + return ( + extract(self.sqrt_alphas_cumprod, t, x_t.shape) * x_t + - extract(self.sqrt_one_minus_alphas_cumprod, t, x_t.shape) * v + ) + + def q_mean_variance(self, x_start, t): + mean = extract(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start + variance = extract(1.0 - self.alphas_cumprod, t, x_start.shape) + log_variance = extract(self.log_one_minus_alphas_cumprod, t, x_start.shape) + return mean, variance, log_variance + + def q_posterior(self, x_start, x_t, t): + posterior_mean = ( + extract(self.posterior_mean_coef1, t, x_t.shape) * x_start + + extract(self.posterior_mean_coef2, t, x_t.shape) * x_t + ) + posterior_variance = extract(self.posterior_variance, t, x_t.shape) + posterior_log_variance_clipped = extract(self.posterior_log_variance_clipped, t, x_t.shape) + return posterior_mean, posterior_variance, posterior_log_variance_clipped + + def q_sample(self, x_start, t, noise=None): + if noise is None: + noise = torch.randn_like(x_start) + noise = torch.clamp(noise, -self.clip_noise, self.clip_noise) + return ( + extract(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start + + extract(self.sqrt_one_minus_alphas_cumprod, t, x_start.shape) * noise + ) + + def p_mean_variance(self, x, t, action_cond=None, pose_cond=None, reference_length=None): + model_pred = self.model_predictions(x=x, t=t, action_cond=action_cond, + pose_cond=pose_cond, reference_length=reference_length, + frame_idx=frame_idx) + x_start = model_pred.pred_x_start + return self.q_posterior(x_start=x_start, x_t=x, t=t) + + def compute_loss_weights(self, noise_levels: torch.Tensor): + + snr = self.snr[noise_levels] + clipped_snr = self.clipped_snr[noise_levels] + normalized_clipped_snr = clipped_snr / self.snr_clip + normalized_snr = snr / self.snr_clip + + if not self.use_fused_snr: + # min SNR reweighting + match self.objective: + case "pred_noise": + return clipped_snr / snr + case "pred_x0": + return clipped_snr + case "pred_v": + return clipped_snr / (snr + 1) + + cum_snr = torch.zeros_like(normalized_snr) + for t in range(0, noise_levels.shape[0]): + if t == 0: + cum_snr[t] = normalized_clipped_snr[t] + else: + cum_snr[t] = self.cum_snr_decay * cum_snr[t - 1] + (1 - self.cum_snr_decay) * normalized_clipped_snr[t] + + cum_snr = F.pad(cum_snr[:-1], (0, 0, 1, 0), value=0.0) + clipped_fused_snr = 1 - (1 - cum_snr * self.cum_snr_decay) * (1 - normalized_clipped_snr) + fused_snr = 1 - (1 - cum_snr * self.cum_snr_decay) * (1 - normalized_snr) + + match self.objective: + case "pred_noise": + return clipped_fused_snr / fused_snr + case "pred_x0": + return clipped_fused_snr * self.snr_clip + case "pred_v": + return clipped_fused_snr * self.snr_clip / (fused_snr * self.snr_clip + 1) + case _: + raise ValueError(f"unknown objective {self.objective}") + + def forward( + self, + x: torch.Tensor, + action_cond: Optional[torch.Tensor], + pose_cond, + noise_levels: torch.Tensor, + reference_length, + frame_idx=None + ): + noise = torch.randn_like(x) + noise = torch.clamp(noise, -self.clip_noise, self.clip_noise) + + noised_x = self.q_sample(x_start=x, t=noise_levels, noise=noise) + + model_pred = self.model_predictions(x=noised_x, t=noise_levels, action_cond=action_cond, + pose_cond=pose_cond,reference_length=reference_length, frame_idx=frame_idx) + + pred = model_pred.model_out + x_pred = model_pred.pred_x_start + + if self.objective == "pred_noise": + target = noise + elif self.objective == "pred_x0": + target = x + elif self.objective == "pred_v": + target = self.predict_v(x, noise_levels, noise) + else: + raise ValueError(f"unknown objective {self.objective}") + + # 训练的时候每个frame随便给噪声 + loss = F.mse_loss(pred, target.detach(), reduction="none") + loss_weight = self.compute_loss_weights(noise_levels) + + loss_weight = loss_weight.view(*loss_weight.shape, *((1,) * (loss.ndim - 2))) + + loss = loss * loss_weight + + return x_pred, loss + + def sample_step( + self, + x: torch.Tensor, + action_cond: Optional[torch.Tensor], + pose_cond, + curr_noise_level: torch.Tensor, + next_noise_level: torch.Tensor, + guidance_fn: Optional[Callable] = None, + current_frame=None, + mode="training", + reference_length=None, + frame_idx=None + ): + real_steps = torch.linspace(-1, self.timesteps - 1, steps=self.sampling_timesteps + 1, device=x.device).long() + + # convert noise levels (0 ~ sampling_timesteps) to real noise levels (-1 ~ timesteps - 1) + curr_noise_level = real_steps[curr_noise_level] + next_noise_level = real_steps[next_noise_level] + + if self.is_ddim_sampling: + return self.ddim_sample_step( + x=x, + action_cond=action_cond, + pose_cond=pose_cond, + curr_noise_level=curr_noise_level, + next_noise_level=next_noise_level, + guidance_fn=guidance_fn, + current_frame=current_frame, + mode=mode, + reference_length=reference_length, + frame_idx=frame_idx + ) + + # FIXME: temporary code for checking ddpm sampling + assert torch.all( + (curr_noise_level - 1 == next_noise_level) | ((curr_noise_level == -1) & (next_noise_level == -1)) + ), "Wrong noise level given for ddpm sampling." + + assert ( + self.sampling_timesteps == self.timesteps + ), "sampling_timesteps should be equal to timesteps for ddpm sampling." + + return self.ddpm_sample_step( + x=x, + action_cond=action_cond, + pose_cond=pose_cond, + curr_noise_level=curr_noise_level, + guidance_fn=guidance_fn, + reference_length=reference_length, + frame_idx=frame_idx + ) + + def ddpm_sample_step( + self, + x: torch.Tensor, + action_cond: Optional[torch.Tensor], + pose_cond, + curr_noise_level: torch.Tensor, + guidance_fn: Optional[Callable] = None, + reference_length=None, + frame_idx=None, + ): + clipped_curr_noise_level = torch.where( + curr_noise_level < 0, + torch.full_like(curr_noise_level, self.stabilization_level - 1, dtype=torch.long), + curr_noise_level, + ) + + # treating as stabilization would require us to scale with sqrt of alpha_cum + orig_x = x.clone().detach() + scaled_context = self.q_sample( + x, + clipped_curr_noise_level, + noise=torch.zeros_like(x), + ) + x = torch.where(self.add_shape_channels(curr_noise_level < 0), scaled_context, orig_x) + + if guidance_fn is not None: + raise NotImplementedError("Guidance function is not implemented for ddpm sampling yet.") + + else: + model_mean, _, model_log_variance = self.p_mean_variance( + x=x, + t=clipped_curr_noise_level, + action_cond=action_cond, + pose_cond=pose_cond, + reference_length=reference_length, + frame_idx=frame_idx + ) + + noise = torch.where( + self.add_shape_channels(clipped_curr_noise_level > 0), + torch.randn_like(x), + 0, + ) + noise = torch.clamp(noise, -self.clip_noise, self.clip_noise) + x_pred = model_mean + torch.exp(0.5 * model_log_variance) * noise + + # only update frames where the noise level decreases + return torch.where(self.add_shape_channels(curr_noise_level == -1), orig_x, x_pred) + + def ddim_sample_step( + self, + x: torch.Tensor, + action_cond: Optional[torch.Tensor], + pose_cond, + curr_noise_level: torch.Tensor, + next_noise_level: torch.Tensor, + guidance_fn: Optional[Callable] = None, + current_frame=None, + mode="training", + reference_length=None, + frame_idx=None + ): + # convert noise level -1 to self.stabilization_level - 1 + clipped_curr_noise_level = torch.where( + curr_noise_level < 0, + torch.full_like(curr_noise_level, self.stabilization_level - 1, dtype=torch.long), + curr_noise_level, + ) + + # treating as stabilization would require us to scale with sqrt of alpha_cum + orig_x = x.clone().detach() + scaled_context = self.q_sample( + x, + clipped_curr_noise_level, + noise=torch.zeros_like(x), + ) + x = torch.where(self.add_shape_channels(curr_noise_level < 0), scaled_context, orig_x) + + alpha = self.alphas_cumprod[clipped_curr_noise_level] + alpha_next = torch.where( + next_noise_level < 0, + torch.ones_like(next_noise_level), + self.alphas_cumprod[next_noise_level], + ) + sigma = torch.where( + next_noise_level < 0, + torch.zeros_like(next_noise_level), + self.ddim_sampling_eta * ((1 - alpha / alpha_next) * (1 - alpha_next) / (1 - alpha)).sqrt(), + ) + c = (1 - alpha_next - sigma**2).sqrt() + + alpha_next = self.add_shape_channels(alpha_next) + c = self.add_shape_channels(c) + sigma = self.add_shape_channels(sigma) + + if guidance_fn is not None: + with torch.enable_grad(): + x = x.detach().requires_grad_() + + model_pred = self.model_predictions( + x=x, + t=clipped_curr_noise_level, + action_cond=action_cond, + pose_cond=pose_cond, + current_frame=current_frame, + mode=mode, + reference_length=reference_length, + frame_idx=frame_idx + ) + + guidance_loss = guidance_fn(model_pred.pred_x_start) + grad = -torch.autograd.grad( + guidance_loss, + x, + )[0] + + pred_noise = model_pred.pred_noise + (1 - alpha_next).sqrt() * grad + x_start = self.predict_start_from_noise(x, clipped_curr_noise_level, pred_noise) + + else: + # print(clipped_curr_noise_level) + model_pred = self.model_predictions( + x=x, + t=clipped_curr_noise_level, + action_cond=action_cond, + pose_cond=pose_cond, + current_frame=current_frame, + mode=mode, + reference_length=reference_length, + frame_idx=frame_idx + ) + x_start = model_pred.pred_x_start + pred_noise = model_pred.pred_noise + + noise = torch.randn_like(x) + noise = torch.clamp(noise, -self.clip_noise, self.clip_noise) + + x_pred = x_start * alpha_next.sqrt() + pred_noise * c + sigma * noise + + # only update frames where the noise level decreases + mask = curr_noise_level == next_noise_level + x_pred = torch.where( + self.add_shape_channels(mask), + orig_x, + x_pred, + ) + + return x_pred diff --git a/algorithms/worldmem/models/dit.py b/algorithms/worldmem/models/dit.py new file mode 100644 index 0000000000000000000000000000000000000000..b04e25e950208ad4baf70446541d16afea3d1dd7 --- /dev/null +++ b/algorithms/worldmem/models/dit.py @@ -0,0 +1,577 @@ +""" +References: + - DiT: https://github.com/facebookresearch/DiT/blob/main/models.py + - Diffusion Forcing: https://github.com/buoyancy99/diffusion-forcing/blob/main/algorithms/diffusion_forcing/models/unet3d.py + - Latte: https://github.com/Vchitect/Latte/blob/main/models/latte.py +""" + +from typing import Optional, Literal +import torch +from torch import nn +from .rotary_embedding_torch import RotaryEmbedding +from einops import rearrange +from .attention import SpatialAxialAttention, TemporalAxialAttention, MemTemporalAxialAttention, MemFullAttention +from timm.models.vision_transformer import Mlp +from timm.layers.helpers import to_2tuple +import math +from collections import namedtuple +from typing import Optional, Callable +from .cameractrl_module import SimpleCameraPoseEncoder + +def modulate(x, shift, scale): + fixed_dims = [1] * len(shift.shape[1:]) + shift = shift.repeat(x.shape[0] // shift.shape[0], *fixed_dims) + scale = scale.repeat(x.shape[0] // scale.shape[0], *fixed_dims) + while shift.dim() < x.dim(): + shift = shift.unsqueeze(-2) + scale = scale.unsqueeze(-2) + return x * (1 + scale) + shift + +def gate(x, g): + fixed_dims = [1] * len(g.shape[1:]) + g = g.repeat(x.shape[0] // g.shape[0], *fixed_dims) + while g.dim() < x.dim(): + g = g.unsqueeze(-2) + return g * x + + +class PatchEmbed(nn.Module): + """2D Image to Patch Embedding""" + + def __init__( + self, + img_height=256, + img_width=256, + patch_size=16, + in_chans=3, + embed_dim=768, + norm_layer=None, + flatten=True, + ): + super().__init__() + img_size = (img_height, img_width) + patch_size = to_2tuple(patch_size) + self.img_size = img_size + self.patch_size = patch_size + self.grid_size = (img_size[0] // patch_size[0], img_size[1] // patch_size[1]) + self.num_patches = self.grid_size[0] * self.grid_size[1] + self.flatten = flatten + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) + self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity() + + def forward(self, x, random_sample=False): + B, C, H, W = x.shape + assert random_sample or (H == self.img_size[0] and W == self.img_size[1]), f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})." + + x = self.proj(x) + if self.flatten: + x = rearrange(x, "B C H W -> B (H W) C") + else: + x = rearrange(x, "B C H W -> B H W C") + x = self.norm(x) + return x + + +class TimestepEmbedder(nn.Module): + """ + Embeds scalar timesteps into vector representations. + """ + + def __init__(self, hidden_size, frequency_embedding_size=256, freq_type='time_step'): + super().__init__() + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True), # hidden_size is diffusion model hidden size + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True), + ) + self.frequency_embedding_size = frequency_embedding_size + self.freq_type = freq_type + + @staticmethod + def timestep_embedding(t, dim, max_period=10000, freq_type='time_step'): + """ + Create sinusoidal timestep embeddings. + :param t: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an (N, D) Tensor of positional embeddings. + """ + # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py + half = dim // 2 + + if freq_type == 'time_step': + freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to(device=t.device) + elif freq_type == 'spatial': # ~(-5 5) + freqs = torch.linspace(1.0, half, half).to(device=t.device) * torch.pi + elif freq_type == 'angle': # 0-360 + freqs = torch.linspace(1.0, half, half).to(device=t.device) * torch.pi / 180 + + + args = t[:, None].float() * freqs[None] + + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) + return embedding + + def forward(self, t): + t_freq = self.timestep_embedding(t, self.frequency_embedding_size, freq_type=self.freq_type) + t_emb = self.mlp(t_freq) + return t_emb + + +class FinalLayer(nn.Module): + """ + The final layer of DiT. + """ + + def __init__(self, hidden_size, patch_size, out_channels): + super().__init__() + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True) + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True)) + + def forward(self, x, c): + shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1) + x = modulate(self.norm_final(x), shift, scale) + x = self.linear(x) + return x + + +class SpatioTemporalDiTBlock(nn.Module): + def __init__( + self, + hidden_size, + num_heads, + reference_length, + mlp_ratio=4.0, + is_causal=True, + spatial_rotary_emb: Optional[RotaryEmbedding] = None, + temporal_rotary_emb: Optional[RotaryEmbedding] = None, + reference_rotary_emb=None, + use_plucker=False, + relative_embedding=False, + cond_only_on_qk=False, + use_reference_attention=False, + ref_mode='sequential' + ): + super().__init__() + self.is_causal = is_causal + mlp_hidden_dim = int(hidden_size * mlp_ratio) + approx_gelu = lambda: nn.GELU(approximate="tanh") + + self.s_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.s_attn = SpatialAxialAttention( + hidden_size, + heads=num_heads, + dim_head=hidden_size // num_heads, + rotary_emb=spatial_rotary_emb + ) + self.s_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.s_mlp = Mlp( + in_features=hidden_size, + hidden_features=mlp_hidden_dim, + act_layer=approx_gelu, + drop=0, + ) + self.s_adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True)) + + self.t_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.t_attn = TemporalAxialAttention( + hidden_size, + heads=num_heads, + dim_head=hidden_size // num_heads, + is_causal=is_causal, + rotary_emb=temporal_rotary_emb, + reference_length=reference_length + ) + self.t_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.t_mlp = Mlp( + in_features=hidden_size, + hidden_features=mlp_hidden_dim, + act_layer=approx_gelu, + drop=0, + ) + self.t_adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True)) + + self.use_reference_attention = use_reference_attention + if self.use_reference_attention: + self.r_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.ref_type = "full_ref" + if self.ref_type == "temporal_ref": + self.r_attn = MemTemporalAxialAttention( + hidden_size, + heads=num_heads, + dim_head=hidden_size // num_heads, + is_causal=is_causal, + rotary_emb=None + ) + elif self.ref_type == "full_ref": + self.r_attn = MemFullAttention( + hidden_size, + heads=num_heads, + dim_head=hidden_size // num_heads, + is_causal=is_causal, + rotary_emb=reference_rotary_emb, + reference_length=reference_length + ) + self.r_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.r_mlp = Mlp( + in_features=hidden_size, + hidden_features=mlp_hidden_dim, + act_layer=approx_gelu, + drop=0, + ) + self.r_adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True)) + + self.use_plucker = use_plucker + if use_plucker: + self.pose_cond_mlp = nn.Linear(hidden_size, hidden_size) + self.temporal_pose_cond_mlp = nn.Linear(hidden_size, hidden_size) + + self.reference_length = reference_length + self.relative_embedding = relative_embedding + self.cond_only_on_qk = cond_only_on_qk + + self.ref_mode = ref_mode + + if self.ref_mode == 'parallel': + self.parallel_map = nn.Linear(hidden_size, hidden_size) + + def forward(self, x, c, current_frame=None, timestep=None, is_last_block=False, + pose_cond=None, mode="training", c_action_cond=None, reference_length=None): + B, T, H, W, D = x.shape + + # spatial block + + s_shift_msa, s_scale_msa, s_gate_msa, s_shift_mlp, s_scale_mlp, s_gate_mlp = self.s_adaLN_modulation(c).chunk(6, dim=-1) + x = x + gate(self.s_attn(modulate(self.s_norm1(x), s_shift_msa, s_scale_msa)), s_gate_msa) + x = x + gate(self.s_mlp(modulate(self.s_norm2(x), s_shift_mlp, s_scale_mlp)), s_gate_mlp) + + # temporal block + if c_action_cond is not None: + t_shift_msa, t_scale_msa, t_gate_msa, t_shift_mlp, t_scale_mlp, t_gate_mlp = self.t_adaLN_modulation(c_action_cond).chunk(6, dim=-1) + else: + t_shift_msa, t_scale_msa, t_gate_msa, t_shift_mlp, t_scale_mlp, t_gate_mlp = self.t_adaLN_modulation(c).chunk(6, dim=-1) + + x_t = x + gate(self.t_attn(modulate(self.t_norm1(x), t_shift_msa, t_scale_msa)), t_gate_msa) + x_t = x_t + gate(self.t_mlp(modulate(self.t_norm2(x_t), t_shift_mlp, t_scale_mlp)), t_gate_mlp) + + if self.ref_mode == 'sequential': + x = x_t + + # memory block + relative_embedding = self.relative_embedding # and mode == "training" + + if self.use_reference_attention: + r_shift_msa, r_scale_msa, r_gate_msa, r_shift_mlp, r_scale_mlp, r_gate_mlp = self.r_adaLN_modulation(c).chunk(6, dim=-1) + + if pose_cond is not None: + if self.use_plucker: + input_cond = self.pose_cond_mlp(pose_cond) + + if relative_embedding: + n_frames = x.shape[1] - reference_length + x1_relative_embedding = [] + r_shift_msa_relative_embedding = [] + r_scale_msa_relative_embedding = [] + for i in range(n_frames): + x1_relative_embedding.append(torch.cat([x[:,i:i+1], x[:, -reference_length:]], dim=1).clone()) + r_shift_msa_relative_embedding.append(torch.cat([r_shift_msa[:,i:i+1], r_shift_msa[:, -reference_length:]], dim=1).clone()) + r_scale_msa_relative_embedding.append(torch.cat([r_scale_msa[:,i:i+1], r_scale_msa[:, -reference_length:]], dim=1).clone()) + x1_zero_frame = torch.cat(x1_relative_embedding, dim=1) + r_shift_msa = torch.cat(r_shift_msa_relative_embedding, dim=1) + r_scale_msa = torch.cat(r_scale_msa_relative_embedding, dim=1) + + # if current_frame == 18: + # import pdb;pdb.set_trace() + + if self.cond_only_on_qk: + attn_input = x1_zero_frame + extra_condition = input_cond + else: + attn_input = input_cond + x1_zero_frame + extra_condition = None + else: + attn_input = input_cond + x + extra_condition = None + # print("input_cond2:", input_cond.abs().mean()) + # print("c:", c.abs().mean()) + # input_cond = x1 + + x = x + gate(self.r_attn(modulate(self.r_norm1(attn_input), r_shift_msa, r_scale_msa), + relative_embedding=relative_embedding, + extra_condition=extra_condition, + cond_only_on_qk=self.cond_only_on_qk, + reference_length=reference_length), r_gate_msa) + else: + # pose_cond *= 0 + x = x + gate(self.r_attn(modulate(self.r_norm1(x+pose_cond[:,:,None, None]), r_shift_msa, r_scale_msa), + current_frame=current_frame, timestep=timestep, + is_last_block=is_last_block, + reference_length=reference_length), r_gate_msa) + else: + x = x + gate(self.r_attn(modulate(self.r_norm1(x), r_shift_msa, r_scale_msa), current_frame=current_frame, timestep=timestep, + is_last_block=is_last_block), r_gate_msa) + + x = x + gate(self.r_mlp(modulate(self.r_norm2(x), r_shift_mlp, r_scale_mlp)), r_gate_mlp) + + if self.ref_mode == 'parallel': + x = x_t + self.parallel_map(x) + + return x + + # print((x1-x2).abs().sum()) + # r_shift_msa, r_scale_msa, r_gate_msa, r_shift_mlp, r_scale_mlp, r_gate_mlp = self.r_adaLN_modulation(c).chunk(6, dim=-1) + # x2 = x1 + gate(self.r_attn(modulate(self.r_norm1(x_), r_shift_msa, r_scale_msa)), r_gate_msa) + # x2 = gate(self.r_mlp(modulate(self.r_norm2(x2), r_shift_mlp, r_scale_mlp)), r_gate_mlp) + # x = x1 + x2 + + # print(x.mean()) + # return x + + +class DiT(nn.Module): + """ + Diffusion model with a Transformer backbone. + """ + + def __init__( + self, + input_h=18, + input_w=32, + patch_size=2, + in_channels=16, + hidden_size=1024, + depth=12, + num_heads=16, + mlp_ratio=4.0, + action_cond_dim=25, + pose_cond_dim=4, + max_frames=32, + reference_length=8, + use_plucker=False, + relative_embedding=False, + cond_only_on_qk=False, + use_reference_attention=False, + add_frame_timestep_embedder=False, + ref_mode='sequential' + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = in_channels + self.patch_size = patch_size + self.num_heads = num_heads + self.max_frames = max_frames + + self.x_embedder = PatchEmbed(input_h, input_w, patch_size, in_channels, hidden_size, flatten=False) + self.t_embedder = TimestepEmbedder(hidden_size) + + self.add_frame_timestep_embedder = add_frame_timestep_embedder + if self.add_frame_timestep_embedder: + self.frame_timestep_embedder = TimestepEmbedder(hidden_size) + + frame_h, frame_w = self.x_embedder.grid_size + + self.spatial_rotary_emb = RotaryEmbedding(dim=hidden_size // num_heads // 2, freqs_for="pixel", max_freq=256) + self.temporal_rotary_emb = RotaryEmbedding(dim=hidden_size // num_heads) + # self.reference_rotary_emb = RotaryEmbedding(dim=hidden_size // num_heads // 2, freqs_for="pixel", max_freq=256) + self.reference_rotary_emb = None + + self.external_cond = nn.Linear(action_cond_dim, hidden_size) if action_cond_dim > 0 else nn.Identity() + + # self.pose_cond = nn.Linear(pose_cond_dim, hidden_size) if pose_cond_dim > 0 else nn.Identity() + + self.use_plucker = use_plucker + if not self.use_plucker: + self.position_embedder = TimestepEmbedder(hidden_size, freq_type='spatial') + self.angle_embedder = TimestepEmbedder(hidden_size, freq_type='angle') + else: + self.pose_embedder = SimpleCameraPoseEncoder(c_in=6, c_out=hidden_size) + + self.blocks = nn.ModuleList( + [ + SpatioTemporalDiTBlock( + hidden_size, + num_heads, + mlp_ratio=mlp_ratio, + is_causal=True, + reference_length=reference_length, + spatial_rotary_emb=self.spatial_rotary_emb, + temporal_rotary_emb=self.temporal_rotary_emb, + reference_rotary_emb=self.reference_rotary_emb, + use_plucker=self.use_plucker, + relative_embedding=relative_embedding, + cond_only_on_qk=cond_only_on_qk, + use_reference_attention=use_reference_attention, + ref_mode=ref_mode + ) + for _ in range(depth) + ] + ) + self.use_reference_attention = use_reference_attention + self.final_layer = FinalLayer(hidden_size, patch_size, self.out_channels) + self.initialize_weights() + + def initialize_weights(self): + # Initialize transformer layers: + def _basic_init(module): + if isinstance(module, nn.Linear): + torch.nn.init.xavier_uniform_(module.weight) + if module.bias is not None: + nn.init.constant_(module.bias, 0) + + self.apply(_basic_init) + + # Initialize patch_embed like nn.Linear (instead of nn.Conv2d): + w = self.x_embedder.proj.weight.data + nn.init.xavier_uniform_(w.view([w.shape[0], -1])) + nn.init.constant_(self.x_embedder.proj.bias, 0) + + # Initialize timestep embedding MLP: + nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02) + nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02) + + if self.use_reference_attention: + if not self.use_plucker: + nn.init.normal_(self.position_embedder.mlp[0].weight, std=0.02) + nn.init.normal_(self.position_embedder.mlp[2].weight, std=0.02) + + nn.init.normal_(self.angle_embedder.mlp[0].weight, std=0.02) + nn.init.normal_(self.angle_embedder.mlp[2].weight, std=0.02) + + if self.add_frame_timestep_embedder: + nn.init.normal_(self.frame_timestep_embedder.mlp[0].weight, std=0.02) + nn.init.normal_(self.frame_timestep_embedder.mlp[2].weight, std=0.02) + + + # Zero-out adaLN modulation layers in DiT blocks: + for block in self.blocks: + nn.init.constant_(block.s_adaLN_modulation[-1].weight, 0) + nn.init.constant_(block.s_adaLN_modulation[-1].bias, 0) + nn.init.constant_(block.t_adaLN_modulation[-1].weight, 0) + nn.init.constant_(block.t_adaLN_modulation[-1].bias, 0) + + if self.use_plucker and self.use_reference_attention: + nn.init.constant_(block.pose_cond_mlp.weight, 0) + nn.init.constant_(block.pose_cond_mlp.bias, 0) + + # Zero-out output layers: + nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0) + nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0) + nn.init.constant_(self.final_layer.linear.weight, 0) + nn.init.constant_(self.final_layer.linear.bias, 0) + + def unpatchify(self, x): + """ + x: (N, H, W, patch_size**2 * C) + imgs: (N, H, W, C) + """ + c = self.out_channels + p = self.x_embedder.patch_size[0] + h = x.shape[1] + w = x.shape[2] + + x = x.reshape(shape=(x.shape[0], h, w, p, p, c)) + x = torch.einsum("nhwpqc->nchpwq", x) + imgs = x.reshape(shape=(x.shape[0], c, h * p, w * p)) + return imgs + + def forward(self, x, t, action_cond=None, pose_cond=None, current_frame=None, mode=None, + reference_length=None, frame_idx=None): + """ + Forward pass of DiT. + x: (B, T, C, H, W) tensor of spatial inputs (images or latent representations of images) + t: (B, T,) tensor of diffusion timesteps + """ + + B, T, C, H, W = x.shape + + # add spatial embeddings + x = rearrange(x, "b t c h w -> (b t) c h w") + + x = self.x_embedder(x) # (B*T, C, H, W) -> (B*T, H/2, W/2, D) , C = 16, D = d_model + # restore shape + x = rearrange(x, "(b t) h w d -> b t h w d", t=T) + # embed noise steps + t = rearrange(t, "b t -> (b t)") + + c_t = self.t_embedder(t) # (N, D) + c = c_t.clone() + c = rearrange(c, "(b t) d -> b t d", t=T) + + if torch.is_tensor(action_cond): + try: + c_action_cond = c + self.external_cond(action_cond) + except: + import pdb;pdb.set_trace() + else: + c_action_cond = None + + if torch.is_tensor(pose_cond): + if not self.use_plucker: + pose_cond = pose_cond.to(action_cond.dtype) + b_, t_, d_ = pose_cond.shape + pos_emb = self.position_embedder(rearrange(pose_cond[...,:3], "b t d -> (b t d)")) + angle_emb = self.angle_embedder(rearrange(pose_cond[...,3:], "b t d -> (b t d)")) + pos_emb = rearrange(pos_emb, "(b t d) c -> b t d c", b=b_, t=t_, d=3).sum(-2) + angle_emb = rearrange(angle_emb, "(b t d) c -> b t d c", b=b_, t=t_, d=2).sum(-2) + pc = pos_emb + angle_emb + else: + pose_cond = pose_cond[:, :, ::40, ::40] + # pc = self.pose_embedder(pose_cond)[0] + # pc = pc.permute(0,2,3,4,1) + pc = self.pose_embedder(pose_cond) + pc = pc.permute(1,0,2,3,4) + + if torch.is_tensor(frame_idx) and self.add_frame_timestep_embedder: + bb = frame_idx.shape[1] + frame_idx = rearrange(frame_idx, "t b -> (b t)") + frame_idx = self.frame_timestep_embedder(frame_idx) + frame_idx = rearrange(frame_idx, "(b t) d -> b t d", b=bb) + pc = pc + frame_idx[:, :, None, None] + + # pc = pc + rearrange(c_t.clone(), "(b t) d -> b t d", t=T)[:,:,None,None] # add time condition for different timestep scaling + else: + pc = None + + for i, block in enumerate(self.blocks): + x = block(x, c, current_frame=current_frame, timestep=t, is_last_block= (i+1 == len(self.blocks)), + pose_cond=pc, mode=mode, c_action_cond=c_action_cond, reference_length=reference_length) # (N, T, H, W, D) + x = self.final_layer(x, c) # (N, T, H, W, patch_size ** 2 * out_channels) + # unpatchify + x = rearrange(x, "b t h w d -> (b t) h w d") + x = self.unpatchify(x) # (N, out_channels, H, W) + x = rearrange(x, "(b t) c h w -> b t c h w", t=T) + + # print("self.blocks[0].pose_cond_mlp.weight:", self.blocks[0].pose_cond_mlp.weight) + # print("self.blocks[0].r_adaLN_modulation[1].weight:", self.blocks[0].r_adaLN_modulation[1].weight) + # print("self.blocks[0].t_adaLN_modulation[1].weight:", self.blocks[0].t_adaLN_modulation[1].weight) + + return x + + +def DiT_S_2(action_cond_dim, pose_cond_dim, reference_length, +use_plucker, relative_embedding, +cond_only_on_qk, use_reference_attention, add_frame_timestep_embedder, +ref_mode): + return DiT( + patch_size=2, + hidden_size=1024, + depth=16, + num_heads=16, + action_cond_dim=action_cond_dim, + pose_cond_dim=pose_cond_dim, + reference_length=reference_length, + use_plucker=use_plucker, + relative_embedding=relative_embedding, + cond_only_on_qk=cond_only_on_qk, + use_reference_attention=use_reference_attention, + add_frame_timestep_embedder=add_frame_timestep_embedder, + ref_mode=ref_mode + ) + + +DiT_models = {"DiT-S/2": DiT_S_2} diff --git a/algorithms/worldmem/models/pose_prediction.py b/algorithms/worldmem/models/pose_prediction.py new file mode 100644 index 0000000000000000000000000000000000000000..0a390b45c5d076b8ca4c376859b8ce9e08348438 --- /dev/null +++ b/algorithms/worldmem/models/pose_prediction.py @@ -0,0 +1,42 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class PosePredictionNet(nn.Module): + def __init__(self, img_channels=16, img_feat_dim=256, pose_dim=5, action_dim=25, hidden_dim=128): + super(PosePredictionNet, self).__init__() + + self.cnn = nn.Sequential( + nn.Conv2d(img_channels, 32, kernel_size=3, stride=2, padding=1), + nn.ReLU(), + nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), + nn.ReLU(), + nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), + nn.ReLU(), + nn.AdaptiveAvgPool2d((1, 1)) + ) + + self.fc_img = nn.Linear(128, img_feat_dim) + + self.mlp_motion = nn.Sequential( + nn.Linear(pose_dim + action_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU() + ) + + self.fc_out = nn.Sequential( + nn.Linear(img_feat_dim + hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, pose_dim) + ) + + def forward(self, img, action, pose): + img_feat = self.cnn(img).view(img.size(0), -1) + img_feat = self.fc_img(img_feat) + + motion_feat = self.mlp_motion(torch.cat([pose, action], dim=1)) + fused_feat = torch.cat([img_feat, motion_feat], dim=1) + pose_next_pred = self.fc_out(fused_feat) + + return pose_next_pred \ No newline at end of file diff --git a/algorithms/worldmem/models/rotary_embedding_torch.py b/algorithms/worldmem/models/rotary_embedding_torch.py new file mode 100644 index 0000000000000000000000000000000000000000..d9af591e49e8cc367e2789e939e889b210e48c7c --- /dev/null +++ b/algorithms/worldmem/models/rotary_embedding_torch.py @@ -0,0 +1,302 @@ +""" +Adapted from https://github.com/lucidrains/rotary-embedding-torch/blob/main/rotary_embedding_torch/rotary_embedding_torch.py +""" + +from __future__ import annotations +from math import pi, log + +import torch +from torch.nn import Module, ModuleList +from torch.amp import autocast +from torch import nn, einsum, broadcast_tensors, Tensor + +from einops import rearrange, repeat + +from typing import Literal + +# helper functions + + +def exists(val): + return val is not None + + +def default(val, d): + return val if exists(val) else d + + +# broadcat, as tortoise-tts was using it + + +def broadcat(tensors, dim=-1): + broadcasted_tensors = broadcast_tensors(*tensors) + return torch.cat(broadcasted_tensors, dim=dim) + + +# rotary embedding helper functions + + +def rotate_half(x): + x = rearrange(x, "... (d r) -> ... d r", r=2) + x1, x2 = x.unbind(dim=-1) + x = torch.stack((-x2, x1), dim=-1) + return rearrange(x, "... d r -> ... (d r)") + + +@autocast("cuda", enabled=False) +def apply_rotary_emb(freqs, t, start_index=0, scale=1.0, seq_dim=-2): + dtype = t.dtype + + if t.ndim == 3: + seq_len = t.shape[seq_dim] + freqs = freqs[-seq_len:] + + rot_dim = freqs.shape[-1] + end_index = start_index + rot_dim + + assert rot_dim <= t.shape[-1], f"feature dimension {t.shape[-1]} is not of sufficient size to rotate in all the positions {rot_dim}" + + # Split t into three parts: left, middle (to be transformed), and right + t_left = t[..., :start_index] + t_middle = t[..., start_index:end_index] + t_right = t[..., end_index:] + + # Apply rotary embeddings without modifying t in place + t_transformed = (t_middle * freqs.cos() * scale) + (rotate_half(t_middle) * freqs.sin() * scale) + + out = torch.cat((t_left, t_transformed, t_right), dim=-1) + + return out.type(dtype) + + +# learned rotation helpers + + +def apply_learned_rotations(rotations, t, start_index=0, freq_ranges=None): + if exists(freq_ranges): + rotations = einsum("..., f -> ... f", rotations, freq_ranges) + rotations = rearrange(rotations, "... r f -> ... (r f)") + + rotations = repeat(rotations, "... n -> ... (n r)", r=2) + return apply_rotary_emb(rotations, t, start_index=start_index) + + +# classes + + +class RotaryEmbedding(Module): + def __init__( + self, + dim, + custom_freqs: Tensor | None = None, + freqs_for: Literal["lang", "pixel", "constant"] = "lang", + theta=10000, + max_freq=10, + num_freqs=1, + learned_freq=False, + use_xpos=False, + xpos_scale_base=512, + interpolate_factor=1.0, + theta_rescale_factor=1.0, + seq_before_head_dim=False, + cache_if_possible=True, + cache_max_seq_len=8192, + ): + super().__init__() + # proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning + # has some connection to NTK literature + # https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/ + + theta *= theta_rescale_factor ** (dim / (dim - 2)) + + self.freqs_for = freqs_for + + if exists(custom_freqs): + freqs = custom_freqs + elif freqs_for == "lang": + freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) + elif freqs_for == "pixel": + freqs = torch.linspace(1.0, max_freq / 2, dim // 2) * pi + elif freqs_for == "spacetime": + time_freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) + freqs = torch.linspace(1.0, max_freq / 2, dim // 2) * pi + elif freqs_for == "constant": + freqs = torch.ones(num_freqs).float() + + if freqs_for == "spacetime": + self.time_freqs = nn.Parameter(time_freqs, requires_grad=learned_freq) + self.freqs = nn.Parameter(freqs, requires_grad=learned_freq) + + self.cache_if_possible = cache_if_possible + self.cache_max_seq_len = cache_max_seq_len + + self.register_buffer("cached_freqs", torch.zeros(cache_max_seq_len, dim), persistent=False) + self.register_buffer("cached_freqs_seq_len", torch.tensor(0), persistent=False) + + self.learned_freq = learned_freq + + # dummy for device + + self.register_buffer("dummy", torch.tensor(0), persistent=False) + + # default sequence dimension + + self.seq_before_head_dim = seq_before_head_dim + self.default_seq_dim = -3 if seq_before_head_dim else -2 + + # interpolation factors + + assert interpolate_factor >= 1.0 + self.interpolate_factor = interpolate_factor + + # xpos + + self.use_xpos = use_xpos + + if not use_xpos: + return + + scale = (torch.arange(0, dim, 2) + 0.4 * dim) / (1.4 * dim) + self.scale_base = xpos_scale_base + + self.register_buffer("scale", scale, persistent=False) + self.register_buffer("cached_scales", torch.zeros(cache_max_seq_len, dim), persistent=False) + self.register_buffer("cached_scales_seq_len", torch.tensor(0), persistent=False) + + # add apply_rotary_emb as static method + + self.apply_rotary_emb = staticmethod(apply_rotary_emb) + + @property + def device(self): + return self.dummy.device + + def get_seq_pos(self, seq_len, device, dtype, offset=0): + return (torch.arange(seq_len, device=device, dtype=dtype) + offset) / self.interpolate_factor + + def rotate_queries_or_keys(self, t, freqs, seq_dim=None, offset=0, scale=None): + seq_dim = default(seq_dim, self.default_seq_dim) + + assert not self.use_xpos or exists(scale), "you must use `.rotate_queries_and_keys` method instead and pass in both queries and keys, for length extrapolatable rotary embeddings" + + device, dtype, seq_len = t.device, t.dtype, t.shape[seq_dim] + + seq = self.get_seq_pos(seq_len, device=device, dtype=dtype, offset=offset) + + seq_freqs = self.forward(seq, freqs, seq_len=seq_len, offset=offset) + + if seq_dim == -3: + seq_freqs = rearrange(seq_freqs, "n d -> n 1 d") + + return apply_rotary_emb(seq_freqs, t, scale=default(scale, 1.0), seq_dim=seq_dim) + + def rotate_queries_with_cached_keys(self, q, k, seq_dim=None, offset=0): + dtype, device, seq_dim = ( + q.dtype, + q.device, + default(seq_dim, self.default_seq_dim), + ) + + q_len, k_len = q.shape[seq_dim], k.shape[seq_dim] + assert q_len <= k_len + + q_scale = k_scale = 1.0 + + if self.use_xpos: + seq = self.get_seq_pos(k_len, dtype=dtype, device=device) + + q_scale = self.get_scale(seq[-q_len:]).type(dtype) + k_scale = self.get_scale(seq).type(dtype) + + rotated_q = self.rotate_queries_or_keys(q, seq_dim=seq_dim, scale=q_scale, offset=k_len - q_len + offset) + rotated_k = self.rotate_queries_or_keys(k, seq_dim=seq_dim, scale=k_scale**-1) + + rotated_q = rotated_q.type(q.dtype) + rotated_k = rotated_k.type(k.dtype) + + return rotated_q, rotated_k + + def rotate_queries_and_keys(self, q, k, freqs, seq_dim=None): + seq_dim = default(seq_dim, self.default_seq_dim) + + assert self.use_xpos + device, dtype, seq_len = q.device, q.dtype, q.shape[seq_dim] + + seq = self.get_seq_pos(seq_len, dtype=dtype, device=device) + + seq_freqs = self.forward(seq, freqs, seq_len=seq_len) + scale = self.get_scale(seq, seq_len=seq_len).to(dtype) + + if seq_dim == -3: + seq_freqs = rearrange(seq_freqs, "n d -> n 1 d") + scale = rearrange(scale, "n d -> n 1 d") + + rotated_q = apply_rotary_emb(seq_freqs, q, scale=scale, seq_dim=seq_dim) + rotated_k = apply_rotary_emb(seq_freqs, k, scale=scale**-1, seq_dim=seq_dim) + + rotated_q = rotated_q.type(q.dtype) + rotated_k = rotated_k.type(k.dtype) + + return rotated_q, rotated_k + + def get_scale(self, t: Tensor, seq_len: int | None = None, offset=0): + assert self.use_xpos + + should_cache = self.cache_if_possible and exists(seq_len) and (offset + seq_len) <= self.cache_max_seq_len + + if should_cache and exists(self.cached_scales) and (seq_len + offset) <= self.cached_scales_seq_len.item(): + return self.cached_scales[offset : (offset + seq_len)] + + scale = 1.0 + if self.use_xpos: + power = (t - len(t) // 2) / self.scale_base + scale = self.scale ** rearrange(power, "n -> n 1") + scale = repeat(scale, "n d -> n (d r)", r=2) + + if should_cache and offset == 0: + self.cached_scales[:seq_len] = scale.detach() + self.cached_scales_seq_len.copy_(seq_len) + + return scale + + def get_axial_freqs(self, *dims): + Colon = slice(None) + all_freqs = [] + + for ind, dim in enumerate(dims): + # only allow pixel freqs for last two dimensions + use_pixel = (self.freqs_for == "pixel" or self.freqs_for == "spacetime") and ind >= len(dims) - 2 + if use_pixel: + pos = torch.linspace(-1, 1, steps=dim, device=self.device) + else: + pos = torch.arange(dim, device=self.device) + + if self.freqs_for == "spacetime" and not use_pixel: + seq_freqs = self.forward(pos, self.time_freqs, seq_len=dim) + else: + seq_freqs = self.forward(pos, self.freqs, seq_len=dim) + + all_axis = [None] * len(dims) + all_axis[ind] = Colon + + new_axis_slice = (Ellipsis, *all_axis, Colon) + all_freqs.append(seq_freqs[new_axis_slice]) + + all_freqs = broadcast_tensors(*all_freqs) + return torch.cat(all_freqs, dim=-1) + + @autocast("cuda", enabled=False) + def forward(self, t: Tensor, freqs: Tensor, seq_len=None, offset=0): + should_cache = self.cache_if_possible and not self.learned_freq and exists(seq_len) and self.freqs_for != "pixel" and (offset + seq_len) <= self.cache_max_seq_len + + if should_cache and exists(self.cached_freqs) and (offset + seq_len) <= self.cached_freqs_seq_len.item(): + return self.cached_freqs[offset : (offset + seq_len)].detach() + + freqs = einsum("..., f -> ... f", t.type(freqs.dtype), freqs) + freqs = repeat(freqs, "... n -> ... (n r)", r=2) + + if should_cache and offset == 0: + self.cached_freqs[:seq_len] = freqs.detach() + self.cached_freqs_seq_len.copy_(seq_len) + + return freqs diff --git a/algorithms/worldmem/models/utils.py b/algorithms/worldmem/models/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..41e6f8a9801649421fe5be46879dc74f5db09a12 --- /dev/null +++ b/algorithms/worldmem/models/utils.py @@ -0,0 +1,163 @@ +""" +Adapted from https://github.com/buoyancy99/diffusion-forcing/blob/main/algorithms/diffusion_forcing/models/utils.py +Action format derived from VPT https://github.com/openai/Video-Pre-Training +Adapted from https://github.com/etched-ai/open-oasis/blob/master/utils.py +""" + +import math +import torch +from torch import nn +from torchvision.io import read_image, read_video +from torchvision.transforms.functional import resize +from einops import rearrange +from typing import Mapping, Sequence +from einops import rearrange, parse_shape + + +def exists(val): + return val is not None + + +def default(val, d): + if exists(val): + return val + return d() if callable(d) else d + + +def extract(a, t, x_shape): + f, b = t.shape + out = a[t] + return out.reshape(f, b, *((1,) * (len(x_shape) - 2))) + + +def linear_beta_schedule(timesteps): + """ + linear schedule, proposed in original ddpm paper + """ + scale = 1000 / timesteps + beta_start = scale * 0.0001 + beta_end = scale * 0.02 + return torch.linspace(beta_start, beta_end, timesteps, dtype=torch.float64) + + +def cosine_beta_schedule(timesteps, s=0.008): + """ + cosine schedule + as proposed in https://openreview.net/forum?id=-NEXDKk8gZ + """ + steps = timesteps + 1 + t = torch.linspace(0, timesteps, steps, dtype=torch.float64) / timesteps + alphas_cumprod = torch.cos((t + s) / (1 + s) * math.pi * 0.5) ** 2 + alphas_cumprod = alphas_cumprod / alphas_cumprod[0] + betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1]) + return torch.clip(betas, 0, 0.999) + + + +def sigmoid_beta_schedule(timesteps, start=-3, end=3, tau=1, clamp_min=1e-5): + """ + sigmoid schedule + proposed in https://arxiv.org/abs/2212.11972 - Figure 8 + better for images > 64x64, when used during training + """ + steps = timesteps + 1 + t = torch.linspace(0, timesteps, steps, dtype=torch.float64) / timesteps + v_start = torch.tensor(start / tau).sigmoid() + v_end = torch.tensor(end / tau).sigmoid() + alphas_cumprod = (-((t * (end - start) + start) / tau).sigmoid() + v_end) / (v_end - v_start) + alphas_cumprod = alphas_cumprod / alphas_cumprod[0] + betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1]) + return torch.clip(betas, 0, 0.999) + + +ACTION_KEYS = [ + "inventory", + "ESC", + "hotbar.1", + "hotbar.2", + "hotbar.3", + "hotbar.4", + "hotbar.5", + "hotbar.6", + "hotbar.7", + "hotbar.8", + "hotbar.9", + "forward", + "back", + "left", + "right", + "cameraX", + "cameraY", + "jump", + "sneak", + "sprint", + "swapHands", + "attack", + "use", + "pickItem", + "drop", +] + + +def one_hot_actions(actions: Sequence[Mapping[str, int]]) -> torch.Tensor: + actions_one_hot = torch.zeros(len(actions), len(ACTION_KEYS)) + for i, current_actions in enumerate(actions): + for j, action_key in enumerate(ACTION_KEYS): + if action_key.startswith("camera"): + if action_key == "cameraX": + value = current_actions["camera"][0] + elif action_key == "cameraY": + value = current_actions["camera"][1] + else: + raise ValueError(f"Unknown camera action key: {action_key}") + max_val = 20 + bin_size = 0.5 + num_buckets = int(max_val / bin_size) + value = (value - num_buckets) / num_buckets + assert -1 - 1e-3 <= value <= 1 + 1e-3, f"Camera action value must be in [-1, 1], got {value}" + else: + value = current_actions[action_key] + assert 0 <= value <= 1, f"Action value must be in [0, 1] got {value}" + actions_one_hot[i, j] = value + + return actions_one_hot + + +IMAGE_EXTENSIONS = {"png", "jpg", "jpeg"} +VIDEO_EXTENSIONS = {"mp4"} + + +def load_prompt(path, video_offset=None, n_prompt_frames=1): + if path.lower().split(".")[-1] in IMAGE_EXTENSIONS: + print("prompt is image; ignoring video_offset and n_prompt_frames") + prompt = read_image(path) + # add frame dimension + prompt = rearrange(prompt, "c h w -> 1 c h w") + elif path.lower().split(".")[-1] in VIDEO_EXTENSIONS: + prompt = read_video(path, pts_unit="sec")[0] + if video_offset is not None: + prompt = prompt[video_offset:] + prompt = prompt[:n_prompt_frames] + else: + raise ValueError(f"unrecognized prompt file extension; expected one in {IMAGE_EXTENSIONS} or {VIDEO_EXTENSIONS}") + assert prompt.shape[0] == n_prompt_frames, f"input prompt {path} had less than n_prompt_frames={n_prompt_frames} frames" + prompt = resize(prompt, (360, 640)) + # add batch dimension + prompt = rearrange(prompt, "t c h w -> 1 t c h w") + prompt = prompt.float() / 255.0 + return prompt + + +def load_actions(path, action_offset=None): + if path.endswith(".actions.pt"): + actions = one_hot_actions(torch.load(path)) + elif path.endswith(".one_hot_actions.pt"): + actions = torch.load(path, weights_only=True) + else: + raise ValueError("unrecognized action file extension; expected '*.actions.pt' or '*.one_hot_actions.pt'") + if action_offset is not None: + actions = actions[action_offset:] + actions = torch.cat([torch.zeros_like(actions[:1]), actions], dim=0) + # add batch dimension + actions = rearrange(actions, "t d -> 1 t d") + return actions diff --git a/algorithms/worldmem/models/vae.py b/algorithms/worldmem/models/vae.py new file mode 100644 index 0000000000000000000000000000000000000000..6cad52b41fd533d4ecdcc964a106c03b170cea64 --- /dev/null +++ b/algorithms/worldmem/models/vae.py @@ -0,0 +1,359 @@ +""" +References: + - VQGAN: https://github.com/CompVis/taming-transformers + - MAE: https://github.com/facebookresearch/mae +""" + +import numpy as np +import math +import functools +from collections import namedtuple +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange +from timm.models.vision_transformer import Mlp +from timm.layers.helpers import to_2tuple +from rotary_embedding_torch import RotaryEmbedding, apply_rotary_emb +from .dit import PatchEmbed + + +class DiagonalGaussianDistribution(object): + def __init__(self, parameters, deterministic=False, dim=1): + self.parameters = parameters + self.mean, self.logvar = torch.chunk(parameters, 2, dim=dim) + if dim == 1: + self.dims = [1, 2, 3] + elif dim == 2: + self.dims = [1, 2] + else: + raise NotImplementedError + self.logvar = torch.clamp(self.logvar, -30.0, 20.0) + self.deterministic = deterministic + self.std = torch.exp(0.5 * self.logvar) + self.var = torch.exp(self.logvar) + if self.deterministic: + self.var = self.std = torch.zeros_like(self.mean).to(device=self.parameters.device) + + def sample(self): + x = self.mean + self.std * torch.randn(self.mean.shape).to(device=self.parameters.device) + return x + + def mode(self): + return self.mean + + +class Attention(nn.Module): + def __init__( + self, + dim, + num_heads, + frame_height, + frame_width, + qkv_bias=False, + ): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.frame_height = frame_height + self.frame_width = frame_width + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.proj = nn.Linear(dim, dim) + + rotary_freqs = RotaryEmbedding( + dim=head_dim // 4, + freqs_for="pixel", + max_freq=frame_height * frame_width, + ).get_axial_freqs(frame_height, frame_width) + self.register_buffer("rotary_freqs", rotary_freqs, persistent=False) + + def forward(self, x): + B, N, C = x.shape + assert N == self.frame_height * self.frame_width + + q, k, v = self.qkv(x).chunk(3, dim=-1) + + q = rearrange( + q, + "b (H W) (h d) -> b h H W d", + H=self.frame_height, + W=self.frame_width, + h=self.num_heads, + ) + k = rearrange( + k, + "b (H W) (h d) -> b h H W d", + H=self.frame_height, + W=self.frame_width, + h=self.num_heads, + ) + v = rearrange( + v, + "b (H W) (h d) -> b h H W d", + H=self.frame_height, + W=self.frame_width, + h=self.num_heads, + ) + + q = apply_rotary_emb(self.rotary_freqs, q) + k = apply_rotary_emb(self.rotary_freqs, k) + + q = rearrange(q, "b h H W d -> b h (H W) d") + k = rearrange(k, "b h H W d -> b h (H W) d") + v = rearrange(v, "b h H W d -> b h (H W) d") + + x = F.scaled_dot_product_attention(q, k, v) + x = rearrange(x, "b h N d -> b N (h d)") + + x = self.proj(x) + return x + + +class AttentionBlock(nn.Module): + def __init__( + self, + dim, + num_heads, + frame_height, + frame_width, + mlp_ratio=4.0, + qkv_bias=False, + attn_causal=False, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + ): + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + num_heads, + frame_height, + frame_width, + qkv_bias=qkv_bias, + ) + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + ) + + def forward(self, x): + x = x + self.attn(self.norm1(x)) + x = x + self.mlp(self.norm2(x)) + return x + + +class AutoencoderKL(nn.Module): + def __init__( + self, + latent_dim, + input_height=256, + input_width=256, + patch_size=16, + enc_dim=768, + enc_depth=6, + enc_heads=12, + dec_dim=768, + dec_depth=6, + dec_heads=12, + mlp_ratio=4.0, + norm_layer=functools.partial(nn.LayerNorm, eps=1e-6), + use_variational=True, + **kwargs, + ): + super().__init__() + self.input_height = input_height + self.input_width = input_width + self.patch_size = patch_size + self.seq_h = input_height // patch_size + self.seq_w = input_width // patch_size + self.seq_len = self.seq_h * self.seq_w + self.patch_dim = 3 * patch_size**2 + + self.latent_dim = latent_dim + self.enc_dim = enc_dim + self.dec_dim = dec_dim + + # patch + self.patch_embed = PatchEmbed(input_height, input_width, patch_size, 3, enc_dim) + + # encoder + self.encoder = nn.ModuleList( + [ + AttentionBlock( + enc_dim, + enc_heads, + self.seq_h, + self.seq_w, + mlp_ratio, + qkv_bias=True, + norm_layer=norm_layer, + ) + for i in range(enc_depth) + ] + ) + self.enc_norm = norm_layer(enc_dim) + + # bottleneck + self.use_variational = use_variational + mult = 2 if self.use_variational else 1 + self.quant_conv = nn.Linear(enc_dim, mult * latent_dim) + self.post_quant_conv = nn.Linear(latent_dim, dec_dim) + + # decoder + self.decoder = nn.ModuleList( + [ + AttentionBlock( + dec_dim, + dec_heads, + self.seq_h, + self.seq_w, + mlp_ratio, + qkv_bias=True, + norm_layer=norm_layer, + ) + for i in range(dec_depth) + ] + ) + self.dec_norm = norm_layer(dec_dim) + self.predictor = nn.Linear(dec_dim, self.patch_dim) # decoder to patch + + # initialize this weight first + self.initialize_weights() + + def initialize_weights(self): + # initialization + # initialize nn.Linear and nn.LayerNorm + self.apply(self._init_weights) + + # initialize patch_embed like nn.Linear (instead of nn.Conv2d) + w = self.patch_embed.proj.weight.data + nn.init.xavier_uniform_(w.view([w.shape[0], -1])) + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + # we use xavier_uniform following official JAX ViT: + nn.init.xavier_uniform_(m.weight) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0.0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0.0) + nn.init.constant_(m.weight, 1.0) + + def patchify(self, x): + # patchify + bsz, _, h, w = x.shape + x = x.reshape( + bsz, + 3, + self.seq_h, + self.patch_size, + self.seq_w, + self.patch_size, + ).permute([0, 1, 3, 5, 2, 4]) # [b, c, h, p, w, p] --> [b, c, p, p, h, w] + x = x.reshape(bsz, self.patch_dim, self.seq_h, self.seq_w) # --> [b, cxpxp, h, w] + x = x.permute([0, 2, 3, 1]).reshape(bsz, self.seq_len, self.patch_dim) # --> [b, hxw, cxpxp] + return x + + def unpatchify(self, x): + bsz = x.shape[0] + # unpatchify + x = x.reshape(bsz, self.seq_h, self.seq_w, self.patch_dim).permute([0, 3, 1, 2]) # [b, h, w, cxpxp] --> [b, cxpxp, h, w] + x = x.reshape( + bsz, + 3, + self.patch_size, + self.patch_size, + self.seq_h, + self.seq_w, + ).permute([0, 1, 4, 2, 5, 3]) # [b, c, p, p, h, w] --> [b, c, h, p, w, p] + x = x.reshape( + bsz, + 3, + self.input_height, + self.input_width, + ) # [b, c, hxp, wxp] + return x + + def encode(self, x): + # patchify + x = self.patch_embed(x) + + # encoder + for blk in self.encoder: + x = blk(x) + x = self.enc_norm(x) + + # bottleneck + moments = self.quant_conv(x) + if not self.use_variational: + moments = torch.cat((moments, torch.zeros_like(moments)), 2) + posterior = DiagonalGaussianDistribution(moments, deterministic=(not self.use_variational), dim=2) + return posterior + + def decode(self, z): + # bottleneck + z = self.post_quant_conv(z) + + # decoder + for blk in self.decoder: + z = blk(z) + z = self.dec_norm(z) + + # predictor + z = self.predictor(z) + + # unpatchify + dec = self.unpatchify(z) + return dec + + def autoencode(self, input, sample_posterior=True): + posterior = self.encode(input) + if self.use_variational and sample_posterior: + z = posterior.sample() + else: + z = posterior.mode() + dec = self.decode(z) + return dec, posterior, z + + def get_input(self, batch, k): + x = batch[k] + if len(x.shape) == 3: + x = x[..., None] + x = x.permute(0, 3, 1, 2).to(memory_format=torch.contiguous_format).float() + return x + + def forward(self, inputs, labels, split="train"): + rec, post, latent = self.autoencode(inputs) + return rec, post, latent + + def get_last_layer(self): + return self.predictor.weight + + +def ViT_L_20_Shallow_Encoder(**kwargs): + if "latent_dim" in kwargs: + latent_dim = kwargs.pop("latent_dim") + else: + latent_dim = 16 + return AutoencoderKL( + latent_dim=latent_dim, + patch_size=20, + enc_dim=1024, + enc_depth=6, + enc_heads=16, + dec_dim=1024, + dec_depth=12, + dec_heads=16, + input_height=360, + input_width=640, + **kwargs, + ) + + +VAE_models = { + "vit-l-20-shallow-encoder": ViT_L_20_Shallow_Encoder, +} diff --git a/algorithms/worldmem/pose_prediction.py b/algorithms/worldmem/pose_prediction.py new file mode 100644 index 0000000000000000000000000000000000000000..fd6148dc7572f1f74a2cbf12293ad698111529a3 --- /dev/null +++ b/algorithms/worldmem/pose_prediction.py @@ -0,0 +1,374 @@ +from omegaconf import DictConfig +import torch +from lightning.pytorch.utilities.types import STEP_OUTPUT +from algorithms.common.metrics import ( + FrechetInceptionDistance, + LearnedPerceptualImagePatchSimilarity, + FrechetVideoDistance, +) +from .df_base import DiffusionForcingBase +from utils.logging_utils import log_video, get_validation_metrics_for_videos +from .models.vae import VAE_models +from .models.dit import DiT_models +from einops import rearrange +from torch import autocast +import numpy as np +from tqdm import tqdm +import torch.nn.functional as F +from .models.pose_prediction import PosePredictionNet +import torchvision.transforms.functional as TF +import random +from torchvision.transforms import InterpolationMode +from PIL import Image +import math +from packaging import version as pver +import torch.distributed as dist +import matplotlib.pyplot as plt + +import torch +import math +import wandb + +import torch.nn as nn +from algorithms.common.base_pytorch_algo import BasePytorchAlgo + +class PosePrediction(BasePytorchAlgo): + + def __init__(self, cfg: DictConfig): + + super().__init__(cfg) + + def _build_model(self): + self.pose_prediction_model = PosePredictionNet() + vae = VAE_models["vit-l-20-shallow-encoder"]() + self.vae = vae.eval() + + def training_step(self, batch, batch_idx) -> STEP_OUTPUT: + xs, conditions, pose_conditions= batch + pose_conditions[:,:,3:] = pose_conditions[:,:,3:] // 15 + xs = self.encode(xs) + + b,f,c,h,w = xs.shape + xs = xs[:,:-1].reshape(-1, c, h, w) + conditions = conditions[:,1:].reshape(-1, 25) + offset_gt = pose_conditions[:,1:] - pose_conditions[:,:-1] + pose_conditions = pose_conditions[:,:-1].reshape(-1, 5) + offset_gt = offset_gt.reshape(-1, 5) + offset_gt[:, 3][offset_gt[:, 3]==23] = -1 + offset_gt[:, 3][offset_gt[:, 3]==-23] = 1 + offset_gt[:, 4][offset_gt[:, 4]==23] = -1 + offset_gt[:, 4][offset_gt[:, 4]==-23] = 1 + + offset_pred = self.pose_prediction_model(xs, conditions, pose_conditions) + criterion = nn.MSELoss() + loss = criterion(offset_pred, offset_gt) + if batch_idx % 200 == 0: + self.log("training/loss", loss.cpu()) + output_dict = { + "loss": loss} + return output_dict + + def encode(self, x): + # vae encoding + B = x.shape[1] + T = x.shape[0] + H, W = x.shape[-2:] + scaling_factor = 0.07843137255 + + x = rearrange(x, "t b c h w -> (t b) c h w") + with torch.no_grad(): + with autocast("cuda", dtype=torch.half): + x = self.vae.encode(x * 2 - 1).mean * scaling_factor + x = rearrange(x, "(t b) (h w) c -> t b c h w", t=T, h=H // self.vae.patch_size, w=W // self.vae.patch_size) + # x = x[:, :n_prompt_frames] + return x + + def decode(self, x): + total_frames = x.shape[0] + scaling_factor = 0.07843137255 + x = rearrange(x, "t b c h w -> (t b) (h w) c") + with torch.no_grad(): + with autocast("cuda", dtype=torch.half): + x = (self.vae.decode(x / scaling_factor) + 1) / 2 + + x = rearrange(x, "(t b) c h w-> t b c h w", t=total_frames) + return x + + def validation_step(self, batch, batch_idx, namespace="validation") -> STEP_OUTPUT: + xs, conditions, pose_conditions= batch + pose_conditions[:,:,3:] = pose_conditions[:,:,3:] // 15 + xs = self.encode(xs) + + b,f,c,h,w = xs.shape + xs = xs[:,:-1].reshape(-1, c, h, w) + conditions = conditions[:,1:].reshape(-1, 25) + offset_gt = pose_conditions[:,1:] - pose_conditions[:,:-1] + pose_conditions = pose_conditions[:,:-1].reshape(-1, 5) + offset_gt = offset_gt.reshape(-1, 5) + offset_gt[:, 3][offset_gt[:, 3]==23] = -1 + offset_gt[:, 3][offset_gt[:, 3]==-23] = 1 + offset_gt[:, 4][offset_gt[:, 4]==23] = -1 + offset_gt[:, 4][offset_gt[:, 4]==-23] = 1 + + offset_pred = self.pose_prediction_model(xs, conditions, pose_conditions) + + criterion = nn.MSELoss() + loss = criterion(offset_pred, offset_gt) + + if batch_idx % 200 == 0: + self.log("validation/loss", loss.cpu()) + output_dict = { + "loss": loss} + return + + @torch.no_grad() + def interactive(self, batch, context_frames, device): + with torch.cuda.amp.autocast(): + condition_similar_length = self.condition_similar_length + # xs_raw, conditions, pose_conditions, c2w_mat, masks, frame_idx = self._preprocess_batch(batch) + + first_frame, new_conditions, new_pose_conditions, new_c2w_mat, new_frame_idx = batch + + if self.frames is None: + first_frame_encode = self.encode(first_frame[None, None].to(device)) + self.frames = first_frame_encode.to(device) + self.actions = new_conditions[None, None].to(device) + self.poses = new_pose_conditions[None, None].to(device) + self.memory_c2w = new_c2w_mat[None, None].to(device) + self.frame_idx = torch.tensor([[new_frame_idx]]).to(device) + return first_frame + else: + self.actions = torch.cat([self.actions, new_conditions[None, None].to(device)]) + self.poses = torch.cat([self.poses, new_pose_conditions[None, None].to(device)]) + self.memory_c2w = torch.cat([self.memory_c2w, new_c2w_mat[None, None].to(device)]) + self.frame_idx = torch.cat([self.frame_idx, torch.tensor([[new_frame_idx]]).to(device)]) + + conditions = self.actions.clone() + pose_conditions = self.poses.clone() + c2w_mat = self.memory_c2w .clone() + frame_idx = self.frame_idx.clone() + + + curr_frame = 0 + horizon = 1 + batch_size = 1 + n_frames = curr_frame + horizon + # context + n_context_frames = context_frames // self.frame_stack + xs_pred = self.frames[:n_context_frames].clone() + curr_frame += n_context_frames + + pbar = tqdm(total=n_frames, initial=curr_frame, desc="Sampling") + + # generation on frame + scheduling_matrix = self._generate_scheduling_matrix(horizon) + chunk = torch.randn((horizon, batch_size, *xs_pred.shape[2:])).to(xs_pred.device) + chunk = torch.clamp(chunk, -self.clip_noise, self.clip_noise) + + xs_pred = torch.cat([xs_pred, chunk], 0) + + # sliding window: only input the last n_tokens frames + start_frame = max(0, curr_frame + horizon - self.n_tokens) + + pbar.set_postfix( + { + "start": start_frame, + "end": curr_frame + horizon, + } + ) + + if condition_similar_length: + + if curr_frame < condition_similar_length: + random_idx = [i for i in range(curr_frame)] + [0] * (condition_similar_length-curr_frame) + random_idx = np.repeat(np.array(random_idx)[:,None], xs_pred.shape[1], -1) + else: + num_samples = 10000 + radius = 30 + samples = torch.rand((num_samples, 1), device=pose_conditions.device) + angles = 2 * np.pi * torch.rand((num_samples,), device=pose_conditions.device) + # points = radius * torch.sqrt(samples) * torch.stack((torch.cos(angles), torch.sin(angles)), dim=1) + + points = generate_points_in_sphere(num_samples, radius).to(pose_conditions.device) + points = points[:, None].repeat(1, pose_conditions.shape[1], 1) + points += pose_conditions[curr_frame, :, :3][None] + fov_half_h = torch.tensor(105/2, device=pose_conditions.device) + fov_half_v = torch.tensor(75/2, device=pose_conditions.device) + # in_fov1 = is_inside_fov(points, pose_conditions[curr_frame, :, [0, 2]], pose_conditions[curr_frame, :, -1], fov_half) + + in_fov1 = is_inside_fov_3d_hv(points, pose_conditions[curr_frame, :, :3], + pose_conditions[curr_frame, :, -2], pose_conditions[curr_frame, :, -1], + fov_half_h, fov_half_v) + + in_fov_list = [] + for pc in pose_conditions[:curr_frame]: + in_fov_list.append(is_inside_fov_3d_hv(points, pc[:, :3], pc[:, -2], pc[:, -1], + fov_half_h, fov_half_v)) + + in_fov_list = torch.stack(in_fov_list) + # v3 + random_idx = [] + + for csl in range(self.condition_similar_length // 2): + overlap_ratio = ((in_fov1[None].bool() & in_fov_list).sum(1))/in_fov1.sum() + # mask = distance > (in_fov1.bool().sum(0) / 4) + #_, r_idx = torch.topk(overlap_ratio / tensor_max_with_number((frame_idx[curr_frame] - frame_idx[:curr_frame]), 10), k=1, dim=0) + + # if csl > self.condition_similar_length: + # _, r_idx = torch.topk(overlap_ratio, k=1, dim=0) + # else: + # _, r_idx = torch.topk(overlap_ratio / tensor_max_with_number((frame_idx[curr_frame] - frame_idx[:curr_frame]), 10), k=1, dim=0) + + _, r_idx = torch.topk(overlap_ratio, k=1, dim=0) + # _, r_idx = torch.topk(overlap_ratio / tensor_max_with_number((frame_idx[curr_frame] - frame_idx[:curr_frame]), 10), k=1, dim=0) + + # if curr_frame >=93: + # import pdb;pdb.set_trace() + + # start_time = time.time() + cos_sim = F.cosine_similarity(xs_pred.to(r_idx.device)[r_idx[:, range(in_fov1.shape[1])], + range(in_fov1.shape[1])], xs_pred.to(r_idx.device)[:curr_frame], dim=2) + cos_sim = cos_sim.mean((-2,-1)) + + mask_sim = cos_sim>0.9 + in_fov_list = in_fov_list & ~mask_sim[:,None].to(in_fov_list.device) + + random_idx.append(r_idx) + + for bi in range(conditions.shape[1]): + if len(torch.nonzero(conditions[:,bi,24] == 1))==0: + pass + else: + last_idx = torch.nonzero(conditions[:,bi,24] == 1)[-1] + in_fov_list[:last_idx,:,bi] = False + + for csl in range(self.condition_similar_length // 2): + overlap_ratio = ((in_fov1[None].bool() & in_fov_list).sum(1))/in_fov1.sum() + # mask = distance > (in_fov1.bool().sum(0) / 4) + #_, r_idx = torch.topk(overlap_ratio / tensor_max_with_number((frame_idx[curr_frame] - frame_idx[:curr_frame]), 10), k=1, dim=0) + + # if csl > self.condition_similar_length: + # _, r_idx = torch.topk(overlap_ratio, k=1, dim=0) + # else: + # _, r_idx = torch.topk(overlap_ratio / tensor_max_with_number((frame_idx[curr_frame] - frame_idx[:curr_frame]), 10), k=1, dim=0) + + _, r_idx = torch.topk(overlap_ratio, k=1, dim=0) + # _, r_idx = torch.topk(overlap_ratio / tensor_max_with_number((frame_idx[curr_frame] - frame_idx[:curr_frame]), 10), k=1, dim=0) + + # if curr_frame >=93: + # import pdb;pdb.set_trace() + + # start_time = time.time() + cos_sim = F.cosine_similarity(xs_pred.to(r_idx.device)[r_idx[:, range(in_fov1.shape[1])], + range(in_fov1.shape[1])], xs_pred.to(r_idx.device)[:curr_frame], dim=2) + cos_sim = cos_sim.mean((-2,-1)) + + mask_sim = cos_sim>0.9 + in_fov_list = in_fov_list & ~mask_sim[:,None].to(in_fov_list.device) + + random_idx.append(r_idx) + + random_idx = torch.cat(random_idx).cpu() + condition_similar_length = len(random_idx) + + xs_pred = torch.cat([xs_pred, xs_pred[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])].clone()], 0) + + if condition_similar_length: + # import pdb;pdb.set_trace() + padding = torch.zeros((condition_similar_length,) + conditions.shape[1:], device=conditions.device, dtype=conditions.dtype) + input_condition = torch.cat([conditions[start_frame : curr_frame + horizon], padding], dim=0) + if self.pose_cond_dim: + # if not self.use_plucker: + input_pose_condition = torch.cat([pose_conditions[start_frame : curr_frame + horizon], pose_conditions[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])]], dim=0).clone() + + if self.use_plucker: + if self.all_zero_frame: + frame_idx_list = [] + input_pose_condition = [] + for i in range(start_frame, curr_frame + horizon): + input_pose_condition.append(convert_to_plucker(torch.cat([c2w_mat[i:i+1],c2w_mat[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])]]).clone(), 0, focal_length=self.focal_length, is_old_setting=self.old_setting).to(xs_pred.dtype)) + frame_idx_list.append(torch.cat([frame_idx[i:i+1]-frame_idx[i:i+1], frame_idx[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])]-frame_idx[i:i+1]])) + input_pose_condition = torch.cat(input_pose_condition) + frame_idx_list = torch.cat(frame_idx_list) + + # print(frame_idx_list[:,0]) + else: + # print(curr_frame-start_frame) + # input_pose_condition = torch.cat([c2w_mat[start_frame : curr_frame + horizon], c2w_mat[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])]], dim=0).clone() + # import pdb;pdb.set_trace() + if self.last_frame_refer: + input_pose_condition = torch.cat([c2w_mat[start_frame : curr_frame + horizon], c2w_mat[-1:]], dim=0).clone() + else: + input_pose_condition = torch.cat([c2w_mat[start_frame : curr_frame + horizon], c2w_mat[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])]], dim=0).clone() + + if self.zero_curr: + # print("="*50) + input_pose_condition = convert_to_plucker(input_pose_condition, curr_frame-start_frame, focal_length=self.focal_length, is_old_setting=self.old_setting) + # input_pose_condition[:curr_frame-start_frame] = input_pose_condition[curr_frame-start_frame:curr_frame-start_frame+1] + # input_pose_condition = convert_to_plucker(input_pose_condition, -self.condition_similar_length-1, focal_length=self.focal_length) + else: + input_pose_condition = convert_to_plucker(input_pose_condition, -condition_similar_length, focal_length=self.focal_length, is_old_setting=self.old_setting) + frame_idx_list = None + else: + input_pose_condition = torch.cat([pose_conditions[start_frame : curr_frame + horizon], pose_conditions[random_idx[:,range(xs_pred.shape[1])], range(xs_pred.shape[1])]], dim=0).clone() + frame_idx_list = None + else: + input_condition = conditions[start_frame : curr_frame + horizon] + input_pose_condition = None + frame_idx_list = None + + for m in range(scheduling_matrix.shape[0] - 1): + from_noise_levels = np.concatenate((np.zeros((curr_frame,), dtype=np.int64), scheduling_matrix[m]))[ + :, None + ].repeat(batch_size, axis=1) + to_noise_levels = np.concatenate( + ( + np.zeros((curr_frame,), dtype=np.int64), + scheduling_matrix[m + 1], + ) + )[ + :, None + ].repeat(batch_size, axis=1) + + if condition_similar_length: + from_noise_levels = np.concatenate([from_noise_levels, np.zeros((condition_similar_length,from_noise_levels.shape[-1]), dtype=np.int32)], axis=0) + to_noise_levels = np.concatenate([to_noise_levels, np.zeros((condition_similar_length,from_noise_levels.shape[-1]), dtype=np.int32)], axis=0) + + from_noise_levels = torch.from_numpy(from_noise_levels).to(self.device) + to_noise_levels = torch.from_numpy(to_noise_levels).to(self.device) + + + if input_pose_condition is not None: + input_pose_condition = input_pose_condition.to(xs_pred.dtype) + + xs_pred[start_frame:] = self.diffusion_model.sample_step( + xs_pred[start_frame:], + input_condition, + input_pose_condition, + from_noise_levels[start_frame:], + to_noise_levels[start_frame:], + current_frame=curr_frame, + mode="validation", + reference_length=condition_similar_length, + frame_idx=frame_idx_list + ) + + # if curr_frame > 14: + # import pdb;pdb.set_trace() + + # if xs_pred_back is not None: + # xs_pred = torch.cat([xs_pred[:6], xs_pred_back[6:12], xs_pred[6:]], dim=0) + + # import pdb;pdb.set_trace() + if condition_similar_length: # and curr_frame+1!=n_frames: + xs_pred = xs_pred[:-condition_similar_length] + + curr_frame += horizon + pbar.update(horizon) + + self.frames = torch.cat([self.frames, xs_pred[n_context_frames:]]) + + xs_pred = self.decode(xs_pred[n_context_frames:]) + + return xs_pred[-1,0].cpu() + diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..69f261e1ec6468a382e1f8c8508ed84733996b0e --- /dev/null +++ b/app.py @@ -0,0 +1,535 @@ +import gradio as gr +import time + +import sys +import subprocess +import time +from pathlib import Path + +import hydra +from omegaconf import DictConfig, OmegaConf +from omegaconf.omegaconf import open_dict + +import numpy as np +import torch +import torchvision.transforms as transforms +import cv2 +import subprocess +from PIL import Image +from datetime import datetime +import spaces +from algorithms.worldmem import WorldMemMinecraft +from huggingface_hub import hf_hub_download +import tempfile + +torch.set_float32_matmul_precision("high") + +ACTION_KEYS = [ + "inventory", + "ESC", + "hotbar.1", + "hotbar.2", + "hotbar.3", + "hotbar.4", + "hotbar.5", + "hotbar.6", + "hotbar.7", + "hotbar.8", + "hotbar.9", + "forward", + "back", + "left", + "right", + "cameraY", + "cameraX", + "jump", + "sneak", + "sprint", + "swapHands", + "attack", + "use", + "pickItem", + "drop", +] + +# Mapping of input keys to action names +KEY_TO_ACTION = { + "Q": ("forward", 1), + "E": ("back", 1), + "W": ("cameraY", -1), + "S": ("cameraY", 1), + "A": ("cameraX", -1), + "D": ("cameraX", 1), + "U": ("drop", 1), + "N": ("noop", 1), + "1": ("hotbar.1", 1), +} + +example_images = [ + ["1", "assets/ice_plains.png", "turn rightgo backward→look up→turn left→look down→turn right→go forward→turn left", 20, 3, 8], + ["2", "assets/place.png", "put item→go backward→put item→go backward→go around", 20, 3, 8], + ["3", "assets/rain_sunflower_plains.png", "turn right→look up→turn right→look down→turn left→go backward→turn left", 20, 3, 8], + ["4", "assets/desert.png", "turn 360 degree→turn right→go forward→turn left", 20, 3, 8], +] + +def load_custom_checkpoint(algo, checkpoint_path): + hf_ckpt = str(checkpoint_path).split('/') + repo_id = '/'.join(hf_ckpt[:2]) + file_name = '/'.join(hf_ckpt[2:]) + model_path = hf_hub_download(repo_id=repo_id, + filename=file_name) + ckpt = torch.load(model_path, map_location=torch.device('cpu')) + algo.load_state_dict(ckpt['state_dict'], strict=False) + + +def parse_input_to_tensor(input_str): + """ + Convert an input string into a (sequence_length, 25) tensor, where each row is a one-hot representation + of the corresponding action key. + + Args: + input_str (str): A string consisting of "WASD" characters (e.g., "WASDWS"). + + Returns: + torch.Tensor: A tensor of shape (sequence_length, 25), where each row is a one-hot encoded action. + """ + # Get the length of the input sequence + seq_len = len(input_str) + + # Initialize a zero tensor of shape (seq_len, 25) + action_tensor = torch.zeros((seq_len, 25)) + + # Iterate through the input string and update the corresponding positions + for i, char in enumerate(input_str): + action, value = KEY_TO_ACTION.get(char.upper()) # Convert to uppercase to handle case insensitivity + if action and action in ACTION_KEYS: + index = ACTION_KEYS.index(action) + action_tensor[i, index] = value # Set the corresponding action index to 1 + + return action_tensor + +def load_image_as_tensor(image_path: str) -> torch.Tensor: + """ + Load an image and convert it to a 0-1 normalized tensor. + + Args: + image_path (str): Path to the image file. + + Returns: + torch.Tensor: Image tensor of shape (C, H, W), normalized to [0,1]. + """ + if isinstance(image_path, str): + image = Image.open(image_path).convert("RGB") # Ensure it's RGB + else: + image = image_path + transform = transforms.Compose([ + transforms.ToTensor(), # Converts to tensor and normalizes to [0,1] + ]) + return transform(image) + +def enable_amp(model, precision="16-mixed"): + original_forward = model.forward + + def amp_forward(*args, **kwargs): + with torch.autocast("cuda", dtype=torch.float16 if precision == "16-mixed" else torch.bfloat16): + return original_forward(*args, **kwargs) + + model.forward = amp_forward + return model + +memory_frames = [] +input_history = "" +ICE_PLAINS_IMAGE = "assets/ice_plains.png" +DESERT_IMAGE = "assets/desert.png" +SAVANNA_IMAGE = "assets/savanna.png" +PLAINS_IMAGE = "assets/plans.png" +PLACE_IMAGE = "assets/place.png" +SUNFLOWERS_IMAGE = "assets/sunflower_plains.png" +SUNFLOWERS_RAIN_IMAGE = "assets/rain_sunflower_plains.png" + +device = torch.device('cuda') + +def save_video(frames, path="output.mp4", fps=10): + h, w, _ = frames[0].shape + out = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*'XVID'), fps, (w, h)) + for frame in frames: + out.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) + out.release() + + ffmpeg_cmd = [ + "ffmpeg", "-y", "-i", path, "-c:v", "libx264", "-crf", "23", "-preset", "medium", path + ] + subprocess.run(ffmpeg_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return path + +cfg = OmegaConf.load("configurations/huggingface.yaml") +worldmem = WorldMemMinecraft(cfg) +load_custom_checkpoint(algo=worldmem.diffusion_model, checkpoint_path=cfg.diffusion_path) +load_custom_checkpoint(algo=worldmem.vae, checkpoint_path=cfg.vae_path) +load_custom_checkpoint(algo=worldmem.pose_prediction_model, checkpoint_path=cfg.pose_predictor_path) +worldmem.to("cuda").eval() +# worldmem = enable_amp(worldmem, precision="16-mixed") + +actions = np.zeros((1, 25), dtype=np.float32) +poses = np.zeros((1, 5), dtype=np.float32) + + + +def get_duration_single_image_to_long_video(first_frame, action, first_pose, device, self_frames, self_actions, + self_poses, self_memory_c2w, self_frame_idx): + return 5 * len(action) if self_actions is not None else 5 + +@spaces.GPU(duration=get_duration_single_image_to_long_video) +def run_interactive(first_frame, action, first_pose, device, self_frames, self_actions, + self_poses, self_memory_c2w, self_frame_idx): + new_frame, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx = worldmem.interactive(first_frame, + action, + first_pose, + device=device, + self_frames=self_frames, + self_actions=self_actions, + self_poses=self_poses, + self_memory_c2w=self_memory_c2w, + self_frame_idx=self_frame_idx) + + return new_frame, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx + +def set_denoising_steps(denoising_steps, sampling_timesteps_state): + worldmem.sampling_timesteps = denoising_steps + worldmem.diffusion_model.sampling_timesteps = denoising_steps + sampling_timesteps_state = denoising_steps + print("set denoising steps to", worldmem.sampling_timesteps) + return sampling_timesteps_state + +def set_context_length(context_length, sampling_context_length_state): + worldmem.n_tokens = context_length + sampling_context_length_state = context_length + print("set context length to", worldmem.n_tokens) + return sampling_context_length_state + +def set_memory_length(memory_length, sampling_memory_length_state): + worldmem.condition_similar_length = memory_length + sampling_memory_length_state = memory_length + print("set memory length to", worldmem.condition_similar_length) + return sampling_memory_length_state + +def generate(keys, input_history, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx): + input_actions = parse_input_to_tensor(keys) + + if self_frames is None: + new_frame, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx = run_interactive(memory_frames[0], + actions[0], + poses[0], + device=device, + self_frames=self_frames, + self_actions=self_actions, + self_poses=self_poses, + self_memory_c2w=self_memory_c2w, + self_frame_idx=self_frame_idx) + + new_frame, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx = run_interactive(memory_frames[0], + input_actions, + None, + device=device, + self_frames=self_frames, + self_actions=self_actions, + self_poses=self_poses, + self_memory_c2w=self_memory_c2w, + self_frame_idx=self_frame_idx) + + memory_frames = np.concatenate([memory_frames, new_frame[:,0]]) + + + out_video = memory_frames.transpose(0,2,3,1).copy() + out_video = np.clip(out_video, a_min=0.0, a_max=1.0) + out_video = (out_video * 255).astype(np.uint8) + + last_frame = out_video[-1].copy() + border_thickness = 2 + out_video[-len(new_frame):, :border_thickness, :, :] = [255, 0, 0] + out_video[-len(new_frame):, -border_thickness:, :, :] = [255, 0, 0] + out_video[-len(new_frame):, :, :border_thickness, :] = [255, 0, 0] + out_video[-len(new_frame):, :, -border_thickness:, :] = [255, 0, 0] + + temporal_video_path = tempfile.NamedTemporaryFile(suffix='.mp4').name + save_video(out_video, temporal_video_path) + input_history += keys + + + # now = datetime.now() + # folder_name = now.strftime("%Y-%m-%d_%H-%M-%S") + # folder_path = os.path.join("/mnt/xiaozeqi/worldmem/output_material", folder_name) + # os.makedirs(folder_path, exist_ok=True) + # data_dict = { + # "input_history": input_history, + # "memory_frames": memory_frames, + # "self_frames": self_frames, + # "self_actions": self_actions, + # "self_poses": self_poses, + # "self_memory_c2w": self_memory_c2w, + # "self_frame_idx": self_frame_idx, + # } + + # np.savez(os.path.join(folder_path, "data_bundle.npz"), **data_dict) + + return last_frame, temporal_video_path, input_history, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx + +def reset(selected_image): + self_frames = None + self_poses = None + self_actions = None + self_memory_c2w = None + self_frame_idx = None + memory_frames = load_image_as_tensor(selected_image).numpy()[None] + input_history = "" + + new_frame, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx = run_interactive(memory_frames[0], + actions[0], + poses[0], + device=device, + self_frames=self_frames, + self_actions=self_actions, + self_poses=self_poses, + self_memory_c2w=self_memory_c2w, + self_frame_idx=self_frame_idx) + + return input_history, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx + +def on_image_click(selected_image): + input_history, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx = reset(selected_image) + return input_history, selected_image, selected_image, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx + +def set_memory(examples_case, image_display, log_output, slider_denoising_step, slider_context_length, slider_memory_length): + if examples_case == '1': + data_bundle = np.load("assets/examples/case1.npz") + input_history = data_bundle['input_history'].item() + memory_frames = data_bundle['memory_frames'] + self_frames = data_bundle['self_frames'] + self_actions = data_bundle['self_actions'] + self_poses = data_bundle['self_poses'] + self_memory_c2w = data_bundle['self_memory_c2w'] + self_frame_idx = data_bundle['self_frame_idx'] + elif examples_case == '2': + data_bundle = np.load("assets/examples/case2.npz") + input_history = data_bundle['input_history'].item() + memory_frames = data_bundle['memory_frames'] + self_frames = data_bundle['self_frames'] + self_actions = data_bundle['self_actions'] + self_poses = data_bundle['self_poses'] + self_memory_c2w = data_bundle['self_memory_c2w'] + self_frame_idx = data_bundle['self_frame_idx'] + elif examples_case == '3': + data_bundle = np.load("assets/examples/case3.npz") + input_history = data_bundle['input_history'].item() + memory_frames = data_bundle['memory_frames'] + self_frames = data_bundle['self_frames'] + self_actions = data_bundle['self_actions'] + self_poses = data_bundle['self_poses'] + self_memory_c2w = data_bundle['self_memory_c2w'] + self_frame_idx = data_bundle['self_frame_idx'] + elif examples_case == '4': + data_bundle = np.load("assets/examples/case4.npz") + input_history = data_bundle['input_history'].item() + memory_frames = data_bundle['memory_frames'] + self_frames = data_bundle['self_frames'] + self_actions = data_bundle['self_actions'] + self_poses = data_bundle['self_poses'] + self_memory_c2w = data_bundle['self_memory_c2w'] + self_frame_idx = data_bundle['self_frame_idx'] + + out_video = memory_frames.transpose(0,2,3,1) + out_video = np.clip(out_video, a_min=0.0, a_max=1.0) + out_video = (out_video * 255).astype(np.uint8) + + temporal_video_path = tempfile.NamedTemporaryFile(suffix='.mp4').name + save_video(out_video, temporal_video_path) + + return input_history, out_video[-1], temporal_video_path, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx + +css = """ +h1 { + text-align: center; + display:block; +} +""" + +with gr.Blocks(css=css) as demo: + gr.Markdown( + """ + # WORLDMEM: Long-term Consistent World Generation with Memory + """ + ) + + gr.Markdown( + """ + ## 🚀 How to Explore WorldMem + + Follow these simple steps to get started: + + 1. **Choose a scene**. + 2. **Input your action sequence**. + 3. **Click "Generate"**. + + - You can continuously click **"Generate"** to **extend the video** and observe how well the world maintains consistency over time. + - For best performance, we recommend **running locally** (1s/frame on H100) instead of Spaces (5s/frame). + - ⭐️ If you like this project, please [give it a star on GitHub]()! + - 💬 For questions or feedback, feel free to open an issue or email me at **zeqixiao1@gmail.com**. + + Happy exploring! 🌍 + """ + ) + #
+ # + # + # + # + + # + # + # + # + + # + # + # + # + + # + # + # + # + #
+ + example_actions = {"turn left→turn right": "AAAAAAAAAAAADDDDDDDDDDDD", + "turn 360 degree": "AAAAAAAAAAAAAAAAAAAAAAAA", + "turn right→go backward→look up→turn left→look down": "DDDDDDDDEEEEEEEEEESSSAAAAAAAAWWW", + "turn right→go forward→turn right": "DDDDDDDDDDDDQQQQQQQQQQQQQQQDDDDDDDDDDDD", + "turn right→look up→turn right→look down": "DDDDWWWDDDDDDDDDDDDDDDDDDDDSSS", + "put item→go backward→put item→go backward":"SSUNNWWEEEEEEEEEAAASSUNNWWEEEEEEEEE"} + + selected_image = gr.State(ICE_PLAINS_IMAGE) + + with gr.Row(variant="panel"): + with gr.Column(): + gr.Markdown("🖼️ Start from this frame.") + image_display = gr.Image(value=selected_image.value, interactive=False, label="Current Frame") + with gr.Column(): + gr.Markdown("🎞️ Generated videos. New contents are marked in red box.") + video_display = gr.Video(autoplay=True, loop=True) + + gr.Markdown("### 🏞️ Choose a scene and start generation.") + + with gr.Row(): + image_display_1 = gr.Image(value=SUNFLOWERS_IMAGE, interactive=False, label="Sunflower Plains") + image_display_2 = gr.Image(value=DESERT_IMAGE, interactive=False, label="Desert") + image_display_3 = gr.Image(value=SAVANNA_IMAGE, interactive=False, label="Savanna") + image_display_4 = gr.Image(value=ICE_PLAINS_IMAGE, interactive=False, label="Ice Plains") + image_display_5 = gr.Image(value=SUNFLOWERS_RAIN_IMAGE, interactive=False, label="Rainy Sunflower Plains") + image_display_6 = gr.Image(value=PLACE_IMAGE, interactive=False, label="Place") + + + with gr.Row(variant="panel"): + with gr.Column(scale=2): + gr.Markdown("### 🕹️ Input action sequences for interaction.") + input_box = gr.Textbox(label="Action Sequences", placeholder="Enter action sequences here, e.g. (AAAAAAAAAAAADDDDDDDDDDDD)", lines=1, max_lines=1) + log_output = gr.Textbox(label="History Sequences", interactive=False) + gr.Markdown( + """ + ### 💡 Action Key Guide + +
+                W: Turn up      S: Turn down     A: Turn left     D: Turn right
+                Q: Go forward   E: Go backward   N: No-op         U: Use item
+                
+ """ + ) + gr.Markdown("### 👇 Click to quickly set action sequence examples.") + with gr.Row(): + buttons = [] + for action_key in list(example_actions.keys())[:2]: + with gr.Column(scale=len(action_key)): + buttons.append(gr.Button(action_key)) + with gr.Row(): + for action_key in list(example_actions.keys())[2:4]: + with gr.Column(scale=len(action_key)): + buttons.append(gr.Button(action_key)) + with gr.Row(): + for action_key in list(example_actions.keys())[4:6]: + with gr.Column(scale=len(action_key)): + buttons.append(gr.Button(action_key)) + + with gr.Column(scale=1): + submit_button = gr.Button("🎬 Generate!", variant="primary") + reset_btn = gr.Button("🔄 Reset") + + # gr.Markdown("
") + + gr.Markdown("### ⚙️ Advanced Settings") + + slider_denoising_step = gr.Slider( + minimum=10, maximum=50, value=worldmem.sampling_timesteps, step=1, + label="Denoising Steps", + info="Higher values yield better quality but slower speed" + ) + slider_context_length = gr.Slider( + minimum=2, maximum=10, value=worldmem.n_tokens, step=1, + label="Context Length", + info="How many previous frames in temporal context window." + ) + slider_memory_length = gr.Slider( + minimum=4, maximum=16, value=worldmem.condition_similar_length, step=1, + label="Memory Length", + info="How many previous frames in memory window." + ) + + + sampling_timesteps_state = gr.State(worldmem.sampling_timesteps) + sampling_context_length_state = gr.State(worldmem.n_tokens) + sampling_memory_length_state = gr.State(worldmem.condition_similar_length) + + memory_frames = gr.State(load_image_as_tensor(selected_image.value)[None].numpy()) + self_frames = gr.State() + self_actions = gr.State() + self_poses = gr.State() + self_memory_c2w = gr.State() + self_frame_idx = gr.State() + + def set_action(action): + return action + + + + for button, action_key in zip(buttons, list(example_actions.keys())): + button.click(set_action, inputs=[gr.State(value=example_actions[action_key])], outputs=input_box) + + gr.Markdown("### 👇 Click to review generated examples, and continue generation based on them.") + + example_case = gr.Textbox(label="Case", visible=False) + image_output = gr.Image(visible=False) + + examples = gr.Examples( + examples=example_images, + inputs=[example_case, image_output, log_output, slider_denoising_step, slider_context_length, slider_memory_length], + cache_examples=False + ) + + example_case.change( + fn=set_memory, + inputs=[example_case, image_output, log_output, slider_denoising_step, slider_context_length, slider_memory_length], + outputs=[log_output, image_display, video_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx] + ) + + submit_button.click(generate, inputs=[input_box, log_output, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx], outputs=[image_display, video_display, log_output, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + reset_btn.click(reset, inputs=[selected_image], outputs=[log_output, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + image_display_1.select(lambda: on_image_click(SUNFLOWERS_IMAGE), outputs=[log_output, selected_image, image_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + image_display_2.select(lambda: on_image_click(DESERT_IMAGE), outputs=[log_output, selected_image, image_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + image_display_3.select(lambda: on_image_click(SAVANNA_IMAGE), outputs=[log_output, selected_image, image_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + image_display_4.select(lambda: on_image_click(ICE_PLAINS_IMAGE), outputs=[log_output, selected_image, image_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + image_display_5.select(lambda: on_image_click(SUNFLOWERS_RAIN_IMAGE), outputs=[log_output, selected_image, image_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + image_display_6.select(lambda: on_image_click(PLACE_IMAGE), outputs=[log_output, selected_image,image_display, memory_frames, self_frames, self_actions, self_poses, self_memory_c2w, self_frame_idx]) + + slider_denoising_step.change(fn=set_denoising_steps, inputs=[slider_denoising_step, sampling_timesteps_state], outputs=sampling_timesteps_state) + slider_context_length.change(fn=set_context_length, inputs=[slider_context_length, sampling_context_length_state], outputs=sampling_context_length_state) + slider_memory_length.change(fn=set_memory_length, inputs=[slider_memory_length, sampling_memory_length_state], outputs=sampling_memory_length_state) + +demo.launch() diff --git a/app.sh b/app.sh new file mode 100644 index 0000000000000000000000000000000000000000..9505359bdf7058398572b340ec468239d869affa --- /dev/null +++ b/app.sh @@ -0,0 +1,50 @@ +wandb disabled +# srun -p a6000_xgpan -w MICL-PanXGSvr2 --gres=gpu:1 --ntasks-per-node=1 --cpus-per-task=8 \ +export WANDB_API_KEY=a4f0741e80f509317597ad944a7292fabcb68bdf + +CHECKPOINT_PATH="checkpoints/diffusion_only.ckpt" + +python -m app +name=pumpkin \ + algorithm=df_video_worldmemminecraft \ + +checkpoint_path=$CHECKPOINT_PATH \ + experiment.tasks=[interactive] \ + dataset.validation_multiplier=1 \ + dataset=video_minecraft \ + +customized_load=true \ + +dataset.n_frames_valid=100 \ + +algorithm.n_tokens=8 \ + +load_vae=false \ + +load_t_to_r=false \ + +zero_init_gate=false \ + experiment.validation.batch_size=1 \ + +algorithm.pose_cond_dim=5 \ + +algorithm.condition_similar_length=8 \ + +dataset.condition_similar_length=8 \ + +algorithm.use_plucker=true \ + +dataset.use_plucker=true \ + +dataset.padding_pool=10 \ + +dataset.focal_length=0.35 \ + +algorithm.focal_length=0.35 \ + +only_tune_refer=false \ + +dataset.customized_validation=true \ + +algorithm.customized_validation=true \ + algorithm.context_frames=90 \ + +algorithm.vis_gt=true \ + +algorithm.relative_embedding=true \ + dataset.save_dir=data/test_pumpkin \ + +algorithm.log_video=true \ + experiment.training.data.num_workers=4 \ + experiment.validation.data.num_workers=4 \ + +dataset.angle_range=30 \ + +dataset.pos_range=0.5 \ + +algorithm.cond_only_on_qk=true \ + +algorithm.add_pose_embed=false \ + +algorithm.use_domain_adapter=false \ + +algorithm.use_reference_attention=true \ + +algorithm.add_frame_timestep_embedder=true \ + +dataset.add_frame_timestep_embedder=true \ + experiment.validation.limit_batch=1 \ + algorithm.diffusion.sampling_timesteps=20 \ + +algorithm.is_interactive=true \ + +vae_path=checkpoints/vae_only.ckpt \ + +pose_predictor_path=checkpoints/pose_prediction_model_only.ckpt diff --git a/assets/desert.png b/assets/desert.png new file mode 100644 index 0000000000000000000000000000000000000000..7c96f86238de348054a78095325f93922e3d8331 --- /dev/null +++ b/assets/desert.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b85899ba8b3d111370fbcc25079d661a04d80563ecb43e55eb0c36f36c44b76 +size 297626 diff --git a/assets/examples/case1.npz b/assets/examples/case1.npz new file mode 100644 index 0000000000000000000000000000000000000000..21b2df98cdff679a9b0b237227dc8436c9c6a8ea --- /dev/null +++ b/assets/examples/case1.npz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38600ac4addd4546591f16c3cbed3e37ea3396286b3aee118938494f6c0527dd +size 201735738 diff --git a/assets/examples/case2.npz b/assets/examples/case2.npz new file mode 100644 index 0000000000000000000000000000000000000000..1433b549c1e910aa2dc283a3aa7bc51634ca3d27 --- /dev/null +++ b/assets/examples/case2.npz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a915bfd1a0ef18734f3b670b228eb4f8346736205fb571d4caa30d0a3cb919e4 +size 277385958 diff --git a/assets/examples/case3.npz b/assets/examples/case3.npz new file mode 100644 index 0000000000000000000000000000000000000000..fc20b16f782da3614a5262c1c4417e6a8876247e --- /dev/null +++ b/assets/examples/case3.npz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9d09230c9d3e5fca2f4a2f0a34823c98c644c7920e9a132ab631b72ad442fa7 +size 198933878 diff --git a/assets/examples/case4.npz b/assets/examples/case4.npz new file mode 100644 index 0000000000000000000000000000000000000000..6f6fefeb215fcffff82134958a111be124515f6d --- /dev/null +++ b/assets/examples/case4.npz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12cea63662d99d6e6349a602b2b5b5044de29b5176e7727e1e1eec528d2a56de +size 179320858 diff --git a/assets/ice_plains.png b/assets/ice_plains.png new file mode 100644 index 0000000000000000000000000000000000000000..dc38f4f9a7b8e9cfe2542f090024e15315a53ab4 --- /dev/null +++ b/assets/ice_plains.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ced8ab54ebb2c8c34b6fd10340dde905dc0f6a3096109521a08ee880688ae9cc +size 237764 diff --git a/assets/place.png b/assets/place.png new file mode 100644 index 0000000000000000000000000000000000000000..8d47e60f548acea600cb54a5e3408726d2e2206e --- /dev/null +++ b/assets/place.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4a1630a6f3e73c38e0dfec88bd902a5cf08bc8f857768e94199ea850d7eff81 +size 211717 diff --git a/assets/plains.png b/assets/plains.png new file mode 100644 index 0000000000000000000000000000000000000000..536aba4c67c9c0dcb41f4ad3a4b3fdf764616dc2 --- /dev/null +++ b/assets/plains.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adf5ad62acc998e35fec82c8e53b2559e26a7d78bb91a0f1cf8039a8610c3c78 +size 263028 diff --git a/assets/rain_sunflower_plains.png b/assets/rain_sunflower_plains.png new file mode 100644 index 0000000000000000000000000000000000000000..f50efb84d345fa9a808179679a0b40c988bbb7ab --- /dev/null +++ b/assets/rain_sunflower_plains.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2488d19febab9dac852b5d0b6e6894ac276f48a1788220f2a8d38c7030cf7a98 +size 386658 diff --git a/assets/savanna.png b/assets/savanna.png new file mode 100644 index 0000000000000000000000000000000000000000..b18f40e05ae68a7fc2c2acb99dbdcb926586d8e8 --- /dev/null +++ b/assets/savanna.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f8df1e988d84cd40f1af49eee73ef42d11f29d2c37a66bb5fda12d5b3278a55 +size 338562 diff --git a/assets/sunflower_plains.png b/assets/sunflower_plains.png new file mode 100644 index 0000000000000000000000000000000000000000..7c0a890c30d3f42dbb2bf6592471aaee48da39a9 --- /dev/null +++ b/assets/sunflower_plains.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98d828eb41fc7fb53909b66083db07208feffd66e88f4ae07092cc482e4e20df +size 283070 diff --git a/checkpoints b/checkpoints new file mode 160000 index 0000000000000000000000000000000000000000..9b993cb45f531cc6cd1951e18163019c72c6081f --- /dev/null +++ b/checkpoints @@ -0,0 +1 @@ +Subproject commit 9b993cb45f531cc6cd1951e18163019c72c6081f diff --git a/configurations/huggingface.yaml b/configurations/huggingface.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c65acad5ec1ab96e6f9285b3a66e49f423d5f270 --- /dev/null +++ b/configurations/huggingface.yaml @@ -0,0 +1,58 @@ +n_tokens: 3 +pose_cond_dim: 5 +use_plucker: true +focal_length: 0.35 +customized_validation: true +condition_similar_length: 8 +log_video: true +relative_embedding: true +cond_only_on_qk: true +add_pose_embed: false +use_domain_adapter: false +use_reference_attention: true +add_frame_timestep_embedder: true +is_interactive: true +diffusion: + sampling_timesteps: 20 + beta_schedule: sigmoid + objective: pred_v + use_fused_snr: True + cum_snr_decay: 0.96 + clip_noise: 20. + ddim_sampling_eta: 0.0 + stabilization_level: 15 + schedule_fn_kwargs: {} + use_snr: False + use_cum_snr: False + snr_clip: 5.0 + timesteps: 1000 + # architecture + architecture: + network_size: 64 + attn_heads: 4 + attn_dim_head: 64 + dim_mults: [1, 2, 4, 8] + resolution: ${dataset.resolution} + attn_resolutions: [16, 32, 64, 128] + use_init_temporal_attn: True + use_linear_attn: True + time_emb_type: rotary + +weight_decay: 2e-3 +warmup_steps: 10000 +optimizer_beta: [0.9, 0.99] +action_cond_dim: 25 +n_frames: 8 +frame_skip: 1 +frame_stack: 1 +uncertainty_scale: 1 +guidance_scale: 0.0 +chunk_size: 1 # -1 for full trajectory diffusion, number to specify diffusion chunk size +scheduling_matrix: autoregressive +noise_level: random_all +causal: True +x_shape: [3, 360, 640] +context_frames: 1 +diffusion_path: yslan/worldmem_checkpoints/diffusion_only.ckpt +vae_path: yslan/worldmem_checkpoints/vae_only.ckpt +pose_predictor_path: yslan/worldmem_checkpoints/pose_prediction_model_only.ckpt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f0251e0578279f5fa4ea6937d0786aaab057f5c2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +torch~=2.4.0 +torchvision~=0.19.1 +lightning~=2.1.2 +wandb~=0.17.0 +hydra-core~=1.3.2 +omegaconf~=2.3.0 +torchmetrics[image]==0.11.4 +wandb-osh==1.2.1 +gluonts[torch]==0.13.1 +pytorchvideo~=0.1.5 +colorama +tqdm +opencv-python +matplotlib +click +moviepy==1.0.3 +imageio +einops +pandas +pyzmq +pyrealsense2 +internetarchive +h5py +rotary_embedding_torch +diffusers +timm \ No newline at end of file diff --git a/split_checkpoint.py b/split_checkpoint.py new file mode 100644 index 0000000000000000000000000000000000000000..c136afcd92ea716754b2e33d6f9318308816ea8b --- /dev/null +++ b/split_checkpoint.py @@ -0,0 +1,9 @@ +import torch + +ckpt_path = "/mnt/xiaozeqi/diffusionforcing/outputs/2025-03-28/16-45-11/checkpoints/epoch0step595000.ckpt" +checkpoint = torch.load(ckpt_path, map_location="cpu") # map_location 可根据需要更换 + +state_dict = checkpoint['state_dict'] +pose_prediction_model_dict = {k.replace('pose_prediction_model.', ''): v for k, v in state_dict.items() if k.startswith('pose_prediction_model.')} + +torch.save({'state_dict': pose_prediction_model_dict}, "pose_prediction_model_only.ckpt") \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000000000000000000000000000000000000..b548eca483865461d19d29623088541f59d81c56 --- /dev/null +++ b/test.py @@ -0,0 +1,29 @@ +import gradio as gr + +css = """ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + +body { + background: linear-gradient(to bottom, #79c152 0%, #79c152 60%, #5c432d 100%) !important; + font-family: 'Press Start 2P', cursive !important; + color: #ffffff; +} + +.gr-button { + background-color: #3e8527 !important; + border: 2px solid #254d16 !important; + color: #ffffff !important; +} + +.gr-button:hover { + background-color: #6fcf44 !important; + border-color: #4a6c2d !important; +} +""" + +with gr.Blocks(css=css) as demo: + gr.Markdown("# 🌱 Minecraft 草地界面") + gr.Textbox(label="你想说啥") + gr.Button("点我") + +demo.launch() diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b0152f0645fbd11637c5b2217fe3021938ba33e3 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,7 @@ +# utils + +This is where you can put useful utilities like visualization, 3d conversion, logging etc + +--- + +This repo is forked from [Boyuan Chen](https://boyuan.space/)'s research template [repo](https://github.com/buoyancy99/research-template). By its MIT license, you must keep the above sentence in `README.md` and the `LICENSE` file to credit the author. diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/utils/ckpt_utils.py b/utils/ckpt_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9aa27f102f50b160b0c7ff5ed5b713f7bfe77b8b --- /dev/null +++ b/utils/ckpt_utils.py @@ -0,0 +1,32 @@ +from pathlib import Path +import wandb + + +def is_run_id(run_id: str) -> bool: + """Check if a string is a run ID.""" + return len(run_id) == 8 and run_id.isalnum() + + +def version_to_int(artifact) -> int: + """Convert versions of the form vX to X. For example, v12 to 12.""" + return int(artifact.version[1:]) + + +def download_latest_checkpoint(run_path: str, download_dir: Path) -> Path: + api = wandb.Api() + run = api.run(run_path) + + # Find the latest saved model checkpoint. + latest = None + for artifact in run.logged_artifacts(): + if artifact.type != "model" or artifact.state != "COMMITTED": + continue + + if latest is None or version_to_int(artifact) > version_to_int(latest): + latest = artifact + + # Download the checkpoint. + download_dir.mkdir(exist_ok=True, parents=True) + root = download_dir / run_path + latest.download(root=root) + return root / "model.ckpt" diff --git a/utils/cluster_utils.py b/utils/cluster_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7ae29488a44a59888ca7a97feb55e39447b2b393 --- /dev/null +++ b/utils/cluster_utils.py @@ -0,0 +1,40 @@ +""" +utils for submitting to clusters, such as slurm +""" + +import os +from omegaconf import DictConfig, OmegaConf +from datetime import datetime +from pathlib import Path + +from utils.print_utils import cyan + +# This is set below. +REPO_DIR = None + + +def submit_slurm_job( + cfg: DictConfig, + python_args: str, + project_root: Path, +): + log_dir = project_root / "slurm_logs" / f"{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}-{cfg.name}" + log_dir.mkdir(exist_ok=True, parents=True) + (project_root / "slurm_logs" / "latest").unlink(missing_ok=True) + (project_root / "slurm_logs" / "latest").symlink_to(log_dir, target_is_directory=True) + + params = dict(name=cfg.name, log_dir=log_dir, project_root=project_root, python_args=python_args) + params.update(cfg.cluster.params) + + slurm_script = cfg.cluster.launch_template.format(**params) + + slurm_script_path = log_dir / "job.slurm" + with slurm_script_path.open("w") as f: + f.write(slurm_script) + + os.system(f"chmod +x {slurm_script_path}") + os.system(f"sbatch {slurm_script_path}") + + print(f"\n{cyan('script:')} {slurm_script_path}\n{cyan('slurm errors and logs:')} {log_dir}\n") + + return log_dir diff --git a/utils/distributed_utils.py b/utils/distributed_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..dde3e98aef29b3fe6b7eb1c58f589b1d0d6c99ce --- /dev/null +++ b/utils/distributed_utils.py @@ -0,0 +1,3 @@ +import wandb + +is_rank_zero = wandb.run is not None diff --git a/utils/logging_utils.py b/utils/logging_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f5b8ac222ff86634440ccc8efa0c3a1f22660843 --- /dev/null +++ b/utils/logging_utils.py @@ -0,0 +1,435 @@ +from typing import Optional +import wandb +import numpy as np +import torch + +import matplotlib.pyplot as plt +import cv2 +import matplotlib.pyplot as plt +from tqdm import trange, tqdm +import matplotlib.animation as animation +from pathlib import Path + +plt.set_loglevel("warning") + +from torchmetrics.functional import mean_squared_error, peak_signal_noise_ratio +from torchmetrics.functional import ( + structural_similarity_index_measure, + universal_image_quality_index, +) +from algorithms.common.metrics import ( + FrechetVideoDistance, + LearnedPerceptualImagePatchSimilarity, + FrechetInceptionDistance, +) + + +# FIXME: clean up & check this util +def log_video( + observation_hat, + observation_gt=None, + step=0, + namespace="train", + prefix="video", + context_frames=0, + color=(255, 0, 0), + logger=None, +): + """ + take in video tensors in range [-1, 1] and log into wandb + + :param observation_hat: predicted observation tensor of shape (frame, batch, channel, height, width) + :param observation_gt: ground-truth observation tensor of shape (frame, batch, channel, height, width) + :param step: an int indicating the step number + :param namespace: a string specify a name space this video logging falls under, e.g. train, val + :param prefix: a string specify a prefix for the video name + :param context_frames: an int indicating how many frames in observation_hat are ground truth given as context + :param color: a tuple of 3 numbers specifying the color of the border for ground truth frames + :param logger: optional logger to use. use global wandb if not specified + """ + if not logger: + logger = wandb + + # observation_gt = torch.zeros_like(observation_hat) + # observation_hat[:context_frames] = observation_gt[:context_frames] + # Add red border of 1 pixel width to the context frames + # for i, c in enumerate(color): + # c = c / 255.0 + # observation_hat[:context_frames, :, i, [0, -1], :] = c + # observation_hat[:context_frames, :, i, :, [0, -1]] = c + + # if observation_gt is not None: + # observation_gt[:context_frames, :, i, [0, -1], :] = c + # observation_gt[:context_frames, :, i, :, [0, -1]] = c + + if observation_gt is not None: + video = torch.cat([observation_hat, observation_gt], -2).detach().cpu().numpy() + else: + video = torch.cat([observation_hat], -1).detach().cpu().numpy() + video = np.transpose(np.clip(video, a_min=0.0, a_max=1.0) * 255, (1, 0, 2, 3, 4)).astype(np.uint8) + # video[..., 1:] = video[..., :1] # remove framestack, only visualize current frame + n_samples = len(video) + # use wandb directly here since pytorch lightning doesn't support logging videos yet + for i in range(n_samples): + logger.log( + { + f"{namespace}/{prefix}_{i}": wandb.Video(video[i], fps=5), + f"trainer/global_step": step, + } + ) + + +def get_validation_metrics_for_videos( + observation_hat, + observation_gt, + lpips_model: Optional[LearnedPerceptualImagePatchSimilarity] = None, + fid_model: Optional[FrechetInceptionDistance] = None, + fvd_model: Optional[FrechetVideoDistance] = None, +): + """ + :param observation_hat: predicted observation tensor of shape (frame, batch, channel, height, width) + :param observation_gt: ground-truth observation tensor of shape (frame, batch, channel, height, width) + :param lpips_model: a LearnedPerceptualImagePatchSimilarity object from algorithm.common.metrics + :param fid_model: a FrechetInceptionDistance object from algorithm.common.metrics + :param fvd_model: a FrechetVideoDistance object from algorithm.common.metrics + :return: a tuple of metrics + """ + frame, batch, channel, height, width = observation_hat.shape + output_dict = {} + observation_gt = observation_gt.type_as(observation_hat) # some metrics don't fully support fp16 + + if frame < 9: + fvd_model = None # FVD requires at least 9 frames + + observation_hat = observation_hat.float() + observation_gt = observation_gt.float() + + # observation_hat = observation_hat.float().to(next(lpips_model.parameters()).device) + # observation_gt = observation_gt.float().to(next(lpips_model.parameters()).device) + # if fvd_model is not None: + # output_dict["fvd"] = fvd_model.compute(torch.clamp(observation_hat, -1.0, 1.0), torch.clamp(observation_gt, -1.0, 1.0)) + + frame_wise_psnr = [] + for f in range(observation_hat.shape[0]): + frame_wise_psnr.append(peak_signal_noise_ratio(observation_hat[f], observation_gt[f], data_range=2.0)) + frame_wise_psnr = torch.stack(frame_wise_psnr) + + output_dict["frame_wise_psnr"] = frame_wise_psnr + observation_hat = observation_hat.view(-1, channel, height, width) + observation_gt = observation_gt.view(-1, channel, height, width) + + output_dict["mse"] = mean_squared_error(observation_hat, observation_gt) + + output_dict["psnr"] = peak_signal_noise_ratio(observation_hat, observation_gt, data_range=2.0) + # output_dict["ssim"] = structural_similarity_index_measure(observation_hat, observation_gt, data_range=2.0) + # output_dict["uiqi"] = universal_image_quality_index(observation_hat, observation_gt) + # operations for LPIPS and FID + observation_hat = torch.clamp(observation_hat, -1.0, 1.0) + observation_gt = torch.clamp(observation_gt, -1.0, 1.0) + + if lpips_model is not None: + lpips_model.update(observation_hat, observation_gt) + lpips = lpips_model.compute().item() + # Reset the states of non-functional metrics + output_dict["lpips"] = lpips + lpips_model.reset() + + if fid_model is not None: + observation_hat_uint8 = ((observation_hat + 1.0) / 2 * 255).type(torch.uint8) + observation_gt_uint8 = ((observation_gt + 1.0) / 2 * 255).type(torch.uint8) + fid_model.update(observation_gt_uint8, real=True) + fid_model.update(observation_hat_uint8, real=False) + fid = fid_model.compute() + output_dict["fid"] = fid + # Reset the states of non-functional metrics + fid_model.reset() + + return output_dict + + +def is_grid_env(env_id): + return "maze2d" in env_id or "diagonal2d" in env_id + + +def get_maze_grid(env_id): + # import gym + # maze_string = gym.make(env_id).str_maze_spec + if "large" in env_id: + maze_string = "############\\#OOOO#OOOOO#\\#O##O#O#O#O#\\#OOOOOO#OOO#\\#O####O###O#\\#OO#O#OOOOO#\\##O#O#O#O###\\#OO#OOO#OGO#\\############" + if "medium" in env_id: + maze_string = "########\\#OO##OO#\\#OO#OOO#\\##OOO###\\#OO#OOO#\\#O#OO#O#\\#OOO#OG#\\########" + if "umaze" in env_id: + maze_string = "#####\\#GOO#\\###O#\\#OOO#\\#####" + lines = maze_string.split("\\") + grid = [line[1:-1] for line in lines] + return grid[1:-1] + + +def get_random_start_goal(env_id, batch_size): + maze_grid = get_maze_grid(env_id) + s2i = {"O": 0, "#": 1, "G": 2} + maze_grid = [[s2i[s] for s in r] for r in maze_grid] + maze_grid = np.array(maze_grid) + x, y = np.nonzero(maze_grid == 0) + indices = np.random.randint(len(x), size=batch_size) + start = np.stack([x[indices], y[indices]], -1) + 1 + x, y = np.nonzero(maze_grid == 2) + goal = np.concatenate([x, y], -1) + goal = np.tile(goal[None, :], (batch_size, 1)) + 1 + return start, goal + + +def plot_maze_layout(ax, maze_grid): + ax.clear() + + if maze_grid is not None: + for i, row in enumerate(maze_grid): + for j, cell in enumerate(row): + if cell == "#": + square = plt.Rectangle((i + 0.5, j + 0.5), 1, 1, edgecolor="black", facecolor="black") + ax.add_patch(square) + + ax.set_aspect("equal") + ax.grid(True, color="white", linewidth=4) + ax.set_axisbelow(True) + ax.spines["top"].set_linewidth(4) + ax.spines["right"].set_linewidth(4) + ax.spines["bottom"].set_linewidth(4) + ax.spines["left"].set_linewidth(4) + ax.set_facecolor("lightgray") + ax.tick_params( + axis="both", + which="both", + bottom=False, + top=False, + left=False, + right=False, + labelbottom=False, + labelleft=False, + ) + ax.set_xticks(np.arange(0.5, len(maze_grid) + 0.5)) + ax.set_yticks(np.arange(0.5, len(maze_grid[0]) + 0.5)) + ax.set_xlim(0.5, len(maze_grid) + 0.5) + ax.set_ylim(0.5, len(maze_grid[0]) + 0.5) + ax.grid(True, color="white", which="minor", linewidth=4) + + +def plot_start_goal(ax, start_goal: None): + def draw_star(center, radius, num_points=5, color="black"): + angles = np.linspace(0.0, 2 * np.pi, num_points, endpoint=False) + 5 * np.pi / (2 * num_points) + inner_radius = radius / 2.0 + + points = [] + for angle in angles: + points.extend( + [ + center[0] + radius * np.cos(angle), + center[1] + radius * np.sin(angle), + center[0] + inner_radius * np.cos(angle + np.pi / num_points), + center[1] + inner_radius * np.sin(angle + np.pi / num_points), + ] + ) + + star = plt.Polygon(np.array(points).reshape(-1, 2), color=color) + ax.add_patch(star) + + start_x, start_y = start_goal[0] + start_outer_circle = plt.Circle((start_x, start_y), 0.16, facecolor="white", edgecolor="black") + ax.add_patch(start_outer_circle) + start_inner_circle = plt.Circle((start_x, start_y), 0.08, color="black") + ax.add_patch(start_inner_circle) + + goal_x, goal_y = start_goal[1] + goal_outer_circle = plt.Circle((goal_x, goal_y), 0.16, facecolor="white", edgecolor="black") + ax.add_patch(goal_outer_circle) + draw_star((goal_x, goal_y), radius=0.08) + + +def make_trajectory_images(env_id, trajectory, batch_size, start, goal, plot_end_points=True): + images = [] + for batch_idx in range(batch_size): + fig, ax = plt.subplots() + if is_grid_env(env_id): + maze_grid = get_maze_grid(env_id) + else: + maze_grid = None + plot_maze_layout(ax, maze_grid) + ax.scatter(trajectory[:, batch_idx, 0], trajectory[:, batch_idx, 1], c=np.arange(len(trajectory)), cmap="Reds"), + if plot_end_points: + start_goal = (start[batch_idx], goal[batch_idx]) + plot_start_goal(ax, start_goal) + # plt.title(f"sample_{batch_idx}") + fig.tight_layout() + fig.canvas.draw() + img_shape = fig.canvas.get_width_height()[::-1] + (4,) + img = np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).copy().reshape(img_shape) + images.append(img) + + plt.close() + return images + + +def make_convergence_animation( + env_id, + plan_history, + trajectory, + start, + goal, + open_loop_horizon, + namespace, + interval=100, + plot_end_points=True, + batch_idx=0, +): + # - plan_history: contains for each time step all the MPC predicted plans for each pyramid noise level. + # Structured as a list of length (episode_len // open_loop_horizon), where each + # element corresponds to a control_time_step and stores a list of length pyramid_height, + # where each element is a plan at a different pyramid noise level and stored as a tensor of + # shape (episode_len // open_loop_horizon - control_time_step, + # batch_size, x_stacked_shape) + + # select index and prune history + start, goal = start[batch_idx], goal[batch_idx] + trajectory = trajectory[:, batch_idx] + plan_history = [[pm[:, batch_idx] for pm in pt] for pt in plan_history] + trajectory, plan_history = prune_history(plan_history, trajectory, goal, open_loop_horizon) + + # animate the convergence of the first plan + fig, ax = plt.subplots() + if "large" in env_id: + fig.set_size_inches(3.5, 5) + else: + fig.set_size_inches(3, 3) + ax.set_axis_off() + fig.subplots_adjust(left=0, bottom=0, right=1, top=1) + + if is_grid_env(env_id): + maze_grid = get_maze_grid(env_id) + else: + maze_grid = None + + def update(frame): + plot_maze_layout(ax, maze_grid) + + plan_history_m = plan_history[0][frame] + plan_history_m = plan_history_m.numpy() + ax.scatter( + plan_history_m[:, 0], + plan_history_m[:, 1], + c=np.arange(len(plan_history_m))[::-1], + cmap="Reds", + ) + + if plot_end_points: + plot_start_goal(ax, (start, goal)) + + frames = tqdm(range(len(plan_history[0])), desc="Making convergence animation") + ani = animation.FuncAnimation(fig, update, frames=frames, interval=interval) + prefix = wandb.run.id if wandb.run is not None else env_id + filename = f"/tmp/{prefix}_{namespace}_convergence.mp4" + ani.save(filename, writer="ffmpeg", fps=5) + return filename + + +def prune_history(plan_history, trajectory, goal, open_loop_horizon): + dist = np.linalg.norm( + trajectory[:, :2] - np.array(goal)[None], + axis=-1, + ) + reached = dist < 0.2 + if reached.any(): + cap_idx = np.argmax(reached) + trajectory = trajectory[: cap_idx + open_loop_horizon + 1] + plan_history = plan_history[: cap_idx // open_loop_horizon + 2] + + pruned_plan_history = [] + for plans in plan_history: + pruned_plan_history.append([]) + for m in range(len(plans)): + plan = plans[m] + pruned_plan_history[-1].append(plan) + plan = pruned_plan_history[-1][-1] + dist = np.linalg.norm(plan.numpy()[:, :2] - np.array(goal)[None], axis=-1) + reached = dist < 0.2 + if reached.any(): + cap_idx = np.argmax(reached) + 1 + pruned_plan_history[-1] = [p[:cap_idx] for p in pruned_plan_history[-1]] + return trajectory, pruned_plan_history + + +def make_mpc_animation( + env_id, + plan_history, + trajectory, + start, + goal, + open_loop_horizon, + namespace, + interval=100, + plot_end_points=True, + batch_idx=0, +): + # - plan_history: contains for each time step all the MPC predicted plans for each pyramid noise level. + # Structured as a list of length (episode_len // open_loop_horizon), where each + # element corresponds to a control_time_step and stores a list of length pyramid_height, + # where each element is a plan at a different pyramid noise level and stored as a tensor of + # shape (episode_len // open_loop_horizon - control_time_step, + # batch_size, x_stacked_shape) + + # select index and prune history + start, goal = start[batch_idx], goal[batch_idx] + trajectory = trajectory[:, batch_idx] + plan_history = [[pm[:, batch_idx] for pm in pt] for pt in plan_history] + trajectory, plan_history = prune_history(plan_history, trajectory, goal, open_loop_horizon) + + # animate the convergence of the plans + fig, ax = plt.subplots() + if "large" in env_id: + fig.set_size_inches(3.5, 5) + else: + fig.set_size_inches(3, 3) + ax.set_axis_off() + fig.subplots_adjust(left=0, bottom=0, right=1, top=1) + trajectory_colors = np.linspace(0, 1, len(trajectory)) + + if is_grid_env(env_id): + maze_grid = get_maze_grid(env_id) + else: + maze_grid = None + + def update(frame): + control_time_step = 0 + while frame >= 0: + frame -= len(plan_history[control_time_step]) + control_time_step += 1 + control_time_step -= 1 + m = frame + len(plan_history[control_time_step]) + num_steps_taken = 1 + open_loop_horizon * control_time_step + plot_maze_layout(ax, maze_grid) + + plan_history_m = plan_history[control_time_step][m] + plan_history_m = plan_history_m.numpy() + ax.scatter( + trajectory[:num_steps_taken, 0], + trajectory[:num_steps_taken, 1], + c=trajectory_colors[:num_steps_taken], + cmap="Blues", + ) + ax.scatter( + plan_history_m[:, 0], + plan_history_m[:, 1], + c=np.arange(len(plan_history_m))[::-1], + cmap="Reds", + ) + + if plot_end_points: + plot_start_goal(ax, (start, goal)) + + num_frames = sum([len(p) for p in plan_history]) + frames = tqdm(range(num_frames), desc="Making MPC animation") + ani = animation.FuncAnimation(fig, update, frames=frames, interval=interval) + prefix = wandb.run.id if wandb.run is not None else env_id + filename = f"/tmp/{prefix}_{namespace}_mpc.mp4" + ani.save(filename, writer="ffmpeg", fps=5) + + return filename diff --git a/utils/print_utils.py b/utils/print_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c1c9052267f0390c1e0068be6f3bac453d3c4d23 --- /dev/null +++ b/utils/print_utils.py @@ -0,0 +1,5 @@ +from colorama import Fore + + +def cyan(x: str) -> str: + return f"{Fore.CYAN}{x}{Fore.RESET}" diff --git a/utils/wandb_utils.py b/utils/wandb_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..3c4df932c82e2b4f2dd3510e39faa7ecfee19279 --- /dev/null +++ b/utils/wandb_utils.py @@ -0,0 +1,175 @@ +from pathlib import Path +from datetime import timedelta +from typing import TYPE_CHECKING, Any, Literal, Mapping, Optional, Union +from typing_extensions import override +from functools import wraps +import os +from wandb_osh.hooks import TriggerWandbSyncHook +import time +from lightning.pytorch.loggers.wandb import WandbLogger, _scan_checkpoints, ModelCheckpoint, Tensor +from lightning.pytorch.utilities.rank_zero import rank_zero_only +from lightning.fabric.utilities.types import _PATH + + +if TYPE_CHECKING: + from wandb.sdk.lib import RunDisabled + from wandb.wandb_run import Run + + +class SpaceEfficientWandbLogger(WandbLogger): + """ + A wandb logger that by default overrides artifacts to save space, instead of creating new version. + A variable expiration_days can be set to control how long older versions of artifacts are kept. + By default, the latest version is kept indefinitely, while older versions are kept for 5 days. + """ + + def __init__( + self, + name: Optional[str] = None, + save_dir: _PATH = ".", + version: Optional[str] = None, + offline: bool = False, + dir: Optional[_PATH] = None, + id: Optional[str] = None, + anonymous: Optional[bool] = None, + project: Optional[str] = None, + log_model: Union[Literal["all"], bool] = False, + experiment: Union["Run", "RunDisabled", None] = None, + prefix: str = "", + checkpoint_name: Optional[str] = None, + expiration_days: Optional[int] = 5, + **kwargs: Any, + ) -> None: + super().__init__( + name=name, + save_dir=save_dir, + version=version, + offline=False, + dir=dir, + id=id, + anonymous=anonymous, + project=project, + log_model=log_model, + experiment=experiment, + prefix=prefix, + checkpoint_name=checkpoint_name, + **kwargs, + ) + + super().__init__( + name=name, + save_dir=save_dir, + version=version, + offline=offline, + dir=dir, + id=id, + anonymous=anonymous, + project=project, + log_model=log_model, + experiment=experiment, + prefix=prefix, + checkpoint_name=checkpoint_name, + **kwargs, + ) + self.expiration_days = expiration_days + self._last_artifacts = [] + + def _scan_and_log_checkpoints(self, checkpoint_callback: ModelCheckpoint) -> None: + import wandb + + # get checkpoints to be saved with associated score + checkpoints = _scan_checkpoints(checkpoint_callback, self._logged_model_time) + + # log iteratively all new checkpoints + artifacts = [] + for t, p, s, tag in checkpoints: + metadata = { + "score": s.item() if isinstance(s, Tensor) else s, + "original_filename": Path(p).name, + checkpoint_callback.__class__.__name__: { + k: getattr(checkpoint_callback, k) + for k in [ + "monitor", + "mode", + "save_last", + "save_top_k", + "save_weights_only", + "_every_n_train_steps", + ] + # ensure it does not break if `ModelCheckpoint` args change + if hasattr(checkpoint_callback, k) + }, + } + if not self._checkpoint_name: + self._checkpoint_name = f"model-{self.experiment.id}" + + artifact = wandb.Artifact(name=self._checkpoint_name, type="model", metadata=metadata) + artifact.add_file(p, name="model.ckpt") + aliases = ["latest", "best"] if p == checkpoint_callback.best_model_path else ["latest"] + self.experiment.log_artifact(artifact, aliases=aliases) + # remember logged models - timestamp needed in case filename didn't change (lastkckpt or custom name) + self._logged_model_time[p] = t + artifacts.append(artifact) + + for artifact in self._last_artifacts: + if not self._offline: + artifact.wait() + artifact.ttl = timedelta(days=self.expiration_days) + artifact.save() + self._last_artifacts = artifacts + + +class OfflineWandbLogger(SpaceEfficientWandbLogger): + """ + Wraps WandbLogger to trigger offline sync hook occasionally. + This is useful when running on slurm clusters, many of which + only has internet on login nodes, not compute nodes. + """ + + def __init__( + self, + name: Optional[str] = None, + save_dir: _PATH = ".", + version: Optional[str] = None, + offline: bool = False, + dir: Optional[_PATH] = None, + id: Optional[str] = None, + anonymous: Optional[bool] = None, + project: Optional[str] = None, + log_model: Union[Literal["all"], bool] = False, + experiment: Union["Run", "RunDisabled", None] = None, + prefix: str = "", + checkpoint_name: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__( + name=name, + save_dir=save_dir, + version=version, + offline=False, + dir=dir, + id=id, + anonymous=anonymous, + project=project, + log_model=log_model, + experiment=experiment, + prefix=prefix, + checkpoint_name=checkpoint_name, + **kwargs, + ) + self._offline = offline + communication_dir = Path(".wandb_osh_command_dir") + communication_dir.mkdir(parents=True, exist_ok=True) + self.trigger_sync = TriggerWandbSyncHook(communication_dir) + self.last_sync_time = 0.0 + self.min_sync_interval = 60 + self.wandb_dir = os.path.join(self._save_dir, "wandb/latest-run") + + @override + @rank_zero_only + def log_metrics(self, metrics: Mapping[str, float], step: Optional[int] = None) -> None: + out = super().log_metrics(metrics, step) + if time.time() - self.last_sync_time > self.min_sync_interval: + self.trigger_sync(self.wandb_dir) + self.last_sync_time = time.time() + return out