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