Skip to content

feat(cpu): Blake3/Alephium CPU mining — first CPU algorithm#172

Merged
luminousmining merged 12 commits into
luminousmining:mainfrom
yuzi-co:feat/cpu-blake3-mining
Jun 19, 2026
Merged

feat(cpu): Blake3/Alephium CPU mining — first CPU algorithm#172
luminousmining merged 12 commits into
luminousmining:mainfrom
yuzi-co:feat/cpu-blake3-mining

Conversation

@yuzi-co

@yuzi-co yuzi-co commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the first CPU mining algorithm by wiring the previously-unused CPU layer to
Blake3/Alephium support. A CPU is enumerated as a mining device alongside GPUs and runs
on the same global Blake3 pool/stratum path. The per-batch nonce scan is parallelized by a
self-owned, persistent, pinned std::thread pool — not OpenMP — exposing two new
controls, --cpu_threads and --cpu_affinity.

Why a custom thread pool instead of OpenMP?

The scan originally used #pragma omp parallel for. That was replaced because:

  1. OpenMP did nothing on Windows. The clang-cl cross build links no OpenMP runtime, so
    the pragma silently degraded to a serial, single-core scan — measured at ~1.5 MH/s
    on Windows vs ~12 MH/s on Linux
    (libomp linked) on the same host, an ~8× gap that was
    purely serial-vs-multicore.
  2. No user control. OpenMP chose the thread count itself; there was no way to set the
    worker count or pin workers to cores. The pool adds --cpu_threads / --cpu_affinity.
  3. Cross-platform runtime baggage. OpenMP required libomp special-casing everywhere — a
    Linux libomp-dev apt-install + libomp.so.5 runtime staging, a macOS Homebrew-libomp
    CMake probe, and a "best-effort serial" caveat on Windows.

The std::jthread pool fixes all three: real multicore on every platform, tunable, and
zero shipped runtime dependency. All OpenMP/libomp plumbing is deleted.

What's included

  • Device layer: DEVICE_TYPE::CPU, DeviceCpu (a no-SDK device modeled on
    DeviceMocker), enumerated by DeviceManager and dispatched through setAlgorithm().
  • Resolver: ResolverCpuBlake3 — reuses the host blake3::hashRef() (double-BLAKE3
    Alephium PoW) for the nonce scan, fanned across the pool, with the same winner predicate /
    share-submit path as the GPU resolvers.
  • CpuThreadPool: persistent worker threads pinned at startup; parallelFor() splits a
    batch into contiguous per-worker chunks and blocks until done. Pure helpers
    (cpu_params.hpp) and the pin wrapper (cpu_affinity) are unit-tested.
  • CLI: --cpu_threads (pool size, default = all logical cores) and --cpu_affinity
    (hex core bitmask, default none). See documentation/CPU_TUNING.md.
  • Build: CPU becomes an orthogonal build axis (GPU=amd|nvidia|both|none ×
    CPU=ON|OFF) across the Linux + Windows-cross Dockerfiles, docker-build.ps1, and CI;
    all find_package(OpenMP) / libomp install / staging removed.
  • Dashboard / defaults: CPU shows as CPU (not UNKNOWN); CPU-appropriate occupancy so
    the hashrate displays out of the box. --threads/--blocks still override.

Benchmarks (BLAKE3, live on HeroMiners, one config at a time)

Host: 16-core CPU + AMD RX 9070 XT.

Config Hashrate
Windows CPU only ~11 MH/s (was ~1.5 MH/s serial under OpenMP)
Linux CPU only ~10.7 MH/s
Windows GPU only ~1.70 GH/s
Windows GPU + CPU (default) ~0.4 GH/s ⚠️

Headline: Windows CPU now matches Linux (~11 vs ~10.7 MH/s) — the OpenMP Windows gap is
closed.

GPU + CPU caveat: running all-core CPU mining next to the GPU starves the GPU's host
feeder thread
, collapsing it to ~0.4 GH/s. Reserve cores for the GPU with affinity —
--cpu_threads=8 --cpu_affinity=0xFF restores the full 1.70 GH/s GPU + ~7 MH/s CPU.
Details in CPU_TUNING.md.

Testing

  • CI unit-test job green; new CpuParams (4), CpuThreadPool (2), and existing
    ResolverBlake3Cpu (3) suites pass.
  • Live-verified on Alephium (HeroMiners) on Windows (CPU, GPU, and GPU+CPU) and Linux CPU
    (Docker) — jobs received, hashed, shares accepted.

Notes

  • All OpenMP/libomp plumbing is removed — no build, CPU or GPU, links it. CPU
    parallelism is now the in-process std::thread pool, which ships zero runtime deps.
  • GPU-only builds (BUILD_CPU=OFF) simply don't compile the CPU resolver, so they're
    otherwise unchanged.
  • History squashed to 3 commits: CPU device + resolver + thread pool · build axes · docs.

@yuzi-co
yuzi-co marked this pull request as draft June 11, 2026 16:25
@yuzi-co
yuzi-co force-pushed the feat/cpu-blake3-mining branch 4 times, most recently from b38c058 to dbca62a Compare June 12, 2026 05:37
@yuzi-co
yuzi-co marked this pull request as ready for review June 12, 2026 05:38

@luminousmining luminousmining left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have only looked at the first files.
I will do a more thorough analysis later.

Comment thread sources/algo/blake3/CMakeLists.txt Outdated
Comment thread sources/device/cpu.cpp Outdated
Comment thread sources/device/cpu.cpp Outdated
Comment thread sources/device/device.cpp Outdated
Comment thread sources/device/device_manager.cpp Outdated
Comment thread sources/device/device_manager.cpp Outdated
Comment thread sources/device/device_manager.cpp Outdated
@yuzi-co
yuzi-co force-pushed the feat/cpu-blake3-mining branch from dbca62a to f3a2304 Compare June 12, 2026 09:45
yuzi-co added 4 commits June 14, 2026 04:28
Add a CPU mining path for BLAKE3 (Alephium): a no-SDK DeviceCpu that mirrors the mocker device and runs on the global BLAKE3 pool, a ResolverCpuBlake3 nonce scanner, and a self-owned persistent pinned std::jthread pool (CpuThreadPool) exposing --cpu_threads (pool size) and --cpu_affinity (hex core bitmask). The pool replaces a serial scan with real multicore on every platform (the previous OpenMP pragma was a no-op under clang-cl, leaving Windows single-threaded). Also enumerate the CPU device, map BLAKE3 to it, and show CPU hashrate in the dashboard.
Make the CPU resolver an independent build axis from the GPU backend: GPU=amd|nvidia|both|none combined with CPU=ON|OFF across docker/Dockerfile.linux, docker/Dockerfile.windows-cross, and scripts/docker-build.ps1, with CI building CPU-only images (GPU=none CPU=ON). CMake gains BUILD_CPU + the CPU_ENABLE define and links the host blake3_ref/blake3_pow under BUILD_CPU. The CPU resolver parallelizes via an in-process std::thread pool, so no OpenMP/libomp runtime is installed or staged.
Update ARCHITECTURE.md and CLAUDE.md (CPU resolver = std::thread pool), document --cpu_threads/--cpu_affinity in PARAMETERS.md, add CPU_TUNING.md (thread-count/affinity guidance incl. reserving cores for the GPU), and refresh the docker build docs for the GPU/CPU axes.
- Self-guard resolver/cpu/blake3.hpp with CPU_ENABLE so device.cpp and
  device_manager.cpp include the CPU headers unconditionally; deactivation is
  the header's own responsibility, not every include site.
