English | 简体中文
一个面向单台 Linux 机器的用户态批处理调度器。把一堆实验交给它,它会在不让机器陷入 swap、不被 OOM 拖垮的前提下尽可能多地并行运行,并且记得哪些已经跑完。
- 内存感知:学习每类命令的峰值内存,监视 MemAvailable 与 PSI,以"慢启动"窗口逐步加大并发,只有在压力持续存在时才抢占。
- 为实验而设计:TOML 任务文件,支持参数扫描、依赖、重试(包括 OOM 后重试)、优先级,以及可配置的停止信号(便于任务保存 checkpoint)。
- 可靠持久:所有任务、尝试和事件都记录在 SQLite 中;
--resume跳过已成功的任务,并能重新接管调度器崩溃后仍在运行的任务。 - 可编程:Unix socket 上的 HTTP API(可直接
curl,提供 OpenAPI、SSE 事件流和 Prometheus 指标),以及命令行客户端、TUI 和 Python SDK。
构建:cargo build --release,二进制位于 target/release/cirno。
# sweep.toml
[defaults]
resources = { mem = "8G" }
timeout = "6h"
retry = { max_attempts = 3, on = ["oom"] }
[[job]]
name = "prepare"
command = "python prep.py --out data/"
[[sweep]]
name = "resnet-lr{lr}-s{seed}"
command = "python train.py --lr {lr} --seed {seed} --out runs/{name}"
depends_on = ["prepare"]
[sweep.matrix]
lr = [0.1, 0.01]
seed = [0, 1, 2]cirno run sweep.toml --dry-run # 查看展开后的 7 个任务
cirno run sweep.toml # 运行;按一次 Ctrl-C 排空,按两次停止
cirno run sweep.toml --resume # 只重跑没有成功的任务可以直接运行的示例见 examples/sweep.toml。
完整参考见 docs/job-file.md,JSON Schema 见
schema/job-file.schema.json(在文件首行写
#:schema <路径> 即可在编辑器中获得补全和校验)。
任务文件使用 TOML 格式:[defaults] 作用于所有任务;[[job]] 定义单个任务;[[sweep]]
把 matrix 展开成多个任务(默认 mode = "product" 做笛卡尔积,"zip" 则按位置一一配对;
exclude = [{ lr = 0.1, seed = 1 }] 可以排除某些组合)。{key} 占位符可用于 name、
command、cwd 和 env,参数值会自动成为任务的标签(tags);原样的大括号写成 {{、}}。任务字段:
| 字段 | 含义 |
|---|---|
command |
argv 数组;或字符串,按 POSIX shell 规则切分参数(不经过 shell) |
shell = true |
用 /bin/sh -c 执行字符串命令(可以使用管道、重定向) |
cwd、env、tags |
工作目录(相对于任务文件所在目录)、环境变量、元数据 |
resources |
mem、cpus,以及计数型的 custom 资源(容量用 --capacity name=N 设置) |
priority |
数值越大越先启动;排队越久,优先级会逐渐提升 |
timeout |
例如 "30s"、"2h30m" |
depends_on |
同一文件中其他任务的名字;依赖失败时,依赖它的任务会被跳过 |
retry |
max_attempts、on = ["exit_code", "signal", "oom", "spawn_error"]、backoff(例如 "10s",每次重试翻倍)、max_preemptions |
stop |
{ signals = [["USR1", "60s"], ["TERM", "10s"]], timeout_signal = "ALRM" };默认先发 TERM,等待 --kill-grace 后发 KILL |
preemptible、profile |
设为不可抢占;指定内存学习的分组名 |
未知的键会直接报错,拼写错误不会被悄悄忽略。文件中还可以写一个 [scheduler] 表,
为这次 cirno run 设置下文"配置"一节中的任意选项。
任务进程可以读取这些环境变量:CIRNO_JOB_ID、CIRNO_JOB_NAME、CIRNO_BATCH_ID、
CIRNO_ATTEMPT、CIRNO_PREEMPTED、CIRNO_OUTPUT_DIR、CIRNO_SOCKET。
每个任务的内存需求取"声明值"与"学习值"中的较大者。学习值来自同一命令模板历史运行的
峰值内存(取 P90,发生 OOM 后会上调);既没有声明也没有历史的任务使用 --default-mem。
- 何时启动:只有当机器处于 Green 状态(内存使用低于
--mem-low,PSI 较低)并且任务能放得下时, 才会启动新任务;同时,还处在内存爬升阶段的任务最多 W 个——系统平稳时 W 翻倍,出现压力时减半。 - 大任务预留:大任务放不下时会获得资源预留,小任务只有在不推迟它的前提下才能插队(回填)。
- 何时抢占:内存使用持续高于
--mem-high(或 PSI 过高)达到--red-hold时,会选择代价最小的 可抢占任务,按它的停止策略停下并重新排队。CPU 压力只会暂停启动新任务,不会触发抢占。 - 保底:
--min-workers个任务无论压力如何都会运行。
任务进程的隔离方式(--isolation auto|cgroup|pgid):可用时,每次运行放在独立的 systemd 用户
scope 中(精确的内存统计、OOM 检测、cgroup.kill 一次清理);否则放在独立会话中,由 cirno
作为 child subreaper 回收孤儿进程。
cirno daemon --name lab & # 常驻实例
cirno submit sweep.toml # 或:cirno submit --mem 4G -- python x.py
cirno ls --state failed # 另有:status、show <job>、report
cirno logs resnet-lr0.1-s0 -f --err
cirno retry --batch 1 --state failed # 另有:cancel、hold、release、signal
cirno set max-workers=8 # 运行时修改配置
cirno wait --batch 1 && echo done
cirno top # 交互式界面停止与重启 daemon 不会丢任务:排队中的任务总是保留,下次启动时继续调度;运行中的任务按
--on-signal 处理——detach(cgroup 隔离时的默认值)让它们继续运行,重启后自动接管;drain
(其他情况的默认值)等它们跑完再退出;stop 停止它们并在重启后重跑。
作为常驻服务:仓库提供了 systemd 用户服务 contrib/systemd/cirno.service:
cp contrib/systemd/cirno.service ~/.config/systemd/user/ # 按需修改 ExecStart 中的路径和参数
systemctl --user daemon-reload
systemctl --user enable --now cirno
loginctl enable-linger "$USER" # 注销后继续运行,开机自动启动cirno run 运行期间同样开放 API,并在 <run-dir>/cirno.sock 创建指向 API socket 的符号链接。
有多个实例时,用 --instance NAME 或 --socket PATH 指定(例如
cirno --socket run/cirno.sock set max-workers=8);在任务内部执行 cirno 时,会自动连接
启动该任务的实例。所有命令都支持 --json 输出。
HTTP API:socket 位于 $XDG_RUNTIME_DIR/cirno/<instance>.sock,接口说明见
sdk/openapi.json 或 GET /v1/openapi.json。
curl --unix-socket $XDG_RUNTIME_DIR/cirno/lab.sock http://cirno/v1/jobs?state=running
curl -N --unix-socket $XDG_RUNTIME_DIR/cirno/lab.sock http://cirno/v1/events通过网络访问:默认只监听 Unix socket。用 --listen ADDR:PORT(可重复)额外监听 TCP:
- 回环地址(
127.0.0.1、[::1])不需要鉴权; - 其他地址(包括
0.0.0.0)必须带Authorization: Bearer <token>,并且必须启用 TLS (--tls-cert/--tls-key;确实需要明文时要显式加--allow-plaintext)。token 来自--token-file,未指定时自动生成在<run-dir>/api-token(权限 0600)。同一 IP 一分钟内失败 20 次会被暂时拒绝。
cirno daemon --listen 0.0.0.0:7878 --tls-cert cert.pem --tls-key key.pem
# 另一台机器上:
CIRNO_TOKEN=$(cat api-token) cirno --url https://server:7878 --ca-cert cert.pem lsPython(pip install sdk/python,无第三方依赖):
import cirno
c = cirno.Client()
batch = c.submit_sweep(name="t{lr}", command="python train.py --lr {lr}",
matrix={"lr": [0.1, 0.01]}, resources={"mem": "8G"})
batch.wait()
for r in batch.to_records():
print(r["name"], r["state"], r["peak_mem"])在任务内部调用 cirno.report(progress=0.4, loss=1.23),进度和指标会显示在 cirno top 与
API 中(不在 cirno 下运行时为空操作)。
钩子:在任务文件中写 [hooks],或使用 --hook EVENT=COMMAND。事件包括
on_job_failed、on_job_succeeded、on_batch_finished、on_preempted。命令通过
CIRNO_* 环境变量获得上下文,并从 stdin 读到事件 JSON;写成 { webhook = "http://..." }
则会把同样的 JSON POST 到该地址。
同一个选项可以出现在多个地方,后者覆盖前者:内置默认值 < ~/.config/cirno/config.toml
(遵循 $XDG_CONFIG_HOME)< 任务文件的 [scheduler] 表 < 环境变量 CIRNO_<KEY> < 命令行参数。
配置文件和 cirno set 中的键名就是命令行长参数名的 snake_case 形式,例如:
# ~/.config/cirno/config.toml
max_workers = 8
default_mem = "6G"
mem_low = 0.6
kill_grace = "30s"对应的环境变量为 CIRNO_MAX_WORKERS、CIRNO_DEFAULT_MEM 等。内存可以写成 4G、512MiB
或字节数;时长可以写成 500ms、2h30m 或秒数。cirno config show [任务文件] 会列出最终
生效的每个值及其来源。
run/ # --run-dir(守护进程默认为 $XDG_STATE_HOME/cirno/<name>)
cirno.db # SQLite:批次、任务、尝试、事件、内存学习数据
cirno.sock # 运行期间指向 API socket 的符号链接
api-token # 非回环 TCP 监听自动生成的 token(0600)
batches/<id>/report.{csv,json}
batches/<id>/<job>/attempt-<n>.{out,err}
结束时会打印汇总,列出失败的任务、它们的 stderr 路径,以及重跑命令。
退出码:
| 退出码 | 含义 |
|---|---|
0 |
全部成功 |
1 |
有任务未成功 |
2 |
输入有误、run 目录正被另一个 cirno 使用,或者 run 目录来自不兼容的 cirno 版本 |
130 |
被中断 |
按一次 Ctrl-C 停止启动新任务,两次停止正在运行的任务,三次立即杀掉它们;尚未启动的任务
保持排队状态,可用 --resume 继续。