mcpField
E
experiment-queue

experiment-queue

github.com/wanshuiyin云与 DevOps18.2k449 次安装4 天前更新
代码仓库

关于 experiment-queue

experiment-queue 是用于通过 SSH 在远程 GPU 服务器上编排批量机器学习实验的技能,面向多任务网格、种子扫描和分阶段运行。它以清单和状态机管理任务,并自动处理 OOM 重试、过期会话清理及波次切换。

主要功能

  1. 01从清单、参数网格或自然语言构建任务
  2. 02通过 SSH 在远程 GPU 服务器运行任务
  3. 03跟踪任务状态并持续保存队列状态
  4. 04检测 OOM 并延迟重试
  5. 05按条件清理会话并切换运行波次
  6. 06支持教师与学生阶段依赖

适用场景

  1. 01并行运行多种子实验网格
  2. 02按波次执行分阶段实验
  3. 03训练教师模型后自动启动学生任务
  4. 04为 OOM 失败任务安排重试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wanshuiyin/auto-claude-code-research-in-sleep --skill experiment-queue

安装后在 Claude Code 中输入/experiment-queue确认已加载。

附带文件scripts/build_manifest.pyscripts/queue_manager.py

详细介绍

实验队列

⏱ 外部轮询:仅用于查看状态。 此技能已运行自己的服务端分离式调度器(每 60 秒轮询一次 + depends_on + 波次转换)。 使用其状态输出查看过夜任务进度(已完成 N 个 / 正在运行 N 个 / 待处理 N 个);不要再套用第二层 /loop / CronCreate 轮询——这会在未协调的时钟上重复调度,并与其旨在防止的波次转换逻辑发生竞态。参见 shared-references/external-cadence.md (“不要重复现有调度器”)。

在 SSH 远程 GPU 服务上编排大批量 ML 实验,并妥善跟踪状态、重试 OOM、清理过期任务和转换波次。

何时使用此技能

当 /run-experiment 无法满足需求时使用:

  • ≥10 个任务,需要跨 GPU 批处理
  • 多随机种子扫描(例如,21 个种子 × 12 个单元)
  • 波次转换(运行第 1 波,等待,运行第 2 波,等待,运行第 3 波……)
  • 教师模型 + 学生模型链(先训练教师模型,再进行蒸馏;教师模型完成后自动触发学生模型)
  • 容易发生 OOM 的配置,需要换用其他 GPU 重试或等待
  • 混合种子网格,需要重新运行失败的单元

以下情况不要使用:

  • 单次临时实验(使用 /run-experiment)
  • Modal/Vast.ai 部署(它们有自己的编排机制)
  • 运行之间需要人工检查的实验

为什么需要此技能

根据 2026-04-16 的会话审计,多随机种子网格实验中占用大量实际运行时间的主要因素有:

  1. 过期的 screen 会话——Python 进程已结束,wandb 仍在上传,screen 会话却卡住,导致下一波受阻
  2. 共享 GPU 上发生 OOM——前一个任务的显存尚未释放
  3. 波次竞态——上一波尚未完全结束,新一波就已启动
  4. 缺少检查点——学生模型在教师模型保存之前就启动了
  5. 解析器重复编写——每批任务都要重新编写多随机种子分析 Python 脚本

这些都只是纯粹的工程摩擦,可以通过编排解决。

核心概念

环境约定:队列任务假设目标环境已根据 ../shared-references/compute-env-contract.md 完成构建和验证(包含规格哈希账本 + 内核见证)。一波任务在导入时全部退出,说明跳过了环境约定,而不是队列存在错误;重新加入队列前,请检查提供商的 .aris/compute/<provider>.md 账本。

任务清单

清单会列出任务及其明确状态:

