返回博客

2026.10.22

两个权重文件一个字节都不差,我拿错的那个跑完了整批实验:修好之后,为什么反而多跑偏了 3 个镜头?

一个能在本地跑的视频模型有两个检查点,文件大小都是 20,970,379,616 字节,张量形状零差异,只有 md5 不一样。我的脚本装错了一整批实验,修好重测,第一次测不出差别,第二次发现「错的」那个反而更好。这篇讲两件事:一个对照实验在开跑前就能算出「注定测不出」的乘法,以及别人贴 A/B 结果时你该先问的三句话。

视频生成实验设计对照实验方法论

我修好了一个 bug,同样 24 个镜头,角色跑偏的从 5 个变成了 8 个。

修之前,我一直装着一个拿错的权重文件。它和正确那个大小都是 20,970,379,616 字节,一个字节都不差。

更难受的是中间那一轮:我换上正确的权重重测了一遍,跑了两个小时,结果是「没差别」。其实那一轮从设计上就测不出任何差别,开跑前做一个乘法就能知道。第 2 节就讲这个乘法,想直接拿走的看那节;第 4 节是怎么拿它去读别人的 A/B。

1. 两个文件,怎么会一样大

背景先交代一句。我在自己的卡上用一个本地视频模型拍一段 24 个镜头的小短片,测的是:同一个角色从第 1 镜拍到第 24 镜,脸和场景会不会慢慢跑偏。每个镜头拍完交给一个看图模型判一次「跑偏了 / 没跑偏」,跑偏的镜头数除以 24,就是漂移率,越低越好。

这个模型有两个检查点(可以理解成同一套架构、训练目标不同的两份成品权重):

  • 首尾帧版:给它一张起始画面,它往下接着拍
  • 参考图版:给它几张角色参考照,它照着人拍

我在测「参考图」这条路,按理该装参考图版。有人看了我的配置问了一句:你一直把这两个当成同一个在用吗,它们分别是什么?

我一查,心里凉了半截。下载参考图版那天我其实看到过两个文件一样大,当时心里过了一下,觉得「同规格嘛,正常」,就没再深究。现在把两份文件摊开对:

首尾帧版 参考图版
文件大小 20,970,379,616 字节 20,970,379,616 字节
张量数 932 932
形状差异 0 0
元数据 空 空
md5 不同 不同

前四行全一样,只有最后一行能把它俩分开。

然后是更要命的一条:加载权重的那个节点根本不检查你给的是哪个版本。装错了照样加载、照样出片,一条报错都没有。

病灶在我自己的跑批代码里。负责批量提交的那段,在需要参考图的时候把生成节点换成了「参考图模式」,但加载权重的那一行没跟着换,文件名是写死的首尾帧版。所以走那条队列跑的所有「参考图实验」,一次都没真正用过参考图版的权重。

那段代码往上数五行,有一条我自己写的注释,大意是「这是今天第六次抓到同一类病:配置被静默忽略,不报错」。这是第七次,就在那条注释下面三行。

这里能拿走的判据很短:判断两份权重是不是同一份,大小、张量数、形状、元数据都不算数,只有哈希算。 加载成功、出片正常,也不算数。

2. 第一次重测:花了两小时,测了个寂寞

修好代码,重启跑批进程(这一步差点又白跑:进程中午就起了,代码晚上才改,内存里跑的还是旧代码),我拿正确的参考图版重跑了之前那版最好的配置。

那版配置叫「混合锚定」:每 6 个镜头算一幕,幕首那一镜用参考图生成,定住角色长相;幕内后面 5 镜用上一镜的最后一帧当起点接着拍,保证动作连贯。之前装着错权重时,它跑出 7/24 = 29%,是五个版本里最好的。

开跑前我写死了判据:落在 6 到 8 个镜头(24%–34%)之间就判「没有实质差异」。

结果 8/24 = 33%,落在带内。「29% 是被错权重拖住的天花板」这个猜想,证伪了。

然后我去数据库里把 24 条记录的生成模式逐条读出来,才看到问题:

第 1、7、13、19 镜:参考图模式   ← 只有幕首这 4 个镜头
其余 20 镜:        接龙模式     ← 跟换哪个权重没关系

换权重这个改动,只碰到了 4/24 = 17% 的镜头。拆开看:

漂移
参考图模式的 4 个幕首镜 1/4 = 25%
接龙模式的 20 镜 7/20 = 35%

