class v5 kits: every worker host uploads the pack's state leaves for igneum_build (the NVRTC worker, the OpenCL host for AMD, Intel and Apple, the Metal worker), checked against the pack's count and FNV first, freed after the build; a v5 pack without its leaves builds nothing

packfile.h reads IGNEUM_STATE_LEAVES, IGNEUM_STATE_LEAVES_FNV64, the file name, root and block (generator 5 = class v5; a generator 5 pack without the count, or another class with one, is refused) and pf_load_leaves reads and checks leaves.bin; the loader test carries the class v5 cases known-failed first (no count, no file, a short file, one flipped bit, a v4 pack with a count) and the checked-in v5-dn3-epoch0 pack. The CUDA driver gains cuMemcpyHtoD; the emulation takes both igneum_build shapes (weak overloads) and refuses a shape mismatch. The OpenCL host sets the build arguments in one place for both shapes, uploads the leaves in --bench-pack, --serve and the compiled-in bench (the host reference derives words from the same leaves). The Metal worker's servePackDataset binds buffers 2 and 3 as packbench.swift does and self-tests the dataset head and last word against vectors.json. tools/class-v5: kits-remote.sh (the kit build and tests on igneum-build-1, the zip to /srv/artefacts/packs), fleet-cuda-v5-bench.sh (the fleet card), pc1-amd-v5-bench.ps1 (PC 1's 9070 XT through the coordinator).

