"""Latent and text-encoder output caching helpers. Functions that move pre-computed VAE latents and text-encoder hidden states between memory and ``.npz`` files on disk. Used both during the explicit ``cache_latents`` / ``cache_text_encoder_outputs`` preprocessing passes and on the fly inside ``BaseDataset``. The leaf image helpers (``load_image`` / ``IMAGE_TRANSFORMS`` / ``trim_and_resize_if_required``) live in ``library.utils``; ``ImageInfo`` is only needed for type checks and stays behind ``TYPE_CHECKING`` to keep this module free of any (circular) dependency on ``library.dataset``. ``HIGH_VRAM`` lives in ``library.accelerator_setup``; it has no cycle with this module so it is imported directly. """ import logging import os from typing import TYPE_CHECKING, List, Tuple import numpy as np import torch from diffusers import AutoencoderKL from library import accelerator_setup from library.device_utils import clean_memory_on_device from library.utils import IMAGE_TRANSFORMS, load_image, trim_and_resize_if_required if TYPE_CHECKING: from library.dataset import ImageInfo logger = logging.getLogger(__name__) def is_disk_cached_latents_is_expected(reso, npz_path: str, flip_aug: bool, alpha_mask: bool): expected_latents_size = (reso[1] // 8, reso[0] // 8) # bucket_resoはWxHなので注意 if not os.path.exists(npz_path): return False try: npz = np.load(npz_path) if "latents" not in npz or "original_size" not in npz or "crop_ltrb" not in npz: # old ver? return False if npz["latents"].shape[1:3] != expected_latents_size: return False if flip_aug: if "latents_flipped" not in npz: return False if npz["latents_flipped"].shape[1:3] != expected_latents_size: return False if alpha_mask: if "alpha_mask" not in npz: return False if (npz["alpha_mask"].shape[1], npz["alpha_mask"].shape[0]) != reso: # HxW => WxH != reso return False else: if "alpha_mask" in npz: return False except Exception as e: logger.error(f"Error loading file: {npz_path}") raise e return True # for new_cache_latents def load_images_and_masks_for_caching( image_infos: List["ImageInfo"], use_alpha_mask: bool, random_crop: bool ) -> Tuple[torch.Tensor, List[np.ndarray], List[Tuple[int, int]], List[Tuple[int, int, int, int]]]: r""" requires image_infos to have: [absolute_path or image], bucket_reso, resized_size returns: image_tensor, alpha_masks, original_sizes, crop_ltrbs image_tensor: torch.Tensor = torch.Size([B, 3, H, W]), ...], normalized to [-1, 1] alpha_masks: List[np.ndarray] = [np.ndarray([H, W]), ...], normalized to [0, 1] original_sizes: List[Tuple[int, int]] = [(W, H), ...] crop_ltrbs: List[Tuple[int, int, int, int]] = [(L, T, R, B), ...] """ images: List[torch.Tensor] = [] alpha_masks: List[np.ndarray] = [] original_sizes: List[Tuple[int, int]] = [] crop_ltrbs: List[Tuple[int, int, int, int]] = [] for info in image_infos: image = ( load_image(info.absolute_path, use_alpha_mask) if info.image is None else np.array(info.image, np.uint8) ) # TODO 画像のメタデータが壊れていて、メタデータから割り当てたbucketと実際の画像サイズが一致しない場合があるのでチェック追加要 image, original_size, crop_ltrb = trim_and_resize_if_required( random_crop, image, info.bucket_reso, info.resized_size, resize_interpolation=info.resize_interpolation ) original_sizes.append(original_size) crop_ltrbs.append(crop_ltrb) if use_alpha_mask: if image.shape[2] == 4: alpha_mask = image[:, :, 3] # [H,W] alpha_mask = alpha_mask.astype(np.float32) / 255.0 alpha_mask = torch.FloatTensor(alpha_mask) # [H,W] else: alpha_mask = torch.ones_like(image[:, :, 0], dtype=torch.float32) # [H,W] else: alpha_mask = None alpha_masks.append(alpha_mask) image = image[:, :, :3] # remove alpha channel if exists image = IMAGE_TRANSFORMS(image) images.append(image) img_tensor = torch.stack(images, dim=0) return img_tensor, alpha_masks, original_sizes, crop_ltrbs def cache_batch_latents( vae: AutoencoderKL, cache_to_disk: bool, image_infos: List["ImageInfo"], flip_aug: bool, use_alpha_mask: bool, random_crop: bool, ) -> None: r""" requires image_infos to have: absolute_path, bucket_reso, resized_size, latents_npz optionally requires image_infos to have: image if cache_to_disk is True, set info.latents_npz flipped latents is also saved if flip_aug is True if cache_to_disk is False, set info.latents latents_flipped is also set if flip_aug is True latents_original_size and latents_crop_ltrb are also set """ images = [] alpha_masks: List[np.ndarray] = [] for info in image_infos: image = ( load_image(info.absolute_path, use_alpha_mask) if info.image is None else np.array(info.image, np.uint8) ) # TODO 画像のメタデータが壊れていて、メタデータから割り当てたbucketと実際の画像サイズが一致しない場合があるのでチェック追加要 image, original_size, crop_ltrb = trim_and_resize_if_required( random_crop, image, info.bucket_reso, info.resized_size, resize_interpolation=info.resize_interpolation ) info.latents_original_size = original_size info.latents_crop_ltrb = crop_ltrb if use_alpha_mask: if image.shape[2] == 4: alpha_mask = image[:, :, 3] # [H,W] alpha_mask = alpha_mask.astype(np.float32) / 255.0 alpha_mask = torch.FloatTensor(alpha_mask) # [H,W] else: alpha_mask = torch.ones_like(image[:, :, 0], dtype=torch.float32) # [H,W] else: alpha_mask = None alpha_masks.append(alpha_mask) image = image[:, :, :3] # remove alpha channel if exists image = IMAGE_TRANSFORMS(image) images.append(image) img_tensors = torch.stack(images, dim=0) img_tensors = img_tensors.to(device=vae.device, dtype=vae.dtype) with torch.no_grad(): latents = vae.encode(img_tensors).latent_dist.sample().to("cpu") if flip_aug: img_tensors = torch.flip(img_tensors, dims=[3]) with torch.no_grad(): flipped_latents = vae.encode(img_tensors).latent_dist.sample().to("cpu") else: flipped_latents = [None] * len(latents) for info, latent, flipped_latent, alpha_mask in zip(image_infos, latents, flipped_latents, alpha_masks): # check NaN if torch.isnan(latents).any() or (flipped_latent is not None and torch.isnan(flipped_latent).any()): raise RuntimeError(f"NaN detected in latents: {info.absolute_path}") if cache_to_disk: # save_latents_to_disk( # info.latents_npz, # latent, # info.latents_original_size, # info.latents_crop_ltrb, # flipped_latent, # alpha_mask, # ) pass else: info.latents = latent if flip_aug: info.latents_flipped = flipped_latent info.alpha_mask = alpha_mask if not accelerator_setup.HIGH_VRAM: clean_memory_on_device(vae.device)