ce43871828
- _validate_mode_deps: 恢复 train 非 resume/fresh 时必须提供 run_id 校验 - 提取 _validate_train_run_id 用 early return 展平条件,避免 radon Grade C - 合并 promote run_id 检查到 diagnose/evolve/promote 统一检查 - 新增 4 个测试:train+run_id / train+resume / train+fresh / train+baseline - radon cc -n C 无输出(全部 Grade B 或更好) - 74 个单元测试全部通过 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
425 lines
16 KiB
Python
425 lines
16 KiB
Python
"""运行配置:RunConfig frozen dataclass 与 YAML + CLI + .env 三层加载。
|
||
|
||
三层合并优先级:CLI > .env > YAML(遵循 CLAUDE.md §4.5 配置管理规范)。
|
||
- YAML:科研实验配置(会在实验中反复扫动的参数),存放于 config/ 下。
|
||
- .env:工程配置(少变路径如 workspace_dir、store_dir),通过环境变量注入。
|
||
- CLI:单次临时覆盖。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import dataclasses
|
||
import os
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
import yaml
|
||
|
||
_VALID_MODES = {"infer", "train", "diagnose", "evolve", "eval", "promote"}
|
||
_VALID_SKILL_MODES = {"auto", "manual", "none"}
|
||
_VALID_SKILL_UPDATE_MODES = {"patch", "rewrite"}
|
||
_PATH_FIELDS = {"workspace_dir", "store_dir"}
|
||
|
||
# Video-MME 的任务类型数量:验证池每类至少保底 eval_min_per_class 题,共 11 类。
|
||
_VIDEO_MME_TASK_TYPE_COUNT = 11
|
||
|
||
# .env 工程配置字段映射(环境变量名 → RunConfig 字段名)。
|
||
# 仅路径类工程配置走 .env,科研实验参数走 YAML。
|
||
_ENV_FIELD_MAP: dict[str, str] = {
|
||
"HARNESS_WORKSPACE_DIR": "workspace_dir",
|
||
"HARNESS_STORE_DIR": "store_dir",
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RunConfig:
|
||
"""实验运行配置,所有参数的唯一归口。
|
||
|
||
frozen=True 确保配置在创建后不可变,防止运行中被意外修改。
|
||
三层合并优先级:CLI > .env > YAML。
|
||
|
||
字段:
|
||
workspace_dir: Workspace 根目录。
|
||
store_dir: Store 根目录。
|
||
mode: 运行模式,"infer" / "train" / "diagnose" / "evolve" / "eval" / "promote"。
|
||
concurrency: 并行 worker 数。
|
||
max_steps: AgentLoop 单题最大步数。
|
||
skill_mode: Skill 加载模式,"auto" / "manual" / "none"。
|
||
n_samples: 题目截取数,0 表示全量。
|
||
questions: 题目在 questions/ 下的相对路径。
|
||
skills_version: Skills 版本号。
|
||
prompts_version: Prompts 版本号。
|
||
epochs: 训练轮数。
|
||
diag_size: 诊断池题目数。
|
||
diag_correct_ratio: 诊断池中正确题目占比。
|
||
val_size: 验证池题目数。
|
||
val_correct_ratio: 验证池中正确题目占比。
|
||
edit_budget_start: 编辑预算前期上限。
|
||
edit_budget_end: 编辑预算后期下限。
|
||
batch_size: mini-batch 单批题目数。
|
||
min_class_per_batch: 单批中每个任务类型至少保留的题目数(< batch_size)。
|
||
eval_min_per_class: 验证池中每个任务类型至少保底的题目数。
|
||
early_stop_patience: 全局 best 连续未提升的容忍轮数,达到即早停。
|
||
test_size: held-out 测试池题目数。
|
||
use_slow_momentum: 是否启用快慢双速进化中的慢速 momentum 更新。
|
||
gate_e_confirm: CE-Gate CONFIRMED 接受的 e 值门槛(1/alpha,Ville 界假阳率 alpha)。
|
||
gate_e_provisional: 题尽暂定接受门 + futility 提前止损的代数界。
|
||
gate_w_net_min: 题尽暂定接受要求的最小净胜数(win - loss)。
|
||
gate_delta_min: 最小点估计效应量下限(承接旧 margin 语义)。
|
||
gate_lambda_dir: Wald 方向拒绝的对数似然比阈值(必须为负)。
|
||
gate_e_rollback: 试用期对称回滚门(回滚 e 值门槛)。
|
||
gate_block: 块序贯验证的块大小(=推理并发度,块内跑满)。
|
||
gate_n_max: 单次 gate 消耗的题数上限。
|
||
gate_p_low: 信息量阶梯 p-hat 保留区间下界(剔除必错零信息题)。
|
||
gate_p_high: 信息量阶梯 p-hat 保留区间上界(剔除必对零信息题)。
|
||
gate_probe_quota: 冷启动探针集比例(全错题中插尾的比例)。
|
||
gate_gamma_decay: 逐题正确率估计 p-hat 的 EMA 衰减系数。
|
||
gate_cooldown_steps: 回滚后该题型跳过进化的冷却 step 数。
|
||
gate_guard_err: gate 内跨块累计 INFRA 错误率护栏。
|
||
skill_update_mode: skill 进化模式,"patch"(局部 edit)/ "rewrite"(整篇重写)。
|
||
appendix_consolidate_threshold: appendix note 条数达此值触发 LLM consolidation。
|
||
run_id: diagnose/evolve 模式要分析的运行 ID,默认空字符串。
|
||
batch_correct_ratio: 单批中正确题目占比,范围 [0, 1)。
|
||
momentum_samples: 慢速 momentum 更新时从诊断池采样的题目数,必须 >= 1。
|
||
seed: fresh 训练的种子名(对应 seed.json),默认 "initial"。
|
||
version: eval/promote 模式指定的 store 版本号(如 "v3")。
|
||
resume: train 模式是否从已有 checkpoint 续训。
|
||
fresh: train 模式是否从种子全新开始。
|
||
"""
|
||
|
||
# ── 必填字段(无默认值,来自 YAML 或 CLI) ──
|
||
workspace_dir: Path
|
||
store_dir: Path
|
||
mode: str
|
||
concurrency: int
|
||
max_steps: int
|
||
skill_mode: str
|
||
n_samples: int
|
||
questions: str
|
||
skills_version: str
|
||
prompts_version: str
|
||
epochs: int
|
||
diag_size: int
|
||
diag_correct_ratio: float
|
||
val_size: int
|
||
val_correct_ratio: float
|
||
edit_budget_start: int
|
||
edit_budget_end: int
|
||
batch_size: int
|
||
min_class_per_batch: int
|
||
eval_min_per_class: int
|
||
early_stop_patience: int
|
||
test_size: int
|
||
use_slow_momentum: bool
|
||
gate_e_confirm: float
|
||
gate_e_provisional: float
|
||
gate_w_net_min: int
|
||
gate_delta_min: float
|
||
gate_lambda_dir: float
|
||
gate_e_rollback: float
|
||
gate_block: int
|
||
gate_n_max: int
|
||
gate_p_low: float
|
||
gate_p_high: float
|
||
gate_probe_quota: float
|
||
gate_gamma_decay: float
|
||
gate_cooldown_steps: int
|
||
gate_guard_err: float
|
||
skill_update_mode: str
|
||
appendix_consolidate_threshold: int
|
||
|
||
# ── 有默认值的字段(通常由 CLI 传入或可选) ──
|
||
run_id: str = ""
|
||
batch_correct_ratio: float = 0.5
|
||
momentum_samples: int = 20
|
||
seed: str = "initial"
|
||
version: str = ""
|
||
resume: bool = False
|
||
fresh: bool = False
|
||
|
||
|
||
def _validate(config: RunConfig) -> None:
|
||
"""校验 RunConfig 全部字段约束。
|
||
|
||
六层校验链:mode → 基础标量 → 编辑预算 → mini-batch → gate 阈值 → gate 阶梯。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: 任一字段值不合法。
|
||
"""
|
||
_validate_mode(config)
|
||
_validate_mode_deps(config)
|
||
_validate_basic(config)
|
||
_validate_edit_budget(config)
|
||
_validate_minibatch(config)
|
||
_validate_gate(config)
|
||
|
||
|
||
def _validate_mode(config: RunConfig) -> None:
|
||
"""校验运行模式枚举合法性。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: mode 值不在合法集合中。
|
||
"""
|
||
if config.mode not in _VALID_MODES:
|
||
raise ValueError(f"mode 必须为 {_VALID_MODES} 之一,实际: {config.mode!r}")
|
||
|
||
|
||
def _validate_mode_deps(config: RunConfig) -> None:
|
||
"""校验各运行模式的依赖字段(run_id、version)。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: 模式依赖字段缺失。
|
||
"""
|
||
if config.mode in ("diagnose", "evolve", "promote") and not config.run_id:
|
||
raise ValueError(f"mode 为 {config.mode!r} 时必须提供 run_id。")
|
||
if config.mode in ("eval", "promote") and not config.version:
|
||
raise ValueError(f"mode 为 {config.mode!r} 时必须提供 --version。")
|
||
_validate_train_run_id(config)
|
||
|
||
|
||
def _validate_train_run_id(config: RunConfig) -> None:
|
||
"""校验 train 模式非 resume/fresh 时必须提供 run_id。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: train 模式既非 resume 也非 fresh 且缺少 run_id。
|
||
"""
|
||
if config.mode != "train":
|
||
return
|
||
if config.resume or config.fresh:
|
||
return
|
||
if not config.run_id:
|
||
raise ValueError("train 非 resume/fresh 时必须提供 run_id(旧式基线 run)。")
|
||
|
||
|
||
def _validate_basic(config: RunConfig) -> None:
|
||
"""校验基础标量字段:枚举合法性与正整数约束。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: 任一基础字段值不合法。
|
||
"""
|
||
if config.skill_mode not in _VALID_SKILL_MODES:
|
||
raise ValueError(
|
||
f"skill_mode 必须为 {_VALID_SKILL_MODES} 之一,实际: {config.skill_mode!r}"
|
||
)
|
||
if config.concurrency <= 0:
|
||
raise ValueError(f"concurrency 必须 > 0,实际: {config.concurrency}")
|
||
if config.max_steps <= 0:
|
||
raise ValueError(f"max_steps 必须 > 0,实际: {config.max_steps}")
|
||
if config.n_samples < 0:
|
||
raise ValueError(f"n_samples 必须 >= 0,实际: {config.n_samples}")
|
||
if config.epochs <= 0:
|
||
raise ValueError(f"epochs 必须 > 0,实际: {config.epochs}")
|
||
if config.skill_update_mode not in _VALID_SKILL_UPDATE_MODES:
|
||
raise ValueError(
|
||
f"skill_update_mode 必须为 {_VALID_SKILL_UPDATE_MODES} 之一,"
|
||
f"实际: {config.skill_update_mode!r}"
|
||
)
|
||
if config.appendix_consolidate_threshold < 1:
|
||
raise ValueError(
|
||
f"appendix_consolidate_threshold 必须 >= 1,"
|
||
f"实际: {config.appendix_consolidate_threshold}"
|
||
)
|
||
|
||
|
||
def _validate_edit_budget(config: RunConfig) -> None:
|
||
"""校验编辑预算退火的前期/后期上限约束。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: edit_budget_start < edit_budget_end,或 end <= 0。
|
||
"""
|
||
if config.edit_budget_start < config.edit_budget_end:
|
||
raise ValueError(
|
||
f"edit_budget_start({config.edit_budget_start}) 必须 >= "
|
||
f"edit_budget_end({config.edit_budget_end})"
|
||
)
|
||
if config.edit_budget_end <= 0:
|
||
raise ValueError(f"edit_budget_end 必须 > 0,实际: {config.edit_budget_end}")
|
||
|
||
|
||
def _validate_minibatch(config: RunConfig) -> None:
|
||
"""校验 mini-batch 自进化闭环参数约束。
|
||
|
||
参数:
|
||
config: 待校验的 RunConfig 配置对象。
|
||
|
||
异常:
|
||
ValueError: 任一约束被违反。
|
||
|
||
关键实现细节:
|
||
val_size 必须 >= eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT,保证验证池
|
||
能为 Video-MME 的全部 11 个任务类型各保底 eval_min_per_class 题。
|
||
"""
|
||
if config.batch_size <= 0:
|
||
raise ValueError(f"batch_size 必须 > 0,实际: {config.batch_size}")
|
||
if not (1 <= config.min_class_per_batch < config.batch_size):
|
||
raise ValueError(
|
||
f"min_class_per_batch 必须满足 1 <= 值 < batch_size"
|
||
f"({config.batch_size}),实际: {config.min_class_per_batch}"
|
||
)
|
||
if config.eval_min_per_class < 1:
|
||
raise ValueError(f"eval_min_per_class 必须 >= 1,实际: {config.eval_min_per_class}")
|
||
floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
|
||
if config.val_size < floor:
|
||
raise ValueError(
|
||
f"val_size 必须 >= eval_min_per_class * {_VIDEO_MME_TASK_TYPE_COUNT}"
|
||
f"(={floor}):Video-MME 共 {_VIDEO_MME_TASK_TYPE_COUNT} 个任务类型,"
|
||
f"每类需 eval_min_per_class 题保底,故验证池下限为 {floor},"
|
||
f"实际: {config.val_size}"
|
||
)
|
||
if config.early_stop_patience <= 0:
|
||
raise ValueError(f"early_stop_patience 必须 > 0,实际: {config.early_stop_patience}")
|
||
if config.test_size <= 0:
|
||
raise ValueError(f"test_size 必须 > 0,实际: {config.test_size}")
|
||
if not (0 <= config.batch_correct_ratio < 1):
|
||
raise ValueError(
|
||
f"batch_correct_ratio 必须满足 0 <= 值 < 1,实际: {config.batch_correct_ratio}"
|
||
)
|
||
if config.momentum_samples < 1:
|
||
raise ValueError(f"momentum_samples 必须 >= 1,实际: {config.momentum_samples}")
|
||
|
||
|
||
def _validate_gate(config: RunConfig) -> None:
|
||
"""校验 CE-Gate 全部参数:判据阈值 + 信息量阶梯。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: 任一 gate 参数不合法。
|
||
"""
|
||
_validate_gate_thresholds(config)
|
||
_validate_gate_ladder(config)
|
||
|
||
|
||
def _validate_gate_thresholds(config: RunConfig) -> None:
|
||
"""校验 CE-Gate 判据阈值参数(e 值、净胜数、效应量、方向拒绝)。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: 任一阈值参数不合法。
|
||
"""
|
||
if config.gate_e_confirm <= 1:
|
||
raise ValueError(f"gate_e_confirm 必须 > 1,实际: {config.gate_e_confirm}")
|
||
if not (1 < config.gate_e_provisional <= config.gate_e_confirm):
|
||
raise ValueError(
|
||
f"gate_e_provisional 必须在 (1, gate_e_confirm] 内,实际: {config.gate_e_provisional}"
|
||
)
|
||
if config.gate_e_rollback <= 1:
|
||
raise ValueError(f"gate_e_rollback 必须 > 1,实际: {config.gate_e_rollback}")
|
||
if config.gate_w_net_min < 1:
|
||
raise ValueError(f"gate_w_net_min 必须 >= 1,实际: {config.gate_w_net_min}")
|
||
if config.gate_delta_min < 0:
|
||
raise ValueError(f"gate_delta_min 必须 >= 0,实际: {config.gate_delta_min}")
|
||
if config.gate_lambda_dir >= 0:
|
||
raise ValueError(f"gate_lambda_dir 必须 < 0,实际: {config.gate_lambda_dir}")
|
||
|
||
|
||
def _validate_gate_ladder(config: RunConfig) -> None:
|
||
"""校验 CE-Gate 信息量阶梯与块序贯参数。
|
||
|
||
参数:
|
||
config: 待校验的配置实例。
|
||
|
||
异常:
|
||
ValueError: 任一阶梯参数不合法。
|
||
"""
|
||
if config.gate_block <= 0 or config.gate_n_max < config.gate_block:
|
||
raise ValueError(
|
||
f"需 0 < gate_block <= gate_n_max,"
|
||
f"实际: block={config.gate_block}, n_max={config.gate_n_max}"
|
||
)
|
||
if not (0 <= config.gate_p_low < config.gate_p_high <= 1):
|
||
raise ValueError(
|
||
f"需 0 <= gate_p_low < gate_p_high <= 1,"
|
||
f"实际: [{config.gate_p_low}, {config.gate_p_high}]"
|
||
)
|
||
if not (0 <= config.gate_probe_quota <= 1):
|
||
raise ValueError(f"gate_probe_quota 须在 [0,1],实际: {config.gate_probe_quota}")
|
||
if not (0 < config.gate_gamma_decay < 1):
|
||
raise ValueError(f"gate_gamma_decay 须在 (0,1),实际: {config.gate_gamma_decay}")
|
||
if config.gate_cooldown_steps < 1:
|
||
raise ValueError(f"gate_cooldown_steps 必须 >= 1,实际: {config.gate_cooldown_steps}")
|
||
if not (0 < config.gate_guard_err < 1):
|
||
raise ValueError(f"gate_guard_err 须在 (0,1),实际: {config.gate_guard_err}")
|
||
|
||
|
||
def _apply_env_overrides(data: dict) -> None:
|
||
"""将 .env 工程配置环境变量覆盖到配置字典中(原地修改)。
|
||
|
||
仅覆盖 _ENV_FIELD_MAP 中声明的工程配置字段(workspace_dir、store_dir)。
|
||
|
||
参数:
|
||
data: 待覆盖的配置字典。
|
||
"""
|
||
for env_key, field_name in _ENV_FIELD_MAP.items():
|
||
env_val = os.environ.get(env_key)
|
||
if env_val is not None:
|
||
data[field_name] = env_val
|
||
|
||
|
||
def load_config(
|
||
yaml_path: Path,
|
||
cli_overrides: dict[str, object] | None = None,
|
||
) -> RunConfig:
|
||
"""从 YAML 加载配置,叠加 .env 和 CLI 覆盖层后构造 RunConfig。
|
||
|
||
三层合并优先级:CLI > .env > YAML。
|
||
|
||
参数:
|
||
yaml_path: YAML 配置文件路径,需包含 ``harness`` 段。
|
||
cli_overrides: CLI 参数字典,值为 None 表示未传入(不覆盖)。
|
||
|
||
返回:
|
||
构造并校验后的 RunConfig 实例。
|
||
|
||
异常:
|
||
FileNotFoundError: YAML 文件不存在。
|
||
ValueError: 校验失败。
|
||
"""
|
||
# Phase 1: 加载 YAML 基础层
|
||
with open(yaml_path, encoding="utf-8") as f:
|
||
raw: dict = yaml.safe_load(f)
|
||
|
||
# 支持嵌套 harness 段和扁平 YAML 两种格式
|
||
yaml_data: dict = raw.get("harness", raw)
|
||
|
||
# Phase 2: .env 覆盖层(仅工程配置字段)
|
||
_apply_env_overrides(yaml_data)
|
||
|
||
# Phase 3: CLI 覆盖层(最高优先级)
|
||
valid_fields = {f.name for f in dataclasses.fields(RunConfig)}
|
||
if cli_overrides:
|
||
for key, value in cli_overrides.items():
|
||
if value is not None and key in valid_fields:
|
||
yaml_data[key] = value
|
||
|
||
# Phase 4: 类型转换 — 路径字段转 Path
|
||
for field_name in _PATH_FIELDS:
|
||
if field_name in yaml_data:
|
||
yaml_data[field_name] = Path(yaml_data[field_name])
|
||
|
||
# Phase 5: 构造并校验
|
||
config = RunConfig(**{k: v for k, v in yaml_data.items() if k in valid_fields})
|
||
_validate(config)
|
||
return config
|