Skip to content
YurziPublic

About

A "smartest" task scheduler

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Cirno

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 ls

Python(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 继续。

更多

About

A "smartest" task scheduler

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages