Files
miner/crates/engine-gpu/src/lib.rs
T
2025-12-26 12:47:42 +08:00

650 lines
25 KiB
Rust

#![deny(rust_2018_idioms)]
#![forbid(unsafe_code)]
use engine_cpu::{Candidate, EngineStatus, FoundOrigin, MinerEngine, Range};
use futures::executor::block_on;
use pow_core::JobContext;
use primitive_types::U512;
use std::cell::RefCell;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
/// Represents a single GPU device context.
struct GpuContext {
device: wgpu::Device,
queue: wgpu::Queue,
pipeline: wgpu::ComputePipeline,
// Cached vendor configuration
optimal_workgroups: u32,
}
#[derive(Clone)]
struct GpuResources {
header_buffer: wgpu::Buffer,
target_buffer: wgpu::Buffer,
start_nonce_buffer: wgpu::Buffer,
results_buffer: wgpu::Buffer,
dispatch_config_buffer: wgpu::Buffer,
staging_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
pub struct GpuEngine {
contexts: Vec<Arc<GpuContext>>,
device_counter: AtomicUsize,
}
// Thread-local storage for consistent GPU device assignment per worker thread
thread_local! {
static ASSIGNED_GPU_DEVICE: RefCell<Option<usize>> = const { RefCell::new(None) };
static WORKER_RESOURCES: RefCell<Option<GpuResources>> = const { RefCell::new(None) };
}
impl GpuContext {
fn create_resources(&self) -> GpuResources {
let bind_group_layout = self.pipeline.get_bind_group_layout(0);
// Header: 8 u32s
let header_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Header Buffer"),
size: 32,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Target: 16 u32s
let target_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Target Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Start Nonce: 16 u32s
let start_nonce_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Start Nonce Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Results: [flag (1), nonce (16), hash (16)] = 33 u32s
let results_size = (1 + 16 + 16) * 4;
let results_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Results Buffer"),
size: results_size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Dispatch config: [total_threads, nonces_per_thread, workgroups, threads_per_workgroup] = 4 u32s
let dispatch_config_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Dispatch Config Buffer"),
size: 16,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Staging Buffer"),
size: results_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Mining Bind Group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: results_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: header_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: start_nonce_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: target_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: dispatch_config_buffer.as_entire_binding(),
},
],
});
GpuResources {
header_buffer,
target_buffer,
start_nonce_buffer,
results_buffer,
dispatch_config_buffer,
staging_buffer,
bind_group,
}
}
}
impl Default for GpuEngine {
fn default() -> Self {
Self::new()
}
}
impl GpuEngine {
pub fn new() -> Self {
block_on(Self::init()).expect("Failed to initialize GPU engine")
}
async fn init() -> Result<Self, Box<dyn std::error::Error>> {
log::info!("Initializing WGPU...");
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let adapters = instance.enumerate_adapters(wgpu::Backends::PRIMARY);
// 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!("No suitable GPU adapters found.");
return Err("No suitable GPU adapters found".into());
}
let mut contexts = Vec::new();
let mut adapter_infos = Vec::new();
for (i, adapter) in adapters.into_iter().enumerate() {
let info = adapter.get_info();
log::info!(
"Initializing GPU adapter {}: {} (Backend: {:?})",
i,
info.name,
info.backend
);
log::info!(target: "gpu_engine", "Adapter {} detailed info:", i);
log::info!(target: "gpu_engine", " Name: {}", info.name);
log::info!(target: "gpu_engine", " Vendor: {}", info.vendor);
log::info!(target: "gpu_engine", " Device: {}", info.device);
log::info!(target: "gpu_engine", " Device Type: {:?}", info.device_type);
log::info!(target: "gpu_engine", " Driver: {}", info.driver);
log::info!(target: "gpu_engine", " Driver Info: {}", info.driver_info);
log::info!(target: "gpu_engine", " Backend: {:?}", info.backend);
log::debug!(target: "gpu_engine", "Adapter {} full info: {:?}", i, info);
adapter_infos.push(info.clone());
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("Mining Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: Default::default(),
..Default::default()
})
.await?;
log::debug!(target: "gpu_engine", "Device and Queue requested for adapter {}", i);
log::info!(target: "gpu_engine", "Device limits for adapter {}:", i);
let limits = device.limits();
log::info!(target: "gpu_engine", " Max workgroups per dimension: {}", limits.max_compute_workgroups_per_dimension);
log::info!(target: "gpu_engine", " Max workgroup size X: {}", limits.max_compute_workgroup_size_x);
log::info!(target: "gpu_engine", " Max workgroup size Y: {}", limits.max_compute_workgroup_size_y);
log::info!(target: "gpu_engine", " Max workgroup size Z: {}", limits.max_compute_workgroup_size_z);
log::info!(target: "gpu_engine", " Max compute invocations per workgroup: {}", limits.max_compute_invocations_per_workgroup);
log::info!(target: "gpu_engine", " Max buffer size: {}", limits.max_buffer_size);
let shader_source = include_str!("mining.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Mining Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Mining Pipeline"),
layout: None,
module: &shader,
entry_point: Some("mining_main"),
compilation_options: Default::default(),
cache: None,
});
log::debug!(target: "gpu_engine", "Pipeline initialized for adapter {}", i);
// Calculate vendor-specific configuration once during initialization
let optimal_workgroups = Self::get_vendor_specific_dispatch(&info, &device);
contexts.push(Arc::new(GpuContext {
device,
queue,
pipeline,
optimal_workgroups,
}));
}
log::info!("GPU engine initialized with {} devices", contexts.len());
// Set engine backend info for metrics
#[cfg(feature = "metrics")]
{
metrics::set_gpu_device_count(contexts.len() as i64);
for (i, adapter_info) in adapter_infos.iter().enumerate() {
let device_id = format!("gpu-{}", i);
let backend_str = format!("{:?}", adapter_info.backend);
let vendor_str = format!("{}", adapter_info.vendor);
let device_type_str = format!("{:?}", adapter_info.device_type);
let clean_name = adapter_info.name.replace(" ", "_").replace(",", "");
// Set general engine backend info
metrics::set_engine_backend(&device_id, &backend_str);
// Set detailed GPU device info
metrics::set_gpu_device_info(
&device_id,
&clean_name,
&backend_str,
&vendor_str,
&device_type_str,
);
// Log GPU device info for monitoring
log::info!(
"📊 GPU Device {}: {} | Backend: {:?} | Vendor: {} | Device Type: {:?}",
i,
adapter_info.name,
adapter_info.backend,
adapter_info.vendor,
adapter_info.device_type
);
}
}
Ok(Self {
contexts,
device_counter: AtomicUsize::new(0),
})
}
/// Returns the number of GPU devices available
pub fn device_count(&self) -> usize {
self.contexts.len()
}
/// Explicitly clear thread-local GPU resources.
/// Call this before thread exit to avoid TLS destruction order issues with wgpu.
pub fn clear_worker_resources() {
WORKER_RESOURCES.with(|resources| {
*resources.borrow_mut() = None;
});
}
}
impl MinerEngine for GpuEngine {
fn name(&self) -> &'static str {
"gpu-wgpu"
}
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
JobContext::new(header_hash, difficulty)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
if self.contexts.is_empty() {
log::warn!("No GPUs available for search.");
return EngineStatus::Exhausted { hash_count: 0 };
}
// Empty or inverted range: nothing to do.
if range.start > range.end {
return EngineStatus::Exhausted { hash_count: 0 };
}
// Use thread-local assignment for consistent worker-to-GPU mapping
let device_index = ASSIGNED_GPU_DEVICE.with(|assigned| {
let mut assigned_ref = assigned.borrow_mut();
if let Some(index) = *assigned_ref {
// This thread already has a GPU assigned
index
} else {
// First time this thread is calling search_range, assign a GPU device
let index = if self.contexts.len() == 1 {
0
} else {
self.device_counter.fetch_add(1, Ordering::SeqCst) % self.contexts.len()
};
*assigned_ref = Some(index);
log::info!(
"Worker thread assigned to GPU device {} (of {} total devices)",
index,
self.contexts.len()
);
index
}
});
let gpu_ctx = &self.contexts[device_index];
log::debug!(
"GPU device {} processing range {}..={} (inclusive)",
device_index,
range.start,
range.end
);
// Ensure resources are initialized for this thread
WORKER_RESOURCES.with(|resources_cell| {
let mut resources = resources_cell.borrow_mut();
if resources.is_none() {
*resources = Some(gpu_ctx.create_resources());
}
});
// Clone resources to use outside the closure (they are cheap handles)
let resources = WORKER_RESOURCES
.with(|resources_cell| resources_cell.borrow().as_ref().unwrap().clone());
// Pre-convert header and target once (not per range)
let mut header_u32s = [0u32; 8];
for (i, item) in header_u32s.iter_mut().enumerate() {
let chunk = &ctx.header[i * 4..(i + 1) * 4];
*item = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
gpu_ctx.queue.write_buffer(
&resources.header_buffer,
0,
bytemuck::cast_slice(&header_u32s),
);
let target_bytes = ctx.target.to_little_endian();
let mut target_u32s = [0u32; 16];
for i in 0..16 {
let chunk = &target_bytes[i * 4..(i + 1) * 4];
target_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
gpu_ctx.queue.write_buffer(
&resources.target_buffer,
0,
bytemuck::cast_slice(&target_u32s),
);
// Length of the inclusive nonce range
let total_range_size = (range.end - range.start + 1).as_u64();
if total_range_size == 0 {
return EngineStatus::Exhausted { hash_count: 0 };
}
// Thread configuration for a single dispatch over this range.
let threads_per_workgroup = 256u32; // Must match shader @workgroup_size(256)
let limits = gpu_ctx.device.limits();
let max_workgroups = limits.max_compute_workgroups_per_dimension;
// Vendor hint, clamped by hardware limits.
let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups);
let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64;
// Choose a logical thread budget: enough threads to fill the GPU, but no more than
// the range length (spawning more threads than nonces is wasteful).
let mut logical_threads = total_range_size.min(hinted_threads);
if logical_threads == 0 {
logical_threads = 1;
}
// Round logical_threads up to a multiple of workgroup size so we have full workgroups.
let mut num_workgroups = (logical_threads as u32).div_ceil(threads_per_workgroup);
if num_workgroups == 0 {
num_workgroups = 1;
}
let total_threads = (num_workgroups * threads_per_workgroup) as u64;
// Derive how many nonces each logical thread should process so that the entire
// range is covered in a single dispatch.
let nonces_per_thread = total_range_size.div_ceil(total_threads).max(1) as u32;
let total_threads_u32 = total_threads as u32;
log::info!(
target: "gpu_engine",
"GPU dispatch configuration: total_range={} nonces, workgroups={}, threads={}, nonces_per_thread={}",
total_range_size,
num_workgroups,
total_threads_u32,
nonces_per_thread
);
// We'll process the full range in a single dispatch.
let hash_count = total_range_size;
// Eliminate intermediate syncs - only sync at end or when solution found
const RESULTS_SIZE: usize = (1 + 16 + 16) * 4;
const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE];
#[cfg(feature = "metrics")]
{
let device_id = "gpu-0";
metrics::set_gpu_batch_size(device_id, total_range_size as f64);
metrics::set_gpu_workgroups(device_id, num_workgroups as f64);
}
if cancel.load(Ordering::Relaxed) {
log::debug!(target: "gpu_engine", "GPU {} cancelled before dispatch.", device_index);
return EngineStatus::Cancelled { hash_count: 0 };
}
// Dispatch configuration for this range:
// [total_threads, nonces_per_thread, total_nonces, threads_per_workgroup]
let total_nonces_u32 = total_range_size.min(u32::MAX as u64) as u32;
let dispatch_config = [
total_threads_u32,
nonces_per_thread,
total_nonces_u32,
threads_per_workgroup,
];
gpu_ctx.queue.write_buffer(
&resources.dispatch_config_buffer,
0,
bytemuck::cast_slice(&dispatch_config),
);
// Starting nonce for this range.
let start_nonce_bytes = range.start.to_little_endian();
gpu_ctx
.queue
.write_buffer(&resources.start_nonce_buffer, 0, &start_nonce_bytes);
// Reset results buffer to detect solutions from this dispatch.
gpu_ctx
.queue
.write_buffer(&resources.results_buffer, 0, &ZEROS);
let total_start = std::time::Instant::now();
// Create command buffer for this range
let mut encoder = gpu_ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
cpass.set_pipeline(&gpu_ctx.pipeline);
cpass.set_bind_group(0, &resources.bind_group, &[]);
cpass.dispatch_workgroups(num_workgroups, 1, 1);
}
encoder.copy_buffer_to_buffer(
&resources.results_buffer,
0,
&resources.staging_buffer,
0,
RESULTS_SIZE as u64,
);
// Submit and wait for completion
gpu_ctx.queue.submit(Some(encoder.finish()));
let buffer_slice = resources.staging_buffer.slice(..);
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
let _ = gpu_ctx.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
let data = buffer_slice.get_mapped_range();
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
if result_u32s[0] != 0 {
// Solution found!
let nonce_u32s = &result_u32s[1..17];
let hash_u32s = &result_u32s[17..33];
let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s));
let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s));
let work = nonce.to_big_endian();
log::info!(
"GPU {} found solution! Nonce: {}, Hash: {:x}",
device_index,
nonce,
hash
);
drop(data);
resources.staging_buffer.unmap();
return EngineStatus::Found {
candidate: Candidate { nonce, work, hash },
// Approximate: we assume a uniform distribution within the dispatch
// and report that we processed the entire range.
hash_count,
origin: FoundOrigin::GpuG1,
};
}
drop(data);
resources.staging_buffer.unmap();
let total_time = total_start.elapsed();
log::info!(
target: "gpu_engine",
"GPU finished range. Total time: {:.2}ms, Batches: 1, Hashes: {}, Performance: {:.0} hashes/sec",
total_time.as_secs_f64() * 1000.0,
hash_count,
hash_count as f64 / total_time.as_secs_f64()
);
log::debug!(target: "gpu_engine", "GPU {} finished range with no solution.", device_index);
EngineStatus::Exhausted { hash_count }
}
}
impl GpuEngine {
/// Get vendor-specific optimal dispatch configuration
fn get_vendor_specific_dispatch(
adapter_info: &wgpu::AdapterInfo,
device: &wgpu::Device,
) -> u32 {
let limits = device.limits();
let max_workgroups = limits.max_compute_workgroups_per_dimension.min(65535);
// Parse vendor from adapter info
let vendor_name = adapter_info.name.to_lowercase();
let _device_name = adapter_info.device.to_string().to_lowercase();
// Vendor-specific heuristics based on architecture knowledge
let optimal_workgroups = if vendor_name.contains("nvidia") || adapter_info.vendor == 4318 {
// NVIDIA GPUs (vendor ID 0x10DE = 4318)
if vendor_name.contains("rtx 40") || vendor_name.contains("rtx 4090") {
(max_workgroups / 8).max(4096)
} else if vendor_name.contains("rtx 30") || vendor_name.contains("rtx 20") {
(max_workgroups / 12).max(2048)
} else if vendor_name.contains("gtx") || vendor_name.contains("rtx 16") {
(max_workgroups / 16).max(1024)
} else {
(max_workgroups / 20).max(512)
}
} else if vendor_name.contains("amd") || adapter_info.vendor == 4098 {
// AMD GPUs (vendor ID 0x1002 = 4098)
if vendor_name.contains("rx 7") || vendor_name.contains("rx 6900") {
(max_workgroups / 10).max(3072)
} else if vendor_name.contains("rx 6") || vendor_name.contains("rx 5700") {
(max_workgroups / 14).max(2048)
} else if vendor_name.contains("rx 5") || vendor_name.contains("rx 580") {
(max_workgroups / 18).max(1024)
} else {
(max_workgroups / 24).max(512)
}
} else if vendor_name.contains("intel") || adapter_info.vendor == 32902 {
// Intel GPUs (vendor ID 0x8086 = 32902)
if vendor_name.contains("arc a7") || vendor_name.contains("arc a770") {
(max_workgroups / 12).max(2048)
} else if vendor_name.contains("arc a5") || vendor_name.contains("arc a380") {
(max_workgroups / 16).max(1024)
} else if vendor_name.contains("iris xe") {
(max_workgroups / 20).max(512)
} else {
(max_workgroups / 24).max(256)
}
} else if adapter_info.backend == wgpu::Backend::Metal {
// Apple GPUs (detected by Metal backend)
let (gpu_cores, workgroups) = if vendor_name.contains("m4 max") {
(40, 800)
} else if vendor_name.contains("m4 pro") {
(20, 400)
} else if vendor_name.contains("m4") {
(10, 200)
} else if vendor_name.contains("m3 max") {
(40, 800)
} else if vendor_name.contains("m3 pro") {
(18, 360)
} else if vendor_name.contains("m3") {
(10, 200)
} else if vendor_name.contains("m2 ultra") {
(76, 1520)
} else if vendor_name.contains("m2 max") {
(38, 760)
} else if vendor_name.contains("m2 pro") {
(19, 380)
} else if vendor_name.contains("m2") {
(10, 200)
} else if vendor_name.contains("m1 ultra") {
(64, 1280)
} else if vendor_name.contains("m1 max") {
(32, 640)
} else if vendor_name.contains("m1 pro") {
(16, 320)
} else {
(8, 160)
};
let clamped_workgroups = workgroups.min(max_workgroups / 4).max(64);
let _ = gpu_cores; // gpu_cores currently unused but kept for potential future tuning
clamped_workgroups
} else {
// Unknown/Generic GPU - use conservative defaults
(max_workgroups / 16).max(512)
};
log::info!(target: "gpu_engine", "Vendor-specific dispatch configuration:");
log::info!(target: "gpu_engine", " Max hardware workgroups: {}", max_workgroups);
log::info!(target: "gpu_engine", " Optimal workgroups: {}", optimal_workgroups);
optimal_workgroups
}
}