Download vae/__init__.py from qvx-o/qvae: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/qvx-o/qvae/resolve/main/vae/__init__.py
- Command line
-
hf download hf://qvx-o/qvae/vae/__init__.py
-
curl -L -o __init__.py https://huggingface.co/qvx-o/qvae/resolve/main/vae/__init__.py
17.1 kB
| from __future__ import annotations | |
| import os | |
| from pathlib import Path | |
| from typing import Optional, Union, Sequence, List | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| IMAGE_SIZE = 512 | |
| LATENT_CHANNELS = 8 | |
| C512 = 32 | |
| C256 = 64 | |
| C128 = 96 | |
| C64 = 128 | |
| NUM_RES_BLOCKS = 2 | |
| LATENT_DTYPE = torch.float16 | |
| MODEL_DTYPE = torch.float32 | |
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| def valid_num_groups(channels: int, preferred: int = 16) -> int: | |
| upper = min(channels, preferred) | |
| for groups in range(upper, 0, -1): | |
| if channels % groups == 0: | |
| return groups | |
| return 1 | |
| class ResBlock(nn.Module): | |
| def __init__(self, channels: int): | |
| super().__init__() | |
| groups = valid_num_groups(channels) | |
| self.norm1 = nn.GroupNorm(groups, channels, eps=1e-5) | |
| self.conv1 = nn.Conv2d(channels, channels, 3, padding=1) | |
| self.norm2 = nn.GroupNorm(groups, channels, eps=1e-5) | |
| self.conv2 = nn.Conv2d(channels, channels, 3, padding=1) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| residual = x | |
| x = self.conv1(F.silu(self.norm1(x))) | |
| x = self.conv2(F.silu(self.norm2(x))) | |
| return residual + x | |
| class Encoder(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.input = nn.Conv2d(3, C512, 3, padding=1) | |
| self.block0 = nn.Sequential( | |
| ResBlock(C512), | |
| ResBlock(C512), | |
| ) | |
| self.down1 = nn.Conv2d(C512, C256, 4, stride=2, padding=1) | |
| self.block1 = nn.Sequential( | |
| ResBlock(C256), | |
| ResBlock(C256), | |
| ) | |
| self.down2 = nn.Conv2d(C256, C128, 4, stride=2, padding=1) | |
| self.block2 = nn.Sequential( | |
| ResBlock(C128), | |
| ResBlock(C128), | |
| ) | |
| self.down3 = nn.Conv2d(C128, C64, 4, stride=2, padding=1) | |
| self.block3 = nn.Sequential( | |
| ResBlock(C64), | |
| ResBlock(C64), | |
| ) | |
| self.final_norm = nn.GroupNorm(valid_num_groups(C64), C64) | |
| self.mu = nn.Conv2d(C64, LATENT_CHANNELS, 3, padding=1) | |
| self.logvar = nn.Conv2d(C64, LATENT_CHANNELS, 3, padding=1) | |
| def forward(self, x: torch.Tensor): | |
| x = self.block0(self.input(x)) | |
| x = self.block1(F.silu(self.down1(x))) | |
| x = self.block2(F.silu(self.down2(x))) | |
| x = self.block3(F.silu(self.down3(x))) | |
| x = F.silu(self.final_norm(x)) | |
| mu = self.mu(x) | |
| logvar = torch.clamp(self.logvar(x), -10.0, 10.0) | |
| return mu, logvar | |
| class Decoder(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.input = nn.Conv2d(LATENT_CHANNELS, C64, 3, padding=1) | |
| self.block3 = nn.Sequential( | |
| ResBlock(C64), | |
| ResBlock(C64), | |
| ) | |
| self.up1 = nn.Conv2d(C64, C128, 3, padding=1) | |
| self.block2 = nn.Sequential( | |
| ResBlock(C128), | |
| ResBlock(C128), | |
| ) | |
| self.up2 = nn.Conv2d(C128, C256, 3, padding=1) | |
| self.block1 = nn.Sequential( | |
| ResBlock(C256), | |
| ResBlock(C256), | |
| ) | |
| self.up3 = nn.Conv2d(C256, C512, 3, padding=1) | |
| self.block0 = nn.Sequential( | |
| ResBlock(C512), | |
| ResBlock(C512), | |
| ) | |
| self.final_norm = nn.GroupNorm(valid_num_groups(C512), C512) | |
| self.output = nn.Conv2d(C512, 3, 3, padding=1) | |
| def forward(self, z: torch.Tensor) -> torch.Tensor: | |
| x = self.block3(self.input(z)) | |
| x = F.interpolate(x, scale_factor=2, mode="nearest") | |
| x = self.block2(F.silu(self.up1(x))) | |
| x = F.interpolate(x, scale_factor=2, mode="nearest") | |
| x = self.block1(F.silu(self.up2(x))) | |
| x = F.interpolate(x, scale_factor=2, mode="nearest") | |
| x = self.block0(F.silu(self.up3(x))) | |
| x = F.silu(self.final_norm(x)) | |
| return torch.tanh(self.output(x)) | |
| class VAE(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.encoder = Encoder() | |
| self.decoder = Decoder() | |
| def encode(self, x: torch.Tensor): | |
| return self.encoder(x) | |
| def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor): | |
| std = torch.exp(0.5 * logvar) | |
| return mu + torch.randn_like(std) * std | |
| def decode(self, z: torch.Tensor): | |
| return self.decoder(z) | |
| def forward(self, x: torch.Tensor, sample: bool = True): | |
| mu, logvar = self.encode(x) | |
| z = self.reparameterize(mu, logvar) if sample else mu | |
| return self.decode(z), mu, logvar, z | |
| _MODEL: Optional[VAE] = None | |
| _CHECKPOINT_PATH: Optional[Path] = None | |
| def _candidate_checkpoints() -> List[Path]: | |
| candidates = [] | |
| env_path = os.environ.get("QARVEXIUM_VAE_CHECKPOINT") | |
| if env_path: | |
| candidates.append(Path(env_path)) | |
| package_dir = Path(__file__).resolve().parent | |
| candidates.extend([ | |
| package_dir / "qvae.pt", | |
| package_dir / "checkpoints" / "qvae.pt", | |
| ]) | |
| result = [] | |
| seen = set() | |
| for path in candidates: | |
| path = path.expanduser().resolve() | |
| if path not in seen: | |
| seen.add(path) | |
| result.append(path) | |
| return result | |
| def _find_checkpoint() -> Path: | |
| candidates = _candidate_checkpoints() | |
| for path in candidates: | |
| if path.is_file(): | |
| return path | |
| searched = "\n".join(f" - {p}" for p in candidates) | |
| raise FileNotFoundError( | |
| "Could not find the Qarvexium VAE checkpoint.\n\n" | |
| f"Searched:\n{searched}\n\n" | |
| "Set QARVEXIUM_VAE_CHECKPOINT to the absolute path " | |
| "of your checkpoint." | |
| ) | |
| def _extract_state_dict(checkpoint): | |
| if not isinstance(checkpoint, dict): | |
| raise RuntimeError("Unsupported checkpoint format.") | |
| if "model" in checkpoint: | |
| state_dict = checkpoint["model"] | |
| elif "state_dict" in checkpoint: | |
| state_dict = checkpoint["state_dict"] | |
| elif "model_state_dict" in checkpoint: | |
| state_dict = checkpoint["model_state_dict"] | |
| else: | |
| state_dict = checkpoint | |
| if not isinstance(state_dict, dict): | |
| raise RuntimeError("Checkpoint model state is not a state_dict.") | |
| return { | |
| key[len("module."):] if key.startswith("module.") else key: value | |
| for key, value in state_dict.items() | |
| } | |
| def load_model( | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| force_reload: bool = False, | |
| ) -> VAE: | |
| global _MODEL, _CHECKPOINT_PATH | |
| requested_path = ( | |
| Path(checkpoint_path).expanduser().resolve() | |
| if checkpoint_path is not None | |
| else None | |
| ) | |
| if ( | |
| not force_reload | |
| and _MODEL is not None | |
| and (requested_path is None or requested_path == _CHECKPOINT_PATH) | |
| ): | |
| return _MODEL | |
| if requested_path is None: | |
| requested_path = _find_checkpoint() | |
| if not requested_path.is_file(): | |
| raise FileNotFoundError( | |
| f"Checkpoint does not exist:\n{requested_path}" | |
| ) | |
| print( | |
| f"[Qarvexium VAE] Loading checkpoint: {requested_path}", | |
| flush=True, | |
| ) | |
| checkpoint = torch.load( | |
| requested_path, | |
| map_location="cpu", | |
| weights_only=False, | |
| ) | |
| if isinstance(checkpoint, dict): | |
| saved_config = checkpoint.get("config") | |
| if saved_config is not None: | |
| expected = { | |
| "image_size": IMAGE_SIZE, | |
| "latent_channels": LATENT_CHANNELS, | |
| "c512": C512, | |
| "c256": C256, | |
| "c128": C128, | |
| "c64": C64, | |
| } | |
| for key, expected_value in expected.items(): | |
| saved_value = saved_config.get(key) | |
| if saved_value is not None and saved_value != expected_value: | |
| raise RuntimeError( | |
| "Checkpoint architecture mismatch:\n" | |
| f" {key}: checkpoint={saved_value}, " | |
| f"package={expected_value}" | |
| ) | |
| model = VAE() | |
| state_dict = _extract_state_dict(checkpoint) | |
| try: | |
| model.load_state_dict(state_dict, strict=True) | |
| except RuntimeError as error: | |
| raise RuntimeError( | |
| "The checkpoint still does not match the " | |
| "Qarvexium VAE architecture.\n\n" | |
| f"{error}" | |
| ) from error | |
| parameter_count = sum(p.numel() for p in model.parameters()) | |
| if isinstance(checkpoint, dict): | |
| saved_parameters = checkpoint.get("parameters") | |
| if ( | |
| saved_parameters is not None | |
| and int(saved_parameters) != parameter_count | |
| ): | |
| raise RuntimeError( | |
| "Checkpoint parameter count mismatch:\n" | |
| f" checkpoint = {saved_parameters:,}\n" | |
| f" package = {parameter_count:,}" | |
| ) | |
| model.float().eval().to(DEVICE) | |
| _MODEL = model | |
| _CHECKPOINT_PATH = requested_path | |
| print( | |
| f"[Qarvexium VAE] Loaded successfully " | |
| f"({parameter_count:,} parameters)", | |
| flush=True, | |
| ) | |
| print(f"[Qarvexium VAE] Model dtype: {MODEL_DTYPE}", flush=True) | |
| print(f"[Qarvexium VAE] Latent dtype: {LATENT_DTYPE}", flush=True) | |
| print(f"[Qarvexium VAE] Device: {DEVICE}", flush=True) | |
| return model | |
| def _prepare_image(image: Image.Image) -> torch.Tensor: | |
| if not isinstance(image, Image.Image): | |
| raise TypeError("Expected PIL.Image.Image") | |
| image = image.convert("RGB").resize( | |
| (IMAGE_SIZE, IMAGE_SIZE), | |
| resample=Image.Resampling.BICUBIC, | |
| ) | |
| array = np.asarray(image, dtype=np.uint8) | |
| if array.shape != (IMAGE_SIZE, IMAGE_SIZE, 3): | |
| raise ValueError( | |
| f"Expected image shape ({IMAGE_SIZE}, {IMAGE_SIZE}, 3), " | |
| f"got {array.shape}" | |
| ) | |
| array = np.transpose(array, (2, 0, 1)) | |
| array = np.ascontiguousarray(array) | |
| tensor = torch.from_numpy(array) | |
| tensor = tensor.float() / 127.5 - 1.0 | |
| return tensor.unsqueeze(0) | |
| def _tensor_to_pil(tensor: torch.Tensor) -> Image.Image: | |
| if tensor.ndim == 4: | |
| if tensor.shape[0] != 1: | |
| raise ValueError("Expected a single image tensor with batch size 1.") | |
| tensor = tensor[0] | |
| if tensor.ndim != 3: | |
| raise ValueError( | |
| f"Expected CHW tensor, got shape {tuple(tensor.shape)}" | |
| ) | |
| tensor = ((tensor.float().clamp(-1.0, 1.0) + 1.0) / 2.0) | |
| tensor = ( | |
| tensor.permute(1, 2, 0) | |
| .cpu() | |
| .numpy() | |
| ) | |
| array = (tensor * 255.0).round().astype(np.uint8) | |
| return Image.fromarray(array, mode="RGB") | |
| def _validate_latent( | |
| latent: torch.Tensor, | |
| allow_batch: bool = True, | |
| ) -> torch.Tensor: | |
| if not isinstance(latent, torch.Tensor): | |
| raise TypeError("latent must be a torch.Tensor") | |
| if latent.ndim == 3: | |
| latent = latent.unsqueeze(0) | |
| if latent.ndim != 4: | |
| raise ValueError( | |
| "Expected latent shape (8,64,64) or (B,8,64,64), " | |
| f"got {tuple(latent.shape)}" | |
| ) | |
| if not allow_batch and latent.shape[0] != 1: | |
| raise ValueError("Expected batch size 1.") | |
| expected = (LATENT_CHANNELS, 64, 64) | |
| if tuple(latent.shape[1:]) != expected: | |
| raise ValueError( | |
| "Invalid latent shape.\n" | |
| f"Expected: (B, {LATENT_CHANNELS}, 64, 64)\n" | |
| f"Got: {tuple(latent.shape)}" | |
| ) | |
| return latent | |
| def encode( | |
| image: Image.Image, | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> torch.Tensor: | |
| model = load_model(checkpoint_path) | |
| x = _prepare_image(image).to(DEVICE, dtype=MODEL_DTYPE) | |
| mu, _ = model.encode(x) | |
| return mu.detach().to(LATENT_DTYPE).cpu() | |
| def decode( | |
| latent: torch.Tensor, | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> Image.Image: | |
| model = load_model(checkpoint_path) | |
| latent = _validate_latent(latent, allow_batch=False) | |
| z = latent.to(DEVICE, dtype=MODEL_DTYPE) | |
| return _tensor_to_pil(model.decode(z)) | |
| def encode_path( | |
| path: Union[str, os.PathLike], | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> torch.Tensor: | |
| path = Path(path).expanduser().resolve() | |
| if not path.is_file(): | |
| raise FileNotFoundError(f"Image not found:\n{path}") | |
| with Image.open(path) as image: | |
| image = image.convert("RGB").copy() | |
| return encode(image, checkpoint_path=checkpoint_path) | |
| def reconstruct_path( | |
| path: Union[str, os.PathLike], | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> Image.Image: | |
| return decode( | |
| encode_path(path, checkpoint_path=checkpoint_path), | |
| checkpoint_path=checkpoint_path, | |
| ) | |
| def reconstruct( | |
| image: Image.Image, | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> Image.Image: | |
| return decode( | |
| encode(image, checkpoint_path=checkpoint_path), | |
| checkpoint_path=checkpoint_path, | |
| ) | |
| def encode_batch( | |
| images: Sequence[Image.Image], | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> torch.Tensor: | |
| if len(images) == 0: | |
| raise ValueError("images cannot be empty.") | |
| model = load_model(checkpoint_path) | |
| batch = torch.stack( | |
| [_prepare_image(image)[0] for image in images], | |
| dim=0, | |
| ).to(DEVICE, dtype=MODEL_DTYPE) | |
| mu, _ = model.encode(batch) | |
| return mu.detach().to(LATENT_DTYPE).cpu() | |
| def decode_batch( | |
| latents: torch.Tensor, | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> List[Image.Image]: | |
| model = load_model(checkpoint_path) | |
| if not isinstance(latents, torch.Tensor): | |
| raise TypeError("latents must be a torch.Tensor") | |
| if latents.ndim != 4: | |
| raise ValueError( | |
| "Expected shape (B, 8, 64, 64), " | |
| f"got {tuple(latents.shape)}" | |
| ) | |
| expected = (LATENT_CHANNELS, 64, 64) | |
| if tuple(latents.shape[1:]) != expected: | |
| raise ValueError( | |
| "Invalid latent shape.\n" | |
| f"Expected: (B, {LATENT_CHANNELS}, 64, 64)\n" | |
| f"Got: {tuple(latents.shape)}" | |
| ) | |
| z = latents.to(DEVICE, dtype=MODEL_DTYPE) | |
| reconstruction = model.decode(z) | |
| reconstruction = ((reconstruction.float().clamp(-1.0, 1.0) + 1.0) / 2.0) | |
| reconstruction = ( | |
| reconstruction.permute(0, 2, 3, 1) | |
| .cpu() | |
| .numpy() | |
| ) | |
| arrays = (reconstruction * 255.0).round().astype(np.uint8) | |
| return [ | |
| Image.fromarray(array, mode="RGB") | |
| for array in arrays | |
| ] | |
| def latent_info(latent: torch.Tensor) -> dict: | |
| latent = _validate_latent(latent) | |
| values_per_image = latent[0].numel() | |
| fp32_bytes = values_per_image * 4 | |
| fp16_bytes = values_per_image * 2 | |
| return { | |
| "shape": tuple(latent.shape), | |
| "dtype": str(latent.dtype), | |
| "device": str(latent.device), | |
| "values_per_image": values_per_image, | |
| "fp32_bytes_per_image": fp32_bytes, | |
| "fp16_bytes_per_image": fp16_bytes, | |
| "fp32_kib_per_image": fp32_bytes / 1024.0, | |
| "fp16_kib_per_image": fp16_bytes / 1024.0, | |
| "storage_reduction": 2.0, | |
| } | |
| def model_info( | |
| checkpoint_path: Optional[Union[str, os.PathLike]] = None, | |
| ) -> dict: | |
| model = load_model(checkpoint_path) | |
| parameter_count = sum( | |
| parameter.numel() | |
| for parameter in model.parameters() | |
| ) | |
| latent_values = LATENT_CHANNELS * 64 * 64 | |
| return { | |
| "parameters": parameter_count, | |
| "parameters_millions": parameter_count / 1_000_000.0, | |
| "device": str(DEVICE), | |
| "model_dtype": str(MODEL_DTYPE), | |
| "latent_dtype": str(LATENT_DTYPE), | |
| "image_size": IMAGE_SIZE, | |
| "latent_channels": LATENT_CHANNELS, | |
| "latent_shape": (LATENT_CHANNELS, 64, 64), | |
| "latent_values": latent_values, | |
| "latent_fp32_kib": latent_values * 4 / 1024.0, | |
| "latent_fp16_kib": latent_values * 2 / 1024.0, | |
| "architecture": "32 / 64 / 96 / 128", | |
| "checkpoint": str(_CHECKPOINT_PATH) if _CHECKPOINT_PATH else None, | |
| } | |
| __all__ = [ | |
| "VAE", | |
| "Encoder", | |
| "Decoder", | |
| "encode", | |
| "decode", | |
| "encode_path", | |
| "reconstruct", | |
| "reconstruct_path", | |
| "encode_batch", | |
| "decode_batch", | |
| "load_model", | |
| "model_info", | |
| "latent_info", | |
| "IMAGE_SIZE", | |
| "LATENT_CHANNELS", | |
| "LATENT_DTYPE", | |
| "MODEL_DTYPE", | |
| "DEVICE", | |
| ] |