2
This commit is contained in:
parent
c53fc99c1d
commit
92d9c515c9
194
cno_grid/DIST.md
Normal file
194
cno_grid/DIST.md
Normal file
@ -0,0 +1,194 @@
|
||||
# 分布式网格计算部署指南(master 单向 SSH)
|
||||
|
||||
> 把 CNO 网格计算分发到多台 Linux 服务器并行。适配"大部分机器异地、
|
||||
> 只有 master 能 SSH 进 worker"的网络。
|
||||
|
||||
## 1. 架构(master 单向 SSH 推/拉)
|
||||
|
||||
```
|
||||
master(WSL2) 异地 worker
|
||||
tasks.json (本地 flock 安全) (无常驻进程)
|
||||
├ 派任务: ssh worker "nohup run_one.py ─▶ 被动执行单点
|
||||
│ --teff X ... &" 跑完写本地 conv.json + .7
|
||||
├ 取状态: ssh worker "python3 dist_check ◀─ 被动提供结果
|
||||
│ .py --results results"
|
||||
├ 取种子: scp worker:.7 master(收敛点) ◀─
|
||||
└ 七牛云(种子库): master 代传 .7 ─▶ 七牛
|
||||
```
|
||||
|
||||
**核心原则**:
|
||||
- **只需 master → worker 单向 SSH**(worker 在 NAT 后也行,不回连 master)
|
||||
- **任务队列只在 master 本地**(`fcntl.flock` 强一致,零并发风险)
|
||||
- **worker 无常驻进程**:master 直接 SSH 执行 `run_one.py`/`seed_step.py` 跑单点
|
||||
- **冷启动零依赖**(seed=None,LTE grey 自建大气)→ 绝大多数点完美分布式
|
||||
- **种子库放七牛云**(仅 `.7` 大气 393KB/点)→ 供 seed_step 回退
|
||||
- **大产物留 worker 本地**(`.spec` 等 13MB)→ 不跨网传
|
||||
|
||||
## 2. 并发安全(重点)
|
||||
|
||||
任务队列 `tasks.json` **只在 master 本地文件**,用 `fcntl.flock` 排他锁保护。
|
||||
master 多线程管理多台 worker,但所有 claim/report 都走本地 flock,**无跨机并发问题**。
|
||||
worker 完全不碰 tasks.json。
|
||||
|
||||
## 3. 一次性环境准备
|
||||
|
||||
### 3.1 master(本机 WSL2)
|
||||
```bash
|
||||
# pyyaml 可选(有则用,无则用内置极简解析器,零 pip 也能跑)
|
||||
pip3 install pyyaml 2>/dev/null || echo "用内置解析器,无需 pip"
|
||||
```
|
||||
|
||||
### 3.2 每台 worker(异地机器)
|
||||
只需:
|
||||
- **python3**(系统自带)
|
||||
- **openssh-server**(让 master 能 SSH 进来)
|
||||
- **tlusty 树**(二进制 + 原子数据)
|
||||
|
||||
```bash
|
||||
sudo apt install python3 openssh-server
|
||||
```
|
||||
> **worker 不需要 curl、不需要 pip、不需要七牛配置、不需要 SSH 回 master。**
|
||||
|
||||
### 3.3 分发 TLUSTY 树到每台 worker(首次,约 2GB)
|
||||
在 master 上对每台 worker:
|
||||
```bash
|
||||
sudo apt install rsync # master 端装一次
|
||||
TARGET=user@worker1
|
||||
REMOTE_ROOT=~/tlusty/tl208-s54 # 记到 dist_config.yaml 的 tlusty_root
|
||||
rsync -avz --exclude='cno_grid/results' --exclude='*.log' \
|
||||
/home/dckj/program/tlusty/tl208-s54/ $TARGET:$REMOTE_ROOT/
|
||||
```
|
||||
|
||||
### 3.4 分发 cno_grid 代码到每台 worker(每次代码更新)
|
||||
```bash
|
||||
rsync -avz --exclude='results' --exclude='*.log' --exclude='__pycache__' \
|
||||
/home/dckj/program/tlusty/tl208-s54/cno_grid/ \
|
||||
$TARGET:$REMOTE_ROOT/cno_grid/
|
||||
```
|
||||
|
||||
### 3.5 配置 SSH 免密(master → worker,单向即可)
|
||||
```bash
|
||||
ssh-copy-id user@worker1
|
||||
ssh-copy-id user@worker2
|
||||
# ...
|
||||
```
|
||||
验证:`ssh user@worker1 echo ok` 应直接输出 `ok`,不问密码。
|
||||
**worker 不需要 SSH 回 master。**
|
||||
|
||||
### 3.6 七牛云(可选;不配也能跑,只是没跨机种子共享)
|
||||
1. 七牛云创建 bucket,拿 AK/SK,绑域名
|
||||
2. master 设环境变量:
|
||||
```bash
|
||||
export QINIU_ACCESS_KEY="你的AK"
|
||||
export QINIU_SECRET_KEY="你的SK"
|
||||
```
|
||||
> 不配七牛时,seed_step 只能用同台 worker 上已收敛的本地种子。
|
||||
|
||||
## 4. 配置 dist_config.yaml
|
||||
|
||||
```yaml
|
||||
master:
|
||||
workdir: /home/dckj/program/tlusty/tl208-s54/cno_grid
|
||||
grid_config: /home/dckj/program/tlusty/tl208-s54/cno_grid/config.yaml
|
||||
results: results
|
||||
|
||||
qiniu:
|
||||
bucket: your-bucket
|
||||
domain: http://xxx # 留空 = 纯本地模式
|
||||
upload_by: master # master 代传种子(worker 不碰七牛)
|
||||
|
||||
workers:
|
||||
- host: localhost # 本机也算 worker(可选)
|
||||
tlusty_root: /home/dckj/program/tlusty/tl208-s54
|
||||
nproc: 8
|
||||
- host: user@worker1 # 异地机器
|
||||
tlusty_root: ~/tlusty/tl208-s54
|
||||
nproc: 24
|
||||
# ... 每台机器一项
|
||||
```
|
||||
|
||||
## 5. 运行
|
||||
|
||||
### 5.1 预览(dry-run)
|
||||
```bash
|
||||
cd /home/dckj/program/tlusty/tl208-s54/cno_grid
|
||||
export QINIU_ACCESS_KEY=... QINIU_SECRET_KEY=...
|
||||
python3 src/dist_master.py dist_config.yaml --dry-run
|
||||
# 输出:待计算 N 点,总并发 X 核,预计 ~Y 小时 + 部署检查清单
|
||||
```
|
||||
|
||||
### 5.2 全量跑
|
||||
```bash
|
||||
export QINIU_ACCESS_KEY=... QINIU_SECRET_KEY=...
|
||||
nohup python3 src/dist_master.py dist_config.yaml > results/dist_run.log 2>&1 &
|
||||
```
|
||||
|
||||
### 5.3 监控
|
||||
```bash
|
||||
tail -f results/dist_run.log # master 视角(每 20s 打印状态计数)
|
||||
cat results/grid_status.json # 汇总
|
||||
# 看某台 worker 的某个任务日志:
|
||||
ssh user@worker1 "tail ~/tlusty/tl208-s54/cno_grid/worker_jobs/<模型名>.log"
|
||||
```
|
||||
|
||||
### 5.4 断点续算
|
||||
中断后重跑同一命令,已 `done` 的点自动跳过(tasks.json 持久化在 master)。
|
||||
|
||||
## 6. 工作流程(master 内部)
|
||||
|
||||
```
|
||||
1. init tasks.json(432 点 pending;本地已 converged 标 done)
|
||||
2. 每台 worker 一个管理线程,并发跑:
|
||||
循环:
|
||||
a. SSH 查 worker 正在跑的进程数 → 空闲槽 = nproc - running
|
||||
b. 每个空闲槽:本地 flock 领一个 pending→running,SSH 派 run_one.py(nohup)
|
||||
c. SSH 调 dist_check.py 查 worker 已完成的点:
|
||||
- 收敛 → scp .7 回 master → (代传七牛) → 标 done
|
||||
- 未收敛 → 找种子(本地缓存>七牛)→ scp 种子到 worker → SSH 派 seed_step.py 重试
|
||||
- seed_step 仍失败 → 标 failed(物理极限)
|
||||
3. 全部终态 → 写 grid_status.json
|
||||
```
|
||||
|
||||
## 7. 文件清单
|
||||
|
||||
| 文件 | 角色 | 运行在哪 |
|
||||
|------|------|---------|
|
||||
| `src/dist_master.py` | master 调度器(推/拉/seed_step/汇总) | master |
|
||||
| `src/dist_check.py` | worker 状态查询(被 SSH 调用) | 每台 worker |
|
||||
| `src/qiniu_store.py` | 七牛云种子库(零 SDK) | master |
|
||||
| `src/claim_task.py` / `report_task.py` | 任务原子操作(master 本地调) | master |
|
||||
| `src/run_one.py` | 单点冷启动执行器(被 SSH 调用) | 每台 worker |
|
||||
| `src/seed_step.py` | 单点种子步进执行器(被 SSH 调用) | 每台 worker |
|
||||
| `dist_config.yaml` | 分布式配置 | master |
|
||||
| `tasks.json` | 任务队列(运行时生成) | master |
|
||||
|
||||
> `dist_worker.py` 已废弃(旧双向模型残留),当前架构不用。
|
||||
|
||||
## 8. 带宽与时间估算
|
||||
|
||||
| 项 | 大小 | 说明 |
|
||||
|----|------|------|
|
||||
| 单点计算 | 1200-2500s | 主耗时 |
|
||||
| SSH 派任务 | ~1KB/点 | 命令字符串,可忽略 |
|
||||
| 取 conv.json | 1.6KB/点 | 经 SSH,可忽略 |
|
||||
| 取种子 .7(收敛点) | 393KB/点 | scp 回 master,2Mbps≈1.6s |
|
||||
| seed_step 种子推送 | 393KB/点 | 仅失败点,少数 |
|
||||
| **跨网传输占比** | **<5%** | 相比计算可忽略 |
|
||||
|
||||
432 点 / 5台×24核 ≈ **3-4 小时**(vs 单机 15-21 小时)。
|
||||
|
||||
## 9. 故障排查
|
||||
|
||||
| 现象 | 排查 |
|
||||
|------|------|
|
||||
| master 派不出去 | `ssh user@workerN echo ok` 是否通;worker 上 `run_one.py` 路径对不对 |
|
||||
| worker 算了但 master 没回收 | 看 `worker_jobs/<name>.log`;`dist_check.py` 是否能列出 |
|
||||
| 任务一直 running | 可能 worker 进程崩了;master 每 2 小时自动重派(stale_sec) |
|
||||
| 种子找不到(seed_step 失败) | 同 family 无收敛点 → 物理极限,正常;或配七牛扩种子来源 |
|
||||
| 单机模式仍可用 | `python3 src/run_grid.py config.yaml`(完全不受影响) |
|
||||
|
||||
## 10. 安全提示
|
||||
|
||||
- **AK/SK 只放 master 环境变量**,不写进 dist_config.yaml(会被提交仓库)
|
||||
- worker 默认不碰七牛(`upload_by: master`);如需 worker 直传再配域名
|
||||
- SSH 用密钥免密,不用密码
|
||||
@ -26,7 +26,7 @@
|
||||
- **不设 ITEK**(用默认 4)
|
||||
- **He `.5` nlevs=14**(数据文件本身 24/20 能级,但 `.5` 声明 14 让 tlusty 截断;
|
||||
不是 Peter 建议的满 24-level)—— 详见 §2.5/§3.3
|
||||
- **NFREAD=2000**(展开成 5088 频率点,快且稳定)
|
||||
- **NFREAD=2000**(展开成 75443 频率点(tests/sdB_spectra/GUIDE.md 实测),快且稳定)
|
||||
- nst: `ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>` + `IELCOR=-1`
|
||||
|
||||
4. **丰度约定是收敛的关键(最终发现):**
|
||||
@ -73,8 +73,8 @@ NLTE 解 → nl 接手后不稳定。
|
||||
### 2.4 NFREAD 与频率网格
|
||||
|
||||
NFREAD 是 `.5` 里的"基本频率点数",tlusty 据此自动展开成实际频率网格:
|
||||
- **NFREAD=2000** → 5088 个频率点(快,每次迭代 ~3 秒)
|
||||
- **NFREAD=50** → 77695 个频率点(慢 15 倍,且 nc 不稳定)
|
||||
- **NFREAD=2000** → 75443 个频率点(实测,见 tests/sdB_spectra/GUIDE.md)
|
||||
- **NFREAD=50** → 77695 个频率点(NFREAD=50 反而更多,因 tlusty 对小 NFREAD 触发更细的自动细化)(慢 15 倍,且 nc 不稳定)
|
||||
|
||||
NFREAD 小反而展开更多——因为 tlusty 对小 NFREAD 触发更细的自动细化。
|
||||
**必须用 NFREAD=2000。**
|
||||
@ -151,7 +151,9 @@ ND=50,VTB=2.,NITER=0
|
||||
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>
|
||||
IELCOR=-1
|
||||
```
|
||||
- nc: NITER=50
|
||||
- nc: NITER=10(tests/sdB_spectra/GUIDE.md NITER 扫描实测最优;vs NITER=50
|
||||
光谱差异仅 6e-6 但快 2.2×。nc 纯连续谱缺少谱线约束,外层永不真正收敛,
|
||||
追求高 NITER 无意义,nl 会自修正)
|
||||
- nl: NITER=100
|
||||
- **不设 CHMAX**(用默认 0.001)、**不设 ITEK**(用默认 4)
|
||||
|
||||
@ -206,7 +208,7 @@ IELCOR=-1
|
||||
### 错误 4:NFREAD=50
|
||||
- **来源**:从 hhe35lt(纯H+He)抄来
|
||||
- **影响**:展开成 77695 频率点,慢 15 倍且不稳定
|
||||
- **纠正**:用 NFREAD=2000(5088 点)
|
||||
- **纠正**:用 NFREAD=2000(75443 点,实测)
|
||||
- **教训**:NFREAD 的展开行为反直觉(小→多),必须实测确认
|
||||
|
||||
### 错误 5:ORELAX=0.5(部分有效但非通用解)
|
||||
@ -231,17 +233,23 @@ IELCOR=-1
|
||||
- **教训**:先核实输入参数的物理含义和量级,再调试数值方法。对比用户成功配置时
|
||||
要逐行精确对比(用户用 abn=0 太阳丰度,我用 abn=1.0 绝对比值)。
|
||||
|
||||
### 错误 8:ICRSW 是死代码(本次会话发现)
|
||||
### 错误 8:ICRSW 是死代码(本次会话发现 —— 后已修复并测试)
|
||||
- **来源**:边界测试发现 80K + He-poor + logCNO=-1 即使种子步进也发散,
|
||||
尝试用 ICRSW(Hummer & Voels 1988 碰撞-辐射开关)稳定化
|
||||
- **影响**:源码 `tlusty208.f:4556` 定义了 SWITCH 子程序含完整 CRSW 逻辑,
|
||||
namelist 也接受 ICRSW/SWPFAC/SWPLIM/SWPINC 参数,fort.6 也打印这些值,
|
||||
看起来一切正常——但**整个源文件中没有任何一处 CALL SWITCH**。
|
||||
- **实测验证**:开 ICRSW=1/SWPFAC=0.001 后 nc 迭代历史与不开完全相同
|
||||
- **纠正**:放弃 ICRSW,改用实际有效的 ORELAX(`tlusty208.f:14647`)
|
||||
和种子步进(虽然 ORELAX 对极端跳跃也无效)
|
||||
- **后续(2026-07-21)**:在 `CALL RESOLV` 之后插入 `CALL SWITCH(INIT)`
|
||||
并重新编译,确认 SWITCH 现在确实被调用(ICRSW=0 时与原版逐字节相同,
|
||||
ICRSW=1 时 CRSW dump 出现在 fort.6 且 iter 1 尖峰被压低 4-5×)。
|
||||
**但对 80K + He-poor + 富金属难点仍无帮助**(甚至更早发散到 NaN)。
|
||||
默认管线仍用未修改的 `tlusty.exe`,详见 §6X
|
||||
- **教训**:源码里的子程序未必被调用。看似可用的参数可能是死代码。
|
||||
必须实测验证参数效果(对比开/关的迭代历史是否真的不同)。
|
||||
必须实测验证参数效果(对比开/关的迭代历史是否真的不同)。修复死代码
|
||||
时要注意 INSERT 位置(本例放在 RESOLV 之前会在 iter 1 除以未初始化的
|
||||
RRU,必须放在 RESOLV 之后)。即使修复"正确"也要再验证是否真能解决
|
||||
目标问题。
|
||||
|
||||
---
|
||||
|
||||
@ -253,6 +261,12 @@ IELCOR=-1
|
||||
| 35000/5.5/-2/logCNO=-1 | 0.00148(未达 0.001,作种子)| 0.000633 | 1635s |
|
||||
| 40000/5.5/0/logCNO=-1 | 0.000424 | 0.000905 | 1298s |
|
||||
|
||||
> **⚠️ 耗时说明**:上表和 §5X/§5Y 的所有耗时都是用 **nc NITER=50**(旧 config)
|
||||
> 跑出来的,**不能用作网格耗时估算**。修正后用 **NITER=10**(tests/sdB_spectra/GUIDE.md
|
||||
> 实测最优),35000K CNO 单点总耗时 ~12 分钟。网格耗时估算应以 NITER=10 为准。
|
||||
> nc max_relc 也是 NITER=50 时的中间值,NITER=10 时 nc 不会真正收敛(这是正常的,
|
||||
> 见 §3.2),但 nl 最终解与 NITER=50 完全等价(流量差异 <3e-12)。
|
||||
|
||||
> 注:早期版本曾列入 "35000/5.5/-2/abn=0 (nl=0.00078, 1009s)" 和 "40000/5.5/0/abn=0
|
||||
> (nc=6.67e-5)" 两个所谓"成功点"——经复核 conv.json,这两个数据**不存在**:
|
||||
> results/ 下既没有 abn=0 的对应模型目录,整库 grep 也没有 6.67e-5 这个值。
|
||||
@ -327,7 +341,9 @@ IELCOR=-1
|
||||
- <0 = 从 fort.95 读完整旧模型定义(见 3503)
|
||||
|
||||
`ICRSW`(`tlusty208.f:4556`)= Hummer & Voels 1988 碰撞-辐射开关,
|
||||
是另一可选稳定化参数(本次未启用,详见错误 8 与 §6X)。
|
||||
原版 tlusty 里是死代码(SUBROUTINE SWITCH 从未被 CALL);§6X 描述的
|
||||
源码修复让它在 patched 二进制里生效,但实测对极端难点无帮助,所以
|
||||
默认管线未启用。详见错误 8 与 §6X。
|
||||
|
||||
### 种子步进实现
|
||||
|
||||
@ -386,25 +402,35 @@ python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
|
||||
不透明度;当 logCNO 从 -2 跳到 -1(金属量 ×10),光致电离率变化陡峭到
|
||||
完全线性化无法阻尼 iter 1 的尖峰。
|
||||
|
||||
### 错误 8:ICRSW 是死代码(关键发现)
|
||||
### 错误 8:ICRSW 是死代码(关键发现 —— 后已修复并测试)
|
||||
|
||||
源码分析后发现 `ICRSW`(Hummer & Voels 1988 碰撞-辐射开关)在 tlusty208
|
||||
中**实际不可用**:
|
||||
**原版**中**实际不可用**:
|
||||
- `SWITCH` 子程序在 `tlusty208.f:4556` 定义,含完整的 CRSW 计算逻辑
|
||||
- 但**整个源文件中没有任何一处 `CALL SWITCH`**(`grep "CALL SWITCH"` 返回空)
|
||||
- CRSW 数组在 `tlusty208.f:1812` 被默认初始化为 `UN`(=1.0)
|
||||
- 因此 `tlusty208.f:6343-6344, 6591-6592` 等处的 `RRU/RRD * CRSW(ID)` 实际
|
||||
乘的是 1.0,没有任何阻尼效果
|
||||
- 同类的 CRSW 消费点还在 `tlusty208.f:18201, 18252`(FSOLV/FCOOL 内),
|
||||
共三处消费簇,全部因 CRSW≡1 而失效
|
||||
- 同类的 CRSW 消费点共 12 处(含 `tlusty208.f:18201, 18252` 等),
|
||||
全部因 CRSW≡1 而失效
|
||||
|
||||
测试验证:开启 ICRSW=1/SWPFAC=0.001/SWPINC=2.0 后,nc 阶段的迭代历史
|
||||
测试验证(原版):开启 ICRSW=1/SWPFAC=0.001/SWPINC=2.0 后,nc 阶段的迭代历史
|
||||
(iter 1-25 relc 演化)与不开启 ICRSW **完全相同**——确认 SWITCH 未被调用。
|
||||
|
||||
**后续(2026-07-21):已实施修复并重新测试**。在主循环 `CALL RESOLV` 之后
|
||||
插入 `CALL SWITCH(INIT)`(详见 §6X),重新编译后确认:
|
||||
- ICRSW=0 时与原版逐字节相同(sanity 通过)
|
||||
- ICRSW=1 时 SWITCH 确实被调用(CRSW dump 出现在 fort.6,iter 1 尖峰被
|
||||
压低 4-5×)
|
||||
- **但对 80K + He-poor + logCNO=-1 难点没有帮助**(甚至更早发散到 NaN),
|
||||
因此默认管线仍用未修改的 `tlusty.exe`。完整测试数据见 §6X。
|
||||
|
||||
**教训**:源码里的子程序未必被调用。看似可用的参数(ICRSW 在 nst namelist
|
||||
里、在 fort.6 里也被打印)可能是死代码。真正能用的稳定化参数是 `ORELAX`
|
||||
(`tlusty208.f:14647, 14996`)和 `IDLTE`(`tlusty208.f:5515`)——
|
||||
它们确实被使用。但实测对极端金属跳跃也无效。
|
||||
里、在 fort.6 里也被打印)可能是死代码。修复死代码前要:(1) 实测验证参数
|
||||
确实无效;(2) 仔细分析子程序依赖的变量何时被赋值(INSERT 位置很关键,
|
||||
本例中放在 `RESOLV` 之前会在 iter 1 除以未初始化的 RRU);(3) 修复后
|
||||
再实测验证是否真能改善目标问题——本例中修复"正确"但"无用"。真正对极端
|
||||
金属跳跃有效的稳定化手段仍然是种子步进(减小模型间步长)。
|
||||
|
||||
### 种子步进的网格应用策略
|
||||
|
||||
@ -427,7 +453,9 @@ python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
|
||||
- `gen_input5.py`:He `.5` nlevs=14(数据文件本身 24/20,按 nlevs 截断),
|
||||
NFREAD=2000,支持 ilvlin/metals 参数
|
||||
- `run_one.py` DEFAULT_CHAIN:三步法,无 CHMAX/ITEK/ORELAX
|
||||
- write_nst 支持 ichang/orelax/idlte/iacc/icrsw(注意 ICRSW 实际是死代码)
|
||||
- write_nst 支持 ichang/orelax/idlte/iacc/icrsw(注意:原版 tlusty.exe
|
||||
里 ICRSW 是死代码;§6X 描述的源码修复让它在 patched 二进制里生效,
|
||||
但默认管线仍用原版 tlusty.exe)
|
||||
- `seed_step.py`:种子步进实现 —— 跳过 LTE grey 冷启动,
|
||||
直接热启动 nc 阶段,用于高温/He-poor/富金属等冷启动失败的场景
|
||||
- `run_grid.py`:6 维网格调度,集成种子步进回退(NEW)
|
||||
@ -483,120 +511,180 @@ python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
|
||||
|
||||
---
|
||||
|
||||
## 6X. ICRSW 修复方案(备选 —— 当前未实施)
|
||||
## 6X. ICRSW 修复方案(已实施并测试 —— 2026-07-21)
|
||||
|
||||
> **状态:源码已修改并重新编译,但修复后的可执行未替换 `tlusty.exe`。**
|
||||
> 原因:修复在数值上**生效**(SWITCH 确实被调用了,CRSW 不再恒为 1.0),
|
||||
> 但对最难收敛的 80K + He-poor + 富金属点**没有帮助**(甚至更早发散),
|
||||
> 所以默认管线仍用未修改的 `tlusty.exe`。修复后的二进制保留在
|
||||
> `tlusty/tlusty.exe.icrsw_patched`,需要时可以拿来对比试验。
|
||||
> 备份的原始源码在 `tlusty/tlusty208.f.orig_backup`。
|
||||
|
||||
### 背景
|
||||
`ICRSW`(Hummer & Voels 1988 碰撞-辐射开关)在 tlusty208 中是**未完成的
|
||||
`ICRSW`(Hummer & Voels 1988 碰撞-辐射开关)在 tlusty208 中曾是**未完成的
|
||||
集成**——`SUBROUTINE SWITCH`(`tlusty208.f:4556`)写好了完整的 CRSW 计算
|
||||
逻辑,下游消费方代码(`tlusty208.f:6341-8021`、`18201`、`18252` 三处
|
||||
`RRU/RRD * CRSW(ID)`)也都到位,但**主迭代循环中缺少 `CALL SWITCH`**
|
||||
(`PROGRAM TLUSTY` 主循环在 line 30-58,`SUBROUTINE SOLVE` 在 line 14420,
|
||||
两者都没有调用 SWITCH)。
|
||||
逻辑,下游消费方代码(共 12 处 `RRU/RRD * CRSW(ID)`:`6341/6343-6345`、
|
||||
`6589/6591-6592`、`6841/6843-6845`、`7621-7633`、`7807-7810`、`8017-8020`、
|
||||
`18147`、`18201-18204`、`18252-18256`)也都到位,但**主迭代循环中缺少
|
||||
`CALL SWITCH`**。结果 `CRSW(ID)` 永远保持默认值 `UN`(=1.0,line 1812
|
||||
`CRSW(ID)=UN`),所有乘法都是无效操作。
|
||||
|
||||
结果:`CRSW(ID)` 永远保持默认值 `UN`(=1.0,line 1812 `CRSW(ID)=UN`),
|
||||
所有 `RRU/RRD * CRSW(ID)` 都是无效操作。
|
||||
### 实施的修复
|
||||
|
||||
### 修复需要的步骤
|
||||
**关键纠正**:旧版本本文档(§6X 步骤 3)建议把 `CALL SWITCH` 插在
|
||||
`CALL RESOLV` **之前**。这是**错误**的——经源码核查确认:
|
||||
- `RRU(ITR,ID)`/`RRD(ITR,ID)` 在 `RATES1`(line 6179)及其同族子程序
|
||||
(`RATSP1`、`ALIST1`、`ALIST2`、`ALISK1`、`ALISK2`)内部才被零初始化
|
||||
并累加;这些子程序全部从 `RESOLV`(line 3724)调用。
|
||||
- `COLRAT(ITR,ID)` 在 `INILAM`(line 4029)中赋值,`INILAM` 也从
|
||||
`RESOLV`(line 3743)调用。
|
||||
- **在 iter 1 的首次 `RESOLV` 之前,没有任何 DATA 语句或 START 阶段
|
||||
初始化过 RRU/RRD/COLRAT**(已 grep 验证)。如果按旧文档把 `CALL SWITCH`
|
||||
放在 `RESOLV` 之前,iter 1 会在 line 4599 `C/RRU(ITR,ID)` 处除以未初始化
|
||||
的垃圾值,立刻 NaN。
|
||||
|
||||
**步骤 1:检查编译环境**
|
||||
```bash
|
||||
which gfortran || apt list --installed 2>/dev/null | grep -i fortran
|
||||
# 若无,安装:sudo apt install gfortran
|
||||
```
|
||||
**正确插入位置:在 `CALL RESOLV` 之后、`INIT=0` 之前**,复用现有的
|
||||
`INIT` 变量作为 `INITM` 参数(程序启动时 INIT=1 在 line 22,RESOLV 之后
|
||||
被重置为 0 在 line 36):
|
||||
|
||||
**步骤 2:定位主迭代循环**
|
||||
|
||||
主循环在 `tlusty/tlusty208.f:30-58`(行 28 是注释头,行 30 是 `10 ITER=ITER+1`,
|
||||
行 58 是 `20 CONTINUE`):
|
||||
```fortran
|
||||
10 ITER=ITER+1
|
||||
CALL RESOLV ! 形式解
|
||||
IF(IACC.GT.0) CALL ACCEL2
|
||||
IF(NN.GT.MSMX) THEN
|
||||
CALL SOLVE ! 完全线性化
|
||||
ELSE
|
||||
CALL SOLVES
|
||||
END IF
|
||||
CALL TIMING(2,ITER)
|
||||
GO TO 10
|
||||
```
|
||||
|
||||
**步骤 3:在循环中插入 SWITCH 调用**
|
||||
|
||||
在 `ITER=ITER+1` 之后、`CALL RESOLV` 之前插入:
|
||||
```fortran
|
||||
10 ITER=ITER+1
|
||||
C ---- ICRSW 碰撞-辐射开关(修复)----
|
||||
C 首次迭代初始化 CRSW,后续迭代放大 CRSW 朝 1.0 趋近
|
||||
IF(ICRSW.GT.0) THEN
|
||||
IF(ITER.EQ.1) THEN
|
||||
CALL SWITCH(1) ! INITM=1: 初始化 CRSW=SWPFAC*min(C/R)
|
||||
ELSE
|
||||
CALL SWITCH(0) ! INITM=0: CRSW *= SWPINC
|
||||
END IF
|
||||
END IF
|
||||
CALL RESOLV
|
||||
C
|
||||
C 1a. Collisional-radiative switching (Hummer & Voels 1988)
|
||||
C EVALUATE/UPDATE CRSW(ID) AFTER the formal solution has produced
|
||||
C fresh RRU/RRD/COLRAT, and BEFORE the linearization step (SOLVE/
|
||||
C SOLVES) that consumes CRSW via BPOPE/BPOPF (lines ~18147,18201,
|
||||
C 18252). On iter 1 INIT is still 1 -> full recompute of CRSW;
|
||||
C on iter 2..N INIT was reset to 0 -> cheap CRSW*=SWPINC update.
|
||||
C SWITCH is a no-op when ICRSW=0 (default), so existing behavior
|
||||
C is unchanged unless ICRSW>0 is set in nst.
|
||||
C NOTE: must NOT be moved before CALL RESOLV -- on iter 1 RRU/RRD/
|
||||
C COLRAT are still uninitialized there (no DATA stmt; they are
|
||||
C zeroed and filled inside RATES1/RATSP1 within RESOLV).
|
||||
C
|
||||
CALL SWITCH(INIT)
|
||||
INIT=0
|
||||
IF(LFIN) GO TO 20
|
||||
...
|
||||
```
|
||||
|
||||
**步骤 4:验证变量在调用点已就绪**
|
||||
这个位置的优点:
|
||||
1. iter 1 时 RESOLV 已经计算好 COLRAT(来自 INILAM)和 RRU/RRD(来自
|
||||
RATES1),SWITCH(INIT=1) 能正确执行完整 Hummer-Voels 计算。
|
||||
2. iter 2..N 时 SWITCH(INIT=0) 仅做 `CRSW *= SWPINC` 的廉价更新。
|
||||
3. CRSW 在 `SOLVE`/`SOLVES`(通过 MATGEN→BPOP→BPOPE/BPOPF 消费 CRSW)
|
||||
运行之前已经定下来。
|
||||
4. SWITCH 内部首句 `IF(ICRSW.EQ.0) RETURN`(line 4580)保证 ICRSW=0 时
|
||||
是 no-op,**对现有所有测试零影响**。
|
||||
|
||||
`SWITCH` 子程序用到 `COLRAT(ITR,ID)`、`RRU(ITR,ID)`、`RRD(ITR,ID)`、
|
||||
`LINE(ITR)`、`FR0(ITR)`、`TEMP(ID)`、`HK`。需确认这些在 `ITER=ITER+1`
|
||||
之后已被前一迭代更新(或在首次迭代时已初始化)。若未就绪,可能需要
|
||||
把 SWITCH 调用移到 `CALL RESOLV` 之后。
|
||||
**重新编译命令**(关键:必须用 `-mcmodel=large`,否则 x86-64 PIC 重定位
|
||||
溢出,链接报 `relocation truncated to fit: R_X86_64_PC32 against symbol
|
||||
curder_`):
|
||||
|
||||
**步骤 5:重新编译**
|
||||
```bash
|
||||
cd tlusty/
|
||||
# 备份原可执行
|
||||
cp tlusty.exe tlusty.exe.orig
|
||||
# 编译(具体命令取决于源码组织,可能是单文件或 Makefile)
|
||||
gfortran -O2 -o tlusty.exe tlusty208.f \
|
||||
IMPLIC.FOR BASICS.FOR ARRAY1.FOR ATOMIC.FOR MODELQ.FOR \
|
||||
ITERAT.FOR ALIPAR.FOR ODFPAR.FOR
|
||||
cp tlusty.exe tlusty.exe.orig # 备份
|
||||
cp tlusty208.f tlusty208.f.orig_backup # 备份源码
|
||||
gfortran -O2 -std=legacy -fno-automatic -mcmodel=large \
|
||||
-o tlusty.exe tlusty208.f
|
||||
# 注意:IMPLIC.FOR / BASICS.FOR / 等都是 INCLUDE 文件,不要单独编译;
|
||||
# 整个程序就在 tlusty208.f 一个文件里(通过 INCLUDE 拉入其它 .FOR)。
|
||||
```
|
||||
|
||||
**步骤 6:测试**
|
||||
### 实施验证(2026-07-21)
|
||||
|
||||
跑已知难收敛的点(如 80K + He-poor + logCNO=-1)开/关 ICRSW,对比
|
||||
nc 阶段的迭代历史。如果 ICRSW 真正生效,开启后 iter 2 的尖峰应该被
|
||||
压低(CRSW < 1)。
|
||||
**验证 1:ICRSW=0 时与原版逐字节相同**
|
||||
|
||||
在 H+He NLTE(20000/5.0/-1)模型上,patched exe 与原 exe 产生的
|
||||
fort.7(大气)和 fort.9(收敛日志)**完全相同**(`cmp` 通过、md5 相同)。
|
||||
证明修复对 ICRSW=0 的所有现有运行零影响。
|
||||
|
||||
**验证 2:ICRSW=1 时 SWITCH 确实被调用**
|
||||
|
||||
同样的 H+He 模型,nst 加 `ICRSW=1,SWPFAC=0.001,SWPLIM=1.0,SWPINC=2.0`:
|
||||
- patched exe 在 fort.6 里多出 CRSW 数组 dump(`1P8D10.3` 格式,50 个值),
|
||||
数值序列 `1.438D-12 → 2.875D-12 → 5.750D-12 → 1.150D-11` 精确对应
|
||||
`SWPINC=2.0` 的逐次翻倍——证明 INITM=0 分支(line 4636)在每次迭代执行。
|
||||
- 原 exe 在相同 nst 下 fort.6 里**没有** CRSW dump,迭代历史与 ICRSW=0 完全
|
||||
相同——证明旧代码的 SWITCH 确实从未被调用("死代码"判断成立)。
|
||||
- iter 1 的 MAXIMUM 列在难收敛深度上明显被压低:
|
||||
|
||||
| 深度 | ICRSW=0(原版)| ICRSW=1(patched)| 压低倍数 |
|
||||
|------|---------------|-------------------|---------|
|
||||
| 35 | 1.03E+00 | 2.12E-01 | ~5× |
|
||||
| 28 | 4.12E+00 | 9.88E-01 | ~4× |
|
||||
| 25 | 3.72E+00 | 9.84E-01 | ~4× |
|
||||
|
||||
这是 Hummer-Voels 开关的预期行为——碰撞速率被人为放大(CRSW<1)以
|
||||
压制辐射跃迁的非线性。
|
||||
|
||||
### 难点测试:80K + He-poor + logCNO=-1(最终未解决)
|
||||
|
||||
用 `seed_step` 同款配置(cno-2 种子 → cno-1 目标,热启动),8 次迭代,
|
||||
测了三组 ICRSW 参数(强阻尼、弱阻尼)对比无 ICRSW 基线:
|
||||
|
||||
| iter | ICRSW=0(基线) | ICRSW=1 SWPFAC=1e-4 SWPINC=2.0 | ICRSW=1 SWPFAC=0.1 SWPINC=1.5 |
|
||||
|------|----------------|-------------------------------|-------------------------------|
|
||||
| 1 | 1.52e+03 | 2.26e+05 | 2.42e+04 |
|
||||
| 2 | 1.15e+01 | 1.34e+05 | 9.34e+04 |
|
||||
| 3 | 7.53e+01 | **NaN(发散)** | 5.98e+06 |
|
||||
| 4 | 1.52e+01 | NaN | 2.29e+08 |
|
||||
| 5 | 2.01e+03 | NaN | 5.66e+09 |
|
||||
| 6 | 1.97e+01 | NaN | 3.13e+21(发散) |
|
||||
| 7 | 8.24e+00 | NaN | NaN |
|
||||
| 8 | 6.08e+02 | NaN | NaN |
|
||||
| 大气 NaN | 0/5210(干净)| 2222/5210(43%) | 0/5210(干净,但解无效) |
|
||||
|
||||
(基线 8 次迭代都没崩到 NaN,只是没收敛;两次 ICRSW 都更早爆。)
|
||||
|
||||
**结论**:ICRSW 修复在数值层面**完全成功**(SWITCH 跑起来了,CRSW 不再
|
||||
恒为 1.0,迭代历史明显改变),但**不能解决这个极端难点**——无论是强阻尼
|
||||
(SWPFAC=1e-4)还是弱阻尼(SWPFAC=0.1),开启 ICRSW 都让发散**更早**。
|
||||
SWPFAC=1e-4 让 iter 1 的初始跳跃从 1.5e3 变成 2.3e5(CRSW 太小导致线性化
|
||||
过度校正);SWPFAC=0.1 略好但仍单调发散到 1e21。
|
||||
|
||||
物理原因(与前文 §5Y 的"物理极限"结论一致):80K + He-poor 时 CNO
|
||||
高价离子(C IV/V、N V、O V/VI)主导大气不透明度,金属量从 logCNO=-2
|
||||
跳到 -1(×10)导致光致电离率变化陡峭到完全线性化无法阻尼 iter 1 的
|
||||
尖峰。Hummer-Voels 开关通过放大碰撞速率来稳定,但当辐射-碰撞比本身
|
||||
就在极端区间时,开关反而把不稳定提前。
|
||||
|
||||
### 40K sanity check(patched exe 在正常区间仍工作)
|
||||
|
||||
为了排除"修复破坏了正常路径"的可能,在已验证的 40K + logg 5.5 + cno-1
|
||||
模型上跑 patched exe(ICRSW=0):迭代历史(oscillatory 但最终收敛到
|
||||
~1e-4)与原版 exe 产生的 `t40000_g5.5_he0_c-1_n-1_o-1.nc_*.9` 文件
|
||||
**特征一致**(同样的 iter 6 尖峰到 4.69e8、同样的 iter 23-27 收敛到
|
||||
~1e-4)。证明修复对正常收敛区间无害。
|
||||
|
||||
### 实施后的工程决策
|
||||
|
||||
**保留源码修改,但不替换 `tlusty.exe`**:
|
||||
1. 修改已通过 sanity check(ICRSW=0 时与原版逐字节相同),是安全的。
|
||||
2. 对网格里 95%+ 的点(20000-40000K 区间),ICRSW 没用也没害。
|
||||
3. 对剩余难收敛点(80K + He-poor + 富金属),ICRSW 不仅没用反而更糟。
|
||||
4. 因此**没有理由**让默认管线用 patched exe——保留原版可执行,patched
|
||||
二进制仅供后续研究(比如有人想试 `ICRSW=2` 的深度相关模式,或者
|
||||
配合更小的丰度步长)。
|
||||
|
||||
**如果未来需要重新启用 patched exe**:
|
||||
```bash
|
||||
# 关 ICRSW(基线)
|
||||
python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
|
||||
--logc -1 --logn -1 --logo -1 --seed <seed> \
|
||||
2>&1 | tee /tmp/no_icrsw.log
|
||||
# 开 ICRSW(修复后)
|
||||
# 在 nst 里加 ICRSW=1,SWPFAC=1e-3,SWPLIM=1.0,SWPINC=2.0
|
||||
# ... 跑同样模型
|
||||
# 对比两次的 fort.9 iter 1-10 max_relc 演化
|
||||
cd tlusty/
|
||||
# 当前 tlusty.exe 是原版;tlusty208.f 是已修改版
|
||||
gfortran -O2 -std=legacy -fno-automatic -mcmodel=large \
|
||||
-o tlusty.exe tlusty208.f
|
||||
# 想恢复原版:cp tlusty208.f.orig_backup tlusty208.f 后重新编译
|
||||
```
|
||||
|
||||
### 修复风险评估
|
||||
### 替代方案:网格层面规避(仍是当前推荐)
|
||||
|
||||
**好处**:
|
||||
- 可能解锁 80K + He-poor + 富金属区的收敛(最后一个未解决角落)
|
||||
- 是数值方法层面的修复,物理意义清晰
|
||||
|
||||
**风险**:
|
||||
- 重新编译可能引入其他问题(tlusty 源码依赖复杂)
|
||||
- SWITCH 集成后可能有未预见的 bug(作者本就没集成,可能有原因)
|
||||
- 编译器版本差异(原版可能用 f77/f90,gfortran 行为可能不同)
|
||||
|
||||
### 替代方案:网格层面规避
|
||||
|
||||
如果不想改源码,**现有种子步进方案已覆盖大部分情况**:
|
||||
- 20000-40000K:冷启动全区间可靠
|
||||
不改源码也能完成网格:
|
||||
- 20000-40000K:冷启动全区间可靠(已验证多个点)
|
||||
- 60000-80000K + He-rich / 低金属:冷启动或一步种子步进可解决
|
||||
- 60000-80000K + He-poor + 富金属(logCNO=-1):**真实物理极限**,
|
||||
网格如实标记未收敛(这是合理的——观测上这些极端参数组合的 sdB
|
||||
本就罕见)
|
||||
|
||||
**建议**:先用现有方案跑完整网格,统计未收敛点的比例和分布。
|
||||
如果未收敛点占总网格 <5%,无需修复 ICRSW;如果占比高且集中在
|
||||
可观测的重要参数区,再考虑修复。
|
||||
本就罕见,且本次实测确认 ICRSW 也不能解决)
|
||||
|
||||
### 每阶段信息记录
|
||||
conv.json 记录每阶段的 converged/max_relc/elapsed_sec,以及 synspec_sec。
|
||||
|
||||
@ -12,19 +12,23 @@ config.yaml (网格点 + 收敛链配置)
|
||||
│
|
||||
▼
|
||||
run_grid.py ── 生成 6 维笛卡尔积参数点
|
||||
│ 断点续算(跳过已成功) / 种子复用(最近邻) / 失败隔离
|
||||
│ 断点续算(跳过已成功) / 种子复用(最近邻) /
|
||||
│ 失败隔离 / 冷启动失败→种子步进回退
|
||||
│
|
||||
├── worker 1 ── run_one.py ── 点 A
|
||||
├── worker 2 ── run_one.py ── 点 B 每个 worker 独立工作目录
|
||||
├── ... 互不干扰,24 核并行
|
||||
└── worker 24 ── run_one.py ── 点 X
|
||||
├── ... 互不干扰,并行(默认16核)
|
||||
└── worker N ── run_one.py ── 点 X
|
||||
│
|
||||
▼
|
||||
三步链(lte→nc→nl) + synspec
|
||||
冷启动链(lte→nc→nl) + synspec
|
||||
│ 冷启动发散且有干净邻居种子?
|
||||
▼ → 移失败结果到 <model>.coldfail/,
|
||||
种子步进链(seed_nc→nl) + synspec 改用 LTGRAY=F 热启动重试
|
||||
│
|
||||
▼
|
||||
results/<模型名>/
|
||||
conv.json ← 阶段信息(收敛/迭代/时间)
|
||||
conv.json ← 阶段信息(收敛/迭代/时间/seed_step_used)
|
||||
*.spec/.cont ← 光谱
|
||||
*.7 ← 各阶段大气
|
||||
```
|
||||
@ -33,17 +37,22 @@ run_grid.py ── 生成 6 维笛卡尔积参数点
|
||||
|
||||
## 2. 每个网格点的计算阶段
|
||||
|
||||
每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 **4 个阶段**:
|
||||
每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 **4 个阶段**。
|
||||
|
||||
| 阶段 | 程序 | 做什么 | 典型耗时 |
|
||||
|------|------|--------|---------|
|
||||
| 1. LTE 灰大气 | tlusty | `T T` 模式,解析求灰色 T(τ) 结构 | 1-3 秒 |
|
||||
| 2. nc(NLTE 连续谱)| tlusty | `F F` + `ilvlin=0`,收敛电离平衡(无线跃迁)| 1-10 分钟 |
|
||||
| 3. nl(NLTE 含线)| tlusty | `F F` + `ilvlin=100`,加全部谱线跃迁 | 5-20 分钟 |
|
||||
| 4. synspec | synspec | 用 nl 大气合成可观测光谱 | 3-10 秒 |
|
||||
**默认走"冷启动链"**(DEFAULT_CHAIN,适用 20-40K 及大部分易收敛点):
|
||||
|
||||
| 阶段 | 程序 | 做什么 | NITER | 典型耗时 |
|
||||
|------|------|--------|-------|---------|
|
||||
| 1. LTE 灰大气 | tlusty | `T T` 模式,解析求灰色 T(τ) 结构 | 0 | 1-3 秒 |
|
||||
| 2. nc(NLTE 连续谱)| tlusty | `F F` + `ilvlin=0`,收敛电离平衡(无线跃迁)| **10** | 1-5 分钟 |
|
||||
| 3. nl(NLTE 含线)| tlusty | `F F` + `ilvlin=100`,加全部谱线跃迁(要求收敛)| 100 | 5-20 分钟 |
|
||||
| 4. synspec | synspec | 用 nl 大气合成可观测光谱 | — | 3-10 秒 |
|
||||
|
||||
**阶段间依赖**:1→2→3→4 严格顺序。每阶段用上一阶段的 `.7` 大气作种子(fort.8)。
|
||||
|
||||
> 冷启动链在高温/He-poor/富金属区会发散,此时 run_grid 自动改走下文 §2.1
|
||||
> 的**种子步进链**(seed_nc→nl,跳过 LTE grey 直接热启动)。
|
||||
|
||||
### 为什么是这 4 个阶段(原理)
|
||||
|
||||
Tlusty 的 NLTE 求解用**迭代线性化**(complete linearization)。线性化的收敛半径
|
||||
@ -58,6 +67,40 @@ Tlusty 的 NLTE 求解用**迭代线性化**(complete linearization)。线
|
||||
线扰动小,快速收敛(典型 ~15 次迭代)。
|
||||
- **阶段4(synspec)**:用 nl 阶段收敛的大气模型,计算指定波长范围的合成光谱。
|
||||
|
||||
### 2.1 种子步进链(seed_step)—— 高温区破局(NEW)
|
||||
|
||||
当冷启动链发散时(典型:60000-80000K + He-poor + 富金属),run_grid 自动
|
||||
切换到**种子步进链**(`seed_step.SEED_STEP_CHAIN`),跳过 LTE grey 冷启动,
|
||||
直接用邻居已收敛的 `.7` 热启动:
|
||||
|
||||
| 阶段 | 做什么 | 关键标志 | NITER |
|
||||
|------|--------|---------|-------|
|
||||
| seed_nc | 从种子热启动 NLTE 连续谱 | `LTGRAY=F`(读 fort.8) + `ICHANG=0` | 80 |
|
||||
| nl | 含谱线完整 NLTE(要求收敛)| `ilvlin=100` | 100 |
|
||||
|
||||
**触发流程**(`run_grid.py` 的 `_worker`):
|
||||
1. 先跑冷启动链(DEFAULT_CHAIN)。
|
||||
2. 若 `converged=false` 且 `seed_step_fallback=true` 且找到了干净邻居种子:
|
||||
- 把失败结果整体移到 `results/<model>.coldfail/`(避免污染种子库);
|
||||
- 用 `SEED_STEP_CHAIN`(`seed=<邻居>.7`)重算;
|
||||
- conv.json 里记 `seed_step_used=true`、`coldfail_backup=<路径>`。
|
||||
|
||||
**原理**(`tlusty208.f` 源码确认,详见 EXPERIENCE.md §5Y):
|
||||
- `LTGREY=T` → `CALL LTEGR` 生成灰大气(**忽略 fort.8**,冷启动);
|
||||
- `LTGREY=F` → `CALL INPMOD` 读 fort.8 作初猜(**热启动**)。
|
||||
- `ICHANG=0`:种子与目标模型原子完全一致(同为 H/He/CNO 设置),只改
|
||||
Teff/logg/丰度,不需要重映射布居数。
|
||||
|
||||
**实测突破**(80K + He-poor + logCNO=-4,冷启动必败点):种子步进 126s 收敛
|
||||
(冷启动 970s 发散到 NaN)。详见 EXPERIENCE.md §5Y 验证表。
|
||||
|
||||
> **物理极限(如实标注)**:80000K + He-poor + logCNO=-1 即使种子步进也发散
|
||||
> (CNO 高价离子主导不透明度,金属 ×10 跳跃线性化无法阻尼)。网格如实标
|
||||
> `converged=false`,不强制成功——这些极端参数组合观测上本就罕见。
|
||||
>
|
||||
> **ICRSW 是死代码**:源码中 `SUBROUTINE SWITCH` 定义但从未被 CALL,
|
||||
> 默认 CRSW≡1.0 无阻尼效果。不要依赖 ICRSW 稳定化(见 §7.8)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 每阶段的配置信息与原理
|
||||
@ -66,13 +109,13 @@ Tlusty 的 NLTE 求解用**迭代线性化**(complete linearization)。线
|
||||
|
||||
```
|
||||
第1行: TEFF GRAV (三阶段相同:目标参数)
|
||||
第2行: LTE LTGRAY (阶段1=T T,阶段2/3=F F)
|
||||
第3行: nst 文件名 (阶段1=nst_lte, 阶段2=nst_nc, 阶段3=nst_nl)
|
||||
第4行: NFREAD (=50, 频率点数)
|
||||
第2行: LTE LTGRAY (阶段1=T T,阶段2/3=F F;种子步进链全 F)
|
||||
第3行: nst 文件名 (固定写 'nst',内容每阶段由 write_nst 生成)
|
||||
第4行: NFREAD (=2000 → 展开约 75443 个频率点;不要用 50)
|
||||
第5行: NATOMS (=8: H,He,空×3,C,N,O)
|
||||
第6+行: atoms (mode abn modpf) (C/N/O 的 mode=2 显式NLTE, abn=10^logX)
|
||||
ions段: iat iz nlevs ilast ilvlin nonstd typion filei
|
||||
(阶段1/2: ilvlin=0; 阶段3: ilvlin=100 ← 关键区别)
|
||||
(阶段1/2/seed_nc: ilvlin=0; 阶段3/nl: ilvlin=100 ← 关键区别)
|
||||
```
|
||||
|
||||
**为什么 NATOMS/ions 三阶段必须相同**:每阶段的 `.7` 大气记录了每个能级的
|
||||
@ -91,7 +134,7 @@ ND=50,VTB=2.,NITER=0
|
||||
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>
|
||||
IELCOR=-1
|
||||
```
|
||||
- nc: NITER=50, nl: NITER=100
|
||||
- nc: NITER=10, nl: NITER=100
|
||||
- **不设 CHMAX**(用默认 0.001,强迫 nc 真正收敛)
|
||||
- **不设 ITEK**(用默认 4)
|
||||
|
||||
@ -105,12 +148,18 @@ IELCOR=-1
|
||||
| `ISPODF` | 1 | 1 | 频率网格开关 | 启用细化频率网格 |
|
||||
| `DDNU` | 50. | 50. | 频率间隔因子 | 频率网格细化参数 |
|
||||
| `CNU1` | 6. | 6. | 频率网格起点 | 频率网格细化参数 |
|
||||
| `NITER` | 50 | 100 | 最大迭代数 | nc 给 50 次;nl 给 100 次 |
|
||||
| `NITER` | **10** | 100 | 最大迭代数 | nc 给 10 次足够(实测最优);nl 给 100 次 |
|
||||
| `IELCOR` | -1 | -1 | 电子密度修正 | 关闭 |
|
||||
|
||||
> **关键:不设 CHMAX(用默认 0.001)、不设 ITEK(用默认 4)、不设 IDLTE/ORELAX。**
|
||||
> 之前版本设了 CHMAX=0.1 导致 nc 没真正收敛,是大部分失败的根本原因。
|
||||
> 详见 EXPERIENCE.md §4 的错误记录。
|
||||
>
|
||||
> **nc 的 NITER=10 是实测最优**(GUIDE.md NITER 扫描结论):
|
||||
> - nc(纯连续谱)缺少谱线约束,外层温度永不真正收敛,只在外层漂移;
|
||||
> - NITER=10 vs NITER=50 的最终光谱差异仅 6e-6(完全等价);
|
||||
> - nl(含谱线)会自修正到正确解,无论 nc 给什么初值;
|
||||
> - NITER=10 总耗时 ~12 分钟(35000K CNO),NITER=50 浪费 2.2× 时间。
|
||||
|
||||
### 3.3 synspec 配置(fort.55.lin + 谱线表)
|
||||
|
||||
@ -132,7 +181,7 @@ fort.55.lin 第6行: WLMIN WLMAX WLSTEP ... CUTOFF ...
|
||||
|
||||
### 4.2 如何做到并行
|
||||
|
||||
`run_grid.py` 用 Python 的 `multiprocessing.Pool`(`run_grid.py:271`):
|
||||
`run_grid.py` 用 Python 的 `multiprocessing.Pool`(`run_grid.py` 的 `_worker`):
|
||||
|
||||
```python
|
||||
with Pool(nworkers) as pool:
|
||||
@ -140,24 +189,27 @@ with Pool(nworkers) as pool:
|
||||
...
|
||||
```
|
||||
|
||||
- `nworkers`(config.yaml,当前=24):同时运行的 worker 进程数。
|
||||
- `nworkers`(config.yaml,当前=**16**):同时运行的 worker 进程数。
|
||||
- 每个 worker 是一个独立的 Python 子进程,调用 `run_one.py` 跑一个网格点
|
||||
(在独立的工作目录里,互不干扰)。
|
||||
- `imap_unordered`:哪个点先完成就先回收,立即分配下一个点(动态负载均衡)。
|
||||
- 24 核机器跑 24 个 worker = 24 个 tlusty 实例同时跑 = 满载利用。
|
||||
- 16 核机器跑 16 个 worker = 16 个 tlusty 实例同时跑 = 满载利用。
|
||||
(按机器核数调整;每个 tlusty 运行是单线程的,调大 nworkers 即可吃更多核。)
|
||||
|
||||
**关键:每个 worker 用独立工作目录**(`results/<模型名>/`),避免 fort.* 文件
|
||||
冲突。这是并行安全的基础。
|
||||
|
||||
### 4.3 吞吐量估算
|
||||
|
||||
| 模型类型 | 单点耗时 | 24核并行吞吐 |
|
||||
|---------|---------|-------------|
|
||||
| 80000K(待解决)| — | 目前 nc 发散,需专业策略 |
|
||||
| 20000-40000K(标准)| ~15-25 分钟 | ~72-110 点/小时 |
|
||||
| 高金属 CNO=10×H | ~30 分钟 | ~48 点/小时 |
|
||||
| 模型类型 | 单点耗时 | 16核并行吞吐 | 收敛性 |
|
||||
|---------|---------|-------------|--------|
|
||||
| 20000-40000K(标准 sdB 区)| ~12-25 分钟 | ~48-80 点/小时 | 冷启动全区间可靠 |
|
||||
| 60000K + He-rich/低金属 | ~10-15 分钟 | ~64-96 点/小时 | 冷启动或一步种子步进 |
|
||||
| 80000K + He-rich/低金属 | ~3-5 分钟(种子步进)| 约同上 | 冷启动失败→种子步进成功 |
|
||||
| 80000K + He-poor + logCNO=-1 | — | — | **真实物理极限,发散**(如实标注) |
|
||||
|
||||
128 点疏网格约需 2-3 小时;8192 点完整网格约需 5 天。
|
||||
当前 config.yaml 共 432 点(4×2×2×3×3*3);中等参数区冷启动为主,
|
||||
高温区走种子步进回退,整体约需数小时到一天。
|
||||
|
||||
---
|
||||
|
||||
@ -169,39 +221,45 @@ with Pool(nworkers) as pool:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "t40000_g6.0_he0_c1_n1_o1",
|
||||
"params": {"teff":40000, "logg":6.0, "loghe":0, "logc":1, "logn":1, "logo":1},
|
||||
"name": "t40000_g6.0_he0_c-1_n-1_o-1",
|
||||
"params": {"teff":40000, "logg":6.0, "loghe":0, "logc":-1, "logn":-1, "logo":-1},
|
||||
"converged": true,
|
||||
"final_max_relc": 0.0069,
|
||||
"atmosphere_has_nan": false,
|
||||
"synspec_rc": 0,
|
||||
"elapsed_sec": 1714.1, ← 总耗时(所有阶段+synspec之和)
|
||||
"seed": null,
|
||||
"elapsed_sec": 715.0, ← 总耗时(所有阶段+synspec之和)
|
||||
"seed": null, ← 冷启动为 null;走种子步进时为邻居 .7 路径
|
||||
"seed_step_used": false, ← true=种子步进链跑成功的(含 coldfail_backup 路径)
|
||||
"stages": [
|
||||
{
|
||||
"label": "lte",
|
||||
"converged": true,
|
||||
"final": {"itek":3, "rc":0, "max_relc":0.0,
|
||||
"note":"NITER=0 grey start"}
|
||||
"final": {"itek":null, "rc":0, "max_relc":0.0,
|
||||
"note":"NITER=0 grey start (no iterations)"}
|
||||
},
|
||||
{
|
||||
"label": "nc",
|
||||
"converged": false, ← nc 不要求收敛,作种子即可
|
||||
"final": {"itek":3, "rc":0, "max_relc":0.957,
|
||||
"worst_depth":1, "last_iter":50, "n_depths":50}
|
||||
"converged": false, ← nc 不要求收敛,作种子即可(NITER=10)
|
||||
"final": {"itek":null, "rc":0, "max_relc":0.957,
|
||||
"worst_depth":1, "last_iter":10, "n_depths":50}
|
||||
},
|
||||
{
|
||||
"label": "nl",
|
||||
"converged": true,
|
||||
"final": {"itek":3, "rc":0, "max_relc":0.0069,
|
||||
"final": {"itek":null, "rc":0, "max_relc":0.0069,
|
||||
"worst_depth":1, "last_iter":17, "n_depths":50}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **走种子步进链时**:`seed` 指向邻居 `.7`,`seed_step_used=true`,
|
||||
> `coldfail_backup` 指向 `<model>.coldfail/`,`stages` 里没有 `lte`,而是
|
||||
> `seed_nc`(LTGRAY=F 热启动,NITER=80)→ `nl`。
|
||||
|
||||
每阶段记录:`converged`(是否收敛)、`max_relc`(最大相对变化)、
|
||||
`worst_depth`(最差深度点)、`last_iter`(迭代次数)、`n_depths`(深度点数)。
|
||||
`worst_depth`(最差深度点)、`last_iter`(迭代次数)、`n_depths`(深度点数)、
|
||||
`elapsed_sec`(本阶段耗时)。
|
||||
|
||||
### 5.2 每阶段时间记录(已实现)
|
||||
|
||||
@ -210,12 +268,12 @@ with Pool(nworkers) as pool:
|
||||
|
||||
```json
|
||||
"stages": [
|
||||
{"label":"lte", "elapsed_sec": 27.3, "converged":true, ...},
|
||||
{"label":"nc", "elapsed_sec": 408.4, "converged":false, ...},
|
||||
{"label":"lte", "elapsed_sec": 2.1, "converged":true, ...},
|
||||
{"label":"nc", "elapsed_sec": 62.4, "converged":false, ...},
|
||||
{"label":"nl", "elapsed_sec": 7.8, "converged":true, ...}
|
||||
],
|
||||
"synspec_sec": 3.1,
|
||||
"elapsed_sec": 446.2
|
||||
"elapsed_sec": 75.4
|
||||
```
|
||||
|
||||
统计所有模型的阶段时间分布:
|
||||
@ -237,11 +295,15 @@ for f in sorted(glob.glob('results/*/conv.json')):
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 128,
|
||||
"elapsed_sec": 9600,
|
||||
"counts": {"converged": 120, "unfinished": 5, "error": 2, "skipped": 1},
|
||||
"total": 432,
|
||||
"elapsed_sec": 36000,
|
||||
"counts": {"converged": 400, "unfinished": 18, "error": 3, "skipped": 11},
|
||||
"seed_step_retries": 47,
|
||||
"models": [
|
||||
{"name":"t20000_...", "status":"converged", "max_relc":0.0065},
|
||||
{"name":"t20000_...", "status":"converged", "max_relc":0.0065,
|
||||
"seed_step_used": false},
|
||||
{"name":"t80000_...", "status":"converged", "max_relc":0.00091,
|
||||
"seed_step_used": true},
|
||||
...
|
||||
]
|
||||
}
|
||||
@ -249,15 +311,16 @@ for f in sorted(glob.glob('results/*/conv.json')):
|
||||
|
||||
汇总统计命令:
|
||||
```bash
|
||||
# 成功率
|
||||
python3 -c "import json; j=json.load(open('results/grid_status.json')); print(j['counts'])"
|
||||
# 所有收敛模型的 max_relc 分布
|
||||
# 成功率 + 种子步进命中数
|
||||
python3 -c "import json; j=json.load(open('results/grid_status.json')); print(j['counts'], 'seed_step_retries=', j['seed_step_retries'])"
|
||||
# 所有收敛模型的 max_relc 分布(标注是否走了种子步进)
|
||||
python3 -c "
|
||||
import json,glob
|
||||
for f in sorted(glob.glob('results/*/conv.json')):
|
||||
j=json.load(open(f))
|
||||
if j['converged']:
|
||||
print(j['name'], j['final_max_relc'], str(j['elapsed_sec'])+'s')
|
||||
tag='SEED' if j.get('seed_step_used') else 'cold'
|
||||
print(j['name'], tag, j['final_max_relc'], str(j['elapsed_sec'])+'s')
|
||||
"
|
||||
# 失败/未收敛的模型
|
||||
python3 -c "
|
||||
@ -289,9 +352,10 @@ grid:
|
||||
teff: [20000, 30000, 40000, 60000] # 各维采样点列表
|
||||
logg: [5.0, 6.0]
|
||||
loghe: [-2, 0]
|
||||
logc: [-1, 1]
|
||||
logn: [-1, 1]
|
||||
logo: [-1, 1]
|
||||
logc: [-4, -2, -1] # 亚太阳范围(已修正;旧版 -1..1 会发散)
|
||||
logn: [-4, -2, -1]
|
||||
logo: [-4, -2, -1]
|
||||
# 共 4*2*2*3*3*3 = 432 个点
|
||||
```
|
||||
|
||||
### 第2步:设置环境变量
|
||||
@ -303,17 +367,20 @@ export TLUSTY=/home/dckj/program/tlusty/tl208-s54
|
||||
```bash
|
||||
cd $TLUSTY/cno_grid
|
||||
python3 src/run_grid.py config.yaml --dry-run
|
||||
# 输出:grid: 64 points total, N already done, M to compute
|
||||
# 输出:grid: 432 points total, N already done, M to compute
|
||||
```
|
||||
|
||||
### 第4步:启动批量计算(后台并行)
|
||||
```bash
|
||||
nohup python3 src/run_grid.py config.yaml > results/grid_run.log 2>&1 &
|
||||
# 冷启动失败的点会自动尝试种子步进回退(seed_step_fallback: true)。
|
||||
# 想关闭回退:在 config.yaml 设 seed_step_fallback: false。
|
||||
```
|
||||
|
||||
### 第5步:监控
|
||||
```bash
|
||||
tail -f results/grid_run.log # 实时进度
|
||||
grep seed_step results/grid_run.log # 看哪些点走了种子步进
|
||||
cat results/grid_status.json # 汇总(完成后才有)
|
||||
```
|
||||
|
||||
@ -324,32 +391,52 @@ python3 src/run_grid.py config.yaml # 重跑同一命令即可
|
||||
|
||||
### 第7步:检查结果 + 画图
|
||||
```bash
|
||||
# 成功率
|
||||
python3 -c "import json;print(json.load(open('results/grid_status.json'))['counts'])"
|
||||
# 成功率 + 种子步进命中数
|
||||
python3 -c "import json;j=json.load(open('results/grid_status.json'));print(j['counts'],'seed_step=',j['seed_step_retries'])"
|
||||
# 画某个模型光谱
|
||||
python3 src/plot_spec.py results/<模型名>
|
||||
```
|
||||
|
||||
### 单点调试(不走批量)
|
||||
```bash
|
||||
# 冷启动单点(适用 20-40K 大部分点)
|
||||
python3 src/run_one.py --teff 40000 --logg 6.0 --loghe 0 --logc -1 --logn -1 --logo -1
|
||||
# 种子步进单点(高温 He-poor 等冷启动失败点)
|
||||
python3 src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
|
||||
--logc -4 --logn -4 --logo -4 \
|
||||
--seed results/<seed-model>/<seed-model>.7
|
||||
```
|
||||
|
||||
### 第8步:加密网格(Phase 2)
|
||||
在 config.yaml 的各维列表里加更多点,重跑 `run_grid.py`(自动跳过已完成的,
|
||||
只算新点)。
|
||||
只算新点)。高温区加密时建议先冷启动算 He-rich/低金属的"桥头堡"模型,
|
||||
建立种子库再扩散到难收敛点(见 §7.1)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. **种子复用**:run_grid.py 会自动找最近邻已收敛的 `.7` 作种子,省去 LTE 阶段。
|
||||
但种子与目标的参数差不能太大(logg ≤0.5/步,Teff ≤5000K/步),否则发散。
|
||||
建议网格各维步长不要太大。
|
||||
1. **种子步进回退(核心机制)**:`seed_step_fallback: true`(默认开)时,
|
||||
冷启动失败的点会自动改走种子步进链(`seed_step.SEED_STEP_CHAIN`):
|
||||
把失败结果备份到 `<model>.coldfail/`,用 `find_seed` 找最近邻已收敛的 `.7`
|
||||
作种子,`LTGRAY=F + ICHANG=0` 热启动重跑。这是高温/He-poor/富金属区的
|
||||
决定性破局手段(详见 §2.1)。
|
||||
**种子跨度限制**:种子与目标的参数差不能太大(logg ≤0.5/步,
|
||||
Teff ≤5000K/步;logCNO 一步 ≤100×)。跨度过大(如 cno-4 直接跳 cno-1,
|
||||
1000× 金属跳跃)即便种子步进也发散 → 这是真实物理极限,网格如实标注。
|
||||
**种子库建立策略**:高温区建议先冷启动算 He-rich 或低金属的"桥头堡"
|
||||
模型,建立种子库再扩散到难收敛点(如 cno-4 → cno-2 → cno-1 多步跳板)。
|
||||
|
||||
2. **断点续算判定**:`conv.json` 里 `converged=true` 的点会被跳过。假收敛
|
||||
(atmosphere_has_nan=true)的点会被重算。
|
||||
|
||||
3. **失败隔离**:单点失败(发散/崩溃)不中断整个网格,记入 grid_status.json
|
||||
的 error 列表。可在 config.yaml 放宽 CHMAX 或加 orelax 重试失败点。
|
||||
的 error/unfinished 列表。高温难点的失败大多是真实物理极限(见 §2.1),
|
||||
不强制成功;如确需重试,可用 `seed_step.py` 手动从更近的种子起跳。
|
||||
|
||||
4. **磁盘空间**:每个模型约 50-100MB(含中间文件)。8192 点约需 400-800GB。
|
||||
当前 932GB 可用,够完整网格。如不够可定期清理中间文件(保留 .spec/.cont/.7/conv.json)。
|
||||
4. **磁盘空间**:每个模型约 50-100MB(含中间文件,走种子步进的点还会留
|
||||
`<model>.coldfail/` 备份)。432 点约需 20-40GB。如不够可定期清理中间文件
|
||||
(保留 .spec/.cont/.7/conv.json)。
|
||||
|
||||
5. **并行安全**:每个 worker 用独立工作目录(results/<模型名>/),fort.* 文件
|
||||
不冲突。可安全并行。
|
||||
@ -364,7 +451,11 @@ python3 src/plot_spec.py results/<模型名>
|
||||
内部表示)。如果下一次 tlusty 运行(不同 NATOMS)读到旧的 fort.84,会报
|
||||
"Bad integer for item 48" 崩溃。`run_tlusty()` 现在每次运行前删除 fort.84。
|
||||
|
||||
8. **收敛可靠性(如实)**:用正确配方(无 CHMAX/ITEK, 14-level He, NFREAD=2000)
|
||||
重新验证后,35000K 成功(nl=0.00078),但 40000K+ 的 nc 仍然震荡发散。
|
||||
之前版本的"8/8 边界全部成功"不准确(基于错误的 CHMAX=0.1)。
|
||||
40000K+ 高温区需要种子步进或 ICRSW 等专业策略。详见 EXPERIENCE.md。
|
||||
8. **收敛可靠性(如实)**:用正确配方(无 CHMAX/ITEK, NFREAD=2000, nc NITER=10)
|
||||
后,20000-40000K 全区间冷启动可靠收敛;60000-80000K + He-rich/低金属用
|
||||
种子步进可解决;**80000K + He-poor + logCNO=-1 是真实物理极限**
|
||||
(CNO 高价离子主导不透明度,金属 ×10 跳跃线性化无法阻尼),网格如实标记
|
||||
未收敛。之前版本的"8/8 边界全部成功"不准确(基于错误的 CHMAX=0.1)。
|
||||
`ICRSW`(Hummer & Voels 切换)在 tlusty208 **原版中是死代码**
|
||||
(SUBROUTINE SWITCH 从未被 CALL,CRSW≡1.0),不要依赖它稳定化;
|
||||
即便源码修复启用后实测对难点也无帮助。详见 EXPERIENCE.md §5Y。
|
||||
|
||||
@ -21,19 +21,26 @@ grid:
|
||||
# 共 4*2*2*3*3*3 = 432 个点
|
||||
|
||||
# ---- 收敛链 ----
|
||||
# 已验证的三步配方 (tests/cno_sdspectrum, Teff=35000 logg=5.5):
|
||||
# 已验证的三步配方 (tests/sdB_spectra/GUIDE.md 实测, Teff=35000 logg=5.5):
|
||||
# LTE grey (T T, NITER=0) -> NLTE 连续谱 (nc, ilvlin=0) -> NLTE 谱线 (nl, ilvlin=100)
|
||||
# nc 步是关键:在不考虑谱线扰动下收敛 NLTE 电离平衡,给 nl 一个稳定种子。
|
||||
# 跳过 nc(grey -> 完整 NLTE)会发散。
|
||||
#
|
||||
# 重要:不要在 nst 里设 CHMAX/ITEK,用 tlusty 默认值(CHMAX=0.001, ITEK=4)。
|
||||
# 设 CHMAX=0.1 会让 nc 提前停止,给 nl 一个坏种子导致发散。
|
||||
#
|
||||
# nc 的 NITER=10 是实测最优(tests/sdB_spectra/GUIDE.md NITER 扫描结论):
|
||||
# - NITER=10 总耗时 12.4min(35000K CNO 完整模型)
|
||||
# - 光谱精度 vs NITER=50 差异仅 6e-6(完全等价)
|
||||
# - NITER=50 浪费 2.2× 时间,NITER=200/500 更浪费且无收益
|
||||
# 物理上 nc(纯连续谱)缺少谱线约束,外层温度永不真正收敛——追求高 NITER
|
||||
# 没意义。nl(含谱线)会自修正到正确解(流量差异 <3e-12)。
|
||||
chain:
|
||||
- {label: lte, lte: T, ltgray: T, ilvlin: 0, require_converged: false, niter: 0}
|
||||
- {label: nc, lte: F, ltgray: F, ilvlin: 0, require_converged: false, niter: 50}
|
||||
- {label: nc, lte: F, ltgray: F, ilvlin: 0, require_converged: false, niter: 10}
|
||||
- {label: nl, lte: F, ltgray: F, ilvlin: 100, require_converged: true, niter: 100}
|
||||
itek_fallback: [] # 留空:ITEK 默认值最稳;非空会重试(实测无效)
|
||||
niter: 100 # 每个 tlusty 运行的默认最大迭代数 (nst NITER)
|
||||
niter: 100 # 每个 tlusty 运行的默认最大迭代数 (nst NITER);nc 阶段在 chain 内覆盖为 10
|
||||
|
||||
# ---- 种子步进回退(NEW)----
|
||||
# 冷启动失败时,自动用已收敛的邻居模型作种子,用 LTGRAY=F 热启动重试。
|
||||
@ -42,7 +49,7 @@ niter: 100 # 每个 tlusty 运行的默认最大迭代数 (nst NITER)
|
||||
seed_step_fallback: true
|
||||
|
||||
# ---- 执行参数 ----
|
||||
nworkers: 24 # 并行 worker 数(每次 tlusty 运行是单线程的;
|
||||
nworkers: 16 # 并行 worker 数(每次 tlusty 运行是单线程的;
|
||||
# 想用更多核心就调大;每个 worker 需要独立工作目录)
|
||||
timeout_sec: 3600 # 单模型墙钟时间上限
|
||||
resume: true # 跳过已完成的模型(conv.json 中 converged=true)
|
||||
|
||||
43
cno_grid/config_dense.yaml
Normal file
43
cno_grid/config_dense.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
# 6 维 CNO NLTE 热亚矮星网格 —— 加密版配置
|
||||
#
|
||||
# 相比 config.yaml 的改动:
|
||||
# 1. CNO 各维加 -3:[-4,-2,-1] → [-4,-3,-2,-1]
|
||||
# 消除 -4→-2 的 100× 丰度跳跃,每步均匀 10×,种子步进更稳
|
||||
# 2. Teff 加 50000:消除 40K→60K 的跨度,中间点有助种子传递
|
||||
# 3. 配合 run_grid.py 的 wave scheduling(按 CNO 总量分批提交)
|
||||
#
|
||||
# 总点数: 5*2*2*4*4*4 = 1280(vs 原 432,约 3 倍)
|
||||
# 预计耗时: 1280/16 * 12min ≈ 16 小时
|
||||
|
||||
# ---- 网格轴 ----
|
||||
grid:
|
||||
teff: [20000, 30000, 40000, 50000, 60000]
|
||||
logg: [5.0, 5.5, 6.0, 6.5]
|
||||
loghe: [-4, -2, 0, 2]
|
||||
logc: [-4, -3, -2, -1]
|
||||
logn: [-4, -3, -2, -1]
|
||||
logo: [-4, -3, -2, -1]
|
||||
# 共 5*4*4*4*4*4 = 5120 个点
|
||||
# CNO 每步丰度跳跃: 10× (均匀)
|
||||
|
||||
# ---- 收敛链(同 config.yaml)----
|
||||
chain:
|
||||
- {label: lte, lte: T, ltgray: T, ilvlin: 0, require_converged: false, niter: 0}
|
||||
- {label: nc, lte: F, ltgray: F, ilvlin: 0, require_converged: false, niter: 10}
|
||||
- {label: nl, lte: F, ltgray: F, ilvlin: 100, require_converged: true, niter: 100}
|
||||
itek_fallback: []
|
||||
niter: 100
|
||||
|
||||
# ---- 种子步进回退 ----
|
||||
seed_step_fallback: true
|
||||
|
||||
# ---- 执行参数 ----
|
||||
nworkers: 20
|
||||
timeout_sec: 3600
|
||||
resume: true
|
||||
|
||||
# ---- 路径 ----
|
||||
template: templates/cno_atmos.5.tpl
|
||||
fort55: templates/fort.55.lin
|
||||
linelist: data/gfVIS99.dat
|
||||
results: results
|
||||
58
cno_grid/dist_config.yaml
Normal file
58
cno_grid/dist_config.yaml
Normal file
@ -0,0 +1,58 @@
|
||||
# 分布式网格计算配置(dist_master.py 用)。
|
||||
# 架构:master 单向 SSH 推/拉(只需 master 能 SSH 进 worker,worker 不回连 master)。
|
||||
# 网格轴本身仍在 config.yaml 定义,这里只配 master/qiniu/workers。
|
||||
#
|
||||
# 部署:填好本文件后
|
||||
# python3 src/dist_master.py dist_config.yaml --dry-run # 预览
|
||||
# python3 src/dist_master.py dist_config.yaml # 全量跑
|
||||
|
||||
master:
|
||||
# master 上 cno_grid 目录(tasks.json 在此,调度在此)
|
||||
workdir: /home/dckj/program/tlusty/tl208-s54/cno_grid
|
||||
# 网格配置(默认 workdir/config.yaml)
|
||||
grid_config: /home/dckj/program/tlusty/tl208-s54/cno_grid/config.yaml
|
||||
# 结果根目录(相对 workdir 或绝对路径;与 config.yaml 的 results 一致)
|
||||
# master 在此汇总 grid_status.json;各 worker 各自有自己的 results/(同名)
|
||||
results: results
|
||||
|
||||
qiniu:
|
||||
# 七牛云 bucket 名(种子库)。填你的实际 bucket。留空则纯本地模式。
|
||||
bucket: your-bucket
|
||||
# AK/SK:仅 master 持有。建议用环境变量,别提交到仓库:
|
||||
# export QINIU_ACCESS_KEY=... ; export QINIU_SECRET_KEY=...
|
||||
access_key: ""
|
||||
secret_key: ""
|
||||
# 七牛绑定域名(下载/上传种子用)。公开空间填 http(s)://域名。
|
||||
domain: ""
|
||||
seed_prefix: seeds
|
||||
# 谁上传种子到七牛:
|
||||
# master(默认)= master 取回 .7 后代传,worker 完全不碰七牛(worker 零配置)
|
||||
# worker = worker 跑完自己传(需 worker 配 QINIU_DOMAIN)
|
||||
upload_by: master
|
||||
|
||||
# ---- worker 节点(master 只需能 SSH 进这些机器)----
|
||||
# 每个 worker:
|
||||
# host : user@host(SSH 目标)。localhost 表示本机也算一个 worker。
|
||||
# tlusty_root: 该机器上 TLUSTY 树根(含 tlusty/tlusty.exe、synspec/、data/)
|
||||
# nproc : 该机贡献的并发进程数(建议 ≤ 物理核数)
|
||||
#
|
||||
# 注意:单向 SSH。只需 master→worker 免密,worker 不需要 SSH 回 master。
|
||||
#
|
||||
# 示例(5 台机器):
|
||||
workers:
|
||||
- host: localhost
|
||||
tlusty_root: /home/dckj/program/tlusty/tl208-s54
|
||||
nproc: 8
|
||||
# - host: user@server2
|
||||
# tlusty_root: ~/tlusty/tl208-s54
|
||||
# nproc: 24
|
||||
# - host: user@server3
|
||||
# tlusty_root: ~/tlusty/tl208-s54
|
||||
# nproc: 24
|
||||
# - host: user@server4
|
||||
# tlusty_root: ~/tlusty/tl208-s54
|
||||
# nproc: 16
|
||||
# - host: user@server5
|
||||
# tlusty_root: ~/tlusty/tl208-s54
|
||||
# nproc: 24
|
||||
|
||||
1
cno_grid/results
Symbolic link
1
cno_grid/results
Symbolic link
@ -0,0 +1 @@
|
||||
/mnt/e/fmq/grid
|
||||
@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 边界点测试脚本(旧版,使用 logCNO=0)。每个点跑完后把收敛情况追加到日志。
|
||||
# 注:当前推荐使用 run_boundary_corrected.sh(修正后的丰度范围)。
|
||||
export TLUSTY=/home/dckj/program/tlusty/tl208-s54
|
||||
cd $TLUSTY/cno_grid
|
||||
LOG=results/boundary_runs.log
|
||||
echo "=== 边界测试开始 $(date) ===" > $LOG
|
||||
|
||||
# 跑一个边界点,并把结果摘要追加到日志
|
||||
run_pt() {
|
||||
local teff=$1 logg=$2 loghe=$3 name=$4
|
||||
echo ">>> [$name] teff=$teff logg=$logg logHe=$loghe $(date)" >> $LOG
|
||||
python3 src/run_one.py --teff $teff --logg $logg --loghe $loghe \
|
||||
--logc 0 --logn 0 --logo 0 >> $LOG 2>&1
|
||||
# 记录结果
|
||||
python3 -c "
|
||||
import json,glob,os
|
||||
d='t{}_g{:.1f}_he{:.0f}_c0_n0_o0'.format(int($teff),$logg,$loghe)
|
||||
p=os.path.join('results',d,'conv.json')
|
||||
try:
|
||||
j=json.load(open(p))
|
||||
print(' {} {} {} -> converged={} max_relc={} elapsed={}'.format(
|
||||
'$name',$teff,$logg,j['converged'],j.get('final_max_relc'),j.get('elapsed_sec')),file=open($LOG,'a'))
|
||||
except Exception as e: print(' $name ERROR:',e,file=open($LOG,'a'))
|
||||
" >> $LOG 2>&1
|
||||
}
|
||||
|
||||
# 边界点1已在独立进程跑,这里跑2-5
|
||||
run_pt 80000 6.5 2 "b2_高温He-rich"
|
||||
run_pt 80000 6.5 -4 "b3_高温He-poor"
|
||||
run_pt 40000 6.0 0 "b4_中心点高金属参考"
|
||||
run_pt 20000 6.5 -4 "b5_低温贫He"
|
||||
|
||||
echo "=== 全部完成 $(date) ===" >> $LOG
|
||||
@ -1,45 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 修正丰度范围(logCNO = -4..-1)后的边界测试。
|
||||
# 之前的"高温发散"实际上是因为 abn=1.0(logX=0)相当于 4000 倍太阳丰度。
|
||||
# 此处用物理上合理的丰度重新跑这组边界。
|
||||
#
|
||||
# 测试点(全部后台并行运行):
|
||||
# B1 80000/6.5/-4/ -1/-1/-1 高温贫 He、富金属(CNO 为 0.1 倍 H)
|
||||
# B2 80000/6.5/-4/ -4/-4/-4 高温贫 He、近太阳 CNO
|
||||
# B3 80000/6.5/ 2/ -1/-1/-1 高温富 He(物理上最难的点)
|
||||
# B4 80000/6.5/ 2/ -4/-4/-4 高温富 He、近太阳
|
||||
# B5 20000/5.0/ 2/ -1/-1/-1 低温富 He、富金属
|
||||
# B6 20000/5.0/ 2/ -4/-4/-4 低温富 He、近太阳
|
||||
# B7 40000/5.5/ 0/ -4/-4/-4 中温类太阳 CNO(丰度下限)
|
||||
# B8 60000/6.0/ 0/ -1/-1/-1 中高温富金属抽查点
|
||||
|
||||
set -u
|
||||
cd /home/dckj/program/tlusty/tl208-s54
|
||||
RESULTS=cno_grid/results
|
||||
mkdir -p "$RESULTS"
|
||||
|
||||
# 后台运行一个模型,把 stdout/stderr 写入对应日志文件
|
||||
run_bg() {
|
||||
local tag="$1"; shift
|
||||
local logfile="$RESULTS/bound_${tag}.log"
|
||||
echo "[$(date +%H:%M:%S)] launching $tag -> $logfile"
|
||||
nohup python3 cno_grid/src/run_one.py "$@" > "$logfile" 2>&1 &
|
||||
echo " pid=$!"
|
||||
}
|
||||
|
||||
# 高温边界(80000K)—— 最重要的复测点
|
||||
run_bg 80k_he-4_cno-1 --teff 80000 --logg 6.5 --loghe -4 --logc -1 --logn -1 --logo -1
|
||||
run_bg 80k_he-4_cno-4 --teff 80000 --logg 6.5 --loghe -4 --logc -4 --logn -4 --logo -4
|
||||
run_bg 80k_he2_cno-1 --teff 80000 --logg 6.5 --loghe 2 --logc -1 --logn -1 --logo -1
|
||||
run_bg 80k_he2_cno-4 --teff 80000 --logg 6.5 --loghe 2 --logc -4 --logn -4 --logo -4
|
||||
|
||||
# 低温富 He 边界(20000K, logHe=2)
|
||||
run_bg 20k_he2_cno-1 --teff 20000 --logg 5.0 --loghe 2 --logc -1 --logn -1 --logo -1
|
||||
run_bg 20k_he2_cno-4 --teff 20000 --logg 5.0 --loghe 2 --logc -4 --logn -4 --logo -4
|
||||
|
||||
# 中温丰度边界
|
||||
run_bg 40k_he0_cno-4 --teff 40000 --logg 5.5 --loghe 0 --logc -4 --logn -4 --logo -4
|
||||
run_bg 60k_he0_cno-1 --teff 60000 --logg 6.0 --loghe 0 --logc -1 --logn -1 --logo -1
|
||||
|
||||
wait
|
||||
echo "[$(date +%H:%M:%S)] ALL BOUNDARY TESTS COMPLETE"
|
||||
@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 针对剩余冷启动失败边界点的种子步进验证测试。
|
||||
#
|
||||
# 策略:冷启动在低温、以及高温+富 He+低 CNO 的区域是可行的。用这些已
|
||||
# 收敛的大气作为种子,去热启动冷启动下会发散的几个点:
|
||||
#
|
||||
# S1 60000/6.0/0 /-1/-1/-1 以 40000/5.5/0/-4/-4/-4 为种子(Teff 不同)
|
||||
# S2 80000/6.5/-4/-1/-1/-1 以 80000/6.5/-4/-4/-4/-4 为种子(同 T、更低金属)
|
||||
# S3 80000/6.5/+2/-1/-1/-1 以 80000/6.5/+2/-4/-4/-4 为种子(同 T、更低金属)
|
||||
#
|
||||
# 注意:S1 把 Teff 从 40000 跨到 60000(原子相同)—— 比同 T、降金属的步进
|
||||
# 更苛刻,但仍值得一试。
|
||||
|
||||
set -u
|
||||
cd /home/dckj/program/tlusty/tl208-s54
|
||||
RESULTS=cno_grid/results/seed_step
|
||||
mkdir -p "$RESULTS"
|
||||
|
||||
# S2 首先需要一个已收敛的 80k_he-4_cno-4 大气 —— 上面的 seed_step/ 运行
|
||||
# 已经有了一个,直接拿它当种子。
|
||||
SEED_80K_HEPOOR_CNO4=cno_grid/results/seed_step/t80000_g6.5_he-4_c-4_n-4_o-4/t80000_g6.5_he-4_c-4_n-4_o-4.7
|
||||
SEED_80K_HERICH_CNO4=cno_grid/results/t80000_g6.5_he2_c-4_n-4_o-4/t80000_g6.5_he2_c-4_n-4_o-4.7
|
||||
SEED_40K_HEPOOR_CNO4=cno_grid/results/t40000_g5.5_he0_c-4_n-4_o-4/t40000_g5.5_he0_c-4_n-4_o-4.7
|
||||
|
||||
# 后台运行一个种子步进模型
|
||||
run_bg() {
|
||||
local tag="$1"; shift
|
||||
echo "[$(date +%H:%M:%S)] launching $tag"
|
||||
nohup python3 cno_grid/src/seed_step.py "$@" > "$RESULTS/${tag}.log" 2>&1 &
|
||||
echo " pid=$!"
|
||||
}
|
||||
|
||||
# S2:80K 贫 He 富金属,以 80K 贫 He 低金属为种子(刚验证过)
|
||||
run_bg s2_80k_he-4_cno-1 --teff 80000 --logg 6.5 --loghe -4 \
|
||||
--logc -1 --logn -1 --logo -1 --seed $SEED_80K_HEPOOR_CNO4
|
||||
|
||||
# S3:80K 富 He 富金属,以 80K 富 He 低金属为种子
|
||||
run_bg s3_80k_he2_cno-1 --teff 80000 --logg 6.5 --loghe 2 \
|
||||
--logc -1 --logn -1 --logo -1 --seed $SEED_80K_HERICH_CNO4
|
||||
|
||||
# S1:60K 贫 He 富金属,以 40K 贫 He 低金属为种子
|
||||
run_bg s1_60k_he0_cno-1 --teff 60000 --logg 6.0 --loghe 0 \
|
||||
--logc -1 --logn -1 --logo -1 --seed $SEED_40K_HEPOOR_CNO4
|
||||
|
||||
wait
|
||||
echo "[$(date +%H:%M:%S)] ALL SEED_STEP TESTS COMPLETE"
|
||||
109
cno_grid/src/claim_task.py
Normal file
109
cno_grid/src/claim_task.py
Normal file
@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""master 端原子任务领取(worker 经 SSH 调用)。
|
||||
|
||||
tasks.json 结构:
|
||||
{
|
||||
"grid": "config.yaml 路径或标识",
|
||||
"created": <unix秒>,
|
||||
"tasks": {
|
||||
"<model_name>": {
|
||||
"params": {"teff":..,"logg":..,"loghe":..,"logc":..,"logn":..,"logo":..},
|
||||
"status": "pending|running|done|failed",
|
||||
"worker": <worker_id 或 null>,
|
||||
"claimed_at": <unix秒>,
|
||||
"finished_at": <unix秒>,
|
||||
"attempts": <int>,
|
||||
"max_relc": <float>,
|
||||
"seed_step_used": <bool>,
|
||||
"note": <str>
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
并发安全:用 fcntl.flock 对 tasks.json 加排他锁,保证多 worker 同时
|
||||
claim/report 不冲突。worker 一次领一个任务(可批量,见 --batch)。
|
||||
|
||||
用法(worker 经 SSH):
|
||||
ssh master "python3 src/claim_task.py --tasks tasks.json --worker w1 [--batch 1]"
|
||||
→ 成功:stdout 打印一个 task 的 JSON(含 params + name);无任务:打印 null
|
||||
"""
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import gen_input5 # noqa: E402 -- model_name
|
||||
|
||||
|
||||
def _lock_and_update(tasks_path, fn):
|
||||
"""对 tasks.json 加排他锁,执行 fn(data) 并写回。返回 fn 的返回值。"""
|
||||
# 先确保文件存在(dist_master 初始化时已建,这里兜底)
|
||||
fd = open(tasks_path, "r+")
|
||||
try:
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_EX)
|
||||
fd.seek(0)
|
||||
try:
|
||||
data = json.load(fd)
|
||||
except ValueError:
|
||||
data = {"tasks": {}}
|
||||
result = fn(data)
|
||||
fd.seek(0)
|
||||
fd.truncate()
|
||||
json.dump(data, fd, indent=2)
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
return result
|
||||
finally:
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
|
||||
fd.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="原子领取一个 pending 任务")
|
||||
ap.add_argument("--tasks", default="tasks.json", help="tasks.json 路径")
|
||||
ap.add_argument("--worker", required=True, help="worker 标识(如 w1@host)")
|
||||
ap.add_argument("--batch", type=int, default=1,
|
||||
help="一次领取 N 个任务(减少 SSH 往返)")
|
||||
ap.add_argument("--phase", default=None,
|
||||
help="阶段过滤:phase1=只领无种子的冷启动;"
|
||||
"phase2=只领之前 failed 的(补算)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.tasks):
|
||||
print(json.dumps(None))
|
||||
return
|
||||
|
||||
def claim(data):
|
||||
tasks = data.get("tasks", {})
|
||||
now = time.time()
|
||||
claimed = []
|
||||
for name, t in tasks.items():
|
||||
if len(claimed) >= args.batch:
|
||||
break
|
||||
if t.get("status") != "pending":
|
||||
continue
|
||||
if args.phase == "phase2" and t.get("attempts", 0) == 0:
|
||||
# phase2 只领 phase1 已尝试过且失败的
|
||||
continue
|
||||
t["status"] = "running"
|
||||
t["worker"] = args.worker
|
||||
t["claimed_at"] = now
|
||||
t["attempts"] = t.get("attempts", 0) + 1
|
||||
claimed.append({"name": name, "params": t["params"]})
|
||||
return claimed
|
||||
|
||||
claimed = _lock_and_update(args.tasks, claim)
|
||||
if not claimed:
|
||||
print(json.dumps(None))
|
||||
elif args.batch <= 1:
|
||||
print(json.dumps(claimed[0]))
|
||||
else:
|
||||
print(json.dumps(claimed))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
59
cno_grid/src/dist_check.py
Normal file
59
cno_grid/src/dist_check.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""worker 端状态查询(被 master 经 SSH 调用)。
|
||||
|
||||
worker 无常驻进程。master 周期性 SSH 进 worker 调本脚本,一次列出该 worker
|
||||
上所有已完成(有 conv.json)的模型及其收敛状态,供 master 回收决策。
|
||||
|
||||
输出(stdout,一行 JSON):{model_name: {converged, final_max_relc, elapsed_sec, ...}}
|
||||
|
||||
用法(master 经 SSH):
|
||||
ssh worker "cd ~/tlusty/tl208-s54/cno_grid && python3 src/dist_check.py --results results"
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def scan_results(results_root):
|
||||
"""扫描 results/<name>/conv.json,返回 {name: summary}。"""
|
||||
out = {}
|
||||
if not os.path.isdir(results_root):
|
||||
return out
|
||||
for d in sorted(os.listdir(results_root)):
|
||||
full = os.path.join(results_root, d)
|
||||
conv = os.path.join(full, "conv.json")
|
||||
if not (os.path.isdir(full) and os.path.isfile(conv)):
|
||||
continue
|
||||
try:
|
||||
with open(conv) as f:
|
||||
meta = json.load(f)
|
||||
except Exception:
|
||||
continue
|
||||
# 只报关键字段,减小 SSH 传输
|
||||
out[d] = {
|
||||
"converged": bool(meta.get("converged", False)),
|
||||
"final_max_relc": meta.get("final_max_relc"),
|
||||
"elapsed_sec": meta.get("elapsed_sec"),
|
||||
"atmosphere_has_nan": meta.get("atmosphere_has_nan", False),
|
||||
"synspec_rc": meta.get("synspec_rc"),
|
||||
"has_7": os.path.isfile(os.path.join(full, d + ".7")),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="worker 状态查询")
|
||||
ap.add_argument("--results", default="results",
|
||||
help="results 根目录(相对 cwd 或绝对)")
|
||||
args = ap.parse_args()
|
||||
results_root = args.results
|
||||
if not os.path.isabs(results_root):
|
||||
# 相对当前工作目录(master SSH 时已 cd 到 cno_grid)
|
||||
results_root = os.path.join(os.getcwd(), results_root)
|
||||
out = scan_results(results_root)
|
||||
print(json.dumps(out))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
733
cno_grid/src/dist_master.py
Normal file
733
cno_grid/src/dist_master.py
Normal file
@ -0,0 +1,733 @@
|
||||
#!/usr/bin/env python3
|
||||
"""分布式 master(单向 SSH 推/拉模型)。
|
||||
|
||||
适配场景:只有 master → worker 单向 SSH 可达(worker 在 NAT 后,SSH 不回 master)。
|
||||
|
||||
模型:
|
||||
- 任务队列 tasks.json 只在 master 本地(fcntl.flock 强一致,零并发风险)
|
||||
- worker 无常驻进程;master 直接 SSH 执行 run_one.py 跑单点(异步 nohup)
|
||||
- master 周期 SSH 进 worker 查状态、取结果(conv.json + .7)
|
||||
- 七牛云只存种子(master 取回 .7 后代传;worker 不碰七牛)
|
||||
|
||||
主循环:
|
||||
init tasks.json(断点续算:本地已 converged 标 done)
|
||||
while 有 pending:
|
||||
for 每个 worker host(并发线程,每台一个):
|
||||
SSH 查正在跑的 run_one 数 (pgrep) → 空闲槽 = nproc - running
|
||||
for 每个空闲槽:
|
||||
task = claim_task()(本地 flock 领一个 pending→running)
|
||||
SSH worker "nohup python3 src/run_one.py --teff .. &"(异步)
|
||||
sleep 轮询
|
||||
for 每个 worker:
|
||||
SSH 查已完成的(conv.json 存在且进程已退出)
|
||||
读结果 → 更新 tasks.json → scp .7(若收敛)→ 上传七牛
|
||||
写 grid_status.json
|
||||
|
||||
并发安全:
|
||||
- claim_task/report_task 用 flock,master 多线程也安全
|
||||
- worker 不碰 tasks.json,无跨机并发问题
|
||||
|
||||
用法:
|
||||
python3 src/dist_master.py dist_config.yaml # 全量
|
||||
python3 src/dist_master.py dist_config.yaml --dry-run # 预览
|
||||
python3 src/dist_master.py dist_config.yaml --max-runtime 4h # 限时
|
||||
"""
|
||||
import argparse
|
||||
import fcntl
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import gen_input5 # noqa: E402
|
||||
import qiniu_store # noqa: E402
|
||||
|
||||
# ---- YAML 加载(优先 pyyaml,否则内置极简解析器,零 pip)----
|
||||
try:
|
||||
import yaml
|
||||
|
||||
def _load_yaml(path):
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
except ImportError:
|
||||
def _load_yaml(path):
|
||||
cfg = {}
|
||||
section = None
|
||||
cur_list = None
|
||||
cur_item = None
|
||||
with open(path) as f:
|
||||
for raw in f:
|
||||
line = raw.split("#", 1)[0].rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
stripped = line.strip()
|
||||
indented = line[:1] in (" ", "\t")
|
||||
if stripped.startswith("- ") and indented and cur_list is not None:
|
||||
body = stripped[2:].strip()
|
||||
if body.startswith("{") and body.endswith("}"):
|
||||
d = {}
|
||||
for kv in body[1:-1].split(","):
|
||||
if ":" in kv:
|
||||
k, v = kv.split(":", 1)
|
||||
d[k.strip()] = _scalar(v.strip())
|
||||
cur_list.append(d)
|
||||
cur_item = None
|
||||
elif ":" in body:
|
||||
k, v = body.split(":", 1)
|
||||
cur_item = {k.strip(): _scalar(v.strip())}
|
||||
cur_list.append(cur_item)
|
||||
else:
|
||||
cur_list.append(_scalar(body))
|
||||
cur_item = None
|
||||
continue
|
||||
if ":" in stripped:
|
||||
key, val = stripped.split(":", 1)
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
if not indented:
|
||||
cur_item = None
|
||||
if val == "":
|
||||
if key in ("workers", "chain"):
|
||||
cur_list = []
|
||||
cfg[key] = cur_list
|
||||
section = None
|
||||
else:
|
||||
new_container = {}
|
||||
cfg[key] = new_container
|
||||
section = new_container
|
||||
cur_list = None
|
||||
else:
|
||||
cfg[key] = _parse_list(val)
|
||||
section = None
|
||||
cur_list = None
|
||||
else:
|
||||
target = cur_item if cur_item is not None else section
|
||||
if target is not None:
|
||||
target[key] = _parse_list(val)
|
||||
return cfg
|
||||
|
||||
def _parse_list(val):
|
||||
val = val.strip()
|
||||
if val.startswith("[") and val.endswith("]"):
|
||||
return [_scalar(x.strip()) for x in val[1:-1].split(",") if x.strip()]
|
||||
return _scalar(val)
|
||||
|
||||
def _scalar(v):
|
||||
if v in ("true", "True"):
|
||||
return True
|
||||
if v in ("false", "False"):
|
||||
return False
|
||||
try:
|
||||
return int(v)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(v)
|
||||
except ValueError:
|
||||
pass
|
||||
if len(v) >= 2 and v[0] in "\"'" and v[-1] == v[0]:
|
||||
return v[1:-1]
|
||||
return v
|
||||
|
||||
|
||||
# ==================== tasks.json 原子操作(本地 flock)====================
|
||||
def _lock_tasks(tasks_path, fn):
|
||||
"""对 tasks.json 加排他锁,执行 fn(data),写回。返回 fn 的返回值。"""
|
||||
fd = open(tasks_path, "r+")
|
||||
try:
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_EX)
|
||||
fd.seek(0)
|
||||
try:
|
||||
data = json.load(fd)
|
||||
except ValueError:
|
||||
data = {"tasks": {}}
|
||||
result = fn(data)
|
||||
fd.seek(0)
|
||||
fd.truncate()
|
||||
json.dump(data, fd, indent=2)
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
return result
|
||||
finally:
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
|
||||
fd.close()
|
||||
|
||||
|
||||
def expand_grid(grid):
|
||||
keys = ["teff", "logg", "loghe", "logc", "logn", "logo"]
|
||||
for combo in itertools.product(*(grid[k] for k in keys)):
|
||||
yield dict(zip(keys, combo))
|
||||
|
||||
|
||||
def model_done_local(results_root, name):
|
||||
p = os.path.join(results_root, name, "conv.json")
|
||||
if not os.path.isfile(p):
|
||||
return False
|
||||
try:
|
||||
return bool(json.load(open(p)).get("converged"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def init_tasks(grid_cfg, results_root, tasks_path, grid_id="config.yaml", force=False):
|
||||
if os.path.isfile(tasks_path) and not force:
|
||||
return json.load(open(tasks_path))
|
||||
tasks = {}
|
||||
for pt in expand_grid(grid_cfg["grid"]):
|
||||
name = gen_input5.model_name(pt["teff"], pt["logg"], pt["loghe"],
|
||||
pt["logc"], pt["logn"], pt["logo"])
|
||||
done = model_done_local(results_root, name)
|
||||
tasks[name] = {"params": pt, "status": "done" if done else "pending",
|
||||
"worker": None, "host": None, "claimed_at": None,
|
||||
"finished_at": None, "attempts": 0, "max_relc": None,
|
||||
"seed_step_used": False, "note": None}
|
||||
data = {"grid": grid_id, "created": time.time(), "tasks": tasks}
|
||||
with open(tasks_path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
return data
|
||||
|
||||
|
||||
def claim_task(tasks_path, host, worker_id, phase=None):
|
||||
"""领一个 pending 任务 → running,返回 {name, params} 或 None。"""
|
||||
def do(data):
|
||||
tasks = data["tasks"]
|
||||
now = time.time()
|
||||
for name, t in tasks.items():
|
||||
if t["status"] != "pending":
|
||||
continue
|
||||
if phase == "phase2" and t.get("attempts", 0) == 0:
|
||||
continue
|
||||
t["status"] = "running"
|
||||
t["host"] = host
|
||||
t["worker"] = worker_id
|
||||
t["claimed_at"] = now
|
||||
t["attempts"] = t.get("attempts", 0) + 1
|
||||
return {"name": name, "params": t["params"]}
|
||||
return None
|
||||
return _lock_tasks(tasks_path, do)
|
||||
|
||||
|
||||
def report_task(tasks_path, name, status, **fields):
|
||||
def do(data):
|
||||
t = data["tasks"].get(name)
|
||||
if t is None:
|
||||
return
|
||||
t["status"] = status
|
||||
for k in ("max_relc", "seed_step_used", "elapsed_sec", "note"):
|
||||
if k in fields and fields[k] is not None:
|
||||
t[k] = fields[k]
|
||||
if status in ("done", "failed"):
|
||||
t["finished_at"] = time.time()
|
||||
_lock_tasks(tasks_path, do)
|
||||
|
||||
|
||||
def count_statuses(data):
|
||||
c = {}
|
||||
for t in data["tasks"].values():
|
||||
c[t["status"]] = c.get(t["status"], 0) + 1
|
||||
return c
|
||||
|
||||
|
||||
def reset_stale(tasks_path, stale_sec):
|
||||
"""running 超 stale_sec → pending(worker 崩溃/SSH 断的兜底)。"""
|
||||
def do(data):
|
||||
now = time.time()
|
||||
reset = 0
|
||||
for t in data["tasks"].values():
|
||||
if t["status"] == "running":
|
||||
if now - (t.get("claimed_at") or 0) > stale_sec:
|
||||
t["status"] = "pending"
|
||||
t["worker"] = None
|
||||
t["host"] = None
|
||||
reset += 1
|
||||
return reset
|
||||
return _lock_tasks(tasks_path, do)
|
||||
|
||||
|
||||
# ==================== SSH 工具(master → worker 单向)====================
|
||||
def ssh_ok(target, cmd, timeout=30):
|
||||
"""SSH 执行命令,返回 (rc, stdout, stderr)。target=user@host 或 host。"""
|
||||
full = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", target, cmd]
|
||||
try:
|
||||
r = subprocess.run(full, capture_output=True, text=True, timeout=timeout)
|
||||
return r.returncode, r.stdout.strip(), r.stderr.strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return 124, "", "ssh timeout"
|
||||
|
||||
|
||||
def count_running_on_worker(target, worker_cwd):
|
||||
"""SSH 查 worker 上正在跑的 run_one 进程数。"""
|
||||
cmd = "pgrep -fc 'run_one.py' 2>/dev/null || echo 0"
|
||||
rc, out, _ = ssh_ok(target, cmd, timeout=20)
|
||||
if rc != 0 or not out:
|
||||
return 0
|
||||
try:
|
||||
return int(out.strip().splitlines()[-1])
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def dispatch_task(target, worker_cwd, tlusty_root, name, params, results,
|
||||
seed_path=None, qiniu_domain="", timeout=3600, grid_config="config.yaml"):
|
||||
"""SSH 派一个任务到 worker(异步 nohup,立即返回)。
|
||||
|
||||
master 本地已 claim;这里只负责在 worker 上启动 run_one.py。
|
||||
"""
|
||||
parts = ["{teff:.0f}", "{logg:.1f}", "{loghe:.0f}",
|
||||
"{logc:.0f}", "{logn:.0f}", "{logo:.0f}"]
|
||||
args = ("--teff {teff} --logg {logg} --loghe {loghe} "
|
||||
"--logc {logc} --logn {logn} --logo {logo}").format(**params)
|
||||
cmd_seed = "--seed '{}'".format(seed_path) if seed_path else ""
|
||||
# run_one.py 在 cno_grid/src 下;results 相对 worker_cwd
|
||||
env = " ".join([
|
||||
"TLUSTY='{}'".format(tlusty_root),
|
||||
"QINIU_DOMAIN='{}'".format(qiniu_domain),
|
||||
"DIST_TASK_NAME='{}'".format(name),
|
||||
])
|
||||
# 日志按任务名,便于查;结果写 worker 本地 results/
|
||||
log_file = "{}/worker_jobs/{}.log".format(worker_cwd, name)
|
||||
run_cmd = (
|
||||
"mkdir -p {cwd}/worker_jobs && cd {cwd} && "
|
||||
"{env} nohup python3 src/run_one.py {args} {seed} "
|
||||
"--results {res} --timeout {to} > {logf} 2>&1 & echo $!"
|
||||
).format(cwd=worker_cwd, env=env, args=args, seed=cmd_seed,
|
||||
res=results, to=timeout, logf=log_file)
|
||||
rc, pid, err = ssh_ok(target, run_cmd, timeout=30)
|
||||
return pid, err
|
||||
|
||||
|
||||
def dispatch_seed_step(target, worker_cwd, tlusty_root, name, params,
|
||||
results, seed_path, timeout=3600):
|
||||
"""SSH 派一个 seed_step 补算任务到 worker(异步 nohup)。
|
||||
|
||||
冷启动失败后,master 从本地/七牛拉到种子 .7,scp 到 worker,再调
|
||||
seed_step.py(LTGRAY=F 热启动)重试。
|
||||
"""
|
||||
args = ("--teff {teff} --logg {logg} --loghe {loghe} "
|
||||
"--logc {logc} --logn {logn} --logo {logo}").format(**params)
|
||||
# seed_path 是 worker 上的路径(已 scp 过去)
|
||||
env = "TLUSTY='{}'".format(tlusty_root)
|
||||
log_file = "{}/worker_jobs/{}.seedstep.log".format(worker_cwd, name)
|
||||
# seed_step.py 把结果写到 results/seed_step/<name>/;这里指定 results 让它和冷启动同目录
|
||||
run_cmd = (
|
||||
"mkdir -p {cwd}/worker_jobs && cd {cwd} && "
|
||||
"{env} nohup python3 src/seed_step.py {args} --seed '{seed}' "
|
||||
"--results {res} > {logf} 2>&1 & echo $!"
|
||||
).format(cwd=worker_cwd, env=env, args=args, seed=seed_path,
|
||||
res=results, logf=log_file)
|
||||
rc, pid, err = ssh_ok(target, run_cmd, timeout=30)
|
||||
return pid, err
|
||||
|
||||
|
||||
def push_seed_to_worker(target, local_seed, remote_seed):
|
||||
"""scp master 本地的 .7 种子到 worker。返回 worker 上路径或 None。"""
|
||||
try:
|
||||
r = subprocess.run(["scp", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15",
|
||||
local_seed, "{}:{}".format(target, remote_seed)],
|
||||
capture_output=True, timeout=60)
|
||||
if r.returncode == 0:
|
||||
return remote_seed
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def fetch_worker_status(target, worker_cwd, results):
|
||||
"""SSH 查 worker 上所有已完成任务(有 conv.json 的)。
|
||||
|
||||
返回 {model_name: conv_summary_dict}。供 master 决定哪些该回收。
|
||||
用 dist_check.py(一次列出全部,比逐个 cat 高效)。
|
||||
"""
|
||||
cmd = "cd {cwd} && python3 src/dist_check.py --results {res} 2>/dev/null".format(
|
||||
cwd=worker_cwd, res=results)
|
||||
rc, out, err = ssh_ok(target, cmd, timeout=40)
|
||||
if rc != 0 or not out:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(out)
|
||||
except ValueError:
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_seed(target, worker_cwd, results, name, dest_path):
|
||||
"""scp 取 worker 上的 .7 大气到 master 本地 dest_path。"""
|
||||
src = "{}:{}/{}/{}.7".format(target, worker_cwd.rstrip("/"), results, name)
|
||||
try:
|
||||
r = subprocess.run(["scp", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15",
|
||||
src, dest_path], capture_output=True, timeout=60)
|
||||
return r.returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
|
||||
|
||||
def fetch_conv(target, worker_cwd, results, name):
|
||||
"""SSH 读 worker 上某模型的 conv.json 内容。"""
|
||||
cmd = "cat {}/{}/{}.conv.json 2>/dev/null || cat {}/{}/conv.json 2>/dev/null".format(
|
||||
worker_cwd.rstrip("/"), results, name, worker_cwd.rstrip("/"), results + "/" + name)
|
||||
# run_one 写的是 results/<name>/conv.json
|
||||
cmd = "cat {0}/{1}/{2}/conv.json 2>/dev/null".format(
|
||||
worker_cwd.rstrip("/"), results, name)
|
||||
rc, out, err = ssh_ok(target, cmd, timeout=20)
|
||||
if rc != 0 or not out:
|
||||
return None
|
||||
try:
|
||||
return json.loads(out)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def cleanup_worker_result(target, worker_cwd, results, name):
|
||||
"""回收后清理 worker 上的结果目录(可选,省 worker 磁盘)。
|
||||
|
||||
保留 .7 给本地 seed_step 用,删大 .spec。
|
||||
默认不删——worker 本地种子对 seed_step 有用。
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# ==================== per-worker 管理线程 ====================
|
||||
class WorkerManager(threading.Thread):
|
||||
"""每台 worker 一个线程:填充空闲槽 + 回收已完成。"""
|
||||
|
||||
def __init__(self, wid, target, tlusty_root, nproc, worker_cwd,
|
||||
results, tasks_path, store, qiniu_upload_by, grid_config,
|
||||
timeout, stale_sec, phase, seed_step_on, seed_dir):
|
||||
super().__init__(daemon=True)
|
||||
self.wid = wid
|
||||
self.target = target
|
||||
self.tlusty_root = tlusty_root
|
||||
self.nproc = nproc
|
||||
self.worker_cwd = worker_cwd
|
||||
self.results = results
|
||||
self.tasks_path = tasks_path
|
||||
self.store = store
|
||||
self.qiniu_upload_by = qiniu_upload_by
|
||||
self.grid_config = grid_config
|
||||
self.timeout = timeout
|
||||
self.stale_sec = stale_sec
|
||||
self.phase = phase
|
||||
self.seed_step_on = seed_step_on
|
||||
self.seed_dir = seed_dir # master 本地存从 worker 取回的 .7
|
||||
self.stop = False
|
||||
|
||||
def log(self, msg):
|
||||
print(" [w{} {}] {}".format(self.wid, self.target, msg), flush=True)
|
||||
|
||||
def run(self):
|
||||
os.makedirs(self.seed_dir, exist_ok=True)
|
||||
while not self.stop:
|
||||
try:
|
||||
self._fill_slots()
|
||||
self._collect_results()
|
||||
except Exception as e:
|
||||
self.log("循环异常: {}".format(e))
|
||||
# 检查是否还有 pending
|
||||
data = json.load(open(self.tasks_path))
|
||||
counts = count_statuses(data)
|
||||
if counts.get("pending", 0) == 0 and counts.get("running", 0) == 0:
|
||||
break
|
||||
time.sleep(15)
|
||||
|
||||
def _fill_slots(self):
|
||||
"""查 worker 空闲槽,派任务。"""
|
||||
running = count_running_on_worker(self.target, self.worker_cwd)
|
||||
free = max(0, self.nproc - running)
|
||||
if free == 0:
|
||||
return
|
||||
for _ in range(free):
|
||||
task = claim_task(self.tasks_path, self.target,
|
||||
"w{}".format(self.wid), phase=self.phase)
|
||||
if task is None:
|
||||
return # 没任务了
|
||||
name = task["name"]
|
||||
params = task["params"]
|
||||
self.log("派发 {} (worker 现有 {})".format(name, running))
|
||||
pid, err = dispatch_task(
|
||||
self.target, self.worker_cwd, self.tlusty_root,
|
||||
name, params, self.results,
|
||||
seed_path=None, qiniu_domain=self.store.domain,
|
||||
timeout=self.timeout, grid_config=self.grid_config)
|
||||
if err and "nohup" not in str(err).lower():
|
||||
self.log("派发 {} 警告: {}".format(name, err[:100]))
|
||||
|
||||
def _collect_results(self):
|
||||
"""回收 worker 上已完成的结果。"""
|
||||
statuses = fetch_worker_status(self.target, self.worker_cwd, self.results)
|
||||
if not statuses:
|
||||
return
|
||||
data = json.load(open(self.tasks_path))
|
||||
claimed_here = {n: t for n, t in data["tasks"].items()
|
||||
if t.get("host") == self.target and t["status"] == "running"}
|
||||
for name in claimed_here:
|
||||
if name not in statuses:
|
||||
continue
|
||||
conv = statuses[name]
|
||||
converged = conv.get("converged", False)
|
||||
max_relc = conv.get("final_max_relc")
|
||||
elapsed = conv.get("elapsed_sec")
|
||||
seed_step_used = conv.get("seed_step_used", False)
|
||||
# 但注意:run_one 的 seed_step 不会自动发生(需 master 触发),
|
||||
# 这里 converged=False 的点,master 后续做 seed_step 补算。
|
||||
if converged:
|
||||
# 取回 .7 种子到 master
|
||||
local_seed = os.path.join(self.seed_dir, name + ".7")
|
||||
ok = fetch_seed(self.target, self.worker_cwd, self.results,
|
||||
name, local_seed)
|
||||
if ok and self.store.domain:
|
||||
# 上传七牛(master 代传)
|
||||
if self.qiniu_upload_by == "master":
|
||||
up_ok, msg = self.store.upload_seed(local_seed, name)
|
||||
self.log("{} 收敛,种子上传七牛: {}".format(
|
||||
name, "ok" if up_ok else msg))
|
||||
report_task(self.tasks_path, name, "done",
|
||||
max_relc=max_relc, elapsed_sec=elapsed,
|
||||
seed_step_used=seed_step_used, note="converged")
|
||||
self.log("{} DONE max_relc={}".format(name, max_relc))
|
||||
else:
|
||||
# 冷启动未收敛:尝试 seed_step 补算(若有种子)。
|
||||
# 找种子:master 本地 seed_dir > 七牛云。seed_step 只试一次。
|
||||
if not self.seed_step_on:
|
||||
report_task(self.tasks_path, name, "failed",
|
||||
max_relc=max_relc, elapsed_sec=elapsed,
|
||||
note="unconverged cold-start (seed_step off)")
|
||||
self.log("{} FAILED (seed_step 关闭)".format(name))
|
||||
continue
|
||||
# 已尝试过 seed_step 仍失败 → 终态 failed
|
||||
if seed_step_used or data["tasks"][name].get("attempts", 0) >= 2:
|
||||
report_task(self.tasks_path, name, "failed",
|
||||
max_relc=max_relc, elapsed_sec=elapsed,
|
||||
seed_step_used=seed_step_used,
|
||||
note="unconverged after seed_step (物理极限?)")
|
||||
self.log("{} FAILED (seed_step 后仍发散)".format(name))
|
||||
continue
|
||||
# 找种子
|
||||
seed = self._find_seed_for(params)
|
||||
if seed is None:
|
||||
report_task(self.tasks_path, name, "failed",
|
||||
max_relc=max_relc, elapsed_sec=elapsed,
|
||||
note="unconverged, no seed available")
|
||||
self.log("{} FAILED (无种子做 seed_step)".format(name))
|
||||
continue
|
||||
# scp 种子到 worker,再派 seed_step
|
||||
remote_seed = os.path.join(self.worker_cwd,
|
||||
".seed_cache", name + ".seed.7")
|
||||
worker_seed = push_seed_to_worker(self.target, seed, remote_seed)
|
||||
if worker_seed is None:
|
||||
report_task(self.tasks_path, name, "failed",
|
||||
max_relc=max_relc, elapsed_sec=elapsed,
|
||||
note="seed_step: push seed failed")
|
||||
self.log("{} seed_step 种子推送失败".format(name))
|
||||
continue
|
||||
# 标记 running(attempts++),重新派 seed_step
|
||||
def mark_retry(d):
|
||||
t = d["tasks"].get(name)
|
||||
if t:
|
||||
t["status"] = "running"
|
||||
t["seed_step_used"] = True
|
||||
t["claimed_at"] = time.time()
|
||||
_lock_tasks(self.tasks_path, mark_retry)
|
||||
# 备份 worker 上的冷启动失败结果(seed_step 会覆盖同目录)
|
||||
# seed_step.py 默认写 results/seed_step/,不会覆盖冷启动目录;
|
||||
# 但为统一回收,这里让它写到 results/ 下同 name 目录会冲突,
|
||||
# 所以让 seed_step 写到 seed_step/ 子目录,回收时合并。
|
||||
pid, err = dispatch_seed_step(
|
||||
self.target, self.worker_cwd, self.tlusty_root,
|
||||
name, params, self.results, worker_seed, timeout=self.timeout)
|
||||
self.log("{} seed_step 重试 (seed={})".format(
|
||||
name, os.path.basename(worker_seed)))
|
||||
|
||||
def _find_seed_for(self, params):
|
||||
"""为某点找种子 .7:master 本地 seed_dir > 七牛云。返回本地路径或 None。"""
|
||||
# 本地:扫 seed_dir 找最近邻
|
||||
teff, logg, loghe = params["teff"], params["logg"], params["loghe"]
|
||||
logc, logn, logo = params["logc"], params["logn"], params["logo"]
|
||||
best = None
|
||||
best_d = None
|
||||
if os.path.isdir(self.seed_dir):
|
||||
for fn in os.listdir(self.seed_dir):
|
||||
if not fn.endswith(".7"):
|
||||
continue
|
||||
nm = fn[:-2]
|
||||
try:
|
||||
parts = nm.split("_")
|
||||
pte = int(parts[0][1:]); plogg = float(parts[1][1:])
|
||||
phe = float(parts[2][2:]); pc = float(parts[3][1:])
|
||||
pn = float(parts[4][1:]); po = float(parts[5][1:])
|
||||
except (IndexError, ValueError):
|
||||
continue
|
||||
if pte == teff and plogg == logg and phe == loghe:
|
||||
d = abs(pc - logc) + abs(pn - logn) + abs(po - logo)
|
||||
else:
|
||||
d = (abs(pte - teff) / 5000.0 + abs(plogg - logg) * 2.0
|
||||
+ abs(phe - loghe) * 0.5) * 10 # 同 family 优先
|
||||
if best_d is None or d < best_d:
|
||||
best_d, best = d, os.path.join(self.seed_dir, fn)
|
||||
if best is not None and best_d is not None and best_d < 100:
|
||||
return best
|
||||
# 七牛云
|
||||
if self.store.domain:
|
||||
try:
|
||||
names = self.store.list_seeds()
|
||||
qb, qd = None, None
|
||||
for nm in names:
|
||||
try:
|
||||
parts = nm.split("_")
|
||||
pte = int(parts[0][1:]); plogg = float(parts[1][1:])
|
||||
phe = float(parts[2][2:]); pc = float(parts[3][1:])
|
||||
pn = float(parts[4][1:]); po = float(parts[5][1:])
|
||||
except (IndexError, ValueError):
|
||||
continue
|
||||
if pte == teff and plogg == logg and phe == loghe:
|
||||
d = abs(pc - logc) + abs(pn - logn) + abs(po - logo)
|
||||
else:
|
||||
d = (abs(pte - teff) / 5000.0 + abs(plogg - logg) * 2.0
|
||||
+ abs(phe - loghe) * 0.5) * 10
|
||||
if qd is None or d < qd:
|
||||
qd, qb = d, nm
|
||||
if qb is not None and qd is not None and qd < 100:
|
||||
local = os.path.join(self.seed_dir, qb + ".7")
|
||||
if not os.path.exists(local):
|
||||
if self.store.download_seed(qb, local):
|
||||
return local
|
||||
else:
|
||||
return local
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def write_grid_status(tasks_path, results_root, elapsed):
|
||||
data = json.load(open(tasks_path))
|
||||
counts = count_statuses(data)
|
||||
seed_step_count = sum(1 for t in data["tasks"].values()
|
||||
if t.get("seed_step_used"))
|
||||
models = [{"name": n, "status": ("converged" if t["status"] == "done"
|
||||
else "unfinished"),
|
||||
"max_relc": t.get("max_relc"),
|
||||
"seed_step_used": t.get("seed_step_used", False)}
|
||||
for n, t in data["tasks"].items()]
|
||||
summary = {"total": len(data["tasks"]), "elapsed_sec": round(elapsed, 1),
|
||||
"counts": {"converged": counts.get("done", 0),
|
||||
"unfinished": counts.get("running", 0) + counts.get("pending", 0),
|
||||
"error": counts.get("failed", 0),
|
||||
"skipped": 0},
|
||||
"seed_step_retries": seed_step_count, "models": models}
|
||||
with open(os.path.join(results_root, "grid_status.json"), "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
return summary
|
||||
|
||||
|
||||
# ==================== 主入口 ====================
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="分布式 master(单向 SSH 推/拉)")
|
||||
ap.add_argument("dist_config")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
ap.add_argument("--phase", default=None, help="phase1=冷启动;phase2=补算")
|
||||
ap.add_argument("--stale-sec", type=int, default=7200,
|
||||
help="running 超此时长无回收 → 重派(默认 2 小时)")
|
||||
ap.add_argument("--force-init", action="store_true")
|
||||
ap.add_argument("--poll-sec", type=int, default=20)
|
||||
ap.add_argument("--timeout", type=int, default=3600, help="单点墙钟上限")
|
||||
ap.add_argument("--no-qiniu", action="store_true",
|
||||
help="不传种子到七牛(仅本地)")
|
||||
args = ap.parse_args()
|
||||
|
||||
dcfg = _load_yaml(args.dist_config)
|
||||
workdir = dcfg["master"]["workdir"]
|
||||
grid_config_path = dcfg["master"].get(
|
||||
"grid_config", os.path.join(workdir, "config.yaml"))
|
||||
grid_cfg = _load_yaml(grid_config_path)
|
||||
results = dcfg["master"].get("results", "results")
|
||||
results_root = results if os.path.isabs(results) else os.path.join(workdir, results)
|
||||
os.makedirs(results_root, exist_ok=True)
|
||||
tasks_path = os.path.join(workdir, "tasks.json")
|
||||
seed_dir = os.path.join(results_root, ".seeds_remote")
|
||||
|
||||
data = init_tasks(grid_cfg, results_root, tasks_path,
|
||||
grid_id=args.dist_config, force=args.force_init)
|
||||
counts = count_statuses(data)
|
||||
print("网格任务:{}".format(counts), flush=True)
|
||||
|
||||
workers = dcfg.get("workers", [])
|
||||
qiniu = dcfg.get("qiniu", {})
|
||||
store = qiniu_store.QiniuStore(
|
||||
access_key=qiniu.get("access_key"),
|
||||
secret_key=qiniu.get("secret_key"),
|
||||
bucket=qiniu.get("bucket"),
|
||||
domain=qiniu.get("domain"),
|
||||
seed_prefix=qiniu.get("seed_prefix", "seeds"))
|
||||
if args.no_qiniu:
|
||||
store = qiniu_store.QiniuStore() # 空 store,不传
|
||||
qiniu_upload_by = qiniu.get("upload_by", "master")
|
||||
|
||||
print("worker 节点 {} 个:".format(len(workers)), flush=True)
|
||||
for i, w in enumerate(workers):
|
||||
print(" [w{}] {} nproc={}".format(i, w["host"], w.get("nproc", 1)),
|
||||
flush=True)
|
||||
|
||||
if args.dry_run:
|
||||
total_cores = sum(w.get("nproc", 1) for w in workers)
|
||||
to_compute = counts.get("pending", 0)
|
||||
est = to_compute * 1400 / max(total_cores, 1)
|
||||
print("\n待计算 {} 点,总并发 {} 核".format(to_compute, total_cores))
|
||||
print("预计 ~{:.1f} 小时(单点 1400s 估算)".format(est / 3600))
|
||||
print("\n部署检查清单:")
|
||||
print(" 1. master 能 SSH 进每台 worker(免密)")
|
||||
print(" 2. 每台 worker 上 {}/src/run_one.py 可执行".format(
|
||||
"<tlusty_root>/cno_grid"))
|
||||
print(" 3. 七牛: {}".format("已配置" if store.domain else "未配置(本地模式)"))
|
||||
return
|
||||
|
||||
if not workers:
|
||||
print("ERROR: workers 为空", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 启动每台 worker 的管理线程
|
||||
print("\n=== 启动 worker 管理线程 ===", flush=True)
|
||||
managers = []
|
||||
for i, w in enumerate(workers):
|
||||
wm = WorkerManager(
|
||||
i, w["host"], w["tlusty_root"], w.get("nproc", 1),
|
||||
os.path.join(w["tlusty_root"], "cno_grid"),
|
||||
results, tasks_path, store, qiniu_upload_by,
|
||||
os.path.basename(grid_config_path), args.timeout,
|
||||
args.stale_sec, args.phase, True, seed_dir)
|
||||
wm.start()
|
||||
managers.append(wm)
|
||||
|
||||
# 主监控循环
|
||||
print("\n=== 监控(Ctrl+C 停止,worker 远端继续跑,重跑即续算)===",
|
||||
flush=True)
|
||||
t0 = time.time()
|
||||
try:
|
||||
while any(m.is_alive() for m in managers):
|
||||
time.sleep(args.poll_sec)
|
||||
reset = reset_stale(tasks_path, args.stale_sec)
|
||||
if reset:
|
||||
print(" [monitor] 重派 {} 个僵死任务".format(reset), flush=True)
|
||||
data = json.load(open(tasks_path))
|
||||
counts = count_statuses(data)
|
||||
elapsed = time.time() - t0
|
||||
print("[{:.0f}min] {}".format(elapsed / 60, counts), flush=True)
|
||||
write_grid_status(tasks_path, results_root, elapsed)
|
||||
if counts.get("pending", 0) == 0 and counts.get("running", 0) == 0:
|
||||
print("\n所有任务终态。", flush=True)
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("\n中断。worker 远端仍在跑,重跑本命令可续算。", flush=True)
|
||||
for m in managers:
|
||||
m.stop = True
|
||||
|
||||
elapsed = time.time() - t0
|
||||
summary = write_grid_status(tasks_path, results_root, elapsed)
|
||||
print("\nDONE. counts={} seed_step={} {:.0f}s".format(
|
||||
summary["counts"], summary["seed_step_retries"], elapsed))
|
||||
print("汇总:{}".format(os.path.join(results_root, "grid_status.json")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
cno_grid/src/dist_worker.py
Normal file
28
cno_grid/src/dist_worker.py
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""[已废弃] 旧的 worker 轮询循环(双向 SSH 模型)。
|
||||
|
||||
当前架构(master 单向 SSH 推/拉)下,worker **无常驻进程、不需要本脚本**:
|
||||
- master 直接 SSH 执行 src/run_one.py 跑单点(冷启动)
|
||||
- master 经 SSH 调 src/dist_check.py 查 worker 状态、取结果
|
||||
- seed_step 补算由 master SSH 调 src/seed_step.py(带 --seed)
|
||||
|
||||
本文件保留仅为文档说明,不参与运行。相关逻辑已迁至:
|
||||
- src/dist_master.py (master 推/拉调度,含 seed_step 触发)
|
||||
- src/dist_check.py (worker 状态查询)
|
||||
- src/run_one.py (单点冷启动执行器,被 SSH 调用)
|
||||
- src/seed_step.py (单点种子步进执行器,被 SSH 调用)
|
||||
|
||||
如果你看到旧文档引用 dist_worker.py,请改用 dist_master.py。
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
raise SystemExit(
|
||||
"dist_worker.py 已废弃。当前用 master 单向 SSH 模型:\n"
|
||||
" master 直接 ssh worker 调 run_one.py / seed_step.py 跑单点。\n"
|
||||
" 请运行: python3 src/dist_master.py dist_config.yaml\n"
|
||||
" 详见 DIST.md。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
263
cno_grid/src/qiniu_store.py
Normal file
263
cno_grid/src/qiniu_store.py
Normal file
@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""七牛云对象存储的轻量封装(零 SDK 依赖)。
|
||||
|
||||
仅用 Python 标准库(hmac/hashlib/base64/json)+ curl 实现七牛云的上传/
|
||||
下载/列举。worker 端无需 pip 装任何东西。
|
||||
|
||||
七牛上传签名机制:
|
||||
PutPolicy = {"scope":"<bucket>:<key>", "deadline":<unix秒>}
|
||||
编码后用 AccessKey/SecretKey 做 HMAC-SHA1 签名,得到 uploadToken。
|
||||
|
||||
用途(本分布式网格):
|
||||
- 七牛云当"种子库":每个收敛模型的 .7 大气(393KB)上传为
|
||||
<seed_prefix>/<model_name>.7,供异地 worker 在 seed_step 回退时拉取。
|
||||
- 仅 master 持有 AK/SK 生成 uptoken;worker 只拿 token 上传(无需密钥)。
|
||||
- 大产物(.spec 等 13MB)不上传,留各机本地。
|
||||
|
||||
环境变量:
|
||||
QINIU_ACCESS_KEY / QINIU_SECRET_KEY / QINIU_BUCKET / QINIU_DOMAIN
|
||||
(可被 dist_config.yaml 的 qiniu 段覆盖;master 在派发时把 token 传给 worker)
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
try:
|
||||
import urllib.request as urlreq
|
||||
except ImportError: # py2 兜底(不会触发)
|
||||
urlreq = None
|
||||
|
||||
|
||||
class QiniuStore:
|
||||
"""七牛云存储客户端。master 与 worker 共用;worker 只用 download/upload(token)。"""
|
||||
|
||||
def __init__(self, access_key=None, secret_key=None, bucket=None,
|
||||
domain=None, seed_prefix="seeds"):
|
||||
self.ak = access_key or os.environ.get("QINIU_ACCESS_KEY", "")
|
||||
self.sk = secret_key or os.environ.get("QINIU_SECRET_KEY", "")
|
||||
self.bucket = bucket or os.environ.get("QINIU_BUCKET", "")
|
||||
# 绑定域名(如 http:// 或 https:// + 域名,无尾斜杠)。公开读下载用。
|
||||
self.domain = (domain or os.environ.get("QINIU_DOMAIN", "")).rstrip("/")
|
||||
self.seed_prefix = seed_prefix
|
||||
|
||||
# ---- base64 url-safe(七牛要求)----
|
||||
@staticmethod
|
||||
def _b64u(data):
|
||||
"""bytes -> url-safe base64 字符串(去 padding 也可,七牛两边都接受)。"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
return base64.urlsafe_b64encode(data).decode("ascii")
|
||||
|
||||
@staticmethod
|
||||
def _b64u_json(obj):
|
||||
return QiniuStore._b64u(json.dumps(obj, separators=(",", ":")))
|
||||
|
||||
def _sign(self, data):
|
||||
"""HMAC-SHA1(data, sk) -> url-safe base64。"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
return self._b64u(hmac.new(self.sk.encode("utf-8"), data,
|
||||
hashlib.sha1).digest())
|
||||
|
||||
# ---- uptoken 生成(master 持有 AK/SK 时调用)----
|
||||
def gen_uptoken(self, key, expires=3600):
|
||||
"""为指定 key 生成上传 token。
|
||||
|
||||
key: 对象名(不含 bucket 前缀),如 "seeds/t30000_g5.0_he0_c-1_n-1_o-1.7"
|
||||
返回 uploadToken 字符串,交给 worker 用 curl 上传。
|
||||
"""
|
||||
scope = "{}:{}".format(self.bucket, key) if key else self.bucket
|
||||
policy = {"scope": scope, "deadline": int(time.time()) + expires}
|
||||
encoded = self._b64u_json(policy)
|
||||
sign = self._sign(encoded)
|
||||
return "{}:{}".format(self.ak, sign) + ":" + encoded
|
||||
|
||||
def gen_private_url(self, key, expires=3600):
|
||||
"""生成私有空间的临时下载 URL(带 e/token 签名)。
|
||||
|
||||
若 bucket 为公开读,直接用 domain/key 即可,不必调这个。
|
||||
"""
|
||||
if not self.domain:
|
||||
raise ValueError("QINIU_DOMAIN 未配置")
|
||||
url = "{}/{}".format(self.domain, key)
|
||||
e = int(time.time()) + expires
|
||||
to_sign = "{}?e={}".format(url, e)
|
||||
token = "{}:{}".format(self.ak, self._sign(to_sign))
|
||||
return "{}?e={}&token={}".format(url, e, token)
|
||||
|
||||
def public_url(self, key):
|
||||
"""公开空间的下载 URL(无签名)。"""
|
||||
if not self.domain:
|
||||
raise ValueError("QINIU_DOMAIN 未配置")
|
||||
return "{}/{}".format(self.domain, key)
|
||||
|
||||
# ---- 上传 / 下载(通过 curl,worker 无需 pip)----
|
||||
def upload(self, local_path, key, uptoken=None):
|
||||
"""上传本地文件到七牛。uptoken 可由 master 预生成传入。
|
||||
|
||||
用 curl 表单 POST(七牛上传接口):
|
||||
POST http://upload.qiniup.com multipart/form-data
|
||||
field "token"=uptoken, "key"=key, "file"=@local_path
|
||||
返回 (ok:bool, resp_text)。
|
||||
"""
|
||||
if uptoken is None:
|
||||
uptoken = self.gen_uptoken(key)
|
||||
# 七牛上传域名:华东 upload.qiniup.com,其它区域用对应域名。
|
||||
upload_host = os.environ.get("QINIU_UPLOAD_HOST", "upload.qiniup.com")
|
||||
url = "http://{}/".format(upload_host)
|
||||
cmd = ["curl", "-sS", "-m", "300", "-X", "POST", url,
|
||||
"-F", "token={}".format(uptoken),
|
||||
"-F", "key={}".format(key),
|
||||
"-F", "file=@{}".format(local_path)]
|
||||
try:
|
||||
out = subprocess.run(cmd, capture_output=True, text=True,
|
||||
timeout=320)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "upload timeout (>300s)"
|
||||
if out.returncode != 0:
|
||||
return False, out.stderr.strip() or out.stdout.strip()
|
||||
try:
|
||||
resp = json.loads(out.stdout)
|
||||
except ValueError:
|
||||
return False, out.stdout.strip()
|
||||
# 成功响应含 "key" 与 "hash";失败含 "error"
|
||||
if "error" in resp:
|
||||
return False, resp["error"]
|
||||
return True, resp.get("key", "")
|
||||
|
||||
def download(self, key, local_path, timeout=120):
|
||||
"""下载对象到本地。公开空间用 public_url,私有空间用 private_url。
|
||||
|
||||
返回 True/False。
|
||||
"""
|
||||
if self.ak and self.sk:
|
||||
url = self.gen_private_url(key)
|
||||
else:
|
||||
url = self.public_url(key)
|
||||
cmd = ["curl", "-sS", "-m", str(timeout), "-f", "-o", local_path, url]
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=timeout + 10)
|
||||
return r.returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
|
||||
# ---- 种子库语义封装 ----
|
||||
def seed_key(self, model_name):
|
||||
"""规范种子对象名:seeds/<model>.7"""
|
||||
return "{}/{}.7".format(self.seed_prefix, model_name)
|
||||
|
||||
def upload_seed(self, local_path, model_name, uptoken=None):
|
||||
"""上传某模型的最终大气 .7 作为种子。返回 (ok, msg)。"""
|
||||
return self.upload(local_path, self.seed_key(model_name), uptoken)
|
||||
|
||||
def download_seed(self, model_name, local_path):
|
||||
"""下载某模型的种子 .7 到 local_path。返回 True/False。"""
|
||||
return self.download(self.seed_key(model_name), local_path)
|
||||
|
||||
# ---- 列举(用于 find_seed 跨网查最近邻种子)----
|
||||
def list_seeds(self):
|
||||
"""返回七牛种子库中所有 <model_name>.7 的模型名列表。
|
||||
|
||||
调用七牛 RS API: GET /list?bucket=<b>&prefix=<seed_prefix>
|
||||
(需 AK/SK 签名)。返回 [] 当未配置或失败。
|
||||
结果缓存到本地文件避免每次都拉(见 _seeds_cache_path)。
|
||||
"""
|
||||
if not (self.ak and self.sk and self.bucket):
|
||||
return []
|
||||
cache = self._seeds_cache_path()
|
||||
# 缓存 5 分钟
|
||||
if os.path.isfile(cache) and time.time() - os.path.getmtime(cache) < 300:
|
||||
try:
|
||||
return json.load(open(cache))
|
||||
except Exception:
|
||||
pass
|
||||
names = self._fetch_seed_list()
|
||||
try:
|
||||
with open(cache, "w") as f:
|
||||
json.dump(names, f)
|
||||
except Exception:
|
||||
pass
|
||||
return names
|
||||
|
||||
def _seeds_cache_path(self):
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", ".qiniu_seeds_cache.json")
|
||||
|
||||
def _fetch_seed_list(self):
|
||||
"""分页拉取七牛种子列表,返回 model_name 列表。"""
|
||||
rs_host = os.environ.get("QINIU_RS_HOST", "http://rsf.qiniuapi.com")
|
||||
names = []
|
||||
marker = ""
|
||||
prefix = self.seed_prefix + "/"
|
||||
while True:
|
||||
path = "/list?bucket={}&prefix={}".format(self.bucket, prefix)
|
||||
if marker:
|
||||
path += "&marker={}".format(marker)
|
||||
# 签名:AccessToken = AK:urlsafe_b64(hmac(sk, path)):<path>
|
||||
encoded_path = path # 七牛签名用原始 path
|
||||
sign = self._sign(encoded_path)
|
||||
access_token = "{}:{}".format(self.ak, sign) + encoded_path
|
||||
url = rs_host + path
|
||||
cmd = ["curl", "-sS", "-m", "60",
|
||||
"-H", "Authorization: QBox " + access_token, url]
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True,
|
||||
timeout=70)
|
||||
if r.returncode != 0:
|
||||
break
|
||||
data = json.loads(r.stdout)
|
||||
except (ValueError, subprocess.TimeoutExpired):
|
||||
break
|
||||
for item in data.get("items", []):
|
||||
k = item.get("key", "")
|
||||
# seeds/<model>.7 -> <model>
|
||||
if k.startswith(prefix) and k.endswith(".7"):
|
||||
names.append(k[len(prefix):-2])
|
||||
marker = data.get("marker", "")
|
||||
if not marker:
|
||||
break
|
||||
return names
|
||||
|
||||
|
||||
def load_from_dist_config(cfg):
|
||||
"""从 dist_config.yaml 的 qiniu 段构造 QiniuStore。"""
|
||||
q = (cfg or {}).get("qiniu", {}) if isinstance(cfg, dict) else {}
|
||||
return QiniuStore(
|
||||
access_key=q.get("access_key"),
|
||||
secret_key=q.get("secret_key"),
|
||||
bucket=q.get("bucket"),
|
||||
domain=q.get("domain"),
|
||||
seed_prefix=q.get("seed_prefix", "seeds"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 自检:打印 token(需配置 AK/SK/Bucket)
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description="qiniu store 自检")
|
||||
ap.add_argument("--gen-token", help="为某 key 生成 uptoken")
|
||||
ap.add_argument("--list", action="store_true", help="列出种子")
|
||||
ap.add_argument("--config", help="dist_config.yaml 路径")
|
||||
args = ap.parse_args()
|
||||
if args.config:
|
||||
try:
|
||||
import yaml
|
||||
cfg = yaml.safe_load(open(args.config))
|
||||
except Exception:
|
||||
# 兜底:不装 yaml 时只从环境变量读
|
||||
cfg = {"qiniu": {}}
|
||||
store = load_from_dist_config(cfg)
|
||||
else:
|
||||
store = QiniuStore()
|
||||
if args.gen_token:
|
||||
print(store.gen_uptoken(args.gen_token))
|
||||
elif args.list:
|
||||
for n in store.list_seeds():
|
||||
print(n)
|
||||
else:
|
||||
print("AK set:", bool(store.ak), "Bucket:", store.bucket,
|
||||
"Domain:", store.domain or "(未配置)")
|
||||
278
cno_grid/src/repair_grid.py
Normal file
278
cno_grid/src/repair_grid.py
Normal file
@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""网格修补脚本:对未收敛的点用"最佳种子 + 种子步进"重试。
|
||||
|
||||
策略(分层推进,从易到难):
|
||||
1. 对每个失败点,在全局种子库中找 CNO 距离最近的已收敛模型作种子
|
||||
2. 用 SEED_STEP_CHAIN(LTGREY=F 热启动)重试
|
||||
3. 成功的点加入种子库,供后续点使用(级联效应)
|
||||
|
||||
关键改进(vs run_grid.py 的 seed_step_fallback):
|
||||
- find_seed 优先选 CNO 距离最近的种子(run_grid 选了全局最近但 CNO 可能远)
|
||||
- 支持中间跳板:如果直接种子太远(CNO距离>3),先算中间点
|
||||
- 清理旧的失败结果再重试(避免 resume 跳过)
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
from multiprocessing import Pool
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import gen_input5
|
||||
import run_one
|
||||
import seed_step
|
||||
|
||||
TLUSTY = os.environ.get("TLUSTY", "/home/dckj/program/tlusty/tl208-s54")
|
||||
|
||||
|
||||
def load_seeds(results_root):
|
||||
"""加载所有已收敛的模型作为种子库。返回 {name: params}。"""
|
||||
seeds = {}
|
||||
for cj in glob.glob(os.path.join(results_root, "t*/conv.json")):
|
||||
if "/seed_step/" in cj:
|
||||
continue
|
||||
d_name = os.path.basename(os.path.dirname(cj))
|
||||
if any(s in d_name for s in [".coldfail", ".COLD_START", ".OLD", ".FAILED"]):
|
||||
continue
|
||||
try:
|
||||
d = json.load(open(cj))
|
||||
p = d.get("params", {})
|
||||
if d.get("converged") and not d.get("atmosphere_has_nan"):
|
||||
seeds[d_name] = p
|
||||
except Exception:
|
||||
pass
|
||||
return seeds
|
||||
|
||||
|
||||
def load_failures(results_root):
|
||||
"""加载所有未收敛的点。返回 [(name, params), ...]。"""
|
||||
failures = []
|
||||
for cj in sorted(glob.glob(os.path.join(results_root, "t*/conv.json"))):
|
||||
if "/seed_step/" in cj:
|
||||
continue
|
||||
d_name = os.path.basename(os.path.dirname(cj))
|
||||
if any(s in d_name for s in [".coldfail", ".COLD_START", ".OLD", ".FAILED"]):
|
||||
continue
|
||||
try:
|
||||
d = json.load(open(cj))
|
||||
p = d.get("params", {})
|
||||
if not (d.get("converged") and not d.get("atmosphere_has_nan")):
|
||||
failures.append((d_name, p))
|
||||
except Exception:
|
||||
pass
|
||||
return failures
|
||||
|
||||
|
||||
def cno_dist(p1, p2):
|
||||
"""CNO 丰度的 L1 距离。"""
|
||||
return (abs(p1["logc"] - p2["logc"]) +
|
||||
abs(p1["logn"] - p2["logn"]) +
|
||||
abs(p1["logo"] - p2["logo"]))
|
||||
|
||||
|
||||
def find_best_seed(target_params, seeds):
|
||||
"""在种子库中找 CNO 距离最近 + Teff/logg/logHe 尽量接近的种子。
|
||||
|
||||
优先级:
|
||||
1. 同 (Teff,logg,logHe) 且 CNO 距离最小
|
||||
2. Teff 接近(±20000K)且 CNO 距离最小
|
||||
3. 全局 CNO 距离最小
|
||||
返回 (seed_name, seed_params, cno_distance) 或 None。
|
||||
"""
|
||||
best_same = None
|
||||
best_same_d = 999
|
||||
best_near = None
|
||||
best_near_d = 999
|
||||
best_global = None
|
||||
best_global_d = 999
|
||||
|
||||
for sname, sp in seeds.items():
|
||||
cd = cno_dist(target_params, sp)
|
||||
# 同 family 优先
|
||||
if (sp["teff"] == target_params["teff"] and
|
||||
sp["logg"] == target_params["logg"] and
|
||||
sp["loghe"] == target_params["loghe"]):
|
||||
if cd < best_same_d:
|
||||
best_same_d = cd
|
||||
best_same = (sname, sp, cd)
|
||||
# Teff 接近(同 Teff 或相邻)
|
||||
elif abs(sp["teff"] - target_params["teff"]) <= 20000:
|
||||
score = cd + abs(sp["teff"] - target_params["teff"]) / 10000 * 0.5
|
||||
if score < best_near_d:
|
||||
best_near_d = score
|
||||
best_near = (sname, sp, cd)
|
||||
# 全局
|
||||
score = cd + abs(sp["teff"] - target_params["teff"]) / 10000
|
||||
if score < best_global_d:
|
||||
best_global_d = score
|
||||
best_global = (sname, sp, cd)
|
||||
|
||||
return best_same or best_near or best_global
|
||||
|
||||
|
||||
def repair_one(args):
|
||||
"""修补单个失败点。Pool worker 函数。"""
|
||||
(name, params, seed_path, results_root, template, fort55, linelist,
|
||||
chain, timeout) = args
|
||||
|
||||
workdir = os.path.join(results_root, name)
|
||||
|
||||
# 备份旧结果
|
||||
backup_dir = workdir + ".repair_backup"
|
||||
if os.path.exists(backup_dir):
|
||||
shutil.rmtree(backup_dir)
|
||||
if os.path.exists(workdir):
|
||||
shutil.move(workdir, backup_dir)
|
||||
|
||||
# 用种子步进链重跑
|
||||
summary = run_one.run_model(
|
||||
params["teff"], params["logg"], params["loghe"],
|
||||
params["logc"], params["logn"], params["logo"],
|
||||
results_root=results_root, template=template,
|
||||
fort55_lin=fort55, linelist=linelist,
|
||||
chain=chain, seed=seed_path, timeout=timeout)
|
||||
|
||||
converged = summary.get("converged", False) and not summary.get("atmosphere_has_nan", False)
|
||||
status = "converged" if converged else "unfinished"
|
||||
relc = summary.get("final_max_relc")
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"status": status,
|
||||
"converged": converged,
|
||||
"final_max_relc": relc,
|
||||
"params": params,
|
||||
"seed": seed_path,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="修补网格中未收敛的点")
|
||||
ap.add_argument("config", help="config.yaml 路径")
|
||||
ap.add_argument("--max-cno-dist", type=int, default=4,
|
||||
help="最大允许的 CNO 距离(超过则跳过,默认 4)")
|
||||
ap.add_argument("--nworkers", type=int, default=16)
|
||||
ap.add_argument("--timeout", type=int, default=3600)
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
import run_grid
|
||||
cfg = run_grid._load_yaml(args.config)
|
||||
results_root = cfg["results"]
|
||||
if not os.path.isabs(results_root):
|
||||
results_root = os.path.join(os.path.dirname(args.config), results_root)
|
||||
template = cfg["template"]
|
||||
if not os.path.isabs(template):
|
||||
template = os.path.join(os.path.dirname(args.config), template)
|
||||
fort55 = cfg["fort55"]
|
||||
if not os.path.isabs(fort55):
|
||||
fort55 = os.path.join(os.path.dirname(args.config), fort55)
|
||||
linelist = cfg["linelist"]
|
||||
if not os.path.isabs(linelist):
|
||||
linelist = os.path.join(TLUSTY, linelist)
|
||||
|
||||
print("=== 网格修补 ===\n")
|
||||
|
||||
# 加载种子库和失败点
|
||||
seeds = load_seeds(results_root)
|
||||
failures = load_failures(results_root)
|
||||
print("种子库: {} 个已收敛模型".format(len(seeds)))
|
||||
print("失败点: {} 个待修补\n".format(len(failures)))
|
||||
|
||||
if not failures:
|
||||
print("没有失败点需要修补。")
|
||||
return
|
||||
|
||||
# 为每个失败点找最佳种子
|
||||
repair_list = []
|
||||
skipped = []
|
||||
for name, params in failures:
|
||||
best = find_best_seed(params, seeds)
|
||||
if best is None:
|
||||
skipped.append((name, "无可用种子"))
|
||||
continue
|
||||
sname, sp, cd = best
|
||||
if cd > args.max_cno_dist:
|
||||
skipped.append((name, "CNO距离={} > {}".format(cd, args.max_cno_dist)))
|
||||
continue
|
||||
seed_path = os.path.join(results_root, sname, sname + ".7")
|
||||
if not os.path.isfile(seed_path):
|
||||
skipped.append((name, "种子文件不存在: {}".format(seed_path)))
|
||||
continue
|
||||
repair_list.append((name, params, seed_path, results_root,
|
||||
template, fort55, linelist,
|
||||
seed_step.SEED_STEP_CHAIN, args.timeout))
|
||||
|
||||
print("将修补: {} 个(CNO距离<={})".format(len(repair_list), args.max_cno_dist))
|
||||
print("跳过: {} 个(距离太远或无种子)\n".format(len(skipped)))
|
||||
|
||||
if skipped:
|
||||
print("跳过的点:")
|
||||
for name, reason in skipped[:10]:
|
||||
print(" {}: {}".format(name, reason))
|
||||
if len(skipped) > 10:
|
||||
print(" ... ({} more)".format(len(skipped) - 10))
|
||||
print()
|
||||
|
||||
if args.dry_run:
|
||||
print("=== DRY RUN: 修补计划 ===\n")
|
||||
for name, params, seed_path, _, _, _ in repair_list[:20]:
|
||||
sname = os.path.basename(os.path.dirname(seed_path))
|
||||
sp = seeds.get(sname, {})
|
||||
cd = cno_dist(params, sp) if sp else "?"
|
||||
print(" {} <- {} (CNO距离={})".format(name, sname, cd))
|
||||
if len(repair_list) > 20:
|
||||
print(" ... ({} more)".format(len(repair_list) - 20))
|
||||
return
|
||||
|
||||
# 并行修补
|
||||
worker_args = [(name, params, seed_path, results_root, template, fort55,
|
||||
linelist, chain, timeout)
|
||||
for name, params, seed_path, _, template, fort55, linelist,
|
||||
chain, timeout in repair_list]
|
||||
|
||||
print("启动 {} 个 worker 并行修补...\n".format(min(args.nworkers, len(worker_args))))
|
||||
t0 = time.time()
|
||||
results = []
|
||||
|
||||
if args.nworkers <= 1:
|
||||
for wa in worker_args:
|
||||
res = repair_one(wa)
|
||||
results.append(res)
|
||||
print(" [{}/{}] {} -> {}".format(
|
||||
len(results), len(worker_args), res["name"], res["status"]))
|
||||
else:
|
||||
with Pool(args.nworkers) as pool:
|
||||
for res in pool.imap_unordered(repair_one, worker_args):
|
||||
results.append(res)
|
||||
print(" [{}/{}] {} -> {}".format(
|
||||
len(results), len(worker_args), res["name"], res["status"]))
|
||||
|
||||
elapsed = time.time() - t0
|
||||
ok = sum(1 for r in results if r["converged"])
|
||||
fail = len(results) - ok
|
||||
|
||||
# 写修补报告
|
||||
report = {
|
||||
"total_repaired": len(results),
|
||||
"converged": ok,
|
||||
"still_failed": fail,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
"details": results,
|
||||
}
|
||||
report_path = os.path.join(results_root, "repair_report.json")
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print("\n=== 修补完成 ===")
|
||||
print("成功: {} / {}".format(ok, len(results)))
|
||||
print("仍失败: {}".format(fail))
|
||||
print("耗时: {:.0f}s".format(elapsed))
|
||||
print("报告: {}".format(report_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
104
cno_grid/src/report_task.py
Normal file
104
cno_grid/src/report_task.py
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""master 端任务结果上报(worker 经 SSH 调用)。
|
||||
|
||||
worker 跑完一个点后调本脚本,把状态写回 tasks.json。同时可附上 conv.json
|
||||
的精简字段(converged / max_relc / seed_step_used / elapsed_sec),
|
||||
master 据此判断是否进入 phase2 补算。
|
||||
|
||||
用法(worker 经 SSH):
|
||||
ssh master "python3 src/report_task.py --tasks tasks.json \
|
||||
--name t30000_g5.0_he0_c-1_n-1_o-1 --status done \
|
||||
--max-relc 0.0069 --seed-step-used false --note 'ok'"
|
||||
|
||||
status 取值:
|
||||
done = 收敛成功(或 synspec 完成)
|
||||
failed = 未收敛/崩溃(attempts 累加,供 phase2 重试)
|
||||
running = 心跳续约(防止 master 误判超时重派)
|
||||
"""
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def _lock_and_update(tasks_path, fn):
|
||||
fd = open(tasks_path, "r+")
|
||||
try:
|
||||
import fcntl as _f
|
||||
_f.flock(fd.fileno(), _f.LOCK_EX)
|
||||
fd.seek(0)
|
||||
try:
|
||||
data = json.load(fd)
|
||||
except ValueError:
|
||||
data = {"tasks": {}}
|
||||
result = fn(data)
|
||||
fd.seek(0)
|
||||
fd.truncate()
|
||||
json.dump(data, fd, indent=2)
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
return result
|
||||
finally:
|
||||
import fcntl as _f
|
||||
_f.flock(fd.fileno(), _f.LOCK_UN)
|
||||
fd.close()
|
||||
|
||||
|
||||
def _bool(s):
|
||||
return str(s).lower() in ("true", "1", "yes")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="上报任务结果到 tasks.json")
|
||||
ap.add_argument("--tasks", default="tasks.json")
|
||||
ap.add_argument("--name", required=True, help="模型名")
|
||||
ap.add_argument("--status", required=True,
|
||||
help="done | failed | running")
|
||||
ap.add_argument("--worker", default=None)
|
||||
ap.add_argument("--max-relc", default=None, help="最终 max_relc(float)")
|
||||
ap.add_argument("--seed-step-used", default=None)
|
||||
ap.add_argument("--elapsed-sec", default=None, help="单点墙钟秒数")
|
||||
ap.add_argument("--note", default=None, help="备注/错误信息")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.tasks):
|
||||
print("ERROR: tasks.json not found")
|
||||
sys.exit(1)
|
||||
|
||||
def update(data):
|
||||
tasks = data.get("tasks", {})
|
||||
t = tasks.get(args.name)
|
||||
if t is None:
|
||||
# 容错:可能 tasks.json 被重建,新建条目
|
||||
t = {"status": "pending"}
|
||||
tasks[args.name] = t
|
||||
now = time.time()
|
||||
t["status"] = args.status
|
||||
if args.worker:
|
||||
t["worker"] = args.worker
|
||||
if args.max_relc is not None:
|
||||
try:
|
||||
t["max_relc"] = float(args.max_relc)
|
||||
except ValueError:
|
||||
pass
|
||||
if args.seed_step_used is not None:
|
||||
t["seed_step_used"] = _bool(args.seed_step_used)
|
||||
if args.elapsed_sec is not None:
|
||||
try:
|
||||
t["elapsed_sec"] = float(args.elapsed_sec)
|
||||
except ValueError:
|
||||
pass
|
||||
if args.note is not None:
|
||||
t["note"] = args.note
|
||||
if args.status in ("done", "failed"):
|
||||
t["finished_at"] = now
|
||||
return t["status"]
|
||||
|
||||
st = _lock_and_update(args.tasks, update)
|
||||
print("OK {} {}".format(args.name, st))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -114,12 +114,67 @@ TLUSTY = os.environ.get(
|
||||
|
||||
|
||||
def expand_grid(grid):
|
||||
"""生成 (teff,logg,loghe,logc,logn,logo) 的完整笛卡尔积。"""
|
||||
"""生成 (teff,logg,loghe,logc,logn,logo) 的完整笛卡尔积。
|
||||
|
||||
返回顺序不保证——如需按难度排序请用 expand_grid_ordered。
|
||||
"""
|
||||
keys = ["teff", "logg", "loghe", "logc", "logn", "logo"]
|
||||
for combo in itertools.product(*(grid[k] for k in keys)):
|
||||
yield dict(zip(keys, combo))
|
||||
|
||||
|
||||
def expand_grid_ordered(grid):
|
||||
"""生成按"难度"排序的网格点列表,让易收敛的点先算、难的后算。
|
||||
|
||||
排序键(优先级从高到低):
|
||||
1. CNO 总丰度 logC+logN+logO 升序 —— 金属越低越易收敛,且是后续点的种子
|
||||
2. Teff 升序 —— 低温冷启动更稳,产出的种子可供高温点用
|
||||
3. logg 降序 —— 高重力大气更紧凑、更稳,先算
|
||||
4. logHe 升序 —— He 越少越难(金属不透明度占比更大),先算 He 足够的
|
||||
|
||||
这样排序配合分批提交(wave scheduling),能让每个难点在计算时
|
||||
都有物理上接近的已收敛邻居作种子,大幅提升 seed_step 成功率。
|
||||
实测:旧版无序提交时 60K 收敛率 25%,按难度排序后期望 >50%。
|
||||
"""
|
||||
points = list(expand_grid(grid))
|
||||
points.sort(key=lambda p: (
|
||||
p["logc"] + p["logn"] + p["logo"], # CNO 总量升序(主键)
|
||||
p["teff"], # Teff 升序
|
||||
-p["logg"], # logg 降序(高 g 先)
|
||||
p["loghe"], # logHe 升序
|
||||
))
|
||||
return points
|
||||
|
||||
|
||||
def _split_waves(worker_args, nworkers):
|
||||
"""把已排序的 worker_args 按 CNO 总量分成多个 wave(批)。
|
||||
|
||||
每个 wave 是一个 worker_args 子列表。分批依据:CNO 总量相同的点
|
||||
放同一 wave。如果某个 wave 的点数 > nworkers,它会自然被 Pool
|
||||
分流(imap_unordered 内部排队),不影响正确性。
|
||||
|
||||
这样保证:低金属 wave 先完成 → 其 .7 文件成为高金属 wave 的种子。
|
||||
"""
|
||||
if not worker_args:
|
||||
return []
|
||||
waves = []
|
||||
current_wave = []
|
||||
current_cno_sum = None
|
||||
for wa in worker_args:
|
||||
pt = wa[0]
|
||||
cno_sum = pt["logc"] + pt["logn"] + pt["logo"]
|
||||
if current_cno_sum is None:
|
||||
current_cno_sum = cno_sum
|
||||
if cno_sum != current_cno_sum:
|
||||
waves.append(current_wave)
|
||||
current_wave = []
|
||||
current_cno_sum = cno_sum
|
||||
current_wave.append(wa)
|
||||
if current_wave:
|
||||
waves.append(current_wave)
|
||||
return waves
|
||||
|
||||
|
||||
def model_done(results_root, name):
|
||||
"""该模型是否已成功完成(conv.json 报告 converged=true)。"""
|
||||
p = os.path.join(results_root, name, "conv.json")
|
||||
@ -211,7 +266,55 @@ def find_seed(results_root, teff, logg, loghe, logc=None, logn=None, logo=None):
|
||||
if closest_d is None or d_total < closest_d:
|
||||
closest_d, closest = d_total, atmo
|
||||
# 同 family 优先(哪怕 CNO 跨度大);否则用全局最近邻
|
||||
return exact_family or closest
|
||||
local_seed = exact_family or closest
|
||||
if local_seed is not None:
|
||||
return local_seed
|
||||
# 分布式模式回退:本地无种子时,查七牛云种子库(QINIU_SEED_FALLBACK=1 开启)。
|
||||
# 单机本地模式默认关闭,完全不影响原有行为。
|
||||
if os.environ.get("QINIU_SEED_FALLBACK") == "1":
|
||||
try:
|
||||
import qiniu_store
|
||||
store = qiniu_store.QiniuStore()
|
||||
names = store.list_seeds()
|
||||
if not names:
|
||||
return None
|
||||
best = None
|
||||
best_d = None
|
||||
best_exact = None
|
||||
best_exact_d = None
|
||||
for nm in names:
|
||||
try:
|
||||
parts = nm.split("_")
|
||||
pte = int(parts[0][1:])
|
||||
plogg = float(parts[1][1:])
|
||||
phe = float(parts[2][2:])
|
||||
pc = float(parts[3][1:])
|
||||
pn = float(parts[4][1:])
|
||||
po = float(parts[5][1:])
|
||||
except (IndexError, ValueError):
|
||||
continue
|
||||
if pte == teff and plogg == logg and phe == loghe:
|
||||
dcno = abs(pc - logc) + abs(pn - logn) + abs(po - logo)
|
||||
if best_exact_d is None or dcno < best_exact_d:
|
||||
best_exact_d, best_exact = dcno, nm
|
||||
continue
|
||||
d_tot = (abs(pte - teff) / 5000.0
|
||||
+ abs(plogg - logg) * 2.0
|
||||
+ abs(phe - loghe) * 0.5)
|
||||
if best_d is None or d_tot < best_d:
|
||||
best_d, best = d_tot, nm
|
||||
chosen = best_exact or best
|
||||
if chosen is None:
|
||||
return None
|
||||
cache = os.path.join(results_root, ".seed_cache", chosen + ".7")
|
||||
os.makedirs(os.path.dirname(cache), exist_ok=True)
|
||||
if not os.path.exists(cache):
|
||||
if not store.download_seed(chosen, cache):
|
||||
return None
|
||||
return cache
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _worker(args):
|
||||
@ -299,7 +402,7 @@ def main():
|
||||
filt[k.strip()] = float(v)
|
||||
|
||||
points = []
|
||||
for pt in expand_grid(cfg["grid"]):
|
||||
for pt in expand_grid_ordered(cfg["grid"]):
|
||||
if all(pt.get(k) == v for k, v in filt.items()):
|
||||
points.append(pt)
|
||||
if args.limit:
|
||||
@ -313,12 +416,6 @@ def main():
|
||||
pt["logc"], pt["logn"], pt["logo"])))
|
||||
print("grid: {} points total, {} already done, {} to compute".format(
|
||||
n_total, n_skip, n_total - n_skip))
|
||||
if args.dry_run:
|
||||
for pt in points[:50]:
|
||||
print(" ", pt)
|
||||
if n_total > 50:
|
||||
print(" ... ({} more)".format(n_total - 50))
|
||||
return
|
||||
|
||||
os.makedirs(results_root, exist_ok=True)
|
||||
chain = cfg.get("chain")
|
||||
@ -329,13 +426,45 @@ def main():
|
||||
# 种子步进回退:冷启动失败时,用已收敛邻居作种子重试(默认启用)
|
||||
seed_step_fallback = cfg.get("seed_step_fallback", True)
|
||||
|
||||
if args.dry_run:
|
||||
# 显示排序后的顺序 + wave 划分
|
||||
waves = _split_waves(
|
||||
[(pt, results_root, template, fort55, linelist,
|
||||
chain, itek_fallback, niter, timeout, seed_step_fallback)
|
||||
for pt in points], nworkers)
|
||||
print("scheduling: {} wave(s) by CNO total abundance".format(len(waves)))
|
||||
for wi, wave in enumerate(waves):
|
||||
cno_sum = wave[0][0]["logc"] + wave[0][0]["logn"] + wave[0][0]["logo"]
|
||||
print(" wave {}: CNO_sum={}, {} points, teff range {}-{}".format(
|
||||
wi + 1, cno_sum, len(wave),
|
||||
min(pt["teff"] for pt, *_ in wave),
|
||||
max(pt["teff"] for pt, *_ in wave)))
|
||||
print()
|
||||
for pt in points[:30]:
|
||||
cno_sum = pt["logc"] + pt["logn"] + pt["logo"]
|
||||
print(" [CNO_sum={:>3}] teff={} logg={} loghe={} c={} n={} o={}".format(
|
||||
cno_sum, pt["teff"], pt["logg"], pt["loghe"],
|
||||
pt["logc"], pt["logn"], pt["logo"]))
|
||||
if n_total > 30:
|
||||
print(" ... ({} more)".format(n_total - 30))
|
||||
return
|
||||
|
||||
worker_args = [(pt, results_root, template, fort55, linelist,
|
||||
chain, itek_fallback, niter, timeout, seed_step_fallback)
|
||||
for pt in points]
|
||||
|
||||
t0 = time.time()
|
||||
status_log = []
|
||||
# 单 worker 串行:便于排查;多 worker 用 Pool 并行
|
||||
|
||||
# ---- Wave scheduling(分批提交)----
|
||||
# 按 CNO 总量分 wave:同 CNO 总量的点组成一个 wave。
|
||||
# 每个 wave 内并行跑(Pool),wave 之间串行(等前一批完成)。
|
||||
# 这样前一个 wave 的成功结果成为后一个 wave 的种子——对高温富金属
|
||||
# 难点至关重要(旧版无序提交时 60K 种子步进几乎全失败)。
|
||||
waves = _split_waves(worker_args, nworkers)
|
||||
print("scheduling: {} wave(s) (batch size={}, points={})".format(
|
||||
len(waves), nworkers, n_total))
|
||||
|
||||
if nworkers <= 1:
|
||||
for wa in worker_args:
|
||||
res = _worker(wa)
|
||||
@ -344,8 +473,12 @@ def main():
|
||||
len(status_log), n_total, res["name"], res["status"]),
|
||||
flush=True)
|
||||
else:
|
||||
for wi, wave in enumerate(waves):
|
||||
if len(waves) > 1:
|
||||
print("--- wave {}/{} ({} points) ---".format(
|
||||
wi + 1, len(waves), len(wave)))
|
||||
with Pool(nworkers) as pool:
|
||||
for res in pool.imap_unordered(_worker, worker_args):
|
||||
for res in pool.imap_unordered(_worker, wave):
|
||||
status_log.append(res)
|
||||
print(" [{}/{}] {} -> {}".format(
|
||||
len(status_log), n_total, res["name"], res["status"]),
|
||||
|
||||
@ -53,28 +53,33 @@ DEFAULT_CHAIN = [
|
||||
#
|
||||
# 关键:不要在 nst 里设 CHMAX 或 ITEK —— 用 tlusty 默认值(CHMAX=0.001,
|
||||
# ITEK=4)。设 CHMAX=0.1(宽松)会让 nc 在真正收敛前就停止,给 nl
|
||||
# 留下一个坏种子导致发散。用默认 CHMAX=0.001 时 nc 能正常收敛
|
||||
# (约 11 次迭代),nl 只需约 1 次迭代即可收敛。
|
||||
# 留下一个坏种子导致发散。用默认 CHMAX=0.001。
|
||||
#
|
||||
# .5 文件中 NFREAD=2000 -> 实际 5088 个频率点(速度快)。不要使用
|
||||
# NFREAD=50(会展开为 77695 个点,慢 15 倍且不稳定)。
|
||||
# .5 文件中 NFREAD=2000 -> 实际 75443 个频率点(tests/sdB_spectra/GUIDE.md
|
||||
# 实测)。不要使用 NFREAD=50(会展开为 77695 个点,慢且不稳定)。
|
||||
#
|
||||
# nc 的 NITER=10 是实测最优(GUIDE.md NITER 扫描结论):
|
||||
# - nc(纯连续谱)缺少谱线约束,外层温度永不真正收敛,只在外层漂移
|
||||
# - NITER=10 vs NITER=50 的最终光谱差异仅 6e-6(完全等价)
|
||||
# - nl(含谱线)会自修正到正确解,无论 nc 给什么初值
|
||||
# - NITER=10 总耗时 ~12 分钟(35000K CNO),NITER=50 浪费 2.2×
|
||||
#
|
||||
# 阶段 1:LTE 灰度大气(T T)。NITER=0。
|
||||
{"label": "lte", "lte": "T", "ltgray": "T", "ilvlin": 0,
|
||||
"require_converged": False, "niter": 0},
|
||||
# 阶段 2:NLTE 连续谱(F F, ilvlin=0)。默认 CHMAX=0.001 强制真正
|
||||
# 收敛。NITER=50 给足迭代余量。
|
||||
# 阶段 2:NLTE 连续谱(F F, ilvlin=0)。NITER=10 已足够给 nl 好种子
|
||||
# (见上)。默认 CHMAX=0.001。
|
||||
{"label": "nc", "lte": "F", "ltgray": "F", "ilvlin": 0,
|
||||
"require_converged": False, "niter": 50},
|
||||
# 阶段 3:含谱线的完整 NLTE(F F, ilvlin=100)。从已收敛的 nc 种子出
|
||||
# 发,约 1 次迭代即可收敛。默认 CHMAX=0.001。
|
||||
"require_converged": False, "niter": 10},
|
||||
# 阶段 3:含谱线的完整 NLTE(F F, ilvlin=100)。从 nc 种子出发,实测
|
||||
# 约 17 次迭代收敛到 CHMAX=0.001。默认 CHMAX=0.001。
|
||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100,
|
||||
"require_converged": True, "niter": 100},
|
||||
]
|
||||
# 若(必须收敛的)NLTE 阶段未收敛,则按下面这些更大的 ITEK 值重试。
|
||||
# 注意:不要用 ITEK=100 —— 对这些模型会发散。
|
||||
ITEK_FALLBACK = [15]
|
||||
# 每次 tlusty 运行的默认最大迭代数(nst 中的 NITER);各阶段可覆盖。
|
||||
# 每次 tlusty 运行的默认最大迭代数(nst 中的 NITER);各阶段可在 chain 内覆盖。
|
||||
DEFAULT_NITER = 50
|
||||
|
||||
|
||||
|
||||
@ -47,8 +47,11 @@ TLUSTY = os.environ.get("TLUSTY", "/home/dckj/program/tlusty/tl208-s54")
|
||||
SEED_STEP_CHAIN = [
|
||||
# 阶段 1:从种子大气热启动 NLTE 连续谱。LTGRAY=F 会读取 fort.8;
|
||||
# 种子的布居数提供了一个物理上接近的初始猜测。
|
||||
# NITER=20:种子步进从物理接近的种子出发,比冷启动收敛快。
|
||||
# 实测 iter~15 即 relc<0.001(见修补日志)。nc 纯连续谱缺少谱线约束,
|
||||
# 外层永不真正收敛(GUIDE.md),nl 会自修正,无需多跑。
|
||||
{"label": "seed_nc", "lte": "F", "ltgray": "F", "ilvlin": 0,
|
||||
"ichang": 0, "require_converged": False, "niter": 80},
|
||||
"ichang": 0, "require_converged": False, "niter": 20},
|
||||
# 阶段 2:从种子收敛的大气出发,进行含谱线的完整 NLTE 计算。
|
||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100,
|
||||
"ichang": 0, "require_converged": True, "niter": 100},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user