Stable Diffusion Nasıl Çalışır: Diffusion Modellerine Giriş
Bir sysadmin olarak genellikle altyapı kuruyoruz, servisler ayağa kaldırıyoruz, logları izliyoruz. Ama son dönemde işin içine yapay zeka araçları da girmeye başladı. Stable Diffusion’ı production ortamına almadan önce, “bu şey aslında nasıl çalışıyor?” sorusunu sormak hem merakımı giderdi hem de kaynakları daha iyi planlamama yardımcı oldu. Bu yazıda teknik arka planı sysadmin gözünden ele alacağız.
Diffusion Modeli Nedir, Neden Önemlidir
Stable Diffusion’ın kalbinde bir diffusion modeli yatıyor. Bunu şöyle düşünebilirsin: eğer bir fotoğrafın üzerine yavaş yavaş rastgele gürültü eklersen, sonunda tamamen beyaz/gri bir “kar” görüntüsü elde edersin. Diffusion modeli ise tam tersini yapıyor, yani saf gürültüden başlayıp adım adım bu gürültüyü temizleyerek anlamlı bir görüntü ortaya çıkarıyor.
Bu fikir ilk olarak 2015 yılında Sohl-Dickstein ve arkadaşları tarafından önerildi, sonra 2020’de DDPM (Denoising Diffusion Probabilistic Models) ile pratiğe döküldü. Stable Diffusion ise bunu bir adım öteye taşıyarak işlemi piksel uzayında değil, daha küçük bir latent uzayda gerçekleştiriyor. Bu detay hem hesaplama verimliliği hem de sysadmin olarak GPU belleği yönetimi açısından kritik bir nokta.
Mimarinin Üç Temel Bileşeni
Stable Diffusion, aslında birbirinden farklı üç büyük modelin bir araya gelmesiyle çalışıyor:
- VAE (Variational Autoencoder): Görüntüyü sıkıştırır ve açar
- U-Net: Asıl gürültü temizleme işini yapar
- CLIP Text Encoder: Metin promptunu matematiksel vektöre dönüştürür
Bu bileşenlerin her biri ayrı model dosyaları olarak diskte yer kaplıyor. Bunu kurulum sırasında fark edeceksin, çünkü toplam model boyutu birkaç GB’ı rahatlıkla aşabiliyor.
VAE: Görüntünün Latent Uzaya Gidip Gelmesi
VAE, bir kapı gibi çalışıyor. 512×512 piksellik bir görüntüyü alıp 64x64x4 boyutunda bir latent tensöre sıkıştırıyor. Bu 8 kat küçültme işlemi sayesinde U-Net, piksel uzayında değil çok daha küçük bir uzayda çalışabiliyor.
Bunu pratik olarak anlamak için şuna bakabilirsin. Bir 512×512 RGB görüntü RAM’de ne kadar yer tutar?
# Piksel uzayi hesabi
width = 512
height = 512
channels = 3 # RGB
bytes_per_pixel = 4 # float32
pixel_space_mb = (width * height * channels * bytes_per_pixel) / (1024**2)
print(f"Piksel uzayi: {pixel_space_mb:.2f} MB")
# Latent uzayi hesabi
latent_width = 64
latent_height = 64
latent_channels = 4
latent_space_mb = (latent_width * latent_height * latent_channels * bytes_per_pixel) / (1024**2)
print(f"Latent uzayi: {latent_space_mb:.4f} MB")
print(f"Kucultme orani: {pixel_space_mb/latent_space_mb:.0f}x")
Bu hesabı kendi ortamında çalıştırdığında, latent uzayın pikselden yaklaşık 48 kat daha verimli olduğunu göreceksin. Bu doğrudan GPU VRAM kullanımını etkiliyor.
U-Net: Gürültü Temizleyici
U-Net mimarisi aslında medikal görüntü segmentasyonu için geliştirilmişti. Encoder kısmı görüntüyü küçülterek soyutlama yapıyor, decoder kısmı ise yeniden büyüterek detay ekliyor. Skip connection’lar sayesinde her seviyedeki bilgi korunuyor.
Stable Diffusion’da U-Net, şu anda ne kadar gürültü olduğunu tahmin etmek için eğitiliyor. Daha doğrusu, hangi gürültünün eklendiğini tahmin ediyor. Bu tahmin sayesinde her adımda gürültü azaltılıyor ve görüntü netleşiyor.
U-Net’in içinde ayrıca cross-attention katmanları var. Bu katmanlar, metin promptunun U-Net’e “rehberlik etmesini” sağlıyor. Yani “a cat sitting on a red sofa” yazısındaki “cat”, “red”, “sofa” kelimeleri görüntünün farklı bölgelerine farklı ağırlıklarla etki ediyor.
CLIP Text Encoder: Kelimelerden Vektörlere
CLIP (Contrastive Language-Image Pre-Training) OpenAI tarafından geliştirildi. Milyarlarca görüntü-metin çiftiyle eğitilen bu model, metni ve görüntüyü aynı anlam uzayında temsil edebiliyor.
# Basit bir CLIP encoding ornegi
from transformers import CLIPTokenizer, CLIPTextModel
import torch
model_id = "openai/clip-vit-large-patch14"
tokenizer = CLIPTokenizer.from_pretrained(model_id)
text_encoder = CLIPTextModel.from_pretrained(model_id)
prompt = "a photorealistic cat sitting on a red sofa, high quality"
tokens = tokenizer(
prompt,
padding="max_length",
max_length=77, # CLIP max token limiti
return_tensors="pt"
)
with torch.no_grad():
embeddings = text_encoder(tokens.input_ids)[0]
print(f"Embedding shape: {embeddings.shape}")
# Cikti: torch.Size([1, 77, 768])
Dikkat etmeni istediğim nokta şu: CLIP maksimum 77 token işleyebiliyor. Bu sınırı aşan promptlar kesilecek. Çok uzun promptlar yazarken bunu göz önünde bulundurman gerekiyor.
Sampling: Adım Adım Görüntü Oluşturma
Şimdi asıl sürece gelelim. Stable Diffusion bir görüntü üretirken şu adımları izliyor:
- Tamamen rastgele bir gürültü tensörü oluşturuluyor (latent uzayda)
- Her adımda U-Net bu tensörü ve zaman bilgisini (timestep) alıyor
- Text embedding’ler cross-attention ile sürece dahil oluyor
- U-Net’in tahminine göre gürültü azaltılıyor
- Bu işlem seçilen adım sayısı kadar tekrarlanıyor
- Son latent tensör VAE ile decode edilerek piksel görüntüsüne dönüştürülüyor
# Pseudocode olarak sampling dongusu
import torch
def sampling_loop(
unet,
scheduler,
text_embeddings,
num_inference_steps=50,
guidance_scale=7.5
):
# Rastgele baslangic latenti
latents = torch.randn((1, 4, 64, 64))
scheduler.set_timesteps(num_inference_steps)
for timestep in scheduler.timesteps:
# Classifier-free guidance icin latenti ikiyle
latent_model_input = torch.cat([latents] * 2)
# U-Net tahmin
with torch.no_grad():
noise_pred = unet(
latent_model_input,
timestep,
encoder_hidden_states=text_embeddings
).sample
# CFG: koşulsuz ve kosullu tahmini birlestir
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (
noise_pred_text - noise_pred_uncond
)
# Scheduler ile latenti guncelle
latents = scheduler.step(noise_pred, timestep, latents).prev_sample
return latents
Classifier-Free Guidance (CFG)
Yukarıdaki kod bloğunda guidance_scale parametresini fark etmişsindir. Bu CFG Scale olarak da biliniyor ve promptun görüntüyü ne kadar yönlendireceğini kontrol ediyor.
- guidance_scale = 1: Prompt neredeyse hiç etkili değil
- guidance_scale = 7-8: Dengeli, önerilen değer aralığı
- guidance_scale = 15+: Prompt çok güçlü ama görüntü kalitesi düşebilir
Teknik olarak şöyle çalışıyor: Model hem “prompt olmadan” hem de “prompt ile” tahmin yapıyor. Sonra bu iki tahminin farkını alıp guidance_scale ile çarpıp koşulsuz tahminin üzerine ekliyor. Bu sayede prompt etkisi güçlendiriliyor.
Scheduler Nedir, Neden Farklı Seçenekler Var
Sampling döngüsünde gürültüyü nasıl ve ne hızda azaltacağını belirleyen algoritma scheduler oluyor. Bu konuyu sysadmin olarak şöyle düşünebilirsin: farklı network protokollerinin farklı trade-off’ları olduğu gibi, her scheduler da hız ve kalite arasında farklı dengeler kuruyor.
# Farkli schedulerlari test etmek
from diffusers import (
DDIMScheduler,
PNDMScheduler,
DPMSolverMultistepScheduler,
EulerAncestralDiscreteScheduler
)
schedulers = {
"DDIM": DDIMScheduler,
"PNDM": PNDMScheduler,
"DPM++ 2M": DPMSolverMultistepScheduler,
"Euler A": EulerAncestralDiscreteScheduler
}
model_path = "./stable-diffusion-v1-5"
for name, scheduler_class in schedulers.items():
scheduler = scheduler_class.from_pretrained(
model_path,
subfolder="scheduler"
)
print(f"{name}: {scheduler.__class__.__name__}")
En yaygın kullanılan scheduler’lar ve karakteristikleri:
- DDIM: Deterministik, aynı seed ile aynı sonucu verir, 20-50 adım önerilir
- PNDM: Orijinal SD scheduler’ı, stabil ama yavaş
- DPM++ 2M: Hızlı ve kaliteli, 20-30 adımda iyi sonuç verir, production önerisi
- Euler A: Stokastik yani her seferinde farklı sonuç, yaratıcı çıktılar için iyi
- LMS: Düşük adım sayılarında bile makul sonuçlar
Gerçek Dünya Senaryosu: API Servisi Kurulumu
Diyelim ki bir ekip için Stable Diffusion’ı REST API olarak sunmak istiyorsun. İşte basit bir FastAPI wrapper:
# sd_api.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
import torch
import base64
from io import BytesIO
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Stable Diffusion API")
class GenerationRequest(BaseModel):
prompt: str
negative_prompt: str = ""
num_steps: int = 25
guidance_scale: float = 7.5
width: int = 512
height: int = 512
seed: int = -1
# Uygulama baslarken model yukle
@app.on_event("startup")
async def load_model():
global pipeline
model_id = "runwayml/stable-diffusion-v1-5"
pipeline = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16,
safety_checker=None
)
# DPM++ scheduler ata
pipeline.scheduler = DPMSolverMultistepScheduler.from_config(
pipeline.scheduler.config
)
pipeline = pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()
logger.info("Model yuklendi, API hazir")
@app.post("/generate")
async def generate_image(request: GenerationRequest):
try:
generator = None
if request.seed != -1:
generator = torch.Generator("cuda").manual_seed(request.seed)
result = pipeline(
prompt=request.prompt,
negative_prompt=request.negative_prompt,
num_inference_steps=request.num_steps,
guidance_scale=request.guidance_scale,
width=request.width,
height=request.height,
generator=generator
)
img = result.images[0]
buffer = BytesIO()
img.save(buffer, format="PNG")
img_base64 = base64.b64encode(buffer.getvalue()).decode()
return {"image": img_base64, "seed": request.seed}
except Exception as e:
logger.error(f"Uretim hatasi: {e}")
raise HTTPException(status_code=500, detail=str(e))
Bunu systemd servisi olarak ayağa kaldırmak için:
# /etc/systemd/system/sd-api.service
cat > /etc/systemd/system/sd-api.service << 'EOF'
[Unit]
Description=Stable Diffusion API Service
After=network.target
[Service]
Type=simple
User=sduser
WorkingDirectory=/opt/stable-diffusion
Environment="CUDA_VISIBLE_DEVICES=0"
ExecStart=/opt/stable-diffusion/venv/bin/uvicorn sd_api:app --host 0.0.0.0 --port 8080 --workers 1
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable sd-api
systemctl start sd-api
# Log takibi
journalctl -u sd-api -f
VRAM Optimizasyonları: Sysadmin’in Kabusu
Stable Diffusion’ı production’a alırken en çok VRAM sorunuyla karşılaşırsın. İşte sistematik yaklaşım:
# GPU durumunu izlemek icin
nvidia-smi --query-gpu=timestamp,name,memory.total,memory.used,memory.free,utilization.gpu
--format=csv -l 5
# Daha detayli izleme
watch -n 2 'nvidia-smi --query-compute-apps=pid,name,used_memory --format=csv'
VRAM kullanımını düşürmek için kullanabileceğin Python optimizasyonları:
# Bellegi verimli kullanmak icin pipeline ayarlari
pipeline = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16, # float32 yerine float16: yaklasik 2x tasarruf
)
# Secenek 1: xformers (onerilen, daha hizli da)
pipeline.enable_xformers_memory_efficient_attention()
# Secenek 2: attention slicing (daha yavash ama daha az VRAM)
pipeline.enable_attention_slicing(1)
# Secenek 3: VAE slicing (yuksek cozunurluk batch isleme icin)
pipeline.enable_vae_slicing()
# Secenek 4: CPU offloading (az VRAM varsa, model parcalari CPU'da bekler)
pipeline.enable_sequential_cpu_offload()
# Secenek 5: Model CPU offload (daha dengeli CPU/GPU kullanimi)
pipeline.enable_model_cpu_offload()
Negative Prompt’un Matematik
Negatif prompt, birçok kullanıcının sihirli bir kara liste olarak gördüğü bir özellik. Ama aslında CFG mekanizmasının bir uzantısı. Matematiksel olarak:
son_tahmin = koşulsuz_tahmin + scale * (pozitif_tahmin - koşulsuz_tahmin)
Negative prompt kullanıldığında “koşulsuz_tahmin” yerine “negatif_tahmin” geliyor:
son_tahmin = negatif_tahmin + scale * (pozitif_tahmin - negatif_tahmin)
Bu yüzden negative prompt’a “blurry, low quality, watermark” gibi şeyler yazarak U-Net’e “bu yönden uzaklaş” diyebiliyoruz.
Model Dosyalarını Anlamak
Bir Stable Diffusion model klasörünün içine baktığında şunları görürsün:
ls -lh ./stable-diffusion-v1-5/
# text_encoder/ - CLIP model
# tokenizer/ - Token sozlugu
# unet/ - Ana U-Net modeli (~3.2 GB)
# vae/ - VAE encoder/decoder
# scheduler/ - Scheduler konfigurasyonu
# model_index.json - Pipeline tanimi
# Dosya boyutlarini kontrol et
du -sh ./stable-diffusion-v1-5/*
Checkpoint dosyaları (.ckpt veya .safetensors) bu klasör yapısını tek bir dosyada birleştiriyor. SafeTensors formatı güvenlik açısından tercih edilmeli çünkü pickle exploitlerinden korunuyor.
# safetensors formatini tercih et
# .ckpt dosyalarini safetensors'a donusturmek icin
python -c "
from safetensors.torch import save_file
import torch
# Eski ckpt yukle
state_dict = torch.load('model.ckpt', map_location='cpu')
if 'state_dict' in state_dict:
state_dict = state_dict['state_dict']
# Guvenli formata kaydet
save_file(state_dict, 'model.safetensors')
print('Donusum tamamlandi')
"
Sonuç
Stable Diffusion’ın nasıl çalıştığını anlamak, salt merak meselesi değil. Bir sysadmin olarak bu bilgi sana şunu sağlıyor: hangi bileşen ne zaman devreye giriyor, VRAM neden doluyor, neden bazı schedulerlar daha hızlı, neden token limiti var. Bunların hepsinin artık somut bir cevabı var.
VAE latent uzayı küçülterek hesaplamayı ucuzlatıyor, U-Net cross-attention ile metni görüntüye yönlendiriyor, CLIP metin ile görüntüyü aynı uzayda temsil ediyor, scheduler ise bu sürecin temposunu ve kalitesini belirliyor. CFG scale ve negative prompt ise bu döngünün yönlendirme mekanizmaları.
Bir sonraki adım olarak LoRA fine-tuning, ControlNet entegrasyonu veya multi-GPU paralel çalıştırma konularına bakabilirsin. Ama önce bu temel sağlam oturmalı. Altyapıyı iyi kurmak istiyorsan, neyin üzerine kurduğunu bilmen gerekiyor.
