One-click CUDA worker: driver API + NVRTC loaded at run time, pack read at run time, Mac emulation check

proto-cuda/nvrtc/worker.cpp serves igneum-miner's worker protocol with nothing installed but the NVIDIA driver:
nvcuda.dll and the redistributable nvrtc64_120_0.dll are opened with LoadLibrary (cuda_api.h), the pack's kernel.cu
and kernel_bound.cu are handed to NVRTC byte for byte up to the host launch wrappers with program.h and memhard.h as
named headers, every pack is self-tested against its vectors.h (cache head, last line, FNV-1a 64, dataset head, last
word, 64 samples, 96 vector lanes) before it serves a job, prepare runs on a thread for the hourly swap, and a job on
seeds without a pair makes the worker find the miner's pack by seeds.txt and build it. packfile.h (C99) reads a pack
directory. fetch-redist.sh verifies and stages the NVIDIA 12.8.93 redistributables and the Khronos headers
(THIRD-PARTY.md records URLs, hashes and the EULA clause); build-windows.sh cross-compiles with mingw (static,
KERNEL32 + Universal CRT only). emu/: the two libraries as host functions, the pack kernels on host threads, the NVRTC
source compared with the pack files; test.sh PASS on the Mac (two real packs, swap, self-heal, 17 sampled hashes equal
igneum-pow hash-bound). Not run here: the real NVRTC compile and driver load (RTX 5090 PC).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-04 09:55:05 +00:00
parent 4c3f8420ce
commit f535abb59d
10 changed files with 1691 additions and 0 deletions

8
.gitignore vendored
View file

@ -16,3 +16,11 @@ sim/__pycache__/
*.pyc
brand/Unbounded.ttf
.vercel
# one-click worker: fetched third-party pieces and the Mac emulation build
proto-cuda/nvrtc/redist/
proto-cuda/nvrtc/emu/build/
proto-cuda/nvrtc/*.exe
proto-opencl/*.exe
proto-opencl/igneum-bench-cl-generic-test
proto-opencl/generic-test*.log
proto-cuda/nvrtc/emu/test-run.log

View file

@ -0,0 +1,62 @@
# Third-party pieces of the one-click Windows workers
Recorded 4 October 2026. Nothing listed here is committed to this repository; `fetch-redist.sh` downloads every
file into `~/.cache/igneum/`, checks its SHA-256 against the values below and stages it under `proto-cuda/nvrtc/redist/`
(gitignored). `build-windows.sh` compiles against the headers; `windows-app/make-package.sh` ships the two NVRTC DLLs
and the two licence texts in the zip. Nothing else third-party is in the package.
## Shipped in the package
| File in the zip | Source | Version | Size (bytes) | SHA-256 |
|---|---|---|---|---|
| `nvrtc64_120_0.dll` | NVIDIA CUDA redistributable, package `cuda_nvrtc` (NVIDIA Runtime Compilation Library) | 12.8.93 (CUDA 12.8 Update 1, released 2025-03-06) | 86,728,192 | `02d2d7ef4690bf1a` (first 16 hex; the archive's hash is below) |
| `nvrtc-builtins64_128.dll` | same package | 12.8.93 | 6,356,480 | `a7afa2a40cbd3f92` (first 16 hex) |
| `LICENSE-NVIDIA-CUDA-EULA.txt` | the `LICENSE` file inside the same archive (NVIDIA Software License Agreement and CUDA Supplement) | | 63,021 | `e2c71babfd18a8e6` (first 16 hex) |
| `LICENSE-Khronos-OpenCL-Headers.txt` | Khronos OpenCL-Headers `LICENSE` (Apache License 2.0); the headers themselves are build-only | v2026.05.29 | 11,358 | `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` |
Archive the two DLLs come from (verified by `fetch-redist.sh`):
| Archive | URL | SHA-256 (from NVIDIA's `redistrib_12.8.1.json`) |
|---|---|---|
| `cuda_nvrtc-windows-x86_64-12.8.93-archive.zip` (305,588,898 bytes) | `https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/windows-x86_64/cuda_nvrtc-windows-x86_64-12.8.93-archive.zip` | `a63302a077f0248a743a1a7caa7dbd80d0fac56c6cfa9c41fa05fac9b7e5eda5` |
The archive also holds `nvrtc64_120_0.alt.dll` and static libraries; they are not shipped and not used.
## Why the two DLLs may be redistributed
The NVIDIA Software License Agreement (the `LICENSE` file above, section 2.2 "Distribution": "The portions of the SDK that
are distributable under the Agreement are listed in Attachment A") lists in Attachment A, under "NVIDIA Runtime
Compilation Library and Header":
> All: nvrtc.h
> Windows: nvrtc.dll, nvrtc-builtins.dll
and the preamble of Attachment A covers the versioned file names ("including certain variations of these files that have
version number or architecture specific information embedded in the file name"), which is what `nvrtc64_120_0.dll` and
`nvrtc-builtins64_128.dll` are. Section 2.1 limits the licence to applications "for use in systems with NVIDIA GPUs",
which is the case: the worker runs on NVIDIA GPUs only. The DLLs are shipped unmodified next to the exe. The licence text
is in the package so the user can read it.
## Build-only (compiled against, never shipped)
| File | Source | Version | SHA-256 |
|---|---|---|---|
| `cuda.h` (the driver API header) | NVIDIA CUDA redistributable, package `cuda_cudart`, archive `cuda_cudart-windows-x86_64-12.8.90-archive.zip` (3,037,735 bytes) at `https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/windows-x86_64/cuda_cudart-windows-x86_64-12.8.90-archive.zip`, archive SHA-256 `4a39058fd8519444a81cfc7ae055d136f48d1a31ffa41ae255b35b2edd61e13b` | 12.8.90 | `167e383a3791226c` (first 16 hex) |
| `nvrtc.h` | the `cuda_nvrtc` archive above (Attachment A lists it as redistributable too) | 12.8.93 | `85d79ff186392c1b` (first 16 hex) |
| `CL/cl.h` | `https://raw.githubusercontent.com/KhronosGroup/OpenCL-Headers/v2026.05.29/CL/cl.h` | v2026.05.29 | `16d09614cd7eef73b4094089cc4ce2af777181d3ffe9613473fb6d552234a4d1` |
| `CL/cl_platform.h` | same tag | | `b125cece6fe41f2e6690d88b24652c151cb6654da13b42180d69d5bd9ed526d7` |
| `CL/cl_version.h` | same tag | | `1e571dd68566330dd066cf778e07909a3cabeb2b969c5578c60f14baba7df1af` |
| `CL/cl_ext.h` | same tag (fetched for completeness; `host.c` includes `cl.h` only) | | `fe8916fd98b739cbc47ecab387bee2d258fa939c1bd2e10415c6f8f8b7055968` |
`cuda.h` is not in Attachment A, so it is used only to compile the worker on the Mac and is not in the package. The
worker never links a CUDA import library: `nvcuda.dll` (installed by every NVIDIA driver) and `nvrtc64_120_0.dll` are
opened with `LoadLibrary` and their entry points fetched with `GetProcAddress` (`cuda_api.h`, `worker.cpp`). The
OpenCL worker does the same with `OpenCL.dll`, the Khronos ICD loader that the AMD, NVIDIA and Intel drivers install
(`proto-opencl/cl_dynamic.h`).
## What the user's PC needs
An NVIDIA driver (for the CUDA worker; the driver API is in `nvcuda.dll`, and the driver must be new enough for
CUDA 12.x) or an AMD, NVIDIA or Intel driver with OpenCL (for the OpenCL worker). Windows 10 or 11 (the exes import
only `KERNEL32.dll` and the Universal CRT `api-ms-win-crt-*` libraries, which both carry). No CUDA Toolkit, no Visual
Studio, no SDK.

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Cross-compiles the two one-click Windows workers on the Mac with Homebrew mingw-w64 (x86_64-w64-mingw32-gcc/g++):
# proto-cuda/nvrtc/igneum-worker-cuda.exe driver API + NVRTC, both loaded at run time (cuda_api.h); needs cuda.h and
# nvrtc.h from fetch-redist.sh to compile, no import library
# proto-opencl/igneum-worker-opencl.exe host.c with IGNEUM_CL_DYNAMIC (OpenCL.dll loaded at run time) and the generic
# --pack serve mode; the pack it is built against is only a placeholder
# Static libgcc/libstdc++/winpthread, so each exe needs only KERNEL32 and the Universal CRT (Windows 10 and 11 have it).
# Usage: build-windows.sh (then windows-app/make-package.sh ships them with the NVRTC DLLs)
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
RED="$HERE/redist"
[ -f "$RED/include/cuda.h" ] && [ -f "$RED/include/nvrtc.h" ] && [ -f "$RED/include/CL/cl.h" ] || { echo "run $HERE/fetch-redist.sh first" >&2; exit 1; }
CXX=x86_64-w64-mingw32-g++
CC=x86_64-w64-mingw32-gcc
STRIP=x86_64-w64-mingw32-strip
PLACEHOLDER="$ROOT/proto-cuda/packs/igneum-devnet-v4-epoch0"
echo "== igneum-worker-cuda.exe"
"$CXX" -std=c++17 -O2 -Wall -Wextra -static -I "$HERE" -I "$RED/include" -o "$HERE/igneum-worker-cuda.exe" "$HERE/worker.cpp"
"$STRIP" "$HERE/igneum-worker-cuda.exe"
echo "== igneum-worker-opencl.exe"
"$CC" -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 \
-I "$RED/include" -I "$PLACEHOLDER" -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' \
-o "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$ROOT/proto-opencl/host.c"
"$STRIP" "$ROOT/proto-opencl/igneum-worker-opencl.exe"
for exe in "$HERE/igneum-worker-cuda.exe" "$ROOT/proto-opencl/igneum-worker-opencl.exe"; do
printf '%s: %d bytes, imports:' "$(basename "$exe")" "$(stat -f %z "$exe")"
x86_64-w64-mingw32-objdump -p "$exe" | sed -n 's/^[[:space:]]*DLL Name: //p' | tr '\n' ' '
echo
done

View file

@ -0,0 +1,61 @@
// cuda_api.h: the slice of the CUDA driver API and of NVRTC that igneum-worker-cuda calls, as function pointers.
// worker.cpp fills them from nvcuda.dll (the driver) and nvrtc64_*.dll (shipped next to the exe) with
// LoadLibrary/GetProcAddress, so no import library is linked and the exe cross-compiles with mingw on the Mac.
// The emulation build (emu/emu_backend.cpp) fills the same table with host functions. 4 October 2026.
#pragma once
#include <cuda.h>
#include <nvrtc.h>
struct Drv {
decltype(&cuInit) init = nullptr;
decltype(&cuDriverGetVersion) driverGetVersion = nullptr;
decltype(&cuDeviceGetCount) deviceGetCount = nullptr;
decltype(&cuDeviceGet) deviceGet = nullptr;
decltype(&cuDeviceGetName) deviceGetName = nullptr;
decltype(&cuDeviceGetAttribute) deviceGetAttribute = nullptr;
decltype(&cuDeviceTotalMem_v2) deviceTotalMem = nullptr;
decltype(&cuDevicePrimaryCtxSetFlags_v2) primaryCtxSetFlags = nullptr;
decltype(&cuDevicePrimaryCtxRetain) primaryCtxRetain = nullptr;
decltype(&cuDevicePrimaryCtxRelease_v2) primaryCtxRelease = nullptr;
decltype(&cuCtxSetCurrent) ctxSetCurrent = nullptr;
decltype(&cuCtxSynchronize) ctxSynchronize = nullptr;
decltype(&cuMemGetInfo_v2) memGetInfo = nullptr;
decltype(&cuMemAlloc_v2) memAlloc = nullptr;
decltype(&cuMemFree_v2) memFree = nullptr;
decltype(&cuMemcpyDtoH_v2) memcpyDtoH = nullptr;
decltype(&cuModuleLoadData) moduleLoadData = nullptr;
decltype(&cuModuleUnload) moduleUnload = nullptr;
decltype(&cuModuleGetFunction) moduleGetFunction = nullptr;
decltype(&cuLaunchKernel) launchKernel = nullptr;
decltype(&cuStreamCreate) streamCreate = nullptr;
decltype(&cuStreamSynchronize) streamSynchronize = nullptr;
decltype(&cuStreamDestroy_v2) streamDestroy = nullptr;
decltype(&cuFuncGetAttribute) funcGetAttribute = nullptr;
decltype(&cuOccupancyMaxActiveBlocksPerMultiprocessor) occupancy = nullptr;
decltype(&cuGetErrorString) getErrorString = nullptr;
decltype(&cuGetErrorName) getErrorName = nullptr;
};
struct Rtc {
decltype(&nvrtcVersion) version = nullptr;
decltype(&nvrtcGetNumSupportedArchs) getNumSupportedArchs = nullptr; // CUDA 11.2 and newer; optional
decltype(&nvrtcGetSupportedArchs) getSupportedArchs = nullptr;
decltype(&nvrtcCreateProgram) createProgram = nullptr;
decltype(&nvrtcDestroyProgram) destroyProgram = nullptr;
decltype(&nvrtcCompileProgram) compileProgram = nullptr;
decltype(&nvrtcGetProgramLogSize) getProgramLogSize = nullptr;
decltype(&nvrtcGetProgramLog) getProgramLog = nullptr;
decltype(&nvrtcGetPTXSize) getPTXSize = nullptr;
decltype(&nvrtcGetPTX) getPTX = nullptr;
decltype(&nvrtcGetCUBINSize) getCUBINSize = nullptr;
decltype(&nvrtcGetCUBIN) getCUBIN = nullptr;
decltype(&nvrtcAddNameExpression) addNameExpression = nullptr;
decltype(&nvrtcGetLoweredName) getLoweredName = nullptr;
decltype(&nvrtcGetErrorString) getErrorString = nullptr;
};
#ifdef IGNEUM_EMU
// emu/emu_backend.cpp: host functions in place of the two libraries (the pack's kernels run on host threads)
void emu_fill_driver(Drv& d);
void emu_fill_nvrtc(Rtc& r);
#endif

View file

@ -0,0 +1,213 @@
// emu_backend.cpp: the CUDA driver API and NVRTC replaced by host functions, for checking igneum-worker-cuda on a
// machine without an NVIDIA GPU (the Mac). Not part of the deliverable. 4 October 2026.
//
// What it proves: the worker's protocol, job walk, init words, pack reading, self-test, prepare thread and swap,
// with the pack's real kernel text running on host threads through proto-cuda/emu (the same shim the host.cu
// emulation uses, 32 threads per warp, a barrier inside __shfl_xor_sync). What it cannot prove: that NVRTC accepts
// the text and that the driver runs it; that is the RTX 5090 run (windows-app/TEST.md).
//
// The NVRTC stand-in records every program the worker creates and checks it against the pack on disk: the source
// must be the pack's kernel file up to the host launch wrappers, byte for byte, the dropped tail must hold host
// wrappers only, and program.h and memhard.h must be the pack's files byte for byte. Two packs may be compiled in
// (IGNEUM_EMU_PACK and IGNEUM_EMU_PACK2, the directories), so a prepare and the swap run with real second-pack code.
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <list>
#include <map>
#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
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);
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);
void igneum_hash_bound(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw);
}
#endif
static std::string readAll(const std::string& path) {
FILE* f = std::fopen(path.c_str(), "rb");
if (!f) return std::string();
std::string s;
char buf[65536];
size_t n;
while ((n = std::fread(buf, 1, sizeof(buf), f)) > 0) s.append(buf, n);
std::fclose(f);
return s;
}
// ---------------------------------------------------------------------------------------------
// NVRTC stand-in
struct EmuProg {
std::string src, name, log;
std::map<std::string, std::string> headers;
std::list<std::string> exprs;
int pack = 0; // 1 = IGNEUM_EMU_PACK, 2 = IGNEUM_EMU_PACK2, 0 = unknown
bool checked = false, ok = false, compiled = false;
};
static const char* packDir(int which) {
const char* e = std::getenv(which == 1 ? "IGNEUM_EMU_PACK" : "IGNEUM_EMU_PACK2");
return e ? e : "";
}
// The source check. Fills p->pack, p->ok, p->log.
static void checkSource(EmuProg* p) {
p->checked = true;
for (int which = 1; which <= 2; ++which) {
std::string dir = packDir(which);
if (dir.empty()) continue;
std::string file = readAll(dir + "/" + p->name);
if (file.empty() || file.size() < p->src.size() || file.compare(0, p->src.size(), p->src) != 0) continue;
std::string tail = file.substr(p->src.size());
bool tailOk = tail.empty() || tail.rfind("// Host-side launch wrappers", 0) == 0 || tail.rfind("cudaError_t ", 0) == 0;
tailOk = tailOk && tail.find("__global__") == std::string::npos && tail.find("__device__") == std::string::npos;
if (!tailOk) { p->log = "emu-nvrtc: the dropped tail of " + p->name + " is not the host launch wrappers"; p->ok = false; p->pack = which; return; }
std::string ph = readAll(dir + "/program.h"), mh = readAll(dir + "/memhard.h");
if (!p->headers.count("program.h") || p->headers["program.h"] != ph) { p->log = "emu-nvrtc: program.h handed to NVRTC differs from the pack's"; p->ok = false; p->pack = which; return; }
if (!p->headers.count("memhard.h") || p->headers["memhard.h"] != mh) { p->log = "emu-nvrtc: memhard.h handed to NVRTC differs from the pack's"; p->ok = false; p->pack = which; return; }
if (!p->headers.count("cuda_runtime.h") || !p->headers.count("cstdint")) { p->log = "emu-nvrtc: the stub headers cuda_runtime.h and cstdint were not handed over"; p->ok = false; p->pack = which; return; }
p->pack = which; p->ok = true;
std::printf("info emu-nvrtc: %s source check PASS for pack %d: %zu bytes handed over = the pack file's first %zu of %zu bytes; dropped tail = host launch wrappers only (%zu bytes); program.h (%zu bytes) and memhard.h (%zu bytes) byte-identical\n",
p->name.c_str(), which, p->src.size(), p->src.size(), file.size(), tail.size(), ph.size(), mh.size());
std::fflush(stdout);
return;
}
p->log = "emu-nvrtc: the source of " + p->name + " is not a prefix of any pack's file (IGNEUM_EMU_PACK / IGNEUM_EMU_PACK2)";
p->ok = false;
}
static nvrtcResult e_version(int* major, int* minor) { *major = 12; *minor = 8; return NVRTC_SUCCESS; }
static int ARCHS[] = { 50, 52, 53, 60, 61, 62, 70, 72, 75, 80, 86, 87, 89, 90, 100, 101, 120 };
static nvrtcResult e_numArchs(int* n) { *n = (int)(sizeof(ARCHS) / sizeof(ARCHS[0])); return NVRTC_SUCCESS; }
static nvrtcResult e_archs(int* out) { for (size_t i = 0; i < sizeof(ARCHS) / sizeof(ARCHS[0]); ++i) out[i] = ARCHS[i]; return NVRTC_SUCCESS; }
static nvrtcResult e_create(nvrtcProgram* prog, const char* src, const char* name, int numHeaders, const char* const* headers, const char* const* includeNames) {
EmuProg* p = new EmuProg();
p->src = src ? src : ""; p->name = name ? name : "";
for (int i = 0; i < numHeaders; ++i) p->headers[includeNames[i]] = headers[i];
*prog = (nvrtcProgram)p;
return NVRTC_SUCCESS;
}
static nvrtcResult e_destroy(nvrtcProgram* prog) { delete (EmuProg*)*prog; *prog = nullptr; return NVRTC_SUCCESS; }
static nvrtcResult e_compile(nvrtcProgram prog, int numOptions, const char* const* options) {
EmuProg* p = (EmuProg*)prog;
bool arch = false, std17 = false;
for (int i = 0; i < numOptions; ++i) { std::string o = options[i]; if (o.rfind("--gpu-architecture=", 0) == 0) arch = true; if (o == "--std=c++17") std17 = true; }
if (!arch || !std17) { p->log = "emu-nvrtc: expected --gpu-architecture=... and --std=c++17"; return NVRTC_ERROR_COMPILATION; }
checkSource(p);
if (!p->ok) return NVRTC_ERROR_COMPILATION;
p->compiled = true;
return NVRTC_SUCCESS;
}
static nvrtcResult e_logSize(nvrtcProgram prog, size_t* n) { *n = ((EmuProg*)prog)->log.size() + 1; return NVRTC_SUCCESS; }
static nvrtcResult e_log(nvrtcProgram prog, char* out) { std::strcpy(out, ((EmuProg*)prog)->log.c_str()); return NVRTC_SUCCESS; }
static std::string image(EmuProg* p) { return std::string("EMU-IMAGE:") + (p->pack == 2 ? "B" : "A"); }
static nvrtcResult e_imgSize(nvrtcProgram prog, size_t* n) { *n = image((EmuProg*)prog).size() + 1; return NVRTC_SUCCESS; }
static nvrtcResult e_img(nvrtcProgram prog, char* out) { std::strcpy(out, image((EmuProg*)prog).c_str()); return NVRTC_SUCCESS; }
static nvrtcResult e_addName(nvrtcProgram prog, const char* e) { ((EmuProg*)prog)->exprs.push_back(e); return NVRTC_SUCCESS; }
static nvrtcResult e_lowered(nvrtcProgram prog, const char* e, const char** out) {
EmuProg* p = (EmuProg*)prog;
for (const std::string& s : p->exprs) if (s == e) { *out = s.c_str(); return NVRTC_SUCCESS; }
return NVRTC_ERROR_NAME_EXPRESSION_NOT_VALID;
}
static const char* e_errstr(nvrtcResult r) { return r == NVRTC_SUCCESS ? "NVRTC_SUCCESS" : r == NVRTC_ERROR_COMPILATION ? "NVRTC_ERROR_COMPILATION" : "NVRTC_ERROR (emulated)"; }
void emu_fill_nvrtc(Rtc& r) {
r.version = e_version; r.getNumSupportedArchs = e_numArchs; r.getSupportedArchs = e_archs;
r.createProgram = e_create; r.destroyProgram = e_destroy; r.compileProgram = e_compile;
r.getProgramLogSize = e_logSize; r.getProgramLog = e_log;
r.getPTXSize = e_imgSize; r.getPTX = e_img; r.getCUBINSize = e_imgSize; r.getCUBIN = e_img;
r.addNameExpression = e_addName; r.getLoweredName = e_lowered; r.getErrorString = e_errstr;
}
// ---------------------------------------------------------------------------------------------
// Driver API stand-in
static CUresult d_init(unsigned) { return CUDA_SUCCESS; }
static CUresult d_driverVersion(int* v) { *v = 12080; return CUDA_SUCCESS; }
static CUresult d_count(int* c) { *c = 1; return CUDA_SUCCESS; }
static CUresult d_get(CUdevice* d, int i) { *d = i; return CUDA_SUCCESS; }
static CUresult d_name(char* out, int len, CUdevice) { std::snprintf(out, (size_t)len, "CPU emulation shim (not a GPU)"); return CUDA_SUCCESS; }
static CUresult d_attr(int* v, CUdevice_attribute a, CUdevice) {
switch (a) {
case CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR: *v = 12; break;
case CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR: *v = 0; break;
case CU_DEVICE_ATTRIBUTE_WARP_SIZE: *v = 32; break;
default: *v = 0; break;
}
return CUDA_SUCCESS;
}
static CUresult d_totalMem(size_t* b, CUdevice) { *b = 16ull << 30; return CUDA_SUCCESS; }
static CUresult d_ctxFlags(CUdevice, unsigned) { return CUDA_SUCCESS; }
static int gCtx = 0;
static CUresult d_ctxRetain(CUcontext* c, CUdevice) { *c = (CUcontext)&gCtx; return CUDA_SUCCESS; }
static CUresult d_ctxRelease(CUdevice) { return CUDA_SUCCESS; }
static CUresult d_ctxSet(CUcontext) { return CUDA_SUCCESS; }
static CUresult d_ctxSync() { return CUDA_SUCCESS; }
static CUresult d_memInfo(size_t* f, size_t* t) { *f = 8ull << 30; *t = 16ull << 30; return CUDA_SUCCESS; }
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_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;
*m = (CUmodule)(uintptr_t)(s[10] == 'B' ? 2 : 1);
return CUDA_SUCCESS;
}
static CUresult d_modUnload(CUmodule) { return CUDA_SUCCESS; }
static CUresult d_getFn(CUfunction* f, CUmodule m, const char* name) {
int pack = (int)(uintptr_t)m, idx;
if (std::strcmp(name, "igneum_cache_fill") == 0) idx = 1;
else if (std::strcmp(name, "igneum_build") == 0) idx = 2;
else if (std::strcmp(name, "igneum_hash_bound") == 0) idx = 3;
else return CUDA_ERROR_NOT_FOUND;
#ifndef IGNEUM_EMU_TWO_PACKS
if (pack == 2) return CUDA_ERROR_NOT_FOUND;
#endif
*f = (CUfunction)(uintptr_t)((pack - 1) * 3 + idx);
return CUDA_SUCCESS;
}
template <class T> static T arg(void** params, int i) { return *(T*)params[i]; }
template <class T> static T* dptr(void** params, int i) { return (T*)(uintptr_t)(*(CUdeviceptr*)params[i]); }
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 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 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;
}
}
static CUresult d_streamCreate(CUstream* s, unsigned) { *s = (CUstream)&gCtx; return CUDA_SUCCESS; }
static CUresult d_streamSync(CUstream) { return CUDA_SUCCESS; }
static CUresult d_streamDestroy(CUstream) { return CUDA_SUCCESS; }
static CUresult d_funcAttr(int* v, CUfunction_attribute, CUfunction) { *v = 0; return CUDA_SUCCESS; }
static CUresult d_occ(int* n, CUfunction, int, size_t) { *n = 0; return CUDA_SUCCESS; }
static CUresult d_errStr(CUresult r, const char** s) { *s = r == CUDA_SUCCESS ? "no error" : "emulated driver error"; return CUDA_SUCCESS; }
static CUresult d_errName(CUresult r, const char** s) { *s = r == CUDA_SUCCESS ? "CUDA_SUCCESS" : "CUDA_ERROR (emulated)"; return CUDA_SUCCESS; }
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.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

@ -0,0 +1,62 @@
#!/usr/bin/env bash
# The protocol check shared by the CUDA emulation (test.sh) and the OpenCL generic worker (proto-opencl/test-generic.sh):
# drives one worker through two jobs on pack A (one across the 32-bit nonce boundary), a background prepare of pack B,
# the swap, a refused job on A after the swap, and compares a sample of the found hashes with igneum-pow hash-bound.
# Usage: serve-check.sh <pack A dir> <pack B dir> <log file> <worker command...>
# The worker command is run as given plus the stdin script; it must already carry --serve --pack <pack A> etc.
set -euo pipefail
PACK_A="$1"; PACK_B="$2"; LOG="$3"; shift 3
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../../.." && pwd)"
POW="$ROOT/igneum-pow/target/release/igneum-pow"
seedline() { sed -n "s/^$2 //p" "$1/seeds.txt"; }
A_EPOCH="$(seedline "$PACK_A" epoch_seed_hex)"; A_DAY="$(seedline "$PACK_A" day_seed_hex)"
B_EPOCH="$(seedline "$PACK_B" epoch_seed_hex)"; B_DAY="$(seedline "$PACK_B" day_seed_hex)"
WAIT="${IGNEUM_PREPARE_WAIT:-40}"
PRE="0000000000000000000000000000000000000000000000000000000000000001"
{
echo "job 1 $PRE ffffffffffffffff 0 64 $A_EPOCH $A_DAY"
echo "job 2 $PRE ffffffffffffffff 4294967264 64 $A_EPOCH $A_DAY"
echo "prepare $B_EPOCH $B_DAY $PACK_B"
sleep "$WAIT"
echo "job 3 $PRE ffffffffffffffff 128 32 $A_EPOCH $A_DAY"
sleep 1
echo "job 4 $PRE ffffffffffffffff 0 32 $B_EPOCH $B_DAY"
echo "job 5 $PRE ffffffffffffffff 96 32 $A_EPOCH $A_DAY"
echo "quit"
} | "$@" | tee "$LOG"
echo "== verdict"
bad=0; n=0
check() { # job nonce epoch day
want="$("$POW" hash-bound --prehash "$PRE" --nonce "$2" --epoch-hex "$3" --day-hex "$4" | tail -1)"
got="$(grep "^found $1 $2 " "$LOG" | awk '{print $4}')"
n=$((n + 1))
if [ "$want" != "$got" ]; then echo "MISMATCH job $1 nonce $2: worker $got cpu $want"; bad=$((bad + 1)); fi
}
grep -q '^ready .* prepare 1 ' "$LOG" || { echo "FAIL: no ready line with prepare 1"; exit 1; }
[ "$(grep -c '^found 1 ' "$LOG")" = 64 ] || { echo "FAIL: job 1 should find 64"; exit 1; }
[ "$(grep -c '^found 2 ' "$LOG")" = 64 ] || { echo "FAIL: job 2 should find 64"; exit 1; }
[ "$(grep -c '^found 3 ' "$LOG")" = 32 ] || { echo "FAIL: job 3 should find 32"; exit 1; }
[ "$(grep -c '^found 4 ' "$LOG")" = 32 ] || { echo "FAIL: job 4 should find 32 (was pack B prepared in time? IGNEUM_PREPARE_WAIT=$WAIT s)"; exit 1; }
grep -q "^prepared $B_EPOCH $B_DAY .*self-test PASS" "$LOG" || { echo "FAIL: no prepared line with self-test PASS"; exit 1; }
grep -q '^info switched to the prepared pair' "$LOG" || { echo "FAIL: no swap"; exit 1; }
# Job 5 is on pack A after the swap dropped it: an ahead-of-time worker refuses it (OpenCL host.c), the CUDA worker
# self-heals by finding pack A next to its first pack and building it again in the foreground (32 found, 5 done lines).
if grep -q '^error 5 epoch seed mismatch' "$LOG"; then
job5="refused (no pair for its seeds)"
[ "$(grep -c '^done ' "$LOG")" = 4 ] || { echo "FAIL: 4 done lines expected"; exit 1; }
elif grep -q '^info job 5 is for epoch .* building its pack' "$LOG" && [ "$(grep -c '^found 5 ' "$LOG")" = 32 ]; then
job5="served after the worker rebuilt pack A by itself (self-heal)"
[ "$(grep -c '^done ' "$LOG")" = 5 ] || { echo "FAIL: 5 done lines expected"; exit 1; }
for x in 96 127; do check 5 $x "$A_EPOCH" "$A_DAY"; done 2>/dev/null || true
else
echo "FAIL: job 5 was neither refused nor self-healed"; exit 1
fi
grep -q '^found 2 4294967295 ' "$LOG" && grep -q '^found 2 4294967296 ' "$LOG" || { echo "FAIL: the 32-bit boundary nonces are missing"; exit 1; }
for x in 0 1 2 31 32 63; do check 1 $x "$A_EPOCH" "$A_DAY"; done
for x in 4294967264 4294967295 4294967296 4294967327; do check 2 $x "$A_EPOCH" "$A_DAY"; done
for x in 128 159; do check 3 $x "$A_EPOCH" "$A_DAY"; done
for x in 0 7 31; do check 4 $x "$B_EPOCH" "$B_DAY"; done
[ "$bad" = 0 ] || { echo "FAIL: $bad of $n sampled hashes differ from igneum-pow"; exit 1; }
echo "PASS: ready + prepare 1; 64 + 64 + 32 found on pack A, prepared B with self-test PASS, swapped, 32 found on B, job 5 $job5; $n sampled hashes (both packs, both sides of the 32-bit nonce boundary) equal igneum-pow hash-bound"

61
proto-cuda/nvrtc/emu/test.sh Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Checks igneum-worker-cuda on the Mac (no NVIDIA GPU): the worker is built with the emulation backend, so the CUDA
# driver API and NVRTC are host functions and the packs' real kernel text runs on host threads (proto-cuda/emu shim).
# What is checked: (1) the source handed to NVRTC equals the pack's kernel files up to the host launch wrappers, with
# program.h and memhard.h byte-identical (the backend prints a PASS line per file); (2) --check self-tests pack A
# against its vectors.h; (3) --serve: jobs, a job across the 32-bit nonce boundary, a prepare of pack B in the
# background, the swap, the mismatch error after the swap; (4) every found hash of a sample of nonces equals
# igneum-pow hash-bound (the Rust CPU reference, the same code the miner re-checks shares with).
# What it cannot check: that the real NVRTC compiles the text and that the real driver runs it (windows-app/TEST.md).
# Usage: emu/test.sh [pack A dir] (default proto-cuda/packs/igneum-devnet-v4-epoch0; needs fetch-redist.sh first)
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
NV="$(cd "$HERE/.." && pwd)"
ROOT="$(cd "$NV/../.." && pwd)"
EMU="$ROOT/proto-cuda/emu"
INC="${IGNEUM_CUDA_INC:-$NV/redist/include}"
POW="$ROOT/igneum-pow/target/release/igneum-pow"
OUT="$HERE/build"
PACK_A_SRC="${1:-$ROOT/proto-cuda/packs/igneum-devnet-v4-epoch0}"
[ -f "$INC/cuda.h" ] && [ -f "$INC/nvrtc.h" ] || { echo "no cuda.h / nvrtc.h under $INC: run $NV/fetch-redist.sh first" >&2; exit 1; }
[ -x "$POW" ] || (cd "$ROOT/igneum-pow" && cargo build --release 2>&1 | tail -2)
CXX="${CXX:-c++}"
rm -rf "$OUT"; mkdir -p "$OUT"
# Pack A: a copy with seeds.txt as igneum-miner export-pack writes it. Pack B: a different epoch and day, exported now.
cp -R "$PACK_A_SRC" "$OUT/pack-a"
A_EPOCH="$(sed -n 's/^#define IGNEUM_SEED_BYTES_HEX "\(.*\)"/\1/p' "$OUT/pack-a/program.h")"
A_DAY="$(sed -n 's/^#define IGNEUM_DAY_BYTES_HEX "\(.*\)"/\1/p' "$OUT/pack-a/program.h")"
[ -n "$A_EPOCH" ] && [ -n "$A_DAY" ] || { echo "pack A has no byte seeds in program.h" >&2; exit 1; }
printf 'epoch_seed_hex %s\nday_seed_hex %s\nepoch_index 0\nday_index 0\ndaa_score 0\n' "$A_EPOCH" "$A_DAY" > "$OUT/pack-a/seeds.txt"
B_EPOCH="5e5d0c3b2a19f8e7d6c5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918f7e6"
B_DAY="69676e65756d2d6461792ffa51000000000000"
"$POW" export --epoch-hex "$B_EPOCH" --day-hex "$B_DAY" --out "$OUT/pack-b" > "$OUT/export-b.log"; head -2 "$OUT/export-b.log"
printf 'epoch_seed_hex %s\nday_seed_hex %s\nday_index 1\n' "$B_EPOCH" "$B_DAY" > "$OUT/pack-b/seeds.txt"
# The kernels as the emulation runs them: the launch syntax rewritten (as emu/emu.sh does), each pack in its own namespace.
emu_kernel() { # pack dir, namespace, file stem
{ 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 "}"; } > "$OUT/$3_$2.cpp"
"$CXX" -std=c++17 -O2 -w -I "$EMU" -I "$1" -c "$OUT/$3_$2.cpp" -o "$OUT/$3_$2.o"
}
emu_kernel "$OUT/pack-a" emu_pack_a kernel
emu_kernel "$OUT/pack-a" emu_pack_a kernel_bound
emu_kernel "$OUT/pack-b" emu_pack_b kernel
emu_kernel "$OUT/pack-b" emu_pack_b kernel_bound
"$CXX" -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -I "$NV" -I "$INC" -c "$NV/worker.cpp" -o "$OUT/worker.o"
"$CXX" -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -DIGNEUM_EMU_TWO_PACKS -I "$NV" -I "$EMU" -I "$INC" -c "$HERE/emu_backend.cpp" -o "$OUT/emu_backend.o"
"$CXX" -std=c++17 -O2 -w -I "$EMU" -c "$EMU/shim.cpp" -o "$OUT/shim.o"
"$CXX" -o "$OUT/igneum-worker-cuda-emu" "$OUT"/*.o -pthread
echo "built $OUT/igneum-worker-cuda-emu (CPU emulation, not a GPU build)"
export IGNEUM_EMU_PACK="$OUT/pack-a" IGNEUM_EMU_PACK2="$OUT/pack-b"
echo "== --check pack A"
"$OUT/igneum-worker-cuda-emu" --check --pack "$OUT/pack-a" | tee "$OUT/check-a.log"
grep -q '^check PASS' "$OUT/check-a.log"
grep -c 'source check PASS' "$OUT/check-a.log" | grep -qx 2
echo "== --serve: two jobs on A, prepare B, a job on B (swap), a job on A after the swap (error), quit"
"$HERE/serve-check.sh" "$OUT/pack-a" "$OUT/pack-b" "$OUT/serve.log" "$OUT/igneum-worker-cuda-emu" --serve --pack "$OUT/pack-a" --batch-log2 13
echo "NVRTC source check PASS for $(grep -c 'source check PASS' "$OUT/serve.log") files in --serve, $(grep -c 'source check PASS' "$OUT/check-a.log") in --check"

View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Fetches the third-party pieces the one-click workers are built and packaged with, verifies them, and stages them
# under proto-cuda/nvrtc/redist/ (gitignored). Nothing third-party is committed. THIRD-PARTY.md records every file.
# NVIDIA cuda_nvrtc 12.8.93 (Windows x86_64): nvrtc64_120_0.dll and nvrtc-builtins64_128.dll (shipped), nvrtc.h (build), LICENSE
# NVIDIA cuda_cudart 12.8.90 (Windows x86_64): cuda.h (build only, the driver API header; not shipped)
# Khronos OpenCL-Headers v2026.05.29: CL/cl.h, cl_platform.h, cl_version.h, cl_ext.h (build only), LICENSE (Apache 2.0)
# The archives are cached in ~/.cache/igneum/ (the nvrtc archive is 305 MB). Usage: fetch-redist.sh
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
OUT="$HERE/redist"
CACHE="${IGNEUM_REDIST_CACHE:-$HOME/.cache/igneum}"
BASE="https://developer.download.nvidia.com/compute/cuda/redist"
NVRTC_REL="cuda_nvrtc/windows-x86_64/cuda_nvrtc-windows-x86_64-12.8.93-archive.zip"
NVRTC_SHA="a63302a077f0248a743a1a7caa7dbd80d0fac56c6cfa9c41fa05fac9b7e5eda5"
CUDART_REL="cuda_cudart/windows-x86_64/cuda_cudart-windows-x86_64-12.8.90-archive.zip"
CUDART_SHA="4a39058fd8519444a81cfc7ae055d136f48d1a31ffa41ae255b35b2edd61e13b"
KHR_TAG="v2026.05.29"
KHR_BASE="https://raw.githubusercontent.com/KhronosGroup/OpenCL-Headers/$KHR_TAG"
# sha256 of the Khronos files at that tag, recorded 4 October 2026 (file sha256, one per line; bash 3 has no maps)
KHR_FILES="CL/cl.h 16d09614cd7eef73b4094089cc4ce2af777181d3ffe9613473fb6d552234a4d1
CL/cl_platform.h b125cece6fe41f2e6690d88b24652c151cb6654da13b42180d69d5bd9ed526d7
CL/cl_version.h 1e571dd68566330dd066cf778e07909a3cabeb2b969c5578c60f14baba7df1af
CL/cl_ext.h fe8916fd98b739cbc47ecab387bee2d258fa939c1bd2e10415c6f8f8b7055968
LICENSE cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"
sha() { shasum -a 256 "$1" | cut -d' ' -f1; }
fetch() { # url dest sha
if [ ! -f "$2" ] || [ "$(sha "$2")" != "$3" ]; then
echo "downloading $(basename "$2")"
curl -sS -L -m 3600 -o "$2.part" "$1" && mv "$2.part" "$2"
fi
[ "$(sha "$2")" = "$3" ] || { echo "sha256 mismatch for $2" >&2; exit 1; }
}
mkdir -p "$CACHE/cuda-redist" "$CACHE/opencl-headers/CL" "$OUT/include/CL" "$OUT/bin"
fetch "$BASE/$NVRTC_REL" "$CACHE/cuda-redist/$(basename "$NVRTC_REL")" "$NVRTC_SHA"
fetch "$BASE/$CUDART_REL" "$CACHE/cuda-redist/$(basename "$CUDART_REL")" "$CUDART_SHA"
echo "$KHR_FILES" | while read -r f h; do fetch "$KHR_BASE/$f" "$CACHE/opencl-headers/$f" "$h"; done
X="$(mktemp -d)"
unzip -q "$CACHE/cuda-redist/$(basename "$NVRTC_REL")" -d "$X"
unzip -q "$CACHE/cuda-redist/$(basename "$CUDART_REL")" -d "$X"
N="$X/cuda_nvrtc-windows-x86_64-12.8.93-archive"
C="$X/cuda_cudart-windows-x86_64-12.8.90-archive"
cp "$N/bin/nvrtc64_120_0.dll" "$N/bin/nvrtc-builtins64_128.dll" "$OUT/bin/"
cp "$N/include/nvrtc.h" "$C/include/cuda.h" "$OUT/include/"
cp "$N/LICENSE" "$OUT/LICENSE-NVIDIA-CUDA-EULA.txt"
cp "$CACHE"/opencl-headers/CL/*.h "$OUT/include/CL/"
cp "$CACHE/opencl-headers/LICENSE" "$OUT/LICENSE-Khronos-OpenCL-Headers.txt"
rm -rf "$X"
echo "staged in $OUT:"
(cd "$OUT" && find . -type f | sort | while read -r f; do printf ' %10d %s %s\n' "$(stat -f %z "$f")" "$(sha "$f" | cut -c1-16)" "$f"; done)

350
proto-cuda/nvrtc/packfile.h Normal file
View file

@ -0,0 +1,350 @@
// packfile.h: read a program pack at run time (C99, header only). 4 October 2026.
//
// The one-click workers (proto-cuda/nvrtc/worker.cpp and proto-opencl/host.c with --pack) are built once and serve
// any pack, so the constants that host.cu and host.c take from program.h and vectors.h at compile time are read
// here from the same files at run time: the dataset and cache sizes, the seed words, the seeds as hex (seeds.txt,
// written by igneum-miner export-pack and --prepare-packs; program.h carries the same bytes), and the self-test
// values (cache head, last line and FNV-1a 64; dataset head, last word and 64 samples; the three vector warps).
// No JSON parser: the headers are scanned for "#define NAME value" and "NAME[..] = { numbers }". Comments are skipped.
//
// Included by C (proto-opencl/host.c) and C++ (worker.cpp). Everything is static.
#ifndef IGNEUM_PACKFILE_H
#define IGNEUM_PACKFILE_H
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define PF_MAX_WARPS 8
#define PF_MAX_SAMPLES 256
#define PF_HEX_CAP 520
typedef struct {
// program.h
uint32_t datasetLog2, cacheLog2Words, cacheSegments, datasetMode, generator;
uint32_t seedw[8], keyw[8];
char seedString[600];
// seeds.txt (or program.h): the seeds as the worker protocol carries them
char epochHex[65];
char dayHex[PF_HEX_CAP];
// vectors.h
int haveVectors;
int vecWarps;
uint32_t vecBase[PF_MAX_WARPS];
uint64_t vecOut[PF_MAX_WARPS][32];
uint32_t dsHead[16];
uint32_t dsLastIndex, dsLast;
int nSamples;
uint32_t sampleIdx[PF_MAX_SAMPLES], sampleVal[PF_MAX_SAMPLES];
uint32_t cacheHead[16], cacheLast[16];
uint64_t cacheFnv;
} PfPack;
static char* pf_read_file(const char* path, size_t* len) {
FILE* f = fopen(path, "rb");
char* buf;
long n;
if (!f) return NULL;
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
if (n < 0) { fclose(f); return NULL; }
buf = (char*)malloc((size_t)n + 1);
if (!buf) { fclose(f); return NULL; }
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return NULL; }
buf[n] = 0;
fclose(f);
if (len) *len = (size_t)n;
return buf;
}
static char* pf_read_pack_file(const char* dir, const char* name, size_t* len) {
char path[2048];
snprintf(path, sizeof(path), "%s/%s", dir, name);
return pf_read_file(path, len);
}
// The text after "#define NAME" (NAME followed by a space or tab) at the start of a line, or NULL.
static const char* pf_find_define(const char* text, const char* name) {
size_t n = strlen(name);
const char* p = text;
while ((p = strstr(p, "#define ")) != NULL) {
const char* q = p + 8;
if (p == text || p[-1] == '\n' || p[-1] == '\r') {
if (strncmp(q, name, n) == 0 && (q[n] == ' ' || q[n] == '\t')) return q + n;
}
p = q;
}
return NULL;
}
// A number with an optional C suffix (u, U, ull, ULL, l, L). *end is left after the suffix.
static int pf_number(const char* s, uint64_t* out, const char** end) {
char* e = NULL;
unsigned long long v;
if (!(isdigit((unsigned char)s[0]))) return 0;
v = strtoull(s, &e, 0);
if (e == s) return 0;
while (*e == 'u' || *e == 'U' || *e == 'l' || *e == 'L') ++e;
*out = (uint64_t)v;
*end = e;
return 1;
}
static int pf_define_u32(const char* text, const char* name, uint32_t* out) {
const char* p = pf_find_define(text, name);
uint64_t v;
const char* e;
if (!p) return 0;
while (*p == ' ' || *p == '\t') ++p;
if (!pf_number(p, &v, &e)) return 0;
*out = (uint32_t)v;
return 1;
}
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;
size_t n;
if (!p) return 0;
while (*p == ' ' || *p == '\t') ++p;
if (*p != '"') return 0;
++p;
q = strchr(p, '"');
if (!q) return 0;
n = (size_t)(q - p);
if (n + 1 > cap) n = cap - 1;
memcpy(out, p, n); out[n] = 0;
return 1;
}
// Numbers inside the braces that follow `start`, comments and commas skipped, nested braces flattened.
// Returns the count read (at most max). `start` points at or before the opening brace.
static int pf_brace_numbers(const char* start, uint64_t* out, int max) {
const char* p = start;
int depth = 0, n = 0;
while (*p && *p != '{') { if (*p == ';') return 0; ++p; }
if (*p != '{') return 0;
for (;;) {
uint64_t v;
const char* e;
if (!*p) break;
if (*p == '/' && p[1] == '/') { while (*p && *p != '\n') ++p; continue; }
if (*p == '/' && p[1] == '*') { const char* c = strstr(p + 2, "*/"); if (!c) break; p = c + 2; continue; }
if (*p == '{') { ++depth; ++p; continue; }
if (*p == '}') { --depth; ++p; if (depth == 0) break; continue; }
if (pf_number(p, &v, &e)) { if (n < max) out[n] = v; ++n; p = e; continue; }
++p;
}
return n < max ? n : max;
}
static int pf_define_words(const char* text, const char* name, uint32_t* out, int max) {
const char* p = pf_find_define(text, name);
uint64_t tmp[PF_MAX_SAMPLES];
int n, i;
if (!p) return 0;
if (max > PF_MAX_SAMPLES) max = PF_MAX_SAMPLES;
n = pf_brace_numbers(p, tmp, max);
for (i = 0; i < n; ++i) out[i] = (uint32_t)tmp[i];
return n;
}
// "NAME" as a C symbol: `... NAME[...] = { ... };` or `... NAME = value;`. Returns the numbers found (at most max).
static int pf_symbol_numbers(const char* text, const char* name, uint64_t* out, int max) {
size_t n = strlen(name);
const char* p = text;
while ((p = strstr(p, name)) != NULL) {
const char* q = p + n;
int before = (p == text) || !(isalnum((unsigned char)p[-1]) || p[-1] == '_');
p = q;
if (!before) continue;
if (isalnum((unsigned char)*q) || *q == '_') continue;
while (*q == '[' ) { const char* c = strchr(q, ']'); if (!c) return 0; q = c + 1; }
while (*q == ' ' || *q == '\t') ++q;
if (*q != '=') continue;
++q;
while (*q == ' ' || *q == '\t') ++q;
if (*q == '{') return pf_brace_numbers(q, out, max);
{ uint64_t v; const char* e; if (pf_number(q, &v, &e)) { if (max > 0) out[0] = v; return max > 0 ? 1 : 0; } }
return 0;
}
return 0;
}
static int pf_symbol_u32s(const char* text, const char* name, uint32_t* out, int max) {
uint64_t tmp[PF_MAX_SAMPLES];
int n, i;
if (max > PF_MAX_SAMPLES) max = PF_MAX_SAMPLES;
n = pf_symbol_numbers(text, name, tmp, max);
for (i = 0; i < n; ++i) out[i] = (uint32_t)tmp[i];
return n;
}
static int pf_unhex(const char* s, uint8_t* out, size_t cap, size_t* len) {
size_t n = strlen(s), i;
if (n % 2 || n / 2 > cap) return 0;
for (i = 0; i < n; i += 2) {
unsigned v = 0;
char two[3];
two[0] = s[i]; two[1] = s[i + 1]; two[2] = 0;
if (!isxdigit((unsigned char)two[0]) || !isxdigit((unsigned char)two[1])) return 0;
if (sscanf(two, "%2x", &v) != 1) return 0;
out[i / 2] = (uint8_t)v;
}
*len = n / 2;
return 1;
}
// seed_words_from_bytes of igneum-pow/src/seed.rs: FNV-1a 64 with four salts, each finalised.
static void pf_seed_words_from_bytes(const uint8_t* b, size_t n, uint32_t out[8]) {
uint64_t salt;
for (salt = 0; salt < 4; ++salt) {
uint64_t h = 0xcbf29ce484222325ull ^ (salt * 0x9E3779B97F4A7C15ull);
size_t i;
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
h ^= h >> 33; h *= 0xff51afd7ed558ccdull; h ^= h >> 33;
out[2 * salt] = (uint32_t)h;
out[2 * salt + 1] = (uint32_t)(h >> 32);
}
}
static uint64_t pf_fnv1a64(const void* p, size_t n) {
const uint8_t* b = (const uint8_t*)p;
uint64_t h = 0xcbf29ce484222325ull;
size_t i;
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
return h;
}
// One "key value" line of seeds.txt.
static int pf_seeds_line(const char* text, const char* key, char* out, size_t cap) {
size_t n = strlen(key);
const char* p = text;
while ((p = strstr(p, key)) != NULL) {
if ((p == text || p[-1] == '\n') && p[n] == ' ') {
const char* q = p + n + 1;
size_t len = 0;
while (q[len] && q[len] != '\r' && q[len] != '\n') ++len;
if (len + 1 > cap) return 0;
memcpy(out, q, len); out[len] = 0;
return 1;
}
p += n;
}
return 0;
}
static int pf_fail(char* err, size_t cap, const char* msg) { if (err && cap) { strncpy(err, msg, cap - 1); err[cap - 1] = 0; } return 0; }
// Reads program.h, seeds.txt (optional) and vectors.h (optional) of a pack directory. Returns 1 on success.
static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
char* prog;
char* seeds;
char* vec;
char ehex[65] = {0}, dhex[PF_HEX_CAP] = {0};
uint8_t bytes[256];
size_t blen;
memset(pk, 0, sizeof(*pk));
prog = pf_read_pack_file(dir, "program.h", NULL);
if (!prog) { char m[480]; snprintf(m, sizeof(m), "cannot read %.400s/program.h", dir); return pf_fail(err, cap, m); }
if (!pf_define_u32(prog, "IGNEUM_DATASET_LOG2", &pk->datasetLog2)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_DATASET_LOG2"); }
if (!pf_define_u32(prog, "IGNEUM_DATASET_MODE", &pk->datasetMode)) pk->datasetMode = 0;
if (pk->datasetMode != 1) { free(prog); return pf_fail(err, cap, "the pack is not memory-hard (IGNEUM_DATASET_MODE 1); the one-click workers serve memory-hard packs only"); }
if (!pf_define_u32(prog, "IGNEUM_CACHE_LOG2_WORDS", &pk->cacheLog2Words)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_LOG2_WORDS"); }
if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); }
if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1;
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"); }
if (pf_define_words(prog, "IGNEUM_KEY_INIT", pk->keyw, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_KEY_INIT with 8 words"); }
if (!pf_define_str(prog, "IGNEUM_SEED_STRING", pk->seedString, sizeof(pk->seedString))) strncpy(pk->seedString, "(no IGNEUM_SEED_STRING)", sizeof(pk->seedString) - 1);
pf_define_str(prog, "IGNEUM_SEED_BYTES_HEX", ehex, sizeof(ehex));
pf_define_str(prog, "IGNEUM_DAY_BYTES_HEX", dhex, sizeof(dhex));
if (pk->datasetLog2 < 20 || pk->datasetLog2 > 31 || pk->cacheLog2Words < 16 || pk->cacheLog2Words > 30 || pk->cacheSegments == 0) { free(prog); return pf_fail(err, cap, "program.h sizes out of range"); }
free(prog);
// seeds.txt wins when present; program.h's bytes must agree with it
seeds = pf_read_pack_file(dir, "seeds.txt", NULL);
if (seeds) {
char e2[65] = {0}, d2[PF_HEX_CAP] = {0};
int he = pf_seeds_line(seeds, "epoch_seed_hex", e2, sizeof(e2));
int hd = pf_seeds_line(seeds, "day_seed_hex", d2, sizeof(d2));
free(seeds);
if (!he || !hd) return pf_fail(err, cap, "seeds.txt has no epoch_seed_hex / day_seed_hex line");
if (ehex[0] && strcmp(ehex, e2) != 0) return pf_fail(err, cap, "seeds.txt epoch_seed_hex differs from program.h IGNEUM_SEED_BYTES_HEX");
if (dhex[0] && strcmp(dhex, d2) != 0) return pf_fail(err, cap, "seeds.txt day_seed_hex differs from program.h IGNEUM_DAY_BYTES_HEX");
strcpy(ehex, e2); strcpy(dhex, d2);
}
if (!ehex[0] || !dhex[0]) return pf_fail(err, cap, "no seeds: neither seeds.txt nor IGNEUM_SEED_BYTES_HEX / IGNEUM_DAY_BYTES_HEX in program.h (a pack from igneum-pow export --seed <name> has no byte seeds)");
if (strlen(ehex) != 64) return pf_fail(err, cap, "epoch seed is not 64 hex characters");
strcpy(pk->epochHex, ehex); strcpy(pk->dayHex, dhex);
// The seed words derived from the bytes must be the pack's own words: otherwise the pack and the seeds disagree
{
uint32_t w[8];
if (!pf_unhex(ehex, bytes, 32, &blen) || blen != 32) return pf_fail(err, cap, "epoch seed hex is malformed");
pf_seed_words_from_bytes(bytes, 32, w);
if (memcmp(w, pk->seedw, 32) != 0) return pf_fail(err, cap, "the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT (wrong seeds.txt for this pack?)");
if (!pf_unhex(dhex, bytes, sizeof(bytes), &blen)) return pf_fail(err, cap, "day seed hex is malformed");
pf_seed_words_from_bytes(bytes, blen, w);
if (memcmp(w, pk->keyw, 32) != 0) return pf_fail(err, cap, "the day seed bytes do not give the pack's IGNEUM_KEY_INIT (wrong seeds.txt for this pack?)");
}
vec = pf_read_pack_file(dir, "vectors.h", NULL);
if (vec) {
uint64_t tmp[PF_MAX_WARPS * 32];
uint32_t warps = 0;
int n, w, l;
if (pf_define_u32(vec, "IGNEUM_VEC_WARPS", &warps) && warps >= 1 && warps <= PF_MAX_WARPS) {
pk->vecWarps = (int)warps;
n = pf_symbol_u32s(vec, "IGNEUM_VEC_BASE", pk->vecBase, PF_MAX_WARPS);
if (n != pk->vecWarps) pk->vecWarps = 0;
n = pf_symbol_numbers(vec, "IGNEUM_VEC_OUT", tmp, PF_MAX_WARPS * 32);
if (n != pk->vecWarps * 32) pk->vecWarps = 0;
for (w = 0; w < pk->vecWarps; ++w) for (l = 0; l < 32; ++l) pk->vecOut[w][l] = tmp[w * 32 + l];
}
if (pf_symbol_u32s(vec, "IGNEUM_DS_HEAD", pk->dsHead, 16) == 16 &&
pf_symbol_u32s(vec, "IGNEUM_DS_LAST_INDEX", &pk->dsLastIndex, 1) == 1 &&
pf_symbol_u32s(vec, "IGNEUM_DS_LAST", &pk->dsLast, 1) == 1 &&
pf_symbol_u32s(vec, "IGNEUM_CACHE_HEAD", pk->cacheHead, 16) == 16 &&
pf_symbol_u32s(vec, "IGNEUM_CACHE_LAST", pk->cacheLast, 16) == 16 &&
pf_symbol_numbers(vec, "IGNEUM_CACHE_FNV64", &pk->cacheFnv, 1) == 1 && pk->vecWarps > 0) {
uint32_t ns = 0;
pk->haveVectors = 1;
if (pf_define_u32(vec, "IGNEUM_DS_SAMPLES", &ns) && ns > 0 && ns <= PF_MAX_SAMPLES) {
int a = pf_symbol_u32s(vec, "IGNEUM_DS_SAMPLE_INDEX", pk->sampleIdx, (int)ns);
int b = pf_symbol_u32s(vec, "IGNEUM_DS_SAMPLE_VALUE", pk->sampleVal, (int)ns);
pk->nSamples = (a == (int)ns && b == (int)ns) ? (int)ns : 0;
}
}
free(vec);
}
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.
static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32_t* cacheLast, uint64_t cacheFnv,
const uint32_t* dsHead, uint32_t dsLast, const uint32_t* sampleVals, const uint64_t* vec,
char* out, size_t cap) {
int okCH = memcmp(cacheHead, pk->cacheHead, 64) == 0, okCL = memcmp(cacheLast, pk->cacheLast, 64) == 0;
int okFnv = (cacheFnv == pk->cacheFnv);
int okDH = memcmp(dsHead, pk->dsHead, 64) == 0, okDL = (dsLast == pk->dsLast);
int badS = 0, badV = 0, i, w, l, firstBadWarp = -1, firstBadLane = -1;
for (i = 0; i < pk->nSamples; ++i) if (sampleVals[i] != pk->sampleVal[i]) ++badS;
for (w = 0; w < pk->vecWarps; ++w) for (l = 0; l < 32; ++l) if (vec[w * 32 + l] != pk->vecOut[w][l]) { if (firstBadWarp < 0) { firstBadWarp = w; firstBadLane = l; } ++badV; }
if (okCH && okCL && okFnv && okDH && okDL && badS == 0 && badV == 0) {
snprintf(out, cap, "self-test PASS (cache head, last line and FNV-1a 64 %016llx; dataset head, word [%u] and %d samples; %d of %d vector lanes)",
(unsigned long long)cacheFnv, pk->dsLastIndex, pk->nSamples, pk->vecWarps * 32, pk->vecWarps * 32);
return 1;
}
snprintf(out, cap, "self-test FAIL (cache head %s, cache last %s, cache FNV %016llx vs pack %016llx %s, dataset head %s, dataset last %s, samples %d bad of %d, vector lanes %d bad of %d%s)",
okCH ? "ok" : "BAD", okCL ? "ok" : "BAD", (unsigned long long)cacheFnv, (unsigned long long)pk->cacheFnv, okFnv ? "ok" : "BAD",
okDH ? "ok" : "BAD", okDL ? "ok" : "BAD", badS, pk->nSamples, badV, pk->vecWarps * 32,
firstBadWarp >= 0 ? " (first bad lane in the warp at base nonce" : "");
if (firstBadWarp >= 0) {
size_t n = strlen(out);
snprintf(out + n, cap > n ? cap - n : 0, " %u lane %d: device %016llx expected %016llx)", pk->vecBase[firstBadWarp], firstBadLane,
(unsigned long long)vec[firstBadWarp * 32 + firstBadLane], (unsigned long long)pk->vecOut[firstBadWarp][firstBadLane]);
}
return 0;
}
#endif

791
proto-cuda/nvrtc/worker.cpp Normal file
View file

@ -0,0 +1,791 @@
// igneum-worker-cuda: the one-click NVIDIA worker for igneum-miner --worker. 4 October 2026.
//
// Nothing to install but the NVIDIA driver. The pack's kernels (kernel.cu: cache fill and dataset build;
// kernel_bound.cu: the header-bound hash) are compiled at run time by NVRTC, the toolkit's runtime compiler, which
// ships next to this exe as nvrtc64_120_0.dll plus nvrtc-builtins64_128.dll (NVIDIA's redistributable, see
// THIRD-PARTY.md). The GPU is driven through the driver API in nvcuda.dll, which every NVIDIA driver installs. Both
// libraries are loaded with LoadLibrary/GetProcAddress (cuda_api.h), so no import library is linked and the exe is
// cross-compiled on the Mac with mingw (build-windows.sh). Plain C++17 otherwise.
//
// The source handed to NVRTC is the pack's own text: kernel.cu and kernel_bound.cu up to the host-side launch
// wrappers (which nvcc compiles for the host and NVRTC has no use for), with the pack's program.h and memhard.h as
// named headers, byte for byte. Two stub headers stand in for <cuda_runtime.h> and <cstdint>, which nvcc takes from
// the toolkit. emu/test.sh checks the equality on the Mac. The memory-hard core is therefore the same text host.cu
// compiles, and the miner's CPU re-check of every found nonce covers the rest.
//
// Worker protocol (the same lines as proto-cuda/host.cu --serve and proto-opencl/host.c --serve):
// stdin: job <job_id> <header_prehash_hex 64> <target_hex 16> <nonce_start u64> <nonce_count u64> <epoch_seed_hex 64> <day_seed_hex>
// prepare <epoch_seed_hex 64> <day_seed_hex> <pack_dir> compile that pack in the background, build its cache
// and dataset, self-test it; a job on it then switches
// quit
// stdout: ready cuda <device> pack <seed string> dataset-log2 N batch B regs R prepare 1 path nvrtc ...
// found <job_id> <nonce u64> <hash_hex 16>
// done <job_id> <hashes> <ms>
// error <job_id> <text>
// prepared <epoch_seed_hex> <day_seed_hex> <ms> ... | prepare-failed <epoch_seed_hex> <day_seed_hex> <text>
// info ...
// The first pack comes from --pack <dir> (igneum-miner export-pack writes it; the launcher passes it). Every pack is
// self-tested before it serves a job: cache head, last line and FNV-1a 64, dataset head, last word and 64 samples,
// and the three vector warps of vectors.h through the bound kernel with the pack's own seed words. A pack that fails
// is refused.
//
// Usage: igneum-worker-cuda --serve --pack <dir> [--device D] [--batch-log2 22] [--block-warps 1] [--arch sm_120|auto]
// igneum-worker-cuda --check --pack <dir> [--device D] compile, build, self-test, print timings, exit 0/1
#include <cstdint>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <iostream>
#include "cuda_api.h"
#include "packfile.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dlfcn.h>
#include <dirent.h>
#endif
static const char* WORKER_VERSION = "1.0 (4 October 2026)";
// ---------------------------------------------------------------------------------------------
// Helpers
static double wallMs() {
using namespace std::chrono;
return duration<double, std::milli>(steady_clock::now().time_since_epoch()).count();
}
static void emit(const std::string& s) { std::fputs(s.c_str(), stdout); std::fputc('\n', stdout); std::fflush(stdout); }
static void info(const std::string& s) { emit("info " + s); }
static std::string fmt(const char* f, ...) {
char buf[2048];
va_list ap;
va_start(ap, f);
vsnprintf(buf, sizeof(buf), f, ap);
va_end(ap);
return buf;
}
static std::string readText(const std::string& path, bool& ok) {
size_t n = 0;
char* b = pf_read_file(path.c_str(), &n);
if (!b) { ok = false; return ""; }
std::string s(b, n);
free(b);
ok = true;
return s;
}
static std::string exeDir() {
#ifdef _WIN32
char buf[MAX_PATH];
DWORD n = GetModuleFileNameA(nullptr, buf, MAX_PATH);
std::string p(buf, n);
size_t i = p.find_last_of("\\/");
return i == std::string::npos ? "." : p.substr(0, i);
#else
return ".";
#endif
}
// ---------------------------------------------------------------------------------------------
// Loading the two libraries
static void* libOpen(const std::string& name) {
#ifdef _WIN32
return (void*)LoadLibraryA(name.c_str());
#else
return dlopen(name.c_str(), RTLD_NOW);
#endif
}
static void* libSym(void* lib, const char* name) {
#ifdef _WIN32
return (void*)GetProcAddress((HMODULE)lib, name);
#else
return dlsym(lib, name);
#endif
}
#define LOAD_SYM(table, field, name) do { table.field = (decltype(table.field))libSym(lib, name); if (!table.field) { missing += std::string(missing.empty() ? "" : ", ") + name; } } while (0)
static bool loadDriver(Drv& d, std::string& err, std::string& libName) {
#ifdef IGNEUM_EMU
emu_fill_driver(d); libName = "emulation (host threads, no GPU)"; (void)err; return true;
#else
#ifdef _WIN32
const char* names[] = { "nvcuda.dll" };
#else
const char* names[] = { "libcuda.so.1", "libcuda.so" };
#endif
void* lib = nullptr;
for (const char* n : names) { lib = libOpen(n); if (lib) { libName = n; break; } }
if (!lib) { err = "the CUDA driver library (nvcuda.dll) is not installed: install or update the NVIDIA driver"; return false; }
std::string missing;
LOAD_SYM(d, init, "cuInit");
LOAD_SYM(d, driverGetVersion, "cuDriverGetVersion");
LOAD_SYM(d, deviceGetCount, "cuDeviceGetCount");
LOAD_SYM(d, deviceGet, "cuDeviceGet");
LOAD_SYM(d, deviceGetName, "cuDeviceGetName");
LOAD_SYM(d, deviceGetAttribute, "cuDeviceGetAttribute");
LOAD_SYM(d, deviceTotalMem, "cuDeviceTotalMem_v2");
LOAD_SYM(d, primaryCtxSetFlags, "cuDevicePrimaryCtxSetFlags_v2");
LOAD_SYM(d, primaryCtxRetain, "cuDevicePrimaryCtxRetain");
LOAD_SYM(d, primaryCtxRelease, "cuDevicePrimaryCtxRelease_v2");
LOAD_SYM(d, ctxSetCurrent, "cuCtxSetCurrent");
LOAD_SYM(d, ctxSynchronize, "cuCtxSynchronize");
LOAD_SYM(d, memGetInfo, "cuMemGetInfo_v2");
LOAD_SYM(d, memAlloc, "cuMemAlloc_v2");
LOAD_SYM(d, memFree, "cuMemFree_v2");
LOAD_SYM(d, memcpyDtoH, "cuMemcpyDtoH_v2");
LOAD_SYM(d, moduleLoadData, "cuModuleLoadData");
LOAD_SYM(d, moduleUnload, "cuModuleUnload");
LOAD_SYM(d, moduleGetFunction, "cuModuleGetFunction");
LOAD_SYM(d, launchKernel, "cuLaunchKernel");
LOAD_SYM(d, streamCreate, "cuStreamCreate");
LOAD_SYM(d, streamSynchronize, "cuStreamSynchronize");
LOAD_SYM(d, streamDestroy, "cuStreamDestroy_v2");
LOAD_SYM(d, funcGetAttribute, "cuFuncGetAttribute");
LOAD_SYM(d, occupancy, "cuOccupancyMaxActiveBlocksPerMultiprocessor");
LOAD_SYM(d, getErrorString, "cuGetErrorString");
LOAD_SYM(d, getErrorName, "cuGetErrorName");
if (!missing.empty()) { err = "the driver library lacks " + missing + " (driver too old; CUDA 11 or newer is needed)"; return false; }
return true;
#endif
}
#ifdef _WIN32
// nvrtc64_<major>0_0.dll next to the exe (any major), then a toolkit on PATH. IGNEUM_NVRTC_DLL overrides.
static std::vector<std::string> nvrtcCandidates() {
std::vector<std::string> v;
if (const char* o = std::getenv("IGNEUM_NVRTC_DLL")) v.push_back(o);
std::string dir = exeDir();
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA((dir + "\\nvrtc64_*_0.dll").c_str(), &fd);
if (h != INVALID_HANDLE_VALUE) {
do { std::string n = fd.cFileName; if (n.find(".alt.") == std::string::npos) v.push_back(dir + "\\" + n); } while (FindNextFileA(h, &fd));
FindClose(h);
}
v.push_back("nvrtc64_120_0.dll");
v.push_back("nvrtc64_130_0.dll");
if (const char* cp = std::getenv("CUDA_PATH")) { v.push_back(std::string(cp) + "\\bin\\nvrtc64_120_0.dll"); v.push_back(std::string(cp) + "\\bin\\nvrtc64_130_0.dll"); }
return v;
}
#endif
static bool loadNvrtc(Rtc& r, std::string& err, std::string& libName) {
#ifdef IGNEUM_EMU
emu_fill_nvrtc(r); libName = "emulation (source recorded and checked, nothing compiled)"; (void)err; return true;
#else
void* lib = nullptr;
#ifdef _WIN32
for (const std::string& n : nvrtcCandidates()) { lib = libOpen(n); if (lib) { libName = n; break; } }
if (!lib) { err = "nvrtc64_120_0.dll (and nvrtc-builtins64_128.dll) must sit next to " + exeDir() + "\\igneum-worker-cuda.exe; they are in the package"; return false; }
#else
const char* names[] = { "libnvrtc.so.12", "libnvrtc.so" };
for (const char* n : names) { lib = libOpen(n); if (lib) { libName = n; break; } }
if (!lib) { err = "libnvrtc.so.12 not found"; return false; }
#endif
std::string missing;
LOAD_SYM(r, version, "nvrtcVersion");
LOAD_SYM(r, createProgram, "nvrtcCreateProgram");
LOAD_SYM(r, destroyProgram, "nvrtcDestroyProgram");
LOAD_SYM(r, compileProgram, "nvrtcCompileProgram");
LOAD_SYM(r, getProgramLogSize, "nvrtcGetProgramLogSize");
LOAD_SYM(r, getProgramLog, "nvrtcGetProgramLog");
LOAD_SYM(r, getPTXSize, "nvrtcGetPTXSize");
LOAD_SYM(r, getPTX, "nvrtcGetPTX");
LOAD_SYM(r, getCUBINSize, "nvrtcGetCUBINSize");
LOAD_SYM(r, getCUBIN, "nvrtcGetCUBIN");
LOAD_SYM(r, addNameExpression, "nvrtcAddNameExpression");
LOAD_SYM(r, getLoweredName, "nvrtcGetLoweredName");
LOAD_SYM(r, getErrorString, "nvrtcGetErrorString");
if (!missing.empty()) { err = "the NVRTC library lacks " + missing; return false; }
r.getNumSupportedArchs = (decltype(r.getNumSupportedArchs))libSym(lib, "nvrtcGetNumSupportedArchs");
r.getSupportedArchs = (decltype(r.getSupportedArchs))libSym(lib, "nvrtcGetSupportedArchs");
return true;
#endif
}
// ---------------------------------------------------------------------------------------------
// The device context
struct Ctx {
Drv drv;
Rtc rtc;
CUdevice dev = 0;
CUcontext ctx = nullptr;
std::string name;
int major = 0, minor = 0, sms = 0, driverVersion = 0, rtcMajor = 0, rtcMinor = 0;
std::vector<int> rtcArchs; // what NVRTC can target (empty when the query is unavailable)
std::string archOpt; // "sm_120" or "compute_120": what the packs are compiled for
bool ptx = false; // true when archOpt is compute_XY (PTX, driver JIT)
std::string why; // how archOpt was chosen
int blockWarps = 1;
std::string err(CUresult r) { const char* s = nullptr; if (drv.getErrorString) drv.getErrorString(r, &s); return s ? s : "CUDA driver error"; }
};
#define DRV_CHECK(c, call, what) do { CUresult r_ = (call); if (r_ != CUDA_SUCCESS) { err = std::string(what) + ": " + (c).err(r_); return false; } } while (0)
static bool openDevice(Ctx& c, int device, const std::string& archArg, std::string& err) {
DRV_CHECK(c, c.drv.init(0), "cuInit");
int count = 0;
DRV_CHECK(c, c.drv.deviceGetCount(&count), "cuDeviceGetCount");
if (count == 0) { err = "no CUDA device"; return false; }
if (device < 0 || device >= count) { err = fmt("device %d out of range (%d devices)", device, count); return false; }
DRV_CHECK(c, c.drv.deviceGet(&c.dev, device), "cuDeviceGet");
char name[256] = {0};
DRV_CHECK(c, c.drv.deviceGetName(name, 255, c.dev), "cuDeviceGetName");
c.name = name;
for (char& ch : c.name) if (ch == ' ') ch = '_';
DRV_CHECK(c, c.drv.deviceGetAttribute(&c.major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, c.dev), "compute capability major");
DRV_CHECK(c, c.drv.deviceGetAttribute(&c.minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, c.dev), "compute capability minor");
DRV_CHECK(c, c.drv.deviceGetAttribute(&c.sms, CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT, c.dev), "multiprocessor count");
c.drv.driverGetVersion(&c.driverVersion);
// Blocking sync, set before the context exists: the host thread sleeps in cuStreamSynchronize instead of spinning
// (one full core per worker at the default spin schedule, measured on the RTX 5090 with eight workers, 3 Oct 2026).
c.drv.primaryCtxSetFlags(c.dev, CU_CTX_SCHED_BLOCKING_SYNC);
DRV_CHECK(c, c.drv.primaryCtxRetain(&c.ctx, c.dev), "cuDevicePrimaryCtxRetain");
DRV_CHECK(c, c.drv.ctxSetCurrent(c.ctx), "cuCtxSetCurrent");
c.rtc.version(&c.rtcMajor, &c.rtcMinor);
if (c.rtc.getNumSupportedArchs && c.rtc.getSupportedArchs) {
int n = 0;
if (c.rtc.getNumSupportedArchs(&n) == NVRTC_SUCCESS && n > 0 && n < 256) { c.rtcArchs.assign((size_t)n, 0); if (c.rtc.getSupportedArchs(c.rtcArchs.data()) != NVRTC_SUCCESS) c.rtcArchs.clear(); }
}
// The target: the device's own SASS (sm_XY) when this NVRTC knows the architecture, else PTX for the newest
// architecture it knows below the device's, which the driver JIT-compiles forward. --arch overrides.
int cc = c.major * 10 + c.minor;
if (archArg != "auto" && !archArg.empty()) {
c.archOpt = archArg; c.ptx = archArg.rfind("compute_", 0) == 0; c.why = "--arch";
} else if (c.rtcArchs.empty()) {
c.archOpt = fmt("sm_%d", cc); c.why = "the device's architecture (NVRTC did not list its targets)";
} else {
bool known = false; int best = 0;
for (int a : c.rtcArchs) { if (a == cc) known = true; if (a <= cc && a > best) best = a; }
if (known) { c.archOpt = fmt("sm_%d", cc); c.why = "the device's architecture, listed by NVRTC"; }
else if (best > 0) { c.archOpt = fmt("compute_%d", best); c.ptx = true; c.why = fmt("this NVRTC does not know sm_%d; PTX for compute_%d, JIT-compiled by the driver", cc, best); }
else { c.archOpt = fmt("compute_%d", c.rtcArchs.front()); c.ptx = true; c.why = fmt("this NVRTC knows nothing at or below sm_%d; PTX for its oldest target", cc); }
}
return true;
}
// ---------------------------------------------------------------------------------------------
// NVRTC: compile one of the pack's kernel files
// The pack's kernel files end in host-side launch wrappers (cudaError_t igneum_launch_* with <<< >>> launches) that
// nvcc compiles for the host. NVRTC compiles device code only, so the text is cut there. The cut is checked: nothing
// device-side may follow it.
static bool deviceOnly(const std::string& text, std::string& out, std::string& err) {
size_t cut = text.find("\n// Host-side launch wrappers");
if (cut == std::string::npos) cut = text.find("\ncudaError_t ");
if (cut == std::string::npos) { out = text; return true; }
std::string tail = text.substr(cut + 1);
if (tail.find("__global__") != std::string::npos || tail.find("__device__") != std::string::npos) { err = "device code after the host launch wrappers; the pack layout is not the one this worker knows"; return false; }
out = text.substr(0, cut + 1);
return true;
}
static const char* STUB_CUDA_RUNTIME =
"// igneum-worker-cuda: stand-in for <cuda_runtime.h> under NVRTC, which has the device built-ins already\n"
"#pragma once\n"
"#ifndef __CUDACC_RTC__\n#error \"this stub is for NVRTC only\"\n#endif\n"
"#ifdef __SIZE_TYPE__\ntypedef __SIZE_TYPE__ size_t;\n#elif defined(__LP64__) || defined(_LP64)\ntypedef unsigned long size_t;\n#else\ntypedef unsigned long long size_t;\n#endif\n";
static const char* STUB_CSTDINT =
"// igneum-worker-cuda: stand-in for <cstdint> under NVRTC (the fixed-width types the packs use)\n"
"#pragma once\n"
"typedef signed char int8_t; typedef unsigned char uint8_t; typedef short int16_t; typedef unsigned short uint16_t;\n"
"typedef int int32_t; typedef unsigned int uint32_t;\n"
"#if defined(__LP64__) || defined(_LP64)\ntypedef long int64_t; typedef unsigned long uint64_t;\n"
"#else\ntypedef long long int64_t; typedef unsigned long long uint64_t;\n#endif\n";
struct Compiled {
std::vector<char> image;
std::vector<std::string> lowered;
double ms = 0;
std::string log;
};
static bool rtcCompile(Ctx& c, const std::string& src, const char* name, const std::string& programH, const std::string& memhardH,
const std::vector<std::string>& nameExprs, Compiled& out, std::string& err) {
double t0 = wallMs();
const char* headers[4] = { STUB_CUDA_RUNTIME, STUB_CSTDINT, programH.c_str(), memhardH.c_str() };
const char* names[4] = { "cuda_runtime.h", "cstdint", "program.h", "memhard.h" };
nvrtcProgram prog = nullptr;
nvrtcResult r = c.rtc.createProgram(&prog, src.c_str(), name, 4, headers, names);
if (r != NVRTC_SUCCESS) { err = std::string("nvrtcCreateProgram: ") + c.rtc.getErrorString(r); return false; }
for (const std::string& e : nameExprs) {
r = c.rtc.addNameExpression(prog, e.c_str());
if (r != NVRTC_SUCCESS) { err = "nvrtcAddNameExpression " + e + ": " + c.rtc.getErrorString(r); c.rtc.destroyProgram(&prog); return false; }
}
std::string archOpt = "--gpu-architecture=" + c.archOpt;
const char* opts[2] = { archOpt.c_str(), "--std=c++17" };
r = c.rtc.compileProgram(prog, 2, opts);
{
size_t logSize = 0;
if (c.rtc.getProgramLogSize(prog, &logSize) == NVRTC_SUCCESS && logSize > 1) {
std::vector<char> log(logSize);
c.rtc.getProgramLog(prog, log.data());
out.log.assign(log.data(), logSize - 1);
}
}
if (r != NVRTC_SUCCESS) {
std::string one;
for (char ch : out.log) { if (ch == '\n' || ch == '\r') { if (one.size() && one.back() != '|') one += " | "; } else one += ch; if (one.size() > 600) break; }
err = std::string("nvrtcCompileProgram ") + name + " for " + c.archOpt + ": " + c.rtc.getErrorString(r) + ": " + one;
c.rtc.destroyProgram(&prog);
return false;
}
for (const std::string& e : nameExprs) {
const char* lowered = nullptr;
r = c.rtc.getLoweredName(prog, e.c_str(), &lowered);
if (r != NVRTC_SUCCESS || !lowered) { err = "nvrtcGetLoweredName " + e + ": " + c.rtc.getErrorString(r); c.rtc.destroyProgram(&prog); return false; }
out.lowered.push_back(lowered);
}
size_t n = 0;
if (c.ptx) {
r = c.rtc.getPTXSize(prog, &n);
if (r == NVRTC_SUCCESS) { out.image.resize(n); r = c.rtc.getPTX(prog, out.image.data()); }
} else {
r = c.rtc.getCUBINSize(prog, &n);
if (r == NVRTC_SUCCESS) { out.image.resize(n); r = c.rtc.getCUBIN(prog, out.image.data()); }
}
c.rtc.destroyProgram(&prog);
if (r != NVRTC_SUCCESS || n == 0) { err = std::string(c.ptx ? "nvrtcGetPTX" : "nvrtcGetCUBIN") + ": " + c.rtc.getErrorString(r); return false; }
out.ms = wallMs() - t0;
return true;
}
// ---------------------------------------------------------------------------------------------
// A resident pair: one pack compiled, its cache and dataset on the device, self-tested
struct Pair {
std::string dir, epochHex, dayHex, seedString;
uint32_t sw[8] = {0}, kw[8] = {0};
uint32_t datasetLog2 = 0, words = 0, cacheWords = 0, cacheSegments = 0;
CUmodule modKernel = nullptr, modBound = nullptr;
CUfunction fCacheFill = nullptr, fBuild = nullptr, fHashBound = nullptr;
CUdeviceptr cache = 0, ds = 0;
double compileMs = 0, cacheMs = 0, dsMs = 0, checkMs = 0;
std::string check;
bool checkPass = false, checked = false;
int regs = 0, blocksPerSM = 0;
};
static void releasePair(Ctx& c, Pair* p) {
if (!p) return;
if (p->ds) c.drv.memFree(p->ds);
if (p->cache) c.drv.memFree(p->cache);
if (p->modBound) c.drv.moduleUnload(p->modBound);
if (p->modKernel) c.drv.moduleUnload(p->modKernel);
delete p;
}
struct IgneumInitWordsArg { uint32_t w[8]; };
// `block` threads per block (32 x warps); `nonces` must be a multiple of it.
static bool launchHash(Ctx& c, Pair* p, CUdeviceptr out, uint32_t baseNonce, const uint32_t iw[8], uint32_t nonces, uint32_t block, CUstream s, std::string& err) {
uint32_t mask = p->words - 1u;
IgneumInitWordsArg a; std::memcpy(a.w, iw, 32);
void* args[5] = { &p->ds, &out, &baseNonce, &mask, &a };
DRV_CHECK(c, c.drv.launchKernel(p->fHashBound, nonces / block, 1, 1, block, 1, 1, 0, s, args, nullptr), "cuLaunchKernel igneum_hash_bound");
return true;
}
// Compiles the pack in `dir`, builds its cache and dataset on stream `s`, runs the self-test. Returns the pair or
// null with `err` set. Runs on the main thread for --pack and --check, on the prepare thread for `prepare`.
static Pair* buildPair(Ctx& c, const std::string& dir, CUstream s, std::string& err) {
PfPack pk;
char perr[512];
if (!pf_load(dir.c_str(), &pk, perr, sizeof(perr))) { err = std::string("pack ") + dir + ": " + perr; return nullptr; }
bool ok1, ok2, ok3, ok4;
std::string kernelCu = readText(dir + "/kernel.cu", ok1), boundCu = readText(dir + "/kernel_bound.cu", ok2);
std::string programH = readText(dir + "/program.h", ok3), memhardH = readText(dir + "/memhard.h", ok4);
if (!ok1 || !ok2 || !ok3 || !ok4) { err = "pack " + dir + " lacks kernel.cu, kernel_bound.cu, program.h or memhard.h"; return nullptr; }
std::string kernelDev, boundDev;
if (!deviceOnly(kernelCu, kernelDev, err) || !deviceOnly(boundCu, boundDev, err)) { err = "pack " + dir + ": " + err; return nullptr; }
Pair* p = new Pair();
p->dir = dir; p->epochHex = pk.epochHex; p->dayHex = pk.dayHex; p->seedString = pk.seedString;
std::memcpy(p->sw, pk.seedw, 32); std::memcpy(p->kw, pk.keyw, 32);
p->datasetLog2 = pk.datasetLog2; p->words = 1u << pk.datasetLog2; p->cacheWords = 1u << pk.cacheLog2Words; p->cacheSegments = pk.cacheSegments;
// Compile
Compiled ck, cb;
if (!rtcCompile(c, kernelDev, "kernel.cu", programH, memhardH, { "igneum_cache_fill", "igneum_build" }, ck, err)) { releasePair(c, p); return nullptr; }
if (!rtcCompile(c, boundDev, "kernel_bound.cu", programH, memhardH, { "igneum_hash_bound" }, cb, err)) { releasePair(c, p); return nullptr; }
p->compileMs = ck.ms + cb.ms;
// Load
{
CUresult r = c.drv.moduleLoadData(&p->modKernel, ck.image.data());
if (r != CUDA_SUCCESS) { err = "cuModuleLoadData kernel.cu (" + c.archOpt + "): " + c.err(r); releasePair(c, p); return nullptr; }
r = c.drv.moduleLoadData(&p->modBound, cb.image.data());
if (r != CUDA_SUCCESS) { err = "cuModuleLoadData kernel_bound.cu (" + c.archOpt + "): " + c.err(r); releasePair(c, p); return nullptr; }
if (c.drv.moduleGetFunction(&p->fCacheFill, p->modKernel, ck.lowered[0].c_str()) != CUDA_SUCCESS) { err = "igneum_cache_fill (" + ck.lowered[0] + ") not in the module"; releasePair(c, p); return nullptr; }
if (c.drv.moduleGetFunction(&p->fBuild, p->modKernel, ck.lowered[1].c_str()) != CUDA_SUCCESS) { err = "igneum_build (" + ck.lowered[1] + ") not in the module"; releasePair(c, p); return nullptr; }
if (c.drv.moduleGetFunction(&p->fHashBound, p->modBound, cb.lowered[0].c_str()) != CUDA_SUCCESS) { err = "igneum_hash_bound (" + cb.lowered[0] + ") not in the module"; releasePair(c, p); return nullptr; }
c.drv.funcGetAttribute(&p->regs, CU_FUNC_ATTRIBUTE_NUM_REGS, p->fHashBound);
c.drv.occupancy(&p->blocksPerSM, p->fHashBound, 32 * c.blockWarps, 0);
}
// Cache
double t0 = wallMs();
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 + (64u << 20)) {
err = fmt("%llu MiB free on the device, this pack needs %llu MiB (cache %llu + dataset %llu)", (unsigned long long)(freeB >> 20), (unsigned long long)((cacheBytes + dsBytes) >> 20), (unsigned long long)(cacheBytes >> 20), (unsigned long long)(dsBytes >> 20));
releasePair(c, p); return nullptr;
}
}
{
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; }
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; }
}
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; }
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; }
}
p->dsMs = wallMs() - t0;
// Self-test against vectors.h
t0 = wallMs();
if (!pk.haveVectors) {
p->checked = false; p->checkPass = true;
p->check = "self-test skipped (no vectors.h in the pack); the miner's CPU re-check covers every found nonce";
} else {
uint32_t cacheHead[16], cacheLast[16], dsHead[16], dsLast = 0;
std::vector<uint32_t> samples((size_t)(pk.nSamples > 0 ? pk.nSamples : 1), 0u);
std::vector<uint64_t> vec((size_t)pk.vecWarps * 32u, 0ull);
std::vector<uint32_t> whole(p->cacheWords);
CUresult r = c.drv.memcpyDtoH(whole.data(), p->cache, cacheBytes);
if (r != CUDA_SUCCESS) { err = "cuMemcpyDtoH cache: " + c.err(r); releasePair(c, p); return nullptr; }
std::memcpy(cacheHead, whole.data(), 64);
std::memcpy(cacheLast, whole.data() + p->cacheWords - 16u, 64);
uint64_t fnv = pf_fnv1a64(whole.data(), cacheBytes);
whole.clear(); whole.shrink_to_fit();
if ((r = c.drv.memcpyDtoH(dsHead, p->ds, 64)) != CUDA_SUCCESS) { err = "cuMemcpyDtoH dataset head: " + c.err(r); releasePair(c, p); return nullptr; }
if (pk.dsLastIndex < p->words) r = c.drv.memcpyDtoH(&dsLast, p->ds + (CUdeviceptr)pk.dsLastIndex * 4u, 4);
for (int i = 0; i < pk.nSamples && r == CUDA_SUCCESS; ++i) if (pk.sampleIdx[i] < p->words) r = c.drv.memcpyDtoH(&samples[(size_t)i], p->ds + (CUdeviceptr)pk.sampleIdx[i] * 4u, 4);
if (r != CUDA_SUCCESS) { err = "cuMemcpyDtoH dataset words: " + c.err(r); releasePair(c, p); return nullptr; }
CUdeviceptr out = 0;
if ((r = c.drv.memAlloc(&out, 32u * (size_t)c.blockWarps * 8u)) != CUDA_SUCCESS) { err = "cuMemAlloc vector out: " + c.err(r); releasePair(c, p); return nullptr; }
for (int w = 0; w < pk.vecWarps; ++w) {
// One block of 32 x block-warps lanes; the vector warp is its first 32 lanes (lane nonce = base + gid)
if (!launchHash(c, p, out, pk.vecBase[w], p->sw, 32u * (uint32_t)c.blockWarps, 32u * (uint32_t)c.blockWarps, s, err)) { c.drv.memFree(out); releasePair(c, p); return nullptr; }
if ((r = c.drv.streamSynchronize(s)) != CUDA_SUCCESS || (r = c.drv.memcpyDtoH(&vec[(size_t)w * 32u], out, 32u * 8u)) != CUDA_SUCCESS) { err = "vector warp: " + c.err(r); c.drv.memFree(out); releasePair(c, p); return nullptr; }
}
c.drv.memFree(out);
char line[1024];
p->checkPass = pf_selftest(&pk, cacheHead, cacheLast, fnv, dsHead, dsLast, samples.data(), vec.data(), line, sizeof(line)) != 0;
p->checked = true;
p->check = line;
}
p->checkMs = wallMs() - t0;
if (!p->checkPass) { err = p->check; releasePair(c, p); return nullptr; }
return p;
}
static std::string pairSummary(const Pair* p) {
return fmt("nvrtc %.0f cache %.0f dataset %.0f check %.0f ms; %s", p->compileMs, p->cacheMs, p->dsMs, p->checkMs, p->check.c_str());
}
// ---------------------------------------------------------------------------------------------
// Prepare, on its own thread
struct PrepareTask {
std::string epochHex, dayHex, dir, error;
std::atomic<bool> done{false};
Pair* result = nullptr;
double t0 = 0;
std::thread thread;
};
static void prepareRun(Ctx* c, PrepareTask* t) {
CUstream s = nullptr;
std::string err;
if (c->drv.ctxSetCurrent(c->ctx) != CUDA_SUCCESS) { t->error = "cuCtxSetCurrent on the prepare thread"; t->done = true; return; }
if (c->drv.streamCreate(&s, CU_STREAM_NON_BLOCKING) != CUDA_SUCCESS) { t->error = "cuStreamCreate on the prepare thread"; t->done = true; return; }
Pair* p = buildPair(*c, t->dir, s, err);
c->drv.streamDestroy(s);
if (p && (p->epochHex != t->epochHex || p->dayHex != t->dayHex)) {
err = "the pack in " + t->dir + " is for epoch " + p->epochHex.substr(0, 16) + " day " + p->dayHex + ", not the prepared seeds";
releasePair(*c, p); p = nullptr;
}
t->result = p;
t->error = err;
t->done = true;
}
// ---------------------------------------------------------------------------------------------
// Serve
struct Options {
bool serve = false, check = false;
int device = 0, batchLog2 = 22, blockWarps = 1;
std::string pack, arch = "auto";
};
static void usage() {
std::printf("igneum-worker-cuda %s\n"
" --serve --pack <dir> GPU worker for igneum-miner --worker: jobs on stdin, found/done lines on stdout\n"
" --check --pack <dir> compile the pack, build its cache and dataset, self-test, print timings, exit 0 or 1\n"
" --device D CUDA device index (default 0)\n"
" --batch-log2 B nonces per dispatch = 2^B (default 22)\n"
" --block-warps W warps per thread block (default 1)\n"
" --arch sm_XY|compute_XY|auto NVRTC target (default auto: the device's architecture)\n", WORKER_VERSION);
}
static Options parseArgs(int argc, char** argv) {
Options o;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
auto next = [&]() -> std::string { if (i + 1 >= argc) { usage(); std::exit(2); } return argv[++i]; };
if (a == "--serve") o.serve = true;
else if (a == "--check") o.check = true;
else if (a == "--pack") o.pack = next();
else if (a == "--device") o.device = std::atoi(next().c_str());
else if (a == "--batch-log2") o.batchLog2 = std::atoi(next().c_str());
else if (a == "--block-warps") o.blockWarps = std::atoi(next().c_str());
else if (a == "--arch") o.arch = next();
else if (a == "--no-prepare") { /* accepted for symmetry with the other workers; prepare is always on here */ }
else if (a == "-h" || a == "--help") { usage(); std::exit(0); }
else { std::printf("unknown argument %s\n", argv[i]); usage(); std::exit(2); }
}
if (o.batchLog2 < 10 || o.batchLog2 > 28) { std::printf("--batch-log2 must be between 10 and 28\n"); std::exit(2); }
if (o.blockWarps < 1 || o.blockWarps > 32) { std::printf("--block-warps must be between 1 and 32\n"); std::exit(2); }
if (!o.serve && !o.check) { usage(); std::exit(2); }
if (o.pack.empty()) { std::printf("--pack <dir> is required (igneum-miner export-pack <node> <dir> writes one)\n"); std::exit(2); }
while (o.pack.size() > 1 && (o.pack.back() == '/' || o.pack.back() == '\\')) o.pack.pop_back();
return o;
}
static std::vector<std::string> split(const std::string& line) {
std::vector<std::string> f;
size_t i = 0;
while (i < line.size()) {
while (i < line.size() && (line[i] == ' ' || line[i] == '\t' || line[i] == '\r')) ++i;
size_t j = i;
while (j < line.size() && line[j] != ' ' && line[j] != '\t' && line[j] != '\r') ++j;
if (j > i) f.push_back(line.substr(i, j - i));
i = j;
}
return f;
}
// A pack directory for the given seeds under `root` (one subdirectory per pack, each with seeds.txt), or "".
static std::string findPackFor(const std::string& root, const std::string& epochHex, const std::string& dayHex) {
if (root.empty()) return "";
std::vector<std::string> names;
#ifdef _WIN32
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA((root + "\\*").c_str(), &fd);
if (h == INVALID_HANDLE_VALUE) return "";
do { if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && fd.cFileName[0] != '.') names.push_back(root + "\\" + fd.cFileName); } while (FindNextFileA(h, &fd));
FindClose(h);
#else
DIR* d = opendir(root.c_str());
if (!d) return "";
while (dirent* e = readdir(d)) if (e->d_name[0] != '.') names.push_back(root + "/" + e->d_name);
closedir(d);
#endif
for (const std::string& dir : names) {
bool ok = false;
std::string seeds = readText(dir + "/seeds.txt", ok);
if (!ok) continue;
char e[65] = {0}, dd[PF_HEX_CAP] = {0};
if (pf_seeds_line(seeds.c_str(), "epoch_seed_hex", e, sizeof(e)) && pf_seeds_line(seeds.c_str(), "day_seed_hex", dd, sizeof(dd)) && epochHex == e && dayHex == dd) return dir;
}
return "";
}
static std::string parentDir(const std::string& p) { size_t i = p.find_last_of("/\\"); return i == std::string::npos ? "." : p.substr(0, i); }
static int runServe(Ctx& c, const Options& o, Pair* cur) {
const uint32_t batch = 1u << o.batchLog2;
CUdeviceptr dOut = 0;
std::string err;
if (c.drv.memAlloc(&dOut, (size_t)batch * 8u) != CUDA_SUCCESS) { emit("error 0 cuMemAlloc out buffer"); return 2; }
std::vector<uint64_t> hOut(batch);
Pair* prepared = nullptr;
Pair* old = nullptr;
PrepareTask* task = nullptr;
std::string prepareRoot; // the parent of the last prepare's pack directory: where the miner writes its packs
emit(fmt("ready cuda %s pack %s dataset-log2 %u batch %u regs %d prepare 1 path nvrtc %d.%d driver %d.%d arch %s worker %s",
c.name.c_str(), cur->seedString.c_str(), cur->datasetLog2, batch, cur->regs, c.rtcMajor, c.rtcMinor, c.driverVersion / 1000, (c.driverVersion % 100) / 10, c.archOpt.c_str(), WORKER_VERSION));
info(fmt("first pack %s: %s", cur->dir.c_str(), pairSummary(cur).c_str()));
std::string line;
while (std::getline(std::cin, line)) {
if (line == "quit") break;
std::vector<std::string> f = split(line);
if (f.empty()) continue;
// A finished prepare is reported here, between lines
if (task && task->done) {
task->thread.join();
if (task->result) {
if (prepared) releasePair(c, prepared);
prepared = task->result;
emit(fmt("prepared %s %s %.1f %s resident 2 programs 2 datasets", prepared->epochHex.c_str(), prepared->dayHex.c_str(), wallMs() - task->t0, pairSummary(prepared).c_str()));
} else {
emit(fmt("prepare-failed %s %s %s", task->epochHex.c_str(), task->dayHex.c_str(), task->error.c_str()));
}
delete task; task = nullptr;
}
if (f[0] == "prepare") {
if (f.size() < 4) { emit(fmt("prepare-failed %s %s a pack directory is needed as the third field (igneum-miner --prepare-packs <dir>)", f.size() > 1 ? f[1].c_str() : "0", f.size() > 2 ? f[2].c_str() : "0")); continue; }
if (f[1].size() != 64) { emit(fmt("prepare-failed %s %s bad field (epoch_seed 64 hex, day_seed hex)", f[1].c_str(), f[2].c_str())); continue; }
if (task) { emit(fmt("prepare-failed %s %s a prepare is still running", f[1].c_str(), f[2].c_str())); continue; }
if (prepared && prepared->epochHex == f[1] && prepared->dayHex == f[2]) { emit(fmt("prepared %s %s 0 (already resident)", f[1].c_str(), f[2].c_str())); continue; }
if (cur->epochHex == f[1] && cur->dayHex == f[2]) { emit(fmt("prepared %s %s 0 (already the current pair)", f[1].c_str(), f[2].c_str())); continue; }
std::string dir = f[3];
for (size_t i = 4; i < f.size(); ++i) dir += " " + f[i]; // a directory with spaces arrives as several fields
prepareRoot = parentDir(dir);
task = new PrepareTask();
task->epochHex = f[1]; task->dayHex = f[2]; task->dir = dir; task->t0 = wallMs();
task->thread = std::thread(prepareRun, &c, task);
info(fmt("prepare started for epoch %.16s day %s from %s (NVRTC %s in the background)", f[1].c_str(), f[2].c_str(), dir.c_str(), c.archOpt.c_str()));
continue;
}
if (f[0] != "job") { info("ignored: " + line); continue; }
std::string jobId = f.size() > 1 ? f[1] : "0";
if (f.size() < 8) { emit("error " + jobId + " malformed job line (need 7 fields after job)"); continue; }
uint8_t prehash[32], epochSeed[32], daySeed[256];
size_t pl = 0, el = 0, dl = 0;
unsigned long long target = 0, nonceStart = 0, nonceCount = 0;
if (!pf_unhex(f[2].c_str(), prehash, 32, &pl) || pl != 32 || std::sscanf(f[3].c_str(), "%llx", &target) != 1 ||
std::sscanf(f[4].c_str(), "%llu", &nonceStart) != 1 || std::sscanf(f[5].c_str(), "%llu", &nonceCount) != 1 ||
!pf_unhex(f[6].c_str(), epochSeed, 32, &el) || el != 32 || !pf_unhex(f[7].c_str(), daySeed, sizeof(daySeed), &dl)) {
emit("error " + jobId + " bad field (prehash 64 hex, target 16 hex, nonce_start u64, nonce_count u64, epoch_seed 64 hex, day_seed hex)"); continue;
}
if (nonceCount == 0 || nonceCount % 32 != 0 || (nonceStart & 31) != 0) { emit("error " + jobId + " nonce_start must be 32-aligned and nonce_count a non-zero multiple of 32"); continue; }
uint32_t sw[8], kw[8];
pf_seed_words_from_bytes(epochSeed, 32, sw);
pf_seed_words_from_bytes(daySeed, dl, kw);
double t0 = wallMs();
bool switched = false;
if ((std::memcmp(sw, cur->sw, 32) != 0 || std::memcmp(kw, cur->kw, 32) != 0) && !task &&
!(prepared && std::memcmp(sw, prepared->sw, 32) == 0 && std::memcmp(kw, prepared->kw, 32) == 0)) {
// Self-heal: a job on seeds this worker has no pair for and no prepare in flight (a prepare failed, or
// the miner never sent one). The miner writes a pack per pair under its --prepare-packs root; find it by
// seeds.txt and build it now, in the foreground. The miner only re-sends prepare for the pair after this one.
std::string dir = findPackFor(prepareRoot, f[6], f[7]);
if (dir.empty()) dir = findPackFor(parentDir(o.pack) + "/prepare", f[6], f[7]);
if (dir.empty()) dir = findPackFor(parentDir(o.pack), f[6], f[7]);
if (!dir.empty()) {
info(fmt("job %s is for epoch %.16s day %s, which is not resident; building its pack %s now (foreground)", jobId.c_str(), f[6].c_str(), f[7].c_str(), dir.c_str()));
std::string berr;
Pair* p = buildPair(c, dir, nullptr, berr);
if (p && (p->epochHex != f[6] || p->dayHex != f[7])) { berr = "the pack in " + dir + " is for other seeds"; releasePair(c, p); p = nullptr; }
if (p) { if (prepared) releasePair(c, prepared); prepared = p; info(fmt("built %s: %s", dir.c_str(), pairSummary(p).c_str())); }
else emit("error " + jobId + " could not build " + dir + ": " + berr);
}
}
if (std::memcmp(sw, cur->sw, 32) != 0 || std::memcmp(kw, cur->kw, 32) != 0) {
if (prepared && std::memcmp(sw, prepared->sw, 32) == 0 && std::memcmp(kw, prepared->kw, 32) == 0) {
if (old) releasePair(c, old);
old = cur; cur = prepared; prepared = nullptr; switched = true;
info(fmt("switched to the prepared pair epoch %.16s day %s in %.2f ms", cur->epochHex.c_str(), cur->dayHex.c_str(), wallMs() - t0));
} else if (std::memcmp(sw, cur->sw, 32) != 0) {
emit(fmt("error %s epoch seed mismatch: this worker holds epoch %.16s (seed words %08x %08x ...)%s, the job's epoch seed %.16s gives %08x %08x ...; send prepare with a pack directory",
jobId.c_str(), cur->epochHex.c_str(), cur->sw[0], cur->sw[1], prepared ? " plus one prepared pair" : "", f[6].c_str(), sw[0], sw[1]));
continue;
} else {
emit(fmt("error %s day seed mismatch: this worker's cache is for key %08x %08x ..., the job's day seed %s gives %08x %08x ...; send prepare with a pack directory",
jobId.c_str(), cur->kw[0], cur->kw[1], f[7].c_str(), kw[0], kw[1]));
continue;
}
}
uint64_t remaining = nonceCount, hashes = 0;
uint32_t hi = (uint32_t)(nonceStart >> 32), lo = (uint32_t)nonceStart;
bool failed = false;
while (remaining > 0) {
uint64_t room = (uint64_t)(0xffffffffu - lo) + 1ull;
uint64_t chunk64 = remaining < batch ? remaining : batch;
if (chunk64 > room) chunk64 = room;
uint32_t chunk = (uint32_t)chunk64;
uint32_t iw[8];
{
uint8_t b[49];
std::memcpy(b, "igneum-block/", 13);
std::memcpy(b + 13, prehash, 32);
b[45] = (uint8_t)hi; b[46] = (uint8_t)(hi >> 8); b[47] = (uint8_t)(hi >> 16); b[48] = (uint8_t)(hi >> 24);
pf_seed_words_from_bytes(b, 49, iw);
}
// A chunk that is not a multiple of the block is finished one 32-lane block at a time
uint32_t block = 32u * (uint32_t)o.blockWarps;
uint32_t main = chunk - (chunk % block);
CUresult r = CUDA_SUCCESS;
if (main > 0 && !launchHash(c, cur, dOut, lo, iw, main, block, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; break; }
if (main < chunk) {
for (uint32_t off = main; off < chunk && !failed; off += 32u) if (!launchHash(c, cur, dOut + (CUdeviceptr)off * 8u, lo + off, iw, 32u, 32u, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; }
if (failed) break;
}
r = c.drv.streamSynchronize(nullptr);
if (r == CUDA_SUCCESS) r = c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)chunk * 8u);
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: " + c.err(r)); failed = true; break; }
for (uint32_t i = 0; i < chunk; ++i) if (hOut[i] <= target) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + i);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)hOut[i]);
}
std::fflush(stdout);
hashes += chunk;
remaining -= chunk;
if (chunk64 == room) { hi += 1u; lo = 0u; } else lo += chunk;
}
if (failed) continue;
emit(fmt("done %s %llu %.2f", jobId.c_str(), (unsigned long long)hashes, wallMs() - t0));
if (switched && old) { releasePair(c, old); old = nullptr; info("dropped the previous pair (its program, cache and dataset)"); }
}
if (task) { task->thread.join(); if (task->result) releasePair(c, task->result); delete task; }
c.drv.memFree(dOut);
if (old) releasePair(c, old);
if (prepared) releasePair(c, prepared);
releasePair(c, cur);
return 0;
}
// ---------------------------------------------------------------------------------------------
// Main
int main(int argc, char** argv) {
Options o = parseArgs(argc, argv);
Ctx c;
c.blockWarps = o.blockWarps;
std::string err, drvLib, rtcLib;
if (!loadDriver(c.drv, err, drvLib)) { emit("error 0 " + err); return 2; }
if (!loadNvrtc(c.rtc, err, rtcLib)) { emit("error 0 " + err); return 2; }
if (!openDevice(c, o.device, o.arch, err)) { emit("error 0 " + err); return 2; }
info(fmt("igneum-worker-cuda %s: device %d %s (sm_%d%d, %d SMs), driver %d.%d from %s, NVRTC %d.%d from %s, target %s (%s)",
WORKER_VERSION, o.device, c.name.c_str(), c.major, c.minor, c.sms, c.driverVersion / 1000, (c.driverVersion % 100) / 10, drvLib.c_str(), c.rtcMajor, c.rtcMinor, rtcLib.c_str(), c.archOpt.c_str(), c.why.c_str()));
double t0 = wallMs();
Pair* cur = buildPair(c, o.pack, nullptr, err);
if (!cur) { emit("error 0 " + err); return 1; }
if (o.check) {
std::printf("check PASS %s in %.0f ms: %s\n", o.pack.c_str(), wallMs() - t0, pairSummary(cur).c_str());
std::printf(" epoch %s day %s, dataset 2^%u words, cache 2^%u words in %u segments, %d registers, %d blocks/SM at %d warp(s)/block, target %s\n",
cur->epochHex.c_str(), cur->dayHex.c_str(), cur->datasetLog2, (unsigned)__builtin_ctz(cur->cacheWords), cur->cacheSegments, cur->regs, cur->blocksPerSM, c.blockWarps, c.archOpt.c_str());
releasePair(c, cur);
return 0;
}
int rc = runServe(c, o, cur);
c.drv.primaryCtxRelease(c.dev);
return rc;
}