Хранить только genome_json+скаляры на прогон, детали строить на лету

decoded_summary_json раньше писал полную build_detail() (геометрия, URL,
электрика по каждой ступени) на каждый из миллионов прогонов — это и
душило CPU при GPU-эволюции (пул процессов нужен был только чтобы успевать
за GPU), и разогнало базу до 70ГБ на 12.9М строк, пока диск не забился под
завязку. genome_json уже достаточен для полного воспроизведения — теперь
пишем дешёвую decoded_summary(), а run_detail() на веб-морде пересчитывает
полную физику по клику (как уже делает render.py для графиков/анимации).
This commit is contained in:
jze9
2026-07-11 21:50:54 +05:00
parent e394fd6d59
commit 5c119990b7
4 changed files with 78 additions and 42 deletions

View File

@@ -314,29 +314,31 @@ def evaluate_genomes_gpu(
def gpu_record_worker(payload) -> RunRecord:
"""Собирает RunRecord (decode + build_detail + стоимость + JSON) в процессе
пула — параллельно с GPU-оценкой следующих батчей. Тяжёлая часть записи
(детализация + сериализация) не должна душить GPU одним потоком Python.
"""Собирает RunRecord (decode + стоимость + JSON) в процессе пула —
параллельно с GPU-оценкой следующих батчей.
detail НЕ строится (build_detail дорогая — полная физика по каждой
ступени — и не нужна для хранения): genome_json уже достаточен, чтобы
воспроизвести всё через decode()+build_detail() на лету при просмотре
конкретного прогона (см. web/stats.run_detail). Пишем дешёвую
decoded_summary() — иначе на миллионах прогонов это дорогая работа,
съедающая и CPU (душит GPU), и диск (десятки ГБ на пустом месте).
Работает в воркере ProcessPoolExecutor с worker_context.init_worker
(база компонентов уже загружена в процессе).
"""
from gausse.optim import worker_context
from gausse.optim.objective import MODEL_VERSION, compute_cost_rub
from gausse.optim.objective import MODEL_VERSION, compute_cost_rub, decoded_summary
genome, outcome, search_mode = payload
db, bounds = worker_context.db, worker_context.bounds
config, ix, iv = decode(genome, db, bounds)
detail = build_detail(
config, _FEASIBLE if outcome["feasible"] else None, db, ix, iv,
genome.tube_inner_d_m, genome.tube_wall_m,
)
config, _ix, _iv = decode(genome, db, bounds)
return RunRecord(
run_id=str(uuid.uuid4()),
timestamp=datetime.now(timezone.utc).isoformat(),
search_mode=search_mode,
genome_json=json.dumps(genome_to_dict(genome)),
decoded_summary_json=json.dumps(detail, ensure_ascii=False),
decoded_summary_json=json.dumps(decoded_summary(genome, db), ensure_ascii=False),
feasible=outcome["feasible"],
model_version=MODEL_VERSION,
infeasible_reason=outcome["reason"],
@@ -396,18 +398,15 @@ def run_gpu_sweep(
def _record(st: _State, db, bounds: SearchBounds, backend, efficiency, exit_v):
from gausse.optim.objective import decoded_summary
feasible = efficiency is not None
cfg, _, _ = decode(st.genome, db, bounds)
detail = build_detail(
cfg, _FEASIBLE if feasible else None, db, st.pre.initial_x_m, st.pre.initial_v_mps,
st.genome.tube_inner_d_m, st.genome.tube_wall_m,
)
return RunRecord(
run_id=str(uuid.uuid4()),
timestamp=datetime.now(timezone.utc).isoformat(),
search_mode=f"gpu-sweep-{backend}",
genome_json=json.dumps(genome_to_dict(st.genome)),
decoded_summary_json=json.dumps(detail, ensure_ascii=False),
decoded_summary_json=json.dumps(decoded_summary(st.genome, db), ensure_ascii=False),
feasible=feasible,
model_version=MODEL_VERSION,
infeasible_reason=st.reason,

View File

@@ -61,10 +61,11 @@ def _wire_resistance_ohm(resistivity_ohm_m: float, gauge_mm: float, wire_length_
def decoded_summary(genome: Genome, db: ComponentDatabase) -> dict:
"""Краткая сводка только по геному (без результатов симуляции).
Оставлена для обратной совместимости; подробную запись со всей физикой
и результатами по каждой ступени строит `build_detail` (её и пишем в БД).
"""Краткая сводка только по геному (без результатов симуляции) — это и
пишется в decoded_summary_json для каждого прогона: genome_json уже
достаточен для полного воспроизведения через build_detail() на лету,
так что хранить дорогую подробную запись для каждого из миллионов
прогонов незачем (см. web/stats.run_detail для просмотра одного прогона).
"""
material = db.projectile_materials[genome.projectile.material_idx % len(db.projectile_materials)]
stages_summary = []
@@ -259,10 +260,15 @@ def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, i
def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> EvaluationResult:
"""detail НЕ строится здесь: build_detail — дорогая (полная физика по
каждой ступени) и не нужна ни фитнесу, ни хранению — geneome_json уже
достаточен, чтобы воспроизвести всё через decode()+build_detail() на
лету (см. web/stats.run_detail). build_run_record() кладёт в БД дешёвую
decoded_summary() вместо этого.
"""
config, initial_x_m, initial_v_mps = decode(genome, db, bounds)
cost_rub = compute_cost_rub(config, db)
result = run_coilgun(config)
detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m)
if not result.feasible:
n_stages = len(config.stages)
@@ -275,7 +281,6 @@ def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Eva
reason=result.reason,
failed_stage_index=result.failed_stage_index,
energy_breakdown={},
detail=detail,
)
energy_breakdown = {
@@ -291,7 +296,6 @@ def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Eva
efficiency=result.efficiency,
exit_velocity_mps=result.exit_v_mps,
energy_breakdown=energy_breakdown,
detail=detail,
)

