Хранить только 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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user