feat(all): seed_step_stab 稳定化种子链与同族种子轮换、ladder/自适应 Teff/丰度轴延拓三级回退与梯级种子入库复用、热启动首末比豁免、workflow 完成翻转回退链阻塞修复、大气收敛权威统计与 M14 回填

收敛攻坚(81/400 失败点根因与实测,见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md):
- runner: 新增 seed_step_stab 策略链——同物理族(同 Teff/logg/logHe)CNO 邻居种子
  + DPSILG=3.0 λ 算子欠松弛 + POPZER=1E-10 微布居置零(联动 POPZR2/RADZER 同值
  + NITZER=1),针对 20kK He 富大气 He I/II 电离前沿布居极限环
- runner: 三级链内延拓回退(主链与 nl_direct 全败后自动触发):
  · 固定 ladder 步进——plan_ladder_steps 按归一化间隔选轴,Δlogg≤0.25/ΔTeff≤2.5kK,≤4 步
  · 自适应 Teff 延拓——步长 1250K 起、成功 ×1.5 恢复、失败二分至 25K 折叠墙,≤48 步
  · C/N 丰度轴延拓——高温域(Teff>30kK)专用,严格同族种子沿 C(优先)/N 轴 0.2 dex 起步
  延拓阶段 NITER 下限提至 300(慢收敛 waypoint 迭代饥饿误判修复)
- runner: 稳定化多档回退(DPSILG/POPZER 三档互补,联合回收 41/65);
  域门控 Teff≤30kK——高温高金属域实测旋钮致散(17 拍爆至 4e16),域外跳过
- 梯级种子持久化: 收敛中间模型登记 ladder_seeds 随上报落 server seeds 表
  (任务失败也上传,合成名 _ladder 与真实网格点零冲突),簇内相邻失败点自动复用

调度与执行:
- scheduler: 策略解析新增 seed_step_stab 臂——find_exact_family_seed_from_db 严格
  同物理族判定(不做 global 退化,防 ladder 中间种子 ΔTeff≤5000K 误命中),
  排除本点历史已用种子实现重试轮换;链在 stab 耗尽时轮换未试过同族邻居重派
- executor: seed_step_stab 补种子下载(漏列曾致 78 任务假失败,seed_nc 无 fort.8 崩溃);
  Teff>30kK 域外自动降级普通种子链

收敛判据:
- conv_check: 热启动豁免——首拍 max_relc<1(种子已近解)时首末比 1e3 判据数学上
  不可达,豁免后交五重物理硬门槛裁决(修复 nl_ladder 0.038→6e-4 物理全过被误杀);
  冷启动仍受判据门控

workflow 生命周期:
- workflows/tasks: 完成 flip 增加「未消费回退链」阻塞子句——failed 点策略链未耗尽
  或链尾 seed_step_stab 尚有未试过同族种子时不得置 completed(修复最后活跃点 cold
  失败上报抢先 flip、still_running 守卫拦截后续策略永不派发);按 failed_stage 归因
  (synspec 失败行只看 synspec 链,防 stale 审计副本永久卡死)+ json_valid 脏行防护

统计与前端:
- 统计新增权威口径 tlusty_converged(不按策略拆)与 seed_step_stab_converged 分项,
  前端详情页色带/概览卡消费权威总数并新增稳定化青色段(修复 stab 收敛点漏计,
  生产 9137/9216 差额);M14 迁移回填历史 completed 点的 tlusty_status

文档:
- 新增 failed81 POPZER/DPSILG 制胜配方根因分析、Windows 节点经跳板 RDP 运维手册;
  failed400 增补 ladder 生产化实现与第二轮 121 残点实测矩阵
