verl-agent / scripts /train.py
zhangzf01
hf run successfully
0eca069
Raw
History Blame Contribute Delete
8.84 kB
"""PoisonClaw unified training entry point.
Integrates PoisonClaw environments and memory modules with the
verl-agent RL training pipeline.
Usage:
# Phase 1: VisualWebArena quick validation (2B + GRPO)
python scripts/train.py \\
--config configs/experiment/main_attack.yaml \\
--algorithm grpo \\
--seed 42
# Ablation: vary friction gap
python scripts/train.py \\
--config configs/experiment/ablation_friction.yaml \\
--override attack.friction_gap=5 \\
--seed 42
# 7B model (reduce num_envs due to memory)
python scripts/train.py \\
--config configs/experiment/main_attack.yaml \\
--model configs/model/qwen2vl_7b.yaml \\
--algorithm grpo \\
--override env.rollout.num_envs=16 \\
--seed 42
# Resume from checkpoint (after Nautilus pod preemption)
python scripts/train.py \\
--config configs/experiment/main_attack.yaml \\
--resume_from outputs/main_attack/checkpoint_5000.pt
"""
import argparse
import logging
import os
import sys
# Ensure project root on path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
)
logger = logging.getLogger("poisonclaw.train")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="PoisonClaw IRFA training script",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--config", required=True, help="Path to experiment YAML config")
parser.add_argument("--model", default=None, help="Optional model YAML to merge")
parser.add_argument(
"--algorithm",
default=None,
choices=["grpo", "ppo", "gigpo", "reinforce++", "rloo", "dapo"],
help="RL algorithm override",
)
parser.add_argument("--seed", type=int, default=None, help="Random seed override")
parser.add_argument(
"--override",
nargs="*",
metavar="KEY=VALUE",
default=[],
help="Dot-notation config overrides, e.g. attack.friction_gap=5",
)
parser.add_argument(
"--resume_from",
default=None,
help="Path to checkpoint to resume from",
)
parser.add_argument(
"--output_dir",
default=None,
help="Override output directory",
)
parser.add_argument(
"--dry_run",
action="store_true",
help="Validate config and environment setup without training",
)
return parser.parse_args()
def load_config(config_path: str, model_path: str | None) -> dict:
"""Load and merge YAML configs using OmegaConf.
Args:
config_path: Path to main experiment config.
model_path: Optional path to model config to merge.
Returns:
Merged OmegaConf DictConfig.
"""
try:
from omegaconf import OmegaConf
except ImportError:
raise ImportError("omegaconf is required. Install with: pip install omegaconf")
cfg = OmegaConf.load(config_path)
if model_path:
model_cfg = OmegaConf.load(model_path)
cfg = OmegaConf.merge(cfg, model_cfg)
return cfg
def apply_overrides(cfg, overrides: list[str], algorithm: str | None, seed: int | None):
"""Apply CLI overrides to the config.
Args:
cfg: OmegaConf DictConfig.
overrides: List of ``"key=value"`` strings.
algorithm: RL algorithm override.
seed: Random seed override.
Returns:
Updated config.
"""
from omegaconf import OmegaConf
for override in overrides:
if "=" not in override:
logger.warning("Skipping malformed override '%s' (no '=')", override)
continue
key, value = override.split("=", 1)
# Try to parse value as int/float/bool
for parser in (int, float):
try:
value = parser(value)
break
except (ValueError, TypeError):
pass
if isinstance(value, str) and value.lower() in ("true", "false"):
value = value.lower() == "true"
OmegaConf.update(cfg, key, value)
if algorithm is not None:
OmegaConf.update(cfg, "trainer.algorithm", algorithm)
if seed is not None:
OmegaConf.update(cfg, "seed", seed)
return cfg
def setup_output_dir(cfg, output_dir_override: str | None) -> str:
"""Set up the output directory with config-derived naming.
Args:
cfg: Config object.
output_dir_override: CLI override for output dir.
Returns:
Final output directory path.
"""
from omegaconf import OmegaConf
base = output_dir_override or OmegaConf.select(cfg, "output_dir", default="outputs/run")
model_name = OmegaConf.select(cfg, "model.actor_lm.model_name", default="unknown")
model_short = model_name.split("/")[-1].lower()
algorithm = OmegaConf.select(cfg, "trainer.algorithm", default="grpo")
seed = OmegaConf.select(cfg, "seed", default=42)
output_dir = os.path.join(base, model_short, algorithm, f"seed{seed}")
os.makedirs(output_dir, exist_ok=True)
return output_dir
def setup_wandb(cfg, output_dir: str) -> None:
"""Initialize wandb if available and configured.
Args:
cfg: Config object.
output_dir: Run output directory (used as wandb dir).
"""
try:
import wandb
from omegaconf import OmegaConf
project = OmegaConf.select(cfg, "logging.wandb_project", default="poisonclaw")
group = OmegaConf.select(cfg, "logging.wandb_group", default="default")
wandb.init(
project=project,
group=group,
dir=output_dir,
config=OmegaConf.to_container(cfg, resolve=True),
)
logger.info("wandb initialized: project=%s group=%s", project, group)
except ImportError:
logger.warning("wandb not installed; skipping experiment tracking.")
except Exception as exc:
logger.warning("wandb init failed: %s", exc)
def build_env_manager(cfg):
"""Instantiate the environment manager from config.
Args:
cfg: Config object.
Returns:
An environment manager instance.
"""
from scripts.register_env import get_env_class
from omegaconf import OmegaConf
env_type = OmegaConf.select(cfg, "env.type", default="poisonclaw-visualwebarena")
env_cls = get_env_class(env_type)
return env_cls(config=cfg, split="train")
def main() -> None:
args = parse_args()
# Load and merge configs
cfg = load_config(args.config, args.model)
cfg = apply_overrides(cfg, args.override or [], args.algorithm, args.seed)
# Set global seed
from poisonclaw.utils.seed import set_seed
from omegaconf import OmegaConf
seed = OmegaConf.select(cfg, "seed", default=42)
set_seed(int(seed))
# Setup output directory
output_dir = setup_output_dir(cfg, args.output_dir)
logger.info("Output directory: %s", output_dir)
# Save resolved config alongside checkpoint
from omegaconf import OmegaConf
config_dump = os.path.join(output_dir, "resolved_config.yaml")
OmegaConf.save(cfg, config_dump)
logger.info("Resolved config saved to %s", config_dump)
if args.dry_run:
logger.info("Dry run complete — config and environment validation passed.")
return
# Initialize wandb
setup_wandb(cfg, output_dir)
# Build environment
env_manager = build_env_manager(cfg)
logger.info("Environment manager created: %s", type(env_manager).__name__)
# === Training loop placeholder ===
# In a full integration, we would pass env_manager to the verl-agent
# recipe trainer (e.g. grpo/trainer.py). The integration point is:
#
# from recipe.grpo.trainer import GRPOTrainer
# trainer = GRPOTrainer(config=cfg, env_manager=env_manager)
# if args.resume_from:
# trainer.load_checkpoint(args.resume_from)
# trainer.train()
#
# Until the verl-agent recipe interface is finalized, this script
# provides the setup plumbing. See CLAUDE.md for integration guide.
num_steps = OmegaConf.select(cfg, "trainer.num_train_steps", default=10000)
algorithm = OmegaConf.select(cfg, "trainer.algorithm", default="grpo")
logger.info(
"Training: algorithm=%s steps=%d output=%s",
algorithm,
num_steps,
output_dir,
)
if args.resume_from:
logger.info("Resuming from checkpoint: %s", args.resume_from)
logger.info(
"Training setup complete. "
"Integrate with verl-agent recipe trainer to start RL training."
)
env_manager.close()
if __name__ == "__main__":
main()