Skip to main content

cuvs/
resources.rs

1use std::{
2    fmt,
3    marker::PhantomData,
4    ptr,
5    sync::{
6        Arc,
7        atomic::{AtomicU64, AtomicUsize, Ordering},
8    },
9};
10
11use cudf::{
12    RawCurrentResourceRouteLease, RawStreamReservationLease, Runtime, StreamReservationLease,
13    runtime::{BindingExtensionInitError, BindingExtensionLease, DeviceEntryGuard, ExecutionBindingId},
14};
15
16use crate::error::{Result, check_cuvs, from_cudf, from_interop, invalid_state, unsupported};
17
18const CREATE_OPERATION: &str = "CuvsResources::for_runtime";
19static NEXT_POOL_MEMBER_ID: AtomicU64 = AtomicU64::new(1);
20
21struct NativeResources {
22    raw: AtomicUsize,
23    device: cudf::DeviceId,
24    stream: usize,
25    memory_resource: usize,
26}
27
28impl NativeResources {
29    fn create(device: cudf::DeviceId, stream: usize, memory_resource: usize) -> Result<Self> {
30        cuvs_sys::validate_runtime_version()
31            .map_err(|error| invalid_state(CREATE_OPERATION, error.to_string()).set_source(error))?;
32        let _device =
33            DeviceEntryGuard::enter(device, CREATE_OPERATION).map_err(|error| from_cudf(CREATE_OPERATION, error))?;
34        let mut raw = 0;
35        unsafe { check_cuvs(cuvs_sys::cuvsResourcesCreate(&mut raw), CREATE_OPERATION)? };
36        if raw == 0 {
37            return Err(invalid_state(CREATE_OPERATION, "cuVS returned a null resources handle"));
38        }
39        let set_stream =
40            unsafe { cuvs_sys::cuvsStreamSet(raw, (stream as *mut std::ffi::c_void).cast::<cuvs_sys::CUstream_st>()) };
41        let set_pool = check_cuvs(set_stream, CREATE_OPERATION)
42            .and_then(|()| unsafe { check_cuvs(cuvs_sys::cuvsResourcesSetStreamPool(raw, 1), CREATE_OPERATION) });
43        if let Err(error) = set_pool {
44            unsafe {
45                let _ = cuvs_sys::cuvsResourcesDestroy(raw);
46            }
47            return Err(error);
48        }
49        Ok(Self { raw: AtomicUsize::new(raw), device, stream, memory_resource })
50    }
51
52    fn raw(&self, operation: &'static str) -> Result<cuvs_sys::cuvsResources_t> {
53        let raw = self.raw.load(Ordering::Acquire);
54        if raw == 0 {
55            return Err(invalid_state(operation, "the runtime-bound cuVS resources have been closed"));
56        }
57        Ok(raw)
58    }
59}
60
61impl Drop for NativeResources {
62    fn drop(&mut self) {
63        let raw = self.raw.swap(0, Ordering::AcqRel);
64        if raw == 0 {
65            return;
66        }
67        if let Ok(_device) = DeviceEntryGuard::enter(self.device, "CuvsResources::drop") {
68            unsafe {
69                let _ = cuvs_sys::cuvsResourcesDestroy(raw);
70            }
71        }
72    }
73}
74
75/// One serialized cuVS resource generation for an exact cudf runtime binding.
76///
77/// cuVS 26.08 supports stream selection but no memory-resource setter. The bridge
78/// requires runtimes configured with the process-lifetime current
79/// [`cudf::DeviceMemoryManager`] resource. Stream reservations remain supported by
80/// routing allocations by stream through that manager resource.
81pub struct CuvsResources {
82    runtime: Arc<Runtime>,
83    native: BindingExtensionLease<NativeResources>,
84    _not_send_sync: PhantomData<*mut ()>,
85}
86
87/// Retains query-domain members or backend routes until native work on the RAFT pool is finished.
88/// Drop this guard before the resources handle destroys or replaces its pool.
89#[must_use = "dropping pool coverage removes its allocation routes"]
90pub struct StreamPoolCoverage {
91    members: Vec<RawStreamReservationLease>,
92    routes: Vec<RawCurrentResourceRouteLease>,
93    _resources: CuvsResources,
94}
95
96impl StreamPoolCoverage {
97    /// Per-stream reservation usage, empty for standalone backend routes.
98    pub fn usage(&self) -> Result<Vec<cudf::ReservationUsage>> {
99        self.members
100            .iter()
101            .map(|member| member.usage().map_err(|error| from_cudf("StreamPoolCoverage::usage", error)))
102            .collect()
103    }
104}
105
106impl CuvsResources {
107    pub fn for_runtime(runtime: Arc<Runtime>) -> Result<Self> {
108        let binding = runtime.execution_binding().map_err(|error| from_interop(CREATE_OPERATION, error))?;
109        let stream = binding.cuda_stream().as_raw() as usize;
110        let memory_resource = binding.memory_resource().as_raw() as usize;
111
112        let manager = runtime.device_memory_manager().map_err(|error| from_cudf(CREATE_OPERATION, error))?;
113        let manager_runtime = Runtime::builder()
114            .with_device(runtime.device())
115            .with_stream(runtime.stream().clone())
116            .with_memory_resource(manager.resource().clone())
117            .build()
118            .map_err(|error| from_cudf(CREATE_OPERATION, error))?;
119        let manager_binding =
120            manager_runtime.execution_binding().map_err(|error| from_interop(CREATE_OPERATION, error))?;
121        let current_memory_resource = manager_binding.memory_resource().as_raw() as usize;
122        if memory_resource != current_memory_resource {
123            return Err(unsupported(
124                CREATE_OPERATION,
125                "cuVS 26.08 cannot bind an arbitrary runtime memory resource; use the device memory manager resource and stream reservations",
126            )
127            .with_context("runtime_memory_resource", format!("0x{memory_resource:x}"))
128            .with_context("current_memory_resource", format!("0x{current_memory_resource:x}")));
129        }
130
131        let native = runtime
132            .get_or_try_init_binding_extension(CREATE_OPERATION, || {
133                NativeResources::create(runtime.device(), stream, memory_resource)
134            })
135            .map_err(|error| match error {
136                BindingExtensionInitError::Lifecycle(error) => from_cudf(CREATE_OPERATION, error),
137                BindingExtensionInitError::Factory(error) => error,
138            })?;
139        let resources = Self { runtime, native, _not_send_sync: PhantomData };
140        resources.validate_exact_binding(CREATE_OPERATION)?;
141        Ok(resources)
142    }
143
144    /// Cover every RAFT pool stream with siblings of the runtime stream's reservation.
145    ///
146    /// When `reservation` is `None`, allocations route to the device manager's backend resource.
147    pub fn stream_pool_coverage(&self, reservation: Option<&StreamReservationLease>) -> Result<StreamPoolCoverage> {
148        let operation = "CuvsResources::stream_pool_coverage";
149        self.validate_exact_binding(operation)?;
150        if reservation.is_some_and(|lease| {
151            lease.stream().identity() != self.runtime.stream().identity() || lease.manager().device() != self.device()
152        }) {
153            return Err(invalid_state(operation, "reservation must cover the runtime stream on the same device"));
154        }
155        let manager = self.runtime.device_memory_manager().map_err(|error| from_cudf(operation, error))?;
156        let limit = reservation
157            .map(|lease| {
158                usize::try_from(lease.usage().map_err(|error| from_cudf(operation, error))?.allocation_limit_bytes)
159                    .map_err(|_| invalid_state(operation, "reservation limit exceeds usize"))
160            })
161            .transpose()?;
162        let count = self.with_native(operation, |native| {
163            let mut count = 0;
164            unsafe {
165                check_cuvs(cuvs_sys::cuvsResourcesGetStreamPoolSize(native, &mut count), operation)?;
166            }
167            Ok(count)
168        })?;
169        if count == 0 {
170            return Err(invalid_state(operation, "cuVS resource has no RAFT stream pool"));
171        }
172        let mut coverage = StreamPoolCoverage {
173            members: Vec::with_capacity(count),
174            routes: Vec::with_capacity(count),
175            _resources: self.retain(),
176        };
177        for index in 0..count {
178            let raw = self.with_native(operation, |native| {
179                let mut raw = ptr::null_mut();
180                unsafe {
181                    check_cuvs(cuvs_sys::cuvsResourcesGetStreamPoolStream(native, index, &mut raw), operation)?;
182                }
183                if raw.is_null() {
184                    return Err(invalid_state(operation, "cuVS returned a null RAFT pool stream"));
185                }
186                Ok(raw)
187            })?;
188            if let Some(reservation) = reservation {
189                let id = NEXT_POOL_MEMBER_ID.fetch_add(1, Ordering::Relaxed);
190                // SAFETY: coverage retains the resources handle and its pool until the member is detached.
191                let member = unsafe { reservation.attach_sibling_raw_stream(raw.cast(), id, limit.expect("limit")) }
192                    .map_err(|error| from_cudf(operation, error))?;
193                coverage.members.push(member);
194            } else {
195                // SAFETY: coverage retains the resources handle and its pool until the route is detached.
196                let route = unsafe { manager.route_current_resource_raw(raw.cast(), manager.resource()) }
197                    .map_err(|error| from_cudf(operation, error))?;
198                coverage.routes.push(route);
199            }
200        }
201        Ok(coverage)
202    }
203
204    pub fn runtime(&self) -> &Arc<Runtime> {
205        &self.runtime
206    }
207
208    pub fn device(&self) -> cudf::DeviceId {
209        self.runtime.device()
210    }
211
212    pub fn binding_id(&self) -> ExecutionBindingId {
213        self.runtime.binding_id()
214    }
215
216    pub(crate) fn validate_exact_binding(&self, operation: &'static str) -> Result<()> {
217        let binding = self.runtime.execution_binding().map_err(|error| from_interop(operation, error))?;
218        let guard = self.native.enter(operation).map_err(|error| from_cudf(operation, error))?;
219        let native = guard.value();
220        let actual_device = binding.device();
221        let actual_stream = binding.cuda_stream().as_raw() as usize;
222        let actual_memory_resource = binding.memory_resource().as_raw() as usize;
223        if actual_device != native.device.ordinal() as i32
224            || actual_stream != native.stream
225            || actual_memory_resource != native.memory_resource
226        {
227            return Err(invalid_state(operation, "the cudf runtime no longer matches the cuVS resource binding")
228                .with_context("expected_device", native.device.ordinal())
229                .with_context("actual_device", actual_device)
230                .with_context("expected_stream", format!("0x{:x}", native.stream))
231                .with_context("actual_stream", format!("0x{actual_stream:x}"))
232                .with_context("expected_memory_resource", format!("0x{:x}", native.memory_resource))
233                .with_context("actual_memory_resource", format!("0x{actual_memory_resource:x}")));
234        }
235        native.raw(operation).map(|_| ())
236    }
237
238    pub(crate) fn retain(&self) -> Self {
239        Self { runtime: Arc::clone(&self.runtime), native: self.native.clone(), _not_send_sync: PhantomData }
240    }
241
242    pub(crate) fn with_native<T>(
243        &self,
244        operation: &'static str,
245        call: impl FnOnce(cuvs_sys::cuvsResources_t) -> Result<T>,
246    ) -> Result<T> {
247        let binding = self.runtime.execution_binding().map_err(|error| from_interop(operation, error))?;
248        let _device = DeviceEntryGuard::enter(self.device(), operation).map_err(|error| from_cudf(operation, error))?;
249        let guard = self.native.enter(operation).map_err(|error| from_cudf(operation, error))?;
250        let native = guard.value();
251        if binding.device() != native.device.ordinal() as i32
252            || binding.cuda_stream().as_raw() as usize != native.stream
253            || binding.memory_resource().as_raw() as usize != native.memory_resource
254        {
255            return Err(invalid_state(operation, "cuVS call rejected an execution-binding mismatch"));
256        }
257        call(native.raw(operation)?)
258    }
259}
260
261impl fmt::Debug for CuvsResources {
262    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263        formatter
264            .debug_struct("CuvsResources")
265            .field("device", &self.device())
266            .field("binding_id", &self.binding_id())
267            .finish_non_exhaustive()
268    }
269}