ICML-2027 / Step-Reward-Paper-Reference.md
Codex Restore
Restore previous repo contents and add deep-dive document
48c208c
|
Raw
History Blame Contribute Delete
54.2 kB

Step-Level Reward for Embodied Trajectory Planning: GIGPO + OPSD 的融合设计与实测分析

用途:论文写作参考文档。完整记录「在已有整条轨迹 reward 的基础上,借鉴 GIGPO(双层优势)+ OPSD(偷看 GT 的代理信号)给出 step-level reward」这条线的:动机、方法推导、两种实现变体、token 对齐工程、以及上机实测踩出来的全部坑与结论

与同目录 Step-Level-Reward-Design.md(早期设计笔记)的区别:本文档额外包含了实测阶段(forward_proxy 失败、KL 塌平、KL 无法判别对错、自蒸馏坍缩、GPU/driver 工程问题)的完整分析——这些是论文 Analysis / Ablation / Discussion 章节的核心素材。

代码落点:verl/trainer/{step_reward.py, teacher_proxy.py, step_viz.py, core_algos.py, ray_trainer.py, config.py}verl/workers/reward/{function.py, verify.py}


0. 摘要式总览(一段话)

任务是离散动作规划(embodied planning):模型在一张图 + 指令下,一次性生成完整 executable_plan(一串 {action_name, action_id})。我们已有整条轨迹的标量 reward(verify 模型判分,∈[−1,1]),用标准 GRPO 优化。本工作的目标是在轨迹级信号之外,加入逐 step 的细粒度信用分配

A_token = A^E  +  ω · A^S_k
  • A^E(episode 级)= 现有 GRPO:整轨标量 reward 在同 prompt 的 N 个 rollout 内组归一化,广播到所有 token。不变
  • A^S(step 级,新增)= 偷看 GT、逐 step 打分,居中后只填到该 step 对应的 token 段。
  • ω = step 权重(默认 1.0)。

方法本质 = 集 GRPO + GIGPO + OPSD 三家之长(详见 §1.5):以 GRPO 的组内归一化轨迹优势为 A^E(免 value/critic),套用 GIGPO 的「episode + step 双层优势 A^E + ω·A^S」框架,但把 GIGPO 依赖环境的 step 信号替换成 OPSD 式「偷看 GT」的离线 step 信号,并以 reward shaping(叠加在 advantage 上、而非 KL loss) 的方式融入——得到一个无 value、无 critic、无环境、单次整轨即可的 step-level RL 方案。

最终的实测结论(详见 §7)是一个对论文很有价值的对比OPSD 式 KL 代理信号(forward_proxy)原本塌平,换未微调 teacher(Qwen2.5-VL-3B-Instruct)+ 挪到有 GPU 的 reward actor 后信号复活(std 1–2,§7.4)——但 KL「分布相似 ≠ 动作正确」的固有问题仍未实测排除(§7.3);而规则匹配(action_id 比对)是最稳的 step reward,作主方法,forward_proxy 作软信号变体 / ablation 对照。⚠️ 注意「换 teacher 是信号复活的关键」是新近实测才确认的(§7.4),论文别把功劳全记给方法本身,也别过度声称 forward_proxy 已 work——它现在「活」但正确性待验证。


1. 问题设定与现状(已核实的事实)

维度 现状
数据布局 一行 = 一整条轨迹的 response(单次生成,非多轮 env rollout)
生成内容 完整 JSON:visual / reasoning / language_plan / executable_plan,其中 executable_plan[{action_name, action_id}, ...]
reward 标量,写在最后一个 token:reward_tensor[i, L-1] = score["overall"] ∈[−1,1](verify 模型判分)
GRPO scores = token_level_rewards.sum(-1) → 按 index = uid(同 prompt 的 N rollout)组归一化 (R−mean)/std → 广播到整行所有 token
GT non_tensor_batch["gt_action_close"]trajectory_actions = [{action_id, action_name}, ...];dataset 另把 GT 重打包进 ground_truthdataset.py:316/567
优势估计器 注册表式分发,compute_advantage 在 driver 上跑(ray_trainer.py

核心约束:reward / advantage 最终都是 (bs, response_length)token 级张量,优化的是每个 token 的 logprob。所以「第 k 步该得多少分」必须落实到「第 k 步对应 response 里哪几个 token」。


1.5 方法定位:集 GRPO + GIGPO + OPSD 三家之长

本工作不是某一家的改良,而是把三种方法各自的精华抽出来拼成一个适配「单次整轨预测 + 离散正确性」的方案。先用一张表说清「我们从每家拿了什么、丢了什么」:

来源 它的核心机制 我们取(精华) 我们丢(不适配处)
GRPO 同 prompt 的 N 个 rollout 组内归一化轨迹标量 reward (R−mean)/std 当 advantage,免 critic/value 整套 episode 级优势 A^E(我们已有的 verify 轨迹 reward 正是走这条),作为骨架直接当 A^E 只有轨迹级一个标量、广播到所有 token → 无法区分轨迹内部哪步对哪步错(信用分配太粗)
GIGPO episode + step 双层优势 A = A^E + ω·A^S,step 级靠「相同环境状态 anchor_obs 分组、组内比 return」造优势,免 value **双层优势的框架 A^E + ω·A^S**:在轨迹优势之外再叠一层 step 优势,这正是我们要的细粒度信用分配 它的 step 信号依赖多轮环境交互 + 反复路过同一状态;我们是单次整轨、无环境,借不了 anchor-state 分组与折扣回报
OPSD teacher 偷看 GT 做 forward,用 teacher−student 的 token logits 差(KL)当稠密监督信号 「偷看 GT 拿 step 级代理信号」的思想,以及同模型两次 forward 的实现机制(forward_proxy 变体) 它把 KL 当直接 loss、摊到每个 token;我们改成 step 级、折算成 reward 叠加(而非 loss),并躲开格式 token(§2、§5.5)

一句话定位

以 GRPO 的组内归一化轨迹优势为 A^E(免 value),套用 GIGPO 的「episode + step 双层优势」框架,但把 GIGPO 依赖环境的 step 信号替换为 OPSD 式「偷看 GT」的离线 step 信号——再把这个 step 信号以 reward shaping(叠加在 advantage 上、而非 KL loss)的方式融入,得到一个无 value、无 critic、无环境、单次整轨即可的 step-level RL 方案。

对应到公式,三家正好各占一块:

A_token =   A^E            +   ω ·   A^S
          └─ GRPO ─┘          └ GIGPO 框架 + OPSD/规则 信号 ┘
       组内归一化轨迹优势        在轨迹优势上叠加的 step 级优势
       (免 value, 已有)         (GIGPO 的双层结构, 信号来自偷看 GT)

论文用法:这一节可直接作为 Method 开篇的「我们的方法 = best of three」定位段;§4、§5.5、§10 是它的展开与论证。


1.6 核心 idea 与动机链条(论文 Introduction 主线)

这一节是论文 Introduction 的主线叙事——一条「现有工具各有短板 → 受 OPSD 启发提出核心 idea → 实测发现并解决关键障碍」的逻辑链。前面 §1.5 讲「方法是什么(三家之长)」,这一节讲「为什么会想到这么做」。

出发点:我们想要 step 级的细粒度信用分配(轨迹级 reward 太粗,分不清同一条轨迹里哪步对哪步错)。但现成的两个工具各有短板:

  1. GIGPO 没法很好地判断每一步的对错。GIGPO 的 step 优势来自「相同环境状态下、不同动作比 return」,它依赖多轮环境交互、反复路过同一状态;本任务是单次整轨预测、无环境,根本没有「同状态重复」可比,借不到它的 step 判别能力
  2. 严格的规则匹配(action_id 逐位比对)虽然干净,但可能带来困惑。它是硬 0/1:一步语义对、但 id/位置对不齐就被判错;只有「对/错」没有「有多对」;对顺序/措辞敏感、脆。对「一串离散决策」它能给骨架,但缺乏对「接近正确」的容忍与分级

核心 idea(本工作的 novelty):既然两条都不够,受 OPSD 的 KL 信号启发——用 teacher 与 student 的 logits 对比所产生的「波动」,去对 reward 做 shaping(引导/加权),看能否影响性能。具体做法:

OPSD 式 logits 对比(teacher 偷看 GT vs student 没看)
   → 逐 token 的 logp 差(波动)
   → 按 step 区间聚合 + squash 到 [−1,1]
   → 作为 step 级的软信号,对已有轨迹 reward 做 shaping:A = A^E + ω·A^S

这就是核心 idea:不把 KL 当 loss、也不靠硬规则,而是把 OPSD logits 对比的波动当成一个软的 step reward,叠加(shape)在轨迹 reward 上——既比 GIGPO 更能定位到「这一步」,又比规则更软、能给分级/部分信用。

实测发现的关键障碍 + 解法(这是故事的转折,也是论文该讲的 insight)

  • 障碍:一开始 teacher 用的就是 student 自己(policy)。但 student 经过 SFT + RL,对自己生成的轨迹太自信了——它对自己采样出来的每个 token 概率都很高,加不加 GT 几乎不变 → logits 波动极小(塌平),shaping 信号近乎为 0。
  • 解法:把 teacher 换成通用模型Qwen2.5-VL-3B-Instruct,没在这些轨迹上微调过)。效果一下子就 work 了——实测 teacher−student logp diff 的 std 从 ≈0 升到 1–2(§7.4)。
  • 为什么换通用模型有效? 两个层面:
    1. 没过拟合到自己的轨迹 → 不像 policy 那样对自己 token 饱和自信,GT 一进上下文分布才会动(这是「信号能不能存在」的必要条件);
    2. 通用模型的推理 / 判断能力更好 → 当它读到 GT 参考计划时,能更好地理解任务、对「该这么走」的 token 给出有意义的信念变化 → 波动更可能朝正确方向(这是「信号有没有用」的关键)。

      ⚠️ 诚实标注:第 2 点是我们对「为什么 work」的解释/假设。目前已确认的是信号复活(波动出现,std 1–2);但「波动是否正确地奖对惩错」尚未实测排除——§7.3 里用通用文本模型(Qwen3-1.7B)做 demo 时,似是而非的错步仍拿到了正分。所以「通用模型判断更好 → 信号正确」是合理假设,但要在实际 VL Instruct teacher 上验证(看错步 A^S 是否真被压负),不能直接当结论。

论文用法:这一节就是 Introduction 的故事线——「GIGPO 判不准每步 + 规则太硬 → 用 OPSD logits 波动做 reward shaping(核心 idea)→ student 过自信致信号塌平 → 换通用模型(判断力更强)救活」。§2 是它的展开(为什么不用 KL-as-loss),§7 是实测证据。


2. 动机:为什么不直接用 OPSD(logits / KL 蒸馏)

我们先在 OPSD_V2_Embody 上做过 logits/KL 蒸馏(teacher 偷看 GT,对比 teacher 与 student 的 token logits 得 KL,用作监督)。可视化调试时发现 KL 对 embodied planning 不合适,这才转向 step reward。理由五条(前两条有实测热力图证据):

  1. KL 优化「分布像不像」≠「动作对不对」。一条流畅但走错(该去 Sink 却去 Table)的轨迹,KL 可以很低。
  2. 格式 token 抢梯度find / a / the / JSON 骨架 {"action_id": 占了绝大多数 token 却无决策信息,KL 把梯度浪费在上面(热力图实测:红色集中在 JSON 骨架,内容反而发白;clip 0.05 只是补丁)。
  3. 决策单位是 step,KL 却摊到每个 token。「去 Sink 还是 Table」是一个决策,不该被切成十几个 token 各自匹配。
  4. privileged-info leakage:teacher 偷看答案,存在把 GT 信息直接泄漏进梯度的风险。
  5. KL 信号难解读:红越多 ≠ 越好;大片黄/白究竟是收敛还是信号塌掉,分不清;正负、clip 都让解读困难。

论文用法:这一节是 Related Work / Motivation 里「为什么需要 step reward 而非 distillation」的直接论据。第 1、3 条尤其是 embodied 任务的本质论点。


3. logits(KL)vs step reward:两个范式的根本区别

logits / KL(OPSD) step reward(本工作)
信号密度 每 token 一个完整词表分布,极稠密 每 step 一个标量,稀疏粗糙
性质 监督式 teacher-forcing,低方差、稳、样本高效 RL,要采 N 条 + 组归一化,高方差
教什么 分布相似度(含置信度/备选项/calibration) 动作对/错
依赖 要 teacher(或自蒸馏),不需要 verifier/GT 对齐 要可靠的 GT 逐 step 匹配
适合 没有可靠 step verifier、想稠密 bootstrap 基础能力 有 GT、任务是「一串离散正确决策」

结论:两者互补、常常串联——KL/SFT 先把基础能力和格式喂出来(稠密、稳),再用 step reward RFT 做探索 + 细粒度纠错(直接对正确性)。embodied planning 有干净的 action_id GT、本质是离散正确决策,step reward 直接命中目标,而 §2 列的 KL 缺点在此任务上全被放大


4. GIGPO 怎么做 step reward,以及为什么不能照搬

参考实现:verl-agentgigpo/core_gigpo.py。它的 step reward 不是跟 GT 比对,而是「同状态下比 return」:

  1. 数据单位:每行 = 一个 step transition(一次 env turn)。
  2. 环境给即时奖励compute_step_discounted_returnscore_gigpo.py:87):按 traj_uid 分组做 γ-折扣回报 R_t = r_t + γ·R_{t+1}。所以 GIGPO 的「step reward」其实是该步往后的折扣回报,不是 0/1 对错。
  3. anchor-state 分组(GIGPO 的灵魂)build_step_group:243):在同 prompt 的 N 条轨迹里,把落到相同环境状态(anchor_obs 相同)的 step 聚成一组(Eq.6「same state, different action」)。
  4. 同状态内归一化step_norm_reward:334):组内 mean(-std) 归一化 → A^S,无需 value network
  5. 合并compute_gigpo_outcome_advantage:138):A = A^E + ω·A^S

为什么不能直接套到我们这里:

GIGPO 我们的设定
一行是什么 一个 step(独立 prompt+response) 一整条轨迹一个 response,step 是 response 内的 token 段
step reward 来源 环境即时奖励 → 折扣回报 GT 逐 step 匹配 / OPSD 代理
step 分组依据 相同环境状态 anchor_obs 相同 step 位置 k
要不要环境 要(多轮交互) 不要(离线、一次生成)
advantage 广播 标量 × 整行 response_mask 一条 response 内不同 token 段不同 advantage

GIGPO 的 anchor-state 分组依赖「多条轨迹反复路过同一状态」这个多轮交互事实,我们一次性预测整轨、没有环境,借不了。真正可复用的只有最后一层 A = A^E + ω·A^S 的「双层优势归一化 + 组合」骨架;reward 来源换成 GT 匹配 / OPSD 代理,分组换成 step 位置 k,并且要把标量广播改成分段广播

论文用法:这是 Method 里「我们与 GIGPO 的关系」一段——借其双层优势框架,但因无环境而重写信号来源与分组


5. 核心概念:step reward 怎么转成 advantage(A = reward − baseline)

这是整个设计最关键的概念点,也是最初卡住的地方。

一句话:A = reward − baseline,两种 reward 唯一的区别是 baseline 取什么。

  • 轨迹 reward 是裸标量(success=1、语义分=0.7)。单看一个数不知道好坏,必须拿同 prompt 的 N 个 rollout 的均值当 baseline → (R−mean)/std(GRPO 组归一化)。没有零点,所以必须组内比较。
  • step reward(GT 匹配)是绝对校准过的:这一步 == GT → 对,!= → 错。「对」该 push up,「错」该 push down,本身就有零点,不需要跟别人比。所以最简单的转法就是直接居中
match    →  +1
mismatch →  −1          # 这已经是合法的 step advantage

A^S_k = r_k − 0.5(r_k∈{0,1}),或 r_k − 该轨迹所有步的均值不需要 N 条对齐、不需要 std。

数值示例:GT = [find Ladle, pick Ladle, find Sink],预测 = [find Ladle ✓, pick Ladle ✓, find Table ✗]:

step tokens GT 匹配 A^S
1 "find a Ladle" +1
2 "pick up the Ladle" +1
3 "find a Table" −1

把 A^S 广播到各自 step 的 token 段,scaffolding token 给 0;A^E 是整轨标量组归一化后广播全 token。相加:即使整条 A^E 为正,第 3 步那段 token 也会被 −1 拉下来 → 实现「同一条轨迹里,对的步强化、错的步惩罚」。

可选升级(GIGPO 式组内归一化):若想进一步降方差,A^S 也可组内归一化,但 baseline 是「N 条轨迹在同一 step 位置 k」:

A^S_k^(i) = (r_k^(i) − mean_j r_k^(j)) / (std_j + eps)     # j 遍历 N 条同 prompt rollout

代价是要按 step 位置 k 对齐 N 条轨迹。简单起见可先不做,跑通再加。


5.5 关键设计选择:为什么是「KL 信号叠加在 reward 上」

这是整个方法的 design choice,也是论文 Method 必须先讲清楚的论证。我们面对的是一个三选一:同样想用「OPSD 偷看 GT 的 KL 信号」,可以有三种用法——

  • (A) 直接 KL loss(标准 OPSD / 知识蒸馏):L = L_PG + λ·KL(teacher‖student),把 KL 当一个独立的监督损失加在策略梯度旁边。
  • (B) 规则匹配当 step rewardaction_id 逐位比对 → 硬 ±1,作为 advantage。
  • (C) 把 KL 折算成 step reward / advantage 叠加本工作选的):A_token = A^E + ω·A^SA^S = squash(logp_teacher − logp_student) 逐 step。

下面分别说明 C 相对 A、相对 B 的动机。核心一句话:用户的本意是「用 KL 来微调已有的轨迹 reward」(原话,2026-05-31 18:22)——不是替换主目标,不是再开一个模仿损失,而是在已有 reward 之上做 step 级的细粒度信用再分配。这天然指向 C。

5.5.1 C 相对 A(KL-as-reward vs KL-as-loss):为什么不直接拿 KL 当 loss

(A) 直接 KL loss (C) KL 叠加在 reward 上
目标个数 两个:on-policy 策略梯度 + off-policy 监督蒸馏,性质/尺度不同,要靠 λ 小心平衡,易一个压过另一个 一个:只有策略梯度;KL 折进 advantage,ω 只调它在优势里的占比,不和主目标打架
对 policy 的作用 每个 token 上直接最小化与 teacher 的散度 = 强行模仿 teacher 分布 只给 policy 自己采样出来的 token 重新分配信用(哪步多加/扣分),policy 仍优化自己的轨迹
teacher 出错时 policy 被拉向 teacher(哪怕 teacher 也错),无探索 KL 只调「轨迹内部各步的相对权重」,探索结构不变
和轨迹 reward 的关系 独立损失,挂不到已有的 verify/成功 reward 上 就是在微调轨迹 reward:主 reward(verify 轨迹标量)回传不变,KL 只做步级再分配
稳定化 裸散度项,无 baseline/clip 走 PPO advantage 通道,自动享受 GRPO 组归一化 + clip
可消融 改 λ 改变整个目标构成,不模块化 ω=0 即退回纯 GRPO,是干净的 ablation 轴
格式 token KL 摊到每个 token,format token 抢梯度(§2.2) A^S 只填决策 token 段,scaffolding=0,主动规避

一句话:KL-as-loss 是「再开一个模仿目标和 RL 抢方向盘」;KL-as-reward 是「让 RL 还是那个 RL,只是把每步该拿多少优势调得更准」。后者保留探索、不被 teacher 带偏、挂在已有轨迹 reward 上、且模块化可关——完全契合「微调轨迹 reward」的本意。

5.5.2 C 相对 B(KL 软信号 vs 规则匹配):为什么想要软的、而非硬 ±1

这是动机(注意 §7 给出的实测结果与动机相反,见下方 caveat):

  1. 软、分级、给部分信用:rule 是硬 ±1(非对即错),KL 是连续值,能表达「这一步有多对 / 偏多少」,对「接近但不完全对」的步给中间分,梯度更平滑。
  2. 不需要精确的离散 GT 对齐:rule 要 action_id 严格相等;当 action 措辞对但 id 不同、或任务根本没有干净标准 id 时,rule 会误判。KL 只要 teacher 能 condition 在 GT 上,不做 id 比对,理论上容忍措辞差异。
  3. 复用已有 OPSD teacher-forward、无需可靠 step verifier:在没有干净 step 级 verifier 的任务上,KL 是更通用的拿 step 信号的方式(不用为每个任务设计/训练一个 step 判分器)。

⚠️ 诚实 caveat(与动机相反的实测结论,见 §7):以上是「为什么想用 KL 代替 rule」的动机;但本任务的 action 恰好是 0–207 的干净离散 id,rule 的前提完全满足,而 KL 在实测中两头落空——自蒸馏塌平(§7.4)、且即便有波动也判不出对错(§7.3,似是而非的错步拿正分)。所以最终 rule 胜出

结论可写进论文:当任务具备干净的离散 GT 时,rule 直接命中正确性,KL 的「软/通用」优势变不成实际收益,反而暴露「分布相似 ≠ 动作正确」的固有缺陷;KL(forward_proxy)的真正适用区间是「没有干净 step GT、只能要软信号」的任务,在本工作中作为 ablation 对照存在。

5.5.3 小结:三者定位

方案 本质 何时用
(A) KL loss 模仿 teacher 分布(再开一个目标) 纯蒸馏 / SFT 打底阶段,不和 RL 混
(B) rule step reward 硬正确性信用(±1) 有干净离散 GT(本任务)→ 推荐
(C) KL step reward(本工作的 forward_proxy) 软信用、微调轨迹 reward、单一 RL 目标 没有干净 step GT、要软信号;本任务作对照

6. 两种 step reward 变体

我们实现了两套可由 config 一键切换的 step reward 来源,共用同一个 grpo_step 估计器和同一套 token-span 对齐:

6.1 变体 A:规则匹配(rule)

compute_step_match_rewards:预测每步的 action_id 与 GT 对应步逐位比较。

r_i = 1  if pred_step_i.action_id == gt_step_i.action_id else 0
A^S = +1 (对) / −1 (错) / 0 (scaffolding)

三种匹配口径:exact(严格逐位)/ prefix(最长正确前缀)/ lcs(最长公共子序列)。默认 exact + action_id

  • 优点:100% 可靠、零额外开销、永远不平、真·对错。
  • 适合:action 空间离散、有标准 id(本任务正是 0–207 离散 id)。

6.2 变体 B:OPSD 式 forward_proxy(teacher forward 代理信号)

不做 id 比对,而是把 GT 喂给 teacher 当条件(偷看答案),对学生轨迹做 forward,从 logp 里按 step 区间聚合代理分数。演化经历了三个版本(见 §7,对论文是关键 ablation):

最终正确形态 = OPSD 的同模型两次 forward

teacher context  = image + [Reference Plan Begin/End 包住 GT] + 衔接语 + pred
student context  = image + [Problem + 指令]                    + pred      (无 GT)
A^S_k = squash( mean_{t∈step_k} [ logp_teacher(x_t) − logp_student(x_t) ] )
  • 同一个模型、两种 context(不是两个不同模型),在同一段 pred token 上比 logp——这才是干净的 OPSD KL。
  • squash 压到 [−1,1] 两种口径(config step_squash):
    • prob = 2·exp(mean teacher_logp) − 1(teacher-only,实测恒平,见 §7.1
    • contrast = tanh(mean(teacher_logp − student_logp))唯一有意义的口径
  • 模板逐字 copy 自 OPSD_V2_Embody/data_collator.py

最终建议(来自实测):embodied planning 用 rule;forward_proxy 仅作为论文 ablation 的对照(展示 KL 为何不适用)。


7. 实测分析(论文 Analysis / Ablation / Discussion 的核心素材)

这一节是把 forward_proxy 路线一步步证伪的完整过程。每一个坑都是一个独立的 finding。

7.1 Finding 1:prob(teacher-only)模式天生是平的

teacher 条件在 GT 上、又是同一个模型,对学生写的每个 token 概率都很高 → A^S ≈ +1 处处一样 → 无波动结论:信号必须来自 student 与 teacher 的差异(KL),不是 teacher 的绝对概率。→ 改用 contrast

7.2 Finding 2:contrast 必须是「同模型两次 forward」,不能混 old_log_probs

第一版 contrast 错误地用「单独加载的 teacher 模型」 vs 「old_log_probs(live policy)」——两个不同模型 + GT 混在一起,不是干净 KL,所以也平。 修正:回到 OPSD 本意——同一个模型,student context(看 problem)vs teacher context(看 GT + 衔接语),同一段 pred 上比 logp。文本模型(Qwen3-1.7B, CPU)验证:diff 范围 [−12,+13]、std≈3.8,确有强波动

7.3 Finding 3(核心负结果):KL 无法判别「对错」——似是而非的错步拿正分

用真实文本模型跑 OPSD 两次 forward,逐 step 看 KL:

step 对/错 KL Δ stepR 问题
find a Ladle +4.23 +1.00
pick up the Ladle +2.22 +0.98
find a Sofa ✗(该是 DiningTable) +4.29 +1.00 错步却 +1
find a Sink +3.72 +1.00
turn on the Faucet −0.40 −0.38 勉强负

「find a Sofa」是错的,但 teacher 读了 GT(含「find a DiningTable」),觉得「find a 家具」这个套路合理 → 给 +1。KL 衡量的是「知道 GT 后这个 token 变得多可信」,不是「对不对」;只有结构上完全跑偏的(turn on Faucet)才勉强为负。这就是 §2 第 1 条「分布相似 ≠ 动作正确」用真实模型的证实。

7.4 Finding 4:换 teacher 是 KL 信号「从塌平到复活」的关键(已实测证实)

问题(teacher = policy 时):真实 run 里 policy 是 RobotGPT-R1,在这些轨迹上微调过 → 对自己生成的 token 极度自信 → teacher(=同模型+GT) ≈ student → 差 ≈ 0 → advline 完全平(自蒸馏坍缩)。没微调的 Qwen3-1.7B 才有波动;微调模型只会更平。

修法 = 换 teacher:teacher 改成未微调的通用模型 Qwen2.5-VL-3B-Instruct(policy 保持 RobotGPT 不变,tokenizer 同为 vocab 151665,response_ids 对齐没问题)。为什么换通用模型有效(两个层面)

  • ① 没过拟合到自己的轨迹(信号能否存在的必要条件):通用模型不像 SFT+RL 后的 policy 那样对自己 token 饱和自信,GT 一进上下文分布才会动 → 波动出现。
  • ② 通用模型的推理 / 判断能力更好(信号有没有用的关键):它读到 GT 参考计划时能更好理解任务,对「该这么走」的 token 给出有意义的信念变化 → 波动更可能朝正确方向。注意:换 teacher 后对比的是 Instruct(看GT) − Instruct(不看GT)整个信号现在完全压在这个通用模型的判断力上(policy 的 logp 已不参与),所以它的推理能力直接决定信号质量。

✅ 实测证实(这是 forward_proxy 信号复活的关键原因):脚本 step_teacher_path=Qwen2.5-VL-3B-Instruct(≠ policy 的 RobotGPT-R1)跑到 step 175,step_adv_heatmaps/_diag.txtteacher−student logp diff std ≈ 1.0–2.3、范围 ±15、mean ≈ +0.1~0.47——不再塌平。Finding 4 的假设成立:换未微调 teacher 是 KL 信号有波动的必要前提

⚠️ 但三点必须说准(避免论文过度声称)

  1. 换 teacher 不是当时 A^S=0 的直接原因。当时 A^S 恰好等于 0 的根因是 §7.6 的工程 bug(teacher forward 跑在无 GPU 的 driver 上崩进兜底)。两个 bug 叠加:换 teacher 治「塌平」+ 挪到 reward actor 治「崩溃」;只换 teacher 不挪卡照样恒 0。现在 std 1–2 是两者都修好之后的结果。
  2. 「有波动」≠「判得出对错」。std 1–2 只说明信号没死;§7.3 的核心负结果换 teacher 治不了——似是而非的错步(find a Sofa vs GT DiningTable)照样可能拿正分。波动是必要条件,不是「正确性信号」的充分条件。要证实这条,需拿「大部分对、个别错」的真实轨迹看错步 A^S 是否真被压负,光看 std 不够。
  3. 语义变了:teacher 换成独立 Instruct 后,对比的是 Instruct(看GT) − Instruct(不看GT)——policy 自己的 logp 没进对比。它不再是 OPSD 原本的「base/teacher vs policy」KL,而是「一个冻结的外部裁判模型的 KL-from-GT」。论文应明确这个设计区别。

7.5 Finding 5:per-step 聚合的「填充词稀释」问题

对整段 action_name(如「find a Sofa」)取均值时,通用的「find a」把有区分度的物体词「Sofa」稀释了 → 错步被拉正。 对策:聚合只取有区分度的决策 token(物体名 / action_id 数字),或按 |teacher−student| 加权;别对整段平均。

7.6 Finding 6(工程,非算法):driver 没有 GPU → forward_proxy 整段失败,A^S 恰好全 0

_diag.txt 的 traceback:RuntimeError: No CUDA GPUs are available。teacher forward 跑在 driver 进程,而 verl/Ray 里 driver 无 GPU(GPU 全在 worker actor 上),每个样本崩在 .to("cuda") → 走 except 兜底 → A^S 恰好全 0(呈现为「完全平的 0 线」,区别于「贴 0 的小噪声」)。 修法:把 teacher forward 挪到 reward actorfunction.pynum_gpus=0.3,verify 模型就在那张卡)——即「复用 verify 那张卡」。新增远程方法 compute_step_adv,driver 用 ray.get(reward_fn.compute_step_adv.remote(...)) 派活。rule 留在 driver(不需 GPU),forward_proxy 走 reward actor。 新坑:reward actor 要同时装 verify + Instruct teacher,0.3 卡上两个 3B bf16 ≈ 12GB 易 OOM;且重启多次老进程没杀干净会占卡 → vLLM 初始化 OOM。对策:worker.reward.num_gpus=1.0(独占一张)或 worker.rollout.gpu_memory_utilization=0.5

7.7 对照:verify(轨迹级 reward)是健康的强信号

同期把 verify reward(Yes Logits,∈[−1,1])画成曲线:2064 样本,mean=−0.37,std=0.78(强波动,真信号),范围 [−1,+1],负样本占 ~70%(早期训练正常)。这条轨迹级信号动得很厉害,正好反证 step reward 全平 = forward_proxy 坏了(GPU/OOM)+ KL 本身不适用,而非任务无信号。

7.8 总结论(写进 Discussion)

forward_proxy 信号现状(已更新):经两处修复——① 换未微调 teacher(治自蒸馏塌平,§7.4)+ ② 挪到有 GPU 的 reward actor(治崩溃恒 0,§7.6)——forward_proxy 现在产出活信号(std 1–2,非塌平,跑到 step 175)。所以不能再说「KL 必然塌平/不可用」;准确说法是「teacher=policy 时塌平;换冻结外部 teacher 后信号复活」。

但目标与工具的错配仍在:KL 衡量的是分布相似,不是动作正确。即便信号活了(有波动),§7.3 的问题——似是而非的错步可能拿正分——换 teacher 治不了,目前尚未实测排除

当前取舍规则匹配(action_id 比对)是最稳的 step reward(永远不平、真·对错、零开销、不占卡),作主方法;forward_proxy(Instruct teacher)作为软信号变体 / ablation 对照——它现在是「活的对照」而非「坏掉的对照」,但其正确性(错步是否真被压负、是否提升下游成功率)仍待验证。


8. Token-span 对齐(唯一真正的工程量)

reward 易分配 ≠ token 易对齐。executable_plan 给了语义的 step 切分,但模型生成的是 token 序列,JSON 元素边界与 token 边界不重合。需要 map_steps_to_token_spans(response_ids, gt_steps) -> [(start,end,r_k), ...](约 40 行):

  1. decode response 文本,容错 json.loadsexecutable_plan list;
  2. 实际 response_ids 逐 token 增量 decode,建每 token 的 [char_start, char_end) 偏移表;
  3. 对每个 step k,在文本里顺序定位子串(限定在 "executable_plan" 之后的区间,避免撞 language_plan),得字符区间;
  4. 偏移表把字符区间映射回 token 下标区间 = step k 的 token span。

4 个边界(务必处理)

  1. 顺序定位,别用全局 find——action_name 会重复(如 3 次 "pick up the Ladle"),必须从上一步结尾往后找,否则错位。
  2. 边界 token 跨元素——},\n{ 常被合成一个 token,同时贴前后两个 step;定规则(归前一步 / 当 scaffolding 不给 step 优势)。
  3. scaffolding token[]"action_name":"action_id":、逗号、空格、id 数字)只给 A^S=0,只把 action 内容 token 算进 step——正是要躲开「格式 token 抢梯度」。
  4. 非法 JSON 兜底——RL 早期生成可能不是合法 JSON,解析失败的样本整条退回纯 episode 优势(A^S 全 0),不能崩整个 batch。
  5. 稳健降级:token 对齐用「decode→重编码 round-trip 校验」,重编码不一致的样本安全跳过 A^S(=0),不错位不崩。

9. 实现细节(改了哪些代码、每处怎么改、为什么这么接)

这是本文档的核心之一。整套改动的设计原则是:A^E 那条(GRPO + verify 轨迹 reward)一行不动;A^S 作为一个 (bs, response_length) 的逐 token 张量 step_adv_raw 旁路注入 batch,在 compute_advantage 时与 A^E 相加。 这样 reward worker 的「单标量回传」不被破坏,新逻辑全部模块化、可由 ω=0/估计器切换关掉。

9.0 端到端数据流(一图看清调用链)

                       ┌──────────────────────── DRIVER(无 GPU)────────────────────────┐
 rollout (n>1) ──► verify reward ──► reward_tensor[i, L-1] = score  ──► (A^E 这条不变)     │
                   (轨迹标量 [-1,1])                                                        │
                                                                                          │
  if adv_estimator == grpo_step:                                                          │
     gt_field = 选 (step_gt_field / gt_action_close / ground_truth) 第一个存在的           │
     step_adv = self._build_step_adv(batch, gt_field)            # ← A^S 入口             │
        ├─ kind="rule":  build_step_adv_raw(...)  本地算(无需 GPU)                       │
        └─ kind="forward_proxy":                                                          │
              ray.get( reward_fn.compute_step_adv.remote(batch, ...) )  ──────────┐       │
     batch.batch["step_adv_raw"] = step_adv                                       │       │
     打印 "[grpo_step] kind=... N/M responses got non-zero step advantage"        │       │
                                                                                  │       │
     compute_advantage(batch, adv_estimator=grpo_step, step_advantage_w=ω)        │       │
        → compute_grpo_step_outcome_advantage:                                    │       │
             A^E = compute_grpo_outcome_advantage(...)        # 组内归一化轨迹优势  │       │
             A   = A^E + ω · (step_adv_raw * response_mask)                        │       │
     _save_total_adv_heatmaps(batch)   # 存 *_total.png / *_advline.png           │       │
                                                                                  │       │
  └───────────────────────────────────────────────────────────────────────────── │ ──────┘
                                                                                  │
      ┌────────────── REWARD ACTOR(有 GPU = verify 那张卡, num_gpus=0.3)─────────▼──────┐
      │ compute_step_adv(batch, teacher_path, squash, ...):                              │
      │   lazy-load TeacherLogpScorer(teacher_path)  # 同 policy 的 Qwen2.5-VL           │
      │   for i in batch:                                                                │
      │     t_logp, s_logp = scorer.logp_teacher_student(instr, gt, image, resp_ids)     │
      │                       └─ step_logp_vl ×2(teacher 看 GT / student 看 problem)     │
      │     out[i] = build_step_adv_vector_from_logp(resp, t_logp, s_logp, squash)       │
      │   return out.cpu(), diag_str   # diag 写进 _diag.txt                              │
      └─────────────────────────────────────────────────────────────────────────────────┘

为什么 rule 在 driver、forward_proxy 在 reward actor:rule 只需 tokenizer + 字符串比对,driver 上即可;forward_proxy 要跑 teacher 的 VL forward 需要 GPU,而 verl/Ray 里 driver 进程没有 GPU(GPU 全分给 worker actor)——这是 §7.6 实测踩出来的坑,所以把它派给本来就有卡、且跟 rollout 错开的 reward actor(verify 模型就住在那)。

9.1 verl/trainer/step_reward.py — 自包含的打分核心(已单测)

整个「step → token → 分数」的纯逻辑都在这,不依赖任何 reward manager。关键函数:

  • extract_plan_steps(text) — 容错解析生成串里的 executable_plan:先 _strip_suffix 去掉尾部 <|im_end|>/<|endoftext|>,再剥 ```json 代码块,json.loads 失败时**用花括号配对**兜底抽第一个顶层 {...};还兼容模型直接吐 bare list。返回 step list 或 None
  • gt_steps_from_close(gt_close) — 把 gt_action_close 解析成 trajectory_actions list(兼容 list / dict / JSON str)。
  • **compute_step_match_rewards(pred, gt, mode, key)**(规则打分)— 按 key(默认 action_id,缺失回退 action_name)逐步比对,三种 mode:
    • exact:严格按位置 pred[i]==gt[i] → 0/1;
    • prefix:最长正确前缀(第一个错之后全 0);
    • lcs:DP 求最长公共子序列,标记参与对齐的预测位置。
  • pred_step_token_indices(response_ids, valid_len, tokenizer)token 对齐的唯一真源,rule 和 forward_proxy 共用):
    1. decode 出文本、extract_plan_steps 拿步列表;
    2. _token_char_offsets:decode→重编码 round-trip 校验tokenizer(text, return_offsets_mapping=True)),若重编码 token 序列 ≠ 原 ids 则返回 offsets=None → 该行安全跳过 A^S(稳健降级,不错位不崩);
    3. 对每步的 action_name 在文本里从上一步结尾 cursor 往后顺序定位(避免 action_name 重复时全局 find 错位,§8 边界1),字符区间再用 offset 表映射成 token 下标 idxs。返回 (pred_steps, spans)
  • **build_step_adv_vector(...)**(规则 A^S):对每步 span,匹配→+1、不匹配→−1,scaffolding/解析失败→0,填进 (full_len,) 张量。
  • build_step_adv_vector_from_logp(..., teacher_logp_row, student_logp_row, squash)(forward_proxy A^S,[−1,1] clamp):对每步 span 内的 token logp 聚合:
    • squash="contrast"唯一有意义):A^S = tanh( mean_t (teacher_logp_t − student_logp_t) ),缺 student_logp 时自动退回 prob 并失去波动;
    • squash="prob"实测恒平,仅对照):A^S = 2·exp(mean teacher_logp) − 1
  • build_step_adv_raw(responses, response_mask, gt_close_list, tokenizer, ...) — 规则版的 batch 包装,driver 上直接调,返回 (bs, response_length)

9.2 verl/workers/reward/verify.py — 新增 step_logp_vl(保留视觉,仿 ask_llm

在你原有的 verify ask_llm 旁边加一个做 forward 取 logp(而非 generate 出 Yes/No)的接口,复用同一套 load_verify / load_processor / load_tokenizer、同 dtype/attn:

step_logp_vl(model, processor, instruction, image, response_ids, device):
  messages = [{user: [image, text=instruction]}]                 # 视觉保留,和 ask_llm 一致
  prompt_ids = processor(apply_chat_template(...), image)         # (1,P),image token 已展开
  input_ids  = cat([prompt_ids, response_ids], dim=1)            # (1, P+R) 把学生原 token 拼后面
  position_ids = model.get_rope_index(input_ids, image_grid_thw, mask)   # HF mrope 4D;失败回退 None
  logits = model(input_ids, attention_mask, position_ids, pixel_values, image_grid_thw).logits[0]
  pred_logits = logits[P-1 : P-1+R]      # logits[t] 预测 token t+1 → response token r 由 logits[P+r-1] 预测
  return log_softmax(pred_logits).gather(resp_ids)   # (R,) 每个学生 token 的 logp,CPU

关键对齐技巧:把学生原样的 response_ids 直接拼在 prompt 后面 → 模型对这些 token 的 logp 在 response 段和学生 token 1:1 对齐,直接套上 §8 的 step span。唯一没法本地验、需上机确认的点就是 VL 的 position_ids/pixel_valuesget_rope_index 那段,已加 try/except 回退)。

9.3 verl/trainer/teacher_proxy.py — OPSD 式「同模型两次 forward」(§7.2 的修正版)

TeacherLogpScorer 加载一个模型(= policy,Qwen2.5-VL),对同一段 pred token 跑两次 step_logp_vl

logp_teacher_student(instruction, gt_text, image, response_ids):
  teacher_text = "Here is a reference successful language plan:\n=== Reference Plan Begin ===\n
                  {GT}\n=== Reference Plan End ===\n{TRANSITION}"      # 偷看答案
  student_text = "Problem: {instruction}\n\n{STUDENT_INSTRUCTION}"     # 不看 GT
  teacher_logp = step_logp_vl(model, ..., teacher_text, image, response_ids)
  student_logp = step_logp_vl(model, ..., student_text, image, response_ids)
  return teacher_logp, student_logp        # 二者之差 = 干净的 OPSD KL(同模型,仅 context 不同)

模板逐字 copy 自 OPSD_V2_Embody/data_collator.pySTUDENT_INSTRUCTION / REFERENCE_BEGIN/END / TRANSITION)。policy_vocab_size 不一致会 WARN(要求 teacher==policy 同 tokenizer 才能拼接对齐)。这是 §7.2 的核心修正:之前错用「独立 teacher 模型 vs old_log_probs」=两个模型混 GT;这里是「同一个模型、两种 context」=真 OPSD。

9.4 verl/workers/reward/function.py — reward actor 上的远程方法 compute_step_adv

active reward manager(function.py,reward actor num_gpus=0.3,verify 模型已在此卡)新增 compute_step_adv(data, teacher_path, squash, gt_field, match_key)

  • lazy-load TeacherLogpScorer(teacher_path, device="cuda:0") 到 actor 自己的卡(_teacher_scorer 缓存);
  • non_tensor_batchgt_field(多 key 兜底)、instructionimage(兼容 multi_modal_data);
  • 逐样本两次 forward → build_step_adv_vector_from_logp(squash) → 攒成 (bs, L)
  • sample0 写诊断:成功 OK (teacher-student) logp diff: std=.. mean=.. min=.. max=..,异常 FAILED sample0 teacher forward: <traceback> —— 这行写进 step_adv_heatmaps/_diag.txt(§7.6 就是靠它定位「driver 无 GPU」);
  • 返回 (step_adv_raw.cpu(), diag),driver 用 ray.get 取回。

9.5 verl/trainer/core_algos.py — 新优势估计器 grpo_step

AdvantageEstimator.GRPO_STEP = "grpo_step"

@register_adv_estimator(GRPO_STEP)
compute_grpo_step_outcome_advantage(token_level_rewards, response_mask, index,
                                    step_adv_raw=None, step_advantage_w=1.0):
    episode_adv = compute_grpo_outcome_advantage(token_level_rewards, response_mask, index)  # A^E 完全复用
    if step_adv_raw is None: return episode_adv, episode_adv     # 无 step 数据 → 自动退化为纯 GRPO
    step_adv = step_adv_raw.to(...) * response_mask
    advantages = episode_adv + step_advantage_w * step_adv       # A = A^E + ω·A^S
    return advantages, advantages

设计要点:A^E 直接调用现成的 compute_grpo_outcome_advantage(不重写 GRPO),grpo_step 只是在它之上加一项;step_adv_raw=None无损退回纯 GRPO,所以即使 step 解析全失败训练也不会崩、只是少了 step 项。

9.6 verl/trainer/ray_trainer.py — 驱动侧接线

  • **_build_step_adv(batch, gt_field)**:分支 rule(driver 本地 build_step_adv_raw)/ forward_proxy(ray.get(reward_fn.compute_step_adv.remote(...))),算完顺手存 *_stepReward.png 热力图。
  • 主循环adv_estimator==grpo_step 时,先选 gt_fieldstep_gt_field → gt_action_close → ground_truth 兜底,都没有则 WARN + 退纯 GRPO),算 step_adv 写进 batch.batch["step_adv_raw"],打印 N/M 非零计数自检。
  • compute_advantage 透传 step_adv_rawadv_inputs 里带上)和 step_advantage_w
  • 算完 advantage 后 _save_total_adv_heatmaps*_total.png / *_advline.png
  • rollout.n>1 的 GRPO 硬校验加上了 GRPO_STEP

9.7 verl/trainer/step_viz.py — 三种可视化

save_step_adv_heatmap:按 token 上色,RdBu_r + TwoSlopeNorm(0 居中)正→红 / 负→蓝 / 0→白(§7 最后对调过一次配色),词上标数值、自动折行、带 colorbar;advline 两栏折线图(上:A^E 横线 + A 总线;下:A^S 单独线)。rule 和 forward_proxy 共用(输入都是 adv 向量)。

9.8 改动文件清单(速查)

文件 改了什么 状态
verl/trainer/step_reward.py 新建:解析 + 规则打分 + token-span 对齐 + 两种 A^S 构造 ✅ 单测过
verl/trainer/teacher_proxy.py 新建:OPSD 同模型两次 forward(logp_teacher_student ⚠️ 对齐测过,VL forward 需上机
verl/workers/reward/verify.py 新增 step_logp_vl(视觉保留,一次 forward 取 logp) ⚠️ 需上机
verl/workers/reward/function.py 新增远程方法 compute_step_adv(reward actor 的卡上跑 teacher) ✅ 接线完成
verl/trainer/core_algos.py 新增 GRPO_STEP 枚举 + compute_grpo_step_outcome_advantage ✅ 测过
verl/trainer/ray_trainer.py _build_step_adv / 主循环接线 / compute_advantage 透传 / heatmap
verl/trainer/step_viz.py 新建:stepReward / total 热力图 + advline 折线图
verl/trainer/config.py AlgorithmConfig 新增 9 个 step_* 字段

Config 开关

algorithm:
  adv_estimator: grpo_step          # grpo → grpo_step
  step_reward_kind: rule            # rule(推荐)/ forward_proxy
  step_advantage_w: 1.0             # ω
  # --- rule ---
  step_match_mode: exact            # exact / prefix / lcs
  step_match_key: action_id
  step_gt_field: gt_action_close    # 多 key 兜底:gt_action_close → ground_truth
  # --- forward_proxy ---
  step_squash: contrast             # contrast(唯一有意义)/ prob(恒平,仅对照)
  step_teacher_path: <同 tokenizer 模型>   # 如 Qwen2.5-VL-3B-Instruct
  step_teacher_device: cuda:0
  # --- 可视化 ---
  step_heatmap_n: 4                 # 每步存 N 条轨迹的图(0 关闭)

rollout.n > 1 是 GRPO 的硬要求(grpo_step 复用 GRPO)。

自检日志

启动看:[grpo_step] kind=... from 'gt_action_close': N/M responses got non-zero step advantage.

  • N/M 接近满 → 解析+对齐正常;0/M → GT 字段/格式不符,调 step_match_key 或解析。
  • forward_proxy 另看 step_adv_heatmaps/_diag.txtOK ... std=.. / FAILED ... traceback

可视化产物(每条轨迹三张,step_adv_heatmaps/

文件 内容 配色
*_stepReward.png step reward([−1,1]) 正→红 / 负→蓝 / 0→白
*_total.png 合并 advantage A=A^E+ω·A^S 同上,标题带 verify_reward=±x
*_advline.png 上:A^E 横线 + A 总线;下:A^S 单独线

注意:reward ∈[−1,1],advantage(GRPO 归一化后)无界*_stepReward 画的是 [−1,1] 的 step reward;*_total/*_advline 画的是合并后的 advantage。verify 的轨迹标量 reward 不单独画(逐 token 均匀),归一化成 A^E 并进 total。


10. 四家对比表:GRPO / GIGPO / OPSD / 本工作(写 Method / Related Work 直接用)

维度 GRPO GIGPO OPSD(logits/KL) 本工作(grpo_step)
数据布局 整轨一行 每行一个 env step 整轨 / 逐 token 整轨一行,step 是 token 段
信号粒度 仅轨迹级(1 标量) episode + step 双层 token 级(KL) episode + step 双层
step 信号来源 环境即时奖励 → γ 折扣回报 teacher−student token logits GT 逐 step 匹配(rule)/ OPSD 代理(fwd_proxy)
step 分组 相同环境状态 anchor_obs 无(逐 token) 相同 step 位置 k
要环境 (多轮交互) (单次整轨)
value/critic 不要(组内归一化) 不要 不要 不要(复用 GRPO 组归一化)
teacher 不要 不要 (偷看 GT) rule 不要 / fwd_proxy 要
优势公式 A^E(组归一化广播) A^E + ω·A^S KL 当 loss **A^E + ω·A^S**(KL/规则当 reward
信用分配 整轨同值(太粗) step(状态对齐) token(格式 token 抢梯度 step(按 token 段,scaffolding=0)
适配本任务 ⚠️ 粒度太粗 ✗ 无环境借不了 anchor ✗ 塌平+判不出对错(§7) ✓ rule 干净;fwd_proxy 作对照

「集三家之长」的读法(对应 §1.5):

  • 从 GRPO 拿 → 第「value/critic」「优势公式 A^E」两行:免 value 的组内归一化轨迹优势,直接当我们的 A^E。
  • 从 GIGPO 拿 → 第「信号粒度」「优势公式」两行:A^E + ω·A^S 的 episode+step 双层结构
  • 从 OPSD 拿 → 第「step 信号来源」「teacher」两行:偷看 GT 拿 step 代理信号的思想(forward_proxy 变体)。
  • 本工作独有的取舍 → 第「要环境」「信用分配」「优势公式」三行:无环境(把 GIGPO 的环境 step 信号换成离线 GT 信号)、step 而非 token(躲开 OPSD 的格式 token 抢梯度)、KL/规则当 reward 而非 loss(§5.5)。

11. 待定 / 可调参数

  • 匹配口径exact(默认)/ prefix / lcs
  • 匹配键action_id(默认,离散纯净)/ action_name(同义不同字时)。
  • ω(step_advantage_w):默认 1.0。规则版 A^S=±1 与归一化后 A^E(≈±1)同量级,ω=1 平衡;forward_proxy 量级不固定,可能需调小或对 A^S 也做组归一化(注意 §7.5 的尺度与稀释问题)。
  • A^S 是否组内归一化:默认否(rule 有零点不需要);要降方差再加。
  • teacher(forward_proxy):同模型自蒸馏(会坍缩,§7.4)vs 未微调 Instruct(有波动但语义改变)。

12. 实验留白(本文档不展开)

本文档聚焦动机与实现,实验设计留给论文正文。仅记两个由 §7 实测自然得出的对照轴备忘:信号来源(rule vs forward_proxy vs 纯 GRPO)、信用粒度(step vs token vs trajectory);以及 ω 与聚合范围(整段均值 vs 仅决策 token,§7.5)两个敏感参数。可视化证据见 §9.7 的三类图。


附录 A:关键公式速查

# episode 优势(= 现有 GRPO,不变)
A^E_i = (R_i − mean_{j∈group(i)} R_j) / (std + eps)          # R = 整轨标量 reward, group = 同 uid 的 N rollout
A^E_token = A^E_i × response_mask_i                          # 广播到整行

# step 优势(rule)
r_k = 1[pred_k.action_id == gt_k.action_id]
A^S_k = +1 (r_k=1) / −1 (r_k=0) / 0 (scaffolding)            # 居中, 无需组归一化
A^S_token = A^S_{step(token)}  if token∈某 step 的内容段 else 0

# step 优势(forward_proxy, contrast)
A^S_k = tanh( mean_{t∈step_k 内容token} [ logp_teacher(x_t | GT) − logp_student(x_t | problem) ] )   # ∈(−1,1)

# 合并
A_token = A^E_token + ω · A^S_token

附录 B:本文档来源

整理自 2026-05-31 ~ 06-01 的设计与实测对话(session 3361bbf8)。关键里程碑:12:40 读 GIGPO 文档 → 12:54 转向 EasyR1-V3-Embody 自有实现 → 13:21 写 grpo_step + rule → 16:40 转 forward_proxy → 17:15 squash[−1,1] → 18:22 发现 prob 平 → 18:30 改同模型两次 forward → 19:18 发现 KL 塌平 → 19:27 发现 KL 判不出对错 → 19:41 换 Instruct teacher → 20:30 定位 driver 无 GPU → 21:01 OOM 排查 → 06-01 verify reward 曲线对照。