remove gpu feature, rename to gpu-devices

rename gpu-workers flag to devices, also check user doesn't specify more than they have (degrades performance)
This commit is contained in:
Nikolaus Heger
2025-12-26 13:44:59 +08:00
parent 23c617bda9
commit f0aa971326
5 changed files with 90 additions and 104 deletions
-4
View File
@@ -71,25 +71,21 @@ jobs:
target: x86_64-unknown-linux-gnu
binary_name: quantus-miner
asset_name: quantus-miner-linux-x86_64
features: "gpu"
# Windows builds
- os: windows-latest
target: x86_64-pc-windows-msvc
binary_name: quantus-miner.exe
asset_name: quantus-miner-windows-x86_64.exe
features: "gpu"
# macOS builds (Intel)
- os: macos-15-intel
target: x86_64-apple-darwin
binary_name: quantus-miner
asset_name: quantus-miner-macos-x86_64
features: "gpu"
# macOS builds (Apple Silicon)
- os: macos-latest
target: aarch64-apple-darwin
binary_name: quantus-miner
asset_name: quantus-miner-macos-aarch64
features: "gpu"
steps:
- uses: actions/checkout@v4
-4
View File
@@ -5,10 +5,6 @@ edition.workspace = true
publish = false
description = "CLI binary to run the Quantus External Miner service"
[features]
default = ["gpu"]
gpu = ["miner-service/gpu"]
[dependencies]
miner-service = { path = "../miner-service" }
clap = { workspace = true, features = ["derive", "env"] }
+34 -48
View File
@@ -21,9 +21,9 @@ enum Command {
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
cpu_workers: Option<usize>,
/// Number of GPU worker threads to use for mining (None = auto-detect)
#[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")]
gpu_workers: Option<usize>,
/// Number of GPU devices to use for mining (None = auto-detect)
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
gpu_devices: Option<usize>,
/// Optional Prometheus metrics exporter port; if omitted, metrics are disabled
#[arg(long, env = "MINER_METRICS_PORT")]
@@ -109,9 +109,9 @@ enum Command {
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
cpu_workers: Option<usize>,
/// Number of GPU workers to use for benchmark
#[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")]
gpu_workers: Option<usize>,
/// Number of GPU devices to use for benchmark
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
gpu_devices: Option<usize>,
/// Benchmark duration in seconds (default: 10)
#[arg(short, long, default_value_t = 10)]
@@ -139,7 +139,7 @@ async fn main() {
match args.command.unwrap_or(Command::Serve {
port: 9833,
cpu_workers: None,
gpu_workers: None,
gpu_devices: None,
metrics_port: None,
verbose: false,
progress_interval_ms: None,
@@ -162,7 +162,7 @@ async fn main() {
Command::Serve {
port,
cpu_workers,
gpu_workers,
gpu_devices,
metrics_port,
verbose,
progress_interval_ms,
@@ -185,7 +185,7 @@ async fn main() {
run_serve_command(
port,
cpu_workers,
gpu_workers,
gpu_devices,
metrics_port,
verbose,
progress_interval_ms,
@@ -209,11 +209,11 @@ async fn main() {
}
Command::Benchmark {
cpu_workers,
gpu_workers,
gpu_devices,
duration,
verbose,
} => {
run_benchmark_command(cpu_workers, gpu_workers, duration, verbose).await;
run_benchmark_command(cpu_workers, gpu_devices, duration, verbose).await;
}
}
}
@@ -222,7 +222,7 @@ async fn main() {
async fn run_serve_command(
port: u16,
cpu_workers: Option<usize>,
gpu_workers: Option<usize>,
gpu_devices: Option<usize>,
metrics_port: Option<u16>,
verbose: bool,
progress_interval_ms: Option<u64>,
@@ -294,7 +294,7 @@ async fn run_serve_command(
let config = ServiceConfig {
port,
cpu_workers,
gpu_workers,
gpu_devices,
metrics_port,
progress_interval_ms,
chunk_size,
@@ -313,7 +313,7 @@ async fn run_serve_command(
async fn run_benchmark_command(
cpu_workers: Option<usize>,
gpu_workers: Option<usize>,
gpu_devices: Option<usize>,
duration: u64,
verbose: bool,
) {
@@ -329,8 +329,8 @@ async fn run_benchmark_command(
env_logger::init();
let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get);
let effective_gpu_workers = gpu_workers.unwrap_or(0);
let total_workers = effective_cpu_workers + effective_gpu_workers;
let effective_gpu_devices = gpu_devices.unwrap_or(0);
let total_workers = effective_cpu_workers + effective_gpu_devices;
println!("🚀 Quantus Miner Benchmark");
println!("==========================");
@@ -340,7 +340,7 @@ async fn run_benchmark_command(
effective_cpu_workers,
num_cpus::get()
);
println!(" GPU Workers: {}", effective_gpu_workers);
println!(" GPU Devices: {}", effective_gpu_devices);
println!("Duration: {} seconds", duration);
println!("Total Workers: {}", total_workers);
@@ -351,16 +351,8 @@ async fn run_benchmark_command(
None
};
let gpu_engine = if effective_gpu_workers > 0 {
#[cfg(feature = "gpu")]
{
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
}
#[cfg(not(feature = "gpu"))]
{
eprintln!("Error: GPU workers requested but this binary was built without GPU support");
std::process::exit(1);
}
let gpu_engine = if effective_gpu_devices > 0 {
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
} else {
None
};
@@ -400,26 +392,22 @@ async fn run_benchmark_command(
// GPU chunk size from 1 - 10M is good
let gpu_chunk = 1_000_000u64;
if effective_gpu_workers > 0 {
if effective_gpu_devices > 0 {
if let Some(gpu_engine_arc) = &gpu_engine {
// We can't easily get device count from MinerEngine trait without downcasting or extending trait.
// But we know it's GpuEngine here if feature=gpu is enabled.
#[cfg(feature = "gpu")]
// But we know it's GpuEngine here.
if let Some(gpu_ptr) = gpu_engine_arc
.as_any()
.downcast_ref::<engine_gpu::GpuEngine>()
{
if let Some(gpu_ptr) = gpu_engine_arc
.as_any()
.downcast_ref::<engine_gpu::GpuEngine>()
{
let device_count = gpu_ptr.device_count();
if effective_gpu_workers > device_count {
println!(
"⚠️ WARNING: You have requested {} GPU workers for {} device(s).",
effective_gpu_workers, device_count
);
println!(" Multiple workers per device creates contention and typically reduces hashrate.");
println!(" Recommended: 1 worker per GPU device.");
println!();
}
let device_count = gpu_ptr.device_count();
if effective_gpu_devices > device_count {
eprintln!(
"❌ ERROR: You have requested {} GPU devices but only {} device(s) are available.",
effective_gpu_devices, device_count
);
eprintln!(" Please check your --gpu-devices setting or GPU hardware.");
std::process::exit(1);
}
}
}
@@ -484,10 +472,8 @@ async fn run_benchmark_command(
}
}
// Cleanup TLS resources explicitly to avoid panic on exit
#[cfg(feature = "gpu")]
engine_gpu::GpuEngine::clear_worker_resources();
worker_hashes
});
@@ -520,7 +506,7 @@ async fn run_benchmark_command(
format!("{:.0}", hash_rate)
};
println!("⏱️ {:.1}s - {} H/s", elapsed, hash_rate_str);
} else if effective_gpu_workers > 0 {
} else if effective_gpu_devices > 0 {
println!("⏱️ {:.1}s - processing...", elapsed);
} else {
println!("⏱️ {:.1}s - starting...", elapsed);
+2 -4
View File
@@ -6,11 +6,9 @@ publish = false
description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner"
[features]
default = ["cpu", "gpu", "metrics"]
default = ["cpu", "metrics"]
# Enable CPU engine by default.
cpu = ["engine-cpu"]
# Enable GPU engine.
gpu = ["engine-gpu"]
# Optional metrics/observability (Prometheus endpoint).
metrics = [
"dep:metrics",
@@ -40,6 +38,6 @@ quantus-miner-api = { workspace = true }
# Local crates
pow-core = { path = "../pow-core" }
engine-cpu = { path = "../engine-cpu", optional = true }
engine-gpu = { path = "../engine-gpu", optional = true }
engine-gpu = { path = "../engine-gpu" }
metrics = { path = "../metrics", optional = true }
miner-telemetry = { path = "../miner-telemetry" }
+54 -44
View File
@@ -23,8 +23,8 @@ pub struct ServiceConfig {
pub port: u16,
/// Number of CPU worker threads to use for mining (None = auto-detect)
pub cpu_workers: Option<usize>,
/// Number of GPU worker threads to use for mining (None = auto-detect)
pub gpu_workers: Option<usize>,
/// Number of GPU devices to use for mining (None = auto-detect)
pub gpu_devices: Option<usize>,
/// Optional metrics port. When Some, metrics endpoint starts; when None, metrics are disabled.
pub metrics_port: Option<u16>,
/// How often to report mining progress (in milliseconds). If None, defaults to 10000ms.
@@ -46,7 +46,7 @@ impl Default for ServiceConfig {
Self {
port: 9833,
cpu_workers: None,
gpu_workers: None,
gpu_devices: None,
metrics_port: None,
progress_interval_ms: None,
chunk_size: None,
@@ -62,10 +62,10 @@ impl fmt::Display for ServiceConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"port={}, cpu_workers={:?}, gpu_workers={:?}, metrics_port={:?}, progress_interval_ms={:?}, chunk_size={:?}, manip_solved_blocks={:?}, manip_base_delay_ns={:?}, manip_step_batch={:?}, manip_throttle_cap={:?}",
"port={}, cpu_workers={:?}, gpu_devices={:?}, metrics_port={:?}, progress_interval_ms={:?}, chunk_size={:?}, manip_solved_blocks={:?}, manip_base_delay_ns={:?}, manip_step_batch={:?}, manip_throttle_cap={:?}",
self.port,
self.cpu_workers,
self.gpu_workers,
self.gpu_devices,
self.metrics_port,
self.progress_interval_ms,
self.chunk_size,
@@ -82,7 +82,7 @@ impl fmt::Display for ServiceConfig {
pub struct MiningService {
pub jobs: Arc<Mutex<HashMap<String, MiningJob>>>,
pub cpu_workers: usize,
pub gpu_workers: usize,
pub gpu_devices: usize,
pub cpu_engine: Option<Arc<dyn MinerEngine>>,
pub gpu_engine: Option<Arc<dyn MinerEngine>>,
/// How often to report mining progress (in milliseconds).
@@ -96,7 +96,7 @@ pub struct MiningService {
impl MiningService {
fn new(
cpu_workers: usize,
gpu_workers: usize,
gpu_devices: usize,
cpu_engine: Option<Arc<dyn MinerEngine>>,
gpu_engine: Option<Arc<dyn MinerEngine>>,
progress_interval_ms: u64,
@@ -105,7 +105,7 @@ impl MiningService {
Self {
jobs: Arc::new(Mutex::new(HashMap::new())),
cpu_workers,
gpu_workers,
gpu_devices,
cpu_engine,
gpu_engine,
progress_interval_ms,
@@ -124,14 +124,14 @@ impl MiningService {
log::info!(
"Adding mining job: {} with {} workers",
job_id,
self.cpu_workers + self.gpu_workers
self.cpu_workers + self.gpu_devices
);
job.job_id = Some(job_id.clone());
#[cfg(feature = "metrics")]
{
let engine_name = if self.cpu_workers > 0 && self.gpu_workers > 0 {
let engine_name = if self.cpu_workers > 0 && self.gpu_devices > 0 {
"hybrid"
} else if self.gpu_workers > 0 {
} else if self.gpu_devices > 0 {
"gpu"
} else {
"cpu"
@@ -149,7 +149,7 @@ impl MiningService {
}
job.start_mining(
self.cpu_workers,
self.gpu_workers,
self.gpu_devices,
self.cpu_engine.clone(),
self.gpu_engine.clone(),
self.progress_interval_ms,
@@ -439,13 +439,13 @@ impl MiningJob {
pub fn start_mining(
&mut self,
cpu_workers: usize,
gpu_workers: usize,
gpu_devices: usize,
cpu_engine: Option<Arc<dyn MinerEngine>>,
gpu_engine: Option<Arc<dyn MinerEngine>>,
progress_interval_ms: u64,
chunk_size: Option<u64>,
) {
let total_workers = cpu_workers + gpu_workers;
let total_workers = cpu_workers + gpu_devices;
let chan_capacity = std::env::var("MINER_RESULT_CHANNEL_CAP")
.ok()
.and_then(|v| v.parse::<usize>().ok())
@@ -454,9 +454,9 @@ impl MiningJob {
self.result_receiver = Some(receiver);
// Set engine name based on what's available
self.engine_name = if cpu_workers > 0 && gpu_workers > 0 {
self.engine_name = if cpu_workers > 0 && gpu_devices > 0 {
"hybrid"
} else if gpu_workers > 0 {
} else if gpu_devices > 0 {
"gpu"
} else {
"cpu"
@@ -473,7 +473,7 @@ impl MiningJob {
log::info!(
"Starting mining with {} CPU + {} GPU workers, total range: {}",
cpu_workers,
gpu_workers,
gpu_devices,
total_range
);
@@ -524,10 +524,10 @@ impl MiningJob {
}
// Create GPU worker threads
if gpu_workers > 0 {
if gpu_devices > 0 {
if let Some(gpu_engine) = gpu_engine {
let ctx = gpu_engine.prepare_context(self.header_hash, self.difficulty);
let gpu_partitions = compute_partitions(current_start, self.nonce_end, gpu_workers);
let gpu_partitions = compute_partitions(current_start, self.nonce_end, gpu_devices);
for (start, end) in gpu_partitions.ranges.into_iter() {
let cancel_flag = self.cancel_flag.clone();
@@ -934,7 +934,6 @@ fn mine_range_with_engine_typed(
}
// Explicitly clear thread-local GPU resources to avoid TLS order panic
#[cfg(feature = "gpu")]
if engine_type == "GPU" {
engine_gpu::GpuEngine::clear_worker_resources();
}
@@ -1319,7 +1318,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
// Calculate CPU workers
let cpu_workers = config.cpu_workers.unwrap_or_else(|| {
if config.gpu_workers.is_none() || config.gpu_workers == Some(0) {
if config.gpu_devices.is_none() || config.gpu_devices == Some(0) {
// CPU-only mode: use default CPU allocation
let default_cpu_workers = effective_cpus
.saturating_sub(effective_cpus / 2)
@@ -1340,10 +1339,10 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
});
// Calculate GPU workers
let gpu_workers = config.gpu_workers.unwrap_or_else(|| {
let gpu_devices = config.gpu_devices.unwrap_or_else(|| {
if config.cpu_workers.is_some() && config.cpu_workers != Some(0) {
// Hybrid mode: default to 1 GPU worker
log::info!("Hybrid mode: defaulting to 1 GPU worker");
log::info!("Hybrid mode: defaulting to 1 GPU device");
1
} else {
// CPU-only mode by default
@@ -1351,16 +1350,16 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
}
});
let total_workers = cpu_workers + gpu_workers;
let total_workers = cpu_workers + gpu_devices;
let (cpu_workers, gpu_workers) = if total_workers == 0 {
let (cpu_workers, gpu_devices) = if total_workers == 0 {
log::warn!(
"No workers specified. Defaulting to CPU-only mode with {} workers.",
effective_cpus
);
(effective_cpus, 0)
} else {
(cpu_workers, gpu_workers)
(cpu_workers, gpu_devices)
};
// Create engines based on worker configuration
@@ -1370,27 +1369,38 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
None
};
let gpu_engine: Option<Arc<dyn MinerEngine>> = if gpu_workers > 0 {
#[cfg(feature = "gpu")]
{
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
}
#[cfg(not(feature = "gpu"))]
{
log::error!("GPU workers requested but this binary was built without the 'gpu' feature. Rebuild with --features gpu.");
return Err(anyhow::anyhow!(
"GPU workers requested but 'gpu' feature not enabled"
));
}
let gpu_engine: Option<Arc<dyn MinerEngine>> = if gpu_devices > 0 {
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
} else {
None
};
// Validate GPU device count if requesting GPU devices
if gpu_devices > 0 {
if let Some(gpu_engine_arc) = &gpu_engine {
if let Some(gpu_ptr) = gpu_engine_arc
.as_any()
.downcast_ref::<engine_gpu::GpuEngine>()
{
let device_count = gpu_ptr.device_count();
if gpu_devices > device_count {
log::error!(
"❌ ERROR: Requested {} GPU devices but only {} device(s) are available.",
gpu_devices,
device_count
);
log::error!(" Please check your --gpu-devices setting or GPU hardware.");
std::process::exit(1);
}
}
}
}
// Log the mining configuration
log::info!(
"🚀 Mining configuration: {} CPU workers, {} GPU workers",
"🚀 Mining configuration: {} CPU workers, {} GPU devices",
cpu_workers,
gpu_workers
gpu_devices
);
if let Some(ref engine) = cpu_engine {
@@ -1406,7 +1416,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
let progress_interval_ms = config.progress_interval_ms.unwrap_or(10000);
let service = MiningService::new(
cpu_workers,
gpu_workers,
gpu_devices,
cpu_engine,
gpu_engine,
progress_interval_ms,
@@ -1506,9 +1516,9 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
let telemetry_handle = telemetry.clone();
let svc = service.clone();
let engine_name_str = if cpu_workers > 0 && gpu_workers > 0 {
let engine_name_str = if cpu_workers > 0 && gpu_devices > 0 {
"hybrid".to_string()
} else if gpu_workers > 0 {
} else if gpu_devices > 0 {
"gpu".to_string()
} else {
"cpu".to_string()
@@ -1539,7 +1549,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
let interval = miner_telemetry::SystemInterval {
uptime_ms,
engine: Some(engine_name_str.clone()),
workers: Some((svc.cpu_workers + svc.gpu_workers) as u32),
workers: Some((svc.cpu_workers + svc.gpu_devices) as u32),
hash_rate: Some(total_rate),
active_jobs: Some(active_jobs),
linked_node_hint: None,