From 51464b2139932649b84817097b3bb7473bb69965 Mon Sep 17 00:00:00 2001 From: jze9 Date: Thu, 9 Jul 2026 12:31:45 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=81=D1=82=D1=80=D0=BE=D0=B2=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D1=8D=D0=B2=D0=BE=D0=BB=D1=8E=D1=86=D0=B8=D1=8F?= =?UTF-8?q?:=20K=20=D0=BF=D0=BE=D0=BF=D1=83=D0=BB=D1=8F=D1=86=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=BE=D0=B4=D0=BD=D0=B8=D0=BC=20GPU-=D0=B1=D0=B0=D1=82?= =?UTF-8?q?=D1=87=D0=B5=D0=BC=20+=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=BA=D0=BE=D0=BB=D1=8C=D1=86?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPU на маленьких батчах упирается в накладные на запуск ядер (1070: батч 4000 = 306/с, 32000 = 1211/с при 99МБ видеопамяти из 8ГБ). Вместо одной гигантской популяции — 8 независимых островов по 4000: их поколения оцениваются одним жирным батчем 32к, а изоляция островов + миграция лучших по кольцу раз в 10 поколений дают разнообразие против скатывания в один локальный оптимум. На более мощной карте достаточно поднять islands. Co-Authored-By: Claude Fable 5 --- src/gausse/optim/evolutionary.py | 62 ++++++++++++++++++++++---------- tests/test_evolutionary.py | 14 ++++++++ 2 files changed, 58 insertions(+), 18 deletions(-) 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