Download GR00T-WholeBodyControl/gear_sonic/trl/modules/universal_token_modules.py from Timsty/groot_deployment: direct link, hf CLI and curl.
- Browser
- Download file 58.3 kB
-
https://huggingface.co/Timsty/groot_deployment/resolve/main/GR00T-WholeBodyControl/gear_sonic/trl/modules/universal_token_modules.py
- Command line
-
hf download hf://Timsty/groot_deployment/GR00T-WholeBodyControl/gear_sonic/trl/modules/universal_token_modules.py
-
curl -L -o universal_token_modules.py https://huggingface.co/Timsty/groot_deployment/resolve/main/GR00T-WholeBodyControl/gear_sonic/trl/modules/universal_token_modules.py
58.3 kB
| from __future__ import annotations | |
| from loguru import logger | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| from gear_sonic.trl.utils import common | |
| def set_fuzzy_config_params(config, candidate_keys, value): | |
| """Set matching config keys to a given value. | |
| Scans ``config`` for any key that appears in ``candidate_keys`` and | |
| overwrites its value. Used to wire dynamically-computed dimensions | |
| (e.g. ``input_dim``, ``output_dim``) into encoder / decoder param | |
| dicts before instantiation. | |
| Args: | |
| config: Mutable mapping of parameter names to values. | |
| candidate_keys: Iterable of key names to look for in ``config``. | |
| value: Replacement value to assign to each matched key. | |
| Returns: | |
| The same ``config`` mapping with matching keys updated in-place. | |
| """ | |
| for key in candidate_keys: | |
| if key in config: | |
| config[key] = value | |
| return config | |
| class UniversalTokenModule(nn.Module): | |
| """SONIC-style action transform module (ATM) with FSQ token bottleneck. | |
| Implements the encoder → FSQ quantizer → decoder pipeline described in the | |
| SONIC paper. Multiple named encoders (e.g. ``g1``, ``smpl``, ``teleop``) | |
| convert different flavors of tokenizer observations into a shared latent | |
| space. An optional Finite Scalar Quantizer (FSQ) discretises the latent, | |
| producing a compact token representation. One or more decoders then map | |
| the tokens (plus proprioception) back to joint-space actions. | |
| Token flow:: | |
| tokenizer_obs ──► encoder(s) ──► [+ additive encoders] | |
| ──► [latent_residual (pre_quantization)] | |
| ──► FSQ quantizer | |
| ──► [+ latent_residual (post_quantization)] | |
| ──► decoder(s) | |
| ──► action_mean | |
| Latent residual modes allow an external HOI policy to inject corrections | |
| into the latent space without retraining the base ATM: | |
| * ``"post_quantization"`` - residual added to FSQ-quantized tokens (default). | |
| * ``"pre_quantization"`` - residual added *before* FSQ; the sum gets quantized. | |
| * ``"pre_quantization_replace"`` - latent is replaced entirely by the residual. | |
| Attributes: | |
| encoders: ``nn.ModuleDict`` of named encoder networks. | |
| quantizer: FSQ quantizer (``None`` when quantization is disabled). | |
| decoders: ``nn.ModuleDict`` of named decoder networks. | |
| aux_loss_func: ``nn.ModuleDict`` of auxiliary loss callables. | |
| token_dim: Dimensionality of a single token (equals ``num_fsq_levels``). | |
| max_num_tokens: Number of tokens produced per timestep. | |
| token_total_dim: ``token_dim * max_num_tokens`` - flat token size. | |
| """ | |
| def __init__( | |
| self, | |
| env_config, | |
| algo_config, | |
| obs_dim_dict=None, | |
| module_dim_dict=None, | |
| proprioception_features=[], | |
| num_fsq_levels=5, | |
| fsq_level_list=16, | |
| max_num_tokens=None, | |
| down_t=2, | |
| num_future_frames=1, | |
| quantizer=None, | |
| encoders=None, | |
| decoders=None, | |
| aux_loss_func={}, | |
| aux_loss_coef={}, | |
| encoder_sample_probs=None, | |
| reencode_smpl_g1_recon=False, | |
| meta_action_dim=None, # For hierarchical policies with split actions | |
| body_action_dim=None, # For separate body decoder (e.g., 29 body joints) | |
| hand_action_dim=None, # For separate hand decoder (e.g., 14 hand joints) | |
| freeze_encoders=False, | |
| freeze_decoders=False, | |
| freeze_quantizer=False, | |
| stiff_compliance_threshold=0.01, # Threshold for stiff mode filtering | |
| optimize_encoders_ratio_for_CHIP=False, # CHIP compliance training optimization | |
| active_encoders=None, # Optional list of encoder names to activate (None = all) | |
| active_decoders=None, # Optional list of decoder names to activate (None = all) | |
| **kwargs, # noqa: ARG002 | |
| ): | |
| """Initialise encoders, FSQ quantizer, decoders, and auxiliary losses. | |
| Args: | |
| env_config: Environment configuration object exposing | |
| ``obs.group_obs_dims``, ``obs.group_obs_names``, and | |
| ``robot.actions_dim``. | |
| algo_config: Algorithm configuration (stored but not directly | |
| accessed during construction). | |
| obs_dim_dict: Mapping from observation name to dimension. Falls | |
| back to ``env_config.robot.algo_obs_dim_dict`` when ``None``. | |
| module_dim_dict: Optional extra dimension overrides for named | |
| intermediate features. | |
| proprioception_features: List of observation keys whose concatenation | |
| forms the proprioception input fed to every decoder. | |
| num_fsq_levels: Number of FSQ levels (equals ``token_dim``). | |
| fsq_level_list: Per-level codebook size. An ``int`` is broadcast | |
| to ``[fsq_level_list] * num_fsq_levels``. | |
| max_num_tokens: Explicit token count per timestep. When ``None`` | |
| this is derived as ``max(1, num_future_frames // 2**down_t)``. | |
| down_t: Temporal downsampling factor for token count derivation. | |
| num_future_frames: Number of future motion frames in the tokenizer | |
| input window. | |
| quantizer: Hydra-instantiable config for the FSQ quantizer. | |
| ``None`` disables quantization (identity passthrough). | |
| encoders: Dict of encoder configs (Hydra DictConfig). Each entry | |
| specifies ``inputs``, ``outputs``, ``params``, and optional | |
| ``additive_to`` / ``sub_encoders`` / ``mask`` / ``freeze``. | |
| decoders: Dict of decoder configs (Hydra DictConfig). Each entry | |
| specifies ``inputs``, ``outputs``, ``conds``, ``params``, and | |
| ``has_temporal_dim``. | |
| aux_loss_func: Dict of auxiliary loss configs to instantiate as | |
| ``nn.Module`` callables (stored in ``self.aux_loss_func``). | |
| aux_loss_coef: Dict mapping loss name → scalar coefficient passed | |
| back to the trainer. | |
| encoder_sample_probs: Ordered dict mapping encoder name → sampling | |
| probability; defines the column order of ``encoder_index`` in | |
| tokenizer observations. | |
| reencode_smpl_g1_recon: When ``True``, re-encode the G1 kinematic | |
| decoder output to compute cycle-consistency auxiliary losses. | |
| meta_action_dim: Action dimensionality for hierarchical policies | |
| where the top-level action differs from the full joint count. | |
| body_action_dim: Dimensionality of body joints for split | |
| body / hand decoding (default 29). | |
| hand_action_dim: Dimensionality of hand joints for split | |
| body / hand decoding (default 14). | |
| freeze_encoders: Freeze all encoder parameters after init. | |
| freeze_decoders: Freeze all decoder parameters after init. | |
| freeze_quantizer: Freeze quantizer parameters after init. | |
| stiff_compliance_threshold: Absolute compliance value below which | |
| an environment is treated as "stiff" for auxiliary loss gating. | |
| optimize_encoders_ratio_for_CHIP: When ``True``, enforce one-hot | |
| encoder selection (CHIP training mode) instead of the default | |
| multi-hot selection where SMPL-native envs activate both SMPL | |
| and G1 encoders. | |
| active_encoders: Optional list of encoder names to instantiate and | |
| run. All encoders are active when ``None``. | |
| active_decoders: Optional list of decoder names to run during | |
| forward. All decoders are run when ``None``. | |
| **kwargs: Absorbed for forward-compatibility. | |
| """ | |
| super().__init__() | |
| # Store freeze flags | |
| self.freeze_encoders = freeze_encoders | |
| self.freeze_decoders = freeze_decoders | |
| self.stiff_compliance_threshold = stiff_compliance_threshold | |
| self.freeze_quantizer = freeze_quantizer | |
| self.optimize_encoders_ratio_for_CHIP = optimize_encoders_ratio_for_CHIP | |
| if self.optimize_encoders_ratio_for_CHIP: | |
| logger.info( | |
| "[CHIP Mode] optimize_encoders_ratio_for_CHIP=True: " | |
| "Native encoder selection is now one-hot (no G1 auto-activation for SMPL). " | |
| "G1 latents for SMPL-native envs computed only in aux losses when compliance=0." | |
| ) | |
| # Cache for last encoded tokens (for external access, e.g., by callbacks) | |
| # These are populated during forward() and can be read afterwards | |
| self._last_encoded_tokens = None # dict[encoder_name -> Tensor] | |
| self._last_encoded_latents = None # dict[encoder_name -> Tensor] (pre-quantization) | |
| self.env_config = env_config | |
| self.algo_config = algo_config | |
| self.encoders_cfg = encoders | |
| self.decoders_cfg = decoders | |
| self.encoder_sample_probs = encoder_sample_probs | |
| self.reencode_smpl_g1_recon = reencode_smpl_g1_recon | |
| self.tokenizer_obs_dims = self.env_config.obs.group_obs_dims["tokenizer"] | |
| self.tokenizer_obs_names = self.env_config.obs.group_obs_names["tokenizer"] | |
| # The tokenizer observation schema is fixed after module construction. | |
| tokenizer_obs_specs = [] | |
| tokenizer_obs_offset = 0 | |
| for name in self.tokenizer_obs_names: | |
| dims = tuple(self.tokenizer_obs_dims[name]) | |
| flat_dim = int(np.prod(dims)) | |
| tokenizer_obs_specs.append( | |
| (name, tokenizer_obs_offset, tokenizer_obs_offset + flat_dim, dims) | |
| ) | |
| tokenizer_obs_offset += flat_dim | |
| self.tokenizer_obs_specs = tuple(tokenizer_obs_specs) | |
| self.tokenizer_obs_total_dim = tokenizer_obs_offset | |
| self.actions_dim = self.env_config.robot.actions_dim | |
| self.meta_action_dim = ( | |
| meta_action_dim # Can be different from actions_dim for hierarchical policies | |
| ) | |
| if obs_dim_dict is None: | |
| obs_dim_dict = getattr(env_config.robot, "algo_obs_dim_dict", {}) | |
| if module_dim_dict is None: | |
| module_dim_dict = {} | |
| self.obs_dim_dict = obs_dim_dict | |
| self.module_dim_dict = module_dim_dict | |
| self.proprioception_features = proprioception_features | |
| self.num_future_frames = num_future_frames | |
| # Initialize auxiliary loss functions (nn.ModuleDict so sub-module buffers | |
| # are part of the model state_dict and visible to DDP) | |
| self.aux_loss_func = nn.ModuleDict() | |
| for name, loss_func in aux_loss_func.items(): | |
| self.aux_loss_func[name] = common.custom_instantiate(loss_func, _resolve=False) | |
| self.aux_loss_coef = aux_loss_coef | |
| module_input_candidate_params = ["input_dim"] | |
| module_output_candidate_params = ["output_dim"] | |
| """ | |
| Initialize quantizer | |
| """ | |
| if isinstance(fsq_level_list, int): | |
| fsq_level_list = [fsq_level_list] * num_fsq_levels | |
| if quantizer is not None: | |
| self.quantizer = common.custom_instantiate( | |
| quantizer, levels=fsq_level_list, _resolve=False | |
| ) | |
| else: | |
| self.quantizer = None | |
| self.num_fsq_levels = num_fsq_levels | |
| self.fsq_level_list = fsq_level_list | |
| # quantizer levels determine the embedding dimension | |
| self.token_dim = self.num_fsq_levels | |
| self.down_t = down_t # default | |
| if max_num_tokens is not None: | |
| self.max_num_tokens = max_num_tokens | |
| else: | |
| self.max_num_tokens = max(1, self.num_future_frames // (2**self.down_t)) | |
| self.token_total_dim = self.token_dim * self.max_num_tokens | |
| logger.info( | |
| f"Motion Encoder and Quantizer initialized with embedding dim: {self.token_total_dim} (num_tokens={self.max_num_tokens}, token_dim={self.token_dim})" # noqa: E501 | |
| ) | |
| """ | |
| Initialize motion encoders dynamically | |
| """ | |
| self.encoders = nn.ModuleDict() | |
| self.sub_encoders = {} | |
| self.encoder_input_features = {} | |
| self.encoder_mask_features = {} | |
| self.encoders_to_iterate = [] | |
| self.additive_encoders = {} # Map: base_encoder_name -> list of additive encoder names | |
| # First pass: identify additive encoders | |
| for encoder_name, encoder_config in self.encoders_cfg.items(): | |
| additive_to = encoder_config.get("additive_to", None) | |
| if additive_to is not None: | |
| if additive_to not in self.additive_encoders: | |
| self.additive_encoders[additive_to] = [] | |
| self.additive_encoders[additive_to].append(encoder_name) | |
| # New config structure - initialize all encoders from config | |
| for encoder_name, encoder_config in self.encoders_cfg.items(): | |
| # Skip non-active encoders early (avoids KeyError on missing obs dims) | |
| if active_encoders is not None and encoder_name not in active_encoders: | |
| continue | |
| sub_encoder_only = encoder_config.get("sub_encoder_only", False) | |
| is_additive = encoder_config.get("additive_to", None) is not None | |
| # Don't iterate additive encoders independently - they're called within their base encoder | |
| if not sub_encoder_only and not is_additive: | |
| self.encoders_to_iterate.append(encoder_name) | |
| self.sub_encoders[encoder_name] = encoder_config.get("sub_encoders", []) | |
| input_features = encoder_config.get("inputs", []) | |
| output_features = encoder_config.get("outputs", []) | |
| input_feature_dim = sum([self.tokenizer_obs_dims[key][-1] for key in input_features]) | |
| if len(output_features) > 0: | |
| output_feature_dim = sum( | |
| [self.tokenizer_obs_dims[key][-1] for key in output_features] | |
| ) | |
| else: | |
| output_feature_dim = self.token_dim | |
| self.encoder_input_features[encoder_name] = input_features | |
| self.encoder_mask_features[encoder_name] = list(encoder_config.get("mask", [])) | |
| if len(self.sub_encoders[encoder_name]) > 0: | |
| continue | |
| encoder_params = encoder_config["params"].copy() | |
| # Set dynamic parameters | |
| set_fuzzy_config_params( | |
| encoder_params, module_input_candidate_params, input_feature_dim | |
| ) | |
| set_fuzzy_config_params( | |
| encoder_params, module_output_candidate_params, output_feature_dim | |
| ) | |
| # Instantiate encoder | |
| encoder = common.custom_instantiate(encoder_params, _resolve=False) | |
| self.encoders[encoder_name] = encoder | |
| # Per-encoder freeze support | |
| if encoder_config.get("freeze", False): | |
| for param in encoder.parameters(): | |
| param.requires_grad = False | |
| logger.info(f"Froze encoder: {encoder_name}") | |
| logger.info(f"Initialized {encoder_name} encoder with input features: {input_features}") | |
| # Log additive encoder relationships | |
| if self.additive_encoders: | |
| for base_encoder, additive_list in self.additive_encoders.items(): | |
| logger.info( | |
| f"Additive encoders for '{base_encoder}': {additive_list} (outputs will be summed)" | |
| ) | |
| """ | |
| Initialize motion decoders dynamically | |
| """ | |
| self.decoders = nn.ModuleDict() | |
| self.decoder_input_features = {} | |
| self.decoder_output_features = {} | |
| self.decoder_output_feature_dims = {} | |
| self.decoder_cond_features = {} # For root-disentangled decoders (external_cond) | |
| self.decoder_mask_features = {} | |
| # Support custom meta_action_dim for hierarchical policies | |
| meta_action_dim = getattr(self, "meta_action_dim", None) or self.actions_dim | |
| # Compute proprioception dim from actual features (not hardcoded actor_obs) | |
| proprioception_dim = sum( | |
| self.obs_dim_dict[key] | |
| for key in self.proprioception_features | |
| if key in self.obs_dim_dict | |
| ) or self.obs_dim_dict.get("actor_obs", 0) | |
| decoder_feature_dims_map = { | |
| "proprioception": proprioception_dim, | |
| "token": self.token_dim, | |
| "token_flattened": self.token_dim * self.max_num_tokens, | |
| "action": self.actions_dim, | |
| "meta_action": meta_action_dim, # For hierarchical policies | |
| "hand_action": hand_action_dim or 14, # 14 hand joints default | |
| "body_action": body_action_dim or 29, # 29 body joints default | |
| } | |
| for k, v in self.tokenizer_obs_dims.items(): | |
| if k not in decoder_feature_dims_map: | |
| decoder_feature_dims_map[k] = v[-1] | |
| self.decoder_feature_dims_map = decoder_feature_dims_map | |
| for decoder_name, decoder_config in self.decoders_cfg.items(): | |
| # Skip non-active decoders early (avoids KeyError on missing obs dims) | |
| if active_decoders is not None and decoder_name not in active_decoders: | |
| continue | |
| decoder_params = decoder_config["params"].copy() | |
| input_features = decoder_config.get("inputs", []) | |
| output_features = decoder_config.get("outputs", []) | |
| cond_features = decoder_config.get("conds", []) | |
| has_temporal_dim = decoder_config.has_temporal_dim | |
| input_feature_dim = 0 | |
| for key in input_features: | |
| input_feature_dim += self.decoder_feature_dims_map[key] | |
| output_feature_dim = 0 | |
| self.decoder_output_feature_dims[decoder_name] = {} | |
| for key in output_features: | |
| if key in decoder_feature_dims_map: | |
| feature_dim = decoder_feature_dims_map[key] | |
| else: | |
| assert has_temporal_dim | |
| feature_dim = self.tokenizer_obs_dims[key][-1] | |
| self.decoder_output_feature_dims[decoder_name][key] = feature_dim | |
| output_feature_dim += feature_dim | |
| set_fuzzy_config_params( | |
| decoder_params, module_input_candidate_params, input_feature_dim | |
| ) | |
| set_fuzzy_config_params( | |
| decoder_params, module_output_candidate_params, output_feature_dim | |
| ) | |
| # For decoders with conds: compute external_cond_dim from cond feature dimensions. | |
| if cond_features: | |
| external_cond_dim = sum(self.tokenizer_obs_dims[key][-1] for key in cond_features) | |
| decoder_params["external_cond_dim"] = external_cond_dim | |
| logger.info( | |
| f"Decoder '{decoder_name}' has conds={cond_features} " | |
| f"with external_cond_dim={external_cond_dim}" | |
| ) | |
| self.decoder_cond_features[decoder_name] = cond_features | |
| self.decoder_mask_features[decoder_name] = list(decoder_config.get("mask", [])) | |
| # Instantiate decoder | |
| decoder = common.custom_instantiate(decoder_params, _resolve=False) | |
| self.decoders[decoder_name] = decoder | |
| self.decoder_input_features[decoder_name] = input_features | |
| self.decoder_output_features[decoder_name] = output_features | |
| logger.info( | |
| f"Initialized {decoder_name} decoder with input features: {input_features} and output features: {output_features}" # noqa: E501 | |
| ) | |
| # Filter active encoders/decoders if specified (for kinematic-only training) | |
| if active_encoders is not None: | |
| self.encoders_to_iterate = [e for e in self.encoders_to_iterate if e in active_encoders] | |
| logger.info(f"Active encoders filtered to: {self.encoders_to_iterate}") | |
| if active_decoders is not None: | |
| # Keep all decoders in ModuleDict (for checkpoint compat), but only iterate active ones | |
| self._active_decoders = set(active_decoders) | |
| logger.info(f"Active decoders filtered to: {list(self._active_decoders)}") | |
| else: | |
| self._active_decoders = None | |
| # Apply freeze logic | |
| if self.freeze_encoders: | |
| for encoder in self.encoders.values(): | |
| for param in encoder.parameters(): | |
| param.requires_grad = False | |
| logger.info(f"Froze encoders: {list(self.encoders.keys())}") | |
| if self.freeze_decoders: | |
| for decoder in self.decoders.values(): | |
| for param in decoder.parameters(): | |
| param.requires_grad = False | |
| logger.info(f"Froze decoders: {list(self.decoders.keys())}") | |
| if self.freeze_quantizer and self.quantizer is not None: | |
| for param in self.quantizer.parameters(): | |
| param.requires_grad = False | |
| logger.info("Froze quantizer") | |
| # Variable frame support: enabled when any encoder or decoder has mask observations | |
| self.variable_frames_enabled = any( | |
| feats | |
| for feats in ( | |
| *self.encoder_mask_features.values(), | |
| *self.decoder_mask_features.values(), | |
| ) | |
| ) | |
| if self.variable_frames_enabled: | |
| # Cache frame indices for mask creation (avoids allocation every forward) | |
| self.register_buffer( | |
| "_frame_indices", | |
| torch.arange(self.num_future_frames).unsqueeze(0), | |
| persistent=False, | |
| ) | |
| def _create_frame_and_token_masks(self, tokenizer_obs): | |
| """Create frame and token masks from num_frames observation. | |
| Returns (frame_mask, token_mask) or (None, None) if disabled. | |
| frame_mask: [B*seq, max_frames] bool, True=valid | |
| token_mask: [B*seq, max_tokens] bool, True=valid | |
| """ | |
| if "command_num_frames" not in tokenizer_obs: | |
| return None, None | |
| num_frames = tokenizer_obs["command_num_frames"] # [B, seq, 1] | |
| num_frames_flat = num_frames.reshape(-1, 1) # [B*seq, 1] | |
| frame_mask = self._frame_indices < num_frames_flat # [B*seq, max_frames] | |
| frames_per_token = 2**self.down_t | |
| token_frame_mask = frame_mask.reshape(-1, self.max_num_tokens, frames_per_token) | |
| token_mask = token_frame_mask.all(dim=-1) # [B*seq, max_tokens] | |
| return frame_mask, token_mask | |
| def parse_tokenizer_obs(self, input_data): | |
| """Split the flat tokenizer observation tensor into a named dict. | |
| The tokenizer observation is stored as a single concatenated vector in | |
| ``input_data["tokenizer"]``. This method reshapes each slice back to | |
| its original multi-dimensional form using the dimension metadata from | |
| ``env_config``. | |
| Args: | |
| input_data: Dict containing ``"tokenizer"`` key with shape | |
| ``(..., total_tokenizer_dim)``. | |
| Returns: | |
| Dict mapping observation name to tensor with shape | |
| ``(*batch_dims, *obs_dims)`` for each registered tokenizer | |
| observation. | |
| """ | |
| tokenizer_obs = input_data["tokenizer"] | |
| tokenizer_obs_dict = {} | |
| for name, start, end, dims in self.tokenizer_obs_specs: | |
| tokenizer_obs_dict[name] = tokenizer_obs[..., start:end].reshape( | |
| tokenizer_obs.shape[:-1] + dims | |
| ) | |
| assert self.tokenizer_obs_total_dim == tokenizer_obs.shape[-1], ( | |
| f"{self.tokenizer_obs_total_dim=}, {tokenizer_obs.shape[-1]=}" | |
| ) | |
| return tokenizer_obs_dict | |
| def create_encoder_masks(self, tokenizer_obs): | |
| """Create encoder masks for each encoder. | |
| When optimize_encoders_ratio_for_CHIP=False (legacy): | |
| - encoder_index is multi-hot: SMPL-native envs have both SMPL=1 and G1=1 | |
| - encoder_masks["g1"] includes G1-native AND SMPL-native envs | |
| - encoder_masks["g1_has_smpl"] identifies which G1-masked envs are SMPL-native | |
| When optimize_encoders_ratio_for_CHIP=True (CHIP mode): | |
| - encoder_index is one-hot: each env has exactly one native encoder | |
| - encoder_masks are disjoint (no overlap) | |
| - encoder_masks["g1_has_smpl"] is empty (G1-native envs don't have SMPL) | |
| - G1 latents for SMPL-native envs are computed separately in aux losses | |
| """ | |
| if len(self.encoders) == 1: | |
| return {list(self.encoders.keys())[0]: None} | |
| encoder_masks = {} | |
| encoder_index = tokenizer_obs["encoder_index"] | |
| for i, encoder_name in enumerate(self.encoder_sample_probs.keys()): | |
| encoder_masks[encoder_name] = encoder_index[..., i].bool().flatten() | |
| # Re-organized creation of intersection encoder masks. | |
| # NOTE: No need to use compliance encoders in this version -- directly use the normal encoders | |
| # For encoder pairs, create combined masks reflecting shared samples. | |
| # The pairs and their intersection masks: | |
| encoder_mask_combinations = [ | |
| # (key1, key2, intersection_mask_names) | |
| ("g1", "smpl", [("g1_has_smpl", "smpl", "g1")]), | |
| ( | |
| "teleop", | |
| "smpl", | |
| [("teleop_has_smpl", "smpl", "teleop"), ("smpl_has_teleop", "teleop", "smpl")], | |
| ), | |
| ("g1", "teleop", [("g1_has_teleop", "teleop", "g1")]), | |
| ("teleop", "g1", [("teleop_has_g1", "g1", "teleop")]), | |
| ("g1", "soma", [("g1_has_soma", "soma", "g1")]), | |
| ] | |
| for key1, key2, mask_defs in encoder_mask_combinations: | |
| if key1 in encoder_masks and key2 in encoder_masks: | |
| for mask_name, mask_src, mask_cond in mask_defs: | |
| encoder_masks[mask_name] = encoder_masks[mask_src][encoder_masks[mask_cond]] | |
| return encoder_masks | |
| def assemble_all_tokens(self, encoded_tokens, encoder_masks, batch_size, seq_len): | |
| """Scatter per-encoder tokens into a single batch-aligned tensor. | |
| Each encoder processes only the environments assigned to it (via | |
| ``encoder_masks``). This method writes each encoder's output back to | |
| the correct rows of a zero-initialised buffer, then reshapes to | |
| ``(batch, seq, num_tokens, token_dim)``. | |
| Args: | |
| encoded_tokens: Dict mapping encoder name to tensor of shape | |
| ``(num_masked, num_tokens, token_dim)``. | |
| encoder_masks: Dict mapping encoder name to a boolean index tensor | |
| of length ``batch*seq``. ``None`` means all samples. | |
| batch_size: Batch dimension ``B``. | |
| seq_len: Sequence dimension ``S``. | |
| Returns: | |
| Tensor of shape ``(B, S, num_tokens, token_dim)`` with each row | |
| filled by its assigned encoder's output. | |
| """ | |
| first_token = encoded_tokens[list(encoded_tokens.keys())[0]] | |
| if len(encoded_tokens) == 1: | |
| return first_token.view(batch_size, seq_len, *first_token.shape[1:]) | |
| all_tokens = torch.zeros( | |
| (batch_size * seq_len,) + first_token.shape[1:], | |
| dtype=first_token.dtype, | |
| device=first_token.device, | |
| ) | |
| for encoder_name in encoded_tokens.keys(): | |
| all_tokens[encoder_masks[encoder_name]] = encoded_tokens[encoder_name] | |
| all_tokens = all_tokens.view(batch_size, seq_len, *all_tokens.shape[1:]) | |
| return all_tokens | |
| def _encode_single(self, encoder_name, tokenizer_obs, encoder_mask=None, frame_mask=None): | |
| """Encode using a single encoder without quantization or additive composition. | |
| This is the core encoding logic used by both regular and additive encoders. | |
| Args: | |
| encoder_name: Name of the encoder to use | |
| tokenizer_obs: Dictionary of tokenizer observations | |
| encoder_mask: Optional mask for selecting specific samples | |
| frame_mask: Optional [B*seq, max_frames] bool mask for variable frame support | |
| Returns: | |
| latent: The encoded latent representation | |
| """ # noqa: D205 | |
| encoder = sub_encoders = None | |
| if encoder_name in self.encoders: | |
| encoder = self.encoders[encoder_name] | |
| else: | |
| sub_encoders = [ | |
| self.encoders[sub_encoder_name] | |
| for sub_encoder_name in self.sub_encoders[encoder_name] | |
| ] | |
| input_features = self.encoder_input_features[encoder_name] | |
| obs_list = [tokenizer_obs[key] for key in input_features] | |
| # Only handle dimension broadcasting for the "smpl" encoder_name and for obs with last dim 9 or 12 | |
| if "smpl" in encoder_name: | |
| max_ndim = max(obs.ndim for obs in obs_list) | |
| # Check if any obs needs broadcasting: (last dim is 9 or 12 and ndim < max_ndim) | |
| need_broadcast = any( | |
| (obs.shape[-1] in (9, 12) and obs.ndim < max_ndim) for obs in obs_list | |
| ) | |
| if need_broadcast: | |
| aligned_obs_list = [] | |
| # Use the first obs of max_ndim as the temporal reference | |
| target_obs = next(o for o in obs_list if o.ndim == max_ndim) | |
| temporal_dim = target_obs.shape[-2] | |
| for obs in obs_list: | |
| if obs.shape[-1] in (9, 12) and obs.ndim < max_ndim: | |
| # Add missing dimension(s) | |
| while obs.ndim < max_ndim: | |
| obs = obs.unsqueeze(-2) # noqa: PLW2901 | |
| # Expand temporal dimension | |
| expand_shape = list(obs.shape) | |
| expand_shape[-2] = temporal_dim | |
| obs = obs.expand(*expand_shape) # noqa: PLW2901 | |
| aligned_obs_list.append(obs) | |
| encoder_input = torch.cat(aligned_obs_list, dim=-1) | |
| else: | |
| encoder_input = torch.cat(obs_list, dim=-1) | |
| else: | |
| encoder_input = torch.cat(obs_list, dim=-1) | |
| encoder_input = encoder_input.view(-1, *encoder_input.shape[2:]) | |
| # Apply encoder_mask to both input and frame_mask | |
| frame_mask_enc = None | |
| if encoder_mask is not None: | |
| encoder_input = encoder_input[encoder_mask] | |
| if frame_mask is not None: | |
| frame_mask_enc = frame_mask[encoder_mask] | |
| elif frame_mask is not None: | |
| frame_mask_enc = frame_mask | |
| # encode using the provided encoder (with gradients enabled for end-to-end training) | |
| if encoder is not None: | |
| if frame_mask_enc is not None and self.encoder_mask_features.get(encoder_name): | |
| latent = encoder(encoder_input, frame_mask=frame_mask_enc) | |
| else: | |
| latent = encoder(encoder_input) | |
| else: | |
| latent = encoder_input | |
| for sub_encoder in sub_encoders: | |
| latent = sub_encoder(latent) | |
| return latent | |
| def encode( | |
| self, encoder_name, tokenizer_obs, encoder_mask=None, no_quantization=False, frame_mask=None | |
| ): | |
| """Encode using the specified encoder, including any additive encoders. | |
| Args: | |
| encoder_name: Name of the base encoder to use | |
| tokenizer_obs: Dictionary of tokenizer observations | |
| encoder_mask: Optional mask for selecting specific samples | |
| no_quantization: If True, skip quantization and return raw latent | |
| frame_mask: Optional [B*seq, max_frames] bool mask for variable frame support | |
| Returns: | |
| If no_quantization: latent tensor | |
| Otherwise: (encoded_tokens, latent) tuple | |
| """ | |
| # Get base encoder latent | |
| latent = self._encode_single( | |
| encoder_name, tokenizer_obs, encoder_mask, frame_mask=frame_mask | |
| ) | |
| # Add contributions from additive encoders | |
| if encoder_name in self.additive_encoders: | |
| for additive_encoder_name in self.additive_encoders[encoder_name]: | |
| additive_latent = self._encode_single( | |
| additive_encoder_name, tokenizer_obs, encoder_mask, frame_mask=frame_mask | |
| ) | |
| latent = latent + additive_latent | |
| if no_quantization: | |
| return latent | |
| # quantize using quantizer | |
| if self.quantizer is not None: | |
| quantized_codes, _ = self.quantizer(latent) | |
| encoded_tokens = quantized_codes.contiguous() | |
| else: | |
| encoded_tokens = latent | |
| return encoded_tokens, latent | |
| def decode(self, decoder_name, decode_input_dict, token_mask=None): | |
| """Run a single named decoder and split its output by feature. | |
| Concatenates the decoder's declared input features from | |
| ``decode_input_dict``, passes them through the decoder network | |
| (optionally supplying ``external_cond`` and ``token_mask``), then | |
| slices the output back into a per-feature dict. | |
| Args: | |
| decoder_name: Key into ``self.decoders`` and associated metadata. | |
| decode_input_dict: Dict containing at least the keys declared in | |
| the decoder's ``inputs`` and ``conds`` config entries. | |
| Common keys include ``"token"``, ``"token_flattened"``, and | |
| ``"proprioception"``. | |
| token_mask: Optional ``(B*seq, max_tokens)`` bool mask for | |
| variable-length token sequences. Only forwarded to decoders | |
| that declare mask features. | |
| Returns: | |
| Dict mapping output feature name to the corresponding slice of the | |
| decoder output tensor, e.g. | |
| ``{"action": Tensor(..., action_dim)}``. | |
| """ | |
| decoder = self.decoders[decoder_name] | |
| input_features = self.decoder_input_features[decoder_name] | |
| output_feature_dims = self.decoder_output_feature_dims[decoder_name] | |
| cond_features = self.decoder_cond_features.get(decoder_name, []) | |
| decoder_input = torch.cat([decode_input_dict[key] for key in input_features], dim=-1) | |
| # Build optional kwargs for the decoder call | |
| kwargs = {} | |
| if cond_features: | |
| kwargs["external_cond"] = torch.cat( | |
| [decode_input_dict[key] for key in cond_features], dim=-1 | |
| ) | |
| if token_mask is not None and self.decoder_mask_features.get(decoder_name): | |
| kwargs["token_mask"] = token_mask | |
| output = decoder(decoder_input, **kwargs) | |
| # parse output | |
| output_dict = {} | |
| index = 0 | |
| for key, dim in output_feature_dims.items(): | |
| output_dict[key] = output[..., index : index + dim] | |
| index += dim | |
| assert index == output.shape[-1], f"{index=}, {output.shape[-1]=}" | |
| return output_dict | |
| def forward( # noqa: D417 | |
| self, | |
| input_data, | |
| compute_aux_loss=False, | |
| return_dict=False, | |
| latent_residual=None, | |
| latent_residual_mode="post_quantization", | |
| **kwargs, # noqa: ARG002 | |
| ): | |
| """Run the full encode → quantize → decode pipeline. | |
| Parses tokenizer observations, routes each environment to its assigned | |
| encoder, optionally applies an external latent residual, then decodes | |
| to joint-space actions. When ``compute_aux_loss=True``, also computes | |
| all registered auxiliary losses (e.g. G1-SMPL alignment, cycle | |
| consistency) and returns a rich result dict. | |
| Args: | |
| input_data: Dict of named observation tensors, all with leading | |
| shape ``(B, S, ...)``. Must contain at minimum ``"actor_obs"`` | |
| (for batch/seq inference) and ``"tokenizer"`` (flat tokenizer | |
| obs of shape ``(B, S, total_tokenizer_dim)``). | |
| compute_aux_loss: When ``True``, evaluate all auxiliary loss | |
| functions and include them in the returned dict. | |
| return_dict: When ``True`` and ``compute_aux_loss=False``, return | |
| the full output dict instead of just ``action_mean``. | |
| latent_residual: Optional additive correction in latent token | |
| space. Shape ``(B, token_total_dim)`` where | |
| ``token_total_dim = max_num_tokens * token_dim``. Allows HOI | |
| policies to steer motion without modifying the base ATM. | |
| latent_residual_mode: Controls *when* ``latent_residual`` is | |
| applied: | |
| * ``"post_quantization"`` - add after FSQ (default, residual | |
| stays continuous). | |
| * ``"pre_quantization"`` - add before FSQ (residual gets | |
| quantized together with the encoder latent). | |
| * ``"pre_quantization_replace"`` - replace the encoder latent | |
| entirely with ``latent_residual`` before quantization. | |
| **kwargs: Passed through; currently unused. | |
| Returns: | |
| When ``compute_aux_loss=True`` or ``return_dict=True``: a dict | |
| with keys: | |
| * ``"action_mean"`` - joint targets, shape ``(B, S, action_dim)``. | |
| * ``"aux_losses"`` - dict of scalar loss tensors (empty when | |
| ``compute_aux_loss=False``). | |
| * ``"aux_loss_coef"`` - per-loss coefficient dict from config. | |
| * ``"decoded_outputs"`` - raw decoder output dicts keyed by | |
| decoder name. | |
| * ``"tokenizer_obs"`` - parsed tokenizer observation dict. | |
| * ``"encoder_masks"`` - per-encoder boolean index tensors. | |
| * ``"encoded_tokens"`` - post-quantization tokens per encoder. | |
| * ``"encoded_latents"`` - pre-quantization latents per encoder. | |
| * ``"encoders_cfg"`` / ``"decoders_cfg"`` - config references. | |
| When ``compute_aux_loss=False`` and ``return_dict=False``: | |
| ``action_mean`` tensor directly. | |
| """ | |
| # parse tokenizer obs | |
| batch_size, seq_len = input_data["actor_obs"].shape[:2] | |
| tokenizer_obs = self.parse_tokenizer_obs(input_data) | |
| proprioception_input = torch.cat( | |
| [input_data[key] for key in self.proprioception_features], dim=-1 | |
| ) | |
| # Reshape residual if provided: (batch, token_total_dim) -> (batch, 1, max_num_tokens, token_dim) | |
| residual_reshaped = None | |
| if latent_residual is not None: | |
| residual_reshaped = latent_residual.view( | |
| batch_size, 1, self.max_num_tokens, self.token_dim | |
| ) | |
| # Variable frame masks | |
| frame_mask, token_mask = self._create_frame_and_token_masks(tokenizer_obs) | |
| # encode motion using all available encoders | |
| encoder_masks = self.create_encoder_masks(tokenizer_obs) | |
| encoded_tokens = {} | |
| encoded_latents = {} | |
| if latent_residual is not None and latent_residual_mode in [ | |
| "pre_quantization", | |
| "pre_quantization_replace", | |
| ]: | |
| # PRE-QUANTIZATION MODES: add or replace residual before FSQ | |
| # Reshape residual to (batch*seq, num_tokens, token_dim) for masking | |
| residual_flat = latent_residual.view( | |
| batch_size * seq_len, self.max_num_tokens, self.token_dim | |
| ) | |
| for encoder_name in self.encoders_to_iterate: | |
| encoder_mask = encoder_masks[encoder_name] | |
| # Get raw latent (including additive encoders) without quantization | |
| latent = self.encode( | |
| encoder_name, | |
| tokenizer_obs, | |
| encoder_mask, | |
| no_quantization=True, | |
| frame_mask=frame_mask, | |
| ) | |
| # Apply same mask to residual before adding | |
| if encoder_mask is not None: | |
| masked_residual = residual_flat[encoder_mask] | |
| else: | |
| masked_residual = residual_flat | |
| # Apply residual before quantization (only if we have samples) | |
| if latent.shape[0] > 0: | |
| if latent_residual_mode == "pre_quantization": | |
| # Add residual to encoder latent | |
| latent = latent + masked_residual | |
| elif latent_residual_mode == "pre_quantization_replace": | |
| # Replace encoder latent with residual (zero out encoder) | |
| latent = masked_residual | |
| else: | |
| raise ValueError(f"Unknown latent_residual_mode: {latent_residual_mode}") | |
| # Now quantize | |
| if self.quantizer is not None: | |
| quantized_codes, _ = self.quantizer(latent) | |
| encoded_tokens[encoder_name] = quantized_codes.contiguous() | |
| else: | |
| encoded_tokens[encoder_name] = latent | |
| encoded_latents[encoder_name] = latent | |
| else: | |
| # STANDARD MODE: encode normally | |
| for encoder_name in self.encoders_to_iterate: | |
| encoded_tokens[encoder_name], encoded_latents[encoder_name] = self.encode( | |
| encoder_name, | |
| tokenizer_obs, | |
| encoder_masks[encoder_name], | |
| frame_mask=frame_mask, | |
| ) | |
| all_tokens = self.assemble_all_tokens(encoded_tokens, encoder_masks, batch_size, seq_len) | |
| # POST-QUANTIZATION MODE: add residual after FSQ tokens (default) | |
| if latent_residual is not None and latent_residual_mode == "post_quantization": | |
| all_tokens = all_tokens + residual_reshaped | |
| # Cache tokens for external access (e.g., by callbacks) | |
| self._last_encoded_tokens = {k: v.detach().cpu() for k, v in encoded_tokens.items()} | |
| self._last_encoded_latents = {k: v.detach().cpu() for k, v in encoded_latents.items()} | |
| # Cache flattened full latent on device for reward computation (token smoothness) | |
| # all_tokens is the post-quantization token that gets sent to decoder | |
| # Shape: (batch, seq, num_tokens, token_dim) -> (batch, seq, latent_dim) | |
| self._last_full_latent_flat = all_tokens.detach().view(*all_tokens.shape[:-2], -1) | |
| # decode action and motion | |
| decode_input_dict = { | |
| "token": all_tokens, | |
| "token_flattened": all_tokens.view(*all_tokens.shape[:-2], -1), | |
| "proprioception": proprioception_input, | |
| } | |
| decode_input_dict.update(tokenizer_obs) | |
| decoded_outputs = {} | |
| decoders_to_run = ( | |
| self._active_decoders if self._active_decoders is not None else self.decoders.keys() | |
| ) | |
| for decoder_name in decoders_to_run: | |
| decoded_outputs[decoder_name] = self.decode( | |
| decoder_name, decode_input_dict, token_mask=token_mask | |
| ) | |
| # Support "body_action", "meta_action", and "action" outputs (g1_dyn may not exist in kinematic-only mode) | |
| if "g1_dyn" in decoded_outputs: | |
| g1_dyn_out = decoded_outputs["g1_dyn"] | |
| if "body_action" in g1_dyn_out: | |
| action_mean = g1_dyn_out["body_action"] | |
| elif "meta_action" in g1_dyn_out: | |
| action_mean = g1_dyn_out["meta_action"] | |
| else: | |
| action_mean = g1_dyn_out["action"] | |
| # Concatenate hand decoder output if present | |
| if "hand_dyn" in decoded_outputs and "hand_action" in decoded_outputs["hand_dyn"]: | |
| hand_action = decoded_outputs["hand_dyn"]["hand_action"] | |
| action_mean = torch.cat([action_mean, hand_action], dim=-1) | |
| else: | |
| action_mean = None | |
| # compute aux losses | |
| if compute_aux_loss: | |
| aux_losses = {} | |
| # Initialize all paired latents | |
| reencoded_smpl_g1_latents = None | |
| paired_g1_smpl_latents = None | |
| paired_compliance_latents = None | |
| original_g1_latents_for_reencode = None | |
| # Determine encoder names | |
| smpl_encoder_name = "smpl" if "smpl" in self.encoders_to_iterate else None | |
| teleop_encoder_name = "teleop" if "teleop" in self.encoders_to_iterate else None | |
| # ========================================================================= | |
| # STIFF-MODE OPTIMIZATION: Check for stiff samples FIRST before computing | |
| # expensive G1 latents. G1-SMPL loss and cycle consistency loss ONLY apply | |
| # in stiff mode (compliance ≈ 0). Skip computation if all samples are compliant. | |
| # ========================================================================= | |
| has_stiff_samples = False | |
| compliance_values = None | |
| smpl_latents = None | |
| smpl_mask = None | |
| if smpl_encoder_name is not None and "g1" in encoded_latents: | |
| smpl_mask = encoder_masks.get(smpl_encoder_name) | |
| if smpl_mask is not None and smpl_mask.sum() > 0: | |
| smpl_latents = encoded_latents[smpl_encoder_name] | |
| # Extract compliance values for these envs FIRST | |
| if "compliance" in tokenizer_obs: | |
| compliance_flat = tokenizer_obs["compliance"].view( | |
| -1, tokenizer_obs["compliance"].shape[-1] | |
| ) | |
| compliance_values = compliance_flat[smpl_mask] | |
| # Check if ANY samples are stiff | |
| is_stiff = (compliance_values.abs() < self.stiff_compliance_threshold).all( | |
| dim=-1 | |
| ) | |
| has_stiff_samples = is_stiff.any().item() | |
| else: | |
| # No compliance info means all samples are "stiff" (default behavior) | |
| has_stiff_samples = True | |
| # Only compute stiff-mode losses if there are stiff samples | |
| if has_stiff_samples and smpl_mask is not None: | |
| # Compute G1 latents for the same envs as SMPL (for proper pairing) | |
| # OPTIMIZATION: Detach here since all downstream losses use g1 as | |
| # fixed target (detach_g1_target=True). This avoids redundant gradient | |
| # computation through the g1 encoder for these latents. | |
| g1_latents_for_smpl = self.encode( | |
| encoder_name="g1", | |
| tokenizer_obs=tokenizer_obs, | |
| encoder_mask=smpl_mask, | |
| no_quantization=True, | |
| ).detach() | |
| paired_g1_smpl_latents = { | |
| "g1": g1_latents_for_smpl, | |
| "smpl": smpl_latents, | |
| "compliance": compliance_values, | |
| } | |
| # Store original G1 latents for cycle consistency loss (already detached) | |
| original_g1_latents_for_reencode = g1_latents_for_smpl | |
| # Cycle consistency: re-encode decoded G1 motion (only if stiff samples exist) | |
| if self.reencode_smpl_g1_recon: | |
| reencoded_smpl_g1_latents = self.encode( | |
| encoder_name="g1", | |
| tokenizer_obs=decoded_outputs["g1_kin"], | |
| encoder_mask=smpl_mask, | |
| no_quantization=True, | |
| ) | |
| # Compute paired teleop-SMPL latents for TeleopSmplComplianceLatentLoss | |
| # NOTE: This applies to ALL compliance values (not just stiff), so no stiff check | |
| if ( | |
| teleop_encoder_name is not None | |
| and smpl_latents is not None | |
| and smpl_mask is not None | |
| ): | |
| # Compute teleop latents for the same envs as SMPL | |
| # OPTIMIZATION: Detach here since TeleopSmplComplianceLatentLoss uses | |
| # teleop as teacher (detach_teleop_target=True). Only SMPL learns. | |
| teleop_latents_for_smpl = self.encode( | |
| encoder_name=teleop_encoder_name, | |
| tokenizer_obs=tokenizer_obs, | |
| encoder_mask=smpl_mask, | |
| no_quantization=True, | |
| ).detach() | |
| paired_compliance_latents = { | |
| "teleop": teleop_latents_for_smpl, | |
| "smpl": smpl_latents, | |
| } | |
| loss_inputs = { | |
| "input_data": input_data, | |
| "tokenizer_obs": tokenizer_obs, | |
| "encoded_tokens": encoded_tokens, | |
| "encoded_latents": encoded_latents, | |
| "encoder_masks": encoder_masks, | |
| "decoded_outputs": decoded_outputs, | |
| "action_mean": action_mean, | |
| "encoders_cfg": self.encoders_cfg, | |
| "frame_mask": ( | |
| frame_mask.view(batch_size, seq_len, -1) if frame_mask is not None else None | |
| ), | |
| "token_mask": ( | |
| token_mask.view(batch_size, seq_len, -1) if token_mask is not None else None | |
| ), | |
| "decoders_cfg": self.decoders_cfg, | |
| "reencoded_smpl_g1_latents": reencoded_smpl_g1_latents, | |
| # New fields for compliance-aware losses | |
| "paired_g1_smpl_latents": paired_g1_smpl_latents, | |
| "paired_compliance_latents": paired_compliance_latents, | |
| "original_g1_latents_for_reencode": original_g1_latents_for_reencode, | |
| } | |
| for loss_name, loss_func in self.aux_loss_func.items(): | |
| aux_losses[loss_name] = loss_func(loss_inputs) | |
| output = { | |
| "action_mean": action_mean, | |
| "aux_losses": aux_losses, | |
| "aux_loss_coef": self.aux_loss_coef, | |
| # Exposed for recon trainer (existing RL callers ignore these keys) | |
| "decoded_outputs": decoded_outputs, | |
| "tokenizer_obs": tokenizer_obs, | |
| "encoder_masks": encoder_masks, | |
| "encoded_tokens": encoded_tokens, | |
| "encoded_latents": encoded_latents, | |
| "encoders_cfg": self.encoders_cfg, | |
| "decoders_cfg": self.decoders_cfg, | |
| } | |
| elif return_dict: | |
| output = { | |
| "action_mean": action_mean, | |
| "aux_losses": {}, | |
| "aux_loss_coef": self.aux_loss_coef, | |
| "decoded_outputs": decoded_outputs, | |
| "tokenizer_obs": tokenizer_obs, | |
| "encoder_masks": encoder_masks, | |
| "encoded_tokens": encoded_tokens, | |
| "encoded_latents": encoded_latents, | |
| "encoders_cfg": self.encoders_cfg, | |
| "decoders_cfg": self.decoders_cfg, | |
| } | |
| else: | |
| output = action_mean | |
| return output | |
| def get_token_info(self): | |
| """Return a summary of the FSQ token configuration. | |
| Returns: | |
| Dict with keys: | |
| * ``"token_dim"`` - dimensionality of one token. | |
| * ``"total_dim"`` - ``token_dim * max_num_tokens``. | |
| * ``"num_levels"`` - number of FSQ levels. | |
| * ``"level_list"`` - per-level codebook sizes, or ``None`` if the | |
| quantizer does not expose a ``levels`` attribute. | |
| * ``"model_available"`` - always ``True``. | |
| """ | |
| return { | |
| "token_dim": self.token_dim, | |
| "total_dim": self.token_total_dim, | |
| "num_levels": self.num_fsq_levels, | |
| "level_list": ( | |
| list(self.quantizer.levels) if hasattr(self.quantizer, "levels") else None | |
| ), | |
| "model_available": True, | |
| } | |
| def forward_with_external_tokens( # noqa: D417 | |
| self, input_data: dict, external_tokens: torch.Tensor, **kwargs # noqa: ARG002 | |
| ) -> torch.Tensor: | |
| """Forward pass with externally provided tokens (bypasses encoder). | |
| This is used when an external model (e.g., kinematic diffusion) provides | |
| pre-computed FSQ tokens, allowing the encoder to be bypassed while still | |
| using the decoder for action generation. | |
| Args: | |
| input_data: Dict with 'actor_obs' for proprioception | |
| external_tokens: Pre-computed FSQ tokens from kinematic diffusion | |
| Shape: (B, 2, 32) or (B, seq, 2, 32) | |
| Returns: | |
| action_mean: (B, action_dim) or (B, seq, action_dim) | |
| """ | |
| # Get proprioception input | |
| proprioception_input = torch.cat( | |
| [input_data[key] for key in self.proprioception_features], dim=-1 | |
| ) | |
| # Handle different input shapes | |
| if external_tokens.dim() == 3: | |
| # (B, 2, 32) -> add seq dim -> (B, 1, 2, 32) | |
| external_tokens = external_tokens.unsqueeze(1) | |
| # external_tokens: (B, seq, 2, 32) or (B, seq, num_tokens, token_dim) | |
| batch_size = external_tokens.shape[0] | |
| seq_len = external_tokens.shape[1] | |
| # Flatten tokens for decoder: (B, seq, 2, 32) -> (B, seq, 64) | |
| token_flattened = external_tokens.view(batch_size, seq_len, -1) | |
| # Build decode input dict | |
| decode_input_dict = { | |
| "token": external_tokens, | |
| "token_flattened": token_flattened, | |
| "proprioception": proprioception_input, | |
| } | |
| # Decode actions using g1_dyn decoder | |
| g1_dyn_out = self.decode("g1_dyn", decode_input_dict) | |
| if "body_action" in g1_dyn_out: | |
| action_mean = g1_dyn_out["body_action"] | |
| elif "meta_action" in g1_dyn_out: | |
| action_mean = g1_dyn_out["meta_action"] | |
| else: | |
| action_mean = g1_dyn_out["action"] | |
| # Concatenate hand decoder output if present | |
| if "hand_dyn" in self.decoders: | |
| hand_dyn_out = self.decode("hand_dyn", decode_input_dict) | |
| if "hand_action" in hand_dyn_out: | |
| action_mean = torch.cat([action_mean, hand_dyn_out["hand_action"]], dim=-1) | |
| # Squeeze seq dim if it was added | |
| if action_mean.shape[1] == 1: | |
| action_mean = action_mean.squeeze(1) | |
| return action_mean | |
| def get_example_input(self, encoder_name, batch_size=1, device="cpu"): | |
| """Generate example input for ONNX export with specific encoder. | |
| Args: | |
| encoder_name: Name of the encoder to use | |
| batch_size: Batch size for the example input | |
| device: Device to create tensors on | |
| Returns: | |
| Tensor with shape (batch_size, feature_dim) including encoder features and proprioception | |
| """ | |
| if encoder_name not in self.encoders: | |
| raise ValueError( | |
| f"Encoder '{encoder_name}' not found. Available encoders: {list(self.encoders.keys())}" | |
| ) | |
| # Calculate total input feature dimension for the specified encoder | |
| encoder_input_features = self.encoder_input_features[encoder_name] | |
| total_feature_dim = 0 | |
| for feature_name in encoder_input_features: | |
| if feature_name in self.tokenizer_obs_dims: | |
| feature_dims = self.tokenizer_obs_dims[feature_name] | |
| total_feature_dim += torch.prod(torch.tensor(feature_dims)).item() | |
| # Add proprioception dimension | |
| proprioception_dim = self.obs_dim_dict.get("actor_obs", 0) | |
| total_feature_dim += proprioception_dim | |
| # Create a single 2D tensor (B, F) including both encoder features and proprioception | |
| example_input = torch.randn(batch_size, total_feature_dim, device=device) | |
| return example_input | |
| def get_all_example_input(self, batch_size=1, device="cpu"): | |
| """Generate example inputs covering all tokenizer observations. | |
| Constructs random tensors whose shapes match the full tokenizer | |
| observation space plus proprioception. Intended for ONNX tracing | |
| or shape debugging. | |
| Args: | |
| batch_size: Number of examples in the batch dimension. | |
| device: Target device for the returned tensors (e.g. ``"cpu"`` | |
| or ``"cuda"``). | |
| Returns: | |
| Tuple of: | |
| * ``example_input`` - flat tensor of shape | |
| ``(batch_size, total_feature_dim)`` where | |
| ``total_feature_dim = tokenizer_feature_dim + proprioception_dim``. | |
| * ``example_dict`` - dict with keys ``"tokenizer"`` and | |
| ``"proprioception"``, each a random tensor with the | |
| corresponding shape. | |
| """ | |
| # Calculate total input feature dimension for the specified encoder | |
| total_feature_dim = 0 | |
| for feature_name in self.tokenizer_obs_names: | |
| feature_dims = self.tokenizer_obs_dims[feature_name] | |
| print( # noqa: T201 | |
| f"{feature_name}: {feature_dims}, start: {total_feature_dim} end: {total_feature_dim + torch.prod(torch.tensor(feature_dims)).item()}" # noqa: E501 | |
| ) | |
| total_feature_dim += torch.prod(torch.tensor(feature_dims)).item() | |
| tokenizer_feature_dim = total_feature_dim | |
| # Add proprioception dimension | |
| proprioception_dim = self.obs_dim_dict.get("actor_obs", 0) | |
| total_feature_dim += proprioception_dim | |
| print(f"tokenizer_feature_dim: {tokenizer_feature_dim}") # noqa: T201 | |
| print(f"proprioception_dim: {proprioception_dim}") # noqa: T201 | |
| # Create a single 2D tensor (B, F) including both encoder features and proprioception | |
| example_input = torch.randn(batch_size, total_feature_dim, device=device) | |
| example_dict = { | |
| "tokenizer": torch.randn(batch_size, tokenizer_feature_dim, device=device), | |
| "proprioception": torch.randn(batch_size, proprioception_dim, device=device), | |
| } | |
| return example_input, example_dict | |