feat(harness): checkpoint.py — TrainState 序列化 + 原子写 + 指纹校验
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
"""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, dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from core.evolution.types import (
|
||||
CaseSample,
|
||||
RejectedEdit,
|
||||
SystemCasePack,
|
||||
ToolCasePack,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
CHECKPOINT_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Probation 类型(Task 10 validate.py 尚未就绪,暂定义在此供 checkpoint 使用)
|
||||
# Task 10 完成后迁移至 app/harness/validate.py 并改为 re-export。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class Probation:
|
||||
"""一个题型的在途试用账本(每题型至多一个)。
|
||||
|
||||
属性:
|
||||
task_type: 题型。
|
||||
anchor_skills_version: 锚版本名(最近一个 CONFIRMED 的 skills 版本)。
|
||||
target_file: 该题型解析后的 skill 文件名。
|
||||
correctness_snapshot: 开账时该题型 val 题的对错快照(回滚时恢复)。
|
||||
opened_step: 开账时的 global_step(观测用)。
|
||||
pending_edits: 试用链上全部候选 edit 的黑名单素材(回滚时整链入黑名单)。
|
||||
"""
|
||||
|
||||
task_type: str
|
||||
anchor_skills_version: str
|
||||
target_file: str
|
||||
correctness_snapshot: dict[str, bool]
|
||||
opened_step: int
|
||||
pending_edits: list[RejectedEdit] = field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 结构性 / 决策性指纹键
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_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 是 set,JSON 无 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。
|
||||
|
||||
返回:
|
||||
复活的 SystemCasePack;failure_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 dict;checkpoint.json 不存在时返回 None。
|
||||
"""
|
||||
path = workspace_dir / "checkpoint.json"
|
||||
if not path.exists():
|
||||
return None
|
||||
return json.loads(path.read_text())
|
||||
Reference in New Issue
Block a user