use crate::compute::copies::MetalCopies;
use crate::memory::MetalStorage;
use cubecl_core::{MemoryConfiguration, server::ServerError};
use cubecl_environment::sync::Mutex;
use cubecl_ir::MemoryDeviceProperties;
use cubecl_server::memory_management::relocation::RelocationReason;
use cubecl_server::{
logging::ServerLogger,
memory_management::{ErrorGraph, FailureId, MemoryManagement, MemoryManagementOptions},
server::BufferBinding,
stream::{EventStreamBackend, StreamMemory},
};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBuffer, MTLCommandBuffer, MTLCommandBufferStatus, MTLCommandQueue, MTLComputeCommandEncoder,
MTLDevice, MTLSharedEvent,
};
use std::ptr::NonNull;
use std::sync::Arc;
pub struct ActiveEncoder {
pub command_buffer: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
pub encoder: Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>,
pub temporaries: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
}
fn install_completion_handler(
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
temporaries: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
signal_event: Option<(Retained<ProtocolObject<dyn MTLSharedEvent>>, u64)>,
fault: Arc<Mutex<Option<String>>>,
) {
let temporaries = Mutex::new(Some(temporaries));
let block = block2::RcBlock::new(
move |cmd_buf: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
let _ = temporaries.lock().take();
let cmd_buf = unsafe { cmd_buf.as_ref() };
if cmd_buf.status() == MTLCommandBufferStatus::Error {
let reason = match cmd_buf.error() {
Some(err) => format!(
"Metal command buffer failed: {}",
err.localizedDescription()
),
None => "Metal command buffer failed with an unknown error".to_string(),
};
log::warn!("{reason}");
let mut slot = fault.lock();
if slot.is_none() {
*slot = Some(reason);
}
if let Some((event, value)) = &signal_event {
event.setSignaledValue(*value);
}
}
},
);
unsafe {
command_buffer.addCompletedHandler(block2::RcBlock::as_ptr(&block) as *mut _);
}
}
pub struct MetalStream {
pub device: Retained<ProtocolObject<dyn MTLDevice>>,
pub queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
pub memory_management: MemoryManagement<MetalStorage>,
pub active_encoder: Option<ActiveEncoder>,
pub batch_ops: usize,
pub batch_bytes: usize,
pub shared_event: Retained<ProtocolObject<dyn MTLSharedEvent>>,
pub event_counter: u64,
pub max_ops_per_batch: usize,
pub max_mb_per_batch: usize,
pub submitted_ops: usize,
pub max_submitted_ops: usize,
pub last_command_buffer: Option<Retained<ProtocolObject<dyn MTLCommandBuffer>>>,
pub profiling: Option<Vec<Retained<ProtocolObject<dyn MTLCommandBuffer>>>>,
pub fault: Arc<Mutex<Option<String>>>,
}
impl std::fmt::Debug for MetalStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetalStream")
.field("has_active_encoder", &self.active_encoder.is_some())
.field("batch_ops", &self.batch_ops)
.field("batch_bytes", &self.batch_bytes)
.field("event_counter", &self.event_counter)
.finish()
}
}
impl StreamMemory for MetalStream {
fn failure(&self, binding: &BufferBinding) -> Option<FailureId> {
self.memory_management
.failure(&binding.memory, binding.range())
}
fn taint(&mut self, binding: &BufferBinding, failure: FailureId, failures: &mut ErrorGraph) {
self.memory_management
.taint(&binding.memory, binding.range(), failure, failures)
}
fn written(&mut self, binding: &BufferBinding, failures: &mut ErrorGraph) {
self.memory_management
.written(&binding.memory, binding.range(), failures)
}
}
impl MetalStream {
pub fn get_or_create_encoder(&mut self) -> &mut ActiveEncoder {
if self.active_encoder.is_none() {
let command_buffer = (*self.queue)
.commandBuffer()
.expect("Failed to create command buffer");
let encoder = (*command_buffer)
.computeCommandEncoder()
.expect("Failed to create compute command encoder");
self.active_encoder = Some(ActiveEncoder {
command_buffer,
encoder,
temporaries: Vec::new(),
});
}
self.active_encoder.as_mut().unwrap()
}
pub fn finish(&mut self) {
use objc2_metal::{MTLCommandBuffer, MTLCommandEncoder};
if let Some(active) = self.active_encoder.take() {
(*active.encoder).endEncoding();
install_completion_handler(
&active.command_buffer,
active.temporaries,
None,
self.fault.clone(),
);
(*active.command_buffer).commit();
if self.batch_ops > 0
&& let Some(buffers) = self.profiling.as_mut()
{
buffers.push(active.command_buffer.clone());
}
self.last_command_buffer = Some(active.command_buffer);
}
self.batch_ops = 0;
self.batch_bytes = 0;
if let Some(command_buffer) = self.last_command_buffer.take() {
(*command_buffer).waitUntilCompleted();
std::sync::atomic::fence(std::sync::atomic::Ordering::Acquire);
}
self.submitted_ops = 0;
}
pub fn relocate(&mut self, reason: RelocationReason, failures: &mut ErrorGraph) {
let mut copier = MetalCopies::new(self.queue.clone());
self.memory_management
.relocate(&mut copier, reason, failures);
}
pub fn regulate(&mut self, ops_in_batch: usize) {
self.submitted_ops += ops_in_batch;
if self.submitted_ops >= self.max_submitted_ops {
if let Some(cmd_buf) = self.last_command_buffer.take() {
(*cmd_buf).waitUntilCompleted();
std::sync::atomic::fence(std::sync::atomic::Ordering::Acquire);
}
self.submitted_ops = 0;
}
}
}
#[derive(Clone)]
pub struct MetalEvent {
shared_event: Retained<ProtocolObject<dyn MTLSharedEvent>>,
pub value: u64,
fault: Arc<Mutex<Option<String>>>,
}
unsafe impl Send for MetalEvent {}
impl MetalEvent {
pub fn new(
shared_event: Retained<ProtocolObject<dyn MTLSharedEvent>>,
value: u64,
fault: Arc<Mutex<Option<String>>>,
) -> Self {
Self {
shared_event,
value,
fault,
}
}
pub fn is_complete(&self) -> bool {
(*self.shared_event).signaledValue() >= self.value
}
pub fn wait_sync(self) -> Result<(), ServerError> {
let timeout_ms = 60_000;
let result = (*self.shared_event).waitUntilSignaledValue_timeoutMS(self.value, timeout_ms);
if !result {
return Err(ServerError::Generic {
reason: "Metal event wait timed out".to_string(),
backtrace: cubecl_environment::backtrace::BackTrace::capture(),
});
}
std::sync::atomic::fence(std::sync::atomic::Ordering::Acquire);
if let Some(reason) = self.fault.lock().clone() {
return Err(ServerError::Generic {
reason: format!("the Metal stream faulted at execution time: {reason}"),
backtrace: cubecl_environment::backtrace::BackTrace::capture(),
});
}
Ok(())
}
pub fn wait_async(self, stream: &mut MetalStream) {
use objc2_metal::{MTLCommandBuffer, MTLCommandEncoder, MTLEvent};
if std::ptr::eq(
&*self.shared_event as *const _,
&*stream.shared_event as *const _,
) {
return;
}
if let Some(active) = stream.active_encoder.take() {
(*active.encoder).endEncoding();
install_completion_handler(
&active.command_buffer,
active.temporaries,
None,
stream.fault.clone(),
);
(*active.command_buffer).commit();
}
let command_buffer = (*stream.queue)
.commandBuffer()
.expect("Failed to create command buffer");
let event_ref: &ProtocolObject<dyn MTLEvent> =
ProtocolObject::from_ref(&*self.shared_event);
(*command_buffer).encodeWaitForEvent_value(event_ref, self.value);
(*command_buffer).commit();
}
}
#[derive(Debug)]
pub struct MetalStreamBackend {
device: Retained<ProtocolObject<dyn MTLDevice>>,
mem_props: MemoryDeviceProperties,
mem_config: MemoryConfiguration,
logger: Arc<ServerLogger>,
}
impl MetalStreamBackend {
pub fn new(
device: Retained<ProtocolObject<dyn MTLDevice>>,
mem_props: MemoryDeviceProperties,
mem_config: MemoryConfiguration,
logger: Arc<ServerLogger>,
) -> Self {
Self {
device,
mem_props,
mem_config,
logger,
}
}
}
impl EventStreamBackend for MetalStreamBackend {
type Stream = MetalStream;
type Event = MetalEvent;
fn create_stream(&self) -> Result<Self::Stream, ServerError> {
let queue = (*self.device)
.newCommandQueue()
.expect("Failed to create command queue");
let shared_event = (*self.device)
.newSharedEvent()
.expect("Failed to create shared event");
let storage = MetalStorage::new(self.device.clone());
let memory_management = MemoryManagement::from_configuration(
storage,
&self.mem_props,
self.mem_config.clone(),
self.logger.clone(),
MemoryManagementOptions::new("Metal GPU Memory"),
);
let arch = (*self.device).architecture().name().to_string();
let (max_ops_per_batch, max_mb_per_batch, max_submitted_ops) = match arch.chars().last() {
Some('s' | 'd') => (50, 50, 512), Some('p') => (20, 20, 256), _ => (40, 40, 512), };
Ok(MetalStream {
device: self.device.clone(),
queue,
memory_management,
active_encoder: None,
batch_ops: 0,
batch_bytes: 0,
shared_event,
event_counter: 0,
max_ops_per_batch,
max_mb_per_batch,
submitted_ops: 0,
max_submitted_ops,
last_command_buffer: None,
profiling: None,
fault: Arc::new(Mutex::new(None)),
})
}
fn flush(stream: &mut Self::Stream, _failures: &mut ErrorGraph) -> Self::Event {
use objc2_metal::{MTLCommandBuffer, MTLCommandEncoder, MTLEvent};
stream.event_counter += 1;
let signal_value = stream.event_counter;
let signal = Some((stream.shared_event.clone(), signal_value));
let command_buffer = if let Some(active) = stream.active_encoder.take() {
(*active.encoder).endEncoding();
let event_ref: &ProtocolObject<dyn MTLEvent> =
ProtocolObject::from_ref(&*stream.shared_event);
(*active.command_buffer).encodeSignalEvent_value(event_ref, signal_value);
install_completion_handler(
&active.command_buffer,
active.temporaries,
signal,
stream.fault.clone(),
);
(*active.command_buffer).commit();
active.command_buffer
} else {
let signal_buffer = (*stream.queue)
.commandBuffer()
.expect("Failed to create command buffer");
let event_ref: &ProtocolObject<dyn MTLEvent> =
ProtocolObject::from_ref(&*stream.shared_event);
(*signal_buffer).encodeSignalEvent_value(event_ref, signal_value);
install_completion_handler(&signal_buffer, Vec::new(), signal, stream.fault.clone());
(*signal_buffer).commit();
signal_buffer
};
let ops_in_batch = stream.batch_ops;
if ops_in_batch > 0
&& let Some(buffers) = stream.profiling.as_mut()
{
buffers.push(command_buffer.clone());
}
stream.last_command_buffer = Some(command_buffer);
stream.batch_ops = 0;
stream.batch_bytes = 0;
stream.regulate(ops_in_batch);
MetalEvent::new(
stream.shared_event.clone(),
signal_value,
stream.fault.clone(),
)
}
fn handle_cursor(stream: &Self::Stream, handle: &BufferBinding) -> u64 {
stream
.memory_management
.get_cursor(handle.memory.clone())
.unwrap_or(u64::MAX)
}
fn wait_event(stream: &mut Self::Stream, event: Self::Event) -> Result<(), ServerError> {
event.wait_async(stream);
Ok(())
}
fn wait_event_sync(event: Self::Event) -> Result<(), ServerError> {
event.wait_sync()
}
}