use super::storage::gpu::{GpuResource, GpuStorage};
use crate::{
compute::{
command::Command,
context::HipContext,
fence::Fence,
stream::{HipStreamBackend, StreamCaptureState},
},
runtime::HipCompiler,
};
use cubecl_common::{bytes::Bytes, profile::ProfileDuration};
use cubecl_core::{
MemoryConfiguration,
ir::MemoryDeviceProperties,
prelude::*,
server::{
Binding, CopyDescriptor, Handle, KernelArguments, ProfileError, ProfilingToken,
ServerCommunication, ServerError, ServerUtilities, StreamErrorMode,
},
};
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::future;
use cubecl_environment::future::DynFut;
use cubecl_environment::stream::StreamId;
use cubecl_runtime::{
allocator::PitchedMemoryLayoutPolicy,
compiler::CubeTask,
config::{CubeClRuntimeConfig, RuntimeConfig},
dry_run::LaunchMode,
id::GraphId,
logging::ServerLogger,
memory_management::{ManagedMemoryHandle, MemoryAllocationMode, MemoryUsage},
server::ComputeServer,
storage::{ComputeStorage, ManagedResource},
stream::MultiStream,
};
use std::collections::HashMap;
use crate::compute::graph::HipGraph;
use std::sync::Arc;
fn hip_check(op: &str, status: cubecl_hip_sys::hipError_t) -> Result<(), ServerError> {
if status == cubecl_hip_sys::HIP_SUCCESS {
Ok(())
} else {
Err(ServerError::Generic {
reason: format!("{op} failed with HIP status {status}"),
backtrace: BackTrace::capture(),
})
}
}
unsafe fn count_memory_nodes(graph: cubecl_hip_sys::hipGraph_t) -> usize {
let mut num_nodes: usize = 0;
let counted =
unsafe { cubecl_hip_sys::hipGraphGetNodes(graph, std::ptr::null_mut(), &mut num_nodes) };
if counted != cubecl_hip_sys::HIP_SUCCESS {
log::warn!(
"hipGraphGetNodes failed with HIP status {counted} while counting the graph's \
nodes; skipping the memory-node check for this capture"
);
return 0;
}
let mut nodes: Vec<cubecl_hip_sys::hipGraphNode_t> = vec![std::ptr::null_mut(); num_nodes];
let mut num_read = num_nodes;
let read =
unsafe { cubecl_hip_sys::hipGraphGetNodes(graph, nodes.as_mut_ptr(), &mut num_read) };
if read != cubecl_hip_sys::HIP_SUCCESS {
log::warn!(
"hipGraphGetNodes failed with HIP status {read} while reading the graph's \
{num_nodes} node(s); skipping the memory-node check for this capture"
);
return 0;
}
nodes
.iter()
.take(num_read)
.filter(|node| {
let mut ty: cubecl_hip_sys::hipGraphNodeType =
cubecl_hip_sys::hipGraphNodeType_hipGraphNodeTypeKernel;
let queried = unsafe { cubecl_hip_sys::hipGraphNodeGetType(**node, &mut ty) };
if queried != cubecl_hip_sys::HIP_SUCCESS {
log::warn!(
"hipGraphNodeGetType failed with HIP status {queried}; treating the node \
as not a memory node"
);
return false;
}
matches!(
ty,
cubecl_hip_sys::hipGraphNodeType_hipGraphNodeTypeMemAlloc
| cubecl_hip_sys::hipGraphNodeType_hipGraphNodeTypeMemFree
)
})
.count()
}
fn graph_state_error(reason: impl Into<String>) -> ServerError {
ServerError::Generic {
reason: reason.into(),
backtrace: BackTrace::capture(),
}
}
fn info_buffer(command: &mut Command<'_>, words: Vec<u64>) -> Result<Handle, ServerError> {
let size = core::mem::size_of_val(words.as_slice());
let cache_mode = command.streams.current().capturing.cache_mode();
command.streams.current().info_cache.mode(cache_mode);
if !command.streams.current().info_cache.should_cache(size) {
return Ok(command.create_with_data(bytemuck::cast_slice(&words))?);
}
if let Some(handle) = command.streams.current().info_cache.get(&words) {
return Ok(handle);
}
let handle = command.create_with_data(bytemuck::cast_slice(&words))?;
command
.streams
.current()
.info_cache
.insert(words, handle.clone());
Ok(handle)
}
#[derive(Debug)]
pub struct HipServer {
ctx: HipContext,
streams: MultiStream<HipStreamBackend>,
utilities: Arc<ServerUtilities<Self>>,
graphs: HashMap<GraphId, HipGraph>,
}
unsafe impl Send for HipServer {}
impl ComputeServer for HipServer {
type Kernel = Box<dyn CubeTask<HipCompiler>>;
type Storage = GpuStorage;
type MemoryLayoutPolicy = PitchedMemoryLayoutPolicy;
type Info = ();
fn logger(&self) -> Arc<ServerLogger> {
self.streams.logger.clone()
}
fn utilities(&self) -> Arc<ServerUtilities<Self>> {
self.utilities.clone()
}
fn staging(&mut self, sizes: &[usize], stream_id: StreamId) -> Result<Vec<Bytes>, ServerError> {
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
)?;
Ok(sizes
.iter()
.map(|size| command.reserve_cpu(*size, true, None))
.collect())
}
fn initialize_memory(&mut self, memory: ManagedMemoryHandle, size: u64, stream_id: StreamId) {
let mut command = match self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
) {
Ok(val) => val,
Err(err) => unreachable!("{err}"),
};
let reserved = command
.reserve(size)
.unwrap_or_else(|err| panic!("failed to reserve {size} bytes of device memory: {err}"));
command.bind(reserved, memory);
}
fn read(
&mut self,
descriptors: Vec<CopyDescriptor>,
stream_id: StreamId,
) -> DynFut<Result<Vec<Bytes>, ServerError>> {
match self.command(
stream_id,
descriptors.iter().map(|d| &d.handle),
StreamErrorMode {
ignore: false,
flush: true,
},
) {
Ok(mut command) => Box::pin(command.read_async(descriptors)),
Err(err) => Box::pin(async move { Err(err) }),
}
}
fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId) {
let mut command = match self.command(
stream_id,
descriptors.iter().map(|desc| &desc.0.handle),
StreamErrorMode {
ignore: true,
flush: false,
},
) {
Ok(val) => val,
Err(err) => unreachable!("{err}"),
};
for (descriptor, data) in descriptors {
if let Err(err) = command.write_to_gpu(descriptor, data) {
command.error(err.into());
return;
}
}
}
unsafe fn launch(
&mut self,
kernel: Self::Kernel,
count: CubeCount,
bindings: KernelArguments,
mode: ExecutionMode,
stream_id: StreamId,
launch_mode: LaunchMode,
) {
if let Err(err) = self.launch_checked(kernel, count, bindings, mode, stream_id, launch_mode)
{
let mut stream = match self.streams.resolve(stream_id, [].into_iter(), false) {
Ok(stream) => stream,
Err(err) => unreachable!("{err}"),
};
stream.current().errors.push(err);
}
}
fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: false,
flush: true,
},
)?;
let current = command.streams.current();
current.drop_queue.flush(|| Fence::new(current.sys));
current.memory_management_gpu.storage().flush();
Ok(())
}
fn graph_prepare(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: false,
flush: true,
},
)?;
let stream = command.streams.current();
match stream.capturing {
StreamCaptureState::NoCapture => {}
StreamCaptureState::Prepare => {
return Err(graph_state_error(
"graph_prepare: a graph capture is already prepared on this stream",
));
}
StreamCaptureState::Capture => {
return Err(graph_state_error(
"graph_prepare: a graph capture is already recording on this stream",
));
}
}
stream.memory_management_gpu.capture_begin();
stream.memory_management_cpu.capture_begin();
stream.capturing = StreamCaptureState::Prepare;
Ok(())
}
fn begin_capture(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: false,
flush: true,
},
)?;
let stream = command.streams.current();
match stream.capturing {
StreamCaptureState::Prepare => {}
StreamCaptureState::NoCapture => {
return Err(graph_state_error(
"begin_capture: call graph_prepare before starting a capture",
));
}
StreamCaptureState::Capture => {
return Err(graph_state_error(
"begin_capture: a graph capture is already recording on this stream",
));
}
}
let sys = stream.sys;
stream.drop_queue.flush(|| Fence::new(sys));
stream.drop_queue.flush(|| Fence::new(sys));
stream.memory_management_gpu.capture_priming_end();
stream.memory_management_cpu.capture_priming_end();
let status = unsafe {
cubecl_hip_sys::hipStreamBeginCapture(
stream.sys,
cubecl_hip_sys::hipStreamCaptureMode_hipStreamCaptureModeGlobal,
)
};
if let Err(err) = hip_check("hipStreamBeginCapture", status) {
stream.memory_management_gpu.capture_end();
stream.memory_management_cpu.capture_end();
stream.info_cache.capture_discard();
stream.capturing = StreamCaptureState::NoCapture;
return Err(err);
}
stream.capturing = StreamCaptureState::Capture;
Ok(())
}
fn end_capture(&mut self, stream_id: StreamId) -> Result<GraphId, ServerError> {
let id = GraphId::new();
let hip_graph = {
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
)?;
let stream = command.streams.current();
if !stream.capturing.is_recording() {
return Err(graph_state_error(
"end_capture: no graph capture is recording on this stream",
));
}
let exec = unsafe {
let mut graph: cubecl_hip_sys::hipGraph_t = std::ptr::null_mut();
hip_check(
"hipStreamEndCapture",
cubecl_hip_sys::hipStreamEndCapture(stream.sys, &mut graph),
)
.and_then(|_| {
let alloc_nodes = count_memory_nodes(graph);
if alloc_nodes > 0 {
cubecl_hip_sys::hipGraphDestroy(graph);
return Err(graph_state_error(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 persistent pool should have served this allocation)"
)));
}
let mut exec: cubecl_hip_sys::hipGraphExec_t = std::ptr::null_mut();
let instantiated = hip_check(
"hipGraphInstantiate",
cubecl_hip_sys::hipGraphInstantiate(
&mut exec,
graph,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
),
);
cubecl_hip_sys::hipGraphDestroy(graph);
instantiated.map(|_| exec)
})
};
stream.capturing = StreamCaptureState::NoCapture;
let mut retained = stream.memory_management_gpu.capture_end();
retained.extend(stream.memory_management_cpu.capture_end());
let sys = stream.sys;
stream.drop_queue.flush(|| Fence::new(sys));
stream.drop_queue.flush(|| Fence::new(sys));
match exec {
Ok(exec) => {
stream.info_cache.capture_commit(id);
let uploaded = unsafe { cubecl_hip_sys::hipGraphUpload(exec, sys) };
if let Err(err) = hip_check("hipGraphUpload", uploaded) {
log::warn!(
"Pre-uploading the captured graph failed; \
the first replay will upload on demand: {err}"
);
}
HipGraph {
exec,
_retained: retained,
}
}
Err(err) => {
stream.info_cache.capture_discard();
return Err(err);
}
}
};
self.graphs.insert(id, hip_graph);
Ok(id)
}
fn replay(&mut self, graph: GraphId, stream_id: StreamId) {
if let Err(err) = self.replay_checked(graph, stream_id) {
let mut stream = match self.streams.resolve(stream_id, [].into_iter(), false) {
Ok(stream) => stream,
Err(err) => unreachable!("{err}"),
};
stream.current().errors.push(err);
}
}
fn graph_destroy(&mut self, graph: GraphId, stream_id: StreamId) {
if !self.graphs.contains_key(&graph) {
return;
}
let synced = cubecl_environment::future::block_on(self.sync(stream_id));
self.graphs.remove(&graph);
if let Ok(mut streams) = self.streams.resolve(stream_id, [].into_iter(), false) {
let stream = streams.current();
stream.info_cache.graph_release(graph);
if let Err(err) = synced {
stream.errors.push(err);
}
}
}
fn sync(&mut self, stream_id: StreamId) -> DynFut<Result<(), ServerError>> {
let command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: false,
flush: true,
},
);
match command {
Ok(mut command) => command.sync(),
Err(err) => Box::pin(async { Err(err) }),
}
}
fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError> {
cubecl_environment::future::block_on(self.sync(stream_id))?;
Ok(self.ctx.timestamps.start())
}
fn end_profile(
&mut self,
stream_id: StreamId,
token: ProfilingToken,
) -> Result<ProfileDuration, ProfileError> {
if let Err(err) = cubecl_environment::future::block_on(self.sync(stream_id)) {
self.ctx
.timestamps
.error(ProfileError::Server(Box::new(err)));
}
self.ctx.timestamps.stop(token)
}
fn get_resource(
&mut self,
binding: Binding,
stream_id: StreamId,
) -> Result<ManagedResource<GpuResource>, ServerError> {
let mut command = self.command(
stream_id,
[&binding].into_iter(),
StreamErrorMode {
ignore: true,
flush: false,
},
)?;
let memory = binding.memory.clone();
let resource = command.resource(binding)?;
Ok(ManagedResource::new(memory, resource))
}
fn memory_usage(&mut self, stream_id: StreamId) -> Result<MemoryUsage, ServerError> {
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: false,
flush: false,
},
)?;
Ok(command.memory_usage())
}
fn stream_ids(&self) -> Vec<StreamId> {
self.streams.stream_ids().collect()
}
fn memory_cleanup(&mut self, stream_id: StreamId) {
let mut command = match self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
) {
Ok(val) => val,
Err(_) => return,
};
command.memory_cleanup()
}
fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId) {
let mut command = match self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
) {
Ok(val) => val,
Err(err) => unreachable!("{err}"),
};
command.allocation_mode(mode)
}
fn configure_memory_pools(&mut self, config: MemoryConfiguration, stream_id: StreamId) -> bool {
self.streams.backend_mut().set_gpu_pools(config.clone());
let (_, props) = self.streams.backend_mut().gpu_pools();
let mut command = match self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
) {
Ok(val) => val,
Err(_) => return false,
};
command.configure_memory_pools(config, &props)
}
}
impl ServerCommunication for HipServer {
const SERVER_COMM_ENABLED: bool = false;
}
impl HipServer {
pub(crate) fn new(
ctx: HipContext,
mem_props: MemoryDeviceProperties,
mem_config: MemoryConfiguration,
mem_alignment: usize,
is_integrated: bool,
utilities: ServerUtilities<Self>,
) -> Self {
let config = CubeClRuntimeConfig::get();
let max_streams = config.streaming.max_streams;
Self {
ctx,
streams: MultiStream::new(
utilities.logger.clone(),
HipStreamBackend::new(
mem_props,
mem_config,
mem_alignment,
is_integrated,
utilities.logger.clone(),
),
max_streams,
),
utilities: Arc::new(utilities),
graphs: HashMap::new(),
}
}
fn command_no_inputs(
&mut self,
stream_id: StreamId,
mode: StreamErrorMode,
) -> Result<Command<'_>, ServerError> {
self.command(stream_id, [].into_iter(), mode)
}
fn command<'a>(
&mut self,
stream_id: StreamId,
handles: impl Iterator<Item = &'a Binding>,
mode: StreamErrorMode,
) -> Result<Command<'_>, ServerError> {
if mode.flush {
let errors = self.flush_errors(stream_id);
if !mode.ignore && !errors.is_empty() {
return Err(ServerError::ServerUnhealthy {
errors,
backtrace: BackTrace::capture(),
});
}
}
let streams = self.streams.resolve(stream_id, handles, !mode.ignore)?;
Ok(Command::new(&mut self.ctx, streams))
}
fn flush_errors(&mut self, stream_id: StreamId) -> Vec<ServerError> {
let mut stream = match self.streams.resolve(stream_id, [].into_iter(), false) {
Ok(stream) => stream,
Err(_) => return Vec::new(),
};
let errors = core::mem::take(&mut stream.current().errors);
if !errors.is_empty() {
self.ctx.timestamps.error(ProfileError::Unknown {
reason: alloc::format!("{errors:?}"),
backtrace: BackTrace::capture(),
});
stream.current().memory_management_gpu.cleanup(false);
}
core::mem::drop(stream);
errors
}
fn launch_checked(
&mut self,
kernel: Box<dyn CubeTask<HipCompiler>>,
count: CubeCount,
bindings: KernelArguments,
mode: ExecutionMode,
stream_id: StreamId,
launch_mode: LaunchMode,
) -> Result<(), ServerError> {
let mut kernel_id = kernel.id();
let logger = self.streams.logger.clone();
kernel_id.mode(mode);
let mut command = self.command(
stream_id,
bindings.buffers.iter(),
StreamErrorMode {
ignore: true,
flush: false,
},
)?;
let count = match count {
CubeCount::Static(x, y, z) => (x, y, z),
CubeCount::Dynamic(binding) => {
let data = future::block_on(command.read_async(vec![CopyDescriptor::new(
binding,
[3].into(),
[1].into(),
4,
)]))
.unwrap();
let data = bytemuck::cast_slice(&data[0]);
assert!(
data.len() == 3,
"Dynamic cube count should contain 3 values"
);
(data[0], data[1], data[2])
}
};
if count.0 == 0 || count.1 == 0 || count.2 == 0 {
return Ok(());
}
let KernelArguments {
buffers,
info,
tensor_maps,
} = bindings;
debug_assert!(tensor_maps.is_empty(), "Can't use tensor maps on HIP");
let info_handle = info_buffer(&mut command, info.data)?;
let mut resources: Vec<_> = buffers
.into_iter()
.map(|b| command.resource(b).expect("Resource to exist."))
.collect();
resources.push(
command
.resource(info_handle.binding())
.expect("Resource to exist."),
);
command.kernel(
kernel_id,
kernel,
mode,
count,
&resources,
logger,
launch_mode,
)?;
Ok(())
}
fn replay_checked(&mut self, graph: GraphId, stream_id: StreamId) -> Result<(), ServerError> {
let exec =
self.graphs
.get(&graph)
.map(|hip| hip.exec)
.ok_or_else(|| ServerError::Generic {
reason: "replay was given an unknown or already-destroyed graph".into(),
backtrace: BackTrace::capture(),
})?;
let mut command = self.command_no_inputs(
stream_id,
StreamErrorMode {
ignore: true,
flush: false,
},
)?;
let stream = command.streams.current();
let status = unsafe { cubecl_hip_sys::hipGraphLaunch(exec, stream.sys) };
hip_check("hipGraphLaunch", status)
}
pub(crate) fn utilities(&self) -> Arc<ServerUtilities<Self>> {
self.utilities.clone()
}
}