Островная эволюция: K популяций одним GPU-батчем + миграция по кольцу

GPU на маленьких батчах упирается в накладные на запуск ядер (1070: батч
4000 = 306/с, 32000 = 1211/с при 99МБ видеопамяти из 8ГБ). Вместо одной
гигантской популяции — 8 независимых островов по 4000: их поколения
оцениваются одним жирным батчем 32к, а изоляция островов + миграция лучших
по кольцу раз в 10 поколений дают разнообразие против скатывания в один
локальный оптимум. На более мощной карте достаточно поднять islands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-09 12:31:45 +05:00
parent fe34bb3672
commit 51464b2139
2 changed files with 58 additions and 18 deletions

View File

@@ -98,7 +98,16 @@ def run_evolution(
polish: bool = True,
log_path: Path | None = None,
use_gpu: bool = False,
islands: int = 1,
migrate_every: int = 10,
) -> dict:
"""islands>1 — островная модель: K независимых популяций эволюционируют
параллельно, а их поколения оцениваются ОДНИМ батчем (GPU любит жирные
батчи: на 1070 батч 4000 давал 306/с, батч 32000 — 1211/с, потому что
маленькие ядра упираются в накладные на запуск). Раз в migrate_every
поколений лучший геном каждого острова мигрирует к соседу по кольцу —
разнообразие против скатывания всех в один локальный оптимум.
"""
n_workers = n_workers or multiprocessing.cpu_count()
rng = random.Random(seed)
db = ComponentDatabase.load(data_dir) if data_dir else ComponentDatabase.load()
@@ -114,8 +123,10 @@ def run_evolution(
gpu_xp, backend = get_backend(prefer_gpu=True)
mode = "evolve" if gpu_xp is None else f"evolve-gpu({backend})"
if islands > 1:
mode += f"-x{islands}остр"
logger = ProgressLogger(
log_path or default_log_path(db_path), mode=mode, total=n_generations * population_size
log_path or default_log_path(db_path), mode=mode, total=n_generations * population_size * islands
)
ctx = multiprocessing.get_context("spawn")
@@ -135,7 +146,9 @@ def run_evolution(
signal.signal(signal.SIGTERM, _terminate)
population = [sample_genome(db, bounds, rng) for _ in range(population_size)]
populations = [
[sample_genome(db, bounds, rng) for _ in range(population_size)] for _ in range(islands)
]
best_pair: tuple[Genome, EvaluationResult] | None = None
n_evaluated = 0
@@ -150,13 +163,15 @@ def run_evolution(
initargs=(data_dir, bounds),
) as executor:
for generation in range(n_generations):
# все острова — ОДНИМ плоским батчем (жирный батч кормит GPU)
flat = [g for island in populations for g in island]
if gpu_xp is not None:
from gausse.gpu.batch_sweep import evaluate_genomes_gpu, gpu_record_worker
results = evaluate_genomes_gpu(
gpu_xp, population, db, bounds, build_details=False, executor=executor
gpu_xp, flat, db, bounds, build_details=False, executor=executor
)
pairs = list(zip(population, results))
pairs_flat = list(zip(flat, results))
payload = [
(g, {
"feasible": r.feasible,
@@ -166,29 +181,40 @@ def run_evolution(
"failed_stage_index": r.failed_stage_index,
"energy_breakdown": r.energy_breakdown,
}, mode)
for g, r in pairs
for g, r in pairs_flat
]
records = executor.map(gpu_record_worker, payload, chunksize=64)
else:
pairs = list(executor.map(_evaluate_genome, population))
records = (build_run_record(g, r, db, search_mode=mode) for g, r in pairs)
n_evaluated += len(pairs)
pairs_flat = list(executor.map(_evaluate_genome, flat))
records = (build_run_record(g, r, db, search_mode=mode) for g, r in pairs_flat)
n_evaluated += len(pairs_flat)
for record, (genome, result) in zip(records, pairs):
for record, (genome, result) in zip(records, pairs_flat):
queue.put(record)
logger.update(result.feasible, result.efficiency)
if best_pair is None or result.fitness > best_pair[1].fitness:
best_pair = (genome, result)
pairs.sort(key=lambda pair: pair[1].fitness, reverse=True)
next_population = [copy.deepcopy(pair[0]) for pair in pairs[:elitism]]
while len(next_population) < population_size:
parent_a = _tournament_select(pairs, rng)
parent_b = _tournament_select(pairs, rng)
child = crossover(parent_a, parent_b, rng)
child = mutate(child, db, bounds, rng, rate=mutation_rate)
next_population.append(child)
population = next_population
# селекция и скрещивание — НА КАЖДОМ ОСТРОВЕ независимо
island_bests: list[Genome] = []
next_populations = []
for i in range(islands):
pairs = pairs_flat[i * population_size:(i + 1) * population_size]
pairs.sort(key=lambda pair: pair[1].fitness, reverse=True)
island_bests.append(pairs[0][0])
next_population = [copy.deepcopy(pair[0]) for pair in pairs[:elitism]]
while len(next_population) < population_size:
parent_a = _tournament_select(pairs, rng)
parent_b = _tournament_select(pairs, rng)
child = crossover(parent_a, parent_b, rng)
child = mutate(child, db, bounds, rng, rate=mutation_rate)
next_population.append(child)
next_populations.append(next_population)
# миграция по кольцу: лучший острова i замещает слот у острова i+1
if islands > 1 and (generation + 1) % migrate_every == 0:
for i in range(islands):
next_populations[(i + 1) % islands][elitism] = copy.deepcopy(island_bests[i])
populations = next_populations
finally:
# с этого места нас нельзя прерывать: дописываем базу до конца
signal.signal(signal.SIGTERM, signal.SIG_IGN)

View File

@@ -79,3 +79,17 @@ def test_run_evolution_gpu_batch_mode(tmp_path):
assert count_runs(conn) >= 120
rows = fetch_runs(conn, limit=5)
assert all(r.search_mode.startswith("evolve-gpu") for r in rows)
def test_run_evolution_islands(tmp_path):
"""Островная модель: K популяций одним батчем + миграция по кольцу."""
from gausse.storage.database import count_runs, open_connection
db_path = tmp_path / "evo_isl.sqlite3"
summary = run_evolution(
db_path, n_generations=4, population_size=15, seed=7, polish=False,
use_gpu=True, islands=3, migrate_every=2,
)
assert summary["n_evaluated"] == 4 * 15 * 3
conn = open_connection(db_path)
assert count_runs(conn) == 4 * 15 * 3