Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/codex_home_example/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
- 用户要求查看 Judger 过程或评测明细时,优先读取 `judger.pkl` 事件流
- `train` 优先使用 Trainer Skill
- SFT 必须使用 `train_stage=sft, train_framework=llamafactory`;GRPO 必须使用 `train_stage=grpo, train_framework=verl`,不要交叉组合
- Verl GRPO 默认使用 Conda 环境 `verl`,输入必须是包含 `prompt`、`data_source`、`reward_model` 的训练/验证 Parquet;优先使用 `auto` 或经过用户确认的 LoopAI Reward 预设,只有预设无法覆盖任务时才使用自定义 reward Python 文件
- Verl GRPO 默认使用 Conda 环境 `verl`;原生输入使用包含 `prompt`、`data_source`、`reward_model` 的 Parquet,Constructor 生成的 JSON/JSONL 则交给 Trainer 在 `prepare()` 内转换、切分并记录 manifest,不要要求 Constructor 输出 Verl 专用格式;优先使用 `auto` 或经过用户确认的 LoopAI Reward 预设,无法可靠识别时必须请用户指定,不得猜测 reward 或 ground truth
- Verl 多轮默认继承上一轮已确认 YAML 的训练超参和成功导出的最佳 Hugging Face 模型,但每轮必须刷新当前数据、输出目录和 version,并重新展示完整 YAML 取得确认
- `obtain`、训练前数据获取、SFT 数据集构造、能力定向提升数据规划,优先读取 Obtainer Skill:`skills/obtainer/SKILL.md`
- 涉及 DataMixer、数据湖入湖、SFT recipe/export、按 math/code/text2sql/reasoning 域找数据时,先按 `skills/obtainer/SKILL.md` 和其中的 ObtainerCLI 流程执行,不要从 `outputs/` 里的旧 run 或旧 recipe 反推当前流程
- 执行数据搜集/下载/入湖时,starter 外层只能通过 CLI wrapper 启动 `dataset-acquisition-agent`;如果运行环境不是当前 shell 的 Python,先设置 `LOOPAI_PYTHON_EXECUTABLE=/path/to/loopai-env/bin/python`,再用 `${LOOPAI_PYTHON_EXECUTABLE:-python} -m loopai.skills.ObtainerCLI.cli dm ... dataset-acquisition-agent start`,或在 start 命令上显式传 `--python-executable /path/to/loopai-env/bin/python`;然后轮询/续跑。不要使用通用 `spawn_agent` worker,不要在外层自己创建 SearchAgent task JSON、调用 `searchagent`、调用 `download manifest` 或直接入湖
Expand Down
14 changes: 12 additions & 2 deletions examples/config/starter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,20 @@ default_states:
verl_algorithm: "grpo"
verl_rollout_backend: "vllm" # vllm 或 sglang
verl_model_backend: "fsdp"
train_input_eval_dataset_path: "" # 验证集 Parquet 路径
verl_reward_mode: "auto" # auto、preset 或 custom
# 可直接填原生 Verl Parquet;多轮闭环也可留空,由 Trainer 优先读取本轮 Constructor JSON/JSONL 输出。
verl_source_dataset_path: ""
verl_source_eval_dataset_path: ""
verl_data_adapter: "auto" # auto、native、messages、alpaca 或 qa
verl_validation_ratio: 0.05 # 没有验证集时确定性切分
verl_split_seed: 42
verl_reuse_previous_validation: true
train_input_eval_dataset_path: "" # 可选;上一轮验证 Parquet 默认复用
verl_reward_mode: "auto" # 明确指定 preset/custom 时优先,否则 Trainer 对生成数据给出保守建议
verl_reward_preset: "auto"
verl_reward_kwargs: {}
verl_selection_metric: "val-core/*/acc/mean@*"
verl_selection_mode: "max"
verl_max_actor_ckpt_to_keep: 10
verl_inherit_previous_config: true
verl_use_previous_best_model: true
verl_multi_round_enabled: true
14 changes: 13 additions & 1 deletion loopai/common/db_tool/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,19 @@ def _extract_state_section(states_config: dict[str, Any], section_name: str) ->

