| from __future__ import annotations |
|
|
| import argparse |
| import math |
| import random |
| import sys |
| import warnings |
| from pathlib import Path |
| from typing import Any |
|
|
| import torch |
| import yaml |
| from PIL import Image |
| from tqdm import tqdm |
|
|
| sys.path.append(str(Path(__file__).resolve().parent)) |
|
|
| from src.diffusion.gaussian_diffusion import GaussianDiffusion |
| from src.diffusion.samplers import DDPMSampler, DDIMSampler |
| from src.models.autoencoder.vae import AutoencoderKL |
| from src.models.conditioning.clip_text import FrozenCLIPTextEncoder |
| from src.models.diffusion.unet import build_latent_diffusion_unet_from_config |
|
|
|
|
| def load_yaml(path: str | Path) -> dict[str, Any]: |
| with open(path, "r", encoding="utf-8") as f: |
| return yaml.safe_load(f) |
|
|
|
|
| def safe_torch_load(path: str | Path, map_location="cpu"): |
| try: |
| return torch.load(path, map_location=map_location, weights_only=True) |
| except TypeError: |
| return torch.load(path, map_location=map_location) |
| except Exception: |
| return torch.load(path, map_location=map_location) |
|
|
|
|
| def get_dtype(name: str) -> torch.dtype: |
| name = name.lower() |
| if name == "fp16": |
| return torch.float16 |
| if name == "bf16": |
| return torch.bfloat16 |
| if name == "fp32": |
| return torch.float32 |
| raise ValueError(f"Unknown precision={name}") |
|
|
|
|
| def autocast_context(device: torch.device, dtype: torch.dtype): |
| enabled = device.type == "cuda" and dtype in (torch.float16, torch.bfloat16) |
| if device.type == "cuda": |
| return torch.autocast("cuda", dtype=dtype, enabled=enabled) |
| return torch.autocast("cpu", enabled=False) |
|
|
|
|
| def clear_cuda_cache(): |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| try: |
| torch.cuda.ipc_collect() |
| except Exception: |
| pass |
|
|
|
|
| def set_seed(seed: int): |
| random.seed(seed) |
| torch.manual_seed(seed) |
| if torch.cuda.is_available(): |
| torch.cuda.manual_seed_all(seed) |
|
|
|
|
| def sanitize_filename(text: str, max_len: int = 80) -> str: |
| text = text.lower().strip() |
| keep = [] |
| for ch in text: |
| if ch.isalnum(): |
| keep.append(ch) |
| elif ch in {" ", "-", "_"}: |
| keep.append("_") |
| out = "".join(keep) |
| while "__" in out: |
| out = out.replace("__", "_") |
| out = out.strip("_") or "sample" |
| return out[:max_len] |
|
|
|
|
| def save_image_tensor(image: torch.Tensor, path: str | Path): |
| if not torch.isfinite(image).all(): |
| finite = image[torch.isfinite(image)] |
| if finite.numel() > 0: |
| stats = ( |
| f"finite_min={finite.min().item():.6f}, " |
| f"finite_max={finite.max().item():.6f}, " |
| f"finite_mean={finite.mean().item():.6f}" |
| ) |
| else: |
| stats = "no finite values" |
|
|
| raise RuntimeError( |
| "Non-finite values found in image tensor before saving. " |
| f"{stats}. " |
| "This usually means fp16 sampling or VAE decoding became unstable. " |
| "Try --precision fp32, lower --guidance-scale, or decode the VAE in fp32." |
| ) |
|
|
| image = image.detach().cpu().clamp(0.0, 1.0) |
| image = image.permute(1, 2, 0).float().numpy() |
| image = (image * 255).round().astype("uint8") |
| Image.fromarray(image).save(path) |
|
|
|
|
| def save_image_grid_from_paths( |
| image_paths: list[Path], |
| path: str | Path, |
| nrow: int | None = None, |
| padding: int = 2, |
| ): |
| if not image_paths: |
| return |
|
|
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
|
|
| images = [Image.open(p).convert("RGB") for p in image_paths] |
|
|
| if nrow is None: |
| nrow = int(math.ceil(math.sqrt(len(images)))) |
| ncol = int(math.ceil(len(images) / nrow)) |
|
|
| widths, heights = zip(*(img.size for img in images)) |
| cell_w, cell_h = max(widths), max(heights) |
| grid_w = nrow * cell_w + padding * (nrow - 1) |
| grid_h = ncol * cell_h + padding * (ncol - 1) |
|
|
| grid = Image.new("RGB", (grid_w, grid_h), color=(255, 255, 255)) |
| for idx, img in enumerate(images): |
| row = idx // nrow |
| col = idx % nrow |
| x = col * (cell_w + padding) |
| y = row * (cell_h + padding) |
| grid.paste(img, (x, y)) |
|
|
| grid.save(path) |
|
|
| for img in images: |
| img.close() |
|
|
|
|
| def load_model_state(module: torch.nn.Module, checkpoint_path: str | Path): |
| checkpoint = safe_torch_load(checkpoint_path, map_location="cpu") |
| if isinstance(checkpoint, dict): |
| state_dict = checkpoint.get("model", checkpoint.get("state_dict", checkpoint)) |
| else: |
| state_dict = checkpoint |
| module.load_state_dict(state_dict, strict=True) |
|
|
|
|
| def build_vae(vae_cfg: dict) -> AutoencoderKL: |
| model_cfg = dict(vae_cfg["model"]) |
| model_cfg.pop("name", None) |
| return AutoencoderKL(**model_cfg) |
|
|
|
|
| def build_diffusion(ldm_cfg: dict) -> GaussianDiffusion: |
| d = ldm_cfg["diffusion"] |
| return GaussianDiffusion( |
| schedule_type=str(d.get("schedule_type", "cosine")), |
| num_timesteps=int(d.get("num_timesteps", 1000)), |
| prediction_type=str(d.get("prediction_type", "v")), |
| loss_type=str(d.get("loss_type", "mse")), |
| beta_start=float(d.get("beta_start", 1e-4)), |
| beta_end=float(d.get("beta_end", 2e-2)), |
| cosine_s=float(d.get("cosine_s", 0.008)), |
| max_beta=float(d.get("max_beta", 0.999)), |
| ) |
|
|
|
|
| def build_text_encoder(ldm_cfg: dict, device: torch.device, local_files_only: bool): |
| warnings.filterwarnings("ignore", message=".*clean_up_tokenization_spaces.*", category=FutureWarning) |
|
|
| text_cfg = dict(ldm_cfg.get("text_encoder", {})) |
| text_encoder = FrozenCLIPTextEncoder( |
| model_name=str(text_cfg.get("model_name", "openai/clip-vit-large-patch14")), |
| max_length=int(text_cfg.get("max_length", 77)), |
| freeze=True, |
| use_last_hidden_state=bool(text_cfg.get("use_last_hidden_state", True)), |
| local_files_only=local_files_only, |
| ) |
| text_encoder.to(device=device) |
| text_encoder.eval() |
| return text_encoder |
|
|
|
|
| @torch.no_grad() |
| def encode_contexts(text_encoder, prompts: list[str], empty_text: str, device: torch.device, dtype: torch.dtype): |
| context_dtype = dtype if device.type == "cuda" else torch.float32 |
| cond_context = text_encoder.encode(prompts, device=device).to(dtype=context_dtype) |
| uncond_context = text_encoder.encode([empty_text] * len(prompts), device=device).to(dtype=context_dtype) |
| return cond_context, uncond_context |
|
|
|
|
| @torch.no_grad() |
| def decode_latents(vae, latents: torch.Tensor, scaling_factor: float, dtype: torch.dtype): |
|
|
| if not torch.isfinite(latents).all(): |
| finite = latents[torch.isfinite(latents)] |
| if finite.numel() > 0: |
| stats = ( |
| f"finite_min={finite.min().item():.6f}, " |
| f"finite_max={finite.max().item():.6f}, " |
| f"finite_mean={finite.mean().item():.6f}" |
| ) |
| else: |
| stats = "no finite values" |
|
|
| raise RuntimeError( |
| "Non-finite values found in sampled latents before VAE decode. " |
| f"{stats}. " |
| "Try --precision fp32 or lower --guidance-scale." |
| ) |
|
|
| |
| z = latents.float() / float(scaling_factor) |
|
|
| |
| if z.is_cuda: |
| with torch.autocast("cuda", enabled=False): |
| images = vae.decode(z, unscale=False) |
| else: |
| images = vae.decode(z, unscale=False) |
|
|
| if hasattr(images, "sample"): |
| images = images.sample |
|
|
| images = images.float() |
|
|
| if not torch.isfinite(images).all(): |
| finite = images[torch.isfinite(images)] |
| if finite.numel() > 0: |
| stats = ( |
| f"finite_min={finite.min().item():.6f}, " |
| f"finite_max={finite.max().item():.6f}, " |
| f"finite_mean={finite.mean().item():.6f}" |
| ) |
| else: |
| stats = "no finite values" |
|
|
| raise RuntimeError( |
| "Non-finite values produced by VAE decode. " |
| f"{stats}. " |
| "Try --precision fp32. If this still happens, inspect the sampled latents." |
| ) |
|
|
| return ((images + 1.0) / 2.0).clamp(0.0, 1.0) |
|
|
|
|
| @torch.no_grad() |
| def decode_and_save_latents( |
| vae, |
| latents: torch.Tensor, |
| prompts: list[str], |
| output_dir: Path, |
| global_start_index: int, |
| scaling_factor: float, |
| dtype: torch.dtype, |
| decode_batch_size: int, |
| ) -> list[Path]: |
| saved_paths: list[Path] = [] |
| decode_batch_size = max(1, int(decode_batch_size)) |
|
|
| for local_start in range(0, latents.shape[0], decode_batch_size): |
| local_end = min(local_start + decode_batch_size, latents.shape[0]) |
| latent_chunk = latents[local_start:local_end] |
|
|
| images = decode_latents( |
| vae=vae, |
| latents=latent_chunk, |
| scaling_factor=scaling_factor, |
| dtype=dtype, |
| ) |
|
|
| for j, image in enumerate(images): |
| local_idx = local_start + j |
| prompt = prompts[local_idx] |
| global_idx = global_start_index + local_idx |
| out_path = output_dir / f"{global_idx:04d}_{sanitize_filename(prompt)}.png" |
| save_image_tensor(image, out_path) |
| saved_paths.append(out_path) |
|
|
| del latent_chunk, images |
| clear_cuda_cache() |
|
|
| return saved_paths |
|
|
|
|
| def read_prompts(args) -> list[str]: |
| prompts: list[str] = [] |
|
|
| if args.prompt is not None: |
| prompts.append(args.prompt) |
|
|
| if args.prompts_file is not None: |
| with open(args.prompts_file, "r", encoding="utf-8") as f: |
| prompts.extend([line.strip() for line in f if line.strip()]) |
|
|
| if not prompts: |
| raise ValueError("Provide --prompt or --prompts-file.") |
|
|
| repeated = [] |
| for prompt in prompts: |
| repeated.extend([prompt] * args.num_images_per_prompt) |
| return repeated |
|
|
|
|
| @torch.no_grad() |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--config", type=str, default="generation_config.yaml") |
| parser.add_argument("--prompt", type=str, default=None) |
| parser.add_argument("--prompts-file", type=str, default=None) |
| parser.add_argument("--output-dir", type=str, default="outputs") |
| parser.add_argument("--sampler", type=str, default=None, choices=["ddim", "ddpm"]) |
| parser.add_argument("--num-steps", type=int, default=None) |
| parser.add_argument("--guidance-scale", type=float, default=None) |
| parser.add_argument("--eta", type=float, default=None) |
| parser.add_argument("--precision", type=str, default=None, choices=["fp32", "bf16", "fp16"]) |
| parser.add_argument("--seed", type=int, default=None) |
| parser.add_argument("--batch-size", type=int, default=None) |
| parser.add_argument("--num-images-per-prompt", type=int, default=1) |
| parser.add_argument("--local-files-only", action="store_true", help="Use only locally cached CLIP files.") |
|
|
| |
| parser.add_argument("--low-vram", action="store_true", help="Offload CLIP/UNet/VAE between stages to reduce peak GPU memory.") |
| parser.add_argument("--decode-batch-size", type=int, default=1, help="Number of latents to VAE-decode at once.") |
| parser.add_argument("--no-grid", action="store_true", help="Skip grid.png creation to reduce CPU RAM usage.") |
| parser.add_argument("--grid-max-images", type=int, default=64, help="Maximum saved images to include in grid.png.") |
|
|
| args = parser.parse_args() |
|
|
| repo_root = Path(__file__).resolve().parent |
| cfg = load_yaml(repo_root / args.config) |
|
|
| gen_cfg = cfg.get("generation", {}) |
| sampler_cfg = cfg.get("sampler", {}) |
|
|
| seed = int(args.seed if args.seed is not None else gen_cfg.get("seed", 42)) |
| precision = str(args.precision if args.precision is not None else gen_cfg.get("precision", "bf16")) |
| batch_size = int(args.batch_size if args.batch_size is not None else gen_cfg.get("batch_size", 4)) |
| sampler_name = str(args.sampler if args.sampler is not None else sampler_cfg.get("type", "ddim")).lower() |
| num_steps = int(args.num_steps if args.num_steps is not None else sampler_cfg.get("num_steps", 50)) |
| guidance_scale = float(args.guidance_scale if args.guidance_scale is not None else sampler_cfg.get("guidance_scale", 3.0)) |
| eta = float(args.eta if args.eta is not None else sampler_cfg.get("eta", 0.0)) |
| clip_denoised = bool(sampler_cfg.get("clip_denoised", False)) |
|
|
| set_seed(seed) |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| dtype = get_dtype(precision) |
| low_vram = bool(args.low_vram and device.type == "cuda") |
|
|
| ldm_cfg_path = repo_root / cfg["ldm"]["config"] |
| ldm_ckpt_path = repo_root / cfg["ldm"]["checkpoint"] |
| vae_cfg_path = repo_root / cfg["vae"]["config"] |
| vae_ckpt_path = repo_root / cfg["vae"]["checkpoint"] |
|
|
| ldm_cfg = load_yaml(ldm_cfg_path) |
| vae_cfg = load_yaml(vae_cfg_path) |
|
|
| vae = build_vae(vae_cfg) |
| load_model_state(vae, vae_ckpt_path) |
| vae.to(device="cpu" if low_vram else device) |
| vae.eval() |
|
|
| unet = build_latent_diffusion_unet_from_config(ldm_cfg) |
| load_model_state(unet, ldm_ckpt_path) |
| unet.to(device=device) |
| unet.eval() |
|
|
| diffusion = build_diffusion(ldm_cfg).to(device) |
| text_encoder = build_text_encoder(ldm_cfg, device=device, local_files_only=args.local_files_only) |
|
|
| if sampler_name == "ddim": |
| sampler = DDIMSampler(diffusion) |
| elif sampler_name == "ddpm": |
| sampler = DDPMSampler(diffusion) |
| else: |
| raise ValueError(f"Unknown sampler: {sampler_name}") |
|
|
| latent_channels = int(ldm_cfg["model"].get("in_channels", 8)) |
| image_size = int(gen_cfg.get("resolution", 256)) |
| latent_size = int(ldm_cfg["model"].get("latent_size", image_size // 8)) |
| scaling_factor = float(cfg["vae"].get("scaling_factor", getattr(vae, "scaling_factor", 1.0))) |
| empty_text = str(cfg.get("conditioning", {}).get("empty_text", "")) |
|
|
| prompts = read_prompts(args) |
|
|
| output_dir = Path(args.output_dir) |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print("=============================================") |
| print("Custom latent diffusion inference") |
| print("Device:", device) |
| print("Precision:", precision) |
| print("Sampler:", sampler_name) |
| print("Steps:", num_steps if sampler_name == "ddim" else diffusion.num_timesteps) |
| print("Guidance scale:", guidance_scale) |
| print("Eta:", eta) |
| print("Seed:", seed) |
| print("Total images:", len(prompts)) |
| print("Batch size:", batch_size) |
| print("Low VRAM:", low_vram) |
| print("Decode batch size:", max(1, int(args.decode_batch_size))) |
| print("Output dir:", output_dir) |
| print("=============================================") |
|
|
| saved_paths: list[Path] = [] |
|
|
| for batch_start in tqdm(range(0, len(prompts), batch_size), desc="batches"): |
| batch_prompts = prompts[batch_start: batch_start + batch_size] |
|
|
| if low_vram: |
| text_encoder.to(device) |
| clear_cuda_cache() |
|
|
| cond_context, uncond_context = encode_contexts( |
| text_encoder=text_encoder, |
| prompts=batch_prompts, |
| empty_text=empty_text, |
| device=device, |
| dtype=dtype, |
| ) |
|
|
| |
| if low_vram: |
| text_encoder.to("cpu") |
| clear_cuda_cache() |
|
|
| shape = (len(batch_prompts), latent_channels, latent_size, latent_size) |
|
|
| if low_vram: |
| unet.to(device) |
| clear_cuda_cache() |
|
|
| with autocast_context(device, dtype): |
| if sampler_name == "ddim": |
| sample_out = sampler.sample( |
| model=unet, |
| shape=shape, |
| device=device, |
| context=cond_context, |
| attention_mask=None, |
| uncond_context=uncond_context, |
| uncond_attention_mask=None, |
| guidance_scale=guidance_scale, |
| num_steps=num_steps, |
| eta=eta, |
| clip_denoised=clip_denoised, |
| return_trajectory=False, |
| progress=True, |
| ) |
| else: |
| sample_out = sampler.sample( |
| model=unet, |
| shape=shape, |
| device=device, |
| context=cond_context, |
| attention_mask=None, |
| uncond_context=uncond_context, |
| uncond_attention_mask=None, |
| guidance_scale=guidance_scale, |
| clip_denoised=clip_denoised, |
| return_trajectory=False, |
| progress=True, |
| ) |
|
|
| latents = sample_out.latents if hasattr(sample_out, "latents") else sample_out |
| del sample_out, cond_context, uncond_context |
|
|
| |
| if low_vram: |
| unet.to("cpu") |
| clear_cuda_cache() |
| vae.to(device) |
| clear_cuda_cache() |
|
|
| batch_saved = decode_and_save_latents( |
| vae=vae, |
| latents=latents, |
| prompts=batch_prompts, |
| output_dir=output_dir, |
| global_start_index=batch_start, |
| scaling_factor=scaling_factor, |
| dtype=dtype, |
| decode_batch_size=args.decode_batch_size, |
| ) |
| saved_paths.extend(batch_saved) |
|
|
| del latents |
|
|
| if low_vram: |
| vae.to("cpu") |
| clear_cuda_cache() |
|
|
| clear_cuda_cache() |
|
|
| if not args.no_grid: |
| grid_paths = saved_paths[: max(0, int(args.grid_max_images))] |
| save_image_grid_from_paths(grid_paths, output_dir / "grid.png") |
|
|
| print("Saved", len(saved_paths), "images to", output_dir) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|