Stable Diffusion:架构与微调 本节摘要:Stable Diffusion 本质上是一个跑到 VAE 隐空间里去的 DDPM——文本通过交叉注意力注入条件,采样用快速确定性 ODE 求解器,方向由无分类器引导(Classifier-Free Guidance)把控。本节把这条流水线拆成五块(VAE、文本编码器、U-Net、调度器、安全检查器),讲清「隐空间扩散」为何能把算力砍掉 48 倍、CFG 的 为什么是文生图能上生产的命门、LoRA 如何只动 1050 MB 就把风格改头换面。读完本节,你能用 跑通文生图、图生图、内绘、ControlNet,并把一个自定义 LoRA 训出来、加载进去。 对应原课程:Phase 4 · Lesson 11 · (原英文 )。
本节摘要:Stable Diffusion 本质上是一个跑到 VAE 隐空间里去的 DDPM——文本通过交叉注意力注入条件,采样用快速确定性 ODE 求解器,方向由无分类器引导(Classifier-Free Guidance)把控。本节把这条流水线拆成五块(VAE、文本编码器、U-Net、调度器、安全检查器),讲清「隐空间扩散」为何能把算力砍掉 48 倍、CFG 的
eps_uncond + w·(eps_cond − eps_uncond)为什么是文生图能上生产的命门、LoRA 如何只动 10~50 MB 就把风格改头换面。读完本节,你能用diffusers跑通文生图、图生图、内绘、ControlNet,并把一个自定义 LoRA 训出来、加载进去。
对应原课程:Phase 4 · Lesson 11 ·
stable-diffusion(原英文phases/04-computer-vision/11-stable-diffusion/docs/en.md)。
阅读完本节,你应当能够:
diffusers 跑通文生图、图生图、内绘、ControlNet 引导生成。直接在 512×512 RGB 图上训 DDPM 太贵。每个训练步都要把一个看到 3×512×512 = 786432 个输入值的 U-Net 反向传播一次,采样还要在同一个 U-Net 上跑 50+ 次前向。要达到 Stable Diffusion 1.5(2022 年发布)那种画质,像素空间扩散大约需要 256 GPU-月训练,且消费级 GPU 上每张图要 10~30 秒。
让开源文生图变得可行的,是隐空间扩散(Latent Diffusion,Rombach 等,CVPR 2022)。先训一个 VAE,把 3×512×512 的图映到 4×64×64 的隐张量并能还原回来,然后在这个隐空间里做扩散。算力直降 (3·512·512)/(4·64·64) = 48 倍。采样在同一块 GPU 上从几十秒掉到两秒以内。
今天几乎所有图像生成模型——SDXL、SD3、FLUX、HunyuanDiT、Wan-Video——都是隐空间扩散模型,只在自编码器、去噪器(U-Net 或 DiT)和文本条件这三处有变体。学会 Stable Diffusion,就学会了这套模板。
朴素文本条件让网络对每个提示 c 学 epsilon_theta(x_t, t, c)。CFG 在训练时以 10% 概率丢弃 c(换成空嵌入),于是同一个模型既能预测条件噪声也能预测无条件噪声。推理时:
eps = eps_uncond + w * (eps_cond - eps_uncond)
w 是引导尺度。w=0 纯无条件,w=1 朴素条件,w>1 把输出推向「更贴合提示」但牺牲多样性。SD 默认 w=7.5。
CFG 是文生图能上生产的根本原因。没有它,提示对输出的偏置很弱;有了它,提示说了算。
VAE 的 4 通道隐变量不只是压缩图。它是一个流形——上面做算术大致对应语义编辑(提示工程和插值都活在这里),扩散 U-Net 也把全部建模预算花在了这个流形上。解码一个随机的 4×64×64 隐变量不会得到一张随机噪声图,而是垃圾,因为只有某个特定子流形上的隐变量才能解码出有效图像。
两个推论:
SD 的 U-Net 就是第 10 节 TinyUNet 的放大版,加了三样东西:
SD 1.5 总参数约 8.6 亿,SDXL 约 26 亿,FLUX 约 120 亿。参数暴涨主要在注意力层。
全参微调 Stable Diffusion 要 20+ GB 显存,且要更新 8.6 亿参数。LoRA(Low-Rank Adaptation)冻结基础模型,只在注意力层注入小的低秩分解矩阵。SD 的一个 LoRA 适配器通常 1050 MB,单张消费级 GPU 上 1060 分钟训完,推理时即插即用。
原始: W_q : (d_in, d_out) 冻结 LoRA: W_q + alpha * (A @ B) 其中 A : (d_in, r), B : (r, d_out) r 通常取 4~32。
几乎所有社区微调都以 LoRA 形式分发。CivitAI 和 Hugging Face 上有上百万个。
换调度器在 diffusers 里是一行改动,有时无需重训就能修好采样瑕疵。
本节全程用 diffusers,不从头重建 Stable Diffusion。要重建的那几块(VAE、文本编码器、U-Net、调度器)各有自己的章节;本节目的就是熟练掌握生产 API。
import torch from diffusers import StableDiffusionPipeline pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16, ).to("cuda") image = pipe( prompt="a dog riding a skateboard in tokyo, studio ghibli style", guidance_scale=7.5, num_inference_steps=25, generator=torch.Generator("cuda").manual_seed(42), ).images[0] image.save("dog.png")
float16 把显存砍半,画质肉眼无损。默认 DPM-Solver++ 下 25 步等同于 DDIM 的 50 步。
from diffusers import DPMSolverMultistepScheduler, EulerAncestralDiscreteScheduler pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
调度器状态与 U-Net 权重解耦。可以拿 DDPM 训练,用任意调度器采样。
from diffusers import StableDiffusionImg2ImgPipeline from PIL import Image img2img = StableDiffusionImg2ImgPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16, ).to("cuda") init_image = Image.open("dog.png").convert("RGB").resize((512, 512)) out = img2img( prompt="a dog riding a skateboard, oil painting", image=init_image, strength=0.6, guidance_scale=7.5, ).images[0]
strength 是去噪前要加多少噪声(0.0 = 不变,1.0 = 完全重生成)。风格迁移通常取 0.5~0.7。
from diffusers import StableDiffusionInpaintPipeline inpaint = StableDiffusionInpaintPipeline.from_pretrained( "runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16, ).to("cuda") image = Image.open("dog.png").convert("RGB").resize((512, 512)) mask = Image.open("dog_mask.png").convert("L").resize((512, 512)) out = inpaint( prompt="a cat", image=image, mask_image=mask, guidance_scale=7.5, ).images[0]
掩码里白像素是要重生成的区域,黑像素保留。
pipe.load_lora_weights("sayakpaul/sd-lora-ghibli") pipe.fuse_lora(lora_scale=0.8) image = pipe(prompt="a village square in ghibli style").images[0]
lora_scale 控制强度;0.0 = 无效果,1.0 = 满效果。fuse_lora 把适配器就地烤进权重以提速,但就不能再换;要换别的适配器前先调 pipe.unfuse_lora()。
真正的 LoRA 训练在 peft 或 diffusers.training 里。轮廓如下:
# 伪代码 for step, batch in enumerate(dataloader): images, prompts = batch latents = vae.encode(images).latent_dist.sample() * 0.18215 t = torch.randint(0, num_train_timesteps, (batch_size,)) noise = torch.randn_like(latents) noisy_latents = scheduler.add_noise(latents, noise, t) text_emb = text_encoder(tokenizer(prompts)) pred_noise = unet(noisy_latents, t, text_emb) # LoRA 权重在这里注入 loss = F.mse_loss(pred_noise, noise) loss.backward() optimizer.step()
只有 LoRA 矩阵收梯度;基础 U-Net、VAE、文本编码器都冻结。批大小为 1、开梯度检查点,8 GB 显存就够。
生产里真正要做的决策:
float16,A100 及更新用 bfloat16,显存吃紧时上 int8(经 bitsandbytes 或 compel)。批量生成用社区工具 AUTO1111 / ComfyUI;生产 API 用 diffusers + accelerate,或 optimum-nvidia 配 TensorRT 编译。
本节产出两个可复用文件(位于原课程 outputs/):
prompt-sd-pipeline-planner.md:一个提示词——给定延迟预算、画质目标和授权约束,在 SD 1.5 / SDXL / SD3 / FLUX 之间挑,并配好调度器和精度。skill-lora-training-setup.md:一个技能——为自定义数据集写出完整 LoRA 训练配置,含 caption、秩、批大小、学习率。guidance_scale 取 [1, 3, 5, 7.5, 10, 15] 各生成一张。描述图像怎么变。伪影在哪个引导值开始出现?StableDiffusionImg2ImgPipeline 上把 strength 取 [0.2, 0.4, 0.6, 0.8, 1.0] 各跑一遍。哪个 strength 能保构图又改风格?为什么 1.0 会完全无视输入?eps = eps_uncond + w·(eps_cond − eps_uncond),默认 w=7.5;没有它提示就压不住输出。下一节转向视频理解——把静态图像的卷积与注意力扩展到时间维度,理解动作、跟踪事件、做视频分类与时序定位。