project: my_grid_experiment
cwd: /home/user/your_project
conda: my_env
# Optional: override conda hook path if conda is not at a standard location.
# Can be a bare path (wrapped automatically) or a full `eval "$(... shell.bash hook)"` string.
# Falls back to auto-detect of ~/anaconda3, ~/miniconda3, /opt/anaconda3, etc.,
# or the ARIS_CONDA_HOOK environment variable.
# conda_hook: /custom/path/to/conda
ssh: gpu-server
default_cmd: >
  python run_distill.py --backbone softmax --lam 0.5
  --K 500 --L 96 --W 16 --n_steps 30000 --batch_size 128 --lr 1e-4

preconditions:
  - type: checkpoint_exists
    path: checkpoints/transformer/teacher_L96_K500_N{N}.pt

gpus: [0, 1, 2, 3, 4, 5, 6, 7]
max_parallel: 8
gpu_free_threshold_mib: 500  # optional, default 500; raise for shared servers, lower for tight packing
oom_retry:
  delay: 120
  max_attempts: 3

jobs:
  - id: s200_N64_n50K
    args: {seed: 200, n_hidden: 64, n_train_subset: 50000, subset_seed: 2024}
  - id: s200_N128_n50K
    args: {seed: 200, n_hidden: 128, n_train_subset: 50000, subset_seed: 2024}
  # ... 14 more
任务状态机
pending → running → completed
                 ↘ failed_oom → pending (after delay) [retry up to N]
                 ↘ failed_other → stuck (needs manual inspection)
stale screen (process gone, screen lingering) → failed_other → stuck

关于 stuck 的操作说明(由 Agent 处理,而非队列处理): 队列会以确定的方式将 failed_other 任务停放为 stuck——这是代码行为,且不会改变。将 stuck 批次交给人工处理之前,操作 Agent 应先检查:如果多个任务反复出现相同故障,只需干净利落地重新实现一次由 Agent 生成的包装脚本 / 尝试脚本——绝不要改动用户或项目源代码(例如你未编写的 run_*.py)、清单、队列状态、日志或结果(参见 shared-references/external-cadence.md § 让有问题的尝试重新开始,而不只是修补)。只有在约定或环境存疑时才交由人工处理,不要仅仅因为尝试代码有问题就交接。

波次编排

“波次”是指适配可用 GPU 的一批任务。只有满足以下条件,才会启动下一波:

  1. 当前波次的所有 Python 进程均已退出
  2. 当前波次的标签不再有过期的 screen 会话
  3. GPU 显存已降至阈值以下(≤500 MiB)
  4. 下一波任务的前置条件检查通过

工作流

步骤 1:解析清单 / 根据网格构建

输入可以是:

  • YAML 清单(明确列出任务,复杂情况下推荐使用)
  • 网格规格(参数值的笛卡尔积,例如,N=[64,128,256] × n=[50K,150K,500K,652K])
  • 自然语言描述(由 Claude 解析为清单)

一次性绑定运行标识符,确保之后的每个步骤(保存清单、scp、启动、监控、恢复)都使用相同路径。生成清单前,将这些值设为本地 shell 变量:

# REPLACE the placeholder path before running, or pre-export PROJECT_DIR:
PROJECT_DIR="${PROJECT_DIR:?set PROJECT_DIR to the local project root}"
RUN_TS=$(date -u +%Y%m%dT%H%M%SZ)             # one timestamp per run, reused everywhere
LOCAL_RUN_DIR="$PROJECT_DIR/experiment_queue/$RUN_TS"
mkdir -p "$LOCAL_RUN_DIR"

将构建好的清单保存到 $LOCAL_RUN_DIR/manifest.json,以便复现。

第 2 步:启动前检查
  • 检查 SSH 连接是否正常
  • 检查远程端是否存在 conda 环境
  • 检查远程端是否存在 cwd
  • 检查所有前置条件(检查点、输入文件)
  • 检查 GPU 是否可用(至少有 max_parallel 个空闲 GPU)

如果任何前置条件不满足,请告知用户哪些作业被阻塞以及原因。

第 3 步:启动调度器

调度器的规范实现位于 skills/experiment-queue/scripts/queue_manager.py(Phase 3.3 迁移,Arch C)。tools/experiment_queue/queue_manager.py 现在是一个 Python os.execv 转发脚本,保留用于兼容旧版解析器链。启动前需要完成以下三项准备工作。

