第五轮:锁/续跑/超时/自动分支/R4 探针与 encode-once/控制台加固;阶段内断点续跑(gen 按条、prep 按 shard、训练与 APG 定时原子存档+体面停止);分歧以实现方意见为准

This commit is contained in:
Hark
2026-10-17 18:57:48 +00:00
parent e3bb78388d
commit a5e779c515
36 changed files with 1199 additions and 285 deletions
+53 -42
View File
@@ -141,6 +141,8 @@
.form label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--dim); }
.form input, .form select { background: #0f1520; color: var(--text); border: 1px solid var(--line2); border-radius: 8px; padding: 7px 9px; font: 13px var(--mono); }
.form .full { grid-column: 1 / -1; }
.ckline { margin-top: 12px; font-size: 12px; color: var(--dim); } .ckline.bad { color: var(--bad); } .ckline .warn { color: var(--warn); }
.demobar { background: #fbbf2418; border: 1px solid #fbbf2455; color: var(--warn); border-radius: 10px; padding: 8px 12px; margin: 10px 0; font-size: 13px; }
.dlg .acts { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.dlg .warnbox { margin-top: 12px; font-size: 12px; color: var(--warn); background: #fbbf2410; border: 1px solid #fbbf2433; border-radius: 8px; padding: 8px 10px; display: none; }
.toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #1b2433; border: 1px solid var(--line2); padding: 9px 16px; border-radius: 10px; font-size: 13px; z-index: 50; display: none; box-shadow: 0 10px 30px #0008; }
@@ -190,16 +192,16 @@
<div class="toast" id="toast"></div>
<div class="modal" id="modal"><div class="dlg">
<h3>开始训练</h3>
<div class="d">后台执行 <code>bash ml/pipeline.sh all</code>,从起始阶段一路跑到结束阶段。网页可以关,训练不受影响;关闭控制台窗口会结束训练。</div>
<div class="d">后台执行 <code>bash ml/pipeline.sh all</code>,从对拍一路跑到验收。已完成的步骤自动跳过;训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
<div class="form">
<label>起始阶段<select id="fFrom"></select></label>
<label>结束阶段<select id="fTo"></select></label>
<label>动作头 CFG<select id="fCfg"><option value="ml/configs/base.json">MeanFlow(base)</option><option value="ml/configs/wta.json">WTA 回退(wta)</option></select></label>
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="fresh">从头重跑(旧产物移到 ml/archive/)</option></select></label>
<label>动作头 CFG<select id="fCfg"><option value="ml/configs/base.json">MeanFlow(base,M5 不过自动转 WTA)</option><option value="ml/configs/wta.json">WTA(wta)</option><option value="ml/configs/small.json">小模型 d=128(R4 不过时用)</option></select></label>
<label>共识过滤 CONSENSUS<select id="fCons"><option value="">按老师门禁自动</option><option value="0">关</option><option value="1">开(生成约慢 5 倍)</option></select></label>
<label>任务数 N_TASKS<input id="fN" value="50000"></label>
<label>生成进程数 NPROC<input id="fP" placeholder="默认 = CPU 核数"></label>
<label>目标样本 TARGET_SAMPLES<input id="fT" value="1700000"></label>
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
<label>老师间隙下限 MIN_GAP_MM<input id="fG" value="3"></label>
</div>
<div class="warnbox" id="fWarn"></div>
<div class="acts"><button id="fCancel">取消</button><button id="fGo" class="primary">▶ 开始</button></div>
@@ -336,12 +338,20 @@ const thin = (pts, n = 400) => { if (pts.length <= n) return pts; const k = pts.
/* ===================== 状态 ===================== */
let cur = null, recs = [], evals = [], offset = 0, timer = null, charts = null, lastMtime = 0, lastStatus = null, runsCache = [];
let logyOn = false, job = null, userPicked = false, conOff = 0;
/* 阶段表:名称、说明、粗估耗时(小时,RTX 3090 + 28 核;只用于全流程百分比的权重) */
const STAGE_META = [
['setup', '安装依赖', 0.15], ['kin', '对拍', 0.02], ['gate1', '老师门禁', 0.45], ['gen', '生成数据', 1.0], ['prep', '打包样本', 0.4],
['train', '预训练', 3.0], ['m5', 'M5 门禁', 0.25], ['apg', 'APG 微调', 3.0], ['mine', '挖错循环', 9.0], ['export', '导出 ONNX', 0.02],
['bench', '部署评测', 1.0], ['accept', '验收汇总', 0.25]];
const SM = Object.fromEntries(STAGE_META.map(([k, n, w]) => [k, { n, w }]));
/* 阶段表:名称、粗估耗时(小时,作全流程百分比权重)、超时上限。
2026-10-18 第五轮(审查 N-14):真实模式下由 /api/status 的 stage_meta 下发(来源 ml/pstate.py STAGES,与超时同源);
下面这份只给示例数据用,数值与 pstate.py 一致。 */
let STAGE_META = [
['setup', '安装依赖', 0.2], ['kin', '对拍', 0.2], ['r4', '推理延迟', 0.2], ['gate1', '一致性门禁', 0.5], ['gen', '生成老师轨迹', 5.0],
['prep', '打包样本', 0.5], ['train', '预训练', 3.0], ['m5', '头门禁', 0.5], ['apg', 'APG 微调', 4.0], ['mine', '挖错循环', 9.0],
['export', '导出', 0.1], ['bench', '部署评测', 1.0], ['accept', '验收汇总', 0.5]];
let SM = Object.fromEntries(STAGE_META.map(([k, n, w]) => [k, { n, w, to: Math.max(1, 3 * w) }]));
function applyStageMeta(m) {
if (!m || !m.length) return;
STAGE_META = m.map(x => [x.id, x.name, x.est_h]); SM = Object.fromEntries(m.map(x => [x.id, { n: x.name, w: x.est_h, to: x.timeout_h }]));
}
/* 开始 / 停止请求带自定义头:跨站网页发不出这种请求(会触发 CORS 预检,控制台不应答),见 monitor.py 审查 N-8 */
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'X-Console': '1', 'Content-Type': 'application/json' }, body: body || '{}' }).then(x => x.json()).catch(e => ({ error: String(e) }));
const smoothv = () => +$('smooth').value;
const ema = (pts, a) => { if (!a) return pts; let m = null; return pts.map(([x, y]) => { if (!isNum(y)) return [x, y]; m = m == null ? y : a * m + (1 - a) * y; return [x, m]; }); };
const xOf = r => r.step ?? r.it ?? 0;
@@ -454,7 +464,7 @@ async function loadRuns() {
async function poll() {
try {
lastStatus = await api('/api/status');
lastStatus = await api('/api/status'); applyStageMeta(lastStatus.stage_meta);
job = await api('/api/job').catch(() => null);
await loadRuns();
if (cur) {
@@ -467,7 +477,7 @@ async function poll() {
render();
}
const P = lastStatus.pipeline, busy = (job && job.running) || (P && P.current);
if (DEMO) setPill('demo', '示例数据');
if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').insertAdjacentHTML('beforebegin', `<div class="demobar" id="demobar">这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>`); }
else if (busy) setPill('live', `运行中 · ${P && P.current ? SM[P.current]?.n || P.current : '启动中'}`);
else if (cur && Date.now() / 1000 - lastMtime < 120) setPill('live', `运行中 · ${ago(lastMtime)}更新`);
else setPill('', '空闲');
@@ -577,9 +587,14 @@ function renderStatus(s) {
/* ===================== 全流程卡片 / 开始停止 / 终端 ===================== */
function renderPipe(s) {
const P = s.pipeline, running = job && job.running;
const btns = `<div class="ctlrow">${running || (P && P.current)
? `<button class="danger" onclick="stopJob()">■ 停止</button><span class="hint">${running ? `PID ${job.pid} · 从「${SM[job.from]?.n}」到「${SM[job.to]?.n}」` : '由终端启动,需在终端里结束'}</span>`
: `<button class="primary" onclick="openStart()">▶ ${P ? '继续 / 重新开始' : '开始训练'}</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出`) : '点击后选择起止阶段与参数'}</span>`}</div>`;
const ck = s.check || {}, lk = s.lock;
const ckline = ck.pending ? '<div class="ckline">环境自检中…</div>'
: ck.problems && ck.problems.length ? `<div class="ckline bad">✕ ${ck.problems.join(';')}</div>`
: ck.python ? `<div class="ckline">Python ${ck.python} · torch ${ck.torch}${ck.gpu ? ' · ' + ck.gpu : ''}${ck.warn ? ` · <span class="warn">${ck.warn}</span>` : ''}</div>` : '';
const who = job && job.external ? `由${(lk && lk.owner || '').startsWith('网页') ? '上一个控制台窗口' : '终端'}启动(pid ${lk && lk.pid},${Math.round(lk && lk.age_s || 0)} 秒前心跳)` : job && job.pid ? `PID ${job.pid}` : '';
const btns = `${ckline}<div class="ctlrow">${running || (P && P.current)
? `<button class="danger" onclick="stopJob()">■ 停止</button><span class="hint">${who}</span>`
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止,再点即从停下的那步继续' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出;修好后再点即续跑`) : '已完成的步骤会自动跳过'}</span>`}</div>`;
if (!P) { $('pipe').innerHTML = `<h2><span>全流程</span><span class="r">尚未开始</span></h2>${empty('还没运行过流水线。点「开始训练」从对拍一直跑到验收;中途每一步的进度都显示在这里。')}${btns}`; return; }
const plan = P.plan && P.plan.length ? P.plan : Object.keys(P.stages);
let tot = 0, got = 0;
@@ -592,7 +607,7 @@ function renderPipe(s) {
let remain = 0; for (const k of plan) { const r = P.stages[k]; if (!r || r.rc == null) remain += (SM[k]?.w || 0.1) * (k === cur ? 1 - (pr.frac || 0) : 1); }
const head = cur ? `<div class="now">正在运行<b>${SM[cur]?.n || cur}</b>${pr.label || ''}</div>`
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${plan.length} 步</div>`
: failed ? `<div class="now">状态<b class="bad">停在「${SM[failed].n}」</b>${P.stages[failed].rc === -15 || P.stages[failed].rc === 143 ? '已手动停止' : '失败,看终端输出'}</div>`
: failed ? `<div class="now">状态<b class="bad">停在「${SM[failed]?.n || failed}」</b>${(P.stages[failed].info || {}).timed_out || P.stages[failed].rc === 124 ? '超过时间上限,判定卡死' : P.stages[failed].rc === -15 || P.stages[failed].rc === 143 ? '已停止' : '失败,看终端输出'}</div>`
: `<div class="now">状态<b>未在运行</b>已完成 ${doneN} / ${plan.length} 步</div>`;
$('pipe').innerHTML = `<h2><span>全流程</span><span class="r">${doneN} / ${plan.length} 步完成</span></h2>
<div class="overall">${head}<div class="bigpct">${(100 * frac).toFixed(1)}<small>%</small></div></div>
@@ -602,7 +617,7 @@ function renderPipe(s) {
<div class="facts">
<div class="fact"><div class="k">总用时</div><div class="v">${hms((cur ? now : Math.max(...Object.values(P.stages).map(r => r.end || r.start))) - P.started)}</div></div>
<div class="fact"><div class="k">本步用时</div><div class="v">${cr ? hms(now - cr.start) : '—'}</div></div>
<div class="fact"><div class="k">本步粗估</div><div class="v">${cur ? hmsShort((SM[cur]?.w || 0) * 3600) : '—'}</div></div>
<div class="fact"><div class="k">本步粗估 / 上限</div><div class="v">${cur ? hmsShort((SM[cur]?.w || 0) * 3600) + ' / ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600) : '—'}</div></div>
<div class="fact"><div class="k">全流程剩余(粗估)</div><div class="v">${allDone ? '—' : '约 ' + hmsShort(remain * 3600)}</div></div>
</div>${btns}`;
}
@@ -624,31 +639,27 @@ async function pollConsole() {
}
function toast(msg) { const t = $('toast'); t.textContent = msg; t.style.display = 'block'; clearTimeout(toast.h); toast.h = setTimeout(() => t.style.display = 'none', 3500); }
function openStart() {
const P = lastStatus && lastStatus.pipeline, keys = STAGE_META.map(x => x[0]);
const opt = keys.map(k => `<option value="${k}">${SM[k].n}(${k})</option>`).join('');
$('fFrom').innerHTML = opt; $('fTo').innerHTML = opt;
// 默认从第一个还没成功完成的阶段开始,到验收结束
let from = 'kin';
if (P) { const k = keys.slice(1).find(k => !(P.stages[k] && P.stages[k].rc === 0)); if (k) from = k; }
$('fFrom').value = from; $('fTo').value = 'accept';
const w = $('fWarn'); const upd = () => { const f = $('fFrom').value; const msgs = [];
if (P && P.stages[f] && P.stages[f].rc === 0) msgs.push(`「${SM[f].n}」上次已成功完成,重跑会覆盖它的产物。`);
if (keys.indexOf(f) > keys.indexOf('gen') && !(lastStatus.data && lastStatus.data.stats)) msgs.push('还没有生成数据,建议从「老师门禁」或「生成数据」开始。');
if (keys.indexOf(f) > keys.indexOf($('fTo').value)) msgs.push('起始阶段在结束阶段之后。');
const P = lastStatus && lastStatus.pipeline, ck = lastStatus && lastStatus.check;
if (ck && ck.problems && ck.problems.length) { toast('环境自检未通过:' + ck.problems.join(';')); return; }
const w = $('fWarn'); const upd = () => { const msgs = [];
const done = P ? (P.plan || []).filter(k => P.stages[k] && P.stages[k].rc === 0) : [];
if ($('fMode').value === 'resume' && done.length) msgs.push(`将跳过已完成的 ${done.length} 步:${done.map(k => SM[k]?.n || k).join('、')}。改了参数会自动改为从头重跑。`);
if ($('fMode').value === 'fresh' && P) msgs.push('从头重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。');
w.style.display = msgs.length ? 'block' : 'none'; w.innerHTML = msgs.join('<br>'); };
$('fFrom').onchange = upd; $('fTo').onchange = upd; upd();
$('fMode').onchange = upd; upd();
$('modal').classList.add('show');
}
async function startJob() {
const env = { CFG: $('fCfg').value, CONSENSUS: $('fCons').value, N_TASKS: $('fN').value.trim(), NPROC: $('fP').value.trim(), TARGET_SAMPLES: $('fT').value.trim(), ROUNDS: $('fR').value.trim() };
const body = JSON.stringify({ from: $('fFrom').value, to: $('fTo').value, env });
const r = DEMO ? await api('/api/start', body) : await fetch('/api/start', { method: 'POST', body }).then(x => x.json()).catch(e => ({ error: String(e) }));
const env = { CFG: $('fCfg').value, CONSENSUS: $('fCons').value, N_TASKS: $('fN').value.trim(), NPROC: $('fP').value.trim(), TARGET_SAMPLES: $('fT').value.trim(), ROUNDS: $('fR').value.trim(), MIN_GAP_MM: $('fG').value.trim() };
const body = JSON.stringify({ fresh: $('fMode').value === 'fresh', env });
const r = DEMO ? await api('/api/start', body) : await post('/api/start', body);
if (r.error) { toast('没能开始:' + r.error); return; }
$('modal').classList.remove('show'); toast('已开始'); conOff = 0; userPicked = false; poll();
}
async function stopJob() {
if (!confirm('确定停止?正在运行的这一步会中断,已完成的步骤保留,之后可以从这一步继续。')) return;
const r = DEMO ? await api('/api/stop', '{}') : await fetch('/api/stop', { method: 'POST' }).then(x => x.json()).catch(e => ({ error: String(e) }));
if (!confirm('确定停止?训练会先存档再退出(最多等 90 秒),生成/打包按已写完的部分保留。之后点「续跑」从断点接着来。')) return;
toast('正在停止…训练阶段会先存档');
const r = DEMO ? await api('/api/stop', '{}') : await post('/api/stop');
toast(r.error ? '没能停止:' + r.error : '已停止'); poll();
}
$('btnStart').onclick = openStart; $('btnStop').onclick = stopJob;
@@ -704,10 +715,10 @@ function demoApi() {
const mkPipe = (done, cur, curStartH) => { const t = now(); const st = {}; let tt = t - curStartH * 3600;
for (let i = done.length - 1; i >= 0; i--) { const [k, dur] = done[i]; st[k] = { start: tt - dur * 3600, end: tt, rc: 0, info: {} }; tt -= dur * 3600; }
if (cur) st[cur] = { start: t - curStartH * 3600, end: null, rc: null, info: {} };
return { started: tt, plan: ['kin', 'gate1', 'gen', 'prep', 'train', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, current: cur }; };
return { started: tt, plan: ['kin', 'r4', 'gate1', 'gen', 'prep', 'train', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, current: cur }; };
const CON = {
train: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== node ml/eval/dump_parity.cjs 2000\n== python -m ml.eval.check_parity\n{"PASS": true, "fk_mm": 0.0004, "gap_mm": 0.0011}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n样本数 1702000 ≥ 目标,完成\n== bash ml/prep/run_prep.sh 28\n== python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json\n{"pairs": 250, "label_std_mean": 0.071, "pass": true}\n== python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval\nstep 2000 loss 1.912 6150 samples/s\n== python -m ml.train --samples ml/data/samples --out ml/runs/base\n',
gen: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== python -m ml.eval.check_parity\n{"PASS": true}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n',
train: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== node ml/eval/dump_parity.cjs 2000\n== python -m ml.eval.check_parity\n{"PASS": true, "fk_mm": 0.0004, "gap_mm": 0.0011}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n样本数 1702000 ≥ 目标,完成\n== bash ml/prep/run_prep.sh 28\n== python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json\n{"pairs": 250, "label_std_mean": 0.071, "pass": true}\n== python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval\nstep 2000 loss 1.912 6150 samples/s\n== python -m ml.train --samples ml/data/samples --out ml/runs/base\n',
gen: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== python -m ml.eval.check_parity\n{"PASS": true}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n',
late: '... 挖错第 2 轮\nround 2 gain 1.1 pp\n== python -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed 1003 --n 50000 --out ml/runs/mine/r3\n{"n": 50000, "E1": 0.009, "E2": 0.019, "ok": 0.938}\n== python -m ml.apg.apg_finetune --init ml/runs/current.pt --out ml/runs/apg_r3 --steps 1000\n', idle: '' };
const GPU = () => [{ name: 'NVIDIA GeForce RTX 3090', util: 93 + Math.round(5 * rnd()), mem_used: 15872 + Math.round(500 * rnd()), mem_total: 24576, temp: 71 + Math.round(3 * rnd()), power: 318 + Math.round(24 * rnd()) }];
const DATA = { kin_pass: true, consistency: 0.86, gen_tasks_written: 56412, shards: 56,
@@ -716,12 +727,12 @@ function demoApi() {
function setup(name) {
sc = name; seed = 7; con = CON[name] || ''; jobS = { running: name !== 'idle', pid: 21844, from: 'kin', to: 'accept', started: now() - 5 * 3600, rc: null, bash: true }; genAt = 31200;
if (name === 'idle') { runs = {}; logs = {}; pipe = null; jobS = { running: false, bash: true }; return; }
if (name === 'gen') { runs = {}; logs = {}; pipe = mkPipe([['kin', 0.02], ['gate1', 0.47]], 'gen', 2.7); pipe.stages.gen.info.target_tasks = 50008; return; }
if (name === 'train') { pipe = mkPipe([['kin', 0.02], ['gate1', 0.47], ['gen', 4.6], ['prep', 0.38]], 'train', 2.15);
if (name === 'gen') { runs = {}; logs = {}; pipe = mkPipe([['kin', 0.02], ['r4', 0.05], ['gate1', 0.47]], 'gen', 2.7); pipe.stages.gen.info.target_tasks = 50008; return; }
if (name === 'train') { pipe = mkPipe([['kin', 0.02], ['r4', 0.05], ['gate1', 0.47], ['gen', 4.6], ['prep', 0.38]], 'train', 2.15);
runs = { base: { kind: 'train', at: 36000, total: 60000, sps: 6208, t0: T0, live: true },
bench_base: { kind: 'bench', at: 2000, total: 2000, sps: 6150, t0: T0 - 700 } };
logs = { base: trainLog(60000, 0, 36000, 6208, T0), bench_base: trainLog(2000, 0, 2000, 6150, T0 - 700, 0) };
} else { pipe = mkPipe([['kin', 0.02], ['gate1', 0.47], ['gen', 4.6], ['prep', 0.38], ['train', 2.9], ['m5', 0.2], ['apg', 2.6]], 'mine', 3.4); pipe.stages.mine.info.rounds = 6;
} else { pipe = mkPipe([['kin', 0.02], ['r4', 0.05], ['gate1', 0.47], ['gen', 4.6], ['prep', 0.38], ['train', 2.9], ['m5', 0.2], ['apg', 2.6]], 'mine', 3.4); pipe.stages.mine.info.rounds = 6;
pipe.stages.export = pipe.stages.bench = pipe.stages.accept = undefined; delete pipe.stages.export; delete pipe.stages.bench; delete pipe.stages.accept;
const t1 = now() - 9 * 3600;
runs = { base: { kind: 'train', at: 60000, total: 60000, sps: 6208, t0: t1 - 3 * 3600 },
@@ -779,8 +790,8 @@ function demoApi() {
if (u.pathname === '/api/status') return status();
if (u.pathname === '/api/job') return jobS;
if (u.pathname === '/api/console') { const off = +u.searchParams.get('offset'); return { text: con.slice(off), offset: con.length, reset: off === 0 || off > con.length }; }
if (u.pathname === '/api/start') { const b = JSON.parse(body || '{}'); jobS = { running: true, pid: 23001, from: b.from, to: b.to, started: now(), rc: null, bash: true };
con = `$ FROM=${b.from} TO=${b.to} bash ml/pipeline.sh all\n(示例数据:真实使用时这里是命令行输出)\n`;
if (u.pathname === '/api/start') { const b = JSON.parse(body || '{}'); b.from = b.from || 'kin'; b.to = b.to || 'accept'; jobS = { running: true, pid: 23001, from: b.from, to: b.to, started: now(), rc: null, bash: true };
con = `$ ${b.fresh ? 'FRESH=1 ' : ''}bash ml/pipeline.sh all\n(示例数据:真实使用时这里是命令行输出)\n`;
if (!pipe) pipe = mkPipe([], b.from, 0.001); else { pipe.current = b.from; pipe.stages[b.from] = { start: now(), end: null, rc: null, info: {} }; }
return { ok: true, pid: 23001 }; }
if (u.pathname === '/api/stop') { jobS = { ...jobS, running: false, rc: -15, stopped: true };