Merge pull request #3 from Quantus-Network/feat/montgomery

fast modular arithmetic (montgomery multiplication)
This commit is contained in:
rob thijssen
2025-09-11 18:40:48 +08:00
committed by GitHub
10 changed files with 1333 additions and 51 deletions
+36 -37
View File
@@ -2,46 +2,45 @@
name: continuous integration
on:
pull_request:
push:
branches:
- main
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
RUSTUP_TOOLCHAIN: nightly-2024-12-14
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
jobs:
quality-control:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ env.RUSTUP_TOOLCHAIN }}
components: rustfmt, clippy
- name: install taplo
run: cargo install taplo-cli --locked
- name: taplo
run: taplo format --check --config taplo.toml
- name: format
run: cargo fmt --all -- --check
timeout-minutes: 5
- name: compile
run: cargo build --locked --workspace
timeout-minutes: 90
- name: clippy
run: cargo clippy --locked --workspace
timeout-minutes: 30
- name: test
run: cargo test --locked --workspace
timeout-minutes: 15
- name: doc
run: cargo doc --locked --workspace --no-deps
timeout-minutes: 15
quality-control:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ env.RUSTUP_TOOLCHAIN }}
components: rustfmt, clippy
- name: install taplo
run: cargo install taplo-cli --locked
- name: taplo
run: taplo format --check --config taplo.toml
- name: format
run: cargo fmt --all -- --check
timeout-minutes: 5
- name: compile
run: cargo build --locked --workspace
timeout-minutes: 90
- name: clippy
run: cargo clippy --locked --workspace
timeout-minutes: 30
- name: test
run: cargo test --locked --workspace
timeout-minutes: 15
- name: doc
run: cargo doc --locked --workspace --no-deps
timeout-minutes: 15
Generated
+31
View File
@@ -333,6 +333,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"rand_core",
"subtle",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -480,6 +490,20 @@ dependencies = [
"thiserror",
]
[[package]]
name = "engine-montgomery"
version = "0.1.0"
dependencies = [
"crypto-bigint",
"engine-cpu",
"log",
"metrics",
"num-traits 0.2.19",
"pow-core",
"primitive-types",
"sha3",
]
[[package]]
name = "enum_primitive"
version = "0.1.1"
@@ -1047,6 +1071,7 @@ dependencies = [
"engine-cpu",
"engine-gpu-cuda",
"engine-gpu-opencl",
"engine-montgomery",
"hex",
"log",
"metrics",
@@ -1706,6 +1731,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
+9 -8
View File
@@ -3,6 +3,7 @@ members = [
"crates/engine-cpu",
"crates/engine-gpu-cuda",
"crates/engine-gpu-opencl",
"crates/engine-montgomery",
"crates/metrics",
"crates/miner-cli",
"crates/miner-service",
@@ -17,28 +18,28 @@ authors = ["Quantus Network"]
description = "Quantus External Miner Workspace"
[workspace.dependencies]
anyhow = "1"
clap = { version = "4.5", features = ["derive", "env"] }
codec = { version = "3.6.12", default-features = false, package = "parity-scale-codec", features = ["derive", "std"] }
crossbeam-channel = "0.5"
env_logger = "0.10"
hex = { version = "0.4.3", features = ["alloc"] }
log = { version = "0.4.22", default-features = false }
num-bigint = { version = "0.4", features = ["rand"] }
num-traits = "0.2"
num_cpus = "1.16"
primitive-types = { version = "0.13.1", default-features = false }
rand = { version = "0.8.5", default-features = false }
resonance-miner-api = { git = "https://github.com/Quantus-Network/chain.git", package = "resonance-miner-api" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0.132", default-features = false }
sha2 = { version = "0.10", default-features = false }
sha3 = { version = "0.10", default-features = false }
thiserror = "1"
tokio = { version = "1.36", features = ["full"] }
warp = "0.3"
# Use local pow-core crate instead of the external qpow-math; keep API compatibility at the engine layer.
# qpow-math intentionally omitted here.
anyhow = "1"
clap = { version = "4.5", features = ["derive", "env"] }
crossbeam-channel = "0.5"
num_cpus = "1.16"
resonance-miner-api = { git = "https://github.com/Quantus-Network/chain.git", package = "resonance-miner-api" }
sha2 = { version = "0.10", default-features = false }
sha3 = { version = "0.10", default-features = false }
thiserror = "1"
# GPU backend dependencies are declared in individual engine crates (no optional deps in workspace)
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "engine-montgomery"
version = "0.1.0"
edition = "2021"
description = "Montgomery-optimized CPU mining engine for Quantus External Miner"
[lib]
name = "engine_montgomery"
path = "src/lib.rs"
[features]
default = []
metrics = ["dep:metrics"]
[dependencies]
# Reuse the engine trait and shared types exposed by engine-cpu
engine-cpu = { path = "../engine-cpu" }
# Access job context and core PoW math interfaces
pow-core = { path = "../pow-core" }
# Workspace-aligned deps
primitive-types = { workspace = true }
log = { workspace = true }
sha3 = { workspace = true }
num-traits = { workspace = true }
metrics = { path = "../metrics", optional = true }
# Fixed-width big-integer backend for 512-bit Montgomery ops
crypto-bigint = "0.5"
+210
View File
@@ -0,0 +1,210 @@
# engine-montgomery
Montgomery-optimized CPU mining engine for the Quantus External Miner.
This crate implements the same public `MinerEngine` trait as the baseline and fast CPU engines, while replacing the per-nonce modular multiplication with a fixed-width 8×64-bit Montgomery implementation. It is selectable via the CLI flag:
- `--engine cpu-montgomery`
The engine mirrors `cpu-fast` control flow and metrics emission so that apples-to-apples comparisons can be made between engines.
Highlights:
- Fixed-width 512-bit arithmetic using 8×64-bit limbs.
- Portable CIOS (Coarsely Integrated Operand Scanning) Montgomery multiply with `u128` intermediates.
- Direct SHA3 over big-endian limbs to avoid intermediate big-integer conversions.
- Per-job precompute cache (Montgomery params and `m_hat`) to reduce setup overhead.
- Runtime backend selection for microarchitecture-optimized kernels (x86_64 BMI2-only and BMI2+ADX; aarch64 UMULH).
- Metrics label for backend selection to aid dashboards and A/B analysis.
---
## Algorithm Overview
QPoW distance per nonce is computed as:
1) y update (group accumulation):
- y0 = m^(h + start_nonce) mod n (one-time per worker).
- y_{k+1} = (y_k * m) mod n (one multiply per nonce).
2) Distance = target XOR SHA3_512(y), where y is encoded as 64 big-endian bytes.
The engine focuses on optimizing step (1) with Montgomery multiplication and reducing overhead in step (2) by hashing directly over the final big-endian representation of y (no extra big-int intermediates).
---
## Montgomery Arithmetic (512-bit, 8×64 limbs)
We implement a 512-bit Montgomery field backed by 8 little-endian 64-bit limbs. For modulus `n` (odd composite, constrained by pow-core), we precompute:
- `n0_inv = -n^{-1} mod 2^64` (using Newton–Raphson).
- `R = 2^(64*8) mod n` (implicit via representation).
- `R^2 mod n` (computed once per job using the existing big-integer reference).
- `to_mont(x) = x * R mod n = mont_mul(x, R^2)`.
- `from_mont(x̂) = x̂ * 1 mod n = mont_mul(x̂, 1)`.
The core multiply is a portable CIOS Montgomery reduction:
```
- acc <- 0
- For each limb i in a:
acc += a[i] * b
m = (acc[0] * n0_inv) mod 2^64
acc += m * n
acc = acc >> 64 (shift down one limb)
- If acc >= n: acc -= n
- return acc
```
Where `acc` is a 9-limb `u128` accumulator to simplify carries.
This yields a single multiply+reduce per nonce with a branchless inner loop (except final conditional subtract).
---
## Hashing Strategy
- We keep `y` in Montgomery domain during iteration to minimize transforms.
- Before SHA3, we convert the residue `ŷ` back to the normal domain via `mont_mul(ŷ, 1)` and serialize as big-endian 64 bytes.
- We reuse a single `Sha3_512` hasher per search call and `finalize_reset()` each iteration to reduce construction overhead.
This preserves consensus behavior while avoiding unnecessary big-int allocations or conversions.
---
## Backend Selection and Optimizations
At runtime, the engine selects a Montgomery multiply backend based on CPU and environment:
- Portable (default/fallback)
- `mont_mul_portable`: pure-Rust, `u128`-based CIOS.
- Available everywhere.
- x86_64 (runtime detected)
- BMI2-only (`_mulx_u64`):
- `mont_mul_bmi2`: uses BMI2 MULX to get 128-bit products efficiently.
- Single carry chain (easier to validate; broadly available on newer CPUs).
- BMI2+ADX:
- `mont_mul_bmi2_adx`: implemented using MULX + ADCX/ADOX dual carry chains for higher ILP.
- aarch64
- UMULH/ADCS:
- `mont_mul_aarch64`: implemented using UMULH for high halves and ADCS-style accumulation via 64-bit ops to reduce dependency on `u128` where beneficial.
- Default backend on Apple Silicon/macOS and Linux ARM64.
You can override backend selection for testing:
- `MINER_MONT_BACKEND=portable|bmi2|bmi2-adx|umulh`
The engine logs the selected backend (and exports it via metrics) at job start. Unsupported overrides safely fall back with a clear warning.
---
## Metrics
The engine emits the same per-job and per-thread metrics as other engines, but adds a backend info gauge:
- `miner_engine_backend{engine="cpu-montgomery", backend="<name>"} = 1`
This makes it easy to pivot in Grafana by backend.
All other metrics (hash rates, progress chunking cadence, counters) are identical to `cpu-fast` for apples-to-apples comparisons.
---
## Correctness and Tests
We keep the portable CIOS path as the ground-truth reference for optimized kernels, and we cross-check against pow-core’s BigUint-based implementations.
Property tests included:
- Portable Montgomery vs reference incremental multiply:
- `from_mont(mul(to_mont(y), to_mont(m))) == step_mul(y)` across multiple steps.
- BMI2 vs Portable:
- For randomized sequences, `bmi2` backend must match `portable` exactly at each step.
- On non-x86_64 platforms, the test still runs but both backends fall back to `portable`.
- aarch64 UMULH vs Portable:
- For randomized sequences, `aarch64-umulh` must match `portable` exactly at each step.
- On non-aarch64 platforms, both tags fall back to `portable`.
- End-to-end parity:
- `cpu-montgomery` vs `cpu-fast` on a small inclusive range (distance and winner parity; identical hash_count accounting).
We recommend running the property tests on machines with and without BMI2/ADX and on aarch64 to cover all optimized code paths.
---
## Safety
- The crate uses `#![deny(unsafe_code)]`.
- `unsafe` is scoped only to tiny backend functions:
- x86_64: `_mulx_u64` and inline asm for ADCX/ADOX dual carry chains.
- aarch64: restricted intrinsics (e.g., UMULH) behind a small boundary.
- All other code remains safe Rust.
- The portable path is always available as a fallback for correctness/regression checks.
---
## Performance Notes
- Relative gains depend on how much time the miner spends in SHA3 vs modular multiply.
- Direct-hash-from-residue + precompute caching already yields a measurable uplift over `cpu-fast`.
- The BMI2-only path should improve throughput on supporting x86_64 hardware.
- BMI2+ADX typically produces the highest gains on the multiply itself (often 1.5–2.0×), with end-to-end uplift bounded by SHA3 share per nonce.
- aarch64 UMULH/ADCS brings similar relative gains on Apple Silicon and other ARM64 platforms.
To minimize orchestration overhead in the service:
- Increase `--progress-chunk-ms` (e.g., 3000–5000) on both engines when comparing, to reduce update traffic and context switching.
---
## Runtime and CI Tips
- Selecting the engine:
- `quantus-miner --engine cpu-montgomery`
- For A/B:
- Keep workers and chunking identical across instances.
- If testing backends explicitly:
- `MINER_MONT_BACKEND=bmi2` or `bmi2-adx` on capable x86_64 hardware
- `MINER_MONT_BACKEND=umulh` on aarch64
- Check logs and metrics for the selected backend label.
- Observability:
- Ensure metrics exporter is enabled (`--metrics-port ...`) for dashboards.
- Filter or group by `engine="cpu-montgomery"` and backend metric to compare microarchitectural paths.
---
## Roadmap
- [x] Portable 8×64 CIOS (u128)
- [x] Per-job precompute cache (`n0_inv`, `R^2 mod n`, `m_hat`)
- [x] Direct SHA3 from normalized big-endian bytes
- [x] Backend selection with log + metric
- [x] x86_64 BMI2-only MULX kernel
- [x] x86_64 BMI2+ADX (MULX + ADCX/ADOX) dual carry chain
- [x] aarch64 UMULH/ADCS kernel (macOS/Linux ARM64)
- [ ] Optional benchmark micro-harness (ns/op for mont_mul backends)
- [ ] Extend tests with more randomized vectors and edge-case sweeps
---
## Design Rationale
- Keep the interface and metrics identical to `cpu-fast` so that any performance deltas reflect algorithmic/microarchitectural improvements rather than service overhead.
- Keep a portable, well-reviewed core (CIOS) as a correctness reference.
- Add microarchitecture-optimized kernels behind runtime dispatch and an env override for safe, controlled rollouts.
- Log and export backend selection so A/B comparisons and regressions are easy to track.
---
## Contributing
- Changes to backends should include:
- Property tests vs portable.
- End-to-end parity checks vs `cpu-fast`.
- A note in this README describing the optimization and any preconditions (e.g., required CPU features).
- Keep unsafe code minimal, private, and well-commented.
- Prefer small, focused PRs for each backend/optimization to simplify review and bisecting.
+972
View File
@@ -0,0 +1,972 @@
#![deny(rust_2018_idioms)]
#![deny(unsafe_code)]
//! Montgomery-optimized CPU mining engine (scaffolding).
//!
//! Goals:
//! - Mirror the cpu-fast engine behavior and metrics (hash counts, progress cadence).
//! - Provide a drop-in engine selectable via `--engine cpu-montgomery`.
//! - Introduce a crypto-bigint based 512-bit fixed-width backend scaffold for future
//! Montgomery multiplication/reduction.
//!
//! Current state:
//! - The search loop mirrors `engine-cpu`'s `FastCpuEngine` to ensure apples-for-apples
//! metrics and correctness parity.
//! - A lightweight Montgomery scaffolding is included (conversions and parameter
//! container) using `crypto-bigint`, ready to be integrated into the hot path.
//!
//! Next steps (planned):
//! - Replace the per-step `step_mul` with Montgomery domain multiplication:
//! y_hat <- montgomery_mul(y_hat, m_hat, n, n') with y kept in Montgomery domain.
//! - Precompute `R`, `R^2`, and `n'` once per job/thread and transform inputs.
//! - Convert out of Montgomery before SHA3-512 distance computation.
//!
//! Important: We intentionally keep emissions (hash_count increments, control flow)
//! identical to `cpu-fast` so metrics are directly comparable when pitting
//! `--engine cpu-fast` vs `--engine cpu-montgomery`.
use core::cmp::Ordering;
use engine_cpu::EngineStatus;
use engine_cpu::{EngineCandidate as Candidate, EngineRange as Range, MinerEngine};
use pow_core::compat;
use pow_core::{init_worker_y0, is_valid_distance, JobContext};
use primitive_types::U512;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex};
/// Montgomery engine for CPU.
pub struct MontgomeryCpuEngine {
#[allow(clippy::type_complexity)]
cache: Mutex<HashMap<([u8; 64], [u8; 64]), Arc<mont_portable::MontCtx>>>,
}
impl Default for MontgomeryCpuEngine {
fn default() -> Self {
Self {
cache: Mutex::new(HashMap::new()),
}
}
}
impl MontgomeryCpuEngine {
pub fn new() -> Self {
Self::default()
}
}
impl MinerEngine for MontgomeryCpuEngine {
fn name(&self) -> &'static str {
"cpu-montgomery"
}
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
// Build the standard pow-core context (m, n, target, etc).
// Montgomery parameters are computed per search call for now; we can
// later thread them through a custom context wrapper if needed.
JobContext::new(header_hash, threshold)
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
// NOTE: For initial integration we mirror the cpu-fast engine logic to ensure
// metrics parity and correctness, while keeping the Montgomery scaffolding
// ready for optimization in follow-ups.
if range.start > range.end {
return EngineStatus::Exhausted { hash_count: 0 };
}
// One-time init per thread: y0 = m^(h + start_nonce) mod n
let mut current = range.start;
let y0 = init_worker_y0(ctx, current);
let mut hash_count: u64 = 0;
// Initialize or reuse per-job Montgomery params and residues from cache
let mont = {
let key = (ctx.m.to_big_endian(), ctx.n.to_big_endian());
let mut guard = self.cache.lock().unwrap();
if let Some(m) = guard.get(&key) {
m.clone()
} else {
let m = Arc::new(mont_portable::MontCtx::from_ctx(ctx));
guard.insert(key, m.clone());
m
}
};
let mut y_hat = mont.to_mont_u512(&y0);
let m_hat = mont.m_hat;
loop {
// Cancellation check (fast and frequent as in cpu-fast)
if cancel.load(AtomicOrdering::Relaxed) {
return EngineStatus::Cancelled { hash_count };
}
// Compute distance from Montgomery accumulator: normalize then hash via pow-core
let y_norm = mont.from_mont_u512(&y_hat);
let distance = pow_core::distance_from_y(ctx, y_norm);
hash_count = hash_count.saturating_add(1);
if is_valid_distance(ctx, distance) {
let work = current.to_big_endian();
return EngineStatus::Found {
candidate: Candidate {
nonce: current,
work,
distance,
},
hash_count,
};
}
// Advance or finish
match current.cmp(&range.end) {
Ordering::Less => {
// Incremental step in Montgomery domain: y_hat <- y_hat * m_hat (mod n)
y_hat = mont.mul(&y_hat, &m_hat);
current = current.saturating_add(U512::one());
}
_ => {
break EngineStatus::Exhausted { hash_count };
}
}
}
}
}
/// Montgomery scaffolding with crypto-bigint.
/// This module provides conversions and parameter containers that we can use
/// to wire up a fixed-width limb backend for 512-bit operations.
mod mont_portable {
use super::*;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
use std::arch::asm;
// Montgomery context with portable CIOS 8x64 implementation (u128 intermediates).
// Limbs are stored little-endian (limb 0 is least significant).
type MulFn = fn(&[u64; 8], &[u64; 8], &[u64; 8], u64) -> [u64; 8];
#[derive(Clone)]
pub struct MontCtx {
n: [u64; 8],
n0_inv: u64, // -n^{-1} mod 2^64
r2: [u64; 8], // R^2 mod n
pub m_hat: [u64; 8],
mul_fn: MulFn,
}
impl MontCtx {
pub fn from_ctx(ctx: &JobContext) -> Self {
let n = u512_to_le(ctx.n);
let n0_inv = mont_n0_inv(n[0]);
let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n);
let r2 = u512_to_le(r2_u512);
let m = u512_to_le(ctx.m);
let (mul_fn, backend) = select_backend();
log::info!(target: "miner", "cpu-montgomery backend selected: {backend}");
#[cfg(feature = "metrics")]
{
metrics::set_engine_backend("cpu-montgomery", backend);
}
let m_hat = mul_fn(&m, &r2, &n, n0_inv);
MontCtx {
n,
n0_inv,
r2,
m_hat,
mul_fn,
}
}
pub fn to_mont_u512(&self, x: &U512) -> [u64; 8] {
let xl = u512_to_le(*x);
(self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv)
}
#[allow(clippy::wrong_self_convention)]
pub fn from_mont_u512(&self, x_hat: &[u64; 8]) -> U512 {
let one = {
let mut o = [0u64; 8];
o[0] = 1;
o
};
let norm_le = (self.mul_fn)(x_hat, &one, &self.n, self.n0_inv);
let norm_be = le_to_be_bytes(&norm_le);
U512::from_big_endian(&norm_be)
}
pub fn mul(&self, a_hat: &[u64; 8], b_hat: &[u64; 8]) -> [u64; 8] {
(self.mul_fn)(a_hat, b_hat, &self.n, self.n0_inv)
}
// Test-only helpers to enable forcing a specific backend and to access limb-level conversions.
// These are useful for property tests and backend A/B validations.
#[cfg(test)]
pub fn from_ctx_with_backend_tag(ctx: &JobContext, tag: &str) -> Self {
let n = u512_to_le(ctx.n);
let n0_inv = mont_n0_inv(n[0]);
let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n);
let r2 = u512_to_le(r2_u512);
let m = u512_to_le(ctx.m);
// Choose mul_fn by tag; fall back to portable when not applicable or CPU features missing.
let (mul_fn, _backend): (MulFn, &'static str) = match tag {
"x86_64-bmi2-adx" | "bmi2-adx" => {
#[cfg(target_arch = "x86_64")]
{
if std::is_x86_feature_detected!("bmi2")
&& std::is_x86_feature_detected!("adx")
{
(mont_mul_bmi2_adx, "x86_64-bmi2-adx")
} else if std::is_x86_feature_detected!("bmi2") {
(mont_mul_bmi2, "x86_64-bmi2")
} else {
(mont_mul_portable, "portable")
}
}
#[cfg(not(target_arch = "x86_64"))]
{
(mont_mul_portable, "portable")
}
}
"x86_64-bmi2" | "bmi2" => {
#[cfg(target_arch = "x86_64")]
{
if std::is_x86_feature_detected!("bmi2") {
(mont_mul_bmi2, "x86_64-bmi2")
} else {
(mont_mul_portable, "portable")
}
}
#[cfg(not(target_arch = "x86_64"))]
{
(mont_mul_portable, "portable")
}
}
"aarch64-umulh" | "umulh" => {
#[cfg(target_arch = "aarch64")]
{
(mont_mul_aarch64, "aarch64-umulh")
}
#[cfg(not(target_arch = "aarch64"))]
{
(mont_mul_portable, "portable")
}
}
_ => (mont_mul_portable, "portable"),
};
let m_hat = mul_fn(&m, &r2, &n, n0_inv);
MontCtx {
n,
n0_inv,
r2,
m_hat,
mul_fn,
}
}
#[cfg(test)]
pub fn to_mont_le_limbs(&self, x: &U512) -> [u64; 8] {
(self.mul_fn)(&u512_to_le(*x), &self.r2, &self.n, self.n0_inv)
}
#[cfg(test)]
#[allow(clippy::wrong_self_convention)]
pub fn from_mont_le_limbs(&self, x_hat: &[u64; 8]) -> [u64; 8] {
let one = {
let mut o = [0u64; 8];
o[0] = 1;
o
};
(self.mul_fn)(x_hat, &one, &self.n, self.n0_inv)
}
}
#[inline]
fn u512_to_le(x: U512) -> [u64; 8] {
let be = x.to_big_endian();
let mut limbs = [0u64; 8];
// Split BE into 8 chunks, then reverse to get LE limb order (least-significant first).
for i in 0..8 {
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&be[i * 8..(i + 1) * 8]);
limbs[i] = u64::from_be_bytes(bytes);
}
limbs.reverse();
limbs
}
#[inline]
fn le_to_be_bytes(limbs: &[u64; 8]) -> [u8; 64] {
let mut out = [0u8; 64];
for i in 0..8 {
let chunk = limbs[7 - i].to_be_bytes();
out[i * 8..(i + 1) * 8].copy_from_slice(&chunk);
}
out
}
// Compute n0_inv = -n[0]^{-1} mod 2^64 using Newton–Raphson (n[0] must be odd).
#[inline]
fn mont_n0_inv(n0: u64) -> u64 {
// Compute inverse of n0 modulo 2^64
let mut x = 1u64;
// 6 iterations suffice for 64-bit modulus
for _ in 0..6 {
let t = x.wrapping_mul(n0);
x = x.wrapping_mul(2u64.wrapping_sub(t));
}
x.wrapping_neg()
}
// Portable CIOS Montgomery multiplication: returns (a * b * R^{-1}) mod n
#[inline]
fn mont_mul_portable(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128;
let mut acc = [0u128; 9];
for &ai_u64 in a.iter().take(8) {
// acc += ai * b
let ai = ai_u64 as u128;
let mut carry = 0u128;
for j in 0..8 {
let sum = acc[j] + ai * (b[j] as u128) + carry;
acc[j] = sum & MASK;
carry = sum >> 64;
}
acc[8] += carry;
// m = (acc[0] * n0_inv) mod 2^64
let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u128;
// acc += m * n
let mut carry2 = 0u128;
for j in 0..8 {
let sum = acc[j] + m * (n[j] as u128) + carry2;
acc[j] = sum & MASK;
carry2 = sum >> 64;
}
acc[8] += carry2;
// shift acc right by one limb
for j in 0..8 {
acc[j] = acc[j + 1];
}
acc[8] = 0;
}
// Convert acc (little-endian limbs) to u64 array
let mut res = [0u64; 8];
for j in 0..8 {
res[j] = acc[j] as u64;
}
// Conditional subtract modulus if res >= n
if ge_le(&res, n) {
sub_le_in_place(&mut res, n);
}
res
}
#[inline]
fn ge_le(a: &[u64; 8], b: &[u64; 8]) -> bool {
for i in (0..8).rev() {
if a[i] != b[i] {
return a[i] > b[i];
}
}
true
}
#[inline]
fn sub_le_in_place(a: &mut [u64; 8], b: &[u64; 8]) {
let mut borrow: u128 = 0;
for i in 0..8 {
let ai = a[i] as u128;
let bi = b[i] as u128;
let tmp = (1u128 << 64) + ai - bi - borrow;
a[i] = (tmp & 0xFFFF_FFFF_FFFF_FFFFu128) as u64;
borrow = if tmp >> 64 == 0 { 1 } else { 0 };
}
}
#[inline]
fn select_backend() -> (MulFn, &'static str) {
// Optional override via env:
// MINER_MONT_BACKEND=portable|bmi2|bmi2-adx
if let Ok(val) = std::env::var("MINER_MONT_BACKEND") {
let forced = val.to_ascii_lowercase();
#[cfg(target_arch = "x86_64")]
{
let bmi2 = std::is_x86_feature_detected!("bmi2");
let adx = std::is_x86_feature_detected!("adx");
match forced.as_str() {
"portable" => {
log::warn!(target: "miner", "cpu-montgomery backend override: forced portable");
return (mont_mul_portable, "forced-portable");
}
"bmi2-adx" | "adx" => {
if bmi2 && adx {
log::warn!(target: "miner", "cpu-montgomery backend override: forced x86_64-bmi2-adx");
return (mont_mul_bmi2_adx, "forced-x86_64-bmi2-adx");
} else if bmi2 {
log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2-adx but ADX unavailable; falling back to x86_64-bmi2");
return (mont_mul_bmi2, "x86_64-bmi2");
} else {
log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2-adx but BMI2/ADX unavailable; falling back to x86_64-generic");
return (mont_mul_portable, "x86_64-generic");
}
}
"bmi2" => {
if bmi2 {
log::warn!(target: "miner", "cpu-montgomery backend override: forced x86_64-bmi2");
return (mont_mul_bmi2, "forced-x86_64-bmi2");
} else {
log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2 but BMI2 unavailable; falling back to x86_64-generic");
return (mont_mul_portable, "x86_64-generic");
}
}
other => {
log::warn!(target: "miner", "cpu-montgomery backend override '{other}' is not recognized on x86_64; using auto-detect");
}
}
}
#[cfg(target_arch = "aarch64")]
{
match forced.as_str() {
"portable" => {
log::warn!(target: "miner", "cpu-montgomery backend override: forced portable");
return (mont_mul_portable, "forced-portable");
}
// x86-only hints on aarch64 -> warn and ignore
"bmi2" | "bmi2-adx" | "adx" => {
log::warn!(target: "miner", "cpu-montgomery backend override '{}' not supported on aarch64; using auto-detect", forced);
}
other => {
log::warn!(target: "miner", "cpu-montgomery backend override '{}' is not recognized on aarch64; using auto-detect", other);
}
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
match forced.as_str() {
"portable" => {
log::warn!(target: "miner", "cpu-montgomery backend override: forced portable");
return (mont_mul_portable, "forced-portable");
}
other => {
log::warn!(target: "miner", "cpu-montgomery backend override '{}' not supported on this arch; using auto-detect", other);
}
}
}
}
// Auto-detection (default) per architecture
#[cfg(target_arch = "x86_64")]
{
let bmi2 = std::is_x86_feature_detected!("bmi2");
if bmi2 {
(mont_mul_bmi2, "x86_64-bmi2")
} else {
(mont_mul_portable, "x86_64-generic")
}
}
#[cfg(target_arch = "aarch64")]
{
(mont_mul_aarch64, "aarch64-umulh")
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
(mont_mul_portable, "portable")
}
}
#[cfg(target_arch = "x86_64")]
#[inline]
#[allow(unsafe_code)]
fn mont_mul_bmi2(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
// BMI2-optimized CIOS using MULX. Single carry chain with u128 accumulation.
// Limbs: little-endian (limb 0 = least significant).
use core::arch::x86_64::_mulx_u64;
const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128;
// 9-limb accumulator in u128 to simplify carries
let mut acc = [0u128; 9];
for &ai in a.iter().take(8) {
// acc += ai * b
let mut carry: u128 = 0;
for j in 0..8 {
let mut hi: u64 = 0;
// lo = (ai * b[j])_lo, hi = (ai * b[j])_hi
let lo = unsafe { _mulx_u64(ai, b[j], &mut hi) };
let sum = acc[j] + (lo as u128) + carry;
acc[j] = sum & MASK;
carry = (sum >> 64) + (hi as u128);
}
acc[8] += carry;
// m = (acc[0] * n0_inv) mod 2^64
let m = (acc[0] as u64).wrapping_mul(n0_inv);
// acc += m * n
let mut carry2: u128 = 0;
for j in 0..8 {
let mut hi2: u64 = 0;
let lo2 = unsafe { _mulx_u64(m, n[j], &mut hi2) };
let sum2 = acc[j] + (lo2 as u128) + carry2;
acc[j] = sum2 & MASK;
carry2 = (sum2 >> 64) + (hi2 as u128);
}
acc[8] += carry2;
// shift acc right by one limb (drop acc[0])
for j in 0..8 {
acc[j] = acc[j + 1];
}
acc[8] = 0;
}
// Convert acc to u64 limbs (little-endian)
let mut res = [0u64; 8];
for j in 0..8 {
res[j] = acc[j] as u64;
}
// Conditional subtraction: if res >= n then res -= n
if ge_le(&res, n) {
sub_le_in_place(&mut res, n);
}
res
}
#[cfg(target_arch = "x86_64")]
#[inline]
#[allow(unsafe_code)]
#[allow(unused_variables, unused_mut)]
fn mont_mul_bmi2_adx(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
// BMI2+ADX-optimized CIOS using MULX + dual carry chains (ADCX/ADOX).
// We retain the same CIOS structure as the portable path:
// - acc is a 9-limb accumulator in 64-bit limbs (little-endian)
// - For each i:
// acc += a[i] * b
// m = (acc[0] * n0_inv) mod 2^64
// acc += m * n
// acc >>= 64 (drop acc[0])
//
// The inner adds use two independent carry chains:
// - ADCX chain accumulates low halves into acc[j] (carry via CF)
// - ADOX chain accumulates high halves into a separate running carry (OF)
//
// Note: We keep the logic in Rust around the asm! blocks for readability; the hot add paths
// are emitted via inline assembly for ADX utilization.
use core::arch::x86_64::_mulx_u64;
use std::arch::asm;
let mut acc: [u64; 9] = [0; 9];
// helper: add (lo, hi) into acc[j] with dual carry chains (CF/OF)
#[inline(always)]
unsafe fn adx_accumulate(acc_j: &mut u64, lo: u64, hi: u64, of_carry: &mut u64) {
// Clear CF and OF, then do:
// acc_j += lo using ADCX (CF chain)
// of_carry += hi using ADOX (OF chain)
asm!(
// Clear CF and OF carry chains
"xor r8d, r8d",
"adcx r8, r8",
"adox r8, r8",
// acc_j = acc_j + lo + CF
"adcx {acc_j}, {lo}",
// of_carry = of_carry + hi + OF
"adox {ofc}, {hi}",
acc_j = inout(reg) *acc_j,
ofc = inout(reg) *of_carry,
lo = in(reg) lo,
hi = in(reg) hi,
out("r8") _,
options(nomem, nostack)
);
}
// helper: fold OF carry into next limb (acc[k] += of_carry)
#[inline(always)]
unsafe fn adx_fold_of(acc_k: &mut u64, of_carry: &mut u64) {
// Fold the overflow-chain carry into acc_k using ADCX with cleared CF.
asm!(
"xor r8d, r8d",
"adcx r8, r8",
"adcx {acc_k}, {ofc}",
acc_k = inout(reg) *acc_k,
ofc = inout(reg) *of_carry,
out("r8") _,
options(nomem, nostack)
);
}
for &ai in a.iter().take(8) {
// acc += ai * b
let mut of_carry: u64 = 0;
// iterate j=0..7: accumulate ai*b[j] into acc[j].. with dual chains
for j in 0..8 {
let mut hi: u64 = 0;
let lo: u64 = unsafe { _mulx_u64(ai, b[j], &mut hi) };
// acc[j] += lo (CF chain), of_carry += hi (OF chain)
unsafe { adx_accumulate(&mut acc[j], lo, hi, &mut of_carry) };
}
// Propagate remaining OF carry into acc[8]
unsafe { adx_fold_of(&mut acc[8], &mut of_carry) };
// m = (acc[0] * n0_inv) mod 2^64
let m: u64 = (acc[0]).wrapping_mul(n0_inv);
// acc += m * n
of_carry = 0;
for j in 0..8 {
let mut hi2: u64 = 0;
let lo2: u64 = unsafe { _mulx_u64(m, n[j], &mut hi2) };
// acc[j] += lo2 (CF chain), of_carry += hi2 (OF chain)
unsafe { adx_accumulate(&mut acc[j], lo2, hi2, &mut of_carry) };
}
// Propagate remaining OF carry into acc[8]
unsafe { adx_fold_of(&mut acc[8], &mut of_carry) };
// shift acc right by one limb (drop acc[0])
for j in 0..8 {
acc[j] = acc[j + 1];
}
acc[8] = 0;
}
// Conditional subtraction: if acc >= n then acc -= n
let mut res = [0u64; 8];
res.copy_from_slice(&acc[0..8]);
if ge_le(&res, n) {
sub_le_in_place(&mut res, n);
}
res
}
#[cfg(target_arch = "aarch64")]
#[inline]
#[allow(unsafe_code)]
fn mont_mul_aarch64(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
use core::arch::aarch64::umulh;
const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128;
let mut acc = [0u128; 9];
for &ai in a.iter().take(8) {
// acc += ai * b
let mut carry: u128 = 0;
for j in 0..8 {
// low 64-bit product
let lo = ai.wrapping_mul(b[j]);
// high 64-bit product via UMULH intrinsic
let hi = unsafe { umulh(ai, b[j]) } as u128;
let sum = acc[j] + (lo as u128) + carry;
acc[j] = sum & MASK;
carry = (sum >> 64) + hi;
}
acc[8] = acc[8] + carry;
// m = (acc[0] * n0_inv) mod 2^64
let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u64;
// acc += m * n
let mut carry2: u128 = 0;
for j in 0..8 {
let lo2 = m.wrapping_mul(n[j]);
let hi2 = unsafe { umulh(m, n[j]) } as u128;
let sum2 = acc[j] + (lo2 as u128) + carry2;
acc[j] = sum2 & MASK;
carry2 = (sum2 >> 64) + hi2;
}
acc[8] = acc[8] + carry2;
// shift acc right by one limb
for j in 0..8 {
acc[j] = acc[j + 1];
}
acc[8] = 0;
}
// Convert acc to result limbs
let mut res = [0u64; 8];
for j in 0..8 {
res[j] = acc[j] as u64;
}
// Conditional subtraction: if res >= n then res -= n
if ge_le(&res, n) {
sub_le_in_place(&mut res, n);
}
res
}
#[cfg(test)]
mod prop_tests {
use super::*;
use crate::MontgomeryCpuEngine;
use engine_cpu::{EngineStatus, FastCpuEngine, MinerEngine};
use pow_core::{init_worker_y0, step_mul, JobContext};
use primitive_types::U512;
use std::sync::atomic::AtomicBool;
fn u64x8_le_to_u512(le: &[u64; 8]) -> U512 {
let be = le_to_be_bytes(le);
U512::from_big_endian(&be)
}
fn make_ctx_with_header_byte(byte: u8) -> JobContext {
let mut header = [0u8; 32];
header.fill(byte);
let threshold = U512::MAX;
JobContext::new(header, threshold)
}
#[test]
fn montgomery_portable_mul_matches_step_mul() {
// Validate that mont_mul agrees with pow_core's step_mul across many steps.
let ctx = make_ctx_with_header_byte(0x5Au8);
let mont = MontCtx::from_ctx_with_backend_tag(&ctx, "portable");
let start = U512::from(12345u64);
let mut y_ref = init_worker_y0(&ctx, start);
let mut y_hat = mont.to_mont_le_limbs(&y_ref);
let m_hat = mont.m_hat;
// Walk 128 steps, comparing at each step
for _ in 0..128 {
// Reference path: y <- y * m (mod n) via pow_core BigUint
y_ref = step_mul(&ctx, y_ref);
// Montgomery path: y_hat <- y_hat * m_hat (mod n), convert out
y_hat = mont.mul(&y_hat, &m_hat);
let y_hat_norm_le = mont.from_mont_le_limbs(&y_hat);
let y_mont = u64x8_le_to_u512(&y_hat_norm_le);
assert_eq!(y_ref, y_mont, "montgomery mul mismatch vs step_mul");
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn montgomery_bmi2_equivalence_to_portable_when_available() {
// On x86_64, ensure bmi2 path produces identical results as portable for the same (a,b).
// On non-x86_64 this test still runs but both tags fall back to portable.
let ctx = make_ctx_with_header_byte(0x3Cu8);
let mont_port = MontCtx::from_ctx_with_backend_tag(&ctx, "portable");
let mont_bmi2 = MontCtx::from_ctx_with_backend_tag(&ctx, "bmi2");
let start = U512::from(999u64);
let mut y_ref = init_worker_y0(&ctx, start);
let mut y_hat_port = mont_port.to_mont_le_limbs(&y_ref);
let mut y_hat_bmi2 = mont_bmi2.to_mont_le_limbs(&y_ref);
let m_hat_port = mont_port.m_hat;
let m_hat_bmi2 = mont_bmi2.m_hat;
for _ in 0..64 {
// advance reference so values change per-iteration
y_ref = step_mul(&ctx, y_ref);
y_hat_port = mont_port.mul(&y_hat_port, &m_hat_port);
y_hat_bmi2 = mont_bmi2.mul(&y_hat_bmi2, &m_hat_bmi2);
let y_port = u64x8_le_to_u512(&mont_port.from_mont_le_limbs(&y_hat_port));
let y_bmi2 = u64x8_le_to_u512(&mont_bmi2.from_mont_le_limbs(&y_hat_bmi2));
assert_eq!(y_port, y_bmi2, "bmi2 path mismatch with portable");
}
}
#[test]
fn engine_end_to_end_matches_cpu_fast_on_small_range() {
// End-to-end parity check against cpu-fast over a small inclusive range.
let header = [0x11u8; 32];
let threshold = U512::MAX;
let ctx = JobContext::new(header, threshold);
let range = crate::Range {
start: U512::from(0u64),
end: U512::from(500u64),
};
let cancel = AtomicBool::new(false);
let mont = MontgomeryCpuEngine::new();
let fast = FastCpuEngine::new();
let s_m = mont.search_range(&ctx, range.clone(), &cancel);
let s_f = fast.search_range(&ctx, range.clone(), &cancel);
match (s_m, s_f) {
(
EngineStatus::Found {
candidate: cm,
hash_count: hm,
},
EngineStatus::Found {
candidate: cf,
hash_count: hf,
},
) => {
assert_eq!(cm.nonce, cf.nonce, "nonce mismatch");
assert_eq!(cm.distance, cf.distance, "distance mismatch");
assert_eq!(hm, hf, "hash_count mismatch");
}
(
EngineStatus::Exhausted { hash_count: hm },
EngineStatus::Exhausted { hash_count: hf },
) => {
assert_eq!(hm, hf, "hash_count mismatch on Exhausted");
}
(m, f) => panic!("expected matching status, got mont={m:?}, fast={f:?}"),
}
}
#[cfg(target_arch = "aarch64")]
#[test]
fn montgomery_aarch64_equivalence_to_portable_when_available() {
// On aarch64, ensure UMULH/ADCS path matches portable. On other arches this test
// still runs but both tags fall back to portable.
let ctx = make_ctx_with_header_byte(0x77u8);
let mont_port = MontCtx::from_ctx_with_backend_tag(&ctx, "portable");
let mont_arm = MontCtx::from_ctx_with_backend_tag(&ctx, "aarch64-umulh");
let start = U512::from(4242u64);
let mut y_ref = init_worker_y0(&ctx, start);
let mut y_hat_port = mont_port.to_mont_le_limbs(&y_ref);
let mut y_hat_arm = mont_arm.to_mont_le_limbs(&y_ref);
let m_hat_port = mont_port.m_hat;
let m_hat_arm = mont_arm.m_hat;
for _ in 0..64 {
// advance reference so values change per-iteration
y_ref = step_mul(&ctx, y_ref);
y_hat_port = mont_port.mul(&y_hat_port, &m_hat_port);
y_hat_arm = mont_arm.mul(&y_hat_arm, &m_hat_arm);
let y_port = u64x8_le_to_u512(&mont_port.from_mont_le_limbs(&y_hat_port));
let y_arm = u64x8_le_to_u512(&mont_arm.from_mont_le_limbs(&y_hat_arm));
assert_eq!(y_port, y_arm, "aarch64 umulh path mismatch with portable");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicBool;
fn make_ctx() -> JobContext {
let header = [1u8; 32];
let threshold = U512::MAX; // permissive threshold for "found" parity test
JobContext::new(header, threshold)
}
#[test]
fn montgomery_engine_matches_fast_engine_on_small_range() {
let ctx = make_ctx();
let range = Range {
start: U512::from(0u64),
end: U512::from(100u64),
};
let cancel = AtomicBool::new(false);
let mont = MontgomeryCpuEngine::new();
let fast = engine_cpu::FastCpuEngine::new();
let m_status = mont.search_range(&ctx, range.clone(), &cancel);
let f_status = fast.search_range(&ctx, range.clone(), &cancel);
match (m_status, f_status) {
(
EngineStatus::Found {
candidate: m_cand,
hash_count: m_hashes,
},
EngineStatus::Found {
candidate: f_cand,
hash_count: f_hashes,
},
) => {
assert_eq!(
m_cand.nonce, f_cand.nonce,
"engines disagreed on winning nonce"
);
assert_eq!(
m_cand.distance, f_cand.distance,
"engines disagreed on distance"
);
assert_eq!(m_hashes, f_hashes, "engines disagreed on hash_count");
}
(m, f) => panic!("expected Found/Found, got montgomery={m:?}, fast={f:?}"),
}
}
#[test]
fn engine_returns_exhausted_when_no_solution_in_range() {
// Very strict threshold to avoid solutions in a tiny range.
let header = [2u8; 32];
let threshold = U512::zero();
let ctx = JobContext::new(header, threshold);
let range = Range {
start: U512::from(1u64),
end: U512::from(1000u64),
};
let cancel = AtomicBool::new(false);
let eng = MontgomeryCpuEngine::new();
let status = eng.search_range(&ctx, range.clone(), &cancel);
match status {
EngineStatus::Exhausted { hash_count } => {
// Inclusive range length = end - start + 1
let expected = (range.end - range.start + U512::one()).as_u64();
assert_eq!(hash_count, expected, "hash_count should equal range length");
}
other => panic!("expected Exhausted, got {other:?}"),
}
}
#[test]
fn engine_respects_immediate_cancellation() {
let ctx = make_ctx();
let range = Range {
start: U512::from(0u64),
end: U512::from(1_000_000u64),
};
let cancel = AtomicBool::new(true); // cancelled before starting
let eng = MontgomeryCpuEngine::new();
let status = eng.search_range(&ctx, range, &cancel);
match status {
EngineStatus::Cancelled { hash_count } => {
assert_eq!(hash_count, 0, "expected no work when cancelled immediately");
}
other => panic!("expected Cancelled, got {other:?}"),
}
}
}
+21
View File
@@ -62,6 +62,21 @@ static ACTIVE_JOBS: Lazy<IntGauge> = Lazy::new(|| {
g
});
static ENGINE_BACKEND_INFO: Lazy<GaugeVec> = Lazy::new(|| {
let g = GaugeVec::new(
opts!(
"miner_engine_backend",
"Engine backend info (label-only gauge set to 1). Labels: engine, backend"
),
&["engine", "backend"],
)
.expect("create miner_engine_backend");
REGISTRY
.register(Box::new(g.clone()))
.expect("register miner_engine_backend");
g
});
static EFFECTIVE_CPUS: Lazy<IntGauge> = Lazy::new(|| {
let g = IntGauge::new(
"miner_effective_cpus",
@@ -246,6 +261,12 @@ pub fn set_active_jobs(n: i64) {
ACTIVE_JOBS.set(n);
}
pub fn set_engine_backend(engine: &str, backend: &str) {
ENGINE_BACKEND_INFO
.with_label_values(&[engine, backend])
.set(1.0);
}
pub fn inc_mine_requests(result: &str) {
MINE_REQUESTS_TOTAL.with_label_values(&[result]).inc();
}
+5 -2
View File
@@ -40,7 +40,7 @@ struct Args {
manip_throttle_cap: Option<u64>,
/// Mining engine to use (default: cpu-fast).
/// Options: cpu-baseline, cpu-fast, cpu-chain-manipulator, gpu-cuda, gpu-opencl
/// Options: cpu-baseline, cpu-fast, cpu-chain-manipulator, cpu-montgomery, gpu-cuda, gpu-opencl
/// Note: GPU engines are currently unimplemented and will return a clear error at runtime.
#[arg(long, env = "MINER_ENGINE", value_enum, default_value_t = EngineCli::CpuFast)]
engine: EngineCli,
@@ -53,6 +53,8 @@ enum EngineCli {
CpuBaseline,
/// Optimized CPU engine (incremental precompute + step_mul)
CpuFast,
/// Montgomery-optimized CPU engine (fixed-width 512-bit ops)
CpuMontgomery,
/// Throttling CPU engine that slows per block to help reduce difficulty
CpuChainManipulator,
/// CUDA GPU engine (unimplemented; selecting will return an error)
@@ -66,6 +68,7 @@ impl From<EngineCli> for EngineSelection {
match value {
EngineCli::CpuBaseline => EngineSelection::CpuBaseline,
EngineCli::CpuFast => EngineSelection::CpuFast,
EngineCli::CpuMontgomery => EngineSelection::CpuMontgomery,
EngineCli::CpuChainManipulator => EngineSelection::CpuChainManipulator,
EngineCli::GpuCuda => EngineSelection::GpuCuda,
EngineCli::GpuOpencl => EngineSelection::GpuOpenCl,
@@ -98,7 +101,7 @@ async fn main() {
manip_throttle_cap: args.manip_throttle_cap,
engine: args.engine.into(),
};
log::info!("Effective config: {}", config);
log::info!("Effective config: {config}");
if let Err(e) = run(config).await {
log::error!("Miner service terminated with error: {e:?}");
+5 -2
View File
@@ -6,14 +6,16 @@ publish = false
description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner"
[features]
default = ["cpu", "metrics"]
default = ["cpu", "metrics", "montgomery"]
# Enable CPU engine by default.
cpu = ["engine-cpu"]
# Optional metrics/observability (Prometheus endpoint).
metrics = ["dep:metrics", "metrics/http-exporter"]
metrics = ["dep:metrics", "engine-montgomery?/metrics", "metrics/http-exporter"]
# Optional GPU backends (off by default).
cuda = ["engine-gpu-cuda"]
opencl = ["engine-gpu-opencl"]
# Optional Montgomery-optimized CPU backend
montgomery = ["engine-montgomery"]
[dependencies]
# Workspace-shared deps
@@ -35,6 +37,7 @@ resonance-miner-api = { workspace = true }
# Local crates
pow-core = { path = "../pow-core" }
engine-cpu = { path = "../engine-cpu", optional = true }
engine-montgomery = { path = "../engine-montgomery", optional = true }
engine-gpu-cuda = { path = "../engine-gpu-cuda", optional = true }
engine-gpu-opencl = { path = "../engine-gpu-opencl", optional = true }
metrics = { path = "../metrics", optional = true }
+15 -2
View File
@@ -44,6 +44,7 @@ pub struct ServiceConfig {
pub enum EngineSelection {
CpuBaseline,
CpuFast,
CpuMontgomery,
CpuChainManipulator,
GpuCuda,
GpuOpenCl,
@@ -70,6 +71,7 @@ impl fmt::Display for ServiceConfig {
let engine = match self.engine {
EngineSelection::CpuBaseline => "cpu-baseline",
EngineSelection::CpuFast => "cpu-fast",
EngineSelection::CpuMontgomery => "cpu-montgomery",
EngineSelection::CpuChainManipulator => "cpu-chain-manipulator",
EngineSelection::GpuCuda => "gpu-cuda",
EngineSelection::GpuOpenCl => "gpu-opencl",
@@ -988,7 +990,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
// Detect effective CPU pool for this process (cpuset if available).
let (effective_cpus, cpuset_mask) = detect_effective_cpus_and_mask();
if let Some(mask) = cpuset_mask.as_ref() {
log::debug!(target: "miner", "Detected cpuset mask: {}", mask);
log::debug!(target: "miner", "Detected cpuset mask: {mask}");
} else {
log::debug!(target: "miner", "No cpuset mask detected; using full logical CPU count");
}
@@ -1046,6 +1048,17 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
let mut engine: Arc<dyn MinerEngine> = match config.engine {
EngineSelection::CpuBaseline => Arc::new(engine_cpu::BaselineCpuEngine::new()),
EngineSelection::CpuFast => Arc::new(engine_cpu::FastCpuEngine::new()),
EngineSelection::CpuMontgomery => {
#[cfg(feature = "montgomery")]
{
Arc::new(engine_montgomery::MontgomeryCpuEngine::new())
}
#[cfg(not(feature = "montgomery"))]
{
// Fallback if montgomery backend is not compiled in
Arc::new(engine_cpu::FastCpuEngine::new())
}
}
EngineSelection::CpuChainManipulator => {
let mut eng = engine_cpu::ChainEngine::new();
// Apply optional throttle parameters if provided.
@@ -1080,7 +1093,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
}
};
log::info!("Using engine: {}", engine.name());
log::info!("Service configuration: {}", config);
log::info!("Service configuration: {config}");
let progress_chunk_ms = config.progress_chunk_ms.unwrap_or(2000);
let service = MiningService::new(workers, engine.clone(), progress_chunk_ms);