use crate::simt::error::{device_assert, device_error, DeviceError};
use crate::simt::scheduling_policies::{
GlobalSchedulingPolicy, SchedulingPolicy, StreamPoolRoundRobin,
};
use cuda_core::{CudaContext, CudaFunction, CudaModule, CudaStream};
use rustc_hash::FxHashMap;
use std::cell::Cell;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
pub const DEFAULT_DEVICE_ID: usize = 0;
pub const DEFAULT_NUM_DEVICES: usize = 1;
pub const DEFAULT_ROUND_ROBIN_STREAM_POOL_SIZE: usize = 4;
pub trait FunctionKey: Hash {
fn get_hash_string(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
}
type DeviceFunctions = FxHashMap<String, (Arc<CudaModule>, Arc<CudaFunction>)>;
pub struct AsyncDeviceContext {
#[allow(dead_code)]
device_id: usize,
context: Arc<CudaContext>,
deallocator_stream: Arc<CudaStream>,
policy: Arc<GlobalSchedulingPolicy>,
functions: DeviceFunctions,
}
pub struct AsyncDeviceContexts {
default_device: Cell<usize>,
devices: Cell<Option<FxHashMap<usize, AsyncDeviceContext>>>,
}
thread_local!(static DEVICE_CONTEXTS: AsyncDeviceContexts = const {
AsyncDeviceContexts {
default_device: Cell::new(DEFAULT_DEVICE_ID),
devices: Cell::new(None),
}
});
pub fn get_default_device() -> usize {
DEVICE_CONTEXTS.with(|ctx| ctx.default_device.get())
}
pub fn init_device_contexts(
default_device_id: usize,
num_devices: usize,
) -> Result<(), DeviceError> {
DEVICE_CONTEXTS.with(|ctx| {
let devices = ctx.devices.take();
let is_uninitialized = devices.is_none();
ctx.devices.set(devices);
device_assert(
default_device_id,
is_uninitialized,
"Context already initialized.",
)
})?;
let devices = FxHashMap::with_capacity_and_hasher(num_devices, Default::default());
DEVICE_CONTEXTS.with(|ctx| {
ctx.default_device.set(default_device_id);
ctx.devices.set(Some(devices));
});
Ok(())
}
fn init_device_contexts_default() -> Result<(), DeviceError> {
let default_device = get_default_device();
init_device_contexts(default_device, DEFAULT_NUM_DEVICES)
}
pub fn new_device_context(
device_id: usize,
mut policy: GlobalSchedulingPolicy,
) -> Result<AsyncDeviceContext, DeviceError> {
let context = CudaContext::new(device_id)?;
policy.init(&context)?;
let deallocator_stream = context.new_stream()?;
Ok(AsyncDeviceContext {
device_id,
context,
deallocator_stream,
policy: Arc::new(policy),
functions: FxHashMap::default(),
})
}
fn init_device(
hashmap: &mut FxHashMap<usize, AsyncDeviceContext>,
device_id: usize,
policy: GlobalSchedulingPolicy,
) -> Result<(), DeviceError> {
let device_context = new_device_context(device_id, policy)?;
let pred = hashmap.insert(device_id, device_context).is_none();
device_assert(device_id, pred, "Device is already initialized.")
}
fn init_with_default_policy(
hashmap: &mut FxHashMap<usize, AsyncDeviceContext>,
device_id: usize,
) -> Result<(), DeviceError> {
let policy =
unsafe { StreamPoolRoundRobin::new(device_id, DEFAULT_ROUND_ROBIN_STREAM_POOL_SIZE) };
init_device(
hashmap,
device_id,
GlobalSchedulingPolicy::RoundRobin(policy),
)
}
fn with_global_device_context<F, R>(device_id: usize, f: F) -> Result<R, DeviceError>
where
F: FnOnce(&AsyncDeviceContext) -> R,
{
DEVICE_CONTEXTS.with(|ctx| {
let mut hashmap = match ctx.devices.take() {
Some(hashmap) => hashmap,
None => {
init_device_contexts_default()?;
ctx.devices
.take()
.ok_or_else(|| device_error(device_id, "Failed to initialize context"))?
}
};
if !hashmap.contains_key(&device_id) {
init_with_default_policy(&mut hashmap, device_id)?;
}
let device_context = hashmap
.get(&device_id)
.ok_or_else(|| device_error(device_id, "Failed to get context"))?;
let r = f(device_context);
ctx.devices.replace(Some(hashmap));
Ok(r)
})
}
fn with_global_device_context_mut<F, R>(device_id: usize, f: F) -> Result<R, DeviceError>
where
F: FnOnce(&mut AsyncDeviceContext) -> R,
{
DEVICE_CONTEXTS.with(|ctx| {
let mut hashmap = match ctx.devices.take() {
Some(hashmap) => hashmap,
None => {
init_device_contexts_default()?;
ctx.devices
.take()
.ok_or_else(|| device_error(device_id, "Failed to initialize context"))?
}
};
if !hashmap.contains_key(&device_id) {
init_with_default_policy(&mut hashmap, device_id)?;
}
let device_context = hashmap
.get_mut(&device_id)
.ok_or_else(|| device_error(device_id, "Failed to get context"))?;
let r = f(device_context);
ctx.devices.replace(Some(hashmap));
Ok(r)
})
}
pub fn with_default_device_policy<F, R>(f: F) -> Result<R, DeviceError>
where
F: FnOnce(&Arc<GlobalSchedulingPolicy>) -> R,
{
let default_device = get_default_device();
with_global_device_context(default_device, |dc| f(&dc.policy))
}
pub unsafe fn with_deallocator_stream<F, R>(device_id: usize, f: F) -> Result<R, DeviceError>
where
F: FnOnce(&Arc<CudaStream>) -> R,
{
with_global_device_context(device_id, |dc| f(&dc.deallocator_stream))
}
pub fn with_cuda_context<F, R>(device_id: usize, f: F) -> Result<R, DeviceError>
where
F: FnOnce(&Arc<CudaContext>) -> R,
{
with_global_device_context(device_id, |dc| f(&dc.context))
}
pub fn set_default_device(default_device_id: usize) {
DEVICE_CONTEXTS.with(|ctx| {
ctx.default_device.set(default_device_id);
})
}
pub fn load_module_from_file(
filename: &str,
device_id: usize,
) -> Result<Arc<CudaModule>, DeviceError> {
with_cuda_context(device_id, |cuda_ctx| {
let module = cuda_ctx.load_module_from_file(filename)?;
Ok(module)
})?
}
pub fn load_module_from_ptx(
ptx_src: &str,
device_id: usize,
) -> Result<Arc<CudaModule>, DeviceError> {
with_cuda_context(device_id, |cuda_ctx| {
let module = cuda_ctx.load_module_from_ptx_src(ptx_src)?;
Ok(module)
})?
}
pub fn insert_cuda_function(
device_id: usize,
func_key: &impl FunctionKey,
value: (Arc<CudaModule>, Arc<CudaFunction>),
) -> Result<(), DeviceError> {
with_global_device_context_mut(device_id, |dc| {
let key = func_key.get_hash_string();
let res = dc.functions.insert(key, value);
device_assert(device_id, res.is_none(), "Unexpected cache key collision.")
})?
}
pub fn get_cuda_function(
device_id: usize,
func_key: &impl FunctionKey,
) -> Result<Arc<CudaFunction>, DeviceError> {
with_global_device_context(device_id, |dc| {
let key = func_key.get_hash_string();
let (_module, function) = dc
.functions
.get(&key)
.ok_or_else(|| device_error(device_id, "Failed to get cuda function."))?;
Ok(Arc::clone(function))
})?
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_duplicate_init_error(result: Result<(), DeviceError>) {
assert!(matches!(
result,
Err(DeviceError::Context {
device_id: 0,
message,
}) if message == "Context already initialized."
));
}
#[test]
fn duplicate_init_preserves_existing_device_contexts() {
std::thread::spawn(|| {
init_device_contexts(0, 1).expect("initial context initialization should succeed");
assert_duplicate_init_error(init_device_contexts(0, 1));
assert_duplicate_init_error(init_device_contexts(0, 1));
})
.join()
.expect("test thread should not panic");
}
}