Ref2VA 768p × 15s 容量修复 — 完整实验记录

这个页面是给 2026-08-13 那次 768p+15s 容量修复过程留的完整证据链,不加修饰,包括失败的(OOM 报错原样贴出)。核心问题:用户当时的原话是"压缩到和输出视频一样的分辨率",我实际部署的是压到约 120px(0.02 百万像素),比这个激进很多,需要拿真实产物出来对比让用户自己判断值不值。

根因:节点默认给参考视频的 token 预算和输出分辨率无关

MiniMaxH3ReferenceToVideo 节点内部有个写死的 adapt_canvas():不管你选 480p/768p/1080p 哪个输出档,参考视频永远按"短边768、面积上限768×1344"这个固定预算编码(除非源视频本身更小,那就不放大)。这些参考 token 会贯穿全部采样步。所以"压缩到和输出一样"这个思路,对 768p 输出来说约等于什么都不做——而"什么都不做"正是下面第一组测试里 OOM 的原因。要把 5 张图 + 2 条视频撑到 768p/15s,必须把参考视频压到比 768p 明显更小。

同一个 case,三种压缩级别,直接对比画质

同一张参考图、同一条参考视频、同一个 seed(4242)、768p、15秒、8步,只有参考视频的压缩程度不同。这是真正意义上的"同一个东西"对比,不是不同 case 之间的感觉。

