VSR Hover Comparison

768p → 2× RTX 5090

鼠标在画面上左右移动即可擦除对比 —— 光标左边是 A,右边是 B。两条视频严格同帧、同步播放。 开 放大镜 看纹理级差异;按 空格 播放/暂停, 逐帧。 暂停 / 逐帧 / 拖进度条时,两边会一起锁到同一帧的中点,控制条右侧显示当前帧号。

本轮的重点是 FlashVSR 原版 vs 调优版,页面默认就开在这两个上;其余方法作为基础参照保留。

📖 想弄懂原理和每一步为什么这么做,看 FlashVSR 是怎么工作的 · 学习笔记 —— 一步蒸馏 / 稀疏注意力 / 流式 KV / 条件解码器的原理, 我们改的每一处及其安全性论证,以及「怎么判断快了但没坏」的方法论。

调优版做到了什么

配置2688×1536 × 141 帧显存峰值
原版(full,kv 3.0,管线默认 VAE 分块)385.2 s29.15 GiB
调优版(tiny + 2 空间块)157.3 s20.13 GiB

2.5× 速度,−31% 显存。15s 容量:16:9 2688×1536 × 360 帧 409.9 s / 20.13 GiB; 21:9 3584×1536 × 360 帧 558.6 s / 22.19 GiB。21:9 只有靠空间分块才成立—— 整幅送进去会死在一个 2.15 GiB 的定长分配上。

但速度不是免费的,而且你未必需要付这笔钱。 2×2 拆解(见下方技术细节)证明感知分的损失全部来自换解码器kv_ratio 和分块都是白捡的。所以还有第三个配置: 保留作者的解码器,只吃内存优化
配置2688×1536×141显存峰值画质
原版(无分块)385.2 s29.15 GiB基准
画质优先:full + kv1.5 + 2 块 + 7% 解码重叠 147.2 s16.52 GiB 与原版同解码器,保真度还略好
速度优先:tiny + kv1.5 + 2 块 157.3 s20.13 GiB 感知分下降(MUSIQ −2.3,CLIP-IQA −0.06)
画质优先版:显存 29.15 → 16.52 GiB(−43%),同时还快 2.6×,画质不掉。 它比速度优先版更省 3.6 GiB,速度只慢 1.9×(不是原来以为的 2.4×)—— 因为砍掉解码重叠之后,Wan VAE 解码器不再是瓶颈。 15s 16:9 / 15s 21:9 / 24 GB 它全部跑得过如果你在意画质,现在几乎没有理由不用它。

24 GB(4090)实测:把 CUDA 分配器硬卡在 23.0 GiB(先用一个已知需 30.5 GiB 的配置做阳性对照, 确认 cap 真的会 OOM),上面四种场景全部通过;15s 21:9 改 3 块后峰值降到 16.44 GiB 而耗时不变(558.0 s), 是 24 GB 卡上更该用的档位。注意:这只模拟了显存预算,没有模拟 sm_89 kernel 和 Ada 更低的带宽, 真 4090 上耗时会更长。

数据集
片源
方法

单击设为 A(左),再次单击同一个设为 B(右)。 也可以直接点另一个方法替换 B。

A
B
载入中…
0.00 / 0.00 s

这条片子上的分数

FlashVSR 原版 vs 调优版 · 配对差值

两个配置跑的是同一批片子,所以看的是逐条配对差值,不是各自的均值—— 片子之间的难度差异会在配对里抵消掉。CI 是 2000 次 bootstrap 重采样,种子固定。 差值 = 调优版 − 原版;CI 不跨 0 才算真差异。

「原版」= 作者发布的模型配置(full 管线 / Wan VAE 解码器,kv_ratio 3.0), 跑在同一套内存脚手架里(输入 pad、尾部补 4 帧、scale=2、空间分块)—— 不共用这套脚手架,两次跑的就不是同一批像素,也就没法比。 所以表里原版在 MiniMax 上的峰值是 19.67 GiB 而不是 29.15 GiB: 29.15 是原版配置分块时的数字,那才是它自己跑 4 Mpx 的代价。