总分 33% 的大头,来自压根没被这次修复影响的那 20 镜。

这就是开头说的那个乘法。一个对照实验能不能测出东西,开跑前就能算:

预期位移(个) ≈ 变量的真实效应 × 被这个变量碰到的样本数
能测出来  ⇔  预期位移 > 噪声带

没有数据的时候不知道「真实效应」有多大,那就先拿上限算:被碰到的样本全部翻转,最多也就动 4 个镜头。我的噪声带是 ±2 个镜头。上限 4 个,只有在那 4 个幕首镜全部朝同一个方向翻的极端情况下才刚好跨过去,正常情况根本够不着。

事后用第 3 节测出来的真实效应(12.5 个百分点)代进去:12.5% × 4 ≈ 0.5 个镜头。半个镜头,对上 ±2 的噪声带。这一轮从设计那一刻起就注定是「没差别」。

再配一个更短的式子,专门看样本够不够细:

分辨率 = 1 ÷ 样本数

24 个镜头,1 镜 = 4.2 个百分点;只看那 4 个幕首镜,1 镜 = 25 个百分点。「幕首镜 1/4 = 25%」这个数,换一个镜头就跳成 0% 或 50%,拿它对参考图版本身下任何结论都是在掷硬币。

这两个式子都是小学算术,我在开跑前一个都没算。两小时的卡时就是这么没的。

3. 第二次:把作用面拉到 100%,「错」的那个赢了

既然问题出在只碰到 17% 的镜头,那就让每个镜头都走参考图模式。我把每幕的长度设成 1 镜,这样 24 个镜头每一个都是「幕首」,作用面 100%。两组只换权重,其余全一样。

开跑前照样先算一遍:

第一次(混合锚定) 第二次(全参考图)
作用面 17%(4/24) 100%(24/24)
效应上限 4 镜 24 镜
噪声带 ±2 镜 ±2 镜
判断 基本测不出 值得跑

判据照样提前写死:差 3 个镜头及以上才算信号,差 2 个以内判「没有实质差异」。

结果:

组 权重 漂移
A 参考图版(「对」的那个) 8/24 = 33%
B 首尾帧版(「错」的那个) 5/24 = 21%

差 3 个镜头,12.5 个百分点,刚好压在我写死的线上。两组跑偏的镜头也基本不重合,交集只有 2 个。

我第一反应是先怀疑自己。毕竟这一天里配置被静默忽略已经抓到七次了,判据里也写着「结果反常先查参数」。所以把两组共 48 条记录逐列对了一遍:LoRA、步数、强度、分辨率、帧数、采样器、调度器、参考图组,全部只有一种取值;起始帧字段,两组都是 0 条非空。真正变的只有那一个权重文件。另外两组的提交我都直接从生成队列里读回来确认过,没信跑批脚本自己打的日志。

光这一步就救了我一次。B 组中途跑批进程崩了,自动重启的时候丢掉了指定权重的那个环境变量,它的默认值恰好是参考图版。要不是去读了进程的真实环境,B 组就会变成「参考图版 vs 参考图版」,我还会认真地分析为什么两组差不多。

所以那个 bug 是真的,修它也是对的,只是在这台机器、这个短片上,修完之后变差了。之前那五版「装错权重」跑出来的数字,没有被高估,反而用的是表现更好的那个。

4. 别人贴 A/B 结果时,先问这三句

我这次自己就生产了一个该被拆穿的数字:「29% 是锚定策略的天花板」。它听上去有理有据,五个版本里三次都卡在 29%。拆它用的就是下面三句,拿去读任何人的「换了 X,提升了 Y%」都适用。

第一句:X 实际碰到了多少比例的样本?

一个改动只作用在 17% 的样本上,它在总分里能留下的痕迹最多也就那 17%。对方只报总分、不报作用面,那个「没差别」或者「涨了」都要打问号。

第二句:同一个配置跑两遍,差多少?

我在混合锚定配置下试过:同配置、同种子复跑,输出的 md5 都不一样。那一次我差点把这种运行间的抖动解释成「新策略在第一段就生效了」,解释得头头是道,但源码白纸黑字写着那个策略只作用于续写段。没量过噪声本底的 A/B,差 1 个百分点和差 10 个百分点你都分不清是不是信号。

第三句:两个数是不是只差了这一个变量?

