Skip to main content

cugraph/graph/
mod.rs

1//! Graph construction and management.
2//!
3//! A [`Graph`] is the central data structure for cuGraph algorithms.
4//!
5//! Graph ingress from cuDF tables is provided by [`crate::cudf_interop`]. This
6//! module provides the underlying [`Graph`] container and [`GraphBuilder`] for
7//! edge lists materialized as typed device arrays.
8//!
9//! # Type-state encoding
10//!
11//! The `Graph<T>` type parameter encodes whether the graph is stored in
12//! transposed form ([`Transposed`]) or forward form ([`Forward`]). Algorithms
13//! enforce their required layout at compile time:
14//!
15//! - **[`Forward`]**: required by [`Graph::bfs`] and [`Graph::louvain`]
16//! - **[`Transposed`]**: required by [`Graph::pagerank`],
17//!   [`Graph::in_degrees`], and [`Graph::degrees`]
18//!
19//! Algorithms that work with either layout (e.g. betweenness centrality,
20//! Force Atlas 2, out_degrees) are generic over `T: Transposition`.
21//!
22//! # Low-level builder example
23//!
24//! ```no_run
25//! # use cudf::Runtime;
26//! # use cugraph::{DeviceArray, GraphBuilder, ResourceHandle, Transposed};
27//! # let runtime = std::sync::Arc::new(Runtime::new().unwrap());
28//! # let handle = ResourceHandle::for_runtime(std::sync::Arc::clone(&runtime)).unwrap();
29//! # let entered = handle.enter("graph module docs").unwrap();
30//! # let src = DeviceArray::from_slice(&entered, &[0i32, 1, 2]).unwrap();
31//! # let dst = DeviceArray::from_slice(&entered, &[1i32, 2, 0]).unwrap();
32//! let src_view = src.view().unwrap();
33//! let dst_view = dst.view().unwrap();
34//! let graph = GraphBuilder::new(&src_view, &dst_view)
35//!     .renumber()
36//!     .build::<Transposed>(&entered)
37//!     .unwrap();
38//! ```
39
40use std::{
41    cell::{Cell, RefCell},
42    marker::PhantomData,
43    mem::ManuallyDrop,
44    ptr,
45};
46
47use cudf::{
48    interop::contract::{InteropError, LeaseCompletion},
49    runtime::{CudaEvent, ExecutionBindingId},
50};
51use cugraph_sys::{cugraph_decompress_to_edgelist, cugraph_edgelist_t, cugraph_error_t, cugraph_graph_t};
52
53use crate::{
54    array::{DeviceArray, DeviceArrayView},
55    cuda::{DeviceGuard, DeviceResourceKind, drop_device_resource},
56    error::{Error, ErrorKind, Result, check_error},
57    handle::{EnteredContext, ResourceHandleLease},
58    random::RngState,
59    sampling::select_random_vertices,
60    structure::{ResolvedVertexSelection, VertexSelection},
61    types::{CugraphElement, DataType, GraphProperties, to_bool_t},
62};
63
64mod algorithms;
65mod builder;
66mod edgelist;
67mod parked;
68
69pub use builder::GraphBuilder;
70pub(crate) use edgelist::DecompressedEdgelist;
71pub use parked::ParkedGraph;
72
73mod sealed {
74    pub trait Sealed {}
75    impl Sealed for super::Forward {}
76    impl Sealed for super::Transposed {}
77}
78
79/// Trait implemented by the graph transposition marker types.
80///
81/// This trait is [sealed]; it cannot be implemented outside this crate.
82///
83/// [sealed]: https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed
84pub trait Transposition: sealed::Sealed {
85    /// Whether this transposition state stores the graph transposed.
86    #[doc(hidden)]
87    fn store_transposed() -> bool;
88}
89
90/// Marker type indicating a graph stored in forward (non-transposed) form.
91///
92/// Required by algorithms like [`Graph::bfs`] and [`Graph::louvain`].
93pub struct Forward;
94impl Transposition for Forward {
95    fn store_transposed() -> bool {
96        false
97    }
98}
99
100/// Marker type indicating a graph stored in transposed form.
101///
102/// Required by algorithms like [`Graph::pagerank`] and [`Graph::in_degrees`].
103pub struct Transposed;
104impl Transposition for Transposed {
105    fn store_transposed() -> bool {
106        true
107    }
108}
109
110/// RAII wrapper for a cuGraph graph object.
111///
112/// The type parameter `T` encodes the transposition state of the graph:
113///
114/// - [`Forward`]: stored in forward (non-transposed) form (the default)
115/// - [`Transposed`]: stored in transposed form
116///
117/// Use [`GraphBuilder::build::<Forward>`](GraphBuilder::build) or
118/// [`GraphBuilder::build::<Transposed>`](GraphBuilder::build) to construct a
119/// graph with the appropriate type state.
120///
121/// The graph carries runtime dtype information for its vertex IDs
122/// ([`vertex_dtype`](Graph::vertex_dtype)) and edge weights
123/// ([`weight_dtype`](Graph::weight_dtype)), enabling downstream code to
124/// interpret algorithm results without guessing.
125///
126/// When a graph is constructed or operated on with a
127/// [`ResourceHandle`](crate::handle::ResourceHandle) created by
128/// [`ResourceHandle::for_runtime`](crate::handle::ResourceHandle::for_runtime),
129/// the graph retains a private lease on that runtime-bound native handle until
130/// the graph is dropped. This keeps native graph teardown safe when cuGraph
131/// records allocations or RAFT-side state associated with the runtime-bound
132/// handle used by construction or later algorithms.
133pub struct Graph<T = Forward> {
134    raw: *mut cugraph_graph_t,
135    device: i32,
136    properties: GraphProperties,
137    vertex_dtype: DataType,
138    weight_dtype: DataType,
139    has_weights: bool,
140    edge_id_dtype: Option<DataType>,
141    pending_input_completion: Option<DeferredInputCompletion>,
142    // Runtime-bound resource handles of the binding that owns the graph's
143    // native allocations can own state referenced during cugraph_graph_free.
144    // Keep those leases alive until after Graph::drop runs. Handles of a
145    // borrowed execution binding are not retained here: their allocations
146    // are call-scoped and must retire with the borrowing attempt.
147    resource_handle_leases: RefCell<Vec<ResourceHandleLease>>,
148    // Binding whose stream is ordered after every tracked graph operation.
149    // Established by construction or resume; every runtime-bound use must
150    // enter under it so one park event keeps proving full stream ordering.
151    // `None` until the first runtime-bound use of a graph built outside a
152    // runtime binding.
153    ordered_binding: Cell<Option<ExecutionBindingId>>,
154    // A graph touched through local or external handles has work whose stream
155    // is not represented by the runtime-bound lease ledger. Such a graph
156    // cannot be parked because one runtime event cannot fence those uses.
157    untracked_resource_handle_use: Cell<bool>,
158    _nosend_nosync: PhantomData<*mut ()>,
159    _transposition: PhantomData<T>,
160}
161
162pub(crate) struct DeferredInputCompletion {
163    completions: Vec<LeaseCompletion>,
164}
165
166impl DeferredInputCompletion {
167    pub(crate) fn new(completions: Vec<LeaseCompletion>) -> Option<Self> {
168        if completions.is_empty() {
169            return None;
170        }
171
172        Some(Self { completions })
173    }
174
175    pub(crate) fn synchronize(&mut self, operation: &'static str) -> Result<()> {
176        for completion in &mut self.completions {
177            completion.synchronize().map_err(|error| map_completion_witness_error(error, operation))?;
178        }
179        Ok(())
180    }
181}
182
183fn map_completion_witness_error(error: InteropError, operation: &'static str) -> Error {
184    let mut mapped = Error::new(ErrorKind::InvalidState, operation)
185        .with_message("failed to synchronize graph input completion witness")
186        .with_context("interop_kind", format!("{:?}", error.kind()))
187        .with_context("interop_message", error.message().to_string());
188
189    for (key, value) in error.context() {
190        mapped = mapped.with_context(key, value.clone());
191    }
192
193    mapped.set_source(error)
194}
195
196fn map_cudf_runtime_error(error: cudf::Error, operation: &'static str, message: &'static str) -> Error {
197    let kind = match error.code() {
198        cudf::ErrorCode::OutOfMemory | cudf::ErrorCode::ForeignResourceExhausted => ErrorKind::OutOfMemory,
199        cudf::ErrorCode::CudaRuntime
200        | cudf::ErrorCode::FatalCuda
201        | cudf::ErrorCode::ReadinessUnavailable
202        | cudf::ErrorCode::ForeignTransientDependencyFailure
203        | cudf::ErrorCode::ForeignPersistentDependencyFailure => ErrorKind::Unexpected,
204        cudf::ErrorCode::RuntimeBindingMismatch
205        | cudf::ErrorCode::ExecutionBindingUnavailable
206        | cudf::ErrorCode::InvariantViolation
207        | cudf::ErrorCode::ForeignInvariantViolation => ErrorKind::InvalidState,
208        cudf::ErrorCode::CrossDeviceInputs | cudf::ErrorCode::CrossProvenanceInputs => ErrorKind::InvalidInput,
209        cudf::ErrorCode::UnsupportedDtype
210        | cudf::ErrorCode::UnsupportedLayout
211        | cudf::ErrorCode::UnsupportedOperation => ErrorKind::UnsupportedType,
212        _ => ErrorKind::Unexpected,
213    };
214
215    let source_status = error.status();
216    let mut mapped = Error::new(kind, operation)
217        .with_message(message)
218        .with_context("cudf_code", error.code().to_string())
219        .with_context("cudf_kind", error.kind().to_string())
220        .with_context("cudf_status", source_status.to_string())
221        .with_context("cudf_operation", error.operation().to_string());
222
223    mapped = match source_status {
224        cudf::ErrorStatus::Permanent => mapped.set_permanent(),
225        cudf::ErrorStatus::Temporary => mapped.set_temporary(),
226        cudf::ErrorStatus::Persistent => mapped.set_persistent(),
227    };
228
229    mapped.set_source(error)
230}
231
232impl<T> Graph<T> {
233    /// The data type of vertex IDs in this graph (`Int32` or `Int64`).
234    pub fn vertex_dtype(&self) -> DataType {
235        self.vertex_dtype
236    }
237
238    /// The data type of edge weights in this graph (`Float32` or `Float64`).
239    ///
240    /// For unweighted graphs, this defaults to `Float32`.
241    pub fn weight_dtype(&self) -> DataType {
242        self.weight_dtype
243    }
244
245    pub(crate) fn has_edge_ids(&self) -> bool {
246        self.edge_id_dtype.is_some()
247    }
248
249    pub(crate) fn edge_id_dtype(&self) -> Option<DataType> {
250        self.edge_id_dtype
251    }
252
253    pub(crate) fn has_weights(&self) -> bool {
254        self.has_weights
255    }
256
257    pub(crate) fn with_pending_input_completion(mut self, pending: DeferredInputCompletion) -> Self {
258        self.pending_input_completion = Some(pending);
259        self
260    }
261
262    /// Waits for graph construction to finish reading prepared inputs and
263    /// releases their retained producer owners.
264    ///
265    /// Prepared construction remains asynchronous until this boundary, parking,
266    /// or graph destruction. This method waits on the construction completion
267    /// witnesses; it does not wait for algorithm work submitted afterward.
268    /// On failure, unfinished witnesses and their owners remain retained.
269    pub fn synchronize_input_reads(&mut self) -> Result<()> {
270        self.finish_input_reads("Graph::synchronize_input_reads")
271    }
272
273    fn finish_input_reads(&mut self, operation: &'static str) -> Result<()> {
274        if let Some(pending) = self.pending_input_completion.as_mut() {
275            pending.synchronize(operation)?;
276        }
277        self.pending_input_completion = None;
278        Ok(())
279    }
280
281    /// Park this graph for transfer to another Rust thread.
282    ///
283    /// Parking consumes the non-Send [`Graph`] and returns a [`ParkedGraph`]
284    /// that is `Send` but not `Sync`. The graph must have been built and used
285    /// only through runtime-bound handles for the same exact cudf execution
286    /// binding as `handle`; otherwise a single park event cannot prove all
287    /// graph work is ordered on one stream.
288    ///
289    /// Prepared-input completion witnesses are synchronized before parking so
290    /// cached graphs do not retain their input column owners indefinitely.
291    /// The ordering proof relies on runtime-bound cuGraph handles using the
292    /// cudf runtime stream directly, without a RAFT stream pool.
293    ///
294    /// If this method returns an error, the consumed graph is dropped.
295    pub fn park(mut self, handle: &EnteredContext<'_>) -> Result<ParkedGraph<T>>
296    where T: Send {
297        let operation = "Graph::park";
298        self.validate_runtime_bound_transfer(handle, operation)?;
299
300        self.finish_input_reads(operation)?;
301        self.park_recorded(handle, operation)
302    }
303
304    /// Park this graph under the ordered execution binding without requiring
305    /// it to match the construction binding (storage/execution split).
306    ///
307    /// The entering binding must be the ordered binding that executed all tracked
308    /// work, ensuring the recorded park event orders after preceding graph operations.
309    /// Teardown still runs under the retained creation leases. Use this after [`resume_under`](ParkedGraph::resume_under);
310    /// parking a graph that ran only under its construction binding must keep
311    /// using [`park`](Graph::park).
312    ///
313    /// If this method returns an error, the consumed graph is dropped.
314    pub fn park_under(mut self, handle: &EnteredContext<'_>) -> Result<ParkedGraph<T>>
315    where T: Send {
316        let operation = "Graph::park_under";
317        if handle.device() != self.device {
318            return Err(Error::new(ErrorKind::InvalidInput, operation)
319                .with_message("graph and resource handle must be on the same CUDA device")
320                .with_context("graph_device", self.device.to_string())
321                .with_context("handle_device", handle.device().to_string()));
322        }
323        let Some(handle_lease) = handle.runtime_bound_lease() else {
324            return Err(Error::new(ErrorKind::InvalidState, operation)
325                .with_message("park_under requires a runtime-bound resource handle"));
326        };
327        if self.untracked_resource_handle_use.get() {
328            return Err(Error::new(ErrorKind::InvalidState, operation)
329                .with_message("graph was used with a non-runtime-bound resource handle and cannot be parked"));
330        }
331        if self.resource_handle_leases.get_mut().is_empty() {
332            return Err(Error::new(ErrorKind::InvalidState, operation)
333                .with_message("graph has no runtime-bound handle leases to prove park stream ownership"));
334        }
335        let entering = handle_lease.runtime().binding_id();
336        if self.ordered_binding.get() != Some(entering) {
337            return Err(Error::new(ErrorKind::InvalidState, operation)
338                .with_message("park handle binding does not match the ordered graph execution binding")
339                .with_context(
340                    "graph_binding_id",
341                    self.ordered_binding.get().map(|id| id.to_string()).as_deref().unwrap_or("none"),
342                )
343                .with_context("handle_binding_id", entering.to_string()));
344        }
345        self.finish_input_reads(operation)?;
346        self.park_recorded(handle, operation)
347    }
348
349    fn park_recorded(self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<ParkedGraph<T>> {
350        let _guard = DeviceGuard::enter_owner_device(self.device, operation)?;
351        let event = CudaEvent::new()
352            .map_err(|error| map_cudf_runtime_error(error, operation, "failed to create graph park CUDA event"))?;
353        let runtime = handle.runtime().expect("validated runtime-bound handle");
354        event
355            .record(runtime.stream())
356            .map_err(|error| map_cudf_runtime_error(error, operation, "failed to record graph park CUDA event"))?;
357        let park_binding = runtime.binding_id();
358
359        let this = ManuallyDrop::new(self);
360        // SAFETY: `this` will not run `Graph::drop`, so moving the lease
361        // ledger out here transfers the only owned `RefCell`.
362        let resource_handle_leases = unsafe { ptr::read(&this.resource_handle_leases).into_inner() };
363
364        Ok(ParkedGraph {
365            raw: this.raw,
366            device: this.device,
367            properties: this.properties,
368            vertex_dtype: this.vertex_dtype,
369            weight_dtype: this.weight_dtype,
370            has_weights: this.has_weights,
371            edge_id_dtype: this.edge_id_dtype,
372            park_event: event,
373            park_binding,
374            resource_handle_leases,
375            _not_sync: PhantomData,
376            _transposition: PhantomData,
377        })
378    }
379
380    fn validate_runtime_bound_transfer(&mut self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<()> {
381        if handle.device() != self.device {
382            return Err(Error::new(ErrorKind::InvalidInput, operation)
383                .with_message("graph and resource handle must be on the same CUDA device")
384                .with_context("graph_device", self.device.to_string())
385                .with_context("handle_device", handle.device().to_string()));
386        }
387
388        let Some(handle_lease) = handle.runtime_bound_lease() else {
389            return Err(Error::new(ErrorKind::InvalidState, operation)
390                .with_message("parked graph transfer requires a runtime-bound resource handle"));
391        };
392        if self.untracked_resource_handle_use.get() {
393            return Err(Error::new(ErrorKind::InvalidState, operation)
394                .with_message("graph was used with a non-runtime-bound resource handle and cannot be parked"));
395        }
396
397        let leases = self.resource_handle_leases.get_mut();
398        if leases.is_empty() {
399            return Err(Error::new(ErrorKind::InvalidState, operation)
400                .with_message("graph has no runtime-bound handle leases to prove park stream ownership"));
401        }
402        if !leases.iter().all(|lease| lease.same_binding(&handle_lease)) {
403            return Err(Error::new(ErrorKind::InvalidState, operation)
404                .with_message("graph execution binding does not match the park handle")
405                .with_context("lease_count", leases.len().to_string()));
406        }
407        // Matching the storage leases is not enough once a graph can execute
408        // under a borrowed binding: the park event has to be recorded on the
409        // stream every tracked use ran on.
410        let entering = handle_lease.runtime().binding_id();
411        if self.ordered_binding.get().is_some_and(|ordered| ordered != entering) {
412            return Err(Error::new(ErrorKind::InvalidState, operation)
413                .with_message("park handle binding does not match the ordered graph execution binding")
414                .with_context(
415                    "graph_binding_id",
416                    self.ordered_binding.get().map(|id| id.to_string()).as_deref().unwrap_or("none"),
417                )
418                .with_context("handle_binding_id", entering.to_string()));
419        }
420
421        Ok(())
422    }
423
424    fn retain_runtime_bound_handle(&self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<()> {
425        if handle.device() != self.device {
426            return Err(Error::new(ErrorKind::InvalidInput, operation)
427                .with_message("graph and resource handle must be on the same CUDA device")
428                .with_context("graph_device", self.device.to_string())
429                .with_context("handle_device", handle.device().to_string()));
430        }
431
432        let Some(lease) = handle.runtime_bound_lease() else {
433            self.untracked_resource_handle_use.set(true);
434            return Ok(());
435        };
436
437        let mut leases = self.resource_handle_leases.borrow_mut();
438
439        // Every runtime-bound use must enter under the ordered binding, so
440        // the park event recorded later is ordered after all tracked work.
441        // The first runtime-bound use of a graph built outside a runtime
442        // binding establishes the order.
443        let entering = lease.runtime().binding_id();
444        match self.ordered_binding.get() {
445            Some(ordered) if ordered != entering => {
446                return Err(Error::new(ErrorKind::InvalidState, operation)
447                    .with_message("graph execution binding does not match the resource handle")
448                    .with_context("graph_binding_id", ordered.to_string())
449                    .with_context("handle_binding_id", entering.to_string()));
450            },
451            None => self.ordered_binding.set(Some(entering)),
452            Some(_) => {},
453        }
454
455        // Only the binding that owns the graph's native allocations is
456        // retained for teardown. A borrowed execution binding allocates just
457        // call-scoped workspace, so retaining its lease would keep its
458        // current-resource stream route; and the query domain behind it;
459        // attached for as long as the parked graph lives, long after the
460        // borrowing attempt released that stream.
461        let owns_teardown = leases.first().is_none_or(|existing| existing.same_binding(&lease));
462        if owns_teardown && !leases.iter().any(|existing| existing.same_native_owner(&lease)) {
463            leases.push(lease);
464        }
465
466        Ok(())
467    }
468
469    /// Structural properties declared when this graph was constructed.
470    ///
471    /// These are the graph-construction properties Rust passed to cuGraph
472    /// when creating this graph. They are reused by structural helpers such
473    /// as [`edge_count`](Self::edge_count) to interpret decompressed edge
474    /// lists with the same simple-vs-multigraph and directed-vs-undirected
475    /// semantics the graph was built with.
476    pub fn properties(&self) -> GraphProperties {
477        self.properties
478    }
479
480    /// Return the number of edges currently stored in this graph.
481    ///
482    /// Materializes a transient edgelist view via the cuGraph C API with O(|E|) cost.
483    ///
484    /// The returned count follows the graph's declared
485    /// [`GraphProperties`]:
486    ///
487    /// - directed simple graphs count unique directed edges
488    /// - undirected simple graphs count unique unordered edge pairs
489    /// - directed multigraphs count all stored directed edge rows
490    /// - undirected multigraphs count unordered edge multiplicities, with
491    ///   self-loops counted once each
492    pub fn edge_count(&mut self, handle: &EnteredContext<'_>) -> Result<usize> {
493        self.edge_count_impl(handle, false, "Graph::edge_count")
494    }
495
496    /// Return the number of edges currently stored in this graph with
497    /// optional expensive validation checks.
498    ///
499    /// This has the same counting semantics as [`edge_count`](Self::edge_count).
500    pub fn edge_count_checked(&mut self, handle: &EnteredContext<'_>, do_expensive_check: bool) -> Result<usize> {
501        self.edge_count_impl(handle, do_expensive_check, "Graph::edge_count_checked")
502    }
503
504    /// Select random vertices from this graph.
505    ///
506    /// The returned [`DeviceArray`] owns the sampled vertex ids on the handle
507    /// device. The requested vertex dtype must match this graph's vertex dtype.
508    pub fn select_random_vertices<V: CugraphElement>(
509        &self,
510        handle: &EnteredContext<'_>,
511        rng_state: &mut RngState,
512        num_vertices: usize,
513    ) -> Result<DeviceArray<V>> {
514        select_random_vertices(handle, self, rng_state, num_vertices)
515    }
516
517    pub(crate) fn as_ptr(&self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<*mut cugraph_graph_t> {
518        self.retain_runtime_bound_handle(handle, operation)?;
519        Ok(self.raw)
520    }
521
522    fn edge_count_impl(
523        &mut self,
524        handle: &EnteredContext<'_>,
525        do_expensive_check: bool,
526        operation: &'static str,
527    ) -> Result<usize> {
528        let mut edgelist: *mut cugraph_edgelist_t = ptr::null_mut();
529        let mut error: *mut cugraph_error_t = ptr::null_mut();
530
531        // SAFETY: `self.raw` is a live cuGraph graph handle and
532        // `edgelist`/`error` are valid out-pointers for the C API.
533        unsafe {
534            let code = cugraph_decompress_to_edgelist(
535                handle.handle_ptr(),
536                self.as_ptr(handle, operation)?,
537                to_bool_t(do_expensive_check),
538                &mut edgelist,
539                &mut error,
540            );
541            check_error(code, error, handle.device(), operation)?;
542        }
543
544        // SAFETY: on success, cuGraph transfers ownership of the edgelist
545        // pointer to the caller.
546        let edgelist =
547            unsafe { DecompressedEdgelist::from_raw(edgelist, handle.device(), handle.runtime_bound_lease()) }?;
548        edgelist.edge_count(handle, self.properties, operation)
549    }
550
551    pub(crate) fn validate_vertex_input_dtype(
552        &self,
553        dtype: DataType,
554        op: &'static str,
555        input_name: &'static str,
556    ) -> Result<()> {
557        if !dtype.is_supported_index_type() {
558            return Err(Error::new(ErrorKind::UnsupportedType, op)
559                .with_message(format!("{input_name} must use Int32 or Int64 vertex ids, got {dtype}")));
560        }
561
562        if dtype != self.vertex_dtype() {
563            return Err(Error::new(ErrorKind::UnsupportedType, op).with_message(format!(
564                "{input_name} dtype {dtype} does not match graph vertex dtype {}",
565                self.vertex_dtype()
566            )));
567        }
568
569        Ok(())
570    }
571
572    pub(crate) fn validate_vertex_selection<V: CugraphElement>(
573        &self,
574        vertices: &VertexSelection<'_, V>,
575        op: &'static str,
576        input_name: &'static str,
577        allow_all: bool,
578    ) -> Result<()> {
579        match vertices {
580            VertexSelection::All if allow_all => {},
581            VertexSelection::All => {
582                return Err(Error::new(ErrorKind::InvalidInput, op).with_message(format!(
583                    "{input_name} must specify an explicit vertex subset; VertexSelection::All is not supported"
584                )));
585            },
586            VertexSelection::Device(_) | VertexSelection::Host(_) => {
587                self.validate_vertex_input_dtype(V::data_type(), op, input_name)?;
588            },
589        }
590        Ok(())
591    }
592
593    pub(crate) fn resolved_vertex_view<'a, V: CugraphElement>(
594        &self,
595        resolved: &'a ResolvedVertexSelection<'_, V>,
596        op: &'static str,
597        input_name: &'static str,
598    ) -> Result<DeviceArrayView<'a, V>> {
599        resolved.view()?.ok_or_else(|| {
600            Error::new(ErrorKind::InvalidInput, op).with_message(format!(
601                "{input_name} must specify an explicit vertex subset; VertexSelection::All is not supported"
602            ))
603        })
604    }
605}
606
607impl<T> std::fmt::Debug for Graph<T> {
608    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609        f.debug_struct("Graph")
610            .field("properties", &self.properties)
611            .field("vertex_dtype", &self.vertex_dtype)
612            .field("weight_dtype", &self.weight_dtype)
613            .field("has_weights", &self.has_weights)
614            .finish_non_exhaustive()
615    }
616}
617
618fn free_graph_raw(raw: &mut *mut cugraph_graph_t, device: i32, operation: &'static str) {
619    drop_device_resource(raw, device, operation, DeviceResourceKind::Graph);
620}
621
622fn free_graph_raw_with_leases(
623    raw: &mut *mut cugraph_graph_t,
624    device: i32,
625    operation: &'static str,
626    leases: &[ResourceHandleLease],
627) {
628    if let Some(lease) = leases.first() {
629        // Teardown recovers exclusive access after use-path poison without
630        // reopening normal entry or exposing the native owner value.
631        lease.with_teardown_gate(|| free_graph_raw(raw, device, operation));
632    } else {
633        free_graph_raw(raw, device, operation);
634    }
635}
636
637impl<T> Drop for Graph<T> {
638    fn drop(&mut self) {
639        // Fence input reads before native teardown; a failed fence leaks the
640        // input owners (see `LeaseCompletion`).
641        drop(self.pending_input_completion.take());
642        free_graph_raw_with_leases(&mut self.raw, self.device, "Graph::drop", self.resource_handle_leases.get_mut());
643    }
644}
645
646#[cfg(test)]
647mod tests;