同一 Agent、同一任务连跑两次分数不同,通常不是单一原因。生成侧没固定(种子、采样数、模型采样参数、分辨率)会改变输出内容;判分侧没固定(渲染后端、设备、降噪、判分器自身的随机源与阈值)会让同一份输出得到不同分。建议按“先留证据 → 固定生成 → 固定判分 → 对照设备 → 对照容差”的顺序排除,最后再决定这次抖动要不要重跑。
先用同一份输出重复判分三次:三次分项一致,抖动在生成侧,去固定种子、采样数和模型参数;判分本身在漂移,先修判分器的随机源和容差。生成侧固定后再复跑三次,用像素差与分项差对照任务容差。像素差接近零而总分跳变,多半是判分卡在阈值边界,不是渲染管线没固定。跨 CPU 与 GPU、跨不同驱动的位级一致一般做不到,只能按容差判断,不能直接判成管线问题。
记录两次运行的完整参数
没有可比对的原始记录,后面的三次复跑只会多出三份无法解释的结果。建议在每次运行开始前把参数落成一个快照文件,与输出文件放在同一目录,命名带 run_id。
需要记录的字段清单(缺一项都可能让复跑失去意义):
- 种子:生成侧随机种子、判分侧若用到大模型判分的采样种子。
- 采样数:渲染采样数(Cycles 的 samples)、EEVEE 的 TAA 渲染采样数,以及 Agent 模型侧的采样数与温度。
- 分辨率:输出分辨率与分辨率百分比,两者不一致时像素差会被误判成随机性。
- 模型参数:模型名与版本、温度、top_p、最大 token 数、系统提示词的哈希值。
- 环境字段:Blender 版本、渲染引擎、设备(CPU 或具体 GPU)、驱动版本、判分器版本、任务 id。
# 放在运行入口里,按实际参数名替换
run_id=$(date +%Y%m%d-%H%M%S)
mkdir -p logs/$run_id
blender `--version` > logs/$run_id/env.txt
python -c "import bpy;print(bpy.context.scene.render.engine); \
print(bpy.context.scene.cycles.samples)" >> logs/$run_id/env.txt
sha256sum runs/out/*.png > logs/$run_id/out.sha256
两次运行的快照文件直接做文本比对即可,差异行就是候选的抖动来源,比凭印象猜测可靠得多。
固定种子与采样后重复跑三次同一任务
这一步只验证生成侧。把种子、采样数、分辨率、模型温度同时钉死,其余保持与出问题那次完全一致,跑三次并分别落到独立目录。
for i in 1 2 3; do
python run_agent.py `--task` task.json \
`--seed` 42 `--samples` 64 `--resolution` 512x512 \
`--temperature` 0 `--out` runs/fixed/r$i
done
sha256sum runs/fixed/r*/out.png
python tools/image_diff.py runs/fixed/r1 runs/fixed/r2 `--metric` ae
观察三种结果:文件哈希三次完全一致,说明生成侧已经固定,抖动不在这里;哈希不同但逐像素差异很小,通常是浮点或设备差异,跳到渲染管线那一节;差异明显(构图、材质、相机位姿都变了),说明种子或模型采样参数没有真正生效,先查参数是否透传到实际执行的那一层,而不是改判分。
如果三次复跑里有一次明显偏离另外两次,把它单独留档,作为后面判断容差的样本。
用同一份输出重复判分三次
取上一步任意一份输出,固定不变,只让判分器重复执行三次。这一步的目的是把生成侧完全排除掉。
python judge.py `--task` task.json \
`--output` runs/fixed/r1 \
`--repeat` 3 `--out-json` logs/judge_r1.json
把三次的分项结果摊在同一个表里对照,不要只看总分:
| 分项 | 第 1 次 | 第 2 次 | 第 3 次 | 是否一致 |
|---|---|---|---|---|
| 总分 | ||||
| 几何/结构项 | ||||
| 材质与外观项 | ||||
| 相机与视角项 | ||||
| 约束/违规计数 |
三次分项完全相同,判分侧是稳的,可以在后续复跑中把它当作常量;分项在跳动,先查判分侧的随机源:判分渲染有没有重新随机采样、有没有采样点随机选取、大模型判分的温度是否为 0、阈值是否存在等于号边界。判分侧不稳时,任何生成侧的复跑结论都不可信。
检查渲染管线中受硬件影响的部分
像素级一致在跨设备时通常做不到,这一点需要在判断前先接受:同一场景在 CPU 与 GPU、在不同型号 GPU、在同一 GPU 的不同驱动版本上,浮点累加顺序不同,结果会落在很小的差异范围内。要确认的是差异是否被控制在这个范围内,而不是追求位级相同。
需要对照的字段:渲染引擎(Cycles 或 EEVEE)、计算设备(CPU / CUDA / OPTIX / HIP / METAL)、设备名称、降噪开关与降噪器类型、自适应采样开关、色彩管理与视图变换、Blender 版本与驱动版本。
import bpy
scene = bpy.context.scene
print("engine:", scene.render.engine)
print("device:", scene.cycles.device)
print("samples:", scene.cycles.samples)
print("denoise:", scene.cycles.use_denoising,
scene.cycles.denoiser)
print("view_transform:", scene.view_settings.view_transform)
在日志里定位方式:Blender 启动时的头部日志通常有一段设备枚举输出,搜索 Device 关键字能看到当前选中的后端;再搜索 Denois 或降噪器名称确认降噪是否被打开。两次运行的这两处输出不一致,就说明管线没固定,而不是种子问题。降噪器在低采样下对结果影响尤其明显,对比时保持开关一致再谈差异。
判断抖动是否可以接受
把容差先写下来,再去比对结果,顺序反过来容易变成事后解释。容差由任务本身决定:分数允许的绝对偏差、哪些分项必须完全一致、像素差异用什么指标衡量、超过多少就必须重跑。这些值需要结合任务定义与环境确认,不能照搬别人的数字。
判断规则可以先按这几条走:
- 同一输出重复判分三次,分项一致 → 判分侧稳定,抖动来自生成侧。
- 生成侧三次复跑像素差接近零,总分仍然跳变 → 检查判分阈值是否卡在边界,通常不是渲染问题。
- 生成侧像素差很小、分项趋势一致、总分差落在容差内 → 视为可接受抖动,保留记录即可,不必重跑。
- 总分差超过容差,或关键分项出现方向性变化 → 先固定设备与降噪设置再复跑一轮,仍超出则按未固定处理。
记录模板可以直接复用,把占位符换成实际值:
{
"task_id": "task.json",
"run_pair": ["r1", "r2"],
"seed_fixed": true,
"samples_fixed": true,
"engine": "CYCLES",
"device": "GPU-型号",
"denoise": "on",
"score_delta": null,
"per_item_delta": {},
"pixel_metric": null,
"tolerance": null,
"verdict": "acceptable | rerun | unfixed"
}
verdict 这一栏不要留空。留空意味着下次遇到同样问题还得从头查一遍,而填上之后,这个任务在同一环境下的抖动范围就有了可以对照的基线。