From 8e47f84d1742902022fda8775ba2aa3886218a9f Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:25:28 +0000 Subject: [PATCH] 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 --prepare-packs --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 --- infra/fast-time/class-v3.mjs | 69 +++++++++++++++++++-- proto-metal/main.swift | 114 ++++++++++++++++++++++++++++++----- 2 files changed, 163 insertions(+), 20 deletions(-) diff --git a/infra/fast-time/class-v3.mjs b/infra/fast-time/class-v3.mjs index ca0af9df..3a82a3ea 100644 --- a/infra/fast-time/class-v3.mjs +++ b/infra/fast-time/class-v3.mjs @@ -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 ] [--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 --prepare-packs +// --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(); diff --git a/proto-metal/main.swift b/proto-metal/main.swift index 09e52fa4..085dbc74 100644 --- a/proto-metal/main.swift +++ b/proto-metal/main.swift @@ -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 )"); 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 {