我还在这批数据上翻过一次车。从两个版本里聚合出一个漂亮的「第 3 层断崖」:离幕首 0–2 镜的漂移 12%,3 镜及以后 62%(48 个镜头的样本)。据此预测把每幕缩到 3 镜能压到 20% 以下,结果实测 29%,预测失败。原因是层数和拍摄场景是共线的:离幕首近的恰好大多是人站在柜台后面的稳定镜头,离得远的恰好大多是门口、屏幕特写这种难拍的。

把场景单独拆出来,三种完全不同的锚定策略下排序都一样:

场景 漂移(三版合计)
柜台(人在固定位置) 9/42 = 21%
门口(人 + 门 + 雨 + 街景) 8/18 = 44%
屏幕特写(画面主体是画中画) 8/12 = 67%

差 3 倍。锚定策略怎么改都在 29%–46% 之间晃,换权重也没跳出 21%–33%。真正决定漂移的是拍什么,怎么接反而是小头。救我那次的是预测提前写进了文件,写死了「20% 以下才算成功」。没写死的话,29% 和最好那版持平,我大概率会说成「持平说明也可行」。

5. 这个结论现在还站不住的地方

把话说死之前,这几条得摊开:

  • 只有 4 步。 我用的是加速采样,每镜 4 步。官方文档写得很明白,4 步是最低值,推荐 6–8 步。参考图版很可能需要更多步数才发挥得出来。所以正确的说法是:「这个短片、这组参考图、4 步下,首尾帧版的漂移低 3/24。」不能说首尾帧版比参考图版好。
  • n = 1,而且刚好压线。 3 个镜头的差距正好等于我的阈值。全参考图这个配置下我没单独量过噪声本底,混合锚定下量过一次,同配置复跑结果就是会变。
  • 单一指标。 漂移率只管身份和场景是不是一致,不管画质、不管动作幅度、不管叙事接不接得上。
  • 一条跟我之前说法冲突的信号。 我之前写过「参考图模式不吃上一镜,所以不会累积漂移」。可 A 组前 12 镜漂移 2/12 = 17%,后 12 镜 6/12 = 50%;B 组反而是前 3/12 = 25%、后 2/12 = 17%。半段只有 12 个镜头,1 镜就是 8.3 个百分点,这条只记录,不当结论。
  • B 组跑了三次才成。 第一次在第 4 镜报显存非法访问,第二次在第 1 镜报一个自相矛盾的显存不足(要 125 MiB,说剩 61 GiB),查下来是驱动死了。重启整机之后第三次 24/24 跑完。前两次的残骸没有混进结果,但这说明长时间高压跑批本身也是个不稳定因素。

还有一条元教训,跟技术无关。发现装错权重的前一天,我还编过一句「参考图版需要另一套 144GB 的权重」,白烧了 4 个小时卡时。纠正的时候我又一下滑到另一头,说成「两个版本共用同一套权重」。正确答案在中间:共用架构和加载节点,但是两份不同的检查点。推翻一个错结论,跟写下它一样需要查证。

接下来按性价比该测的:先在全参考图配置下同种子复跑 8 个镜头,量出噪声本底,这一步最便宜,直接决定这 3 镜之差信不信得过;再跑 6 步和 8 步,看参考图版会不会反超。

6. 下次做对照实验,照这个顺序

  1. 先确认你装上的就是你以为的那个。 权重、配置、环境变量,一律从下游读回来:哈希对文件,队列对提交,进程环境对变量。日志是程序的自述,不算。
  2. 改完代码,确认跑的进程是改完之后才起的。 进程启动时间早于文件修改时间,就是在跑旧代码。
  3. 数一下这个变量碰到了多少样本。 用「预期位移 ≈ 效应 × 被碰到的样本数」算上限,上限都跨不过噪声带,就别跑,先改设计把作用面拉上去。
  4. 用「分辨率 = 1 ÷ 样本数」看你的样本够不够细。 1 个样本就是 25 个百分点的子组,不要拿来下结论。
  5. 先量噪声本底。 同配置跑两遍,看差多少。这个数就是你的噪声带。
  6. 判据和预测写进文件再开跑。 阈值、枚举结果、停止条件,跑完不许改。
  7. 结果反常,先逐列对参数。 对完确认只差一个变量,再相信它。
  8. 写结论时把作用域一起写上。 哪个样本、哪组参数、几次运行,缺一个就别裸引用。

修一个 bug 修出负收益这种事,我以为只会发生在别人的代码里。