xlog_cuda/provider/
builder.rs1use std::collections::HashMap;
2use std::sync::{Arc, Mutex, OnceLock, Weak};
3
4use xlog_core::{MemoryBudget, Result, XlogError};
5
6use super::{warmup_profiling_enabled, CudaKernelProvider, PtxLoadProfile};
7use crate::device_runtime::{
8 AsyncCudaResource, DeviceMemoryResource, GlobalDeviceBudget, LoggingResource, LoggingSink,
9 StreamPool, XlogDeviceRuntime,
10};
11use crate::{CudaDevice, GpuMemoryManager};
12
13pub struct CudaProviderBuilder {
22 device_ordinal: usize,
23 memory_budget: MemoryBudget,
24 runtime_budget_bytes: Option<u64>,
25 logging_sink: Option<Arc<dyn LoggingSink>>,
26 stream_capacity: Option<usize>,
27}
28
29impl CudaProviderBuilder {
30 pub fn new(device_ordinal: usize, memory_budget: MemoryBudget) -> Self {
33 Self {
34 device_ordinal,
35 memory_budget,
36 runtime_budget_bytes: None,
37 logging_sink: None,
38 stream_capacity: None,
39 }
40 }
41
42 pub fn with_logging_sink(mut self, sink: Arc<dyn LoggingSink>) -> Self {
45 self.logging_sink = Some(sink);
46 self
47 }
48
49 pub fn with_runtime_budget_limit(mut self, bytes: u64) -> Self {
52 self.runtime_budget_bytes = Some(bytes);
53 self
54 }
55
56 pub fn with_stream_capacity(mut self, stream_capacity: usize) -> Self {
59 self.stream_capacity = Some(stream_capacity);
60 self
61 }
62
63 pub fn build(self) -> Result<CudaKernelProvider> {
65 let runtime_ordinal =
66 u32::try_from(self.device_ordinal).map_err(|_| XlogError::Configuration {
67 name: "device_ordinal".to_string(),
68 value: self.device_ordinal.to_string(),
69 expected: "an integer representable as u32",
70 })?;
71 let runtime_budget_limit = checked_runtime_budget_limit(self.runtime_budget_bytes())?;
72
73 let profiling = warmup_profiling_enabled()?;
74 let (device, ptx_load_profile) = loaded_device(self.device_ordinal, profiling)?;
75 let stream_pool = Arc::new(match self.stream_capacity {
76 Some(stream_capacity) => StreamPool::new(Arc::clone(&device), stream_capacity),
77 None => StreamPool::with_defaults(Arc::clone(&device)),
78 });
79 let asynchronous = AsyncCudaResource::new(
80 Arc::clone(&device),
81 runtime_ordinal,
82 Arc::clone(&stream_pool),
83 );
84 debug_assert!(Arc::ptr_eq(asynchronous.stream_pool(), &stream_pool));
85
86 let mut resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
87 GlobalDeviceBudget::new(Box::new(asynchronous), runtime_budget_limit),
88 );
89 if let Some(sink) = self.logging_sink {
90 resource = Box::new(LoggingResource::new(resource, sink));
91 }
92
93 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
94 Arc::clone(&device),
95 runtime_ordinal,
96 stream_pool,
97 resource,
98 ));
99 let memory = Arc::new(GpuMemoryManager::with_runtime(
100 Arc::clone(&device),
101 self.memory_budget,
102 runtime,
103 ));
104
105 CudaKernelProvider::from_loaded_runtime_parts(device, memory, ptx_load_profile)
106 }
107
108 fn runtime_budget_bytes(&self) -> u64 {
109 self.runtime_budget_bytes
110 .unwrap_or(self.memory_budget.device_bytes)
111 }
112}
113
114fn loaded_device(
115 ordinal: usize,
116 profiling: bool,
117) -> Result<(Arc<CudaDevice>, Option<PtxLoadProfile>)> {
118 static DEVICES: OnceLock<Mutex<HashMap<usize, Weak<CudaDevice>>>> = OnceLock::new();
119
120 let mut devices = DEVICES
121 .get_or_init(|| Mutex::new(HashMap::new()))
122 .lock()
123 .map_err(|_| XlogError::Kernel("canonical CUDA device registry is poisoned".to_string()))?;
124 if let Some(device) = devices.get(&ordinal).and_then(Weak::upgrade) {
125 return Ok((device, None));
126 }
127
128 let device = Arc::new(CudaDevice::new(ordinal)?);
129 let ptx_load_profile = CudaKernelProvider::load_all_kernel_modules(&device, profiling)?;
130 devices.insert(ordinal, Arc::downgrade(&device));
131 Ok((device, ptx_load_profile))
132}
133
134fn checked_runtime_budget_limit(bytes: u64) -> Result<usize> {
135 checked_runtime_budget_limit_for_platform(bytes, usize::MAX as u64)
136}
137
138fn checked_runtime_budget_limit_for_platform(bytes: u64, platform_max: u64) -> Result<usize> {
139 if bytes > platform_max {
140 return Err(XlogError::Configuration {
141 name: "memory_budget.device_bytes".to_string(),
142 value: bytes.to_string(),
143 expected: "a byte count representable as usize",
144 });
145 }
146 usize::try_from(bytes).map_err(|_| XlogError::Configuration {
147 name: "memory_budget.device_bytes".to_string(),
148 value: bytes.to_string(),
149 expected: "a byte count representable as usize",
150 })
151}
152
153#[cfg(test)]
154mod tests {
155 use xlog_core::MemoryBudget;
156
157 use super::{checked_runtime_budget_limit_for_platform, CudaProviderBuilder};
158
159 #[test]
160 fn byte_budget_conversion_does_not_saturate() {
161 let error = checked_runtime_budget_limit_for_platform(u32::MAX as u64 + 1, u32::MAX as u64)
162 .expect_err("out-of-range byte budgets must be rejected");
163 assert!(error.to_string().contains("memory_budget.device_bytes"));
164 }
165
166 #[test]
167 fn runtime_budget_defaults_to_manager_budget_and_can_be_set_independently() {
168 let default = CudaProviderBuilder::new(0, MemoryBudget::with_limit(128));
169 assert_eq!(default.runtime_budget_bytes(), 128);
170
171 let independent = default.with_runtime_budget_limit(512);
172 assert_eq!(independent.runtime_budget_bytes(), 512);
173 }
174}