Select GPU adapters by backend to fix multi-backend OOM (#67)

On Windows wgpu enumerates each physical GPU once per backend (Vulkan +
DX12) plus a CPU-emulated fallback ("Microsoft Basic Render Driver").
Building a mining context for every entry causes VRAM contention and
OOMs the process during benchmark/serve startup (#61).

Instead of deduplicating by (vendor, device) PCI IDs - which would
collapse rigs with multiple identical cards into a single context - drop
CPU-emulated adapters and keep all adapters from the highest-ranked
backend present (Vulkan/Metal, then DX12). Within a single backend each
physical GPU appears exactly once, so identical cards are preserved by
construction and no physical-ID matching is needed.

Selected adapters are ordered discrete-first so `--gpu-devices 1` picks
the discrete card on hybrid laptops. Skipped adapters are logged at info
level; if nothing usable remains, init fails with an explicit error.

Selection logic is a pure index-based function unit-tested against the
exact enumeration reported in #61, identical multi-GPU rigs, DX12-only
machines, and software-only environments.

Co-authored-by: illuzen <illuzen@users.noreply.github.com>
This commit is contained in:
Nikolaus Heger
2026-06-30 13:13:45 +08:00
committed by GitHub
co-authored by illuzen
parent 7296c003bb
commit d99be29a64
2 changed files with 193 additions and 37 deletions
+192 -36
View File
@@ -142,6 +142,68 @@ impl GpuContext {
}
}
/// Rank backends for mining: native compute APIs first.
fn backend_rank(backend: wgpu::Backend) -> u8 {
match backend {
wgpu::Backend::Vulkan | wgpu::Backend::Metal => 0,
wgpu::Backend::Dx12 => 1,
_ => 2,
}
}
/// Select which adapters to mine on, returning indices into `infos` ordered
/// discrete-first.
///
/// wgpu can enumerate the same physical GPU once per backend (Vulkan + DX12 on
/// Windows) plus CPU-emulated fallbacks such as "Microsoft Basic Render Driver".
/// Mining on every entry causes VRAM contention and OOM (issue #61), so CPU
/// 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.
fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec<usize> {
let usable: Vec<usize> = (0..infos.len())
.filter(|&i| {
if infos[i].device_type == wgpu::DeviceType::Cpu {
log::info!(
target: "gpu_engine",
"Skipping CPU-emulated adapter: {} ({:?})",
infos[i].name,
infos[i].backend
);
return false;
}
true
})
.collect();
let Some(best) = usable.iter().map(|&i| backend_rank(infos[i].backend)).min() else {
return Vec::new();
};
let mut selected: Vec<usize> = usable
.into_iter()
.filter(|&i| {
if backend_rank(infos[i].backend) != best {
log::info!(
target: "gpu_engine",
"Skipping adapter on lower-priority backend: {} ({:?})",
infos[i].name,
infos[i].backend
);
return false;
}
true
})
.collect();
selected.sort_by_key(|&i| match infos[i].device_type {
wgpu::DeviceType::DiscreteGpu => 0,
wgpu::DeviceType::IntegratedGpu => 1,
_ => 2,
});
selected
}
impl GpuEngine {
/// Try to initialize the GPU engine with the given batch size and throttle (ms between batches).
///
@@ -149,7 +211,7 @@ impl GpuEngine {
///
/// Returns an error if:
/// - `batch_size` is zero (no work would be performed)
/// - No suitable GPU adapters are found
/// - No usable GPU adapters are found
pub fn try_new(batch_size: u32, throttle_ms: u64) -> Result<Self, Box<dyn std::error::Error>> {
if batch_size == 0 {
return Err("batch_size must be non-zero".into());
@@ -177,40 +239,35 @@ impl GpuEngine {
});
let adapters = instance.enumerate_adapters(wgpu::Backends::PRIMARY);
let infos: Vec<wgpu::AdapterInfo> = adapters.iter().map(|a| a.get_info()).collect();
// Collect adapters to a vector to check count and iterate with index
let adapters: Vec<_> = adapters.into_iter().collect();
if adapters.is_empty() {
log::error!(target: "gpu_engine", "No suitable GPU adapters found.");
return Err("No suitable GPU adapters found".into());
let selected = select_adapters(&infos);
if selected.is_empty() {
log::error!(
target: "gpu_engine",
"No usable GPU adapters found ({} enumerated). Use --gpu-devices 0 to disable GPU mining.",
infos.len()
);
return Err("No usable GPU adapters found".into());
}
let mut adapters: Vec<Option<wgpu::Adapter>> = adapters.into_iter().map(Some).collect();
let mut contexts = Vec::new();
let mut adapter_infos = Vec::new();
// Timeout for initializing each adapter (30 seconds should be plenty)
let init_timeout = std::time::Duration::from_secs(30);
for (i, adapter) in adapters.into_iter().enumerate() {
let info = adapter.get_info();
log::info!(target: "gpu_engine", "Initializing adapter {}: {} ...", i, info.name);
// Skip software renderers - they're too slow for mining
if info.name.to_lowercase().contains("microsoft basic")
|| info.name.to_lowercase().contains("software")
|| info.name.to_lowercase().contains("llvmpipe")
|| info.device_type == wgpu::DeviceType::Cpu
{
log::warn!(
target: "gpu_engine",
"Skipping software renderer adapter {}: {}",
i, info.name
);
continue;
}
adapter_infos.push(info.clone());
for (i, idx) in selected.into_iter().enumerate() {
let adapter = adapters[idx].take().expect("adapter selected exactly once");
let info = &infos[idx];
log::info!(
target: "gpu_engine",
"Initializing GPU device {i}: {} ({:?}, {:?})",
info.name,
info.device_type,
info.backend
);
log::debug!(target: "gpu_engine", "Adapter {i} raw info: {info:?}");
// Try to initialize this adapter with a proper timeout.
// If the driver hangs, we'll skip this adapter after the timeout.
@@ -227,8 +284,8 @@ impl GpuEngine {
Err(_) => {
log::warn!(
target: "gpu_engine",
"Adapter {} ({}) timed out after {}s during initialization, skipping",
i, info.name, init_timeout.as_secs()
"GPU device {i} ({}) timed out after {}s during initialization, skipping",
info.name, init_timeout.as_secs()
);
continue;
}
@@ -239,8 +296,8 @@ impl GpuEngine {
Err(e) => {
log::warn!(
target: "gpu_engine",
"Failed to initialize adapter {} ({}): {}. Skipping.",
i, info.name, e
"Failed to initialize GPU device {i} ({}): {}. Skipping.",
info.name, e
);
continue;
}
@@ -248,8 +305,7 @@ impl GpuEngine {
// Log device limits at debug level
let limits = device.limits();
log::debug!(target: "gpu_engine", "Adapter {} limits: max_workgroups={}, max_workgroup_size={}x{}x{}, max_buffer={}",
i,
log::debug!(target: "gpu_engine", "GPU device {i} limits: max_workgroups={}, max_workgroup_size={}x{}x{}, max_buffer={}",
limits.max_compute_workgroups_per_dimension,
limits.max_compute_workgroup_size_x,
limits.max_compute_workgroup_size_y,
@@ -278,12 +334,12 @@ impl GpuEngine {
let pipeline_elapsed = pipeline_start.elapsed();
log::info!(
target: "gpu_engine",
"Adapter {} ({}) initialized successfully (pipeline compiled in {:.1}s)",
i, info.name, pipeline_elapsed.as_secs_f64()
"GPU device {i} ({}) initialized successfully (pipeline compiled in {:.1}s)",
info.name, pipeline_elapsed.as_secs_f64()
);
// Calculate vendor-specific configuration once during initialization
let optimal_workgroups = get_vendor_specific_dispatch(&info, &device);
let optimal_workgroups = get_vendor_specific_dispatch(info, &device);
contexts.push(Arc::new(GpuContext {
device,
@@ -773,3 +829,103 @@ fn get_vendor_specific_dispatch(adapter_info: &wgpu::AdapterInfo, device: &wgpu:
optimal_workgroups
}
#[cfg(test)]
mod adapter_selection_tests {
use super::*;
fn info(
name: &str,
device_type: wgpu::DeviceType,
backend: wgpu::Backend,
) -> wgpu::AdapterInfo {
wgpu::AdapterInfo {
name: name.into(),
vendor: 0,
device: 0,
device_type,
driver: String::new(),
driver_info: String::new(),
backend,
}
}
#[test]
fn windows_multi_backend_keeps_one_context_per_physical_gpu() {
// The exact enumeration from issue #61
let infos = [
info(
"AMD Radeon(TM) Graphics",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Vulkan,
),
info(
"NVIDIA GeForce RTX 3070",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
info(
"AMD Radeon(TM) Graphics",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Dx12,
),
info(
"NVIDIA GeForce RTX 3070",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Dx12,
),
info(
"Microsoft Basic Render Driver",
wgpu::DeviceType::Cpu,
wgpu::Backend::Dx12,
),
];
assert_eq!(select_adapters(&infos), vec![1, 0]);
}
#[test]
fn identical_multi_gpu_rig_keeps_every_card() {
let infos = [
info(
"RTX 3090",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
info(
"RTX 3090",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
info(
"RTX 3090",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
];
assert_eq!(select_adapters(&infos), vec![0, 1, 2]);
}
#[test]
fn dx12_only_machine_keeps_all_dx12_adapters() {
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]);
}
#[test]
fn software_only_environment_selects_nothing() {
let infos = [info(
"llvmpipe",
wgpu::DeviceType::Cpu,
wgpu::Backend::Vulkan,
)];
assert!(select_adapters(&infos).is_empty());
}
#[test]
fn empty_enumeration_selects_nothing() {
assert!(select_adapters(&[]).is_empty());
}
}
+1 -1
View File
@@ -380,7 +380,7 @@ fn worker_loop(
..
} => {
log::info!(
"{type_str} worker {thread_id} found solution! Nonce: {}, Hash: {} (job {job_id})",
"🎯 {type_str} worker {thread_id} found solution! Nonce: {}, Hash: {} (job {job_id})",
format_u512(nonce),
format_u512(hash),
);