"""Single LoRA-task run via any optimizer. Usage:: python -m ahdcma.cli.run_task \\ --algo ahdcma --task cifar100_vit --seed 0 \\ --pop 8 --gens 5 --num-steps 50 --output outputs/runs/lora/ Writes ``{output}/{run_id}/result.json`` with the best LoRA config, final fitness, history, and config snapshot. Each fitness call is a short LoRA fine-tune (``num_steps`` optimiser steps) — the wall-time per call dominates total runtime. """ from __future__ import annotations import argparse import json import time from pathlib import Path from typing import Any import numpy as np import yaml from ahdcma.algorithms.base import SearchSpace from ahdcma.cli.run_benchmark import ALGO_REGISTRY from ahdcma.fitness.lora_finetune import TaskConfig, lora_fitness from ahdcma.search_space.encoder import DIM, DIM_NAMES from ahdcma.utils.logging import make_run_id, setup_logging from ahdcma.utils.seed import set_global_seed def _load_task_config(task_name: str) -> TaskConfig: cfg_path = Path(__file__).resolve().parents[3] / "configs" / "tasks" / f"{task_name}.yaml" with cfg_path.open() as f: raw = yaml.safe_load(f) return TaskConfig(**raw) def _load_algo_config(algo: str, *, seed: int, pop: int, gens: int) -> dict[str, Any]: cfg_path = Path(__file__).resolve().parents[3] / "configs" / "algo" / f"{algo}.yaml" with cfg_path.open() as f: cfg = yaml.safe_load(f) cfg["seed"] = seed cfg["population_size"] = pop cfg["max_generations"] = gens return dict(cfg) def run_lora_task( algo: str, task_name: str, seed: int, *, pop: int = 8, gens: int = 10, num_steps: int = 100, output_dir: Path | str = "outputs/runs/lora", ) -> dict[str, Any]: """Run one (algo, task, seed) LoRA tuning job and write result.json.""" if algo not in ALGO_REGISTRY: raise KeyError(f"unknown algorithm {algo!r}") set_global_seed(seed) run_id = make_run_id(algo, task_name, seed) out_root = Path(output_dir) / run_id out_root.mkdir(parents=True, exist_ok=True) setup_logging(run_id, log_dir=out_root / "logs") task = _load_task_config(task_name) algo_cfg = _load_algo_config(algo, seed=seed, pop=pop, gens=gens) search_space = SearchSpace.unit_cube(DIM, names=DIM_NAMES) eval_log: list[dict[str, Any]] = [] def fitness(x: np.ndarray[Any, np.dtype[np.float64]]) -> float: f = lora_fitness(x, task, num_steps=num_steps, seed=seed) eval_log.append({"x": x.tolist(), "f": f}) return f cls = ALGO_REGISTRY[algo] opt = cls(algo_cfg, fitness, search_space, run_id=run_id) t0 = time.time() result = opt.optimize() wall = time.time() - t0 summary: dict[str, Any] = { "run_id": run_id, "algo": algo, "task": task_name, "seed": seed, "pop": pop, "gens": gens, "num_steps": num_steps, "best_f": float(result.best_f), "best_x": result.best_x.tolist(), "best_accuracy": float(-result.best_f), "wall_time": wall, "n_generations": len(result.history), "n_evals": len(eval_log), "config": algo_cfg, "best_fitness_curve": list(result.history.best_fitness), "mode_curve": list(result.history.mode_per_gen), } (out_root / "result.json").write_text(json.dumps(summary, indent=2)) (out_root / "evaluations.json").write_text(json.dumps(eval_log, indent=2)) return summary def main() -> None: p = argparse.ArgumentParser(description="Run a single LoRA tuning job.") p.add_argument("--algo", required=True, choices=sorted(ALGO_REGISTRY)) p.add_argument("--task", required=True) p.add_argument("--seed", type=int, default=0) p.add_argument("--pop", type=int, default=8) p.add_argument("--gens", type=int, default=10) p.add_argument("--num-steps", type=int, default=100) p.add_argument("--output", default="outputs/runs/lora") args = p.parse_args() summary = run_lora_task( args.algo, args.task, args.seed, pop=args.pop, gens=args.gens, num_steps=args.num_steps, output_dir=args.output, ) print( json.dumps( { "run_id": summary["run_id"], "best_accuracy": summary["best_accuracy"], "wall_time": summary["wall_time"], } ) ) if __name__ == "__main__": main()