From 0c823947b70228960a57b789890d3f9721c66b45 Mon Sep 17 00:00:00 2001 From: Madson Dias Date: Wed, 7 Oct 2026 07:58:58 -0300 Subject: [PATCH] fix(algos): Fixes infinite loop in mutate_parents when mutations are invalid. --- archai/discrete_search/algos/bananas.py | 3 +- archai/discrete_search/algos/local_search.py | 2 +- .../algos/regularized_evolution.py | 2 +- tests/discrete_search/algos/test_bananas.py | 29 +++++++++++++++++ .../algos/test_local_search.py | 29 +++++++++++++++++ .../algos/test_regularized_evolution.py | 31 +++++++++++++++++++ 6 files changed, 92 insertions(+), 4 deletions(-) diff --git a/archai/discrete_search/algos/bananas.py b/archai/discrete_search/algos/bananas.py index 78b0b7c2b..b7cc42342 100644 --- a/archai/discrete_search/algos/bananas.py +++ b/archai/discrete_search/algos/bananas.py @@ -167,6 +167,7 @@ def mutate_parents( nb_tries = 0 while len(candidates) < mutations_per_parent and nb_tries < patience: + nb_tries += 1 mutated_model = self.search_space.mutate(p) mutated_model.metadata["parent"] = p.archid @@ -176,8 +177,6 @@ def mutate_parents( if mutated_model.archid not in self.seen_archs: candidates[mutated_model.archid] = mutated_model - nb_tries += 1 - mutations.update(candidates) if len(mutations) == 0: diff --git a/archai/discrete_search/algos/local_search.py b/archai/discrete_search/algos/local_search.py index 126bf4735..5cece8068 100644 --- a/archai/discrete_search/algos/local_search.py +++ b/archai/discrete_search/algos/local_search.py @@ -122,6 +122,7 @@ def mutate_parents( nb_tries = 0 while len(candidates) < mutations_per_parent and nb_tries < patience: + nb_tries += 1 mutated_model = self.search_space.mutate(p) mutated_model.metadata["parent"] = p.archid @@ -131,7 +132,6 @@ def mutate_parents( if mutated_model.archid not in self.seen_archs: mutated_model.metadata["generation"] = self.iter_num candidates[mutated_model.archid] = mutated_model - nb_tries += 1 mutations.update(candidates) return list(mutations.values()) diff --git a/archai/discrete_search/algos/regularized_evolution.py b/archai/discrete_search/algos/regularized_evolution.py index ed8f00bb5..0a12160b6 100644 --- a/archai/discrete_search/algos/regularized_evolution.py +++ b/archai/discrete_search/algos/regularized_evolution.py @@ -135,6 +135,7 @@ def mutate_parents( nb_tries = 0 while len(candidates) < mutations_per_parent and nb_tries < patience: + nb_tries += 1 mutated_model = self.search_space.mutate(p) mutated_model.metadata["parent"] = p.archid @@ -144,7 +145,6 @@ def mutate_parents( if mutated_model.archid not in self.seen_archs: mutated_model.metadata["generation"] = self.iter_num candidates[mutated_model.archid] = mutated_model - nb_tries += 1 mutations.update(candidates) return list(mutations.values()) diff --git a/tests/discrete_search/algos/test_bananas.py b/tests/discrete_search/algos/test_bananas.py index c168b1f82..d412c6d1f 100644 --- a/tests/discrete_search/algos/test_bananas.py +++ b/tests/discrete_search/algos/test_bananas.py @@ -37,3 +37,32 @@ def test_bananas(output_dir, search_space, search_objectives, surrogate_model): # Checks if all registered models satisfy constraints _, valid_models = search_objectives.validate_constraints(all_models) assert len(valid_models) == len(all_models) + + +def test_bananas_mutate_parents_stops_on_invalid_mutations( + output_dir, search_space, search_objectives, surrogate_model, monkeypatch +): + algo = MoBananasSearch( + search_space=search_space, + search_objectives=search_objectives, + output_dir=output_dir, + surrogate_model=surrogate_model, + num_iters=2, + init_num_models=5, + ) + + mutate_calls = [] + + def counting_mutate(model): + mutate_calls.append(model.archid) + # Fails instead of hanging if `patience` is not honored + assert len(mutate_calls) <= 3 + return mutate(model) + + mutate = search_space.mutate + monkeypatch.setattr(search_space, "mutate", counting_mutate) + monkeypatch.setattr(algo.so, "is_model_valid", lambda model: False) + + parent = search_space.random_sample() + assert algo.mutate_parents([parent], mutations_per_parent=1, patience=3) == [] + assert len(mutate_calls) == 3 diff --git a/tests/discrete_search/algos/test_local_search.py b/tests/discrete_search/algos/test_local_search.py index ec56f8b07..2c3b39aa1 100644 --- a/tests/discrete_search/algos/test_local_search.py +++ b/tests/discrete_search/algos/test_local_search.py @@ -35,3 +35,32 @@ def test_local_search(output_dir, search_space, search_objectives): # Checks if all registered models satisfy constraints _, valid_models = search_objectives.validate_constraints(all_models) assert len(valid_models) == len(all_models) + + +def test_local_search_mutate_parents_stops_on_invalid_mutations( + output_dir, search_space, search_objectives, monkeypatch +): + algo = LocalSearch( + search_space, + search_objectives, + output_dir, + num_iters=2, + init_num_models=5, + seed=1, + ) + + mutate_calls = [] + + def counting_mutate(model): + mutate_calls.append(model.archid) + # Fails instead of hanging if `patience` is not honored + assert len(mutate_calls) <= 3 + return mutate(model) + + mutate = search_space.mutate + monkeypatch.setattr(search_space, "mutate", counting_mutate) + monkeypatch.setattr(algo.so, "is_model_valid", lambda model: False) + + parent = search_space.random_sample() + assert algo.mutate_parents([parent], mutations_per_parent=1, patience=3) == [] + assert len(mutate_calls) == 3 diff --git a/tests/discrete_search/algos/test_regularized_evolution.py b/tests/discrete_search/algos/test_regularized_evolution.py index 7f668f742..b5f5ca188 100644 --- a/tests/discrete_search/algos/test_regularized_evolution.py +++ b/tests/discrete_search/algos/test_regularized_evolution.py @@ -38,3 +38,34 @@ def test_regularized_evolution(output_dir, search_space, search_objectives, surr # Checks if all registered models satisfy constraints _, valid_models = search_objectives.validate_constraints(all_models) assert len(valid_models) == len(all_models) + + +def test_regularized_evolution_mutate_parents_stops_on_invalid_mutations( + output_dir, search_space, search_objectives, monkeypatch +): + algo = RegularizedEvolutionSearch( + search_space=search_space, + search_objectives=search_objectives, + output_dir=output_dir, + num_iters=2, + init_num_models=4, + pareto_sample_size=4, + history_size=10, + seed=1, + ) + + mutate_calls = [] + + def counting_mutate(model): + mutate_calls.append(model.archid) + # Fails instead of hanging if `patience` is not honored + assert len(mutate_calls) <= 3 + return mutate(model) + + mutate = search_space.mutate + monkeypatch.setattr(search_space, "mutate", counting_mutate) + monkeypatch.setattr(algo.so, "is_model_valid", lambda model: False) + + parent = search_space.random_sample() + assert algo.mutate_parents([parent], mutations_per_parent=1, patience=3) == [] + assert len(mutate_calls) == 3