论文概述

Stable Video Infinity (SVI) 是由 EPFL VITA Lab 提出的无限长度视频生成方法,在 ICLR 2026 获得 Oral 论文荣誉1

核心贡献

贡献描述
错误回收微调Error-Recycling Fine-Tuning (ERFT)
无限长度生成可从几秒扩展到任意时长
多条件支持音频、姿态、文本等多模态条件
零额外推理开销与基线模型相同的计算成本

作者团队

  • Wuyang Li, Wentao Pan, Po-Chien Luan, Yang Gao, Alexandre Alahi
  • VITA Lab, EPFL (瑞士洛桑联邦理工学院)

问题背景:视频生成的drift问题

什么是 Drift 问题

在视频生成领域,漂移问题 (Drift Problem) 是制约长视频生成的核心障碍1。当视频生成超过一定长度后,会出现:

  • 外观漂移:角色外观逐渐改变甚至变形
  • 场景漂移:背景布局无法保持一致
  • 物体消失/重复:物体莫名其妙出现或消失
  • 物理失效:重力、光影等物理规律崩溃

Drift 的本质原因

SVI 论文识别出 drift 问题的根本原因并非仅仅是误差累积,而是一个更根本的假设鸿沟:

训练假设:模型在干净数据上训练, conditioning on

测试现实:模型在自生成输出上推理, conditioning on

这种假设鸿沟 (Hypothesis Gap) 导致:

现有方法的局限

方法技术局限
修改噪声调度器调整去噪轨迹仅适用于单一提示
帧锚定固定关键帧场景同质化
循环平滑后处理技术无法纠正根本问题

这些方法都试图通过外部干预来缓解问题,而非让模型主动学习识别和纠正错误


核心创新:错误回收机制

核心洞察

SVI 的核心洞察是:让 DiT 模型学会主动检测并纠正自身的复合错误

与其尝试在推理时掩盖错误,不如在训练时就让模型学会处理错误累积的分布。

Error-Recycling Fine-Tuning (ERFT)

错误回收微调是 SVI 的核心创新,包含三个关键步骤:

┌─────────────────────────────────────────────────────────────┐
│                  Error-Recycling Fine-Tuning               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 注入 (Inject)                                           │
│     将历史错误注入干净输入,模拟错误累积轨迹                   │
│           x_clean ──► 注入错误  ──► x_error                 │
│                                                             │
│  2. 收集 (Collect)                                          │
│     单步双向集成近似预测,计算残差作为错误                     │
│           x_error ──► 一步去噪  ──► ε                       │
│                                                             │
│  3. 存储 (Bank)                                             │
│     动态存储错误到重放记忆,按时间步离散化                     │
│           Error Bank ◄── 跨时间步存储 ◄── 残差              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键机制详解

1. 错误注入 (Error Injection)

扩散模型流匹配 框架中注入历史错误:

其中:

  • 是干净的真实帧
  • 是 DiT 在之前生成中累积的历史错误
  • 是注入强度系数

2. 单步双向集成 (One-Step Bidirectional Integration)

使用近似预测计算残差:

这种双向集成方法高效地估算了模型对当前状态的”误差感知”。

3. 动态错误银行 (Dynamic Error Bank)

将错误存储在按时间步离散化的重放记忆中:

在训练新片段时,从银行中重采样错误模式进行监督。


技术实现

架构概述

SVI 基于 Diffusion Transformer (DiT) 架构,包括:

  1. 3D VAE:视频时空压缩
  2. DiT 主干:潜在空间去噪
  3. 条件编码器:多模态条件注入
  4. 错误银行模块:记忆化错误分布

训练流程

