Files
DCTS/docs/tlusty&synspec收敛性判断.md
fmq cd370d88e7 feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构
将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。

引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
  normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
  trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
  force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
  (mark_grid_point_running 仅 pending/queued→running;
  record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照

僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
  活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复

动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
  缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效

科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
  场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)

前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
  pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画

文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
  光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
2026-08-04 23:40:52 +08:00

23 KiB
Raw Permalink Blame History

任务失败判定逻辑调研:Tlusty&Synspec收敛性判断LUSTY / SYNSPEC 退出码可靠性与兜底机制

调研日期:2026-08-04 范围:crates/common/src/runner.rscrates/node/src/reporter.rscrates/common/src/conv_check.rs 的任务成败判定逻辑,对照 TLUSTY 208 / SYNSPEC 54 源码(tlusty/tlusty208.fsynspec/synspec54.f) 与 gfortran 运行时退出码行为。 方法:源码级静态分析 + gfortran 实测(退出码、运行时错误)+ 三方独立复核。 本文仅记录调研结论,不含修复实现。


0. TL;DR

调度系统通过子进程退出码 rc 判定 TLUSTY / SYNSPEC 成败,但 gfortran 下裸 STOP 默认返回 rc=0,而两个 Fortran 程序的几乎所有错误路径用的都是裸 STOPcall quit(内部亦为裸 stop)。因此 rc 本身不可靠

当前判定能"基本不出错",靠的是 rc 之外的兜底:

  • TLUSTYrc==0 && fort.7 存在 才进入 check_fort9max_relc < chmax+ atmosphere_has_nan 双重否决。这三重条件对"显式发散 STOP"和"硬崩溃(信号)"覆盖可靠。
  • SYNSPEC仅靠 synspec_rc==0,下游零内容校验,是当前系统最大的科学正确性风险。

复核(含 gfortran 运行时错误实测)发现两类此前未点出的风险:

  1. gfortran 运行时错误默认返回 rc=0 且静默(实数 IEEE 异常、数组越界),生产编译未启用 -fcheck/-ffpe-trap
  2. fort.9 缺失分支无条件判收敛,且写入虚构的 best_max_relc=0.0,会污染种子选择并向相邻网格点扩散。

修复优先级收敛为:SYNSPEC .spec 内容校验 > 收紧 atmosphere_has_nan 阈值 > 修正 fort.9 缺失分支 > 补齐 SOLVES/RYBSOL 发散路径归因。


1. 现状:失败判定的四层结构

node 端任务失败判定从底向上分四层:

execute_task (executor.rs)      ← 前置 IO 失败 → 直接返回 Err
  └─ run_model_with_timeout     ← 计算失败 → 产出 ModelSummary(converged 字段)
       └─ derive_report_status  ← 半失败判定 → Completed / Failed
            └─ infer_failed_stage ← 失败归因 → tlusty / synspec

第一层:execute_taskexecutor.rs:11)—— 前置 IO 失败

直接返回 Err、进不到计算:

失败情形 处理 行号
原子数据文件拉取失败 warn继续(非致命) executor.rs:46-50
种子大气下载失败(HTTP 非 2xx / 网络) bail! 返回 Err executor.rs:114-137
沙盒目录创建失败 ? 返回 Err executor.rs:57

返回 Err 时,worker.rs:456 转为 Stringreporter 走 Err(e) 分支(reporter.rs:78-89): status=Failed, converged=false, error_message=e

第二层:run_model_with_timeoutrunner.rs:232)—— 计算过程收敛判定

核心,final_converged 在此累积,分三块:

① TLUSTY 阶段链收敛判定(runner.rs:402-469

每个阶段执行后,只有 rc==0 && fort.7 存在 才进入收敛检查,否则该阶段 converged=false

// runner.rs:402
if rc == 0 && fort7.is_file() {
    if fort9.is_file() {
        let res = check_fort9(&fort9, eff_chmax);
        stage_summary.converged = res.converged;       // max_relc < chmax
    } else {
        // NITER=0 grey start without fort.9   ← 见 §4 漏洞 2
        stage_summary.converged = true;
        stage_summary.best_max_relc = Some(0.0);
    }
} else {
    stage_summary.note = Some(format!("tlusty rc={} or missing fort.7", rc));
}

check_fort9 的收敛判据(conv_check.rs:163):

converged = max_relc.is_finite() && max_relc < chmax

其中 max_relc 是 fort.9 最后一次迭代中所有深度点里最大的相对修正绝对值。关键防御:

  • chmax ≤ 0 或非有限 → 直接判失败并报错(conv_check.rs:58,防误配)
  • fort.9 无有效迭代数据 → max_relc=Infinity,判失败(conv_check.rs:127
  • 发散值(NaN/Inf/无-E 记数法 -1.35+118)→ is_finite()=false → 判失败(conv_check.rs:160
  • fort.9 缺失但 rc=0 → 视为"NITER=0 grey start"判收敛runner.rs:419)—— 见 §4 漏洞 2

阶段间联动:若某阶段 require_converged=true 且未收敛 → break,中止后续所有阶段(runner.rs:463)。 final_converged 取最后执行阶段的值。

② 大气 NaN 强制否决(runner.rs:488-491

let atmo_has_nan = atmosphere_has_nan(&final_7);
if atmo_has_nan { final_converged = false; }

atmosphere_has_nanconv_check.rs:190)检测大气文件里 NaN/Inf/********Fortran 字段溢出), 超 10% 行命中即判无效。此步会推翻前面收敛结论 —— 见 §4 漏洞 3 的阈值盲区。

③ SYNSPEC 阶段(runner.rs:498-603

SYNSPEC 不直接改 final_converged,记录到 synspec_rc / synspec_error

  • fort.8(大气输入)复制失败 → 记 synspec_error,跳过(runner.rs:508
  • fort.55(控制卡)写入失败 → 记 synspec_error,跳过(runner.rs:531
  • synspec 进程 rc≠0 或超时(上限 min(600, timeout_sec))→ 记 synspec_rcrunner.rs:568-575

仅 SYNSPEC 场景(tlusty 关闭)的收敛重判(runner.rs:611-616): final_converged = (synspec_rc == 0)

第三层:derive_report_statusreporter.rs:38)—— 半失败 = 失败

let synspec_failed = synspec_error.is_some() || matches!(synspec_rc, Some(rc) if rc != 0);
if s.converged && !synspec_failed { TaskStatus::Completed } else { TaskStatus::Failed }

旧逻辑 if converged { Completed } 只看大气收敛,TLUSTY 收敛但 SYNSPEC 失败时 converged 仍为 true → 误报成功 → 服务端吸收为终态。新逻辑:大气好 + 光谱坏 = 整体失败,让服务端弹 synspec 链重试。 converged 字段仍按大气真实状态上报(为 true 时服务端仍存 .7 作种子)。

第四层:infer_failed_stagereporter.rs:15)—— 失败归因

条件 归因
converged=false + tlusty 启用 tlusty(含级联)
converged=false + tlusty 关闭 synspec
converged=true 但 synspec 出错/rc≠0 synspec
全部成功 None

2. rc==0 的真实含义与不可靠性

2.1 gfortran 退出码实测(实证)

/tmp 用最小 Fortran 程序实测(gfortran 15.2.0-fno-automatic -O3 等价于生产编译环境):

Fortran 语句 / 错误类型 触发方式 退出码 rc runner 能否抓住
STOP stop 0 靠 fort.9 兜
STOP 'msg' stop 'x' 0 靠 fort.9 兜
STOP 1(带数字) stop 1 1 TLUSTY 未用此形式)
自然END 主程序结束 0
实数除零1.0/0.0 IEEE 默认 0 产 Inf,无陷阱
0.0/0.0 IEEE 默认 0 产 NaN
实数溢出1e30*1e30 IEEE 默认 0 产 Inf
sqrt(-1) / log(0) IEEE 默认 0 产 NaN/-Inf
整数除零 1/0 SIGFPE 136128+8
数组越界(默认) -fcheck 0,静默写越界内存
数组越界(-fcheck=bounds 启用检查 2
READ 遇 EOF(无end= 守卫) 运行时 2
READ 类型不匹配(无err= 守卫) 运行时 2
栈溢出 / 段错误 SIGSEGV 139128+11

生产编译命令(deployment.md:86,90)为 gfortran -fno-automatic -O3未启用 -fcheck=bounds-ffpe-trap=invalid,zero,overflow。因此表中所有 rc=0 的静默失败在生产二进制中真实存在。

2.2 核心矛盾

Fortran 的 STOP 语句默认返回 0,而 TLUSTY/SYNSPEC 几乎所有错误处理用的都是裸 STOPcall quit(内部裸 stop)。导致进程异常终止的退出码 = 正常完成的退出码 = 0

  • TLUSTY subroutine quittlusty208.f:29946-29955)末行裸 stop
  • SYNSPEC subroutine quitsynspec54.f:12014-12021)末行裸 stop
  • 两者全文 80+ 处 STOP/call quit,错误路径几乎全走裸 STOP

3. 源码级失败路径分析

3.1 TLUSTY:三重兜底对"显式发散"覆盖可靠

主循环结构(tlusty208.f:1-62):

   10 ITER=ITER+1
      CALL RESOLV           形式解;内部 CALL OUTPUT(:3807/:3828) 每次迭代写 fort.7
      IF(LFIN) GO TO 20     上一轮设了 LFIN 就跳出
      IF(IACC.GT.0) CALL ACCEL2
      IF(IFRYB.EQ.0) THEN
         IF(NN.GT.MSMX) THEN
            CALL SOLVE      含发散 STOP(:14703)
          ELSE
            CALL SOLVES     含发散 STOP(:15050)
         END IF
       ELSE
         CALL RYBSOL        含发散 STOP(:47585-47590)   修正点:第三条路径
      END IF
      GO TO 10
   20 STOP                  主程序正常结束,裸 STOP  rc=0
      END

关键事实:

  • fort.7 每次迭代都写CALL OUTPUTtlusty208.f:3807, 3828)在 RESOLV 内,每次迭代执行。
  • 发散 STOP 在三处平行求解器内(不止 SOLVE):
    • SOLVE :14701-14706CHMX > 1.D16 → 裸 STOP
    • SOLVES :15047-15051(同逻辑)
    • RYBSOL/RYBCHN :47585-47590(同逻辑,IFRYB≠0 时走此路) 三处发散检查都在 PRCHAN/RYBCHN 写完 fort.9 之后,故发散 STOP 时 fort.7 与 fort.9 均已落盘。
  • 达到 NITER 未收敛LFIN=ABS(CHMX).LE.CHMAX.OR.ITER.GE.NITER(:14728),走主程序正常 ENDrc=0fort.9 含未收敛的 max_relc。

TLUSTY 失败场景对照:

场景 Fortran 处理 rc 写 fort.7 写 fort.9 runner 判定 是否合适
正常收敛 主程序 END 0 check_fort9 → converged
达到 NITER 未收敛 LFINEND 0 check_fort9 → max_relc≥chmax → 未收敛
数值发散 CHMX>1e16 SOLVE/SOLVES/RYBSOL 内裸 STOP 0 check_fort9 → 发散值未收敛
输入错误(call quit 裸 stop 0 rc=0 但 fort.7 缺失 → 失败
temp 越界 stop 'partf; temp...'(:44624) 0 fort.7 缺失 → 失败

结论rc 虽不可靠,但 rc==0 + fort.7 存在 + check_fort9 + atmosphere_has_nan 四重条件对"显式 STOP 类发散"和"硬崩溃(信号)"覆盖可靠 —— 这是一个巧合但有效的设计。

3.2 SYNSPEC:仅靠 rc,基本裸奔

主程序结构(synspec54.f:1-174): 主循环 :10 标签到 :150 IF(IBLANK.LT.NBLANK) GO TO 10 CALL OUTPRI:143)写谱输出(unit 7 谱 / 17 连续谱 / 16 部分等宽 / 6 诊断)且在循环内、多次追加写(无 REWIND)。 主程序以自然 END:174)结束,正常路径 rc=0。

call quit / STOP 相对 OUTPRI 的时序分类(复核产出):

归类 数量 代表行号 风险
OUTPRI 之前START 阶段输入校验) 多数 :726, :808, :937, :1062, :1856, :2229, :12168, :12224, :12252, :12323, :12324, :16745, :19455, :19601 低 —— 谱未生成,靠"文件缺失"兜得住
OUTPRI 之后 / 之间(主循环内 RESOLV/RESOLW/OPAC 调用链) 6 处 :4203, :8208, :9935, :18863, :18865, :19891 高 —— 脏数据风险
死代码 / 坏码 3 处 :17414stop23(坏码)、:17851 CHCKAB(调用被注释)、:18383 MOLSET(从未调用)

脏数据风险的精确触发条件:仅在 NBLANK>1 或分子/集合重处理synspec54.f:152-162)导致前序迭代已写 fort.7 之后,后续迭代在 INISET/OPAC/RESOLW/SETRAY 等处中途 call quit → 残缺谱已落盘 + rc=0 → 被误判成功。单集合单次运行(NBLANK=1)所有 quit 都在首次 OUTPRI 之前,无脏数据

SYNSPEC 失败场景对照:

场景 Fortran 处理 rc runner 判定 是否合适
正常完成 主程序 END:174 0 synspec_rc=0 → 成功
输入数据错误(OUTPRI 前 quit 裸 stop 0 谱未生成 → .spec 缺失(runner.rs:579 is_file 不成立)→ 未产谱 ⚠️ 靠文件缺失
计算中途错误(OUTPRI 后 quit,多轮场景) 裸 stop 0 synspec_rc=0 →误判成功 严重

4. 复核发现的漏洞(按严重度排序)

以下为源码级静态分析与 gfortran 实测、经三方独立复核确认的风险。

漏洞 1(最严重):SYNSPEC 产出的 .spec 全链路零内容校验

runner 对 SYNSPEC 产出(runner.rs:578-599只有 is_file() 存在性检查 + rename/copy,无任何内容校验:

if model_dir.join("fort.7").is_file() {
    let _ = tokio::fs::rename(..., ... ".spec").await;   // 不查内容
}

下游全部不拦截:

  • executor.rs:191-215:只读 .7(种子),从不碰 .spec
  • result_filter.rs:64-110:纯文件名后缀白名单,spec 在白名单内(result_filter.rs:28),0 字节 .spec 与干净的 50MB .spec 同等归档
  • derive_report_statusreporter.rs:38-45):只看 synspec_rcsynspec_error,不看产物。

完整穿透链:SYNSPEC call quitrc=0+ 写出含 NaN/Inf/0 字节的 .specis_file() 成立 → rename 成 .specfinal_converged = rc==0runner.rs:612)→ synspec_failed=falsereporter.rs:39)→ Completed → 脏谱归档、服务端吸收为终态、不重试。没有任何一道防线检查 .spec 内容。

漏洞 2(严重):fort.9 缺失分支无条件判收敛 + 虚构 best_max_relc=0.0

runner.rs:417-421,当 rc==0 && fort.7 存在 但 fort.9 不存在时:

} else {
    // NITER=0 grey start without fort.9
    stage_summary.converged = true;            // 无条件判收敛
    stage_summary.best_max_relc = Some(0.0);   // 虚构"完美收敛"值
    stage_summary.note = Some("NITER=0 grey start".to_string());
}
  • 注释"NITER=0 grey start"是断言而非检查,代码未校验本次 stage 的 niter 配置是否真为 0。
  • best_max_relc = Some(0.0) 进入 final_max_relcrunner.rs:458-459)→ 上报(reporter.rs:73)。若服务端用 max_relc 选种子,会把误判模型当"完美收敛"优先选作下一轮种子,污染向相邻网格点扩散
  • 合法 grey startlte/nc 阶段,require_converged=falserunner.rs:23/38)与致命崩溃共用同一段无校验代码,无法区分。

触发条件rc==0gfortran 裸 STOP 或运行时静默错误)+ fort.7 已写 + fort.9 缺失。严重度比漏洞 1 低,因为多数发散情况 fort.9 会写出含发散值的行,反而触发 check_fort9is_finite()=false 判不收敛。

漏洞 3(严重):gfortran 运行时错误默认 rc=0 且静默

见 §2.1 实测表。在生产编译(无 -fcheck/-ffpe-trap)下:

  • 实数 IEEE 异常(除零、溢出、NaN 产生)→ rc=0,无陷阱,静默产出 NaN/Inf。
  • 数组越界(无 -fcheck=bounds)→ rc=0静默写越界内存,可能产出物理上错误但无 NaN 标记的大气。

这两类若发生在 RESOLV 写完 fort.7tlusty208.f:3828)之后、SOLVE 写 fort.9 之前 → fort.7 在、fort.9 不在 → 命中漏洞 2 → 误判收敛。atmosphere_has_nan 能否兜住取决于越界是否恰好产生 NaN/Inf 字面值;若只是静默改写相邻内存,此防线无效。

漏洞 4(中):atmosphere_has_nan 的 10% 阈值对物理产物过松

conv_check.rs:212(bad_lines as f64) > (total_lines as f64 * 0.1),严格大于。

典型 .7 文件 60-100 个深度行,阈值 6-10 行:

场景 bad/total 判定 物理正确性
全 NaN(彻底发散) 100% true → 否决 正确
6/100 行 NaN(表层发散) 6% false → 放过 错误
5/60 行 NaN(典型深度网格) 8.3% false → 放过 错误
单行 NaN 1% false → 放过 错误

物理上任意一个深度点的温度/密度是 NaN,整个大气就不可用(积分会传播 NaN)。10% 是统计学语义,与大气可用性的布尔语义不匹配。表层(深度浅、温度高)最易数值发散,往往只有最外几层出 NaN —— 正好落在盲区。

补充盲区:regex (?i)(\bnan\b|\binf(?:inity)?\b|\*{3,})conv_check.rs:199)只匹配字面 NaN/Inf/***;若 Fortran 把发散值写成接近 f64 上限但未溢出的普通科学记数法(如 1.0E+308),不命中任何模式。

漏洞 5(低):infer_failed_stage / 日志归因对 RYBSOL 路径不完整

发散 STOP 在 SOLVE / SOLVES / RYBSOL 三处(§3.1),但失败 note 统一记 tlusty rc=0 or missing fort.7runner.rs:429),丢失"为何未收敛"的线索。对走 RYBSOLIFRYB≠0)的发散,归因信息不完整。

漏洞 6(低):仅 SYNSPEC 场景 final_converged 重判只看 rc

runner.rs:611-616TLUSTY 关闭的纯复算场景,final_converged = (synspec_rc == 0),与漏洞 1 同源,场景更窄。


5. 被排除的伪风险

超时 / shutdown 后半成品 fort.7 进 check_fort9 误判收敛 —— 不存在。

run_child_async_with_timeout 在超时/shutdown 时返回 Errrunner.rs:151, 156)→ 上层 match rc = -1 runner.rs:380-383 / 568-574)。于是 rc==0 && fort.7 存在 前置门不成立 → 进 else 分支 → converged 保持 false → check_fort9 根本不调用。SYNSPEC 超时同理 → synspec_rc=-1 → 判失败。 超时路径是可靠的。


6. 修复优先级(仅建议,不含实现)

优先级 漏洞 建议 改动范围
P0 漏洞 1 SYNSPEC.spec 内容校验:NaN/Inf/*** + 行数下限 + 非全零,命中则置 synspec_rc 为非零或设 synspec_error runner.rsSYNSPEC 产出后,runner.rs:585 附近)
P1 漏洞 4 收紧物理产物 NaN 阈值:产物准入用 0 行容忍(任意一行 NaN 即否决),而非 10% conv_check.rs:212 或新增严格版函数
P1 漏洞 2 runner.rs:417-421:至少把best_max_relcSome(0.0) 改为 None(消除污染扩散);理想是校验 stage 的 niter 配置才走该分支 runner.rs:417-421
P2 漏洞 5 补齐 SOLVES / RYBSOL 发散路径的归因,失败 note 区分求解器类型 runner.rs:429
P3 漏洞 3 生产编译加-fcheck=bounds -ffpe-trap=invalid,zero,overflow,让数组越界与 IEEE 异常返回 rc≠0(性能换正确性) deployment.md 编译命令

7. 参考资料

  • 源码:
    • tlusty/tlusty208.f(主程序 :1-62;发散 STOP :14703/:15050/:47585quit :29946-29955 PRCHAN :22876-22944RYBCHN :47445-47598RESOLV :3724-3903OUTPUT :14055 起)
    • synspec/synspec54.f(主程序 :1-174OUTPRI :3343-3458quit :12014-12021 脏数据风险点 :4203/:8208/:9935/:18863/:18865/:19891
  • 判定逻辑:
    • crates/common/src/runner.rsrun_model_with_timeout :232-682;漏洞 1/2/5/6 所在)
    • crates/node/src/reporter.rsderive_report_status :38-45infer_failed_stage :15-27
    • crates/common/src/conv_check.rscheck_fort9 :54-175atmosphere_has_nan :190-213
  • 编译配置:docs/deployment.md:86,90(确认生产编译无 -fcheck/-ffpe-trap
  • 相关设计:docs/task_engine_decoupling_design.md(阶段独立配置、半失败判定语义)