DuoMatching 的匹配环节最容易出错的地方不在损失函数,而在采样循环:教师和学生必须先读到同一份 x_t 与同一个时间步 t,然后再比较输出。判断顺序通常是先确认输入是否一致,再比较同一时间步下两边的预测差异,最后用固定随机种子重新运行,确认差异可以复现。如果教师与学生各自采样噪声,或者各自推进 scheduler,那么后面的任何对比数值都没有参考意义。
建议把检查重点放在采样循环里的 (t, x_t):确认噪声只采一次、时间步来自同一个 scheduler 实例、教师与学生读的是同一份张量。验证方式是在同一步取出两边预测做逐元素差值统计,再用固定种子复跑一次看结果是否稳定。若两边各自采样或各自逐步推进会破坏对齐,此时应先修正采样路径,不要继续调整损失或超参。该检查适用于能单机复现的采样路径,异步或分布式采样需另外确认。
在采样循环里定位教师与学生共用的时间步与噪声输入
先读循环本身,不要从损失里倒推。教师与学生的输入通常来自三个地方:latents(来自数据加载或编码器)、noise(随机生成)、timesteps(来自 scheduler)。只要这三个对象中有一个在两边被重新生成,起点就不一致了。下面是通用骨架代码,变量名可以替换为自己工程里的名字,重点是噪声只在循环外采一次、scheduler 只保留一份实例。
noise = torch.randn(latents.shape, generator=g, device=latents.device, dtype=latents.dtype)
x = scheduler.add_noise(latents, noise, scheduler.timesteps[:1])
for i, t in enumerate(scheduler.timesteps):
if i in probe_steps:
print("step", i, "t", int(t), "x_shape", tuple(x.shape),
"x_std", float(x.float().std()),
"noise_ptr", noise.data_ptr(), "x_ptr", x.data_ptr())
t_batch = t.expand(x.shape[0])
eps_teacher = teacher(x, t_batch)
eps_student = student(x, t_batch)
x = scheduler.step(eps_student, t, x).prev_sample
打印位置建议放在循环体的最前面,保证打印的是“进入这一步时”的状态,而不是推进之后的状态。需要重点确认:noise.data_ptr() 在整轮采样里是否始终不变;x_ptr 在教师与学生的调用之间是否相同;t 是否只有一个来源。如果工程里存在两份 scheduler(例如教师跟学生各自一份),两边 step 的内部计数可能已经不同,此时比较 scheduler.timesteps 的长度和当前索引即可看出偏移。
以下清单建议逐条确认:
- 噪声生成只出现在循环外一次,循环内没有任何
randn、randn_like或多卡广播后的重新采样。 - 教师与学生拿到的是同一个
x张量(同一 device、同一 dtype),不是各复制一份后经过不同前处理。 - 时间步来自同一个
timesteps序列,且没有在教师侧做t+1、t-1之类的偏移。 - 两个模型都处于 eval 模式;若任一模型开了 dropout 或训练态 BN,即使输入相同,输出也会不同。
记录同一步下两边的预测输出并做逐元素对比
只观察损失曲线无法区分是输入没对齐还是容量差异。建议在选定的一小段时间步(例如前几步和中间几步)保存两个预测张量,直接计算差值统计。下面是通用的统计片段,接口名按自己工程替换。
a, b = eps_teacher.float(), eps_student.float()
if a.shape != b.shape:
raise SystemExit(f"shape mismatch: {tuple(a.shape)} vs {tuple(b.shape)}")
d = a - b
stats = {
"step": i, "t": int(t), "shape": tuple(a.shape),
"max_abs": d.abs().max().item(),
"mean_abs": d.abs().mean().item(),
"rms": d.pow(2).mean().sqrt().item(),
"ref_abs": a.abs().mean().item(),
"cos": torch.nn.functional.cosine_similarity(
a.flatten(1), b.flatten(1), dim=1).mean().item(),
"finite": bool(torch.isfinite(d).all()),
}
判读顺序建议是:先看形状和 finite,形状不同就不要比数值;再看 cos,它衡量方向是否一致;然后看 mean_abs 与 ref_abs 的相对关系,而不是只看绝对量,因为不同时间步下预测的量级本身会变。若某些步的 max_abs 明显高于相邻步,通常是那几步存在额外分支(例如不同的 guidance 设置或条件注入没传给学生)。每个时间步都记录,比只记录最后一次更方便定位。
固定随机种子复跑一次,确认对比结果可复现
单次运行里的差异可能来自随机初始化或采样噪声。建议把种子固定在三个位置:进程级随机、CUDA 随机、以及噪声用的 torch.Generator。另外数据加载的 worker 顺序也建议固定,否则 batch 顺序变化会带来输入差异。
def set_seed(seed, device):
random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
return torch.Generator(device=device).manual_seed(seed)
g = set_seed(cfg.seed, latents.device)
# ... 采样与统计 ...
torch.save(stats_list, f"align_seed{cfg.seed}_run{runtag}.pt")
两次运行分别保存统计结果后,直接比较 max_abs、mean_abs、cos 三个字段。理想情况是同种子两次运行的对应字段完全相等;出现极小浮点差异通常是算子实现差异,需要在同一机器同一后端上再确认。若差异明显,按这个顺序排查:噪声是否在循环内被重采;两个模型是否都处于 eval 模式;是否有未固定的 dataloader 顺序;是否开启了不确定性的算子或图优化;两次运行的配置是否真的相同(打印 config 哈希最省事)。
把对齐检查整理成一个可复用的小脚本
把上面三段检查收进一个脚本,每次改配置后跑一次,比在训练里反复猜要快。脚本只需要读配置、跑若干步采样、输出 JSON,不参与训练。
# check_align.py
# 用法: python check_align.py `--config` cfg/duo.yaml `--steps` 0,5,10 `--seed` 1234 `--out` align.json
def main(args):
cfg = load_config(args.config) # 替换为工程内的配置读取方式
pipe = build_pipeline(cfg) # 替换为工程内的构建入口
g = set_seed(args.seed, pipe.device)
records = run_probe(pipe, cfg, args.steps, g)
save_json(records, args.out)
return verdict(records, cfg.align_thresholds)
输入输出约定建议写清楚:输入是配置文件路径、探针步列表、种子、输出路径;输出是一份 JSON 数组,每条记录包含 step、t、shape、max_abs、mean_abs、cos、finite。退出码建议分开:0 表示形状一致且数值有限、在阈值内;1 表示教师与学生形状不一致;2 表示出现 nan 或 inf;3 表示数值超出配置阈值。失败时的提示信息要能直接指向动作,例如“step 5: teacher shape (2,4,64,64) != student shape (2,4,32,32),检查下采样层或 latent 分辨率配置”,而不是只打印一个 False。阈值从配置文件读,不要写死在脚本里,也不要为了让脚本通过而放宽阈值。脚本里不要引入工程中不存在的接口,构建入口按实际代码替换。
记录不同对齐时机下的现象对照表
把每次实验的时机选择和观察到的现象写进一张表,方便下次改配置时对照。表里只填能通过日志、脚本输出或页面行为复现的内容,不要填主观印象。
| 对齐时机 | 共享的输入 | 记录位置 | 观察点 | 可复现现象 |
|---|---|---|---|---|
| 同噪声起点 + 同时间步 + 单 scheduler | noise、x_t、t 全部共用 | 循环体首行打印 + 探针统计 | shape 一致、finite 为真、cos 与差值统计在两次同种子运行中一致 | 同种子下两次运行的统计字段相等或仅有极小浮点差异 |
| 同噪声起点,两边各自推进 scheduler | 初始 noise 相同,后续 x_t 不同 | 两边的步索引与 x_std | 步索引错位、x_std 随步数分化 | x_ptr 不同;差值统计随步数增大而发散 |
| 两边各自采噪声,仅对齐时间步 | 只有 t 相同 | 循环内打印 noise_ptr | noise_ptr 每步变化或在两侧不同 | 同种子复跑时统计字段不稳定 |
| 只在最终输出上比较 | 无中间共享量 | 采样结束后的输出张量 | 输出差异无法归因到具体时间步 | 无法定位是输入问题还是模型差异 |
表格的边界说明要一起写在表下面:这里记录的是采样路径与中间量层面的现象,只覆盖能在单机、单进程、固定种子条件下重复出来的情况;分布式采样、异步调度或带缓存的推理路径,中间量可能出现与上表不同的表现,需要单独确认后再填入。不要在这张表里写效果收益或性能结论,那些结论需要另一套可核验的度量,与对齐检查是两件事。