Files
Video-Tree-TRM5/app/harness/checkpoint.py
T

275 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""step 级续训 checkpoint_TrainState 可持久化字段的序列化 / 反序列化。
_TrainState 的累加包均为扁平纯数据 dataclass,经 dataclasses.asdict 序列化为
纯 JSON dict;反序列化时用 Cls(**d) 还原,其中 SystemCasePack 含嵌套 CaseSample
列表、Probation 含嵌套 RejectedEdit 列表,需逐个重建。
不持久化的字段:gate_pools / baseline_cache(各自文件级自持久化,resume 时按
指纹重载)、best_*(从 manifest best 指针读)、global_step(存 progress 块,
由 train 单独赋值)。gate_epoch_observed 持久化:warm p-hat 在 gate_pools.json
幸存,观测开关须随行,否则 resume 后阶梯排序回退冷启动序。
"""
from __future__ import annotations
import json
import os
from dataclasses import asdict
from typing import TYPE_CHECKING, Any
from app.harness.validate import Probation
from core.evolution.types import (
CaseSample,
RejectedEdit,
SystemCasePack,
ToolCasePack,
)
if TYPE_CHECKING:
from pathlib import Path
CHECKPOINT_SCHEMA_VERSION = 1
# ---------------------------------------------------------------------------
# 结构性 / 决策性指纹键
# ---------------------------------------------------------------------------
_STRUCTURAL_KEYS = (
"batch_size",
"min_class_per_batch",
"epochs",
"diag_size",
"val_size",
"batch_correct_ratio",
)
_DECISION_KEYS = (
"edit_budget_start",
"edit_budget_end",
"early_stop_patience",
"use_slow_momentum",
"skill_update_mode",
"appendix_consolidate_threshold",
"momentum_samples",
"gate_e_confirm",
"gate_e_provisional",
"gate_w_net_min",
"gate_delta_min",
"gate_lambda_dir",
"gate_e_rollback",
"gate_block",
"gate_n_max",
"gate_p_low",
"gate_p_high",
"gate_probe_quota",
"gate_gamma_decay",
"gate_cooldown_steps",
"gate_guard_err",
)
# ---------------------------------------------------------------------------
# 序列化 / 反序列化
# ---------------------------------------------------------------------------
def serialize_state(state: Any) -> dict[str, Any]:
"""把 _TrainState 的可持久化字段转为纯 JSON dict。
参数:
state: _TrainState 实例(duck-typed,仅需含可持久化字段)。
返回:
纯 JSON 可序列化的 dict,不含 gate_pools / baseline_cache /
best_* / global_step。
关键实现细节:
- changed_task_types_this_epoch 是 setJSON 无 set,故 sorted 成有序列表。
- dataclass 均经 asdict 递归转 dict(含 SystemCasePack 嵌套 CaseSample、
Probation 嵌套 RejectedEdit)。
"""
return {
"correctness": state.correctness,
"eval_prev_acc": state.eval_prev_acc,
"eval_prev_run_id": state.eval_prev_run_id,
"baseline_skills_version": state.baseline_skills_version,
"baseline_prompts_version": state.baseline_prompts_version,
"steps_since_best_improved": state.steps_since_best_improved,
"epoch_start_skills": state.epoch_start_skills,
"changed_task_types_this_epoch": sorted(state.changed_task_types_this_epoch),
"rejected_buffer": {k: [asdict(x) for x in v] for k, v in state.rejected_buffer.items()},
"system_packs": [asdict(x) for x in state.system_packs],
"tool_packs": [asdict(x) for x in state.tool_packs],
"probations": {t: asdict(p) for t, p in state.probations.items()},
"gate_cooldown": state.gate_cooldown,
"gate_epoch_observed": state.gate_epoch_observed,
}
def _restore_system_pack(d: dict[str, Any]) -> SystemCasePack:
"""还原 SystemCasePack,含嵌套 CaseSample 列表。
参数:
d: asdict(SystemCasePack) 产出的 dict。
返回:
复活的 SystemCasePackfailure_cases / success_cases 重建为 CaseSample 实例。
"""
return SystemCasePack(
stats=d["stats"],
failure_cases=[CaseSample(**c) for c in d["failure_cases"]],
success_cases=[CaseSample(**c) for c in d["success_cases"]],
)
def deserialize_state_fields(d: dict[str, Any]) -> dict[str, Any]:
"""把序列化 dict 还原为可填入 _TrainState 的字段字典(dataclass 复活)。
参数:
d: serialize_state 产出并经 JSON 往返的 dict。
返回:
字段名 -> 值的 dict,可直接铺到 _TrainState;其中各 dataclass 已复活、
changed_task_types_this_epoch 还原为 set。
关键实现细节:
- RejectedEdit / ToolCasePack 字段均为标量/dict/list[dict]Cls(**d) 直接构造。
- SystemCasePack 含嵌套 CaseSample,交由 _restore_system_pack 重建。
- Probation 含嵌套 RejectedEdit 列表(pending_edits),先重建内层再构造外层。
- 直接取 d[...] 不用 .get 兜底:serialize 后的 checkpoint 必带全部键,
缺键即 checkpoint 损坏,应硬失败(P5 不掩盖)。
"""
return {
"correctness": d["correctness"],
"eval_prev_acc": d["eval_prev_acc"],
"eval_prev_run_id": d["eval_prev_run_id"],
"baseline_skills_version": d["baseline_skills_version"],
"baseline_prompts_version": d["baseline_prompts_version"],
"steps_since_best_improved": d["steps_since_best_improved"],
"epoch_start_skills": d["epoch_start_skills"],
"changed_task_types_this_epoch": set(d["changed_task_types_this_epoch"]),
"rejected_buffer": {
k: [RejectedEdit(**x) for x in v] for k, v in d["rejected_buffer"].items()
},
"system_packs": [_restore_system_pack(x) for x in d["system_packs"]],
"tool_packs": [ToolCasePack(**x) for x in d["tool_packs"]],
"probations": {
t: Probation(
**{
**d_p,
"pending_edits": [RejectedEdit(**x) for x in d_p["pending_edits"]],
}
)
for t, d_p in d["probations"].items()
},
"gate_cooldown": d["gate_cooldown"],
"gate_epoch_observed": d["gate_epoch_observed"],
}
# ---------------------------------------------------------------------------
# 配置指纹
# ---------------------------------------------------------------------------
def compute_fingerprint(config: Any) -> dict[str, Any]:
"""采集影响训练轨迹的配置项(结构性 + 决策性)。
参数:
config: 训练配置对象(duck-typed,需含 _STRUCTURAL_KEYS + _DECISION_KEYS 属性)。
返回:
指纹 dict,键为配置项名,值为对应配置值。
"""
return {k: getattr(config, k) for k in _STRUCTURAL_KEYS + _DECISION_KEYS}
def check_fingerprint(saved: dict[str, Any], config: Any) -> tuple[list[str], list[str]]:
"""比对保存的指纹与当前配置。返回 (结构性不一致项, 决策性不一致项)。
参数:
saved: checkpoint 中保存的 config_fingerprint。
config: 当前训练配置对象。
返回:
(structural, decision) 两个不一致项名列表。
关键实现细节:
结构性不一致(batch_size/min_class_per_batch/epochs/diag_size/val_size/
batch_correct_ratio)→ 调用方应拒绝 resume;决策性不一致 → 仅告警放行。
"""
cur = compute_fingerprint(config)
structural = [k for k in _STRUCTURAL_KEYS if saved.get(k) != cur[k]]
decision = [k for k in _DECISION_KEYS if saved.get(k) != cur[k]]
return structural, decision
# ---------------------------------------------------------------------------
# 读写 checkpoint
# ---------------------------------------------------------------------------
def write_checkpoint(
workspace_dir: Path,
*,
state: Any,
epoch: int,
step_completed: int,
phase: str,
global_step: int,
total_steps: int,
version_snapshot: dict[str, str],
epoch_batches: list[list[str]],
config: Any,
) -> None:
"""原子写 checkpoint.json.tmp 再 os.replace)。
参数:
workspace_dir: workspace 目录,checkpoint.json 写入其下。
state: _TrainState 实例,交由 serialize_state 序列化。
epoch: 当前 epoch 序号。
step_completed: 本 epoch 内已完成的 step 数。
phase: 续训阶段标识(如 "in_epoch")。
global_step: 全局 step 序号。
total_steps: 全局总 step 数。
version_snapshot: skills/prompts 版本快照。
epoch_batches: 本 epoch 的 batch 划分(question_id 列表的列表)。
config: 训练配置对象,用于计算 config_fingerprint。
关键实现细节:
先写 checkpoint.json.tmp 再 os.replace,保证 checkpoint 不被写一半的中断破坏。
"""
payload = {
"schema_version": CHECKPOINT_SCHEMA_VERSION,
"progress": {
"epoch": epoch,
"step_completed": step_completed,
"phase": phase,
"global_step": global_step,
"total_steps": total_steps,
},
"version_snapshot": version_snapshot,
"epoch_batches": epoch_batches,
"config_fingerprint": compute_fingerprint(config),
"state": serialize_state(state),
}
path = workspace_dir / "checkpoint.json"
tmp = path.with_name("checkpoint.json.tmp")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
os.replace(tmp, path)
def load_checkpoint(workspace_dir: Path) -> dict[str, Any] | None:
"""读 checkpoint.json,不存在返回 None。
参数:
workspace_dir: workspace 目录。
返回:
checkpoint payload dictcheckpoint.json 不存在时返回 None。
"""
path = workspace_dir / "checkpoint.json"
if not path.exists():
return None
return json.loads(path.read_text())