Download code/model.py from amlink/so101-diffusion-policy-resnet18-baseline: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/amlink/so101-diffusion-policy-resnet18-baseline/resolve/main/code/model.py
- Command line
-
hf download hf://amlink/so101-diffusion-policy-resnet18-baseline/code/model.py
-
curl -L -o model.py https://huggingface.co/amlink/so101-diffusion-policy-resnet18-baseline/resolve/main/code/model.py
18.6 kB
| """Pinned official Diffusion Policy components, adapted to the SO101 data contract. | |
| The upstream encoder, ResNet-18 getter, crop randomizer, Mish U-Net and EMA are | |
| vendored without edits. This wrapper deliberately keeps Issue 18's *current-t* | |
| action convention, train-only min/max statistics and full-window dataset. It is | |
| not an unmodified official task/runner or a pretrained real-robot policy. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import hashlib | |
| import importlib.metadata | |
| import inspect | |
| import json | |
| import os | |
| from pathlib import Path | |
| import sys | |
| import types | |
| import numpy as np | |
| import torch | |
| from torch import Tensor, nn | |
| HERE = Path(__file__).resolve().parent | |
| REPO_ROOT = HERE.parents[1] | |
| VENDOR = HERE / "vendor" | |
| UPSTREAM_COMMIT = "5ba07ac6661db573af695b419a7947ecb704690f" | |
| FORMAT = "so101_official_dp.visual_policy.v1" | |
| NORMALIZER_KEYS = ("action_center", "action_span", "state_center", "state_span") | |
| STATS_KEYS = ("action_min", "action_max", "state_min", "state_max") | |
| DEFAULTS = { | |
| "history": 2, "horizon": 16, "image_size": 96, | |
| "crop_shape": None, "resize_shape": None, "random_crop": False, "imagenet_norm": True, | |
| "cameras": ["front", "gripper"], "context_dim": 2060, | |
| "execution_start": 0, "execute_steps": 8, "control_hz": 10, | |
| "down_dims": [512, 1024, 2048], "diffusion_step_embed_dim": 128, | |
| "kernel_size": 5, "n_groups": 8, "cond_predict_scale": True, | |
| "num_train_timesteps": 100, "beta_schedule": "squaredcos_cap_v2", | |
| "prediction_type": "epsilon", "clip_sample": True, | |
| "variance_type": "fixed_small", "default_sampler": "ddpm", | |
| "default_inference_steps": 100, | |
| "total_updates": 100000, "batch_size": 64, "microbatch_size": 8, | |
| "learning_rate": 1e-4, "adam_betas": [0.95, 0.999], | |
| "weight_decay": 1e-6, "warmup_steps": 500, "gradient_clip": None, | |
| "ema_power": 0.75, "ema_max_value": 0.9999, | |
| "checkpoint_every": 10000, "log_every": 100, "num_workers": 0, | |
| "validation_batch_size": 8, "validation_sampler": "ddim", | |
| "validation_inference_steps": 16, "validation_noise_seed": 7041, | |
| "no_real_robot": True, "test_only": False, | |
| } | |
| def canonical_config(config: dict) -> dict: | |
| cfg = copy.deepcopy(DEFAULTS) | |
| cfg.update(copy.deepcopy(config)) | |
| required = {"history": 2, "horizon": 16, "image_size": 96, | |
| "crop_shape": None, "resize_shape": None, "random_crop": False, "imagenet_norm": True, | |
| "cameras": ["front", "gripper"], "context_dim": 2060, | |
| "execution_start": 0, "execute_steps": 8, "control_hz": 10, | |
| "no_real_robot": True, "num_train_timesteps": 100, | |
| "beta_schedule": "squaredcos_cap_v2", "prediction_type": "epsilon", | |
| "clip_sample": True, "variance_type": "fixed_small"} | |
| for key, value in required.items(): | |
| if cfg[key] != value: | |
| raise ValueError(f"SO101 official-component contract requires {key}={value!r}") | |
| if not cfg["test_only"]: | |
| for key in ("down_dims", "diffusion_step_embed_dim", "kernel_size", "n_groups", | |
| "cond_predict_scale"): | |
| if cfg[key] != DEFAULTS[key]: | |
| raise ValueError(f"Production architecture requires {key}={DEFAULTS[key]!r}") | |
| if len(cfg["down_dims"]) != 3 or any(int(x) <= 0 or int(x) % cfg["n_groups"] for x in cfg["down_dims"]): | |
| raise ValueError("Use three positive group-divisible U-Net channel widths") | |
| for key in ("batch_size", "microbatch_size", "total_updates", "checkpoint_every", | |
| "log_every", "validation_batch_size", "validation_inference_steps"): | |
| if isinstance(cfg[key], bool) or int(cfg[key]) != cfg[key] or cfg[key] <= 0: | |
| raise ValueError(f"{key} must be a positive integer") | |
| if cfg["batch_size"] % cfg["microbatch_size"]: | |
| raise ValueError("microbatch_size must divide effective batch_size") | |
| if cfg["num_workers"] != 0: | |
| raise ValueError("Exact-resume batch stream uses num_workers=0") | |
| if cfg["warmup_steps"] < 0 or cfg["learning_rate"] <= 0: | |
| raise ValueError("Invalid optimizer schedule") | |
| if cfg["gradient_clip"] is not None and cfg["gradient_clip"] <= 0: | |
| raise ValueError("gradient_clip must be positive or None") | |
| return cfg | |
| def canonical_stats(stats: dict) -> dict: | |
| out = copy.deepcopy(stats) | |
| for key in STATS_KEYS: | |
| value = np.asarray(stats[key], dtype=np.float32) | |
| if value.shape != (6,) or not np.isfinite(value).all(): | |
| raise ValueError(f"{key} requires six finite values") | |
| out[key] = value.tolist() | |
| for prefix in ("action", "state"): | |
| if np.any(np.asarray(out[prefix + "_max"]) < np.asarray(out[prefix + "_min"])): | |
| raise ValueError(f"Invalid {prefix} min/max") | |
| return out | |
| def sha256(path: str | Path) -> str: | |
| h = hashlib.sha256() | |
| with Path(path).open("rb") as stream: | |
| for part in iter(lambda: stream.read(1024 * 1024), b""): | |
| h.update(part) | |
| return h.hexdigest() | |
| def _official_imports(): | |
| # A regular installed diffusion_policy package would otherwise take priority | |
| # over a namespace-only vendored snapshot. Do not silently mix the two. | |
| package_path = VENDOR / "diffusion_policy" | |
| existing = sys.modules.get("diffusion_policy") | |
| if existing is None: | |
| package = types.ModuleType("diffusion_policy") | |
| package.__path__ = [str(package_path)] | |
| package.__package__ = "diffusion_policy" | |
| sys.modules["diffusion_policy"] = package | |
| elif [Path(p).resolve() for p in getattr(existing, "__path__", [])] != [package_path.resolve()]: | |
| raise ImportError("Another diffusion_policy package is loaded; use an isolated process") | |
| from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder | |
| from diffusion_policy.model.vision.model_getter import get_resnet | |
| from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D | |
| from diffusion_policy.model.diffusion.ema_model import EMAModel | |
| for component in (MultiImageObsEncoder, get_resnet, ConditionalUnet1D, EMAModel): | |
| if not Path(inspect.getfile(component)).resolve().is_relative_to(VENDOR.resolve()): | |
| raise ImportError("An official component resolved outside the pinned vendor directory") | |
| return MultiImageObsEncoder, get_resnet, ConditionalUnet1D, EMAModel | |
| def official_ema(model: nn.Module, cfg: dict): | |
| """Upstream EMA owns its input; deepcopy is REQUIRED, not done upstream.""" | |
| *_, EMA = _official_imports() | |
| return EMA(copy.deepcopy(model), update_after_step=0, inv_gamma=1.0, | |
| power=float(cfg["ema_power"]), min_value=0.0, | |
| max_value=float(cfg["ema_max_value"])) | |
| class OfficialDP(nn.Module): | |
| kind = "teacher" | |
| def __init__(self, cfg: dict, stats: dict): | |
| super().__init__() | |
| self.cfg = canonical_config(cfg) | |
| self.stats = canonical_stats(stats) | |
| for prefix in ("action", "state"): | |
| low = torch.tensor(self.stats[prefix + "_min"], dtype=torch.float32) | |
| high = torch.tensor(self.stats[prefix + "_max"], dtype=torch.float32) | |
| self.register_buffer(prefix + "_center", (high + low) / 2) | |
| self.register_buffer(prefix + "_span", (high - low).clamp_min(1e-3)) | |
| Encoder, get_resnet, Unet, _ = _official_imports() | |
| shape_meta = {"obs": { | |
| "front": {"shape": [3, 96, 96], "type": "rgb"}, | |
| "gripper": {"shape": [3, 96, 96], "type": "rgb"}, | |
| "q": {"shape": [6], "type": "low_dim"}}, | |
| "action": {"shape": [6]}} | |
| self.encoder = Encoder( | |
| shape_meta=shape_meta, rgb_model=get_resnet("resnet18", weights=None), | |
| resize_shape=None, crop_shape=None, random_crop=False, | |
| use_group_norm=True, share_rgb_model=False, imagenet_norm=True) | |
| self.denoiser = Unet( | |
| input_dim=6, local_cond_dim=None, global_cond_dim=2060, | |
| diffusion_step_embed_dim=self.cfg["diffusion_step_embed_dim"], | |
| down_dims=self.cfg["down_dims"], kernel_size=self.cfg["kernel_size"], | |
| n_groups=self.cfg["n_groups"], cond_predict_scale=self.cfg["cond_predict_scale"]) | |
| from diffusers.schedulers.scheduling_ddpm import DDPMScheduler | |
| from diffusers.schedulers.scheduling_ddim import DDIMScheduler | |
| schedule = dict(num_train_timesteps=100, beta_start=0.0001, beta_end=0.02, | |
| beta_schedule="squaredcos_cap_v2", clip_sample=True, | |
| prediction_type="epsilon", timestep_spacing="leading", | |
| steps_offset=0) | |
| self.noise_scheduler = DDPMScheduler(**schedule, variance_type="fixed_small") | |
| self.ddim_scheduler = DDIMScheduler(**schedule, set_alpha_to_one=True) | |
| def normalize_action(self, action: Tensor) -> Tensor: | |
| return 2 * (action - self.action_center) / self.action_span | |
| def normalize_state(self, q: Tensor) -> Tensor: | |
| return 2 * (q - self.state_center) / self.state_span | |
| def decode(self, normalized_action: Tensor) -> Tensor: | |
| """Linear inverse only; the evaluator logs both explicit clamp stages.""" | |
| return normalized_action * self.action_span / 2 + self.action_center | |
| def encode(self, images: Tensor, q: Tensor) -> Tensor: | |
| if images.ndim != 6 or tuple(images.shape[1:]) != (2, 2, 3, 96, 96): | |
| raise ValueError("Expected [B,previous/current,front/gripper,RGB,96,96]") | |
| if images.dtype != torch.uint8 or tuple(q.shape) != (images.shape[0], 2, 6): | |
| raise ValueError("Images must be uint8 and q must be [B,2,6] environment coordinates") | |
| if not q.is_floating_point(): | |
| raise ValueError("q must contain floating-point environment-normalized joint states") | |
| b = images.shape[0] | |
| rgb = images.float() / 255.0 | |
| state = self.normalize_state(q) | |
| obs = {"front": rgb[:, :, 0].reshape(b * 2, 3, 96, 96), | |
| "gripper": rgb[:, :, 1].reshape(b * 2, 3, 96, 96), | |
| "q": state.reshape(b * 2, 6)} | |
| return self.encoder(obs).reshape(b, 2060) | |
| def head_predict(self, context: Tensor, noise: Tensor | None = None, *, | |
| sampler: str = "ddpm", inference_steps: int = 100, | |
| generator: torch.Generator | None = None) -> Tensor: | |
| """Return all 16 actions starting at current t, BEFORE deployment clamps. | |
| DDPM draws new Gaussian noise at every nonterminal reverse step. Supplying | |
| only initial noise does not make DDPM deterministic: pass a seeded generator | |
| as well. DDIM uses eta=0. Both use diffusers' official update equations. | |
| """ | |
| if context.ndim != 2 or context.shape[1] != 2060: | |
| raise ValueError("Expected context [B,2060]") | |
| if sampler not in ("ddpm", "ddim") or not 1 <= int(inference_steps) <= 100: | |
| raise ValueError("Use ddpm/ddim with 1..100 inference steps") | |
| shape = (context.shape[0], 16, 6) | |
| if noise is None: | |
| sample = torch.randn(shape, dtype=context.dtype, device=context.device, generator=generator) | |
| else: | |
| if tuple(noise.shape) != shape or noise.device != context.device or noise.dtype != context.dtype: | |
| raise ValueError("Initial noise must match prediction shape/device/dtype") | |
| sample = noise.clone() | |
| scheduler = self.noise_scheduler if sampler == "ddpm" else self.ddim_scheduler | |
| scheduler.set_timesteps(int(inference_steps), device=context.device) | |
| finite = torch.isfinite(sample).all() & torch.isfinite(context).all() | |
| for t in scheduler.timesteps: | |
| epsilon = self.denoiser(sample, t, global_cond=context) | |
| finite = finite & torch.isfinite(epsilon).all() | |
| if sampler == "ddpm": | |
| sample = scheduler.step(epsilon, t, sample, generator=generator).prev_sample | |
| else: | |
| sample = scheduler.step(epsilon, t, sample, eta=0.0, generator=generator).prev_sample | |
| finite = finite & torch.isfinite(sample).all() | |
| if not bool(finite): | |
| raise FloatingPointError("Non-finite intermediate diffusion sample") | |
| return sample | |
| def predict(self, images: Tensor, q: Tensor, noise: Tensor | None = None, *, | |
| sampler: str = "ddpm", inference_steps: int = 100, | |
| generator: torch.Generator | None = None) -> Tensor: | |
| raw = self.head_predict(self.encode(images, q), noise, sampler=sampler, | |
| inference_steps=inference_steps, generator=generator) | |
| return self.decode(raw.clamp(-1, 1)).clamp(-1, 1) | |
| def training_loss(self, batch: dict, *, generator: torch.Generator | None = None) -> Tensor: | |
| target = self.normalize_action(batch["action"]) | |
| if target.shape[1:] != (16, 6) or batch["mask"].shape != target.shape[:2]: | |
| raise ValueError("Training target must be a masked [B,16,6] current-time chunk") | |
| noise = torch.randn(target.shape, device=target.device, dtype=target.dtype, generator=generator) | |
| timestep = torch.randint(0, 100, (target.shape[0],), device=target.device, generator=generator) | |
| noisy = self.noise_scheduler.add_noise(target, noise, timestep) | |
| prediction = self.denoiser(noisy, timestep, global_cond=self.encode(batch["images"], batch["q"])) | |
| mask = batch["mask"].unsqueeze(-1).expand_as(target) | |
| count = mask.sum() | |
| if count <= 0: | |
| raise ValueError("Empty action mask") | |
| return ((prediction - noise).square() * mask).sum() / count | |
| def make_dataset(root: str | Path, cfg: dict, split: str): | |
| """Use the ORIGINAL manifest configuration; new optimizer keys are unrelated. | |
| No new demonstrations, boundary padding, statistics fit or test examples are | |
| introduced. Requested semantic fields must agree with the recorded dataset. | |
| """ | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| from experiments.so101_pbit_sim.data import SimDataset | |
| manifest = json.loads((Path(root) / "manifest.json").read_text(encoding="utf-8")) | |
| original = manifest["config"] | |
| shared = ("task_id", "cameras", "camera_layout", "image_size", "horizon", "history", | |
| "execution_start", "control_hz", "success_contract", "action", "no_real_robot", | |
| "train_seeds", "validation_seeds", "development_seeds") | |
| checked = canonical_config(cfg) | |
| for key in shared: | |
| if key in checked and checked[key] != original.get(key): | |
| raise ValueError(f"Original dataset semantic contract differs at {key}") | |
| if "test_seeds" in checked and checked["test_seeds"] != original["test_seeds"]: | |
| if checked.get("historical_test_seeds") != original["test_seeds"]: | |
| raise ValueError("Declare the complete historical test set before choosing a fresh holdout") | |
| used = set().union(*(set(original[k]) for k in | |
| ("train_seeds", "validation_seeds", "test_seeds", "development_seeds"))) | |
| if used.intersection(checked["test_seeds"]) or len(set(checked["test_seeds"])) != len(checked["test_seeds"]): | |
| raise ValueError("Fresh holdout overlaps a prior split or contains duplicate seeds") | |
| data = SimDataset(root, original, split) | |
| if data.stats.get("fit_seeds") != original["train_seeds"]: | |
| raise ValueError("Normalization was not fit exclusively on training episodes") | |
| return data | |
| def source_record() -> dict: | |
| manifest_path = VENDOR / "source_manifest.json" | |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) | |
| if manifest.get("commit") != UPSTREAM_COMMIT: | |
| raise ValueError("Unexpected upstream source revision") | |
| for row in manifest["files"]: | |
| path = VENDOR / row["path"] | |
| if sha256(path) != row["sha256"]: | |
| raise ValueError(f"Pinned vendor file changed: {row['path']}") | |
| return {"upstream_commit": UPSTREAM_COMMIT, | |
| "vendor_manifest_sha256": sha256(manifest_path), | |
| "git_commit_declared_by_launcher": os.environ.get("SOURCE_SHA"), | |
| "file_sha256": {p.relative_to(REPO_ROOT).as_posix(): sha256(p) | |
| for p in (HERE / "model.py", HERE / "train.py") if p.is_file()}, | |
| "versions": {name: importlib.metadata.version(name) | |
| for name in ("torch", "torchvision", "diffusers", "einops")}} | |
| def policy_payload(model: OfficialDP, *, step: int, manifest_sha: str, | |
| extra: dict | None = None) -> dict: | |
| return {"format": FORMAT, "kind": "teacher", "config": model.cfg, | |
| "stats": model.stats, "state_dict": model.state_dict(), "step": int(step), | |
| "dataset_manifest_sha256": manifest_sha, "source": source_record(), | |
| "action_time_origin": "current_t", "execution_start": 0, | |
| "clamp_contract": "data_clamp_then_linear_decode_then_environment_clamp", | |
| "weights": "EMA", "extra": copy.deepcopy(extra or {})} | |
| def atomic_torch_save(payload: dict, path: str | Path) -> dict: | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| temporary = path.with_name(path.name + ".tmp") | |
| torch.save(payload, temporary) | |
| temporary.replace(path) | |
| return {"path": path.name, "sha256": sha256(path), "bytes": path.stat().st_size, | |
| "step": payload.get("step")} | |
| def load_policy(path: str | Path, device: str | torch.device = "cpu") -> tuple[OfficialDP, dict]: | |
| ck = torch.load(Path(path), map_location="cpu", weights_only=True) | |
| if ck.get("format") != FORMAT or ck.get("action_time_origin") != "current_t": | |
| raise ValueError("Not an official-component SO101 EMA inference checkpoint") | |
| current_source = source_record() | |
| for key in ("upstream_commit", "vendor_manifest_sha256"): | |
| if ck.get("source", {}).get(key) != current_source[key]: | |
| raise ValueError(f"Checkpoint official-component identity differs: {key}") | |
| model = OfficialDP(ck["config"], ck["stats"]) | |
| expected = {name: getattr(model, name).clone() for name in NORMALIZER_KEYS} | |
| model.load_state_dict(ck["state_dict"], strict=True) | |
| for name, value in expected.items(): | |
| if not torch.equal(value, getattr(model, name)): | |
| raise ValueError(f"Checkpoint normalizer contradicts train statistics: {name}") | |
| for name, value in model.state_dict().items(): | |
| if not torch.isfinite(value).all(): | |
| raise ValueError(f"Non-finite checkpoint tensor: {name}") | |
| return model.to(device).eval(), ck | |