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

302 lines
11 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, 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 是 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())