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
+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())