评审稿仅设计方案,不含已上线功能。资源规模与内部主机信息已省略。

计算资源自动申请与调度系统 · 架构设计评审稿

版本:v1 评审稿 编写时间:2026-11-01

本文件是设计评审稿,不含已完成功能的宣称。第 3 节的现状事实均来自本机可核验的文件、接口与命令输出(表内注明来源与时间);其余章节为待实现的设计方案;第 17.3 节的三项需要你确认后才能定稿并进入编码。


1. 摘要(一页)

我们要做的是一个面向多个 agent 的计算资源控制面,把现在分散的三件事收拢成一条流水线:

  1. 资源从哪来:手工找机器 → 统一资源目录 + 可插拔的自动置备(已有静态池、devbox 动态申请、PAI 配额作业)。
  2. 任务怎么跑:口头协调"这张卡我用" → 队列 + 租约(lease)+ 抢占与配额,避免两个 agent 抢同一张卡。
  3. 机器坏了怎么办:断了重来 → 心跳、故障检测、重连、换机器续跑(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 热力图、队列与任务时间线、效率趋势、异常;数据带观测时间
G6agent 共享所有 agent(各类编码 agent 与远端 agent)通过同一 CLI/HTTP/MCP 接口查询资源、提交任务、看结果;共享黑板记录"谁在用什么、结论是什么"
G7合规与可追溯凭据只存位置不入库;远端任务代码放独占隐藏目录并按生命周期回收;每次租约、置备、清理都有审计事件与回执
G8公用卡占用上限人类可在看板设定池上限与保留额;agent 超限时自我限流,限额内贴着上限运行;高优先级可抢占低优先级并从 checkpoint 恢复

2.2 非目标(本期不做)


3. 现状盘点(事实与证据)

3.1 已有资产

资产位置事实来源
资源台账<本机配置路径>记录各计算资源的归属、授权范围、连接方式、硬件与历史核验结论,并区分“已核验 / 不可达 / 已停止”等状态台账读取,2026-11-01
台账写入器技能 scripts/registry.pyflock 串行化 + 深度合并 + 追加 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.pyapp-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 现状痛点(来自真实使用记录)

  1. 资源事实分散:台账(技能)、采样 JSONL、quota-manager、Prometheus 各说一部分,没有一个"当前可用 GPU 在哪"的查询入口。
  2. 协调靠人:并发任务时靠对话/口径约定,没有租约,出现"两个人同时认为某卡空闲"的风险。
  3. 置备靠手:devbox 创建、环境安装、代码同步、启动、断线重连都是逐步手工;devbox 创建还曾遇到 容器初始化超时 后实例 stopped(4 例历史记录)。
  4. 没有"谁在用"视图:只能看 GPU 利用率,看不出是哪个 agent、哪个任务、哪份代码。
  5. 重连语义缺失:通信断开后既不敢重发(怕双跑),也无法可靠续跑(缺 checkpoint 契约)。

4. 参考的成熟方案与取舍

4.1 对照表

能力成熟方案取什么不取什么(及原因)
队列/配额/抢占Slurm、Volcano、KueueKueue 的概念最贴合: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


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 与现有台账的关系


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适用现状依据需要的前置
1static-ssh大显存资源池 两端点、已有 devbox、PAI 节点台账 verified,SSH 可达已具备
2大显存资源池-ssh大显存(95.6 GiB)突发用户已授权全量使用已具备(注意 独立数据盘 与系统盘容量)
3workflow-devbox需要独立容器/隔离环境API 端点已确认token 自动化(§18.1)
4pai-job走 PAI 配额,配额内 72 卡已 100% 分配配额作业接口 只读已验证写权限与配额释放策略

选择策略:先复用、后新建(与技能"环境与资产复用"一致)。同一需求下按 成本(占用时长) → 排队时延 → 亲和性(已有环境/数据) → 碎片率 排序,尽量填满而不是留洞。

9.3 置备失败处理


10. 自动部署与重连

10.1 幂等部署(Bootstrapper)

单次部署 = 一组可重复执行的步骤,每步有 done 标记与版本哈希:

  1. 预检:hostname、驱动/CUDA、GPU 查询命令、磁盘(,独立数据盘 分开判断)、已有进程。
  2. 目录:任务独占隐藏根 <本机配置路径>(权限 700)或 <独立数据盘>/<task-id>/;登记所有者标记文件,避免误删他人目录。
  3. 代码/资产:优先复用已有共享资产(只读),只传差量;传完校验 SHA256 清单。
  4. 依赖:优先复用目标机已有环境;需要新建时用隔离 venv/conda env,不污染系统 Python。
  5. 启动:写 systemd user unit 或 supervisor 脚本,带 Restart=on-failure、RestartSec=30、StartLimitBurst=3/300s、日志路径、fencing token 环境变量。
  6. 健康检查:进程存活 + 端口/心跳文件 + 首个 GPU 批次的真实前向(不允许"进程起来就算成功")。
  7. 登记:attempts 写 deploying→running,事件记录版本、命令、路径、哈希。

10.2 通信与重连(沿用已验证的远端 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/P95jobs/events供需是否失衡
置备时延ready - provision_start P95attempts平台健康度
部署时延running - ready P95attempts环境/网络瓶颈
成功率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 时间序列与存储


12. 可视化界面

看板部署在本机(已确认):http://内网主机:48794/(沿用 40000–50000 端口约束;48792 目前由设计评审页占用,评审结束后释放),由本机 systemd user unit 提供静态页与 /api,不另建服务器。Grafana 保持 :3000 内部使用、按需反代到看板子路径。看板需要人类登录态,因此只发布到局域网,不登记公网子域名;评审页用过的公网子域名与本看板无关。

12.1 页面结构(5 个主区块,避免堆栏目)

  1. 总览:可用 GPU/总 GPU(按池)、运行/排队任务数、P95 排队、今日失败数、异常列表(每个数字带观测时间)。
  2. 资源:按机器分组的卡阵列热力图(util 颜色 + 显存条),点开看进程、驱动、健康检查结果、当前租约与 owner/agent。
  3. 任务:队列与运行中列表 + 甘特式时间线(排队→置备→部署→运行→结束),失败原因分类,点开看日志尾与 checkpoint。
  4. 效率:利用率和排队趋势(Grafana iframe 或本地图表)、Top 消耗(按 agent/owner)、闲置卡时。
  5. agent:哪些 agent 在线、用什么模型、当前持有哪些租约、最近提交/结论笔记;"被哪些 agent 使用"直接可查。

设计约束:

12.2 人类操作接口(看板内的控制面板)

看板不只是只读视图:公用卡必须有一个人能改的"闸门",否则 agent 会一直往上顶。因此看板提供一组只对人类开放的写操作:

操作作用生效方式
设置/调整池上限某个共享池最多允许我们占用多少张卡(cap_gpu)立即;下调时超出部分按优先级进入 draining
设置人类保留从池里预留多少张给人类,agent 不可用(reserved_gpu)立即;已在跑的租约到期后不再续
调整按主体上限某个人 / 某个 agent 的并发与总量上限立即,新提交按新上限裁决
调整优先级改队列或单个任务的优先级,影响抢占顺序下一次调度循环
暂停 / 恢复队列冻结新分配,不影响运行中任务立即
取消任务 / 驱逐结束指定任务(优雅停止 + 释放租约)立即,需二次确认
批准自动置备新建实例前的人工确认开关(可全局或按池)对后续置备请求生效
查审计谁在什么时候改了什么上限、驱逐了谁只读

权限与安全:

12.3 与工作汇报页的关系

运维看板 ≠ 成果汇报页。工作汇报页只放结果(沿用 汇报页技能 的内容边界);调度看板放运行态数据。两者可互相链接,但不混排。


13. 占用上限、抢占与自我限流

公用卡的核心矛盾:agent 越多、跑得越久,越容易把大家共用的池子占满,人反而排不进来。因此系统必须有一个硬的占用天花板,并且 agent 要自己认这个天花板——而不是等人来手工 kill。

13.1 三层上限

层级对象含义谁能改
L1 池上限一个共享池(如某个公用机器组)我们最多占用多少张卡;超出即不可再分配人类(看板)+ 可按池授权给指定 agent 代管
L2 主体上限某个人 / 某个 agent并发任务数、GPU 总量、GPU·小时/天人类;也可给 agent 一个自设上限的额度
L3 保留额池内预留给人类、agent 不得动用的卡数人类

约束关系:已占用 ≤ 池上限 − 人类保留额 且 该主体占用 ≤ 主体上限。两个条件同时满足才批准新租约。

上限是软实时的:上调立即生效;下调不打断正在跑的任务,而是把超额部分标记为 draining——不再续租,任务自然结束后回收,必要时按优先级驱逐(见 13.3)。

13.2 "贴着上限跑"的自我限流

这是本设计对 agent 行为的明确要求:在不超过上限的前提下尽量用满,而不是保守地只用一点点,也不能越过上限。

实现方式是把上限直接暴露给 agent,让它无需猜测:

配套的调度侧规则:

13.3 抢占(已确认开启)

高优先级任务可以抢占低优先级任务,但必须"可恢复、可解释、有上限":

优先级:critical(30) > high(20) > normal(10) > batch(0),数字大者优先;同级按提交时间 FIFO。优先级可由人类在看板调整,agent 只能在自己被授予的范围内设置(默认只能 normal 及以下,防止 agent 互相抬价)。

可抢占条件(全部满足才动手):

  1. 申请方优先级严格高于被抢方;
  2. 被抢任务 resumable=true(有 checkpoint 契约)或已接近完成且人类确认可弃;
  3. 抢占后能真正腾出所需的整卡/显存,而不是只挪走一半;
  4. 被抢任务未处于关键写入窗口(如 checkpoint 落盘中的保护期,默认前后 30 秒不抢)。

抢占流程:preempt_notice → 保存 checkpoint(最多等 grace_s,默认 120 秒)→ 优雅停止进程 → 释放租约 → 抢占方获租约 → 被抢任务回到队列头部(preempted 状态,保留原优先级与 epoch,恢复时 epoch+1、新 fencing token)。

配额内抢占:抢占用的是池上限内的额度,不突破 13.1 的天花板;若抢完之后总占用仍在上限内则允许,否则拒绝并给出原因。

频控与防抖:

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 侧必须遵守的规则(写进技能与工具描述)

  1. 提交前先查 quota_status,按 available 决定并发。
  2. 达到上限时排队或等待,不循环重试、不改名绕过、不直接 SSH 抢卡。
  3. 拿到租约后保持利用率(贴上限),长时间空转会被判定为闲置并回收。
  4. 被抢占时配合保存 checkpoint,不抗拒、不重启抢占。
  5. 人类保留额与上限由人设定,agent 不得自行上调(除非被显式代管授权)。

14. 多 agent 共享

14.1 共享内容

共享对象内容写入方读取方
资源事实台账镜像 + 当次健康台账(人/agent)所有 agent
租约与任务谁在用什么、跑到哪控制面所有 agent
结论与经验环境可用性、资产路径、失败模式agent 主动写 note所有 agent
事件流状态变化、审计控制面订阅者

14.2 身份与权限

14.3 与 DSH/其他体系的关系

本控制面是"资源与任务的共享事实源",不替代各 agent 自己的任务编排(例如 内部评测流水线)。集成方式:那些流水线把 ResourceQueue 指向控制面的租约接口,或直接用 csched submit 提交。


15. 安全、生命周期与合规

  1. 凭据:只存文件位置(0600),不入台账、不入库、不入仓、不进日志;registry.py 的凭据检测逻辑复用。
  2. 远端代码生命周期:任务代码只进独占隐藏根,任务结束取回证据→核验 SHA256→删除→留清理回执;中断保留待清理状态。
  3. 最小暴露:控制面默认监听 LAN,token 鉴权;不默认暴露公网;MCP 工具不带"删除他人资源"类能力。
  4. 审计:所有写操作 append-only 事件(谁、何时、对哪个对象、参数摘要)。
  5. 破坏性操作:destroy 实例、cancel 他人任务需显式 --yes 并记审计;控制面不自动 kill 非本系统启动的进程。
  6. 数据分级:看板展示资源与任务元数据,不展示任务内部数据内容;敏感数据只留路径与哈希。

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/cancel2~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 / 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):

  1. 抢占:开启。高优先级可抢占低优先级任务,按 §13.3 的条件与频控执行,被抢占任务从 checkpoint 恢复。
  2. 看板落点:放本机,局域网访问(http://内网主机:48794/),不登记公网子域名。
  3. 自动创建 devbox:允许由系统自动创建,但当前权限未知——需要先做一次只读权限核对(见下)。
  4. 公用卡上限:必须可设上限,超限时由 agent 自我限流,平时贴着上限运行(§13.1、§13.2)。

仍待落实的前置(P2 之前必须解决):

具体核对项:

核对项方法通过标准
当前凭据可用性用现有 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 与现有技能的衔接


(本稿为评审稿,P0/P1 的实现细节在确认 §18.3 后进入编码。)