feat(training): RL parity — evaluate(), staged_reward, vectorized PPO, gym adapter (+ render output_path)#915
Conversation
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
This PR lands two related bodies of work: four MuJoCo-sim correctness fixes and four RL-training parity additions. The sim fixes add robot_action_keys() (an ABC default mirroring joint names plus a MuJoCo override that returns namespace-scoped actuators, so policies on tendon-gripper / passive-joint robots stop silently no-op'ing), a world-+Y fallback in _target_quat for vertical cameras, an optional render(output_path=...) with a bad-char + ..-component guard on the LLM-supplied path, eval_policy auto-resolving the single robot to match run_policy, and a per-episode stateful-reward reset in DeclarativeBenchmark.on_episode_start. The RL parity work adds a declarative staged_reward phase machine (compiled through the existing closed predicate registry, no eval), BaseRLAlgo.evaluate() / load_checkpoint() / _deterministic_action(), SimEnv.n_substeps (default 5), a vectorized PPO rollout path gated on num_envs > 1, and the VecSimEnv / GymSimEnv adapters.
I walked the changed paths against the repo conventions. The render output_path guard rejects shell metacharacters and .. components on the user-supplied path; the vectorized GAE correctly broadcasts the scalar advantage recurrence over the (T, N) env axis and bootstraps truncations from the captured pre-reset terminal_obs; latest_checkpoint referenced from the base evaluate() is defined on both concrete trainers (ppo, fast_sac) so it resolves at runtime; the single executor in VecSimEnv is created once and reused per the AGENTS.md perf rule. The n_substeps=5 default is a training-dynamics tuning change rather than a wire/schema/CLI one-way door. New code carries no emojis or host paths and __all__ exports only public names. No blocking concerns found.
Verification suggestions
hatch run test tests/training/test_rl_ppo_vectorized.py tests/training/test_rl_vec_env.py tests/training/test_rl_evaluate.py tests/training/test_rl_gym_env.py tests/simulation/test_staged_reward.pyto confirm the RL parity suite is green in isolation.hatch run test tests/simulation/mujoco/test_robot_action_keys.py tests/simulation/mujoco/test_camera_vertical_lookat.pyto confirm the embodiment action-key and vertical-camera regressions pass with MuJoCo present.
975b32f to
347daa1
Compare
Maintainer disposition (empirically re-verified against
|
02ed4e0 to
a58fd13
Compare
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
This PR adds RL-training parity on top of the from-scratch RL trainers (#877): BaseRLAlgo.evaluate()/load_checkpoint()/_deterministic_action() (deterministic eval peer of train(), frozen normalizer, side-effect-free train/eval mode restore, weights_only=True checkpoint load), a declarative staged_reward phase-machine predicate compiled through the closed registry (no eval), a vectorized PPO rollout path (num_envs>1 via VecSimEnv with per-env GAE bootstrapping from captured terminal_obs), a GymSimEnv gymnasium adapter with the correct terminated-vs-truncated split, SimEnv.n_substeps, and an optional render(output_path=...) with a path-traversal/shell-metachar guard. The diff matches the description: the sim-bug fixes that landed independently are dropped, and the remaining work is net-new RL parity.
I verified the changed paths against the codebase: n_substeps is part of the SimEngine.send_action ABC contract and the MuJoCo override, latest_checkpoint exists on both PpoTrainer/FastSacTrainer, the (T,N) GAE recurrence in compute_gae broadcasts correctly over the env axis, VecSimEnv reuses a single ThreadPoolExecutor (AGENTS.md perf rule), and the staged_reward factory stays inside the closed-registry/no-eval contract. The render(output_path) write is argv-style (open(...,'wb'), no shell) with no trusted-prefix join, so the ../shell-metachar guard is reasonable defense-in-depth rather than a traversal boundary. No emojis in tool/result/log strings, no host paths, ValueError guards on bad inputs. Lint is clean and the full RL/staged-reward unit suite passes locally (48 tests). The two prior CodeQL findings (unused register_predicate import, unused sb3 variable) are already resolved at head.
What's good
- Behaviour-preserving dispatch:
num_envs==1keeps the byte-identical single-env path; vectorization is opt-in. evaluate()restores pre-eval train/eval mode and freezes the normalizer, closing the BatchNorm/Dropout stat-freeze footgun.load_checkpointusesweights_only=True, closing the legacy-unpickler RCE surface.- Correct GAE bootstrapping: truncations bootstrap from the captured terminal obs, real terminals zero the bootstrap.
- Stateful-term
reset()wired in bothSimEnv.resetandDeclarativeBenchmark.on_episode_startso phase state never leaks across episodes.
Verification suggestions
hatch run test -- tests/training/test_rl_vec_env.py tests/training/test_rl_ppo_vectorized.py tests/training/test_rl_evaluate.py tests/training/test_rl_gym_env.py tests/simulation/test_staged_reward.py- GPU/MuJoCo host:
hatch run test-integ -- tests_integ/training/test_so101_pick_place_e2e.pyto confirm the reach-convergence assertion (>50% gripper->cube distance closure).
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
This PR adds RL-training parity on top of the from-scratch RL trainers: a deterministic BaseRLAlgo.evaluate() (frozen normalizer via update=False, gradients off, side-effect-free train/eval-mode restore, weights_only=True checkpoint load) plus load_checkpoint()/_deterministic_action(); a declarative staged_reward phase-machine predicate compiled entirely through make_predicate (closed registry, no eval); SimEnv.n_substeps; a vectorized PPO rollout path that wraps N independent SimEnv in a reused-thread-pool VecSimEnv and bootstraps truncations from captured terminal_obs; a GymSimEnv gymnasium adapter that correctly splits terminated vs truncated; and an optional render(output_path=...) with a path-traversal/shell-metacharacter guard. The diff is additive: the num_envs != 1 PPO validation is loosened (single-env path is byte-for-byte unchanged), new symbols are net-new exports, and send_action(..., n_substeps=...) matches the existing SimEngine ABC signature so no call site breaks.
What's good
evaluate()is correctly side-effect-free: it captures and restores the pre-eval.trainingflag for both the actor-critic and the normalizer, and freezes the normalizer withupdate=Falseso eval never shifts running stats.- Stateful reward
reset()is hooked in bothSimEnv.resetandDeclarativeBenchmark.on_episode_startvia duck-typedgetattr(term, "reset", None), so per-episode phase state can't leak and stateless terms are untouched. VecSimEnvreuses a singleThreadPoolExecutorfor its lifetime (AGENTS.md: never create executors in the hot loop) and fails loudly on heterogeneous sub-env dims; the autoreset path captures the true terminal obs before reset, which is load-bearing for GAE truncation bootstrapping.load_checkpointusesweights_only=True, closing the legacy-unpickler RCE surface.- Good scope discipline and dense regression tests pinning the eval schema, determinism, success-rate semantics, phase monotonicity, one-time bonus, and validation/safety rejections.
Verification suggestions
hatch run test tests/training/test_rl_evaluate.py tests/training/test_rl_vec_env.py tests/training/test_rl_ppo_vectorized.py tests/simulation/test_staged_reward.pyto exercise the CPU-only fakes.- Spot-check the vectorized GAE path matches single-env on
num_envs=1(_collect_rollout_singlevs_collect_rollout_vectorizedshould agree on identical seeds).
…torized PPO, render output_path + precise env union
ebe89e2 to
d335c0b
Compare
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
This PR carries the RL-training parity work atop the from-scratch RL trainers: a declarative staged_reward phase-machine predicate compiled through the closed make_predicate registry (no eval), SimEnv.n_substeps plumbing plus per-episode reset() of stateful reward terms, a GymSimEnv gymnasium adapter that splits terminated vs truncated correctly at the source, a vectorized VecSimEnv-backed PPO rollout path (num_envs > 1), and an ergonomic render(output_path=...) with a ../shell-metacharacter path guard. As the description makes explicit, this branch is a Draft decomposition tracker, not a mergeable unit: its pieces are landing as focused children (#918 VecSimEnv, #920 evaluate(), queued follow-ups) and the tracker will be closed, not merged once they all land. Draft status makes accidental merge mechanically impossible.
Reviewing the diff itself: the staged_reward factory validates stages eagerly (non-empty list, allowed keys, predicate-call dicts, non-final-stage gate, numeric bonus) and stays inside the closed-registry safety contract; the monotonic phase machine and one-time-bonus semantics are pinned by dedicated CPU tests. Stateful-term reset() is wired into both SimEnv.reset and DeclarativeBenchmark.on_episode_start via duck-typed getattr, so stateless function terms are unaffected. The vectorized PPO path captures the pre-reset terminal_obs for correct truncation value-bootstrapping and flattens (T, N, ...) to (T*N, ...) for the shared minibatch update; evaluate() defensively routes through a single sub-env on a VecSimEnv. The render(output_path=...) guard rejects .. components and shell metacharacters before writing, consistent with the project's existing path-safety conventions. The substantive content was already reviewed and APPROVED on a prior commit; no new commit introduces a new instance of a previously-flagged bug class. No blocking concerns found.
What's good
- Scope discipline: the multi-concern mega-diff is being decomposed into individually-reviewable children rather than force-rebased, and the tracker is held as a Draft so it cannot be merged out from under the decomposition.
staged_rewardis authored as data through the closed predicate registry (noeval), matching the project's DSL-safety stance.- Strong test coverage for the net-new pieces: phase advance / one-time bonus / monotonicity / reset, the gymnasium
check_envconformance and terminated-vs-truncated split, and vectorized batch-shape / throughput-multiplier pins. terminatedvstruncatedis split at the adapter source (the SB3TimeLimitfootgun), so downstream learners get value-bootstrapping right for free.
Verification suggestions
hatch run test tests/simulation/test_staged_reward.py tests/training/test_rl_ppo_vectorized.py tests/training/test_rl_gym_env.pyto confirm the 47 PR-specific tests pass on the head commit.- With GPU + MuJoCo weights:
pytest tests_integ/training/test_so101_pick_place_e2e.py -vto exercise the full RL stack (staged_reward + SimEnv + evaluate + GymSimEnv/SB3 + vectorized PPO) against real SO-101 physics.
Several merged features in the post-v0.4.0 (v0.4.1) wave landed on main without a dedicated CHANGELOG entry, leaving the release-notes backfill as the gating step before cutting the v0.4.1 tag. Add dedicated Added/Security entries for the genuinely-undocumented merged PRs, each grounded in the merged change: - Security: mimicgen dependency-confusion removal (#931) - from-scratch RL trainers PPO + FastSAC (#877, #886) and RL parity remainder (#915) - NVIDIA EGL vendor-ICD auto-register (#921) and EGL software-fallback warning (#909) - add_object material/texture/reflectance (#901) - MotionBricks provider (#856), WBCGaitPolicy (#852), CompositePolicy (#849) - reBot B601-DM registry entries (#799) - DAgger correction-collection action (#736) - end-to-end VLA-on-G1 workflow example (#662) Docs-only; ASCII-only, no code change. Entries inserted newest-merged-first under [Unreleased] to match the existing convention. Co-authored-by: strands-agent <217235299+strands-agent@users.noreply.github.com>
The README covered create_policy providers in depth but had no section for create_trainer, its training-side peer. Add a Training providers section that mirrors the Policy providers layout: the create_trainer/TrainSpec usage, a provider table (lerobot_local, groot, cosmos3, mock, ppo, fast_sac), and a note on the from-scratch RL trainers (BaseRLAlgo lifecycle, VecSimEnv rollouts, evaluate()). Surfaces the RL lane (#915, #918, #920) that only lived in the CHANGELOG. Co-authored-by: Sundar Raghavan <sdraghav@amazon.com>
Status: decomposing into focused PRs (not force-rebasing)
Per review disposition, this multi-concern branch is being landed as small, separately-reviewable units instead of a single mega-diff. The sim-bug fixes it originally carried (
_target_quatvertical-camera +Y fallback,eval_policysole-robot resolve, actuator-keyed policies) already landed independently onmain, so they are dropped. The remaining net-new RL-parity work is being split:VecSimEnv(N-env threaded CPU/MuJoCo vec-env)BaseRLAlgo.evaluate()+load_checkpoint()+ testsstaged_rewarddeclarative phase-machine + testsGymSimEnvadapter + vectorized PPO rollout pathrender(output_path=...)ergonomicFollow-ups are opened one at a time to keep each diff surgical and individually validatable (and to respect parallel-PR limits). This PR stays open (as a Draft) as the decomposition tracker until its pieces have all landed; it will not be force-rebased into a single un-reviewable unit, and it will be closed rather than merged.
Original Summary (for reference)
The RL-training parity work: deterministic
evaluate(), declarativestaged_reward,SimEnv.n_substeps, a vectorized PPO rollout path, and theVecSimEnv/GymSimEnvadapters, plus an optionalrender(output_path=...)with a path-traversal guard. All net-new on top of the from-scratch RL trainers; none overlapping anything already onmain.§13 Review Round Changelog
6770922main. Decompose into focused PRs (#918, #920, queued follow-ups) rather than force-rebase.