内容
<div align="center">
# jobd
[](https://github.com/musharna/jobd/actions/workflows/ci.yml)
[](https://pypi.org/project/jobd/)

[](https://glama.ai/mcp/servers/musharna/jobd)
[](https://github.com/musharna/jobd/blob/main/LICENSE)
**一个可自行托管、支持GPU的作业调度器,适用于您自己的机器 — 具有原生的MCP代理集成。**
> 类似 [task-spooler](https://manpages.ubuntu.com/manpages/noble/man1/tsp.1.html),但可跨多台机器使用 — 且支持VRAM感知。
</div>
<!-- mcp-name: io.github.musharna/jobd -->
<p align="center">
<img src="https://raw.githubusercontent.com/musharna/jobd/main/docs/assets/demo.svg" alt="jobd in action: submit a GPU job, watch it route to a worker with free VRAM and stream back, then inspect the full lifecycle" width="100%">
</p>
您拥有几台配备GPU的机器 — 工作站、服务器,也许还有笔记本电脑 — 通过[Tailscale](https://tailscale.com/)或局域网连接在一起。您想从任何地方启动训练任务、数据管道和长时间批处理作业,让它们在具有空闲VRAM的机器上运行,在会话之间存活下来,并在有更重要的任务出现时被干净地抢占。您没有云、Kubernetes集群或Slurm安装,也不想要这些。
jobd就是那个缺失的部分:一个小型调度器,它将几台个人机器变成一个单一的队列 — 而LLM代理可以直接驱动它。
```bash
# 从您尾网上的任何机器:
job submit --project myproj --gpu --vram-required 16 --wait -- python train.py
# → 路由到具有≥16 GB空闲VRAM的worker,流式传输回您的终端
```
## 为什么存在
大多数调度器都假设数据中心。轻量级调度器(bare `nohup`, tmux会话,ssh-and-pray脚本)无法提供任何功能:没有队列,没有VRAM感知路由,没有抢占,没有记录运行了什么。jobd填补了“ssh登录并运行”和“建立Slurm”之间的空白:
- **VRAM拟合路由。** 调度器根据每个作业的实时工作节点容量(空闲VRAM / RAM / CPU,功能标签,架构/操作系统)进行匹配,并调度到实际适合的worker — 而不是您猜测哪台机器是空闲的。
- **抢占 + 检查点。** 优先级更高的作业可以抢占正在运行的作业:工作节点发送`SIGTERM`,工作负载获得宽限期以检查点,然后`SIGKILL`。被抢占的作业达到终端`preempted`状态,并带有持久检查点以便从中恢复 — 它不会被静默地重新运行。(参见 [docs/preemption.md](https://github.com/musharna/jobd/blob/main/docs/preemption.md).)
- **会话存活。** 提交,关闭笔记本电脑,隔天再查看。作业存在于调度器中,而不是您的shell中。
- **原生代理。** 配备有原生的[MCP](https://modelcontextprotocol.io/)服务器,以便LLM代理(Claude Code等)可以直接提交、监控和照顾作业 — 大多数调度器都是事后才添加的这个功能,如果有的话。
- **属于您。** 一个调度器进程您自己运行在一个您拥有的机器上。没有账号,没有出口,没有按GPU小时计费。默认绑定到尾网。
## 为什么不直接使用……?
| 工具 | 它为您提供什么 | 为什么选择jobd |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **`nohup` / `tmux` / ssh-and-pray** | 在一台机器上运行命令 | 没有队列,没有VRAM感知路由,没有抢占,没有记录运行了什么 |
| **[task-spooler](https://manpages.ubuntu.com/manpages/noble/man1/tsp.1.html)** | 一个真正的作业队列 — 在单台机器上 | jobd在**所有**您的机器上排队并根据实时VRAM/CPU匹配 |
| **Slurm** | 数据中心级调度 | 对于2-3台个人机器来说太重了;jobd是一个进程 + 一个主机轮询器 |
| **SkyPilot / Modal / dstack** | 在云上(SkyPilot还支持本地,通过SSH)提供和运行 | jobd针对您**已有**的硬件,没有云/K8s假设,占用更小 |
| **Ray** | 分布式计算框架 | jobd是一个**作业队列**,不是编程模型 — 提交任何命令,无需代码更改,内置GPU拟合路由 |
与task-spooler(单节点)和本地SkyPilot(更重,云状)最接近。 jobd的定位是2-3 GPU家庭实验室:多机器VRAM拟合路由 + 抢占/检查点 + 原生代理接口,无需建立任何东西。
## 架构
<p align="center">
<img src="https://raw.githubusercontent.com/musharna/jobd/main/docs/assets/architecture.svg" alt="Architecture: job CLI, jobd-mcp MCP tools, and HTTP/SSE clients talk to the jobd broker (FastAPI — queue, matcher, priorities, SQLite), which dispatches via poll to workers A (24 GB GPU), B (8 GB GPU), and C (CPU-only)" width="560">
</p>
<details>
<summary>图表源代码(mermaid)</summary>
```mermaid
flowchart TD
CLI["job CLI"]:::client --> B
MCP["jobd-mcp<br/>MCP tools"]:::client --> B
API["HTTP · SSE"]:::client --> B
B["<b>jobd broker</b> — FastAPI<br/>queue · matcher · priorities · SQLite"]:::broker
B <-->|poll · dispatch| WA["worker A<br/>24 GB GPU"]:::worker
B <-->|poll · dispatch| WB["worker B<br/>8 GB GPU"]:::worker
B <-->|poll · dispatch| WC["worker C<br/>CPU-only"]:::worker
classDef client fill:#1f2937,stroke:#4b5563,color:#e5e7eb;
classDef broker fill:#0e7490,stroke:#155e75,color:#ecfeff;
classDef worker fill:#14532d,stroke:#166534,color:#dcfce7;
```
</details>
工作节点**轮询**调度器(拉模型 — 没有到工作节点的入站连接);调度器根据每个作业的实时容量进行匹配,并通过轮询将其发送回工作节点。一个调度器进程,一个主机轮询器。
- **调度器** — 一个FastAPI + SQLite服务。持有队列,运行匹配器,解析每个项目的优先级和默认值,暴露一个小型HTTP API和SSE流。单一真实来源。
- **工作节点** — 轻量级轮询代理,每个主机一个。每个代理通过心跳广告宣传实时容量,声明它可以运行的作业,执行它们(`shell=False`,没有shell注入表面),流式传输日志,并响应抢占信号。
- **客户端** — `job` CLI,`jobd-mcp` MCP服务器,或任何支持HTTP API的客户端。
## 安装
```bash
pip install jobd # 调度器 + CLI
pip install "jobd[mcp]" # 添加MCP服务器
pip install "jobd[worker]" # 添加工作节点守护进程(jobd-worker)
```
需要Python ≥ 3.11。所有功能都包含在一个`jobd`包中:调度器(`jobd`),CLI(`job`),MCP服务器(`jobd-mcp`),和工作节点(`jobd-worker`)。工作节点的运行时依赖(httpx,psutil,pyyaml,nvidia-ml-py)位于 `[worker]`扩展中,因为它们仅在实际运行作业的机器上需要。`scripts/install-worker.sh`使用自己的venv和生成的配置文件在`~/jobd-worker`下设置工作节点。
## 快速开始(单主机)
```bash
# 1. 启动调度器(默认绑定到127.0.0.1:8765)
JOBD_ALLOW_NO_AUTH=1 jobd # 在仅限回环的调度器上可以不进行身份验证
# 2. 在另一个shell中,安装 + 启动指向它的工作节点
pip install "jobd[worker]"
JOBD_URL=http://127.0.0.1:8765 JOBD_WORKER_HOST=local jobd-worker
# 3. 提交一个作业并等待它完成
job submit --project demo --wait -- echo hello
job list
job logs <id>
```
有关真正的多主机部署(Docker调度器 + systemd工作节点,Tailscale绑定,共享身份验证令牌),请参见 **[docs/security.md](https://github.com/musharna/jobd/blob/main/docs/security.md)** 和 `docker-compose.yml` 和 `scripts/`中的模板(代理编排,`install-worker.sh`,`job-worker.service`)。第2天的操作(健康状况,排空工作节点,升级,令牌轮换,备份)位于 **[docs/runbook.md](https://github.com/musharna/jobd/blob/main/docs/runbook.md)**。
## 支持的平台
每个地方的Python 3.11+。
| 组件 | Linux | macOS | Windows |
| -------------------------------------- | ------- | ----------- | -------------------- |
| **调度器** (`jobd`) | ✅ | ✅ | ✅ (推荐WSL) |
| **CLI** (`job`) / **MCP** (`jobd-mcp`) | ✅ | ✅ | ✅ |
| **工作节点** (`jobd-worker`) | ✅ 完整 | ⚠️ 降级 | ⚠️ 降级 |
**工作节点**在Linux上运行最佳,带有systemd用户实例:内存上限,进程回收和抢占使用`systemd-run --user`作用域和cgroup。在非systemd主机上,工作节点仍然执行作业,但静默放弃这些保证 — 适用于单个受信任的盒子,不适用于硬资源隔离。GPU功能需要NVIDIA + `nvidia-ml-py`。调度器,CLI和MCP服务器是纯Python且可移植的。
## CLI
```
job submit -p PROJ [--gpu] [--vram-required N] [--needs TAG]... [--count N | --sweep K=v1,v2]... [--wait] -- CMD...
job list [--state STATE] [--project P] [--array A<id>] # 队列 + 最近的作业
job status ID | A<id> [--watch] # 一个作业,或一个数组的聚合
job logs ID [-n BYTES] # 捕获输出的尾部
job wait ID # 阻塞直到终端
job cancel ID / job preempt ID # 停止作业
job workers # 机队快照 + 健康状况
job projects list | set NAME PRI | nudge NAME DELTA
job audit [--project P] [--since 24h] # 事件历史记录
```
`job submit --explain` 干运行解析(优先级,配置文件,项目默认值,主机固定)并在不排队任何内容的情况下打印有效配置。
### 作业数组
使用 `--count N` 提交N个作业来自一个模板。每个成员都是一个正常的作业 — 它路由,运行,抢占,并独立检查点 — `{i}`在命令中替换为成员的0基索引:
```bash
job submit -p train --count 8 -- python train.py --fold {i}
# → 已提交数组A42:8个作业(id 42..49)
job list --array A42 # 成员及其索引注解
job status A42 # 聚合:状态计数 + 成员汇总
```
数组标识为`A<id>`(第一个成员的作业id)。`job status A42` 如果任何成员以非完成终端状态结束,则退出非零,因此它与shell `&&` 组合使用。
对于网格搜索,请使用 `--sweep KEY=v1,v2,v3` (可重复)而不是 `--count`。调度器分出所有轴的笛卡尔积,为每个成员替换 `{KEY}`; `{i}`(扁平成员索引)也可用:
```bash
job submit -p train --sweep lr=0.1,0.01 --sweep seed=1,2,3 \
-- python train.py --lr {lr} --seed {seed} --out run-{i}
# → 已提交数组A50:6个作业(id 50..55) # 2 × 3 = 6 成员
```
`--sweep` 和 `--count` 是互斥的,产品上限为1000个成员,并且 `i` 保留为轴键。替换是一个文字 `{key}` 替换(不是 `str.format`),因此JSON字面量和shell大括号中的命令不改变。
## MCP / 代理集成
jobd配备了一个MCP服务器(`jobd-mcp`),通过九个工具公开队列:`jobd_submit`,`jobd_status`,`jobd_logs`,`jobd_list`,`jobd_cancel`,`jobd_preempt`,`jobd_workers`,`jobd_job_get`,`jobd_worker_delete`。
[Claude Code](https://docs.claude.com/en/docs/claude-code) 的一句话示例:
```bash
claude mcp add jobd --env JOBD_URL=http://127.0.0.1:8765 --env JOBD_API_TOKEN=<your-token> -- jobd-mcp
```
或将任何其他MCP客户端指向它:
```json
{
"mcpServers": {
"jobd": {
"command": "jobd-mcp",
"env": {
"JOBD_URL": "http://127.0.0.1:8765",
"JOBD_API_TOKEN": "<your-token>"
}
}
}
}
```
`JOBD_API_TOKEN` 必须与调度器的令牌匹配,否则每个调用都返回401。仅当调度器使用 `JOBD_ALLOW_NO_AUTH=1` 运行而不进行身份验证时,才省略它。
现在,代理可以“运行此作业”,检查下一个会话,并通过调度器路由GPU工作而不是在共享卡上冲突。 `examples/claude-code-hooks/` 目录中有可选的 [Claude Code](https://docs.claude.com/en/docs/claude-code) 钩子,它们 **推动** (或硬阻塞)代理提交重命令通过jobd — 包括VRAM感知GPU保护 `# NO_GPU` / `# CONCURRENT_OK` / `# VRAM=NGB` 覆盖标记。
## 配置
`JOBD_CONFIG_DIR` 下的三个可选YAML文件(默认配置在 `config/`中提供):
- **`projects.yaml`** — 每个项目的基准优先级和提交默认值(可抢占性,墙壁/空闲超时,主机固定,功能需求)。有关完整的解析模型,请参阅 [docs/plans/projects-yaml.md](https://github.com/musharna/jobd/blob/main/docs/projects-yaml.md)。
- **`profiles.yaml`** — 命名资源包( `--profile gpu-train-large`)匹配器用于调整作业大小。
- **`classifier.yaml`** — 根据命令字符串自动建议配置文件。
这三个都是可选的;如果没有配置文件,则每个作业都在全局默认优先级运行。
## 并发(多槽)
默认情况下,每个工作节点一次运行 **一个作业**(`JOBD_WORKER_MAX_CONCURRENT_JOBS=1`)。提高它以让工作节点并行运行多个适合的作业:
```bash
JOBD_WORKER_MAX_CONCURRENT_JOBS=3 jobd-worker
```
匹配器是资源感知的,因此这不是 **盲目N增加超订阅**。每个正在进行的作业保留其 `vram_gb` / `ram_gb` / `cpus`占用空间,并且工作节点的心跳广告仅显示剩余的内容(`free_vram = raw − Σ 正在进行的作业`)。调度器不会放置一个作业,如果它不适合剩余的余量。实际效果:**一个CPU作业和一个GPU作业同时运行** - CPU作业保留0 VRAM,因此永远不会阻塞GPU槽,反之亦然。两个GPU作业仅在两者都适合实时VRAM( `/next-job` 准入门是对过度声明的最终安全网)时才会一起运行。
`job workers` 报告每个工作节点的槽位使用情况 — `running` 作业超出 `max_concurrent` — 以及实时资源广告:
```jsonc
// job workers
{ "host": "desktop", "state": "online", "running": 2, "max_concurrent": 3,
"free_vram_gb": 9.1, "idle_cpus": 6, ... }
```
从工作节点环境(systemd单元,shell,或 `worker.yaml` 环境)设置每个工作节点的限制 — 它是工作节点本地的设置,而不是调度器设置。
## 保留
默认情况下,jobd **永久保留每个作业记录和.log文件** — 历史记录永远不会丢失。在长时间运行的调度器上,选择启用修剪:```bash
JOBD_JOB_RETENTION_DAYS=30 jobd # 30天后删除终端作业及其日志
```
清除程序删除处于终端状态的作业,其`finished_at`时间戳早于该范围,然后取消链接其作业特定的 `.log`,并发出 `jobs_pruned` 事件。释放的 SQLite 页面在 WAL 下被重用,因此数据库文件在没有全局锁定的情况下保持有界。默认值为 `0`,保留所有内容;修剪旧终端父级是安全的,对于任何仍待处理的依赖项都是安全的。
## 安全
调度器 **没有TCP层身份验证**,除了一个共享的承载令牌,因此它被设计为在受信任的网络上运行(回环或Tailscale尾网),永远不会在公共接口上运行。两个堆叠的控件:
1. **接口绑定** — `JOBD_HOST` 必须是 `127.0.0.1` 或Tailscale CGNAT地址(`100.64.0.0/10`),而非 `0.0.0.0`。CI lint(`tests/test_deploy_lint.py`)强制执行此操作Docker部署。
2. **承载令牌** — 在每个调度器/工作节点/CLI/MCP主机上设置 `JOBD_API_TOKEN`(≥32个随机字节)。调度器在没有它的情况下拒绝启动,除非您显式设置 `JOBD_ALLOW_NO_AUTH=1`。 **`JOBD_ALLOW_NO_AUTH=1`** 仅适用于仅限回环的调度器(`JOBD_HOST=127.0.0.1`)- 对于非回环 `JOBD_HOST`,它将未经身份验证的RCE端点公开给整个尾网;调度器如果这样做,会记录启动警告。不要。
完整的威胁模型,环境变量参考和令牌轮换: **[docs/security.md](https://github.com/musharna/jobd/blob/main/docs/security.md)**。
## 许可
MIT - 参见 [LICENSE](https://github.com/musharna/jobd/blob/main/LICENSE)。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器