QwenGyre 的在线采样不能当回放池用 / 轨迹时效先弄清

文章导读
把在线采样得到的轨迹全存下来反复喂给训练,在 QwenGyre 这类以在线 rollout 为主的框架里通常不等于“回放池”。在线采样得到的是当前策略附近的样本,采样步数一旦往前推进,旧轨迹对应的策略就和正在训练的模型不是一个版本了。是否能用、能用几次,取决于轨迹从采样到被消费之间隔了多少步;判断顺序建议是:先在配置里量出采样与消费的节奏,再做复用次数对比,最后把时效标记和丢弃逻辑落到代码里、用日
📋 目录
  1. 壹 在配置里找出控制采样与消费节奏的项
  2. 贰 对比同一批轨迹复用一次与复用多次的差异
  3. 叁 给每条轨迹打上时效标记
  4. 肆 验证丢弃逻辑确实生效
A A

把在线采样得到的轨迹全存下来反复喂给训练,在 QwenGyre 这类以在线 rollout 为主的框架里通常不等于“回放池”。在线采样得到的是当前策略附近的样本,采样步数一旦往前推进,旧轨迹对应的策略就和正在训练的模型不是一个版本了。是否能用、能用几次,取决于轨迹从采样到被消费之间隔了多少步;判断顺序建议是:先在配置里量出采样与消费的节奏,再做复用次数对比,最后把时效标记和丢弃逻辑落到代码里、用日志验证。

在线采样轨迹可以直接用于训练,但不宜默认当作长期回放池。先确认采样批次、缓冲区容量、消费频率三类配置,算出一轮周转大概要多少步;再用固定随机种子的短跑对比复用 1 次与多次的差异;然后在采样时写入步数或时间戳,消费时按阈值丢弃过期轨迹,并用丢弃条数、实际参与训练条数、采样总数三者对账。阈值需结合自己的更新频率确认,短跑只反映趋势,不能外推为长训练结论。

在配置里找出控制采样与消费节奏的项

轨迹会不会积压、会不会被反复取用,由三类配置共同决定:一次采样产生多少条轨迹、缓冲区能存多少条、训练侧多快把缓冲区取空。参数名各仓库写法不同,建议直接在配置目录里搜关键词,而不是凭记忆猜。

# 在仓库根目录执行,先看清有哪些相关项,再逐项确认含义
 grep -rn -E "replay|buffer|queue|maxlen|capacity|pool" configs/ | head -50
 grep -rn -E "rollout|sample_batch|n_samples|batch_size|group_size" configs/ | head -50
 grep -rn -E "train_|update_|steps_per|interval|ratio|reuse" configs/ | head -50

三个方向分别对应:采样批次决定轨迹产生的速度,缓冲区容量决定最多能积压多少条,消费频率(每次训练消耗多少条、多少次采样触发一次训练)决定消耗速度。把找到的键名和值抄成一张小表,标注“产生速度 / 容量 / 消耗速度”,能直接看出缓冲区是在涨还是在清空。若容量远大于单轮消耗量,旧轨迹被重复取到的概率就高,这时才需要往下看时效问题。具体键名以仓库实际配置为准,改之前先在版本控制里留一份原始配置。

对比同一批轨迹复用一次与复用多次的差异

不要直接接受“复用多了会崩”或“复用几次没问题”的说法,用一次可控的短跑自己看。做法是:固定随机种子,除复用次数外其余配置完全一致,先跑“取出一条即用一次”,再跑“同一批轨迹连续用于若干次训练”,两次跑相同的采样步数后比较训练侧记录。

  • 需要固定的项:随机种子、模型初始权重、采样批次、缓冲区容量、总采样步数、评测间隔。
  • 需要记录的字段:训练 loss(含 policy loss 与 value loss,若框架有)、entropy、KL 或重要性比相关的裁剪比例、采样侧 reward 或 return 均值、当前采样步数、每条轨迹实际被复用次数、缓冲区剩余条数。
  • 观察方式:把训练侧指标的曲线和采样侧 reward 均值画在同一时间轴上。复用次数提高后,若训练侧 loss 明显下行而采样侧 reward 均值不跟随,或 entropy 下行速度变快、裁剪比例持续偏高,说明轨迹已经偏离当前策略,应回到复用一次。

这里要留个边界:短跑只用来判断趋势和方向,样本量小、步数少,不能据此断言长训练也会这样。结论要写成“在当前配置和步数范围内观察到什么”,而不是绝对判断。

给每条轨迹打上时效标记

靠人记“这批轨迹大概是几步前采的”不可行,必须在写入时就落下字段。下面是与框架无关的最小骨架,字段名和缓冲区对象按实际代码替换,插入位置是采样产出轨迹、进入缓冲区之前。

import time

MAX_AGE_STEPS = 3        # 允许的最大年龄,单位是采样步

def make_record(payload, sample_step, policy_ver=None):
    return {
        'data': payload,
        'sample_step': sample_step,        # 采样完成时的全局采样步
        'wall_clock': time.time(),         # 便于排查卡顿或异步延迟
        'policy_ver': policy_ver,          # 框架有版本号/权重哈希就记,没有就置 None
    }

# 消费侧:取一批用于训练,顺手把过期的丢掉
def take_batch(buf, batch_size, current_step, max_age=MAX_AGE_STEPS):
    keep, dropped = [], 0
    while buf and len(keep) < batch_size:
        rec = buf.popleft()
        age = current_step - rec['sample_step']
        if age > max_age:
            dropped += 1
            continue
        keep.append(rec)
    log.info('dropped_stale=%d taken=%d buffer_left=%d', dropped, len(keep), len(buf))
    return keep

替换项有三个:MAX_AGE_STEPS 按自己在第一节能算出的周转步数定,不确定时先取一个小值;popleft 换成实际缓冲区的取数接口;log.info 换成框架的日志器,字段名尽量保持稳定,后面要靠它统计。丢弃旧轨迹只是让训练数据不偏离当前策略,不是让训练更快的办法,别把它当性能优化写进说明。

验证丢弃逻辑确实生效

策略写了但没被执行是最常见的情况:消费路径走了分支、缓冲区换了实现、日志打在了不生效的分支里,都会让丢弃看起来“好像在工作”。验证要从日志数字对账做起。

  1. 日志里至少要能看到三个量:dropped_stale(本轮被丢弃轨迹条数)、taken(实际参与训练的条数)、采样侧累计的 sampled_total。
  2. 对账关系:累计采样总数 ≈ 累计 taken + 累计 dropped_stale + 缓冲区剩余条数。差额明显就要查是否有别的消费路径绕过了这段逻辑。
  3. 做一次阈值扰动:把 MAX_AGE_STEPS 调到很小,丢弃条数应明显上升;调大后丢弃条数应降到接近零。两次都没有变化,基本可以判定这一段没有真正被调用。
# 从训练日志里汇总丢弃量,确认与采样总数能对上
 grep -o 'dropped_stale=[0-9]*' train.log | awk -F= '{s+=$2} END{print "total_dropped="s}'
 grep -o 'taken=[0-9]*' train.log | awk -F= '{s+=$2} END{print "total_taken="s}'

对账通过之后,再回头看第二节的对比记录:如果复用次数高的那一组在开启丢弃后曲线向复用一次的组靠拢,说明时效控制起了作用;如果没有靠拢,问题多半在缓冲区容量或消费频率本身,而不在丢弃阈值上。阈值放在哪个数合适,需要结合自己的采样步长、单步耗时和训练评测周期确认,没有通用值。