将 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 笔误
361 lines
23 KiB
Markdown
361 lines
23 KiB
Markdown
# 任务失败判定逻辑调研:Tlusty&Synspec收敛性判断LUSTY / SYNSPEC 退出码可靠性与兜底机制
|
||
|
||
> 调研日期:2026-08-04
|
||
> 范围:`crates/common/src/runner.rs`、`crates/node/src/reporter.rs`、`crates/common/src/conv_check.rs`
|
||
> 的任务成败判定逻辑,对照 TLUSTY 208 / SYNSPEC 54 源码(`tlusty/tlusty208.f`、`synspec/synspec54.f`)
|
||
> 与 gfortran 运行时退出码行为。
|
||
> 方法:源码级静态分析 + gfortran 实测(退出码、运行时错误)+ 三方独立复核。
|
||
> 本文仅记录调研结论,不含修复实现。
|
||
|
||
---
|
||
|
||
## 0. TL;DR
|
||
|
||
调度系统通过子进程退出码 `rc` 判定 TLUSTY / SYNSPEC 成败,但 **gfortran 下裸 `STOP` 默认返回 rc=0**,而两个 Fortran 程序的几乎所有错误路径用的都是裸 `STOP` 或 `call quit`(内部亦为裸 `stop`)。因此 **rc 本身不可靠**。
|
||
|
||
当前判定能"基本不出错",靠的是 rc 之外的兜底:
|
||
|
||
- **TLUSTY**:`rc==0 && fort.7 存在` 才进入 `check_fort9`(max_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_task`(executor.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 转为 `String`,reporter 走 `Err(e)` 分支(reporter.rs:78-89):
|
||
`status=Failed, converged=false, error_message=e`。
|
||
|
||
### 第二层:`run_model_with_timeout`(runner.rs:232)—— 计算过程收敛判定
|
||
|
||
核心,`final_converged` 在此累积,分三块:
|
||
|
||
**① TLUSTY 阶段链收敛判定(runner.rs:402-469)**
|
||
|
||
每个阶段执行后,只有 `rc==0 && fort.7 存在` 才进入收敛检查,否则该阶段 `converged=false`:
|
||
|
||
```rust
|
||
// 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)**
|
||
|
||
```rust
|
||
let atmo_has_nan = atmosphere_has_nan(&final_7);
|
||
if atmo_has_nan { final_converged = false; }
|
||
```
|
||
|
||
`atmosphere_has_nan`(conv_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_rc`(runner.rs:568-575)
|
||
|
||
**仅 SYNSPEC 场景**(tlusty 关闭)的收敛重判(runner.rs:611-616):
|
||
`final_converged = (synspec_rc == 0)`。
|
||
|
||
### 第三层:`derive_report_status`(reporter.rs:38)—— 半失败 = 失败
|
||
|
||
```rust
|
||
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_stage`(reporter.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 | **136**(128+8) | ✅ |
|
||
| **数组越界(默认)** | 无`-fcheck` | **0,静默写越界内存** | ❌ |
|
||
| 数组越界(`-fcheck=bounds`) | 启用检查 | 2 | ✅ |
|
||
| READ 遇 EOF(无`end=` 守卫) | 运行时 | **2** | ✅ |
|
||
| READ 类型不匹配(无`err=` 守卫) | 运行时 | **2** | ✅ |
|
||
| 栈溢出 / 段错误 | SIGSEGV | **139**(128+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 几乎所有错误处理用的都是裸 `STOP` 或 `call quit`(内部裸 `stop`)。导致**进程异常终止的退出码 = 正常完成的退出码 = 0**。
|
||
|
||
- TLUSTY `subroutine quit`(tlusty208.f:29946-29955)末行裸 `stop`。
|
||
- SYNSPEC `subroutine quit`(synspec54.f:12014-12021)末行裸 `stop`。
|
||
- 两者全文 80+ 处 `STOP`/`call quit`,错误路径几乎全走裸 `STOP`。
|
||
|
||
---
|
||
|
||
## 3. 源码级失败路径分析
|
||
|
||
### 3.1 TLUSTY:三重兜底对"显式发散"覆盖可靠
|
||
|
||
**主循环结构(tlusty208.f:1-62):**
|
||
|
||
```fortran
|
||
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 OUTPUT`(tlusty208.f:3807, 3828)在 RESOLV 内,每次迭代执行。
|
||
- **发散 STOP 在三处平行求解器内**(不止 SOLVE):
|
||
- `SOLVE` :14701-14706(`CHMX > 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),走主程序正常 END,rc=0,fort.9 含未收敛的 max_relc。
|
||
|
||
**TLUSTY 失败场景对照:**
|
||
|
||
| 场景 | Fortran 处理 | rc | 写 fort.7 | 写 fort.9 | runner 判定 | 是否合适 |
|
||
| --------------------- | --------------------------------- | ----------- | --------- | --------- | ---------------------------------------- | -------- |
|
||
| 正常收敛 | 主程序 END | 0 | ✅ | ✅ | check_fort9 → converged | ✅ |
|
||
| 达到 NITER 未收敛 | `LFIN`,END | 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 处 | :17414`stop23`(坏码)、: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**,无任何内容校验:
|
||
|
||
```rust
|
||
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_status`(reporter.rs:38-45):只看 `synspec_rc` 与 `synspec_error`,不看产物。
|
||
|
||
完整穿透链:SYNSPEC `call quit`(rc=0)+ 写出含 NaN/Inf/0 字节的 `.spec` → `is_file()` 成立 → rename 成 `.spec` →
|
||
`final_converged = rc==0`(runner.rs:612)→ `synspec_failed=false`(reporter.rs:39)→ `Completed` →
|
||
脏谱归档、服务端吸收为终态、不重试。**没有任何一道防线检查 `.spec` 内容。**
|
||
|
||
### 漏洞 2(严重):fort.9 缺失分支无条件判收敛 + 虚构 best_max_relc=0.0
|
||
|
||
runner.rs:417-421,当 `rc==0 && fort.7 存在` 但 fort.9 **不存在**时:
|
||
|
||
```rust
|
||
} 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_relc`(runner.rs:458-459)→ 上报(reporter.rs:73)。若服务端用 max_relc 选种子,会把误判模型当"完美收敛"优先选作下一轮种子,**污染向相邻网格点扩散**。
|
||
- 合法 grey start(`lte`/`nc` 阶段,`require_converged=false`,runner.rs:23/38)与致命崩溃共用同一段无校验代码,无法区分。
|
||
|
||
**触发条件**:rc==0(gfortran 裸 STOP 或运行时静默错误)+ fort.7 已写 + fort.9 缺失。严重度比漏洞 1 低,因为多数发散情况 fort.9 会写出含发散值的行,反而触发 `check_fort9` 的 `is_finite()=false` 判不收敛。
|
||
|
||
### 漏洞 3(严重):gfortran 运行时错误默认 rc=0 且静默
|
||
|
||
见 §2.1 实测表。在生产编译(无 `-fcheck`/`-ffpe-trap`)下:
|
||
|
||
- **实数 IEEE 异常**(除零、溢出、NaN 产生)→ rc=0,无陷阱,静默产出 NaN/Inf。
|
||
- **数组越界**(无 `-fcheck=bounds`)→ rc=0,**静默写越界内存**,可能产出物理上错误但无 NaN 标记的大气。
|
||
|
||
这两类若发生在 RESOLV 写完 fort.7(tlusty208.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.7`(runner.rs:429),丢失"为何未收敛"的线索。对走 RYBSOL(`IFRYB≠0`)的发散,归因信息不完整。
|
||
|
||
### 漏洞 6(低):仅 SYNSPEC 场景 final_converged 重判只看 rc
|
||
|
||
runner.rs:611-616,TLUSTY 关闭的纯复算场景,`final_converged = (synspec_rc == 0)`,与漏洞 1 同源,场景更窄。
|
||
|
||
---
|
||
|
||
## 5. 被排除的伪风险
|
||
|
||
**超时 / shutdown 后半成品 fort.7 进 check_fort9 误判收敛 —— 不存在。**
|
||
|
||
`run_child_async_with_timeout` 在超时/shutdown 时返回 `Err`(runner.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.rs(SYNSPEC 产出后,runner.rs:585 附近) |
|
||
| P1 | 漏洞 4 | 收紧物理产物 NaN 阈值:产物准入用 0 行容忍(任意一行 NaN 即否决),而非 10% | conv_check.rs:212 或新增严格版函数 |
|
||
| P1 | 漏洞 2 | runner.rs:417-421:至少把`best_max_relc` 从 `Some(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/:47585;quit :29946-29955;
|
||
PRCHAN :22876-22944;RYBCHN :47445-47598;RESOLV :3724-3903;OUTPUT :14055 起)
|
||
- `synspec/synspec54.f`(主程序 :1-174;OUTPRI :3343-3458;quit :12014-12021;
|
||
脏数据风险点 :4203/:8208/:9935/:18863/:18865/:19891)
|
||
- 判定逻辑:
|
||
- `crates/common/src/runner.rs`(run_model_with_timeout :232-682;漏洞 1/2/5/6 所在)
|
||
- `crates/node/src/reporter.rs`(derive_report_status :38-45;infer_failed_stage :15-27)
|
||
- `crates/common/src/conv_check.rs`(check_fort9 :54-175;atmosphere_has_nan :190-213)
|
||
- 编译配置:`docs/deployment.md:86,90`(确认生产编译无 `-fcheck`/`-ffpe-trap`)
|
||
- 相关设计:`docs/task_engine_decoupling_design.md`(阶段独立配置、半失败判定语义)
|