FlowMo-WM / train.py
cccat6's picture
Complete FlowMo reproducibility materials and record input provenance
eb4fc81 verified
Raw History Blame Contribute Delete
6.25 kB
"""Train the published FlowMo configurations without overwriting supplied weights."""
from pathlib import Path
import argparse
import json
import os
import subprocess
import sys
import time
CORE = ['flowmo', 'history32', 'history8', 'flowmo_additive', 'flowmo_film']
INITIAL = ['flowmo', 'leworldmodel', 'planet', 'tdmpc2']
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--package', type=Path, default=Path(__file__).resolve().parent)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--groups', nargs='+', choices=['initial', 'core', 'ood', 'scaling', 'all'], default=['core'])
parser.add_argument('--methods', nargs='+', choices=sorted(set(CORE + INITIAL)))
parser.add_argument('--seeds', nargs='+', type=int, default=list(range(1, 9)))
parser.add_argument('--device', default='cuda')
parser.add_argument('--smoke', action='store_true', help='One optimizer step; never formal paper statistics')
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
root, out = args.package.resolve(), args.output.resolve()
if out == root or out in root.parents or any(out == root / d or root / d in out.parents for d in ['data', 'experiments', 'paper', 'assets', 'provenance']):
raise ValueError('Choose a separate training output directory')
if any(s not in range(1, 9) for s in args.seeds):
raise ValueError('Formal seeds are 1 through 8')
cfg = json.loads((root / 'experiments/shared/config/rebuttal_v1.json').read_text())
groups = ['initial', 'core', 'ood', 'scaling'] if 'all' in args.groups else args.groups
jobs = []
for group in groups:
methods = INITIAL if group == 'initial' else (['flowmo'] if group == 'scaling' else CORE)
if args.methods:
methods = [m for m in methods if m in args.methods]
for method in methods:
for seed in ([19] if group == 'initial' else args.seeds):
for count in ([300, 600, 1200] if group == 'scaling' else [2400]):
checkpoint = ('paper.pt' if group == 'initial' else
f'rebuttal_scale_e{count:04d}_seed_{seed:04d}.pt' if group == 'scaling' else
f'rebuttal_{group}_seed_{seed:04d}.pt')
train_windows = cfg['training']['train_windows'] * count // 2400
job = dict(group=group, method=method, seed=seed, episodes=count, checkpoint=checkpoint)
cmd = [sys.executable, '-m', 'experiments.train_image_world_models', '--methods', method,
'--train-source', str(root / cfg['data']['train_source']), '--test-source', str(root / cfg['data']['test_source']),
'--train-episodes', str(64 if args.smoke else count), '--test-episodes', str(64 if args.smoke else 480),
'--history-len', '32', '--horizon', '60', '--train-windows', str(512 if args.smoke else train_windows),
'--test-windows', str(64 if args.smoke else 24576), '--batch-size', '256',
'--microbatch-size', '64', '--eval-batch-size', '64',
'--steps', '1' if args.smoke else '20000', '--lr', '0.0003', '--log-every', '1' if args.smoke else '200',
'--seed', str(seed), '--seed-mode', 'method_offset' if group == 'initial' else 'paired',
'--device', args.device, '--num-workers', '4', '--image-size', '160', '--visual-scale', '2.5',
'--render-mode', 'device', '--precision', 'bf16', '--target-mode', 'absolute_normalized',
'--heading-weight', '2.0', '--current-pose-weight', '1.0', '--motion-weight', '0.5',
'--checkpoint-name', checkpoint, '--checkpoint-interval', '0']
if group == 'ood':
families = [f for f in cfg['flow_families'] if f not in cfg['heldout_flow_families']]
cmd += ['--train-flow-families', *families, '--test-flow-families', *cfg['heldout_flow_families']]
if group == 'initial':
for flag in ['--microbatch-size', '--eval-batch-size', '--seed-mode']:
index = cmd.index(flag)
del cmd[index:index + 2]
job['source_tree'] = 'protocols/initial/src' if group == 'initial' else '.'
jobs.append(dict(job, command=cmd))
if not jobs:
raise ValueError('No configurations selected')
if args.dry_run:
print(json.dumps({'training_jobs': len(jobs), 'smoke': args.smoke, 'jobs': jobs}, indent=2))
return
out.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env['PYTHONPATH'] = str(root) + os.pathsep + env.get('PYTHONPATH', '')
env['OMP_NUM_THREADS'] = '1'
env['OPENBLAS_NUM_THREADS'] = '1'
env['PYTHONPYCACHEPREFIX'] = str(out / 'pycache')
(out / 'training_plan.json').write_text(json.dumps({'smoke': args.smoke, 'jobs': jobs}, indent=2))
records = []
for job in jobs:
env['PYTHONPATH'] = str(root / job['source_tree']) + os.pathsep + str(root)
endpoint = out / 'experiments' / job['method'] / 'checkpoint' / job['checkpoint']
if endpoint.exists():
raise FileExistsError(f'Checkpoint already exists: {endpoint}; choose another run directory')
started = time.monotonic()
log = out / f'{job["group"]}_{job["method"]}_{job["seed"]}_{job["episodes"]}.log'
with log.open('w') as f:
subprocess.run(job['command'], cwd=out, env=env, stdout=f, stderr=subprocess.STDOUT, check=True)
assert endpoint.is_file()
records.append({k: v for k, v in job.items() if k != 'command'} | {'elapsed_seconds': time.monotonic() - started})
(out / 'training_receipt.json').write_text(json.dumps({'smoke': args.smoke, 'completed': len(records), 'planned': len(jobs), 'jobs': records}, indent=2))
print(json.dumps({'completed': len(records), 'planned': len(jobs), 'configuration': records[-1]}), flush=True)
if __name__ == '__main__':
main()