FlashVSR · 画质优先 vs 解码优化

768p → 2× RTX 5090

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

这一页只有 FlashVSR。部署已定为画质优先 + 解码优化(4090 上线); TCDecoder 速度版作为速度对照收录(注意:给 TCDecoder 加"解码优化"是无效组合vae-stride 只作用于 Wan VAE 解码器,实测两种 stride 输出 md5 逐位相同—— 所以速度版就是 tiny + kv1.5 + 分块本身)。外加原版和真值作参照。

📖 想弄懂 FlashVSR 的原理和每一步为什么这么做,看 学习笔记

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

两个候选,差别只有一个参数

两个候选用的是同一个解码器(作者的 Wan VAE,不是 tiny 的 TCDecoder), 同样的 kv_ratio 1.5、同样的空间分块。唯一的区别是 VAE 解码的分块步长: 管线默认 stride=30(50% 重叠,每个像素被解码约 4 次), 优化版改成 stride=56(7% 重叠)。

配置解码器解码重叠2688×1536×141显存峰值
原版(参照)Wan VAE 50%385.2 s29.15 GiB
画质优先Wan VAE 50%287.3 s16.52 GiB
画质优先 + 解码优化Wan VAE 7%147.2 s16.52 GiB

原版不做空间分块,所以它的显存是 29.15 GiB。 两个候选都比原版快、比原版省 43% 显存,且用的是同一个解码器。
优化版比画质优先再快 1.95×,显存一点没涨,代价见下面 YouHQ40 n=40 的全量画质对比 ——保真度反而略好,CLIP-IQA 掉 0.005。

该选哪个?如果没有特别理由,选解码优化版。 三套数据集 70 条片子的结论一致:快约 1.6×、显存完全相同、保真度略好, 唯一稳定的代价是 CLIP-IQA 掉 0.005~0.009 (换 tiny 解码器那次是 −0.061,小一个数量级)。 这一页放两个版本的全部 70 条视频,就是让你自己肉眼确认这个判断。
数据集
片源
方法

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

A
B
载入中…
0.00 / 0.00 s

这条片子上的分数

两个候选的全量画质对比 · 三套数据集 70 条

逐条配对相减(同一批片子,片间难度差异会抵消),2000 次 bootstrap,种子固定 20260821。 差值 = 解码优化(7%) − 画质优先(50%)。CI 跨 0 = 测不出差异。

MiniMax-20 · 真实 768p→2× 产出(无真值,n=20)

指标解码优化画质优先差值95% CI
MUSIQ ↑64.403664.3972+0.0064[−0.0132, +0.0257] 跨 0
CLIP-IQA ↑0.56840.5774−0.0090[−0.0116, −0.0064]
MANIQA ↑0.35490.3539+0.0009[+0.0005, +0.0014]
NIQE ↓4.05164.0156+0.0360[+0.0189, +0.0521]
Downsample-LPIPS ↓0.14540.1455−0.0001[−0.0001, −0.0000]

Downsample-LPIPS 是无真值集合上唯一的保真度代理(把输出缩回原尺寸和输入比, 衡量改写了多少原内容)——解码优化版更好,方向和有真值集合上的 PSNR 一致。

YouHQ40 ×2(有真值,n=40)

指标解码优化画质优先差值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] 跨 0
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]
MANIQA ↑0.39800.3986−0.0006[−0.0009, −0.0003]
NIQE ↓3.11853.1157+0.0028[−0.0123, +0.0245] 跨 0

UDM10 ×2(有真值,n=10)

指标解码优化画质优先差值95% CI
PSNR-Y ↑26.646526.6397+0.0068[+0.0043, +0.0092]
SSIM-Y ↑0.78030.7797+0.0005[+0.0004, +0.0007]
LPIPS ↓0.19120.1912+0.0001[−0.0001, +0.0002] 跨 0
DISTS ↓0.10270.1031−0.0005[−0.0006, −0.0003]
MUSIQ ↑69.597469.5626+0.0347[+0.0223, +0.0475]
CLIP-IQA ↑0.55770.5638−0.0061[−0.0094, −0.0026]
MANIQA ↑0.40110.4005+0.0006[+0.0002, +0.0011]
NIQE ↓3.82943.7950+0.0344[+0.0008, +0.0747]

成本(同机实测)

数据集解码优化画质优先快多少显存峰值
MiniMax-20 4 Mpx 244.8 s/条391.5 s/条1.60×16.52 GiB 相同
YouHQ40 2.0 Mpx 21.8 s/条35.0 s/条1.61×15.50 GiB 相同
UDM10 0.9 Mpx 10.2 s/条16.1 s/条1.58×10.02 GiB 相同
三套数据集、70 条片子的一致结论:解码优化版快约 1.6×,显存完全相同, 保真度略好,无参考观感分掉一点点。
保真度那边(PSNR-Y / SSIM-Y / DISTS / Downsample-LPIPS)三套集合方向一致,全是解码优化版赢—— 重叠融合本身就是一次轻微模糊,去掉反而更锐、更贴近真值。
无参考观感分那边只有 CLIP-IQA 稳定掉一点(−0.005~−0.009), MUSIQ 和 MANIQA 在三套集合上方向不一致(UDM10 上还是解码优化版赢), 说明这个量级已经贴近这些指标本身的噪声。
对比一下换 tiny 解码器那次是 CLIP-IQA −0.061,这里小一个数量级。
诚实标注一处口径不齐:上面「成本」表里画质优先在 YouHQ40 是 n=80 条记录而不是 40—— 因为这个配置在 YouHQ40 上先后跑过两轮(中途为腾磁盘删过一次产出又补跑), 两轮的计时记录都留在了 jsonl 里。均值是 80 条的均值。 两轮配置逐字相同、峰值一致,所以不影响结论,但这个 n 不等于片子数。

部署版 vs TCDecoder 速度版 · 三套数据集

差值 = TCDecoder 速度版 − 部署版(画质优先+解码优化),逐条配对 + 2000 次 bootstrap。 这就是「换解码器」的完整代价/收益表——kv、分块、解码步长两边一致,唯一变量是解码器。

指标UDM10 n=10YouHQ40 n=40MiniMax n=20谁赢
PSNR-Y ↑+0.518+0.446无真值速度版
SSIM-Y ↑+0.0102+0.0099速度版
LPIPS ↓−0.0106−0.0059速度版
DISTS ↓−0.0056−0.0035速度版
Downsample-LPIPS ↓−0.0060速度版
MUSIQ ↑−2.61−2.24−1.65部署版
CLIP-IQA ↑−0.054−0.056−0.048部署版
MANIQA ↑−0.032−0.026−0.012部署版
NIQE ↓+0.172+0.138+0.160部署版

所有 CI 均不跨 0。速度:MiniMax-20 上速度版 167.1 s/条 vs 部署版 244.8 s/条(快 1.47×), 5s 16:9 纯推理 74.8 s vs 147.2 s(快 1.97×)。

结论和之前的 2×2 拆解一致,现在是三套集合全量确认:这是一个真实的取舍,不是谁碾压谁。
TCDecoder 编造得少:保真度全线更好(PSNR-Y +0.45~+0.52),无真值集合上的 Downsample-LPIPS 也更好——它更忠实于输入。
Wan VAE 解码器更讨好眼睛和 IQA 模型:MUSIQ / CLIP-IQA / MANIQA / NIQE 全线更好。
部署选了后者(交付是给人看的),速度版收录在此供随时肉眼复核这个决定—— 选一条片子把两个版本 A/B 擦一擦,纹理差异在放大镜下很明显。

NVENC 输出 —— 生产不再写 PNG

生产路径已从「逐帧写无损 PNG」改成「帧经小队列直接进 ffmpeg/NVENC 出 mp4」。 同机同卡三路对照(每行独立进程独占一卡):

场景纯推理PNGNVENCs/Mpx帧
5s 16:9124.2 s233.1 s125.0 s0.213 / 0.400 / 0.215
5s 21:9163.7 s304.0 s167.7 s0.211 / 0.392 / 0.216
5s 9:16121.6 s226.0 s123.6 s0.209 / 0.388 / 0.212
15s 16:9282.3 s561.7 s279.3 s0.190 / 0.378 / 0.188
15s 21:9376.1 s740.1 s383.0 s0.190 / 0.373 / 0.193

均值:纯推理 0.203 / PNG 0.386 / NVENC 0.205 s·Mpx⁻¹·帧⁻¹。 NVENC 端到端落在纯推理地板上——输出成本基本为零。编码与推理确实重叠: 帧全部入队后等编码器收尾只要 0.09–0.43 s。PNG 模式并行跑时能看到 GPU 利用率 掉到 0%(同步写盘在推理循环里),NVENC 模式没有这个空转。

【已修正】第一版 NVENC 参数是错的,画质损失比应有的大得多。 用户在 YouHQ40 024 上肉眼看出问题后追查:-rc vbr -cq N -b:v 0 这个组合下 -cq 根本没生效——cq 19 和 cq 15 产出逐字节相同的 16 Mbps 文件。 换成 -rc constqp -qp 12 直接设量化参数才真正生效。
我最初的排查方向也是错的:我先在另一条内容简单的片子上测, 那条 16 Mbps 就够用(40.9 dB),于是我误判「CQ 无效说明瓶颈是色度抽样」。 实际是那条片子掩盖了码率不足。一条片子的结论不该外推

YouHQ40 024 上的编码器参数扫描(对无损 PNG 打分)

设置码率PSNR 均值说明
420 vbr cq19 / cq1516 Mbps26.72 dB旧配置,两档输出逐字节相同
420 constqp 1973 Mbps35.28 dB改对模式就跳一大截
420 constqp 12(现配置)139 Mbps39.08 dB通用解码器都吃得下
444 constqp 12143 Mbps40.66 dB多 1.6 dB,但需 Rext profile
444 lossless554 Mbps52.16 dB体积 4×,生产不必要

420 + constqp 12:比 4:4:4 只差 1.6 dB,但不需要 Rext profile (很多播放器和浏览器不支持 Rext)。024 从 26.72 → 39.08 dB。

片源选择器里的 「速度版 @4090 · NVENC 输出」 就是修好后的生产产物, 和 「速度版 @4090」(PNG 路径)擦一擦。
口径:两条网页视频都经过站上统一的 H.264 重编码,所以 NVENC 那条是 两次有损、PNG 那条只有一次——你看到的差异不小于真实生产差异。 修复后两条网页视频互比 32.9 dB(修复前 26.3 dB)。 要位精确 QA 用 FLASHVSR_SINK=png 切回无损。

4090 vs 5090 —— 真机实测,不是模拟

之前站上的 24 GB 结论是用分配器上限模拟出来的。现在在一台 8×RTX 4090 24 GB 真机上从零装了一遍环境重跑,可以直接对照。 每行都是独立进程、--no-write、一行一卡。

范围声明:4090 上只测了速度和显存。那 8 行全部带 --no-write, 一张 PNG 都没落盘,也没跑任何 benchmark 数据集。 本页所有画质数字(PSNR / LPIPS / MUSIQ / 接缝检测)全部来自 5090

跨机像素比对 —— 一个我原本以为不用验的假设,结果它不成立

我一直默认「同样的权重、同样的代码,两台机器出的是同一张画面」。 专门去验了:在 4090 上真的写 PNG 跑一条 YouHQ40 clip(1056×1056 × 31 帧), 和 5090 上同一条逐像素比。结论是:这个假设错了,但错得不要紧——理由在下面。

比较PSNRmd5 一致差异像素
4090 vs 509031.04 dB 0/3183.0%,最大差 192/255
4090 同卡跑两次∞(逐位相同)31/310%
4090 换一张卡跑∞(逐位相同)31/310%

机内逐位一致、跨机 83% 像素不同——所以这不是随机性, 是一个稳定可复现的机器差异。(这组数字我用自己写的脚本独立复算过一遍:31.044 dB, 与 subagent 的 31.028 dB 一致。)

差在哪?不是接缝,不是错位,是细纹理

把两边同时降采样后再比PSNR
原始分辨率31.04 dB
1/233.32 dB
1/437.29 dB
1/842.05 dB
1/1644.84 dB

降采样后迅速收敛,说明低频结构两边是一致的,分歧只在高频细节。 另外三项排除:时间偏移扫描在 0 处尖峰(没有错帧)、空间平移扫描在 (0,0) 处尖峰(没有错位)、 这个尺寸下驱动只用 1×1 个窗口根本没有接缝可言),差异是全画面弥散的。 锐度也没塌(Laplacian 方差 282.9 vs 293.9)。

那到底哪台"对"?—— 都对

各自与同一份真值比PSNR-YSSIM-Y
409027.77550.80461
509027.69270.80549
差值+0.0828 dB
95% CI [+0.064, +0.103],29/31 帧一致
−0.0009

两台机器是两个同样合法的纹理实现,没有谁坏掉。 换成 tiny(完全不同的解码器)重跑,分歧的量级和性质几乎不变 (32.23 dB,1/8 降采样 42.1 dB)——说明源头在共享的 DiT / 稀疏注意力路径, 不在解码器。最可能的机制:BSA 在 4090 上编成 sm_80 cubin 走 is_sm8x 的 48 KB smem 分块,5090 走 sm_120 分块, 不同的分块几何 = 不同的 bf16 累加顺序,差异从注意力开始播种再经解码器放大。 torch 2.6/2.8 与 cuDNN 9.1/9.10 的差异叠在上面。 这三者谁占主导没有隔离出来——那需要在同一台机器上换 kernel 重编,没做。

这条对怎么做实验有直接约束:
  1. 永远不要跨机比 PNG 或 md5。它们永远不会相同,这是预期行为不是 bug。
  2. 指标级结论可以跨机迁移,跨机偏差只有 +0.08 dB PSNR-Y。
  3. 但任何 A/B 必须在同一台机器上做。+0.08 dB 这个偏差, 和我们判定为"真实效应"的量级是同一个数量级—— 比如 kv 1.5 vs 3.0 的 PSNR 效应就是 +0.09 dB。 换台机器就能完全淹没这么小的效应。 本站所有 A/B(原版 vs 候选、两候选之间、分块与否、解码重叠)全部在 5090 单机完成, 不受影响;但 ±0.2 dB 以内的结论,换机器就不可信了。
配置40905090慢多少显存 4090 / 5090
tiny,141 帧 @2688×153699.0 s74.8 s 1.32×20.13 / 20.13 GiB
画质优先(50% 重叠),141 帧413.7 s287.3 s 1.44×16.52 / 16.52 GiB
解码优化(7% 重叠),141 帧203.7 s147.2 s 1.38×16.52 / 16.52 GiB

三行的显存峰值和 5090 逐位一致——同一个分配器、同一个模型,本来就该如此, 这也是「移植没走样」最强的证据。速度慢 1.3–1.4×,方向合理。

15 秒长片在 4090 上的容量(360 帧)

配置 / 场景耗时显存峰值余量
tiny,16:9 2688×1536261.1 s20.13 GiB3.9 GiB
tiny,21:9 3584×1536386.6 s23.04 GiB 不到 1 GiB
tiny,21:9 + 更细分块367.6 s16.44 GiB7.6 GiB
解码优化,16:9 2688×1536541.4 s16.52 GiB7.5 GiB
解码优化,21:9 3584×1536669.2 s19.52 GiB4.5 GiB
8 行全过,零 OOM。之前用 23 GiB 上限模拟出的结论,真机验证成立。
但有一条模拟没告诉我们的:tiny 跑 21:9 15s 只剩不到 1 GiB 余量(23.04/23.99), 这在真机上是危险区。而两个画质候选都留有 4.5–7.5 GiB 余量,反而更安全—— 因为砍掉解码重叠之后,Wan VAE 解码器的显存占用比 TCDecoder 还低。

✅ 24 GB 部署预设 —— 已在真 4090 上验证,部署目录就位

默认阈值(tile-px 3.0e6 / px-budget 140e6)是按 32 GB 标定的, 4090 上 21:9 峰值 19.52 GiB、余量仅 4.5 GiB。部署预设改为 --tile-px 2.5e6 --px-budget 110e6(其余与部署配置相同), 在真 4090 上五行验证全过:

场景耗时峰值分块
5s 16:9 2688×1536237.4 s16.30 GiB2×5
15s 16:9 2688×1536536.6 s16.30 GiB2×11
5s 21:9 3584×1536306.0 s15.33 GiB3×4
15s 21:9 3584×1536729.0 s15.33 GiB3×9
5s 9:16 竖版 1536×2688239.9 s16.30 GiB2×5
对照:15s 21:9 旧默认(同卡同日复测) 688.9 s19.52 GiB2×11

全画幅族最大峰值 16.30 GiB,余量 ≥7.69 GiB(此前是 4.5)。 账目干净:21:9 峰值 −4.19 GiB,代价 +5.8% 耗时;16:9 分块不变,基本白送。 有个反直觉的点如实写明:峰值上界由 16:9 决定而不是 21:9—— 21:9 被切成更小的 1408×1536 块(2.16 Mpx),16:9 的 1536×1536 块(2.36 Mpx) 低于 2.5 Mpx 阈值不再细分,所以它才是最大的块。 竖版 1536×2688 沿长轴对称分块,峰值与横版逐位相同。
部署目录:4090:~/vsr_deploy/fv_deploy_4090/run_batch.sh, flags 写死、头部注释含标定依据与验证数据,已用真实 job 冒烟(写 PNG, 首帧 md5 与此前跨机校验产物逐位一致——4090 机内确定性再次确认)。

✅ TCDecoder 速度版的 24 GB 预设 —— 也验证过了,但有一个要知道的取舍

tiny 默认阈值在 4090 上 21:9 峰值 23.04 GiB,余量不到 1 GiB,生产不可用。 预设改为 --variant tiny --tile-px 1.8e6(vae-stride 对 TCDecoder 是死参数,不加), 真 4090 六行验证全过:

场景耗时峰值分块
5s / 15s 16:9 2688×1536121.9 / 282.8 s16.44 GiB3×3 / 3×6
5s / 15s 21:9 3584×1536164.4 / 374.8 s16.44 GiB4×3 / 4×6
5s / 15s 9:16 1536×2688122.6 / 285.4 s16.44 GiB3×3 / 3×6
对照:15s 16:9 默认阈值(同机复测) 263.9 s20.13 GiB2×8

六种画幅×时长峰值完全一致 16.44 GiB(同一个块内存形态封顶),余量 7.55 GiB。 速度上方向分裂:21:9 细分块更快(374.8 vs 386.6 s),16:9 慢 7.2% (282.8 vs 263.9 s)——"细分块对 tiny 双赢"只在 21:9 成立,16:9 是拿 7.2% 速度换 3.7 GiB 余量。 部署目录:4090:~/vsr_deploy/fv_deploy_4090_tiny/run_batch.sh

细分块对 tiny 的画质代价(5090 同机 n=40 配对实测,别只看容量): 保真度反而略好(PSNR-Y +0.055、SSIM-Y +0.0027、DISTS −0.0011,CI 均不跨 0), 但无参考观感分小幅下降(MUSIQ −0.36、CLIP-IQA −0.007、MANIQA −0.005), 约为换解码器代价的 1/6
接缝聚类检测有一个真信号:37 条被切开的片子里,最坏梯度列落在融合带内的有 16 条(随机预期 5 条,3.2 倍富集),其中 9 条超 4σ、最高 5.3σ—— 细分块留下了统计可检、系统性的微弱接缝痕迹,这和大块 2 分块时的干净通过 (0.9σ/1.1σ)不同。块切窄后两侧纹理实现分歧变大,crossfade 带变得可检出。 量级仍然很小(指标只动了 0.005 量级),但如果部署后有人报告竖条纹,先查这里。

kernel:Block-Sparse-Attention 能不能上 Ada

能,而且一行源码都没改。用 BLOCK_SPARSE_ATTN_CUDA_ARCHS=80 编译, cuobjdump --list-elf 确认产物只含 sm_80 cubin(没有 sm_89、也没有 PTX), 在 4090 上照样加载执行——靠的是 CUDA 次版本二进制兼容。
而且是数值验证过的,不是「不报错就算过」:对稠密掩码与 scaled_dot_product_attention 比对,bf16 下最大绝对误差 1.16e-03; 换成真正稀疏的下三角块掩码会给出不同且非退化的结果(证明稀疏路径真的在走)。 编译 ~7 分钟(ninja + MAX_JOBS=32,25 个目标文件),无需任何补丁。

这组数字的三点保留

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

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

分块是谁做的为什么要
空间分块
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 上每一行的耗时都会更长。