Skip to content

Commit 2e7f739

Browse files
committed
fix: Add worker crash recovery to ProcessParallelController
1 parent 65cbbe8 commit 2e7f739

1 file changed

Lines changed: 81 additions & 5 deletions

File tree

openevolve/process_parallel.py

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
import asyncio
66
import logging
77
import multiprocessing as mp
8-
import pickle
9-
import signal
108
import time
11-
from concurrent.futures import Future, ProcessPoolExecutor
9+
from concurrent.futures import BrokenExecutor, Future, ProcessPoolExecutor
1210
from concurrent.futures import TimeoutError as FutureTimeoutError
1311
from dataclasses import asdict, dataclass
14-
from pathlib import Path
15-
from typing import Any, Dict, List, Optional, Tuple
12+
from typing import Any, Dict, List, Optional
1613

1714
from openevolve.config import Config
1815
from openevolve.database import Program, ProgramDatabase
@@ -357,6 +354,10 @@ def __init__(
357354
self.num_workers = config.evaluator.parallel_evaluations
358355
self.num_islands = config.database.num_islands
359356

357+
# Recovery tracking for process pool crashes
358+
self.recovery_attempts = 0
359+
self.max_recovery_attempts = 3
360+
360361
logger.info(f"Initialized process parallel controller with {self.num_workers} workers")
361362

362363
def _serialize_config(self, config: Config) -> dict:
@@ -434,6 +435,38 @@ def stop(self) -> None:
434435

435436
logger.info("Stopped process pool")
436437

438+
def _recover_process_pool(self, failed_iterations: list[int] | None = None) -> None:
439+
"""Recover from a crashed process pool by recreating it.
440+
441+
Args:
442+
failed_iterations: List of iteration numbers that failed and need re-queuing
443+
"""
444+
import gc
445+
446+
logger.warning("Process pool crashed, attempting recovery...")
447+
448+
# Shutdown broken executor without waiting (it's already broken)
449+
if self.executor:
450+
try:
451+
self.executor.shutdown(wait=False, cancel_futures=True)
452+
except Exception:
453+
pass # Executor may already be in bad state
454+
self.executor = None
455+
456+
# Force garbage collection to free memory before restarting
457+
gc.collect()
458+
459+
# Brief delay to let system stabilize (memory freed, processes cleaned up)
460+
time.sleep(2.0)
461+
462+
# Recreate the pool
463+
self.start()
464+
465+
if failed_iterations:
466+
logger.info(f"Pool recovered. {len(failed_iterations)} iterations will be re-queued.")
467+
else:
468+
logger.info("Pool recovered successfully.")
469+
437470
def request_shutdown(self) -> None:
438471
"""Request graceful shutdown"""
439472
logger.info("Graceful shutdown requested...")
@@ -559,6 +592,14 @@ async def run_evolution(
559592
# Reconstruct program from dict
560593
child_program = Program(**result.child_program_dict)
561594

595+
# Reset recovery counter on successful iteration
596+
if self.recovery_attempts > 0:
597+
logger.info(
598+
f"Pool stable after recovery, resetting recovery counter "
599+
f"(was {self.recovery_attempts})"
600+
)
601+
self.recovery_attempts = 0
602+
562603
# Add to database with explicit target_island to ensure proper island placement
563604
# This fixes issue #391: children should go to the target island, not inherit
564605
# from the parent (which may be from a different island due to fallback sampling)
@@ -752,6 +793,38 @@ async def run_evolution(
752793
)
753794
# Cancel the future to clean up the process
754795
future.cancel()
796+
except BrokenExecutor as e:
797+
logger.error(f"Process pool crashed during iteration {completed_iteration}: {e}")
798+
799+
# Collect all failed iterations from pending futures
800+
failed_iterations = [completed_iteration] + list(pending_futures.keys())
801+
802+
# Clear pending futures (they're all invalid now)
803+
pending_futures.clear()
804+
for island_id in island_pending:
805+
island_pending[island_id].clear()
806+
807+
# Attempt recovery
808+
self.recovery_attempts += 1
809+
if self.recovery_attempts > self.max_recovery_attempts:
810+
logger.error(
811+
f"Max recovery attempts ({self.max_recovery_attempts}) exceeded. "
812+
f"Stopping evolution."
813+
)
814+
break
815+
816+
self._recover_process_pool(failed_iterations)
817+
818+
# Re-queue failed iterations (distribute across islands)
819+
for i, failed_iter in enumerate(failed_iterations):
820+
if failed_iter < total_iterations:
821+
island_id = i % self.num_islands
822+
future = self._submit_iteration(failed_iter, island_id)
823+
if future:
824+
pending_futures[failed_iter] = future
825+
island_pending[island_id].append(failed_iter)
826+
827+
continue
755828
except Exception as e:
756829
logger.error(f"Error processing result from iteration {completed_iteration}: {e}")
757830

@@ -822,6 +895,9 @@ def _submit_iteration(
822895

823896
return future
824897

898+
except BrokenExecutor:
899+
# Let this propagate up to run_evolution for recovery
900+
raise
825901
except Exception as e:
826902
logger.error(f"Error submitting iteration {iteration}: {e}")
827903
return None

0 commit comments

Comments
 (0)