关于 experiment-queue
experiment-queue 是用于通过 SSH 在远程 GPU 服务器上编排批量机器学习实验的技能,面向多任务网格、种子扫描和分阶段运行。它以清单和状态机管理任务,并自动处理 OOM 重试、过期会话清理及波次切换。
主要功能
- 01从清单、参数网格或自然语言构建任务
- 02通过 SSH 在远程 GPU 服务器运行任务
- 03跟踪任务状态并持续保存队列状态
- 04检测 OOM 并延迟重试
- 05按条件清理会话并切换运行波次
- 06支持教师与学生阶段依赖
适用场景
- 01并行运行多种子实验网格
- 02按波次执行分阶段实验
- 03训练教师模型后自动启动学生任务
- 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 的会话审计,多随机种子网格实验中占用大量实际运行时间的主要因素有:
- 过期的 screen 会话——Python 进程已结束,wandb 仍在上传,screen 会话却卡住,导致下一波受阻
- 共享 GPU 上发生 OOM——前一个任务的显存尚未释放
- 波次竞态——上一波尚未完全结束,新一波就已启动
- 缺少检查点——学生模型在教师模型保存之前就启动了
- 解析器重复编写——每批任务都要重新编写多随机种子分析 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 的一批任务。只有满足以下条件,才会启动下一波:
- 当前波次的所有 Python 进程均已退出
- 当前波次的标签不再有过期的 screen 会话
- GPU 显存已降至阈值以下(≤500 MiB)
- 下一波任务的前置条件检查通过
工作流
步骤 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
检测到后:
- 将作业标记为
failed_oom - 终止 screen
- 等待
oom_retry.delay秒 - 检查当前 GPU 是否空闲;如果不空闲,则尝试另一个空闲 GPU
- 重新加入队列,状态设为
pending - 达到
oom_retry.max_attempts次后,将作业标记为stuck
陈旧 Screen 检测
每 60 秒检查每个运行中的 screen:
- 检查 screen 是否存在(
screen -ls) - 检查 Python PID 是否仍在运行(
ps -p) - 如果 screen 存在,但 Python 已退出:
- 如果预期输出文件存在 → 标记为
completed,终止陈旧的 screen - 如果输出文件不存在 → 标记为
failed_other,终止 screen
- 如果预期输出文件存在 → 标记为
重启后恢复
如果调度器崩溃或被终止:
- 读取
queue_state.json - 对每个状态为
running的作业:检查 screen;如果仍在运行,则保留;否则重新评估状态 - 对每个状态为
pending的作业:正常继续 - 幂等:安全地重启调度器,不会丢失状态
输出:摘要报告
# 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-experiment | experiment-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:
- 将描述解析为 2 阶段清单
- 阶段 1:T5(16 个任务,无 teacher 依赖)+ T6 teacher 训练(2 个任务)
- 阶段 2:T6 蒸馏(24 个任务,依赖 teacher)
- 通过 nohup 部署调度器
- 报告:“调度器 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。