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
+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 {