【Agentic RL】Dressage Staleness 控制深度分析
【Agentic RL】Dressage Staleness 控制深度分析文章目录【Agentic RL】Dressage Staleness 控制深度分析0x00 概要0x01. Staleness 是什么问题1.1 问题背景1.2 两个不同来源的 Staleness1.3 核心矛盾0x02. 总体设计整体设计哲学从防到治两个粒度正交约束0x03. Trajectory 粒度约束单条轨迹的版本跨度3.1 前提Partial Rollout 在 Proxy 层的透明中断续写3.2 问题反复中断带来 Trajectory 粒度的 Staleness3.3 控制逻辑版本跨度上限3.4 超限后的处理0x04. Batch 粒度约束进入 batch 的版本窗口4.1 对外接口4.2 版本判定是轨迹级的4.3 核心机制推进窗口 回溯复查4.4 两个关键设计选择选择一版本号按观测顺序排列选择二整 GRPO Group 丢弃4.5 不完整元数据的容错0x05. 两个粒度的关系与互补5.1 训练侧的 Token-Level Loss Mask5.2 配套机制0x06. 目前缺点与优化方向6.1 当前缺点6.2 优化方向0x07. 架构图7.1 整体架构7.2 Staleness 组件关系7.3 数据流图Staleness 相关0x08. 总结0xFF 参考0x00 概要https://github.com/Accio-Lab/Dressage 是非常有特色的 Agent RL 系统。本文就学习 Dressage 的 Staleness 实现。0x01. Staleness 是什么问题1.1 问题背景异步强化学习中Rollout数据采集与 Training模型更新是并行执行的。Trainer 一边用当前策略更新权重一边让 Rollout 工作器用旧权重持续采样。当一个 Rollout 批次最终送入 Trainer 时生产这批样本所用的权重可能已更新数轮——这就是Staleness陈旧性。时间轴 t1: 权重 v1 → Rollout 开始用 v1 采样 t2: 权重 v2 ← Trainer 完成一次更新 t3: 权重 v3 ← Trainer 又完成一次更新 t4: Rollout 采样结束 → 样本送入 Trainer 但样本基于 v1 生成当前权重已是 v3 ↑ 这就是 staleness gap1.2 两个不同来源的 StalenessStaleness 在异步链路中有两个不同来源必须分开讨论来源粒度成因后果单条长轨迹内部Trajectory 级Partial rollout 下一条轨迹被反复中断续写前后 token 来自不同权重版本陈旧前缀决定状态分布即使只在最新 token 上算 loss样本仍从陈旧上下文续写Batch 组装时Batch 级异步采样与训练并行Rollout 持续产出已完成的轨迹在队列中等待时权重已更新多次Train-Inference Gap 扩大梯度方向偏置极端情况下训练发散1.3 核心矛盾异步带来吞吐但吞吐带来了陈旧性。吞吐越高Rollout 与 Training 的重叠越多Staleness 就越严重。Dressage 的目标是在不牺牲吞吐的前提下在两个粒度上分别控制版本的偏移。0x02. 总体设计整体设计哲学从防到治Dressage 不是用一个单一的开关解决 Staleness而是构建了多层次防御体系两个粒度正交约束Dressage 按两个粒度分别设计了控制逻辑0x03. Trajectory 粒度约束单条轨迹的版本跨度3.1 前提Partial Rollout 在 Proxy 层的透明中断续写这一级控制建立在 Partial Rollout 之上。我们先理解 Partial Rollout 的设计。当一次长生成正遇上权重更新整条轨迹作废太浪费硬用旧权重跑到底又会混入过时输出。Partial Rollout 的做法是在 token 边界暂停、改用新权重续写。难点在于黑盒、不可控的多步 Agent如 OpenCode 这类跑着自有 harness 的外部进程框架并不掌控其执行流程插不进暂停点。Dressage 把这件事下沉到 Proxy 层前提是所有模型调用都经过 ProxyAgent 从不直接连推理引擎于是 Proxy 成了唯一的收口关键设计决策Proxy 按 request-id 中止 SGLang 在途生成接住已产出的 partial tokenAgent 的 HTTP 调用原地挂起在 Agent 看来它只是在等待模型返回对后台的权重切换毫不知情新权重就绪后Proxy 从原有前缀 已生成 partial续写最后把多段拼成完整响应交还给 Agent整个过程对 Agent 完全透明——不依赖 Agent 自身配合对任意白盒/黑盒 Agent、任意本地或远程 Sandbox 都成立3.2 问题反复中断带来 Trajectory 粒度的 StalenessPartial Rollout 让长轨迹不被浪费但也带来新问题一条轨迹若被反复中断就会横跨太多版本。更麻烦的是它的前缀、工具调用历史与环境状态很大程度上是由更旧的 policy 决定的。即便训练时只在最后一个版本的 token 上算 loss样本仍是从一个陈旧前缀和陈旧状态分布上续写而来信息量更低还会给当前策略更新引入偏差和噪声。这里实际上有两个层面在同时起作用分工很明确训练样本构造阶段管的是这条轨迹里到底训哪些 token——通过_mask_nonlast_version_tokens()只让最后一个真实版本产出的 token 参与 loss其余屏蔽掉。这是过滤偏移的影响。Rollout 阶段管的是这条轨迹还能不能继续攒新版本——直接限制它的版本跨度。这是从源头限制偏移的幅度。本篇关注的是后者。只做 loss mask 不够因为样本终究是从陈旧前缀续写的必须让 rollout 侧在轨迹跨过太多版本时就停止。3.3 控制逻辑版本跨度上限Dressage 为单条 Partial 轨迹设定一个版本跨度上限max_partial_rollout_preempts例如最多切换 2 次版本意味着一条轨迹最多横跨 3 个版本。实现位于dressage/proxy/server.py:125-168def_raise_if_partial_version_span_exceeded(*,session,candidate_versions,partial_rollout,max_partial_rollout_preempts):ifnotpartial_rolloutormax_partial_rollout_preemptsisNone:return# 收集 session 历史 本次候选版本去重后按观测顺序排列versions_ordered_real_versions([*_session_response_versions(session),*candidate_versions])version_spanlen(versions)version_switchesmax(0,version_span-1)ifversion_switchesmax_partial_rollout_preempts:return# 超限 → HTTP 502当场终止raiseHTTPException(status_code502,detail{error:partial_rollout_staleness_exceeded,...})3.4 超限后的处理超限的轨迹走统一失败重试路径Proxy 返回 HTTP 502error 名为partial_rollout_staleness_exceeded黑盒服务器rollout_llm_proxy.py识别该错误名 → 记录到 turn scope → 返回合成空响应给 AgentAgent 不崩溃Rollout 收集器检测到状态标记 → 将 Group 标记为失败 → 默认重试DRESSAGE_ROLLOUT_MAX_RETRIES次重试耗尽后整组丢弃Rollout 日志持续汇报版本跨度分布staleness/version_span_mean/max/min和拒绝比例staleness/partial_rollout_rejected_groups判断这道控制是否频繁触发软拒绝机制的核心价值Agent 永远不会因为陈旧性而崩溃最多是当前这条轨迹被丢弃重试。0x04. Batch 粒度约束进入 batch 的版本窗口4.1 对外接口这一级控制只暴露一个参数一个训练 Batch 中最多允许包含最近观察到的若干个模型版本配置项dressage_staleness_keep_versions。# examples/scripts/default/dressage_staleness.yamldressage_staleness_keep_versions:0# 0关闭默认, N0开启取值含义0关闭 batch 级控制默认N0只保留最近 N 个观测到的版本。如keep_versions2则 batch 中可同时包含当前最新版本与上一版本更早版本的轨迹被丢弃最核心的权衡窗口越窄数据越新鲜但被丢弃的样本也越多。4.2 版本判定是轨迹级的Dressage 在生成时记录了 token 级的版本信息full_versions[]单条轨迹内部完全可能横跨多个版本。但在做 batch 级判定时它以整条轨迹最后一个可训练 token的版本作为该轨迹新鲜度的代表# staleness.py:65-84deftrajectory_version_infos(group):latest_by_key{}fororder,sampleinenumerate(group):versionreal_version(metadata.get(dressage_end_token_version))keytrajectory_key(sample)# 只保留同一条轨迹中 segment_index 最大的 segment 的版本position_segment_index(sample,order)ifkeynotinlatest_by_keyorlatest_by_key[key][0]position:latest_by_key[key](position,version)return[TrajectoryVersionInfo(keyk,versionv)fork,(_,v)inlatest_by_key.items()]这是一个有意为之的选择即便一条轨迹早期的若干片段由旧版本生成只要它最终收敛于一个足够新的版本就不会因开头偏旧而被整条误判丢弃。4.3 核心机制推进窗口 回溯复查Batch 级控制的逻辑由两个核心动作配合完成实现位于dressage/rollout/staleness.py动作一推进窗口。每当一个新完成的组带来一个此前未出现的新版本StalenessTracker将版本追加到观测列表current_version_index和cutoff_version_index随之前移# staleness.py:111-119defobserve_group(self,group):previous_countlen(self.versions)forinfointrajectory_version_infos(group):ifinfo.versionnotinself.versions:self.versions.append(info.version)returnlen(self.versions)!previous_count# 是否推进了窗口动作二回溯复查。Batch 收集流程采用了先缓存、触发新版本时再重过滤的惰性策略。这意味着不会在每批新版本到来时立即丢弃陈旧 Group而是等有新版本到来时批量重检——这是一个吞吐和时效性的权衡设计。窗口前移后此前已进入候选 batch、但现已落到窗口之外的组会被重新筛查并移除# fully_async_rollout.py:446-451advanced_versionstaleness_filter.observe_completed(completed_groups)ifadvanced_versionanddata:datastaleness_filter.filter_pending(data,logger)同时每个新加入的组在正式纳入 batch 前也会再次对照窗口检查只要组内存在任一版本过旧的轨迹即整组丢弃# staleness.py:121-129defshould_drop_group(self,group):cutoffself.cutoff_version_indexifcutoffisNone:returnFalsereturnany(self.version_index(info.version)cutoffforinfointrajectory_version_infos(group))4.4 两个关键设计选择选择一版本号按观测顺序排列Dressage 的一个关键设计选择版本号按被观测到的顺序排列而不是按版本标签的字典序或数值序。# staleness.py:117ifinfo.versionnotinself.versions:self.versions.append(info.version)# 追加到末尾为什么这么做因为模型版本号通常是 Git commit hash、时间戳或递增 ID。如果按字典序排序可能出现v10 v2这种反直觉的结果。按观测顺序排列保证了版本窗口的正确性。选择二整 GRPO Group 丢弃GRPO 的 advantage normalization 依赖完整的 Group 结构。如果只删除 Group 内部分 trajectoryGroup 的 reward 均值和标准差会偏移破坏 advantage 计算。所以丢弃粒度是整组。# staleness.py:152-158defkeep_group(self,group_id:int,group:list[Any],logger:Any)-bool:...ifnotself.tracker.should_drop_group(group):returnTrueself._drop_group(group_id,logger)returnFalse丢整个 Group 而不是 Group 内的部分 Sample。这是因为 GRPO 的 advantage normalization 依赖完整的 Group 结构如果只删部分 trajectoryGroup 统计量会偏移引入新的训练偏置。4.5 不完整元数据的容错当parent_traj_id缺失或版本号为unknown/-1/none/空字符串时该样本既不推进版本追踪器也不触发丢弃# staleness.py:44-50defreal_version(value):ifvalueisNoneorstr(value).strip().lower()in_NON_REAL_VERSIONS:returnNonereturnstr(value).strip()这确保了陈旧性控制只作用于可靠的版本信息上。0x05. 两个粒度的关系与互补维度Trajectory 级控制Batch 级控制作用位置Proxy 侧生成时Rollout 侧收集时控制对象单条轨迹内部整批轨迹集合触发时机每次生成续写前检查Batch 组装循环中超限后果502 → 整组重试整组丢弃核心参数max_partial_rollout_preemptskeep_versions对应函数_raise_if_partial_version_span_exceededStalenessGroupFilter二者可同时启用trajectory 控制约束单条轨迹能积累多少版本偏移batch 控制约束进入 trainer 的整体版本新鲜度。此外还有其它机制5.1 训练侧的 Token-Level Loss Mask位置dressage/rollout/artifacts/samples.py:197-213当一条轨迹确实跨越了多个版本时Dressage 在构造训练样本时只保留最后一个真实版本对应 token 的 loss将非最终版本 token 的 loss mask 置为 0。这确保了梯度只来自最新策略版本的 token不被陈旧 token 污染。这是治——过滤偏移的影响与 rollout 阶段的防——从源头限制偏移幅度形成互补。Token 级别 Loss Mask 流程训练样本构造时5.2 配套机制Proxy 暂停/恢复机制dressage/training/train_async_with_rollout_pause.py在 Trainer 更新权重前通过 HTTP API 暂停 Proxy 上的活跃 SGLang 生成请求等待安全边界后再更新权重更新完成后恢复生成。这是 Partial Rollout 和 epoch staleness 检查的基础设施。黑盒 Agent 的 Staleness 错误处理blackbox_server/proxy/rollout_llm_proxy.py当 Proxy 返回因陈旧拒绝(HTTP 502)时黑盒服务器识别特定错误名并返回合成空响应给 Agent避免 Agent 崩溃。版本号全链路记录从 Proxy 到 Rollout 到 Sample 构建链路上每个环节都记录token_version确保可追溯。0x06. 目前缺点与优化方向6.1 当前缺点缺点说明N/M 是静态超参数keep_versions和max_preempts训练启动时固定不能自适应不同阶段。训练早期需更大窗口后期需更小Group 级丢弃代价高丢弃整组意味着丢掉所有 trajectory含新鲜的尤其当组内版本分布不均匀时浪费大二元丢弃策略要么全留、要么全扔。没有软权重如版本越旧在 loss 中权重越低的平滑过渡版本号的语义仅止于标签版本号不含实际策略差异信息KL 散度、参数变化量。两个版本差异极小也一样丢弃黑盒 Agent 空操作浪费合成空响应虽不 crash但 Agent 会继续执行空操作直到被 Terminate浪费计算Partial Rollout 重试延迟Staleness 拒绝后整组重试高压力场景下可能反复失败影响吞吐6.2 优化方向方向具体建议自适应版本窗口根据 KL 散度或 logprob 变化速率动态调整keep_versions。变化大时缩小稳定时扩大Trajectory 级选择性过滤Group 内只标记陈旧 trajectory 为 no_grad 而非丢弃整组需配套改造 GRPO advantage 计算Soft Staleness Weighting引入 0-1 连续权重版本越旧的轨迹 loss 权重越低实现平滑过渡基于策略差异的版本管理用参数更新量或 KL 散度度量版本差异变化小时放宽限制阶段化配置将keep_versions和max_preempts设为 YAML 的阶段化参数不同 rollout_id 区间不同取值Partial Rollout 智能预判Proxy 根据训练节奏预估这次生成大概多久会被中断预测性调整 chunk 大小0x07. 架构图7.1 整体架构7.2 Staleness 组件关系7.3 数据流图Staleness 相关批次版本过滤流程Batch-Level Staleness0x08. 总结Dressage 对 Staleness 问题的处理体现了两个粒度、正交约束的设计思想Trajectory 粒度基于 Proxy 层透明的 Partial Rollout 机制限制单条轨迹的版本跨度上限。在 rollout 侧当场终止超限轨迹从源头限制偏移幅度。Batch 粒度通过推进窗口 回溯复查两个核心动作将进入 Trainer 的轨迹约束在最近几个版本内。版本判定以整条轨迹最后一个可训练 token 为准丢弃粒度为整个 GRPO Group。配套机制Token 级 Loss Mask 过滤偏移影响Proxy 暂停/恢复提供基础设施黑盒合成空响应保证容错。可观测通过 wandb 暴露staleness/*指标版本跨度分布、丢弃组数、版本 gap让训练者实时监控 Staleness 状况。两组实验AlfWorld DAPO/OpenCode一致表明控制住版本偏移后reward 学习更快rollout 与当前 policy 的 logprob 差距显著缩小。当前主要局限在于静态配置和二元丢弃策略未来方向包括自适应窗口、软权重衰减和基于策略差异的版本管理。0xFF 参考Dressage 的 True Staleness Control