# ============================================================================= # openpi drop-in: training/serving config for g1-inspire-piston-pi05 # ============================================================================= # This file is a REFERENCE extract, not an importable module. It gathers the # three pieces of openpi source that this checkpoint's config # (`pi05_g1_inspire_piston_lora_abs_st`) is built from, so you can splice them # into your openpi checkout. See INSTALL.md for exactly where each goes and for # our fork state vs upstream. # # Verbatim as of our openpi HEAD 9088f209 (= Physical-Intelligence/openpi # @ 15a9616 + 5 local commits). See INSTALL.md. # ============================================================================= # ----------------------------------------------------------------------------- # (1) TrainConfig entry -> add to the `_CONFIGS` list in # src/openpi/training/config.py. This is the config the checkpoint was # trained with and the one you pass to get_config(...) at serve time. # ----------------------------------------------------------------------------- # # Custom: CLOSURE-OBSERVABILITY retrain of pi05_g1_inspire_piston_lora_abs. # The abs (v3) policy passed the full gate ladder but is STATE-BLIND # (discrete_state_input=False), and ego images are ambiguous about finger # closure: each replanned chunk regresses/samples a hand-open mode -> # closing sawtooth -> the grip never establishes (2026-07-16 GUI failure). # Fix: let the model SEE its hand state while keeping the anti-anchor # properties for the arm trajectory. Differences vs abs: # (1) discrete_state_input=True -- pi0.5's NATIVE state path and the # pi05_base pretraining format: ModelTransformFactory (config.py, PI05 # branch) passes the flag to TokenizePrompt, which discretizes the # normalized state into 256 bins inside the prompt string # ("Task: ..., State: ;\nAction: ", tokenizer.py). No model # parameters change (pi0.py's pi05 branch has no continuous state # pathway), so pi05_base weights and the LoRA freeze filter are untouched. # (2) state-TOKEN dropout p=0.8 (DataConfig.state_token_dropout_prob): # 80% of training samples build the prompt WITHOUT the state segment # (the tokenizer's first-class state=None format) so state cannot become # a crutch for the arm trajectory. NOT zero-fill dropout: a zeroed # normalized state discretizes to plausible mid-bin (~128) pose tokens -- # a contaminating marker, not an absence signal. Train-only (injected by # data_loader.transform_dataset); serving ALWAYS tokenizes the real state. # (3) Absolute actions kept (use_delta_actions=False): the v3 structural fix # stays; labels are raw absolute joint targets. # Norm stats: REUSED from the abs run via the AssetsConfig redirect below. # (At serve time norm stats come from the checkpoint's own assets/ dir; the # redirect only matters for recomputing/loading stats at train time.) TrainConfig( name="pi05_g1_inspire_piston_lora_abs_st", model=pi0_config.Pi0Config( pi05=True, action_horizon=10, discrete_state_input=True, paligemma_variant="gemma_2b_lora", action_expert_variant="gemma_300m_lora", ), data=LeRobotG1InspireDataConfig( repo_id="g1-inspire-piston-pick-place-success", assets=AssetsConfig(assets_dir="./assets/pi05_g1_inspire_piston_lora_abs"), base_config=DataConfig( prompt_from_task=True, action_sequence_keys=("action",), state_token_dropout_prob=0.8, ), use_delta_actions=False, ), weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"), num_train_steps=15_000, batch_size=8, freeze_filter=pi0_config.Pi0Config( pi05=True, action_horizon=10, discrete_state_input=True, paligemma_variant="gemma_2b_lora", action_expert_variant="gemma_300m_lora", ).get_freeze_filter(), ema_decay=None, keep_period=None, ), # ----------------------------------------------------------------------------- # (2) LeRobotG1InspireDataConfig -> add to src/openpi/training/config.py # (alongside the other DataConfigFactory subclasses). Referenced by the # TrainConfig above. Uses the G1Inspire transforms in openpi_files/ # g1_inspire_policy.py (copy that to src/openpi/policies/g1_inspire_policy.py # and `from openpi.policies import g1_inspire_policy`). # ----------------------------------------------------------------------------- @dataclasses.dataclass(frozen=True) class LeRobotG1InspireDataConfig(DataConfigFactory): """Data config for the Unitree G1 + Inspire red-ball LeRobot dataset. Mirrors LeRobotLiberoDataConfig but: (1) remaps this dataset's standard LeRobot keys (observation.state / observation.images.ego_view / action), (2) uses the G1Inspire transforms, (3) converts the 14 arm action dims to deltas (hands/base-height/nav stay absolute) since the raw actions are verified absolute joint targets and pi0.5 base expects deltas for joints. """ # If False, skip the DeltaActions/AbsoluteActions push entirely: training # labels stay the RAW absolute joint targets, and (because serving rebuilds # this same pipeline via train_config.data.create() -- policy_config.py) # the policy output stack has NO AbsoluteActions re-anchoring step. This is # the fix for state-blind delta replay: a policy that never reads state # would otherwise emit memorized delta chunks that serving anchors to the # real (drifted) state, carrying closed-loop offsets 1:1. use_delta_actions: bool = True @override def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig: repack_transform = _transforms.Group( inputs=[ _transforms.RepackTransform( { "observation/image": "observation.images.ego_view", "observation/state": "observation.state", "actions": "action", "prompt": "prompt", } ) ] ) data_transforms = _transforms.Group( inputs=[g1_inspire_policy.G1InspireInputs(model_type=model_config.model_type)], outputs=[g1_inspire_policy.G1InspireOutputs()], ) # Arms (first 14 action dims) -> delta vs the chunk's first state; # hands + base_height + navigate (dims 14:30) stay absolute. Mask is # length 14 (not 30) on purpose: DeltaActions indexes state[:len(mask)], # and our proprio state is only 29-dim, so a 30-wide mask would break # the broadcast. Untouched action dims default to absolute anyway. if self.use_delta_actions: delta_action_mask = _transforms.make_bool_mask(14) data_transforms = data_transforms.push( inputs=[_transforms.DeltaActions(delta_action_mask)], outputs=[_transforms.AbsoluteActions(delta_action_mask)], ) model_transforms = ModelTransformFactory()(model_config) return dataclasses.replace( self.create_base_config(assets_dirs, model_config), repack_transforms=repack_transform, data_transforms=data_transforms, model_transforms=model_transforms, ) # ----------------------------------------------------------------------------- # (3) DataConfig field additions -> add these two fields to the `DataConfig` # dataclass in src/openpi/training/config.py. The config in (1) passes # `state_token_dropout_prob=0.8`, so the field MUST exist or construction # fails. Both are TRAIN-ONLY (read by data_loader.transform_dataset, never # by the serving stack) -- but the field must be present to build the # config at serve time too. # ----------------------------------------------------------------------------- # # # If > 0, the TRAINING data loader zeroes the model's state input with this # # probability per sample (transforms.StateDropout), injected after Normalize # # and before model_transforms in data_loader.transform_dataset. Training-only # # by construction: the serving pipeline builds its own transform stack and # # never reads this field, so inference always sees the real state. # state_dropout_prob: float = 0.0 # # If > 0, the TRAINING data loader marks samples with this probability so that # # TokenizePrompt builds the prompt WITHOUT the discrete state segment # # (transforms.StateTokenDropout) -- the state-token analogue of # # state_dropout_prob for pi0.5 configs with discrete_state_input=True. Uses # # the tokenizer's first-class state=None format (NOT zero-fill). Train-only. # state_token_dropout_prob: float = 0.0