feat(harness): add task_types, pool_split_mode, train_ratio, test_questions to RunConfig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:46:28 -04:00
parent 73ae1f7143
commit e5b07ac974
2 changed files with 101 additions and 10 deletions
+28 -10
View File
@@ -18,6 +18,7 @@ import yaml
_VALID_MODES = {"infer", "train", "diagnose", "evolve", "eval", "promote"} _VALID_MODES = {"infer", "train", "diagnose", "evolve", "eval", "promote"}
_VALID_SKILL_MODES = {"auto", "manual", "none"} _VALID_SKILL_MODES = {"auto", "manual", "none"}
_VALID_SKILL_UPDATE_MODES = {"patch", "rewrite"} _VALID_SKILL_UPDATE_MODES = {"patch", "rewrite"}
_VALID_POOL_SPLIT_MODES = {"global", "per_category"}
_PATH_FIELDS = {"workspace_dir", "store_dir"} _PATH_FIELDS = {"workspace_dir", "store_dir"}
# Video-MME 的任务类型数量:验证池每类至少保底 eval_min_per_class 题,共 12 类。 # Video-MME 的任务类型数量:验证池每类至少保底 eval_min_per_class 题,共 12 类。
@@ -85,6 +86,10 @@ class RunConfig:
version: eval/promote 模式指定的 store 版本号(如 "v3")。 version: eval/promote 模式指定的 store 版本号(如 "v3")。
resume: train 模式是否从已有 checkpoint 续训。 resume: train 模式是否从已有 checkpoint 续训。
fresh: train 模式是否从种子全新开始。 fresh: train 模式是否从种子全新开始。
task_types: 限定参与的任务类型子集,None 表示全部。
pool_split_mode: 池划分策略,"global"(全局统一划分)/ "per_category"(按类别独立划分)。
train_ratio: 训练集占比,范围 (0, 1)。
test_questions: 测试题目集路径(相对路径)。
""" """
# ── 必填字段(无默认值,来自 YAML 或 CLI) ── # ── 必填字段(无默认值,来自 YAML 或 CLI) ──
@@ -136,6 +141,10 @@ class RunConfig:
version: str = "" version: str = ""
resume: bool = False resume: bool = False
fresh: bool = False fresh: bool = False
task_types: tuple[str, ...] | None = None
pool_split_mode: str = "global"
train_ratio: float = 0.667
test_questions: str = "benchmarks/Video-MME"
def _validate(config: RunConfig) -> None: def _validate(config: RunConfig) -> None:
@@ -236,6 +245,13 @@ def _validate_basic(config: RunConfig) -> None:
f"appendix_consolidate_threshold 必须 >= 1" f"appendix_consolidate_threshold 必须 >= 1"
f"实际: {config.appendix_consolidate_threshold}" f"实际: {config.appendix_consolidate_threshold}"
) )
if config.pool_split_mode not in _VALID_POOL_SPLIT_MODES:
raise ValueError(
f"pool_split_mode 必须为 {_VALID_POOL_SPLIT_MODES} 之一,"
f"实际: {config.pool_split_mode!r}"
)
if not (0 < config.train_ratio < 1):
raise ValueError(f"train_ratio 必须在 (0, 1) 内,实际: {config.train_ratio}")
def _validate_edit_budget(config: RunConfig) -> None: def _validate_edit_budget(config: RunConfig) -> None:
@@ -266,8 +282,9 @@ def _validate_minibatch(config: RunConfig) -> None:
ValueError: 任一约束被违反。 ValueError: 任一约束被违反。
关键实现细节: 关键实现细节:
val_size 必须 >= eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT,保证验证池 pool_split_mode != "per_category" 时,val_size 必须 >= eval_min_per_class *
能为 Video-MME 的全部 12 个任务类型各保底 eval_min_per_class 题。 _VIDEO_MME_TASK_TYPE_COUNT,保证验证池能为 Video-MME 的全部 12 个任务类型
各保底 eval_min_per_class 题。per_category 模式下跳过此硬编码 12 类保底检查。
""" """
if config.batch_size <= 0: if config.batch_size <= 0:
raise ValueError(f"batch_size 必须 > 0,实际: {config.batch_size}") raise ValueError(f"batch_size 必须 > 0,实际: {config.batch_size}")
@@ -278,14 +295,15 @@ def _validate_minibatch(config: RunConfig) -> None:
) )
if config.eval_min_per_class < 1: if config.eval_min_per_class < 1:
raise ValueError(f"eval_min_per_class 必须 >= 1,实际: {config.eval_min_per_class}") 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.pool_split_mode != "per_category":
if config.val_size < floor: floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
raise ValueError( if config.val_size < floor:
f"val_size 必须 >= eval_min_per_class * {_VIDEO_MME_TASK_TYPE_COUNT}" raise ValueError(
f"(={floor})Video-MME 共 {_VIDEO_MME_TASK_TYPE_COUNT} 个任务类型," f"val_size 必须 >= eval_min_per_class * {_VIDEO_MME_TASK_TYPE_COUNT}"
f"每类需 eval_min_per_class 题保底,故验证池下限为 {floor}" f"(={floor})Video-MME 共 {_VIDEO_MME_TASK_TYPE_COUNT} 个任务类型"
f"实际: {config.val_size}" f"每类需 eval_min_per_class 题保底,故验证池下限为 {floor}"
) f"实际: {config.val_size}"
)
if config.early_stop_patience <= 0: if config.early_stop_patience <= 0:
raise ValueError(f"early_stop_patience 必须 > 0,实际: {config.early_stop_patience}") raise ValueError(f"early_stop_patience 必须 > 0,实际: {config.early_stop_patience}")
if config.test_size <= 0: if config.test_size <= 0:
+73
View File
@@ -600,3 +600,76 @@ def test_video_mme_task_type_count_is_12():
from app.harness.config import _VIDEO_MME_TASK_TYPE_COUNT from app.harness.config import _VIDEO_MME_TASK_TYPE_COUNT
assert _VIDEO_MME_TASK_TYPE_COUNT == 12 assert _VIDEO_MME_TASK_TYPE_COUNT == 12
# ────────────────────── test_run_config_new_fields ─────────────────────────
class TestRunConfigNewFields:
"""RunConfig 新增字段校验(task_types / pool_split_mode / train_ratio / test_questions)。"""
def test_pool_split_mode_valid(self) -> None:
"""pool_split_mode 合法值应通过校验。"""
for mode in ("global", "per_category"):
cfg = _make_config(pool_split_mode=mode)
_validate(cfg)
def test_pool_split_mode_invalid(self) -> None:
"""pool_split_mode 非法值应报错。"""
cfg = _make_config(pool_split_mode="invalid")
with pytest.raises(ValueError, match="pool_split_mode"):
_validate(cfg)
def test_task_types_tuple(self) -> None:
"""task_types 接受 tuple。"""
cfg = _make_config(task_types=("short", "medium"))
_validate(cfg)
assert cfg.task_types == ("short", "medium")
def test_task_types_none(self) -> None:
"""task_types 默认 None。"""
cfg = _make_config()
assert cfg.task_types is None
def test_train_ratio_valid(self) -> None:
"""train_ratio 在 (0, 1) 内应通过。"""
cfg = _make_config(train_ratio=0.5)
_validate(cfg)
def test_train_ratio_zero_rejected(self) -> None:
"""train_ratio <= 0 应报错。"""
cfg = _make_config(train_ratio=0.0)
with pytest.raises(ValueError, match="train_ratio"):
_validate(cfg)
def test_train_ratio_one_rejected(self) -> None:
"""train_ratio >= 1 应报错。"""
cfg = _make_config(train_ratio=1.0)
with pytest.raises(ValueError, match="train_ratio"):
_validate(cfg)
def test_train_ratio_negative_rejected(self) -> None:
"""train_ratio < 0 应报错。"""
cfg = _make_config(train_ratio=-0.1)
with pytest.raises(ValueError, match="train_ratio"):
_validate(cfg)
def test_test_questions_default(self) -> None:
"""test_questions 默认值为 benchmarks/Video-MME。"""
cfg = _make_config()
assert cfg.test_questions == "benchmarks/Video-MME"
def test_per_category_skips_val_size_check(self) -> None:
"""per_category 模式跳过 val_size >= eval_min_per_class * 12 的校验。
val_size=1 在 global 模式会因低于 floor 而报错,
但 per_category 模式应跳过该检查。
"""
cfg = _make_config(pool_split_mode="per_category", val_size=1)
_validate(cfg)
def test_global_mode_enforces_val_size_check(self) -> None:
"""global 模式仍然执行 val_size floor 检查。"""
cfg = _make_config(pool_split_mode="global", eval_min_per_class=3, val_size=30)
with pytest.raises(ValueError, match="val_size"):
_validate(cfg)