diff options
| author | Bitflash <btf01@proton.me> | 2026-07-31 11:27:50 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-31 11:27:50 -0300 |
| commit | f05274afd0becc7311fa69aa60b0215f300e5f28 (patch) | |
| tree | 85f5842158688f59bfe88e3349866687ce0e5551 | |
| parent | cca4e0c38595653846d9907650870df8c0768d77 (diff) | |
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.
| -rw-r--r-- | src/randomx_pow.cpp | 23 |
1 files changed, 23 insertions, 0 deletions
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 @@ | |||
| 7 | #include <randomx.h> | 7 | #include <randomx.h> |
| 8 | #include <thread> | 8 | #include <thread> |
| 9 | #include <vector> | 9 | #include <vector> |
| 10 | #include <mutex> | ||
| 10 | 11 | ||
| 11 | // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it | 12 | // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it |
| 12 | // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal | 13 | // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal |
| @@ -52,10 +53,32 @@ bool RandomXInit() | |||
| 52 | } | 53 | } |
| 53 | 54 | ||
| 54 | 55 | ||
| 56 | // One dataset, once, no matter how many miners ask for it. | ||
| 57 | // | ||
| 58 | // Every miner thread calls this at startup. The g_fFast guard below is only | ||
| 59 | // set at the very end, after the ~2 GB dataset has been filled, which takes | ||
| 60 | // tens of seconds -- so without this lock all of them sail past the guard | ||
| 61 | // together and each allocates its own dataset. Measured in production: a | ||
| 62 | // 32-core machine logged "initializing ~2 GB dataset" 34 times and committed | ||
| 63 | // 65 GB of private pages against a startup message promising 2142 MB. | ||
| 64 | // | ||
| 65 | // The waste was the mild half. g_dataset is assigned the moment the memory is | ||
| 66 | // allocated, before it holds anything, so the last thread to allocate would | ||
| 67 | // swing the pointer out from under the threads still filling their own. A | ||
| 68 | // miner could then hash against a dataset another thread had not finished | ||
| 69 | // writing, producing proof of work that no other node can reproduce. | ||
| 70 | // | ||
| 71 | // Holding the lock across the whole initialisation is deliberate: a thread | ||
| 72 | // that arrives mid-init has nothing useful to do until the dataset exists, | ||
| 73 | // and blocking is how it waits for exactly that. | ||
| 74 | static std::mutex g_csDataset; | ||
| 75 | |||
| 55 | bool RandomXInitDataset(int nThreads) | 76 | bool RandomXInitDataset(int nThreads) |
| 56 | { | 77 | { |
| 57 | if (!g_fInit && !RandomXInit()) | 78 | if (!g_fInit && !RandomXInit()) |
| 58 | return false; | 79 | return false; |
| 80 | |||
| 81 | std::lock_guard<std::mutex> lock(g_csDataset); | ||
| 59 | if (g_fFast) | 82 | if (g_fFast) |
| 60 | return true; | 83 | return true; |
| 61 | 84 | ||