class ErrorRecyclingFineTuning:
    """
    错误回收微调主流程
    """
    def __init__(self, model, error_bank_size=10000):
        self.model = model
        self.error_bank = ErrorBank(size=error_bank_size)
        self.noise_scheduler = FlowMatchingScheduler()
        
    def training_step(self, batch, global_step):
        """
        单步训练
        """
        # 1. 采样干净视频片段
        x_clean = batch['video']  # [B, T, C, H, W]
        
        # 2. 时间步采样
        t = sample_timesteps(len(x_clean), self.model.num_timesteps)
        
        # 3. 添加噪声 (流匹配)
        noise = torch.randn_like(x_clean)
        x_noisy = self.noise_scheduler.add_noise(x_clean, t, noise)
        
        # 4. 错误注入 (关键创新)
        if global_step > self.warmup_steps:
            # 从错误银行采样历史错误
            sampled_errors = self.error_bank.sample(batch_size=len(x_clean))
            
            # 注入到输入中
            x_injected = self.inject_errors(x_noisy, sampled_errors, t)
        else:
            x_injected = x_noisy
        
        # 5. 预测噪声/速度
        noise_pred = self.model(x_injected, t, condition=batch['condition'])
        
        # 6. 计算损失
        loss = self.compute_loss(noise_pred, noise)
        
        # 7. 计算并存储残差 (错误)
        with torch.no_grad():
            residual = self.compute_residual(x_injected, noise_pred, t)
            
            # 按时间步存储
            self.error_bank.add(residual, t)
        
        return loss
 
 
class ErrorBank:
    """
    动态错误银行
    按时间步离散化存储错误
    """
    def __init__(self, size=10000):
        self.size = size
        self.bank = {}  # {timestep: list of (error, count)}
        self.counts = {}
        
    def add(self, residual, timestep):
        """添加残差到银行"""
        t_idx = self.discretize_timestep(timestep)
        
        if t_idx not in self.bank:
            self.bank[t_idx] = []
            self.counts[t_idx] = 0
        
        # FIFO 缓冲
        if len(self.bank[t_idx]) >= self.max_per_timestep:
            self.bank[t_idx].pop(0)
            
        self.bank[t_idx].append(residual.detach())
        self.counts[t_idx] += 1
        
    def sample(self, batch_size):
        """从银行采样错误"""
        errors = []
        
        for _ in range(batch_size):
            # 随机选择时间步
            t_idx = random.choice(list(self.bank.keys()))
            
            if len(self.bank[t_idx]) > 0:
                # 随机采样该时间步的错误
                error = random.choice(self.bank[t_idx])
                errors.append(error)
            else:
                errors.append(None)
        
        return errors
    
    def discretize_timestep(self, t, num_bins=100):
        """离散化时间步"""
        return int(t * num_bins / self.model.num_timesteps)

推理流程

@torch.no_grad()
def generate_infinite_video(model, start_clip, condition_fn, 
                              window_size=16, step_size=12):
    """
    无限长度视频生成
    
    Args:
        model: SVI 模型
        start_clip: 初始片段
        condition_fn: 条件函数(文本、音频等)
        window_size: 窗口大小
        step_size: 步长 (< window_size 用于重叠)
    """
    video_frames = []
    overlap_buffer = None
    
    # 生成初始片段
    current = start_clip
    
    while True:
        # 1. 获取当前窗口的条件
        condition = condition_fn(
            current_frame_idx=len(video_frames),
            context=overlap_buffer
        )
        
        # 2. 生成当前窗口
        window = model.generate(
            start_frame=current,
            num_frames=window_size,
            condition=condition,
            guidance_scale=7.5
        )
        
        # 3. 拼接视频
        if len(video_frames) == 0:
            video_frames = window[:window_size]
        else:
            # 保留非重叠部分
            video_frames = torch.cat([
                video_frames,
                window[window_size - step_size:]
            ], dim=0)
        
        # 4. 更新重叠缓冲区
        overlap_buffer = window[-step_size:]
        current = overlap_buffer[0:1]  # 下一窗口的起始帧
        
        # 5. 检查停止条件
        if condition_fn.is_end():
            break
    
    return video_frames

实验结果

基准测试

SVI 在三个基准上进行了全面评估:

基准描述评估指标
一致性基准场景、角色、外观一致性FID, FVD, CS
创意基准复杂动作、场景转换人工评分
条件基准音频驱动、姿态控制CLIPScore

与 SOTA 方法对比

方法最大长度时间一致性场景多样性推理开销
自回归 DiT~10秒0.451.0x
Self-Forcing~2分钟0.613.0x
LoL~10分钟0.682.5x
MALT任意0.721.5x
SVI无限0.821.0x

关键发现

  1. 零额外推理开销:SVI 无需额外的推理计算,与基线模型相同
  2. 质量稳定:视频质量不随长度下降
  3. 场景切换自然:支持复杂的场景转换
  4. 多条件兼容:音频、姿态、文本等条件无缝集成

代码实现

PyTorch 伪代码

import torch
import torch.nn as nn
from typing import Callable, Optional, List
 
 
class ErrorRecyclingModule(nn.Module):
    """
    错误回收模块
    SVI 的核心组件
    """
    def __init__(self, model: nn.Module, num_timesteps: int = 1000,
                 error_bank_size: int = 10000, inject_strength: float = 0.1):
        super().__init__()
        self.model = model
        self.num_timesteps = num_timesteps
        self.inject_strength = inject_strength
        
        # 错误银行:按时间步组织
        self.register_buffer('error_bank', torch.zeros(
            num_timesteps, error_bank_size, model.latent_dim
        ))
        self.error_bank_ptr = torch.zeros(num_timesteps, dtype=torch.long)
        
    def inject_errors(self, x: torch.Tensor, t: torch.Tensor,
                      errors: Optional[torch.Tensor] = None) -> torch.Tensor:
        """
        将历史错误注入干净输入
        
        Args:
            x: 输入张量 [B, T, C, H, W]
            t: 时间步 [B]
            errors: 可选的预计算错误
        
        Returns:
            注入错误后的张量
        """
        if errors is None:
            # 从错误银行采样
            errors = self.sample_errors_from_bank(t, x.shape)
        
        if errors is None:
            return x
            
        # 缩放错误并注入
        errors = errors.to(x.device)
        if errors.shape != x.shape:
            errors = errors.repeat_interleave(
                x.shape[1] // errors.shape[1] + 1, dim=1
            )[:, :x.shape[1]]
        
        return x + self.inject_strength * errors
    
    def sample_errors_from_bank(self, t: torch.Tensor, 
                                  shape: tuple) -> Optional[torch.Tensor]:
        """从错误银行采样"""
        batch_size = shape[0]
        sampled = []
        
        for i in range(batch_size):
            t_idx = int(t[i].item() * len(self.error_bank) / self.num_timesteps)
            t_idx = min(t_idx, len(self.error_bank) - 1)
            
            ptr = self.error_bank_ptr[t_idx]
            if ptr > 0:
                idx = torch.randint(0, ptr, (1,))
                sampled.append(self.error_bank[t_idx, idx])
            else:
                sampled.append(None)
        
        # 过滤 None 值
        valid = [s for s in sampled if s is not None]
        if len(valid) == 0:
            return None
            
        return torch.stack(valid)
    
    def update_error_bank(self, x: torch.Tensor, pred: torch.Tensor, 
                           t: torch.Tensor):
        """更新错误银行"""
        # 计算残差作为错误
        residual = x - pred.detach()
        
        for i in range(len(t)):
            t_idx = int(t[i].item() * len(self.error_bank) / self.num_timesteps)
            t_idx = min(t_idx, len(self.error_bank) - 1)
            
            ptr = self.error_bank_ptr[t_idx].item()
            if ptr < self.error_bank.shape[1]:
                self.error_bank[t_idx, ptr] = residual[i].detach()
                self.error_bank_ptr[t_idx] = ptr + 1
    
    def forward(self, x: torch.Tensor, t: torch.Tensor,
                condition: Optional[torch.Tensor] = None,
                inject: bool = True) -> torch.Tensor:
        """
        前向传播
        
        Args:
            x: 噪声/干净视频
            t: 时间步
            condition: 条件嵌入
            inject: 是否注入错误
        """
        if inject and self.training:
            x = self.inject_errors(x, t)
        
        output = self.model(x, t, condition)
        
        if self.training:
            self.update_error_bank(x, output, t)
        
        return output
 
 
class SVIGenerator:
    """
    Stable Video Infinity 生成器
    """
    def __init__(self, model: ErrorRecyclingModule,
                 vae, scheduler, device: str = 'cuda'):
        self.model = model
        self.vae = vae
        self.scheduler = scheduler
        self.device = device
        
    @torch.no_grad()
    def generate(self, 
                 prompt: str,
                 num_frames: int = 16,
                 window_size: int = 16,
                 step_size: int = 12,
                 guidance_scale: float = 7.5,
                 num_inference_steps: int = 50) -> torch.Tensor:
        """
        生成无限长度视频
        
        Args:
            prompt: 文本提示
            num_frames: 目标帧数
            window_size: 生成窗口大小
            step_size: 步长
            guidance_scale: CFG 引导强度
            num_inference_steps: 去噪步数
        
        Returns:
            生成的视频 [T, C, H, W]
        """
        # 编码文本条件
        text_emb = self.encode_prompt(prompt)
        
        # 初始化潜在表示
        latents = torch.randn(
            1, num_frames, self.vae.latent_dim,
            device=self.device
        )
        
        # 去噪过程
        for t in reversed(range(num_inference_steps)):
            # 扩展潜在表示以覆盖所有窗口
            t_tensor = torch.full((latents.shape[0],), t, 
                                  device=self.device, dtype=torch.long)
            
            # 预测噪声
            noise_pred = self.model(
                latents, t_tensor, condition=text_emb, inject=False
            )
            
            # 应用引导
            if guidance_scale > 1.0:
                noise_pred = guidance_scale * noise_pred - (guidance_scale - 1) * latents
            
            # 调度器步骤
            latents = self.scheduler.step(noise_pred, t, latents)
        
        # 解码潜在表示
        video = self.vae.decode(latents)
        
        return video
    
    def encode_prompt(self, prompt: str) -> torch.Tensor:
        """编码文本提示"""
        # 简化实现
        return self.model.model.condition_encoder(prompt)
 
 
# 训练示例
def train_step(model, batch, optimizer):
    """
    SVI 训练步骤
    """
    # 解码视频到潜在空间
    with torch.no_grad():
        x_clean = model.vae.encode(batch['video'])
    
    # 采样时间步
    t = torch.randint(0, model.num_timesteps, (batch['video'].shape[0],),
                      device=batch['video'].device)
    
    # 添加噪声
    noise = torch.randn_like(x_clean)
    x_noisy = model.scheduler.add_noise(x_clean, t, noise)
    
    # 预测噪声
    noise_pred = model(
        x_noisy, t, 
        condition=batch['condition'],
        inject=True  # 注入错误进行学习
    )
    
    # 计算损失
    loss = nn.functional.mse_loss(noise_pred, noise)
    
    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    return loss.item()

与其他长视频方法对比

维度自回归分层生成滑动窗口记忆增强SVI
核心技术逐帧生成金字塔固定拼接记忆查询错误回收
长度限制受限中等固定任意无限
一致性↓↓↑↑
场景切换不支持简单不支持支持完全支持
计算开销
质量保持优秀

SVI 的独特优势

  1. 假设对齐:训练时模拟推理错误分布,弥合假设鸿沟
  2. 无需后处理:无需额外的平滑或修正模块
  3. 即插即用:可应用于多种 DiT 架构
  4. 持续学习:错误银行随训练不断丰富

参考资源

论文

  • arXiv: 2510.09212 - Stable Video Infinity: Infinite-Length Video Generation with Error Recycling

代码

演示

相关论文


总结

Stable Video Infinity 通过 Error-Recycling Fine-Tuning 机制解决了一个根本性问题:

训练与推理之间的假设鸿沟

核心要点

  1. 问题本质:Drift 不是简单的误差累积,而是训练-推理分布不匹配
  2. 解决方案:让模型主动学习识别和纠正自身错误
  3. 实现方式:错误注入 + 残差计算 + 动态存储
  4. 效果:无限长度、高质量、多条件支持

未来方向

  • 更高效的错误银行管理
  • 自适应错误注入强度
  • 与世界模型的结合

本页面基于 arXiv:2510.09212 构建,ICLR 2026 Oral 论文。

Footnotes

  1. SVI 论文获得 ICLR 2026 Oral 认可,展示了视频生成领域的重要突破。 2