Download starVLA/model/tools.py from mmmmmcyaaa/ReTime: direct link, hf CLI and curl.
- Browser
- Download file 17.7 kB
-
https://huggingface.co/mmmmmcyaaa/ReTime/resolve/main/starVLA/model/tools.py
- Command line
-
hf download hf://mmmmmcyaaa/ReTime/starVLA/model/tools.py
-
curl -L -o tools.py https://huggingface.co/mmmmmcyaaa/ReTime/resolve/main/starVLA/model/tools.py
17.7 kB
| def auto_get_module_keys(module, max_depth=0, prefix_list=None, current_depth=0, current_prefix=""): | |
| """ | |
| get all submodule keys of a module, support setting recursion depth and prefix list. | |
| :param module: the module to traverse. | |
| :param max_depth: the maximum recursion depth, default is 1. | |
| :param prefix_list: only include modules with specified prefix, default is None means no restriction. | |
| :param current_depth: the current recursion depth, internal use. | |
| :param current_prefix: the current prefix, internal use. | |
| :return: the list of module keys. | |
| """ | |
| if current_depth > max_depth: | |
| return [] | |
| module_keys = [] | |
| for name, sub_module in module.named_children(): | |
| full_name = f"{current_prefix}.{name}" if current_prefix else name | |
| if prefix_list is None or any(full_name.startswith(prefix) for prefix in prefix_list): | |
| module_keys.append(full_name) | |
| module_keys.extend(auto_get_module_keys(sub_module, max_depth, prefix_list, current_depth + 1, full_name)) | |
| return module_keys | |
| def is_module_trainable(module): | |
| """ | |
| check if a module is trainable: if the module itself has parameters, then all its parameters require_grad must be True; | |
| if the module itself has no parameters, then its trainability depends on its submodules. | |
| """ | |
| params = list(module.parameters(recurse=False)) | |
| if params: | |
| return all(p.requires_grad for p in params) | |
| else: | |
| # for container modules with no direct parameters, consider them trainable (the final result depends on their submodules) | |
| return True | |
| def auto_get_trainable_modules(module, prefix="", max_depth=None): | |
| """ | |
| recursively traverse the module, return the list of all trainable module names. | |
| if all submodules of a module are trainable, then only return the name of the parent module, no longer recursively output the names of its submodules. | |
| parameters: | |
| - module: the module to traverse. | |
| - prefix: the name prefix of the current module (internal use). | |
| - max_depth: the maximum recursion depth, None means infinite recursion. | |
| return: | |
| a list of module names. | |
| """ | |
| # get all direct submodules of the current module | |
| children = list(module.named_children()) | |
| # if the maximum depth is reached or there are no submodules, return the current module (if trainable and prefix is not empty) | |
| if (max_depth is not None and max_depth <= 0) or not children: | |
| return [prefix] if prefix and is_module_trainable(module) else [] | |
| child_keys = [] | |
| all_children_trainable = True | |
| for name, child in children: | |
| full_name = f"{prefix}.{name}" if prefix else name | |
| # recursively get the trainable keys of the submodules | |
| keys = auto_get_trainable_modules(child, full_name, None if max_depth is None else max_depth - 1) | |
| if not keys: | |
| # if the submodule does not return any further submodules, check the submodule itself | |
| if is_module_trainable(child): | |
| keys = [full_name] | |
| else: | |
| all_children_trainable = False | |
| else: | |
| # if the submodule returns multiple names, it means that it cannot be merged | |
| if len(keys) > 1: | |
| all_children_trainable = False | |
| child_keys.extend(keys) | |
| # if the current module is trainable and all submodules are trainable, return the name of the current module | |
| if is_module_trainable(module) and all_children_trainable and child_keys: | |
| return [prefix] if prefix else child_keys | |
| else: | |
| return child_keys | |
| def print_freeze_status(self): | |
| """ | |
| for each top-level submodule, if all its parameters are in the same state (all frozen or all trainable), only print the top-level module. | |
| if some top-level submodule has mixed parameter states (some frozen, some trainable), list the state of each parameter under the submodule. | |
| """ | |
| from collections import defaultdict | |
| # collect the state of parameters under each top-level module | |
| status_dict = defaultdict(lambda: {"Frozen": 0, "Trainable": 0, "params": []}) | |
| for full_name, param in self.named_parameters(): | |
| # full_name is like "qwen_vl_interface.model.layer.weight" | |
| top_module = full_name.split(".", 1)[0] # get the top-level module name | |
| state = "Frozen" if not param.requires_grad else "Trainable" | |
| status_dict[top_module]["params"].append((full_name, state)) | |
| status_dict[top_module][state] += 1 | |
| print("=== module parameter freezing status ===") | |
| for top_module, info in status_dict.items(): | |
| frozen_count = info["Frozen"] | |
| trainable_count = info["Trainable"] | |
| if frozen_count > 0 and trainable_count == 0: | |
| # all frozen | |
| print(f"{top_module:40s} | all Frozen ({frozen_count} parameters)") | |
| elif trainable_count > 0 and frozen_count == 0: | |
| # all trainable | |
| print(f"{top_module:40s} | all Trainable ({trainable_count} parameters)") | |
| else: | |
| # mixed state, first print the module name summary, then list the state of each parameter | |
| print(f"{top_module:40s} | mixed state → Frozen: {frozen_count}, Trainable: {trainable_count}") | |
| for pname, pstate in info["params"]: | |
| print(f" {pname:60s} | {pstate}") | |
| print("=========================\n") | |
| class Registry: | |
| def __init__(self, name: str): | |
| self.name = name | |
| self._registry = {} | |
| def register(self, key: str): | |
| """Decorator: register a builder function or class""" | |
| def decorator(framework_class): | |
| if key in self._registry: | |
| # print(ImportWarning(f"{key} already registered to {self.name}")) | |
| pass | |
| self._registry[key] = framework_class | |
| return framework_class | |
| return decorator | |
| def __getitem__(self, key): | |
| return self._registry[key] | |
| def list(self): | |
| """ | |
| List currently registered keys; if with_values=True (not used here) return mapping {key: value_obj}. | |
| Using class name as value is also intuitive, e.g., framework.__name__. | |
| """ | |
| return {k: v for k, v in self._registry.items()} | |
| FRAMEWORK_REGISTRY = Registry("frameworks") | |
| import json | |
| import os | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from omegaconf import OmegaConf | |
| from PIL import Image | |
| from torchvision import transforms as TF | |
| from starVLA.training.trainer_utils import initialize_overwatch | |
| # Initialize Overwatch =>> Wraps `logging.Logger` | |
| overwatch = initialize_overwatch(__name__) | |
| def read_mode_config(pretrained_checkpoint): | |
| """Re-export from share_tools — canonical version lives in starVLA.model.framework.share_tools.""" | |
| from starVLA.model.framework.share_tools import read_mode_config as _read_mode_config | |
| return _read_mode_config(pretrained_checkpoint) | |
| class FrameworkTools: | |
| """Model-agnostic utility helpers for action (un)normalization and trainable-module discovery. | |
| These are pure functions / static methods that do NOT depend on any model state. | |
| ``baseframework`` holds a class-level reference so that legacy call-sites like | |
| ``model.unnormalize_actions(...)`` keep working, but new code should prefer:: | |
| from starVLA.model.tools import FrameworkTools | |
| actions = FrameworkTools.unnormalize_actions(norm_actions, stats) | |
| """ | |
| def check_unnorm_key(norm_stats: dict, unnorm_key: str | None) -> str: | |
| """Infer or validate the dataset stats key used for un-normalization. | |
| Args: | |
| norm_stats: ``{dataset_key: stats_block}`` mapping. | |
| unnorm_key: Explicit key, or ``None`` to auto-resolve (only when single dataset). | |
| Returns: | |
| Resolved dataset key. | |
| Raises: | |
| AssertionError: If ambiguous (multiple datasets without key) or key not found. | |
| """ | |
| if unnorm_key is None: | |
| assert len(norm_stats) == 1, ( | |
| f"Your model was trained on more than one dataset, " | |
| f"please pass a `unnorm_key` from the following options to choose the statistics " | |
| f"used for un-normalizing actions: {norm_stats.keys()}" | |
| ) | |
| unnorm_key = next(iter(norm_stats.keys())) | |
| assert unnorm_key in norm_stats, ( | |
| f"The `unnorm_key` you chose is not in the set of available dataset statistics, " | |
| f"please choose from: {norm_stats.keys()}" | |
| ) | |
| return unnorm_key | |
| def get_action_stats(norm_stats: dict, unnorm_key: str | None = None) -> dict: | |
| """Retrieve raw action normalization statistics for a dataset. | |
| Args: | |
| norm_stats: Full norm-stats dict (loaded from ``dataset_statistics.json``). | |
| unnorm_key: Optional dataset key; auto-resolved when single dataset. | |
| Returns: | |
| Stats sub-dict (e.g. ``{"q01": ..., "q99": ..., "mask": ...}``). | |
| """ | |
| unnorm_key = FrameworkTools.check_unnorm_key(norm_stats, unnorm_key) | |
| return norm_stats[unnorm_key]["action"] | |
| def unnormalize_actions( | |
| normalized_actions: np.ndarray, | |
| action_norm_stats: dict, | |
| gripper_channel_idx: int = 6, | |
| ) -> np.ndarray: | |
| """Map normalized actions back to original value range. | |
| Steps: | |
| 1. Clamp to [-1, 1] | |
| 2. Threshold gripper channel to binary {0, 1} | |
| 3. Linear rescale masked dims: ``original = 0.5*(norm+1)*(q99-q01) + q01`` | |
| Args: | |
| normalized_actions: ``[T, action_dim]`` array. | |
| action_norm_stats: Dict with ``q01``, ``q99``, optional ``mask``. | |
| gripper_channel_idx: Which channel is the binary gripper (default 6 for 7-DoF). | |
| Returns: | |
| Unnormalized actions (same shape). | |
| """ | |
| mask = action_norm_stats.get("mask", np.ones_like(action_norm_stats["q01"], dtype=bool)) | |
| action_high, action_low = np.array(action_norm_stats["q99"]), np.array(action_norm_stats["q01"]) | |
| normalized_actions = np.clip(normalized_actions, -1, 1) | |
| if 0 <= gripper_channel_idx < normalized_actions.shape[-1]: | |
| normalized_actions[:, gripper_channel_idx] = np.where( | |
| normalized_actions[:, gripper_channel_idx] < 0.5, 0, 1 | |
| ) | |
| actions = np.where( | |
| mask, | |
| 0.5 * (normalized_actions + 1) * (action_high - action_low) + action_low, | |
| normalized_actions, | |
| ) | |
| return actions | |
| def get_trainable_module_keys(model, max_depth: int = 1) -> list: | |
| """Enumerate trainable sub-module names up to *max_depth*. | |
| Args: | |
| model: ``nn.Module`` instance. | |
| max_depth: How deep to traverse the module tree. | |
| Returns: | |
| List of module-path strings considered trainable. | |
| """ | |
| return auto_get_trainable_modules(model, max_depth=max_depth) | |
| class CrossAttention(nn.Module): | |
| def __init__(self, d_model: int, d_hidden: int, nhead: int = 8, dropout: float = 0.0, kv_dim: int = 2048): | |
| super().__init__() | |
| self.d_model = d_model | |
| self.d_hidden = d_hidden if d_hidden is not None else d_model | |
| self.nhead = nhead | |
| self.head_dim = self.d_hidden // nhead | |
| assert self.d_hidden % nhead == 0, "d_hidden must be divisible by nhead" | |
| # Projections | |
| self.q_proj = nn.Linear(d_model, self.d_hidden) | |
| self.k_proj = nn.Linear(kv_dim, self.d_hidden) | |
| self.v_proj = nn.Linear(kv_dim, self.d_hidden) | |
| self.out_proj = nn.Linear(self.d_hidden, d_model) | |
| self.dropout_attn = nn.Dropout(dropout) | |
| self.dropout_out = nn.Dropout(dropout) | |
| self.norm = nn.LayerNorm(d_model) | |
| def forward(self, image_feature: torch.Tensor, spatial_feature: torch.Tensor): | |
| """ | |
| Args: | |
| image_feature: (B, N_img, d_model) — Query | |
| vggt_feature: (B, N_vggt, kv_dim) — Key and Value | |
| Returns: | |
| fused_image_feature: (B, N_img, d_model) | |
| """ | |
| B, N_img, _ = image_feature.shape | |
| _, N_spatial, _ = spatial_feature.shape | |
| # Project to d_hidden | |
| q = self.q_proj(image_feature) # (B, N_img, d_hidden) | |
| k = self.k_proj(spatial_feature) # (B, N_vggt, d_hidden) | |
| v = self.v_proj(spatial_feature) # (B, N_vggt, d_hidden) | |
| # Reshape for multi-head: (B, N, d_hidden) -> (B, N, nhead, head_dim) -> (B, nhead, N, head_dim) | |
| q = q.view(B, N_img, self.nhead, self.head_dim).transpose(1, 2) # (B, nhead, N_img, head_dim) | |
| k = k.view(B, N_spatial, self.nhead, self.head_dim).transpose(1, 2) # (B, nhead, N_vggt, head_dim) | |
| v = v.view(B, N_spatial, self.nhead, self.head_dim).transpose(1, 2) # (B, nhead, N_vggt, head_dim) | |
| # Scaled Dot-Product Attention | |
| scale = self.head_dim**-0.5 | |
| attn_weights = torch.matmul(q, k.transpose(-2, -1)) * scale # (B, nhead, N_img, N_vggt) | |
| attn_weights = F.softmax(attn_weights, dim=-1) | |
| attn_weights = self.dropout_attn(attn_weights) | |
| # Weighted sum over values | |
| attn_output = torch.matmul(attn_weights, v) # (B, nhead, N_img, head_dim) | |
| # Concatenate heads and project back | |
| attn_output = attn_output.transpose(1, 2).contiguous() # (B, N_img, nhead, head_dim) | |
| attn_output = attn_output.view(B, N_img, self.d_hidden) # (B, N_img, d_hidden) | |
| # Final projection to d_model | |
| output = self.out_proj(attn_output) # (B, N_img, d_model) | |
| output = self.dropout_out(output) | |
| # Residual connection + LayerNorm | |
| output = self.norm(image_feature + output) | |
| return output | |
| def preprocess_images(image_list, target_size, mode="crop"): # [B,[PLT]] | |
| batch_images = [] | |
| shapes = set() | |
| to_tensor = TF.ToTensor() | |
| # target_size = 518 | |
| # First process all images and collect their shapes | |
| for imgs in image_list: | |
| epi_images = [] | |
| for img in imgs: | |
| width, height = img.size | |
| if mode == "pad": | |
| # Make the largest dimension 518px while maintaining aspect ratio | |
| if width >= height: | |
| new_width = target_size | |
| new_height = round(height * (new_width / width) / 14) * 14 # Make divisible by 14 | |
| else: | |
| new_height = target_size | |
| new_width = round(width * (new_height / height) / 14) * 14 # Make divisible by 14 | |
| else: # mode == "crop" | |
| # Original behavior: set width to 518px | |
| new_width = target_size | |
| # Calculate height maintaining aspect ratio, divisible by 14 | |
| new_height = round(height * (new_width / width) / 14) * 14 | |
| # Resize with new dimensions (width, height) | |
| # img = img.resize((new_width, new_height), Image.Resampling.BICUBIC) | |
| img = img.resize((new_width, new_height), Image.Resampling.BICUBIC) | |
| img = to_tensor(img) # Convert to tensor (0, 1) | |
| # Center crop height if it's larger than 518 (only in crop mode) | |
| if mode == "crop" and new_height > target_size: | |
| start_y = (new_height - target_size) // 2 | |
| img = img[:, start_y : start_y + target_size, :] | |
| # For pad mode, pad to make a square of target_size x target_size | |
| if mode == "pad": | |
| h_padding = target_size - img.shape[1] | |
| w_padding = target_size - img.shape[2] | |
| if h_padding > 0 or w_padding > 0: | |
| pad_top = h_padding // 2 | |
| pad_bottom = h_padding - pad_top | |
| pad_left = w_padding // 2 | |
| pad_right = w_padding - pad_left | |
| # Pad with white (value=1.0) | |
| img = torch.nn.functional.pad( | |
| img, (pad_left, pad_right, pad_top, pad_bottom), mode="constant", value=1.0 | |
| ) | |
| shapes.add((img.shape[1], img.shape[2])) | |
| epi_images.append(img) | |
| batch_images.append(torch.stack(epi_images)) | |
| # Check if we have different shapes | |
| # In theory our model can also work well with different shapes | |
| if len(shapes) > 1: | |
| print(f"Warning: Found images with different shapes: {shapes}") | |
| # Find maximum dimensions | |
| max_height = max(shape[0] for shape in shapes) | |
| max_width = max(shape[1] for shape in shapes) | |
| # Pad images if necessary | |
| padded_images = [] | |
| for img in batch_images: | |
| h_padding = max_height - img.shape[1] | |
| w_padding = max_width - img.shape[2] | |
| if h_padding > 0 or w_padding > 0: | |
| pad_top = h_padding // 2 | |
| pad_bottom = h_padding - pad_top | |
| pad_left = w_padding // 2 | |
| pad_right = w_padding - pad_left | |
| img = torch.nn.functional.pad( | |
| img, (pad_left, pad_right, pad_top, pad_bottom), mode="constant", value=1.0 | |
| ) | |
| padded_images.append(img) | |
| batch_images = padded_images | |
| batch_images = torch.stack(batch_images) # concatenate images | |
| # Ensure correct shape when single image | |
| if len(image_list) == 1: | |
| # Verify shape is (1, C, H, W) | |
| if batch_images.dim() == 3: | |
| batch_images = batch_images.unsqueeze(0) | |
| return batch_images | |