use crate::MetalCompiler;
use cubecl_common::hash::StableHash;
use cubecl_core::prelude::*;
use cubecl_core::server::{LaunchError, ResourceLimitError};
use cubecl_environment::backtrace::BackTrace;
use cubecl_server::kernel::{BufferIOAttr, DebugInformation};
use cubecl_server::{
compiler::{CompilationRecording, KernelCacheKey, build_id_hash},
kernel::CubeKernel,
logging::ServerLogger,
};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSString;
use objc2_metal::{
MTLCompileOptions, MTLComputePipelineState, MTLDevice, MTLLanguageVersion, MTLLibrary,
MTLMathFloatingPointFunctions, MTLMathMode,
};
use std::sync::Arc;
use cubecl_environment::persistence::Store;
use cubecl_server::compiler::{CompilationCache, compilation_store, store_compiled};
#[derive(Debug, Clone)]
pub struct CompiledKernel {
pub(crate) pipeline: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub(crate) cube_dim: CubeDim,
pub(crate) shared_memory_bytes: usize,
pub(crate) io: Option<std::sync::Arc<[BufferIOAttr]>>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
pub struct MslCacheEntry {
entrypoint_name: String,
cube_dim: (u32, u32, u32),
source: String,
#[serde(default)]
io: Option<Vec<BufferIOAttr>>,
}
#[derive(Debug)]
pub struct MetalContext {
device: Retained<ProtocolObject<dyn MTLDevice>>,
compiled_kernels: CompilationCache<KernelId, CompiledKernel>,
msl_cache: Option<Store<KernelCacheKey, MslCacheEntry>>,
build_id: StableHash,
compilation_options: cubecl_cpp::shared::CompilationOptions,
msl_compile_options: Retained<MTLCompileOptions>,
}
impl MetalContext {
pub fn new(
device: Retained<ProtocolObject<dyn MTLDevice>>,
compilation_options: cubecl_cpp::shared::CompilationOptions,
) -> Self {
let msl_compile_options = MTLCompileOptions::new();
msl_compile_options.setLanguageVersion(MTLLanguageVersion::Version3_2);
msl_compile_options.setMathMode(MTLMathMode::Safe);
msl_compile_options.setMathFloatingPointFunctions(MTLMathFloatingPointFunctions::Precise);
let device_key = device.name().to_string();
let msl_cache = compilation_store("metal", format!("msl_{device_key}"));
Self {
compiled_kernels: CompilationCache::mirroring(&msl_cache),
msl_cache,
build_id: build_id_hash(),
device,
compilation_options,
msl_compile_options,
}
}
pub fn compile_kernel(
&mut self,
kernel_id: &KernelId,
kernel: Box<dyn CubeKernel>,
max_shared_memory_size: usize,
logger: Arc<ServerLogger>,
) -> Result<CompiledKernel, LaunchError> {
if let Some(compiled) = self.compiled_kernels.get(kernel_id) {
return Ok(compiled.clone());
}
let mut recording = CompilationRecording::new(kernel_id);
let cache_key = KernelCacheKey::new(kernel_id, self.build_id);
if let Some(cache) = self.msl_cache.as_mut()
&& let Some(entry) = cache.remove(&cache_key)
{
log::trace!("Using MSL cache");
let mut compiled = self.create_pipeline_from_source(
&entry.source,
&entry.entrypoint_name,
entry.cube_dim.into(),
)?;
compiled.io = entry.io.map(std::sync::Arc::from);
self.compiled_kernels
.insert(kernel_id.clone(), compiled.clone());
recording.loaded();
return Ok(compiled);
}
log::trace!("Compiling kernel to MSL");
let definition = kernel.define();
recording.defined(&definition);
let mut kernel_compiled = cubecl_server::kernel::CompiledKernel::compile(
&*kernel,
definition,
&mut MetalCompiler::default(),
&self.compilation_options,
)?;
if logger.compilation_source_activated() {
kernel_compiled.debug_info = Some(DebugInformation::new("msl", kernel_id.clone()));
}
logger.log_compilation(&kernel_compiled);
let entrypoint_name = kernel_compiled.entrypoint_name.clone();
let cube_dim = kernel_compiled.cube_dim;
let source = kernel_compiled.source.clone();
let io = kernel_compiled.io.take();
let shared_memory_bytes = kernel_compiled
.repr
.as_ref()
.map(|r| r.shared_memory_size)
.unwrap_or(0);
if shared_memory_bytes > max_shared_memory_size {
return Err(LaunchError::TooManyResources(
ResourceLimitError::SharedMemory {
requested: shared_memory_bytes,
max: max_shared_memory_size,
backtrace: BackTrace::capture(),
},
));
}
let mut compiled = self.create_pipeline_from_source(&source, &entrypoint_name, cube_dim)?;
compiled.shared_memory_bytes = shared_memory_bytes;
compiled.io = io.clone().map(std::sync::Arc::from);
recording.source(&source);
let stored = match &mut self.msl_cache {
Some(cache) => store_compiled(
cache,
cache_key,
MslCacheEntry {
entrypoint_name,
cube_dim: (cube_dim.x, cube_dim.y, cube_dim.z),
source,
io,
},
),
None => false,
};
recording.compiled(stored);
self.compiled_kernels
.insert(kernel_id.clone(), compiled.clone());
Ok(compiled)
}
fn create_pipeline_from_source(
&self,
source: &str,
entrypoint_name: &str,
cube_dim: CubeDim,
) -> Result<CompiledKernel, cubecl_server::compiler::CompilationError> {
use objc2_metal::MTLDevice;
let source_ns = NSString::from_str(source);
let library = self
.device
.newLibraryWithSource_options_error(&source_ns, Some(&self.msl_compile_options))
.map_err(|err| cubecl_server::compiler::CompilationError::Generic {
reason: format!("Failed to compile MSL: {:?}", err.localizedDescription()),
backtrace: BackTrace::capture(),
})?;
let entrypoint_ns = NSString::from_str(entrypoint_name);
let function = library.newFunctionWithName(&entrypoint_ns).ok_or_else(|| {
cubecl_server::compiler::CompilationError::Generic {
reason: format!("Function '{}' not found in library", entrypoint_name),
backtrace: BackTrace::capture(),
}
})?;
let pipeline = self
.device
.newComputePipelineStateWithFunction_error(&function)
.map_err(|err| cubecl_server::compiler::CompilationError::Generic {
reason: format!(
"Failed to create compute pipeline: {:?}",
err.localizedDescription()
),
backtrace: BackTrace::capture(),
})?;
let max_units = pipeline.maxTotalThreadsPerThreadgroup();
let requested = (cube_dim.x as usize) * (cube_dim.y as usize) * (cube_dim.z as usize);
if requested > max_units {
return Err(cubecl_server::compiler::CompilationError::Generic {
reason: format!(
"Cube dim {}x{}x{} ({requested} units) exceeds this kernel's limit of \
{max_units} threads per threadgroup",
cube_dim.x, cube_dim.y, cube_dim.z
),
backtrace: BackTrace::capture(),
});
}
Ok(CompiledKernel {
pipeline,
cube_dim,
shared_memory_bytes: 0,
io: None,
})
}
pub fn get_kernel(&mut self, kernel_id: &KernelId) -> Option<&CompiledKernel> {
self.compiled_kernels.get(kernel_id)
}
}
unsafe impl Send for MetalContext {}
unsafe impl Sync for MetalContext {}