mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-02 01:47:43 +00:00
feat: Add parallel run system with API key distribution
- Add predix_parallel.py: Run multiple factor experiments concurrently
* python predix_parallel.py --runs 5 --api-keys 2 -m openrouter
* Round-robin API key distribution across available keys
* Rich live dashboard with per-run status, elapsed time, exit codes
* Graceful shutdown (Ctrl+C kills all children cleanly)
- Add --run-id parameter to predix.py for isolated single runs
* Separate log files: fin_quant_run{N}.log
* Separate results: results/runs/run{N}/
* Separate workspace: RD-Agent_workspace_run{N}/
* Separate databases per run
- Modify CoSTEER and FactorRunner for PARALLEL_RUN_ID isolation
* _save_intermediate_results uses run-specific directories
* _save_result_to_database and _write_run_log isolated per run
* _ensure_results_dirs creates run-specific paths
- Reduce max_loop from 10 to 3 for faster iterations
- Add docs/parallel_runs.md with full documentation
Tests: 103 passed
This commit is contained in:
@@ -114,7 +114,13 @@ class CoSTEER(Developer[Experiment]):
|
||||
reached_max_seconds = False
|
||||
|
||||
evo_fb = None
|
||||
iteration_count = 0
|
||||
|
||||
# Save initial state before first iteration
|
||||
self._save_intermediate_results(evo_exp, None, 0, start_datetime)
|
||||
|
||||
for evo_exp in self.evolve_agent.multistep_evolve(evo_exp, self.evaluator):
|
||||
iteration_count += 1
|
||||
assert isinstance(evo_exp, Experiment) # multiple inheritance
|
||||
evo_fb = self._get_last_fb()
|
||||
update_fallback = self.should_use_new_evo(
|
||||
@@ -129,6 +135,10 @@ class CoSTEER(Developer[Experiment]):
|
||||
logger.log_object(evo_exp.sub_workspace_list, tag="evolving code")
|
||||
for sw in evo_exp.sub_workspace_list:
|
||||
logger.info(f"evolving workspace: {sw}")
|
||||
|
||||
# Save intermediate results after each iteration
|
||||
self._save_intermediate_results(evo_exp, evo_fb, iteration_count, start_datetime)
|
||||
|
||||
if max_seconds is not None and (datetime.now() - start_datetime).total_seconds() > max_seconds:
|
||||
logger.info(f"Reached max time limit {max_seconds} seconds, stop evolving")
|
||||
reached_max_seconds = True
|
||||
@@ -154,6 +164,100 @@ class CoSTEER(Developer[Experiment]):
|
||||
exp.experiment_workspace = evo_exp.experiment_workspace
|
||||
return exp
|
||||
|
||||
def _save_intermediate_results(self, evo_exp, evo_fb, iteration: int, start_datetime) -> None:
|
||||
"""
|
||||
Save intermediate CoSTEER results to results/ directory after each iteration.
|
||||
|
||||
This ensures results are visible even if CoSTEER takes a long time
|
||||
or ultimately fails.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
evo_exp : EvolvingItem
|
||||
Current evolving experiment
|
||||
evo_fb : CoSTEERMultiFeedback
|
||||
Feedback from the evaluator
|
||||
iteration : int
|
||||
Current iteration number
|
||||
start_datetime : datetime
|
||||
When the develop process started
|
||||
"""
|
||||
import json as _json
|
||||
import os as _os
|
||||
from datetime import datetime as _dt
|
||||
|
||||
try:
|
||||
# Go up from rdagent/components/coder/CoSTEER/ to project root (5 levels)
|
||||
project_root = Path(__file__).parent.parent.parent.parent.parent
|
||||
|
||||
# Parallel run isolation: use run-specific directory if PARALLEL_RUN_ID is set
|
||||
parallel_run_id = _os.getenv("PARALLEL_RUN_ID", "0")
|
||||
if parallel_run_id != "0":
|
||||
results_dir = project_root / "results" / "runs" / f"run{parallel_run_id}" / "costeer"
|
||||
else:
|
||||
results_dir = project_root / "results" / "runs"
|
||||
results_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build summary
|
||||
summary = {
|
||||
"timestamp": _dt.now().isoformat(),
|
||||
"iteration": iteration,
|
||||
"elapsed_seconds": (_dt.now() - start_datetime).total_seconds(),
|
||||
"factors": [],
|
||||
}
|
||||
|
||||
# Extract factor info from sub_workspace_list
|
||||
if hasattr(evo_exp, "sub_workspace_list") and evo_exp.sub_workspace_list:
|
||||
for i, sw in enumerate(evo_exp.sub_workspace_list):
|
||||
factor = {"index": i, "file_count": 0, "code_preview": None}
|
||||
if hasattr(sw, "file_dict") and sw.file_dict:
|
||||
factor["file_count"] = len(sw.file_dict)
|
||||
code = sw.file_dict.get("factor.py", "")
|
||||
if code:
|
||||
# First 200 chars as preview
|
||||
factor["code_preview"] = code[:200]
|
||||
summary["factors"].append(factor)
|
||||
|
||||
# Extract feedback info
|
||||
if evo_fb is not None:
|
||||
summary["feedback_count"] = len(evo_fb) if hasattr(evo_fb, "__len__") else 0
|
||||
accepted = 0
|
||||
rejected = 0
|
||||
for fb in evo_fb:
|
||||
if fb is not None:
|
||||
if fb.is_acceptable():
|
||||
accepted += 1
|
||||
else:
|
||||
rejected += 1
|
||||
summary["accepted"] = accepted
|
||||
summary["rejected"] = rejected
|
||||
summary["status"] = "accepted" if accepted > 0 else "rejected"
|
||||
else:
|
||||
summary["feedback_count"] = 0
|
||||
summary["accepted"] = 0
|
||||
summary["rejected"] = 0
|
||||
summary["status"] = "initialized"
|
||||
|
||||
# Write JSON file
|
||||
ts = _dt.now().strftime("%Y%m%d_%H%M%S")
|
||||
if parallel_run_id != "0":
|
||||
json_path = results_dir / f"costeer_run{parallel_run_id}_iter{iteration:02d}_{ts}.json"
|
||||
else:
|
||||
json_path = results_dir / f"costeer_iter{iteration:02d}_{ts}.json"
|
||||
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
_json.dump(summary, f, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
logger.info(
|
||||
f"CoSTEER iteration {iteration}: "
|
||||
f"accepted={summary.get('accepted', 0)}, "
|
||||
f"rejected={summary.get('rejected', 0)}, "
|
||||
f"saved to {json_path.name}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save intermediate CoSTEER results: {e}")
|
||||
|
||||
def _exp_postprocess_by_feedback(self, evo: Experiment, feedback: CoSTEERMultiFeedback) -> Experiment:
|
||||
"""
|
||||
Responsibility:
|
||||
|
||||
@@ -12,8 +12,8 @@ class CoSTEERSettings(ExtendedBaseSettings):
|
||||
coder_use_cache: bool = False
|
||||
"""Indicates whether to use cache for the coder"""
|
||||
|
||||
max_loop: int = 10
|
||||
"""Maximum number of task implementation loops"""
|
||||
max_loop: int = 3
|
||||
"""Maximum number of task implementation loops (reduced from 10 for faster iterations)"""
|
||||
|
||||
fail_task_trial_limit: int = 20
|
||||
|
||||
|
||||
Reference in New Issue
Block a user