Skip to content

No.41 SDPO实现与复现 - #237

Open
ZiyiTsang wants to merge 274 commits into
redai-studio:mainfrom
ZiyiTsang:sdpo
Open

No.41 SDPO实现与复现#237
ZiyiTsang wants to merge 274 commits into
redai-studio:mainfrom
ZiyiTsang:sdpo

Conversation

@ZiyiTsang

@ZiyiTsang ZiyiTsang commented Aug 4, 2026

Copy link
Copy Markdown

总结

本 PR 在 OPD/OPSD teacher-prefill 通路上增加一个低侵入的 Relax-SDPO 路径:plain OPD
直接用 rollout token;ordinary OPSD 经 --opd-feedback-class OPSDFeedback 消费数据集
teacher prompt 列(ingestion 渲染进 metadata["opd_teacher_prompt"],rollout 时由
feedback 类赋给 Sample.teacher_prompt);SDPO 在 group rollout 完成并获得 reward 后,
统一经 EnvironmentFeedback.record_sample_feedback()prepare_teacher_prompts(group, rewards),由 feedback 类动态构造 teacher prompt。teacher 仅对原 response 在 student
Top-K token ids 上重打分。纯蒸馏复用现有 OPD loss:

--opd-loss-coef 1.0 --opd-kl-coef 0.0 --opd-disable-rl-reward

SDPO 仅支持 student_topk,仅走文本、TP/CP、无 PP 路径。普通 OPD/OPSD 继续复用已有
teacher prefill、Top-K selection、KL/JSD 计算和 loss reducer。本 PR 另增 opd_sample_mask
过滤无动态 teacher context 的 sample;teacher 为冻结 snapshot(EMA 更新为后续阶段)。

OPD 与 SDPO 的差异

项目 普通 OPD / OPSD Relax-SDPO 做法 / 组件
teacher prompt plain OPD 用 sample.rollout_tokens or sample.tokens;ordinary OPSD 用数据集列渲染的 privileged prompt(OPSDFeedback 赋值) reward 后动态生成 Sample.teacher_prompt,可附同 group 成功 response SDPO 在 generate_and_rm_group() 的 reward 后、OpdManager.prefill() 前注入
teacher 现有 teacher 请求 static:保持初始 teacher snapshot 现有 teacher manager 与 weight 生命周期
response 对 rollout response 算 teacher log-prob teacher prompt 变长,response suffix 仍为原 response 现有 teacher input/offset 逻辑保持对齐
token support student_sampled/student_topk/teacher_topk/union student_topk TopkWorker Strategy / Specification
teacher Top-K 普通 OPD selection 选 support teacher 不重选 support,仅在 student 选出 ids 上打分 TopkWorker.build_teacher_payload()
divergence 现有 OPD KL/JSD 现有 compute_policy_opd_loss 配置为纯蒸馏 --opd-loss-coef=1 --opd-kl-coef=0
sample mask 不设置该字段 无动态 teacher context 的 sample 不贡献 loss 梯度(折叠进共享 loss_masks) Sample.opd_sample_mask;get_batch 折叠进 loss_masks,compute_policy_opd_loss 据其存在选 CP 感知归约
无效 teacher 数据 普通 OPD 保留失败处理 context 缺失回退并屏蔽 sample;shape/id/payload 工程错误仍校验失败 validate_sdpo_text_onlyvalidate_sdpo_topk_payload
并行布局 现有 TP/CP response layout 同 row layout;K 列不在 CP 下切分 现有 OPD CP row slicing 与 TP global-id gather
多模态 普通 OPD/OPSD 保持能力 SDPO teacher 边界遇 image/video/audio 或结构化媒体即报错 validate_sdpo_text_only Guard
flowchart TD
    classDef existing fill:#eef3f8,stroke:#64748b,stroke-width:1px,color:#1e293b
    classDef changed fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#991b1b
    classDef shared fill:#eef3f8,stroke:#2563eb,stroke-width:2px,color:#1e293b

    DATA["RolloutDataSource"]:::existing --> ROLLOUT["SGLangRollout<br/>student group rollout"]:::existing
    ROLLOUT --> RM["batched_async_rm(args, group)<br/>one reward per sample"]:::existing

    subgraph FEEDBACK_API["EnvironmentFeedback"]
        direction TB
        subgraph RECORD_STAGE["record_sample_feedback(sample, reward)"]
            direction LR
            OPD_RECORD["OPDFeedback<br/>no-op"]:::existing
            OPSD_RECORD["OPSDFeedback<br/>no-op"]:::existing
            SDPO_RECORD["GoldenAnswer / Code<br/>record sample feedback metadata"]:::changed
        end
        subgraph PREPARE_STAGE["prepare_teacher_prompts(group, rewards)"]
            direction LR
            OPD_PREPARE["OPDFeedback<br/>plain OPD: rollout tokens"]:::existing
            OPSD_PREPARE["OPSDFeedback<br/>ordinary OPSD: Sample.teacher_prompt"]:::existing
            SDPO_PREPARE["GoldenAnswer / Code<br/>dynamic Sample.teacher_prompt<br/>+ Sample.opd_sample_mask"]:::changed
        end
        OPD_RECORD --> OPD_PREPARE
        OPSD_RECORD --> OPSD_PREPARE
        SDPO_RECORD --> SDPO_PREPARE
    end

    RM --> OPD_RECORD
    RM --> OPSD_RECORD
    RM --> SDPO_RECORD

    subgraph LIFECYCLE["Existing OpdManager teacher-prefill lifecycle"]
        MANAGER["OpdManager.prefill(samples)"]:::existing
        PREPARE_INPUT["teacher input prep<br/>OPSD/SDPO: OpsdWorker 读 Sample.teacher_prompt"]:::existing
        REQUEST["_teacher_prefill()<br/>teacher request + response offset"]:::existing
        TRANSFER["_assemble_transfer()<br/>Top-K / teacher log-prob fields"]:::shared
        MANAGER --> PREPARE_INPUT --> REQUEST --> TRANSFER
    end

    OPD_PREPARE --> MANAGER
    OPSD_PREPARE --> MANAGER
    SDPO_PREPARE --> MANAGER
    TRANSFER --> LOSS_INPUT["OPD transfer to training"]:::shared
    LOSS_INPUT --> LAYOUT["existing OPD TP/CP layout<br/>+ SDPO sample gate"]:::changed
    LAYOUT --> FORWARD["Megatron student forward"]:::shared
    FORWARD --> LOSS["compute_policy_opd_loss<br/>existing OPD loss + sample mask"]:::existing
    RM --> RL_MASK["advantage + loss_masks"]:::existing
    RL_MASK --> LOSS
    LOSS --> OPT["Megatron optimizer update"]:::shared

    class DATA,ROLLOUT,RM,MANAGER,PREPARE_INPUT,REQUEST,LOSS existing
    class SDPO_RECORD,SDPO_PREPARE,LAYOUT changed
    class TRANSFER,LOSS_INPUT,FORWARD,OPT,RL_MASK shared
Loading

红色节点为本 PR 新增/改变的边界逻辑;蓝色边框为复用组件;OPD/OPSD 分支保持灰色。三者经同一
调用路径进入 record_sample_feedback()prepare_teacher_prompts(),但实现不同:OPD/OPSD
为空实现(OPSD 在 prepare_teacher_prompts 中赋值数据集 privileged prompt),GoldenAnswer SDPO
feedback 记录当前 sample feedback 并由 batch-level 实现决定是否读取同组成功 response。特权信息只进入 Sample.teacher_prompt 与 teacher input,不改 student
prompt/response 或 rollout token ids。opd_sample_mask 随 transfer 进入训练侧,在 get_batch_apply_opd_sample_mask()
折叠进 loss_masks、并由 compute_policy_opd_loss 选择 CP 感知归约来屏蔽无有效 teacher
target 的 sample;普通 OPD/OPSD 不设该字段,保持原数值路径。

组件与设计模式

设计原则:低侵入地增加 dynamic feedback prompt、sample-level OPD mask 与 static teacher 更新。

组件 / 接口 关键决策 接口 / 注入点
Loss 计算 复用现有 OPD loss 入口表达纯蒸馏 compute_policy_opd_loss()--opd-loss-coef 1.0 --opd-kl-coef 0.0
环境交互 统一 sample-level 记录 + batch-level 构建两阶段;OPD/OPSD/SDPO 不同实现 record_sample_feedback(sample, reward)prepare_teacher_prompts(group, rewards)generate_and_rm_group() 在 reward 后依次调用
数据集 SDPO feedback batch 内按 group 组织成功解,sample 内合并本 sample feedback;由 --opd-feedback-class 选择 GoldenAnswerSDPOFeedback(静态 golden-answer 文本任务)、CodeSDPOFeedback(占位)
Sample.opd_sample_mask 动态 teacher target 是否有效作为 sample-level gate OpdManager.produce_opd_transfer_data() 写入 train_dataget_batch_apply_opd_sample_mask() 折叠进 loss_maskscompute_policy_opd_loss() 据其存在选 CP 感知归约
SDPO validation 文本、student Top-K ids、teacher payload 检查置于 SDPO 边界 relax/utils/opd/sdpo/validation.pyOpdManager.prefill() / _assemble_transfer() 调用
Teacher 更新 冻结 snapshot:actor update 后 teacher 权重不变(EMA 为后续阶段) 现有 teacher manager 生命周期,不更新
Teacher prefill / Top-K / TP-CP layout 复用现有 OPD/OPSD 请求、response offset、Top-K transfer 与并行布局 OpdManagerTopkWorker 及现有 OPD loss/reducer

EnvironmentFeedback

所有 OPD/OPSD/SDPO 路径经同一组接口,行为由 --opd-feedback-class 选定的 feedback 类决定。
参数 schema 声明在接口层,经单一 CLI 入口 --opd-feedback-kwargs(JSON)传入,运行时
cls(**kwargs) 绑定到具体构造函数,各子类只消费自己用到的字段:

class EnvironmentFeedback:
    def __init__(self, teacher_prompt_key: str | None = None,
                 success_reward_threshold: float = 1.0) -> None: ...
    def record_sample_feedback(self, sample: Sample, reward: Any) -> None: ...
    def prepare_teacher_prompts(self, group: list[Sample], rewards: list[Any]) -> None: ...
  • plain OPD:默认 OPDFeedback + 空 kwargs(teacher 复用 student prompt)
  • OPSD:OPSDFeedback + {"teacher_prompt_key": "<数据列名>"}(必填, ingestion 用该列渲染
    privileged prompt)
  • SDPO:SDPOFeedback 系 + 可选 {"success_reward_threshold": 0.8}(成功分数线,默认 1.0)

注入点在 relax/engine/rollout/sglang_rollout.pygenerate_and_rm_group():student
group 完成 rollout 后,先由 batched_async_rm() 产生与 group 对应的 rewards,再依次
record_sample_feedback()prepare_teacher_prompts(),最后进现有 OpdManager.prefill()

record_sample_feedback() 只处理当前 sample:OPD/OPSD 空实现,SDPO 将 reward payload 中
feedback 写入 sample.metadata["env_feedback"]prepare_teacher_prompts() 处理完整 group,
按决策矩阵逐样本决定注入内容(见「METH 决策矩阵」)。无有效 solution/feedback 时复制原 prompt
并将 opd_sample_mask 设为 False。student 的 prompt/response/rollout token ids/student
Top-K ids 均不变。

特权信息使用

GoldenAnswerSDPOFeedback 与占位的 CodeSDPOFeedback 共用一套矩阵(reward 无关),各数据集反馈文本由示例 reward.py 生成。
所有注入内容均不含金标(正确答案 / expected 动作参数)。

样本状态 有成功 peer 无 peer
正确 注入 peer 正确解 ✅ 注入自身正确解 ✅
格式错 / 截断 注入 peer 解(丢弃 feedback)✅ 注入格式 / 截断反馈 ✅
普通算错 注入 peer 解 ✅ 无注入 ❌(不蒸馏)
  • peer 正确解 / 自身正确解:同 group_index(或 metadata.uid) 内 reward ≥ success_reward_threshold(默认 1.0)的成功回答,包装为 <successful_attempt>
  • 格式反馈仅在无 peer 时出现;有 peer 时一律只注入 peer 解、丢弃 feedback。
  • 截断优先于格式;普通算错且无 peer → opd_sample_mask=False,因折叠进共享 loss_masks 而不贡献任何 loss 梯度(纯蒸馏配置下基础 RL 梯度本就≈0,可观察效果集中在 OPD)。
  • 工程错误(payload shape/dtype、负 token id、teacher log-prob NaN/+inf)仍在边界校验处报错。

变更

CLI 与兼容性

位置 变化
--opd-feedback-class 绑定 OPD 家族算法的 EnvironmentFeedback 子类;不传默认 relax.utils.opd.feedback.OPDFeedback(plain OPD/MOPD:teacher 复用 student prompt)。
--opd-feedback-kwargs(新增) feedback 构造参数,JSON dict,schema 声明在接口层:teacher_prompt_key(OPSD 必填,数据集 teacher prompt 列名)、success_reward_threshold(SDPO 成功分数线,默认 1.0)。传未知字段在构造时报 TypeError
--opd-teacher-prompt-key(删除) 原 OPSD teacher prompt 列名 flag。该通路并入 --opd-feedback-kwargs {"teacher_prompt_key": ...},单一入口。
--opd-loss-coef / --opd-kl-coef --opd-loss-coef 1.0 --opd-kl-coef 0.0 表达纯蒸馏。
--opd-token-selection / --opd-jsd-alpha SDPO 用 student_topk;对称 JSD 配 --opd-kl-type jsd --opd-jsd-alpha 0.5
SDPO 校验 需 SGLang、--group-rmstudent_topk、正 opd_loss_coef、per-token loss;拒绝 PP/MTP/多模态。
普通 OPD/OPSD 继续用原有 token selection、teacher prompt、loss 配置;已有 launcher 零改动。

文件变更

文件 变更
relax/engine/rollout/on_policy_distillation.py OpdManagerload_feedback(path, kwargs) 构造 feedback;将 opd_sample_mask 纳入 SDPO transfer schema,在 teacher prefill/transfer 边界执行 SDPO payload 校验。
relax/utils/opd/feedback.py 接口层声明参数 schema 并新增 load_feedbackrelax/utils/opd/opsd/feedback.pyOPSDFeedback 赋值数据集 privileged prompt;relax/utils/opd/sdpo/feedback.py 依 reward/group context 生成 teacher prompt 与 sample mask(GoldenAnswerSDPOFeedback / CodeSDPOFeedback)。
relax/utils/opd/opd_utils.py 新增 --opd-feedback-kwargs、删除 --opd-teacher-prompt-key;在 compute_policy_opd_loss 增加 CP 感知归约分支(get_sum_of_sample_mean);普通 OPD 走原 reduce_opd_loss,数值路径不变。
relax/utils/types.py / relax/engine/rollout/data_source.py Sample 增加 opd_sample_mask 字段;ingestion 从 --opd-feedback-kwargsteacher_prompt_key 渲染数据集 teacher prompt 列。
examples/on_policy_distillation/sdpo/ 数据准备、reward 与按统一参数分组组织的训练启动脚本。

验证

可复制命令

聚焦单元测试:

python -m pytest -q \
  tests/utils/opd/test_feedback.py \
  tests/utils/opd/test_opd_topk_log_probs.py \
  tests/utils/opd/test_opd_legacy_regression.py \
  tests/engine/rollout/test_on_policy_distillation_payload.py \
  tests/utils/test_arguments_opd_teacher_colocate.py \
  tests/backends/megatron/test_opd_loss_aggregation.py \
  tests/backends/megatron/weight_update/test_lora_weight_sync.py \
  tests/distributed/ray/test_weight_sync.py \
  tests/utils/training/ \
  tests/examples/sdpo/test_prepare_data.py \
  tests/examples/sdpo/test_data_reward.py

Ray / SGLang teacher prefill 集成(需空闲训练 pod):

CUDA_VISIBLE_DEVICES=0,1 python -m pytest -q \
  tests/distributed/ray/test_opd_teacher_controller_colocate.py

语法与静态检查:

python -m compileall -q relax examples/on_policy_distillation/sdpo tests
bash -n examples/on_policy_distillation/sdpo/*.sh
git diff --check
pre-commit run --all-files

新增测试

范围 测试文件 验收内容
feedback routing tests/utils/opd/test_feedback.py reward/metadata feedback 写入 sample;SciKnowEval 仅同 group 共享成功解;无 context 回退原 prompt 并关 gate;普通 OPD/OPSD 不设 gate。
sample mask / legacy regression tests/utils/opd/test_opd_legacy_regression.py True 与普通 OPD 数值一致;False sample 不产生 OPD 梯度;全 False 时 loss/梯度为零;普通路径保持原 oracle。
teacher payload tests/engine/rollout/test_on_policy_distillation_payload.py opd_sample_mask 进 transfer;student Top-K 与 teacher payload 的 shape/内容边界正确。
Top-K gather tests/utils/opd/test_opd_topk_log_probs.py global token id、id 0、负 sentinel、越界输入处理正确。
arguments / colocate tests/utils/test_arguments_opd_teacher_colocate.py SDPO/OPD 参数校验、colocate 资源切分、CP/TP 约束正确。
OPD loss 聚合 tests/backends/megatron/test_opd_loss_aggregation.py CP=1/CP>1 下 OPD loss 聚合与 mask reduction。
weight sync tests/backends/megatron/weight_update/test_lora_weight_sync.pytests/distributed/ray/test_weight_sync.py teacher/actor 权重同步路径正确。
data / reward tests/examples/sdpo/test_prepare_data.pytests/examples/sdpo/test_data_reward.py 示例数据归一化与 SciKnowEval/ToolAlpaca reward payload 稳定。

单元、集成与端到端测试结果

环境:python -m compileallbash -n 全部通过(exit 0)。聚焦测试套件结果如下:

层级 结果
feedback routing(test_feedback 34 passed
sample mask / legacy regression(test_opd_legacy_regression 26 passed, 2 skipped
teacher payload(test_on_policy_distillation_payload 32 passed
Top-K gather(test_opd_topk_log_probs 2 passed
arguments / colocate(test_arguments_opd_teacher_colocate 10 passed
data / reward(test_prepare_data + test_data_reward 6 + 14 passed
OPD loss 聚合(test_opd_loss_aggregation 1 error*
weight sync lora(test_lora_weight_sync 23 passed, 3 skipped
weight sync ray(test_weight_sync 30 skipped(需 Ray 集群)
Ray/SGLang teacher prefill 集成(test_opd_teacher_controller_colocate 待训练 pod 验收
合计 147 passed, 35 skipped, 1 error

* test_opd_loss_token_mean 在其 fixture 调用 pytest.importorskip("torch", exc_type=...)
该 kwarg 在 pytest 9 已移除,属测试脚手架与 pytest 版本不兼容,非产品逻辑回归;同文件其余
用例因共享该 fixture 未能执行。

实际训练结果

仅汇报 SciKnowEval Biology 子集的 SDPO 训练(run ZiyiTsang/relax-sdpo/7f3n0ylw
wandb 记录至 step 400)。

训练环境介绍

配置
模型 Qwen3-8B(student 与 teacher,bridge 模式)
GPU 4× NVIDIA H100 80GB HBM3
并行 / 部署 TP=4, CP=1, PP=1;actor/rollout/teacher colocate
资源分配 actor [1,4],rollout SGLang [1,3],teacher SGLang [1,1]
优化器 Adam,lr=1e-6(constant),wd=0.01,clip-grad=1.0,CPU offload + selective offload
Batch global 256 / micro 1,n=8 samples/prompt,rollout-batch 32,num-rollout 5000
SDPO 关键参数 --opd-loss-coef 1.0 --opd-kl-coef 0.0(纯蒸馏),student_topk k=16,jsd α=0.5,--opd-norm-mode tail,teacher static
数据 sciknoweval/biology train.jsonl;eval-interval=5,n=16 eval samples/prompt
软件 PyTorch 2.9 / Megatron(bridge) / SGLang / Ray,CUDA 13.0

结果汇报(Biology · SDPO · ≤ step 400)

image
指标 初始 (step 0) 最终 (≈ step 400) 最佳 说明
eval acc(sciknoweval-biology) 34.7% 41.7%(step 392) 45.1%(step 228) +6.4pp final / +10.4pp peak(基线 34.7%)
rollout 组内 acc 41.9% mean 每 step 组内平均正确率
train/loss(纯蒸馏 OPD) 0.021 0.026 mean 0.029
valid distillation ratio(opd_sample_mask 0.69 0.69 0.97 mean 0.82,有有效 teacher target 的 sample 占比
throughput 2.6k tok/gpu/s mean 2.2k

注:rollout/rewards--opd-disable-rl-reward 接近 0,故以 eval/rollout 正确率作为
主指标。

风险与回退

已知限制

  • SDPO 仅支持文本、SGLang teacher、student_topk、无 PP/MTP 路径;多模态 teacher prompt
    allgather_cp=True Top-K 路径不在本 PR 范围。
  • 缺 solution/feedback 时 teacher prompt 回退原 prompt,teacher prefill 继续,但该 sample
    opd_sample_mask=False 不参与 OPD loss。
  • 真实 Ray/SGLang teacher actor、Megatron TP/CP forward/backward 与单数据集训练仍需训练 pod 验收。

风险

  • teacher prompt tokenize/offset 错误会使 response rows 错位;需结合
    test_on_policy_distillation_payload.py 与真实 teacher prefill 验收。
  • CP row layout 或 TP global id 错误可能产生 shape 正确但语义错误的 loss;需用现有 OPD
    layout / global-id contract 验证。

关闭开关或回退方式

  • 普通 GRPO baseline 用 examples/on_policy_distillation/sdpo/run-grpo.sh,不传 SDPO
    feedback 与 teacher 参数。

其他

复现过程中发现若干bug,已经提交PR解决:#287

检查清单

  • dynamic teacher prompt 在 group reward 完成后、teacher prefill 前构建。
  • opd_sample_mask 作为 sample-level transfer field 在 CP=1/CP>1 reduction 生效;fallback sample 不贡献 OPD 梯度。
  • 保留 response/token、vocabulary、sample 三层 mask 语义。
  • teacher 采用 static 模式(actor update 后不更新 teacher 权重)。
  • 示例脚本按 EVAL_ARGS/ROLLOUT_ARGS/OPD_ARGS/GRPO_ARGS/OPTIMIZER_ARGS/PERF_ARGS 分组组织。
  • 文档不含 checkpoint、数据集或机器私有绝对路径。
  • 聚焦单元测试 147 passed / 35 skipped / 1 error(error 为测试脚手架与 pytest 版本不兼容)。
  • Ray/SGLang teacher prefill 与 Megatron TP/CP GPU 验收待训练 pod 执行。
  • 单数据集几十步以上训练、有效 target 比例与吞吐/显存打点待补。

More info: issue #86

Yangruipis and others added 30 commits April 30, 2026 16:35
# 🐛 Bug Fix

## Fix torch.cuda patch broken by device abstraction refactor

- The device abstraction commit (632b29c) replaced hardcoded
  `torch.cuda.get_device_properties` / `torch.cuda.get_device_capability`
  patch targets with `torch.{device_utils.get_device_name()}.*`
- When no accelerator is available, `get_device_name()` returns `"cpu"`,
  so patches targeted `torch.cpu.*` instead of `torch.cuda.*`
- Megatron validate_args internally always calls `torch.cuda.*`,
  so the patches must target `torch.cuda` regardless of device abstraction
- Restore hardcoded `torch.cuda.*` patch targets with explanatory comment
# 🔩 Chore

## Remove dead helpers from data module

- Delete the unreferenced `filter_long_prompt` helper from `relax/utils/data/data.py`
- Delete the dead `_build_messages` helper that was shadowed by `relax/utils/data/data_utils.py`
- Delete the unused `process_rollout_data` helper and the imports it required
# 🐛 Bug Fix

## Keep multimodal prompt building non-destructive

- Build multimodal message content without mutating cached prompt rows
- Prevent reused raw samples from carrying expanded message content into later reads

## Support sliced eager dataset paths

- Parse per-file generalized slice syntax in eager file readers
- Keep multi-file eager path behavior aligned with streaming path semantics
The rollout component exits its main loop on the final training step, leaving the eval handler un-awaited. This caused a race condition where the controller's atexit shutdown tore down SGLang engines mid-flight. This fix blocks until the evaluation finishes at the end of training.
# ⭐ Feature

## Migrate from Megatron-LM to Megatron-Bridge

- Replace direct Megatron-LM checkout with Megatron-Bridge (commit 2faedbf6) in Dockerfile
- Upgrade transformer_engine from 2.10.0 to 2.14.1
- Archive old megatron patch (3714d81d) and add new patch for 20260506-85bced0ae

## Adapt Relax backend to Megatron-Bridge API changes

- Update vocab_size_with_padding import with fallback for new module path
- Rename enable_gloo_process_groups to use_gloo_process_groups
- Rename norm_epsilon to layernorm_epsilon in HF config validation
- Accept **kwargs in wrapped_provider for new model_provider signature
- Relax partition_stride assertion for GLU/SwiGLU linear_fc1 layers (stride=2)
- Guard checkpoint_write_patch against removed write_preloaded_data_multiproc
# ⭐ Feature

## Add Qwen3.6 model support with automatic expert format detection

- Add Qwen3.6-35B-A3B model configuration script with MoE parameters (256 experts, 8-way routing)
- Implement MTP MoE expert weight format detection in Qwen35VL bridge
  - Qwen3.5: per-expert storage (gate_proj/up_proj/down_proj per expert)
  - Qwen3.6: packed format (gate_up_proj/down_proj shared tensor)
- Add training script for Qwen3.6-35B-A3B 8xGPU colocate mode with multimodal support
- Extend Megatron bridge patch with format-aware weight mappings

---

# 🐛 Bug Fix

## Fix multimodal data counting and training script paths

- Fix remain_data counter for pre-structured multimodal content (was skipping already-processed items)
- Remove invalid dataset slice notation (@[0:1000]) from PROMPT_SET path in training script
# ⭐ Feature

## Add rollout reward field metrics

- Aggregate numeric fields from reward dictionaries during rollout logging
- Skip the primary reward key and raw_reward to preserve existing reward metrics
- Reuse the shared helper from the SGLang rollout metrics path
# 🐛 Bug Fix

## Strip consecutive `<|image_pad|>` tokens in pre-tokenized prompts

- Add `QwenVLImageProcessor._strip_image_token` static helper that collapses
  runs of `<|image_pad|>` (token id 151655) into a single placeholder while
  leaving the surrounding `<|vision_start|>`/`<|vision_end|>` markers intact.
- Apply the helper to `prompt` before calling `load_mm_data` in
  `process_mm_data_async`, so pre-tokenized `input_ids` (where each image is
  already expanded to N image-pad tokens, one per visual patch) no longer
  collide with `load_mm_data` re-expanding the placeholder itself. Without
  the collapse, the pipeline saw `N x M` image-pad tokens and miscounted
  positions, breaking mrope bookkeeping.
- Raw text (`str`) prompts are passed through unchanged.
# 🐛 Bug Fix

## Disable torch.compile around GatedDeltaNet QKV prep

- Wrap `_prepare_qkv_for_gated_delta_rule` with `torch._dynamo.config.patch(disable=True)` in the Megatron patch
- Avoids torch.compile failure on the Qwen3.6 GatedDeltaNet path

---

# ⭐ Feature

## Generalize unsplit-forward path to text-only Qwen3.6 / Qwen3.5

- Detect `Qwen3VLModel` at model build and set `args.uses_unsplit_forward`; the bridge model does CP+SP splitting internally for both VL and text-only Qwen3.5/3.6 sharing the same architecture
- Route unsplit tokens + tp*cp*2-aligned `cu_seqlens` through `forward_only` / `train_one_step` whenever the flag is on, not just for VL inputs
- Propagate the flag through `data.get_batch`, `loss.compute_advantages_and_returns`, `log_rollout_data`, and `stream_dataloader.post_process_rollout_data` so padding stays consistent

## Add Qwen3.6-35B-A3B 8xGPU DAPO-math training script

- New `scripts/training/text/run-qwen36-35B-A3B-8xgpu.sh` for sync GRPO training with TP=2/PP=2/CP=2/EP=4 and partial rollout
# 🐛 Bug Fix

## Propagate fp16 to SGLang Mamba conv dtype

- `relax/distributed/ray/genrm.py` and `relax/distributed/ray/rollout.py`:
  pass `SGLANG_MAMBA_CONV_DTYPE=float16` to the engine env when `--fp16` is
  set, so Qwen3.6 hybrid-Mamba layers use the matching dtype in rollout/GenRM
- `scripts/training/multimodal/run-qwen3-vl-30B-A3B-8xgpu.sh`: add
  `--fp16 --use-rollout-routing-replay --use-slime-router`
- `scripts/training/multimodal/run-qwen35-35B-A3B-8xgpu.sh`: add
  `--use-rollout-routing-replay --use-slime-router`; document why fp16 is
  intentionally left disabled for Qwen3.5

## Skip routing replay for MTP layers

- `relax/utils/training/routing_replay.py`: MTP routers exist in training
  but rollout (sglang) does not run MTP, so there is nothing to record or
  replay against. Install a pre-hook that clears the global
  `ROUTING_REPLAY` (so `compute_topk` falls through to the original impl)
  and skip registration in `all_routing_replays` to keep the per-layer
  accounting consistent
- Guard `compute_topk` against `ROUTING_REPLAY is None`

## Detect Ray 2.x head node by internal resource

- `relax/utils/utils.py::get_serve_url`: Ray 2.x auto-registers
  `node:__internal_head__` on the head node; legacy setups also tag it with
  a custom `head` resource. Accept either when scanning `ray.nodes()` so
  head IP discovery works on both

---

# 📝 Documentation

## Announce Qwen3.6 support in README

- `README.md` / `README_zh.md`: add 05/11/2026 news entry noting Qwen3.6
  series (text + VLM) support
# ⭐ Feature

## Auto-detect shared-GPU colocate sub-mode for GenRM

- Pick mode from GPU allocation: `R+G==A` keeps the existing split layout; `R==G==A` activates the new shared layout where rollout and genrm overlap on the same bundles. Other combinations are now rejected at startup with a clear error.
- Drop the bundle offset for genrm in shared mode so both engines schedule on the same `[0, A)` bundles, and lower genrm Ray fractional `num_gpus` default from 0.2 to 0.1 to leave room alongside rollout.
- Plumb `mem_fraction_static` through `--genrm-engine-config`; rollout keeps using `--sglang-mem-fraction-static`. Two engines can now split each GPU independently.
- Onload rollout weights and genrm KV in parallel inside `update_weights()` so both engines come back together before the next rollout step.

---

# 📝 Documentation

## Document the new GenRM colocate sub-mode (en + zh)

- Add a second ASCII architecture diagram for the shared layout and a sub-mode auto-detection table.
- Introduce a Shared-mode launch example with `mem_fraction_static` settings and a warning to keep the per-GPU sum < 1.0.
- Update Best Practices with sub-mode selection guidance and OOM troubleshooting for shared mode.
- Fix stale defaults in the sampling-config table (temperature 0.2 -> 0.1, max_response_len 1024 -> 4096) and add `ep_size` / `mem_fraction_static` to the engine-config table.
- Update the example launch script to demonstrate shared mode (rollout 0.5 + genrm 0.3, both on 8 GPU).

---

# 🔩 Chore

## Add py-spy multi-PID dump helper

- `scripts/tools/_pyspy_dump.sh` runs `py-spy dump` over a list of PIDs in one ray-job submission, used by the debug-hang skill to avoid per-PID submission overhead.
This commit integrates the glm_moe_dsa model, updates Megatron backend, sets up corresponding training scripts, and modifies entrypoint scripts (local.sh, ray-job.sh, spmd-multinode.sh) to support environment variable overrides, keeping the cleanup logic intact.
# 🔩 Chore

## Update torch_memory_saver dependency

- Switch source repo from `fzyzcjy/torch_memory_saver` to `redai-infra/torch_memory_saver` fork
- Pin to commit `afc13785c50119048e2dd8ac497cc9e29ec75bd4`
- Set `TMS_CUDA_MAJOR=12` build-time env var for CUDA 12 compatibility
# ♻️ Refactor

## Split path env vars in launcher scripts

- Introduce `MODEL_DIR` (HF weights / `--ref-load`) and `DATA_DIR`
  (`PROMPT_SET` / `--eval-prompt-data`) alongside `EXP_DIR`
  (`--load` / `--save`) across 31 training and example scripts
- Each variable is overridable independently; `MODEL_DIR` and
  `DATA_DIR` fall back to `EXP_DIR`, while `EXP_DIR` falls back to
  `MODEL_DIR` to preserve the legacy `export MODEL_DIR=/root` flow
- Wire `omni-16xgpu-async` defaults (`HF_CHECKPOINT`, `PROMPT_SET`,
  `EVAL_PROMPT_DATA`) through the new vars instead of `/path/to/...`
  placeholders so the convention is uniform

---

# 📝 Documentation

## Document the path variable convention

- Add a tip block in `docs/{en,zh}/guide/customize-training.md`
  describing the three directories and the fallback chain
- Update the example `--hf-checkpoint` / `--ref-load` snippet to use
  `${MODEL_DIR}` instead of `${EXP_DIR}`
…eyes script

# 🔩 Chore

## Align launch argument order in deepeyes run script

- Remove duplicate `--rollout-num-gpus-per-engine` definition in `examples/deepeyes/run_deepeyes.sh`.
- Keep the effective `--rollout-num-gpus-per-engine 2` from the SGLang config section and remove the earlier conflicting value.
# 🐛 Bug Fix

## Preserve DeepEyes multimodal state across partial resume

- Keep `sample.multimodal_inputs` as the read-only dataset input instead of appending observation images into the shared shallow-copy reference
- Make `_prepare_initial_inputs()` return the initial processor output as local `init_mm_train` without overwriting `sample.multimodal_train_inputs`
- Keep observation images in `current_image_data` and append observation processor chunks to `multimodal_train_inputs_buffer`
- Route the budget-exhausted early return through `_finalize_sample()` so resumed samples leave `generate()` with merged multimodal train inputs
# ⭐ Feature

## Auto-enable true-on-policy mode in fully-async

- Auto-enable `--true-on-policy-mode` in `slime_validate_args` when `fully_async` and `rollout_batch_size * n_samples_per_prompt == global_batch_size`, since the train forward log_probs equal what actor_fwd would produce
- Add `ROLES_FULLY_ASYNC_ON_POLICY` (no actor_fwd) and route to it from `process_role` when the mode is on
- Recompute `old_log_probs` inline in `policy_loss_function` via `log_probs.detach()`, recovering vanilla PG (ratio ≡ 1) while keeping TIS valid against rollout_log_probs
- Drop `log_probs` from required data fields in `MegatronTrainRayActor` and `Advantages` when actor_fwd is absent; treat `rollout_log_probs` as kl-zero template in advantages compute
- Make `Controller` fully-async weight-sync skip `actor_fwd` recv when the role is not registered, and skip the actor_fwd HTTP probe in the actor health check
- Fall back to `rollout_log_probs` for entropy logging in `log_rollout_data` when `log_probs` is unavailable

---

# 🔩 Chore

## Tune training scripts and runtime env

- Add `NVSHMEM_BOOTSTRAP_UID_SOCK_IFNAME` default in `scripts/entrypoint/local.sh`
- Drop `actor_fwd`/`reference` from resource specs in async scripts (35B/9B text, 9B openr1mm-mm) now that true-on-policy mode handles them
- Add `--log-probs-max-tokens-per-gpu` and adjust parallelism/recompute/MoE-dispatcher knobs across qwen3/qwen35/qwen36 scripts to fit larger micro-batches
# 🐛 Bug Fix

## PR #1889: validate MoE HF config when dense layers exist

- Add `_has_dense_moe_layers` / `_is_moe_config` helpers in
  `relax/backends/megatron/arguments.py`
- Validate `moe_intermediate_size` and `shared_expert_intermediate_size`
- Skip `intermediate_size` check when model is pure MoE with no dense layer
- Ref: THUDM/slime#1889

## PR #1880: fix per-actor HTTP POST concurrency split

- Replace node-count-only divisor with `len(nodes) * num_gpus_per_node`
- Use ceiling division `(c + n - 1) // n` to avoid losing concurrency budget
- File: `relax/utils/http_utils.py`
- Ref: THUDM/slime#1880

## PR #1873: avoid blocking the asyncio event loop on ray.get

- Replace `asyncio.to_thread(ray.get, obj_ref)` with direct `await obj_ref`
- Removes the hard ThreadPoolExecutor cap on parallel POSTs
- File: `relax/utils/http_utils.py`
- Ref: THUDM/slime#1873

## PR #1888: actor save_model must wake_up / sleep, not just reload PG

- `save_model` was calling `reload_process_groups()` without resuming
  `torch_memory_saver`, leaving GPU tensors unreachable when NCCL is
  triggered during checkpoint save
- Use `self.wake_up()` / `self.sleep()` instead so PG and TMS stay in sync
- File: `relax/backends/megatron/actor.py`
- Ref: THUDM/slime#1888

## PR #1882: disaggregate PPO must reconnect rollout NCCL across sleep

- Add `disconnect_rollout_engines` to `UpdateWeightFromDistributed`
- In `actor.sleep()`, tear down weight-sync NCCL group for disaggregate
  PPO (`use_critic` + not `colocate`) before `destroy_process_groups()`
- In `actor.update_weights()`, force `wake_up` + `connect_rollout_engines`
  + `sleep` when the disconnect path was taken
- Drop the buggy `args.use_critic` GPU-offset branch in
  `sglang_engine.get_base_gpu_id`
- Auto-enable `offload_train` when `use_critic` is on
- Files: `relax/backends/megatron/actor.py`,
  `relax/backends/megatron/weight_update/update_weight_from_distributed.py`,
  `relax/backends/sglang/sglang_engine.py`, `relax/utils/arguments.py`
- Ref: THUDM/slime#1882

## PR #1878: reinitialize critic output_layer when ckpt shape mismatches

- Detect missing or shape-mismatched `output_layer.{weight,bias}` in the
  critic checkpoint metadata before / after `load_checkpoint`
- Reinitialize with `normal_(0, 0.02)` for weight and zero for bias, plus
  `optimizer.reload_model_params()` for fp16/bf16 master sync
- Gated on `role == "critic"`, no effect on actor or non-PPO algorithms
- File: `relax/backends/megatron/model.py`
- Ref: THUDM/slime#1878

---

# ⭐ Feature

## PR #1890: add missing spec / prefix-cache rollout metrics

- Wire `_compute_spec_metrics` and `_compute_prefix_cache_metrics` into
  `compute_metrics_from_samples`
- File: `relax/distributed/ray/rollout.py`
- Ref: THUDM/slime#1890

---

# 🔩 Chore

## PR #1862: improve `slice_log_prob_with_cp` assert message

- Include `len(log_prob)`, `response_length`, `total_length` in the assert
  so failures are diagnosable
- File: `relax/backends/megatron/cp_utils.py`
- Ref: THUDM/slime#1862

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# ⭐ Feature

## Add --custom-prompt-path for prompt transformation hook

- Add CLI argument in add_data_arguments (arguments.py)
- Load custom function via load_function in data_source.py
- Thread custom_prompt_func through build_messages, process_raw_sample,
  BaseDataset, Dataset, and StreamingDataset
- Custom function is called after prompt extraction, before
  conversation/multimodal processing

## Add --image-resize-scale-factor for image dimension alignment control

- Add CLI argument in add_data_arguments (arguments.py)
- Add image_resize_scale_factor field to MultimodalConfig with
  from_args propagation and getter function
- Update fetch_image with 3-way logic: None uses default patch_factor,
  0 disables alignment, positive int uses custom value

---

# 📝 Documentation

## Update configuration reference docs

- Add --custom-prompt-path to Dataset table (EN + ZH)
- Add --image-resize-scale-factor to Multimodal Data table (EN + ZH)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# ⭐ Feature

## Add hybrid training mode combining async data pipeline with colocate weight sharing

- Introduce `--hybrid` flag that sets `fully_async=True` and `colocate=True`
  so actor/ref/actor_fwd share GPUs via TensorBackuper+_switch_model while
  rollout runs on a separate GPU placement group with streaming transfer queue
- Add `train_hybrid()` method in MegatronTrainRayActor: collects sub-batches
  from transfer queue, runs ref/teacher/actor forward per sub-batch, merges
  all sub-batches, computes advantages with correct global normalization,
  then trains on the full merged batch
- Register hybrid mode in `process_role()` to use ROLES_COLOCATE (actor +
  rollout only, no separate reference/actor_fwd services)
- Update controller to skip shared placement groups and fully-async DCS
  weight sync setup when hybrid is active
- Skip actor_fwd health probe in `_check_services_health` for hybrid mode
  to avoid spurious warnings
- Add `train_hybrid()` dispatch in RayTrainGroup and Actor component
- Validate argument combinations: `--hybrid` is the supported way to combine
  async pipeline with colocate weight sharing; bare `--fully-async --colocate`
  now raises ValueError
- Set `offload_train=False`, `offload_rollout=False`, and
  `compute_advantages_and_returns=True` for hybrid mode
- Add Qwen3-4B 8xGPU hybrid-async training launch script
# ⭐ Feature

## Add `relax/utils/visualize` rollout result viewer

- Add web viewer adapted from rlsp/utils/visualize: FastAPI + single-page UI
  for browsing `<save>/rollout_result/{train,eval}/{step}.jsonl`, with
  step dropdown, sample nav, sort by reward / response_length, sample-info
  card, and prompt / response / label rendering with chat-template / tool-call
  / `<think>` highlighting
- Auto-discover `train/` and `eval/` subdirs and render a tab toggle when both
  exist; fall back to a single anonymous bucket for flat dirs
- Add terminal UI mode (`--tui`) adapted from redaccel/verl reward_viewer_v2:
  sync-load the first step then stream remaining steps via a daemon thread
  (newest first), with step / sample / dataset / sort dropdowns, field
  filter, fuzzy search (`f`/`enter`/`esc`), vim-style page nav, and
  text/table render toggle
- Default theme switched to dark; header shows the Relax wordmark linking
  to the GitHub repo plus a GitHub icon
- Mask multimodal pad tokens (`<|image_pad|>`, etc.) by default in the TUI
  via `--mask-str`

## Add `relax/entrypoints/visualize` thin wrapper

- One command for both modes: `python -m relax.entrypoints.visualize <dir>`
  (web) and `... --tui` (terminal)
- `DATA_DIR` is a required positional argument
- TUI dependencies (`textual`, `rich`) are lazy-imported; clear error
  message if missing

---

# 📝 Documentation

## Add bilingual rollout result viewer guide

- Add `docs/{en,zh}/guide/rollout-result-viewer.md` covering data layout,
  launch command, flags, page features, terminal UI key bindings, and
  reverse-proxy notes
- Register the new pages under the existing "Operations & Debugging" /
  "运维与调试" sidebar group in `docs/.vitepress/config.mts`
- Add `docs/public/relax-viewer.png` screenshot (palette-compressed PNG
  to stay under the 500 KB pre-commit limit)
# 🐛 Bug Fix

## Fix MODEL_DIR/EXP_DIR initialization in qwen35-9B hybrid-async script

- Replace buggy `EXP_DIR="${MODEL_DIR:=...}"` side-effect assignment with separate `EXP_DIR`/`MODEL_DIR`/`DATA_DIR` defaults, matching `run-qwen35-9B-8xgpu-openr1mm-async.sh`
- Point `--hf-checkpoint` / `--ref-load` at `${MODEL_DIR}` and `PROMPT_SET` at `${DATA_DIR}` so model and dataset roots can be overridden independently of the experiment output dir

(cherry picked from commit 466c779)
# 🔩 Chore

## Align DeepEyes dataset inputs

- Align fp16, GenRM, and partial-rollout scripts with examples/deepeyes/run_deepeyes.sh
- Use the same Deepeyes v1 training shards as the main script
- Use the same thinklite reasoning accuracy eval slice as the main script

(cherry picked from commit df613b4)
# 📝 Documentation

## Add bilingual Hybrid training mode guide

- Add `docs/en/guide/hybrid-training.md` and `docs/zh/guide/hybrid-training.md` describing the hybrid execution mode (streaming TransferQueue + in-process TensorBackuper weight sharing)
- Cover mode comparison vs Colocate / Fully Async, role layout (`ROLES_COLOCATE` with disjoint actor/rollout placement groups), `--hybrid` flag resolution, and the three-phase `train_hybrid` loop
- Document required and optional flags (`--hybrid`, `--num-iters-per-train-update`, `--max-staleness`, `--balance-data`) and the default overrides applied in `relax/utils/arguments.py`
- Include the 8-GPU multimodal reference launch from `scripts/training/multimodal/run-qwen35-9B-8xgpu-openr1mm-hybrid-async.sh` and troubleshooting tips (stalled sub-batches, balance-data rejection)
- List planned next steps: integrate DCS for weight sync, split `train_actor` by `num_iters_per_train_update`

## Register pages in VitePress sidebar

- Add Hybrid Training Mode under the Advanced group in both `en` and `zh` sidebars in `docs/.vitepress/config.mts`

(cherry picked from commit 7f78ff7)
# ⭐ Feature

## Add JSON provider config dump

- Keep the existing transformer_config.pkl dump for compatibility
- Also write transformer_config.json next to it for easier inspection
- Convert non-JSON-safe values recursively and fall back to str() when needed

(cherry picked from commit 598c15c)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- restore configs/env.yaml (clean upstream template) so the PR does not delete it; local secret-bearing copy stays on disk untracked-by-intent
- drop sdpo-only .gitignore entries (configs/env.yaml, wandb/, scripts/training/sdpo/) to match upstream/main
- remove tests/utils/opd/test_opd_legacy_regression.py (not needed)
- ruff, ruff-format, mdformat, docformatter, clang-format fixes
- covers sdpo README, megatron data.py, opd/metrics/tracking utils
Copilot AI review requested due to automatic review settings August 22, 2026 10:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 22, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

# 🐛 Bug Fix

## Score tool-use trajectories as ordered (Action, Input) pairs

- _extract_tool_calls parses (Action, Action Input) pairs in document
  order instead of merging every input into one flat dict;
  _golden_tool_call returns the same ordered pair structure
- Correctness is now format_ok and predicted pairs == golden pairs, so
  step order and per-step Action<->Input pairing matter: 37% of the
  tooluse rows are multi-step and 12% carry the same input key with
  different values across steps, which the old merged-dict comparison
  could not distinguish from a correct trajectory

# ✅ Tests

- Add order-swap, cross-step value-swap and exact multi-step cases
# ♻️ Refactor

## Route every algorithm difference through EnvironmentFeedback hooks

- Split feedback implementations per algorithm: feedback.py (base +
  OPD), opsd_feedback.py (OPSD), sdpo/feedback.py (SDPO); base-class
  defaults reproduce plain OPD so OPD/MOPD need zero overrides
- Delete all 21 is_sdpo branches from OpdManager together with the
  is_sdpo_feedback / is_sdpo_prompt_routing_enabled reflection;
  --opd-feedback-class now defaults to OPDFeedback
- SDPO preflight (text-only check, stale-payload clearing, teacher
  prompt presence) moves into SDPOFeedback.prepare_teacher_prompts;
  teacher fetch/response failures degrade like plain OPD and surface
  via the assembly-time check_transfer_channels validation
- GenerateState takes its feedback from OpdManager; the
  generate_and_rm_group call sites are unchanged
- Rebind example launchers: vision_opd/mopd -> opsd_feedback.
  OPSDFeedback, sdpo -> sdpo.feedback.*, math_opd uses the default

## Drop the OPD top-K context-parallel slicing

- Delete slice_opd_topk_rollout_fields and its call sites; launch
  validation rejects top-K token selection combined with CP > 1 /
  dynamic CP / allgather-cp
- Restore cp_utils get_cp_local_num_tokens to upstream shape (the int
  cast lives on fix/cp-num-tokens-int) and restore the
  stream_dataloader / metrics / tracking churn to upstream
- Gate _apply_opd_sample_mask behind use_opd so non-OPD runs skip it

# ✅ Tests

- Update payload / feedback / arguments tests to the hook contract
- Add OPD no-op contract, SDPO escalation hooks, default-class
  resolution, top-K x CP rejection and SDPO launch validation coverage
# ♻️ Refactor

## Make EnvironmentFeedback concrete with behavioral defaults

- Replace the abstractmethod pair with defaults: record_sample_feedback records reward-dict env feedback and prepare_teacher_prompts clears sample.teacher_prompt/opd_sample_mask; OPDFeedback becomes an explicit alias subclass

## Route the OPSD dataset privilege through OPSDFeedback

- process_raw_sample stores the rendered teacher prompt under metadata["opd_teacher_prompt"] instead of Sample(teacher_prompt=...)
- OPSDFeedback.prepare_teacher_prompts assigns metadata["opd_teacher_prompt"] -> sample.teacher_prompt at rollout time; samples without it keep the OpsdWorker student-prompt fallback

## Run both feedback hooks on every OPD rollout path

- Add _record_feedback_and_prefill in sglang_rollout; the group_rm branch and both non-group branches (multi-sample / single-sample) now share record -> prepare -> prefill
- Delete SDPOFeedback.record_sample_feedback override and its module helper (base default is equivalent)

---

# ✅ Tests

## Pin the new feedback contract

- Base recording applies to OPD/OPSD/SDPO subclasses alike
- OPSD assigns privilege from metadata; missing key leaves teacher_prompt None
- process_raw_sample surfaces teacher prompt as metadata instead of the Sample field
Copilot AI review requested due to automatic review settings August 24, 2026 10:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

# Conflicts:
#	relax/backends/megatron/model.py
# 🐛 Bug Fix

## Restore Megatron/sglang-free import isolation

- Stub sglang_router and relax.backends.sglang.arguments before importing
  relax.utils.arguments so the CPU CI runners (no sglang installed) can
  collect the opd teacher colocate test suite
- Keep the sdpo-specific assertions (per-token loss, feedback class,
  allgather-cp rejection) on top of the restored fixture
# ♻️ Refactor

## Remove unused TopkWorker.TRANSFER_FIELDS

- Superseded by topk_transfer_fields(); no consumers remain

# ✅ Tests

## Remove ghost attrs from colocate arguments test

- Drop opd_mask_on_success and opd_log_prob_dump_dir kwargs
  that the argument parser no longer defines
# ⭐ Feature

## Unify the sdpo example launchers

- Source examples/on_policy_distillation/sdpo/env.sh in every
  4xgpu launcher
- Add --teacher-sglang-enable-weights-cpu-backup to every launcher
- Align training and eval params with the biology script
  (5000 rollouts, batch 32, 8 samples per prompt, global batch
  256, eval every 5 iters with 16 samples per eval prompt)
- Wire the eval split into the toolalpaca launcher

---

# ♻️ Refactor

## Stop writing unused choices metadata

- prepare_data.py no longer emits a never-consumed choices field

---

# 📝 Documentation

## Update the sdpo README

- Correct model size, resource layout, config table and eval flow
- Document the env.sh contract sourced by every launcher
Copilot AI review requested due to automatic review settings August 27, 2026 16:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ZiyiTsang ZiyiTsang mentioned this pull request Aug 29, 2026
9 tasks
Comment thread relax/engine/rollout/sglang_rollout.py Outdated

# OPD manager (singleton — one OpdManager per GenerateState)
self.opd_manager = opd.OpdManager(args) if opd.is_opd_enabled(args) else None
self.feedback = self.opd_manager.feedback if self.opd_manager is not None else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是否可以直接opd_manager.feedback?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已改


OPD_ARGS=(
--use-opd
--opd-feedback-class relax.utils.opd.opsd.feedback.OPSDFeedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是否可以保持其他的recipe没有变动?且mopd也不是opsd

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已改。确实疏忽

# ♻️ Refactor

## Remove GenerateState.feedback alias

- Delete the `self.feedback` mirror of `opd_manager.feedback` in GenerateState
- Access `state.opd_manager.feedback` directly inside `_record_feedback_and_prefill`
- Rollout behavior is unchanged; the OPD injection point stays between rm and prefill
# ♻️ Refactor

## Revert feedback-class flags in existing recipes

- Drop `--opd-feedback-class OPSDFeedback` from the three mopd and three
  vision_opd launchers: none passes `--opd-teacher-prompt-key`, so the flag
  never had an effect there and MOPD/vision recipes are not OPSD
- Restore `--sglang-disable-cuda-graph` in the three mopd launchers
- The 2B mopd launcher now matches main byte-for-byte
# ⭐ Feature

## Interface-level feedback parameter schema

- Declare the constructor schema once on EnvironmentFeedback
  (teacher_prompt_key, success_reward_threshold); the selected subclass
  binds the same --opd-feedback-kwargs dict at construction time
- Add load_feedback(path, kwargs) with loud TypeError on unknown fields;
  OPD default stays OPDFeedback with empty kwargs
- OPSDFeedback now requires teacher_prompt_key and validates the sglang
  teacher path in validate_launch_args
- SDPOFeedback consumes success_reward_threshold (default 1.0) instead of
  a hardcoded score >= 1.0 boundary

---

# 🐛 Bug Fix

## Drop the --opd-teacher-prompt-key engine flag

- The flag silently lost its effect when the teacher prompt moved to the
  feedback strategy; the key now enters only via --opd-feedback-kwargs
- OpsdWorker.from_args activates OPSD from image keys alone; prompt-key
  routing is owned by OPSDFeedback

---

# ♻️ Refactor

## Collapse empty SDPO domain subclasses

- Merge SciKnowEvalSDPOFeedback and ToolUseSDPOFeedback (both were `pass`)
  into GoldenAnswerSDPOFeedback for static golden-answer text datasets;
  keep CodeSDPOFeedback as the raising placeholder
- Point the six SDPO launchers and the README at the merged class
- Ingestion reads teacher_prompt_key from --opd-feedback-kwargs in
  data_source; rendering and metadata hand-off are unchanged

---

# ✅ Tests

## Cover kwargs binding and the merged classes

- Add load_feedback binding/error tests and a success-threshold test
- Update payload, feedback, and arguments tests for the new flag and class
Copilot AI review requested due to automatic review settings August 30, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

# 🎨 Style

## Make mdformat and docformatter hooks idempotent

- Re-wrap GoldenAnswerSDPOFeedback docstring to satisfy docformatter 1.3.1 (--wrap-descriptions 79)
- Shrink the Feedback column of the launcher table by one cell width to satisfy mdformat 0.7.9
Copilot AI review requested due to automatic review settings August 30, 2026 14:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.