- DeviceCpu::getMinimumKernelExecuted: use common::max_limit (project helper)
  in place of std::min.
- Drop the explanatory comments flagged in review.
@yuzi-co
yuzi-co force-pushed the feat/cpu-blake3-mining branch from f3a2304 to 943f6ba Compare June 14, 2026 01:34
@yuzi-co

yuzi-co commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main to clear the conflict (force-pushed; tip 943f6ba).

The conflict came from the vcpkg/deps CMake refactor that landed on main after this branch was opened. Since that refactor already introduces the CPU build axis, I dropped my old root-CMakeLists.txt edits entirely — they're now redundant:

  • BUILD_CPU option and add_compile_definitions(CPU_ENABLE) now live in cmake/deps/cpu.cmake.
  • sources/resolver/CMakeLists.txt already does if (BUILD_CPU) add_subdirectory(cpu), and sources/resolver/cpu/CMakeLists.txt globs *.cpp/*.hpp, so the new CPU resolver/thread-pool/affinity sources are picked up automatically. sources/device/ globs device/cpu.cpp the same way.
  • The only build-file change this branch still carries is the small sources/algo/blake3/CMakeLists.txt hunk that links blake3_ref under BUILD_CPU.

The docker/scripts CPU-axis changes merged cleanly onto the refactored docker layout, and the --cpu_threads/--cpu_affinity rows in PARAMETERS.md merged alongside your new --cpu row.

Verified by cross-compiling the new tree (GPU=amd CPU=ON BUILD_TESTS=ON via docker/Dockerfile.windows-cross) and running the suite on an RX 9070 XT host — 15/15 green:
ResolverBlake3CpuTest (3), CpuParams (4), CpuThreadPool (2), plus Blake3Ref/Blake3SharedKat/AutolykosV2CpuShare.

Ready for the deeper review when you have time.

@luminousmining

Copy link
Copy Markdown
Owner

Thank you for this first CPU resolver implementation!

My understanding:
If I understood your implementation correctly, we have one DeviceCPU per physical CPU present on the machine and therefore one ResolverCpuX per thread.
By default we create one device / thread in DeviceManager.
Then a ResolverCpuX creates N threads via CpuThreadPool to resolve hashes.

Remarks:
Currently the nonce resolution does not perform any collaborative work between threads.
We simply have a distribution of a nonce batch across threads.

The pool.parallelFor function performs a kind of "wait" — we launch execution on N threads and wait for all threads to finish before processing another batch.

Proposal 1:
I propose creating as many DeviceCpu instances as the number of threads we want to use, which will therefore also create as many ResolverCpuX as needed.
The nonce dispatcher is already implemented by default — we would keep the same logic as the other devices.
This would allow us to no longer have a "wait" between threads.

Proposal 2:
DeviceCpu is probably too generic — the Blake3 code has no asm instructions, but when I want to optimize CPU algorithms we will need to switch to asm. Obviously asm OPCODEs are not compatible across architectures, so we need to plan for at least 2 types of DeviceCpu: one for x64 and one for arm.

Problem:
My proposal raises one issue — in the case of MoneroX or other algorithms (we could also run progpow on CPU) that need to create a dataset, we would need to implement a barrier system and collaborative work so that all DeviceCpu instances available on the same algorithm can share the work of creating this dataset.

I will let you tell me what you think — I am open to any proposal.
It is better to think carefully and define the DeviceCpu architecture properly even if it takes time ;)

Comment thread sources/device/device.cpp Outdated

uint32_t device::Device::getMinimumKernelExecuted() const
{
return common::Config::instance().occupancy.kernelMinimunExecuteNeeded.value_or(100u);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common::Config& config{ common::Config::instance() };
uint32_t const  executeCount{ .occupancy.kernelMinimunExecuteNeeded };
return executeCount;

Does not need .value_or(100u) the default value is 100

}
#endif

#if defined(CPU_ENABLE)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need 2 empty lines

Comment thread sources/device/device_manager.cpp
Comment thread sources/resolver/cpu/cpu_params.hpp Outdated
#include <utility>


namespace resolver::cpu_detail

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong define namespace look resolver nvidia or amd
namespace resolver { namespace cpu

Comment thread sources/resolver/cpu/cpu_params.hpp
Comment thread sources/resolver/cpu/cpu_affinity.cpp Outdated
bool resolver::pinThisThreadToCore([[maybe_unused]] uint32_t const coreIndex)
{
#if defined(_WIN32)
DWORD_PTR const mask{ static_cast<DWORD_PTR>(1ull << coreIndex) };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create a custom cast in cast.hpp ?

Comment thread sources/resolver/cpu/cpu_affinity.hpp Outdated
{
// Pin the CALLING thread to a single logical core. Returns true on success.
// macOS (and any platform without a hard-affinity API): best-effort no-op, returns false.
bool pinThisThreadToCore(uint32_t coreIndex);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint32_t const coreIndex

Comment thread sources/resolver/cpu/cpu_params.hpp
Comment thread sources/resolver/cpu/thread_pool.cpp Outdated
resolver::CpuThreadPool::~CpuThreadPool()
{
{
std::lock_guard<std::mutex> const lock{ mutex };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use custom UNIQUE_LOCK from custom.hpp

Comment thread sources/resolver/cpu/thread_pool.hpp Outdated
// core at startup. parallelFor() splits [0, count) into contiguous per-worker chunks and
// blocks until all workers finish. Mining hits are astronomically rare, so callers guard
// any shared write with their own mutex inside the chunk function.
class CpuThreadPool

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the use of jthread but in this project I prefer to stay close to the boost framework, and to keep consistency with device_manager can you study the possibility of switching to boost::thread or boost::scoped_thread or other alternatives — I will leave the research to you.
Taking into account my remark that 1 thread could be a devicecpu, CpuThreadPool would therefore no longer be needed.

@yuzi-co

yuzi-co commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful read, and for wanting to settle the DeviceCpu architecture before we add more algorithms — agreed it's worth the time. You read the current design correctly: one DeviceCpu, an internal CpuThreadPool, and parallelFor that fans a nonce batch across N threads and joins at the end.

On the "wait" — you're right, and I'd fix it one layer in rather than with device-per-thread

The batch barrier in parallelFor is a real cost, and it's worst exactly where we're headed: heterogeneous cores (Intel/Apple P+E, ARM big.LITTLE) make a fast core finish early and idle waiting on a slow one — on a 2:1 pair, equal static chunks throw away ~⅓ of the fast core. So the batch barrier should go regardless.

But I'd remove it by making the pool workers persistent and work-stealing — each pulls its next nonce range from a lock-free counter and submits shares immediately — rather than by exploding into one device per thread. Mining has no fixed completion point (you search a job's nonce space until the next job arrives, ~1/s), so this turns the sync rate from per-batch into per-job, and that one sync is the atomic job flag we already use. The per-range atomic amortizes to well under 1% (tunable via range size; invisible for slow algos). It also stays correct where we can't pin — macOS gives no hard affinity on Apple Silicon, only QoS hints — because stealing self-balances wherever the OS places a thread.

Worth noting the hand-managed pool already beats OpenMP substantially on Windows (1.5 → 11 MH/s). So OpenMP is settled; the open question now is just the work-distribution model on top of the pool, where the steady-state delta between approaches is small and the real differentiator is dataset ownership.

Why I'd keep parallelism inside the device

The manager's per-device model assumes devices are independent and share no state — true for GPUs, where each card is its own world. CPU cores running a dataset algorithm are not independent: they share a multi-GB dataset. Device-per-thread then forces either N copies of that dataset (RandomX fast-mode is 2 GB × N threads → OOM) or a new shared-dataset-plus-barrier subsystem in the manager — which is exactly the Problem you flagged. The pool already is that subsystem: it's the one piece the device model deliberately lacks, parallel workers that share state.

To be fair about it: for dataset algos the pool does carry a barrier — all workers wait for the dataset to be built. But that barrier fires once per epoch/seed change (~3 days for RandomX, ~5 for ethash-family), the build is parallelizable across the same pool workers (seconds, not minutes), and it amortizes to ~0.002%. It also exists in every shared-dataset design — device-per-thread has the same barrier, just harder (cross-device coordination instead of one owner). And if those seconds ever matter, the pool can double-buffer the dataset exactly like the GPU path double-buffers streams: mine the old dataset while building the new, then hot-swap.

Why this isn't a BLAKE3-only call — the algo landscape

CPU algorithms split cleanly by one question: self-contained hash, or shared dataset?

Algos Architecture impact
A. Independent per-worker yespower/yescrypt family (Sugarchain, CPUchain, MicroBitcoin…), VerusHash, GhostRider/Raptoreum, CryptoNight, BLAKE3 neutral — pool or device-per-thread both fine
B. Shared dataset + epoch barrier RandomX/Monero, Verthash, ProgPoW/ethash-on-CPU decides the design — needs a shared-dataset owner

Bucket A doesn't push back on either design (the whole yespower family is one parametrized resolver — nice cheap breadth later). Bucket B is where the architecture is decided, and it holds the algos actually worth CPU-mining. The next target after BLAKE3 is RandomX — I've already prototyped a resolver that shares a light-mode cache across per-worker VMs, and that sharing is trivial with one owner and painful across N devices. ProgPoW-on-CPU (your suggestion) isn't competitive to mine (4 GB DAG, bandwidth-bound), but it's a great free validation of the Bucket B path since the GPU side already has the DAG infra.

So I'd let Bucket B drive it, which points to: one DeviceCpu per cache/NUMA domain, a work-stealing pool over that domain's cores sharing one dataset replica. "Per physical CPU" is too coarse for multi-CCD/dual-socket (remote cores eat NUMA latency); "per thread" is too fine (can't share the dataset). To keep this PR small, I'd define the abstraction now — device = pool over cores + optional shared dataset — ship it as a single device over all cores, and defer the per-domain split until RandomX needs it.

Proposal 2 (x64 vs arm) — agree on the goal, one layer down

None of the Device's jobs are arch-specific; only the inner hash kernel is, and that already lives in the resolver. A static x64/arm split is also too coarse — on x64 the throughput-relevant splits are AES-NI-vs-not and AVX2-vs-AVX-512, and one binary runs on all of them, so we need runtime CPU-feature dispatch (cpuid/getauxval) regardless. Every serious CPU algo proves this: VerusHash wants AES-NI and PCLMULQDQ, yespower wants AVX2/SHA-NI, BLAKE3 wants AVX2/AVX-512/NEON — and RandomX literally JITs native code per VM at runtime, which is runtime ISA dispatch taken to its conclusion. So: keep one DeviceCpu; put ISA-specialized hot loops in the resolver in separate TUs (blake3_avx2.cpp, blake3_neon.cpp, target-attribute guarded), selected at runtime. Affinity becomes an optional platform policy behind one interface (Linux sched_setaffinity, Windows SetThreadAffinityMask/SetThreadGroupAffinity, macOS QoS, else no-op), with the design correct when pinning is off.

Net

Keep the device as the owner of CPU parallelism, swap the batch barrier for a work-stealing pool, set granularity at the cache/NUMA domain (one device for now), and push arch/ISA into the resolver via runtime dispatch. The steady-state perf difference between this and device-per-thread is ~nil; the real difference is dataset ownership, where the pool wins. Happy to sketch the CpuThreadPool work-stealing change and the resolver dispatch seam as a follow-up commit on this branch — or to talk any of it through first.

- cpu_params.hpp: inline specifier on its own line (nthSetBit, parseHexMask);
  use castU32/castU64 from cast.hpp instead of static_cast
- cpu_affinity.hpp: const-qualify pinThisThreadToCore parameter
- tests/cpu_params.cpp: move using-namespace into each TEST body
- device_manager.cpp: two blank lines between CPU/CUDA guard blocks
@yuzi-co

yuzi-co commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 08e90fb addressing the mechanical style nits from your review:

  • cpu_params.hppinline specifier on its own line for nthSetBit and parseHexMask (the other two already did it); swapped the static_cast<…> calls for castU32/castU64 from cast.hpp.
  • cpu_affinity.hpp — const-qualified the parameter: pinThisThreadToCore(uint32_t const coreIndex) to match the definition.
  • tests/cpu_params.cpp — removed the file-scope using namespace and moved it into each TEST body.
  • device_manager.cpp — two blank lines between the #if-guarded init blocks.

Still open, intentionally:

  • thread_pool.hpp (jthreadboost, and "1 thread could be a DeviceCpu") — this is the core of the architecture discussion above, so I've left CpuThreadPool untouched until we converge on device granularity. If we keep a pool, I'll switch it to the boost equivalent then.
  • max_limit vs min_limit (cpu.cpp) — replied inline; max_limit is the ceiling (std::min) we want here. Happy to switch if you intended different semantics.
  • A few I'd like your steer on before touching: dropping .value_or(100u) (the config field is currently std::optional, so that needs the field itself to become a plain uint32_t{100} — fine to do, just wanted to confirm); the cpu_detail namespace (keep the _detail name or rename to cpu?); and the DWORD_PTR custom cast (it's a Windows-only type, so I'd guard it in cast.hpp behind _WIN32 — ok?).

@luminousmining

Copy link
Copy Markdown
Owner

Your solution works perfectly for me!

I would change only one thing to keep the same logic throughout the code. I would like parallelFor to take a callback — so do a setCallback then trigger a run instead of passing a lambda. Each ResolverCpu should call pool.run and not pool.parallelFor — this changes nothing performance-wise but stays consistent with the overall current architecture of the software.

Also, pool to refer to CpuThreadPool is too abbreviated and can cause confusion with the notion of a mining pool, so it would be better to rename pool to threadPool?

@luminousmining

Copy link
Copy Markdown
Owner

Regarding .value_or(100u): if we indicate that the flag has a default value then the 100 should be handled directly. If we indicate optional it simply means that the user does not need to define it and it can be ignored, but if we add a default value then this flag is still taken into account with a default value to be defined in the config .hpp.

cpu_detail → rename to cpu. The namespace logic is the same everywhere across all files — it corresponds to the folder layer. If the file is in toto/tata/titi then the namespace is:

namespace toto
{
    namespace tata
    {
        namespace titi
        {
            // code here
        }
    }
}

- thread pool: switch std::jthread/std::mutex/std::condition_variable to
  boost::thread/boost::mutex/boost::condition_variable + the UNIQUE_LOCK
  macro for consistency with device_manager; join workers explicitly in
  the destructor.
- thread pool API: replace parallelFor(count, fn) with setCallback(fn) +
  run(count); ResolverCpuBlake3 calls threadPool.run().
- rename pool member `pool` -> `threadPool` (avoid confusion with a
  mining pool).
- rename namespace resolver::cpu_detail -> resolver::cpu (folder layer).
- config: occupancy.kernelMinimunExecuteNeeded becomes a plain
  uint32_t{100} default instead of optional + value_or(100u) at the read.
- resolver/cpu/cpu.cpp: hoist the threads/blocks ternaries into locals.
- blake3.cpp: drop the anonymous namespace; move the occupancy constants
  into the constructor body.
- cast.hpp: add a _WIN32-guarded castDWORDPTR macro; use it in
  cpu_affinity.cpp.

CPU + Blake3 unit suites green (CpuParams, CpuThreadPool, ResolverBlake3Cpu).
@yuzi-co

yuzi-co commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The round-2 items are in (the callback setCallback/run shape, poolthreadPool, cpu_detailcpu, the kernelMinimunExecuteNeeded default, and the style nits).

One thing to settle before I wire setCallback + run in for good: the work-stealing change we discussed reshapes run(), so it's worth deciding the API now rather than twice.

The change. Move the CPU pool from blocking per-batch (run(count)) to a continuous, work-stealing engine synced per job: workers pull small nonce ranges from a lock-free cursor and run free over a job's space, syncing only at a job change. The CPU device gets its own observer loop behind a new virtual mineLoop(); the GPU loop is untouched.

This is intentionally not the GPU model. The GPU already balances in hardware and hides batch cost with double-buffered streams, and its kernels are bounded launches — a persistent "run until the job changes" kernel would risk driver-watchdog kills — so the batch loop is the right fit there. The CPU has neither property: static equal chunks leave fast cores idle on heterogeneous cores, and CPU threads can run free. So the CPU path diverges, behind one virtual method, with the GPU path unchanged.

Pros

  • Removes the idle-fast-core case: ~+15–30% for BLAKE3 on a 2:1 P/E split, and smooths per-hash variance (GhostRider, RandomX) even on uniform CPUs.
  • Sync drops from per-batch to per-job (~1/s).
  • Fits RandomX (next target): a dataset-agnostic pool + two-tier job/memory path lets its shared ~2 GB dataset (one replica across workers — device-per-thread would OOM) drop in without engine rework; a per-resolver grain keeps one engine balanced from cheap (BLAKE3) to expensive (RandomX) hashes.
  • Keeps setCallback and the callback signature; reuses submit/isStale/stats; works without pinning (macOS QoS).

Cons

  • API churn: it replaces the run(count) we just settled on with a non-blocking drive (start/newJob/quiesce/takeProcessed/stop). The callback survives; only the driving verb changes.
  • Most of the gain is the stealing itself; the part unique to per-job sync is <1% (→0% on the heavy shared-dataset algos) — added complexity for a small slice on top of the balancing win.
  • The CPU diverges from the shared loop (own observer/stats/hit-handoff) and adds concurrency surface (quiesce barrier, epoch fence, atomic cursor) that needs careful tests.

Lighter alternative. Dynamic stealing inside the existing run(count) — keeps the API and the shared loop, captures essentially all the balancing win, leaves the <1% barrier, and is lower-risk. It doesn't pre-build the RandomX seams as cleanly.

Mild preference for the continuous version given RandomX is next, but the lighter one is a reasonable stopping point if you'd rather keep run(count) and minimize churn. Happy to build to whichever you prefer.

@luminousmining

Copy link
Copy Markdown
Owner

For the CPU batch part there is an even simpler method that would cause no interruption. We are going to "copy" the GPU buffering principle.

Some explanations:
The ::executeSync function means it has a synchronous execution on the process — that is, when it is executed it will "block", we will have to wait for all executions to finish → it causes a "wait".

The ::executeAsync function indicates that it can be launched and then we move on to something else — we are not forced to wait for the executions to stop.

For GPUs, the use of 2 streams allows launching an executeAsync call and during that time continuing to synchronize with the stratum, then on the next executeAsync call we switch streams and process the result of the previous stream → this allows a continuous execution flow.

Parallel with the CPU:

The executeSync function will therefore launch threadPool.run() and cause a wait — this is not a problem, it is even the desired behavior! For the development / debug / test phase it is much more practical!

The executeAsync function must not cause this blocking!
To achieve this we can have a rotating buffer — that is, we execute the nonce search by indicating in which buffer we want the output to be written; on the next executeAsync call the swapIndex allows switching the output buffer index!
So while we retrieve the stratum info and process the response provided by the previous executeAsync, our threads are still computing without stopping.

This can cause a "blocking" in the unique case where executeAsync finds an enormous number of nonces (near-zero probability) or if emitting mining.submit takes time.

But even in that scenario we can ignore this "latency".

@yuzi-co

yuzi-co commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Agreed — the double-buffer is the cleaner route, and it reuses the stream pattern that's already there. It also keeps run(count) as-is (the executeSync path) and keeps executeSync/executeAsync meaning what they say, so the churn I was worried about goes away.

To confirm I have it right: executeSync stays blocking (threadPool.run()) for tests/debug; executeAsync launches the pool into output buffer [idx] and returns immediately, and the next call flips swapIndex — so while the device thread reads/submits the previous buffer and syncs with stratum, the workers keep computing into the other one. The only stall is the rare "huge number of hits / slow submit" case, which we can ignore.

One orthogonal addition worth considering. The double-buffer removes the wait between batches (the thing you flagged). What it doesn't touch is imbalance inside a batch: with static equal chunks, on heterogeneous cores (P/E, big.LITTLE, SMT, thermal) a fast core finishes its chunk early and idles at the batch tail until the slow one catches up. A cheap fix that composes with the double-buffer: inside a batch, replace the equal split with a small atomic cursor each worker pulls ranges from, so a fast core just takes more ranges. No change to the buffer scheme or the loop — purely how run() hands out work internally.

It's optional — on a uniform desktop it's a no-op, and it mainly pays off on laptops/hybrid CPUs and on variable-cost algos (GhostRider, RandomX). Happy to fold it in or leave it; either way I'll start with the double-buffer.

@luminousmining

Copy link
Copy Markdown
Owner

You understood correctly!

For executeAsync the index should only be swapped if the previous batch has finished → take the GPU implementation as a reference. A mini blocking can occur if a batch was launched and we don't have the response yet → acceptable behavior.

For the execution imbalance, I think this is an acceptable tradeoff to keep the code readable and maintainable!

We could bench both approaches in a separate executable outside of this scope to compare if we really have any waiting. The benchmark should be run on a machine with no other processes running to be ISO prod on a real mining session.

@yuzi-co

yuzi-co commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — that settles the double-buffer. I've wired executeAsync exactly as you described: it wait()s for the in-flight batch first and only then swaps the output buffer, so the index never flips before the previous batch is done; the rare "launched but no response yet" case is the small acceptable block. executeSync stays blocking for tests/debug.

On the in-batch imbalance — I hear you on keeping it simple, and I don't want a second dispatch path to maintain. So instead of a static-vs-stealing fork, what about one path with a grain knob: workers pull grain-sized ranges from a single atomic cursor, and grain defaults to ceil(count / workers) — i.e. one chunk per worker, behaviour-identical to the equal-split today. "Work-stealing" then isn't a separate mode, it's just a smaller grain, opt-in and off by default. One code path, default unchanged, nothing extra to read in the prod loop.

And I'd settle it with data the way you suggested: an A/B (default grain vs small grain) in a separate benchmark exe, idle machine, ISO-prod — keep the knob only if it actually shows waiting on a hybrid CPU; otherwise it stays a no-op default and we drop it.

If you'd rather not touch the equal-split at all for this PR, I'm happy to ship the double-buffer alone and revisit the knob alongside RandomX (where the shared dataset makes it matter more). Your call.

@luminousmining

Copy link
Copy Markdown
Owner

Regarding grain, I am having trouble understanding what you have in mind.
Could you make a diagram / pseudo code to make it clearer?

@yuzi-co

yuzi-co commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Sure — here's the whole idea in pseudocode. The pool keeps one dispatch path; grain is just the size of the range each worker claims at a time.

// per batch (shared):
atomic<u64> cursor = 0
u64 count            // blocks * threads, the batch size
u64 grain            // how big a slice each worker grabs

// every worker runs the same loop:
for (;;)
{
    u64 lo = cursor.fetch_add(grain)   // atomically claim the next slice
    if (lo >= count) break             // nothing left
    u64 hi = min(lo + grain, count)
    callback(lo, hi)                   // hash nonces [lo, hi)
}

Behaviour is entirely controlled by grain:

Default — grain = ceil(count / workers) → same as today's equal split.
4 workers, count = 4000 → grain = 1000, so the cursor hands out exactly 4 slices, one per worker:

[0,1000) [1000,2000) [2000,3000) [3000,4000)
   w         w           w           w

Each worker does ~one fetch_add and exits — same slices as the static split, just produced by the cursor instead of chunkRange.

Opt-in — smaller grain → stealing.
Same batch, grain = 500 → 8 slices:

[0,500)[500,1k)[1k,1500)[1500,2k)[2k,2500)[2500,3k)[3k,3500)[3500,4k)

A fast core that finishes its slice loops back and grabs the next free one; a slow core grabs fewer. Coverage is still exactly [0,count), once each — the only difference is a fast core no longer idles at the tail waiting on the slowest.

So there's no second mode and no flag branch in the loop: the prod default reproduces the current behaviour (same slices, one per worker), and "work-stealing" is purely "use a smaller grain" — which I'd only enable if the separate benchmark shows real tail-waiting on a hybrid CPU.

@luminousmining

Copy link
Copy Markdown
Owner

Thread Pool Design — Atomic Cursor & Work-Stealing

Variables

Variable Value Meaning
threads 1024 Number of nonces per block
blocks 128 Number of blocks
count 131 072 Total nonces to hash (threads × blocks)
workers 4 Number of std::jthread in CpuThreadPool
grain 32 768 Slice size per worker (count / workers)

CpuThreadPool

Each DeviceCpu owns its own CpuThreadPool. If the machine has multiple CPUs, each DeviceCpu manages its pool independently.

Each worker runs the same loop:

lo = cursor.fetch_add(grain, std::memory_order_acquire);
if (lo >= count) break;
callback(lo, lo + grain);

The acquire ordering is critical: without it, a CPU core could read a stale cached value of the cursor and claim an already-taken slice.

With grain = 32768 (default):

W0 → callback(0,     32768)  →  nonces 0 to 32767
W1 → callback(32768, 65536)  →  nonces 32768 to 65535
W2 → callback(65536, 98304)  →  nonces 65536 to 98303
W3 → callback(98304, 131072) →  nonces 98304 to 131071

Each worker does exactly 1 fetch_add then exits. Identical behaviour to a static split.

The 3 grain cases

grain Behaviour Problem
>= count 1 worker does everything, others do nothing Near-sequential
= count / workers 1 slice per worker, current default None
Too small Work-stealing, fast cores cover slow ones Contention on the atomic

⚠️ count too large → log warning

If count is very large, each worker ends up with a huge slice to process. All workers take a long time to finish — significant wait before the next batch. The grid is not optimised and the user must be warned:

[WARNING] Grid size is too large (threads=X blocks=Y count=Z).
          Consider reducing --threads or --blocks to improve batch responsiveness.

@luminousmining

Copy link
Copy Markdown
Owner

If my previous message is correct and this suits you, we can move on to the implementation.

@yuzi-co

yuzi-co commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Your spec matches what I had in mind — agreed, let's implement.

Final design as I'll build it:

  • Double-buffer: executeSync blocks (threadPool.run()) for tests/debug; executeAsync launches into output buffer [idx], swaps swapIndex only after the in-flight batch's wait() completes — same as the GPU stream path.
  • Grain dispatch: one loop, cursor.fetch_add(grain); grain = count / workers by default (one slice per worker, identical to today's static split); smaller grain = opt-in stealing.
  • Grid warning: [WARNING] when count is large enough to hurt batch responsiveness.

One small note on the cursor ordering: since fetch_add is an atomic RMW, it always reads the latest value in the modification order, so a slice can't be double-claimed even under memory_order_relaxedacquire only matters for ordering the callback's own reads/writes against the cursor, which here are independent ranges. I'll go with relaxed for the claim unless you'd prefer acquire for clarity. No impact on correctness either way.

I'll also switch CpuThreadPool off jthread to the boost equivalent in the same pass, per your earlier note.

Mirror the GPU two-stream double-buffer on the CPU path so the device thread
reads/submits the previous batch and syncs with stratum while the worker pool
computes the next one:

- CpuThreadPool gains runAsync()/wait() alongside the blocking run(); a batch is
  now dispatched through a single atomic cursor that workers pull grain-sized
  slices from. The default grain (ceil(count / workers)) reproduces the old
  static equal split exactly; a smaller grain turns the same loop into
  work-stealing (no-op default, left for a separate benchmark to enable).
- ResolverCpuBlake3::executeAsync now waits on the in-flight batch, harvests it,
  swaps buffers and launches the next one before returning, matching
  ResolverNvidiaBlake3::executeAsync. Header/target/base are copied per buffer
  since the worker closure outlives the dispatching call. executeSync stays
  blocking for tests/debug.
- Warn when blocks*threads is large enough to hurt batch responsiveness.

Tests: cursor coverage under small grain, runAsync()+wait() coverage, and an
executeAsync double-buffer find/submit. CPU suites green (CpuThreadPool,
ResolverBlake3CpuTest, CpuParams, ResolverProgpowCpuTest).
@yuzi-co

yuzi-co commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Pushed cc757df implementing the design we settled on.

Double-buffer (ResolverCpuBlake3) — CPU mirror of the GPU two-stream path (ResolverNvidiaBlake3::executeAsync):

  • executeAsync waits on the batch dispatched by the previous call, harvests it into resultShare, swaps buffers, then launches the next batch into the now-idle buffer and returns immediately. While the device reads/submits the harvested buffer and syncs with stratum, the workers keep computing. The index only flips after the in-flight batch's wait() returns, so the rare "launched but no response yet" case is the only (acceptable) block.
  • executeSync stays blocking (threadPool.run()) for tests/debug.
  • Header/target/base are copied per buffer (Batch) since the worker closure outlives the executeAsync call that dispatched it — it can't hold references to the caller's jobInfo. Result labeling uses the live jobInfo like the GPU path; submit() drops anything isStale.

Grain cursor (CpuThreadPool) — one dispatch path:

  • Added runAsync()/wait() alongside the blocking run(). A batch is dispatched through a single atomic cursor; each worker pulls grain-sized slices via cursor.fetch_add(grain, std::memory_order_relaxed) until [0, count) is drained.
  • Default grain = ceil(count / workers) → one slice per worker, behaviour-identical to the old static equal split. A smaller grain turns the same loop into work-stealing — no-op default, left for the separate benchmark to enable.
  • relaxed ordering: fetch_add is an atomic RMW, so it always sees the latest value in the modification order and never double-claims a slice; the slices are independent and the lone shared write (the hit append) keeps its own mutex.

Grid warningoverrideOccupancy logs [WARNING] when blocks*threads is large enough to hurt batch responsiveness, suggesting smaller --threads/--blocks.

jthreadboost::thread was already done in the round-2 pass, so no change there.

Tests (CPU suite green on an arm64 macOS host, CPU-only build): cursor coverage under small grain, runAsync()+wait() coverage, and an executeAsync double-buffer find/submit, plus the existing CpuThreadPool/ResolverBlake3CpuTest/CpuParams/ResolverProgpowCpuTest suites.

The in-batch imbalance knob (smaller grain) is wired but off by default per your call — I'll A/B it in the separate benchmark exe on an idle hybrid CPU and only keep it if it shows real tail-waiting.

…freed

threadPool is declared before batch[], so it is destroyed last: when the device
exits with an async batch still in flight, a worker finishing that batch during
threadPool's join would read already-freed batch[] memory (use-after-free). Wait
on the in-flight batch in ~ResolverCpuBlake3 before the buffers are torn down.
@yuzi-co

yuzi-co commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Pushed b57d5dd — a shutdown use-after-free fix in the CPU Blake3 resolver.

CpuThreadPool threadPool is declared before Batch batch[2], so on destruction batch[] is freed first and threadPool's destructor (which joins the workers) runs last. If the device thread exits with an async batch still in flight — the loop doesn't drain on exit — a worker finishing that batch during the join reads already-freed batch[] memory.

Fix: drain the in-flight batch in ~ResolverCpuBlake3 before the buffers are torn down:

resolver::ResolverCpuBlake3::~ResolverCpuBlake3()
{
    if (true == inFlight)
    {
        threadPool.wait();
        inFlight = false;
    }
}

It's a narrow window (shutdown / algorithm switch with a batch outstanding) and on Blake3 it's a benign read of POD buffers rather than a crash, but it's a real UAF so worth closing. Blake3+thread-pool tests stay green.

Comment thread sources/common/cast.hpp Outdated
Comment thread sources/device/device.cpp Outdated

uint32_t device::Device::getMinimumKernelExecuted() const
{
return common::Config::instance().occupancy.kernelMinimunExecuteNeeded;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add tmp var

Comment thread sources/device/device_manager.cpp Outdated
#include <device/device_manager.hpp>
#include <device/mocker.hpp>
#include <device/nvidia.hpp>
#include <device/cpu.hpp>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alpha order

Comment thread sources/resolver/cpu/cpu_params.hpp Outdated
uint64_t const mask,
uint32_t const hardwareConcurrency)
{
if (threads.has_value() && 0u < *threads)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefere compare std::nullopt

Comment thread sources/resolver/cpu/cpu_params.hpp Outdated
{
// 0-based bit position of the k-th set bit of mask, or 64 if fewer than k+1 bits set.
inline
uint32_t nthSetBit(uint64_t const mask, uint32_t k)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a function specific to the CPU but rather bit manipulation.
Place it in bitwise.hpp.

Comment thread sources/resolver/cpu/tests/blake3.cpp Outdated
#include <stratum/job_info.hpp>


namespace

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace empty is forbidden

{
using namespace resolver::cpu;

for (uint64_t const total : { 0ull, 1ull, 7ull, 100ull, 262144ull })

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create tmp var contains { 0ull, 1ull, 7ull, 100ull, 262144ull }


for (uint64_t const total : { 0ull, 1ull, 7ull, 100ull, 262144ull })
{
for (uint32_t const n : { 1u, 3u, 8u })

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create var to contain { 1u, 3u, 8u }.


for (uint64_t const total : { 0ull, 1ull, 7ull, 100ull, 262144ull })
{
for (uint32_t const n : { 1u, 3u, 8u })

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n ? explicit name please

Comment thread sources/resolver/cpu/blake3.cpp Outdated

// Parse the affinity mask exactly once: the worker-count resolution needs it (popcount
// when --cpu_threads is unset) and the pool needs it for pinning.
uint64_t const mask{ config.cpu.affinity.has_value() ? resolver::cpu::parseHexMask(*config.cpu.affinity) : 0ull };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefere std::null opt

@luminousmining

Copy link
Copy Markdown
Owner

I have done a preliminary review.
I am now going to carefully re-read ResolverCpu and CpuThreadPool!

- move nthSetBit and parseHexMask into algo/bitwise.hpp; rename the latter
  hexToDecimal and template it over 8/16/32/64-bit widths
- nest CpuThreadPool and the affinity helper under resolver::cpu
- compare std::nullopt instead of std::optional::has_value()
- hoist temporaries (getMinimumKernelExecuted, affinity success flags, test
  literal lists) and drop the static one-shot warning guard
- alphabetize the device/cpu.hpp include; drop a stale cast.hpp comment
- replace the anonymous namespace in the CPU blake3 test with file-scope
  constexpr; relocate the moved helpers' tests to algo/tests/bitwise.cpp
Comment thread sources/resolver/cpu/thread_pool.hpp Outdated
class CpuThreadPool
{
public:
using ChunkFn = std::function<void(uint64_t lo, uint64_t hi, uint32_t workerIndex)>;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

look stratum.hpp alias for function must start with callback
Exemples :

        using callbackUpdateJob = std::function<void(uint32_t const stratumUUID, StratumJobInfo const&)>;
        using callbackShareStatus =
            std::function<void(bool const isValid, uint32_t const requestID, uint32_t const stratumUUID)>;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3a7b8eb — renamed the alias ChunkFncallbackJob to match the callback* convention in stratum.hpp.

Comment thread sources/resolver/cpu/thread_pool.hpp Outdated
CpuThreadPool& operator=(CpuThreadPool const&) = delete;

void setCallback(ChunkFn const& fn);
// grain == 0 selects the default ceil(count / workers) (one slice per worker).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3a7b8eb — removed.

Comment thread sources/resolver/cpu/thread_pool.hpp Outdated
private:
void workerLoop(uint32_t index);

uint32_t poolSize;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global message, variable should be all time initialize by default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3a7b8ebpoolSize{ 1u } and mask{ 0ull } are now default-initialized.

Comment thread sources/resolver/cpu/thread_pool.cpp
// boost::thread does not auto-join on destruction: join each worker explicitly.
for (boost::thread& worker : workers)
{
if (true == worker.joinable())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use .interrupt()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the explicit stopRequested flag rather than .interrupt(). boost::thread::interrupt() works by throwing boost::thread_interrupted at interruption points, which adds an exception path through the worker hot loop; the pool is also always quiesced before teardown (~ResolverCpuBlake3 drains any in-flight batch first), so the flag + notify_all is enough and keeps shutdown exception-free. Glad to switch to .interrupt() if you prefer it for consistency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6389b21 — switched to worker.interrupt() in the destructor; the worker loop now runs while (false == boost::this_thread::interruption_requested()) and catches the boost::thread_interrupted thrown out of the interruptible cvWork.wait(). The stop flag is gone.

Comment thread sources/resolver/cpu/thread_pool.cpp Outdated
UNIQUE_LOCK(mutex);
cvDone.wait(
lock,
[&]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create lambda auto isRemaining

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3a7b8eb — hoisted to auto const isRemaining{ [&]() { return 0u == remaining; } };.

Comment thread sources/resolver/cpu/thread_pool.hpp Outdated

uint32_t poolSize;
uint64_t mask;
ChunkFn job{};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cbJob cf ChunkFn rename

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3a7b8eb — member jobcbJob.

Comment thread sources/resolver/cpu/thread_pool.hpp Outdated
ChunkFn job{};
uint64_t total{ 0ull };
uint64_t sliceGrain{ 1ull };
std::atomic<uint64_t> cursor{ 0ull };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use AtomicCounter sources/common/atomic_counter.hpp
You can add new function setMemoryOrder to define if the boost::memory_order.
current.fetch_add(value, boost::memory_order::seq_cst); -> current.fetch_add(value, memoryOrder);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtomicCounter currently exposes only void add(), but the cursor needs the previous value returned by the RMW (lo = cursor.fetch_add(grain)), which AtomicCounter cannot provide today. To adopt it I would add a value-returning fetchAdd() plus a setMemoryOrder() to common/atomic_counter.hpp (the only other user is device.hpp). OK to extend the shared type that way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6389b21AtomicCounter::add (and sub) now return the RMW result and there is a setMemoryOrder() (plus a store() for resetting). get/sub/store all honor the configurable memoryOrder, which defaults to seq_cst so the device.cpp synchronizer counters are unchanged. The cursor is now common::AtomicCounter<uint64_t> set to relaxed ordering.

Comment thread sources/resolver/cpu/thread_pool.cpp Outdated
{
break;
}
uint64_t const hi{ std::min<uint64_t>(lo + jobGrain, jobTotal) };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use math.hpp -> MIN()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find sources/common/math.hpp or a MIN() anywhere on this branch (or on main). Did you mean a helper landing separately, or common/custom.hpp's max_limit/min_limit (those clamp, not min)? Kept std::min for now — point me at the intended one and I will switch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6389b21 — now uses algo::min() from sources/algo/math.hpp (I had been looking under common/). Thanks for the pointer.

Comment thread sources/resolver/cpu/thread_pool.cpp
Thread pool (sources/resolver/cpu/thread_pool.*):
- rename callback alias ChunkFn -> callbackJob (callback-prefixed, matching
  the stratum.hpp convention) and member job -> cbJob
- default-initialize poolSize and mask
- replace both for(;;) worker loops with explicit conditions: the outer loop
  is driven by an atomic stopRequested, the inner cursor drain by a for-loop
  condition
- hoist the wait predicates into named hasWork / isRemaining lambdas and the
  pinned-core index into a local
- drop a redundant comment

Blake3 resolver (sources/resolver/cpu/blake3.*):
- install the chunk callback once in the constructor instead of reassigning it
  on every execute call; it scans into whichever buffer currentIndexStream
  selects
- reuse the base-class double-buffer index (currentIndexStream/swapIndexStream)
  instead of a private currentIndex
- mark the unused worker-index parameter [[maybe_unused]]
- name the nonce-width magic numbers (SEARCH/FULL_NONCE_HEX_LENGTH)

Verified: CPU-only unit_test (GPU=none CPU=ON BUILD_TESTS=ON) builds and the
CPU/thread-pool/blake3 suites pass (incl. the executeAsync double-buffer test).
@yuzi-co

yuzi-co commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 3a7b8eb addressing round-4. Most items are mechanical and done (replied inline): the callbackJob/cbJob renames, default-initialized members, explicit loop conditions (stopRequested is now std::atomic<bool>, the cursor drain is a for-condition), hoisted hasWork/isRemaining/core locals, the callback installed once in the constructor, reuse of the base currentIndexStream/swapIndexStream(), [[maybe_unused]], and named nonce-width constants.

Verified: CPU-only unit_test (GPU=none CPU=ON BUILD_TESTS=ON) builds and the CPU / thread-pool / blake3 suites pass, including the executeAsync double-buffer test. All files run through clang-format 15.0.7.

Three I left as-is pending your steer (details inline):

  • math.hppMIN(): I cannot find common/math.hpp or any MIN() on this branch or main — which helper did you mean? Kept std::min for now.
  • AtomicCounter for the cursor: it only has void add(), but the cursor needs the value-returning RMW. I would add fetchAdd() + setMemoryOrder() to the shared type — OK to extend it?
  • .interrupt() vs the stop flag, and the two-line constructor initializer lists (clang-format 15 rejoins them to one line): kept the current form; happy to change either if you prefer.

@luminousmining

luminousmining commented Jun 19, 2026

Copy link
Copy Markdown
Owner

math.hpphttps://github.com/luminousmining/miner/blob/main/sources/algo/math.hpp#L12 — use the function algo::min().

You can make the add function return a value and add the setMemoryOrder() function.
The add function performs a fetch_add:

        void add(T const value)
        {
            current.fetch_add(value, boost::memory_order::seq_cst);
        }

I prefer .interrupt over flag stop.

…, interrupt)

Following the maintainer's answers to the three open questions:

- thread_pool: use algo::min() (sources/algo/math.hpp) instead of std::min.
- AtomicCounter (sources/common/atomic_counter.hpp): add/sub now return the
  RMW result, add setMemoryOrder() and store(); get/sub/store honor the
  configurable memoryOrder (default seq_cst, so device.cpp's synchronizer
  counters are unchanged). The CpuThreadPool cursor is now a
  common::AtomicCounter<uint64_t> set to relaxed ordering.
- thread_pool shutdown: replace the stop flag with boost::thread::interrupt();
  the worker loop runs while !interruption_requested() and catches
  boost::thread_interrupted thrown out of the interruptible cvWork.wait().

Verified: CPU-only unit_test (GPU=none CPU=ON BUILD_TESTS=ON) builds and the
full suite passes (92/92), incl. the executeAsync double-buffer test and the
thread-pool / device synchronizer coverage.
@yuzi-co

yuzi-co commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 6389b21 — applies all three of your answers (replied inline):

  • algo::min() from sources/algo/math.hpp replaces std::min in the pool.
  • AtomicCounter: add/sub now return the RMW result, added setMemoryOrder() + store(); all accessors honor the configurable memoryOrder (default seq_cst, so the device.cpp synchronizer counters are unchanged). The cursor is now common::AtomicCounter<uint64_t> on relaxed ordering.
  • .interrupt() replaces the stop flag: the worker loop runs while (!interruption_requested()) and catches boost::thread_interrupted out of the interruptible cvWork.wait().

Verified: CPU-only unit_test (GPU=none CPU=ON BUILD_TESTS=ON) builds and the full suite passes (92/92), including the executeAsync double-buffer test and the device synchronizer coverage. All files clang-format 15.0.7 clean. I believe that closes the open round-4 items.

Comment thread sources/algo/tests/bitwise.cpp Outdated
// (returns 0), while the widest value that fits is parsed exactly.
TEST(Bitwise, hexToDecimalWidths)
{
EXPECT_EQ(static_cast<uint8_t>(0xABu), algo::hexToDecimal<uint8_t>("AB"));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast.hpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55castU8/castU16 from cast.hpp instead of static_cast.

Comment thread sources/common/cast.hpp Outdated


#if defined(_WIN32)
////////////////////////////////////////////////////////////////////////////////

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this line ///////

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — removed the stray separator line.

Comment thread sources/device/device.cpp Outdated

uint32_t device::Device::getMinimumKernelExecuted() const
{
uint32_t const executeCount{ common::Config::instance().occupancy.kernelMinimunExecuteNeeded };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common::Config const& config{ common::Config::instance() };
return config..occupancy.kernelMinimunExecuteNeeded;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55getMinimumKernelExecuted now takes a config const-ref and returns config.occupancy.kernelMinimunExecuteNeeded directly.

// batch sizes including count < workers and count not divisible by workers.
TEST(CpuThreadPool, runCoversAllIndices)
{
for (uint32_t const workers : { 1u, 2u, 4u, 8u })

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create variable to assign { 1u, 2u, 4u, 8u }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — hoisted to constexpr std::array<uint32_t, 4> workerCounts{ 1u, 2u, 4u, 8u };.

{
for (uint32_t const workers : { 1u, 2u, 4u, 8u })
{
for (uint64_t const count : { 0ull, 1ull, 5ull, 1000ull })

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create variable to assign { 0ull, 1ull, 5ull, 1000ull }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — hoisted to constexpr std::array<uint64_t, 4> counts{ 0ull, 1ull, 5ull, 1000ull };.

{
constexpr uint64_t count{ 5000ull };
resolver::cpu::CpuThreadPool pool{ 4u, 0ull };
std::vector<std::atomic<int>> seen(static_cast<size_t>(count));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast.hpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — using castSize() from cast.hpp.

{
for (uint64_t i{ lo }; i < hi; ++i)
{
seen[static_cast<size_t>(i)].fetch_add(1);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast.hpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — using castSize() from cast.hpp.

{
constexpr uint64_t count{ 1000ull };
resolver::cpu::CpuThreadPool pool{ workers, 0ull };
std::vector<std::atomic<int>> seen(static_cast<size_t>(count));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast.hpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — using castSize() from cast.hpp.

{
for (uint64_t i{ lo }; i < hi; ++i)
{
seen[static_cast<size_t>(i)].fetch_add(1);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast.hpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — using castSize() from cast.hpp.

for (uint64_t const count : { 0ull, 1ull, 5ull, 1000ull })
{
resolver::cpu::CpuThreadPool pool{ workers, 0ull };
std::vector<std::atomic<int>> seen(static_cast<size_t>(count));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast.hpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8122a55 — using castSize() from cast.hpp.

@luminousmining

Copy link
Copy Markdown
Owner

Last remarks and it's fine !

…test lists)

- thread_pool test: hoist the worker-count / count / grain literal lists into
  constexpr std::array variables (matching the cpu_params test convention) and
  use castSize() from cast.hpp instead of static_cast<size_t>.
- bitwise test: use castU8/castU16 from cast.hpp instead of static_cast.
- cast.hpp: drop the stray separator line inside the _WIN32 castDWORDPTR block.
- device::getMinimumKernelExecuted: take a config const-ref and return the
  field directly instead of through a temporary.

Verified: CPU-only unit_test (GPU=none CPU=ON BUILD_TESTS=ON) builds and the
full suite passes (92/92).
@yuzi-co

yuzi-co commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 8122a55 — applies the last remarks (replied inline):

  • thread_pool test: hoisted the worker-count / count / grain literal lists into constexpr std::array variables, and switched the index casts to castSize() from cast.hpp.
  • bitwise test: castU8/castU16 instead of static_cast.
  • cast.hpp: dropped the stray separator line in the _WIN32 castDWORDPTR block.
  • device::getMinimumKernelExecuted: config const-ref + direct return.

Verified: CPU-only unit_test builds and the full suite passes (92/92); clang-format 15.0.7 clean. That should be everything.

@luminousmining
luminousmining merged commit 8400267 into luminousmining:main Jun 19, 2026
12 checks passed
@yuzi-co
yuzi-co deleted the feat/cpu-blake3-mining branch June 19, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants