From 52318aeaad0d4e61572b40743f5f75f3803cf47e Mon Sep 17 00:00:00 2001 From: illuzen Date: Sat, 13 Dec 2025 17:38:24 +0800 Subject: [PATCH] support multiple gpus --- crates/engine-cpu/src/lib.rs | 15 +++++++++ crates/engine-gpu/src/lib.rs | 56 +++++++++++++++++++++++++++++---- crates/miner-service/src/lib.rs | 32 +++++++++++++------ 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/crates/engine-cpu/src/lib.rs b/crates/engine-cpu/src/lib.rs index c6c7536..efb5858 100644 --- a/crates/engine-cpu/src/lib.rs +++ b/crates/engine-cpu/src/lib.rs @@ -89,6 +89,9 @@ pub trait MinerEngine: Send + Sync { /// - Return `Exhausted` if the range is fully searched without a solution. /// - Return `Cancelled` if `cancel` was observed during the search. fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus; + + /// Enable downcasting to concrete engine types. + fn as_any(&self) -> &dyn std::any::Any; } /// Baseline CPU engine. @@ -113,6 +116,10 @@ impl MinerEngine for BaselineCpuEngine { JobContext::new(header_hash, difficulty) } + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { // Ensure start <= end (inclusive range). If not, treat as exhausted. if range.start > range.end { @@ -183,6 +190,10 @@ impl MinerEngine for FastCpuEngine { JobContext::new(header_hash, difficulty) } + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { use pow_core::{hash_from_nonce, is_valid_hash, step_nonce}; @@ -274,6 +285,10 @@ impl MinerEngine for ChainManipulatorEngine { ctx } + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { use pow_core::{hash_from_nonce, is_valid_hash, step_nonce}; diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 7c28f18..47cf608 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -5,8 +5,11 @@ use engine_cpu::{Candidate, EngineStatus, FoundOrigin, MinerEngine, Range}; use futures::executor::block_on; use pow_core::JobContext; use primitive_types::U512; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::cell::RefCell; +use std::sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, +}; /// Represents a single GPU device context. struct GpuContext { @@ -28,6 +31,12 @@ struct GpuContext { pub struct GpuEngine { contexts: Vec>, + device_counter: AtomicUsize, +} + +// Thread-local storage for consistent GPU device assignment per worker thread +thread_local! { + static ASSIGNED_GPU_DEVICE: RefCell> = RefCell::new(None); } impl Default for GpuEngine { @@ -252,7 +261,15 @@ impl GpuEngine { } } - Ok(Self { contexts }) + Ok(Self { + contexts, + device_counter: AtomicUsize::new(0), + }) + } + + /// Returns the number of GPU devices available + pub fn device_count(&self) -> usize { + self.contexts.len() } } @@ -265,6 +282,10 @@ impl MinerEngine for GpuEngine { JobContext::new(header_hash, difficulty) } + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { if self.contexts.is_empty() { log::warn!("No GPUs available for search."); @@ -276,10 +297,33 @@ impl MinerEngine for GpuEngine { return EngineStatus::Exhausted { hash_count: 0 }; } - // Use first GPU only for now to simplify and avoid threading overhead - let gpu_ctx = &self.contexts[0]; + // Use thread-local assignment for consistent worker-to-GPU mapping + let device_index = ASSIGNED_GPU_DEVICE.with(|assigned| { + let mut assigned_ref = assigned.borrow_mut(); + if let Some(index) = *assigned_ref { + // This thread already has a GPU assigned + index + } else { + // First time this thread is calling search_range, assign a GPU device + let index = if self.contexts.len() == 1 { + 0 + } else { + self.device_counter.fetch_add(1, Ordering::SeqCst) % self.contexts.len() + }; + *assigned_ref = Some(index); + log::info!( + "Worker thread assigned to GPU device {} (of {} total devices)", + index, + self.contexts.len() + ); + index + } + }); + + let gpu_ctx = &self.contexts[device_index]; log::debug!( - "Starting GPU search on device 0 for nonce range {}..={} (inclusive)", + "GPU device {} processing range {}..={} (inclusive)", + device_index, range.start, range.end ); diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 0cdca87..a4080db 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -1276,15 +1276,6 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { workers = effective_cpus.max(1); } - // Force workers=1 for GPU engine to prevent concurrent buffer mapping panics - if matches!(config.engine, EngineSelection::Gpu) && workers > 1 { - log::info!( - "GPU engine selected. Forcing workers to 1 (was {}) to avoid buffer contention.", - workers - ); - workers = 1; - } - log::info!( "Using {workers} worker thread(s) for mining (effective logical CPUs available: {effective_cpus})" ); @@ -1338,6 +1329,29 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { EngineSelection::Gpu => "High-performance GPU mining", } ); + + // Adjust worker count for GPU engine based on available devices + if matches!(config.engine, EngineSelection::Gpu) { + #[cfg(feature = "gpu")] + { + if let Some(gpu_engine) = engine.as_any().downcast_ref::() { + let gpu_device_count = gpu_engine.device_count(); + if gpu_device_count > 0 { + // Use one worker per GPU device, but don't exceed the configured worker count + workers = workers.min(gpu_device_count); + log::info!( + "GPU engine: {} GPU devices detected, using {} workers", + gpu_device_count, + workers + ); + } else { + log::warn!("GPU engine: No GPU devices detected, falling back to 1 worker"); + workers = 1; + } + } + } + } + log::info!("⚙️ Service configuration: {config}"); let progress_interval_ms = config.progress_interval_ms.unwrap_or(10000);