计算资源自动申请与调度系统 · 架构设计评审稿
版本:v1 评审稿 编写时间:2026-11-01
本文件是设计评审稿,不含已完成功能的宣称。第 3 节的现状事实均来自本机可核验的文件、接口与命令输出(表内注明来源与时间);其余章节为待实现的设计方案;第 17.3 节的三项需要你确认后才能定稿并进入编码。
1. 摘要(一页)
我们要做的是一个面向多个 agent 的计算资源控制面,把现在分散的三件事收拢成一条流水线:
- 资源从哪来:手工找机器 → 统一资源目录 + 可插拔的自动置备(已有静态池、devbox 动态申请、PAI 配额作业)。
- 任务怎么跑:口头协调"这张卡我用" → 队列 + 租约(lease)+ 抢占与配额,避免两个 agent 抢同一张卡。
- 机器坏了怎么办:断了重来 → 心跳、故障检测、重连、换机器续跑(checkpoint 契约),并用 fencing token 保证不出现"同一任务跑两份"。
同时提供一块所有人共用的看板:谁在用哪些卡、任务排队/运行/失败、GPU 有效利用率、排队与置备时延、每任务的 GPU·小时成本、以及哪些 agent 正在使用哪台机器、干的是什么任务。看板还带人类操作入口——公用卡可以设占用上限、预留人类份额、调优先级、驱逐任务;agent 则按上限自我限流,平时贴着上限跑。
设计原则:不自造重轮子。资源台账继续用 资源台账技能 技能的 资源台账文件;指标采集继续用现有 指标采集器 采样器;时间序列复用已装的 Prometheus + Grafana;队列与租约复用本机已验证的 内部租约队列(SQLite 租约队列)并泛化到多机;自动置备与故障转移参考 SkyPilot managed jobs 的生命周期模型;配额/抢占语义参考 Kueue/Volcano;对外共享接口按 Kubernetes 式的"声明式资源 + 控制器收敛"+ MCP 工具暴露给 agent。
交付节奏建议先做 Phase 0/1(只读聚合 + 队列租约),这两步不依赖任何新权限,1~2 天可见可用结果;内部开发机平台 的 devbox 自动创建需要先解决 token 自动化(见 §18.1)。
2. 目标与非目标
2.1 目标
| # | 目标 | 可验收结果 |
|---|---|---|
| G1 | 资源自动申请 | 提交一个"要 1×GPU ≥ 40 GB 显存、2 小时"的申请,系统在授权池内自动选点并置备(已有机器优先,必要时新建 devbox),返回可直接 SSH 的端点 |
| G2 | 统一调度 | 多 agent 并发提交时按优先级/配额排队,同一物理 GPU 不被两个租约同时占用;支持 TTL、续租、抢占 |
| G3 | 自动部署 | 置备完成后自动完成"环境探测 → 代码/资产同步 → 依赖安装 → 启动进程/systemd → 健康检查",幂等可重跑 |
| G4 | 自动重连与续跑 | 连接断开/进程退出/节点失联时自动重连;失败后可换机并从最近 checkpoint 续跑;全程同一任务不出现双跑 |
| G5 | 效率与任务可视化 | 一块局域网看板:资源池总览、GPU 热力图、队列与任务时间线、效率趋势、异常;数据带观测时间 |
| G6 | agent 共享 | 所有 agent(各类编码 agent 与远端 agent)通过同一 CLI/HTTP/MCP 接口查询资源、提交任务、看结果;共享黑板记录"谁在用什么、结论是什么" |
| G7 | 合规与可追溯 | 凭据只存位置不入库;远端任务代码放独占隐藏目录并按生命周期回收;每次租约、置备、清理都有审计事件与回执 |
| G8 | 公用卡占用上限 | 人类可在看板设定池上限与保留额;agent 超限时自我限流,限额内贴着上限运行;高优先级可抢占低优先级并从 checkpoint 恢复 |
2.2 非目标(本期不做)
- 不做 Kubernetes 集群化改造,不引入 K8s 控制面(成本远高于收益,见 §4.3)。
- 不做多租户收费系统,只做内部成本分摊视图(GPU·小时)。
- 不接管他人已在跑的作业;不改动共享机器的系统级配置。
- 不重写资源台账:资源台账文件 仍是资源事实的唯一来源,调度器只读它 + 写自己的运行态库。
- 不在本设计里自动执行任何远端变更;所有写操作都在实现阶段的显式开关下进行。
3. 现状盘点(事实与证据)
3.1 已有资产
| 资产 | 位置 | 事实 | 来源 |
|---|---|---|---|
| 资源台账 | <本机配置路径> | 记录各计算资源的归属、授权范围、连接方式、硬件与历史核验结论,并区分“已核验 / 不可达 / 已停止”等状态 | 台账读取,2026-11-01 |
| 台账写入器 | 技能 scripts/registry.py | flock 串行化 + 深度合并 + 追加 events.jsonl;拒绝疑似凭据入库 | 代码审阅 |
| 指标采样 | 指标采集器/collector.py | /usr/bin/python3 -m 指标采集器.collector --sample-only,systemd timer 每 30 分钟写 <本机配置路径> | 指标采集器.timer,最近一次 2026-11-01 15:30 |
| 采样内容 | 同上 | 多台开发机,按卡记录利用率、显存占用与错误信息;累计 1500+ 个样本 | 最新样本 2026-11-01 15:30 |
| 两份定时报告 | compute-resource-report.timer | 每天 11:00 / 20:00 生成飞书消息(工作日门控) | unit 文件 |
| PAI 配额视图 | 配额客户端 + token 文件(0600) | 配额集群:72 GPU(已分配 72)、864 CPU、9 节点、轮询策略;接口 配额接口、作业与节点接口 | 实测调用 2026-11-01 15:47 |
| 大显存池 | 大显存资源池 两端点 | 各 8×97887 MiB(合计 16 卡)、驱动 580.159.04、cc 12.0、180 核、约 1.8 TiB 内存;独立数据盘 约 985 GB | 台账记录,2026-11-01 核验 |
| devbox 平台 API | 内部开发机平台 | 开发机与作业管理接口组;Authorization 请求头携带访问令牌(浏览器本地存储),登录走 平台登录入口 | 前端 bundle 反查 + 既有历史证据 |
| 队列原型 | 内部租约队列模块 | SQLite 单机租约队列:resources/jobs 表、状态 queued/leased/running/finished/unknown、TTL 租约、heartbeat、provisioning/draining/quarantined、run_elastic + CatalogProvisioner | 代码审阅 |
| 监控栈 | 本机 | Prometheus 2.45.3 :9090(当前只抓 Ray 3 个 target)、Grafana 11.3.0 :3000、node_exporter :9100 | 端口与 API 实测 |
| 远端 agent | 技能 scripts/agent_client.py + apply_bundle.py | app-server WebSocket(capability-token、token 文件 600)、thread/start、turn/start、resume/read/interrupt、sandbox 映射(readOnly/workspaceWrite/dangerFullAccess) | 代码审阅 |
| 汇报/看板骨架 | 汇报汇总服务 :18780 → LAN :48780 | 已有统一工作汇总页与 40000–50000 端口约束 | 技能与 service 文件 |
3.2 现状痛点(来自真实使用记录)
- 资源事实分散:台账(技能)、采样 JSONL、quota-manager、Prometheus 各说一部分,没有一个"当前可用 GPU 在哪"的查询入口。
- 协调靠人:并发任务时靠对话/口径约定,没有租约,出现"两个人同时认为某卡空闲"的风险。
- 置备靠手:devbox 创建、环境安装、代码同步、启动、断线重连都是逐步手工;devbox 创建还曾遇到 容器初始化超时 后实例
stopped(4 例历史记录)。 - 没有"谁在用"视图:只能看 GPU 利用率,看不出是哪个 agent、哪个任务、哪份代码。
- 重连语义缺失:通信断开后既不敢重发(怕双跑),也无法可靠续跑(缺 checkpoint 契约)。
4. 参考的成熟方案与取舍
4.1 对照表
| 能力 | 成熟方案 | 取什么 | 不取什么(及原因) |
|---|---|---|---|
| 队列/配额/抢占 | Slurm、Volcano、Kueue | Kueue 的概念最贴合:Workload 是准入单位,ClusterQueue 管配额,Cohort 允许队列间借配额,优先级独立于 pod priority;Volcano 的 gang scheduling 用于"多卡一起要" | 不引入 K8s/CRD:我们只有十几台机器、几十张卡,装 K8s 控制面与 PAI 现有调度冲突,运维成本不成比例 |
| 任务全生命周期 | SkyPilot managed jobs | 明确的四件事:置备资源、自动从抢占/GPU 故障/节点崩溃中恢复、重试应用错误、结束后清理;长任务必须周期性 checkpoint 才能续跑;支持 job groups/流水线 | 不直接用 SkyPilot 本体:它强依赖云 provider/k8s 抽象,我们一半资源是 SSH + 内部 devbox API,适配成本高于按它的生命周期模型自建薄控制器 |
| 编排与健康检查 | Nomad + Consul | 声明式 job spec、健康检查驱动重调度、节点 drain | 不引入 Consul 集群;用 SQLite + 心跳替代服务注册 |
| 资源抽象/弹性 | Ray autoscaler | "逻辑资源池 → 物理节点"的两层映射、pending 任务驱动扩容 | 不用 Ray 做调度:现有 Ray 只用于特定实验,跨机器异构(PAI/SSH/devbox)不统一 |
| GPU 指标 | DCGM-Exporter + Prometheus + Grafana | 指标维度:util、显存、功耗、温度、XID/ECC 错误、每进程 GPU 占用;Grafana 做时间序列面板 | 暂不强制部署 DCGM(部分机器无容器权限/驱动差异);先用 GPU 查询命令 采到同等关键字段,DCGM 作为可选增强 |
| 成本/分摊 | 同期成熟项目的成本统计口径 | 按 GPU·小时分摊,区分已分配与有效占用 | 只做内部分摊视图,不做云账单级核算;单价内部折算 |
| 对外接口 | Kubernetes 声明式 API + 控制器、MCP | 资源/任务用声明式对象 + reconcile 循环收敛;对 agent 暴露 MCP 工具(list_resources / submit_job / job_status / lease_heartbeat…) | 不做 CRD/etcd;用 SQLite 表当对象存储 + 单进程 reconcile |
4.2 结论
采用"薄控制面 + 复用现有组件":SQLite 存对象与事件,单进程 reconciler 收敛状态,Prometheus 存时间序列,Grafana + 自研静态页做可视化,MCP/CLI 给 agent 用。总代码量控制在可控范围(预估 3.5k~5k 行 Python + 静态前端),不新增重量级依赖。
4.3 为什么不上 Kubernetes
- 现有资源一半是 SSH 直连机器(大显存资源池、devbox、PAI 节点),不是我们可管理的集群;K8s 的节点管理在这些资源上不可用。
- PAI 集群已有自己的调度器(轮询策略),我们再叠一层 K8s 调度会产生双调度冲突。
- 目标是"人和 agent 能可靠拿到卡并跑起来",不是平台工程;轻量控制面更容易审计、排障和回收。
5. 总体架构
┌──────────────────────────────────────────────┐
agents (any) │ 共享接入层 │
各类编码 agent │ CLI `csched` · HTTP API · MCP tools │
远端 agent └───────────────┬──────────────────────────────┘
agent │ token (file, 0600)
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 控制面 compute-scheduler(单进程,systemd user unit) │
│ │
│ API 层 ── 对象存储(SQLite/WAL) ── Reconciler 循环(1s) ── 事件总线 │
│ │ │ │ │
│ │ ├ resources (只读镜像自台账) │
│ │ ├ jobs / attempts / leases / agents / notes │
│ │ └ events(append-only 审计) │
│ │ │
│ ┌───┴────────────┬───────────────┬──────────────┬────────────────┐ │
│ │ Scheduler │ Provisioner │ Bootstrapper │ Supervisor │ │
│ │ 队列/配额/抢占 │ Provider 适配 │ 部署/启动 │ 心跳/重连/回收 │ │
│ └───┬────────────┴───────┬───────┴──────┬───────┴───────┬────────┘ │
└──────┼────────────────────┼──────────────┼───────────────┼───────────┘
│ │ │ │
┌─────▼─────┐ ┌──────────▼───────┐ ┌───▼────────┐ ┌───▼──────────┐
│ 静态池 │ │ workflow devbox │ │ SSH/隧道 │ │ 指标采集 │
│ 大显存资源池 16卡 │ │ 开发机接口 │ │ app-server │ │ sampler │
│ 开发机 │ │ PAI 配额作业 │ │ systemd │ │ DCGM(可选) │
└───────────┘ └──────────────────┘ └────────────┘ └───┬──────────┘
│
┌──────────────┐ ┌──────────────────────────┐ │
│ Prometheus │◄───────┤ push/Pull: 指标样本 │◄──────┘
│ :9090 │ └──────────────────────────┘
└──────┬───────┘
▼
┌──────────────┐ ┌──────────────────────────────┐
│ Grafana :3000│ │ 自研看板 /dashboard (LAN) │
│ 时间序列 │ │ 资源/队列/任务/agent/效率 │
└──────────────┘ └──────────────────────────────┘
5.1 分层职责
| 层 | 职责 | 不做什么 |
|---|---|---|
| 接入层 | 鉴权、参数校验、幂等键、把请求翻译成控制面对象;MCP 工具包装 | 不做调度决策 |
| 调度器 | 从队列取候选、按资源/配额/优先级选点、生成租约、续租/过期/抢占 | 不直接 SSH、不直接改远端 |
| 置备器 | Provider 适配:静态池选机、devbox 创建/启动、PAI 作业占位;把"机器"变成"可用端点" | 不装业务环境 |
| 引导器 | 幂等部署:目录、代码/资产、依赖、systemd 单元、健康检查 | 不决定跑什么任务 |
| 监督器 | 心跳、进程/网络/GPU 健康、故障判定、重连/换机、回收与清理回执 | 不修改任务内部逻辑 |
| 观测层 | 采样、写入时序库、汇总视图、告警 | 不做准入决策(低利用率告警只读) |
6. 核心数据模型
对象都带 schema_version、created_at、updated_at、labels。控制面库沿用 WAL 模式(与 ResourceQueue 一致:BEGIN IMMEDIATE + busy_timeout=30000)。
6.1 表结构(DDL 草案)
-- 资源:以台账为准的只读镜像 + 运行时状态
CREATE TABLE IF NOT EXISTS resources (
resource_id TEXT PRIMARY KEY,
provider TEXT NOT NULL, -- static-ssh | workflow-devbox | pai-job | 大显存资源池-ssh
platform TEXT NOT NULL, -- linux-大显存资源池 | devbox-snow5090 | pai-配额集群 ...
capacity TEXT NOT NULL, -- JSON: {gpu:[{index,model,mem_mib}],cpu,mem_mib,disk}
capabilities TEXT NOT NULL, -- JSON: {docker,root,cuda,driver,gpu_cc,network}
classification TEXT NOT NULL, -- trusted-dedicated | shared | unclassified
sandbox TEXT NOT NULL, -- dangerFullAccess | workspaceWrite | readOnly
endpoint TEXT, -- JSON: {kind:ssh|ws, host, port, user, jump}
state TEXT NOT NULL CHECK (state IN
('ready','provisioning','draining','quarantined','retired','unreachable')),
ledger_status TEXT, -- 台账原值,便于对账
last_seen_at REAL, health TEXT, -- JSON: 最近健康检查结果
updated_at REAL NOT NULL
);
-- 任务(用户/agent 的申请单位)
CREATE TABLE IF NOT EXISTS jobs (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
job_id TEXT NOT NULL UNIQUE,
idempotency_key TEXT UNIQUE,
owner TEXT NOT NULL, -- human:herbert | agent:<agent_id>
agent_id TEXT, -- 发起方 agent(用于"被哪些 agent 使用")
platform TEXT, -- 硬约束:devbox/pai/大显存资源池
requirements TEXT NOT NULL, -- JSON: {gpu_count, gpu_mem_mib, cpu, mem_mib, gpu_type, hours, gang}
priority INTEGER NOT NULL DEFAULT 0, -- 0 normal, 10 high, 20 critical
queue TEXT NOT NULL DEFAULT 'default',
spec TEXT NOT NULL, -- JSON: 镜像/代码来源/启动命令/checkpoint 契约
state TEXT NOT NULL CHECK (state IN
('queued','provisioning','deploying','running','succeeded','failed',
'preempted','cancelled','unknown')),
checkpoint TEXT, -- JSON: {path, last_step, updated_at}
resume_count INTEGER NOT NULL DEFAULT 0,
created_at REAL NOT NULL, updated_at REAL NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS one_job_per_idem ON jobs(idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS jobs_queue ON jobs(state, priority DESC, seq);
-- 租约(防止两个任务占同一张卡;fencing token 防双跑)
CREATE TABLE IF NOT EXISTS leases (
lease_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES jobs(job_id),
resource_id TEXT NOT NULL,
allocation TEXT NOT NULL, -- JSON: {gpu:[0,1],cpu:8,mem_mib:32768}
fencing_token INTEGER NOT NULL, -- 单调递增;任务侧校验,旧 token 不得写结果
state TEXT NOT NULL CHECK (state IN ('leased','running','released','expired','revoked')),
acquired_at REAL NOT NULL,
heartbeat_at REAL NOT NULL,
expires_at REAL NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS one_active_lease_per_device
ON leases(resource_id, json_extract(allocation,'$.gpu[0]'))
WHERE state IN ('leased','running'); -- 实际实现用 allocation_fingerprint 列
-- 尝试(每次置备/部署/运行算一次 attempt,故障转移产生新 attempt)
CREATE TABLE IF NOT EXISTS attempts (
attempt_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL, resource_id TEXT, lease_id TEXT,
epoch INTEGER NOT NULL, -- 第几次尝试
state TEXT NOT NULL, -- provisioning|deploying|running|failed|succeeded
exit_code INTEGER, log_path TEXT,
error_class TEXT, -- oom|xid|network|auth|code|preempted|unknown
started_at REAL, ended_at REAL
);
-- agent 注册与心跳
CREATE TABLE IF NOT EXISTS agents (
agent_id TEXT PRIMARY KEY,
kind TEXT NOT NULL, -- local|remote|human
owner TEXT, model TEXT, host TEXT, cwd TEXT,
capabilities TEXT, -- JSON
state TEXT NOT NULL, -- active|idle|offline
last_seen_at REAL NOT NULL
);
-- 共享黑板(结论/环境/资产复用经验,只在内部可见)
CREATE TABLE IF NOT EXISTS notes (
note_id TEXT PRIMARY KEY,
topic TEXT NOT NULL, -- env|asset|result|warning
resource_id TEXT, job_id TEXT,
author TEXT NOT NULL, -- agent_id 或 human
body TEXT NOT NULL,
evidence TEXT, -- JSON: 路径/哈希/命令输出位置
created_at REAL NOT NULL
);
-- 审计(append-only,不 UPDATE/DELETE)
CREATE TABLE IF NOT EXISTS events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at REAL NOT NULL, actor TEXT NOT NULL,
kind TEXT NOT NULL, -- lease.acquire|job.state|provision.start|...
object_id TEXT, payload TEXT
);
-- 指标汇总(细粒度原始样本继续走 JSONL/Prometheus)
CREATE TABLE IF NOT EXISTS metric_rollups (
bucket_start REAL NOT NULL, bucket_seconds INTEGER NOT NULL,
resource_id TEXT NOT NULL, metric TEXT NOT NULL,
value REAL, sample_count INTEGER,
PRIMARY KEY (bucket_start, bucket_seconds, resource_id, metric)
);
配额、上限审计与抢占记录的表结构见 §13.4(quotas、quota_events、preemptions),与本节同库。
6.2 与现有台账的关系
- 资源台账文件 唯一可写仍归技能
registry.py;控制面只读并镜像到resources表(ledger_status保存原值)。 - 台账里的
status(verified/unreachable/...)是历史核验结论,控制面的state才是当前运行态;两者同时展示,避免"把历史核验当实时可用"。 - 忙闲必须当次查询(
GPU 查询命令/ quota API),不能拿台账里的gpu_inventory当实时。
7. 对外 API
统一前缀 /api/v1,认证 Authorization 请求头携带令牌(token 文件 0600,放 <本机配置路径>,不入库不入仓)。写操作支持 Idempotency-Key。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /resources | 资源列表;过滤 ?state=ready&platform=大显存资源池&gpu_min=40;返回 capacity/health/当前租约 |
| GET | /resources/{id}/health | 当次探测(SSH + GPU 查询命令 + 进程),带观测时间 |
| GET | /agents | 哪些 agent 在线、在跑什么、最近心跳 |
| POST | /jobs | 提交任务(声明式 spec + requirements),返回 job_id;幂等 |
| GET | /jobs | 队列/运行/历史;?state=&owner=&agent_id=&since= |
| GET | /jobs/{id} | 详情:attempts、lease、日志路径、checkpoint、错误分类 |
| POST | /jobs/{id}/cancel | 取消(释放租约 + 远端优雅停止) |
| POST | /jobs/{id}/retry | 显式重试(默认从 checkpoint 续跑) |
| POST | /leases/{id}/heartbeat | 续租(任务侧/监督器调用) |
| GET | /metrics/summary | 效率汇总:池利用率、排队 P50/P95、置备 P95、成功率、闲置卡时 |
| GET | /metrics/series?metric=gpu_util&ids=...&from=&to= | 时间序列(转 Prometheus 查询或读 rollup) |
| GET | /notes / POST /notes | 共享黑板 |
| GET | /events?since= | 审计/事件流(供其他 agent 订阅) |
| GET | /quota | 各池与各主体的 cap / used / reserved / available 及"还能开几张卡" |
| GET | /quota/events | 上限变更审计 |
| PUT | /quota/{id} | 人类设定/调整上限(池上限、主体上限、人类保留额);agent 无权限 |
| POST | /queues/{name}/pause · /resume | 人类冻结或恢复某队列的新分配 |
| POST | /jobs/{id}/preempt | 抢占指定任务(受 13.3 条件约束,记 preemptions) |
| GET | /healthz | 控制面自身健康 |
7.1 MCP 工具(给 agent 直接调用)
csched.list_resources(filters) -> [{id, gpu, state, free_gpu, health}]
csched.submit_job(spec) -> {job_id}
csched.job_status(job_id) -> {state, attempt, checkpoint, log_tail_url}
csched.lease_heartbeat(lease_id) -> {expires_at}
csched.cancel_job(job_id) -> {state}
csched.note_add(topic, body, evidence) -> {note_id}
csched.metrics_summary(window) -> {...}
csched.quota_status() -> {pools:[{cap,used,reserved,available}], me:{...}}
csched.quota_status 是 agent 的必查项:提交前先问自己还能用多少,按 available 决定并发;达到上限时排队或换池,不循环重试。人类侧的上限调整、驱逐、暂停队列不暴露为 MCP 工具,只在看板的人类登录态下可用。
CLI 与 MCP 共用同一 HTTP 客户端,工具描述里写明"这是共享控制面,不是私有资源",避免 agent 绕过租约直接 SSH 抢卡。
8. 关键流程
8.1 提交到运行
agent ──POST /jobs──► API ──► jobs(state=queued)
│
Scheduler 选取(配额/优先级/亲和)
▼
jobs(state=provisioning) + attempts(epoch=1)
│
Provisioner:静态池?已有机器?需要新建?
│ │
ready 机器 workflow 建机接口
│ │ 轮询 detail 直到 running/ssh 可用
└────────┬───────────┘
▼
lease(leased, fencing_token) + 资源置 draining 标记(他人不可抢)
▼
Bootstrapper:同步代码到隐藏根 → 装依赖 → 写 systemd unit → 启动
▼
healthcheck 通过 → jobs(running) + lease(running)
▼
Supervisor:心跳 / 日志尾 / checkpoint 监控
8.2 故障 → 重连 → 续跑
心跳超时 / SSH 失败 / 进程退出 / GPU 查询命令 XID 错误
│
▼
attempts.state=failed(error_class)
│
┌────┴─────────────────────────┐
│ 可原地恢复? │ 不可原地恢复(节点失联/驱动故障/被抢占)
│ (网络抖动/隧道断) │
▼ ▼
重连(退避 30/60/120s) 换机:新 attempt(epoch+1)
同一 lease 续租 新 lease,新 fencing_token
│ │ 从 checkpoint.resume_from 续跑
└──────────┬───────────────────┘
▼
若超过 max_attempts 或 error_class=code(应用错误)→ failed,不再自动重试
双跑防护:任务侧写结果/checkpoint 前必须校验 fencing_token 等于当前租约;旧 attempt 的写入被监督器丢弃并记 stale_write 事件。远端任务启动脚本带 CSCHED_EPOCH,同一 epoch 已有存活进程时拒绝重复启动(pidfile + 进程启动时间双重校验,避免 PID 复用误判)。
8.3 结束与回收
任务结束 → 结果与证据取回(清单+SHA256)→ 远端隐藏根清理 → 清理回执(events)
→ lease=released → 资源 state 回 ready(或 draining 完成后 ready)
清理失败不阻塞租约释放,但会生成 cleanup_pending 事件并在看板"异常"里挂起,直到回执成功(沿用技能里"测试结束必须清理"的规则,中断也要保留待清理状态)。
9. 自动置备("自动申请")
9.1 Provider 接口
class Provider(Protocol):
name: str
def list_candidates(self, req: Requirements) -> list[ResourceCandidate]: ...
def provision(self, cand: ResourceCandidate, spec: JobSpec) -> ProvisionHandle: ...
def wait_ready(self, handle: ProvisionHandle, deadline: float) -> EndpointInfo: ...
def release(self, handle: ProvisionHandle, *, destroy: bool) -> CleanupReceipt: ...
9.2 适配器与优先级
| 顺序 | Provider | 适用 | 现状依据 | 需要的前置 |
|---|---|---|---|---|
| 1 | static-ssh | 大显存资源池 两端点、已有 devbox、PAI 节点 | 台账 verified,SSH 可达 | 已具备 |
| 2 | 大显存资源池-ssh | 大显存(95.6 GiB)突发 | 用户已授权全量使用 | 已具备(注意 独立数据盘 与系统盘容量) |
| 3 | workflow-devbox | 需要独立容器/隔离环境 | API 端点已确认 | token 自动化(§18.1) |
| 4 | pai-job | 走 PAI 配额,配额内 72 卡已 100% 分配 | 配额作业接口 只读已验证 | 写权限与配额释放策略 |
选择策略:先复用、后新建(与技能"环境与资产复用"一致)。同一需求下按 成本(占用时长) → 排队时延 → 亲和性(已有环境/数据) → 碎片率 排序,尽量填满而不是留洞。
9.3 置备失败处理
- devbox 容器初始化超时(历史 4 例)→ 标记该节点
quarantined12 小时,换下一个候选;连续 2 次同类失败升级为页面告警。 - 创建成功但 SSH 不通 → 有限次数重试(3 次,指数退避),仍失败则
destroy回收,避免"僵尸实例占配额"。 - 任何新建实例都写入
provision_handles(含创建人、用途、TTL),TTL 到期自动回收,防止忘记删除。
10. 自动部署与重连
10.1 幂等部署(Bootstrapper)
单次部署 = 一组可重复执行的步骤,每步有 done 标记与版本哈希:
- 预检:
hostname、驱动/CUDA、GPU 查询命令、磁盘(,独立数据盘分开判断)、已有进程。 - 目录:任务独占隐藏根
<本机配置路径>(权限 700)或<独立数据盘>/<task-id>/;登记所有者标记文件,避免误删他人目录。 - 代码/资产:优先复用已有共享资产(只读),只传差量;传完校验 SHA256 清单。
- 依赖:优先复用目标机已有环境;需要新建时用隔离 venv/conda env,不污染系统 Python。
- 启动:写 systemd user unit 或 supervisor 脚本,带
Restart=on-failure、RestartSec=30、StartLimitBurst=3/300s、日志路径、fencing token 环境变量。 - 健康检查:进程存活 + 端口/心跳文件 + 首个 GPU 批次的真实前向(不允许"进程起来就算成功")。
- 登记:
attempts写deploying→running,事件记录版本、命令、路径、哈希。
10.2 通信与重连(沿用已验证的远端 agent 模式)
- 优先
app-server(WebSocket + capability token + token 文件 600),SSH 仅用于 bootstrap/修复。 - 无私网时用一条持久 SSH 隧道(
ServerAliveInterval=60、ExitOnForwardFailure=yes),后续只走 WS。 - 断线处理:先
resume/read读已保存的 thread/job 状态,绝不重发启动命令;认证失败不重试(避免锁号),网络失败退避 30/60/120s 后停止并上报。 - 长任务保存
thread/turn id、job_id、checkpoint、日志路径,供任何 agent 接手。
10.3 checkpoint 契约
任务 spec 里声明:
{"checkpoint": {"dir": "独立数据盘/<task>/ckpt", "interval_s": 300,
"resume_arg": "--resume {path}", "manifest": "latest.json"}}
监督器只负责:按间隔检查 latest.json 是否更新、记录 last_step、故障转移时把路径传给新 attempt。任务自己负责写一致性 checkpoint;无法 checkpoint 的任务在 spec 里标 resumable=false,故障转移时明确"从零重跑"并把这一事实写进任务详情(不假装续跑)。
11. 效率账本与指标定义
11.1 指标
| 指标 | 定义 | 数据源 | 用途 |
|---|---|---|---|
| 池占用率 | Σ 已租约 GPU·分钟 / Σ 可用 GPU·分钟 | leases + resources | 是否"有卡没人用" |
| 有效利用率 | Σ min(gpu_util, 100) 的采样均值(按卡×时间加权) | sampler / DCGM | 是否"占着不用" |
| 显存占用 | used/total(按卡) | sampler | 选卡与 OOM 预警 |
| 排队时延 | started_provision - created 的 P50/P95 | jobs/events | 供需是否失衡 |
| 置备时延 | ready - provision_start P95 | attempts | 平台健康度 |
| 部署时延 | running - ready P95 | attempts | 环境/网络瓶颈 |
| 成功率 | succeeded / (succeeded+failed),按 platform/agent 分组 | jobs | 质量与归因 |
| 故障转移率 | resume_count>0 的任务占比 | jobs | 稳定性 |
| MTTR | 故障发生到恢复运行的中位时长 | events | 重连能力 |
| 闲置浪费 | 租约内 util<10% 的 GPU·小时 | leases×sampler | 回收与提醒 |
| 单位成本 | GPU·小时 × 内部单价;按 owner/agent 汇总 | rollups | 分摊视图 |
| 低利用率告警 | util<40% 且非豁免 owner(与现有口径对齐) | 配额客户端 | 与现有报告一致 |
采样粒度:调度层用 1 分钟(现在 30 分钟太粗,只能做趋势不能做回收判定);30 分钟样本继续保留做长期归档,两者写不同 bucket。保留期建议:原始 30 天,rollup 1 年。
11.2 时间序列与存储
- 新采样器把
gpu_util / gpu_mem / lease_state / job_state通过 Prometheus pushgateway 或 textfile collector 暴露(本机已有 node_exporter,可用 textfile 目录,零新组件),Grafana 直接出图。 - 明细与任务视图走控制面 SQLite,不塞进 Prometheus(高基数标签会拖垮时序库)。
12. 可视化界面
看板部署在本机(已确认):http://内网主机:48794/(沿用 40000–50000 端口约束;48792 目前由设计评审页占用,评审结束后释放),由本机 systemd user unit 提供静态页与 /api,不另建服务器。Grafana 保持 :3000 内部使用、按需反代到看板子路径。看板需要人类登录态,因此只发布到局域网,不登记公网子域名;评审页用过的公网子域名与本看板无关。
12.1 页面结构(5 个主区块,避免堆栏目)
- 总览:可用 GPU/总 GPU(按池)、运行/排队任务数、P95 排队、今日失败数、异常列表(每个数字带观测时间)。
- 资源:按机器分组的卡阵列热力图(util 颜色 + 显存条),点开看进程、驱动、健康检查结果、当前租约与 owner/agent。
- 任务:队列与运行中列表 + 甘特式时间线(排队→置备→部署→运行→结束),失败原因分类,点开看日志尾与 checkpoint。
- 效率:利用率和排队趋势(Grafana iframe 或本地图表)、Top 消耗(按 agent/owner)、闲置卡时。
- agent:哪些 agent 在线、用什么模型、当前持有哪些租约、最近提交/结论笔记;"被哪些 agent 使用"直接可查。
设计约束:
- 静态前端(无 CDN 依赖,图表库本地 vendored),30 秒能看懂"现在能不能跑、卡在哪、谁在用"。
- 所有时间戳是观测时间,不是页面刷新时间;数据源故障时显示"未知/过期",不把失败画成 0。
- 移动端单列可用;表格可筛选,详情走抽屉,不整页跳转。
12.2 人类操作接口(看板内的控制面板)
看板不只是只读视图:公用卡必须有一个人能改的"闸门",否则 agent 会一直往上顶。因此看板提供一组只对人类开放的写操作:
| 操作 | 作用 | 生效方式 |
|---|---|---|
| 设置/调整池上限 | 某个共享池最多允许我们占用多少张卡(cap_gpu) | 立即;下调时超出部分按优先级进入 draining |
| 设置人类保留 | 从池里预留多少张给人类,agent 不可用(reserved_gpu) | 立即;已在跑的租约到期后不再续 |
| 调整按主体上限 | 某个人 / 某个 agent 的并发与总量上限 | 立即,新提交按新上限裁决 |
| 调整优先级 | 改队列或单个任务的优先级,影响抢占顺序 | 下一次调度循环 |
| 暂停 / 恢复队列 | 冻结新分配,不影响运行中任务 | 立即 |
| 取消任务 / 驱逐 | 结束指定任务(优雅停止 + 释放租约) | 立即,需二次确认 |
| 批准自动置备 | 新建实例前的人工确认开关(可全局或按池) | 对后续置备请求生效 |
| 查审计 | 谁在什么时候改了什么上限、驱逐了谁 | 只读 |
权限与安全:
- 人类身份走浏览器登录(或本机私有 token 文件),agent 身份不能调用这些写接口;只有人类显式把某个池的"代管"授权给指定 agent 时例外,且该授权单独记审计。
- 所有写操作写 append-only 审计事件(操作者、对象、旧值、新值、原因备注)。
- 上限下调、驱逐、取消都属破坏性操作:二次确认 + 结果回执(成功/失败原因),失败不能显示成成功。
- 页面上每个上限都显示"当前值 / 谁在何时设的 / 生效中还是在等待释放"。
12.3 与工作汇报页的关系
运维看板 ≠ 成果汇报页。工作汇报页只放结果(沿用 汇报页技能 的内容边界);调度看板放运行态数据。两者可互相链接,但不混排。
13. 占用上限、抢占与自我限流
公用卡的核心矛盾:agent 越多、跑得越久,越容易把大家共用的池子占满,人反而排不进来。因此系统必须有一个硬的占用天花板,并且 agent 要自己认这个天花板——而不是等人来手工 kill。
13.1 三层上限
| 层级 | 对象 | 含义 | 谁能改 |
|---|---|---|---|
| L1 池上限 | 一个共享池(如某个公用机器组) | 我们最多占用多少张卡;超出即不可再分配 | 人类(看板)+ 可按池授权给指定 agent 代管 |
| L2 主体上限 | 某个人 / 某个 agent | 并发任务数、GPU 总量、GPU·小时/天 | 人类;也可给 agent 一个自设上限的额度 |
| L3 保留额 | 池内 | 预留给人类、agent 不得动用的卡数 | 人类 |
约束关系:已占用 ≤ 池上限 − 人类保留额 且 该主体占用 ≤ 主体上限。两个条件同时满足才批准新租约。
上限是软实时的:上调立即生效;下调不打断正在跑的任务,而是把超额部分标记为 draining——不再续租,任务自然结束后回收,必要时按优先级驱逐(见 13.3)。
13.2 "贴着上限跑"的自我限流
这是本设计对 agent 行为的明确要求:在不超过上限的前提下尽量用满,而不是保守地只用一点点,也不能越过上限。
实现方式是把上限直接暴露给 agent,让它无需猜测:
GET /api/v1/quota返回每个池与每个主体的cap / used / reserved / available,以及"现在还能开几张卡"。csched submit在超限时返回明确错误(quota_exceeded,带当前用量与上限),不排队也不偷偷降级。- MCP 工具
csched.quota_status()让 agent 在规划阶段先问一句"我还能用多少",再决定并发。 - 提交时若
available == 0,agent 应当选择:等待队列、降低并发、或换池;而不是反复重试刷请求。 - 用量接近上限(如 ≥ 90%)时,看板与 agent 侧都给出提示;达到上限则只允许排队与替换(先释放再申请)。
配套的调度侧规则:
- 一个池只要有空闲额度就立刻分配,不做人为节流("贴上限"是目标而非保守策略)。
- 碎片整理:当剩余额度不足以放下排队任务、但可通过迁移小任务腾出整卡时,按 13.3 的 drain 规则腾挪。
- 上限内优先填满而不是留洞,除非触发了人类保留额。
13.3 抢占(已确认开启)
高优先级任务可以抢占低优先级任务,但必须"可恢复、可解释、有上限":
优先级:critical(30) > high(20) > normal(10) > batch(0),数字大者优先;同级按提交时间 FIFO。优先级可由人类在看板调整,agent 只能在自己被授予的范围内设置(默认只能 normal 及以下,防止 agent 互相抬价)。
可抢占条件(全部满足才动手):
- 申请方优先级严格高于被抢方;
- 被抢任务
resumable=true(有 checkpoint 契约)或已接近完成且人类确认可弃; - 抢占后能真正腾出所需的整卡/显存,而不是只挪走一半;
- 被抢任务未处于关键写入窗口(如 checkpoint 落盘中的保护期,默认前后 30 秒不抢)。
抢占流程:preempt_notice → 保存 checkpoint(最多等 grace_s,默认 120 秒)→ 优雅停止进程 → 释放租约 → 抢占方获租约 → 被抢任务回到队列头部(preempted 状态,保留原优先级与 epoch,恢复时 epoch+1、新 fencing token)。
配额内抢占:抢占用的是池上限内的额度,不突破 13.1 的天花板;若抢完之后总占用仍在上限内则允许,否则拒绝并给出原因。
频控与防抖:
- 同一任务在 30 分钟内被抢占 ≥ 3 次,自动提升其抗抢占等级(或暂停其入队重排),避免来回抖动。
- 抢占方在抢占后短时间内若立刻空闲(如 10 分钟内利用率 < 10%),记一次
wasteful_preemption审计并要求说明,连续发生则限制其抢占权。 - 人类保留额内不允许任何抢占发生。
13.4 上限与抢占的数据结构补充
-- 配额定义(L1 池上限、L2 主体上限、L3 保留额)
CREATE TABLE IF NOT EXISTS quotas (
quota_id TEXT PRIMARY KEY, -- pool:<id> | subject:<owner> | subject:<agent_id>
kind TEXT NOT NULL CHECK (kind IN ('pool','subject','reserved')),
scope TEXT NOT NULL, -- 池名或主体名
max_gpu INTEGER, -- NULL = 不限
max_jobs INTEGER,
max_gpu_hours_per_day REAL,
updated_by TEXT NOT NULL, -- human:<id> | agent:<id>(仅被授权时)
updated_at REAL NOT NULL,
reason TEXT
);
-- 上限变更审计(append-only,与 events 分开便于快速查询当前有效值)
CREATE TABLE IF NOT EXISTS quota_events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at REAL NOT NULL, actor TEXT NOT NULL,
quota_id TEXT NOT NULL,
field TEXT NOT NULL, old_value TEXT, new_value TEXT, reason TEXT
);
-- 抢占记录
CREATE TABLE IF NOT EXISTS preemptions (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at REAL NOT NULL,
victim_job TEXT NOT NULL, preemptor_job TEXT NOT NULL,
reason TEXT, checkpoint_saved INTEGER, grace_used_s REAL,
outcome TEXT -- resumed | cancelled | failed
);
13.5 Agent 侧必须遵守的规则(写进技能与工具描述)
- 提交前先查
quota_status,按available决定并发。 - 达到上限时排队或等待,不循环重试、不改名绕过、不直接 SSH 抢卡。
- 拿到租约后保持利用率(贴上限),长时间空转会被判定为闲置并回收。
- 被抢占时配合保存 checkpoint,不抗拒、不重启抢占。
- 人类保留额与上限由人设定,agent 不得自行上调(除非被显式代管授权)。
14. 多 agent 共享
14.1 共享内容
| 共享对象 | 内容 | 写入方 | 读取方 |
|---|---|---|---|
| 资源事实 | 台账镜像 + 当次健康 | 台账(人/agent) | 所有 agent |
| 租约与任务 | 谁在用什么、跑到哪 | 控制面 | 所有 agent |
| 结论与经验 | 环境可用性、资产路径、失败模式 | agent 主动写 note | 所有 agent |
| 事件流 | 状态变化、审计 | 控制面 | 订阅者 |
14.2 身份与权限
- 每个 agent 注册
agent_id(如agent-<主机>、agent-<会话>),携带owner、model、host、cwd、能力标签。 - 权限直接复用台账的
classification+sandbox:trusted-dedicated可全权限;shared按该项配置;未知不猜测。 - 跨 agent 操作必须经过租约:不允许绕过控制面直接 SSH 抢正在租约内的卡;检测到旁路占用(租约内 util=0 但有他人进程)时记
lease_conflict事件并告警,但不擅自 kill 他人作业。
14.3 与 DSH/其他体系的关系
本控制面是"资源与任务的共享事实源",不替代各 agent 自己的任务编排(例如 内部评测流水线)。集成方式:那些流水线把 ResourceQueue 指向控制面的租约接口,或直接用 csched submit 提交。
15. 安全、生命周期与合规
- 凭据:只存文件位置(0600),不入台账、不入库、不入仓、不进日志;
registry.py的凭据检测逻辑复用。 - 远端代码生命周期:任务代码只进独占隐藏根,任务结束取回证据→核验 SHA256→删除→留清理回执;中断保留待清理状态。
- 最小暴露:控制面默认监听 LAN,token 鉴权;不默认暴露公网;MCP 工具不带"删除他人资源"类能力。
- 审计:所有写操作 append-only 事件(谁、何时、对哪个对象、参数摘要)。
- 破坏性操作:
destroy实例、cancel他人任务需显式--yes并记审计;控制面不自动 kill 非本系统启动的进程。 - 数据分级:看板展示资源与任务元数据,不展示任务内部数据内容;敏感数据只留路径与哈希。
16. 部署与运行方式
| 组件 | 位置 | 形态 |
|---|---|---|
| 控制面 API+reconciler | 本机 2404(内网主机) | systemd user unit 控制面服务,SQLite 在 <本机配置路径>(0600/0700) |
| 采样器 | 本机 | 扩展现有 指标采集器.timer,1 分钟;输出同时写 JSONL 与 node_exporter textfile |
| 看板 + 人类接口 | 本机(已确认) | 静态页 + 控制面同源 /api,0.0.0.0:48794,仅局域网可访问 |
| Grafana/Prometheus | 本机 | 复用现有实例,新增 dashboard JSON 版本化 |
| 远端 bootstrap | 各资源 | 一次性 SSH + 常驻 app-server(token 600),任务根 <本机配置路径> |
网络与代理:PAI/quota 接口经本机 7897 代理;大显存资源池/devbox 走内网直连;Grafana 仅内网。所有出站失败退避,不并发风暴。
17. 分阶段落地计划与验收
| 阶段 | 内容 | 预计工作量 | 验收标准 |
|---|---|---|---|
| P0 只读聚合(无新权限) | 台账镜像 + 采样器 1 分钟改造 + /resources /metrics/summary + 看板总览/资源两页 | 1~2 天 | 页面能回答"现在哪些卡可用、谁在用";数据带观测时间;断源时显示未知 |
| P1 队列与租约 | 泛化 ResourceQueue:多机资源、TTL、心跳、fencing token、/jobs 全套;CLI csched submit/status/cancel | 2~3 天 | 两个 agent 并发提交 3 个任务,无同卡冲突;租约过期自动释放;取消能真停 |
| P2 置备 | static-ssh/大显存资源池-ssh Provider;workflow-devbox Provider(先只读核对权限,通过后开自动建机,未通过则保留人工确认);实例 TTL 回收 | 2~3 天 | 静态池自动选点成功;失败候选自动换机;僵尸实例 0;devbox 建机权限核对有结论 |
| P3 部署与重连 | Bootstrapper 七步、systemd 单元、健康检查、断线重连、checkpoint 续跑、fencing 校验 | 3~5 天 | 人为 kill 进程/断网/重启节点后任务自动恢复并续跑;双跑事故 0 |
| P4 效率与共享 | 指标 rollup、Grafana dashboard、效率页、agent 注册与"谁在用"矩阵、note 黑板、MCP 工具 | 2~3 天 | 看板能给出每个 agent 的 GPU·小时与利用率;其他 agent 通过 MCP 完成一次真实提交 |
| P5 配额与抢占(已确认要做) | 三层上限(池/主体/保留额)、/quota 与 csched.quota_status、优先级抢占、drain 迁移、抢占频控 | 3~4 天 | 上限可设且立即生效;超限被明确拒绝;高优先级可抢占并从 checkpoint 恢复;抢占抖动被频控压住 |
看板的人类操作接口(§12.2)不单列阶段:P0 先出只读视图,P1 起随租约一起提供取消/驱逐,P5 补齐上限设置、队列暂停与审计查询。
每阶段结束都做一次真实端到端演练(不是单测):提交 → 置备 → 部署 → 跑 → 断网 → 恢复 → 结束 → 清理回执,并保留证据。
18. 风险与未决问题
18.1 devbox 自动建机:权限核对与凭据取用(P2 前置)
已确认允许系统自动创建 devbox,但是否具备建机权限尚未核实。这里有两个独立问题,必须分开解决:
问题一:权限与范围(先核对,不改动任何东西)
按 §18.3 的核对表做只读探查:现有凭据能否列集群/节点、能建在哪些集群、建机数量或规格有没有上限、是否需要审批。核对通过前,自动置备一律走"人工确认"模式——系统只提议选点与规格,看板上点一次确认才真正建机。
问题二:凭据怎么取(核对之后再定)
平台写接口需要 Authorization 请求头携带令牌,token 存在浏览器 localStorage(访问令牌),登录走飞书 OAuth,无法在无交互 SSH 里刷新。三条路:
- A. 人工提供长期 token 文件(token 存 0600 文件,控制面只读),过期需人工换一次。
- B. 浏览器辅助刷新:在本机已登录的浏览器上读取 localStorage 并写回 token 文件(不落日志、不打印)。需要明确授权这种"用你自己的浏览器会话取 token"的方式。
- C. 只在页面提供一键入口:不自动取 token,人工点按钮触发建机。
当前建议:先做问题一的只读核对;核对通过后再在 A / B 中选一种(A 更简单,B 体验更好但需要授权)。C 作为兜底随时可用,不阻塞 P0/P1。
18.2 其它风险
| 风险 | 影响 | 缓解 |
|---|---|---|
| 配额池已满 | 无法在配额内新开作业 | 走大显存资源池突发;配额释放靠人工/现有流程,不擅自动他人作业 |
| 共享机器上他人任务 | 抢卡冲突、被 OOM | 租约只登记"我们要用的卡",启动前 GPU 查询命令 复核;冲突记事件并让位给显存更紧的一方(可配置) |
| 驱动版本不一致 | 环境不通用 | 每个资源记驱动与算力版本作为调度硬约束;不跨代复用环境包 |
独立数据盘 与系统盘混用 | 大文件写爆 overlay | 资源能力里记磁盘余量,置备前检查;大文件默认 独立数据盘 |
| 采样 30 分钟粒度 | 无法支撑回收/抢占判定 | P0 起改 1 分钟;Prometheus textfile 暴露 |
| 断线重连误判 | 双跑或误杀 | fencing token + 进程启动时间校验 + 先读状态再恢复 |
| 清理遗漏 | 远端残留 | 清理回执强制、cleanup_pending 在看板常驻、TTL 兜底 |
18.3 已确认的决策与仍待落实的前置
已确认(2026-11-01):
- 抢占:开启。高优先级可抢占低优先级任务,按 §13.3 的条件与频控执行,被抢占任务从 checkpoint 恢复。
- 看板落点:放本机,局域网访问(
http://内网主机:48794/),不登记公网子域名。 - 自动创建 devbox:允许由系统自动创建,但当前权限未知——需要先做一次只读权限核对(见下)。
- 公用卡上限:必须可设上限,超限时由 agent 自我限流,平时贴着上限运行(§13.1、§13.2)。
仍待落实的前置(P2 之前必须解决):
- devbox 自动创建的权限与凭据:先做一次只读核对,确认现有凭据能否调用建机接口、能建在哪些集群/节点、有没有配额或审批限制;凭据本身的自动化取用方式(人工长期 token / 浏览器辅助刷新 / 只在页面提供一键入口)在核对后确定。核对结论会更新本节。
具体核对项:
| 核对项 | 方法 | 通过标准 |
|---|---|---|
| 当前凭据可用性 | 用现有 token 调只读接口(列表、节点占用) | 返回 200 且能列出目标集群 |
| 建机权限 | 建一台最小规格 devbox 后立即删除 | 创建成功、SSH 可达、删除干净 |
| 配额/上限 | 连续创建至被拒绝,记录拒绝原因 | 明确的数量或资源上限 |
| 集群/节点范围 | 对照返回的集群与节点清单 | 与台账已授权范围一致,无越权 |
在核对完成前,自动置备默认走"人工确认"模式:系统可以提议(选点、报价、所需规格),但真正建机需要看板上点一次确认。核对通过后再切换为全自动。
19. 附录
19.1 建议目录结构
compute-scheduler/
├─ compute_scheduler/
│ ├─ api.py # HTTP 路由、鉴权、幂等
│ ├─ store.py # SQLite 对象存储 + 事件
│ ├─ scheduler.py # 队列/配额/优先级/抢占
│ ├─ providers/ # static_ssh.py workflow_devbox.py pai_job.py 大显存资源池_ssh.py
│ ├─ bootstrap.py # 幂等部署七步
│ ├─ supervisor.py # 心跳/健康/重连/回收
│ ├─ metrics.py # 采样、rollup、Prometheus textfile
│ ├─ ledger.py # 只读台账镜像
│ └─ mcp.py # MCP 工具封装
├─ cli/csched.py
├─ dashboard/ # 静态看板(本地 vendored 图表库)
├─ deploy/systemd/*.service|*.timer
├─ tests/ # 单元 + 端到端演练脚本
└─ docs/design/*.md
19.2 CLI 示例
csched resources --gpu-min 40 --state ready
csched submit --name eval-001 --gpus 1 --gpu-mem 40 --hours 2 \
--image herbert/train:2026-10 --code git@gitlab.x2robot.com:herbert/x.git#abc123 \
--cmd "python -m train --config cfg.yaml" --checkpoint 独立数据盘/eval-001/ckpt
csched status eval-001 # 状态 + attempt + checkpoint + 日志尾
csched lease heartbeat <id>
csched cancel eval-001
csched agents # 哪些 agent 在线、各持有什么租约
csched metrics --window 24h
19.3 任务 spec 示例
{
"name": "eval-001",
"owner": "agent:<agent-id>",
"requirements": {"gpu_count": 1, "gpu_mem_mib": 40960, "cpu": 8,
"mem_mib": 65536, "hours": 2, "gang": false},
"platform_hint": ["大显存资源池", "devbox-snow5090"],
"priority": 0,
"code": {"repo": "git@gitlab.x2robot.com:herbert/x.git", "ref": "abc123"},
"env": {"reuse": "auto", "python": "3.12"},
"command": "python -m train --config cfg.yaml",
"checkpoint": {"dir": "独立数据盘/eval-001/ckpt", "interval_s": 300,
"resume_arg": "--resume {path}", "manifest": "latest.json"},
"ttl_hours": 4,
"cleanup": {"keep_results": true, "remove_code": true}
}
19.4 与现有技能的衔接
资源台账技能:继续负责台账权威性与远端 agent 安装/验收;新增一节"如何通过csched申请资源",避免 agent 直接 SSH 抢卡。汇报页技能:成果汇报页只引用结果;调度看板独立。remote-gpu-trainer:训练/评估类任务优先通过csched submit走租约,日志与 OOM/NaN 排查保留在该技能内。
(本稿为评审稿,P0/P1 的实现细节在确认 §18.3 后进入编码。)