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_truth(dataset.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 太粗,分不清同一条轨迹里哪步对哪步错)。但现成的两个工具各有短板:
- GIGPO 没法很好地判断每一步的对错。GIGPO 的 step 优势来自「相同环境状态下、不同动作比 return」,它依赖多轮环境交互、反复路过同一状态;本任务是单次整轨预测、无环境,根本没有「同状态重复」可比,借不到它的 step 判别能力。
- 严格的规则匹配(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)。 - 为什么换通用模型有效? 两个层面:
- 没过拟合到自己的轨迹 → 不像 policy 那样对自己 token 饱和自信,GT 一进上下文分布才会动(这是「信号能不能存在」的必要条件);
- 通用模型的推理 / 判断能力更好 → 当它读到 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。理由五条(前两条有实测热力图证据):
- KL 优化「分布像不像」≠「动作对不对」。一条流畅但走错(该去 Sink 却去 Table)的轨迹,KL 可以很低。
- 格式 token 抢梯度。
find/a/the/ JSON 骨架{"action_id":占了绝大多数 token 却无决策信息,KL 把梯度浪费在上面(热力图实测:红色集中在 JSON 骨架,内容反而发白;clip 0.05 只是补丁)。 - 决策单位是 step,KL 却摊到每个 token。「去 Sink 还是 Table」是一个决策,不该被切成十几个 token 各自匹配。
- privileged-info leakage:teacher 偷看答案,存在把 GT 信息直接泄漏进梯度的风险。
- 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-agent 的 gigpo/core_gigpo.py。它的 step reward 不是跟 GT 比对,而是「同状态下比 return」:
- 数据单位:每行 = 一个 step transition(一次 env turn)。
- 环境给即时奖励 →
compute_step_discounted_returns(core_gigpo.py:87):按traj_uid分组做 γ-折扣回报R_t = r_t + γ·R_{t+1}。所以 GIGPO 的「step reward」其实是该步往后的折扣回报,不是 0/1 对错。 - anchor-state 分组(GIGPO 的灵魂) →
build_step_group(:243):在同 prompt 的 N 条轨迹里,把落到相同环境状态(anchor_obs 相同)的 step 聚成一组(Eq.6「same state, different action」)。 - 同状态内归一化 →
step_norm_reward(:334):组内 mean(-std) 归一化 → A^S,无需 value network。 - 合并 →
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 reward:
action_id逐位比对 → 硬 ±1,作为 advantage。 - (C) 把 KL 折算成 step reward / advantage 叠加(本工作选的):
A_token = A^E + ω·A^S,A^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):
- 软、分级、给部分信用:rule 是硬 ±1(非对即错),KL 是连续值,能表达「这一步有多对 / 偏多少」,对「接近但不完全对」的步给中间分,梯度更平滑。
- 不需要精确的离散 GT 对齐:rule 要
action_id严格相等;当 action 措辞对但 id 不同、或任务根本没有干净标准 id 时,rule 会误判。KL 只要 teacher 能 condition 在 GT 上,不做 id 比对,理论上容忍措辞差异。 - 复用已有 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] 两种口径(configstep_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.txt 的 teacher−student logp diff std ≈ 1.0–2.3、范围 ±15、mean ≈ +0.1~0.47——不再塌平。Finding 4 的假设成立:换未微调 teacher 是 KL 信号有波动的必要前提。
⚠️ 但三点必须说准(避免论文过度声称):
- 换 teacher 不是当时 A^S=0 的直接原因。当时 A^S 恰好等于 0 的根因是 §7.6 的工程 bug(teacher forward 跑在无 GPU 的 driver 上崩进兜底)。两个 bug 叠加:换 teacher 治「塌平」+ 挪到 reward actor 治「崩溃」;只换 teacher 不挪卡照样恒 0。现在 std 1–2 是两者都修好之后的结果。
- 「有波动」≠「判得出对错」。std 1–2 只说明信号没死;§7.3 的核心负结果换 teacher 治不了——似是而非的错步(
find a Sofavs GTDiningTable)照样可能拿正分。波动是必要条件,不是「正确性信号」的充分条件。要证实这条,需拿「大部分对、个别错」的真实轨迹看错步 A^S 是否真被压负,光看 std 不够。 - 语义变了: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 actor(function.py,num_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 行):
- decode response 文本,容错
json.loads拿executable_planlist; - 对实际 response_ids 逐 token 增量 decode,建每 token 的
[char_start, char_end)偏移表; - 对每个 step k,在文本里顺序定位子串(限定在
"executable_plan"之后的区间,避免撞language_plan),得字符区间; - 偏移表把字符区间映射回 token 下标区间 = step k 的 token span。
4 个边界(务必处理):
- 顺序定位,别用全局 find——action_name 会重复(如 3 次 "pick up the Ladle"),必须从上一步结尾往后找,否则错位。
- 边界 token 跨元素——
},\n{常被合成一个 token,同时贴前后两个 step;定规则(归前一步 / 当 scaffolding 不给 step 优势)。 - scaffolding token(
[、]、"action_name":、"action_id":、逗号、空格、id 数字)只给 A^S=0,只把 action 内容 token 算进 step——正是要躲开「格式 token 抢梯度」。 - 非法 JSON 兜底——RL 早期生成可能不是合法 JSON,解析失败的样本整条退回纯 episode 优势(A^S 全 0),不能崩整个 batch。
- 稳健降级: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_actionslist(兼容 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 共用):- decode 出文本、
extract_plan_steps拿步列表; _token_char_offsets:decode→重编码 round-trip 校验(tokenizer(text, return_offsets_mapping=True)),若重编码 token 序列 ≠ 原 ids 则返回offsets=None→ 该行安全跳过 A^S(稳健降级,不错位不崩);- 对每步的
action_name在文本里从上一步结尾cursor往后顺序定位(避免 action_name 重复时全局 find 错位,§8 边界1),字符区间再用 offset 表映射成 token 下标idxs。返回(pred_steps, spans)。
- decode 出文本、
- **
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_values(get_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.py(STUDENT_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_batch取gt_field(多 key 兜底)、instruction、image(兼容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_field(step_gt_field → gt_action_close → ground_truth兜底,都没有则 WARN + 退纯 GRPO),算step_adv写进batch.batch["step_adv_raw"],打印N/M非零计数自检。 compute_advantage透传step_adv_raw(adv_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.txt:OK ... 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 曲线对照。