diff --git a/skills/model-training-lite/SKILL.md b/skills/model-training-lite/SKILL.md new file mode 100644 index 0000000..c9ff0dc --- /dev/null +++ b/skills/model-training-lite/SKILL.md @@ -0,0 +1,167 @@ +--- +name: model-training-lite +name_zh: 轻量模型训练 +description: 从 Codex、Claude Code 或其他 Agent 通过用户提供的 Jupyter 环境和数据版本发起一次模型训练,聚焦“准备远端工作区、同步数据、提交 CML SFT、返回 JobID”,不包含完整自主迭代闭环。 +when_to_use: 当用户已经准备好训练数据,想让 Agent 使用 Jupyter/CloudML 发起一次模型训练、复用当前仓库的训练脚本和模板、或询问如何把模型训练流程沉淀成轻量 skill 时使用。 +aliases: jupyter-model-training, sft-submit, 轻量训练, 模型训练提交 +examples: + - 用这个 Jupyter 和 ai-planning 当前分支重新发起训练 + - 数据已经提交到 autoresearch-v1,帮我提交一次 SFT + - 帮我把 Codex 通过 Jupyter 发起模型训练的流程跑起来 + - 检查一下训练工作区和 CML 环境,然后提交训练任务 +allowed_tools: read_file, write_file, grep_search, glob_search, ask_user_question, python_exec, bash +--- + +# 轻量模型训练 + +本 skill 只负责一次训练提交: + +```text +Jupyter 授权环境 + -> 创建/复用远端 kernel + -> 准备隔离训练工作区 + -> 同步训练脚本与数据版本 + -> 检查 CML / 数据 / zk_trainer + -> 提交 SFT + -> 返回 JobID、产物路径、下一步评测入口 +``` + +不要把它扩展成 `model-iteration` 那种完整“baseline -> 分析 -> 增强 -> 训练 -> 评测 -> 再分析”的自主循环。 + +## 必要输入 + +开始前必须拿到: + +- **Jupyter 地址和授权方式**:URL;密码、token、cookie/session,或说明当前环境已有可用登录态。 +- **数据版本**:git repo + branch + commit,或远端工作区中已存在的数据路径。 +- **训练配方**:如果是当前 ZK 中控 SFT,默认复用 `skills/model-iteration/scripts/`;如果不是,必须让用户提供训练脚本、模板或命令。 + +通常用户只需要显式给 `Jupyter 地址` 和 `数据版本`。当前项目的默认训练配方可从本仓库继承,不必每次追问。 + +可选但推荐确认: + +- `owner`:远端输出目录使用的用户前缀,例如 `wuyang6`。 +- `run_name`:本次训练工作区名,例如 `manual_YYYYMMDD_zk_intent_xxx`。 +- 是否训练后立刻发起评测。默认只提交训练,不自动串接评测。 + +## 默认 ZK SFT 配方 + +当前 ZK 中控 SFT 复用这些文件: + +```text +skills/model-iteration/scripts/ + prepare_and_train_sft.py + resolve_run_ids.sh + submit_sft.sh + submit_cml_eval.sh + sft_train_job.yaml.tpl +skills/model-iteration/assets/config.yaml +``` + +默认基模和训练模板见 [references/zk_sft_defaults.md](references/zk_sft_defaults.md)。只有用户明确要求或训练任务不是 ZK 中控 SFT 时,才修改这些默认值。 + +## 推荐流程 + +### 1. 先生成提交计划 + +优先执行 portable script: + +```bash +python skills/model-training-lite/scripts/render_sft_submission_plan.py --input input.json +``` + +输入示例: + +```json +{ + "jupyter_url": "https://.../lab?", + "data_repo": "git@git.n.xiaomi.com:ai-service/ai-planning.git", + "data_branch": "autoresearch-v1", + "data_commit": "b92be709", + "owner": "wuyang6", + "run_name": "manual_20260528_zk_intent_clean_train", + "recipe": "zk_sft" +} +``` + +脚本只生成计划和命令草案,不连接 Jupyter,不提交训练。 + +### 2. 连接 Jupyter + +如果已经有 session/cookie,可直接调用 Jupyter REST API: + +- `GET /api/kernels` 检查登录态。 +- `POST /api/kernels` 创建 kernel;POST 需要 `X-XSRFToken`。 +- websocket 连接 `/api/kernels//channels` 执行 Python/Bash。 + +如果没有登录态,用用户提供的密码/token 登录;不要把密码写入仓库、日志或最终报告。 + +### 3. 准备远端工作区 + +远端路径默认: + +```text +/mnt/wangsenhao/autoresearch-zk-users// +``` + +工作区内必须有: + +```text +scripts/ +assets/ +results/ +output/ +ai-planning/ +zk_trainer/ +``` + +把默认 ZK SFT 配方里的 `scripts/` 和 `config.yaml` 同步到远端工作区。再 clone 或更新数据仓库,并 checkout 到用户指定 commit。 + +### 4. 提交前检查 + +提交训练前必须输出: + +- `ai-planning` 当前 commit。 +- 训练 JSONL 文件数量和总行数。 +- 关键增量文件行数。 +- 目标评测集行数,如果用户指定了评测集。 +- `source ~/.cloudml-cli/.profile` 后 `cml config show` 可用。 +- `zk_trainer` 已存在或 clone 成功。 + +如果 `cml` 不在 PATH,先检查 `~/.cloudml-cli/.profile`。不要因为 `which cml` 为空就直接判定不可用。 + +### 5. 提交 SFT + +ZK SFT 提交命令: + +```bash +source ~/.cloudml-cli/.profile +export AUTORESEARCH_CHAT_ROOT= +export AUTORESEARCH_ROOT="$AUTORESEARCH_CHAT_ROOT" +cd "$AUTORESEARCH_CHAT_ROOT" +eval "$(./scripts/resolve_run_ids.sh)" +./scripts/submit_sft.sh "$SFT_RUNDIC" +``` + +注意:`resolve_run_ids.sh` 只输出 `SFT_RUNDIC` 和 `EVAL_RUNDIC`,不要读取不存在的 `RUNDIC`。 + +### 6. 成功判定 + +提交成功后必须回报: + +- CloudML JobID。 +- CloudML 链接。 +- `SFT_RUNDIC` 和 `EVAL_RUNDIC`。 +- 远端训练工作区。 +- 成功标记路径:`/sft_output/_SUCCESS`。 +- 查看状态命令:`cml custom_train describe `。 + +再查一次 `cml custom_train describe `,确认状态不是提交后立即失败。 + +## 边界 + +- 不自动修改训练数据。 +- 不自动做错误归因。 +- 不自动生成增强样本。 +- 不默认串接评测;训练成功后是否评测由用户或后续明确指令决定。 +- 不把 Jupyter 密码、CloudML key、cookie 写进产物或最终回复。 diff --git a/skills/model-training-lite/references/zk_sft_defaults.md b/skills/model-training-lite/references/zk_sft_defaults.md new file mode 100644 index 0000000..990ec41 --- /dev/null +++ b/skills/model-training-lite/references/zk_sft_defaults.md @@ -0,0 +1,80 @@ +# ZK SFT 默认训练配方 + +当前轻量训练默认复用 `model-iteration` 的 SFT 提交流程,但只使用其中的训练提交能力。 + +## 训练脚本 + +```text +skills/model-iteration/scripts/prepare_and_train_sft.py +skills/model-iteration/scripts/resolve_run_ids.sh +skills/model-iteration/scripts/submit_sft.sh +skills/model-iteration/scripts/submit_cml_eval.sh +skills/model-iteration/scripts/sft_train_job.yaml.tpl +skills/model-iteration/assets/config.yaml +``` + +## 默认基模 + +```text +/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507 +``` + +`sft_train_job.yaml.tpl` 内固定从该基模启动 SFT。不要从上一轮 `sft_output` 续训,除非用户明确改变训练策略。 + +## 默认数据仓库 + +```text +git@git.n.xiaomi.com:ai-service/ai-planning.git +branch: autoresearch-v1 +``` + +训练脚本会扫描: + +```text +ai-planning/data/train_set/*/*.jsonl +``` + +其中 `*_valid.jsonl` 会作为验证数据,其余训练 jsonl 进入训练集。 + +## 默认远端工作区 + +```text +/mnt/wangsenhao/autoresearch-zk-users// +``` + +训练产物: + +```text +/sft_output/ +/sft_output/_SUCCESS +``` + +## CML 环境 + +CloudML CLI 通常位于: + +```bash +source ~/.cloudml-cli/.profile +``` + +该 profile 会设置: + +```bash +PATH=$HOME/.cloudml-cli/bin:$PATH +CLOUDML_CONFIG=$HOME/.config/cloudml/config.yaml +``` + +检查命令: + +```bash +cml config show +cml custom_train describe +``` + +## 常见坑 + +- Jupyter POST 请求缺少 `_xsrf`:从 cookie 取 `_xsrf`,请求头带 `X-XSRFToken`。 +- websocket 证书校验失败:内网证书场景可在客户端禁用证书校验,但不要降低服务端安全配置。 +- `which cml` 为空:先 `source ~/.cloudml-cli/.profile`。 +- `resolve_run_ids.sh` 没有 `RUNDIC`:只使用 `SFT_RUNDIC` / `EVAL_RUNDIC`。 +- git clone 看似卡住:先检查目标目录是否已经完整、是否存在残留进程,不要重复提交训练。 diff --git a/skills/model-training-lite/scripts/render_sft_submission_plan.py b/skills/model-training-lite/scripts/render_sft_submission_plan.py new file mode 100644 index 0000000..d57c2d6 --- /dev/null +++ b/skills/model-training-lite/scripts/render_sft_submission_plan.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import PurePosixPath +from typing import Any + + +DEFAULT_DATA_REPO = "git@git.n.xiaomi.com:ai-service/ai-planning.git" +DEFAULT_BRANCH = "autoresearch-v1" +DEFAULT_RECIPE = "zk_sft" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Render a lightweight SFT submission plan.") + parser.add_argument("--input", "-i", help="JSON input file. Reads stdin when omitted.") + args = parser.parse_args() + text = open(args.input, encoding="utf-8").read() if args.input else sys.stdin.read() + payload = json.loads(text or "{}") + result = render_plan(payload) + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 if result["ok"] else 1 + + +def render_plan(payload: dict[str, Any]) -> dict[str, Any]: + missing: list[str] = [] + jupyter_url = clean(payload.get("jupyter_url")) + data_commit = clean(payload.get("data_commit") or payload.get("commit")) + data_path = clean(payload.get("data_path")) + if not jupyter_url: + missing.append("jupyter_url") + if not data_commit and not data_path: + missing.append("data_commit or data_path") + + owner = clean(payload.get("owner")) or "wuyang6" + run_name = clean(payload.get("run_name")) or default_run_name(data_commit or data_path or "manual") + workspace = clean(payload.get("workspace")) or str( + PurePosixPath("/mnt/wangsenhao/autoresearch-zk-users") / owner / run_name + ) + data_repo = clean(payload.get("data_repo")) or DEFAULT_DATA_REPO + data_branch = clean(payload.get("data_branch")) or DEFAULT_BRANCH + recipe = clean(payload.get("recipe")) or DEFAULT_RECIPE + + commands = { + "prepare_workspace": [ + f"mkdir -p {workspace}/{{scripts,assets,results,output}}", + f"cd {workspace}", + ], + "sync_data": [ + f"[ -d ai-planning/.git ] || git clone -b {data_branch} {data_repo} ai-planning", + f"git -C ai-planning fetch origin {data_branch}", + f"git -C ai-planning checkout {data_branch}", + f"git -C ai-planning reset --hard {data_commit}" if data_commit else f"# use existing data_path: {data_path}", + ], + "submit_sft": [ + "source ~/.cloudml-cli/.profile", + f"export AUTORESEARCH_CHAT_ROOT={workspace}", + 'export AUTORESEARCH_ROOT="$AUTORESEARCH_CHAT_ROOT"', + f"cd {workspace}", + 'eval "$(./scripts/resolve_run_ids.sh)"', + './scripts/submit_sft.sh "$SFT_RUNDIC"', + ], + "check_status": [ + "cml custom_train describe ", + f"test -f {workspace}/sft_output/_SUCCESS", + ], + } + return { + "ok": not missing, + "missing": missing, + "plan": { + "jupyter_url": jupyter_url, + "recipe": recipe, + "workspace": workspace, + "data_repo": data_repo, + "data_branch": data_branch, + "data_commit": data_commit, + "data_path": data_path, + "success_marker": f"{workspace}/sft_output/_SUCCESS", + }, + "commands": commands, + "required_outputs": [ + "CloudML JobID", + "CloudML task URL", + "SFT_RUNDIC", + "EVAL_RUNDIC", + "workspace", + "success_marker", + ], + } + + +def clean(value: Any) -> str: + return str(value or "").strip() + + +def default_run_name(seed: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9_-]+", "_", seed).strip("_")[:32] or "manual" + return f"manual_sft_{slug}" + + +if __name__ == "__main__": + raise SystemExit(main())