Островная эволюция: 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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user