Fix windows again (#72)
* handle lost device correctly * Update lib.rs * allow-integrated flag otherwise exclude * DeviceLost is a status * Integrated GPU skipped when discrete init fails * nits
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ pub struct GpuEngine {
|
||||
thread_local! {
|
||||
static ASSIGNED_GPU_DEVICE: RefCell<Option<usize>> = const { RefCell::new(None) };
|
||||
static WORKER_RESOURCES: RefCell<Option<GpuResources>> = 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<bool> = 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<usize> {
|
||||
let usable: Vec<usize> = (0..infos.len())
|
||||
.filter(|&i| {
|
||||
@@ -196,6 +206,8 @@ fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec<usize> {
|
||||
})
|
||||
.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<usize> {
|
||||
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<usize> {
|
||||
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<Self, Box<dyn std::error::Error>> {
|
||||
pub fn try_new(
|
||||
batch_size: u32,
|
||||
throttle_ms: u64,
|
||||
allow_integrated: bool,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
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<Self, Box<dyn std::error::Error>> {
|
||||
async fn init(
|
||||
batch_size: u32,
|
||||
throttle_ms: u64,
|
||||
allow_integrated: bool,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
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<Option<wgpu::Adapter>> = adapters.into_iter().map(Some).collect();
|
||||
let mut contexts = Vec::new();
|
||||
|
||||
// Track successfully initialized contexts with their device type
|
||||
struct InitializedGpu {
|
||||
context: Arc<GpuContext>,
|
||||
device_type: wgpu::DeviceType,
|
||||
name: String,
|
||||
}
|
||||
let mut initialized: Vec<InitializedGpu> = 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<Arc<GpuContext>> = 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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<usize>,
|
||||
batch_size: u32,
|
||||
throttle_ms: u64,
|
||||
allow_integrated: bool,
|
||||
) -> anyhow::Result<(Option<Arc<dyn MinerEngine>>, 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
|
||||
|
||||
Reference in New Issue
Block a user