- ml/configs/apg.json:batch 128→48。本机实测(d=192 同结构、每进程 19.2 GB 硬帽、1 步即到位)batch=128 无论 plans 取 60 还是 150 都 OOM(崩前已分配 17.85 GiB;d=32 小模型也 17.54 GiB,说明是展开图吃显存而非模型大小);48/plans=150 为 13.08、48/60 为 7.67。展开长度 plans 没动,降 plans 会把长任务绕出段截掉。
- ml/common/vram.py(新):limit() 设每进程帽 VRAM_FRAC 默认 0.78≈18.7 GiB 并打印上限;peak_gib() 供日志。train.py 与 apg_finetune.py 调用,每条记录新增 vram 字段(实测 train batch=1024 → 2.71,APG 自检档 → 0.33)。没帽时 APG 会把整卡顶到 23.6 GB、连桌面一起挤掉。
- ml/monitor.py / monitor.html:状态带 vram_budget_gb(默认 20,VRAM_BUDGET_GB 可改),GPU 显存条按预算算并超线变色,指标卡加「显存峰值」(>14 黄 >17 红)。headless 加载渲染 DOM 复核过。
- ml/configs/smoke.json 补 apg 段(batch=8)+ selftest.sh 的 APG 两项带 --config:原先不带 → 自检第 6 项按正式 batch=128 跑,本机必 OOM。
- ml/selftest.sh:gen_resume 判据由 cmp -s 改为剔除 plan_ms/cert.ms 两个墙钟字段后逐条比(旧判据恒 false);rec() 值非合法 JSON 时记 false 不再静默丢项。
- ml/selftest_proc.py:alive() 读 tasklist 改 text=True+errors='replace' 并防 None(中文 Windows 表头 GBK 会让读线程抛 UnicodeDecodeError→stdout=None→TypeError);wait_pids 带上最后一次异常。
- ml/monitor.py find_bash():便携版 git 的 mingw64 布局要再上一层找 bash,并兜 which('bash')(排除 System32 的 WSL)。本机原先返回 None → 网页「开始训练」起不动、自检进程两项没跑。
- 整轮自检重跑:gen_resume/prep_rerun/train_stop/train_stopfile/apg_resume/tree_killed/survive_parent_exit 全 true,仅 train_term false(timeout 的 TERM 送不到原生 python,rc=124 硬杀);用时 3.5 分钟。
- 文档:新增第七节(显存实测表、自检 8 项结果、四条判据/环境坑、本机改动清单);A-3 改为「实现方已改、待复跑验证」,C-5/C-6 标为已实测,D 组复核后关闭。
76 lines
3.9 KiB
Python
76 lines
3.9 KiB
Python
"""本机自检的进程部分(2026-10-18 第七轮,遗留 C-5:Windows 生命周期从未实测)。由 ml/selftest.sh 调用。
|
||
tree :按控制台同一套标志(Windows: CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW;POSIX: 新会话)起 bash → python + node 两个孙进程,
|
||
再用控制台同一套停止方法(Windows: taskkill /T /F;POSIX: 按会话 TERM)结束,检查孙进程是否全部消失。
|
||
detach :起一个"启动器"进程,它按同一标志拉起子树后立刻退出,检查子树是否还活着(等价于关掉控制台黑窗口后训练是否继续)。
|
||
输出一行 JSON:{"tree_killed": bool, "survive_parent_exit": bool, ...}"""
|
||
import json, os, subprocess, sys, time, tempfile, shutil
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
sys.path.insert(0, ROOT)
|
||
from ml.monitor import find_bash, kill_session # noqa: E402
|
||
|
||
FLAGS = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP | 0x08000000} if os.name == 'nt' else {'start_new_session': True}
|
||
|
||
|
||
def alive(pid):
|
||
if os.name == 'nt':
|
||
# 中文 Windows 的 tasklist 表头是 GBK,text=True 会让读线程解码崩掉(stdout 变 None)→ 按字节读再容错解码
|
||
r = subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'], capture_output=True, text=True, errors='replace')
|
||
return str(pid) in (r.stdout or '')
|
||
try: os.kill(pid, 0); return True
|
||
except OSError: return False
|
||
|
||
|
||
def tree_cmd(d):
|
||
py = sys.executable.replace('\\', '/'); dd = d.replace('\\', '/')
|
||
return (f'"{py}" -c "import os,time;open(\'{dd}/p.pid\',\'w\').write(str(os.getpid()));time.sleep(300)" & '
|
||
f'node -e "require(\'fs\').writeFileSync(\'{dd}/n.pid\',String(process.pid));setInterval(()=>{{}},1000)" & wait')
|
||
|
||
|
||
WAITERR = ['']
|
||
|
||
|
||
def wait_pids(d, t=20):
|
||
for _ in range(t * 4):
|
||
try: return int(open(os.path.join(d, 'p.pid')).read()), int(open(os.path.join(d, 'n.pid')).read())
|
||
except Exception as e: WAITERR[0] = type(e).__name__ + ': ' + str(e)[:90]; time.sleep(0.25)
|
||
return None, None # 起不来时把最后一次异常带进结果,别让 tree_started=false 变成没有原因的失败
|
||
|
||
|
||
def main():
|
||
bash = find_bash(); out = {'os': os.name, 'bash': bash}
|
||
if not bash: out['error'] = '找不到 bash'; print(json.dumps(out, ensure_ascii=False)); return
|
||
# tree
|
||
d = tempfile.mkdtemp(prefix='st_tree_')
|
||
p = subprocess.Popen([bash, '-c', tree_cmd(d)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **FLAGS)
|
||
pp, pn = wait_pids(d)
|
||
out['tree_started'] = pp is not None and pn is not None
|
||
if out['tree_started']:
|
||
if os.name == 'nt': subprocess.run(['taskkill', '/T', '/F', '/PID', str(p.pid)], capture_output=True)
|
||
else: kill_session(p.pid)
|
||
time.sleep(3)
|
||
out['tree_left'] = {'python': alive(pp), 'node': alive(pn)}
|
||
out['tree_killed'] = not any(out['tree_left'].values())
|
||
for x in (pp, pn): # 清场
|
||
if alive(x):
|
||
if os.name == 'nt': subprocess.run(['taskkill', '/F', '/PID', str(x)], capture_output=True)
|
||
else: os.kill(x, 9)
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
# detach:启动器拉起子树后立即退出
|
||
d = tempfile.mkdtemp(prefix='st_det_')
|
||
launcher = ('import subprocess,sys;' f'f={FLAGS!r};' 'subprocess.Popen([sys.argv[1],"-c",sys.argv[2]],stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**f)')
|
||
subprocess.run([sys.executable, '-c', launcher, bash, tree_cmd(d)], timeout=30)
|
||
pp, pn = wait_pids(d)
|
||
time.sleep(3)
|
||
out['survive_parent_exit'] = bool(pp and pn and alive(pp) and alive(pn))
|
||
for x in (pp, pn):
|
||
if x and alive(x):
|
||
if os.name == 'nt': subprocess.run(['taskkill', '/F', '/PID', str(x)], capture_output=True)
|
||
else: os.kill(x, 9)
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
print(json.dumps(out, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|