From f05274afd0becc7311fa69aa60b0215f300e5f28 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Fri, 31 Jul 2026 11:27:50 -0300 Subject: randomx: allocate the dataset once, not once per miner thread (#68) Every miner thread calls RandomXInitDataset at startup. Its only guard is g_fFast, which is set at the very end -- after the ~2 GB dataset has been filled, which takes tens of seconds. So all of them sailed past the guard together and each allocated its own dataset. Measured in production on a 32-core machine running 31 miner threads: the log says "initializing ~2 GB dataset" 34 times, and the process holds 65,127 MB of private pages while its own startup line promises 2142 MB "(~2080 MB shared RandomX dataset + ~2 MB per thread)". The word shared was not true. A 4-core machine showed the same thing at its own scale: 4 allocations for 3 threads. The waste was the mild half. g_dataset is assigned the moment memory is allocated, before it holds anything, so the last thread to allocate swung the pointer out from under threads still filling their own. A miner could then hash against a dataset another thread had not finished writing, and proof of work computed that way is not reproducible by any other node -- the block would be found invalid by everyone, including this node's own verifier, which uses the cache rather than the dataset. This only became reachable with multi-threaded mining (#52); with one thread there was nobody to race. Holding the lock across the whole initialisation is deliberate: a thread arriving mid-init has nothing useful to do until the dataset exists, and blocking is how it waits for exactly that. Verified with three mining threads: one "initializing ~2 GB dataset", one "dataset ready", and 2362 MB of private memory against 2086 MB advertised -- the difference being the 256 MB cache. Before, three threads meant three datasets. --- src/randomx_pow.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'src/randomx_pow.cpp') diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index 17bf455..d617ba2 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal @@ -52,10 +53,32 @@ bool RandomXInit() } +// One dataset, once, no matter how many miners ask for it. +// +// Every miner thread calls this at startup. The g_fFast guard below is only +// set at the very end, after the ~2 GB dataset has been filled, which takes +// tens of seconds -- so without this lock all of them sail past the guard +// together and each allocates its own dataset. Measured in production: a +// 32-core machine logged "initializing ~2 GB dataset" 34 times and committed +// 65 GB of private pages against a startup message promising 2142 MB. +// +// The waste was the mild half. g_dataset is assigned the moment the memory is +// allocated, before it holds anything, so the last thread to allocate would +// swing the pointer out from under the threads still filling their own. A +// miner could then hash against a dataset another thread had not finished +// writing, producing proof of work that no other node can reproduce. +// +// Holding the lock across the whole initialisation is deliberate: a thread +// that arrives mid-init has nothing useful to do until the dataset exists, +// and blocking is how it waits for exactly that. +static std::mutex g_csDataset; + bool RandomXInitDataset(int nThreads) { if (!g_fInit && !RandomXInit()) return false; + + std::lock_guard lock(g_csDataset); if (g_fFast) return true; -- cgit v1.2.3