diff --git a/skills/model-iteration/SKILL.md b/skills/model-iteration/SKILL.md index 3cbedf9..5ae9489 100644 --- a/skills/model-iteration/SKILL.md +++ b/skills/model-iteration/SKILL.md @@ -18,7 +18,7 @@ when_to_use: | | ❌ 禁止说的话 | ✅ 正确做法 | |---------|---------| -| "要不要我做训练集近邻检索?" | 这是 §2.3 分析必做项,**直接做**,做完把结果落 report | +| "要不要我做训练集近邻检索?" | 这是 §2.3 分析必做项,**直接做**,做完把结果落 dist-analysis 报告(`workflow.md`) | | "下一步可以继续吗?" | 永远不问。看 program.md 流程图自己判断 | | "我先把控制权交回,等你来问跑完了吗" | watcher 接手 UI 同步,长任务用 `bash(run_in_background=true, wait_for_completion=true)` 提交后本轮主动结束;后端会在产物落盘后自动起新一轮把结果送回,**直接进 Step 1** | | "这一步是关键决策点,需要你拍板" | program.md 没写就不是。**自主决策 + 落 iteration_log** | @@ -48,16 +48,17 @@ when_to_use: | - [ ] watcher 声明已写入 program-state.jsonl - [ ] cml workflow + 等 metric_diff 落盘已用 `bash(run_in_background=true, wait_for_completion=true)` 提交(**不要**在 bash 内 sleep+poll) -### Step 1 准入(`dist-analysis`) +### Step 1 准入(`dist-analysis`,含原 Step 2 报告产出) - [ ] 读 `metric_diff/lark_template.json` 提分层指标(specific / 大盘车载 / 目标专项) - [ ] 读 `metric_diff/specific_comparison.csv` 提取 baseline 错误分布 - [ ] **§2.3 失败 case 与训练数据的关联**:每条错例在 `train_set/zk_intent/*.jsonl` 做近邻检索(前 3 近邻),输出"有近邻 / 无近邻"分类 +- [ ] **§2.3 mislabel 命中必须落盘 CSV**:列固定为 `file,line,query,old_label,是否改(1/0),建议新label`,写到 `$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_.csv`(前端「分层结果分析」卡片就读这一份)。本轮无 mislabel 也写一份只含表头的空 CSV,明示"已检查、无候选"。**禁止**只 print 到 stdout 或只写进 workflow.md——卡片读不到 CSV 等同漏交 +- [ ] **§2.3 判别器自检**:写完任何"把 raw output 抽成可比对值"的函数后(complex 二值 / tag / function / code label / slot 等都算)、跑全量前——端点验证(每类训练侧 ≥1 条 + eval 侧 ≥1 条;二分类 4 条、N 分类 ≥2N 条)+ 报告并列原文 + 零计数兜底(详见 program.md §2.3 自检小节) - [ ] **§2.3 Reward 对齐检查**:全量失败 case 用 `zk_reward_fn` 验 reward 方向(不抽样) - [ ] §0.1 Gold drift 检查(如未做) - -### Step 2 准入(`report`) - [ ] §2.4 根因归类表(每个 pattern 必归一类,可并列但要主次) -- [ ] 写入 `results/workflow.md` +- [ ] 写入 `results/workflow.md`(根因 / 跨轮 diff / 需求集合深度分析 / 天花板诊断) +- [ ] `output/relabel_candidates_.csv` 已落盘(无候选则空表头 CSV) - [ ] error_registry 追加本轮错误 ### Step 3 准入(`hypothesis`) — analysis 收尾,不是 train 开头 @@ -65,7 +66,7 @@ when_to_use: | - [ ] 假设必须有依据(指向 §2.3 / §2.4 的具体发现) - [ ] **`step:"hypothesis"` entry 写在当前 round(R{n})名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类,是 R{n}·Baseline / R{n}·Analysis 的最后一张卡,不是 R{n+1}·Train 的开头。这样 gate(Human Check / Review)会插在 hypothesis 卡之后、R{n+1}·Train 之前,**用户看到假设内容再拍板是否进 train**。 -### Step 1 → Step 2 → Step 3 边界(**NEVER STOP 硬连接**,R0 / R1+ regression 都适用) +### Step 1 → Step 3 边界(**NEVER STOP 硬连接**,R0 / R1+ regression 都适用) - [ ] dist-analysis 算完 delta(new_fail / new_fix / persistent / 子集分布)那一刻起,**同一轮 bash 不许结束**:紧接着写 `results/workflow.md`(包含完整指标表 + 病灶定位 + 假设 + 回滚/继续建议)→ append `iteration_log.jsonl` R{n} entry(`results` 字段填本轮 metric,`hypothesis` 字段填下一动作) - [ ] **regression 场景同样适用**:哪怕 R1 出现导航bvt 纯劣化、可聊可控大幅 -25 这种"必须回滚"信号,**先把分析落到 workflow.md 和 iteration_log,再用 chat reply 给人回滚选项**。文件先落、聊天再发——顺序不能反。 - [ ] **不许"诊断只写聊天回复 / 等用户拍板再补盘"**:前端「分层结果分析」卡片读的是 `chat_root/results/workflow.md`;你不写盘卡片永远显示上一轮,看不到 R1 结论。 @@ -75,10 +76,11 @@ when_to_use: | ### Step 4 准入(`augment`,需要修改训练集才进) - [ ] **§4.0 原始训练数据清洗(增强前必做)**——按 4 种动作走完逐 pattern 判定 - [ ] §4.0.1 Step A:候选定位输出 csv,**不直接改** -- [ ] §4.0.1 Step B:量级判定走对应支线(≤50 自动 / 51-200 全量人审 / >200 触发 #3,全量交付) +- [ ] §4.0.1 Step A.5:**先走 label-master 预审**(用推荐标签覆盖 new_label,剔除 label-master 不认同要改的条目) +- [ ] §4.0.1 Step B:量级判定基于 label-master 处理后的**残留候选量**走对应支线(≤50 自动按 label-master 推荐改 / 51-200 全量人审 / >200 触发 #3,全量交付) - [ ] §4.0.1 Step C:备份 `.bak` 文件 - [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入 -- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:对 H1 `modified_samples.jsonl` + H2 `augment_.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),verdict 写 `results/data_clean_/label_master_review.jsonl`,不通过比例 ≤ 5% 才能进 Step 5 +- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),verdict 写 `results/data_clean_/label_master_review.jsonl`,不通过比例 ≤ 5% 才能进 Step 5 ### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑) - [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft_via_cml.sh` → 挂 watcher @@ -148,6 +150,75 @@ echo '{"step":"cml","status":"running","progress":0.4,"ts":"'$(date -Iseconds)'" UI 卡底部进度条会跟着动;不写就一直显示初始进度。 +### R5 — bg 任务作用域 = 单 step(**反复踩坑**) + +bg 任务(`bash(run_in_background=true)`)的合法范围是**一个 step 内的"提交一个远端动作 + 等它的产物落盘"**——比如「提 cml workflow run + 等 metric_diff 落盘」、「提 SFT custom_train + 等 `_SUCCESS`」。这是 §「典型用法」推荐的标准结构。 + +**禁止**把 bg 范围扩到跨 step: + +| ❌ 禁止 | ✅ 正确 | +|---|---| +| 一个 bg 串行 `等 SFT _SUCCESS → 提 cml workflow run(评测)→ 等评测 lark_template.json` | sft watcher fire complete 那一刻,agent 主对话**当轮**完成:写 `cml=running` + 起新 bg(提评测 + 等 lark_template.json)+ 注册新 watcher | +| bg 内部 sleep+poll 跨多个 step 的状态 | 每个 step 一个 bg + 一个 watcher;watcher fire = step 边界 = 主对话轮回来推进 | + +**为什么是死规则**:bg 脚本本身不会 append `program-state.jsonl`,状态只活在 bg 进程内存。一个 bg 跨 step 跑起来后,前端从第一个 step complete 之后就完全感知不到后续 step——卡片永远停在第一个 step,watcher 也没注册新的。**bg 进程死了 / session 重连 / 中途想插手** 都补不回来,因为 program-state.jsonl 上没任何线索。 + +**自查判据**:bg 脚本里出现新 step 的关键词(`cml workflow run` 提评测、`submit_sft_via_cml.sh`、对应新 step 的 `echo running`),就是越界。bg 只该做"提一个远端 job 然后等它的产物",**绝不能跨 step 边界**。 + +**watcher fire → 当轮 4 件套**(漏一件就断片): +1. echo 上一 step `complete`(watcher 已自动写也算) +2. echo 下一 step `running` +3. 起新 bg「提交下一 step 的远端动作 + 等产物」 +4. 注册新 watcher 监听下一 step 的产物文件 + +回主对话轮前自问一句:**"如果我现在被 kill,单看 program-state.jsonl 最后一行,下一个 agent 能不能接着干?"** 答不上来就是 4 件套没补全。 + +**绝不"事后补 status"绕过 watcher**:发现 step 在 UI 上一直 running、watcher 没自动标 complete,**正确做法是 debug watcher**(看 `_SUCCESS` / `lark_template.json` 是否真的落盘、watcher 进程是否还在),**不是**自己手 echo 一条 `step=complete` 把状态条点亮。后者掩盖 bug 但留下错位的 run_id,下一轮 watcher 会接着错。 + +### R6 — 同秒 running + complete 视为虚假完成(**反复踩坑**) + +每个真实 step 都有最低耗时下限: + +| step | 最低耗时 | 原因 | +|---|---|---| +| `cml` | 数分钟到数十分钟 | 远端 workflow 启动 + 评测 | +| `sft` | 30 分钟以上 | 训练 pod | +| `augment` | 数分钟 | GPT 仿写 + sanity + label-master 复核 | +| `dist-analysis` | 1-3 分钟 | 读 metric_diff + 跨轮 diff + 写 workflow.md | +| `gold-drift` | 1-2 分钟 | drift 检测 | + +如果 `step=running` 和 `step=complete` 两条 entry 的 `ts` 在**同一秒**(或差距远小于该 step 最低耗时),就是**虚假完成**——agent 平铺 echo 状态绕过了真实工作。**禁止**。 + +最常见踩坑:human-review / human-check 拿到用户决策后,agent 在同一轮里把 `hypothesis + augment + sft + watcher` 一口气 echo 出来,只真做了最后一步(提 SFT),中间的 `augment` 被跳过——`augment_.jsonl` 根本没生成,前端 augment 卡永远空。 + +**正确做法**:human-review 之后必须按 program.md 逐 step 真跑: +1. 写 `hypothesis=running` → 真写假设到 iteration_log → 写 `hypothesis=complete` +2. 写 `augment=running` → 真跑 §4.0.1 + §4.1 + §4.2 + §4.3 + §4.5 label-master 复核 → 写 `augment=complete`(**这一步至少 5 分钟**) +3. 写 `sft=running` → 提交 cml custom_train + 起 watcher + +**自查**:连续两个 step 的 `ts` 间隔 < 60 秒,必然有一个是假的。复盘时 grep program-state.jsonl 看相邻 entry 时间戳。 + +**前端表现的迷惑性**:虚假完成的 step **状态条仍显示 ✓ complete**(因为 entry 写了 complete),但**详情卡空**(因为 backend 按 step 关联的产物文件读内容——augment 卡读 4 个 `augment_.*` 文件,文件不存在 = 卡片空)。**状态条说做完了、详情卡却空**——这是最具迷惑性的失败模式,必须从源头杜绝。 + +### R7 — `step=running` 与对应 `watch` 条目必须同事务落盘(**反复踩坑**) + +watcher 触发 complete 时使用的 `run_id` **必须等于触发它的 watch 条目的 run_id**。后端 scanner 改造后实施两道闸门: + +1. **每个 step 只 register 最新一条 watch 条目**(append-only jsonl 里历史 round 的 watch 不再被复活) +2. watch 条目的 `run_id` 必须**等于**该 step 最新一条 `status=running` 条目的 `run_id`,否则跳过等下一轮 scanner + +这意味着 agent 一侧必须遵守: + +- **写 `step=running, run_id=R` 之后,必须紧接着写 `watch {step, run_id=R, ...}`**——同一秒、同一个工具调用块内、不要被别的写入打断。理想是把"reset status + watcher 声明"做成一次 append 数组写入。 +- **跨 round 的 watch path 即使重复也要重发**(如同样的 `_SUCCESS` 路径),不能依赖"上一 round 那个 watch 还在"——`run_id` 不一样,watcher 任务也是新的。 +- **`run_id` 字段必填**,缺失会被 scanner 当成 `'?'` 处理,`'?' != 'R2'` 直接 skip,watcher 永远起不来。 + +**反模式(实际踩过的坑)**: +- agent 写完 `sft=running` 后没在同事务里写 watch → scanner 这一轮扫到时只有 status,没有匹配 run_id 的 watch → 用历史 watch 的 path 注册 watcher(旧逻辑下),run_id 错位 +- watcher fire 后 agent 看 status 没动,又手动 echo 一条 `sft=complete`(违反 R5 末段)→ 后端的 scanner 已经修,但 agent 协议层面还是要 hold 住 + +**自查**:grep `program-state.jsonl` 里所有 `watch` 条目,每条的 `run_id` 必须能在前面(同一 round)找到匹配的 `step=, status=running, run_id=<同值>` 条目。 + ### State 文件位置 ``` @@ -175,8 +246,7 @@ mkdir -p "$SESSION_OUTPUT" |----------|----------| | `cml` | CML 评测 | | `gold-drift` | Gold Drift | -| `dist-analysis` | 分层结果分析 | -| `report` | 问题分析 & 报告 | +| `dist-analysis` | 分层结果分析 & 报告(含根因归类、workflow.md 产出) | | `hypothesis` | 形成假设 | | `augment` | 数据增强 | | `verify` | 修改返回验证 | @@ -280,7 +350,7 @@ echo '{"watch":{"step":"cml","kind":"file_exists","path":"'$ROOT/workflow$RUNDIC 要把日志推到 UI 底部那块黑色 Live Log 区,追加: ```bash -echo '{"log":{"ts":"11:55:10","iter":"R3","text":"Step 3 开始: 问题分析 & 报告生成"}}' >> "$SESSION_OUTPUT/program-state.jsonl" +echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 workflow.md,根因归类完成"}}' >> "$SESSION_OUTPUT/program-state.jsonl" ``` ### 不要做的事 @@ -339,6 +409,11 @@ echo '{"log":{"ts":"11:55:10","iter":"R3","text":"Step 3 开始: 问题分析 & 2. **假设驱动**:每轮必须能回答"这轮验证了什么?学到了什么?下一轮改什么?" 3. **NEVER STOP**:全程不打断用户,循环直到达标或人类主动打断 4. **从 basemodel 训练**:每轮 SFT 的 `--model_path` 必须是 basemodel,禁止从上轮 ckpt 继续训 +5. **复杂度优先**:分析类任务(近邻检索 / 聚类 / 相似度统计)选算法前先估数据规模。**忌**对几万行训练集用 `sklearn.fit_transform` 这类要全量物化稀疏矩阵或 embedding 矩阵的 API — 单次 600s 仍可能 timeout。**>1 万行训练集做近邻必须先粗筛后精排(倒排索引 → 每错例 50–300 候选 → 精排),禁止 O(M·N) 全量比对**——37×30k=1.1M 次朴素比较 ~3min,粗筛后 7.4k 次 ~10s。错例侧的"剪枝"只是常数优化,不能替代粗筛。详见 [references/program.md §2.3](references/program.md) +6. **大文件先切片**:>10MB 量级的远端 CSV/JSONL(如 `specific_comparison.csv`),先用 bash `awk/grep/head` 在远端切出目标子集再处理。**忌**直接 `pd.read_csv` 整体加载或把 DataFrame 当返回值穿过工具调用 — 会撞 runtime 的输出/返回大小限制(实测 99MB 直接读 → "太大了" 失败) +7. **跨脚本复用走 /tmp pickle 缓存**:同一轮 dist-analysis 里 all_train.jsonl 这种"多个脚本都要读的训练集"必须缓存——第一次读完用 `pickle.dump(rows, '/tmp/all_train_.pkl')`(pod /tmp 是本地 SSD),后续脚本以 `if path.exists(): pickle.load(path) else: 重建 + 落盘` 模式启动。倒排索引同理。**忌**每个脚本都重新 `for line in open(jsonl): json.loads(line)` 全表 — 30k 行 NFS jsonl 每次 5–15s,从本地 pickle unpickle <1s,v32 R0 5 次重读累计 ~50s 可省。脚本文件名最好写 cache key(如 `kn_search_v2_.py`),换轮换 runDic 自动失效。 +8. **stdout 即 LLM 下一轮 input,分级 print**:每个分析脚本的 stdout 会原样喂回 LLM 当下一轮上下文,每 KB 都要 prefill。**只 print 决策需要的聚合**(端点抽样总数、各 verdict 的计数、top-3 exemplars 文字),**明细落 `/tmp/__detail.json`**(每条错例的 3 近邻 + 相似度 + token overlap 等)后续要回看再 `cat` 该文件。v32 R0 实测 kn_search 一次 stdout 18KB、judge_check 4.7KB、mislabel 11.3KB,60% 是重复的 per-case 全量 dump,LLM 看完只用一句话总结——这部分 prefill cost 是直接浪费。还有:`cat > /tmp/x.py < ` 回显进 stdout(实测一次累计 1–2KB),改用 `python3 -c "open('/tmp/x.py','w').write(open('/dev/stdin').read())" <<'PY' ... PY` 或 `base64 -d` 注入,可以彻底消掉 `> > >` 噪声。 +9. **一律走 bash + 脚本文件,禁用 python_exec**:所有 python 任务把代码写到远端 `/tmp/xxx.py`,用 `bash` 跑 `python3 -u /tmp/xxx.py 2>&1 | tee /tmp/xxx.log`。**忌**用 python_exec — 它 stdout 全缓冲、turn 结束才 flush,超时被 kill 时 buffer 直接丢,故障时连 phase 时序都看不到(实测 600s/1800s 撞墙后 `tool_result` 没有任何 stdout 字段)。bash + tee 模式 stdout 实时落盘,超时也能 `tail /tmp/xxx.log` 看到死在哪。 ## 达标条件 @@ -365,8 +440,7 @@ step key 跟 Step 编号映射: |------|----------|------| | Step 0 | `cml` | CML 评测当前模型 | | Step 0 (附带) | `gold-drift` | Gold drift 检查 | -| Step 1 | `dist-analysis` | 分层结果分析 | -| Step 2 | `report` | 问题分析 & 报告 | +| Step 1 | `dist-analysis` | 分层结果分析 + 问题分析 & 报告(写 workflow.md) | | Step 3 | `hypothesis` | 形成假设(写 iteration_log) | | Step 4 | `augment` | 数据增强 / 生成 | | Step (干预后) | `verify` | 修改返回验证 | @@ -377,8 +451,7 @@ step key 跟 Step 编号映射: ``` LOOP: Step 0: CML 评测(当前模型)+ Gold drift 检查 - Step 1: 分层结果分析(需求集合 → 大盘车载 → specific test) - Step 2: 问题分析 & 报告(根因归类 / 跨轮 diff / 天花板诊断) + Step 1: 分层结果分析 + 问题分析 & 报告(需求集合 → 大盘车载 → specific test;根因归类 / 跨轮 diff / 天花板诊断 / 写 workflow.md) Step 3: 形成假设(写入 iteration_log.jsonl) Step 4: 数据生成(仅 data 归因时做) Step 5: SFT 训练(submit_sft_via_cml.sh) @@ -423,6 +496,27 @@ assets/ - 跨子集净退步(#4) - 连续 3 轮目标子集净提升 ≤ 0.5pp(#5) +### ⛔ 落 gate 前必读:30 秒自检三问(**任一不过 = 不写 gate,直接进下一步**) + +写 gate entry / 在 chat 里向用户提问之前,**逐条过这三问**。下面任何一段流程示例都默认你已经过了这三问;过不了,再漂亮的 summary/proposal/ask 也是干扰用户。 + +**Q1. 是真 §HiTL 信号吗?** +- ✅ 真信号:label-master 预审后残留 > 50/200、Gold drift ≥ 10、跨子集净退步、连续 3 轮无提升 等列表里写明的条件 +- ❌ 凑出来的理由:`第一次跑想让用户校方向` / `我担心副作用` / `想让用户拍板更稳` / `proposal 听起来风险大` —— 这些都是脑补,不是信号 + +**Q2. `ask` 是真分叉吗?** +- ✅ 真分叉 = 用户的判断能改变下一步动作:候选量收窄策略、回滚 vs 续训、换目标子集思路、改阈值 +- ⛔ **"H? 假设组合"不是分叉**:`H1+H2 一起 vs 只跑 H1`、`先做 H1 还是先做 H2`、`激进 vs 保守 vs 中庸`、`要不要追加 H3` —— 假设的**拆分、组合、顺序、激进度**全是 agent 自己根据信号该决定的,决定完写进 `proposal` 公布即可,**不要甩给用户** + +**Q3. 拆 H 的依据是数据信号还是脑补 trade-off?** +- ✅ 数据信号:mislabel 近邻数、no_neighbor 占比、pattern 分布、其他子集的实测分数差 +- ❌ 脑补:`改这 12 条可能会让 X 子集下降` / `H2 加多了可能过拟合` —— 没跑过就是推测;推测可以写进 `proposal` 当 caveat("预期回收 ~10 条,trade-off 待 R1 验证"),**做不出 ask** + +> **反复踩坑案例**(5/24 真实复盘):R0 baseline 跑完,37 错例里 12 条 mislabel(远低于 50 阈值),agent 自己脑补"H1 修标可能拖累线上挖掘召回complex 子集",于是抛 `H1+H2 一起 vs 只跑 H1(激进 vs 保守)` 让用户拍板。**三问全挂**:信号没命中(Q1 ❌,12 < 50)、ask 是 H 组合(Q2 ❌)、副作用是推测(Q3 ❌)。**正确做法**:定下方案写进 proposal 公布,直接进 augment,trade-off 留给 R1 评测验证。 + +如果三问都过 = 真有事要用户拍 → 按下面 `### 写 gate 节点` 的写法落盘 + chat ask。 +如果有一条不过 = 没事 → 不写 gate、不在 chat 里凑问题、直接进下一步。 + ### 写 gate 节点(让 UI 显示 Human Check / Review 卡) 凡是要让用户拍板的检查点,**必须** append 一条 gate entry 到 `program-state.jsonl`,**再** 用 chat reply 提问。UI 会在当前 round section 之后插入「Human Check」或「Human Review」横向卡片(IN PROGRESS 状态)。 @@ -435,10 +529,10 @@ assets/ | 时机 | gate 类型 | run_id | |---|---|---| -| R0 baseline 分析完成、要让用户确认是否进 R1 train(命中 HiTL 信号 / 候选量大 / 目标子集偏低 / 第一次跑想让用户校方向 等) | `human-check` | `R0` | +| R0 baseline 分析完成、命中 §HiTL 信号要让用户拍板(label-master 预审后残留候选超阈值、目标子集异常、跨子集分歧大 等真实信号;⛔ **不是**"第一次跑想让用户校方向" / "我担心 H1 修标的副作用" / "想让用户在激进/保守里选"这种凑出来的理由——先过上面的自检三问) | `human-check` | `R0` | | R{n≥1} analysis 完成、命中 §HiTL 信号(gold drift / regression / 跨子集退步 / 连续 3 轮无提升 等) | `human-review` | `R{n}` | -| §4.0.1 Step B:候选量 > 200 条命中触发条件 #3 | `human-check` | 当前 round | -| 其他 HiTL 信号(要批改旧标签 > 50 条、Gold drift ≥ 10 条 等) | `human-check`(开始前)/ `human-review`(结果后) | 当前 round | +| §4.0.1 Step B:**label-master 预审后**残留候选 > 200 条命中触发条件 #3(原扫数不算数) | `human-check` | 当前 round | +| 其他 HiTL 信号(label-master 预审后残留 > 50 条、Gold drift ≥ 10 条 等) | `human-check`(开始前)/ `human-review`(结果后) | 当前 round | 判断标准就一条:**只要你下一步打算 chat-ask 用户拍板,就先写 gate entry 再问**。 @@ -447,11 +541,16 @@ assets/ **强烈推荐结构化三段写法**(`summary` / `proposal` / `ask`)——UI 会渲染成"现状 / 提议 / 请选"三个带标签行,用户一眼看明白。`reason` 留作 fallback。 ```bash -# R0 baseline 之后想让用户拍板是否进 train(推荐结构化) +# ⛔ 写之前已经过了上面 §"30 秒自检三问"——否则不要写这条 gate。 +# ⛔ ask 必须是真分叉。⛔ "H1+H2 一起 vs 只跑 H1"、"激进 vs 保守"、"先 H1 还是先 H2" 这类 +# "假设组合"统统不是分叉——是 agent 自己根据信号决定的,决定完写进 proposal 公布即可。 +# R0 baseline 后命中真实 HiTL 信号(这里是候选量超阈值),让用户拍板更精细 pattern +# ⚠️ 给用户排板的候选数量必须是 §4.0.1 Step A.5 label-master 预审之后的残留数(推荐 != old_label 的那部分), +# 不是 wide pattern 原始扫出来的数。原始数 label-master 还要剔掉一大半,先用原始数找用户=干扰用户判断。 echo '{"step":"human-check","status":"running","run_id":"R0", - "summary":"R0 baseline 完成;复杂导航过召专项0511 = 58.89%(53/90),37 错全为 complex 误判", - "proposal":"H1(标签纠错):把 7 条原标 complex=true 但实际是简单导航的样本改回 complex=false。 H2(数据增强):仿写 100 条 complex=false 的简单导航 query 补进训练集,平衡正负样本", - "ask":"是否按 H1+H2 一起进 R1 train?还是先只跑 H1 验证一轮?", + "summary":"R0 baseline 完成;复杂导航过召专项0511 = 58.89%(53/90);wide pattern 原扫 412 条,label-master 预审后残留 287 条(> 200 阈值)", + "proposal":"H1(标签纠错):按 label-master 推荐改这 287 条 complex=true → complex=false。 H2(数据增强):仿写 100 条 complex=false 的简单导航 query 补进训练集", + "ask":"残留 287 条仍偏多,是全部按 label-master 推荐改、还是先收窄到 query 含「打开/进入」的子集(约 110 条)单独审一轮?", "ts":"'$(date -Iseconds)'"}' >> $S # R1 评测发现 regression @@ -462,7 +561,8 @@ echo '{"step":"human-review","status":"running","run_id":"R1", "ts":"'$(date -Iseconds)'"}' >> $S # fallback:只写 reason 也能跑(前端会启发式切分),但不如结构化清晰 -echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量 412 命中规则上限,需要人工定更精细 pattern 收窄","ts":"'$(date -Iseconds)'"}' >> $S +# 注意 reason 里的"412"也是 label-master 预审后残留数,不是原扫数 +echo '{"step":"human-check","status":"running","run_id":"R0","reason":"label-master 预审后残留 412 条仍超阈值,需要人工定更精细 pattern 收窄","ts":"'$(date -Iseconds)'"}' >> $S ``` **顺序不能反**:先 echo gate entry → 再 chat reply 给用户。否则用户先看到聊天问话、UI 里却没卡,会困惑"流程是不是卡死了"。 @@ -476,7 +576,7 @@ echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量 | `run_id` | ✅ | 当前所在轮次(决定 gate 插在哪个 section 后) | | `summary` | 🔼 | **现状一句话**:跑了什么、关键数字。例:`R0 baseline 完成;专项 58.89%(53/90),37 错全为 complex 误判` | | `proposal` | 🔼 | **打算怎么干**:每个 H 单独说"H? (类型):具体做什么"。详见下面规则 | -| `ask` | 🔼 | **让用户选什么**:用问句给出 A/B 选项。例:`是否按 H1+H2 进 R1 train?还是先只跑 H1 验证一轮?` | +| `ask` | 🔼 | **让用户选什么**:必须是真分叉(用户的判断能改变下一步动作)。例:`label-master 预审后残留 287 条偏多,全改、还是收窄到「打开/进入」子集(~110 条)单独审一轮?` ⚠️ 写候选数量必须是 §4.0.1 Step A.5 label-master 预审后的残留数,不是原扫数。⛔ 反例:`H1+H2 一起 vs 只跑 H1`——假设组合是你定的,不甩给用户 | | `reason` | ⭕ | 兜底用:没写 summary/proposal/ask 时前端会拿 reason 做启发式切分。但**优先用结构化三段**,别只写 reason | | `ts` | ✅ | ISO 时间戳 | @@ -496,6 +596,12 @@ echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量 - 流程自洽说明(`需要人工逐条审 1/0`、`走 sanity check`、`过 label-master 复核`)—— 这些是 agent 内部流程,对用户决策没用 - 候选量区间括号注释(`100~150 触发 51-200 区间`)—— 数量 OK,区间归属归属是规则细节,删 +**`ask` 字段尤其要注意:** + +⛔ **不要把"H? 假设组合"作为分叉抛给用户**——如 "H1+H2 一起 vs 只跑 H1"、"先做 H1 还是先做 H2"。假设的拆分和组合是 agent 自己根据信号决定的,决定完写进 proposal 公布即可,不需要用户拍板。`ask` 只在**真有分叉**时才写:候选量收窄策略、回滚还是续训、目标子集换思路 等用户判断能改变下一步动作的场景。 + +如果一个 R0/R{n} 没有任何真分叉(只是想"汇报+确认"),不要硬凑 ask、也不要写 gate——直接进下一步。 + 写 proposal 时问自己:**"这句话如果交给一个新加入的产品同学看,他能不能 5 秒内明白要干什么"**。能 = ✅;得回头查 §X.X.X 才能懂 = ❌,重写。 **用户拍板回复后**:append `status:"complete"` 同 step + run_id 一行,gate 卡变 COMPLETED;然后才继续往下走(继续/回滚/调参)。 @@ -524,7 +630,7 @@ xiaomi_cloudml: |---|---|---| | `results/iteration_log.jsonl` | 每轮假设/干预/判定完整记录 | hypothesis / log | | `results/error_registry.jsonl` | 跨轮错误追踪(case_hash → 出错轮次) | log | -| `results/workflow.md` | 每轮回归分析报告 | dist-analysis / report | +| `results/workflow.md` | 每轮回归分析报告 | dist-analysis | | `output/relabel_candidates_.csv` | **阶段一**:预计修改训练集候选清单(complex 翻转候选等) | **dist-analysis(分层结果分析)** | | `results/data_clean_/modified_samples.jsonl` | **阶段二**:确认修改最终落盘(用户审核 1/0 后实际生效的改动) | **augment(数据增强)** | | `results/data_clean_/deleted_samples.jsonl` | 旧数据清洗存档(删除项) | — | diff --git a/skills/model-iteration/references/program.md b/skills/model-iteration/references/program.md index 0e089dd..c79f97e 100644 --- a/skills/model-iteration/references/program.md +++ b/skills/model-iteration/references/program.md @@ -16,12 +16,11 @@ 用户发 **"开始,<需求集合名>"**(如 `"开始,icl_test"`)→ **立即用当前模型跑一轮评测**,不再中途确认参数,直到报告完成: 1. **Setup 检查**:cml 环境 + SSH key(见下)、从 `config.yaml` 读默认参数、初始化 `error_registry.jsonl` -2. **准备评测参数**:确定 `model_path_new`(当前模型)/ `model_path_old`(基线),自动分配 runDic(扫描 `run_history_dir` 最大值 +1);首次评测两者设为同一基准模型 +2. **准备评测参数**:确定 `model_path_new`(当前模型)/ `model_path_old`(基线),runDic 一律由 `eval "$(./scripts/resolve_run_ids.sh)"` 解析出来——**不要自己 `ls workflow5 \| tail -1` 心算 +1**;首次评测两者设为同一基准模型 3. **CML 评测**(Step 0):执行 `cml workflow run`,后台轮询 `metric_diff/lark_template.json` 直到结果就绪 -4. **分层结果分析**(Step 1):按优先级逐层检查 需求集合(≥95%)→ 大盘车载(降幅≤0.3%)→ specific test(降幅≤1%) -5. **问题分析 & 报告**(Step 2):根因归类(reward/data/格式/hparam)、跨轮 diff(persistent/new/regressed)、需求集合深度分析(训练数据关联 + reward 对齐),写入 `results/workflow.md` -6. **决策分支**:全部达标 → 部署并结束;否则 → 形成假设(Step 3)→ 按归因干预:数据增强(Step 4)/ 改 reward / 改格式 / 调超参 → SFT 训练(Step 5)→ 记录到 `iteration_log.jsonl`(Step 6)→ 回到第 3 步评测新 checkpoint -7. **全程不打断用户**,循环直到达标或人类打断 +4. **分层结果分析 + 问题分析 & 报告**(Step 1):按优先级逐层检查 需求集合(≥95%)→ 大盘车载(降幅≤0.3%)→ specific test(降幅≤1%);做根因归类(reward/data/格式/hparam)、跨轮 diff(persistent/new/regressed)、需求集合深度分析(训练数据关联 + reward 对齐)、SFT 天花板诊断,写入 `results/workflow.md` +5. **决策分支**:全部达标 → 部署并结束;否则 → 形成假设(Step 3)→ 按归因干预:数据增强(Step 4)/ 改 reward / 改格式 / 调超参 → SFT 训练(Step 5)→ 记录到 `iteration_log.jsonl`(Step 6)→ 回到第 3 步评测新 checkpoint +6. **全程不打断用户**,循环直到达标或人类打断 > **关键**:"开始"的第一个动作永远是**评测当前模型**,而不是直接训练。先看清楚当前模型在各个集合上的表现和错误分布,再基于证据形成本轮假设 → 做干预。**不要没看评测就开始训练。** @@ -64,7 +63,7 @@ ssh -T git@git.n.xiaomi.com "$AUTORESEARCH_CHAT_ROOT/ai-planning" ``` -之后所有训练数据读写都走 `$AUTORESEARCH_CHAT_ROOT/ai-planning/...`(包括 augment_.jsonl 写入、近邻检索、训练集修改)。`prepare_and_train_sft.py` 用 `Path(__file__)/../ai-planning` 解析数据目录,因为脚本被推到了 `$AUTORESEARCH_CHAT_ROOT/scripts/` 旁边,自然落在 chat 副本上。 +之后所有训练数据读写都走 `$AUTORESEARCH_CHAT_ROOT/ai-planning/...`(包括 augment_.jsonl 写入、近邻检索、训练集修改)。`prepare_and_train_sft.py` 在脚本顶部统一以 `CHAT_ROOT = Path(__file__).resolve().parent.parent` 解析 chat 工作区(脚本本身位于 `$AUTORESEARCH_CHAT_ROOT/scripts/`),`AI_PLANNING` 和 `zk_trainer_dir` 都基于此推导,**不要在 agent 侧手动 patch `Path(__file__)`**——以前需要改两处的坑已在脚本侧统一。 zk_trainer 的 clone 在首次 SFT 前进行(见 §5.0),目标也是 `$AUTORESEARCH_CHAT_ROOT/zk_trainer`。 @@ -185,11 +184,11 @@ for k in prev: **决策**: - 全部达标 → 部署(`python modules/cloudml_deploy.py `),记录,循环结束 -- 不达标 → 进入 Step 2,**分析顺序:需求集合 → 车载大盘 → specific test** +- 不达标 → 在 Step 1 内继续做问题分析(见下文),**分析顺序:需求集合 → 车载大盘 → specific test** -> **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接进入 Step 2 分析基准模型的错误分布,为后续迭代建立对照基准。 +> **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接在 Step 1 内分析基准模型的错误分布,为后续迭代建立对照基准。 -### 2. 问题分析 & 报告 +### 1.A 问题分析 & 报告(在 Step 1 同一 step 内继续做,不再单独开 step) 目标不是"列错误",是回答三个问题: - **根因属于哪一类?**(reward / data / 格式) @@ -207,7 +206,7 @@ for k in prev: | 旧对新错率 | B 数 / 总数 × 100% | | 相对基线变化 | 新模型准确率 − 基线准确率(百分点) | | 准确率 | 该子集 GSB 统一准确率(`cleaned_predict` vs `label`) | -| 分流错误 | `origin_predict_base` 含 `ComplexTask` 且 `origin_predict_dev` 以 `complex=false` 前缀开头。complex 判断本身错了,**即使 cleaned tag 相同也计为错** | +| 分流错误 | base 模型和 dev 模型对该 case 的「是否复杂」二值判断不一致(一边判复杂、一边判不复杂),cleaned tag 是否相同不影响判定。具体怎么从两列原始输出里抽出「是否复杂」这个判断,由 agent 分析前先 head 当前评测产出反推,**不要照抄历史固定字符串** | | 语义错误 | `cleaned_predict_base != cleaned_predict_dev`,意图/tag 本身判错 | | 持久错误 | 该 case 在上一轮也错(查 `error_registry.jsonl`) | | 新引入错误 | 该 case 在上一轮对,本轮错 —— **最危险的信号,说明上轮干预有副作用** | @@ -216,8 +215,8 @@ for k in prev: | 字段 | 含义 | | --- | --- | -| `origin_predict_base` | 旧模型原始输出,可能是 `ComplexTask(tag="...")` | -| `origin_predict_dev` | 新模型原始输出,始终以 `complex=true/false` 前缀开头 | +| `origin_predict_base` | 旧模型原始输出。具体格式(是否含 `complex=` 前缀、tag 包装、自定义 class 名等)以当前评测产出为准,**分析前 head 一下实物** | +| `origin_predict_dev` | 新模型原始输出。同上,格式以当前产出为准 | | `cleaned_predict_*` | 清洗后输出,用于 GSB 对比 | | `label` / `code_label_base` | ground truth;`label` 为空时回退解析 `code_label_base` | @@ -251,7 +250,104 @@ for k in prev: 2. **失败 case 与训练数据的关联**:对每个失败 case 在 `ai-planning/data/train_set/zk_intent/` 下所有 `.jsonl`(包括历史增强 `augment_*.jsonl` 和原始种子训练文件,排除 `*_valid.jsonl`)里做近似检索(前 3 近邻),三档判定: - **无近邻**(最高相似度 < 阈值)→ 分布外,补数据 - **有近邻 + 近邻 label 与本 case gold 全部一致** → 训练数据正确,SFT 学不动 → 加 epoch / 改 reward - - **有近邻 + 任一近邻 label 与 gold 矛盾** ⚠️ → **训练集打错标**,必须**逐条列出该 mislabeled 训练样本**:`{train_file:line, hash, 当前 label, 应改 label, 与失败 case 相似度, 违反的标签规则}`,进入下一轮的数据修订清单 + - **有近邻 + 任一近邻 label 与 gold 矛盾** ⚠️ → **训练集打错标**,必须**落盘到 `$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_.csv`**,列固定为 `file,line,query,old_label,是否改(1/0),建议新label`(注意:CSV 落 `output/`,**不是** `results/`;前端「分层结果分析」卡片就读这一份)。**禁止**只把 mislabeled 列表打印到 stdout 或仅写进 workflow.md——卡片读不到 CSV 等同于本步漏交。本轮无 mislabel 也要写一份只含表头的空 CSV,明示"已检查、无候选" + + ⚠️ **判别器自检(写完任何"把 raw output 抽成可比对值"的函数后、跑全量前必做,跳过=本步无效)**: + + 适用范围:凡是从原始模型输出里抽出某个判定值供后续比对/统计的函数都算判别器,无论抽的是什么—— + - 二值(如 complex true/false、是否为 Agent) + - 字符串/枚举(如 tag 名、function 名、code label) + - 结构化字段(如某个 slot 的值) + + 1. **采端点**(覆盖**所有要区分的类**,每类训练侧 ≥1 条 + eval 侧 ≥1 条;二分类→4 条,N 分类→≥2N 条): + - 训练侧:`head` 几条 `train_set/zk_intent/*.jsonl`,靠 cleaned label / 现成 gold 字段挑出每类各 ≥1 条**已知真值**的样本。 + - eval 侧:`head` 几条评测 CSV 的 `origin_predict_base` / `origin_predict_dev` 列实物,肉眼判一下属于哪类,每类各 ≥1 条。 + - **禁止**只 head 一条就开写代码——一条样本不能验证判别器对所有类都能分对。 + 2. **跑判别器在端点上**(用真实抓到的字符串当输入,**不要**用想象出来的格式): + ``` + 判别器(已知 X 类的训练样本 output) == "X" # 对每个类都跑一遍 + 判别器(已知 X 类的 eval baseline 输出) == "X" # 训练侧+eval 侧两侧都过 + ``` + 任一不对 → 判别器没对齐当前数据格式(最常见死法:把历史文档/旧版本里的 class 名/字符串当成 hardcoded substring 写进了 `'XXX' in out`,结果当前数据里根本没出现过这个字面量),**禁止跑全量**,回去重写。 + 3. **最终报告强制并列原文**:每条候选必须同时打印**原始 output 字符串前 80 字符**和**判别器返回值**,读者肉眼可对一遍——光报"近邻一致 N 条 / mislabel M 条"不够。 + 4. **零计数兜底**:mislabel 跑出 = 0 时**禁止**直接结论"训练集没问题",必须先回去验步骤 2 的端点是否全过——zero-count 默认是判别器 bug 信号、不是结论信号。 + + ⚠️ **算法选型**:错例集 O(几十)、训练集 O(几万) — 不要用复杂度高、要把训练集全量物化成稀疏矩阵或 embedding 的算法(如 `sklearn.TfidfVectorizer.fit_transform` 全量 + cosine — 实测 35k 行字符 n-gram 600s 仍 timeout)。 + + ⚠️ **>1 万行训练集做近邻必须先粗筛后精排,不许直接 O(M·N) 全量比对**——37 错例 × 30k 训练 = 1.1M 次精排,朴素写法实测 ~3 min(v32 R0 case:错例侧逐条扫训练集 + 每例剪枝仍 182s,且后续每加一步 mislabel 定位都要再跑一遍)。两段式: + + 1. **粗筛(倒排索引,全表只扫一次)**:训练集一次性建 `token → set(line_idx)` 倒排表,key 用 query 的 2-gram 字符 token(30k 行 query 通常 3k–5k 唯一 token,建表 O(N) 秒级)。 + 2. **错例侧逐条取候选**:每条错例提它自己的 2-gram tokens,union 训练集对应 posting list → 通常每条得到 50–300 候选行(不是 30k)。 + 3. **精排(小集合上做精细打分)**:仅对粗筛候选跑 Jaccard / char-overlap / Levenshtein 取 top-3。 + + 预算对照(37 × 30k):朴素 O(M·N)=1.1M 次精排(~3 min);倒排粗筛后 ~37×200=7.4k 次精排(~10s)。错例侧的"轻量剪枝"只是常数优化,**不能替代粗筛**——粗筛把候选基数砍到 1/100 才是数量级提速的来源。 + + ⚠️ **训练集 + 倒排索引必须 pickle 缓存到 /tmp,不要每个脚本重建**:dist-analysis 一轮要跑 kn_search / judge_check / mislabel_list / cross_subset 等多个脚本,每个都要读 30k 行 all_train.jsonl + 重建倒排索引——v32 R0 实测 5 次重读累计 ~50s 全是浪费。pod `/tmp` 是本地 SSD,`pickle.dump` 之后所有后续脚本 unpickle <1s。模板: + + ```python + # 每个会读 all_train 的脚本头部统一这一段,cache key 用文件 mtime 自动失效 + import os, json, pickle + from pathlib import Path + ALL_TRAIN = Path(os.environ['AUTORESEARCH_CHAT_ROOT']) / 'ai-planning/data/train_set/zk_intent/all_train.jsonl' + CACHE = Path(f'/tmp/all_train_{int(ALL_TRAIN.stat().st_mtime)}.pkl') # mtime 当版本号 + IDX_CACHE = Path(f'/tmp/all_train_idx_{int(ALL_TRAIN.stat().st_mtime)}.pkl') + + if CACHE.exists(): + rows = pickle.loads(CACHE.read_bytes()) # <1s + else: + rows = [json.loads(l) for l in ALL_TRAIN.open('r', encoding='utf-8')] # 5–15s NFS + CACHE.write_bytes(pickle.dumps(rows)) + + # 倒排索引同样缓存(first script 建好就一直复用) + if IDX_CACHE.exists(): + inv = pickle.loads(IDX_CACHE.read_bytes()) + else: + inv = {} + for i, r in enumerate(rows): + q = r.get('query', '') + for tok in {q[k:k+2] for k in range(len(q)-1)}: # 2-gram + inv.setdefault(tok, set()).add(i) + IDX_CACHE.write_bytes(pickle.dumps(inv)) + ``` + + 忌:每个脚本头都重写一遍 `for line in open(jsonl): json.loads(line)`;用 `/tmp/.pkl` 而忽略 mtime(训练集中途被改不失效);把 cache 写到 NFS(白做了)。 + + ⚠️ **stdout 是下一轮 LLM 的 input,分析脚本只 print 决策聚合,明细落 `/tmp/__detail.json`**:每个分析脚本的 stdout 会原样喂回 LLM 当下一轮上下文,每 KB 都要 prefill。v32 R0 实测 kn_search 一次 stdout 18KB(37 错例 × 3 邻居 × 6 列)、judge_check 4.7KB、mislabel 11.3KB,整个 dist-analysis 累计 ~50KB 全是重复模式行。kn_search 模板(其他步骤同构): + + ```python + # 不要 print 整张 candidate × neighbors 表;只 print 三档分布 + 每档 top-3 范例 + import json, collections, os + buckets = collections.Counter() + exemplars = collections.defaultdict(list) + detail = [] # 全量明细,落盘不 print + for case in failed_cases: + neigh = topk(case, k=3) # 粗筛 → 精排已就位 + verdict = classify(case, neigh) # 'no_neighbor' / 'consistent' / 'mislabel' + buckets[verdict] += 1 + if len(exemplars[verdict]) < 3: + exemplars[verdict].append({ + 'query': case['query'][:50], + 'gold': case['gold'], + 'top_neigh': [{'q': n['query'][:50], 'label': n['label']} for n in neigh], + }) + detail.append({'case': case['idx'], 'verdict': verdict, 'neigh': neigh}) + + detail_path = f'/tmp/kn_search_{os.environ["RUN_DIC"]}_detail.json' + json.dump(detail, open(detail_path, 'w'), ensure_ascii=False) + + # 只 print 决策需要的聚合(目标 < 2KB) + print(f'[kn_search] total={len(failed_cases)} buckets={dict(buckets)} detail={detail_path}') + for v, exs in exemplars.items(): + print(f'-- top-3 of {v} --') + for e in exs: + print(json.dumps(e, ensure_ascii=False)) + ``` + + 忌:把每条错例的 k 个邻居全 print;`DataFrame.to_string()` 直接喂 stdout(自带 padding,一张表 2KB+);用 `cat > /tmp/x.py <` 回显进 stdout,一次累计 1–2KB),改 `python3 - <<'PY' ... PY` 或 `python3 -c "$(base64 -d <<<'...')"` 注入。 + + ⚠️ **大文件 I/O**:>10MB 量级的远端 CSV/JSONL(如本步要读的 `specific_comparison.csv`,常见 90+MB),**先用 bash `awk/grep/head` 在远端切目标子集再处理**。直接 `pd.read_csv` 整体加载、或把 DataFrame 当返回值穿过工具调用 → 会撞 runtime 输出/返回大小限制(实测 99MB 直接读报"太大了")。 + + ⚠️ **执行通道**:所有 python 一律走 `bash` + `/tmp/xxx.py` + `python3 -u … | tee /tmp/xxx.log`,禁用 `python_exec`。`python_exec` stdout 全缓冲、turn 结束才 flush,超时被 kill 时 buffer 直接丢光(实测 600s/1800s 撞墙后 `tool_result` 没有任何 stdout 字段)。bash + tee 模式实时落盘,超时也能 `tail /tmp/xxx.log` 看到死在哪个 phase。 3. **Reward 对齐检查**(全量失败 case,不抽样):用 `zk_reward_fn` 对"正确 label"和"实际输出"分别打分,验证 reward 方向是否和准确率一致。如果 reward 给错误输出的分更高 → reward 函数本身就有问题。 #### 2.4 根因归类 @@ -499,10 +595,14 @@ def get_label(row: dict) -> str: return clb.strip("[]'").replace('\\n', '\n ') def classify(row: dict) -> tuple[bool, bool]: - """返回 (is_triage_err, is_semantic_err)""" - opb, opd = row['origin_predict_base'], row['origin_predict_dev'] + """返回 (is_triage_err, is_semantic_err) + + is_triage_err 的判定无法用通用代码——agent 先 head 当前 + origin_predict_base / origin_predict_dev 实物,根据真实格式现写 parser + 抽出 base / dev 的「是否复杂」二值判断再比对。 + """ cpb, cpd = row['cleaned_predict_base'], row['cleaned_predict_dev'] - is_triage = 'ComplexTask' in opb and opd.startswith('complex=false') + is_triage = ... # TODO: 见 docstring,按当前评测产出格式现写 is_semantic = cpb != cpd return is_triage, is_semantic @@ -544,7 +644,7 @@ for r in b_rows: stats[sc][tag] += 1 ``` -### 3. 本轮假设(基于 Step 1–2 的证据) +### 3. 本轮假设(基于 Step 1 的证据) **只有证据充分时才往下走。** 在开始训练前,写入 `results/iteration_log.jsonl` 的 `hypothesis` 字段: - **假设**:本轮要验证什么?(例:"分流错误主因是 reward 对 complex 误判的惩罚太弱") @@ -557,14 +657,14 @@ for r in b_rows: ### 4. 数据生成(GPT-5.4 从 badcase 增强) -**只在 Step 2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由: +**只在 §2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由: | 归因 | 本步动作 | 跳到哪步 | |---|---|---| | `data` | 做 Step 4 数据增强(在已有数据基础上增删改) | Step 5(SFT 重训) | | `格式`(prompt / label schema 不对齐) | 改 prompt 模板或 label 渲染逻辑 | Step 5(SFT 重训,格式变了必须重新 SFT) | | `hparam` | 改 `config.yaml`(LR / batch / epoch 等) | Step 5(SFT 重训) | -| 假设本身站不住(Step 2 证据不支持 Step 3 的假设) | 不做任何训练 | **回 Step 3**,重写假设 | +| 假设本身站不住(Step 1 证据不支持 Step 3 的假设) | 不做任何训练 | **回 Step 3**,重写假设 | 换句话说:Step 4 是"data 归因专属"的干预入口,其他归因各有各的干预点,往下找对应的 Step 就行。 @@ -643,14 +743,44 @@ with open(out_csv, 'w', encoding='utf-8-sig') as fp: 候选阶段(246 条等量级)的 CSV 必须写到 `output/relabel_candidates_.csv` 让分析卡片可读;确认修改阶段(用户审核后落盘的 135 条)写入 `results/data_clean_/modified_samples.jsonl` 让数据增强卡片可读。**不要混落**——把候选 CSV 写到 `/tmp/` 或 `data_clean_/` 都会让 UI 看不到。 -**Step B:量级判定(决定是否走人审)** +> **runDic 命名语义(backend 已对齐,改 backend 时勿 revert)**:augment 类产物(`data_clean_/`、`augment_raw/augment__raw.jsonl`、`zk_intent/augment_.jsonl`、`label_master_review.jsonl`)的 `` 永远是**「源轮次的 runDic」**,也就是 `R{n-1}.runDic`——agent 在 R{n} 跑 augment 时处理的是 R{n-1} 评测落地的错例,文件命名沿用 R{n-1} 的 workflow id。Backend 的 step-detail 端点 (`server.py` 内 `_step_artifact_paths('augment', ...)` 调用方) 在 `step=='augment'` 时也是查 `R{n-1}` 的 runDic,跟落盘命名一一对应。其他 step(cml / dist-analysis / hypothesis / gold-drift)的产物按本轮自己的 runDic 命名,没有偏移。**不要把 augment 产物改名成 R{n} 的 runDic**——会同时打破历史归档和 backend 解析。 +> +> ⚠️ **runDic 不许手算**:augment 落盘、SFT yaml、归档目录、cml workflow run 用的 runDic **必须**从 `scripts/resolve_run_ids.sh` 取,不准 agent 自己 `ls workflow5 | tail -1` 然后心算 +1。规则简单到没必要靠模型: +> +> ```bash +> eval "$(./scripts/resolve_run_ids.sh)" +> # 现在 shell 里有: +> # $SFT_RUNDIC = R{n-1}.runDic (augment 落盘 / yaml / 归档全用这个,不 +1) +> # $EVAL_RUNDIC = R{n-1}.runDic + 1 (仅 cml workflow run 时使用,唯一一次 +1) +> ``` +> +> 已踩坑:R2 augment 把 `data_clean_/` 写成了 `EVAL_RUNDIC`(多 +1 一次),跳过了 R1.runDic 整段命名空间,前端 augment 卡片直接读不到。**写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。 -| 候选量 | 处理 | +**Step A.5:label-master 预审(量级判定之前必跑,强制)** + +把 Step A 产出的候选交给 label-master 做语义判定,**用 label-master 的"推荐标签"覆盖候选原始推测的 new_label**,目的是在拿去人审之前先消化掉 label-master 自己就能定的那部分,缩小残留候选量。 + +具体步骤: + +1. 抽出每条候选的 `(file, line, query, old_label, suspected_new_label)` +2. **逐条**调 `Skill(skill="label-master", args=...)`——每次只传**一条** `(query, old_label)`(**不**给 suspected_new_label,避免锚定),让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 输出 → `{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`。 + - ⚠️ **禁止把多条 query 塞进同一次 Skill 调用**——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑) + - 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐 +3. 把 label-master 的"推荐标签"**回写到** `relabel_candidates_.csv` 覆盖原 `建议新label` 列;新增列 `verdict`、`label_master_理由`,便于回查 +4. 收尾时按以下规则筛 candidate list(**残留候选 = 真正进入 Step B 的列表**): + - `推荐标签 == old_label`:label-master 不认同要改 → 从候选里**剔除**(这条原 label 可能本来就是对的) + - `推荐标签 != old_label`:label-master 同意要改(不论与 suspected_new_label 是否一致)→ **保留**,`new_label = 推荐标签` + +**Step B:量级判定(基于 label-master 处理后的残留候选量)** + +| 残留候选量 | 处理 | |---|---| -| ≤ 50 条 | 程序化 sanity check + 自动改 | -| 51 ~ 200 条 | **必须**全量导出到飞书 sheet 让人逐条审 1/0(不抽样) | +| ≤ 50 条 | 程序化 sanity check + 按 label-master 推荐自动改(不再走人审) | +| 51 ~ 200 条 | **必须**全量导出到飞书 sheet 让人逐条审 1/0(不抽样);导出时带 `query / old_label / 推荐标签 / label_master_理由` 四列 | | > 200 条 | **强制 H-i-T-L 介入**(触发条件 #3),让人定更精细 pattern 收窄 | +注:阈值仍按原来 autoresearch 的人审规则;变化只在于残留候选已经过 label-master 一遍语义筛减,避免把 label-master 自己就能定的条目也塞进飞书让人重审。 + **Step C:备份(强制,覆盖前必做)** ```bash @@ -725,7 +855,7 @@ ai-planning/data/train_set/zk_intent/ #### 4.1 输入 -- **Badcase 来源**:Step 2 里 `纯模型GSB == 'B'` 的 case。按两个维度排优先级: +- **Badcase 来源**:Step 1 里 `纯模型GSB == 'B'` 的 case。按两个维度排优先级: 1. **测试集维度**:本次需求集合 > 其他 specific test / 大盘(需求集合是当前瓶颈,优先补) 2. **跨轮维度**:`new` / `regressed` > `persistent`(新引入/回退的先处理,持久错误兜底) @@ -969,7 +1099,7 @@ def check_label_rules(query: str, label: str, history: list = None) -> tuple[boo ##### 触发时机 -每轮 Step 2 错例分析后,对**所有未被 R1~R{N} 已知规则覆盖的错例**做自动归纳。 +每轮 Step 1 错例分析后,对**所有未被 R1~R{N} 已知规则覆盖的错例**做自动归纳。 ##### Confidence 的定义(两个独立指标,必须同时满足) @@ -1103,14 +1233,14 @@ JSONL 行里的 `sub_cate` 字段仅用于内部路由/去重,归档时丢弃 #### 4.5 label-master 标签复核(落盘后、SFT 前,**强制**) -任何写入训练目录的修改/新增样本都必须经 [label-master skill](../../label-master/SKILL.md) 复核**两层**:格式 + 语义。**没过这关不许进 Step 5。** +任何写入训练目录的修改/新增样本都必须经 [label-master skill](../../label-master/SKILL.md) 复核:格式 + 语义。**没过这关不许进 Step 5。** -**复核范围**(两份必须全过): +**复核范围**(两份必须过格式层;语义层只对 H2): -| 文件 | 来源 | 复核什么 | -|---|---|---| -| `$AUTORESEARCH_CHAT_ROOT/results/data_clean_/modified_samples.jsonl` | H1 改标(complex 翻转 / tag 改写等) | 改后的 `output` 字段 | -| `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_.jsonl` | H2 仿写(新增训练样本) | `output` 字段 | +| 文件 | 来源 | 层 1(格式) | 层 2(语义,Skill 调 label-master) | +|---|---|---|---| +| `$AUTORESEARCH_CHAT_ROOT/results/data_clean_/modified_samples.jsonl` | H1 改标(complex 翻转 / tag 改写等) | ✅ 必跑 | ❌ 已在 §4.0.1 Step A.5 完成,**不再重跑**(label-master 不会推翻自己的推荐,重跑只是浪费 token) | +| `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_.jsonl` | H2 仿写(新增训练样本,未经过 §4.0.1) | ✅ 必跑 | ✅ 必跑 | **层 1:格式校验(确定性,自动跑)** @@ -1128,16 +1258,17 @@ python skills/label-master/scripts/validate_label_output.py \ 任一行 fail(tag 不存在 / Agent 包装错 / function 引用错)→ **必须修,不许直接跳过**。修完原地重跑直到全过。 -**层 2:语义复核(Skill 调用 label-master Agent)** +**层 2:语义复核(Skill 调用 label-master Agent,仅 H2)** -格式过了不代表标对了。每条修改/新增样本要用 label-master 做边界判断: +H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重跑**。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定: -1. 抽出 `(query, output_after)` 或 `(query, output)` 对,按 `sub_cate` 分组(每组 ≤30 条作为一批) -2. 每批用 `Skill(skill="label-master", args=...)` 调用,args 里给: - - 全部 `(query, 当前 label)` 对 - - 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定每条 - - 输出格式:每条 → `{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}` -3. 收集 verdict,写入 `$AUTORESEARCH_CHAT_ROOT/results/data_clean_/label_master_review.jsonl`,每行一条 verdict +1. 从 `augment_.jsonl` 抽出 `(query, output)` 对 +2. **逐条**调 `Skill(skill="label-master", args=...)`——每次只传**一条** `(query, 当前 label)`: + - 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定该条 + - 输出格式:`{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}` + - ⚠️ **禁止把多条 query 塞进同一次 Skill 调用**——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑) + - 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐 +3. 收集 verdict,写入 `$AUTORESEARCH_CHAT_ROOT/results/data_clean_/label_master_review.jsonl`,每行一条 verdict(只含 H2 条目) **verdict 处置规则(自动)**: @@ -1345,19 +1476,25 @@ R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启 ##### 一键提交脚本 +⚠️ **runDic 不许手敲**:先 `eval` 一下 `resolve_run_ids.sh` 拿到这一轮的 `$SFT_RUNDIC` / `$EVAL_RUNDIC`,再原样传给 `submit_sft_via_cml.sh`。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 `data_clean_/` 跳过了 R1 命名空间。 + ```bash cd "$AUTORESEARCH_CHAT_ROOT" -./scripts/submit_sft_via_cml.sh [PREV_RUNDIC] +eval "$(./scripts/resolve_run_ids.sh)" # 解析出 $SFT_RUNDIC / $EVAL_RUNDIC +./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC" -# 例:R29 训练(上一轮 R28) -./scripts/submit_sft_via_cml.sh 17756 17755 +# 等价示例(仅供阅读,**实际调用一律走 eval 上面那行**): +# R29 上一轮已落盘的 workflow 是 17755,因此 SFT_RUNDIC=17755, EVAL_RUNDIC=17756 +# ./scripts/submit_sft_via_cml.sh 17755 17756 ``` +`submit_sft_via_cml.sh` 接受三个位置参数:` [PREV_RUNDIC]`。`PREV_RUNDIC` 默认 `SFT_RUNDIC-1`,仅用作上一轮 `sft_output_r` 改名。脚本内部断言 `EVAL_RUNDIC > SFT_RUNDIC`,否则直接报错退出。 + 脚本做的事: -1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 RUNDIC / PREV_RUNDIC) +1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 `{RUNDIC}` → `$SFT_RUNDIC`、`{PREV_RUNDIC}` → `$PREV_RUNDIC`) 2. `cml custom_train submit --filename ` 提交任务,拿 `JobID` 3. 后台 watcher 进程:每 60s `cml custom_train describe` 轮询任务状态 -4. 任务 succeed → 验证 `sft_output/_SUCCESS` 存在 → 自动起 cml workflow run 评测 +4. 任务 succeed → 验证 `sft_output/_SUCCESS` 存在 → 自动起 cml workflow run,**评测的 `runDic=$EVAL_RUNDIC`**(这是整条流水线唯一一次 +1) 5. 任务 failed/killed → 写日志报警 ##### yaml 模板要点(`scripts/sft_train_job.yaml.tpl`) @@ -1523,9 +1660,14 @@ tail -f /tmp/r_logs/cml_watcher.log 1. **触发原因**:哪一条信号 + 具体数字 2. **现状量化**:目标子集 +X / 其他子集 -Y / 大盘 ±Z -3. **2-4 个选项**(用 AskUserQuestion 工具):每个选项含预期收益 + 风险 -4. **推荐选项**:基于经验给出推荐(标 "(推荐)") -5. **本地产物**:全量错例 csv / 飞书 sheet 链接 +3. **全量 case 直接铺进对话**(凡是"该不该改 / 该不该删 / 该不该新增 N 条"型决策都适用,**包括但不限于** H-i-T-L #1/#2/#3/#6 与 T1/T2/T3/T5): + - 必须把"经过 label-master 认证的全部候选"按 pattern 分组、每条一行(紧凑表)贴到**同一条** ask 消息体里,不是只给 CSV 路径或飞书链接,**也不要分段连发**——一段全铺,让用户一次滚完 + - **不允许抽样、不允许"前 N 条样例"、不允许"代表 case"**——抽样让用户看不到边界外的长尾,决策无意义 + - 每条至少含:`query / old_label / 推荐标签 / 理由(≤40字)`;H1(改标)类必含 `file:line`,H2(仿写)类必含 `pattern_id` + - **高置信 + 低置信(潜在影响半径 / 类似 case)都得铺,缺一不可**:当 ask 里出现「确认要改 X 条 + 还有 Y 条类似的 / 潜在影响 Y 条 / 同 pattern 还有 Y 条疑似」这种二段叙述时,**Y 条也必须全量铺进同一条对话消息**(同样按 pattern 分组、紧凑表),并对每条标 `confidence=high / low`。理由:用户的决策本身就是「只改 X」vs「扩到 X+Y」vs「再收窄 pattern」,看不到 Y 就只能瞎选。**只展示高置信 X 条、把 Y 条藏在数字背后**视为违反 skill。 +4. **2-4 个选项**(用 AskUserQuestion 工具):每个选项含预期收益 + 风险 +5. **推荐选项**:基于经验给出推荐(标 "(推荐)") +6. **本地产物**:全量错例 csv / 飞书 sheet 链接(作为对话铺陈的备份和事后回查渠道,**不替代**第 3 项) ### 介入后的恢复 diff --git a/skills/model-iteration/scripts/prepare_and_train_sft.py b/skills/model-iteration/scripts/prepare_and_train_sft.py index 1dcb978..014e58c 100644 --- a/skills/model-iteration/scripts/prepare_and_train_sft.py +++ b/skills/model-iteration/scripts/prepare_and_train_sft.py @@ -26,7 +26,8 @@ from pathlib import Path logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) -AI_PLANNING = Path(__file__).resolve().parent / "ai-planning" +CHAT_ROOT = Path(__file__).resolve().parent.parent +AI_PLANNING = CHAT_ROOT / "ai-planning" TRAIN_SET_DIR = AI_PLANNING / "data" / "train_set" DATA_TRAIN_DIR = AI_PLANNING / "data_train" GENERATE_TRAIN_SCRIPT = DATA_TRAIN_DIR / "generate_train.py" @@ -148,7 +149,7 @@ def run_sft_training(data_dir: str, model_path: str, model_type: str, train_outp epochs: int = 3, lr: float = 1e-5, max_seq_length: int = 1024, per_device_batch: int = 1, grad_accum: int = 1): """基于 zk_trainer 的 accelerate launch 方式启动 SFT 训练。""" - zk_trainer_dir = str(Path(__file__).resolve().parent / "zk_trainer") + zk_trainer_dir = str(CHAT_ROOT / "zk_trainer") if not os.path.isdir(zk_trainer_dir): log.error(f"zk_trainer 目录不存在: {zk_trainer_dir}") return None diff --git a/skills/model-iteration/scripts/resolve_run_ids.sh b/skills/model-iteration/scripts/resolve_run_ids.sh new file mode 100755 index 0000000..056711e --- /dev/null +++ b/skills/model-iteration/scripts/resolve_run_ids.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# resolve_run_ids.sh —— SFT/EVAL runDic 单一可信来源 +# +# 规则(与 program.md §746 + §5.2 对齐): +# SFT_RUNDIC = workflow5/ 下「已落盘」(含 metric_diff/lark_template.json)的最大 runDic +# 即 R{n-1}.runDic —— augment 落盘、SFT yaml、modified_samples 归档全用这个值 +# EVAL_RUNDIC = SFT_RUNDIC + 1 +# 仅 cml workflow run 提交本轮评测时使用 +# +# 用法: +# eval "$(./scripts/resolve_run_ids.sh)" +# echo "$SFT_RUNDIC $EVAL_RUNDIC" +# +# 失败行为:找不到任何已落盘 workflow 时打 ERR 到 stderr 并 exit 1,调用方 set -e 时直接终止。 + +set -euo pipefail + +WF_ROOT=${WF_ROOT:-/mnt/xiaoai-zk-model-train-tj5/workflow5} + +if [ ! -d "$WF_ROOT" ]; then + echo "ERR: WF_ROOT=$WF_ROOT 不存在或不可读" >&2 + exit 1 +fi + +# 只挑「已落盘」的 workflow:必须存在 metric_diff/lark_template.json +# 这样可以避免 CML 创建了空目录就被算成最大值 +MAX=$( + for d in "$WF_ROOT"/workflow*; do + [ -d "$d" ] || continue + [ -f "$d/metric_diff/lark_template.json" ] || continue + name=$(basename "$d") + echo "${name#workflow}" + done | sort -n | tail -1 +) + +if [ -z "$MAX" ]; then + echo "ERR: $WF_ROOT 下没有任何已落盘的 workflow(含 metric_diff/lark_template.json)" >&2 + exit 1 +fi + +echo "SFT_RUNDIC=$MAX" +echo "EVAL_RUNDIC=$((MAX+1))" diff --git a/skills/model-iteration/scripts/sft_train_job.yaml.tpl b/skills/model-iteration/scripts/sft_train_job.yaml.tpl new file mode 100644 index 0000000..6b926f6 --- /dev/null +++ b/skills/model-iteration/scripts/sft_train_job.yaml.tpl @@ -0,0 +1,117 @@ +jobName: "sft-train-r{RUNDIC}" +description: "AutoResearch R{RUNDIC} SFT training (40k+ samples, qwen3-4B base, 3 epochs FSDP2)" +accessType: PUBLIC + +imageConfig: + imageUrl: micr.cloud.mioffice.cn/vllm-image/ai-arch-llm-prod:vllm-v0.12.0-f098b188 + imageCommand: |- + set -ex + cd {AUTORESEARCH_ROOT} + + # 0a. 验环境 + 装缺的包(vllm 镜像 py3.12 + torch 已含) + python3 --version + python3 -c "import torch; print(f'torch={torch.__version__}')" + pip install -q --no-deps "accelerate==1.7.0" 2>&1 | tail -3 + pip install -q --ignore-installed blinker 2>&1 | tail -2 + pip install -q peft 2>&1 | tail -3 + pip install -q wandb bitsandbytes 2>&1 | tail -3 + pip install -q luigi mlflow scikit-learn openpyxl pyyaml sentencepiece tiktoken protobuf pynvml datasets 2>&1 | tail -3 + pip install -q "transformers>=4.45" 2>&1 | tail -3 + python3 -c 'import torch, accelerate, transformers, peft; print(torch.__version__, accelerate.__version__, transformers.__version__, peft.__version__)' + + # 0c. 把 HF cache 重定向到容器本地大盘(默认在 ~/.cache 可能是 juicefs,mmap 易 SIGBUS) + export HF_HOME=/tmp/hf_cache + export HF_DATASETS_CACHE=/tmp/hf_cache/datasets + export TRANSFORMERS_CACHE=/tmp/hf_cache/transformers + mkdir -p /tmp/hf_cache/datasets /tmp/hf_cache/transformers + df -h /tmp /dev/shm 2>/dev/null || true + + # 强制 datasets 加载进内存而不是 arrow mmap(避免 /dev/shm 溢出 SIGBUS) + export HF_DATASETS_IN_MEMORY_MAX_SIZE=20000000000 + export HF_DATASETS_NUM_PROC=1 + export TOKENIZERS_PARALLELISM=false + export OMP_NUM_THREADS=1 + export MKL_NUM_THREADS=1 + # 关闭 NCCL 用 shm 通信(改 socket 通道) + export NCCL_SHM_DISABLE=1 + export NCCL_P2P_DISABLE=0 + export NCCL_DEBUG=WARN + + # 0b. 把上一轮产出搬走(cml job 内幂等,本地搬过就跳过) + if [ -d sft_output ] && [ ! -d sft_output_r{PREV_RUNDIC} ]; then + mv sft_output sft_output_r{PREV_RUNDIC} + fi + rm -rf sft_output + + # 1. 组装数据 + python3 {AUTORESEARCH_ROOT}/prepare_and_train_sft.py prepare \ + --output_dir {AUTORESEARCH_ROOT}/sft_data + + # 2. 启动训练(绝对路径,避免相对路径在容器内找不到 zk_trainer) + python3 {AUTORESEARCH_ROOT}/prepare_and_train_sft.py train \ + --data_dir {AUTORESEARCH_ROOT}/sft_data \ + --model_path /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507 \ + --model_type qwen3 \ + --train_output {AUTORESEARCH_ROOT}/sft_output \ + --epochs 3 --lr 1e-5 + + # 3. 训练成功标记(被 watcher 检测) + [ -f {AUTORESEARCH_ROOT}/sft_output/config.json ] && \ + touch {AUTORESEARCH_ROOT}/sft_output/_SUCCESS + +# 挂载 wangsenhao + xiaoai-zk-model-train-tj5 + verl_zk 所在卷 +juiceFsMountConfigs: + - volume: wangsenhao + juiceFsCluster: tj5-common + subPath: / + mountPath: /mnt/wangsenhao + readOnly: false + - volume: xiaoai-zk-model-train-tj5 + juiceFsCluster: tj5-common + subPath: / + mountPath: /mnt/xiaoai-zk-model-train-tj5 + readOnly: false + +envConfigs: + - key: PYTHONPATH + value: {AUTORESEARCH_ROOT}/zk_trainer + - key: WANDB_DISABLED + value: "true" + - key: WANDB_MODE + value: offline + - key: VOLUME_PREFIX + value: /mnt/xiaoai-zk-model-train-tj5 + - key: HF_HOME + value: /mnt/wangsenhao/.hf_cache + +# h20-96g 8 卡 FSDP2 +queueId: "6052" +priority: 5 +preemptible: false +framework: pytorch +resourceConfigs: + - nodeRole: worker + nodeNumber: 1 + perNodeResourceSpec: + resourcePriority: GUARANTEED + resourceName: cloudml.ng2h20-8-8.20-199 + resourceNumber: 8 + +# 故障自动重试(节点级失败) +retryConfig: + enableRetry: true + maxRetryTimes: 2 + policySets: + - NodeFailure + +# 失败/完成飞书告警 +alertConfig: + enableAlert: true + alertItems: + - alertConditions: + - FAILED + - SUCCEED + alertLevel: P2 + alertReceivers: + persons: + - wangsenhao diff --git a/skills/model-iteration/scripts/submit_sft_via_cml.sh b/skills/model-iteration/scripts/submit_sft_via_cml.sh new file mode 100755 index 0000000..f476745 --- /dev/null +++ b/skills/model-iteration/scripts/submit_sft_via_cml.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# 用 cml custom_train submit 提交 SFT 训练,训练完成后自动起 cml workflow run 评测 +# +# 用法: +# ./submit_sft_via_cml.sh [PREV_RUNDIC] +# +# 推荐调用方式(runDic 由 resolve_run_ids.sh 决定,不要手敲): +# eval "$(./scripts/resolve_run_ids.sh)" +# ./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC" +# +# 语义(与 program.md §746 + §5.2 对齐): +# SFT_RUNDIC = R{n-1}.runDic(augment / yaml / 归档全用这个,不 +1) +# EVAL_RUNDIC = R{n-1}.runDic + 1(仅本步起 eval 时使用,仅在这一处 +1) +# PREV_RUNDIC = SFT_RUNDIC - 1(默认;只用作上一轮 sft_output 改名) + +set -euo pipefail + +SFT_RUNDIC=${1:?usage: $0 [PREV_RUNDIC]} +EVAL_RUNDIC=${2:?usage: $0 [PREV_RUNDIC]} +PREV_RUNDIC=${3:-$((SFT_RUNDIC-1))} + +# 防御:EVAL_RUNDIC 必须严格大于 SFT_RUNDIC,否则一定是调用方算错 +if [ "$EVAL_RUNDIC" -le "$SFT_RUNDIC" ]; then + echo "[cml-sft] ❌ EVAL_RUNDIC ($EVAL_RUNDIC) 必须 > SFT_RUNDIC ($SFT_RUNDIC)" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT=${AUTORESEARCH_ROOT:-/mnt/wangsenhao/autoresearch-zk} +MODEL_OLD=${MODEL_OLD:-/mnt/wangsenhao/verl_zk/qwen4b_cispo_wokl_add_bvt_2/global_step_5/actor/huggingface} +TPL=${SFT_TRAIN_JOB_TEMPLATE:-$SCRIPT_DIR/sft_train_job.yaml.tpl} +YAML=/tmp/sft_train_job_r${SFT_RUNDIC}.yaml + +if [ -f ~/.cloudml-cli/.profile ]; then + source ~/.cloudml-cli/.profile +else + echo "[cml-sft] ❌ 未找到 ~/.cloudml-cli/.profile,请先安装并初始化 cml" >&2 + exit 1 +fi + +# 1. 渲染 yaml 模板(用 SFT_RUNDIC,不是 EVAL_RUNDIC) +sed \ + -e "s|{RUNDIC}|${SFT_RUNDIC}|g" \ + -e "s|{PREV_RUNDIC}|${PREV_RUNDIC}|g" \ + -e "s|{AUTORESEARCH_ROOT}|${ROOT}|g" \ + "$TPL" > "$YAML" +echo "[cml-sft] yaml: $YAML (SFT_RUNDIC=$SFT_RUNDIC EVAL_RUNDIC=$EVAL_RUNDIC PREV_RUNDIC=$PREV_RUNDIC)" + +# 2. 提交训练任务 +SUBMIT_OUT=$(cml custom_train submit --filename "$YAML" 2>&1) +echo "$SUBMIT_OUT" +JOB_ID=$(echo "$SUBMIT_OUT" | grep -oE 't-[0-9]+-[a-z0-9]+' | head -1) +if [ -z "$JOB_ID" ]; then + echo "[cml-sft] ❌ 提交失败" >&2 + exit 1 +fi +echo "[cml-sft] ✅ JobID: $JOB_ID" + +# 3. 后台 watcher:等训练成功 → 自动起评测(用 EVAL_RUNDIC) +WATCHER_LOG=/tmp/r${SFT_RUNDIC}_logs/cml_watcher.log +mkdir -p "$(dirname "$WATCHER_LOG")" + +nohup bash -c ' +JOB_ID='"$JOB_ID"' +SFT_RUNDIC='"$SFT_RUNDIC"' +EVAL_RUNDIC='"$EVAL_RUNDIC"' +LOG='"$WATCHER_LOG"' +ROOT='"$ROOT"' +MODEL_NEW="$ROOT/sft_output" +MODEL_OLD='"$MODEL_OLD"' +EVAL_WORKFLOW_ID=f-20260408161444-wu3pz +EVAL_VERSION=v28 + +source ~/.cloudml-cli/.profile + +echo "[$(date)] 等待 cml job $JOB_ID 完成..." >> "$LOG" +while true; do + STATE=$(cml custom_train describe "$JOB_ID" 2>/dev/null | grep -o "\"state\": \"[a-z]*\"" | head -1 | sed "s/.*: \"//;s/\"//") + case "$STATE" in + succeed) + echo "[$(date)] 训练成功" >> "$LOG" + break ;; + failed|killed) + echo "[$(date)] ❌ 训练 $STATE" >> "$LOG" + exit 1 ;; + *) + sleep 60 ;; + esac +done + +# 验证产出 +[ ! -f "$MODEL_NEW/_SUCCESS" ] && [ ! -f "$MODEL_NEW/config.json" ] && { + echo "[$(date)] ❌ 产出缺失" >> "$LOG"; exit 1 +} + +# 起评测(这里且仅这里用 EVAL_RUNDIC) +echo "[$(date)] 启动 CML 评测 EVAL_RUNDIC=$EVAL_RUNDIC" >> "$LOG" +cml workflow run \ + --workflow_id $EVAL_WORKFLOW_ID --version $EVAL_VERSION \ + --global_inputs runDic=$EVAL_RUNDIC \ + --global_inputs model_path_new=$MODEL_NEW \ + --global_inputs model_path_old=$MODEL_OLD >> "$LOG" 2>&1 +echo "[$(date)] cml workflow run 提交完成 (workflow$EVAL_RUNDIC)" >> "$LOG" +' > /tmp/r${SFT_RUNDIC}_logs/watcher_runner.log 2>&1 & + +WATCHER_PID=$! +echo "[cml-sft] watcher PID: $WATCHER_PID(log: $WATCHER_LOG)" + +# 4. 立即输出可查看命令 +cat <