diff --git a/src/gausse/optim/evolutionary.py b/src/gausse/optim/evolutionary.py index a889da2..f49101d 100644 --- a/src/gausse/optim/evolutionary.py +++ b/src/gausse/optim/evolutionary.py @@ -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) diff --git a/tests/test_evolutionary.py b/tests/test_evolutionary.py index 965c8f8..9d18c26 100644 --- a/tests/test_evolutionary.py +++ b/tests/test_evolutionary.py @@ -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