3a. 确定本地辅助脚本目录。 两个辅助脚本(queue_manager.py、build_manifest.py)现在位于 ARIS 仓库的 skills/experiment-queue/scripts/ 下,并在 tools/experiment_queue/ 下提供转发脚本,以兼容旧版解析器层。使用以下混合路径链,确保该技能适用于任意项目布局:

# Layer 0: self-contained (CC 1.0+ exposes $CLAUDE_SKILL_DIR).
QUEUE_TOOLS=""
if [ -n "${CLAUDE_SKILL_DIR:-}" ] && [ -f "$CLAUDE_SKILL_DIR/scripts/queue_manager.py" ]; then
  QUEUE_TOOLS="$CLAUDE_SKILL_DIR/scripts"
fi
# Layers 1-4: legacy chain via tools/experiment_queue/ shims.
if [ -z "$QUEUE_TOOLS" ]; then
  cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" || exit 1
  if [ -z "${ARIS_REPO:-}" ] && [ -f .aris/installed-skills.txt ]; then
      ARIS_REPO=$(awk -F'\t' '$1=="repo_root"{print $2; exit}' .aris/installed-skills.txt 2>/dev/null) || true
  fi
  if [ -z "${ARIS_REPO:-}" ] && [ -f "$HOME/.aris/repo" ]; then
      ARIS_REPO=$(cat "$HOME/.aris/repo" 2>/dev/null) || true
  fi
  QUEUE_TOOLS=".aris/tools/experiment_queue"
  [ -f "$QUEUE_TOOLS/queue_manager.py" ] || QUEUE_TOOLS="tools/experiment_queue"
  [ -f "$QUEUE_TOOLS/queue_manager.py" ] || { [ -n "${ARIS_REPO:-}" ] && QUEUE_TOOLS="$ARIS_REPO/tools/experiment_queue"; }
  [ -f "$QUEUE_TOOLS/queue_manager.py" ] || QUEUE_TOOLS=""
fi
[ -z "$QUEUE_TOOLS" ] && { echo "ERROR: experiment_queue helpers not found (layer 0: \$CLAUDE_SKILL_DIR/scripts/; layers 1-4: .aris/tools/, tools/, \$ARIS_REPO/tools/, \$ARIS_REPO/tools/ via ~/.aris/repo). Rerun install_aris.sh or smart_update.sh (refreshes ~/.aris/repo), set ARIS_REPO, or copy the canonical scripts from \$ARIS_REPO/skills/experiment-queue/scripts/." >&2; exit 1; }