Measured on the Mac under the measure lock, 7 October 2026: v5-dn3-epoch0 fingerprint 82b19cbde8557ea5 on Metal (packbench, M5 Max, 18:34:47Z, 14.163 MH/s GPU time) and on Apple OpenCL (--bench-pack, 18:34:59Z, 15.265 MH/s wall; the compiled-in bench's dataset self-test PASS at 18:34:54Z), cache FNV 7334fa46e5d972eb PASS, vectors 3 of 3 on both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 18:36:17 +00:00
parent 6fdd357434
commit 8bf1c4f43d
11 changed files with 624 additions and 46 deletions

View file

@ -23,6 +23,7 @@ struct Drv {
decltype(&cuMemAlloc_v2) memAlloc = nullptr;
decltype(&cuMemFree_v2) memFree = nullptr;
decltype(&cuMemcpyDtoH_v2) memcpyDtoH = nullptr;
decltype(&cuMemcpyHtoD_v2) memcpyHtoD = nullptr; // class v5: the state leaves go up for igneum_build (7 October 2026)
decltype(&cuModuleLoadData) moduleLoadData = nullptr;
decltype(&cuModuleUnload) moduleUnload = nullptr;
decltype(&cuModuleGetFunction) moduleGetFunction = nullptr;

View file

@ -24,20 +24,28 @@
#include "../cuda_api.h" // the real cuda.h / nvrtc.h types
#include "cuda_runtime.h" // the shim (proto-cuda/emu): emu_launch and the kernel symbols' world
// igneum_build has two shapes: (ds, cache, nItems) for classes v2 to v4 and (ds, cache, leaves, nLeaves, nItems) for class v5
// (7 October 2026). A pack defines one of them; both are declared weak so the one the pack lacks is a null symbol, and
// d_launch picks the shape by the argument count the worker passed (5 for a class v5 pack).
#define EMU_WEAK __attribute__((weak))
namespace emu_pack_a {
struct IgneumInitWords { uint32_t w[8]; }; // the same definition as the pack's kernel_bound.cu, inside its namespace
void igneum_cache_fill(uint32_t* cache, uint32_t nSegments);
void igneum_build(uint32_t* ds, const uint32_t* cache, uint32_t nItems);
EMU_WEAK void igneum_build(uint32_t* ds, const uint32_t* cache, uint32_t nItems);
EMU_WEAK void igneum_build(uint32_t* ds, const uint32_t* cache, const uint32_t* leaves, uint32_t nLeaves, uint32_t nItems);
void igneum_hash_bound(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw);
}
#ifdef IGNEUM_EMU_TWO_PACKS
namespace emu_pack_b {
struct IgneumInitWords { uint32_t w[8]; };
void igneum_cache_fill(uint32_t* cache, uint32_t nSegments);
void igneum_build(uint32_t* ds, const uint32_t* cache, uint32_t nItems);
EMU_WEAK void igneum_build(uint32_t* ds, const uint32_t* cache, uint32_t nItems);
EMU_WEAK void igneum_build(uint32_t* ds, const uint32_t* cache, const uint32_t* leaves, uint32_t nLeaves, uint32_t nItems);
void igneum_hash_bound(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw);
}
#endif
typedef void (*EmuBuild3)(uint32_t*, const uint32_t*, uint32_t);
typedef void (*EmuBuild5)(uint32_t*, const uint32_t*, const uint32_t*, uint32_t, uint32_t);
static std::string readAll(const std::string& path) {
FILE* f = std::fopen(path.c_str(), "rb");
@ -198,6 +206,7 @@ static CUresult d_memInfo(size_t* f, size_t* t) { *f = 8ull << 30; *t = 16ull <<
static CUresult d_alloc(CUdeviceptr* p, size_t n) { void* m = std::malloc(n); if (!m) return CUDA_ERROR_OUT_OF_MEMORY; *p = (CUdeviceptr)(uintptr_t)m; return CUDA_SUCCESS; }
static CUresult d_free(CUdeviceptr p) { std::free((void*)(uintptr_t)p); return CUDA_SUCCESS; }
static CUresult d_dtoh(void* dst, CUdeviceptr src, size_t n) { std::memcpy(dst, (const void*)(uintptr_t)src, n); return CUDA_SUCCESS; }
static CUresult d_htod(CUdeviceptr dst, const void* src, size_t n) { std::memcpy((void*)(uintptr_t)dst, src, n); return CUDA_SUCCESS; }
static CUresult d_modLoad(CUmodule* m, const void* img) {
const char* s = (const char*)img;
if (std::strncmp(s, "EMU-IMAGE:", 10) != 0) return CUDA_ERROR_INVALID_IMAGE;
@ -222,11 +231,28 @@ template <class T> static T* dptr(void** params, int i) { return (T*)(uintptr_t)
static CUresult d_launch(CUfunction f, unsigned gx, unsigned, unsigned, unsigned bx, unsigned, unsigned, unsigned, CUstream, void** params, void**) {
switch ((int)(uintptr_t)f) {
case 1: emu_launch(emu_pack_a::igneum_cache_fill, gx, bx, dptr<uint32_t>(params, 0), arg<uint32_t>(params, 1)); return CUDA_SUCCESS;
case 2: emu_launch(emu_pack_a::igneum_build, gx, bx, dptr<uint32_t>(params, 0), dptr<const uint32_t>(params, 1), arg<uint32_t>(params, 2)); return CUDA_SUCCESS;
case 2: {
// the worker passes 5 arguments for a class v5 pack (params[3] is the leaf count, params[4] the item count) and 3 otherwise;
// the pack's kernel.cu defines exactly one shape: a mismatch (a v5 pack built without its leaves, or leaves handed to a
// v4 kernel) is the known-failed case and fails the launch instead of running the wrong kernel
EmuBuild5 b5 = (EmuBuild5)emu_pack_a::igneum_build; EmuBuild3 b3 = (EmuBuild3)emu_pack_a::igneum_build;
bool five = params[3] != nullptr && params[4] != nullptr;
if (five && b5) { emu_launch(b5, gx, bx, dptr<uint32_t>(params, 0), dptr<const uint32_t>(params, 1), dptr<const uint32_t>(params, 2), arg<uint32_t>(params, 3), arg<uint32_t>(params, 4)); return CUDA_SUCCESS; }
if (!five && b3) { emu_launch(b3, gx, bx, dptr<uint32_t>(params, 0), dptr<const uint32_t>(params, 1), arg<uint32_t>(params, 2)); return CUDA_SUCCESS; }
std::fprintf(stderr, "emu: igneum_build called with %d arguments but pack A's kernel has the %s shape\n", five ? 5 : 3, b5 ? "class v5 (leaves)" : "class v2 to v4");
return CUDA_ERROR_INVALID_VALUE;
}
case 3: emu_launch(emu_pack_a::igneum_hash_bound, gx, bx, dptr<const uint32_t>(params, 0), dptr<uint64_t>(params, 1), arg<uint32_t>(params, 2), arg<uint32_t>(params, 3), arg<emu_pack_a::IgneumInitWords>(params, 4)); return CUDA_SUCCESS;
#ifdef IGNEUM_EMU_TWO_PACKS
case 4: emu_launch(emu_pack_b::igneum_cache_fill, gx, bx, dptr<uint32_t>(params, 0), arg<uint32_t>(params, 1)); return CUDA_SUCCESS;
case 5: emu_launch(emu_pack_b::igneum_build, gx, bx, dptr<uint32_t>(params, 0), dptr<const uint32_t>(params, 1), arg<uint32_t>(params, 2)); return CUDA_SUCCESS;
case 5: {
EmuBuild5 b5 = (EmuBuild5)emu_pack_b::igneum_build; EmuBuild3 b3 = (EmuBuild3)emu_pack_b::igneum_build;
bool five = params[3] != nullptr && params[4] != nullptr;
if (five && b5) { emu_launch(b5, gx, bx, dptr<uint32_t>(params, 0), dptr<const uint32_t>(params, 1), dptr<const uint32_t>(params, 2), arg<uint32_t>(params, 3), arg<uint32_t>(params, 4)); return CUDA_SUCCESS; }
if (!five && b3) { emu_launch(b3, gx, bx, dptr<uint32_t>(params, 0), dptr<const uint32_t>(params, 1), arg<uint32_t>(params, 2)); return CUDA_SUCCESS; }
std::fprintf(stderr, "emu: igneum_build called with %d arguments but pack B's kernel has the %s shape\n", five ? 5 : 3, b5 ? "class v5 (leaves)" : "class v2 to v4");
return CUDA_ERROR_INVALID_VALUE;
}
case 6: emu_launch(emu_pack_b::igneum_hash_bound, gx, bx, dptr<const uint32_t>(params, 0), dptr<uint64_t>(params, 1), arg<uint32_t>(params, 2), arg<uint32_t>(params, 3), arg<emu_pack_b::IgneumInitWords>(params, 4)); return CUDA_SUCCESS;
#endif
default: return CUDA_ERROR_INVALID_HANDLE;
@ -244,7 +270,7 @@ void emu_fill_driver(Drv& d) {
d.init = d_init; d.driverGetVersion = d_driverVersion; d.deviceGetCount = d_count; d.deviceGet = d_get; d.deviceGetName = d_name;
d.deviceGetAttribute = d_attr; d.deviceTotalMem = d_totalMem; d.primaryCtxSetFlags = d_ctxFlags; d.primaryCtxRetain = d_ctxRetain;
d.primaryCtxRelease = d_ctxRelease; d.ctxSetCurrent = d_ctxSet; d.ctxSynchronize = d_ctxSync; d.memGetInfo = d_memInfo;
d.memAlloc = d_alloc; d.memFree = d_free; d.memcpyDtoH = d_dtoh; d.moduleLoadData = d_modLoad; d.moduleUnload = d_modUnload;
d.memAlloc = d_alloc; d.memFree = d_free; d.memcpyDtoH = d_dtoh; d.memcpyHtoD = d_htod; d.moduleLoadData = d_modLoad; d.moduleUnload = d_modUnload;
d.moduleGetFunction = d_getFn; d.launchKernel = d_launch; d.streamCreate = d_streamCreate; d.streamSynchronize = d_streamSync;
d.streamDestroy = d_streamDestroy; d.funcGetAttribute = d_funcAttr; d.occupancy = d_occ; d.getErrorString = d_errStr; d.getErrorName = d_errName;
}

View file

@ -172,6 +172,72 @@ int main(int argc, char** argv) {
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "generator 1 is not") != NULL, "a pack with no generator line (generator 1) is refused");
}
// Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): the state leaves. Known-failed first: a generator 5
// pack without the leaf count, one whose leaves file is missing, one whose file has another FNV, one of another class
// carrying a count; then the checked-in v5 pack (argv[2]) loads and its leaves load with the pack's FNV.
{
char sw[200], kw[200], text[2600], why[256], path[1024];
uint32_t* leaves = NULL;
size_t bytes = 0;
FILE* f;
int i;
words_hex(bare, sw); words_hex(keyw, kw);
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 5\n#define IGNEUM_PROGRAM_CLASS \"v5\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_SHADOW_REPS 27\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "without IGNEUM_STATE_LEAVES") != NULL, "known-failed: a generator 5 pack without the leaf count is refused in plain words");
// the count and the FNV of two leaves of 64 bytes (0x00..0x3f, 0x40..0x7f), the file not yet written
{
uint8_t raw[128];
uint64_t fnv;
for (i = 0; i < 128; ++i) raw[i] = (uint8_t)i;
fnv = pf_fnv1a64(raw, sizeof(raw));
snprintf(text + strlen(text), sizeof(text) - strlen(text), "#define IGNEUM_STATE_LEAVES 2\n#define IGNEUM_STATE_LEAVES_FNV64 0x%016llxull\n#define IGNEUM_STATE_ROOT_HEX \"%s\"\n", (unsigned long long)fnv, EPOCH_33);
write_file(dir, "program.h", text);
snprintf(path, sizeof(path), "%s/leaves.bin", dir); remove(path);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && pk.stateLeaves == 2 && pk.stateLeavesFnv == fnv && strcmp(pk.programClass, "v5") == 0 && strcmp(pk.stateLeavesFile, "leaves.bin") == 0, "a generator 5 pack with the leaf count loads as class v5 (count, FNV and file name read back)");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v5", EPOCH_33, why, sizeof(why)) == 1, "the v5 pack matches a job naming class v5 and its era");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_33, why, sizeof(why)) == 0 && strstr(why, "program class mismatch") == why, "a job naming class v4 refuses the v5 pack");
err[0] = 0;
CHECK(pf_load_leaves(dir, &pk, &leaves, &bytes, err, sizeof(err)) == 0 && leaves == NULL && strstr(err, "without its leaves file") != NULL, "known-failed: the leaves file missing refuses the build in plain words");
f = fopen(path, "wb"); fwrite(raw, 1, 64, f); fclose(f);
err[0] = 0;
CHECK(pf_load_leaves(dir, &pk, &leaves, &bytes, err, sizeof(err)) == 0 && leaves == NULL && strstr(err, "is 64 bytes, the pack says 2 leaves of 64") != NULL, "known-failed: a short leaves file is refused with both sizes named");
raw[5] ^= 0x80;
f = fopen(path, "wb"); fwrite(raw, 1, 128, f); fclose(f);
raw[5] ^= 0x80;
err[0] = 0;
CHECK(pf_load_leaves(dir, &pk, &leaves, &bytes, err, sizeof(err)) == 0 && leaves == NULL && strstr(err, "is not the pack's IGNEUM_STATE_LEAVES_FNV64") != NULL, "known-failed: leaves of another root (one bit) are refused by the FNV");
f = fopen(path, "wb"); fwrite(raw, 1, 128, f); fclose(f);
err[0] = 0;
CHECK(pf_load_leaves(dir, &pk, &leaves, &bytes, err, sizeof(err)) == 1 && leaves != NULL && bytes == 128 && memcmp(leaves, raw, 128) == 0, "known-good: the right leaves load as 2 x 16 words");
free(leaves); leaves = NULL;
remove(path);
}
// a class v4 pack carrying a leaf count is a mis-stamped export
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_STATE_LEAVES 2\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "state leaves belong to class v5") != NULL, "known-failed: a generator 4 pack carrying IGNEUM_STATE_LEAVES is refused");
// a class v4 pack without leaves loads with stateLeaves 0 and pf_load_leaves hands back nothing
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && pk.stateLeaves == 0 && pf_load_leaves(dir, &pk, &leaves, &bytes, err, sizeof(err)) == 1 && leaves == NULL && bytes == 0, "a class v4 pack has no leaves and the leaf loader hands back none");
if (argc > 2) {
// the checked-in class v5 pack (proto-cuda/packs-ca3-v5/v5-dn3-epoch0): loads, its leaves load under the pack's FNV
err[0] = 0;
CHECK(pf_load(argv[2], &pk, err, sizeof(err)) == 1 && strcmp(pk.programClass, "v5") == 0 && pk.generator == 5 && pk.stateLeaves > 0 && pk.haveVectors, "known-good: the checked-in class v5 pack loads with its leaf count and vectors");
if (err[0]) printf(" %s\n", err);
err[0] = 0;
CHECK(pf_load_leaves(argv[2], &pk, &leaves, &bytes, err, sizeof(err)) == 1 && leaves != NULL && bytes == (size_t)pk.stateLeaves * 64u, "known-good: the checked-in pack's leaves.bin loads under the pack's FNV-1a 64");
if (err[0]) printf(" %s\n", err);
printf(" v5 pack: %u leaves, FNV-1a 64 %016llx, state root %s\n", (unsigned)pk.stateLeaves, (unsigned long long)pk.stateLeavesFnv, pk.stateRootHex);
free(leaves); leaves = NULL;
}
}
printf("%s: %d failure(s)\n", argv[0], failures);
return failures ? 1 : 0;
}

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# The pack loader's seed rule (packfile.h) on a known-good and a known-mismatched pack: emu/packfile-test.c, C99,
# no GPU. Runs on the Mac in a second and in CI. Usage: emu/packfile-test.sh
# The pack loader's seed rule (packfile.h) on a known-good and a known-mismatched pack, and the class v5 leaf rule on the
# checked-in v5 pack (7 October 2026): emu/packfile-test.c, C99, no GPU. Runs on the Mac in a second and in CI. Usage: emu/packfile-test.sh
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../../.." && pwd)"
@ -8,4 +8,4 @@ OUT="${TMPDIR:-/tmp}/igneum-packfile-test"
mkdir -p "$OUT"
CC="${CC:-cc}"
"$CC" -std=c99 -Wall -Wextra -Wno-unused-function -O1 -o "$OUT/packfile-test" "$HERE/packfile-test.c"
"$OUT/packfile-test" "$ROOT/proto-cuda/packs/igneum-devnet-v4-epoch0"
"$OUT/packfile-test" "$ROOT/proto-cuda/packs/igneum-devnet-v4-epoch0" "$ROOT/proto-cuda/packs-ca3-v5/v5-dn3-epoch0"

View file

@ -31,6 +31,15 @@ typedef struct {
char loadClass[64];
char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2", "v3" (Counter ASIC 2.0) or "v4" (Counter ASIC 3.0); absent = the generator's class */
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 or v4 chain pack; empty otherwise */
/* Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): the window's state leaves. leaves.bin beside program.h
* holds IGNEUM_STATE_LEAVES leaves of 16 little-endian words (64 B each), leaf(t) = leaves[t mod stateLeaves]; every item
* of the dataset is keyed by one, so a host that has not uploaded them builds nothing (pf_load_leaves). stateLeaves is 0
* for every other class; a generator 5 pack without them, or a pack of another class with them, is refused by pf_load. */
uint32_t stateLeaves;
uint64_t stateLeavesFnv; /* IGNEUM_STATE_LEAVES_FNV64: FNV-1a 64 over leaves.bin's bytes */
char stateLeavesFile[64]; /* IGNEUM_STATE_LEAVES_FILE, "leaves.bin" when absent */
char stateRootHex[65]; /* IGNEUM_STATE_ROOT_HEX: the state root the leaves hash under */
char stateBlockHex[65]; /* IGNEUM_STATE_BLOCK_HEX: the window's reference chain block */
// Counter ASIC 2.0 (5 October 2026): the mixer multiplier of the item derivation (IGNEUM_MIXER_MULT, 1 when absent:
// version 2; 4 under class v3). The emitted memhard.h / kernel.cl carry it in their text; this is for the log lines.
uint32_t mixerMult;
@ -117,6 +126,15 @@ static int pf_define_u32(const char* text, const char* name, uint32_t* out) {
return 1;
}
/* A 64-bit define (`0x...ull`), the pack's FNV lines in program.h. */
static int pf_define_u64(const char* text, const char* name, uint64_t* out) {
const char* p = pf_find_define(text, name);
const char* e;
if (!p) return 0;
while (*p == ' ' || *p == '\t') ++p;
return pf_number(p, out, &e);
}
static int pf_define_str(const char* text, const char* name, char* out, size_t cap) {
const char* p = pf_find_define(text, name);
const char* q;
@ -286,12 +304,14 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
/* Spec 01 section 1.4.5: a pack whose generator version is not one this worker runs is refused. Generator 2 is
* program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0), generator 4
* is class v4 (Counter ASIC 3.0, 6 October 2026: class v3 plus the latency-shadow block, emitted in the pack's own
* kernel text, so this loader needs nothing new beyond the number and the class token). */
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4) {
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3 or 4)", (unsigned)pk->generator);
* kernel text, so this loader needs nothing new beyond the number and the class token), generator 5 is class v5
* (proof of stored state, 7 October 2026: class v4 over a dataset keyed by the window's state leaves, leaves.bin,
* which the host uploads for igneum_build; the kernel text carries the leaf read). */
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4 && pk->generator != 5) {
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3, 4 or 5)", (unsigned)pk->generator);
free(prog); return pf_fail(err, cap, m);
}
strcpy(pk->programClass, pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
strcpy(pk->programClass, pk->generator == 5 ? "v5" : pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
{
/* Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4. A generator 3 pack with IGNEUM_SHADOW_INSTRS
* is a v4 program stamped as v3 (the old export path; it carried the v3 control's program id) and is refused;
@ -299,6 +319,7 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
uint32_t shadow = 0;
if (!pf_define_u32(prog, "IGNEUM_SHADOW_INSTRS", &shadow)) shadow = 0;
if (pk->generator == 4 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack"); }
if (pk->generator == 5 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack (class v5 is class v4 over the state leaves)"); }
if (pk->generator == 3 && shadow != 0) { /* a generator 2 pack with a shadow is the measurement ladder (a class-bearing id) and loads */
char m[220]; snprintf(m, sizeof(m), "program pack generator %u with a shadow block (IGNEUM_SHADOW_INSTRS %u): a class v4 program is generator 4 (export the pack as class v4)", (unsigned)pk->generator, (unsigned)shadow);
free(prog); return pf_fail(err, cap, m);
@ -312,6 +333,24 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
}
}
pk->eraHex[0] = 0; pf_define_str(prog, "IGNEUM_ERA_SEED_HEX", pk->eraHex, sizeof(pk->eraHex));
/* Class v5: the state leaves. The count, the FNV and the file name come from program.h; the bytes are read by
* pf_load_leaves when the host builds. A v5 pack without the count (or with 0) is refused: nothing could key its items;
* a pack of another class carrying a count is a mis-stamped export and is refused too. */
pk->stateLeaves = 0; pk->stateLeavesFnv = 0; pk->stateRootHex[0] = 0; pk->stateBlockHex[0] = 0;
strcpy(pk->stateLeavesFile, "leaves.bin");
if (!pf_define_u32(prog, "IGNEUM_STATE_LEAVES", &pk->stateLeaves)) pk->stateLeaves = 0;
if (pk->generator == 5 && pk->stateLeaves == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)"); }
if (pk->generator != 5 && pk->stateLeaves != 0) {
char m[200]; snprintf(m, sizeof(m), "program pack generator %u carries IGNEUM_STATE_LEAVES %u: state leaves belong to class v5 (generator 5)", (unsigned)pk->generator, (unsigned)pk->stateLeaves);
free(prog); return pf_fail(err, cap, m);
}
if (pk->stateLeaves) {
if (pk->stateLeaves > 0x02000000u) { free(prog); return pf_fail(err, cap, "program.h IGNEUM_STATE_LEAVES is above 2^25 (the designed dataset's item cap)"); }
if (!pf_define_u64(prog, "IGNEUM_STATE_LEAVES_FNV64", &pk->stateLeavesFnv)) { free(prog); return pf_fail(err, cap, "program pack class v5 without IGNEUM_STATE_LEAVES_FNV64: the leaves cannot be checked"); }
pf_define_str(prog, "IGNEUM_STATE_LEAVES_FILE", pk->stateLeavesFile, sizeof(pk->stateLeavesFile));
pf_define_str(prog, "IGNEUM_STATE_ROOT_HEX", pk->stateRootHex, sizeof(pk->stateRootHex));
pf_define_str(prog, "IGNEUM_STATE_BLOCK_HEX", pk->stateBlockHex, sizeof(pk->stateBlockHex));
}
if (!pf_define_u32(prog, "IGNEUM_PROGRAM_ATTEMPT", &pk->attempt)) pk->attempt = 0;
if (pf_define_words(prog, "IGNEUM_SEEDW_INIT", pk->seedw, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_SEEDW_INIT with 8 words"); }
pk->loadsPerHash = 128; pf_define_u32(prog, "IGNEUM_LOADS_PER_HASH", &pk->loadsPerHash);
@ -405,6 +444,31 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
return 1;
}
/* Class v5: reads the pack's leaves file into a malloc'd buffer of stateLeaves x 64 bytes (16 little-endian words per leaf,
* the layout igneum_build reads), checked against the pack's count (the byte length) and its FNV-1a 64
* (IGNEUM_STATE_LEAVES_FNV64) before anything is uploaded. Returns 1 with *out and *bytes set (NULL and 0 for a pack of
* another class, which has no leaves); 0 with err and nothing allocated. The caller frees *out after the build. */
static int pf_load_leaves(const char* dir, const PfPack* pk, uint32_t** out, size_t* bytes, char* err, size_t cap) {
char* raw;
size_t n = 0;
uint64_t fnv;
*out = NULL; *bytes = 0;
if (pk->stateLeaves == 0) return 1;
raw = pf_read_pack_file(dir, pk->stateLeavesFile, &n);
if (!raw) { char m[600]; snprintf(m, sizeof(m), "class v5 pack without its leaves file %.400s/%.60s (the state leaves key every item: nothing can be built without them)", dir, pk->stateLeavesFile); return pf_fail(err, cap, m); }
if (n != (size_t)pk->stateLeaves * 64u) {
char m[300]; snprintf(m, sizeof(m), "%.60s is %llu bytes, the pack says %u leaves of 64 (%llu bytes)", pk->stateLeavesFile, (unsigned long long)n, (unsigned)pk->stateLeaves, (unsigned long long)pk->stateLeaves * 64ull);
free(raw); return pf_fail(err, cap, m);
}
fnv = pf_fnv1a64(raw, n);
if (fnv != pk->stateLeavesFnv) {
char m[300]; snprintf(m, sizeof(m), "%.60s FNV-1a 64 %016llx is not the pack's IGNEUM_STATE_LEAVES_FNV64 %016llx (the leaves are of another state root; export the pack again)", pk->stateLeavesFile, (unsigned long long)fnv, (unsigned long long)pk->stateLeavesFnv);
free(raw); return pf_fail(err, cap, m);
}
*out = (uint32_t*)raw; *bytes = n;
return 1;
}
// The self-test verdict from values the host read back from the device. `vec` holds vecWarps x 32 outputs of the
// bound kernel run with the pack's own seed words as init words (that is igneum_hash of kernel.cu). Writes one line.
// A hot-table pack (pk->hotMb) also hands the hot table's head, last line and FNV-1a 64 (NULL and 0 otherwise); a
@ -443,7 +507,7 @@ static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32
}
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3|v4>` and `era=<hex>` tokens (sent
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3|v4|v5>` and `era=<hex>` tokens (sent
* only when the chain is on class v3 or v4, so every v2 line is the line of before). A pack matches the line when its
* class is the named class and, when an era is named, its era seed is that era (every class after v2 carries one).
* Empty wanted strings accept any pack. Returns 1 on a match, else 0 with the reason in `why`. */

View file

@ -166,6 +166,7 @@ static bool loadDriver(Drv& d, std::string& err, std::string& libName) {
LOAD_SYM(d, memAlloc, "cuMemAlloc_v2");
LOAD_SYM(d, memFree, "cuMemFree_v2");
LOAD_SYM(d, memcpyDtoH, "cuMemcpyDtoH_v2");
LOAD_SYM(d, memcpyHtoD, "cuMemcpyHtoD_v2");
LOAD_SYM(d, moduleLoadData, "cuModuleLoadData");
LOAD_SYM(d, moduleUnload, "cuModuleUnload");
LOAD_SYM(d, moduleGetFunction, "cuModuleGetFunction");
@ -438,6 +439,8 @@ struct Pair {
// pack's igneum_hot_fill, the argument after the init words
uint32_t hotMb = 0, hotWords = 0, hotSegments = 0, hotSlots = 0;
CUfunction fHotFill = nullptr;
// class v5 (7 October 2026): the pack's state leaves, uploaded for igneum_build and freed after it (0 for other classes)
uint32_t stateLeaves = 0;
CUdeviceptr hot = 0;
double hotMs = 0;
};
@ -841,7 +844,14 @@ static Pair* buildPair(Ctx& c, const std::string& dir, CUstream s, std::string&
}
p->loadClass = pk.loadClass; p->loadsPerHash = pk.loadsPerHash; p->bytesPerHash = pk.bytesPerHash; p->scratchOps = pk.scratchOps;
p->programClass = pk.programClass; p->eraHex = pk.eraHex;
p->stateLeaves = pk.stateLeaves;
p->persistent = pk.persistent != 0;
// Class v5 (docs/design/class-v5-stored-state.md): the window's leaves (leaves.bin), read and checked against the pack's
// count and FNV-1a 64 before anything is allocated; uploaded for igneum_build below and freed right after it, so device
// memory while hashing is the class v4 worker's. A v5 pack without its leaves builds nothing (the known-failed case).
uint32_t* hLeaves = nullptr;
size_t leavesBytes = 0;
{ char lerr[700]; if (!pf_load_leaves(dir.c_str(), &pk, &hLeaves, &leavesBytes, lerr, sizeof(lerr))) { err = "pack " + dir + ": " + lerr; releasePair(c, p); return nullptr; } }
p->hotMb = pk.hotMb; p->hotWords = pk.hotWords; p->hotSegments = pk.hotSegments; p->hotSlots = pk.hotSlots;
p->residentWarps = p->blocksPerSM * c.blockWarps * c.sms;
size_t scratchBytes = 0, hotBytes = (size_t)pk.hotWords * 4u;
@ -860,14 +870,14 @@ static Pair* buildPair(Ctx& c, const std::string& dir, CUstream s, std::string&
size_t cacheBytes = (size_t)p->cacheWords * 4u, dsBytes = (size_t)p->words * 4u;
{
size_t freeB = 0, totalB = 0;
if (c.drv.memGetInfo(&freeB, &totalB) == CUDA_SUCCESS && freeB < cacheBytes + dsBytes + scratchBytes + hotBytes + (64u << 20)) {
err = fmt("%llu MiB free on the device, this pack needs %llu MiB (cache %llu + dataset %llu + scratch %llu + hot %llu)", (unsigned long long)(freeB >> 20), (unsigned long long)((cacheBytes + dsBytes + scratchBytes + hotBytes) >> 20), (unsigned long long)(cacheBytes >> 20), (unsigned long long)(dsBytes >> 20), (unsigned long long)(scratchBytes >> 20), (unsigned long long)(hotBytes >> 20));
releasePair(c, p); return nullptr;
if (c.drv.memGetInfo(&freeB, &totalB) == CUDA_SUCCESS && freeB < cacheBytes + dsBytes + scratchBytes + hotBytes + leavesBytes + (64u << 20)) {
err = fmt("%llu MiB free on the device, this pack needs %llu MiB (cache %llu + dataset %llu + scratch %llu + hot %llu + leaves %llu)", (unsigned long long)(freeB >> 20), (unsigned long long)((cacheBytes + dsBytes + scratchBytes + hotBytes + leavesBytes) >> 20), (unsigned long long)(cacheBytes >> 20), (unsigned long long)(dsBytes >> 20), (unsigned long long)(scratchBytes >> 20), (unsigned long long)(hotBytes >> 20), (unsigned long long)(leavesBytes >> 20));
std::free(hLeaves); releasePair(c, p); return nullptr;
}
}
if (p->persistent) {
CUresult r = c.drv.memAlloc(&p->scratch, scratchBytes);
if (r != CUDA_SUCCESS) { err = "cuMemAlloc scratch: " + c.err(r); p->scratch = 0; releasePair(c, p); return nullptr; }
if (r != CUDA_SUCCESS) { err = "cuMemAlloc scratch: " + c.err(r); p->scratch = 0; std::free(hLeaves); releasePair(c, p); return nullptr; }
p->scratchBytes = scratchBytes;
int after = 0;
c.drv.occupancy(&after, p->fHashBound, 32 * c.blockWarps, 0);
@ -876,24 +886,41 @@ static Pair* buildPair(Ctx& c, const std::string& dir, CUstream s, std::string&
}
{
CUresult r = c.drv.memAlloc(&p->cache, cacheBytes);
if (r != CUDA_SUCCESS) { err = "cuMemAlloc cache: " + c.err(r); p->cache = 0; releasePair(c, p); return nullptr; }
if (r != CUDA_SUCCESS) { err = "cuMemAlloc cache: " + c.err(r); p->cache = 0; std::free(hLeaves); releasePair(c, p); return nullptr; }
uint32_t nSeg = p->cacheSegments, block = 256u, grid = (nSeg + block - 1u) / block;
void* args[2] = { &p->cache, &nSeg };
r = c.drv.launchKernel(p->fCacheFill, grid, 1, 1, block, 1, 1, 0, s, args, nullptr);
if (r == CUDA_SUCCESS) r = c.drv.streamSynchronize(s);
if (r != CUDA_SUCCESS) { err = "cache fill: " + c.err(r); releasePair(c, p); return nullptr; }
if (r != CUDA_SUCCESS) { err = "cache fill: " + c.err(r); std::free(hLeaves); releasePair(c, p); return nullptr; }
}
p->cacheMs = wallMs() - t0;
// Dataset
t0 = wallMs();
{
CUresult r = c.drv.memAlloc(&p->ds, dsBytes);
if (r != CUDA_SUCCESS) { err = "cuMemAlloc dataset: " + c.err(r); p->ds = 0; releasePair(c, p); return nullptr; }
if (r != CUDA_SUCCESS) { err = "cuMemAlloc dataset: " + c.err(r); p->ds = 0; std::free(hLeaves); releasePair(c, p); return nullptr; }
uint32_t nItems = p->words / 16u, block = 256u, grid = (nItems + block - 1u) / block;
void* args[3] = { &p->ds, &p->cache, &nItems };
r = c.drv.launchKernel(p->fBuild, grid, 1, 1, block, 1, 1, 0, s, args, nullptr);
if (r == CUDA_SUCCESS) r = c.drv.streamSynchronize(s);
if (r != CUDA_SUCCESS) { err = "dataset build: " + c.err(r); releasePair(c, p); return nullptr; }
if (hLeaves) {
// class v5: igneum_build(ds, cache, leaves, nLeaves, nItems), the leaf buffer freed once the build has run
CUdeviceptr dLeaves = 0;
uint32_t nLeaves = pk.stateLeaves;
r = c.drv.memAlloc(&dLeaves, leavesBytes);
if (r != CUDA_SUCCESS) { err = "cuMemAlloc state leaves: " + c.err(r); std::free(hLeaves); releasePair(c, p); return nullptr; }
r = c.drv.memcpyHtoD(dLeaves, hLeaves, leavesBytes);
std::free(hLeaves); hLeaves = nullptr;
if (r == CUDA_SUCCESS) {
void* args[5] = { &p->ds, &p->cache, &dLeaves, &nLeaves, &nItems };
r = c.drv.launchKernel(p->fBuild, grid, 1, 1, block, 1, 1, 0, s, args, nullptr);
}
if (r == CUDA_SUCCESS) r = c.drv.streamSynchronize(s);
c.drv.memFree(dLeaves);
if (r != CUDA_SUCCESS) { err = "dataset build (class v5, " + std::to_string(nLeaves) + " state leaves): " + c.err(r); releasePair(c, p); return nullptr; }
} else {
void* args[5] = { &p->ds, &p->cache, &nItems, nullptr, nullptr }; // five slots: the driver reads the kernel's three, the emulation reads the shape from the two null tails
r = c.drv.launchKernel(p->fBuild, grid, 1, 1, block, 1, 1, 0, s, args, nullptr);
if (r == CUDA_SUCCESS) r = c.drv.streamSynchronize(s);
if (r != CUDA_SUCCESS) { err = "dataset build: " + c.err(r); releasePair(c, p); return nullptr; }
}
}
p->dsMs = wallMs() - t0;
// Hot table (hot-table experiment): filled from the epoch seed by the pack's own kernel, never shipped
@ -958,7 +985,8 @@ static Pair* buildPair(Ctx& c, const std::string& dir, CUstream s, std::string&
}
static std::string pairSummary(const Pair* p) {
return fmt("nvrtc %.0f cache %.0f dataset %.0f hot %.0f check %.0f race %.0f ms variant %s; %s", p->compileMs, p->cacheMs, p->dsMs, p->hotMs, p->checkMs, p->raceMs, p->variant.c_str(), p->check.c_str());
return fmt("nvrtc %.0f cache %.0f dataset %.0f hot %.0f check %.0f race %.0f ms variant %s class %s%s; %s", p->compileMs, p->cacheMs, p->dsMs, p->hotMs, p->checkMs, p->raceMs, p->variant.c_str(), p->programClass.c_str(),
p->stateLeaves ? fmt(" (state leaves %u, uploaded for the build and freed)", p->stateLeaves).c_str() : "", p->check.c_str());
}
// ---------------------------------------------------------------------------------------------

View file

@ -2992,9 +2992,9 @@ final class ServeDataset {
/// Counter ASIC 2.0 and 3.0: the classes this worker runs from a prepared pack only (never from the Swift version 2
/// generator): class v3 (generator 3) and class v4 (generator 4, class v3 plus the latency-shadow block, which is in
/// the pack's own program_bound.metal). Each carries an era seed.
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" }
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" || cls == "v5" } // class v5 (7 October 2026): class v4 over the state leaves, from a pack
/// The class name of a pack's IGNEUM_GENERATOR (packfile.h's rule).
func packClassOf(generator: UInt32) -> String { generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
func packClassOf(generator: UInt32) -> String { generator == 5 ? "v5" : generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
// The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background).
final class ServeStore {
@ -3085,13 +3085,17 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U
}
func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) }
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
guard generator == 2 || generator == 3 || generator == 4 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3 or 4)") }
guard generator == 2 || generator == 3 || generator == 4 || generator == 5 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3, 4 or 5)") }
let packClass = packClassOf(generator: generator)
if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") }
// Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4 (packfile.h's rule): a generator 3 pack with
// IGNEUM_SHADOW_INSTRS is a v4 program stamped v3 (the v3 control's program id) and a generator 4 pack without it is no v4 pack
let shadow = defineU32("IGNEUM_SHADOW_INSTRS") ?? 0
if generator == 4 && shadow == 0 { throw refuse("program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack") }
// class v5 (7 October 2026) is class v4 over the state leaves: the shadow block and IGNEUM_STATE_LEAVES both mark it
if generator == 5 && shadow == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack") }
if generator == 5 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)") }
if generator != 5 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) != 0 { throw refuse("program pack generator \(generator) carries IGNEUM_STATE_LEAVES: state leaves belong to class v5 (generator 5)") }
if generator == 3 && shadow != 0 { throw refuse("program pack generator \(generator) with a shadow block (IGNEUM_SHADOW_INSTRS \(shadow)): a class v4 program is generator 4 (export the pack as class v4)") }
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") }
@ -3159,6 +3163,29 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
let fillPipe = try gpu.device.makeComputePipelineState(function: fillFn)
let buildPipe = try gpu.device.makeComputePipelineState(function: buildFn)
let words = 1 << datasetLog2
// Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): the window's state leaves, leaves.bin beside program.h
// (IGNEUM_STATE_LEAVES leaves of 16 little-endian words), checked against the pack's count and FNV-1a 64
// (IGNEUM_STATE_LEAVES_FNV64) and bound as buffer 2 of igneum_build with the count in buffer 3 (packbench.swift's shape,
// the kernel text the Rust emitter wrote). A v5 pack without its leaves builds nothing (the known-failed case); the buffer
// is dropped after the build, so device memory while hashing is the class v4 worker's.
func defineU64(_ name: String) -> UInt64? {
guard let re = try? NSRegularExpression(pattern: "#define \(name) 0x([0-9a-fA-F]+)ull"), let m = re.firstMatch(in: programH, range: NSRange(programH.startIndex..., in: programH)) else { return nil }
return UInt64(programH[Range(m.range(at: 1), in: programH)!], radix: 16)
}
let stateLeaves = defineU32("IGNEUM_STATE_LEAVES") ?? 0
if (packClass == "v5") != (stateLeaves > 0) { throw refuse(packClass == "v5" ? "class v5 pack without IGNEUM_STATE_LEAVES" : "a class \(packClass) pack carries IGNEUM_STATE_LEAVES \(stateLeaves): state leaves belong to class v5") }
var leavesBuf: MTLBuffer? = nil
var nLeaves: UInt32 = 0
if stateLeaves > 0 {
let file = defineStr("IGNEUM_STATE_LEAVES_FILE") ?? "leaves.bin"
guard let data = FileManager.default.contents(atPath: dir + "/" + file) else { throw refuse("class v5 pack without its leaves file \(file) (the state leaves key every item: nothing can be built without them)") }
if data.count != Int(stateLeaves) * 64 { throw refuse("\(file) is \(data.count) bytes, the pack says \(stateLeaves) leaves of 64") }
guard let want = defineU64("IGNEUM_STATE_LEAVES_FNV64") else { throw refuse("class v5 pack without IGNEUM_STATE_LEAVES_FNV64: the leaves cannot be checked") }
let got = fnv1a64Bytes([UInt8](data))
if got != want { throw refuse("\(file) FNV-1a 64 \(h64(got)) is not the pack's IGNEUM_STATE_LEAVES_FNV64 \(h64(want)) (the leaves are of another state root; export the pack again)") }
guard let b = gpu.device.makeBuffer(bytes: (data as NSData).bytes, length: data.count, options: .storageModeShared) else { throw refuse("cannot allocate the \(data.count) byte leaf buffer") }
leavesBuf = b; nLeaves = stateLeaves
}
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 {
@ -3174,8 +3201,29 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
}
let buildMs = try run { enc in
enc.setComputePipelineState(buildPipe); enc.setBuffer(cache, offset: 0, index: 0); enc.setBuffer(dataset, offset: 0, index: 1)
if let lb = leavesBuf { enc.setBuffer(lb, offset: 0, index: 2); var n = nLeaves; enc.setBytes(&n, length: 4, index: 3) }
enc.dispatchThreadgroups(MTLSize(width: words / 16 / 256, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 256, height: 1, depth: 1))
}
leavesBuf = nil
// The build's self-test when the pack carries vectors.json (igneum-pow export writes it): the dataset's head 16 words and
// its last word against the CPU reference, as the one-click workers' pf_selftest does; a mismatch refuses the day (the
// miner's CPU re-check would catch every found nonce later, but a wrong dataset is wrong on every hash, so it stops here)
if let vdata = FileManager.default.contents(atPath: dir + "/vectors.json"), let vj = (try? JSONSerialization.jsonObject(with: vdata)) as? [String: Any],
let headHex = vj["dataset_head"] as? [String], headHex.count == 16, let lastIdx = (vj["dataset_last_index"] as? NSNumber)?.uint64Value, let lastHex = vj["dataset_last"] as? String {
func hex32(_ x: String) -> UInt32? { UInt32(x.hasPrefix("0x") ? String(x.dropFirst(2)) : x, radix: 16) }
let wantHead = headHex.compactMap(hex32)
if wantHead.count == 16, let wantLast = hex32(lastHex), lastIdx < UInt64(words) {
let copy = gpu.device.makeBuffer(length: 64 + 4, options: .storageModeShared)!
let cb = gpu.queue.makeCommandBuffer()!; let bl = cb.makeBlitCommandEncoder()!
bl.copy(from: dataset, sourceOffset: 0, to: copy, destinationOffset: 0, size: 64)
bl.copy(from: dataset, sourceOffset: Int(lastIdx) * 4, to: copy, destinationOffset: 64, size: 4)
bl.endEncoding(); cb.commit(); cb.waitUntilCompleted()
let p = copy.contents().bindMemory(to: UInt32.self, capacity: 17)
let headOk = (0..<16).allSatisfy { p[$0] == wantHead[$0] }
let lastOk = p[16] == wantLast
if !headOk || !lastOk { throw refuse("dataset self-test FAIL against vectors.json (head \(headOk ? "ok" : "BAD"), word [\(lastIdx)] \(lastOk ? "ok" : "BAD")\(stateLeaves > 0 ? "; class v5 with \(stateLeaves) state leaves" : ""))") }
}
}
let sd = ServeDataset(dayHex: dayHex, packClass: packClass, eraHex: eraHex, buffer: dataset, cacheFillGPUms: fillMs, buildGPUms: buildMs)
store.add(sd)
return (sd, ms(t0, nowNs()))
@ -3206,8 +3254,8 @@ func runServe(_ opts: Options) -> Never {
}
if wantClass != "" && wantClass != "v2" && !isPackClass(wantClass) {
let id = f.count > 1 ? f[1] : "0"
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2, v3, v4 or v5)") }
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2, v3, v4 or v5)") }
continue
}
if f[0] == "prepare" {

View file

@ -184,10 +184,19 @@ static uint64_t fnv1a64(const void* p, size_t n) {
}
static const uint32_t CACHE_WORDS_HOST = 1u << IGNEUM_CACHE_LOG2_WORDS;
static uint32_t* hCache = NULL;
#ifdef IGNEUM_STATE_LEAVES
/* class v5 compiled-in pack: the host copy of the pack's leaves (leaves.bin beside IGNEUM_KERNEL_PATH, loaded and checked by
* compiledInLeavesBuffer before the build), so the host reference derives the same words the device does */
static uint32_t* hLeaves = NULL;
#endif
/* dataset[w] through the pack's own mh_word (memhard.h), which carries the pack's item-to-word layout (era layout,
* 5 October 2026; the former w >> 4 / w & 15 here failed the random points of every interleaved pack). */
static uint32_t host_ds_word(uint32_t w) {
#ifdef IGNEUM_STATE_LEAVES
return mh_word(hCache, hLeaves, IGNEUM_STATE_LEAVES, w); /* class v5: every item keyed by leaf(t) = leaves[t mod n] */
#else
return mh_word(hCache, w);
#endif
}
#endif
@ -505,6 +514,13 @@ typedef struct {
int groupSize; // work-group size the program was built for (IGNEUM_GROUP = 32 x group-warps)
} Device;
/* class v5 build arguments and leaf buffers (defined after the serve pair below) */
static cl_int setBuildArgs(cl_kernel k, cl_mem ds, cl_mem cache, cl_mem leaves, cl_uint nLeaves, cl_uint nItems);
static int packLeavesBuffer(Device* dv, const char* packDir, cl_mem* leaves, cl_uint* nLeaves, char* err, size_t errCap);
#ifdef IGNEUM_STATE_LEAVES
static int compiledInLeavesBuffer(Device* dv, cl_mem* leaves, cl_uint* nLeaves, char* err, size_t errCap);
#endif
static const char* exchangeName(int m) { return m == 1 ? "sub_group_shuffle_xor (cl_khr_subgroup_shuffle)" : m == 2 ? "intel_sub_group_shuffle_xor (cl_intel_subgroups)" : "local-memory exchange with barrier"; }
// Returns 0 on success, 1 on build failure (log printed).
@ -819,10 +835,19 @@ static SizeResult runSize(Device* dv, const DeviceInfo* di, const Options* o, in
#if IGNEUM_DATASET_MODE == 1
cl_uint nItems = r.words / 16u;
size_t local = kernelMaxLocal(dv, dv->kBuild, di, 256);
CL_CHECK(clSetKernelArg(dv->kBuild, 0, sizeof(cl_mem), &dDs));
CL_CHECK(clSetKernelArg(dv->kBuild, 1, sizeof(cl_mem), &gCache));
CL_CHECK(clSetKernelArg(dv->kBuild, 2, sizeof(cl_uint), &nItems));
cl_mem leaves = NULL;
cl_uint nLeaves = 0;
#ifdef IGNEUM_STATE_LEAVES
{ /* class v5 compiled-in pack: its leaves from the pack directory beside the kernel */
char lerr[800];
if (!compiledInLeavesBuffer(dv, &leaves, &nLeaves, lerr, sizeof(lerr))) { printf("FAIL: %s\n", lerr); exit(2); }
if (pass == 0) printf("class v5: %u state leaves uploaded for the build (FNV-1a 64 %016llx checked against the pack)\n", (unsigned)nLeaves, (unsigned long long)IGNEUM_STATE_LEAVES_FNV64);
}
#endif
CL_CHECK(setBuildArgs(dv->kBuild, dDs, gCache, leaves, nLeaves, nItems));
ev = launch1D(dv, dv->kBuild, nItems, local);
CL_CHECK(clFinish(dv->q));
if (leaves) { clReleaseMemObject(leaves); ++gMemReleased; }
#else
cl_uint n = r.words, d0 = IGNEUM_DAY0, d1 = IGNEUM_DAY1;
size_t local = kernelMaxLocal(dv, dv->kFill, di, 256);
@ -1088,6 +1113,7 @@ typedef struct {
int checked;
char programClass[8]; /* Counter ASIC 2.0: the pack's class ("v2" or "v3") and era seed, from packfile.h */
char eraHex[65];
cl_uint stateLeaves; /* class v5: the state leaves the dataset was built from (uploaded for the build, released after); 0 otherwise */
} ServePair;
static int hexEq(const char* a, const char* b) {
@ -1132,6 +1158,65 @@ static void releasePair(ServePair* p) {
free(p);
}
/* Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): igneum_build takes the window's state leaves. The
* emitted kernel of a class v5 pack is igneum_build(ds, cache, leaves, nLeaves, nItems); every other class's is
* igneum_build(ds, cache, nItems). One place sets the arguments for both shapes. */
static cl_int setBuildArgs(cl_kernel k, cl_mem ds, cl_mem cache, cl_mem leaves, cl_uint nLeaves, cl_uint nItems) {
cl_int e = clSetKernelArg(k, 0, sizeof(cl_mem), &ds);
if (e == CL_SUCCESS) e = clSetKernelArg(k, 1, sizeof(cl_mem), &cache);
if (leaves) {
if (e == CL_SUCCESS) e = clSetKernelArg(k, 2, sizeof(cl_mem), &leaves);
if (e == CL_SUCCESS) e = clSetKernelArg(k, 3, sizeof(cl_uint), &nLeaves);
if (e == CL_SUCCESS) e = clSetKernelArg(k, 4, sizeof(cl_uint), &nItems);
} else if (e == CL_SUCCESS) e = clSetKernelArg(k, 2, sizeof(cl_uint), &nItems);
return e;
}
/* The leaves of a pack directory as a device buffer (NULL with *nLeaves 0 for a pack of another class): read and checked
* against the pack's count and FNV-1a 64 by pf_load_leaves before the upload. The caller releases the buffer after the
* build (the leaves are not needed while hashing). Returns 1, or 0 with err set. */
static int packLeavesBuffer(Device* dv, const char* packDir, cl_mem* leaves, cl_uint* nLeaves, char* err, size_t errCap) {
PfPack pk;
char perr[700];
uint32_t* h = NULL;
size_t bytes = 0;
cl_int e = 0;
*leaves = NULL; *nLeaves = 0;
if (!packDir || !packDir[0]) return 1;
if (!pf_load(packDir, &pk, perr, sizeof(perr))) { snprintf(err, errCap, "pack %s: %s", packDir, perr); return 0; }
if (!pf_load_leaves(packDir, &pk, &h, &bytes, perr, sizeof(perr))) { snprintf(err, errCap, "pack %s: %s", packDir, perr); return 0; }
if (!h) return 1;
*leaves = clCreateBuffer(dv->ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, bytes, h, &e);
free(h);
if (e != CL_SUCCESS) { *leaves = NULL; snprintf(err, errCap, "clCreateBuffer state leaves (%s)", clErrName(e)); return 0; }
++gMemCreated;
*nLeaves = pk.stateLeaves;
return 1;
}
#ifdef IGNEUM_STATE_LEAVES
/* The compiled-in pack is a class v5 pack: its leaves live beside IGNEUM_KERNEL_PATH (the directory build.sh compiled
* against). The plain bench and the placeholder serve path build from them; without the file they build nothing. */
static int compiledInLeavesBuffer(Device* dv, cl_mem* leaves, cl_uint* nLeaves, char* err, size_t errCap) {
static char dir[1200];
const char* k = IGNEUM_KERNEL_PATH;
const char* slash = strrchr(k, '/');
#ifdef _WIN32
const char* bslash = strrchr(k, '\\');
if (bslash && (!slash || bslash > slash)) slash = bslash;
#endif
if (slash) { size_t n = (size_t)(slash - k); if (n >= sizeof(dir)) n = sizeof(dir) - 1; memcpy(dir, k, n); dir[n] = 0; } else strcpy(dir, ".");
if (!hLeaves) {
/* the host copy for host_ds_word (the 64 random points of the dataset self-test), checked by pf_load_leaves */
PfPack pk; char perr[700]; size_t bytes = 0;
if (!pf_load(dir, &pk, perr, sizeof(perr))) { snprintf(err, errCap, "pack %s: %s", dir, perr); return 0; }
if (!pf_load_leaves(dir, &pk, &hLeaves, &bytes, perr, sizeof(perr))) { snprintf(err, errCap, "pack %s: %s", dir, perr); return 0; }
if (!hLeaves || pk.stateLeaves != IGNEUM_STATE_LEAVES) { snprintf(err, errCap, "pack %s: program.h on disk says %u state leaves, this exe was built for %u", dir, (unsigned)pk.stateLeaves, (unsigned)IGNEUM_STATE_LEAVES); return 0; }
}
return packLeavesBuffer(dv, dir, leaves, nLeaves, err, errCap);
}
#endif
/* The prepare request and its result, handed between the main loop and the prepare thread. */
typedef struct {
Device* dv;
@ -1254,12 +1339,19 @@ static int pairBuffers(Device* dv, const DeviceInfo* di, cl_command_queue q, Ser
++gMemCreated;
local = kernelMaxLocal(dv, p->kBuild, di, 256);
g = ((nItems + local - 1) / local) * local;
e = clSetKernelArg(p->kBuild, 0, sizeof(cl_mem), &p->ds);
if (e == CL_SUCCESS) e = clSetKernelArg(p->kBuild, 1, sizeof(cl_mem), &p->cache);
if (e == CL_SUCCESS) e = clSetKernelArg(p->kBuild, 2, sizeof(cl_uint), &nItems);
if (e == CL_SUCCESS) e = clEnqueueNDRangeKernel(q, p->kBuild, 1, NULL, &g, &local, 0, NULL, NULL);
if (e == CL_SUCCESS) e = clFinish(q);
if (e != CL_SUCCESS) { snprintf(err, errCap, "dataset build (%s)", clErrName(e)); return 0; }
{
/* class v5: the pack's state leaves, uploaded for the build (checked by pf_load_leaves) and released after it;
* a v5 pack without them returns 0 here and the pair is never served (the known-failed case) */
cl_mem leaves = NULL;
cl_uint nLeaves = 0;
if (!packLeavesBuffer(dv, packDir, &leaves, &nLeaves, err, errCap)) return 0;
e = setBuildArgs(p->kBuild, p->ds, p->cache, leaves, nLeaves, nItems);
if (e == CL_SUCCESS) e = clEnqueueNDRangeKernel(q, p->kBuild, 1, NULL, &g, &local, 0, NULL, NULL);
if (e == CL_SUCCESS) e = clFinish(q);
if (leaves) { clReleaseMemObject(leaves); ++gMemReleased; }
if (e != CL_SUCCESS) { snprintf(err, errCap, "dataset build (%s%s)", clErrName(e), nLeaves ? ", class v5 with the state leaves" : ""); return 0; }
p->stateLeaves = nLeaves;
}
p->datasetMs = wallMs() - tb;
if (p->kHotFill && p->hotWords) {
/* hot-table experiment: the epoch's table from the pack's own fill kernel (one work-item per segment) */
@ -1395,7 +1487,8 @@ static int runBenchPack(Device* dv, const DeviceInfo* di, const Options* o) {
memcpy(cur->sw, gPack.seedw, 32); memcpy(cur->kw, gPack.keyw, 32);
if (!pairHotKernel(cur, o->packDir, perr, sizeof(perr))) { printf("FAIL: pack %s: %s\n", o->packDir, perr); return 1; }
if (!pairBuffers(dv, di, dv->q, cur, words, gServeCacheWords, gServeSegments, o->packDir, perr, sizeof(perr))) { printf("FAIL: pack %s: %s\n", o->packDir, perr); return 1; }
printf("pack %s: cache %.0f dataset %.0f hot %.0f check %.0f ms (%.0f ms in all); %s\n", o->packDir, cur->cacheMs, cur->datasetMs, cur->hotMs, cur->checkMs, wallMs() - t0, cur->check);
printf("pack %s: cache %.0f dataset %.0f hot %.0f check %.0f ms (%.0f ms in all); class %s%s; %s\n", o->packDir, cur->cacheMs, cur->datasetMs, cur->hotMs, cur->checkMs, wallMs() - t0,
gPack.programClass, cur->stateLeaves ? " (class v5: the state leaves uploaded for the build and released)" : "", cur->check);
printKernelInfo(di, cur->kHashBound, "igneum_hash_bound", (int)groupSize, "");
dOut = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, (size_t)nonces * sizeof(uint64_t), NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer out");
dInit = clCreateBuffer(dv->ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, 32, cur->sw, &err); CL_CHECK_ERR(err, "clCreateBuffer init words");
@ -1478,17 +1571,26 @@ static int runServe(Device* dv, const DeviceInfo* di, const Options* o) {
strncpy(cur->programClass, gPack.programClass, sizeof(cur->programClass) - 1); strncpy(cur->eraHex, gPack.eraHex, sizeof(cur->eraHex) - 1);
if (!pairHotKernel(cur, o->packDir, perr, sizeof(perr))) { printf("error 0 pack %s: %s\n", o->packDir, perr); fflush(stdout); return 1; }
if (!pairBuffers(dv, di, dv->q, cur, words, gServeCacheWords, gServeSegments, o->packDir, perr, sizeof(perr))) { printf("error 0 pack %s: %s\n", o->packDir, perr); fflush(stdout); return 1; }
printf("info first pack %s: cache %.0f dataset %.0f check %.0f ms (%.0f ms in all); %s\n", o->packDir, cur->cacheMs, cur->datasetMs, cur->checkMs, wallMs() - t0, cur->check);
printf("info first pack %s: cache %.0f dataset %.0f check %.0f ms (%.0f ms in all); class %s%s; %s\n", o->packDir, cur->cacheMs, cur->datasetMs, cur->checkMs, wallMs() - t0,
cur->programClass[0] ? cur->programClass : "v2", cur->stateLeaves ? " (class v5: the state leaves uploaded for the build and released)" : "", cur->check);
} else {
if (!setupCache(dv, di)) { printf("error 0 cache check failed (device cache differs from the host cache or the pack's FNV)\n"); fflush(stdout); return 1; }
memcpy(cur->sw, SEEDW, 32); memcpy(cur->kw, KEYW, 32);
cur->cache = gCache; gCache = NULL;
cur->ds = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, (size_t)words * 4u, NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer dataset");
CL_CHECK(clSetKernelArg(cur->kBuild, 0, sizeof(cl_mem), &cur->ds));
CL_CHECK(clSetKernelArg(cur->kBuild, 1, sizeof(cl_mem), &cur->cache));
CL_CHECK(clSetKernelArg(cur->kBuild, 2, sizeof(cl_uint), &nItems));
countRelease(launch1D(dv, cur->kBuild, nItems, kernelMaxLocal(dv, cur->kBuild, di, 256)));
CL_CHECK(clFinish(dv->q));
{
cl_mem leaves = NULL;
cl_uint nLeaves = 0;
#ifdef IGNEUM_STATE_LEAVES
char lerr[800];
if (!compiledInLeavesBuffer(dv, &leaves, &nLeaves, lerr, sizeof(lerr))) { printf("error 0 %s\n", lerr); fflush(stdout); return 1; }
#endif
CL_CHECK(setBuildArgs(cur->kBuild, cur->ds, cur->cache, leaves, nLeaves, nItems));
countRelease(launch1D(dv, cur->kBuild, nItems, kernelMaxLocal(dv, cur->kBuild, di, 256)));
CL_CHECK(clFinish(dv->q));
if (leaves) { clReleaseMemObject(leaves); ++gMemReleased; }
cur->stateLeaves = nLeaves;
}
}
dOut = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, (size_t)batch * sizeof(uint64_t), NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer out");
dInit = clCreateBuffer(dv->ctx, CL_MEM_READ_ONLY, 32, NULL, &err); CL_CHECK_ERR(err, "clCreateBuffer init words");

View file

@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Class v5 kit bench on one fleet NVIDIA card (7 October 2026, docs/design/class-v5-stored-state.md section 13, "the kit:
# CUDA"). Run ON the pod by the fleet lane, in a directory holding the unzipped kit (packs-ca3-v5-<stamp>.zip from
# igneum-build-1:/srv/artefacts/packs/): the kit's Linux NVRTC worker (bin/linux/igneum-worker-cuda, THIS tree's
# proto-cuda/nvrtc/worker.cpp with the class v5 leaf upload) runs --check and --bench on the first class v5 pack,
# packs/v5-dn3-epoch0 (program id e5a4ac5978462156, 11 leaves under state root 7e37a9fb..., cache FNV 7334fa46e5d972eb);
# then src/bench.cu (the nvcc harness of the 4090 rows) on the same pack. Expected on every line: the self-test PASS and
# the fingerprint of the 2^24 outputs at base nonce 0 equal to the Metal reading 82b19cbde8557ea5 (M5 Max, 18:07:09Z).
# One card, about a minute. Prints RESULT lines; exit 0 only when both fingerprints match.
# bash tools/fleet-cuda-v5-bench.sh [device index, default 0] [sm arch for nvcc, default from nvidia-smi]
set -u
DEV="${1:-0}"
EXPECTED=82b19cbde8557ea5
KIT="$(cd "$(dirname "$0")/.." && pwd)"
export PATH=/usr/local/cuda/bin:$PATH
echo "RESULT start $(date -u +%Y-%m-%dT%H:%M:%SZ) host=$(hostname) kit=$KIT expected_fingerprint=$EXPECTED"
nvidia-smi -i "$DEV" --query-gpu=name,driver_version,power.limit,memory.total --format=csv,noheader | sed 's/^/RESULT card /'
ARCH="${2:-}"
if [ -z "$ARCH" ]; then cc=$(nvidia-smi -i "$DEV" --query-gpu=compute_cap --format=csv,noheader | tr -d ' .'); ARCH="sm_$cc"; fi
PACK="$KIT/packs/v5-dn3-epoch0"
[ -f "$PACK/leaves.bin" ] || { echo "RESULT error leaves.bin missing in $PACK (a class v5 pack without its leaves builds nothing)"; exit 2; }
echo "RESULT leaves $(wc -c < "$PACK/leaves.bin") bytes sha256 $(sha256sum "$PACK/leaves.bin" | cut -c1-64)"
W="$KIT/bin/linux/igneum-worker-cuda"
chmod +x "$W" 2>/dev/null
echo "RESULT worker $W sha256 $(sha256sum "$W" | cut -c1-64)"
ok=0
echo "RESULT check start $(date -u +%H:%M:%SZ) cmd=igneum-worker-cuda --check --pack packs/v5-dn3-epoch0 --device $DEV"
"$W" --check --pack "$PACK" --device "$DEV" 2>&1 | sed 's/^/RESULT check out /'
echo "RESULT bench start $(date -u +%H:%M:%SZ) cmd=igneum-worker-cuda --bench --pack packs/v5-dn3-epoch0 --device $DEV --batch-log2 24 --batches 5"
out=$("$W" --bench --pack "$PACK" --device "$DEV" --batch-log2 24 --batches 5 2>&1); rc=$?
echo "$out" | grep -E '^(pack |RESULT|FAIL|warm-up)' | sed 's/^/RESULT bench out /'
fp=$(echo "$out" | grep -o 'fingerprint=[0-9a-f]*' | tail -1 | cut -d= -f2)
mhs=$(echo "$out" | grep -o 'mhs=[0-9.]*' | tail -1 | cut -d= -f2)
chk=$(echo "$out" | grep -o 'check=[A-Za-z]*' | tail -1 | cut -d= -f2)
match=no; [ "$fp" = "$EXPECTED" ] && [ "$chk" = PASS ] && { match=yes; ok=$((ok + 1)); }
echo "RESULT worker-v5 fingerprint=$fp expected=$EXPECTED match=$match check=$chk mhs=$mhs exit=$rc $(date -u +%H:%M:%SZ)"
# the nvcc harness (the 4090 rows of the design page, section 7): compiled in the pack directory against its kernel.cu
if command -v nvcc > /dev/null; then
( cd "$PACK" && nvcc -O3 -std=c++17 -arch="$ARCH" -Xcompiler -pthread -I. -o bench "$KIT/src/bench.cu" kernel.cu 2>&1 | grep -v '^$' | sed 's/^/RESULT nvcc /'; echo "RESULT nvcc rc ${PIPESTATUS[0]} arch=$ARCH" )
if [ -x "$PACK/bench" ]; then
out2=$(cd "$PACK" && CUDA_VISIBLE_DEVICES="$DEV" ./bench --batches 10 --power-seconds 0 2>&1); rc2=$?
echo "$out2" | sed 's/^/RESULT bench.cu out /'
fp2=$(echo "$out2" | grep -o 'fingerprint of 2^24 outputs at base 0: [0-9a-f]*' | grep -o '[0-9a-f]\{16\}$')
m2=no; [ "$fp2" = "$EXPECTED" ] && echo "$out2" | grep -q 'vector warps against the pack: 3 of 3' && { m2=yes; ok=$((ok + 1)); }
echo "RESULT bench.cu-v5 fingerprint=$fp2 expected=$EXPECTED match=$m2 exit=$rc2 $(date -u +%H:%M:%SZ)"
fi
else
echo "RESULT nvcc absent: the bench.cu row is skipped (the worker row stands)"; ok=$((ok + 1))
fi
echo "RESULT end $(date -u +%Y-%m-%dT%H:%M:%SZ) rows_matched=$ok of 2"
[ "$ok" = 2 ]

112
tools/class-v5/kits-remote.sh Executable file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env bash
# The class v5 kits for every platform, built and tested on igneum-build-1 (docs/design/class-v5-stored-state.md section 13,
# the kit rows; 7 October 2026). From THIS worktree's commit (HEAD through the bare mirror, as tools/build-remote.sh moves
# sources; nothing is built on the Mac): the pack loader test with the class v5 cases (proto-cuda/nvrtc/emu/packfile-test.c),
# the CPU emulation of the NVRTC worker running --check on the class v5 pack (the real kernel text on host threads, the
# dataset built from the pack's leaves and checked against the pack's cache FNV, dataset head, last word, 64 samples and
# three vector warps: the "exactly as igneum-pow's verifier" line without a GPU), the Linux and Windows builds of the two
# one-click workers (proto-cuda/nvrtc/worker.cpp through NVRTC, proto-opencl/host.c for AMD, Intel and Apple), then one kit
# zip at /srv/artefacts/packs/packs-ca3-v5-<stamp>.zip holding the three packs (v4-genesis, v5-genesis, v5-dn3-epoch0), the
# binaries, the sources, the bench scripts for the fleet and PC 1, and SHA256SUMS. Prints the zip's sha256 last.
#
# IGNEUM_AGENT=v5-kits tools/class-v5/kits-remote.sh [--box 1] [--skip-emu]
#
# Takes one of the box's build slots through infra/build-server/remote-run.sh (bs_remote_run), box 1 by default (the
# build-server lane's word of 7 October 2026, 19:4x BST: the kit builds on build-1 explicitly).
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BS_TOOL=class-v5-kits
# shellcheck source=../../infra/build-server/lib.sh
. "$HERE/../../infra/build-server/lib.sh"
BOX=1; SKIP_EMU=0
while [ $# -gt 0 ]; do
case "$1" in --box) BOX="$2"; shift 2 ;; --skip-emu) SKIP_EMU=1; shift ;; *) echo "unknown argument $1" >&2; exit 2 ;; esac
done
export IGNEUM_AGENT="${IGNEUM_AGENT:-v5-kits}"
bs_host "$BOX"
pushd "$HERE/../../igneum-pow" > /dev/null; bs_context; popd > /dev/null
WT="$BS_REMOTE_WT"
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
ZIP="/srv/artefacts/packs/packs-ca3-v5-$STAMP.zip"
bs_toolchain_check
bs_sync_sources
bs_log "sources at $WT (commit $BS_SHA on $BS_BRANCH); building the class v5 kits on box $BOX"
read -r -d '' CMD <<EOF2 || true
set -u
cd '$WT'
T=\$(mktemp -d /tmp/v5-kits.XXXXXX); S=\$T/stage; mkdir -p \$S/bin/linux \$S/bin/windows \$S/src \$S/packs \$S/tools
rc=0
step() { echo "STEP \$1 \$(date -u +%H:%M:%SZ)"; }
fail() { echo "FAIL \$1"; rc=1; }
V5=proto-cuda/packs-ca3-v5/v5-dn3-epoch0
step packfile-test
cc -std=c99 -Wall -Wextra -Wno-unused-function -O1 -o \$T/packfile-test proto-cuda/nvrtc/emu/packfile-test.c && \$T/packfile-test proto-cuda/packs/igneum-devnet-v4-epoch0 \$V5 > \$T/packfile-test.log 2>&1 || fail "packfile-test (\$(tail -1 \$T/packfile-test.log))"
grep -E '^(FAIL| v5 pack)' \$T/packfile-test.log; grep -c '^ok' \$T/packfile-test.log | sed 's/^/packfile-test ok lines /'
step linux-opencl-worker
gcc -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I proto-cuda/packs/igneum-devnet-v4-epoch0 -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o \$S/bin/linux/igneum-worker-opencl proto-opencl/host.c -ldl -lpthread 2> \$T/cl-linux.log || { fail "linux opencl worker"; head -20 \$T/cl-linux.log; }
step linux-cuda-worker
g++ -std=c++17 -O2 -Wall -Wextra -I proto-cuda/nvrtc -I /usr/local/cuda/include -o \$S/bin/linux/igneum-worker-cuda proto-cuda/nvrtc/worker.cpp -ldl -lpthread 2> \$T/cuda-linux.log || { fail "linux cuda worker"; head -20 \$T/cuda-linux.log; }
step windows-cuda-worker
x86_64-w64-mingw32-g++ -std=c++17 -O2 -Wall -Wextra -static -I proto-cuda/nvrtc -I /usr/local/cuda/include -o \$S/bin/windows/igneum-worker-cuda.exe proto-cuda/nvrtc/worker.cpp 2> \$T/cuda-win.log && x86_64-w64-mingw32-strip \$S/bin/windows/igneum-worker-cuda.exe || { fail "windows cuda worker"; head -20 \$T/cuda-win.log; }
step windows-opencl-worker
x86_64-w64-mingw32-gcc -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I /usr/include -I proto-cuda/packs/igneum-devnet-v4-epoch0 -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o \$S/bin/windows/igneum-worker-opencl.exe proto-opencl/host.c 2> \$T/cl-win.log && x86_64-w64-mingw32-strip \$S/bin/windows/igneum-worker-opencl.exe || { fail "windows opencl worker"; head -20 \$T/cl-win.log; }
if [ $SKIP_EMU = 0 ]; then
step emu-check-v5
# the CPU emulation (proto-cuda/nvrtc/emu/test.sh's build) with pack A = the class v5 pack and pack B = the v4 control
E=\$T/emu; mkdir -p \$E
emu_kernel() { { echo '#include <cuda_runtime.h>'; echo '#include <cstdint>'; echo "namespace \$2 {"; sed -E 's/([A-Za-z_0-9]+)<<<([^,]+), ([^>]+)>>>\\(/emu_launch(\\1, \\2, \\3, /' "\$1/\$3.cu"; echo "}"; } > "\$E/\$3_\$2.cpp"; g++ -std=c++17 -O2 -w -I proto-cuda/emu -I "\$1" -c "\$E/\$3_\$2.cpp" -o "\$E/\$3_\$2.o"; }
emu_kernel \$V5 emu_pack_a kernel && emu_kernel \$V5 emu_pack_a kernel_bound && emu_kernel proto-cuda/packs-ca3-v5/v4-genesis emu_pack_b kernel && emu_kernel proto-cuda/packs-ca3-v5/v4-genesis emu_pack_b kernel_bound \\
&& g++ -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -I proto-cuda/nvrtc -I /usr/local/cuda/include -c proto-cuda/nvrtc/worker.cpp -o \$E/worker.o \\
&& g++ -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -DIGNEUM_EMU_TWO_PACKS -I proto-cuda/nvrtc -I proto-cuda/emu -I /usr/local/cuda/include -c proto-cuda/nvrtc/emu/emu_backend.cpp -o \$E/emu_backend.o \\
&& g++ -std=c++17 -O2 -w -I proto-cuda/emu -c proto-cuda/emu/shim.cpp -o \$E/shim.o \\
&& g++ -o \$E/igneum-worker-cuda-emu \$E/*.o -pthread 2> \$T/emu-build.log || { fail "emu build"; head -30 \$T/emu-build.log; }
if [ -x \$E/igneum-worker-cuda-emu ]; then
( IGNEUM_EMU_PACK=\$PWD/\$V5 IGNEUM_EMU_PACK2=\$PWD/proto-cuda/packs-ca3-v5/v4-genesis timeout 1500 nice -n 10 \$E/igneum-worker-cuda-emu --check --pack \$V5 > \$T/emu-check.log 2>&1 ) || fail "emu --check on the class v5 pack (rc \$?)"
grep -E '^check PASS|self-test|FAIL|error' \$T/emu-check.log | cut -c1-400
fi
fi
step stage
for p in v4-genesis v5-genesis v5-dn3-epoch0; do mkdir -p \$S/packs/\$p; cp proto-cuda/packs-ca3-v5/\$p/* \$S/packs/\$p/; done
cp proto-cuda/nvrtc/worker.cpp proto-cuda/nvrtc/packfile.h proto-cuda/nvrtc/cuda_api.h proto-opencl/host.c proto-opencl/cl_dynamic.h proto-newpow/class-v5/bench.cu proto-newpow/class-v5/run.sh proto-metal/packbench.swift \$S/src/
cp tools/class-v5/fleet-cuda-v5-bench.sh tools/class-v5/pc1-amd-v5-bench.ps1 \$S/tools/
cat > \$S/README.txt <<'R'
packs-ca3-v5: the class v5 kit (Igneum, 7 October 2026; docs/design/class-v5-stored-state.md)
packs/v5-dn3-epoch0 the first class v5 pack: Devnet 3 epoch 0, program id e5a4ac5978462156, 11 state leaves (leaves.bin, 704 B) under
state root 7e37a9fb19b154d32daf5bf30a50d339a75029fbc9eec9ea20e95439dba5a311, cache FNV-1a 64 7334fa46e5d972eb
expected fingerprint of the 2^24 outputs at base nonce 0: 82b19cbde8557ea5 (Metal, M5 Max, 18:07:09Z) on every platform
packs/v5-genesis the string-seed class v5 pack over the devnet's 93 leaves; packs/v4-genesis the class v4 control (sub-version 3)
bin/linux igneum-worker-cuda (NVRTC, libcuda + libnvrtc.so.12 at run time), igneum-worker-opencl (libOpenCL.so.1 at run time)
bin/windows igneum-worker-cuda.exe (nvcuda.dll + nvrtc64 at run time), igneum-worker-opencl.exe (OpenCL.dll at run time)
Every worker reads a class v5 pack's leaves.bin, checks it against the pack's IGNEUM_STATE_LEAVES_FNV64, uploads it for igneum_build and
frees it after the build; a pack without its leaves builds nothing. Self-test: cache head, last line and FNV; dataset head, last word and
64 samples; 96 vector lanes. Bench: igneum-worker-cuda --bench --pack packs/v5-dn3-epoch0 --batch-log2 24 (fleet-cuda-v5-bench.sh);
igneum-worker-opencl --bench-pack --pack packs/v5-dn3-epoch0 --batch-log2 24 --device <n> (pc1-amd-v5-bench.ps1); src/bench.cu with nvcc.
R
( cd \$S && find . -type f ! -name SHA256SUMS | sort | xargs sha256sum > SHA256SUMS )
mkdir -p /srv/artefacts/packs
python3 - "\$S" '$ZIP' <<'PY'
import os, sys, zipfile
stage, out = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z:
for dp, dn, fn in os.walk(stage):
dn.sort()
for f in sorted(fn):
p = os.path.join(dp, f); rel = os.path.relpath(p, stage)
zi = zipfile.ZipInfo(rel, date_time=(2026, 10, 7, 0, 0, 0)); zi.compress_type = zipfile.ZIP_DEFLATED
zi.external_attr = (0o755 if rel.startswith('bin/') or rel.endswith('.sh') else 0o644) << 16
with open(p, 'rb') as fh: z.writestr(zi, fh.read())
PY
sha256sum '$ZIP' | cut -c1-64 > '$ZIP.sha256'
echo "KIT $ZIP bytes \$(stat -c %s '$ZIP') files \$(python3 -c "import zipfile,sys; print(len(zipfile.ZipFile(sys.argv[1]).namelist()))" '$ZIP')"
echo "KIT sha256 \$(cat '$ZIP.sha256')"
for b in \$S/bin/linux/* \$S/bin/windows/*; do echo "BIN \$(basename \$(dirname \$b))/\$(basename \$b) \$(stat -c %s \$b) bytes sha256 \$(sha256sum \$b | cut -c1-64)"; done
cp \$T/*.log /srv/builds/_log/v5-class/kits-$STAMP/ 2>/dev/null || { mkdir -p /srv/builds/_log/v5-class/kits-$STAMP && cp \$T/*.log /srv/builds/_log/v5-class/kits-$STAMP/; }
echo "LOGS /srv/builds/_log/v5-class/kits-$STAMP"
rm -rf \$T
( exit \$rc )
EOF2
BR_KIND=build BR_COMMAND="class v5 kits (packfile-test, emu --check v5, Linux and Windows workers, kit zip)" BR_TARGET="x86_64-linux+windows" BR_ARTEFACTS="" \
bs_remote_run "$WT" "$BS_WT class-v5 kits" "$CMD" 2>&1 | tee "/tmp/v5-kits-$STAMP.log" | grep -E '^(STEP|FAIL|KIT|BIN|LOGS|check PASS|self-test|packfile-test|emu:| v5|build-remote: RESULT)' || true
rc=${PIPESTATUS[0]}
bs_wt_unlock
[ "$rc" = 0 ] && bs_log "class v5 kits built: $ZIP" || bs_die "class v5 kits FAILED (rc $rc); full log /tmp/v5-kits-$STAMP.log"

View file

@ -0,0 +1,80 @@
# Class v5 kit bench on PC 1's RX 9070 XT (7 October 2026, docs/design/class-v5-stored-state.md section 13, "the kit:
# OpenCL (AMD)"): the kit's igneum-worker-opencl.exe (THIS tree's proto-opencl/host.c with the class v5 leaf upload:
# igneum_build(ds, cache, leaves, nLeaves, nItems), the leaves of leaves.bin checked against the pack's FNV-1a 64 first)
# runs --bench-pack on the first class v5 pack, proto-cuda/packs-ca3-v5/v5-dn3-epoch0 (program id e5a4ac5978462156, 11
# leaves under state root 7e37a9fb..., cache FNV 7334fa46e5d972eb), on the gfx1201 device. Expected: the self-test
# PASS line (cache head, last line and FNV; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes) and the
# fingerprint of the 2^24 outputs at base nonce 0 equal to the Metal reading, 82b19cbde8557ea5 (the M5 Max, 7 October
# 2026, 18:07:09Z). The v4-genesis control pack runs after it so the run has a known class v4 row beside the v5 row.
# Beside the miners (a correctness and fingerprint gate; the rate row is labelled loaded when the card mines): no card is
# switched, nothing is posted to the installed app, nothing is built on the PC. Published by the Counter ASIC coordinator
# only (the PC 1 queue is its); the kit is fetch job $kitId. Every result line starts with RESULT; SUMMARY {json} ends it.
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-amd-v5-bench'
$kitId = $env:IGNEUM_V5_KIT_ID; if (-not $kitId) { $kitId = 'fetch-ca3-v5-kit-20261007' }
$expected = '82b19cbde8557ea5'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION expected_fingerprint=$expected"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kit = Join-Path $jobs $kitId
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
$exe = Join-Path $kit 'bin\windows\igneum-worker-opencl.exe'
$packs = Join-Path $kit 'packs'
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)"
foreach ($pk in @('v5-dn3-epoch0', 'v4-genesis')) {
$d = Join-Path $packs $pk
if (-not (Test-Path (Join-Path $d 'kernel_bound.cl'))) { "RESULT error pack $pk missing at $d"; Summary 'failed' @{ error = "pack $pk missing" }; exit 2 }
"RESULT pack $pk kernel_bound.cl sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'kernel_bound.cl')).Hash.ToLower()) program.h sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'program.h')).Hash.ToLower())"
}
$leaves = Join-Path $packs 'v5-dn3-epoch0\leaves.bin'
if (-not (Test-Path $leaves)) { "RESULT error leaves.bin missing at $leaves (a class v5 pack without its leaves builds nothing)"; Summary 'failed' @{ error = 'leaves missing' }; exit 2 }
"RESULT leaves $leaves bytes $((Get-Item $leaves).Length) sha256 $((Get-FileHash -Algorithm SHA256 $leaves).Hash.ToLower())"
# the OpenCL device index of the 9070 XT (the installed worker's list when present: the app's own indices)
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
$listExe = $exe
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' }
$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" })
$list | ForEach-Object { "RESULT list $_" }
$dev = $null
foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1201' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
if ($null -eq $dev) { "RESULT error no gfx1201 device in --list (the eGPU is off the bus: the AMD v5 row stays OWED)"; Summary 'failed' @{ error = 'no gfx1201' }; exit 2 }
"RESULT device $dev gfx1201 (list from $listExe)"
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
$w = @(Workers)
$loaded = ($w | Where-Object { $_ -match "igneum-worker-opencl.*--device\s+$dev(\s|$)" }).Count -gt 0
"RESULT workers_before $(Stamp) $($w -join ' ')"
$state = if ($loaded) { 'loaded' } else { 'quiet' }
"RESULT context card_state=$state (a fingerprint gate: the load changes the rate row, never the bytes)"
$rows = @{}
$fpOk = $false
foreach ($pk in @('v5-dn3-epoch0', 'v4-genesis')) {
$d = Join-Path $packs $pk
$t0 = Get-Date
"RESULT bench $pk start $(Stamp) cmd=igneum-worker-opencl.exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5"
$out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5 2>&1 | ForEach-Object { "$_" })
$code = $LASTEXITCODE
$secs = [int]((Get-Date) - $t0).TotalSeconds
foreach ($l in $out) { if ($l -match '^(pack |class v5|RESULT |FAIL|error|warm-up)') { "RESULT bench $pk out $l" } }
$res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1
$fp = ''; $mhs = ''; $check = ''
if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -match 'mhs=([0-9.]+)') { $mhs = $Matches[1] }
if ($res -match 'check=(\w+)') { $check = $Matches[1] }
$rows[$pk] = [ordered]@{ exit = $code; seconds = $secs; fingerprint = $fp; mhs = $mhs; check = $check; card_state = $state }
if ($pk -eq 'v5-dn3-epoch0') {
$fpOk = ($fp -eq $expected -and $check -eq 'PASS')
"RESULT v5 fingerprint=$fp expected=$expected match=$fpOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)"
} else {
"RESULT v4-control fingerprint=$fp check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)"
}
}
"RESULT workers_after $(Stamp) $((@(Workers)) -join ' ')"
Summary $(if ($fpOk) { 'done' } else { 'failed' }) @{ expected = $expected; v5 = $rows['v5-dn3-epoch0']; v4_control = $rows['v4-genesis']; device = $dev; kit = $kitId }
exit $(if ($fpOk) { 0 } else { 1 })