画质优先配置的容量 —— 15s / 21:9 / 24 GB 全过

full + kv1.5 + 空间分块,也就是作者的解码器 + 全部内存优化。 这些行是纯容量探针:关掉 PNG 落盘(--no-write)跑的, 因为 wall_s / 峰值都是围着模型量的,而写 360 张 4 Mpx 的 PNG 比被测的推理本身还久, 对结论没有贡献。所以这里的耗时不含落盘,同一条 5s 16:9 带落盘是 381.8 s、不带是 285.1 s。

场景23 GiB cap50% 重叠7% 重叠显存峰值
5s 16:9 2688×1536通过285.1 s147.2 s16.52 GiB
5s 21:9 3584×1536通过415.9 s209.9 s19.52 GiB
15s 16:9 2688×1536通过751.6 s385.0 s16.52 GiB
15s 21:9 3584×1536通过931.8 s468.2 s19.52 GiB
15s 16:9,改 3 块通过744.3 s14.15 GiB
15s 21:9,改 3 块通过1012.4 s14.15 GiB
不设 cap(对照) 763.1 / 965.0 s385.0 s(16:9)同上

带 cap 和不带 cap 的峰值完全一致、耗时相差 1.5–3.5%,说明这个配置没有贴着 24 GB 的上限—— 不是勉强挤进去的。15s 21:9 还剩 3.5 GiB 余量,换 3 块能再降到 14.15 GiB, 代价是慢 8%。
也就是说:要作者的画质、又要 15s 21:9、又要 24 GB,这三件事不冲突, 需要放弃的只是那 2.4× 的速度。

为什么要分块?分块到底有没有影响?

这里有两种完全不同的分块,之前混着说是我的表述问题:

分块是谁做的为什么要
空间分块
tile-px
我们加的,FlashVSR 原本没有 整幅 3584×1536 送进 DiT 会死在一个 2.15 GiB 的定长分配上, 这个分配跟任何旋钮都无关。不切块,21:9 根本跑不了。
VAE 解码分块
tile_size/stride
FlashVSR 自带的,默认开启 解码器一次吃不下整帧。它默认 50% 重叠是为了盖住接缝, 但重叠量给多了(见下一节)。

空间分块有没有影响?—— MiniMax-20 全量 A/B

专门跑了一组 --tile-px 1e12(永不分块)的对照,其余配置逐字相同。 分块只在每帧 > 3.0 Mpx 时触发,所以 20 条片子天然分成两组:

① 阴性对照(4 条 1:1,2.36 Mpx,两边都没分块): 逐字节 md5 比对 4/4 完全相同。这一步是必须的——如果这里就不一样, 说明管线本身不确定,下面所有数字都是噪声。

指标(16 条 >3 Mpx 的片子)分块不分块差值95% CI
MUSIQ ↑60.968060.9355+0.0325[−0.2494, +0.2895] 跨 0
CLIP-IQA ↑0.51100.5110+0.0000[−0.0045, +0.0041] 跨 0
MANIQA ↑0.34260.3431−0.0005[−0.0019, +0.0010] 跨 0
NIQE ↓4.31914.3649−0.0458[−0.0758, −0.0086]
Downsample-LPIPS ↓0.14710.1483−0.0012[−0.0028, +0.0004] 跨 0
「一点影响都没有」不准确,准确说法是:五个指标里四个的 CI 跨 0(测不出差异), 唯一测得出的 NIQE 还是分块更好一点(−0.046)。没有任何一个指标显示分块更差。 加上接缝检测(0.9σ / 1.1σ,远低于 4σ 判定线), 结论是空间分块在画质上不需要付代价——但它不是「零改变」, 输出确实不逐位相同,只是差异小到测不出方向。

切块只切长边、按 128 对齐、重叠 256 输出像素,融合用线性 ramp 互补权重。 【更正】我之前写过「重叠远大于模型 88 px 的感受野」——那个数字算错了。 正确推算:输出像素 →(VAE ÷8)→ latent →(DiT patch_size=[1,2,2] ÷2)→ token, 1 token = 16 px,1 注意力块 = 8×8 token = 128 px,local_range=11 块 → ±640 px重叠其实小于感受野,接缝附近确实看到截断的上下文—— 但上面的全量 A/B 和接缝检测都测不出代价,原因见 学习页 5.3 节
而且对 tiny 来说分块是双赢:2 块比整幅又快又省 34% 显存 (155.5 s / 20.13 GiB vs 163.4 s / 30.49 GiB)——注意力矩阵随边长平方增长, 切小之后省下的算力超过了跑两趟的开销。

先测再优化 —— 时间到底花在哪

在动任何优化之前先分块计时(FVPROF=1,每个计时器都 cuda.synchronize(),量的是真实 GPU 时间不是异步下发延迟)。 2688×1536 × 141 帧,不含落盘:

配置总耗时解码器DiT + 前处理
tiny(TCDecoder)74.8 s12.3 s 16%62.4 s 83%
full(Wan VAE 解码器)283.4 s220.1 s 78%63.3 s 22%

DiT 两边都是 ~63 s,两个配置的全部差距都在解码器上。 所以画质优先配置该优化的地方只有一个:那 220 秒。

解码重叠 —— 白烧掉的 140 秒

FlashVSR 的 pipeline 解码时用 tile_size=(60,104), tile_stride=(30,52), 也就是 50% 重叠——每个 latent 像素大约被解码 4 次。 重叠是用来盖住 VAE 分块接缝的,问题是需要这么多吗。

tile / stride重叠总耗时解码器显存峰值
60 / 30 (管线默认)50%287.3 s221.7 s16.52 GiB
60 / 4427%177.1 s113.3 s17.03 GiB
60 / 567%147.2 s81.3 s16.52 GiB
96 / 4850%228.1 s161.9 s19.87 GiB
96 / 888%154.3 s86.0 s19.87 GiB
128 / 1206%139.8 s24.50 GiB
160 / 1525%179.3 s30.41 GiB
256 / 2483%OOM>32 GiB

解码 221.7 s → 81.3 s,快 2.7×,显存一点没涨。 分块开大(96/128/160)反而更慢且吃显存——省时间的是减少重叠,不是加大分块。

但省下来的是不是画质?—— YouHQ40 全量 n=40 验过

指标7% 重叠50% 重叠差值95% CI
PSNR-Y ↑24.647424.6395+0.0079[+0.0065, +0.0094]
SSIM-Y ↑0.67080.6703+0.0005[+0.0004, +0.0006]
LPIPS ↓0.22330.2234−0.0000[−0.0001, +0.0001]
DISTS ↓0.10500.1053−0.0003[−0.0004, −0.0002]
MUSIQ ↑71.412671.4389−0.0263[−0.0397, −0.0141]
CLIP-IQA ↑0.61330.6182−0.0049[−0.0056, −0.0042]
NIQE ↓3.11853.1157+0.0028[−0.0123, +0.0245]

保真度略微变好(重叠融合本身就是一次轻微模糊),CLIP-IQA 掉 0.005—— 对比一下换解码器那次是掉 0.061,这里小一个数量级。 接缝专项检测:4 条片子 × 横竖两个方向 × 2 个接缝位置,相对 50% 重叠版的额外梯度 最大 3.7σ,全部低于 4σ 判定线;最大偏差点落在画面内容边缘而不是接缝上。无可见接缝。

这是本轮最划算的一处优化:画质优先配置 287.3 s → 147.2 s(快 1.95×),显存不变,画质不掉。 加上这条之后,画质优先版比原版快 2.6×(原版无分块 385.2 s), 而不是原来的「速度基本不变」。

torch.compile —— 试了,没用

配置eagercompile 解码器compile DiT两个都 compile
full + 7% 重叠145.8 s145.7 s147.0 s
tiny78.6 s75.9 s78.6 s78.2 s

编译确实生效了(日志里有 [compile] decoder compiled / [compile] dit compiled,不是静默失败),但收益在测量噪声以内(≤3%)。原因:

结论:这个模型上 torch.compile 不值得开。同样是想省时间, 改一个 tile_stride 参数拿到了 2.7×,而 compile 拿到 0%—— 这正是「先分块计时再决定优化目标」的价值。

