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,