Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8359892406 | ||
|
|
d99be29a64 | ||
|
|
7296c003bb | ||
|
|
d37f1dc0e5 | ||
|
|
4d53fff54b | ||
|
|
18e8539fa3 | ||
|
|
4ab6914331 | ||
|
|
16fb76c5e8 | ||
|
|
157aa6fb5c | ||
|
|
5ce62b3e12 | ||
|
|
8de756f456 | ||
|
|
d7bf27854f | ||
|
|
f563d24241 | ||
|
|
334e852edd | ||
|
|
67cce3f3c9 | ||
|
|
9ffb5b12e2 |
@@ -7,6 +7,11 @@ on:
|
||||
description: "Optional: Specify a full version (e.g., v0.3.0 or v0.3.1-beta.1) to build. If empty, uses the latest release tag. MUST start with 'v'."
|
||||
required: false
|
||||
default: ""
|
||||
use_main_dockerfile:
|
||||
description: "Optional: Use the main Dockerfile instead of the tag version-specific Dockerfile. If empty, uses the tag version-specific Dockerfile."
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -96,19 +101,8 @@ jobs:
|
||||
with:
|
||||
ref: ${{ env.TARGET_VERSION_WITH_V }}
|
||||
|
||||
- name: Check if Dockerfile exists in tag
|
||||
id: check_dockerfile
|
||||
run: |
|
||||
if [ ! -f Dockerfile ]; then
|
||||
echo "::warning::Dockerfile not found in tag ${{ env.TARGET_VERSION_WITH_V }}, falling back to main branch Dockerfile"
|
||||
echo "use_main=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Dockerfile found in tag"
|
||||
echo "use_main=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout main branch for Dockerfile (if needed)
|
||||
if: steps.check_dockerfile.outputs.use_main == 'true'
|
||||
if: github.event.inputs.use_main_dockerfile == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
@@ -119,13 +113,13 @@ jobs:
|
||||
path: dockerfile-source
|
||||
|
||||
- name: Copy Dockerfile from main (if needed)
|
||||
if: steps.check_dockerfile.outputs.use_main == 'true'
|
||||
if: github.event.inputs.use_main_dockerfile == 'true'
|
||||
run: |
|
||||
cp dockerfile-source/Dockerfile ./
|
||||
[ -f dockerfile-source/.dockerignore ] && cp dockerfile-source/.dockerignore ./ || true
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
Generated
+41
-27
@@ -571,18 +571,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "engine-cpu"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"hex",
|
||||
"pow-core",
|
||||
"primitive-types 0.13.1",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "engine-gpu"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"criterion",
|
||||
@@ -591,16 +591,16 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"log",
|
||||
"p3-field",
|
||||
"p3-goldilocks",
|
||||
"pow-core",
|
||||
"primitive-types 0.13.1",
|
||||
"qp-plonky2",
|
||||
"qp-plonky2-field",
|
||||
"qp-poseidon-constants",
|
||||
"qp-poseidon-core",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"regex",
|
||||
"tokio",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
@@ -831,6 +831,18 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
@@ -839,7 +851,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
@@ -1458,7 +1470,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "metrics"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -1486,7 +1498,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-cli"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"engine-cpu",
|
||||
@@ -1497,13 +1509,13 @@ dependencies = [
|
||||
"miner-service",
|
||||
"num_cpus",
|
||||
"primitive-types 0.13.1",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miner-service"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam-channel",
|
||||
@@ -1524,7 +1536,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-telemetry"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -2008,7 +2020,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pow-core"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"primitive-types 0.13.1",
|
||||
@@ -2215,22 +2227,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "qp-poseidon-core"
|
||||
version = "1.4.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51774769a44a1c65f9ad3959ff1497a9c315a16f5718151873721b3c5631413"
|
||||
dependencies = [
|
||||
"p3-field",
|
||||
"p3-goldilocks",
|
||||
"p3-poseidon2",
|
||||
"p3-symmetric",
|
||||
"qp-poseidon-constants",
|
||||
"rand_chacha 0.9.0",
|
||||
]
|
||||
checksum = "78172860cd960773c72e97fba07ead9e5a1c13086c5746283744933e2e4a577b"
|
||||
|
||||
[[package]]
|
||||
name = "qpow-math"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Quantus-Network/chain.git?tag=v0.4.11-star-dust#89f0260cdba4eb71533cb060215cca3d166108d1"
|
||||
source = "git+https://github.com/Quantus-Network/chain.git?tag=v0.7.1-q-day-2#5ea94cb1e5347b12d091b6fa9f9234ceb897ec42"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"log",
|
||||
@@ -2240,8 +2244,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quantus-miner-api"
|
||||
version = "0.0.3"
|
||||
source = "git+https://github.com/Quantus-Network/chain.git?tag=v0.4.11-star-dust#89f0260cdba4eb71533cb060215cca3d166108d1"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/Quantus-Network/chain.git?tag=v0.7.1-q-day-2#5ea94cb1e5347b12d091b6fa9f9234ceb897ec42"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2305,6 +2309,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
@@ -2334,6 +2344,7 @@ version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
@@ -2382,6 +2393,9 @@ name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
|
||||
+5
-5
@@ -15,7 +15,7 @@ resolver = "2"
|
||||
edition = "2021"
|
||||
authors = ["Quantus Network"]
|
||||
description = "Quantus External Miner Workspace"
|
||||
version = "3.0.0"
|
||||
version = "3.3.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
@@ -29,10 +29,10 @@ num-bigint = { version = "0.4", features = ["rand"] }
|
||||
num-traits = "0.2"
|
||||
num_cpus = "1.16"
|
||||
primitive-types = { version = "0.13.1", default-features = false }
|
||||
qp-poseidon-core = { version = "1.4.0", default-features = false }
|
||||
qpow-math = { git = "https://github.com/Quantus-Network/chain.git", tag = "v0.4.11-star-dust", package = "qpow-math", default-features = false }
|
||||
quantus-miner-api = { git = "https://github.com/Quantus-Network/chain.git", tag = "v0.4.11-star-dust" }
|
||||
rand = { version = "0.8.5", default-features = false }
|
||||
qp-poseidon-core = { version = "2.1.0", default-features = false }
|
||||
qpow-math = { git = "https://github.com/Quantus-Network/chain.git", tag = "v0.7.1-q-day-2", package = "qpow-math", default-features = false }
|
||||
quantus-miner-api = { git = "https://github.com/Quantus-Network/chain.git", tag = "v0.7.1-q-day-2" }
|
||||
rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.132", default-features = false }
|
||||
thiserror = "1"
|
||||
|
||||
@@ -18,7 +18,6 @@ WORKDIR /build
|
||||
# Copy workspace files
|
||||
COPY Cargo.toml Cargo.lock rust-toolchain taplo.toml ./
|
||||
COPY crates ./crates
|
||||
COPY tests ./tests
|
||||
|
||||
# Build the miner-cli in release mode
|
||||
RUN cargo build --release -p miner-cli --locked
|
||||
|
||||
@@ -34,10 +34,13 @@ The binary will be available at `target/release/quantus-miner`.
|
||||
|
||||
| Argument | Environment Variable | Description | Default |
|
||||
|----------|---------------------|-------------|---------|
|
||||
| `--node-addr <ADDR>` | `MINER_NODE_ADDR` | Node address to connect to | `127.0.0.1:9833` |
|
||||
| `--cpu-workers <N>` | `MINER_CPU_WORKERS` | Number of CPU worker threads | Auto-detect |
|
||||
| `--gpu-devices <N>` | `MINER_GPU_DEVICES` | Number of GPU devices | 0 |
|
||||
| `--port <PORT>` | `MINER_PORT` | QUIC server port | 9833 |
|
||||
| `--metrics-port <PORT>` | `MINER_METRICS_PORT` | Prometheus metrics port | Disabled |
|
||||
| `--gpu-devices <N>` | `MINER_GPU_DEVICES` | Number of GPU devices | Auto-detect |
|
||||
| `--gpu-batch-size <N>` | `MINER_GPU_BATCH_SIZE` | GPU batch size in nonces | 1000000 |
|
||||
| `--cpu-batch-size <N>` | `MINER_CPU_BATCH_SIZE` | CPU batch size in hashes | 10000 |
|
||||
| `--gpu-throttle-ms <MS>` | `MINER_GPU_THROTTLE_MS` | Sleep duration (ms) between GPU batches | 0 |
|
||||
| `--metrics-port <PORT>` | `MINER_METRICS_PORT` | Prometheus metrics port | 9900 |
|
||||
|
||||
## GPU Mining
|
||||
|
||||
@@ -74,6 +77,9 @@ cargo build -p miner-cli --release
|
||||
# Pure GPU mining
|
||||
./target/release/quantus-miner serve --gpu-devices 1
|
||||
|
||||
# GPU mining with throttle (reduce GPU utilization)
|
||||
./target/release/quantus-miner serve --gpu-devices 1 --gpu-throttle-ms 50
|
||||
|
||||
# Hybrid mining: 4 CPU + 1 GPU workers
|
||||
./target/release/quantus-miner serve --cpu-workers 4 --gpu-devices 1
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use engine_cpu::{FastCpuEngine, MinerEngine, Range};
|
||||
use engine_cpu::{AtomicBoolCancelCheck, FastCpuEngine, MinerEngine, Range};
|
||||
use pow_core::{hash_from_nonce, JobContext};
|
||||
use primitive_types::U512;
|
||||
use rand::RngCore;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
fn bench_cpu_fast_engine(c: &mut Criterion) {
|
||||
// Create the engine
|
||||
let engine = FastCpuEngine::new();
|
||||
// Create the engine with batch size of 10000
|
||||
let engine = FastCpuEngine::new(10_000);
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
let large_range = Range {
|
||||
start: U512::from(0u64),
|
||||
@@ -18,14 +19,14 @@ fn bench_cpu_fast_engine(c: &mut Criterion) {
|
||||
c.bench_function("cpu_fast_large_range", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(10_000_000u64);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(large_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -35,7 +36,7 @@ fn bench_cpu_fast_engine(c: &mut Criterion) {
|
||||
fn bench_hash_from_nonce(c: &mut Criterion) {
|
||||
// Create a test job context
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(1000u64);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use pow_core::JobContext;
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
/// An inclusive nonce range to search.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -53,6 +53,36 @@ pub enum EngineStatus {
|
||||
},
|
||||
}
|
||||
|
||||
/// Cancellation checker passed to search_range.
|
||||
/// Returns true if the search should be cancelled.
|
||||
pub trait CancelCheck: Send + Sync {
|
||||
fn is_cancelled(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Simple cancel check using an AtomicBool flag.
|
||||
/// Useful for benchmarks and simple scenarios.
|
||||
pub struct AtomicBoolCancelCheck<'a>(pub &'a AtomicBool);
|
||||
|
||||
impl CancelCheck for AtomicBoolCancelCheck<'_> {
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.0.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel check using job ID comparison.
|
||||
/// When a new job starts, the current_job_id is incremented.
|
||||
/// Workers compare their job_id against the current to detect cancellation.
|
||||
pub struct JobIdCancelCheck<'a> {
|
||||
pub current_job_id: &'a AtomicU64,
|
||||
pub my_job_id: u64,
|
||||
}
|
||||
|
||||
impl CancelCheck for JobIdCancelCheck<'_> {
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.current_job_id.load(Ordering::Relaxed) != self.my_job_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract mining engine interface.
|
||||
///
|
||||
/// The service layer depends only on this trait to manage jobs.
|
||||
@@ -65,19 +95,28 @@ pub trait MinerEngine: Send + Sync {
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext;
|
||||
|
||||
/// Search an inclusive nonce range with cancellation support.
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus;
|
||||
fn search_range(
|
||||
&self,
|
||||
ctx: &JobContext,
|
||||
range: Range,
|
||||
cancel: &dyn CancelCheck,
|
||||
) -> EngineStatus;
|
||||
|
||||
/// Enable downcasting to concrete engine types.
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
/// Fast CPU engine using optimized pow-core helpers.
|
||||
#[derive(Default)]
|
||||
pub struct FastCpuEngine;
|
||||
pub struct FastCpuEngine {
|
||||
/// How often to check for cancellation (in hashes)
|
||||
batch_size: u64,
|
||||
}
|
||||
|
||||
impl FastCpuEngine {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
pub fn new(batch_size: u64) -> Self {
|
||||
Self {
|
||||
batch_size: batch_size.max(1), // Ensure at least 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +133,12 @@ impl MinerEngine for FastCpuEngine {
|
||||
self
|
||||
}
|
||||
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
|
||||
fn search_range(
|
||||
&self,
|
||||
ctx: &JobContext,
|
||||
range: Range,
|
||||
cancel: &dyn CancelCheck,
|
||||
) -> EngineStatus {
|
||||
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
|
||||
|
||||
if range.start > range.end {
|
||||
@@ -103,11 +147,19 @@ impl MinerEngine for FastCpuEngine {
|
||||
|
||||
let mut current = range.start;
|
||||
let mut hash_count: u64 = 0;
|
||||
// Use decrementing counter to avoid modulo division in hot loop
|
||||
// Initialize to 0 so we check cancellation immediately on first iteration
|
||||
let mut until_check: u64 = 0;
|
||||
|
||||
loop {
|
||||
if cancel.load(AtomicOrdering::Relaxed) {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
// Check for cancellation every batch_size hashes
|
||||
if until_check == 0 {
|
||||
if cancel.is_cancelled() {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
until_check = self.batch_size;
|
||||
}
|
||||
until_check -= 1;
|
||||
|
||||
let hash = hash_from_nonce(ctx, current);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
@@ -157,9 +209,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let cancel = AtomicBool::new(false);
|
||||
let engine = FastCpuEngine::new();
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel);
|
||||
let engine = FastCpuEngine::new(1000); // check every 1000 hashes
|
||||
|
||||
let status = engine.search_range(&ctx, range.clone(), &cancel);
|
||||
let status = engine.search_range(&ctx, range.clone(), &cancel_check);
|
||||
match status {
|
||||
EngineStatus::Exhausted { hash_count } => {
|
||||
let expected = (range.end - range.start + U512::one()).as_u64();
|
||||
@@ -181,9 +234,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let cancel = AtomicBool::new(true); // pre-cancelled
|
||||
let engine = FastCpuEngine::new();
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel);
|
||||
let engine = FastCpuEngine::new(1); // check every hash for immediate cancellation
|
||||
|
||||
let status = engine.search_range(&ctx, range, &cancel);
|
||||
let status = engine.search_range(&ctx, range, &cancel_check);
|
||||
match status {
|
||||
EngineStatus::Cancelled { hash_count } => {
|
||||
assert_eq!(hash_count, 0);
|
||||
|
||||
@@ -26,20 +26,21 @@ qp-poseidon-core = { workspace = true }
|
||||
qp-poseidon-constants = "1.1"
|
||||
qp-plonky2-field = { version = "1.1.1" }
|
||||
plonky2 = { package = "qp-plonky2", version = "1.1.3" }
|
||||
p3-goldilocks = "0.3"
|
||||
p3-field = "0.3"
|
||||
|
||||
wgpu = { version = "27.0.1" } # GPU compute library
|
||||
futures = "0.3" # For async executor
|
||||
bytemuck = "1.16" # For buffer mapping
|
||||
wgpu = { version = "27.0.1" } # GPU compute library
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "time"] } # Async runtime with timeout support
|
||||
futures = "0.3" # For async oneshot channels in test modules
|
||||
bytemuck = "1.16" # For buffer mapping
|
||||
rand = { workspace = true, features = ["std", "std_rng"] }
|
||||
rand_chacha = "0.3" # For deterministic random test generation
|
||||
rand_chacha = "0.9" # For deterministic random test generation
|
||||
regex = "1" # For GPU name pattern matching
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { workspace = true }
|
||||
criterion = "0.5"
|
||||
rand = { workspace = true, features = ["std", "std_rng"] }
|
||||
env_logger.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "time"] } # Add macros for #[tokio::main]
|
||||
|
||||
[[bench]]
|
||||
name = "gpu_engine_bench"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use engine_cpu::{FastCpuEngine, MinerEngine, Range};
|
||||
use engine_cpu::{AtomicBoolCancelCheck, FastCpuEngine, MinerEngine, Range};
|
||||
use engine_gpu::GpuEngine;
|
||||
use pow_core::JobContext;
|
||||
use primitive_types::U512;
|
||||
@@ -7,9 +7,10 @@ use rand::RngCore;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
fn bench_cpu_vs_gpu_small(c: &mut Criterion) {
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let cpu_engine = FastCpuEngine::new(10_000);
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
// Small range: 10K nonces - reasonable for benchmarking
|
||||
let small_range = Range {
|
||||
@@ -24,14 +25,14 @@ fn bench_cpu_vs_gpu_small(c: &mut Criterion) {
|
||||
group.bench_function("cpu", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = cpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(small_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -40,14 +41,14 @@ fn bench_cpu_vs_gpu_small(c: &mut Criterion) {
|
||||
group.bench_function("gpu", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(small_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -57,9 +58,10 @@ fn bench_cpu_vs_gpu_small(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_cpu_vs_gpu_medium(c: &mut Criterion) {
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let cpu_engine = FastCpuEngine::new(10_000);
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
// Medium range: 100K nonces
|
||||
let medium_range = Range {
|
||||
@@ -74,14 +76,14 @@ fn bench_cpu_vs_gpu_medium(c: &mut Criterion) {
|
||||
group.bench_function("cpu", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = cpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(medium_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -90,14 +92,14 @@ fn bench_cpu_vs_gpu_medium(c: &mut Criterion) {
|
||||
group.bench_function("gpu", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(medium_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -107,9 +109,10 @@ fn bench_cpu_vs_gpu_medium(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_cpu_vs_gpu_large(c: &mut Criterion) {
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let cpu_engine = FastCpuEngine::new(10_000);
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
// Large range: 1M nonces - where GPU should really shine
|
||||
let large_range = Range {
|
||||
@@ -124,14 +127,14 @@ fn bench_cpu_vs_gpu_large(c: &mut Criterion) {
|
||||
group.bench_function("cpu", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = cpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(large_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -140,14 +143,14 @@ fn bench_cpu_vs_gpu_large(c: &mut Criterion) {
|
||||
group.bench_function("gpu", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(large_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -157,9 +160,10 @@ fn bench_cpu_vs_gpu_large(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_solution_finding(c: &mut Criterion) {
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let cpu_engine = FastCpuEngine::new(10_000);
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
// Range where we expect to find solutions quickly
|
||||
let solution_range = Range {
|
||||
@@ -174,14 +178,14 @@ fn bench_solution_finding(c: &mut Criterion) {
|
||||
group.bench_function("cpu_find_solution", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(10_000u64); // Easy difficulty - should find solution
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = cpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(solution_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -190,14 +194,14 @@ fn bench_solution_finding(c: &mut Criterion) {
|
||||
group.bench_function("gpu_find_solution", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(10_000u64); // Easy difficulty - should find solution
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(solution_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -207,9 +211,10 @@ fn bench_solution_finding(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_throughput_per_second(c: &mut Criterion) {
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let cpu_engine = FastCpuEngine::new(10_000);
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
// Fixed time benchmark - see how many hashes we can do in 1 second
|
||||
let throughput_range = Range {
|
||||
@@ -224,14 +229,14 @@ fn bench_throughput_per_second(c: &mut Criterion) {
|
||||
group.bench_function("cpu_throughput", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = cpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(throughput_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -240,14 +245,14 @@ fn bench_throughput_per_second(c: &mut Criterion) {
|
||||
group.bench_function("gpu_throughput", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(throughput_range.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -257,8 +262,9 @@ fn bench_throughput_per_second(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_gpu_batch_efficiency(c: &mut Criterion) {
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel_flag);
|
||||
|
||||
let mut group = c.benchmark_group("gpu_batch_sizes");
|
||||
group.sample_size(10);
|
||||
@@ -283,14 +289,14 @@ fn bench_gpu_batch_efficiency(c: &mut Criterion) {
|
||||
group.bench_function("gpu_1k_batch", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(small_batch.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -299,14 +305,14 @@ fn bench_gpu_batch_efficiency(c: &mut Criterion) {
|
||||
group.bench_function("gpu_50k_batch", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(medium_batch.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
@@ -315,14 +321,14 @@ fn bench_gpu_batch_efficiency(c: &mut Criterion) {
|
||||
group.bench_function("gpu_500k_batch", |b| {
|
||||
b.iter(|| {
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::from(u64::MAX);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let result = gpu_engine.search_range(
|
||||
black_box(&ctx),
|
||||
black_box(large_batch.clone()),
|
||||
black_box(&cancel_flag),
|
||||
black_box(&cancel_check),
|
||||
);
|
||||
black_box(result)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use engine_cpu::{EngineStatus, FastCpuEngine, MinerEngine, Range};
|
||||
use engine_cpu::{AtomicBoolCancelCheck, EngineStatus, FastCpuEngine, MinerEngine, Range};
|
||||
use engine_gpu::GpuEngine;
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -16,16 +16,17 @@ fn main() {
|
||||
// Use a fixed header and easy difficulty (1) so any nonce is valid
|
||||
let header = [1u8; 32];
|
||||
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let cpu_engine = FastCpuEngine::new(10_000);
|
||||
let ctx = cpu_engine.prepare_context(header, difficulty);
|
||||
|
||||
log::info!("Context prepared. Difficulty: {}", difficulty);
|
||||
|
||||
let cancel = AtomicBool::new(false);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel);
|
||||
|
||||
// 3. Verify with GPU engine
|
||||
log::info!("Initializing GPU engine...");
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU");
|
||||
|
||||
// Search a small range around the valid nonce
|
||||
let gpu_range = Range {
|
||||
@@ -39,7 +40,7 @@ fn main() {
|
||||
gpu_range.end
|
||||
);
|
||||
let start = std::time::Instant::now();
|
||||
let gpu_result = gpu_engine.search_range(&ctx, gpu_range, &cancel);
|
||||
let gpu_result = gpu_engine.search_range(&ctx, gpu_range, &cancel_check);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
log::info!("GPU search took {:?}", elapsed);
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
//! GPU tier detection using regex patterns
|
||||
//!
|
||||
//! This module provides a table-driven approach to GPU detection,
|
||||
//! using regex patterns with word boundaries to avoid substring matching issues.
|
||||
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// GPU tier configuration
|
||||
struct GpuTier {
|
||||
/// Regex pattern to match GPU name (case-insensitive)
|
||||
pattern: &'static str,
|
||||
/// Human-readable tier name
|
||||
name: &'static str,
|
||||
/// Divisor for max_workgroups (higher = more conservative)
|
||||
workgroup_divisor: u32,
|
||||
/// Minimum workgroups to use
|
||||
min_workgroups: u32,
|
||||
}
|
||||
|
||||
/// Compiled GPU tier with regex
|
||||
struct CompiledGpuTier {
|
||||
regex: Regex,
|
||||
name: &'static str,
|
||||
workgroup_divisor: u32,
|
||||
min_workgroups: u32,
|
||||
}
|
||||
|
||||
impl CompiledGpuTier {
|
||||
fn from_tier(tier: &GpuTier) -> Self {
|
||||
Self {
|
||||
regex: Regex::new(&format!("(?i){}", tier.pattern)).expect("Invalid GPU tier regex"),
|
||||
name: tier.name,
|
||||
workgroup_divisor: tier.workgroup_divisor,
|
||||
min_workgroups: tier.min_workgroups,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GPU tiers are checked in order - first match wins
|
||||
// Use word boundaries (\b) to avoid substring issues like "550" matching "5500"
|
||||
|
||||
const NVIDIA_TIERS: &[GpuTier] = &[
|
||||
// Blackwell (RTX 50 series)
|
||||
GpuTier {
|
||||
pattern: r"\b50[89]0\b",
|
||||
name: "NVIDIA RTX 50 Flagship (Blackwell)",
|
||||
workgroup_divisor: 6,
|
||||
min_workgroups: 5120,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b50[67]0\b|rtx 50",
|
||||
name: "NVIDIA RTX 50 (Blackwell)",
|
||||
workgroup_divisor: 7,
|
||||
min_workgroups: 4608,
|
||||
},
|
||||
// Ada Lovelace (RTX 40 series)
|
||||
GpuTier {
|
||||
pattern: r"\b40[89]0\b",
|
||||
name: "NVIDIA RTX 40 Flagship (Ada)",
|
||||
workgroup_divisor: 8,
|
||||
min_workgroups: 4096,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b40[67]0\b|rtx 40",
|
||||
name: "NVIDIA RTX 40 (Ada)",
|
||||
workgroup_divisor: 10,
|
||||
min_workgroups: 3072,
|
||||
},
|
||||
// Ampere/Turing (RTX 30/20 series)
|
||||
GpuTier {
|
||||
pattern: r"\b30[5-9]0\b|\b20[6-8]0\b|rtx 30|rtx 20",
|
||||
name: "NVIDIA RTX 30/20 (Ampere/Turing)",
|
||||
workgroup_divisor: 12,
|
||||
min_workgroups: 2048,
|
||||
},
|
||||
// Turing/Pascal (GTX 16/10 series)
|
||||
GpuTier {
|
||||
pattern: r"\b16[56]0\b|\b10[3-8]0\b|gtx 16|gtx 10",
|
||||
name: "NVIDIA GTX 16/10 (Turing/Pascal)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1024,
|
||||
},
|
||||
// Maxwell (GTX 900 series)
|
||||
GpuTier {
|
||||
pattern: r"\b9[5-8]0\b|gtx 9",
|
||||
name: "NVIDIA GTX 900 (Maxwell)",
|
||||
workgroup_divisor: 18,
|
||||
min_workgroups: 768,
|
||||
},
|
||||
// Kepler/Maxwell (GTX 700 series)
|
||||
GpuTier {
|
||||
pattern: r"\b7[5-8]0\b|gtx 7",
|
||||
name: "NVIDIA GTX 700 (Kepler/Maxwell)",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
// Legacy GTX
|
||||
GpuTier {
|
||||
pattern: r"gtx [456]",
|
||||
name: "NVIDIA GTX Legacy (Fermi/Kepler)",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 384,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\bgtx\b",
|
||||
name: "NVIDIA GTX (Unknown)",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
// Mobile
|
||||
GpuTier {
|
||||
pattern: r"\bmx[1-5]\d0|geforce mx",
|
||||
name: "NVIDIA MX (Mobile)",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 384,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"geforce gt\b|\bgt \d{3}\b",
|
||||
name: "NVIDIA GT (Entry-Level)",
|
||||
workgroup_divisor: 28,
|
||||
min_workgroups: 256,
|
||||
},
|
||||
// Professional
|
||||
GpuTier {
|
||||
pattern: r"quadro|rtx a\d|tesla|\ba100\b|\bh100\b|\bl4\b",
|
||||
name: "NVIDIA Quadro/Professional",
|
||||
workgroup_divisor: 10,
|
||||
min_workgroups: 2560,
|
||||
},
|
||||
];
|
||||
|
||||
const AMD_TIERS: &[GpuTier] = &[
|
||||
// RDNA 4 (RX 9000 series)
|
||||
GpuTier {
|
||||
pattern: r"rx 9|\b90[78]0\b",
|
||||
name: "AMD RX 9000 (RDNA 4)",
|
||||
workgroup_divisor: 8,
|
||||
min_workgroups: 4096,
|
||||
},
|
||||
// RDNA 3 Discrete
|
||||
GpuTier {
|
||||
pattern: r"\b7900\b",
|
||||
name: "AMD RX 7900 (RDNA 3 Flagship)",
|
||||
workgroup_divisor: 9,
|
||||
min_workgroups: 3584,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"rx 7|\b7[6-8]00\b",
|
||||
name: "AMD RX 7000 (RDNA 3)",
|
||||
workgroup_divisor: 10,
|
||||
min_workgroups: 3072,
|
||||
},
|
||||
// RDNA 3 APUs - check before discrete to avoid substring match
|
||||
GpuTier {
|
||||
pattern: r"\b780m\b|radeon 780m",
|
||||
name: "AMD Radeon 780M (RDNA 3 APU)",
|
||||
workgroup_divisor: 12,
|
||||
min_workgroups: 2048,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b7[46]0m\b|radeon 7[46]0m",
|
||||
name: "AMD Radeon 7x0M (RDNA 3 APU)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1024,
|
||||
},
|
||||
// RDNA 2 Discrete
|
||||
GpuTier {
|
||||
pattern: r"\b6[89][05]0\b",
|
||||
name: "AMD RX 6900/6800 (RDNA 2 Flagship)",
|
||||
workgroup_divisor: 12,
|
||||
min_workgroups: 2560,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b6[67][05]0\b",
|
||||
name: "AMD RX 6700/6600 (RDNA 2)",
|
||||
workgroup_divisor: 14,
|
||||
min_workgroups: 2048,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b6[45]00\b",
|
||||
name: "AMD RX 6500/6400 (RDNA 2 Entry)",
|
||||
workgroup_divisor: 22,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"rx 6\d{3}",
|
||||
name: "AMD RX 6000 (RDNA 2)",
|
||||
workgroup_divisor: 14,
|
||||
min_workgroups: 2048,
|
||||
},
|
||||
// RDNA 2 APUs - check before discrete
|
||||
GpuTier {
|
||||
pattern: r"\b680m\b|radeon 680m",
|
||||
name: "AMD Radeon 680M (RDNA 2 APU)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1536,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b6[16]0m\b|radeon 6[16]0m",
|
||||
name: "AMD Radeon 6x0M (RDNA 2 APU)",
|
||||
workgroup_divisor: 22,
|
||||
min_workgroups: 768,
|
||||
},
|
||||
// RDNA 1 (RX 5000 series) - 4-digit patterns to avoid matching Polaris 3-digit
|
||||
GpuTier {
|
||||
pattern: r"\b5700\b",
|
||||
name: "AMD RX 5700 (RDNA 1)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1536,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\b5[56]00\b|rx 5\d{3}",
|
||||
name: "AMD RX 5000 (RDNA 1)",
|
||||
workgroup_divisor: 18,
|
||||
min_workgroups: 1024,
|
||||
},
|
||||
// Polaris (RX 400/500 series) - 3-digit models with boundaries
|
||||
GpuTier {
|
||||
pattern: r"rx [45]\d0\b|\b[45][6-9]0\b|\b590\b|rx 5.0x",
|
||||
name: "AMD RX 500/400 (Polaris)",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 768,
|
||||
},
|
||||
// Vega
|
||||
GpuTier {
|
||||
pattern: r"radeon vii\b",
|
||||
name: "AMD Radeon VII (Vega 20)",
|
||||
workgroup_divisor: 12,
|
||||
min_workgroups: 2048,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"vega\s*64",
|
||||
name: "AMD Vega 64 (Discrete)",
|
||||
workgroup_divisor: 14,
|
||||
min_workgroups: 1536,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"vega\s*56",
|
||||
name: "AMD Vega 56 (Discrete)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1280,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"vega",
|
||||
name: "AMD Vega (APU)",
|
||||
workgroup_divisor: 28,
|
||||
min_workgroups: 384,
|
||||
},
|
||||
// GCN
|
||||
GpuTier {
|
||||
pattern: r"fury|nano",
|
||||
name: "AMD R9 Fury/Nano (Fiji)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1280,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"r9.*(3[89]0|2[89]0)|\b[23][89]0x?\b",
|
||||
name: "AMD R9 (GCN)",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 768,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"r7.*(3[67]0|2[67]0)|\b[23][67]0x?\b",
|
||||
name: "AMD R7 (GCN)",
|
||||
workgroup_divisor: 22,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
// Professional
|
||||
GpuTier {
|
||||
pattern: r"radeon pro|instinct mi|mi[123]\d0|firepro|w[567]\d{3}",
|
||||
name: "AMD Radeon Pro/Instinct",
|
||||
workgroup_divisor: 10,
|
||||
min_workgroups: 2560,
|
||||
},
|
||||
// OEM/APU fallbacks
|
||||
GpuTier {
|
||||
pattern: r"radeon\s*\(tm\)\s*[67]\d{2}\b|radeon [67]\d{2}\b",
|
||||
name: "AMD Radeon OEM (Polaris Rebrand)",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"radeon\s*(\(tm\)\s*)?graphics",
|
||||
name: "AMD Radeon Graphics (APU)",
|
||||
workgroup_divisor: 26,
|
||||
min_workgroups: 384,
|
||||
},
|
||||
];
|
||||
|
||||
const INTEL_TIERS: &[GpuTier] = &[
|
||||
// Battlemage (Arc B-Series)
|
||||
GpuTier {
|
||||
pattern: r"arc b|\bb5[78]0\b",
|
||||
name: "Intel Arc B-Series (Battlemage)",
|
||||
workgroup_divisor: 10,
|
||||
min_workgroups: 2560,
|
||||
},
|
||||
// Alchemist Mobile - check BEFORE desktop (a770m contains a770)
|
||||
GpuTier {
|
||||
pattern: r"\ba7[37]0m\b",
|
||||
name: "Intel Arc A7 Mobile (Alchemist)",
|
||||
workgroup_divisor: 14,
|
||||
min_workgroups: 1536,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\ba5[57]0m\b",
|
||||
name: "Intel Arc A5 Mobile (Alchemist)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1024,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\ba3[57]0m\b",
|
||||
name: "Intel Arc A3 Mobile (Alchemist)",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
// Alchemist Desktop
|
||||
GpuTier {
|
||||
pattern: r"\ba7[57]0\b",
|
||||
name: "Intel Arc A7 (Alchemist)",
|
||||
workgroup_divisor: 12,
|
||||
min_workgroups: 2048,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\ba580\b",
|
||||
name: "Intel Arc A5 (Alchemist)",
|
||||
workgroup_divisor: 14,
|
||||
min_workgroups: 1536,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\ba3[18]0\b",
|
||||
name: "Intel Arc A3 (Alchemist)",
|
||||
workgroup_divisor: 18,
|
||||
min_workgroups: 768,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"arc a|\barc\b",
|
||||
name: "Intel Arc (Unknown)",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1024,
|
||||
},
|
||||
// Integrated
|
||||
GpuTier {
|
||||
pattern: r"iris xe max",
|
||||
name: "Intel Iris Xe Max (Discrete)",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"iris xe",
|
||||
name: "Intel Iris Xe (Integrated)",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 384,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"iris pro",
|
||||
name: "Intel Iris Pro (Integrated)",
|
||||
workgroup_divisor: 26,
|
||||
min_workgroups: 256,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"iris plus|iris\b",
|
||||
name: "Intel Iris Plus (Integrated)",
|
||||
workgroup_divisor: 26,
|
||||
min_workgroups: 320,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"uhd.*(7\d{2}|graphics 7)",
|
||||
name: "Intel UHD 700 (Integrated)",
|
||||
workgroup_divisor: 26,
|
||||
min_workgroups: 320,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"uhd.*(6\d{2}|graphics 6)",
|
||||
name: "Intel UHD 600 (Integrated)",
|
||||
workgroup_divisor: 28,
|
||||
min_workgroups: 256,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"uhd",
|
||||
name: "Intel UHD Graphics (Integrated)",
|
||||
workgroup_divisor: 28,
|
||||
min_workgroups: 256,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"hd graphics|hd [456]\d{2}",
|
||||
name: "Intel HD Graphics (Integrated)",
|
||||
workgroup_divisor: 30,
|
||||
min_workgroups: 192,
|
||||
},
|
||||
];
|
||||
|
||||
const QUALCOMM_TIERS: &[GpuTier] = &[
|
||||
// Snapdragon X (Adreno X1)
|
||||
GpuTier {
|
||||
pattern: r"x elite|x plus|adreno x|x1-[89]",
|
||||
name: "Qualcomm Adreno X1 (Snapdragon X)",
|
||||
workgroup_divisor: 14,
|
||||
min_workgroups: 1536,
|
||||
},
|
||||
// Adreno 700 series (730, 740, 750, etc.)
|
||||
GpuTier {
|
||||
pattern: r"adreno\s*7[0-9]{2}\b|\b7[345]0\b",
|
||||
name: "Qualcomm Adreno 700 Series",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 1024,
|
||||
},
|
||||
// Adreno 600 series (610, 612, 615, 618, 619, 620, 630, 640, 650, 660, etc.)
|
||||
GpuTier {
|
||||
pattern: r"adreno\s*6[0-9]{2}\b|\b6[1-6][0-9]\b",
|
||||
name: "Qualcomm Adreno 600 Series",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 512,
|
||||
},
|
||||
// Adreno 500 series (505, 506, 508, 509, 510, 512, 530, 540)
|
||||
GpuTier {
|
||||
pattern: r"adreno\s*5[0-4][0-9]\b|\b5[0-4][0-9]\b",
|
||||
name: "Qualcomm Adreno 500 Series",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 384,
|
||||
},
|
||||
];
|
||||
|
||||
const APPLE_TIERS: &[GpuTier] = &[
|
||||
// M4 series
|
||||
GpuTier {
|
||||
pattern: r"m4 ultra",
|
||||
name: "Apple M4 Ultra",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 1600,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m4 max",
|
||||
name: "Apple M4 Max",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 800,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m4 pro",
|
||||
name: "Apple M4 Pro",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 400,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\bm4\b",
|
||||
name: "Apple M4",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 200,
|
||||
},
|
||||
// M3 series
|
||||
GpuTier {
|
||||
pattern: r"m3 ultra",
|
||||
name: "Apple M3 Ultra",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 1520,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m3 max",
|
||||
name: "Apple M3 Max",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 800,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m3 pro",
|
||||
name: "Apple M3 Pro",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 360,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\bm3\b",
|
||||
name: "Apple M3",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 200,
|
||||
},
|
||||
// M2 series
|
||||
GpuTier {
|
||||
pattern: r"m2 ultra",
|
||||
name: "Apple M2 Ultra",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 1520,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m2 max",
|
||||
name: "Apple M2 Max",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 760,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m2 pro",
|
||||
name: "Apple M2 Pro",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 380,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\bm2\b",
|
||||
name: "Apple M2",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 200,
|
||||
},
|
||||
// M1 series
|
||||
GpuTier {
|
||||
pattern: r"m1 ultra",
|
||||
name: "Apple M1 Ultra",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 1280,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m1 max",
|
||||
name: "Apple M1 Max",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 640,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"m1 pro",
|
||||
name: "Apple M1 Pro",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 320,
|
||||
},
|
||||
GpuTier {
|
||||
pattern: r"\bm1\b",
|
||||
name: "Apple M1",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 160,
|
||||
},
|
||||
];
|
||||
|
||||
/// Compiled GPU tier tables (lazily initialized)
|
||||
struct GpuTierTables {
|
||||
nvidia: Vec<CompiledGpuTier>,
|
||||
amd: Vec<CompiledGpuTier>,
|
||||
intel: Vec<CompiledGpuTier>,
|
||||
qualcomm: Vec<CompiledGpuTier>,
|
||||
apple: Vec<CompiledGpuTier>,
|
||||
}
|
||||
|
||||
static GPU_TIERS: LazyLock<GpuTierTables> = LazyLock::new(|| GpuTierTables {
|
||||
nvidia: NVIDIA_TIERS
|
||||
.iter()
|
||||
.map(CompiledGpuTier::from_tier)
|
||||
.collect(),
|
||||
amd: AMD_TIERS.iter().map(CompiledGpuTier::from_tier).collect(),
|
||||
intel: INTEL_TIERS.iter().map(CompiledGpuTier::from_tier).collect(),
|
||||
qualcomm: QUALCOMM_TIERS
|
||||
.iter()
|
||||
.map(CompiledGpuTier::from_tier)
|
||||
.collect(),
|
||||
apple: APPLE_TIERS.iter().map(CompiledGpuTier::from_tier).collect(),
|
||||
});
|
||||
|
||||
/// Result of GPU tier matching
|
||||
pub struct GpuTierMatch {
|
||||
pub name: &'static str,
|
||||
pub workgroup_divisor: u32,
|
||||
pub min_workgroups: u32,
|
||||
pub is_fallback: bool,
|
||||
}
|
||||
|
||||
/// Find matching tier from a list of compiled tiers
|
||||
fn find_matching_tier(name: &str, tiers: &[CompiledGpuTier]) -> Option<GpuTierMatch> {
|
||||
tiers
|
||||
.iter()
|
||||
.find(|tier| tier.regex.is_match(name))
|
||||
.map(|tier| GpuTierMatch {
|
||||
name: tier.name,
|
||||
workgroup_divisor: tier.workgroup_divisor,
|
||||
min_workgroups: tier.min_workgroups,
|
||||
is_fallback: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Detect GPU tier based on adapter info
|
||||
///
|
||||
/// Returns (tier_name, workgroup_divisor, min_workgroups, is_fallback)
|
||||
pub fn detect_gpu_tier(vendor_name: &str, vendor_id: u32, is_metal_backend: bool) -> GpuTierMatch {
|
||||
let name_lower = vendor_name.to_lowercase();
|
||||
|
||||
// Determine vendor and find matching tier
|
||||
if name_lower.contains("nvidia") || vendor_id == 0x10DE {
|
||||
find_matching_tier(&name_lower, &GPU_TIERS.nvidia).unwrap_or(GpuTierMatch {
|
||||
name: "NVIDIA Unknown",
|
||||
workgroup_divisor: 20,
|
||||
min_workgroups: 512,
|
||||
is_fallback: true,
|
||||
})
|
||||
} else if name_lower.contains("amd") || name_lower.contains("radeon") || vendor_id == 0x1002 {
|
||||
find_matching_tier(&name_lower, &GPU_TIERS.amd).unwrap_or(GpuTierMatch {
|
||||
name: "AMD Unknown",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 512,
|
||||
is_fallback: true,
|
||||
})
|
||||
} else if name_lower.contains("intel") || vendor_id == 0x8086 {
|
||||
find_matching_tier(&name_lower, &GPU_TIERS.intel).unwrap_or(GpuTierMatch {
|
||||
name: "Intel Unknown",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 256,
|
||||
is_fallback: true,
|
||||
})
|
||||
} else if name_lower.contains("qualcomm")
|
||||
|| name_lower.contains("adreno")
|
||||
|| vendor_id == 0x5143
|
||||
{
|
||||
find_matching_tier(&name_lower, &GPU_TIERS.qualcomm).unwrap_or(GpuTierMatch {
|
||||
name: "Qualcomm Adreno (Unknown)",
|
||||
workgroup_divisor: 24,
|
||||
min_workgroups: 384,
|
||||
is_fallback: true,
|
||||
})
|
||||
} else if is_metal_backend {
|
||||
// Apple Silicon - detected by Metal backend
|
||||
find_matching_tier(&name_lower, &GPU_TIERS.apple).unwrap_or(GpuTierMatch {
|
||||
name: "Apple Silicon Unknown",
|
||||
workgroup_divisor: 4,
|
||||
min_workgroups: 160,
|
||||
is_fallback: true,
|
||||
})
|
||||
} else {
|
||||
GpuTierMatch {
|
||||
name: "Unknown GPU",
|
||||
workgroup_divisor: 16,
|
||||
min_workgroups: 512,
|
||||
is_fallback: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_nvidia_detection() {
|
||||
// RTX 40 series
|
||||
let tier = detect_gpu_tier("NVIDIA GeForce RTX 4090", 0x10DE, false);
|
||||
assert_eq!(tier.name, "NVIDIA RTX 40 Flagship (Ada)");
|
||||
assert!(!tier.is_fallback);
|
||||
|
||||
let tier = detect_gpu_tier("NVIDIA GeForce RTX 4070 Ti", 0x10DE, false);
|
||||
assert_eq!(tier.name, "NVIDIA RTX 40 (Ada)");
|
||||
|
||||
// RTX 30 series
|
||||
let tier = detect_gpu_tier("NVIDIA GeForce RTX 3080", 0x10DE, false);
|
||||
assert_eq!(tier.name, "NVIDIA RTX 30/20 (Ampere/Turing)");
|
||||
|
||||
// GTX 10 series
|
||||
let tier = detect_gpu_tier("NVIDIA GeForce GTX 1080 Ti", 0x10DE, false);
|
||||
assert_eq!(tier.name, "NVIDIA GTX 16/10 (Turing/Pascal)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_amd_rdna_vs_polaris() {
|
||||
// RDNA 1 - should NOT match Polaris
|
||||
let tier = detect_gpu_tier("AMD Radeon RX 5500 XT", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD RX 5000 (RDNA 1)");
|
||||
assert!(!tier.is_fallback);
|
||||
|
||||
let tier = detect_gpu_tier("AMD Radeon RX 5600 XT", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD RX 5000 (RDNA 1)");
|
||||
|
||||
let tier = detect_gpu_tier("AMD Radeon RX 5700 XT", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD RX 5700 (RDNA 1)");
|
||||
|
||||
// Polaris - should match Polaris
|
||||
let tier = detect_gpu_tier("AMD Radeon RX 580", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD RX 500/400 (Polaris)");
|
||||
|
||||
let tier = detect_gpu_tier("AMD Radeon RX 560X", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD RX 500/400 (Polaris)");
|
||||
|
||||
let tier = detect_gpu_tier("AMD Radeon RX 550", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD RX 500/400 (Polaris)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_amd_vega_apu() {
|
||||
let tier = detect_gpu_tier("AMD Radeon(TM) Vega 8 Graphics", 0x1002, false);
|
||||
assert_eq!(tier.name, "AMD Vega (APU)");
|
||||
assert!(!tier.is_fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intel_arc_mobile_vs_desktop() {
|
||||
// Mobile should match mobile tier
|
||||
let tier = detect_gpu_tier("Intel Arc A770M", 0x8086, false);
|
||||
assert_eq!(tier.name, "Intel Arc A7 Mobile (Alchemist)");
|
||||
assert!(!tier.is_fallback);
|
||||
|
||||
// Desktop should match desktop tier
|
||||
let tier = detect_gpu_tier("Intel Arc A770", 0x8086, false);
|
||||
assert_eq!(tier.name, "Intel Arc A7 (Alchemist)");
|
||||
assert!(!tier.is_fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apple_silicon() {
|
||||
let tier = detect_gpu_tier("Apple M1 Pro", 0, true);
|
||||
assert_eq!(tier.name, "Apple M1 Pro");
|
||||
assert!(!tier.is_fallback);
|
||||
|
||||
let tier = detect_gpu_tier("Apple M3 Max", 0, true);
|
||||
assert_eq!(tier.name, "Apple M3 Max");
|
||||
assert!(!tier.is_fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualcomm_adreno_series() {
|
||||
// Adreno 627 should match 600 series, NOT 700 series
|
||||
let tier = detect_gpu_tier("Qualcomm Adreno 627", 0x5143, false);
|
||||
assert_eq!(tier.name, "Qualcomm Adreno 600 Series");
|
||||
assert!(!tier.is_fallback);
|
||||
|
||||
// Adreno 730 should match 700 series
|
||||
let tier = detect_gpu_tier("Qualcomm Adreno 730", 0x5143, false);
|
||||
assert_eq!(tier.name, "Qualcomm Adreno 700 Series");
|
||||
assert!(!tier.is_fallback);
|
||||
|
||||
// Adreno 540 should match 500 series
|
||||
let tier = detect_gpu_tier("Qualcomm Adreno 540", 0x5143, false);
|
||||
assert_eq!(tier.name, "Qualcomm Adreno 500 Series");
|
||||
assert!(!tier.is_fallback);
|
||||
}
|
||||
}
|
||||
+619
-408
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
||||
use futures::executor::block_on;
|
||||
|
||||
mod end_to_end_tests;
|
||||
mod tests;
|
||||
|
||||
fn main() {
|
||||
block_on(run()).unwrap();
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
run().await
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
@@ -194,8 +194,7 @@ const MDS_MATRIX_DIAG_12: array<array<u32, 2>, 12> = array<array<u32, 2>, 12>(
|
||||
@group(0) @binding(1) var<storage, read> header: array<u32, 8>; // 32 bytes
|
||||
@group(0) @binding(2) var<storage, read> start_nonce: array<u32, 16>; // 64 bytes
|
||||
@group(0) @binding(3) var<storage, read> difficulty_target: array<u32, 16>; // 64 bytes (U512 target)
|
||||
@group(0) @binding(4) var<storage, read> dispatch_config: array<u32, 4>; // [total_threads, nonces_per_thread, total_nonces, cancel_check_interval]
|
||||
@group(0) @binding(5) var<storage, read> cancel_flag: array<u32, 1>; // 0 = running, 1 = cancel requested
|
||||
@group(0) @binding(4) var<storage, read> dispatch_config: array<u32, 3>; // [total_threads, nonces_per_thread, total_nonces]
|
||||
|
||||
// Goldilocks field element represented as [limb0, limb1]
|
||||
// where the value is limb0 + limb1*2^32
|
||||
@@ -804,20 +803,16 @@ fn is_below_target(hash: array<u32, 16>, difficulty_tgt: array<u32, 16>) -> bool
|
||||
// Main mining kernel
|
||||
@compute @workgroup_size(256)
|
||||
fn mining_main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
// If solution already found or cancelled, exit early
|
||||
// If solution already found, exit early
|
||||
if (atomicLoad(&results[0]) != 0u) {
|
||||
return;
|
||||
}
|
||||
if (cancel_flag[0] != 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
let thread_id = global_id.x;
|
||||
// Read dispatch configuration from buffer
|
||||
let total_threads = dispatch_config[0]; // Total logical threads in this dispatch
|
||||
let nonces_per_thread = dispatch_config[1]; // Nonces processed by each thread
|
||||
let total_nonces = dispatch_config[2]; // Total logical nonces this dispatch should cover
|
||||
let cancel_check_interval = dispatch_config[3]; // How often to check cancel flag
|
||||
|
||||
// Guard against threads beyond configured total_threads
|
||||
if (thread_id >= total_threads) {
|
||||
@@ -834,14 +829,6 @@ fn mining_main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Periodic checks for early exit
|
||||
// Check cancel flag every cancel_check_interval iterations
|
||||
if (cancel_check_interval > 0u && (j % cancel_check_interval) == 0u) {
|
||||
if (cancel_flag[0] != 0u) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if solution already found (early exit for entire dispatch)
|
||||
if (atomicLoad(&results[0]) != 0u) {
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use p3_field::PrimeField64 as P3PrimeField64;
|
||||
use p3_goldilocks::Goldilocks as P3Goldilocks;
|
||||
use plonky2::hash::poseidon2::P2Permuter;
|
||||
use qp_plonky2_field::goldilocks_field::GoldilocksField;
|
||||
use qp_plonky2_field::types::{Field, Field64, PrimeField64};
|
||||
use qp_poseidon_core::Goldilocks as QpGoldilocks;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
@@ -204,8 +203,8 @@ fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
||||
|
||||
// Random small values (both operands < 2^32)
|
||||
for _ in 0..50 {
|
||||
let a = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
||||
let a = GoldilocksField::from_canonical_u64(rng.random::<u32>() as u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.random::<u32>() as u64);
|
||||
vectors.push(GfMulTestCase {
|
||||
a,
|
||||
b,
|
||||
@@ -215,8 +214,8 @@ fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
||||
|
||||
// Random mixed cases (one small, one large)
|
||||
for _ in 0..80 {
|
||||
let a = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>());
|
||||
let a = GoldilocksField::from_canonical_u64(rng.random::<u32>() as u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.random::<u64>());
|
||||
vectors.push(GfMulTestCase {
|
||||
a,
|
||||
b,
|
||||
@@ -226,8 +225,8 @@ fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
||||
|
||||
// Random large values (both operands >= 2^32)
|
||||
for _ in 0..100 {
|
||||
let a = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
||||
let a = GoldilocksField::from_canonical_u64(rng.random::<u64>() | 0x100000000u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.random::<u64>() | 0x100000000u64);
|
||||
vectors.push(GfMulTestCase {
|
||||
a,
|
||||
b,
|
||||
@@ -237,9 +236,9 @@ fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
||||
|
||||
// Random values near the field modulus
|
||||
for _ in 0..30 {
|
||||
let offset = rng.gen::<u32>() as u64;
|
||||
let offset = rng.random::<u32>() as u64;
|
||||
let a = GoldilocksField::from_canonical_u64(GoldilocksField::ORDER - offset);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>());
|
||||
let b = GoldilocksField::from_canonical_u64(rng.random::<u64>());
|
||||
vectors.push(GfMulTestCase {
|
||||
a,
|
||||
b,
|
||||
@@ -250,7 +249,7 @@ fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
||||
// Add more specific high-limb test cases to detect patterns
|
||||
for i in 20..40 {
|
||||
let val1 = 1u64 << i; // Powers of 2 from 2^20 to 2^39
|
||||
let val2 = rng.gen::<u32>() as u64 | (1u64 << 32); // Random value with high bit set
|
||||
let val2 = rng.random::<u32>() as u64 | (1u64 << 32); // Random value with high bit set
|
||||
let a = GoldilocksField::from_canonical_u64(val1);
|
||||
let b = GoldilocksField::from_canonical_u64(val2);
|
||||
vectors.push(GfMulTestCase {
|
||||
@@ -262,9 +261,9 @@ fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
||||
|
||||
// Test values with specific limb patterns
|
||||
for _ in 0..20 {
|
||||
let high_val = (rng.gen::<u32>() as u64) << 32;
|
||||
let high_val = (rng.random::<u32>() as u64) << 32;
|
||||
let a = GoldilocksField::from_canonical_u64(high_val);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
||||
let b = GoldilocksField::from_canonical_u64(rng.random::<u64>() | 0x100000000u64);
|
||||
vectors.push(GfMulTestCase {
|
||||
a,
|
||||
b,
|
||||
@@ -331,21 +330,21 @@ fn generate_sbox_test_vectors() -> Vec<SboxTestCase> {
|
||||
|
||||
// Random small values (< 2^32)
|
||||
for _ in 0..20 {
|
||||
let input = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
||||
let input = GoldilocksField::from_canonical_u64(rng.random::<u32>() as u64);
|
||||
let expected = input.exp_u64(7);
|
||||
vectors.push(SboxTestCase { input, expected });
|
||||
}
|
||||
|
||||
// Random large values (>= 2^32)
|
||||
for _ in 0..30 {
|
||||
let input = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
||||
let input = GoldilocksField::from_canonical_u64(rng.random::<u64>() | 0x100000000u64);
|
||||
let expected = input.exp_u64(7);
|
||||
vectors.push(SboxTestCase { input, expected });
|
||||
}
|
||||
|
||||
// Values near the field modulus
|
||||
for _ in 0..10 {
|
||||
let offset = rng.gen::<u32>() as u64;
|
||||
let offset = rng.random::<u32>() as u64;
|
||||
let input = GoldilocksField::from_canonical_u64(GoldilocksField::ORDER - offset);
|
||||
let expected = input.exp_u64(7);
|
||||
vectors.push(SboxTestCase { input, expected });
|
||||
@@ -387,7 +386,7 @@ fn generate_external_linear_layer_test_vectors() -> Vec<ExternalLinearLayerTestC
|
||||
for _ in 0..15 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
let val = rng.gen::<u16>() as u64;
|
||||
let val = rng.random::<u16>() as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let expected = apply_full_external_linear_layer_to_state(&input);
|
||||
@@ -398,7 +397,7 @@ fn generate_external_linear_layer_test_vectors() -> Vec<ExternalLinearLayerTestC
|
||||
for _ in 0..10 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
let val = rng.gen::<u32>() as u64;
|
||||
let val = rng.random::<u32>() as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let expected = apply_full_external_linear_layer_to_state(&input);
|
||||
@@ -432,7 +431,7 @@ fn generate_internal_linear_layer_test_vectors() -> Vec<InternalLinearLayerTestC
|
||||
for _ in 0..10 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
*item = GoldilocksField::from_canonical_u64(rng.gen_range(0..100));
|
||||
*item = GoldilocksField::from_canonical_u64(rng.random_range(0..100));
|
||||
}
|
||||
let expected = apply_internal_linear_layer_to_state(&input);
|
||||
vectors.push(InternalLinearLayerTestCase { input, expected });
|
||||
@@ -442,7 +441,7 @@ fn generate_internal_linear_layer_test_vectors() -> Vec<InternalLinearLayerTestC
|
||||
for _ in 0..20 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
*item = GoldilocksField::from_noncanonical_u64(rng.gen());
|
||||
*item = GoldilocksField::from_noncanonical_u64(rng.random());
|
||||
}
|
||||
let expected = apply_internal_linear_layer_to_state(&input);
|
||||
vectors.push(InternalLinearLayerTestCase { input, expected });
|
||||
@@ -487,7 +486,7 @@ fn generate_mds_test_vectors() -> Vec<MdsTestCase> {
|
||||
for _ in 0..50 {
|
||||
let mut input = [GoldilocksField::ZERO; 4];
|
||||
for item in &mut input {
|
||||
*item = GoldilocksField::from_canonical_u64(rng.gen::<u16>() as u64);
|
||||
*item = GoldilocksField::from_canonical_u64(rng.random::<u16>() as u64);
|
||||
}
|
||||
let expected = apply_external_linear_layer_to_chunk(&input);
|
||||
vectors.push(MdsTestCase { input, expected });
|
||||
@@ -497,7 +496,7 @@ fn generate_mds_test_vectors() -> Vec<MdsTestCase> {
|
||||
for _ in 0..30 {
|
||||
let mut input = [GoldilocksField::ZERO; 4];
|
||||
for item in &mut input {
|
||||
*item = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
||||
*item = GoldilocksField::from_canonical_u64(rng.random::<u32>() as u64);
|
||||
}
|
||||
let expected = apply_external_linear_layer_to_chunk(&input);
|
||||
vectors.push(MdsTestCase { input, expected });
|
||||
@@ -507,7 +506,7 @@ fn generate_mds_test_vectors() -> Vec<MdsTestCase> {
|
||||
for _ in 0..30 {
|
||||
let mut input = [GoldilocksField::ZERO; 4];
|
||||
for item in &mut input {
|
||||
*item = GoldilocksField::from_canonical_u64(rng.gen::<u64>());
|
||||
*item = GoldilocksField::from_canonical_u64(rng.random::<u64>());
|
||||
}
|
||||
let expected = apply_external_linear_layer_to_chunk(&input);
|
||||
vectors.push(MdsTestCase { input, expected });
|
||||
@@ -517,7 +516,7 @@ fn generate_mds_test_vectors() -> Vec<MdsTestCase> {
|
||||
for _ in 0..10 {
|
||||
let mut input = [GoldilocksField::ZERO; 4];
|
||||
for item in &mut input {
|
||||
let offset = rng.gen::<u32>() as u64;
|
||||
let offset = rng.random::<u32>() as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(GoldilocksField::ORDER - offset);
|
||||
}
|
||||
let expected = apply_external_linear_layer_to_chunk(&input);
|
||||
@@ -710,7 +709,7 @@ fn generate_poseidon2_test_vectors() -> Vec<Poseidon2TestCase> {
|
||||
for _ in 0..15 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
let val = rng.gen::<u16>() as u64;
|
||||
let val = rng.random::<u16>() as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
||||
@@ -721,7 +720,7 @@ fn generate_poseidon2_test_vectors() -> Vec<Poseidon2TestCase> {
|
||||
for _ in 0..20 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
let val = rng.gen::<u16>() as u64;
|
||||
let val = rng.random::<u16>() as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
||||
@@ -732,7 +731,7 @@ fn generate_poseidon2_test_vectors() -> Vec<Poseidon2TestCase> {
|
||||
for _ in 0..15 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
let val = rng.gen::<u32>() as u64;
|
||||
let val = rng.random::<u32>() as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
||||
@@ -744,7 +743,7 @@ fn generate_poseidon2_test_vectors() -> Vec<Poseidon2TestCase> {
|
||||
for _ in 0..15 {
|
||||
let mut input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut input {
|
||||
let val = rng.gen_range(65536..u32::MAX) as u64;
|
||||
let val = rng.random_range(65536..u32::MAX) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
||||
@@ -802,10 +801,10 @@ fn generate_field_to_bytes_test_vectors() -> Vec<FieldToBytesTestCase> {
|
||||
// Test 4: Random values
|
||||
for _ in 0..10 {
|
||||
let input = [
|
||||
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.random::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.random::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.random::<u64>() % GoldilocksField::ORDER),
|
||||
GoldilocksField::from_canonical_u64(rng.random::<u64>() % GoldilocksField::ORDER),
|
||||
];
|
||||
let mut expected = [0u8; 32];
|
||||
for (i, &felt) in input.iter().enumerate() {
|
||||
@@ -888,8 +887,8 @@ fn generate_bytes_to_field_test_vectors() -> Vec<BytesToFieldTestCase> {
|
||||
let mut vectors = Vec::new();
|
||||
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0x98765432);
|
||||
|
||||
// Helper to convert P3 Goldilocks to P2 (Plonky2) GoldilocksField
|
||||
fn p3_to_p2(felts: Vec<P3Goldilocks>) -> Vec<GoldilocksField> {
|
||||
// Helper to convert qp_poseidon_core::Goldilocks to Plonky2 GoldilocksField
|
||||
fn qp_to_p2(felts: Vec<QpGoldilocks>) -> Vec<GoldilocksField> {
|
||||
felts
|
||||
.into_iter()
|
||||
.map(|g| GoldilocksField::from_noncanonical_u64(g.as_canonical_u64()))
|
||||
@@ -898,7 +897,7 @@ fn generate_bytes_to_field_test_vectors() -> Vec<BytesToFieldTestCase> {
|
||||
|
||||
// Test 1: All zeros
|
||||
let zero_input = [0u8; 96];
|
||||
let zero_expected = p3_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&zero_input));
|
||||
let zero_expected = qp_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&zero_input));
|
||||
vectors.push(BytesToFieldTestCase {
|
||||
input_bytes: zero_input,
|
||||
expected_felts: zero_expected,
|
||||
@@ -906,7 +905,7 @@ fn generate_bytes_to_field_test_vectors() -> Vec<BytesToFieldTestCase> {
|
||||
|
||||
// Test 2: All 0xFF
|
||||
let ff_input = [0xFFu8; 96];
|
||||
let ff_expected = p3_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&ff_input));
|
||||
let ff_expected = qp_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&ff_input));
|
||||
vectors.push(BytesToFieldTestCase {
|
||||
input_bytes: ff_input,
|
||||
expected_felts: ff_expected,
|
||||
@@ -917,7 +916,7 @@ fn generate_bytes_to_field_test_vectors() -> Vec<BytesToFieldTestCase> {
|
||||
for (i, byte) in seq_input.iter_mut().enumerate() {
|
||||
*byte = (i % 256) as u8;
|
||||
}
|
||||
let seq_expected = p3_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&seq_input));
|
||||
let seq_expected = qp_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&seq_input));
|
||||
vectors.push(BytesToFieldTestCase {
|
||||
input_bytes: seq_input,
|
||||
expected_felts: seq_expected,
|
||||
@@ -927,7 +926,7 @@ fn generate_bytes_to_field_test_vectors() -> Vec<BytesToFieldTestCase> {
|
||||
for _ in 0..10 {
|
||||
let mut input = [0u8; 96];
|
||||
rng.fill(&mut input[..]);
|
||||
let expected = p3_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&input));
|
||||
let expected = qp_to_p2(qp_poseidon_core::serialization::bytes_to_felts(&input));
|
||||
vectors.push(BytesToFieldTestCase {
|
||||
input_bytes: input,
|
||||
expected_felts: expected,
|
||||
@@ -1135,7 +1134,7 @@ fn generate_true_internal_only_test_vectors() -> Vec<InternalRoundsTestCase> {
|
||||
for _ in 0..15 {
|
||||
let mut random_input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut random_input {
|
||||
let val = (rng.gen::<u8>()) as u64;
|
||||
let val = (rng.random::<u8>()) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let after_initial_external = apply_initial_external(random_input);
|
||||
@@ -1150,7 +1149,7 @@ fn generate_true_internal_only_test_vectors() -> Vec<InternalRoundsTestCase> {
|
||||
for _ in 0..10 {
|
||||
let mut random_input = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut random_input {
|
||||
let val = (rng.gen::<u32>()) as u64;
|
||||
let val = (rng.random::<u32>()) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
let after_initial_external = apply_initial_external(random_input);
|
||||
@@ -1461,7 +1460,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
||||
for _ in 0..15 {
|
||||
let mut state = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut state {
|
||||
let val = (rng.gen::<u32>()) as u64;
|
||||
let val = (rng.random::<u32>()) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
test_cases.push(state);
|
||||
@@ -1471,7 +1470,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
||||
for _ in 0..10 {
|
||||
let mut state = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut state {
|
||||
let val = (rng.gen::<u16>()) as u64;
|
||||
let val = (rng.random::<u16>()) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
test_cases.push(state);
|
||||
@@ -1720,7 +1719,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
||||
for _ in 0..15 {
|
||||
let mut state = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut state {
|
||||
let val = (rng.gen::<u8>()) as u64;
|
||||
let val = (rng.random::<u8>()) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
test_cases.push(state);
|
||||
@@ -1730,7 +1729,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
||||
for _ in 0..10 {
|
||||
let mut state = [GoldilocksField::ZERO; 12];
|
||||
for item in &mut state {
|
||||
let val = (rng.gen::<u16>()) as u64;
|
||||
let val = (rng.random::<u16>()) as u64;
|
||||
*item = GoldilocksField::from_canonical_u64(val);
|
||||
}
|
||||
test_cases.push(state);
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
// Always re-run if version env or git state might change
|
||||
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
|
||||
println!("cargo:rerun-if-env-changed=GITHUB_SHA");
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs");
|
||||
println!("cargo:rerun-if-changed=.git/packed-refs");
|
||||
|
||||
let pkg_version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
|
||||
let short_sha = match env::var("GITHUB_SHA") {
|
||||
Ok(s) => s.get(0..8).map(|t| t.to_string()),
|
||||
Err(_) => discover_short_git_sha().ok(),
|
||||
};
|
||||
|
||||
let version = match short_sha {
|
||||
Some(sha) if !sha.is_empty() => format!("{pkg_version}+{sha}"),
|
||||
_ => pkg_version.clone(),
|
||||
};
|
||||
|
||||
// Expose to the crate as an env! binding
|
||||
println!("cargo:rustc-env=MINER_VERSION={version}");
|
||||
|
||||
// Optional extra info for diagnostics
|
||||
if let Ok(sha) = env::var("GITHUB_SHA") {
|
||||
println!("cargo:rustc-env=MINER_BUILD_SHA={}", sha);
|
||||
}
|
||||
|
||||
// Log the computed version for debugging
|
||||
println!("miner-cli: computed MINER_VERSION={version}");
|
||||
}
|
||||
|
||||
/// Attempts to determine the short git SHA (first 8 hex chars) without requiring the `git` binary.
|
||||
/// Handles both normal repositories and worktrees where `.git` is a file pointing to the real gitdir.
|
||||
fn discover_short_git_sha() -> io::Result<String> {
|
||||
let git_dir = find_git_dir()?;
|
||||
let head_path = git_dir.join("HEAD");
|
||||
let head = fs::read_to_string(&head_path)?.trim().to_string();
|
||||
|
||||
// HEAD can be either a direct SHA or a ref: refs/heads/branch
|
||||
if let Some(ref_line) = head.strip_prefix("ref: ") {
|
||||
let ref_path = git_dir.join(ref_line.trim());
|
||||
if ref_path.is_file() {
|
||||
let mut contents = String::new();
|
||||
fs::File::open(ref_path)?.read_to_string(&mut contents)?;
|
||||
let sha = contents.trim();
|
||||
return Ok(shorten_sha(sha));
|
||||
}
|
||||
// Not a loose ref; try packed-refs
|
||||
let packed = git_dir.join("packed-refs");
|
||||
if packed.is_file() {
|
||||
let contents = fs::read_to_string(packed)?;
|
||||
for line in contents.lines() {
|
||||
// Skip comments and peeled lines
|
||||
if line.starts_with('#') || line.starts_with('^') || line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Format: <sha> <ref>
|
||||
if let Some((sha, r)) = line.split_once(' ') {
|
||||
if r.trim() == ref_line.trim() {
|
||||
return Ok(shorten_sha(sha.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!(
|
||||
"Could not resolve ref '{}' in {:?}",
|
||||
ref_line.trim(),
|
||||
git_dir
|
||||
),
|
||||
))
|
||||
} else {
|
||||
// HEAD contained a SHA directly (e.g., detached HEAD)
|
||||
Ok(shorten_sha(head.trim()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the actual .git directory, handling both:
|
||||
/// - Standard repo: a ".git" directory exists, or
|
||||
/// - Worktree/submodule: ".git" is a file containing "gitdir: <path>"
|
||||
fn find_git_dir() -> io::Result<PathBuf> {
|
||||
let mut dir = env::current_dir()?;
|
||||
loop {
|
||||
let dot_git = dir.join(".git");
|
||||
if dot_git.is_dir() {
|
||||
return Ok(dot_git);
|
||||
} else if dot_git.is_file() {
|
||||
// Read the pointer file
|
||||
let contents = fs::read_to_string(&dot_git)?;
|
||||
if let Some(p) = contents.strip_prefix("gitdir:") {
|
||||
let p = p.trim();
|
||||
let path = Path::new(p);
|
||||
// If relative, it's relative to the location of the .git file's parent
|
||||
let resolved = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
dir.join(path)
|
||||
};
|
||||
if resolved.is_dir() {
|
||||
return Ok(resolved);
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("Resolved gitdir does not exist: {}", resolved.display()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ascend until root
|
||||
if !dir.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"Could not locate .git directory",
|
||||
))
|
||||
}
|
||||
|
||||
fn shorten_sha(full: &str) -> String {
|
||||
let s = full.trim();
|
||||
if s.len() >= 8 {
|
||||
s[..8].to_string()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use engine_cpu::{EngineRange, MinerEngine};
|
||||
use engine_cpu::{AtomicBoolCancelCheck, EngineRange, MinerEngine};
|
||||
use miner_service::{run, ServiceConfig};
|
||||
use primitive_types::U512;
|
||||
use rand::RngCore;
|
||||
@@ -8,12 +8,16 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// CLI defaults
|
||||
const DEFAULT_GPU_BATCH_SIZE: u32 = 1_000_000;
|
||||
const DEFAULT_CPU_BATCH_SIZE: u64 = 10_000;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Run the mining service
|
||||
Serve {
|
||||
/// Address of the node to connect to (e.g., "127.0.0.1:9833")
|
||||
#[arg(long, env = "MINER_NODE_ADDR")]
|
||||
/// Address of the node to connect to
|
||||
#[arg(long, env = "MINER_NODE_ADDR", default_value = "127.0.0.1:9833")]
|
||||
node_addr: std::net::SocketAddr,
|
||||
|
||||
/// Number of CPU worker threads to use for mining (default: auto-detect)
|
||||
@@ -24,9 +28,13 @@ enum Command {
|
||||
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
|
||||
gpu_devices: Option<usize>,
|
||||
|
||||
/// GPU cancel check interval in nonces (default: 10000)
|
||||
#[arg(long = "gpu-cancel-interval", env = "MINER_GPU_CANCEL_INTERVAL")]
|
||||
gpu_cancel_interval: Option<u32>,
|
||||
/// GPU batch size in nonces - controls how often GPU checks for cancellation
|
||||
#[arg(long = "gpu-batch-size", env = "MINER_GPU_BATCH_SIZE", default_value_t = DEFAULT_GPU_BATCH_SIZE)]
|
||||
gpu_batch_size: u32,
|
||||
|
||||
/// CPU batch size in hashes - controls how often CPU checks for cancellation
|
||||
#[arg(long = "cpu-batch-size", env = "MINER_CPU_BATCH_SIZE", default_value_t = DEFAULT_CPU_BATCH_SIZE)]
|
||||
cpu_batch_size: u64,
|
||||
|
||||
/// Port for Prometheus metrics HTTP endpoint (default: 9900)
|
||||
#[arg(
|
||||
@@ -36,6 +44,14 @@ enum Command {
|
||||
)]
|
||||
metrics_port: u16,
|
||||
|
||||
/// GPU throttle delay in milliseconds between batches (0 = no throttle)
|
||||
#[arg(
|
||||
long = "gpu-throttle-ms",
|
||||
env = "MINER_GPU_THROTTLE_MS",
|
||||
default_value_t = 0
|
||||
)]
|
||||
gpu_throttle_ms: u64,
|
||||
|
||||
/// Enable verbose logging
|
||||
#[arg(short, long, env = "MINER_VERBOSE")]
|
||||
verbose: bool,
|
||||
@@ -51,6 +67,14 @@ enum Command {
|
||||
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
|
||||
gpu_devices: Option<usize>,
|
||||
|
||||
/// GPU batch size in nonces - controls how often GPU checks for cancellation
|
||||
#[arg(long = "gpu-batch-size", env = "MINER_GPU_BATCH_SIZE", default_value_t = DEFAULT_GPU_BATCH_SIZE)]
|
||||
gpu_batch_size: u32,
|
||||
|
||||
/// CPU batch size in hashes - controls how often CPU checks for cancellation
|
||||
#[arg(long = "cpu-batch-size", env = "MINER_CPU_BATCH_SIZE", default_value_t = DEFAULT_CPU_BATCH_SIZE)]
|
||||
cpu_batch_size: u64,
|
||||
|
||||
/// Benchmark duration in seconds (default: 10)
|
||||
#[arg(short, long, default_value_t = 10)]
|
||||
duration: u64,
|
||||
@@ -63,7 +87,7 @@ enum Command {
|
||||
|
||||
/// Quantus External Miner CLI
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = option_env!("MINER_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), about, long_about = None)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
@@ -74,7 +98,7 @@ async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let Some(command) = args.command else {
|
||||
eprintln!("Error: No command provided. Use 'serve --node-addr <ADDRESS>' to start mining.");
|
||||
eprintln!("Error: No command provided. Use 'serve' to start mining (defaults to local node at 127.0.0.1:9833).");
|
||||
eprintln!("Example: quantus-miner serve --node-addr 127.0.0.1:9833");
|
||||
std::process::exit(1);
|
||||
};
|
||||
@@ -84,7 +108,9 @@ async fn main() {
|
||||
node_addr,
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
gpu_cancel_interval,
|
||||
gpu_batch_size,
|
||||
cpu_batch_size,
|
||||
gpu_throttle_ms,
|
||||
metrics_port,
|
||||
verbose,
|
||||
} => {
|
||||
@@ -106,7 +132,9 @@ async fn main() {
|
||||
node_addr,
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
gpu_cancel_interval,
|
||||
gpu_batch_size,
|
||||
cpu_batch_size,
|
||||
gpu_throttle_ms,
|
||||
};
|
||||
|
||||
if let Err(e) = run(config).await {
|
||||
@@ -118,33 +146,49 @@ async fn main() {
|
||||
Command::Benchmark {
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
gpu_batch_size,
|
||||
cpu_batch_size,
|
||||
duration,
|
||||
verbose,
|
||||
} => {
|
||||
init_logger(verbose);
|
||||
run_benchmark(cpu_workers, gpu_devices, duration).await;
|
||||
run_benchmark(
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
gpu_batch_size,
|
||||
cpu_batch_size,
|
||||
duration,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logger(verbose: bool) {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
// Filter out noisy wgpu/naga shader compilation logs
|
||||
let log_level = if verbose {
|
||||
"debug,miner=debug,gpu_engine=debug,engine_cpu=debug"
|
||||
"debug,miner=debug,gpu_engine=debug,engine_cpu=debug,wgpu=warn,wgpu_core=warn,wgpu_hal=warn,naga=warn"
|
||||
} else {
|
||||
"info,miner=info,gpu_engine=info"
|
||||
"info,miner=info,gpu_engine=info,wgpu=error,wgpu_core=error,wgpu_hal=error,naga=error"
|
||||
};
|
||||
std::env::set_var("RUST_LOG", log_level);
|
||||
}
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
async fn run_benchmark(cpu_workers: Option<usize>, gpu_devices: Option<usize>, duration: u64) {
|
||||
async fn run_benchmark(
|
||||
cpu_workers: Option<usize>,
|
||||
gpu_devices: Option<usize>,
|
||||
gpu_batch_size: u32,
|
||||
cpu_batch_size: u64,
|
||||
duration: u64,
|
||||
) {
|
||||
let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get);
|
||||
|
||||
// Initialize GPU engine
|
||||
// Initialize GPU engine (no throttle for benchmark)
|
||||
let (gpu_engine, effective_gpu_devices) =
|
||||
match miner_service::resolve_gpu_configuration(gpu_devices, None) {
|
||||
match miner_service::resolve_gpu_configuration(gpu_devices, gpu_batch_size, 0) {
|
||||
Ok((engine, count)) => (engine, count),
|
||||
Err(e) => {
|
||||
eprintln!("❌ ERROR: {}", e);
|
||||
@@ -172,7 +216,7 @@ async fn run_benchmark(cpu_workers: Option<usize>, gpu_devices: Option<usize>, d
|
||||
|
||||
// Create CPU engine
|
||||
let cpu_engine: Option<Arc<dyn MinerEngine>> = if effective_cpu_workers > 0 {
|
||||
Some(Arc::new(engine_cpu::FastCpuEngine::new()))
|
||||
Some(Arc::new(engine_cpu::FastCpuEngine::new(cpu_batch_size)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -182,7 +226,7 @@ async fn run_benchmark(cpu_workers: Option<usize>, gpu_devices: Option<usize>, d
|
||||
|
||||
// Random header hash for benchmark
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
rand::rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::MAX; // High difficulty - no solutions expected
|
||||
|
||||
let ref_engine = cpu_engine.as_ref().or(gpu_engine.as_ref()).unwrap();
|
||||
@@ -224,7 +268,8 @@ async fn run_benchmark(cpu_workers: Option<usize>, gpu_devices: Option<usize>, d
|
||||
break;
|
||||
}
|
||||
|
||||
let result = engine.search_range(&ctx, worker_range.clone(), &cancel);
|
||||
let cancel_check = AtomicBoolCancelCheck(&cancel);
|
||||
let result = engine.search_range(&ctx, worker_range.clone(), &cancel_check);
|
||||
|
||||
match result {
|
||||
engine_cpu::EngineStatus::Found { hash_count, .. }
|
||||
|
||||
+139
-106
@@ -11,10 +11,10 @@
|
||||
pub mod quic;
|
||||
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
use engine_cpu::{EngineCandidate, EngineRange, MinerEngine};
|
||||
use pow_core::format_u512;
|
||||
use engine_cpu::{EngineCandidate, EngineRange, JobIdCancelCheck, MinerEngine};
|
||||
use pow_core::{format_hashrate, format_u512};
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
@@ -27,8 +27,12 @@ pub struct ServiceConfig {
|
||||
pub cpu_workers: Option<usize>,
|
||||
/// Number of GPU devices to use for mining (None = auto-detect)
|
||||
pub gpu_devices: Option<usize>,
|
||||
/// GPU cancel check interval in nonces (None = use default of 10,000)
|
||||
pub gpu_cancel_interval: Option<u32>,
|
||||
/// GPU batch size in nonces (u32 since GPU dispatch protocol uses 32-bit counts)
|
||||
pub gpu_batch_size: u32,
|
||||
/// CPU batch size in hashes
|
||||
pub cpu_batch_size: u64,
|
||||
/// GPU throttle delay in milliseconds between batches (0 = no throttle)
|
||||
pub gpu_throttle_ms: u64,
|
||||
}
|
||||
|
||||
/// Engine type for tracking metrics per compute type.
|
||||
@@ -38,17 +42,6 @@ pub enum EngineType {
|
||||
Gpu,
|
||||
}
|
||||
|
||||
impl Default for ServiceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
node_addr: "127.0.0.1:9833".parse().unwrap(),
|
||||
cpu_workers: None,
|
||||
gpu_devices: None,
|
||||
gpu_cancel_interval: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result from a single worker thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerResult {
|
||||
@@ -102,8 +95,6 @@ pub struct WorkerPool {
|
||||
job_senders: Vec<Sender<MiningJob>>,
|
||||
/// Receiver for collecting results from all workers
|
||||
result_rx: Receiver<WorkerResult>,
|
||||
/// Shared cancellation flag for all workers
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
/// Job ID counter - incremented on each new job to detect stale results
|
||||
current_job_id: Arc<AtomicU64>,
|
||||
/// Thread handles (for cleanup)
|
||||
@@ -124,7 +115,6 @@ impl WorkerPool {
|
||||
) -> Self {
|
||||
let total_workers = cpu_workers + gpu_devices;
|
||||
let (result_tx, result_rx) = bounded(total_workers * 64);
|
||||
let cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let current_job_id = Arc::new(AtomicU64::new(0));
|
||||
|
||||
let mut job_senders = Vec::with_capacity(total_workers);
|
||||
@@ -141,25 +131,20 @@ impl WorkerPool {
|
||||
if cpu_workers > 0 {
|
||||
if let Some(ref engine) = cpu_engine {
|
||||
for _ in 0..cpu_workers {
|
||||
let (job_tx, job_rx) = bounded::<MiningJob>(1);
|
||||
// Use bounded channel to prevent unbounded queue growth.
|
||||
// Capacity of 16 allows sender to queue jobs without realistic
|
||||
// risk of drops during normal operation. Worker drains to get
|
||||
// the latest job.
|
||||
let (job_tx, job_rx) = bounded::<MiningJob>(16);
|
||||
job_senders.push(job_tx);
|
||||
|
||||
let eng = engine.clone();
|
||||
let tx = result_tx.clone();
|
||||
let cancel = cancel_flag.clone();
|
||||
let job_id_counter = current_job_id.clone();
|
||||
let tid = thread_id;
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
worker_loop(
|
||||
tid,
|
||||
EngineType::Cpu,
|
||||
eng,
|
||||
job_rx,
|
||||
tx,
|
||||
cancel,
|
||||
job_id_counter,
|
||||
);
|
||||
worker_loop(tid, EngineType::Cpu, eng, job_rx, tx, job_id_counter);
|
||||
});
|
||||
handles.push(handle);
|
||||
thread_id += 1;
|
||||
@@ -171,25 +156,20 @@ impl WorkerPool {
|
||||
if gpu_devices > 0 {
|
||||
if let Some(ref engine) = gpu_engine {
|
||||
for _ in 0..gpu_devices {
|
||||
let (job_tx, job_rx) = bounded::<MiningJob>(1);
|
||||
// Use bounded channel to prevent unbounded queue growth.
|
||||
// Capacity of 16 allows sender to queue jobs without realistic
|
||||
// risk of drops during normal operation. Worker drains to get
|
||||
// the latest job.
|
||||
let (job_tx, job_rx) = bounded::<MiningJob>(16);
|
||||
job_senders.push(job_tx);
|
||||
|
||||
let eng = engine.clone();
|
||||
let tx = result_tx.clone();
|
||||
let cancel = cancel_flag.clone();
|
||||
let job_id_counter = current_job_id.clone();
|
||||
let tid = thread_id;
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
worker_loop(
|
||||
tid,
|
||||
EngineType::Gpu,
|
||||
eng,
|
||||
job_rx,
|
||||
tx,
|
||||
cancel,
|
||||
job_id_counter,
|
||||
);
|
||||
worker_loop(tid, EngineType::Gpu, eng, job_rx, tx, job_id_counter);
|
||||
});
|
||||
handles.push(handle);
|
||||
thread_id += 1;
|
||||
@@ -200,7 +180,6 @@ impl WorkerPool {
|
||||
Self {
|
||||
job_senders,
|
||||
result_rx,
|
||||
cancel_flag,
|
||||
current_job_id,
|
||||
_handles: handles,
|
||||
cpu_worker_count: cpu_workers,
|
||||
@@ -216,14 +195,7 @@ impl WorkerPool {
|
||||
// will be detected as stale when workers check the job ID before sending results
|
||||
let new_job_id = self.current_job_id.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
|
||||
// Cancel any running job
|
||||
self.cancel_flag.store(true, Ordering::SeqCst);
|
||||
|
||||
// Brief pause to let workers see the cancellation
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
|
||||
// Reset cancel flag for new job
|
||||
self.cancel_flag.store(false, Ordering::SeqCst);
|
||||
log::debug!("[JOB DISPATCH] Starting job {new_job_id}");
|
||||
|
||||
// Create job context (shared across all workers)
|
||||
let ctx = pow_core::JobContext::new(header_hash, difficulty);
|
||||
@@ -232,25 +204,31 @@ impl WorkerPool {
|
||||
job_id: new_job_id,
|
||||
};
|
||||
|
||||
// Dispatch job to all workers
|
||||
for tx in &self.job_senders {
|
||||
// Non-blocking send - if worker is still processing old job, it will
|
||||
// see the cancel flag and exit soon
|
||||
let _ = tx.try_send(job.clone());
|
||||
// Dispatch job to all workers using bounded channels (capacity 16).
|
||||
// Workers drain to get the latest job, so we just need room to queue.
|
||||
for (i, tx) in self.job_senders.iter().enumerate() {
|
||||
if let Err(e) = tx.try_send(job.clone()) {
|
||||
log::error!(
|
||||
"Failed to send job {new_job_id} to worker {i}: {e}. \
|
||||
Channel full - worker may be stuck or jobs arriving too fast."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Job dispatched to {} workers (job_id {})",
|
||||
self.job_senders.len(),
|
||||
new_job_id
|
||||
);
|
||||
let worker_count = self.job_senders.len();
|
||||
log::debug!("[JOB DISPATCH] Job {new_job_id} dispatched to {worker_count} workers");
|
||||
|
||||
new_job_id
|
||||
}
|
||||
|
||||
/// Cancel the current job.
|
||||
/// Cancel the current job by incrementing the job ID.
|
||||
/// Workers will detect the change and stop processing.
|
||||
///
|
||||
/// Note: This increments job_id by 1, and start_job() also increments by 1,
|
||||
/// so job IDs in logs may become non-contiguous after disconnects/cancellations.
|
||||
/// This is expected behavior - job IDs only need to be unique, not sequential.
|
||||
pub fn cancel(&self) {
|
||||
self.cancel_flag.store(true, Ordering::SeqCst);
|
||||
self.current_job_id.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Get the result receiver for collecting worker results.
|
||||
@@ -258,11 +236,6 @@ impl WorkerPool {
|
||||
&self.result_rx
|
||||
}
|
||||
|
||||
/// Get the shared cancel flag.
|
||||
pub fn cancel_flag(&self) -> &Arc<AtomicBool> {
|
||||
&self.cancel_flag
|
||||
}
|
||||
|
||||
/// Total number of workers.
|
||||
pub fn worker_count(&self) -> usize {
|
||||
self.job_senders.len()
|
||||
@@ -279,6 +252,26 @@ impl WorkerPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Log worker completion with hash rate info.
|
||||
fn log_worker_completion(
|
||||
type_str: &str,
|
||||
thread_id: usize,
|
||||
status: &str,
|
||||
hash_count: u64,
|
||||
elapsed: std::time::Duration,
|
||||
) {
|
||||
let hash_rate = if elapsed.as_secs_f64() > 0.0 {
|
||||
hash_count as f64 / elapsed.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
log::info!(
|
||||
"{type_str} worker {thread_id} {status}: {hash_count} hashes in {:.2}s ({})",
|
||||
elapsed.as_secs_f64(),
|
||||
format_hashrate(hash_rate)
|
||||
);
|
||||
}
|
||||
|
||||
/// Main loop for a persistent worker thread.
|
||||
fn worker_loop(
|
||||
thread_id: usize,
|
||||
@@ -286,7 +279,6 @@ fn worker_loop(
|
||||
engine: Arc<dyn MinerEngine>,
|
||||
job_rx: Receiver<MiningJob>,
|
||||
result_tx: Sender<WorkerResult>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
current_job_id: Arc<AtomicU64>,
|
||||
) {
|
||||
let type_str = match engine_type {
|
||||
@@ -298,8 +290,10 @@ fn worker_loop(
|
||||
|
||||
// Main job processing loop
|
||||
loop {
|
||||
// Wait for a job
|
||||
let job = match job_rx.recv() {
|
||||
log::debug!("[WORKER {type_str}-{thread_id}] Waiting for job...");
|
||||
|
||||
// Wait for a job (blocking)
|
||||
let mut job = match job_rx.recv() {
|
||||
Ok(job) => job,
|
||||
Err(_) => {
|
||||
// Channel closed, pool is shutting down
|
||||
@@ -308,35 +302,51 @@ fn worker_loop(
|
||||
}
|
||||
};
|
||||
|
||||
// Drain channel to get the latest job (in case multiple jobs queued while we were busy)
|
||||
let mut skipped = 0;
|
||||
while let Ok(newer_job) = job_rx.try_recv() {
|
||||
skipped += 1;
|
||||
job = newer_job;
|
||||
}
|
||||
if skipped > 0 {
|
||||
log::debug!("[WORKER {type_str}-{thread_id}] Drained {skipped} stale jobs from queue");
|
||||
}
|
||||
|
||||
// Capture the job's ID for later validation
|
||||
let job_id = job.job_id;
|
||||
|
||||
// Check if already cancelled before starting
|
||||
if cancel_flag.load(Ordering::Relaxed) {
|
||||
log::debug!("{type_str} worker {thread_id} skipping cancelled job");
|
||||
continue;
|
||||
}
|
||||
log::debug!("[WORKER {type_str}-{thread_id}] Received job {job_id}");
|
||||
|
||||
// Generate random starting nonce for this job
|
||||
let start = generate_random_nonce();
|
||||
let end = U512::MAX;
|
||||
|
||||
log::debug!(
|
||||
"{type_str} worker {thread_id} processing job {job_id}: range {} to {}",
|
||||
format_u512(start),
|
||||
format_u512(end)
|
||||
);
|
||||
log::debug!("[WORKER {type_str}-{thread_id}] Starting search for job {job_id}");
|
||||
|
||||
// Execute the search
|
||||
// Execute the search - both CPU and GPU use job ID comparison for cancellation
|
||||
let search_start = std::time::Instant::now();
|
||||
let range = EngineRange { start, end };
|
||||
let result = engine.search_range(&job.ctx, range, &cancel_flag);
|
||||
let cancel_check = JobIdCancelCheck {
|
||||
current_job_id: ¤t_job_id,
|
||||
my_job_id: job_id,
|
||||
};
|
||||
let result = engine.search_range(&job.ctx, range, &cancel_check);
|
||||
let search_elapsed = search_start.elapsed();
|
||||
|
||||
let result_type = match &result {
|
||||
engine_cpu::EngineStatus::Found { .. } => "FOUND",
|
||||
engine_cpu::EngineStatus::Exhausted { .. } => "EXHAUSTED",
|
||||
engine_cpu::EngineStatus::Cancelled { .. } => "CANCELLED",
|
||||
engine_cpu::EngineStatus::Running { .. } => "RUNNING",
|
||||
};
|
||||
log::debug!(
|
||||
"[WORKER {type_str}-{thread_id}] Job {job_id} search finished: {} in {:.2}s",
|
||||
result_type,
|
||||
search_elapsed.as_secs_f64()
|
||||
);
|
||||
|
||||
// Check if job ID changed during search - if so, this result is stale
|
||||
let actual_job_id = current_job_id.load(Ordering::SeqCst);
|
||||
if actual_job_id != job_id {
|
||||
log::debug!(
|
||||
"⏰ {type_str} worker {thread_id} discarding stale result (job {job_id} != current {actual_job_id})"
|
||||
);
|
||||
// Still send hash count for metrics, but without the candidate
|
||||
let hash_count = match result {
|
||||
engine_cpu::EngineStatus::Found { hash_count, .. } => hash_count,
|
||||
@@ -344,6 +354,13 @@ fn worker_loop(
|
||||
engine_cpu::EngineStatus::Cancelled { hash_count } => hash_count,
|
||||
engine_cpu::EngineStatus::Running { .. } => 0,
|
||||
};
|
||||
log_worker_completion(
|
||||
type_str,
|
||||
thread_id,
|
||||
"interrupted by new block",
|
||||
hash_count,
|
||||
search_elapsed,
|
||||
);
|
||||
let _ = result_tx.try_send(WorkerResult {
|
||||
thread_id,
|
||||
engine_type,
|
||||
@@ -363,18 +380,24 @@ fn worker_loop(
|
||||
..
|
||||
} => {
|
||||
log::info!(
|
||||
"🎉 {type_str} worker {thread_id} found solution! Nonce: {}, Hash: {} (job {job_id})",
|
||||
"🎯 {type_str} worker {thread_id} found solution! Nonce: {}, Hash: {} (job {job_id})",
|
||||
format_u512(nonce),
|
||||
format_u512(hash),
|
||||
);
|
||||
(Some(MiningCandidate { nonce, work, hash }), hash_count)
|
||||
}
|
||||
engine_cpu::EngineStatus::Exhausted { hash_count } => {
|
||||
log::debug!("{type_str} worker {thread_id} exhausted range ({hash_count} hashes)");
|
||||
log_worker_completion(
|
||||
type_str,
|
||||
thread_id,
|
||||
"exhausted range",
|
||||
hash_count,
|
||||
search_elapsed,
|
||||
);
|
||||
(None, hash_count)
|
||||
}
|
||||
engine_cpu::EngineStatus::Cancelled { hash_count } => {
|
||||
log::debug!("{type_str} worker {thread_id} cancelled ({hash_count} hashes)");
|
||||
log_worker_completion(type_str, thread_id, "new block", hash_count, search_elapsed);
|
||||
(None, hash_count)
|
||||
}
|
||||
engine_cpu::EngineStatus::Running { .. } => {
|
||||
@@ -405,7 +428,8 @@ fn worker_loop(
|
||||
/// Resolve GPU configuration and initialize the engine.
|
||||
pub fn resolve_gpu_configuration(
|
||||
requested_devices: Option<usize>,
|
||||
cancel_interval: Option<u32>,
|
||||
batch_size: u32,
|
||||
throttle_ms: u64,
|
||||
) -> anyhow::Result<(Option<Arc<dyn MinerEngine>>, usize)> {
|
||||
// Explicit 0 means no GPU
|
||||
if requested_devices == Some(0) {
|
||||
@@ -413,17 +437,14 @@ pub fn resolve_gpu_configuration(
|
||||
}
|
||||
|
||||
// Try to initialize GPU engine
|
||||
let engine = match cancel_interval {
|
||||
Some(interval) => engine_gpu::GpuEngine::try_with_cancel_interval(interval),
|
||||
None => engine_gpu::GpuEngine::try_new(),
|
||||
};
|
||||
let engine = engine_gpu::GpuEngine::try_new(batch_size, throttle_ms);
|
||||
let engine = match engine {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
if requested_devices.is_some() {
|
||||
anyhow::bail!("Failed to initialize GPU engine: {}", e);
|
||||
}
|
||||
log::info!("No GPU available: {}", e);
|
||||
log::info!("No GPU available: {e}");
|
||||
return Ok((None, 0));
|
||||
}
|
||||
};
|
||||
@@ -443,7 +464,7 @@ pub fn resolve_gpu_configuration(
|
||||
return Ok((None, 0));
|
||||
}
|
||||
None => {
|
||||
log::info!("Auto-detected {} GPU device(s)", available);
|
||||
log::info!("Auto-detected {available} GPU device(s)");
|
||||
available
|
||||
}
|
||||
};
|
||||
@@ -457,8 +478,11 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
let effective_cpus = num_cpus::get().max(1);
|
||||
|
||||
// Resolve GPU configuration
|
||||
let (gpu_engine, gpu_devices) =
|
||||
resolve_gpu_configuration(config.gpu_devices, config.gpu_cancel_interval)?;
|
||||
let (gpu_engine, gpu_devices) = resolve_gpu_configuration(
|
||||
config.gpu_devices,
|
||||
config.gpu_batch_size,
|
||||
config.gpu_throttle_ms,
|
||||
)?;
|
||||
|
||||
// Resolve CPU workers
|
||||
let cpu_workers = config.cpu_workers.unwrap_or_else(|| {
|
||||
@@ -478,7 +502,9 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
|
||||
// Create CPU engine
|
||||
let cpu_engine: Option<Arc<dyn MinerEngine>> = if cpu_workers > 0 {
|
||||
Some(Arc::new(engine_cpu::FastCpuEngine::new()))
|
||||
Some(Arc::new(engine_cpu::FastCpuEngine::new(
|
||||
config.cpu_batch_size,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -491,19 +517,26 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
if let Some(ref engine) = cpu_engine {
|
||||
log::info!("🖥️ CPU engine: {}", engine.name());
|
||||
let name = engine.name();
|
||||
log::info!("🖥️ CPU engine: {name}");
|
||||
}
|
||||
if let Some(ref engine) = gpu_engine {
|
||||
log::info!("🎮 GPU engine: {}", engine.name());
|
||||
let name = engine.name();
|
||||
log::info!("🎮 GPU engine: {name}");
|
||||
if config.gpu_throttle_ms > 0 {
|
||||
log::info!(
|
||||
"⏳ GPU throttle: {}ms between batches",
|
||||
config.gpu_throttle_ms
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"⛏️ Mining service ready with {} total workers",
|
||||
cpu_workers + gpu_devices
|
||||
);
|
||||
let total_workers = cpu_workers + gpu_devices;
|
||||
log::info!("⛏️ Mining service ready with {total_workers} total workers");
|
||||
|
||||
// Connect to node and start mining
|
||||
log::info!("🌐 Connecting to node at {}", config.node_addr);
|
||||
let node_addr = config.node_addr;
|
||||
log::info!("🌐 Connecting to node at {node_addr}");
|
||||
quic::connect_and_mine(
|
||||
config.node_addr,
|
||||
cpu_engine,
|
||||
|
||||
@@ -227,9 +227,9 @@ async fn handle_connection(
|
||||
match msg_result {
|
||||
Ok(MinerMessage::NewJob(request)) => {
|
||||
log::info!(
|
||||
"⛏️ Received job: id={}, hash={}...",
|
||||
"⛏️ Received job: id={}, hash=0x{}",
|
||||
request.job_id,
|
||||
&request.mining_hash[..8]
|
||||
request.mining_hash
|
||||
);
|
||||
|
||||
// Parse header hash
|
||||
@@ -253,10 +253,10 @@ async fn handle_connection(
|
||||
};
|
||||
|
||||
// Parse difficulty
|
||||
let difficulty = match U512::from_dec_str(&request.distance_threshold) {
|
||||
let difficulty = match U512::from_dec_str(&request.difficulty) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
log::warn!("Invalid difficulty in request");
|
||||
log::warn!("Invalid difficulty in request: parse error");
|
||||
let result = MiningResult {
|
||||
status: ApiResponseStatus::Failed,
|
||||
job_id: request.job_id,
|
||||
@@ -272,6 +272,23 @@ async fn handle_connection(
|
||||
}
|
||||
};
|
||||
|
||||
// Reject zero difficulty to prevent division-by-zero panic in JobContext::new
|
||||
if difficulty.is_zero() {
|
||||
log::warn!("Invalid difficulty in request: zero is not allowed");
|
||||
let result = MiningResult {
|
||||
status: ApiResponseStatus::Failed,
|
||||
job_id: request.job_id,
|
||||
nonce: None,
|
||||
work: None,
|
||||
hash_count: 0,
|
||||
elapsed_time: 0.0,
|
||||
miner_id: None,
|
||||
};
|
||||
let msg = MinerMessage::JobResult(result);
|
||||
send_message_checked(&connection, &mut send, &msg).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset state for new job
|
||||
cpu_hashes = 0;
|
||||
gpu_hashes = 0;
|
||||
|
||||
@@ -106,33 +106,28 @@ impl TelemetryHandle {
|
||||
}
|
||||
|
||||
/// Emit a fully custom payload (must include `"msg"`).
|
||||
pub async fn emit_payload(&self, payload: Value, verbosity: Option<u8>) {
|
||||
///
|
||||
/// This is intentionally lossy: if the internal channel is full, the message is dropped
|
||||
/// silently to avoid blocking mining paths or causing unbounded memory growth.
|
||||
pub fn emit_payload(&self, payload: Value, verbosity: Option<u8>) {
|
||||
if self.senders.is_empty() {
|
||||
return;
|
||||
}
|
||||
let msg = EnvelopeMsg { payload, verbosity };
|
||||
for tx in self.senders.iter() {
|
||||
// Drop on full; do not block mining paths. This is intentionally lossy under backpressure.
|
||||
if tx.try_send(msg.clone()).is_err() {
|
||||
// Best-effort: if try_send failed due to full buffer, attempt bounded send with small timeout.
|
||||
// Avoid blocking indefinitely.
|
||||
let tx = tx.clone();
|
||||
let msg = msg.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tx.send(msg).await;
|
||||
});
|
||||
}
|
||||
// Drop on full; do not block mining paths.
|
||||
let _ = tx.try_send(msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a `system.connected` message using configuration defaults and an optional link.
|
||||
pub async fn emit_system_connected(&self, link: Option<&TelemetryNodeLink>) {
|
||||
pub fn emit_system_connected(&self, link: Option<&TelemetryNodeLink>) {
|
||||
let payload = build_system_connected_payload(&self.config, link);
|
||||
self.emit_payload(payload, Some(self.verbosity)).await;
|
||||
self.emit_payload(payload, Some(self.verbosity));
|
||||
}
|
||||
|
||||
/// Emit a `system.interval` message with lightweight health stats and an optional link hint.
|
||||
pub async fn emit_system_interval(&self, interval: &SystemInterval, link_hint: Option<&str>) {
|
||||
pub fn emit_system_interval(&self, interval: &SystemInterval, link_hint: Option<&str>) {
|
||||
let mut payload = json!({
|
||||
"msg": "system.interval",
|
||||
"uptime_ms": interval.uptime_ms,
|
||||
@@ -169,7 +164,7 @@ impl TelemetryHandle {
|
||||
.insert("linked_node_hint".to_string(), json!(hint));
|
||||
}
|
||||
|
||||
self.emit_payload(payload, Some(self.verbosity)).await;
|
||||
self.emit_payload(payload, Some(self.verbosity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,9 +458,7 @@ mod tests {
|
||||
let cfg = TelemetryConfig::default();
|
||||
let handle = start(cfg);
|
||||
assert!(!handle.is_enabled());
|
||||
handle
|
||||
.emit_system_interval(&SystemInterval::default(), None)
|
||||
.await;
|
||||
handle.emit_system_connected(None).await;
|
||||
handle.emit_system_interval(&SystemInterval::default(), None);
|
||||
handle.emit_system_connected(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@ pub struct JobContext {
|
||||
}
|
||||
|
||||
impl JobContext {
|
||||
/// Build a new context from header and difficulty
|
||||
/// Build a new context from header and difficulty.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `difficulty` is zero (division by zero in target calculation).
|
||||
/// Callers must validate that difficulty is non-zero before calling this function.
|
||||
pub fn new(header: [u8; 32], difficulty: U512) -> Self {
|
||||
// In Bitcoin-style PoW, target = max_target / difficulty
|
||||
let max_target = U512::MAX;
|
||||
@@ -216,4 +221,14 @@ mod tests {
|
||||
// With impossible difficulty, should not find solution
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "division by zero")]
|
||||
fn test_zero_difficulty_panics() {
|
||||
// Zero difficulty causes division by zero in target calculation.
|
||||
// Callers MUST validate difficulty > 0 before calling JobContext::new.
|
||||
let header = [7u8; 32];
|
||||
let zero_difficulty = U512::zero();
|
||||
let _ctx = JobContext::new(header, zero_difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
use primitive_types::U512;
|
||||
use quantus_miner::*;
|
||||
use resonance_miner_api::*;
|
||||
use warp::test::request;
|
||||
use warp::Filter; // Import shared API types
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mine_endpoint() {
|
||||
let state = MiningState::new();
|
||||
let state_clone = state.clone();
|
||||
let state_filter = warp::any().map(move || state_clone.clone());
|
||||
|
||||
let mine_route = warp::post()
|
||||
.and(warp::path("mine"))
|
||||
.and(warp::body::json())
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_mine_request);
|
||||
|
||||
// Test valid request
|
||||
let valid_request = MiningRequest {
|
||||
job_id: "test".to_string(),
|
||||
mining_hash: "a".repeat(64),
|
||||
distance_threshold: "1000".to_string(),
|
||||
nonce_start: "0".repeat(128),
|
||||
nonce_end: "1".repeat(128),
|
||||
};
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&valid_request)
|
||||
.reply(&mine_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::Accepted);
|
||||
assert_eq!(body.job_id, "test");
|
||||
|
||||
// Test duplicate job ID
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&valid_request)
|
||||
.reply(&mine_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 409);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::Error);
|
||||
assert!(body.message.is_some());
|
||||
assert!(body.message.unwrap().contains("Job already exists"));
|
||||
|
||||
// Test invalid request
|
||||
let invalid_request = MiningRequest {
|
||||
job_id: "".to_string(), // Empty job ID
|
||||
mining_hash: "a".repeat(64),
|
||||
distance_threshold: "1000".to_string(),
|
||||
nonce_start: "0".repeat(128),
|
||||
nonce_end: "1".repeat(128),
|
||||
};
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&invalid_request)
|
||||
.reply(&mine_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 400);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_endpoint() {
|
||||
let state = MiningState::new();
|
||||
let state_clone = state.clone();
|
||||
let state_filter = warp::any().map(move || state_clone.clone());
|
||||
|
||||
// First create a job
|
||||
let job = MiningJob::new([0; 32], U512::from(1000), U512::from(0), U512::from(1000));
|
||||
state.add_job("test".to_string(), job).await.unwrap();
|
||||
|
||||
let result_route = warp::get()
|
||||
.and(warp::path("result"))
|
||||
.and(warp::path::param())
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_result_request);
|
||||
|
||||
// Test existing job
|
||||
let resp = request()
|
||||
.method("GET")
|
||||
.path("/result/test")
|
||||
.reply(&result_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: MiningResult = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::Running);
|
||||
assert_eq!(body.job_id, "test");
|
||||
|
||||
// Test non-existent job
|
||||
let resp = request()
|
||||
.method("GET")
|
||||
.path("/result/nonexistent")
|
||||
.reply(&result_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 404);
|
||||
let body: MiningResult = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::NotFound);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cancel_endpoint() {
|
||||
let state = MiningState::new();
|
||||
let state_clone = state.clone();
|
||||
let state_filter = warp::any().map(move || state_clone.clone());
|
||||
|
||||
// First create a job
|
||||
let job = MiningJob::new([0; 32], U512::from(1000), U512::from(0), U512::from(1000));
|
||||
state.add_job("test".to_string(), job).await.unwrap();
|
||||
|
||||
let cancel_route = warp::post()
|
||||
.and(warp::path("cancel"))
|
||||
.and(warp::path::param())
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_cancel_request);
|
||||
|
||||
// Test cancel existing job
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/cancel/test")
|
||||
.reply(&cancel_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::Cancelled);
|
||||
assert_eq!(body.job_id, "test");
|
||||
|
||||
// Test cancel non-existent job
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/cancel/nonexistent")
|
||||
.reply(&cancel_route)
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), 404);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::NotFound);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_access() {
|
||||
let state = MiningState::new();
|
||||
let state_clone = state.clone();
|
||||
let state_filter = warp::any().map(move || state_clone.clone());
|
||||
|
||||
// Create multiple jobs concurrently
|
||||
let mut handles = vec![];
|
||||
for i in 0..10 {
|
||||
let state = state.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let job = MiningJob::new([0; 32], U512::from(1000), U512::from(0), U512::from(1000));
|
||||
state.add_job(format!("test{}", i), job).await
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all jobs to be created
|
||||
for handle in handles {
|
||||
assert!(handle.await.unwrap().is_ok());
|
||||
}
|
||||
|
||||
// Verify all jobs exist
|
||||
for i in 0..10 {
|
||||
assert!(state.get_job(&format!("test{}", i)).await.is_some());
|
||||
}
|
||||
|
||||
// Test concurrent result checks
|
||||
let result_route = warp::get()
|
||||
.and(warp::path("result"))
|
||||
.and(warp::path::param())
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_result_request);
|
||||
|
||||
let mut result_handles = vec![];
|
||||
for i in 0..10 {
|
||||
let route = result_route.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
request()
|
||||
.method("GET")
|
||||
.path(&format!("/result/test{}", i))
|
||||
.reply(&route)
|
||||
.await
|
||||
});
|
||||
result_handles.push(handle);
|
||||
}
|
||||
|
||||
// Verify all result checks succeed
|
||||
for handle in result_handles {
|
||||
let resp = handle.await.unwrap();
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: MiningResult = serde_json::from_slice(resp.body()).unwrap();
|
||||
assert_eq!(body.status, ApiResponseStatus::Running);
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
// Verifying that our API spec is in sync with the implementation
|
||||
// Manually generated from the OpenAPI spec but it should still flag when we get out of sync and need to update.
|
||||
|
||||
use primitive_types::U512;
|
||||
use quantus_miner::*;
|
||||
use resonance_miner_api::*; // Use the shared API types
|
||||
use serde_json::json;
|
||||
use warp::test::request;
|
||||
use warp::Filter; // For sending custom/invalid JSON
|
||||
|
||||
// Helper function to setup the routes with a fresh state for each test
|
||||
fn setup_routes() -> (
|
||||
MiningState,
|
||||
impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone,
|
||||
) {
|
||||
let state = MiningState::new();
|
||||
let state_clone = state.clone();
|
||||
let state_filter = warp::any().map(move || state_clone.clone());
|
||||
|
||||
let mine_route = warp::post()
|
||||
.and(warp::path("mine"))
|
||||
.and(warp::body::json::<MiningRequest>()) // Expect valid MiningRequest
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_mine_request);
|
||||
|
||||
let result_route = warp::get()
|
||||
.and(warp::path("result"))
|
||||
.and(warp::path::param::<String>())
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_result_request);
|
||||
|
||||
let cancel_route = warp::post()
|
||||
.and(warp::path("cancel"))
|
||||
.and(warp::path::param::<String>())
|
||||
.and(state_filter.clone())
|
||||
.and_then(handle_cancel_request);
|
||||
|
||||
let routes = mine_route.or(result_route).or(cancel_route);
|
||||
(state, routes)
|
||||
}
|
||||
|
||||
// --- /mine Endpoint Tests ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_mine_valid_request() {
|
||||
let (_state, routes) = setup_routes();
|
||||
let valid_req = MiningRequest {
|
||||
job_id: "job-valid-1".to_string(),
|
||||
mining_hash: "a".repeat(64),
|
||||
distance_threshold: "12345".to_string(),
|
||||
nonce_start: "0".repeat(128),
|
||||
nonce_end: "f".repeat(128),
|
||||
};
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&valid_req)
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 200 OK, response matches MiningResponseAccepted
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
200,
|
||||
"Spec requires 200 OK for valid /mine request"
|
||||
);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body())
|
||||
.expect("Valid response should deserialize to MiningResponse");
|
||||
assert_eq!(
|
||||
body.status,
|
||||
ApiResponseStatus::Accepted,
|
||||
"Spec requires status 'accepted'"
|
||||
);
|
||||
assert_eq!(body.job_id, valid_req.job_id);
|
||||
assert!(
|
||||
body.message.is_none(),
|
||||
"Spec shows no message field for success"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_mine_duplicate_job_id() {
|
||||
let (state, routes) = setup_routes();
|
||||
let valid_req = MiningRequest {
|
||||
job_id: "job-duplicate-1".to_string(),
|
||||
mining_hash: "b".repeat(64),
|
||||
distance_threshold: "54321".to_string(),
|
||||
nonce_start: "1".repeat(128),
|
||||
nonce_end: "e".repeat(128),
|
||||
};
|
||||
|
||||
// Add the job first
|
||||
let job = MiningJob::new(
|
||||
hex::decode(valid_req.mining_hash.clone())
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
U512::from_dec_str(&valid_req.distance_threshold).unwrap(),
|
||||
U512::from_str_radix(&valid_req.nonce_start, 16).unwrap(),
|
||||
U512::from_str_radix(&valid_req.nonce_end, 16).unwrap(),
|
||||
);
|
||||
state.add_job(valid_req.job_id.clone(), job).await.unwrap();
|
||||
|
||||
// Try to submit again
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&valid_req)
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 409 Conflict, response matches MiningResponseError
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
409,
|
||||
"Spec requires 409 Conflict for duplicate job ID"
|
||||
);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body())
|
||||
.expect("Valid error response should deserialize to MiningResponse");
|
||||
assert_eq!(
|
||||
body.status,
|
||||
ApiResponseStatus::Error,
|
||||
"Spec requires status 'error'"
|
||||
);
|
||||
assert!(
|
||||
body.message.is_some(),
|
||||
"Spec requires message field for error"
|
||||
);
|
||||
assert!(body.message.unwrap().contains("Job already exists"));
|
||||
assert_eq!(body.job_id, valid_req.job_id);
|
||||
}
|
||||
|
||||
// Test cases for invalid formats (hash length, nonce length, difficulty non-numeric)
|
||||
// These should ideally result in 400 Bad Request
|
||||
#[tokio::test]
|
||||
async fn spec_mine_invalid_hash_format() {
|
||||
let (_state, routes) = setup_routes();
|
||||
let invalid_req = json!({
|
||||
"job_id": "job-invalid-hash",
|
||||
"mining_hash": "a".repeat(63), // Too short
|
||||
"distance_threshold": "1000",
|
||||
"nonce_start": "0".repeat(128),
|
||||
"nonce_end": "f".repeat(128)
|
||||
});
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&invalid_req)
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 400 Bad Request, response matches MiningResponseError
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
400,
|
||||
"Spec requires 400 Bad Request for invalid hash format"
|
||||
);
|
||||
let body: MiningResponse =
|
||||
serde_json::from_slice(resp.body()).expect("Error response should deserialize");
|
||||
assert_eq!(body.status, ApiResponseStatus::Error);
|
||||
assert!(body.message.is_some());
|
||||
assert!(body
|
||||
.message
|
||||
.unwrap()
|
||||
.contains("mining_hash must be 64 hex characters"));
|
||||
}
|
||||
|
||||
// Add similar tests for invalid nonce_start, nonce_end, difficulty format...
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_mine_missing_required_field() {
|
||||
let (_state, routes) = setup_routes();
|
||||
// Missing 'difficulty'
|
||||
let invalid_req = json!({
|
||||
"job_id": "job-missing-field",
|
||||
"mining_hash": "c".repeat(64),
|
||||
"nonce_start": "0".repeat(128),
|
||||
"nonce_end": "f".repeat(128)
|
||||
});
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path("/mine")
|
||||
.json(&invalid_req)
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Warp's default JSON body deserialization handles missing fields
|
||||
// It should return 400 Bad Request based on warp::body::json()
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
400,
|
||||
"Spec requires 400 Bad Request for missing required field"
|
||||
);
|
||||
// The body might be a plain text error from Warp, not necessarily our JSON structure
|
||||
// We can check if the body indicates a deserialization error
|
||||
let body_bytes = resp.body();
|
||||
let body_string = String::from_utf8_lossy(body_bytes);
|
||||
assert!(
|
||||
body_string.contains("Failed to deserialize") || body_string.contains("missing field"),
|
||||
"Response body should indicate deserialization failure"
|
||||
);
|
||||
}
|
||||
|
||||
// --- /result/{job_id} Endpoint Tests ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_result_job_running() {
|
||||
let (state, routes) = setup_routes();
|
||||
let job_id = "job-running-1".to_string();
|
||||
// Add a job
|
||||
let job = MiningJob::new([1u8; 32], U512::from(1000), U512::from(0), U512::from(1000));
|
||||
state.add_job(job_id.clone(), job).await.unwrap();
|
||||
|
||||
let resp = request()
|
||||
.method("GET")
|
||||
.path(&format!("/result/{}", job_id))
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 200 OK, response matches MiningResult with status 'running'
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: MiningResult =
|
||||
serde_json::from_slice(resp.body()).expect("Should deserialize to MiningResult");
|
||||
assert_eq!(body.status, ApiResponseStatus::Running);
|
||||
assert_eq!(body.job_id, job_id);
|
||||
assert!(body.nonce.is_none()); // Should be none initially
|
||||
assert!(body.work.is_none()); // No work yet
|
||||
// hash_count and elapsed_time should be present
|
||||
// assert_eq!(body.hash_count, 0); // Initial state
|
||||
// assert!(body.elapsed_time >= 0.0);
|
||||
}
|
||||
|
||||
// Add tests for spec_result_job_completed, spec_result_job_failed (need to manipulate state)
|
||||
// Need to carefully simulate a completed/failed state or run miner loop briefly
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_result_job_not_found() {
|
||||
let (_state, routes) = setup_routes();
|
||||
let job_id = "job-not-found-1";
|
||||
|
||||
let resp = request()
|
||||
.method("GET")
|
||||
.path(&format!("/result/{}", job_id))
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 404 Not Found, response matches MiningResultNotFound
|
||||
assert_eq!(resp.status(), 404);
|
||||
let body: MiningResult = serde_json::from_slice(resp.body())
|
||||
.expect("Should deserialize to MiningResult (NotFound variant)");
|
||||
assert_eq!(body.status, ApiResponseStatus::NotFound);
|
||||
assert_eq!(body.job_id, job_id);
|
||||
assert!(body.nonce.is_none());
|
||||
assert!(body.work.is_none());
|
||||
assert_eq!(body.hash_count, 0);
|
||||
assert_eq!(body.elapsed_time, 0.0);
|
||||
}
|
||||
|
||||
// --- /cancel/{job_id} Endpoint Tests ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_cancel_existing_job() {
|
||||
let (state, routes) = setup_routes();
|
||||
let job_id = "job-cancel-1".to_string();
|
||||
let job = MiningJob::new([2u8; 32], U512::from(1000), U512::from(0), U512::from(1000));
|
||||
state.add_job(job_id.clone(), job).await.unwrap();
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path(&format!("/cancel/{}", job_id))
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 200 OK, response matches MiningResponseCancelled
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: MiningResponse =
|
||||
serde_json::from_slice(resp.body()).expect("Should deserialize to MiningResponse");
|
||||
assert_eq!(body.status, ApiResponseStatus::Cancelled);
|
||||
assert_eq!(body.job_id, job_id);
|
||||
assert!(body.message.is_none());
|
||||
|
||||
// Verify job is actually cancelled
|
||||
let job = state.get_job(&job_id).await.unwrap();
|
||||
assert_eq!(job.status, JobStatus::Cancelled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spec_cancel_non_existent_job() {
|
||||
let (_state, routes) = setup_routes();
|
||||
let job_id = "job-cancel-not-found-1";
|
||||
|
||||
let resp = request()
|
||||
.method("POST")
|
||||
.path(&format!("/cancel/{}", job_id))
|
||||
.reply(&routes)
|
||||
.await;
|
||||
|
||||
// Spec: 404 Not Found, response matches MiningResponseNotFound
|
||||
assert_eq!(resp.status(), 404);
|
||||
let body: MiningResponse = serde_json::from_slice(resp.body())
|
||||
.expect("Should deserialize to MiningResponse (NotFound variant)");
|
||||
assert_eq!(body.status, ApiResponseStatus::NotFound);
|
||||
assert_eq!(body.job_id, job_id);
|
||||
assert!(body.message.is_some());
|
||||
}
|
||||
|
||||
// Removed placeholder test
|
||||
Reference in New Issue
Block a user