native(完全不压缩)
768p/15s · OOM 失败,没有产物
Allocation on device 0 would exceed allowed memory. (out of memory)
Currently allocated: 23.45 GiB
Requested: 8.17 GiB
peak_vram: 30584 MiB
elapsed: 234.2s(跑到失败为止)
256px 短边(0.035MP)
✅ 成功 · 681.5s · 峰值显存 31160MiB
120px 短边(0.02MP,生产环境现在用这个
✅ 成功 · 669.5s · 峰值显存 31320MiB

原始素材:参考图 · 参考视频原片(480×704)

更重的组合:5张图 + 2条视频(0-5×0-2 矩阵里最重的一格)

160px(测试脚本,非生产配置)
✅ 成功 · 峰值显存 31496MiB
120px(生产环境真实验证,未清空显存缓存的 GPU4)
✅ 成功 · 458.4s

这条是直接打生产 API 跑出来的(image0..4 + video0/1 全部 5 图 2 视频),而且是在 GPU4 已经因为之前任务残留显存碎片的真实状态下测的,不是干净重启的卡——更接近实际排队场景。同样 0.035MP(256px) 在这个最重组合上第一次是失败的(碎片化显存下 OOM),说明重的组合下 256px 余量不够,这也是最终选 120px 而不是 256px 的直接原因。

2图 + 2视频(256px 已经够用的中间档位)

256px
✅ 成功 · 峰值显存 31580MiB

完整 18 格矩阵(int8,参考视频压缩到 0.035MP≈256px,768p,15秒)

参考图 \ 参考视频0条1条2条
0张387s378s429s
1张357s381s426s
2张362s405s436s
3张366s408sOOM
4张378s465sOOM
5张396s471sOOM

这张表用的是 int8_convrot(生产同款底模)。同一批还测了 nvfp4,结果反而更差更慢(nvfp4 在 3-5张图+2视频也 OOM,比 int8 覆盖范围更小),所以没有换底模。表里的 0.035MP 是这次压测用的固定值,最后 4/5 张图+2条视频这两格在真实碎片化显存下失败(见上面的对照),所以生产环境最终统一用了更保守的 0.02MP,不是按格子分别调的。

"768 原比例"实验:参考视频完全不压缩,只是拉伸到和输出画布一样

用户原话:"如果保留和输出视频相同的比例,也就占一倍的空间吧应该?那768p应该能跑的呀?" ——这个实验就是直接验证这句话:参考视频帧数完全不变(362帧,和输出等长),只把每一帧的分辨率强制拉伸到跟 768p 输出画布同一个比例/规格(不是缩小,是精确匹配),然后看能不能跑。

组合参考视频规格结果报错
0图 + 1视频768×1126,362帧,和输出画布同比例❌ OOM Requested 8.56 GiB, allocated 26.30 GiB, limit 31.36 GiB
1图 + 1视频同上❌ OOM Requested 8.64 GiB, allocated 26.54 GiB, limit 31.36 GiB
0图 + 2视频768×1126 + 768×972,各362帧❌ OOM Requested 12.16 GiB, allocated 24.31 GiB, limit 31.36 GiB

结论:哪怕只挂 1 条参考视频、0 张参考图,只要这条视频跟输出视频"同比例同规格",362帧 15秒的 768p 输出就直接 OOM。"占一倍空间"的直觉不成立——原因见下面的计算拆解:参考视频贵在帧数,不是分辨率,一条和输出等长的参考视频,哪怕分辨率再小,光时间维度的开销就摊掉了输出本身 3 成以上的预算;分辨率再拉到跟输出一样大,直接干到 85% 以上,几乎是又生成了一份输出。

严格计算:分块拆解 token 预算,每一块怎么影响 OOM

公式直接从 comfy_extras/nodes_minimax_h3.py 源码扣出来,不是拍脑袋估的。

基础公式

每个"视频类"输入(输出视频本身、或一条参考视频)占用的 token 数:

token = 时间维度(latent_t) × 空间维度(spatial)

latent_t(帧数 F) = 2                       若 F ≤ 5
                  = ((F-5)//17)×5 + 2        否则
                  (VAE 时间压缩约3.4倍:362帧 → 107个latent时间步)

spatial(像素面积 A) = A / 256
                  (VAE 空间压缩 16×16 = 256倍)

参考图片("match"模式):缩放到不超过输出画布的像素面积,时间维度按最小值 2 估算(图片开销本身很小,这个近似不太影响结论)。

参考视频的帧数会先按 min(源视频帧数, 输出帧数) 截断,再往下取整吸附到 17k+5 网格——如果源视频比输出还长,也不会超过输出帧数,但只要源视频≥输出时长(我们这次源视频恰好也是362帧,跟15秒输出一样长),这个截断不会帮你省任何东西。

768p/15秒(362帧)输出的 base 预算

base = latent_t(362) × spatial(1344×768)
     = 107 × 4032
     = 431,424 token

1条完整(362帧)参考视频,不同分辨率下占多少 token

参考视频规格token占 base 比例
120px 短边(0.02MP,生产现在用这个)8,3591.9%
256px 短边(0.035MP)14,6293.4%
160px 短边15,6493.6%
原生 ~480p(不特意压缩,源视频本身大小)141,24032.7%
768 原比例(跟输出画布完全一致,用户这次要求的实验)361,44683.8%

同一条视频,从120px压到"跟输出一样大",token 从 1.9% 暴涨到 83.8%——差了 43 倍。这就是为什么"保持原比例"和"压小"之间画质/容量的取舍这么剧烈,不是线性的、也不是"多一倍"这种量级。

先纠正我自己的一个错误

我最初按"额外token占base的百分比"去校准分界线,用的是 480p/768p/1080p 三档的旧压测数据。结果发现同一个百分比在不同分辨率下判断不一致——追查下去发现是压测脚本自己的探测逻辑有问题:它用一个"跨档共享的显存上限"做剪枝,如果同一档里先测到的、比较轻的组合在 362 帧失败了,后面更重的组合会直接被跳过 362 帧、只测更小的候选值——不是真的测过 362 帧失败,是被跳过没测。我一开始把这些"没测过"的当成了"测过且失败",得出的百分比结论是错的。改用只看真正测过的成功/失败边界之后,结论完全不一样,见下面。

修正后:用"绝对 token 数"而不是"占比"来定界,跨分辨率校验过

把 480p/768p/1080p 三档里真正测到失败-成功边界的组合(不是被剪枝跳过的)、加上这次专门测的几组,全部换算成绝对 token 数:

组合状态绝对token
768p 5图2视频 @160px✅成功503,042
768p 2图2视频 @256px✅成功476,810
768p 0图2视频 @243帧(原生~480p视频)✅成功467,424
1080p 0图2视频 @124帧(原生~480p视频)✅成功392,940
768p 2图2视频 @243帧(原生~480p视频)❌OOM483,552
768p 1图1视频 @原生~480p、362帧❌OOM580,728
768p 0图1视频 @362帧(原生~480p视频)❌OOM563,034
1080p 0图0视频 @362帧(纯输出,不含任何参考)❌OOM873,120

最高的确认成功案例(503,042)和最低的确认失败案例(483,552)有交叉——差距只有约4%,这个量级的重叠就是显存碎片化造成的真实噪声(同一个配置在干净卡上成功、在跑过其他任务的卡上失败,我们已经在生产上真实遇到过一次)。

结论:绝对 token 数是比"占base百分比"更可靠的预测指标,且不随分辨率档位漂移(横跨480p/768p/1080p验证一致)。安全阈值建议定在 ≤450,000 token(留够碎片化余量);450,000~500,000 之间是灰色地带,可能成功可能失败;超过500,000 基本必炸。

手把手:你自己怎么算

以"768p输出、15秒(362帧)、1张参考图、1条参考视频(源视频原本也是362帧,压到120px)"为例,一步步algorithm:

第1步 输出本身的 token:
  latent_t(362) = ((362-5)//17)×5+2 = 21×5+2 = 107
  spatial(1344×768) = 1344×768/256 = 4032
  base = 107 × 4032 = 431,424

第2步 参考图的 token(1张,"match"模式缩到输出画布面积):
  spatial(1344×768) = 4032,时间维度按最小值2估
  img_tok = 4032 × 2 = 8,064

第3步 参考视频的 token(1条,源视频362帧,压到120px/0.02MP):
  帧数先截断: min(362源视频帧数, 362输出帧数) = 362,再吸附17k+5网格,仍是362
  latent_t(362) = 107 (和输出一样,因为帧数一样)
  spatial(0.02MP) = 20,000/256 = 78.1
  vid_tok = 107 × 78.1 = 8,359

第4步 总和:
  总token = base + img_tok + vid_tok
          = 431,424 + 8,064 + 8,359
          = 447,847   ← 低于 450,000 安全阈值,应该稳

(这正好对应我们实测的"1图1视频@120px"那组,实测确实成功,总token算出来是447,847,和上面手算一致)

要推广到别的规格,把第1步换成你要的输出分辨率/时长;第2步按参考图数量重复;第3步是关键——每条参考视频要单独算,且要用那条视频自己的(帧数,压缩后像素面积),帧数是最敏感的变量,压缩分辨率对token是线性的,但"视频比输出短"这件事能省下的更多(下面有专门验证)。

怎样可能保留得更好:压"帧数"而不是压"分辨率"

从公式能看出来:token = 帧数决定的latent_t × 分辨率决定的spatial,是乘法关系。目前生产的做法是保留参考视频全部362帧、疯狂压分辨率(到120px)。另一个思路:把参考视频裁短(比如只取前5秒=124帧),省下来的token预算换成更高的分辨率——同样的token价格,362帧配120px,vs 124帧配约200px(124帧的latent_t=37,是362帧的107的约1/3,所以分辨率预算能扩大约3倍,短边从120px提到约200px)。

代价:只保留参考视频的前5秒,如果视频后半段有主要动作,会丢失那部分动作信息——这是"空间清晰度"和"时间覆盖度"之间的真实取舍,不是免费午餐。

⚠ 老实说这组对比有个瑕疵:测试用的 prompt 统一写的是"A person walks across a sunlit room",但参考图其实是两只猫(橘猫+灰猫)——prompt 说"人",模型就真的生成了人,没有照参考图的猫身份走。这个不一致在今天所有对比里都一样(120px/256px/裁帧版全用的这同一个 prompt),所以几组之间的相对对比依然公平,但没有一组真实体现"参考图身份保留得好不好",这一点如果要严谨评估画质还需要重新用匹配的 prompt 补测。

当前生产方案:362帧(完整) @ 120px
替代方案:裁到124帧(前5秒) @ 约200px
✅ 成功 · 420.5s · 峰值显存 31192MiB

目前的取舍,等你拍板

现在生产环境所有 case 统一用 0.02MP(约120px),这样能保证最重的组合(5图+2视频)在显存碎片化的真实条件下也稳定成功。代价是所有 case(包括只有1张图1条视频的轻量组合)的参考视频画质都被压到这个程度,尽管上面的对比看起来肉眼差异很小。

如果你觉得没必要"一刀切",可选的方向: