先画清采样与训练的交接面——QwenGyre 接入在线强化学习的顺序

文章导读
要把 QwenGyre 接进在线强化学习,先别急着改算法,先把采样与训练之间的交接面画出来:采样一侧交出什么,训练一侧期望收什么,键名和形状是否对得上。交接面一旦明确,接入顺序就从“两端同时改、出错不知道查哪边”变成“从左到右逐段验证”。
📋 目录
  1. A 把一次采样回合的产出物写清楚
  2. B 确认训练侧接收的数据结构
  3. C 做一次字段级对齐并记录差异
  4. D 按先单侧、再半程、后全流程的顺序接入
  5. E 记录一次完整循环的时间与计数
A A

要把 QwenGyre 接进在线强化学习,先别急着改算法,先把采样与训练之间的交接面画出来:采样一侧交出什么,训练一侧期望收什么,键名和形状是否对得上。交接面一旦明确,接入顺序就从“两端同时改、出错不知道查哪边”变成“从左到右逐段验证”。

如果采样产物无法被训练侧原生读取,先写一层字段映射,不要改训练循环。适用场景是已有训练流程、准备挂上在线采样;操作动作是先固定采样回合字段,再对照 dataloader 或 train_step 的键名,最后按单侧、半程、全流程三步接入;验证方式是每步打印字段清单、形状断言和计数;风险边界是字段语义在不同仓库可能不一致,例如 done 到底是终止还是截断,必须以 QwenGyre 仓库文档为准。

把一次采样回合的产出物写清楚

交接面左侧先定字段。一次采样回合建议至少包含以下内容,命名以仓库文档为准,这里只给语义清单:

  • 状态:观测(observation / obs)的形状与 dtype;多模态时把图像、文本等键分开记录。
  • 动作:实际执行的动作值;如果用得上,再带 logprob、entropy 这类分布参数,用不上可以先不写。
  • 奖励:即时奖励与折扣累计回报(return)分开,不要混成一个键。
  • 时间步:step 序号、回合序号(episode_id)、是否终止(done / terminated / truncated 需要确认哪个是哪个)。
  • 辅助:策略版本号或采样权重版本号,便于后续排查新旧策略混批。

最小可行产物可以落成 JSONL,一行一个时间步。先把一个完整回合打出来看,确认没有 None、没有维度跳动,再谈批量。

确认训练侧接收的数据结构

训练侧要什么,去看数据加载处而不是看论文。通常有两处可查:dataloader 的 __getitem__ / collate_fn,以及 train_step 里第一次用到 batch 的地方。把 batch 键打出来是最直接的核对方式:

先画清采样与训练的交接面——QwenGyre 接入在线强化学习的顺序
for batch in loader:
    print(type(batch))
    if isinstance(batch, dict):
        print({k: (v.shape if hasattr(v, 'shape') else type(v))
               for k, v in batch.items()})
    break

对照表建议写成两列:训练侧键名、期望形状与 dtype。常见形如 obs、action、reward、done、advantage、logprob、mask。注意批次维度:单步采样往往是 [D],成批之后通常是 [B, T, D]。只有先确认 B 和 T 的含义,后面 advantage 的广播才不会错位。

做一次字段级对齐并记录差异

两侧字段常常不同名。与其在训练循环里到处改名,不如在读入前放一层映射函数,把差异集中暴露。

先画清采样与训练的交接面——QwenGyre 接入在线强化学习的顺序
FIELD_MAP = {
    'obs':        'observation',
    'act':        'action',
    'rew':        'reward',
    'terminated': 'done',
    'ep_id':      'episode_id',
    't':          'step',
}

def to_train_batch(rollout):
    out = {}
    for train_key, sample_key in FIELD_MAP.items():
        if sample_key not in rollout:
            raise KeyError(f'missing sample field: {sample_key}, '\
                           f'have {list(rollout)}')
        out[train_key] = rollout[sample_key]
    return out

键缺失通常表现为 KeyError,或者 collate 阶段的 NoneType、形状不匹配报错。如果用了默认值静默填充,会更隐蔽——训练能跑但回报曲线异常,所以建议宁可直接报错,不要补零。映射写完后把差异分三档记录:同名同形状、改名同形状、需要转换(例如把 terminated 和 truncated 合成 done)。

按先单侧、再半程、后全流程的顺序接入

接入按三步走,每步只允许改一侧,失败时能定位。

  1. 单侧:只跑采样,不接训练。用固定策略采若干个回合,打印字段清单和 shape。通过判据是每个回合字段齐全、无 None、shape 与文档一致、计数器递增正常。
  2. 半程:把采样产物喂给映射函数与 dataloader,跑一次前向,不更新参数。通过判据是 batch 键齐全、形状为 [B, T, D]、loss 能算出来且不是 NaN。
  3. 全流程:打开参数更新,跑若干个采样—训练周期。通过判据是循环能正常结束、计数与时间戳对得上、没有新旧策略版本混批。

每步失败,先判断是采样侧字段缺失,还是训练侧形状期望不一致,不要同时改两边。

先画清采样与训练的交接面——QwenGyre 接入在线强化学习的顺序

记录一次完整循环的时间与计数

为后续调参留基线,需要在采样开始、采样结束、训练开始、训练结束四点各打一次时间戳,并带上计数。

import time
t0 = time.time()
# ... rollout ...
t1 = time.time()
# ... train step ...
t2 = time.time()
print(f'rollout_s={t1-t0:.3f} train_s={t2-t1:.3f} '
      f'steps={n_steps} episodes={n_episodes} batch={B}x{T}')

记录成表格的字段:run_id、rollout_s、train_s、total_s、n_steps、n_episodes、batch_shape、policy_version、备注。备注里只写能复现的现象,例如“done 由 terminated 映射”。同一配置重复几次,看波动范围,再决定调采样批量还是训练批量。