# Fast CLIP-guided diffusion. Same math as the repo's CLIP-guided diffusion baseline # (OpenAI 256/512 uncond model, p_sample + cond_fn nudging each step toward CLIP text), # restructured for modern GPUs: one UNet forward per step instead of two (p_mean_variance # and cond_fn share the same deterministic model output), bf16 conv torso, branch-free # fixed-shape cutouts with randomness passed in as tensors, and one full timestep # (UNet + guidance grad + DDPM update) as a single functional graph (torch.func.vjp/grad) # that torch.compile's or AOTI-compiles via aokit (warm start = load-only, no JIT). import argparse import sys import time import numpy as np import torch import torch.nn as nn import torch.nn.functional as F from PIL import Image from torchvision.transforms import functional as TF sys.path.append('guided-diffusion') _argv = sys.argv sys.argv = [sys.argv[0]] # generate_fast parses CLI args at import from generate_fast import clip, aoti_build_or_load, convert_clip_visual # noqa: E402 sys.argv = _argv from guided_diffusion.script_util import create_model_and_diffusion, model_and_diffusion_defaults # noqa: E402 torch.backends.cudnn.benchmark = True torch.set_float32_matmul_precision('high') device = torch.device('cuda:0') def parse(): p = argparse.ArgumentParser(description='Fast CLIP-guided diffusion') p.add_argument('-p', '--prompt', type=str, default='a photograph of a lighthouse in a storm', dest='prompt') p.add_argument('--steps', type=str, default='250', help='timestep_respacing (e.g. 250, 100)') p.add_argument('--image_size', type=int, default=512, help='256 or 512 (match the model)') p.add_argument('-m', '--clip_model', type=str, default='ViT-B/16', dest='clip_model') p.add_argument('--clip_guidance_scale', type=float, default=1000.) p.add_argument('--tv_scale', type=float, default=150.) p.add_argument('--range_scale', type=float, default=50.) p.add_argument('-cuts', '--num_cuts', type=int, default=16, dest='cutn') p.add_argument('--cut_pow', type=float, default=1.) p.add_argument('-sd', '--seed', type=int, default=0, dest='seed') p.add_argument('-o', '--output', type=str, default='diffusion_fast.png', dest='output') p.add_argument('-se', '--save_every', type=int, default=50, dest='save_every') p.add_argument('--model', type=str, default=None, help='checkpoint path (auto by image_size)') p.add_argument('--backend', choices=['baseline', 'eager', 'compile', 'aoti'], default='aoti', dest='backend') p.add_argument('--aoti_cache', type=str, default='aoti_cache', dest='aoti_cache') p.add_argument('--fp32', action='store_true', help='disable bf16 for the UNet/CLIP') p.add_argument('--eval', action='store_true', help='print fp32 CLIP score of the final image') return p.parse_args() MODEL_CFG = { 'attention_resolutions': '32, 16, 8', 'class_cond': False, 'diffusion_steps': 1000, 'rescale_timesteps': True, 'learn_sigma': True, 'noise_schedule': 'linear', 'num_channels': 256, 'num_head_channels': 64, 'num_res_blocks': 2, 'resblock_updown': True, 'use_fp16': False, 'use_scale_shift_norm': True, } def load_diffusion(image_size, steps, ckpt): cfg = model_and_diffusion_defaults() cfg.update(MODEL_CFG) cfg.update({'image_size': image_size, 'timestep_respacing': str(steps)}) model, diffusion = create_model_and_diffusion(**cfg) model.load_state_dict(torch.load(ckpt, map_location='cpu')) model.requires_grad_(False).eval().to(device) return model, diffusion def convert_unet_bf16(model): # bf16 analog of convert_to_fp16: conv torso in bf16, norms/linears/out stay fp32 for m in [model.input_blocks, model.middle_block, model.output_blocks]: for l in m.modules(): if isinstance(l, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): l.weight.data = l.weight.data.bfloat16() if l.bias is not None: l.bias.data = l.bias.data.bfloat16() model.dtype = torch.bfloat16 return model def tv_loss(inp): inp = F.pad(inp, (0, 1, 0, 1), 'replicate') x = inp[..., :-1, 1:] - inp[..., :-1, :-1] y = inp[..., 1:, :-1] - inp[..., :-1, :-1] return (x ** 2 + y ** 2).mean([1, 2, 3]) def range_loss(inp): return (inp - inp.clamp(-1, 1)).pow(2).mean([1, 2, 3]) class FusedStep(nn.Module): """One full CLIP-guided DDPM timestep as a pure graph. The respaced schedule lives on the host: each step's scalars (UNet timestep value, alpha/posterior coefs, log-variance bounds, t!=0 noise mask) and the guidance scales enter as tensor inputs -> one AOTI package per (image_size, cutn, dtype) serves any steps/scales.""" def __init__(self, model, clip_visual, cutn, cut_size, image_size, heavy_dtype): super().__init__() self.model = convert_unet_bf16(model) if heavy_dtype == torch.bfloat16 else model self.visual = convert_clip_visual(clip_visual.float(), heavy_dtype) self.cutn, self.cut_size, self.image_size = cutn, cut_size, image_size self.heavy_dtype = heavy_dtype self.register_buffer('norm_mean', torch.tensor([0.48145466, 0.4578275, 0.40821073]).view(1, 3, 1, 1)) self.register_buffer('norm_std', torch.tensor([0.26862954, 0.26130258, 0.27577711]).view(1, 3, 1, 1)) self.register_buffer('lin', torch.arange(cut_size).float()) def make_cutouts(self, img, size, offx, offy): # random resized crop (branch-free, fixed cut_size output) via grid_sample; # size/offx/offy sampled to match MakeCutouts(cut_pow) distribution cs, n = self.cut_size, self.cutn H = W = self.image_size px = offx[:, None] + (self.lin[None, :] + 0.5) * size[:, None] / cs - 0.5 # [n, cs] py = offy[:, None] + (self.lin[None, :] + 0.5) * size[:, None] / cs - 0.5 gx = (px + 0.5) * 2 / W - 1 gy = (py + 0.5) * 2 / H - 1 grid = torch.stack([gx[:, None, :].expand(n, cs, cs), gy[:, :, None].expand(n, cs, cs)], dim=-1) # [n, cs, cs, 2] batch = img.expand(n, -1, -1, -1) return F.grid_sample(batch, grid, mode='bilinear', padding_mode='border', align_corners=False) def _model_output(self, x, model_ts): out = self.model(x, model_ts) eps, var_values = torch.split(out, 3, dim=1) return eps.float(), var_values.float() def producer(self, x, model_ts, sqrt_recip, sqrt_recipm1, fac): eps, var_values = self._model_output(x, model_ts) pred_xstart = sqrt_recip * x - sqrt_recipm1 * eps x_in = pred_xstart * fac + x * (1 - fac) return (x_in, pred_xstart), var_values def guide(self, x_in, embeds, weights, size, offx, offy, cgs, tvs): x01 = x_in.add(1).div(2) cuts = self.make_cutouts(x01, size, offx, offy) cuts = (cuts - self.norm_mean) / self.norm_std emb = self.visual(cuts.to(self.heavy_dtype)).float() img_n = F.normalize(emb, dim=-1) # [cutn, D] txt_n = F.normalize(embeds, dim=-1) # [P, D] dists = (img_n.unsqueeze(1) - txt_n.unsqueeze(0)).norm(dim=-1).div(2).arcsin().pow(2).mul(2) # [cutn, P] losses = (dists * weights).sum(1).mean(0) return losses * cgs + tv_loss(x_in).sum() * tvs def forward(self, x, model_ts, sqrt_recip, sqrt_recipm1, fac, pmc1, pmc2, min_log, max_log, nonzero, embeds, weights, cgs, tvs, rs, noise, size, offx, offy): (x_in, pred_xstart), vjp_fn, var_values = torch.func.vjp( lambda xx: self.producer(xx, model_ts, sqrt_recip, sqrt_recipm1, fac), x, has_aux=True) g_xin = torch.func.grad( lambda xi: self.guide(xi, embeds, weights, size, offx, offy, cgs, tvs))(x_in) # range_loss(pred_xstart) cotangent in closed form (d/dp (p-clamp(p))^2 = 2(p-clamp(p))); # note the baseline's range term was dead code (grad of a non-descendant wrt x_in = 0), # this applies the intended stabilizer; rs=0 reproduces the old graph exactly g_pred = rs * 2 * (pred_xstart - pred_xstart.clamp(-1, 1)) / pred_xstart[0].numel() guidance = -vjp_fn((g_xin, g_pred))[0] # -grad of guidance loss wrt x # LEARNED_RANGE variance from the same forward frac = (var_values + 1) / 2 model_log_variance = frac * max_log + (1 - frac) * min_log model_variance = torch.exp(model_log_variance) model_mean = pmc1 * pred_xstart + pmc2 * x new_mean = model_mean + model_variance * guidance sample = new_mean + nonzero * torch.exp(0.5 * model_log_variance) * noise return sample, pred_xstart def make_sched(diffusion, dev): # host-side respaced schedule tables; per-step scalars are fed to the graph as inputs d = diffusion def T(a): return torch.tensor(np.asarray(a), dtype=torch.float32, device=dev) model_ts = T(d.timestep_map) if d.rescale_timesteps: model_ts = model_ts * (1000.0 / d.original_num_steps) nz = torch.ones(d.num_timesteps, device=dev) nz[0] = 0. return { 'model_ts': model_ts, 'sqrt_recip': T(d.sqrt_recip_alphas_cumprod), 'sqrt_recipm1': T(d.sqrt_recipm1_alphas_cumprod), 'fac': T(d.sqrt_one_minus_alphas_cumprod), 'pmc1': T(d.posterior_mean_coef1), 'pmc2': T(d.posterior_mean_coef2), 'min_log': T(d.posterior_log_variance_clipped), 'max_log': T(np.log(d.betas)), 'nz': nz, } def sched_at(s, i): return (s['model_ts'][i:i + 1], s['sqrt_recip'][i], s['sqrt_recipm1'][i], s['fac'][i], s['pmc1'][i], s['pmc2'][i], s['min_log'][i], s['max_log'][i], s['nz'][i]) def sample_step_rands(gen, cutn, cut_size, image_size, cut_pow, shape): max_size, min_size = image_size, min(image_size, cut_size) u = torch.rand(cutn, generator=gen, device=device) size = (u.pow(cut_pow) * (max_size - min_size) + min_size).floor() offx = (torch.rand(cutn, generator=gen, device=device) * (image_size - size + 1)).floor() offy = (torch.rand(cutn, generator=gen, device=device) * (image_size - size + 1)).floor() noise = torch.randn(shape, generator=gen, device=device) return noise, size, offx, offy def encode_text(perceptor, prompt): embeds, weights = [], [] for part in prompt.split('|'): part = part.strip() if not part: continue vals = part.rsplit(':', 1) txt = vals[0] w = float(vals[1]) if len(vals) == 2 else 1.0 with torch.no_grad(): embeds.append(perceptor.encode_text(clip.tokenize(txt).to(device)).float()[0]) weights.append(w) embeds = torch.stack(embeds) weights = torch.tensor(weights, device=device) weights = weights / weights.sum().abs() return embeds, weights # ---------- baseline (reference): literal 2-forward fp32 loop via the diffusion object ---------- def run_baseline(args, model, diffusion, perceptor, cut_size, embeds, weights): mean = torch.tensor([0.48145466, 0.4578275, 0.40821073], device=device).view(1, 3, 1, 1) std = torch.tensor([0.26862954, 0.26130258, 0.27577711], device=device).view(1, 3, 1, 1) isz, cutn, cp = args.image_size, args.cutn, args.cut_pow def make_cutouts(img): cuts = [] for _ in range(cutn): size = int(torch.rand([])**cp * (isz - min(isz, cut_size)) + min(isz, cut_size)) ox = torch.randint(0, isz - size + 1, ()) oy = torch.randint(0, isz - size + 1, ()) c = img[:, :, oy:oy + size, ox:ox + size] cuts.append(F.adaptive_avg_pool2d(c, cut_size)) return torch.cat(cuts) total = diffusion.num_timesteps cur_t = [0] def cond_fn(x, t, y=None): with torch.enable_grad(): x = x.detach().requires_grad_() my_t = torch.ones([x.shape[0]], device=device, dtype=torch.long) * cur_t[0] out = diffusion.p_mean_variance(model, x, my_t, clip_denoised=False, model_kwargs={}) fac = diffusion.sqrt_one_minus_alphas_cumprod[cur_t[0]] x_in = out['pred_xstart'] * fac + x * (1 - fac) clip_in = (make_cutouts(x_in.add(1).div(2)) - mean) / std image_embeds = perceptor.encode_image(clip_in).float() d = (F.normalize(image_embeds.unsqueeze(1), dim=-1) - F.normalize(embeds.unsqueeze(0), dim=-1)) dists = d.norm(dim=-1).div(2).arcsin().pow(2).mul(2) losses = (dists * weights).sum(1).mean(0) x_in_grad = torch.autograd.grad(losses.sum() * args.clip_guidance_scale, x_in)[0] loss = tv_loss(x_in).sum() * args.tv_scale + range_loss(out['pred_xstart']).sum() * args.range_scale x_in_grad = x_in_grad + torch.autograd.grad(loss, x_in)[0] return -torch.autograd.grad(x_in, x, x_in_grad)[0] cur_t[0] = diffusion.num_timesteps - 1 t0 = None img = None for j, sample in enumerate(diffusion.p_sample_loop_progressive( model, (1, 3, isz, isz), clip_denoised=False, model_kwargs={}, cond_fn=cond_fn, progress=False, randomize_class=False)): if j == 5: torch.cuda.synchronize(); t0 = time.time() cur_t[0] -= 1 img = sample['pred_xstart'] if j % args.save_every == 0: TF.to_pil_image(img.add(1).div(2).clamp(0, 1)[0].cpu()).save(args.output) torch.cuda.synchronize() n = total - 5 print(f'baseline: {n} steps in {time.time()-t0:.2f}s = {n/(time.time()-t0):.2f} step/s (fp32, {isz}px)') final = img.add(1).div(2).clamp(0, 1) TF.to_pil_image(final[0].cpu()).save(args.output) return final # ---------- fast path (eager / compile / aoti) ---------- def build_engine(args, step_mod, embeds, weights): # steps/scales are NOT in the key: one package per (clip, ckpt, size, cutn, dtype) isz = args.image_size shape = (1, 3, isz, isz) gen = torch.Generator(device=device).manual_seed(0) ex = sample_step_rands(gen, args.cutn, step_mod.cut_size, isz, args.cut_pow, shape) # every example input must be a DISTINCT tensor object: make_fx binds repeated # tensors to one placeholder, silently miswiring the runtime inputs def s(v): return torch.full((), float(v), device=device) ex_sched = (torch.full((1,), 999., device=device), s(1.1), s(1.2), s(0.9), s(0.5), s(0.6), s(-9.), s(-8.), s(1.)) ex_args = (torch.randn(shape, device=device), *ex_sched, embeds, weights, s(1000.), s(150.), s(50.), *ex) aoti = aoti_build_or_load('diffstep3-dyn', lambda *a: step_mod(*a), ex_args, (args.clip_model, args.model, isz, args.cutn, str(step_mod.heavy_dtype)), args.aoti_cache) return lambda *a: aoti(*a) def run_fast(args, step_mod, diffusion, embeds, weights): isz = args.image_size total = diffusion.num_timesteps shape = (1, 3, isz, isz) if args.backend == 'compile': step_mod.forward = torch.compile(step_mod.forward, fullgraph=True) step_call = step_mod elif args.backend == 'aoti': step_call = build_engine(args, step_mod, embeds, weights) else: step_call = step_mod sched = make_sched(diffusion, device) cgs = torch.tensor(args.clip_guidance_scale, device=device) tvs = torch.tensor(args.tv_scale, device=device) rs = torch.tensor(args.range_scale, device=device) gen = torch.Generator(device=device).manual_seed(args.seed) x = torch.randn(shape, generator=gen, device=device) t0 = None img = None for j, i in enumerate(reversed(range(total))): noise, size, offx, offy = sample_step_rands(gen, args.cutn, step_mod.cut_size, isz, args.cut_pow, shape) x, pred_xstart = step_call(x, *sched_at(sched, i), embeds, weights, cgs, tvs, rs, noise, size, offx, offy) x = x.contiguous() img = pred_xstart if j == 5: torch.cuda.synchronize(); t0 = time.time() if j % args.save_every == 0: TF.to_pil_image(img.add(1).div(2).clamp(0, 1)[0].float().cpu()).save(args.output) torch.cuda.synchronize() n = total - 5 dt = time.time() - t0 print(f'{args.backend}: {n} steps in {dt:.2f}s = {n/dt:.2f} step/s ' f'({"fp32" if args.fp32 else "bf16"}, {isz}px, cutn {args.cutn})') final = img.add(1).div(2).clamp(0, 1).float() TF.to_pil_image(final[0].cpu()).save(args.output) return final @torch.no_grad() def clip_score(clip_model, image01, prompt): # clean fp32 CLIP cosine of the final image vs the prompt (fresh model: the main # perceptor's visual was mutated to bf16 in place by convert_clip_visual) perceptor = clip.load(clip_model, jit=False)[0].eval().float().to(device) txt = perceptor.encode_text(clip.tokenize(prompt.split('|')[0].rsplit(':', 1)[0]).to(device)).float() img = F.interpolate(image01, 224, mode='bicubic', align_corners=False) mean = torch.tensor([0.48145466, 0.4578275, 0.40821073], device=device).view(1, 3, 1, 1) std = torch.tensor([0.26862954, 0.26130258, 0.27577711], device=device).view(1, 3, 1, 1) emb = perceptor.encode_image((img - mean) / std).float() return F.cosine_similarity(F.normalize(emb, dim=-1), F.normalize(txt, dim=-1)).item() def main(): args = parse() if args.model is None: args.model = ('checkpoints/256x256_diffusion_uncond.pt' if args.image_size == 256 else 'checkpoints/512x512_diffusion_uncond_finetune_008100.pt') heavy_dtype = torch.float32 if args.fp32 else torch.bfloat16 torch.manual_seed(args.seed) print(f'CLIP-guided diffusion | {args.image_size}px | steps {args.steps} | backend {args.backend} | ' f'cgs {args.clip_guidance_scale} tv {args.tv_scale} range {args.range_scale}') model, diffusion = load_diffusion(args.image_size, args.steps, args.model) perceptor = clip.load(args.clip_model, jit=False)[0].eval().requires_grad_(False).to(device) cut_size = perceptor.visual.input_resolution embeds, weights = encode_text(perceptor, args.prompt) print(f'prompt: {args.prompt}') if args.backend == 'baseline': final = run_baseline(args, model, diffusion, perceptor, cut_size, embeds, weights) else: step_mod = FusedStep(model, perceptor.visual, args.cutn, cut_size, args.image_size, heavy_dtype).to(device).eval() final = run_fast(args, step_mod, diffusion, embeds, weights) print(f'saved {args.output}') if args.eval: print(f'CLIP score (fp32, vs prompt): {clip_score(args.clip_model, final, args.prompt):.4f}') if __name__ == '__main__': main()