Instructions to use AlexWortega/tinyvla with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use AlexWortega/tinyvla with LeRobot:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Upload tinyvla2/scripts/train_groot_morph.py with huggingface_hub
Browse files
tinyvla2/scripts/train_groot_morph.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Convert the id-conditioned GR00T checkpoint to morphology conditioning.
|
| 3 |
+
|
| 4 |
+
Why this run exists: the physical_ai_ft checkpoint is `conditioning: id`, and a
|
| 5 |
+
learned id is a lookup table — it has no row for a robot that was never trained on,
|
| 6 |
+
and none can be synthesised. Measured on held-out LeKiwi: every trained id scores
|
| 7 |
+
1.18-1.33 on the absolute dims (worse than holding still) and a PURE RANDOM identity
|
| 8 |
+
vector does just as well. Simply switching the config to `morph` and grafting
|
| 9 |
+
tv2_C_scaled's morph encoder does not help either (1.30/1.11) — the expert spent 60k
|
| 10 |
+
steps learning to read an id token and has never seen morph tokens.
|
| 11 |
+
|
| 12 |
+
So the expert has to be taught to read the descriptor. Everything else is already
|
| 13 |
+
trained, so this only has to re-route one conditioning slot: 16 physical numbers ->
|
| 14 |
+
MLP -> 2 tokens, in place of the 1 id token.
|
| 15 |
+
|
| 16 |
+
Data is streamed from the hub exactly as the original run did (no disk), with the
|
| 17 |
+
six GR00T family descriptors from configs/groot_morphology.yaml.
|
| 18 |
+
|
| 19 |
+
The payoff is the project's central claim, measured rather than argued: if a
|
| 20 |
+
descriptor-conditioned model transfers to LeKiwi where the id-conditioned one cannot,
|
| 21 |
+
that is the difference between a code you can look up and a code you can compute.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import argparse
|
| 27 |
+
import time
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
import torch
|
| 31 |
+
import yaml
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
ap = argparse.ArgumentParser()
|
| 36 |
+
ap.add_argument("--resume", default="/home/alexw/tinyvla/outputs/physical_ai_ft_morph")
|
| 37 |
+
ap.add_argument("--out", default="/home/alexw/tinyvla/outputs/groot_morph")
|
| 38 |
+
ap.add_argument("--spec", default="/home/alexw/tinyvla/configs/physical_ai_stream.yaml")
|
| 39 |
+
ap.add_argument("--descriptors", default="/home/alexw/tinyvla/configs/groot_morphology.yaml")
|
| 40 |
+
ap.add_argument("--steps", type=int, default=6000)
|
| 41 |
+
ap.add_argument("--batch-size", type=int, default=32)
|
| 42 |
+
ap.add_argument("--num-workers", type=int, default=6)
|
| 43 |
+
ap.add_argument("--lr", type=float, default=1.0e-4)
|
| 44 |
+
ap.add_argument("--backbone-lr-mult", type=float, default=0.1)
|
| 45 |
+
ap.add_argument("--warmup", type=int, default=200)
|
| 46 |
+
ap.add_argument("--save-freq", type=int, default=1000)
|
| 47 |
+
ap.add_argument("--log-freq", type=int, default=50)
|
| 48 |
+
args = ap.parse_args()
|
| 49 |
+
|
| 50 |
+
from safetensors.torch import load_file
|
| 51 |
+
from transformers import AutoTokenizer
|
| 52 |
+
|
| 53 |
+
from tinyvla.data.hub_stream import HubEpisodeStream
|
| 54 |
+
from tinyvla.modeling_tinyvla import TinyVLAPolicy
|
| 55 |
+
from tinyvla.modules.embodiment import MORPH_FIELDS
|
| 56 |
+
|
| 57 |
+
out = Path(args.out)
|
| 58 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
|
| 60 |
+
pol = TinyVLAPolicy.from_pretrained(args.resume)
|
| 61 |
+
pol.load_state_dict(load_file(f"{args.resume}/model.safetensors"), strict=True)
|
| 62 |
+
cfg = pol.config
|
| 63 |
+
assert cfg.conditioning == "morph", f"expected morph conditioning, got {cfg.conditioning}"
|
| 64 |
+
pol = pol.cuda().train()
|
| 65 |
+
tok = AutoTokenizer.from_pretrained(cfg.lm_model_name)
|
| 66 |
+
print(f"resumed {args.resume} | conditioning={cfg.conditioning} "
|
| 67 |
+
f"max_state={cfg.max_state_dim} max_action={cfg.max_action_dim}", flush=True)
|
| 68 |
+
|
| 69 |
+
# same field scaling the rest of the project uses (see MORPH_FIELDS in embodiment.py)
|
| 70 |
+
sc = {"arm_dof": 0.1, "reach_m": 2, "gripper_width_m": 10, "num_cameras": 1 / 3,
|
| 71 |
+
"control_hz": 1 / 30, "joint_lo_mean": 1 / 3.1416, "joint_hi_mean": 1 / 3.1416,
|
| 72 |
+
"workspace_x": 2, "workspace_y": 2, "workspace_z": 2, "payload_kg": 0.2}
|
| 73 |
+
raw = yaml.safe_load(Path(args.descriptors).read_text())
|
| 74 |
+
morph = {k: torch.tensor([v.get(f, 0) * sc.get(f, 1) for f in MORPH_FIELDS], dtype=torch.float32)
|
| 75 |
+
for k, v in raw.items()}
|
| 76 |
+
print("descriptors:", list(morph))
|
| 77 |
+
|
| 78 |
+
specs = yaml.safe_load(Path(args.spec).read_text())["datasets"]
|
| 79 |
+
missing = {s.get("morph_key") for s in specs} - set(morph)
|
| 80 |
+
assert not missing, f"specs reference descriptors that do not exist: {missing}"
|
| 81 |
+
|
| 82 |
+
# Fix the sampler bug the original run shipped with: HubEpisodeStream draws a
|
| 83 |
+
# DATASET PER EPISODE, so a weight of sqrt(frames) is silently multiplied by
|
| 84 |
+
# episode length. r1_pro episodes are ~37x longer, which turned an intended 49%
|
| 85 |
+
# share into a realized 96.5% — and here it also starves the loader, because
|
| 86 |
+
# every draw pulls a ~190 MB two-camera episode and the first batch never
|
| 87 |
+
# arrives. Dividing by relative episode length restores the intended mixture.
|
| 88 |
+
EP_LEN = {"r1_pro": 37.0, "gr1": 1.0, "panda_single": 1.0,
|
| 89 |
+
"panda_bi_gripper": 1.0, "panda_bi_hand": 1.0, "g1": 0.6}
|
| 90 |
+
for sp in specs:
|
| 91 |
+
sp["weight"] = float(sp["weight"]) / EP_LEN.get(sp.get("morph_key"), 1.0)
|
| 92 |
+
from collections import defaultdict as _dd
|
| 93 |
+
_agg = _dd(float)
|
| 94 |
+
for sp in specs:
|
| 95 |
+
_agg[sp["morph_key"]] += sp["weight"]
|
| 96 |
+
_tot = sum(_agg.values())
|
| 97 |
+
print("reweighted family shares:", {k: round(v / _tot, 3) for k, v in sorted(_agg.items())})
|
| 98 |
+
|
| 99 |
+
stream = HubEpisodeStream(specs, chunk=cfg.chunk_size, image_size=cfg.image_size,
|
| 100 |
+
max_state_dim=cfg.max_state_dim, max_action_dim=cfg.max_action_dim,
|
| 101 |
+
shuffle_buffer=64, seed=1234, morph_descriptors=morph)
|
| 102 |
+
loader = torch.utils.data.DataLoader(
|
| 103 |
+
stream, batch_size=args.batch_size, num_workers=args.num_workers,
|
| 104 |
+
pin_memory=True, prefetch_factor=4, persistent_workers=True,
|
| 105 |
+
multiprocessing_context="spawn", # fork deadlocks the hub HTTP pool (B200.md)
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
backbone, head = [], []
|
| 109 |
+
for n, p in pol.named_parameters():
|
| 110 |
+
if not p.requires_grad:
|
| 111 |
+
continue
|
| 112 |
+
(backbone if n.startswith("semantic.") else head).append(p)
|
| 113 |
+
opt = torch.optim.AdamW([
|
| 114 |
+
{"params": head, "lr": args.lr},
|
| 115 |
+
{"params": backbone, "lr": args.lr * args.backbone_lr_mult},
|
| 116 |
+
], weight_decay=1e-4)
|
| 117 |
+
sched = torch.optim.lr_scheduler.LambdaLR(
|
| 118 |
+
opt, lambda s: min(1.0, (s + 1) / args.warmup))
|
| 119 |
+
scaler_dtype = torch.bfloat16
|
| 120 |
+
print(f"head {sum(p.numel() for p in head)/1e6:.1f}M | "
|
| 121 |
+
f"backbone {sum(p.numel() for p in backbone)/1e6:.1f}M at {args.backbone_lr_mult}x", flush=True)
|
| 122 |
+
|
| 123 |
+
step, t0 = 0, time.time()
|
| 124 |
+
for batch in loader:
|
| 125 |
+
t = tok(list(batch["task"]), padding="max_length", truncation=True,
|
| 126 |
+
max_length=48, return_tensors="pt")
|
| 127 |
+
b = {k: v.cuda(non_blocking=True) for k, v in batch.items()
|
| 128 |
+
if isinstance(v, torch.Tensor)}
|
| 129 |
+
b["observation.language.tokens"] = t["input_ids"].cuda()
|
| 130 |
+
b["observation.language.attention_mask"] = t["attention_mask"].bool().cuda()
|
| 131 |
+
with torch.autocast("cuda", scaler_dtype):
|
| 132 |
+
loss, info = pol.forward(b)
|
| 133 |
+
loss.backward()
|
| 134 |
+
torch.nn.utils.clip_grad_norm_([p for g in opt.param_groups for p in g["params"]], 10.0)
|
| 135 |
+
opt.step()
|
| 136 |
+
sched.step()
|
| 137 |
+
opt.zero_grad(set_to_none=True)
|
| 138 |
+
step += 1
|
| 139 |
+
if step % args.log_freq == 0:
|
| 140 |
+
print(f"step {step}/{args.steps} loss {info['loss']:.4f} "
|
| 141 |
+
f"{step/(time.time()-t0):.2f} it/s", flush=True)
|
| 142 |
+
if step % args.save_freq == 0 or step == args.steps:
|
| 143 |
+
d = out / (f"step_{step}" if step < args.steps else "final")
|
| 144 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 145 |
+
pol.save_pretrained(str(d))
|
| 146 |
+
print(f"saved {d}", flush=True)
|
| 147 |
+
if step >= args.steps:
|
| 148 |
+
break
|
| 149 |
+
print("DONE", flush=True)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
main()
|