FlashVSR 技术细节

整节只写 FlashVSR,不涉及任何其他模型。下文的「官方实现」一律指 FlashVSR/examples/WanVSR/infer_flashvsr_v1.1_*.py 和它自己的 pipeline, 也就是 FlashVSR 作者发布的代码;「我们的驱动」指 sr_eval/flashvsr_runner.py。分两块:怎么把一条形状奇怪的片子喂进去, 和怎么调的、每一步验了什么

一、奇葩视频怎么处理 —— FlashVSR 自己的七个硬约束

FlashVSR 对输入的形状要求很硬,而它自己的官方脚本处理不合法输入的方式是悄悄改掉你的片子, 不报错。下面每一条都会在评测里变成一个静默的错误结果,所以驱动 flashvsr_runner.py 全部改成显式处理。

约束FlashVSR 官方实现怎么做我们的驱动改成怎么做
画布必须是 128 的倍数 目标尺寸向下取整到 128 再中心裁。1920×1056 静默变成 1920×1024, 凭空丢 32 行真实内容。 改成把输入边缘复制 pad 到合法画布,模型在合法尺寸上跑, 出来再裁回原始取景。一行不丢,构图不动。
帧数必须是 8k+1 不满足就按它自己的方式截断。 尾部重复最后一帧补到 8k+1,产出后再截回原长。
只吐 num_frames−4 帧 静默少 4 帧,而且丢的是尾巴不是头。 先多喂 4 帧再截。这一条是验出来的不是猜的:把输出和真值做 ±8 帧偏移的 PSNR 扫描,峰值必须落在 0。我第一次补在头部,扫描峰值出现在 +4, 直接证明丢的是尾部。
最短时域窗口 33 帧 17 帧和 25 帧都会在它自己的时序窗口里 torch.cat([]) 空列表崩掉。 短片补到 ≥33;低于这个值直接抛明确错误,不让它崩在库里面。
窗口长度取整方向 (我自己引入的 bug)向下取整到 8k+1。 必须向上取整再补帧。向下取整会让最后一窗盖不到片尾, 141 帧只出 135 帧。靠打印逐帧覆盖表(wins / produced / missing)才抓出来。
scale 固定为 4 FlashVSR 是一个 ×4 模型,不是 ×2。官方 6 个推理脚本全部是 seed, scale, dtype, device = 0, 4.0, ...prepare_input_tensorcompute_scaled_and_target_dims 的默认参数都是 scale=4;连 LQ 投影层的类名都叫 Causal_LQ4x_Proj。论文里的场景也是 540p→4K。 它是 pre-upsample 架构:LQ 先被 bicubic 放大到目标尺寸, DiT 才在目标分辨率上工作。所以 scale 是前处理的参数,没有烧进网络权重, 传 2 进去就是真 ×2,而不是 ×4 之后再缩回来(那会多一次重采样、白烧 4 倍算力)。 本项目全部按 scale=2 跑。
超长 / 超宽 整条整幅塞进去,装不下就 OOM。 时域窗口(重叠 8 帧,流式融合)+ 空间分块 (只切长边、128 对齐、重叠 256 px、线性 ramp 互补权重融合)。

空间分块只切长边、按 128 对齐、重叠 256 输出像素,线性 ramp 融合。 (更正:重叠量其实小于模型 ±640 px 的局部注意力半跨度, 我早先算成 88 px 是错的;实测无代价的原因见 学习页。) 接缝做了专项检测——不是看全局指标(1 像素的缝会被均值淹没),而是量 分块版相对整幅版多出来的水平梯度,在两个拼接位置只有 0.9σ 和 1.1σ, 全帧最大偏差落在 x=690(画面自身的边缘)而不是拼接处。没有缝。

