你把 seed 锁成 42,提示词原封不动复制过来,过一周再跑一遍。出来的图像很接近,但不一样——头发飞丝不同,耳环换了一只,地平线略微偏移。你以为 seed 是输出的哈希,所以这看起来像 bug。其实不是。一张 diffusion 图像是 seed、采样器、调度器、模型权重、CFG、步数、分辨率、所有 LoRA / ControlNet / refiner pass 的确定性函数。其中任何一个变量改动——包括服务方在你不知情时悄悄更新模型文件——都会产出不同图像。可复现性是一套纪律,不是一个开关。
常见原因
按”同样 seed 出不同结果”的实际锅大小排序。
1. 模型版本被静默升级
托管服务(Midjourney、Firefly、DALL-E、托管 SDXL)会按自己的节奏更新底模。seed 在新权重下映射到不同的噪声张量。你什么都没做错,是后端换了。
如何识别:查看服务商的版本说明 / 版本下拉框。今天的版本号与当初保存时不同 = 模型变了。
2. 采样器或调度器被换掉
同 seed 下 DPM++ 2M Karras 和 Euler a 出图就不一样。一些 UI 默认为”auto”,会根据分辨率或模型自动选采样器——也就是说采样器可能悄悄翻了,你没察觉。
如何识别:打开保存的 PNG 元数据(或 API 请求日志),把采样器名字和今天的请求比一比。采样器不匹配 = 不同图像。
3. 步数、CFG 或分辨率有一格之差
CFG 7.0 与 7.5、步数 28 与 30、分辨率 1024x1024 与 1024x1080,同 seed 下都会出明显不同的图。UI 滑块经常隐式四舍五入;从数值输入框复制粘贴更稳妥。
如何识别:把完整参数 dump 出来逐字节比对,不要靠肉眼。
4. LoRA / ControlNet / refiner 加上了或权重变了
LoRA 强度 0.6 vs 0.7 看得出差别。Refiner pass 切换比例 0.8 vs 0.75 会改变高频细节。ControlNet 的预处理(canny 边、深度图)在某些管线里本身就对 seed 敏感。
如何识别:把每一个适配器、LoRA、embedding、后处理 pass 连同确切权重列出来。哪一项缺失或不同,就是它。
5. CUDA / 硬件非确定性
PyTorch 的 cudnn.deterministic = False(默认)允许 GPU 为同一操作选择不同的 kernel,产生微小的浮点差异,并在 diffusion 步数中累积。同 seed 在同一张 GPU 上仍可能出微差。
如何识别:本地跑时设 torch.use_deterministic_algorithms(True) 和 CUBLAS_WORKSPACE_CONFIG=:4096:8。若可复现性变好,这是贡献因素之一。
6. 提示词被无意改动
末尾多一个空格、弯引号 vs 直引号、em 破折号 vs 短横——分词器看它们是不同 token。从富文本应用(Notion、Google Docs)复制粘贴往往会被悄悄改写标点。
如何识别:用 diff 或 cmp 比对提示词字符串。肉眼检查不可靠。
7. 服务商加了隐藏的后处理
一些托管 API 会在输出环节跑人脸增强、放大、安全过滤。这些 pass 不会跟随 seed,会加入随机性。你得到一个确定性的核心图像加一个非确定性的修饰层。
如何识别:如果 API 支持,请求未经处理的原图(enhance: false、upscale: 1x)。原图稳定但成片不稳,说明后处理是元凶。
开始前准备
- 在开始任何新的运行前,保存原始”好图”和它的完整元数据(PNG
parameters块,或 API 响应 JSON)。 - 记下出原图时所用的具体服务商、模型名和版本号字符串。
- 在重跑之前,记录今天的服务商版本号字符串。
- 决定你的目标:在托管 API 上很难做到逐字节一致;“视觉上不可区分”是更现实的目标。
需要收集的信息
- seed 值(整数;确认是以 int 传递的,不是字符串
"42")。 - 采样器名和调度器名,精确字符串。
- 步数、CFG、宽度和高度。
- 完整提示词字符串和负向提示词字符串,逐字节。
- 每一个 LoRA、embedding、ControlNet、IP-Adapter 及其权重值。
- 模型文件名 + 哈希(本地)或版本号字符串(托管)。
- 硬件:GPU 型号、CUDA 版本、torch 版本(本地)。
一步步修复
按从最小代价到最大代价排序。
步骤 1:比对参数 dump
把两套元数据保存到文本文件并 diff:
exiftool -PNG:parameters good.png > good.txt
exiftool -PNG:parameters bad.png > bad.txt
diff good.txt bad.txt
任何一行不同就是嫌疑点。哪怕只有一个参数不匹配,也先把它修了再往下走。
步骤 2:显式锁定模型版本
托管 API 里传版本 ID,不只是模型名:
{
"model": "stable-diffusion-xl-base-1.0",
"model_version": "39ed52f2-a78c-4ce1-9c1d-aaaaaaaaaa",
"seed": 42
}
如果服务商不提供版本锁,换一家支持的(Replicate、RunPod,或自部署)。
步骤 3:锁定采样器和调度器
明确指定两者。不要让”auto”来选:
{
"sampler": "DPM++ 2M Karras",
"scheduler": "karras",
"steps": 30,
"cfg_scale": 7.0
}
如果你的 UI 把它们藏起来了,换 API 或换暴露这些参数的 UI(ComfyUI、InvokeAI、AUTOMATIC1111)。
步骤 4:从管线里剥掉隐式随机源
可复现性测试期间关闭人脸修复、放大、refiner。只跑底模。底模稳定之后再逐个加回每个后处理,并验证每一个也是可 seed 化的。
{
"enhance_face": false,
"upscale": 1,
"refiner": null
}
步骤 5:强制 torch 确定性(本地管线)
import torch
import os
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
torch.use_deterministic_algorithms(True, warn_only=True)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
generator = torch.Generator(device="cuda").manual_seed(42)
会损失 10-20% 推理速度,但同一硬件上能得到逐字节一致的输出。
步骤 6:把提示词当字节而非文本
把提示词和参数 JSON 一起存到文本文件里。不要从截图里重新打字。用 cmp 比对:
cmp good_prompt.txt new_prompt.txt
这里的差异通常是看不见的空白或标点替换。参见 提示词太长反而效果更差,讲提示词脆弱性的更多模式。
步骤 7:本地快照模型权重以做真正的长期可复现
对于需要逐年可复现的项目,把模型文件(.safetensors)下载下来并保存 sha256。托管版本会消失;你的本地快照不会。
sha256sum sd_xl_base_1.0.safetensors > sd_xl_base_1.0.sha256
验证
- 同一提示词加同一 seed 连续跑两次。对两张 PNG 像素缓冲做哈希(
shasum -a 256 a.png b.png)。在完全锁定的管线下,哈希一致。 - 跨两个会话 / 两天跑同样的提示词和 seed。除非服务商升级了模型,哈希应仍相同。
- 故意改一个参数(比如 cfg 从 7.0 改成 7.5),确认输出现在不同。证明你的管线响应正常,不是卡在缓存图上。
长期预防
- 把参数集合视为一个元组
(model_version, seed, sampler, scheduler, steps, cfg, width, height, prompt_sha, negprompt_sha, adapters_json)。把每个元组连同输出一起记录。 - 在每张保存图旁边存完整的元数据 JSON。若工具支持,把它嵌入 PNG。
- 在你的服务商客户端里钉住模型版本;订阅服务商的”模型即将废弃”邮件列表,提前知道哪个版本要下线。
- 对几个月后可能要重跑的客户项目,本地快照模型文件。
- 本地管线默认开启确定性模式;只有需要吞吐时再关掉。
- 复制粘贴后用
cmp比对提示词;不要相信肉眼。
常见坑
- 以为”锁了 seed”就够了。Seed 只是约十个参数之一。
- 通过 Slack / Notion 复制提示词,引号和破折号会被悄悄改写。
- 相信托管服务商没升级。查版本号字符串。
- 信任 UI 里的”auto”采样器,跨发布是非确定性的。
- 肉眼比较两张图就宣布”一样”,而像素 diff 说不一样——真正的可复现性需要像素哈希相等。
- 跳过后处理剥除——人脸增强器是头号隐藏随机源。
FAQ
Q:我什么都锁了,但图像还是略有差异。为什么?
很可能是 GPU 上的 cudnn 非确定性。开启 torch.use_deterministic_algorithms(True)。如果运行的 GPU 型号与原图不同,可能永远拿不到逐字节一致——接受视觉等价就好。
Q:Midjourney 不让我设 seed,怎么办?
Midjourney 有 --seed 参数,但模型频繁升级,--seed 是尽力而为。对可复现性要求严格的工作请用钉版本的 SDXL 或 Flux。
Q:我直接保存输出文件不就行了,何必复现?
成片确实应该保存 PNG。可复现性的价值在于迭代时(改一个参数干净地看效果),或一年前的图被客户要新变体时。
Q:增加步数会让输出更确定吗?
不会。步数多只是降噪更彻底,并不能让 seed-to-image 函数更稳定。确定性是整个管线配置的属性,不是步数的。