use std::{
cell::{Cell, RefCell},
marker::PhantomData,
mem::ManuallyDrop,
ptr,
};
use cudf::{
interop::contract::{InteropError, LeaseCompletion},
runtime::{CudaEvent, ExecutionBindingId},
};
use cugraph_sys::{cugraph_decompress_to_edgelist, cugraph_edgelist_t, cugraph_error_t, cugraph_graph_t};
use crate::{
array::{DeviceArray, DeviceArrayView},
cuda::{DeviceGuard, DeviceResourceKind, drop_device_resource},
error::{Error, ErrorKind, Result, check_error},
handle::{EnteredContext, ResourceHandleLease},
random::RngState,
sampling::select_random_vertices,
structure::{ResolvedVertexSelection, VertexSelection},
types::{CugraphElement, DataType, GraphProperties, to_bool_t},
};
mod algorithms;
mod builder;
mod edgelist;
mod parked;
pub use builder::GraphBuilder;
pub(crate) use edgelist::DecompressedEdgelist;
pub use parked::ParkedGraph;
mod sealed {
pub trait Sealed {}
impl Sealed for super::Forward {}
impl Sealed for super::Transposed {}
}
pub trait Transposition: sealed::Sealed {
#[doc(hidden)]
fn store_transposed() -> bool;
}
pub struct Forward;
impl Transposition for Forward {
fn store_transposed() -> bool {
false
}
}
pub struct Transposed;
impl Transposition for Transposed {
fn store_transposed() -> bool {
true
}
}
pub struct Graph<T = Forward> {
raw: *mut cugraph_graph_t,
device: i32,
properties: GraphProperties,
vertex_dtype: DataType,
weight_dtype: DataType,
has_weights: bool,
edge_id_dtype: Option<DataType>,
pending_input_completion: Option<DeferredInputCompletion>,
resource_handle_leases: RefCell<Vec<ResourceHandleLease>>,
ordered_binding: Cell<Option<ExecutionBindingId>>,
untracked_resource_handle_use: Cell<bool>,
_nosend_nosync: PhantomData<*mut ()>,
_transposition: PhantomData<T>,
}
pub(crate) struct DeferredInputCompletion {
completions: Vec<LeaseCompletion>,
}
impl DeferredInputCompletion {
pub(crate) fn new(completions: Vec<LeaseCompletion>) -> Option<Self> {
if completions.is_empty() {
return None;
}
Some(Self { completions })
}
pub(crate) fn synchronize(&mut self, operation: &'static str) -> Result<()> {
for completion in &mut self.completions {
completion.synchronize().map_err(|error| map_completion_witness_error(error, operation))?;
}
Ok(())
}
}
fn map_completion_witness_error(error: InteropError, operation: &'static str) -> Error {
let mut mapped = Error::new(ErrorKind::InvalidState, operation)
.with_message("failed to synchronize graph input completion witness")
.with_context("interop_kind", format!("{:?}", error.kind()))
.with_context("interop_message", error.message().to_string());
for (key, value) in error.context() {
mapped = mapped.with_context(key, value.clone());
}
mapped.set_source(error)
}
fn map_cudf_runtime_error(error: cudf::Error, operation: &'static str, message: &'static str) -> Error {
let kind = match error.code() {
cudf::ErrorCode::OutOfMemory | cudf::ErrorCode::ForeignResourceExhausted => ErrorKind::OutOfMemory,
cudf::ErrorCode::CudaRuntime
| cudf::ErrorCode::FatalCuda
| cudf::ErrorCode::ReadinessUnavailable
| cudf::ErrorCode::ForeignTransientDependencyFailure
| cudf::ErrorCode::ForeignPersistentDependencyFailure => ErrorKind::Unexpected,
cudf::ErrorCode::RuntimeBindingMismatch
| cudf::ErrorCode::ExecutionBindingUnavailable
| cudf::ErrorCode::InvariantViolation
| cudf::ErrorCode::ForeignInvariantViolation => ErrorKind::InvalidState,
cudf::ErrorCode::CrossDeviceInputs | cudf::ErrorCode::CrossProvenanceInputs => ErrorKind::InvalidInput,
cudf::ErrorCode::UnsupportedDtype
| cudf::ErrorCode::UnsupportedLayout
| cudf::ErrorCode::UnsupportedOperation => ErrorKind::UnsupportedType,
_ => ErrorKind::Unexpected,
};
let source_status = error.status();
let mut mapped = Error::new(kind, operation)
.with_message(message)
.with_context("cudf_code", error.code().to_string())
.with_context("cudf_kind", error.kind().to_string())
.with_context("cudf_status", source_status.to_string())
.with_context("cudf_operation", error.operation().to_string());
mapped = match source_status {
cudf::ErrorStatus::Permanent => mapped.set_permanent(),
cudf::ErrorStatus::Temporary => mapped.set_temporary(),
cudf::ErrorStatus::Persistent => mapped.set_persistent(),
};
mapped.set_source(error)
}
impl<T> Graph<T> {
pub fn vertex_dtype(&self) -> DataType {
self.vertex_dtype
}
pub fn weight_dtype(&self) -> DataType {
self.weight_dtype
}
pub(crate) fn has_edge_ids(&self) -> bool {
self.edge_id_dtype.is_some()
}
pub(crate) fn edge_id_dtype(&self) -> Option<DataType> {
self.edge_id_dtype
}
pub(crate) fn has_weights(&self) -> bool {
self.has_weights
}
pub(crate) fn with_pending_input_completion(mut self, pending: DeferredInputCompletion) -> Self {
self.pending_input_completion = Some(pending);
self
}
pub fn synchronize_input_reads(&mut self) -> Result<()> {
self.finish_input_reads("Graph::synchronize_input_reads")
}
fn finish_input_reads(&mut self, operation: &'static str) -> Result<()> {
if let Some(pending) = self.pending_input_completion.as_mut() {
pending.synchronize(operation)?;
}
self.pending_input_completion = None;
Ok(())
}
pub fn park(mut self, handle: &EnteredContext<'_>) -> Result<ParkedGraph<T>>
where T: Send {
let operation = "Graph::park";
self.validate_runtime_bound_transfer(handle, operation)?;
self.finish_input_reads(operation)?;
self.park_recorded(handle, operation)
}
pub fn park_under(mut self, handle: &EnteredContext<'_>) -> Result<ParkedGraph<T>>
where T: Send {
let operation = "Graph::park_under";
if handle.device() != self.device {
return Err(Error::new(ErrorKind::InvalidInput, operation)
.with_message("graph and resource handle must be on the same CUDA device")
.with_context("graph_device", self.device.to_string())
.with_context("handle_device", handle.device().to_string()));
}
let Some(handle_lease) = handle.runtime_bound_lease() else {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("park_under requires a runtime-bound resource handle"));
};
if self.untracked_resource_handle_use.get() {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("graph was used with a non-runtime-bound resource handle and cannot be parked"));
}
if self.resource_handle_leases.get_mut().is_empty() {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("graph has no runtime-bound handle leases to prove park stream ownership"));
}
let entering = handle_lease.runtime().binding_id();
if self.ordered_binding.get() != Some(entering) {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("park handle binding does not match the ordered graph execution binding")
.with_context(
"graph_binding_id",
self.ordered_binding.get().map(|id| id.to_string()).as_deref().unwrap_or("none"),
)
.with_context("handle_binding_id", entering.to_string()));
}
self.finish_input_reads(operation)?;
self.park_recorded(handle, operation)
}
fn park_recorded(self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<ParkedGraph<T>> {
let _guard = DeviceGuard::enter_owner_device(self.device, operation)?;
let event = CudaEvent::new()
.map_err(|error| map_cudf_runtime_error(error, operation, "failed to create graph park CUDA event"))?;
let runtime = handle.runtime().expect("validated runtime-bound handle");
event
.record(runtime.stream())
.map_err(|error| map_cudf_runtime_error(error, operation, "failed to record graph park CUDA event"))?;
let park_binding = runtime.binding_id();
let this = ManuallyDrop::new(self);
let resource_handle_leases = unsafe { ptr::read(&this.resource_handle_leases).into_inner() };
Ok(ParkedGraph {
raw: this.raw,
device: this.device,
properties: this.properties,
vertex_dtype: this.vertex_dtype,
weight_dtype: this.weight_dtype,
has_weights: this.has_weights,
edge_id_dtype: this.edge_id_dtype,
park_event: event,
park_binding,
resource_handle_leases,
_not_sync: PhantomData,
_transposition: PhantomData,
})
}
fn validate_runtime_bound_transfer(&mut self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<()> {
if handle.device() != self.device {
return Err(Error::new(ErrorKind::InvalidInput, operation)
.with_message("graph and resource handle must be on the same CUDA device")
.with_context("graph_device", self.device.to_string())
.with_context("handle_device", handle.device().to_string()));
}
let Some(handle_lease) = handle.runtime_bound_lease() else {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("parked graph transfer requires a runtime-bound resource handle"));
};
if self.untracked_resource_handle_use.get() {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("graph was used with a non-runtime-bound resource handle and cannot be parked"));
}
let leases = self.resource_handle_leases.get_mut();
if leases.is_empty() {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("graph has no runtime-bound handle leases to prove park stream ownership"));
}
if !leases.iter().all(|lease| lease.same_binding(&handle_lease)) {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("graph execution binding does not match the park handle")
.with_context("lease_count", leases.len().to_string()));
}
let entering = handle_lease.runtime().binding_id();
if self.ordered_binding.get().is_some_and(|ordered| ordered != entering) {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("park handle binding does not match the ordered graph execution binding")
.with_context(
"graph_binding_id",
self.ordered_binding.get().map(|id| id.to_string()).as_deref().unwrap_or("none"),
)
.with_context("handle_binding_id", entering.to_string()));
}
Ok(())
}
fn retain_runtime_bound_handle(&self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<()> {
if handle.device() != self.device {
return Err(Error::new(ErrorKind::InvalidInput, operation)
.with_message("graph and resource handle must be on the same CUDA device")
.with_context("graph_device", self.device.to_string())
.with_context("handle_device", handle.device().to_string()));
}
let Some(lease) = handle.runtime_bound_lease() else {
self.untracked_resource_handle_use.set(true);
return Ok(());
};
let mut leases = self.resource_handle_leases.borrow_mut();
let entering = lease.runtime().binding_id();
match self.ordered_binding.get() {
Some(ordered) if ordered != entering => {
return Err(Error::new(ErrorKind::InvalidState, operation)
.with_message("graph execution binding does not match the resource handle")
.with_context("graph_binding_id", ordered.to_string())
.with_context("handle_binding_id", entering.to_string()));
},
None => self.ordered_binding.set(Some(entering)),
Some(_) => {},
}
let owns_teardown = leases.first().is_none_or(|existing| existing.same_binding(&lease));
if owns_teardown && !leases.iter().any(|existing| existing.same_native_owner(&lease)) {
leases.push(lease);
}
Ok(())
}
pub fn properties(&self) -> GraphProperties {
self.properties
}
pub fn edge_count(&mut self, handle: &EnteredContext<'_>) -> Result<usize> {
self.edge_count_impl(handle, false, "Graph::edge_count")
}
pub fn edge_count_checked(&mut self, handle: &EnteredContext<'_>, do_expensive_check: bool) -> Result<usize> {
self.edge_count_impl(handle, do_expensive_check, "Graph::edge_count_checked")
}
pub fn select_random_vertices<V: CugraphElement>(
&self,
handle: &EnteredContext<'_>,
rng_state: &mut RngState,
num_vertices: usize,
) -> Result<DeviceArray<V>> {
select_random_vertices(handle, self, rng_state, num_vertices)
}
pub(crate) fn as_ptr(&self, handle: &EnteredContext<'_>, operation: &'static str) -> Result<*mut cugraph_graph_t> {
self.retain_runtime_bound_handle(handle, operation)?;
Ok(self.raw)
}
fn edge_count_impl(
&mut self,
handle: &EnteredContext<'_>,
do_expensive_check: bool,
operation: &'static str,
) -> Result<usize> {
let mut edgelist: *mut cugraph_edgelist_t = ptr::null_mut();
let mut error: *mut cugraph_error_t = ptr::null_mut();
unsafe {
let code = cugraph_decompress_to_edgelist(
handle.handle_ptr(),
self.as_ptr(handle, operation)?,
to_bool_t(do_expensive_check),
&mut edgelist,
&mut error,
);
check_error(code, error, handle.device(), operation)?;
}
let edgelist =
unsafe { DecompressedEdgelist::from_raw(edgelist, handle.device(), handle.runtime_bound_lease()) }?;
edgelist.edge_count(handle, self.properties, operation)
}
pub(crate) fn validate_vertex_input_dtype(
&self,
dtype: DataType,
op: &'static str,
input_name: &'static str,
) -> Result<()> {
if !dtype.is_supported_index_type() {
return Err(Error::new(ErrorKind::UnsupportedType, op)
.with_message(format!("{input_name} must use Int32 or Int64 vertex ids, got {dtype}")));
}
if dtype != self.vertex_dtype() {
return Err(Error::new(ErrorKind::UnsupportedType, op).with_message(format!(
"{input_name} dtype {dtype} does not match graph vertex dtype {}",
self.vertex_dtype()
)));
}
Ok(())
}
pub(crate) fn validate_vertex_selection<V: CugraphElement>(
&self,
vertices: &VertexSelection<'_, V>,
op: &'static str,
input_name: &'static str,
allow_all: bool,
) -> Result<()> {
match vertices {
VertexSelection::All if allow_all => {},
VertexSelection::All => {
return Err(Error::new(ErrorKind::InvalidInput, op).with_message(format!(
"{input_name} must specify an explicit vertex subset; VertexSelection::All is not supported"
)));
},
VertexSelection::Device(_) | VertexSelection::Host(_) => {
self.validate_vertex_input_dtype(V::data_type(), op, input_name)?;
},
}
Ok(())
}
pub(crate) fn resolved_vertex_view<'a, V: CugraphElement>(
&self,
resolved: &'a ResolvedVertexSelection<'_, V>,
op: &'static str,
input_name: &'static str,
) -> Result<DeviceArrayView<'a, V>> {
resolved.view()?.ok_or_else(|| {
Error::new(ErrorKind::InvalidInput, op).with_message(format!(
"{input_name} must specify an explicit vertex subset; VertexSelection::All is not supported"
))
})
}
}
impl<T> std::fmt::Debug for Graph<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Graph")
.field("properties", &self.properties)
.field("vertex_dtype", &self.vertex_dtype)
.field("weight_dtype", &self.weight_dtype)
.field("has_weights", &self.has_weights)
.finish_non_exhaustive()
}
}
fn free_graph_raw(raw: &mut *mut cugraph_graph_t, device: i32, operation: &'static str) {
drop_device_resource(raw, device, operation, DeviceResourceKind::Graph);
}
fn free_graph_raw_with_leases(
raw: &mut *mut cugraph_graph_t,
device: i32,
operation: &'static str,
leases: &[ResourceHandleLease],
) {
if let Some(lease) = leases.first() {
lease.with_teardown_gate(|| free_graph_raw(raw, device, operation));
} else {
free_graph_raw(raw, device, operation);
}
}
impl<T> Drop for Graph<T> {
fn drop(&mut self) {
drop(self.pending_input_completion.take());
free_graph_raw_with_leases(&mut self.raw, self.device, "Graph::drop", self.resource_handle_leases.get_mut());
}
}
#[cfg(test)]
mod tests;