Support real SO-101 arm training and fix some bugs - #4
Open
Ghosty2003 wants to merge 2 commits into
Open
Conversation
architecture, reward output clamping, and a planner warm-start bug fix (reset_iter_state was discarding the previous plan every step) from the mpail-lerobot fork. Also fixes dataclass mutable-default fields (RewardConfig() -> field(default_factory=RewardConfig), etc.) that broke under Python 3.11+, and a double-divide-by-255 / dotted-key bug in obs_normalizer.py. LR/gp_coeff/target_entropy/enc_lr_scale updated to match the fork's tuned values.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add SO-101 real-arm support + port anti-collapse/bugfix patches from mpail-lerobot fork
Summary
Two commits, two different purposes:
b55aff0— purely additive: a newmpail2/envs/real/so101/environment stack fortraining on the real SO-101 arm. Touches no existing library code.
39402f8— ports several fixes/features from thempail-lerobotfork into the sharedcore library (
dynamics.py,learner.py,planner.py,reward.py, configs,obs_normalizer.py). These affect all envs (sim + real), not just SO-101.1. SO-101 real-arm environment (
b55aff0)New package
mpail2/envs/real/so101/, mirroring the existing Franka/Kinova real-env layout:env_factory.py,so101_env.py,wrappers.py,ik_utils.py,robot_limits.py—the Gym-style env itself: joint-space IK, per-joint limits/tolerances (gripper handled
separately since it's a slower/weaker actuator than the 5 arm joints).
network/server.py+transport/*_pb2*.py— a gRPC server/client pair so the env canstep a physically remote robot process (arm + cameras run in a separate
lerobotconda envthat doesn't have
mpail2installed;mpail2talks to it over gRPC instead of importing itdirectly).
transport/*_pb2*.pyare generated protobuf code fromtransport/so101_robot.proto— not hand-written, please skip line-by-line review of those.lerobot_patch/async_inference/— a local copy oflerobot's async inference stack(
policy_server.py,robot_client.py,robot_server.py,rl_client.py,teleop_with_planner.py, etc.) adapted to serve MPAIL2 policies instead of LeRobot's own.training/— SO-101-specific entry points:train_so101_local.py(drives the robotin-process via the standard
MPAIL2Runner.learn()rollout loop, blocking on realenv.step()/env.reset()so no observation is ever dropped — as opposed todemo_recording_server.py, which reacts to inbound RPCs from a separately-runninglerobot robot_client), plusconvert.py/convert_lerobot.py(demo format conversion),replay_demo.py,check_encoder_collapse.py.soa.urdf— SO-101 URDF for IK.README.md— full setup instructions (separatelerobotconda env + clone).pyproject.toml— newso101extra (opencv-python,pyrealsense2,grpcio,grpcio-tools,ikpy).docs/INSTALL.md— new row/bullet pointing at theso101extra and the README above.No existing files are modified beyond the two-line
pyproject.toml/INSTALL.mdadditions.2. Core library changes ported from
mpail-lerobotfork (39402f8)SIGReg anti-collapse regularizer
mpail2/dynamics.py— newSIGReg(nn.Module). Projects a batch of latents ontonum_projrandom 1D directions and penalizes deviation of each projection's empiricalcharacteristic function from a standard Gaussian's (quadrature-integrated over
knotspoints). Pushes latent variance to spread across directions instead of collapsing onto a
few — i.e. an encoder-collapse regularizer.
mpail2/learner.pyMPAIL2Learner.__init__— buildsself._sigreg = SIGReg(knots=..., num_proj=...)whendynamics_learner_cfg.sigreg_coeffis set and> 0;Noneotherwise (opt-in, no-op bydefault since
sigreg_coeffdefaults toNonein the basecfgs.py).MPAIL2Learner.update_dynamics— addssigreg_coeff * sigreg_lossto the JEP loss andlogs
Dyn/sigreg_loss.configs/cfgs.py):sigreg_coeff,sigreg_knots,sigreg_num_projon
DynamicsLearnerCfg. Tuned defaults (configs/defs.py,DynamicsLearnerConfig):sigreg_coeff=0.02,sigreg_knots=17,sigreg_num_proj=1024— a conservative starting point (the fork used
0.1on a sim pick-place task; startedlower here so it doesn't swamp the JEP loss before being tuned on this codebase).
Encoder architecture: LayerNorm-ending instead of trailing SiLU
configs/defs.py,CNNCoderConfig/MultiCoderConfig—model_kwargsswitched from"override_last_layer_activation": Trueto"override_last_layer_norm": True. Rationalein-line: a trailing one-sided SiLU after the encoder's LayerNorm distorts the
normalized/zero-centered geometry the LayerNorm just established.
Reward output clamping
mpail2/reward.py,Reward.forward— clamps output to[-reward_clip, reward_clip]when
cfg.reward_clipis notNone(defaultNone→ disabled). Motivated by observeddrift in the WGAN-style critic's unbounded output scale (
mean_demo_rewardclimbing~6→16,
Value/mean_q_valueswinging -187↔+164 across training iterations).reward_cliponRewardCfg/RewardConfig(defaults toNone,i.e. no behavior change unless explicitly set).
Runtime reward scaling hook
mpail2/learner.py,update_valueand_n_step_return_lambda— both now scale thereward used in the value target by
getattr(self, '_reward_scale', 1.0). This isn't aconfig field; it's a plain instance attribute set at runtime — currently only from
training/train_so101_local.py(runner.learner._reward_scale = float(args.reward_scale)).No-op (
1.0) for every existing sim/Isaac training path.Planner warm-start bug fix
mpail2/planner.py,Planner.optimize— was callingself.sampling.reset_iter_state()with no arguments every single step, which resets
_iter_meanto zero each time —discarding the warm-started
_opt_controlscarried over from the previous step and turning~95% of each decision's candidates (the noise-sampled ones, not the small
policy_proportionfraction) into a fresh zero-mean/max-std blind search with no memory ofthe previous decision's converged plan. Now passes
reset_iter_state(prev_controls=self._opt_controls), matchingreset_iter_state's owndocstring contract.
Planner.act_policy_only()— returns the policy network's own deterministic(tanh-squashed mean) action directly, bypassing CEM/MPPI entirely (no noise rollouts, no
elite re-weighting). Useful for evaluating what the policy net alone has learned,
independent of the planner's search.
Python 3.11+ dataclass mutable-default fix
configs/defs.py—PlannerConfig,LearnerConfig: fields likereward_cfg: RewardConfig = RewardConfig()(a mutable default shared across all instances)changed to
field(default_factory=RewardConfig). This pattern happened to work on whateverPython version was previously used but raises
ValueError: mutable default <class '...'> for field ... is not allowedunder Python 3.11+'s stricter dataclass checks — blocking anyoneon a newer Python from importing the configs at all.
obs_normalizer.pyfixesFixedObsNormalizerregistered buffers asf"{key}_mean"/f"{key}_std", which breaks (register_bufferrejects dots in names) for any obs keycontaining a dot (e.g. SO-101's
cam.wrist-style keys). Added_buf_name()(key.replace(".", "_")) and routed all three read/write sites(
_compute_statistics,forward,inverse) through it.CamOnlyObsNormalizer.forward/inverseunconditionally didvalue / 255.0 - 0.5/(value + 0.5) * 255.0. Some callers (so101_env.py,convert.py)already divide by 255 before calling this, so the unconditional divide silently
double-divided and crushed the whole batch into a near-zero-variance sliver near
-0.5regardless of image content. Now guards with the same
value.max() > 1.0checkconvert.pyalready uses, andinverseno longer re-multiplies by 255 (this codebase'sactual convention is that callers always hand
forward()already-[0,1]images, soscaling back to
[0,255]was the wrong inverse).Tuned hyperparameter defaults (
configs/defs.py)LR(shared)3e-42e-4RewardLearnerConfig.gp_coeff0.15.0PolicyLearnerConfig.target_entropy-3.0(-ACTION_DIM)-2.0(more exploration;-ACTION_DIM=-5judged too conservative)DynamicsLearnerConfig.enc_lr_scale0.10.08All four match the tuned values used in the
mpail-lerobotfork.Notes for reviewers
39402f8's changes are not SO-101-specific — they touch shared config defaults andloss functions used by every existing sim/Isaac training run. Worth a sanity training run
on an existing sim task (e.g. Ant-v5 or the Franka pick-place) to confirm the LR/gp_coeff/
target_entropy/enc_lr_scale changes and the encoder LayerNorm-ending switch don't regress
current results, since none of the new features (
sigreg_coeff,reward_clip) are on bydefault but these defaults changes are.
transport/*_pb2*.pyare generated protobuf code — regenerate fromtransport/so101_robot.protorather than hand-editing if changes are needed there._reward_scaleis a runtime-set instance attribute rather than a config field (see above);flagging in case a config field was actually intended instead.