从「它凭什么能一步出图」讲到「我们改了什么、为什么改完更快还不掉画质」。 每个数字都标了来源,凡是没实测的地方会写明「未验证」。
面向自学 全部结论可复现 测试机 8×RTX 5090 32G
先说清楚超分(Super-Resolution)要解决什么。把一张 960×540 的图放大到 1920×1080, 你需要凭空造出 4 倍的像素。传统方法(双三次插值)只会在已知像素之间做平滑过渡, 所以放大后是糊的——它没有新增任何信息,只是把旧信息摊开。
要造出看起来合理的细节,就需要一个「见过很多真实图像」的模型来猜: 这块模糊的区域,按照真实世界的统计规律,应该长什么样。扩散模型特别擅长这件事。
标准扩散模型的工作方式是从纯噪声开始,反复去噪几十次,每次去掉一点, 最后收敛成一张图。这对单张图还好,对视频是灾难:
| 成本来源 | 为什么贵 |
|---|---|
| 多步采样 | 50 步意味着同一个大网络要跑 50 遍。一条 5 秒 24fps 的视频有 120 帧, 就是 6000 次网络前向。 |
| 注意力的平方复杂度 | Transformer 的自注意力里,每个位置都要和所有其他位置算相关性。 序列长度翻倍,计算量变 4 倍。视频的序列长度 = 帧数 × 高 × 宽,一下就爆了。 |
| 训练/测试分辨率不一致 | 模型在 512×512 上训练,你拿 2688×1536 去推理, 注意力要处理的序列长度差了 15 倍,模型没见过这种尺度,容易崩。 |
| VAE 解码 | 扩散在压缩过的「潜空间」里进行,最后要用解码器还原成像素。 解码器本身也是个大网络。 |
tiny 变体解决的):
一步蒸馏干掉多步、局部约束稀疏注意力干掉平方复杂度和尺度不匹配、
流式处理让长视频不必一次性装进显存、微型条件解码器干掉解码开销。
论文报告在单张 A100 上 768×1408 能跑到 ~17 FPS。
蒸馏的意思是:先训练一个慢但好的「老师」模型(多步扩散), 然后训练一个「学生」模型去一步直接跳到老师走 50 步才到的终点。 DMD(Distribution Matching Distillation)是其中一种做法, 它匹配的不是单条轨迹而是输出分布,所以学生不会只学会复刻老师的某几个样本。
代码里能直接看到这一点——权重文件叫
diffusion_pytorch_model_streaming_dmd.safetensors,而 pipeline 里:
self.scheduler.set_timesteps(1, denoising_strength=1.0, shift=5.0) self.timestep = torch.tensor([1000.], ...) # 固定在最大噪声档位 self.t = self.dit.time_embedding(sinusoidal_embedding_1d(...)) self.t_mod = self.dit.time_projection(self.t).unflatten(1, (6, self.dit.dim))
注意 self.t 和 self.t_mod 是在循环外算一次然后一直复用的。
因为只有一步,时间步永远是 1000,时间嵌入是个常量。多步扩散里这是每步都要重算的。
去噪本身就一行:
noise_pred = model_fn_wan_video(self.dit, x=cur_latents, timestep=self.timestep, ...) cur_latents = cur_latents - noise_pred # 一步到位,没有循环
num_inference_steps 这类参数完全没用,
调它不影响任何东西。因为这个模型结构上就只有一步,
「步数」这个旋钮在蒸馏之后已经不存在了。
这是 FlashVSR 最核心也最巧妙的部分。作者在 README 里专门强调: 有些第三方实现(早期 ComfyUI 版本)没有实现 LCSA、退化成了稠密注意力, 会导致明显的画质下降,尤其在高分辨率下。所以这不是个可选的加速开关, 它是模型正确性的一部分。
win = (2, 8, 8) # 2 帧 × 8 × 8 的潜空间块 q_w = WindowPartition3D.partition(q, win) k_w = WindowPartition3D.partition(k, win) v_w = WindowPartition3D.partition(v, win)
注意力不再以「单个 token」为单位,而是以块为单位。这一步就把序列长度降了 128 倍 (2×8×8)。
self.local_attn_mask = build_local_block_mask_shifted_vec_normal_slide(
h//8, w//8, local_range, local_range, include_self=True, ...)
这个函数生成的是一个「每个块只允许看它周围 local_range 范围内的块」的布尔矩阵。
默认 local_range=11,即以自己为中心的 11×11 块邻域。
为什么局部就够?因为超分是个局部任务。要恢复某块砖的纹理, 你需要的是这块砖周围的上下文,不是画面另一头那片天空。 这同时也解决了训练/测试分辨率不一致的问题——不管你输入 512 还是 2688, 每个块看到的邻域大小是固定的,模型面对的局部统计量始终和训练时一致。 这就是论文说的「bridging the train–test resolution gap」。
avgpool_q = torch.mean(q_w, dim=1) # 每个块压成一个代表向量
avgpool_k = torch.mean(k_w, dim=1)
scores = einsum("hld,hmd->hlm", q_heads, k_heads) / sqrt(D) # 块级别的粗略打分
scores = scores + local_attn_mask # 局部之外直接 -inf
attn_map = softmax(scores, dim=-1)
thresholds = topk(flat, k=topk+1, dim=1).values[:, -1] # 只留最强的 topk 个块对
这叫 draft mask(草稿掩码):先用极廉价的「块平均向量」算一遍粗略注意力,
看看哪些块对之间真的有关系,只把这些块对交给真正的注意力算子去精算。
最后调用 block_sparse_attn_func——这是一个专门的 CUDA 算子,
能跳过掩码为 0 的块,不算就是不算,不是算完再乘 0。
topk 由参数 sparse_ratio 控制:
topk_ratio = sparse_ratio * 768 * 1280 / (th * tw)
注意这个式子——它把 topk 按分辨率反比缩放,基准是 768×1280。 分辨率越高,保留的比例越小,保证绝对计算量大致恒定。这也是它能「scale 到超高分辨率」的原因。
视频的时间维度也是序列的一部分。如果一次处理 360 帧,注意力的序列长度是 360×H×W,显存直接爆炸。FlashVSR 的做法是滑动窗口 + KV 缓存:
process_total_num = (num_frames - 1) // 8 - 2 # 每轮推进 8 帧
for cur_process_idx in range(process_total_num):
if cur_process_idx == 0:
pre_cache_k = [None] * len(self.dit.blocks) # 第一轮:冷启动,喂 7 段 LQ
inner_loop_num = 7
cur_latents = latents[:, :, :6, :, :]
else:
inner_loop_num = 2 # 之后每轮只喂 2 段新 LQ
cur_latents = latents[:, :, 4+idx*2 : 6+idx*2, :, :]
noise_pred, pre_cache_k, pre_cache_v = model_fn_wan_video(
..., pre_cache_k=pre_cache_k, pre_cache_v=pre_cache_v, kv_ratio=kv_ratio)
每一轮只处理一小段新帧,但把之前算好的 K/V 带着往前走, 所以新帧仍然「看得见」历史。这就是「streaming」的含义—— 理论上可以处理无限长的视频,显存占用恒定。
kv_ratio 控制这个缓存留多长:
kv_len = min(max(int(window_size * kv_ratio), 1), int(window_size*seqlen)-1)
if cache_num > kv_len:
cache_k = k_w[one_len:, :, :] # 丢掉最老的一段
cache_v = v_w[one_len:, :, :]
kv_ratio 是唯一能压 DiT 显存峰值的旋钮
(3.0 → 1.5 让峰值从 28.7 降到 25.8 GiB),因为它直接决定缓存张量有多大。
而它降到 1.5 以下就不再有效——那时缓存已经不是显存大头了。
标准流程里,DiT 在潜空间输出结果,再由 Wan2.1 VAE 解码器还原成像素。 这个解码器是完整的 VAE,485 MB,很重。
FlashVSR 提供了一个替代品 TCDecoder(181 MB),关键在于它是有条件的:
pipe.TCDecoder = build_tcdecoder(new_channels=[512, 256, 128, 128],
new_latent_channels=16 + 768)
...
def decode_video(self, x, ..., cond=None):
if cond is not None:
x = torch.cat([self.pixel_shuffle(cond), x], dim=2)
看那个 16 + 768:16 是潜空间通道数,768 = 3 × 16 × 16,
正是把 LQ 原图做 16×16 pixel-shuffle 之后的通道数。也就是说,
解码器除了潜变量,还直接看到了低清原图。
这就是它能做小的原因:它不需要从潜变量里重建一切, 低频结构、颜色、大致轮廓都可以直接从 LQ 抄,它只需要补高频细节。 调用时确实把 LQ 传进去了:
frames = self.TCDecoder.decode_video(latents.transpose(1,2), parallel=False,
cond=LQ_video[:,:,:LQ_cur_idx,:,:])
seed, scale, dtype, device = 0, 4.0, ...;
prepare_input_tensor 和 compute_scaled_and_target_dims
的默认参数都是 scale=4;连 LQ 投影层的类名都叫
Causal_LQ4x_Proj;论文的场景是 540p→4K。这是设计规格,不是 bug。
那为什么我们能让它做 ×2?因为它是 pre-upsample(先放大后处理)架构。
看 prepare_input_tensor 的逻辑:LQ 图先被普通插值放大到目标尺寸,
然后 DiT 在目标分辨率上工作。DiT 的任务不是「放大」,
而是「把一张已经放大但很糊的图修清楚」。
所以 scale 是前处理的参数,没有烧进网络权重。传 2 进去, LQ 就被放大 2 倍,DiT 在 2 倍尺寸上修复,出来就是真 ×2。
反过来说,如果不这么做,想要 ×2 就只能×4 之后再缩小回去—— 那会白烧 4 倍的算力(DiT 在 4 倍面积上工作),还多一次重采样损失。
把上面的部件串起来,一条 141 帧 1344×768 → 2688×1536 的片子是这样走的:
标了 [我们加的] 的部分是官方实现里没有的。 官方脚本假设你喂给它的片子形状本来就合法、长度本来就装得下、 分辨率本来就不超过显存——现实中这三条经常不成立。
FlashVSR 对输入的形状要求很硬。麻烦的地方在于: 官方脚本处理不合法输入的方式,是悄悄改掉你的片子,而不是报错。 在评测场景下,每一条都会变成一个静默的错误结果。
| 约束 | 官方实现 | 我们的驱动 |
|---|---|---|
| 画布必须是 128 的倍数 | 目标尺寸向下取整到 128,再中心裁。1920×1056 静默变成 1920×1024, 凭空丢掉 32 行真实内容。 | 改成把输入边缘复制 pad 到合法画布,模型在合法尺寸上跑, 出来再裁回原始取景。一行不丢,构图不动。 |
| 帧数必须是 8k+1 | 不满足就按它自己的方式截断。 | 在尾部重复最后一帧补齐,产出后再截回原长。 |
| 只吐 num_frames−4 帧 | 静默少 4 帧,而且丢的是尾巴不是头。 | 先多喂 4 帧再截。这条是验出来的不是猜的: 对输出和真值做 ±8 帧偏移的 PSNR 扫描,峰值必须落在 0。 我第一次补在头部,扫描峰值出现在 +4,直接证明丢的是尾部。 |
| 最短时域窗口 33 帧 | 17 帧和 25 帧都会在它自己的流式循环里
torch.cat([]) 空列表崩掉。 |
短片补到 ≥33;低于这个值直接抛明确错误,不让它崩在库内部。 |
| 窗口长度取整方向 | (这条是我自己引入的 bug,不是官方的) | 必须向上取整到 8k+1 再补帧。向下取整会让最后一窗盖不到片尾, 141 帧只出 135 帧。 |
| scale 固定为 4 | 设计规格就是 ×4(见 2.5 节)。 | 利用 pre-upsample 架构,前处理传 scale=2,得到真 ×2。 |
| 超长 / 超宽 | 整条整幅塞进去,装不下就 OOM。 | 时域窗口(重叠 8 帧,流式融合)+ 空间分块 (只切长边、128 对齐、重叠 256 px、线性 ramp 互补权重融合)。 |
FVPROF=1 给每个块加了同步计时器
(每个计时器都 cuda.synchronize(),量的是真实 GPU 时间,不是异步下发延迟)。
2688×1536 × 141 帧,不含 PNG 落盘:
| 配置 | 总耗时 | 解码器 | DiT + 前处理 |
|---|---|---|---|
| tiny(TCDecoder) | 74.8 s | 12.3 s 16% | 62.4 s 83% |
| full(Wan VAE 解码器) | 283.4 s | 220.1 s 78% | 63.3 s 22% |
DiT 两边都是 ~63 s。两个配置的全部差距都在解码器上。 所以画质优先配置该优化的地方只有一个:那 220 秒。
FlashVSR 的 pipeline 解码时用
tile_size=(60,104), tile_stride=(30,52)——50% 重叠。
重叠区域的像素会被解码两次(横竖都重叠就是约 4 次)然后加权融合。
为什么要重叠?因为分块解码时,每块的边缘缺少块外的上下文, 直接拼会有接缝。重叠 + 融合可以把接缝抹掉。
但需要 50% 这么多吗?实测:
| tile / stride | 重叠 | 总耗时 | 解码器 | 显存峰值 |
|---|---|---|---|---|
| 60 / 30 (管线默认) | 50% | 287.3 s | 221.7 s | 16.52 GiB |
| 60 / 44 | 27% | 177.1 s | 113.3 s | 17.03 GiB |
| 60 / 56 | 7% | 147.2 s | 81.3 s | 16.52 GiB |
| 96 / 48 | 50% | 228.1 s | 161.9 s | 19.87 GiB |
| 96 / 88 | 8% | 154.3 s | 86.0 s | 19.87 GiB |
| 128 / 120 | 6% | 139.8 s | — | 24.50 GiB |
| 160 / 152 | 5% | 179.3 s | — | 30.41 GiB |
| 256 / 248 | 3% | OOM | — | >32 GiB |
解码 221.7 s → 81.3 s,快 2.7×,显存一点没涨。 注意分块开大(96/128/160)反而更慢且更吃显存—— 省时间的是减少重叠,不是加大分块。
① 全量画质对比(YouHQ40 n=40,配对 + bootstrap CI):
| 指标 | 7% 重叠 | 50% 重叠 | 差值 | 95% CI |
|---|---|---|---|---|
| PSNR-Y ↑ | 24.6474 | 24.6395 | +0.0079 | [+0.0065, +0.0094] |
| SSIM-Y ↑ | 0.6708 | 0.6703 | +0.0005 | [+0.0004, +0.0006] |
| LPIPS ↓ | 0.2233 | 0.2234 | −0.0000 | [−0.0001, +0.0001] |
| DISTS ↓ | 0.1050 | 0.1053 | −0.0003 | [−0.0004, −0.0002] |
| MUSIQ ↑ | 71.4126 | 71.4389 | −0.0263 | [−0.0397, −0.0141] |
| CLIP-IQA ↑ | 0.6133 | 0.6182 | −0.0049 | [−0.0056, −0.0042] |
保真度略微变好——这不奇怪,重叠融合本身就是一次轻微的模糊,去掉它反而更锐。 CLIP-IQA 掉 0.005,对比换解码器那次掉 0.061,小一个数量级。
② 接缝专项检测。全局指标有个致命弱点:一条 1 像素宽的缝会被平均值淹没。 所以要专门找这个失效模式。做法是量「低重叠版相对高重叠版多出来的水平梯度」, 只看接缝那几列:
extra = gradient(低重叠版) − gradient(高重叠版) 在接缝位置的 extra 偏离均值多少个 σ?
为什么要相减?因为画面本身就有强边缘(比如一根电线杆), 那里两个版本的梯度都很大,直接看绝对梯度会误报。 只有分块版独有、整幅版没有的梯度才是接缝。
结果:4 条片子 × 横竖两个方向 × 2 个接缝位置, 额外梯度最大 3.7σ,全部低于 4σ 判定线; 而且最大偏差点落在画面内容边缘(如 x=866)而不是接缝上。无可见接缝。
tile-px):我们加的,FlashVSR 原本没有。
整幅 3584×1536 送进 DiT 会死在一个 2.15 GiB 的定长分配上,
这个分配跟任何旋钮都无关。不切块,21:9 根本跑不了。
tile_size/stride):FlashVSR 自带的,默认开启,
就是上一节讨论的那个。
空间分块有没有代价?跑了一组 --tile-px 1e12(永不分块)的对照,
其余配置逐字相同。分块只在每帧 > 3.0 Mpx 时触发,所以 MiniMax-20 天然分成两组:
① 阴性对照(4 条 1:1,2.36 Mpx,两边都没触发分块): 逐字节 md5 比对 4/4 完全相同。 这一步是必须的——如果这里就不一样,说明管线本身不确定,下面所有数字都是噪声。
② 16 条 >3 Mpx 的真实 A/B:
| 指标 | 分块 | 不分块 | 差值 | 95% CI |
|---|---|---|---|---|
| MUSIQ ↑ | 60.9680 | 60.9355 | +0.0325 | [−0.2494, +0.2895] 跨 0 |
| CLIP-IQA ↑ | 0.5110 | 0.5110 | +0.0000 | [−0.0045, +0.0041] 跨 0 |
| MANIQA ↑ | 0.3426 | 0.3431 | −0.0005 | [−0.0019, +0.0010] 跨 0 |
| NIQE ↓ | 4.3191 | 4.3649 | −0.0458 | [−0.0758, −0.0086] |
| Downsample-LPIPS ↓ | 0.1471 | 0.1483 | −0.0012 | [−0.0028, +0.0004] 跨 0 |
准确的说法:五个指标里四个的 CI 跨 0(测不出差异), 唯一测得出的 NIQE 还是分块更好一点。没有任何指标显示分块更差。 但也不能说「零改变」——输出确实不逐位相同,只是差异小到测不出方向。
local_range=11 ≈ 88 px 的感受野,所以每个像素的上下文是完整的」。
这个数字算错了,差了大约 7 倍。正确的推算是:
输出像素 →(VAE ÷8)→ latent →(DiT patch_size=[1,2,2] ÷2)→ 注意力 token
1 个 token = 16 个输出像素
1 个注意力块 = 8×8 token = 128×128 个输出像素
local_range=11 块,以自己为中心 → ±5 块 = ±640 个输出像素
所以局部注意力的半跨度是 ±640 px,而我们的重叠只有 256 px(离接缝 ±128 px)。
重叠量其实远小于感受野,接缝附近的像素确实看到了被截断的上下文。
那为什么实测没有代价?我认为有三个原因,但要说清楚—— 下面是解释,不是证据;证据是上面那张 A/B 表和接缝检测:
这也说明一件事:我最初那条「有足够余量」的推理即使数字算对了也不该作为结论—— 它是个理论论证,而理论论证代替不了 16 条片子的配对测量和接缝检测。 数字错了但结论没错,纯属运气好,不是方法对。
而且对 tiny 来说分块是双赢:2 块比整幅又快又省 34% 显存
(155.5 s / 20.13 GiB vs 163.4 s / 30.49 GiB)。
为什么切开反而更快?因为注意力的计算量随边长平方增长,
切成两半之后单块计算量是原来的 1/4,跑两趟也才 1/2,
省下的算力超过了跑两趟的固定开销。
| 配置 | eager | compile 解码器 | compile DiT | 都 compile |
|---|---|---|---|---|
| full + 7% 重叠 | 145.8 s | 145.7 s | 147.0 s | — |
| tiny | 78.6 s | 75.9 s | 78.6 s | 78.2 s |
编译确实生效了(日志里有 [compile] decoder compiled /
[compile] dit compiled,不是静默失败),但收益 ≤3%,在测量噪声以内。三个原因:
dynamic=True 之下拿不到静态 shape 特化的好处。tile_stride 参数拿到 2.7×,
而 torch.compile(业界公认的加速手段)拿到 0%。
如果一开始就凭直觉去上 compile,会白白花掉一天。
这一节是最可迁移的部分——换个模型、换个项目,这套判断方法一样用。
两个配置跑的是同一批片子,所以要看逐条相减的结果。 片子之间的难度差异(有的片子所有方法都得分低)会在配对里抵消掉。 直接比两个均值,真实差异会被片间方差淹没。
差值 = Σ(配置A在片i的分 − 配置B在片i的分) / N # 配对 而不是 mean(配置A所有分) − mean(配置B所有分) # 会被噪声淹没
有了差值均值还不够——它是 +0.03 还是 −0.03,可能纯属抽样波动。 Bootstrap 的做法是:从这 N 个差值里有放回地重抽 N 个,算一次均值; 重复 2000 次,得到 2000 个均值,取 2.5% 和 97.5% 分位数。
如果这个区间跨过 0,就是「测不出差异」,不能说谁更好。 随机种子固定(20260821),所以重跑不会让区间飘。
分块 A/B 里那 4 条低于阈值的片子,是专门用来验证测量工具的: 两边都没触发分块,所以必须逐字节相同。如果这里就不同, 说明管线有随机性,后面 16 条的差异全是噪声,整个实验作废。
同理,24 GB 显存模拟里我先跑了一个阳性对照—— 一个已知需要 30.5 GiB 的配置,在 23 GiB cap 下必须 OOM。 它确实 OOM 了,才证明 cap 真的在起作用,后面「通过」的行才有意义。
接缝检测是最好的例子。一条 1 像素宽的缝,在 PSNR 里的影响约等于 0, 但人眼一看就出戏。全局指标测不出局部失效。 所以要问:这个改动可能怎么坏?然后专门去测那个坏法。
| 类型 | 代表 | 衡量什么 | 什么时候用 |
|---|---|---|---|
| 全参考保真度 | PSNR、SSIM、LPIPS、DISTS | 和真值差多远 | 有 GT 时。判断「对不对」 |
| 无参考观感分 | MUSIQ、CLIP-IQA、MANIQA、NIQE | 看起来像不像一张高质量图 | 没 GT 时。判断「好不好看」 |
| 视频质量 | DOVER | 含时间维度的整体观感 | 判断闪烁、抖动 |
| 改动量 | Downsample-LPIPS | 把输出缩回原尺寸,和输入比——改写了多少原内容 | 没 GT 时的保真度代理 |
只等成功标记的监控会在失败时永久挂住。每个后台任务必须区分:
「进程死了且没成功标记」= 失败,不是「还在跑」。
放这里不是为了自罚,是因为这些坑的形状是通用的。
我曾经报告 FlashVSR 在 5090 上做不了 2688×1536 输出。
这是错的。挡住它的是我自己设的一个 px-budget 门限——
那个门限是按 kv_ratio 3.0 标定的,后来配置变了却没更新。
改掉门限后,连原版配置都能跑(385 s / 29.15 GiB)。
教训:把自己的保护性检查误当成硬件限制。 遇到「跑不了」,先确认是真的 OOM还是自己的代码拒绝启动。
我把整条片子的融合缓冲开成 float32,13.7 GB 的张量被反复拷贝 15 次, 一个 GPU 任务变成了 memcpy 瓶颈,跑 28 分钟还没完。 改成预分配 uint8 后同一条 625 s 跑完,输出逐位相同。
教训:GPU 任务跑得慢,先看 GPU 利用率。如果是 0%,问题不在 GPU。
缓冲按 m 帧分配,但管线只填 m−4 帧,剩下的尾巴保持零值,写出来是黑帧。
141 帧只写出 135 帧。靠打印逐帧覆盖表(wins / produced / missing)定位。
教训:涉及分块/窗口的代码,加一个「哪些索引被写过」的诊断输出, 比事后猜测省一个数量级的时间。
编排器用 Popen 开了 stdout 和 stderr 两个管道,却只 drain stdout。
full 变体加载 Wan VAE 时在 stderr 上话很多,64 KB 内核缓冲写满,
子进程卡在 pipe_write,不报错、不超时、永久挂住。
改成 stderr=STDOUT 合并。
容量测试跑得异常慢,一度以为是模型问题。实际是 PNG 落盘——
4 Mpx 一帧 0.69 s,360 帧就是 4 分钟,还和其他并发任务抢 IO(争用时飙到 5.5 s/帧)。
加了 --no-write 之后,容量探针的时间从两个多小时压到 20 分钟。
教训:测什么就只测什么。容量探针要量的是模型,不该把编码器算进去。
| 场景 | 配置 | 2688×1536×141 | 显存 |
|---|---|---|---|
| 画质优先 给人看 / 用 IQA 打分 |
--variant full --kv-ratio 1.5 --vae-tile 60 --vae-stride 56 |
147.2 s | 16.52 GiB |
| 速度优先 对着参考评 / 批量跑 |
--variant tiny(其余默认) |
157.3 s → 74.8 s* | 20.13 GiB |
| 原版参考 | --variant full --kv-ratio 3.0 且不分块 |
385.2 s | 29.15 GiB |
* 157.3 s 含 PNG 落盘,74.8 s 是纯推理(--no-write)。
两个数字口径不同,别混着比。
| 场景 | 23 GiB cap | 耗时 | 显存峰值 |
|---|---|---|---|
| 15s 16:9 2688×1536 | 通过 | 385.0 s | 16.52 GiB |
| 15s 21:9 3584×1536 | 通过 | 468.2 s | 19.52 GiB |
| 旋钮 | 作用 | 该不该调 |
|---|---|---|
variant | full=Wan VAE 解码器,tiny=TCDecoder | 看需求。这是唯一真正影响画质的旋钮 |
kv_ratio | 流式 KV 缓存长度 | 调到 1.5。省 2.9 GiB,画质影响 CI 跨 0 |
vae_stride | 解码分块步长(重叠量) | 调到 56。解码快 2.7×,保真度还略好 |
tile_px | 空间分块阈值 | 开着。21:9 的必要条件,对 tiny 还是双赢 |
vae_tile | 解码分块大小 | 保持 60。开大更慢更吃显存 |
sparse_ratio / local_range | LCSA 稀疏度 / 局部范围 | 别动。时间和显存都没有可测量影响,但它们是模型正确性的一部分 |
torch.compile | — | 别开。收益 ≤3%,在噪声内 |
所有数字均在 viggle_new_5090(8×RTX 5090 32G)上实测。 画质对比一律在无损 PNG 上计算,不经过网页视频那层编码。 配对统计用 2000 次 bootstrap 重采样,随机种子 20260821 固定。 本页只讨论 FlashVSR,不涉及其他模型。