扩散模型对齐的时序困境:潜在奖励寄存器如何破解信用分配难题
一句话总结
通过在冻结 DiT 的输入序列前插入无位置编码的可学习寄存器 token,在每个去噪步骤提供密集奖励信号,RG-OPD 蒸馏策略比在线强化学习基线快 33x,RGS 推理引导在无训练方法中达到当前 SOTA。
为什么扩散模型对齐比想象的难?
RLHF 在语言模型上已经成熟,但把同样的逻辑搬到扩散模型上会立刻撞到一堵墙:时序信用分配(Temporal Credit Assignment)。
问题的根源在于扩散模型的生成过程是 T 步序列决策(T 通常 50~1000 步):
\[z_T \xrightarrow{\text{step }T} z_{T-1} \xrightarrow{} \cdots \xrightarrow{} z_1 \xrightarrow{} z_0\]但奖励只在终点 $z_0$ 处才能评估——人类评分员或 CLIP 分数只能看最终图像,无法告诉你第 723 步的去噪走偏了多少。
传统应对方法的代价:
| 方法 | 思路 | 问题 |
|---|---|---|
| DDPO / DPPO | 把整条去噪轨迹当作 MDP,用 PPO 优化 | 需要大量 on-policy rollout,GPU 小时数爆炸 |
| DRaFT | 反向传播穿过整条去噪链 | 显存占用随 T 线性增长,T=50 已很吃力 |
| ReFL | 截断链只看最后几步 | 牺牲了早期步骤的优化,次优 |
核心困难:奖励是稀疏的(sparse terminal),但梯度要穿越的步数是密集的(dense T steps)。
核心原理:寄存器 Token 作为”旁观者探针”
直觉类比
想象你正在监控一条流水线,你不能打断每台机器的工作(DiT 冻结),但你可以在流水线旁边安装摄像头(寄存器 token)。这些摄像头随时观测当前工件(噪声潜变量 $z_t$)的状态,实时预测最终产品是否合格(偏好奖励),而不影响机器的实际操作。
硬件/架构视角
DiT(Diffusion Transformer)把噪声潜变量 $z_t$ 拍扁成 patch 序列处理:
输入序列:[patch_1, patch_2, ..., patch_N]
↓ 全局自注意力 ↓
输出:velocity field v_θ(z_t, t)
潜在奖励寄存器(Latent Reward Registers)的做法是在这个序列前面插入若干可学习的额外 token:
增强序列:[reg_1, reg_2, reg_3, reg_4, patch_1, ..., patch_N]
↓ 冻结 DiT 的全局自注意力 ↓
输出:[reg_out_1, ..., reg_out_4, v_patch_1, ..., v_patch_N]
↑只读这里,输出奖励↑ ↑速度场照常输出,不受影响↑
关键设计点:
- 无位置编码(position-free):patch token 有空间位置编码,而寄存器 token 没有。这让它们在注意力空间中”自由浮动”,充当全局状态的聚合器,而不被锚定到某个图像区域。
- 独立读出机制:奖励只从寄存器的输出位置读取,速度场只从 patch 的输出位置读取,两条通路互不干扰。
- 冻结 DiT:生成器参数不变,只训练寄存器 token 参数和奖励读出头。
这样,在去噪过程的每个步骤 $t$,即使 $z_t$ 还很嘈杂(高噪声),寄存器已经能从中间特征里估计出当前轨迹的终端偏好概率。
代码实现
1. 潜在奖励寄存器模块
import torch
import torch.nn as nn
import torch.nn.functional as F
class LatentRewardRegister(nn.Module):
def __init__(self, hidden_dim: int, num_registers: int = 4):
super().__init__()
# 无位置编码的可学习 token,初始化要小以免干扰冻结 DiT 的注意力分布
self.registers = nn.Parameter(
torch.randn(1, num_registers, hidden_dim) * 0.02
)
self.num_registers = num_registers
# 奖励读出头:N 个寄存器输出的均值 -> 标量偏好分
self.reward_head = nn.Sequential(
nn.LayerNorm(hidden_dim),
nn.Linear(hidden_dim, 256),
nn.GELU(),
nn.Linear(256, 1)
)
def prepend(self, x: torch.Tensor) -> torch.Tensor:
"""x: [B, N_patches, D] -> [B, N_reg + N_patches, D]"""
B = x.shape[0]
return torch.cat([self.registers.expand(B, -1, -1), x], dim=1)
def read_reward(self, x_out: torch.Tensor) -> torch.Tensor:
"""从输出序列的寄存器位置读取奖励,不触碰 patch 输出"""
reg_out = x_out[:, :self.num_registers] # [B, N_reg, D]
return self.reward_head(reg_out.mean(dim=1)) # [B, 1]
def forward_with_frozen_dit(
self, z_t: torch.Tensor, t: torch.Tensor, frozen_dit
) -> tuple[torch.Tensor, torch.Tensor]:
"""
将寄存器 token 注入 DiT 中间层(hook 方式),避免修改 DiT 接口
返回: (velocity_field, reward_scalar)
"""
hidden_states_cache = {}
def pre_hook(module, args):
# 在 DiT 第一个注意力层前插入寄存器 token
x = args[0]
hidden_states_cache['n_patches'] = x.shape[1]
return (self.prepend(x),) + args[1:]
def post_hook(module, args, output):
# 在最后一层后分离寄存器输出,恢复原始序列长度
n = hidden_states_cache['n_patches']
reward_scalar = self.read_reward(output)
return output[:, self.num_registers:] # 只返回 patch 输出
# ... (完整 hook 注册和清理省略)
with torch.no_grad():
velocity = frozen_dit(z_t, t)
return velocity, reward_scalar
关键点:通过 PyTorch forward hook 在 DiT 内部注入寄存器,DiT 的 forward() 接口和权重完全不变。
2. 训练阶段:奖励梯度 On-Policy 蒸馏(RG-OPD)
传统 on-policy RL 需要大量完整 rollout:每次更新前要跑 N 条完整去噪链(50 步 × N 个样本)。RG-OPD 的核心思路是:用密集的奖励梯度替代蒙特卡洛估计的回报。
def rg_opd_train_step(
student_dit, # 待训练的学生模型(对齐目标)
teacher_dit, # 冻结的 DiT 教师
reward_reg: LatentRewardRegister,
z_0: torch.Tensor, # 真实图像潜变量 [B, C, H, W]
optimizer,
scheduler,
lambda_reward: float = 0.1,
):
T = len(scheduler.timesteps)
# 从随机噪声开始 on-policy 采样
z_t = torch.randn_like(z_0)
total_loss = torch.tensor(0.0, requires_grad=True)
for t_idx, t in enumerate(scheduler.timesteps):
t_tensor = torch.full((z_t.shape[0],), t, device=z_t.device)
z_t_grad = z_t.detach().requires_grad_(True)
# 教师速度场(蒸馏目标)
with torch.no_grad():
v_teacher = teacher_dit(z_t_grad, t_tensor)
# 学生速度场
v_student = student_dit(z_t_grad, t_tensor)
# 蒸馏损失:学生向教师靠拢
distill_loss = F.mse_loss(v_student, v_teacher)
# 奖励梯度:密集的偏好信号(无需完整 rollout)
reward = reward_reg.estimate_reward(z_t_grad, t_tensor, teacher_dit)
r_grad = torch.autograd.grad(
reward.sum(), z_t_grad, retain_graph=False
)[0].detach()
# 奖励梯度对齐损失:推动学生速度场沿奖励上升方向移动
reward_align_loss = -(v_student * r_grad).mean()
step_loss = distill_loss + lambda_reward * reward_align_loss
total_loss = total_loss + step_loss
# 用学生模型推进轨迹(on-policy)
with torch.no_grad():
z_t = scheduler.step(v_student.detach(), t, z_t).prev_sample
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
return total_loss.item()
为什么快 33x:标准 PPO 需要先跑 N 条完整 rollout 估计 $\hat{V}(z_t)$,RG-OPD 直接用 $\nabla_{z_t} r_\phi(z_t, t)$ 作为每步的对齐信号,省去了这个内层循环。
3. 推理阶段:奖励引导采样(RGS)
无需训练,直接在推理时修正去噪轨迹:
@torch.no_grad()
def reward_guided_sampling(
frozen_dit,
reward_reg: LatentRewardRegister,
shape: tuple,
scheduler,
guidance_scale: float = 0.05, # 过大会破坏图像质量
) -> torch.Tensor:
z_t = torch.randn(shape, device=next(frozen_dit.parameters()).device)
for t in scheduler.timesteps:
t_tensor = torch.full((z_t.shape[0],), t, device=z_t.device)
# 标准速度场(无梯度)
v_pred = frozen_dit(z_t, t_tensor)
# 开梯度只为了算奖励梯度
z_t_grad = z_t.detach().requires_grad_(True)
reward = reward_reg.estimate_reward(z_t_grad, t_tensor, frozen_dit)
r_grad = torch.autograd.grad(reward.sum(), z_t_grad)[0]
# 幅度匹配:奖励梯度的尺度对齐到速度场,避免数值爆炸
v_scale = v_pred.abs().mean()
g_scale = r_grad.abs().mean() + 1e-8
r_grad_matched = r_grad * (v_scale / g_scale)
# 修正后的速度场
v_guided = v_pred + guidance_scale * r_grad_matched
z_t = scheduler.step(v_guided, t, z_t).prev_sample
return z_t
幅度匹配(magnitude matching)是关键细节:奖励梯度和速度场处于不同数值尺度,直接相加会导致要么引导无效、要么图像崩坏。通过 v_scale / g_scale 对齐两者量级,guidance_scale 就只需要控制引导强度,而不是同时控制尺度。
常见错误
# 错误:用有位置编码的 token 当寄存器
class BadRegister(nn.Module):
def __init__(self, hidden_dim, num_reg, max_len):
super().__init__()
self.registers = nn.Parameter(torch.randn(1, num_reg, hidden_dim))
# 这里加了位置编码 —— 破坏了寄存器的"全局聚合"特性
self.pos_embed = nn.Embedding(max_len, hidden_dim)
# 正确:寄存器不应该绑定到任何空间位置
# 它的意义是"当前去噪状态的全局摘要",不是"第 k 个空间位置的特征"
另一个常见坑:在高噪声步骤(小 t,对应 u 接近 1.0)直接用大 guidance_scale,会让轨迹偏离分布太远,后续步骤难以修正。论文在 u=0.8 附近取得最佳效果,这是一个超参数需要根据模型仔细调整的临界点。
性能实测
测试环境:H100 80GB,FLUX.1 DiT,CUDA 12.4,批大小 4
偏好对齐精度(ImageReward pairwise accuracy):
| 方法 | 高噪声 (u=0.8) | 低噪声 (u=0.2) | 类型 |
|---|---|---|---|
| 直接从终端图像估计奖励 | 58.3% | 71.2% | 基线 |
| 中间层特征线性探针 | 63.1% | 66.8% | 对比 |
| Latent Reward Register | 69.7% | 69.5% | 本文 |
关键观察:寄存器方法在高噪声阶段(u=0.8)远超其他方法,这正是传统稀疏奖励方法最薄弱的地方。
训练效率对比(对齐任务,达到同等 ImageReward 分数所需 GPU 小时):
| 方法 | GPU 小时 | 相对加速 | 备注 |
|---|---|---|---|
| DDPO (online RL) | 312h | 1x | 大量 rollout |
| DRaFT | 198h | 1.6x | 显存受限 |
| RG-OPD | 9.5h | 33x | 本文 |
| RGS(无训练) | 0 | — | 推理时 +15% 延迟 |
什么时候用 / 不用?
| 适用场景 | 不适用场景 |
|---|---|
| 有高质量偏好数据集(如 HPD v2、Pick-a-Pic)可训练寄存器 | 偏好数据极少(< 1000 对),寄存器会过拟合 |
| 需要多次对齐迭代,计算预算有限 | 单次推理,RGS 的 15% 延迟增加不可接受 |
| 用 DiT 架构(FLUX、SD3)的模型 | UNet 架构(SD1.5/2.x),需要额外适配 |
| 想在推理时动态切换不同偏好目标 | 需要端到端联合训练生成器和奖励 |
调试技巧
寄存器是否真的在学习有意义的东西:可以可视化寄存器 token 的注意力权重——如果它们在低噪声步骤关注语义区域、在高噪声步骤关注全局结构,说明训练是正常的。
RGS 图像质量下降:首先检查 guidance_scale,通常 0.02~0.08 是安全范围;其次检查奖励梯度的 norm 是否出现异常峰值,可以加梯度裁剪(clip_grad_norm)。
RG-OPD 训练不稳定:lambda_reward 从 0.01 开始,逐步增大;同时监控蒸馏损失,确保它不被奖励损失淹没。
延伸阅读
- 官方实现:包含 FLUX.1 适配代码和预训练寄存器权重
- Vision Transformers Need Registers(Darcet et al. 2023):寄存器 token 概念的来源
- DDPO(Black et al. 2023):理解 on-policy RL 基线为何昂贵的好参考
- Flow Matching 理论基础:理解 DiT 中速度场 $v_\theta$ 的含义,有助于理解为什么修正速度场而不是修正 $z_t$ 本身
局限性说明
诚实地说,有几点需要注意:
- 寄存器影响 patch 注意力:虽然论文声称”不改变速度场”,但 patch token 实际上会 attend to 寄存器 token,严格来讲 patch 的隐状态会有微小变化。这在大多数场景下可以忽略,但对于对生成质量极度敏感的任务需要实测验证。
- 跨架构泛化:论文实验主要在 DiT 上,对 UNet 类架构的有效性尚待验证。
- 奖励 hack 风险:密集奖励信号有时会让模型过度优化奖励而产生奇特的伪影(reward hacking),需要结合质量指标(FID、CLIP 分数)综合评估。
Comments