igneum/tools/reliability/fake-worker.mjs
igneum-labs 23c1127bd3 fake worker: the OpenCL listing carries the real worker's header line, so the engine reads an answered enumeration and removes a card that left
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ecd798747a26d339ff514da04773be858780636c)
2026-10-07 13:12:08 +00:00

106 lines
5.9 KiB
JavaScript
Executable file

#!/usr/bin/env node
// A GPU worker stand-in that speaks the miner's serve protocol (ready / job / done / found / prepare / prepared /
// quit) and misbehaves on command, so the miner's fault guards and the app's watchdog can be measured on a Mac with
// no faulty GPU at hand. It never hashes: every `done` is invented, every `found` is wrong on purpose.
//
// FAKE_WORKER_CTL=<file> the file's first word is the mode, re-read before every job:
// ok every job: `done` after 100 ms with the job's nonce count as hashes (about 167 MH/s at 2^24 nonces)
// slow `done` after 5,000 ms (a self-heal build inside a job, a game on the GPU)
// fast `done` after 0.3 ms with the full count: a runtime answering success while running nothing
// (the 4 October 2026 gfx1036 fault)
// zero `done` after 100 ms with 0 hashes: the worker completes jobs and produces nothing
// silent never answers a job (the queue fills and nothing comes back)
// badfound a `found` with a wrong hash before every `done`: a worker on a wrong program
// preparefail every `prepare` is answered `prepare-failed`; jobs run as in ok
// exit the process exits with code 7 at the next job
// absent `--list` shows no device (the card is unplugged or in a problem code); jobs run as in ok
// selftest the worker prints a self-test failure and exits 3 before ready (MF-4)
// Extra lines in the control file: `devices N` (how many --list shows), `dev N <mode>` (a mode for device N only)
// The mode is read from argv `--mode <m>` when no control file is set. `--serve` is accepted and ignored.
// Lines on stderr are prefixed `fake-worker:` and never match a miner pattern.
import { readFileSync, existsSync, writeSync } from 'node:fs';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
import { createInterface } from 'node:readline';
// `--list` (the engine's OpenCL enumeration on Windows and Linux, src/detect.rs): one fake GPU, or none while the
// control file says `absent` (the hot-plug step: a card that appears later must start without a tap)
if (process.argv.includes('--list')) {
const m = (() => { try { return require('node:fs').readFileSync(process.env.FAKE_WORKER_CTL, 'utf8').trim().split(/\s+/)[0]; } catch { return 'ok'; } })();
const n = m === 'absent' ? 0 : (() => { try { const l = require('node:fs').readFileSync(process.env.FAKE_WORKER_CTL, 'utf8').split('\n').map(x => x.trim().split(/\s+/)).find(f => f[0] === 'devices'); return l ? Number(l[1]) : 1; } catch { return 1; } })();
// the real worker's header line: the engine reads a listing without it as "the tool did not answer" and removes
// no card on it (src/detect.rs parse_opencl_list), which is right for a crashed driver and wrong for a stand-in
process.stdout.write(`OpenCL devices (${n}):\n`);
for (let i = 0; i < n; i++) {
process.stdout.write(`[${i}] Fake GPU ${i + 1} | Fake Platform (OpenCL 1.2)\n GPU, vendor Fake Silicon, driver 1.0.0, OpenCL C 1.2, 8 compute units, 8192 MB\n`);
}
process.exit(0);
}
const ctl = process.env.FAKE_WORKER_CTL;
const argMode = (() => { const i = process.argv.indexOf('--mode'); return i >= 0 ? process.argv[i + 1] : 'ok'; })();
// the device this process serves (`--device N` from the engine's OpenCL path); the control file's first word is the
// mode for every device, a line `dev N <mode>` overrides it for device N, a line `devices N` sets how many --list shows
const device = (() => { const i = process.argv.indexOf('--device'); return i >= 0 ? process.argv[i + 1] : '0'; })();
function mode() {
if (ctl && existsSync(ctl)) {
const lines = readFileSync(ctl, 'utf8').trim().split('\n');
const own = lines.map(l => l.trim().split(/\s+/)).find(f => f[0] === 'dev' && f[1] === device);
if (own && own[2]) return own[2];
const m = (lines[0] || '').trim().split(/\s+/)[0];
if (m) return m;
}
return argMode;
}
function deviceCount() {
try { const l = readFileSync(ctl, 'utf8').split('\n').map(x => x.trim().split(/\s+/)).find(f => f[0] === 'devices'); return l ? Number(l[1]) : 1; } catch { return 1; }
}
// synchronous writes: a piped stdout is asynchronous in Node and process.exit would drop pending lines
const out = (s) => { writeSync(1, s + '\n'); };
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
if (mode() === 'selftest') {
// MF-4: the worker fails its self-test and exits before it is ready (the Arc B580 on PC 1, 7 October 2026)
out('error 0 self-test FAIL: 3 of 96 vectors mismatched (fake worker, device ' + device + ')');
process.exit(3);
}
out('ready fake Fake_GPU dataset-log2 28 batch 16777216 prepare 1');
const queue = [];
let running = false;
async function pump() {
if (running) return;
running = true;
while (queue.length) {
const f = queue.shift();
const [id, , , nonceStart, nonces] = f;
const m = mode();
if (m === 'exit') process.exit(7);
if (m === 'silent') { await sleep(1_000_000); continue; }
const t0 = performance.now();
let hashes = Number(nonces);
let ms = 100;
if (m === 'slow') ms = 5000;
else if (m === 'fast') ms = 0.3;
else if (m === 'zero') hashes = 0;
await sleep(ms);
if (m === 'badfound') out(`found ${id} ${BigInt(nonceStart) + 17n} 0000000000000001`);
out(`done ${id} ${hashes} ${(performance.now() - t0).toFixed(2)}`);
}
running = false;
}
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const f = line.trim().split(/\s+/);
switch (f[0]) {
case 'job': queue.push(f.slice(1)); pump(); break;
case 'prepare':
if (mode() === 'preparefail') out(`prepare-failed ${f[1]} ${f[2]} fake worker refuses`);
else setTimeout(() => out(`prepared ${f[1]} ${f[2]} 50 fake resident 2 programs 2 datasets`), 50);
break;
case 'quit': process.exit(0);
default: break;
}
});
rl.on('close', () => process.exit(0));