GPU в эволюции: оценка поколения одним батчем (use_gpu=True)

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 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-08 03:22:47 +05:00
parent ecd6508f29
commit ef83512601
3 changed files with 152 additions and 52 deletions

View File

@@ -49,6 +49,7 @@ class _State:
kinetic_delta_j: float = 0.0 kinetic_delta_j: float = 0.0
alive: bool = True alive: bool = True
reason: str | None = None 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: 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 }, 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( def run_gpu_sweep(
db_path: Path, db_path: Path,
n_runs: int, n_runs: int,
@@ -131,49 +234,7 @@ def run_gpu_sweep(
centers.append(centers[-1] + gap) centers.append(centers[-1] + gap)
states.append(_State(g, cfg, ix, iv, centers, cur_x=ix, cur_v=iv)) 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) _simulate_states(xp, 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])
records = [] records = []
for st in states: for st in states:

View File

@@ -7,6 +7,7 @@
компонентов. компонентов.
""" """
import contextlib
import copy import copy
import multiprocessing import multiprocessing
import random import random
@@ -96,13 +97,25 @@ def run_evolution(
mutation_rate: float = 0.2, mutation_rate: float = 0.2,
polish: bool = True, polish: bool = True,
log_path: Path | None = None, log_path: Path | None = None,
use_gpu: bool = False,
) -> dict: ) -> dict:
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()
# 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( 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") ctx = multiprocessing.get_context("spawn")
@@ -115,18 +128,28 @@ def run_evolution(
n_evaluated = 0 n_evaluated = 0
try: try:
with ProcessPoolExecutor( executor_cm = (
max_workers=n_workers, ProcessPoolExecutor(
mp_context=ctx, max_workers=n_workers,
initializer=worker_context.init_worker, mp_context=ctx,
initargs=(data_dir, bounds), initializer=worker_context.init_worker,
) as executor: initargs=(data_dir, bounds),
)
if gpu_xp is None
else contextlib.nullcontext()
)
with executor_cm as executor:
for generation in range(n_generations): 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) n_evaluated += len(pairs)
for genome, result in 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) 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:

View File

@@ -63,3 +63,19 @@ def test_polish_does_not_make_the_best_genome_worse():
_, polished_result = polish_best(genome, DB, bounds) _, polished_result = polish_best(genome, DB, bounds)
assert polished_result.fitness >= baseline_fitness - 1e-9 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)