Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8ecc6f03c | ||
|
|
91170c6aea | ||
|
|
4131b60ebb | ||
|
|
de3ffa9fb9 | ||
|
|
ad46f200c0 | ||
|
|
d14ebdf65c | ||
|
|
6834dd2dff | ||
|
|
af5564796b | ||
|
|
ffc5b8f83c | ||
|
|
3a694ecee1 | ||
|
|
b9e080e20c | ||
|
|
3705fa96e7 | ||
|
|
c3e7d70a75 | ||
|
|
0b0731b869 | ||
|
|
3b5f759fbd | ||
|
|
06933d5745 | ||
|
|
96853cad23 | ||
|
|
010eff6e71 | ||
|
|
4e7280cb0f | ||
|
|
c2773cb80d | ||
|
|
b79a9368e0 | ||
|
|
f0aa971326 | ||
|
|
23c617bda9 | ||
|
|
2a36eeb3c3 | ||
|
|
a548f74f7d | ||
|
|
97ec1350de | ||
|
|
ac87758996 |
@@ -45,28 +45,17 @@ jobs:
|
||||
name: 🛠️ Build & Test
|
||||
needs: fast-checks
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
features: ["", "gpu"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- name: compile (${{ matrix.features || 'cpu-only' }})
|
||||
- name: compile (gpu)
|
||||
run: |
|
||||
if [ -n "${{ matrix.features }}" ]; then
|
||||
cargo build --locked --workspace --features ${{ matrix.features }}
|
||||
else
|
||||
cargo build --locked --workspace
|
||||
fi
|
||||
cargo build --locked --workspace
|
||||
timeout-minutes: 90
|
||||
- name: test (${{ matrix.features || 'cpu-only' }})
|
||||
- name: test (gpu)
|
||||
run: |
|
||||
if [ -n "${{ matrix.features }}" ]; then
|
||||
cargo test --locked --workspace --features ${{ matrix.features }}
|
||||
else
|
||||
cargo test --locked --workspace
|
||||
fi
|
||||
cargo test --locked --workspace
|
||||
timeout-minutes: 15
|
||||
|
||||
analysis:
|
||||
@@ -95,7 +84,7 @@ jobs:
|
||||
- name: setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- name: build benchmark binary
|
||||
run: cargo build -p miner-cli --release --features gpu
|
||||
run: cargo build -p miner-cli --release
|
||||
timeout-minutes: 60
|
||||
- name: run cpu benchmark
|
||||
run: ./target/release/quantus-miner benchmark --cpu-workers 2 --duration 5
|
||||
|
||||
@@ -71,45 +71,21 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-linux-x86_64
|
||||
features: ""
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-linux-x86_64-gpu
|
||||
features: "gpu"
|
||||
# Windows builds
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
binary_name: quantus-miner.exe
|
||||
asset_name: quantus-miner-windows-x86_64.exe
|
||||
features: ""
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
binary_name: quantus-miner.exe
|
||||
asset_name: quantus-miner-windows-x86_64-gpu.exe
|
||||
features: "gpu"
|
||||
# macOS builds (Intel)
|
||||
- os: macos-15-intel
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-macos-x86_64
|
||||
features: ""
|
||||
- os: macos-15-intel
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-macos-x86_64-gpu
|
||||
features: "gpu"
|
||||
# macOS builds (Apple Silicon)
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-macos-aarch64
|
||||
features: ""
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-macos-aarch64-gpu
|
||||
features: "gpu"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
Generated
+7
-7
@@ -557,7 +557,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "engine-cpu"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"criterion",
|
||||
@@ -571,7 +571,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "engine-gpu"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
@@ -1458,7 +1458,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "metrics"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -1488,7 +1488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-cli"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"engine-cpu",
|
||||
@@ -1504,7 +1504,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-service"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam-channel",
|
||||
@@ -1530,7 +1530,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-telemetry"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -2076,7 +2076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pow-core"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hex",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ resolver = "2"
|
||||
edition = "2021"
|
||||
authors = ["Quantus Network"]
|
||||
description = "Quantus External Miner Workspace"
|
||||
version = "2.0.2"
|
||||
version = "2.1.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
|
||||
@@ -9,7 +9,7 @@ High-performance external mining service for Quantus Network with support for CP
|
||||
cargo build -p miner-cli --release
|
||||
|
||||
# With GPU support (recommended)
|
||||
cargo build -p miner-cli --features gpu --release
|
||||
cargo build -p miner-cli --release
|
||||
```
|
||||
|
||||
The binary will be available at `target/release/quantus-miner`.
|
||||
@@ -18,16 +18,16 @@ The binary will be available at `target/release/quantus-miner`.
|
||||
|
||||
```bash
|
||||
# CPU-only mining (default: auto-detected CPU cores)
|
||||
./target/release/quantus-miner --cpu-workers 4
|
||||
./target/release/quantus-miner serve --cpu-workers 4
|
||||
|
||||
# GPU-only mining (requires --features gpu build)
|
||||
./target/release/quantus-miner --gpu-workers 1
|
||||
# GPU-only mining
|
||||
./target/release/quantus-miner serve --gpu-devices 1
|
||||
|
||||
# Hybrid CPU+GPU mining
|
||||
./target/release/quantus-miner --cpu-workers 4 --gpu-workers 1
|
||||
./target/release/quantus-miner serve --cpu-workers 4 --gpu-devices 1
|
||||
|
||||
# Custom port and metrics
|
||||
./target/release/quantus-miner --cpu-workers 2 --port 8000 --metrics-port 9900
|
||||
./target/release/quantus-miner serve --cpu-workers 2 --port 8000 --metrics-port 9900
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -35,7 +35,7 @@ The binary will be available at `target/release/quantus-miner`.
|
||||
| Argument | Environment Variable | Description | Default |
|
||||
|----------|---------------------|-------------|---------|
|
||||
| `--cpu-workers <N>` | `MINER_CPU_WORKERS` | Number of CPU worker threads | Auto-detect |
|
||||
| `--gpu-workers <N>` | `MINER_GPU_WORKERS` | Number of GPU worker threads | 0 |
|
||||
| `--gpu-devices <N>` | `MINER_GPU_DEVICES` | Number of GPU devices | 0 |
|
||||
| `--port <PORT>` | `MINER_PORT` | HTTP API port | 9833 |
|
||||
| `--metrics-port <PORT>` | `MINER_METRICS_PORT` | Prometheus metrics port | Disabled |
|
||||
|
||||
@@ -51,7 +51,7 @@ GPU support uses WGPU for cross-platform acceleration:
|
||||
|
||||
**Build with GPU support:**
|
||||
```bash
|
||||
cargo build -p miner-cli --features gpu --release
|
||||
cargo build -p miner-cli --release
|
||||
```
|
||||
|
||||
**Platform requirements:**
|
||||
@@ -69,21 +69,21 @@ cargo build -p miner-cli --features gpu --release
|
||||
|
||||
```bash
|
||||
# CPU mining with 8 workers
|
||||
./target/release/quantus-miner --cpu-workers 8
|
||||
./target/release/quantus-miner serve --cpu-workers 8
|
||||
|
||||
# Pure GPU mining
|
||||
./target/release/quantus-miner --gpu-workers 1
|
||||
./target/release/quantus-miner serve --gpu-devices 1
|
||||
|
||||
# Hybrid mining: 4 CPU + 1 GPU workers
|
||||
./target/release/quantus-miner --cpu-workers 4 --gpu-workers 1
|
||||
./target/release/quantus-miner serve --cpu-workers 4 --gpu-devices 1
|
||||
|
||||
# With verbose logging
|
||||
RUST_LOG=debug ./target/release/quantus-miner --cpu-workers 2 --gpu-workers 1
|
||||
RUST_LOG=debug ./target/release/quantus-miner serve --cpu-workers 2 --gpu-devices 1
|
||||
|
||||
# Production setup with metrics
|
||||
./target/release/quantus-miner \
|
||||
./target/release/quantus-miner serve \
|
||||
--cpu-workers 6 \
|
||||
--gpu-workers 1 \
|
||||
--gpu-devices 1 \
|
||||
--port 9833 \
|
||||
--metrics-port 9900
|
||||
```
|
||||
@@ -107,7 +107,7 @@ docker run -d -p 9833:9833 -p 9900:9900 \
|
||||
|
||||
# Build from source
|
||||
docker build -t quantus-miner .
|
||||
docker run -d -p 9833:9833 quantus-miner --cpu-workers 4
|
||||
docker run -d -p 9833:9833 quantus-miner serve --cpu-workers 4
|
||||
```
|
||||
|
||||
## Benchmarking
|
||||
@@ -117,8 +117,8 @@ docker run -d -p 9833:9833 quantus-miner --cpu-workers 4
|
||||
./target/release/quantus-miner benchmark --cpu-workers 8 --duration 30
|
||||
|
||||
# Benchmark GPU performance
|
||||
./target/release/quantus-miner benchmark --gpu-workers 1 --duration 30
|
||||
./target/release/quantus-miner benchmark --gpu-devices 1 --duration 30
|
||||
|
||||
# Benchmark hybrid performance
|
||||
./target/release/quantus-miner benchmark --cpu-workers 4 --gpu-workers 1 --duration 30
|
||||
./target/release/quantus-miner benchmark --cpu-workers 4 --gpu-devices 1 --duration 30
|
||||
```
|
||||
|
||||
@@ -5,10 +5,11 @@ use pow_core::JobContext;
|
||||
use primitive_types::U512;
|
||||
use rand::RngCore;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
fn bench_cpu_vs_gpu_small(c: &mut Criterion) {
|
||||
let cpu_engine = FastCpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new(Duration::from_millis(3000));
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
// Small range: 10K nonces - reasonable for benchmarking
|
||||
@@ -58,7 +59,7 @@ 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 gpu_engine = GpuEngine::new(Duration::from_millis(3000));
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
// Medium range: 100K nonces
|
||||
@@ -108,7 +109,7 @@ 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 gpu_engine = GpuEngine::new(Duration::from_millis(3000));
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
// Large range: 1M nonces - where GPU should really shine
|
||||
@@ -158,7 +159,7 @@ 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 gpu_engine = GpuEngine::new(Duration::from_millis(3000));
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
// Range where we expect to find solutions quickly
|
||||
@@ -208,7 +209,7 @@ 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 gpu_engine = GpuEngine::new(Duration::from_millis(3000));
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
// Fixed time benchmark - see how many hashes we can do in 1 second
|
||||
@@ -257,7 +258,7 @@ 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::new(Duration::from_millis(3000));
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
let mut group = c.benchmark_group("gpu_batch_sizes");
|
||||
|
||||
@@ -2,6 +2,7 @@ use engine_cpu::{BaselineCpuEngine, EngineStatus, MinerEngine, Range};
|
||||
use engine_gpu::GpuEngine;
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging
|
||||
@@ -25,7 +26,7 @@ fn main() {
|
||||
|
||||
// 3. Verify with GPU engine
|
||||
log::info!("Initializing GPU engine...");
|
||||
let gpu_engine = GpuEngine::new();
|
||||
let gpu_engine = GpuEngine::new(Duration::from_millis(3000));
|
||||
|
||||
// Search a small range around the valid nonce
|
||||
let gpu_range = Range {
|
||||
|
||||
+370
-255
@@ -7,50 +7,158 @@ use pow_core::JobContext;
|
||||
use primitive_types::U512;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Represents a single GPU device context.
|
||||
struct GpuContext {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
bind_group: wgpu::BindGroup,
|
||||
|
||||
// Cached vendor configuration
|
||||
optimal_workgroups: u32,
|
||||
// Reusable buffers
|
||||
|
||||
// Dynamic batch size tuning
|
||||
batch_size: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GpuResources {
|
||||
header_buffer: wgpu::Buffer,
|
||||
target_buffer: wgpu::Buffer,
|
||||
start_nonce_buffer: wgpu::Buffer,
|
||||
results_buffer: wgpu::Buffer,
|
||||
dispatch_config_buffer: wgpu::Buffer,
|
||||
staging_buffer: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
pub struct GpuEngine {
|
||||
contexts: Vec<Arc<GpuContext>>,
|
||||
device_counter: AtomicUsize,
|
||||
target_batch_duration: Duration,
|
||||
}
|
||||
|
||||
// Thread-local storage for consistent GPU device assignment per worker thread
|
||||
thread_local! {
|
||||
static ASSIGNED_GPU_DEVICE: RefCell<Option<usize>> = const { RefCell::new(None) };
|
||||
static WORKER_RESOURCES: RefCell<Option<GpuResources>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
impl GpuContext {
|
||||
fn create_resources(&self) -> GpuResources {
|
||||
let bind_group_layout = self.pipeline.get_bind_group_layout(0);
|
||||
|
||||
// Header: 8 u32s
|
||||
let header_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Header Buffer"),
|
||||
size: 32,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Target: 16 u32s
|
||||
let target_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Target Buffer"),
|
||||
size: 64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Start Nonce: 16 u32s
|
||||
let start_nonce_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Start Nonce Buffer"),
|
||||
size: 64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Results: [flag (1), nonce (16), hash (16)] = 33 u32s
|
||||
let results_size = (1 + 16 + 16) * 4;
|
||||
let results_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Results Buffer"),
|
||||
size: results_size,
|
||||
usage: wgpu::BufferUsages::STORAGE
|
||||
| wgpu::BufferUsages::COPY_SRC
|
||||
| wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Dispatch config: [total_threads, nonces_per_thread, workgroups, threads_per_workgroup] = 4 u32s
|
||||
let dispatch_config_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Dispatch Config Buffer"),
|
||||
size: 16,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Staging Buffer"),
|
||||
size: results_size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Mining Bind Group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: results_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: header_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: start_nonce_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: target_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: dispatch_config_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
GpuResources {
|
||||
header_buffer,
|
||||
target_buffer,
|
||||
start_nonce_buffer,
|
||||
results_buffer,
|
||||
dispatch_config_buffer,
|
||||
staging_buffer,
|
||||
bind_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GpuEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
// Default to 3 seconds if not specified via new()
|
||||
Self::new(Duration::from_secs(3))
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuEngine {
|
||||
pub fn new() -> Self {
|
||||
block_on(Self::init()).expect("Failed to initialize GPU engine")
|
||||
pub fn new(target_batch_duration: Duration) -> Self {
|
||||
block_on(Self::init(target_batch_duration)).expect("Failed to initialize GPU engine")
|
||||
}
|
||||
|
||||
async fn init() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
/// Try to initialize the GPU engine, returning an error if initialization fails.
|
||||
pub fn try_new(target_batch_duration: Duration) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
block_on(Self::init(target_batch_duration))
|
||||
}
|
||||
|
||||
async fn init(target_batch_duration: Duration) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
log::info!("Initializing WGPU...");
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
@@ -123,87 +231,7 @@ impl GpuEngine {
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
||||
|
||||
// Pre-allocate buffers
|
||||
// Header: 8 u32s
|
||||
let header_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Header Buffer"),
|
||||
size: 32,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Target: 16 u32s
|
||||
let target_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Target Buffer"),
|
||||
size: 64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Start Nonce: 16 u32s
|
||||
let start_nonce_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Start Nonce Buffer"),
|
||||
size: 64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Results: [flag (1), nonce (16), hash (16)] = 33 u32s
|
||||
let results_size = (1 + 16 + 16) * 4;
|
||||
let results_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Results Buffer"),
|
||||
size: results_size,
|
||||
usage: wgpu::BufferUsages::STORAGE
|
||||
| wgpu::BufferUsages::COPY_SRC
|
||||
| wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Dispatch config: [total_threads, nonces_per_thread, workgroups, threads_per_workgroup] = 4 u32s
|
||||
let dispatch_config_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Dispatch Config Buffer"),
|
||||
size: 16,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Staging Buffer"),
|
||||
size: results_size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Mining Bind Group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: results_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: header_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: start_nonce_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: target_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: dispatch_config_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
log::debug!(target: "gpu_engine", "Buffers and bind group initialized for adapter {}", i);
|
||||
log::debug!(target: "gpu_engine", "Pipeline initialized for adapter {}", i);
|
||||
|
||||
// Calculate vendor-specific configuration once during initialization
|
||||
let optimal_workgroups = Self::get_vendor_specific_dispatch(&info, &device);
|
||||
@@ -212,14 +240,8 @@ impl GpuEngine {
|
||||
device,
|
||||
queue,
|
||||
pipeline,
|
||||
bind_group,
|
||||
optimal_workgroups,
|
||||
header_buffer,
|
||||
target_buffer,
|
||||
start_nonce_buffer,
|
||||
results_buffer,
|
||||
dispatch_config_buffer,
|
||||
staging_buffer,
|
||||
batch_size: AtomicU64::new(1_000_000),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -264,6 +286,7 @@ impl GpuEngine {
|
||||
Ok(Self {
|
||||
contexts,
|
||||
device_counter: AtomicUsize::new(0),
|
||||
target_batch_duration,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -271,6 +294,14 @@ impl GpuEngine {
|
||||
pub fn device_count(&self) -> usize {
|
||||
self.contexts.len()
|
||||
}
|
||||
|
||||
/// Explicitly clear thread-local GPU resources.
|
||||
/// Call this before thread exit to avoid TLS destruction order issues with wgpu.
|
||||
pub fn clear_worker_resources() {
|
||||
WORKER_RESOURCES.with(|resources| {
|
||||
*resources.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl MinerEngine for GpuEngine {
|
||||
@@ -321,13 +352,28 @@ impl MinerEngine for GpuEngine {
|
||||
});
|
||||
|
||||
let gpu_ctx = &self.contexts[device_index];
|
||||
log::debug!(
|
||||
"GPU device {} processing range {}..={} (inclusive)",
|
||||
let initial_batch_size = gpu_ctx.batch_size.load(Ordering::Relaxed);
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} search started: range {}..={} (inclusive), initial batch size: {}",
|
||||
device_index,
|
||||
range.start,
|
||||
range.end
|
||||
range.end,
|
||||
initial_batch_size
|
||||
);
|
||||
|
||||
// Ensure resources are initialized for this thread
|
||||
WORKER_RESOURCES.with(|resources_cell| {
|
||||
let mut resources = resources_cell.borrow_mut();
|
||||
if resources.is_none() {
|
||||
*resources = Some(gpu_ctx.create_resources());
|
||||
}
|
||||
});
|
||||
|
||||
// Clone resources to use outside the closure (they are cheap handles)
|
||||
let resources = WORKER_RESOURCES
|
||||
.with(|resources_cell| resources_cell.borrow().as_ref().unwrap().clone());
|
||||
|
||||
// Pre-convert header and target once (not per range)
|
||||
let mut header_u32s = [0u32; 8];
|
||||
for (i, item) in header_u32s.iter_mut().enumerate() {
|
||||
@@ -335,7 +381,7 @@ impl MinerEngine for GpuEngine {
|
||||
*item = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
}
|
||||
gpu_ctx.queue.write_buffer(
|
||||
&gpu_ctx.header_buffer,
|
||||
&resources.header_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&header_u32s),
|
||||
);
|
||||
@@ -347,179 +393,248 @@ impl MinerEngine for GpuEngine {
|
||||
target_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
}
|
||||
gpu_ctx.queue.write_buffer(
|
||||
&gpu_ctx.target_buffer,
|
||||
&resources.target_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&target_u32s),
|
||||
);
|
||||
|
||||
// Length of the inclusive nonce range
|
||||
let total_range_size = (range.end - range.start + 1).as_u64();
|
||||
if total_range_size == 0 {
|
||||
return EngineStatus::Exhausted { hash_count: 0 };
|
||||
}
|
||||
// --- Batch Processing Loop ---
|
||||
let mut current_nonce = range.start;
|
||||
let mut total_hashes_processed = 0u64;
|
||||
|
||||
// Thread configuration for a single dispatch over this range.
|
||||
let threads_per_workgroup = 256u32; // Must match shader @workgroup_size(256)
|
||||
let limits = gpu_ctx.device.limits();
|
||||
let max_workgroups = limits.max_compute_workgroups_per_dimension;
|
||||
// Loop until range is covered or cancelled
|
||||
while current_nonce <= range.end {
|
||||
// Check for cancellation between batches to ensure responsiveness
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} search cancelled: processed {} hashes",
|
||||
device_index,
|
||||
total_hashes_processed
|
||||
);
|
||||
return EngineStatus::Cancelled {
|
||||
hash_count: total_hashes_processed,
|
||||
};
|
||||
}
|
||||
|
||||
// Vendor hint, clamped by hardware limits.
|
||||
let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups);
|
||||
let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64;
|
||||
// Determine dynamic batch size for this iteration
|
||||
let batch_size = gpu_ctx.batch_size.load(Ordering::Relaxed);
|
||||
|
||||
// Choose a logical thread budget: enough threads to fill the GPU, but no more than
|
||||
// the range length (spawning more threads than nonces is wasteful).
|
||||
let mut logical_threads = total_range_size.min(hinted_threads);
|
||||
if logical_threads == 0 {
|
||||
logical_threads = 1;
|
||||
}
|
||||
|
||||
// Round logical_threads up to a multiple of workgroup size so we have full workgroups.
|
||||
let mut num_workgroups = (logical_threads as u32).div_ceil(threads_per_workgroup);
|
||||
if num_workgroups == 0 {
|
||||
num_workgroups = 1;
|
||||
}
|
||||
let total_threads = (num_workgroups * threads_per_workgroup) as u64;
|
||||
|
||||
// Derive how many nonces each logical thread should process so that the entire
|
||||
// range is covered in a single dispatch.
|
||||
let nonces_per_thread = total_range_size.div_ceil(total_threads).max(1) as u32;
|
||||
|
||||
let total_threads_u32 = total_threads as u32;
|
||||
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU dispatch configuration: total_range={} nonces, workgroups={}, threads={}, nonces_per_thread={}",
|
||||
total_range_size,
|
||||
num_workgroups,
|
||||
total_threads_u32,
|
||||
nonces_per_thread
|
||||
);
|
||||
|
||||
// We'll process the full range in a single dispatch.
|
||||
let hash_count = total_range_size;
|
||||
|
||||
// Eliminate intermediate syncs - only sync at end or when solution found
|
||||
const RESULTS_SIZE: usize = (1 + 16 + 16) * 4;
|
||||
const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE];
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
let device_id = "gpu-0";
|
||||
metrics::set_gpu_batch_size(device_id, total_range_size as f64);
|
||||
metrics::set_gpu_workgroups(device_id, num_workgroups as f64);
|
||||
}
|
||||
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
log::debug!(target: "gpu_engine", "GPU {} cancelled before dispatch.", device_index);
|
||||
return EngineStatus::Cancelled { hash_count: 0 };
|
||||
}
|
||||
|
||||
// Dispatch configuration for this range:
|
||||
// [total_threads, nonces_per_thread, total_nonces, threads_per_workgroup]
|
||||
let total_nonces_u32 = total_range_size.min(u32::MAX as u64) as u32;
|
||||
let dispatch_config = [
|
||||
total_threads_u32,
|
||||
nonces_per_thread,
|
||||
total_nonces_u32,
|
||||
threads_per_workgroup,
|
||||
];
|
||||
gpu_ctx.queue.write_buffer(
|
||||
&gpu_ctx.dispatch_config_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&dispatch_config),
|
||||
);
|
||||
|
||||
// Starting nonce for this range.
|
||||
let start_nonce_bytes = range.start.to_little_endian();
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&gpu_ctx.start_nonce_buffer, 0, &start_nonce_bytes);
|
||||
|
||||
// Reset results buffer to detect solutions from this dispatch.
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&gpu_ctx.results_buffer, 0, &ZEROS);
|
||||
|
||||
let total_start = std::time::Instant::now();
|
||||
|
||||
// Create command buffer for this range
|
||||
let mut encoder = gpu_ctx
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
{
|
||||
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: None,
|
||||
timestamp_writes: None,
|
||||
});
|
||||
cpass.set_pipeline(&gpu_ctx.pipeline);
|
||||
cpass.set_bind_group(0, &gpu_ctx.bind_group, &[]);
|
||||
cpass.dispatch_workgroups(num_workgroups, 1, 1);
|
||||
}
|
||||
encoder.copy_buffer_to_buffer(
|
||||
&gpu_ctx.results_buffer,
|
||||
0,
|
||||
&gpu_ctx.staging_buffer,
|
||||
0,
|
||||
RESULTS_SIZE as u64,
|
||||
);
|
||||
|
||||
// Submit and wait for completion
|
||||
gpu_ctx.queue.submit(Some(encoder.finish()));
|
||||
|
||||
let buffer_slice = gpu_ctx.staging_buffer.slice(..);
|
||||
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
|
||||
let _ = gpu_ctx.device.poll(wgpu::PollType::Wait {
|
||||
submission_index: None,
|
||||
timeout: None,
|
||||
});
|
||||
|
||||
let data = buffer_slice.get_mapped_range();
|
||||
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
|
||||
|
||||
if result_u32s[0] != 0 {
|
||||
// Solution found!
|
||||
let nonce_u32s = &result_u32s[1..17];
|
||||
let hash_u32s = &result_u32s[17..33];
|
||||
|
||||
let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s));
|
||||
let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s));
|
||||
let work = nonce.to_big_endian();
|
||||
|
||||
log::info!(
|
||||
"GPU {} found solution! Nonce: {}, Hash: {:x}",
|
||||
log::debug!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} starting batch: nonce {}..{}, batch size: {}",
|
||||
device_index,
|
||||
nonce,
|
||||
hash
|
||||
current_nonce,
|
||||
current_nonce.saturating_add(U512::from(batch_size)).saturating_sub(U512::from(1u64)).min(range.end),
|
||||
batch_size
|
||||
);
|
||||
|
||||
drop(data);
|
||||
gpu_ctx.staging_buffer.unmap();
|
||||
// Calculate inclusive end for this batch
|
||||
let batch_end = current_nonce
|
||||
.saturating_add(U512::from(batch_size))
|
||||
.saturating_sub(U512::from(1u64));
|
||||
|
||||
return EngineStatus::Found {
|
||||
candidate: Candidate { nonce, work, hash },
|
||||
// Approximate: we assume a uniform distribution within the dispatch
|
||||
// and report that we processed the entire range.
|
||||
hash_count,
|
||||
origin: FoundOrigin::GpuG1,
|
||||
let actual_end = if batch_end > range.end {
|
||||
range.end
|
||||
} else {
|
||||
batch_end
|
||||
};
|
||||
|
||||
// Range size for this specific batch
|
||||
let range_size_u512 = actual_end
|
||||
.saturating_sub(current_nonce)
|
||||
.saturating_add(U512::from(1u64));
|
||||
let range_size = range_size_u512.as_u64(); // batch_size is u64, so this is safe
|
||||
|
||||
if range_size == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Dispatch Logic (same as before but using range_size) ---
|
||||
let threads_per_workgroup = 256u32; // Must match shader
|
||||
let limits = gpu_ctx.device.limits();
|
||||
let max_workgroups = limits.max_compute_workgroups_per_dimension;
|
||||
|
||||
let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups);
|
||||
let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64;
|
||||
|
||||
let mut logical_threads = range_size.min(hinted_threads);
|
||||
if logical_threads == 0 {
|
||||
logical_threads = 1;
|
||||
}
|
||||
|
||||
let mut num_workgroups = (logical_threads as u32).div_ceil(threads_per_workgroup);
|
||||
if num_workgroups == 0 {
|
||||
num_workgroups = 1;
|
||||
}
|
||||
let total_threads = (num_workgroups * threads_per_workgroup) as u64;
|
||||
let nonces_per_thread = range_size.div_ceil(total_threads).max(1) as u32;
|
||||
let total_threads_u32 = total_threads as u32;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
let device_id = "gpu-0"; // Simplified; ideally propagate device ID to metrics
|
||||
metrics::set_gpu_batch_size(device_id, range_size as f64);
|
||||
metrics::set_gpu_workgroups(device_id, num_workgroups as f64);
|
||||
}
|
||||
|
||||
// Dispatch config: [total_threads, nonces_per_thread, total_nonces, threads_per_workgroup]
|
||||
let total_nonces_u32 = range_size.min(u32::MAX as u64) as u32;
|
||||
let dispatch_config = [
|
||||
total_threads_u32,
|
||||
nonces_per_thread,
|
||||
total_nonces_u32,
|
||||
threads_per_workgroup,
|
||||
];
|
||||
gpu_ctx.queue.write_buffer(
|
||||
&resources.dispatch_config_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&dispatch_config),
|
||||
);
|
||||
|
||||
// Starting nonce for this batch
|
||||
let start_nonce_bytes = current_nonce.to_little_endian();
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&resources.start_nonce_buffer, 0, &start_nonce_bytes);
|
||||
|
||||
// Reset results
|
||||
const RESULTS_SIZE: usize = (1 + 16 + 16) * 4;
|
||||
const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE];
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&resources.results_buffer, 0, &ZEROS);
|
||||
|
||||
let batch_start_time = std::time::Instant::now();
|
||||
|
||||
// Encode and submit
|
||||
let mut encoder = gpu_ctx
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
{
|
||||
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: None,
|
||||
timestamp_writes: None,
|
||||
});
|
||||
cpass.set_pipeline(&gpu_ctx.pipeline);
|
||||
cpass.set_bind_group(0, &resources.bind_group, &[]);
|
||||
cpass.dispatch_workgroups(num_workgroups, 1, 1);
|
||||
}
|
||||
encoder.copy_buffer_to_buffer(
|
||||
&resources.results_buffer,
|
||||
0,
|
||||
&resources.staging_buffer,
|
||||
0,
|
||||
RESULTS_SIZE as u64,
|
||||
);
|
||||
|
||||
gpu_ctx.queue.submit(Some(encoder.finish()));
|
||||
|
||||
let buffer_slice = resources.staging_buffer.slice(..);
|
||||
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
|
||||
let _ = gpu_ctx.device.poll(wgpu::PollType::Wait {
|
||||
submission_index: None,
|
||||
timeout: None,
|
||||
});
|
||||
|
||||
let data = buffer_slice.get_mapped_range();
|
||||
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
|
||||
|
||||
if result_u32s[0] != 0 {
|
||||
// Solution found!
|
||||
let nonce_u32s = &result_u32s[1..17];
|
||||
let hash_u32s = &result_u32s[17..33];
|
||||
let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s));
|
||||
let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s));
|
||||
let work = nonce.to_big_endian();
|
||||
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} found solution! Nonce: {}, Hash: {:x}",
|
||||
device_index,
|
||||
nonce,
|
||||
hash
|
||||
);
|
||||
|
||||
drop(data);
|
||||
resources.staging_buffer.unmap();
|
||||
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} search ended (solution found): processed {} hashes",
|
||||
device_index,
|
||||
total_hashes_processed + range_size
|
||||
);
|
||||
|
||||
// Even if found, return total hashes processed including this batch
|
||||
return EngineStatus::Found {
|
||||
candidate: Candidate { nonce, work, hash },
|
||||
hash_count: total_hashes_processed + range_size,
|
||||
origin: FoundOrigin::GpuG1,
|
||||
};
|
||||
}
|
||||
|
||||
drop(data);
|
||||
resources.staging_buffer.unmap();
|
||||
|
||||
// --- Auto-tuning Logic ---
|
||||
let batch_elapsed = batch_start_time.elapsed();
|
||||
let batch_elapsed_secs = batch_elapsed.as_secs_f64();
|
||||
|
||||
// Update stats
|
||||
total_hashes_processed += range_size;
|
||||
|
||||
if batch_elapsed_secs > 0.0 {
|
||||
let target_secs = self.target_batch_duration.as_secs_f64();
|
||||
|
||||
// Calculate ideal batch size: (current_size / elapsed) * target
|
||||
let hashrate = range_size as f64 / batch_elapsed_secs;
|
||||
let ideal_batch_size = (hashrate * target_secs) as u64;
|
||||
|
||||
let old_size = gpu_ctx.batch_size.load(Ordering::Relaxed);
|
||||
|
||||
// Smooth update: 50% old, 50% new to avoid oscillation
|
||||
// Also clamp to avoid drastic changes (0.5x to 2.0x)
|
||||
let min_clamp = old_size / 2;
|
||||
let max_clamp = old_size * 2;
|
||||
|
||||
// Ensure we don't go below a reasonable minimum (e.g. 100k) to avoid overhead dominance
|
||||
let clamped_ideal = ideal_batch_size.clamp(min_clamp, max_clamp).max(100_000);
|
||||
|
||||
// Simple moving average
|
||||
let new_size = (old_size + clamped_ideal) / 2;
|
||||
|
||||
if new_size != old_size {
|
||||
gpu_ctx.batch_size.store(new_size, Ordering::Relaxed);
|
||||
log::debug!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} auto-tune: batch {} -> {} (elapsed: {:.3}s, target: {:.1}s, rate: {:.1} MH/s)",
|
||||
device_index,
|
||||
old_size,
|
||||
new_size,
|
||||
batch_elapsed_secs,
|
||||
target_secs,
|
||||
hashrate / 1_000_000.0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next batch
|
||||
current_nonce = actual_end.saturating_add(U512::from(1u64));
|
||||
}
|
||||
|
||||
drop(data);
|
||||
gpu_ctx.staging_buffer.unmap();
|
||||
|
||||
let total_time = total_start.elapsed();
|
||||
// Finished entire range without solution
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU finished range. Total time: {:.2}ms, Batches: 1, Hashes: {}, Performance: {:.0} hashes/sec",
|
||||
total_time.as_secs_f64() * 1000.0,
|
||||
hash_count,
|
||||
hash_count as f64 / total_time.as_secs_f64()
|
||||
"GPU {} search completed: processed {} hashes, final batch size: {}",
|
||||
device_index,
|
||||
total_hashes_processed,
|
||||
gpu_ctx.batch_size.load(Ordering::Relaxed)
|
||||
);
|
||||
|
||||
log::debug!(target: "gpu_engine", "GPU {} finished range with no solution.", device_index);
|
||||
EngineStatus::Exhausted { hash_count }
|
||||
EngineStatus::Exhausted {
|
||||
hash_count: total_hashes_processed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@ edition.workspace = true
|
||||
publish = false
|
||||
description = "CLI binary to run the Quantus External Miner service"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
gpu = ["miner-service/gpu"]
|
||||
|
||||
[dependencies]
|
||||
miner-service = { path = "../miner-service" }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
|
||||
@@ -21,9 +21,9 @@ enum Command {
|
||||
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
|
||||
cpu_workers: Option<usize>,
|
||||
|
||||
/// Number of GPU worker threads to use for mining (None = auto-detect)
|
||||
#[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")]
|
||||
gpu_workers: Option<usize>,
|
||||
/// Number of GPU devices to use for mining (None = auto-detect)
|
||||
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
|
||||
gpu_devices: Option<usize>,
|
||||
|
||||
/// Optional Prometheus metrics exporter port; if omitted, metrics are disabled
|
||||
#[arg(long, env = "MINER_METRICS_PORT")]
|
||||
@@ -60,6 +60,10 @@ enum Command {
|
||||
#[arg(long = "manip-throttle-cap", env = "MINER_MANIP_THROTTLE_CAP")]
|
||||
manip_throttle_cap: Option<u64>,
|
||||
|
||||
/// Target duration for GPU mining batches in milliseconds (default: 3000)
|
||||
#[arg(long = "gpu-batch-duration-ms", env = "MINER_GPU_BATCH_DURATION_MS")]
|
||||
gpu_batch_duration_ms: Option<u64>,
|
||||
|
||||
/// Telemetry endpoints (repeat --telemetry-endpoint or comma-separated)
|
||||
#[arg(long = "telemetry-endpoint", env = "MINER_TELEMETRY_ENDPOINTS", value_delimiter = ',', num_args = 0.., value_name = "URL")]
|
||||
telemetry_endpoints: Option<Vec<String>>,
|
||||
@@ -109,9 +113,9 @@ enum Command {
|
||||
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
|
||||
cpu_workers: Option<usize>,
|
||||
|
||||
/// Number of GPU workers to use for benchmark
|
||||
#[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")]
|
||||
gpu_workers: Option<usize>,
|
||||
/// Number of GPU devices to use for benchmark
|
||||
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
|
||||
gpu_devices: Option<usize>,
|
||||
|
||||
/// Benchmark duration in seconds (default: 10)
|
||||
#[arg(short, long, default_value_t = 10)]
|
||||
@@ -139,7 +143,7 @@ async fn main() {
|
||||
match args.command.unwrap_or(Command::Serve {
|
||||
port: 9833,
|
||||
cpu_workers: None,
|
||||
gpu_workers: None,
|
||||
gpu_devices: None,
|
||||
metrics_port: None,
|
||||
verbose: false,
|
||||
progress_interval_ms: None,
|
||||
@@ -148,6 +152,7 @@ async fn main() {
|
||||
manip_base_delay_ns: None,
|
||||
manip_step_batch: None,
|
||||
manip_throttle_cap: None,
|
||||
gpu_batch_duration_ms: None,
|
||||
telemetry_endpoints: None,
|
||||
telemetry_enabled: None,
|
||||
telemetry_verbosity: None,
|
||||
@@ -162,7 +167,7 @@ async fn main() {
|
||||
Command::Serve {
|
||||
port,
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
metrics_port,
|
||||
verbose,
|
||||
progress_interval_ms,
|
||||
@@ -171,6 +176,7 @@ async fn main() {
|
||||
manip_base_delay_ns,
|
||||
manip_step_batch,
|
||||
manip_throttle_cap,
|
||||
gpu_batch_duration_ms,
|
||||
telemetry_endpoints,
|
||||
telemetry_enabled,
|
||||
telemetry_verbosity,
|
||||
@@ -185,7 +191,7 @@ async fn main() {
|
||||
run_serve_command(
|
||||
port,
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
metrics_port,
|
||||
verbose,
|
||||
progress_interval_ms,
|
||||
@@ -194,6 +200,7 @@ async fn main() {
|
||||
manip_base_delay_ns,
|
||||
manip_step_batch,
|
||||
manip_throttle_cap,
|
||||
gpu_batch_duration_ms,
|
||||
telemetry_endpoints,
|
||||
telemetry_enabled,
|
||||
telemetry_verbosity,
|
||||
@@ -209,11 +216,11 @@ async fn main() {
|
||||
}
|
||||
Command::Benchmark {
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
duration,
|
||||
verbose,
|
||||
} => {
|
||||
run_benchmark_command(cpu_workers, gpu_workers, duration, verbose).await;
|
||||
run_benchmark_command(cpu_workers, gpu_devices, duration, verbose).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +229,7 @@ async fn main() {
|
||||
async fn run_serve_command(
|
||||
port: u16,
|
||||
cpu_workers: Option<usize>,
|
||||
gpu_workers: Option<usize>,
|
||||
gpu_devices: Option<usize>,
|
||||
metrics_port: Option<u16>,
|
||||
verbose: bool,
|
||||
progress_interval_ms: Option<u64>,
|
||||
@@ -231,6 +238,7 @@ async fn run_serve_command(
|
||||
manip_base_delay_ns: Option<u64>,
|
||||
manip_step_batch: Option<u64>,
|
||||
manip_throttle_cap: Option<u64>,
|
||||
gpu_batch_duration_ms: Option<u64>,
|
||||
telemetry_endpoints: Option<Vec<String>>,
|
||||
telemetry_enabled: Option<bool>,
|
||||
telemetry_verbosity: Option<u8>,
|
||||
@@ -294,7 +302,7 @@ async fn run_serve_command(
|
||||
let config = ServiceConfig {
|
||||
port,
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
metrics_port,
|
||||
progress_interval_ms,
|
||||
chunk_size,
|
||||
@@ -302,6 +310,7 @@ async fn run_serve_command(
|
||||
manip_base_delay_ns,
|
||||
manip_step_batch,
|
||||
manip_throttle_cap,
|
||||
gpu_batch_duration_ms,
|
||||
};
|
||||
log::info!("Effective config: {config}");
|
||||
|
||||
@@ -313,7 +322,7 @@ async fn run_serve_command(
|
||||
|
||||
async fn run_benchmark_command(
|
||||
cpu_workers: Option<usize>,
|
||||
gpu_workers: Option<usize>,
|
||||
gpu_devices: Option<usize>,
|
||||
duration: u64,
|
||||
verbose: bool,
|
||||
) {
|
||||
@@ -329,18 +338,31 @@ async fn run_benchmark_command(
|
||||
env_logger::init();
|
||||
|
||||
let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get);
|
||||
let effective_gpu_workers = gpu_workers.unwrap_or(0);
|
||||
let total_workers = effective_cpu_workers + effective_gpu_workers;
|
||||
|
||||
// Initialize GPU engine and determine effective GPU devices
|
||||
let (gpu_engine, effective_gpu_devices) =
|
||||
match miner_service::resolve_gpu_configuration(gpu_devices, None) {
|
||||
Ok((engine, count)) => (engine, count),
|
||||
Err(e) => {
|
||||
eprintln!("❌ ERROR: {}", e);
|
||||
eprintln!(" Please check your --gpu-devices setting or GPU hardware.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let total_workers = effective_cpu_workers + effective_gpu_devices;
|
||||
|
||||
println!("🚀 Quantus Miner Benchmark");
|
||||
println!("==========================");
|
||||
println!("Configuration:");
|
||||
println!(
|
||||
"Configuration: {} CPU workers, {} GPU workers",
|
||||
effective_cpu_workers, effective_gpu_workers
|
||||
" CPU Workers: {} (Available Cores: {})",
|
||||
effective_cpu_workers,
|
||||
num_cpus::get()
|
||||
);
|
||||
println!(" GPU Devices: {}", effective_gpu_devices);
|
||||
println!("Duration: {} seconds", duration);
|
||||
println!("Total Workers: {}", total_workers);
|
||||
println!("Available CPUs: {}", num_cpus::get());
|
||||
|
||||
// Create engines based on configuration
|
||||
let cpu_engine = if effective_cpu_workers > 0 {
|
||||
@@ -349,46 +371,29 @@ async fn run_benchmark_command(
|
||||
None
|
||||
};
|
||||
|
||||
let gpu_engine = if effective_gpu_workers > 0 {
|
||||
#[cfg(feature = "gpu")]
|
||||
{
|
||||
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
|
||||
}
|
||||
#[cfg(not(feature = "gpu"))]
|
||||
{
|
||||
eprintln!("Error: GPU workers requested but this binary was built without GPU support");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// For benchmark, we'll use CPU engine as primary for simplicity
|
||||
let engine = cpu_engine
|
||||
.or(gpu_engine)
|
||||
.expect("At least one engine must be available");
|
||||
|
||||
if total_workers == 0 {
|
||||
eprintln!("Error: No workers specified");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("Workers: {}", total_workers);
|
||||
println!();
|
||||
|
||||
// Run benchmark
|
||||
let cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let benchmark_start = Instant::now();
|
||||
|
||||
// Create a large range that should take the full duration
|
||||
let benchmark_range = EngineRange {
|
||||
start: U512::from(0u64),
|
||||
end: U512::from(100_000_000u64), // 100M nonces - should be plenty
|
||||
end: U512::from(100_000_000u64), // 100M nonces
|
||||
};
|
||||
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::MAX; // High difficulty - no solutions expected
|
||||
let ctx = engine.prepare_context(header, difficulty);
|
||||
|
||||
// We need a context from *some* engine. They should be compatible (same job).
|
||||
// Just pick one.
|
||||
let ref_engine = cpu_engine.as_ref().or(gpu_engine.as_ref()).unwrap();
|
||||
let ctx = ref_engine.prepare_context(header, difficulty);
|
||||
|
||||
println!("⛏️ Starting benchmark...");
|
||||
|
||||
@@ -396,30 +401,42 @@ async fn run_benchmark_command(
|
||||
let mut handles = Vec::new();
|
||||
let total_hashes_arc = Arc::new(std::sync::Mutex::new(0u64));
|
||||
|
||||
// Use larger ranges for GPU (1M) vs CPU (10K)
|
||||
let nonces_per_worker = if effective_gpu_workers > 0 {
|
||||
1_000_000u64 // 1M nonces per GPU worker
|
||||
} else {
|
||||
10_000u64 // 10K nonces per CPU worker
|
||||
};
|
||||
// Chunk sizes
|
||||
let cpu_chunk = 10_000u64;
|
||||
// GPU chunk size from 1 - 10M is good
|
||||
let gpu_chunk = 1_000_000u64;
|
||||
|
||||
for worker_id in 0..total_workers {
|
||||
let engine = engine.clone();
|
||||
// Determine type and engine
|
||||
let (engine, nonces_per_worker) = if worker_id < effective_cpu_workers {
|
||||
(
|
||||
cpu_engine.as_ref().expect("CPU engine missing").clone(),
|
||||
cpu_chunk,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
gpu_engine.as_ref().expect("GPU engine missing").clone(),
|
||||
gpu_chunk,
|
||||
)
|
||||
};
|
||||
|
||||
let ctx = ctx.clone();
|
||||
let cancel_flag = cancel_flag.clone();
|
||||
let total_hashes = total_hashes_arc.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
// We'll just define a range based on worker ID and a large multiplier
|
||||
// so they don't overlap easily, though it doesn't matter for pure hashrate.
|
||||
// Use a large stride to separate workers.
|
||||
let stride = U512::from(1_000_000_000_000u64);
|
||||
let worker_start = benchmark_range
|
||||
.start
|
||||
.saturating_add(U512::from(worker_id as u64).saturating_mul(stride));
|
||||
|
||||
let worker_range = EngineRange {
|
||||
start: benchmark_range.start.saturating_add(
|
||||
U512::from(worker_id as u64).saturating_mul(U512::from(nonces_per_worker)),
|
||||
),
|
||||
end: benchmark_range
|
||||
.start
|
||||
.saturating_add(
|
||||
U512::from((worker_id + 1) as u64)
|
||||
.saturating_mul(U512::from(nonces_per_worker)),
|
||||
)
|
||||
start: worker_start,
|
||||
end: worker_start
|
||||
.saturating_add(U512::from(nonces_per_worker))
|
||||
.saturating_sub(U512::from(1u64)),
|
||||
};
|
||||
|
||||
@@ -448,6 +465,8 @@ async fn run_benchmark_command(
|
||||
}
|
||||
}
|
||||
|
||||
engine_gpu::GpuEngine::clear_worker_resources();
|
||||
|
||||
worker_hashes
|
||||
});
|
||||
|
||||
@@ -480,7 +499,7 @@ async fn run_benchmark_command(
|
||||
format!("{:.0}", hash_rate)
|
||||
};
|
||||
println!("⏱️ {:.1}s - {} H/s", elapsed, hash_rate_str);
|
||||
} else if effective_gpu_workers > 0 {
|
||||
} else if effective_gpu_devices > 0 {
|
||||
println!("⏱️ {:.1}s - processing...", elapsed);
|
||||
} else {
|
||||
println!("⏱️ {:.1}s - starting...", elapsed);
|
||||
|
||||
@@ -9,8 +9,6 @@ description = "Service layer: HTTP API, job orchestration, and engine abstractio
|
||||
default = ["cpu", "metrics"]
|
||||
# Enable CPU engine by default.
|
||||
cpu = ["engine-cpu"]
|
||||
# Enable GPU engine.
|
||||
gpu = ["engine-gpu"]
|
||||
# Optional metrics/observability (Prometheus endpoint).
|
||||
metrics = [
|
||||
"dep:metrics",
|
||||
@@ -40,6 +38,6 @@ quantus-miner-api = { workspace = true }
|
||||
# Local crates
|
||||
pow-core = { path = "../pow-core" }
|
||||
engine-cpu = { path = "../engine-cpu", optional = true }
|
||||
engine-gpu = { path = "../engine-gpu", optional = true }
|
||||
engine-gpu = { path = "../engine-gpu" }
|
||||
metrics = { path = "../metrics", optional = true }
|
||||
miner-telemetry = { path = "../miner-telemetry" }
|
||||
|
||||
+276
-80
@@ -23,8 +23,8 @@ pub struct ServiceConfig {
|
||||
pub port: u16,
|
||||
/// Number of CPU worker threads to use for mining (None = auto-detect)
|
||||
pub cpu_workers: Option<usize>,
|
||||
/// Number of GPU worker threads to use for mining (None = auto-detect)
|
||||
pub gpu_workers: Option<usize>,
|
||||
/// Number of GPU devices to use for mining (None = auto-detect)
|
||||
pub gpu_devices: Option<usize>,
|
||||
/// Optional metrics port. When Some, metrics endpoint starts; when None, metrics are disabled.
|
||||
pub metrics_port: Option<u16>,
|
||||
/// How often to report mining progress (in milliseconds). If None, defaults to 10000ms.
|
||||
@@ -39,6 +39,8 @@ pub struct ServiceConfig {
|
||||
pub manip_step_batch: Option<u64>,
|
||||
/// Optional cap on solved-blocks throttle index for manipulator engine.
|
||||
pub manip_throttle_cap: Option<u64>,
|
||||
/// Optional target duration for GPU batches in milliseconds.
|
||||
pub gpu_batch_duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for ServiceConfig {
|
||||
@@ -46,7 +48,7 @@ impl Default for ServiceConfig {
|
||||
Self {
|
||||
port: 9833,
|
||||
cpu_workers: None,
|
||||
gpu_workers: None,
|
||||
gpu_devices: None,
|
||||
metrics_port: None,
|
||||
progress_interval_ms: None,
|
||||
chunk_size: None,
|
||||
@@ -54,6 +56,7 @@ impl Default for ServiceConfig {
|
||||
manip_base_delay_ns: None,
|
||||
manip_step_batch: None,
|
||||
manip_throttle_cap: None,
|
||||
gpu_batch_duration_ms: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,17 +65,18 @@ impl fmt::Display for ServiceConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"port={}, cpu_workers={:?}, gpu_workers={:?}, metrics_port={:?}, progress_interval_ms={:?}, chunk_size={:?}, manip_solved_blocks={:?}, manip_base_delay_ns={:?}, manip_step_batch={:?}, manip_throttle_cap={:?}",
|
||||
"port={}, cpu_workers={:?}, gpu_devices={:?}, metrics_port={:?}, progress_interval_ms={:?}, chunk_size={:?}, manip_solved_blocks={:?}, manip_base_delay_ns={:?}, manip_step_batch={:?}, manip_throttle_cap={:?}, gpu_batch_duration_ms={:?}",
|
||||
self.port,
|
||||
self.cpu_workers,
|
||||
self.gpu_workers,
|
||||
self.gpu_devices,
|
||||
self.metrics_port,
|
||||
self.progress_interval_ms,
|
||||
self.chunk_size,
|
||||
self.manip_solved_blocks,
|
||||
self.manip_base_delay_ns,
|
||||
self.manip_step_batch,
|
||||
self.manip_throttle_cap
|
||||
self.manip_throttle_cap,
|
||||
self.gpu_batch_duration_ms
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,7 +86,7 @@ impl fmt::Display for ServiceConfig {
|
||||
pub struct MiningService {
|
||||
pub jobs: Arc<Mutex<HashMap<String, MiningJob>>>,
|
||||
pub cpu_workers: usize,
|
||||
pub gpu_workers: usize,
|
||||
pub gpu_devices: usize,
|
||||
pub cpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
pub gpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
/// How often to report mining progress (in milliseconds).
|
||||
@@ -96,7 +100,7 @@ pub struct MiningService {
|
||||
impl MiningService {
|
||||
fn new(
|
||||
cpu_workers: usize,
|
||||
gpu_workers: usize,
|
||||
gpu_devices: usize,
|
||||
cpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
gpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
progress_interval_ms: u64,
|
||||
@@ -105,7 +109,7 @@ impl MiningService {
|
||||
Self {
|
||||
jobs: Arc::new(Mutex::new(HashMap::new())),
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
cpu_engine,
|
||||
gpu_engine,
|
||||
progress_interval_ms,
|
||||
@@ -121,17 +125,13 @@ impl MiningService {
|
||||
return Err("Job already exists".to_string());
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Adding mining job: {} with {} workers",
|
||||
job_id,
|
||||
self.cpu_workers + self.gpu_workers
|
||||
);
|
||||
log::info!("Adding mining job: {}", job_id,);
|
||||
job.job_id = Some(job_id.clone());
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
let engine_name = if self.cpu_workers > 0 && self.gpu_workers > 0 {
|
||||
let engine_name = if self.cpu_workers > 0 && self.gpu_devices > 0 {
|
||||
"hybrid"
|
||||
} else if self.gpu_workers > 0 {
|
||||
} else if self.gpu_devices > 0 {
|
||||
"gpu"
|
||||
} else {
|
||||
"cpu"
|
||||
@@ -149,7 +149,7 @@ impl MiningService {
|
||||
}
|
||||
job.start_mining(
|
||||
self.cpu_workers,
|
||||
self.gpu_workers,
|
||||
self.gpu_devices,
|
||||
self.cpu_engine.clone(),
|
||||
self.gpu_engine.clone(),
|
||||
self.progress_interval_ms,
|
||||
@@ -439,13 +439,13 @@ impl MiningJob {
|
||||
pub fn start_mining(
|
||||
&mut self,
|
||||
cpu_workers: usize,
|
||||
gpu_workers: usize,
|
||||
gpu_devices: usize,
|
||||
cpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
gpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
progress_interval_ms: u64,
|
||||
chunk_size: Option<u64>,
|
||||
) {
|
||||
let total_workers = cpu_workers + gpu_workers;
|
||||
let total_workers = cpu_workers + gpu_devices;
|
||||
let chan_capacity = std::env::var("MINER_RESULT_CHANNEL_CAP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
@@ -454,9 +454,9 @@ impl MiningJob {
|
||||
self.result_receiver = Some(receiver);
|
||||
|
||||
// Set engine name based on what's available
|
||||
self.engine_name = if cpu_workers > 0 && gpu_workers > 0 {
|
||||
self.engine_name = if cpu_workers > 0 && gpu_devices > 0 {
|
||||
"hybrid"
|
||||
} else if gpu_workers > 0 {
|
||||
} else if gpu_devices > 0 {
|
||||
"gpu"
|
||||
} else {
|
||||
"cpu"
|
||||
@@ -473,7 +473,7 @@ impl MiningJob {
|
||||
log::info!(
|
||||
"Starting mining with {} CPU + {} GPU workers, total range: {}",
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
total_range
|
||||
);
|
||||
|
||||
@@ -524,10 +524,10 @@ impl MiningJob {
|
||||
}
|
||||
|
||||
// Create GPU worker threads
|
||||
if gpu_workers > 0 {
|
||||
if gpu_devices > 0 {
|
||||
if let Some(gpu_engine) = gpu_engine {
|
||||
let ctx = gpu_engine.prepare_context(self.header_hash, self.difficulty);
|
||||
let gpu_partitions = compute_partitions(current_start, self.nonce_end, gpu_workers);
|
||||
let gpu_partitions = compute_partitions(current_start, self.nonce_end, gpu_devices);
|
||||
|
||||
for (start, end) in gpu_partitions.ranges.into_iter() {
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
@@ -588,7 +588,9 @@ impl MiningJob {
|
||||
}
|
||||
|
||||
while let Some(handle) = self.thread_handles.pop() {
|
||||
let _ = handle.join();
|
||||
// Do not wait for threads to finish; detach them so they can exit gracefully
|
||||
// when they detect the cancellation flag. This prevents blocking the control loop.
|
||||
drop(handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,14 +781,26 @@ fn mine_range_with_engine_typed(
|
||||
progress_interval_ms: u64,
|
||||
chunk_size: Option<u64>,
|
||||
) {
|
||||
log::info!(
|
||||
"⛏️ Job {} {} thread {} mining range {} to {} (inclusive)",
|
||||
job_id,
|
||||
engine_type,
|
||||
thread_id,
|
||||
range.start,
|
||||
range.end
|
||||
);
|
||||
if engine_type == "CPU" {
|
||||
log::info!(
|
||||
target: "miner-service",
|
||||
"CPU thread {} search started: Job {} range {} to {} (inclusive)",
|
||||
thread_id,
|
||||
job_id,
|
||||
range.start,
|
||||
range.end
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
target: "miner-service",
|
||||
"⛏️ Job {} {} thread {} mining range {} to {} (inclusive)",
|
||||
job_id,
|
||||
engine_type,
|
||||
thread_id,
|
||||
range.start,
|
||||
range.end
|
||||
);
|
||||
}
|
||||
|
||||
// Chunk the range into subranges to emit periodic progress updates for metrics
|
||||
let mut current_start = range.start;
|
||||
@@ -796,19 +810,42 @@ fn mine_range_with_engine_typed(
|
||||
let chunk_size_u64 = chunk_size.unwrap_or_else(|| {
|
||||
// Use engine-specific defaults if not configured
|
||||
if engine.name().contains("gpu") {
|
||||
// GPU can handle much larger chunks efficiently
|
||||
100_000_000 // 100M hashes per chunk
|
||||
// GPU can handle much larger chunks efficiently.
|
||||
// Increased to 4B to allow auto-tuned batches (e.g. 3s @ 1GH/s = 3B) to grow sufficiently.
|
||||
4_000_000_000
|
||||
} else if engine.name() == "hybrid" {
|
||||
// Hybrid engines use GPU-sized chunks since they route to GPU workers
|
||||
100_000_000 // 100M hashes per chunk for hybrid
|
||||
4_000_000_000
|
||||
} else {
|
||||
// CPU uses time-based chunks
|
||||
let est_ops_per_sec = 100_000u64; // 100K ops/sec for CPU
|
||||
((est_ops_per_sec.saturating_mul(target_ms)) / 1000).max(5_000)
|
||||
}
|
||||
});
|
||||
|
||||
// Log the effective chunk size to assist in debugging/tuning
|
||||
if engine_type == "GPU" {
|
||||
log::info!(
|
||||
target: "miner-service",
|
||||
"Job {} GPU thread {} configured with chunk size: {}",
|
||||
job_id,
|
||||
thread_id,
|
||||
chunk_size_u64
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
target: "miner-service",
|
||||
"Job {} {} thread {} configured with chunk size: {}",
|
||||
job_id,
|
||||
engine_type,
|
||||
thread_id,
|
||||
chunk_size_u64
|
||||
);
|
||||
}
|
||||
|
||||
let chunk_size = U512::from(chunk_size_u64);
|
||||
let mut done = false;
|
||||
let mut total_hashes_processed = 0u64;
|
||||
|
||||
while current_start <= end && !cancel_flag.load(Ordering::Relaxed) {
|
||||
let mut current_end = current_start
|
||||
@@ -824,7 +861,7 @@ fn mine_range_with_engine_typed(
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"Job {} {} thread {} processing subrange {}..{} (inclusive)",
|
||||
"Job {} {} thread {} starting search on subrange {}..{} (inclusive)",
|
||||
job_id,
|
||||
engine_type,
|
||||
thread_id,
|
||||
@@ -832,6 +869,20 @@ fn mine_range_with_engine_typed(
|
||||
sub_range.end
|
||||
);
|
||||
let status = engine.search_range(&ctx, sub_range.clone(), &cancel_flag);
|
||||
let status_str = match status {
|
||||
engine_cpu::EngineStatus::Found { .. } => "found",
|
||||
engine_cpu::EngineStatus::Exhausted { .. } => "exhausted",
|
||||
engine_cpu::EngineStatus::Cancelled { .. } => "cancelled",
|
||||
engine_cpu::EngineStatus::Running { .. } => "running",
|
||||
};
|
||||
log::info!(
|
||||
target: "miner-service",
|
||||
"{} thread {} finished search, status: {} job: {}",
|
||||
engine_type,
|
||||
thread_id,
|
||||
status_str,
|
||||
job_id,
|
||||
);
|
||||
|
||||
match status {
|
||||
engine_cpu::EngineStatus::Found {
|
||||
@@ -866,6 +917,7 @@ fn mine_range_with_engine_typed(
|
||||
break;
|
||||
}
|
||||
engine_cpu::EngineStatus::Exhausted { hash_count } => {
|
||||
total_hashes_processed += hash_count;
|
||||
// Send intermediate progress update for this chunk
|
||||
let update = ThreadResult {
|
||||
thread_id,
|
||||
@@ -894,6 +946,7 @@ fn mine_range_with_engine_typed(
|
||||
}
|
||||
}
|
||||
engine_cpu::EngineStatus::Cancelled { hash_count } => {
|
||||
total_hashes_processed += hash_count;
|
||||
// Send last progress update and stop
|
||||
let update = ThreadResult {
|
||||
thread_id,
|
||||
@@ -905,6 +958,16 @@ fn mine_range_with_engine_typed(
|
||||
if sender.try_send(update).is_err() {
|
||||
log::warn!(target: "miner", "Job {job_id} thread {thread_id} failed to send cancel update");
|
||||
}
|
||||
if engine_type == "CPU" {
|
||||
log::info!(
|
||||
target: "miner-service",
|
||||
"CPU thread {} search cancelled: Job {} processed {} hashes (total: {})",
|
||||
thread_id,
|
||||
job_id,
|
||||
hash_count,
|
||||
total_hashes_processed
|
||||
);
|
||||
}
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
@@ -919,6 +982,20 @@ fn mine_range_with_engine_typed(
|
||||
current_start = current_end.saturating_add(U512::from(1u64));
|
||||
}
|
||||
|
||||
// Check if loop exited due to cancellation flag
|
||||
if !done && cancel_flag.load(Ordering::Relaxed) {
|
||||
if engine_type == "CPU" {
|
||||
log::info!(
|
||||
target: "miner-service",
|
||||
"CPU thread {} search cancelled (flag set): Job {} processed {} hashes",
|
||||
thread_id,
|
||||
job_id,
|
||||
total_hashes_processed
|
||||
);
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
|
||||
if !done {
|
||||
// Signal thread completion with no final candidate
|
||||
let final_result = ThreadResult {
|
||||
@@ -931,6 +1008,20 @@ fn mine_range_with_engine_typed(
|
||||
if sender.try_send(final_result).is_err() {
|
||||
log::warn!(target: "miner", "Job {job_id} thread {thread_id} failed to send completion status after chunked search");
|
||||
}
|
||||
if engine_type == "CPU" {
|
||||
log::info!(
|
||||
target: "miner-service",
|
||||
"CPU thread {} search completed: Job {} exhausted range, processed {} hashes",
|
||||
thread_id,
|
||||
job_id,
|
||||
total_hashes_processed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly clear thread-local GPU resources to avoid TLS order panic
|
||||
if engine_type == "GPU" {
|
||||
engine_gpu::GpuEngine::clear_worker_resources();
|
||||
}
|
||||
|
||||
log::debug!(target: "miner", "Job {job_id} thread {thread_id} completed.");
|
||||
@@ -977,7 +1068,7 @@ pub async fn handle_mine_request(
|
||||
request: MiningRequest,
|
||||
state: MiningService,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
log::debug!("Received mine request: {request:?}");
|
||||
log::debug!(target: "miner-servce", "Mine request: {request:?}");
|
||||
if let Err(e) = validate_mining_request(&request) {
|
||||
log::warn!("Invalid mine request ({}): {}", request.job_id, e);
|
||||
#[cfg(feature = "metrics")]
|
||||
@@ -1006,7 +1097,7 @@ pub async fn handle_mine_request(
|
||||
|
||||
match state.add_job(request.job_id.clone(), job).await {
|
||||
Ok(_) => {
|
||||
log::debug!(target: "miner", "Accepted mine request for job ID: {}", request.job_id);
|
||||
log::debug!(target: "miner-servce", "Accepted mine request for job ID: {}", request.job_id);
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
metrics::inc_mine_requests("accepted");
|
||||
@@ -1048,7 +1139,7 @@ pub async fn handle_result_request(
|
||||
job_id: String,
|
||||
state: MiningService,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
log::debug!("Received result request for job: {job_id}");
|
||||
log::debug!(target: "miner-servce", "Result request: {job_id}");
|
||||
|
||||
let job = match state.get_job(&job_id).await {
|
||||
Some(job) => job,
|
||||
@@ -1158,7 +1249,7 @@ pub async fn handle_cancel_request(
|
||||
job_id: String,
|
||||
state: MiningService,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
log::debug!("Received cancel request for job: {job_id}");
|
||||
log::info!(target: "miner-servce", "Cancel job: {job_id}");
|
||||
|
||||
if state.cancel_job(&job_id).await {
|
||||
log::debug!(target: "miner", "Successfully cancelled job: {job_id}");
|
||||
@@ -1245,6 +1336,54 @@ fn compute_partitions(start: U512, end: U512, workers: usize) -> Partitions {
|
||||
Partitions { ranges }
|
||||
}
|
||||
|
||||
pub fn resolve_gpu_configuration(
|
||||
requested_devices: Option<usize>,
|
||||
gpu_batch_duration_ms: Option<u64>,
|
||||
) -> anyhow::Result<(Option<Arc<dyn MinerEngine>>, usize)> {
|
||||
// Default to 3000ms if not specified
|
||||
let duration = std::time::Duration::from_millis(gpu_batch_duration_ms.unwrap_or(3000));
|
||||
|
||||
if let Some(req_count) = requested_devices {
|
||||
if req_count == 0 {
|
||||
return Ok((None, 0));
|
||||
}
|
||||
// Explicit request > 0
|
||||
let engine = engine_gpu::GpuEngine::try_new(duration)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize GPU engine: {}", e))?;
|
||||
|
||||
let available = engine.device_count();
|
||||
if req_count > available {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Requested {} GPU devices but only {} device(s) are available.",
|
||||
req_count,
|
||||
available
|
||||
));
|
||||
}
|
||||
Ok((Some(Arc::new(engine)), req_count))
|
||||
} else {
|
||||
// Auto-detect
|
||||
match engine_gpu::GpuEngine::try_new(duration) {
|
||||
Ok(engine) => {
|
||||
let available = engine.device_count();
|
||||
if available > 0 {
|
||||
log::info!(
|
||||
"Auto-detected {} GPU device(s). Using all available GPUs.",
|
||||
available
|
||||
);
|
||||
Ok((Some(Arc::new(engine)), available))
|
||||
} else {
|
||||
log::info!("GPU auto-detection found 0 devices. Defaulting to CPU only.");
|
||||
Ok((None, 0))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("GPU auto-detection failed (no suitable GPU found): {}. Defaulting to CPU only.", e);
|
||||
Ok((None, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the miner service with the given configuration.
|
||||
/// - Spawns the mining loop.
|
||||
/// - Optionally exposes a metrics endpoint if `metrics_port` is provided and the `metrics` feature is enabled.
|
||||
@@ -1311,9 +1450,24 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
metrics::set_effective_cpus(effective_cpus as i64);
|
||||
}
|
||||
|
||||
// Initialize GPU engine if requested or for auto-detection
|
||||
let (gpu_engine, gpu_devices): (Option<Arc<dyn MinerEngine>>, usize) =
|
||||
match resolve_gpu_configuration(config.gpu_devices, config.gpu_batch_duration_ms) {
|
||||
Ok((engine, count)) => (engine, count),
|
||||
Err(e) => {
|
||||
log::error!("❌ ERROR: {}", e);
|
||||
if config.gpu_devices.is_some() {
|
||||
log::error!(" Please check your --gpu-devices setting or GPU hardware.");
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
(None, 0)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate CPU workers
|
||||
let cpu_workers = config.cpu_workers.unwrap_or_else(|| {
|
||||
if config.gpu_workers.is_none() || config.gpu_workers == Some(0) {
|
||||
if gpu_devices == 0 {
|
||||
// CPU-only mode: use default CPU allocation
|
||||
let default_cpu_workers = effective_cpus
|
||||
.saturating_sub(effective_cpus / 2)
|
||||
@@ -1328,33 +1482,24 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
} else {
|
||||
// Hybrid mode: default to half of effective CPUs for CPU
|
||||
let default_cpu_workers = effective_cpus / 2;
|
||||
log::info!("Hybrid mode: defaulting to {} CPU workers", default_cpu_workers);
|
||||
log::info!(
|
||||
"Hybrid mode: defaulting to {} CPU workers",
|
||||
default_cpu_workers
|
||||
);
|
||||
default_cpu_workers
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate GPU workers
|
||||
let gpu_workers = config.gpu_workers.unwrap_or_else(|| {
|
||||
if config.cpu_workers.is_some() && config.cpu_workers != Some(0) {
|
||||
// Hybrid mode: default to 1 GPU worker
|
||||
log::info!("Hybrid mode: defaulting to 1 GPU worker");
|
||||
1
|
||||
} else {
|
||||
// CPU-only mode by default
|
||||
0
|
||||
}
|
||||
});
|
||||
let total_workers = cpu_workers + gpu_devices;
|
||||
|
||||
let total_workers = cpu_workers + gpu_workers;
|
||||
|
||||
let (cpu_workers, gpu_workers) = if total_workers == 0 {
|
||||
let (cpu_workers, gpu_devices) = if total_workers == 0 {
|
||||
log::warn!(
|
||||
"No workers specified. Defaulting to CPU-only mode with {} workers.",
|
||||
effective_cpus
|
||||
);
|
||||
(effective_cpus, 0)
|
||||
} else {
|
||||
(cpu_workers, gpu_workers)
|
||||
(cpu_workers, gpu_devices)
|
||||
};
|
||||
|
||||
// Create engines based on worker configuration
|
||||
@@ -1364,27 +1509,11 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let gpu_engine: Option<Arc<dyn MinerEngine>> = if gpu_workers > 0 {
|
||||
#[cfg(feature = "gpu")]
|
||||
{
|
||||
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
|
||||
}
|
||||
#[cfg(not(feature = "gpu"))]
|
||||
{
|
||||
log::error!("GPU workers requested but this binary was built without the 'gpu' feature. Rebuild with --features gpu.");
|
||||
return Err(anyhow::anyhow!(
|
||||
"GPU workers requested but 'gpu' feature not enabled"
|
||||
));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Log the mining configuration
|
||||
log::info!(
|
||||
"🚀 Mining configuration: {} CPU workers, {} GPU workers",
|
||||
"🚀 Mining configuration: {} CPU workers, {} GPU devices",
|
||||
cpu_workers,
|
||||
gpu_workers
|
||||
gpu_devices
|
||||
);
|
||||
|
||||
if let Some(ref engine) = cpu_engine {
|
||||
@@ -1400,7 +1529,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
let progress_interval_ms = config.progress_interval_ms.unwrap_or(10000);
|
||||
let service = MiningService::new(
|
||||
cpu_workers,
|
||||
gpu_workers,
|
||||
gpu_devices,
|
||||
cpu_engine,
|
||||
gpu_engine,
|
||||
progress_interval_ms,
|
||||
@@ -1500,9 +1629,9 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
|
||||
let telemetry_handle = telemetry.clone();
|
||||
let svc = service.clone();
|
||||
let engine_name_str = if cpu_workers > 0 && gpu_workers > 0 {
|
||||
let engine_name_str = if cpu_workers > 0 && gpu_devices > 0 {
|
||||
"hybrid".to_string()
|
||||
} else if gpu_workers > 0 {
|
||||
} else if gpu_devices > 0 {
|
||||
"gpu".to_string()
|
||||
} else {
|
||||
"cpu".to_string()
|
||||
@@ -1533,7 +1662,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
let interval = miner_telemetry::SystemInterval {
|
||||
uptime_ms,
|
||||
engine: Some(engine_name_str.clone()),
|
||||
workers: Some((svc.cpu_workers + svc.gpu_workers) as u32),
|
||||
workers: Some((svc.cpu_workers + svc.gpu_devices) as u32),
|
||||
hash_rate: Some(total_rate),
|
||||
active_jobs: Some(active_jobs),
|
||||
linked_node_hint: None,
|
||||
@@ -1965,4 +2094,71 @@ mod tests {
|
||||
"final completion message carries zero hash_count"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cancel_returns_immediately() {
|
||||
use engine_cpu::{EngineRange, EngineStatus, MinerEngine};
|
||||
use pow_core::JobContext;
|
||||
use std::any::Any;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
struct SlowEngine;
|
||||
|
||||
impl MinerEngine for SlowEngine {
|
||||
fn name(&self) -> &'static str {
|
||||
"slow-engine"
|
||||
}
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
|
||||
JobContext::new(header_hash, difficulty)
|
||||
}
|
||||
fn search_range(
|
||||
&self,
|
||||
_ctx: &JobContext,
|
||||
_range: EngineRange,
|
||||
cancel: &AtomicBool,
|
||||
) -> EngineStatus {
|
||||
// Sleep to simulate a long-running batch on GPU
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
EngineStatus::Cancelled { hash_count: 0 }
|
||||
} else {
|
||||
EngineStatus::Exhausted { hash_count: 0 }
|
||||
}
|
||||
}
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
let engine = Arc::new(SlowEngine);
|
||||
let service = MiningService::new(1, 0, Some(engine), None, 10000, None);
|
||||
|
||||
let job = MiningJob::new([0u8; 32], U512::MAX, U512::zero(), U512::from(1000u64));
|
||||
|
||||
service
|
||||
.add_job("slow-job".to_string(), job)
|
||||
.await
|
||||
.expect("Failed to add job");
|
||||
|
||||
// Give thread a moment to start and enter sleep
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let start = Instant::now();
|
||||
service.cancel_job("slow-job").await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Cancel should be near-instant (<< 2 seconds)
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(500),
|
||||
"Cancel took too long: {:?} (expected < 500ms)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// Verify job is actually cancelled
|
||||
let job = service.get_job("slow-job").await.unwrap();
|
||||
assert_eq!(job.status, JobStatus::Cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user