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:
@@ -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:
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
ProcessPoolExecutor(
|
||||||
max_workers=n_workers,
|
max_workers=n_workers,
|
||||||
mp_context=ctx,
|
mp_context=ctx,
|
||||||
initializer=worker_context.init_worker,
|
initializer=worker_context.init_worker,
|
||||||
initargs=(data_dir, bounds),
|
initargs=(data_dir, bounds),
|
||||||
) as executor:
|
)
|
||||||
|
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):
|
||||||
|
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))
|
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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user