Skip to main content

cugraph/graph/
parked.rs

1//! Parked graph transfer state for cross-thread moves.
2
3use std::{
4    cell::{Cell, RefCell},
5    marker::PhantomData,
6    mem::ManuallyDrop,
7    ptr,
8};
9
10use cudf::runtime::{CudaEvent, ExecutionBindingId};
11use cugraph_sys::cugraph_graph_t;
12
13use super::{Forward, Graph, map_cudf_runtime_error};
14use crate::{
15    cuda::{DeviceResourceKind, drop_device_resource, quarantine_device_drop},
16    error::{Error, ErrorKind, Result},
17    handle::{EnteredContext, ResourceHandleLease},
18    types::{DataType, GraphProperties},
19};
20
21/// A parked cuGraph graph that can be moved across Rust threads.
22///
23/// `Graph<T>` itself remains `!Send`/`!Sync` because native cuGraph operations
24/// require a thread-local entered execution context. `ParkedGraph<T>` is the
25/// transfer state for an idle graph: it owns the native graph pointer, retains
26/// runtime-bound handle leases needed for teardown, and carries a CUDA event
27/// recorded after tracked work on the parking binding's stream, plus that
28/// binding's identity. Exact [`resume`](ParkedGraph::resume) requires an
29/// entered context for the binding that parked the graph;
30/// [`resume_under`](ParkedGraph::resume_under) executes under a different
31/// same-device binding after ordering it behind the park event.
32pub struct ParkedGraph<T = Forward> {
33    pub(super) raw: *mut cugraph_graph_t,
34    pub(super) device: i32,
35    pub(super) properties: GraphProperties,
36    pub(super) vertex_dtype: DataType,
37    pub(super) weight_dtype: DataType,
38    pub(super) has_weights: bool,
39    pub(super) edge_id_dtype: Option<DataType>,
40    pub(super) park_event: CudaEvent,
41    // Binding whose stream the park event was recorded on. Exact resume
42    // requires this binding; `resume_under` orders another same-device
43    // binding behind the event instead.
44    pub(super) park_binding: ExecutionBindingId,
45    pub(super) resource_handle_leases: Vec<ResourceHandleLease>,
46    pub(super) _not_sync: PhantomData<Cell<()>>,
47    pub(super) _transposition: PhantomData<T>,
48}
49
50// SAFETY: parked graphs do not permit native graph use. The raw graph pointer
51// is only resumed into a non-Send `Graph<T>` under a matching entered runtime
52// context, or freed after the park event completes. Runtime-bound leases are
53// retention-only metadata and are Send under their own safety contract.
54unsafe impl<T: Send> Send for ParkedGraph<T> {}
55
56impl<T> ParkedGraph<T> {
57    /// The CUDA device this parked graph resides on.
58    pub fn device(&self) -> i32 {
59        self.device
60    }
61
62    /// Structural properties declared when this graph was constructed.
63    pub fn properties(&self) -> GraphProperties {
64        self.properties
65    }
66
67    /// The data type of vertex IDs in this graph.
68    pub fn vertex_dtype(&self) -> DataType {
69        self.vertex_dtype
70    }
71
72    /// The data type of edge weights in this graph.
73    pub fn weight_dtype(&self) -> DataType {
74        self.weight_dtype
75    }
76
77    /// Resume this parked graph under a runtime-bound entered context.
78    ///
79    /// Resume is exact-binding by construction: `handle` must use the cudf
80    /// runtime stream/MR binding that parked the graph. The runtime stream
81    /// waits on the park event before the live [`Graph`] is returned to the
82    /// caller. Retained creation leases stay untouched for teardown; use
83    /// [`resume_under`](ParkedGraph::resume_under) to execute under a
84    /// different same-device binding.
85    ///
86    /// If this method returns an error, the consumed parked graph is dropped.
87    pub fn resume(self, handle: &EnteredContext<'_>) -> Result<Graph<T>> {
88        let operation = "ParkedGraph::resume";
89        self.validate_resume_handle(handle, operation)?;
90        self.resume_ordered(handle, operation)
91    }
92
93    /// Resume this parked graph under a different runtime-bound execution
94    /// binding on the same device (storage/execution split).
95    ///
96    /// The resume stream waits on the park event, ordering preceding storage
97    /// writes before borrower kernels read the graph. Algorithm temporaries
98    /// allocate through the entering handle, charging workspace, auxiliary
99    /// streams, and outputs to the borrower domain. cuGraph algorithms observe the
100    /// graph through a const view: the borrower must not mutate shared
101    /// storage, and the exclusive checkout guard that owns this parked graph
102    /// must outlive every kernel launched under the borrower binding.
103    /// Teardown still runs under the retained creation leases. Re-park with
104    /// [`park_under`](Graph::park_under) under the same borrower binding.
105    ///
106    /// If this method returns an error, the consumed parked graph is dropped.
107    pub fn resume_under(self, handle: &EnteredContext<'_>) -> Result<Graph<T>> {
108        let operation = "ParkedGraph::resume_under";
109        if handle.device() != self.device {
110            return Err(Error::new(ErrorKind::InvalidInput, operation)
111                .with_message("parked graph and resource handle must be on the same CUDA device")
112                .with_context("graph_device", self.device.to_string())
113                .with_context("handle_device", handle.device().to_string()));
114        }
115        if handle.runtime().is_none() {
116            return Err(Error::new(ErrorKind::InvalidState, operation)
117                .with_message("parked graph resume requires a runtime-bound resource handle"));
118        }
119        self.resume_ordered(handle, operation)
120    }
121
122    fn resume_ordered(self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<Graph<T>> {
123        let runtime = handle.runtime().expect("validated runtime-bound handle");
124        runtime
125            .stream()
126            .wait_event(&self.park_event)
127            .map_err(|error| map_cudf_runtime_error(error, operation, "failed to wait on graph park CUDA event"))?;
128        let ordered_binding = runtime.binding_id();
129
130        let this = ManuallyDrop::new(self);
131        // SAFETY: `this` will not run `ParkedGraph::drop`, so moving fields
132        // out here transfers native graph ownership to the returned Graph.
133        let resource_handle_leases = unsafe { ptr::read(&this.resource_handle_leases) };
134        // SAFETY: the park event is no longer needed once the runtime stream
135        // has been ordered after it. Drop the moved event normally.
136        let park_event = unsafe { ptr::read(&this.park_event) };
137        drop(park_event);
138
139        Ok(Graph {
140            raw: this.raw,
141            device: this.device,
142            properties: this.properties,
143            vertex_dtype: this.vertex_dtype,
144            weight_dtype: this.weight_dtype,
145            has_weights: this.has_weights,
146            edge_id_dtype: this.edge_id_dtype,
147            pending_input_completion: None,
148            resource_handle_leases: RefCell::new(resource_handle_leases),
149            ordered_binding: Cell::new(Some(ordered_binding)),
150            untracked_resource_handle_use: Cell::new(false),
151            _nosend_nosync: PhantomData,
152            _transposition: PhantomData,
153        })
154    }
155
156    fn validate_resume_handle(&self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<()> {
157        if handle.device() != self.device {
158            return Err(Error::new(ErrorKind::InvalidInput, operation)
159                .with_message("parked graph and resource handle must be on the same CUDA device")
160                .with_context("graph_device", self.device.to_string())
161                .with_context("handle_device", handle.device().to_string()));
162        }
163
164        let Some(runtime) = handle.runtime() else {
165            return Err(Error::new(ErrorKind::InvalidState, operation)
166                .with_message("parked graph resume requires a runtime-bound resource handle"));
167        };
168        if runtime.binding_id() != self.park_binding {
169            return Err(Error::new(ErrorKind::InvalidState, operation)
170                .with_message("parked graph execution binding does not match the resume handle")
171                .with_context("graph_binding_id", self.park_binding.to_string())
172                .with_context("handle_binding_id", runtime.binding_id().to_string()));
173        }
174
175        Ok(())
176    }
177}
178
179impl<T> std::fmt::Debug for ParkedGraph<T> {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.debug_struct("ParkedGraph")
182            .field("device", &self.device)
183            .field("properties", &self.properties)
184            .field("vertex_dtype", &self.vertex_dtype)
185            .field("weight_dtype", &self.weight_dtype)
186            .field("has_weights", &self.has_weights)
187            .finish_non_exhaustive()
188    }
189}
190
191fn free_parked_graph_raw_with_leases(
192    raw: &mut *mut cugraph_graph_t,
193    device: i32,
194    park_event: &CudaEvent,
195    leases: &[ResourceHandleLease],
196) {
197    let mut free = || match park_event.synchronize() {
198        Ok(()) => drop_device_resource(raw, device, "ParkedGraph::drop", DeviceResourceKind::Graph),
199        Err(error) => {
200            let owned = std::mem::replace(raw, ptr::null_mut());
201            if !owned.is_null() {
202                quarantine_device_drop("ParkedGraph::drop", device, &error);
203            }
204        },
205    };
206    if let Some(lease) = leases.first() {
207        lease.with_teardown_gate(free);
208    } else {
209        free();
210    }
211}
212
213impl<T> Drop for ParkedGraph<T> {
214    fn drop(&mut self) {
215        free_parked_graph_raw_with_leases(&mut self.raw, self.device, &self.park_event, &self.resource_handle_leases);
216    }
217}