tinyvla / tinyvla2 /scripts /expand_checkpoint.py
AlexWortega's picture
Upload tinyvla2/scripts/expand_checkpoint.py with huggingface_hub
11ae309 verified
Raw
History Blame Contribute Delete
4.17 kB
#!/usr/bin/env python
"""Grow a checkpoint's embodiment/state/action dimensions without losing what it learned.
Adding the unitree + navigation packs widens three things:
max_state_dim 16 -> 32 (G1 Dex3 has 28 joint channels)
max_action_dim 8 -> 32 (same, and nav actions are 3-dim)
num_embodiments 16 -> 24 (packed ids reach 19)
Every affected weight is grown by ZERO padding, never re-initialised:
state_proj.weight (d, 16) -> (d, 32) new input columns = 0
action_in.weight (d, 8) -> (d, 32) new input columns = 0
action_out.weight (8, d) -> (32, d) new output rows = 0
action_out.bias (8,) -> (32,) new outputs = 0
embodiment emb (16, d) -> (24, d) new rows = 0
Zeroed input columns receive zero-padded inputs anyway, and zeroed output rows
predict 0 for dims no old robot had — so on the OLD data the expanded model is
bit-for-bit the old model, and the new dims start from a clean slate. That makes
"did the new data help or hurt?" answerable, which a re-initialised head would not.
Usage:
python scripts/expand_checkpoint.py outputs/tv2_C_scaled/final outputs/tv2_C_scaled_x32
"""
from __future__ import annotations
import json
import shutil
import sys
from pathlib import Path
import torch
from safetensors.torch import load_file, save_file
# param name suffix -> axis that must grow (0 = rows/out, 1 = cols/in)
GROW = {
"state_proj.weight": 1,
"action_in.weight": 1,
"action_out.weight": 0,
"action_out.bias": 0,
}
def main():
src = Path(sys.argv[1])
dst = Path(sys.argv[2])
new_state = int(sys.argv[3]) if len(sys.argv) > 3 else 32
new_action = int(sys.argv[4]) if len(sys.argv) > 4 else 32
new_emb = int(sys.argv[5]) if len(sys.argv) > 5 else 24
dst.mkdir(parents=True, exist_ok=True)
for f in src.iterdir():
if f.suffix in (".json", ".txt") or f.name.startswith("README"):
shutil.copy(f, dst / f.name)
cfg_path = dst / "config.json"
cfg = json.loads(cfg_path.read_text())
old_state = cfg.get("max_state_dim", 16)
old_action = cfg.get("max_action_dim", 8)
old_emb = cfg.get("num_embodiments", 16)
cfg["max_state_dim"] = new_state
cfg["max_action_dim"] = new_action
cfg["num_embodiments"] = new_emb
# input/output_features carry their own copy of the dims. Leaving them stale
# makes the checkpoint load with a 32-wide expert but an 8-wide declared
# action, so anything reading the feature spec silently truncates.
if "action" in cfg.get("output_features", {}):
cfg["output_features"]["action"]["shape"] = [new_action]
if "observation.state" in cfg.get("input_features", {}):
cfg["input_features"]["observation.state"]["shape"] = [new_state]
cfg_path.write_text(json.dumps(cfg, indent=2))
print(f"config: state {old_state}->{new_state} action {old_action}->{new_action} "
f"embodiments {old_emb}->{new_emb}")
weights = src / "model.safetensors"
sd = load_file(str(weights))
grown = []
for k, v in list(sd.items()):
target = None
for suffix, axis in GROW.items():
if k.endswith(suffix):
want = new_state if "state_proj" in k else new_action
if v.shape[axis] < want:
target = (axis, want)
break
# embodiment table: a 2-D embedding whose row count equals num_embodiments
if target is None and v.dim() == 2 and v.shape[0] == old_emb and "emb" in k.lower():
target = (0, new_emb)
if target is None:
continue
axis, want = target
shape = list(v.shape)
pad = want - shape[axis]
if pad <= 0:
continue
shape[axis] = pad
sd[k] = torch.cat([v, v.new_zeros(*shape)], dim=axis)
grown.append(f" {k}: {tuple(v.shape)} -> {tuple(sd[k].shape)}")
save_file(sd, str(dst / "model.safetensors"), metadata={"format": "pt"})
print("grown tensors:" if grown else "nothing to grow (already wide enough)")
print("\n".join(grown))
print(f"wrote {dst}")
if __name__ == "__main__":
main()