Download src/physics_pretraining.py from tfayiz/arc-ai-embodied-intelligence: direct link, hf CLI and curl.
- Browser
- Download file 26.8 kB
-
https://huggingface.co/tfayiz/arc-ai-embodied-intelligence/resolve/main/src/physics_pretraining.py
- Command line
-
hf download hf://tfayiz/arc-ai-embodied-intelligence/src/physics_pretraining.py
-
curl -L -o physics_pretraining.py https://huggingface.co/tfayiz/arc-ai-embodied-intelligence/resolve/main/src/physics_pretraining.py
26.8 kB
| """ | |
| Physics-Grounded Pretraining for Diffusion Policy using THE WELL. | |
| Streams diverse physics simulation data from HuggingFace to pretrain | |
| the temporal prediction backbone before fine-tuning on robot demonstrations. | |
| Architecture: | |
| THE WELL physics data → Spatiotemporal Encoder → Temporal Prediction Head | |
| ↓ (transfer) | |
| Robot demonstrations → Same Encoder → Diffusion Policy → Actions | |
| """ | |
| import math | |
| import logging | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Optional | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from torch.utils.data import DataLoader, IterableDataset | |
| logger = logging.getLogger(__name__) | |
| # --------------------------------------------------------------------------- | |
| # Configuration | |
| # --------------------------------------------------------------------------- | |
| class PhysicsPretrainConfig: | |
| # THE WELL datasets (streamed from HuggingFace) | |
| datasets: list[str] = field(default_factory=lambda: [ | |
| "polymathic-ai/gray_scott", | |
| "polymathic-ai/acoustic_scattering", | |
| "polymathic-ai/rayleigh_benard", | |
| ]) | |
| streaming: bool = True | |
| # Architecture | |
| hidden_dim: int = 256 | |
| n_layers: int = 4 | |
| n_heads: int = 8 | |
| context_frames: int = 4 | |
| prediction_horizon: int = 16 | |
| spatial_patch_size: int = 8 | |
| # Training | |
| batch_size: int = 32 | |
| learning_rate: float = 3e-4 | |
| weight_decay: float = 1e-5 | |
| num_steps: int = 200_000 | |
| warmup_steps: int = 5_000 | |
| gradient_clip: float = 1.0 | |
| mixed_precision: bool = True | |
| seed: int = 42 | |
| # Checkpointing | |
| checkpoint_dir: str = "./checkpoints/physics_pretrain" | |
| checkpoint_every: int = 10_000 | |
| eval_every: int = 5_000 | |
| # HuggingFace | |
| hf_repo: str = "arc-ai/diffusion-policy-physics" | |
| push_to_hub: bool = True | |
| push_every: int = 50_000 | |
| class FinetuneConfig: | |
| pretrained_path: str = "" | |
| dataset_repo: str = "arc-ai/sim-demonstrations" | |
| obs_dim: int = 20 | |
| action_dim: int = 7 | |
| action_horizon: int = 16 | |
| batch_size: int = 256 | |
| learning_rate: float = 1e-4 | |
| num_steps: int = 500_000 | |
| warmup_steps: int = 2_000 | |
| freeze_encoder_steps: int = 50_000 | |
| gradient_clip: float = 1.0 | |
| mixed_precision: bool = True | |
| # Domain randomization during fine-tuning | |
| domain_randomization: bool = True | |
| noise_sensor_std: float = 0.05 | |
| noise_actuator_std: float = 0.02 | |
| mass_range: tuple[float, float] = (0.7, 1.5) | |
| friction_range: tuple[float, float] = (0.5, 2.0) | |
| gravity_range: tuple[float, float] = (9.0, 10.5) | |
| # --------------------------------------------------------------------------- | |
| # THE WELL Streaming Dataset | |
| # --------------------------------------------------------------------------- | |
| class WellStreamingDataset(IterableDataset): | |
| """Streams physics trajectories from THE WELL via HuggingFace.""" | |
| def __init__(self, config: PhysicsPretrainConfig): | |
| self.config = config | |
| self.datasets = config.datasets | |
| self.context_frames = config.context_frames | |
| self.prediction_horizon = config.prediction_horizon | |
| self.patch_size = config.spatial_patch_size | |
| def _load_stream(self): | |
| from datasets import load_dataset, interleave_datasets | |
| streams = [] | |
| for ds_name in self.datasets: | |
| try: | |
| ds = load_dataset(ds_name, split="train", streaming=True) | |
| streams.append(ds) | |
| logger.info(f"Streaming from {ds_name}") | |
| except Exception as e: | |
| logger.warning(f"Failed to load {ds_name}: {e}") | |
| if not streams: | |
| raise RuntimeError("No datasets loaded") | |
| return interleave_datasets(streams) | |
| def _extract_trajectory(self, sample): | |
| """Extract spatiotemporal trajectory from WELL sample. | |
| THE WELL format: B × T × H [× W [× D]] × C | |
| We extract temporal windows for prediction. | |
| """ | |
| if "trajectory" in sample: | |
| data = np.array(sample["trajectory"], dtype=np.float32) | |
| elif "fields" in sample: | |
| data = np.array(sample["fields"], dtype=np.float32) | |
| else: | |
| keys = [k for k in sample.keys() if k not in ("metadata", "id", "label")] | |
| if keys: | |
| data = np.array(sample[keys[0]], dtype=np.float32) | |
| else: | |
| return None | |
| if data.ndim < 2: | |
| return None | |
| T = data.shape[0] | |
| total_needed = self.context_frames + self.prediction_horizon | |
| if T < total_needed: | |
| return None | |
| return data | |
| def _patchify(self, spatial_data: np.ndarray) -> np.ndarray: | |
| """Convert spatial field to patch embeddings.""" | |
| if spatial_data.ndim == 1: | |
| return spatial_data | |
| if spatial_data.ndim == 2: | |
| H, C = spatial_data.shape | |
| n_patches = H // self.patch_size | |
| if n_patches == 0: | |
| return spatial_data.reshape(-1) | |
| patches = spatial_data[:n_patches * self.patch_size].reshape(n_patches, self.patch_size, C) | |
| return patches.mean(axis=1).reshape(-1) | |
| if spatial_data.ndim == 3: | |
| H, W, C = spatial_data.shape | |
| pH = H // self.patch_size | |
| pW = W // self.patch_size | |
| if pH == 0 or pW == 0: | |
| return spatial_data.reshape(-1)[:256] | |
| patches = spatial_data[:pH*self.patch_size, :pW*self.patch_size].reshape( | |
| pH, self.patch_size, pW, self.patch_size, C | |
| ) | |
| return patches.mean(axis=(1, 3)).reshape(-1)[:256] | |
| return spatial_data.reshape(-1)[:256] | |
| def __iter__(self): | |
| stream = self._load_stream() | |
| for sample in stream: | |
| trajectory = self._extract_trajectory(sample) | |
| if trajectory is None: | |
| continue | |
| T = trajectory.shape[0] | |
| total_needed = self.context_frames + self.prediction_horizon | |
| start = np.random.randint(0, T - total_needed + 1) | |
| window = trajectory[start:start + total_needed] | |
| context_patches = [] | |
| for t in range(self.context_frames): | |
| patch = self._patchify(window[t]) | |
| context_patches.append(patch) | |
| target_patches = [] | |
| for t in range(self.context_frames, total_needed): | |
| patch = self._patchify(window[t]) | |
| target_patches.append(patch) | |
| context = np.stack(context_patches, axis=0) | |
| target = np.stack(target_patches, axis=0) | |
| yield { | |
| "context": torch.from_numpy(context), | |
| "target": torch.from_numpy(target), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Physics-Aware Temporal Encoder | |
| # --------------------------------------------------------------------------- | |
| class SinusoidalEmbedding(nn.Module): | |
| def __init__(self, dim: int): | |
| super().__init__() | |
| self.dim = dim | |
| def forward(self, t: torch.Tensor) -> torch.Tensor: | |
| half = self.dim // 2 | |
| emb = math.log(10000) / (half - 1) | |
| emb = torch.exp(torch.arange(half, device=t.device) * -emb) | |
| emb = t.unsqueeze(-1) * emb.unsqueeze(0) | |
| return torch.cat([emb.sin(), emb.cos()], dim=-1) | |
| class PhysicsTemporalEncoder(nn.Module): | |
| """Encoder that learns spatiotemporal dynamics from physics simulations. | |
| Pretrained on THE WELL, then transferred to robot policy. | |
| """ | |
| def __init__(self, config: PhysicsPretrainConfig): | |
| super().__init__() | |
| self.config = config | |
| dim = config.hidden_dim | |
| self.input_proj = nn.Sequential( | |
| nn.LazyLinear(dim), | |
| nn.SiLU(), | |
| nn.Linear(dim, dim), | |
| ) | |
| self.time_embed = SinusoidalEmbedding(dim) | |
| self.time_proj = nn.Sequential( | |
| nn.Linear(dim, dim), | |
| nn.SiLU(), | |
| nn.Linear(dim, dim), | |
| ) | |
| encoder_layer = nn.TransformerEncoderLayer( | |
| d_model=dim, | |
| nhead=config.n_heads, | |
| dim_feedforward=dim * 4, | |
| dropout=0.1, | |
| activation="gelu", | |
| batch_first=True, | |
| norm_first=True, | |
| ) | |
| self.transformer = nn.TransformerEncoder( | |
| encoder_layer, num_layers=config.n_layers | |
| ) | |
| self.output_norm = nn.LayerNorm(dim) | |
| def forward(self, context: torch.Tensor, timesteps: Optional[torch.Tensor] = None): | |
| """ | |
| Args: | |
| context: (B, T, D) - temporal context frames (patchified) | |
| timesteps: (B,) - optional diffusion timesteps | |
| Returns: | |
| (B, T, hidden_dim) - encoded temporal representation | |
| """ | |
| B, T, _ = context.shape | |
| x = self.input_proj(context) | |
| if timesteps is not None: | |
| t_emb = self.time_proj(self.time_embed(timesteps.float())) | |
| x = x + t_emb.unsqueeze(1) | |
| pos = torch.arange(T, device=x.device).float() | |
| pos_emb = self.time_embed(pos).unsqueeze(0).expand(B, -1, -1) | |
| x = x + pos_emb[:, :T, :x.shape[-1]] | |
| x = self.transformer(x) | |
| x = self.output_norm(x) | |
| return x | |
| # --------------------------------------------------------------------------- | |
| # Prediction Head (for pretraining) | |
| # --------------------------------------------------------------------------- | |
| class PhysicsPredictionHead(nn.Module): | |
| """Predicts future physics states from encoded context.""" | |
| def __init__(self, config: PhysicsPretrainConfig): | |
| super().__init__() | |
| dim = config.hidden_dim | |
| self.horizon = config.prediction_horizon | |
| self.predictor = nn.Sequential( | |
| nn.Linear(dim, dim * 2), | |
| nn.SiLU(), | |
| nn.Linear(dim * 2, dim * 2), | |
| nn.SiLU(), | |
| ) | |
| self.output_proj = nn.LazyLinear(1) # will adapt to target dim | |
| def forward(self, encoded: torch.Tensor, target_dim: int): | |
| """ | |
| Args: | |
| encoded: (B, T_context, dim) | |
| target_dim: output dimension per timestep | |
| Returns: | |
| (B, horizon, target_dim) | |
| """ | |
| pooled = encoded.mean(dim=1) | |
| x = self.predictor(pooled) | |
| if self.output_proj.in_features == 1: | |
| self.output_proj = nn.Linear( | |
| x.shape[-1], self.horizon * target_dim | |
| ).to(x.device) | |
| out = self.output_proj(x) | |
| return out.reshape(-1, self.horizon, target_dim) | |
| # --------------------------------------------------------------------------- | |
| # Diffusion Policy with Physics-Pretrained Encoder | |
| # --------------------------------------------------------------------------- | |
| class PhysicsDiffusionPolicy(nn.Module): | |
| """Diffusion Policy using physics-pretrained temporal encoder.""" | |
| def __init__(self, obs_dim: int, action_dim: int, config: PhysicsPretrainConfig): | |
| super().__init__() | |
| self.obs_dim = obs_dim | |
| self.action_dim = action_dim | |
| self.action_horizon = config.prediction_horizon | |
| dim = config.hidden_dim | |
| self.encoder = PhysicsTemporalEncoder(config) | |
| self.obs_proj = nn.Sequential( | |
| nn.Linear(obs_dim, dim), | |
| nn.SiLU(), | |
| nn.Linear(dim, dim), | |
| ) | |
| self.noise_pred = nn.Sequential( | |
| nn.Linear(dim * 2, dim * 2), | |
| nn.SiLU(), | |
| nn.Linear(dim * 2, dim * 2), | |
| nn.SiLU(), | |
| nn.Linear(dim * 2, action_dim * self.action_horizon), | |
| ) | |
| n_steps = 100 | |
| betas = torch.linspace(1e-4, 0.02, n_steps) | |
| alphas = 1.0 - betas | |
| alphas_cumprod = torch.cumprod(alphas, dim=0) | |
| self.register_buffer("betas", betas) | |
| self.register_buffer("alphas_cumprod", alphas_cumprod) | |
| self.register_buffer("sqrt_alphas_cumprod", torch.sqrt(alphas_cumprod)) | |
| self.register_buffer("sqrt_one_minus_alphas_cumprod", torch.sqrt(1 - alphas_cumprod)) | |
| self.n_steps = n_steps | |
| def forward(self, obs: torch.Tensor, actions: torch.Tensor, timesteps: torch.Tensor): | |
| """Training forward: predict noise added to actions. | |
| Args: | |
| obs: (B, T_obs, obs_dim) - observation history | |
| actions: (B, horizon, action_dim) - ground truth action chunk | |
| timesteps: (B,) - diffusion timesteps | |
| """ | |
| encoded = self.encoder(self.obs_proj(obs), timesteps) | |
| context = encoded.mean(dim=1) | |
| noise = torch.randn_like(actions) | |
| B = actions.shape[0] | |
| t = timesteps.long() | |
| sqrt_alpha = self.sqrt_alphas_cumprod[t].view(B, 1, 1) | |
| sqrt_one_minus = self.sqrt_one_minus_alphas_cumprod[t].view(B, 1, 1) | |
| noisy_actions = sqrt_alpha * actions + sqrt_one_minus * noise | |
| noisy_flat = noisy_actions.reshape(B, -1) | |
| combined = torch.cat([context, noisy_flat[:, :context.shape[-1]]], dim=-1) | |
| noise_pred = self.noise_pred(combined) | |
| noise_pred = noise_pred.reshape(B, self.action_horizon, self.action_dim) | |
| return F.mse_loss(noise_pred, noise) | |
| def predict(self, obs: torch.Tensor, n_inference_steps: int = 10) -> torch.Tensor: | |
| """DDIM inference: generate action chunk from observation. | |
| Args: | |
| obs: (B, T_obs, obs_dim) | |
| n_inference_steps: number of denoising steps | |
| Returns: | |
| (B, horizon, action_dim) | |
| """ | |
| B = obs.shape[0] | |
| device = obs.device | |
| actions = torch.randn(B, self.action_horizon, self.action_dim, device=device) | |
| step_indices = torch.linspace(self.n_steps - 1, 0, n_inference_steps).long().to(device) | |
| for step_t in step_indices: | |
| t_batch = step_t.expand(B) | |
| encoded = self.encoder(self.obs_proj(obs), t_batch.float()) | |
| context = encoded.mean(dim=1) | |
| noisy_flat = actions.reshape(B, -1) | |
| combined = torch.cat([context, noisy_flat[:, :context.shape[-1]]], dim=-1) | |
| noise_pred = self.noise_pred(combined) | |
| noise_pred = noise_pred.reshape(B, self.action_horizon, self.action_dim) | |
| alpha = self.alphas_cumprod[step_t] | |
| alpha_prev = self.alphas_cumprod[max(step_t - self.n_steps // n_inference_steps, 0)] | |
| pred_x0 = (actions - torch.sqrt(1 - alpha) * noise_pred) / torch.sqrt(alpha) | |
| pred_x0 = pred_x0.clamp(-1, 1) | |
| actions = torch.sqrt(alpha_prev) * pred_x0 + torch.sqrt(1 - alpha_prev) * noise_pred | |
| return actions | |
| # --------------------------------------------------------------------------- | |
| # Pretraining Loop | |
| # --------------------------------------------------------------------------- | |
| class PhysicsPretrainer: | |
| """Pretrains temporal encoder on THE WELL physics data.""" | |
| def __init__(self, config: PhysicsPretrainConfig): | |
| self.config = config | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.encoder = PhysicsTemporalEncoder(config).to(self.device) | |
| self.pred_head = PhysicsPredictionHead(config).to(self.device) | |
| params = list(self.encoder.parameters()) + list(self.pred_head.parameters()) | |
| self.optimizer = torch.optim.AdamW( | |
| params, lr=config.learning_rate, weight_decay=config.weight_decay | |
| ) | |
| self.scaler = torch.amp.GradScaler("cuda", enabled=config.mixed_precision) | |
| self.step = 0 | |
| def _get_lr(self) -> float: | |
| if self.step < self.config.warmup_steps: | |
| return self.config.learning_rate * self.step / self.config.warmup_steps | |
| progress = (self.step - self.config.warmup_steps) / ( | |
| self.config.num_steps - self.config.warmup_steps | |
| ) | |
| return self.config.learning_rate * 0.5 * (1 + math.cos(math.pi * progress)) | |
| def _update_lr(self): | |
| lr = self._get_lr() | |
| for pg in self.optimizer.param_groups: | |
| pg["lr"] = lr | |
| def train(self): | |
| """Run pretraining loop streaming from THE WELL.""" | |
| logger.info(f"Starting physics pretraining on {self.device}") | |
| logger.info(f"Datasets: {self.config.datasets}") | |
| logger.info(f"Steps: {self.config.num_steps}") | |
| dataset = WellStreamingDataset(self.config) | |
| dataloader = DataLoader( | |
| dataset, batch_size=self.config.batch_size, num_workers=2, pin_memory=True | |
| ) | |
| checkpoint_dir = Path(self.config.checkpoint_dir) | |
| checkpoint_dir.mkdir(parents=True, exist_ok=True) | |
| running_loss = 0.0 | |
| log_interval = 100 | |
| for batch in dataloader: | |
| if self.step >= self.config.num_steps: | |
| break | |
| context = batch["context"].to(self.device) | |
| target = batch["target"].to(self.device) | |
| self._update_lr() | |
| with torch.amp.autocast("cuda", enabled=self.config.mixed_precision): | |
| encoded = self.encoder(context) | |
| target_flat = target.reshape(target.shape[0], target.shape[1], -1) | |
| target_dim = target_flat.shape[-1] | |
| predicted = self.pred_head(encoded, target_dim) | |
| min_horizon = min(predicted.shape[1], target_flat.shape[1]) | |
| min_dim = min(predicted.shape[2], target_flat.shape[2]) | |
| loss = F.mse_loss( | |
| predicted[:, :min_horizon, :min_dim], | |
| target_flat[:, :min_horizon, :min_dim], | |
| ) | |
| self.optimizer.zero_grad() | |
| self.scaler.scale(loss).backward() | |
| self.scaler.unscale_(self.optimizer) | |
| nn.utils.clip_grad_norm_( | |
| list(self.encoder.parameters()) + list(self.pred_head.parameters()), | |
| self.config.gradient_clip, | |
| ) | |
| self.scaler.step(self.optimizer) | |
| self.scaler.update() | |
| running_loss += loss.item() | |
| self.step += 1 | |
| if self.step % log_interval == 0: | |
| avg_loss = running_loss / log_interval | |
| lr = self._get_lr() | |
| logger.info( | |
| f"Step {self.step}/{self.config.num_steps} | " | |
| f"Loss: {avg_loss:.6f} | LR: {lr:.2e}" | |
| ) | |
| running_loss = 0.0 | |
| if self.step % self.config.checkpoint_every == 0: | |
| self._save_checkpoint(checkpoint_dir / f"step_{self.step}.pt") | |
| if self.step % self.config.push_every == 0 and self.config.push_to_hub: | |
| self._push_to_hub() | |
| self._save_checkpoint(checkpoint_dir / "final.pt") | |
| logger.info(f"Pretraining complete at step {self.step}") | |
| return self.encoder | |
| def _save_checkpoint(self, path: Path): | |
| torch.save({ | |
| "step": self.step, | |
| "encoder_state": self.encoder.state_dict(), | |
| "pred_head_state": self.pred_head.state_dict(), | |
| "optimizer_state": self.optimizer.state_dict(), | |
| "config": self.config, | |
| }, path) | |
| logger.info(f"Saved checkpoint: {path}") | |
| def _push_to_hub(self): | |
| try: | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| path = Path(self.config.checkpoint_dir) / f"step_{self.step}.pt" | |
| api.upload_file( | |
| path_or_fileobj=str(path), | |
| path_in_repo=f"checkpoints/step_{self.step}.pt", | |
| repo_id=self.config.hf_repo, | |
| repo_type="model", | |
| ) | |
| logger.info(f"Pushed step {self.step} to {self.config.hf_repo}") | |
| except Exception as e: | |
| logger.warning(f"Hub push failed: {e}") | |
| # --------------------------------------------------------------------------- | |
| # Fine-tuning on Robot Demonstrations | |
| # --------------------------------------------------------------------------- | |
| class DomainRandomizer: | |
| """Applies domain randomization during fine-tuning.""" | |
| def __init__(self, config: FinetuneConfig): | |
| self.config = config | |
| def __call__(self, obs: torch.Tensor, actions: torch.Tensor): | |
| if not self.config.domain_randomization: | |
| return obs, actions | |
| B = obs.shape[0] | |
| # Sensor noise | |
| obs = obs + torch.randn_like(obs) * self.config.noise_sensor_std | |
| # Actuator noise | |
| actions = actions + torch.randn_like(actions) * self.config.noise_actuator_std | |
| # Mass scaling (affects dynamics — simulated via obs perturbation) | |
| mass_scale = torch.empty(B, 1, 1, device=obs.device).uniform_( | |
| *self.config.mass_range | |
| ) | |
| if obs.shape[-1] >= 14: | |
| obs[:, :, 7:14] = obs[:, :, 7:14] / mass_scale | |
| return obs, actions | |
| class PolicyFinetuner: | |
| """Fine-tunes physics-pretrained encoder for robot manipulation.""" | |
| def __init__(self, pretrained_encoder: PhysicsTemporalEncoder, config: FinetuneConfig): | |
| self.config = config | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| pretrain_config = PhysicsPretrainConfig( | |
| hidden_dim=pretrained_encoder.config.hidden_dim, | |
| n_layers=pretrained_encoder.config.n_layers, | |
| n_heads=pretrained_encoder.config.n_heads, | |
| prediction_horizon=config.action_horizon, | |
| ) | |
| self.policy = PhysicsDiffusionPolicy( | |
| obs_dim=config.obs_dim, | |
| action_dim=config.action_dim, | |
| config=pretrain_config, | |
| ).to(self.device) | |
| # Transfer pretrained weights | |
| self.policy.encoder.load_state_dict(pretrained_encoder.state_dict(), strict=False) | |
| logger.info("Loaded pretrained encoder weights") | |
| self.randomizer = DomainRandomizer(config) | |
| self.optimizer = torch.optim.AdamW( | |
| self.policy.parameters(), lr=config.learning_rate | |
| ) | |
| self.scaler = torch.amp.GradScaler("cuda", enabled=config.mixed_precision) | |
| self.step = 0 | |
| def _freeze_encoder(self, freeze: bool): | |
| for p in self.policy.encoder.parameters(): | |
| p.requires_grad = not freeze | |
| def train(self, dataloader: DataLoader): | |
| """Fine-tune on robot demonstrations with domain randomization.""" | |
| logger.info("Starting fine-tuning with domain randomization") | |
| self._freeze_encoder(True) | |
| running_loss = 0.0 | |
| for batch in dataloader: | |
| if self.step >= self.config.num_steps: | |
| break | |
| if self.step == self.config.freeze_encoder_steps: | |
| self._freeze_encoder(False) | |
| logger.info(f"Unfreezing encoder at step {self.step}") | |
| obs = batch["obs"].to(self.device) | |
| actions = batch["actions"].to(self.device) | |
| obs, actions = self.randomizer(obs, actions) | |
| t = torch.randint(0, self.policy.n_steps, (obs.shape[0],), device=self.device) | |
| with torch.amp.autocast("cuda", enabled=self.config.mixed_precision): | |
| loss = self.policy(obs, actions, t.float()) | |
| self.optimizer.zero_grad() | |
| self.scaler.scale(loss).backward() | |
| self.scaler.unscale_(self.optimizer) | |
| nn.utils.clip_grad_norm_(self.policy.parameters(), self.config.gradient_clip) | |
| self.scaler.step(self.optimizer) | |
| self.scaler.update() | |
| running_loss += loss.item() | |
| self.step += 1 | |
| if self.step % 100 == 0: | |
| logger.info(f"Step {self.step} | Loss: {running_loss/100:.6f}") | |
| running_loss = 0.0 | |
| return self.policy | |
| # --------------------------------------------------------------------------- | |
| # HuggingFace Hub Utilities | |
| # --------------------------------------------------------------------------- | |
| def setup_hf_repo(repo_id: str, repo_type: str = "model"): | |
| """Create HuggingFace repo if it doesn't exist.""" | |
| from huggingface_hub import HfApi, create_repo | |
| api = HfApi() | |
| try: | |
| create_repo(repo_id, repo_type=repo_type, exist_ok=True) | |
| logger.info(f"HF repo ready: {repo_id}") | |
| except Exception as e: | |
| logger.warning(f"Repo setup: {e}") | |
| return api | |
| def upload_model(model: nn.Module, repo_id: str, filename: str = "model.pt"): | |
| """Upload model checkpoint to HuggingFace.""" | |
| import tempfile | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| with tempfile.NamedTemporaryFile(suffix=".pt") as f: | |
| torch.save(model.state_dict(), f.name) | |
| api.upload_file( | |
| path_or_fileobj=f.name, | |
| path_in_repo=filename, | |
| repo_id=repo_id, | |
| repo_type="model", | |
| ) | |
| logger.info(f"Uploaded {filename} to {repo_id}") | |
| def upload_dataset(data_dir: str, repo_id: str): | |
| """Upload demonstration dataset to HuggingFace.""" | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| api.upload_folder( | |
| folder_path=data_dir, | |
| repo_id=repo_id, | |
| repo_type="dataset", | |
| ) | |
| logger.info(f"Uploaded dataset to {repo_id}") | |
| # --------------------------------------------------------------------------- | |
| # Main Entry Point | |
| # --------------------------------------------------------------------------- | |
| def run_physics_pretraining(config_path: Optional[str] = None): | |
| """Full pipeline: pretrain on physics → fine-tune on robot demos.""" | |
| import yaml | |
| if config_path: | |
| with open(config_path) as f: | |
| cfg = yaml.safe_load(f) | |
| pretrain_cfg = PhysicsPretrainConfig(**{ | |
| k: v for k, v in cfg.get("pretraining", {}).items() | |
| if k in PhysicsPretrainConfig.__dataclass_fields__ | |
| }) | |
| else: | |
| pretrain_cfg = PhysicsPretrainConfig() | |
| # Phase 1: Pretrain on THE WELL | |
| logger.info("=" * 60) | |
| logger.info("PHASE 1: Physics Pretraining (THE WELL)") | |
| logger.info("=" * 60) | |
| pretrainer = PhysicsPretrainer(pretrain_cfg) | |
| encoder = pretrainer.train() | |
| # Phase 2: Fine-tune on robot demonstrations | |
| logger.info("=" * 60) | |
| logger.info("PHASE 2: Fine-tuning on Robot Demonstrations") | |
| logger.info("=" * 60) | |
| finetune_cfg = FinetuneConfig( | |
| pretrained_path=str(Path(pretrain_cfg.checkpoint_dir) / "final.pt"), | |
| ) | |
| finetuner = PolicyFinetuner(encoder, finetune_cfg) | |
| # Load robot demonstration data (from HF or local) | |
| # In production, this streams from arc-ai/sim-demonstrations | |
| logger.info("Fine-tuning requires demonstration dataloader — provide via API") | |
| return finetuner.policy | |
| if __name__ == "__main__": | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| run_physics_pretraining() | |