| |
| |
| |
| |
| |
| from __future__ import annotations |
|
|
| from typing import Any, Optional |
|
|
| import jax |
| import jax.numpy as jnp |
| from jax import random |
|
|
| from flax import linen as nn |
| from flax.training import train_state |
| from flax import struct |
| import optax |
| from .ann import ANNBackend, make_ann |
| from .ddpm import DDPM |
| |
| from .dmap import DMAP |
| from .gplm import GPLM |
|
|
| __all__ = ["DDPM", "EpsMLP", "cosine_schedule", "sinusoidal_embedding"] |
|
|
|
|
| |
| |
| |
|
|
| class DDIM(DDPM): |
| """ |
| DDIM sampler for D-dimensional latents. |
| |
| Notes |
| ----- |
| * Training is identical to DDPM (epsilon prediction). |
| * Sampling / refinement uses the DDIM update: |
| x_{t_prev} = sqrt(a_bar_prev) * x0_hat |
| + sqrt(1 - a_bar_prev - sigma^2) * eps_pred |
| + sigma * z, |
| where sigma = eta * sqrt( (1-a_bar_prev)/(1-a_bar_t) * (1 - a_bar_t/a_bar_prev) ). |
| |
| With eta=0.0 the reverse process is deterministic conditioned on x_t. |
| |
| Added (HF I/O): |
| - state_dict / save_local / load_local / from_state |
| - upload_to_huggingface / download_from_huggingface |
| """ |
|
|
| def __init__( |
| self, |
| Z_iX: jnp.ndarray, |
| *, |
| eta: float = 0.0, |
| steps: Optional[int] = None, |
| **ddpm_kwargs: Any, |
| ): |
| super().__init__(Z_iX, **ddpm_kwargs) |
| self.eta = float(eta) |
| self.steps = None if steps is None else int(steps) |
|
|
| @staticmethod |
| def _make_ddim_step(params_ema, apply_fn, alpha_bar_s, eps: float, eta: float): |
| eta = float(eta) |
|
|
| @jax.jit |
| def step(carry, t_pair): |
| key, x = carry |
| t, t_prev = t_pair |
|
|
| |
| a_bar_t = jnp.clip(alpha_bar_s[t], eps, 1.0) |
| a_bar_prev = jnp.clip(alpha_bar_s[t_prev], eps, 1.0) |
|
|
| B = x.shape[0] |
| t_batch = jnp.full((B, 1), t, dtype=jnp.int32) |
|
|
| eps_pred = apply_fn({"params": params_ema}, x, t_batch) |
|
|
| |
| sqrt_one_minus = jnp.sqrt(jnp.clip(1.0 - a_bar_t, eps, 1.0)) |
| x0_hat = (x - sqrt_one_minus * eps_pred) / jnp.sqrt(a_bar_t) |
|
|
| |
| frac = (1.0 - a_bar_prev) / jnp.clip(1.0 - a_bar_t, eps, 1.0) |
| inside = 1.0 - a_bar_t / jnp.clip(a_bar_prev, eps, 1.0) |
| sigma = eta * jnp.sqrt(jnp.clip(frac * inside, 0.0, 1.0)) |
|
|
| |
| dir_coeff = jnp.sqrt(jnp.clip(1.0 - a_bar_prev - sigma**2, 0.0, 1.0)) |
| x_prev = jnp.sqrt(a_bar_prev) * x0_hat + dir_coeff * eps_pred |
|
|
| |
| if eta > 0.0: |
| key, k = random.split(key) |
| z = random.normal(k, x.shape) |
| x_prev = x_prev + sigma * z |
|
|
| return (key, x_prev), x_prev |
|
|
| return step |
|
|
| def _t_schedule(self, t_start: int, *, steps: Optional[int] = None) -> jnp.ndarray: |
| """ |
| Descending integer timesteps from t_start -> 0. |
| |
| If steps is provided (or self.steps), we use a reduced DDIM schedule |
| by rounding a linear grid and making it unique. |
| """ |
| t_start = int(t_start) |
| if t_start <= 0: |
| return jnp.array([0], dtype=jnp.int32) |
|
|
| S = self.steps if steps is None else int(steps) |
| if S is None or S >= (t_start + 1): |
| return jnp.arange(t_start, -1, -1, dtype=jnp.int32) |
|
|
| grid = jnp.linspace(float(t_start), 0.0, int(S), dtype=jnp.float32) |
| ts = jnp.unique(jnp.round(grid).astype(jnp.int32)) |
| ts = ts[::-1] |
|
|
| |
| if ts[0] != t_start: |
| ts = jnp.concatenate([jnp.array([t_start], dtype=jnp.int32), ts]) |
| if ts[-1] != 0: |
| ts = jnp.concatenate([ts, jnp.array([0], dtype=jnp.int32)]) |
| return ts |
|
|
| def refine_latents( |
| self, |
| z0: jnp.ndarray, |
| t_start: int = 10, |
| key: Optional[jax.Array] = None, |
| add_noise: bool = True, |
| *, |
| eta: Optional[float] = None, |
| steps: Optional[int] = None, |
| ) -> jnp.ndarray: |
| """ |
| Refine latents by: |
| (optional) forward-noise z0 to step t_start |
| DDIM reverse from t_start -> 0 using EMA params. |
| """ |
| z0 = jnp.asarray(z0, dtype=jnp.float32) |
| if z0.ndim != 2 or z0.shape[1] != self.D: |
| raise ValueError(f"z0 must have shape (B,{self.D}).") |
| if not (0 <= int(t_start) < self.T): |
| raise ValueError(f"t_start must be in [0, {self.T-1}]") |
| t_start = int(t_start) |
|
|
| |
| if key is None: |
| self.key, key = random.split(self.key) |
| else: |
| |
| self.key, _ = random.split(key) |
|
|
| |
| if add_noise and t_start > 0: |
| key, k_eps = random.split(key) |
| eps_noise = random.normal(k_eps, z0.shape) |
| a_bar_t = jnp.clip(self.alpha_bar_s[t_start], self.eps, 1.0) |
| z_t = jnp.sqrt(a_bar_t) * z0 + jnp.sqrt(1.0 - a_bar_t) * eps_noise |
| else: |
| z_t = z0 |
|
|
| |
| ts = self._t_schedule(t_start, steps=steps) |
| if ts.shape[0] == 1: |
| return z_t |
|
|
| t_pairs = jnp.stack([ts[:-1], ts[1:]], axis=1) |
|
|
| step = self._make_ddim_step( |
| self.state.ema_params, |
| self.state.apply_fn, |
| self.alpha_bar_s, |
| self.eps, |
| self.eta if eta is None else float(eta), |
| ) |
|
|
| (final_key, _), trace = jax.lax.scan(step, (key, z_t), xs=t_pairs) |
|
|
| self.key = final_key |
| return trace[-1] |
|
|
| def __call__( |
| self, |
| z0: jnp.ndarray, |
| t_start: int = 10, |
| key: Optional[jax.Array] = None, |
| add_noise: bool = True, |
| *, |
| eta: Optional[float] = None, |
| steps: Optional[int] = None, |
| ) -> jnp.ndarray: |
| return self.refine_latents( |
| z0, t_start=t_start, key=key, add_noise=add_noise, eta=eta, steps=steps |
| ) |
|
|
| def reverse_from_T(self, x_T: jnp.ndarray, *, eta: Optional[float] = None, steps: Optional[int] = None) -> jnp.ndarray: |
| x_T = jnp.asarray(x_T, dtype=jnp.float32) |
| if x_T.ndim != 2 or x_T.shape[1] != self.D: |
| raise ValueError(f"x_T must have shape (B,{self.D}).") |
|
|
| self.key, k0 = random.split(self.key) |
|
|
| ts = self._t_schedule(self.T - 1, steps=steps) |
| t_pairs = jnp.stack([ts[:-1], ts[1:]], axis=1) |
|
|
| step = self._make_ddim_step( |
| self.state.ema_params, |
| self.state.apply_fn, |
| self.alpha_bar_s, |
| self.eps, |
| self.eta if eta is None else float(eta), |
| ) |
|
|
| (_, _), trace = jax.lax.scan(step, (k0, x_T), xs=t_pairs) |
| return trace[-1] |
|
|
| def sample(self, N: int = 10_000, *, eta: Optional[float] = None, steps: Optional[int] = None) -> jnp.ndarray: |
| self.key, k = random.split(self.key) |
| noise = random.normal(k, (int(N), self.D)) |
| return self.reverse_from_T(noise, eta=eta, steps=steps) |
|
|
| |
| |
| |
|
|
| def state_dict(self): |
| """ |
| Msgpack-safe checkpoint dict (includes DDIM-specific eta/steps). |
| Uses flax.serialization.to_state_dict for TrainStateEMA to avoid tuple issues (opt_state). |
| """ |
| from flax import serialization as flax_ser |
| import numpy as np |
|
|
| return dict( |
| |
| T=int(self.T), |
| D=int(self.D), |
| hidden_dim=int(getattr(self, "hidden_dim", 0) or self.hidden_dim), |
| t_embed_dim=int(getattr(self, "t_embed_dim", 0) or self.t_embed_dim), |
| learning_rate=float(self.learning_rate), |
| ema_decay=float(self.ema_decay), |
| beta_max=float(self.beta_max), |
| batch_size=None if (self.batch_size is None) else int(self.batch_size), |
| eps=float(self.eps), |
|
|
| |
| eta=float(self.eta), |
| steps=None if (self.steps is None) else int(self.steps), |
|
|
| |
| key=np.asarray(self.key), |
|
|
| |
| train_state=flax_ser.to_state_dict(self.state), |
| ) |
|
|
| def save_local(self, weights_file: str = "ddim.msgpack", config_file: Optional[str] = "ddim_config.json") -> None: |
| from flax import serialization as flax_ser |
| import json as _json |
|
|
| ckpt = self.state_dict() |
| blob = flax_ser.msgpack_serialize(ckpt) |
|
|
| with open(weights_file, "wb") as f: |
| f.write(blob) |
|
|
| if config_file is not None: |
| cfg = dict( |
| T=int(ckpt["T"]), |
| D=int(ckpt["D"]), |
| hidden_dim=int(ckpt["hidden_dim"]), |
| t_embed_dim=int(ckpt["t_embed_dim"]), |
| learning_rate=float(ckpt["learning_rate"]), |
| ema_decay=float(ckpt["ema_decay"]), |
| beta_max=float(ckpt["beta_max"]), |
| batch_size=ckpt["batch_size"], |
| eps=float(ckpt["eps"]), |
| eta=float(ckpt["eta"]), |
| steps=ckpt["steps"], |
| ) |
| with open(config_file, "w") as f: |
| _json.dump(cfg, f, indent=2) |
|
|
| @classmethod |
| def from_state(cls, ckpt, *, key: Optional[jax.Array] = None) -> "DDIM": |
| """ |
| Rehydrate a DDIM instance without retraining: |
| - rebuilds a skeleton DDIM (which builds schedules/model/optimizer) |
| - restores TrainStateEMA via flax.serialization.from_state_dict |
| """ |
| from flax import serialization as flax_ser |
| import numpy as np |
|
|
| T = int(ckpt["T"]) |
| D = int(ckpt["D"]) |
| hidden_dim = int(ckpt["hidden_dim"]) |
| t_embed_dim = int(ckpt["t_embed_dim"]) |
| learning_rate = float(ckpt["learning_rate"]) |
| ema_decay = float(ckpt["ema_decay"]) |
| beta_max = float(ckpt["beta_max"]) |
| batch_size = ckpt.get("batch_size", None) |
| eps = float(ckpt.get("eps", 1e-5)) |
|
|
| eta = float(ckpt.get("eta", 0.0)) |
| steps = ckpt.get("steps", None) |
| steps = None if (steps is None) else int(steps) |
|
|
| if key is None: |
| if "key" in ckpt: |
| key = jnp.asarray(np.asarray(ckpt["key"])) |
| else: |
| key = random.PRNGKey(0) |
|
|
| |
| dummy = jnp.zeros((1, D), dtype=jnp.float32) |
| obj = cls( |
| dummy, |
| T=T, |
| hidden_dim=hidden_dim, |
| t_embed_dim=t_embed_dim, |
| learning_rate=learning_rate, |
| n_iter=0, |
| ema_decay=ema_decay, |
| beta_max=beta_max, |
| batch_size=batch_size, |
| key=key, |
| verbose_every=0, |
| eps=eps, |
| eta=eta, |
| steps=steps, |
| ) |
|
|
| obj.state = flax_ser.from_state_dict(obj.state, ckpt["train_state"]) |
|
|
| |
| if "key" in ckpt: |
| obj.key = jnp.asarray(np.asarray(ckpt["key"])) |
| obj.eta = eta |
| obj.steps = steps |
|
|
| return obj |
|
|
| @classmethod |
| def load_local(cls, weights_file: str = "ddim.msgpack", *, key: Optional[jax.Array] = None) -> "DDIM": |
| from flax import serialization as flax_ser |
|
|
| with open(weights_file, "rb") as f: |
| ckpt = flax_ser.msgpack_restore(f.read()) |
| return cls.from_state(ckpt, key=key) |
|
|
| def upload_to_huggingface( |
| self, |
| repo_id: str, |
| *, |
| weights_file: str = "ddim.msgpack", |
| config_file: str = "ddim_config.json", |
| token: Optional[str] = None, |
| repo_type: str = "model", |
| revision: Optional[str] = None, |
| ) -> None: |
| """ |
| Upload DDIM checkpoint to HF Hub (msgpack + JSON). |
| Mirrors the DMAP/DDPM pattern. |
| """ |
| try: |
| from huggingface_hub import HfApi, HfFolder, upload_file |
| except Exception as e: |
| raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).") from e |
|
|
| self.save_local(weights_file=weights_file, config_file=config_file) |
|
|
| if token is None: |
| token = HfFolder.get_token() |
| if token is None: |
| raise RuntimeError("No HF token found. Run `huggingface-cli login`, or pass `token=...`.") |
|
|
| import os as _os |
|
|
| api = HfApi() |
| api.create_repo(repo_id=repo_id, repo_type=repo_type, exist_ok=True, token=token) |
|
|
| wf = _os.path.basename(weights_file) |
| cf = _os.path.basename(config_file) |
|
|
| upload_file( |
| path_or_fileobj=weights_file, |
| path_in_repo=wf, |
| repo_id=repo_id, |
| token=token, |
| repo_type=repo_type, |
| revision=revision, |
| ) |
| upload_file( |
| path_or_fileobj=config_file, |
| path_in_repo=cf, |
| repo_id=repo_id, |
| token=token, |
| repo_type=repo_type, |
| revision=revision, |
| ) |
|
|
| @classmethod |
| def download_from_huggingface( |
| cls, |
| repo_id: str, |
| *, |
| weights_file: str = "ddim.msgpack", |
| token: Optional[str] = None, |
| repo_type: str = "model", |
| revision: Optional[str] = None, |
| key: Optional[jax.Array] = None, |
| ) -> "DDIM": |
| """ |
| Download DDIM checkpoint from HF Hub and return a rehydrated DDIM instance. |
| """ |
| try: |
| from huggingface_hub import hf_hub_download |
| except Exception as e: |
| raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).") from e |
|
|
| path = hf_hub_download( |
| repo_id=repo_id, |
| filename=weights_file, |
| token=token, |
| repo_type=repo_type, |
| revision=revision, |
| ) |
| return cls.load_local(path, key=key) |
|
|
| |
| download_to_huggingface = download_from_huggingface |
|
|
|
|
| __all__ = ["DDPM", "DDIM", "EpsMLP", "cosine_schedule", "sinusoidal_embedding"] |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|