二、怎么调的 —— 四个旋钮,只有两个有用
旋钮作用实测
variant full→tiny把 Wan VAE 解码器换成 TCDecoder 速度的大杠杆,2.4×;也是全部感知分损失的来源(见下)
kv_ratio 3.0→1.5压 DiT 的 KV 缓存 唯一能降 DiT 峰值的(28.7→25.8 GiB),画质完全免费;低于 1.5 不再有效
vae_tileVAE 解码分块尺寸 只影响解码阶段峰值——但管线自带的默认值 60×104 正是 4 Mpx 解码 OOM 的元凶
tile_px(空间分块)超过多少像素就切块 对 tiny 是双赢:2 块比整幅又快又省 34%(注意力矩阵变小的收益超过跑两趟的开销)
sparse_ratio / local_range稀疏注意力形状 时间和显存都没有可测量的影响,不用调

21:9 只有靠空间分块才成立:3584×1536 整幅送进去会死在一个 2.15 GiB 的定长分配上, 这个分配跟任何旋钮都无关,只能靠把画面切小来绕开。

感知分是哪个旋钮丢的?——2×2 拆解

调优版同时动了两个旋钮,两两对比说不清是谁的责任,所以把四格全跑了。 UDM10 / YouHQ40 每帧都在 3 Mpx 分块阈值以下,四格都没有分块,不是混淆项。

三、我自己引入过的三个 bug(都已修)
  • float32 全片画布:把整条片子的融合缓冲开成 float32,13.7 GB 的张量被反复拷贝 15 次, 一个 GPU 任务变成 memcpy 瓶颈,跑 28 分钟还没完。改成预分配 uint8 后同一条 625 s 跑完, 输出逐位相同(对齐 PSNR 仍是 21.447)。
  • 多块分支尾部黑帧:缓冲按 m 帧分配,但管线只填 m−4 帧,剩下的尾巴保持零值, 写出来是黑帧。改成按实际产出帧数截断。
  • 窗口向下取整漏片尾:141 帧只写出 135 帧。靠打印 wins / produced / missing 逐帧覆盖表定位。

还有一条结论性错误:我曾经报告「FlashVSR 在 5090 上做不了 4 Mpx 输出」。 挡住它的其实是我自己按 kv 3.0 标定、后来不再适用的 px-budget 门限,不是真 OOM。 改掉门限后连原版配置都能跑 2688×1536×141。

四、24 GB(RTX 4090)能不能跑

显存这半边是实测的:用 set_per_process_memory_fraction 把分配器硬卡在 23.0 GiB(24 GB 减掉约 1 GB 的 CUDA context 等分配器外开销),torch 会在这个上限 精确抛 OOM,和小卡一样。先跑阳性对照——一个已知需 30.5 GiB 的配置在 cap 下确实 OOM, 确认 cap 真的在起作用,后面的行才有意义。

场景结果耗时峰值
对照:15s 16:9 不分块(需 30.5 GiB)OOM ✓ 符合预期
5s 16:9 2688×1536通过158.7 s20.13 GiB
5s 21:9 3584×1536通过235.8 s22.19 GiB
15s 16:9 2688×1536通过409.9 s20.13 GiB
15s 21:9 3584×1536通过558.6 s22.19 GiB
15s 21:9,改 3 块通过558.0 s16.44 GiB
15s 21:9,改 4 块通过602.1 s14.11 GiB

24 GB 卡上该用 3 块:和 2 块一样快(558.0 vs 558.6 s),峰值却低 5.75 GiB。 原因是 2 块贴着上限时分配器一直在刷缓存重试,给它留余量反而不慢。
kernel 侧:Block-Sparse-Attention 的 setup.py 默认 arch 里没有 89, 但源码有一段专门为 sm_86/sm_89 写的分支is_sm8x,hdim128 非因果走 128×32、 只要 48 KB shared memory),正是为了迁就 Ada 更小的 smem;README 也把 Ada 列进 bf16 支持。 用 BLOCK_SPARSE_ATTN_CUDA_ARCHS=80 编即可(sm_80 cubin 按 CUDA 次版本二进制兼容 规则能在 sm_89 上加载)。
没有实机验证过:这里只模拟了显存预算,没有模拟 sm_89 kernel,也没有模拟 Ada 更低的 带宽——真 4090 上每一行的耗时都会更长。

其余方法 · 基础排名

这张表是背景参照,用来给 FlashVSR 的位置定标;本轮的重点是上面那组对比。