AIGC搞图党必看:Stable Diffusion底层架构拆解(附实战避坑指南)
AIGC搞图党必看:Stable Diffusion底层架构拆解(附实战避坑指南)
友情提示:本文全程高能,代码管饱,吐槽满载,建议先收藏再慢慢啃。别问为什么没图,问就是穷(划掉)——主要是怕你们只看图不看字,错过重点。
先别急着点“Generate”,你真的知道SD在干嘛吗?
很多小伙伴第一次打开WebUI,看见那个“Generate”按钮就跟看见自动贩卖机似的,哐哐一顿点,结果出来的图不是三头六臂就是背景糊成马赛克。别急着骂模型,它其实挺冤的——你都没告诉它你到底想要啥,它只能随缘开盲盒。
Stable Diffusion(后面简称SD)说白了就是个噪声消除器,但它不是那种“哐哐一顿高斯模糊”的粗暴滤镜,而是在潜空间里跳了一支80步的芭蕾,每一步都踩着 latent 鼓点,把一张纯噪声图慢慢“跳”成你想要的模样。听着玄乎?别急,咱们把它的底裤一层层扒下来看。
噪声图到底长啥样?先给你看一眼“鬼画符”
在SD眼里,一张512×512的图根本不是你以为的“像素矩阵”,而是一个64×64×4的潜空间张量。换句话说,它把图压成了“鬼画符”,省显存、省算力、省得你心疼显卡。
// 前端视角:把canvas图像压成latent,得先整一个“迷你版”
// 注意,这行代码只是示意,真实潜空间是模型内部产物,咱拿不到
const canvas = document.querySelector('#source');
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.8));
const compressed = await compressToLatent(blob); // 伪代码,别真跑
console.log('latent shape:', compressed.shape); // [1, 4, 64, 64]
看到没?64×64×4,这就是SD真正折腾的“画布”。你传进去的512×512大图,先被VAE encoder一口吞下去,嚼成64×64的小豆腐块,再交给U-Net当橡皮泥捏。生成完再让VAE decoder吐回来,变成你能看得懂的RGB。整套流程就是:
RGB(512×512×3)
↓ VAE encoder
Latent(64×64×4)
↓ U-Net去噪80步
Latent(64×64×4)
↓ VAE decoder
RGB(512×512×3)
U-Net:那个在潜空间里跳芭蕾的主舞
U-Net这货长得像个沙漏,左边下采样、右边上采样,中间还有skip connection当“安全带”,防止它跳嗨了把 latent 掰断。SD用的U-Net大概长这样(PyTorch版,前端同学当小说看就行):
class CrossAttnUpBlock2D(nn.Module):
def __init__(self, in_ch, out_ch, num_layers=1):
super().__init__()
self.attn = CrossAttention(query_dim=out_ch) # 和文本emb眉来眼去
self.resnets = nn.ModuleList([
ResnetBlock2D(out_ch, out_ch) for _ in range(num_layers)
])
def forward(self, hidden_states, temb, encoder_hidden_states):
# hidden_states: 潜空间特征图
# temb: 时间步emb,告诉模型“现在跳到第几步”
# encoder_hidden_states: CLIP文本emb,告诉模型“我想要个猫娘”
for resnet in self.resnets:
hidden_states = resnet(hidden_states, temb)
hidden_states = self.attn(hidden_states, encoder_hidden_states)
return hidden_states
看到CrossAttention没?这就是文本和图像偷偷牵手的地方。你把“a cute cat girl”扔给CLIP,它吐出77×768的向量,U-Net在每一步都去“瞄一眼”这些向量,确保自己正在画的是“猫娘”而不是“猫又”。
时间步Emb:告诉U-Net“现在第几步,别跳错拍”
diffusion 过程把80步的噪声强度编码成一个小向量,像DJ打碟一样,第0步最嗨(纯噪声),第80步最静(高清图)。U-Net每次都要听这个“拍子”,不然它能把猫娘画成克苏鲁。
def get_timestep_embedding(timesteps, dim=320):
half = dim // 2
emb = math.log(10000) / (half - 1)
emb = torch.exp(torch.arange(half, dtype=torch.float32) * -emb)
emb = timesteps[:, None].float() * emb[None, :]
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
return emb # [B, 320]
前端同学可以把这玩意想成progress bar的玄学版,只不过它控制的不是下载速度,而是神经网络的大脑波长。
VAE:图像世界的“压缩饼干”与“膨胀薯片”
VAE encoder把512×512压成64×64,听起来像山寨ZIP,但它可不是无脑降采样。它要学分布,要让你解压回来还能看清小姐姐的睫毛。下面这段代码是VAE decoder的简化版,感受一下它怎么把“小豆腐块”膨胀回高清:
class VAEDecoder(nn.Module):
def __init__(self, z_channels=4, ch=128):
super().__init__()
self.conv_in = nn.Conv2d(z_channels, ch, 3, padding=1)
self.blocks = nn.ModuleList([
ResnetBlock2D(ch, ch) for _ in range(3)
])
self.up = nn.ConvTranspose2d(ch, ch, 4, stride=2, padding=1) # 上采样
self.conv_out = nn.Conv2d(ch, 3, 3, padding=1) # 吐RGB
def forward(self, z):
h = self.conv_in(z) # [B, 128, 64, 64]
for blk in self.blocks:
h = blk(h)
h = self.up(h) # [B, 128, 128, 128]
h = torch.nn.functional.interpolate(h, scale_factor=2) # 再插值到512
return self.conv_out(h) # [B, 3, 512, 512]
看到ConvTranspose2d+interpolate没?这就是膨胀薯片环节, latent 被吹得比原来大八倍,但还得保持纹理细节,不然你得到的就是高清马赛克。
CLIP:把人类语言翻译成AI能听懂的“暗号”
没有CLIP,SD就是“聋哑人”。CLIP负责把“a cute cat girl, cyberpunk style, neon background”翻译成77×768的矩阵,让U-Net在每一步都能“偷看”一眼,确保自己没把猫娘画成哈士奇。
from transformers import CLIPTextModel, CLIPTokenizer
tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-base-patch32")
text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-base-patch32")
prompt = "a cute cat girl, cyberpunk, neon"
inputs = tokenizer(prompt, padding="max_length", max_length=77, return_tensors="pt")
embeds = text_encoder(**inputs).last_hidden_state # [1, 77, 768]
前端同学如果想在浏览器里实时改提示词,可以用ONNX把CLIP转成wasm,但注意模型体积80M,第一次加载能把4G用户吓哭。折中方案是:后端提前把文本emb缓存好,前端只传索引,秒级切换风格。
实战:把SD塞进浏览器?真有人这么干
你以为SD只能跑在GPU农场?错!ONNXRuntime-Web + WASM就能让它在浏览器里裸奔,当然,生成一张512×512需要3分钟,风扇响得跟无人机似的,但真的可行。
// 1. 加载ONNX模型(提前用optimum导出)
const sess = await ort.InferenceSession.create(
'./sd_unet.onnx',
{ executionProviders: ['wasm'] }
);
// 2. 准备输入张量
const latent = new ort.Tensor('float32', randn(1*4*64*64), [1,4,64,64]);
const timestep = new ort.Tensor('int32', [50], [1]);
const textEmb = new ort.Tensor('float32', textEncoder(prompt), [1,77,768]);
// 3. 推理一步
const feeds = { latent_in: latent, t: timestep, context: textEmb };
const results = await sess.run(feeds);
console.log('denoised latent:', results.latent_out.data);
注意,WASM后端不支持CUDA,纯CPU跑80步能把你手机烫成暖手宝。生产环境还是老老实实回GPU云函数,单张A10G 1.5秒出图,香得多。
显存爆炸三连:batch_size、attention、fp16
本地跑图最怕啥?显存爆炸。一张512×512在fp32下大概吃4.2GB,你要是手滑把batch_size=4,直接24G卡原地去世。三板斧收好:
-
fp16:精度砍半,显存砍半,画质肉眼几乎无损。
pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16 ).to("cuda") -
attention slicing:把大矩阵拆成小块,慢10%,省30%显存。
pipe.enable_attention_slicing(slice_size="max") -
cpu offload:用完即扔,显存只留当前层。
pipe.enable_model_cpu_offload()
前端同学如果搞多人并发,一定记得在后台加队列,别让4个用户同时batch=4,不然云厂商直接给你发账单炸弹。
LoRA:给模型打“瘦身高光针”
同样一句话,别人出赛博朋克大片,你出城乡结合部霓虹灯?大概率人家加了LoRA——低秩适配,外挂风格滤镜。原理简单粗暴:只改attention里的两个线性层,附加参数量<原模型1%,却能让你秒变宫崎骏、秒变赛博、秒变水墨。
# 加载LoRA(假设已经merge好)
lora_path = "./cyberpunk_lora.safetensors"
state_dict = load_file(lora_path)
pipe.unet.load_state_dict(state_dict, strict=False)
前端想动态切换LoRA?把每个LoRA拆成独立bin,按需lazy load,别一次性全塞进显存,不然10个风格就能把你的24G卡撑到怀疑人生。
提示词玄学?不,是权重博弈
别再迷信“万能负向提示词”了,负向只是反向加权,不会给你魔法。真正有用的是语法权重:
(a cute cat girl:1.2), (cyberpunk style:1.3), (neon background:1.1), umbrella, rain
括号+冒号=权重,>1强化,<1弱化,别一口气全给1.5,模型直接画风崩坏。前端可以做实时slider,让用户拖拖拽拽就能调权重,背后就是字符串拼接,简单到令人发指:
function buildPrompt(base, map) {
return Object.entries(map)
.map(([k, v]) => `(${k}:${v.toFixed(2)})`)
.join(', ');
}
生成崩坏急救包:黑图、绿图、双重人格
- 黑图:学习率爆炸,检查是否用了fp16却忘了
torch_dtype=torch.float16。 - 绿图:VAE decoder溢出,把latent clamp到[-4,4]就能救回来。
- 双重人格:CLIP最大token 77,超长提示词被截断,后半截失效,切成75+2再拼回去。
def truncate_prompt(prompt, max_token=75):
tokens = tokenizer.tokenize(prompt)
if len(tokens) <= max_token:
return prompt
return tokenizer.convert_tokens_to_string(tokens[:max_token])
写在最后的碎碎念
SD这玩意就像一只高冷猫,你不懂它,它就给你一副“你谁啊”的表情;你摸准它的脾气,它就能在你怀里打滚撒娇。别再把“提示词”当抽奖券,搞清楚它每一步在干嘛,你才能真正成为“调教师”而不是“赌徒”。
今天咱们把底裤扒得够干净了:从潜空间到U-Net,从VAE到CLIP,再到前端塞浏览器、显存爆炸急救、LoRA外挂、权重博弈……能给的都给了,不能给的(比如模型文件)自己去HuggingFace下,别再问我要百度网盘链接,真没有。
下回想聊啥?ControlNet?SDXL?还是如何把SD塞进小程序让甲方爸爸一键出图?留言区……哦对,没有留言区,那就等下次我在群里语音60秒轰炸你们吧。

800

被折叠的 条评论
为什么被折叠?



