码到成功

Stable Diffusion V3

## 先看 SD3 的核心:MMDiT + Flow Matching

Stable Diffusion 3 的模型卡和 Diffusers 文档都提到,它使用的是 Multimodal Diffusion Transformer 架构。  
简单理解,`MMDiT` 把文本和图像 latent 放进更偏 Transformer 的建模方式里处理,而不是继续只依赖传统卷积式 U-Net 主干。

![SD3 架构图](https://zoytown-sg.oss-ap-southeast-1.aliyuncs.com/blogs/sd3-mmdit-architecture-qlnx.svg)

可以把 SD3 的推理链路粗略拆成几段:

- 文本编码器把 prompt 转成语义表示
- VAE 把图像空间和 latent 空间连接起来
- MMDiT 在 latent 空间里逐步去噪
- Flow Matching 调度器负责控制从噪声到图像的生成轨迹
- 最终 latent 被 VAE 解码成图像

Diffusers 里的 `StableDiffusion3Pipeline` 也能看到这个结构:

- `SD3Transformer2DModel`:负责 denoise latent 的 MMDiT 主体
- `FlowMatchEulerDiscreteScheduler`:和 transformer 配合完成采样
- `AutoencoderKL`:负责 latent 和图像之间的编码解码
- `CLIPTextModelWithProjection`:两路 CLIP 文本编码器
- `T5EncoderModel`:一路 T5 文本编码器

这里很关键的一点是:SD3 不是只吃一个文本编码器。  
模型卡里说明,SD3 Medium 使用了三路固定的预训练文本编码器,包括 OpenCLIP-ViT/G、CLIP-ViT/L 和 T5-XXL。  
这也是它在复杂文本描述和图中文字任务上更有发挥空间的原因之一。

## 安装环境:先把 `diffusers` 跑通

本地使用 SD3,最常见的方式是通过 Hugging Face `diffusers`。  
模型是 gated 模型,使用前需要在 Hugging Face 页面接受访问条件,并准备好登录 token。

先安装依赖:

```bash
python -m venv .venv
source .venv/bin/activate

python -m pip install -U pip
python -m pip install torch torchvision torchaudio
python -m pip install -U diffusers transformers accelerate sentencepiece protobuf safetensors
python -m pip install pillow
```

登录 Hugging Face:

```bash
huggingface-cli login
```

如果你只想在脚本里使用 token,也可以通过环境变量读取:

```bash
export HF_TOKEN="你的 Hugging Face Token"
```

## 最小 Python 推理示例

下面是最小可用版本:

```python
import torch
from diffusers import StableDiffusion3Pipeline


model_id = "stabilityai/stable-diffusion-3-medium-diffusers"

pipe = StableDiffusion3Pipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
)
pipe = pipe.to("cuda")

image = pipe(
    prompt="A cinematic photo of a glass greenhouse floating above a quiet black lake, soft moonlight, ultra detailed",
    negative_prompt="low quality, blurry, distorted",
    num_inference_steps=28,
    guidance_scale=7.0,
    width=1024,
    height=1024,
).images[0]

image.save("sd3_greenhouse.png")
```

如果没有 CUDA,可以先用 CPU offload 或者更小分辨率测试:

```python
import torch
from diffusers import StableDiffusion3Pipeline


pipe = StableDiffusion3Pipeline.from_pretrained(
    "stabilityai/stable-diffusion-3-medium-diffusers",
    torch_dtype=torch.float16,
)
pipe.enable_model_cpu_offload()

image = pipe(
    prompt="A tiny robot painter drawing stars on a dark studio wall",
    negative_prompt="bad anatomy, blurry, messy text",
    num_inference_steps=24,
    guidance_scale=6.5,
    width=768,
    height=768,
).images[0]

image.save("sd3_robot.png")
```

如果是 Apple Silicon,可以尝试 MPS,不过 SD3 对显存和算子支持更敏感。  
不稳定时建议回到 CPU、降低尺寸,或使用云端 GPU。

```python
device = "mps" if torch.backends.mps.is_available() else "cpu"
pipe = pipe.to(device)
```

## Pipeline 参数怎么调

SD3 的生成质量很大程度取决于 prompt 和采样参数。  
先看几个高频参数:

- `prompt`:正向描述,决定主体、构图、风格和细节
- `negative_prompt`:反向描述,用来压掉明显不想要的特征
- `num_inference_steps`:采样步数,越大通常越稳,但速度会下降
- `guidance_scale`:文本引导强度,太低容易跑偏,太高可能发硬
- `width / height`:输出尺寸,越大越吃显存
- `generator`:控制随机种子,方便复现

![Python Pipeline 面板](https://zoytown-sg.oss-ap-southeast-1.aliyuncs.com/blogs/sd3-python-pipeline-qlnx.svg)

可以写一个简单的生成函数:

```python
from pathlib import Path
import torch


def generate_image(
    pipe,
    prompt: str,
    output: str,
    negative_prompt: str = "",
    seed: int = 42,
    steps: int = 28,
    guidance: float = 7.0,
    width: int = 1024,
    height: int = 1024,
):
    generator = torch.Generator(device=pipe.device).manual_seed(seed)

    result = pipe(
        prompt=prompt,
        negative_prompt=negative_prompt,
        num_inference_steps=steps,
        guidance_scale=guidance,
        width=width,
        height=height,
        generator=generator,
    )

    image = result.images[0]
    Path(output).parent.mkdir(parents=True, exist_ok=True)
    image.save(output)
    return output
```

用法:

```python
generate_image(
    pipe,
    prompt="A clean product photo of a matte black smart speaker on a stone table, soft studio lighting",
    negative_prompt="blur, low quality, extra objects, broken text",
    output="outputs/speaker.png",
    seed=314,
    steps=28,
    guidance=7.0,
)
```

这类封装比在 notebook 里反复复制 Pipeline 调用更利于维护。  
后面做批量生成、API 服务或自动化评测时,可以直接复用。

## Prompt 写法:别只堆关键词

SD3 对复杂 prompt 的理解更好,但这不代表 prompt 可以随便堆词。  
更稳的写法是把 prompt 拆成几个层次:

- 主体:画什么
- 场景:在哪里
- 构图:远景、近景、俯视、对称、中心构图
- 风格:摄影、插画、电影感、产品渲染
- 光线:柔光、逆光、霓虹、自然光
- 细节:材质、纹理、颜色、镜头语言
- 文字:如果需要图中文字,尽量短而明确

![Prompt 工作台](https://zoytown-sg.oss-ap-southeast-1.aliyuncs.com/blogs/sd3-prompt-workbench-qlnx.svg)

可以写一个 prompt 组装器:

```python
def build_prompt(
    subject: str,
    scene: str,
    style: str,
    lighting: str,
    details: list[str] | None = None,
    text: str | None = None,
):
    parts = [
        subject,
        f"scene: {scene}",
        f"style: {style}",
        f"lighting: {lighting}",
    ]

    if details:
        parts.append("details: " + ", ".join(details))

    if text:
        parts.append(f'clear readable text: "{text}"')

    return ", ".join(parts)


prompt = build_prompt(
    subject="a futuristic coffee cup shaped like a small spaceship",
    scene="on a dark marble table, minimal background",
    style="premium product photography",
    lighting="soft rim light and gentle reflections",
    details=["black ceramic", "silver edge", "steam glow"],
    text="MOON BREW",
)
```

反向 prompt 也别写成一大串无脑词。  
可以先从这些通用项开始:

```python
negative_prompt = "low quality, blurry, distorted hands, unreadable text, watermark, extra objects"
```

如果发现某类图总是出问题,再针对性追加,而不是一开始就把负面词写成超长列表。

## 批量生成:把 prompt 当数据管理

如果你要做一组封面图、商品图、角色设定图,最好把 prompt 放进 JSON 或 CSV,而不是写死在脚本里。

示例 `prompts.json`:

```json
[
  {
    "name": "neon-library",
    "prompt": "A neon-lit library inside a space station, cinematic composition, rich details",
    "negative_prompt": "blurry, low quality, messy shelves",
    "seed": 11
  },
  {
    "name": "glass-garden",
    "prompt": "A glass garden room floating above a dark ocean, elegant concept art",
    "negative_prompt": "bad perspective, noisy",
    "seed": 12
  }
]
```

批量生成脚本:

```python
import json
from pathlib import Path


def batch_generate(pipe, config_path="prompts.json", output_dir="outputs"):
    jobs = json.loads(Path(config_path).read_text(encoding="utf-8"))
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    results = []
    for job in jobs:
        name = job["name"]
        output_path = output_dir / f"{name}.png"

        generate_image(
            pipe=pipe,
            prompt=job["prompt"],
            negative_prompt=job.get("negative_prompt", ""),
            seed=job.get("seed", 42),
            output=str(output_path),
            steps=job.get("steps", 28),
            guidance=job.get("guidance", 7.0),
            width=job.get("width", 1024),
            height=job.get("height", 1024),
        )

        results.append({"name": name, "file": str(output_path)})

    Path(output_dir / "manifest.json").write_text(
        json.dumps(results, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

    return results
```

这样做有几个好处:

- prompt 可以版本化
- 生成结果能和配置对应
- 后续可以做自动评分、人工筛选、批量重跑
- 适合接入工作流系统

## 用 Flask 包一个轻量 API

如果你想把 SD3 变成内部小服务,可以先用 Flask 做一个轻量接口。  
注意:图像生成任务比较重,这个示例适合单机小工具;如果要多人使用,建议接任务队列。

![批量与服务化](https://zoytown-sg.oss-ap-southeast-1.aliyuncs.com/blogs/sd3-service-dashboard-qlnx.svg)

```python
from pathlib import Path
from uuid import uuid4

from flask import Flask, jsonify, request, send_from_directory


app = Flask(__name__)
OUTPUT_DIR = Path("outputs")
OUTPUT_DIR.mkdir(exist_ok=True)


@app.post("/api/generate")
def api_generate():
    data = request.get_json(force=True)
    prompt = data.get("prompt", "").strip()
    if not prompt:
        return jsonify({"error": "prompt is required"}), 400

    negative_prompt = data.get("negative_prompt", "")
    seed = int(data.get("seed", 42))
    width = int(data.get("width", 1024))
    height = int(data.get("height", 1024))

    width = max(512, min(width, 1024))
    height = max(512, min(height, 1024))

    name = f"{uuid4().hex}.png"
    output_path = OUTPUT_DIR / name

    generate_image(
        pipe=pipe,
        prompt=prompt,
        negative_prompt=negative_prompt,
        seed=seed,
        output=str(output_path),
        width=width,
        height=height,
    )

    return jsonify({"image_url": f"/outputs/{name}"})


@app.get("/outputs/<name>")
def outputs(name):
    return send_from_directory(OUTPUT_DIR, name)
```

请求示例:

```bash
curl -X POST http://127.0.0.1:5000/api/generate \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A calm black studio scene with a glowing crystal camera, product photography",
    "negative_prompt": "low quality, blur, watermark",
    "seed": 123,
    "width": 1024,
    "height": 1024
  }'
```

工程上建议再补几件事:

- 使用队列处理生成任务
- 限制请求频率和最大尺寸
- 记录 prompt、seed、参数和输出路径
- 给输出文件做定期清理
- 对公开服务加内容安全策略

## 显存和速度优化

SD3 Medium 并不算轻,尤其是完整文本编码器和较大输出尺寸会吃很多显存。  
常见优化手段包括:

- `torch_dtype=torch.float16`
- `enable_model_cpu_offload()`
- 降低 `width / height`
- 降低 batch size
- 复用 Pipeline,不要每次请求重新加载
- 使用更少的 steps 做预览图
- 用 `generator` 固定 seed,方便对比参数变化

Diffusers 文档还提到可以用 Tiny AutoEncoder for Stable Diffusion 3,也就是 `TAESD3`,让 latent 预览解码更快。

```python
import torch
from diffusers import AutoencoderTiny, StableDiffusion3Pipeline


pipe = StableDiffusion3Pipeline.from_pretrained(
    "stabilityai/stable-diffusion-3-medium-diffusers",
    torch_dtype=torch.float16,
)

pipe.vae = AutoencoderTiny.from_pretrained(
    "madebyollin/taesd3",
    torch_dtype=torch.float16,
)

pipe = pipe.to("cuda")
```

如果你要做的是正式出图,建议先比较 Tiny VAE 和原 VAE 的结果差异,再决定是否用于主流程。

## 授权和安全边界

SD3 Medium 的 Hugging Face 模型页包含访问条件和授权说明。  
使用前要确认自己的用途是否符合对应 license,尤其是商业化、对外服务和再分发场景。

另外,图像生成系统不要只考虑“能不能生成”,还要考虑“生成结果怎么被使用”。  
建议至少做这些限制:

- 对 prompt 做基础内容检查
- 对输出做人工审核或自动过滤
- 记录请求参数和生成结果
- 不把模型当成事实来源
- 不用它生成真实人物或敏感场景的误导性内容
- 对外接口要有鉴权和频率限制

这部分不是装饰,而是生成式应用上线时必须认真处理的工程边界。