From 5e87019100745388714f965bb8cb9563f6d30f46 Mon Sep 17 00:00:00 2001 From: smthem <2102561739@qq.com> Date: Fri, 9 Aug 2024 20:56:22 +0800 Subject: [PATCH] add new node --- FoleyCrafter_node.py | 351 ++ __init__.py | 4 + assets/foleycrafter.png | Bin 0 -> 539788 bytes foleycrafter/data/__init__.py | 27 + foleycrafter/data/dataset.py | 155 + foleycrafter/data/video_transforms.py | 404 ++ .../models/adapters/attention_processor.py | 655 +++ foleycrafter/models/adapters/ip_adapter.py | 189 + foleycrafter/models/adapters/resampler.py | 158 + foleycrafter/models/adapters/transformer.py | 352 ++ foleycrafter/models/adapters/utils.py | 86 + foleycrafter/models/auffusion/attention.py | 668 ++++ .../models/auffusion/attention_processor.py | 2691 +++++++++++++ .../models/auffusion/dual_transformer_2d.py | 155 + .../models/auffusion/loaders/ip_adapter.py | 524 +++ foleycrafter/models/auffusion/loaders/unet.py | 992 +++++ foleycrafter/models/auffusion/resnet.py | 685 ++++ .../models/auffusion/transformer_2d.py | 460 +++ .../models/auffusion/unet_2d_blocks.py | 3501 +++++++++++++++++ foleycrafter/models/auffusion_unet.py | 1257 ++++++ foleycrafter/models/onset/__init__.py | 5 + foleycrafter/models/onset/r2plus1d_18.py | 48 + foleycrafter/models/onset/resnet.py | 323 ++ foleycrafter/models/onset/torch_utils.py | 113 + foleycrafter/models/onset/video_onset_net.py | 31 + foleycrafter/models/time_detector/model.py | 17 + foleycrafter/models/time_detector/resnet.py | 322 ++ foleycrafter/pipelines/auffusion_pipeline.py | 2155 ++++++++++ foleycrafter/pipelines/pipeline_controlnet.py | 1341 +++++++ foleycrafter/utils/converter.py | 494 +++ foleycrafter/utils/spec_to_mel.py | 399 ++ foleycrafter/utils/util.py | 1721 ++++++++ requirements.txt | 6 + 33 files changed, 20289 insertions(+) create mode 100644 FoleyCrafter_node.py create mode 100644 __init__.py create mode 100644 assets/foleycrafter.png create mode 100644 foleycrafter/data/__init__.py create mode 100644 foleycrafter/data/dataset.py create mode 100644 foleycrafter/data/video_transforms.py create mode 100644 foleycrafter/models/adapters/attention_processor.py create mode 100644 foleycrafter/models/adapters/ip_adapter.py create mode 100644 foleycrafter/models/adapters/resampler.py create mode 100644 foleycrafter/models/adapters/transformer.py create mode 100644 foleycrafter/models/adapters/utils.py create mode 100644 foleycrafter/models/auffusion/attention.py create mode 100644 foleycrafter/models/auffusion/attention_processor.py create mode 100644 foleycrafter/models/auffusion/dual_transformer_2d.py create mode 100644 foleycrafter/models/auffusion/loaders/ip_adapter.py create mode 100644 foleycrafter/models/auffusion/loaders/unet.py create mode 100644 foleycrafter/models/auffusion/resnet.py create mode 100644 foleycrafter/models/auffusion/transformer_2d.py create mode 100644 foleycrafter/models/auffusion/unet_2d_blocks.py create mode 100644 foleycrafter/models/auffusion_unet.py create mode 100644 foleycrafter/models/onset/__init__.py create mode 100644 foleycrafter/models/onset/r2plus1d_18.py create mode 100644 foleycrafter/models/onset/resnet.py create mode 100644 foleycrafter/models/onset/torch_utils.py create mode 100644 foleycrafter/models/onset/video_onset_net.py create mode 100644 foleycrafter/models/time_detector/model.py create mode 100644 foleycrafter/models/time_detector/resnet.py create mode 100644 foleycrafter/pipelines/auffusion_pipeline.py create mode 100644 foleycrafter/pipelines/pipeline_controlnet.py create mode 100644 foleycrafter/utils/converter.py create mode 100644 foleycrafter/utils/spec_to_mel.py create mode 100644 foleycrafter/utils/util.py create mode 100644 requirements.txt diff --git a/FoleyCrafter_node.py b/FoleyCrafter_node.py new file mode 100644 index 0000000..390c600 --- /dev/null +++ b/FoleyCrafter_node.py @@ -0,0 +1,351 @@ +# !/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +import random +import sys +import cv2 +import numpy as np +import torchaudio +from PIL import Image + +import platform +import subprocess +import os.path as osp +import soundfile as sf +import torch +import torchvision +from huggingface_hub import snapshot_download, hf_hub_download +from moviepy.editor import AudioFileClip, VideoFileClip + +from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection +from .foleycrafter.models.onset import torch_utils +from .foleycrafter.models.time_detector.model import VideoOnsetNet +from .foleycrafter.pipelines.auffusion_pipeline import Generator, denormalize_spectrogram +from. foleycrafter.utils.util import build_foleycrafter, read_frames_with_moviepy +import folder_paths +from comfy.utils import common_upscale + +vision_transform_list = [ + torchvision.transforms.Resize((128, 128)), + torchvision.transforms.CenterCrop((112, 112)), + torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), +] +video_transform = torchvision.transforms.Compose(vision_transform_list) + +MAX_SEED = np.iinfo(np.int32).max + +ffmpeg_path = os.getenv('FFMPEG_PATH') +if ffmpeg_path is None and platform.system() in ['Linux', 'Darwin']: + try: + result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True) + if result.returncode == 0: + ffmpeg_path = result.stdout.strip() + print(f"FFmpeg is installed at: {ffmpeg_path}") + else: + print("FFmpeg is not installed. Please download ffmpeg-static and export to FFMPEG_PATH.") + print("For example: export FFMPEG_PATH=/you_path/ffmpeg-4.4-amd64-static") + except Exception as e: + pass + +if ffmpeg_path is not None and ffmpeg_path not in os.getenv('PATH'): + print("Adding FFMPEG_PATH to PATH") + os.environ["PATH"] = f"{ffmpeg_path}:{os.environ['PATH']}" + +device = ("cuda" + if torch.cuda.is_available() + else "mps" if torch.backends.mps.is_available() else "cpu" +) + +weigths_current_path = os.path.join(folder_paths.models_dir, "foleycrafter") +if not os.path.exists(weigths_current_path): + os.makedirs(weigths_current_path) + +weigths_semantic_current_path = os.path.join(weigths_current_path, "semantic") +if not os.path.exists(weigths_semantic_current_path): + os.makedirs(weigths_semantic_current_path) + +weigths_vocoder_current_path = os.path.join(weigths_current_path, "vocoder") +if not os.path.exists(weigths_vocoder_current_path): + os.makedirs(weigths_vocoder_current_path) + + +def tensor_to_pil(tensor): + image_np = tensor.squeeze().mul(255).clamp(0, 255).byte().numpy() + image = Image.fromarray(image_np, mode='RGB') + return image + + +def pil2narry(img): + narry = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0) + return narry + + +def narry_list(list_in): + for i in range(len(list_in)): + value = list_in[i] + modified_value = pil2narry(value) + list_in[i] = modified_value + return list_in + + +def nomarl_upscale(img_tensor, width, height): + samples = img_tensor.movedim(-1, 1) + img = common_upscale(samples, width, height, "nearest-exact", "center") + samples = img.movedim(1, -1) + img_pil = tensor_to_pil(samples) + return img_pil + + +def get_instance_path(path): + instance_path = os.path.normpath(path) + if sys.platform == 'win32': + instance_path = instance_path.replace('\\', "/") + return instance_path + + +def download_weights(file_dir,repo_id,subfolder="",pt_name=""): + if subfolder: + file_path = os.path.join(file_dir,subfolder, pt_name) + sub_dir=os.path.join(file_dir,subfolder) + if not os.path.exists(sub_dir): + os.makedirs(sub_dir) + if not os.path.exists(file_path): + pt_path = hf_hub_download( + repo_id=repo_id, + subfolder=subfolder, + filename=pt_name, + local_dir = file_dir, + ) + else: + pt_path=get_instance_path(file_path) + return pt_path + else: + file_path = os.path.join(file_dir, pt_name) + if not os.path.exists(file_dir): + os.makedirs(file_dir) + if not os.path.exists(file_path): + pt_path = hf_hub_download( + repo_id=repo_id, + filename=pt_name, + local_dir=file_dir, + ) + else: + pt_path=get_instance_path(file_path) + return pt_path + + +def build_models(repo,device,semantic_scale): + # download ckpt + if not repo: + repo = snapshot_download("auffusion/auffusion-full-no-adapter") + # pre models + download_weights(weigths_current_path, "ymzhang319/FoleyCrafter", subfolder="semantic", + pt_name="semantic_adapter.bin") + download_weights(weigths_current_path, "ymzhang319/FoleyCrafter", subfolder="vocoder", + pt_name="vocoder.pt") + download_weights(weigths_current_path, "ymzhang319/FoleyCrafter", subfolder="vocoder", + pt_name="config.json") + + # ckpt path + temporal_ckpt_path=download_weights(weigths_current_path, "ymzhang319/FoleyCrafter", + pt_name="temporal_adapter.ckpt") + # load vocoder + vocoder = Generator.from_pretrained(weigths_vocoder_current_path).to(device) + + # load time_detector + time_detector_ckpt=download_weights(weigths_current_path, "ymzhang319/FoleyCrafter", + pt_name="timestamp_detector.pth.tar") + + time_detector = VideoOnsetNet(False) + time_detector, _ = torch_utils.load_model(time_detector_ckpt, time_detector, device=device, strict=True) + + # load adapters + pipe = build_foleycrafter(repo).to(device) + ckpt = torch.load(temporal_ckpt_path) + + # load temporal adapter + if "state_dict" in ckpt.keys(): + ckpt = ckpt["state_dict"] + load_gligen_ckpt = {} + for key, value in ckpt.items(): + if key.startswith("module."): + load_gligen_ckpt[key[len("module.") :]] = value + else: + load_gligen_ckpt[key] = value + m, u = pipe.controlnet.load_state_dict(load_gligen_ckpt, strict=False) + print(f"### Control Net missing keys: {len(m)}; \n### unexpected keys: {len(u)};") + + # load semantic adapter + pipe.load_ip_adapter(weigths_semantic_current_path, subfolder="", weight_name="semantic_adapter.bin", image_encoder_folder=None) + pipe.set_ip_adapter_scale(semantic_scale) + + return pipe, vocoder, time_detector + + +class FoleyCrafter_LoadModel: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "repo_id":("STRING", {"default": "auffusion/auffusion-full-no-adapter"}), + "ip_repo":("STRING", {"default": "h94/IP-Adapter"}), + "semantic_scale":("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1, "round": 0.01}), + } + } + + RETURN_TYPES = ("MODEL","MODEL","MODEL","MODEL",) + RETURN_NAMES = ("pipe","vocoder","time_detector","image_encoder",) + FUNCTION = "main_loader" + CATEGORY = "FoleyCrafter" + + def main_loader(self,repo_id,ip_repo,semantic_scale): + pipe, vocoder, time_detector=build_models(repo_id, device, semantic_scale) + image_encoder = CLIPVisionModelWithProjection.from_pretrained(ip_repo, subfolder="models/image_encoder").to(device) + return (pipe,vocoder, time_detector,image_encoder,) + + +class FoleyCrafter_Sampler: + @classmethod + def INPUT_TYPES(s): + input_path = folder_paths.get_input_directory() + video_files = [f for f in os.listdir(input_path) if + os.path.isfile(os.path.join(input_path, f)) and f.split('.')[-1] in ['webm', 'mp4', 'mkv','gif']] + return { + "required": { + "pipe": ("MODEL",), + "vocoder": ("MODEL",), + "time_detector": ("MODEL",), + "image_encoder": ("MODEL",), + "video_files": (["none"] + video_files,), + "prompt": ("STRING", {"multiline": True,"default": "1 girl"}), + "negative_prompt": ("STRING", {"multiline": True,"default": "bad quality"}), + "max_frame": ("INT", {"default": 150, "min": 64, "step": 1,"max": 500}), + "seeds": ("INT", {"default": 0, "min": 0, "max": 10000}), + "controlnet_scale": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.1, "round": 0.01}), + "steps": ("INT", {"default": 25, "min": 1, "max": 100}), + "sample_width": ("INT", {"default": 1024, "min": 128, "max": 1024, "step": 64, "display": "number"}), + "sample_height": ("INT", {"default": 256, "min": 128, "max": 1024, "step": 64, "display": "number"}), + "video_dubbing": ("BOOLEAN", {"default": False},), } + } + + RETURN_TYPES = ("IMAGE", "AUDIO", "FLOAT") + RETURN_NAMES = ("image", "audio", "frame_rate",) + FUNCTION = "fc_main" + CATEGORY = "FoleyCrafter" + + def run_inference(self,pipe, vocoder, time_detector,controlnet_scale,seeds,image_encoder,video_files,max_frame,prompt,negative_prompt,steps,width,height,video_dubbing): + + generator = torch.Generator(device=device) + generator.manual_seed(seeds) + image_processor = CLIPImageProcessor() + + source_video = cv2.VideoCapture(video_files) + fps = source_video.get(cv2.CAP_PROP_FPS) + v_width = int(source_video.get(cv2.CAP_PROP_FRAME_WIDTH)) # 获取视频的fps w h + v_height = int(source_video.get(cv2.CAP_PROP_FRAME_HEIGHT)) + source_video.release() + + with torch.no_grad(): + print(f" >>> Begin Inference: {video_files} <<< ") + frames, duration,frames_list = read_frames_with_moviepy(video_files, max_frame_nums=max_frame) + + time_frames = torch.FloatTensor(frames).permute(0, 3, 1, 2) + time_frames = video_transform(time_frames) + time_frames = {"frames": time_frames.unsqueeze(0).permute(0, 2, 1, 3, 4)} + preds = time_detector(time_frames) + preds = torch.sigmoid(preds) + + # duration + # import ipdb; ipdb.set_trace() + time_condition = [ + -1 if preds[0][int(i / (1024 / 10 * duration) * 150)] < 0.5 else 1 + for i in range(int(1024 / 10 * duration)) + ] + time_condition = time_condition + [-1] * (1024 - len(time_condition)) + # w -> b c h w + time_condition = ( + torch.FloatTensor(time_condition) + .unsqueeze(0) + .unsqueeze(0) + .unsqueeze(0) + .repeat(1, 1, 256, 1) + .to("cuda") + ) + images = image_processor(images=frames, return_tensors="pt").to(device) + image_embeddings = image_encoder(**images).image_embeds + image_embeddings = torch.mean(image_embeddings, dim=0, keepdim=True).unsqueeze(0).unsqueeze(0) + neg_image_embeddings = torch.zeros_like(image_embeddings) + image_embeddings = torch.cat([neg_image_embeddings, image_embeddings], dim=1) + + sample = pipe( + prompt=prompt, + negative_prompt=negative_prompt, + ip_adapter_image_embeds=image_embeddings, + image=time_condition, + # audio_length_in_s=10, + controlnet_conditioning_scale=controlnet_scale, + num_inference_steps=steps, + height=height, + width=width, + output_type="pt", + generator=generator, + # guidance_scale=0, + ) + + audio_img = sample.images[0] + audio = denormalize_spectrogram(audio_img) + audio = vocoder.inference(audio, lengths=160000)[0] + + waveform = audio[: int(duration * 16000)] + # save + rand_file_prefix = ''.join(random.choice("0123456789") for _ in range(6)) + save_path = osp.join(folder_paths.output_directory, f"{rand_file_prefix}.wav") + sf.write(save_path, waveform, 16000) + + # output + waveform, sample_rate = torchaudio.load(save_path) + audio_output = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate} + + if video_dubbing: + output_video_path = os.path.join(folder_paths.output_directory, f"{rand_file_prefix}_audio.mp4") + video_clip = VideoFileClip(video_files) + audio_clip = AudioFileClip(save_path) + final_clip = video_clip.set_audio(audio_clip) + final_clip.write_videofile( + output_video_path, + codec="libx264", audio_codec="aac") + print(f"saving {output_video_path}") + video_clip.reader.close() + audio_clip.close() + final_clip.reader.close() + + return frames_list, audio_output, fps,v_width,v_height + + + def fc_main(self, pipe, vocoder, time_detector,image_encoder,video_files, prompt,negative_prompt, seeds,max_frame,controlnet_scale, steps,sample_width,sample_height, video_dubbing, ): + if video_files != "none": + video_files=osp.join(folder_paths.input_directory, video_files) + images,audio,fps,v_width,v_height=self.run_inference( pipe, vocoder, time_detector,controlnet_scale,seeds,image_encoder,video_files,max_frame,prompt,negative_prompt,steps,sample_width,sample_height,video_dubbing) + else: + raise "need video file" + frame_rate=float(fps) + gen = narry_list(images) + images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (v_height, v_width, 3))))) + torch.cuda.empty_cache() + + return (images,audio,frame_rate,) + + + +NODE_CLASS_MAPPINGS = { + "FoleyCrafter_LoadModel":FoleyCrafter_LoadModel, + "FoleyCrafter_Sampler": FoleyCrafter_Sampler +} +NODE_DISPLAY_NAME_MAPPINGS = { + "FoleyCrafter_LoadModel":"FoleyCrafter_LoadModel", + "FoleyCrafter_Sampler": "FoleyCrafter_Sampler", +} diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e0e1c2e --- /dev/null +++ b/__init__.py @@ -0,0 +1,4 @@ + +from .FoleyCrafter_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/assets/foleycrafter.png b/assets/foleycrafter.png new file mode 100644 index 0000000000000000000000000000000000000000..f2d4207c2221cd3c6f370c8903d7c66c585b03d2 GIT binary patch literal 539788 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

hyK^I|~WjpSe%);hD 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%0AK^>>ep*EKVPsYU1 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

&k@6fuX9{Y}1b-@xE_A=Zn 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%0DWyYg4#>zXO^aG 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�^kI^M9jcTjTc^FqxC(E*UT=}x_Q?*KeZ>xl#lZzY~Wz$~LmGU_DD zv8?Ra#%1laIHxjNWdNo?S-)pc&SrF44Nk0K`%7_JOl^V@+|VBDjlBWnFd2nb(*o8*d1t&%RUw$x5o8Df85l;KTsyM<^jYCY)l; z<3cLxf>SJI&#-y<4ZIq;0Uri0!J7Ed2y$!%Pmiwf5@1tRyE{stSO<7Jwu6^r8~Do_ zYL~X~bZoAysrJ+~r?TdA0J(|m$@Vh>y!*cmMysqpMl;6P<(ua>=&TfvLR<6E}Jla&P3rasWFZU^m#w$N^C z5AE?T&|Ra4c$X^u0pI6-FM6eH@XvVe*&yg%48_;ygYoql-}l8>J|>u-vwleU-C|?E z!p2(5hJT5TwU%D$6*ksue4oq1QFkdHhnM6cC88Y-BB4M~E79Oe6nHXJ4k@rAxNY62 ztl3emT;gHTdN_O!oP{4D=|zXg^~_z^pCK3}(2StAsvxk>-9*@x8mxJ^3vVASLEY<_ zbRr0^pXf=yBh-FjdB=|OlE2MEcFgPieQLS^=lEC>^H+8Ss`}#f(UI7DW+6siKZ8Un z=jiiK5l<%}>g;{QR0*&jBcA8dFFixk>Ssu)c>>>)_hGf~AuM=*CfbHvWVGIZ4=%$R)nGjRUW~hHY2PF^=pp(@T+Af{K7Wy zHF*qlt+J`q2x=+KWS*Kzo1oUW=HnIwc58xM>W=xmmJfZ0Vip3SeF>pn0BxUQe3`Zw zpDHTw>CGEFe()G8maas*R^9pe2r;U9wJI96HnIF%RM_e`!~8X!4;xXKRGT(3Pc14= z=0gcPQPyfZ9?Q;50&yA?0kt(@sghx50XWae5V_PUMbXM*3Gkw#rNI$JA)r>21@BQn zEr6ESkb4EVtPQ&d$^C|V1lYCTlTnvE z17G8(pe~Iym9m!5{u(zCU!r8)G#$ibUW+E26Q%=kgf$g4WQkVe33y{V3b(!+ftyCT zIBh&0dG))%_iv$SSl^9C+@PdQWw@JQHvKjy{nwH#b2=Or{LGfJ;RYcsBOeW%BVgCB z7=EiRz;)eigzS2N$b*j%bL=@m)qpUV+CFD(R)y5VV$XUVjszTkara>rF#C-t9Nx&Emz!!5eE~-gG1(&F>|F zV|Fa!WXX`Xd_Qrfcux8M^ZzJ;gRRp-P_?-Ow3mlMccvdc?I%=swnyFeHu$o=H9qcX zhp%UQqV9Hoe3fl_Z}o-l3T@eWiVs3sceXe5)r2+Gv;G3hS=!O6p{P4RsBdIn+t3YP z*LR0@3*o-3AHK{RiVHbIF*rIEj^=@|r$epb=Te!4p`~{-+>;^hzEtJ{Wh!NTCptEL zDXRL??sejMIoF-A?!ou$N~fVye@t_weUtU@1g(_~;mcZzsA$PdX4n~vj;ZtsI-h?H zd5f}EcE}QN6X>#Q+8{#rk;^39@mP%RKeT|WQ80Y1qTy>Aiy-qvC5SAbVeB_hCn1b5 zZp1pAAdWIgMO1@C#1hDoLNrA{E%Vg%;|OYr`cC@K4;LdkF4^Cn0nw5YKjivNuX1??h5Qi*nemjhL_OB*HH+l@wDGL!jxv?07(KoXxwM7Xa@JNVeO zhKFlA#Mz|cNoPW4{0Mv*OYd+3;h0T;<_w_1=5fIwXfr!1YWm5rcIfHRSP33dAWd-C zQ^olb+%vnh!n-AGIBWYtyO>_6><+WOC-j@Tqi!3$<8s3L0vq-v*3!uDuyNn*&C&~X z59zr+9|`SadX9I7ppH=1Rd#2Gp=V96R(G8Dl@roc{CwxxSkJK3(1|!l5Ay;&&kKb( zyCzRr7GXsINZx5{0f7vM3%FfUX>gJhk6MQGtXyJX-KHOc_nblK`RDLE`w-4mH{pEd z2JEYDz^R&w;o>!9zdnTzj}{WXgx6&%&0Bo`8-(yRLilQDdeChM<@Ug7K0ZQ+hn*Qa zIsiZN{I<3EHu2T6Ye%(SutMRG28XtdtiMw}iU@6sN%hc|0 zbEGme3xO>mr34xh&~~`S}Pp875W$Cy~*LS`~1MA~h&$0jB|=0zv}wo2r$- zGs9f4C|w#L8XrYn6WB6buH3`t8x*m@IH?4%d~I14_Lm}-_mF$!H532~oW&UsCqN!6 z?7z95qY>7}T_3_b!=A zknc`_$#2LaQQiV-iS#zr@LbdW2wZgrUYqYA;>dHPl)pgg@n>j$@+F!Q*2#ySBmUr1 zq#k>Q)T7VPXwPFbI`$Hct6sxp*BzKGu13URD({mo5VYhPY(`bUuFrmq8kCP;W_AbW zP_0f*g?3~T4#lORmv3Jr^S6i{ydCbtE8t#u5#DpJ!fOFRz33`D7hi(cip%idcmqBg zt|N5MJp}B$1D_pt5YF0Zzzj4LILrT7Kusr4fGj~$GTbdo&je7CvCGKH2i@8S;^sc~ z-HSb;E$fOe2fN|RE`oYZ2k5r4k8J6QPwTtl2aQ??Q*5Gev+ze0QRXR?ZXN9r@dJ>4l3??aAAq8CAB4CJbKneX-TDKYsCE zj4igg2>LD*Zl;akXAuiOb3?>;5Y@B{e}|YRA=ES(p#*ZcNeUuZqF7=?O&g~mfpsz; z%T|{XOxBbzPp=>MpC3f_KmQe#KQ<|mi0^6v^(z!ry`;K+fuhQ%D5jD&l&4hCk5O`3 z(uY1m5kXx_HC=N0KJr;|2=Bs^kFaw|HC_zfh>rtTVnNJsc-g1JgW&Zdq(x0jLK8PT z{?i>=z}=x4&$WcFt8COEIlx;ekNw;w&1f6=IT7Xrx~nQ3GyR(ZSz}N)ZUl5i1SXZW zo`9BlYh?Fg>4rbY^+sk`6dc9#BN!ajE#P1$*~d~by>m1CzK~ws!miLRCxBP*IZN3A zrL1QIV%j(0;8yZ_EkR9S*InpBX!G%XzV8h>1?T%iD?mTaj&O)x>j^ge@)6J< zW6g6oMQ^g4pX&raR|QL@c&${prwQzf**LW-3$Y>1X^^Qb?Nedz+(Zd3)3~I;O0s6T zP-VI%vjG#M|V8UxlNTvzO7UayP0s7D2a?PQqSxl2iOX z;x(UReVWe8iPpejs)7Biw>P08Xb!BSLRi8wlO4Z^C9@UY4{w7r;|F2%+Czvw@fZ=+ zuMu|o1p)mW;gwGiT>cn=6%P?D@ILhru~ia~^B9rm9>eS8UHG58gRrGXp$hJ*%s)F* zNjqtpvs0%i>#1dv4qHOpPB!h3>}@=jrCVwhVQ9;mp0J9FPUdT^O+3+=ovqTTH~!(+ zAMc%e;B(Sw=-U#)O~*l(Ovk5520(zL2}>J^49}#h9tV9()-8EWg3Lr|cam&Zla;w^ zeH+50OCjD5Ex|7<*5Un)TKwbZXSjajDweF=fKDCy!N)Zmw&ub7d;}R)w1%*@BdBc% zIy;%mCCEix+lUG!?9_@HH2_=`tiaL`DQ)j@#dj##N&% z95NY=%)hmU*T48sNxLgi@G>nRn@K3qzXEC&QCG_FuQ)7(nw1O{%kM+y%7Tj2EVM1W zX6%RW=3DUEau4xT%t^ERMLcI1YsRf@dEC9ZlK}v zN;vGjfux#Oh&%QK?$fJbH|zlHMjS!xz{S`-sy}r3v=^DJalJ!RboFlq@5Dm*_1*~2 zQAglaTnq2XSKvM83OohY3$MbRp!QpL1Abd?!EX!8_FM4SbPN95?!i501HN-ghN%RJ z@&6`!FH4p4zQO^cdEwwV_ ztvG5jht0?G``8oImVW3`FBUf)Cg6qlLJT%)561?9@V1DApIIFIOk&}0N;OSL8-mH= zsi-BOOqORQBa&)5l64f-v?yxXK}(XJ2&kJ7(oI=6HU2LWMD{=bm7IOGMgq!;&l-lk zOQ@)esiKRirc2IIWmi3=dKNYPL#<6iJkY>ZmqS99OKkbY8Y&4EEq_*lrIH+Z=v@L?{n_@}# z*7(Q#ZqUx|h_AD{LcfGw-U_}3;f7^=KD|i&Dgt^#f9SXJJr4K7*MnW~^;8dnn&+?3 z13g0pf0&K`0Pi_A1o|UX%ZI3%j}C+WARFcp-gA_X%L$ajeC=a&B+A%WPx9PJc8=4$ zhrq_gEL5$|MO;V=HcUe@6gyeF>zo3$V=~n2z-nhgOj3{%5GFPuaO;(cgcDZ~cJV%3 zDsRB1>LRR8o`LA{rN-rbannSfrK z#15E3WsnU0*cSNEtu5}xwnh~d>8$2c(5crpMCP7Hz&y!FMmQgPg^;qB2sr*2er1p0 zTlNr*2ZYTCro7%EpKvDJn@de#w4Fv^&*W=N!N(3o_3Z2zUuw~{ijz1bTvLKT#XNXC*x~!Asr%uHdZ3J3Fv4!p2(UGP|8&3VhQY6IzdtV zeW|jwQG|2!MAS8=^C&Qnr?Mv4-@6sy$qyrOz1|pHG|9m>qmk(K_jI`aix zX|wiF9iQd7D#G$4?OgdVo*RO%OM5|2IO|u9fOh@>=oizDF6VtqhtWO~-qR>frt!LL zD_l&wIFZ+~_&8%A-eiqNQ4;&MWNkF1lVnaO+$srkVmsig%tyEBPRF=6^pbh3BVjM| z(j91DJN83m6a|_QoiQta+C7=lFszO+r;=9YsAc$D#bdQWLH{*RtvE1tY!fyt1igiQ z7+ikv#tKzGyzyCvJx=)u{(X^&+tN8=#A*rscR$=BUrgQ22A6dx|2- z^-WM0oqCL-iU%kzzfVw8QBz&#DnVs;G2z%_?3;ZS{}`|azYki8d6C26FLTcXwUEd<7EV@8u{we(a4bEyaa504 zY>au`sH6#`q8>_e6n|41B?(eeH-V#uj(}AY%x&Eqf6VEQx>+6Zc}8b8MtYnJ2wN%3 z*tk}*p)O^ETEhmui69kN|42`AA78(sAL^G;EfifPLw9cn+P5 z@bq!WOYe%`X7ab$%Fev4J+O~SFy{YXjxG3G z2)vYIekW0&DhB~v7#g^S!eQie_^rAMxApfCK_EvH$bJN3=;C-Hq@b z%do3!7S6OCg!a)x;h9(pP1_C73_cEzq8j*2zXG=@7vMSj5_}f2|E#(VkEPe(wfY7; z*WHB6n(I{6cM!a=3T9D5VZ`=e;SdK?{?BBomF%!5bMDIgG=XGh+ZgrCJkY&eDDEHY zp^T8#9i?!fc+l>k4O`y<`t{PcX$Q9wh+Ao2Hq&lxq#gT_c5F+3XgBnL?jY^e3EHdu z1E``0qHYOonoRwzqD@*!2X`^A&EvHNv`dQ#?rF4p)A>5H2<;-ib_uVE7|N%;%cA|F z?R}k*fx*GepfP28HIIjtWitG1Qg9%iLZE$D_A7$AofO*HwzNTn_J`&JFxR=t5;uTaS*j%}n!kqx(^)x63ROR}trw13*HXL)SdGyp zUEyTp2M>z~_?X2bfME8cg7!0tQ|73p$k2C)k!;LCMQxN!P^Tf%h^m@Ujy08lGOB5_ zR3(k5?4Fg{Anrdm6WRajudwRHV^P#alI-N%YZO!w(BFo>i>Rmz1kjbTH0v=+Xsk=A zt|a@u5cH%Y3ZbmijL!kTFw}XpKbF_14fmaemn9&z?W7!z8*;os^;~(RC zp|4L0tSn+-FHo?f_h#7&1+gvhkEvZzH>DGmba!ay(VL{Ah79>EGz3gw1sk*M6tj{I zbTu32IyTJBtl79x!3KM*FSL8u!1oda0_Z(#ynExvPlCN9La|Sx%1)xOj)O|JWz-}n zLw)AX;cy(DM`e2n&QPWV{>N1cf1O5g9xJjl)Y2l)M;z?KZ)=(@yw|E=Saydr05&6CyS~h25lT zxG%niz-`arvE?zmc053%qmKz{!us?hMDqXNn4pd!pkpf@BJ|ip_#C2<+;bNmLl)7X zHdU5sIoZjUm`xQ`ZL1_e5rr*Fwg9f24jmo-xF0>M0mM#HjgtsVc!dr&Db?GB`j;gx}f!(^8;3hoU zQ1P{u1RPX^t!4o2rqiHEX0u#;9y%ExXRgDC6E%4M^cjBn8Kr z*4BhPk5#-TMWAfw5Kj<`A{Nyx_tFVape;&TfGt2K;N`jkY5s2V{<5@;uW4mTMQ%!C zM>t!XHHHh@P-;>SCq#02Of2xWp=->4Y+aQ?PQ)A1#s2tS*T z!kzC2;by~esQ!K&)_m6o-ToyWPXFow(+1v0C5X&KmZI^uwV*?! zY~v`fCa5hPC4ej(4V~yL^~guys%khbz6{T`cad=DC1MEWKq~4$wpITh?;-5qBSaiw z+5ZTE>!@_s-GS@+%TTYXg7vOy1lHU~{H-@gy!i?thwi|4K`nyjU4{Sj8hGZNg8P^g z@aw$=<69PDG9AR=*b>+@Sq!`0``|S0EW9RMg7>tmaG!P&9&;{JO;bfLzYh16e6O{) z;IYC`hpxT|^VG4ZZx;_!wh2rqB zz0bQlqHYJlx1%d`>)JrOPWm;~>L!AEJk5_K~=r-T`P$yV{bUryWaMIvE`X;#f!rcr^6l_cbK!k;FZg zigIQjQg+BP#;buV@q52zSR6Tqux$=^XTq22*pndjc5I6PR{?e_!kfqL9pLNQ7Ou9< zAi2Q3sIEm(EBU|eBnNm)_}aF_%1A2c3>k{)1>_UdCH=5Gp%wf@p;-}le7>tqI}Gq` zj@N8xI)eJ^G%D&TJ=q}H=w^3?UUtx0!N$0V0A0>Ty_yhSDv;*uuA?`)o{jQHHd-oU z?M}jdHz9n8P}s``yORz00FD1n0-2pazlYcM5e~c92@cAA21Tu?Y^e_s%7m<)t@V-;SP*}3>>n0Tp)i+gx*XFx$rqK+fnsDtnli#HY9MvshZ{3{# z8!BoWNj8(hLP*;rLW0R;szPl|7+DcQD#A=f2+>2fB4kwN1dh2s9E<`m%Crei+4RRd z+rjwRwjX}??1xYB{9iPe{NM%95!l*9nYSKCl|2gDR2rftqm+noq-6434zw)^>{cu- z1llqjK9z=rFltWVG@k@*^Agl`pM;ND^YCHoVf=C72HwAZi{F0#1CL+4MpgAytk}2@ zL&i=vBj}6}w){T!bhIR$tVY1CiY3ewppo_T*0P3MlC22L zfN1?Vr&7@)NWdZ{g$WV|YJu89sEJh0jf=p^k9X z$InuRu=VkxrU~3={!UcKpt?p3ft|$Piy%idl{MA2E@C2dVTA-V@0I!JXxsg-m&Q>4-4mupRMAw;%Gz)X{*SFBc#Hh6+?eNNXYq=}0uR4~1>V40zA4g!B9wSeKrL+q_F?wEiA^7t)cPau%*LYv4Ss z1}-yd;I*h09!svlY5q+(ExHNo#Z=PU&%sY}1>AX!rgvW>s^$Uw)?Gy76*mz)Pezrf zjwjZ@yWkw1&I5>VGam^}XTiDkT4?(0gIo4_xE0jGd*Wqy&A0+@!rgQ3Wq2>V0$)Pf zXDwCr3M%O}w-LGiCahX!<9j+%rc}~q{9h_+S|vHW`G2#Eri!LRVjF>mR{m%j>w~MC z+tZ;Zq!0JRmpxsf-A-t4CHS_`9&P9b-Bv<*I}25`ehs0xk+zF=OTURWW&>^0T0Y)F zTeD>tZ5tuIf?{IDU}$IZ`3nX?JB#NQ(QeJ59hy3Xh4;^*jhablPvY~ZQKTqIr0Dc! zP+*Lq-OJ+Zjik*RJ`C6TjYMm=RM?oZotnofj^S{RIJ|Gc&)A$os1@yVhu(PKY9RWt zKbsr-KqV-OuC1+Np<;(q6KE>gc2okOWUkdp zfXz;99zdhukCsL;*k#onFYSinXQx5<)vY`J4Ce2gD%s5PsG`Rc&f}p=H1EHVWCwQ(tavmS@Xe;bOs^~T_qNgj?Gk3`9L9nk1sX>jXNNz-VXn~RbTW(lOx z4pnM%Sz9YfF2wP%4OgO{W$Q`{88vW9AgH5YB5Q3^`@whmF*wY<0M(?^a4fBa>%?k! zOu7JaYgnM|8UK(bn>;LYrpz?Wf;hC2xJ|n?oFBB!6f9i=60WRr8 z1=PwMHDN8o-}z5OV%vH-~9R#=Ryk*};87_BIx4^u3 zLOP=}w3$6n*tiXBjm6NngR@;%1gM%|YnM*YPalFWrG3~adqX>opq|l-$87v_dOz?ZWDuV7ilvX;=MH(Ixfs(L*eD-q;#gltQP5O3SZ1ykZOBnLZoN=}X|=VH=!# z?1RVX3V3FpK`@O={OLD{t@;V6=YL1@i@zcI$P+j$z67@=cWChLzk))E9TZVlrUCSf!!X05C9c`^!CU*j_}RG^{_yOH zuiiDV!@pC*s=WpDoAeg^?{S9CLc!RG`AL2{xC4AbpA0LaC;Y+{i_#&eQ=~Tr@1a2IG95b1KmhU^2 zikV=JDTY3R4veVhXqFfm#^(KTyoaFHHx`vmXC{JmV>&nj=or2xAOGQzg=eNiasOY3 z;8wkHxL}csjr9kk&)-tv|8G=OG_*20BAMvS%|l?uVoG3JS`yBZ4wZ&pNrIuwRZ0Y~ zVQ5%#Un|S7Y#YHeyaQG2UbxJwA*9d1d3rUxrcyx{pMmF;3vi!Al{@1y+@@ZFJ3;F? z>l$39-+&WAZ9DTCtQTE{ciC-(-+n|;KS#{X=ZH9W4}qI6q7fBvFt3L$x{RQ?weXv9 z3Eop`31yZE7vVR74rwu!_GGsGsaFh2dKPOss_yf6ZLuuNx&rsbRPThnA01guzs2~T z{Z{6sP5C_(B`vU~x;E!IOTydSR#JyXB0Mw{r&qTHs;HO`bVA*pE_D0}kTBnc?{=TOt~Crh;oDh+F?3uPU( zM1EI2M*(47`2Sa^tSbrXllNHeqWBcyOhuhVMLqrmRrS%kn6d2+&WzcOFFjY`a_hZbKZ-4ud(CIKmP zGxRLAvk2+=yiRb-d^J|EaSE`Pi&x4^Gu=)w?_i^rIUz-D(}Uf^j{*mvqlV0s1oY{vCq(HoOUH@443q z>?`nHd;_jCczsqa9H$Y|b1%VRRt+3yUx0sU8PtBglr_~(n$}d1%?RcuN;+2=3YWFl zwl;J|EM@3h5{$5x`C2))Gz{;mRZ&!vGQ``M-gyX|O~TOMJQ>H;UGUOz0AW1trf$xt_-Rn-JCs0(QbGNeV%g zLKT@tsHIHj<4IKPgjM@lsOvQ!pT@1m$Jx8_aq|g$I9h{`SMKA>t2g*eaR2$oZ}|C_ zw|M>fC7wNdg8TRH;_B6_IDPsY4(>mWo!j?g)4E+)y?h&%Em)6*v)5qZjMZ2;eKnTN zU5C|+H)G4%UD!hqmmNBZ>NC~2b>k);KYW1KFQ4PrpWouc@4w*--{;G3KjZVOSNM4A z9zIrGz^5HY@L@I`ijj-i_hv)aT2gxwt^(ua=`6DhsyKn)OW-TYIaVM|_{L3yUgn|+ zW(Cev+=vi(^E`{dS%$q4#e1o;5mAKqKG}F-ITTOo4aHNF@woFt4o#$pR z2?0m%A?o@wgxz?G#+xZ7N| zvT>p+1hUF^sQ2go33H?OlJ`yh!F!MBomY6&V+*3Hvl z6F|$nv~3tncwW+mN^qUAeIT5S-LZjie&@dmWp2|E`9mC>sipYhUc*TW40|ZJ$l} zrSBrFh#Y`myC!gTAdnphW@XO0JymrVMPbVjxF-#HfO7}mt4xB;E`=T{J#$SsG*nlP8_Hq1r4q*L zj>Blx5ty$$47-^-;W}gnmW&t)Eam6TY=cpuE#VfK3)kdnaBaI9&Yd>HWxzhTk12;& zaShxiR@2y>N6dkTNUwQ~L^^9pXWyVXq22o88^j-f2)D(x@LF&KJ`3)^YxWIz&%O@d zc~{}X;x_#Tg5f zDodGnl^`pFs+PbgC9S6fVp+-3qHw6Guq|ai){+X_v=QPOMq!e9bJRNY$8-B3_}QT! zOBa0dr$G|cEOXaX%%YmLiCKh|C~d+ynXqm`_1!d20XO70p30l|=u`Nd7=k>Jz)C5F zE`e}LmLYMLcmgV!@RA@a*?gkuOnhxMgNl0&z6@E8&pGSxVcHhFU$Gy5?yba!$}9ME z8X*~?NaLd`x5Ke$G~Y0w z>NfmV)gX9jErOO@M!>ua2$@}rP?lg8->DbjOI7VZjp}(aUt5&=|ILGa4!a2>RapSc-~ZK9}_W2lIm)r&d~4gPG?^V)#)yRdMClWNmi%uF+~GpK6-T@iVIQJe7uT9Mgx}+ z)T?M)7SPtrqMcbn;Lf9+nnn9Gg^wjrY%=YE3~$TQs|f;a+PC5%sFO^Oxr8%qr#_c9 zi-6W;@bNg_lflQs`TRjrC{ze*t!!V}dn_)r8j47&Uu*MFSg~JvnKim8<`i(6Z`+qZ(ZO9y4ym827OwH1{ukK4e5pcWwe zyLN&*!Ruz*jH;T-+ey-nwpE76<@qjlsR;8*p_kT@UUx4(w-0tTBOr&<(3TeBOVKEn z-uODPFFv!>%^r@g(^!{wr^=?fo;TNSMy1~y_sTDz7VXEV=jqstMmd~r(B@Xxrl)E*U_}*C7PUng(g+6(CqAMq@8(%mbGt? zT=p0qRMtLJ)_!wtvD}33oa^wKL8U?C18s%UGOnSQ=PonRJF$B!{6 z>GXU8osdo)4_*9N!i}msHVe8q-Yd#FRUkcq&mo-BMU~1hF>495lJ&)s%p+~KqE7WG zbNKvOe2=-%HlL3#9hTr@w^jJiXAM3K+lW8MZ^MV8o%lTO7(OjNfln(d@Oec!J}y0u z56h0>M&n4@fb zp_P2wsj~=X3HBmj(`G8e*Lpf-@;UYK1g}6@6tVz1Z94R^{0;(XNn{eunoyQVZf)a2 zs%Jh|qPN2c>2UrAax5x5aw@)t6ydGoNCnUj>JP&+^ISYIE=09uHl`SLM~m+hVE=Dk zP{~}hsUNHeU^BB|WhBJh{M#_MY;nk1qP}I0T5_Z-+ZW1kr?o7{krbg)8pFuB5$w9= z(*dl2hAP^=hzh!>5_Y*K;Z#_u%t?DqsD=-rD#O+8r5E8sNV`v9JDhk44pYy=|L84* z)ZT}0^?ih0dq!Xr(lrm^wc`>(ml5JishsE6AdreUbY3k&C@_L&T_m8XrrEZYxoQI2 zm+jkQ>IFKem*6prke);5lnUHq{w27w_%FQ-n;x^Mq*JM+BVj_tY+@sTj;4~1p{+`w zVoW5AVqipP+snllTS`(;cS?r4C9=C6bbGr32MOT)Jt+h_(ynx(eUU&iLU;qq8mi|N zy{MjfT@oG9Mqx4UUqPpTA?*-_hXjma(QsfE?a>_CpXszQlWCWx6VwE>ZZd^O0d0?D zbUy-nf?>jY)^Th>qqnI*%w zVJw3AI9Rfhnaai-DF`K`Wn+#=Qx+b}T51{kj{C2&OV99zT9o&cBoU06G}hO=EdJ9jF~ z2ze8`4(J=u6CZmP;m_ES_%e{7E*XW76Z)WTQh$6Y?MEd|4|6&j@C>Th>9Sssjc_*A z^h`pV4O=^(4Sumen;>4w`@}U7Z(USTV;<3^bT}4P!ZDY%5=+>|SOW=kCpsnL` zx3FV~$`%#AjrVMmV}gD&p)G-EKk_~H&}-e%gPoD@O@*zKWnN_?fRiJkKTfEZk3n5o z36_l=1uy4ju(59fdxF}=E>VdDSFu(*CP8H{OSgDVVC_WZ(P}W%GdIC}RRxR}ABXAu z128S!4(HCr*xG$8Hg(E?U)&_vw^$BM$F*?iw->Hs02o2%zRKX9cM9H^?DS0nn^12nrpP}5;+a^@MDo_>GcVA*`ZbbHJSD7Yyyjg;(8AjasNHGEWZ2P1 zQ8O3eOm*%kpr+Dsm{bkh$>-rX=^|YFuBSq31v|;VF7wrdvq>NX8ecnw$MT*{cwZbp zM;h;w`DZ0En_y1iaUwsfEU!wH&&kjKEkTJsk`72@34a6LE6cGOiLxf3m7#7PQ!#`0 zNU{?|PC{K^0e-U|i&thN@W^;H9#cU-vMj_E>q2ZX8IC?bq{8`oNnqm1|A_>QQAHEX zW;Cjzo~{iIOJmCu5is&HDI){M6cMw|p z7(S=(!JnWGz3>>(7at?|_%(Pfzku)+SJ7zsRkme9TgqGldTKQSrW4#V311e^i8b(~ z)9F3qB0Q&EfF~jDIgd{1{3~#sONU^=Wdtv|3ER$7QBM;EBUy6gkN|VpY(rpeC0WSg zU~Xp!Dr-P+J3IPdb51h89wnHMbf!J%4DIet&CIHq0%;KlqM~JZ zd$~;GQH6_&UQ9bg`=y^tkWQllpF?{!ht59%trtK~qz#)g0(FJ7RTBx{iL^I`v~@YO zVT7`dH412datZL!w2`A}6SH_dlkgr%dp3slOm@r~#OLg$|o_ zsInx>+&+}fj5oR%#o)HfM7;4^i7_TU;qrYjJm{o*ni&GgWc{?KQLGaA?JJ2+OcIn$ zIsACepCw3geVe8#DMSU*vL|%}m39=*M>kAB%zxzsC;zKIs%l43)rBmQPSh|AE<@i> zkxNMDmOnt=NdvHDjyk{W778VZ?8F_y`X)hr1DVHeB7@3$@{SugGjAe<@J1<0k z@2+suq$?`f*Fom0sitk)!c)^m*^oo#s+9mTw~mB%8+bXjf{$x^csR6XX$CLX)^H{0 z9i3XhLgqOMw4EtzNHnZpg0k@`bR0hR9*a+GjGs#S7=p?svr*5YrzjbBW)G%E$;Ld3 z27GEyXlL^pm9=gW8{}eMo6knLl8uxKTf2Y_e<>U0a=!kW;m|ENC~f_^5zrFey7g?x z>*%4bVk6!>Qt3!KQQ=$od@61IdY<3FbFwyjJL_!(^frm)Jszz_2Mj_`F3@i~%) z^dw<_VkkbH$i|9{!SHi#0b9G~gtQ^R%+@|dNr+-0fmZg01Sw+RtO#q z&%koYX*8UE7^WjvAu=rwsV(QhwfzRT_WTjfgZIHD^90=TE8$jL1y3pj4;mkL8XxyL zR0a!c;l1WE(#}3blXEYTaQX?M{TxjM)-26xsIV)Z!++%!_|B6dz#B?njn~Y}aGP9B zgGNO|NXt;VGYy<84Vh ze2#T3JWDRaIai#{i)eJ>CW6mDgy)I7@T7eTsD6Ne^N$c-NocPnh!pN+yHWJp$2=8Tt^x|HGH0_EAiiL#voIV6JVNDgSn@Za8Ruam*6m2iX~gU%Fwm|+DzarbJI351ZBS3m&)2O>`f(YmZI#Qr3`(W$c7wL)&B#iV|Y%2$)f7V#ec)zKj&ZGspq>T z?{~gHn#wwls#=D<3r;_x!hS?pKSbV%`zR>CuPABRpd(L)zmHx=4nduL@;1gDyN9k_WuC zl3vtJ6gDC4V&4K@ZXMvl^OAwxk2*kLT{*dV`5?n{MD z6)O#R3IREjz@APIbPgNjOg6;XY`AmT_!bbFbNE<_H2Q@E=ThD)%Nte_peyN}O3>L7 zs&f{sV?!pe^=k=bQQ%bA$}rSMb^-zR8ae=5`JAl;{#te*QPx|iNJ4bhMnD-irUV#2|d*WB}s~; zh?F1BBwLyy2Uy8@mBriTnkPdp1Z8X->_ zCGYu{;7)_%xs*y_aV;9}lp*jJNUnNLK+9$vFVLd;rJ|}kU4Dyp=U&5)io{zYUZ-Dy z`!qg(Iw3uUuS-z7OdzZYZif;Xt~?E!qDoj#Is?a&(^U1d>1d=IHtnFgwpBNwCvJ!U zx2CFATgUUUlsF#865fVLa4QSRU2Vu`Z6&otgsZ5mWf>O%Zf-{7ZsG&a2JYxmKNQQ& zTjG+sKOWl*!YkJA2<`W715g(=j_}F_niUd!g#;R*97TXf6526T(XlxMGGR_QBZbN{ zj^JjER6;v}Z7fX`HeuF;U`rz4WSwymfh|Mg5`ZT2*fPwVLZAvf30MWrqP7!RA_#GT zbP^#gB}P=R3`Yxi=h9&yJmYzeN*S?y4HkVOVVp=%3&0ha3#>(1^BiHVBqEV4VG=MV zfnW--W$0dBdouJA1V&|y7=C7%e-;=^fS4?^QqHr;TIld%Xd4ybA710|+HMG*nheDK zdc$znBm-AW^Ksg|0PD<#V`%+$i1@BCZ0mW#yrDZR&8ebHC90bMw)i%j%_1sVfNUXw zLP`lyl=OdWx*)^dVM<~j*#S&)nTydj4Q&P2tc`G_lC~{84d|@cekdZG<92(NWc5n#6vhk_-laSUO>V&$T zZK2ymKp!B0x3?plThWp4MaQ1t-b^@4_Oa#dc}yE~g8#=Av_10(?%4$JJUafqj{D4?e8%j7-d3F{2npG?}V z;e@}a=;4HRf7-I%{7eJ+83zu>m-I}$aTtXs=3{Y(<-W-%T(BO6NMjj22!@q?BibW6 z&l2UWqLLOhElaZqYMG}N6>ZoGR+eN*dtkc|buDYBX~!%aBz>$8dYL8RuIE&|^k0mT zW*uPLAQWyE;qb9aAfTgJ2y2sA_?X7S+n6D=Qt z3F;fjJ!VkU;|c1lLpLyG%T1gfw;Lb(tj5c}E0N*fn+m!W!Q6q6ZVx}Fj_{?j_ONTC zz}wH6YTB`#qM}`?xI7%2Q-QaHhckAYsu6@l`YE}uws+~ zZT);6FJa@Q>XxB#E!8&Gj)HzM0lSI~e=W;Wb{0v`w?UM(T<2$!w4|HZIo9=tZf#Gc zXsNDsJNUd^e2@M7Tt}$R4-xpsGJxY$d?!XjTRs{%GZwlt1=vzD3?W|4p_1%g_GvU^ z@k+6CkxXU@JWilGp%SGLkN^zRh_0qW;Sjg?1t}$%h(fe$)SNGL_KMI zL`l04)E+cU?lY)_=Us%?0;+`tweVVciRB{Vk3JxzUm@}2GqgJY3ats})-^w|euIuR zgmu*`1g(`oGEp1^_6#Zvg5P~I;Xk!nQ6dgBs7_Qr_B6J(6DnawLu*%j3Ys>PX`Iuj ztkbBh32Lfp8#N1^FKdF@%2I~FsXnMctSlqH%~w-#DaQmgm9(Vnl-;2O)CRRpWoJ$( zmg;^Qef|WIeMe>m-4>)O;*;G9M>>1G4}>hP5)>%_1sXvVP5ol@9wZLa)+6T7k_UWVcCvS;4HtrfZ8I&5Y5eV zqNpXRTY*Q0)aJNQMdKQ)w)Th{{ z&cJE>2{`BQ9D(hWdm3)V)$k~)rqVtG|JBzJK`4h*J%raGLi)HtO?yyK`0||pp_}lY zd!Fj~3f1%t1W`$cExe3SI-GvfY7`|MBr6%%M_k!Iyjc54K;$&O2Vw0wy9RDE*|udx z!|ZC9G|56eetsh=XH)h^69U^*6AdG_NmE%TZNta>z75sk6c@hOkeiAx#|Yp9hM{iV zZbEurH((FTb{_9$y{{**jkU~8ujvFm)wTZ43Vi+Z0&T)PLYmK?L6FK0S5hRS*i_n_ zX|zvN(UPzP#XKkb^;1D3pS28iXVE6*4O5_8H=6b@k2Xw{bS{O?IG!6uK{1;5kK{E8 z9vi~f8ajfY=K22oEd7V#LrMl-n2*G>dKq|5$L)bh9-dkh;JQUF;uSPkYTCDEy+9Zh%MWP|$ z$v?ZA{_9UvwK7l5BA^yUom>7$iTp00suo3^U;Y5OC+{HdBw>C0779=Fi$4dgL|N-8h;wQUH^Ml;wJSkQWv!$ZZEx7Pg9`hvd289b zT6WcPwQH)(Pm8*CwMmBy0q)}56izP9(TU3Ymxye@m#WCK2=JZ+o!=Dv>7I>`$)o5w zWZq)ynh}WFM+LJ z%yR@dmJ!?wd7liK%TW0uf_e!-FZpy=^L|m+gta6ika=v#JoY+X+s4<}MDJAQtq%~= zRM*;L`IuzkPEupq)PB;^8&S|i5AkggMXzXIB zL}b1?fsH=`7Md^^hqr@O&X2H|RSk9V8EEpV;7k?mI*CSxD%YKG_LxaSG(&1an#RX_ zel0wx4m=2KZ;9?+NQJTbGU87@M^eReq*uH`t8+i0W#wx$Dd*!;FVXreVSV-;8m+rV z;1kYN)gCfmJ&CU~@f_9_9Yn4|400AcQszn%;ambuY2%K4~6ko#HW71i8PB9F5`%q!K@>u$e3xVxE_bPm* zl*2q_EdH*Jgt2`Tm2^Ce*%wXNCXGZ*vyIBox0zie>Z`)w;OvE!`APV4lmI^1m4GIs zcM`&TI|GM!?f~H|$9q_|cA}l2-B?3#FKG%~q_g7Wfo$5CvAjp>F|>j{`+)Eld4rWNaJCytF83wgZ{Nmf>&0{jV)9EgT4Hs_Eh8sd(i# z9nV6SU@-fwgHa${ts2AKEE1lkvG6vhiZ+vIZ<(Wxq@s>dQi%FfK?j+pAdo;7NCz4v z8lt{sX%>sDp^i{Mo%GMIrvLhrxoQQ{in4x+oRfrf$)g@M;u?4S|(EZ4)Z$7KF7yReQR$f-_<5V3UT8{)AvyKI*8dweDGf zKaH*5M11tj!si5fj+x`3FQq~*V1t&mgHwjH5U3OTK|6)&R(6e_^z+(aQ z9Nxc(&!^|A6Yq5qJHujjf+eh_$YW985^*W>*OE_HSth~PQ$XE^(58}K%?7`LpK&wQ z^~UbNu71$%;q~2A@&|{1D|8YLvhy4p18o_dh6;ANN&>q)6DOvPL(_kuGH{{db)7-D z&Z&VHf$k>9R0+QGFTrQQMffhHdRRbpvE>Gm`2SBi@eC_RM9Ral?3%EI24?Ob@mb19E^D6r;6WkWx zmWUXxQK^}QQ}Kntl$~5v-xq=31)x*C2uwF>jzgBcaa}b8Pi%(bne|}2vL>|c2jF+_ zq4*S$0bLU+XMQ$GFZ%C8+*Hzt&LiLoSyOFOY3rkCM5Bs%%xf`(c05(83^}JxgD!yz zR))YODx0cU0ILjJCla=?0$@H)ooASLRsc*83!FtA%RF=v>u3U40W}r7iLX?_ z+9)~{G9S*zlFC!Stn6+jL)fz3IgC)2^D^fw02g>GYP$s5kbHb_8;f_EQFv}V0uSns zB%m|##JrHdMG@}U{r2`dn;B>;9K=zx;{=nDuol*_|84^GyQ$y$A&f+rV zEYB!v{d#H(7)S+ufH9_=ijUF z_48rEoC-L%gR;XFrV_%1gnSWgi-?dy+NK=ZsT=_{Z4<$)8$TGj4BDkaQP6xn8QLC4 zdp4ZHVkCvipuW%%)Vjg^3?pg72GTC|qTL$6*Ozm>#^SwS7G9c+!?XH?b%RX2G8&H; zrrCIFQ-bZ5L*e^f2*1CdGQ6!QXaf1KA#GEt=)Y98lI%nQxFO-#w+J}0FS}br!<|an%_s&QRL`Eu@+{Ue1Wr)@15J|< zz&eOv4mFdZZ>s7B@rF5SqXa|};!(y)|2@?7Uw?V0p6)J`rCL?bkSlAa3F=(Jxj=@# zWto=v?*T}RHbn^e>{sjjK0k6y+2W7jbD&^2Tnyo&|PYjAz&W_%vH5f|G| zM>7{0)^1Hm6V&#yj#^TTvPe=Cs%wb?7e(!1-wJM2)b93D(hcfbw$OBL2`5ccXq=iO z(XJI9N~R>ALTJ6lL+g=2SQkMTI0>J@IY5goV=*0v&RW>BAuBhu}e7yy1 z>?``<>#DA(Tg6UFHLcs)3p#=HZg!CEJl<`H2AA}s`XeKiVQ}qHe#R5ypg+#{Iyo8; zbzL?Vwadq%T}(R#Vj71OMQK{w64*2pR+?y7vRK&?XtJ4sC~Lo#a2&lA_A{?RJ@X1Q zQ!m1i%D{1YHJqnWiBKJQ&Lp^}o?}g;L{R(8qnf6&_MyV|TYLqc1a{=^yJ&Lq1riSt z*hgNXWyNc>I7ww)_6jM-enQgWS7=-L7KuOJh0BCf@F=N**9024qH}P{FNaI4ey*Qk5Zafwjz6E5T)wT2%I|w&wSw za<-H?X;IKLin8ajJhtGmg+-VGb5YmkG7QdhqN**-=%t$k!d%vMHuQr>eSf6ZZ-flv zCRk(EnJRk#9uiv5Y)9aQxd(|Mh>5(p-9)AI>f0r?EbUe1crmbvO=0$ql= zsj~HvRMCwm@-_IpI6}KI@0a;%K2Im=TAn9!+u;PXC}tT#4<)1taYOEHDU+ZJE5fJ1 zT)g+p#5?t1!g(NGnhv8H8jF`^xp=9Xfcxr6C|BiTx%Ci?HfoFH`tfk7Povww3nnHk zroKu(ZY4;JFcuXp3R#A@Ek!9)F)Kse)+`FNWtf}i`B+g;gtTn&DN0&`#*}S8WlmZm zya{Pj=O{RJEhT(Uz=mzchV4T$_6Y1VkHeA2PT415H|`jm2xf`;7RBq3U&%IHiR7bq z(emtLgdMmIw;yj3(0AdvpTIuwfHgtQ+H3DUcr2)<5+;<{AA)9Ff$yXm_$d+NZ0{4# z!<(<^HR&9@C!a^avCXP&S`(D@jXjmQXe%2@Y!U!V8UB_fSTYxFYf#q8 zu(wL)spTF4Hrs^2n(ai%PUa8-wS_lIEn480@OiiwHW%&cw}hsC7~Cvl;b|EU52|PZ zwHE>HMOE!VK+6!gM0`uWZ&B5fN>tWRhnOcDHsvr*KqyNiDrqI*$$y8r>Hqmpi2y(S z3m!8r;VZ~GaupfJZeZ+@8z|m&6Wb;n z!%zKJoS7{HqmJeTb_=-MHG_vkbGQ-I zp3be{VsF^**xk7u+??7_VKzgl_WjO(s=a-AmvP`GnIG}{e zx|m*MQ9r6|Hcpwh7ElYEWxhv-!>7t)b^;k5m!(}Sx|zIhK0!Q}=jX_{E_n!;h^YB16=U#-*!prbl za2Y;}E+Jv>BSi0c%tB>-^fi(R?BruF5Od%e3*lY<7Tv3VLz_cS5HOd{K>jIs6`zMk z(HSMW+;zfPWq8u5xE!{Gwk4r$nROf%S!Hx44#FmCG;C~}z)rH3*`yQJDOAr%gftxy zc2Xs{%qD>_pd+G+hP6b6t78a&D8eF~5SL+a!kMbZ+^i8*o|JIqv84iSLc*Nq%p?_Q zBdCmmVBa7BAwLA5hjApPnWtl~RcBncAB@L#L-9sE0`IJcrGkzmh~>FaERj4P%Io1gPM8NYn#%KpJAsZy0%^W?W7!jW z3cdv8^0SV?Tc=TYts0DHCIeZ9tV2wuLxjpNBP;!;op-4Na+@J?hg~ zHt>hJksnNqS(^t^In%fboT;KsWLGmD%UrZXaVzuElFM1<83|)6wG3?wz^SHKn~9p{ zISC%KQuF^npviJ9fwgS+X>CWa*@r{ZJ|FJ14Yma*pw2r9P38%>WRyWOPKLhAp&oYv z4h5&+NGH#=m~APi0)g|Y(XRXvnpHkVMlzo%a&HYhCsRpJVcTW@aGhEW z+d(VvgI9Ajpz|S+micHCejj6Nf?37KtR;`Pu}wJYSvH2Ne;~HcYl5#QshIauA@8M< z-rtK*X8+$R!`{81|B*nJ)S+wJLA#MEc?Y4pmjEuC4^%A%F0KG>t%m;jR_I@CfbJsO z+HwBxw=V-Gvz-;wk)J?&R7k*P(H4!Q9g%>r0y_F*2?Sm!_ z!%*#-gFhpdVWC}rczoXoP9_ml(^SzGaa7bSCUFFGEQ_QMjf1aQB0-%5KNf!iTC$S` zND5INhZ5Ebs0r{e){O<$|J~H|Uw@*i6;M}*ntr6fTB5#lzYTxqNFbRcJ)yEzhQ0;T zN3NmZ_;rH%8piSQm;+alx$iortiOcXp_@?GeO3-` z^P#Ht%2brKK422++_UgCghqBi2J{jISwdJA(hDtN@v#-KIN%cuZJ;She z+I<#bJ*yfXa|ml5%W$}W+G8%SiNcpOolQe9%>`aB(8`soS z+g3yH%$y38uzS?7KOP(P$5YdORHS|J+_DeV_b~kFJ_;YHJU$0ype~GUFNz9WiX;|M za%+o9o=;E{wkZptm*aE-J8=%QqO1vQUF1}%W2)#Vs%!yo*fgqP!Z>;k5HX8|#yfBd zbU{?=fkpW0m5VR_IrtcqgJ1nJ@yclg9@`AVUG*?rw-|&QWLw_0Xb} zzo}<)OB!K;ucUfZmRuPWv@)D2=VUmOk1cKZJIj5nMO~{1I2Bc>1d*wzJS|<4VbiGu zj>X4eS5m2f+L{iW?Rd7EtP0p?orY~@1=~m!oF|-tCifWZCsZL}`!)2ed5k6}9>RCq zZFv898v%r}@1Fbc*hN6^xd)f+cj34D5&V~4fmgv91kb)k)l5LkW*QR+V}e>))>U{0 zegv`i6heD4`$6e>rS_V979NDVPsu5$T9)8%>|aLeAecyWw;h$VO(a20NE6DYvP~tw zlQF?vkH1Y=SP+gZZv#}aZy)WBx?Sw=y9wp}1o1X1>J0?-cB<=5RMN}a7G>hg7&_+?1hzDXN}SHwm9@aVO~Co(!1|@YsBEAWZBuuGb|7ua2m(2SupUcW zG>hl*PuJ)SCh1OZGq8-m36{#4NdYuOD;CxK!^czy`&VL#d`KE^-_hdyKQtH&t3 zs4w%;*?7hB#+0D8pprJv$8Xk?P+^~iFw-EIT6^;U8_531Hb8M8pjK2h!EM-knr%s0 zk`=`NJDoUzHqTioYo|qRhrvt}0(%oLth4Qf-@}*UoL4E*zKepLaR}9P6x_{Y;AR@d zei{u=nU|JGZ>nf7$?t8N3_mj#1=8_^bvy!jEOXQ$ycWzl%tV%EDMR1?{nYede-Z(n zM^!DL&ZCmfm*H+m&XvW!1^9b*aW z>^--zby_9f^k0KdLl$9AY6c=T&8d)OnU?IVB|&8bHx;#yBtiKGYgwx0A-TYvTEoRw zHuz`-H(qnGZv%ISHhkQHimVNKI(EW8sH)+S2c35gwBCfWcNP#(3V4CZ+zr}QZ1CA^{A~#CCUhE_j0KX}nUeULP3c)@vtw>!XW7o@?d1FI z9YHUfK;A`2@8@SYGKLD99Zj~$JVB*hF_MMvRXGU19vF;CJzLXhiD$!?xoX+Jn82o& zueOgPxMOK3BiRX}*cs@pI#9hd9|xbAwQ!nu0}j(_;VjFkrWu0Dyyjel@BCWBP&n1L z1eUog(i)$Q=&}=cfqY{RE-gA0Ud*j^6nc5j!6vYA=Dk`zg9qy~c=Z z@6i6(eYnjy0jtb|R7J<&Mq}xif10Yg5_SbLSA7&Vxirv)Ww0i=?Z)naI(C#Y2+-=w!j~aBx!}^`@_=iq-+OR8LnfJgub#MH`X#n24(&3W?A_C}m z!Zx0DljYE-Eg_@{<0t|;VKx=^460|=p#*y{flVlD!>EuOmEwa}7M|LU#$C%HxXI7) zOf?j@=rG(c8;DxVfvB_?fD7soxS}45Tk34wuwt>z#T5d&LY0AyrUNjM&c`5rm!|dO z;a4vNc0UlxGPKQNWFqX(}QO<<4F&#jO?lz%{Hc~gn-_3jw7v+trwXK0Ov;#-Gp>7|+ zEK%KidjdNM!cBD4x5#E2Js_KHU_~c%D5AJUm;vU53^(X8H(IyR`W6@(6eh(Oq=k-V8O~V|#X^@52gtQEIzpLSyt8vt~x#|?i(ee?!$j!7YykFY4PzJjp_FJtWf>zK6d2CA}t#2@_^;8ovw$PMZZck5;Z zZ+rN;c7`W`Es@`@HnM)Ym7=UAb*K!5E8r%q-JM#(*{%gVTnKg--m7WD(gC(M=~&@O zKvHq&J#wJ)%7D&4TY)yb3kdBZ!kS8?5mo9yLUZys=n4lxTR^WjTIxF_;;C-Xi&#te1}KxqVS2Xb!DTQ8}ry z4#PZaKP+>PK$UkKZUnk(N-iCbG-%Yxu(wWyy-gb7oJb&0UE31gvWbU^khYRJ>o^78 zG8}HDCWQHYRkA}7VJ&J}RJBBln=2c75EzyOwhV_0ta*=Gls925&^9-v!7vGhsWHK1 zL~u9ohjo2#xc=aWpnAbb`mQlLejkg$^^#D~pe2@=cEOLP-Eq*U2P#bZ;7r3FsA<>} z)vPZz=!8o@w8hOI+Teb@=6LHwLnsMPQddwR)9A)5fG$FknS2W_i=NIhlgfKObiu5H zsl3DH;cH+Cu3GiLk?&ez*WbI~P=kS}upEJN7Nc?2ECbc*9GtSwLYZ|oj@f2mw>kqG z=t#^l?Td_tZPBSgDv}z+Ah2E#?CW{MqMiqpvuro%4P#?pIyn5EgtGhw0%VzUw)mFr zQ@|_WmS8bCr>JSmK&tF-3Yt)sATh~0rrfI%)y!iV;%044<*AZmwmsPy$y!4-ErDY) zXYJh-j@=i)weS?3wMwY-sub01pM4tE<4!`AQATI(6w4Vpai`!w6`lO!RrJ610$pmK zA>z;-xUIVhzpZx=y!}1`wmpQ~#(Qwv`~Yrje}20j!)@tp*q2nmYZBXUDcg4uVLb67 zTno-B>#kjkshUfvtS8sNO_a4HPx-dwOO&>z_i8lo>xlX?|18_gQc(+}P1v7gGYu29 z?0gli>?tjfHc|PbLwX>tY!)Tm2fD+(2x+S3gM{b~s_E?#(M@1)Ae^@lkn7n0SM&T5 zmi(6Z+%E;%`Qw2D%LsKkato&5OXuGBBR(G5O;do&dw|VLpzqihbrWdYZtjNm)-KfT zoP#ez`I>!cdj`{Pj3uat(~b;$8GjulgI%FXH-@;*M8VYBs zX=l?&W!T%zL;}g8;YGzPL*M_dsHv2FSbH^$RaA9=M0|^qHcug_Qk>BXi3ITs!m^Y`cM|WN%}yaO zo+!$i^*jQUwSG1`prrL1LJzhzRdh>&x+UMgIUNH5bQ+)6jK|IB!O}s%7TzNpy>Amx z^K3Y3%&3NkM1oI0 zN40&9_sLpo8Z*M%b;=odExLrD4fhek614g@!ZzGNB*7iJ;Vv3&e+YjXORu#&Po>lR z*h}=Q`5nE^{fwA(H(*~xg+2B#Y_gBTI_n73*@t17wGXP?gD}oGNN690%lO0aXg7o3 zp*idGM!qns?+42t0$@|mA1>bq zQI&=v?7I-eejkcfgkjhEaTsBoiZLeX$Tv&JMAI}ZvuK0;s@AyU)(>Aprc+HXRf;}* zE_7itpbei6T{M++!aT#Abm$D|8qWb}un@HbuUy9AxKS5uHSU7#wu7<7ZWLCihhmXM zf6TM!gDIAskYU;q1B}zr>W3JFe-{ex@BHCd&j+d>JYibjgNoT3Mh$pu>_tb!o8Qle zzefOpOT#KqmffuQ-OU8XRKQjOT!FKt40{WdX;2AlMFsygq-`xmmyZ=Nvu06IRVpb# z6-XPPY)wGRoV2xrtV$4Q(^>Ru0muGxp~*W6o4g9BbIM_ta|&uYakf;@HrXd(TfpOj zGG0H4&{ehQbp9cFTz!GmvPbYCoZZ&mg7@b8@ZR(Q9_#MGlf`S}LwIj_%=3@nzvez1 zCtjn2Nau2Ltpa3Efie}d*TibL^SlQYw2x%vn^3Lf4R_*akz^{~`BeC9f8RT$!AQgY z;}pkQBDgMaVf}5$+ae!7Sry9;WBlz+~g-v7IQ*;Lh{rn6;!y4)~VolRApQ+|)i+OT_8 zo&=SNvOaPRSx2rQhj7ki$s(x7v5Y!&86&Bz3wB<|p3>v^NAKnM*nc^e#E(KCm6J0e z?QZvN_*-CYE6cJZ*LO$4x(%GDyxj?7SAyGvpmw(>#1+Ne7QW81nMZ3Xux1#l>4=}h zasdKbPgv`{#!*p^W6cK7#sJS^=v*_QjU)g^5!U%*fLubbkd3=kpe=Cb<03X{83r%u z4_!XJ;9?mn7f|y)83HdMj0MyZDL#{peHuIAe4d-Z$G!NxHhjMpECOsP>FhY^>>$nA zp_=lw^LWqZ40d3imz{BB&+8rZgm>_LcJe)Ci@Re2>S53xrJ6p@QpWcusI@0~UqwIY zP7K2NdHvBeC;>K-cGHFcBd}z-mP&@kZQ}^)IChLUn5iUd64kX!B5XUA!oBn?VSO1+ zG+<7XX|N>y=Ts_<85iI>na5M8KBh|KD~o`eAa~+9zhzeuyq?B!(RBo^x&_~5*AcMl zHUih*MZl)}@T4JiU49K-t8OBK3cO9(3-qk{1#M5gK;X(+SmYdlWyXF&dmrqw55hWA z9`A!SkIe~i4VAV_=LN9l|J~Xu85)9GqfUdZvV1F%VCLty<*`*fK`W{{-T-U04Ef9a zyZ}3fAc-O@Sj#kksAm}l7bsg1Al3#|EugZL{jLSxJSR|Bg3+k5O-*P}sLIV)8vrhf zo61|Z@S_SaHTG9vCfhffnovowSepbxZ5#ww<1qM}L?Y270_&Z-qK*n0;Z)Vp3xODx z2*R51)<^SvV?sP=DnZT1p;XyHgm|NcxUTAr!)9GDyg?e8TF5%kI0QG0fKP)ExYYBb zLGw~nvUvk{nACS8l%=@CxFH`KiE{RViHSc0G3wle)7wM65~d~_qIHIi*C0%}=? zC6Eqi4^7{t1n?nPiL;h-k`5bTtrP+}pNcw*=X1;8x1b8Gj@(55E6>oX@)1IRyaU(8 z*WtYQHe8n9WqH8zkf4497ed;ZaCTmG7YWUxvB25T+YxBUTW~ zOJoTaAJ66eAZDS|}fTRH(Onb(ntdAb}tw=(XL9`*g`PsS; z$EU`_@RP|1{A5H(8)xFRX%=1?XA#r|cx_ckNKeEa`)TNH+7`wZbogYRSzs;m&;n3T$-Lu*e z(#=`7hbNCc9c1UNW;~{v7EsGFE>YYBwTDY5xLKuRxd(w6N)J>ZtrWsIkg8hNS$h@& z9wk)VnW&2z0{sv+_>z&(WfI1DqNLe?31cM%-()sui4ZSlDdaVt*G(WGi`g+Iu`|g0 zwLp6ckEin7Y`(9Y?@usy=KHkZ`=yU2ocS4~PNy1fPLH=OJ5CAf)r9ss9!pXbf*PWx z_wqgW^8F9+df6C)nhO6o9fuQq?=l_}&bku=P*+9;e~Ry2H5fP6^v9s&BsdddYTG2( z*vCUngQL=p&xLnrf8r4WSJ%L%_!z9mA0n)ez$WV;G&%cWnzIn1W7z0okUe_*mX;`$5acbsjk)PWY{X(f6^I|z%m+ENd#l5mf>&-T#6Y!_*+OcvZA^LIw~GhIho70nyP35OH?@F&f0>GkeL~uM^!E$ zwND{qP82)hF*rl{iXt8bMPf(4k{bA#%7Cl@cWw=jJ|gyjGbWegCm^;SC|pTW+tMP z1cm~czRfIG@L&w>u@7)RYYzbi*>ej>&%v92J_@p+W18n9H4>Nl{whPC1 zbYy}81F&&o8a|&SWRG=*Zg)3mw{?T=$DYt1q+Qrf7;o(k{e~XUui^2A?u0MlEJNN) z`5f6;V|I7wa@yca9@Y4MiiWd<@U=zI-(3yevO?4~Z31Q4RZjvpsuezmr{l}aT3>W-5VW0_u0xC3x#N4U;T;!O>XMGTV{0%rytGo$#12 zmgsJSqBa0qarP9)j^|~XMA{RJ5@;xD+AbKr=6=}W)CX^aXX2>cXvF`G{lz#Cj#iOy zGL42C;p}P>%VLQ3mL*v-^zF^!Ll{f6x3`fY&$oBOSi(A4SxYUd+F#O%@}5wWq~97F zr~Py7H~s(iFZWsSA6!G{~+tZAw2vgJ<5+w=KRROi?I3CDmu!kP`*gOK#5y7i^f z_NJli5k8vUA9WAF~D8eB6hS?UYYY=NJOXn)0(Wqk5Jh@FwET63`2IewDzQ_itk7 z*~r(}$!mKE>wSE0Wyuy*_HlkLf%P$-lOgajUKe1OQF&LgBc5g_JU0NZcK1a|x8{U3 zAwzYkqKdX9oUI*_VXmgK)+DkdD8-x*leM2lUQJ-vbuL`;&%?gx0vwBK;8=Wyz&;0; zNh}l35ZLG7IFag`0GGrjjN zIPsjzvTIb`w-B@aK6+Na#jtC?phv}1#IL^uhe^j_nY$mVtlhB6Jpi+U128Gr2dlh& za2>S~c2R?2CRxeU2?RF5%p&vF)+z#9plw6v#4d>d;dlJ@y{#lUiolt_g^EhZoZnT7 zr3{5rQA@G378NaUCa4Hq!+AayaLdrSM2m|;mlUmpwK+j8@RucFh9EV*20>>|P|JF4 zD{~qq3n_dZf?8!pHEGfanKsSwC4c}9lws|;&Ex#Xs zV{w2anTbH$mY}nyBdC^nNuE>14uX2XYFK6*gh}RM7!%f(*~eg>Sw?6}s@pPn(-~|{ zDEF;-hF+ImBL2WVcrL#Iw|Up#G~)^!W?qByyxZ_xdJoP^AHaLnW4Nz=PGH|h>ZY66 zeE%)p)@kwb_fP1&{34u7F2iRk9Y;E$E+rS>UUHGgwQ!nHqXeiqNE?@uc?f1H<5ABk z5kK%dm^c#94hb-ENK^oAL}$%N{{M7*MPWCj)6u}f7tP}QP`SJXRV|@=gm9*k*6)#B zuL$2C*&h$EAMa#;UeEJuc@LE|*6?_B4??;NAXy7#i^{pQ83mo89nlOr2}Z4&4E@8k zz?H?&O3sHK?SL*Vfv&u7SQfta9*Iwxqo7+u+p?Pqd@}8c%r*C98AMyqPhd=YBFnD^ z@bzQ}d<1QasA_@q0N&G^&O$Giek1WEVGQ0{45Nz9z)KTZCrwCOX5qC;mSW}Nokb!3 zu$_V9>y<3U5@oFf8A>b+|M#M*|5DI~LZ?>FQF&X~hro{Q zHe1sIj{_#*mS-u3*J}>@9|ECKg~QPz8qTJ%1a$)3r4YIv#_?3t22gwP*qe&khiX}t zW%(EqW@g%~Fkf5X@RxBjv#9zD)FNl2f3pahj=NidlNmMNf? zy{HA$RMdoZ#=*^s2Ir88W3 zPd`l$ya_B-mTc)f#~C8Py(Qf!;mLFGE27daK%M(2Xye#`#}nwegkuI3a{=KfL*WAF zVm5HeXqHPT7tm7{l`U(qW$wC&9cU6ehp1>#)w5Wp^LZnAPaAfk7JR?vED}(bPEa>x zr|HD!PNd_ome*JF@w#Ea76N%AUuQGRHWsRC{az}2!Wze@w2$!f9pz^uw6#YF=rTGG zC+T3E;PL5x(4J=QkN3gyF)b15*a)(8NjCBjWo_$_220yy8eIXkVMyG9>eH04 zGO8Z%kW#Cue{M`ap>;p!&%7%0Qr^9kaG zKrmt5XdadG0_Z~p-UM{$Oe$-tZ3W!3@F{FAj+=GG63b2mwL8okI#Hz?(s!Cm0uoWr z=Ke4xtWC}QU_wY6hP4T20Wa0GL~C0R*akJtWBH8A+_HQJLRV^nSq!Rx*D#dLcB80f zIx#Zu%-=-TJxfrSvfN7G&2!?^$h@?GTz)@8X$%|3NZ4p-sIB~9>7E8vlU%5V{zxDn zhI!r*nB`GXvlwL@f-3(wLKoMd^N9x-apf8MUVed?J@=F;6Bo80%}hFYv#!IQ%GhE0 zbwc|#d{^CPyLv!p@By5c-9x9HwWxXf4``tW^eoU}Z<;_gp7)Q>i$Ki@-!wFzeZP}&8Vm!_JQHPX61e6NADGrf6FFTQ8bA%ye@{A4i@ zKM~Sz&GPX|l9`wh(pIdkbMeM955KA>;hJqBTA4(np`|bTLIC@dVR+kIO*kt<-aIC# zWx7KYw9HKlzzs1B;`B*-;bR*@_itgaF$sr*Wi%X2 zW8g|CyO|{#)HGr3$>L>3Xj3`M9JRMG)ieQZP}K&7EpydA#<9v8>HsS3;D+&!1B~NL zzM<>?TYov__giNX(phEqkwak1nrdYTyzCCfQB99$$vJ*YSwo#gKxZAKs^+l-myIK& z$L_7c*n_ngLvWATeF;-nok!J}t*9Hg3^zMYLKmkN@UUwES514k$$YhJ*g^QZYh>P< zkDWRysYhK1b5YnXRMsxG&EVrkkUO-4hom2Mm0|QYaJFfNQcZt+RRYN}q4yjQcu>X4 z+_fLU>C56lhoyW~vSsG@EOwYtRM8_0DMt&~@Cj^vJ^`IafakE&l(6v^2%y;! zSoB5gC<1Ls={Hf%volN_L+_O!ZYlH81avyzR|3g8@tzWbcQHY{jGb9_-&#jiy@}_x zv25mNkWRgu=lAmc_p==0xx>8w_z-9h_hsn={Skf^QPt&y_3@tc?E64_ib}t#7qln) zZxois$|5*ckPJNv}K(vHyPW7$-|JiI@g2JeJZ;U#GD zsXX$|z#;el$KGE?S$S>mzG&83Ns!X%UhXcqySrNijeed4;oW0LK)!JyUOsRLy`HueUPal2sQ3c18a#ae{ zZ;s4oJx`@gU@y8xb#ej0i!LGJl{*N2g%DnOQx#K@>0H4Z-bH}G`PF*}-}-@4N1@#J zTTO*Sl@zd6!sPeR@#t3=eDU8HcKK`cIrkOf_q>a+*U!Uu@o5_Oqj1mL4^z=T_?7NM z(8Sd+2M>iw*BTD9C~GOFD&cTRDkG%D5s?M@CG<^LOOu-DGF~moW+p=0Kw~V;>?Nw3 z@RsOo`KXC4t?FEndRmOHdc@RM~ZL1T~fQ)A$uQsu_Vb1a;6$ zfzW#R6VkHYb0pzQ7)vr3w;E5XYJ<#qrm>W!#+_*tsgxz`t-{&jK*%vcE#Yfzy_)_r z>@QFjb*!MP&WXd~R6zn?smra7#SklX_U*0cAhqRvCWTD+U9f4Ije>F1 zDi|{lL0eb`cdBPi$w?R|pF+U03+TG%Hd3z9$^ZD@7<2I}v^)4Qg15g9$2vAbR!ZM` z&wm$Q^WH_ox;uzi!?yY!A~t@2fR*5h!#MjbJl`lu#CkIjc z$q`gn@~V|kwy4Qh+D$g^$J=5Ez4A@M6z5$Tc&SK;;~G{nkZ1uKy{Z)Jhr7! zNoV7G&19bW2d($-49vf!kU-pHVrX3EeHiWh!hQF`eO9E&*V}!JkfL6(3 zYATk%SsL#(qby;`dy)5zj7cj8kV;%@jYKoINF4M@!naZLQRb74ZZEfk)ukD{_3hxR zYY$&S+Q+>u{G@rbL9mX{_H3sVbqFCH>P~1=QHK%85n2JZsB5W8)&UWstOe3MZsO4? zw;o&nHUFrnZ?Tl?H_m}hii`Wv_>4xz*nLwV)&ZWwY@;S2V)m z^gMr5V9o6@wv)-IJuNLosp{I(^pwllPSGPT=XF&Q-e-AjY^Y4ezqe1qjKMt-M8hfx zS9S@5OOhF3Eg9jmaDdK4Wl4oC$z~dBTX=>Jf@Sa$_!bk;rBv2aXxJuK!F!r>Jz%eC zQj3`?d-esYZK=sz3I8RR5xSA;W&IuaExQ35mA8-76<_-vqPBd1AS&qaEgur*ci>M& z9kA*>_$k|5VD^|wVO^Vn$T zdCA(ADt8hRS7VNRJ&&cpj#5k3mg+SYFFJ!fFXuEtU7_hy43G4;;97hdjf+k}KeZfz ziz?CKwev_meFte(pCkAEZ!q-C=VjEEIf-V6Br`FG(@ zP)&4qESyaA$vTm&gg>t#P!13qO-P6=1&( zQK;C^8_%R1=qW<=gp>fLnwBCa?3dV2K)+4s?&bC%`|1JqGU^ z7Q*^fzINqsU@ebV655OTz5;8iYRu+2Nl0D9_g+HSE~m4ah zJHp3Ypl+=|EzLPjEm_}ANK2FKZ&B3}{*LtQf+#9)H8!p3SovR8P5=2dD7%p_rh~KXMZ}hpr>@z*S`K zzmBPI-N2#ghw($k7Caog1}mGSBizv4nek0U9b_e>sir0L?Q7~y1x?lMkc{xYRMk}2 zK9YpiPpwrg@RoVkQYa;e`#xU%(9zf%*TZFenx3fC0T1SlFH^Qc2y00+3#Z2#G*z{l z&>U0`q~b2(xykfK3wV>464<551UTU-pq7eebE&FJs9+@&F8fk9d}b2r=CE_f81*7H zsbsc-0A3=cND1pTRN3qKUaQ%W1khX931!XejqGe&+1a+RBgrBjJNaIcDSCkKahUIS zjE{?==C(a6l8j7eqR|p{Epx9mK2)X6Q(;Tk1mBWNgz*)&3$PR3_7Z}c z>e@kYdrgyq6z5={R<73Q4W~i%U3DAQd6(h6goakutA6!8_!Hm~`u16M3jtI|0&Cxu z@4~U{CTt6CKsVzO?DNkfe#bquEB^{j4}Fff1D~P!aT@YtpQ7dA4-mWM2CVZ>z+8F| z=F~S~?KK&upx&_AI>C#o+F|bstECeSiLAiYkp@GS1Ql?Lx@P0&lu*A>g~quhv<2b< zUMHCUgg3!0p>Fw31l9^>GOqn+NSw#=I6*CA+$zC}##|NlAjr%Fp8~XmnFWqi)QP6< zcoI)g$Gt*Wucd-sg*pXjf;z5#yjl{>WGnNisj{C@WnVN+#0J9{f;s>i4CWhG77TTucVVtmmU_J(q85J-tJdb9ZE@8|u z+JWnzBD?BCOgQ@qTJ8D>;oCk$$Sd!|Z`}v5EGMK1YUA8nu*{(Yx8N2cmfc18(t8M5 zcn6_`a^NDuc`2Q|Meo6nk2~hxfMNP|7-y9s`}j5d@cgM#(?aYibpHKke?k>~7rwKo z&ZoT#%f$U~Yh8?n)-Ld{NJzUKG`5a}F&pmo)NnZc7R85}upR$4mJ9Lheta9hgHWer+-8mSv%KGM(`Z_W5Kw z=o1Ox6#h@i^3I|?kg;f)Cq0ShWVy<0zD`2pGA%1L7tecV;jvc|9(rcsk+ujAWGWVw z^!LUYcxauA?+r5v>pA$rwg^?`VzhE?sfJffmKfgG(cH?|b3LrBa{op1zIV=*c8*WW zy(P!IzvMn+)64j@oy`!1(e7<<(<=`T!O$STp+eVdal69%2_=zlmIT!(!J_wP8I~;^J zfi0_Th5E@jc0Z-QgB+4E-V3Ab!|+Y?bi!GtY}F^0g%HwVl5j>v9V}t+>8O)}DB)?S z9!`i)DNrlV)D}>w&LAjfu_Kfcl+)P(W)ra7hlIps^%|LXJ(B=kO8APRp2dzKA@F5< z?lL}SCC|xRYi{dg;rCTM{|b*~L61#*&8vK_j9KmAdu(UtlsT8X$D(G>Bs|!Xgz5@H z{veGpYG_OmbT_*d9s6E40$@iqnt~rx}%d5sBp+jdl)HJ4U zusFKW&~~EX?MyXIH6gH)F@#^CMoV=6xB&U3b2g=a8aWAC?pzt;7QYU zRLZZ?DWqa1s1ZphOJbRXy5qP{Fe8ThO{uP1t;Vz1Ww>Td!aBo91T^qfpw`o1>Sb%k*QCPR1gwHKe`lSjX;IN?ytxH`|5p5- z_+7}cQqTPSe7?zx5T|lh&r=I(NQQlLX!&|i|Nb!ao(ume2N1CQB0|<)MZ2BvVQj^R zD8BUx#-F)|9tZ9rZrfdi?)VrXZ+?Q%Eg!;v)w^tWVOnq-hB?<^nsp2Q1amNzvj93+ zvbGlx&hrS~dAAU-o}0@c`2{*H7j{;sX&_B#Z=TYxIdZyBbWQxbO>_z{5-yYKA&4a+mXrl z&!!qr8H?KS38+h>ZOP*MWv~gDCF5J5E$TW~)HH34WQAvD$7Cd|Jk%#euW%p0qWka}-B-Ho-oxiFgn(57z>xVNk;^u(~ycx2Zk631VM1 z9|GD}l(e=3`~}v!jtC}vgFL9JC89D@X@XlSlhLCtQX%l0s^t*W`Bc<-gm&is zD@fma6?yO6#HOWX_%!)7JWt(>Q+?;4i%nE@Z&d^($UI1u7?tT-5(1Yw(;;312yjAL zDeS)RvvgOG4xyTs<8Ya4ZFZ_}UwdE7@EM6GQS?fs5K0h1EDLvpNcg*$&=yrKz@EY5 zDTH-8VVz6wm93aS%%xH-As}U{L@D7qg`Gwewv1m(J#Ru9vk3FK>>RQ>mt=`AAXp_k zd?`DPTJVESz`cgAC%mze9cneV0_shouz9?l$J^Oiw-2PU9)lz4qtVj02^OXH!lQju z=f^Yfj2`^+;{>n(Tc&JP@c&k(6VA!%Tka&T5=mvZN&hEMu@nQIvd+hS1Um4TQDleE6gvr^3EW19_gxnvgE3 z;=UBAsDin;g4=TV%X-&q?!s@`y9lIe4p?zZC7F2<)|O@0;IsA)VSO96^2

5e#2)+v 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⁢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

kxRjb~Mr5 zwhUqfu&<;|1o&fEK)YjE5kvW1sYu ze)8d`>gbzyst&(NIKP3h`(MSl1A?{Ms@1332I+AE_3oPn)~2mm(B5$kWdgH6O+{V0 zo@$z9BvrM*UApExhAlsf;VUm-^!&5fGT|tGQF0L9_g{zKX4PX;`!aO&=#KyejZrDb zMHJg2!M&B8Y!Kl+kWd~Z2{h0k?qq3$0EvcKY{8arUor0GMts80?O1z9Xl4y@uN*!wN)xRmX*B% z%@Vks;6{gPAbTE=GY2hQSlJ9>MUp=g568|z(~L$m&z_G*b7m2=vtU^~gCL#>i~61} zqpDa}2kVB}u&kk~Ur8fjIV-mHtjxAh1+C?Eq^{Spf?lmb3-ZvkzXp{P8T|R;}vld}UpAv*d zcb_^cds?URA2llx%D{Cxb5aMFlhrVUzsj8)#? z@1xYHavdw*HR0VN_pw~cFAAW9kcNI#pYfMy@JM0bA@~*4rS4O5e}6)y{4Ve5!$E{} zKN<{#^e4m7^!`Zv^j0~3Ak-fz@yf@%o>HH_BT$b(^BW^!d99T9!)w1zsJ_PU@j7Au z_E_9rU5epd2cT=GL8fnV8$$D0dqva7qb(Jfol`sB3ZwuiSl+_?kJ!034Wi0Q?O{5BK70p;@A;xj@fW^ z=9JfPb&O$&MPA1o{9zB@(KB_h93`j^@Etxtqx8@Ws%aVr2U(6*x5f|T{a*9@@Lzgn;^fBu$n zfmiNUma!CQPNk})uD4%9sd0T@MA^m*Ea#}G&!BYeS>pyTS$zS;%g>>F{uwNr zd<^fG9mC&-Y{6HZXX8-pIP~%zfJi@$oK=X(04nOhJQHMQ3pBH(8C75F5CT2er!N)s zV3STXJg7gS!?nIA!QPvYwt+dsw>LukdLe@FjtJ_<{RsEK9*E{2s3xcczhz~CXexjx zRx)arjwd)3M3y!MmUx1hpte$B3)DtcbDQ=<3c=f9I?#>kpcgCK0X0AYE5~6}Gh?Y1 zCf8D3a9#CuSQ}{!EavhOLVI;BT9&aQTT6AZm6g|0!g(>TyM~n~6}EK+fwGoxSVpj} zF2Y|A%tKOqci1}xcs;w(%b+z)Nq#lzjH16&nQM8)pVE*LYaSmFNb1Kj=N9o+xZ zD|qzx_wnc-?-AT@;+Lvt!Wq#+|d}c}vq68tDSAKPq6Pu9HpcXd;XKfdtsjTH< zS9dA}y-7*GM-~0vaNK>Z5-&Voj|D~}BR1i#}Ef|+o}F<$Qwjey5oz@bAeN{(R7f{@DLUD zUK)z~`OY5WJG!5W{q|}cpaS-4E9mnbaE_o-_eX91K(riMghwmKp=m`W9xSfJqekAx zLjEV}C(sb(|6{=vwA4{y*Am<{UM$Ku^P?2pg!IBI^lds%3)sVd!k!C!8My6Vjoxa6gdy_|L!tKPqg0 z8p9!r@jk&__c z>hN11k3V8l*5eMUKQ)2PQb9;dRSVb^&)-JHPEJ|Mx4&Srlqs!f+2+dz)D?>UUUwEF z)|{rIK7+EgQr1-2E6<{21()Zaz}zVZae4G1{CVhh{G;0fTq6(*eRC1zJD3nIq>9c( z45yIzDK(KEw1L|<9=k`H;xqmlK1!qF~-`lPG8U-Fq zV21{D$3VY+xD+-PkJSDgOSKlsNTk-5YzZr|6+F)>PM33; ziW`dvXjY8Y<%G=I5op?8iIa0D(Xvl9m63}FD}C1vgmnrW+~QF;bu=D+x||T={oWXg zrYrras0(rb3L$@8{ib=2D^!A)hN0DUcdjxEIR-#vl9{Nf6J`rV6Y{^};0zPgNizdeP2y|NpB+&-DAooeVJmH#CwJ_R|w zKNrVV%!K`O5hjU-vsWhUT{}`)rXavG2{-l;u3t`v<>w{9=i_jK3e>?R4$l%=b_BG8 zXD8TuW^w-vXt>C=OQ^Emu7~C2GJ>%TO*aTFsp#7T^$S$d*QoN84CKWsSZ?q>UgU3e zgWCmb?W+VKCiNz-X9gf4_~fp>LqNYZ0zXlmZ>t-IzP$&)E2I~G=F$^w+hB}PlGKS4h{jx2#v?zch0Ap zoN5Yl%AYA!!& zKL_8$&BtNia%9<2#kwj%i7h}(Q%_Gqn6MV0ZSHIVJjDiXvrf_6f;6{rorbhjAepUQ zS|HDYPhhrD)t-SiNPD_QBhRH9PK8XsuVNPCgAOY&Cukg^?3Go_mJ(Fp6>xo6@0fOJ z6ExO?_hc&`p6Z4r6)k|bDr&j0jgq!0Ycqy)tE*dW+3F)L6)lKA$vXDr^Z)Vl7}lc+ zXSu=!X=N)LW8B|&FlNuo7$aaCHGR{lYqeFEZM}w3Tdoq+S1@wJ1=FuuuJ4gj)B^SD zGZ?j+pk8(gCCg7^#KM!9SbGdd#vI45i}vB~y_eywjH#FqI2ciUVKq**2Ji^+8iO=yY>d7+-gM0SEl^rwj_04%y);z}x zd9Ym1Cq#zBa)}E2d?8vcjzG(WVzitY%F4MEZ*QtY2rDN$LebeV9d1;5uFe8=1l&B@ z!`C|$vEec3*DVVx=S;-e1FP`So&EUni&OaV&MMqHN%eGQGSA2Fev;oq?W{Me@Zg1c)nbs>P;wTVYnaXudXhL!)XC*Xr4rHBeh##3#mES=L~M-^>HD7JM^ zr6S3|jx|&9hzh*r#z=k_evca^oF}|)5zH^}_qf1k^a8JYi_h;WVR4O$?m7)Ajk`>E zn*c2?<2oUyOkOW>{fz>`d^nFQ!$&*EBELs}IJxIg!S{xP$51$V7QlrDqO(^YxOw-X zQPiK$yEok2y28OR6L#$~kQm$q{oI+DGo=c{`V2*&ullLS@SJf3bv&MG7mrS{h4{<5{gApm+@Rv1G&{U&ar{K{J0&nLm;NU!1p64@seFgT-oC^zYiT&t^ZC!7j7Jj*?djvtJcZA`nksuNpM4eI>(MkqHP-te z8lHV?VC`8Cbms4vLQp5FiK!ONkpyt?bgJT+coa$yhu7mS)$-jSDQm)-(??X+4@~+` zKI7nOSi&1&iCTd7qi14*>tF=gD+^#EVJI*Qx>UbZ#&UbB8oTy4$-Lc=!2W#wfIf?dc2vs3RQk1;HM-m z?&??lxJ|mJQNA{}w%|)JOIf$NyNyCNZP1VB31F#ewLhD6gn8@KsAxf3fR_8)v&Y9z zvW`9Z{D1uj)?*I6{&Ceos_Iryk0zu??;))Byuxw^6}w)Qg9PbqXU_oi?y8U49ZHmYhKGf|D3mcM@C19>dRz_TX=Q zm*SJIQ_vVO0LdQR5I{JG$@LwaOMnzX<6O6=s#bbY4b~ABNOkO|Hfg!L`w{K~5W+u> z+Nw2gT;bfOy;0HJhwGxk1{sitsu18o{8RMu8;YYb>fUDTz=> zWJx2;mC-DQz)qohNMxl%fP-p5S+)sXTr<*H0nj zs#yWnvx1w=vYf{+;dyEZlg45y?XmcH(;Re&=nPjXXcsDICsxeP?gF+dD$U2B9V>ee z!f`&N+AS1mq&a7d@{nBFGKRlWsk?Lbbf1JjJM*O}?3)B$#K z=dQbV*@KF?8=Slc@&1Yl>0&th4u^x=AlR{DcJ%7U z^@FI`2E)fA7oMDVb>%wy?s)c@4D?CrL6A+tqstS}bg2yY&kV;uPgJ8{<^UQWnRw2T z%G|yKVyS39`{X3>Zzo~>@)%k^Ifgwn&H`N{3I6shRM`$4;KzIUVACq|ESnEXJx_*Z zAHTzX-t!@T=Y3qauN>Gj4x5XI!p=Fyjjw<6d)zpF79FE|!L3buxI4tc zgNA^MeI&eD%F+w*yXA`sUaHj%lW>1C;kS>V-cH54Zz8_CxEB4R_**;|W?JM3BXLbwprxrp3M82%-_C>??f@D#eAm+v&gH|nWcLJjaUAU((2I? zCly_TCb_u7YN(WFped9FLx>dfOdF8H2xkKEKDRvxn2v`bwP*^Og_iI+_>Pd?<5z-g z2O5=n_r0i~30~WflyYwqv;?)u%Wc%PO);A>$61th>~TM7trxT(M|UgRm{Njz5!~`z zc?6~Ky@*1nV-#k24#10HwfJrFCR~eKgffpp_&Q~q{^g#&1gW}9395p!x?c&(#=Wie zT4w{aZE6c1Gj4A}*udIbZt&iwU$gEq&|L{k!9=q?gkH_uP_|&ga zMN3WJLFMk3QSrPK^-EOLw=sIBx@z6PD2=AI?FLHLU!tNWq&Ho}$o1zjV$CU(QB{|% zJ%b7^m#jR65z9^x)~8Xt_!Np3oIv@kV^}`+Al@2AWj$yGzUnapd*e&d-Ajs?%2|=$ zRN7|vOYgp>ijXONVw+B^tF9YfJ!BX0q_BT)u^Y4YI zP_WGe3lMaU9*^*nwC z70fKE!2J!Fl~;&oo(d++W)ql%*g5m-d`$^6#^7%aH&l1)r3F5N^^JxP5GND3nx16nnh1+qB_jHLcxip%P z<~A7-*T><#?bV16?SeKgo#7VPAC8{G3FQhxx){$m_U1ii!_Ti9TwJ>nw5mJ@!PmEt zm1a5GxmCh1s05p8rsLy{WATq&Wq5F`68Db|!-J!IuGec&pFbQ=J*)3RL%=x=!T!m3 zuJo^!kRG~ zE&Ts9MNvt|)f**kVR;lf)1(7^6e?iX;h}=V0y)q14-`;FP&aeCHH7yTxd`t>OhLI* zSNPh+!c8fAO`1;vIY2>Q>gQ~OwHYH?5LR2WXNpnL+SfMDG`9s1+;V$!e=}qy0qw0Z zr3L(;j_{z0cK1(!vu6}ioRhH9vk>n`HsX(&TX8(P0X>`sz|$!U?uz6VP-)oP+M5Ms zwJ&$!^$2ARuE9FT0NPhwtZZGaq>gPtW(p2d-{@A?w$yYhxXrl;dL2V>Kgr+ybE^*F>d3wLDt9fu?K=-w)2dM5 z-xuxq$1pj+{i&1#1{zq0`4iAaS+_=e8)e-WA%t+als2a!v{DsV7R*0Tc(4u7S|8@$ z6Op0)32K5oL`s__s3(G`{<`uHw&`UlM;?5VB+zdOESTogb|%P z;hTdK@Q@Y${mliyj&l5c_Zq~9_a=M^XXg&4ozulJ8qSVkXk!8MHwlBx&#Vatqs1~7KyiE*Xb=YYD|&wP>aR@Y9Von3SD|c-IV+<_y3)Gil_k;BVVV zLtz@9xdOz-^L?$Lp*nmLEQOQNk~ag^0sIei<9~ogskIYJD&OZ0QqgLUt|zExqPcyw z0dsRi9f3^)n9~RTRLxY=%^`$!5aAt2wH-v`H<-tV&B3?rsiNIWk=`~HZjN#Aa8D%2 z_}+UZ8};nzr7U8Cv@I>K5}4Q`x2c@v{#LRQDQgy!vs@@7%;5}PvSegw2}Wh<(i$N#(AyLD=Vb2t94yYMEa;!&B6ZIV( zJQNZB>Tf*|5rlTQKY<%E$N)N!e}qs%+Nfp%S}8@Pq|Gv+9OBoVU?$|HqFHGN`*uV7 zus(gbQI4yZY=IC9*=Ln zIEYWbxrS38UPsydg$T={3iKzKUE-*ysg_vjxDtNO9x-rsi={#(u$`%}-Qw_!a|qT| z^`pXMMZ9k;D}GkIhimZVp`F}M3SCN?6_gW|ms2DwLw{72jzmsQ23)-Y(az2r?Hqjx zDqq6Qm*@A!Tbo 8a&RSN4tDyiL6s|TwHZ36Lh1)9$iSif3~BTFa4?is3a*G#y& zXY+bFaCFPTvkn9`i)oj3q?&fi#Lozj9zA>G%ggi7ba@IM3dYAq!g6pp)g-4!O3XucML>9e>{vA!|GduM;o4L@x;hiq7w4nt&8>KA$1)V=l;Y{<2H@$o zgK%ywpXD_|?O+k^9~_291a@R20 z{MsVmwWauQ=?p~c89b8=2SVuCHnHfEkb}P+<#i8@<}>B@+QuSPeQ+XLc2dpm=6B%l z_ijTaf(R!Er+9)o0xr&xaCPBt-X;om&qbl_E7#8aWC!s)pbxD(WV9w`W*ap&zC zi(uD8OzM`4-_4tZ9~O?sPs=K4z)iuu-7|21|7`qi>uh{6e=6=R;rqToO*2%=RM04A zQPWL6jmUvi(t~SY?N1Q*;4|yZce*PL(M-Po$+c)ns^kBo4whJMkFG-#RdiE(xxNYL z_Ege@a&vGEnpm1cs;QoB%a8o|zY3X!me4tPmk_UT?2S-6n+HjQXDC;Z_9&z=dBJ}SK95+8 zU&O9Kz4t^!*%6*@+~2^Ls#xx80@sT`HseSua7^xQ^_w>CZ(A{nnnmz6u=dbA!K``p zmp1P0$Lk1jlNQt@GTc55`oR4_UdhF zr@oF-LRzp^V40M)U_EN1qQD7js_NmZPZ?kjTX_NnOOB#w@evd+Iga5=kE3wjAr#C$ zic!;#V9l7rcz^gd{4iu8{?xk$DJCshE=7#<7@Z@SAP?z+4*Vmn2rI|$)9R>{ z8(~dgrJ6XMl@ft0C7nQ(ks&3aN{XX(o^G84yV)xh304o<%Y~F!*}L!q+HMgm$1 zOUCoP9mjXLWIU{Sd~b5OT+Da3A7R~#MV_G^d~TiiUU%gCoK7XJZjLFlVT~hfsiG~> zwXnoB5Y&XWlr$mzkYE;|Tf%3eDU{a^nv7;DZEGm69npwCM9;)Vuc1h2mkbxzB!Y_o zraD%_lE;zU8ja3ffn-`1plP`H2B_Ux!DP0mZYgJ91(>zw>h|E&mI}0^t?QMx1*XF- zAQ4X9kqCE+!VIr|xD`@^-=}QE8)+*s#%mZn+I6BqoXvCCz-rvpH1cdIKou}%3mOxs zHIB50_LZXMIvboHw>?Wu_vEos*j%TsSQHcg(Z;Q!)Sx|{K>Eq&f6hm+9=-3?k1O{m zr6_^@{2eN4r4(%i_4Zo^(q&t1>RK-Fk`0$^ZPqL!)}BK#L9OYCHD{=(Pnh=V;^ij@ z>Ju2g=m-kt9zls-w7ByaV6nF2~mcXX12fDY|>+AcQZwqPimpTjS35 zBWRVGj4Il=wyBuI2w(M`R{OM3&YXr5#KHXYwGZY#K|Kj)Wiq2u=e*WMQc*{T5#)Yd z5gO19!NENc64)IB{03up)Fk{XZ85<+7cD8Qpi@v zNB{0{pz__hoN#%6F4g1^UVjLh2wcmdLP9u~_cDY=!%*OA75;f@3O?UG8BOOV4Pr|a`xsI0Ari(tLF5WO;T@ibLfTcE5zNE+yAI~G&t!n=+1iKOx^dqge1|*p zUGBtbGS_#ghed{~MyQKasY6S2J;BWXMZ#<}b6)MvmiBdc6h>-P-<4fGZ=J z$?2`OYeCsR6CPSt&{&|9bP8PkXxzAkBcojcmbnean~}5eyUa~E9X}WSorb{EzB8X! zXBq=kxu%aZf$T>}n^CZ&s%-&eg0!vQw7|>#l%zxv+bV>xyTHtO19`!mQ@OWIdQdKV z$vw{VnJ!pQApPX?KlAgr&AObB9!+2?pzQIql%Ou(!9r!NQnK+1L46seoC?&XEW-t9 zD(j+ECo!C*n6Ms3C0(%a2#N^mqWOnVyzm(E=Nv^Ym33kLaa2z@fHTG0@W-MR_&#qA z-pC${O7Gr?<)1-)rH$K~3Mrg`4)G^+3FILDnL_#JG9V|2ZNX%Mvq}$B+PAh#hXrz( z3R=oq^8$1@w?~93^I0E)x+empz-d{J@Xf=u*ecxZuzJKL;4Z z3U4rBHhe7pI(8&JttrN@b}hv3KfHtwKDdaBAKb!(MJtiruL5rE2f#hDGp-z)kERQ= zaDOu^{O4JA7US;m#poQ<33kp>B^}`Ano7k*CFT?cH}^oKbpJ z(JBi5Qx`89XU~Np$UPc8Qgg7ZVFrHv<}rNz!7e;_s~&e>n~BE3*(gluig7u8akIJ% z_m|VKn9plg^Zgt{<52ybi}?N(623(&1$+;C^S$iK_5Jl8ayg6dWH)Z>NFd7q?Z|hz zLoHh3XRuVGC6<7W;rky^&3C^MR>IjD!R-WbGvVAET1{|spC~EosZ`XwE)}*VvH@=f zR-wwNA3_~8L~IJ-Mi?vEh}w|}SGm2_t;)E&y;2BCDrSP&jC@5ndvd+HT*>9_AEYc} zw&-s!MSrUunhM${FpIz>#JQh>#$0^k;O!8Be8(&t@*0T`+Bf2(%=K6vGzoF`*`__( zfYi4KTwA+ZdAN5Wlm*=!coDRZ+o!z=YqdKI-o_oS3}aN$rfU|b-X@A$iuv(Sl~U#k zChKZ*b#uLkt)KK0NI&`f&;LAzHH%Tza)p=eP**LL+Zeg|1}X&YtuLT-(^ZVxeBBJr zF><5EtiFO_tInW^O1f~xaTF2K#ftu3cG9#{7cDq~{CS5tJ&OFfhcUSRAd2b_W8&lk z*ju^-pBAjc{UHnSWw#nE4l6{OYYrl)e#5AG+Y7q>Jz9ZFg&nFiqP=b1w*m=Vsch{- z_=Xa`nhyz}x|YkEppFce8t-P#6{_=v$#pKbIpH1H6(NLouzy#?5k57cMR+HLl~;y< zJr7pG*mT*_KpqK`Sb-5@>bj+_UU7u5hSW@BWhO`~X-W!<`eAFn1NZAdz<1!X`eJuy zh1iW{072c4C6Ct_HV4*GR4Zd9;O^K8{BF!J{Bl|@e!X)JzPP>#pMUiVE`RVUPQ7^n zUw?KG_l{Pxvp71$29&aw4f^gqd!ip6l!R0DNt-&}PBUsslvoZ^Xn>VL^A&5?h z!r-C-n6rE~UV7m$zWvoTG@Ycv-!c--TgL%A`2CcRd>d8eUM{P@?=eF3*i< zsiMcAc^{2}1C#h{`Fyv_nBcwb9}Zlq!HRLy(6(I`>>WF!E!Cj|f%;tAC_K}akamdU zava*WiA9?>VJ6D{)h+y8&orQE{ct?kScIkx{Jl0-plMSXESrWA()=EqitxguTocLd zg<53lh*@vB|<;nl_X_WT(9Yj1D-aG*CH(pYKPGljsX zLAY`%ER)IzXc|~$d`1O)@05Fc5J4?3PCr3fs#)*fY+Gza4xy}O9d%3WKm{!oO@q^t zSZm(*7Bg)y-LI^1RMT;@2<%z|XNwfGqP@eW6VCh(g!4L7(1@LbKS#~PYWKlNw*GX{aBx+}vjBZ`)>E-vqcBmQtWMDMZyyok`$k zn0(zH{u*0777@+~nCdnFR|BTv)39ZDJ$@y|`i?+|eHQ-*t^K3rvL;}SV)hk))gDdI zdJ)tDw7O>r%q&W3^0?jFmf%FmP?T1b>e;kW+f=lsCSqLRX3@x4EKeZ);N3gEk z{mLD+S&!NCD#~}h#A1VW$(HMcwMNytZeTrwz)2e%W{z?6W#>3lBOV8N}Ky?cuhhZUAVowDp5{{&_ppuXbt4toMnZCPIj~vk zk5Ji7qvQnD)!}}?>-E^TV;!EMVpPs=J3*SjR@&2NsAMLNFT%I4EybfFZY{ zr#U@Y0_$l)`xLJ&cl&<+c7$@XRP=rtT!#oJx&L?bH{3M>I5!=)Hg2PG?nqDz)@fAE z32;>2?=~@b=GhpuC1l$Y-gXXAEO8jrxfp*ry_N(yv^r54}P;P{RP**_Qb!@nB3;imjAtYIm+&X4L?ZO~HMg0$w1 z+dHr$VMstzNo!yYFS*T)t2-XvG>m#WW@4kyNW2v~2fvD6hht$4=o0a}FZ6en0XE>!KO^ zQ8#uMPL^%OSB2~GUH=7mGi^M^`}9LH|1|A`2N1UXZ0*%l(&2%EGUt`p#82+*eh4L` zL#d7hVVlAx;DfpwwJevn6m>+H+~@?k+NlX=sq1h8JS>RE5a!__JrLlZ&3&@a$2$jG z!;0}oxw$hJ5w>ySy47LnWHF5 z?3#h5gS@_m=3GA(Evs4CQ~e3l*h;mzgUfpf*n|D4sOMwJ{Ka^xT_ho$1SeL&Qqqnz z3|#%9ar<-w9-N+xCIOGfHf<)HsHQDjxx9(zq5)vWDGOZoi^@#ykgY+A4y4g|ClmApz@r}pR&6^S1% z^EsZOVn0RTos^5a1T9q4a^d47!F!tLJkRAblkn>`U{(+*vynKdvvt z4-NfrzcC;8CJn=baU;+?fpD&&!8enDoy7O1ioe?^-s5nt8^-BSzIXWz1TvSqaK0Pg z*-nIYHou=T>}3hke0N%#ZLIO!PUYN^K%kCUT0vtrSex{prZ2Qj zHG3+u+t&ta6aCF~CbUtl(T0y`1VX>j*Vgu721BHR)%#-k@r1&zZm z;uqtC4;fZJpG(=`(H4rzme8%X8YsmR5VV_U-;;fQ=`g|K~sD&%b;?!DZzNC^Nxj1ozgP zRMa;KYN=}$0(-;;f|^CjdYA_2pt2r8Rb8~4ikh%4T1H?mK1yYM2nF+x7+B}C6wG6p zeGo(H_G9QQD(&hcsF-*FTSxA~r}WW~$9PXP#a4S+=UAlT8h^k$n4i6RZ zWdL-eBJYCWK*HQF8w0844@VT^`wUik9a;Gb*h&Cl)b(T=q!oNdNLyp5{-Wh3XXUBr z@pM*d>fV*i?a5r1#!6G_T8f+Cwkr56o6zsYV~YvpN>-MYtSCn}0+Sa5Qwi;{oS(=t zqYjoSRPghtXjV?bPy6QM*YCZH^S55bx{FsZWX=)USYe&Pnnm}7i#k-MUkPCP_!L~macx@4; zPO8UKRFRHU>@Gg(oF<`-eH^-W>yLlCI)}=a5Z*S5ATCEU;oQ8Q=h?z@?%*@oAeBB5 zEd;Vvik-%RbpwIDp2omNLV7b*^%g3`Ej-6ss@`o>_NEZjTL|49gzMHqD)a(8I5`8C zw;h3(d%Wq_?Bo;yXV(bWI|Sp@Ql9VDRFkz0=SIMy(Yj9ZJZD)>4+oAH0H;Rb!Kqm& z>z#`MT?gT%)id$kPX0!_rqQUNs^3iYzMYUGOtHNj|2nuCU1R#=8T$-4IA&1&Cek1Z z!;(df_`@gXFne+ZT-$}9okM##yCG``!#-+Fa19xN)v!{x*AV95Y9 zuNaK`Ylow%=RiC~buMM@Nf7l*&&Su>m*IX54U(z6hpD`W3cf$%2;lJzuudRs$MfAQ z<2gt3ev3IRA+YngUw6J+3JmMOcQ1$gD>#hG8QFYal@}m|^YPps%Mx#MX*aj$_l>T$ zspvSqb1_ucF;dQQYtv{Xpe->h?fH&J^1X}VwG$fgD83PI29Cx=ryN8o0gF40K)w&& zKAG_G>tu>t-5y?oGeNCB(1flHys4a5L^sv5AT2Qa+k(n8?;Rkh3)E_lR#Z37tNzpe zgnwWiCt2F!x1RI^Pn zo9J$C_mG+vm<3vb*QTg#^J;GvnB^unL1%=vqQj+}1!tqG33Ekx`}k_atX@xss(kYK zU-uca=ar2FHY#>WUEf0KmYXQsb_->kmJrq@8w6^qYtBnu3)Dp`&Y@`KSrjfmg~Da0 z3G0(6GV1ymim0p$7bv*w2y$odNA9eBoF1SUIDo;mhcKx6D27cwjK;D(xG`)W{xWnc z{@iadjwX&qA^$K@?x_eOXcY(+LV$*v=y1ZB#k4z9Ne9zfma+~G=xX*2mVzdjLjri5 zARVlrGD4bAm%28P4k5@1X2CjyO8c?m4&uIkYTu@jFwnOTjzx~f*R14_N^qu7@hazW zJRz&-aKhReJ)V`>RJ5o+b}Rv`i11`8Y~wa3oICN@4urEt>B?dyoW^x>jb{|Ld%lkjZ27>piCwRocu&D%=RLO@&A z@wly(u&gcxHWAcYt6*J2=&$4a8kP-&@jAj;=`7cdrE$RPuIBQ3DrxR#T}Obg<~cU; z7%FhJmnXMj$7#3x0o`0K3KSb*UIF z=ZauCL!&@_ug@rAoQBBhDL6c-3?4Kzen!xQMh`<7jiui%6f<|8B+e`TIiz8S*vkESJrfu!1ww)Ept!Vge7}2K` ze_AsUmL>UkxL^n#%^!rO1p{$^RSEV@n}!L6WtdY^j$88=;-9P50<-7Bsw5=ic`u_0 z+>r!r1&y^4{H@CQ+bi$(aL!9X58_nsiTXY(K}knWJMtY$(>&KJH-O?OSgf&pzX;ow z2rfqo!c@@hIgOTrZs0tj9X<^$QB=@T$|N?4+h|l$U8_wR(G7SI+kn@@DpBLy12JvY zolNb+a$O6$t$|`(_E7gL&Z}KnL14BBZY`?~THUJT&gTAda~mZsc<;kLn9sAsiHD~X8#v7pSN_Gg=l)_N*sxyZTwap0IOm8j-z(cNmJ_RuydZYgiy-U9W; zCy;*f`CtAi-F|zK2`Ez_nIOIOmYFZvbOR*{C|fTzeHF#)uCQFf@YPh)E6$)~<#_|@ zqUEPedv*T8BPI$wkD$(*PbJ-8gLG~Km38AG44Ju)8;6HIkI#nh zz~2U~#51|YWsJdyXeVTtpDP_|J%@Rt*EM+WkhZ4G` zU7AIi%0dFEunBIP+9r&puFd)`hz##-1HEbQCj0|?z@OWqeLADmqc_fk7UPFhR$|Gl zL_4rTm10R?#YT0lpfXF$IJCr4Nva)NZQ260>DonzP=yQB)=XBY8LX5uxKDSUOOfJP zJWp2|0NuFUXBOejV~6nkc?7nG+brXJDfcVkdJPUTkr1CW6F=3=z#nIh#jjT_#3u)K z;@8*C;qx#5h|8~kf~_a6Vd|P)h|L)XNB>@UipnrDBm-aW=J6}2m<8$;gz0jEQ7-UR zRM!M`%W5j=jYDAFGYI!zUx5KdWAL;?GL>{199@!4j`C+5qEJ^e0eEQvEL*5x*HJ}p z*lertmN`0s_3-@@ahUK^BNnu{{||*^)zHw3fu(QGQv^w8@Zm!+q#+xcoS9S z8ctX79D?Q+E^nb4-dKWvojZxjqDfS?zBE4C!^Jt2z>a~lM?4CvZ5Rh*O%hK+#zV1Js6fbxqMDE2#&7cxtH)+tOn*U0;b70<9(0fv~oUwZ_W$U zBlsIjH4o&wGLY|30gbj^Tvoh8AH6qR-<8K`02+0%lDCJDw!|w4QtoQLD{*pRPeXHr z3`kpBvo(rFVFdRfoLj=D*a{W2HG=PEG)p9}6$ zy?r}U4QCOwiq58rHsF>DRw9xtvyXzuOq;Z_ju}wfg2+r8wbn~bQ#ot!4BtR1@&GDw z9`7lI9h7d;fV#UyBFixqtGo*Eepmzkn7jrbr7pt^j}nC2b)})!$w1izcc~qjMg5?? zrF`AGz}u*3DQvm2A5R5xThV^DE?PD~KhEuK5}gp{>N9PE#}sg;wrWx=1eC;LU{uZdEri)gcL$L~>U9jvF z@)oO|`Xq+VBdix4Hvwk3a}Qx?;{nsZdPqGXUAxx=mkq7ihXK=eWAOBSC>g&Oi$`w5 z>%(^8UxoYeRsXd(oiG6-J$qAKrxT=ERKeY;io2UW)xo}9sHg>CxvjgJpfa^VOHtcg z;9RdrZT@0BpUsLm3x3`m5kVC(gev<~`(pfE zkWMAA6?CS))l$(aRMah0(HaQEsu8;68rM|q+G@L2V2|>QE67Y;x^f+$2`iwXC2UrxOU%ad&+^zTQ3ue?GGwfBfJE z{`{9;;PWqjffqmi3^Uf9!GTo^fL&DnvkHI(gwZmBY=M*{?~9PMuA%xQ5V3X;T6Xlq zy${wRd%!3>?U)98jlrzqnu6!t;;?!p@8QNWST>DBGojkFN?nk~plKx`N!VIB-$F=3 z(^XQ-1nwq&=e2w;%cQ6UX2N?JLA{8_tP$8*1a&i(H-Wi=+g7oxr~0Piy|-@#ZtOS% zKi6ou+EZ0KhQP%&9FFdhXiIP>$0lOMq@g&us2FD!m*Q}J5$Z<`LdS%3f;J8ggpI4K zh5}6{s0oPhLY$n--*6>=o4JJaBK{6@D`~{=`fJAG^0aYqcFD%m?J{6zpGFYJ!re0h zufMPk`1%+geX#*8f8C4ACzqi^;!s|rE5gIl(5q(;Tw6f!D`0OC4WRk_zDxOAF68e! zo!2Piv#8|nr-V4exxR$Ya^zfsnZNl!ts|6&^0ytxWsTt0m(wnKFF2n?gHDEBCf|cj zd@q!kCy8Kg1$9e|yf_4MEUy#K{Rrz8&Rb&$=ZG4rYr;E>?_?;iLq%;3=Xa0dd^q<{ zY{Wf6`u(tpnC;vb3HFKbbc!duQVA`>)kI(u&IGX;_gSuQ<>C&|2v%x?w!v4?-I^*n z$>RVrxwzH+%1bI+!DL+a;6A>g*~ZnaU@@-%s_TGcxO>MT%q0e8E?saXcmn4# zumwlk&q05Oq42QFG9$XlmF-RiZTdUQ%}o&d5W4DWC4hSi-s*NGB`tMLNdJ5LvvF@f zhO-U!w%{>A+_p_gP7K)PN|zhltH*b|US(_b_~i5C^S{rhbnA`oWji!r$IB?CsurkA z)m}|iZCv0~)>73aQr2rOVZ`c-7*17PwEPT;mYt!hK8^fECy~407=e8l`JCp@KZ>EV z_Y>6nF}QXQa_jbDsHTMWpc(rxh_KF|v>TJlw&G~f9(+-J6n`K3Jl^j-53_^vk?xj` z5Z_J+A#kO7)s-vMbmdYscn>I3nH0C&-vLy_K>;}k4kEw}m~EP`{fu|2m5#8c{rtF2=&guU>XJcBZ~1424)i`O99;dc?m(bl5kwX{Z??g zie>F&LY()tl=rooYM-FSavmpeudc*hp7)y z2()vGf-<~0y2ioLISy^@qtMPi63)u{pjD)?KM5Sj)24fA=ep)$e<>3;k@2XPYR^$D|T4Zhi3&C8n*~*0;J?fn+A2>^hb8WfT$6MXN4QOQ4x_W*}2AnP4155C;;pa)k#G(o)hP zW~!+0E(X9-*(SKmH;db{5lHnM8caA-c}raf2j&>{9_Y(`)!r>|6V`!2oe;qaX@E}- z)`#cetModwWKf-D5ZEcK{F2ojh83`a&mMzXeX-*Rg%}of=TbDe#=;h`6||Pjic>=h zc4S3u+?ibN%z47vDrME1r7QPU`o{sBR|T)}vh#Reb@!S?#Ws%n*07Rp;PH#+Q8CYk zWo`pn>bUP5?l*fP!9*C<5`MD@%lW*QdaCOMHU(>0lt(p5P#?(0pYCi&Vy80LIpo0J zz9Sr!=){TYDx@QB9^3#NnTkhi3aOlJL21oP%cy)Oz_OAGlBIb;38y^oDlQYq)}><& zsIh7imF75D=2fy(m|_NwSj2sojpsd=bDqa6u0YE|f?rUV`d?O#?{=@nukXBt?4+J( z^Ryq;aWqv@JQbGGsS?6e+m5c0a56cxsl2(}necX|LUVG8!Lw~5;mY5B#nw&u*PT7M zH3p8k`HX7Haev-;%<41*S9UGO?|yp{zxm~Hynbv6&XkSBx9i5? z(bZ}^dSwinF7p|lo{yFdb+9bw_n$Wk%`+dyVwMHb)_s$)y$a#pI#2w%Cz>j`NBK*~Fw6>( zUqtt;!D!be2o4S!(vzy06}=0UwUcugm%|9@a4N8HIN68O(1=Ex=c16*aVQR7IEjC~ zd<_3qHOvO)DSYOW^^62-0&*gs)6_|5sj9+1sI)(;D8x_eN8=xh%JKEgQTTq`cr;HW z_!?%xGJiT+8u{Ba&`_I6RXc^>X-pNYr98(lf|9?7bugc~QO*2)m0ng*&fz+>H>=xT zcI)q{_yrkf0<=bOQ|7QF?k7m6@b}fbApem#x(Rpf^Y7_Dc}#EzxckM! z%Owgqyoc4kL-0;yE&i6d4(}!}MV(g(g5~CR%`$_+NC~?O%7nD~J3of6>E}#9`}*}T zZPcC~>V74+HkGv5-=>-kh_$bQu^>!EE)ch>X!VgcS;h!*GYXdW@qUsL^vUNx;iq)l z?F)(kAGzfQN;Y0MkS-Cl*Iz<0m31+}ZB+F#s_EtDZNMh13l^V2?z|IJ)W?xKSKYOa zV2HYDH5@R?dT`wys_H!$LRb%&wgW?|)n2^|119f4p9$MBV8SlsR&2+NqMbND^Z@=k zWG{Z(V=<0Lk3x}47NT5I5x|Nih^ja&#Mb5<5~+ouZ_1n1+^#3^X$ zxPZW2O5iSLMJbid3RvA`Qdq&p5m3pjgw>v{_G_)v(1NUhOD{2<2 z`ebV$5M?^cRwd8MH=E~>YrOk3SgEwF1Gubq&i*`SF3&TR=a-v&1dFm*l@Z93xqTvm zGL!c(nE;%{ZFStgx)Rn|TwXjL4>#1~ce}P=&WaN_@X{;T_xd#~yKx;8cHcy0?H2sw z0>QkH z_cfQtEaZL-JjWs~FC?tF&N`RCSE3KKAvf|k9`obEnRs~lBL4X9dzdw2HrfZL!O0;C zZQF+7+2?}s?6ZM*?q_~z^OPT+dnyoiZKL5& zzxxC}`zp?tb6vTf71xdCa~?e#mcn{GDx8H!xwFus_T*7it|R%|RMHR|H3ul=vD25s zI*w|$gfJ|xhegrZz4`2i@;C3x^Y*Vtiw1A$#pAp4UC{5?i6xucGx(e3aGK6z9%m2J z2yUkTJ|UdSZR%|x1+9c7dOs3al*(4_?noJeHE5QCj--J>1uf&ycyy$qbzJf++~avZ zikyH&o`VU-RQNc=!_8go&zXd-x;F{Xgs9q{Ob6$fG}r$}w<()Kwnb|T#@a5IxB|j7uWnaV;e@Zc zS1Bc^T-%zOfjV@I)Hb(!aX$~8+pEWuKGIJ<|9L+pn{O-{)!JRFc*7Or`Zn#=YpOv~(qE(krwBn+Hb>Y%;1obHtEIf(a`5LeK1Yxa=Wyg^>?Soyz35-P6N3oq zK{KeVYxZE^6fUy})&nN*M8Amy_So$xDcXvSgLmTRL-ye7?i=x|q*`qBD?mS&PKfqM zM_3@CsuDnD%nB&TxUdxkPBko7IhD07&`beiTy70E3k&T*U~_+}>QKTyG)S=SL?zB; zDr>DS| zahsA}C^EQ@fWYJXa=shS-=FjS2=V@eV-c@a#&cD$GM~x&n9Ad(^19Pls;F{nc&=)y z+i6s0a~9#j>P`6F^hLNat^sGJEyNf52)ToMac{#kSXL0yE5;DO1pH#E>v@$_%33CL z8>ogC5Zaob&3OS=%Y?91%Y?IaF%@4ek8PmwFqdFnO3=*XbdJEzdzr_58pqkn9IEQs z{62FCvc_p>s;$EPjZ1O=m0S4im3Odh>p_g2G!y-Yj6_z~p~&ns5IMbzFraiQX0F+d z+wZ)CKmOt!{ASmB{IHPFtKsi3iC`VeXC;q=x<<+SpgzhKgzzvv4*|J^`wZjnSH|CO zD1W~^f?2^{G5VJzh=C|jfX+X>z8IBGoe-trP&GWNSE>Q2BDCe!_Tqc)V}_2Tf;I_D2w6pPx4OF( zET%*zUQ*Cf&II%0KGA}+pYn4nNQ~>y(89 zJ|po-NFDx`vKH^AE<&A0F(RMKhO0{#8WCzU?rFfPU@mWgRZv#Ym|W8WcWb1!Kx<2I z!tH{t0XCtoOk)DGAl<5@wavCa&#Qen@9s`Dt+s5QSCFO=@TB|Ilh1$tPsx@S@=ROx zrfVo(e-%Xpv=p^m;e}Mx!&YCxu$6@M@(ajca@GWuspJaI^G{$%quQ#au8(4{`cuz7 zfI;>9FhHQ5xd#KL?=pl*(Oq#;8eSHnleMNB2Fe?1$052YRdpSlk1XM zF=ui=lkJS_R0wEmXP!^WO|aHr3_ZABK}y{@?MDa?<#YtEtAM6TZa1SX@tE=4e>AsO za{Dx@J|)_iK)_60L_n^g65d95FQIa*M>Ex^bpiKV!08eKUy!XG14{#eO6A)!hoz2S zo-bug!$8V;0ry)tg{qlTs`i$8f_*mCbRFj#C$LO}rIzztw#<==<~g~KwSh{S=dm{M zT6KILGkM)vyzkk3mUWYGzjgwC+O!zoUpj>EfBriD@!MbE55N5-zWB{A@Vl?Rz(4-< zIsW;^4gA}|wfN@>s?YiSzP0>bm4s`FKuq|J_G!dsbXn7nQ6~@Se0j6aRxH5G&S)D{2e>+H&IZS zep|IYYf2~^Fe}xkyesmmXw)_>3)Jd970vfUQQUe@6s;Y_ZCr2Ko)tuv%==55151Yn z{Hpy}tnnU*fzJFN*d@ZvC7$3)CWLIz6+}JxE^F9G1&Nt{&@4*dX}V5%Dkrxsh)h6# zoW#WD{-%N^c;)7{Wg%0ys|+e|o15EH{i&& zv8iGKnGk*qdb!LEs9FBUeN14P;BU?$wcY(Ys_!R}-A_LMbv`3EUQH?8dh-wJwpC0; zJ#wQ0%dTShnrjBsg#>lJT;MCtBbRDAZ_z0fDY$I$8RRb@wCA26pbuln>_e??@Pimw zyO(7TnL;&Py$b^g?159MrYCQwx>nhN{u8#N-?$wZIC>{a3pQg@zpZ$0;7%3-0Zo_( zP(8P*Yg@FpBEbo2s*XTbLV;8XK`erG5U=6y%_%`1MrdnlR5w+4FqOS8RY$OY3Uat# zL&#vf7C#=}bYO*=F4y=%PFc}QX-he&VpdyXBEcO$8O=&rYUZhwOnW6OXLapLXXTtq zP^S~P*{rmc+gZw5UA~k`lJiyto*8&^ALW2nFq+i025cBa$f%!g3HOzH)aXm22#gWj zK8)88yhaOH+^>?b);PxHyx%I`?_>h2k<&WvGkq*jPorVhM6^sJU^#CkK&@)GRq$FP zK|N~%nx|G!JrlfyvesFq66`ZnsGJE;Yb}qfCFooCvCQQBY~IHVF4ywj>RGCJovA#Z z+S+SqOsLGLM@w}b9!{BoA8TgfyLpZHX88hqw`M+mTG4=}In}UCpMa+6gz!W@uL|CC z8NZj@*5#b$@mxcA&f$DMdA!DOK93=s*9cU3{O-ee4Rw_o%=JYLunyt+0bJjM$Mxg6 z25`9#fvooC-rQGF+8RPKhw5BS7C8jC>FdmrP1sW1LLL*vCMYRR29H&)c=Z;Lp{CTe zNi3Sib6Jp9mn#+|a33k=Xx^{h1+_iLX*=(OfNn`@z#qBooPROKId(;oeG)t!VhFip z)BY@Xwgz#L8{3=jr~x&>tTt(*pp|UIxULnkZF6UvHfDjB#Y6eIX@J_?-^v{JIG1-T zz)iYOO*@;eS8{VJnp@*VyL(0<(k+hv%Wk+HFdARP&c`2;SL0^Xe2jJ}Mz~!LRdhBD z3BuB&mw~2fKlWe|prw`xW)loXDBH%8?rQor8)YomwzfAbFidW3AHP2QuDuCnttYSv zf14Y;_1prr<~7ymSYAE8^LP?8_T=+l@uMKKl1(q%8L^&_UL!ZS5};fqsIL;#7g4n0 zf&q0AOW~5U$X##}Lsx3i@i3J&fjx_`p1Bu;2<-lZ^}rds2bDoe6VbMC%UL8ucEk#a9%dIWdEJZB{2L(_)v%(X& z1!_TD#TGG6gB~-D*PNAaMRRC98r~27uP(7w| zyK##P7_7{Da4L7K`sHebr5@aW0QZr~))0wm(;mcig9sBvCl_$taNbh^ivrb@kfUlb z)wia+XSMH6;J%Z256VYf%jIcQ%u{%8)if?nnKAN-R>!Qpj^Cxrn}FKChF@`w^V;xy?k=^S=6XyIkD^ z_`KwikbyIp%Zkp{z!%-Qe>V3~wmEgD%VyzoH_a1W2w+XKZDXCAl%7(~YJ)c2tEk}B z%pv!-x>qTZJDKM)#+snb=b%V!wNXd$_mAasl(8nyiU6)aGQBIE>Tx%%26w_rvCyv< zvh3pF;~WDQw*aI5iL#%&DAx`7WFGY3{E?CsNWLa*3-Ano8R{h=31DhKqM30-Pf;!aW;D!Yc4t z>;n8Xa~(cSUyY@HWk_q61s6x%pRGHUg1@Awy*zDg$EJ%^E2vFhX>Wm+O4_tns}0&V zRaPksY@ZgWwa&}8r`n^R1dToU{MY`B+;C%K@w#gm z!7_}%&L^M?2%omjFnHD>3Y7gA zP_qw%IaRy$z-k4T5!mXkHC0`$(9S#8zXQcRu<$rlP%Tl7X!sAswgzJ3im)T%# zgL42YzTlwFgu4yeQrH1hCjzq6v~i0QvBOCq5sAS;KshTF`j zK}!NF^>iMiU?mOCAebm&Mh1`R$cjFP6}{TCdvjYKf}tOm`x4NDxqmMARUp(*f=%kN zALo_cbO4QqTy9sEtRY-Cgy$jjtYbK@9MlyAPX+f=+xG;*rHcEFrUF*L)?_L|MM=-1 zx~=7X&ft9gOjst2NAmLT8+JP9&92Js%10ZcCJ zzC3p!=hcUKF!vRt`|;e}X2RNu`_j;9QM7&cT26Uy#9*iuygP{g`UH70#M^5Zr2uHZE(a zU!$yT0QMDly;|WcwM_M_WF?P5t&C)D1iQPyE`=?(c&ozp3F1Cf&@MhvaCZ(xjzcWg zdFSE%=sNs8bsfISSdU|o)fnP55I*hF;o`1q9&rs!yrp$H#N|KZ3|wfiIA2-sJDL!CJ@gnoruJpM3uNd`dRHkY~DU zt-p%l>#m@HaL!*YHGR>z!1I@$L*Y`cTXYtAgmwNR!rHjM3F_HLkV|L}tW#U{0Zs+% zJyg}tqdy_tf9g&QVCg%4EBcQopvO~LkKc)YW4EGL`DXMYtb3JhL7(Cc7@oTp3;S=x zm44gt+X370_pWR3PQqj?_UwZmPMr|$or6H7APwt5;M&x40Kx3%+tEag2N2X+mMRVo zAYeIFB)J03LI`K0pb6t(scFJl?rpii1zy{UA3_5_?bnZ0wz`KY5sRY5 zeSN6JsdREYvr!i?0GA`n@Ed};MX*j^#L8Ljal%K@yXwZpirbo`c1o(#q)BL@nzqDK zZ7O++x^tzmQWv~42@HbTD&^gg`==8SnS{06v})VVBoyQp&*65t$$RqH-kcY}2hN67 z%e`3C*S8O^-Idqq%Vo8H_aIc%hg&I52XnjJ#Of!kv5`j!q6A6_uQQz297Tv1bN@=- z%TxlXir;Mq;5ReXVX$GCuMM-$DF-LpeW!5FWy*iN@wJdE8!aI6 z#BIGe?ZatbD&f9-207eE9*J&su&9}#6VI7Lqa~B~sVrfFw4kj1@@YI)-KS(srSSV{ z+-DhCisqJzRy=}=$y`TcMZcjGvs~Cz-9|YJ;>M-TqTg9AZ^bgC%))n6(yxY�I|s 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 G82*2e-3&hf literal 0 HcmV?d00001 diff --git a/foleycrafter/data/__init__.py b/foleycrafter/data/__init__.py new file mode 100644 index 0000000..be53682 --- /dev/null +++ b/foleycrafter/data/__init__.py @@ -0,0 +1,27 @@ +from .dataset import AudioSetStrong, CPU_Unpickler, VGGSound, dynamic_range_compression, get_mel, zero_rank_print +from .video_transforms import ( + CenterCropVideo, + KineticsRandomCropResizeVideo, + NormalizeVideo, + RandomHorizontalFlipVideo, + TemporalRandomCrop, + ToTensorVideo, + UCFCenterCropVideo, +) + + +__all__ = [ + "zero_rank_print", + "get_mel", + "dynamic_range_compression", + "CPU_Unpickler", + "AudioSetStrong", + "VGGSound", + "UCFCenterCropVideo", + "KineticsRandomCropResizeVideo", + "CenterCropVideo", + "NormalizeVideo", + "ToTensorVideo", + "RandomHorizontalFlipVideo", + "TemporalRandomCrop", +] diff --git a/foleycrafter/data/dataset.py b/foleycrafter/data/dataset.py new file mode 100644 index 0000000..14bea8c --- /dev/null +++ b/foleycrafter/data/dataset.py @@ -0,0 +1,155 @@ +import glob +import io +import pickle +import random + +import numpy as np +import torch +import torch.distributed as dist +import torchaudio +import torchvision.transforms as transforms +from torch.utils.data.dataset import Dataset + + +def zero_rank_print(s): + if (not dist.is_initialized()) or (dist.is_initialized() and dist.get_rank() == 0): + print("### " + s, flush=True) + + +@torch.no_grad() +def get_mel(audio_data, audio_cfg): + # mel shape: (n_mels, T) + mel = torchaudio.transforms.MelSpectrogram( + sample_rate=audio_cfg["sample_rate"], + n_fft=audio_cfg["window_size"], + win_length=audio_cfg["window_size"], + hop_length=audio_cfg["hop_size"], + center=True, + pad_mode="reflect", + power=2.0, + norm=None, + onesided=True, + n_mels=64, + f_min=audio_cfg["fmin"], + f_max=audio_cfg["fmax"], + ).to(audio_data.device) + mel = mel(audio_data) + # we use log mel spectrogram as input + mel = torchaudio.transforms.AmplitudeToDB(top_db=None)(mel) + return mel # (T, n_mels) + + +def dynamic_range_compression(x, normalize_fun=torch.log, C=1, clip_val=1e-5): + """ + PARAMS + ------ + C: compression factor + """ + return normalize_fun(torch.clamp(x, min=clip_val) * C) + + +class CPU_Unpickler(pickle.Unpickler): + def find_class(self, module, name): + if module == "torch.storage" and name == "_load_from_bytes": + return lambda b: torch.load(io.BytesIO(b), map_location="cpu") + else: + return super().find_class(module, name) + + +class AudioSetStrong(Dataset): + # read feature and audio + def __init__( + self, + data_path="data/AudioSetStrong/train/feature", + video_path="data/AudioSetStrong/train/video", + ): + super().__init__() + self.data_path = data_path + self.data_list = list(self.data_path) + self.length = len(self.data_list) + # get video feature + self.video_path = video_path + vision_transform_list = [ + transforms.Resize((128, 128)), + transforms.CenterCrop((112, 112)), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] + self.video_transform = transforms.Compose(vision_transform_list) + + def get_batch(self, idx): + embeds = self.data_list[idx] + mel = embeds["mel"] + save_bsz = mel.shape[0] + audio_info = embeds["audio_info"] + text_embeds = embeds["text_embeds"] + + # audio_info['label_list'] = np.array(audio_info['label_list']) + audio_info_array = np.array(audio_info["label_list"]) + prompts = [] + for i in range(save_bsz): + prompts.append(", ".join(audio_info_array[i, : audio_info["event_num"][i]].tolist())) + + return mel, audio_info, text_embeds, prompts + + def __len__(self): + return self.length + + def __getitem__(self, idx): + while True: + try: + mel, audio_info, text_embeds, prompts, videos = self.get_batch(idx) + break + except Exception: + zero_rank_print(" >>> load error <<<") + idx = random.randint(0, self.length - 1) + sample = { + "mel": mel, + "audio_info": audio_info, + "text_embeds": text_embeds, + "prompts": prompts, + "videos": videos, + } + return sample + + +class VGGSound(Dataset): + # read feature and audio + def __init__( + self, + data_path="data/VGGSound/train/video", + visual_data_path="data/VGGSound/train/feature", + ): + super().__init__() + self.data_path = data_path + self.visual_data_path = visual_data_path + self.embeds_list = glob.glob(f"{self.data_path}/*.pt") + self.visual_list = glob.glob(f"{self.visual_data_path}/*.pt") + self.length = len(self.embeds_list) + + def get_batch(self, idx): + embeds = torch.load(self.embeds_list[idx], map_location="cpu") + visual_embeds = torch.load(self.visual_list[idx], map_location="cpu") + + # audio_embeds = embeds['audio_embeds'] + visual_embeds = visual_embeds["visual_embeds"] + # video_name = embeds["video_name"] + text = embeds["text"] + mel = embeds["mel"] + + audio = mel + + return visual_embeds, audio, text + + def __len__(self): + return self.length + + def __getitem__(self, idx): + while True: + try: + visual_embeds, audio, text = self.get_batch(idx) + break + except Exception: + zero_rank_print("load error") + idx = random.randint(0, self.length - 1) + sample = {"visual_embeds": visual_embeds, "audio": audio, "text": text} + return sample diff --git a/foleycrafter/data/video_transforms.py b/foleycrafter/data/video_transforms.py new file mode 100644 index 0000000..6104ec5 --- /dev/null +++ b/foleycrafter/data/video_transforms.py @@ -0,0 +1,404 @@ +import numbers +import random + +import torch + + +def _is_tensor_video_clip(clip): + if not torch.is_tensor(clip): + raise TypeError("clip should be Tensor. Got %s" % type(clip)) + + if not clip.ndimension() == 4: + raise ValueError("clip should be 4D. Got %dD" % clip.dim()) + + return True + + +def crop(clip, i, j, h, w): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (T, C, H, W) + """ + if len(clip.size()) != 4: + raise ValueError("clip should be a 4D tensor") + return clip[..., i : i + h, j : j + w] + + +def resize(clip, target_size, interpolation_mode): + if len(target_size) != 2: + raise ValueError(f"target size should be tuple (height, width), instead got {target_size}") + return torch.nn.functional.interpolate(clip, size=target_size, mode=interpolation_mode, align_corners=False) + + +def resize_scale(clip, target_size, interpolation_mode): + if len(target_size) != 2: + raise ValueError(f"target size should be tuple (height, width), instead got {target_size}") + _, _, H, W = clip.shape + scale_ = target_size[0] / min(H, W) + return torch.nn.functional.interpolate(clip, scale_factor=scale_, mode=interpolation_mode, align_corners=False) + + +def resized_crop(clip, i, j, h, w, size, interpolation_mode="bilinear"): + """ + Do spatial cropping and resizing to the video clip + Args: + clip (torch.tensor): Video clip to be cropped. Size is (T, C, H, W) + i (int): i in (i,j) i.e coordinates of the upper left corner. + j (int): j in (i,j) i.e coordinates of the upper left corner. + h (int): Height of the cropped region. + w (int): Width of the cropped region. + size (tuple(int, int)): height and width of resized clip + Returns: + clip (torch.tensor): Resized and cropped clip. Size is (T, C, H, W) + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + clip = crop(clip, i, j, h, w) + clip = resize(clip, size, interpolation_mode) + return clip + + +def center_crop(clip, crop_size): + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + h, w = clip.size(-2), clip.size(-1) + th, tw = crop_size + if h < th or w < tw: + raise ValueError("height and width must be no smaller than crop_size") + + i = int(round((h - th) / 2.0)) + j = int(round((w - tw) / 2.0)) + return crop(clip, i, j, th, tw) + + +def random_shift_crop(clip): + """ + Slide along the long edge, with the short edge as crop size + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + h, w = clip.size(-2), clip.size(-1) + + if h <= w: + # long_edge = w + short_edge = h + else: + # long_edge = h + short_edge = w + + th, tw = short_edge, short_edge + + i = torch.randint(0, h - th + 1, size=(1,)).item() + j = torch.randint(0, w - tw + 1, size=(1,)).item() + return crop(clip, i, j, th, tw) + + +def to_tensor(clip): + """ + Convert tensor data type from uint8 to float, divide value by 255.0 and + permute the dimensions of clip tensor + Args: + clip (torch.tensor, dtype=torch.uint8): Size is (T, C, H, W) + Return: + clip (torch.tensor, dtype=torch.float): Size is (T, C, H, W) + """ + _is_tensor_video_clip(clip) + if not clip.dtype == torch.uint8: + raise TypeError("clip tensor should have data type uint8. Got %s" % str(clip.dtype)) + # return clip.float().permute(3, 0, 1, 2) / 255.0 + return clip.float() / 255.0 + + +def normalize(clip, mean, std, inplace=False): + """ + Args: + clip (torch.tensor): Video clip to be normalized. Size is (T, C, H, W) + mean (tuple): pixel RGB mean. Size is (3) + std (tuple): pixel standard deviation. Size is (3) + Returns: + normalized clip (torch.tensor): Size is (T, C, H, W) + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + if not inplace: + clip = clip.clone() + mean = torch.as_tensor(mean, dtype=clip.dtype, device=clip.device) + print(mean) + std = torch.as_tensor(std, dtype=clip.dtype, device=clip.device) + clip.sub_(mean[:, None, None, None]).div_(std[:, None, None, None]) + return clip + + +def hflip(clip): + """ + Args: + clip (torch.tensor): Video clip to be normalized. Size is (T, C, H, W) + Returns: + flipped clip (torch.tensor): Size is (T, C, H, W) + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + return clip.flip(-1) + + +class RandomCropVideo: + def __init__(self, size): + if isinstance(size, numbers.Number): + self.size = (int(size), int(size)) + else: + self.size = size + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (T, C, H, W) + Returns: + torch.tensor: randomly cropped video clip. + size is (T, C, OH, OW) + """ + i, j, h, w = self.get_params(clip) + return crop(clip, i, j, h, w) + + def get_params(self, clip): + h, w = clip.shape[-2:] + th, tw = self.size + + if h < th or w < tw: + raise ValueError(f"Required crop size {(th, tw)} is larger than input image size {(h, w)}") + + if w == tw and h == th: + return 0, 0, h, w + + i = torch.randint(0, h - th + 1, size=(1,)).item() + j = torch.randint(0, w - tw + 1, size=(1,)).item() + + return i, j, th, tw + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(size={self.size})" + + +class UCFCenterCropVideo: + def __init__( + self, + size, + interpolation_mode="bilinear", + ): + if isinstance(size, tuple): + if len(size) != 2: + raise ValueError(f"size should be tuple (height, width), instead got {size}") + self.size = size + else: + self.size = (size, size) + + self.interpolation_mode = interpolation_mode + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (T, C, H, W) + Returns: + torch.tensor: scale resized / center cropped video clip. + size is (T, C, crop_size, crop_size) + """ + clip_resize = resize_scale(clip=clip, target_size=self.size, interpolation_mode=self.interpolation_mode) + clip_center_crop = center_crop(clip_resize, self.size) + return clip_center_crop + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(size={self.size}, interpolation_mode={self.interpolation_mode}" + + +class KineticsRandomCropResizeVideo: + """ + Slide along the long edge, with the short edge as crop size. And resie to the desired size. + """ + + def __init__( + self, + size, + interpolation_mode="bilinear", + ): + if isinstance(size, tuple): + if len(size) != 2: + raise ValueError(f"size should be tuple (height, width), instead got {size}") + self.size = size + else: + self.size = (size, size) + + self.interpolation_mode = interpolation_mode + + def __call__(self, clip): + clip_random_crop = random_shift_crop(clip) + clip_resize = resize(clip_random_crop, self.size, self.interpolation_mode) + return clip_resize + + +class CenterCropVideo: + def __init__( + self, + size, + interpolation_mode="bilinear", + ): + if isinstance(size, tuple): + if len(size) != 2: + raise ValueError(f"size should be tuple (height, width), instead got {size}") + self.size = size + else: + self.size = (size, size) + + self.interpolation_mode = interpolation_mode + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (T, C, H, W) + Returns: + torch.tensor: center cropped video clip. + size is (T, C, crop_size, crop_size) + """ + clip_center_crop = center_crop(clip, self.size) + return clip_center_crop + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(size={self.size}, interpolation_mode={self.interpolation_mode}" + + +class NormalizeVideo: + """ + Normalize the video clip by mean subtraction and division by standard deviation + Args: + mean (3-tuple): pixel RGB mean + std (3-tuple): pixel RGB standard deviation + inplace (boolean): whether do in-place normalization + """ + + def __init__(self, mean, std, inplace=False): + self.mean = mean + self.std = std + self.inplace = inplace + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): video clip must be normalized. Size is (C, T, H, W) + """ + return normalize(clip, self.mean, self.std, self.inplace) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(mean={self.mean}, std={self.std}, inplace={self.inplace})" + + +class ToTensorVideo: + """ + Convert tensor data type from uint8 to float, divide value by 255.0 and + permute the dimensions of clip tensor + """ + + def __init__(self): + pass + + def __call__(self, clip): + """ + Args: + clip (torch.tensor, dtype=torch.uint8): Size is (T, C, H, W) + Return: + clip (torch.tensor, dtype=torch.float): Size is (T, C, H, W) + """ + return to_tensor(clip) + + def __repr__(self) -> str: + return self.__class__.__name__ + + +class RandomHorizontalFlipVideo: + """ + Flip the video clip along the horizontal direction with a given probability + Args: + p (float): probability of the clip being flipped. Default value is 0.5 + """ + + def __init__(self, p=0.5): + self.p = p + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Size is (T, C, H, W) + Return: + clip (torch.tensor): Size is (T, C, H, W) + """ + if random.random() < self.p: + clip = hflip(clip) + return clip + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(p={self.p})" + + +# ------------------------------------------------------------ +# --------------------- Sampling --------------------------- +# ------------------------------------------------------------ +class TemporalRandomCrop(object): + """Temporally crop the given frame indices at a random location. + + Args: + size (int): Desired length of frames will be seen in the model. + """ + + def __init__(self, size): + self.size = size + + def __call__(self, total_frames): + rand_end = max(0, total_frames - self.size - 1) + begin_index = random.randint(0, rand_end) + end_index = min(begin_index + self.size, total_frames) + return begin_index, end_index + + +if __name__ == "__main__": + import os + + import numpy as np + import torchvision.io as io + from torchvision import transforms + from torchvision.utils import save_image + + vframes, aframes, info = io.read_video(filename="./v_Archery_g01_c03.avi", pts_unit="sec", output_format="TCHW") + + trans = transforms.Compose( + [ + ToTensorVideo(), + RandomHorizontalFlipVideo(), + UCFCenterCropVideo(512), + # NormalizeVideo(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True), + transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True), + ] + ) + + target_video_len = 32 + frame_interval = 1 + total_frames = len(vframes) + print(total_frames) + + temporal_sample = TemporalRandomCrop(target_video_len * frame_interval) + + # Sampling video frames + start_frame_ind, end_frame_ind = temporal_sample(total_frames) + # print(start_frame_ind) + # print(end_frame_ind) + assert end_frame_ind - start_frame_ind >= target_video_len + frame_indice = np.linspace(start_frame_ind, end_frame_ind - 1, target_video_len, dtype=int) + + select_vframes = vframes[frame_indice] + + select_vframes_trans = trans(select_vframes) + + select_vframes_trans_int = ((select_vframes_trans * 0.5 + 0.5) * 255).to(dtype=torch.uint8) + + io.write_video("./test.avi", select_vframes_trans_int.permute(0, 2, 3, 1), fps=8) + + for i in range(target_video_len): + save_image( + select_vframes_trans[i], os.path.join("./test000", "%04d.png" % i), normalize=True, value_range=(-1, 1) + ) diff --git a/foleycrafter/models/adapters/attention_processor.py b/foleycrafter/models/adapters/attention_processor.py new file mode 100644 index 0000000..de3de05 --- /dev/null +++ b/foleycrafter/models/adapters/attention_processor.py @@ -0,0 +1,655 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +from diffusers.utils import logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class AttnProcessor(nn.Module): + r""" + Default processor for performing attention-related computations. + """ + + def __init__( + self, + hidden_size=None, + cross_attention_dim=None, + ): + super().__init__() + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + ): + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class IPAttnProcessor(nn.Module): + r""" + Attention processor for IP-Adapater. + Args: + hidden_size (`int`): + The hidden size of the attention layer. + cross_attention_dim (`int`): + The number of channels in the `encoder_hidden_states`. + scale (`float`, defaults to 1.0): + the weight scale of image prompt. + num_tokens (`int`, defaults to 4 when do ip_adapter_plus it should be 16): + The context length of the image features. + """ + + def __init__(self, hidden_size, cross_attention_dim=None, scale=1.0, num_tokens=4): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.scale = scale + self.num_tokens = num_tokens + + self.to_k_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + ): + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + else: + # get encoder_hidden_states, ip_hidden_states + end_pos = encoder_hidden_states.shape[1] - self.num_tokens + encoder_hidden_states, ip_hidden_states = ( + encoder_hidden_states[:, :end_pos, :], + encoder_hidden_states[:, end_pos:, :], + ) + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # for ip-adapter + ip_key = self.to_k_ip(ip_hidden_states) + ip_value = self.to_v_ip(ip_hidden_states) + + ip_key = attn.head_to_batch_dim(ip_key) + ip_value = attn.head_to_batch_dim(ip_value) + + ip_attention_probs = attn.get_attention_scores(query, ip_key, None) + self.attn_map = ip_attention_probs + ip_hidden_states = torch.bmm(ip_attention_probs, ip_value) + ip_hidden_states = attn.batch_to_head_dim(ip_hidden_states) + + hidden_states = hidden_states + self.scale * ip_hidden_states + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class AttnProcessor2_0(torch.nn.Module): + r""" + Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). + """ + + def __init__( + self, + hidden_size=None, + cross_attention_dim=None, + ): + super().__init__() + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + ): + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class AttnProcessor2_0WithProjection(torch.nn.Module): + r""" + Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). + """ + + def __init__( + self, + hidden_size=None, + cross_attention_dim=None, + ): + super().__init__() + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + self.before_proj_size = 1024 + self.after_proj_size = 768 + self.visual_proj = nn.Linear(self.before_proj_size, self.after_proj_size) + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + ): + residual = hidden_states + # encoder_hidden_states = self.visual_proj(encoder_hidden_states) + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class IPAttnProcessor2_0(torch.nn.Module): + r""" + Attention processor for IP-Adapater for PyTorch 2.0. + Args: + hidden_size (`int`): + The hidden size of the attention layer. + cross_attention_dim (`int`): + The number of channels in the `encoder_hidden_states`. + scale (`float`, defaults to 1.0): + the weight scale of image prompt. + num_tokens (`int`, defaults to 4 when do ip_adapter_plus it should be 16): + The context length of the image features. + """ + + def __init__(self, hidden_size, cross_attention_dim=None, scale=1.0, num_tokens=4): + super().__init__() + + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.scale = scale + self.num_tokens = num_tokens + + self.to_k_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + ): + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + else: + # get encoder_hidden_states, ip_hidden_states + end_pos = encoder_hidden_states.shape[1] - self.num_tokens + encoder_hidden_states, ip_hidden_states = ( + encoder_hidden_states[:, :end_pos, :], + encoder_hidden_states[:, end_pos:, :], + ) + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # for ip-adapter + ip_key = self.to_k_ip(ip_hidden_states) + ip_value = self.to_v_ip(ip_hidden_states) + + ip_key = ip_key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + ip_value = ip_value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + ip_hidden_states = F.scaled_dot_product_attention( + query, ip_key, ip_value, attn_mask=None, dropout_p=0.0, is_causal=False + ) + with torch.no_grad(): + self.attn_map = query @ ip_key.transpose(-2, -1).softmax(dim=-1) + # print(self.attn_map.shape) + + ip_hidden_states = ip_hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + ip_hidden_states = ip_hidden_states.to(query.dtype) + + hidden_states = hidden_states + self.scale * ip_hidden_states + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +## for controlnet +class CNAttnProcessor: + r""" + Default processor for performing attention-related computations. + """ + + def __init__(self, num_tokens=4): + self.num_tokens = num_tokens + + def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None, temb=None): + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + else: + end_pos = encoder_hidden_states.shape[1] - self.num_tokens + encoder_hidden_states = encoder_hidden_states[:, :end_pos] # only use text + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class CNAttnProcessor2_0: + r""" + Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). + """ + + def __init__(self, num_tokens=4): + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + self.num_tokens = num_tokens + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + ): + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + else: + end_pos = encoder_hidden_states.shape[1] - self.num_tokens + encoder_hidden_states = encoder_hidden_states[:, :end_pos] # only use text + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states diff --git a/foleycrafter/models/adapters/ip_adapter.py b/foleycrafter/models/adapters/ip_adapter.py new file mode 100644 index 0000000..73d18f7 --- /dev/null +++ b/foleycrafter/models/adapters/ip_adapter.py @@ -0,0 +1,189 @@ +import torch +import torch.nn as nn + + +class IPAdapter(torch.nn.Module): + """IP-Adapter""" + + def __init__(self, unet, image_proj_model, adapter_modules, ckpt_path=None): + super().__init__() + self.unet = unet + self.image_proj_model = image_proj_model + self.adapter_modules = adapter_modules + + if ckpt_path is not None: + self.load_from_checkpoint(ckpt_path) + + def forward(self, noisy_latents, timesteps, encoder_hidden_states, image_embeds): + ip_tokens = self.image_proj_model(image_embeds) + encoder_hidden_states = torch.cat([encoder_hidden_states, ip_tokens], dim=1) + # Predict the noise residual + noise_pred = self.unet(noisy_latents, timesteps, encoder_hidden_states).sample + return noise_pred + + def load_from_checkpoint(self, ckpt_path: str): + # Calculate original checksums + orig_ip_proj_sum = torch.sum(torch.stack([torch.sum(p) for p in self.image_proj_model.parameters()])) + orig_adapter_sum = torch.sum(torch.stack([torch.sum(p) for p in self.adapter_modules.parameters()])) + + state_dict = torch.load(ckpt_path, map_location="cpu") + + # Load state dict for image_proj_model and adapter_modules + self.image_proj_model.load_state_dict(state_dict["image_proj"], strict=True) + self.adapter_modules.load_state_dict(state_dict["ip_adapter"], strict=True) + + # Calculate new checksums + new_ip_proj_sum = torch.sum(torch.stack([torch.sum(p) for p in self.image_proj_model.parameters()])) + new_adapter_sum = torch.sum(torch.stack([torch.sum(p) for p in self.adapter_modules.parameters()])) + + # Verify if the weights have changed + assert orig_ip_proj_sum != new_ip_proj_sum, "Weights of image_proj_model did not change!" + assert orig_adapter_sum != new_adapter_sum, "Weights of adapter_modules did not change!" + + print(f"Successfully loaded weights from checkpoint {ckpt_path}") + + +class ImageProjModel(torch.nn.Module): + """Projection Model""" + + def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024, clip_extra_context_tokens=4): + super().__init__() + + self.cross_attention_dim = cross_attention_dim + self.clip_extra_context_tokens = clip_extra_context_tokens + self.proj = torch.nn.Linear(clip_embeddings_dim, self.clip_extra_context_tokens * cross_attention_dim) + self.norm = torch.nn.LayerNorm(cross_attention_dim) + + def forward(self, image_embeds): + embeds = image_embeds + clip_extra_context_tokens = self.proj(embeds).reshape( + -1, self.clip_extra_context_tokens, self.cross_attention_dim + ) + clip_extra_context_tokens = self.norm(clip_extra_context_tokens) + return clip_extra_context_tokens + + +class MLPProjModel(torch.nn.Module): + """SD model with image prompt""" + + def zero_initialize(module): + for param in module.parameters(): + param.data.zero_() + + def zero_initialize_last_layer(module): + last_layer = None + for module_name, layer in module.named_modules(): + if isinstance(layer, torch.nn.Linear): + last_layer = layer + + if last_layer is not None: + last_layer.weight.data.zero_() + last_layer.bias.data.zero_() + + def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024): + super().__init__() + + self.proj = torch.nn.Sequential( + torch.nn.Linear(clip_embeddings_dim, clip_embeddings_dim), + torch.nn.GELU(), + torch.nn.Linear(clip_embeddings_dim, cross_attention_dim), + torch.nn.LayerNorm(cross_attention_dim), + ) + # zero initialize the last layer + # self.zero_initialize_last_layer() + + def forward(self, image_embeds): + clip_extra_context_tokens = self.proj(image_embeds) + return clip_extra_context_tokens + + +class V2AMapperMLP(torch.nn.Module): + def __init__(self, cross_attention_dim=512, clip_embeddings_dim=512, mult=4): + super().__init__() + self.proj = torch.nn.Sequential( + torch.nn.Linear(clip_embeddings_dim, clip_embeddings_dim * mult), + torch.nn.GELU(), + torch.nn.Linear(clip_embeddings_dim * mult, cross_attention_dim), + torch.nn.LayerNorm(cross_attention_dim), + ) + + def forward(self, image_embeds): + clip_extra_context_tokens = self.proj(image_embeds) + return clip_extra_context_tokens + + +class TimeProjModel(torch.nn.Module): + def __init__(self, positive_len, out_dim, feature_type="text-only", frame_nums: int = 64): + super().__init__() + self.positive_len = positive_len + self.out_dim = out_dim + + self.position_dim = frame_nums + + if isinstance(out_dim, tuple): + out_dim = out_dim[0] + + if feature_type == "text-only": + self.linears = nn.Sequential( + nn.Linear(self.positive_len + self.position_dim, 512), + nn.SiLU(), + nn.Linear(512, 512), + nn.SiLU(), + nn.Linear(512, out_dim), + ) + self.null_positive_feature = torch.nn.Parameter(torch.zeros([self.positive_len])) + + elif feature_type == "text-image": + self.linears_text = nn.Sequential( + nn.Linear(self.positive_len + self.position_dim, 512), + nn.SiLU(), + nn.Linear(512, 512), + nn.SiLU(), + nn.Linear(512, out_dim), + ) + self.linears_image = nn.Sequential( + nn.Linear(self.positive_len + self.position_dim, 512), + nn.SiLU(), + nn.Linear(512, 512), + nn.SiLU(), + nn.Linear(512, out_dim), + ) + self.null_text_feature = torch.nn.Parameter(torch.zeros([self.positive_len])) + self.null_image_feature = torch.nn.Parameter(torch.zeros([self.positive_len])) + + # self.null_position_feature = torch.nn.Parameter(torch.zeros([self.position_dim])) + + def forward( + self, + boxes, + masks, + positive_embeddings=None, + ): + masks = masks.unsqueeze(-1) + + # # embedding position (it may includes padding as placeholder) + # xyxy_embedding = self.fourier_embedder(boxes) # B*N*4 -> B*N*C + + # # learnable null embedding + # xyxy_null = self.null_position_feature.view(1, 1, -1) + + # # replace padding with learnable null embedding + # xyxy_embedding = xyxy_embedding * masks + (1 - masks) * xyxy_null + + time_embeds = boxes + + # positionet with text only information + if positive_embeddings is not None: + # learnable null embedding + positive_null = self.null_positive_feature.view(1, 1, -1) + + # replace padding with learnable null embedding + positive_embeddings = positive_embeddings * masks + (1 - masks) * positive_null + + objs = self.linears(torch.cat([positive_embeddings, time_embeds], dim=-1)) + + # positionet with text and image information + else: + raise NotImplementedError + + return objs diff --git a/foleycrafter/models/adapters/resampler.py b/foleycrafter/models/adapters/resampler.py new file mode 100644 index 0000000..2426667 --- /dev/null +++ b/foleycrafter/models/adapters/resampler.py @@ -0,0 +1,158 @@ +# modified from https://github.com/mlfoundations/open_flamingo/blob/main/open_flamingo/src/helpers.py +# and https://github.com/lucidrains/imagen-pytorch/blob/main/imagen_pytorch/imagen_pytorch.py + +import math + +import torch +import torch.nn as nn +from einops import rearrange +from einops.layers.torch import Rearrange + + +# FFN +def FeedForward(dim, mult=4): + inner_dim = int(dim * mult) + return nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, inner_dim, bias=False), + nn.GELU(), + nn.Linear(inner_dim, dim, bias=False), + ) + + +def reshape_tensor(x, heads): + bs, length, width = x.shape + # (bs, length, width) --> (bs, length, n_heads, dim_per_head) + x = x.view(bs, length, heads, -1) + # (bs, length, n_heads, dim_per_head) --> (bs, n_heads, length, dim_per_head) + x = x.transpose(1, 2) + # (bs, n_heads, length, dim_per_head) --> (bs*n_heads, length, dim_per_head) + x = x.reshape(bs, heads, length, -1) + return x + + +class PerceiverAttention(nn.Module): + def __init__(self, *, dim, dim_head=64, heads=8): + super().__init__() + self.scale = dim_head**-0.5 + self.dim_head = dim_head + self.heads = heads + inner_dim = dim_head * heads + + self.norm1 = nn.LayerNorm(dim) + self.norm2 = nn.LayerNorm(dim) + + self.to_q = nn.Linear(dim, inner_dim, bias=False) + self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False) + self.to_out = nn.Linear(inner_dim, dim, bias=False) + + def forward(self, x, latents): + """ + Args: + x (torch.Tensor): image features + shape (b, n1, D) + latent (torch.Tensor): latent features + shape (b, n2, D) + """ + x = self.norm1(x) + latents = self.norm2(latents) + + b, l, _ = latents.shape + + q = self.to_q(latents) + kv_input = torch.cat((x, latents), dim=-2) + k, v = self.to_kv(kv_input).chunk(2, dim=-1) + + q = reshape_tensor(q, self.heads) + k = reshape_tensor(k, self.heads) + v = reshape_tensor(v, self.heads) + + # attention + scale = 1 / math.sqrt(math.sqrt(self.dim_head)) + weight = (q * scale) @ (k * scale).transpose(-2, -1) # More stable with f16 than dividing afterwards + weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype) + out = weight @ v + + out = out.permute(0, 2, 1, 3).reshape(b, l, -1) + + return self.to_out(out) + + +class Resampler(nn.Module): + def __init__( + self, + dim=1024, + depth=8, + dim_head=64, + heads=16, + num_queries=8, + embedding_dim=768, + output_dim=1024, + ff_mult=4, + max_seq_len: int = 257, # CLIP tokens + CLS token + apply_pos_emb: bool = False, + num_latents_mean_pooled: int = 0, # number of latents derived from mean pooled representation of the sequence + ): + super().__init__() + self.pos_emb = nn.Embedding(max_seq_len, embedding_dim) if apply_pos_emb else None + + self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5) + + self.proj_in = nn.Linear(embedding_dim, dim) + + self.proj_out = nn.Linear(dim, output_dim) + self.norm_out = nn.LayerNorm(output_dim) + + self.to_latents_from_mean_pooled_seq = ( + nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, dim * num_latents_mean_pooled), + Rearrange("b (n d) -> b n d", n=num_latents_mean_pooled), + ) + if num_latents_mean_pooled > 0 + else None + ) + + self.layers = nn.ModuleList([]) + for _ in range(depth): + self.layers.append( + nn.ModuleList( + [ + PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads), + FeedForward(dim=dim, mult=ff_mult), + ] + ) + ) + + def forward(self, x): + if self.pos_emb is not None: + n, device = x.shape[1], x.device + pos_emb = self.pos_emb(torch.arange(n, device=device)) + x = x + pos_emb + + latents = self.latents.repeat(x.size(0), 1, 1) + + x = self.proj_in(x) + + if self.to_latents_from_mean_pooled_seq: + meanpooled_seq = masked_mean(x, dim=1, mask=torch.ones(x.shape[:2], device=x.device, dtype=torch.bool)) + meanpooled_latents = self.to_latents_from_mean_pooled_seq(meanpooled_seq) + latents = torch.cat((meanpooled_latents, latents), dim=-2) + + for attn, ff in self.layers: + latents = attn(x, latents) + latents + latents = ff(latents) + latents + + latents = self.proj_out(latents) + return self.norm_out(latents) + + +def masked_mean(t, *, dim, mask=None): + if mask is None: + return t.mean(dim=dim) + + denom = mask.sum(dim=dim, keepdim=True) + mask = rearrange(mask, "b n -> b n 1") + masked_t = t.masked_fill(~mask, 0.0) + + return masked_t.sum(dim=dim) / denom.clamp(min=1e-5) diff --git a/foleycrafter/models/adapters/transformer.py b/foleycrafter/models/adapters/transformer.py new file mode 100644 index 0000000..3141657 --- /dev/null +++ b/foleycrafter/models/adapters/transformer.py @@ -0,0 +1,352 @@ +from typing import Optional, Tuple + +import torch +import torch.nn as nn +import torch.utils.checkpoint + + +class Attention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__(self, hidden_size, num_attention_heads, attention_head_dim, attention_dropout=0.0): + super().__init__() + self.embed_dim = hidden_size + self.num_heads = num_attention_heads + self.head_dim = attention_head_dim + + self.scale = self.head_dim**-0.5 + self.dropout = attention_dropout + + self.inner_dim = self.head_dim * self.num_heads + + self.k_proj = nn.Linear(self.embed_dim, self.inner_dim) + self.v_proj = nn.Linear(self.embed_dim, self.inner_dim) + self.q_proj = nn.Linear(self.embed_dim, self.inner_dim) + self.out_proj = nn.Linear(self.inner_dim, self.embed_dim) + + def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int): + return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).transpose(1, 2).contiguous() + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + causal_attention_mask: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = False, + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + """Input shape: Batch x Time x Channel""" + + bsz, tgt_len, embed_dim = hidden_states.size() + + # get query proj + query_states = self.q_proj(hidden_states) * self.scale + key_states = self._shape(self.k_proj(hidden_states), -1, bsz) + value_states = self._shape(self.v_proj(hidden_states), -1, bsz) + + proj_shape = (bsz * self.num_heads, -1, self.head_dim) + query_states = self._shape(query_states, tgt_len, bsz).view(*proj_shape) + key_states = key_states.view(*proj_shape) + value_states = value_states.view(*proj_shape) + + src_len = key_states.size(1) + attn_weights = torch.bmm(query_states, key_states.transpose(1, 2)) + + if attn_weights.size() != (bsz * self.num_heads, tgt_len, src_len): + raise ValueError( + f"Attention weights should be of size {(bsz * self.num_heads, tgt_len, src_len)}, but is" + f" {attn_weights.size()}" + ) + + # apply the causal_attention_mask first + if causal_attention_mask is not None: + if causal_attention_mask.size() != (bsz, 1, tgt_len, src_len): + raise ValueError( + f"Attention mask should be of size {(bsz, 1, tgt_len, src_len)}, but is" + f" {causal_attention_mask.size()}" + ) + attn_weights = attn_weights.view(bsz, self.num_heads, tgt_len, src_len) + causal_attention_mask + attn_weights = attn_weights.view(bsz * self.num_heads, tgt_len, src_len) + + if attention_mask is not None: + if attention_mask.size() != (bsz, 1, tgt_len, src_len): + raise ValueError( + f"Attention mask should be of size {(bsz, 1, tgt_len, src_len)}, but is {attention_mask.size()}" + ) + attn_weights = attn_weights.view(bsz, self.num_heads, tgt_len, src_len) + attention_mask + attn_weights = attn_weights.view(bsz * self.num_heads, tgt_len, src_len) + + attn_weights = nn.functional.softmax(attn_weights, dim=-1) + + if output_attentions: + # this operation is a bit akward, but it's required to + # make sure that attn_weights keeps its gradient. + # In order to do so, attn_weights have to reshaped + # twice and have to be reused in the following + attn_weights_reshaped = attn_weights.view(bsz, self.num_heads, tgt_len, src_len) + attn_weights = attn_weights_reshaped.view(bsz * self.num_heads, tgt_len, src_len) + else: + attn_weights_reshaped = None + + attn_probs = nn.functional.dropout(attn_weights, p=self.dropout, training=self.training) + + attn_output = torch.bmm(attn_probs, value_states) + + if attn_output.size() != (bsz * self.num_heads, tgt_len, self.head_dim): + raise ValueError( + f"`attn_output` should be of size {(bsz, self.num_heads, tgt_len, self.head_dim)}, but is" + f" {attn_output.size()}" + ) + + attn_output = attn_output.view(bsz, self.num_heads, tgt_len, self.head_dim) + attn_output = attn_output.transpose(1, 2) + attn_output = attn_output.reshape(bsz, tgt_len, self.inner_dim) + + attn_output = self.out_proj(attn_output) + + return attn_output, attn_weights_reshaped + + +class MLP(nn.Module): + def __init__(self, hidden_size, intermediate_size, mult=4): + super().__init__() + self.activation_fn = nn.SiLU() + self.fc1 = nn.Linear(hidden_size, intermediate_size * mult) + self.fc2 = nn.Linear(intermediate_size * mult, hidden_size) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + hidden_states = self.fc1(hidden_states) + hidden_states = self.activation_fn(hidden_states) + hidden_states = self.fc2(hidden_states) + return hidden_states + + +class Transformer(nn.Module): + def __init__(self, depth=12): + super().__init__() + self.layers = nn.ModuleList([TransformerBlock() for _ in range(depth)]) + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: torch.Tensor = None, + causal_attention_mask: torch.Tensor = None, + output_attentions: Optional[bool] = False, + ) -> Tuple[torch.FloatTensor]: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch, seq_len, embed_dim)` + attention_mask (`torch.FloatTensor`): attention mask of size + `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. + `(config.encoder_attention_heads,)`. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + """ + for layer in self.layers: + hidden_states = layer( + hidden_states=hidden_states, + attention_mask=attention_mask, + causal_attention_mask=causal_attention_mask, + output_attentions=output_attentions, + ) + + return hidden_states + + +class TransformerBlock(nn.Module): + def __init__( + self, + hidden_size=512, + num_attention_heads=12, + attention_head_dim=64, + attention_dropout=0.0, + dropout=0.0, + eps=1e-5, + ): + super().__init__() + self.embed_dim = hidden_size + self.self_attn = Attention( + hidden_size=hidden_size, num_attention_heads=num_attention_heads, attention_head_dim=attention_head_dim + ) + self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=eps) + self.mlp = MLP(hidden_size=hidden_size, intermediate_size=hidden_size) + self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=eps) + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: torch.Tensor = None, + causal_attention_mask: torch.Tensor = None, + output_attentions: Optional[bool] = False, + ) -> Tuple[torch.FloatTensor]: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch, seq_len, embed_dim)` + attention_mask (`torch.FloatTensor`): attention mask of size + `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. + `(config.encoder_attention_heads,)`. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + """ + residual = hidden_states + + hidden_states = self.layer_norm1(hidden_states) + hidden_states, attn_weights = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + causal_attention_mask=causal_attention_mask, + output_attentions=output_attentions, + ) + hidden_states = residual + hidden_states + + residual = hidden_states + hidden_states = self.layer_norm2(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + outputs = (hidden_states,) + + if output_attentions: + outputs += (attn_weights,) + + return outputs[0] + + +class DiffusionTransformerBlock(nn.Module): + def __init__( + self, + hidden_size=512, + num_attention_heads=12, + attention_head_dim=64, + attention_dropout=0.0, + dropout=0.0, + eps=1e-5, + ): + super().__init__() + self.embed_dim = hidden_size + self.self_attn = Attention( + hidden_size=hidden_size, num_attention_heads=num_attention_heads, attention_head_dim=attention_head_dim + ) + self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=eps) + self.mlp = MLP(hidden_size=hidden_size, intermediate_size=hidden_size) + self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=eps) + self.output_token = nn.Parameter(torch.randn(1, hidden_size)) + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: torch.Tensor = None, + causal_attention_mask: torch.Tensor = None, + output_attentions: Optional[bool] = False, + ) -> Tuple[torch.FloatTensor]: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch, seq_len, embed_dim)` + attention_mask (`torch.FloatTensor`): attention mask of size + `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. + `(config.encoder_attention_heads,)`. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + """ + output_token = self.output_token.unsqueeze(0).repeat(hidden_states.shape[0], 1, 1) + hidden_states = torch.cat([output_token, hidden_states], dim=1) + residual = hidden_states + + hidden_states = self.layer_norm1(hidden_states) + hidden_states, attn_weights = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + causal_attention_mask=causal_attention_mask, + output_attentions=output_attentions, + ) + hidden_states = residual + hidden_states + + residual = hidden_states + hidden_states = self.layer_norm2(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + outputs = (hidden_states,) + + if output_attentions: + outputs += (attn_weights,) + + return outputs[0][:, 0:1, ...] + + +class V2AMapperMLP(nn.Module): + def __init__(self, input_dim=512, output_dim=512, expansion_rate=4): + super().__init__() + self.linear = nn.Linear(input_dim, input_dim * expansion_rate) + self.silu = nn.SiLU() + self.layer_norm = nn.LayerNorm(input_dim * expansion_rate) + self.linear2 = nn.Linear(input_dim * expansion_rate, output_dim) + + def forward(self, x): + x = self.linear(x) + x = self.silu(x) + x = self.layer_norm(x) + x = self.linear2(x) + + return x + + +class ImageProjModel(torch.nn.Module): + """Projection Model""" + + def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024, clip_extra_context_tokens=4): + super().__init__() + + self.cross_attention_dim = cross_attention_dim + self.clip_extra_context_tokens = clip_extra_context_tokens + self.proj = torch.nn.Linear(clip_embeddings_dim, self.clip_extra_context_tokens * cross_attention_dim) + self.norm = torch.nn.LayerNorm(cross_attention_dim) + + self.zero_initialize_last_layer() + + def zero_initialize_last_layer(module): + last_layer = None + for module_name, layer in module.named_modules(): + if isinstance(layer, torch.nn.Linear): + last_layer = layer + + if last_layer is not None: + last_layer.weight.data.zero_() + last_layer.bias.data.zero_() + + def forward(self, image_embeds): + embeds = image_embeds + clip_extra_context_tokens = self.proj(embeds).reshape( + -1, self.clip_extra_context_tokens, self.cross_attention_dim + ) + clip_extra_context_tokens = self.norm(clip_extra_context_tokens) + return clip_extra_context_tokens + + +class VisionAudioAdapter(torch.nn.Module): + def __init__( + self, + embedding_size=768, + expand_dim=4, + token_num=4, + ): + super().__init__() + + self.mapper = V2AMapperMLP( + embedding_size, + embedding_size, + expansion_rate=expand_dim, + ) + + self.proj = ImageProjModel( + cross_attention_dim=embedding_size, + clip_embeddings_dim=embedding_size, + clip_extra_context_tokens=token_num, + ) + + def forward(self, image_embeds): + image_embeds = self.mapper(image_embeds) + image_embeds = self.proj(image_embeds) + return image_embeds diff --git a/foleycrafter/models/adapters/utils.py b/foleycrafter/models/adapters/utils.py new file mode 100644 index 0000000..2b01fb3 --- /dev/null +++ b/foleycrafter/models/adapters/utils.py @@ -0,0 +1,86 @@ +import numpy as np +import torch +import torch.nn.functional as F +from PIL import Image + + +attn_maps = {} + + +def hook_fn(name): + def forward_hook(module, input, output): + if hasattr(module.processor, "attn_map"): + attn_maps[name] = module.processor.attn_map + del module.processor.attn_map + + return forward_hook + + +def register_cross_attention_hook(unet): + for name, module in unet.named_modules(): + if name.split(".")[-1].startswith("attn2"): + module.register_forward_hook(hook_fn(name)) + + return unet + + +def upscale(attn_map, target_size): + attn_map = torch.mean(attn_map, dim=0) + attn_map = attn_map.permute(1, 0) + temp_size = None + + for i in range(0, 5): + scale = 2**i + if (target_size[0] // scale) * (target_size[1] // scale) == attn_map.shape[1] * 64: + temp_size = (target_size[0] // (scale * 8), target_size[1] // (scale * 8)) + break + + assert temp_size is not None, "temp_size cannot is None" + + attn_map = attn_map.view(attn_map.shape[0], *temp_size) + + attn_map = F.interpolate( + attn_map.unsqueeze(0).to(dtype=torch.float32), size=target_size, mode="bilinear", align_corners=False + )[0] + + attn_map = torch.softmax(attn_map, dim=0) + return attn_map + + +def get_net_attn_map(image_size, batch_size=2, instance_or_negative=False, detach=True): + idx = 0 if instance_or_negative else 1 + net_attn_maps = [] + + for name, attn_map in attn_maps.items(): + attn_map = attn_map.cpu() if detach else attn_map + attn_map = torch.chunk(attn_map, batch_size)[idx].squeeze() + attn_map = upscale(attn_map, image_size) + net_attn_maps.append(attn_map) + + net_attn_maps = torch.mean(torch.stack(net_attn_maps, dim=0), dim=0) + + return net_attn_maps + + +def attnmaps2images(net_attn_maps): + # total_attn_scores = 0 + images = [] + + for attn_map in net_attn_maps: + attn_map = attn_map.cpu().numpy() + # total_attn_scores += attn_map.mean().item() + + normalized_attn_map = (attn_map - np.min(attn_map)) / (np.max(attn_map) - np.min(attn_map)) * 255 + normalized_attn_map = normalized_attn_map.astype(np.uint8) + # print("norm: ", normalized_attn_map.shape) + image = Image.fromarray(normalized_attn_map) + + # image = fix_save_attn_map(attn_map) + images.append(image) + + # print(total_attn_scores) + return images + + +def is_torch2_available(): + return hasattr(F, "scaled_dot_product_attention") diff --git a/foleycrafter/models/auffusion/attention.py b/foleycrafter/models/auffusion/attention.py new file mode 100644 index 0000000..f19ae80 --- /dev/null +++ b/foleycrafter/models/auffusion/attention.py @@ -0,0 +1,668 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Dict, Optional + +import torch +import torch.nn.functional as F +from torch import nn + +from diffusers.models.activations import GEGLU, GELU, ApproximateGELU +from diffusers.models.embeddings import SinusoidalPositionalEmbedding +from diffusers.models.lora import LoRACompatibleLinear +from diffusers.models.normalization import AdaLayerNorm, AdaLayerNormContinuous, AdaLayerNormZero, RMSNorm +from diffusers.utils import USE_PEFT_BACKEND +from diffusers.utils.torch_utils import maybe_allow_in_graph +from .attention_processor import Attention + + +def _chunked_feed_forward( + ff: nn.Module, hidden_states: torch.Tensor, chunk_dim: int, chunk_size: int, lora_scale: Optional[float] = None +): + # "feed_forward_chunk_size" can be used to save memory + if hidden_states.shape[chunk_dim] % chunk_size != 0: + raise ValueError( + f"`hidden_states` dimension to be chunked: {hidden_states.shape[chunk_dim]} has to be divisible by chunk size: {chunk_size}. Make sure to set an appropriate `chunk_size` when calling `unet.enable_forward_chunking`." + ) + + num_chunks = hidden_states.shape[chunk_dim] // chunk_size + if lora_scale is None: + ff_output = torch.cat( + [ff(hid_slice) for hid_slice in hidden_states.chunk(num_chunks, dim=chunk_dim)], + dim=chunk_dim, + ) + else: + # TODO(Patrick): LoRA scale can be removed once PEFT refactor is complete + ff_output = torch.cat( + [ff(hid_slice, scale=lora_scale) for hid_slice in hidden_states.chunk(num_chunks, dim=chunk_dim)], + dim=chunk_dim, + ) + + return ff_output + + +@maybe_allow_in_graph +class GatedSelfAttentionDense(nn.Module): + r""" + A gated self-attention dense layer that combines visual features and object features. + + Parameters: + query_dim (`int`): The number of channels in the query. + context_dim (`int`): The number of channels in the context. + n_heads (`int`): The number of heads to use for attention. + d_head (`int`): The number of channels in each head. + """ + + def __init__(self, query_dim: int, context_dim: int, n_heads: int, d_head: int): + super().__init__() + + # we need a linear projection since we need cat visual feature and obj feature + self.linear = nn.Linear(context_dim, query_dim) + + self.attn = Attention(query_dim=query_dim, heads=n_heads, dim_head=d_head) + self.ff = FeedForward(query_dim, activation_fn="geglu") + + self.norm1 = nn.LayerNorm(query_dim) + self.norm2 = nn.LayerNorm(query_dim) + + self.register_parameter("alpha_attn", nn.Parameter(torch.tensor(0.0))) + self.register_parameter("alpha_dense", nn.Parameter(torch.tensor(0.0))) + + self.enabled = True + + def forward(self, x: torch.Tensor, objs: torch.Tensor) -> torch.Tensor: + if not self.enabled: + return x + + n_visual = x.shape[1] + objs = self.linear(objs) + + x = x + self.alpha_attn.tanh() * self.attn(self.norm1(torch.cat([x, objs], dim=1)))[:, :n_visual, :] + x = x + self.alpha_dense.tanh() * self.ff(self.norm2(x)) + + return x + + +@maybe_allow_in_graph +class BasicTransformerBlock(nn.Module): + r""" + A basic Transformer block. + + Parameters: + dim (`int`): The number of channels in the input and output. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm (: + obj: `int`, *optional*): The number of diffusion steps used during training. See `Transformer2DModel`. + attention_bias (: + obj: `bool`, *optional*, defaults to `False`): Configure if the attentions should contain a bias parameter. + only_cross_attention (`bool`, *optional*): + Whether to use only cross-attention layers. In this case two cross attention layers are used. + double_self_attention (`bool`, *optional*): + Whether to use two self-attention layers. In this case no cross attention layers are used. + upcast_attention (`bool`, *optional*): + Whether to upcast the attention computation to float32. This is useful for mixed precision training. + norm_elementwise_affine (`bool`, *optional*, defaults to `True`): + Whether to use learnable elementwise affine parameters for normalization. + norm_type (`str`, *optional*, defaults to `"layer_norm"`): + The normalization layer to use. Can be `"layer_norm"`, `"ada_norm"` or `"ada_norm_zero"`. + final_dropout (`bool` *optional*, defaults to False): + Whether to apply a final dropout after the last feed-forward layer. + attention_type (`str`, *optional*, defaults to `"default"`): + The type of attention to use. Can be `"default"` or `"gated"` or `"gated-text-image"`. + positional_embeddings (`str`, *optional*, defaults to `None`): + The type of positional embeddings to apply to. + num_positional_embeddings (`int`, *optional*, defaults to `None`): + The maximum number of positional embeddings to apply. + """ + + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + dropout=0.0, + cross_attention_dim: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + attention_bias: bool = False, + only_cross_attention: bool = False, + double_self_attention: bool = False, + upcast_attention: bool = False, + norm_elementwise_affine: bool = True, + norm_type: str = "layer_norm", # 'layer_norm', 'ada_norm', 'ada_norm_zero', 'ada_norm_single' + norm_eps: float = 1e-5, + final_dropout: bool = False, + attention_type: str = "default", + positional_embeddings: Optional[str] = None, + num_positional_embeddings: Optional[int] = None, + ada_norm_continous_conditioning_embedding_dim: Optional[int] = None, + ada_norm_bias: Optional[int] = None, + ff_inner_dim: Optional[int] = None, + ff_bias: bool = True, + attention_out_bias: bool = True, + ): + super().__init__() + self.only_cross_attention = only_cross_attention + + self.use_ada_layer_norm_zero = (num_embeds_ada_norm is not None) and norm_type == "ada_norm_zero" + self.use_ada_layer_norm = (num_embeds_ada_norm is not None) and norm_type == "ada_norm" + self.use_ada_layer_norm_single = norm_type == "ada_norm_single" + self.use_layer_norm = norm_type == "layer_norm" + self.use_ada_layer_norm_continuous = norm_type == "ada_norm_continuous" + + if norm_type in ("ada_norm", "ada_norm_zero") and num_embeds_ada_norm is None: + raise ValueError( + f"`norm_type` is set to {norm_type}, but `num_embeds_ada_norm` is not defined. Please make sure to" + f" define `num_embeds_ada_norm` if setting `norm_type` to {norm_type}." + ) + + if positional_embeddings and (num_positional_embeddings is None): + raise ValueError( + "If `positional_embedding` type is defined, `num_positition_embeddings` must also be defined." + ) + + if positional_embeddings == "sinusoidal": + self.pos_embed = SinusoidalPositionalEmbedding(dim, max_seq_length=num_positional_embeddings) + else: + self.pos_embed = None + + # Define 3 blocks. Each block has its own normalization layer. + # 1. Self-Attn + if self.use_ada_layer_norm: + self.norm1 = AdaLayerNorm(dim, num_embeds_ada_norm) + elif self.use_ada_layer_norm_zero: + self.norm1 = AdaLayerNormZero(dim, num_embeds_ada_norm) + elif self.use_ada_layer_norm_continuous: + self.norm1 = AdaLayerNormContinuous( + dim, + ada_norm_continous_conditioning_embedding_dim, + norm_elementwise_affine, + norm_eps, + ada_norm_bias, + "rms_norm", + ) + else: + self.norm1 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine, eps=norm_eps) + + self.attn1 = Attention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=cross_attention_dim if (only_cross_attention and not double_self_attention) else None, + upcast_attention=upcast_attention, + out_bias=attention_out_bias, + ) + + # 2. Cross-Attn + if cross_attention_dim is not None or double_self_attention: + # We currently only use AdaLayerNormZero for self attention where there will only be one attention block. + # I.e. the number of returned modulation chunks from AdaLayerZero would not make sense if returned during + # the second cross attention block. + if self.use_ada_layer_norm: + self.norm2 = AdaLayerNorm(dim, num_embeds_ada_norm) + elif self.use_ada_layer_norm_continuous: + self.norm2 = AdaLayerNormContinuous( + dim, + ada_norm_continous_conditioning_embedding_dim, + norm_elementwise_affine, + norm_eps, + ada_norm_bias, + "rms_norm", + ) + else: + self.norm2 = nn.LayerNorm(dim, norm_eps, norm_elementwise_affine) + + self.attn2 = Attention( + query_dim=dim, + cross_attention_dim=cross_attention_dim if not double_self_attention else None, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + upcast_attention=upcast_attention, + out_bias=attention_out_bias, + ) # is self-attn if encoder_hidden_states is none + else: + self.norm2 = None + self.attn2 = None + + # 3. Feed-forward + if self.use_ada_layer_norm_continuous: + self.norm3 = AdaLayerNormContinuous( + dim, + ada_norm_continous_conditioning_embedding_dim, + norm_elementwise_affine, + norm_eps, + ada_norm_bias, + "layer_norm", + ) + elif not self.use_ada_layer_norm_single: + self.norm3 = nn.LayerNorm(dim, norm_eps, norm_elementwise_affine) + + self.ff = FeedForward( + dim, + dropout=dropout, + activation_fn=activation_fn, + final_dropout=final_dropout, + inner_dim=ff_inner_dim, + bias=ff_bias, + ) + + # 4. Fuser + if attention_type == "gated" or attention_type == "gated-text-image": + self.fuser = GatedSelfAttentionDense(dim, cross_attention_dim, num_attention_heads, attention_head_dim) + + # 5. Scale-shift for PixArt-Alpha. + if self.use_ada_layer_norm_single: + self.scale_shift_table = nn.Parameter(torch.randn(6, dim) / dim**0.5) + + # let chunk size default to None + self._chunk_size = None + self._chunk_dim = 0 + + def set_chunk_feed_forward(self, chunk_size: Optional[int], dim: int = 0): + # Sets chunk feed-forward + self._chunk_size = chunk_size + self._chunk_dim = dim + + def forward( + self, + hidden_states: torch.FloatTensor, + attention_mask: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + timestep: Optional[torch.LongTensor] = None, + cross_attention_kwargs: Dict[str, Any] = None, + class_labels: Optional[torch.LongTensor] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + ) -> torch.FloatTensor: + # Notice that normalization is always applied before the real computation in the following blocks. + # 0. Self-Attention + batch_size = hidden_states.shape[0] + + if self.use_ada_layer_norm: + norm_hidden_states = self.norm1(hidden_states, timestep) + elif self.use_ada_layer_norm_zero: + norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1( + hidden_states, timestep, class_labels, hidden_dtype=hidden_states.dtype + ) + elif self.use_layer_norm: + norm_hidden_states = self.norm1(hidden_states) + elif self.use_ada_layer_norm_continuous: + norm_hidden_states = self.norm1(hidden_states, added_cond_kwargs["pooled_text_emb"]) + elif self.use_ada_layer_norm_single: + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ( + self.scale_shift_table[None] + timestep.reshape(batch_size, 6, -1) + ).chunk(6, dim=1) + norm_hidden_states = self.norm1(hidden_states) + norm_hidden_states = norm_hidden_states * (1 + scale_msa) + shift_msa + norm_hidden_states = norm_hidden_states.squeeze(1) + else: + raise ValueError("Incorrect norm used") + + if self.pos_embed is not None: + norm_hidden_states = self.pos_embed(norm_hidden_states) + + # 1. Retrieve lora scale. + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + + # 2. Prepare GLIGEN inputs + cross_attention_kwargs = cross_attention_kwargs.copy() if cross_attention_kwargs is not None else {} + gligen_kwargs = cross_attention_kwargs.pop("gligen", None) + + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states if self.only_cross_attention else None, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + if self.use_ada_layer_norm_zero: + attn_output = gate_msa.unsqueeze(1) * attn_output + elif self.use_ada_layer_norm_single: + attn_output = gate_msa * attn_output + + hidden_states = attn_output + hidden_states + if hidden_states.ndim == 4: + hidden_states = hidden_states.squeeze(1) + + # 2.5 GLIGEN Control + if gligen_kwargs is not None: + hidden_states = self.fuser(hidden_states, gligen_kwargs["objs"]) + + # 3. Cross-Attention + if self.attn2 is not None: + if self.use_ada_layer_norm: + norm_hidden_states = self.norm2(hidden_states, timestep) + elif self.use_ada_layer_norm_zero or self.use_layer_norm: + norm_hidden_states = self.norm2(hidden_states) + elif self.use_ada_layer_norm_single: + # For PixArt norm2 isn't applied here: + # https://github.com/PixArt-alpha/PixArt-alpha/blob/0f55e922376d8b797edd44d25d0e7464b260dcab/diffusion/model/nets/PixArtMS.py#L70C1-L76C103 + norm_hidden_states = hidden_states + elif self.use_ada_layer_norm_continuous: + norm_hidden_states = self.norm2(hidden_states, added_cond_kwargs["pooled_text_emb"]) + else: + raise ValueError("Incorrect norm") + + if self.pos_embed is not None and self.use_ada_layer_norm_single is False: + norm_hidden_states = self.pos_embed(norm_hidden_states) + + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=encoder_attention_mask, + **cross_attention_kwargs, + ) + hidden_states = attn_output + hidden_states + + # 4. Feed-forward + if self.use_ada_layer_norm_continuous: + norm_hidden_states = self.norm3(hidden_states, added_cond_kwargs["pooled_text_emb"]) + elif not self.use_ada_layer_norm_single: + norm_hidden_states = self.norm3(hidden_states) + + if self.use_ada_layer_norm_zero: + norm_hidden_states = norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None] + + if self.use_ada_layer_norm_single: + norm_hidden_states = self.norm2(hidden_states) + norm_hidden_states = norm_hidden_states * (1 + scale_mlp) + shift_mlp + + if self._chunk_size is not None: + # "feed_forward_chunk_size" can be used to save memory + ff_output = _chunked_feed_forward( + self.ff, norm_hidden_states, self._chunk_dim, self._chunk_size, lora_scale=lora_scale + ) + else: + ff_output = self.ff(norm_hidden_states, scale=lora_scale) + + if self.use_ada_layer_norm_zero: + ff_output = gate_mlp.unsqueeze(1) * ff_output + elif self.use_ada_layer_norm_single: + ff_output = gate_mlp * ff_output + + hidden_states = ff_output + hidden_states + if hidden_states.ndim == 4: + hidden_states = hidden_states.squeeze(1) + + return hidden_states + + +@maybe_allow_in_graph +class TemporalBasicTransformerBlock(nn.Module): + r""" + A basic Transformer block for video like data. + + Parameters: + dim (`int`): The number of channels in the input and output. + time_mix_inner_dim (`int`): The number of channels for temporal attention. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + """ + + def __init__( + self, + dim: int, + time_mix_inner_dim: int, + num_attention_heads: int, + attention_head_dim: int, + cross_attention_dim: Optional[int] = None, + ): + super().__init__() + self.is_res = dim == time_mix_inner_dim + + self.norm_in = nn.LayerNorm(dim) + + # Define 3 blocks. Each block has its own normalization layer. + # 1. Self-Attn + self.norm_in = nn.LayerNorm(dim) + self.ff_in = FeedForward( + dim, + dim_out=time_mix_inner_dim, + activation_fn="geglu", + ) + + self.norm1 = nn.LayerNorm(time_mix_inner_dim) + self.attn1 = Attention( + query_dim=time_mix_inner_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + cross_attention_dim=None, + ) + + # 2. Cross-Attn + if cross_attention_dim is not None: + # We currently only use AdaLayerNormZero for self attention where there will only be one attention block. + # I.e. the number of returned modulation chunks from AdaLayerZero would not make sense if returned during + # the second cross attention block. + self.norm2 = nn.LayerNorm(time_mix_inner_dim) + self.attn2 = Attention( + query_dim=time_mix_inner_dim, + cross_attention_dim=cross_attention_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + ) # is self-attn if encoder_hidden_states is none + else: + self.norm2 = None + self.attn2 = None + + # 3. Feed-forward + self.norm3 = nn.LayerNorm(time_mix_inner_dim) + self.ff = FeedForward(time_mix_inner_dim, activation_fn="geglu") + + # let chunk size default to None + self._chunk_size = None + self._chunk_dim = None + + def set_chunk_feed_forward(self, chunk_size: Optional[int], **kwargs): + # Sets chunk feed-forward + self._chunk_size = chunk_size + # chunk dim should be hardcoded to 1 to have better speed vs. memory trade-off + self._chunk_dim = 1 + + def forward( + self, + hidden_states: torch.FloatTensor, + num_frames: int, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + # Notice that normalization is always applied before the real computation in the following blocks. + # 0. Self-Attention + batch_size = hidden_states.shape[0] + + batch_frames, seq_length, channels = hidden_states.shape + batch_size = batch_frames // num_frames + + hidden_states = hidden_states[None, :].reshape(batch_size, num_frames, seq_length, channels) + hidden_states = hidden_states.permute(0, 2, 1, 3) + hidden_states = hidden_states.reshape(batch_size * seq_length, num_frames, channels) + + residual = hidden_states + hidden_states = self.norm_in(hidden_states) + + if self._chunk_size is not None: + hidden_states = _chunked_feed_forward(self.ff_in, hidden_states, self._chunk_dim, self._chunk_size) + else: + hidden_states = self.ff_in(hidden_states) + + if self.is_res: + hidden_states = hidden_states + residual + + norm_hidden_states = self.norm1(hidden_states) + attn_output = self.attn1(norm_hidden_states, encoder_hidden_states=None) + hidden_states = attn_output + hidden_states + + # 3. Cross-Attention + if self.attn2 is not None: + norm_hidden_states = self.norm2(hidden_states) + attn_output = self.attn2(norm_hidden_states, encoder_hidden_states=encoder_hidden_states) + hidden_states = attn_output + hidden_states + + # 4. Feed-forward + norm_hidden_states = self.norm3(hidden_states) + + if self._chunk_size is not None: + ff_output = _chunked_feed_forward(self.ff, norm_hidden_states, self._chunk_dim, self._chunk_size) + else: + ff_output = self.ff(norm_hidden_states) + + if self.is_res: + hidden_states = ff_output + hidden_states + else: + hidden_states = ff_output + + hidden_states = hidden_states[None, :].reshape(batch_size, seq_length, num_frames, channels) + hidden_states = hidden_states.permute(0, 2, 1, 3) + hidden_states = hidden_states.reshape(batch_size * num_frames, seq_length, channels) + + return hidden_states + + +class SkipFFTransformerBlock(nn.Module): + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + kv_input_dim: int, + kv_input_dim_proj_use_bias: bool, + dropout=0.0, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + attention_out_bias: bool = True, + ): + super().__init__() + if kv_input_dim != dim: + self.kv_mapper = nn.Linear(kv_input_dim, dim, kv_input_dim_proj_use_bias) + else: + self.kv_mapper = None + + self.norm1 = RMSNorm(dim, 1e-06) + + self.attn1 = Attention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=cross_attention_dim, + out_bias=attention_out_bias, + ) + + self.norm2 = RMSNorm(dim, 1e-06) + + self.attn2 = Attention( + query_dim=dim, + cross_attention_dim=cross_attention_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + out_bias=attention_out_bias, + ) + + def forward(self, hidden_states, encoder_hidden_states, cross_attention_kwargs): + cross_attention_kwargs = cross_attention_kwargs.copy() if cross_attention_kwargs is not None else {} + + if self.kv_mapper is not None: + encoder_hidden_states = self.kv_mapper(F.silu(encoder_hidden_states)) + + norm_hidden_states = self.norm1(hidden_states) + + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + **cross_attention_kwargs, + ) + + hidden_states = attn_output + hidden_states + + norm_hidden_states = self.norm2(hidden_states) + + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + **cross_attention_kwargs, + ) + + hidden_states = attn_output + hidden_states + + return hidden_states + + +class FeedForward(nn.Module): + r""" + A feed-forward layer. + + Parameters: + dim (`int`): The number of channels in the input. + dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`. + mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + final_dropout (`bool` *optional*, defaults to False): Apply a final dropout. + bias (`bool`, defaults to True): Whether to use a bias in the linear layer. + """ + + def __init__( + self, + dim: int, + dim_out: Optional[int] = None, + mult: int = 4, + dropout: float = 0.0, + activation_fn: str = "geglu", + final_dropout: bool = False, + inner_dim=None, + bias: bool = True, + ): + super().__init__() + if inner_dim is None: + inner_dim = int(dim * mult) + dim_out = dim_out if dim_out is not None else dim + linear_cls = LoRACompatibleLinear if not USE_PEFT_BACKEND else nn.Linear + + if activation_fn == "gelu": + act_fn = GELU(dim, inner_dim, bias=bias) + if activation_fn == "gelu-approximate": + act_fn = GELU(dim, inner_dim, approximate="tanh", bias=bias) + elif activation_fn == "geglu": + act_fn = GEGLU(dim, inner_dim, bias=bias) + elif activation_fn == "geglu-approximate": + act_fn = ApproximateGELU(dim, inner_dim, bias=bias) + + self.net = nn.ModuleList([]) + # project in + self.net.append(act_fn) + # project dropout + self.net.append(nn.Dropout(dropout)) + # project out + self.net.append(linear_cls(inner_dim, dim_out, bias=bias)) + # FF as used in Vision Transformer, MLP-Mixer, etc. have a final dropout + if final_dropout: + self.net.append(nn.Dropout(dropout)) + + def forward(self, hidden_states: torch.Tensor, scale: float = 1.0) -> torch.Tensor: + compatible_cls = (GEGLU,) if USE_PEFT_BACKEND else (GEGLU, LoRACompatibleLinear) + for module in self.net: + if isinstance(module, compatible_cls): + hidden_states = module(hidden_states, scale) + else: + hidden_states = module(hidden_states) + return hidden_states diff --git a/foleycrafter/models/auffusion/attention_processor.py b/foleycrafter/models/auffusion/attention_processor.py new file mode 100644 index 0000000..45c1a0e --- /dev/null +++ b/foleycrafter/models/auffusion/attention_processor.py @@ -0,0 +1,2691 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math +from importlib import import_module +from typing import Callable, List, Optional, Union + +import torch +import torch.nn.functional as F +from einops import rearrange +from torch import nn + +from diffusers.models.lora import LoRACompatibleLinear, LoRALinearLayer +from diffusers.utils import USE_PEFT_BACKEND, deprecate, logging +from diffusers.utils.import_utils import is_xformers_available +from diffusers.utils.torch_utils import maybe_allow_in_graph + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +if is_xformers_available(): + import xformers + import xformers.ops +else: + xformers = None + + +@maybe_allow_in_graph +class Attention(nn.Module): + r""" + A cross attention layer. + + Parameters: + query_dim (`int`): + The number of channels in the query. + cross_attention_dim (`int`, *optional*): + The number of channels in the encoder_hidden_states. If not given, defaults to `query_dim`. + heads (`int`, *optional*, defaults to 8): + The number of heads to use for multi-head attention. + dim_head (`int`, *optional*, defaults to 64): + The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): + The dropout probability to use. + bias (`bool`, *optional*, defaults to False): + Set to `True` for the query, key, and value linear layers to contain a bias parameter. + upcast_attention (`bool`, *optional*, defaults to False): + Set to `True` to upcast the attention computation to `float32`. + upcast_softmax (`bool`, *optional*, defaults to False): + Set to `True` to upcast the softmax computation to `float32`. + cross_attention_norm (`str`, *optional*, defaults to `None`): + The type of normalization to use for the cross attention. Can be `None`, `layer_norm`, or `group_norm`. + cross_attention_norm_num_groups (`int`, *optional*, defaults to 32): + The number of groups to use for the group norm in the cross attention. + added_kv_proj_dim (`int`, *optional*, defaults to `None`): + The number of channels to use for the added key and value projections. If `None`, no projection is used. + norm_num_groups (`int`, *optional*, defaults to `None`): + The number of groups to use for the group norm in the attention. + spatial_norm_dim (`int`, *optional*, defaults to `None`): + The number of channels to use for the spatial normalization. + out_bias (`bool`, *optional*, defaults to `True`): + Set to `True` to use a bias in the output linear layer. + scale_qk (`bool`, *optional*, defaults to `True`): + Set to `True` to scale the query and key by `1 / sqrt(dim_head)`. + only_cross_attention (`bool`, *optional*, defaults to `False`): + Set to `True` to only use cross attention and not added_kv_proj_dim. Can only be set to `True` if + `added_kv_proj_dim` is not `None`. + eps (`float`, *optional*, defaults to 1e-5): + An additional value added to the denominator in group normalization that is used for numerical stability. + rescale_output_factor (`float`, *optional*, defaults to 1.0): + A factor to rescale the output by dividing it with this value. + residual_connection (`bool`, *optional*, defaults to `False`): + Set to `True` to add the residual connection to the output. + _from_deprecated_attn_block (`bool`, *optional*, defaults to `False`): + Set to `True` if the attention block is loaded from a deprecated state dict. + processor (`AttnProcessor`, *optional*, defaults to `None`): + The attention processor to use. If `None`, defaults to `AttnProcessor2_0` if `torch 2.x` is used and + `AttnProcessor` otherwise. + """ + + def __init__( + self, + query_dim: int, + cross_attention_dim: Optional[int] = None, + heads: int = 8, + dim_head: int = 64, + dropout: float = 0.0, + bias: bool = False, + upcast_attention: bool = False, + upcast_softmax: bool = False, + cross_attention_norm: Optional[str] = None, + cross_attention_norm_num_groups: int = 32, + added_kv_proj_dim: Optional[int] = None, + norm_num_groups: Optional[int] = None, + spatial_norm_dim: Optional[int] = None, + out_bias: bool = True, + scale_qk: bool = True, + only_cross_attention: bool = False, + eps: float = 1e-5, + rescale_output_factor: float = 1.0, + residual_connection: bool = False, + _from_deprecated_attn_block: bool = False, + processor: Optional["AttnProcessor"] = None, + out_dim: int = None, + ): + super().__init__() + self.inner_dim = out_dim if out_dim is not None else dim_head * heads + self.query_dim = query_dim + self.cross_attention_dim = cross_attention_dim if cross_attention_dim is not None else query_dim + self.upcast_attention = upcast_attention + self.upcast_softmax = upcast_softmax + self.rescale_output_factor = rescale_output_factor + self.residual_connection = residual_connection + self.dropout = dropout + self.fused_projections = False + self.out_dim = out_dim if out_dim is not None else query_dim + + # we make use of this private variable to know whether this class is loaded + # with an deprecated state dict so that we can convert it on the fly + self._from_deprecated_attn_block = _from_deprecated_attn_block + + self.scale_qk = scale_qk + self.scale = dim_head**-0.5 if self.scale_qk else 1.0 + + self.heads = out_dim // dim_head if out_dim is not None else heads + # for slice_size > 0 the attention score computation + # is split across the batch axis to save memory + # You can set slice_size with `set_attention_slice` + self.sliceable_head_dim = heads + + self.added_kv_proj_dim = added_kv_proj_dim + self.only_cross_attention = only_cross_attention + + if self.added_kv_proj_dim is None and self.only_cross_attention: + raise ValueError( + "`only_cross_attention` can only be set to True if `added_kv_proj_dim` is not None. Make sure to set either `only_cross_attention=False` or define `added_kv_proj_dim`." + ) + + if norm_num_groups is not None: + self.group_norm = nn.GroupNorm(num_channels=query_dim, num_groups=norm_num_groups, eps=eps, affine=True) + else: + self.group_norm = None + + if spatial_norm_dim is not None: + self.spatial_norm = SpatialNorm(f_channels=query_dim, zq_channels=spatial_norm_dim) + else: + self.spatial_norm = None + + if cross_attention_norm is None: + self.norm_cross = None + elif cross_attention_norm == "layer_norm": + self.norm_cross = nn.LayerNorm(self.cross_attention_dim) + elif cross_attention_norm == "group_norm": + if self.added_kv_proj_dim is not None: + # The given `encoder_hidden_states` are initially of shape + # (batch_size, seq_len, added_kv_proj_dim) before being projected + # to (batch_size, seq_len, cross_attention_dim). The norm is applied + # before the projection, so we need to use `added_kv_proj_dim` as + # the number of channels for the group norm. + norm_cross_num_channels = added_kv_proj_dim + else: + norm_cross_num_channels = self.cross_attention_dim + + self.norm_cross = nn.GroupNorm( + num_channels=norm_cross_num_channels, num_groups=cross_attention_norm_num_groups, eps=1e-5, affine=True + ) + else: + raise ValueError( + f"unknown cross_attention_norm: {cross_attention_norm}. Should be None, 'layer_norm' or 'group_norm'" + ) + + if USE_PEFT_BACKEND: + linear_cls = nn.Linear + else: + linear_cls = LoRACompatibleLinear + + self.linear_cls = linear_cls + self.to_q = linear_cls(query_dim, self.inner_dim, bias=bias) + + if not self.only_cross_attention: + # only relevant for the `AddedKVProcessor` classes + self.to_k = linear_cls(self.cross_attention_dim, self.inner_dim, bias=bias) + self.to_v = linear_cls(self.cross_attention_dim, self.inner_dim, bias=bias) + else: + self.to_k = None + self.to_v = None + + if self.added_kv_proj_dim is not None: + self.add_k_proj = linear_cls(added_kv_proj_dim, self.inner_dim) + self.add_v_proj = linear_cls(added_kv_proj_dim, self.inner_dim) + + self.to_out = nn.ModuleList([]) + self.to_out.append(linear_cls(self.inner_dim, self.out_dim, bias=out_bias)) + self.to_out.append(nn.Dropout(dropout)) + + # set attention processor + # We use the AttnProcessor2_0 by default when torch 2.x is used which uses + # torch.nn.functional.scaled_dot_product_attention for native Flash/memory_efficient_attention + # but only if it has the default `scale` argument. TODO remove scale_qk check when we move to torch 2.1 + if processor is None: + processor = ( + AttnProcessor2_0() if hasattr(F, "scaled_dot_product_attention") and self.scale_qk else AttnProcessor() + ) + self.set_processor(processor) + + def set_use_memory_efficient_attention_xformers( + self, use_memory_efficient_attention_xformers: bool, attention_op: Optional[Callable] = None + ) -> None: + r""" + Set whether to use memory efficient attention from `xformers` or not. + + Args: + use_memory_efficient_attention_xformers (`bool`): + Whether to use memory efficient attention from `xformers` or not. + attention_op (`Callable`, *optional*): + The attention operation to use. Defaults to `None` which uses the default attention operation from + `xformers`. + """ + is_lora = hasattr(self, "processor") and isinstance( + self.processor, + LORA_ATTENTION_PROCESSORS, + ) + is_custom_diffusion = hasattr(self, "processor") and isinstance( + self.processor, + (CustomDiffusionAttnProcessor, CustomDiffusionXFormersAttnProcessor, CustomDiffusionAttnProcessor2_0), + ) + is_added_kv_processor = hasattr(self, "processor") and isinstance( + self.processor, + ( + AttnAddedKVProcessor, + AttnAddedKVProcessor2_0, + SlicedAttnAddedKVProcessor, + XFormersAttnAddedKVProcessor, + LoRAAttnAddedKVProcessor, + ), + ) + + if use_memory_efficient_attention_xformers: + if is_added_kv_processor and (is_lora or is_custom_diffusion): + raise NotImplementedError( + f"Memory efficient attention is currently not supported for LoRA or custom diffusion for attention processor type {self.processor}" + ) + if not is_xformers_available(): + raise ModuleNotFoundError( + ( + "Refer to https://github.com/facebookresearch/xformers for more information on how to install" + " xformers" + ), + name="xformers", + ) + elif not torch.cuda.is_available(): + raise ValueError( + "torch.cuda.is_available() should be True but is False. xformers' memory efficient attention is" + " only available for GPU " + ) + else: + try: + # Make sure we can run the memory efficient attention + _ = xformers.ops.memory_efficient_attention( + torch.randn((1, 2, 40), device="cuda"), + torch.randn((1, 2, 40), device="cuda"), + torch.randn((1, 2, 40), device="cuda"), + ) + except Exception as e: + raise e + + if is_lora: + # TODO (sayakpaul): should we throw a warning if someone wants to use the xformers + # variant when using PT 2.0 now that we have LoRAAttnProcessor2_0? + processor = LoRAXFormersAttnProcessor( + hidden_size=self.processor.hidden_size, + cross_attention_dim=self.processor.cross_attention_dim, + rank=self.processor.rank, + attention_op=attention_op, + ) + processor.load_state_dict(self.processor.state_dict()) + processor.to(self.processor.to_q_lora.up.weight.device) + elif is_custom_diffusion: + processor = CustomDiffusionXFormersAttnProcessor( + train_kv=self.processor.train_kv, + train_q_out=self.processor.train_q_out, + hidden_size=self.processor.hidden_size, + cross_attention_dim=self.processor.cross_attention_dim, + attention_op=attention_op, + ) + processor.load_state_dict(self.processor.state_dict()) + if hasattr(self.processor, "to_k_custom_diffusion"): + processor.to(self.processor.to_k_custom_diffusion.weight.device) + elif is_added_kv_processor: + # TODO(Patrick, Suraj, William) - currently xformers doesn't work for UnCLIP + # which uses this type of cross attention ONLY because the attention mask of format + # [0, ..., -10.000, ..., 0, ...,] is not supported + # throw warning + logger.info( + "Memory efficient attention with `xformers` might currently not work correctly if an attention mask is required for the attention operation." + ) + processor = XFormersAttnAddedKVProcessor(attention_op=attention_op) + else: + processor = XFormersAttnProcessor(attention_op=attention_op) + else: + if is_lora: + attn_processor_class = ( + LoRAAttnProcessor2_0 if hasattr(F, "scaled_dot_product_attention") else LoRAAttnProcessor + ) + processor = attn_processor_class( + hidden_size=self.processor.hidden_size, + cross_attention_dim=self.processor.cross_attention_dim, + rank=self.processor.rank, + ) + processor.load_state_dict(self.processor.state_dict()) + processor.to(self.processor.to_q_lora.up.weight.device) + elif is_custom_diffusion: + attn_processor_class = ( + CustomDiffusionAttnProcessor2_0 + if hasattr(F, "scaled_dot_product_attention") + else CustomDiffusionAttnProcessor + ) + processor = attn_processor_class( + train_kv=self.processor.train_kv, + train_q_out=self.processor.train_q_out, + hidden_size=self.processor.hidden_size, + cross_attention_dim=self.processor.cross_attention_dim, + ) + processor.load_state_dict(self.processor.state_dict()) + if hasattr(self.processor, "to_k_custom_diffusion"): + processor.to(self.processor.to_k_custom_diffusion.weight.device) + else: + # set attention processor + # We use the AttnProcessor2_0 by default when torch 2.x is used which uses + # torch.nn.functional.scaled_dot_product_attention for native Flash/memory_efficient_attention + # but only if it has the default `scale` argument. TODO remove scale_qk check when we move to torch 2.1 + processor = ( + AttnProcessor2_0() + if hasattr(F, "scaled_dot_product_attention") and self.scale_qk + else AttnProcessor() + ) + + self.set_processor(processor) + + def set_attention_slice(self, slice_size: int) -> None: + r""" + Set the slice size for attention computation. + + Args: + slice_size (`int`): + The slice size for attention computation. + """ + if slice_size is not None and slice_size > self.sliceable_head_dim: + raise ValueError(f"slice_size {slice_size} has to be smaller or equal to {self.sliceable_head_dim}.") + + if slice_size is not None and self.added_kv_proj_dim is not None: + processor = SlicedAttnAddedKVProcessor(slice_size) + elif slice_size is not None: + processor = SlicedAttnProcessor(slice_size) + elif self.added_kv_proj_dim is not None: + processor = AttnAddedKVProcessor() + else: + # set attention processor + # We use the AttnProcessor2_0 by default when torch 2.x is used which uses + # torch.nn.functional.scaled_dot_product_attention for native Flash/memory_efficient_attention + # but only if it has the default `scale` argument. TODO remove scale_qk check when we move to torch 2.1 + processor = ( + AttnProcessor2_0() if hasattr(F, "scaled_dot_product_attention") and self.scale_qk else AttnProcessor() + ) + + self.set_processor(processor) + + def set_processor(self, processor: "AttnProcessor", _remove_lora: bool = False) -> None: + r""" + Set the attention processor to use. + + Args: + processor (`AttnProcessor`): + The attention processor to use. + _remove_lora (`bool`, *optional*, defaults to `False`): + Set to `True` to remove LoRA layers from the model. + """ + if not USE_PEFT_BACKEND and hasattr(self, "processor") and _remove_lora and self.to_q.lora_layer is not None: + deprecate( + "set_processor to offload LoRA", + "0.26.0", + "In detail, removing LoRA layers via calling `set_default_attn_processor` is deprecated. Please make sure to call `pipe.unload_lora_weights()` instead.", + ) + # TODO(Patrick, Sayak) - this can be deprecated once PEFT LoRA integration is complete + # We need to remove all LoRA layers + # Don't forget to remove ALL `_remove_lora` from the codebase + for module in self.modules(): + if hasattr(module, "set_lora_layer"): + module.set_lora_layer(None) + + # if current processor is in `self._modules` and if passed `processor` is not, we need to + # pop `processor` from `self._modules` + if ( + hasattr(self, "processor") + and isinstance(self.processor, torch.nn.Module) + and not isinstance(processor, torch.nn.Module) + ): + logger.info(f"You are removing possibly trained weights of {self.processor} with {processor}") + self._modules.pop("processor") + + self.processor = processor + + def get_processor(self, return_deprecated_lora: bool = False) -> "AttentionProcessor": + r""" + Get the attention processor in use. + + Args: + return_deprecated_lora (`bool`, *optional*, defaults to `False`): + Set to `True` to return the deprecated LoRA attention processor. + + Returns: + "AttentionProcessor": The attention processor in use. + """ + if not return_deprecated_lora: + return self.processor + + # TODO(Sayak, Patrick). The rest of the function is needed to ensure backwards compatible + # serialization format for LoRA Attention Processors. It should be deleted once the integration + # with PEFT is completed. + is_lora_activated = { + name: module.lora_layer is not None + for name, module in self.named_modules() + if hasattr(module, "lora_layer") + } + + # 1. if no layer has a LoRA activated we can return the processor as usual + if not any(is_lora_activated.values()): + return self.processor + + # If doesn't apply LoRA do `add_k_proj` or `add_v_proj` + is_lora_activated.pop("add_k_proj", None) + is_lora_activated.pop("add_v_proj", None) + # 2. else it is not possible that only some layers have LoRA activated + if not all(is_lora_activated.values()): + raise ValueError( + f"Make sure that either all layers or no layers have LoRA activated, but have {is_lora_activated}" + ) + + # 3. And we need to merge the current LoRA layers into the corresponding LoRA attention processor + non_lora_processor_cls_name = self.processor.__class__.__name__ + lora_processor_cls = getattr(import_module(__name__), "LoRA" + non_lora_processor_cls_name) + + hidden_size = self.inner_dim + + # now create a LoRA attention processor from the LoRA layers + if lora_processor_cls in [LoRAAttnProcessor, LoRAAttnProcessor2_0, LoRAXFormersAttnProcessor]: + kwargs = { + "cross_attention_dim": self.cross_attention_dim, + "rank": self.to_q.lora_layer.rank, + "network_alpha": self.to_q.lora_layer.network_alpha, + "q_rank": self.to_q.lora_layer.rank, + "q_hidden_size": self.to_q.lora_layer.out_features, + "k_rank": self.to_k.lora_layer.rank, + "k_hidden_size": self.to_k.lora_layer.out_features, + "v_rank": self.to_v.lora_layer.rank, + "v_hidden_size": self.to_v.lora_layer.out_features, + "out_rank": self.to_out[0].lora_layer.rank, + "out_hidden_size": self.to_out[0].lora_layer.out_features, + } + + if hasattr(self.processor, "attention_op"): + kwargs["attention_op"] = self.processor.attention_op + + lora_processor = lora_processor_cls(hidden_size, **kwargs) + lora_processor.to_q_lora.load_state_dict(self.to_q.lora_layer.state_dict()) + lora_processor.to_k_lora.load_state_dict(self.to_k.lora_layer.state_dict()) + lora_processor.to_v_lora.load_state_dict(self.to_v.lora_layer.state_dict()) + lora_processor.to_out_lora.load_state_dict(self.to_out[0].lora_layer.state_dict()) + elif lora_processor_cls == LoRAAttnAddedKVProcessor: + lora_processor = lora_processor_cls( + hidden_size, + cross_attention_dim=self.add_k_proj.weight.shape[0], + rank=self.to_q.lora_layer.rank, + network_alpha=self.to_q.lora_layer.network_alpha, + ) + lora_processor.to_q_lora.load_state_dict(self.to_q.lora_layer.state_dict()) + lora_processor.to_k_lora.load_state_dict(self.to_k.lora_layer.state_dict()) + lora_processor.to_v_lora.load_state_dict(self.to_v.lora_layer.state_dict()) + lora_processor.to_out_lora.load_state_dict(self.to_out[0].lora_layer.state_dict()) + + # only save if used + if self.add_k_proj.lora_layer is not None: + lora_processor.add_k_proj_lora.load_state_dict(self.add_k_proj.lora_layer.state_dict()) + lora_processor.add_v_proj_lora.load_state_dict(self.add_v_proj.lora_layer.state_dict()) + else: + lora_processor.add_k_proj_lora = None + lora_processor.add_v_proj_lora = None + else: + raise ValueError(f"{lora_processor_cls} does not exist.") + + return lora_processor + + def forward( + self, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + **cross_attention_kwargs, + ) -> torch.Tensor: + r""" + The forward method of the `Attention` class. + + Args: + hidden_states (`torch.Tensor`): + The hidden states of the query. + encoder_hidden_states (`torch.Tensor`, *optional*): + The hidden states of the encoder. + attention_mask (`torch.Tensor`, *optional*): + The attention mask to use. If `None`, no mask is applied. + **cross_attention_kwargs: + Additional keyword arguments to pass along to the cross attention. + + Returns: + `torch.Tensor`: The output of the attention layer. + """ + # The `Attention` class can call different attention processors / attention functions + # here we simply pass along all tensors to the selected processor class + # For standard processors that are defined here, `**cross_attention_kwargs` is empty + return self.processor( + self, + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + + def batch_to_head_dim(self, tensor: torch.Tensor) -> torch.Tensor: + r""" + Reshape the tensor from `[batch_size, seq_len, dim]` to `[batch_size // heads, seq_len, dim * heads]`. `heads` + is the number of heads initialized while constructing the `Attention` class. + + Args: + tensor (`torch.Tensor`): The tensor to reshape. + + Returns: + `torch.Tensor`: The reshaped tensor. + """ + head_size = self.heads + batch_size, seq_len, dim = tensor.shape + tensor = tensor.reshape(batch_size // head_size, head_size, seq_len, dim) + tensor = tensor.permute(0, 2, 1, 3).reshape(batch_size // head_size, seq_len, dim * head_size) + return tensor + + def head_to_batch_dim(self, tensor: torch.Tensor, out_dim: int = 3) -> torch.Tensor: + r""" + Reshape the tensor from `[batch_size, seq_len, dim]` to `[batch_size, seq_len, heads, dim // heads]` `heads` is + the number of heads initialized while constructing the `Attention` class. + + Args: + tensor (`torch.Tensor`): The tensor to reshape. + out_dim (`int`, *optional*, defaults to `3`): The output dimension of the tensor. If `3`, the tensor is + reshaped to `[batch_size * heads, seq_len, dim // heads]`. + + Returns: + `torch.Tensor`: The reshaped tensor. + """ + head_size = self.heads + batch_size, seq_len, dim = tensor.shape + tensor = tensor.reshape(batch_size, seq_len, head_size, dim // head_size) + tensor = tensor.permute(0, 2, 1, 3) + + if out_dim == 3: + tensor = tensor.reshape(batch_size * head_size, seq_len, dim // head_size) + + return tensor + + def get_attention_scores( + self, query: torch.Tensor, key: torch.Tensor, attention_mask: torch.Tensor = None + ) -> torch.Tensor: + r""" + Compute the attention scores. + + Args: + query (`torch.Tensor`): The query tensor. + key (`torch.Tensor`): The key tensor. + attention_mask (`torch.Tensor`, *optional*): The attention mask to use. If `None`, no mask is applied. + + Returns: + `torch.Tensor`: The attention probabilities/scores. + """ + dtype = query.dtype + if self.upcast_attention: + query = query.float() + key = key.float() + + if attention_mask is None: + baddbmm_input = torch.empty( + query.shape[0], query.shape[1], key.shape[1], dtype=query.dtype, device=query.device + ) + beta = 0 + else: + baddbmm_input = attention_mask + beta = 1 + + attention_scores = torch.baddbmm( + baddbmm_input, + query, + key.transpose(-1, -2), + beta=beta, + alpha=self.scale, + ) + del baddbmm_input + + if self.upcast_softmax: + attention_scores = attention_scores.float() + + attention_probs = attention_scores.softmax(dim=-1) + del attention_scores + + attention_probs = attention_probs.to(dtype) + + return attention_probs + + def prepare_attention_mask( + self, attention_mask: torch.Tensor, target_length: int, batch_size: int, out_dim: int = 3 + ) -> torch.Tensor: + r""" + Prepare the attention mask for the attention computation. + + Args: + attention_mask (`torch.Tensor`): + The attention mask to prepare. + target_length (`int`): + The target length of the attention mask. This is the length of the attention mask after padding. + batch_size (`int`): + The batch size, which is used to repeat the attention mask. + out_dim (`int`, *optional*, defaults to `3`): + The output dimension of the attention mask. Can be either `3` or `4`. + + Returns: + `torch.Tensor`: The prepared attention mask. + """ + head_size = self.heads + if attention_mask is None: + return attention_mask + + current_length: int = attention_mask.shape[-1] + if current_length != target_length: + if attention_mask.device.type == "mps": + # HACK: MPS: Does not support padding by greater than dimension of input tensor. + # Instead, we can manually construct the padding tensor. + padding_shape = (attention_mask.shape[0], attention_mask.shape[1], target_length) + padding = torch.zeros(padding_shape, dtype=attention_mask.dtype, device=attention_mask.device) + attention_mask = torch.cat([attention_mask, padding], dim=2) + else: + # TODO: for pipelines such as stable-diffusion, padding cross-attn mask: + # we want to instead pad by (0, remaining_length), where remaining_length is: + # remaining_length: int = target_length - current_length + # TODO: re-enable tests/models/test_models_unet_2d_condition.py#test_model_xattn_padding + attention_mask = F.pad(attention_mask, (0, target_length), value=0.0) + + if out_dim == 3: + if attention_mask.shape[0] < batch_size * head_size: + attention_mask = attention_mask.repeat_interleave(head_size, dim=0) + elif out_dim == 4: + attention_mask = attention_mask.unsqueeze(1) + attention_mask = attention_mask.repeat_interleave(head_size, dim=1) + + return attention_mask + + def norm_encoder_hidden_states(self, encoder_hidden_states: torch.Tensor) -> torch.Tensor: + r""" + Normalize the encoder hidden states. Requires `self.norm_cross` to be specified when constructing the + `Attention` class. + + Args: + encoder_hidden_states (`torch.Tensor`): Hidden states of the encoder. + + Returns: + `torch.Tensor`: The normalized encoder hidden states. + """ + assert self.norm_cross is not None, "self.norm_cross must be defined to call self.norm_encoder_hidden_states" + + if isinstance(self.norm_cross, nn.LayerNorm): + encoder_hidden_states = self.norm_cross(encoder_hidden_states) + elif isinstance(self.norm_cross, nn.GroupNorm): + # Group norm norms along the channels dimension and expects + # input to be in the shape of (N, C, *). In this case, we want + # to norm along the hidden dimension, so we need to move + # (batch_size, sequence_length, hidden_size) -> + # (batch_size, hidden_size, sequence_length) + encoder_hidden_states = encoder_hidden_states.transpose(1, 2) + encoder_hidden_states = self.norm_cross(encoder_hidden_states) + encoder_hidden_states = encoder_hidden_states.transpose(1, 2) + else: + assert False + + return encoder_hidden_states + + @torch.no_grad() + def fuse_projections(self, fuse=True): + is_cross_attention = self.cross_attention_dim != self.query_dim + device = self.to_q.weight.data.device + dtype = self.to_q.weight.data.dtype + + if not is_cross_attention: + # fetch weight matrices. + concatenated_weights = torch.cat([self.to_q.weight.data, self.to_k.weight.data, self.to_v.weight.data]) + in_features = concatenated_weights.shape[1] + out_features = concatenated_weights.shape[0] + + # create a new single projection layer and copy over the weights. + self.to_qkv = self.linear_cls(in_features, out_features, bias=False, device=device, dtype=dtype) + self.to_qkv.weight.copy_(concatenated_weights) + + else: + concatenated_weights = torch.cat([self.to_k.weight.data, self.to_v.weight.data]) + in_features = concatenated_weights.shape[1] + out_features = concatenated_weights.shape[0] + + self.to_kv = self.linear_cls(in_features, out_features, bias=False, device=device, dtype=dtype) + self.to_kv.weight.copy_(concatenated_weights) + + self.fused_projections = fuse + + +class AttnProcessor: + r""" + Default processor for performing attention-related computations. + """ + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> torch.Tensor: + residual = hidden_states + + args = () if USE_PEFT_BACKEND else (scale,) + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states, *args) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states, *args) + value = attn.to_v(encoder_hidden_states, *args) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states, *args) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class CustomDiffusionAttnProcessor(nn.Module): + r""" + Processor for implementing attention for the Custom Diffusion method. + + Args: + train_kv (`bool`, defaults to `True`): + Whether to newly train the key and value matrices corresponding to the text features. + train_q_out (`bool`, defaults to `True`): + Whether to newly train query matrices corresponding to the latent image features. + hidden_size (`int`, *optional*, defaults to `None`): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*, defaults to `None`): + The number of channels in the `encoder_hidden_states`. + out_bias (`bool`, defaults to `True`): + Whether to include the bias parameter in `train_q_out`. + dropout (`float`, *optional*, defaults to 0.0): + The dropout probability to use. + """ + + def __init__( + self, + train_kv: bool = True, + train_q_out: bool = True, + hidden_size: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + out_bias: bool = True, + dropout: float = 0.0, + ): + super().__init__() + self.train_kv = train_kv + self.train_q_out = train_q_out + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + + # `_custom_diffusion` id for easy serialization and loading. + if self.train_kv: + self.to_k_custom_diffusion = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_custom_diffusion = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + if self.train_q_out: + self.to_q_custom_diffusion = nn.Linear(hidden_size, hidden_size, bias=False) + self.to_out_custom_diffusion = nn.ModuleList([]) + self.to_out_custom_diffusion.append(nn.Linear(hidden_size, hidden_size, bias=out_bias)) + self.to_out_custom_diffusion.append(nn.Dropout(dropout)) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.Tensor: + batch_size, sequence_length, _ = hidden_states.shape + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + if self.train_q_out: + query = self.to_q_custom_diffusion(hidden_states).to(attn.to_q.weight.dtype) + else: + query = attn.to_q(hidden_states.to(attn.to_q.weight.dtype)) + + if encoder_hidden_states is None: + crossattn = False + encoder_hidden_states = hidden_states + else: + crossattn = True + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + if self.train_kv: + key = self.to_k_custom_diffusion(encoder_hidden_states.to(self.to_k_custom_diffusion.weight.dtype)) + value = self.to_v_custom_diffusion(encoder_hidden_states.to(self.to_v_custom_diffusion.weight.dtype)) + key = key.to(attn.to_q.weight.dtype) + value = value.to(attn.to_q.weight.dtype) + else: + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + if crossattn: + detach = torch.ones_like(key) + detach[:, :1, :] = detach[:, :1, :] * 0.0 + key = detach * key + (1 - detach) * key.detach() + value = detach * value + (1 - detach) * value.detach() + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + if self.train_q_out: + # linear proj + hidden_states = self.to_out_custom_diffusion[0](hidden_states) + # dropout + hidden_states = self.to_out_custom_diffusion[1](hidden_states) + else: + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class AttnAddedKVProcessor: + r""" + Processor for performing attention-related computations with extra learnable key and value matrices for the text + encoder. + """ + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> torch.Tensor: + residual = hidden_states + + args = () if USE_PEFT_BACKEND else (scale,) + + hidden_states = hidden_states.view(hidden_states.shape[0], hidden_states.shape[1], -1).transpose(1, 2) + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states, *args) + query = attn.head_to_batch_dim(query) + + encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states, *args) + encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states, *args) + encoder_hidden_states_key_proj = attn.head_to_batch_dim(encoder_hidden_states_key_proj) + encoder_hidden_states_value_proj = attn.head_to_batch_dim(encoder_hidden_states_value_proj) + + if not attn.only_cross_attention: + key = attn.to_k(hidden_states, *args) + value = attn.to_v(hidden_states, *args) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + key = torch.cat([encoder_hidden_states_key_proj, key], dim=1) + value = torch.cat([encoder_hidden_states_value_proj, value], dim=1) + else: + key = encoder_hidden_states_key_proj + value = encoder_hidden_states_value_proj + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states, *args) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(residual.shape) + hidden_states = hidden_states + residual + + return hidden_states + + +class AttnAddedKVProcessor2_0: + r""" + Processor for performing scaled dot-product attention (enabled by default if you're using PyTorch 2.0), with extra + learnable key and value matrices for the text encoder. + """ + + def __init__(self): + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError( + "AttnAddedKVProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0." + ) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> torch.Tensor: + residual = hidden_states + + args = () if USE_PEFT_BACKEND else (scale,) + + hidden_states = hidden_states.view(hidden_states.shape[0], hidden_states.shape[1], -1).transpose(1, 2) + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size, out_dim=4) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states, *args) + query = attn.head_to_batch_dim(query, out_dim=4) + + encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states) + encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states) + encoder_hidden_states_key_proj = attn.head_to_batch_dim(encoder_hidden_states_key_proj, out_dim=4) + encoder_hidden_states_value_proj = attn.head_to_batch_dim(encoder_hidden_states_value_proj, out_dim=4) + + if not attn.only_cross_attention: + key = attn.to_k(hidden_states, *args) + value = attn.to_v(hidden_states, *args) + key = attn.head_to_batch_dim(key, out_dim=4) + value = attn.head_to_batch_dim(value, out_dim=4) + key = torch.cat([encoder_hidden_states_key_proj, key], dim=2) + value = torch.cat([encoder_hidden_states_value_proj, value], dim=2) + else: + key = encoder_hidden_states_key_proj + value = encoder_hidden_states_value_proj + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, residual.shape[1]) + + # linear proj + hidden_states = attn.to_out[0](hidden_states, *args) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(residual.shape) + hidden_states = hidden_states + residual + + return hidden_states + + +class XFormersAttnAddedKVProcessor: + r""" + Processor for implementing memory efficient attention using xFormers. + + Args: + attention_op (`Callable`, *optional*, defaults to `None`): + The base + [operator](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.AttentionOpBase) to + use as the attention operator. It is recommended to set to `None`, and allow xFormers to choose the best + operator. + """ + + def __init__(self, attention_op: Optional[Callable] = None): + self.attention_op = attention_op + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.Tensor: + residual = hidden_states + hidden_states = hidden_states.view(hidden_states.shape[0], hidden_states.shape[1], -1).transpose(1, 2) + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + query = attn.head_to_batch_dim(query) + + encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states) + encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states) + encoder_hidden_states_key_proj = attn.head_to_batch_dim(encoder_hidden_states_key_proj) + encoder_hidden_states_value_proj = attn.head_to_batch_dim(encoder_hidden_states_value_proj) + + if not attn.only_cross_attention: + key = attn.to_k(hidden_states) + value = attn.to_v(hidden_states) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + key = torch.cat([encoder_hidden_states_key_proj, key], dim=1) + value = torch.cat([encoder_hidden_states_value_proj, value], dim=1) + else: + key = encoder_hidden_states_key_proj + value = encoder_hidden_states_value_proj + + hidden_states = xformers.ops.memory_efficient_attention( + query, key, value, attn_bias=attention_mask, op=self.attention_op, scale=attn.scale + ) + hidden_states = hidden_states.to(query.dtype) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(residual.shape) + hidden_states = hidden_states + residual + + return hidden_states + + +class XFormersAttnProcessor: + r""" + Processor for implementing memory efficient attention using xFormers. + + Args: + attention_op (`Callable`, *optional*, defaults to `None`): + The base + [operator](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.AttentionOpBase) to + use as the attention operator. It is recommended to set to `None`, and allow xFormers to choose the best + operator. + """ + + def __init__(self, attention_op: Optional[Callable] = None): + self.attention_op = attention_op + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> torch.FloatTensor: + residual = hidden_states + + args = () if USE_PEFT_BACKEND else (scale,) + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, key_tokens, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + attention_mask = attn.prepare_attention_mask(attention_mask, key_tokens, batch_size) + if attention_mask is not None: + # expand our mask's singleton query_tokens dimension: + # [batch*heads, 1, key_tokens] -> + # [batch*heads, query_tokens, key_tokens] + # so that it can be added as a bias onto the attention scores that xformers computes: + # [batch*heads, query_tokens, key_tokens] + # we do this explicitly because xformers doesn't broadcast the singleton dimension for us. + _, query_tokens, _ = hidden_states.shape + attention_mask = attention_mask.expand(-1, query_tokens, -1) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states, *args) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states, *args) + value = attn.to_v(encoder_hidden_states, *args) + + query = attn.head_to_batch_dim(query).contiguous() + key = attn.head_to_batch_dim(key).contiguous() + value = attn.head_to_batch_dim(value).contiguous() + + hidden_states = xformers.ops.memory_efficient_attention( + query, key, value, attn_bias=attention_mask, op=self.attention_op, scale=attn.scale + ) + hidden_states = hidden_states.to(query.dtype) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states, *args) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class AttnProcessor2_0: + r""" + Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). + """ + + def __init__(self): + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + **kwargs, + ) -> torch.FloatTensor: + residual = hidden_states + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + args = () if USE_PEFT_BACKEND else (scale,) + query = attn.to_q(hidden_states, *args) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states, *args) + value = attn.to_v(encoder_hidden_states, *args) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # linear proj + hidden_states = attn.to_out[0](hidden_states, *args) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class FusedAttnProcessor2_0: + r""" + Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). + It uses fused projection layers. For self-attention modules, all projection matrices (i.e., query, + key, value) are fused. For cross-attention modules, key and value projection matrices are fused. + + + + This API is currently 🧪 experimental in nature and can change in future. + + + """ + + def __init__(self): + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError( + "FusedAttnProcessor2_0 requires at least PyTorch 2.0, to use it. Please upgrade PyTorch to > 2.0." + ) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> torch.FloatTensor: + residual = hidden_states + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + args = () if USE_PEFT_BACKEND else (scale,) + if encoder_hidden_states is None: + qkv = attn.to_qkv(hidden_states, *args) + split_size = qkv.shape[-1] // 3 + query, key, value = torch.split(qkv, split_size, dim=-1) + else: + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + query = attn.to_q(hidden_states, *args) + + kv = attn.to_kv(encoder_hidden_states, *args) + split_size = kv.shape[-1] // 2 + key, value = torch.split(kv, split_size, dim=-1) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # linear proj + hidden_states = attn.to_out[0](hidden_states, *args) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class CustomDiffusionXFormersAttnProcessor(nn.Module): + r""" + Processor for implementing memory efficient attention using xFormers for the Custom Diffusion method. + + Args: + train_kv (`bool`, defaults to `True`): + Whether to newly train the key and value matrices corresponding to the text features. + train_q_out (`bool`, defaults to `True`): + Whether to newly train query matrices corresponding to the latent image features. + hidden_size (`int`, *optional*, defaults to `None`): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*, defaults to `None`): + The number of channels in the `encoder_hidden_states`. + out_bias (`bool`, defaults to `True`): + Whether to include the bias parameter in `train_q_out`. + dropout (`float`, *optional*, defaults to 0.0): + The dropout probability to use. + attention_op (`Callable`, *optional*, defaults to `None`): + The base + [operator](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.AttentionOpBase) to use + as the attention operator. It is recommended to set to `None`, and allow xFormers to choose the best operator. + """ + + def __init__( + self, + train_kv: bool = True, + train_q_out: bool = False, + hidden_size: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + out_bias: bool = True, + dropout: float = 0.0, + attention_op: Optional[Callable] = None, + ): + super().__init__() + self.train_kv = train_kv + self.train_q_out = train_q_out + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.attention_op = attention_op + + # `_custom_diffusion` id for easy serialization and loading. + if self.train_kv: + self.to_k_custom_diffusion = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_custom_diffusion = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + if self.train_q_out: + self.to_q_custom_diffusion = nn.Linear(hidden_size, hidden_size, bias=False) + self.to_out_custom_diffusion = nn.ModuleList([]) + self.to_out_custom_diffusion.append(nn.Linear(hidden_size, hidden_size, bias=out_bias)) + self.to_out_custom_diffusion.append(nn.Dropout(dropout)) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if self.train_q_out: + query = self.to_q_custom_diffusion(hidden_states).to(attn.to_q.weight.dtype) + else: + query = attn.to_q(hidden_states.to(attn.to_q.weight.dtype)) + + if encoder_hidden_states is None: + crossattn = False + encoder_hidden_states = hidden_states + else: + crossattn = True + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + if self.train_kv: + key = self.to_k_custom_diffusion(encoder_hidden_states.to(self.to_k_custom_diffusion.weight.dtype)) + value = self.to_v_custom_diffusion(encoder_hidden_states.to(self.to_v_custom_diffusion.weight.dtype)) + key = key.to(attn.to_q.weight.dtype) + value = value.to(attn.to_q.weight.dtype) + else: + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + if crossattn: + detach = torch.ones_like(key) + detach[:, :1, :] = detach[:, :1, :] * 0.0 + key = detach * key + (1 - detach) * key.detach() + value = detach * value + (1 - detach) * value.detach() + + query = attn.head_to_batch_dim(query).contiguous() + key = attn.head_to_batch_dim(key).contiguous() + value = attn.head_to_batch_dim(value).contiguous() + + hidden_states = xformers.ops.memory_efficient_attention( + query, key, value, attn_bias=attention_mask, op=self.attention_op, scale=attn.scale + ) + hidden_states = hidden_states.to(query.dtype) + hidden_states = attn.batch_to_head_dim(hidden_states) + + if self.train_q_out: + # linear proj + hidden_states = self.to_out_custom_diffusion[0](hidden_states) + # dropout + hidden_states = self.to_out_custom_diffusion[1](hidden_states) + else: + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class CustomDiffusionAttnProcessor2_0(nn.Module): + r""" + Processor for implementing attention for the Custom Diffusion method using PyTorch 2.0’s memory-efficient scaled + dot-product attention. + + Args: + train_kv (`bool`, defaults to `True`): + Whether to newly train the key and value matrices corresponding to the text features. + train_q_out (`bool`, defaults to `True`): + Whether to newly train query matrices corresponding to the latent image features. + hidden_size (`int`, *optional*, defaults to `None`): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*, defaults to `None`): + The number of channels in the `encoder_hidden_states`. + out_bias (`bool`, defaults to `True`): + Whether to include the bias parameter in `train_q_out`. + dropout (`float`, *optional*, defaults to 0.0): + The dropout probability to use. + """ + + def __init__( + self, + train_kv: bool = True, + train_q_out: bool = True, + hidden_size: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + out_bias: bool = True, + dropout: float = 0.0, + ): + super().__init__() + self.train_kv = train_kv + self.train_q_out = train_q_out + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + + # `_custom_diffusion` id for easy serialization and loading. + if self.train_kv: + self.to_k_custom_diffusion = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_custom_diffusion = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + if self.train_q_out: + self.to_q_custom_diffusion = nn.Linear(hidden_size, hidden_size, bias=False) + self.to_out_custom_diffusion = nn.ModuleList([]) + self.to_out_custom_diffusion.append(nn.Linear(hidden_size, hidden_size, bias=out_bias)) + self.to_out_custom_diffusion.append(nn.Dropout(dropout)) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + batch_size, sequence_length, _ = hidden_states.shape + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + if self.train_q_out: + query = self.to_q_custom_diffusion(hidden_states) + else: + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + crossattn = False + encoder_hidden_states = hidden_states + else: + crossattn = True + if attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + if self.train_kv: + key = self.to_k_custom_diffusion(encoder_hidden_states.to(self.to_k_custom_diffusion.weight.dtype)) + value = self.to_v_custom_diffusion(encoder_hidden_states.to(self.to_v_custom_diffusion.weight.dtype)) + key = key.to(attn.to_q.weight.dtype) + value = value.to(attn.to_q.weight.dtype) + + else: + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + if crossattn: + detach = torch.ones_like(key) + detach[:, :1, :] = detach[:, :1, :] * 0.0 + key = detach * key + (1 - detach) * key.detach() + value = detach * value + (1 - detach) * value.detach() + + inner_dim = hidden_states.shape[-1] + + head_dim = inner_dim // attn.heads + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + if self.train_q_out: + # linear proj + hidden_states = self.to_out_custom_diffusion[0](hidden_states) + # dropout + hidden_states = self.to_out_custom_diffusion[1](hidden_states) + else: + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class SlicedAttnProcessor: + r""" + Processor for implementing sliced attention. + + Args: + slice_size (`int`, *optional*): + The number of steps to compute attention. Uses as many slices as `attention_head_dim // slice_size`, and + `attention_head_dim` must be a multiple of the `slice_size`. + """ + + def __init__(self, slice_size: int): + self.slice_size = slice_size + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + residual = hidden_states + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + dim = query.shape[-1] + query = attn.head_to_batch_dim(query) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + batch_size_attention, query_tokens, _ = query.shape + hidden_states = torch.zeros( + (batch_size_attention, query_tokens, dim // attn.heads), device=query.device, dtype=query.dtype + ) + + for i in range(batch_size_attention // self.slice_size): + start_idx = i * self.slice_size + end_idx = (i + 1) * self.slice_size + + query_slice = query[start_idx:end_idx] + key_slice = key[start_idx:end_idx] + attn_mask_slice = attention_mask[start_idx:end_idx] if attention_mask is not None else None + + attn_slice = attn.get_attention_scores(query_slice, key_slice, attn_mask_slice) + + attn_slice = torch.bmm(attn_slice, value[start_idx:end_idx]) + + hidden_states[start_idx:end_idx] = attn_slice + + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class SlicedAttnAddedKVProcessor: + r""" + Processor for implementing sliced attention with extra learnable key and value matrices for the text encoder. + + Args: + slice_size (`int`, *optional*): + The number of steps to compute attention. Uses as many slices as `attention_head_dim // slice_size`, and + `attention_head_dim` must be a multiple of the `slice_size`. + """ + + def __init__(self, slice_size): + self.slice_size = slice_size + + def __call__( + self, + attn: "Attention", + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + hidden_states = hidden_states.view(hidden_states.shape[0], hidden_states.shape[1], -1).transpose(1, 2) + + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + dim = query.shape[-1] + query = attn.head_to_batch_dim(query) + + encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states) + encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states) + + encoder_hidden_states_key_proj = attn.head_to_batch_dim(encoder_hidden_states_key_proj) + encoder_hidden_states_value_proj = attn.head_to_batch_dim(encoder_hidden_states_value_proj) + + if not attn.only_cross_attention: + key = attn.to_k(hidden_states) + value = attn.to_v(hidden_states) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + key = torch.cat([encoder_hidden_states_key_proj, key], dim=1) + value = torch.cat([encoder_hidden_states_value_proj, value], dim=1) + else: + key = encoder_hidden_states_key_proj + value = encoder_hidden_states_value_proj + + batch_size_attention, query_tokens, _ = query.shape + hidden_states = torch.zeros( + (batch_size_attention, query_tokens, dim // attn.heads), device=query.device, dtype=query.dtype + ) + + for i in range(batch_size_attention // self.slice_size): + start_idx = i * self.slice_size + end_idx = (i + 1) * self.slice_size + + query_slice = query[start_idx:end_idx] + key_slice = key[start_idx:end_idx] + attn_mask_slice = attention_mask[start_idx:end_idx] if attention_mask is not None else None + + attn_slice = attn.get_attention_scores(query_slice, key_slice, attn_mask_slice) + + attn_slice = torch.bmm(attn_slice, value[start_idx:end_idx]) + + hidden_states[start_idx:end_idx] = attn_slice + + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(residual.shape) + hidden_states = hidden_states + residual + + return hidden_states + + +class SpatialNorm(nn.Module): + """ + Spatially conditioned normalization as defined in https://arxiv.org/abs/2209.09002. + + Args: + f_channels (`int`): + The number of channels for input to group normalization layer, and output of the spatial norm layer. + zq_channels (`int`): + The number of channels for the quantized vector as described in the paper. + """ + + def __init__( + self, + f_channels: int, + zq_channels: int, + ): + super().__init__() + self.norm_layer = nn.GroupNorm(num_channels=f_channels, num_groups=32, eps=1e-6, affine=True) + self.conv_y = nn.Conv2d(zq_channels, f_channels, kernel_size=1, stride=1, padding=0) + self.conv_b = nn.Conv2d(zq_channels, f_channels, kernel_size=1, stride=1, padding=0) + + def forward(self, f: torch.FloatTensor, zq: torch.FloatTensor) -> torch.FloatTensor: + f_size = f.shape[-2:] + zq = F.interpolate(zq, size=f_size, mode="nearest") + norm_f = self.norm_layer(f) + new_f = norm_f * self.conv_y(zq) + self.conv_b(zq) + return new_f + + +## Deprecated +class LoRAAttnProcessor(nn.Module): + r""" + Processor for implementing the LoRA attention mechanism. + + Args: + hidden_size (`int`, *optional*): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*): + The number of channels in the `encoder_hidden_states`. + rank (`int`, defaults to 4): + The dimension of the LoRA update matrices. + network_alpha (`int`, *optional*): + Equivalent to `alpha` but it's usage is specific to Kohya (A1111) style LoRAs. + kwargs (`dict`): + Additional keyword arguments to pass to the `LoRALinearLayer` layers. + """ + + def __init__( + self, + hidden_size: int, + cross_attention_dim: Optional[int] = None, + rank: int = 4, + network_alpha: Optional[int] = None, + **kwargs, + ): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.rank = rank + + q_rank = kwargs.pop("q_rank", None) + q_hidden_size = kwargs.pop("q_hidden_size", None) + q_rank = q_rank if q_rank is not None else rank + q_hidden_size = q_hidden_size if q_hidden_size is not None else hidden_size + + v_rank = kwargs.pop("v_rank", None) + v_hidden_size = kwargs.pop("v_hidden_size", None) + v_rank = v_rank if v_rank is not None else rank + v_hidden_size = v_hidden_size if v_hidden_size is not None else hidden_size + + out_rank = kwargs.pop("out_rank", None) + out_hidden_size = kwargs.pop("out_hidden_size", None) + out_rank = out_rank if out_rank is not None else rank + out_hidden_size = out_hidden_size if out_hidden_size is not None else hidden_size + + self.to_q_lora = LoRALinearLayer(q_hidden_size, q_hidden_size, q_rank, network_alpha) + self.to_k_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank, network_alpha) + self.to_v_lora = LoRALinearLayer(cross_attention_dim or v_hidden_size, v_hidden_size, v_rank, network_alpha) + self.to_out_lora = LoRALinearLayer(out_hidden_size, out_hidden_size, out_rank, network_alpha) + + def __call__(self, attn: Attention, hidden_states: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + self_cls_name = self.__class__.__name__ + deprecate( + self_cls_name, + "0.26.0", + ( + f"Make sure use {self_cls_name[4:]} instead by setting" + "LoRA layers to `self.{to_q,to_k,to_v,to_out[0]}.lora_layer` respectively. This will be done automatically when using" + " `LoraLoaderMixin.load_lora_weights`" + ), + ) + attn.to_q.lora_layer = self.to_q_lora.to(hidden_states.device) + attn.to_k.lora_layer = self.to_k_lora.to(hidden_states.device) + attn.to_v.lora_layer = self.to_v_lora.to(hidden_states.device) + attn.to_out[0].lora_layer = self.to_out_lora.to(hidden_states.device) + + attn._modules.pop("processor") + attn.processor = AttnProcessor() + return attn.processor(attn, hidden_states, *args, **kwargs) + + +class LoRAAttnProcessor2_0(nn.Module): + r""" + Processor for implementing the LoRA attention mechanism using PyTorch 2.0's memory-efficient scaled dot-product + attention. + + Args: + hidden_size (`int`): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*): + The number of channels in the `encoder_hidden_states`. + rank (`int`, defaults to 4): + The dimension of the LoRA update matrices. + network_alpha (`int`, *optional*): + Equivalent to `alpha` but it's usage is specific to Kohya (A1111) style LoRAs. + kwargs (`dict`): + Additional keyword arguments to pass to the `LoRALinearLayer` layers. + """ + + def __init__( + self, + hidden_size: int, + cross_attention_dim: Optional[int] = None, + rank: int = 4, + network_alpha: Optional[int] = None, + **kwargs, + ): + super().__init__() + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.rank = rank + + q_rank = kwargs.pop("q_rank", None) + q_hidden_size = kwargs.pop("q_hidden_size", None) + q_rank = q_rank if q_rank is not None else rank + q_hidden_size = q_hidden_size if q_hidden_size is not None else hidden_size + + v_rank = kwargs.pop("v_rank", None) + v_hidden_size = kwargs.pop("v_hidden_size", None) + v_rank = v_rank if v_rank is not None else rank + v_hidden_size = v_hidden_size if v_hidden_size is not None else hidden_size + + out_rank = kwargs.pop("out_rank", None) + out_hidden_size = kwargs.pop("out_hidden_size", None) + out_rank = out_rank if out_rank is not None else rank + out_hidden_size = out_hidden_size if out_hidden_size is not None else hidden_size + + self.to_q_lora = LoRALinearLayer(q_hidden_size, q_hidden_size, q_rank, network_alpha) + self.to_k_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank, network_alpha) + self.to_v_lora = LoRALinearLayer(cross_attention_dim or v_hidden_size, v_hidden_size, v_rank, network_alpha) + self.to_out_lora = LoRALinearLayer(out_hidden_size, out_hidden_size, out_rank, network_alpha) + + def __call__(self, attn: Attention, hidden_states: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + self_cls_name = self.__class__.__name__ + deprecate( + self_cls_name, + "0.26.0", + ( + f"Make sure use {self_cls_name[4:]} instead by setting" + "LoRA layers to `self.{to_q,to_k,to_v,to_out[0]}.lora_layer` respectively. This will be done automatically when using" + " `LoraLoaderMixin.load_lora_weights`" + ), + ) + attn.to_q.lora_layer = self.to_q_lora.to(hidden_states.device) + attn.to_k.lora_layer = self.to_k_lora.to(hidden_states.device) + attn.to_v.lora_layer = self.to_v_lora.to(hidden_states.device) + attn.to_out[0].lora_layer = self.to_out_lora.to(hidden_states.device) + + attn._modules.pop("processor") + attn.processor = AttnProcessor2_0() + return attn.processor(attn, hidden_states, *args, **kwargs) + + +class LoRAXFormersAttnProcessor(nn.Module): + r""" + Processor for implementing the LoRA attention mechanism with memory efficient attention using xFormers. + + Args: + hidden_size (`int`, *optional*): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*): + The number of channels in the `encoder_hidden_states`. + rank (`int`, defaults to 4): + The dimension of the LoRA update matrices. + attention_op (`Callable`, *optional*, defaults to `None`): + The base + [operator](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.AttentionOpBase) to + use as the attention operator. It is recommended to set to `None`, and allow xFormers to choose the best + operator. + network_alpha (`int`, *optional*): + Equivalent to `alpha` but it's usage is specific to Kohya (A1111) style LoRAs. + kwargs (`dict`): + Additional keyword arguments to pass to the `LoRALinearLayer` layers. + """ + + def __init__( + self, + hidden_size: int, + cross_attention_dim: int, + rank: int = 4, + attention_op: Optional[Callable] = None, + network_alpha: Optional[int] = None, + **kwargs, + ): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.rank = rank + self.attention_op = attention_op + + q_rank = kwargs.pop("q_rank", None) + q_hidden_size = kwargs.pop("q_hidden_size", None) + q_rank = q_rank if q_rank is not None else rank + q_hidden_size = q_hidden_size if q_hidden_size is not None else hidden_size + + v_rank = kwargs.pop("v_rank", None) + v_hidden_size = kwargs.pop("v_hidden_size", None) + v_rank = v_rank if v_rank is not None else rank + v_hidden_size = v_hidden_size if v_hidden_size is not None else hidden_size + + out_rank = kwargs.pop("out_rank", None) + out_hidden_size = kwargs.pop("out_hidden_size", None) + out_rank = out_rank if out_rank is not None else rank + out_hidden_size = out_hidden_size if out_hidden_size is not None else hidden_size + + self.to_q_lora = LoRALinearLayer(q_hidden_size, q_hidden_size, q_rank, network_alpha) + self.to_k_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank, network_alpha) + self.to_v_lora = LoRALinearLayer(cross_attention_dim or v_hidden_size, v_hidden_size, v_rank, network_alpha) + self.to_out_lora = LoRALinearLayer(out_hidden_size, out_hidden_size, out_rank, network_alpha) + + def __call__(self, attn: Attention, hidden_states: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + self_cls_name = self.__class__.__name__ + deprecate( + self_cls_name, + "0.26.0", + ( + f"Make sure use {self_cls_name[4:]} instead by setting" + "LoRA layers to `self.{to_q,to_k,to_v,add_k_proj,add_v_proj,to_out[0]}.lora_layer` respectively. This will be done automatically when using" + " `LoraLoaderMixin.load_lora_weights`" + ), + ) + attn.to_q.lora_layer = self.to_q_lora.to(hidden_states.device) + attn.to_k.lora_layer = self.to_k_lora.to(hidden_states.device) + attn.to_v.lora_layer = self.to_v_lora.to(hidden_states.device) + attn.to_out[0].lora_layer = self.to_out_lora.to(hidden_states.device) + + attn._modules.pop("processor") + attn.processor = XFormersAttnProcessor() + return attn.processor(attn, hidden_states, *args, **kwargs) + + +class LoRAAttnAddedKVProcessor(nn.Module): + r""" + Processor for implementing the LoRA attention mechanism with extra learnable key and value matrices for the text + encoder. + + Args: + hidden_size (`int`, *optional*): + The hidden size of the attention layer. + cross_attention_dim (`int`, *optional*, defaults to `None`): + The number of channels in the `encoder_hidden_states`. + rank (`int`, defaults to 4): + The dimension of the LoRA update matrices. + network_alpha (`int`, *optional*): + Equivalent to `alpha` but it's usage is specific to Kohya (A1111) style LoRAs. + kwargs (`dict`): + Additional keyword arguments to pass to the `LoRALinearLayer` layers. + """ + + def __init__( + self, + hidden_size: int, + cross_attention_dim: Optional[int] = None, + rank: int = 4, + network_alpha: Optional[int] = None, + ): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.rank = rank + + self.to_q_lora = LoRALinearLayer(hidden_size, hidden_size, rank, network_alpha) + self.add_k_proj_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank, network_alpha) + self.add_v_proj_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank, network_alpha) + self.to_k_lora = LoRALinearLayer(hidden_size, hidden_size, rank, network_alpha) + self.to_v_lora = LoRALinearLayer(hidden_size, hidden_size, rank, network_alpha) + self.to_out_lora = LoRALinearLayer(hidden_size, hidden_size, rank, network_alpha) + + def __call__(self, attn: Attention, hidden_states: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + self_cls_name = self.__class__.__name__ + deprecate( + self_cls_name, + "0.26.0", + ( + f"Make sure use {self_cls_name[4:]} instead by setting" + "LoRA layers to `self.{to_q,to_k,to_v,add_k_proj,add_v_proj,to_out[0]}.lora_layer` respectively. This will be done automatically when using" + " `LoraLoaderMixin.load_lora_weights`" + ), + ) + attn.to_q.lora_layer = self.to_q_lora.to(hidden_states.device) + attn.to_k.lora_layer = self.to_k_lora.to(hidden_states.device) + attn.to_v.lora_layer = self.to_v_lora.to(hidden_states.device) + attn.to_out[0].lora_layer = self.to_out_lora.to(hidden_states.device) + + attn._modules.pop("processor") + attn.processor = AttnAddedKVProcessor() + return attn.processor(attn, hidden_states, *args, **kwargs) + + +class IPAdapterAttnProcessor(nn.Module): + r""" + Attention processor for IP-Adapater. + + Args: + hidden_size (`int`): + The hidden size of the attention layer. + cross_attention_dim (`int`): + The number of channels in the `encoder_hidden_states`. + num_tokens (`int`, defaults to 4): + The context length of the image features. + scale (`float`, defaults to 1.0): + the weight scale of image prompt. + """ + + def __init__(self, hidden_size, cross_attention_dim=None, num_tokens=4, scale=1.0): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.num_tokens = num_tokens + self.scale = scale + + self.to_k_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + + def __call__( + self, + attn, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + temb=None, + scale=1.0, + ): + if scale != 1.0: + logger.warning("`scale` of IPAttnProcessor should be set with `set_ip_adapter_scale`.") + residual = hidden_states + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + # split hidden states + end_pos = encoder_hidden_states.shape[1] - self.num_tokens + encoder_hidden_states, ip_hidden_states = ( + encoder_hidden_states[:, :end_pos, :], + encoder_hidden_states[:, end_pos:, :], + ) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # for ip-adapter + ip_key = self.to_k_ip(ip_hidden_states) + ip_value = self.to_v_ip(ip_hidden_states) + + ip_key = attn.head_to_batch_dim(ip_key) + ip_value = attn.head_to_batch_dim(ip_value) + + ip_attention_probs = attn.get_attention_scores(query, ip_key, None) + ip_hidden_states = torch.bmm(ip_attention_probs, ip_value) + ip_hidden_states = attn.batch_to_head_dim(ip_hidden_states) + + hidden_states = hidden_states + self.scale * ip_hidden_states + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class VPTemporalAdapterAttnProcessor2_0(torch.nn.Module): + r""" + Attention processor for IP-Adapter for PyTorch 2.0. + + Args: + hidden_size (`int`): + The hidden size of the attention layer. + cross_attention_dim (`int`): + The number of channels in the `encoder_hidden_states`. + num_tokens (`int`, `Tuple[int]` or `List[int]`, defaults to `(4,)`): + The context length of the image features. + scale (`float` or `List[float]`, defaults to 1.0): + the weight scale of image prompt. + """ + + """ + Support frame-wise VP-Adapter + encoder_hidden_states : I(num of ip_adapters), B, N * T(num of time condition), C + ip_adapter_masks(bool): (I, B, N * T, C) == encoder_hidden_states.shape + + """ + + def __init__(self, hidden_size, cross_attention_dim=None, num_tokens=(4,), scale=1.0): + super().__init__() + + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError( + f"{self.__class__.__name__} requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0." + ) + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + + if not isinstance(num_tokens, (tuple, list)): + num_tokens = [num_tokens] + self.num_tokens = num_tokens + + if not isinstance(scale, list): + scale = [scale] * len(num_tokens) + if len(scale) != len(num_tokens): + raise ValueError("`scale` should be a list of integers with the same length as `num_tokens`.") + self.scale = scale + + self.to_k_ip = nn.ModuleList( + [nn.Linear(cross_attention_dim, hidden_size, bias=False) for _ in range(len(num_tokens))] + ) + self.to_v_ip = nn.ModuleList( + [nn.Linear(cross_attention_dim, hidden_size, bias=False) for _ in range(len(num_tokens))] + ) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ip_adapter_masks: Optional[torch.FloatTensor] = None, + time_conditions: Optional[list] = None, + audio_length_in_s: Optional[int] = None, + ): + residual = hidden_states + + # separate ip_hidden_states from encoder_hidden_states + if encoder_hidden_states is not None: + if isinstance(encoder_hidden_states, tuple): + encoder_hidden_states, ip_hidden_states = encoder_hidden_states + else: + deprecation_message = ( + "You have passed a tensor as `encoder_hidden_states`. This is deprecated and will be removed in a future release." + " Please make sure to update your script to pass `encoder_hidden_states` as a tuple to suppress this warning." + ) + deprecate("encoder_hidden_states not a tuple", "1.0.0", deprecation_message, standard_warn=False) + end_pos = encoder_hidden_states.shape[1] - self.num_tokens[0] + encoder_hidden_states, ip_hidden_states = ( + encoder_hidden_states[:, :end_pos, :], + [encoder_hidden_states[:, end_pos:, :]], + ) + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + if ip_adapter_masks is not None: + if not isinstance(ip_adapter_masks, List): + # for backward compatibility, we accept `ip_adapter_mask` as a tensor of shape [num_ip_adapter, 1, height, width] + ip_adapter_masks = list(ip_adapter_masks.unsqueeze(1)) + if not (len(ip_adapter_masks) == len(self.scale) == len(ip_hidden_states)): + raise ValueError( + f"Length of ip_adapter_masks array ({len(ip_adapter_masks)}) must match " + f"length of self.scale array ({len(self.scale)}) and number of ip_hidden_states " + f"({len(ip_hidden_states)})" + ) + else: + for index, (mask, scale, ip_state) in enumerate(zip(ip_adapter_masks, self.scale, ip_hidden_states)): + if not isinstance(mask, torch.Tensor) or mask.ndim != 4: + raise ValueError( + "Each element of the ip_adapter_masks array should be a tensor with shape " + "[1, num_images_for_ip_adapter, height, width]." + " Please use `IPAdapterMaskProcessor` to preprocess your mask" + ) + if mask.shape[1] != ip_state.shape[1]: + raise ValueError( + f"Number of masks ({mask.shape[1]}) does not match " + f"number of ip images ({ip_state.shape[1]}) at index {index}" + ) + if isinstance(scale, list) and not len(scale) == mask.shape[1]: + raise ValueError( + f"Number of masks ({mask.shape[1]}) does not match " + f"number of scales ({len(scale)}) at index {index}" + ) + else: + ip_adapter_masks = [None] * len(self.scale) + # for ip-adapter + for current_ip_hidden_states, scale, to_k_ip, to_v_ip, mask in zip( + ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip, ip_adapter_masks + ): + skip = False + if isinstance(scale, list): + if all(s == 0 for s in scale): + skip = True + elif scale == 0: + skip = True + if not skip: + time_condition_masks = None + for time_condition in time_conditions: + # hard code + time_condition_mask = ( + torch.zeros( + ( + batch_size, + int(math.sqrt(hidden_states.shape[1]) // 2), + int(2 * math.sqrt(hidden_states.shape[1])), + ) + ) + .bool() + .to(device=hidden_states.device) + ) + mel_latent_length = time_condition_mask.shape[-1] + time_start, time_end = ( + int(time_condition[0] // audio_length_in_s * mel_latent_length), + int(time_condition[1] // audio_length_in_s * mel_latent_length), + ) + + time_condition_mask[:, :, time_start:time_end] = True + time_condition_mask = time_condition_mask.flatten(-2).unsqueeze(-1).repeat(1, 1, 4) + if time_condition_masks is None: + time_condition_masks = time_condition_mask + else: + time_condition_masks = torch.cat([time_condition_masks, time_condition_mask], dim=-1) + + current_ip_hidden_states = rearrange(current_ip_hidden_states, "L B N C -> B (L N) C") + ip_key = to_k_ip(current_ip_hidden_states) + ip_value = to_v_ip(current_ip_hidden_states) + + ip_key = ip_key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + ip_value = ip_value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + time_condition_masks = time_condition_masks.unsqueeze(1).repeat(1, attn.heads, 1, 1) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + current_ip_hidden_states = F.scaled_dot_product_attention( + query, ip_key, ip_value, attn_mask=time_condition_masks, dropout_p=0.0, is_causal=False + ) + + current_ip_hidden_states = current_ip_hidden_states.transpose(1, 2).reshape( + batch_size, -1, attn.heads * head_dim + ) + current_ip_hidden_states = current_ip_hidden_states.to(query.dtype) + + hidden_states = hidden_states + scale * current_ip_hidden_states + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +class IPAdapterAttnProcessor2_0(torch.nn.Module): + r""" + Attention processor for IP-Adapter for PyTorch 2.0. + + Args: + hidden_size (`int`): + The hidden size of the attention layer. + cross_attention_dim (`int`): + The number of channels in the `encoder_hidden_states`. + num_tokens (`int`, `Tuple[int]` or `List[int]`, defaults to `(4,)`): + The context length of the image features. + scale (`float` or `List[float]`, defaults to 1.0): + the weight scale of image prompt. + """ + + def __init__(self, hidden_size, cross_attention_dim=None, num_tokens=(4,), scale=1.0): + super().__init__() + + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError( + f"{self.__class__.__name__} requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0." + ) + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + + if not isinstance(num_tokens, (tuple, list)): + num_tokens = [num_tokens] + self.num_tokens = num_tokens + + if not isinstance(scale, list): + scale = [scale] * len(num_tokens) + if len(scale) != len(num_tokens): + raise ValueError("`scale` should be a list of integers with the same length as `num_tokens`.") + self.scale = scale + self.to_k_ip = nn.ModuleList( + [nn.Linear(cross_attention_dim, hidden_size, bias=False) for _ in range(len(num_tokens))] + ) + self.to_v_ip = nn.ModuleList( + [nn.Linear(cross_attention_dim, hidden_size, bias=False) for _ in range(len(num_tokens))] + ) + + def __call__( + self, + attn: Attention, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + temb: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ip_adapter_masks: Optional[torch.FloatTensor] = None, + ): + residual = hidden_states + + # separate ip_hidden_states from encoder_hidden_states + if encoder_hidden_states is not None: + if isinstance(encoder_hidden_states, tuple): + encoder_hidden_states, ip_hidden_states = encoder_hidden_states + else: + deprecation_message = ( + "You have passed a tensor as `encoder_hidden_states`. This is deprecated and will be removed in a future release." + " Please make sure to update your script to pass `encoder_hidden_states` as a tuple to suppress this warning." + ) + deprecate("encoder_hidden_states not a tuple", "1.0.0", deprecation_message, standard_warn=False) + end_pos = encoder_hidden_states.shape[1] - self.num_tokens[0] + encoder_hidden_states, ip_hidden_states = ( + encoder_hidden_states[:, :end_pos, :], + [encoder_hidden_states[:, end_pos:, :]], + ) + + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + if ip_adapter_masks is not None: + if not isinstance(ip_adapter_masks, List): + # for backward compatibility, we accept `ip_adapter_mask` as a tensor of shape [num_ip_adapter, 1, height, width] + ip_adapter_masks = list(ip_adapter_masks.unsqueeze(1)) + if not (len(ip_adapter_masks) == len(self.scale) == len(ip_hidden_states)): + raise ValueError( + f"Length of ip_adapter_masks array ({len(ip_adapter_masks)}) must match " + f"length of self.scale array ({len(self.scale)}) and number of ip_hidden_states " + f"({len(ip_hidden_states)})" + ) + else: + for index, (mask, scale, ip_state) in enumerate(zip(ip_adapter_masks, self.scale, ip_hidden_states)): + if not isinstance(mask, torch.Tensor) or mask.ndim != 4: + raise ValueError( + "Each element of the ip_adapter_masks array should be a tensor with shape " + "[1, num_images_for_ip_adapter, height, width]." + " Please use `IPAdapterMaskProcessor` to preprocess your mask" + ) + if mask.shape[1] != ip_state.shape[1]: + raise ValueError( + f"Number of masks ({mask.shape[1]}) does not match " + f"number of ip images ({ip_state.shape[1]}) at index {index}" + ) + if isinstance(scale, list) and not len(scale) == mask.shape[1]: + raise ValueError( + f"Number of masks ({mask.shape[1]}) does not match " + f"number of scales ({len(scale)}) at index {index}" + ) + else: + ip_adapter_masks = [None] * len(self.scale) + + # for ip-adapter + for current_ip_hidden_states, scale, to_k_ip, to_v_ip, mask in zip( + ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip, ip_adapter_masks + ): + skip = False + if isinstance(scale, list): + if all(s == 0 for s in scale): + skip = True + elif scale == 0: + skip = True + if not skip: + ip_key = to_k_ip(current_ip_hidden_states) + ip_value = to_v_ip(current_ip_hidden_states) + + ip_key = ip_key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + ip_value = ip_value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + current_ip_hidden_states = F.scaled_dot_product_attention( + query, ip_key, ip_value, attn_mask=None, dropout_p=0.0, is_causal=False + ) + + current_ip_hidden_states = current_ip_hidden_states.transpose(1, 2).reshape( + batch_size, -1, attn.heads * head_dim + ) + current_ip_hidden_states = current_ip_hidden_states.to(query.dtype) + + hidden_states = hidden_states + scale * current_ip_hidden_states + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + + return hidden_states + + +LORA_ATTENTION_PROCESSORS = ( + LoRAAttnProcessor, + LoRAAttnProcessor2_0, + LoRAXFormersAttnProcessor, + LoRAAttnAddedKVProcessor, +) + +ADDED_KV_ATTENTION_PROCESSORS = ( + AttnAddedKVProcessor, + SlicedAttnAddedKVProcessor, + AttnAddedKVProcessor2_0, + XFormersAttnAddedKVProcessor, + LoRAAttnAddedKVProcessor, +) + +CROSS_ATTENTION_PROCESSORS = ( + AttnProcessor, + AttnProcessor2_0, + XFormersAttnProcessor, + SlicedAttnProcessor, + LoRAAttnProcessor, + LoRAAttnProcessor2_0, + LoRAXFormersAttnProcessor, + IPAdapterAttnProcessor, + IPAdapterAttnProcessor2_0, +) + +AttentionProcessor = Union[ + AttnProcessor, + AttnProcessor2_0, + FusedAttnProcessor2_0, + XFormersAttnProcessor, + SlicedAttnProcessor, + AttnAddedKVProcessor, + SlicedAttnAddedKVProcessor, + AttnAddedKVProcessor2_0, + XFormersAttnAddedKVProcessor, + CustomDiffusionAttnProcessor, + CustomDiffusionXFormersAttnProcessor, + CustomDiffusionAttnProcessor2_0, + # deprecated + LoRAAttnProcessor, + LoRAAttnProcessor2_0, + LoRAXFormersAttnProcessor, + LoRAAttnAddedKVProcessor, +] diff --git a/foleycrafter/models/auffusion/dual_transformer_2d.py b/foleycrafter/models/auffusion/dual_transformer_2d.py new file mode 100644 index 0000000..0256829 --- /dev/null +++ b/foleycrafter/models/auffusion/dual_transformer_2d.py @@ -0,0 +1,155 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +from torch import nn + +from .transformer_2d import Transformer2DModel, Transformer2DModelOutput + + +class DualTransformer2DModel(nn.Module): + """ + Dual transformer wrapper that combines two `Transformer2DModel`s for mixed inference. + + Parameters: + num_attention_heads (`int`, *optional*, defaults to 16): The number of heads to use for multi-head attention. + attention_head_dim (`int`, *optional*, defaults to 88): The number of channels in each head. + in_channels (`int`, *optional*): + Pass if the input is continuous. The number of channels in the input and output. + num_layers (`int`, *optional*, defaults to 1): The number of layers of Transformer blocks to use. + dropout (`float`, *optional*, defaults to 0.1): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The number of encoder_hidden_states dimensions to use. + sample_size (`int`, *optional*): Pass if the input is discrete. The width of the latent images. + Note that this is fixed at training time as it is used for learning a number of position embeddings. See + `ImagePositionalEmbeddings`. + num_vector_embeds (`int`, *optional*): + Pass if the input is discrete. The number of classes of the vector embeddings of the latent pixels. + Includes the class for the masked latent pixel. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm ( `int`, *optional*): Pass if at least one of the norm_layers is `AdaLayerNorm`. + The number of diffusion steps used during training. Note that this is fixed at training time as it is used + to learn a number of embeddings that are added to the hidden states. During inference, you can denoise for + up to but not more than steps than `num_embeds_ada_norm`. + attention_bias (`bool`, *optional*): + Configure if the TransformerBlocks' attention should contain a bias parameter. + """ + + def __init__( + self, + num_attention_heads: int = 16, + attention_head_dim: int = 88, + in_channels: Optional[int] = None, + num_layers: int = 1, + dropout: float = 0.0, + norm_num_groups: int = 32, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + sample_size: Optional[int] = None, + num_vector_embeds: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + ): + super().__init__() + self.transformers = nn.ModuleList( + [ + Transformer2DModel( + num_attention_heads=num_attention_heads, + attention_head_dim=attention_head_dim, + in_channels=in_channels, + num_layers=num_layers, + dropout=dropout, + norm_num_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + attention_bias=attention_bias, + sample_size=sample_size, + num_vector_embeds=num_vector_embeds, + activation_fn=activation_fn, + num_embeds_ada_norm=num_embeds_ada_norm, + ) + for _ in range(2) + ] + ) + + # Variables that can be set by a pipeline: + + # The ratio of transformer1 to transformer2's output states to be combined during inference + self.mix_ratio = 0.5 + + # The shape of `encoder_hidden_states` is expected to be + # `(batch_size, condition_lengths[0]+condition_lengths[1], num_features)` + self.condition_lengths = [77, 257] + + # Which transformer to use to encode which condition. + # E.g. `(1, 0)` means that we'll use `transformers[1](conditions[0])` and `transformers[0](conditions[1])` + self.transformer_index_for_condition = [1, 0] + + def forward( + self, + hidden_states, + encoder_hidden_states, + timestep=None, + attention_mask=None, + cross_attention_kwargs=None, + return_dict: bool = True, + ): + """ + Args: + hidden_states ( When discrete, `torch.LongTensor` of shape `(batch size, num latent pixels)`. + When continuous, `torch.FloatTensor` of shape `(batch size, channel, height, width)`): Input + hidden_states. + encoder_hidden_states ( `torch.LongTensor` of shape `(batch size, encoder_hidden_states dim)`, *optional*): + Conditional embeddings for cross attention layer. If not given, cross-attention defaults to + self-attention. + timestep ( `torch.long`, *optional*): + Optional timestep to be applied as an embedding in AdaLayerNorm's. Used to indicate denoising step. + attention_mask (`torch.FloatTensor`, *optional*): + Optional attention mask to be applied in Attention. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under + `self.processor` in + [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain tuple. + + Returns: + [`~models.transformer_2d.Transformer2DModelOutput`] or `tuple`: + [`~models.transformer_2d.Transformer2DModelOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + input_states = hidden_states + + encoded_states = [] + tokens_start = 0 + # attention_mask is not used yet + for i in range(2): + # for each of the two transformers, pass the corresponding condition tokens + condition_state = encoder_hidden_states[:, tokens_start : tokens_start + self.condition_lengths[i]] + transformer_index = self.transformer_index_for_condition[i] + encoded_state = self.transformers[transformer_index]( + input_states, + encoder_hidden_states=condition_state, + timestep=timestep, + cross_attention_kwargs=cross_attention_kwargs, + return_dict=False, + )[0] + encoded_states.append(encoded_state - input_states) + tokens_start += self.condition_lengths[i] + + output_states = encoded_states[0] * self.mix_ratio + encoded_states[1] * (1 - self.mix_ratio) + output_states = output_states + input_states + + if not return_dict: + return (output_states,) + + return Transformer2DModelOutput(sample=output_states) diff --git a/foleycrafter/models/auffusion/loaders/ip_adapter.py b/foleycrafter/models/auffusion/loaders/ip_adapter.py new file mode 100644 index 0000000..a081058 --- /dev/null +++ b/foleycrafter/models/auffusion/loaders/ip_adapter.py @@ -0,0 +1,524 @@ +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Dict, List, Optional, Union + +import torch +from huggingface_hub.utils import validate_hf_hub_args +from safetensors import safe_open + +from diffusers.models.modeling_utils import _LOW_CPU_MEM_USAGE_DEFAULT +from diffusers.utils import ( + _get_model_file, + is_accelerate_available, + is_torch_version, + is_transformers_available, + logging, +) + + +if is_transformers_available(): + from transformers import ( + CLIPImageProcessor, + CLIPVisionModelWithProjection, + ) + + from diffusers.models.attention_processor import ( + IPAdapterAttnProcessor, + ) + +from ..attention_processor import ( + IPAdapterAttnProcessor2_0, + VPTemporalAdapterAttnProcessor2_0, +) + + +logger = logging.get_logger(__name__) + + +class IPAdapterMixin: + """Mixin for handling IP Adapters.""" + + @validate_hf_hub_args + def load_ip_adapter( + self, + pretrained_model_name_or_path_or_dict: Union[str, List[str], Dict[str, torch.Tensor]], + subfolder: Union[str, List[str]], + weight_name: Union[str, List[str]], + image_encoder_folder: Optional[str] = "image_encoder", + **kwargs, + ): + """ + Parameters: + pretrained_model_name_or_path_or_dict (`str` or `List[str]` or `os.PathLike` or `List[os.PathLike]` or `dict` or `List[dict]`): + Can be either: + + - A string, the *model id* (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on + the Hub. + - A path to a *directory* (for example `./my_model_directory`) containing the model weights saved + with [`ModelMixin.save_pretrained`]. + - A [torch state + dict](https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict). + subfolder (`str` or `List[str]`): + The subfolder location of a model file within a larger model repository on the Hub or locally. + If a list is passed, it should have the same length as `weight_name`. + weight_name (`str` or `List[str]`): + The name of the weight file to load. If a list is passed, it should have the same length as + `weight_name`. + image_encoder_folder (`str`, *optional*, defaults to `image_encoder`): + The subfolder location of the image encoder within a larger model repository on the Hub or locally. + Pass `None` to not load the image encoder. If the image encoder is located in a folder inside `subfolder`, + you only need to pass the name of the folder that contains image encoder weights, e.g. `image_encoder_folder="image_encoder"`. + If the image encoder is located in a folder other than `subfolder`, you should pass the path to the folder that contains image encoder weights, + for example, `image_encoder_folder="different_subfolder/image_encoder"`. + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory where a downloaded pretrained model configuration is cached if the standard cache + is not used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to resume downloading the model weights and configuration files. If set to `False`, any + incompletely downloaded files are deleted. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + local_files_only (`bool`, *optional*, defaults to `False`): + Whether to only load local model weights and configuration files or not. If set to `True`, the model + won't be downloaded from the Hub. + token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from + `diffusers-cli login` (stored in `~/.huggingface`) is used. + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier + allowed by Git. + low_cpu_mem_usage (`bool`, *optional*, defaults to `True` if torch version >= 1.9.0 else `False`): + Speed up model loading only loading the pretrained weights and not initializing the weights. This also + tries to not use more than 1x model size in CPU memory (including peak memory) while loading the model. + Only supported for PyTorch >= 1.9.0. If you are using an older version of PyTorch, setting this + argument to `True` will raise an error. + """ + + # handle the list inputs for multiple IP Adapters + if not isinstance(weight_name, list): + weight_name = [weight_name] + + if not isinstance(pretrained_model_name_or_path_or_dict, list): + pretrained_model_name_or_path_or_dict = [pretrained_model_name_or_path_or_dict] + if len(pretrained_model_name_or_path_or_dict) == 1: + pretrained_model_name_or_path_or_dict = pretrained_model_name_or_path_or_dict * len(weight_name) + + if not isinstance(subfolder, list): + subfolder = [subfolder] + if len(subfolder) == 1: + subfolder = subfolder * len(weight_name) + + if len(weight_name) != len(pretrained_model_name_or_path_or_dict): + raise ValueError("`weight_name` and `pretrained_model_name_or_path_or_dict` must have the same length.") + + if len(weight_name) != len(subfolder): + raise ValueError("`weight_name` and `subfolder` must have the same length.") + + # Load the main state dict first. + cache_dir = kwargs.pop("cache_dir", None) + force_download = kwargs.pop("force_download", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", None) + token = kwargs.pop("token", None) + revision = kwargs.pop("revision", None) + low_cpu_mem_usage = kwargs.pop("low_cpu_mem_usage", _LOW_CPU_MEM_USAGE_DEFAULT) + + if low_cpu_mem_usage and not is_accelerate_available(): + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + user_agent = { + "file_type": "attn_procs_weights", + "framework": "pytorch", + } + state_dicts = [] + for pretrained_model_name_or_path_or_dict, weight_name, subfolder in zip( + pretrained_model_name_or_path_or_dict, weight_name, subfolder + ): + if not isinstance(pretrained_model_name_or_path_or_dict, dict): + model_file = _get_model_file( + pretrained_model_name_or_path_or_dict, + weights_name=weight_name, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + token=token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + if weight_name.endswith(".safetensors"): + state_dict = {"image_proj": {}, "ip_adapter": {}} + with safe_open(model_file, framework="pt", device="cpu") as f: + for key in f.keys(): + if key.startswith("image_proj."): + state_dict["image_proj"][key.replace("image_proj.", "")] = f.get_tensor(key) + elif key.startswith("ip_adapter."): + state_dict["ip_adapter"][key.replace("ip_adapter.", "")] = f.get_tensor(key) + else: + state_dict = torch.load(model_file, map_location="cpu") + else: + state_dict = pretrained_model_name_or_path_or_dict + + keys = list(state_dict.keys()) + if keys != ["image_proj", "ip_adapter"]: + raise ValueError("Required keys are (`image_proj` and `ip_adapter`) missing from the state dict.") + + state_dicts.append(state_dict) + + # load CLIP image encoder here if it has not been registered to the pipeline yet + if hasattr(self, "image_encoder") and getattr(self, "image_encoder", None) is None: + if image_encoder_folder is not None: + if not isinstance(pretrained_model_name_or_path_or_dict, dict): + logger.info(f"loading image_encoder from {pretrained_model_name_or_path_or_dict}") + if image_encoder_folder.count("/") == 0: + image_encoder_subfolder = Path(subfolder, image_encoder_folder).as_posix() + else: + image_encoder_subfolder = Path(image_encoder_folder).as_posix() + + image_encoder = CLIPVisionModelWithProjection.from_pretrained( + pretrained_model_name_or_path_or_dict, + subfolder=image_encoder_subfolder, + low_cpu_mem_usage=low_cpu_mem_usage, + ).to(self.device, dtype=self.dtype) + self.register_modules(image_encoder=image_encoder) + else: + raise ValueError( + "`image_encoder` cannot be loaded because `pretrained_model_name_or_path_or_dict` is a state dict." + ) + else: + logger.warning( + "image_encoder is not loaded since `image_encoder_folder=None` passed. You will not be able to use `ip_adapter_image` when calling the pipeline with IP-Adapter." + "Use `ip_adapter_image_embeds` to pass pre-generated image embedding instead." + ) + + # create feature extractor if it has not been registered to the pipeline yet + if hasattr(self, "feature_extractor") and getattr(self, "feature_extractor", None) is None: + feature_extractor = CLIPImageProcessor() + self.register_modules(feature_extractor=feature_extractor) + + # load ip-adapter into unet + unet = getattr(self, self.unet_name) if not hasattr(self, "unet") else self.unet + unet._load_ip_adapter_weights(state_dicts) + + def set_ip_adapter_scale(self, scale): + """ + Sets the conditioning scale between text and image. + + Example: + + ```py + pipeline.set_ip_adapter_scale(0.5) + ``` + """ + unet = getattr(self, self.unet_name) if not hasattr(self, "unet") else self.unet + for attn_processor in unet.attn_processors.values(): + if isinstance(attn_processor, (IPAdapterAttnProcessor, IPAdapterAttnProcessor2_0)): + if not isinstance(scale, list): + scale = [scale] * len(attn_processor.scale) + if len(attn_processor.scale) != len(scale): + raise ValueError( + f"`scale` should be a list of same length as the number if ip-adapters " + f"Expected {len(attn_processor.scale)} but got {len(scale)}." + ) + attn_processor.scale = scale + + def unload_ip_adapter(self): + """ + Unloads the IP Adapter weights + + Examples: + + ```python + >>> # Assuming `pipeline` is already loaded with the IP Adapter weights. + >>> pipeline.unload_ip_adapter() + >>> ... + ``` + """ + # remove CLIP image encoder + if hasattr(self, "image_encoder") and getattr(self, "image_encoder", None) is not None: + self.image_encoder = None + self.register_to_config(image_encoder=[None, None]) + + # remove feature extractor only when safety_checker is None as safety_checker uses + # the feature_extractor later + if not hasattr(self, "safety_checker"): + if hasattr(self, "feature_extractor") and getattr(self, "feature_extractor", None) is not None: + self.feature_extractor = None + self.register_to_config(feature_extractor=[None, None]) + + # remove hidden encoder + self.unet.encoder_hid_proj = None + self.config.encoder_hid_dim_type = None + + # restore original Unet attention processors layers + self.unet.set_default_attn_processor() + + +class VPAdapterMixin: + """Mixin for handling IP Adapters.""" + + @validate_hf_hub_args + def load_ip_adapter( + self, + pretrained_model_name_or_path_or_dict: Union[str, List[str], Dict[str, torch.Tensor]], + subfolder: Union[str, List[str]], + weight_name: Union[str, List[str]], + image_encoder_folder: Optional[str] = "image_encoder", + **kwargs, + ): + """ + Parameters: + pretrained_model_name_or_path_or_dict (`str` or `List[str]` or `os.PathLike` or `List[os.PathLike]` or `dict` or `List[dict]`): + Can be either: + + - A string, the *model id* (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on + the Hub. + - A path to a *directory* (for example `./my_model_directory`) containing the model weights saved + with [`ModelMixin.save_pretrained`]. + - A [torch state + dict](https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict). + subfolder (`str` or `List[str]`): + The subfolder location of a model file within a larger model repository on the Hub or locally. + If a list is passed, it should have the same length as `weight_name`. + weight_name (`str` or `List[str]`): + The name of the weight file to load. If a list is passed, it should have the same length as + `weight_name`. + image_encoder_folder (`str`, *optional*, defaults to `image_encoder`): + The subfolder location of the image encoder within a larger model repository on the Hub or locally. + Pass `None` to not load the image encoder. If the image encoder is located in a folder inside `subfolder`, + you only need to pass the name of the folder that contains image encoder weights, e.g. `image_encoder_folder="image_encoder"`. + If the image encoder is located in a folder other than `subfolder`, you should pass the path to the folder that contains image encoder weights, + for example, `image_encoder_folder="different_subfolder/image_encoder"`. + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory where a downloaded pretrained model configuration is cached if the standard cache + is not used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to resume downloading the model weights and configuration files. If set to `False`, any + incompletely downloaded files are deleted. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + local_files_only (`bool`, *optional*, defaults to `False`): + Whether to only load local model weights and configuration files or not. If set to `True`, the model + won't be downloaded from the Hub. + token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from + `diffusers-cli login` (stored in `~/.huggingface`) is used. + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier + allowed by Git. + low_cpu_mem_usage (`bool`, *optional*, defaults to `True` if torch version >= 1.9.0 else `False`): + Speed up model loading only loading the pretrained weights and not initializing the weights. This also + tries to not use more than 1x model size in CPU memory (including peak memory) while loading the model. + Only supported for PyTorch >= 1.9.0. If you are using an older version of PyTorch, setting this + argument to `True` will raise an error. + """ + + # handle the list inputs for multiple IP Adapters + if not isinstance(weight_name, list): + weight_name = [weight_name] + + if not isinstance(pretrained_model_name_or_path_or_dict, list): + pretrained_model_name_or_path_or_dict = [pretrained_model_name_or_path_or_dict] + if len(pretrained_model_name_or_path_or_dict) == 1: + pretrained_model_name_or_path_or_dict = pretrained_model_name_or_path_or_dict * len(weight_name) + + if not isinstance(subfolder, list): + subfolder = [subfolder] + if len(subfolder) == 1: + subfolder = subfolder * len(weight_name) + + if len(weight_name) != len(pretrained_model_name_or_path_or_dict): + raise ValueError("`weight_name` and `pretrained_model_name_or_path_or_dict` must have the same length.") + + if len(weight_name) != len(subfolder): + raise ValueError("`weight_name` and `subfolder` must have the same length.") + + # Load the main state dict first. + cache_dir = kwargs.pop("cache_dir", None) + force_download = kwargs.pop("force_download", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", None) + token = kwargs.pop("token", None) + revision = kwargs.pop("revision", None) + low_cpu_mem_usage = kwargs.pop("low_cpu_mem_usage", _LOW_CPU_MEM_USAGE_DEFAULT) + + if low_cpu_mem_usage and not is_accelerate_available(): + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + user_agent = { + "file_type": "attn_procs_weights", + "framework": "pytorch", + } + state_dicts = [] + for pretrained_model_name_or_path_or_dict, weight_name, subfolder in zip( + pretrained_model_name_or_path_or_dict, weight_name, subfolder + ): + if not isinstance(pretrained_model_name_or_path_or_dict, dict): + model_file = _get_model_file( + pretrained_model_name_or_path_or_dict, + weights_name=weight_name, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + token=token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + if weight_name.endswith(".safetensors"): + state_dict = {"image_proj": {}, "ip_adapter": {}} + with safe_open(model_file, framework="pt", device="cpu") as f: + for key in f.keys(): + if key.startswith("image_proj."): + state_dict["image_proj"][key.replace("image_proj.", "")] = f.get_tensor(key) + elif key.startswith("ip_adapter."): + state_dict["ip_adapter"][key.replace("ip_adapter.", "")] = f.get_tensor(key) + else: + state_dict = torch.load(model_file, map_location="cpu") + else: + state_dict = pretrained_model_name_or_path_or_dict + + keys = list(state_dict.keys()) + if keys != ["image_proj", "ip_adapter"]: + raise ValueError("Required keys are (`image_proj` and `ip_adapter`) missing from the state dict.") + + state_dicts.append(state_dict) + + # load CLIP image encoder here if it has not been registered to the pipeline yet + if hasattr(self, "image_encoder") and getattr(self, "image_encoder", None) is None: + if image_encoder_folder is not None: + if not isinstance(pretrained_model_name_or_path_or_dict, dict): + logger.info(f"loading image_encoder from {pretrained_model_name_or_path_or_dict}") + if image_encoder_folder.count("/") == 0: + image_encoder_subfolder = Path(subfolder, image_encoder_folder).as_posix() + else: + image_encoder_subfolder = Path(image_encoder_folder).as_posix() + + image_encoder = CLIPVisionModelWithProjection.from_pretrained( + pretrained_model_name_or_path_or_dict, + subfolder=image_encoder_subfolder, + low_cpu_mem_usage=low_cpu_mem_usage, + ).to(self.device, dtype=self.dtype) + self.register_modules(image_encoder=image_encoder) + else: + raise ValueError( + "`image_encoder` cannot be loaded because `pretrained_model_name_or_path_or_dict` is a state dict." + ) + else: + logger.warning( + "image_encoder is not loaded since `image_encoder_folder=None` passed. You will not be able to use `ip_adapter_image` when calling the pipeline with IP-Adapter." + "Use `ip_adapter_image_embeds` to pass pre-generated image embedding instead." + ) + + # create feature extractor if it has not been registered to the pipeline yet + if hasattr(self, "feature_extractor") and getattr(self, "feature_extractor", None) is None: + feature_extractor = CLIPImageProcessor() + self.register_modules(feature_extractor=feature_extractor) + + # load ip-adapter into unet + unet = getattr(self, self.unet_name) if not hasattr(self, "unet") else self.unet + unet._load_ip_adapter_weights_VPAdapter(state_dicts) + + def set_ip_adapter_scale(self, scale): + """ + Sets the conditioning scale between text and image. + + Example: + + ```py + pipeline.set_ip_adapter_scale(0.5) + ``` + """ + unet = getattr(self, self.unet_name) if not hasattr(self, "unet") else self.unet + for attn_processor in unet.attn_processors.values(): + if isinstance(attn_processor, (IPAdapterAttnProcessor, VPTemporalAdapterAttnProcessor2_0)): + if not isinstance(scale, list): + scale = [scale] * len(attn_processor.scale) + if len(attn_processor.scale) != len(scale): + raise ValueError( + f"`scale` should be a list of same length as the number if ip-adapters " + f"Expected {len(attn_processor.scale)} but got {len(scale)}." + ) + attn_processor.scale = scale + + def unload_ip_adapter(self): + """ + Unloads the IP Adapter weights + + Examples: + + ```python + >>> # Assuming `pipeline` is already loaded with the IP Adapter weights. + >>> pipeline.unload_ip_adapter() + >>> ... + ``` + """ + # remove CLIP image encoder + if hasattr(self, "image_encoder") and getattr(self, "image_encoder", None) is not None: + self.image_encoder = None + self.register_to_config(image_encoder=[None, None]) + + # remove feature extractor only when safety_checker is None as safety_checker uses + # the feature_extractor later + if not hasattr(self, "safety_checker"): + if hasattr(self, "feature_extractor") and getattr(self, "feature_extractor", None) is not None: + self.feature_extractor = None + self.register_to_config(feature_extractor=[None, None]) + + # remove hidden encoder + self.unet.encoder_hid_proj = None + self.config.encoder_hid_dim_type = None + + # restore original Unet attention processors layers + self.unet.set_default_attn_processor() diff --git a/foleycrafter/models/auffusion/loaders/unet.py b/foleycrafter/models/auffusion/loaders/unet.py new file mode 100644 index 0000000..95d1832 --- /dev/null +++ b/foleycrafter/models/auffusion/loaders/unet.py @@ -0,0 +1,992 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from collections import defaultdict +from contextlib import nullcontext +from functools import partial +from typing import Callable, Dict, List, Optional, Tuple, Union + +import safetensors +import torch +import torch.nn.functional as F +from huggingface_hub.utils import validate_hf_hub_args +from torch import nn + +from diffusers.loaders.utils import AttnProcsLayers +from diffusers.models.embeddings import ImageProjection +from diffusers.models.modeling_utils import _LOW_CPU_MEM_USAGE_DEFAULT, load_model_dict_into_meta +from diffusers.utils import ( + USE_PEFT_BACKEND, + _get_model_file, + delete_adapter_layers, + is_accelerate_available, + is_torch_version, + logging, + set_adapter_layers, + set_weights_and_activate_adapters, +) +from ..attention_processor import ( + AttnProcessor2_0, + IPAdapterAttnProcessor2_0, + VPTemporalAdapterAttnProcessor2_0, +) + + +if is_accelerate_available(): + from accelerate import init_empty_weights + from accelerate.hooks import AlignDevicesHook, CpuOffload, remove_hook_from_module + +logger = logging.get_logger(__name__) + + +class VPAdapterImageProjection(nn.Module): + def __init__(self, IPAdapterImageProjectionLayers: Union[List[nn.Module], Tuple[nn.Module]]): + super().__init__() + self.image_projection_layers = nn.ModuleList(IPAdapterImageProjectionLayers) + + def forward(self, image_embeds: List[torch.FloatTensor]): + projected_image_embeds = [] + + # currently, we accept `image_embeds` as + # 1. a tensor (deprecated) with shape [batch_size, embed_dim] or [batch_size, sequence_length, embed_dim] + # 2. list of `n` tensors where `n` is number of ip-adapters, each tensor can hae shape [batch_size, num_images, embed_dim] or [batch_size, num_images, sequence_length, embed_dim] + if not isinstance(image_embeds, list): + # deprecation_message = ( + # "You have passed a tensor as `image_embeds`.This is deprecated and will be removed in a future release." + # " Please make sure to update your script to pass `image_embeds` as a list of tensors to suppress this warning." + # ) + image_embeds = [image_embeds.unsqueeze(1)] + + if len(image_embeds) != len(self.image_projection_layers): + raise ValueError( + f"image_embeds must have the same length as image_projection_layers, got {len(image_embeds)} and {len(self.image_projection_layers)}" + ) + + for image_embed, image_projection_layer in zip(image_embeds, self.image_projection_layers): + image_embed = image_embed.squeeze(1) + batch_size, num_images = image_embed.shape[0], image_embed.shape[1] + image_embed = image_embed.reshape((batch_size * num_images,) + image_embed.shape[2:]) + image_embed = image_projection_layer(image_embed) + image_embed = image_embed.reshape((batch_size, num_images) + image_embed.shape[1:]) + + projected_image_embeds.append(image_embed) + + return projected_image_embeds + + +class MultiIPAdapterImageProjection(nn.Module): + def __init__(self, IPAdapterImageProjectionLayers: Union[List[nn.Module], Tuple[nn.Module]]): + super().__init__() + self.image_projection_layers = nn.ModuleList(IPAdapterImageProjectionLayers) + + def forward(self, image_embeds: List[torch.FloatTensor]): + projected_image_embeds = [] + + # currently, we accept `image_embeds` as + # 1. a tensor (deprecated) with shape [batch_size, embed_dim] or [batch_size, sequence_length, embed_dim] + # 2. list of `n` tensors where `n` is number of ip-adapters, each tensor can hae shape [batch_size, num_images, embed_dim] or [batch_size, num_images, sequence_length, embed_dim] + if not isinstance(image_embeds, list): + # deprecation_message = ( + # "You have passed a tensor as `image_embeds`.This is deprecated and will be removed in a future release." + # " Please make sure to update your script to pass `image_embeds` as a list of tensors to suppress this warning." + # ) + image_embeds = [image_embeds.unsqueeze(1)] + + if len(image_embeds) != len(self.image_projection_layers): + raise ValueError( + f"image_embeds must have the same length as image_projection_layers, got {len(image_embeds)} and {len(self.image_projection_layers)}" + ) + + for image_embed, image_projection_layer in zip(image_embeds, self.image_projection_layers): + batch_size, num_images = image_embed.shape[0], image_embed.shape[1] + image_embed = image_embed.reshape((batch_size * num_images,) + image_embed.shape[2:]) + image_embed = image_projection_layer(image_embed) + image_embed = image_embed.reshape((batch_size, num_images) + image_embed.shape[1:]) + + projected_image_embeds.append(image_embed) + + return projected_image_embeds + + +TEXT_ENCODER_NAME = "text_encoder" +UNET_NAME = "unet" + +LORA_WEIGHT_NAME = "pytorch_lora_weights.bin" +LORA_WEIGHT_NAME_SAFE = "pytorch_lora_weights.safetensors" + +CUSTOM_DIFFUSION_WEIGHT_NAME = "pytorch_custom_diffusion_weights.bin" +CUSTOM_DIFFUSION_WEIGHT_NAME_SAFE = "pytorch_custom_diffusion_weights.safetensors" + + +class UNet2DConditionLoadersMixin: + """ + Load LoRA layers into a [`UNet2DCondtionModel`]. + """ + + text_encoder_name = TEXT_ENCODER_NAME + unet_name = UNET_NAME + + @validate_hf_hub_args + def load_attn_procs(self, pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]], **kwargs): + r""" + Load pretrained attention processor layers into [`UNet2DConditionModel`]. Attention processor layers have to be + defined in + [`attention_processor.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py) + and be a `torch.nn.Module` class. + + Parameters: + pretrained_model_name_or_path_or_dict (`str` or `os.PathLike` or `dict`): + Can be either: + + - A string, the model id (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on + the Hub. + - A path to a directory (for example `./my_model_directory`) containing the model weights saved + with [`ModelMixin.save_pretrained`]. + - A [torch state + dict](https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict). + + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory where a downloaded pretrained model configuration is cached if the standard cache + is not used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to resume downloading the model weights and configuration files. If set to `False`, any + incompletely downloaded files are deleted. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + local_files_only (`bool`, *optional*, defaults to `False`): + Whether to only load local model weights and configuration files or not. If set to `True`, the model + won't be downloaded from the Hub. + token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from + `diffusers-cli login` (stored in `~/.huggingface`) is used. + low_cpu_mem_usage (`bool`, *optional*, defaults to `True` if torch version >= 1.9.0 else `False`): + Speed up model loading only loading the pretrained weights and not initializing the weights. This also + tries to not use more than 1x model size in CPU memory (including peak memory) while loading the model. + Only supported for PyTorch >= 1.9.0. If you are using an older version of PyTorch, setting this + argument to `True` will raise an error. + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier + allowed by Git. + subfolder (`str`, *optional*, defaults to `""`): + The subfolder location of a model file within a larger model repository on the Hub or locally. + mirror (`str`, *optional*): + Mirror source to resolve accessibility issues if you’re downloading a model in China. We do not + guarantee the timeliness or safety of the source, and you should refer to the mirror site for more + information. + + Example: + + ```py + from diffusers import AutoPipelineForText2Image + import torch + + pipeline = AutoPipelineForText2Image.from_pretrained( + "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16 + ).to("cuda") + pipeline.unet.load_attn_procs( + "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_name="cinematic" + ) + ``` + """ + from diffusers.models.attention_processor import CustomDiffusionAttnProcessor + from diffusers.models.lora import LoRACompatibleConv, LoRACompatibleLinear, LoRAConv2dLayer, LoRALinearLayer + + cache_dir = kwargs.pop("cache_dir", None) + force_download = kwargs.pop("force_download", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", None) + token = kwargs.pop("token", None) + revision = kwargs.pop("revision", None) + subfolder = kwargs.pop("subfolder", None) + weight_name = kwargs.pop("weight_name", None) + use_safetensors = kwargs.pop("use_safetensors", None) + low_cpu_mem_usage = kwargs.pop("low_cpu_mem_usage", _LOW_CPU_MEM_USAGE_DEFAULT) + # This value has the same meaning as the `--network_alpha` option in the kohya-ss trainer script. + # See https://github.com/darkstorm2150/sd-scripts/blob/main/docs/train_network_README-en.md#execute-learning + network_alphas = kwargs.pop("network_alphas", None) + + _pipeline = kwargs.pop("_pipeline", None) + + is_network_alphas_none = network_alphas is None + + allow_pickle = False + + if use_safetensors is None: + use_safetensors = True + allow_pickle = True + + user_agent = { + "file_type": "attn_procs_weights", + "framework": "pytorch", + } + + if low_cpu_mem_usage and not is_accelerate_available(): + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + model_file = None + if not isinstance(pretrained_model_name_or_path_or_dict, dict): + # Let's first try to load .safetensors weights + if (use_safetensors and weight_name is None) or ( + weight_name is not None and weight_name.endswith(".safetensors") + ): + try: + model_file = _get_model_file( + pretrained_model_name_or_path_or_dict, + weights_name=weight_name or LORA_WEIGHT_NAME_SAFE, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + token=token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + state_dict = safetensors.torch.load_file(model_file, device="cpu") + except IOError as e: + if not allow_pickle: + raise e + # try loading non-safetensors weights + pass + if model_file is None: + model_file = _get_model_file( + pretrained_model_name_or_path_or_dict, + weights_name=weight_name or LORA_WEIGHT_NAME, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + token=token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + state_dict = torch.load(model_file, map_location="cpu") + else: + state_dict = pretrained_model_name_or_path_or_dict + + # fill attn processors + lora_layers_list = [] + + is_lora = all(("lora" in k or k.endswith(".alpha")) for k in state_dict.keys()) and not USE_PEFT_BACKEND + is_custom_diffusion = any("custom_diffusion" in k for k in state_dict.keys()) + + if is_lora: + # correct keys + state_dict, network_alphas = self.convert_state_dict_legacy_attn_format(state_dict, network_alphas) + + if network_alphas is not None: + network_alphas_keys = list(network_alphas.keys()) + used_network_alphas_keys = set() + + lora_grouped_dict = defaultdict(dict) + mapped_network_alphas = {} + + all_keys = list(state_dict.keys()) + for key in all_keys: + value = state_dict.pop(key) + attn_processor_key, sub_key = ".".join(key.split(".")[:-3]), ".".join(key.split(".")[-3:]) + lora_grouped_dict[attn_processor_key][sub_key] = value + + # Create another `mapped_network_alphas` dictionary so that we can properly map them. + if network_alphas is not None: + for k in network_alphas_keys: + if k.replace(".alpha", "") in key: + mapped_network_alphas.update({attn_processor_key: network_alphas.get(k)}) + used_network_alphas_keys.add(k) + + if not is_network_alphas_none: + if len(set(network_alphas_keys) - used_network_alphas_keys) > 0: + raise ValueError( + f"The `network_alphas` has to be empty at this point but has the following keys \n\n {', '.join(network_alphas.keys())}" + ) + + if len(state_dict) > 0: + raise ValueError( + f"The `state_dict` has to be empty at this point but has the following keys \n\n {', '.join(state_dict.keys())}" + ) + + for key, value_dict in lora_grouped_dict.items(): + attn_processor = self + for sub_key in key.split("."): + attn_processor = getattr(attn_processor, sub_key) + + # Process non-attention layers, which don't have to_{k,v,q,out_proj}_lora layers + # or add_{k,v,q,out_proj}_proj_lora layers. + rank = value_dict["lora.down.weight"].shape[0] + + if isinstance(attn_processor, LoRACompatibleConv): + in_features = attn_processor.in_channels + out_features = attn_processor.out_channels + kernel_size = attn_processor.kernel_size + + ctx = init_empty_weights if low_cpu_mem_usage else nullcontext + with ctx(): + lora = LoRAConv2dLayer( + in_features=in_features, + out_features=out_features, + rank=rank, + kernel_size=kernel_size, + stride=attn_processor.stride, + padding=attn_processor.padding, + network_alpha=mapped_network_alphas.get(key), + ) + elif isinstance(attn_processor, LoRACompatibleLinear): + ctx = init_empty_weights if low_cpu_mem_usage else nullcontext + with ctx(): + lora = LoRALinearLayer( + attn_processor.in_features, + attn_processor.out_features, + rank, + mapped_network_alphas.get(key), + ) + else: + raise ValueError(f"Module {key} is not a LoRACompatibleConv or LoRACompatibleLinear module.") + + value_dict = {k.replace("lora.", ""): v for k, v in value_dict.items()} + lora_layers_list.append((attn_processor, lora)) + + if low_cpu_mem_usage: + device = next(iter(value_dict.values())).device + dtype = next(iter(value_dict.values())).dtype + load_model_dict_into_meta(lora, value_dict, device=device, dtype=dtype) + else: + lora.load_state_dict(value_dict) + + elif is_custom_diffusion: + attn_processors = {} + custom_diffusion_grouped_dict = defaultdict(dict) + for key, value in state_dict.items(): + if len(value) == 0: + custom_diffusion_grouped_dict[key] = {} + else: + if "to_out" in key: + attn_processor_key, sub_key = ".".join(key.split(".")[:-3]), ".".join(key.split(".")[-3:]) + else: + attn_processor_key, sub_key = ".".join(key.split(".")[:-2]), ".".join(key.split(".")[-2:]) + custom_diffusion_grouped_dict[attn_processor_key][sub_key] = value + + for key, value_dict in custom_diffusion_grouped_dict.items(): + if len(value_dict) == 0: + attn_processors[key] = CustomDiffusionAttnProcessor( + train_kv=False, train_q_out=False, hidden_size=None, cross_attention_dim=None + ) + else: + cross_attention_dim = value_dict["to_k_custom_diffusion.weight"].shape[1] + hidden_size = value_dict["to_k_custom_diffusion.weight"].shape[0] + train_q_out = True if "to_q_custom_diffusion.weight" in value_dict else False + attn_processors[key] = CustomDiffusionAttnProcessor( + train_kv=True, + train_q_out=train_q_out, + hidden_size=hidden_size, + cross_attention_dim=cross_attention_dim, + ) + attn_processors[key].load_state_dict(value_dict) + elif USE_PEFT_BACKEND: + # In that case we have nothing to do as loading the adapter weights is already handled above by `set_peft_model_state_dict` + # on the Unet + pass + else: + raise ValueError( + f"{model_file} does not seem to be in the correct format expected by LoRA or Custom Diffusion training." + ) + + # + + def convert_state_dict_legacy_attn_format(self, state_dict, network_alphas): + is_new_lora_format = all( + key.startswith(self.unet_name) or key.startswith(self.text_encoder_name) for key in state_dict.keys() + ) + if is_new_lora_format: + # Strip the `"unet"` prefix. + is_text_encoder_present = any(key.startswith(self.text_encoder_name) for key in state_dict.keys()) + if is_text_encoder_present: + warn_message = "The state_dict contains LoRA params corresponding to the text encoder which are not being used here. To use both UNet and text encoder related LoRA params, use [`pipe.load_lora_weights()`](https://huggingface.co/docs/diffusers/main/en/api/loaders#diffusers.loaders.LoraLoaderMixin.load_lora_weights)." + logger.warn(warn_message) + unet_keys = [k for k in state_dict.keys() if k.startswith(self.unet_name)] + state_dict = {k.replace(f"{self.unet_name}.", ""): v for k, v in state_dict.items() if k in unet_keys} + + # change processor format to 'pure' LoRACompatibleLinear format + if any("processor" in k.split(".") for k in state_dict.keys()): + + def format_to_lora_compatible(key): + if "processor" not in key.split("."): + return key + return key.replace(".processor", "").replace("to_out_lora", "to_out.0.lora").replace("_lora", ".lora") + + state_dict = {format_to_lora_compatible(k): v for k, v in state_dict.items()} + + if network_alphas is not None: + network_alphas = {format_to_lora_compatible(k): v for k, v in network_alphas.items()} + return state_dict, network_alphas + + def save_attn_procs( + self, + save_directory: Union[str, os.PathLike], + is_main_process: bool = True, + weight_name: str = None, + save_function: Callable = None, + safe_serialization: bool = True, + **kwargs, + ): + r""" + Save attention processor layers to a directory so that it can be reloaded with the + [`~loaders.UNet2DConditionLoadersMixin.load_attn_procs`] method. + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to save an attention processor to (will be created if it doesn't exist). + is_main_process (`bool`, *optional*, defaults to `True`): + Whether the process calling this is the main process or not. Useful during distributed training and you + need to call this function on all processes. In this case, set `is_main_process=True` only on the main + process to avoid race conditions. + save_function (`Callable`): + The function to use to save the state dictionary. Useful during distributed training when you need to + replace `torch.save` with another method. Can be configured with the environment variable + `DIFFUSERS_SAVE_MODE`. + safe_serialization (`bool`, *optional*, defaults to `True`): + Whether to save the model using `safetensors` or with `pickle`. + + Example: + + ```py + import torch + from diffusers import DiffusionPipeline + + pipeline = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + torch_dtype=torch.float16, + ).to("cuda") + pipeline.unet.load_attn_procs("path-to-save-model", weight_name="pytorch_custom_diffusion_weights.bin") + pipeline.unet.save_attn_procs("path-to-save-model", weight_name="pytorch_custom_diffusion_weights.bin") + ``` + """ + from diffusers.models.attention_processor import ( + CustomDiffusionAttnProcessor, + CustomDiffusionAttnProcessor2_0, + CustomDiffusionXFormersAttnProcessor, + ) + + if os.path.isfile(save_directory): + logger.error(f"Provided path ({save_directory}) should be a directory, not a file") + return + + if save_function is None: + if safe_serialization: + + def save_function(weights, filename): + return safetensors.torch.save_file(weights, filename, metadata={"format": "pt"}) + + else: + save_function = torch.save + + os.makedirs(save_directory, exist_ok=True) + + is_custom_diffusion = any( + isinstance( + x, + (CustomDiffusionAttnProcessor, CustomDiffusionAttnProcessor2_0, CustomDiffusionXFormersAttnProcessor), + ) + for (_, x) in self.attn_processors.items() + ) + if is_custom_diffusion: + model_to_save = AttnProcsLayers( + { + y: x + for (y, x) in self.attn_processors.items() + if isinstance( + x, + ( + CustomDiffusionAttnProcessor, + CustomDiffusionAttnProcessor2_0, + CustomDiffusionXFormersAttnProcessor, + ), + ) + } + ) + state_dict = model_to_save.state_dict() + for name, attn in self.attn_processors.items(): + if len(attn.state_dict()) == 0: + state_dict[name] = {} + else: + model_to_save = AttnProcsLayers(self.attn_processors) + state_dict = model_to_save.state_dict() + + if weight_name is None: + if safe_serialization: + weight_name = CUSTOM_DIFFUSION_WEIGHT_NAME_SAFE if is_custom_diffusion else LORA_WEIGHT_NAME_SAFE + else: + weight_name = CUSTOM_DIFFUSION_WEIGHT_NAME if is_custom_diffusion else LORA_WEIGHT_NAME + + # Save the model + save_function(state_dict, os.path.join(save_directory, weight_name)) + logger.info(f"Model weights saved in {os.path.join(save_directory, weight_name)}") + + def fuse_lora(self, lora_scale=1.0, safe_fusing=False, adapter_names=None): + self.lora_scale = lora_scale + self._safe_fusing = safe_fusing + self.apply(partial(self._fuse_lora_apply, adapter_names=adapter_names)) + + def _fuse_lora_apply(self, module, adapter_names=None): + if not USE_PEFT_BACKEND: + if hasattr(module, "_fuse_lora"): + module._fuse_lora(self.lora_scale, self._safe_fusing) + + if adapter_names is not None: + raise ValueError( + "The `adapter_names` argument is not supported in your environment. Please switch" + " to PEFT backend to use this argument by installing latest PEFT and transformers." + " `pip install -U peft transformers`" + ) + + def unfuse_lora(self): + self.apply(self._unfuse_lora_apply) + + def _unfuse_lora_apply(self, module): + if not USE_PEFT_BACKEND: + if hasattr(module, "_unfuse_lora"): + module._unfuse_lora() + + def set_adapters( + self, + adapter_names: Union[List[str], str], + weights: Optional[Union[List[float], float]] = None, + ): + """ + Set the currently active adapters for use in the UNet. + + Args: + adapter_names (`List[str]` or `str`): + The names of the adapters to use. + adapter_weights (`Union[List[float], float]`, *optional*): + The adapter(s) weights to use with the UNet. If `None`, the weights are set to `1.0` for all the + adapters. + + Example: + + ```py + from diffusers import AutoPipelineForText2Image + import torch + + pipeline = AutoPipelineForText2Image.from_pretrained( + "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16 + ).to("cuda") + pipeline.load_lora_weights( + "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_name="cinematic" + ) + pipeline.load_lora_weights("nerijs/pixel-art-xl", weight_name="pixel-art-xl.safetensors", adapter_name="pixel") + pipeline.set_adapters(["cinematic", "pixel"], adapter_weights=[0.5, 0.5]) + ``` + """ + if not USE_PEFT_BACKEND: + raise ValueError("PEFT backend is required for `set_adapters()`.") + + adapter_names = [adapter_names] if isinstance(adapter_names, str) else adapter_names + + if weights is None: + weights = [1.0] * len(adapter_names) + elif isinstance(weights, float): + weights = [weights] * len(adapter_names) + + if len(adapter_names) != len(weights): + raise ValueError( + f"Length of adapter names {len(adapter_names)} is not equal to the length of their weights {len(weights)}." + ) + + set_weights_and_activate_adapters(self, adapter_names, weights) + + def disable_lora(self): + """ + Disable the UNet's active LoRA layers. + + Example: + + ```py + from diffusers import AutoPipelineForText2Image + import torch + + pipeline = AutoPipelineForText2Image.from_pretrained( + "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16 + ).to("cuda") + pipeline.load_lora_weights( + "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_name="cinematic" + ) + pipeline.disable_lora() + ``` + """ + if not USE_PEFT_BACKEND: + raise ValueError("PEFT backend is required for this method.") + set_adapter_layers(self, enabled=False) + + def enable_lora(self): + """ + Enable the UNet's active LoRA layers. + + Example: + + ```py + from diffusers import AutoPipelineForText2Image + import torch + + pipeline = AutoPipelineForText2Image.from_pretrained( + "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16 + ).to("cuda") + pipeline.load_lora_weights( + "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_name="cinematic" + ) + pipeline.enable_lora() + ``` + """ + if not USE_PEFT_BACKEND: + raise ValueError("PEFT backend is required for this method.") + set_adapter_layers(self, enabled=True) + + def delete_adapters(self, adapter_names: Union[List[str], str]): + """ + Delete an adapter's LoRA layers from the UNet. + + Args: + adapter_names (`Union[List[str], str]`): + The names (single string or list of strings) of the adapter to delete. + + Example: + + ```py + from diffusers import AutoPipelineForText2Image + import torch + + pipeline = AutoPipelineForText2Image.from_pretrained( + "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16 + ).to("cuda") + pipeline.load_lora_weights( + "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_names="cinematic" + ) + pipeline.delete_adapters("cinematic") + ``` + """ + if not USE_PEFT_BACKEND: + raise ValueError("PEFT backend is required for this method.") + + if isinstance(adapter_names, str): + adapter_names = [adapter_names] + + for adapter_name in adapter_names: + delete_adapter_layers(self, adapter_name) + + # Pop also the corresponding adapter from the config + if hasattr(self, "peft_config"): + self.peft_config.pop(adapter_name, None) + + def _convert_ip_adapter_image_proj_to_diffusers(self, state_dict, low_cpu_mem_usage=False): + if low_cpu_mem_usage: + if is_accelerate_available(): + from accelerate import init_empty_weights + + else: + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + updated_state_dict = {} + image_projection = None + init_context = init_empty_weights if low_cpu_mem_usage else nullcontext + + if "proj.weight" in state_dict: + # IP-Adapter + num_image_text_embeds = 4 + clip_embeddings_dim = state_dict["proj.weight"].shape[-1] + cross_attention_dim = state_dict["proj.weight"].shape[0] // num_image_text_embeds + + with init_context(): + image_projection = ImageProjection( + cross_attention_dim=cross_attention_dim, + image_embed_dim=clip_embeddings_dim, + num_image_text_embeds=num_image_text_embeds, + ) + + for key, value in state_dict.items(): + diffusers_name = key.replace("proj", "image_embeds") + updated_state_dict[diffusers_name] = value + + if not low_cpu_mem_usage: + image_projection.load_state_dict(updated_state_dict) + else: + load_model_dict_into_meta(image_projection, updated_state_dict, device=self.device, dtype=self.dtype) + + return image_projection + + def _convert_ip_adapter_attn_to_diffusers_VPAdapter(self, state_dicts, low_cpu_mem_usage=False): + from diffusers.models.attention_processor import ( + AttnProcessor, + IPAdapterAttnProcessor, + ) + + if low_cpu_mem_usage: + if is_accelerate_available(): + from accelerate import init_empty_weights + + else: + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + # set ip-adapter cross-attention processors & load state_dict + attn_procs = {} + key_id = 1 + init_context = init_empty_weights if low_cpu_mem_usage else nullcontext + for name in self.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else self.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = self.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(self.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = self.config.block_out_channels[block_id] + + if cross_attention_dim is None or "motion_modules" in name or "fuser" in name: + attn_processor_class = ( + AttnProcessor2_0 if hasattr(F, "scaled_dot_product_attention") else AttnProcessor + ) + attn_procs[name] = attn_processor_class() + else: + attn_processor_class = ( + VPTemporalAdapterAttnProcessor2_0 + if hasattr(F, "scaled_dot_product_attention") + else IPAdapterAttnProcessor + ) + num_image_text_embeds = [] + for state_dict in state_dicts: + if "proj.weight" in state_dict["image_proj"]: + # IP-Adapter + num_image_text_embeds += [4] + elif "proj.3.weight" in state_dict["image_proj"]: + # IP-Adapter Full Face + num_image_text_embeds += [257] # 256 CLIP tokens + 1 CLS token + else: + # IP-Adapter Plus + num_image_text_embeds += [state_dict["image_proj"]["latents"].shape[1]] + + with init_context(): + attn_procs[name] = attn_processor_class( + hidden_size=hidden_size, + cross_attention_dim=cross_attention_dim, + scale=1.0, + num_tokens=num_image_text_embeds, + ) + + value_dict = {} + for i, state_dict in enumerate(state_dicts): + value_dict.update({f"to_k_ip.{i}.weight": state_dict["ip_adapter"][f"{key_id}.to_k_ip.weight"]}) + value_dict.update({f"to_v_ip.{i}.weight": state_dict["ip_adapter"][f"{key_id}.to_v_ip.weight"]}) + + if not low_cpu_mem_usage: + attn_procs[name].load_state_dict(value_dict) + else: + device = next(iter(value_dict.values())).device + dtype = next(iter(value_dict.values())).dtype + load_model_dict_into_meta(attn_procs[name], value_dict, device=device, dtype=dtype) + + key_id += 2 + + return attn_procs + + def _convert_ip_adapter_attn_to_diffusers(self, state_dicts, low_cpu_mem_usage=False): + from diffusers.models.attention_processor import ( + AttnProcessor, + IPAdapterAttnProcessor, + ) + + if low_cpu_mem_usage: + if is_accelerate_available(): + from accelerate import init_empty_weights + + else: + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + # set ip-adapter cross-attention processors & load state_dict + attn_procs = {} + key_id = 1 + init_context = init_empty_weights if low_cpu_mem_usage else nullcontext + for name in self.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else self.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = self.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(self.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = self.config.block_out_channels[block_id] + + if cross_attention_dim is None or "motion_modules" in name or "fuser" in name: + attn_processor_class = ( + AttnProcessor2_0 if hasattr(F, "scaled_dot_product_attention") else AttnProcessor + ) + attn_procs[name] = attn_processor_class() + else: + attn_processor_class = ( + IPAdapterAttnProcessor2_0 if hasattr(F, "scaled_dot_product_attention") else IPAdapterAttnProcessor + ) + num_image_text_embeds = [] + for state_dict in state_dicts: + if "proj.weight" in state_dict["image_proj"]: + # IP-Adapter + num_image_text_embeds += [4] + elif "proj.3.weight" in state_dict["image_proj"]: + # IP-Adapter Full Face + num_image_text_embeds += [257] # 256 CLIP tokens + 1 CLS token + else: + # IP-Adapter Plus + num_image_text_embeds += [state_dict["image_proj"]["latents"].shape[1]] + + with init_context(): + attn_procs[name] = attn_processor_class( + hidden_size=hidden_size, + cross_attention_dim=cross_attention_dim, + scale=1.0, + num_tokens=num_image_text_embeds, + ) + + value_dict = {} + for i, state_dict in enumerate(state_dicts): + value_dict.update({f"to_k_ip.{i}.weight": state_dict["ip_adapter"][f"{key_id}.to_k_ip.weight"]}) + value_dict.update({f"to_v_ip.{i}.weight": state_dict["ip_adapter"][f"{key_id}.to_v_ip.weight"]}) + + if not low_cpu_mem_usage: + attn_procs[name].load_state_dict(value_dict) + else: + device = next(iter(value_dict.values())).device + dtype = next(iter(value_dict.values())).dtype + load_model_dict_into_meta(attn_procs[name], value_dict, device=device, dtype=dtype) + + key_id += 2 + + return attn_procs + + def _load_ip_adapter_weights(self, state_dicts, low_cpu_mem_usage=False): + attn_procs = self._convert_ip_adapter_attn_to_diffusers(state_dicts, low_cpu_mem_usage=low_cpu_mem_usage) + self.set_attn_processor(attn_procs) + + # convert IP-Adapter Image Projection layers to diffusers + image_projection_layers = [] + for state_dict in state_dicts: + image_projection_layer = self._convert_ip_adapter_image_proj_to_diffusers( + state_dict["image_proj"], low_cpu_mem_usage=low_cpu_mem_usage + ) + image_projection_layers.append(image_projection_layer) + + self.encoder_hid_proj = MultiIPAdapterImageProjection(image_projection_layers) + self.config.encoder_hid_dim_type = "ip_image_proj" + + self.to(dtype=self.dtype, device=self.device) + + def _load_ip_adapter_weights_VPAdapter(self, state_dicts, low_cpu_mem_usage=False): + attn_procs = self._convert_ip_adapter_attn_to_diffusers_VPAdapter( + state_dicts, low_cpu_mem_usage=low_cpu_mem_usage + ) + self.set_attn_processor(attn_procs) + + # convert IP-Adapter Image Projection layers to diffusers + image_projection_layers = [] + for state_dict in state_dicts: + image_projection_layer = self._convert_ip_adapter_image_proj_to_diffusers( + state_dict["image_proj"], low_cpu_mem_usage=low_cpu_mem_usage + ) + image_projection_layers.append(image_projection_layer) + + self.encoder_hid_proj = VPAdapterImageProjection(image_projection_layers) + self.config.encoder_hid_dim_type = "ip_image_proj" + + self.to(dtype=self.dtype, device=self.device) diff --git a/foleycrafter/models/auffusion/resnet.py b/foleycrafter/models/auffusion/resnet.py new file mode 100644 index 0000000..f0537b1 --- /dev/null +++ b/foleycrafter/models/auffusion/resnet.py @@ -0,0 +1,685 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# `TemporalConvLayer` Copyright 2023 Alibaba DAMO-VILAB, The ModelScope Team and The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import partial +from typing import Optional, Tuple, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from diffusers.models.activations import get_activation +from diffusers.models.downsampling import ( # noqa + Downsample1D, + Downsample2D, + FirDownsample2D, + KDownsample2D, + downsample_2d, +) +from diffusers.models.lora import LoRACompatibleConv, LoRACompatibleLinear +from diffusers.models.normalization import AdaGroupNorm +from diffusers.models.upsampling import ( # noqa + FirUpsample2D, + KUpsample2D, + Upsample1D, + Upsample2D, + upfirdn2d_native, + upsample_2d, +) +from diffusers.utils import USE_PEFT_BACKEND +from .attention_processor import SpatialNorm + + +class ResnetBlock2D(nn.Module): + r""" + A Resnet block. + + Parameters: + in_channels (`int`): The number of channels in the input. + out_channels (`int`, *optional*, default to be `None`): + The number of output channels for the first conv2d layer. If None, same as `in_channels`. + dropout (`float`, *optional*, defaults to `0.0`): The dropout probability to use. + temb_channels (`int`, *optional*, default to `512`): the number of channels in timestep embedding. + groups (`int`, *optional*, default to `32`): The number of groups to use for the first normalization layer. + groups_out (`int`, *optional*, default to None): + The number of groups to use for the second normalization layer. if set to None, same as `groups`. + eps (`float`, *optional*, defaults to `1e-6`): The epsilon to use for the normalization. + non_linearity (`str`, *optional*, default to `"swish"`): the activation function to use. + time_embedding_norm (`str`, *optional*, default to `"default"` ): Time scale shift config. + By default, apply timestep embedding conditioning with a simple shift mechanism. Choose "scale_shift" or + "ada_group" for a stronger conditioning with scale and shift. + kernel (`torch.FloatTensor`, optional, default to None): FIR filter, see + [`~models.resnet.FirUpsample2D`] and [`~models.resnet.FirDownsample2D`]. + output_scale_factor (`float`, *optional*, default to be `1.0`): the scale factor to use for the output. + use_in_shortcut (`bool`, *optional*, default to `True`): + If `True`, add a 1x1 nn.conv2d layer for skip-connection. + up (`bool`, *optional*, default to `False`): If `True`, add an upsample layer. + down (`bool`, *optional*, default to `False`): If `True`, add a downsample layer. + conv_shortcut_bias (`bool`, *optional*, default to `True`): If `True`, adds a learnable bias to the + `conv_shortcut` output. + conv_2d_out_channels (`int`, *optional*, default to `None`): the number of channels in the output. + If None, same as `out_channels`. + """ + + def __init__( + self, + *, + in_channels: int, + out_channels: Optional[int] = None, + conv_shortcut: bool = False, + dropout: float = 0.0, + temb_channels: int = 512, + groups: int = 32, + groups_out: Optional[int] = None, + pre_norm: bool = True, + eps: float = 1e-6, + non_linearity: str = "swish", + skip_time_act: bool = False, + time_embedding_norm: str = "default", # default, scale_shift, ada_group, spatial + kernel: Optional[torch.FloatTensor] = None, + output_scale_factor: float = 1.0, + use_in_shortcut: Optional[bool] = None, + up: bool = False, + down: bool = False, + conv_shortcut_bias: bool = True, + conv_2d_out_channels: Optional[int] = None, + ): + super().__init__() + self.pre_norm = pre_norm + self.pre_norm = True + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + self.use_conv_shortcut = conv_shortcut + self.up = up + self.down = down + self.output_scale_factor = output_scale_factor + self.time_embedding_norm = time_embedding_norm + self.skip_time_act = skip_time_act + + linear_cls = nn.Linear if USE_PEFT_BACKEND else LoRACompatibleLinear + conv_cls = nn.Conv2d if USE_PEFT_BACKEND else LoRACompatibleConv + + if groups_out is None: + groups_out = groups + + if self.time_embedding_norm == "ada_group": + self.norm1 = AdaGroupNorm(temb_channels, in_channels, groups, eps=eps) + elif self.time_embedding_norm == "spatial": + self.norm1 = SpatialNorm(in_channels, temb_channels) + else: + self.norm1 = torch.nn.GroupNorm(num_groups=groups, num_channels=in_channels, eps=eps, affine=True) + + self.conv1 = conv_cls(in_channels, out_channels, kernel_size=3, stride=1, padding=1) + + if temb_channels is not None: + if self.time_embedding_norm == "default": + self.time_emb_proj = linear_cls(temb_channels, out_channels) + elif self.time_embedding_norm == "scale_shift": + self.time_emb_proj = linear_cls(temb_channels, 2 * out_channels) + elif self.time_embedding_norm == "ada_group" or self.time_embedding_norm == "spatial": + self.time_emb_proj = None + else: + raise ValueError(f"unknown time_embedding_norm : {self.time_embedding_norm} ") + else: + self.time_emb_proj = None + + if self.time_embedding_norm == "ada_group": + self.norm2 = AdaGroupNorm(temb_channels, out_channels, groups_out, eps=eps) + elif self.time_embedding_norm == "spatial": + self.norm2 = SpatialNorm(out_channels, temb_channels) + else: + self.norm2 = torch.nn.GroupNorm(num_groups=groups_out, num_channels=out_channels, eps=eps, affine=True) + + self.dropout = torch.nn.Dropout(dropout) + conv_2d_out_channels = conv_2d_out_channels or out_channels + self.conv2 = conv_cls(out_channels, conv_2d_out_channels, kernel_size=3, stride=1, padding=1) + + self.nonlinearity = get_activation(non_linearity) + + self.upsample = self.downsample = None + if self.up: + if kernel == "fir": + fir_kernel = (1, 3, 3, 1) + self.upsample = lambda x: upsample_2d(x, kernel=fir_kernel) + elif kernel == "sde_vp": + self.upsample = partial(F.interpolate, scale_factor=2.0, mode="nearest") + else: + self.upsample = Upsample2D(in_channels, use_conv=False) + elif self.down: + if kernel == "fir": + fir_kernel = (1, 3, 3, 1) + self.downsample = lambda x: downsample_2d(x, kernel=fir_kernel) + elif kernel == "sde_vp": + self.downsample = partial(F.avg_pool2d, kernel_size=2, stride=2) + else: + self.downsample = Downsample2D(in_channels, use_conv=False, padding=1, name="op") + + self.use_in_shortcut = self.in_channels != conv_2d_out_channels if use_in_shortcut is None else use_in_shortcut + + self.conv_shortcut = None + if self.use_in_shortcut: + self.conv_shortcut = conv_cls( + in_channels, + conv_2d_out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=conv_shortcut_bias, + ) + + def forward( + self, + input_tensor: torch.FloatTensor, + temb: torch.FloatTensor, + scale: float = 1.0, + ) -> torch.FloatTensor: + hidden_states = input_tensor + + if self.time_embedding_norm == "ada_group" or self.time_embedding_norm == "spatial": + hidden_states = self.norm1(hidden_states, temb) + else: + hidden_states = self.norm1(hidden_states) + + hidden_states = self.nonlinearity(hidden_states) + + if self.upsample is not None: + # upsample_nearest_nhwc fails with large batch sizes. see https://github.com/huggingface/diffusers/issues/984 + if hidden_states.shape[0] >= 64: + input_tensor = input_tensor.contiguous() + hidden_states = hidden_states.contiguous() + input_tensor = ( + self.upsample(input_tensor, scale=scale) + if isinstance(self.upsample, Upsample2D) + else self.upsample(input_tensor) + ) + hidden_states = ( + self.upsample(hidden_states, scale=scale) + if isinstance(self.upsample, Upsample2D) + else self.upsample(hidden_states) + ) + elif self.downsample is not None: + input_tensor = ( + self.downsample(input_tensor, scale=scale) + if isinstance(self.downsample, Downsample2D) + else self.downsample(input_tensor) + ) + hidden_states = ( + self.downsample(hidden_states, scale=scale) + if isinstance(self.downsample, Downsample2D) + else self.downsample(hidden_states) + ) + + hidden_states = self.conv1(hidden_states, scale) if not USE_PEFT_BACKEND else self.conv1(hidden_states) + + if self.time_emb_proj is not None: + if not self.skip_time_act: + temb = self.nonlinearity(temb) + temb = ( + self.time_emb_proj(temb, scale)[:, :, None, None] + if not USE_PEFT_BACKEND + # NOTE: Maybe we can use different prompt in different time + else self.time_emb_proj(temb)[:, :, None, None] + ) + + if temb is not None and self.time_embedding_norm == "default": + hidden_states = hidden_states + temb + + if self.time_embedding_norm == "ada_group" or self.time_embedding_norm == "spatial": + hidden_states = self.norm2(hidden_states, temb) + else: + hidden_states = self.norm2(hidden_states) + + if temb is not None and self.time_embedding_norm == "scale_shift": + scale, shift = torch.chunk(temb, 2, dim=1) + hidden_states = hidden_states * (1 + scale) + shift + + hidden_states = self.nonlinearity(hidden_states) + + hidden_states = self.dropout(hidden_states) + hidden_states = self.conv2(hidden_states, scale) if not USE_PEFT_BACKEND else self.conv2(hidden_states) + + if self.conv_shortcut is not None: + input_tensor = ( + self.conv_shortcut(input_tensor, scale) if not USE_PEFT_BACKEND else self.conv_shortcut(input_tensor) + ) + + output_tensor = (input_tensor + hidden_states) / self.output_scale_factor + + return output_tensor + + +# unet_rl.py +def rearrange_dims(tensor: torch.Tensor) -> torch.Tensor: + if len(tensor.shape) == 2: + return tensor[:, :, None] + if len(tensor.shape) == 3: + return tensor[:, :, None, :] + elif len(tensor.shape) == 4: + return tensor[:, :, 0, :] + else: + raise ValueError(f"`len(tensor)`: {len(tensor)} has to be 2, 3 or 4.") + + +class Conv1dBlock(nn.Module): + """ + Conv1d --> GroupNorm --> Mish + + Parameters: + inp_channels (`int`): Number of input channels. + out_channels (`int`): Number of output channels. + kernel_size (`int` or `tuple`): Size of the convolving kernel. + n_groups (`int`, default `8`): Number of groups to separate the channels into. + activation (`str`, defaults to `mish`): Name of the activation function. + """ + + def __init__( + self, + inp_channels: int, + out_channels: int, + kernel_size: Union[int, Tuple[int, int]], + n_groups: int = 8, + activation: str = "mish", + ): + super().__init__() + + self.conv1d = nn.Conv1d(inp_channels, out_channels, kernel_size, padding=kernel_size // 2) + self.group_norm = nn.GroupNorm(n_groups, out_channels) + self.mish = get_activation(activation) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + intermediate_repr = self.conv1d(inputs) + intermediate_repr = rearrange_dims(intermediate_repr) + intermediate_repr = self.group_norm(intermediate_repr) + intermediate_repr = rearrange_dims(intermediate_repr) + output = self.mish(intermediate_repr) + return output + + +# unet_rl.py +class ResidualTemporalBlock1D(nn.Module): + """ + Residual 1D block with temporal convolutions. + + Parameters: + inp_channels (`int`): Number of input channels. + out_channels (`int`): Number of output channels. + embed_dim (`int`): Embedding dimension. + kernel_size (`int` or `tuple`): Size of the convolving kernel. + activation (`str`, defaults `mish`): It is possible to choose the right activation function. + """ + + def __init__( + self, + inp_channels: int, + out_channels: int, + embed_dim: int, + kernel_size: Union[int, Tuple[int, int]] = 5, + activation: str = "mish", + ): + super().__init__() + self.conv_in = Conv1dBlock(inp_channels, out_channels, kernel_size) + self.conv_out = Conv1dBlock(out_channels, out_channels, kernel_size) + + self.time_emb_act = get_activation(activation) + self.time_emb = nn.Linear(embed_dim, out_channels) + + self.residual_conv = ( + nn.Conv1d(inp_channels, out_channels, 1) if inp_channels != out_channels else nn.Identity() + ) + + def forward(self, inputs: torch.Tensor, t: torch.Tensor) -> torch.Tensor: + """ + Args: + inputs : [ batch_size x inp_channels x horizon ] + t : [ batch_size x embed_dim ] + + returns: + out : [ batch_size x out_channels x horizon ] + """ + t = self.time_emb_act(t) + t = self.time_emb(t) + out = self.conv_in(inputs) + rearrange_dims(t) + out = self.conv_out(out) + return out + self.residual_conv(inputs) + + +class TemporalConvLayer(nn.Module): + """ + Temporal convolutional layer that can be used for video (sequence of images) input Code mostly copied from: + https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/models/multi_modal/video_synthesis/unet_sd.py#L1016 + + Parameters: + in_dim (`int`): Number of input channels. + out_dim (`int`): Number of output channels. + dropout (`float`, *optional*, defaults to `0.0`): The dropout probability to use. + """ + + def __init__( + self, + in_dim: int, + out_dim: Optional[int] = None, + dropout: float = 0.0, + norm_num_groups: int = 32, + ): + super().__init__() + out_dim = out_dim or in_dim + self.in_dim = in_dim + self.out_dim = out_dim + + # conv layers + self.conv1 = nn.Sequential( + nn.GroupNorm(norm_num_groups, in_dim), + nn.SiLU(), + nn.Conv3d(in_dim, out_dim, (3, 1, 1), padding=(1, 0, 0)), + ) + self.conv2 = nn.Sequential( + nn.GroupNorm(norm_num_groups, out_dim), + nn.SiLU(), + nn.Dropout(dropout), + nn.Conv3d(out_dim, in_dim, (3, 1, 1), padding=(1, 0, 0)), + ) + self.conv3 = nn.Sequential( + nn.GroupNorm(norm_num_groups, out_dim), + nn.SiLU(), + nn.Dropout(dropout), + nn.Conv3d(out_dim, in_dim, (3, 1, 1), padding=(1, 0, 0)), + ) + self.conv4 = nn.Sequential( + nn.GroupNorm(norm_num_groups, out_dim), + nn.SiLU(), + nn.Dropout(dropout), + nn.Conv3d(out_dim, in_dim, (3, 1, 1), padding=(1, 0, 0)), + ) + + # zero out the last layer params,so the conv block is identity + nn.init.zeros_(self.conv4[-1].weight) + nn.init.zeros_(self.conv4[-1].bias) + + def forward(self, hidden_states: torch.Tensor, num_frames: int = 1) -> torch.Tensor: + hidden_states = ( + hidden_states[None, :].reshape((-1, num_frames) + hidden_states.shape[1:]).permute(0, 2, 1, 3, 4) + ) + + identity = hidden_states + hidden_states = self.conv1(hidden_states) + hidden_states = self.conv2(hidden_states) + hidden_states = self.conv3(hidden_states) + hidden_states = self.conv4(hidden_states) + + hidden_states = identity + hidden_states + + hidden_states = hidden_states.permute(0, 2, 1, 3, 4).reshape( + (hidden_states.shape[0] * hidden_states.shape[2], -1) + hidden_states.shape[3:] + ) + return hidden_states + + +class TemporalResnetBlock(nn.Module): + r""" + A Resnet block. + + Parameters: + in_channels (`int`): The number of channels in the input. + out_channels (`int`, *optional*, default to be `None`): + The number of output channels for the first conv2d layer. If None, same as `in_channels`. + temb_channels (`int`, *optional*, default to `512`): the number of channels in timestep embedding. + eps (`float`, *optional*, defaults to `1e-6`): The epsilon to use for the normalization. + """ + + def __init__( + self, + in_channels: int, + out_channels: Optional[int] = None, + temb_channels: int = 512, + eps: float = 1e-6, + ): + super().__init__() + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + + kernel_size = (3, 1, 1) + padding = [k // 2 for k in kernel_size] + + self.norm1 = torch.nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=eps, affine=True) + self.conv1 = nn.Conv3d( + in_channels, + out_channels, + kernel_size=kernel_size, + stride=1, + padding=padding, + ) + + if temb_channels is not None: + self.time_emb_proj = nn.Linear(temb_channels, out_channels) + else: + self.time_emb_proj = None + + self.norm2 = torch.nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=eps, affine=True) + + self.dropout = torch.nn.Dropout(0.0) + self.conv2 = nn.Conv3d( + out_channels, + out_channels, + kernel_size=kernel_size, + stride=1, + padding=padding, + ) + + self.nonlinearity = get_activation("silu") + + self.use_in_shortcut = self.in_channels != out_channels + + self.conv_shortcut = None + if self.use_in_shortcut: + self.conv_shortcut = nn.Conv3d( + in_channels, + out_channels, + kernel_size=1, + stride=1, + padding=0, + ) + + def forward(self, input_tensor: torch.FloatTensor, temb: torch.FloatTensor) -> torch.FloatTensor: + hidden_states = input_tensor + + hidden_states = self.norm1(hidden_states) + hidden_states = self.nonlinearity(hidden_states) + hidden_states = self.conv1(hidden_states) + + if self.time_emb_proj is not None: + temb = self.nonlinearity(temb) + temb = self.time_emb_proj(temb)[:, :, :, None, None] + temb = temb.permute(0, 2, 1, 3, 4) + hidden_states = hidden_states + temb + + hidden_states = self.norm2(hidden_states) + hidden_states = self.nonlinearity(hidden_states) + hidden_states = self.dropout(hidden_states) + hidden_states = self.conv2(hidden_states) + + if self.conv_shortcut is not None: + input_tensor = self.conv_shortcut(input_tensor) + + output_tensor = input_tensor + hidden_states + + return output_tensor + + +# VideoResBlock +class SpatioTemporalResBlock(nn.Module): + r""" + A SpatioTemporal Resnet block. + + Parameters: + in_channels (`int`): The number of channels in the input. + out_channels (`int`, *optional*, default to be `None`): + The number of output channels for the first conv2d layer. If None, same as `in_channels`. + temb_channels (`int`, *optional*, default to `512`): the number of channels in timestep embedding. + eps (`float`, *optional*, defaults to `1e-6`): The epsilon to use for the spatial resenet. + temporal_eps (`float`, *optional*, defaults to `eps`): The epsilon to use for the temporal resnet. + merge_factor (`float`, *optional*, defaults to `0.5`): The merge factor to use for the temporal mixing. + merge_strategy (`str`, *optional*, defaults to `learned_with_images`): + The merge strategy to use for the temporal mixing. + switch_spatial_to_temporal_mix (`bool`, *optional*, defaults to `False`): + If `True`, switch the spatial and temporal mixing. + """ + + def __init__( + self, + in_channels: int, + out_channels: Optional[int] = None, + temb_channels: int = 512, + eps: float = 1e-6, + temporal_eps: Optional[float] = None, + merge_factor: float = 0.5, + merge_strategy="learned_with_images", + switch_spatial_to_temporal_mix: bool = False, + ): + super().__init__() + + self.spatial_res_block = ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=eps, + ) + + self.temporal_res_block = TemporalResnetBlock( + in_channels=out_channels if out_channels is not None else in_channels, + out_channels=out_channels if out_channels is not None else in_channels, + temb_channels=temb_channels, + eps=temporal_eps if temporal_eps is not None else eps, + ) + + self.time_mixer = AlphaBlender( + alpha=merge_factor, + merge_strategy=merge_strategy, + switch_spatial_to_temporal_mix=switch_spatial_to_temporal_mix, + ) + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + image_only_indicator: Optional[torch.Tensor] = None, + ): + num_frames = image_only_indicator.shape[-1] + hidden_states = self.spatial_res_block(hidden_states, temb) + + batch_frames, channels, height, width = hidden_states.shape + batch_size = batch_frames // num_frames + + hidden_states_mix = ( + hidden_states[None, :].reshape(batch_size, num_frames, channels, height, width).permute(0, 2, 1, 3, 4) + ) + hidden_states = ( + hidden_states[None, :].reshape(batch_size, num_frames, channels, height, width).permute(0, 2, 1, 3, 4) + ) + + if temb is not None: + temb = temb.reshape(batch_size, num_frames, -1) + + hidden_states = self.temporal_res_block(hidden_states, temb) + hidden_states = self.time_mixer( + x_spatial=hidden_states_mix, + x_temporal=hidden_states, + image_only_indicator=image_only_indicator, + ) + + hidden_states = hidden_states.permute(0, 2, 1, 3, 4).reshape(batch_frames, channels, height, width) + return hidden_states + + +class AlphaBlender(nn.Module): + r""" + A module to blend spatial and temporal features. + + Parameters: + alpha (`float`): The initial value of the blending factor. + merge_strategy (`str`, *optional*, defaults to `learned_with_images`): + The merge strategy to use for the temporal mixing. + switch_spatial_to_temporal_mix (`bool`, *optional*, defaults to `False`): + If `True`, switch the spatial and temporal mixing. + """ + + strategies = ["learned", "fixed", "learned_with_images"] + + def __init__( + self, + alpha: float, + merge_strategy: str = "learned_with_images", + switch_spatial_to_temporal_mix: bool = False, + ): + super().__init__() + self.merge_strategy = merge_strategy + self.switch_spatial_to_temporal_mix = switch_spatial_to_temporal_mix # For TemporalVAE + + if merge_strategy not in self.strategies: + raise ValueError(f"merge_strategy needs to be in {self.strategies}") + + if self.merge_strategy == "fixed": + self.register_buffer("mix_factor", torch.Tensor([alpha])) + elif self.merge_strategy == "learned" or self.merge_strategy == "learned_with_images": + self.register_parameter("mix_factor", torch.nn.Parameter(torch.Tensor([alpha]))) + else: + raise ValueError(f"Unknown merge strategy {self.merge_strategy}") + + def get_alpha(self, image_only_indicator: torch.Tensor, ndims: int) -> torch.Tensor: + if self.merge_strategy == "fixed": + alpha = self.mix_factor + + elif self.merge_strategy == "learned": + alpha = torch.sigmoid(self.mix_factor) + + elif self.merge_strategy == "learned_with_images": + if image_only_indicator is None: + raise ValueError("Please provide image_only_indicator to use learned_with_images merge strategy") + + alpha = torch.where( + image_only_indicator.bool(), + torch.ones(1, 1, device=image_only_indicator.device), + torch.sigmoid(self.mix_factor)[..., None], + ) + + # (batch, channel, frames, height, width) + if ndims == 5: + alpha = alpha[:, None, :, None, None] + # (batch*frames, height*width, channels) + elif ndims == 3: + alpha = alpha.reshape(-1)[:, None, None] + else: + raise ValueError(f"Unexpected ndims {ndims}. Dimensions should be 3 or 5") + + else: + raise NotImplementedError + + return alpha + + def forward( + self, + x_spatial: torch.Tensor, + x_temporal: torch.Tensor, + image_only_indicator: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + alpha = self.get_alpha(image_only_indicator, x_spatial.ndim) + alpha = alpha.to(x_spatial.dtype) + + if self.switch_spatial_to_temporal_mix: + alpha = 1.0 - alpha + + x = alpha * x_spatial + (1.0 - alpha) * x_temporal + return x diff --git a/foleycrafter/models/auffusion/transformer_2d.py b/foleycrafter/models/auffusion/transformer_2d.py new file mode 100644 index 0000000..c4184c1 --- /dev/null +++ b/foleycrafter/models/auffusion/transformer_2d.py @@ -0,0 +1,460 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dataclasses import dataclass +from typing import Any, Dict, Optional + +import torch +import torch.nn.functional as F +from torch import nn + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.models.embeddings import ImagePositionalEmbeddings, PatchEmbed, PixArtAlphaTextProjection +from diffusers.models.lora import LoRACompatibleConv, LoRACompatibleLinear +from diffusers.models.modeling_utils import ModelMixin +from diffusers.models.normalization import AdaLayerNormSingle +from diffusers.utils import USE_PEFT_BACKEND, BaseOutput, deprecate, is_torch_version +from .attention import BasicTransformerBlock + + +@dataclass +class Transformer2DModelOutput(BaseOutput): + """ + The output of [`Transformer2DModel`]. + + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` or `(batch size, num_vector_embeds - 1, num_latent_pixels)` if [`Transformer2DModel`] is discrete): + The hidden states output conditioned on the `encoder_hidden_states` input. If discrete, returns probability + distributions for the unnoised latent pixels. + """ + + sample: torch.FloatTensor + + +class Transformer2DModel(ModelMixin, ConfigMixin): + """ + A 2D Transformer model for image-like data. + + Parameters: + num_attention_heads (`int`, *optional*, defaults to 16): The number of heads to use for multi-head attention. + attention_head_dim (`int`, *optional*, defaults to 88): The number of channels in each head. + in_channels (`int`, *optional*): + The number of channels in the input and output (specify if the input is **continuous**). + num_layers (`int`, *optional*, defaults to 1): The number of layers of Transformer blocks to use. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The number of `encoder_hidden_states` dimensions to use. + sample_size (`int`, *optional*): The width of the latent images (specify if the input is **discrete**). + This is fixed during training since it is used to learn a number of position embeddings. + num_vector_embeds (`int`, *optional*): + The number of classes of the vector embeddings of the latent pixels (specify if the input is **discrete**). + Includes the class for the masked latent pixel. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to use in feed-forward. + num_embeds_ada_norm ( `int`, *optional*): + The number of diffusion steps used during training. Pass if at least one of the norm_layers is + `AdaLayerNorm`. This is fixed during training since it is used to learn a number of embeddings that are + added to the hidden states. + + During inference, you can denoise for up to but not more steps than `num_embeds_ada_norm`. + attention_bias (`bool`, *optional*): + Configure if the `TransformerBlocks` attention should contain a bias parameter. + """ + + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + num_attention_heads: int = 16, + attention_head_dim: int = 88, + in_channels: Optional[int] = None, + out_channels: Optional[int] = None, + num_layers: int = 1, + dropout: float = 0.0, + norm_num_groups: int = 32, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + sample_size: Optional[int] = None, + num_vector_embeds: Optional[int] = None, + patch_size: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + double_self_attention: bool = False, + upcast_attention: bool = False, + norm_type: str = "layer_norm", + norm_elementwise_affine: bool = True, + norm_eps: float = 1e-5, + attention_type: str = "default", + caption_channels: int = None, + ): + super().__init__() + self.use_linear_projection = use_linear_projection + self.num_attention_heads = num_attention_heads + self.attention_head_dim = attention_head_dim + inner_dim = num_attention_heads * attention_head_dim + + conv_cls = nn.Conv2d if USE_PEFT_BACKEND else LoRACompatibleConv + linear_cls = nn.Linear if USE_PEFT_BACKEND else LoRACompatibleLinear + + # 1. Transformer2DModel can process both standard continuous images of shape `(batch_size, num_channels, width, height)` as well as quantized image embeddings of shape `(batch_size, num_image_vectors)` + # Define whether input is continuous or discrete depending on configuration + self.is_input_continuous = (in_channels is not None) and (patch_size is None) + self.is_input_vectorized = num_vector_embeds is not None + self.is_input_patches = in_channels is not None and patch_size is not None + + if norm_type == "layer_norm" and num_embeds_ada_norm is not None: + deprecation_message = ( + f"The configuration file of this model: {self.__class__} is outdated. `norm_type` is either not set or" + " incorrectly set to `'layer_norm'`.Make sure to set `norm_type` to `'ada_norm'` in the config." + " Please make sure to update the config accordingly as leaving `norm_type` might led to incorrect" + " results in future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it" + " would be very nice if you could open a Pull request for the `transformer/config.json` file" + ) + deprecate("norm_type!=num_embeds_ada_norm", "1.0.0", deprecation_message, standard_warn=False) + norm_type = "ada_norm" + + if self.is_input_continuous and self.is_input_vectorized: + raise ValueError( + f"Cannot define both `in_channels`: {in_channels} and `num_vector_embeds`: {num_vector_embeds}. Make" + " sure that either `in_channels` or `num_vector_embeds` is None." + ) + elif self.is_input_vectorized and self.is_input_patches: + raise ValueError( + f"Cannot define both `num_vector_embeds`: {num_vector_embeds} and `patch_size`: {patch_size}. Make" + " sure that either `num_vector_embeds` or `num_patches` is None." + ) + elif not self.is_input_continuous and not self.is_input_vectorized and not self.is_input_patches: + raise ValueError( + f"Has to define `in_channels`: {in_channels}, `num_vector_embeds`: {num_vector_embeds}, or patch_size:" + f" {patch_size}. Make sure that `in_channels`, `num_vector_embeds` or `num_patches` is not None." + ) + + # 2. Define input layers + if self.is_input_continuous: + self.in_channels = in_channels + + self.norm = torch.nn.GroupNorm(num_groups=norm_num_groups, num_channels=in_channels, eps=1e-6, affine=True) + if use_linear_projection: + self.proj_in = linear_cls(in_channels, inner_dim) + else: + self.proj_in = conv_cls(in_channels, inner_dim, kernel_size=1, stride=1, padding=0) + elif self.is_input_vectorized: + assert sample_size is not None, "Transformer2DModel over discrete input must provide sample_size" + assert num_vector_embeds is not None, "Transformer2DModel over discrete input must provide num_embed" + + self.height = sample_size + self.width = sample_size + self.num_vector_embeds = num_vector_embeds + self.num_latent_pixels = self.height * self.width + + self.latent_image_embedding = ImagePositionalEmbeddings( + num_embed=num_vector_embeds, embed_dim=inner_dim, height=self.height, width=self.width + ) + elif self.is_input_patches: + assert sample_size is not None, "Transformer2DModel over patched input must provide sample_size" + + self.height = sample_size + self.width = sample_size + + self.patch_size = patch_size + interpolation_scale = self.config.sample_size // 64 # => 64 (= 512 pixart) has interpolation scale 1 + interpolation_scale = max(interpolation_scale, 1) + self.pos_embed = PatchEmbed( + height=sample_size, + width=sample_size, + patch_size=patch_size, + in_channels=in_channels, + embed_dim=inner_dim, + interpolation_scale=interpolation_scale, + ) + + # 3. Define transformers blocks + self.transformer_blocks = nn.ModuleList( + [ + # NOTE: remember to change + BasicTransformerBlock( + inner_dim, + num_attention_heads, + attention_head_dim, + dropout=dropout, + cross_attention_dim=cross_attention_dim, + activation_fn=activation_fn, + num_embeds_ada_norm=num_embeds_ada_norm, + attention_bias=attention_bias, + only_cross_attention=only_cross_attention, + double_self_attention=double_self_attention, + upcast_attention=upcast_attention, + norm_type=norm_type, + norm_elementwise_affine=norm_elementwise_affine, + norm_eps=norm_eps, + attention_type=attention_type, + ) + for d in range(num_layers) + ] + ) + + # 4. Define output layers + self.out_channels = in_channels if out_channels is None else out_channels + if self.is_input_continuous: + # TODO: should use out_channels for continuous projections + if use_linear_projection: + self.proj_out = linear_cls(inner_dim, in_channels) + else: + self.proj_out = conv_cls(inner_dim, in_channels, kernel_size=1, stride=1, padding=0) + elif self.is_input_vectorized: + self.norm_out = nn.LayerNorm(inner_dim) + self.out = nn.Linear(inner_dim, self.num_vector_embeds - 1) + elif self.is_input_patches and norm_type != "ada_norm_single": + self.norm_out = nn.LayerNorm(inner_dim, elementwise_affine=False, eps=1e-6) + self.proj_out_1 = nn.Linear(inner_dim, 2 * inner_dim) + self.proj_out_2 = nn.Linear(inner_dim, patch_size * patch_size * self.out_channels) + elif self.is_input_patches and norm_type == "ada_norm_single": + self.norm_out = nn.LayerNorm(inner_dim, elementwise_affine=False, eps=1e-6) + self.scale_shift_table = nn.Parameter(torch.randn(2, inner_dim) / inner_dim**0.5) + self.proj_out = nn.Linear(inner_dim, patch_size * patch_size * self.out_channels) + + # 5. PixArt-Alpha blocks. + self.adaln_single = None + self.use_additional_conditions = False + if norm_type == "ada_norm_single": + self.use_additional_conditions = self.config.sample_size == 128 + # TODO(Sayak, PVP) clean this, for now we use sample size to determine whether to use + # additional conditions until we find better name + self.adaln_single = AdaLayerNormSingle(inner_dim, use_additional_conditions=self.use_additional_conditions) + + self.caption_projection = None + if caption_channels is not None: + self.caption_projection = PixArtAlphaTextProjection(in_features=caption_channels, hidden_size=inner_dim) + + self.gradient_checkpointing = False + + def _set_gradient_checkpointing(self, module, value=False): + if hasattr(module, "gradient_checkpointing"): + module.gradient_checkpointing = value + + def forward( + self, + hidden_states: torch.Tensor, + encoder_hidden_states: Optional[torch.Tensor] = None, + timestep: Optional[torch.LongTensor] = None, + added_cond_kwargs: Dict[str, torch.Tensor] = None, + class_labels: Optional[torch.LongTensor] = None, + cross_attention_kwargs: Dict[str, Any] = None, + attention_mask: Optional[torch.Tensor] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + return_dict: bool = True, + ): + """ + The [`Transformer2DModel`] forward method. + + Args: + hidden_states (`torch.LongTensor` of shape `(batch size, num latent pixels)` if discrete, `torch.FloatTensor` of shape `(batch size, channel, height, width)` if continuous): + Input `hidden_states`. + encoder_hidden_states ( `torch.FloatTensor` of shape `(batch size, sequence len, embed dims)`, *optional*): + Conditional embeddings for cross attention layer. If not given, cross-attention defaults to + self-attention. + timestep ( `torch.LongTensor`, *optional*): + Used to indicate denoising step. Optional timestep to be applied as an embedding in `AdaLayerNorm`. + class_labels ( `torch.LongTensor` of shape `(batch size, num classes)`, *optional*): + Used to indicate class labels conditioning. Optional class labels to be applied as an embedding in + `AdaLayerZeroNorm`. + cross_attention_kwargs ( `Dict[str, Any]`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under + `self.processor` in + [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + attention_mask ( `torch.Tensor`, *optional*): + An attention mask of shape `(batch, key_tokens)` is applied to `encoder_hidden_states`. If `1` the mask + is kept, otherwise if `0` it is discarded. Mask will be converted into a bias, which adds large + negative values to the attention scores corresponding to "discard" tokens. + encoder_attention_mask ( `torch.Tensor`, *optional*): + Cross-attention mask applied to `encoder_hidden_states`. Two formats supported: + + * Mask `(batch, sequence_length)` True = keep, False = discard. + * Bias `(batch, 1, sequence_length)` 0 = keep, -10000 = discard. + + If `ndim == 2`: will be interpreted as a mask, then converted into a bias consistent with the format + above. This bias will be added to the cross-attention scores. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain + tuple. + + Returns: + If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a + `tuple` where the first element is the sample tensor. + """ + # ensure attention_mask is a bias, and give it a singleton query_tokens dimension. + # we may have done this conversion already, e.g. if we came here via UNet2DConditionModel#forward. + # we can tell by counting dims; if ndim == 2: it's a mask rather than a bias. + # expects mask of shape: + # [batch, key_tokens] + # adds singleton query_tokens dimension: + # [batch, 1, key_tokens] + # this helps to broadcast it as a bias over attention scores, which will be in one of the following shapes: + # [batch, heads, query_tokens, key_tokens] (e.g. torch sdp attn) + # [batch * heads, query_tokens, key_tokens] (e.g. xformers or classic attn) + if attention_mask is not None and attention_mask.ndim == 2: + # assume that mask is expressed as: + # (1 = keep, 0 = discard) + # convert mask into a bias that can be added to attention scores: + # (keep = +0, discard = -10000.0) + attention_mask = (1 - attention_mask.to(hidden_states.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # convert encoder_attention_mask to a bias the same way we do for attention_mask + if encoder_attention_mask is not None and encoder_attention_mask.ndim == 2: + encoder_attention_mask = (1 - encoder_attention_mask.to(hidden_states.dtype)) * -10000.0 + encoder_attention_mask = encoder_attention_mask.unsqueeze(1) + + # Retrieve lora scale. + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + + # 1. Input + if self.is_input_continuous: + batch, _, height, width = hidden_states.shape + inner_dim = hidden_states.shape[1] + residual = hidden_states + + hidden_states = self.norm(hidden_states) + if not self.use_linear_projection: + hidden_states = ( + self.proj_in(hidden_states, scale=lora_scale) + if not USE_PEFT_BACKEND + else self.proj_in(hidden_states) + ) + inner_dim = hidden_states.shape[1] + hidden_states = hidden_states.permute(0, 2, 3, 1).reshape(batch, height * width, inner_dim) + else: + inner_dim = hidden_states.shape[1] + hidden_states = hidden_states.permute(0, 2, 3, 1).reshape(batch, height * width, inner_dim) + hidden_states = ( + self.proj_in(hidden_states, scale=lora_scale) + if not USE_PEFT_BACKEND + else self.proj_in(hidden_states) + ) + + elif self.is_input_vectorized: + hidden_states = self.latent_image_embedding(hidden_states) + elif self.is_input_patches: + height, width = hidden_states.shape[-2] // self.patch_size, hidden_states.shape[-1] // self.patch_size + self.height, self.width = height, width + hidden_states = self.pos_embed(hidden_states) + + if self.adaln_single is not None: + if self.use_additional_conditions and added_cond_kwargs is None: + raise ValueError( + "`added_cond_kwargs` cannot be None when using additional conditions for `adaln_single`." + ) + batch_size = hidden_states.shape[0] + timestep, embedded_timestep = self.adaln_single( + timestep, added_cond_kwargs, batch_size=batch_size, hidden_dtype=hidden_states.dtype + ) + + if self.caption_projection is not None: + batch_size = hidden_states.shape[0] + encoder_hidden_states = self.caption_projection(encoder_hidden_states) + encoder_hidden_states = encoder_hidden_states.view(batch_size, -1, hidden_states.shape[-1]) + # 2. Blocks + for block in self.transformer_blocks: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(block), + hidden_states, + attention_mask, + encoder_hidden_states, + encoder_attention_mask, + timestep, + cross_attention_kwargs, + class_labels, + **ckpt_kwargs, + ) + else: + hidden_states = block( + hidden_states, + attention_mask=attention_mask, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + timestep=timestep, + cross_attention_kwargs=cross_attention_kwargs, + class_labels=class_labels, + ) + + # 3. Output + if self.is_input_continuous: + if not self.use_linear_projection: + hidden_states = hidden_states.reshape(batch, height, width, inner_dim).permute(0, 3, 1, 2).contiguous() + hidden_states = ( + self.proj_out(hidden_states, scale=lora_scale) + if not USE_PEFT_BACKEND + else self.proj_out(hidden_states) + ) + else: + hidden_states = ( + self.proj_out(hidden_states, scale=lora_scale) + if not USE_PEFT_BACKEND + else self.proj_out(hidden_states) + ) + hidden_states = hidden_states.reshape(batch, height, width, inner_dim).permute(0, 3, 1, 2).contiguous() + + output = hidden_states + residual + elif self.is_input_vectorized: + hidden_states = self.norm_out(hidden_states) + logits = self.out(hidden_states) + # (batch, self.num_vector_embeds - 1, self.num_latent_pixels) + logits = logits.permute(0, 2, 1) + + # log(p(x_0)) + output = F.log_softmax(logits.double(), dim=1).float() + + if self.is_input_patches: + if self.config.norm_type != "ada_norm_single": + conditioning = self.transformer_blocks[0].norm1.emb( + timestep, class_labels, hidden_dtype=hidden_states.dtype + ) + shift, scale = self.proj_out_1(F.silu(conditioning)).chunk(2, dim=1) + hidden_states = self.norm_out(hidden_states) * (1 + scale[:, None]) + shift[:, None] + hidden_states = self.proj_out_2(hidden_states) + elif self.config.norm_type == "ada_norm_single": + shift, scale = (self.scale_shift_table[None] + embedded_timestep[:, None]).chunk(2, dim=1) + hidden_states = self.norm_out(hidden_states) + # Modulation + hidden_states = hidden_states * (1 + scale) + shift + hidden_states = self.proj_out(hidden_states) + hidden_states = hidden_states.squeeze(1) + + # unpatchify + if self.adaln_single is None: + height = width = int(hidden_states.shape[1] ** 0.5) + hidden_states = hidden_states.reshape( + shape=(-1, height, width, self.patch_size, self.patch_size, self.out_channels) + ) + hidden_states = torch.einsum("nhwpqc->nchpwq", hidden_states) + output = hidden_states.reshape( + shape=(-1, self.out_channels, height * self.patch_size, width * self.patch_size) + ) + + if not return_dict: + return (output,) + + return Transformer2DModelOutput(sample=output) diff --git a/foleycrafter/models/auffusion/unet_2d_blocks.py b/foleycrafter/models/auffusion/unet_2d_blocks.py new file mode 100644 index 0000000..17e73be --- /dev/null +++ b/foleycrafter/models/auffusion/unet_2d_blocks.py @@ -0,0 +1,3501 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Dict, Optional, Tuple, Union + +import numpy as np +import torch +import torch.nn.functional as F +from torch import nn + +from diffusers.models.activations import get_activation +from diffusers.models.normalization import AdaGroupNorm +from diffusers.utils import is_torch_version, logging +from diffusers.utils.torch_utils import apply_freeu +from .attention_processor import Attention, AttnAddedKVProcessor, AttnAddedKVProcessor2_0 +from .dual_transformer_2d import DualTransformer2DModel +from .resnet import ( + Downsample2D, + FirDownsample2D, + FirUpsample2D, + KDownsample2D, + KUpsample2D, + ResnetBlock2D, + Upsample2D, +) +from .transformer_2d import Transformer2DModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def get_down_block( + down_block_type: str, + num_layers: int, + in_channels: int, + out_channels: int, + temb_channels: int, + add_downsample: bool, + resnet_eps: float, + resnet_act_fn: str, + transformer_layers_per_block: int = 1, + num_attention_heads: Optional[int] = None, + resnet_groups: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + downsample_padding: Optional[int] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + attention_type: str = "default", + resnet_skip_time_act: bool = False, + resnet_out_scale_factor: float = 1.0, + cross_attention_norm: Optional[str] = None, + attention_head_dim: Optional[int] = None, + downsample_type: Optional[str] = None, + dropout: float = 0.0, +): + # If attn head dim is not defined, we default it to the number of heads + if attention_head_dim is None: + logger.warn( + f"It is recommended to provide `attention_head_dim` when calling `get_down_block`. Defaulting `attention_head_dim` to {num_attention_heads}." + ) + attention_head_dim = num_attention_heads + + down_block_type = down_block_type[7:] if down_block_type.startswith("UNetRes") else down_block_type + if down_block_type == "DownBlock2D": + return DownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "ResnetDownsampleBlock2D": + return ResnetDownsampleBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + ) + elif down_block_type == "AttnDownBlock2D": + if add_downsample is False: + downsample_type = None + else: + downsample_type = downsample_type or "conv" # default to 'conv' + return AttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + downsample_type=downsample_type, + ) + elif down_block_type == "CrossAttnDownBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnDownBlock2D") + return CrossAttnDownBlock2D( + num_layers=num_layers, + transformer_layers_per_block=transformer_layers_per_block, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + ) + elif down_block_type == "SimpleCrossAttnDownBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for SimpleCrossAttnDownBlock2D") + return SimpleCrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + ) + elif down_block_type == "SkipDownBlock2D": + return SkipDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnSkipDownBlock2D": + return AttnSkipDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "DownEncoderBlock2D": + return DownEncoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnDownEncoderBlock2D": + return AttnDownEncoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "KDownBlock2D": + return KDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + ) + elif down_block_type == "KCrossAttnDownBlock2D": + return KCrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + add_self_attention=True if not add_downsample else False, + ) + raise ValueError(f"{down_block_type} does not exist.") + + +def get_up_block( + up_block_type: str, + num_layers: int, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + add_upsample: bool, + resnet_eps: float, + resnet_act_fn: str, + resolution_idx: Optional[int] = None, + transformer_layers_per_block: int = 1, + num_attention_heads: Optional[int] = None, + resnet_groups: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + attention_type: str = "default", + resnet_skip_time_act: bool = False, + resnet_out_scale_factor: float = 1.0, + cross_attention_norm: Optional[str] = None, + attention_head_dim: Optional[int] = None, + upsample_type: Optional[str] = None, + dropout: float = 0.0, +) -> nn.Module: + # If attn head dim is not defined, we default it to the number of heads + if attention_head_dim is None: + logger.warn( + f"It is recommended to provide `attention_head_dim` when calling `get_up_block`. Defaulting `attention_head_dim` to {num_attention_heads}." + ) + attention_head_dim = num_attention_heads + + up_block_type = up_block_type[7:] if up_block_type.startswith("UNetRes") else up_block_type + if up_block_type == "UpBlock2D": + return UpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "ResnetUpsampleBlock2D": + return ResnetUpsampleBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + ) + elif up_block_type == "CrossAttnUpBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnUpBlock2D") + return CrossAttnUpBlock2D( + num_layers=num_layers, + transformer_layers_per_block=transformer_layers_per_block, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + ) + elif up_block_type == "SimpleCrossAttnUpBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for SimpleCrossAttnUpBlock2D") + return SimpleCrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + ) + elif up_block_type == "AttnUpBlock2D": + if add_upsample is False: + upsample_type = None + else: + upsample_type = upsample_type or "conv" # default to 'conv' + + return AttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + upsample_type=upsample_type, + ) + elif up_block_type == "SkipUpBlock2D": + return SkipUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "AttnSkipUpBlock2D": + return AttnSkipUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "UpDecoderBlock2D": + return UpDecoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + temb_channels=temb_channels, + ) + elif up_block_type == "AttnUpDecoderBlock2D": + return AttnUpDecoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + temb_channels=temb_channels, + ) + elif up_block_type == "KUpBlock2D": + return KUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + ) + elif up_block_type == "KCrossAttnUpBlock2D": + return KCrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + ) + + raise ValueError(f"{up_block_type} does not exist.") + + +class AutoencoderTinyBlock(nn.Module): + """ + Tiny Autoencoder block used in [`AutoencoderTiny`]. It is a mini residual module consisting of plain conv + ReLU + blocks. + + Args: + in_channels (`int`): The number of input channels. + out_channels (`int`): The number of output channels. + act_fn (`str`): + ` The activation function to use. Supported values are `"swish"`, `"mish"`, `"gelu"`, and `"relu"`. + + Returns: + `torch.FloatTensor`: A tensor with the same shape as the input tensor, but with the number of channels equal to + `out_channels`. + """ + + def __init__(self, in_channels: int, out_channels: int, act_fn: str): + super().__init__() + act_fn = get_activation(act_fn) + self.conv = nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1), + act_fn, + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + act_fn, + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + ) + self.skip = ( + nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False) + if in_channels != out_channels + else nn.Identity() + ) + self.fuse = nn.ReLU() + + def forward(self, x: torch.FloatTensor) -> torch.FloatTensor: + return self.fuse(self.conv(x) + self.skip(x)) + + +class UNetMidBlock2D(nn.Module): + """ + A 2D UNet mid-block [`UNetMidBlock2D`] with multiple residual blocks and optional attention blocks. + + Args: + in_channels (`int`): The number of input channels. + temb_channels (`int`): The number of temporal embedding channels. + dropout (`float`, *optional*, defaults to 0.0): The dropout rate. + num_layers (`int`, *optional*, defaults to 1): The number of residual blocks. + resnet_eps (`float`, *optional*, 1e-6 ): The epsilon value for the resnet blocks. + resnet_time_scale_shift (`str`, *optional*, defaults to `default`): + The type of normalization to apply to the time embeddings. This can help to improve the performance of the + model on tasks with long-range temporal dependencies. + resnet_act_fn (`str`, *optional*, defaults to `swish`): The activation function for the resnet blocks. + resnet_groups (`int`, *optional*, defaults to 32): + The number of groups to use in the group normalization layers of the resnet blocks. + attn_groups (`Optional[int]`, *optional*, defaults to None): The number of groups for the attention blocks. + resnet_pre_norm (`bool`, *optional*, defaults to `True`): + Whether to use pre-normalization for the resnet blocks. + add_attention (`bool`, *optional*, defaults to `True`): Whether to add attention blocks. + attention_head_dim (`int`, *optional*, defaults to 1): + Dimension of a single attention head. The number of attention heads is determined based on this value and + the number of input channels. + output_scale_factor (`float`, *optional*, defaults to 1.0): The output scale factor. + + Returns: + `torch.FloatTensor`: The output of the last residual block, which is a tensor of shape `(batch_size, + in_channels, height, width)`. + + """ + + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", # default, spatial + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + attn_groups: Optional[int] = None, + resnet_pre_norm: bool = True, + add_attention: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + ): + super().__init__() + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + self.add_attention = add_attention + + if attn_groups is None: + attn_groups = resnet_groups if resnet_time_scale_shift == "default" else None + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {in_channels}." + ) + attention_head_dim = in_channels + + for _ in range(num_layers): + if self.add_attention: + attentions.append( + Attention( + in_channels, + heads=in_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=attn_groups, + spatial_norm_dim=temb_channels if resnet_time_scale_shift == "spatial" else None, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + else: + attentions.append(None) + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None) -> torch.FloatTensor: + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + if attn is not None: + hidden_states = attn(hidden_states, temb=temb) + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class UNetMidBlock2DCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + num_attention_heads: int = 1, + output_scale_factor: float = 1.0, + cross_attention_dim: int = 1280, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + upcast_attention: bool = False, + attention_type: str = "default", + ): + super().__init__() + + self.has_cross_attention = True + self.num_attention_heads = num_attention_heads + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + # support for variable transformer layers per block + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * num_layers + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for i in range(num_layers): + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + num_attention_heads, + in_channels // num_attention_heads, + in_channels=in_channels, + num_layers=transformer_layers_per_block[i], + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + num_attention_heads, + in_channels // num_attention_heads, + in_channels=in_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + hidden_states = self.resnets[0](hidden_states, temb, scale=lora_scale) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + else: + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + + return hidden_states + + +class UNetMidBlock2DSimpleCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + cross_attention_dim: int = 1280, + skip_time_act: bool = False, + only_cross_attention: bool = False, + cross_attention_norm: Optional[str] = None, + ): + super().__init__() + + self.has_cross_attention = True + + self.attention_head_dim = attention_head_dim + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + self.num_heads = in_channels // self.attention_head_dim + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ] + attentions = [] + + for _ in range(num_layers): + processor = ( + AttnAddedKVProcessor2_0() if hasattr(F, "scaled_dot_product_attention") else AttnAddedKVProcessor() + ) + + attentions.append( + Attention( + query_dim=in_channels, + cross_attention_dim=in_channels, + heads=self.num_heads, + dim_head=self.attention_head_dim, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + processor=processor, + ) + ) + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + lora_scale = cross_attention_kwargs.get("scale", 1.0) + + if attention_mask is None: + # if encoder_hidden_states is defined: we are doing cross-attn, so we should use cross-attn mask. + mask = None if encoder_hidden_states is None else encoder_attention_mask + else: + # when attention_mask is defined: we don't even check for encoder_attention_mask. + # this is to maintain compatibility with UnCLIP, which uses 'attention_mask' param for cross-attn masks. + # TODO: UnCLIP should express cross-attn mask via encoder_attention_mask param instead of via attention_mask. + # then we can simplify this whole if/else block to: + # mask = attention_mask if encoder_hidden_states is None else encoder_attention_mask + mask = attention_mask + + hidden_states = self.resnets[0](hidden_states, temb, scale=lora_scale) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + # attn + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + + # resnet + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + + return hidden_states + + +class AttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + downsample_padding: int = 1, + downsample_type: str = "conv", + ): + super().__init__() + resnets = [] + attentions = [] + self.downsample_type = downsample_type + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if downsample_type == "conv": + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + elif downsample_type == "resnet": + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + + lora_scale = cross_attention_kwargs.get("scale", 1.0) + + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + cross_attention_kwargs.update({"scale": lora_scale}) + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + hidden_states = attn(hidden_states, **cross_attention_kwargs) + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + if self.downsample_type == "resnet": + hidden_states = downsampler(hidden_states, temb=temb, scale=lora_scale) + else: + hidden_states = downsampler(hidden_states, scale=lora_scale) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class CrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + num_attention_heads: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + downsample_padding: int = 1, + add_downsample: bool = True, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + attention_type: str = "default", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.num_attention_heads = num_attention_heads + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * num_layers + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + # Transformer2DModelWithSwitcher + attentions.append( + Transformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=transformer_layers_per_block[i], + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + additional_residuals: Optional[torch.FloatTensor] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + output_states = () + + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + + blocks = list(zip(self.resnets, self.attentions)) + + for i, (resnet, attn) in enumerate(blocks): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + + # apply additional residuals to the output of the last pair of resnet and attention blocks + if i == len(blocks) - 1 and additional_residuals is not None: + hidden_states = hidden_states + additional_residuals + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, scale=lora_scale) + + output_states = output_states + (hidden_states,) + + return hidden_states, output_states + + +class DownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, scale: float = 1.0 + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb, scale=scale) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, scale=scale) + + output_states = output_states + (hidden_states,) + + return hidden_states, output_states + + +class DownEncoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states: torch.FloatTensor, scale: float = 1.0) -> torch.FloatTensor: + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb=None, scale=scale) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, scale) + + return hidden_states + + +class AttnDownEncoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + resnets = [] + attentions = [] + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states: torch.FloatTensor, scale: float = 1.0) -> torch.FloatTensor: + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb=None, scale=scale) + cross_attention_kwargs = {"scale": scale} + hidden_states = attn(hidden_states, **cross_attention_kwargs) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, scale) + + return hidden_states + + +class AttnSkipDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = np.sqrt(2.0), + add_downsample: bool = True, + ): + super().__init__() + self.attentions = nn.ModuleList([]) + self.resnets = nn.ModuleList([]) + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + self.resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(in_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + self.attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=32, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + if add_downsample: + self.resnet_down = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + down=True, + kernel="fir", + ) + self.downsamplers = nn.ModuleList([FirDownsample2D(out_channels, out_channels=out_channels)]) + self.skip_conv = nn.Conv2d(3, out_channels, kernel_size=(1, 1), stride=(1, 1)) + else: + self.resnet_down = None + self.downsamplers = None + self.skip_conv = None + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + skip_sample: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...], torch.FloatTensor]: + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb, scale=scale) + cross_attention_kwargs = {"scale": scale} + hidden_states = attn(hidden_states, **cross_attention_kwargs) + output_states += (hidden_states,) + + if self.downsamplers is not None: + hidden_states = self.resnet_down(hidden_states, temb, scale=scale) + for downsampler in self.downsamplers: + skip_sample = downsampler(skip_sample) + + hidden_states = self.skip_conv(skip_sample) + hidden_states + + output_states += (hidden_states,) + + return hidden_states, output_states, skip_sample + + +class SkipDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + output_scale_factor: float = np.sqrt(2.0), + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + self.resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(in_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + if add_downsample: + self.resnet_down = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + down=True, + kernel="fir", + ) + self.downsamplers = nn.ModuleList([FirDownsample2D(out_channels, out_channels=out_channels)]) + self.skip_conv = nn.Conv2d(3, out_channels, kernel_size=(1, 1), stride=(1, 1)) + else: + self.resnet_down = None + self.downsamplers = None + self.skip_conv = None + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + skip_sample: Optional[torch.FloatTensor] = None, + scale: float = 1.0, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...], torch.FloatTensor]: + output_states = () + + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb, scale) + output_states += (hidden_states,) + + if self.downsamplers is not None: + hidden_states = self.resnet_down(hidden_states, temb, scale) + for downsampler in self.downsamplers: + skip_sample = downsampler(skip_sample) + + hidden_states = self.skip_conv(skip_sample) + hidden_states + + output_states += (hidden_states,) + + return hidden_states, output_states, skip_sample + + +class ResnetDownsampleBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + skip_time_act: bool = False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, scale: float = 1.0 + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb, scale) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, temb, scale) + + output_states = output_states + (hidden_states,) + + return hidden_states, output_states + + +class SimpleCrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + skip_time_act: bool = False, + only_cross_attention: bool = False, + cross_attention_norm: Optional[str] = None, + ): + super().__init__() + + self.has_cross_attention = True + + resnets = [] + attentions = [] + + self.attention_head_dim = attention_head_dim + self.num_heads = out_channels // self.attention_head_dim + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + processor = ( + AttnAddedKVProcessor2_0() if hasattr(F, "scaled_dot_product_attention") else AttnAddedKVProcessor() + ) + + attentions.append( + Attention( + query_dim=out_channels, + cross_attention_dim=out_channels, + heads=self.num_heads, + dim_head=attention_head_dim, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + processor=processor, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + output_states = () + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + + lora_scale = cross_attention_kwargs.get("scale", 1.0) + + if attention_mask is None: + # if encoder_hidden_states is defined: we are doing cross-attn, so we should use cross-attn mask. + mask = None if encoder_hidden_states is None else encoder_attention_mask + else: + # when attention_mask is defined: we don't even check for encoder_attention_mask. + # this is to maintain compatibility with UnCLIP, which uses 'attention_mask' param for cross-attn masks. + # TODO: UnCLIP should express cross-attn mask via encoder_attention_mask param instead of via attention_mask. + # then we can simplify this whole if/else block to: + # mask = attention_mask if encoder_hidden_states is None else encoder_attention_mask + mask = attention_mask + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, temb, scale=lora_scale) + + output_states = output_states + (hidden_states,) + + return hidden_states, output_states + + +class KDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: int = 32, + add_downsample: bool = False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + temb_channels=temb_channels, + groups=groups, + groups_out=groups_out, + eps=resnet_eps, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + # YiYi's comments- might be able to use FirDownsample2D, look into details later + self.downsamplers = nn.ModuleList([KDownsample2D()]) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, scale: float = 1.0 + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb, scale) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states, output_states + + +class KCrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + cross_attention_dim: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_group_size: int = 32, + add_downsample: bool = True, + attention_head_dim: int = 64, + add_self_attention: bool = False, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + temb_channels=temb_channels, + groups=groups, + groups_out=groups_out, + eps=resnet_eps, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + attentions.append( + KAttentionBlock( + out_channels, + out_channels // attention_head_dim, + attention_head_dim, + cross_attention_dim=cross_attention_dim, + temb_channels=temb_channels, + attention_bias=True, + add_self_attention=add_self_attention, + cross_attention_norm="layer_norm", + group_size=resnet_group_size, + ) + ) + + self.resnets = nn.ModuleList(resnets) + self.attentions = nn.ModuleList(attentions) + + if add_downsample: + self.downsamplers = nn.ModuleList([KDownsample2D()]) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + output_states = () + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + + if self.downsamplers is None: + output_states += (None,) + else: + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states, output_states + + +class AttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: int = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + upsample_type: str = "conv", + ): + super().__init__() + resnets = [] + attentions = [] + + self.upsample_type = upsample_type + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if upsample_type == "conv": + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + elif upsample_type == "resnet": + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + scale: float = 1.0, + ) -> torch.FloatTensor: + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb, scale=scale) + cross_attention_kwargs = {"scale": scale} + hidden_states = attn(hidden_states, **cross_attention_kwargs) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + if self.upsample_type == "resnet": + hidden_states = upsampler(hidden_states, temb=temb, scale=scale) + else: + hidden_states = upsampler(hidden_states, scale=scale) + + return hidden_states + + +class CrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + num_attention_heads: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + attention_type: str = "default", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.num_attention_heads = num_attention_heads + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * num_layers + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + # Transformer2DModelWithSwitcher + attentions.append( + Transformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=transformer_layers_per_block[i], + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + upsample_size: Optional[int] = None, + attention_mask: Optional[torch.FloatTensor] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + is_freeu_enabled = ( + getattr(self, "s1", None) + and getattr(self, "s2", None) + and getattr(self, "b1", None) + and getattr(self, "b2", None) + ) + + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + + # FreeU: Only operate on the first two stages + if is_freeu_enabled: + hidden_states, res_hidden_states = apply_freeu( + self.resolution_idx, + hidden_states, + res_hidden_states, + s1=self.s1, + s2=self.s2, + b1=self.b1, + b2=self.b2, + ) + + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size, scale=lora_scale) + + return hidden_states + + +class UpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + scale: float = 1.0, + ) -> torch.FloatTensor: + is_freeu_enabled = ( + getattr(self, "s1", None) + and getattr(self, "s2", None) + and getattr(self, "b1", None) + and getattr(self, "b2", None) + ) + + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + + # FreeU: Only operate on the first two stages + if is_freeu_enabled: + hidden_states, res_hidden_states = apply_freeu( + self.resolution_idx, + hidden_states, + res_hidden_states, + s1=self.s1, + s2=self.s2, + b1=self.b1, + b2=self.b2, + ) + + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb, scale=scale) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size, scale=scale) + + return hidden_states + + +class UpDecoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", # default, spatial + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + temb_channels: Optional[int] = None, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + input_channels = in_channels if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.resolution_idx = resolution_idx + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, scale: float = 1.0 + ) -> torch.FloatTensor: + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb=temb, scale=scale) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class AttnUpDecoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + temb_channels: Optional[int] = None, + ): + super().__init__() + resnets = [] + attentions = [] + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `out_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + input_channels = in_channels if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups if resnet_time_scale_shift != "spatial" else None, + spatial_norm_dim=temb_channels if resnet_time_scale_shift == "spatial" else None, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.resolution_idx = resolution_idx + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, scale: float = 1.0 + ) -> torch.FloatTensor: + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb=temb, scale=scale) + cross_attention_kwargs = {"scale": scale} + hidden_states = attn(hidden_states, temb=temb, **cross_attention_kwargs) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, scale=scale) + + return hidden_states + + +class AttnSkipUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = np.sqrt(2.0), + add_upsample: bool = True, + ): + super().__init__() + self.attentions = nn.ModuleList([]) + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + self.resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(resnet_in_channels + res_skip_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + if attention_head_dim is None: + logger.warn( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `out_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + self.attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=32, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.upsampler = FirUpsample2D(in_channels, out_channels=out_channels) + if add_upsample: + self.resnet_up = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + up=True, + kernel="fir", + ) + self.skip_conv = nn.Conv2d(out_channels, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) + self.skip_norm = torch.nn.GroupNorm( + num_groups=min(out_channels // 4, 32), num_channels=out_channels, eps=resnet_eps, affine=True + ) + self.act = nn.SiLU() + else: + self.resnet_up = None + self.skip_conv = None + self.skip_norm = None + self.act = None + + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + skip_sample=None, + scale: float = 1.0, + ) -> Tuple[torch.FloatTensor, torch.FloatTensor]: + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb, scale=scale) + + cross_attention_kwargs = {"scale": scale} + hidden_states = self.attentions[0](hidden_states, **cross_attention_kwargs) + + if skip_sample is not None: + skip_sample = self.upsampler(skip_sample) + else: + skip_sample = 0 + + if self.resnet_up is not None: + skip_sample_states = self.skip_norm(hidden_states) + skip_sample_states = self.act(skip_sample_states) + skip_sample_states = self.skip_conv(skip_sample_states) + + skip_sample = skip_sample + skip_sample_states + + hidden_states = self.resnet_up(hidden_states, temb, scale=scale) + + return hidden_states, skip_sample + + +class SkipUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + output_scale_factor: float = np.sqrt(2.0), + add_upsample: bool = True, + upsample_padding: int = 1, + ): + super().__init__() + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + self.resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min((resnet_in_channels + res_skip_channels) // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.upsampler = FirUpsample2D(in_channels, out_channels=out_channels) + if add_upsample: + self.resnet_up = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + up=True, + kernel="fir", + ) + self.skip_conv = nn.Conv2d(out_channels, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) + self.skip_norm = torch.nn.GroupNorm( + num_groups=min(out_channels // 4, 32), num_channels=out_channels, eps=resnet_eps, affine=True + ) + self.act = nn.SiLU() + else: + self.resnet_up = None + self.skip_conv = None + self.skip_norm = None + self.act = None + + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + skip_sample=None, + scale: float = 1.0, + ) -> Tuple[torch.FloatTensor, torch.FloatTensor]: + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb, scale=scale) + + if skip_sample is not None: + skip_sample = self.upsampler(skip_sample) + else: + skip_sample = 0 + + if self.resnet_up is not None: + skip_sample_states = self.skip_norm(hidden_states) + skip_sample_states = self.act(skip_sample_states) + skip_sample_states = self.skip_conv(skip_sample_states) + + skip_sample = skip_sample + skip_sample_states + + hidden_states = self.resnet_up(hidden_states, temb, scale=scale) + + return hidden_states, skip_sample + + +class ResnetUpsampleBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + skip_time_act: bool = False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + scale: float = 1.0, + ) -> torch.FloatTensor: + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb, scale=scale) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, temb, scale=scale) + + return hidden_states + + +class SimpleCrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + skip_time_act: bool = False, + only_cross_attention: bool = False, + cross_attention_norm: Optional[str] = None, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attention_head_dim = attention_head_dim + + self.num_heads = out_channels // self.attention_head_dim + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + processor = ( + AttnAddedKVProcessor2_0() if hasattr(F, "scaled_dot_product_attention") else AttnAddedKVProcessor() + ) + + attentions.append( + Attention( + query_dim=out_channels, + cross_attention_dim=out_channels, + heads=self.num_heads, + dim_head=self.attention_head_dim, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + processor=processor, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + + lora_scale = cross_attention_kwargs.get("scale", 1.0) + if attention_mask is None: + # if encoder_hidden_states is defined: we are doing cross-attn, so we should use cross-attn mask. + mask = None if encoder_hidden_states is None else encoder_attention_mask + else: + # when attention_mask is defined: we don't even check for encoder_attention_mask. + # this is to maintain compatibility with UnCLIP, which uses 'attention_mask' param for cross-attn masks. + # TODO: UnCLIP should express cross-attn mask via encoder_attention_mask param instead of via attention_mask. + # then we can simplify this whole if/else block to: + # mask = attention_mask if encoder_hidden_states is None else encoder_attention_mask + mask = attention_mask + + for resnet, attn in zip(self.resnets, self.attentions): + # resnet + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, temb, scale=lora_scale) + + return hidden_states + + +class KUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + resolution_idx: int, + dropout: float = 0.0, + num_layers: int = 5, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: Optional[int] = 32, + add_upsample: bool = True, + ): + super().__init__() + resnets = [] + k_in_channels = 2 * out_channels + k_out_channels = in_channels + num_layers = num_layers - 1 + + for i in range(num_layers): + in_channels = k_in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=k_out_channels if (i == num_layers - 1) else out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=groups, + groups_out=groups_out, + dropout=dropout, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([KUpsample2D()]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + scale: float = 1.0, + ) -> torch.FloatTensor: + res_hidden_states_tuple = res_hidden_states_tuple[-1] + if res_hidden_states_tuple is not None: + hidden_states = torch.cat([hidden_states, res_hidden_states_tuple], dim=1) + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb, scale=scale) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class KCrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + resolution_idx: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: int = 32, + attention_head_dim: int = 1, # attention dim_head + cross_attention_dim: int = 768, + add_upsample: bool = True, + upcast_attention: bool = False, + ): + super().__init__() + resnets = [] + attentions = [] + + is_first_block = in_channels == out_channels == temb_channels + is_middle_block = in_channels != out_channels + add_self_attention = True if is_first_block else False + + self.has_cross_attention = True + self.attention_head_dim = attention_head_dim + + # in_channels, and out_channels for the block (k-unet) + k_in_channels = out_channels if is_first_block else 2 * out_channels + k_out_channels = in_channels + + num_layers = num_layers - 1 + + for i in range(num_layers): + in_channels = k_in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + if is_middle_block and (i == num_layers - 1): + conv_2d_out_channels = k_out_channels + else: + conv_2d_out_channels = None + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + conv_2d_out_channels=conv_2d_out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=groups, + groups_out=groups_out, + dropout=dropout, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + attentions.append( + KAttentionBlock( + k_out_channels if (i == num_layers - 1) else out_channels, + k_out_channels // attention_head_dim + if (i == num_layers - 1) + else out_channels // attention_head_dim, + attention_head_dim, + cross_attention_dim=cross_attention_dim, + temb_channels=temb_channels, + attention_bias=True, + add_self_attention=add_self_attention, + cross_attention_norm="layer_norm", + upcast_attention=upcast_attention, + ) + ) + + self.resnets = nn.ModuleList(resnets) + self.attentions = nn.ModuleList(attentions) + + if add_upsample: + self.upsamplers = nn.ModuleList([KUpsample2D()]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + upsample_size: Optional[int] = None, + attention_mask: Optional[torch.FloatTensor] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + res_hidden_states_tuple = res_hidden_states_tuple[-1] + if res_hidden_states_tuple is not None: + hidden_states = torch.cat([hidden_states, res_hidden_states_tuple], dim=1) + + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +# can potentially later be renamed to `No-feed-forward` attention +class KAttentionBlock(nn.Module): + r""" + A basic Transformer block. + + Parameters: + dim (`int`): The number of channels in the input and output. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + attention_bias (`bool`, *optional*, defaults to `False`): + Configure if the attention layers should contain a bias parameter. + upcast_attention (`bool`, *optional*, defaults to `False`): + Set to `True` to upcast the attention computation to `float32`. + temb_channels (`int`, *optional*, defaults to 768): + The number of channels in the token embedding. + add_self_attention (`bool`, *optional*, defaults to `False`): + Set to `True` to add self-attention to the block. + cross_attention_norm (`str`, *optional*, defaults to `None`): + The type of normalization to use for the cross attention. Can be `None`, `layer_norm`, or `group_norm`. + group_size (`int`, *optional*, defaults to 32): + The number of groups to separate the channels into for group normalization. + """ + + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + dropout: float = 0.0, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + upcast_attention: bool = False, + temb_channels: int = 768, # for ada_group_norm + add_self_attention: bool = False, + cross_attention_norm: Optional[str] = None, + group_size: int = 32, + ): + super().__init__() + self.add_self_attention = add_self_attention + + # 1. Self-Attn + if add_self_attention: + self.norm1 = AdaGroupNorm(temb_channels, dim, max(1, dim // group_size)) + self.attn1 = Attention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=None, + cross_attention_norm=None, + ) + + # 2. Cross-Attn + self.norm2 = AdaGroupNorm(temb_channels, dim, max(1, dim // group_size)) + self.attn2 = Attention( + query_dim=dim, + cross_attention_dim=cross_attention_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + upcast_attention=upcast_attention, + cross_attention_norm=cross_attention_norm, + ) + + def _to_3d(self, hidden_states: torch.FloatTensor, height: int, weight: int) -> torch.FloatTensor: + return hidden_states.permute(0, 2, 3, 1).reshape(hidden_states.shape[0], height * weight, -1) + + def _to_4d(self, hidden_states: torch.FloatTensor, height: int, weight: int) -> torch.FloatTensor: + return hidden_states.permute(0, 2, 1).reshape(hidden_states.shape[0], -1, height, weight) + + def forward( + self, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + # TODO: mark emb as non-optional (self.norm2 requires it). + # requires assessing impact of change to positional param interface. + emb: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + + # 1. Self-Attention + if self.add_self_attention: + norm_hidden_states = self.norm1(hidden_states, emb) + + height, weight = norm_hidden_states.shape[2:] + norm_hidden_states = self._to_3d(norm_hidden_states, height, weight) + + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=None, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + attn_output = self._to_4d(attn_output, height, weight) + + hidden_states = attn_output + hidden_states + + # 2. Cross-Attention/None + norm_hidden_states = self.norm2(hidden_states, emb) + + height, weight = norm_hidden_states.shape[2:] + norm_hidden_states = self._to_3d(norm_hidden_states, height, weight) + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask if encoder_hidden_states is None else encoder_attention_mask, + **cross_attention_kwargs, + ) + attn_output = self._to_4d(attn_output, height, weight) + + hidden_states = attn_output + hidden_states + + return hidden_states diff --git a/foleycrafter/models/auffusion_unet.py b/foleycrafter/models/auffusion_unet.py new file mode 100644 index 0000000..74a40e4 --- /dev/null +++ b/foleycrafter/models/auffusion_unet.py @@ -0,0 +1,1257 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.nn as nn +import torch.utils.checkpoint + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.models.activations import get_activation + +# from diffusers import StableDiffusionGLIGENPipeline +from diffusers.models.attention_processor import ( + ADDED_KV_ATTENTION_PROCESSORS, + CROSS_ATTENTION_PROCESSORS, + Attention, + AttentionProcessor, + AttnAddedKVProcessor, + AttnProcessor, + XFormersAttnProcessor, +) +from diffusers.models.embeddings import ( + GaussianFourierProjection, + ImageHintTimeEmbedding, + ImageProjection, + ImageTimeEmbedding, + TextImageProjection, + TextImageTimeEmbedding, + TextTimeEmbedding, + TimestepEmbedding, + Timesteps, +) +from diffusers.models.modeling_utils import ModelMixin +from diffusers.utils import USE_PEFT_BACKEND, BaseOutput, deprecate, logging, scale_lora_layers, unscale_lora_layers +from diffusers.utils.import_utils import is_xformers_available +from .adapters.ip_adapter import TimeProjModel +from .auffusion.attention_processor import AttnProcessor2_0 +from .auffusion.loaders.unet import UNet2DConditionLoadersMixin +from .auffusion.unet_2d_blocks import ( + UNetMidBlock2D, + UNetMidBlock2DCrossAttn, + UNetMidBlock2DSimpleCrossAttn, + get_down_block, + get_up_block, +) + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +class UNet2DConditionOutput(BaseOutput): + """ + The output of [`UNet2DConditionModel`]. + + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + The hidden states output conditioned on `encoder_hidden_states` input. Output of last layer of model. + """ + + sample: torch.FloatTensor = None + + +class UNet2DConditionModel(ModelMixin, ConfigMixin, UNet2DConditionLoadersMixin): + r""" + A conditional 2D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample + shaped output. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for it's generic methods implemented + for all models (such as downloading or saving). + + Parameters: + sample_size (`int` or `Tuple[int, int]`, *optional*, defaults to `None`): + Height and width of input/output sample. + in_channels (`int`, *optional*, defaults to 4): Number of channels in the input sample. + out_channels (`int`, *optional*, defaults to 4): Number of channels in the output. + center_input_sample (`bool`, *optional*, defaults to `False`): Whether to center the input sample. + flip_sin_to_cos (`bool`, *optional*, defaults to `False`): + Whether to flip the sin to cos in the time embedding. + freq_shift (`int`, *optional*, defaults to 0): The frequency shift to apply to the time embedding. + down_block_types (`Tuple[str]`, *optional*, defaults to `("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "DownBlock2D")`): + The tuple of downsample blocks to use. + mid_block_type (`str`, *optional*, defaults to `"UNetMidBlock2DCrossAttn"`): + Block type for middle of UNet, it can be one of `UNetMidBlock2DCrossAttn`, `UNetMidBlock2D`, or + `UNetMidBlock2DSimpleCrossAttn`. If `None`, the mid block layer is skipped. + up_block_types (`Tuple[str]`, *optional*, defaults to `("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D")`): + The tuple of upsample blocks to use. + only_cross_attention(`bool` or `Tuple[bool]`, *optional*, default to `False`): + Whether to include self-attention in the basic transformer blocks, see + [`~models.attention.BasicTransformerBlock`]. + block_out_channels (`Tuple[int]`, *optional*, defaults to `(320, 640, 1280, 1280)`): + The tuple of output channels for each block. + layers_per_block (`int`, *optional*, defaults to 2): The number of layers per block. + downsample_padding (`int`, *optional*, defaults to 1): The padding to use for the downsampling convolution. + mid_block_scale_factor (`float`, *optional*, defaults to 1.0): The scale factor to use for the mid block. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + norm_num_groups (`int`, *optional*, defaults to 32): The number of groups to use for the normalization. + If `None`, normalization and activation layers is skipped in post-processing. + norm_eps (`float`, *optional*, defaults to 1e-5): The epsilon to use for the normalization. + cross_attention_dim (`int` or `Tuple[int]`, *optional*, defaults to 1280): + The dimension of the cross attention features. + transformer_layers_per_block (`int`, `Tuple[int]`, or `Tuple[Tuple]` , *optional*, defaults to 1): + The number of transformer blocks of type [`~models.attention.BasicTransformerBlock`]. Only relevant for + [`~models.unet_2d_blocks.CrossAttnDownBlock2D`], [`~models.unet_2d_blocks.CrossAttnUpBlock2D`], + [`~models.unet_2d_blocks.UNetMidBlock2DCrossAttn`]. + reverse_transformer_layers_per_block : (`Tuple[Tuple]`, *optional*, defaults to None): + The number of transformer blocks of type [`~models.attention.BasicTransformerBlock`], in the upsampling + blocks of the U-Net. Only relevant if `transformer_layers_per_block` is of type `Tuple[Tuple]` and for + [`~models.unet_2d_blocks.CrossAttnDownBlock2D`], [`~models.unet_2d_blocks.CrossAttnUpBlock2D`], + [`~models.unet_2d_blocks.UNetMidBlock2DCrossAttn`]. + encoder_hid_dim (`int`, *optional*, defaults to None): + If `encoder_hid_dim_type` is defined, `encoder_hidden_states` will be projected from `encoder_hid_dim` + dimension to `cross_attention_dim`. + encoder_hid_dim_type (`str`, *optional*, defaults to `None`): + If given, the `encoder_hidden_states` and potentially other embeddings are down-projected to text + embeddings of dimension `cross_attention` according to `encoder_hid_dim_type`. + attention_head_dim (`int`, *optional*, defaults to 8): The dimension of the attention heads. + num_attention_heads (`int`, *optional*): + The number of attention heads. If not defined, defaults to `attention_head_dim` + resnet_time_scale_shift (`str`, *optional*, defaults to `"default"`): Time scale shift config + for ResNet blocks (see [`~models.resnet.ResnetBlock2D`]). Choose from `default` or `scale_shift`. + class_embed_type (`str`, *optional*, defaults to `None`): + The type of class embedding to use which is ultimately summed with the time embeddings. Choose from `None`, + `"timestep"`, `"identity"`, `"projection"`, or `"simple_projection"`. + addition_embed_type (`str`, *optional*, defaults to `None`): + Configures an optional embedding which will be summed with the time embeddings. Choose from `None` or + "text". "text" will use the `TextTimeEmbedding` layer. + addition_time_embed_dim: (`int`, *optional*, defaults to `None`): + Dimension for the timestep embeddings. + num_class_embeds (`int`, *optional*, defaults to `None`): + Input dimension of the learnable embedding matrix to be projected to `time_embed_dim`, when performing + class conditioning with `class_embed_type` equal to `None`. + time_embedding_type (`str`, *optional*, defaults to `positional`): + The type of position embedding to use for timesteps. Choose from `positional` or `fourier`. + time_embedding_dim (`int`, *optional*, defaults to `None`): + An optional override for the dimension of the projected time embedding. + time_embedding_act_fn (`str`, *optional*, defaults to `None`): + Optional activation function to use only once on the time embeddings before they are passed to the rest of + the UNet. Choose from `silu`, `mish`, `gelu`, and `swish`. + timestep_post_act (`str`, *optional*, defaults to `None`): + The second activation function to use in timestep embedding. Choose from `silu`, `mish` and `gelu`. + time_cond_proj_dim (`int`, *optional*, defaults to `None`): + The dimension of `cond_proj` layer in the timestep embedding. + conv_in_kernel (`int`, *optional*, default to `3`): The kernel size of `conv_in` layer. conv_out_kernel (`int`, + *optional*, default to `3`): The kernel size of `conv_out` layer. projection_class_embeddings_input_dim (`int`, + *optional*): The dimension of the `class_labels` input when + `class_embed_type="projection"`. Required when `class_embed_type="projection"`. + class_embeddings_concat (`bool`, *optional*, defaults to `False`): Whether to concatenate the time + embeddings with the class embeddings. + mid_block_only_cross_attention (`bool`, *optional*, defaults to `None`): + Whether to use cross attention with the mid block when using the `UNetMidBlock2DSimpleCrossAttn`. If + `only_cross_attention` is given as a single boolean and `mid_block_only_cross_attention` is `None`, the + `only_cross_attention` value is used as the value for `mid_block_only_cross_attention`. Default to `False` + otherwise. + """ + + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + sample_size: Optional[int] = None, + in_channels: int = 4, + out_channels: int = 4, + center_input_sample: bool = False, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ), + mid_block_type: Optional[str] = "UNetMidBlock2DCrossAttn", + up_block_types: Tuple[str] = ("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D"), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int] = (320, 640, 1280, 1280), + layers_per_block: Union[int, Tuple[int]] = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + dropout: float = 0.0, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: Union[int, Tuple[int]] = 1280, + transformer_layers_per_block: Union[int, Tuple[int], Tuple[Tuple]] = 1, + reverse_transformer_layers_per_block: Optional[Tuple[Tuple[int]]] = None, + encoder_hid_dim: Optional[int] = None, + encoder_hid_dim_type: Optional[str] = None, + attention_head_dim: Union[int, Tuple[int]] = 8, + num_attention_heads: Optional[Union[int, Tuple[int]]] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + addition_embed_type: Optional[str] = None, + addition_time_embed_dim: Optional[int] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + resnet_skip_time_act: bool = False, + resnet_out_scale_factor: int = 1.0, + time_embedding_type: str = "positional", + time_embedding_dim: Optional[int] = None, + time_embedding_act_fn: Optional[str] = None, + timestep_post_act: Optional[str] = None, + time_cond_proj_dim: Optional[int] = None, + conv_in_kernel: int = 3, + conv_out_kernel: int = 3, + projection_class_embeddings_input_dim: Optional[int] = None, + attention_type: str = "default", + class_embeddings_concat: bool = False, + mid_block_only_cross_attention: Optional[bool] = None, + cross_attention_norm: Optional[str] = None, + addition_embed_type_num_heads=64, + # param for joint + video_feature_dim: tuple = (320, 640, 1280, 1280), + video_cross_attn_dim: int = 1024, + video_frame_nums: int = 16, + ): + super().__init__() + + self.sample_size = sample_size + + if num_attention_heads is not None: + raise ValueError( + "At the moment it is not possible to define the number of attention heads via `num_attention_heads` because of a naming issue as described in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131. Passing `num_attention_heads` will only be supported in diffusers v0.19." + ) + + # If `num_attention_heads` is not defined (which is the case for most models) + # it will default to `attention_head_dim`. This looks weird upon first reading it and it is. + # The reason for this behavior is to correct for incorrectly named variables that were introduced + # when this library was created. The incorrect naming was only discovered much later in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131 + # Changing `attention_head_dim` to `num_attention_heads` for 40,000+ configurations is too backwards breaking + # which is why we correct for the naming here. + num_attention_heads = num_attention_heads or attention_head_dim + + # Check inputs + if len(down_block_types) != len(up_block_types): + raise ValueError( + f"Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`: {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `only_cross_attention` as `down_block_types`. `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(num_attention_heads, int) and len(num_attention_heads) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `num_attention_heads` as `down_block_types`. `num_attention_heads`: {num_attention_heads}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(attention_head_dim, int) and len(attention_head_dim) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `attention_head_dim` as `down_block_types`. `attention_head_dim`: {attention_head_dim}. `down_block_types`: {down_block_types}." + ) + + if isinstance(cross_attention_dim, list) and len(cross_attention_dim) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `cross_attention_dim` as `down_block_types`. `cross_attention_dim`: {cross_attention_dim}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(layers_per_block, int) and len(layers_per_block) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `layers_per_block` as `down_block_types`. `layers_per_block`: {layers_per_block}. `down_block_types`: {down_block_types}." + ) + if isinstance(transformer_layers_per_block, list) and reverse_transformer_layers_per_block is None: + for layer_number_per_block in transformer_layers_per_block: + if isinstance(layer_number_per_block, list): + raise ValueError("Must provide 'reverse_transformer_layers_per_block` if using asymmetrical UNet.") + + # input + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in = nn.Conv2d( + in_channels, block_out_channels[0], kernel_size=conv_in_kernel, padding=conv_in_padding + ) + + # time + if time_embedding_type == "fourier": + time_embed_dim = time_embedding_dim or block_out_channels[0] * 2 + if time_embed_dim % 2 != 0: + raise ValueError(f"`time_embed_dim` should be divisible by 2, but is {time_embed_dim}.") + self.time_proj = GaussianFourierProjection( + time_embed_dim // 2, set_W_to_weight=False, log=False, flip_sin_to_cos=flip_sin_to_cos + ) + timestep_input_dim = time_embed_dim + elif time_embedding_type == "positional": + time_embed_dim = time_embedding_dim or block_out_channels[0] * 4 + + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + else: + raise ValueError( + f"{time_embedding_type} does not exist. Please make sure to use one of `fourier` or `positional`." + ) + + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + post_act_fn=timestep_post_act, + cond_proj_dim=time_cond_proj_dim, + ) + + if encoder_hid_dim_type is None and encoder_hid_dim is not None: + encoder_hid_dim_type = "text_proj" + self.register_to_config(encoder_hid_dim_type=encoder_hid_dim_type) + logger.info("encoder_hid_dim_type defaults to 'text_proj' as `encoder_hid_dim` is defined.") + + if encoder_hid_dim is None and encoder_hid_dim_type is not None: + raise ValueError( + f"`encoder_hid_dim` has to be defined when `encoder_hid_dim_type` is set to {encoder_hid_dim_type}." + ) + + if encoder_hid_dim_type == "text_proj": + self.encoder_hid_proj = nn.Linear(encoder_hid_dim, cross_attention_dim) + elif encoder_hid_dim_type == "text_image_proj": + # image_embed_dim DOESN'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image_proj"` (Kadinsky 2.1)` + self.encoder_hid_proj = TextImageProjection( + text_embed_dim=encoder_hid_dim, + image_embed_dim=cross_attention_dim, + cross_attention_dim=cross_attention_dim, + ) + elif encoder_hid_dim_type == "image_proj": + # Kandinsky 2.2 + self.encoder_hid_proj = ImageProjection( + image_embed_dim=encoder_hid_dim, + cross_attention_dim=cross_attention_dim, + ) + elif encoder_hid_dim_type is not None: + raise ValueError( + f"encoder_hid_dim_type: {encoder_hid_dim_type} must be None, 'text_proj' or 'text_image_proj'." + ) + else: + self.encoder_hid_proj = None + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim, act_fn=act_fn) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + elif class_embed_type == "projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'projection' requires `projection_class_embeddings_input_dim` be set" + ) + # The projection `class_embed_type` is the same as the timestep `class_embed_type` except + # 1. the `class_labels` inputs are not first converted to sinusoidal embeddings + # 2. it projects from an arbitrary input dimension. + # + # Note that `TimestepEmbedding` is quite general, being mainly linear layers and activations. + # When used for embedding actual timesteps, the timesteps are first converted to sinusoidal embeddings. + # As a result, `TimestepEmbedding` can be passed arbitrary vectors. + self.class_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + elif class_embed_type == "simple_projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'simple_projection' requires `projection_class_embeddings_input_dim` be set" + ) + self.class_embedding = nn.Linear(projection_class_embeddings_input_dim, time_embed_dim) + else: + self.class_embedding = None + + if addition_embed_type == "text": + if encoder_hid_dim is not None: + text_time_embedding_from_dim = encoder_hid_dim + else: + text_time_embedding_from_dim = cross_attention_dim + + self.add_embedding = TextTimeEmbedding( + text_time_embedding_from_dim, time_embed_dim, num_heads=addition_embed_type_num_heads + ) + elif addition_embed_type == "text_image": + # text_embed_dim and image_embed_dim DON'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image"` (Kadinsky 2.1)` + self.add_embedding = TextImageTimeEmbedding( + text_embed_dim=cross_attention_dim, image_embed_dim=cross_attention_dim, time_embed_dim=time_embed_dim + ) + elif addition_embed_type == "text_time": + self.add_time_proj = Timesteps(addition_time_embed_dim, flip_sin_to_cos, freq_shift) + self.add_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + elif addition_embed_type == "image": + # Kandinsky 2.2 + self.add_embedding = ImageTimeEmbedding(image_embed_dim=encoder_hid_dim, time_embed_dim=time_embed_dim) + elif addition_embed_type == "image_hint": + # Kandinsky 2.2 ControlNet + self.add_embedding = ImageHintTimeEmbedding(image_embed_dim=encoder_hid_dim, time_embed_dim=time_embed_dim) + elif addition_embed_type is not None: + raise ValueError(f"addition_embed_type: {addition_embed_type} must be None, 'text' or 'text_image'.") + + if time_embedding_act_fn is None: + self.time_embed_act = None + else: + self.time_embed_act = get_activation(time_embedding_act_fn) + + self.down_blocks = nn.ModuleList([]) + self.up_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + if mid_block_only_cross_attention is None: + mid_block_only_cross_attention = only_cross_attention + + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if mid_block_only_cross_attention is None: + mid_block_only_cross_attention = False + + if isinstance(num_attention_heads, int): + num_attention_heads = (num_attention_heads,) * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + if isinstance(cross_attention_dim, int): + cross_attention_dim = (cross_attention_dim,) * len(down_block_types) + + if isinstance(layers_per_block, int): + layers_per_block = [layers_per_block] * len(down_block_types) + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * len(down_block_types) + + if class_embeddings_concat: + # The time embeddings are concatenated with the class embeddings. The dimension of the + # time embeddings passed to the down, middle, and up blocks is twice the dimension of the + # regular time embeddings + blocks_time_embed_dim = time_embed_dim * 2 + else: + blocks_time_embed_dim = time_embed_dim + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block[i], + transformer_layers_per_block=transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + temb_channels=blocks_time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim[i], + num_attention_heads=num_attention_heads[i], + downsample_padding=downsample_padding, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + resnet_skip_time_act=resnet_skip_time_act, + resnet_out_scale_factor=resnet_out_scale_factor, + cross_attention_norm=cross_attention_norm, + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + dropout=dropout, + ) + self.down_blocks.append(down_block) + + # mid + if mid_block_type == "UNetMidBlock2DCrossAttn": + self.mid_block = UNetMidBlock2DCrossAttn( + transformer_layers_per_block=transformer_layers_per_block[-1], + in_channels=block_out_channels[-1], + temb_channels=blocks_time_embed_dim, + dropout=dropout, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim[-1], + num_attention_heads=num_attention_heads[-1], + resnet_groups=norm_num_groups, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + elif mid_block_type == "UNetMidBlock2DSimpleCrossAttn": + self.mid_block = UNetMidBlock2DSimpleCrossAttn( + in_channels=block_out_channels[-1], + temb_channels=blocks_time_embed_dim, + dropout=dropout, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + cross_attention_dim=cross_attention_dim[-1], + attention_head_dim=attention_head_dim[-1], + resnet_groups=norm_num_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + only_cross_attention=mid_block_only_cross_attention, + cross_attention_norm=cross_attention_norm, + ) + elif mid_block_type == "UNetMidBlock2D": + self.mid_block = UNetMidBlock2D( + in_channels=block_out_channels[-1], + temb_channels=blocks_time_embed_dim, + dropout=dropout, + num_layers=0, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_groups=norm_num_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + add_attention=False, + ) + elif mid_block_type is None: + self.mid_block = None + else: + raise ValueError(f"unknown mid_block_type : {mid_block_type}") + + # count how many layers upsample the images + self.num_upsamplers = 0 + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_num_attention_heads = list(reversed(num_attention_heads)) + reversed_layers_per_block = list(reversed(layers_per_block)) + reversed_cross_attention_dim = list(reversed(cross_attention_dim)) + reversed_transformer_layers_per_block = ( + list(reversed(transformer_layers_per_block)) + if reverse_transformer_layers_per_block is None + else reverse_transformer_layers_per_block + ) + only_cross_attention = list(reversed(only_cross_attention)) + + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + is_final_block = i == len(block_out_channels) - 1 + + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + # add upsample block for all BUT final layer + if not is_final_block: + add_upsample = True + self.num_upsamplers += 1 + else: + add_upsample = False + + up_block = get_up_block( + up_block_type, + num_layers=reversed_layers_per_block[i] + 1, + transformer_layers_per_block=reversed_transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=blocks_time_embed_dim, + add_upsample=add_upsample, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resolution_idx=i, + resnet_groups=norm_num_groups, + cross_attention_dim=reversed_cross_attention_dim[i], + num_attention_heads=reversed_num_attention_heads[i], + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + resnet_skip_time_act=resnet_skip_time_act, + resnet_out_scale_factor=resnet_out_scale_factor, + cross_attention_norm=cross_attention_norm, + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + dropout=dropout, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + if norm_num_groups is not None: + self.conv_norm_out = nn.GroupNorm( + num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=norm_eps + ) + + self.conv_act = get_activation(act_fn) + + else: + self.conv_norm_out = None + self.conv_act = None + + conv_out_padding = (conv_out_kernel - 1) // 2 + self.conv_out = nn.Conv2d( + block_out_channels[0], out_channels, kernel_size=conv_out_kernel, padding=conv_out_padding + ) + + if attention_type in ["gated", "gated-text-image"]: + positive_len = 768 + if isinstance(cross_attention_dim, int): + positive_len = cross_attention_dim + elif isinstance(cross_attention_dim, tuple) or isinstance(cross_attention_dim, list): + positive_len = cross_attention_dim[0] + + feature_type = "text-only" if attention_type == "gated" else "text-image" + self.position_net = TimeProjModel( + positive_len=positive_len, out_dim=cross_attention_dim, feature_type=feature_type + ) + + # additional settings + self.video_feature_dim = video_feature_dim + self.cross_attention_dim = cross_attention_dim + self.video_cross_attn_dim = video_cross_attn_dim + self.video_frame_nums = video_frame_nums + + self.multi_frames_condition = False + + def load_attention(self): + attn_dict = {} + for name in self.attn_processors.keys(): + # if self-attention, save feature + if name.endswith("attn1.processor"): + if is_xformers_available(): + attn_dict[name] = XFormersAttnProcessor() + else: + attn_dict[name] = AttnProcessor() + else: + attn_dict[name] = AttnProcessor2_0() + self.set_attn_processor(attn_dict) + + def get_writer_feature(self): + return self.attn_feature_writer.get_cross_attention_feature() + + def clear_writer_feature(self): + self.attn_feature_writer.clear_cross_attention_feature() + + def disable_feature_adapters(self): + raise NotImplementedError + + def set_reader_feature(self, features: list): + return self.attn_feature_reader.set_cross_attention_feature(features) + + @property + def attn_processors(self) -> Dict[str, AttentionProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, processors: Dict[str, AttentionProcessor]): + if hasattr(module, "get_processor"): + processors[f"{name}.processor"] = module.get_processor(return_deprecated_lora=True) + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + def set_attn_processor( + self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]], _remove_lora=False + ): + r""" + Sets the attention processor to use to compute attention. + + Parameters: + processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + for **all** `Attention` layers. + + If `processor` is a dict, the key needs to define the path to the corresponding cross attention + processor. This is strongly recommended when setting trainable attention processors. + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor, _remove_lora=_remove_lora) + else: + module.set_processor(processor.pop(f"{name}.processor"), _remove_lora=_remove_lora) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + def set_default_attn_processor(self): + """ + Disables custom attention processors and sets the default attention implementation. + """ + if all(proc.__class__ in ADDED_KV_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnAddedKVProcessor() + elif all(proc.__class__ in CROSS_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnProcessor() + else: + raise ValueError( + f"Cannot call `set_default_attn_processor` when attention processors are of type {next(iter(self.attn_processors.values()))}" + ) + + self.set_attn_processor(processor, _remove_lora=True) + + def set_attention_slice(self, slice_size): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module splits the input tensor in slices to compute attention in + several steps. This is useful for saving some memory in exchange for a small decrease in speed. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, input to the attention heads is halved, so attention is computed in two steps. If + `"max"`, maximum amount of memory is saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_sliceable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_sliceable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_sliceable_dims(module) + + num_sliceable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_sliceable_layers * [1] + + slice_size = num_sliceable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value=False): + if hasattr(module, "gradient_checkpointing"): + module.gradient_checkpointing = value + + def enable_freeu(self, s1, s2, b1, b2): + r"""Enables the FreeU mechanism from https://arxiv.org/abs/2309.11497. + + The suffixes after the scaling factors represent the stage blocks where they are being applied. + + Please refer to the [official repository](https://github.com/ChenyangSi/FreeU) for combinations of values that + are known to work well for different pipelines such as Stable Diffusion v1, v2, and Stable Diffusion XL. + + Args: + s1 (`float`): + Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to + mitigate the "oversmoothing effect" in the enhanced denoising process. + s2 (`float`): + Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to + mitigate the "oversmoothing effect" in the enhanced denoising process. + b1 (`float`): Scaling factor for stage 1 to amplify the contributions of backbone features. + b2 (`float`): Scaling factor for stage 2 to amplify the contributions of backbone features. + """ + for i, upsample_block in enumerate(self.up_blocks): + setattr(upsample_block, "s1", s1) + setattr(upsample_block, "s2", s2) + setattr(upsample_block, "b1", b1) + setattr(upsample_block, "b2", b2) + + def disable_freeu(self): + """Disables the FreeU mechanism.""" + freeu_keys = {"s1", "s2", "b1", "b2"} + for i, upsample_block in enumerate(self.up_blocks): + for k in freeu_keys: + if hasattr(upsample_block, k) or getattr(upsample_block, k, None) is not None: + setattr(upsample_block, k, None) + + def fuse_qkv_projections(self): + """ + Enables fused QKV projections. For self-attention modules, all projection matrices (i.e., query, + key, value) are fused. For cross-attention modules, key and value projection matrices are fused. + + + + This API is 🧪 experimental. + + + """ + self.original_attn_processors = None + + for _, attn_processor in self.attn_processors.items(): + if "Added" in str(attn_processor.__class__.__name__): + raise ValueError("`fuse_qkv_projections()` is not supported for models having added KV projections.") + + self.original_attn_processors = self.attn_processors + + for module in self.modules(): + if isinstance(module, Attention): + module.fuse_projections(fuse=True) + + def unfuse_qkv_projections(self): + """Disables the fused QKV projection if enabled. + + + + This API is 🧪 experimental. + + + + """ + if self.original_attn_processors is not None: + self.set_attn_processor(self.original_attn_processors) + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + mid_block_additional_residual: Optional[torch.Tensor] = None, + down_intrablock_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + return_dict: bool = True, + ) -> Union[UNet2DConditionOutput, Tuple]: + # import ipdb; ipdb.set_trace() + r""" + The [`UNet2DConditionModel`] forward method. + + Args: + sample (`torch.FloatTensor`): + The noisy input tensor with the following shape `(batch, channel, height, width)`. + timestep (`torch.FloatTensor` or `float` or `int`): The number of timesteps to denoise an input. + encoder_hidden_states (`torch.FloatTensor`): + The encoder hidden states with shape `(batch, sequence_length, feature_dim)`. + class_labels (`torch.Tensor`, *optional*, defaults to `None`): + Optional class labels for conditioning. Their embeddings will be summed with the timestep embeddings. + timestep_cond: (`torch.Tensor`, *optional*, defaults to `None`): + Conditional embeddings for timestep. If provided, the embeddings will be summed with the samples passed + through the `self.time_embedding` layer to obtain the timestep embeddings. + attention_mask (`torch.Tensor`, *optional*, defaults to `None`): + An attention mask of shape `(batch, key_tokens)` is applied to `encoder_hidden_states`. If `1` the mask + is kept, otherwise if `0` it is discarded. Mask will be converted into a bias, which adds large + negative values to the attention scores corresponding to "discard" tokens. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under + `self.processor` in + [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + added_cond_kwargs: (`dict`, *optional*): + A kwargs dictionary containing additional embeddings that if specified are added to the embeddings that + are passed along to the UNet blocks. + down_block_additional_residuals: (`tuple` of `torch.Tensor`, *optional*): + A tuple of tensors that if specified are added to the residuals of down unet blocks. + mid_block_additional_residual: (`torch.Tensor`, *optional*): + A tensor that if specified is added to the residual of the middle unet block. + encoder_attention_mask (`torch.Tensor`): + A cross-attention mask of shape `(batch, sequence_length)` is applied to `encoder_hidden_states`. If + `True` the mask is kept, otherwise if `False` it is discarded. Mask will be converted into a bias, + which adds large negative values to the attention scores corresponding to "discard" tokens. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain + tuple. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the [`AttnProcessor`]. + added_cond_kwargs: (`dict`, *optional*): + A kwargs dictionary containin additional embeddings that if specified are added to the embeddings that + are passed along to the UNet blocks. + down_block_additional_residuals (`tuple` of `torch.Tensor`, *optional*): + additional residuals to be added to UNet long skip connections from down blocks to up blocks for + example from ControlNet side model(s) + mid_block_additional_residual (`torch.Tensor`, *optional*): + additional residual to be added to UNet mid block output, for example from ControlNet side model + down_intrablock_additional_residuals (`tuple` of `torch.Tensor`, *optional*): + additional residuals to be added within UNet down blocks, for example from T2I-Adapter side model(s) + + Returns: + [`~models.unet_2d_condition.UNet2DConditionOutput`] or `tuple`: + If `return_dict` is True, an [`~models.unet_2d_condition.UNet2DConditionOutput`] is returned, otherwise + a `tuple` is returned where the first element is the sample tensor. + """ + # By default samples have to be AT least a multiple of the overall upsampling factor. + # The overall upsampling factor is equal to 2 ** (# num of upsampling layers). + # However, the upsampling interpolation output size can be forced to fit any upsampling size + # on the fly if necessary. + default_overall_up_factor = 2**self.num_upsamplers + + # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor` + forward_upsample_size = False + upsample_size = None + + for dim in sample.shape[-2:]: + if dim % default_overall_up_factor != 0: + # Forward upsample size to force interpolation output size. + forward_upsample_size = True + break + + # ensure attention_mask is a bias, and give it a singleton query_tokens dimension + # expects mask of shape: + # [batch, key_tokens] + # adds singleton query_tokens dimension: + # [batch, 1, key_tokens] + # this helps to broadcast it as a bias over attention scores, which will be in one of the following shapes: + # [batch, heads, query_tokens, key_tokens] (e.g. torch sdp attn) + # [batch * heads, query_tokens, key_tokens] (e.g. xformers or classic attn) + if attention_mask is not None: + # assume that mask is expressed as: + # (1 = keep, 0 = discard) + # convert mask into a bias that can be added to attention scores: + # (keep = +0, discard = -10000.0) + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # convert encoder_attention_mask to a bias the same way we do for attention_mask + if encoder_attention_mask is not None: + encoder_attention_mask = (1 - encoder_attention_mask.to(sample.dtype)) * -10000.0 + encoder_attention_mask = encoder_attention_mask.unsqueeze(1) + + # 0. center input if necessary + if self.config.center_input_sample: + sample = 2 * sample - 1.0 + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # `Timesteps` does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=sample.dtype) + + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + # `Timesteps` does not contain any weights and will always return f32 tensors + # there might be better ways to encapsulate this. + class_labels = class_labels.to(dtype=sample.dtype) + + class_emb = self.class_embedding(class_labels).to(dtype=sample.dtype) + + if self.config.class_embeddings_concat: + emb = torch.cat([emb, class_emb], dim=-1) + else: + emb = emb + class_emb + + if self.config.addition_embed_type == "text": + aug_emb = self.add_embedding(encoder_hidden_states) + elif self.config.addition_embed_type == "text_image": + # Kandinsky 2.1 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_image' which requires the keyword argument `image_embeds` to be passed in `added_cond_kwargs`" + ) + + image_embs = added_cond_kwargs.get("image_embeds") + text_embs = added_cond_kwargs.get("text_embeds", encoder_hidden_states) + aug_emb = self.add_embedding(text_embs, image_embs) + elif self.config.addition_embed_type == "text_time": + # SDXL - style + if "text_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `text_embeds` to be passed in `added_cond_kwargs`" + ) + text_embeds = added_cond_kwargs.get("text_embeds") + if "time_ids" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `time_ids` to be passed in `added_cond_kwargs`" + ) + time_ids = added_cond_kwargs.get("time_ids") + time_embeds = self.add_time_proj(time_ids.flatten()) + time_embeds = time_embeds.reshape((text_embeds.shape[0], -1)) + add_embeds = torch.concat([text_embeds, time_embeds], dim=-1) + add_embeds = add_embeds.to(emb.dtype) + aug_emb = self.add_embedding(add_embeds) + elif self.config.addition_embed_type == "image": + # Kandinsky 2.2 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'image' which requires the keyword argument `image_embeds` to be passed in `added_cond_kwargs`" + ) + image_embs = added_cond_kwargs.get("image_embeds") + aug_emb = self.add_embedding(image_embs) + elif self.config.addition_embed_type == "image_hint": + # Kandinsky 2.2 - style + if "image_embeds" not in added_cond_kwargs or "hint" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'image_hint' which requires the keyword arguments `image_embeds` and `hint` to be passed in `added_cond_kwargs`" + ) + image_embs = added_cond_kwargs.get("image_embeds") + hint = added_cond_kwargs.get("hint") + aug_emb, hint = self.add_embedding(image_embs, hint) + sample = torch.cat([sample, hint], dim=1) + + emb = emb + aug_emb if aug_emb is not None else emb + + if self.time_embed_act is not None: + emb = self.time_embed_act(emb) + + if self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "text_proj": + encoder_hidden_states = self.encoder_hid_proj(encoder_hidden_states) + elif self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "text_image_proj": + # Kadinsky 2.1 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'text_image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + + image_embeds = added_cond_kwargs.get("image_embeds") + encoder_hidden_states = self.encoder_hid_proj(encoder_hidden_states, image_embeds) + elif self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "image_proj": + # Kandinsky 2.2 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + image_embeds = added_cond_kwargs.get("image_embeds") + encoder_hidden_states = self.encoder_hid_proj(image_embeds) + elif self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "ip_image_proj": + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'ip_image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + image_embeds = added_cond_kwargs.get("image_embeds") + image_embeds = self.encoder_hid_proj(image_embeds) + if isinstance(image_embeds, list): + image_embeds = [image_embed.to(encoder_hidden_states.dtype) for image_embed in image_embeds] + else: + image_embeds = image_embeds.to(encoder_hidden_states.dtype) + encoder_hidden_states = (encoder_hidden_states, image_embeds) + # encoder_hidden_states = torch.cat([encoder_hidden_states, image_embeds], dim=1) + # import ipdb; ipdb.set_trace() + # 2. pre-process + sample = self.conv_in(sample) + + # 2.5 GLIGEN position net + if cross_attention_kwargs is not None and cross_attention_kwargs.get("gligen", None) is not None: + cross_attention_kwargs = cross_attention_kwargs.copy() + gligen_args = cross_attention_kwargs.pop("gligen") + cross_attention_kwargs["gligen"] = {"objs": self.position_net(**gligen_args)} + + # 3. down + lora_scale = cross_attention_kwargs.get("scale", 1.0) if cross_attention_kwargs is not None else 1.0 + if USE_PEFT_BACKEND: + # weight the lora layers by setting `lora_scale` for each PEFT layer + scale_lora_layers(self, lora_scale) + + is_controlnet = mid_block_additional_residual is not None and down_block_additional_residuals is not None + # using new arg down_intrablock_additional_residuals for T2I-Adapters, to distinguish from controlnets + is_adapter = down_intrablock_additional_residuals is not None + # maintain backward compatibility for legacy usage, where + # T2I-Adapter and ControlNet both use down_block_additional_residuals arg + # but can only use one or the other + if not is_adapter and mid_block_additional_residual is None and down_block_additional_residuals is not None: + deprecate( + "T2I should not use down_block_additional_residuals", + "1.3.0", + "Passing intrablock residual connections with `down_block_additional_residuals` is deprecated \ + and will be removed in diffusers 1.3.0. `down_block_additional_residuals` should only be used \ + for ControlNet. Please make sure use `down_intrablock_additional_residuals` instead. ", + standard_warn=False, + ) + down_intrablock_additional_residuals = down_block_additional_residuals + is_adapter = True + # import ipdb; ipdb.set_trace() + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + # For t2i-adapter CrossAttnDownBlock2D + additional_residuals = {} + if is_adapter and len(down_intrablock_additional_residuals) > 0: + additional_residuals["additional_residuals"] = down_intrablock_additional_residuals.pop(0) + + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + **additional_residuals, + ) + # import ipdb; ipdb.set_trace() + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb, scale=lora_scale) + if is_adapter and len(down_intrablock_additional_residuals) > 0: + sample += down_intrablock_additional_residuals.pop(0) + + down_block_res_samples += res_samples + + if is_controlnet: + new_down_block_res_samples = () + + for down_block_res_sample, down_block_additional_residual in zip( + down_block_res_samples, down_block_additional_residuals + ): + down_block_res_sample = down_block_res_sample + down_block_additional_residual + new_down_block_res_samples = new_down_block_res_samples + (down_block_res_sample,) + + down_block_res_samples = new_down_block_res_samples + # 4. mid + if self.mid_block is not None: + if hasattr(self.mid_block, "has_cross_attention") and self.mid_block.has_cross_attention: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + sample = self.mid_block(sample, emb) + + # To support T2I-Adapter-XL + if ( + is_adapter + and len(down_intrablock_additional_residuals) > 0 + and sample.shape == down_intrablock_additional_residuals[0].shape + ): + sample += down_intrablock_additional_residuals.pop(0) + + if is_controlnet: + sample = sample + mid_block_additional_residual + # import ipdb; ipdb.set_trace() + # 5. up + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block and forward_upsample_size: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + ) + else: + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + upsample_size=upsample_size, + scale=lora_scale, + ) + # import ipdb; ipdb.set_trace() + # 6. post-process + if self.conv_norm_out: + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + if USE_PEFT_BACKEND: + # remove `lora_scale` from each PEFT layer + unscale_lora_layers(self, lora_scale) + + if not return_dict: + return (sample,) + # import ipdb; ipdb.set_trace() + return UNet2DConditionOutput(sample=sample) diff --git a/foleycrafter/models/onset/__init__.py b/foleycrafter/models/onset/__init__.py new file mode 100644 index 0000000..a4dc82f --- /dev/null +++ b/foleycrafter/models/onset/__init__.py @@ -0,0 +1,5 @@ +from .r2plus1d_18 import r2plus1d18KeepTemp +from .video_onset_net import VideoOnsetNet + + +__all__ = ["r2plus1d18KeepTemp", "VideoOnsetNet"] diff --git a/foleycrafter/models/onset/r2plus1d_18.py b/foleycrafter/models/onset/r2plus1d_18.py new file mode 100644 index 0000000..b33da45 --- /dev/null +++ b/foleycrafter/models/onset/r2plus1d_18.py @@ -0,0 +1,48 @@ +# Copied from specvqgan/onset_baseline/models/r2plus1d_18.py + +import torch +import torch.nn as nn + +from .resnet import r2plus1d_18 + + +class r2plus1d18KeepTemp(nn.Module): + def __init__(self, pretrained=True): + super().__init__() + + self.model = r2plus1d_18(pretrained=pretrained) + + self.model.layer2[0].conv1[0][3] = nn.Conv3d( + 230, 128, kernel_size=(3, 1, 1), stride=(1, 1, 1), padding=(1, 0, 0), bias=False + ) + self.model.layer2[0].downsample = nn.Sequential( + nn.Conv3d(64, 128, kernel_size=(1, 1, 1), stride=(1, 2, 2), bias=False), + nn.BatchNorm3d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), + ) + self.model.layer3[0].conv1[0][3] = nn.Conv3d( + 460, 256, kernel_size=(3, 1, 1), stride=(1, 1, 1), padding=(1, 0, 0), bias=False + ) + self.model.layer3[0].downsample = nn.Sequential( + nn.Conv3d(128, 256, kernel_size=(1, 1, 1), stride=(1, 2, 2), bias=False), + nn.BatchNorm3d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), + ) + self.model.layer4[0].conv1[0][3] = nn.Conv3d( + 921, 512, kernel_size=(3, 1, 1), stride=(1, 1, 1), padding=(1, 0, 0), bias=False + ) + self.model.layer4[0].downsample = nn.Sequential( + nn.Conv3d(256, 512, kernel_size=(1, 1, 1), stride=(1, 2, 2), bias=False), + nn.BatchNorm3d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), + ) + self.model.avgpool = nn.AdaptiveAvgPool3d((None, 1, 1)) + self.model.fc = nn.Identity() + + def forward(self, x): + # import pdb; pdb.set_trace() + x = self.model(x) + return x + + +if __name__ == "__main__": + model = r2plus1d18KeepTemp(False).cuda() + rand_input = torch.randn((1, 3, 30, 112, 112)).cuda() + out = model(rand_input) diff --git a/foleycrafter/models/onset/resnet.py b/foleycrafter/models/onset/resnet.py new file mode 100644 index 0000000..5838c11 --- /dev/null +++ b/foleycrafter/models/onset/resnet.py @@ -0,0 +1,323 @@ +# Copied from specvqgan/onset_baseline/models/resnet.py +import torch.nn as nn +from torch.hub import load_state_dict_from_url + + +__all__ = ["r3d_18", "mc3_18", "r2plus1d_18"] + +model_urls = { + "r3d_18": "https://download.pytorch.org/models/r3d_18-b3b3357e.pth", + "mc3_18": "https://download.pytorch.org/models/mc3_18-a90a0ba3.pth", + "r2plus1d_18": "https://download.pytorch.org/models/r2plus1d_18-91a641e6.pth", +} + + +class Conv3DSimple(nn.Conv3d): + def __init__(self, in_planes, out_planes, midplanes=None, stride=1, padding=1): + super(Conv3DSimple, self).__init__( + in_channels=in_planes, + out_channels=out_planes, + kernel_size=(3, 3, 3), + stride=stride, + padding=padding, + bias=False, + ) + + @staticmethod + def get_downsample_stride(stride): + return stride, stride, stride + + +class Conv2Plus1D(nn.Sequential): + def __init__(self, in_planes, out_planes, midplanes, stride=1, padding=1): + super(Conv2Plus1D, self).__init__( + nn.Conv3d( + in_planes, + midplanes, + kernel_size=(1, 3, 3), + stride=(1, stride, stride), + padding=(0, padding, padding), + bias=False, + ), + nn.BatchNorm3d(midplanes), + nn.ReLU(inplace=True), + nn.Conv3d( + midplanes, + out_planes, + kernel_size=(3, 1, 1), + stride=(stride, 1, 1), + padding=(padding, 0, 0), + bias=False, + ), + ) + + @staticmethod + def get_downsample_stride(stride): + return stride, stride, stride + + +class Conv3DNoTemporal(nn.Conv3d): + def __init__(self, in_planes, out_planes, midplanes=None, stride=1, padding=1): + super(Conv3DNoTemporal, self).__init__( + in_channels=in_planes, + out_channels=out_planes, + kernel_size=(1, 3, 3), + stride=(1, stride, stride), + padding=(0, padding, padding), + bias=False, + ) + + @staticmethod + def get_downsample_stride(stride): + return 1, stride, stride + + +class BasicBlock(nn.Module): + expansion = 1 + + def __init__(self, inplanes, planes, conv_builder, stride=1, downsample=None): + midplanes = (inplanes * planes * 3 * 3 * 3) // (inplanes * 3 * 3 + 3 * planes) + + super(BasicBlock, self).__init__() + self.conv1 = nn.Sequential( + conv_builder(inplanes, planes, midplanes, stride), nn.BatchNorm3d(planes), nn.ReLU(inplace=True) + ) + self.conv2 = nn.Sequential(conv_builder(planes, planes, midplanes), nn.BatchNorm3d(planes)) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.conv2(out) + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class Bottleneck(nn.Module): + expansion = 4 + + def __init__(self, inplanes, planes, conv_builder, stride=1, downsample=None): + super(Bottleneck, self).__init__() + midplanes = (inplanes * planes * 3 * 3 * 3) // (inplanes * 3 * 3 + 3 * planes) + + # 1x1x1 + self.conv1 = nn.Sequential( + nn.Conv3d(inplanes, planes, kernel_size=1, bias=False), nn.BatchNorm3d(planes), nn.ReLU(inplace=True) + ) + # Second kernel + self.conv2 = nn.Sequential( + conv_builder(planes, planes, midplanes, stride), nn.BatchNorm3d(planes), nn.ReLU(inplace=True) + ) + + # 1x1x1 + self.conv3 = nn.Sequential( + nn.Conv3d(planes, planes * self.expansion, kernel_size=1, bias=False), + nn.BatchNorm3d(planes * self.expansion), + ) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.conv2(out) + out = self.conv3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class BasicStem(nn.Sequential): + """The default conv-batchnorm-relu stem""" + + def __init__(self): + super(BasicStem, self).__init__( + nn.Conv3d(3, 64, kernel_size=(3, 7, 7), stride=(1, 2, 2), padding=(1, 3, 3), bias=False), + nn.BatchNorm3d(64), + nn.ReLU(inplace=True), + ) + + +class R2Plus1dStem(nn.Sequential): + """R(2+1)D stem is different than the default one as it uses separated 3D convolution""" + + def __init__(self): + super(R2Plus1dStem, self).__init__( + nn.Conv3d(3, 45, kernel_size=(1, 7, 7), stride=(1, 2, 2), padding=(0, 3, 3), bias=False), + nn.BatchNorm3d(45), + nn.ReLU(inplace=True), + nn.Conv3d(45, 64, kernel_size=(3, 1, 1), stride=(1, 1, 1), padding=(1, 0, 0), bias=False), + nn.BatchNorm3d(64), + nn.ReLU(inplace=True), + ) + + +class VideoResNet(nn.Module): + def __init__(self, block, conv_makers, layers, stem, num_classes=400, zero_init_residual=False): + """Generic resnet video generator. + Args: + block (nn.Module): resnet building block + conv_makers (list(functions)): generator function for each layer + layers (List[int]): number of blocks per layer + stem (nn.Module, optional): Resnet stem, if None, defaults to conv-bn-relu. Defaults to None. + num_classes (int, optional): Dimension of the final FC layer. Defaults to 400. + zero_init_residual (bool, optional): Zero init bottleneck residual BN. Defaults to False. + """ + super(VideoResNet, self).__init__() + self.inplanes = 64 + + self.stem = stem() + + self.layer1 = self._make_layer(block, conv_makers[0], 64, layers[0], stride=1) + self.layer2 = self._make_layer(block, conv_makers[1], 128, layers[1], stride=2) + self.layer3 = self._make_layer(block, conv_makers[2], 256, layers[2], stride=2) + self.layer4 = self._make_layer(block, conv_makers[3], 512, layers[3], stride=2) + + self.avgpool = nn.AdaptiveAvgPool3d((1, 1, 1)) + self.fc = nn.Linear(512 * block.expansion, num_classes) + + # init weights + self._initialize_weights() + + if zero_init_residual: + for m in self.modules(): + if isinstance(m, Bottleneck): + nn.init.constant_(m.bn3.weight, 0) + + def forward(self, x): + x = self.stem(x) + + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + + x = self.avgpool(x) + # Flatten the layer to fc + # x = x.flatten(1) + # x = self.fc(x) + N = x.shape[0] + x = x.squeeze() + if N == 1: + x = x[None] + + return x + + def _make_layer(self, block, conv_builder, planes, blocks, stride=1): + downsample = None + + if stride != 1 or self.inplanes != planes * block.expansion: + ds_stride = conv_builder.get_downsample_stride(stride) + downsample = nn.Sequential( + nn.Conv3d(self.inplanes, planes * block.expansion, kernel_size=1, stride=ds_stride, bias=False), + nn.BatchNorm3d(planes * block.expansion), + ) + layers = [] + layers.append(block(self.inplanes, planes, conv_builder, stride, downsample)) + + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes, conv_builder)) + + return nn.Sequential(*layers) + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Conv3d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + if m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm3d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + nn.init.constant_(m.bias, 0) + + +def _video_resnet(arch, pretrained=False, progress=True, **kwargs): + model = VideoResNet(**kwargs) + + if pretrained: + state_dict = load_state_dict_from_url(model_urls[arch], progress=progress) + model.load_state_dict(state_dict) + return model + + +def r3d_18(pretrained=False, progress=True, **kwargs): + """Construct 18 layer Resnet3D model as in + https://arxiv.org/abs/1711.11248 + Args: + pretrained (bool): If True, returns a model pre-trained on Kinetics-400 + progress (bool): If True, displays a progress bar of the download to stderr + Returns: + nn.Module: R3D-18 network + """ + + return _video_resnet( + "r3d_18", + pretrained, + progress, + block=BasicBlock, + conv_makers=[Conv3DSimple] * 4, + layers=[2, 2, 2, 2], + stem=BasicStem, + **kwargs, + ) + + +def mc3_18(pretrained=False, progress=True, **kwargs): + """Constructor for 18 layer Mixed Convolution network as in + https://arxiv.org/abs/1711.11248 + Args: + pretrained (bool): If True, returns a model pre-trained on Kinetics-400 + progress (bool): If True, displays a progress bar of the download to stderr + Returns: + nn.Module: MC3 Network definition + """ + return _video_resnet( + "mc3_18", + pretrained, + progress, + block=BasicBlock, + conv_makers=[Conv3DSimple] + [Conv3DNoTemporal] * 3, + layers=[2, 2, 2, 2], + stem=BasicStem, + **kwargs, + ) + + +def r2plus1d_18(pretrained=False, progress=True, **kwargs): + """Constructor for the 18 layer deep R(2+1)D network as in + https://arxiv.org/abs/1711.11248 + Args: + pretrained (bool): If True, returns a model pre-trained on Kinetics-400 + progress (bool): If True, displays a progress bar of the download to stderr + Returns: + nn.Module: R(2+1)D-18 network + """ + return _video_resnet( + "r2plus1d_18", + pretrained, + progress, + block=BasicBlock, + conv_makers=[Conv2Plus1D] * 4, + layers=[2, 2, 2, 2], + stem=R2Plus1dStem, + **kwargs, + ) diff --git a/foleycrafter/models/onset/torch_utils.py b/foleycrafter/models/onset/torch_utils.py new file mode 100644 index 0000000..fb2a048 --- /dev/null +++ b/foleycrafter/models/onset/torch_utils.py @@ -0,0 +1,113 @@ +# Copied from https://github.com/XYPB/CondFoleyGen/blob/main/specvqgan/onset_baseline/utils/torch_utils.py +import os +import sys +from collections import OrderedDict + +import numpy as np +import torch +from torch.utils.data import DataLoader + +from ... import data + + +# ---------------------------------------------------- # +def load_model(cp_path, net, device=None, strict=True): + if not device: + device = torch.device("cpu") + if os.path.isfile(cp_path): + print("=> loading checkpoint '{}'".format(cp_path)) + checkpoint = torch.load(cp_path, map_location=device) + + # check if there is module + if list(checkpoint["state_dict"].keys())[0][:7] == "module.": + state_dict = OrderedDict() + for k, v in checkpoint["state_dict"].items(): + name = k[7:] + state_dict[name] = v + else: + state_dict = checkpoint["state_dict"] + net.load_state_dict(state_dict, strict=strict) + + print("=> loaded checkpoint '{}' (epoch {})".format(cp_path, checkpoint["epoch"])) + start_epoch = checkpoint["epoch"] + else: + print("=> no checkpoint found at '{}'".format(cp_path)) + start_epoch = 0 + sys.exit() + + return net, start_epoch + + +# ---------------------------------------------------- # +def binary_acc(pred, target, threshold): + pred = pred > threshold + acc = np.sum(pred == target) / target.shape[0] + return acc + + +def calc_acc(prob, labels, k): + pred = torch.argsort(prob, dim=-1, descending=True)[..., :k] + top_k_acc = torch.sum(pred == labels.view(-1, 1)).float() / labels.size(0) + return top_k_acc + + +# ---------------------------------------------------- # + + +def get_dataloader(args, pr, split="train", shuffle=False, drop_last=False, batch_size=None): + data_loader = getattr(data, pr.dataloader) + if split == "train": + read_list = pr.list_train + elif split == "val": + read_list = pr.list_val + elif split == "test": + read_list = pr.list_test + dataset = data_loader(args, pr, read_list, split=split) + batch_size = batch_size if batch_size else args.batch_size + dataset.getitem_test(1) + loader = DataLoader( + dataset, + batch_size=batch_size, + shuffle=shuffle, + num_workers=args.num_workers, + pin_memory=True, + drop_last=drop_last, + ) + + return dataset, loader + + +# ---------------------------------------------------- # +def make_optimizer(model, args): + """ + Args: + model: NN to train + Returns: + optimizer: pytorch optmizer for updating the given model parameters. + """ + if args.optim == "SGD": + optimizer = torch.optim.SGD( + filter(lambda p: p.requires_grad, model.parameters()), + lr=args.lr, + momentum=args.momentum, + weight_decay=args.weight_decay, + nesterov=False, + ) + elif args.optim == "Adam": + optimizer = torch.optim.Adam( + filter(lambda p: p.requires_grad, model.parameters()), + lr=args.lr, + weight_decay=args.weight_decay, + ) + return optimizer + + +def adjust_learning_rate(optimizer, epoch, args): + """Decay the learning rate based on schedule""" + lr = args.lr + if args.schedule == "cos": # cosine lr schedule + lr *= 0.5 * (1.0 + np.cos(np.pi * epoch / args.epochs)) + elif args.schedule == "none": # no lr schedule + lr = args.lr + for param_group in optimizer.param_groups: + param_group["lr"] = lr diff --git a/foleycrafter/models/onset/video_onset_net.py b/foleycrafter/models/onset/video_onset_net.py new file mode 100644 index 0000000..e5f742f --- /dev/null +++ b/foleycrafter/models/onset/video_onset_net.py @@ -0,0 +1,31 @@ +# Copied from specvqgan/onset_baseline/models/video_onset_net.py + +import torch +import torch.nn as nn + +from .r2plus1d_18 import r2plus1d18KeepTemp + + +class VideoOnsetNet(nn.Module): + # Video Onset detection network + def __init__(self, pretrained): + super(VideoOnsetNet, self).__init__() + self.net = r2plus1d18KeepTemp(pretrained=pretrained) + self.fc = nn.Sequential(nn.Linear(512, 128), nn.ReLU(True), nn.Linear(128, 1)) + + def forward(self, inputs, loss=False, evaluate=False): + # import pdb; pdb.set_trace() + x = inputs["frames"] + x = self.net(x) + x = x.transpose(-1, -2) + x = self.fc(x) + x = x.squeeze(-1) + + return x + + +if __name__ == "__main__": + model = VideoOnsetNet(False).cuda() + rand_input = torch.randn((1, 3, 30, 112, 112)).cuda() + inputs = {"frames": rand_input} + out = model(inputs) diff --git a/foleycrafter/models/time_detector/model.py b/foleycrafter/models/time_detector/model.py new file mode 100644 index 0000000..1ba4082 --- /dev/null +++ b/foleycrafter/models/time_detector/model.py @@ -0,0 +1,17 @@ +import torch.nn as nn + +from ..onset import VideoOnsetNet + + +class TimeDetector(nn.Module): + def __init__(self, video_length=150, audio_length=1024): + super(TimeDetector, self).__init__() + self.pred_net = VideoOnsetNet(pretrained=False) + self.soft_fn = nn.Tanh() + self.up_sampler = nn.Linear(video_length, audio_length) + + def forward(self, inputs): + x = self.pred_net(inputs) + x = self.up_sampler(x) + x = self.soft_fn(x) + return x diff --git a/foleycrafter/models/time_detector/resnet.py b/foleycrafter/models/time_detector/resnet.py new file mode 100644 index 0000000..680d2d1 --- /dev/null +++ b/foleycrafter/models/time_detector/resnet.py @@ -0,0 +1,322 @@ +import torch.nn as nn +from torch.hub import load_state_dict_from_url + + +__all__ = ["r3d_18", "mc3_18", "r2plus1d_18"] + +model_urls = { + "r3d_18": "https://download.pytorch.org/models/r3d_18-b3b3357e.pth", + "mc3_18": "https://download.pytorch.org/models/mc3_18-a90a0ba3.pth", + "r2plus1d_18": "https://download.pytorch.org/models/r2plus1d_18-91a641e6.pth", +} + + +class Conv3DSimple(nn.Conv3d): + def __init__(self, in_planes, out_planes, midplanes=None, stride=1, padding=1): + super(Conv3DSimple, self).__init__( + in_channels=in_planes, + out_channels=out_planes, + kernel_size=(3, 3, 3), + stride=stride, + padding=padding, + bias=False, + ) + + @staticmethod + def get_downsample_stride(stride): + return stride, stride, stride + + +class Conv2Plus1D(nn.Sequential): + def __init__(self, in_planes, out_planes, midplanes, stride=1, padding=1): + super(Conv2Plus1D, self).__init__( + nn.Conv3d( + in_planes, + midplanes, + kernel_size=(1, 3, 3), + stride=(1, stride, stride), + padding=(0, padding, padding), + bias=False, + ), + nn.BatchNorm3d(midplanes), + nn.ReLU(inplace=True), + nn.Conv3d( + midplanes, + out_planes, + kernel_size=(3, 1, 1), + stride=(stride, 1, 1), + padding=(padding, 0, 0), + bias=False, + ), + ) + + @staticmethod + def get_downsample_stride(stride): + return stride, stride, stride + + +class Conv3DNoTemporal(nn.Conv3d): + def __init__(self, in_planes, out_planes, midplanes=None, stride=1, padding=1): + super(Conv3DNoTemporal, self).__init__( + in_channels=in_planes, + out_channels=out_planes, + kernel_size=(1, 3, 3), + stride=(1, stride, stride), + padding=(0, padding, padding), + bias=False, + ) + + @staticmethod + def get_downsample_stride(stride): + return 1, stride, stride + + +class BasicBlock(nn.Module): + expansion = 1 + + def __init__(self, inplanes, planes, conv_builder, stride=1, downsample=None): + midplanes = (inplanes * planes * 3 * 3 * 3) // (inplanes * 3 * 3 + 3 * planes) + + super(BasicBlock, self).__init__() + self.conv1 = nn.Sequential( + conv_builder(inplanes, planes, midplanes, stride), nn.BatchNorm3d(planes), nn.ReLU(inplace=True) + ) + self.conv2 = nn.Sequential(conv_builder(planes, planes, midplanes), nn.BatchNorm3d(planes)) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.conv2(out) + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class Bottleneck(nn.Module): + expansion = 4 + + def __init__(self, inplanes, planes, conv_builder, stride=1, downsample=None): + super(Bottleneck, self).__init__() + midplanes = (inplanes * planes * 3 * 3 * 3) // (inplanes * 3 * 3 + 3 * planes) + + # 1x1x1 + self.conv1 = nn.Sequential( + nn.Conv3d(inplanes, planes, kernel_size=1, bias=False), nn.BatchNorm3d(planes), nn.ReLU(inplace=True) + ) + # Second kernel + self.conv2 = nn.Sequential( + conv_builder(planes, planes, midplanes, stride), nn.BatchNorm3d(planes), nn.ReLU(inplace=True) + ) + + # 1x1x1 + self.conv3 = nn.Sequential( + nn.Conv3d(planes, planes * self.expansion, kernel_size=1, bias=False), + nn.BatchNorm3d(planes * self.expansion), + ) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.conv2(out) + out = self.conv3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class BasicStem(nn.Sequential): + """The default conv-batchnorm-relu stem""" + + def __init__(self): + super(BasicStem, self).__init__( + nn.Conv3d(3, 64, kernel_size=(3, 7, 7), stride=(1, 2, 2), padding=(1, 3, 3), bias=False), + nn.BatchNorm3d(64), + nn.ReLU(inplace=True), + ) + + +class R2Plus1dStem(nn.Sequential): + """R(2+1)D stem is different than the default one as it uses separated 3D convolution""" + + def __init__(self): + super(R2Plus1dStem, self).__init__( + nn.Conv3d(3, 45, kernel_size=(1, 7, 7), stride=(1, 2, 2), padding=(0, 3, 3), bias=False), + nn.BatchNorm3d(45), + nn.ReLU(inplace=True), + nn.Conv3d(45, 64, kernel_size=(3, 1, 1), stride=(1, 1, 1), padding=(1, 0, 0), bias=False), + nn.BatchNorm3d(64), + nn.ReLU(inplace=True), + ) + + +class VideoResNet(nn.Module): + def __init__(self, block, conv_makers, layers, stem, num_classes=400, zero_init_residual=False): + """Generic resnet video generator. + Args: + block (nn.Module): resnet building block + conv_makers (list(functions)): generator function for each layer + layers (List[int]): number of blocks per layer + stem (nn.Module, optional): Resnet stem, if None, defaults to conv-bn-relu. Defaults to None. + num_classes (int, optional): Dimension of the final FC layer. Defaults to 400. + zero_init_residual (bool, optional): Zero init bottleneck residual BN. Defaults to False. + """ + super(VideoResNet, self).__init__() + self.inplanes = 64 + + self.stem = stem() + + self.layer1 = self._make_layer(block, conv_makers[0], 64, layers[0], stride=1) + self.layer2 = self._make_layer(block, conv_makers[1], 128, layers[1], stride=2) + self.layer3 = self._make_layer(block, conv_makers[2], 256, layers[2], stride=2) + self.layer4 = self._make_layer(block, conv_makers[3], 512, layers[3], stride=2) + + self.avgpool = nn.AdaptiveAvgPool3d((1, 1, 1)) + self.fc = nn.Linear(512 * block.expansion, num_classes) + + # init weights + self._initialize_weights() + + if zero_init_residual: + for m in self.modules(): + if isinstance(m, Bottleneck): + nn.init.constant_(m.bn3.weight, 0) + + def forward(self, x): + x = self.stem(x) + + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + + x = self.avgpool(x) + # Flatten the layer to fc + # x = x.flatten(1) + # x = self.fc(x) + N = x.shape[0] + x = x.squeeze() + if N == 1: + x = x[None] + + return x + + def _make_layer(self, block, conv_builder, planes, blocks, stride=1): + downsample = None + + if stride != 1 or self.inplanes != planes * block.expansion: + ds_stride = conv_builder.get_downsample_stride(stride) + downsample = nn.Sequential( + nn.Conv3d(self.inplanes, planes * block.expansion, kernel_size=1, stride=ds_stride, bias=False), + nn.BatchNorm3d(planes * block.expansion), + ) + layers = [] + layers.append(block(self.inplanes, planes, conv_builder, stride, downsample)) + + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes, conv_builder)) + + return nn.Sequential(*layers) + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Conv3d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + if m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm3d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + nn.init.constant_(m.bias, 0) + + +def _video_resnet(arch, pretrained=False, progress=True, **kwargs): + model = VideoResNet(**kwargs) + + if pretrained: + state_dict = load_state_dict_from_url(model_urls[arch], progress=progress) + model.load_state_dict(state_dict) + return model + + +def r3d_18(pretrained=False, progress=True, **kwargs): + """Construct 18 layer Resnet3D model as in + https://arxiv.org/abs/1711.11248 + Args: + pretrained (bool): If True, returns a model pre-trained on Kinetics-400 + progress (bool): If True, displays a progress bar of the download to stderr + Returns: + nn.Module: R3D-18 network + """ + + return _video_resnet( + "r3d_18", + pretrained, + progress, + block=BasicBlock, + conv_makers=[Conv3DSimple] * 4, + layers=[2, 2, 2, 2], + stem=BasicStem, + **kwargs, + ) + + +def mc3_18(pretrained=False, progress=True, **kwargs): + """Constructor for 18 layer Mixed Convolution network as in + https://arxiv.org/abs/1711.11248 + Args: + pretrained (bool): If True, returns a model pre-trained on Kinetics-400 + progress (bool): If True, displays a progress bar of the download to stderr + Returns: + nn.Module: MC3 Network definition + """ + return _video_resnet( + "mc3_18", + pretrained, + progress, + block=BasicBlock, + conv_makers=[Conv3DSimple] + [Conv3DNoTemporal] * 3, + layers=[2, 2, 2, 2], + stem=BasicStem, + **kwargs, + ) + + +def r2plus1d_18(pretrained=False, progress=True, **kwargs): + """Constructor for the 18 layer deep R(2+1)D network as in + https://arxiv.org/abs/1711.11248 + Args: + pretrained (bool): If True, returns a model pre-trained on Kinetics-400 + progress (bool): If True, displays a progress bar of the download to stderr + Returns: + nn.Module: R(2+1)D-18 network + """ + return _video_resnet( + "r2plus1d_18", + pretrained, + progress, + block=BasicBlock, + conv_makers=[Conv2Plus1D] * 4, + layers=[2, 2, 2, 2], + stem=R2Plus1dStem, + **kwargs, + ) diff --git a/foleycrafter/pipelines/auffusion_pipeline.py b/foleycrafter/pipelines/auffusion_pipeline.py new file mode 100644 index 0000000..8870964 --- /dev/null +++ b/foleycrafter/pipelines/auffusion_pipeline.py @@ -0,0 +1,2155 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import json +import os +import warnings +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Union + +import matplotlib.pyplot as plt +import numpy as np +import PIL +import torch +import torch.nn as nn +import torch.nn.functional as F +from huggingface_hub import snapshot_download +from packaging import version +from torch.nn import Conv1d, ConvTranspose1d +from torch.nn.utils import remove_weight_norm, weight_norm +from transformers import ( + AutoTokenizer, + CLIPImageProcessor, + CLIPTextModel, + CLIPTokenizer, + CLIPVisionModelWithProjection, + PretrainedConfig, +) + +from diffusers import PNDMScheduler +from diffusers.configuration_utils import FrozenDict +from diffusers.image_processor import PipelineImageInput, VaeImageProcessor +from diffusers.loaders import FromSingleFileMixin, LoraLoaderMixin, TextualInversionLoaderMixin +from diffusers.models import AutoencoderKL, ImageProjection +from diffusers.models.attention_processor import FusedAttnProcessor2_0 +from diffusers.pipelines.pipeline_utils import DiffusionPipeline +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import KarrasDiffusionSchedulers +from diffusers.utils import ( + deprecate, + is_accelerate_available, + is_accelerate_version, + logging, +) +from diffusers.utils.outputs import BaseOutput +from diffusers.utils.torch_utils import randn_tensor +from ..models.auffusion.loaders.ip_adapter import IPAdapterMixin +from ..models.auffusion_unet import UNet2DConditionModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def json_dump(data_json, json_save_path): + with open(json_save_path, "w") as f: + json.dump(data_json, f, indent=4) + f.close() + + +def json_load(json_path): + with open(json_path, "r") as f: + data = json.load(f) + f.close() + return data + + +def import_model_class_from_model_name_or_path(pretrained_model_name_or_path: str): + text_encoder_config = PretrainedConfig.from_pretrained(pretrained_model_name_or_path) + model_class = text_encoder_config.architectures[0] + + if model_class == "CLIPTextModel": + from transformers import CLIPTextModel + + return CLIPTextModel + if "t5" in model_class.lower(): + from transformers import T5EncoderModel + + return T5EncoderModel + if "clap" in model_class.lower(): + from transformers import ClapTextModelWithProjection + + return ClapTextModelWithProjection + else: + raise ValueError(f"{model_class} is not supported.") + + +class ConditionAdapter(nn.Module): + def __init__(self, config): + super(ConditionAdapter, self).__init__() + self.config = config + self.proj = nn.Linear(self.config["condition_dim"], self.config["cross_attention_dim"]) + self.norm = torch.nn.LayerNorm(self.config["cross_attention_dim"]) + print(f"INITIATED: ConditionAdapter: {self.config}") + + def forward(self, x): + x = self.proj(x) + x = self.norm(x) + return x + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path): + config_path = os.path.join(pretrained_model_name_or_path, "config.json") + ckpt_path = os.path.join(pretrained_model_name_or_path, "condition_adapter.pt") + config = json.loads(open(config_path).read()) + instance = cls(config) + instance.load_state_dict(torch.load(ckpt_path)) + print(f"LOADED: ConditionAdapter from {pretrained_model_name_or_path}") + return instance + + def save_pretrained(self, pretrained_model_name_or_path): + os.makedirs(pretrained_model_name_or_path, exist_ok=True) + config_path = os.path.join(pretrained_model_name_or_path, "config.json") + ckpt_path = os.path.join(pretrained_model_name_or_path, "condition_adapter.pt") + json_dump(self.config, config_path) + torch.save(self.state_dict(), ckpt_path) + print(f"SAVED: ConditionAdapter {self.config['model_name']} to {pretrained_model_name_or_path}") + + +def rescale_noise_cfg(noise_cfg, noise_pred_text, guidance_rescale=0.0): + """ + Rescale `noise_cfg` according to `guidance_rescale`. Based on findings of [Common Diffusion Noise Schedules and + Sample Steps are Flawed](https://arxiv.org/pdf/2305.08891.pdf). See Section 3.4 + """ + std_text = noise_pred_text.std(dim=list(range(1, noise_pred_text.ndim)), keepdim=True) + std_cfg = noise_cfg.std(dim=list(range(1, noise_cfg.ndim)), keepdim=True) + # rescale the results from guidance (fixes overexposure) + noise_pred_rescaled = noise_cfg * (std_text / std_cfg) + # mix with the original results from guidance by factor guidance_rescale to avoid "plain looking" images + noise_cfg = guidance_rescale * noise_pred_rescaled + (1 - guidance_rescale) * noise_cfg + return noise_cfg + + +LRELU_SLOPE = 0.1 +MAX_WAV_VALUE = 32768.0 + + +class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + +def get_config(config_path): + config = json.loads(open(config_path).read()) + config = AttrDict(config) + return config + + +def init_weights(m, mean=0.0, std=0.01): + classname = m.__class__.__name__ + if classname.find("Conv") != -1: + m.weight.data.normal_(mean, std) + + +def apply_weight_norm(m): + classname = m.__class__.__name__ + if classname.find("Conv") != -1: + weight_norm(m) + + +def get_padding(kernel_size, dilation=1): + return int((kernel_size * dilation - dilation) / 2) + + +class ResBlock1(torch.nn.Module): + def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5)): + super(ResBlock1, self).__init__() + self.h = h + self.convs1 = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[0], + padding=get_padding(kernel_size, dilation[0]), + ) + ), + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[1], + padding=get_padding(kernel_size, dilation[1]), + ) + ), + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[2], + padding=get_padding(kernel_size, dilation[2]), + ) + ), + ] + ) + self.convs1.apply(init_weights) + + self.convs2 = nn.ModuleList( + [ + weight_norm( + Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1)) + ), + weight_norm( + Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1)) + ), + weight_norm( + Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1)) + ), + ] + ) + self.convs2.apply(init_weights) + + def forward(self, x): + for c1, c2 in zip(self.convs1, self.convs2): + xt = F.leaky_relu(x, LRELU_SLOPE) + xt = c1(xt) + xt = F.leaky_relu(xt, LRELU_SLOPE) + xt = c2(xt) + x = xt + x + return x + + def remove_weight_norm(self): + for l in self.convs1: + remove_weight_norm(l) + for l in self.convs2: + remove_weight_norm(l) + + +class ResBlock2(torch.nn.Module): + def __init__(self, h, channels, kernel_size=3, dilation=(1, 3)): + super(ResBlock2, self).__init__() + self.h = h + self.convs = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[0], + padding=get_padding(kernel_size, dilation[0]), + ) + ), + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[1], + padding=get_padding(kernel_size, dilation[1]), + ) + ), + ] + ) + self.convs.apply(init_weights) + + def forward(self, x): + for c in self.convs: + xt = F.leaky_relu(x, LRELU_SLOPE) + xt = c(xt) + x = xt + x + return x + + def remove_weight_norm(self): + for l in self.convs: + remove_weight_norm(l) + + +class Generator(torch.nn.Module): + def __init__(self, h): + super(Generator, self).__init__() + self.h = h + self.num_kernels = len(h.resblock_kernel_sizes) + self.num_upsamples = len(h.upsample_rates) + # self.conv_pre = weight_norm(Conv1d(80, h.upsample_initial_channel, 7, 1, padding=3)) + self.conv_pre = weight_norm( + Conv1d(h.num_mels, h.upsample_initial_channel, 7, 1, padding=3) + ) # change: 80 --> 512 + resblock = ResBlock1 if h.resblock == "1" else ResBlock2 + + self._device = "cuda" if torch.cuda.is_available() else "cpu" + + self.ups = nn.ModuleList() + for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)): + if (k - u) % 2 == 0: + self.ups.append( + weight_norm( + ConvTranspose1d( + h.upsample_initial_channel // (2**i), + h.upsample_initial_channel // (2 ** (i + 1)), + k, + u, + padding=(k - u) // 2, + ) + ) + ) + else: + self.ups.append( + weight_norm( + ConvTranspose1d( + h.upsample_initial_channel // (2**i), + h.upsample_initial_channel // (2 ** (i + 1)), + k, + u, + padding=(k - u) // 2 + 1, + output_padding=1, + ) + ) + ) + + # self.ups.append(weight_norm( + # ConvTranspose1d(h.upsample_initial_channel//(2**i), h.upsample_initial_channel//(2**(i+1)), + # k, u, padding=(k-u)//2))) + + self.resblocks = nn.ModuleList() + for i in range(len(self.ups)): + ch = h.upsample_initial_channel // (2 ** (i + 1)) + for j, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)): + self.resblocks.append(resblock(h, ch, k, d)) + + self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, padding=3)) + self.ups.apply(init_weights) + self.conv_post.apply(init_weights) + + @property + def device(self) -> torch.device: + return torch.device(self._device) + + @property + def dtype(self): + return self.type + + def forward(self, x): + x = self.conv_pre(x) + for i in range(self.num_upsamples): + x = F.leaky_relu(x, LRELU_SLOPE) + x = self.ups[i](x) + xs = None + for j in range(self.num_kernels): + if xs is None: + xs = self.resblocks[i * self.num_kernels + j](x) + else: + xs += self.resblocks[i * self.num_kernels + j](x) + x = xs / self.num_kernels + x = F.leaky_relu(x) + x = self.conv_post(x) + x = torch.tanh(x) + + return x + + def remove_weight_norm(self): + print("Removing weight norm...") + for l in self.ups: + remove_weight_norm(l) + for l in self.resblocks: + l.remove_weight_norm() + remove_weight_norm(self.conv_pre) + remove_weight_norm(self.conv_post) + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path, subfolder=None): + if subfolder is not None: + pretrained_model_name_or_path = os.path.join(pretrained_model_name_or_path, subfolder) + config_path = os.path.join(pretrained_model_name_or_path, "config.json") + ckpt_path = os.path.join(pretrained_model_name_or_path, "vocoder.pt") + + config = get_config(config_path) + vocoder = cls(config) + + state_dict_g = torch.load(ckpt_path) + vocoder.load_state_dict(state_dict_g["generator"]) + vocoder.eval() + vocoder.remove_weight_norm() + return vocoder + + @torch.no_grad() + def inference(self, mels, lengths=None): + self.eval() + with torch.no_grad(): + wavs = self(mels).squeeze(1) + + wavs = (wavs.cpu().numpy() * MAX_WAV_VALUE).astype("int16") + + if lengths is not None: + wavs = wavs[:, :lengths] + + return wavs + + +def normalize_spectrogram( + spectrogram: torch.Tensor, + max_value: float = 200, + min_value: float = 1e-5, + power: float = 1.0, +) -> torch.Tensor: + # Rescale to 0-1 + max_value = np.log(max_value) # 5.298317366548036 + min_value = np.log(min_value) # -11.512925464970229 + spectrogram = torch.clamp(spectrogram, min=min_value, max=max_value) + data = (spectrogram - min_value) / (max_value - min_value) + # Apply the power curve + data = torch.pow(data, power) + # 1D -> 3D + data = data.repeat(3, 1, 1) + # Flip Y axis: image origin at the top-left corner, spectrogram origin at the bottom-left corner + data = torch.flip(data, [1]) + + return data + + +def denormalize_spectrogram( + data: torch.Tensor, + max_value: float = 200, + min_value: float = 1e-5, + power: float = 1, +) -> torch.Tensor: + assert len(data.shape) == 3, "Expected 3 dimensions, got {}".format(len(data.shape)) + + max_value = np.log(max_value) + min_value = np.log(min_value) + # Flip Y axis: image origin at the top-left corner, spectrogram origin at the bottom-left corner + data = torch.flip(data, [1]) + if data.shape[0] == 1: + data = data.repeat(3, 1, 1) + assert data.shape[0] == 3, "Expected 3 channels, got {}".format(data.shape[0]) + data = data[0] + # Reverse the power curve + data = torch.pow(data, 1 / power) + # Rescale to max value + spectrogram = data * (max_value - min_value) + min_value + + return spectrogram + + +@staticmethod +def pt_to_numpy(images: torch.FloatTensor) -> np.ndarray: + """ + Convert a PyTorch tensor to a NumPy image. + """ + images = images.cpu().permute(0, 2, 3, 1).float().numpy() + return images + + +@staticmethod +def numpy_to_pil(images: np.ndarray) -> PIL.Image.Image: + """ + Convert a numpy image or a batch of images to a PIL image. + """ + if images.ndim == 3: + images = images[None, ...] + images = (images * 255).round().astype("uint8") + if images.shape[-1] == 1: + # special case for grayscale (single channel) images + pil_images = [PIL.Image.fromarray(image.squeeze(), mode="L") for image in images] + else: + pil_images = [PIL.Image.fromarray(image) for image in images] + + return pil_images + + +def image_add_color(spec_img): + cmap = plt.get_cmap("viridis") + # cmap_r = cmap.reversed() + image = cmap(np.array(spec_img)[:, :, 0])[:, :, :3] # 省略透明度通道 + image = (image - image.min()) / (image.max() - image.min()) + image = PIL.Image.fromarray(np.uint8(image * 255)) + return image + + +@dataclass +class PipelineOutput(BaseOutput): + """ + Output class for audio pipelines. + + Args: + audios (`np.ndarray`) + List of denoised audio samples of a NumPy array of shape `(batch_size, num_channels, sample_rate)`. + """ + + images: Union[List[PIL.Image.Image], np.ndarray] + spectrograms: Union[List[np.ndarray], np.ndarray] + audios: Union[List[np.ndarray], np.ndarray] + + +class AuffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + In addition the pipeline inherits the following loading methods: + - *Textual-Inversion*: [`loaders.TextualInversionLoaderMixin.load_textual_inversion`] + - *LoRA*: [`loaders.LoraLoaderMixin.load_lora_weights`] + - *Ckpt*: [`loaders.FromSingleFileMixin.from_single_file`] + + as well as the following saving methods: + - *LoRA*: [`loaders.LoraLoaderMixin.save_lora_weights`] + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPImageProcessor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + _optional_components = [ + "safety_checker", + "feature_extractor", + "text_encoder_list", + "tokenizer_list", + "adapter_list", + "vocoder", + ] + + def __init__( + self, + vae: AutoencoderKL, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPImageProcessor, + text_encoder_list: Optional[List[Callable]] = None, + tokenizer_list: Optional[List[Callable]] = None, + vocoder: Generator = None, + requires_safety_checker: bool = False, + adapter_list: Optional[List[Callable]] = None, + tokenizer_model_max_length: Optional[ + int + ] = 77, # 77 is the default value for the CLIPTokenizer(and set for other models) + ): + super().__init__() + + self.text_encoder_list = text_encoder_list + self.tokenizer_list = tokenizer_list + self.vocoder = vocoder + self.adapter_list = adapter_list + self.tokenizer_model_max_length = tokenizer_model_max_length + + self.register_modules( + vae=vae, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: str = "auffusion/auffusion-full-no-adapter", + dtype: torch.dtype = torch.float16, + device: str = "cuda", + ): + if not os.path.isdir(pretrained_model_name_or_path): + pretrained_model_name_or_path = snapshot_download(pretrained_model_name_or_path) + + vae = AutoencoderKL.from_pretrained(pretrained_model_name_or_path, subfolder="vae") + unet = UNet2DConditionModel.from_pretrained(pretrained_model_name_or_path, subfolder="unet") + feature_extractor = CLIPImageProcessor.from_pretrained( + pretrained_model_name_or_path, subfolder="feature_extractor" + ) + scheduler = PNDMScheduler.from_pretrained(pretrained_model_name_or_path, subfolder="scheduler") + + vocoder = Generator.from_pretrained(pretrained_model_name_or_path, subfolder="vocoder").to(device, dtype) + + text_encoder_list, tokenizer_list, adapter_list = [], [], [] + + condition_json_path = os.path.join(pretrained_model_name_or_path, "condition_config.json") + condition_json_list = json.loads(open(condition_json_path).read()) + + for i, condition_item in enumerate(condition_json_list): + # Load Condition Adapter + text_encoder_path = os.path.join(pretrained_model_name_or_path, condition_item["text_encoder_name"]) + tokenizer = AutoTokenizer.from_pretrained(text_encoder_path) + tokenizer_list.append(tokenizer) + text_encoder_cls = import_model_class_from_model_name_or_path(text_encoder_path) + text_encoder = text_encoder_cls.from_pretrained(text_encoder_path).to(device, dtype) + text_encoder_list.append(text_encoder) + print(f"LOADING CONDITION ENCODER {i}") + + # Load Condition Adapter + adapter_path = os.path.join(pretrained_model_name_or_path, condition_item["condition_adapter_name"]) + adapter = ConditionAdapter.from_pretrained(adapter_path).to(device, dtype) + adapter_list.append(adapter) + print(f"LOADING CONDITION ADAPTER {i}") + + pipeline = cls( + vae=vae, + unet=unet, + text_encoder_list=text_encoder_list, + tokenizer_list=tokenizer_list, + vocoder=vocoder, + adapter_list=adapter_list, + scheduler=scheduler, + safety_checker=None, + feature_extractor=feature_extractor, + ) + pipeline = pipeline.to(device, dtype) + + return pipeline + + def to(self, device, dtype=None): + super().to(device, dtype) + + self.vocoder.to(device, dtype) + + for text_encoder in self.text_encoder_list: + text_encoder.to(device, dtype) + + if self.adapter_list is not None: + for adapter in self.adapter_list: + adapter.to(device, dtype) + + return self + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. + + When this option is enabled, the VAE will split the input tensor in slices to compute decoding in several + steps. This is useful to save some memory and allow larger batch sizes. + """ + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously invoked, this method will go back to + computing decoding in one step. + """ + self.vae.disable_slicing() + + def enable_vae_tiling(self): + r""" + Enable tiled VAE decoding. + + When this option is enabled, the VAE will split the input tensor into tiles to compute decoding and encoding in + several steps. This is useful to save a large amount of memory and to allow the processing of larger images. + """ + self.vae.enable_tiling() + + def disable_vae_tiling(self): + r""" + Disable tiled VAE decoding. If `enable_vae_tiling` was previously invoked, this method will go back to + computing decoding in one step. + """ + self.vae.disable_tiling() + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + Note that offloading happens on a submodule basis. Memory savings are higher than with + `enable_model_cpu_offload`, but performance is lower. + """ + if is_accelerate_available() and is_accelerate_version(">=", "0.14.0"): + from accelerate import cpu_offload + else: + raise ImportError("`enable_sequential_cpu_offload` requires `accelerate v0.14.0` or higher") + + device = torch.device(f"cuda:{gpu_id}") + + if self.device.type != "cpu": + self.to("cpu", silence_dtype_warnings=True) + torch.cuda.empty_cache() # otherwise we don't see the memory savings (but they probably exist) + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + def enable_model_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared + to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward` + method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with + `enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`. + """ + if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"): + from accelerate import cpu_offload_with_hook + else: + raise ImportError("`enable_model_cpu_offload` requires `accelerate v0.17.0` or higher.") + + device = torch.device(f"cuda:{gpu_id}") + + if self.device.type != "cpu": + self.to("cpu", silence_dtype_warnings=True) + torch.cuda.empty_cache() # otherwise we don't see the memory savings (but they probably exist) + + hook = None + for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]: + _, hook = cpu_offload_with_hook(cpu_offloaded_model, device, prev_module_hook=hook) + + if self.safety_checker is not None: + _, hook = cpu_offload_with_hook(self.safety_checker, device, prev_module_hook=hook) + + # We'll offload the last model manually. + self.final_offload_hook = hook + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + assert len(self.text_encoder_list) == len( + self.tokenizer_list + ), "Number of text_encoders must match number of tokenizers" + if self.adapter_list is not None: + assert len(self.text_encoder_list) == len( + self.adapter_list + ), "Number of text_encoders must match number of adapters" + + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + def get_prompt_embeds(prompt_list, device): + if isinstance(prompt_list, str): + prompt_list = [prompt_list] + + prompt_embeds_list = [] + for prompt in prompt_list: + encoder_hidden_states_list = [] + + # Generate condition embedding + for j in range(len(self.text_encoder_list)): + # get condition embedding using condition encoder + input_ids = self.tokenizer_list[j](prompt, return_tensors="pt").input_ids.to(device) + cond_embs = self.text_encoder_list[j](input_ids).last_hidden_state # [bz, text_len, text_dim] + # padding to max_length + if cond_embs.shape[1] < self.tokenizer_model_max_length: + cond_embs = torch.functional.F.pad( + cond_embs, (0, 0, 0, self.tokenizer_model_max_length - cond_embs.shape[1]), value=0 + ) + else: + cond_embs = cond_embs[:, : self.tokenizer_model_max_length, :] + + # use condition adapter + if self.adapter_list is not None: + cond_embs = self.adapter_list[j](cond_embs) + encoder_hidden_states_list.append(cond_embs) + + prompt_embeds = torch.cat(encoder_hidden_states_list, dim=1) + prompt_embeds_list.append(prompt_embeds) + + prompt_embeds = torch.cat(prompt_embeds_list, dim=0) + return prompt_embeds + + if prompt_embeds is None: + prompt_embeds = get_prompt_embeds(prompt, device) + + prompt_embeds = prompt_embeds.to(dtype=self.unet.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + if do_classifier_free_guidance and negative_prompt_embeds is None: + if negative_prompt is None: + negative_prompt_embeds = torch.zeros_like(prompt_embeds).to(dtype=prompt_embeds.dtype, device=device) + + elif prompt is not None and type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + negative_prompt = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + negative_prompt_embeds = get_prompt_embeds(negative_prompt, device) + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.unet.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is None: + has_nsfw_concept = None + else: + if torch.is_tensor(image): + feature_extractor_input = self.image_processor.postprocess(image, output_type="pil") + else: + feature_extractor_input = self.image_processor.numpy_to_pil(image) + safety_checker_input = self.feature_extractor(feature_extractor_input, return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + return image, has_nsfw_concept + + def decode_latents(self, latents): + warnings.warn( + "The decode_latents method is deprecated and will be removed in a future version. Please" + " use VaeImageProcessor instead", + FutureWarning, + ) + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents, return_dict=False)[0] + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = torch.randn(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + height: Optional[int] = 256, + width: Optional[int] = 1024, + num_inference_steps: int = 100, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pt", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: int = 1, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + guidance_rescale: float = 0.0, + duration: Optional[float] = 10, + ): + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + audio_length = int(duration * 16000) + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, height, width, callback_steps, negative_prompt, prompt_embeds, negative_prompt_embeds + ) + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.unet.config.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + cross_attention_kwargs=cross_attention_kwargs, + return_dict=False, + )[0] + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + if do_classifier_free_guidance and guidance_rescale > 0.0: + # Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf + noise_pred = rescale_noise_cfg(noise_pred, noise_pred_text, guidance_rescale=guidance_rescale) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0] + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + if not output_type == "latent": + image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False)[0] + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + else: + image = latents + has_nsfw_concept = None + + if has_nsfw_concept is None: + do_denormalize = [True] * image.shape[0] + else: + do_denormalize = [not has_nsfw for has_nsfw in has_nsfw_concept] + + image = self.image_processor.postprocess(image, output_type=output_type, do_denormalize=do_denormalize) + + # Offload last model to CPU + if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None: + self.final_offload_hook.offload() + + # Generate audio + spectrograms, audios = [], [] + for img in image: + spectrogram = denormalize_spectrogram(img) + audio = self.vocoder.inference(spectrogram, lengths=audio_length)[0] + audios.append(audio) + spectrograms.append(spectrogram) + + # Convert to PIL + images = pt_to_numpy(image) + images = numpy_to_pil(images) + images = [image_add_color(image) for image in images] + + if not return_dict: + return (images, audios, spectrograms) + + return PipelineOutput(images=images, audios=audios, spectrograms=spectrograms) + + +def retrieve_timesteps( + scheduler, + num_inference_steps: Optional[int] = None, + device: Optional[Union[str, torch.device]] = None, + timesteps: Optional[List[int]] = None, + **kwargs, +): + """ + Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles + custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`. + + Args: + scheduler (`SchedulerMixin`): + The scheduler to get timesteps from. + num_inference_steps (`int`): + The number of diffusion steps used when generating samples with a pre-trained model. If used, + `timesteps` must be `None`. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + timesteps (`List[int]`, *optional*): + Custom timesteps used to support arbitrary spacing between timesteps. If `None`, then the default + timestep spacing strategy of the scheduler is used. If `timesteps` is passed, `num_inference_steps` + must be `None`. + + Returns: + `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the + second element is the number of inference steps. + """ + if timesteps is not None: + accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys()) + if not accepts_timesteps: + raise ValueError( + f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom" + f" timestep schedules. Please check whether you are using the correct scheduler." + ) + scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs) + timesteps = scheduler.timesteps + num_inference_steps = len(timesteps) + else: + scheduler.set_timesteps(num_inference_steps, device=device, **kwargs) + timesteps = scheduler.timesteps + return timesteps, num_inference_steps + + +class AuffusionNoAdapterPipeline( + DiffusionPipeline, TextualInversionLoaderMixin, LoraLoaderMixin, IPAdapterMixin, FromSingleFileMixin +): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods + implemented for all pipelines (downloading, saving, running on a particular device, etc.). + + The pipeline also inherits the following loading methods: + - [`~loaders.TextualInversionLoaderMixin.load_textual_inversion`] for loading textual inversion embeddings + - [`~loaders.LoraLoaderMixin.load_lora_weights`] for loading LoRA weights + - [`~loaders.LoraLoaderMixin.save_lora_weights`] for saving LoRA weights + - [`~loaders.FromSingleFileMixin.from_single_file`] for loading `.ckpt` files + - [`~loaders.IPAdapterMixin.load_ip_adapter`] for loading IP Adapters + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) model to encode and decode images to and from latent representations. + text_encoder ([`~transformers.CLIPTextModel`]): + Frozen text-encoder ([clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14)). + tokenizer ([`~transformers.CLIPTokenizer`]): + A `CLIPTokenizer` to tokenize text. + unet ([`UNet2DConditionModel`]): + A `UNet2DConditionModel` to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for more details + about a model's potential harms. + feature_extractor ([`~transformers.CLIPImageProcessor`]): + A `CLIPImageProcessor` to extract features from generated images; used as inputs to the `safety_checker`. + """ + + model_cpu_offload_seq = "text_encoder->image_encoder->unet->vae" + _optional_components = ["safety_checker", "feature_extractor", "image_encoder"] + _exclude_from_cpu_offload = ["safety_checker"] + _callback_tensor_inputs = ["latents", "prompt_embeds", "negative_prompt_embeds"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPImageProcessor, + image_encoder: CLIPVisionModelWithProjection = None, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + image_encoder=image_encoder, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to + compute decoding in several steps. This is useful to save some memory and allow larger batch sizes. + """ + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously enabled, this method will go back to + computing decoding in one step. + """ + self.vae.disable_slicing() + + def enable_vae_tiling(self): + r""" + Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to + compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow + processing larger images. + """ + self.vae.enable_tiling() + + def disable_vae_tiling(self): + r""" + Disable tiled VAE decoding. If `enable_vae_tiling` was previously enabled, this method will go back to + computing decoding in one step. + """ + self.vae.disable_tiling() + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + lora_scale: Optional[float] = None, + **kwargs, + ): + deprecation_message = "`_encode_prompt()` is deprecated and it will be removed in a future version. Use `encode_prompt()` instead. Also, be aware that the output format changed from a concatenated tensor to a tuple." + deprecate("_encode_prompt()", "1.0.0", deprecation_message, standard_warn=False) + + prompt_embeds_tuple = self.encode_prompt( + prompt=prompt, + device=device, + num_images_per_prompt=num_images_per_prompt, + do_classifier_free_guidance=do_classifier_free_guidance, + negative_prompt=negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + lora_scale=lora_scale, + **kwargs, + ) + + # concatenate for backwards comp + prompt_embeds = torch.cat([prompt_embeds_tuple[1], prompt_embeds_tuple[0]]) + + return prompt_embeds + + def encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + lora_scale: Optional[float] = None, + clip_skip: Optional[int] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is + less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + lora_scale (`float`, *optional*): + A LoRA scale that will be applied to all LoRA layers of the text encoder if LoRA layers are loaded. + clip_skip (`int`, *optional*): + Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that + the output of the pre-final layer will be used for computing the prompt embeddings. + """ + # set lora scale so that monkey patched LoRA + # function of text encoder can correctly access it + if lora_scale is not None and isinstance(self, LoraLoaderMixin): + self._lora_scale = lora_scale + + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + # textual inversion: procecss multi-vector tokens if necessary + if isinstance(self, TextualInversionLoaderMixin): + prompt = self.maybe_convert_prompt(prompt, self.tokenizer) + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + if clip_skip is None: + prompt_embeds = self.text_encoder(text_input_ids.to(device), attention_mask=attention_mask) + prompt_embeds = prompt_embeds[0] + else: + prompt_embeds = self.text_encoder( + text_input_ids.to(device), attention_mask=attention_mask, output_hidden_states=True + ) + # Access the `hidden_states` first, that contains a tuple of + # all the hidden states from the encoder layers. Then index into + # the tuple to access the hidden states from the desired layer. + prompt_embeds = prompt_embeds[-1][-(clip_skip + 1)] + # We also need to apply the final LayerNorm here to not mess with the + # representations. The `last_hidden_states` that we typically use for + # obtaining the final prompt representations passes through the LayerNorm + # layer. + prompt_embeds = self.text_encoder.text_model.final_layer_norm(prompt_embeds) + + if self.text_encoder is not None: + prompt_embeds_dtype = self.text_encoder.dtype + elif self.unet is not None: + prompt_embeds_dtype = self.unet.dtype + else: + prompt_embeds_dtype = prompt_embeds.dtype + + prompt_embeds = prompt_embeds.to(dtype=prompt_embeds_dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif prompt is not None and type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + # textual inversion: procecss multi-vector tokens if necessary + if isinstance(self, TextualInversionLoaderMixin): + uncond_tokens = self.maybe_convert_prompt(uncond_tokens, self.tokenizer) + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=prompt_embeds_dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + return prompt_embeds, negative_prompt_embeds + + def prepare_ip_adapter_image_embeds( + self, ip_adapter_image, ip_adapter_image_embeds, device, num_images_per_prompt, do_classifier_free_guidance + ): + if ip_adapter_image_embeds is None: + if not isinstance(ip_adapter_image, list): + ip_adapter_image = [ip_adapter_image] + + if len(ip_adapter_image) != len(self.unet.encoder_hid_proj.image_projection_layers): + raise ValueError( + f"`ip_adapter_image` must have same length as the number of IP Adapters. Got {len(ip_adapter_image)} images and {len(self.unet.encoder_hid_proj.image_projection_layers)} IP Adapters." + ) + + image_embeds = [] + for single_ip_adapter_image, image_proj_layer in zip( + ip_adapter_image, self.unet.encoder_hid_proj.image_projection_layers + ): + output_hidden_state = not isinstance(image_proj_layer, ImageProjection) + single_image_embeds, single_negative_image_embeds = self.encode_image( + single_ip_adapter_image, device, 1, output_hidden_state + ) + single_image_embeds = torch.stack([single_image_embeds] * num_images_per_prompt, dim=0) + single_negative_image_embeds = torch.stack( + [single_negative_image_embeds] * num_images_per_prompt, dim=0 + ) + + if do_classifier_free_guidance: + single_image_embeds = torch.cat([single_negative_image_embeds, single_image_embeds]) + single_image_embeds = single_image_embeds.to(device) + + image_embeds.append(single_image_embeds) + else: + repeat_dims = [1] + image_embeds = [] + for single_image_embeds in ip_adapter_image_embeds: + if do_classifier_free_guidance: + single_negative_image_embeds, single_image_embeds = single_image_embeds.chunk(2) + single_image_embeds = single_image_embeds.repeat( + num_images_per_prompt, *(repeat_dims * len(single_image_embeds.shape[1:])) + ) + single_negative_image_embeds = single_negative_image_embeds.repeat( + num_images_per_prompt, *(repeat_dims * len(single_negative_image_embeds.shape[1:])) + ) + single_image_embeds = torch.cat([single_negative_image_embeds, single_image_embeds]) + else: + single_image_embeds = single_image_embeds.repeat( + num_images_per_prompt, *(repeat_dims * len(single_image_embeds.shape[1:])) + ) + image_embeds.append(single_image_embeds) + + return image_embeds + + def encode_image(self, image, device, num_images_per_prompt, output_hidden_states=None): + dtype = next(self.image_encoder.parameters()).dtype + + if not isinstance(image, torch.Tensor): + image = self.feature_extractor(image, return_tensors="pt").pixel_values + + image = image.to(device=device, dtype=dtype) + if output_hidden_states: + image_enc_hidden_states = self.image_encoder(image, output_hidden_states=True).hidden_states[-2] + image_enc_hidden_states = image_enc_hidden_states.repeat_interleave(num_images_per_prompt, dim=0) + uncond_image_enc_hidden_states = self.image_encoder( + torch.zeros_like(image), output_hidden_states=True + ).hidden_states[-2] + uncond_image_enc_hidden_states = uncond_image_enc_hidden_states.repeat_interleave( + num_images_per_prompt, dim=0 + ) + return image_enc_hidden_states, uncond_image_enc_hidden_states + else: + image_embeds = self.image_encoder(image).image_embeds + image_embeds = image_embeds.repeat_interleave(num_images_per_prompt, dim=0) + uncond_image_embeds = torch.zeros_like(image_embeds) + + return image_embeds, uncond_image_embeds + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is None: + has_nsfw_concept = None + else: + if torch.is_tensor(image): + feature_extractor_input = self.image_processor.postprocess(image, output_type="pil") + else: + feature_extractor_input = self.image_processor.numpy_to_pil(image) + safety_checker_input = self.feature_extractor(feature_extractor_input, return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + return image, has_nsfw_concept + + def decode_latents(self, latents): + deprecation_message = "The decode_latents method is deprecated and will be removed in 1.0.0. Please use VaeImageProcessor.postprocess(...) instead" + deprecate("decode_latents", "1.0.0", deprecation_message, standard_warn=False) + + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents, return_dict=False)[0] + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + callback_on_step_end_tensor_inputs=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + if callback_on_step_end_tensor_inputs is not None and not all( + k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs + ): + raise ValueError( + f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}" + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + def enable_freeu(self, s1: float, s2: float, b1: float, b2: float): + r"""Enables the FreeU mechanism as in https://arxiv.org/abs/2309.11497. + + The suffixes after the scaling factors represent the stages where they are being applied. + + Please refer to the [official repository](https://github.com/ChenyangSi/FreeU) for combinations of the values + that are known to work well for different pipelines such as Stable Diffusion v1, v2, and Stable Diffusion XL. + + Args: + s1 (`float`): + Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to + mitigate "oversmoothing effect" in the enhanced denoising process. + s2 (`float`): + Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to + mitigate "oversmoothing effect" in the enhanced denoising process. + b1 (`float`): Scaling factor for stage 1 to amplify the contributions of backbone features. + b2 (`float`): Scaling factor for stage 2 to amplify the contributions of backbone features. + """ + if not hasattr(self, "unet"): + raise ValueError("The pipeline must have `unet` for using FreeU.") + self.unet.enable_freeu(s1=s1, s2=s2, b1=b1, b2=b2) + + def disable_freeu(self): + """Disables the FreeU mechanism if enabled.""" + self.unet.disable_freeu() + + # Copied from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl.StableDiffusionXLPipeline.fuse_qkv_projections + def fuse_qkv_projections(self, unet: bool = True, vae: bool = True): + """ + Enables fused QKV projections. For self-attention modules, all projection matrices (i.e., query, + key, value) are fused. For cross-attention modules, key and value projection matrices are fused. + + + + This API is 🧪 experimental. + + + + Args: + unet (`bool`, defaults to `True`): To apply fusion on the UNet. + vae (`bool`, defaults to `True`): To apply fusion on the VAE. + """ + self.fusing_unet = False + self.fusing_vae = False + + if unet: + self.fusing_unet = True + self.unet.fuse_qkv_projections() + self.unet.set_attn_processor(FusedAttnProcessor2_0()) + + if vae: + if not isinstance(self.vae, AutoencoderKL): + raise ValueError("`fuse_qkv_projections()` is only supported for the VAE of type `AutoencoderKL`.") + + self.fusing_vae = True + self.vae.fuse_qkv_projections() + self.vae.set_attn_processor(FusedAttnProcessor2_0()) + + # Copied from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl.StableDiffusionXLPipeline.unfuse_qkv_projections + def unfuse_qkv_projections(self, unet: bool = True, vae: bool = True): + """Disable QKV projection fusion if enabled. + + + + This API is 🧪 experimental. + + + + Args: + unet (`bool`, defaults to `True`): To apply fusion on the UNet. + vae (`bool`, defaults to `True`): To apply fusion on the VAE. + + """ + if unet: + if not self.fusing_unet: + logger.warning("The UNet was not initially fused for QKV projections. Doing nothing.") + else: + self.unet.unfuse_qkv_projections() + self.fusing_unet = False + + if vae: + if not self.fusing_vae: + logger.warning("The VAE was not initially fused for QKV projections. Doing nothing.") + else: + self.vae.unfuse_qkv_projections() + self.fusing_vae = False + + # Copied from diffusers.pipelines.latent_consistency_models.pipeline_latent_consistency_text2img.LatentConsistencyModelPipeline.get_guidance_scale_embedding + def get_guidance_scale_embedding(self, w, embedding_dim=512, dtype=torch.float32): + """ + See https://github.com/google-research/vdm/blob/dc27b98a554f65cdc654b800da5aa1846545d41b/model_vdm.py#L298 + + Args: + timesteps (`torch.Tensor`): + generate embedding vectors at these timesteps + embedding_dim (`int`, *optional*, defaults to 512): + dimension of the embeddings to generate + dtype: + data type of the generated embeddings + + Returns: + `torch.FloatTensor`: Embedding vectors with shape `(len(timesteps), embedding_dim)` + """ + assert len(w.shape) == 1 + w = w * 1000.0 + + half_dim = embedding_dim // 2 + emb = torch.log(torch.tensor(10000.0)) / (half_dim - 1) + emb = torch.exp(torch.arange(half_dim, dtype=dtype) * -emb) + emb = w.to(dtype)[:, None] * emb[None, :] + emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=1) + if embedding_dim % 2 == 1: # zero pad + emb = torch.nn.functional.pad(emb, (0, 1)) + assert emb.shape == (w.shape[0], embedding_dim) + return emb + + @property + def guidance_scale(self): + return self._guidance_scale + + @property + def guidance_rescale(self): + return self._guidance_rescale + + @property + def clip_skip(self): + return self._clip_skip + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + @property + def do_classifier_free_guidance(self): + return self._guidance_scale > 1 and self.unet.config.time_cond_proj_dim is None + + @property + def cross_attention_kwargs(self): + return self._cross_attention_kwargs + + @property + def num_timesteps(self): + return self._num_timesteps + + @property + def interrupt(self): + return self._interrupt + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + timesteps: List[int] = None, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ip_adapter_image: Optional[PipelineImageInput] = None, + ip_adapter_image_embeds: Optional[List[torch.FloatTensor]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + guidance_rescale: float = 0.0, + clip_skip: Optional[int] = None, + callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None, + callback_on_step_end_tensor_inputs: List[str] = ["latents"], + **kwargs, + ): + r""" + The call function to the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide image generation. If not defined, you need to pass `prompt_embeds`. + height (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + timesteps (`List[int]`, *optional*): + Custom timesteps to use for the denoising process with schedulers which support a `timesteps` argument + in their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is + passed will be used. Must be in descending order. + guidance_scale (`float`, *optional*, defaults to 7.5): + A higher guidance scale value encourages the model to generate images closely linked to the text + `prompt` at the expense of lower image quality. Guidance scale is enabled when `guidance_scale > 1`. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide what to not include in image generation. If not defined, you need to + pass `negative_prompt_embeds` instead. Ignored when not using guidance (`guidance_scale < 1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) from the [DDIM](https://arxiv.org/abs/2010.02502) paper. Only applies + to the [`~schedulers.DDIMScheduler`], and is ignored in other schedulers. + generator (`torch.Generator` or `List[torch.Generator]`, *optional*): + A [`torch.Generator`](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make + generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor is generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs (prompt weighting). If not + provided, text embeddings are generated from the `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs (prompt weighting). If + not provided, `negative_prompt_embeds` are generated from the `negative_prompt` input argument. + ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generated image. Choose between `PIL.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the [`AttentionProcessor`] as defined in + [`self.processor`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + guidance_rescale (`float`, *optional*, defaults to 0.0): + Guidance rescale factor from [Common Diffusion Noise Schedules and Sample Steps are + Flawed](https://arxiv.org/pdf/2305.08891.pdf). Guidance rescale factor should fix overexposure when + using zero terminal SNR. + clip_skip (`int`, *optional*): + Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that + the output of the pre-final layer will be used for computing the prompt embeddings. + callback_on_step_end (`Callable`, *optional*): + A function that calls at the end of each denoising steps during the inference. The function is called + with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int, + callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by + `callback_on_step_end_tensor_inputs`. + callback_on_step_end_tensor_inputs (`List`, *optional*): + The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list + will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the + `._callback_tensor_inputs` attribute of your pipeline class. + + Examples: + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + If `return_dict` is `True`, [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] is returned, + otherwise a `tuple` is returned where the first element is a list with the generated images and the + second element is a list of `bool`s indicating whether the corresponding generated image contains + "not-safe-for-work" (nsfw) content. + """ + + callback = kwargs.pop("callback", None) + callback_steps = kwargs.pop("callback_steps", None) + + if callback is not None: + deprecate( + "callback", + "1.0.0", + "Passing `callback` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`", + ) + if callback_steps is not None: + deprecate( + "callback_steps", + "1.0.0", + "Passing `callback_steps` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`", + ) + + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + # to deal with lora scaling and other possible forward hooks + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, + height, + width, + callback_steps, + negative_prompt, + prompt_embeds, + negative_prompt_embeds, + callback_on_step_end_tensor_inputs, + ) + + self._guidance_scale = guidance_scale + self._guidance_rescale = guidance_rescale + self._clip_skip = clip_skip + self._cross_attention_kwargs = cross_attention_kwargs + self._interrupt = False + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + + # 3. Encode input prompt + lora_scale = ( + self.cross_attention_kwargs.get("scale", None) if self.cross_attention_kwargs is not None else None + ) + + prompt_embeds, negative_prompt_embeds = self.encode_prompt( + prompt, + device, + num_images_per_prompt, + self.do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + lora_scale=lora_scale, + clip_skip=self.clip_skip, + ) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + if self.do_classifier_free_guidance: + if prompt_embeds.shape != negative_prompt_embeds.shape: + tmp_embeds = negative_prompt_embeds.clone() + tmp_embeds[:, 0:1, :] = prompt_embeds + prompt_embeds = tmp_embeds + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + # TODO + if ip_adapter_image is not None or ip_adapter_image_embeds is not None: + image_embeds = self.prepare_ip_adapter_image_embeds( + ip_adapter_image, + ip_adapter_image_embeds, + device, + batch_size * num_images_per_prompt, + self.do_classifier_free_guidance, + ) + + # 4. Prepare timesteps + timesteps, num_inference_steps = retrieve_timesteps(self.scheduler, num_inference_steps, device, timesteps) + + # 5. Prepare latent variables + num_channels_latents = self.unet.config.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 6.1 Add image embeds for IP-Adapter + added_cond_kwargs = {"image_embeds": image_embeds} if ip_adapter_image is not None else None + + # 6.2 Optionally get Guidance Scale Embedding + timestep_cond = None + if self.unet.config.time_cond_proj_dim is not None: + guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(batch_size * num_images_per_prompt) + timestep_cond = self.get_guidance_scale_embedding( + guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim + ).to(device=device, dtype=latents.dtype) + + # 7. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + self._num_timesteps = len(timesteps) + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + if self.interrupt: + continue + + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + timestep_cond=timestep_cond, + cross_attention_kwargs=self.cross_attention_kwargs, + added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[0] + + # perform guidance + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + + if self.do_classifier_free_guidance and self.guidance_rescale > 0.0: + # Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf + noise_pred = rescale_noise_cfg(noise_pred, noise_pred_text, guidance_rescale=self.guidance_rescale) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0] + + if callback_on_step_end is not None: + callback_kwargs = {} + for k in callback_on_step_end_tensor_inputs: + callback_kwargs[k] = locals()[k] + callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) + + latents = callback_outputs.pop("latents", latents) + prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) + negative_prompt_embeds = callback_outputs.pop("negative_prompt_embeds", negative_prompt_embeds) + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + step_idx = i // getattr(self.scheduler, "order", 1) + callback(step_idx, t, latents) + + if not output_type == "latent": + image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False, generator=generator)[ + 0 + ] + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + else: + image = latents + has_nsfw_concept = None + + if has_nsfw_concept is None: + do_denormalize = [True] * image.shape[0] + else: + do_denormalize = [not has_nsfw for has_nsfw in has_nsfw_concept] + + image = self.image_processor.postprocess(image, output_type=output_type, do_denormalize=do_denormalize) + + # Offload all models + self.maybe_free_model_hooks() + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/foleycrafter/pipelines/pipeline_controlnet.py b/foleycrafter/pipelines/pipeline_controlnet.py new file mode 100644 index 0000000..6c0867f --- /dev/null +++ b/foleycrafter/pipelines/pipeline_controlnet.py @@ -0,0 +1,1341 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import inspect +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import numpy as np +import PIL.Image +import torch +import torch.nn.functional as F +from transformers import CLIPImageProcessor, CLIPTextModel, CLIPTokenizer, CLIPVisionModelWithProjection + +from diffusers.image_processor import PipelineImageInput, VaeImageProcessor +from diffusers.loaders import FromSingleFileMixin, LoraLoaderMixin, TextualInversionLoaderMixin +from diffusers.models import AutoencoderKL, ControlNetModel, ImageProjection +from diffusers.models.lora import adjust_lora_scale_text_encoder +from diffusers.pipelines.controlnet.multicontrolnet import MultiControlNetModel +from diffusers.pipelines.pipeline_utils import DiffusionPipeline +from diffusers.pipelines.stable_diffusion.pipeline_output import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import KarrasDiffusionSchedulers +from diffusers.utils import ( + USE_PEFT_BACKEND, + deprecate, + logging, + replace_example_docstring, + scale_lora_layers, + unscale_lora_layers, +) +from diffusers.utils.torch_utils import is_compiled_module, is_torch_version, randn_tensor +from ..models.auffusion.loaders.ip_adapter import IPAdapterMixin +from ..models.auffusion_unet import UNet2DConditionModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +EXAMPLE_DOC_STRING = """ + Examples: + ```py + >>> # !pip install opencv-python transformers accelerate + >>> from diffusers import StableDiffusionControlNetPipeline, ControlNetModel, UniPCMultistepScheduler + >>> from diffusers.utils import load_image + >>> import numpy as np + >>> import torch + + >>> import cv2 + >>> from PIL import Image + + >>> # download an image + >>> image = load_image( + ... "https://hf.co/datasets/huggingface/documentation-images/resolve/main/diffusers/input_image_vermeer.png" + ... ) + >>> image = np.array(image) + + >>> # get canny image + >>> image = cv2.Canny(image, 100, 200) + >>> image = image[:, :, None] + >>> image = np.concatenate([image, image, image], axis=2) + >>> canny_image = Image.fromarray(image) + + >>> # load control net and stable diffusion v1-5 + >>> controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny", torch_dtype=torch.float16) + >>> pipe = StableDiffusionControlNetPipeline.from_pretrained( + ... "runwayml/stable-diffusion-v1-5", controlnet=controlnet, torch_dtype=torch.float16 + ... ) + + >>> # speed up diffusion process with faster scheduler and memory optimization + >>> pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config) + >>> # remove following line if xformers is not installed + >>> pipe.enable_xformers_memory_efficient_attention() + + >>> pipe.enable_model_cpu_offload() + + >>> # generate image + >>> generator = torch.manual_seed(0) + >>> image = pipe( + ... "futuristic-looking woman", num_inference_steps=20, generator=generator, image=canny_image + ... ).images[0] + ``` +""" + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps +def retrieve_timesteps( + scheduler, + num_inference_steps: Optional[int] = None, + device: Optional[Union[str, torch.device]] = None, + timesteps: Optional[List[int]] = None, + **kwargs, +): + """ + Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles + custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`. + + Args: + scheduler (`SchedulerMixin`): + The scheduler to get timesteps from. + num_inference_steps (`int`): + The number of diffusion steps used when generating samples with a pre-trained model. If used, + `timesteps` must be `None`. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + timesteps (`List[int]`, *optional*): + Custom timesteps used to support arbitrary spacing between timesteps. If `None`, then the default + timestep spacing strategy of the scheduler is used. If `timesteps` is passed, `num_inference_steps` + must be `None`. + + Returns: + `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the + second element is the number of inference steps. + """ + if timesteps is not None: + accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys()) + if not accepts_timesteps: + raise ValueError( + f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom" + f" timestep schedules. Please check whether you are using the correct scheduler." + ) + scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs) + timesteps = scheduler.timesteps + num_inference_steps = len(timesteps) + else: + scheduler.set_timesteps(num_inference_steps, device=device, **kwargs) + timesteps = scheduler.timesteps + return timesteps, num_inference_steps + + +class StableDiffusionControlNetPipeline( + DiffusionPipeline, TextualInversionLoaderMixin, LoraLoaderMixin, IPAdapterMixin, FromSingleFileMixin +): + r""" + Pipeline for text-to-image generation using Stable Diffusion with ControlNet guidance. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods + implemented for all pipelines (downloading, saving, running on a particular device, etc.). + + The pipeline also inherits the following loading methods: + - [`~loaders.TextualInversionLoaderMixin.load_textual_inversion`] for loading textual inversion embeddings + - [`~loaders.LoraLoaderMixin.load_lora_weights`] for loading LoRA weights + - [`~loaders.LoraLoaderMixin.save_lora_weights`] for saving LoRA weights + - [`~loaders.FromSingleFileMixin.from_single_file`] for loading `.ckpt` files + - [`~loaders.IPAdapterMixin.load_ip_adapter`] for loading IP Adapters + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) model to encode and decode images to and from latent representations. + text_encoder ([`~transformers.CLIPTextModel`]): + Frozen text-encoder ([clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14)). + tokenizer ([`~transformers.CLIPTokenizer`]): + A `CLIPTokenizer` to tokenize text. + unet ([`UNet2DConditionModel`]): + A `UNet2DConditionModel` to denoise the encoded image latents. + controlnet ([`ControlNetModel`] or `List[ControlNetModel]`): + Provides additional conditioning to the `unet` during the denoising process. If you set multiple + ControlNets as a list, the outputs from each ControlNet are added together to create one combined + additional conditioning. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for more details + about a model's potential harms. + feature_extractor ([`~transformers.CLIPImageProcessor`]): + A `CLIPImageProcessor` to extract features from generated images; used as inputs to the `safety_checker`. + """ + + model_cpu_offload_seq = "text_encoder->image_encoder->unet->vae" + _optional_components = ["safety_checker", "feature_extractor", "image_encoder"] + _exclude_from_cpu_offload = ["safety_checker"] + _callback_tensor_inputs = ["latents", "prompt_embeds", "negative_prompt_embeds"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + controlnet: Union[ControlNetModel, List[ControlNetModel], Tuple[ControlNetModel], MultiControlNetModel], + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPImageProcessor, + image_encoder: CLIPVisionModelWithProjection = None, + requires_safety_checker: bool = True, + ): + super().__init__() + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + if isinstance(controlnet, (list, tuple)): + controlnet = MultiControlNetModel(controlnet) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + controlnet=controlnet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + image_encoder=image_encoder, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor, do_convert_rgb=True) + self.control_image_processor = VaeImageProcessor( + vae_scale_factor=self.vae_scale_factor, do_convert_rgb=True, do_normalize=False + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_vae_slicing + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to + compute decoding in several steps. This is useful to save some memory and allow larger batch sizes. + """ + self.vae.enable_slicing() + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.disable_vae_slicing + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously enabled, this method will go back to + computing decoding in one step. + """ + self.vae.disable_slicing() + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_vae_tiling + def enable_vae_tiling(self): + r""" + Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to + compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow + processing larger images. + """ + self.vae.enable_tiling() + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.disable_vae_tiling + def disable_vae_tiling(self): + r""" + Disable tiled VAE decoding. If `enable_vae_tiling` was previously enabled, this method will go back to + computing decoding in one step. + """ + self.vae.disable_tiling() + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + lora_scale: Optional[float] = None, + **kwargs, + ): + deprecation_message = "`_encode_prompt()` is deprecated and it will be removed in a future version. Use `encode_prompt()` instead. Also, be aware that the output format changed from a concatenated tensor to a tuple." + deprecate("_encode_prompt()", "1.0.0", deprecation_message, standard_warn=False) + + prompt_embeds_tuple = self.encode_prompt( + prompt=prompt, + device=device, + num_images_per_prompt=num_images_per_prompt, + do_classifier_free_guidance=do_classifier_free_guidance, + negative_prompt=negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + lora_scale=lora_scale, + **kwargs, + ) + + # concatenate for backwards comp + prompt_embeds = torch.cat([prompt_embeds_tuple[1], prompt_embeds_tuple[0]]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.encode_prompt + def encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + lora_scale: Optional[float] = None, + clip_skip: Optional[int] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is + less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + lora_scale (`float`, *optional*): + A LoRA scale that will be applied to all LoRA layers of the text encoder if LoRA layers are loaded. + clip_skip (`int`, *optional*): + Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that + the output of the pre-final layer will be used for computing the prompt embeddings. + """ + # set lora scale so that monkey patched LoRA + # function of text encoder can correctly access it + if lora_scale is not None and isinstance(self, LoraLoaderMixin): + self._lora_scale = lora_scale + + # dynamically adjust the LoRA scale + if not USE_PEFT_BACKEND: + adjust_lora_scale_text_encoder(self.text_encoder, lora_scale) + else: + scale_lora_layers(self.text_encoder, lora_scale) + + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + # textual inversion: procecss multi-vector tokens if necessary + if isinstance(self, TextualInversionLoaderMixin): + prompt = self.maybe_convert_prompt(prompt, self.tokenizer) + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + if clip_skip is None: + prompt_embeds = self.text_encoder(text_input_ids.to(device), attention_mask=attention_mask) + prompt_embeds = prompt_embeds[0] + else: + prompt_embeds = self.text_encoder( + text_input_ids.to(device), attention_mask=attention_mask, output_hidden_states=True + ) + # Access the `hidden_states` first, that contains a tuple of + # all the hidden states from the encoder layers. Then index into + # the tuple to access the hidden states from the desired layer. + prompt_embeds = prompt_embeds[-1][-(clip_skip + 1)] + # We also need to apply the final LayerNorm here to not mess with the + # representations. The `last_hidden_states` that we typically use for + # obtaining the final prompt representations passes through the LayerNorm + # layer. + prompt_embeds = self.text_encoder.text_model.final_layer_norm(prompt_embeds) + + if self.text_encoder is not None: + prompt_embeds_dtype = self.text_encoder.dtype + elif self.unet is not None: + prompt_embeds_dtype = self.unet.dtype + else: + prompt_embeds_dtype = prompt_embeds.dtype + + prompt_embeds = prompt_embeds.to(dtype=prompt_embeds_dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif prompt is not None and type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + # textual inversion: procecss multi-vector tokens if necessary + if isinstance(self, TextualInversionLoaderMixin): + uncond_tokens = self.maybe_convert_prompt(uncond_tokens, self.tokenizer) + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=prompt_embeds_dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + if isinstance(self, LoraLoaderMixin) and USE_PEFT_BACKEND: + # Retrieve the original scale by scaling back the LoRA layers + unscale_lora_layers(self.text_encoder, lora_scale) + + return prompt_embeds, negative_prompt_embeds + + def prepare_ip_adapter_image_embeds( + self, ip_adapter_image, ip_adapter_image_embeds, device, num_images_per_prompt, do_classifier_free_guidance + ): + if ip_adapter_image_embeds is None: + if not isinstance(ip_adapter_image, list): + ip_adapter_image = [ip_adapter_image] + + if len(ip_adapter_image) != len(self.unet.encoder_hid_proj.image_projection_layers): + raise ValueError( + f"`ip_adapter_image` must have same length as the number of IP Adapters. Got {len(ip_adapter_image)} images and {len(self.unet.encoder_hid_proj.image_projection_layers)} IP Adapters." + ) + + image_embeds = [] + for single_ip_adapter_image, image_proj_layer in zip( + ip_adapter_image, self.unet.encoder_hid_proj.image_projection_layers + ): + output_hidden_state = not isinstance(image_proj_layer, ImageProjection) + single_image_embeds, single_negative_image_embeds = self.encode_image( + single_ip_adapter_image, device, 1, output_hidden_state + ) + single_image_embeds = torch.stack([single_image_embeds] * num_images_per_prompt, dim=0) + single_negative_image_embeds = torch.stack( + [single_negative_image_embeds] * num_images_per_prompt, dim=0 + ) + + if do_classifier_free_guidance: + single_image_embeds = torch.cat([single_negative_image_embeds, single_image_embeds]) + single_image_embeds = single_image_embeds.to(device) + + image_embeds.append(single_image_embeds) + else: + repeat_dims = [1] + image_embeds = [] + for single_image_embeds in ip_adapter_image_embeds: + if do_classifier_free_guidance: + single_negative_image_embeds, single_image_embeds = single_image_embeds.chunk(2) + single_image_embeds = single_image_embeds.repeat( + num_images_per_prompt, *(repeat_dims * len(single_image_embeds.shape[1:])) + ) + single_negative_image_embeds = single_negative_image_embeds.repeat( + num_images_per_prompt, *(repeat_dims * len(single_negative_image_embeds.shape[1:])) + ) + single_image_embeds = torch.cat([single_negative_image_embeds, single_image_embeds]) + else: + single_image_embeds = single_image_embeds.repeat( + num_images_per_prompt, *(repeat_dims * len(single_image_embeds.shape[1:])) + ) + image_embeds.append(single_image_embeds) + + return image_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.encode_image + def encode_image(self, image, device, num_images_per_prompt, output_hidden_states=None): + dtype = next(self.image_encoder.parameters()).dtype + + if not isinstance(image, torch.Tensor): + image = self.feature_extractor(image, return_tensors="pt").pixel_values + + image = image.to(device=device, dtype=dtype) + if output_hidden_states: + image_enc_hidden_states = self.image_encoder(image, output_hidden_states=True).hidden_states[-2] + image_enc_hidden_states = image_enc_hidden_states.repeat_interleave(num_images_per_prompt, dim=0) + uncond_image_enc_hidden_states = self.image_encoder( + torch.zeros_like(image), output_hidden_states=True + ).hidden_states[-2] + uncond_image_enc_hidden_states = uncond_image_enc_hidden_states.repeat_interleave( + num_images_per_prompt, dim=0 + ) + return image_enc_hidden_states, uncond_image_enc_hidden_states + else: + image_embeds = self.image_encoder(image).image_embeds + image_embeds = image_embeds.repeat_interleave(num_images_per_prompt, dim=0) + uncond_image_embeds = torch.zeros_like(image_embeds) + + return image_embeds, uncond_image_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is None: + has_nsfw_concept = None + else: + if torch.is_tensor(image): + feature_extractor_input = self.image_processor.postprocess(image, output_type="pil") + else: + feature_extractor_input = self.image_processor.numpy_to_pil(image) + safety_checker_input = self.feature_extractor(feature_extractor_input, return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + deprecation_message = "The decode_latents method is deprecated and will be removed in 1.0.0. Please use VaeImageProcessor.postprocess(...) instead" + deprecate("decode_latents", "1.0.0", deprecation_message, standard_warn=False) + + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents, return_dict=False)[0] + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, + prompt, + image, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + controlnet_conditioning_scale=1.0, + control_guidance_start=0.0, + control_guidance_end=1.0, + callback_on_step_end_tensor_inputs=None, + ): + if callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if callback_on_step_end_tensor_inputs is not None and not all( + k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs + ): + raise ValueError( + f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}" + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # `prompt` needs more sophisticated handling when there are multiple + # conditionings. + if isinstance(self.controlnet, MultiControlNetModel): + if isinstance(prompt, list): + logger.warning( + f"You have {len(self.controlnet.nets)} ControlNets and you have passed {len(prompt)}" + " prompts. The conditionings will be fixed across the prompts." + ) + + # Check `image` + is_compiled = hasattr(F, "scaled_dot_product_attention") and isinstance( + self.controlnet, torch._dynamo.eval_frame.OptimizedModule + ) + if ( + isinstance(self.controlnet, ControlNetModel) + or is_compiled + and isinstance(self.controlnet._orig_mod, ControlNetModel) + ): + self.check_image(image, prompt, prompt_embeds) + elif ( + isinstance(self.controlnet, MultiControlNetModel) + or is_compiled + and isinstance(self.controlnet._orig_mod, MultiControlNetModel) + ): + if not isinstance(image, list): + raise TypeError("For multiple controlnets: `image` must be type `list`") + + # When `image` is a nested list: + # (e.g. [[canny_image_1, pose_image_1], [canny_image_2, pose_image_2]]) + elif any(isinstance(i, list) for i in image): + raise ValueError("A single batch of multiple conditionings is not supported at the moment.") + elif len(image) != len(self.controlnet.nets): + raise ValueError( + f"For multiple controlnets: `image` must have the same length as the number of controlnets, but got {len(image)} images and {len(self.controlnet.nets)} ControlNets." + ) + + for image_ in image: + self.check_image(image_, prompt, prompt_embeds) + else: + assert False + + # Check `controlnet_conditioning_scale` + if ( + isinstance(self.controlnet, ControlNetModel) + or is_compiled + and isinstance(self.controlnet._orig_mod, ControlNetModel) + ): + if not isinstance(controlnet_conditioning_scale, float): + raise TypeError("For single controlnet: `controlnet_conditioning_scale` must be type `float`.") + elif ( + isinstance(self.controlnet, MultiControlNetModel) + or is_compiled + and isinstance(self.controlnet._orig_mod, MultiControlNetModel) + ): + if isinstance(controlnet_conditioning_scale, list): + if any(isinstance(i, list) for i in controlnet_conditioning_scale): + raise ValueError("A single batch of multiple conditionings is not supported at the moment.") + elif isinstance(controlnet_conditioning_scale, list) and len(controlnet_conditioning_scale) != len( + self.controlnet.nets + ): + raise ValueError( + "For multiple controlnets: When `controlnet_conditioning_scale` is specified as `list`, it must have" + " the same length as the number of controlnets" + ) + else: + assert False + + if not isinstance(control_guidance_start, (tuple, list)): + control_guidance_start = [control_guidance_start] + + if not isinstance(control_guidance_end, (tuple, list)): + control_guidance_end = [control_guidance_end] + + if len(control_guidance_start) != len(control_guidance_end): + raise ValueError( + f"`control_guidance_start` has {len(control_guidance_start)} elements, but `control_guidance_end` has {len(control_guidance_end)} elements. Make sure to provide the same number of elements to each list." + ) + + if isinstance(self.controlnet, MultiControlNetModel): + if len(control_guidance_start) != len(self.controlnet.nets): + raise ValueError( + f"`control_guidance_start`: {control_guidance_start} has {len(control_guidance_start)} elements but there are {len(self.controlnet.nets)} controlnets available. Make sure to provide {len(self.controlnet.nets)}." + ) + + for start, end in zip(control_guidance_start, control_guidance_end): + if start >= end: + raise ValueError( + f"control guidance start: {start} cannot be larger or equal to control guidance end: {end}." + ) + if start < 0.0: + raise ValueError(f"control guidance start: {start} can't be smaller than 0.") + if end > 1.0: + raise ValueError(f"control guidance end: {end} can't be larger than 1.0.") + + def check_image(self, image, prompt, prompt_embeds): + image_is_pil = isinstance(image, PIL.Image.Image) + image_is_tensor = isinstance(image, torch.Tensor) + image_is_np = isinstance(image, np.ndarray) + image_is_pil_list = isinstance(image, list) and isinstance(image[0], PIL.Image.Image) + image_is_tensor_list = isinstance(image, list) and isinstance(image[0], torch.Tensor) + image_is_np_list = isinstance(image, list) and isinstance(image[0], np.ndarray) + + if ( + not image_is_pil + and not image_is_tensor + and not image_is_np + and not image_is_pil_list + and not image_is_tensor_list + and not image_is_np_list + ): + raise TypeError( + f"image must be passed and be one of PIL image, numpy array, torch tensor, list of PIL images, list of numpy arrays or list of torch tensors, but is {type(image)}" + ) + + if image_is_pil: + image_batch_size = 1 + else: + image_batch_size = len(image) + + if prompt is not None and isinstance(prompt, str): + prompt_batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + prompt_batch_size = len(prompt) + elif prompt_embeds is not None: + prompt_batch_size = prompt_embeds.shape[0] + + if image_batch_size != 1 and image_batch_size != prompt_batch_size: + raise ValueError( + f"If image batch size is not 1, image batch size must be same as prompt batch size. image batch size: {image_batch_size}, prompt batch size: {prompt_batch_size}" + ) + + def prepare_image( + self, + image, + width, + height, + batch_size, + num_images_per_prompt, + device, + dtype, + do_classifier_free_guidance=False, + guess_mode=False, + ): + image = self.control_image_processor.preprocess(image, height=height, width=width).to(dtype=torch.float32) + image_batch_size = image.shape[0] + + if image_batch_size == 1: + repeat_by = batch_size + else: + # image batch size is the same as prompt batch size + repeat_by = num_images_per_prompt + + image = image.repeat_interleave(repeat_by, dim=0) + + image = image.to(device=device, dtype=dtype) + + if do_classifier_free_guidance and not guess_mode: + image = torch.cat([image] * 2) + + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_freeu + def enable_freeu(self, s1: float, s2: float, b1: float, b2: float): + r"""Enables the FreeU mechanism as in https://arxiv.org/abs/2309.11497. + + The suffixes after the scaling factors represent the stages where they are being applied. + + Please refer to the [official repository](https://github.com/ChenyangSi/FreeU) for combinations of the values + that are known to work well for different pipelines such as Stable Diffusion v1, v2, and Stable Diffusion XL. + + Args: + s1 (`float`): + Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to + mitigate "oversmoothing effect" in the enhanced denoising process. + s2 (`float`): + Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to + mitigate "oversmoothing effect" in the enhanced denoising process. + b1 (`float`): Scaling factor for stage 1 to amplify the contributions of backbone features. + b2 (`float`): Scaling factor for stage 2 to amplify the contributions of backbone features. + """ + if not hasattr(self, "unet"): + raise ValueError("The pipeline must have `unet` for using FreeU.") + self.unet.enable_freeu(s1=s1, s2=s2, b1=b1, b2=b2) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.disable_freeu + def disable_freeu(self): + """Disables the FreeU mechanism if enabled.""" + self.unet.disable_freeu() + + # Copied from diffusers.pipelines.latent_consistency_models.pipeline_latent_consistency_text2img.LatentConsistencyModelPipeline.get_guidance_scale_embedding + def get_guidance_scale_embedding(self, w, embedding_dim=512, dtype=torch.float32): + """ + See https://github.com/google-research/vdm/blob/dc27b98a554f65cdc654b800da5aa1846545d41b/model_vdm.py#L298 + + Args: + timesteps (`torch.Tensor`): + generate embedding vectors at these timesteps + embedding_dim (`int`, *optional*, defaults to 512): + dimension of the embeddings to generate + dtype: + data type of the generated embeddings + + Returns: + `torch.FloatTensor`: Embedding vectors with shape `(len(timesteps), embedding_dim)` + """ + assert len(w.shape) == 1 + w = w * 1000.0 + + half_dim = embedding_dim // 2 + emb = torch.log(torch.tensor(10000.0)) / (half_dim - 1) + emb = torch.exp(torch.arange(half_dim, dtype=dtype) * -emb) + emb = w.to(dtype)[:, None] * emb[None, :] + emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=1) + if embedding_dim % 2 == 1: # zero pad + emb = torch.nn.functional.pad(emb, (0, 1)) + assert emb.shape == (w.shape[0], embedding_dim) + return emb + + @property + def guidance_scale(self): + return self._guidance_scale + + @property + def clip_skip(self): + return self._clip_skip + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + @property + def do_classifier_free_guidance(self): + return self._guidance_scale > 1 and self.unet.config.time_cond_proj_dim is None + + @property + def cross_attention_kwargs(self): + return self._cross_attention_kwargs + + @property + def num_timesteps(self): + return self._num_timesteps + + @torch.no_grad() + @replace_example_docstring(EXAMPLE_DOC_STRING) + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: PipelineImageInput = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + timesteps: List[int] = None, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ip_adapter_image: Optional[PipelineImageInput] = None, + ip_adapter_image_embeds: Optional[List[torch.FloatTensor]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + controlnet_conditioning_scale: Union[float, List[float]] = 1.0, + controlnet_prompt_embeds: Optional[List[torch.FloatTensor]] = None, + guess_mode: bool = False, + control_guidance_start: Union[float, List[float]] = 0.0, + control_guidance_end: Union[float, List[float]] = 1.0, + clip_skip: Optional[int] = None, + callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None, + callback_on_step_end_tensor_inputs: List[str] = ["latents"], + **kwargs, + ): + r""" + The call function to the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide image generation. If not defined, you need to pass `prompt_embeds`. + image (`torch.FloatTensor`, `PIL.Image.Image`, `np.ndarray`, `List[torch.FloatTensor]`, `List[PIL.Image.Image]`, `List[np.ndarray]`,: + `List[List[torch.FloatTensor]]`, `List[List[np.ndarray]]` or `List[List[PIL.Image.Image]]`): + The ControlNet input condition to provide guidance to the `unet` for generation. If the type is + specified as `torch.FloatTensor`, it is passed to ControlNet as is. `PIL.Image.Image` can also be + accepted as an image. The dimensions of the output image defaults to `image`'s dimensions. If height + and/or width are passed, `image` is resized accordingly. If multiple ControlNets are specified in + `init`, images must be passed as a list such that each element of the list can be correctly batched for + input to a single ControlNet. + height (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + timesteps (`List[int]`, *optional*): + Custom timesteps to use for the denoising process with schedulers which support a `timesteps` argument + in their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is + passed will be used. Must be in descending order. + guidance_scale (`float`, *optional*, defaults to 7.5): + A higher guidance scale value encourages the model to generate images closely linked to the text + `prompt` at the expense of lower image quality. Guidance scale is enabled when `guidance_scale > 1`. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide what to not include in image generation. If not defined, you need to + pass `negative_prompt_embeds` instead. Ignored when not using guidance (`guidance_scale < 1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) from the [DDIM](https://arxiv.org/abs/2010.02502) paper. Only applies + to the [`~schedulers.DDIMScheduler`], and is ignored in other schedulers. + generator (`torch.Generator` or `List[torch.Generator]`, *optional*): + A [`torch.Generator`](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make + generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor is generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs (prompt weighting). If not + provided, text embeddings are generated from the `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs (prompt weighting). If + not provided, `negative_prompt_embeds` are generated from the `negative_prompt` input argument. + ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generated image. Choose between `PIL.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that calls every `callback_steps` steps during inference. The function is called with the + following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function is called. If not specified, the callback is called at + every step. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the [`AttentionProcessor`] as defined in + [`self.processor`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + controlnet_conditioning_scale (`float` or `List[float]`, *optional*, defaults to 1.0): + The outputs of the ControlNet are multiplied by `controlnet_conditioning_scale` before they are added + to the residual in the original `unet`. If multiple ControlNets are specified in `init`, you can set + the corresponding scale as a list. + guess_mode (`bool`, *optional*, defaults to `False`): + The ControlNet encoder tries to recognize the content of the input image even if you remove all + prompts. A `guidance_scale` value between 3.0 and 5.0 is recommended. + control_guidance_start (`float` or `List[float]`, *optional*, defaults to 0.0): + The percentage of total steps at which the ControlNet starts applying. + control_guidance_end (`float` or `List[float]`, *optional*, defaults to 1.0): + The percentage of total steps at which the ControlNet stops applying. + clip_skip (`int`, *optional*): + Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that + the output of the pre-final layer will be used for computing the prompt embeddings. + callback_on_step_end (`Callable`, *optional*): + A function that calls at the end of each denoising steps during the inference. The function is called + with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int, + callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by + `callback_on_step_end_tensor_inputs`. + callback_on_step_end_tensor_inputs (`List`, *optional*): + The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list + will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the + `._callback_tensor_inputs` attribute of your pipeline class. + + Examples: + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + If `return_dict` is `True`, [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] is returned, + otherwise a `tuple` is returned where the first element is a list with the generated images and the + second element is a list of `bool`s indicating whether the corresponding generated image contains + "not-safe-for-work" (nsfw) content. + """ + + callback = kwargs.pop("callback", None) + callback_steps = kwargs.pop("callback_steps", None) + + if callback is not None: + deprecate( + "callback", + "1.0.0", + "Passing `callback` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`", + ) + if callback_steps is not None: + deprecate( + "callback_steps", + "1.0.0", + "Passing `callback_steps` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`", + ) + + controlnet = self.controlnet._orig_mod if is_compiled_module(self.controlnet) else self.controlnet + + # align format for control guidance + if not isinstance(control_guidance_start, list) and isinstance(control_guidance_end, list): + control_guidance_start = len(control_guidance_end) * [control_guidance_start] + elif not isinstance(control_guidance_end, list) and isinstance(control_guidance_start, list): + control_guidance_end = len(control_guidance_start) * [control_guidance_end] + elif not isinstance(control_guidance_start, list) and not isinstance(control_guidance_end, list): + mult = len(controlnet.nets) if isinstance(controlnet, MultiControlNetModel) else 1 + control_guidance_start, control_guidance_end = ( + mult * [control_guidance_start], + mult * [control_guidance_end], + ) + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, + image, + callback_steps, + negative_prompt, + prompt_embeds, + negative_prompt_embeds, + controlnet_conditioning_scale, + control_guidance_start, + control_guidance_end, + callback_on_step_end_tensor_inputs, + ) + + self._guidance_scale = guidance_scale + self._clip_skip = clip_skip + self._cross_attention_kwargs = cross_attention_kwargs + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + + if isinstance(controlnet, MultiControlNetModel) and isinstance(controlnet_conditioning_scale, float): + controlnet_conditioning_scale = [controlnet_conditioning_scale] * len(controlnet.nets) + + global_pool_conditions = ( + controlnet.config.global_pool_conditions + if isinstance(controlnet, ControlNetModel) + else controlnet.nets[0].config.global_pool_conditions + ) + guess_mode = guess_mode or global_pool_conditions + + # 3. Encode input prompt + text_encoder_lora_scale = ( + self.cross_attention_kwargs.get("scale", None) if self.cross_attention_kwargs is not None else None + ) + prompt_embeds, negative_prompt_embeds = self.encode_prompt( + prompt, + device, + num_images_per_prompt, + self.do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + lora_scale=text_encoder_lora_scale, + clip_skip=self.clip_skip, + ) + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + if self.do_classifier_free_guidance: + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + if ip_adapter_image is not None or ip_adapter_image_embeds is not None: + image_embeds = self.prepare_ip_adapter_image_embeds( + ip_adapter_image, + ip_adapter_image_embeds, + device, + batch_size * num_images_per_prompt, + self.do_classifier_free_guidance, + ) + + # 4. Prepare image + if isinstance(controlnet, ControlNetModel): + image = self.prepare_image( + image=image, + width=width, + height=height, + batch_size=batch_size * num_images_per_prompt, + num_images_per_prompt=num_images_per_prompt, + device=device, + dtype=controlnet.dtype, + do_classifier_free_guidance=self.do_classifier_free_guidance, + guess_mode=guess_mode, + ) + height, width = image.shape[-2:] + elif isinstance(controlnet, MultiControlNetModel): + images = [] + + for image_ in image: + image_ = self.prepare_image( + image=image_, + width=width, + height=height, + batch_size=batch_size * num_images_per_prompt, + num_images_per_prompt=num_images_per_prompt, + device=device, + dtype=controlnet.dtype, + do_classifier_free_guidance=self.do_classifier_free_guidance, + guess_mode=guess_mode, + ) + + images.append(image_) + + image = images + height, width = image[0].shape[-2:] + else: + assert False + + # 5. Prepare timesteps + timesteps, num_inference_steps = retrieve_timesteps(self.scheduler, num_inference_steps, device, timesteps) + self._num_timesteps = len(timesteps) + + # 6. Prepare latent variables + num_channels_latents = self.unet.config.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6.5 Optionally get Guidance Scale Embedding + timestep_cond = None + if self.unet.config.time_cond_proj_dim is not None: + guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(batch_size * num_images_per_prompt) + timestep_cond = self.get_guidance_scale_embedding( + guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim + ).to(device=device, dtype=latents.dtype) + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7.1 Add image embeds for IP-Adapter + added_cond_kwargs = {"image_embeds": image_embeds} if image_embeds is not None else None + + # 7.2 Create tensor stating which controlnets to keep + controlnet_keep = [] + for i in range(len(timesteps)): + keeps = [ + 1.0 - float(i / len(timesteps) < s or (i + 1) / len(timesteps) > e) + for s, e in zip(control_guidance_start, control_guidance_end) + ] + controlnet_keep.append(keeps[0] if isinstance(controlnet, ControlNetModel) else keeps) + + # 8. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + is_unet_compiled = is_compiled_module(self.unet) + is_controlnet_compiled = is_compiled_module(self.controlnet) + is_torch_higher_equal_2_1 = is_torch_version(">=", "2.1") + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # Relevant thread: + # https://dev-discuss.pytorch.org/t/cudagraphs-in-pytorch-2-0/1428 + if (is_unet_compiled and is_controlnet_compiled) and is_torch_higher_equal_2_1: + torch._inductor.cudagraph_mark_step_begin() + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # controlnet(s) inference + if guess_mode and self.do_classifier_free_guidance: + # Infer ControlNet only for the conditional batch. + control_model_input = latents + control_model_input = self.scheduler.scale_model_input(control_model_input, t) + controlnet_prompt_embeds = prompt_embeds.chunk(2)[1] + else: + control_model_input = latent_model_input + controlnet_prompt_embeds = prompt_embeds + + if isinstance(controlnet_keep[i], list): + cond_scale = [c * s for c, s in zip(controlnet_conditioning_scale, controlnet_keep[i])] + else: + controlnet_cond_scale = controlnet_conditioning_scale + if isinstance(controlnet_cond_scale, list): + controlnet_cond_scale = controlnet_cond_scale[0] + cond_scale = controlnet_cond_scale * controlnet_keep[i] + + down_block_res_samples, mid_block_res_sample = self.controlnet( + control_model_input, + t, + encoder_hidden_states=controlnet_prompt_embeds, + controlnet_cond=image, + conditioning_scale=cond_scale, + guess_mode=guess_mode, + return_dict=False, + ) + + if guess_mode and self.do_classifier_free_guidance: + # Inferred ControlNet only for the conditional batch. + # To apply the output of ControlNet to both the unconditional and conditional batches, + # add 0 to the unconditional batch to keep it unchanged. + down_block_res_samples = [torch.cat([torch.zeros_like(d), d]) for d in down_block_res_samples] + mid_block_res_sample = torch.cat([torch.zeros_like(mid_block_res_sample), mid_block_res_sample]) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + timestep_cond=timestep_cond, + cross_attention_kwargs=self.cross_attention_kwargs, + down_block_additional_residuals=down_block_res_samples, + mid_block_additional_residual=mid_block_res_sample, + added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[0] + + # perform guidance + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0] + + if callback_on_step_end is not None: + callback_kwargs = {} + for k in callback_on_step_end_tensor_inputs: + callback_kwargs[k] = locals()[k] + callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) + + latents = callback_outputs.pop("latents", latents) + prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) + negative_prompt_embeds = callback_outputs.pop("negative_prompt_embeds", negative_prompt_embeds) + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + step_idx = i // getattr(self.scheduler, "order", 1) + callback(step_idx, t, latents) + + # If we do sequential model offloading, let's offload unet and controlnet + # manually for max memory savings + if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None: + self.unet.to("cpu") + self.controlnet.to("cpu") + torch.cuda.empty_cache() + + if not output_type == "latent": + image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False, generator=generator)[ + 0 + ] + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + else: + image = latents + has_nsfw_concept = None + + if has_nsfw_concept is None: + do_denormalize = [True] * image.shape[0] + else: + do_denormalize = [not has_nsfw for has_nsfw in has_nsfw_concept] + + image = self.image_processor.postprocess(image, output_type=output_type, do_denormalize=do_denormalize) + + # Offload all models + self.maybe_free_model_hooks() + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/foleycrafter/utils/converter.py b/foleycrafter/utils/converter.py new file mode 100644 index 0000000..71f7bf1 --- /dev/null +++ b/foleycrafter/utils/converter.py @@ -0,0 +1,494 @@ +# Copy from https://github.com/happylittlecat2333/Auffusion/blob/main/converter.py +import json +import os +import random + +import librosa +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.utils.data +from librosa.filters import mel as librosa_mel_fn + +# from librosa.util import normalize +from scipy.io.wavfile import read +from torch.nn import Conv1d, ConvTranspose1d +from torch.nn.utils import remove_weight_norm, weight_norm + + +MAX_WAV_VALUE = 32768.0 + + +def load_wav(full_path): + sampling_rate, data = read(full_path) + return data, sampling_rate + + +def dynamic_range_compression(x, C=1, clip_val=1e-5): + return np.log(np.clip(x, a_min=clip_val, a_max=None) * C) + + +def dynamic_range_decompression(x, C=1): + return np.exp(x) / C + + +def dynamic_range_compression_torch(x, C=1, clip_val=1e-5): + return torch.log(torch.clamp(x, min=clip_val) * C) + + +def dynamic_range_decompression_torch(x, C=1): + return torch.exp(x) / C + + +def spectral_normalize_torch(magnitudes): + output = dynamic_range_compression_torch(magnitudes) + return output + + +def spectral_de_normalize_torch(magnitudes): + output = dynamic_range_decompression_torch(magnitudes) + return output + + +mel_basis = {} +hann_window = {} + + +def mel_spectrogram(y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False): + if torch.min(y) < -1.0: + print("min value is ", torch.min(y)) + if torch.max(y) > 1.0: + print("max value is ", torch.max(y)) + + global mel_basis, hann_window + if fmax not in mel_basis: + mel = librosa_mel_fn(sr=sampling_rate, n_fft=n_fft, n_mels=num_mels, fmin=fmin, fmax=fmax) + mel_basis[str(fmax) + "_" + str(y.device)] = torch.from_numpy(mel).float().to(y.device) + hann_window[str(y.device)] = torch.hann_window(win_size).to(y.device) + + y = torch.nn.functional.pad( + y.unsqueeze(1), (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)), mode="reflect" + ) + y = y.squeeze(1) + + # complex tensor as default, then use view_as_real for future pytorch compatibility + spec = torch.stft( + y, + n_fft, + hop_length=hop_size, + win_length=win_size, + window=hann_window[str(y.device)], + center=center, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) + spec = torch.view_as_real(spec) + spec = torch.sqrt(spec.pow(2).sum(-1) + (1e-9)) + + spec = torch.matmul(mel_basis[str(fmax) + "_" + str(y.device)], spec) + spec = spectral_normalize_torch(spec) + + return spec + + +def spectrogram(y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False): + if torch.min(y) < -1.0: + print("min value is ", torch.min(y)) + if torch.max(y) > 1.0: + print("max value is ", torch.max(y)) + + global hann_window + hann_window[str(y.device)] = torch.hann_window(win_size).to(y.device) + + y = torch.nn.functional.pad( + y.unsqueeze(1), (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)), mode="reflect" + ) + y = y.squeeze(1) + + # complex tensor as default, then use view_as_real for future pytorch compatibility + spec = torch.stft( + y, + n_fft, + hop_length=hop_size, + win_length=win_size, + window=hann_window[str(y.device)], + center=center, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) + spec = torch.view_as_real(spec) + spec = torch.sqrt(spec.pow(2).sum(-1) + (1e-9)) + + return spec + + +def normalize_spectrogram( + spectrogram: torch.Tensor, + max_value: float = 200, + min_value: float = 1e-5, + power: float = 1.0, + inverse: bool = False, +) -> torch.Tensor: + # Rescale to 0-1 + max_value = np.log(max_value) # 5.298317366548036 + min_value = np.log(min_value) # -11.512925464970229 + + assert spectrogram.max() <= max_value and spectrogram.min() >= min_value + + data = (spectrogram - min_value) / (max_value - min_value) + + # Invert + if inverse: + data = 1 - data + + # Apply the power curve + data = torch.pow(data, power) + + # 1D -> 3D + data = data.repeat(3, 1, 1) + + # Flip Y axis: image origin at the top-left corner, spectrogram origin at the bottom-left corner + data = torch.flip(data, [1]) + + return data + + +def denormalize_spectrogram( + data: torch.Tensor, + max_value: float = 200, + min_value: float = 1e-5, + power: float = 1, + inverse: bool = False, +) -> torch.Tensor: + max_value = np.log(max_value) + min_value = np.log(min_value) + + # Flip Y axis: image origin at the top-left corner, spectrogram origin at the bottom-left corner + data = torch.flip(data, [1]) + + assert len(data.shape) == 3, "Expected 3 dimensions, got {}".format(len(data.shape)) + + if data.shape[0] == 1: + data = data.repeat(3, 1, 1) + + assert data.shape[0] == 3, "Expected 3 channels, got {}".format(data.shape[0]) + data = data[0] + + # Reverse the power curve + data = torch.pow(data, 1 / power) + + # Invert + if inverse: + data = 1 - data + + # Rescale to max value + spectrogram = data * (max_value - min_value) + min_value + + return spectrogram + + +def get_mel_spectrogram_from_audio(audio, device="cpu"): + audio = audio / MAX_WAV_VALUE + audio = librosa.util.normalize(audio) * 0.95 + # print(' >>> normalize done <<< ') + + audio = torch.FloatTensor(audio) + audio = audio.unsqueeze(0) + + waveform = audio.to(device) + spec = mel_spectrogram( + waveform, + n_fft=2048, + num_mels=256, + sampling_rate=16000, + hop_size=160, + win_size=1024, + fmin=0, + fmax=8000, + center=False, + ) + return audio, spec + + +LRELU_SLOPE = 0.1 +MAX_WAV_VALUE = 32768.0 + + +class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + +def get_config(config_path): + config = json.loads(open(config_path).read()) + config = AttrDict(config) + return config + + +def init_weights(m, mean=0.0, std=0.01): + classname = m.__class__.__name__ + if classname.find("Conv") != -1: + m.weight.data.normal_(mean, std) + + +def apply_weight_norm(m): + classname = m.__class__.__name__ + if classname.find("Conv") != -1: + weight_norm(m) + + +def get_padding(kernel_size, dilation=1): + return int((kernel_size * dilation - dilation) / 2) + + +class ResBlock1(torch.nn.Module): + def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5)): + super(ResBlock1, self).__init__() + self.h = h + self.convs1 = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[0], + padding=get_padding(kernel_size, dilation[0]), + ) + ), + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[1], + padding=get_padding(kernel_size, dilation[1]), + ) + ), + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[2], + padding=get_padding(kernel_size, dilation[2]), + ) + ), + ] + ) + self.convs1.apply(init_weights) + + self.convs2 = nn.ModuleList( + [ + weight_norm( + Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1)) + ), + weight_norm( + Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1)) + ), + weight_norm( + Conv1d(channels, channels, kernel_size, 1, dilation=1, padding=get_padding(kernel_size, 1)) + ), + ] + ) + self.convs2.apply(init_weights) + + def forward(self, x): + for c1, c2 in zip(self.convs1, self.convs2): + xt = F.leaky_relu(x, LRELU_SLOPE) + xt = c1(xt) + xt = F.leaky_relu(xt, LRELU_SLOPE) + xt = c2(xt) + x = xt + x + return x + + def remove_weight_norm(self): + for l in self.convs1: + remove_weight_norm(l) + for l in self.convs2: + remove_weight_norm(l) + + +class ResBlock2(torch.nn.Module): + def __init__(self, h, channels, kernel_size=3, dilation=(1, 3)): + super(ResBlock2, self).__init__() + self.h = h + self.convs = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[0], + padding=get_padding(kernel_size, dilation[0]), + ) + ), + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + 1, + dilation=dilation[1], + padding=get_padding(kernel_size, dilation[1]), + ) + ), + ] + ) + self.convs.apply(init_weights) + + def forward(self, x): + for c in self.convs: + xt = F.leaky_relu(x, LRELU_SLOPE) + xt = c(xt) + x = xt + x + return x + + def remove_weight_norm(self): + for l in self.convs: + remove_weight_norm(l) + + +class Generator(torch.nn.Module): + def __init__(self, h): + super(Generator, self).__init__() + self.h = h + self.num_kernels = len(h.resblock_kernel_sizes) + self.num_upsamples = len(h.upsample_rates) + self.conv_pre = weight_norm( + Conv1d(h.num_mels, h.upsample_initial_channel, 7, 1, padding=3) + ) # change: 80 --> 512 + resblock = ResBlock1 if h.resblock == "1" else ResBlock2 + + self.ups = nn.ModuleList() + for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)): + if (k - u) % 2 == 0: + self.ups.append( + weight_norm( + ConvTranspose1d( + h.upsample_initial_channel // (2**i), + h.upsample_initial_channel // (2 ** (i + 1)), + k, + u, + padding=(k - u) // 2, + ) + ) + ) + else: + self.ups.append( + weight_norm( + ConvTranspose1d( + h.upsample_initial_channel // (2**i), + h.upsample_initial_channel // (2 ** (i + 1)), + k, + u, + padding=(k - u) // 2 + 1, + output_padding=1, + ) + ) + ) + + # self.ups.append(weight_norm( + # ConvTranspose1d(h.upsample_initial_channel//(2**i), h.upsample_initial_channel//(2**(i+1)), + # k, u, padding=(k-u)//2))) + + self.resblocks = nn.ModuleList() + for i in range(len(self.ups)): + ch = h.upsample_initial_channel // (2 ** (i + 1)) + for j, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)): + self.resblocks.append(resblock(h, ch, k, d)) + + self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, padding=3)) + self.ups.apply(init_weights) + self.conv_post.apply(init_weights) + + def forward(self, x): + x = self.conv_pre(x) + for i in range(self.num_upsamples): + x = F.leaky_relu(x, LRELU_SLOPE) + x = self.ups[i](x) + xs = None + for j in range(self.num_kernels): + if xs is None: + xs = self.resblocks[i * self.num_kernels + j](x) + else: + xs += self.resblocks[i * self.num_kernels + j](x) + x = xs / self.num_kernels + x = F.leaky_relu(x) + x = self.conv_post(x) + x = torch.tanh(x) + + return x + + def remove_weight_norm(self): + for l in self.ups: + remove_weight_norm(l) + for l in self.resblocks: + l.remove_weight_norm() + remove_weight_norm(self.conv_pre) + remove_weight_norm(self.conv_post) + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path, subfolder=None): + if subfolder is not None: + pretrained_model_name_or_path = os.path.join(pretrained_model_name_or_path, subfolder) + config_path = os.path.join(pretrained_model_name_or_path, "config.json") + ckpt_path = os.path.join(pretrained_model_name_or_path, "vocoder.pt") + + config = get_config(config_path) + vocoder = cls(config) + + state_dict_g = torch.load(ckpt_path) + vocoder.load_state_dict(state_dict_g["generator"]) + vocoder.eval() + vocoder.remove_weight_norm() + return vocoder + + @torch.no_grad() + def inference(self, mels, lengths=None): + self.eval() + with torch.no_grad(): + wavs = self(mels).squeeze(1) + + wavs = (wavs.cpu().numpy() * MAX_WAV_VALUE).astype("int16") + + if lengths is not None: + wavs = wavs[:, :lengths] + + return wavs + + +def normalize(images): + """ + Normalize an image array to [-1,1]. + """ + if images.min() >= 0: + return 2.0 * images - 1.0 + else: + return images + + +def pad_spec(spec, spec_length, pad_value=0, random_crop=True): # spec: [3, mel_dim, spec_len] + assert spec_length % 8 == 0, "spec_length must be divisible by 8" + if spec.shape[-1] < spec_length: + # pad spec to spec_length + spec = F.pad(spec, (0, spec_length - spec.shape[-1]), value=pad_value) + else: + # random crop + if random_crop: + start = random.randint(0, spec.shape[-1] - spec_length) + spec = spec[:, :, start : start + spec_length] + else: + spec = spec[:, :, :spec_length] + return spec diff --git a/foleycrafter/utils/spec_to_mel.py b/foleycrafter/utils/spec_to_mel.py new file mode 100644 index 0000000..93c10b0 --- /dev/null +++ b/foleycrafter/utils/spec_to_mel.py @@ -0,0 +1,399 @@ +import librosa.util as librosa_util +import numpy as np +import torch +import torch.nn.functional as F +import torchaudio +from librosa.filters import mel as librosa_mel_fn +from librosa.util import pad_center, tiny +from scipy.signal import get_window + + +# spectrogram to mel + + +class STFT(torch.nn.Module): + """adapted from Prem Seetharaman's https://github.com/pseeth/pytorch-stft""" + + def __init__(self, filter_length, hop_length, win_length, window="hann"): + super(STFT, self).__init__() + self.filter_length = filter_length + self.hop_length = hop_length + self.win_length = win_length + self.window = window + self.forward_transform = None + scale = self.filter_length / self.hop_length + fourier_basis = np.fft.fft(np.eye(self.filter_length)) + + cutoff = int((self.filter_length / 2 + 1)) + fourier_basis = np.vstack([np.real(fourier_basis[:cutoff, :]), np.imag(fourier_basis[:cutoff, :])]) + + forward_basis = torch.FloatTensor(fourier_basis[:, None, :]) + inverse_basis = torch.FloatTensor(np.linalg.pinv(scale * fourier_basis).T[:, None, :]) + + if window is not None: + assert filter_length >= win_length + # get window and zero center pad it to filter_length + fft_window = get_window(window, win_length, fftbins=True) + fft_window = pad_center(fft_window, filter_length) + fft_window = torch.from_numpy(fft_window).float() + + # window the bases + forward_basis *= fft_window + inverse_basis *= fft_window + + self.register_buffer("forward_basis", forward_basis.float()) + self.register_buffer("inverse_basis", inverse_basis.float()) + + def transform(self, input_data): + num_batches = input_data.size(0) + num_samples = input_data.size(1) + + self.num_samples = num_samples + + # similar to librosa, reflect-pad the input + input_data = input_data.view(num_batches, 1, num_samples) + input_data = F.pad( + input_data.unsqueeze(1), + (int(self.filter_length / 2), int(self.filter_length / 2), 0, 0), + mode="reflect", + ) + input_data = input_data.squeeze(1) + + forward_transform = F.conv1d( + input_data, + torch.autograd.Variable(self.forward_basis, requires_grad=False), + stride=self.hop_length, + padding=0, + ).cpu() + + cutoff = int((self.filter_length / 2) + 1) + real_part = forward_transform[:, :cutoff, :] + imag_part = forward_transform[:, cutoff:, :] + + magnitude = torch.sqrt(real_part**2 + imag_part**2) + phase = torch.autograd.Variable(torch.atan2(imag_part.data, real_part.data)) + + return magnitude, phase + + def inverse(self, magnitude, phase): + recombine_magnitude_phase = torch.cat([magnitude * torch.cos(phase), magnitude * torch.sin(phase)], dim=1) + + inverse_transform = F.conv_transpose1d( + recombine_magnitude_phase, + torch.autograd.Variable(self.inverse_basis, requires_grad=False), + stride=self.hop_length, + padding=0, + ) + + if self.window is not None: + window_sum = window_sumsquare( + self.window, + magnitude.size(-1), + hop_length=self.hop_length, + win_length=self.win_length, + n_fft=self.filter_length, + dtype=np.float32, + ) + # remove modulation effects + approx_nonzero_indices = torch.from_numpy(np.where(window_sum > tiny(window_sum))[0]) + window_sum = torch.autograd.Variable(torch.from_numpy(window_sum), requires_grad=False) + window_sum = window_sum + inverse_transform[:, :, approx_nonzero_indices] /= window_sum[approx_nonzero_indices] + + # scale by hop ratio + inverse_transform *= float(self.filter_length) / self.hop_length + + inverse_transform = inverse_transform[:, :, int(self.filter_length / 2) :] + inverse_transform = inverse_transform[:, :, : -int(self.filter_length / 2) :] + + return inverse_transform + + def forward(self, input_data): + self.magnitude, self.phase = self.transform(input_data) + reconstruction = self.inverse(self.magnitude, self.phase) + return reconstruction + + +def window_sumsquare( + window, + n_frames, + hop_length, + win_length, + n_fft, + dtype=np.float32, + norm=None, +): + """ + # from librosa 0.6 + Compute the sum-square envelope of a window function at a given hop length. + + This is used to estimate modulation effects induced by windowing + observations in short-time fourier transforms. + + Parameters + ---------- + window : string, tuple, number, callable, or list-like + Window specification, as in `get_window` + + n_frames : int > 0 + The number of analysis frames + + hop_length : int > 0 + The number of samples to advance between frames + + win_length : [optional] + The length of the window function. By default, this matches `n_fft`. + + n_fft : int > 0 + The length of each analysis frame. + + dtype : np.dtype + The data type of the output + + Returns + ------- + wss : np.ndarray, shape=`(n_fft + hop_length * (n_frames - 1))` + The sum-squared envelope of the window function + """ + if win_length is None: + win_length = n_fft + + n = n_fft + hop_length * (n_frames - 1) + x = np.zeros(n, dtype=dtype) + + # Compute the squared window at the desired length + win_sq = get_window(window, win_length, fftbins=True) + win_sq = librosa_util.normalize(win_sq, norm=norm) ** 2 + win_sq = librosa_util.pad_center(win_sq, n_fft) + + # Fill the envelope + for i in range(n_frames): + sample = i * hop_length + x[sample : min(n, sample + n_fft)] += win_sq[: max(0, min(n_fft, n - sample))] + return x + + +def griffin_lim(magnitudes, stft_fn, n_iters=30): + """ + PARAMS + ------ + magnitudes: spectrogram magnitudes + stft_fn: STFT class with transform (STFT) and inverse (ISTFT) methods + """ + + angles = np.angle(np.exp(2j * np.pi * np.random.rand(*magnitudes.size()))) + angles = angles.astype(np.float32) + angles = torch.autograd.Variable(torch.from_numpy(angles)) + signal = stft_fn.inverse(magnitudes, angles).squeeze(1) + + for i in range(n_iters): + _, angles = stft_fn.transform(signal) + signal = stft_fn.inverse(magnitudes, angles).squeeze(1) + return signal + + +def dynamic_range_compression(x, normalize_fun=torch.log, C=1, clip_val=1e-5): + """ + PARAMS + ------ + C: compression factor + """ + return normalize_fun(torch.clamp(x, min=clip_val) * C) + + +def dynamic_range_decompression(x, C=1): + """ + PARAMS + ------ + C: compression factor used to compress + """ + return torch.exp(x) / C + + +class TacotronSTFT(torch.nn.Module): + def __init__( + self, + filter_length, + hop_length, + win_length, + n_mel_channels, + sampling_rate, + mel_fmin, + mel_fmax, + ): + super(TacotronSTFT, self).__init__() + self.n_mel_channels = n_mel_channels + self.sampling_rate = sampling_rate + self.stft_fn = STFT(filter_length, hop_length, win_length) + mel_basis = librosa_mel_fn(sampling_rate, filter_length, n_mel_channels, mel_fmin, mel_fmax) + mel_basis = torch.from_numpy(mel_basis).float() + self.register_buffer("mel_basis", mel_basis) + + def spectral_normalize(self, magnitudes, normalize_fun): + output = dynamic_range_compression(magnitudes, normalize_fun) + return output + + def spectral_de_normalize(self, magnitudes): + output = dynamic_range_decompression(magnitudes) + return output + + def mel_spectrogram(self, y, normalize_fun=torch.log): + """Computes mel-spectrograms from a batch of waves + PARAMS + ------ + y: Variable(torch.FloatTensor) with shape (B, T) in range [-1, 1] + + RETURNS + ------- + mel_output: torch.FloatTensor of shape (B, n_mel_channels, T) + """ + assert torch.min(y.data) >= -1, torch.min(y.data) + assert torch.max(y.data) <= 1, torch.max(y.data) + + magnitudes, phases = self.stft_fn.transform(y) + magnitudes = magnitudes.data + mel_output = torch.matmul(self.mel_basis, magnitudes) + mel_output = self.spectral_normalize(mel_output, normalize_fun) + energy = torch.norm(magnitudes, dim=1) + + log_magnitudes = self.spectral_normalize(magnitudes, normalize_fun) + + return mel_output, log_magnitudes, energy + + +def pad_wav(waveform, segment_length): + waveform_length = waveform.shape[-1] + assert waveform_length > 100, "Waveform is too short, %s" % waveform_length + if segment_length is None or waveform_length == segment_length: + return waveform + elif waveform_length > segment_length: + return waveform[:, :segment_length] + elif waveform_length < segment_length: + temp_wav = np.zeros((1, segment_length)) + temp_wav[:, :waveform_length] = waveform + return temp_wav + + +def normalize_wav(waveform): + waveform = waveform - np.mean(waveform) + waveform = waveform / (np.max(np.abs(waveform)) + 1e-8) + return waveform * 0.5 + + +def _pad_spec(fbank, target_length=1024): + n_frames = fbank.shape[0] + p = target_length - n_frames + # cut and pad + if p > 0: + m = torch.nn.ZeroPad2d((0, 0, 0, p)) + fbank = m(fbank) + elif p < 0: + fbank = fbank[0:target_length, :] + + if fbank.size(-1) % 2 != 0: + fbank = fbank[..., :-1] + + return fbank + + +def get_mel_from_wav(audio, _stft): + audio = torch.clip(torch.FloatTensor(audio).unsqueeze(0), -1, 1) + audio = torch.autograd.Variable(audio, requires_grad=False) + melspec, log_magnitudes_stft, energy = _stft.mel_spectrogram(audio) + melspec = torch.squeeze(melspec, 0).numpy().astype(np.float32) + log_magnitudes_stft = torch.squeeze(log_magnitudes_stft, 0).numpy().astype(np.float32) + energy = torch.squeeze(energy, 0).numpy().astype(np.float32) + return melspec, log_magnitudes_stft, energy + + +def read_wav_file_io(bytes): + # waveform, sr = librosa.load(filename, sr=None, mono=True) # 4 times slower + waveform, sr = torchaudio.load(bytes, format="mp4") # Faster!!! + waveform = torchaudio.functional.resample(waveform, orig_freq=sr, new_freq=16000) + # waveform = waveform.numpy()[0, ...] + # waveform = normalize_wav(waveform) + # waveform = waveform[None, ...] + + # waveform = waveform / (np.max(np.abs(waveform)) + 1e-8) + # waveform = 0.5 * waveform + + return waveform + + +def load_audio(bytes, sample_rate=16000): + waveform, sr = torchaudio.load(bytes, format="mp4") + waveform = torchaudio.functional.resample(waveform, orig_freq=sr, new_freq=sample_rate) + return waveform + + +def read_wav_file(filename): + # waveform, sr = librosa.load(filename, sr=None, mono=True) # 4 times slower + waveform, sr = torchaudio.load(filename) # Faster!!! + waveform = torchaudio.functional.resample(waveform, orig_freq=sr, new_freq=16000) + waveform = waveform.numpy()[0, ...] + waveform = normalize_wav(waveform) + waveform = waveform[None, ...] + + waveform = waveform / np.max(np.abs(waveform)) + waveform = 0.5 * waveform + + return waveform + + +def norm_wav_tensor(waveform: torch.FloatTensor): + waveform = waveform.numpy()[0, ...] + waveform = normalize_wav(waveform) + waveform = waveform[None, ...] + waveform = waveform / (np.max(np.abs(waveform)) + 1e-8) + waveform = 0.5 * waveform + return waveform + + +def wav_to_fbank(filename, target_length=1024, fn_STFT=None): + if fn_STFT is None: + fn_STFT = TacotronSTFT( + 1024, # filter_length + 160, # hop_length + 1024, # win_length + 64, # n_mel + 16000, # sample_rate + 0, # fmin + 8000, # fmax + ) + + # mixup + waveform = read_wav_file(filename, target_length * 160) # hop size is 160 + + waveform = waveform[0, ...] + waveform = torch.FloatTensor(waveform) + + fbank, log_magnitudes_stft, energy = get_mel_from_wav(waveform, fn_STFT) + + fbank = torch.FloatTensor(fbank.T) + log_magnitudes_stft = torch.FloatTensor(log_magnitudes_stft.T) + + fbank, log_magnitudes_stft = _pad_spec(fbank, target_length), _pad_spec(log_magnitudes_stft, target_length) + + return fbank, log_magnitudes_stft, waveform + + +def wav_tensor_to_fbank(waveform, target_length=512, fn_STFT=None): + if fn_STFT is None: + fn_STFT = TacotronSTFT( + 1024, # filter_length + 160, # hop_length + 1024, # win_length + 256, # n_mel + 16000, # sample_rate + 0, # fmin + 8000, # fmax + ) # In practice used + + fbank, log_magnitudes_stft, energy = get_mel_from_wav(waveform, fn_STFT) + + fbank = torch.FloatTensor(fbank.T) + log_magnitudes_stft = torch.FloatTensor(log_magnitudes_stft.T) + + fbank, log_magnitudes_stft = _pad_spec(fbank, target_length), _pad_spec(log_magnitudes_stft, target_length) + + return fbank diff --git a/foleycrafter/utils/util.py b/foleycrafter/utils/util.py new file mode 100644 index 0000000..e01a353 --- /dev/null +++ b/foleycrafter/utils/util.py @@ -0,0 +1,1721 @@ +import glob +import io +import os +import os.path as osp +import random +import typing as T +import warnings +from dataclasses import dataclass +from enum import Enum +from typing import Union + +import decord +import imageio +import numpy as np +import pydub +import soundfile as sf +import torch +import torch.distributed as dist +import torchaudio +import torchvision +import torchvision.transforms as transforms +from einops import rearrange +from moviepy.editor import AudioFileClip, ImageSequenceClip, VideoFileClip +from PIL import Image, ImageOps +from scipy.io import wavfile +from transformers import CLIPTextModel, CLIPTokenizer + +from diffusers import ControlNetModel +from diffusers.models import AutoencoderKL +from diffusers.schedulers import DDIMScheduler, PNDMScheduler +from ..models.auffusion_unet import UNet2DConditionModel as af_UNet2DConditionModel +from ..pipelines.pipeline_controlnet import StableDiffusionControlNetPipeline + + +def zero_rank_print(s): + if (not dist.is_initialized()) or (dist.is_initialized() and dist.get_rank() == 0): + print("### " + s, flush=True) + + +def build_foleycrafter( + pretrained_model_name_or_path: str = "auffusion/auffusion-full-no-adapter", +) -> StableDiffusionControlNetPipeline: + vae = AutoencoderKL.from_pretrained(pretrained_model_name_or_path, subfolder="vae") + unet = af_UNet2DConditionModel.from_pretrained(pretrained_model_name_or_path, subfolder="unet") + scheduler = PNDMScheduler.from_pretrained(pretrained_model_name_or_path, subfolder="scheduler") + tokenizer = CLIPTokenizer.from_pretrained(pretrained_model_name_or_path, subfolder="tokenizer") + text_encoder = CLIPTextModel.from_pretrained(pretrained_model_name_or_path, subfolder="text_encoder") + + controlnet = ControlNetModel.from_unet(unet, conditioning_channels=1) + + pipe = StableDiffusionControlNetPipeline( + vae=vae, + controlnet=controlnet, + unet=unet, + scheduler=scheduler, + tokenizer=tokenizer, + text_encoder=text_encoder, + feature_extractor=None, + safety_checker=None, + requires_safety_checker=False, + ) + + return pipe + + +def save_videos_grid(videos: torch.Tensor, path: str, rescale=False, n_rows=6, fps=8): + if len(videos.shape) == 4: + videos = videos.unsqueeze(0) + videos = rearrange(videos, "b c t h w -> t b c h w") + outputs = [] + for x in videos: + x = torchvision.utils.make_grid(x, nrow=n_rows) + x = x.transpose(0, 1).transpose(1, 2).squeeze(-1) + if rescale: + x = (x + 1.0) / 2.0 # -1,1 -> 0,1 + x = torch.clamp((x * 255), 0, 255).numpy().astype(np.uint8) + outputs.append(x) + os.makedirs(os.path.dirname(path), exist_ok=True) + imageio.mimsave(path, outputs, fps=fps) + + +def save_videos_from_pil_list(videos: list, path: str, fps=7): + for i in range(len(videos)): + videos[i] = ImageOps.scale(videos[i], 255) + + imageio.mimwrite(path, videos, fps=fps) + + +def seed_everything(seed: int) -> None: + r"""Sets the seed for generating random numbers in :pytorch:`PyTorch`, + :obj:`numpy` and :python:`Python`. + + Args: + seed (int): The desired seed. + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + +def get_video_frames(video: np.ndarray, num_frames: int = 200): + video_length = video.shape[0] + video_idx = np.linspace(0, video_length - 1, num_frames, dtype=int) + video = video[video_idx, ...] + return video + + +def random_audio_video_clip( + audio: np.ndarray, video: np.ndarray, fps: float, sample_rate: int = 16000, duration: int = 5, num_frames: int = 20 +): + """ + Random sample video clips with duration + """ + video_length = video.shape[0] + audio_length = audio.shape[-1] + av_duration = int(video_length / fps) + assert av_duration >= duration, f"video duration {av_duration} is less than {duration}" + + # random sample start time + start_time = random.uniform(0, av_duration - duration) + end_time = start_time + duration + + start_idx, end_idx = start_time / av_duration, end_time / av_duration + + video_start_frame, video_end_frame = video_length * start_idx, video_length * end_idx + audio_start_frame, audio_end_frame = audio_length * start_idx, audio_length * end_idx + + # print(f"time_idx : {start_time}:{end_time}") + # print(f"video_idx: {video_start_frame}:{video_end_frame}") + # print(f"audio_idx: {audio_start_frame}:{audio_end_frame}") + + audio_idx = np.linspace(audio_start_frame, audio_end_frame, sample_rate * duration, dtype=int) + video_idx = np.linspace(video_start_frame, video_end_frame, num_frames, dtype=int) + + audio = audio[..., audio_idx] + video = video[video_idx, ...] + + return audio, video + + +def get_full_indices(reader: Union[decord.VideoReader, decord.AudioReader]) -> np.ndarray: + if isinstance(reader, decord.VideoReader): + return np.linspace(0, len(reader) - 1, len(reader), dtype=int) + elif isinstance(reader, decord.AudioReader): + return np.linspace(0, reader.shape[-1] - 1, reader.shape[-1], dtype=int) + + +def get_frames(video_path: str, onset_list, frame_nums=1024): + video = decord.VideoReader(video_path) + video_frame = len(video) + + frames_list = [] + for start, end in onset_list: + video_start = int(start / frame_nums * video_frame) + video_end = int(end / frame_nums * video_frame) + + frames_list.extend(range(video_start, video_end)) + frames = video.get_batch(frames_list).asnumpy() + return frames + + +def get_frames_in_video(video_path: str, onset_list, frame_nums=1024, audio_length_in_s=10): + # this function consider the video length + video = decord.VideoReader(video_path) + video_frame = len(video) + duration = video_frame / video.get_avg_fps() + frames_list = [] + video_onset_list = [] + for start, end in onset_list: + if int(start / frame_nums * duration) >= audio_length_in_s: + continue + video_start = int(start / audio_length_in_s * duration / frame_nums * video_frame) + if video_start >= video_frame: + continue + video_end = int(end / audio_length_in_s * duration / frame_nums * video_frame) + video_onset_list.append([int(start / audio_length_in_s * duration), int(end / audio_length_in_s * duration)]) + frames_list.extend(range(video_start, video_end)) + frames = video.get_batch(frames_list).asnumpy() + return frames, video_onset_list + + +def save_multimodal(video, audio, output_path, audio_fps: int = 16000, video_fps: int = 8, remove_audio: bool = True): + imgs = list(video) + # if audio.shape[0] == 1 or audio.shape[0] == 2: + # audio = audio.T #[len, channel] + # audio = np.repeat(audio, 2, axis=1) + output_dir = osp.dirname(output_path) + try: + wavfile.write(osp.join(output_dir, "audio.wav"), audio_fps, audio) + except Exception: + sf.write(osp.join(output_dir, "audio.wav"), audio, audio_fps) + audio_clip = AudioFileClip(osp.join(output_dir, "audio.wav")) + # audio_clip = AudioArrayClip(audio, fps=audio_fps) + video_clip = ImageSequenceClip(imgs, fps=video_fps) + video_clip = video_clip.set_audio(audio_clip) + video_clip.write_videofile(output_path, video_fps, audio=True, audio_fps=audio_fps) + if remove_audio: + os.remove(osp.join(output_dir, "audio.wav")) + return + + +def save_multimodal_by_frame(video, audio, output_path, audio_fps: int = 16000): + imgs = list(video) + # if audio.shape[0] == 1 or audio.shape[0] == 2: + # audio = audio.T #[len, channel] + # audio = np.repeat(audio, 2, axis=1) + # output_dir = osp.dirname(output_path) + output_dir = output_path + wavfile.write(osp.join(output_dir, "audio.wav"), audio_fps, audio) + # audio_clip = AudioFileClip(osp.join(output_dir, "audio.wav")) + # audio_clip = AudioArrayClip(audio, fps=audio_fps) + os.makedirs(osp.join(output_dir, "frames"), exist_ok=True) + for num, img in enumerate(imgs): + if isinstance(img, np.ndarray): + img = Image.fromarray(img.astype(np.uint8)) + img.save(osp.join(output_dir, "frames", f"{num}.jpg")) + return + + +def sanity_check(data: dict, save_path: str = "sanity_check", batch_size: int = 4, sample_rate: int = 16000): + video_path = osp.join(save_path, "video") + audio_path = osp.join(save_path, "audio") + av_path = osp.join(save_path, "av") + + video, audio, text = data["pixel_values"], data["audio"], data["text"] + video = (video / 2 + 0.5).clamp(0, 1) + + zero_rank_print(f"Saving {text} audio: {audio[0].shape} video: {video[0].shape}") + + for bsz in range(batch_size): + os.makedirs(video_path, exist_ok=True) + os.makedirs(audio_path, exist_ok=True) + os.makedirs(av_path, exist_ok=True) + # save_videos_grid(video[bsz:bsz+1,...], f"{osp.join(video_path, str(bsz) + '.mp4')}") + bsz_audio = audio[bsz, ...].permute(1, 0).cpu().numpy() + bsz_video = video_tensor_to_np(video[bsz, ...]) + sf.write(f"{osp.join(audio_path, str(bsz) + '.wav')}", bsz_audio, sample_rate) + save_multimodal(bsz_video, bsz_audio, osp.join(av_path, str(bsz) + ".mp4")) + + +def video_tensor_to_np(video: torch.Tensor, rescale: bool = True, scale: bool = False): + if scale: + video = (video / 2 + 0.5).clamp(0, 1) + # c f h w -> f h w c + if video.shape[0] == 3: + video = video.permute(1, 2, 3, 0).detach().cpu().numpy() + elif video.shape[1] == 3: + video = video.permute(0, 2, 3, 1).detach().cpu().numpy() + if rescale: + video = video * 255 + return video + + +def composite_audio_video(video: str, audio: str, path: str, video_fps: int = 7, audio_sample_rate: int = 16000): + video = decord.VideoReader(video) + audio = decord.AudioReader(audio, sample_rate=audio_sample_rate) + audio = audio.get_batch(get_full_indices(audio)).asnumpy() + video = video.get_batch(get_full_indices(video)).asnumpy() + save_multimodal(video, audio, path, audio_fps=audio_sample_rate, video_fps=video_fps) + return + + +# for video pipeline +def append_dims(x, target_dims): + """Appends dimensions to the end of a tensor until it has target_dims dimensions.""" + dims_to_append = target_dims - x.ndim + if dims_to_append < 0: + raise ValueError(f"input has {x.ndim} dims but target_dims is {target_dims}, which is less") + return x[(...,) + (None,) * dims_to_append] + + +def resize_with_antialiasing(input, size, interpolation="bicubic", align_corners=True): + h, w = input.shape[-2:] + factors = (h / size[0], w / size[1]) + + # First, we have to determine sigma + # Taken from skimage: https://github.com/scikit-image/scikit-image/blob/v0.19.2/skimage/transform/_warps.py#L171 + sigmas = ( + max((factors[0] - 1.0) / 2.0, 0.001), + max((factors[1] - 1.0) / 2.0, 0.001), + ) + + # Now kernel size. Good results are for 3 sigma, but that is kind of slow. Pillow uses 1 sigma + # https://github.com/python-pillow/Pillow/blob/master/src/libImaging/Resample.c#L206 + # But they do it in the 2 passes, which gives better results. Let's try 2 sigmas for now + ks = int(max(2.0 * 2 * sigmas[0], 3)), int(max(2.0 * 2 * sigmas[1], 3)) + + # Make sure it is odd + if (ks[0] % 2) == 0: + ks = ks[0] + 1, ks[1] + + if (ks[1] % 2) == 0: + ks = ks[0], ks[1] + 1 + + input = _gaussian_blur2d(input, ks, sigmas) + + output = torch.nn.functional.interpolate(input, size=size, mode=interpolation, align_corners=align_corners) + return output + + +def _gaussian_blur2d(input, kernel_size, sigma): + if isinstance(sigma, tuple): + sigma = torch.tensor([sigma], dtype=input.dtype) + else: + sigma = sigma.to(dtype=input.dtype) + + ky, kx = int(kernel_size[0]), int(kernel_size[1]) + bs = sigma.shape[0] + kernel_x = _gaussian(kx, sigma[:, 1].view(bs, 1)) + kernel_y = _gaussian(ky, sigma[:, 0].view(bs, 1)) + out_x = _filter2d(input, kernel_x[..., None, :]) + out = _filter2d(out_x, kernel_y[..., None]) + + return out + + +def _filter2d(input, kernel): + # prepare kernel + b, c, h, w = input.shape + tmp_kernel = kernel[:, None, ...].to(device=input.device, dtype=input.dtype) + + tmp_kernel = tmp_kernel.expand(-1, c, -1, -1) + + height, width = tmp_kernel.shape[-2:] + + padding_shape: list[int] = _compute_padding([height, width]) + input = torch.nn.functional.pad(input, padding_shape, mode="reflect") + + # kernel and input tensor reshape to align element-wise or batch-wise params + tmp_kernel = tmp_kernel.reshape(-1, 1, height, width) + input = input.view(-1, tmp_kernel.size(0), input.size(-2), input.size(-1)) + + # convolve the tensor with the kernel. + output = torch.nn.functional.conv2d(input, tmp_kernel, groups=tmp_kernel.size(0), padding=0, stride=1) + + out = output.view(b, c, h, w) + return out + + +def _gaussian(window_size: int, sigma): + if isinstance(sigma, float): + sigma = torch.tensor([[sigma]]) + + batch_size = sigma.shape[0] + + x = (torch.arange(window_size, device=sigma.device, dtype=sigma.dtype) - window_size // 2).expand(batch_size, -1) + + if window_size % 2 == 0: + x = x + 0.5 + + gauss = torch.exp(-x.pow(2.0) / (2 * sigma.pow(2.0))) + + return gauss / gauss.sum(-1, keepdim=True) + + +def _compute_padding(kernel_size): + """Compute padding tuple.""" + # 4 or 6 ints: (padding_left, padding_right,padding_top,padding_bottom) + # https://pytorch.org/docs/stable/nn.html#torch.nn.functional.pad + if len(kernel_size) < 2: + raise AssertionError(kernel_size) + computed = [k - 1 for k in kernel_size] + + # for even kernels we need to do asymmetric padding :( + out_padding = 2 * len(kernel_size) * [0] + + for i in range(len(kernel_size)): + computed_tmp = computed[-(i + 1)] + + pad_front = computed_tmp // 2 + pad_rear = computed_tmp - pad_front + + out_padding[2 * i + 0] = pad_front + out_padding[2 * i + 1] = pad_rear + + return out_padding + + +def print_gpu_memory_usage(info: str, cuda_id: int = 0): + print(f">>> {info} <<<") + reserved = torch.cuda.memory_reserved(cuda_id) / 1024**3 + used = torch.cuda.memory_allocated(cuda_id) / 1024**3 + + print("total: ", reserved, "G") + print("used: ", used, "G") + print("available: ", reserved - used, "G") + + +# use for dsp mel2spec +@dataclass(frozen=True) +class SpectrogramParams: + """ + Parameters for the conversion from audio to spectrograms to images and back. + + Includes helpers to convert to and from EXIF tags, allowing these parameters to be stored + within spectrogram images. + + To understand what these parameters do and to customize them, read `spectrogram_converter.py` + and the linked torchaudio documentation. + """ + + # Whether the audio is stereo or mono + stereo: bool = False + + # FFT parameters + sample_rate: int = 44100 + step_size_ms: int = 10 + window_duration_ms: int = 100 + padded_duration_ms: int = 400 + + # Mel scale parameters + num_frequencies: int = 200 + # TODO(hayk): Set these to [20, 20000] for newer models + min_frequency: int = 0 + max_frequency: int = 10000 + mel_scale_norm: T.Optional[str] = None + mel_scale_type: str = "htk" + max_mel_iters: int = 200 + + # Griffin Lim parameters + num_griffin_lim_iters: int = 32 + + # Image parameterization + power_for_image: float = 0.25 + + class ExifTags(Enum): + """ + Custom EXIF tags for the spectrogram image. + """ + + SAMPLE_RATE = 11000 + STEREO = 11005 + STEP_SIZE_MS = 11010 + WINDOW_DURATION_MS = 11020 + PADDED_DURATION_MS = 11030 + + NUM_FREQUENCIES = 11040 + MIN_FREQUENCY = 11050 + MAX_FREQUENCY = 11060 + + POWER_FOR_IMAGE = 11070 + MAX_VALUE = 11080 + + @property + def n_fft(self) -> int: + """ + The number of samples in each STFT window, with padding. + """ + return int(self.padded_duration_ms / 1000.0 * self.sample_rate) + + @property + def win_length(self) -> int: + """ + The number of samples in each STFT window. + """ + return int(self.window_duration_ms / 1000.0 * self.sample_rate) + + @property + def hop_length(self) -> int: + """ + The number of samples between each STFT window. + """ + return int(self.step_size_ms / 1000.0 * self.sample_rate) + + def to_exif(self) -> T.Dict[int, T.Any]: + """ + Return a dictionary of EXIF tags for the current values. + """ + return { + self.ExifTags.SAMPLE_RATE.value: self.sample_rate, + self.ExifTags.STEREO.value: self.stereo, + self.ExifTags.STEP_SIZE_MS.value: self.step_size_ms, + self.ExifTags.WINDOW_DURATION_MS.value: self.window_duration_ms, + self.ExifTags.PADDED_DURATION_MS.value: self.padded_duration_ms, + self.ExifTags.NUM_FREQUENCIES.value: self.num_frequencies, + self.ExifTags.MIN_FREQUENCY.value: self.min_frequency, + self.ExifTags.MAX_FREQUENCY.value: self.max_frequency, + self.ExifTags.POWER_FOR_IMAGE.value: float(self.power_for_image), + } + + +class SpectrogramImageConverter: + """ + Convert between spectrogram images and audio segments. + + This is a wrapper around SpectrogramConverter that additionally converts from spectrograms + to images and back. The real audio processing lives in SpectrogramConverter. + """ + + def __init__(self, params: SpectrogramParams, device: str = "cuda"): + self.p = params + self.device = device + self.converter = SpectrogramConverter(params=params, device=device) + + def spectrogram_image_from_audio( + self, + segment: pydub.AudioSegment, + ) -> Image.Image: + """ + Compute a spectrogram image from an audio segment. + + Args: + segment: Audio segment to convert + + Returns: + Spectrogram image (in pillow format) + """ + assert int(segment.frame_rate) == self.p.sample_rate, "Sample rate mismatch" + + if self.p.stereo: + if segment.channels == 1: + print("WARNING: Mono audio but stereo=True, cloning channel") + segment = segment.set_channels(2) + elif segment.channels > 2: + print("WARNING: Multi channel audio, reducing to stereo") + segment = segment.set_channels(2) + else: + if segment.channels > 1: + print("WARNING: Stereo audio but stereo=False, setting to mono") + segment = segment.set_channels(1) + + spectrogram = self.converter.spectrogram_from_audio(segment) + + image = image_from_spectrogram( + spectrogram, + power=self.p.power_for_image, + ) + + # Store conversion params in exif metadata of the image + exif_data = self.p.to_exif() + exif_data[SpectrogramParams.ExifTags.MAX_VALUE.value] = float(np.max(spectrogram)) + exif = image.getexif() + exif.update(exif_data.items()) + + return image + + def audio_from_spectrogram_image( + self, + image: Image.Image, + apply_filters: bool = True, + max_value: float = 30e6, + ) -> pydub.AudioSegment: + """ + Reconstruct an audio segment from a spectrogram image. + + Args: + image: Spectrogram image (in pillow format) + apply_filters: Apply post-processing to improve the reconstructed audio + max_value: Scaled max amplitude of the spectrogram. Shouldn't matter. + """ + spectrogram = spectrogram_from_image( + image, + max_value=max_value, + power=self.p.power_for_image, + stereo=self.p.stereo, + ) + + segment = self.converter.audio_from_spectrogram( + spectrogram, + apply_filters=apply_filters, + ) + + return segment + + +def image_from_spectrogram(spectrogram: np.ndarray, power: float = 0.25) -> Image.Image: + """ + Compute a spectrogram image from a spectrogram magnitude array. + + This is the inverse of spectrogram_from_image, except for discretization error from + quantizing to uint8. + + Args: + spectrogram: (channels, frequency, time) + power: A power curve to apply to the spectrogram to preserve contrast + + Returns: + image: (frequency, time, channels) + """ + # Rescale to 0-1 + max_value = np.max(spectrogram) + data = spectrogram / max_value + + # Apply the power curve + data = np.power(data, power) + + # Rescale to 0-255 + data = data * 255 + + # Invert + data = 255 - data + + # Convert to uint8 + data = data.astype(np.uint8) + + # Munge channels into a PIL image + if data.shape[0] == 1: + # TODO(hayk): Do we want to write single channel to disk instead? + image = Image.fromarray(data[0], mode="L").convert("RGB") + elif data.shape[0] == 2: + data = np.array([np.zeros_like(data[0]), data[0], data[1]]).transpose(1, 2, 0) + image = Image.fromarray(data, mode="RGB") + else: + raise NotImplementedError(f"Unsupported number of channels: {data.shape[0]}") + + # Flip Y + image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + + return image + + +def spectrogram_from_image( + image: Image.Image, + power: float = 0.25, + stereo: bool = False, + max_value: float = 30e6, +) -> np.ndarray: + """ + Compute a spectrogram magnitude array from a spectrogram image. + + This is the inverse of image_from_spectrogram, except for discretization error from + quantizing to uint8. + + Args: + image: (frequency, time, channels) + power: The power curve applied to the spectrogram + stereo: Whether the spectrogram encodes stereo data + max_value: The max value of the original spectrogram. In practice doesn't matter. + + Returns: + spectrogram: (channels, frequency, time) + """ + # Convert to RGB if single channel + if image.mode in ("P", "L"): + image = image.convert("RGB") + + # Flip Y + image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + + # Munge channels into a numpy array of (channels, frequency, time) + data = np.array(image).transpose(2, 0, 1) + if stereo: + # Take the G and B channels as done in image_from_spectrogram + data = data[[1, 2], :, :] + else: + data = data[0:1, :, :] + + # Convert to floats + data = data.astype(np.float32) + + # Invert + data = 255 - data + + # Rescale to 0-1 + data = data / 255 + + # Reverse the power curve + data = np.power(data, 1 / power) + + # Rescale to max value + data = data * max_value + + return data + + +class SpectrogramConverter: + """ + Convert between audio segments and spectrogram tensors using torchaudio. + + In this class a "spectrogram" is defined as a (batch, time, frequency) tensor with float values + that represent the amplitude of the frequency at that time bucket (in the frequency domain). + Frequencies are given in the perceptul Mel scale defined by the params. A more specific term + used in some functions is "mel amplitudes". + + The spectrogram computed from `spectrogram_from_audio` is complex valued, but it only + returns the amplitude, because the phase is chaotic and hard to learn. The function + `audio_from_spectrogram` is an approximate inverse of `spectrogram_from_audio`, which + approximates the phase information using the Griffin-Lim algorithm. + + Each channel in the audio is treated independently, and the spectrogram has a batch dimension + equal to the number of channels in the input audio segment. + + Both the Griffin Lim algorithm and the Mel scaling process are lossy. + + For more information, see https://pytorch.org/audio/stable/transforms.html + """ + + def __init__(self, params: SpectrogramParams, device: str = "cuda"): + self.p = params + + self.device = check_device(device) + + if device.lower().startswith("mps"): + warnings.warn( + "WARNING: MPS does not support audio operations, falling back to CPU for them", + stacklevel=2, + ) + self.device = "cpu" + + # https://pytorch.org/audio/stable/generated/torchaudio.transforms.Spectrogram.html + self.spectrogram_func = torchaudio.transforms.Spectrogram( + n_fft=params.n_fft, + hop_length=params.hop_length, + win_length=params.win_length, + pad=0, + window_fn=torch.hann_window, + power=None, + normalized=False, + wkwargs=None, + center=True, + pad_mode="reflect", + onesided=True, + ).to(self.device) + + # https://pytorch.org/audio/stable/generated/torchaudio.transforms.GriffinLim.html + self.inverse_spectrogram_func = torchaudio.transforms.GriffinLim( + n_fft=params.n_fft, + n_iter=params.num_griffin_lim_iters, + win_length=params.win_length, + hop_length=params.hop_length, + window_fn=torch.hann_window, + power=1.0, + wkwargs=None, + momentum=0.99, + length=None, + rand_init=True, + ).to(self.device) + + # https://pytorch.org/audio/stable/generated/torchaudio.transforms.MelScale.html + self.mel_scaler = torchaudio.transforms.MelScale( + n_mels=params.num_frequencies, + sample_rate=params.sample_rate, + f_min=params.min_frequency, + f_max=params.max_frequency, + n_stft=params.n_fft // 2 + 1, + norm=params.mel_scale_norm, + mel_scale=params.mel_scale_type, + ).to(self.device) + + # https://pytorch.org/audio/stable/generated/torchaudio.transforms.InverseMelScale.html + self.inverse_mel_scaler = torchaudio.transforms.InverseMelScale( + n_stft=params.n_fft // 2 + 1, + n_mels=params.num_frequencies, + sample_rate=params.sample_rate, + f_min=params.min_frequency, + f_max=params.max_frequency, + # max_iter=params.max_mel_iters, # for higher version of torchaudio + # tolerance_loss=1e-5, # for higher version of torchaudio + # tolerance_change=1e-8, # for higher version of torchaudio + # sgdargs=None, # for higher version of torchaudio + norm=params.mel_scale_norm, + mel_scale=params.mel_scale_type, + ).to(self.device) + + def spectrogram_from_audio( + self, + audio: pydub.AudioSegment, + ) -> np.ndarray: + """ + Compute a spectrogram from an audio segment. + + Args: + audio: Audio segment which must match the sample rate of the params + + Returns: + spectrogram: (channel, frequency, time) + """ + assert int(audio.frame_rate) == self.p.sample_rate, "Audio sample rate must match params" + + # Get the samples as a numpy array in (batch, samples) shape + waveform = np.array([c.get_array_of_samples() for c in audio.split_to_mono()]) + + # Convert to floats if necessary + if waveform.dtype != np.float32: + waveform = waveform.astype(np.float32) + + waveform_tensor = torch.from_numpy(waveform).to(self.device) + amplitudes_mel = self.mel_amplitudes_from_waveform(waveform_tensor) + return amplitudes_mel.cpu().numpy() + + def audio_from_spectrogram( + self, + spectrogram: np.ndarray, + apply_filters: bool = True, + ) -> pydub.AudioSegment: + """ + Reconstruct an audio segment from a spectrogram. + + Args: + spectrogram: (batch, frequency, time) + apply_filters: Post-process with normalization and compression + + Returns: + audio: Audio segment with channels equal to the batch dimension + """ + # Move to device + amplitudes_mel = torch.from_numpy(spectrogram).to(self.device) + + # Reconstruct the waveform + waveform = self.waveform_from_mel_amplitudes(amplitudes_mel) + + # Convert to audio segment + segment = audio_from_waveform( + samples=waveform.cpu().numpy(), + sample_rate=self.p.sample_rate, + # Normalize the waveform to the range [-1, 1] + normalize=True, + ) + + # Optionally apply post-processing filters + if apply_filters: + segment = apply_filters_func( + segment, + compression=False, + ) + + return segment + + def mel_amplitudes_from_waveform( + self, + waveform: torch.Tensor, + ) -> torch.Tensor: + """ + Torch-only function to compute Mel-scale amplitudes from a waveform. + + Args: + waveform: (batch, samples) + + Returns: + amplitudes_mel: (batch, frequency, time) + """ + # Compute the complex-valued spectrogram + spectrogram_complex = self.spectrogram_func(waveform) + + # Take the magnitude + amplitudes = torch.abs(spectrogram_complex) + + # Convert to mel scale + return self.mel_scaler(amplitudes) + + def waveform_from_mel_amplitudes( + self, + amplitudes_mel: torch.Tensor, + ) -> torch.Tensor: + """ + Torch-only function to approximately reconstruct a waveform from Mel-scale amplitudes. + + Args: + amplitudes_mel: (batch, frequency, time) + + Returns: + waveform: (batch, samples) + """ + # Convert from mel scale to linear + amplitudes_linear = self.inverse_mel_scaler(amplitudes_mel) + + # Run the approximate algorithm to compute the phase and recover the waveform + return self.inverse_spectrogram_func(amplitudes_linear) + + +def check_device(device: str, backup: str = "cpu") -> str: + """ + Check that the device is valid and available. If not, + """ + cuda_not_found = device.lower().startswith("cuda") and not torch.cuda.is_available() + mps_not_found = device.lower().startswith("mps") and not torch.backends.mps.is_available() + + if cuda_not_found or mps_not_found: + warnings.warn(f"WARNING: {device} is not available, using {backup} instead.", stacklevel=3) + return backup + + return device + + +def audio_from_waveform(samples: np.ndarray, sample_rate: int, normalize: bool = False) -> pydub.AudioSegment: + """ + Convert a numpy array of samples of a waveform to an audio segment. + + Args: + samples: (channels, samples) array + """ + # Normalize volume to fit in int16 + if normalize: + samples *= np.iinfo(np.int16).max / np.max(np.abs(samples)) + + # Transpose and convert to int16 + samples = samples.transpose(1, 0) + samples = samples.astype(np.int16) + + # Write to the bytes of a WAV file + wav_bytes = io.BytesIO() + wavfile.write(wav_bytes, sample_rate, samples) + wav_bytes.seek(0) + + # Read into pydub + return pydub.AudioSegment.from_wav(wav_bytes) + + +def apply_filters_func(segment: pydub.AudioSegment, compression: bool = False) -> pydub.AudioSegment: + """ + Apply post-processing filters to the audio segment to compress it and + keep at a -10 dBFS level. + """ + # TODO(hayk): Come up with a principled strategy for these filters and experiment end-to-end. + # TODO(hayk): Is this going to make audio unbalanced between sequential clips? + + if compression: + segment = pydub.effects.normalize( + segment, + headroom=0.1, + ) + + segment = segment.apply_gain(-10 - segment.dBFS) + + # TODO(hayk): This is quite slow, ~1.7 seconds on a beefy CPU + segment = pydub.effects.compress_dynamic_range( + segment, + threshold=-20.0, + ratio=4.0, + attack=5.0, + release=50.0, + ) + + desired_db = -12 + segment = segment.apply_gain(desired_db - segment.dBFS) + + segment = pydub.effects.normalize( + segment, + headroom=0.1, + ) + + return segment + + +def shave_segments(path, n_shave_prefix_segments=1): + """ + Removes segments. Positive values shave the first segments, negative shave the last segments. + """ + if n_shave_prefix_segments >= 0: + return ".".join(path.split(".")[n_shave_prefix_segments:]) + else: + return ".".join(path.split(".")[:n_shave_prefix_segments]) + + +def renew_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item.replace("in_layers.0", "norm1") + new_item = new_item.replace("in_layers.2", "conv1") + + new_item = new_item.replace("out_layers.0", "norm2") + new_item = new_item.replace("out_layers.3", "conv2") + + new_item = new_item.replace("emb_layers.1", "time_emb_proj") + new_item = new_item.replace("skip_connection", "conv_shortcut") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("nin_shortcut", "conv_shortcut") + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + # new_item = new_item.replace('norm.weight', 'group_norm.weight') + # new_item = new_item.replace('norm.bias', 'group_norm.bias') + + # new_item = new_item.replace('proj_out.weight', 'proj_attn.weight') + # new_item = new_item.replace('proj_out.bias', 'proj_attn.bias') + + # new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("norm.weight", "group_norm.weight") + new_item = new_item.replace("norm.bias", "group_norm.bias") + + new_item = new_item.replace("q.weight", "to_q.weight") + new_item = new_item.replace("q.bias", "to_q.bias") + + new_item = new_item.replace("k.weight", "to_k.weight") + new_item = new_item.replace("k.bias", "to_k.bias") + + new_item = new_item.replace("v.weight", "to_v.weight") + new_item = new_item.replace("v.bias", "to_v.bias") + + new_item = new_item.replace("proj_out.weight", "to_out.0.weight") + new_item = new_item.replace("proj_out.bias", "to_out.0.bias") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + return mapping + + +def assign_to_checkpoint( + paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None +): + """ + This does the final conversion step: take locally converted weights and apply a global renaming to them. It splits + attention layers, and takes into account additional replacements that may arise. + + Assigns the weights to the new checkpoint. + """ + assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." + + # Splits the attention layers into three variables. + if attention_paths_to_split is not None: + for path, path_map in attention_paths_to_split.items(): + old_tensor = old_checkpoint[path] + channels = old_tensor.shape[0] // 3 + + target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) + + num_heads = old_tensor.shape[0] // config["num_head_channels"] // 3 + + old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) + query, key, value = old_tensor.split(channels // num_heads, dim=1) + + checkpoint[path_map["query"]] = query.reshape(target_shape) + checkpoint[path_map["key"]] = key.reshape(target_shape) + checkpoint[path_map["value"]] = value.reshape(target_shape) + + for path in paths: + new_path = path["new"] + + # These have already been assigned + if attention_paths_to_split is not None and new_path in attention_paths_to_split: + continue + + # Global renaming happens here + new_path = new_path.replace("middle_block.0", "mid_block.resnets.0") + new_path = new_path.replace("middle_block.1", "mid_block.attentions.0") + new_path = new_path.replace("middle_block.2", "mid_block.resnets.1") + + if additional_replacements is not None: + for replacement in additional_replacements: + new_path = new_path.replace(replacement["old"], replacement["new"]) + + # proj_attn.weight has to be converted from conv 1D to linear + if "proj_attn.weight" in new_path: + checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0] + elif "to_out.0.weight" in new_path: + checkpoint[new_path] = old_checkpoint[path["old"]].squeeze() + elif any(qkv in new_path for qkv in ["to_q", "to_k", "to_v"]): + checkpoint[new_path] = old_checkpoint[path["old"]].squeeze() + else: + checkpoint[new_path] = old_checkpoint[path["old"]] + + +def conv_attn_to_linear(checkpoint): + keys = list(checkpoint.keys()) + attn_keys = ["query.weight", "key.weight", "value.weight"] + for key in keys: + if ".".join(key.split(".")[-2:]) in attn_keys: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0, 0] + elif "proj_attn.weight" in key: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0] + + +def create_unet_diffusers_config(original_config, image_size: int, controlnet=False): + """ + Creates a config for the diffusers based on the config of the LDM model. + """ + if controlnet: + unet_params = original_config.model.params.control_stage_config.params + else: + unet_params = original_config.model.params.unet_config.params + + vae_params = original_config.model.params.first_stage_config.params.ddconfig + + block_out_channels = [unet_params.model_channels * mult for mult in unet_params.channel_mult] + + down_block_types = [] + resolution = 1 + for i in range(len(block_out_channels)): + block_type = "CrossAttnDownBlock2D" if resolution in unet_params.attention_resolutions else "DownBlock2D" + down_block_types.append(block_type) + if i != len(block_out_channels) - 1: + resolution *= 2 + + up_block_types = [] + for i in range(len(block_out_channels)): + block_type = "CrossAttnUpBlock2D" if resolution in unet_params.attention_resolutions else "UpBlock2D" + up_block_types.append(block_type) + resolution //= 2 + + vae_scale_factor = 2 ** (len(vae_params.ch_mult) - 1) + + head_dim = unet_params.num_heads if "num_heads" in unet_params else None + use_linear_projection = ( + unet_params.use_linear_in_transformer if "use_linear_in_transformer" in unet_params else False + ) + if use_linear_projection: + # stable diffusion 2-base-512 and 2-768 + if head_dim is None: + head_dim = [5, 10, 20, 20] + + class_embed_type = None + projection_class_embeddings_input_dim = None + + if "num_classes" in unet_params: + if unet_params.num_classes == "sequential": + class_embed_type = "projection" + assert "adm_in_channels" in unet_params + projection_class_embeddings_input_dim = unet_params.adm_in_channels + else: + raise NotImplementedError(f"Unknown conditional unet num_classes config: {unet_params.num_classes}") + + config = { + "sample_size": image_size // vae_scale_factor, + "in_channels": unet_params.in_channels, + "down_block_types": tuple(down_block_types), + "block_out_channels": tuple(block_out_channels), + "layers_per_block": unet_params.num_res_blocks, + "cross_attention_dim": unet_params.context_dim, + "attention_head_dim": head_dim, + "use_linear_projection": use_linear_projection, + "class_embed_type": class_embed_type, + "projection_class_embeddings_input_dim": projection_class_embeddings_input_dim, + } + + if not controlnet: + config["out_channels"] = unet_params.out_channels + config["up_block_types"] = tuple(up_block_types) + + return config + + +def create_vae_diffusers_config(original_config, image_size: int): + """ + Creates a config for the diffusers based on the config of the LDM model. + """ + vae_params = original_config.model.params.first_stage_config.params.ddconfig + _ = original_config.model.params.first_stage_config.params.embed_dim + + block_out_channels = [vae_params.ch * mult for mult in vae_params.ch_mult] + down_block_types = ["DownEncoderBlock2D"] * len(block_out_channels) + up_block_types = ["UpDecoderBlock2D"] * len(block_out_channels) + + config = { + "sample_size": image_size, + "in_channels": vae_params.in_channels, + "out_channels": vae_params.out_ch, + "down_block_types": tuple(down_block_types), + "up_block_types": tuple(up_block_types), + "block_out_channels": tuple(block_out_channels), + "latent_channels": vae_params.z_channels, + "layers_per_block": vae_params.num_res_blocks, + } + return config + + +def create_diffusers_schedular(original_config): + schedular = DDIMScheduler( + num_train_timesteps=original_config.model.params.timesteps, + beta_start=original_config.model.params.linear_start, + beta_end=original_config.model.params.linear_end, + beta_schedule="scaled_linear", + ) + return schedular + + +def convert_ldm_unet_checkpoint(checkpoint, config, path=None, extract_ema=False, controlnet=False): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + + # extract state_dict for UNet + unet_state_dict = {} + keys = list(checkpoint.keys()) + + if controlnet: + unet_key = "control_model." + else: + unet_key = "model.diffusion_model." + + # at least a 100 parameters have to start with `model_ema` in order for the checkpoint to be EMA + if sum(k.startswith("model_ema") for k in keys) > 100 and extract_ema: + print(f"Checkpoint {path} has both EMA and non-EMA weights.") + print( + "In this conversion only the EMA weights are extracted. If you want to instead extract the non-EMA" + " weights (useful to continue fine-tuning), please make sure to remove the `--extract_ema` flag." + ) + for key in keys: + if key.startswith("model.diffusion_model"): + flat_ema_key = "model_ema." + "".join(key.split(".")[1:]) + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(flat_ema_key) + else: + if sum(k.startswith("model_ema") for k in keys) > 100: + print( + "In this conversion only the non-EMA weights are extracted. If you want to instead extract the EMA" + " weights (usually better for inference), please make sure to add the `--extract_ema` flag." + ) + + for key in keys: + if key.startswith(unet_key): + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(key) + + new_checkpoint = {} + + new_checkpoint["time_embedding.linear_1.weight"] = unet_state_dict["time_embed.0.weight"] + new_checkpoint["time_embedding.linear_1.bias"] = unet_state_dict["time_embed.0.bias"] + new_checkpoint["time_embedding.linear_2.weight"] = unet_state_dict["time_embed.2.weight"] + new_checkpoint["time_embedding.linear_2.bias"] = unet_state_dict["time_embed.2.bias"] + + if config["class_embed_type"] is None: + # No parameters to port + ... + elif config["class_embed_type"] == "timestep" or config["class_embed_type"] == "projection": + new_checkpoint["class_embedding.linear_1.weight"] = unet_state_dict["label_emb.0.0.weight"] + new_checkpoint["class_embedding.linear_1.bias"] = unet_state_dict["label_emb.0.0.bias"] + new_checkpoint["class_embedding.linear_2.weight"] = unet_state_dict["label_emb.0.2.weight"] + new_checkpoint["class_embedding.linear_2.bias"] = unet_state_dict["label_emb.0.2.bias"] + else: + raise NotImplementedError(f"Not implemented `class_embed_type`: {config['class_embed_type']}") + + new_checkpoint["conv_in.weight"] = unet_state_dict["input_blocks.0.0.weight"] + new_checkpoint["conv_in.bias"] = unet_state_dict["input_blocks.0.0.bias"] + + if not controlnet: + new_checkpoint["conv_norm_out.weight"] = unet_state_dict["out.0.weight"] + new_checkpoint["conv_norm_out.bias"] = unet_state_dict["out.0.bias"] + new_checkpoint["conv_out.weight"] = unet_state_dict["out.2.weight"] + new_checkpoint["conv_out.bias"] = unet_state_dict["out.2.bias"] + + # Retrieves the keys for the input blocks only + num_input_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "input_blocks" in layer}) + input_blocks = { + layer_id: [key for key in unet_state_dict if f"input_blocks.{layer_id}" in key] + for layer_id in range(num_input_blocks) + } + + # Retrieves the keys for the middle blocks only + num_middle_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "middle_block" in layer}) + middle_blocks = { + layer_id: [key for key in unet_state_dict if f"middle_block.{layer_id}" in key] + for layer_id in range(num_middle_blocks) + } + + # Retrieves the keys for the output blocks only + num_output_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "output_blocks" in layer}) + output_blocks = { + layer_id: [key for key in unet_state_dict if f"output_blocks.{layer_id}" in key] + for layer_id in range(num_output_blocks) + } + + for i in range(1, num_input_blocks): + block_id = (i - 1) // (config["layers_per_block"] + 1) + layer_in_block_id = (i - 1) % (config["layers_per_block"] + 1) + + resnets = [ + key for key in input_blocks[i] if f"input_blocks.{i}.0" in key and f"input_blocks.{i}.0.op" not in key + ] + attentions = [key for key in input_blocks[i] if f"input_blocks.{i}.1" in key] + + if f"input_blocks.{i}.0.op.weight" in unet_state_dict: + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.weight"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.weight" + ) + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.bias"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.bias" + ) + + paths = renew_resnet_paths(resnets) + meta_path = {"old": f"input_blocks.{i}.0", "new": f"down_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = {"old": f"input_blocks.{i}.1", "new": f"down_blocks.{block_id}.attentions.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + resnet_0 = middle_blocks[0] + attentions = middle_blocks[1] + resnet_1 = middle_blocks[2] + + resnet_0_paths = renew_resnet_paths(resnet_0) + assign_to_checkpoint(resnet_0_paths, new_checkpoint, unet_state_dict, config=config) + + resnet_1_paths = renew_resnet_paths(resnet_1) + assign_to_checkpoint(resnet_1_paths, new_checkpoint, unet_state_dict, config=config) + + attentions_paths = renew_attention_paths(attentions) + meta_path = {"old": "middle_block.1", "new": "mid_block.attentions.0"} + assign_to_checkpoint( + attentions_paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + for i in range(num_output_blocks): + block_id = i // (config["layers_per_block"] + 1) + layer_in_block_id = i % (config["layers_per_block"] + 1) + output_block_layers = [shave_segments(name, 2) for name in output_blocks[i]] + output_block_list = {} + + for layer in output_block_layers: + layer_id, layer_name = layer.split(".")[0], shave_segments(layer, 1) + if layer_id in output_block_list: + output_block_list[layer_id].append(layer_name) + else: + output_block_list[layer_id] = [layer_name] + + if len(output_block_list) > 1: + resnets = [key for key in output_blocks[i] if f"output_blocks.{i}.0" in key] + attentions = [key for key in output_blocks[i] if f"output_blocks.{i}.1" in key] + + resnet_0_paths = renew_resnet_paths(resnets) + paths = renew_resnet_paths(resnets) + + meta_path = {"old": f"output_blocks.{i}.0", "new": f"up_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + output_block_list = {k: sorted(v) for k, v in output_block_list.items()} + if ["conv.bias", "conv.weight"] in output_block_list.values(): + index = list(output_block_list.values()).index(["conv.bias", "conv.weight"]) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.weight" + ] + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.bias" + ] + + # Clear attentions as they have been attributed above. + if len(attentions) == 2: + attentions = [] + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = { + "old": f"output_blocks.{i}.1", + "new": f"up_blocks.{block_id}.attentions.{layer_in_block_id}", + } + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + else: + resnet_0_paths = renew_resnet_paths(output_block_layers, n_shave_prefix_segments=1) + for path in resnet_0_paths: + old_path = ".".join(["output_blocks", str(i), path["old"]]) + new_path = ".".join(["up_blocks", str(block_id), "resnets", str(layer_in_block_id), path["new"]]) + + new_checkpoint[new_path] = unet_state_dict[old_path] + + if controlnet: + # conditioning embedding + + orig_index = 0 + + new_checkpoint["controlnet_cond_embedding.conv_in.weight"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.weight" + ) + new_checkpoint["controlnet_cond_embedding.conv_in.bias"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.bias" + ) + + orig_index += 2 + + diffusers_index = 0 + + while diffusers_index < 6: + new_checkpoint[f"controlnet_cond_embedding.blocks.{diffusers_index}.weight"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.weight" + ) + new_checkpoint[f"controlnet_cond_embedding.blocks.{diffusers_index}.bias"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.bias" + ) + diffusers_index += 1 + orig_index += 2 + + new_checkpoint["controlnet_cond_embedding.conv_out.weight"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.weight" + ) + new_checkpoint["controlnet_cond_embedding.conv_out.bias"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.bias" + ) + + # down blocks + for i in range(num_input_blocks): + new_checkpoint[f"controlnet_down_blocks.{i}.weight"] = unet_state_dict.pop(f"zero_convs.{i}.0.weight") + new_checkpoint[f"controlnet_down_blocks.{i}.bias"] = unet_state_dict.pop(f"zero_convs.{i}.0.bias") + + # mid block + new_checkpoint["controlnet_mid_block.weight"] = unet_state_dict.pop("middle_block_out.0.weight") + new_checkpoint["controlnet_mid_block.bias"] = unet_state_dict.pop("middle_block_out.0.bias") + + return new_checkpoint + + +def convert_ldm_vae_checkpoint(checkpoint, config, only_decoder=False, only_encoder=False): + # extract state dict for VAE + vae_state_dict = {} + vae_key = "first_stage_model." + keys = list(checkpoint.keys()) + for key in keys: + if key.startswith(vae_key): + vae_state_dict[key.replace(vae_key, "")] = checkpoint.get(key) + + new_checkpoint = {} + + new_checkpoint["encoder.conv_in.weight"] = vae_state_dict["encoder.conv_in.weight"] + new_checkpoint["encoder.conv_in.bias"] = vae_state_dict["encoder.conv_in.bias"] + new_checkpoint["encoder.conv_out.weight"] = vae_state_dict["encoder.conv_out.weight"] + new_checkpoint["encoder.conv_out.bias"] = vae_state_dict["encoder.conv_out.bias"] + new_checkpoint["encoder.conv_norm_out.weight"] = vae_state_dict["encoder.norm_out.weight"] + new_checkpoint["encoder.conv_norm_out.bias"] = vae_state_dict["encoder.norm_out.bias"] + + new_checkpoint["decoder.conv_in.weight"] = vae_state_dict["decoder.conv_in.weight"] + new_checkpoint["decoder.conv_in.bias"] = vae_state_dict["decoder.conv_in.bias"] + new_checkpoint["decoder.conv_out.weight"] = vae_state_dict["decoder.conv_out.weight"] + new_checkpoint["decoder.conv_out.bias"] = vae_state_dict["decoder.conv_out.bias"] + new_checkpoint["decoder.conv_norm_out.weight"] = vae_state_dict["decoder.norm_out.weight"] + new_checkpoint["decoder.conv_norm_out.bias"] = vae_state_dict["decoder.norm_out.bias"] + + new_checkpoint["quant_conv.weight"] = vae_state_dict["quant_conv.weight"] + new_checkpoint["quant_conv.bias"] = vae_state_dict["quant_conv.bias"] + new_checkpoint["post_quant_conv.weight"] = vae_state_dict["post_quant_conv.weight"] + new_checkpoint["post_quant_conv.bias"] = vae_state_dict["post_quant_conv.bias"] + + # Retrieves the keys for the encoder down blocks only + num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "encoder.down" in layer}) + down_blocks = { + layer_id: [key for key in vae_state_dict if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + # Retrieves the keys for the decoder up blocks only + num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "decoder.up" in layer}) + up_blocks = { + layer_id: [key for key in vae_state_dict if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks) + } + + for i in range(num_down_blocks): + resnets = [key for key in down_blocks[i] if f"down.{i}" in key and f"down.{i}.downsample" not in key] + + if f"encoder.down.{i}.downsample.conv.weight" in vae_state_dict: + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.weight" + ) + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.bias" + ) + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"down.{i}.block", "new": f"down_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "encoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"encoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "encoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + resnets = [ + key for key in up_blocks[block_id] if f"up.{block_id}" in key and f"up.{block_id}.upsample" not in key + ] + + if f"decoder.up.{block_id}.upsample.conv.weight" in vae_state_dict: + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.weight"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.weight" + ] + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.bias"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.bias" + ] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"up.{block_id}.block", "new": f"up_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "decoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"decoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "decoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + + if only_decoder: + new_checkpoint = { + k: v for k, v in new_checkpoint.items() if k.startswith("decoder") or k.startswith("post_quant") + } + elif only_encoder: + new_checkpoint = {k: v for k, v in new_checkpoint.items() if k.startswith("encoder") or k.startswith("quant")} + + return new_checkpoint + + +def convert_ldm_clip_checkpoint(checkpoint): + keys = list(checkpoint.keys()) + + text_model_dict = {} + for key in keys: + if key.startswith("cond_stage_model.transformer"): + text_model_dict[key[len("cond_stage_model.transformer.") :]] = checkpoint[key] + + return text_model_dict + + +def convert_lora_model_level( + state_dict, unet, text_encoder=None, LORA_PREFIX_UNET="lora_unet", LORA_PREFIX_TEXT_ENCODER="lora_te", alpha=0.6 +): + """convert lora in model level instead of pipeline leval""" + + visited = [] + + # directly update weight in diffusers model + for key in state_dict: + # it is suggested to print out the key, it usually will be something like below + # "lora_te_text_model_encoder_layers_0_self_attn_k_proj.lora_down.weight" + + # as we have set the alpha beforehand, so just skip + if ".alpha" in key or key in visited: + continue + + if "text" in key: + layer_infos = key.split(".")[0].split(LORA_PREFIX_TEXT_ENCODER + "_")[-1].split("_") + assert text_encoder is not None, "text_encoder must be passed since lora contains text encoder layers" + curr_layer = text_encoder + else: + layer_infos = key.split(".")[0].split(LORA_PREFIX_UNET + "_")[-1].split("_") + curr_layer = unet + + # find the target layer + temp_name = layer_infos.pop(0) + while len(layer_infos) > -1: + try: + curr_layer = curr_layer.__getattr__(temp_name) + if len(layer_infos) > 0: + temp_name = layer_infos.pop(0) + elif len(layer_infos) == 0: + break + except Exception: + if len(temp_name) > 0: + temp_name += "_" + layer_infos.pop(0) + else: + temp_name = layer_infos.pop(0) + + pair_keys = [] + if "lora_down" in key: + pair_keys.append(key.replace("lora_down", "lora_up")) + pair_keys.append(key) + else: + pair_keys.append(key) + pair_keys.append(key.replace("lora_up", "lora_down")) + + # update weight + # NOTE: load lycon, maybe have bugs :( + if "conv_in" in pair_keys[0]: + weight_up = state_dict[pair_keys[0]].to(torch.float32) + weight_down = state_dict[pair_keys[1]].to(torch.float32) + weight_up = weight_up.view(weight_up.size(0), -1) + weight_down = weight_down.view(weight_down.size(0), -1) + shape = list(curr_layer.weight.data.shape) + shape[1] = 4 + curr_layer.weight.data[:, :4, ...] += alpha * (weight_up @ weight_down).view(*shape) + elif "conv" in pair_keys[0]: + weight_up = state_dict[pair_keys[0]].to(torch.float32) + weight_down = state_dict[pair_keys[1]].to(torch.float32) + weight_up = weight_up.view(weight_up.size(0), -1) + weight_down = weight_down.view(weight_down.size(0), -1) + shape = list(curr_layer.weight.data.shape) + curr_layer.weight.data += alpha * (weight_up @ weight_down).view(*shape) + elif len(state_dict[pair_keys[0]].shape) == 4: + weight_up = state_dict[pair_keys[0]].squeeze(3).squeeze(2).to(torch.float32) + weight_down = state_dict[pair_keys[1]].squeeze(3).squeeze(2).to(torch.float32) + curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down).unsqueeze(2).unsqueeze(3).to( + curr_layer.weight.data.device + ) + else: + weight_up = state_dict[pair_keys[0]].to(torch.float32) + weight_down = state_dict[pair_keys[1]].to(torch.float32) + curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down).to(curr_layer.weight.data.device) + + # update visited list + for item in pair_keys: + visited.append(item) + + return unet, text_encoder + + +def denormalize_spectrogram( + data: torch.Tensor, + max_value: float = 200, + min_value: float = 1e-5, + power: float = 1, + inverse: bool = False, +) -> torch.Tensor: + max_value = np.log(max_value) + min_value = np.log(min_value) + + # Flip Y axis: image origin at the top-left corner, spectrogram origin at the bottom-left corner + data = torch.flip(data, [1]) + + assert len(data.shape) == 3, "Expected 3 dimensions, got {}".format(len(data.shape)) + + if data.shape[0] == 1: + data = data.repeat(3, 1, 1) + + assert data.shape[0] == 3, "Expected 3 channels, got {}".format(data.shape[0]) + data = data[0] + + # Reverse the power curve + data = torch.pow(data, 1 / power) + + # Invert + if inverse: + data = 1 - data + + # Rescale to max value + spectrogram = data * (max_value - min_value) + min_value + + return spectrogram + + +class ToTensor1D(torchvision.transforms.ToTensor): + def __call__(self, tensor: np.ndarray): + tensor_2d = super(ToTensor1D, self).__call__(tensor[..., np.newaxis]) + + return tensor_2d.squeeze_(0) + + +def scale(old_value, old_min, old_max, new_min, new_max): + old_range = old_max - old_min + new_range = new_max - new_min + new_value = (((old_value - old_min) * new_range) / old_range) + new_min + + return new_value + + +def read_frames_with_moviepy(video_path, max_frame_nums=None): + clip = VideoFileClip(video_path) + duration = clip.duration + frames = [] + for frame in clip.iter_frames(): + frames.append(frame) + if max_frame_nums is not None: + frames_idx = np.linspace(0, len(frames) - 1, max_frame_nums, dtype=int) + clip.close() + return np.array(frames)[frames_idx, ...], duration,frames + + +def read_frames_with_moviepy_resample(video_path, save_path): + vision_transform_list = [ + transforms.Resize((128, 128)), + transforms.CenterCrop((112, 112)), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] + video_transform = transforms.Compose(vision_transform_list) + os.makedirs(save_path, exist_ok=True) + command = f'ffmpeg -v quiet -y -i "{video_path}" -f image2 -vf "scale=-1:360,fps=15" -qscale:v 3 "{save_path}"/frame%06d.jpg' + os.system(command) + frame_list = glob.glob(f"{save_path}/*.jpg") + frame_list.sort() + convert_tensor = transforms.ToTensor() + frame_list = [convert_tensor(np.array(Image.open(frame))) for frame in frame_list] + imgs = torch.stack(frame_list, dim=0) + imgs = video_transform(imgs) + imgs = imgs.permute(1, 0, 2, 3) + return imgs diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7f58658 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +#soundfile +#safetensors +#moviepy +#decord +#imageio +#h5py \ No newline at end of file