From ef835126010a18823cf979649e5d52aef08e0531 Mon Sep 17 00:00:00 2001 From: jze9 Date: Wed, 8 Jul 2026 03:22:47 +0500 Subject: [PATCH] =?UTF-8?q?GPU=20=D0=B2=20=D1=8D=D0=B2=D0=BE=D0=BB=D1=8E?= =?UTF-8?q?=D1=86=D0=B8=D0=B8:=20=D0=BE=D1=86=D0=B5=D0=BD=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=D0=BE=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=B8=D0=BC=20=D0=B1=D0=B0=D1=82=D1=87=D0=B5=D0=BC?= =?UTF-8?q?=20(use=5Fgpu=3DTrue)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit evaluate_genomes_gpu в gpu/batch_sweep.py — общий раундовый симулятор (_simulate_states) для sweep и эволюции, та же формула фитнеса, что в objective.evaluate (КПД либо -1+доля пройденных ступеней). run_evolution получил use_gpu: поколение целиком уходит в батч-интегратор (на сервере cupy/GTX 1070), полировка остаётся точной на CPU. search_mode=evolve-gpu(...). Co-Authored-By: Claude Fable 5 --- src/gausse/gpu/batch_sweep.py | 147 ++++++++++++++++++++++--------- src/gausse/optim/evolutionary.py | 41 +++++++-- tests/test_evolutionary.py | 16 ++++ 3 files changed, 152 insertions(+), 52 deletions(-) diff --git a/src/gausse/gpu/batch_sweep.py b/src/gausse/gpu/batch_sweep.py index 02f380d..26ae632 100644 --- a/src/gausse/gpu/batch_sweep.py +++ b/src/gausse/gpu/batch_sweep.py @@ -49,6 +49,7 @@ class _State: kinetic_delta_j: float = 0.0 alive: bool = True reason: str | None = None + failed_stage_index: int | None = None def _coast_velocity(v0: float, distance_m: float, retard_const_n: float, drag_coeff: float, mass_kg: float) -> float: @@ -101,6 +102,108 @@ def _prepare_stage(stage, projectile, cur_x_global, cur_v, coil_center_global): }, None +def _simulate_states(xp, states: list) -> None: + """Раунд-за-раундом прогоняет разряды всех живых ступеней батчами (мутирует states).""" + if not states: + return + max_ns = max(len(s.config.stages) for s in states) + for s_idx in range(max_ns): + prepared = [] # (state, prep) + for st in states: + if not st.alive or len(st.config.stages) <= s_idx: + continue + p, reason = _prepare_stage( + st.config.stages[s_idx], st.config.projectile, st.cur_x, st.cur_v, st.coil_centers[s_idx] + ) + if p is None: + st.alive = False + st.reason = f"ступень {s_idx}: {reason}" + st.failed_stage_index = s_idx + else: + prepared.append((st, p)) + if not prepared: + continue + + models = [p["phys"].inductance_model for _, p in prepared] + cps = [p["phys"].circuit_params for _, p in prepared] + params = params_from_models(xp, models, cps) + out = integrate_batch_discharge( + xp, + xp.asarray([p["q0"] for _, p in prepared]), + xp.asarray([p["x_fire"] for _, p in prepared]), + xp.asarray([p["v_fire"] for _, p in prepared]), + params, dt=2e-6, max_steps=15000, + ) + exit_v = to_cpu(xp, out["exit_v"]); exit_x = to_cpu(xp, out["exit_x"]) + peak_i = to_cpu(xp, out["peak_current"]); feas = to_cpu(xp, out["feasible"]) + for k, (st, p) in enumerate(prepared): + stage = p["stage"] + if not bool(feas[k]): + st.alive = False + st.reason = f"ступень {s_idx}: разряд не скоммутировался" + st.failed_stage_index = s_idx + continue + surge = switch_pulse_limit_a(stage.switch) + if float(peak_i[k]) > surge: + st.alive = False + st.reason = f"ступень {s_idx}: пиковый ток {float(peak_i[k]):.0f}А > импульсного предела ключа ({surge:.0f}А)" + st.failed_stage_index = s_idx + continue + ev = float(exit_v[k]) + st.kinetic_delta_j += 0.5 * p["mass"] * (ev**2 - p["v_fire"] ** 2) + st.energy_in_j += p["energy_in"] + st.cur_v = ev + st.cur_x = p["coil_center"] + float(exit_x[k]) + + +def evaluate_genomes_gpu(xp, genomes: list, db: ComponentDatabase, bounds: SearchBounds) -> list: + """Оценка списка геномов ОДНИМ батчем (для эволюции): та же физика и та же + формула фитнеса, что в objective.evaluate (КПД либо -1+доля пройденных + ступеней), но разряды всех геномов интегрируются вместе на GPU/numpy. + """ + from gausse.optim.objective import EvaluationResult, compute_cost_rub + + states = [] + for g in genomes: + cfg, ix, iv = decode(g, db, bounds) + centers = [0.0] + for gap in cfg.inter_stage_gaps_m: + centers.append(centers[-1] + gap) + states.append(_State(g, cfg, ix, iv, centers, cur_x=ix, cur_v=iv)) + + _simulate_states(xp, states) + + results = [] + for st in states: + cost = compute_cost_rub(st.config, db) + ok = st.alive and st.energy_in_j > 0 + detail = build_detail( + st.config, _FEASIBLE if ok else None, db, st.initial_x_m, st.initial_v_mps, + st.genome.tube_inner_d_m, st.genome.tube_wall_m, + ) + if ok: + eff = st.kinetic_delta_j / st.energy_in_j + results.append(EvaluationResult( + feasible=True, cost_rub=cost, fitness=eff, efficiency=eff, + exit_velocity_mps=st.cur_v, + energy_breakdown={ + "total_energy_in_j": st.energy_in_j, + "total_kinetic_energy_delta_j": st.kinetic_delta_j, + }, + detail=detail, + )) + else: + n_stages = len(st.config.stages) + progress = (st.failed_stage_index or 0) / n_stages if n_stages else 0.0 + results.append(EvaluationResult( + feasible=False, cost_rub=cost, fitness=-1.0 + progress, + reason=st.reason or "нет ни одной сработавшей ступени", + failed_stage_index=st.failed_stage_index, + energy_breakdown={}, detail=detail, + )) + return results + + def run_gpu_sweep( db_path: Path, n_runs: int, @@ -131,49 +234,7 @@ def run_gpu_sweep( centers.append(centers[-1] + gap) states.append(_State(g, cfg, ix, iv, centers, cur_x=ix, cur_v=iv)) - max_ns = max(len(s.config.stages) for s in states) - for s_idx in range(max_ns): - prepared = [] # (state, prep) - for st in states: - if not st.alive or len(st.config.stages) <= s_idx: - continue - p, reason = _prepare_stage( - st.config.stages[s_idx], st.config.projectile, st.cur_x, st.cur_v, st.coil_centers[s_idx] - ) - if p is None: - st.alive = False - st.reason = f"ступень {s_idx}: {reason}" - else: - prepared.append((st, p)) - if not prepared: - continue - - models = [p["phys"].inductance_model for _, p in prepared] - cps = [p["phys"].circuit_params for _, p in prepared] - params = params_from_models(xp, models, cps) - out = integrate_batch_discharge( - xp, - xp.asarray([p["q0"] for _, p in prepared]), - xp.asarray([p["x_fire"] for _, p in prepared]), - xp.asarray([p["v_fire"] for _, p in prepared]), - params, dt=2e-6, max_steps=15000, - ) - exit_v = to_cpu(xp, out["exit_v"]); exit_x = to_cpu(xp, out["exit_x"]) - peak_i = to_cpu(xp, out["peak_current"]); feas = to_cpu(xp, out["feasible"]) - for k, (st, p) in enumerate(prepared): - stage = p["stage"] - if not bool(feas[k]): - st.alive = False; st.reason = f"ступень {s_idx}: разряд не скоммутировался"; continue - surge = switch_pulse_limit_a(stage.switch) - if float(peak_i[k]) > surge: - st.alive = False - st.reason = f"ступень {s_idx}: пиковый ток {float(peak_i[k]):.0f}А > импульсного предела ключа ({surge:.0f}А)" - continue - ev = float(exit_v[k]) - st.kinetic_delta_j += 0.5 * p["mass"] * (ev**2 - p["v_fire"] ** 2) - st.energy_in_j += p["energy_in"] - st.cur_v = ev - st.cur_x = p["coil_center"] + float(exit_x[k]) + _simulate_states(xp, states) records = [] for st in states: diff --git a/src/gausse/optim/evolutionary.py b/src/gausse/optim/evolutionary.py index 2733f90..4e44560 100644 --- a/src/gausse/optim/evolutionary.py +++ b/src/gausse/optim/evolutionary.py @@ -7,6 +7,7 @@ компонентов. """ +import contextlib import copy import multiprocessing import random @@ -96,13 +97,25 @@ def run_evolution( mutation_rate: float = 0.2, polish: bool = True, log_path: Path | None = None, + use_gpu: bool = False, ) -> dict: n_workers = n_workers or multiprocessing.cpu_count() rng = random.Random(seed) db = ComponentDatabase.load(data_dir) if data_dir else ComponentDatabase.load() + # GPU-режим: всё поколение оценивается ОДНИМ батчем (gpu/batch_sweep) — + # та же физика через общий build_stage_physics, фикс.шаг RK4 вместо + # адаптивного solve_ivp (сверено в тестах, ~3%). Полировка остаётся на CPU. + gpu_xp = None + backend = "cpu-pool" + if use_gpu: + from gausse.gpu.backend import get_backend + + gpu_xp, backend = get_backend(prefer_gpu=True) + + mode = "evolve" if gpu_xp is None else f"evolve-gpu({backend})" logger = ProgressLogger( - log_path or default_log_path(db_path), mode="evolve", total=n_generations * population_size + log_path or default_log_path(db_path), mode=mode, total=n_generations * population_size ) ctx = multiprocessing.get_context("spawn") @@ -115,18 +128,28 @@ def run_evolution( n_evaluated = 0 try: - with ProcessPoolExecutor( - max_workers=n_workers, - mp_context=ctx, - initializer=worker_context.init_worker, - initargs=(data_dir, bounds), - ) as executor: + executor_cm = ( + ProcessPoolExecutor( + max_workers=n_workers, + mp_context=ctx, + initializer=worker_context.init_worker, + initargs=(data_dir, bounds), + ) + if gpu_xp is None + else contextlib.nullcontext() + ) + with executor_cm as executor: for generation in range(n_generations): - pairs = list(executor.map(_evaluate_genome, population)) + if gpu_xp is not None: + from gausse.gpu.batch_sweep import evaluate_genomes_gpu + + pairs = list(zip(population, evaluate_genomes_gpu(gpu_xp, population, db, bounds))) + else: + pairs = list(executor.map(_evaluate_genome, population)) n_evaluated += len(pairs) for genome, result in pairs: - record: RunRecord = build_run_record(genome, result, db, search_mode="evolve") + record: RunRecord = build_run_record(genome, result, db, search_mode=mode) queue.put(record) logger.update(result.feasible, result.efficiency) if best_pair is None or result.fitness > best_pair[1].fitness: diff --git a/tests/test_evolutionary.py b/tests/test_evolutionary.py index 45bf6a0..965c8f8 100644 --- a/tests/test_evolutionary.py +++ b/tests/test_evolutionary.py @@ -63,3 +63,19 @@ def test_polish_does_not_make_the_best_genome_worse(): _, polished_result = polish_best(genome, DB, bounds) assert polished_result.fitness >= baseline_fitness - 1e-9 + + +def test_run_evolution_gpu_batch_mode(tmp_path): + """GPU-режим эволюции (батч-оценка поколения; здесь numpy-бэкенд): + пишет прогоны в ту же БД и находит реализуемые конфигурации.""" + from gausse.storage.database import count_runs, fetch_runs, open_connection + + db_path = tmp_path / "evo_gpu.sqlite3" + summary = run_evolution( + db_path, n_generations=3, population_size=40, seed=5, polish=False, use_gpu=True + ) + assert summary["n_evaluated"] == 120 + conn = open_connection(db_path) + assert count_runs(conn) >= 120 + rows = fetch_runs(conn, limit=5) + assert all(r.search_mode.startswith("evolve-gpu") for r in rows)