Skip to content

Commit 545f72f

Browse files
authored
Merge pull request #279 from codelion/fix-parallel-sample-island
Fix parallel sample island
2 parents 7486028 + 5e2d645 commit 545f72f

File tree

3 files changed

+468
-51
lines changed

3 files changed

+468
-51
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.15"
3+
__version__ = "0.2.16"

openevolve/database.py

Lines changed: 159 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -368,87 +368,70 @@ def sample_from_island(
368368
) -> Tuple[Program, List[Program]]:
369369
"""
370370
Sample a program and inspirations from a specific island without modifying current_island
371-
371+
372372
This method is thread-safe and doesn't modify shared state, avoiding race conditions
373373
when multiple workers sample from different islands concurrently.
374-
374+
375+
Uses the same exploration/exploitation/random strategy as sample() to ensure
376+
consistent behavior between single-process and parallel execution modes.
377+
375378
Args:
376379
island_id: The island to sample from
377380
num_inspirations: Number of inspiration programs to sample (defaults to 5)
378-
381+
379382
Returns:
380383
Tuple of (parent_program, inspiration_programs)
381384
"""
382385
# Ensure valid island ID
383386
island_id = island_id % len(self.islands)
384-
387+
385388
# Get programs from the specific island
386389
island_programs = list(self.islands[island_id])
387-
390+
388391
if not island_programs:
389392
# Island is empty, fall back to sampling from all programs
390393
logger.debug(f"Island {island_id} is empty, sampling from all programs")
391394
return self.sample(num_inspirations)
392-
393-
# Select parent from island programs
394-
if len(island_programs) == 1:
395-
parent_id = island_programs[0]
395+
396+
# Use exploration_ratio and exploitation_ratio to decide sampling strategy
397+
# This matches the logic in _sample_parent() for consistent behavior
398+
rand_val = random.random()
399+
400+
if rand_val < self.config.exploration_ratio:
401+
# EXPLORATION: Sample randomly from island (diverse sampling)
402+
parent = self._sample_from_island_random(island_id)
403+
sampling_mode = "exploration"
404+
elif rand_val < self.config.exploration_ratio + self.config.exploitation_ratio:
405+
# EXPLOITATION: Sample from archive (elite programs)
406+
parent = self._sample_from_archive_for_island(island_id)
407+
sampling_mode = "exploitation"
396408
else:
397-
# Use weighted sampling based on program scores
398-
island_program_objects = [
399-
self.programs[pid] for pid in island_programs
400-
if pid in self.programs
401-
]
402-
403-
if not island_program_objects:
404-
# Fallback if programs not found
405-
parent_id = random.choice(island_programs)
406-
else:
407-
# Calculate weights based on fitness scores
408-
weights = []
409-
for prog in island_program_objects:
410-
fitness = get_fitness_score(prog.metrics, self.config.feature_dimensions)
411-
# Add small epsilon to avoid zero weights
412-
weights.append(max(fitness, 0.001))
413-
414-
# Normalize weights
415-
total_weight = sum(weights)
416-
if total_weight > 0:
417-
weights = [w / total_weight for w in weights]
418-
else:
419-
weights = [1.0 / len(island_program_objects)] * len(island_program_objects)
420-
421-
# Sample parent based on weights
422-
parent = random.choices(island_program_objects, weights=weights, k=1)[0]
423-
parent_id = parent.id
424-
425-
parent = self.programs.get(parent_id)
426-
if not parent:
427-
# Should not happen, but handle gracefully
428-
logger.error(f"Parent program {parent_id} not found in database")
429-
return self.sample(num_inspirations)
430-
409+
# WEIGHTED: Use fitness-weighted sampling (remaining probability)
410+
parent = self._sample_from_island_weighted(island_id)
411+
sampling_mode = "weighted"
412+
431413
# Select inspirations from the same island
432414
if num_inspirations is None:
433415
num_inspirations = 5 # Default for backward compatibility
434-
416+
435417
# Get other programs from the island for inspirations
436-
other_programs = [pid for pid in island_programs if pid != parent_id]
437-
418+
other_programs = [pid for pid in island_programs if pid != parent.id]
419+
438420
if len(other_programs) < num_inspirations:
439421
# Not enough programs in island, use what we have
440422
inspiration_ids = other_programs
441423
else:
442424
# Sample inspirations
443425
inspiration_ids = random.sample(other_programs, num_inspirations)
444-
426+
445427
inspirations = [
446-
self.programs[pid] for pid in inspiration_ids
428+
self.programs[pid] for pid in inspiration_ids
447429
if pid in self.programs
448430
]
449-
431+
450432
logger.debug(
451-
f"Sampled parent {parent.id} and {len(inspirations)} inspirations from island {island_id}"
433+
f"Sampled parent {parent.id} and {len(inspirations)} inspirations from island {island_id} "
434+
f"(mode: {sampling_mode}, rand_val: {rand_val:.3f})"
452435
)
453436
return parent, inspirations
454437

