Metal worker: a class v3 day is built from the pack (memhard.metal: the mixer multiplier, the cache size, the era layout), datasets keyed by (day, class, era); class-v3.mjs --metal runs node 0's miner on the Metal worker (gate G4b) and --genesis-bits
The Swift DatasetContext is the version 2 item construction; a v3 program over it would hash another dataset than the node's (every found refused by the CPU re-check). servePackDataset compiles the pack's memhard.metal and runs igneum_cache_fill and igneum_build as packbench does, releases the cache, and the v3 job path takes the pack program and the pack day of its class and era only (need + error otherwise). The gate script's --metal mode drives igneum-miner --worker <igneum-bench> --prepare-packs <dir> --exit-on-seed-change (the app's shape) on node 0 and reports the PREPARE and prepared lines, need / mismatch / refusal lines, accepted blocks after the switch, the CPU re-check counter and the swap line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
104be2dbcf
commit
8e47f84d17
2 changed files with 163 additions and 20 deletions
|
|
@ -14,7 +14,15 @@
|
|||
// check passes. Never touches the live devnet (26610/26611, 26640/26641, 28640) or the simnet.mjs ports.
|
||||
//
|
||||
// node infra/fast-time/class-v3.mjs [--secs 420] [--activation 150] [--epochs-after 2] [--threads 1]
|
||||
// [--metal <igneum-bench>] [--genesis-bits 0x1f010000]
|
||||
// IGNEUMD and IGNEUM_MINER name the binaries (default: the ca2 fork worktree's target-ca2/release).
|
||||
// --metal: node 0's miner is a real Metal GPU worker (igneum-miner --worker <igneum-bench> --prepare-packs <dir>
|
||||
// --exit-on-seed-change, the app's own shape), so the Metal worker's class v3 path (the prepare line with the pack
|
||||
// directory and the class and era tokens, the pack-built program and day) mines across the boundary (gate G4b);
|
||||
// the report then adds the miner's PREPARE lines, the worker's prepared lines, any need / mismatch / refusal line,
|
||||
// the accepted blocks on each side and the swap time. --genesis-bits raises the CPU difficulty for a GPU miner
|
||||
// (0x1e010000 = 2^24 hashes per block: under a second on an M5 Max; the CPU miners on nodes 1 and 2 then verify
|
||||
// and rarely find).
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
|
|
@ -28,9 +36,11 @@ const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
|||
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||
const TMP = '/tmp/igneum-fast-time-v3';
|
||||
const BASE = 29600, SUFFIX = 960;
|
||||
const GENESIS_BITS = 0x1f010000;
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
|
||||
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
||||
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
||||
const METAL = sflag('metal');
|
||||
const SECS = flag('secs', 420);
|
||||
const ACTIVATION = flag('activation', 150);
|
||||
const EPOCHS_AFTER = flag('epochs-after', 2);
|
||||
|
|
@ -38,7 +48,7 @@ const THREADS = flag('threads', 1);
|
|||
const started = [];
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
for (const b of [IGNEUMD, CPU_MINER, ...(METAL ? [METAL] : [])]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
// The file is merged as TEXT, never through JSON.parse: a `never` height is 18446744073709551615, which JavaScript
|
||||
|
|
@ -95,7 +105,8 @@ async function stopAll() {
|
|||
}
|
||||
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); // a thrown start leaves no node behind
|
||||
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||
const minerName = (i) => (METAL && i === 0) ? 'metal0' : `cpu${i}`;
|
||||
const minerLog = (i) => { try { return readFileSync(`${TMP}/${minerName(i)}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||
|
||||
const t0 = Date.now();
|
||||
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||
|
|
@ -110,8 +121,18 @@ for (const n of nodes) {
|
|||
log(`n0 PoW schedule: ${n0.grepLog(/PoW schedule from the override file/).map(l => l.replace(/^.*?(PoW schedule)/, '$1')).join(' | ') || '(no line)'}`);
|
||||
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
||||
|
||||
// one real CPU miner per node, 1 thread, 2^16 expected hashes per block at genesis
|
||||
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, String(THREADS), String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
|
||||
// one real CPU miner per node, 1 thread, 2^16 expected hashes per block at genesis; with --metal, node 0's miner drives
|
||||
// the Metal worker the way the app does (the pack for every prepared pair under packs/prepare, exit 42/44 on a refusal)
|
||||
const PACKS = `${TMP}/packs/prepare`;
|
||||
mkdirSync(PACKS, { recursive: true });
|
||||
nodes.forEach((n, i) => {
|
||||
if (METAL && i === 0) {
|
||||
miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), 'metal0', '--engine', 'igneum-pow', '--worker', METAL, '--prepare-packs', PACKS, '--exit-on-seed-change', '--payout-label', 'metal0', '--status-secs', '30', '--no-vote'], 'metal0', { IGNEUM_POW_DAY_MS: String(DAY_MS) });
|
||||
log(`n0 miner: Metal worker ${METAL}, packs under ${PACKS}`);
|
||||
} else {
|
||||
miner(CPU_MINER, ['mine', n.grpc, String(THREADS), String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) });
|
||||
}
|
||||
});
|
||||
const pay = devAddress('fast-time-v3');
|
||||
|
||||
const epochs = new Map(); // epoch index -> { class, firstSeenDaa, at }
|
||||
|
|
@ -187,6 +208,29 @@ const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
|||
const counts = dag.map(d => d.blockCount ?? '?');
|
||||
const tips = dag.map(d => (d.tipHashes || []).length);
|
||||
|
||||
// the Metal miner's protocol lines (gate G4b): PREPARE sent (the miner), prepared / need / error (the worker), the swap
|
||||
let metal = null;
|
||||
if (METAL) {
|
||||
const L = minerLog(0);
|
||||
const t = (l) => { const m = /^(\d+\.\d+) /.exec(l); return m ? +m[1] * 1000 : null; };
|
||||
const switchAt = firstV3 ? t0 + firstV3.at * 1000 : null;
|
||||
const prepares = L.filter(l => /PREPARE sent for epoch seed/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const prepared = L.filter(l => /worker: prepared /.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const preparedV3 = prepared.filter(l => / class v3 /.test(l));
|
||||
const need = L.filter(l => /worker: need |^\S+ worker error:.*need /.test(l) || /\bneed [0-9a-f]{64}/.test(l));
|
||||
const mismatch = L.filter(l => /program class mismatch|era seed mismatch|PACK OUT OF DATE|pack .*: program pack/.test(l));
|
||||
const refused = L.filter(l => /exiting with code 4[24]|refused the program pack|prepare-failed/.test(l));
|
||||
const swaps = L.filter(l => /swapped with no pause|compiles inline|worker without prepare support/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const accepted = L.filter(l => /ACCEPTED block/.test(l));
|
||||
const acceptedAfter = switchAt ? accepted.filter(l => (t(l) || 0) >= switchAt).length : 0;
|
||||
const found = L.filter(l => /worker: found |^\S+ found /.test(l)).length;
|
||||
const status = L.filter(l => /miner 'metal0' \[igneum-pow\]/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const lastStatus = status.at(-1) || '';
|
||||
const mismatched = +(/mismatched=(\d+)/.exec(lastStatus)?.[1] ?? 0);
|
||||
const rate = /hash=([\d.]+) MH\/s/.exec(lastStatus)?.[1];
|
||||
metal = { worker: METAL, prepares, prepared, prepared_v3: preparedV3, need: need.length, mismatch_lines: mismatch, refused, swaps, accepted_total: accepted.length, accepted_after_switch: acceptedAfter, found_lines: found, cpu_recheck_mismatched: mismatched, rate_mh_s: rate, last_status: lastStatus };
|
||||
}
|
||||
|
||||
const checks = {
|
||||
switch_line_on_every_node: switchLines.every(Boolean),
|
||||
switch_line_names_the_rounded_epoch: switchLines.every(l => l && l.includes(`active from epoch ${FIRST_V3_EPOCH} `)),
|
||||
|
|
@ -200,6 +244,14 @@ const checks = {
|
|||
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
|
||||
sinks_agree: new Set(sinks).size === 1,
|
||||
block_counts_agree: new Set(counts.map(String)).size === 1,
|
||||
...(METAL ? {
|
||||
metal_prepare_sent_for_v3: metal.prepares.some(l => / class v3 /.test(l)),
|
||||
metal_worker_prepared_v3_pack: metal.prepared_v3.length > 0,
|
||||
metal_no_need_or_mismatch: metal.need === 0 && metal.mismatch_lines.length === 0 && metal.refused.length === 0,
|
||||
metal_accepted_blocks_after_switch: metal.accepted_after_switch > 0,
|
||||
metal_cpu_recheck_clean: metal.cpu_recheck_mismatched === 0,
|
||||
metal_swapped_without_pause: metal.swaps.some(l => /swapped with no pause/.test(l)),
|
||||
} : {}),
|
||||
};
|
||||
const pass = Object.values(checks).every(Boolean);
|
||||
const summary = {
|
||||
|
|
@ -210,10 +262,15 @@ const summary = {
|
|||
programs: programRows, accepted_per_miner: accepted,
|
||||
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
|
||||
rejected_lines: [...rejectedMiner.flat(), ...rejectedNode.flat()].slice(0, 20),
|
||||
sinks, block_counts: counts, tips_per_node: tips, switch_lines: switchLines, samples,
|
||||
sinks, block_counts: counts, tips_per_node: tips, switch_lines: switchLines, samples, metal,
|
||||
};
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
||||
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: blocks ${before.length} before / ${after.length} after the boundary at DAA ${BOUNDARY} (chain ${chainBefore} / ${chainAfter}); programs ${programRows.map(p => `e${p.epoch}:${p.class}:${p.program_id}:${Math.max(...p.ready_ms)}ms`).join(' ')}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} (${checks.sinks_agree ? 'agree' : 'DIFFER'}); block counts ${counts.join('/')}; switch lines ${switchLines.filter(Boolean).length}/3`);
|
||||
if (metal) {
|
||||
log(`METAL: ${metal.prepares.length} PREPARE lines (${metal.prepares.filter(l => / class v3 /.test(l)).length} class v3), ${metal.prepared.length} prepared (${metal.prepared_v3.length} v3), need ${metal.need}, mismatch/refusal lines ${metal.mismatch_lines.length + metal.refused.length}, accepted ${metal.accepted_total} (${metal.accepted_after_switch} after the switch), cpu re-check mismatched ${metal.cpu_recheck_mismatched}, rate ${metal.rate_mh_s} MH/s`);
|
||||
for (const l of [...metal.prepares, ...metal.prepared, ...metal.swaps]) log(` ${l.slice(0, 260)}`);
|
||||
for (const l of [...metal.mismatch_lines, ...metal.refused].slice(0, 10)) log(` BAD ${l.slice(0, 260)}`);
|
||||
}
|
||||
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
|
||||
log(`summary: ${TMP}/summary.json`);
|
||||
await stopAll();
|
||||
|
|
|
|||
|
|
@ -2964,9 +2964,29 @@ func raceProgram(_ gpu: GPU, _ program: ServeProgram, dataset: MTLBuffer, datase
|
|||
|
||||
final class ServeDataset {
|
||||
let dayHex: String
|
||||
let ctx: DatasetContext
|
||||
/// The Swift-side context of a class v2 day (the version 2 item construction); nil for a day built from a pack
|
||||
/// (class v3, Counter ASIC 2.0: the pack's memhard.metal carries the mixer multiplier and the era layout)
|
||||
let ctx: DatasetContext?
|
||||
let buffer: MTLBuffer
|
||||
init(dayHex: String, ctx: DatasetContext, buffer: MTLBuffer) { self.dayHex = dayHex; self.ctx = ctx; self.buffer = buffer }
|
||||
/// Counter ASIC 2.0: the class and era this day's dataset was built for; a v3 program hashes over the dataset of
|
||||
/// its own class and era only (the item construction and the layout differ from v2's)
|
||||
let programClass: String
|
||||
let eraHex: String
|
||||
let cacheFillGPUms: Double
|
||||
let buildGPUms: Double
|
||||
init(dayHex: String, ctx: DatasetContext, buffer: MTLBuffer) {
|
||||
self.dayHex = dayHex; self.ctx = ctx; self.buffer = buffer; self.programClass = "v2"; self.eraHex = ""
|
||||
self.cacheFillGPUms = ctx.cacheFillGPUms; self.buildGPUms = ctx.lastBuildGPUms
|
||||
}
|
||||
init(dayHex: String, packClass: String, eraHex: String, buffer: MTLBuffer, cacheFillGPUms: Double, buildGPUms: Double) {
|
||||
self.dayHex = dayHex; self.ctx = nil; self.buffer = buffer; self.programClass = packClass; self.eraHex = eraHex
|
||||
self.cacheFillGPUms = cacheFillGPUms; self.buildGPUms = buildGPUms
|
||||
}
|
||||
func matches(cls: String, era: String) -> Bool {
|
||||
if cls != "" && cls != programClass { return false }
|
||||
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background).
|
||||
|
|
@ -2978,7 +2998,7 @@ final class ServeStore {
|
|||
var current: (String, String)? = nil
|
||||
|
||||
func program(_ seedHex: String, cls: String = "", era: String = "") -> ServeProgram? { lock.lock(); defer { lock.unlock() }; return programs.first { $0.seedHex == seedHex && $0.matches(cls: cls, era: era) } }
|
||||
func dataset(_ dayHex: String) -> ServeDataset? { lock.lock(); defer { lock.unlock() }; return datasets.first { $0.dayHex == dayHex } }
|
||||
func dataset(_ dayHex: String, cls: String = "", era: String = "") -> ServeDataset? { lock.lock(); defer { lock.unlock() }; return datasets.first { $0.dayHex == dayHex && $0.matches(cls: cls, era: era) } }
|
||||
func counts() -> (Int, Int) { lock.lock(); defer { lock.unlock() }; return (programs.count, datasets.count) }
|
||||
|
||||
/// Adds a program; with more than two resident, the oldest one that is not the current job's goes.
|
||||
|
|
@ -2990,7 +3010,7 @@ final class ServeStore {
|
|||
}
|
||||
func add(_ d: ServeDataset) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
if datasets.contains(where: { $0.dayHex == d.dayHex }) { return }
|
||||
if datasets.contains(where: { $0.dayHex == d.dayHex && $0.programClass == d.programClass && $0.eraHex == d.eraHex }) { return }
|
||||
datasets.append(d)
|
||||
while datasets.count > 2, let i = datasets.firstIndex(where: { $0.dayHex != current?.1 && $0.dayHex != d.dayHex }) { datasets.remove(at: i) }
|
||||
}
|
||||
|
|
@ -3088,6 +3108,66 @@ func serveDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, day: [UInt8],
|
|||
return (sd, ms(t0, nowNs()))
|
||||
}
|
||||
|
||||
// Counter ASIC 2.0: the day's cache and dataset of a class v3 pack, built on the device from the pack's own memhard.metal
|
||||
// (igneum_cache_fill, igneum_build: the mixer multiplier, the cache size of the day under the growth rule and the era's
|
||||
// interleaved layout are all in that text), as packbench.swift builds it; the Swift DatasetContext is the version 2
|
||||
// construction and must never serve a v3 program. The cache buffer is released after the build. program.h gives the
|
||||
// sizes and the identity; a pack with a hot table (IGNEUM_HOT_MB > 0) is refused here (the hot table is not adopted).
|
||||
func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: String, wantClass: String, wantEra: String) throws -> (ServeDataset, Double) {
|
||||
if let d = store.dataset(dayHex, cls: wantClass, era: wantEra) { return (d, 0) }
|
||||
let t0 = nowNs()
|
||||
func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 3, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) }
|
||||
guard let programH = try? String(contentsOfFile: dir + "/program.h", encoding: .utf8) else { throw refuse("cannot read program.h") }
|
||||
func defineU32(_ name: String) -> UInt32? {
|
||||
guard let re = try? NSRegularExpression(pattern: "#define \(name) ([0-9a-fA-Fx]+)"), let m = re.firstMatch(in: programH, range: NSRange(programH.startIndex..., in: programH)) else { return nil }
|
||||
let v = String(programH[Range(m.range(at: 1), in: programH)!]).replacingOccurrences(of: "u", with: "")
|
||||
return v.hasPrefix("0x") ? UInt32(v.dropFirst(2), radix: 16) : UInt32(v)
|
||||
}
|
||||
func defineStr(_ name: String) -> String? {
|
||||
guard let re = try? NSRegularExpression(pattern: "#define \(name) \"([^\"]*)\""), let m = re.firstMatch(in: programH, range: NSRange(programH.startIndex..., in: programH)) else { return nil }
|
||||
return String(programH[Range(m.range(at: 1), in: programH)!])
|
||||
}
|
||||
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
|
||||
let packClass = generator == 3 ? "v3" : "v2"
|
||||
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
|
||||
if let packDay = defineStr("IGNEUM_DAY_BYTES_HEX"), packDay.lowercased() != dayHex.lowercased() { throw refuse("the pack's day is \(packDay), not \(dayHex)") }
|
||||
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass)") }
|
||||
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") }
|
||||
if (defineU32("IGNEUM_HOT_MB") ?? 0) > 0 { throw refuse("a hot-table pack (IGNEUM_HOT_MB) is not served by this worker") }
|
||||
guard (defineU32("IGNEUM_DATASET_MODE") ?? 0) == 1 else { throw refuse("not a memory-hard pack (IGNEUM_DATASET_MODE 1)") }
|
||||
let datasetLog2 = Int(defineU32("IGNEUM_DATASET_LOG2") ?? 28)
|
||||
let cacheLog2 = Int(defineU32("IGNEUM_CACHE_LOG2_WORDS") ?? 26)
|
||||
let cacheSegments = Int(defineU32("IGNEUM_CACHE_SEGMENTS") ?? 65536)
|
||||
guard datasetLog2 >= 20 && datasetLog2 <= 31 && cacheLog2 >= 16 && cacheLog2 <= 30 && cacheSegments % 256 == 0 && cacheSegments > 0 else { throw refuse("program.h sizes out of range") }
|
||||
guard let mhSrc = try? String(contentsOfFile: dir + "/memhard.metal", encoding: .utf8) else { throw refuse("cannot read memhard.metal") }
|
||||
let lib: MTLLibrary
|
||||
do { lib = try gpu.device.makeLibrary(source: mhSrc, options: MTLCompileOptions()) } catch { throw refuse("Metal compile of memhard.metal: \(error)") }
|
||||
guard let fillFn = lib.makeFunction(name: "igneum_cache_fill"), let buildFn = lib.makeFunction(name: "igneum_build") else { throw refuse("memhard.metal lacks igneum_cache_fill or igneum_build") }
|
||||
let fillPipe = try gpu.device.makeComputePipelineState(function: fillFn)
|
||||
let buildPipe = try gpu.device.makeComputePipelineState(function: buildFn)
|
||||
let words = 1 << datasetLog2
|
||||
guard let cache = gpu.device.makeBuffer(length: (1 << cacheLog2) * 4, options: .storageModePrivate) else { throw refuse("cannot allocate the 2^\(cacheLog2) word cache") }
|
||||
guard let dataset = gpu.device.makeBuffer(length: words * 4, options: .storageModePrivate) else { throw refuse("cannot allocate the 2^\(datasetLog2) word dataset") }
|
||||
func run(_ body: (MTLComputeCommandEncoder) -> Void) throws -> Double {
|
||||
let cb = gpu.queue.makeCommandBuffer()!
|
||||
let enc = cb.makeComputeCommandEncoder()!
|
||||
body(enc); enc.endEncoding(); cb.commit(); cb.waitUntilCompleted()
|
||||
if let e = cb.error { throw refuse("command buffer: \(e)") }
|
||||
return (cb.gpuEndTime - cb.gpuStartTime) * 1000
|
||||
}
|
||||
let fillMs = try run { enc in
|
||||
enc.setComputePipelineState(fillPipe); enc.setBuffer(cache, offset: 0, index: 0)
|
||||
enc.dispatchThreadgroups(MTLSize(width: cacheSegments / 256, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 256, height: 1, depth: 1))
|
||||
}
|
||||
let buildMs = try run { enc in
|
||||
enc.setComputePipelineState(buildPipe); enc.setBuffer(cache, offset: 0, index: 0); enc.setBuffer(dataset, offset: 0, index: 1)
|
||||
enc.dispatchThreadgroups(MTLSize(width: words / 16 / 256, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 256, height: 1, depth: 1))
|
||||
}
|
||||
let sd = ServeDataset(dayHex: dayHex, packClass: packClass, eraHex: eraHex, buffer: dataset, cacheFillGPUms: fillMs, buildGPUms: buildMs)
|
||||
store.add(sd)
|
||||
return (sd, ms(t0, nowNs()))
|
||||
}
|
||||
|
||||
func runServe(_ opts: Options) -> Never {
|
||||
let gpu = GPU()
|
||||
let datasetLog2 = opts.datasetLog2
|
||||
|
|
@ -3123,7 +3203,7 @@ func runServe(_ opts: Options) -> Never {
|
|||
emit("prepare-failed \(f[1]) \(f[2]) bad field (epoch_seed 64 hex, day_seed hex)"); continue
|
||||
}
|
||||
let (epochHex, dayHex) = (f[1], f[2])
|
||||
let have = (store.program(epochHex, cls: wantClass, era: wantEra) != nil, store.dataset(dayHex) != nil)
|
||||
let have = (store.program(epochHex, cls: wantClass, era: wantEra) != nil, store.dataset(dayHex, cls: wantClass, era: wantEra) != nil)
|
||||
if have.0 && have.1 { emit("prepared \(epochHex) \(dayHex) 0 program 0 dataset 0 (already resident)"); continue }
|
||||
let packDir = f.count > 3 ? f[3...].joined(separator: " ") : ""
|
||||
if wantClass == "v3" && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class v3 program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue }
|
||||
|
|
@ -3133,7 +3213,10 @@ func runServe(_ opts: Options) -> Never {
|
|||
let (sp, progMs) = wantClass == "v3"
|
||||
? try servePackProgram(gpu, store, seedHex: epochHex, seed: epochSeed, dir: packDir, wantClass: wantClass, wantEra: wantEra)
|
||||
: try serveProgram(gpu, store, seedHex: epochHex, seed: epochSeed, datasetLog2: datasetLog2)
|
||||
let (sd, dsMs) = serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2)
|
||||
// a class v3 day is built from the pack (its construction and layout), a v2 day by the Swift context
|
||||
let (sd, dsMs) = wantClass == "v3"
|
||||
? try servePackDataset(gpu, store, dayHex: dayHex, dir: packDir, wantClass: wantClass, wantEra: wantEra)
|
||||
: serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2)
|
||||
// The race: the prepared program against its own dataset, exclusive windows between jobs
|
||||
var raceMs = 0.0
|
||||
if sp.raceDue && opts.race != "off" {
|
||||
|
|
@ -3142,7 +3225,7 @@ func runServe(_ opts: Options) -> Never {
|
|||
raceMs = ms(r0, nowNs())
|
||||
}
|
||||
let (np, nd) = store.counts()
|
||||
emit("prepared \(epochHex) \(dayHex) \(fmt(ms(t0, nowNs()), 1)) program \(fmt(progMs, 1)) dataset \(fmt(dsMs, 1)) race \(fmt(raceMs, 1)) variant \(sp.compiled.variant) class \(sp.programClass) loads/hash \(sp.loadsPerHash) cache-fill \(fmt(sd.ctx.cacheFillGPUms, 1)) resident \(np) programs \(nd) datasets")
|
||||
emit("prepared \(epochHex) \(dayHex) \(fmt(ms(t0, nowNs()), 1)) program \(fmt(progMs, 1)) dataset \(fmt(dsMs, 1)) race \(fmt(raceMs, 1)) variant \(sp.compiled.variant) class \(sp.programClass) loads/hash \(sp.loadsPerHash) cache-fill \(fmt(sd.cacheFillGPUms, 1)) build \(fmt(sd.buildGPUms, 1)) resident \(np) programs \(nd) datasets")
|
||||
} catch { emit("prepare-failed \(epochHex) \(dayHex) \(wantClass == "v3" ? "" : "Metal compile failed: ")\(error.localizedDescription)") }
|
||||
}
|
||||
continue
|
||||
|
|
@ -3165,22 +3248,25 @@ func runServe(_ opts: Options) -> Never {
|
|||
let program: ServeProgram
|
||||
let dataset: ServeDataset
|
||||
do {
|
||||
if store.program(pair.0, cls: wantClass, era: wantEra) == nil || store.dataset(pair.1) == nil { prepareQueue.sync {} }
|
||||
if store.program(pair.0, cls: wantClass, era: wantEra) == nil || store.dataset(pair.1, cls: wantClass, era: wantEra) == nil { prepareQueue.sync {} }
|
||||
let sp: ServeProgram
|
||||
let sd: ServeDataset
|
||||
if wantClass == "v3" {
|
||||
guard let resident = store.program(pair.0, cls: wantClass, era: wantEra) else {
|
||||
// a v3 job runs on the pack program and the pack-built day of its class and era, both prepared; nothing
|
||||
// is compiled inline (the Swift generator and context are the version 2 construction)
|
||||
guard let resident = store.program(pair.0, cls: wantClass, era: wantEra), let day = store.dataset(pair.1, cls: wantClass, era: wantEra) else {
|
||||
emit("need \(pair.0) \(pair.1)")
|
||||
emit("error \(jobId) pack (metal): program class mismatch: no class v3 program for epoch \(pair.0.prefix(16)) is resident (this worker compiles v3 from a pack); send prepare with a pack directory")
|
||||
emit("error \(jobId) pack (metal): program class mismatch: no class v3 program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds v3 from a pack); send prepare with a pack directory")
|
||||
continue
|
||||
}
|
||||
sp = resident
|
||||
sp = resident; sd = day
|
||||
} else {
|
||||
let (p, progMs) = try serveProgram(gpu, store, seedHex: pair.0, seed: epochSeed, datasetLog2: datasetLog2)
|
||||
if progMs > 0 { emit("info program epoch \(pair.0.prefix(16)) loads/hash \(p.loadsPerHash) compiled inline in \(fmt(progMs, 1)) ms (not prepared)") }
|
||||
sp = p
|
||||
let (d, dsMs) = serveDataset(gpu, store, dayHex: pair.1, day: daySeed, datasetLog2: datasetLog2)
|
||||
if dsMs > 0 { emit("info dataset day \(pair.1) built inline in \(fmt(dsMs, 1)) ms (cache fill \(fmt(d.cacheFillGPUms, 1)) ms, build \(fmt(d.buildGPUms, 1)) ms GPU; not prepared)") }
|
||||
sp = p; sd = d
|
||||
}
|
||||
let (sd, dsMs) = serveDataset(gpu, store, dayHex: pair.1, day: daySeed, datasetLog2: datasetLog2)
|
||||
if dsMs > 0 { emit("info dataset day \(pair.1) built inline in \(fmt(dsMs, 1)) ms (cache fill \(fmt(sd.ctx.cacheFillGPUms, 1)) ms, build \(fmt(sd.ctx.lastBuildGPUms, 1)) ms GPU; not prepared)") }
|
||||
program = sp; dataset = sd
|
||||
} catch { emit("error \(jobId) Metal compile failed: \(error)"); continue }
|
||||
if switched, let prev = lastPair {
|
||||
|
|
|
|||
Loading…
Reference in a new issue