From 929a61af3edb0730719a3bdc93df5c527bd3be83 Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 30 Jun 2026 15:18:58 +0800 Subject: [PATCH 1/6] handle lost device correctly --- crates/engine-gpu/src/lib.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index efc33d5..f08b7a4 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -47,6 +47,9 @@ pub struct GpuEngine { thread_local! { static ASSIGNED_GPU_DEVICE: RefCell> = const { RefCell::new(None) }; static WORKER_RESOURCES: RefCell> = const { RefCell::new(None) }; + /// Set to true when this worker's GPU device is lost/unresponsive. + /// Once set, the worker will immediately return Cancelled on any search attempt. + static DEVICE_LOST: RefCell = const { RefCell::new(false) }; } impl GpuContext { @@ -408,6 +411,13 @@ impl MinerEngine for GpuEngine { return EngineStatus::Exhausted { hash_count: 0 }; } + // Check if this worker's GPU device was previously lost + let device_is_lost = DEVICE_LOST.with(|lost| *lost.borrow()); + if device_is_lost { + // Device was lost in a previous call - don't attempt any GPU operations + return EngineStatus::Cancelled { hash_count: 0 }; + } + // Empty or inverted range: nothing to do. if range.start > range.end { return EngineStatus::Exhausted { hash_count: 0 }; @@ -559,8 +569,11 @@ impl MinerEngine for GpuEngine { total_hashes += hash_count; } BatchResult::DeviceLost => { - // GPU device is lost/unresponsive - log loudly and return cancelled - // This prevents spinning at 0 H/s indefinitely on a dead device + // GPU device is lost/unresponsive - mark as permanently dead + // and clear resources to prevent "buffer already mapped" panics + DEVICE_LOST.with(|lost| *lost.borrow_mut() = true); + WORKER_RESOURCES.with(|res| *res.borrow_mut() = None); + log::error!( target: "gpu_engine", "GPU {} device lost or unresponsive - stopping worker. \ From e7dd03edf921369d0983a24c9427cc361b344e5d Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 30 Jun 2026 15:32:21 +0800 Subject: [PATCH 2/6] Update lib.rs --- crates/engine-gpu/src/lib.rs | 89 ++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index f08b7a4..1e34e24 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -163,6 +163,9 @@ fn backend_rank(backend: wgpu::Backend) -> u8 { /// adapters are dropped and only the best-ranked backend present is kept. /// Within a single backend each physical GPU appears exactly once, so rigs with /// multiple identical cards keep every card. +/// +/// When discrete GPUs are present, integrated GPUs (APUs) are skipped to avoid +/// resource contention and driver instability from mining on both simultaneously. fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { let usable: Vec = (0..infos.len()) .filter(|&i| { @@ -199,6 +202,27 @@ fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { }) .collect(); + // Check if we have any discrete GPUs + let has_discrete = selected + .iter() + .any(|&i| infos[i].device_type == wgpu::DeviceType::DiscreteGpu); + + // If discrete GPUs exist, filter out integrated GPUs to avoid contention + if has_discrete { + selected.retain(|&i| { + if infos[i].device_type == wgpu::DeviceType::IntegratedGpu { + log::info!( + target: "gpu_engine", + "Skipping integrated GPU (discrete GPU available): {} ({:?})", + infos[i].name, + infos[i].backend + ); + return false; + } + true + }); + } + selected.sort_by_key(|&i| match infos[i].device_type { wgpu::DeviceType::DiscreteGpu => 0, wgpu::DeviceType::IntegratedGpu => 1, @@ -893,7 +917,7 @@ mod adapter_selection_tests { wgpu::Backend::Dx12, ), ]; - assert_eq!(select_adapters(&infos), vec![1, 0]); + assert_eq!(select_adapters(&infos), vec![1]); } #[test] @@ -919,12 +943,31 @@ mod adapter_selection_tests { } #[test] - fn dx12_only_machine_keeps_all_dx12_adapters() { + fn dx12_only_machine_discrete_preferred_over_integrated() { let infos = [ info("iGPU", wgpu::DeviceType::IntegratedGpu, wgpu::Backend::Dx12), info("dGPU", wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Dx12), ]; - assert_eq!(select_adapters(&infos), vec![1, 0]); + // Only discrete GPU kept when both discrete and integrated present + assert_eq!(select_adapters(&infos), vec![1]); + } + + #[test] + fn integrated_only_machine_keeps_integrated() { + // When no discrete GPU exists, integrated GPUs should be used + let infos = [ + info( + "Intel UHD", + wgpu::DeviceType::IntegratedGpu, + wgpu::Backend::Vulkan, + ), + info( + "AMD Vega 8", + wgpu::DeviceType::IntegratedGpu, + wgpu::Backend::Vulkan, + ), + ]; + assert_eq!(select_adapters(&infos), vec![0, 1]); } #[test] @@ -941,4 +984,44 @@ mod adapter_selection_tests { fn empty_enumeration_selects_nothing() { assert!(select_adapters(&[]).is_empty()); } + + /// Exact scenario from Windows ASUS laptop with RX 560X + Vega 8 APU. + /// Both GPUs appear on both Vulkan and Dx12 backends. + /// Expected: Only the discrete RX 560X on Vulkan should be selected. + #[test] + fn windows_amd_discrete_plus_apu_uses_only_discrete() { + let infos = [ + info( + "Microsoft Basic Render Driver", + wgpu::DeviceType::Cpu, + wgpu::Backend::Dx12, + ), + info( + "AMD Radeon(TM) Vega 8 Graphics", + wgpu::DeviceType::IntegratedGpu, + wgpu::Backend::Dx12, + ), + info( + "Radeon RX 560X", + wgpu::DeviceType::DiscreteGpu, + wgpu::Backend::Dx12, + ), + info( + "AMD Radeon(TM) Vega 8 Graphics", + wgpu::DeviceType::IntegratedGpu, + wgpu::Backend::Vulkan, + ), + info( + "Radeon RX 560X", + wgpu::DeviceType::DiscreteGpu, + wgpu::Backend::Vulkan, + ), + ]; + // Should select only the discrete GPU on Vulkan (index 4) + // - Index 0: Skipped (CPU emulated) + // - Index 1, 2: Skipped (Dx12 lower priority than Vulkan) + // - Index 3: Skipped (integrated, discrete available) + // - Index 4: Selected (discrete, Vulkan) + assert_eq!(select_adapters(&infos), vec![4]); + } } From 6324c195b436c9d19bf6b95d6553c7fc458471a0 Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 30 Jun 2026 15:40:39 +0800 Subject: [PATCH 3/6] allow-integrated flag otherwise exclude --- crates/engine-gpu/benches/gpu_engine_bench.rs | 12 ++-- crates/engine-gpu/examples/verify_nonce.rs | 2 +- crates/engine-gpu/src/lib.rs | 68 ++++++++++++++----- crates/miner-cli/src/main.rs | 35 +++++++--- crates/miner-service/src/lib.rs | 6 +- 5 files changed, 89 insertions(+), 34 deletions(-) diff --git a/crates/engine-gpu/benches/gpu_engine_bench.rs b/crates/engine-gpu/benches/gpu_engine_bench.rs index 4517d91..6ad4b17 100644 --- a/crates/engine-gpu/benches/gpu_engine_bench.rs +++ b/crates/engine-gpu/benches/gpu_engine_bench.rs @@ -8,7 +8,7 @@ use std::sync::atomic::AtomicBool; fn bench_cpu_vs_gpu_small(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(10_000); - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); let cancel_flag = AtomicBool::new(false); let cancel_check = AtomicBoolCancelCheck(&cancel_flag); @@ -59,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(10_000); - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); let cancel_flag = AtomicBool::new(false); let cancel_check = AtomicBoolCancelCheck(&cancel_flag); @@ -110,7 +110,7 @@ fn bench_cpu_vs_gpu_medium(c: &mut Criterion) { fn bench_cpu_vs_gpu_large(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(10_000); - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); let cancel_flag = AtomicBool::new(false); let cancel_check = AtomicBoolCancelCheck(&cancel_flag); @@ -161,7 +161,7 @@ fn bench_cpu_vs_gpu_large(c: &mut Criterion) { fn bench_solution_finding(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(10_000); - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); let cancel_flag = AtomicBool::new(false); let cancel_check = AtomicBoolCancelCheck(&cancel_flag); @@ -212,7 +212,7 @@ fn bench_solution_finding(c: &mut Criterion) { fn bench_throughput_per_second(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(10_000); - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); let cancel_flag = AtomicBool::new(false); let cancel_check = AtomicBoolCancelCheck(&cancel_flag); @@ -262,7 +262,7 @@ fn bench_throughput_per_second(c: &mut Criterion) { } fn bench_gpu_batch_efficiency(c: &mut Criterion) { - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); let cancel_flag = AtomicBool::new(false); let cancel_check = AtomicBoolCancelCheck(&cancel_flag); diff --git a/crates/engine-gpu/examples/verify_nonce.rs b/crates/engine-gpu/examples/verify_nonce.rs index fce9d22..895e55b 100644 --- a/crates/engine-gpu/examples/verify_nonce.rs +++ b/crates/engine-gpu/examples/verify_nonce.rs @@ -26,7 +26,7 @@ fn main() { // 3. Verify with GPU engine log::info!("Initializing GPU engine..."); - let gpu_engine = GpuEngine::try_new(10_000_000, 0).expect("Failed to init GPU"); + let gpu_engine = GpuEngine::try_new(10_000_000, 0, false).expect("Failed to init GPU"); // Search a small range around the valid nonce let gpu_range = Range { diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 1e34e24..9df2401 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -164,9 +164,10 @@ fn backend_rank(backend: wgpu::Backend) -> u8 { /// Within a single backend each physical GPU appears exactly once, so rigs with /// multiple identical cards keep every card. /// -/// When discrete GPUs are present, integrated GPUs (APUs) are skipped to avoid -/// resource contention and driver instability from mining on both simultaneously. -fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { +/// When discrete GPUs are present, integrated GPUs (APUs) are skipped by default +/// to avoid resource contention and driver instability from mining on both +/// simultaneously. Set `allow_integrated` to true to override this behavior. +fn select_adapters(infos: &[wgpu::AdapterInfo], allow_integrated: bool) -> Vec { let usable: Vec = (0..infos.len()) .filter(|&i| { if infos[i].device_type == wgpu::DeviceType::Cpu { @@ -207,13 +208,13 @@ fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { .iter() .any(|&i| infos[i].device_type == wgpu::DeviceType::DiscreteGpu); - // If discrete GPUs exist, filter out integrated GPUs to avoid contention - if has_discrete { + // If discrete GPUs exist and allow_integrated is false, filter out integrated GPUs + if has_discrete && !allow_integrated { selected.retain(|&i| { if infos[i].device_type == wgpu::DeviceType::IntegratedGpu { log::info!( target: "gpu_engine", - "Skipping integrated GPU (discrete GPU available): {} ({:?})", + "Skipping integrated GPU (discrete GPU available, use --allow-integrated to override): {} ({:?})", infos[i].name, infos[i].backend ); @@ -234,12 +235,21 @@ fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { impl GpuEngine { /// Try to initialize the GPU engine with the given batch size and throttle (ms between batches). /// + /// # Arguments + /// * `batch_size` - Number of nonces per batch + /// * `throttle_ms` - Delay between batches in milliseconds (0 = no throttle) + /// * `allow_integrated` - If true, use integrated GPUs even when discrete GPUs are available + /// /// # Errors /// /// Returns an error if: /// - `batch_size` is zero (no work would be performed) /// - No usable GPU adapters are found - pub fn try_new(batch_size: u32, throttle_ms: u64) -> Result> { + pub fn try_new( + batch_size: u32, + throttle_ms: u64, + allow_integrated: bool, + ) -> Result> { if batch_size == 0 { return Err("batch_size must be non-zero".into()); } @@ -248,17 +258,23 @@ impl GpuEngine { match tokio::runtime::Handle::try_current() { Ok(handle) => { // We're inside a tokio runtime - use block_in_place to allow blocking - tokio::task::block_in_place(|| handle.block_on(Self::init(batch_size, throttle_ms))) + tokio::task::block_in_place(|| { + handle.block_on(Self::init(batch_size, throttle_ms, allow_integrated)) + }) } Err(_) => { // No runtime exists - create a temporary one let rt = tokio::runtime::Runtime::new()?; - rt.block_on(Self::init(batch_size, throttle_ms)) + rt.block_on(Self::init(batch_size, throttle_ms, allow_integrated)) } } } - async fn init(batch_size: u32, throttle_ms: u64) -> Result> { + async fn init( + batch_size: u32, + throttle_ms: u64, + allow_integrated: bool, + ) -> Result> { log::info!(target: "gpu_engine", "Initializing WGPU..."); let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, @@ -268,7 +284,7 @@ impl GpuEngine { let adapters = instance.enumerate_adapters(wgpu::Backends::PRIMARY); let infos: Vec = adapters.iter().map(|a| a.get_info()).collect(); - let selected = select_adapters(&infos); + let selected = select_adapters(&infos, allow_integrated); if selected.is_empty() { log::error!( target: "gpu_engine", @@ -917,7 +933,8 @@ mod adapter_selection_tests { wgpu::Backend::Dx12, ), ]; - assert_eq!(select_adapters(&infos), vec![1]); + // Default: only discrete GPU on best backend (Vulkan) + assert_eq!(select_adapters(&infos, false), vec![1]); } #[test] @@ -939,7 +956,7 @@ mod adapter_selection_tests { wgpu::Backend::Vulkan, ), ]; - assert_eq!(select_adapters(&infos), vec![0, 1, 2]); + assert_eq!(select_adapters(&infos, false), vec![0, 1, 2]); } #[test] @@ -949,7 +966,7 @@ mod adapter_selection_tests { info("dGPU", wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Dx12), ]; // Only discrete GPU kept when both discrete and integrated present - assert_eq!(select_adapters(&infos), vec![1]); + assert_eq!(select_adapters(&infos, false), vec![1]); } #[test] @@ -967,7 +984,7 @@ mod adapter_selection_tests { wgpu::Backend::Vulkan, ), ]; - assert_eq!(select_adapters(&infos), vec![0, 1]); + assert_eq!(select_adapters(&infos, false), vec![0, 1]); } #[test] @@ -977,12 +994,12 @@ mod adapter_selection_tests { wgpu::DeviceType::Cpu, wgpu::Backend::Vulkan, )]; - assert!(select_adapters(&infos).is_empty()); + assert!(select_adapters(&infos, false).is_empty()); } #[test] fn empty_enumeration_selects_nothing() { - assert!(select_adapters(&[]).is_empty()); + assert!(select_adapters(&[], false).is_empty()); } /// Exact scenario from Windows ASUS laptop with RX 560X + Vega 8 APU. @@ -1022,6 +1039,21 @@ mod adapter_selection_tests { // - Index 1, 2: Skipped (Dx12 lower priority than Vulkan) // - Index 3: Skipped (integrated, discrete available) // - Index 4: Selected (discrete, Vulkan) - assert_eq!(select_adapters(&infos), vec![4]); + assert_eq!(select_adapters(&infos, false), vec![4]); + } + + /// Test --allow-integrated flag: when set, both discrete and integrated GPUs are used + #[test] + fn allow_integrated_flag_keeps_both_gpu_types() { + let infos = [ + info( + "iGPU", + wgpu::DeviceType::IntegratedGpu, + wgpu::Backend::Vulkan, + ), + info("dGPU", wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Vulkan), + ]; + // With allow_integrated=true, both GPUs should be selected (discrete first) + assert_eq!(select_adapters(&infos, true), vec![1, 0]); } } diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 1b4d627..c3dc092 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -52,6 +52,12 @@ enum Command { )] gpu_throttle_ms: u64, + /// Allow integrated GPUs (APUs) even when discrete GPUs are available. + /// By default, integrated GPUs are skipped when a discrete GPU is present + /// to avoid resource contention and driver instability. + #[arg(long = "allow-integrated", env = "MINER_ALLOW_INTEGRATED")] + allow_integrated: bool, + /// Enable verbose logging #[arg(short, long, env = "MINER_VERBOSE")] verbose: bool, @@ -79,6 +85,10 @@ enum Command { #[arg(short, long, default_value_t = 10)] duration: u64, + /// Allow integrated GPUs (APUs) even when discrete GPUs are available + #[arg(long = "allow-integrated", env = "MINER_ALLOW_INTEGRATED")] + allow_integrated: bool, + /// Enable verbose logging #[arg(short, long, env = "MINER_VERBOSE")] verbose: bool, @@ -112,6 +122,7 @@ async fn main() { cpu_batch_size, gpu_throttle_ms, metrics_port, + allow_integrated, verbose, } => { init_logger(verbose); @@ -135,6 +146,7 @@ async fn main() { gpu_batch_size, cpu_batch_size, gpu_throttle_ms, + allow_integrated, }; if let Err(e) = run(config).await { @@ -149,6 +161,7 @@ async fn main() { gpu_batch_size, cpu_batch_size, duration, + allow_integrated, verbose, } => { init_logger(verbose); @@ -158,6 +171,7 @@ async fn main() { gpu_batch_size, cpu_batch_size, duration, + allow_integrated, ) .await; } @@ -183,18 +197,23 @@ async fn run_benchmark( gpu_batch_size: u32, cpu_batch_size: u64, duration: u64, + allow_integrated: bool, ) { let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get); // Initialize GPU engine (no throttle for benchmark) - let (gpu_engine, effective_gpu_devices) = - match miner_service::resolve_gpu_configuration(gpu_devices, gpu_batch_size, 0) { - Ok((engine, count)) => (engine, count), - Err(e) => { - eprintln!("❌ ERROR: {}", e); - std::process::exit(1); - } - }; + let (gpu_engine, effective_gpu_devices) = match miner_service::resolve_gpu_configuration( + gpu_devices, + gpu_batch_size, + 0, + allow_integrated, + ) { + Ok((engine, count)) => (engine, count), + Err(e) => { + eprintln!("❌ ERROR: {}", e); + std::process::exit(1); + } + }; let total_workers = effective_cpu_workers + effective_gpu_devices; diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 3a503b8..cfbaa00 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -33,6 +33,8 @@ pub struct ServiceConfig { pub cpu_batch_size: u64, /// GPU throttle delay in milliseconds between batches (0 = no throttle) pub gpu_throttle_ms: u64, + /// Allow integrated GPUs even when discrete GPUs are available + pub allow_integrated: bool, } /// Engine type for tracking metrics per compute type. @@ -430,6 +432,7 @@ pub fn resolve_gpu_configuration( requested_devices: Option, batch_size: u32, throttle_ms: u64, + allow_integrated: bool, ) -> anyhow::Result<(Option>, usize)> { // Explicit 0 means no GPU if requested_devices == Some(0) { @@ -437,7 +440,7 @@ pub fn resolve_gpu_configuration( } // Try to initialize GPU engine - let engine = engine_gpu::GpuEngine::try_new(batch_size, throttle_ms); + let engine = engine_gpu::GpuEngine::try_new(batch_size, throttle_ms, allow_integrated); let engine = match engine { Ok(e) => e, Err(e) => { @@ -482,6 +485,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { config.gpu_devices, config.gpu_batch_size, config.gpu_throttle_ms, + config.allow_integrated, )?; // Resolve CPU workers From cbfab541664a4f8231687d4673dbb4f040ba8d0d Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 30 Jun 2026 15:47:15 +0800 Subject: [PATCH 4/6] DeviceLost is a status --- crates/engine-cpu/src/lib.rs | 4 ++++ crates/engine-gpu/examples/verify_nonce.rs | 3 +++ crates/engine-gpu/src/lib.rs | 6 +++--- crates/miner-cli/src/main.rs | 8 +++++++- crates/miner-service/src/lib.rs | 17 +++++++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/engine-cpu/src/lib.rs b/crates/engine-cpu/src/lib.rs index b8bd9d2..b3bbe8e 100644 --- a/crates/engine-cpu/src/lib.rs +++ b/crates/engine-cpu/src/lib.rs @@ -51,6 +51,10 @@ pub enum EngineStatus { Cancelled { hash_count: u64, }, + /// GPU device lost or unresponsive. Worker should exit permanently. + DeviceLost { + hash_count: u64, + }, } /// Cancellation checker passed to search_range. diff --git a/crates/engine-gpu/examples/verify_nonce.rs b/crates/engine-gpu/examples/verify_nonce.rs index 895e55b..8ad8215 100644 --- a/crates/engine-gpu/examples/verify_nonce.rs +++ b/crates/engine-gpu/examples/verify_nonce.rs @@ -56,6 +56,9 @@ fn main() { EngineStatus::Cancelled { .. } => { log::error!("FAILURE: GPU search cancelled!"); } + EngineStatus::DeviceLost { .. } => { + log::error!("FAILURE: GPU device lost!"); + } EngineStatus::Running { .. } => { log::error!("FAILURE: GPU returned Running status!"); } diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 9df2401..7d9e12d 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -454,8 +454,8 @@ impl MinerEngine for GpuEngine { // Check if this worker's GPU device was previously lost let device_is_lost = DEVICE_LOST.with(|lost| *lost.borrow()); if device_is_lost { - // Device was lost in a previous call - don't attempt any GPU operations - return EngineStatus::Cancelled { hash_count: 0 }; + // Device was lost in a previous call - signal worker should exit + return EngineStatus::DeviceLost { hash_count: 0 }; } // Empty or inverted range: nothing to do. @@ -620,7 +620,7 @@ impl MinerEngine for GpuEngine { This GPU will not process further batches.", device_index ); - return EngineStatus::Cancelled { + return EngineStatus::DeviceLost { hash_count: total_hashes, }; } diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index c3dc092..8ab8568 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -293,12 +293,18 @@ async fn run_benchmark( match result { engine_cpu::EngineStatus::Found { hash_count, .. } | engine_cpu::EngineStatus::Exhausted { hash_count } - | engine_cpu::EngineStatus::Cancelled { hash_count } => { + | engine_cpu::EngineStatus::Cancelled { hash_count } + | engine_cpu::EngineStatus::DeviceLost { hash_count } => { *hashes.lock().unwrap() += hash_count; } engine_cpu::EngineStatus::Running { .. } => {} } + // Exit if device is lost + if matches!(result, engine_cpu::EngineStatus::DeviceLost { .. }) { + break; + } + if start.elapsed() >= Duration::from_secs(duration) { break; } diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index cfbaa00..124978c 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -338,6 +338,7 @@ fn worker_loop( engine_cpu::EngineStatus::Found { .. } => "FOUND", engine_cpu::EngineStatus::Exhausted { .. } => "EXHAUSTED", engine_cpu::EngineStatus::Cancelled { .. } => "CANCELLED", + engine_cpu::EngineStatus::DeviceLost { .. } => "DEVICE_LOST", engine_cpu::EngineStatus::Running { .. } => "RUNNING", }; log::debug!( @@ -354,6 +355,7 @@ fn worker_loop( engine_cpu::EngineStatus::Found { hash_count, .. } => hash_count, engine_cpu::EngineStatus::Exhausted { hash_count } => hash_count, engine_cpu::EngineStatus::Cancelled { hash_count } => hash_count, + engine_cpu::EngineStatus::DeviceLost { hash_count } => hash_count, engine_cpu::EngineStatus::Running { .. } => 0, }; log_worker_completion( @@ -402,6 +404,21 @@ fn worker_loop( log_worker_completion(type_str, thread_id, "new block", hash_count, search_elapsed); (None, hash_count) } + engine_cpu::EngineStatus::DeviceLost { hash_count } => { + log::error!( + "{type_str} worker {thread_id} GPU device lost - worker exiting permanently" + ); + // Send final result before exiting + let _ = result_tx.try_send(WorkerResult { + thread_id, + engine_type, + job_id, + candidate: None, + hash_count, + completed: true, + }); + break; // Exit the worker loop + } engine_cpu::EngineStatus::Running { .. } => { // Should not happen for synchronous search (None, 0) From 90a274bac403ed62d02accc7d37193ec0019fc48 Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 30 Jun 2026 15:50:44 +0800 Subject: [PATCH 5/6] Integrated GPU skipped when discrete init fails --- crates/engine-gpu/src/lib.rs | 130 +++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 7d9e12d..9438219 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -167,7 +167,10 @@ fn backend_rank(backend: wgpu::Backend) -> u8 { /// When discrete GPUs are present, integrated GPUs (APUs) are skipped by default /// to avoid resource contention and driver instability from mining on both /// simultaneously. Set `allow_integrated` to true to override this behavior. -fn select_adapters(infos: &[wgpu::AdapterInfo], allow_integrated: bool) -> Vec { +/// +/// Note: This function no longer filters integrated GPUs - that decision is made +/// after initialization, so we can fall back to integrated if discrete fails. +fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { let usable: Vec = (0..infos.len()) .filter(|&i| { if infos[i].device_type == wgpu::DeviceType::Cpu { @@ -203,27 +206,8 @@ fn select_adapters(infos: &[wgpu::AdapterInfo], allow_integrated: bool) -> Vec 0, wgpu::DeviceType::IntegratedGpu => 1, @@ -284,7 +268,7 @@ impl GpuEngine { let adapters = instance.enumerate_adapters(wgpu::Backends::PRIMARY); let infos: Vec = adapters.iter().map(|a| a.get_info()).collect(); - let selected = select_adapters(&infos, allow_integrated); + let selected = select_adapters(&infos); if selected.is_empty() { log::error!( target: "gpu_engine", @@ -295,7 +279,14 @@ impl GpuEngine { } let mut adapters: Vec> = adapters.into_iter().map(Some).collect(); - let mut contexts = Vec::new(); + + // Track successfully initialized contexts with their device type + struct InitializedGpu { + context: Arc, + device_type: wgpu::DeviceType, + name: String, + } + let mut initialized: Vec = Vec::new(); // Timeout for initializing each adapter (30 seconds should be plenty) let init_timeout = std::time::Duration::from_secs(30); @@ -384,19 +375,49 @@ impl GpuEngine { // Calculate vendor-specific configuration once during initialization let optimal_workgroups = get_vendor_specific_dispatch(info, &device); - contexts.push(Arc::new(GpuContext { - device, - queue, - pipeline, - optimal_workgroups, - })); + initialized.push(InitializedGpu { + context: Arc::new(GpuContext { + device, + queue, + pipeline, + optimal_workgroups, + }), + device_type: info.device_type, + name: info.name.clone(), + }); } - if contexts.is_empty() { + if initialized.is_empty() { log::error!(target: "gpu_engine", "No GPU adapters could be initialized successfully."); return Err("No GPU adapters could be initialized".into()); } + // Now filter integrated GPUs if discrete GPUs successfully initialized + // (unless allow_integrated is set) + let has_discrete = initialized + .iter() + .any(|g| g.device_type == wgpu::DeviceType::DiscreteGpu); + + let contexts: Vec> = if has_discrete && !allow_integrated { + initialized + .into_iter() + .filter(|g| { + if g.device_type == wgpu::DeviceType::IntegratedGpu { + log::info!( + target: "gpu_engine", + "Dropping integrated GPU (discrete GPU initialized successfully, use --allow-integrated to override): {}", + g.name + ); + return false; + } + true + }) + .map(|g| g.context) + .collect() + } else { + initialized.into_iter().map(|g| g.context).collect() + }; + log::info!( target: "gpu_engine", "GPU engine initialized with {} devices (batch size: {} nonces, throttle: {}ms)", @@ -933,8 +954,9 @@ mod adapter_selection_tests { wgpu::Backend::Dx12, ), ]; - // Default: only discrete GPU on best backend (Vulkan) - assert_eq!(select_adapters(&infos, false), vec![1]); + // select_adapters returns all non-CPU adapters on best backend, discrete first + // Integrated filtering now happens after init in the init() function + assert_eq!(select_adapters(&infos), vec![1, 0]); } #[test] @@ -956,17 +978,17 @@ mod adapter_selection_tests { wgpu::Backend::Vulkan, ), ]; - assert_eq!(select_adapters(&infos, false), vec![0, 1, 2]); + assert_eq!(select_adapters(&infos), vec![0, 1, 2]); } #[test] - fn dx12_only_machine_discrete_preferred_over_integrated() { + fn dx12_only_machine_returns_all_adapters_sorted() { let infos = [ info("iGPU", wgpu::DeviceType::IntegratedGpu, wgpu::Backend::Dx12), info("dGPU", wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Dx12), ]; - // Only discrete GPU kept when both discrete and integrated present - assert_eq!(select_adapters(&infos, false), vec![1]); + // select_adapters returns both, discrete first (integrated filtering is post-init) + assert_eq!(select_adapters(&infos), vec![1, 0]); } #[test] @@ -984,7 +1006,7 @@ mod adapter_selection_tests { wgpu::Backend::Vulkan, ), ]; - assert_eq!(select_adapters(&infos, false), vec![0, 1]); + assert_eq!(select_adapters(&infos), vec![0, 1]); } #[test] @@ -994,19 +1016,20 @@ mod adapter_selection_tests { wgpu::DeviceType::Cpu, wgpu::Backend::Vulkan, )]; - assert!(select_adapters(&infos, false).is_empty()); + assert!(select_adapters(&infos).is_empty()); } #[test] fn empty_enumeration_selects_nothing() { - assert!(select_adapters(&[], false).is_empty()); + assert!(select_adapters(&[]).is_empty()); } /// Exact scenario from Windows ASUS laptop with RX 560X + Vega 8 APU. /// Both GPUs appear on both Vulkan and Dx12 backends. - /// Expected: Only the discrete RX 560X on Vulkan should be selected. + /// select_adapters returns both Vulkan adapters (discrete first). + /// The init() function will later drop the integrated one if discrete succeeds. #[test] - fn windows_amd_discrete_plus_apu_uses_only_discrete() { + fn windows_amd_discrete_plus_apu_selects_both_on_best_backend() { let infos = [ info( "Microsoft Basic Render Driver", @@ -1034,26 +1057,11 @@ mod adapter_selection_tests { wgpu::Backend::Vulkan, ), ]; - // Should select only the discrete GPU on Vulkan (index 4) + // select_adapters returns both Vulkan adapters, discrete first // - Index 0: Skipped (CPU emulated) // - Index 1, 2: Skipped (Dx12 lower priority than Vulkan) - // - Index 3: Skipped (integrated, discrete available) - // - Index 4: Selected (discrete, Vulkan) - assert_eq!(select_adapters(&infos, false), vec![4]); - } - - /// Test --allow-integrated flag: when set, both discrete and integrated GPUs are used - #[test] - fn allow_integrated_flag_keeps_both_gpu_types() { - let infos = [ - info( - "iGPU", - wgpu::DeviceType::IntegratedGpu, - wgpu::Backend::Vulkan, - ), - info("dGPU", wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Vulkan), - ]; - // With allow_integrated=true, both GPUs should be selected (discrete first) - assert_eq!(select_adapters(&infos, true), vec![1, 0]); + // - Index 4: Selected first (discrete, Vulkan) + // - Index 3: Selected second (integrated, Vulkan) + assert_eq!(select_adapters(&infos), vec![4, 3]); } } From ce250e1d836beaeb751056894ef02bd39b42fd5d Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 30 Jun 2026 16:27:51 +0800 Subject: [PATCH 6/6] nits --- crates/engine-gpu/src/lib.rs | 123 +++++++++++++++++++++++++------- crates/miner-service/src/lib.rs | 26 ++++++- 2 files changed, 122 insertions(+), 27 deletions(-) diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 9438219..cb0a6b3 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -216,6 +216,35 @@ fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { selected } +/// Filter initialized GPUs based on device types. +/// Returns indices of GPUs to keep. +/// +/// Rules: +/// - If any discrete GPU initialized successfully and `allow_integrated` is false, +/// drop all integrated GPUs +/// - Otherwise keep all GPUs +/// +/// This is extracted as a pure function for testability. +fn filter_initialized_gpus( + device_types: &[wgpu::DeviceType], + allow_integrated: bool, +) -> Vec { + let has_discrete = device_types.contains(&wgpu::DeviceType::DiscreteGpu); + + if has_discrete && !allow_integrated { + // Keep only discrete GPUs + device_types + .iter() + .enumerate() + .filter(|(_, &dt)| dt != wgpu::DeviceType::IntegratedGpu) + .map(|(i, _)| i) + .collect() + } else { + // Keep all + (0..device_types.len()).collect() + } +} + impl GpuEngine { /// Try to initialize the GPU engine with the given batch size and throttle (ms between batches). /// @@ -392,31 +421,26 @@ impl GpuEngine { return Err("No GPU adapters could be initialized".into()); } - // Now filter integrated GPUs if discrete GPUs successfully initialized - // (unless allow_integrated is set) - let has_discrete = initialized - .iter() - .any(|g| g.device_type == wgpu::DeviceType::DiscreteGpu); - - let contexts: Vec> = if has_discrete && !allow_integrated { - initialized - .into_iter() - .filter(|g| { - if g.device_type == wgpu::DeviceType::IntegratedGpu { - log::info!( - target: "gpu_engine", - "Dropping integrated GPU (discrete GPU initialized successfully, use --allow-integrated to override): {}", - g.name - ); - return false; - } - true - }) - .map(|g| g.context) - .collect() - } else { - initialized.into_iter().map(|g| g.context).collect() - }; + // Filter integrated GPUs if discrete GPUs successfully initialized + let device_types: Vec<_> = initialized.iter().map(|g| g.device_type).collect(); + let keep_indices = filter_initialized_gpus(&device_types, allow_integrated); + + let contexts: Vec> = initialized + .into_iter() + .enumerate() + .filter_map(|(i, g)| { + if keep_indices.contains(&i) { + Some(g.context) + } else { + log::info!( + target: "gpu_engine", + "Dropping integrated GPU (discrete GPU initialized successfully, use --allow-integrated to override): {}", + g.name + ); + None + } + }) + .collect(); log::info!( target: "gpu_engine", @@ -1064,4 +1088,53 @@ mod adapter_selection_tests { // - Index 3: Selected second (integrated, Vulkan) assert_eq!(select_adapters(&infos), vec![4, 3]); } + + // Tests for filter_initialized_gpus (post-init filtering) + + #[test] + fn filter_discrete_present_drops_integrated() { + use wgpu::DeviceType::*; + // Discrete at index 0, integrated at index 1 + let types = vec![DiscreteGpu, IntegratedGpu]; + assert_eq!(filter_initialized_gpus(&types, false), vec![0]); + } + + #[test] + fn filter_discrete_failed_keeps_integrated() { + use wgpu::DeviceType::*; + // Only integrated initialized (discrete failed/timed out) + let types = vec![IntegratedGpu]; + assert_eq!(filter_initialized_gpus(&types, false), vec![0]); + } + + #[test] + fn filter_allow_integrated_keeps_both() { + use wgpu::DeviceType::*; + let types = vec![DiscreteGpu, IntegratedGpu]; + // With allow_integrated=true, keep both + assert_eq!(filter_initialized_gpus(&types, true), vec![0, 1]); + } + + #[test] + fn filter_multiple_discrete_keeps_all_discrete() { + use wgpu::DeviceType::*; + let types = vec![DiscreteGpu, DiscreteGpu, IntegratedGpu]; + // Drops integrated, keeps both discrete + assert_eq!(filter_initialized_gpus(&types, false), vec![0, 1]); + } + + #[test] + fn filter_only_discrete_keeps_all() { + use wgpu::DeviceType::*; + let types = vec![DiscreteGpu, DiscreteGpu]; + assert_eq!(filter_initialized_gpus(&types, false), vec![0, 1]); + } + + #[test] + fn filter_multiple_integrated_no_discrete_keeps_all() { + use wgpu::DeviceType::*; + let types = vec![IntegratedGpu, IntegratedGpu]; + // No discrete, so keep all integrated + assert_eq!(filter_initialized_gpus(&types, false), vec![0, 1]); + } } diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 124978c..9336590 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -208,12 +208,34 @@ impl WorkerPool { // Dispatch job to all workers using bounded channels (capacity 16). // Workers drain to get the latest job, so we just need room to queue. + let mut disconnected_count = 0; for (i, tx) in self.job_senders.iter().enumerate() { if let Err(e) = tx.try_send(job.clone()) { + match e { + crossbeam_channel::TrySendError::Disconnected(_) => { + // Worker thread has exited (e.g., device lost) + disconnected_count += 1; + log::debug!("Worker {i} channel disconnected (worker exited)"); + } + crossbeam_channel::TrySendError::Full(_) => { + log::warn!( + "Failed to send job {new_job_id} to worker {i}: channel full - \ + worker may be stuck or jobs arriving too fast" + ); + } + } + } + } + + if disconnected_count > 0 { + let active = self.job_senders.len() - disconnected_count; + if active == 0 { log::error!( - "Failed to send job {new_job_id} to worker {i}: {e}. \ - Channel full - worker may be stuck or jobs arriving too fast." + "All workers have exited! No workers available to process jobs. \ + Consider restarting the miner." ); + } else { + log::warn!("{disconnected_count} worker(s) have exited, {active} still active"); } }