Skip to main content

xlog_cuda/provider/
builder.rs

1use 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
13/// Builds the complete production CUDA provider ownership graph.
14///
15/// Every provider created here owns one stream pool, one asynchronous resource
16/// stack, one runtime, and one memory manager. Providers on the same device
17/// share the canonical loaded device handle so CUDA module images are owned
18/// once per overlapping device lifetime. The same handle is shared through
19/// each provider graph; callers cannot inject mismatched allocator or device
20/// components.
21pub 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    /// Select a CUDA device and the byte budget enforced by both the memory
31    /// manager and the runtime resource stack.
32    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    /// Record resource lifecycle events in `sink` without changing allocation
43    /// or budget semantics.
44    pub fn with_logging_sink(mut self, sink: Arc<dyn LoggingSink>) -> Self {
45        self.logging_sink = Some(sink);
46        self
47    }
48
49    /// Set the global runtime-resource byte limit independently of the memory
50    /// manager limit. By default both layers use `memory_budget.device_bytes`.
51    pub fn with_runtime_budget_limit(mut self, bytes: u64) -> Self {
52        self.runtime_budget_bytes = Some(bytes);
53        self
54    }
55
56    /// Set the maximum number of pooled streams for workloads that need more
57    /// concurrent lanes than the production default.
58    pub fn with_stream_capacity(mut self, stream_capacity: usize) -> Self {
59        self.stream_capacity = Some(stream_capacity);
60        self
61    }
62
63    /// Construct and validate the complete provider-owned runtime.
64    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}