DuoMatching 这类视频扩散蒸馏方案,通常把工作拆成三个可检查的角色:教师负责给出多步或更充分的去噪预测,学生负责在少步设置下复现这条轨迹,匹配环节负责在采样路径上把学生与教师的预测对齐。看到仓库名想知道它具体做哪一段、教师和学生在代码里怎么对上,靠读 README 往往不够,更有效的方式是回到目录、配置和一次最小前向:先在仓库里定位教师、学生、匹配三处代码,再核对配置里哪些字段决定结构、哪些只是训练超参,最后用一次小规模前向确认匹配分支真的被调用,而不是只跑通了数据加载。
判断 DuoMatching 是否接对,不用看论文结论,看三处:配置里学生的结构字段与教师权重路径是否分开写清;匹配模块是否在前向路径上被真实调用;采样步数配置改动后,日志中的学生分支调用次数是否随之变化。三者都对得上,再谈训练;任一环缺失,先补定位,不要直接开训。
在仓库目录里分清教师模型、学生模型和匹配模块的代码位置
这类蒸馏仓库的目录通常分几层:configs/ 放实验配置,models/、networks/ 或 modules/ 放网络定义,trainer/、engine/ 或 pipelines/ 放训练与推理解析流程,scripts/ 放启动命令。教师模型多数不单独建类,而是复用同一套网络定义、加载不同权重,所以文件名上更容易看到的是 student 与 match。具体命名以你 clone 的版本为准,先用命令把候选列出来,不要凭猜测打开文件:
git ls-files | grep -Ei 'teacher|student|match|distill|sampler'
grep -rn `--include`=*.py -Ei 'def .*match|class .*Match' . | head -40
grep -rn `--include`=*.py 'teacher' . | grep -v test | head -40
第一条命令给出文件名候选,第二条定位匹配环节的定义位置(函数名或类名),第三条看教师权重在哪里被加载、被调用。需要认准的是“匹配环节被谁调用”:如果匹配函数只在测试文件或脚本里出现,说明训练主路径可能并没有走它。配置文件通常位于 configs/ 下,用 ls configs/ 确认实际文件名后再打开,不要照抄示例路径。找到入口脚本后,沿着 train 或 fit 的调用链往上读两层,把教师前向、学生前向、匹配计算三处的行号记下来,后面排查都以这三处为锚点。
对照配置文件确认教师与学生的初始化方式
配置文件里最容易混淆的是“决定学生网络结构”的字段和“只影响训练过程”的字段。判断方法很朴素:结构字段改动后,模型参数量或模块列表会变;超参字段改动后,只影响损失权重、优化器、步数调度。常见字段可以按下面方式归位,字段名以仓库实际为准:
- 结构类:
model.*、student.arch、student.hidden_size、num_layers、patch_size、in_channels这类字段,改一个就应该在模型构造函数里产生可见差异。 - 权重类:
teacher.ckpt_path、student.init_from,只决定从哪个文件加载,不改变结构;路径写错通常是加载即报错或打印缺失键。 - 训练超参:匹配损失权重(形如
lambda_*、loss.*_weight)、学习率、batch size、梯度累积,这些改动不应改变参数形状。 - 采样步数类:
num_inference_steps、timesteps、sampling.steps、schedule,进入第 4 节单独核对。
读完要打印确认,不要只靠肉眼扫 YAML 缩进。用下面这段在仓库根目录执行,把结构相关字段单独打出来:
python - <<'PY'
import yaml, json
cfg = yaml.safe_load(open("configs/你实际使用的配置.yaml"))
for key in ("model", "student", "teacher"):
if key in cfg:
print("==", key)
print(json.dumps(cfg[key], indent=2, ensure_ascii=False)[:1200])
PY
如果配置是分层展开的(例如通过 defaults 继承),打印前要先走仓库自己的配置加载函数,直接 yaml.safe_load 可能只拿到一半字段。确认方式是:打印结果里应当能同时看到学生结构与教师权重路径;缺一项就说明你读的是未合并的原始文件。
用最小规模的一次前向确认匹配环节真的被调用
只跑通数据加载会造成误判:日志里有一堆 batch 打印,看起来在训练,实际匹配分支被条件判断跳过了。验证手段是在匹配函数入口加一行打印或断点,然后用极小分辨率、单帧或两帧、batch 为 2 的输入跑一步:
def match_step(student_pred, teacher_pred, timesteps, **kw):
print("[match] called", tuple(student_pred.shape),
tuple(teacher_pred.shape), tuple(timesteps.shape))
breakpoint() # 只在本地调试时保留,提交前删除
...
期望看到的输入形状是:学生预测与教师预测形状一致,通常为 [B, C, T, H, W](latent 或像素空间,取决于仓库在 VAE 之前还是之后做匹配),时间步为 [B] 或 [B, T]。判定被调用成功的标准有三条同时满足:打印出现且只出现一次每步;两个预测张量形状相同、dtype 相同;返回的匹配损失是有限值(torch.isfinite(loss).all() 为真),且对学生的输出存在梯度(loss.backward() 后学生参数 grad 非 None)。若打印不出现,优先检查匹配步骤是否被包在 if step % k == 0 之类的分支里,以及教师是否处于 no_grad 状态却把学生也一起包了进去。
把匹配环节的输出与采样步数设置对上号
少步采样的关键证据是:步数配置改了,经过匹配环节的调用次数或时间步列表跟着变。先找到步数配置项(常见为 num_inference_steps、student.steps、sampling.timesteps),再在构造时间步的位置加一行打印:
print("[timesteps]", timesteps.tolist() if hasattr(timesteps, "tolist") else timesteps)
print("[match] pass", len(timesteps), "steps")
可复现的核对方式是:把步数从配置里的默认值改成另一个较小的值(例如 4 改成 2),重新跑同一个最小前向,观察 [timesteps] 列表长度与 [match] pass 打印次数是否同步变化。两者同步,说明学生确实按少步路径走并且每一步都经过匹配;只变一个,说明存在另一条绕过匹配的采样分支(例如推理脚本里的 dpm_solver 或独立 sampler),需要回到第 1 节确认训练用的是 trainer 还是 scripts/inference。只记录能重复出现的现象:改配置、重跑、对比打印,不要以一次随机波动的 loss 曲线作为判断依据。
整理一份角色—配置—验证点对照表
把上面的定位结果落成表,跑训练前逐行过一遍;某行填不出来,就说明那一环还没接上。
| 角色 | 配置项与代码位置 | 验证点(查看位置或命令) |
|---|---|---|
| 教师模型 | teacher.ckpt_path;加载代码在 models/ 或 trainer 的 setup 里 |
启动日志中的权重加载行;参数应全为 requires_grad=False,可在加载后打印 sum(p.numel()) |
| 学生模型 | model.* / student.* 的结构字段;构造函数所在文件 |
改一个结构字段重跑,打印 model 的模块列表应随之变化 |
| 匹配环节 | 匹配函数或类的定义处,以及 trainer 中调用它的位置 | 最小前向时 [match] 打印出现、两个预测形状一致、loss 有限且学生有梯度 |
| 采样步数 | num_inference_steps 或同名步数字段 |
改步数后 [timesteps] 长度与 [match] pass 次数同步变化 |
| 入口脚本 | scripts/ 下实际启动的训练脚本 |
脚本里 import 的 trainer 与配置文件路径与预期一致,避免跑成推理脚本 |
缺项时的补查顺序建议固定为:先 git ls-files 与 grep 补入口,再打印配置确认字段合并结果,然后加断点跑一次最小前向,最后改步数对比打印。不要跳过前两步直接训练,否则匹配没被调用的情况会被正常运行日志掩盖,直到采样效果异常才回头查,代价更高。各个字段名和目录结构在不同版本里会有差异,以上命名是常见写法,落到你的仓库时以实际 grep 结果为准。