igneum/tools/proving-v1/agg-cost-table.mjs
igneum-labs 7dade79bcb Aggregation cost on the 5090 (5 October, night): the chained aggregation is 2.1 s alone and 9.7 s beside the miner, the batch-log2 curve (2^16 buys 1.6x for a fifth of the hash rate), batch and tree folds estimated, two streams and SP1 knobs closed; the host times the stdin build and names the knobs, --save-shards; the PC 2 job scripts and the readers
The statement and the pinned guests are unchanged; every fixture proof verifies as before. The defaults stay (batch-log2 22, SP1 defaults): the one knob that moves a mining card's prover costs a fifth of the hash rate; the plan carries the trade for the project lead and the batch fold for the next pin. Measured: docs/bench-log.md "aggregation cost on the RTX 5090"; the plan line: docs/plans/proving-v1.md "Aggregation cost (5 October, night)". Also: make-package's gate skips the exporter's .node-plan.json side files and takes the run lock for its execute step; the state-reply class (/api/state answering {} once paid_wei passes u64::MAX) found on the way and fixed on the app branch at 42f36b3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ea38ece9eaa5949dd657cbfea5308c4948177ff8)
2026-10-06 07:04:50 +00:00

50 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
// The aggregation-cost curve from one or more pc2-agg-cost.ps1 jobs (5 October 2026 night): per phase, the shard and
// aggregation seconds per block (the chain and aggregate-only RESULT lines), the GPU utilisation and memory peak of
// the phase, the own miner's rate (MH/s wall) and its batch-log2. Reads the job uploads from the log intake like
// tools/jobs.mjs (DATABASE_URL in ~/.config/igneum/env). Prints a markdown table.
// node tools/proving-v1/agg-cost-table.mjs agg-cost-pc2-1 agg-cost-pc2-2 ...
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
const ids = process.argv.slice(2);
if (!ids.length) { console.error('usage: agg-cost-table.mjs <job id> [...]'); process.exit(2); }
const m = /^DATABASE_URL=(.*)$/m.exec(readFileSync(`${homedir()}/.config/igneum/env`, 'utf8'));
const url = m[1].trim().replace(/^['"]|['"]$/g, '');
const host = new URL(url).hostname.replace('-pooler', '');
const sql = async (query, params = []) => {
const r = await fetch(`https://${host}/sql`, { method: 'POST', headers: { 'Neon-Connection-String': url, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, params }) });
const j = await r.json();
if (!r.ok) throw new Error(j.message || JSON.stringify(j));
return j.rows;
};
const f1 = x => (Math.round(x * 10) / 10).toFixed(1);
const rows = [];
for (const id of ids) {
const ups = await sql('SELECT lines FROM miner_logs WHERE run_id LIKE $1 ORDER BY received_at DESC LIMIT 1', [`job-${id}-%`]);
if (!ups.length) { console.error(`${id}: no upload`); continue; }
const lines = ups[0].lines.split('\n');
const phases = new Map();
const ph = l => { if (!phases.has(l)) phases.set(l, { job: id, label: l, shard: [], agg: [], deferred: [], util: '', mem: '', rate: '', batch: '', note: '' }); return phases.get(l); };
for (const raw of lines) {
const l = raw.replace(/^\d+(\.\d+)? job \S+: /, '');
let x;
if ((x = /^([A-Z0-9]+): RESULT chain block \d+ shard \d+: compressed prove ([\d.]+) s/.exec(l))) ph(x[1]).shard.push(Number(x[2]));
else if ((x = /^([A-Z0-9]+): RESULT (?:chain|aggregate) block \d+: .*?(?:aggregate )?prove ([\d.]+) s \(stdin [\d.]+ s, (\d+) deferred/.exec(l))) { ph(x[1]).agg.push(Number(x[2])); ph(x[1]).deferred.push(Number(x[3])); }
else if ((x = /^RESULT phase_gpu ([A-Z0-9]+) samples=\d+ memory_used_max_mib=(\d+) util_mean_pct=([\d.]+)/.exec(l))) { ph(x[1]).mem = x[2]; ph(x[1]).util = x[3]; }
else if ((x = /^RESULT ([A-Z0-9]+) miner rate (n=\d+ mean=[\d.]+)/.exec(l))) ph(x[1]).rate = x[2].replace('n=', 'n ').replace(' mean=', ', mean ');
else if ((x = /^RESULT ([A-Z0-9]+) own miner started .* batch_log2=(\d+)/.exec(l))) ph(x[1]).batch = x[2];
else if ((x = /^RESULT ([A-Z0-9]+) own miner (not hashing|EXITED|: no app miner)/.exec(l))) ph(x[1]).note = 'own miner ' + x[2];
else if ((x = /^RESULT phase ([A-Z0-9]+) end .* exit (\d+) wall ([\d.]+) s/.exec(l))) { const p = ph(x[1]); p.exit = x[2]; p.wall = x[3]; }
else if ((x = /^RESULT phase ([A-Z0-9]+) skipped/.exec(l))) ph(x[1]).note = 'skipped';
else if ((x = /^RESULT (H (?:choice|knobs)): (.*)$/.exec(l))) console.error(`${id}: ${x[1]}: ${x[2]}`);
}
for (const p of phases.values()) rows.push(p);
}
const mean = a => a.length ? a.reduce((s, v) => s + v, 0) / a.length : NaN;
console.log('| Job | Phase | batch-log2 | Shard s (each) | Aggregation s (each, deferred proofs) | Block s (shard + aggregation, mean) | GPU util % | GPU peak MiB | Miner MH/s | Note |');
console.log('|---|---|---|---|---|---|---|---|---|---|');
for (const p of rows) {
const chained = p.agg.filter((_, i) => p.deferred[i] >= 2);
const block = p.shard.length && p.agg.length ? f1(mean(p.shard) + mean(chained.length ? chained : p.agg)) : '';
console.log(`| ${p.job} | ${p.label} | ${p.batch || (p.label.startsWith('E') && /^E\d+$/.test(p.label) ? p.label.slice(1) : '')} | ${p.shard.map(f1).join(', ')} | ${p.agg.map((a, i) => `${f1(a)} (${p.deferred[i]})`).join(', ')} | ${block} | ${p.util} | ${p.mem} | ${p.rate} | ${[p.note, p.exit && p.exit !== '0' ? `exit ${p.exit}` : ''].filter(Boolean).join('; ')} |`);
}