Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions synalinks/src/optimizers/evolutionary_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ async def propose_new_candidates(
best_candidates = trainable_variable.get("best_candidates")
selected_candidate = self.select_candidate(best_candidates)

# On the very first training step `best_candidates` is still
# empty (it is populated downstream by `maybe_add_candidate`).
# Fall back to a seed candidate so mutation/crossover has
# something to work with, mirroring the same fallback used in
# `on_batch_begin`.
if selected_candidate is None:
seed_candidates = trainable_variable.get("seed_candidates")
if seed_candidates:
selected_candidate = random.choice(seed_candidates)

if strategy == "mutation":
new_candidate = await self.mutate_candidate(
step,
Expand Down
71 changes: 71 additions & 0 deletions synalinks/src/optimizers/evolutionary_optimizer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,77 @@ def test_sampling_temperature_default(self):
optimizer = EvolutionaryOptimizer()
self.assertEqual(optimizer.sampling_temperature, 0.3)

async def test_propose_new_candidates_falls_back_to_seed_when_best_empty(
self,
):
"""Regression test: on the very first training step `best_candidates`
is still empty (it is populated later by `maybe_add_candidate`).
`propose_new_candidates` used to pass the resulting `None` into
`mutate_candidate`, which crashed downstream (e.g. OMEGA's
`selected_candidate.items()` in `omega.py`). It must instead fall
back to a random seed candidate, mirroring `on_batch_begin`.
"""
seen = {}

class _RecordingOptimizer(EvolutionaryOptimizer):
async def mutate_candidate(
self,
step,
trainable_variable,
selected_candidate,
x=None,
y=None,
y_pred=None,
training=False,
):
seen["selected_candidate"] = selected_candidate
return None

async def merge_candidate(
self,
step,
trainable_variable,
current_candidate,
other_candidate,
x=None,
y=None,
y_pred=None,
training=False,
):
return None

# merging_rate=0 keeps the strategy on "mutation" regardless of epoch.
optimizer = _RecordingOptimizer(selection="random", merging_rate=0.0)

seed_candidate = {"prompt": "seed_prompt"}
trainable_variable = JsonDataModel(
json={
"seed_candidates": [seed_candidate],
"best_candidates": [],
"nb_visit": 0,
"cumulative_reward": 0.0,
"prompt": "initial",
},
schema={
"type": "object",
"properties": {
"seed_candidates": {"type": "array"},
"best_candidates": {"type": "array"},
"nb_visit": {"type": "integer"},
"cumulative_reward": {"type": "number"},
"prompt": {"type": "string"},
},
},
name="trainable_var",
)

await optimizer.propose_new_candidates(
step=0,
trainable_variables=[trainable_variable],
)

self.assertEqual(seen.get("selected_candidate"), seed_candidate)

async def test_select_variable_name_to_update_does_not_raise(self):
"""Regression test: `select_variable_name_to_update` on an
EvolutionaryOptimizer used to raise
Expand Down
Loading