Cache CUDA context and module in CudaEngine for reuse

Initialize CUDA context and load module once per engine instance,
storing them in a mutex-protected cache to avoid repeated setups. Use
cached context/module for kernel launches, creating a fresh stream each
call. Adjust grid dimension calculation to consider multiprocessor
count.
This commit is contained in:
rob thijssen
2025-09-14 18:49:19 +03:00
parent 7e6eee8c72
commit 2480d7828b
2 changed files with 151 additions and 95 deletions

View File

@@ -57,12 +57,31 @@ include!(concat!(env!("OUT_DIR"), "/ptx_bindings.rs"));
/// CUDA engine with host-side G1 launcher and CPU fallback.
#[derive(Default, Debug)]
pub struct CudaEngine;
pub struct CudaEngine {
#[cfg(feature = "cuda")]
cache: std::sync::Mutex<Option<CudaCache>>,
}
#[cfg(feature = "cuda")]
#[derive(Debug)]
struct CudaCache {
context: cuda::context::Context,
module: cuda::module::Module,
}
impl CudaEngine {
/// Construct a new CUDA engine.
pub fn new() -> Self {
Self
#[cfg(feature = "cuda")]
{
Self {
cache: std::sync::Mutex::new(None),
}
}
#[cfg(not(feature = "cuda"))]
{
Self
}
}
/// Human-readable name for logs/metrics.
@@ -124,65 +143,22 @@ impl CudaEngine {
return Ok(EngineStatus::Exhausted { hash_count: 0 });
}
// Initialize CUDA context and keep it current for the entire GPU work scope
let (_context, _ctx_guard) = match cuda::quick_init() {
Ok(ctx) => {
// Ensure the context stays current while this function runs
let guard = ContextStack::push(&ctx)?;
(ctx, guard)
}
Err(e) => {
log::warn!(target: "miner", "CUDA quick_init failed: {e:?}; attempting manual device/context init");
let dev = Device::get_device(0)?;
let ctx = Context::new(dev)?;
let guard = ContextStack::push(&ctx)?;
(ctx, guard)
}
};
// Initialize/reuse CUDA context and module from engine cache; create a fresh stream per call.
let mut cache_guard = self.cache.lock().expect("CudaEngine cache mutex poisoned");
// Prefer embedded CUBIN (native SASS) by default; allow env override; fall back to embedded/env PTX if unavailable.
let image_override = std::env::var("MINER_CUDA_IMAGE")
.ok()
.map(|s| s.to_ascii_lowercase());
let module = match image_override.as_deref() {
Some("ptx") => {
// Force PTX
let (ptx_text, ptx_origin) = if let Some(s) = ptx_embedded::get("qpow_kernel") {
(s.to_string(), String::from("embedded"))
} else {
let ptx_dir = std::env::var("ENGINE_GPU_CUDA_PTX_DIR").map_err(|_| {
anyhow::anyhow!("ENGINE_GPU_CUDA_PTX_DIR not set and no embedded PTX")
})?;
let ptx_path = std::path::Path::new(&ptx_dir).join("qpow_kernel.ptx");
let txt = std::fs::read_to_string(&ptx_path).map_err(|e| {
anyhow::anyhow!("failed to read PTX at {}: {e}", ptx_path.display())
})?;
(txt, format!("env:{}", ptx_path.display()))
};
log::info!(target: "miner", "CUDA: using PTX source = {ptx_origin} (forced by MINER_CUDA_IMAGE=ptx)");
let ptx_cstr = CString::new(ptx_text)?;
cuda::module::Module::from_ptx_cstr(&ptx_cstr, &[])
.with_context(|| "load PTX module")?
}
Some("cubin") => {
// Force CUBIN
if let Some(cubin) = cubin_embedded::get_cubin("qpow_kernel") {
log::info!(target: "miner", "CUDA: using CUBIN (embedded) (forced by MINER_CUDA_IMAGE=cubin)");
cuda::module::Module::from_cubin(cubin, &[])
.with_context(|| "load CUBIN module")?
} else {
anyhow::bail!(
"MINER_CUDA_IMAGE=cubin requested but no embedded CUBIN image was found"
);
}
}
_ => {
if let Some(cubin) = cubin_embedded::get_cubin("qpow_kernel") {
log::info!(target: "miner", "CUDA: using CUBIN (embedded)");
cuda::module::Module::from_cubin(cubin, &[])
.with_context(|| "load CUBIN module")?
} else {
// Load PTX module: prefer embedded PTX; fall back to env-dir PTX
if cache_guard.is_none() {
// Create device/context once and load the module based on env selection.
let dev = Device::get_device(0)?;
let ctx = Context::new(dev)?;
let _push = ContextStack::push(&ctx)?;
// Prefer embedded CUBIN (native SASS) by default; allow env override; fall back to embedded/env PTX if unavailable.
let image_override = std::env::var("MINER_CUDA_IMAGE")
.ok()
.map(|s| s.to_ascii_lowercase());
let loaded_module = match image_override.as_deref() {
Some("ptx") => {
// Force PTX
let (ptx_text, ptx_origin) = if let Some(s) = ptx_embedded::get("qpow_kernel") {
(s.to_string(), String::from("embedded"))
} else {
@@ -195,13 +171,66 @@ impl CudaEngine {
})?;
(txt, format!("env:{}", ptx_path.display()))
};
log::info!(target: "miner", "CUDA: using PTX source = {ptx_origin}");
log::info!(target: "miner", "CUDA: using PTX source = {ptx_origin} (forced by MINER_CUDA_IMAGE=ptx)");
let ptx_cstr = CString::new(ptx_text)?;
cuda::module::Module::from_ptx_cstr(&ptx_cstr, &[])
.with_context(|| "load PTX module")?
}
}
};
Some("cubin") => {
// Force CUBIN
if let Some(cubin) = cubin_embedded::get_cubin("qpow_kernel") {
log::info!(target: "miner", "CUDA: using CUBIN (embedded) (forced by MINER_CUDA_IMAGE=cubin)");
cuda::module::Module::from_cubin(cubin, &[])
.with_context(|| "load CUBIN module")?
} else {
anyhow::bail!(
"MINER_CUDA_IMAGE=cubin requested but no embedded CUBIN image was found"
);
}
}
_ => {
if let Some(cubin) = cubin_embedded::get_cubin("qpow_kernel") {
log::info!(target: "miner", "CUDA: using CUBIN (embedded)");
cuda::module::Module::from_cubin(cubin, &[])
.with_context(|| "load CUBIN module")?
} else {
// Load PTX module: prefer embedded PTX; fall back to env-dir PTX
let (ptx_text, ptx_origin) = if let Some(s) =
ptx_embedded::get("qpow_kernel")
{
(s.to_string(), String::from("embedded"))
} else {
let ptx_dir =
std::env::var("ENGINE_GPU_CUDA_PTX_DIR").map_err(|_| {
anyhow::anyhow!(
"ENGINE_GPU_CUDA_PTX_DIR not set and no embedded PTX"
)
})?;
let ptx_path = std::path::Path::new(&ptx_dir).join("qpow_kernel.ptx");
let txt = std::fs::read_to_string(&ptx_path).map_err(|e| {
anyhow::anyhow!("failed to read PTX at {}: {e}", ptx_path.display())
})?;
(txt, format!("env:{}", ptx_path.display()))
};
log::info!(target: "miner", "CUDA: using PTX source = {ptx_origin}");
let ptx_cstr = CString::new(ptx_text)?;
cuda::module::Module::from_ptx_cstr(&ptx_cstr, &[])
.with_context(|| "load PTX module")?
}
}
};
*cache_guard = Some(CudaCache {
context: ctx,
module: loaded_module,
});
}
// Make cached context current for this thread and keep guard alive for module ref lifetime.
let cache_ref = cache_guard.as_ref().expect("cache initialized");
let _ctx_guard = ContextStack::push(&cache_ref.context)?;
let module_ref = &cache_ref.module;
let stream = cuda::stream::Stream::new(cuda::stream::StreamFlags::DEFAULT, None)
.with_context(|| "create stream")?;
// Select kernel by mode; try G2 when requested, otherwise use G1. If G2 unavailable, fall back to G1.
@@ -213,7 +242,7 @@ impl CudaEngine {
_ => "qpow_montgomery_g1_kernel",
};
let is_g2;
let func = match module.get_function(func_name) {
let func = match module_ref.get_function(func_name) {
Ok(f) => {
is_g2 = func_name == "qpow_montgomery_g2_kernel";
f
@@ -222,7 +251,7 @@ impl CudaEngine {
if func_name == "qpow_montgomery_g2_kernel" {
log::warn!(target: "miner", "CUDA: G2 kernel unavailable ({e:?}); falling back to G1");
is_g2 = false;
module
module_ref
.get_function("qpow_montgomery_g1_kernel")
.with_context(|| "get kernel function 'qpow_montgomery_g1_kernel'")?
} else {
@@ -239,7 +268,7 @@ impl CudaEngine {
// Probe device kernel ABI version if symbol is present (best-effort)
if is_g2 {
if let Ok(sym) =
module.get_global::<u32>(std::ffi::CString::new("C_ABI_VERSION")?.as_c_str())
module_ref.get_global::<u32>(std::ffi::CString::new("C_ABI_VERSION")?.as_c_str())
{
let mut abi: u32 = 0;
if sym.copy_to(&mut abi).is_ok() {
@@ -323,7 +352,15 @@ impl CudaEngine {
let num_threads = threads;
// Use many iterations per thread to reduce host exponentiations per launch
let grid_dim = ((num_threads + block_dim - 1) / block_dim).max(1);
let sm_count = cust::device::Device::get_device(0)
.ok()
.and_then(|dev| {
dev.get_attribute(cust::device::DeviceAttribute::MultiprocessorCount)
.ok()
})
.unwrap_or(1) as u32;
let grid_dim =
std::cmp::max(((num_threads + block_dim - 1) / block_dim).max(1), sm_count);
// Compute effective iterations per thread based on remaining coverage for this launch
let rem_be_all = end.saturating_sub(current).to_big_endian();
@@ -398,13 +435,13 @@ impl CudaEngine {
Ok(mut c_thresh),
Ok(mut c_ready),
) = (
module.get_global::<[u64; 8]>(CString::new("C_N")?.as_c_str()),
module.get_global::<[u64; 8]>(CString::new("C_R2")?.as_c_str()),
module.get_global::<[u64; 8]>(CString::new("C_MHAT")?.as_c_str()),
module.get_global::<u64>(CString::new("C_N0_INV")?.as_c_str()),
module.get_global::<[u64; 8]>(CString::new("C_TARGET")?.as_c_str()),
module.get_global::<[u64; 8]>(CString::new("C_THRESH")?.as_c_str()),
module.get_global::<i32>(CString::new("C_CONSTS_READY")?.as_c_str()),
module_ref.get_global::<[u64; 8]>(CString::new("C_N")?.as_c_str()),
module_ref.get_global::<[u64; 8]>(CString::new("C_R2")?.as_c_str()),
module_ref.get_global::<[u64; 8]>(CString::new("C_MHAT")?.as_c_str()),
module_ref.get_global::<u64>(CString::new("C_N0_INV")?.as_c_str()),
module_ref.get_global::<[u64; 8]>(CString::new("C_TARGET")?.as_c_str()),
module_ref.get_global::<[u64; 8]>(CString::new("C_THRESH")?.as_c_str()),
module_ref.get_global::<i32>(CString::new("C_CONSTS_READY")?.as_c_str()),
) {
// Copy constants into constant memory
c_n.copy_from(&n_le)?;
@@ -427,24 +464,24 @@ impl CudaEngine {
Ok(mut c_samp_index),
Ok(mut c_samp_dec),
) = (
module.get_global::<i32>(
module_ref.get_global::<i32>(
CString::new("C_SAMPLER_ENABLE")?.as_c_str(),
),
module.get_global::<[u8; 64]>(
module_ref.get_global::<[u8; 64]>(
CString::new("C_SAMPLER_Y_BE")?.as_c_str(),
),
module.get_global::<[u8; 64]>(
module_ref.get_global::<[u8; 64]>(
CString::new("C_SAMPLER_H_BE")?.as_c_str(),
),
module.get_global::<[u8; 64]>(
module_ref.get_global::<[u8; 64]>(
CString::new("C_SAMPLER_TARGET_BE")?.as_c_str(),
),
module.get_global::<[u8; 64]>(
module_ref.get_global::<[u8; 64]>(
CString::new("C_SAMPLER_THRESH_BE")?.as_c_str(),
),
module
module_ref
.get_global::<u32>(CString::new("C_SAMPLER_INDEX")?.as_c_str()),
module.get_global::<u32>(
module_ref.get_global::<u32>(
CString::new("C_SAMPLER_DECISION")?.as_c_str(),
),
) {
@@ -470,7 +507,7 @@ impl CudaEngine {
} else {
1
};
if let Ok(mut c_force) = module
if let Ok(mut c_force) = module_ref
.get_global::<i32>(CString::new("C_DEBUG_FORCE_WIN")?.as_c_str())
{
c_force.copy_from(&force)?;
@@ -482,8 +519,8 @@ impl CudaEngine {
} else {
log::warn!(target: "miner", "CUDA G2: MINER_CUDA_FORCE_WIN set but device symbol C_DEBUG_FORCE_WIN unavailable");
}
} else if let Ok(mut c_force) =
module.get_global::<i32>(CString::new("C_DEBUG_FORCE_WIN")?.as_c_str())
} else if let Ok(mut c_force) = module_ref
.get_global::<i32>(CString::new("C_DEBUG_FORCE_WIN")?.as_c_str())
{
let zero: i32 = 0;
let _ = c_force.copy_from(&zero);
@@ -495,7 +532,7 @@ impl CudaEngine {
}
if !consts_ready_set {
if let Ok(mut c_ready) =
module.get_global::<i32>(CString::new("C_CONSTS_READY")?.as_c_str())
module_ref.get_global::<i32>(CString::new("C_CONSTS_READY")?.as_c_str())
{
let zero: i32 = 0;
c_ready.copy_from(&zero)?;
@@ -532,7 +569,17 @@ impl CudaEngine {
active_threads = num_threads as u64;
}
let active_threads_usize = active_threads as usize;
let grid_dim = (((active_threads as u32) + block_dim - 1) / block_dim).max(1);
let sm_count = cust::device::Device::get_device(0)
.ok()
.and_then(|dev| {
dev.get_attribute(cust::device::DeviceAttribute::MultiprocessorCount)
.ok()
})
.unwrap_or(1) as u32;
let grid_dim = std::cmp::max(
(((active_threads as u32) + block_dim - 1) / block_dim).max(1),
sm_count,
);
let force_enabled = std::env::var("MINER_CUDA_FORCE_WIN")
.ok()
.map(|v| v != "0" && !v.is_empty() && !v.eq_ignore_ascii_case("false"))
@@ -785,15 +832,18 @@ impl CudaEngine {
.unwrap_or(false)
{
if let (Ok(c_en), Ok(c_idx), Ok(c_dec), Ok(c_y), Ok(c_h), Ok(c_t), Ok(c_th)) = (
module.get_global::<i32>(CString::new("C_SAMPLER_ENABLE")?.as_c_str()),
module.get_global::<u32>(CString::new("C_SAMPLER_INDEX")?.as_c_str()),
module.get_global::<u32>(CString::new("C_SAMPLER_DECISION")?.as_c_str()),
module.get_global::<[u8; 64]>(CString::new("C_SAMPLER_Y_BE")?.as_c_str()),
module.get_global::<[u8; 64]>(CString::new("C_SAMPLER_H_BE")?.as_c_str()),
module.get_global::<[u8; 64]>(
module_ref.get_global::<i32>(CString::new("C_SAMPLER_ENABLE")?.as_c_str()),
module_ref.get_global::<u32>(CString::new("C_SAMPLER_INDEX")?.as_c_str()),
module_ref
.get_global::<u32>(CString::new("C_SAMPLER_DECISION")?.as_c_str()),
module_ref
.get_global::<[u8; 64]>(CString::new("C_SAMPLER_Y_BE")?.as_c_str()),
module_ref
.get_global::<[u8; 64]>(CString::new("C_SAMPLER_H_BE")?.as_c_str()),
module_ref.get_global::<[u8; 64]>(
CString::new("C_SAMPLER_TARGET_BE")?.as_c_str(),
),
module.get_global::<[u8; 64]>(
module_ref.get_global::<[u8; 64]>(
CString::new("C_SAMPLER_THRESH_BE")?.as_c_str(),
),
) {

View File

@@ -233,8 +233,8 @@ impl MiningService {
metrics::set_hash_rate(total_rate);
metrics::set_active_jobs(running_jobs);
}
drop(jobs_guard);
if last_watchdog.elapsed().as_secs() >= 30 {
let do_watchdog = last_watchdog.elapsed().as_secs() >= 30;
let (total, running, completed, failed, cancelled) = if do_watchdog {
let mut running = 0usize;
let mut completed = 0usize;
let mut cancelled = 0usize;
@@ -248,6 +248,12 @@ impl MiningService {
JobStatus::Failed => failed += 1,
}
}
(total, running, completed, failed, cancelled)
} else {
(0, 0, 0, 0, 0)
};
drop(jobs_guard);
if do_watchdog {
log::info!(
target: "miner",
"Watchdog: jobs total={}, running={}, completed={}, failed={}, cancelled={}, loop_iter={}",