跳转到内容

v0.1 深度测试报告

日期:2026-08-18。范围:在 v0.1 真机验收(docs/05)之后,针对系统各层做一轮深度测试,目标是尽量找出各方面问题。方法:4 个并行测试 agent(API 边界 / 调度器不变量 / runner 故障模式 / 数据层语义),各自独立起本地 server+agent、独立端口(18701–18704),外加人工 WebUI 走查与 5090 真机复验。

方向 覆盖内容 主要发现
REST 边界 畸形 ID/JSON、auth 矩阵、分页上下界、cancel/retry 语义、SDK 批量去重、超大载荷、logs 端点 int64 溢出 500、负 limit、幽灵 checkpoint id、seq 烧掉
调度器不变量 优先级序、FIFO、cpu 车道上限、GPU-less 节点排队、重试矩阵、node_selector、dispatch 超时、churn 13 探针:10 OK、1 BUG(队头阻塞,缺陷 11)、2 WEIRD 已接受(§5);优先级序真机复验 ✓
runner 故障模式 命令不存在、cwd 缺失、退出码、OOM 正则、cancel 竞态、大日志 ring、unicode、env 传递、agent 重启中途 12 探针:10 OK、1 WEIRD(env/token 泄漏,缺陷 19/20)、1 BUG(优雅关停误标,缺陷 18);另见 SDK PYTHONPATH(缺陷 10)
数据层 cursor 去重/空洞、summary 数学、checkpoint attach 先后、SDK 本地 spool、server 重启中途、import 幂等、并发写、事件过滤 37 探针:26 OK、6 BUG(缺陷 5/9/13/12/15/16)、5 WEIRD(3 修复/接受,8b 实为缺陷 17);SDK flush 竞态(缺陷 9,真机 19/20 → 20/20)
WebUI 首页三卡、RunDetail 四 tab、JobDetail 日志、NaN 显示 latest=null 时 Overview 崩溃(toPrecision on null)
# 缺陷 现象 修复
1 int64 溢出 → 500(6 端点) priority=10**30before=10**30、metrics ts/seq/step、checkpoint step 越界均 500 请求模型/Query 加 ge/le=2**62 边界 → 422(models.py MAX_I64_SAFE
2 负 limit/tail 被接受 SQLite LIMIT -1 = 无界,击穿行数上限 所有分页/日志 Query 加 ge=1(metrics limit 加 ge=1, include_tail ge=0)
3 checkpoint 重放返回幽灵 id 同 (run,path) 二次 POST 返回新 id(DB 实际 upsert 保旧 id)→ best.checkpoint_id 指向不存在 id upsert_checkpoint 回读存储行返回(store.py)
4 seq 先消费后插入 → 静默丢批 插入失败后同 seq 重试被 dedup,数据永久丢失 cursor+insert 合并为单事务(insert_metrics_once / insert_node_metrics_once / append_logs_once),失败不烧 seq
5 metrics LIMIT 取最早 N 点 长 run 图表/趋势只看开头;trend 冻结在 run 前 20 点,永不反映近期走势 ORDER BY ts DESC LIMIT n 后反转(store.py query_metrics / query_node_metrics)
6 RunDetail latest=null 崩溃 NaN/Inf 处理后 latest 可为 null,Overview v.toPrecision(5) 抛 TypeError,整页白屏 null 渲染为红色 non-finite(RunDetail.tsx + api.ts 类型)
7 错误信封不一致 pydantic 422 / 未知路由 404 / 错动词 405 返回 FastAPI 裸 {"detail":...} 统一 {error:{code,message,details}} 处理器(app.py)
8 静默接受非法值 gpu=-1max_attempts=0/-3name=''source=bogus、checkpoint step=-5/size=-1 照收 请求模型加约束(name min_length、gpu ge=0、max_attempts ge=1、source pattern、step/size ge=0)
9 SDK flush 竞态丢最后一批 20 个指标点只落 19 个(真机复现):finish() 只等队列空,daemon 发送线程在进程退出时被杀死,pending 批量丢失 按需 flush:sentinel 唤醒 + _pending_n/_in_flight 追踪,flush 等到真正交付完(_run.py);真机复验 20/20
10 作业内 import gpuctl 失败 runner 注入 GPUCTL_* 环境但未注入 SDK 路径,conda python 作业 ModuleNotFoundError runner 自动注入版本匹配的 SDK src 到 PYTHONPATH(显式 job PYTHONPATH 优先);真机复验 SDK attach 成功
11 调度器队头阻塞(test-sched #4b) GPU 任务在队首且无空闲槽位时调度器直接返回,其后的可调度任务(如 cpu_only)永远不被派发 _dispatch_next 遍历全部 QUEUED、跳过不可调度项(单 tick 仍只派发一个);回归测试:GPU 占用 + cpu 任务 → 后者被派发 ✓
12 spool 重放在新流量之后(test-data #6) 恢复联机后先 POST 新批次推高 ingest cursor,spool 里的旧 seq 被 server 当重放 dedup → 断线期间的点永久丢失(docstring 声称先重放,代码实际相反) _deliver_metrics/_deliver_drain_spool() 再发新批次(_run.py);回归测试验证 POST 顺序 [旧 seq, 新 seq] 与失败保序 ✓
13 import httpx 在 try 块外(test-data #5c) 环境缺 httpx 时 _register 初始化即抛异常,本地 spool 兜底失效(观测 errors=12 / state=new / run_id=None) import 移入 try + fallback 日志(_run.py);回归测试 monkeypatch sys.modules['httpx']=None → 本地模式 ✓
14 flush sentinel 无 kind 键杀死发送线程 {"_flush": True} 不含 “kind”,sender 分支 item.get("kind")=="_flush" 永不命中 → 每次 flush 交付后线程 KeyError 崩溃;首次 flush 因崩溃前已交付而掩盖问题,同进程后续 flush 挂起丢批(回归测试暴露) 分支改判 item.get("_flush")(_run.py);回归测试单进程双 flush;真机 3×flush 全 queued=0 ✓
15 step 过滤在 LIMIT 之后(test-data #11c) step_to 窗口 + limit 组合下先截断后过滤,窄窗口整体被过滤空 step 条件并入 SQL WHERE(先于 LIMIT)(store.py query_metrics);回归测试 step_to=1&limit=3 → [0,1] ✓
16 ensure_project/experiment 并发竞态(test-data #12b) SELECT-then-INSERT 窗口期第二个注册抛 IntegrityError → 500 INSERT … ON CONFLICT DO NOTHING 优先再 SELECT(store.py);回归测试 asyncio.gather 并发 ✓
17 backup 同秒覆盖(test-data #8b) 文件名精度仅到秒;同一秒两次备份 → 第二次静默覆盖第一次 datetime.now().strftime('%Y%m%d-%H%M%S-%f')time.strftime 不支持 %f,会输出字面量 f —— 首轮修复即栽在此处);真机同秒两次 → 两个文件 ✓
18 agent 优雅关停误标 FAILED/LOST(test-agent #P11) 设计:agent SIGTERM → 取消任务进程组 → 报告 CANCELLED。实际:disconnect 即标 LOST;重连后 reconcile 先于 spool 回放,local_state.exited 恒为空 → _on_reconcile 终态化 FAILED/LOST,spool 里的真实 CANCELLED(-15) 被 is_terminal 守卫丢弃 → 假 JOB_LOST CRITICAL、真 exit_code 丢失、无 RUN_FINISHED 双向修复:agent 重连先回放 spool 再 reconcile(main.py,与 runner 注释意图一致);server 对优雅关闭(ws close code 1000)跳过 LOST 标记(gateway.py),只有异常断开立即标 LOST。真机复验:RUNNING 中杀 agent 重启 → CANCELLED exit=-15、事件流无 JOB_LOST ✓
19 write scope token 注入作业(test-agent #P9a) 无 sdk-scope token 时调度器回退注入 write(admin)token —— 作业代码获得提交/取消/备份的完整 API 权限 token 注入仅限 sdk scope;缺失时警告并降级为 spool 摄取(scheduler.py);真机复验 job env GPUCTL_TOKEN=wsl5090-sdk-token
20 agent 环境泄漏进作业(test-agent #P9b) env=dict(os.environ) 把 agent 的 GPUCTL_AGENT_CONFIG(含 ws token)等整份环境拷给作业 继承环境剥离 GPUCTL_* 前缀后再注入受控变量(runner.py);回归测试断言 AC=

2b. 判定为设计内行为(验证通过)

Section titled “2b. 判定为设计内行为(验证通过)”
  • 优先级排序(真机):排队中的任务按 priority DESC 先来先启动——GPU 释放后 100→50→-50 依次启动 ✓
  • 非抢占(设计 docs/03 §: 不抢占):已在跑的低优先级任务不会被新提交的高优先级任务打断(先提交先运行)✓
  • NaN/Inf 全链路:SDK→server→SQLite→API→UI(non-finite 红色标记 + 图表断线 + LOSS_NAN 事件)✓

3. 真机复验(WSL 5090,通过 18601 转发)

Section titled “3. 真机复验(WSL 5090,通过 18601 转发)”
  • limit=-1 → 422 validation_error 信封;空 name → 422;未知路由 → 404 信封 ✓
  • NaN probe run:latest 为 null、series 为 null 空洞、LOSS_NAN critical 事件、best 保持 0.4@step2(跳过非有限值)✓(截图 nan_overview/nan_metrics/nan_events.png)
  • 服务重启后节点 3s 内回 ONLINE ✓
  • 多 flush 单进程探针(缺陷 14 复验):同一进程内 3 次 flush(),每次返回时 queued=0,最终 3/3 指标点落库 ✓(旧代码首次 flush 即杀死发送线程)
  • 同秒两次 backup(缺陷 17 复验):…174356-447483.db…174356-525055.db 两个文件并存 ✓
  • agent 优雅重启(缺陷 18 复验):RUNNING 中 pkill -f gpuctl-agent 后重启 → 任务 CANCELLED、exit_code=-15、事件流仅 RUN_FINISHED/JOB_DISPATCHED/JOB_QUEUED(无 JOB_LOST / AGENT_DISCONNECTED)✓
  • 作业环境(缺陷 19/20 复验):job env 为 GPUCTL_TOKEN=wsl5090-sdk-token(sdk scope,非 admin write),无 GPUCTL_AGENT_CONFIG
  • 首页 NodeCard:CPU 利用率数字 + sparkline(修复此前仅图形无数值的问题);新增 MEM / VRAM 横向 Meter(绿<60% / 琥珀<85% / 红≥85%,显示 used/total)
  • RunDetail tab 进入 URL(?tab=Metrics),可分享/书签(顺带使无头验证可行)
  • RunDetail Overview 对非有限 latest 显示红色 non-finite(事件流里有精确 NaN/Inf 详情)
  • settoken.html 纳入 web/public/(构建自动拷贝;新格式 ?tk=TOKEN&to=/route,不再硬编码 token)

5. 边界行为(v0.1 判定可接受,文档化)

Section titled “5. 边界行为(v0.1 判定可接受,文档化)”
  • GET /jobs/(尾斜杠)→ 404 信封(static mount 吞掉 307;返回 JSON 错误而非 HTML,可接受)
  • GET /jobs/nonexistent/runs → 200 [](集合空语义)
  • logs?stream=bogus → 200 空行 + note(容忍未知 stream)
  • GET /jobs/{id}/cancel 用错动词 → 404(FastAPI 路径注册语义)
  • 乱序 seq(test-data #1d):server 按 (run_id, seq) 单调去重;空洞后补发的旧 seq 被当重放 dedup(文档化行为;SDK 单线程顺序发送,实际不会产生该形态)
  • 重复 import 生成重复 run(test-data #7):import 是显式动作,重复执行不幂等(按设计)
  • ws-ping 超时先于 dispatch-ack(test-sched #7b):agent 被 SIGSTOP 时 ws ping 超时 → 节点判离线 → 运行中任务标记 LOST,而非 dispatch-ack 超时 requeue;SIGSTOP-resume 后 agent 重连恢复正确(LOST 非终止、可重连,接受)

新增 21 个用例(31 → 52 总数):validation bounds、envelope 一致性、checkpoint 重放同 id、cursor 失败不烧 seq、limit 取最新 + trend 跟随近期窗口、step 过滤先于 LIMIT(缺陷 15)、ensure_project 并发竞态(缺陷 16)、backup 文件名唯一(缺陷 17)、HOL 队头跳过(缺陷 11,scheduler 单测)、graceful/abrupt disconnect 的 LOST 差异(缺陷 18)、sdk-only token 注入(缺陷 19)、SDK flush 等在途交付(3 个)、spool 先于新流量(缺陷 12)、spool 失败保序、flush sentinel 不杀线程(缺陷 14)、httpx 缺失本地兜底(缺陷 13)、runner SDK PYTHONPATH 注入、runner 环境不泄漏(缺陷 20)。README 计数已同步为 52。

四个 agent 各自独立起本地 server+agent(端口 18701–18704)完成全部场景;test-sched / test-data / test-agent 经 teammate 通道交付了完整报告,结论可信:

agent 场景 判定
test-api REST 边界(44 次提交) 交付 final_findings.json;其 BUG 均已在缺陷 1–10 轮修复并回归
test-sched 调度器不变量 13 探针 10 OK(优先级序 / FIFO / cpu 车道 / 重试 / 取消 / churn / ack 超时精确 30s);1 BUG = 缺陷 11(HOL 队头阻塞,已修复);2 WEIRD 已接受(§5:7b ws-ping 优先、10 事件缺口 → 已补 JOB_CANCELLED/JOB_REQUEUED/JOB_DISPATCH_FAILED)
test-data 数据层 37 探针 26 OK;6 BUG = 缺陷 5(2f LIMIT 序)、9(5a flush)、13(5c httpx)、12(6 spool 序)、15(11c step 过滤)、16(12b 并发注册),全部已修复并回归;5 WEIRD:9d/extra-a 已修复,1d/7 接受(§5),8b 实为缺陷 17
test-agent runner 故障模式 12 探针 10 OK(启动失败→DISPATCH_FAILED 快速失败、exit0/3→SUCCEEDED/EXIT_CODE、segv→SIGNAL(11)、OOM 检测、取消幂等、大日志 tail=2000、unicode 逐字节往返、runtime.json、并发 cpu 车道 2+2 全过);1 WEIRD = 缺陷 19/20(token 与 env 泄漏,已修复);1 BUG = 缺陷 18(优雅关停误标 FAILED/LOST,已修复并真机复验)