| """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 |
|
|
| |
| 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) |
| |
| 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() |
|
|
| |
| cfg = load_config(args.config, args.model) |
| cfg = apply_overrides(cfg, args.override or [], args.algorithm, args.seed) |
|
|
| |
| from poisonclaw.utils.seed import set_seed |
| from omegaconf import OmegaConf |
| seed = OmegaConf.select(cfg, "seed", default=42) |
| set_seed(int(seed)) |
|
|
| |
| output_dir = setup_output_dir(cfg, args.output_dir) |
| logger.info("Output directory: %s", output_dir) |
|
|
| |
| 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 |
|
|
| |
| setup_wandb(cfg, output_dir) |
|
|
| |
| env_manager = build_env_manager(cfg) |
| logger.info("Environment manager created: %s", type(env_manager).__name__) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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() |
|
|