def _coerce_update_item(raw_item: Any) -> dict[str, Any]:
if isinstance(raw_item, dict):
return format_value(dict(raw_item))
# Configer accepts both schema-style wrappers (``{"value": ...}``)
# and direct field values. A direct mapping value must not be mistaken
# for the wrapper itself; that used to turn fields such as
# constructor.mapping_results into None. Likewise, infer the wrapper
# type when callers omit it so ``{"value": {...}}`` remains a mapping
# instead of being stringified.
if "value" not in raw_item and "default" not in raw_item and "type" not in raw_item:
return {"value": dict(raw_item), "type": "dict"}
item = dict(raw_item)
if "type" not in item:
value = item.get("value", item.get("default"))
item["type"] = wrap_attr(value)["type"]
return format_value(item)
return {"value": raw_item}


Expand Down
113 changes: 112 additions & 1 deletion loopai/schema/states.py
Original file line number Diff line number Diff line change
Expand Up @@ -1358,9 +1358,60 @@ class TrainerState(BaseModel):
train_input_eval_dataset_path: str = Field(
default="",
title="验证数据集路径",
description="Verl GRPO 使用的验证 Parquet 路径",
description="Verl GRPO 使用的验证 Parquet 路径;生成数据可由 Trainer 自动切分",
json_schema_extra={"ui_type": "file_path", "ui_group": "训练模型"}
)
verl_source_dataset_path: str = Field(
default="",
title="Verl 原始训练数据",
description="JSON/JSONL/Parquet 原始数据;留空时优先使用本轮 Constructor 输出",
json_schema_extra={"ui_type": "file_path", "ui_group": "训练模型"},
)
verl_source_dataset_origin: str = Field(
default="",
title="Verl 原始数据来源",
description="Trainer 记录 user/constructor/obtainer,用于下一轮区分显式覆盖与旧路径",
json_schema_extra={"ui_type": "text", "readOnly": True, "ui_group": "训练模型"},
)
verl_source_eval_dataset_path: str = Field(
default="",
title="Verl 原始验证数据",
description="可选的 JSON/JSONL/Parquet 验证数据;留空时自动切分或复用上一轮验证集",
json_schema_extra={"ui_type": "file_path", "ui_group": "训练模型"},
)
verl_data_adapter: str = Field(
default="auto",
title="Verl 数据适配器",
description="Trainer 将生成数据转换为 Verl Parquet 时使用的字段适配器",
json_schema_extra={"ui_type": "list", "ui_group": "训练模型",
"allowed_values": ["auto", "native", "messages", "alpaca", "qa"]},
)
verl_data_source: str = Field(
default="",
title="Verl data_source 覆盖值",
description="通常留空由 Trainer/reward 决定;仅在数据协议明确时手动填写",
json_schema_extra={"ui_type": "text", "ui_group": "训练模型"},
)
verl_validation_ratio: float = Field(
default=0.05,
gt=0.0,
lt=1.0,
title="Verl 验证集比例",
description="未提供验证数据时 Trainer 的确定性切分比例",
json_schema_extra={"ui_type": "number", "ui_group": "训练模型"},
)
verl_split_seed: int = Field(
default=42,
title="Verl 数据切分种子",
description="控制生成数据去重后的确定性训练/验证切分",
json_schema_extra={"ui_type": "number", "ui_group": "训练模型"},
)
verl_reuse_previous_validation: bool = Field(
default=True,
title="复用上一轮验证集",
description="多轮训练时保持验证集稳定;显式提供本轮验证源时以本轮为准",
json_schema_extra={"ui_type": "toggle_switch", "ui_group": "训练模型"},
)
verl_reward_function_path: str = Field(
default="",
title="Verl Reward 函数路径",
Expand All @@ -1380,6 +1431,12 @@ class TrainerState(BaseModel):
json_schema_extra={"ui_type": "list", "ui_group": "训练模型",
"allowed_values": ["auto", "preset", "custom"]},
)
verl_reward_origin: str = Field(
default="",
title="Verl Reward 选择来源",
description="记录 reward 是自动匹配还是用户指定,用于新一轮按新数据重新匹配",
json_schema_extra={"ui_type": "text", "readOnly": True, "ui_group": "训练模型"},
)
verl_reward_preset: str = Field(
default="auto",
title="Verl Reward 预设",
Expand Down Expand Up @@ -1416,6 +1473,24 @@ class TrainerState(BaseModel):
description="最多保留的 actor checkpoint 数量",
json_schema_extra={"ui_type": "number", "ui_group": "训练模型"}
)
verl_inherit_previous_config: bool = Field(
default=True,
title="继承上一轮 Verl 配置",
description="下一轮以此前已确认 YAML 为基线,仅刷新数据、模型、reward 和运行字段",
json_schema_extra={"ui_type": "toggle_switch", "ui_group": "训练模型"},
)
verl_use_previous_best_model: bool = Field(
default=True,
title="使用上一轮最佳模型",
description="上一轮成功导出 Hugging Face 模型后,自动作为下一轮 GRPO 初始模型",
json_schema_extra={"ui_type": "toggle_switch", "ui_group": "训练模型"},
)
verl_multi_round_enabled: bool = Field(
default=True,
title="启用 Verl 多轮衔接",
description="确保保存 checkpoint 并导出下一轮可直接加载的 Hugging Face 模型",
json_schema_extra={"ui_type": "toggle_switch", "ui_group": "训练模型"},
)
CUDA_VISIBLE_DEVICES: str = Field(
default="",
title="CUDA 可见设备",
Expand Down Expand Up @@ -1598,6 +1673,42 @@ class TrainerState(BaseModel):
description="每次启动 Trainer 子节点时生成的 version_id,用于 TaskRuntimeItem 和版本化输出目录",
json_schema_extra={"ui_type": "text", "ui_group": "训练模型"}
)
trainer_parent_version_id: str = Field(
default="",
title="上一轮 Trainer 版本 ID",
description="当前训练轮继承的数据、配置和模型所来自的 Trainer 版本",
json_schema_extra={"ui_type": "text", "readOnly": True, "ui_group": "训练模型"},
)
trainer_round_index: int = Field(
default=0,
title="Trainer 轮次",
description="同一 task 下从 1 开始递增的训练轮次",
json_schema_extra={"ui_type": "number", "readOnly": True, "ui_group": "训练模型"},
)
trainer_model_inheritance: Dict[str, Any] = Field(
default_factory=dict,
title="Trainer 模型继承结果",
description="是否采用上一轮最佳模型及其校验原因",
json_schema_extra={"ui_type": "json_viewer", "readOnly": True, "ui_group": "训练模型"},
)
verl_data_manifest_path: str = Field(
default="",
title="Verl 数据清单路径",
description="Trainer 数据转换、切分、去重和 reward 决策的版本化清单",
json_schema_extra={"ui_type": "file_path", "readOnly": True, "ui_group": "训练模型"},
)
verl_data_prepare_result: Dict[str, Any] = Field(
default_factory=dict,
title="Verl 数据准备结果",
description="本轮生成数据适配为 Verl Parquet 的统计结果",
json_schema_extra={"ui_type": "json_viewer", "readOnly": True, "ui_group": "训练模型"},
)
verl_reward_recommendation: Dict[str, Any] = Field(
default_factory=dict,
title="Verl Reward 建议",
description="Trainer 自动选择或确认 reward 的依据;不明确时不会猜测",
json_schema_extra={"ui_type": "json_viewer", "readOnly": True, "ui_group": "训练模型"},
)
trainer_output_dir: str = Field(
default="",
title="Trainer 本次输出目录",
Expand Down
37 changes: 35 additions & 2 deletions loopai/skills/Trainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ result = run_prepared(

GRPO 训练/验证 Parquet 至少需要 `prompt`、`data_source`、`reward_model` 三列。训练通过 `conda run --no-capture-output -n verl python -m verl.trainer.main_ppo ...` 启动;指标写入 Trainer 的 `metrics` 目录,checkpoint 按 `global_step_N` 识别,根据 YAML 中的验证指标选择最佳项,再把选中的 FSDP actor 合并为 Hugging Face 模型。

## 生成数据与多轮 GRPO

Constructor 仍负责生成和清洗数据;当 `train_framework=verl` 时,Trainer 在每轮 `prepare()` 中负责最后一段训练协议适配:

1. 优先读取本轮 `constructor.mapping_results.output_file`,也可用 `verl_source_dataset_path` 显式覆盖。
2. 自动识别原生 Verl、messages/ShareGPT、Alpaca 或普通 QA,转换为本轮目录下的 `prepared_data/train.parquet`。
3. 若未提供验证数据,按 `verl_validation_ratio` 和 `verl_split_seed` 确定性切分;多轮仅在 reward 协议兼容时复用上一轮验证 Parquet。
4. 生成 `dataset_manifest.json` 和 `rejected_rows.jsonl`,记录来源、去重、拒绝、切分、哈希和 reward 决策。
5. 以上一轮已确认的 Verl YAML 为超参基线,只刷新本轮数据、最佳模型、reward、GPU、输出目录和 version;然后仍向用户展示整份新 YAML,等待本轮确认。

生成数据的最小增量配置如下;通常不需要用户手写 Verl Parquet:

```yaml
trainer:
train_framework: verl
train_stage: grpo
verl_dir: /path/to/verl
verl_env_path: verl
train_input_model_name: /path/to/base-model
train_input_task_description: Mathematics reasoning with GRPO
verl_source_dataset_path: "" # 留空时使用本轮 Constructor 输出
verl_data_adapter: auto
verl_validation_ratio: 0.05
verl_reuse_previous_validation: true
verl_inherit_previous_config: true
verl_use_previous_best_model: true
verl_multi_round_enabled: true
verl_reward_mode: auto
```

`auto` 会针对每轮新的上游数据重新判断,只在任务/数据来源或答案标记能可靠对应已有 preset 时采用建议;无法判断时 `prepare()` 会停止并要求设置 `verl_reward_mode=preset`/`verl_reward_preset` 或 custom reward,不会猜测 ground truth 或 reward 语义。上一轮模型只有在训练成功、导出无错误且目录包含可加载的 Hugging Face 配置和权重时才会自动传给下一轮;若要改用指定模型,可在本轮调用显式传 `train_input_model_name`,或关闭 `verl_use_previous_best_model`。

## Verl Reward 预设

LoopAI 通过 `loopai/skills/Trainer/rewards/router.py` 提供稳定入口,预设只调用当前 Verl 环境中的实现,不复制 Verl 源码。
Expand Down Expand Up @@ -89,10 +121,11 @@ Verl 实时进度优先读取 `metrics/verl_metrics.jsonl` 的 `training/global_

### 1. 数据检查节点 (Data Check Node)

**功能:** 验证数据集格式是否符合 LlamaFactory 要求
**功能:** 验证 SFT 数据;对 Verl 则先适配生成数据,再验证训练/验证 Parquet 与 reward 协议

**输入:**
- `train_input_dataset_path`: 数据集文件路径(支持 JSON/JSONL 格式)
- `train_input_dataset_path`: SFT 数据或原生 Verl Parquet
- `verl_source_dataset_path`: 可选的生成数据路径;未配置时读取 Constructor 输出

**输出:**
- 数据格式验证报告
Expand Down
11 changes: 5 additions & 6 deletions loopai/skills/Trainer/nodes/config_generation_node.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""
配置生成节点
根据任务描述生成 LlamaFactory 训练配置(YAML格式)
生成 LlamaFactory SFT 或 Verl GRPO 训练配置(YAML格式)
"""

import os
import yaml
from pathlib import Path
from loopai.schema.states import LoopAIState
from loopai.skills.Trainer.utils.config_generator import ConfigGenerator, generate_config_explanation
from loopai.skills.Trainer.utils.verl_config_generator import (
Expand All @@ -22,7 +21,7 @@ def config_generation_node(state: LoopAIState) -> LoopAIState:
"""
配置生成节点

根据任务描述和数据集信息生成合理的 LlamaFactory 训练配置
根据任务描述和数据集信息生成对应后端训练配置

Args:
state: LoopAIState 对象,需要包含:
Expand Down Expand Up @@ -155,9 +154,9 @@ def config_generation_node(state: LoopAIState) -> LoopAIState:
or './output/trainer'
)
os.makedirs(output_dir, exist_ok=True)
config_output_path = state.get('trainer', {}).get('train_output_config_path')
if not config_output_path or Path(str(config_output_path)).suffix.lower() not in {'.yaml', '.yml'}:
config_output_path = os.path.join(output_dir, 'training_config.yaml')
# Every prepare() round owns a version-scoped YAML. Reusing a stale
# path would overwrite the previous round's approved configuration.
config_output_path = os.path.join(output_dir, 'training_config.yaml')

config = generate_verl_grpo_config(state, template_path)
config_output_path = save_verl_grpo_config(config, config_output_path)
Expand Down
Loading