OPD 在 Diffusion / Flow Matching 上的应用:三兄弟 + 分布匹配蒸馏 + 统一视角

On-Policy Distillation(OPD,同策略蒸馏) 是 LLM 后训练里把"student 自己采样、teacher 逐步纠正"做成范式的一套方法(GKD, Agarwal et al. 2023)。2026 年 5 月,几乎同一周冒出了把它搬进文生图扩散/flow matching 的"三兄弟"——Flow-OPDDiffusionOPDD-OPSD——三者共享同一个漂亮的数学折叠,却在"要不要 RL、要不要 reward、teacher 是谁"上给出不同答案。

本教程用 7 个螺旋结构章节(直觉 → 最小 demo → 正式化 → 真实代码 → 洞察)把这条线讲透:§1–§2 立地基(on-policy 为什么治 exposure bias;SDE 同协方差 → reverse-KL = 速度场 L2);§3–§5 是三兄弟;§6 讲近亲 DMD/SiD 分布匹配蒸馏(为提速而非对齐);§7 把 RL-for-diffusion、知识蒸馏、分布匹配三条线收敛成一个统一模板 + 四个选择轴。所有 Step-4 代码 verbatim 引自四个真实训练脚本(Flow-OPD / DiffusionOPD / D-OPSD / DMD2),并经 diff 逐行验证。

阅读提示:§2 是全篇支柱(KL→L2 折叠),后面每节都站在它上面;§7 是把五个方法压成一句话的综合。带着"teacher 在 student 轨迹上给一个 per-state 目标"这把尺子读,各方法的异同会一目了然。

1. 为什么需要 OPD:从 LLM 的 GKD 到扩散的两道死结

1.1 直觉

学写作文有两种方式。第一种是"照抄范文":老师把一篇满分作文摊在你面前,你照着抄、记住它每个句子。问题是,真到考场上自己写,你写出的句子和范文长得不一样,一旦开头跑偏,后面越写越离谱——而范文里根本没有"如何从你这个错误的开头补救"的示范。这种"训练时只见过范文、自己写时却到了没人教过的地方"的毛病,就叫 exposure bias(曝光偏差)

第二种是"自己写、老师逐句改":你先按自己的水平写一句,老师立刻在你这句话的基础上告诉你下一句该怎么接。监督打在你自己真会写出的句子上,正好治住第一种的病。

把"老师"换成一个强模型(teacher)、"学生"换成待训练的模型(student),这第二种就是本教程的主角:OPD = On-Policy Distillation(同策略蒸馏)——student 用自己采样出的样本,teacher 在同一个状态上给出学习目标。图 1 把这两种监督画在了一起。

1.2 最小 demo

下面用一个一维玩具说明:offline 监督打在 teacher 分布上,但 student 自己真正会走到的区域,offline 根本没覆盖。

import numpy as np
rng = np.random.default_rng(0)

# teacher 分布集中在 0 附近;student 当前还很偏,集中在 3 附近
teacher_samples = rng.normal(0.0, 1.0, size=2000)   # offline 监督打在这里
student_samples = rng.normal(3.0, 1.0, size=2000)   # student 自己真正会到的状态

def covered(region, samples):       # 落在某区间的样本占比
    lo, hi = region
    return np.mean((samples >= lo) & (samples < hi))

student_region = (2.0, 4.0)         # student 高频访问区
print("offline 在该区监督密度:", covered(student_region, teacher_samples))  # ~0.02
print("on-policy 在该区监督密度:", covered(student_region, student_samples)) # ~0.68

offline 的监督几乎不落在 student 实际访问的 (2,4) 区——那里没人教,exposure bias 由此而来;on-policy 把监督直接搬到 student 自采的样本上。

1.3 正式化

(1) GKD 的 on-policy 目标。 在 LLM 上,GKD / On-Policy Distillation(Agarwal et al., arXiv:2306.13649, ICLR 2024)的核心是:序列由 student 策略 \(\pi_\theta\) 自己采样,再让 teacher \(\pi_T\) 在这些状态上逐步纠正:

\[\min_\theta\ \mathbb{E}_{x\sim\pi_\theta}\big[\mathrm{KL}(\pi_\theta\Vert\pi_T)\big]\]

—— 翻译:期望下标 $x\sim\pi_\theta$ 表示样本由 student 自己采;在这些样本上把 student 分布拉向 teacher 分布,监督只打在 student 真会到的状态。

(2) 扩散多任务 RL 的两道死结。 把这套搬到扩散/flow matching 上,会撞上两个结构性难题。第一道是 reward 稀疏:一个标量 reward 只在去噪末步(\(t=0\))才有,中间每一步都没有信号,只能把同一个优势 \(A\) 广播回每一步:

\[A_t = A,\qquad \forall\, t \in \{T, T-1, \dots, 1\}\]

—— 翻译:每一步 $t$ 拿到的优势 $A_t$ 都等于末步那个唯一的标量 $A$,中间步骤没有独立信号,信用分配靠"把末步结果摊给全程"。

第二道是 梯度干扰:多个异构目标 \(J_k\)(如美学、文本对齐、构图)被联合优化,当两个目标的梯度方向相反时,会互相破坏,呈现"跷跷板"——按下一个翘起另一个:

\[\langle \nabla J_k,\ \nabla J_l\rangle \lt 0 \ \Longrightarrow\ \text{更新}\ \nabla J_k\ \text{会拉低}\ J_l\]

—— 翻译:两个目标梯度内积为负,说明方向冲突;沿其中一个目标更新参数,会同时降低另一个目标的表现,这就是多任务优化里的"跷跷板"效应。

记住这两道结(reward 稀疏 + 梯度干扰),后面每个方法本质上都在拆其中一道。

1.4 代码引用

on-policy 的第一前提,是 student 得能"采样出多样轨迹"。但 flow matching 的原生采样是确定性 ODE:同一个初值只会走出同一条轨迹,没法探索,也没有可微的 log 概率。Flow-GRPO(arXiv:2505.05470)的关键一步,是把这条确定性 ODE 改写成随机 SDE:

Flow-GRPO/flow_grpo/diffusers_patch/sd3_sde_with_logprob.py:L46-L68 — ODE→SDE:把确定性 flow 变成随机策略,on-policy 探索的前提

    sigma_max = self.sigmas[1].item()
    dt = sigma_prev - sigma

    if sde_type == 'sde':
        std_dev_t = torch.sqrt(sigma / (1 - torch.where(sigma == 1, sigma_max, sigma)))*noise_level

        # our sde
        prev_sample_mean = sample*(1+std_dev_t**2/(2*sigma)*dt)+model_output*(1+std_dev_t**2*(1-sigma)/(2*sigma))*dt

        if prev_sample is None:
            variance_noise = randn_tensor(
                model_output.shape,
                generator=generator,
                device=model_output.device,
                dtype=model_output.dtype,
            )
            prev_sample = prev_sample_mean + std_dev_t * torch.sqrt(-1*dt) * variance_noise

        log_prob = (
            -((prev_sample.detach() - prev_sample_mean) ** 2) / (2 * ((std_dev_t * torch.sqrt(-1*dt))**2))
            - torch.log(std_dev_t * torch.sqrt(-1*dt))
            - torch.log(torch.sqrt(2 * torch.as_tensor(math.pi)))
        )

关键在 std_dev_t:它给原本确定的下一步均值 prev_sample_mean 注入了一份随机噪声 std_dev_t * sqrt(-dt) * variance_noise。这一注入把确定性 flow 变成了随机策略——同一状态可以采出不同的下一步(on-policy 探索),同时高斯形式让 log_prob 可解析计算。没有这步随机化,就没有可采样的策略、没有 log 概率,也就谈不上 RL 或 OPD。(其中的 velocity 含义、SDE 均值 \(\mu\)\(\sigma_t\) 与 KL→L2 的关系,留到 §2 系统展开。)

1.5 洞察

teacher / data 分布 offline 监督打在这里 student 自采分布 on-policy 监督打在这里 student 访问区 offline 无监督 = exposure bias
图 1 — offline 蒸馏在 teacher 分布上监督,但 student 自己采样会跑到没监督的区域(exposure bias);on-policy 直接在 student 轨迹上让 teacher 纠正。

2. 核心折叠:SDE 同协方差 → reverse-KL = 速度场 L2

2.1 直觉

想象两条形状一模一样、只是中心错开的钟形曲线:它们的标准差完全相同,唯一的区别是峰顶的横坐标不一样。直觉上,这两条曲线的"差异"只跟两个中心之间的距离有关,跟曲线的胖瘦无关——而 KL 散度恰好把这个直觉精确化了。

§1 说 on-policy distillation 让 student 自己采轨迹、teacher 在同一个状态上给目标。在扩散里,这个"同一状态"意味着 student 和 teacher 的反向单步分布是两个同协方差的高斯:它们的噪声尺度 \(\bar\sigma_t\) 由调度器决定,两边完全一致,只有均值不同。于是 reverse-KL 不需要任何近似,就解析地坍缩成两个均值之间的 L2 距离。

而均值之差又是速度场之差的仿射函数,所以最终我们得到的是一个稠密的、每一步都存在的速度场监督——不是稀疏的终点奖励,而是沿着整条采样轨迹每一刻都有的 per-state 目标。这就是整条 OPD-on-diffusion 路线的数学地基,见图 2。

μ^S μ^T KL = ‖μ^S−μ^T‖² / 2σ² ODE: σ→0 ⇒ 纯 L2
图 2 — 学生/教师单步分布同协方差,reverse-KL 坍缩成两个均值的 L2;σ→0 的 ODE 极限退化成纯 velocity L2。

2.2 最小 demo

import numpy as np

# 两个同协方差(同 sigma)的各向同性高斯
d = 8
sigma = 0.7
mu_s = np.array([1.0, 0.5, -0.3, 0.2, 0.0, 0.9, -1.1, 0.4])  # student 均值
mu_t = np.array([1.2, 0.1, -0.5, 0.6, 0.3, 0.7, -0.8, 0.0])  # teacher 均值

# 蒙特卡洛估计 KL(N_s || N_t):在 student 分布上采样
N = 2_000_000
x = mu_s + sigma * np.random.randn(N, d)
log_ps = -0.5 * ((x - mu_s) ** 2).sum(1) / sigma**2   # 去掉公共归一化常数
log_pt = -0.5 * ((x - mu_t) ** 2).sum(1) / sigma**2
kl_mc = (log_ps - log_pt).mean()

kl_closed = ((mu_s - mu_t) ** 2).sum() / (2 * sigma**2)  # ‖Δμ‖²/(2σ²)
print(f"MC   KL = {kl_mc:.4f}")     # ≈ 0.7245
print(f"形式  KL = {kl_closed:.4f}")  # = 0.7245 —— 完全吻合

同协方差高斯的 KL 的归一化常数与 trace 项全部抵消,只剩两个均值差的 L2,除以 \(2\sigma^2\)。蒙特卡洛值与闭式值在三位小数上吻合,这就是 §2.3 推导的数值确证。

2.3 正式化

(1) 反向 SDE 的单步分布。 用 Euler–Maruyama 离散反向 SDE,从 \(x_t\) 走一步到 \(x_{t-1}\),得到一个高斯转移:

\[x_{t-1}\sim\mathcal N\!\big(\mu_\theta(x_t),\,\bar\sigma_t^2 I\big),\qquad \mu_\theta(x_t)=A_t\,x_t + B_t\,v_\theta(x_t,t)\,dt.\]

—— 翻译:单步落点是高斯;均值 $\mu_\theta$ 是当前 latent $x_t$ 加速度场 $v_\theta$ 的仿射组合,$A_t,B_t$ 由调度器给定,与谁是 student/teacher 无关。

teacher 用完全相同\(A_t,B_t,\bar\sigma_t\),只把 \(v_\theta^S\) 换成 \(v_\theta^T\):\(\mu^T = A_t x_t + B_t v_\theta^T dt\)。因为 student 和 teacher 在同一个 \(x_t\) 上评估,\(A_t x_t\) 这一项两边相同,会在作差时消掉。

(2) 同协方差两高斯的 KL。 当两个高斯协方差都是 \(\bar\sigma_t^2 I\) 时,KL 的 log-det 项与 trace 项抵消,只剩均值差:

\[\mathrm{KL}\big(\mathcal N(\mu^S,\bar\sigma_t^2 I)\,\Vert\,\mathcal N(\mu^T,\bar\sigma_t^2 I)\big)=\frac{\lVert\mu^S-\mu^T\rVert^2}{2\,\bar\sigma_t^2}.\]

—— 翻译:两个只差中心位置的同形钟形曲线,其 KL 等于中心距离的平方除以 $2\bar\sigma_t^2$——与 §2.2 demo 数值一致。

(3) 代入 → 带权 velocity L2。\(\mu^S-\mu^T = B_t\,dt\,(v_\theta^S - v_\theta^T)\) 代入:

\[\mathrm{KL}=\frac{B_t^2\,dt^2}{2\,\bar\sigma_t^2}\,\big\lVert v_\theta^S - v_\theta^T\big\rVert^2 = w(t)\,\big\lVert v_\theta^S - v_\theta^T\big\rVert^2,\quad w(t)=\frac{B_t^2\,dt^2}{2\,\bar\sigma_t^2}.\]

—— 翻译:单步 reverse-KL 精确等于学生/教师速度场之差的 L2,前面只挂一个时间权重 $w(t)$——稀疏的分布匹配变成了稠密的速度回归。

(4) ODE 极限。\(\bar\sigma_t\to 0\)(确定性反向 ODE),分母趋零会让 KL 发散;但实现上直接取未加权的纯 L2 作为 per-step 目标:

\[\text{per\_step\_kl}=\tfrac12\big\lVert v_\theta^S - v_\theta^T\big\rVert^2 = \tfrac12\lVert\Delta\rVert^2,\qquad \Delta=\mu^S-\mu^T.\]

—— 翻译:ODE 极限退化成纯 L2 transition matching:把随机性和 $1/\bar\sigma_t^2$ 权重一并丢掉,只留半个均值差平方——SDE 与 ODE 在同一个公式族里统一。

2.4 代码引用

DiffusionOPD/scripts/train_sd3_opd.py:L1176-L1191 — KL→L2 折叠:noise≤0 走 ODE 纯 L2(0.5·delta²),否则 SDE 的 delta²/(2σ²);delta = μ^S−μ^T

                        if float(config.sample.noise_level) <= 0.0:
                            per_step_kl = 0.5 * (delta ** 2)
                        else:

                            sigma_sq = (
                                std_dev_t.float() ** 2
                            ).clamp(min=1e-8).unsqueeze(1)
                            per_step_kl = (delta ** 2) / (2.0 * sigma_sq)

                        per_step_kl_per_teacher_scalar = per_step_kl.mean(
                            dim=tuple(range(2, per_step_kl.ndim))
                        )
                        per_step_kl_scalar = per_step_kl_per_teacher_scalar.sum(dim=1)

                        distill_loss = per_step_kl_scalar.mean()
                        loss = distill_loss

对照 §2.3:delta 就是学生与教师 SDE 均值之差 \(\mu^S-\mu^T\)noise_level <= 0 分支里 per_step_kl = 0.5 * (delta ** 2) 正是 §2.3(4) 的纯 L2(\(\tfrac12\lVert\Delta\rVert^2\),ODE 极限);否则分支 (delta ** 2) / (2.0 * sigma_sq) 正是 §2.3(2) 的同协方差 KL(\(\lVert\Delta\rVert^2/2\bar\sigma_t^2\),SDE)。一个 if 分支就把 ODE 和 SDE 统一进同一份 loss——这正是 DiffusionOPD(arXiv:2605.15055)标题 "A Unified Perspective" 的字面来源。这个 ODE↔SDE 折叠最早出现在 Flow-GRPO(2505.05470)的 ODE→SDE 改写中。

2.5 洞察

3. 三兄弟① Flow-OPD:留住 RL(PPO + task reward + MAR)

3.1 直觉

想象一个学生:老师每天批改他的作业(蒸馏),指着错处说"这步该往这边改"。如果学生照老师改作业,天花板就是老师本人——永远学不会老师不会的东西。Flow-OPD 的做法是:学生既照着老师改作业,自己去考一场真实的考试(GenEval 看构图对不对、OCR 看字写对没、PickScore/DeQA 看好不好看),考完拿真实分数回来调整自己。这样学生能反超老师。

具体怎么塞进 RL?§2 把"学生与老师的差异"折叠成了 \(\mathrm{KL}=\lVert\mu^S-\mu^T\rVert^2/2\bar\sigma_t^2\)——这是一个每一步都有的稠密信号,正好当 RL 的稠密 reward,补足真实任务 reward 只在末步出分的稀疏性。再外挂一个 MAR(美学锚),防止学生为了刷指标把画面练丑。三股力:稀疏真分 + 稠密蒸馏 + 美学锚,合成一次更新。参见图 3。

Flow-OPD 主结果:SD3.5-M 上 GenEval 63→92、OCR 59→94,同一模型四指标接近或超过专家 teacher
图 3 — Flow-OPD 主结果(SD3.5-M):GenEval 从 63 提升到 92、OCR 从 59 提升到 94;同一个模型在 GenEval/OCR/PickScore/DeQA 四项上同时接近或超过各自的专家 teacher。

3.2 最小 demo

# Toy:一条 8 步轨迹,task reward 只在末步出分(稀疏),kl 每步都有(稠密)
kl_scale = 0.1
mu_S = [0.0, 0.3, 0.5, 0.4, 0.6, 0.7, 0.8, 0.9]   # 学生每步均值
mu_T = [0.0, 0.2, 0.4, 0.4, 0.5, 0.6, 0.7, 0.8]   # teacher 每步均值
sigma = 0.5
final_task_reward = 1.0          # 真实考试只在末步给分(稀疏)

advantages = []
for t in range(8):
    # 稠密项:每步都有,-KL = 越像 teacher 越高(§2 的 KL→L2)
    kl_reward = -((mu_S[t] - mu_T[t]) ** 2) / (2 * sigma ** 2)
    task_adv = final_task_reward if t == 7 else 0.0   # 中间步是 0
    advantages.append(task_adv + kl_scale * kl_reward) # 稠密补稀疏
print(advantages)  # 中间步不再全是 0,每步都有梯度可用

可以看到:没有 kl_reward 时,前 7 步的优势全是 0,梯度只在末步;加上稠密的 kl_scale·kl_reward 后,每一步都有非零优势,把稀疏的真实分摊到整条轨迹上。

3.3 正式化

(1) 混合优势。 把 §2 的 KL→L2 取负号当稠密 reward,与真实任务优势相加:

\[A_t = A_\text{task} + \lambda_\text{kl}\cdot r^\text{kl}_t,\qquad r^\text{kl}_t=-\frac{\lVert\mu^S_t-\mu^T_t\rVert^2}{2\sigma_t^2}\]

—— 翻译:每步优势 = 稀疏的真实任务优势 + 权重 $\lambda_\text{kl}$ 乘以稠密的负 KL;取负号是因为我们要**最小化** KL,等价于**最大化** $-\mathrm{KL}$。

(2) PPO 截断目标。 用学生当前策略与采样时旧策略的比值做重要性加权,并截断:

\[\mathcal L=-\,\mathbb E\!\left[\min\!\big(\rho_t A_t,\ \mathrm{clip}(\rho_t,1-\epsilon,1+\epsilon)\,A_t\big)\right],\quad \rho_t=\exp(\log\pi_\theta-\log\pi_{\theta_\text{old}})\]

—— 翻译:取"未截断"与"截断到 $[1-\epsilon,1+\epsilon]$"两者的较小值,防止单步更新跨度过大;$\rho_t$ 是新旧策略对数概率之差的指数,即 on-policy 重要性比。

(3) MAR 美学锚。 再加一项指向 task-agnostic 美学 teacher 的 L2:

\[\mathcal L_\text{total}=\mathcal L+\beta\cdot\frac{\lVert\mu^S-\mu^\text{aes}\rVert^2}{2\sigma_t^2}\]

—— 翻译:在 PPO 损失上加一个把学生均值往美学 teacher 均值 $\mu^\text{aes}$ 拉的约束,系数 $\beta$;它与任务无关,全数据锚定,专门防止刷指标导致的美学退化。

3.4 代码引用

混合优势(\(A_t = A_\text{task} + \lambda_\text{kl} r^\text{kl}_t\),对应 §3.3(1)):prev_sample_mean_ref_lora 就是路由到的 teacher 均值,kl_reward 即 §2 的 KL→L2。

Flow-OPD/scripts/train_sd3_opd_mix.py:L1707-L1728 — mixed:稀疏 task_advantage + 稠密 kl_reward(KL→L2 当 PPO 优势)

if config.train.get("kl_reward_level") == "step_wise" and config.train.get("kl_scale", 0) != 0:
    if prev_sample_mean_ref_lora is not None:
        kl_reward = ((prev_sample_mean - prev_sample_mean_ref_lora) ** 2).mean(dim=(1, 2, 3), keepdim=True) / (2 * std_dev_t ** 2)
    else:
        kl_reward = torch.zeros_like(prev_sample_mean.mean(dim=(1, 2, 3), keepdim=True))
    kl_reward = kl_reward.squeeze(-1).squeeze(-1)

    if kl_norm == "per_sample":
        kl_reward = (kl_reward - kl_reward.mean()) / (kl_reward.std() + 1e-4)
    elif kl_norm == "per_timestep":
        kl_reward = (kl_reward - kl_reward.mean()) / (kl_reward.std() + 1e-4)
    elif kl_norm == "global":
        kl_reward = (kl_reward - kl_reward.mean()) / (kl_reward.std() + 1e-4)

    advantages = task_advantage + config.train.kl_scale * kl_reward
    advantages = torch.clamp(
        advantages,
        -config.train.adv_clip_max,
        config.train.adv_clip_max,
    )
else:
    advantages = task_advantage

PPO 截断 + MAR(对应 §3.3(2)(3)):ratio/clamp/maximum 是 PPO,prev_sample_mean_ref_base(来自 mar_transformer 的美学 teacher)进 kl_loss,loss = policy_loss + beta·kl_loss 即 MAR。

Flow-OPD/scripts/train_sd3_opd_mix.py:L1753-L1767 — PPO clipped surrogate + MAR(beta·KL-to-aesthetic-teacher)

else:
    ratio = torch.exp(log_prob - sample["log_probs"][:, j])
    unclipped_loss = -advantages * ratio
    clipped_loss = -advantages * torch.clamp(
        ratio,
        1.0 - config.train.clip_range,
        1.0 + config.train.clip_range,
    )
    policy_loss = torch.mean(torch.maximum(unclipped_loss, clipped_loss))
    if config.train.beta > 0:
        kl_loss = ((prev_sample_mean - prev_sample_mean_ref_base) ** 2).mean(dim=(1,2,3), keepdim=True) / (2 * std_dev_t ** 2)
        kl_loss = torch.mean(kl_loss)
        loss = policy_loss + config.train.beta * kl_loss
    else:
        loss = policy_loss

kl_reward = ((prev_sample_mean - prev_sample_mean_ref_lora)**2)/(2*std_dev_t**2) 一字不差就是 §2 的 KL→L2(只是这里 prev_sample_mean_ref_lora路由到的 teacher 均值);advantages = task_advantage + kl_scale*kl_reward 落实 §3.3(1);第二段的 ratio / clamp / maximum 落实 §3.3(2) 的 PPO;loss = policy_loss + beta*kl_loss(prev_sample_mean_ref_base 来自 mar_transformer)落实 §3.3(3) 的 MAR。

3.5 洞察

4. 三兄弟② DiffusionOPD:砍掉 RL(closed-form 直接 loss)

4.1 直觉

§3 的 Flow-OPD 把 KL→L2 的负值当 reward 喂给 PPO,让 student 通过采样动作、估计优势、裁剪 ratio 一步步逼近 teacher。但这里有个刺眼的浪费:奖励能直接算出来,就别再用采样去估计它。在 §2 我们已经看到,师生两支去噪在 ODE 分支下的 per-step KL 直接折叠成解析的 \(\lVert\mu^S-\mu^T\rVert^2/2\sigma^2\)——它对 student 参数 \(\theta\) 是处处可微的闭式表达。既然如此,DiffusionOPD 的论证锋利得近乎挑衅:KL 有 closed form,直接当 loss 回归就行,PPO 在它外面再套一层"采样动作 + 重要性比 + 优势估计",只是把一个已经确定的标量重新用蒙特卡洛打散,平白多引入一层方差。这正是近期"把 RL 拽回 supervised target"潮流里最干净的一刀:同样的期望梯度,为什么要用方差更大的估计器?见图 4-1——闭式直接回归的收敛曲线明显更陡更稳。

DiffusionOPD loss/noise 消融:closed-form 直接回归 vs PPO 风格策略梯度的收敛对比
图 4-1 — loss / noise 消融:closed-form 直接回归(ODE,noise=0)比 PPO 风格策略梯度收敛更陡更稳(约 5×),印证 §4.3 的方差分析。

4.2 最小 demo

下面用纯 numpy 在同一个标量目标上对照两种梯度估计器:pathwise(直接对解析量求导)与 score-function / REINFORCE(采样估计)。两者期望相同,但后者方差大一个量级。

import numpy as np
rng = np.random.default_rng(0)
mu, sigma = 1.5, 0.7          # 高斯策略 a ~ N(mu, sigma^2)
target = 2.0                  # 目标:最小化 0.5*(a-target)^2 的期望
# 对 E_a[0.5*(a-target)^2] 关于 mu 求导,真值 = mu - target = -0.5
N = 20000
eps = rng.standard_normal(N)
a = mu + sigma * eps          # 重参数化采样
# pathwise:闭式量 0.5*(a-target)^2 直接对 mu 求导(da/dmu = 1)
g_pathwise = (a - target)
# score-function:把 0.5*(a-target)^2 当不可导的 reward,乘 dlogpi/dmu
g_score = 0.5 * (a - target) ** 2 * (eps / sigma)
print(f"pathwise  mean={g_pathwise.mean():+.4f}  std={g_pathwise.std():.4f}")
print(f"score-fn  mean={g_score.mean():+.4f}  std={g_score.std():.4f}")
# pathwise  mean=-0.4980  std=0.7001
# score-fn  mean=-0.4960  std=1.6300  <- 同期望,方差 ~2.3x

两条 mean 都收敛到真值 \(-0.5\),但 score-fnstdpathwise 的两倍多——score-function 项把采样噪声原封不动注入了梯度。

4.3 正式化

把 teacher 当 process reward,负 per-step KL 当优势 \(\Delta_j=-D_\text{KL}\big(\pi^S_j\,\Vert\,\pi^T_j\big)\)。Flow-OPD 风格的策略梯度对单步目标 \(\rho_j\Delta_j\)(其中 \(\rho_j\) 是重要性比,初始为 1)求导,可拆成两项:

\[\nabla_\theta(\rho_j\Delta_j)=\underbrace{\nabla_\theta\Delta_j}_{\text{pathwise}}+\underbrace{\Delta_j\,\nabla_\theta\log\pi_\theta(a_j)}_{\text{score-function}}\]

—— 翻译:总梯度 = "优势本身随参数变化"的 pathwise 项 + "优势加权对数似然梯度"的 score-function 项。

关键观察:优势 \(\Delta_j\) 是 student/teacher 两支均值的解析函数,不依赖采样出的动作 \(a_j\);而对任意策略 \(\mathbb E_{a_j\sim\pi_\theta}[\nabla_\theta\log\pi_\theta(a_j)]=0\)。于是

\[\mathbb E\big[\Delta_j\,\nabla_\theta\log\pi_\theta(a_j)\big]=\Delta_j\,\mathbb E\big[\nabla_\theta\log\pi_\theta(a_j)\big]=0\]

—— 翻译:score-function 项的期望严格为 0,因为 $\Delta_j$ 可提到期望外、剩下的 score 期望本身就是 0。

因此两个估计器的期望梯度完全相等:

\[\mathbb E\big[\nabla_\theta\mathcal L_\text{PG}\big]=\nabla_\theta\Delta_j=\nabla_\theta\mathcal L_\text{OPD}\]

—— 翻译:PPO 风格策略梯度的期望,恰好等于直接对闭式 KL 求导得到的 OPD 梯度;两者在期望意义上是同一个目标。

但 score-function 项虽期望为 0,方差不为 0。对高斯策略 \(a_j=\mu^S+\bar\sigma_j\varepsilon_j\),有

\[\nabla_\theta\log\pi_\theta(a_j)=\frac{a_j-\mu^S}{\bar\sigma_j^2}\,\nabla_\theta\mu^S=\frac{\varepsilon_j}{\bar\sigma_j}\,\nabla_\theta\mu^S\]

—— 翻译:对数似然梯度正比于采样噪声 $\varepsilon_j$,它把每一步的随机噪声直接耦合进了梯度。

这一项 \(\Delta_j\cdot(\varepsilon_j/\bar\sigma_j)\nabla_\theta\mu^S\) 随机摇摆、期望相消,对优化没有任何贡献,纯粹是方差。结论顺理成章:既然 \(\Delta_j\) 闭式可微,就直接用 §2 的 per_step_kl 当 loss 回归,把这层采样噪声整个砍掉。

4.4 代码引用

第一段是 teacher 选择逻辑:round-robin 轮流。i % len(teachers) 每个 step 选一个 teacher,切到它对应的 LoRA adapter——3 个 teacher 共享一份 backbone,只换 adapter,不占 3 份显存。

DiffusionOPD/scripts/train_sd3_opd.py:L875-L881 — round-robin:i%M 选 teacher,切对应 LoRA adapter

            teacher_idx = i % len(teachers)
            slot_adapters = slot_adapter_names[teacher_idx]
            slot_guidance_scales = slot_adapter_guidance_scales[teacher_idx]
            train_samplers[teacher_idx].set_epoch(
                epoch * config.sample.num_batches_per_epoch + i
            )
            prompts, prompt_metadata = next(train_iters[teacher_idx])

第二段是核心:loss = distill_loss,而 distill_loss 就是 §2 那个 per_step_kl 求和后 .mean()。注意这里没有任何 ratio、没有任何 clip——对照 §3 Flow-OPD 把 KL 当 reward 推进 PPO 的整套机制,这里只是纯粹的 closed-form 直接回归。

DiffusionOPD/scripts/train_sd3_opd.py:L1188-L1197 — closed-form 直接 loss:loss = distill_loss,无 PPO ratio/clip

                        per_step_kl_scalar = per_step_kl_per_teacher_scalar.sum(dim=1)

                        distill_loss = per_step_kl_scalar.mean()
                        loss = distill_loss

                        info["policy_loss"].append(distill_loss)
                        info["train_reverse_kl"].append(per_step_kl_scalar.mean())
                        info["loss"].append(loss)

                        accelerator.backward(loss)

teacher_idx = i % len(teachers) 就是 §4.5 说的 round-robin;loss = distill_loss = per_step_kl.mean() 直接对 §2 的闭式 KL 回归——没有 §3 那套 ratio/clip/采样优势,正是 §4.3 的结论"砍掉 score-function 那层纯方差"。

DiffusionOPD round-robin 多 teacher 蒸馏算法
图 4-2:DiffusionOPD 的 round-robin 多 teacher 蒸馏流程。每个 step 用 i % M 轮流选一个 teacher,切到对应 LoRA adapter 算 per-step KL,直接作为 loss 回归——无采样动作、无 ratio/clip。

4.5 洞察

5. 三兄弟③ D-OPSD:teacher 是 EMA 的自己(reward-free 自蒸馏)

5.1 直觉

前两兄弟都要一个"外部裁判":Flow-OPD(§3)要 reward。D-OPSD 把裁判也省了——它让模型自己当自己的老师,只不过当老师时多偷看了一眼标准答案。

具体说,同一个模型分饰两角:student 分支只看文本条件 \(y\),teacher 分支看文本 + 目标图像的多模态条件 \((y, x_0)\),因此 teacher"知道更多";而 teacher 的权重是 student 的 EMA(指数滑动平均)副本,是 student 慢一拍、更平滑的自己。训练在 student 自己跑出来的少步采样轨迹(8 步 / 4 步)上进行,teacher 在同一状态上给出目标,student 去对齐——全程不需要任何 reward

目的很纯粹:让一个已经 step-distilled 的少步模型(Z-Image-Turbo 8 步、FLUX.2-klein 4 步)"边用边学"新概念,又不掉少步能力。架构见图 5-1,而它赖以成立的物理前提见图 5-2。

D-OPSD 架构:student 看文本条件、teacher 看文本+目标图像的多模态条件,共享 VLM encoder;teacher 权重是 student 的 EMA 副本
图 5-1 — D-OPSD 架构:student 分支看文本条件、teacher 分支看文本+目标图像的多模态条件,二者共享同一 VLM encoder;teacher 权重是 student 的 EMA 副本,在 student 自采的少步轨迹上做自蒸馏。

5.2 最小 demo

# Toy:EMA self-teacher + 强条件 teacher / 弱条件 student
# 同一个 model,只是喂的条件不同;teacher 用 EMA 权重 + 多偷看 x0

def x0_from_v(x_t, t, v):                 # x0 = x_t + (1 - t) * v
    return x_t + (1 - t) * v

def dopsd_step(model, ema_model, x_t, t, y, x0_target):
    c_student = encode(y)                 # 弱条件:只看文本
    c_teacher = encode(y, x0_target)      # 强条件:文本 + 目标图(多偷看一眼)

    v_s = model(x_t, t, c_student)        # student:当前权重 theta
    v_t = ema_model(x_t, t, c_teacher)    # teacher:EMA 权重 theta_bar,同一个 x_t

    x0_s = x0_from_v(x_t, t, v_s)
    x0_t = x0_from_v(x_t, t, v_t)
    loss = mse(x0_s, x0_t.detach())       # 只回传到 student
    return loss

def ema_update(ema_model, model, decay=0.9999):   # teacher = 慢一拍的自己
    for pe, p in zip(ema_model.params(), model.params()):
        pe.data = decay * pe.data + (1 - decay) * p.data

注意两点:teacher 评估的 \(x_t\) 与 student 完全是同一个状态(on-policy 的关键),且 .detach() 切断了流向 teacher 的梯度。

5.3 正式化

(1) 强弱条件。 student 与 teacher 共享同一个 VLM encoder,只是输入不同:

\[c_s = f_\text{text}(y), \qquad c_t = f_\text{mm}(y, x_0)\]

—— 翻译:student 的条件 $c_s$ 只编码文本 $y$;teacher 的条件 $c_t$ 把文本 $y$ 和目标图 $x_0$ 一起喂进多模态编码器,因此携带更多信息。

(2) teacher 在 student 状态上预测。 设 student 自采轨迹上第 \(k\) 步的状态为 \(x_{t_k}^s\),teacher 用 EMA 权重 \(\bar\theta\)同一状态上给出 velocity:

\[u_k^t = v_{\bar\theta}(x_{t_k}^s,\, t_k,\, c_t)\]

—— 翻译:teacher 不另起炉灶采样,而是站在 student 当前所处的 $x_{t_k}^s$ 上预测,保证师生评估的是同一个点(on-policy)。

(3) 损失。 论文写 velocity 匹配,实现里用 \(x_0\) 的 MSE。由 \(x_0 = x_t + (1-t)\,v\):

\[\mathcal{L} = \big\lVert x_0^S - \mathrm{sg}(x_0^T) \big\rVert^2,\qquad x_0^{\{S,T\}} = x_{t_k}^s + (1-t_k)\,v_{\{ \theta,\bar\theta\}}\]

—— 翻译:student 算出的 $x_0^S$ 去对齐 teacher 的 $x_0^T$($\mathrm{sg}$ 是 stop-gradient,梯度只走 student);两者只差一个 $(1-t_k)$ 因子,故等价于**时间加权的 velocity MSE**(早期小 $t$ 权重大)。

(4) EMA 更新。 teacher 权重每步向 student 平滑靠拢:

\[\bar\theta \leftarrow 0.9999\,\bar\theta + 0.0001\,\theta\]

—— 翻译:衰减率 $0.9999$ 意味着 teacher 是 student 极慢的滑动平均,提供一个稳定、不抖的自蒸馏目标。

5.4 代码引用

D-OPSD/z-image-turbo_self-distill-vlm/train_dopsd.py:L588-L602 — teacher/student 在同一 student latent 上算 x0,loss = mse(x0_student, x0_teacher.detach())

                        v_pred_student = torch.stack(v_pred_student, dim=0).squeeze(2)

                    latents_student_cur = latents_student
                    x_0_student = latents_student_cur + (1 - t.reshape(bsz, 1, 1, 1)) * v_pred_student
                    latents_student = latents_student_cur + v_pred_student * dt.reshape(bsz, 1, 1, 1)
                    
                    #  we use x_0 loss  here, which is different as shown in our paper, It can be regarded as a weighted sum of v loss and t (with a greater weight in the early steps). We found that this leads to faster convergence.
                    # legacy:
                    # loss_dopsd = F.mse_loss(
                    #     v_pred_student,v_pred_teacher.detach(), reduction="mean"
                    # )
                    
                    loss_dopsd =  F.mse_loss(
                        x_0_student, x_0_teacher.detach(), reduction="mean"
                    )

三处对照 §5.3:(i) 在它上游,latents_teacher_cur = latents_student——teacher 评估的就是 student 的状态,这是 on-policy 的关键;(ii) x_0 = latent + (1 - t) * v 正是 §5.3(3) 的 \(x_0 = x_t + (1-t)v\);(iii) loss_dopsd = F.mse_loss(x_0_student, x_0_teacher.detach()).detach() 让梯度只走 student。代码注释也直白承认:用 \(x_0\) loss 等价于"v loss 的时间加权和(早 t 权重更大)",收敛更快。另外,训练循环每步都有 latents_student = latents_student.detach().requires_grad_(True)(train_dopsd.py:L554),这是跨步 detach——把少步链一段段切开,避免对整条 8 步 / 4 步 solver 做 BPTT 而爆显存。

5.5 洞察

emergent in-context 实证:prompt 与目标图一起编码,VLM-条件的 T2I 模型无需额外训练即可生成保概念变体
图 5-2 — emergent in-context 实证:把 prompt 与目标图一起编码,VLM-条件的 T2I 模型无需额外训练即可生成"保概念变体",这是 D-OPSD teacher 强条件分支能成立的物理前提。

6. 邻居:分布匹配蒸馏 DMD / SiD —— 同核心,为提速

6.1 直觉

§1–§5 的 OPD 三兄弟,像"老师拿红笔逐句对照改你的作文":在 student 自己采的样本上,§2 的 velocity 匹配给出 per-state 的逐点目标。DMD/SiD 走的是另一条路——它不在乎你某一句写得对不对,只要求你写出来的整批文章的"风格分布"和老师的一致

DMD(Distribution Matching Distillation)= 分布匹配蒸馏:把一个多步扩散 teacher 蒸成一步生成器,让 student 自己生成样本的分布去匹配 teacher 的分布,全程 reward-free。它和 OPD 是近亲——同样"在 student 样本上给 per-state 目标"——区别只在两点:目标写成 score(分数函数)之差而非 velocity;目的是提速(蒸成一步)而非对齐。下图(图 6)是这套机制的骨架。

6.2 最小 demo

import numpy as np
# 两个一维高斯的解析 score: s(x) = grad_x log p = -(x-mu)/sig^2
def score(x, mu, sig): return -(x - mu) / sig**2
mu_real, sig_real = 2.0, 1.0      # teacher 分布
mu_fake, sig_fake = 0.0, 1.0      # student 当前分布(自采样)
x = np.random.randn(8) * sig_fake + mu_fake   # student 一步生成的样本
for step in range(200):
    s_real = score(x, mu_real, sig_real)      # real teacher score
    s_fake = score(x, mu_fake, sig_fake)      # fake student score
    grad = s_real - s_fake                    # VSD 骨架:两 score 之差
    x = x + 0.05 * grad                       # 把样本往 real 高、fake 低推
print("mean drifts 0 -> ", round(x.mean(), 3))  # ≈ 2.0,向 teacher 收敛

—— 没有任何 reward,仅靠"real 分数减 fake 分数"这一项梯度,student 样本就从 μ=0 漂到 teacher 的 μ=2。这正是 DMD 的核心。

6.3 正式化

(1) DMD 目标 = 分布匹配,梯度 = 两 score 之差。 设 student(参数 \(\theta\))一步生成样本 \(x=G_\theta(z)\),其分布为 \(p_\text{fake}\),teacher 分布为 \(p_\text{real}\)。DMD 最小化二者(在各噪声尺度上近似的)KL 散度,其对 \(\theta\) 的梯度可写成

\[\nabla_\theta \,\mathrm{KL}\!\left(p_\text{fake}\,\|\,p_\text{real}\right)\;\approx\;\mathbb E_{x}\Big[\,s_\text{fake}(x)-s_\text{real}(x)\,\Big]\,\nabla_\theta x,\qquad s(x)=\nabla_x\log p(x).\]

—— 翻译:把 student 分布拉向 teacher 分布,梯度恰是"fake 分数减 real 分数"乘上样本对 $\theta$ 的雅可比;$s$ 是 score(对数密度的梯度),由两个去噪网络分别估计。

(2) DMD ≈ VSD + GAN loss。 上式的 \(s_\text{fake}-s_\text{real}\) 即 Variational Score Distillation(VSD)的核心项;DMD2 进一步去掉原版的回归正则,改为再加一个判别真假样本的对抗项:

\[\mathcal L_\text{DMD2}\;=\;\underbrace{\mathbb E\big[s_\text{fake}(x)-s_\text{real}(x)\big]}_{\text{VSD 分布匹配}}\;+\;\lambda\,\underbrace{\mathcal L_\text{GAN}(x)}_{\text{判别 real/fake}}.\]

—— 翻译:DMD2 = VSD 分布匹配项 + 一个 GAN 判别器项;后者鼓励 student 样本在判别器看来"以假乱真",可摆脱对真实数据回归的依赖。

(3) SiD:score identity,data-free,可反超 teacher。 SiD 用一条 score 恒等式直接估梯度,无需任何真实数据,且实测可反超 teacher。它与 §2 velocity 匹配是同一类目标:扩散里 score 与预测的 \(x_0\)、velocity 互为仿射,

\[s(x_t)\;=\;\nabla_{x_t}\log p_t(x_t)\;\propto\;\frac{\hat x_0(x_t)-x_t}{\sigma_t^{2}}\;\propto\;v_\theta(x_t).\]

—— 翻译:score、预测的干净图 $\hat x_0$、velocity 三者只差仿射变换;所以"score 之差"和 §2 的"velocity 之差"是同一类逐点目标,只是换了坐标。

6.4 代码引用

DMD2/main/sd_guidance.py:L235-L241 — 分布匹配:grad = (p_real − p_fake)/‖p_real‖,loss = mse(x, (x−grad).detach())

            p_real = (latents - pred_real_image)
            p_fake = (latents - pred_fake_image)

            grad = (p_real - p_fake) / torch.abs(p_real).mean(dim=[1, 2, 3], keepdim=True) 
            grad = torch.nan_to_num(grad)

        loss = 0.5 * F.mse_loss(original_latents.float(), (original_latents-grad).detach().float(), reduction="mean")

p_real = latents - pred_real_imagep_fake = latents - pred_fake_image 是两个 score 方向——分别来自 real teacher 与 fake student 的去噪预测(latents − x̂₀ 正比于该分布的 score)。grad = (p_real - p_fake)/… 就是 §6.3(1) 的 score 之差(分母只是把它归一化稳定量纲)。最后 loss = 0.5*mse(x, (x-grad).detach()) 把这个梯度包装成可反传的回归 loss:目标 (x-grad)detach,只让 student 朝 grad 方向动一步——和 §5 的 mse(x0, x0.detach()) 形式上同构,都是"detach 掉目标、把一阶方向伪装成 MSE"。

6.5 洞察

z 一步生成器 G_θ x student 样本 real teacher score ↑ fake student score ↓ grad = real − fake teacher 分布
图 6 — 分布匹配蒸馏:student 一步生成的样本,被"real teacher score 减 fake student score"的梯度推向 teacher 分布(reward-free,为提速)。

7. 统一视角:一个模板,四个选择轴

7.1 直觉

前面五节像五个独立方法:§3 的 Flow-OPD 是带 task reward 的 PPO,§4 的 DiffusionOPD 是 closed-form 直接 loss,§5 的 D-OPSD 是 reward-free 的 EMA 自蒸馏,§6 的 DMD/SiD 是一步生成的分布匹配。但把它们并排看,会发现这是同一个模板的五种填法。RL-for-diffusion、知识蒸馏、分布匹配这三条看似不同的线,其实都在做同一件事:在 student 自己采出的轨迹 \(x_t\sim\pi_\theta\) 上,用一个 per-state 的 teacher 目标(velocity / score / 分布)去匹配。区别只在四个旋钮怎么拧——teacher 从哪来、要不要 reward、用 PPO 还是直接 loss、多教师怎么编排。下面那棵自画的演进树(图 7)就是把 LLM 的 GKD、扩散 RL 谱系、OPD 三兄弟和 DMD/SiD 旁支全部收敛到这一句话的可视化。

7.2 最小 demo

def match_teacher_target_on_student_trajectory(
        student, teacher, get_target, use_reward=False, use_ppo=False):
    # 轴③ 公共骨架:always 在 student 自采轨迹上算 per-state teacher 目标
    x_t, t, logp_old = student.rollout()          # on-policy 采样
    g_s = student.per_state_quantity(x_t, t)       # velocity / mean / score
    g_T = get_target(teacher, x_t, t).detach()     # 轴① stop-gradient teacher 目标
    loss = l2(g_s, g_T)                            # 轴③ 直接 loss(低方差)

    if use_ppo:                                    # Flow-OPD:PPO ratio + clip
        ratio = (student.logp(x_t, t) - logp_old).exp()
        loss = ppo_clip(ratio, advantage=reward(x_t))
    if use_reward:                                 # 轴② 对齐项(task reward)
        loss = loss + lam * (-reward(x_t))
    return loss

# 四种特化 = 四组旋钮取值:
# Flow-OPD     : get_target=multi_teacher, use_reward=True,  use_ppo=True
# DiffusionOPD : get_target=multi_teacher, use_reward=False, use_ppo=False
# D-OPSD       : get_target=ema_self,      use_reward=False, use_ppo=False
# DMD/SiD      : get_target=real_fake_score,use_reward=False,use_ppo=False

四行注释就是四个方法的全部差异:换 get_target(轴①)、开关 use_reward(轴②)、开关 use_ppo(轴③),骨架一字不动。

7.3 正式化

把五节的损失抽象成一个统一模板:

\[\mathcal L=\mathbb E_{x_t\sim\pi_\theta}\big[\,w(t)\,D\big(g_\theta(x_t),\,\mathrm{sg}(g_T(x_t))\big)\,\big]\;(+\,\lambda\,\mathcal R_{\text{reward}})\]

—— 翻译:在 student 自己采样的状态 $x_t$ 上,用距离 $D$ 把 student 的 per-state 量 $g_\theta$ 拉向 stop-gradient 的 teacher 目标 $g_T$,$w(t)$ 是时间权重;可选地再加一个 reward 项。

其中三个占位符决定一切:\(g\) 是 per-state 量(velocity / mean / score),\(D\) 是距离(L2 或 KL,§2 已证两者在 on-policy 下折叠),\(g_T\) 是 teacher 目标。四个选择轴把模板特化成具体方法:

每个方法就是这四个轴上的一组取值:

方法 ① target ② reward ③ 优化 ④ 编排
Flow-OPD 外部多 teacher ✅ task reward PPO+clip routing/merge
DiffusionOPD 外部多 teacher closed-form L2 round-robin
D-OPSD EMA self 直接 loss 单教师(自己)
DMD/SiD real−fake score 直接 loss 双 score 网络
\[\mathcal L_{\text{FlowOPD}}\!=\!\underbrace{\mathrm{PPO}(r_{\text{task}})}_{\text{轴②③}},\quad \mathcal L_{\text{DiffOPD}}\!=\!\lVert v_\theta-\mathrm{sg}(v_T)\rVert^2,\quad \mathcal L_{\text{DMD}}\!=\!\lVert x-\mathrm{sg}(x-(p_r-p_f))\rVert^2\]

—— 翻译:同一个模板里,Flow-OPD 把 $D$ 换成 PPO 目标并接 reward;DiffusionOPD 是 velocity 的 L2;DMD 是把 score 之差 $p_r-p_f$ 包成一个 L2——三者结构同形,只是 $g$、$D$、reward 三个旋钮取值不同。

7.4 代码引用

两条"一行核心差异"并排,印证统一模板。先看 DiffusionOPD 的 closed-form 均值/velocity 匹配:

DiffusionOPD/scripts/train_sd3_opd.py:L1188-L1197 — 速度/均值匹配:loss = distill_loss(closed-form L2)

                        per_step_kl_scalar = per_step_kl_per_teacher_scalar.sum(dim=1)

                        distill_loss = per_step_kl_scalar.mean()
                        loss = distill_loss

                        info["policy_loss"].append(distill_loss)
                        info["train_reverse_kl"].append(per_step_kl_scalar.mean())
                        info["loss"].append(loss)

                        accelerator.backward(loss)

再看 DMD2 的分布匹配:

DMD2/main/sd_guidance.py:L235-L241 — 分布匹配:grad = (p_real − p_fake),loss = mse(x,(x−grad).detach())

            p_real = (latents - pred_real_image)
            p_fake = (latents - pred_fake_image)

            grad = (p_real - p_fake) / torch.abs(p_real).mean(dim=[1, 2, 3], keepdim=True) 
            grad = torch.nan_to_num(grad)

        loss = 0.5 * F.mse_loss(original_latents.float(), (original_latents-grad).detach().float(), reduction="mean")

两段代码结构惊人一致:都是"student 样本上、teacher 目标、detach、L2 包装"。DiffusionOPD 的 teacher 目标是各教师的均值/velocity(per_step_kl 在 on-policy 下退化为 L2);DMD2 的 teacher 目标是 real 与 fake 两个 score 之差 (p_real - p_fake),通过 (original_latents-grad).detach() 构造出"想要 student 落到的点",再用 mse_loss 包成 L2。一个匹配均值、一个匹配 score 之差,但 §7.3 的统一模板 \(D(g_\theta,\mathrm{sg}(g_T))\) 一字不变。

7.5 洞察

GKD (LLM OPD, 2023) Flow-GRPO (2025) DiffusionNFT (2025) Flow-OPD PPO+reward,反超 DiffusionOPD closed-form,整合 D-OPSD EMA self,reward-free DMD / SiD(旁支) 分布匹配,一步生成 统一模板 student 轨迹 × teacher per-state 目标
图 7 — 演进树:LLM 的 GKD → 扩散 RL(Flow-GRPO/DiffusionNFT)→ OPD 三兄弟 + DMD/SiD 旁支,全部收敛到"在 student 轨迹上匹配 teacher 的 per-state 目标"。

参考文献与代码仓库

arxiv ID 均经 2026-06 网络核验。标 [本站] 者为本博客已有精读。

OPD 的本源(LLM)

扩散 / flow RL 谱系(三兄弟的上游)

★ OPD 三兄弟(2026-05,本站均已精读)

近亲:分布匹配蒸馏(为一步/少步提速)

代码仓库(本教程 Step-4 引用,均 diff 验证)

本仓相关

讨论 / Comments

评论托管在本仓库的 GitHub Discussions, 需 GitHub 账号。