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/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..8ad8215 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 { @@ -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 efc33d5..cb0a6b3 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 { @@ -160,6 +163,13 @@ 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 by default +/// to avoid resource contention and driver instability from mining on both +/// simultaneously. Set `allow_integrated` to true to override this behavior. +/// +/// 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| { @@ -196,6 +206,8 @@ fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec { }) .collect(); + // Sort discrete GPUs first, but keep all adapters for now + // (integrated filtering happens after init, based on what actually succeeded) selected.sort_by_key(|&i| match infos[i].device_type { wgpu::DeviceType::DiscreteGpu => 0, wgpu::DeviceType::IntegratedGpu => 1, @@ -204,15 +216,53 @@ 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). /// + /// # 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()); } @@ -221,17 +271,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, @@ -252,7 +308,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); @@ -341,19 +404,44 @@ 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()); } + // 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", "GPU engine initialized with {} devices (batch size: {} nonces, throttle: {}ms)", @@ -408,6 +496,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 - signal worker should exit + return EngineStatus::DeviceLost { hash_count: 0 }; + } + // Empty or inverted range: nothing to do. if range.start > range.end { return EngineStatus::Exhausted { hash_count: 0 }; @@ -559,15 +654,18 @@ 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. \ This GPU will not process further batches.", device_index ); - return EngineStatus::Cancelled { + return EngineStatus::DeviceLost { hash_count: total_hashes, }; } @@ -880,6 +978,8 @@ mod adapter_selection_tests { wgpu::Backend::Dx12, ), ]; + // 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]); } @@ -906,14 +1006,33 @@ mod adapter_selection_tests { } #[test] - fn dx12_only_machine_keeps_all_dx12_adapters() { + 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), ]; + // select_adapters returns both, discrete first (integrated filtering is post-init) assert_eq!(select_adapters(&infos), vec![1, 0]); } + #[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] fn software_only_environment_selects_nothing() { let infos = [info( @@ -928,4 +1047,94 @@ 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. + /// 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_selects_both_on_best_backend() { + 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, + ), + ]; + // select_adapters returns both Vulkan adapters, discrete first + // - Index 0: Skipped (CPU emulated) + // - Index 1, 2: Skipped (Dx12 lower priority than Vulkan) + // - Index 4: Selected first (discrete, Vulkan) + // - 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-cli/src/main.rs b/crates/miner-cli/src/main.rs index 1b4d627..8ab8568 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; @@ -274,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 3a503b8..9336590 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. @@ -206,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"); } } @@ -336,6 +360,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!( @@ -352,6 +377,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( @@ -400,6 +426,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) @@ -430,6 +471,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 +479,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 +524,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