File size: 13,905 Bytes
8101696 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | """One-command adapter to the accompanying StarVLA training backend.
The CLI prepares temporal proposals and dispatches StarVLA training.
The sampler and ordinary importance correction are unchanged.
"""
from __future__ import annotations
import argparse
import json
import math
import os
from pathlib import Path
import subprocess
import sys
def parser() -> argparse.ArgumentParser:
result = argparse.ArgumentParser(description=__doc__)
result.add_argument("--config", type=Path, required=True)
result.add_argument("--base-policy", type=Path, required=True,
help="Complete Accelerate checkpoint directory, committed single-process model file, or model file with --start weights.")
result.add_argument("--start", choices=("resume", "weights"), default="weights",
help="resume restores the full state; weights explicitly starts a new optimizer/schedule.")
result.add_argument("--alpha", type=float, default=0.2)
result.add_argument("--K", "--k", dest="k", type=int, default=8)
result.add_argument("--mode", nargs="+", choices=("retime", "uniform", "shuffle"),
default=["retime"])
result.add_argument("--manifest", type=Path)
result.add_argument("--data-root", type=Path, help="Override the dataset root in the config")
result.add_argument("--data-mix", type=str, help="Override the named dataset mixture in the config")
result.add_argument("--vlm-path", type=Path, help="Override the Florence/Qwen backbone location")
result.add_argument("--statistics-path", type=Path, help="Override precomputed dataset statistics, if required")
result.add_argument(
"--omit-data-identity-files",
action="store_true",
help="Use the manifest's computed dataset identity without separate provenance/fingerprint files.",
)
result.add_argument("--epsilon", type=float, default=0.1)
result.add_argument("--shuffle-seed", type=int, default=12345)
result.add_argument("--accelerate-config", type=Path, required=True)
result.add_argument("--output", type=Path, required=True)
result.add_argument("--enable-wandb", action="store_true")
result.add_argument("--backend-root", type=Path, default=Path(__file__).resolve().parents[1],
help="ReTime-enabled StarVLA source tree shipped with this artifact.")
result.add_argument("--dry-run", action="store_true",
help="Validate local inputs and print commands; do not score, train, or write files.")
return result
def _file(path: Path, label: str) -> Path:
path = path.expanduser().resolve()
if not path.is_file():
raise ValueError(f"{label} is not a file: {path}")
return path
def _directory(path: Path, label: str) -> Path:
path = path.expanduser().resolve()
if not path.is_dir():
raise ValueError(f"{label} is not a directory: {path}")
return path
def _native_commit_path(model_path: Path) -> Path:
for suffix in ("_pytorch_model.pt", "_model.safetensors"):
if model_path.name.endswith(suffix):
return model_path.with_name(
model_path.name[: -len(suffix)] + "_checkpoint.json"
)
raise ValueError(
"A single-process resume checkpoint must end in "
"_pytorch_model.pt or _model.safetensors"
)
def _validate_resume_checkpoint(base: Path, world_size: int) -> tuple[Path, dict]:
if base.is_dir():
model = _file(base / "pytorch_model.pt", "base-policy model")
commit = json.loads(
_file(base / "checkpoint.json", "checkpoint commit").read_text(
encoding="utf-8"
)
)
if int(commit.get("world_size", -1)) != world_size:
raise ValueError("Base checkpoint world size differs from the launch contract")
entries = commit.get("files", [])
if not entries or any(
not (base / entry["name"]).is_file()
or (base / entry["name"]).stat().st_size != int(entry["bytes"])
for entry in entries
):
raise ValueError("Base Accelerate checkpoint is incomplete")
return model, commit
model = _file(base, "base-policy model")
if world_size != 1:
raise ValueError(
"A committed single-process checkpoint requires a one-process launch"
)
commit_path = _native_commit_path(model)
commit = json.loads(_file(commit_path, "checkpoint commit").read_text(encoding="utf-8"))
files = commit.get("files", {})
for label in ("model", "training_state"):
entry = files.get(label, {})
path = model.parent / str(entry.get("name", ""))
if not path.is_file() or path.stat().st_size != int(entry.get("bytes", -1)):
raise ValueError(f"Base single-process checkpoint has an invalid {label} file")
if files["model"]["name"] != model.name:
raise ValueError("Checkpoint commit does not describe --base-policy")
return model, commit
def build_plan(args: argparse.Namespace) -> dict:
from omegaconf import OmegaConf
if not math.isfinite(args.alpha) or not 0 <= args.alpha < 1:
raise ValueError("alpha must satisfy 0 <= alpha < 1")
if args.k < 1 or not math.isfinite(args.epsilon) or args.epsilon <= 0:
raise ValueError("K and epsilon must be positive")
if len(set(args.mode)) != len(args.mode):
raise ValueError("Each mode can be requested only once")
root = args.backend_root.expanduser().resolve()
config = _file(args.config, "config")
launch_config = _file(args.accelerate_config, "accelerate config")
cfg = OmegaConf.load(config)
if not args.enable_wandb:
cfg.wandb_project = None
launch = OmegaConf.load(launch_config)
output = args.output.expanduser().resolve()
cfg.run_root_dir = str(output / "runs")
cfg.output_dir = str(output / "prepare")
if args.data_root:
cfg.datasets.vla_data.data_root_dir = str(_directory(args.data_root, "dataset root"))
if args.data_mix:
cfg.datasets.vla_data.data_mix = args.data_mix
if args.vlm_path:
cfg.framework.qwenvl.base_vlm = str(_directory(args.vlm_path, "VLM backbone"))
if args.statistics_path:
cfg.datasets.vla_data.precomputed_statistics_path = str(_file(args.statistics_path, "statistics"))
if args.omit_data_identity_files:
cfg.datasets.vla_data.dataset_provenance_path = None
cfg.datasets.vla_data.dataset_content_fingerprint_path = None
# Model-only initialization derives support from the current dataset.
if args.start == "weights":
cfg.trainer.is_resume = False
cfg.trainer.resume_accelerate_checkpoint = None
cfg.trainer.resume_checkpoint_integrity_manifest = None
if OmegaConf.select(cfg, "trainer.initial_completed_steps") is not None:
cfg.trainer.initial_completed_steps = 0
if args.manifest is None:
cfg.datasets.vla_data.valid_anchor_manifest_path = None
cfg.datasets.vla_data.dataset_provenance_path = None
cfg.datasets.vla_data.dataset_content_fingerprint_path = None
cfg.datasets.vla_data.temporal_proposal_path = None
if args.manifest is not None:
cfg.datasets.vla_data.valid_anchor_manifest_path = str(args.manifest.expanduser().resolve())
if OmegaConf.select(cfg, "framework.name") != "QwenGR00T":
raise ValueError("This adapter supports the supplied Florence/Qwen GR00T configs only")
if not OmegaConf.select(cfg, "datasets.vla_data.traceable_sampling", default=False):
raise ValueError("ReTime requires traceable_sampling and a frozen valid-anchor manifest")
world_size = int(launch.num_processes)
if int(OmegaConf.select(launch, "num_machines", default=1)) != 1:
raise ValueError("The one-command launcher currently supports one machine")
if world_size != int(cfg.datasets.vla_data.expected_world_size):
raise ValueError("Accelerate process count differs from the dataset training contract")
accumulation = int(cfg.trainer.gradient_accumulation_steps)
ds_accumulation = OmegaConf.select(launch, "deepspeed_config.gradient_accumulation_steps")
if ds_accumulation is not None and int(ds_accumulation) != accumulation:
raise ValueError("Accelerate and trainer gradient accumulation differ")
batch = int(cfg.datasets.vla_data.per_device_batch_size)
if min(world_size, accumulation, batch) < 1:
raise ValueError("World size, accumulation and batch size must be positive")
configured_manifest = OmegaConf.select(cfg, "datasets.vla_data.valid_anchor_manifest_path")
manifest = args.manifest or (Path(configured_manifest) if configured_manifest else None)
prepare_manifest = manifest is None
if prepare_manifest and args.start == "resume":
raise ValueError("Exact resume requires the parent run's manifest; use --manifest")
manifest = output / "proposal" / "manifest.json" if prepare_manifest else _file(manifest, "manifest")
if configured_manifest and Path(configured_manifest).expanduser().resolve() != manifest:
raise ValueError("--manifest differs from the config manifest")
base = args.base_policy.expanduser().resolve()
if args.start == "resume":
checkpoint, _ = _validate_resume_checkpoint(base, world_size)
else:
checkpoint = _file(base / "pytorch_model.pt" if base.is_dir() else base, "base-policy model")
cfg.trainer.pretrained_checkpoint = str(checkpoint) if args.start == "weights" else None
cfg.trainer.resume_accelerate_checkpoint = str(base) if args.start == "resume" else None
cfg.trainer.is_resume = args.start == "resume"
modes = list(args.mode)
for mode in modes:
if (output / "runs" / mode).exists():
raise ValueError(f"Training output already exists: {output / 'runs' / mode}; choose a fresh --output")
commands = []
cfg.datasets.vla_data.valid_anchor_manifest_path = str(manifest)
# Materialize all paths once so every arm and subprocess uses the same config.
config_text = OmegaConf.to_yaml(cfg, resolve=True)
config = output / "config.yaml"
if prepare_manifest:
commands.append([sys.executable, "-m", "retime.prepare", "--config", str(config), "--output", str(manifest)])
def script(name: str, *arguments: str) -> None:
module = {
"build_residual_temporal_proposal.py": "retime.commands.proposal",
"build_shuffle_temporal_proposal.py": "retime.commands.shuffle",
}[name]
commands.append([sys.executable, "-m", module, *map(str, arguments)])
residual = output / "proposal" / "retime.json"
shuffled = output / "proposal" / "shuffle.json"
if any(mode != "uniform" for mode in modes):
script("build_residual_temporal_proposal.py", "--config", config, "--checkpoint", checkpoint,
"--manifest", manifest, "--cache-dir", output / "cache",
"--output", residual, "--grid-size", args.k, "--alpha", args.alpha, "--epsilon", args.epsilon)
if "shuffle" in modes:
script("build_shuffle_temporal_proposal.py", "--manifest", manifest,
"--residual-proposal", residual, "--permutation-seed", args.shuffle_seed,
"--output", shuffled)
training = _file(root / "starVLA" / "training" / "train_starvla.py", "training backend")
for mode in modes:
proposal = {"uniform": None, "retime": str(residual), "shuffle": str(shuffled)}[mode]
overrides = {
"run_id": mode,
"run_root_dir": str(output / "runs"),
"output_dir": str(output / "runs" / mode),
"datasets.vla_data.valid_anchor_manifest_path": str(manifest),
"datasets.vla_data.temporal_proposal_path": proposal,
"trainer.is_resume": args.start == "resume",
"trainer.allow_model_only_resume": False,
"trainer.resume_accelerate_checkpoint": str(base) if args.start == "resume" else None,
"trainer.pretrained_checkpoint": str(checkpoint) if args.start == "weights" else None,
}
commands.append([
sys.executable, "-m", "accelerate.commands.launch", "--config_file", str(launch_config),
"--num_processes", str(world_size), str(training), "--config_yaml", str(config),
*[f"--{key}={json.dumps(value)}" for key, value in overrides.items()],
])
return {
"backend_root": str(root), "output": str(output), "base_policy": str(base),
"start": args.start, "alpha": args.alpha, "K": args.k, "modes": modes,
"global_batch_size": batch * world_size * accumulation,
"config_text": config_text,
"commands": commands,
}
def main(argv: list[str] | None = None) -> int:
arguments = parser()
args = arguments.parse_args(argv)
try:
plan = build_plan(args)
except (ValueError, OSError, ImportError, KeyError) as error:
arguments.error(str(error))
print(json.dumps({key: value for key, value in plan.items() if key != "config_text"}, indent=2), flush=True)
if args.dry_run:
return 0
output = Path(plan["output"])
output.mkdir(parents=True, exist_ok=True)
(output / "proposal").mkdir(exist_ok=True)
(output / "config.yaml").write_text(plan["config_text"], encoding="utf-8")
(output / "plan.json").write_text(json.dumps(plan, indent=2) + "\n", encoding="utf-8")
env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join(filter(None, [plan["backend_root"], env.get("PYTHONPATH")]))
env.setdefault("WANDB_MODE", "offline")
env["PYTHONUNBUFFERED"] = "1"
for command in plan["commands"]:
subprocess.run(command, cwd=plan["backend_root"], env=env, check=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|