View File

@@ -14,6 +14,11 @@ CREATE TABLE IF NOT EXISTS runs (
timestamp TEXT NOT NULL,
search_mode TEXT NOT NULL,
genome_json TEXT NOT NULL,
-- ДЕШЁВАЯ сводка (objective.decoded_summary) — НЕ полная физика по
-- ступеням. genome_json сам по себе достаточен, чтобы воспроизвести
-- всё через decode()+build_detail() на лету (см. web/stats.run_detail);
-- хранить дорогую детализацию для каждого из миллионов прогонов
-- бессмысленно — так база росла на пустом месте (70ГБ на 12.9М строк).
decoded_summary_json TEXT NOT NULL,
feasible INTEGER NOT NULL,
infeasible_reason TEXT,

View File

@@ -72,11 +72,11 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di
def _summarize(rows):
out = []
for r in rows:
detail = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {}
summary = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {}
out.append({
"run_id": r.run_id, "search_mode": r.search_mode, "efficiency": r.efficiency,
"exit_velocity_mps": r.exit_velocity_mps, "cost_rub": r.cost_rub,
"n_stages": detail.get("n_stages"), "timestamp": r.timestamp,
"n_stages": len(summary.get("stages", [])), "timestamp": r.timestamp,
})
return out
@@ -115,11 +115,41 @@ def _last_cycle_best(log_lines: list[str]) -> float | None:
def run_detail(db_path: Path, run_id: str) -> dict | None:
"""Подробности одного прогона — НЕ читаются из БД (там только дешёвая
decoded_summary), а пересчитываются на лету из genome_json тем же
decode()+build_detail(), что и остальной рендер веб-морды (см.
web/render.py). Это единичный запрос по клику, не миллионы прогонов —
пересимулировать дешевле, чем хранить полную физику для каждого."""
from gausse.components.database import ComponentDatabase
from gausse.optim.objective import build_detail
from gausse.optim.search_space import SearchBounds, decode, genome_from_dict
from gausse.sim.coilgun import run_coilgun
conn = open_connection(db_path)
try:
row = fetch_run_by_id(conn, run_id)
finally:
conn.close()
if row is None:
return None
genome = genome_from_dict(json.loads(row.genome_json))
db = ComponentDatabase.load()
bounds = SearchBounds()
config, initial_x_m, initial_v_mps = decode(genome, db, bounds)
result = run_coilgun(config)
detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m)
energy_breakdown = (
{
"total_energy_in_j": result.total_energy_in_j,
"total_energy_dissipated_j": result.total_energy_dissipated_j,
"total_kinetic_energy_delta_j": result.total_kinetic_energy_delta_j,
"exit_kinetic_energy_j": result.exit_kinetic_energy_j,
}
if result.feasible
else {}
)
return {
"run_id": row.run_id,
"search_mode": row.search_mode,
@@ -132,8 +162,6 @@ def run_detail(db_path: Path, run_id: str) -> dict | None:
"exit_velocity_mps": row.exit_velocity_mps,
"cost_rub": row.cost_rub,
"timestamp": row.timestamp,
"detail": json.loads(row.decoded_summary_json) if row.decoded_summary_json else {},
"energy_breakdown": json.loads(row.energy_breakdown_json) if row.energy_breakdown_json else {},
"detail": detail,
"energy_breakdown": energy_breakdown,
}
finally:
conn.close()