fix(training/rl): the three RL posture flags are checked, not read by truthiness - #3548
Conversation
… truthiness normalize_obs, normalize_advantage and autotune_alpha each select a posture, and every RL backend read the ones it consumes by truthiness while its validate() graded every numeric knob around them. So "false", "no" and "0" selected the affirmative branch - the normalizers or the temperature optimizer the caller had declined - and 0 or None selected the negative one without being a declared spelling of it. autotune_alpha also gates whether alpha_lr is read, and that gate read it by truthiness too: autotune_alpha="false", alpha_lr=-1.0 was refused as the rate of an optimizer the caller asked not to build. Each flag now takes the shared boolean_flag_error domain through a field-scoped Trainer gate, consulted by exactly the backends that read it: all three for normalize_obs, PPO for normalize_advantage, FastSAC for autotune_alpha - ahead of the alpha_lr check, which now reads the rate only once the flag is a usable True. The one-owner scan derives the reader set from the tree, so a backend that starts reading a flag is graded on arrival.
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Holds the three RL posture flags (normalize_obs, normalize_advantage, autotune_alpha) to the shared boolean_flag_error domain through three field-scoped Trainer gates, consulted by exactly the backends that read each flag - all three RL backends for normalize_obs, PPO alone for normalize_advantage, FastSAC alone for autotune_alpha. temperature_learning_rate_problems now reads alpha_lr only once autotune_alpha is a usable boolean, and FastSAC's validate consults the flag gate ahead of the rate gate it depends on, so a misread posture is refused by the flag's own name rather than as the rate it would have selected. This matches the PR description exactly and follows AGENTS.md's "Posture flags are checked, never read by truthiness" convention, including the lerobot_camera async_mode/timeout_ms shape for a flag that gates whether a numeric field is read at all.
What's good
- Verified locally at
505f6401: the new test module (123 passed), both extended meta-guards (test_spec_field_read_discovery.py+test_spec_fields_are_documented.py, 111 passed), the fulltests/training/suite (5538 passed, 4 skipped), and the whole-tree graders most likely to bite this diff (test_docstring_xref_roles_resolve.py,test_no_host_paths.py,test_log_strings_are_ascii.py,test_test_case_names_describe_behaviour.py, 74 passed) are all green. - The one-owner scan derives its reader set from the tree via the shared
reads_spec_fieldrule and roots the walk atinspect.getfile(Trainer), so a backend that starts reading a flag is graded on arrival - and the planted-defect control keeps the scanner non-vacuous. - The biconditional holds in both directions: ignorers report nothing (
fast_td3withnormalize_advantage="false"is[]), andtest_no_backend_gates_a_field_it_does_not_readpins the converse. - The
temperature_learning_rate_problemschange keeps the ordering safe by construction: the only module that consults the rate gate also consults the flag gate first, pinned both behaviourally and structurally (AST overfast_sac.py). - Changelog fragment named with the PR number, docs paragraph placed beside the
alpha_lrprose it qualifies,IsaacConfig's four flags correctly left to the merge order with #3343 rather than pulled into scope.
What
normalize_obs,normalize_advantageandautotune_alphaonRLTrainSpecare held toboolean_flag_error- the domainTrainSpec.resumeandstreamingalready take - through three field-scopedTrainergates, each consulted by exactly the backends that read the flag.Why
Each flag selects a posture: wrap both observation streams in
EmpiricalNormalizationor feed them raw; standardize advantages per batch or use them as computed; build a temperature optimizer or holdalphaatinit_alpha. Every RL backend read the ones it consumes by truthiness (... if spec.normalize_obs else Noneon all three,if spec.normalize_advantage:at two PPO sites,if self.autotune_alpha:in SAC) while itsvalidate()graded every numeric knob around them. So the spellings a caller reaches for to opt out selected the affirmative branch, and0/Noneselected the negative one without being a declared spelling of it.Measured on
b4c3ea8throughcreate_trainer(<provider>).validate(spec):"false"/"no"/"0"/0/Nonenormalize_obsppo,fast_sac,fast_td3[]on all three - the first three build the normalizers the caller declinednormalize_advantageppo[]- advantages standardized against the opt-outautotune_alphafast_sac[]-"false"builds the temperature optimizertest_rl_fast_sac.pypins thatFalsemust not;0holds the temperature fixed while spelling neitherautotune_alphaalso gates whetheralpha_lris read, andtemperature_learning_rate_problemsread it by truthiness too:autotune_alphaalpha_lr"false"-1.0['fast_sac: alpha_lr must be > 0, got -1.0.']- the rate of an optimizer the caller asked not to buildautotune_alphaTrue-1.0alpha_lrFalse-1.0[]This is the shape AGENTS.md records for
lerobot_camera'sasync_mode/timeout_ms("a flag that gates whether a numeric row is READ is checked ahead of the numeric guard"): the flag gate goes ahead of the rate gate inFastSacTrainer.validate, and the rate gate readsalpha_lronly once the flag is a usableTrue, so a misread posture is refused by its own name rather than as the knob it would have selected. The ordering is pinned both behaviourally and structurally (AST over the one module that consults both).Where the checks live
The existing pattern:
_posture_flag_problemsintraining/_validate.py(already the owner ofresume/streaming) gains three callers,Trainergains three thin gates beside_resume_problems/_streaming_problems, and each RLvalidate()consults the gates for the flags it reads - three fornormalize_obs, PPO alone fornormalize_advantage, SAC alone forautotune_alpha. The biconditional the numeric gates document holds here too: a backend that ignores a flag reports nothing about it (fast_td3withnormalize_advantage="false"reports[]), pinned byTestABackendThatIgnoresTheFieldReportsNothingand by the one-owner scan, whose reader set is derived from the tree with the sharedreads_spec_fieldrule so a backend that starts reading a flag is graded on arrival.The two meta-guards that grade gates from the outside are extended rather than bypassed:
FIELD_SCOPED_GATESand the guard roster intest_spec_field_read_discovery.py, and_GATE_FIELDSintest_spec_fields_are_documented.py- the latter is what failed first when the gates landed without a table entry, which is the table doing its job.Four corners
ImportError- the gate functions do not existtests/training/)main-> 174 failed / 5138 passed here, byte-identical failing node ids (alltorch/lerobot/mujocoabsent)The 51 are every refusal cell across the five reader cells, the ordering cells, and the one-owner scan's
every_reader_routes_through_the_gatefor all three gates. The 72 that pass on both trees are controls: usable booleans untouched, defaults report nothing, ignorers report nothing, the gates carry their context.Gate
ruff check+ruff format --checkon touched filesmypyon_validate.py,base.py,rl/base_algo.pymain(73 of 73), alllerobot/qpsolvers/ torch-mock, 0 attributableCost
Production:
+3gate functions and+2lines in the rate gate (_validate.py),+3thinTrainermethods,+4validate()calls across three backends, spec docstring sentences. Tests: one new module in the shape oftest_posture_flag_domain.py, plus the two table entries the meta-guards require. Docs: one paragraph indocs/training/rl.mdbeside thealpha_lrparagraph it qualifies.Not touching
IsaacConfig's four unchecked flags (headless,ground_plane,enable_rtx_sensors,verbose), which the same sweep found: #3343 (open) editssimulation/isaac/config.py, so that is a merge-order question for after it lands.Opened as a draft so the changelog fragment - which needs this number - ships before any approval can be dismissed by it. Auto-merge (squash) armed once ready.