tanvirnwu's picture
Upload folder using huggingface_hub (part 7)
0ec2e87 verified
Raw
History Blame Contribute Delete
16.2 kB
"""
Main training script using dataclass configuration instead of Hydra.
"""
import os
import sys
import logging
import random
import shutil
import datetime
import yaml
import numpy as np
import torch
import wandb
from wandb.errors import CommError, UsageError
import multiprocessing as mp
# Set multiprocessing start method to 'spawn' to avoid CUDA issues
try:
mp.set_start_method('spawn', force=True)
except RuntimeError:
# Already set, ignore
pass
os.environ['NUMEXPR_MAX_THREADS'] = '64'
# Add the current directory to Python path
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, REPO_ROOT)
# Prefer the bundled LIBERO-PRO package over any globally installed libero package.
# We want imports like `from libero.libero import ...` to resolve to:
# <repo>/LIBERO-PRO/libero/libero/...
LIBERO_PRO_PYTHON_ROOT = os.path.join(REPO_ROOT, "LIBERO-PRO")
if os.path.isdir(LIBERO_PRO_PYTHON_ROOT) and LIBERO_PRO_PYTHON_ROOT not in sys.path:
sys.path.insert(1, LIBERO_PRO_PYTHON_ROOT)
from configs.config import (
create_libero_train_config,
create_libero_pro_eval_config,
)
from configs.factory import create_model, create_trainer, create_simulation
from dataloader.video_paths import eval_video_root
# Set up logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
def set_seed_everywhere(seed):
"""Set random seed for reproducibility."""
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
def _safe_dirname(value: str | None, fallback: str) -> str:
"""Return a filesystem-safe directory name."""
name = (value or "").strip()
if not name:
name = fallback
return name.replace(os.sep, "_").replace(" ", "_")
def _ensure_unique_run_dir(base_dir: str) -> str:
"""Ensure the run directory is unique by appending a counter if needed."""
if not os.path.exists(base_dir):
return base_dir
counter = 1
while True:
candidate = f"{base_dir}_{counter:02d}"
if not os.path.exists(candidate):
return candidate
counter += 1
def _build_training_run_dir(cfg) -> tuple[str, str, str]:
project_name = _safe_dirname(cfg.wandb.project, "default_project")
mode_name = _safe_dirname(cfg.wandb.mode, "default_mode")
run_id = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
output_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
run_dir = os.path.join(output_root, project_name, mode_name, run_id)
run_dir = _ensure_unique_run_dir(run_dir)
run_id = os.path.basename(run_dir)
return run_dir, project_name, mode_name
def _resolve_run_dir_from_checkpoint(checkpoint_path: str) -> str:
if os.path.isfile(checkpoint_path):
checkpoint_dir = os.path.dirname(checkpoint_path)
else:
checkpoint_dir = checkpoint_path
if os.path.basename(os.path.normpath(checkpoint_dir)) == "checkpoints":
return os.path.dirname(checkpoint_dir)
return checkpoint_dir
def _extract_project_mode(run_dir: str) -> tuple[str | None, str | None]:
parts = os.path.normpath(run_dir).split(os.sep)
if "logs" in parts:
idx = parts.index("logs")
if len(parts) >= idx + 3:
return parts[idx + 1], parts[idx + 2]
return None, None
def _ensure_run_subdirs(run_dir: str) -> dict[str, str]:
subdirs = {
"checkpoints": os.path.join(run_dir, "checkpoints"),
"wandb": os.path.join(run_dir, "wandb"),
"logs": os.path.join(run_dir, "logs"),
}
for path in subdirs.values():
os.makedirs(path, exist_ok=True)
return subdirs
def _copy_config_file(run_dir: str) -> None:
config_src = os.path.join(os.path.dirname(os.path.abspath(__file__)), "configs", "config.py")
config_dest = os.path.join(run_dir, "config.py")
shutil.copyfile(config_src, config_dest)
def _resolve_wandb_mode(wandb_cfg) -> str | None:
valid_modes = {"dryrun", "online", "run", "offline", "disabled"}
env_mode = (os.getenv("WANDB_MODE") or "").strip()
if env_mode:
if env_mode in valid_modes:
return env_mode
log.warning(
"Ignoring unsupported WANDB_MODE '%s'. Expected one of %s.",
env_mode,
sorted(valid_modes),
)
return None
cfg_mode = (wandb_cfg.mode or "").strip()
if not cfg_mode:
return None
if cfg_mode not in valid_modes:
log.warning(
"Ignoring unsupported W&B mode '%s'. Expected one of %s.",
cfg_mode,
sorted(valid_modes),
)
return None
return cfg_mode
def _merge_wandb_tags(existing_tags, required_tags: list[str]) -> list[str]:
merged: list[str] = []
def _add(tag) -> None:
# W&B rejects empty/whitespace tags (must be 1-64 chars), so skip them.
if tag is None:
return
tag = str(tag).strip()
if tag and tag not in merged:
merged.append(tag)
if existing_tags:
if isinstance(existing_tags, (list, tuple, set)):
for tag in existing_tags:
_add(tag)
else:
_add(existing_tags)
for tag in required_tags:
_add(tag)
return merged
def init_wandb_logging(
cfg,
wandb_config,
*,
run_name: str,
wandb_dir: str,
group: str | None,
job_type: str,
tags: list[str],
):
"""Initialise Weights & Biases logging if enabled in the config."""
wandb_cfg = cfg.wandb
if not getattr(wandb_cfg, "enabled", True):
log.info("W&B logging disabled via configuration; skipping initialisation.")
return None
project = wandb_cfg.project
entity = os.getenv("WANDB_ENTITY", wandb_cfg.entity)
mode = _resolve_wandb_mode(wandb_cfg) or ""
if mode:
os.environ["WANDB_MODE"] = mode
if project is None:
log.info("No W&B project specified; skipping remote logging.")
return None
os.makedirs(wandb_dir, exist_ok=True)
init_kwargs = {
"project": project,
"config": wandb_config,
"name": run_name,
"dir": wandb_dir,
"job_type": job_type,
}
if group:
init_kwargs["group"] = group
if entity:
init_kwargs["entity"] = entity
merged_tags = _merge_wandb_tags(getattr(wandb_cfg, "tags", None), tags)
if merged_tags:
init_kwargs["tags"] = merged_tags
try:
return wandb.init(**init_kwargs)
except (CommError, UsageError) as err:
log.warning("W&B initialisation failed (%s); continuing without remote logging.", err)
except Exception as err: # noqa: BLE001 - broad catch to keep training running
log.warning("Unexpected error during W&B initialisation (%s). Continuing without remote logging.", err)
return None
def _configure_libero_pro_paths(repo_root: str) -> None:
"""Point LIBERO path resolution to the bundled LIBERO-PRO assets."""
libero_root = os.path.join(repo_root, "LIBERO-PRO", "libero", "libero")
if not os.path.isdir(libero_root):
log.warning("LIBERO-PRO root not found at %s; using existing LIBERO path configuration.", libero_root)
return
config_root = os.path.join(repo_root, ".libero")
os.makedirs(config_root, exist_ok=True)
os.environ["LIBERO_CONFIG_PATH"] = config_root
path_config = {
"benchmark_root": libero_root,
"bddl_files": os.path.join(libero_root, "bddl_files"),
"init_states": os.path.join(libero_root, "init_files"),
"datasets": os.path.join(libero_root, "..", "datasets"),
"assets": os.path.join(libero_root, "assets"),
}
config_file = os.path.join(config_root, "config.yaml")
with open(config_file, "w", encoding="utf-8") as f:
yaml.safe_dump(path_config, f)
log.info(
"Configured LIBERO paths for LIBERO-PRO eval (bddl=%s, init=%s).",
path_config["bddl_files"],
path_config["init_states"],
)
def _clear_local_libero_pro_override(repo_root: str) -> None:
"""Clear repo-local LIBERO-PRO path override so vanilla LIBERO config is used."""
config_root = os.path.join(repo_root, ".libero")
configured_path = os.environ.get("LIBERO_CONFIG_PATH")
if configured_path and os.path.abspath(configured_path) == os.path.abspath(config_root):
os.environ.pop("LIBERO_CONFIG_PATH", None)
log.info("Cleared repo-local LIBERO_CONFIG_PATH override to use vanilla LIBERO paths.")
def main(train_suite: str = "libero_object", eval_suite: str | None = None, checkpoint_path: str | None = None) -> None:
"""
Main training function.
Args:
train_suite: The train task suite to use ('libero_object', 'libero_spatial', 'libero_goal', 'libero_90', 'libero_10')
eval_suite: Optional LIBERO PRO eval suffix ('swap', 'object', 'lan', 'task', 'temp')
"""
repo_root = os.path.dirname(os.path.abspath(__file__))
if eval_suite is None:
_clear_local_libero_pro_override(repo_root)
cfg = create_libero_train_config(train_suite)
else:
cfg = create_libero_pro_eval_config(train_suite, eval_suite)
_configure_libero_pro_paths(repo_root)
set_seed_everywhere(cfg.seed)
# Initialize wandb logger
wandb_config = {
"project": cfg.wandb.project,
"entity": cfg.wandb.entity,
"group": cfg.wandb.mode or cfg.group,
"seed": cfg.seed,
"benchmark_type": cfg.dataset.benchmark_type,
"demos_per_task": cfg.dataset.demos_per_task,
"chunck_size": cfg.chunck_size,
"perception_seq_len": cfg.perception_seq_len,
"action_seq_len": cfg.action_seq_len,
"train_batch_size": cfg.train_batch_size,
"epoch": cfg.epoch,
"device": cfg.device,
"len_embd": cfg.len_embd,
"latent_dim": cfg.latent_dim,
"action_dim": cfg.action_dim,
"state_dim": cfg.state_dim,
}
is_evaluation = checkpoint_path is not None
if is_evaluation:
run_dir = _resolve_run_dir_from_checkpoint(checkpoint_path)
run_id = os.path.basename(os.path.normpath(run_dir))
extracted_project, extracted_mode = _extract_project_mode(run_dir)
project_name = extracted_project or cfg.wandb.project
mode_name = cfg.wandb.mode or extracted_mode
evaluation_dir = os.path.join(run_dir, "evaluation")
os.makedirs(evaluation_dir, exist_ok=True)
wandb_dir = os.path.join(evaluation_dir, "wandb")
logs_dir = os.path.join(evaluation_dir, "logs")
os.makedirs(wandb_dir, exist_ok=True)
os.makedirs(logs_dir, exist_ok=True)
else:
run_dir, project_name, mode_name = _build_training_run_dir(cfg)
subdirs = _ensure_run_subdirs(run_dir)
wandb_dir = subdirs["wandb"]
_copy_config_file(run_dir)
run_id = os.path.basename(run_dir)
if is_evaluation:
wandb_run_name = "evaluation"
wandb_group = mode_name
wandb_job_type = "eval"
wandb_tags = ["evaluation", run_id]
else:
wandb_run_name = cfg.wandb.mode
wandb_group = cfg.wandb.mode
wandb_job_type = "train"
wandb_tags = [cfg.wandb.mode, run_id]
visuals_dir = os.path.join(run_dir, "visuals")
visuals_training_dir = os.path.join(visuals_dir, "training")
visuals_testing_dir = os.path.join(visuals_dir, "testing")
os.makedirs(visuals_training_dir, exist_ok=True)
os.makedirs(visuals_testing_dir, exist_ok=True)
run = init_wandb_logging(
cfg,
wandb_config,
run_name=wandb_run_name,
wandb_dir=wandb_dir,
group=wandb_group,
job_type=wandb_job_type,
tags=wandb_tags,
)
checkpoints_dir = os.path.join(run_dir, "checkpoints")
cfg.simulation.save_video_dir = eval_video_root(checkpoint_path, checkpoints_dir)
if cfg.simulation.save_video:
log.info("Evaluation videos will be saved under %s", cfg.simulation.save_video_dir)
# Create model and set its working_dir to the run-specific directory
model = create_model(cfg)
model.working_dir = checkpoints_dir
# Create trainer and set its working_dir as well
trainer = create_trainer(cfg)
trainer.working_dir = checkpoints_dir
trainer.configure_visuals_dir(visuals_training_dir)
# Get model parameters for logging
model.get_params()
# If a checkpoint is provided, load it and skip training
if checkpoint_path is not None:
# Set scaler from trainer to ensure inference works
model.set_scaler(trainer.scaler)
# Resolve checkpoint: file path or directory
if os.path.isfile(checkpoint_path):
state_dict = torch.load(checkpoint_path, weights_only=True)
missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False)
if missing_keys or unexpected_keys:
log.warning(
"Checkpoint load had mismatched keys (missing=%s, unexpected=%s).",
missing_keys,
unexpected_keys,
)
log.info(f"Loaded checkpoint from file: {checkpoint_path}")
elif os.path.isdir(checkpoint_path):
candidates = [
os.path.join(checkpoint_path, "final_model.pth"),
os.path.join(checkpoint_path, "model_state_dict.pth"),
]
loaded = False
for cand in candidates:
if os.path.isfile(cand):
state_dict = torch.load(cand, weights_only=True)
missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False)
if missing_keys or unexpected_keys:
log.warning(
"Checkpoint load had mismatched keys (missing=%s, unexpected=%s).",
missing_keys,
unexpected_keys,
)
log.info(f"Loaded checkpoint from directory: {cand}")
loaded = True
break
if not loaded:
raise FileNotFoundError(f"No checkpoint file found in {checkpoint_path} (looked for final_model.pth, model_state_dict.pth)")
else:
# Train the model if no checkpoint provided
trainer.main(model)
# Create simulation environment
env_sim = create_simulation(cfg)
env_sim.configure_visuals(cfg.visuals, visuals_testing_dir)
# Test the model
env_sim.test_model(model, cfg.model_cfg, epoch=cfg.epoch)
log.info("Training done")
log.info("state_dict saved in {}".format(model.working_dir))
if run is not None:
wandb.finish()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Train SUREFlow model")
parser.add_argument(
"--train_suite",
type=str,
default="libero_object",
choices=["libero_object", "libero_spatial", "libero_goal", "libero_90", "libero_10"],
help="Task suite to use for training dataset and training-language embeddings"
)
parser.add_argument(
"--eval_suite",
type=str,
default=None,
choices=["swap", "object", "lan", "task", "temp"],
help="Optional LIBERO PRO evaluation suffix; when set, simulation uses <train_suite>_<eval_suite>."
)
parser.add_argument(
"--checkpoint_path",
type=str,
default=None,
help="Path to checkpoint (.pth file or directory). If provided, skips training and evaluates with this checkpoint."
)
args = parser.parse_args()
main(args.train_suite, args.eval_suite, args.checkpoint_path)