Skip to content

Commit c3c0eec

Browse files
committed
feat(config): add checkpoint_on_improvement option to save only on new best
Adds a new config flag, checkpoint_on_improvement, that triggers a checkpoint callback whenever a new-best program is found, in addition to the existing checkpoint_interval gate. Default False preserves existing behavior. When the optimization is making slow progress, checkpoint_interval saves work that the next interval would overwrite without any new best. This option lets users say 'only checkpoint when there's actually something new to save.' Wires the flag through the worker config dict (process_parallel.py:386) and adds a unit test that verifies the callback fires for a new-best run and not for a not-best run when checkpoint_interval is set high. Fixes #434
1 parent 80945ed commit c3c0eec

3 files changed

Lines changed: 87 additions & 6 deletions

File tree

openevolve/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ class Config:
404404
# General settings
405405
max_iterations: int = 10000
406406
checkpoint_interval: int = 100
407+
checkpoint_on_improvement: bool = False
407408
log_level: str = "INFO"
408409
log_dir: Optional[str] = None
409410
random_seed: Optional[int] = 42

openevolve/process_parallel.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ def _serialize_config(self, config: Config) -> dict:
384384
"evaluator": asdict(config.evaluator),
385385
"max_iterations": config.max_iterations,
386386
"checkpoint_interval": config.checkpoint_interval,
387+
"checkpoint_on_improvement": config.checkpoint_on_improvement,
387388
"log_level": config.log_level,
388389
"log_dir": config.log_dir,
389390
"random_seed": config.random_seed,
@@ -658,21 +659,33 @@ async def run_evolution(
658659
self._warned_about_combined_score = True
659660

660661
# Check for new best
661-
if self.database.best_program_id == child_program.id:
662+
is_new_best = self.database.best_program_id == child_program.id
663+
if is_new_best:
662664
logger.info(
663665
f"🌟 New best solution found at iteration {completed_iteration}: "
664666
f"{child_program.id}"
665667
)
666668

667669
# Checkpoint callback
668670
# Don't checkpoint at iteration 0 (that's just the initial program)
669-
if (
671+
interval_hit = (
670672
completed_iteration > 0
671673
and completed_iteration % self.config.checkpoint_interval == 0
672-
):
673-
logger.info(
674-
f"Checkpoint interval reached at iteration {completed_iteration}"
675-
)
674+
)
675+
improvement_hit = (
676+
completed_iteration > 0
677+
and self.config.checkpoint_on_improvement
678+
and is_new_best
679+
)
680+
if interval_hit or improvement_hit:
681+
if interval_hit:
682+
logger.info(
683+
f"Checkpoint interval reached at iteration {completed_iteration}"
684+
)
685+
else:
686+
logger.info(
687+
f"Checkpointing new best solution at iteration {completed_iteration}"
688+
)
676689
self.database.log_island_status()
677690
if checkpoint_callback:
678691
checkpoint_callback(completed_iteration)

tests/test_process_parallel.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,73 @@ async def run_test():
153153
# Run the async test
154154
asyncio.run(run_test())
155155

156+
def test_checkpoint_on_improvement_only_fires_for_new_best(self):
157+
"""Test checkpoint callback fires on improvement when interval is not reached"""
158+
159+
async def run_test():
160+
self.config.checkpoint_on_improvement = True
161+
self.config.checkpoint_interval = 10000
162+
controller = ProcessParallelController(self.config, self.eval_file, self.database)
163+
checkpoint_calls = []
164+
165+
with patch.object(controller, "_submit_iteration") as mock_submit:
166+
mock_future1 = MagicMock()
167+
mock_result1 = SerializableResult(
168+
child_program_dict={
169+
"id": "child_best",
170+
"code": "def evolved(): return 1",
171+
"language": "python",
172+
"parent_id": "test_0",
173+
"generation": 1,
174+
"metrics": {"score": 1.0, "performance": 1.0},
175+
"iteration_found": 1,
176+
"metadata": {"changes": "improved", "island": 0},
177+
},
178+
parent_id="test_0",
179+
iteration_time=0.1,
180+
iteration=1,
181+
target_island=0,
182+
)
183+
mock_future1.done.return_value = True
184+
mock_future1.result.return_value = mock_result1
185+
mock_future1.cancel.return_value = True
186+
187+
mock_future2 = MagicMock()
188+
mock_result2 = SerializableResult(
189+
child_program_dict={
190+
"id": "child_not_best",
191+
"code": "def evolved(): return 0",
192+
"language": "python",
193+
"parent_id": "test_0",
194+
"generation": 1,
195+
"metrics": {"score": 0.1, "performance": 0.1},
196+
"iteration_found": 2,
197+
"metadata": {"changes": "not improved", "island": 1},
198+
},
199+
parent_id="test_0",
200+
iteration_time=0.1,
201+
iteration=2,
202+
target_island=1,
203+
)
204+
mock_future2.done.return_value = True
205+
mock_future2.result.return_value = mock_result2
206+
mock_future2.cancel.return_value = True
207+
208+
mock_submit.side_effect = [mock_future1, mock_future2]
209+
210+
controller.start()
211+
await controller.run_evolution(
212+
start_iteration=1,
213+
max_iterations=2,
214+
target_score=None,
215+
checkpoint_callback=checkpoint_calls.append,
216+
)
217+
controller.stop()
218+
219+
self.assertEqual(checkpoint_calls, [1])
220+
221+
asyncio.run(run_test())
222+
156223
def test_request_shutdown(self):
157224
"""Test graceful shutdown request"""
158225
controller = ProcessParallelController(self.config, self.eval_file, self.database)

0 commit comments

Comments
 (0)