This commit is contained in:
fmq
2026-09-02 19:37:25 +08:00
parent b058e66722
commit f2700031ce
30 changed files with 3045 additions and 49 deletions
+11
View File
@@ -1147,6 +1147,15 @@ pub struct ChainStep {
pub idlte: Option<i32>,
pub iacc: Option<i32>,
pub orelax: Option<f64>,
/// DPSILG(λ 算子欠松弛上限,默认不生效)。难收敛角落(如 20kK He 富大气的
/// He I/II 电离前沿布居极限环)用 3.0 抑制——2026-08-18 实测,见
/// docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md。
#[serde(default)]
pub dpsilg: Option<f64>,
/// POPZER(微布居置零阈值)。设置时同时写 POPZR2/RADZER=同值 + NITZER=1
/// 把可忽略微布居从方程中剔除(官方标准输入恒设 1E-20;难收敛角落用 1E-10)。
#[serde(default)]
pub popzer: Option<f64>,
}
fn default_false_str() -> String {
@@ -1991,6 +2000,8 @@ mod tests {
idlte: None,
iacc: None,
orelax: None,
dpsilg: None,
popzer: None,
};
let input5 = make_input5(&grid, &chain, &yaml_loaded);
// 兜底后应含完整 ions 能级数据(而非只有终止行)
+43 -1
View File
@@ -719,7 +719,14 @@ pub fn check_convergence_trace(
.windows(2)
.all(|w| w[0] >= w[1] * 0.5); // 容忍 Ng 加速的局部反弹(×2 内)
let valid = ratio >= min_ratio;
// 热启动豁免(2026-08-18 修复):首拍 max_relc < 1 说明种子已接近收敛解,
// 首末比在此情形下数学上不可能达到 1e3 量级(首拍被钳在低位),判据失效。
// 误杀实例:t55000_g5.0_he-4_c-3_n-4_o-1 的 nl_ladder 阶段(首 0.038→末 6e-4
// 四项物理硬门全过)被否决为未收敛。真伪收敛的最终裁决交给五重物理硬门槛。
const HOT_START_FIRST_MAX_RELC: f64 = 1.0;
let hot_start_exempt = first < HOT_START_FIRST_MAX_RELC;
let valid = ratio >= min_ratio || hot_start_exempt;
Some(ConvergenceTraceCheckResult {
valid,
first_max_relc: first,
@@ -729,7 +736,13 @@ pub fn check_convergence_trace(
n_iters: itek.len(),
min_ratio,
error: if valid {
if hot_start_exempt && ratio < min_ratio {
Some(format!(
"热启动豁免:首拍 max_relc {first:.1e} < {HOT_START_FIRST_MAX_RELC},首末比判据不适用"
))
} else {
None
}
} else {
Some(format!(
"假收敛排查失败:首末 max_relc 比 {:.1e} < {:.0e}(可能 Ng 加速伪收敛)",
@@ -1509,6 +1522,35 @@ mod tests {
assert!(res.error.is_some());
}
#[test]
fn test_convergence_trace_hot_start_exempt() {
// 热启动豁免(2026-08-18):首拍 < 1(种子已接近解)时首末比数学上达不到 1e3,
// 旧判据误杀(实例 t55000_g5.0_he-4_c-3_n-4_o-1 nl_ladder0.038→6e-4,物理硬门全过)。
let itek: Vec<IterCheck> = (0..7)
.map(|i| IterCheck {
iter: i + 1,
max_relc: 0.038 / (1.0 + i as f64),
n_depths: 50,
})
.collect();
let res = check_convergence_trace(&itek, 1000.0).expect("≥3拍应判定");
assert!(res.valid, "热启动首拍 <1 应回退到物理硬门槛裁决,不应判假收敛");
assert!(res.ratio < 1000.0);
assert!(res.error.is_some(), "豁免时应带豁免说明");
}
#[test]
fn test_convergence_trace_cold_start_still_gated() {
// 冷启动(首拍 >= 1)不受豁免影响:比值不足仍判假收敛嫌疑。
let itek: Vec<IterCheck> = vec![
IterCheck { iter: 1, max_relc: 5.0, n_depths: 50 },
IterCheck { iter: 2, max_relc: 2.0, n_depths: 50 },
IterCheck { iter: 3, max_relc: 0.05, n_depths: 50 },
];
let res = check_convergence_trace(&itek, 1000.0).expect("≥3拍应判定");
assert!(!res.valid, "冷启动首拍 ≥1 且比值 <1000 仍应失败");
}
#[test]
fn test_convergence_trace_too_short() {
// 2拍:无法判定 → None
+2
View File
@@ -232,6 +232,8 @@ mod tests {
idlte: None,
iacc: None,
orelax: None,
dpsilg: None,
popzer: None,
}
}
+45
View File
@@ -223,6 +223,27 @@ impl GridPointParams {
)
}
/// 从规范网格点名解析参数:`t60000_g5.5_he-2_c-4_n-4_o-4` → GridPointParams。
/// 接受可选的 `_ladder` 等后缀(ladder 合成种子名)。解析失败返回 None。
/// 用途:node 端从 seed_point_name 取种子参数(ladder 步进规划)、
/// server 端从上传文件名取中间种子参数。
pub fn parse_point_name(name: &str) -> Option<Self> {
let re = regex::Regex::new(
r"^t(\d+(?:\.\d+)?)_g(-?\d+(?:\.\d+)?)_he(-?\d+(?:\.\d+)?)_c(-?\d+(?:\.\d+)?)_n(-?\d+(?:\.\d+)?)_o(-?\d+(?:\.\d+)?)(?:_.*)?$",
)
.ok()?;
let caps = re.captures(name)?;
let v = |i: usize| -> Option<f64> { caps.get(i)?.as_str().parse::<f64>().ok() };
Some(GridPointParams {
teff: GridAxisValue::from_value(v(1)?),
logg: GridAxisValue::from_value(v(2)?),
loghe: GridAxisValue::from_value(v(3)?),
logc: GridAxisValue::from_value(v(4)?),
logn: GridAxisValue::from_value(v(5)?),
logo: GridAxisValue::from_value(v(6)?),
})
}
/// CNO 对数丰度之和 (`logc + logn + logo`)。
///
/// 注:此数值专门用于网格调度中的 Wave 难度分级与保序分组(对数和越小代表重元素丰度越低,
@@ -832,9 +853,33 @@ pub struct ModelSummary {
/// `.bfac` b 因子合理性校验结果。None = 未做校验。
#[serde(default)]
pub bfac_check: Option<BfacCheckResult>,
/// ladder 步进链的中间收敛模型(node→server 种子持久化清单)。空 = 未触发 ladder
/// 或无合格中间步。中间模型已过收敛 + NaN 否决,可安全入种子库供相邻失败点复用。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ladder_seeds: Vec<LadderSeedInfo>,
pub note: Option<String>,
}
/// ladder 步进链中间种子信息。
///
/// `point_name` 是合成种子名(中间参数 model_name + `_ladder` 后缀),与任何真实
/// 网格点名不冲突;server 端按其中的参数落 seeds 表(bucket 查找按 teff/logg/loghe
/// 数值列匹配,天然覆盖中间参数点)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LadderSeedInfo {
/// 产生该模型的阶段标签(如 `ladder_g6.25`)。
pub label: String,
/// 合成种子名(server 端 seeds.point_name / 磁盘文件名,不含 .7 后缀)。
pub point_name: String,
/// 中间步大气参数(teff/logg 为中间值,丰度同目标点)。
pub teff: f64,
pub logg: f64,
pub loghe: f64,
pub logc: f64,
pub logn: f64,
pub logo: f64,
}
#[cfg(test)]
mod tests {
use super::*;
+46
View File
@@ -82,6 +82,21 @@ pub fn generate_nst_content(chain: &ChainStep, global: &TlustyInput) -> String {
line2.push(format!("ICHANG={}", ichang));
written_keys.insert("ICHANG".to_string());
}
if let Some(dpsilg) = chain.dpsilg {
line2.push(format!("DPSILG={}", dpsilg));
written_keys.insert("DPSILG".to_string());
}
if let Some(popzer) = chain.popzer {
// POPZER 联动组:官方标准输入恒 POPZER=POPZR2=RADZER + NITZER=1
// tests/tlusty/*/.6 回显),缺一则置零机制不完整。
line2.push(format!("POPZER={:.E}", popzer));
line2.push(format!("POPZR2={:.E}", popzer));
line2.push(format!("RADZER={:.E}", popzer));
line2.push("NITZER=1".to_string());
for k in ["POPZER", "POPZR2", "RADZER", "NITZER"] {
written_keys.insert(k.to_string());
}
}
line2.push(format!("IELCOR={}", n.physics.ielcor));
written_keys.insert("IELCOR".to_string());
push_wrapped(&mut out, &line2);
@@ -224,6 +239,8 @@ mod tests {
idlte: None,
iacc: None,
orelax: None,
dpsilg: None,
popzer: None,
}
}
@@ -331,6 +348,35 @@ mod tests {
assert!(!content.contains("FRLMIN"), "FRLMIN=0 不应写出");
}
/// DPSILG/POPZER 稳定化旋钮(2026-08-18seed_step_stab 策略):
/// dpsilg → DPSILG=<val>popzer → POPZER/POPZR2/RADZER 三键同值 + NITZER=1
/// (官方标准输入联动组),且行宽仍受 75 字符保护(自动换行)。
#[test]
fn test_nst_dpsilg_popzer_stabilization() {
let mut chain = chain_nc();
chain.dpsilg = Some(3.0);
chain.popzer = Some(1e-10);
let content = generate_nst_content(&chain, &TlustyInput::default());
assert!(content.contains("DPSILG=3"), "DPSILG 应写出: {}", content);
assert!(content.contains("POPZER=1E-10"), "POPZER 应以 Fortran E 格式写出");
assert!(content.contains("POPZR2=1E-10"));
assert!(content.contains("RADZER=1E-10"));
assert!(content.contains("NITZER=1"), "POPZER 联动 NITZER=1");
for (i, line) in content.lines().enumerate() {
assert!(
line.chars().count() <= 75,
"nst 第 {} 行超宽({} 字符): {}",
i + 1,
line.chars().count(),
line
);
}
// 默认(None)不写出,保持字节级兼容
let plain = generate_nst_content(&chain_nc(), &TlustyInput::default());
assert!(!plain.contains("DPSILG"));
assert!(!plain.contains("POPZER"));
}
/// fmt_real 边界:-0.0 归一为 "0."(审查 Nit-1)。
#[test]
fn test_fmt_real_negative_zero() {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -109,6 +109,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
}
}
+84 -7
View File
@@ -20,7 +20,7 @@ pub async fn execute_task(
result_dir: &Path,
task: &TaskSpec,
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
) -> Result<(ModelSummary, Option<Vec<u8>>)> {
) -> Result<(ModelSummary, Option<Vec<u8>>, Vec<(String, Vec<u8>)>)> {
info!(
"开始执行计算任务 {} (网格点: {})",
task.task_id, task.point_name
@@ -68,7 +68,11 @@ pub async fn execute_task(
// 全新 slot 内不可能已有目标大气;重试任务的 slot 亦全新(task_id 唯一)。
let tlusty_enabled = task.tlusty_config.enabled;
let current_strategy = task.tlusty_config.current_strategy("cold_run");
let needs_seed_download = (tlusty_enabled && current_strategy == "seed_step")
// seed_step_stab2026-08-18 稳定化种子步进)同样是种子热启动链,必须下载种子;
// 2026-08-19 生产事故:漏列该策略导致种子未下载,seed_nc 无 fort.8 直接
// EOF 崩溃(rc=2),78 个任务全部假失败。
let needs_seed_download = (tlusty_enabled
&& matches!(current_strategy, "seed_step" | "seed_step_stab"))
|| (!tlusty_enabled && task.synspec_config.enabled);
if needs_seed_download {
@@ -223,6 +227,7 @@ pub async fn execute_task(
&task.tlusty_chain_params,
&task.seed_chain_params,
&task.task_id.to_string(),
task.params.teff.value(),
);
// TLUSTY 输入文件全局参数(NFREAD/ions 表/nst extra_keys 等)。
// None → runner 用代码内硬编码默认(向后兼容)。
@@ -239,6 +244,12 @@ pub async fn execute_task(
}
}
});
// 种子参数(ladder 步进规划需要种子与目标的参数差):从权威 seed_point_name
// 解析。解析失败(命名不符)→ None → ladder 回退不触发,安全降级。
let seed_params = task
.seed_point_name
.as_deref()
.and_then(common::models::GridPointParams::parse_point_name);
let summary = runner
.run_model_with_timeout(
&task.params,
@@ -248,6 +259,7 @@ pub async fn execute_task(
current_strategy,
Some(chain),
seed_atmos_path.as_deref(),
seed_params.as_ref(),
synspec_cfg.as_ref(),
// 阶段独立配置开关(见 docs/task_engine_decoupling_design.md §5)。
task.tlusty_config.enabled,
@@ -303,7 +315,30 @@ pub async fn execute_task(
slot_work_dir.display()
);
Ok((summary, seed_bytes))
// ladder 中间梯级种子字节:即便最终失败也上报(簇内相邻失败点可复用已收敛梯级)。
// 阶段快照命名 <name>.<label>.7execute_tlusty_stage 写入 model_dir)。
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
if !summary.ladder_seeds.is_empty() {
let model_sub_dir = slot_work_dir.join(&summary.name);
for info in &summary.ladder_seeds {
let path = model_sub_dir.join(format!("{}.{}.7", summary.name, info.label));
if path.is_file() {
if let Ok(bytes) = tokio::fs::read(&path).await {
info!(
"读取 ladder 中间种子 {}{}{} 字节)",
info.point_name,
path.display(),
bytes.len()
);
ladder_seed_files.push((info.point_name.clone(), bytes));
}
} else {
warn!("ladder 种子快照缺失: {}", path.display());
}
}
}
Ok((summary, seed_bytes, ladder_seed_files))
}
/// 清理任务在 Node 端的沙盒目录
@@ -518,6 +553,7 @@ fn resolve_execution_chain(
tlusty_chain_params: &Option<serde_json::Value>,
seed_chain_params: &Option<serde_json::Value>,
task_id: &str,
teff: f64,
) -> Vec<ChainStep> {
if current_strategy == "seed_step" {
seed_chain_params
@@ -534,6 +570,27 @@ fn resolve_execution_chain(
})
.filter(|c| !c.is_empty())
.unwrap_or_else(common::runner::default_seed_chain)
} else if current_strategy == "seed_step_stab" {
// 稳定化种子步进:POPZER+DPSILG 稳定化参数即本策略的实质——用户自定义链
// tlusty_chain/seed_chain)不带这些旋钮,热启动会重演发散,故不采纳、
// 恒用 default_seed_stab_chain。
// 2026-08-21 域门控(60k 攻坚实测,docs/failed81_...md §十一):DPSILG/POPZER
// 族旋钮仅在低温 He 富域(Teff≤30kK)有效;高温高金属域(60k/g5.0 o-1 角)
// 实测自复现收敛种子加档后 17 拍爆到 4e16——致散。域外改用普通种子链,
// 交给 runner 的自适应 Teff 延拓处理。
if teff <= 30000.0 {
common::runner::default_seed_stab_chain()
} else {
info!(
"任务 {}Teff={}>30kK 域外,seed_step_stab 降级为普通种子链(稳定化旋钮高温致散)",
task_id, teff
);
seed_chain_params
.as_ref()
.and_then(|v| serde_json::from_value::<Vec<ChainStep>>(v.clone()).ok())
.filter(|c| !c.is_empty())
.unwrap_or_else(common::runner::default_seed_chain)
}
} else {
tlusty_chain_params
.as_ref()
@@ -585,6 +642,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
}
}
@@ -604,7 +662,7 @@ mod tests {
]);
// seed_chain_params=None → 走 default_seed_chaintlusty_chain_params 被忽略。
let chain = resolve_execution_chain("seed_step", &Some(custom_cold), &None, "t1");
let chain = resolve_execution_chain("seed_step", &Some(custom_cold), &None, "t1", 20000.0);
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
// 首步必须是非灰 LTEltgray=F),否则会删 fort.8 丢弃种子。
assert_eq!(chain[0].ltgray, "F");
@@ -618,13 +676,32 @@ mod tests {
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100, "orelax": 0.5},
]);
let chain = resolve_execution_chain("seed_step", &None, &Some(custom_seed), "t1b");
let chain = resolve_execution_chain("seed_step", &None, &Some(custom_seed), "t1b", 20000.0);
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
// 用户配置的 orelax 生效。
assert_eq!(chain[0].orelax, Some(0.3));
assert_eq!(chain[1].orelax, Some(0.5));
}
/// seed_step_stab 温度域门控(2026-08-2160k 攻坚):
/// Teff≤30kK 用稳定化链(全步带 DPSILG/POPZER);
/// 高温域(60k/g5.0 高金属角实测旋钮致散)降级为普通种子链。
#[test]
fn seed_step_stab_teff_domain_gating() {
let cold = resolve_execution_chain("seed_step_stab", &None, &None, "hot1", 20000.0);
assert!(
cold.iter().all(|s| s.dpsilg.is_some() && s.popzer.is_some()),
"低温域 seed_step_stab 应带稳定化参数"
);
let hot = resolve_execution_chain("seed_step_stab", &None, &None, "hot2", 60000.0);
assert!(
hot.iter().all(|s| s.dpsilg.is_none() && s.popzer.is_none()),
"高温域 seed_step_stab 应降级为普通种子链(不带稳定化旋钮)"
);
assert_eq!(labels(&hot), vec!["seed_nc", "nl"]);
}
#[test]
fn cold_run_uses_custom_chain_when_provided() {
let custom = serde_json::json!([
@@ -633,13 +710,13 @@ mod tests {
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100},
]);
let chain = resolve_execution_chain("cold_run", &Some(custom), &None, "t2");
let chain = resolve_execution_chain("cold_run", &Some(custom), &None, "t2", 20000.0);
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
}
#[test]
fn cold_run_falls_back_to_default_when_no_custom_chain() {
let chain = resolve_execution_chain("cold_run", &None, &None, "t3");
let chain = resolve_execution_chain("cold_run", &None, &None, "t3", 20000.0);
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
}
+25 -2
View File
@@ -50,12 +50,22 @@ fn derive_report_status(s: &ModelSummary) -> TaskStatus {
}
}
/// executor 返回值:(summary, 最终种子字节, ladder 中间梯级种子字节列表)。
pub type ExecOutcome = Result<
(
ModelSummary,
Option<Vec<u8>>,
Vec<(String, Vec<u8>)>,
),
String,
>;
pub async fn report_result(
client: &Client,
server_url: &str,
node_id: &str,
task: &TaskSpec,
exec_res: Result<(ModelSummary, Option<Vec<u8>>), String>,
exec_res: ExecOutcome,
) -> Result<()> {
let report_url = format!("{}/api/task/report", server_url);
@@ -69,8 +79,9 @@ pub async fn report_result(
summary_json,
seed_bytes,
failed_stage,
ladder_seed_files,
) = match exec_res {
Ok((s, s_bytes)) => (
Ok((s, s_bytes, ladder_files)) => (
derive_report_status(&s),
s.result_valid,
s.final_max_relc,
@@ -80,6 +91,7 @@ pub async fn report_result(
serde_json::to_string(&s).unwrap_or_default(),
s_bytes,
infer_failed_stage(task, &s),
ladder_files,
),
Err(e) => (
TaskStatus::Failed,
@@ -91,6 +103,7 @@ pub async fn report_result(
serde_json::json!({"error": e}).to_string(),
None,
None,
Vec::new(),
),
};
@@ -128,6 +141,15 @@ pub async fn report_result(
}
}
// ladder 中间梯级种子:即便最终任务失败也上传(executor 只收集收敛+无 NaN
// 的中间模型),server 端落 seeds 表供相邻失败点复用。
for (seed_name, bytes) in &ladder_seed_files {
let part = Part::bytes(bytes.clone())
.file_name(format!("{}.7", seed_name))
.mime_str("application/octet-stream")?;
form = form.part("ladder_seed", part);
}
match client.post(&report_url).multipart(form).send().await {
Ok(resp) if resp.status().is_success() => {
info!(
@@ -239,6 +261,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
}
}
+1 -1
View File
@@ -542,7 +542,7 @@ impl NodeWorker {
// 严重失败(runner 抛 Err)时 summary_opt=None,此时回退到 task.point_name
// (二者均源自 params.model_name())作为归档目录名,确保失败任务的
// 排错日志也能落盘而非随沙盒删除丢失。
let summary_opt = res.as_ref().ok().map(|(s, _)| s.clone());
let summary_opt = res.as_ref().ok().map(|(s, _, _)| s.clone());
let result_name = summary_opt
.as_ref()
.map(|s| s.name.clone())
+54
View File
@@ -81,6 +81,8 @@ pub async fn report_task(
) -> Result<impl IntoResponse, crate::api::AppError> {
let mut report_json: Option<TaskReport> = None;
let mut seed_file_data: Option<Vec<u8>> = None;
// ladder 中间梯级种子(可重复字段):文件名(去 .7)即合成种子名,参数从名称解析。
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
let mut multipart_error = false;
// 遍历全部 multipart 字段。旧实现 `while let Ok(Some(field))` 在首个字段读取错误时
@@ -114,6 +116,24 @@ pub async fn report_task(
multipart_error = true;
}
}
} else if field_name == "ladder_seed" {
// 文件名即合成种子名(<model_name>_ladder.7
let file_name = field
.file_name()
.unwrap_or("")
.trim_end_matches(".7")
.to_string();
match field.bytes().await {
Ok(bytes) => {
if !file_name.is_empty() {
ladder_seed_files.push((file_name, bytes.to_vec()));
}
}
Err(e) => {
warn!("读取 ladder_seed 字段失败: {}", e);
multipart_error = true;
}
}
}
}
Ok(None) => break,
@@ -329,6 +349,40 @@ pub async fn report_task(
}
}
}
// ladder 中间梯级种子持久化(2026-08-17):node 端仅上报「收敛 + 无 NaN」的
// 中间模型;此处原子落盘 + 按 _ladder 合成名入 seeds 表(ON CONFLICT 仅刷新
// file_path,绝不触碰真实网格点种子行)。名称解析失败 → 丢弃并告警(防脏名)。
for (seed_name, bytes) in &ladder_seed_files {
let Some(lp) = common::models::GridPointParams::parse_point_name(seed_name) else {
warn!("ladder 种子名 {} 无法解析参数,丢弃", seed_name);
continue;
};
let ladder_dir = Path::new(&state.seeds_dir).join(seed_name);
if fs::create_dir_all(&ladder_dir).await.is_err() {
warn!("ladder 种子目录创建失败: {}", ladder_dir.display());
continue;
}
let tmp = ladder_dir.join(format!("{}.7.{}.tmp", seed_name, uuid::Uuid::new_v4().simple()));
let path = ladder_dir.join(format!("{}.7", seed_name));
if fs::write(&tmp, bytes).await.is_ok() && fs::rename(&tmp, &path).await.is_ok() {
if let Err(e) = state
.db
.insert_seed_named(seed_name, &lp, &path.to_string_lossy())
.await
{
warn!("ladder 种子 {} 入库失败: {}", seed_name, e);
} else {
info!(
"ladder 中间种子入库: {} (t{} g{} he{})",
seed_name,
lp.teff.value(),
lp.logg.value(),
lp.loghe.value()
);
}
}
}
}
if state_changed
+15 -2
View File
@@ -576,8 +576,10 @@ impl Database {
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS seed_step_stab_converged,
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
@@ -586,7 +588,7 @@ impl Database {
params![name],
|r| {
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11), n(12), n(13)))
},
),
None => conn.query_row(
@@ -597,8 +599,10 @@ impl Database {
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS seed_step_stab_converged,
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
@@ -607,7 +611,7 @@ impl Database {
[],
|r| {
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11), n(12), n(13)))
},
),
}?;
@@ -618,8 +622,10 @@ impl Database {
running,
completed,
failed,
tlusty_converged,
cold_run_converged,
seed_step_converged,
seed_step_stab_converged,
tlusty_failed,
synspec_converged,
synspec_failed,
@@ -633,8 +639,13 @@ impl Database {
"running": running,
"completed": completed,
"failed": failed,
// tlusty_converged2026-08-25):大气收敛的权威总数(不按 method 拆分)。
// 前端「N 大气收敛」此前用 cold+seed 之和——seed_step_stab2026-08-18 引入)
// 收敛的 70 点被漏计(生产 9137/9216 差额主因)。
"tlusty_converged": tlusty_converged,
"cold_run_converged": cold_run_converged,
"seed_step_converged": seed_step_converged,
"seed_step_stab_converged": seed_step_stab_converged,
"tlusty_failed": tlusty_failed,
"synspec_converged": synspec_converged,
"synspec_failed": synspec_failed,
@@ -733,6 +744,8 @@ impl Database {
failed,
cold_run_converged: g("cold_run_converged"),
seed_step_converged: g("seed_step_converged"),
tlusty_converged: g("tlusty_converged"),
seed_step_stab_converged: g("seed_step_stab_converged"),
tlusty_failed: g("tlusty_failed"),
synspec_converged: g("synspec_converged"),
synspec_failed: g("synspec_failed"),
+481
View File
@@ -710,6 +710,12 @@ pub struct WorkflowListStats {
pub running: i64,
pub cold_run_converged: i64,
pub seed_step_converged: i64,
/// TLUSTY 阶段收敛总数(tlusty_status='converged')——权威口径,语义同
/// WorkflowStats.tlusty_converged2026-08-25 补,见彼处注释)。
#[serde(default)]
pub tlusty_converged: i64,
#[serde(default)]
pub seed_step_stab_converged: i64,
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
pub tlusty_failed: i64,
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
@@ -754,6 +760,15 @@ pub struct WorkflowStats {
pub failed: i64,
pub cold_run_converged: i64,
pub seed_step_converged: i64,
/// TLUSTY 阶段收敛总数(tlusty_status='converged',不按 method 拆分)——权威口径。
/// 前端「N 大气收敛」应消费此字段;cold/seed/stab 为策略细分。2026-08-25 补:
/// 此前前端用 cold+seed 之和,seed_step_stab2026-08-18 引入)收敛点被漏计
/// (生产 9137/9216 差额主因)。
#[serde(default)]
pub tlusty_converged: i64,
/// TLUSTY 阶段以 seed_step_stab 策略收敛的点数(稳定化/waypoint 种子链)。
#[serde(default)]
pub seed_step_stab_converged: i64,
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
pub tlusty_failed: i64,
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
@@ -1199,6 +1214,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
db.upsert_point_summary(&summary.name, "wf_a", &summary, "seed_step")
@@ -1215,12 +1231,16 @@ mod tests {
assert_eq!(all["completed"], 1);
assert_eq!(all["cold_run_converged"], 0);
assert_eq!(all["seed_step_converged"], 1);
// tlusty_converged2026-08-25):不按 method 拆分的权威总数。
assert_eq!(all["tlusty_converged"], 1);
assert_eq!(all["seed_step_stab_converged"], 0);
// 单工作流 wf_a1 pending + 1 seed_step 收敛
let a = db.get_grid_summary_stats(Some("wf_a")).await.unwrap();
assert_eq!(a["total"], 2);
assert_eq!(a["pending"], 1);
assert_eq!(a["queued"], 0);
assert_eq!(a["seed_step_converged"], 1);
assert_eq!(a["tlusty_converged"], 1);
// 不存在的工作流:0
let none = db
.get_grid_summary_stats(Some("nonexistent"))
@@ -1264,6 +1284,120 @@ mod tests {
assert_eq!(m.unwrap().name, exact.model_name());
}
/// find_exact_family_seed_from_db 严格同物理族判定(2026-08-19 生产修复回归):
/// ladder 中间种子(如 teff=22500)即便 CNO 距离为 0,也因 Teff 不同被排除;
/// 同 Teff/logg/logHe 的 CNO 邻居正常命中;排除目标自身名。
#[tokio::test]
async fn test_find_exact_family_seed_strict_same_family() {
let temp_dir = tempfile::tempdir().unwrap();
let db = Database::new(&temp_dir.path().join("seed_stab_db.db").to_string_lossy())
.await
.unwrap();
let target = GridPointParams {
teff: 20000.0.into(),
logg: 6.5.into(),
loghe: 2.0.into(),
logc: (-4.0).into(),
logn: (-3.0).into(),
logo: (-4.0).into(),
};
// ladder 中间种子:同 logg/logHe、CNO 完全一致,但 teff=22500 —— 必须排除。
let ladder = GridPointParams {
teff: 22500.0.into(),
logg: 6.5.into(),
loghe: 2.0.into(),
logc: (-4.0).into(),
logn: (-3.0).into(),
logo: (-4.0).into(),
};
// 合法 CNO 邻居:同 Teff/logg/logHe,Δo=1。
let cno_neighbor = GridPointParams {
teff: 20000.0.into(),
logg: 6.5.into(),
loghe: 2.0.into(),
logc: (-4.0).into(),
logn: (-3.0).into(),
logo: (-3.0).into(),
};
db.insert_seed(&ladder, "/tmp/ladder.7").await.unwrap();
db.insert_seed(&cno_neighbor, "/tmp/neighbor.7").await.unwrap();
let m = db
.find_exact_family_seed_from_db(&target, &[target.model_name()])
.await
.unwrap();
assert!(m.is_some(), "应命中 CNO 邻居");
assert_eq!(
m.unwrap().name,
cno_neighbor.model_name(),
"不得选 teff 不同的 ladder 中间种子(即便其 CNO 距离为 0)"
);
// 无严格同族候选时返回 None(不退化到 global)。
let lonely = GridPointParams {
teff: 30000.0.into(),
logg: 5.0.into(),
loghe: (-2.0).into(),
logc: (-2.0).into(),
logn: (-2.0).into(),
logo: (-2.0).into(),
};
let none = db
.find_exact_family_seed_from_db(&lonely, &[lonely.model_name()])
.await
.unwrap();
assert!(none.is_none());
}
/// 种子轮换回归(2026-08-20 修复):find_exact_family_seed_from_db 排除列表
/// 应使调用方在重试间轮换到下一个未试过的同族 CNO 邻居;全部排除后返回 None。
#[tokio::test]
async fn test_find_exact_family_seed_rotation() {
let temp_dir = tempfile::tempdir().unwrap();
let db = Database::new(&temp_dir.path().join("seed_rot_db.db").to_string_lossy())
.await
.unwrap();
let base = |logc: f64, logn: f64, logo: f64| GridPointParams {
teff: 20000.0.into(),
logg: 6.0.into(),
loghe: 2.0.into(),
logc: logc.into(),
logn: logn.into(),
logo: logo.into(),
};
let target = base(-3.0, -3.0, -1.0);
let n1 = base(-3.0, -2.0, -1.0);
let n2 = base(-3.0, -3.0, -2.0);
db.insert_seed(&n1, "/tmp/rot_n1.7").await.unwrap();
db.insert_seed(&n2, "/tmp/rot_n2.7").await.unwrap();
let self_name = target.model_name();
// 首次:选距离最近的未排除邻居。
let first = db
.find_exact_family_seed_from_db(&target, &[self_name.clone()])
.await
.unwrap()
.expect("应命中 CNO 邻居");
// 排除首个后:轮换到另一个邻居。
let second = db
.find_exact_family_seed_from_db(&target, &[self_name, first.name.clone()])
.await
.unwrap()
.expect("排除首个邻居后应轮换到第二个邻居");
assert_ne!(first.name, second.name);
// 全部排除后:None(终态 failed,不死循环)。
let none = db
.find_exact_family_seed_from_db(
&target,
&[target.model_name(), first.name, second.name],
)
.await
.unwrap();
assert!(none.is_none(), "邻居全部试过后应返回 None");
}
/// "种子回退仅一次"守卫语义(2026-08-02 涡旋事故定稿):has_seed_step_attempt
/// 统计**一切** seed_step 行(含 pending)。pending 行在新架构下只有两种来源:
/// (a) 真在途(排队/已领用)——计数它正是对在途回退的去重,挡住救援途中迟到失败
@@ -3265,6 +3399,347 @@ mod tests {
assert_eq!(item.status, "completed");
}
/// 完成 flip 的「未消费回退链」阻塞回归(2026-08-22 修复):
/// 最后一个活跃点失败时,其最新 failed 行策略链弹掉失败首项后仍有顺位
/// (长度 >1)→ workflow 不得置 completed(否则 trigger_strategy_fallback
/// 的 still_running 守卫拦截,链上 seed_step/seed_step_stab 永不派发;
/// 生产实证 sdB_cno/t60000_g5.0_he-4_c-4_n-4_o-18-22 04:00:50)。
/// 链耗尽(长度 1)后放行翻转。
#[tokio::test]
async fn test_workflow_flip_blocked_by_unconsumed_fallback_chain() {
let temp_dir = tempfile::tempdir().unwrap();
let db = Database::new(
&temp_dir
.path()
.join("flip_block_db.db")
.to_string_lossy(),
)
.await
.unwrap();
let params = GridPointParams {
teff: 60000.0.into(),
logg: 5.0.into(),
loghe: (-4.0).into(),
logc: (-4.0).into(),
logn: (-4.0).into(),
logo: (-1.0).into(),
};
let name = params.model_name();
db.upsert_workflow("wf_fb2", None, "config", "idle")
.await
.unwrap();
db.update_workflow_status("wf_fb2", "running").await.unwrap();
db.upsert_grid_point(&params, 0, "wf_fb2").await.unwrap();
// 点终态 failed、无 pending/queued/running——旧条件已满足 flip。
db.update_grid_status(&name, GridPointStatus::Failed, "wf_fb2")
.await
.unwrap();
// 最新 failed 行携带 3 顺位链(cold_run 刚失败,尚余 seed_step/seed_step_stab)。
let t1 = uuid::Uuid::new_v4();
db.insert_task(&common::models::TaskSpec {
task_id: t1,
point_name: name.clone(),
params: params.clone(),
timeout_sec: 7200,
workflow_name: Some("wf_fb2".to_string()),
wave: 0,
tlusty_config: PhaseConfig {
strategies: vec![
"cold_run".to_string(),
"seed_step".to_string(),
"seed_step_stab".to_string(),
],
..PhaseConfig::default_tlusty()
},
..Default::default()
})
.await
.unwrap();
{
let pool = db.pool.clone();
let tid = t1.to_string();
tokio::task::spawn_blocking(move || {
let conn = pool.get().unwrap();
conn.execute(
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
WHERE task_id = ?1",
params![tid],
)
.unwrap();
})
.await
.unwrap();
}
db.sync_all_running_workflows_completion().await.unwrap();
let wf = db.get_workflow("wf_fb2").await.unwrap().unwrap();
assert_eq!(
wf.status, "running",
"存在未消费回退链的 failed 点时,workflow 不应翻转 completed"
);
// 回退推进到链尾(更新的 failed 行仅剩单顺位)→ 阻塞解除,翻转放行。
let t2 = uuid::Uuid::new_v4();
db.insert_task(&common::models::TaskSpec {
task_id: t2,
point_name: name.clone(),
params: params.clone(),
timeout_sec: 7200,
workflow_name: Some("wf_fb2".to_string()),
wave: 0,
tlusty_config: PhaseConfig {
strategies: vec!["seed_step_stab".to_string()],
..PhaseConfig::default_tlusty()
},
..Default::default()
})
.await
.unwrap();
{
let pool = db.pool.clone();
let tid = t2.to_string();
tokio::task::spawn_blocking(move || {
let conn = pool.get().unwrap();
conn.execute(
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
params![tid],
)
.unwrap();
})
.await
.unwrap();
}
db.sync_all_running_workflows_completion().await.unwrap();
let wf2 = db.get_workflow("wf_fb2").await.unwrap().unwrap();
assert_eq!(
wf2.status, "completed",
"链耗尽(长度 1)后应恢复完成翻转"
);
}
/// 完成 flip 的「种子轮换待执行」阻塞回归(2026-08-22 审查补充):
/// 链尾 [seed_step_stab] 失败 + 存在未试过的同物理族干净种子 → 不翻转
/// (否则 rotation 臂被 still_running 守卫吞掉,waypoint 只有一次机会);
/// 同族种子全部用过后放行翻转。
#[tokio::test]
async fn test_workflow_flip_blocked_by_seed_rotation_pending() {
let temp_dir = tempfile::tempdir().unwrap();
let db = Database::new(
&temp_dir
.path()
.join("flip_rot_db.db")
.to_string_lossy(),
)
.await
.unwrap();
let params = GridPointParams {
teff: 60000.0.into(),
logg: 5.0.into(),
loghe: (-4.0).into(),
logc: (-4.0).into(),
logn: (-4.0).into(),
logo: (-1.0).into(),
};
let name = params.model_name();
db.upsert_workflow("wf_rot", None, "config", "idle")
.await
.unwrap();
db.update_workflow_status("wf_rot", "running").await.unwrap();
db.upsert_grid_point(&params, 0, "wf_rot").await.unwrap();
db.update_grid_status(&name, GridPointStatus::Failed, "wf_rot")
.await
.unwrap();
// 链尾 [seed_step_stab] 失败行。
let t1 = uuid::Uuid::new_v4();
db.insert_task(&common::models::TaskSpec {
task_id: t1,
point_name: name.clone(),
params: params.clone(),
timeout_sec: 7200,
workflow_name: Some("wf_rot".to_string()),
wave: 0,
tlusty_config: PhaseConfig {
strategies: vec!["seed_step_stab".to_string()],
..PhaseConfig::default_tlusty()
},
..Default::default()
})
.await
.unwrap();
{
let pool = db.pool.clone();
let tid = t1.to_string();
tokio::task::spawn_blocking(move || {
let conn = pool.get().unwrap();
conn.execute(
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
WHERE task_id = ?1",
params![tid],
)
.unwrap();
})
.await
.unwrap();
}
// 无同族种子 → 无可轮换 → 放行翻转。
db.sync_all_running_workflows_completion().await.unwrap();
assert_eq!(
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
"completed",
"无同族候选种子时链尾失败应放行翻转"
);
// 重启场景:workflow 回 running,注入一个未用过的同族干净种子。
db.update_workflow_status("wf_rot", "running").await.unwrap();
let neighbor = GridPointParams {
logn: (-3.489798).into(),
..params.clone()
};
db.insert_seed_named(
"t60000_g5_he-4_c-4_n-3.489798_o-1_ladder",
&neighbor,
"/tmp/seed.7",
)
.await
.unwrap();
db.sync_all_running_workflows_completion().await.unwrap();
assert_eq!(
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
"running",
"存在未试过的同族种子时,链尾 seed_step_stab 失败应阻塞翻转(rotation 待执行)"
);
// 轮换消费该种子(任务行携带 seed_point_name)→ 候选耗尽 → 放行。
let t2 = uuid::Uuid::new_v4();
db.insert_task(&common::models::TaskSpec {
task_id: t2,
point_name: name.clone(),
params: params.clone(),
seed_point_name: Some("t60000_g5_he-4_c-4_n-3.489798_o-1_ladder".to_string()),
timeout_sec: 7200,
workflow_name: Some("wf_rot".to_string()),
wave: 0,
tlusty_config: PhaseConfig {
strategies: vec!["seed_step_stab".to_string()],
..PhaseConfig::default_tlusty()
},
..Default::default()
})
.await
.unwrap();
{
let pool = db.pool.clone();
let tid = t2.to_string();
tokio::task::spawn_blocking(move || {
let conn = pool.get().unwrap();
conn.execute(
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
params![tid],
)
.unwrap();
})
.await
.unwrap();
}
db.sync_all_running_workflows_completion().await.unwrap();
assert_eq!(
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
"completed",
"同族种子全部用过(无新候选)后应放行翻转"
);
}
/// 完成 flip 的阶段归因回归(2026-08-22 审查补充):synspec 失败行的
/// tlusty_strategies 是完整审计副本(scheduler.rs synspec 回退刻意保留),
/// 阻塞判定必须按 failed_stage='synspec' 只看 synspec 链——否则 synspec
/// 工作流会因 stale 审计副本永久卡 running。
#[tokio::test]
async fn test_workflow_flip_stage_attribution_ignores_tlusty_audit_copy() {
let temp_dir = tempfile::tempdir().unwrap();
let db = Database::new(
&temp_dir
.path()
.join("flip_syn_db.db")
.to_string_lossy(),
)
.await
.unwrap();
let params = GridPointParams {
teff: 30000.0.into(),
logg: 5.0.into(),
loghe: (-2.0).into(),
logc: (-2.0).into(),
logn: (-2.0).into(),
logo: (-2.0).into(),
};
let name = params.model_name();
db.upsert_workflow("wf_syn2", None, "config", "idle")
.await
.unwrap();
db.update_workflow_status("wf_syn2", "running").await.unwrap();
db.upsert_grid_point(&params, 0, "wf_syn2").await.unwrap();
db.update_grid_status(&name, GridPointStatus::Failed, "wf_syn2")
.await
.unwrap();
// synspec 失败行:synspec 链已耗尽(长度 1),但 tlusty 链是完整审计
// 副本(长度 3)——阶段归因下不应阻塞。
let t1 = uuid::Uuid::new_v4();
db.insert_task(&common::models::TaskSpec {
task_id: t1,
point_name: name.clone(),
params: params.clone(),
timeout_sec: 7200,
workflow_name: Some("wf_syn2".to_string()),
wave: 0,
tlusty_config: PhaseConfig {
strategies: vec![
"cold_run".to_string(),
"seed_step".to_string(),
"seed_step_stab".to_string(),
],
..PhaseConfig::default_tlusty()
},
..Default::default()
})
.await
.unwrap();
{
let pool = db.pool.clone();
let tid = t1.to_string();
tokio::task::spawn_blocking(move || {
let conn = pool.get().unwrap();
conn.execute(
"UPDATE tasks SET status = 'failed', failed_stage = 'synspec', \
synspec_strategies = '[\"standard\"]', completed_at = datetime('now') \
WHERE task_id = ?1",
params![tid],
)
.unwrap();
})
.await
.unwrap();
}
db.sync_all_running_workflows_completion().await.unwrap();
assert_eq!(
db.get_workflow("wf_syn2").await.unwrap().unwrap().status,
"completed",
"synspec 链已耗尽的失败行,其 tlusty 审计副本不应阻塞完成翻转"
);
}
#[tokio::test]
async fn test_take_pending_node_token_atomic() {
let temp_dir = tempfile::tempdir().unwrap();
@@ -3695,6 +4170,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
let report1 = TaskReport {
@@ -3761,6 +4237,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
let report2 = TaskReport {
@@ -3927,6 +4404,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
let report1 = TaskReport {
@@ -3984,6 +4462,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: Some("synspec 失败".into()),
};
let report2 = TaskReport {
@@ -4108,6 +4587,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
let report1 = TaskReport {
@@ -4183,6 +4663,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
let report2 = TaskReport {
+56
View File
@@ -196,4 +196,60 @@ impl Database {
Ok(None)
}
}
/// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子,供
/// `seed_step_stab` 稳定化策略使用(2026-08-18docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md)。
///
/// 与 `find_best_seed_from_db` 的区别:不做 global 退化——稳定化配方的验证前提
/// 是种子与目标仅差 CNO 丰度(微扰),跨 Teff/logg 的种子不在其适用域内。
/// `exclude` 排除种子名列表(至少含目标点自身名,防止误把自己旧产物当种子)。
/// 2026-08-20 种子轮换修复:稳定化配方对种子**逐点敏感**——同 CNO 距离的不同
/// 邻居(换 N 还是换 O)收敛性不同。调用方传入本点历史任务已用过的种子名,
/// 使每次 seed_step_stab 重试轮换到下一个未试过的同族邻居,而非重复同一组合。
pub async fn find_exact_family_seed_from_db(
&self,
target: &GridPointParams,
exclude: &[String],
) -> Result<Option<common::seed_finder::SeedMatch>> {
let exact_candidates: Vec<SeedCacheItem> = {
let idx_lock = self.seed_index.read().await;
let keys = SeedBucketKey::from_params(target);
let mut out = Vec::new();
for key in keys {
if let Some(bucket) = idx_lock.get(&key) {
out.extend(bucket.iter().cloned());
}
}
out
};
let mut best: Option<(String, std::path::PathBuf, f64)> = None;
for item in &exact_candidates {
if exclude.iter().any(|ex| &item.point_name == ex) {
continue;
}
// 严格同物理族(2026-08-19 生产修复):必须 Teff/logg/logHe 逐值相同、
// 仅 CNO 不同。不能用 calculate_seed_distance 的 is_exact——其判定
// 容忍 ΔTeff≤5000K(一个网格档),会把 ladder 中间种子(如
// t22500_g6.5_...)当成同族并因 CNO 距离 0 排最前,而稳定化配方
// POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰、同温同重力。
let strict_same_family = (item.params.teff.value() - target.teff.value()).abs()
< 1e-9
&& (item.params.logg.value() - target.logg.value()).abs() < 1e-9
&& (item.params.loghe.value() - target.loghe.value()).abs() < 1e-9;
if !strict_same_family {
continue;
}
let (_, d) = common::seed_finder::calculate_seed_distance(&item.params, target);
if best.is_none() || d < best.as_ref().unwrap().2 {
let path = std::path::PathBuf::from(&item.file_path);
best = Some((item.point_name.clone(), path, d));
}
}
Ok(best.map(|(name, path, distance)| common::seed_finder::SeedMatch {
name,
path,
distance,
}))
}
}
+37 -1
View File
@@ -292,6 +292,33 @@ impl Database {
/// `strategy` 传 Some("seed_step")/Some("cold_run") 时按当前策略过滤(Phase 6 起派生:
/// 比较 `json_extract(tlusty_strategies, '$[0]')`,不再依赖已删除的 task_type 列),
/// None 不过滤。生产调用仅传 None,Some 分支供测试断言用。
/// 本点历史任务已用过的全部种子名(DISTINCT、非 NULL)。
/// 2026-08-20 种子轮换修复:`seed_step_stab` 重试时排除这些种子,
/// 使 `find_exact_family_seed_from_db` 轮换到未试过的同族 CNO 邻居。
pub async fn list_used_seed_names(
&self,
point_name: &str,
workflow_name: &str,
) -> Result<Vec<String>> {
let pool = self.pool.clone();
let point = point_name.to_string();
let wf = workflow_name.to_string();
tokio::task::spawn_blocking(move || -> Result<Vec<String>> {
let conn = pool.get().map_err(|e| anyhow::anyhow!("DB Pool Error: {}", e))?;
let mut stmt = conn.prepare(
"SELECT DISTINCT seed_point_name FROM tasks
WHERE point_name = ?1 AND workflow_name = ?2 AND seed_point_name IS NOT NULL",
)?;
let rows = stmt.query_map(params![point, wf], |r| r.get::<_, String>(0))?;
let mut out = Vec::new();
for r in rows {
out.push(r?);
}
Ok(out)
})
.await?
}
pub async fn has_pending_tasks_for_point(
&self,
point_name: &str,
@@ -621,7 +648,13 @@ impl Database {
// 审查修复 #S1workflow 完成 flip 并入同一事务(原实现在 tx.commit() 后单独 UPDATE
// 崩溃窗口期 task 已终态但 workflow 卡 running;且 `let _ =` 丢弃 I/O 错误)。
// 现在事务内更新,与 task/grid_points 结算原子提交,错误正常传播。
//
// 2026-08-22 修复:「无 pending/queued/running」之外还须无「未消费回退链」的
// failed 点(UNCONSUMED_FALLBACK_BLOCKER_SQL)——否则最后一个活跃点 cold_run
// 失败的这份上报会先把 workflow 置 completed,紧随其后的
// trigger_strategy_fallback 被 still_running 守卫拦截,链上后续策略永不派发。
tx.execute(
&format!(
"UPDATE workflows
SET status = 'completed', updated_at = datetime('now')
WHERE name = ?1
@@ -631,7 +664,10 @@ impl Database {
SELECT 1 FROM grid_points
WHERE workflow_name = workflows.name
AND status IN ('pending', 'queued', 'running')
)",
)
{}",
super::workflows::UNCONSUMED_FALLBACK_BLOCKER_SQL
),
params![wf],
)?;
+87 -5
View File
@@ -2,6 +2,78 @@
//! `impl Database` 的 工作流 域方法。共享基础设施(Database struct、连接管理、类型、helper)见父模块 `super`crate::db)。
use super::*;
/// 工作流完成 flip 的「未消费回退」阻塞子句(两条 flip SQL 共用:
/// 本文件后台对账 + tasks.rs `record_task_report` 事务内 flip)。
///
/// 2026-08-22 修复:最后一批活跃点失败时,flip 只看「无 pending/queued/running」,
/// 而 failed 点的失败上报**先于**策略链回退被处理——workflow 被提前置 completed
/// `trigger_strategy_fallback` 的 still_running 守卫随即拦截,cold_run 之后的
/// seed_step/seed_step_stab 永不派发(生产实证:sdB_cno 仅剩
/// t60000_g5.0_he-4_c-4_n-4_o-1 时 cold 失败即终局)。
///
/// 阻塞语义(对每个 failed 点取最新 failed/timeout 任务行,行选择口径镜像
/// `pop_stage_strategy_for_fallback`ORDER BY created_at DESC, rowid DESC):
///
/// 1. **未消费策略链**:按该行 failed_stage 归因(镜像 pop 的 stage→列映射,
/// NULL 归因 tlusty)取对应策略链,json 长度 >1——弹掉刚失败首项后仍有
/// 顺位。链随每次失败上报严格变短,终会耗尽,不会永久阻塞。
/// 2. **种子轮换待执行**2026-08-22 审查补充):链尾 [`seed_step_stab`] 失败
/// 且仍存在未试过的同物理族干净种子(teff/logg/loghe 逐值相等、is_clean=1、
/// 非自身、不在本点历史 seed_point_name 集合内)——镜像 scheduler.rs
/// rotation 臂(trigger_strategy_fallback 内)的派发条件,否则链尾失败时
/// flip 仍会先于 rotation 吞掉换种重试。轮换每次消耗一个种子,used 集合
/// 单调增长,终会耗尽同族种子,不会永久阻塞。
///
/// JSON 防护:json_array_length/json_extract 对非法 JSON **抛错**而非返回 NULL
/// SQLite 3.45 实测)——若不加防护,一条脏行会让 record_task_report 整个
/// 结算事务失败。列 NOT NULL DEFAULT 且唯一写入方是 serde_json,正常不可达;
/// json_valid 守卫下脏行按长度 0 处理(保守放行翻转)。
pub(crate) const UNCONSUMED_FALLBACK_BLOCKER_SQL: &str = "AND NOT EXISTS (
SELECT 1 FROM grid_points gp
JOIN tasks t
ON t.point_name = gp.name
AND t.workflow_name = gp.workflow_name
AND t.status IN ('failed', 'timeout')
AND t.rowid = (
SELECT t2.rowid FROM tasks t2
WHERE t2.point_name = gp.name
AND t2.workflow_name = gp.workflow_name
AND t2.status IN ('failed', 'timeout')
ORDER BY t2.created_at DESC, t2.rowid DESC LIMIT 1
)
WHERE gp.workflow_name = workflows.name
AND gp.status = 'failed'
AND (
CASE WHEN t.failed_stage = 'synspec'
THEN CASE WHEN json_valid(t.synspec_strategies)
THEN json_array_length(t.synspec_strategies)
ELSE 0 END
ELSE CASE WHEN json_valid(t.tlusty_strategies)
THEN json_array_length(t.tlusty_strategies)
ELSE 0 END
END > 1
OR (
json_valid(t.tlusty_strategies)
AND json_array_length(t.tlusty_strategies) = 1
AND json_extract(t.tlusty_strategies, '$[0]') = 'seed_step_stab'
AND EXISTS (
SELECT 1 FROM seeds s
WHERE s.is_clean = 1
AND s.point_name != gp.name
AND s.teff = gp.teff
AND s.logg = gp.logg
AND s.loghe = gp.loghe
AND s.point_name NOT IN (
SELECT t3.seed_point_name FROM tasks t3
WHERE t3.point_name = gp.name
AND t3.workflow_name = gp.workflow_name
AND t3.seed_point_name IS NOT NULL
)
)
)
)
)";
impl Database {
pub async fn upsert_workflow(
&self,
@@ -47,7 +119,9 @@ impl Database {
// L6 修复:原 `let _ =` 丢弃 r2d2/rusqlite 错误——若该后台对账 UPDATE 失败,
// 工作流可持续卡在 running 无任何提示。现记录错误(仍返回 Ok 不中断主流程,
// 因为每份上报内的 workflow-completion flip 才是主路径,见 record_task_report)。
// 「未消费回退链」子句见 UNCONSUMED_FALLBACK_BLOCKER_SQL2026-08-22 修复)。
if let Err(e) = conn.execute(
&format!(
"UPDATE workflows
SET status = 'completed', updated_at = datetime('now')
WHERE status = 'running'
@@ -56,7 +130,10 @@ impl Database {
SELECT 1 FROM grid_points
WHERE workflow_name = workflows.name
AND status IN ('pending', 'queued', 'running')
)",
)
{}",
UNCONSUMED_FALLBACK_BLOCKER_SQL
),
[],
) {
tracing::error!("后台对账:同步 running 工作流完成态失败: {}", e);
@@ -90,6 +167,7 @@ impl Database {
}
// 一次 GROUP BY 聚合全部工作流的网格计数并回填(不做逐工作流查询,无 N+1)。
// tlusty 总收敛与 stab 分项为 2026-08-25 补(口径见 WorkflowStats.tlusty_converged)。
let mut agg_stmt = conn.prepare(
"SELECT workflow_name,
COUNT(*) AS total,
@@ -98,6 +176,8 @@ impl Database {
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed,
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS stab,
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
@@ -114,10 +194,12 @@ impl Database {
running: r.get::<_, Option<i64>>(4)?.unwrap_or(0),
cold_run_converged: r.get::<_, Option<i64>>(5)?.unwrap_or(0),
seed_step_converged: r.get::<_, Option<i64>>(6)?.unwrap_or(0),
tlusty_failed: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
synspec_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
synspec_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
synspec_pending: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
tlusty_converged: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
seed_step_stab_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
tlusty_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
synspec_converged: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
synspec_failed: r.get::<_, Option<i64>>(11)?.unwrap_or(0),
synspec_pending: r.get::<_, Option<i64>>(12)?.unwrap_or(0),
},
))
})?;
+76
View File
@@ -176,6 +176,32 @@ pub const MIGRATIONS: &[Migration] = &[
detect: |c| Ok(!has_column(c, "grid_points", "success_method")?),
up: &["ALTER TABLE grid_points DROP COLUMN success_method"],
},
// M142026-08-25,大气收敛统计补漏):Phase 5b 阶段列(tlusty_status)上线前完成的历史
// 点该列为 NULL——但 tlusty_success_method 仅在整管线成功(tlusty_enabled=1 且收敛)时
// 写入(record_task_report 成功分支),故 completed + method 非空 ⟹ 大气必已收敛。
// 回填 'converged',否则 stats 的 CASE WHEN tlusty_status='converged' 漏计
// (生产实证:sdB_cno 9216 点中 9 个此形态,前端 9137/9216 差额的一部分)。
Migration {
version: 14,
name: "backfill-tlusty-status-for-completed",
detect: |c| {
let n: i64 = c.query_row(
"SELECT COUNT(*) FROM grid_points
WHERE status = 'completed'
AND COALESCE(tlusty_success_method, '') != ''
AND COALESCE(tlusty_status, '') = ''",
[],
|r| r.get(0),
)?;
Ok(n == 0)
},
up: &[
"UPDATE grid_points SET tlusty_status = 'converged' \
WHERE status = 'completed' \
AND COALESCE(tlusty_success_method, '') != '' \
AND COALESCE(tlusty_status, '') = ''",
],
},
];
/// 当前 schema 版本(`PRAGMA user_version`)。
@@ -562,4 +588,54 @@ mod tests {
);
assert_eq!(current_version(&conn).unwrap(), 13);
}
/// M142026-08-25):completed + tlusty_success_method 非空 + tlusty_status 空
/// 的历史点回填 'converged'method 仅在整管线成功时写入);其它形态不动。
#[test]
fn m14_backfills_tlusty_status_for_legacy_completed() {
let mut conn = mem_conn();
conn.execute_batch(
"CREATE TABLE grid_points (
name TEXT NOT NULL,
workflow_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
tlusty_success_method TEXT,
synspec_success_method TEXT,
tlusty_status TEXT,
synspec_status TEXT,
pending_strategies TEXT
);
INSERT INTO grid_points (name, workflow_name, status, tlusty_success_method, tlusty_status) VALUES
('legacy_ok', 'wf', 'completed', 'cold_run', NULL),
('legacy_seed', 'wf', 'completed', 'seed_step', ''),
('already_failed', 'wf', 'failed', 'cold_run', 'failed'),
('already_conv', 'wf', 'completed', 'cold_run', 'converged'),
('no_method', 'wf', 'completed', NULL, NULL);",
)
.unwrap();
// 仅跑 M14(全量 MIGRATIONS 含 M1 的 tasks 迁移,本夹具只建 grid_points)。
let m14 = MIGRATIONS.iter().find(|m| m.version == 14).unwrap();
apply_migrations_with(&mut conn, &[*m14]).unwrap();
let read = |c: &Connection, name: &str| -> Option<String> {
c.query_row(
"SELECT tlusty_status FROM grid_points WHERE name = ?1",
rusqlite::params![name],
|r| r.get(0),
)
.unwrap()
};
assert_eq!(read(&conn, "legacy_ok").as_deref(), Some("converged"));
assert_eq!(read(&conn, "legacy_seed").as_deref(), Some("converged"));
// failed 点 / 已有状态 / 无 method 的点不受影响。
assert_eq!(read(&conn, "already_failed").as_deref(), Some("failed"));
assert_eq!(read(&conn, "already_conv").as_deref(), Some("converged"));
assert_eq!(read(&conn, "no_method"), None);
assert_eq!(current_version(&conn).unwrap(), 14);
// 幂等:再跑一遍 detect 命中(无待回填行)→ 仅推进版本,不改数据。
apply_migrations_with(&mut conn, &[*m14]).unwrap();
assert_eq!(read(&conn, "no_method"), None);
}
}
+112 -9
View File
@@ -360,6 +360,7 @@ impl GridScheduler {
/// 「弹出再压回」保持原序而陷入死循环。
async fn resolve_dispatchable_chain(
db: &Database,
workflow_name: &str,
params: &GridPointParams,
mut chain: Vec<String>,
) -> Result<(Vec<String>, Option<String>)> {
@@ -394,6 +395,36 @@ impl GridScheduler {
}
}
}
// 稳定化种子步进(2026-08-18docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md):
// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子——稳定化配方
// POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰,跨 Teff/logg 种子
// 不在适用域。排除目标点自身名(防止把自己旧产物当种子)。
// 2026-08-20 种子轮换:配方对种子逐点敏感(同距离换 N/换 O 邻居收敛性不同),
// 排除本点历史任务已用过的种子,重试时轮换到下一个未试过的同族邻居。
Some("seed_step_stab") => {
let mut exclude = vec![params.model_name()];
exclude.extend(db.list_used_seed_names(&params.model_name(), workflow_name).await?);
let found_seed = db
.find_exact_family_seed_from_db(params, &exclude)
.await?;
match found_seed {
Some(seed) => {
if !skipped.is_empty() {
chain.extend(skipped);
}
return Ok((chain, Some(seed.name.clone())));
}
None if chain.len() > 1 => {
let head = chain.remove(0);
skipped.push(head);
info!("策略解析:seed_step_stab 无同物理族 CNO 邻居种子,暂存顺位");
}
None => {
chain.clear();
return Ok((chain, None));
}
}
}
// 空链或非 seed_step 顺位:可直接派发。先前跳过的 seed_step 追加到链尾,
// 保留为后续回退顺位(空链由调用方判空处理)。
_ => {
@@ -561,7 +592,7 @@ impl GridScheduler {
};
let (dispatch_chain, seed_point_name) = if tlusty_cfg.enabled {
let (chain, seed) =
Self::resolve_dispatchable_chain(&self.db, &params, base_chain).await?;
Self::resolve_dispatchable_chain(&self.db, workflow_name, &params, base_chain).await?;
if chain.is_empty() {
// enabled 阶段无可派发顺位(基础链全为无种子的 seed_step)→ 打回 pending
// 待近邻种子出现后由下轮调度自愈派发(不是终态,不能被卡死在 queued)。
@@ -896,13 +927,6 @@ impl GridScheduler {
popped,
policy: row_policy,
} = snap;
if rest_strategies.is_empty() {
info!(
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
name, stage_label, popped
);
return Ok(false);
}
// 2026-08-04 语义修正:策略链回退**只由策略链驱动**(弹出失败首项、派发下一顺位,
// 链耗尽保持 failed),不再受执行策略门控——策略只决定启动工作流时对历史终态点的
@@ -929,6 +953,83 @@ impl GridScheduler {
.await
.unwrap_or((None, None, None, None, None, None, None, None));
// 2026-08-20 种子轮换:seed_step_stab 常为链上最后一项,失败弹出后链空。
// 稳定化配方对种子逐点敏感(同 CNO 距离、换不同元素方向的邻居收敛性不同,
// 见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md §九)——首个选中
// 的邻居失败不代表配方无效。排除本点历史已用种子后若还有未试过的同族
// 邻居,则以 seed_step_stab 单顺位重链重派。轮换次数天然受同族邻居数约束:
// 全部试过后此处不再命中,落入下方 failed 终态,无死循环。
if rest_strategies.is_empty() && failed_stage != "synspec" && popped == "seed_step_stab" {
let mut exclude = vec![params.model_name()];
exclude.extend(self.db.list_used_seed_names(name, workflow_name).await?);
if let Some(fresh_seed) = self
.db
.find_exact_family_seed_from_db(params, &exclude)
.await?
{
info!(
"策略回退:工作流 {} 网格点 {} seed_step_stab 失败但存在未试过的同族邻居种子 {},轮换种子重派(已试过 {:?})",
workflow_name, name, fresh_seed.name, &exclude[1..]
);
// 轮换任务显式注入新种子(绕过 resolve 的种子查找,避免其再次
// 命中同一邻居)。
let mut stab_cfg = tlusty_cfg.clone();
stab_cfg.strategies = vec!["seed_step_stab".to_string()];
stab_cfg.policy = row_policy.clone();
let task_spec = TaskSpec {
task_id: Uuid::new_v4(),
point_name: name.to_string(),
params: params.clone(),
seed_point_name: Some(fresh_seed.name),
timeout_sec,
workflow_name: Some(workflow_name.to_string()),
wave: 0,
tlusty_config: stab_cfg,
synspec_config: synspec_cfg.clone(),
synspec_params,
tlusty_chain_params: tlusty_chain.clone(),
seed_chain_params: seed_chain.clone(),
tlusty_input_params: tlusty_input.clone(),
atmosphere_ref: None,
energy_tolerance,
temp_max_factor,
temp_floor,
temp_ceiling,
emflux_tolerance,
convergence_min_ratio,
bfac_max,
bfac_min,
linelist: linelist.clone(),
};
self.db.insert_task(&task_spec).await?;
self.db
.update_grid_status(name, common::models::GridPointStatus::Queued, workflow_name)
.await?;
if let Err(e) = self.queue.push_task(&task_spec).await {
let _ = self
.db
.update_grid_status(
name,
common::models::GridPointStatus::Pending,
workflow_name,
)
.await;
let _ = self.queue.remove_task(&task_spec.task_id.to_string()).await;
let _ = self.db.delete_task(&task_spec.task_id).await;
return Err(e);
}
return Ok(true);
}
}
if rest_strategies.is_empty() {
info!(
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
name, stage_label, popped
);
return Ok(false);
}
// SYNSPEC 链回退:重试光谱合成。无邻居种子门控(大气来自目标点自身既有产物,
// 见 docs/task_engine_decoupling_design.md §5)——旧实现把 synspec 失败误归因到
// TLUSTY 链并做无意义的种子搜索(修复审查 #2)。
@@ -1011,7 +1112,7 @@ impl GridScheduler {
// pending_restresolve 前的基础剩余链(含全部 seed_step 顺位),H1 空链分支据此记录标记。
let pending_rest = rest_strategies.clone();
let (rest_strategies, seed_point_name) =
Self::resolve_dispatchable_chain(&self.db, params, rest_strategies).await?;
Self::resolve_dispatchable_chain(&self.db, workflow_name, params, rest_strategies).await?;
if rest_strategies.is_empty() {
// H1 修复:resolve_dispatchable_chain 仅在「剩余顺位全为无近邻种子的 seed_step」
// 时返回空(调用方已在上方 line 790 排除了真耗尽)。此时**不能**保持 failed 终态:
@@ -2909,6 +3010,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
// 无种子 → 重复 seed_step 链应判空(不派发、不死循环)。
let (chain, seed) = GridScheduler::resolve_dispatchable_chain(
&db,
"wf",
&params,
vec!["seed_step".to_string(), "seed_step".to_string()],
)
@@ -2921,6 +3023,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
// 两个无种子 seed_step 保留为回退顺位且链首可派发。
let (chain2, seed2) = GridScheduler::resolve_dispatchable_chain(
&db,
"wf",
&params,
vec![
"seed_step".to_string(),
+2
View File
@@ -28,6 +28,7 @@ async fn mark_imported(db: &Database, name: &str, wf: &str, params: &GridPointPa
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
db.upsert_point_summary(name, wf, &summary, method)
@@ -1866,6 +1867,7 @@ async fn dispatch_and_report(
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
};
let report = common::models::TaskReport {
@@ -41,6 +41,7 @@ fn make_converged_summary(name: &str, params: &GridPointParams) -> ModelSummary
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
}
}
+2
View File
@@ -69,6 +69,8 @@ const TOKEN_KEY = 'dcts_admin_token';
* @property {number} failed
* @property {number} cold_run_converged - TLUSTY 视图冷启动收敛
* @property {number} seed_step_converged - TLUSTY 视图种子步进收敛
* @property {number} [tlusty_converged] - TLUSTY 视图大气收敛总数权威口径不按策略拆2026-08-25
* @property {number} [seed_step_stab_converged] - TLUSTY 视图稳定化种子收敛2026-08-25
* @property {number} tlusty_failed - TLUSTY 视图大气发散
* @property {number} synspec_converged - SYNSPEC 视图光谱有效
* @property {number} synspec_failed - SYNSPEC 视图光谱失败
+5
View File
@@ -1045,6 +1045,8 @@ body::before {
.seg-converged { background: var(--color-success); }
/* 种子步进段(TLUSTY 视图):紫色,与冷启动绿区分收敛策略。 */
.seg-seed { background: var(--accent-purple); }
/* 稳定化/waypoint 种子段(TLUSTY 视图,2026-08-25 补):青色,与普通种子步进区分。 */
.seg-stab { background: var(--accent-teal); }
/* 色盲友好color-not-only失败段在红色之上叠加斜线纹理
即使无法分辨红/绿也能凭纹理识别失败占比 */
.seg-failed {
@@ -1439,6 +1441,7 @@ body::before {
/* 状态配色(与 method-badge/status-badge 配色族一致) */
.ps-cold { fill: var(--color-success); }
.ps-seed { fill: var(--accent-purple); }
.ps-stab { fill: var(--accent-teal); }
.ps-synspec { fill: var(--accent-teal); }
.ps-converged { fill: var(--color-success); }
.ps-failed { fill: var(--color-danger); }
@@ -1448,6 +1451,7 @@ body::before {
/* 色带默认 fill-opacity 已定,色块需不透明 → 用 stroke 不可,故色块状态类同时设 fill */
.ps-seg.ps-cold { fill: var(--color-success); }
.ps-seg.ps-seed { fill: var(--accent-purple); }
.ps-seg.ps-stab { fill: var(--accent-teal); }
.ps-seg.ps-synspec { fill: var(--accent-teal); }
.ps-seg.ps-converged { fill: var(--color-success); }
.ps-seg.ps-failed { fill: var(--color-danger); }
@@ -1469,6 +1473,7 @@ body::before {
/* 状态轴取值标签染色 = 自带图例(与色块/色带配色一致) */
.ps-vlabel-cold { fill: var(--color-success); font-weight: 600; }
.ps-vlabel-seed { fill: var(--accent-purple); font-weight: 600; }
.ps-vlabel-stab { fill: var(--accent-teal); font-weight: 600; }
.ps-vlabel-synspec { fill: var(--accent-teal); font-weight: 600; }
.ps-vlabel-converged { fill: var(--color-success); font-weight: 600; }
.ps-vlabel-failed { fill: var(--color-danger); font-weight: 600; }
+12 -3
View File
@@ -20,6 +20,7 @@ import { ICONS } from '../../utils/icons.js';
const SEG_DEFS_TLUSTY = [
['cold', 'seg-converged', '冷启动收敛'],
['seed', 'seg-seed', '种子步进'],
['stab', 'seg-stab', '稳定化种子'],
['failed', 'seg-failed', '大气发散'],
['running', 'seg-running', '运行'],
['queued', 'seg-queued', '排队'],
@@ -41,6 +42,7 @@ function segDefsFor(phase) {
function segCount(s, key, phase) {
if (key === 'cold') return s.cold_run_converged || 0;
if (key === 'seed') return s.seed_step_converged || 0;
if (key === 'stab') return s.seed_step_stab_converged || 0;
if (key === 'converged') return s.synspec_converged || 0;
if (key === 'failed') return phase === 'synspec' ? (s.synspec_failed || 0) : (s.tlusty_failed || 0);
if (key === 'pending') return phase === 'synspec' ? (s.synspec_pending || 0) : (s.pending || 0);
@@ -89,7 +91,11 @@ export function renderStrip(s, phase = 'tlusty') {
parts.push(`${s.synspec_failed || 0} 光谱失败`);
parts.push(`${s.synspec_pending || 0} 未运行`);
} else {
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
// tlusty_converged2026-08-25):权威总数,不按 method 拆——此前 cold+seed 之和
// 漏计 seed_step_stab 收敛点(生产 9137/9216)。旧 server 无该字段时回退三桶之和。
const conv = (s.tlusty_converged != null)
? s.tlusty_converged
: (s.cold_run_converged || 0) + (s.seed_step_converged || 0) + (s.seed_step_stab_converged || 0);
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
parts.push(`${conv} 大气收敛${s.total ? ` (${pct}%)` : ''}`);
parts.push(`${s.tlusty_failed || 0} 大气发散`);
@@ -223,11 +229,14 @@ function updateOverview(ctx, s) {
setOvText('ov-m-failed', s.synspec_failed || 0);
setOvText('ov-m-failed-sub', `未运行 ${s.synspec_pending || 0}`);
} else {
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
// tlusty_converged 为权威总数(2026-08-25 补,见 renderStrip 同名注释),旧 server 回退三桶和。
const conv = (s.tlusty_converged != null)
? s.tlusty_converged
: (s.cold_run_converged || 0) + (s.seed_step_converged || 0) + (s.seed_step_stab_converged || 0);
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
setOvText('ov-m-converged-title', '大气收敛');
setOvText('ov-m-converged', conv);
setOvText('ov-m-converged-sub', `冷启动 ${s.cold_run_converged || 0} · 种子步进 ${s.seed_step_converged || 0}${pct}%`);
setOvText('ov-m-converged-sub', `冷启动 ${s.cold_run_converged || 0} · 种子 ${s.seed_step_converged || 0} · 稳定化 ${s.seed_step_stab_converged || 0}${pct}%`);
setOvText('ov-m-failed-title', '大气发散');
setOvText('ov-m-failed', s.tlusty_failed || 0);
setOvText('ov-m-failed-sub', '收敛阈值未达标且无种子可救');
+3 -2
View File
@@ -67,10 +67,10 @@ export function restorePsCache(ctx) {
* cold/seed = 大气收敛按策略拆failed = 大气发散running/queued/pending = 未跑完
* SYNSPEC 视图光谱阶段基于 synspec_status
* converged = 光谱有效failed = 光谱坏Balmer edge NaN pending = 未运行多因 tlusty 未收敛阻塞 */
const PS_STATUS_TLUSTY = ['cold', 'seed', 'failed', 'running', 'queued', 'pending'];
const PS_STATUS_TLUSTY = ['cold', 'seed', 'stab', 'failed', 'running', 'queued', 'pending'];
const PS_STATUS_SYNSPEC = ['converged', 'failed', 'pending'];
const PS_STATUS_LABEL = {
cold: '冷启动收敛', seed: '种子步进', failed: '失败',
cold: '冷启动收敛', seed: '种子步进', stab: '稳定化种子', failed: '失败',
running: '运行', queued: '排队', pending: '未开始',
converged: '光谱有效',
};
@@ -94,6 +94,7 @@ function psSlot(p, phase) {
// TLUSTY 视图
if (p.tlusty_status === 'converged') {
if (p.tlusty_success_method === 'seed_step') return 'seed';
if (p.tlusty_success_method === 'seed_step_stab') return 'stab';
return 'cold'; // cold_run 或缺省归冷启动
}
if (p.tlusty_status === 'failed') return 'failed';
@@ -82,16 +82,70 @@ salvage .7),direct-nl= nl_direct 回退的等价路径):
连续步进即完全收敛**。机理:单步 Δ 超出 Newton 收敛域时,把参数空间距离
切分为多段,每段以段首收敛解热启动段尾。
### 5.4 生产化建议
### 5.4 生产化ladder 策略已实现(2026-08-17
1. 部署本修复(NaN 伪收敛否决 + nl_tight 回退)重试 400 点:预期回收 261 簇
>65%/种子×多候选)+ 7 个能量边际点;
2. 为剩余硬核实现 ladder 策略:失败点自动生成 2–3 个中间参数步进链
(优先 logg 维度,其次 Teff;步长 Δlogg≤0.25 / ΔTeff≤2.5kK),
每步独立 nl + require_converged 串联,最后一步产出目标点模型。
实测两簇代表点总耗时 <6 分钟/点(3 步 × ~2 分钟)。
三层回退架构(runner 内自动触发,零配置/调度器改动):
## 六、遗留问题
```
主链(nst 修复后) → nl_direct(污染种子跳过) → nl_ladder(连续步进) → nl_tight(能量边际)
```
- 20k/6.5 富氦簇解分支跳变:同一点不同种子在好解/坏解(emflux 0.69)间跳变
ladder 步进天然规避(每步验证收敛与物理性后才前进)
- **规划**`plan_ladder_steps(seed, target)`——归一化间隔更大的轴
步长 Δlogg≤0.25 / ΔTeff≤2.5kK,≤4 个中间步;双轴均在单步域内不触发
种子参数由 executor 从权威 `seed_point_name` 解析(`parse_point_name`)。
- **执行**:每中间步 clone 终阶段 ChainSteprequire_converged),前步收敛解作
后步 fort.8;链尾 `nl_ladder` 用目标参数收尾。任一步发散即中止。
- **持久化**:中间步「收敛 + fort.7 无 NaN」即登记 `ModelSummary.ladder_seeds`
node executor 读快照字节随 TaskReport 上报(**任务失败也上传**)→ server
`POST /api/task/report``seeds_dir/<name>_ladder/` + seeds 表(ON CONFLICT
仅刷新 file_path`_ladder` 后缀名与真实网格点零冲突;bucket 查找按数值列
匹配天然覆盖中间参数点)。簇内相邻失败点自动复用已收敛梯级。
- **重复网格点**:未来工作流若含与中间种子同参数的网格点,照常计算(种子≠结果),
seed_step 命中同参数种子后 ~1–2 迭代收敛——重验五重门槛,成本可忽略。
- 测试:`test_plan_ladder_steps`(规划纯函数)、`unit_ladder_fallback_wiring`
(假 tlusty 六调用接线:seed_nc/nl/nl_direct 失败→ladder_g5.75/ladder_g6/
nl_ladder 收敛 + ladder_seeds 登记)、`test_parse_point_name`
物理有效性由手工 ladder 实测证明(§5.3,与生产同一 ChainStep 配置与种子)。
workspace 全测试 10 套件通过。
部署:server + node 重建重部署后,对 400 失败点发起重试即可(ladder 自动生效)。
## 七、第二轮重跑后 121 残点分析与 ladder 实测(2026-08-18
### 7.1 121 残点分类(最新 DB,每点最新任务)
| 类别 | 数量 | 归因 |
|---|---:|---|
| NaN 伪收敛(否决未生效) | **32** | **全部来自 node-fmq-ubuntu-01 旧二进制**(其他节点同期已有 nl_tight 记录证明新二进制)——重新部署该节点即解决,随重试进入 nl_direct/正常链路 |
| 真发散(20kK/logg≥6 富氦簇) | ~62 | nl/nl_direct 均 1e16 STOP |
| 真发散(60kK/logg5.0 最贫 CNO 簇) | ~19 | 同上,且对一切种子/轴免疫 |
| 能量边际残留 | 2 | nl_tight 已触发(其一 tight 数值收敛但模型含 NaN 被否决) |
### 7.2 ladder 实测矩阵(12 点分层抽样 + 变体,test/20260817_failed400/
| 变体 | 配置 | 结果 |
|---|---|---|
| ladder121 | 生产语义(nl 步进,Δlogg 0.25 / ΔTeff 2.5k | **1/12** |
| ladder121_teff | 换 Teff 轴(同 CNO ±5kK rung | +2/7 |
| ladder121_nc | 中间步 seed_nc 式(ilvlin=0 + ORELAX=0.3 + NITER=20 | 0/9(发散转"卡住" |
| ladder121_nc100 | 同上 + NITER=100 + 2× 细化(0.125/1.25k | 0/9(单步可达 1e-4@34it,但链条后段单深度 POP 振荡 ~0.2 或 NITER 耗尽;每步最长达 1h+) |
| he_seed | 60k 簇换 he0 种子直接 nl | 1/4t55000 完整收敛 83it/3.7e-4/emflux 1.0008 |
| ladder20k_final | 20k 簇 nc+细化+7200s/步 | 0/3step3 处 iter100 单深度 0.199 振荡) |
### 7.3 结论
1. **部署项收益最大**:重新部署 ubuntu-01 → 直接回收 32 点(确定性)。
2. **ladder 对本轮残点增益有限**:全部变体合计 ~4-6/87。已实现的 ladder 保留
(自动触发、零额外成本、对上一轮 5 个验证点有效),但不应期望它清掉这批残点。
3. **残点本质**:20kK 富氦高重力簇在阻尼下不雪崩但呈**单深度 POP 极限环**
iter 100 时个别深度 |ΔPOP|≈0.2 不衰减);60kK 贫 CNO 低重力簇从任何
CNO/He/logg/Teff 种子均在前几迭代雪崩。两者均已超出"步进/种子"工程层
可解范围,属 TLUSTY Newton 求解器在这些极端参数组合下的固有收敛域缺失。
4. **可行后续**(求解器层,需另行立项):针对极限环深度的逐深度阻尼/POPZER
策略调优;或物理上接受这 ~85 点为"不可收敛角"并在工作流中显式标记。
### 7.4 测试工件
`test/20260817_failed400/``ladder121/``ladder121_teff/``ladder121_nc/`
`ladder121_nc100/``ladder20k_final/``he_seed/``batch_ladder.py`(支持
`--nc-int`/`--fine=N` 变体的通用 ladder 驱动)。
@@ -0,0 +1,472 @@
# 81 点未收敛根因分析与稳定化种子步进修复(2026-08-18)
数据库快照:`/home/fmq/下载/dcts.db`9135 completed / 81 failed)。
测试目录:`dcts/test/20260818_failed81/`
## 一、81 点分类
| 群 | 点数 | 特征 |
|---|---|---|
| 20kK / he2logg 5.5/6.0/6.5 | 66 | 全链(seed_nc→nl→nl_direct→Teff 轴 ladder)发散;ladder 中间步(22.5kK)收敛但最终步 20kK 发散 |
| 5560kK / g5.0he-4 / he-2 | 15 | 同样全链发散;57.5k 中间步部分收敛但 60k 最终步发散 |
| 假收敛否决误杀 | 2(含在上面) | nl_ladder 实际收敛且物理硬门全过,被"首末 max_relc 比 < 1e3"否决 |
## 二、逐项排除的假设(20k 群代表点 t20000_g6.5_he2_c-4_n-3_o-4
基线复现:用生产的 22.5k ladder 种子热启动 20k 最终步,轨迹与生产逐拍一致
iter1=18.2 → KANTOROVICH 加速介入 → iter10 雪崩 1.13e19STOP@10)。
| 变体 | 结果 | 结论 |
|---|---|---|
| ORELAX 0.5→0.3→0.2 | 雪崩轨迹几乎不变 | 欠松弛无效 |
| IACC=0(试图禁加速) | 与基线**逐字节相同** | 源码 `IF(IACC.LE.4) IACC=7`tlusty208.f:1928)——IACC=0 被重置,无效 |
| IACC=100(真禁加速) | STOP@7,更快爆 | 加速不是根因 |
| logg 轴 ladderg6.0→6.25→6.5 | 6.25 收敛,6.5 雪崩 | 换轴无效 |
| 更细 logg 步(Δ0.125 | 极限环 0.22 | 该区存在真实电离前沿不稳定性 |
| CNO 邻居种子(同 T/g) | 雪崩 1.7e4 | 光换种子不够 |
| POPZER=1E-20(官方默认值) | 雪崩不变 | 太保守 |
| POPZER=1E-10 | **雪崩消失**,极限环 0.41 | 微布居置零是关键稳定器 |
| POPZER=1E-8 | 雪崩 | 过度置零破坏解 |
| **CNO 邻居种子 + POPZER=1E-10 + DPSILG=3.0** | **收敛 2e-4** ✅ | 制胜配方 |
制胜模型全过五重物理硬门:温度结构三项 ✅、emflux=1.0013 ✅、
首末 max_relc 比 4.5e5(不触发假收敛排查)✅。
## 三、物理根因
- 20k 群:fort.9 列语义(ITER ID TEMP NE **POP** RAD MAXIMUM ilev ifr)显示
雪崩在**布居列**,振荡能级为碳(ilev 38=C I 基态、103=C III)与氦
ilev 20=He I,深度 49 外层 |ΔPOP|≈0.2 极限环)。He 富(He/H=100)大气
在 20kK 的 He I/II 电离前沿 + 微量碳(logC=-4)的电离平衡处于牛顿迭代
临界点:微布居在深度间振荡放大成雪崩。POPZER 把可忽略微布居剔除出方程
(官方标准输入恒设此机制,我们生产 nst 此前完全未设)、DPSILG 限制 λ 算子
单步变化幅度,两者组合把迭代拉回收敛域。
- 60k/g5.0 群(15 点):同 CNO 的 g5.5 收敛模型、仅 Δ0.25 logg 的步进也在
3 拍内雪崩(3.3e23)——目标解在牛顿域之外,与第三轮结论一致。工程层
(种子/步进/阻尼/置零)全部无效,属求解器级问题或应接受为不收敛角落。
## 四、假收敛否决误杀(2 点)
`t55000_g5.0_he-4_c-3_n-4_o-1`nl_ladder 末次 max_relc=6.03e-4 < chmax
能量/温度/emflux/b 因子四项校验全过,却被"首末比 63 < 1000"硬否决。
热启动时种子已接近解,首拍 max_relc 被钳在低位,首末比**数学上不可能**达到
1e3 量级——判据对热启动系统性误判。
修复(conv_check.rs `check_convergence_trace`):首拍 max_relc < 1.0 时豁免
首末比判据(valid=true + 豁免说明),真伪收敛交由五重物理硬门槛裁决。
## 五、生产集成:`seed_step_stab` 策略
- `ChainStep` 新增 `dpsilg` / `popzer` 字段(nst_writer 写出 DPSILG 及
POPZER/POPZR2/RADZER 同值联动组 + NITZER=1,行宽自动保护 ≤75 字符)。
- `runner::default_seed_stab_chain()`:种子链(seed_nc→nl)全步带
DPSILG=3.0 + POPZER=1E-10。
- 调度:`seed_step_stab` 顺位仅在 **exact_family**(同 Teff/logg/logHe、仅 CNO
不同)内找种子(新 db API `find_exact_family_seed_from_db`,排除自身名),
排除跨 Teff/logg 种子(不在配方适用域)。
- executor:该策略恒用稳定化链(用户自定义链不带旋钮,不采纳)。
- 工作流 YAML`strategies: [cold_run, seed_step, seed_step_stab]`
## 六、验证
- 单测:热启动豁免 ×2、nst 稳定化旋钮写出、stab 链形状/旋钮,全绿
(10 个测试套件全过,clippy 仅 2 个预存在 div_ceil 警告)。
- 全 20k 群 65/66 点批量实测(test/20260818_failed81/full20k*,最近同物理族
干净种子):稳定化配方对 **(DPSILG, POPZER) 组合逐点敏感**,四档互补——
- 一档 DPSILG=3.0/POPZER=1E-1017 点(全 g6.5
- 二档 DPSILG=2.0/POPZER=3E-11+14 点(g6.0/g6.5
- 三档 DPSILG=2.0/POPZER=1E-11+10 点(含全部 4 个 g5.5
- 四档 DPSILG=2.5/POPZER=1E-11+1 点
- 联合覆盖 **42/65**,全部通过温度结构物理判据;剩余 23 点对四档均不收敛
(需逐点参数搜索或求解器级工作,后续迭代)
生产接线:`seed_step_stab` 策略链携带一档;runner 内稳定化多档回退
`nl_stab2/3/4`(全链失败后逐档自原始种子重跑,任一档收敛即采纳)。
- 60k/g5.0 群 15 点:本轮再次确认工程层不可解,保持"求解器级/接受不收敛"结论。
## 七、部署要求
重新部署 **server + node**(双方都有改动:scheduler/db 与 runner/nst_writer)。
重跑策略链后预期:20k 群 42 点由 seed_step_stab(一档)+ nl_stab2/3/4(二至
四档)回收;2 个假收敛误杀点修复后重试即收敛;其余(20k 群 ~23 点 +
60k/g5.0 群 15 点)预计仍失败(如实归因)——20k 残余点需逐点参数搜索
(后续迭代),60k/g5.0 群属求解器级/接受不收敛。
## 八、2026-08-19 部署后 79 点复盘(两个接线 bug)
重新部署后失败数 81→79(仅回收 2 个假收敛误杀点;42 个验证点全部未回收)。
DB 239 个新任务分析定位两个 bug:
1. **Bug A(主力,78 个任务 rc=2**executor 的 `needs_seed_download` 只认
`seed_step` 策略,不认 `seed_step_stab` → 种子未下载,seed_nc 阶段无 fort.8
TLUSTY 读种子直接 EOF 崩溃(`tlusty rc=2 or missing fort.7`0.2s 内失败)。
修复:`matches!(current_strategy, "seed_step" | "seed_step_stab")`
2. **Bug B(种子选错)**`find_exact_family_seed_from_db` 复用了
`calculate_seed_distance` 的 is_exact 判定,其容忍 ΔTeff≤5000K——把 ladder
中间种子(如 t22500_g6.5_...CNO 距离 0)当成同族且排最前。稳定化配方的
验证前提是同温同重力仅差丰度,必须严格 Teff/logg/logHe 逐值相同。
修复:改为严格判定 + 回归测试 `test_find_exact_family_seed_strict_same_family`
60k/g5.0 群(15 点)与 20k 残余 23 点的失败为真实物理失败(如期),不受这两个
bug 影响。修复后需再次重新部署 server + node。
## 九、2026-08-20 部署后 44 点复盘(种子逐点敏感 → 种子轮换修复)
Bug A/B 修复部署后失败 79→44(回收 35 点,链路全部按设计执行:
seed_nc→nl→nl_direct→nl_stab2/3/4,种子均为严格同族 CNO 邻居,无 rc=2)。
### 归因
44 点分布:20k/he2 群 31 点(g5.5×4 / g6.0×8 / g6.5×19+ 5560k/g5.0 群 13 点。
与 08-18 直接测试的四档矩阵结果交叉比对(union 收敛 45/65,物理收敛判据
末拍 max_relc<1e-3 且 fort.7 无 NaN):
- **17 个 20k 点在测试中收敛、生产却失败**。逐一比对种子:测试制胜种子与
生产选中种子**全部不同**(同为 CNO 距离 1–2 的邻居,但换的元素方向不同,
如测试用 ΔO=1 而生产用 ΔN=1)。配方不仅对 (DPSILG,POPZER) 逐点敏感,
**对种子本身也逐点敏感**
- 其中 11 点的制胜组合就是 tier1DPSILG=3.0/POPZER=1E-10+ 特定距离 1
邻居——即生产首轮就会用到的档位,只差换对种子。
- 其余 14 个 20k 点在测试四档下也全部失败(真残余,需逐点参数搜索或接受)。
- 13 个 60k/g5.0 点维持求解器级失败的既有结论;但它们在 seeds 表中有同族
候选(40–295 个),轮换机制会一并尝试,或有少量意外回收。
### 根因(工程层)
生产每次重试 `find_exact_family_seed_from_db` 按 CNO 距离排序**稳定选中同一
个种子**,重试只是重复同一组合(种子×四档)白跑;且 `seed_step_stab` 是链上
最后一项,失败弹出后链空 → 直接终态 failed,从未轮换过种子。
### 修复:种子轮换
1. `find_exact_family_seed_from_db``exclude: Option<&str>` 改为
`&[String]` 排除列表。
2. 新增 `db.list_used_seed_names(point, workflow)`:本点历史任务用过的全部
种子名(DISTINCT)。
3. 调度两处接入排除:
- `resolve_dispatchable_chain` 的 seed_step_stab 分支注入已用种子 →
每次重派自然轮换;
- `trigger_strategy_fallback`seed_step_stab 失败且链耗尽时,若还有
未试过的同族邻居则以单顺位 `["seed_step_stab"]` 重链重派(wave=0 优先);
邻居全部试过后落入 failed 终态——轮换次数受同族邻居数(45–60 个)天然
约束,无死循环风险。实际预期在前几次轮换(距离 1 邻居圈)内命中制胜组合。
4. 回归测试:`test_find_exact_family_seed_rotation`(轮换到第二个邻居、
全排除后 None)。全套 10 个测试套件通过。
### 部署与预期
重新部署 server(node 无改动)+ 重启工作流(skip_converged)。预期 20k 群
再回收 ~17 点(测试已验证的组合),残余约 14(20k 真残余)+ 0–1360k)。
## 十、2026-08-20 残余 27 点直接测试:20k 全可回收、60k 求解器级定论(60k 结论后被 §十三 翻案)
针对 §九 后仍失败的 14 个 20k 真残余点与 13 个 60k/g5.0 点,批次
`dcts/test/20260820_residual27/`(249 个单阶段热启动运行,判据 = 末拍
max_relc<1e-3 + fort.7 无 NaN + check_temperature 三项):
1. **20k14/14 全部物理收敛**。制胜组合均为 t1/t2(生产链内建档位)×
特定 CNO 邻居种子(距离 2-3,非最近邻);g6.5 存在「种子星」
`t20000_g6.5_he2_c-2_n-3_o-2`(对 9 点有效)。§九 的种子轮换按距离
升序枚举即可命中,**无需新增生产代码**——部署轮换修复后 20k 群 31 个
失败点预期全部回收。
2. **60k/g5.013/13 仍不可收敛,定论为求解器级**。在近邻种子基础上
穷尽五族输入层旋钮——tier1 配方、DPSILT/DPSILN/DPSILD 热分量钳制、
ORELAX=0.3/0.2 强松弛、极端钳制(DPSILG=2/DPSILT=1.2/DPSILN=1.2)——
全部在 iter 4-10 内 `**** STOP in SOLVE`(牛顿矩阵奇异、线性解失败,
阻尼类旋钮作用于更新步、到不了 SOLVE 内部)。与 §一/§三「Newton
排除域」结论一致:除非修改 TLUSTY 求解器本体,建议接受这 13 点为
不收敛角落。
## 十一、2026-08-20 60k/g5.0 群 13 点攻坚:10 点物理收敛,3 点折叠排除("折叠排除"已被 §十三 翻案为 13/13)
继 §十 后针对 60k 群的专项测试(`test/20260820_residual27/`,累计 500+
次运行)。过程中修正两个方法论错误:① fort.9 每拍 max_relc 应取该拍
**全深度最大值**(非末行);② salvage 种子库存在跨尝试工件混叠,种子
必须配对同阶段 `.9` 末拍收敛(真验证池 259 个)。另发现 **DPSILG/POPZER
稳定化旋钮在高 CNO 60k 上致散**(自复现实验证实),§五/§九 配方仅在
20k He-rich 域有效。
**制胜配方(10/13 点物理收敛,三项判据全过)**:同 CNO 跨 Teff(Δ5k
冷端)真收敛种子 → **纯档自适应 Teff 延拓**plain nst,步长 1250K 起、
失败二分、最小 25K、成功×1.5 恢复)。本地无跨温种子时级联(50k→55k
收敛后再 55k→60k)。
**折叠排除 3 点**he-2 c-4_n-4、he-4 c-4_n-3、he-4 c-4_n-4):Teff
(二分至 25K)、logg(5.125 以下全爆)、O 丰度(二分至 0.025 dex)三条
独立延拓轴全部在目标前折叠,IACC=100/ORELAX=0.3/NITER=500 亦无效——
判定为静力解不存在的物理角落(60k/g5.0 最大金属组合,辐射加速度逼近
引力),建议接受为不收敛终态。
**对 15-80k 网格扩展的工程要求**:① runner 需新增自适应 Teff 延拓
(替换固定 2500K 阶梯);② 种子搜索需纳入同 CNO 跨 Teff 方向(冷端
优先);③ 稳定化旋钮按温度域门控(仅 20k He-rich);④ 设计「折叠排除」
终态标记避免扩展网格时对此类角落无限重试。
## 十二、2026-08-21 生产代码落地(§十一 工程要求实现)
1. **自适应 Teff 延拓**`runner.rs` ladder 回退块):Teff 轴为主且间隔
>2500K 时,替换固定 ≤2500K 均分档为自适应步长控制——初始 1250K、
成功 ×1.5(上限 1250K)、失败二分(下限 25K)、最多 48 步;失败步
产物不传递(沿用上一收敛态),步长低于 25K 判折叠墙中止。收敛梯级
照旧登记 ladder_seeds 供簇内共享;终点由 nl_ladder 收尾。logg 轴与
短间隔维持原固定档(自适应仅 Teff 轴经 60k 验证)。
2. **稳定化旋钮温度域门控**(两处):
- `executor.rs resolve_execution_chain`Teff>30kK 时 seed_step_stab
降级为普通种子链(不再恒用 default_seed_stab_chain);
- `runner.rs` STAB_TIERS 多档回退:Teff>30kK 跳过全部档位。
依据:60k/g5.0 高金属域实测 DPSILG/POPZER 族旋钮致散(§十一)。
新增回归测试 `seed_step_stab_teff_domain_gating`
3. **同 CNO 跨温种子方向**:无需改动——`calculate_seed_distance`
exact_family2026-08-14 修复)已含 ΔTeff≤5000K 且同 CNO 距离为 0
(最优排序),55k→60k / 50k→55k 制胜种子方向天然可用。
4. **折叠排除终态标记**:暂缓(需新增 GridPointStatus 变体并波及前端/
调度语义,属产品决策);当前 3 个折叠点保持 failed 终态,§十一 已
记录三轴证据。
全套 210 测试通过;clippy 仅余 2 个既有 div_ceil 警告。需重新部署
server + node(两 crate 均有改动)。
## 十三、2026-08-21 独立审查 + C/N 丰度轴翻案:13/13 全部可收敛
### 独立审查(subagent 全量复核)
- **证实**fort.9 判据(源码级:PRCHAN 恒 9 列、MAXIMUM=第 7 字段、CHM
取全深度最大、LFIN=|CHMX|≤CHMAX);10 个收敛点零假阳性(末拍全深度
最大 4.0e-4~9.9e-4、fort.7 干净、温度结构三项过、种子链 md5 连续);
稳定化致散对照实验公平(selfrepro3 vs smoke 仅 nst 旋钮行不同);
§十二 生产代码正确。
- **证伪两处失败侧证据**:① tlad3 的 64 个 stab 步是 nst 行超长解析崩溃
(非物理发散)——出现在脚本修复前的日志里;② glad logg 轴链被失败态
污染(失败步 fort.7 被当种子传递)+ stage.5 logg 截断(5.125→"5.12"
——logg 轴"二分耗尽"证据无效。两点均不改变当时总判决,但提示
he-2_c-4_n-4 的折叠证据实际只剩 Teff+O 两轴。
- 审查修复已入代码:自适应延拓 label 精度 0.01k(防重名);到达目标后
直接采纳(去冗余 nl_ladder 重跑)。
### C/N 丰度轴延拓:折叠结论翻案
针对 3 个"折叠点"补测此前未走通的 C/N 丰度轴(同 T/g/He 的 CNO 邻居
真收敛种子,0.2 dex 起二分至 0.025 dex):
| 点 | C 轴 | N 轴 |
|---|---|---|
| he-2 c-4_n-4_o-1 | ✅ 13 步 | ✅ 9 步 |
| he-4 c-4_n-3_o-1 | ✅ 13 步 | 爬升至 -2.41(冗余证据) |
| he-4 c-4_n-4_o-1 | ✅ 13 步 | 爬升中(冗余证据) |
三点 C 轴全部物理收敛(三项判据过)→ **"静力解不存在"结论撤销**:
解存在,可达方向在 C/N 丰度轴(Teff/logg/O 三轴确实折叠,但非充分
证据)。**13/13 全部可收敛**。
### 生产落地(runner 丰度轴延拓回退)
`runner.rs` 新增丰度轴延拓回退块(与域门控互补,仅 Teff>30kK 启用):
严格同族(同 T/g/He 仅 CNO 不同)种子 → C 轴优先、N 轴兜底,0.2 dex
起、成功 ×1.4(上限 0.25)、失败二分(下限 0.025)、每轴 40 步、
失败步不传递;收敛梯级登记 ladder_seeds。接线单测
`unit_axlad_c_axis_fallback_wiring` 通过;全套 211 测试通过。
生产路径闭环:seed_step_stab(高温降级普通链)注入 exact-family CNO
邻居种子 → seed_nc/nl 失败 → Teff 延拓(种子同温无步)→ stab 跳过
(域门控)→ **丰度轴延拓收敛**。§九 种子轮换在失败时自动换下一邻居。
## 十四、2026-08-22 最后 1 点收编:axlad 迭代饥饿修复(LADDER_STAGE_NITER+ server 部署不一致发现
§十二/§十三 代码部署后 44 点中 43 点恢复,唯一残余
`t60000_g5.0_he-4_c-4_n-4_o-1`60k/g5.0/氢主导、高氧 o-1 角,24 次尝试)。
详见 `test/20260822_last1/RESULT.md`
### 根因 A:延拓阶段迭代饥饿(物理层,已修复)
8-21 12:11 生产任务(种子 n-2axlad_n 已从 -2 收敛到 -3.490,随后
-3.568/-3.529 两步 best=0.002/0.005 **卡满 100 迭代且仍在单调下降**——不是
发散,是 NITER=100 迭代饥饿;二分触底 0.025 dex 后被误判"轴折叠"。
离线复现(NITER=300,种子=生产 n-3 收敛大气):n=-3.52373 于第 **103**
迭代收敛(恰好超旧上限 3 次);全程 5 个 waypoint 需 >100 迭代
103/111/111/127/147),16 步走通 -3.0→-4.0,目标点 4.1e-04、三项温度
检查过、无 NaN。**9216/9216 全网格物理可解。**
修复:`runner.rs` 新增 `LADDER_STAGE_NITER=300`,自适应 Teff 延拓/固定梯/
丰度轴延拓三站点统一 `niter.max(300)`(TLUSTY 收敛即停,快阶段与真发散
步均不受损)。全套工作区测试通过。
### 根因 B:server 部署二进制与工作区不一致(部署层,需重建)
12:11 任务给 n-4 挂了 8-19 已用过的 n-2 种子——违反当前代码的
`list_used_seed_names` 排除;且按 directed_cno_distance 排除后应选
c-2_n-4_o-1。与"无排除 + 种子表序"旧行为吻合 → **部署的 server 二进制
构建自中间状态(至少缺 resolve 路径种子轮换排除)**。node 端为新
(axlad 已运行)。需以当前工作区重建重部署两端。
### 有利条件与预期恢复
- n-3 点 8-21 12:05 任务已用同款 axlad 从 -2 走到 -3.000 收敛(24 步),
种子已注册;
- n-4 失败任务已注册 waypoint 种子最深至 `..._n-3.489798_o-1_ladder`
(距目标 0.51 dex、从未用过)——重部署后首个 seed_step_stab 派发自然
选中它,axlad_n 以 300 迭代走完剩余 0.51 dex 即收敛。
## 十五、2026-08-22 二次重试仍失败:完成翻转 × 回退触发顺序竞态(flip 阻塞修复)
§十四 修复部署后(8-22 03:46)重试仍失败,且**只派发了 1 个 cold_run 任务
(14 分钟发散)后整链静默终止**——无 seed_step/seed_step_stab 后续任务。
### 根因(server 顺序竞态,非物理)
失败上报处理链(api/task.rs → record_task_report 事务 → trigger_strategy_fallback):
1. 事务内:任务行 failed + 网格点 failed + **workflow 完成 flip**(条件仅
「无 pending/queued/running 点」);
2. 事务后:trigger_strategy_fallback 派发下一策略——但其 still_running
守卫发现 workflow 已是 completed → **直接跳过**
该点为最后一个活跃点(9215 completed + 1 failed),失败上报的同一事务内
flip 条件即满足(workflows.updated_at=04:00:50 = 上报时刻,实证)→
cold_run 之后的策略链永不派发。此前从未暴露:只有当失败点不是最后一个
活跃点时,flip 条件不满足、回退正常(8-21 的 09:45→10:04→12:11 三连回退
正是因为当时还有其它点在跑)。
### 修复:完成 flip 增加「未消费回退链」阻塞子句
`db/workflows.rs` 新增共享子句 `UNCONSUMED_FALLBACK_BLOCKER_SQL`,两条
flip SQLrecord_task_report 事务内 + 后台 30s 对账)统一追加:
> 存在 failed 点、其最新 failed/timeout 任务行(镜像 pop 的
> ORDER BY created_at DESC, rowid DESC 口径)的 tlusty 或 synspec 策略链
> json_array_length > 1(弹掉刚失败首项后仍有顺位)→ 不翻转 completed。
- 链随每次失败上报严格变短,终会耗尽(长度 1)→ 不永久阻塞完成;
- JSON 异常 → json_array_length=NULLNULL>1 不为真)→ 保守放行;
- 修复后顺序:失败上报 → flip 被阻塞 → 回退正常派发 → 点回 queued →
链耗尽后 flip 恢复。
回归测试 `test_workflow_flip_blocked_by_unconsumed_fallback_chain`
(3 顺位链阻塞 → 单顺位链放行)通过;全套 212 测试通过。
### 部署要求
本轮仅改 serverdb/tasks.rs、db/workflows.rs、db/mod.rs 测试);
node 无需重新部署。**server 需用当前工作区再次重建重部署**,然后按既有
流程重试 failed 点。预期链路:cold 失败(flip 不再提前杀链)→ seed_step
55k ladder 种子,失败)→ seed_step_stab 排除已用种子后选中
`..._n-3.489798_o-1_ladder`0.51 dex)→ axlad_n 300 迭代走完 → 收敛。
### 审查驱动补强(2026-08-22 subagent 复审,4 项修复)
复审确认主路径有效(SQL 口径与 pop 严格镜像、快照实证同一行、恢复链路
seed_step→waypoint 逐步核实、无永久卡 running、全测通过),另发现 4 项:
1. **【高】链尾 rotation 仍会被吞**(同构 bug 后退一环):最后一个活跃点以
`[seed_step_stab]`(长度 1)失败时,blocker 放行 flip → rotation 臂
(换种子重派,§九机制)被 still_running 守卫拦截 → waypoint 只有
一次机会。修复:blocker 增加「种子轮换待执行」子句——链尾
seed_step_stab 失败且存在未试过的同物理族干净种子(teff/logg/loghe
逐值相等、is_clean=1、不在本点已用种子集)→ 阻塞翻转。快照实测该点
有 **96 个**未用同族候选(waypoint 阶梯 -3.4898/-3.4337/…),轮换
每次消耗一个种子、单调收敛,不会永久阻塞。
2. **【中】synspec 双链 OR 永久卡死隐患**:synspec 回退任务行刻意保留完整
tlusty 审计链(scheduler.rs synspec 臂 clone),len≥2 恒真 → synspec
启用的工作流 flip 永久阻塞。修复:按行 failed_stage 阶段归因(镜像
pop 的 stage→列映射,NULL→tlusty)只看对应链。
3. **【低】固定梯末步 nl_ladder 漏提升**:中间梯级 300、最难的目标逼近末步
反而 100(越近目标越慢收敛)。修复:`final_step.niter.max(300)`
4. **【低】json 防护**json_array_length/json_extract 对非法 JSON 抛
`malformed JSON` 错误而非返回 NULL——一条脏行会让 record_task_report
整个结算事务失败。修复:json_valid 守卫(脏行按长度 0 放行)+ 注释更正。
回归测试新增 `test_workflow_flip_blocked_by_seed_rotation_pending`
(无候选放行 → 注入候选阻塞 → 消耗候选后放行)与
`test_workflow_flip_stage_attribution_ignores_tlusty_audit_copy`
synspec 耗尽行 + tlusty 审计副本 len3 不阻塞);全套 **214 测试**通过。
**运维附注**(审查 #5/#6):
- NITER=300 拉长任务墙钟(55k→60k 重走 + axlad 可能 >6h),而 stale 重投
阈值 21600sclaimed_at 不随心跳刷新)——攻坚期间建议调大
`DCTS_STALE_SEC`(如 43200),避免任务被重投双跑。
- seed_step 臂将在 35 个 d=0.0 并列种子中选中最早插入的 55k 真实点
exact 族内距离不含 Teff 项),节点需重走 55→57.7k 折叠墙(预期失败、
耗时数小时)后才轮到 seed_step_stab→waypoint;属效率问题非正确性,
后续可考虑并列时按 Teff 邻近决胜(暂不动 seed_finder 语义)。
## 十六、2026-08-25 全网格收官核验 + 前端「9137/9216 大气收敛」显示口径修复
### 收敛核验(快照 dcts.db
- **9216/9216 网格点 status=completedtlusty 收敛 9207 + 9 个历史空状态点
(M14 回填后 9216)——全网格物理收敛达成。**
- 末点 t60000_g5.0_he-4_c-4_n-4_o-1 制胜链(8-24):55k 种子任务重走折叠墙
失败(23417s)→ waypoint -3.4898 种子任务失败(axlad 爬至 -3.825 后
中断)→ **rotation 换 -3.6248 waypoint 种子** → axlad 以 **107/107/175
迭代**三连收敛(-3.725/-3.865/-4.000final 9.46e-04)——三个 waypoint
全部超过旧 NITER=100 上限,LADDER_STAGE_NITER=300 与 rotation/flip 修复
均为必要条件;五重物理门全过(energy 7.7e-4 / temp / emflux 1.00056 /
bfac 0.0077 / result_valid)。
### 「9137 大气收敛」差额解剖
前端口径 `大气收敛 = cold_run_converged + seed_step_converged`(按
tlusty_success_method 分类),漏掉两类:
| 差额 | 数量 | 成因 |
|---|---|---|
| seed_step_stab 收敛点 | 70 | 统计口径写于 2026-08-18 引入 stab 策略之前,未跟进 |
| tlusty_status 空的历史点 | 9 | Phase 5b 阶段列上线前完成的点,列值为 NULL |
9207 70 − 9 = 9137,与前端显示精确吻合。)
### 修复(server 端 + 前端,均随 server 二进制打包)
- **后端统计**`get_grid_summary_stats`grid.rs 双分支)与
`list_workflows` 聚合新增 `tlusty_converged`(不按 method 拆的权威总数)
`seed_step_stab_converged`WorkflowStats/WorkflowListStats 同步
serde default 兼容旧 JSON)。
- **前端**:详情页计数行与指标卡改用 `tlusty_converged`(旧 server 回退
三桶和);TLUSTY 分段进度条与 parSets 新增「稳定化种子」档(teal 色,
psSlot 按 method 三分)。
- **M14 迁移**`completed + method 非空 + tlusty_status 空` → 回填
'converged'method 仅在整管线成功时写入,回填安全;detect 幂等)。
快照实测回填 9 行,修复后前端显示 **9216 大气收敛 (100%)**
(冷启动 5573 · 种子 3573 · 稳定化 70)。
- 回归:M14 迁移测试 + stats 断言扩展;全套 **215 测试**过、clippy 0 错。
### 光谱侧现状(非本轮问题,仅记录)
工作流配置 `synspec_stage.enabled: false`(本攻坚期 TLUSTY-only)。快照
synspec_status7452 converged / 459 failed / 1305 空——459 个 failed 均
为 8-7 之前旧时代残留(无成功方法标记,synspec_success_method 全空),
1305 个空多为 TLUSTY-only 重跑后 clear_synspec 置空。若后续需要光谱全覆盖,
需另开一轮(重新启用 synspec_stage + skip_converged)。
### 附录(2026-08-26):9 个 tlusty_status 空历史点的逐点审查
**是否真收敛——三重独立证据,结论:是。**
1. **任务链**:9 点各有且仅有一个 completed 完整管线任务(tlusty_enabled=1、
synspec_enabled=1、cold_run/seed_step),max_relc 0.000114~0.000933 全部
< 1e-3elapsed 1500~7500s 为真实完整计算;
2. **种子库**9/9 在 seeds 表注册 is_clean=1 且服务端 `.7` 在档——种子注册
仅发生在「收敛 + 大气无 NaN」的成功上报/导入路径;
3. **功能验证(最硬)**:6/9 的大气被后续任务用作种子,5/9 有制胜案例——
`t55000_g5.0_he0_c-4_n-2_o-4` 的大气 8-15 作为种子把
`t60000_g5.0_he0_c-4_n-2_o-4` 带到收敛(9.77e-04);
`t45000_g5.0_he-4_c-4_n-4_o-2` 的大气 8-18 带收敛
`t50000_g5.0_he-4_c-4_n-4_o-2`(2.77e-04)。发散/垃圾大气做种子只会
立即爆炸,不可能制胜。
(注:Aug-1 时代产物未进 salvage、tasks.summary_json 早于 M1 列,故无法
做配对 .9 复核;以上为可得的最强证据。)
**为什么恰好 9 个空(机制清楚)**
- 9 点的 TLUSTY 制胜上报全部发生在 **8-1~8-2**(重复派发混沌期,每点恰好
一次成功、前后皆失败重复派发;completed 终态被守卫保护未被翻黑);
- `tlusty_status` 阶段列 **8-6d16b3d3/M8)才存在**M8 只 ADD COLUMN
不回填;此后 9 点再无任何任务(attempt_count 冻结),无上报可补写;
- 同期(8-2 前制胜)606 点中 597 个的列已被一次**中期批量修复**补齐(该
修复同时写了全队列的 synspec_success_method='standard'——git 中无此
语句,应为当时手工修复 SQL),但它漏掉了这 9 个(8/9 的最后任务行是
failed,疑被其筛选条件排除;3 个例外说明条件并非单纯按最后任务状态)。
精确 SQL 已不可考,不影响收敛性判定。
**补全**M14 迁移(status='completed' AND method 非空 AND tlusty_status
空 → 'converged')即为此而设;method 的三个写入方(成功上报/mark_grid_
point_imported/upsert_point_summary)全部以收敛为前提,判定安全。快照实测
恰回填 9 行。若需绝对 belt-and-braces,可对这 9 点单独 force 重跑(各
~30min),让列经由真实计算路径写入。
+113
View File
@@ -0,0 +1,113 @@
# Windows 节点远程桌面连接手册(经 Linux 跳板中转)
> 2026-08-20 实战总结。背景:两台 Windows 计算节点(node-fmq-dckj-02/03)与本机不直连,
> 必须经 Linux 跳板 node-fmq-dckj-01dckj@100.66.1.5Tailscale 网段)中转。
> 本机无节点小宝直连 Windows 的能力,所有 Windows 运维(尤其 Docker Desktop 登录自启)
> 都走本文的 SSH 隧道 + RDP 方案。
## 1. 网络拓扑与凭据
| 节点 | 地址 | 登录 | 密码 | 可达路径 |
|---|---|---|---|---|
| 跳板 dckj-01 | 100.66.1.5 | dckj | dckjzndx | 本机直连 |
| node-fmq-dckj-02 | 192.168.6.102 | fmq | 1235 | 仅经跳板 |
| node-fmq-dckj-03 | 192.168.6.103 | Administrator | dev#2dckj@zndx | 仅经跳板 |
- Windows 侧 SSH22)和 RDP(3389)均开放,但只对跳板所在的内网段可达。
- Windows 上运行**节点小宝**(内网穿透工具,服务名 `NodeBabyLinkService`
`StartMode=Auto` 开机自启),负责与 dckj-01 之间的 P2P 链路——
跳板能通 SSH 本身就说明节点小宝是活的。
- ⚠️ 密码写在文档里仅图方便,机器若更换使用人请先改密;长期方案建议配 SSH 公钥。
## 2. 核心方案:SSH 隧道转发 RDP(无需在跳板装任何桌面软件)
原理:本机 `ssh -N -L` 建立经跳板的加密隧道,把远端 3389 映射到本机端口,
再用本机任意 RDP 客户端(Remmina / xfreerdp / GNOME Connections)连 localhost。
```bash
# 隧道一:本机 13389 → dckj-03 (192.168.6.103) RDP
sshpass -p 'dev#2dckj@zndx' ssh -N \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=6 \
-o ProxyCommand="sshpass -p dckjzndx ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p dckj@100.66.1.5" \
-L 13389:192.168.6.103:3389 Administrator@192.168.6.103 &
# 隧道二:本机 13390 → dckj-02 (192.168.6.102) RDP
sshpass -p 1235 ssh -N \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=6 \
-o ProxyCommand="sshpass -p dckjzndx ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p dckj@100.66.1.5" \
-L 13390:192.168.6.102:3389 fmq@192.168.6.102 &
```
然后 RDP 客户端连接:
- dckj-03 → `localhost:13389`(用户 Administrator
- dckj-02 → `localhost:13390`(用户 fmq
要点:
- `ProxyCommand` 里层 `sshpass` 付**跳板机**密码,外层 `sshpass` 付**目标机**密码,
两级密码各管一段,这是双密码跳板场景的关键结构。
- `ServerAliveCountMax=6`(默认 3):节点小宝 P2P 链路偶发抖动,放宽容忍避免隧道被误杀。
- 验证隧道:`timeout 3 bash -c '</dev/tcp/127.0.0.1/13389' && echo OK`
- 隧道断了(症状:RDP 卡死、后台 ssh 报 "Timeout, server not responding")直接重跑上面命令即可。
## 3. 经跳板执行远程命令(非交互,运维日常)
```bash
rwin() { # rwin <win-02|win-03> "命令"
case "$1" in
win-02) h=fmq@192.168.6.102; p=1235 ;;
win-03) h=Administrator@192.168.6.103; p='dev#2dckj@zndx' ;;
esac
sshpass -p "$p" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ProxyCommand="sshpass -p dckjzndx ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p dckj@100.66.1.5" \
"$h" "$2"
}
# 用例:rwin win-03 "docker ps"
```
`scripts/fetch_results.sh` / `scripts/deploy.sh` 已原生支持跳板:profile 里加
`REMOTE_PROXY_JUMP=dckj@100.66.1.5` 即自动 `-o ProxyJump`win-01/win-02 两个 profile 已配好)。
## 4. 重要经验:Windows 节点重启后的恢复顺序
**Docker Desktop 是 GUI 程序,必须有所属用户的交互桌面会话才能启动引擎。**
从 SSH 远程 `Start-Process 'Docker Desktop.exe'` 无效——进程落在无桌面的 SSH 会话里,
GUI 初始化不了,引擎 named pipe(`dockerDesktopLinuxEngine`)不会创建,
`docker ps` 报 "unable to start" / "cannot find the file specified"。
正确的恢复链:
1. 确认节点小宝活着(能 SSH 进去即活着;服务 `NodeBabyLinkService` Auto 自启,一般不用管)。
2. 建隧道、RDP 登录对应账户的桌面(**必须是与 Docker 数据同账户的桌面**,
dckj-03 的 Docker 挂在 Administrator 下,登录别的用户如 gaoyuqi 没用)。
3. 登录后 Docker Desktop 自动启动(两台均已开 AutoStart),任务栏鲸鱼图标静止即就绪。
4. `dcts-node` 容器配了 restart 策略,引擎就绪后**自动恢复运行**,无需手动 compose up。
5. 验证:`rwin win-03 "docker ps"` 看到 `dcts-node Up`,再看看板心跳上线。
诊断命令速查(经 `rwin` 执行):
```powershell
# 开机时间(判断是否重启过)
(Get-CimInstance Win32_OperatingSystem).LastBootUpTime
# 节点小宝服务状态
Get-CimInstance Win32_Service | Where-Object {$_.Name -match 'NodeBaby'} | Format-List Name,State,StartMode
# Docker 引擎是否就绪
docker ps
# 当前桌面会话归属(判断该登录哪个账户)
quser
```
## 5. 本次事故时间线(2026-08-20
- 两台 Windows 于 14:52 左右重启;节点小宝 Auto 自启正常(开机 16 秒内拉起),
网络链路一直通。
- Docker Desktop 虽配了 AutoStart,但因 **Administrator/fmq 未登录桌面**Docker Desktop
依赖登录会话),引擎未启动,节点掉线。
- 远程 Start-Process 反复失败 → 查日志发现 backend 未被拉起 → `quser` 发现桌面会话
属于第三方用户且已断开 → 定位根因。
- 经 SSH 隧道 RDP 登录桌面后,Docker 引擎启动、`dcts-node` 容器自动恢复。
治本方向(未做,待定):给两台 Windows 配置 Administrator/fmq 自动登录
(注册表 `AutoAdminLogon`,密码明文存储需评估安全),彻底摆脱重启后必须人工 RDP。
@@ -0,0 +1,279 @@
# sdB_cno 光谱计算小结
**日期**2026-08-25
**数据来源**:分布式计算框架的任务记录数据库快照(共 21384 条任务执行记录)、2026-07-30 分布式框架上线前单机执行期经导入工具入库的成果(无任务行记录),及 `data/salvage/` 中的计算产物;单机执行期的起点与过程依据 tl208-s54 仓库 git 提交记录(首提交 2026-07-15)与 `cno_grid/EXPERIENCE.md` 调试记录。求解器为 TLUSTY v208Hubeny & Lanz),光谱合成为 SYNSPEC。全部 9216 个网格点的大气模型均已收敛并通过物理有效性检验。
---
## 摘要
本文总结 sdB_cno 模型大气网格 9216 个网格点的收敛方法分布、各收敛策略的构造与物理依据,以及策略体系随计算进程的演化。60.5% 的网格点由分级松弛冷启动收敛(LTE 初猜逐步放开至全非局域热动平衡),38.8% 由近邻收敛模型热启动收敛,0.8% 依赖稳定化参数(布居变化阻尼与微布居置零)在低温富氦区收敛;另有三类链内参数延拓(Teff 延拓、稳定化多档、C/N 丰度轴延拓)处理常规路径失效的困难网格点。各策略在参数空间中的分布与恒星大气物理预期一致:有效温度越高、表面重力越低,能级布居偏离局域热动平衡越大,冷启动初猜越远离真实解,热启动的必要性随之上升。全部收敛结果均通过统一的物理有效性检验(波长积分流量守恒、能量守恒、温度结构合理性、迭代收敛判据、数值有效性)。计算成本方面,9216 点制胜任务合计 5791 核时(单点中位数约 24–36 分钟),计入失败尝试后的全周期投入 14038 核时(约 585 核日)。
---
## 一、网格定义、求解器与收敛判据
### 1.1 网格
网格面向热亚矮星 B 型(sdB,极端水平支)恒星 CNO 丰度响应研究,共 9216 个模型:Teff ∈ {20, 25, 30, 35, 40, 45, 50, 55, 60} kK9 值)× log g ∈ {5.0, 5.5, 6.0, 6.5} × log He ∈ {4, 2, 0, +2} × log C ∈ {4, 3, 2, 1} × log N ∈ {4, 3, 2, 1} × log O ∈ {4, 3, −2, −1}(9 × 4⁵)。模型命名约定 `t{Teff/kK}_g{logg}_he{logHe}_c{logC}_n{logN}_o{logO}`
### 1.2 求解器与初猜问题
TLUSTY 求解统计平衡方程(能级布居速率方程)、辐射转移与能量方程的强非线性耦合系统,核心算法为完全线性化(complete linearizationAuer & Mihalas 1969TLUSTY 实现:Hubeny 1988),并辅以 Kantorovich 型雅可比更新与 Ng 加速。此类牛顿型迭代对初猜高度敏感:仅当初猜位于真解的吸引域内时迭代才收敛。本文的核心问题即——**对每个网格点,如何为其构造一个落在吸引域内的初猜,以及当初猜难以获得时如何延拓逼近**(方法学文献见文末参考文献,按主题与本文小节对应)。
### 1.3 收敛判据与物理有效性检验
"收敛"采用 TLUSTY 自身判据:相邻迭代间全部深度上各求解量(温度结构、能级布居)的最大相对变化 CHMAX < 1×10⁻³(收敛史记录于 fort.9 文件),且末次迭代全深度均满足。收敛仅表明迭代达到不动点,**不保证是物理解**,故所有收敛结果另需通过五项物理有效性检验:
1. 温度结构合理性检验(温度—深度分布单调性与量级);
2. 迭代收敛判据(fort.9 末次迭代全深度最大相对变化 < 1×10⁻³);
3. 数值有效性(全部输出无 NaN/Inf);
4. 波长积分流量守恒(bolometric flux:∫F_λ dλ ≈ σT_eff⁴,实测比值如 1.0013;该检验曾存在遗漏 4π 立体角因子的程序缺陷,2026-08-17 修正);
5. 能量守恒检验。
收敛策略只负责把迭代带入不动点;结果是否为物理上可接受的大气模型,由上述检验统一裁决。
---
## 二、收敛策略体系总览
策略体系分两级:**策略链**`[冷启动, 热启动, 稳定化热启动]`,由调度端逐项回退,热启动失败后按距离度量轮换初猜模型),以及每条策略链内部的**链内延拓回退**(Teff 自适应延拓、固定阶梯、稳定化多档、丰度轴延拓)。
| 收敛方法 | 点数 | 占比 | 物理内涵 |
|---|---|---|---|
| `cold_run`(冷启动) | 5573 | 60.5% | 从灰大气 LTE 解出发,分级松弛至全非 LTE |
| `seed_step`(热启动) | 3573 | 38.8% | 以同物理族近邻的收敛模型为初猜 |
| `seed_step_stab`(稳定化热启动) | 70 | 0.8% | 热启动链叠加布居稳定化参数,用于低温富氦区 |
(含 9 个 2026-08-01/02 完成的早期网格点,其状态字段晚于完成时间引入而为空值,收敛性已由任务记录与初猜复用记录独立验证,详见技术附注。)
链内延拓在最终成功任务中的使用量:稳定化多档 47 点、Teff 自适应延拓 12 点、C/N 丰度轴延拓 4 点、固定阶梯末步 3 点——使用频率低,但均为最困难网格点的决定性路径。
---
## 三、各策略的构造与物理依据
### 3.1 冷启动链:LTE → 连续谱非 LTE → 全非 LTE
| 阶段 | 主要设置 | 迭代上限 | 物理含义 |
|---|---|---|---|
| `lte` | LTE 解(灰大气初猜) | — | 布居仅由局域温度、密度决定的零级近似 |
| `nc` | 关闭谱线 | 10 | 连续谱不透明度自洽的非 LTE 预解 |
| `nl` | 全谱线(ILVLIN=100),强制收敛 | 100 | 全谱线统计平衡 + 辐射转移耦合终解 |
**依据**:LTE 解虽然在高温低密度大气中偏离真实解很远,但提供了一个"拓扑正确"的起点;先在连续谱层面引入非 LTE,再放开全部谱线,使非线性逐级进入迭代。这是 Hubeny/Lanz 分级松弛的经典构造。适用域为参数平缓、LTE 初猜仍在吸引域内的区域:实测 Teff ≤ 35 kK 区间冷启动成功率 71–77%。
### 3.2 热启动链:近邻收敛模型初猜 + 参数延拓
| 阶段 | 主要设置 | 迭代上限 | 物理含义 |
|---|---|---|---|
| `seed_nc` | 关闭谱线,松弛因子 0.3 | 20 | 以近邻模型为初猜的连续谱预适应 |
| `nl` | 全谱线,松弛因子 0.5,强制收敛 | 100 | 终解(欠松弛抑制丰度突变处的过冲) |
**依据**:大气结构随 (Teff, log g, 丰度) 连续变化,即静力解构成参数空间中的连续解分支;一个已收敛的近邻模型(本文取严格同物理族:Teff/log g/log He 相同、仅 CNO 丰度不同)比 LTE 初猜距离目标解近若干数量级。松弛因子 0.3/0.5 表示仅施加部分牛顿修正(欠松弛),以适应丰度改变引起的陡峭响应。
**初猜选择度量**:按 CNO 距离取最近邻,但区分方向——目标相对初猜"减丰"方向权重 ×1、"增丰"方向 ×4(增丰使金属线不透明度上升,非线性更强);初猜失败后排除已用模型逐次轮换。
**参数延拓**:实测单步收敛域约为 ΔTeff ≤ 2500 K 且 Δlog g ≤ 0.25 dex,当初猜与目标参数差超出该域时:
- **Teff 自适应延拓**:从初猜 Teff 向目标逐步推进,步长 1250 K(与网格间距一致),失败步二分(下限 25 K,触底判定该方向解分支折叠,中止),最多 48 步;中间收敛模型自动登记为可用初猜。
- **固定阶梯**:沿差值更大的轴均匀切分(中间步 ≤ 4),末步强制收敛判据。
**依据**:解分支沿参数曲面连续延伸,但一步跨越过大将离开吸引域;小步延拓(continuation method)即逐点跟踪解分支,步长自适应是伪弧长延拓的简化实现。
### 3.3 稳定化热启动链:低温富氦区的布居极限环
热启动链各步叠加两个 TLUSTY 稳定化参数:**DPSILG**(限制能级布居单次迭代的最大相对变化,即布居阻尼)与 **POPZER**(布居置零阈值,低于该值的布居置零,消除深壳层痕量能级的数值噪声)。此外设三档链内回退(DPSILG, POPZER = (2.0, 3×10⁻¹¹), (2.0, 1×10⁻¹¹), (2.5, 1×10⁻¹¹)),在常规路径全部失败后自原始初猜逐档重试。
**依据**:20 kK 富氦大气的病灶是 He I/He II 电离前沿处能级布居的**极限环振荡**——电离平衡陡变区对温度扰动的响应使迭代映射出现周期性数值不稳定,而静力解本身存在。布居阻尼抑制振荡增幅,微布居置零消除噪声源。批量实测(65 个困难点)三档互补,联合回收 41 点,全部通过温度结构检验。
**域边界**:该族参数仅在 Teff ≤ 30 kK 有效;高温区实测对本已可收敛的迭代加档反而致散(17 次迭代后相对变化发散至 4×10¹⁶),故按温度域门控启用。
### 3.4 丰度轴延拓:高温高金属区的路径切换
Teff 60 kK、log g 5.0 的高金属角,实测 Teff 轴、log g 轴、O 丰度轴延拓全部在目标前折叠,而 **C 或 N 丰度轴延拓全部走通**——静力解存在,只是可达方向在 C/N。配方:严格同物理族初猜沿 C 轴(优先,3 个测试点全部验证)或 N 轴向目标丰度延拓:初始步长 0.2 dex,成功 ×1.4(上限 0.25 dex),失败二分(下限 0.025 dex),每轴 ≤ 40 步,失败步产物不作为后续初猜。仅 Teff > 30 kK 域启用(低温区由稳定化档覆盖)。
**依据**:CNO 丰度直接改变金属线不透明度与辐射加速,结构响应是非线性的,但解分支沿不同参数方向的连通性不同——延拓方向本身是自由度,"更换延拓路径"与"调整延拓步长"同属延拓法的基本手段。
### 3.5 延拓阶段迭代上限(100 → 300)
全部延拓/阶梯阶段的迭代上限由 100 提升至 300。生产实证(t60000_g5.0_he-4_c-4_n-4_o-1N 轴延拓在 log N = 3.53 处):迭代残差 0.002–0.005 在 100 次迭代上限处仍单调下降——表明迭代仍在正常收敛进程中被上限过早截断,并非发散;此前该现象被误判为解分支折叠。TLUSTY 达到收敛即停止,提高上限只对慢收敛阶段给出余量,不影响快阶段,也不影响真发散阶段的提前中止。最终网格点即经此修复后收敛:丰度轴延拓三段分别以 107/107/175 次迭代收敛(全部超过旧上限 100),终解最大相对变化 9.46×10⁻⁴。
---
## 四、收敛方法在参数空间中的分布及其物理解释
### 4.1 有效温度(每档 1024 点)
| Teff | 冷启动 | 热启动 | 稳定化 | 冷启动占比 |
|---|---|---|---|---|
| 20 kK | 725 | 233 | 66 | 70.8% |
| 25 kK | 793 | 231 | 0 | 77.4% |
| 30 kK | 760 | 264 | 0 | 74.2% |
| 35 kK | 756 | 268 | 0 | 73.8% |
| 40 kK | 587 | 437 | 0 | 57.3% |
| 45 kK | 525 | 499 | 0 | 51.3% |
| 50 kK | 558 | 466 | 0 | 54.5% |
| 55 kK | 457 | 567 | 0 | 44.6% |
| 60 kK | 412 | 608 | 4 | 40.2% |
**解释**Teff ≤ 35 kK 冷启动主导(约 74%),≥ 40 kK 热启动主导。有效温度升高时辐射场增强而电子碰撞速率相对下降,能级布居的偏离系数(departure coefficients)增大,LTE 初猜(偏离系数恒为 1)远离真实解,牛顿迭代起点落在吸引域之外;而已收敛的近邻模型始终位于同一解分支附近,是更可靠的内插起点。
### 4.2 表面重力(每档 2304 点)
| log g | 热启动占比 | 稳定化点数 |
|---|---|---|
| 5.0 | 55.1% | 4 |
| 5.5 | 47.1% | 4 |
| 6.0 | 31.3% | 18 |
| 6.5 | 21.6% | 44 |
**解释**:低重力对应低密度大气,电子碰撞激发/电离速率下降,布居由辐射场主导,与 LTE 偏离更大,冷启动更难。稳定化点数则向高重力集中——布居极限环需要足够密集的 He 电离前沿区。
### 4.3 氦丰度(每档 2304 点)
| log He | 冷启动占比 | 稳定化点数 |
|---|---|---|
| 4.0 | 57.2% | 3 |
| 2.0 | 54.7% | 1 |
| 0.0 | 62.4% | 0 |
| +2.0 | 67.6% | 66 |
**解释**:氦是主要电子施主,氦丰度升高使电子密度上升、碰撞耦合增强,布居更接近 LTE 行为,冷启动反而更容易。唯一的例外正是 20 kK 富氦角——66 个稳定化点集中于 (Teff = 20 kK, log He = +2, log g = 5.56.5),物理上"应当容易"的参数组合恰是 He I/He II 电离前沿极限环的发生区,需要最强的数值稳定化。
### 4.4 CNO 丰度轴(各轴 4 值 × 2304 点)
C 轴冷启动占比 58.761.8%N 轴 57.364.1%O 轴 60.461.1%——分布近乎平坦(±3%)。在本网格的 CNO 丰度范围内(各跨 3 dex),金属丰度不是收敛难度的主控因素(相对 Teff/log g/He 而言);丰度轴延拓仅在高温高金属角的个别点成为必经路径。
---
## 五、初猜模型复用网络
热启动是一个自我强化的复用体系:全部计算中共执行 9203 次以其他点收敛模型为初猜的任务,**2727 个网格点(占 29.6%)至少一次被用作初猜供体**。供体分布高度集中于 55–60 kK、log g 5.0–5.5 区域(热启动最主要的需求区),形成"先收敛的骨干模型带动邻近区域"的传播结构。供体使用频次前列:
| 供体模型 | 被用作初猜次数 |
|---|---|
| t55000_g5.0_he-2_c-3_n-4_o-4 | 222 |
| t50000_g5.0_he-2_c-1_n-2_o-2 | 153 |
| t60000_g5.0_he-4_c-3_n-2_o-4 | 121 |
| t55000_g5.0_he-4_c-3_n-2_o-2 | 105 |
| t60000_g5.0_he-2_c-1_n-2_o-2 | 101 |
---
## 六、收敛所需计算时间
每个网格点记录其最终成功任务(制胜任务)的实际执行时长(单机执行期导入的成果为其单机运行记录,量级与分布式执行一致:4335 个此类点平均 27.2 分钟、跨度 9.5–153.1 分钟)。全部 9216 点的制胜任务耗时按方法统计:
| 方法 | 点数 | 中位数 | P75 | P90 | 平均 | 最长 |
|---|---|---|---|---|---|---|
| 冷启动 | 5573 | 24.3 min | 29.1 min | 47.7 min | 31.7 min | 141.5 min |
| 热启动 | 3573 | 36.2 min | 49.2 min | 94.9 min | 47.1 min | 153.1 min |
| 稳定化热启动 | 70 | 29.9 min | 47.5 min | 63.8 min | 36.4 min | 123.2 min |
冷启动任务的耗时中位数反而最短:冷启动得以成功的点本就是参数平缓区域,迭代次数少;热启动更长(中位数 36 分钟、P90 达 95 分钟)——需要热启动的点收敛更慢,且参数延拓(Teff 延拓最多 48 步、丰度轴延拓最多 40 步)作为链内回退全部计入单任务时长,慢收敛长尾即由此而来。按 Teff 分层的中位数在 22–38 分钟间无系统趋势,P90 的层间差异(34–99 分钟)主要反映各层困难点比例与所在批次节点的硬件差异。稳定化任务时长介于两者之间——稳定化旋钮本身不显著增加单步成本,其耗时结构需按两层重试口径分别核算:
- **任务内链内重试已全部计入制胜任务时长**:多档稳定化(自原始初猜逐档重跑最后收敛阶段)与延拓中间模型的逐段计算均发生在同一任务内。70 个稳定化点的制胜任务中 47 个含多档重试、4 个含延拓阶段;典型如某一制胜任务内 seed_nc → nl → nl_direct → nl_stab2(未收敛)→ nl_stab3(收敛)五段合计约 30 分钟,全部计入该点时长(与表中位数一致)。
- **跨任务重试不计入制胜时长,仅计入全周期总账**:初猜轮换与策略回退产生的每次独立任务各有自己的执行时长。70 个稳定化点的制胜任务合计仅 42.4 核时,但其全部 1588 次任务执行合计 926 核时(平均每点 22.7 次任务、13.2 核时)——全周期成本约为制胜成本的 22 倍,是全网格中重试强度最高的群体(作为对比,全网格平均为 2.4 倍)。
**总量核算(核时)**
- 制胜任务合计 **5791 小时**(约 241 核日),单点平均 37.7 分钟;
- 计入全部失败尝试后,任务执行总量 **14038 小时**(约 585 核日),为制胜时长的 2.4 倍——即平均每个网格点的全周期投入约 1.5 核时,其中约 40% 消耗于最终未被采纳的尝试;743 个任务接近 7200 秒超时上限;
- 光谱合成(SYNSPEC)单点约 4 秒(单机期实测 3.6 秒),相对大气计算可忽略;本网格当前阶段的分布式任务仅含大气计算。
时长均为节点实际执行的墙钟时间,不同批次节点硬件不同,层间横向比较包含硬件异质性。
---
## 七、策略体系的演化:基于 21384 次任务记录
本节回答"每一层策略是哪类失败引入的"。计算分两段:**单机执行期**2026-07-15 起,07-29 止)以单机流程完成三千余点收敛后,转入**分布式阶段**(07-28 框架首提交、07-30 首批任务入库),共执行 21384 个任务(平均每点 2.3 次),其中收敛 6283、失败/超时 15101,任务级成功率 29.4%——策略体系是随失败类型逐步建构的。按时间聚合为十个阶段:
| 阶段 | 日期 | 任务数(收敛/失败) | 失败特征 | 诊断与变更 |
|---|---|---|---|---|
| ⓪ 单机执行期 | 07-15→07-29git:单机管线 07-15/23/27 三次提交;分布式框架 07-28 首提交) | 约 4300 点收敛(无分布式任务记录) | nc 阶段发散;CNO 谱线全 NaN;宽松判据伪收敛(见 7.2) | 单机管线(16 核并行、断点续算、失败隔离)确立三步法收敛链与"金属谱线自洽进入大气模型"方案(纯 H+He 大气无法产生 CNO 谱线),并实现最近邻初猜复用与冷启动失败→热启动回退——此即策略链体系的原型;定型求解器配方(收敛判据取默认 1×10⁻³、nc 步 NITER=10、He 模型原子 14 能级、NFREAD=2000)。完成约 4300 点收敛(数据库中无分布式完成记录的收敛点共 4335 个:冷启动 2898、热启动 1437,其中 3876 个从未被分布式系统重算);成果经导入工具写入数据库,构成分布式初期的初猜库——07-30/31 首批 1252 个热启动任务中 1249 个的初猜供体属此类无任务记录的点;07-30 分布式框架上线接管剩余计算 |
| ① 分布式初期 | 07-30→08-04 | 88944978/3916 | 大气输出 >10% NaN 行 1775 例 | 高温低重力区冷启动大量发散;热启动(最近邻初猜复用)继承自单机管线并自始使用,08-04 将其正式化为策略链体系 |
| ② 物理检验强化期 | 08-06→08-10 | 79050/7905 | NaN/Inf 大气 2707 例;流量比 ≈0.08(后证实为检验缺陷);光谱整表 NaN | 五项物理检验与输入文件结构化上线,输入谱线数据错位与检验程序缺陷集中暴露,当周零收敛 |
| ③ 根因分析与方案证伪 | 08-11→08-14 | 0(停机分析) | — | 根因定位为吸引域狭窄且缺乏自适应初猜机制;数值参数调整方案经 6 点 × 4 配置 = 24 次对照实验全部证伪(见 7.2),方向转向初猜质量 |
| ④ 热启动批量复算 | 08-15→08-16 | 3162905/2257 | 非收敛发散 | 以热启动规模化重算存量失败点 |
| ⑤ 参数延拓引入 | 08-17 | 547279/268) | 初猜与目标参数差超出单步收敛域 | 引入固定阶梯与 Teff 自适应延拓(实测确定单步收敛域);修正流量检验 4π 因子缺陷;增设 NaN 伪收敛否决 |
| ⑥ 低温富氦区稳定化 | 08-18→08-19 | 68277/605 | 20 kK 富氦区布居极限环 | 引入稳定化热启动策略与三档 DPSILG/POPZER 回退(65 困难点联合回收 41) |
| ⑦ 初猜轮换机制 | 08-20 | 0(离线开发) | 同一初猜反复失败 | 调度端排除已用初猜,按方向加权距离轮换最近未用近邻 |
| ⑧ 高温高金属区丰度延拓 | 08-21 | 18943/146 | 60 kK/log g 5.0 角多轴折叠 | 引入 C/N 丰度轴延拓;稳定化参数按温度域门控;延拓迭代上限 100→300 |
| ⑨ 收尾 | 08-22→08-24 | 5(1/4) | 完成状态翻转与回退派发的竞态 | 最终点 t60000_g5.0_he-4_c-4_n-4_o-1 经初猜轮换(N 丰度 −3.62 中间模型)+ 丰度轴延拓收敛(107/107/175 次迭代);竞态由数据库层约束修复 |
策略链形式的演化轨迹(任务记录中各链形式的首现日期):
```
07-15 单机管线(原型):冷启动链 + 冷启动失败→最近邻热启动回退
07-30 [cold_run] ← 分布式初期纯冷启动(早期记录归档回填)
08-04 [cold_run, seed_step] ← 策略链体系上线,热启动入链
08-19 [cold_run, seed_step, seed_step_stab] ← 稳定化补入链尾
```
### 7.1 分布注释
网格点的重试计数字段为分布式阶段后期引入;计零的 3876 个点即单机执行期导入、从未被分布式系统重算的成果(其中热启动完成 1056 个),故重试分布统计仅对分布式阶段有效。重试 ≥ 16 次的 114 个点几乎全部落在稳定化策略覆盖的两个困难区域;单点最深重试 103 次,属调度早期配置不当的遗留记录。
### 7.2 五次被证伪的方案及其方法论价值
前两次证伪发生在单机执行期(记录于 `cno_grid/EXPERIENCE.md`):
1. **宽松收敛判据导致伪收敛**(单机期):早期以 CHMAX = 0.1 运行,曾据以得出"全部边界点收敛"的结论;改回默认判据 1×10⁻³ 复测后该结论被推翻。教训:收敛结论的有效性完全取决于判据的严格性——判据取值须与求解器默认值一致,任何放宽都需单独论证并复测。
2. **丰度输入约定误读**(单机期):TLUSTY 的 abn 输入字段语义为 0 = 太阳丰度、负值 = 太阳丰度的倍数、正值 = 绝对数密度比 N(X)/N(H);早期将 logC = 0 等取值直接换算写入,实为 C/H = 1.0(约 4000 倍太阳丰度),nc 阶段因此普遍发散,且一度被误归因于高温区物理。教训:非标准输入约定必须对照求解器手册逐字段核实。
3. **数值阻尼与加速参数无法弥补初猜缺陷**08-12 证伪):调整 Kantorovich/ Ng 加速与布居阻尼(ITEK/IACC/DPSILG)的 24 次对照实验 0 次收敛——其中 ITEK=0 实为冻结布居、IACC=0 被求解器内部钳制为空操作;真正禁用加速后发散幅度下降 10¹²–10¹⁴ 倍但仍不收敛(末次残差 10³–10⁷)。结论:牛顿型迭代起点在吸引域之外时,松弛与加速参数均无效;**初猜质量优先于数值参数**。附带教训:此前"禁用加速后残差降至 0.2"的结论源于收敛史文件(fort.9)的部分读取假象——TLUSTY 逐深度写入、缓冲未刷新时只读到部分深度的最大值。
4. **检验程序自身须经检验**08-06 引入,08-17 修正):流量守恒检验遗漏 4π 立体角因子,将正确的波长积分流量(∫F_λdλ/σT_eff⁴ ≈ 0.08)判为失败约千例。新检验上线引发的失败潮中,须先审查检验自身再归因于物理。
5. **迭代上限耗尽不等于发散**(08-21 修正):残差在上限处仍单调下降,说明迭代仍在收敛进程中被上限截断,并非解分支折叠;延拓迭代上限提升至 300 后,最终点三段延拓以超过旧上限的迭代数(107/107/175)收敛,构成直接反证。
---
## 八、结论
sdB_cno 网格 9216 个模型大气的收敛路径构成为:60.5% 分级松弛冷启动(低 Teff、高 log g、富 He 的平缓参数域),38.8% 近邻收敛模型热启动(高 Teff、低 log g 的强非 LTE 域,29.6% 的网格点被复用为初猜供体,形成自强化传播网络),0.8% 稳定化热启动(20 kK 富氦角 He I/He II 电离前沿布居极限环);叠加三类链内延拓(Teff 自适应延拓、稳定化多档、C/N 丰度轴延拓)与延拓迭代上限 300,处理"静力解存在但延拓路径曲折"的极端网格点。策略分布与恒星大气物理一致:非 LTE 偏离程度(由 Teff、log g、电子施主丰度控制)决定冷启动初猜的适用边界。全部结果通过统一的五项物理有效性检验——策略决定如何到达收敛域,物理解由检验裁决。
---
## 参考文献(按主题,与本文小节对应)
**求解器与控制参数**
1. Auer, L. H., & Mihalas, D. 1969, ApJ, 158, 641 —— 完全线性化方法原始文献(§1.2、§3.1 的方法出处)。
2. Hubeny, I. 1988, Comput. Phys. Commun., 52, 103 —— TLUSTY 程序原始文献。
3. Hubeny, I., & Lanz, T. 2017, arXiv:1706.01859(用户指南卷 I:简明指南)、arXiv:1706.01935(卷 II:参考手册——迭代控制参数的权威定义所在;同系列另有卷 III 操作手册)。
4. Mihalas, D. 1978, Stellar Atmospheres, 2nd ed.San Francisco: Freeman);Hubeny, I., & Mihalas, D. 2014, Theory of Stellar AtmospheresPrinceton Univ. Press)——教科书;开放获取 NLTE 入门综述:Kubát, J. 2015, in Determination of Atmospheric Parameters of B-, A-, F- and G-Type Stars, ed. E. Niemczura et al.Springer),arXiv:1406.3553"Basics of the NLTE physics")。
**迭代加速与统计平衡求解**
5. Ng, K.-C. 1974, J. Chem. Phys., 61, 2680 —— Ng 加速方法原始文献(本文§7.2 证伪实验涉及的 IACC/Ng 加速)。
6. Olson, G. L., Auer, L. H., & Buchler, J.-R. 1986, JQSRT, 35, 431 ——对角 Λ 算子近似,加速 Λ 迭代(ALI)。
7. Rybicki, G. B., & Hummer, D. G. 1991, A&A, 245, 171 —— 多能级 ALI 与预条件统计平衡方程。
8. Hubeny, I., Lanz, T., & Jeffery, C. S. 1994, A&A, 282, 151 —— 混合完全线性化/ALI 处理金属线覆盖(TLUSTY 现代架构的基础)。
**网格延拓与初猜实践**
9. Lanz, T., & Hubeny, I. 2003, ApJS, 146, 417 —— OSTAR2002 网格论文;以邻近已收敛模型为初猜逐点推进网格的标准实践(§3.2 的直接先例)。
10. Lanz, T., & Hubeny, I. 2007, ApJS, 169, 83 —— BSTAR2006 网格(同上)。
11. Keller, H. B. 1977, in Applications of Bifurcation Theory, ed. P. H. RabinowitzNew York: Academic Press, 359 —— 伪弧长延拓,参数延拓法与折叠点处理的经典文献(§3.2/§3.4 的方法学基础)。
12. Allgower, E. L., & Georg, K. 2003, Introduction to Numerical Continuation MethodsSIAM, Classics in Applied Mathematics 451990 年 Springer 初版书名为 Numerical Continuation Methods: An Introduction)—— 延拓法教科书(可选)。
**热亚矮星(sdB/sdO)应用**
13. Heber, U. 2016, PASP, 128, 082001 —— 热亚矮星综述(§1.1 科学背景)。
14. Németh, P. 2012, MNRAS, 427, 2180 —— TLUSTY/SYNSPEC 分析热亚矮星样本。
15. Németh, P. 2014, ASP Conf. Ser., 481, 95 —— O/B 型亚矮星合成光谱网格。
16. Pacheco, T. A., et al. 2021, ApJS, 256, 41 —— 亚矮星非 LTE 线覆盖大气模型与光谱网格(H/He 与金属丰度维度,与本文网格同类);更新版 arXiv:2307.08362。
**关于原创性的说明**:§3.3 的稳定化档位配方(DPSILG/POPZER 逐档组合)、布居极限环的判别-处置流程、§3.2 的方向加权初猜距离与轮换调度,属本项目工程实现,文献中未见面世;上述文献支撑的是其方法学基础——完全线性化对初猜的敏感性、参数延拓、迭代加速、以及网格逐点热启动的通行实践。参数语义的权威定义见 Hubeny & Lanz (2017) 卷 II。
---
## 技术附注(工程细节,与物理结论无关)
- **数据来源**server 端 SQLite 快照;`grid_points` 表记录逐点最终状态与方法、制胜任务时长(`last_elapsed_sec`),`tasks` 表记录每次任务执行(时间戳、耗时、初猜供体、失败阶段、错误摘要、策略链)。
- **单机执行期版本记录**:单机代码仓库为 tl208-s54git 首提交 2026-07-15cno_grid 单机管线于 07-15/23/27 三次提交,run_grid.py 含笛卡尔积网格、断点续算、最近邻初猜复用、冷启动失败→热启动回退、失败隔离、16 核并行);调试史与配方复盘见 `cno_grid/EXPERIENCE.md`(如实记录 8 项已纠正的错误)。分布式框架 dcts 为该仓库内嵌的独立 git 仓库,首提交 2026-07-28。
- **单机执行期成果的数据库痕迹**:无分布式完成记录的收敛点共 4335 个(冷启动 2898、热启动 1437):3876 个无任何任务行且重试计数为零(从未被分布式系统碰过);另 459 个曾被分布式阶段重试(重试计数 > 0)但未产生完成记录,保留单机期结果。07-30/31 首批热启动任务 1252 个中 1249 个的初猜供体属于此类点,即分布式初期的初猜库直接继承自单机期成果。
- **9 个空状态历史点**:完成于 2026-08-01/02(全管线、末次迭代最大相对变化 1.14×10⁻⁴–9.33×10⁻⁴),早于阶段状态列引入时间(08-06,加列不回填),此后无任务再执行;数据库迁移 M14 将按"已完成 + 方法已记录"条件回填为收敛,快照实测恰为 9 行。9 点均登记为可用初猜供体,其中 6 点被后续任务实际使用。
- **光谱合成**:本工作流 SYNSPEC 阶段当前关闭;历史记录中 459 个光谱失败与 1305 个空值属早期 TLUSTY-only 重算批,不在本文收敛判定范围。
- **策略链字段**:08-04 策略体系上线前的任务记录(含 07-31 批 1252 个热启动任务)归档时统一回填为 `["cold_run"]`,此后记录携带真实链形式。
- **代码版本**:阶段⑥⑨(稳定化、初猜轮换、丰度轴延拓、迭代上限、竞态修复)的实现位于版本控制工作树,最后提交停留在 2026-08-17b058e66)。
+4
View File
@@ -45,6 +45,10 @@
- **原因**:在较长时效(如1-2小时)的运算完结回传一瞬间,Server 恰遇热更重启或遭遇防火墙短暂会话剔除。
- **容灾机制**:Worker 内置了超强的 8 轮指数级自适应退避长跳上报防护(跨度可自 1s 到 60s 顺次延迟,支撑 2 分钟以上的长时断裂耐受窗口);上报失败时产物仍会**尽力归档**到本地 `DCTS_RESULT_DIR/<model_name>/`(白名单保留科学产物),沙盒随后清理。若真正超出了总重试界限,可从 `result_dir/<model_name>/` 直接调出最终收敛物(`.7`/`.spec` 等)手工补录。
### 现象 4Windows 节点重启后掉线(Docker Desktop 未自启)
- **原因**Docker Desktop 是 GUI 程序,必须有所属用户的**交互桌面会话**才能启动引擎;重启后无人登录桌面时,即使配了 AutoStart 引擎也不会起来(节点小宝网络链路本身 Auto 自启,不受影响)。
- **解决办法**:经跳板 SSH 隧道转发 RDP 登录对应账户桌面,Docker 引擎随登录自启、`dcts-node` 容器靠 restart 策略自动恢复。完整拓扑、凭据、隧道命令与诊断速查见 **[remote_desktop_via_jump.md](remote_desktop_via_jump.md)**。
---
## 3. 日志与现场诊断
+5 -1
View File
@@ -184,7 +184,11 @@ synspec_input:
tlusty_stage:
enabled: true
policy: skip_converged
strategies: [cold_run, seed_step]
# seed_step_stab:稳定化种子步进(2026-08-18)——同物理族(同 Teff/logg/logHe
# 不同 CNO 邻居种子 + POPZER=1E-10 微布居置零 + DPSILG=3.0 λ 算子欠松弛。
# 针对 20kK He 富大气 He I/II 电离前沿布居极限环(seed_step 全部发散的难收敛角落),
# 实测代表点收敛且五重物理硬门全过,见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md。
strategies: [cold_run, seed_step, seed_step_stab]
# ---- 种子步进回退(旧字段,保留兼容;新配置见下方 tlusty_stage 块)----
# 冷启动失败时,自动用已收敛的邻居模型作种子,用 LTGRAY=F 热启动重试。