Skip to content

Commit 80945ed

Browse files
codelionclaude
andauthored
Fix bugs (#442)
* Fix library API evaluators broken with process-based parallelism The callable evaluators in evolve_function and evolve_algorithm used closure-based functions stored in globals(), which don't survive across process boundaries. Since the switch to process-based parallelism (c2f668a), subprocess workers cannot access the parent process memory, causing "module has no attribute '_openevolve_evaluator_*'" errors. Fix by serializing evaluators as self-contained code strings instead of closures. Also adds combined_score to returned metrics to prevent the misleading score averaging warning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Bump version to 0.2.27 and add subprocess evaluator tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update api.py --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 65cbbe8 commit 80945ed

File tree

3 files changed

+242
-79
lines changed

3 files changed

+242
-79
lines changed

openevolve/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version information for openevolve package."""
22

3-
__version__ = "0.2.26"
3+
__version__ = "0.2.27"

openevolve/api.py

Lines changed: 129 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ async def _run_evolution_async(
141141
# Process evaluator
142142
evaluator_path = _prepare_evaluator(evaluator, temp_dir, temp_files)
143143

144+
# Auto-disable cascade evaluation if the evaluator doesn't define stage functions
145+
if config_obj.evaluator.cascade_evaluation:
146+
with open(evaluator_path, "r") as f:
147+
eval_content = f.read()
148+
if "evaluate_stage1" not in eval_content:
149+
config_obj.evaluator.cascade_evaluation = False
150+
144151
# Create and run controller
145152
controller = OpenEvolve(
146153
initial_program_path=program_path,
@@ -239,13 +246,40 @@ def _prepare_evaluator(
239246

240247
# If it's a callable, create a wrapper module
241248
if callable(evaluator):
242-
# Create a unique global name for this evaluator
243-
evaluator_id = f"_openevolve_evaluator_{uuid.uuid4().hex[:8]}"
249+
# Try to get the source code of the callable so it can be serialized
250+
# into a standalone file that works in subprocesses
251+
try:
252+
func_source = inspect.getsource(evaluator)
253+
# Dedent in case the function was defined inside another scope
254+
import textwrap
255+
256+
func_source = textwrap.dedent(func_source)
257+
func_name = evaluator.__name__
258+
259+
# Build a self-contained evaluator module with the function source
260+
# and an evaluate() entry point that calls it
261+
evaluator_code = f"""
262+
# Auto-generated evaluator from user-provided callable
263+
import importlib.util
264+
import sys
265+
import os
266+
import copy
267+
import json
268+
import time
269+
270+
{func_source}
244271
245-
# Store in globals so the wrapper can find it
246-
globals()[evaluator_id] = evaluator
272+
def evaluate(program_path):
273+
'''Wrapper that calls the user-provided evaluator function'''
274+
return {func_name}(program_path)
275+
"""
276+
except (OSError, TypeError):
277+
# If we can't get source (e.g. built-in, lambda, or closure),
278+
# fall back to the globals-based approach
279+
evaluator_id = f"_openevolve_evaluator_{uuid.uuid4().hex[:8]}"
280+
globals()[evaluator_id] = evaluator
247281

248-
evaluator_code = f"""
282+
evaluator_code = f"""
249283
# Wrapper for user-provided evaluator function
250284
import {__name__} as api_module
251285
@@ -335,57 +369,67 @@ def initial_sort(arr):
335369
lines.insert(func_end + 1, " " * (indent + 4) + "# EVOLVE-BLOCK-END")
336370
func_source = "\n".join(lines)
337371

338-
# Create evaluator that tests the function
339-
def evaluator(program_path):
340-
import importlib.util
341-
import sys
372+
# Create a self-contained evaluator as a code string so it works in subprocesses.
373+
# Closure-based evaluators fail with process-based parallelism because subprocess
374+
# workers cannot access the parent process's memory.
375+
evaluator_code = f"""
376+
import importlib.util
377+
import copy
342378
343-
# Load the evolved program
344-
spec = importlib.util.spec_from_file_location("evolved", program_path)
345-
if spec is None or spec.loader is None:
346-
return {"score": 0.0, "error": "Failed to load program"}
379+
FUNC_NAME = {func_name!r}
380+
TEST_CASES = {test_cases!r}
347381
348-
module = importlib.util.module_from_spec(spec)
382+
def evaluate(program_path):
383+
'''Auto-generated evaluator for evolve_function'''
384+
# Load the evolved program
385+
spec = importlib.util.spec_from_file_location("evolved", program_path)
386+
if spec is None or spec.loader is None:
387+
return {{"combined_score": 0.0, "score": 0.0, "error": "Failed to load program"}}
349388
350-
try:
351-
spec.loader.exec_module(module)
352-
except Exception as e:
353-
return {"score": 0.0, "error": f"Failed to execute program: {str(e)}"}
389+
module = importlib.util.module_from_spec(spec)
354390
355-
if not hasattr(module, func_name):
356-
return {"score": 0.0, "error": f"Function '{func_name}' not found"}
391+
try:
392+
spec.loader.exec_module(module)
393+
except Exception as e:
394+
return {{"combined_score": 0.0, "score": 0.0, "error": f"Failed to execute program: {{str(e)}}"}}
357395
358-
evolved_func = getattr(module, func_name)
359-
correct = 0
360-
total = len(test_cases)
361-
errors = []
396+
if not hasattr(module, FUNC_NAME):
397+
return {{"combined_score": 0.0, "score": 0.0, "error": f"Function '{{FUNC_NAME}}' not found"}}
362398
363-
for input_val, expected in test_cases:
364-
try:
365-
# Handle case where input is a list/mutable - make a copy
366-
if isinstance(input_val, list):
367-
test_input = input_val.copy()
368-
else:
369-
test_input = input_val
370-
371-
result = evolved_func(test_input)
372-
if result == expected:
373-
correct += 1
374-
else:
375-
errors.append(f"Input {input_val}: expected {expected}, got {result}")
376-
except Exception as e:
377-
errors.append(f"Input {input_val}: {str(e)}")
378-
379-
return {
380-
"score": correct / total,
381-
"test_pass_rate": correct / total,
382-
"tests_passed": correct,
383-
"total_tests": total,
384-
"errors": errors[:3], # Limit error details
385-
}
399+
evolved_func = getattr(module, FUNC_NAME)
400+
correct = 0
401+
total = len(TEST_CASES)
402+
errors = []
403+
404+
for input_val, expected in TEST_CASES:
405+
try:
406+
# Handle case where input is a list/mutable - make a copy
407+
if isinstance(input_val, list):
408+
test_input = input_val.copy()
409+
else:
410+
test_input = input_val
411+
412+
result = evolved_func(test_input)
413+
if result == expected:
414+
correct += 1
415+
else:
416+
errors.append(f"Input {{input_val}}: expected {{expected}}, got {{result}}")
417+
except Exception as e:
418+
errors.append(f"Input {{input_val}}: {{str(e)}}")
419+
420+
score = correct / total if total > 0 else 0.0
421+
return {{
422+
"combined_score": score,
423+
"score": score,
424+
"test_pass_rate": score,
425+
"tests_passed": correct,
426+
"total_tests": total,
427+
"errors": errors[:3],
428+
}}
429+
"""
386430

387431
return run_evolution(
388-
initial_program=func_source, evaluator=evaluator, iterations=iterations, **kwargs
432+
initial_program=func_source, evaluator=evaluator_code, iterations=iterations, **kwargs
389433
)
390434

391435

@@ -447,36 +491,51 @@ def benchmark_sort(instance):
447491
lines.append(" " * (indent + 4) + "# EVOLVE-BLOCK-END")
448492
class_source = "\n".join(lines)
449493

450-
# Create evaluator
451-
def evaluator(program_path):
452-
import importlib.util
494+
# Create a self-contained evaluator as a code string so it works in subprocesses.
495+
import textwrap
453496

454-
# Load the evolved program
455-
spec = importlib.util.spec_from_file_location("evolved", program_path)
456-
if spec is None or spec.loader is None:
457-
return {"score": 0.0, "error": "Failed to load program"}
497+
class_name = algorithm_class.__name__
498+
benchmark_source = textwrap.dedent(inspect.getsource(benchmark))
458499

459-
module = importlib.util.module_from_spec(spec)
500+
evaluator_code = f"""
501+
import importlib.util
460502
461-
try:
462-
spec.loader.exec_module(module)
463-
except Exception as e:
464-
return {"score": 0.0, "error": f"Failed to execute program: {str(e)}"}
503+
CLASS_NAME = {class_name!r}
465504
466-
if not hasattr(module, algorithm_class.__name__):
467-
return {"score": 0.0, "error": f"Class '{algorithm_class.__name__}' not found"}
505+
{benchmark_source}
468506
469-
AlgorithmClass = getattr(module, algorithm_class.__name__)
507+
def evaluate(program_path):
508+
'''Auto-generated evaluator for evolve_algorithm'''
509+
spec = importlib.util.spec_from_file_location("evolved", program_path)
510+
if spec is None or spec.loader is None:
511+
return {{"combined_score": 0.0, "score": 0.0, "error": "Failed to load program"}}
470512
471-
try:
472-
instance = AlgorithmClass()
473-
metrics = benchmark(instance)
474-
return metrics if isinstance(metrics, dict) else {"score": metrics}
475-
except Exception as e:
476-
return {"score": 0.0, "error": str(e)}
513+
module = importlib.util.module_from_spec(spec)
514+
515+
try:
516+
spec.loader.exec_module(module)
517+
except Exception as e:
518+
return {{"combined_score": 0.0, "score": 0.0, "error": f"Failed to execute program: {{str(e)}}"}}
519+
520+
if not hasattr(module, CLASS_NAME):
521+
return {{"combined_score": 0.0, "score": 0.0, "error": f"Class '{{CLASS_NAME}}' not found"}}
522+
523+
AlgorithmClass = getattr(module, CLASS_NAME)
524+
525+
try:
526+
instance = AlgorithmClass()
527+
metrics = {benchmark.__name__}(instance)
528+
if not isinstance(metrics, dict):
529+
metrics = {{"score": metrics}}
530+
if "combined_score" not in metrics:
531+
metrics["combined_score"] = metrics.get("score", 0.0)
532+
return metrics
533+
except Exception as e:
534+
return {{"combined_score": 0.0, "score": 0.0, "error": str(e)}}
535+
"""
477536

478537
return run_evolution(
479-
initial_program=class_source, evaluator=evaluator, iterations=iterations, **kwargs
538+
initial_program=class_source, evaluator=evaluator_code, iterations=iterations, **kwargs
480539
)
481540

482541

0 commit comments

Comments
 (0)