use crate::compute::driver::Cuda;
use crate::compute::stream::Stream;
use cubecl_environment::backtrace::BackTrace;
use cubecl_server::command::GraphDriver;
use cubecl_server::server::ServerError;
use cudarc::driver::sys::{CUgraph, CUgraphExec, CUgraphNode, CUresult, CUstream};
pub struct Executable(CUgraphExec);
impl Drop for Executable {
fn drop(&mut self) {
unsafe {
cudarc::driver::sys::cuGraphExecDestroy(self.0);
}
}
}
impl GraphDriver for Cuda {
type Executable = Executable;
fn begin(stream: &mut Stream) -> Result<(), ServerError> {
let status = unsafe {
cudarc::driver::sys::cuStreamBeginCapture_v2(
stream.sys,
cudarc::driver::sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_GLOBAL,
)
};
checked("cuStreamBeginCapture", status)
}
fn instantiate(
stream: &mut Stream,
doomed: Option<ServerError>,
) -> Result<Executable, ServerError> {
unsafe { instantiate_recording(stream.sys, doomed) }
}
fn upload(exec: &Executable, stream: &mut Stream) {
let uploaded = unsafe { cudarc::driver::sys::cuGraphUpload(exec.0, stream.sys) };
if let Err(err) = checked("cuGraphUpload", uploaded) {
log::warn!(
"Pre-uploading the captured graph failed; \
the first replay will upload on demand: {err}"
);
}
}
fn replay(exec: &Executable, stream: &mut Stream) -> Result<(), ServerError> {
let status = unsafe { cudarc::driver::sys::cuGraphLaunch(exec.0, stream.sys) };
checked("cuGraphLaunch", status)
}
}
unsafe fn instantiate_recording(
sys: CUstream,
doomed: Option<ServerError>,
) -> Result<Executable, ServerError> {
unsafe {
let mut graph: CUgraph = std::ptr::null_mut();
checked(
"cuStreamEndCapture",
cudarc::driver::sys::cuStreamEndCapture(sys, &mut graph),
)?;
if let Some(doomed) = doomed {
cudarc::driver::sys::cuGraphDestroy(graph);
return Err(doomed);
}
let alloc_nodes = count_memory_nodes(graph);
if alloc_nodes > 0 {
cudarc::driver::sys::cuGraphDestroy(graph);
return Err(ServerError::graph_state(format!(
"capture recorded {alloc_nodes} memory node(s): an allocation inside the capture \
window makes the graph un-relaunchable, so the capture is rejected (the \
pages held before the capture should have served this allocation)"
)));
}
let mut exec: CUgraphExec = std::ptr::null_mut();
let instantiated = checked(
"cuGraphInstantiateWithFlags",
cudarc::driver::sys::cuGraphInstantiateWithFlags(&mut exec, graph, 0),
);
cudarc::driver::sys::cuGraphDestroy(graph);
instantiated.map(|_| Executable(exec))
}
}
unsafe fn count_memory_nodes(graph: CUgraph) -> usize {
let mut num_nodes: usize = 0;
let counted = unsafe {
cudarc::driver::sys::cuGraphGetNodes(graph, std::ptr::null_mut(), &mut num_nodes)
};
if let Err(err) = checked("cuGraphGetNodes", counted) {
log::warn!("{err} while counting the graph's nodes; skipping the memory-node check");
return 0;
}
let mut nodes: Vec<CUgraphNode> = vec![std::ptr::null_mut(); num_nodes];
let mut num_read = num_nodes;
let read =
unsafe { cudarc::driver::sys::cuGraphGetNodes(graph, nodes.as_mut_ptr(), &mut num_read) };
if let Err(err) = checked("cuGraphGetNodes", read) {
log::warn!(
"{err} while reading the graph's {num_nodes} node(s); \
skipping the memory-node check"
);
return 0;
}
nodes
.iter()
.take(num_read)
.filter(|node| {
let mut ty = cudarc::driver::sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_KERNEL;
let queried = unsafe { cudarc::driver::sys::cuGraphNodeGetType(**node, &mut ty) };
if let Err(err) = checked("cuGraphNodeGetType", queried) {
log::warn!("{err}; treating the node as not a memory node");
return false;
}
matches!(
ty,
cudarc::driver::sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_ALLOC
| cudarc::driver::sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_FREE
)
})
.count()
}
fn checked(op: &str, status: CUresult) -> Result<(), ServerError> {
status.result().map_err(|err| ServerError::Generic {
reason: format!("{op} failed: {err}"),
backtrace: BackTrace::capture(),
})
}