use metal::{Buffer, CommandBufferRef};
use objc::runtime::{BOOL, NO, Object, YES};
use objc::{class, msg_send, sel, sel_impl};
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock, RwLock};
static CACHE_GUARD: RwLock<()> = RwLock::new(());
#[link(name = "MetalPerformanceShaders", kind = "framework")]
unsafe extern "C" {}
pub fn mps_command_buffer_wrap(cmd_buf: &CommandBufferRef) -> *mut Object {
unsafe {
let cls = class!(MPSCommandBuffer);
msg_send![cls, commandBufferWithCommandBuffer: cmd_buf]
}
}
#[allow(non_upper_case_globals, dead_code)]
mod mps_dtype {
pub const Float32: u32 = 0x10000000 | 32;
pub const Float16: u32 = 0x10000000 | 16;
}
pub fn mps_supports_matmul() -> bool {
static AVAIL: OnceLock<bool> = OnceLock::new();
*AVAIL.get_or_init(|| objc::runtime::Class::get("MPSMatrixMultiplication").is_some())
}
struct KernelCache {
map: Mutex<HashMap<(usize, usize, usize, bool), usize>>,
}
unsafe impl Send for KernelCache {}
unsafe impl Sync for KernelCache {}
fn kernel_cache() -> &'static KernelCache {
static CACHE: OnceLock<KernelCache> = OnceLock::new();
CACHE.get_or_init(|| KernelCache {
map: Mutex::new(HashMap::new()),
})
}
struct MatrixCache {
matrices: Mutex<HashMap<(usize, usize, usize, usize), usize>>,
descriptors: Mutex<HashMap<(usize, usize), usize>>,
}
unsafe impl Send for MatrixCache {}
unsafe impl Sync for MatrixCache {}
fn matrix_cache() -> &'static MatrixCache {
static CACHE: OnceLock<MatrixCache> = OnceLock::new();
CACHE.get_or_init(|| MatrixCache {
matrices: Mutex::new(HashMap::new()),
descriptors: Mutex::new(HashMap::new()),
})
}
unsafe fn get_or_build_descriptor(rows: usize, cols: usize, dtype: u32) -> *mut Object {
let cache = matrix_cache();
let mut map = cache.descriptors.lock().expect("descriptor cache poisoned");
let key = (rows, cols * 8 + dtype as usize); if let Some(&p) = map.get(&key) {
return p as *mut Object;
}
let cls = class!(MPSMatrixDescriptor);
let bytes_per_elem = if dtype == mps_dtype::Float16 { 2 } else { 4 };
let row_bytes = cols * bytes_per_elem;
let desc: *mut Object = msg_send![cls,
matrixDescriptorWithRows: rows as u64
columns: cols as u64
rowBytes: row_bytes as u64
dataType: dtype];
let _: () = msg_send![desc, retain];
map.insert(key, desc as usize);
desc
}
unsafe fn get_or_build_matrix(
buf: &Buffer,
offset: usize,
rows: usize,
cols: usize,
dtype: u32,
) -> *mut Object {
unsafe {
let cache = matrix_cache();
let buf_ptr = (&**buf as *const metal::BufferRef) as usize;
let key = (buf_ptr, offset, rows, cols * 8 + dtype as usize);
let mut map = cache.matrices.lock().expect("matrix cache poisoned");
if let Some(&p) = map.get(&key) {
return p as *mut Object;
}
let desc = get_or_build_descriptor(rows, cols, dtype);
let cls = class!(MPSMatrix);
let alloc: *mut Object = msg_send![cls, alloc];
let buf_ref: &metal::BufferRef = buf;
let mat: *mut Object = msg_send![alloc,
initWithBuffer: buf_ref
offset: offset as u64
descriptor: desc];
map.insert(key, mat as usize);
mat
}
}
pub fn invalidate_caches() {
let _guard = CACHE_GUARD.write().expect("MPS cache guard poisoned");
let cache = matrix_cache();
{
let mut mats = cache.matrices.lock().expect("matrix cache poisoned");
release_cached_ptrs(mats.drain().map(|(_, p)| p));
}
{
let mut descs = cache.descriptors.lock().expect("descriptor cache poisoned");
release_cached_ptrs(descs.drain().map(|(_, p)| p));
}
let kcache = kernel_cache();
{
let mut km = kcache.map.lock().expect("kernel cache poisoned");
release_cached_ptrs(km.drain().map(|(_, p)| p));
}
}
fn release_cached_ptrs(ptrs: impl Iterator<Item = usize>) {
unsafe {
for p in ptrs {
if p != 0 {
let obj = p as *mut Object;
let _: () = msg_send![obj, release];
}
}
}
}
unsafe fn get_or_build_kernel(m: usize, k: usize, n: usize, transpose_b: bool) -> *mut Object {
let cache = kernel_cache();
let mut map = cache.map.lock().expect("kernel cache poisoned");
if let Some(&p) = map.get(&(m, k, n, transpose_b)) {
return p as *mut Object;
}
use crate::device::metal_device;
let dev = metal_device().expect("Metal device required");
let cls = class!(MPSMatrixMultiplication);
let alloc: *mut Object = msg_send![cls, alloc];
let dev_ref: &metal::DeviceRef = &dev.device;
let kernel: *mut Object = msg_send![alloc,
initWithDevice: dev_ref
transposeLeft: NO as BOOL
transposeRight: if transpose_b { YES } else { NO } as BOOL
resultRows: m as u64
resultColumns: n as u64
interiorColumns: k as u64
alpha: 1.0_f64
beta: 0.0_f64
];
map.insert((m, k, n, transpose_b), kernel as usize);
kernel
}
pub fn encode_mps_sgemm(
cmd_buf: &CommandBufferRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
encode_mps_matmul(
cmd_buf,
arena,
a_off,
b_off,
c_off,
m,
k,
n,
mps_dtype::Float32,
false,
);
}
pub fn encode_mps_sgemm_bt(
cmd_buf: &CommandBufferRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
encode_mps_matmul(
cmd_buf,
arena,
a_off,
b_off,
c_off,
m,
k,
n,
mps_dtype::Float32,
true,
);
}
pub fn encode_mps_hgemm(
cmd_buf: &CommandBufferRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
encode_mps_matmul(
cmd_buf,
arena,
a_off,
b_off,
c_off,
m,
k,
n,
mps_dtype::Float16,
false,
);
}
fn encode_mps_matmul(
cmd_buf: &CommandBufferRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
dtype: u32,
transpose_b: bool,
) {
let _guard = CACHE_GUARD.read().expect("MPS cache guard poisoned");
unsafe {
let a_mat = get_or_build_matrix(arena, a_off, m, k, dtype);
let (b_rows, b_cols) = if transpose_b { (n, k) } else { (k, n) };
let b_mat = get_or_build_matrix(arena, b_off, b_rows, b_cols, dtype);
let c_mat = get_or_build_matrix(arena, c_off, m, n, dtype);
let kernel = get_or_build_kernel(m, k, n, transpose_b);
let _: () = msg_send![kernel,
encodeToCommandBuffer: cmd_buf
leftMatrix: a_mat
rightMatrix: b_mat
resultMatrix: c_mat
];
}
}