From ac87758996384609f360c489a8a7e32b38bf996b Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 10:53:00 +0800 Subject: [PATCH 01/15] Fix GPU contention, benchmarking --- crates/engine-gpu/src/lib.rs | 225 +++++++++++++++++++---------------- crates/miner-cli/src/main.rs | 95 ++++++++++----- 2 files changed, 191 insertions(+), 129 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 8539334..9a784aa 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -16,17 +16,20 @@ struct GpuContext { device: wgpu::Device, queue: wgpu::Queue, pipeline: wgpu::ComputePipeline, - bind_group: wgpu::BindGroup, // Cached vendor configuration optimal_workgroups: u32, - // Reusable buffers +} + +#[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 { @@ -37,6 +40,100 @@ pub struct GpuEngine { // Thread-local storage for consistent GPU device assignment per worker thread thread_local! { static ASSIGNED_GPU_DEVICE: RefCell> = const { RefCell::new(None) }; + static WORKER_RESOURCES: RefCell> = 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 { @@ -123,87 +220,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 +229,7 @@ impl GpuEngine { device, queue, pipeline, - bind_group, optimal_workgroups, - header_buffer, - target_buffer, - start_nonce_buffer, - results_buffer, - dispatch_config_buffer, - staging_buffer, })); } @@ -328,6 +338,19 @@ impl MinerEngine for GpuEngine { range.end ); + // 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 +358,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,7 +370,7 @@ 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), ); @@ -425,7 +448,7 @@ impl MinerEngine for GpuEngine { threads_per_workgroup, ]; gpu_ctx.queue.write_buffer( - &gpu_ctx.dispatch_config_buffer, + &resources.dispatch_config_buffer, 0, bytemuck::cast_slice(&dispatch_config), ); @@ -434,12 +457,12 @@ impl MinerEngine for GpuEngine { let start_nonce_bytes = range.start.to_little_endian(); gpu_ctx .queue - .write_buffer(&gpu_ctx.start_nonce_buffer, 0, &start_nonce_bytes); + .write_buffer(&resources.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); + .write_buffer(&resources.results_buffer, 0, &ZEROS); let total_start = std::time::Instant::now(); @@ -453,13 +476,13 @@ impl MinerEngine for GpuEngine { timestamp_writes: None, }); cpass.set_pipeline(&gpu_ctx.pipeline); - cpass.set_bind_group(0, &gpu_ctx.bind_group, &[]); + cpass.set_bind_group(0, &resources.bind_group, &[]); cpass.dispatch_workgroups(num_workgroups, 1, 1); } encoder.copy_buffer_to_buffer( - &gpu_ctx.results_buffer, + &resources.results_buffer, 0, - &gpu_ctx.staging_buffer, + &resources.staging_buffer, 0, RESULTS_SIZE as u64, ); @@ -467,7 +490,7 @@ impl MinerEngine for GpuEngine { // Submit and wait for completion gpu_ctx.queue.submit(Some(encoder.finish())); - let buffer_slice = gpu_ctx.staging_buffer.slice(..); + let buffer_slice = resources.staging_buffer.slice(..); buffer_slice.map_async(wgpu::MapMode::Read, |_| {}); let _ = gpu_ctx.device.poll(wgpu::PollType::Wait { @@ -495,7 +518,7 @@ impl MinerEngine for GpuEngine { ); drop(data); - gpu_ctx.staging_buffer.unmap(); + resources.staging_buffer.unmap(); return EngineStatus::Found { candidate: Candidate { nonce, work, hash }, @@ -507,7 +530,7 @@ impl MinerEngine for GpuEngine { } drop(data); - gpu_ctx.staging_buffer.unmap(); + resources.staging_buffer.unmap(); let total_time = total_start.elapsed(); log::info!( diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 30f83cb..e5eb261 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -334,13 +334,15 @@ async fn run_benchmark_command( 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 Workers: {}", effective_gpu_workers); 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 { @@ -363,16 +365,10 @@ async fn run_benchmark_command( 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 @@ -380,15 +376,23 @@ async fn run_benchmark_command( let benchmark_start = Instant::now(); // Create a large range that should take the full duration + // 100M is definitely not enough for modern GPUs. + // Let's make it huge so we don't run out of range logic in this simple benchmark. + // However, the worker range logic below relies on fixed offsets. + // If we want correct benchmarking, we should ideally dynamic dispatch, but fixed ranges are okay for short duration. let benchmark_range = EngineRange { start: U512::from(0u64), - end: U512::from(100_000_000u64), // 100M nonces - should be plenty + end: U512::MAX, // Effectively infinite }; 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,31 +400,66 @@ 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; + 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(); + // Calculate a disjoint range for this worker. + // Simple strategy: Worker K gets [K*chunk, (K+1)*chunk] repeatedly? + // The original code was: + // worker_range = start + id*chunk .. start + (id+1)*chunk + // And then loop engine.search_range inside. + // Wait, the original code Loop: + // loop { ... search_range ... } + // BUT it doesn't advance the range in the loop! + // It keeps searching the SAME range? + // + // Original: + // let worker_range = ... (calculated once) + // loop { engine.search_range(&ctx, worker_range.clone(), ...) } + // + // If search_range returns Exhausted, it just searches it again? + // That's fine for benchmarking hash rate (doing same work repeatedly). + // But for "Total hashes", we are counting correctly. + // + // However, if we mix CPU and GPU, we have different chunk sizes. + // We need to make sure ranges don't overlap if that matters? + // For benchmarking hash rate, overlap doesn't matter (we just want to burn cycles). + // But to be clean: + // Let's just give each worker a dedicated slice of the huge space. + // Or just stick to the "loop same small range" approach which fits CPU cache better maybe? + // Actually, for GPU, repeatedly doing same range is fine. + 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)), - ) - .saturating_sub(U512::from(1u64)), + start: worker_start, + end: worker_start.saturating_add(U512::from(nonces_per_worker)).saturating_sub(U512::from(1u64)), }; let mut worker_hashes = 0u64; From 97ec1350def63e5dbaae2f4ac6552d73e901746e Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 12:46:57 +0800 Subject: [PATCH 02/15] Fix for #39 Fix GPU worker crash, enable GPU default, and improve benchmark output --- crates/engine-gpu/src/lib.rs | 8 ++++++++ crates/miner-cli/Cargo.toml | 2 +- crates/miner-cli/src/main.rs | 25 +++++++++++++++++++++++++ crates/miner-service/Cargo.toml | 2 +- crates/miner-service/src/lib.rs | 6 ++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 9a784aa..93bd20c 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -281,6 +281,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 { diff --git a/crates/miner-cli/Cargo.toml b/crates/miner-cli/Cargo.toml index 2e1358c..c2bacd8 100644 --- a/crates/miner-cli/Cargo.toml +++ b/crates/miner-cli/Cargo.toml @@ -6,7 +6,7 @@ publish = false description = "CLI binary to run the Quantus External Miner service" [features] -default = [] +default = ["gpu"] gpu = ["miner-service/gpu"] [dependencies] diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index e5eb261..e4c942b 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -402,8 +402,29 @@ async fn run_benchmark_command( // Chunk sizes let cpu_chunk = 10_000u64; + // Increase GPU chunk size to amortize submission overhead (10M) + // 1M might be too small for fast GPUs, 100M might be too large for short benchmarks. let gpu_chunk = 1_000_000u64; + if effective_gpu_workers > 0 { + if let Some(gpu_engine_arc) = &gpu_engine { + // We can't easily get device count from MinerEngine trait without downcasting or extending trait. + // But we know it's GpuEngine here if feature=gpu is enabled. + #[cfg(feature = "gpu")] + { + if let Some(gpu_ptr) = gpu_engine_arc.as_any().downcast_ref::() { + let device_count = gpu_ptr.device_count(); + if effective_gpu_workers > device_count { + println!("⚠️ WARNING: You have requested {} GPU workers for {} device(s).", effective_gpu_workers, device_count); + println!(" Multiple workers per device creates contention and typically reduces hashrate."); + println!(" Recommended: 1 worker per GPU device."); + println!(); + } + } + } + } + } + for worker_id in 0..total_workers { // Determine type and engine let (engine, nonces_per_worker) = if worker_id < effective_cpu_workers { @@ -487,6 +508,10 @@ async fn run_benchmark_command( } } + // Cleanup TLS resources explicitly to avoid panic on exit + #[cfg(feature = "gpu")] + engine_gpu::GpuEngine::clear_worker_resources(); + worker_hashes }); diff --git a/crates/miner-service/Cargo.toml b/crates/miner-service/Cargo.toml index ac8f33d..89b19b3 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -6,7 +6,7 @@ publish = false description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner" [features] -default = ["cpu", "metrics"] +default = ["cpu", "gpu", "metrics"] # Enable CPU engine by default. cpu = ["engine-cpu"] # Enable GPU engine. diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 7bd18b4..49ace5d 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -933,6 +933,12 @@ fn mine_range_with_engine_typed( } } + // Explicitly clear thread-local GPU resources to avoid TLS order panic + #[cfg(feature = "gpu")] + if engine_type == "GPU" { + engine_gpu::GpuEngine::clear_worker_resources(); + } + log::debug!(target: "miner", "Job {job_id} thread {thread_id} completed."); } From a548f74f7d0917fc58dfe782f0e983be5cf6f4b4 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 12:47:42 +0800 Subject: [PATCH 03/15] fmt --- crates/engine-gpu/src/lib.rs | 5 ++-- crates/miner-cli/src/main.rs | 58 ++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 93bd20c..c088d2a 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -355,9 +355,8 @@ impl MinerEngine for GpuEngine { }); // 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() - }); + 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]; diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index e4c942b..3982878 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -376,7 +376,7 @@ async fn run_benchmark_command( let benchmark_start = Instant::now(); // Create a large range that should take the full duration - // 100M is definitely not enough for modern GPUs. + // 100M is definitely not enough for modern GPUs. // Let's make it huge so we don't run out of range logic in this simple benchmark. // However, the worker range logic below relies on fixed offsets. // If we want correct benchmarking, we should ideally dynamic dispatch, but fixed ranges are okay for short duration. @@ -388,7 +388,7 @@ async fn run_benchmark_command( let mut header = [0u8; 32]; rand::thread_rng().fill_bytes(&mut header); let difficulty = U512::MAX; // High difficulty - no solutions expected - + // 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(); @@ -408,20 +408,26 @@ async fn run_benchmark_command( if effective_gpu_workers > 0 { if let Some(gpu_engine_arc) = &gpu_engine { - // We can't easily get device count from MinerEngine trait without downcasting or extending trait. - // But we know it's GpuEngine here if feature=gpu is enabled. - #[cfg(feature = "gpu")] - { - if let Some(gpu_ptr) = gpu_engine_arc.as_any().downcast_ref::() { - let device_count = gpu_ptr.device_count(); - if effective_gpu_workers > device_count { - println!("⚠️ WARNING: You have requested {} GPU workers for {} device(s).", effective_gpu_workers, device_count); - println!(" Multiple workers per device creates contention and typically reduces hashrate."); - println!(" Recommended: 1 worker per GPU device."); - println!(); - } - } - } + // We can't easily get device count from MinerEngine trait without downcasting or extending trait. + // But we know it's GpuEngine here if feature=gpu is enabled. + #[cfg(feature = "gpu")] + { + if let Some(gpu_ptr) = gpu_engine_arc + .as_any() + .downcast_ref::() + { + let device_count = gpu_ptr.device_count(); + if effective_gpu_workers > device_count { + println!( + "⚠️ WARNING: You have requested {} GPU workers for {} device(s).", + effective_gpu_workers, device_count + ); + println!(" Multiple workers per device creates contention and typically reduces hashrate."); + println!(" Recommended: 1 worker per GPU device."); + println!(); + } + } + } } } @@ -445,7 +451,7 @@ async fn run_benchmark_command( // Calculate a disjoint range for this worker. // Simple strategy: Worker K gets [K*chunk, (K+1)*chunk] repeatedly? - // The original code was: + // The original code was: // worker_range = start + id*chunk .. start + (id+1)*chunk // And then loop engine.search_range inside. // Wait, the original code Loop: @@ -462,25 +468,27 @@ async fn run_benchmark_command( // But for "Total hashes", we are counting correctly. // // However, if we mix CPU and GPU, we have different chunk sizes. - // We need to make sure ranges don't overlap if that matters? + // We need to make sure ranges don't overlap if that matters? // For benchmarking hash rate, overlap doesn't matter (we just want to burn cycles). // But to be clean: // Let's just give each worker a dedicated slice of the huge space. // Or just stick to the "loop same small range" approach which fits CPU cache better maybe? // Actually, for GPU, repeatedly doing same range is fine. - + 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 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: worker_start, - end: worker_start.saturating_add(U512::from(nonces_per_worker)).saturating_sub(U512::from(1u64)), + end: worker_start + .saturating_add(U512::from(nonces_per_worker)) + .saturating_sub(U512::from(1u64)), }; let mut worker_hashes = 0u64; From 2a36eeb3c3567ac159846875c1a958e26622943a Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:02:43 +0800 Subject: [PATCH 04/15] Change workflows to have only GPU build --- .github/workflows/ci.yml | 19 ++++--------------- .github/workflows/release-publish.yml | 20 -------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dc3b0e..8d4d3e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 --features gpu 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 --features gpu timeout-minutes: 15 analysis: diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 2e4a3ae..e34f911 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -71,44 +71,24 @@ 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: From 23c617bda95d5b8b9937da99efe95706b11bb7f7 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:10:46 +0800 Subject: [PATCH 05/15] remove some AI dust --- crates/miner-cli/src/main.rs | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 3982878..a5c46c1 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -375,14 +375,9 @@ async fn run_benchmark_command( let cancel_flag = Arc::new(AtomicBool::new(false)); let benchmark_start = Instant::now(); - // Create a large range that should take the full duration - // 100M is definitely not enough for modern GPUs. - // Let's make it huge so we don't run out of range logic in this simple benchmark. - // However, the worker range logic below relies on fixed offsets. - // If we want correct benchmarking, we should ideally dynamic dispatch, but fixed ranges are okay for short duration. let benchmark_range = EngineRange { start: U512::from(0u64), - end: U512::MAX, // Effectively infinite + end: U512::from(100_000_000u64), // 100M nonces }; let mut header = [0u8; 32]; @@ -402,8 +397,7 @@ async fn run_benchmark_command( // Chunk sizes let cpu_chunk = 10_000u64; - // Increase GPU chunk size to amortize submission overhead (10M) - // 1M might be too small for fast GPUs, 100M might be too large for short benchmarks. + // GPU chunk size from 1 - 10M is good let gpu_chunk = 1_000_000u64; if effective_gpu_workers > 0 { @@ -449,32 +443,6 @@ async fn run_benchmark_command( let cancel_flag = cancel_flag.clone(); let total_hashes = total_hashes_arc.clone(); - // Calculate a disjoint range for this worker. - // Simple strategy: Worker K gets [K*chunk, (K+1)*chunk] repeatedly? - // The original code was: - // worker_range = start + id*chunk .. start + (id+1)*chunk - // And then loop engine.search_range inside. - // Wait, the original code Loop: - // loop { ... search_range ... } - // BUT it doesn't advance the range in the loop! - // It keeps searching the SAME range? - // - // Original: - // let worker_range = ... (calculated once) - // loop { engine.search_range(&ctx, worker_range.clone(), ...) } - // - // If search_range returns Exhausted, it just searches it again? - // That's fine for benchmarking hash rate (doing same work repeatedly). - // But for "Total hashes", we are counting correctly. - // - // However, if we mix CPU and GPU, we have different chunk sizes. - // We need to make sure ranges don't overlap if that matters? - // For benchmarking hash rate, overlap doesn't matter (we just want to burn cycles). - // But to be clean: - // Let's just give each worker a dedicated slice of the huge space. - // Or just stick to the "loop same small range" approach which fits CPU cache better maybe? - // Actually, for GPU, repeatedly doing same range is fine. - 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. From f0aa971326de926814d4d4decb0359f1c955ccb6 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:44:59 +0800 Subject: [PATCH 06/15] remove gpu feature, rename to gpu-devices rename gpu-workers flag to devices, also check user doesn't specify more than they have (degrades performance) --- .github/workflows/release-publish.yml | 4 -- crates/miner-cli/Cargo.toml | 4 -- crates/miner-cli/src/main.rs | 82 ++++++++++------------ crates/miner-service/Cargo.toml | 6 +- crates/miner-service/src/lib.rs | 98 +++++++++++++++------------ 5 files changed, 90 insertions(+), 104 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index e34f911..9882fa8 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -71,25 +71,21 @@ jobs: target: x86_64-unknown-linux-gnu binary_name: quantus-miner asset_name: quantus-miner-linux-x86_64 - 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: "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: "gpu" # macOS builds (Apple Silicon) - os: macos-latest target: aarch64-apple-darwin binary_name: quantus-miner asset_name: quantus-miner-macos-aarch64 - features: "gpu" steps: - uses: actions/checkout@v4 diff --git a/crates/miner-cli/Cargo.toml b/crates/miner-cli/Cargo.toml index c2bacd8..9334381 100644 --- a/crates/miner-cli/Cargo.toml +++ b/crates/miner-cli/Cargo.toml @@ -5,10 +5,6 @@ edition.workspace = true publish = false description = "CLI binary to run the Quantus External Miner service" -[features] -default = ["gpu"] -gpu = ["miner-service/gpu"] - [dependencies] miner-service = { path = "../miner-service" } clap = { workspace = true, features = ["derive", "env"] } diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index a5c46c1..d2ec5cc 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -21,9 +21,9 @@ enum Command { #[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")] cpu_workers: Option, - /// Number of GPU worker threads to use for mining (None = auto-detect) - #[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")] - gpu_workers: Option, + /// Number of GPU devices to use for mining (None = auto-detect) + #[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")] + gpu_devices: Option, /// Optional Prometheus metrics exporter port; if omitted, metrics are disabled #[arg(long, env = "MINER_METRICS_PORT")] @@ -109,9 +109,9 @@ enum Command { #[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")] cpu_workers: Option, - /// Number of GPU workers to use for benchmark - #[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")] - gpu_workers: Option, + /// Number of GPU devices to use for benchmark + #[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")] + gpu_devices: Option, /// Benchmark duration in seconds (default: 10) #[arg(short, long, default_value_t = 10)] @@ -139,7 +139,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, @@ -162,7 +162,7 @@ async fn main() { Command::Serve { port, cpu_workers, - gpu_workers, + gpu_devices, metrics_port, verbose, progress_interval_ms, @@ -185,7 +185,7 @@ async fn main() { run_serve_command( port, cpu_workers, - gpu_workers, + gpu_devices, metrics_port, verbose, progress_interval_ms, @@ -209,11 +209,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 +222,7 @@ async fn main() { async fn run_serve_command( port: u16, cpu_workers: Option, - gpu_workers: Option, + gpu_devices: Option, metrics_port: Option, verbose: bool, progress_interval_ms: Option, @@ -294,7 +294,7 @@ async fn run_serve_command( let config = ServiceConfig { port, cpu_workers, - gpu_workers, + gpu_devices, metrics_port, progress_interval_ms, chunk_size, @@ -313,7 +313,7 @@ async fn run_serve_command( async fn run_benchmark_command( cpu_workers: Option, - gpu_workers: Option, + gpu_devices: Option, duration: u64, verbose: bool, ) { @@ -329,8 +329,8 @@ 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; + let effective_gpu_devices = gpu_devices.unwrap_or(0); + let total_workers = effective_cpu_workers + effective_gpu_devices; println!("🚀 Quantus Miner Benchmark"); println!("=========================="); @@ -340,7 +340,7 @@ async fn run_benchmark_command( effective_cpu_workers, num_cpus::get() ); - println!(" GPU Workers: {}", effective_gpu_workers); + println!(" GPU Devices: {}", effective_gpu_devices); println!("Duration: {} seconds", duration); println!("Total Workers: {}", total_workers); @@ -351,16 +351,8 @@ 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) - } - #[cfg(not(feature = "gpu"))] - { - eprintln!("Error: GPU workers requested but this binary was built without GPU support"); - std::process::exit(1); - } + let gpu_engine = if effective_gpu_devices > 0 { + Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc) } else { None }; @@ -400,26 +392,22 @@ async fn run_benchmark_command( // GPU chunk size from 1 - 10M is good let gpu_chunk = 1_000_000u64; - if effective_gpu_workers > 0 { + if effective_gpu_devices > 0 { if let Some(gpu_engine_arc) = &gpu_engine { // We can't easily get device count from MinerEngine trait without downcasting or extending trait. - // But we know it's GpuEngine here if feature=gpu is enabled. - #[cfg(feature = "gpu")] + // But we know it's GpuEngine here. + if let Some(gpu_ptr) = gpu_engine_arc + .as_any() + .downcast_ref::() { - if let Some(gpu_ptr) = gpu_engine_arc - .as_any() - .downcast_ref::() - { - let device_count = gpu_ptr.device_count(); - if effective_gpu_workers > device_count { - println!( - "⚠️ WARNING: You have requested {} GPU workers for {} device(s).", - effective_gpu_workers, device_count - ); - println!(" Multiple workers per device creates contention and typically reduces hashrate."); - println!(" Recommended: 1 worker per GPU device."); - println!(); - } + let device_count = gpu_ptr.device_count(); + if effective_gpu_devices > device_count { + eprintln!( + "❌ ERROR: You have requested {} GPU devices but only {} device(s) are available.", + effective_gpu_devices, device_count + ); + eprintln!(" Please check your --gpu-devices setting or GPU hardware."); + std::process::exit(1); } } } @@ -484,10 +472,8 @@ async fn run_benchmark_command( } } - // Cleanup TLS resources explicitly to avoid panic on exit - #[cfg(feature = "gpu")] engine_gpu::GpuEngine::clear_worker_resources(); - + worker_hashes }); @@ -520,7 +506,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); diff --git a/crates/miner-service/Cargo.toml b/crates/miner-service/Cargo.toml index 89b19b3..88e7c6a 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -6,11 +6,9 @@ publish = false description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner" [features] -default = ["cpu", "gpu", "metrics"] +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" } diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 49ace5d..c24a712 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -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, - /// Number of GPU worker threads to use for mining (None = auto-detect) - pub gpu_workers: Option, + /// Number of GPU devices to use for mining (None = auto-detect) + pub gpu_devices: Option, /// Optional metrics port. When Some, metrics endpoint starts; when None, metrics are disabled. pub metrics_port: Option, /// How often to report mining progress (in milliseconds). If None, defaults to 10000ms. @@ -46,7 +46,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, @@ -62,10 +62,10 @@ 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={:?}", self.port, self.cpu_workers, - self.gpu_workers, + self.gpu_devices, self.metrics_port, self.progress_interval_ms, self.chunk_size, @@ -82,7 +82,7 @@ impl fmt::Display for ServiceConfig { pub struct MiningService { pub jobs: Arc>>, pub cpu_workers: usize, - pub gpu_workers: usize, + pub gpu_devices: usize, pub cpu_engine: Option>, pub gpu_engine: Option>, /// How often to report mining progress (in milliseconds). @@ -96,7 +96,7 @@ pub struct MiningService { impl MiningService { fn new( cpu_workers: usize, - gpu_workers: usize, + gpu_devices: usize, cpu_engine: Option>, gpu_engine: Option>, progress_interval_ms: u64, @@ -105,7 +105,7 @@ impl MiningService { Self { jobs: Arc::new(Mutex::new(HashMap::new())), cpu_workers, - gpu_workers, + gpu_devices, cpu_engine, gpu_engine, progress_interval_ms, @@ -124,14 +124,14 @@ impl MiningService { log::info!( "Adding mining job: {} with {} workers", job_id, - self.cpu_workers + self.gpu_workers + self.cpu_workers + self.gpu_devices ); 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>, gpu_engine: Option>, progress_interval_ms: u64, chunk_size: Option, ) { - 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::().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(); @@ -934,7 +934,6 @@ fn mine_range_with_engine_typed( } // Explicitly clear thread-local GPU resources to avoid TLS order panic - #[cfg(feature = "gpu")] if engine_type == "GPU" { engine_gpu::GpuEngine::clear_worker_resources(); } @@ -1319,7 +1318,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { // Calculate CPU workers let cpu_workers = config.cpu_workers.unwrap_or_else(|| { - if config.gpu_workers.is_none() || config.gpu_workers == Some(0) { + if config.gpu_devices.is_none() || config.gpu_devices == Some(0) { // CPU-only mode: use default CPU allocation let default_cpu_workers = effective_cpus .saturating_sub(effective_cpus / 2) @@ -1340,10 +1339,10 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { }); // Calculate GPU workers - let gpu_workers = config.gpu_workers.unwrap_or_else(|| { + let gpu_devices = config.gpu_devices.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"); + log::info!("Hybrid mode: defaulting to 1 GPU device"); 1 } else { // CPU-only mode by default @@ -1351,16 +1350,16 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { } }); - let total_workers = cpu_workers + gpu_workers; + let total_workers = cpu_workers + gpu_devices; - 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 @@ -1370,27 +1369,38 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { None }; - let gpu_engine: Option> = if gpu_workers > 0 { - #[cfg(feature = "gpu")] - { - Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc) - } - #[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" - )); - } + let gpu_engine: Option> = if gpu_devices > 0 { + Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc) } else { None }; + // Validate GPU device count if requesting GPU devices + if gpu_devices > 0 { + if let Some(gpu_engine_arc) = &gpu_engine { + if let Some(gpu_ptr) = gpu_engine_arc + .as_any() + .downcast_ref::() + { + let device_count = gpu_ptr.device_count(); + if gpu_devices > device_count { + log::error!( + "❌ ERROR: Requested {} GPU devices but only {} device(s) are available.", + gpu_devices, + device_count + ); + log::error!(" Please check your --gpu-devices setting or GPU hardware."); + std::process::exit(1); + } + } + } + } + // 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 { @@ -1406,7 +1416,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, @@ -1506,9 +1516,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() @@ -1539,7 +1549,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, From b79a9368e0c5b5a863f4be72e28838aa09fe60fb Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:45:18 +0800 Subject: [PATCH 07/15] fmt --- crates/miner-cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index d2ec5cc..3270cfd 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -473,7 +473,7 @@ async fn run_benchmark_command( } engine_gpu::GpuEngine::clear_worker_resources(); - + worker_hashes }); From c2773cb80d50afd6860aef7799fb2fa23947f0c7 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:50:48 +0800 Subject: [PATCH 08/15] remove feature cpu --- .github/workflows/ci.yml | 6 +++--- README.md | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d4d3e8..1669b2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,11 +51,11 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 - name: compile (gpu) run: | - cargo build --locked --workspace --features gpu + cargo build --locked --workspace timeout-minutes: 90 - name: test (gpu) run: | - cargo test --locked --workspace --features gpu + cargo test --locked --workspace timeout-minutes: 15 analysis: @@ -84,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 diff --git a/README.md b/README.md index 6be830b..2e31772 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -20,11 +20,11 @@ The binary will be available at `target/release/quantus-miner`. # CPU-only mining (default: auto-detected CPU cores) ./target/release/quantus-miner --cpu-workers 4 -# GPU-only mining (requires --features gpu build) -./target/release/quantus-miner --gpu-workers 1 +# GPU-only mining +./target/release/quantus-miner --gpu-devices 1 # Hybrid CPU+GPU mining -./target/release/quantus-miner --cpu-workers 4 --gpu-workers 1 +./target/release/quantus-miner --cpu-workers 4 --gpu-devices 1 # Custom port and metrics ./target/release/quantus-miner --cpu-workers 2 --port 8000 --metrics-port 9900 @@ -35,7 +35,7 @@ The binary will be available at `target/release/quantus-miner`. | Argument | Environment Variable | Description | Default | |----------|---------------------|-------------|---------| | `--cpu-workers ` | `MINER_CPU_WORKERS` | Number of CPU worker threads | Auto-detect | -| `--gpu-workers ` | `MINER_GPU_WORKERS` | Number of GPU worker threads | 0 | +| `--gpu-devices ` | `MINER_GPU_WORKERS` | Number of GPU worker threads | 0 | | `--port ` | `MINER_PORT` | HTTP API port | 9833 | | `--metrics-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:** @@ -72,18 +72,18 @@ cargo build -p miner-cli --features gpu --release ./target/release/quantus-miner --cpu-workers 8 # Pure GPU mining -./target/release/quantus-miner --gpu-workers 1 +./target/release/quantus-miner --gpu-devices 1 # Hybrid mining: 4 CPU + 1 GPU workers -./target/release/quantus-miner --cpu-workers 4 --gpu-workers 1 +./target/release/quantus-miner --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 --cpu-workers 2 --gpu-devices 1 # Production setup with metrics ./target/release/quantus-miner \ --cpu-workers 6 \ - --gpu-workers 1 \ + --gpu-devices 1 \ --port 9833 \ --metrics-port 9900 ``` @@ -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 ``` From 4e7280cb0f9f1b23ea30fb6e5c7c3e2e3f95f99c Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:58:27 +0800 Subject: [PATCH 09/15] fux ci --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1669b2f..909bef8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,11 +51,11 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 - name: compile (gpu) run: | - cargo build --locked --workspace + cargo build --locked --workspace timeout-minutes: 90 - name: test (gpu) run: | - cargo test --locked --workspace + cargo test --locked --workspace timeout-minutes: 15 analysis: @@ -84,8 +84,8 @@ jobs: - name: setup rust uses: actions-rust-lang/setup-rust-toolchain@v1 - name: build benchmark binary - run: cargo build -p miner-cli --release + 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 + run: ./target/release/quantus-miner --cpu-workers 2 benchmark --duration 5 timeout-minutes: 10 From 010eff6e71c3e74f9d2832807097b5277e01948f Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 13:59:21 +0800 Subject: [PATCH 10/15] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e31772..0e176df 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The binary will be available at `target/release/quantus-miner`. | Argument | Environment Variable | Description | Default | |----------|---------------------|-------------|---------| | `--cpu-workers ` | `MINER_CPU_WORKERS` | Number of CPU worker threads | Auto-detect | -| `--gpu-devices ` | `MINER_GPU_WORKERS` | Number of GPU worker threads | 0 | +| `--gpu-devices ` | `MINER_GPU_DEVICES` | Number of GPU devices | 0 | | `--port ` | `MINER_PORT` | HTTP API port | 9833 | | `--metrics-port ` | `MINER_METRICS_PORT` | Prometheus metrics port | Disabled | From 96853cad23622303b2f8947c48cfeb53597d94cf Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 14:03:00 +0800 Subject: [PATCH 11/15] restore serve command --- .github/workflows/ci.yml | 2 +- README.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 909bef8..44c68f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,5 +87,5 @@ jobs: run: cargo build -p miner-cli --release timeout-minutes: 60 - name: run cpu benchmark - run: ./target/release/quantus-miner --cpu-workers 2 benchmark --duration 5 + run: ./target/release/quantus-miner serve --cpu-workers 2 benchmark --duration 5 timeout-minutes: 10 diff --git a/README.md b/README.md index 0e176df..6d47689 100644 --- a/README.md +++ b/README.md @@ -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 -./target/release/quantus-miner --gpu-devices 1 +./target/release/quantus-miner serve --gpu-devices 1 # Hybrid CPU+GPU mining -./target/release/quantus-miner --cpu-workers 4 --gpu-devices 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 @@ -69,16 +69,16 @@ cargo build -p miner-cli --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-devices 1 +./target/release/quantus-miner serve --gpu-devices 1 # Hybrid mining: 4 CPU + 1 GPU workers -./target/release/quantus-miner --cpu-workers 4 --gpu-devices 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-devices 1 +RUST_LOG=debug ./target/release/quantus-miner serve --cpu-workers 2 --gpu-devices 1 # Production setup with metrics ./target/release/quantus-miner \ @@ -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 From 06933d574504ee96c5c1f42c9418db77e911543a Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 26 Dec 2025 14:06:49 +0800 Subject: [PATCH 12/15] fix ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44c68f7..20fd0c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,5 +87,5 @@ jobs: run: cargo build -p miner-cli --release timeout-minutes: 60 - name: run cpu benchmark - run: ./target/release/quantus-miner serve --cpu-workers 2 benchmark --duration 5 + run: ./target/release/quantus-miner benchmark --cpu-workers 2 --duration 5 timeout-minutes: 10 From 3b5f759fbd6fea0a03899e68438f022d979006b2 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sat, 27 Dec 2025 11:40:03 +0800 Subject: [PATCH 13/15] refactored and unified gpu devices number logic use all GPUs by default, handle the parameter, disallow more GPU devices than available --- crates/engine-gpu/src/lib.rs | 5 ++ crates/miner-cli/src/main.rs | 40 +++++--------- crates/miner-service/src/lib.rs | 96 +++++++++++++++++++-------------- 3 files changed, 72 insertions(+), 69 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index c088d2a..31bd8b4 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -147,6 +147,11 @@ impl GpuEngine { block_on(Self::init()).expect("Failed to initialize GPU engine") } + /// Try to initialize the GPU engine, returning an error if initialization fails. + pub fn try_new() -> Result> { + block_on(Self::init()) + } + async fn init() -> Result> { log::info!("Initializing WGPU..."); let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 3270cfd..afd2fcd 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -329,7 +329,18 @@ async fn run_benchmark_command( env_logger::init(); let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get); - let effective_gpu_devices = gpu_devices.unwrap_or(0); + + // Initialize GPU engine and determine effective GPU devices + let (gpu_engine, effective_gpu_devices) = + match miner_service::resolve_gpu_configuration(gpu_devices) { + 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"); @@ -351,12 +362,6 @@ async fn run_benchmark_command( None }; - let gpu_engine = if effective_gpu_devices > 0 { - Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc) - } else { - None - }; - if total_workers == 0 { eprintln!("Error: No workers specified"); std::process::exit(1); @@ -392,27 +397,6 @@ async fn run_benchmark_command( // GPU chunk size from 1 - 10M is good let gpu_chunk = 1_000_000u64; - if effective_gpu_devices > 0 { - if let Some(gpu_engine_arc) = &gpu_engine { - // We can't easily get device count from MinerEngine trait without downcasting or extending trait. - // But we know it's GpuEngine here. - if let Some(gpu_ptr) = gpu_engine_arc - .as_any() - .downcast_ref::() - { - let device_count = gpu_ptr.device_count(); - if effective_gpu_devices > device_count { - eprintln!( - "❌ ERROR: You have requested {} GPU devices but only {} device(s) are available.", - effective_gpu_devices, device_count - ); - eprintln!(" Please check your --gpu-devices setting or GPU hardware."); - std::process::exit(1); - } - } - } - } - for worker_id in 0..total_workers { // Determine type and engine let (engine, nonces_per_worker) = if worker_id < effective_cpu_workers { diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index c24a712..cd550d8 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -1250,6 +1250,41 @@ fn compute_partitions(start: U512, end: U512, workers: usize) -> Partitions { Partitions { ranges } } +pub fn resolve_gpu_configuration(requested_devices: Option) -> anyhow::Result<(Option>, usize)> { + 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() + .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() { + 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. @@ -1316,9 +1351,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>, usize) = + match resolve_gpu_configuration(config.gpu_devices) { + 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_devices.is_none() || config.gpu_devices == Some(0) { + if gpu_devices == 0 { // CPU-only mode: use default CPU allocation let default_cpu_workers = effective_cpus .saturating_sub(effective_cpus / 2) @@ -1333,23 +1383,14 @@ 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_devices = config.gpu_devices.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 device"); - 1 - } else { - // CPU-only mode by default - 0 - } - }); - let total_workers = cpu_workers + gpu_devices; let (cpu_workers, gpu_devices) = if total_workers == 0 { @@ -1369,33 +1410,6 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { None }; - let gpu_engine: Option> = if gpu_devices > 0 { - Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc) - } else { - None - }; - - // Validate GPU device count if requesting GPU devices - if gpu_devices > 0 { - if let Some(gpu_engine_arc) = &gpu_engine { - if let Some(gpu_ptr) = gpu_engine_arc - .as_any() - .downcast_ref::() - { - let device_count = gpu_ptr.device_count(); - if gpu_devices > device_count { - log::error!( - "❌ ERROR: Requested {} GPU devices but only {} device(s) are available.", - gpu_devices, - device_count - ); - log::error!(" Please check your --gpu-devices setting or GPU hardware."); - std::process::exit(1); - } - } - } - } - // Log the mining configuration log::info!( "🚀 Mining configuration: {} CPU workers, {} GPU devices", From 0b0731b86942076fb0465ecc6bde44278767dffb Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sat, 27 Dec 2025 11:40:11 +0800 Subject: [PATCH 14/15] fmt --- crates/miner-service/src/lib.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index cd550d8..f529027 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -1250,7 +1250,9 @@ fn compute_partitions(start: U512, end: U512, workers: usize) -> Partitions { Partitions { ranges } } -pub fn resolve_gpu_configuration(requested_devices: Option) -> anyhow::Result<(Option>, usize)> { +pub fn resolve_gpu_configuration( + requested_devices: Option, +) -> anyhow::Result<(Option>, usize)> { if let Some(req_count) = requested_devices { if req_count == 0 { return Ok((None, 0)); @@ -1258,10 +1260,14 @@ pub fn resolve_gpu_configuration(requested_devices: Option) -> anyhow::Re // Explicit request > 0 let engine = engine_gpu::GpuEngine::try_new() .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)); + return Err(anyhow::anyhow!( + "Requested {} GPU devices but only {} device(s) are available.", + req_count, + available + )); } Ok((Some(Arc::new(engine)), req_count)) } else { @@ -1270,7 +1276,10 @@ pub fn resolve_gpu_configuration(requested_devices: Option) -> anyhow::Re Ok(engine) => { let available = engine.device_count(); if available > 0 { - log::info!("Auto-detected {} GPU device(s). Using all available GPUs.", available); + 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."); From c3e7d70a752c054a17e3223285c449d58286f6a9 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sat, 27 Dec 2025 11:57:12 +0800 Subject: [PATCH 15/15] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d47689..5deeea8 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ cargo build -p miner-cli --release 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-devices 1 \ --port 9833 \