@@ -1264,6 +1247,132 @@ def _sample_random_parent(self) -> Program:
12641247
program_id = random.choice(list(self.programs.keys()))
12651248
return self.programs[program_id]
12661249

1250+
def _sample_from_island_weighted(self, island_id: int) -> Program:
1251+
"""
1252+
Sample a parent from a specific island using fitness-weighted selection
1253+
1254+
Args:
1255+
island_id: The island to sample from
1256+
1257+
Returns:
1258+
Parent program selected using fitness-weighted sampling
1259+
"""
1260+
island_id = island_id % len(self.islands)
1261+
island_programs = list(self.islands[island_id])
1262+
1263+
if not island_programs:
1264+
# Island is empty, fall back to any available program
1265+
logger.debug(f"Island {island_id} is empty, sampling from all programs")
1266+
return self._sample_random_parent()
1267+
1268+
# Select parent from island programs
1269+
if len(island_programs) == 1:
1270+
parent_id = island_programs[0]
1271+
else:
1272+
# Use weighted sampling based on program scores
1273+
island_program_objects = [
1274+
self.programs[pid] for pid in island_programs
1275+
if pid in self.programs
1276+
]
1277+
1278+
if not island_program_objects:
1279+
# Fallback if programs not found
1280+
parent_id = random.choice(island_programs)
1281+
else:
1282+
# Calculate weights based on fitness scores
1283+
weights = []
1284+
for prog in island_program_objects:
1285+
fitness = get_fitness_score(prog.metrics, self.config.feature_dimensions)
1286+
# Add small epsilon to avoid zero weights
1287+
weights.append(max(fitness, 0.001))
1288+
1289+
# Normalize weights
1290+
total_weight = sum(weights)
1291+
if total_weight > 0:
1292+
weights = [w / total_weight for w in weights]
1293+
else:
1294+
weights = [1.0 / len(island_program_objects)] * len(island_program_objects)
1295+
1296+
# Sample parent based on weights
1297+
parent = random.choices(island_program_objects, weights=weights, k=1)[0]
1298+
parent_id = parent.id
1299+
1300+
parent = self.programs.get(parent_id)
1301+
if not parent:
1302+
# Should not happen, but handle gracefully
1303+
logger.error(f"Parent program {parent_id} not found in database")
1304+
return self._sample_random_parent()
1305+
1306+
return parent
1307+
1308+
def _sample_from_island_random(self, island_id: int) -> Program:
1309+
"""
1310+
Sample a completely random parent from a specific island (uniform distribution)
1311+
1312+
Args:
1313+
island_id: The island to sample from
1314+
1315+
Returns:
1316+
Parent program selected uniformly at random
1317+
"""
1318+
island_id = island_id % len(self.islands)
1319+
island_programs = list(self.islands[island_id])
1320+
1321+
if not island_programs:
1322+
# Island is empty, fall back to any available program
1323+
logger.debug(f"Island {island_id} is empty, sampling from all programs")
1324+
return self._sample_random_parent()
1325+
1326+
# Clean up stale references
1327+
valid_programs = [pid for pid in island_programs if pid in self.programs]
1328+
1329+
if not valid_programs:
1330+
logger.warning(f"Island {island_id} has no valid programs, falling back to random sampling")
1331+
return self._sample_random_parent()
1332+
1333+
# Uniform random selection
1334+
parent_id = random.choice(valid_programs)
1335+
return self.programs[parent_id]
1336+
1337+
def _sample_from_archive_for_island(self, island_id: int) -> Program:
1338+
"""
1339+
Sample a parent from the archive, preferring programs from the specified island
1340+
1341+
Args:
1342+
island_id: The island to prefer programs from
1343+
1344+
Returns:
1345+
Parent program from archive (preferably from the specified island)
1346+
"""
1347+
if not self.archive:
1348+
# Fallback to weighted sampling from island
1349+
logger.debug(f"Archive is empty, falling back to weighted island sampling")
1350+
return self._sample_from_island_weighted(island_id)
1351+
1352+
# Clean up stale references in archive
1353+
valid_archive = [pid for pid in self.archive if pid in self.programs]
1354+
1355+
if not valid_archive:
1356+
logger.warning("Archive has no valid programs, falling back to weighted island sampling")
1357+
return self._sample_from_island_weighted(island_id)
1358+
1359+
island_id = island_id % len(self.islands)
1360+
1361+
# Prefer programs from the specified island in archive
1362+
archive_programs_in_island = [
1363+
pid
1364+
for pid in valid_archive
1365+
if self.programs[pid].metadata.get("island") == island_id
1366+
]
1367+
1368+
if archive_programs_in_island:
1369+
parent_id = random.choice(archive_programs_in_island)
1370+
return self.programs[parent_id]
1371+
else:
1372+
# Fall back to any valid archive program if island has none
1373+
parent_id = random.choice(valid_archive)
1374+
return self.programs[parent_id]
1375+
12671376
def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
12681377
"""
12691378
Sample inspiration programs for the next evolution step.

0 commit comments

Comments
 (0)