GPUPlane 系统设计文档
版本:v0.1-draft · 2026-08-17 本文档是 GPUPlane 的工程落地设计。产品定位与功能边界见 02-product-design.md,选型依据见 01-product-research.md。
1. 设计目标与硬约束
Section titled “1. 设计目标与硬约束”1.1 目标
Section titled “1.1 目标”- 替代个人 GPU 工作流中的 SSH + tmux 训练管理方式;
- 不改变用户既有 PyTorch 工作流(
python train.py永远可独立运行); - 训练进程与平台故障隔离:平台任何组件失效不影响训练;
- 人(Web/CLI)与 AI Agent(MCP)共享同一控制面;
- 从单机单卡平滑扩展到少量异构节点,但不引入集群调度复杂度。
1.2 硬约束
Section titled “1.2 硬约束”| 约束 | 决策 |
|---|---|
| 基础设施最小化 | FastAPI + SQLite + Agent + React,无 Kafka/Redis/Prometheus |
| 单写者原则 | SQLite 只允许 Server 单进程写库;Agent 一律走网络上报 |
| 单 worker 约束 | uvicorn 单 worker 运行(内存态 pub/sub 与多 worker 不兼容);如需扩容再引入外部 pub/sub |
| 首要平台 Linux | macOS 支持开发调试;Windows 不考虑(NVML 进程级数据在 WDDM 下不可用) |
| Best-effort telemetry | SDK/Agent 所有上报失败只落本地缓冲,绝不向训练进程抛异常 |
2. 总体架构
Section titled “2. 总体架构”2.1 进程视图
Section titled “2.1 进程视图”┌─────────────────────────── 控制端(可与 GPU 主机同机) ───────────────────────────┐│ gpuctl-server (uvicorn 单 worker, FastAPI) ││ ├── REST API /api/v1/... ││ ├── Agent Gateway /api/v1/agent/ws (WebSocket, agent 主动外连) ││ ├── UI Stream /api/v1/jobs/{id}/logs/stream (SSE) + /ws (WS) ││ ├── Scheduler (asyncio task, 2s tick) ││ ├── Event Detector (规则引擎, 消费 metrics/logs) ││ ├── Rollup Worker (metrics 降采样与 retention) ││ └── SQLite (WAL) ~/.gpuctl/server/gpuctl.db ││ ││ gpuctl-mcp (独立进程, streamable HTTP / stdio) —— Core Service 的 Agent 适配器 ││ web/ (React SPA, 构建后由 gpuctl-server 静态托管) │└───────────────────────────────────┬─────────────────────────────────────────────┘ │ WebSocket 长连接 (agent → server, NAT 友好)┌───────────────────────────────────▼────────────────── GPU 主机 ──────────────────┐│ gpuctl-agent (asyncio 常驻进程, systemd user service) ││ ├── NodeMonitor NVML + psutil, 2s 采集 ││ ├── Runner ProcessRunner (一等公民) / DockerRunner (v0.2) ││ ├── LogCollector stdout/stderr → 本地文件 + WS 批量上报 ││ ├── TBAdapter tail TensorBoard event 文件 ││ ├── CheckpointWatcher 输出目录监视 (防抖 + 指纹) ││ └── SpoolBuffer 断线时 jsonl 本地缓冲, 重连回放 ││ ││ 用户训练进程 python train.py (+ 可选 gpuctl-sdk) │└──────────────────────────────────────────────────────────────────────────────────┘2.2 部署视图
Section titled “2.2 部署视图”- server 与 agent 可同机(个人最常见:一台 5090 工作站 = server + agent + web);
- 也可分离(server 在常开的 NAS/小主机,agent 在 GPU 机);agent 主动外连,GPU 主机无需入站端口;
- 远端访问走 Tailscale/内网 + Bearer Token,不暴露公网(见 §13 安全)。
3. 技术选型决策表
Section titled “3. 技术选型决策表”全部选型于 2026-08-17 经官方文档/PyPI/GitHub 验证,详细调研记录见 01 号文档 §5。
| 领域 | 决策 | 备选 | 关键理由与坑 |
|---|---|---|---|
| NVML 绑定 | nvidia-ml-py(导入名仍是 pynvml) |
DCGM(过重,否) | 旧 pynvml 包已弃用,必须卸载避免 FutureWarning;RTX 50 系随驱动支持(R570+);NVML init 用单例 |
| TB event 解析 | tensorboard.backend.event_processing.EventAccumulator 周期 Reload() |
tbparse(离线导入好用);自写 TFRecord 增量读(高频 tail 优化) | PyTorch 新版 scalar 写成 tensor summary,Scalars() 可能为空,必须读 Tensors();size_guidance={'scalars':0,'tensors':0} 关降采样;捕获 DataLossError(半写 record)下轮重试 |
| 数据库 | SQLite WAL + synchronous=NORMAL,raw/rollup 双表 |
DuckDB/Parquet(个人规模过度设计,仅做冷归档备选) | 批量 executemany + 单事务;索引 (name, ts);定期 ANALYZE + auto_vacuum=INCREMENTAL |
| UI 日志流 | SSE 为默认(EventSource 自带重连);需要交互(发信号/stdin)时 WS |
WebSocket | uvicorn 单 worker;生成器内检查 request.is_disconnected();15s 心跳注释行;nginx 前置需 X-Accel-Buffering: no |
| Agent↔Server | 单条 WebSocket 长连接,type 字段多路复用 |
HTTP 批量轮询(降级模式) | server 需主动下发 dispatch/cancel,故 WS;应用层心跳 20-30s(防 LB idle 断连);websockets 库锁定大版本(15.x asyncio API 有 breaking change) |
| 断线缓冲 | agent 端 append-only jsonl spool + (source, seq) 幂等回放 |
— | 轮转上限(100MB/24h);回放限流去重;O_APPEND 单行原子写 |
| 进程执行 | asyncio.create_subprocess_exec(..., start_new_session=True) + os.killpg 整组杀 |
pexpect(交互场景) | 只杀单 PID 会留孤儿(dataloader worker);SIGTERM→5s→SIGKILL→await proc.wait() 收割;env 注入 PYTHONUNBUFFERED=1 避免块缓冲;PTY 仅在需要 TTY 行为时开 |
| CLI | typer + uv tool install 分发 |
click / argparse | 重依赖(torch)不得顶层 import,子命令内延迟导入保证启动速度 |
| MCP | fastmcp 3.4.x 钉版(FastMCP.from_fastapi 或直接定义 tools) |
官方 mcp 2.0 MCPServer(依赖最少) |
mcp 1.x→2.0 硬 breaking,fastmcp 3.x 依赖 mcp 1.x,严禁同 venv 混装 fastmcp3 + mcp2;远程传输用 streamable HTTP(SSE 传输已废弃) |
| 前端图表 | uPlot(高频实时曲线,10 万点 60fps)+ ECharts 6(概览/对比,echarts/core 按需导入) |
Recharts(>5k 点不可用,否) | 实时曲线必须窗口化 + rAF 节流批量 setData;React 19 StrictMode 注意双挂载清理 |
| WS 重连(前端) | partysocket(指数退避) |
react-use-websocket / 原生 EventSource(SSE 场景) | — |
| 部署 | systemd user service ×2(server/agent)+ loginctl enable-linger |
system service | 不 linger 是“重启后服务没起来”的最常见原因;unit 内显式声明 PATH/CUDA 环境(user service 不继承 shell rc);uv sync --frozen --no-dev 可复现部署 |
| ID 生成 | ULID,带类型前缀(job_、run_、ckpt_、nd_、ev_) |
UUID | 可读、可排序、URL 安全 |
| ORM/校验 | SQLAlchemy 2.x(async)+ Pydantic v2(协议模型共享包) | SQLModel | 协议模型在 server/agent/sdk/cli/mcp 间共享,放 gpuctl-common |
4. 数据模型
Section titled “4. 数据模型”4.1 实体关系
Section titled “4.1 实体关系”Project ──< Experiment ──< Run ──< Checkpoint ──< Evaluation │ ▲ │ │ (parent_checkpoint 自关联, resume 链) ├─< MetricPoint (raw/rollup) ├─< Event └─< RunLogRefJob ──< (1..N attempt) ──< Run Node ──< GPUSlot ── owner_job ──> JobNode ──< NodeMetric (raw/rollup)MetricDefinition (指标语义元数据)Job 与 Run 的关系(对原方案的细化):
- Job 是调度单位,Run 是观测单位;
runs.job_id + runs.attempt关联; - Job 每次 dispatch(含 retry)创建新 Run(同 job_id,attempt 递增)——重试的 metrics 不混淆;
- Run 也可以没有 Job:
source = sdk | import(训练在平台外启动后自注册,或import-run离线导入); - Job 也可以不产生 Run:
type = CUSTOM的纯脚本任务只记录日志与退出码。
4.2 DDL(SQLite)
Section titled “4.2 DDL(SQLite)”PRAGMA journal_mode = WAL;PRAGMA synchronous = NORMAL;
CREATE TABLE projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT, created_at INTEGER NOT NULL -- ms epoch);
CREATE TABLE experiments ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), name TEXT NOT NULL, description TEXT, created_at INTEGER NOT NULL, UNIQUE (project_id, name));
CREATE TABLE nodes ( id TEXT PRIMARY KEY, -- nd_... name TEXT NOT NULL UNIQUE, status TEXT NOT NULL, -- ONLINE/OFFLINE/UNHEALTHY (BUSY/IDLE 由 slot 推导) agent_version TEXT, labels TEXT, -- JSON ["5090","linux"] hardware TEXT, -- JSON {gpus:[...], cpu, mem_mb, disk_mb, cuda, driver} last_heartbeat_at INTEGER, created_at INTEGER NOT NULL);
-- 每张物理 GPU 一个调度槽;独占调度的判定依据就是 owner_job_idCREATE TABLE gpu_slots ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES nodes(id), gpu_index INTEGER NOT NULL, model TEXT, vram_mb INTEGER, owner_job_id TEXT REFERENCES jobs(id), -- NULL = free UNIQUE (node_id, gpu_index));
CREATE TABLE jobs ( id TEXT PRIMARY KEY, -- job_... name TEXT NOT NULL, type TEXT NOT NULL, -- TRAIN/EVALUATE/INFERENCE/EXPORT/CUSTOM status TEXT NOT NULL, -- CREATED/QUEUED/DISPATCHING/RUNNING/ -- SUCCEEDED/FAILED/CANCELLED/LOST priority INTEGER NOT NULL DEFAULT 0, runner TEXT NOT NULL DEFAULT 'process', -- process/docker working_dir TEXT, command TEXT NOT NULL, env TEXT, -- JSON resources TEXT NOT NULL DEFAULT '{"gpu":1}', -- JSON {gpu:N, cpu_only:false} node_selector TEXT, -- JSON {labels:[...], node:"..."} attempt INTEGER NOT NULL DEFAULT 0, max_attempts INTEGER NOT NULL DEFAULT 1, -- retry: max_attempts>1 project_id TEXT REFERENCES projects(id), experiment_id TEXT REFERENCES experiments(id), node_id TEXT REFERENCES nodes(id), created_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER, exit_code INTEGER, failure_reason TEXT, -- OOM/SIGNAL/EXIT_CODE/TIMEOUT/LOST... cancel_requested INTEGER NOT NULL DEFAULT 0);CREATE INDEX idx_jobs_queue ON jobs(status, priority DESC, created_at);
CREATE TABLE runs ( id TEXT PRIMARY KEY, -- run_... job_id TEXT REFERENCES jobs(id), attempt INTEGER, -- 与 jobs.attempt 对应 node_id TEXT, project_id TEXT REFERENCES projects(id), experiment_id TEXT REFERENCES experiments(id), name TEXT, source TEXT NOT NULL, -- job/sdk/import status TEXT NOT NULL, -- RUNNING/SUCCEEDED/FAILED/CANCELLED/LOST/IMPORTED config TEXT, -- JSON: 超参/配置 env_snapshot TEXT, -- JSON: {git_commit, git_dirty, python, argv, cuda, key_pkgs} created_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER, summary TEXT -- JSON: RunSummary 缓存, 增量更新);CREATE INDEX idxruns_exp ON runs(experiment_id, created_at DESC);
CREATE TABLE metric_definitions ( name TEXT PRIMARY KEY, -- "eval/loss", "ndcg@10" display_name TEXT, direction TEXT, -- minimize/maximize scope TEXT, -- train/eval/system is_primary INTEGER NOT NULL DEFAULT 0, unit TEXT, created_at INTEGER NOT NULL);
-- 指标方向/单位是全局元数据;primary 选择按 ownership hierarchy 覆盖。-- 解析顺序 experiment → project → global metric_definitions.is_primary。CREATE TABLE project_primary_metrics ( project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE, metric_name TEXT NOT NULL REFERENCES metric_definitions(name) ON DELETE CASCADE, created_at INTEGER NOT NULL);CREATE TABLE experiment_primary_metrics ( experiment_id TEXT PRIMARY KEY REFERENCES experiments(id) ON DELETE CASCADE, metric_name TEXT NOT NULL REFERENCES metric_definitions(name) ON DELETE CASCADE, created_at INTEGER NOT NULL);
-- 训练/评估指标:双表法 (raw + rollup)CREATE TABLE metrics_raw ( run_id TEXT NOT NULL REFERENCES runs(id), ts INTEGER NOT NULL, step INTEGER, name TEXT NOT NULL, value REAL NOT NULL);CREATE INDEX idx_metrics_raw ON metrics_raw(run_id, name, ts);
CREATE TABLE metrics_rollup ( run_id TEXT NOT NULL, name TEXT NOT NULL, granularity TEXT NOT NULL, -- 1m / 5m bucket_ts INTEGER NOT NULL, min REAL, max REAL, avg REAL, last REAL, cnt INTEGER, PRIMARY KEY (run_id, name, granularity, bucket_ts));
-- 系统指标按节点存储, 与 run 解耦 (run 通过 node_id+时间窗关联)CREATE TABLE node_metrics_raw ( node_id TEXT NOT NULL, ts INTEGER NOT NULL, name TEXT NOT NULL, -- gpu0.utilization/gpu0.mem_used_mb/gpu0.temp_c/ -- gpu0.power_w/cpu.utilization/mem.used_mb/disk.used_mb value REAL NOT NULL);CREATE INDEX idx_node_metrics ON node_metrics_raw(node_id, name, ts);-- node_metrics_rollup 结构同 metrics_rollup, 省略
CREATE TABLE events ( id TEXT PRIMARY KEY, -- ev_... ts INTEGER NOT NULL, type TEXT NOT NULL, -- RUN_STARTED/RUN_FINISHED/CHECKPOINT_CREATED/ -- LOSS_NAN/LOSS_SPIKE/OOM/GPU_UNDERUTILIZED/ -- PROCESS_EXITED/AGENT_DISCONNECTED/DISK_LOW/... severity TEXT NOT NULL, -- info/warning/critical run_id TEXT, job_id TEXT, node_id TEXT, step INTEGER, message TEXT, context TEXT -- JSON);CREATE INDEX idx_events_run ON events(run_id, ts);CREATE INDEX idx_events_type ON events(type, ts);
CREATE TABLE checkpoints ( id TEXT PRIMARY KEY, -- ckpt_... run_id TEXT NOT NULL REFERENCES runs(id), step INTEGER, epoch REAL, path TEXT NOT NULL, -- agent 端绝对路径 size_bytes INTEGER, fingerprint TEXT, -- sha1(size + mtime + 头尾各1MB) 快速指纹; 非全量 hash parent_id TEXT REFERENCES checkpoints(id), metadata TEXT, -- JSON created_at INTEGER NOT NULL, UNIQUE (run_id, path));
CREATE TABLE evaluations ( id TEXT PRIMARY KEY, checkpoint_id TEXT NOT NULL REFERENCES checkpoints(id), job_id TEXT REFERENCES jobs(id), -- 由哪个 EVALUATE job 产生 run_id TEXT REFERENCES runs(id), suite TEXT, -- "beir" dataset TEXT, -- "beir/nfcorpus" dataset_version TEXT, metrics TEXT NOT NULL, -- JSON {"ndcg@10":0.47, "mrr":0.51} status TEXT NOT NULL, -- RUNNING/SUCCEEDED/FAILED runtime_s REAL, created_at INTEGER NOT NULL);
CREATE TABLE artifacts ( -- v0.2: 非 checkpoint 产物 (图表/样例/导出模型) id TEXT PRIMARY KEY, run_id TEXT NOT NULL, kind TEXT, path TEXT NOT NULL, metadata TEXT, created_at INTEGER NOT NULL);
CREATE TABLE webhook_subscriptions ( -- v0.2: 事件外推 (ntfy/Bark/自定义) id TEXT PRIMARY KEY, url TEXT NOT NULL, event_types TEXT NOT NULL, -- JSON 过滤 secret TEXT, enabled INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL);4.3 Retention 与降采样
Section titled “4.3 Retention 与降采样”| 数据 | 粒度与保留 |
|---|---|
node_metrics_raw |
采集 2s → agent 聚合上报 5s 一批;raw 保留 24h |
node_metrics_rollup |
1m 保留 7d;5m 保留 30d;更早 DELETE |
metrics_raw(训练指标) |
频率低(step 级),长期保留;>90d 的 run 可降采样到 1m rollup 后清 raw |
events |
长期保留(量小、语义价值高) |
| 日志文件 | ~/.gpuctl/jobs/<job-id>/ 本地保留;server 只存尾部 N 行 + 元数据,全文留在 agent 端(见 §7.3) |
Rollup Worker:每 60s 执行一次 INSERT INTO ... SELECT ... GROUP BY + DELETE 过期 raw(Zabbix history/trends 同款模式),单事务执行。
5. Agent ↔ Server 通信协议
Section titled “5. Agent ↔ Server 通信协议”5.1 通道
Section titled “5.1 通道”单条 WebSocket:wss://<server>/api/v1/agent/ws,凭据仅通过 Authorization: Bearer <agent_token> 请求头发送,避免 URL/访问日志泄漏。agent 作 client 主动外连(穿透 NAT)。所有帧为 JSON,含 type 字段多路复用。应用层心跳 25s(防中间设备 idle 断连)。
5.2 消息类型
Section titled “5.2 消息类型”Agent → Server:
| type | 载荷要点 | 说明 |
|---|---|---|
hello |
{node_name, agent_version, hardware, labels} |
连接建立后首帧(认证在 URL token);server 回复 hello_ok 分配 node_id |
heartbeat |
{ts, slots:[{gpu_index, owner_job_id}], disk, load} |
25s;server 据此维护 nodes.status 与 LOST 判定 |
sys_metrics |
{seq, points:[{name, ts, value}]} |
5s 一批 |
run_metrics |
{run_id, seq, points:[{name, ts, step, value}]} |
SDK/TB adapter 汇出的训练指标 |
log_batch |
{job_id, stream, seq, lines:[...]} |
200 行或 1s 刷一批 |
event |
{type, severity, run_id?, step?, context} |
agent 本地检测到的事件(如 CHECKPOINT_CREATED) |
job_status |
{job_id, attempt, status, exit_code?, failure_reason?} |
状态变迁回执(RUNNING/SUCCEEDED/FAILED/…) |
checkpoint_seen |
{run_id, path, step?, size, fingerprint} |
watcher 发现新 checkpoint |
reconcile |
{running:[{job_id, attempt, pid}], exited:[...]} |
重连后上报本地真实进程状态 |
Server → Agent:
| type | 说明 |
|---|---|
hello_ok / hello_err |
注册结果 |
dispatch |
{job: {...完整 JobSpec, attempt}} |
cancel |
{job_id} → agent 对进程组 SIGTERM→SIGKILL |
ack |
{type, seq} 可选回执(日志/指标批量确认) |
ping |
保活 |
5.3 幂等与断线重放
Section titled “5.3 幂等与断线重放”- 每类上报流各自维护单调
seq;spool 落盘格式:{"stream":"log","job_id":...,"seq":...,"payload":...}一行一条; - server 按
(stream, source_id, seq)去重落库(metrics/logs天然可重复写时以 seq 覆盖); - 断线期间 agent 继续采集写 spool(上限 100MB/24h 轮转);重连后限速回放(防积压打爆 server);
- 心跳超时:3 个心跳周期(~90s)无心跳 → node 标记 OFFLINE,其 RUNNING job 标记 LOST 并产生
AGENT_DISCONNECTED事件;agent 重连后reconcile对齐真实状态(进程还在 → 恢复 RUNNING;不在 → FAILED/lost-confirmed)。
6. Job 生命周期与调度器
Section titled “6. Job 生命周期与调度器”6.1 状态机
Section titled “6.1 状态机”CREATED → QUEUED → DISPATCHING → RUNNING → SUCCEEDED │ ├────→ FAILED ──(retry, attempt+1)──→ QUEUED │ ├────→ CANCELLED │ └────→ LOST ──(reconcile)──→ RUNNING | FAILED └─(dispatch ack 超时)──→ QUEUED (重新调度, 上限 3 次)LOST是核心状态:server 认为 RUNNING 但 agent 失联;- 用户 cancel:QUEUED 直接 CANCELLED;RUNNING 置
cancel_requested,经 WScancel下发; failure_reason由 agent 初步判定(exit code / signal / 日志 OOM 模式)+ server EventDetector 补充。
6.2 调度器(asyncio task,2s tick)
Section titled “6.2 调度器(asyncio task,2s tick)”# 伪代码 —— 刻意保持 SQL 级简单job = SELECT * FROM jobs WHERE status='QUEUED' ORDER BY priority DESC, created_at ASC LIMIT 1if not job: returnslots = 该 job.node_selector 匹配节点上的 free gpu_slotsif len(slots) < job.resources.gpu: return # 无资源, 等下一 ticktx: job.status = DISPATCHING; job.node_id = ...; job.attempt += 1 slots[i].owner_job_id = job.id run = INSERT INTO runs(job_id, attempt, source='job', status='RUNNING' ...)ws.send(node, dispatch(job, run_id, attempt))关键决策(对原方案的修订):
- 独占单位是 GPU Slot 而非节点:多卡机按
gpu_slots逐卡分配,dispatch 时注入CUDA_VISIBLE_DEVICES=<分配的 gpu_index>; - CPU-only job 不占 GPU slot:
resources.gpu=0的 EVALUATE/EXPORT 可与训练并发,每节点设max_cpu_jobs(默认 2)防失控; - 不用 GPU Utilization 判断空闲(原方案正确,保留):util=0 可能是 DataLoader/Eval 阶段;
- 不抢占:个人场景排队即可,抢占带来的 checkpoint/resume 复杂度不值。
7. GPU Agent 设计
Section titled “7. GPU Agent 设计”7.1 模块与采集循环
Section titled “7.1 模块与采集循环”gpuctl-agent├── Config ~/.gpuctl/agent.yaml {server_url, token, node_name, labels, watch_dirs}├── WSClient 连接管理/重连(指数退避)/spool 回放├── NodeMonitor 2s: NVML(单例 init) + psutil → 5s 批量上报├── RunnerManager job_id → Runner 实例; 并发上限: gpu slot 数 + max_cpu_jobs│ ├── ProcessRunner asyncio subprocess, start_new_session, killpg, exit code│ └── DockerRunner v0.2, docker SDK, 同等日志/退出码语义├── LogCollector 管道 → tee: 本地文件 + WS 批量; drop-oldest 有界队列(1000 行)├── TBAdapter watchfiles 监听 run 的 runs/ 目录 → EventAccumulator 增量 Reload├── CkptWatcher watch_dirs 防抖扫描 → 指纹 → checkpoint_seen└── EnvSniffer dispatch 时采集: git rev-parse HEAD / dirty / python -V / pip freeze(关键包)本地目录约定:
~/.gpuctl/├── agent.yaml├── jobs/<job-id>/attempt-<n>/{stdout.log, stderr.log, runtime.json, events.jsonl}├── spool/{metrics,logs,events}.jsonl # 断线缓冲, 100MB 轮转└── cache/tb_offsets.json # TB 增量读取 offset7.2 ProcessRunner 细节(一等公民)
Section titled “7.2 ProcessRunner 细节(一等公民)”asyncio.create_subprocess_exec(shlex.split? → 直接 shell=False 列表, start_new_session=True, cwd=working_dir, env={...os.environ, **job.env, PYTHONUNBUFFERED:'1', CUDA_VISIBLE_DEVICES:...})- 整组杀:
os.killpg(os.getpgid(pid), SIGTERM)→ 5s →SIGKILL→await proc.wait();禁止只proc.kill()(dataloader worker 会变孤儿); - 非 TTY 块缓冲问题:默认注入
PYTHONUNBUFFERED=1;用户命令包stdbuf -oL或 PTY 模式(pty: true时 stdout/stderr 合并,记录stream: combined); - exit code →
job_status;signal 退出映射failure_reason=SIGNAL(<sig>); runtime.json记录 pid/pgid/start/end/cmd/env,供 reconcile 与人工排查。
7.3 日志策略(修订点)
Section titled “7.3 日志策略(修订点)”原方案“实时发送至 Control Server + WebSocket 实时查看”修订为:
- 全文日志只存 agent 本地文件(训练日志可上 GB,全量进 SQLite 是自找麻烦);
- WS 实时上报 = server 内存态 pub/sub 转发给 UI SSE + **server 只持久化尾部 ring buffer(默认 2000 行)**进
jobs.log_tail; gpuctl logs <job>/ MCPtail_logs默认读 server tail;--follow走实时流;--full时 server 向 agent 拉取本地文件(agent 在线才可用,离线则明确提示);- UI 日志用 SSE(单向、自带重连),WS 仅用于交互式操作。
7.4 TensorBoard Adapter(Level 1 兼容层)
Section titled “7.4 TensorBoard Adapter(Level 1 兼容层)”- run 启动时 agent 在
working_dir下按约定发现 event 目录(runs/,logs/,outputs/,TB_LOG_DIRenv),也允许 job yaml 显式tb_dir; - 增量解析:
EventAccumulator(path, size_guidance={'scalars':0,'tensors':0})+ 周期Reload();捕获DataLossError(半写 CRC)下轮重试; - 必读
Tensors()(PyTorch 新 SummaryWriter 把 scalar 写成 tensor summary),同时兼容Scalars(); - 大文件优化:
cache/tb_offsets.json记录文件 offset,自行按 record 边界增量读,避免全量 CRC; - step 回退(重启训练):accumulator purge 语义下,以
(step, ts)上报、server 端允许同 step 多值(保留,不覆盖); - tag → metric name 直通(
train/loss等约定自然成立);首次见到的 name 自动建metric_definitions(direction 按名称启发式:loss/error→minimize,acc/f1/ndcg/mrr→maximize,可人工改)。
7.5 Checkpoint Watcher
Section titled “7.5 Checkpoint Watcher”- 监视
watch_dirs(默认./checkpoints ./outputs ./runs)+ run config 中声明的目录; - 防抖:文件 mtime 稳定 10s 才上报(大 checkpoint 写入慢,避免注册半成品);识别
.tmp/.partial后缀跳过; - 指纹而非全量 hash(修订点):
sha1(size + mtime + 头 1MB + 尾 1MB)——GB 级文件全量 hash 太贵,指纹足以判断“是否同一文件”;需要严格校验时提供gpuctl ckpt verify <id>手动全量; - 目录形态识别:HF
checkpoint-*/(读trainer_state.json提取 step/epoch)、单文件*.pt/*.safetensors、DeepSpeed 分片目录; - 只发现与注册,绝不移动/改写模型文件。
7.6 SDK 接收(Level 2)
Section titled “7.6 SDK 接收(Level 2)”训练进程内 gpuctl-sdk 直接上报(不经 agent 管道):
- 优先 HTTP
POST /api/v1/runs/{run_id}/metrics:batch(带 sdk token);server 不可达时写本地~/.gpuctl/spool/sdk-<run_id>.jsonl,由 agent 下次连接时代为回放(若同机有 agent)或 SDK 后台线程自行重试; - SDK 内部:
log()→ 有界内存队列(满则 drop-oldest + 计数)→ 后台 sender 线程批量发送;atexitflush 一次(超时 2s)。
8. Training SDK 设计(gpuctl-sdk)
Section titled “8. Training SDK 设计(gpuctl-sdk)”from gpuctl import run # 单例, framework-neutral
run.init(project="qwen-sft", experiment="lr-2e5", config={...}) # 可选; 无 init 时 log 也工作(lazy run)run.log({"train/loss": loss.item(), "lr": lr}, step=step) # 绝不抛异常run.log_event("phase", {"name": "epoch-2-start"})run.log_checkpoint(path, step=20000) # register, 不 save; torch.save 仍是用户自己的事run.summary() # 本地只读视图- 零配置发现:在平台 dispatch 的 job 里,env 注入
GPUCTL_SERVER/RUN_ID/TOKEN→ SDK 自动工作;平台外裸跑时run.init()自注册 run(source=sdk),或完全静默降级为本地 jsonl; - 依赖最小化:仅
httpx(硬依赖pydantic都不要,避免与训练环境撞版本); - 显式不做 framework callback 的强绑定(Lightning/HF Trainer callback 作为
gpuctl-sdk[integrations]可选 extras 薄封装,v0.2)。
9. Metrics 管线
Section titled “9. Metrics 管线”SDK ──HTTP batch──┐TBAdapter ──WS────┼──► Server MetricService ──► metrics_raw (批量 executemany, 单事务)NodeMonitor ──WS──┘ │ ├─► EventDetector (流式规则判定, §10) ├─► RunSummary 增量更新 (runs.summary JSON 缓存) └─► Rollup Worker (60s tick, 降采样+retention)- 写入收口在 server 单进程(SQLite 单写者);
- 查询接口按时间窗自动选择 raw 或 rollup(
?window=1h→ raw;7d→ 1m rollup),对调用方透明; metric_definitions是 Agent 理解指标的关键:direction驱动get_best_checkpoint/compare的排序逻辑;primary 使用 experiment → project → global 继承。跨实验比较只使用共同 project 选择,跨 project 比较只使用 global 默认或显式metric,避免用不同指标静默排名。
10. Event 与 Diagnosis 语义层
Section titled “10. Event 与 Diagnosis 语义层”10.1 EventDetector(server 端规则引擎,v0.2 完整 / v0.1 含最小集)
Section titled “10.1 EventDetector(server 端规则引擎,v0.2 完整 / v0.1 含最小集)”| 规则 | 触发条件(默认阈值可配) | severity |
|---|---|---|
LOSS_NAN |
loss is NaN/Inf | critical |
LOSS_SPIKE |
当前值 > 滚动中位数 + 6×MAD | warning |
OVERFITTING_SUSPECTED |
eval/loss 距最低点回升 >10% 且 train/loss 仍下降,持续 3 次 eval | warning |
GPU_UNDERUTILIZED |
RUNNING 中 gpu util <20% 且显存 <30%,持续 10min | warning |
OOM |
日志匹配 CUDA out of memory / exit code 配合显存曲线 |
critical |
DISK_LOW |
输出盘剩余 <5% | critical |
AGENT_DISCONNECTED / JOB_LOST |
心跳超时 | critical |
RUN_STARTED/FINISHED、CHECKPOINT_CREATED、EVALUATION_FINISHED |
生命周期 | info |
10.2 Run Summary(runs.summary 增量缓存)
Section titled “10.2 Run Summary(runs.summary 增量缓存)”结构即原方案 §17 的 JSON:status / progress(step,max_steps,epoch) / latest / trend(滚动窗口斜率判定 increasing/decreasing/stable) / best(按 metric_definitions.direction 计算) / health.warnings。Agent 与 UI 首屏都只读 summary,需要细节再 query_metrics。
10.3 语义工具的实现定位
Section titled “10.3 语义工具的实现定位”diagnose_run / compare_runs / compare_checkpoints / explain_failure / get_best_checkpoint 全部实现为 Core Service 的普通 Python 函数(规则 + 统计,非 LLM),REST 与 MCP 只是暴露层。LLM 推理留给调用方 Agent——平台负责把 10 万 metric points 压缩成它可直接消费的语义结论。这保证:结果可复现、无 LLM 成本、离线可用。
11. API 设计
Section titled “11. API 设计”11.1 REST(/api/v1,Bearer Token)
Section titled “11.1 REST(/api/v1,Bearer Token)”| 资源 | 端点 |
|---|---|
| Nodes | GET /nodes GET /nodes/{id}(含 slots 与最新系统指标快照) |
| Jobs | POST /jobs GET /jobs?status= GET /jobs/{id} POST /jobs/{id}/cancel POST /jobs/{id}/retry |
| Logs | GET /jobs/{id}/logs?stream=&tail= GET /jobs/{id}/logs/stream(SSE) |
| Projects/Experiments | 标准 CRUD |
| Runs | GET /runs GET /runs/{id} GET /runs/{id}/summary GET /runs/{id}/metrics?names=&window=&step_range= |
| Metrics | POST /runs/{id}/metrics:batch(SDK 用) POST /runs/metrics:query(v0.4 批量查询,叠加图一次取数) GET /runs/{id}/metrics/stream(v0.4 SSE 实时推点,PubSub fan-out) GET/PUT/PATCH /metric-definitions PUT /primary-metric(global/project/experiment) |
| Events | GET /events?run_id=&type=&severity= GET /events/stream(SSE,供 UI/Agent 长轮询替代) |
| Checkpoints | GET /runs/{id}/checkpoints GET /checkpoints/{id} |
| Evaluations | POST /checkpoints/{id}/evaluations(创建 EVALUATE job) GET /checkpoints/{id}/evaluations |
| Semantics | GET /runs/{id}/diagnosis GET /runs/compare?ids= GET /checkpoints/compare?ids= GET /experiments/{id}/best-checkpoint?metric= |
| Agent | WS /agent/ws |
| Webhooks (v0.2) | POST/DELETE /webhooks |
约定:列表默认按 created_at DESC 分页(cursor);错误统一 {"error":{"code","message","details"}};所有写端点幂等(客户端带 Idempotency-Key 或自然键唯一约束)。
11.2 MCP Server(gpuctl-mcp)
Section titled “11.2 MCP Server(gpuctl-mcp)”协议基线:MCP 2026-07-28 规范——无状态核心(无 initialize 握手 / Mcp-Session-Id)、Tasks 扩展转正(长时操作标准模式)、旧 HTTP+SSE 传输废弃、Roots/Sampling/Logging 进入废弃期。调研细节见 01 号文档 §3.1。
- 实现:fastmcp 3.4.x 钉版;tool 函数薄封装(MCP 进程经 REST 调用 server,保持纯适配器);
- 传输:仅 streamable HTTP(
/mcp,Bearer token);本机客户端用gpuctl mcp-stdio桥。不做已废弃的 SSE 传输; - 长时任务建模(关键设计):
submit_job/evaluate_checkpoint/cancel_job按 Tasks 扩展建模——调用立即返回 task handle(taskId/ttlMs/pollIntervalMs,内含 job_id),进度经notifications/progress(step/epoch/loss 摘要),状态机对齐working / input_required / completed / failed / cancelled;绝不把一次数小时的训练伪装成同步 tool 调用。轮询为主、推送为辅:get_job/get_run_summary永远可拉取关键状态(客户端渲染参差,VS Code 不显示 tasks/status 的 statusMessage),tasks/get返回建议轮询间隔(GPU 作业 5–30s)。fastmcp 3.4 基于 SDK v1,Tasks 语义先在应用层对齐(submit 即返回 handle +get_job查询),SDK v2 / fastmcp 4 GA 后平移到协议原生 tasks——适配层内部演进,tool 语义不变; - 危险操作确认:
cancel_job等破坏性操作支持input_required+tasks/update(MRTR)二次确认流程,而非另造 approval 工具; - 实现三个必需 headers(
MCP-Protocol-Version、Mcp-Method、Mcp-Name)与 per-request_meta能力声明;MCP server 无状态(状态全落 SQLite),发布时声明io.modelcontextprotocol/taskscapability; - Tools v0.3(read 默认开放,write 需 token 具备 write scope):
| 类别 | tools |
|---|---|
| 观测(read) | list_nodes get_node_status list_jobs get_job get_run_summary query_metrics tail_logs list_events list_checkpoints list_evaluations |
| 控制(write) | submit_job cancel_job retry_job evaluate_checkpoint |
| 语义(read) | diagnose_run compare_runs compare_checkpoints get_best_checkpoint explain_failure |
- 返回一律结构化 JSON(summary 优先、raw 可选展开),附
next_actions提示字段(如 diagnose 返回{"overfitting": {...}, "next_actions": ["compare_checkpoints(run_id)", "get_best_checkpoint(metric='eval/loss')"]})——引导 Agent 走正确流程而无需轮询。
11.3 事件唤醒机制(修订点:补上 Agent “按事件介入” 的落地通道)
Section titled “11.3 事件唤醒机制(修订点:补上 Agent “按事件介入” 的落地通道)”原方案“平台持续监控,Agent 按事件介入”缺少最后一步——Agent 如何被唤醒。MCP 2026-07-28 的 subscriptions/listen 订阅流提供了协议内通道,但多数 agent harness 的 MCP 客户端并不会主动消费订阅通知,因此设计三通道兜底:
- Webhook 外推(v0.2):事件 → POST 到 ntfy/Bark/自定义 URL(手机推送给人);
- Agent harness hook:提供
gpuctl event-hook --types OOM,RUN_FINISHED -- claude -p "..."包装命令,事件到达时拉起/唤醒本地 agent 会话; - 被动拉取:Agent 会话内
list_events(severity=critical)+get_run_summary低成本轮询(分钟级,非 30s 级);MCP 通道可用时优先用 task progress 通知替代轮询。
12. Web UI(React + Vite + Tailwind + uPlot/ECharts)
Section titled “12. Web UI(React + Vite + Tailwind + uPlot/ECharts)”v0.4 实况:页面扩为七个——Dashboard / Runs / Run Detail / Experiments / Compare / Jobs / Nodes / Events。 图表统一走自研
ChartPanel(EMA 平滑 + crosshair 精确读数 + 拖拽缩放 + LTTB 降采样 + run 稳定配色); 实时性 =POST /runs/metrics:query批量取数 +/runs/{id}/metrics/streamSSE 逐点推流 + 5s 水位轮询兜底, 未缩放时曲线自动跟随新数据。Experiments 重设计为 W&B 式分析工作区(多指标面板 + ★ primary 置顶 + URL 深链视图状态)。ECharts 最终未引入(uPlot 足够),下文保留最初的四页设计意图备查。
四个核心页面(与原方案一致),补充实现要点:
- Dashboard:节点卡片(GPU util/显存/温度/功耗,2s 刷新,uPlot sparkline)、当前 job 进度条、队列前 5;
- Jobs:状态分tab列表、提交对话框(yaml/表单两模式)、cancel/retry 操作;
- Run Detail(最重要):Overview(summary JSON 渲染)/ Metrics(uPlot 实时曲线,窗口化 + LTTB 降采样,多 run 叠加对比)/ System Metrics / Events 时间线 / Logs(SSE tail,虚拟滚动)/ Checkpoints + Evaluations 表(按 primary metric 高亮最优);
- Nodes:硬件信息、slot 占用、心跳时间、agent 版本。
工程要点:WS 消息进 ref 缓冲 + rAF 节流批量 setData(防高频 setState 打爆 React);ECharts 按需模块化 import;partysocket 管理重连;React 19 StrictMode 下妥善处理 effect cleanup(双挂载不双连)。
13. 安全与部署(原方案缺失,新增)
Section titled “13. 安全与部署(原方案缺失,新增)”| 项 | 决策 |
|---|---|
| 认证 | 单用户多 token:server.yaml 配置 [{name, token_hash, scope: read/write/agent/sdk}];REST/WS/MCP 统一 Bearer;MCP/REST 的 write scope 才有 submit/cancel |
| 传输 | 信任网络(LAN/Tailscale)可明文 HTTP;跨网必须 TLS——推荐 Tailscale 兜底,或 Caddy 自动证书反代 |
| 命令注入 | job command 来自“拥有者本人”(个人产品定位),不做多租户隔离;但 Web UI 表单模式对参数做 shell 转义,yaml 模式原样传递并明示风险 |
| 数据备份 | gpuctl backup 在线备份 Server SQLite;在每台 Agent 主机运行 gpuctl backup-agent 归档完整 jobs 日志/runtime 与离线 spool。分体部署必须同时保留两类备份;可选 litestream 持续复制 SQLite 到外部目录 |
| 部署 | deploy/systemd/gpuplane-server.service / gpuplane-agent.service(user service + linger);升级 = git pull && uv sync --frozen --no-dev && systemctl --user restart |
14. 代码结构(uv workspace monorepo)
Section titled “14. 代码结构(uv workspace monorepo)”GPUPlane/├── pyproject.toml # uv workspace 根├── packages/│ ├── common/ gpuctl-common # Pydantic 协议模型/WsMsg 类型/ID 生成/常量 (零重依赖)│ ├── sdk/ gpuctl-sdk # 训练侧 SDK (仅 httpx)│ ├── agent/ gpuctl-agent # NodeMonitor/Runner/TBAdapter/CkptWatcher/WSClient│ ├── server/ gpuctl-server # FastAPI app + Scheduler + EventDetector + RollupWorker│ ├── mcp/ gpuctl-mcp # MCP 适配器 (fastmcp 钉版)│ └── cli/ gpuctl # typer CLI: status/node/job/run/logs/metrics/submit/import-run/mcp-stdio├── web/ # React + Vite + Tailwind + uPlot/ECharts├── deploy/systemd/ # unit 模板├── docs/├── examples/ # 示例训练项目 (mnist / hf-sft) 用于端到端验证└── tests/ # e2e: 假 agent + 真 server + sqlite包依赖方向:common ← sdk / agent / server / cli / mcp,server 不依赖 agent,agent 不依赖 server。CLI 顶层不 import 任何重库。
CLI 命令面(v0.1 完整,v0.2+ 标注)
Section titled “CLI 命令面(v0.1 完整,v0.2+ 标注)”gpuctl status # 总览: 节点/GPU/当前job/队列gpuctl node list|show <id>gpuctl submit --name N --gpu 1 --dir D [--priority] [--retry N] -- <cmd...>gpuctl job list|show|cancel|retry <id>gpuctl logs <job-id> [-f] [--full] [--stream stderr]gpuctl run list|show <id> # show 打印 RunSummarygpuctl metrics <run-id> --names train/loss --window 6hgpuctl checkpoints <run-id>gpuctl import-run <dir> # v0.1: TB events + logs + ckpt 识别导入gpuctl server serve|backup # 管理命令gpuctl agent run # 前台运行 (systemd 托管时也由此启动)gpuctl mcp-stdio # v0.3gpuctl event-hook ... # v0.215. 关键风险与缓解
Section titled “15. 关键风险与缓解”| 风险 | 影响 | 缓解 |
|---|---|---|
| MCP 生态版本动荡(mcp 1→2 硬 breaking,fastmcp 3/4 交替期) | 依赖冲突、升级断裂 | uv.lock 钉死 fastmcp 3.4.x + mcp 1.x;MCP 放独立包,坏了不影响 server;v0.3 才接入 |
| TB event 解析边界(tensor-scalar、半写、step 回退、大文件 CRC) | Level 1 指标丢失/错乱 | 按 §7.4 逐条处理 + 集成测试用真实 PyTorch SummaryWriter 产物;SDK 路径作为可靠主线 |
| 单 worker 内存态 | 水平扩展受限 | 个人规模可接受;抽象 pub/sub 接口,未来可换 Redis |
| 日志全量入库 | SQLite 膨胀 | §7.3:全文留 agent 端,server 只存 tail |
| checkpoint 半成品注册 | 评估到损坏模型 | 防抖 + 指纹 + .tmp 约定 + verify 命令 |
| 训练环境与平台依赖撞车 | 装 sdk 破坏训练 venv | sdk 零重依赖;agent/server 永远装在自己的 venv,不进训练环境 |
| 个人产品过度设计 | 维护成本拖垮项目 | 路线图每里程碑可独立交付使用;明确不做清单(见 02 号文档) |