Repository navigation
Replies: 3 comments
补充:吞吐声明的独立复测与控制变量归因(2026-10-20, pc823)动机上文报告引用了 #2285/#2287 的"+4~9% 加速"结论。本次在 pc823(RTX 4090,FlashSAC + g1_motion_tracking/mjwarp,CUDA MPS 开启,完整训练含 learner)对该声明做了独立复测,并用严格控制变量实验分解了"实验加速 vs 生产无感"的落差。核心结论:mask 载体语义本身真加速(+10.4%),但发布的生产实现(packed composer 路径)把收益全部消耗,env_ids 与生产版 reset_mask 吞吐打平。 实验设置
结果(500 迭代协议,tail env steps/s,n=5)
跨协议一致性(motion 任务):160 迭代协议 env_ids 118.0k / mask 实验 129.4k / mask 生产(087291c1)109.3k / v1.4.0 116.4k;25k 完整训练 env_ids 115.1k / mask 生产 108.2k / v1.4.0 114.1k。三个协议下排序与幅度一致。 归因分解
结论修正
产物pc823:/home/pc823/ws/unilabsim/issue2285-rerun/( |
E2E 吞吐验证:集成分支 head(含 #2460 env_ids 重构)pc823,RTX 4090 (GPU0, MPS),flashsac 被测 commit:
对照(同协议、同机、不重跑):
结论:持平,无吞吐回退。 相对两条基线的偏差均在 ±2% 噪声带内(远低于 3% 告警阈值)。#2460 的 reset env_ids 公共 API 恢复对 E2E 吞吐无可测影响。 产物: |
roadmap #2459 集成分支最终验收吞吐(2026-10-21)reset 公共 API 恢复 env_ids 的全部改造已合入集成分支
三轮离散度 ±1.7%,run-2(117.1k)达到 env_ids 基线水平;最终均值与基线之差落在运行间噪声范围内。结论:env_ids 公共 API 恢复无吞吐回退,roadmap #2459 的验收标准「E2E 吞吐不低于 env_ids 基线」按噪声等价判定达成。 附:第二后端抽查(#2464)——genesis/newton 后端 FlashSAC 短训均正常,in-tree reset terms 全部走 env_ids 公共 API、无 mask deprecation 警告。 |
Uh oh!
There was an error while loading. Please reload this page.
调研报告:环境 reset 从
env_ids到reset_mask的演进TL;DR
15032a68 "perf(reset): remove dynamic row lifecycle"已包含在 v1.3.5 中,git tag --contains可验证)。v1.3.4 及以前是纯env_ids模式;v1.3.5 起(含 1.4.0)是纯reset_mask模式。done.nonzero() → env_ids → 按行 gather/scatter → 动态宽度 set_state)是 collector 级 CUDA graph 的主要障碍(Issue perf(mjwarp): quantify removing dynamic reset row flow for CUDA graph safety #2285:动态 shape、nonzero()、Python 分支和 host sync 阻碍 graph capture/replay)。env_ids模式,但它在 sim 边界内部把env_ids转成全宽 bool mask 喂给mjwarp.reset_data(可 CUDA graph 捕获)。UniLab 的演进相当于把这条 mask 边界从 sim 层一路上推到 env/manager/事件项全链路。一、三种实现的机制对比
1.1 UniLab ≤ v1.3.4:
env_ids(selected-row / dynamic row)模式签名(
15032a68^:src/unilab/base/torch_env.py:208):env.device上,强制唯一、取值在[0, num_envs);None展开为arange(num_envs)(_normalize_reset_indices,torch_env.py:214-235)。done = terminated | truncated(全宽 bool),随即rows = done.nonzero().flatten().to(int64)—— 行索引在这里物化(torch_env.py:481-544)。env_indices沿 env → event/command/observation/reward/metrics/curriculum/termination manager → 事件函数func(env, env_ids, **params)逐层透传;事件项按default_state[env_ids]gather、采样后write_*_to_sim(..., env_ids=ids);各缓冲用buf[rows] = ...局部更新。dirty_ids = flatnonzero(dirty_mask),恰好一次SimBackend.set_state(dirty_ids, qpos[dirty_ids], qvel[dirty_ids], ...)—— 即按行索引 gather 出动态宽度批次(reset_state.py:1845-1897)。越集写 fail-closed。(len(rows), dim)),外层用index_copy_散射回全批 obs;终态 obs 经index_select/index_copy进final_observation。1.2 third-party/mjlab:IsaacLab 式
env_ids模式签名(
third-party/mjlab/src/mjlab/envs/manager_based_rl_env.py:358):reset_buf.nonzero()产生reset_env_ids(:468)→_reset_idx(reset_env_ids):curriculum.compute(env_ids)→sim.reset(env_ids)→scene.reset(env_ids)→event_manager.apply(mode="reset", env_ids=...)→ 各 manager.reset(env_ids)。func(env, env_ids, **params)直接索引写 sim 的 qpos/qvel torch 视图(外积索引_outer_index),立即生效、逐 term 多次小写,无 staging 事务。sim.reset(env_ids)(sim/sim.py:507-518)在 sim 边界把 env_ids 转成全宽 bool mask 调用mjwarp.reset_data(..., reset=mask_wp)—— 因为后者要进 CUDA graph。即 mjlab 已经认识到"graph 友好的载体是 mask",只是把转换放在了最底层。1.3 UniLab 1.4.0:
reset_mask(fixed-shape mask-driven)模式签名(
src/unilab/base/torch_env.py:227、src/unilab/envs/manager_based_rl_env.py:1474):Mask 不变量(各层反复校验,fail-closed):
torch.bool、一维(num_envs,)、在env.device上;True = 该行需要 reset;None= 全量(内部替换为预分配的 ones buffer)。nonzero()。数据流:
TerminationManager.compute()聚合出_terminated_buf/_truncated_buf(全宽 bool)→reset_buf = reset_terminated | reset_time_outs。TorchEnv.step的空 mask 门控:did_reset = autoreset and bool((terminated | truncated).any())—— 空 done 不消耗 RNG、不发布 terminal 载体(torch_env.py:311-315)。_reset_done_envs:done mask 直接作为reset(done)的入参;info["steps"].masked_fill_(done, 0);post-reset obs 用torch.where(done[:, None], new, old)合并(torch_env.py:488-551)。ResetStateTransaction/ packed composer(src/unilab/base/reset_state.py):begin_state_mask(reset_mask)校验后用torch.where(mask[:,None], default, staged)全宽原地恢复默认状态,注释明确 "keeps the staging tensors full width and never materializes true-row indices";commit 时把整幅 staging + mask 交给SimBackend.set_state_tensor_mask(mask, qpos, qvel, ...),后端只改 true 行、保留 false 行;无任何写入或 mask 全 False 时不调后端直接返回。ManagerBase.reset(reset_mask):事件项必须提供 mask-preserving 实现(term_func.reset(env, reset_mask, **params)),没有 row fallback(command_manager.py:358:"a row fallback would invalidate the fixed-shape contract");内部典型操作为masked_fill_/torch.where/counter += mask.to(int64)。third-party/unisim):把调用方的固定宽度 mask 直接 copy 进稳定的 CUDA-graph 载体,注释:"the graph-safe replacement for zero + index_fill"。能力契约:
ManagerBasedRlEnv.__init__把 packed reset 作为必需 backend capability,缺失即 fail closed,不存在 non-packed fallback;min_step_count_between_reset与固定宽度 mask 契约不兼容,配置时直接报错。二、两种模式的本质区别
env_ids(≤1.3.4 / mjlab)reset_mask(≥1.3.5 / 1.4.0)(n_reset,)(num_envs,)nonzero()、index_select/gather、index_copy_/scatter、动态宽度 H2Dmasked_fill_、torch.where、全宽原地写nonzero输出长度依赖步时数据,伴随 D2H/syncdone.any()(本就存在)index_copy_散射torch.where合并,done 行已就地替换func(env, env_ids),支持min_step_count_between_reset行过滤func.reset(env, mask),逐 term mask 边界,不兼容 min-step 过滤为什么"对未 reset 行做了无谓计算"反而更快? A/B 数据给出了解释:动态 row 模式省下的 FLOPs 远不抵它引入的开销——每步
nonzero()(SAC 每步 14.4 次 → 3.9 次)、kernel launch 数(637 → 532)、D2H 传输(57.6 → 37.3 次/步)、host sync(78.2 → 43.1 次/步)。固定宽度 masked 写是稠密、形状恒定、可融合的 kernel,GPU 利用率反而更高;同时它让后端得以用稳定的 CUDA-graph 载体承接 reset("graph-safe replacement for zero + index_fill")。三、为什么改:完整历史证据链
3.1 问题提出 —— Issue #2285(perf: quantify removing dynamic reset row flow for CUDA graph safety)
#2274 已确认 MJWarp collector CUDA graph 的主要障碍不是 tensor 放置,而是热路径中的动态 row flow:
两个互相冲突的目标:性能(动态 rows 避免对未 reset env 做无谓采样/写 state/forward)vs graph safety(动态 shape、nonzero、Python 分支、host sync 阻碍 graph capture)。Issue 预先声明了量化判定标准:canonical 吞吐下降 ≤2% 且 reset-heavy ≤5% → 删除动态 row flow。
3.2 A/B/C 实验 —— PR #2287(
ed5f5391,2026-10-11)新增
env.reset_row_flowresearch mode,三组变体:A / dynamic_rows(baseline)、B / mask_only(全宽 bool mask + false 行保留)、C / mask_full_refresh(B + 全批 forward,诊断用代价上限)。测量口径:160 iterations × 2048 envs,tail window 100,3 次 interleaved 轮换消除机器漂移,配 t 检验(脚本summarize_issue2285_reset_flow.py)。结果与预期相反——不是"降速多少",而是全面加速(pc823, RTX 4090):
Collector 归因 B/A ≈ +15.5%(reset 行数分布不变,排除"reset 变少"假因);cold/full reset 无惩罚。Decision record:
remove_dynamic_rows。3.3 两轮 review 争论与语义修复(PR #2287 内)
第一轮审计提出六个问题、四个阻塞,最关键的是:
ActionManager.clear_action_state_mask()、ObservationManager.reset_mask()等),漏掉 baseline 会做的 action history / episode metric / curriculum reset —— "+6~9% 不是等语义 A/B"。mask_full_refresh退化为mask_only而非 fail closed。/tmp须 durable 化。dynamic_rows,不得直接Closes #2285→ 催生 child issue perf(reset): remove production dynamic reset row flow #2288。随后同一天内 9 个 fix 提交逐条补齐各 manager/term 的 mask reset 边界(
b9aa746dcomplete mask reset semantics、d6797554events、4abc7f29curriculum、1877e6e9rewards、e0400da6termination、419f275dobs、daeb9780gait、ed2044e3/4020575bmotion 等)。等语义重跑后结论仍然成立:canonical +4.82%(SAC)/ +7.16%(FlashSAC),reset-heavy +3.95% / +7.37%;持久化证据发布为 GitHub releaseevidence-2285-current-head(含 archive SHA-256)。3.4 生产化 —— Issue #2288 / PR #2292(
15032a68,2026-10-12)把 mask carrier 语义提升为唯一生产路径,删除 production 的 done-mask →
nonzero()→ dynamic env-id 生命周期。PR 正文明确 breaking changes:同时加入禁止 row materialization 的架构守卫测试(
tests/base/test_reset_architecture.py)。3.5 稳定化 —— Issue #2296 / PR #2300(
dc2c7383,2026-10-12)packed reset 落地后四条 canonical 训练入口全挂:SAC MuJoCo 继承了不支持的
pd_gainsreset term;MuJoCo adapter 未声明mask_selected_reset触发 fail closed;MJWarp 两条路径把root_state.index_select(0, rows)[:, :7]这类非连续列切片传给要求 contiguous float32 的 composer。修复原则:在 UniLab owner 边界归一化为 contiguous,不放松 unisim 校验器,不在训练代码把 mask 转回 row IDs;MuJoCo PD gain 组合成最终 kp/kd 模型字段;升级unisim-core>=1.7.17。3.6 后续定位
docs/sphinx/source/changelog.md:348):"Selected reset execution replaced dynamically materialized row carriers with fixed-width packed masks…commit only touched rows, and preserve active state elsewhere (perf(mjwarp): quantify removing dynamic reset row flow for CUDA graph safety #2285, perf(reset): remove production dynamic reset row flow #2288)"。四、结论与分析
env_ids模式的nonzero()→ 动态形状 → gather/scatter 链条在 eager 模式下只是小开销,但对 collector 级 graph capture 是结构性障碍。mask 是唯一既是合法 device state、又形状恒定的 reset 载体。min_step_count_between_reset这类逐行过滤能力。src/unilab/rl/contracts/env.py的 EnvProtocol 仍文档化为reset(env_indices)(一维整数张量),与实际 bool-mask 实现不符——所幸 RL 侧所有调用方(offpolicy worker、APPO worker、rsl_rl adapter)都只传None做全量 reset,reset_mask 并不暴露给 RL runner。这处文档漂移值得后续清理。主要参考
ed5f5391(A/B 实验)、15032a68(remove dynamic row lifecycle)、dc2c7383(stabilize packed mask reset)及 9 个语义修复提交evidence-2285-current-head;benchmark 工具:scripts/benchmark/rl/benchmark_offpolicy_collector_active.pysrc/unilab/base/torch_env.py、src/unilab/base/reset_state.py、src/unilab/envs/manager_based_rl_env.py;旧版对照:git show v1.3.4:src/unilab/base/torch_env.py;mjlab:third-party/mjlab/src/mjlab/envs/manager_based_rl_env.py、third-party/mjlab/src/mjlab/sim/sim.py:507All reactions