| import torch
|
| import torch.nn as nn
|
| from typing import Optional, Tuple, List, Union
|
| import numpy as np
|
| from tqdm import tqdm
|
| import math
|
|
|
|
|
| class DDIMSampler:
|
| """DDIM采样器"""
|
| def __init__(self, model: nn.Module, diffusion, num_inference_steps: int = 50):
|
| self.model = model
|
| self.diffusion = diffusion
|
| self.num_inference_steps = num_inference_steps
|
|
|
|
|
| self.set_timesteps(num_inference_steps)
|
|
|
| def set_timesteps(self, num_inference_steps: int):
|
| """设置推理时间步"""
|
| self.num_inference_steps = num_inference_steps
|
|
|
|
|
| if self.diffusion.num_train_timesteps == num_inference_steps:
|
| timesteps = np.arange(0, self.diffusion.num_train_timesteps)
|
| else:
|
| step_ratio = self.diffusion.num_train_timesteps // num_inference_steps
|
| timesteps = np.arange(0, self.diffusion.num_train_timesteps, step_ratio)
|
|
|
| self.timesteps = torch.from_numpy(timesteps).long().flip(0)
|
|
|
| @torch.no_grad()
|
| def step(
|
| self,
|
| model_output: torch.Tensor,
|
| timestep: int,
|
| sample: torch.Tensor,
|
| eta: float = 0.0,
|
| use_clipped_model_output: bool = False
|
| ) -> torch.Tensor:
|
| """DDIM单步采样"""
|
|
|
| prev_timestep = timestep - self.diffusion.num_train_timesteps // self.num_inference_steps
|
|
|
|
|
| alpha_prod_t = self.diffusion.extract(self.diffusion.alphas_cumprod, timestep, sample.shape)
|
| alpha_prod_t_prev = self.diffusion.extract(
|
| self.diffusion.alphas_cumprod,
|
| prev_timestep,
|
| sample.shape
|
| ) if prev_timestep >= 0 else torch.ones_like(alpha_prod_t)
|
|
|
|
|
| if self.diffusion.prediction_type == "epsilon":
|
| pred_original_sample = (sample - (1 - alpha_prod_t) ** 0.5 * model_output) / alpha_prod_t ** 0.5
|
| pred_epsilon = model_output
|
| elif self.diffusion.prediction_type == "sample":
|
| pred_original_sample = model_output
|
| pred_epsilon = (sample - alpha_prod_t ** 0.5 * pred_original_sample) / (1 - alpha_prod_t) ** 0.5
|
| elif self.diffusion.prediction_type == "v_prediction":
|
| pred_original_sample = (alpha_prod_t ** 0.5) * sample - (1 - alpha_prod_t) ** 0.5 * model_output
|
| pred_epsilon = (alpha_prod_t ** 0.5) * model_output + (1 - alpha_prod_t) ** 0.5 * sample
|
| else:
|
| raise ValueError(f"Unsupported prediction type: {self.diffusion.prediction_type}")
|
|
|
|
|
| if use_clipped_model_output:
|
| pred_original_sample = torch.clamp(pred_original_sample, -1, 1)
|
|
|
|
|
| variance = (1 - alpha_prod_t_prev) / (1 - alpha_prod_t) * (1 - alpha_prod_t / alpha_prod_t_prev)
|
| std_dev_t = eta * variance ** 0.5
|
|
|
|
|
| if eta > 0:
|
| noise = torch.randn_like(model_output)
|
| variance = std_dev_t ** 2
|
| else:
|
| noise = 0
|
| variance = 0
|
|
|
|
|
| pred_sample_direction = (1 - alpha_prod_t_prev - variance) ** 0.5 * pred_epsilon
|
| prev_sample = alpha_prod_t_prev ** 0.5 * pred_original_sample + pred_sample_direction
|
|
|
|
|
| if eta > 0:
|
| prev_sample = prev_sample + std_dev_t * noise
|
|
|
| return prev_sample
|
|
|
| @torch.no_grad()
|
| def sample(
|
| self,
|
| prompt_embeds: torch.Tensor,
|
| negative_prompt_embeds: Optional[torch.Tensor] = None,
|
| height: int = 512,
|
| width: int = 512,
|
| num_images_per_prompt: int = 1,
|
| guidance_scale: float = 7.5,
|
| eta: float = 0.0,
|
| generator: Optional[torch.Generator] = None,
|
| progress_bar: bool = True
|
| ) -> torch.Tensor:
|
| """生成样本"""
|
|
|
| self.model.eval()
|
|
|
|
|
| batch_size = prompt_embeds.shape[0]
|
|
|
|
|
| latents = torch.randn(
|
| (batch_size * num_images_per_prompt, self.model.in_channels, height // 8, width // 8),
|
| device=prompt_embeds.device,
|
| generator=generator
|
| )
|
|
|
|
|
| if negative_prompt_embeds is not None:
|
| prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
|
|
|
|
|
| do_classifier_free_guidance = guidance_scale > 1.0
|
| if do_classifier_free_guidance:
|
| latents = torch.cat([latents] * 2)
|
|
|
|
|
| timesteps = self.timesteps.to(latents.device)
|
|
|
| if progress_bar:
|
| timesteps = tqdm(timesteps, desc="DDIM Sampling")
|
|
|
| for i, t in enumerate(timesteps):
|
|
|
| latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
|
| latent_model_input = self.diffusion.scale_model_input(latent_model_input, t)
|
|
|
|
|
| noise_pred = self.model(latent_model_input, t, prompt_embeds)
|
|
|
|
|
| if do_classifier_free_guidance:
|
| noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
|
| noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
|
|
|
|
|
| latents = self.step(noise_pred, t, latents, eta)
|
|
|
| return latents
|
|
|
|
|
| class DPMSampler:
|
| """DPM采样器(更快)"""
|
| def __init__(self, model: nn.Module, diffusion, num_inference_steps: int = 20):
|
| self.model = model
|
| self.diffusion = diffusion
|
| self.num_inference_steps = num_inference_steps
|
|
|
| @torch.no_grad()
|
| def sample(
|
| self,
|
| prompt_embeds: torch.Tensor,
|
| height: int = 512,
|
| width: int = 512,
|
| guidance_scale: float = 7.5,
|
| progress_bar: bool = True
|
| ) -> torch.Tensor:
|
| """DPM采样"""
|
| self.model.eval()
|
|
|
|
|
| latents = torch.randn(
|
| (1, self.model.in_channels, height // 8, width // 8),
|
| device=prompt_embeds.device
|
| )
|
|
|
|
|
| timesteps = torch.linspace(1, 0, self.num_inference_steps + 1, device=latents.device)
|
|
|
| if progress_bar:
|
| timesteps_iter = tqdm(enumerate(timesteps[:-1]), total=len(timesteps)-1, desc="DPM Sampling")
|
| else:
|
| timesteps_iter = enumerate(timesteps[:-1])
|
|
|
| for i, t in timesteps_iter:
|
|
|
| noise_pred = self.model(latents, t.unsqueeze(0) * 999, prompt_embeds)
|
|
|
|
|
| if guidance_scale > 1.0:
|
|
|
| noise_pred = noise_pred * guidance_scale
|
|
|
|
|
| dt = timesteps[i + 1] - t
|
| latents = latents + dt * noise_pred
|
|
|
| return latents
|
|
|
|
|
| class LCMSampler:
|
| """LCM(潜在一致性模型)采样器,极快"""
|
| def __init__(self, model: nn.Module, diffusion, num_inference_steps: int = 4):
|
| self.model = model
|
| self.diffusion = diffusion
|
| self.num_inference_steps = num_inference_steps
|
|
|
|
|
| self.c_skip = 1.0
|
| self.c_out = 1.0
|
| self.c_in = 1.0
|
| self.c_noise = 1.0
|
|
|
| @torch.no_grad()
|
| def sample(
|
| self,
|
| prompt_embeds: torch.Tensor,
|
| height: int = 512,
|
| width: int = 512,
|
| guidance_scale: float = 7.5,
|
| progress_bar: bool = True
|
| ) -> torch.Tensor:
|
| """LCM采样(极快,只需要4-8步)"""
|
| self.model.eval()
|
|
|
|
|
| latents = torch.randn(
|
| (1, self.model.in_channels, height // 8, width // 8),
|
| device=prompt_embeds.device
|
| )
|
|
|
|
|
| timesteps = torch.linspace(1, 0, self.num_inference_steps + 1, device=latents.device)
|
|
|
| if progress_bar:
|
| timesteps_iter = tqdm(enumerate(timesteps[:-1]), total=len(timesteps)-1, desc="LCM Sampling")
|
| else:
|
| timesteps_iter = enumerate(timesteps[:-1])
|
|
|
| for i, t in timesteps_iter:
|
|
|
| c_skip = self.c_skip
|
| c_out = self.c_out
|
| c_in = self.c_in
|
| c_noise = self.c_noise
|
|
|
|
|
| scaled_latents = c_in * latents
|
|
|
|
|
| noise_pred = self.model(scaled_latents, c_noise * t.unsqueeze(0), prompt_embeds)
|
|
|
|
|
| denoised = c_skip * latents + c_out * noise_pred
|
|
|
|
|
| dt = timesteps[i + 1] - t
|
| latents = denoised + dt * noise_pred
|
|
|
| return latents
|
|
|
|
|
| class SamplerFactory:
|
| """采样器工厂"""
|
| @staticmethod
|
| def create_sampler(
|
| sampler_type: str,
|
| model: nn.Module,
|
| diffusion,
|
| num_inference_steps: int = 50
|
| ):
|
| """创建采样器"""
|
| if sampler_type == "ddim":
|
| return DDIMSampler(model, diffusion, num_inference_steps)
|
| elif sampler_type == "dpm":
|
| return DPMSampler(model, diffusion, num_inference_steps)
|
| elif sampler_type == "lcm":
|
| return LCMSampler(model, diffusion, num_inference_steps)
|
| else:
|
| raise ValueError(f"未知的采样器类型: {sampler_type}")
|
|
|
|
|
| class TextToImagePipeline:
|
| """文本到图像管道"""
|
| def __init__(
|
| self,
|
| model: nn.Module,
|
| diffusion,
|
| text_encoder,
|
| vae_decoder,
|
| sampler_type: str = "ddim",
|
| device: str = "cuda"
|
| ):
|
| self.model = model.to(device)
|
| self.diffusion = diffusion
|
| self.text_encoder = text_encoder
|
| self.vae_decoder = vae_decoder
|
| self.sampler_type = sampler_type
|
| self.device = device
|
|
|
|
|
| self.sampler = SamplerFactory.create_sampler(
|
| sampler_type, model, diffusion
|
| )
|
|
|
|
|
| self.model.eval()
|
| if self.vae_decoder is not None:
|
| self.vae_decoder.eval()
|
|
|
| @torch.no_grad()
|
| def __call__(
|
| self,
|
| prompt: str,
|
| negative_prompt: str = "",
|
| height: int = 512,
|
| width: int = 512,
|
| num_inference_steps: int = 50,
|
| guidance_scale: float = 7.5,
|
| num_images: int = 1,
|
| seed: Optional[int] = None,
|
| progress_bar: bool = True
|
| ) -> List:
|
| """生成图像"""
|
|
|
| if seed is not None:
|
| torch.manual_seed(seed)
|
| if torch.cuda.is_available():
|
| torch.cuda.manual_seed(seed)
|
|
|
|
|
| prompt_embeds = self.text_encoder.encode([prompt]).to(self.device)
|
| negative_prompt_embeds = None
|
|
|
| if negative_prompt:
|
| negative_prompt_embeds = self.text_encoder.encode([negative_prompt]).to(self.device)
|
|
|
|
|
| latents = self.sampler.sample(
|
| prompt_embeds=prompt_embeds,
|
| negative_prompt_embeds=negative_prompt_embeds,
|
| height=height,
|
| width=width,
|
| num_images_per_prompt=num_images,
|
| guidance_scale=guidance_scale,
|
| progress_bar=progress_bar
|
| )
|
|
|
|
|
| images = []
|
| for i in range(num_images):
|
| latent = latents[i:i+1]
|
|
|
| if self.vae_decoder is not None:
|
| image = self.vae_decoder(latent)
|
| else:
|
|
|
| image = latent
|
|
|
| images.append(image.cpu())
|
|
|
| return images
|
|
|
| def generate_grid(
|
| self,
|
| prompts: List[str],
|
| grid_size: Tuple[int, int] = (2, 2),
|
| **kwargs
|
| ) -> torch.Tensor:
|
| """生成图像网格"""
|
| images = []
|
|
|
| for prompt in prompts[:grid_size[0] * grid_size[1]]:
|
| image = self(prompt, **kwargs)[0]
|
| images.append(image)
|
|
|
|
|
| from torchvision.utils import make_grid
|
| grid = make_grid(torch.cat(images, dim=0), nrow=grid_size[1])
|
|
|
| return grid
|
|
|
|
|
| def test_sampler():
|
| """测试采样器"""
|
| import torch.nn as nn
|
|
|
|
|
| class MockModel(nn.Module):
|
| def __init__(self):
|
| super().__init__()
|
| self.in_channels = 4
|
|
|
| def forward(self, x, t, context):
|
|
|
| return torch.randn_like(x)
|
|
|
|
|
| class MockDiffusion:
|
| def __init__(self):
|
| self.num_train_timesteps = 1000
|
| self.alphas_cumprod = torch.ones(1000)
|
| self.prediction_type = "epsilon"
|
|
|
| def extract(self, a, t, x_shape):
|
| return torch.ones(x_shape[0], 1, 1, 1)
|
|
|
| def scale_model_input(self, x, t):
|
| return x
|
|
|
| model = MockModel()
|
| diffusion = MockDiffusion()
|
|
|
|
|
| sampler = DDIMSampler(model, diffusion, num_inference_steps=10)
|
|
|
|
|
| prompt_embeds = torch.randn(1, 77, 768)
|
| latents = sampler.sample(prompt_embeds, height=64, width=64, progress_bar=False)
|
|
|
| print(f"DDIM采样完成,潜在表示形状: {latents.shape}")
|
|
|
| return sampler, latents
|
|
|
|
|
| if __name__ == '__main__':
|
| test_sampler() |