Download GR00T-WholeBodyControl/gear_sonic/utils/running_mean_std.py from Timsty/groot_deployment: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/Timsty/groot_deployment/resolve/main/GR00T-WholeBodyControl/gear_sonic/utils/running_mean_std.py
- Command line
-
hf download hf://Timsty/groot_deployment/GR00T-WholeBodyControl/gear_sonic/utils/running_mean_std.py
-
curl -L -o running_mean_std.py https://huggingface.co/Timsty/groot_deployment/resolve/main/GR00T-WholeBodyControl/gear_sonic/utils/running_mean_std.py
10.9 kB
| import torch | |
| import torch.nn as nn | |
| """ | |
| updates statistic from a full data | |
| Memory optimization note: If you encounter CUDA out of memory errors, consider setting: | |
| export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True | |
| This can help reduce memory fragmentation. | |
| """ | |
| class RunningMeanStd(nn.Module): | |
| def __init__(self, insize, epsilon=1e-05, per_channel=False, norm_only=False): | |
| super().__init__() | |
| print("RunningMeanStd: ", insize) | |
| self.insize = insize | |
| self.mean_size = insize[0] | |
| self.epsilon = epsilon | |
| self.norm_only = norm_only | |
| self.per_channel = per_channel | |
| if per_channel: | |
| if len(self.insize) == 3: | |
| self.axis = [0, 2, 3] | |
| if len(self.insize) == 2: | |
| self.axis = [0, 2] | |
| if len(self.insize) == 1: | |
| self.axis = [0] | |
| in_size = self.insize[0] | |
| else: | |
| self.axis = [0] | |
| in_size = insize | |
| self.register_buffer("running_mean", torch.zeros(in_size, dtype=torch.float32)) | |
| self.register_buffer("running_var", torch.ones(in_size, dtype=torch.float32)) | |
| self.register_buffer("count", torch.ones((), dtype=torch.float32)) | |
| self.frozen = False | |
| self.frozen_partial = False | |
| def freeze(self): | |
| self.frozen = True | |
| def unfreeze(self): | |
| self.frozen = False | |
| def freeze_partial(self, diff): | |
| self.frozen_partial = True | |
| self.diff = diff | |
| def sync_across_gpus(self, accelerator): | |
| if accelerator.num_processes <= 1: | |
| return | |
| # ZL: this is the right formulation but a crude sync. The correct way is to sync | |
| # batch stats. | |
| flat_stats = torch.cat( | |
| [ | |
| self.running_mean.flatten(), # (D,) | |
| self.running_var.flatten(), # (D,) | |
| ] | |
| ) # shape = (2D + C,) | |
| # Gather stats from all processes | |
| gathered = accelerator.gather(flat_stats[None]) # shape = (world_size, 2D + C) | |
| # Reshape | |
| world_size = gathered.shape[0] | |
| D = self.running_mean.numel() | |
| means_ = gathered[:, :D].reshape(world_size, *self.running_mean.shape).mean(dim=0) | |
| vars_ = gathered[:, D : 2 * D].reshape(world_size, *self.running_var.shape).mean(dim=0) | |
| # Update local stats | |
| self.running_mean.copy_(means_) | |
| self.running_var.copy_(vars_) | |
| def _update_mean_var_count_from_moments( | |
| self, mean, var, count, batch_mean, batch_var, batch_count | |
| ): | |
| delta = batch_mean - mean | |
| tot_count = count + batch_count | |
| new_mean = mean + delta * batch_count / tot_count | |
| m_a = var * count | |
| m_b = batch_var * batch_count | |
| M2 = m_a + m_b + delta**2 * count * batch_count / tot_count | |
| new_var = M2 / tot_count | |
| new_count = tot_count | |
| return new_mean, new_var, new_count | |
| def forward(self, input, unnorm=False): | |
| # change shape | |
| input_shape = input.shape | |
| if len(input.shape) == 3: | |
| input = input.reshape(-1, input_shape[-1]) | |
| if self.per_channel: | |
| if len(self.insize) == 3: | |
| # Use broadcasting instead of expand_as to avoid memory issues | |
| current_mean = self.running_mean.view([1, self.insize[0], 1, 1]) | |
| current_var = self.running_var.view([1, self.insize[0], 1, 1]) | |
| elif len(self.insize) == 2: | |
| current_mean = self.running_mean.view([1, self.insize[0], 1]) | |
| current_var = self.running_var.view([1, self.insize[0], 1]) | |
| elif len(self.insize) == 1: | |
| current_mean = self.running_mean.view([1, self.insize[0]]) | |
| current_var = self.running_var.view([1, self.insize[0]]) | |
| else: | |
| current_mean = self.running_mean | |
| current_var = self.running_var | |
| # get output | |
| if unnorm: | |
| y = torch.clamp(input, min=-5.0, max=5.0) | |
| y = torch.sqrt(current_var + self.epsilon) * y + current_mean | |
| else: | |
| if self.norm_only: | |
| y = input / torch.sqrt(current_var + self.epsilon) | |
| else: | |
| # Use in-place operations where possible to reduce memory usage | |
| y = input - current_mean | |
| y = y / torch.sqrt(current_var + self.epsilon) | |
| y = torch.clamp(y, min=-5.0, max=5.0) | |
| # update After normalization, so that the values used for training and testing are the same. | |
| if self.training and not self.frozen: | |
| mean = input.mean(self.axis) # along channel axis | |
| var = input.var(self.axis) | |
| new_mean, new_var, new_count = self._update_mean_var_count_from_moments( | |
| self.running_mean, self.running_var, self.count, mean, var, input.size()[0] | |
| ) | |
| if self.frozen_partial: | |
| # Only update the last bit (futures) | |
| self.running_mean[-self.diff :], self.running_var[-self.diff :], self.count = ( | |
| new_mean[-self.diff :], | |
| new_var[-self.diff :], | |
| new_count, | |
| ) | |
| else: | |
| self.running_mean, self.running_var, self.count = new_mean, new_var, new_count | |
| if len(input_shape) == 3: | |
| y = y.view(input_shape) | |
| return y | |
| class RunningMeanStdObs(nn.Module): | |
| def __init__(self, insize, epsilon=1e-05, per_channel=False, norm_only=False): | |
| assert isinstance(insize, dict) | |
| super().__init__() | |
| self.running_mean_std = nn.ModuleDict( | |
| {k: RunningMeanStd(v, epsilon, per_channel, norm_only) for k, v in insize.items()} | |
| ) | |
| def forward(self, input, unnorm=False): | |
| res = {k: self.running_mean_std[k](v, unnorm) for k, v in input.items()} | |
| return res | |
| from collections.abc import Sequence | |
| from copy import deepcopy | |
| class VecNorm(nn.Module): | |
| """Simple running normalization for observations. | |
| Keeps track of running mean and variance to normalize observations on-the-fly. | |
| Args: | |
| obs_keys: List of observation keys to normalize | |
| decay: Decay rate for running statistics (default: 0.99) | |
| eps: Small constant for numerical stability (default: 1e-4) | |
| device: Device to store statistics on | |
| """ | |
| def __init__( | |
| self, | |
| obs_keys: Sequence[str] | None, | |
| decay: float = 0.9999, | |
| eps: float = 1e-4, | |
| ): | |
| super().__init__() | |
| self.obs_keys = obs_keys | |
| self.decay = decay | |
| self.eps = eps | |
| # Running statistics as nn.Parameters | |
| self.sum: dict[str, nn.Parameter] = nn.ParameterDict({}) | |
| self.ssq: dict[str, nn.Parameter] = nn.ParameterDict({}) | |
| self.cnt: dict[str, nn.Parameter] = nn.ParameterDict({}) | |
| self.mean: dict[str, nn.Parameter] = nn.ParameterDict({}) | |
| self.var: dict[str, nn.Parameter] = nn.ParameterDict({}) | |
| self.initialized = False | |
| self.frozen = False | |
| def init_stats(self, obs_dict: dict[str, torch.Tensor]): | |
| """Initialize running statistics based on observation shapes.""" | |
| if self.obs_keys is None: | |
| self.obs_keys = list(obs_dict.keys()) | |
| for key in self.obs_keys: | |
| if key in obs_dict: | |
| v = obs_dict[key] | |
| self.sum[key] = nn.Parameter(torch.zeros_like(v[0]), requires_grad=False) | |
| self.ssq[key] = nn.Parameter(torch.zeros_like(v[0]), requires_grad=False) | |
| self.cnt[key] = nn.Parameter(torch.zeros_like(v[0]), requires_grad=False) | |
| self.mean[key] = nn.Parameter(torch.zeros_like(v[0]), requires_grad=False) | |
| self.var[key] = nn.Parameter(torch.ones_like(v[0]), requires_grad=False) | |
| self.initialized = True | |
| def update(self, obs_dict: dict[str, torch.Tensor]): | |
| """Update running statistics with new observations.""" | |
| if not self.initialized: | |
| self.init_stats(obs_dict) | |
| if self.frozen: | |
| return | |
| for key in self.obs_keys: | |
| if key not in obs_dict: | |
| continue | |
| x = obs_dict[key] | |
| sum_x = x.sum(dim=0) | |
| ssq_x = (x**2).sum(dim=0) | |
| cnt_x = x.shape[0] | |
| self.sum[key] = self.sum[key] * self.decay + sum_x | |
| self.ssq[key] = self.ssq[key] * self.decay + ssq_x | |
| self.cnt[key] = self.cnt[key] * self.decay + cnt_x | |
| self.mean[key] = self.sum[key] / self.cnt[key] | |
| self.var[key] = (self.ssq[key] / self.cnt[key] - self.mean[key] ** 2).clamp( | |
| min=self.eps | |
| ) | |
| def normalize(self, obs_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| """Normalize observations using running statistics.""" | |
| if not self.initialized: | |
| self.init_stats(obs_dict) | |
| return obs_dict.copy() | |
| normalized = obs_dict.copy() | |
| for key in self.obs_keys: | |
| normalized[key] = (normalized[key] - self.mean[key]) / self.var[key].sqrt() | |
| return normalized | |
| def denormalize(self, obs_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: | |
| """Denormalize observations using running statistics.""" | |
| if not self.initialized: | |
| self.init_stats(obs_dict) | |
| return obs_dict.copy() | |
| denormalized = obs_dict.copy() | |
| for key in self.obs_keys: | |
| denormalized[key] = denormalized[key] * self.var[key].sqrt() + self.mean[key] | |
| return denormalized | |
| def freeze(self): | |
| """Freeze running statistics updates.""" | |
| self.frozen = True | |
| def unfreeze(self): | |
| """Unfreeze running statistics updates.""" | |
| self.frozen = False | |
| def get_stats(self): | |
| """Get current running statistics.""" | |
| return { | |
| "sum": {key: param.data.clone() for key, param in self.sum.items()}, | |
| "ssq": {key: param.data.clone() for key, param in self.ssq.items()}, | |
| "cnt": {key: param.data.clone() for key, param in self.cnt.items()}, | |
| "mean": {key: param.data.clone() for key, param in self.mean.items()}, | |
| "var": {key: param.data.clone() for key, param in self.var.items()}, | |
| } | |
| def load_stats(self, stats): | |
| """Load running statistics.""" | |
| for key in stats["sum"]: | |
| self.sum[key] = nn.Parameter(deepcopy(stats["sum"][key]), requires_grad=False) | |
| self.ssq[key] = nn.Parameter(deepcopy(stats["ssq"][key]), requires_grad=False) | |
| self.cnt[key] = nn.Parameter(deepcopy(stats["cnt"][key]), requires_grad=False) | |
| self.mean[key] = nn.Parameter(deepcopy(stats["mean"][key]), requires_grad=False) | |
| self.var[key] = nn.Parameter(deepcopy(stats["var"][key]), requires_grad=False) | |
| self.initialized = True | |