.aris/tools 符号链接由 install_aris.sh(#174)创建。未配置该符号链接的旧版安装会依次回退到 tools/experiment_queue(从 ARIS 仓库内部调用时可用)、$ARIS_REPO/tools/experiment_queue,或通过全局指针文件 ~/.aris/repo(#366)解析出的相同路径(适用于没有项目本地清单的安装)。Phase 3.3 之后,这些旧版路径中的每一个都包含一个 Python os.execv 转发脚本,会将调用转发到规范位置 skills/experiment-queue/scripts/,因此现有用户无需重新运行任何内容。

3b. 计算远程路径。 同时使用远程相对路径(用于 scp 目标路径——现代 scp 以 SFTP 模式运行,不会可靠地在目标路径中展开 $HOME)和添加 $HOME 前缀的路径(用于 ssh ... command 字符串,其中远程 bash 会展开 $HOME):

REMOTE_RUN_REL=".aris_queue/runs/$RUN_TS"          # for scp destinations (relative to remote home)
REMOTE_RUN_DIR="\$HOME/$REMOTE_RUN_REL"            # for ssh command strings (literal $HOME, expanded on remote)

3c. 初始化远程运行目录并复制辅助脚本和清单。 每次调用都执行,且可重复执行。请使用唯一的运行目录,而非 /tmp,以避免并发队列冲突,并确保崩溃后可复现地恢复运行。

ssh <server> "mkdir -p \"$REMOTE_RUN_DIR/logs\" \"\$HOME/.aris_queue\""
scp "$QUEUE_TOOLS/queue_manager.py" "$QUEUE_TOOLS/build_manifest.py" <server>:.aris_queue/
scp "$LOCAL_RUN_DIR/manifest.json" <server>:"$REMOTE_RUN_REL/manifest.json"

3d. 在 SSH 主机上以分离的 nohup 进程启动调度器:

ssh <server> "nohup python3 \"\$HOME/.aris_queue/queue_manager.py\" \\
  --manifest \"$REMOTE_RUN_DIR/manifest.json\" \\
  --state    \"$REMOTE_RUN_DIR/queue_state.json\" \\
  --log-dir  \"$REMOTE_RUN_DIR/logs\" \\
  > \"$REMOTE_RUN_DIR/queue_mgr.log\" 2>&1 &"

调用方注意事项:

  • queue_manager.py 实际使用的是 --log-dir(用于每个作业的日志文件,以检测 OOM)。不要传入 --log <path>——该标志虽有声明但未使用,而且将所有日志合并为一个文件会破坏按作业进行的陈旧 screen / OOM 启发式检测。

  • 将 RUN_TS / REMOTE_RUN_REL / REMOTE_RUN_DIR 持久化到磁盘,以便监控和恢复时无需重新生成即可重新加载:

    {
      printf 'PROJECT_DIR=%q\n'    "$PROJECT_DIR"
      printf 'RUN_TS=%q\n'         "$RUN_TS"
      printf 'LOCAL_RUN_DIR=%q\n'  "$LOCAL_RUN_DIR"
      printf 'REMOTE_RUN_REL=%q\n' "$REMOTE_RUN_REL"
      printf 'REMOTE_RUN_DIR=%q\n' "$REMOTE_RUN_DIR"
    } > "$LOCAL_RUN_DIR/run_meta.txt"
    

    %q 会对值进行 shell 转义,确保文件之后可以安全地作为脚本加载。注意,REMOTE_RUN_DIR 保留了字面值 $HOME(不要在本地展开),这是之后在 ssh "..." 字符串中重复使用的正确格式。

3e. 恢复现有队列(仅在用户提出要求时)。 每次调用都生成新的 RUN_TS,适用于新队列。要恢复崩溃的队列,请勿重新生成 RUN_TS——重新加载记录的值,只重新运行启动命令(第 3d 步),不要重新执行初始化(第 3c 步):

LOCAL_RUN_DIR="/abs/path/to/project/experiment_queue/<existing-run-ts>"   # the run dir to resume
. "$LOCAL_RUN_DIR/run_meta.txt"                                            # reloads PROJECT_DIR / RUN_TS / REMOTE_RUN_REL / REMOTE_RUN_DIR
# Then re-run Step 3d verbatim. Do NOT re-run Step 3c (would overwrite manifest.json + state.json).

在 2026-08 调度器修复之前写入的 queue_state.json 会记录旧代码误判为失败的作业。恢复时,这些作业会被标记为 stuck——不会重试,因此如果状态文件中的作业全都为 failed_other,最终将不会启动任何作业。请删除该状态文件,并从第 3c 步重新运行清单。

调度器会:

  • 读取清单
  • 循环处理:为每个待处理作业分配空闲 GPU,并通过 screen 启动
  • 轮询作业状态(每 60 秒)
  • 检测陈旧 screen(Python 进程已退出但 screen 仍处于分离状态 → 终止)
  • 检测 OOM(日志中出现 CUDA OOM → 标记为 failed_oom → 延迟后重试)
  • 检测完成情况(预期输出 JSON/文件存在)→ 标记为已完成
  • 当前批次处理完毕后启动下一批次
  • 持续将状态写入 queue_state.json
步骤 4:监控

用户可以随时使用步骤 3b 中的 $REMOTE_RUN_DIR 检查状态(对于较早的运行,也可以从 $LOCAL_RUN_DIR/run_meta.txt 重新加载):

ssh <server> "cat \"$REMOTE_RUN_DIR/queue_state.json\"" \
  | jq '.jobs | group_by(.status) | map({(.[0].status): length}) | add'

注意:/monitor-experiment 目前侧重于 screen 会话、结果 JSON 和 W&B;尚不会直接读取 queue_state.json。如需监控队列状态,请使用上面的原样命令,并指定记录的 REMOTE_RUN_DIR。(后续将跟进 /monitor-experiment 的队列状态集成。)

步骤 5:完成后

当 manifest.json 中的所有作业都处于 completed 或 stuck 状态时:

  • 远程调度器(queue_manager.py)会正常退出,并向其自身的标准输出打印 All jobs done(内容会捕获到 $REMOTE_RUN_DIR/queue_mgr.log 中)。它不会写入本地摘要。
  • 本地技能 Agent 随后会将状态汇总到 $LOCAL_RUN_DIR/summary.md(读取 $REMOTE_RUN_DIR/queue_state.json,按状态分组,并可选择拉取每个作业的日志)。
  • 如果 analyze_on_complete: true,本地技能 Agent 会调用 /analyze-results。

网格规格语法

无需手动编写 24 个作业条目:

grid:
  N: [64, 128, 256]
  n: [50000, 150000, 500000, 652000]
  seed: [42, 200, 201]
template:
  id: "s${seed}_N${N}_n${n}"
  args: {seed: ${seed}, n_hidden: ${N}, n_train_subset: ${n}}

会自动扩展为 36 个作业。

波次串联

对于顺序执行的阶段(教师 → 学生):

phases:
  - name: train_teachers
    grid:
      N: [384, 512]
    template:
      cmd: python run_train.py --direction c --backbone softmax --n_hidden ${N} ...
      expected_output: checkpoints/transformer/teacher_L96_K500_N${N}.pt
  
  - name: distill_students
    depends_on: [train_teachers]        # must be a LIST, even for a single dependency
    grid:
      N: [384, 512]
      seed: [42, 200, 201]
    template:
      cmd: python run_distill.py --n_hidden ${N} --seed ${seed} ...
      expected_output: figures/distill_sw_N${N}_*_seed${seed}.json

调度器会强制执行 depends_on:distill_students 作业会保持 pending 状态,直到每个 train_teachers 作业都进入终态——completed 或 stuck。教师作业失败不会阻止学生作业,因此在信任依赖波次之前,请检查 queue_state.json 中是否有 stuck 作业。

OOM 处理

从标准输出检测 OOM:

torch\.OutOfMemoryError: CUDA out of memory

检测到后:

  1. 将作业标记为 failed_oom
  2. 终止 screen
  3. 等待 oom_retry.delay 秒
  4. 检查当前 GPU 是否空闲;如果不空闲,则尝试另一个空闲 GPU
  5. 重新加入队列,状态设为 pending
  6. 达到 oom_retry.max_attempts 次后,将作业标记为 stuck

陈旧 Screen 检测

每 60 秒检查每个运行中的 screen:

  1. 检查 screen 是否存在(screen -ls)
  2. 检查 Python PID 是否仍在运行(ps -p)
  3. 如果 screen 存在,但 Python 已退出:
    • 如果预期输出文件存在 → 标记为 completed,终止陈旧的 screen
    • 如果输出文件不存在 → 标记为 failed_other,终止 screen

重启后恢复

如果调度器崩溃或被终止:

  1. 读取 queue_state.json
  2. 对每个状态为 running 的作业:检查 screen;如果仍在运行,则保留;否则重新评估状态
  3. 对每个状态为 pending 的作业:正常继续
  4. 幂等:安全地重启调度器,不会丢失状态

输出:摘要报告

# Experiment Queue Summary

**Project**: my_grid_experiment
**Started**: 2026-04-16 11:36:29
**Completed**: 2026-04-16 18:02:14
**Total wall-clock**: 6h 25m
**Jobs**: 40 completed, 2 OOM-retried then completed, 0 stuck

## Phases
| Phase | Jobs | Success | OOM retries | Duration |
| --- | --- | --- | --- | --- |
| train_teachers | 2 | 2 | 0 | 58m |
| distill_students | 24 | 24 | 2 | 4h 02m |
| multi_seed_validation | 16 | 16 | 0 | 1h 25m |

## Results Files
- 42 JSON files in `figures/distill_sw_*.json`

## Next Steps
- Run `/analyze-results` on output JSONs
- Figures auto-regen via `artifact-sync` (if configured)

与 /run-experiment 的比较

功能/run-experimentexperiment-queue
单次实验✅✅(大材小用)
多 GPU 并行基础完备的调度
波次切换手动自动
OOM 重试手动自动
清理陈旧 Screen手动自动
教师→学生串联手动内置
状态持久化否是(JSON)
崩溃后恢复否是
网格展开手动声明式

规则:作业数 ≤5 时使用 /run-experiment。作业数 ≥10 或包含任何阶段时使用 experiment-queue。

关键规则

  • 切勿在同一 GPU 上重叠运行多个 Screen——启动新作业前,务必等待 memory.used < 500 MiB
  • 始终将状态写入磁盘——每次状态变更都刷新到 queue_state.json
  • 调度器必须幂等——可安全重启;会从状态文件恢复
  • 根据预期输出判定完成——不要只相信 Screen 状态;应确认输出文件存在
  • 限制重试次数——最多重试 OOM N 次,之后将作业标记为 stuck 并发出提醒
  • 启动时强制执行依赖关系——只有当其依赖阶段中的每个作业都达到终态后,波次才会启动。请注意,终态包括 stuck:如果教师作业失败,该阶段仍会完成,其学生作业会在缺少检查点的情况下启动。在信任依赖波次的结果之前,请检查 queue_state.json 中是否有 stuck 作业。

已知故障模式

  • 调度期间 SSH 连接断开:调度器会在远程端继续运行(nohup),重新连接并检查即可
  • GPU 被其他用户占用:调度器会等待,不会抢占
  • 远程端磁盘空间已满:调度器会检测到写入失败,将所有待处理任务标记为 stuck,并发出警报

会话示例

用户:“运行 T5+T6 的全部实验:T5 = N∈{80,192} × n 取 4 个值 × seed {200,201},T6 = N∈{384,512} × n 取 4 个值 × seed {42,200,201};T6 需要先训练 teacher”

Claude 调用 /experiment-queue:

  1. 将描述解析为 2 阶段清单
  2. 阶段 1:T5(16 个任务,无 teacher 依赖)+ T6 teacher 训练(2 个任务)
  3. 阶段 2:T6 蒸馏(24 个任务,依赖 teacher)
  4. 通过 nohup 部署调度器
  5. 报告:“调度器 PID 93534,共 42 个任务,预计耗时 6-7h”

之后,用户可以随时查看状态,或等待汇总报告。

另请参阅

  • /run-experiment — 部署单个实验
  • /monitor-experiment — 检查进度(现在从 queue_state.json 读取)
  • /analyze-results — 事后分析
  • skills/experiment-queue/scripts/queue_manager.py(规范版本,Phase 3.3 迁移)— 调度器实现;在运行时通过 Step 3a 中的回退链解析。旧入口 tools/experiment_queue/queue_manager.py 是一个 os.execv 转发脚本。
  • skills/experiment-queue/scripts/build_manifest.py(规范版本,Phase 3.3 迁移)— 根据网格规范构建清单;使用相同的解析链。旧入口 tools/experiment_queue/build_manifest.py 是一个 os.execv 转发脚本。

原因 / 来源

根据 2026-04-16 的事后分析(Codex GPT-5.5 xhigh),从一次持续 1.5 天的多 seed 论文实验会话中识别出:

  • 耗时瓶颈:过时的 screen 会话、OOM、批次切换、手动解析器
  • Token 消耗:每次会话都要重写编排代码
  • 认知负担:追踪哪些单元格成功、哪些失败,以及哪些需要重试

此技能专门针对耗时瓶颈;其他两项请参阅 artifact-sync 和 paper-fix-auto-apply。

评论

支持 Markdown