use metal::{Buffer, CommandQueue, Device, MTLResourceOptions};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;
use crate::gpu_backend::metal_full_layer;
use crate::gpu_backend::metal_prefill;
use super::buffers::{alloc_buf, download_f32, upload_bytes, upload_f32, MetalBuffers};
use super::error::{MetalGraphError, MetalWeightHandle};
use super::pipelines::MetalPipelines;
use super::reformat::{reformat_q1_aos_to_soa, reformat_tq2_aos_to_soa};
static GLOBAL_METAL_GRAPH: OnceLock<Mutex<Option<Arc<MetalGraph>>>> = OnceLock::new();
static GEMM_POOL_ALLOC_COUNT: AtomicU64 = AtomicU64::new(0);
static JOINT_ATTN_POOL_ALLOC_COUNT: AtomicU64 = AtomicU64::new(0);
struct GemmIoPool {
input: Buffer,
input_cap_bytes: usize,
output: Buffer,
output_cap_bytes: usize,
}
pub(super) struct JointAttnIoPool {
pub(super) q: Buffer,
pub(super) k: Buffer,
pub(super) v: Buffer,
qkv_cap_bytes: usize,
pub(super) out: Buffer,
out_cap_bytes: usize,
}
pub struct MetalGraph {
pub(crate) device: Device,
pub(crate) command_queue: CommandQueue,
pub(crate) pipelines: MetalPipelines,
buffers: Mutex<Option<MetalBuffers>>,
weight_cache: Mutex<HashMap<u64, Arc<MetalWeightHandle>>>,
pub(crate) kv_cache: Mutex<Option<metal_full_layer::GpuKvCache>>,
pub(crate) full_layer_buffers: Mutex<Option<metal_full_layer::FullLayerBuffers>>,
pub(crate) logits_buf: Mutex<Option<Buffer>>,
pub(crate) token_id_buf: Mutex<Option<Buffer>>,
pub(crate) prefill_buffers: Mutex<Option<metal_prefill::PrefillBuffers>>,
gemm_io_pool: Mutex<Option<GemmIoPool>>,
pub(super) joint_attn_pool: Mutex<Option<JointAttnIoPool>>,
}
unsafe impl Send for MetalGraph {}
unsafe impl Sync for MetalGraph {}
impl MetalGraph {
pub fn new() -> Result<Self, MetalGraphError> {
let device = Device::system_default().ok_or(MetalGraphError::DeviceNotFound)?;
let command_queue = device.new_command_queue();
let pipelines = MetalPipelines::compile(&device)?;
Ok(Self {
device,
command_queue,
pipelines,
buffers: Mutex::new(None),
weight_cache: Mutex::new(HashMap::new()),
kv_cache: Mutex::new(None),
full_layer_buffers: Mutex::new(None),
logits_buf: Mutex::new(None),
token_id_buf: Mutex::new(None),
prefill_buffers: Mutex::new(None),
gemm_io_pool: Mutex::new(None),
joint_attn_pool: Mutex::new(None),
})
}
pub fn global() -> Result<Arc<Self>, MetalGraphError> {
let mutex = GLOBAL_METAL_GRAPH.get_or_init(|| Mutex::new(None));
let mut guard = mutex
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("MetalGraph lock poisoned".into()))?;
if let Some(ref cached) = *guard {
return Ok(Arc::clone(cached));
}
let graph = Arc::new(Self::new()?);
*guard = Some(Arc::clone(&graph));
Ok(graph)
}
pub fn upload_weight(&self, data: &[u8]) -> Result<MetalWeightHandle, MetalGraphError> {
let buffer = upload_bytes(&self.device, data)?;
Ok(MetalWeightHandle {
byte_len: data.len(),
buffer,
})
}
pub fn get_or_upload_weight(
&self,
key: u64,
raw_bytes: &[u8],
) -> Result<Arc<MetalWeightHandle>, MetalGraphError> {
let mut cache = self
.weight_cache
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("weight cache lock poisoned".into()))?;
if let Some(w) = cache.get(&key) {
return Ok(Arc::clone(w));
}
let handle = Arc::new(self.upload_weight(raw_bytes)?);
cache.insert(key, Arc::clone(&handle));
Ok(handle)
}
pub fn get_or_upload_weight_lazy(
&self,
key: u64,
data_fn: impl FnOnce() -> Vec<u8>,
) -> Result<Arc<MetalWeightHandle>, MetalGraphError> {
let mut cache = self
.weight_cache
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("weight cache lock poisoned".into()))?;
if let Some(w) = cache.get(&key) {
return Ok(Arc::clone(w));
}
let bytes = data_fn();
let handle = Arc::new(self.upload_weight(&bytes)?);
cache.insert(key, Arc::clone(&handle));
Ok(handle)
}
pub fn upload_q1_weight_soa(
&self,
aos_data: &[u8],
) -> Result<MetalWeightHandle, MetalGraphError> {
let soa_data = reformat_q1_aos_to_soa(aos_data).ok_or_else(|| {
MetalGraphError::ExecutionFailed(format!(
"Q1 SoA reformat failed: input length {} is not a multiple of 18",
aos_data.len()
))
})?;
let buffer = upload_bytes(&self.device, &soa_data)?;
Ok(MetalWeightHandle {
byte_len: soa_data.len(),
buffer,
})
}
pub fn get_or_upload_q1_weight_soa(
&self,
key: u64,
aos_bytes: &[u8],
) -> Result<Arc<MetalWeightHandle>, MetalGraphError> {
let mut cache = self
.weight_cache
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("weight cache lock poisoned".into()))?;
if let Some(w) = cache.get(&key) {
return Ok(Arc::clone(w));
}
let handle = Arc::new(self.upload_q1_weight_soa(aos_bytes)?);
cache.insert(key, Arc::clone(&handle));
Ok(handle)
}
pub fn get_or_upload_q1_weight_soa_lazy(
&self,
key: u64,
data_fn: impl FnOnce() -> Vec<u8>,
) -> Result<Arc<MetalWeightHandle>, MetalGraphError> {
let mut cache = self
.weight_cache
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("weight cache lock poisoned".into()))?;
if let Some(w) = cache.get(&key) {
return Ok(Arc::clone(w));
}
let aos_bytes = data_fn();
let handle = Arc::new(self.upload_q1_weight_soa(&aos_bytes)?);
cache.insert(key, Arc::clone(&handle));
Ok(handle)
}
pub fn upload_tq2_weight_soa(
&self,
aos_data: &[u8],
) -> Result<MetalWeightHandle, MetalGraphError> {
let soa_data = reformat_tq2_aos_to_soa(aos_data).ok_or_else(|| {
MetalGraphError::ExecutionFailed(format!(
"TQ2 SoA reformat failed: input length {} is not a multiple of 34",
aos_data.len()
))
})?;
let buffer = upload_bytes(&self.device, &soa_data)?;
Ok(MetalWeightHandle {
byte_len: soa_data.len(),
buffer,
})
}
pub fn get_or_upload_tq2_weight_soa(
&self,
key: u64,
aos_bytes: &[u8],
) -> Result<Arc<MetalWeightHandle>, MetalGraphError> {
let mut cache = self
.weight_cache
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("weight cache lock poisoned".into()))?;
if let Some(w) = cache.get(&key) {
return Ok(Arc::clone(w));
}
let handle = Arc::new(self.upload_tq2_weight_soa(aos_bytes)?);
cache.insert(key, Arc::clone(&handle));
Ok(handle)
}
pub fn get_or_upload_tq2_weight_soa_lazy(
&self,
key: u64,
data_fn: impl FnOnce() -> Vec<u8>,
) -> Result<Arc<MetalWeightHandle>, MetalGraphError> {
let mut cache = self
.weight_cache
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("weight cache lock poisoned".into()))?;
if let Some(w) = cache.get(&key) {
return Ok(Arc::clone(w));
}
let aos_bytes = data_fn();
let handle = Arc::new(self.upload_tq2_weight_soa(&aos_bytes)?);
cache.insert(key, Arc::clone(&handle));
Ok(handle)
}
pub fn encode_gemv(
&self,
weight: &MetalWeightHandle,
input: &[f32],
output: &mut [f32],
n_rows: usize,
k: usize,
) -> Result<(), MetalGraphError> {
if input.len() < k {
return Err(MetalGraphError::EncodingFailed(format!(
"input too short: need {k}, got {}",
input.len()
)));
}
if output.len() < n_rows {
return Err(MetalGraphError::EncodingFailed(format!(
"output too short: need {n_rows}, got {}",
output.len()
)));
}
let opts = MTLResourceOptions::StorageModeShared;
let input_bytes = std::mem::size_of_val(input) as u64;
let output_bytes = (n_rows * std::mem::size_of::<f32>()) as u64;
let input_buf = alloc_buf(&self.device, input_bytes, opts)?;
let output_buf = alloc_buf(&self.device, output_bytes, opts)?;
unsafe { upload_f32(&input_buf, input) };
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_gemv_q1(
encoder,
&weight.buffer,
&input_buf,
&output_buf,
n_rows as u32,
k as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&output_buf, &mut output[..n_rows]) };
Ok(())
}
pub fn encode_gemv_tq2(
&self,
weight: &MetalWeightHandle,
input: &[f32],
output: &mut [f32],
n_rows: usize,
k: usize,
) -> Result<(), MetalGraphError> {
if input.len() < k {
return Err(MetalGraphError::EncodingFailed(format!(
"input too short: need {k}, got {}",
input.len()
)));
}
if output.len() < n_rows {
return Err(MetalGraphError::EncodingFailed(format!(
"output too short: need {n_rows}, got {}",
output.len()
)));
}
let opts = MTLResourceOptions::StorageModeShared;
let input_bytes = std::mem::size_of_val(input) as u64;
let output_bytes = (n_rows * std::mem::size_of::<f32>()) as u64;
let input_buf = alloc_buf(&self.device, input_bytes, opts)?;
let output_buf = alloc_buf(&self.device, output_bytes, opts)?;
unsafe { upload_f32(&input_buf, input) };
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_gemv_tq2(
encoder,
&weight.buffer,
&input_buf,
&output_buf,
n_rows as u32,
k as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&output_buf, &mut output[..n_rows]) };
Ok(())
}
pub fn encode_gemm_tq2(
&self,
weight: &MetalWeightHandle,
input: &[f32],
output: &mut [f32],
m: usize,
n_rows: usize,
k: usize,
) -> Result<(), MetalGraphError> {
if k % 128 != 0 {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_gemm_tq2: k must be a multiple of 128, got {k}"
)));
}
let expected_in = m.checked_mul(k).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_tq2: m*k overflow (m={m}, k={k})"
))
})?;
if input.len() != expected_in {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_gemm_tq2: input len {} != m*k {expected_in} (m={m}, k={k})",
input.len()
)));
}
let expected_out = m.checked_mul(n_rows).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_tq2: m*n_rows overflow (m={m}, n_rows={n_rows})"
))
})?;
if output.len() != expected_out {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_gemm_tq2: output len {} != m*n_rows {expected_out} (m={m}, n_rows={n_rows})",
output.len()
)));
}
if expected_in == 0 || expected_out == 0 {
return Ok(());
}
let input_bytes = std::mem::size_of_val(input);
let output_bytes = expected_out
.checked_mul(std::mem::size_of::<f32>())
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_tq2: output byte size overflow (m={m}, n_rows={n_rows})"
))
})?;
let mut pool_guard = self
.gemm_io_pool
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("gemm_io_pool lock poisoned".into()))?;
let pool =
Self::ensure_gemm_pool(&self.device, &mut pool_guard, input_bytes, output_bytes)?;
unsafe { upload_f32(&pool.input, input) };
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_gemm_tq2_v10(
encoder,
&weight.buffer,
&pool.input,
&pool.output,
n_rows as u32,
k as u32,
m as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&pool.output, output) };
Ok(())
}
pub fn encode_gemm_f32(
&self,
weight: &MetalWeightHandle,
input: &[f32],
output: &mut [f32],
m: usize,
n_rows: usize,
k: usize,
) -> Result<(), MetalGraphError> {
let expected_in = m.checked_mul(k).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: m*k overflow (m={m}, k={k})"
))
})?;
if input.len() != expected_in {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: input len {} != m*k {expected_in} (m={m}, k={k})",
input.len()
)));
}
let expected_out = m.checked_mul(n_rows).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: m*n_rows overflow (m={m}, n_rows={n_rows})"
))
})?;
if output.len() != expected_out {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: output len {} != m*n_rows {expected_out} (m={m}, n_rows={n_rows})",
output.len()
)));
}
let weight_floats = n_rows.checked_mul(k).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: n_rows*k overflow (n_rows={n_rows}, k={k})"
))
})?;
let weight_bytes = weight_floats
.checked_mul(std::mem::size_of::<f32>())
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: weight byte size overflow (n_rows={n_rows}, k={k})"
))
})?;
if weight.byte_len < weight_bytes {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: weight handle holds {} bytes < n_rows*k*4 {weight_bytes} (n_rows={n_rows}, k={k})",
weight.byte_len
)));
}
if expected_in == 0 || expected_out == 0 {
return Ok(());
}
let input_bytes = std::mem::size_of_val(input);
let output_bytes = expected_out
.checked_mul(std::mem::size_of::<f32>())
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_gemm_f32: output byte size overflow (m={m}, n_rows={n_rows})"
))
})?;
let mut pool_guard = self
.gemm_io_pool
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("gemm_io_pool lock poisoned".into()))?;
let pool =
Self::ensure_gemm_pool(&self.device, &mut pool_guard, input_bytes, output_bytes)?;
unsafe { upload_f32(&pool.input, input) };
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_gemm_f32(
encoder,
&weight.buffer,
&pool.input,
&pool.output,
n_rows as u32,
k as u32,
m as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&pool.output, output) };
Ok(())
}
fn ensure_gemm_pool<'g>(
device: &Device,
guard: &'g mut std::sync::MutexGuard<'_, Option<GemmIoPool>>,
input_bytes: usize,
output_bytes: usize,
) -> Result<&'g mut GemmIoPool, MetalGraphError> {
let opts = MTLResourceOptions::StorageModeShared;
match guard.as_mut() {
None => {
let input = alloc_buf(device, input_bytes as u64, opts)?;
let output = alloc_buf(device, output_bytes as u64, opts)?;
GEMM_POOL_ALLOC_COUNT.fetch_add(2, Ordering::Relaxed);
**guard = Some(GemmIoPool {
input,
input_cap_bytes: input_bytes,
output,
output_cap_bytes: output_bytes,
});
}
Some(pool) => {
if pool.input_cap_bytes < input_bytes {
pool.input = alloc_buf(device, input_bytes as u64, opts)?;
pool.input_cap_bytes = input_bytes;
GEMM_POOL_ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
}
if pool.output_cap_bytes < output_bytes {
pool.output = alloc_buf(device, output_bytes as u64, opts)?;
pool.output_cap_bytes = output_bytes;
GEMM_POOL_ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
}
}
}
guard
.as_mut()
.ok_or_else(|| MetalGraphError::ExecutionFailed("gemm_io_pool not allocated".into()))
}
pub fn gemm_pool_alloc_count() -> u64 {
GEMM_POOL_ALLOC_COUNT.load(Ordering::Relaxed)
}
fn joint_attn_validate(
q: &[f32],
k: &[f32],
v: &[f32],
out: &[f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(usize, usize, f32), MetalGraphError> {
use crate::gpu_backend::kernel_sources::{DIT_ATTN_MAX_HEAD_DIM, DIT_ATTN_MAX_SEQ};
if num_heads == 0 || seq == 0 || head_dim == 0 {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention: dims must be non-zero (num_heads={num_heads}, seq={seq}, head_dim={head_dim})"
)));
}
if seq > DIT_ATTN_MAX_SEQ {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention: seq {seq} exceeds kernel cap {DIT_ATTN_MAX_SEQ}"
)));
}
if head_dim > DIT_ATTN_MAX_HEAD_DIM {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention: head_dim {head_dim} exceeds kernel cap {DIT_ATTN_MAX_HEAD_DIM}"
)));
}
let qkv_len = num_heads * seq * head_dim;
let out_len = seq * num_heads * head_dim;
if q.len() < qkv_len || k.len() < qkv_len || v.len() < qkv_len {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention: q/k/v too short (need {qkv_len}, got {}/{}/{})",
q.len(),
k.len(),
v.len()
)));
}
if out.len() < out_len {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention: out too short (need {out_len}, got {})",
out.len()
)));
}
let scale = 1.0f32 / (head_dim as f32).sqrt();
Ok((qkv_len, out_len, scale))
}
pub(super) fn ensure_joint_attn_pool<'g>(
device: &Device,
guard: &'g mut std::sync::MutexGuard<'_, Option<JointAttnIoPool>>,
qkv_bytes: usize,
out_bytes: usize,
) -> Result<&'g mut JointAttnIoPool, MetalGraphError> {
let opts = MTLResourceOptions::StorageModeShared;
match guard.as_mut() {
None => {
let q = alloc_buf(device, qkv_bytes as u64, opts)?;
let k = alloc_buf(device, qkv_bytes as u64, opts)?;
let v = alloc_buf(device, qkv_bytes as u64, opts)?;
let out = alloc_buf(device, out_bytes as u64, opts)?;
JOINT_ATTN_POOL_ALLOC_COUNT.fetch_add(4, Ordering::Relaxed);
**guard = Some(JointAttnIoPool {
q,
k,
v,
qkv_cap_bytes: qkv_bytes,
out,
out_cap_bytes: out_bytes,
});
}
Some(pool) => {
if pool.qkv_cap_bytes < qkv_bytes {
pool.q = alloc_buf(device, qkv_bytes as u64, opts)?;
pool.k = alloc_buf(device, qkv_bytes as u64, opts)?;
pool.v = alloc_buf(device, qkv_bytes as u64, opts)?;
pool.qkv_cap_bytes = qkv_bytes;
JOINT_ATTN_POOL_ALLOC_COUNT.fetch_add(3, Ordering::Relaxed);
}
if pool.out_cap_bytes < out_bytes {
pool.out = alloc_buf(device, out_bytes as u64, opts)?;
pool.out_cap_bytes = out_bytes;
JOINT_ATTN_POOL_ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
}
}
}
guard
.as_mut()
.ok_or_else(|| MetalGraphError::ExecutionFailed("joint_attn_pool not allocated".into()))
}
pub fn joint_attn_pool_alloc_count() -> u64 {
JOINT_ATTN_POOL_ALLOC_COUNT.load(Ordering::Relaxed)
}
#[allow(clippy::too_many_arguments)]
pub fn joint_attn_resident_prepare(
&self,
q: &[f32],
k: &[f32],
v: &[f32],
out: &[f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(), MetalGraphError> {
let (qkv_len, out_len, _scale) =
Self::joint_attn_validate(q, k, v, out, num_heads, seq, head_dim)?;
let qkv_bytes = qkv_len * std::mem::size_of::<f32>();
let out_bytes = out_len * std::mem::size_of::<f32>();
let mut guard = self.joint_attn_pool.lock().map_err(|_| {
MetalGraphError::ExecutionFailed("joint_attn_pool lock poisoned".into())
})?;
let pool = Self::ensure_joint_attn_pool(&self.device, &mut guard, qkv_bytes, out_bytes)?;
unsafe {
upload_f32(&pool.q, &q[..qkv_len]);
upload_f32(&pool.k, &k[..qkv_len]);
upload_f32(&pool.v, &v[..qkv_len]);
}
Ok(())
}
pub fn joint_attn_resident_download(&self, out: &mut [f32]) -> Result<(), MetalGraphError> {
let guard = self.joint_attn_pool.lock().map_err(|_| {
MetalGraphError::ExecutionFailed("joint_attn_pool lock poisoned".into())
})?;
let pool = guard.as_ref().ok_or_else(|| {
MetalGraphError::ExecutionFailed(
"joint_attn_resident_download: pool not prepared".into(),
)
})?;
let out_floats = pool.out_cap_bytes / std::mem::size_of::<f32>();
if out.len() > out_floats {
return Err(MetalGraphError::ExecutionFailed(format!(
"joint_attn_resident_download: out len {} exceeds resident capacity {out_floats}",
out.len()
)));
}
unsafe { download_f32(&pool.out, out) };
Ok(())
}
pub(super) fn joint_attn_flash_validate(
q: &[f32],
k: &[f32],
v: &[f32],
out: &[f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(usize, usize, f32), MetalGraphError> {
let (qkv_len, out_len, scale) =
Self::joint_attn_validate(q, k, v, out, num_heads, seq, head_dim)?;
if head_dim % 8 != 0 {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention_flash: head_dim {head_dim} must be a multiple of 8"
)));
}
if head_dim > 128 {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention_flash: head_dim {head_dim} exceeds the flash kernel cap (128)"
)));
}
Ok((qkv_len, out_len, scale))
}
#[allow(clippy::too_many_arguments)]
pub fn encode_joint_attention_flash(
&self,
q: &[f32],
k: &[f32],
v: &[f32],
out: &mut [f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(), MetalGraphError> {
let (qkv_len, out_len, scale) =
Self::joint_attn_flash_validate(q, k, v, out, num_heads, seq, head_dim)?;
let opts = MTLResourceOptions::StorageModeShared;
let qkv_bytes = (qkv_len * std::mem::size_of::<f32>()) as u64;
let out_bytes = (out_len * std::mem::size_of::<f32>()) as u64;
let q_buf = alloc_buf(&self.device, qkv_bytes, opts)?;
let k_buf = alloc_buf(&self.device, qkv_bytes, opts)?;
let v_buf = alloc_buf(&self.device, qkv_bytes, opts)?;
let out_buf = alloc_buf(&self.device, out_bytes, opts)?;
unsafe {
upload_f32(&q_buf, &q[..qkv_len]);
upload_f32(&k_buf, &k[..qkv_len]);
upload_f32(&v_buf, &v[..qkv_len]);
}
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_joint_attention_flash(
encoder,
&q_buf,
&k_buf,
&v_buf,
&out_buf,
num_heads as u32,
seq as u32,
head_dim as u32,
scale,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&out_buf, &mut out[..out_len]) };
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn encode_joint_attention_flash_pooled(
&self,
q: &[f32],
k: &[f32],
v: &[f32],
out: &mut [f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(), MetalGraphError> {
let (qkv_len, out_len, scale) =
Self::joint_attn_flash_validate(q, k, v, out, num_heads, seq, head_dim)?;
let qkv_bytes = qkv_len * std::mem::size_of::<f32>();
let out_bytes = out_len * std::mem::size_of::<f32>();
let mut guard = self.joint_attn_pool.lock().map_err(|_| {
MetalGraphError::ExecutionFailed("joint_attn_pool lock poisoned".into())
})?;
let pool = Self::ensure_joint_attn_pool(&self.device, &mut guard, qkv_bytes, out_bytes)?;
unsafe {
upload_f32(&pool.q, &q[..qkv_len]);
upload_f32(&pool.k, &k[..qkv_len]);
upload_f32(&pool.v, &v[..qkv_len]);
}
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_joint_attention_flash(
encoder,
&pool.q,
&pool.k,
&pool.v,
&pool.out,
num_heads as u32,
seq as u32,
head_dim as u32,
scale,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&pool.out, &mut out[..out_len]) };
Ok(())
}
pub fn joint_attn_flash_resident_dispatch(
&self,
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(), MetalGraphError> {
use crate::gpu_backend::kernel_sources::{DIT_ATTN_MAX_HEAD_DIM, DIT_ATTN_MAX_SEQ};
if num_heads == 0 || seq == 0 || head_dim == 0 {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention_flash: dims must be non-zero (num_heads={num_heads}, seq={seq}, head_dim={head_dim})"
)));
}
if seq > DIT_ATTN_MAX_SEQ || head_dim > DIT_ATTN_MAX_HEAD_DIM {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention_flash: seq {seq} / head_dim {head_dim} exceed caps \
({DIT_ATTN_MAX_SEQ}/{DIT_ATTN_MAX_HEAD_DIM})"
)));
}
if head_dim % 8 != 0 || head_dim > 128 {
return Err(MetalGraphError::InvalidDimensions(format!(
"joint_attention_flash: head_dim {head_dim} must be a multiple of 8 and <= 128"
)));
}
let qkv_len = num_heads * seq * head_dim;
let out_len = seq * num_heads * head_dim;
let qkv_bytes = qkv_len * std::mem::size_of::<f32>();
let out_bytes = out_len * std::mem::size_of::<f32>();
let scale = 1.0f32 / (head_dim as f32).sqrt();
let guard = self.joint_attn_pool.lock().map_err(|_| {
MetalGraphError::ExecutionFailed("joint_attn_pool lock poisoned".into())
})?;
let pool = guard.as_ref().ok_or_else(|| {
MetalGraphError::ExecutionFailed(
"joint_attn_flash_resident_dispatch: pool not prepared (call joint_attn_resident_prepare first)"
.into(),
)
})?;
if pool.qkv_cap_bytes < qkv_bytes || pool.out_cap_bytes < out_bytes {
return Err(MetalGraphError::ExecutionFailed(
"joint_attn_flash_resident_dispatch: resident pool smaller than requested shape"
.into(),
));
}
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_joint_attention_flash(
encoder,
&pool.q,
&pool.k,
&pool.v,
&pool.out,
num_heads as u32,
seq as u32,
head_dim as u32,
scale,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn encode_ffn_phase(
&self,
hidden: &mut [f32],
attn_out: &[f32],
norm_weight: &[f32],
attn_proj_weight: &MetalWeightHandle,
gate_up_weight: &MetalWeightHandle,
down_weight: &MetalWeightHandle,
hidden_size: usize,
intermediate_size: usize,
eps: f32,
) -> Result<(), MetalGraphError> {
static FFN_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
let call_num = FFN_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
let t_total = Instant::now();
if hidden.len() < hidden_size {
return Err(MetalGraphError::EncodingFailed(format!(
"hidden too short: need {hidden_size}, got {}",
hidden.len()
)));
}
if attn_out.len() < hidden_size {
return Err(MetalGraphError::EncodingFailed(format!(
"attn_out too short: need {hidden_size}, got {}",
attn_out.len()
)));
}
if norm_weight.len() < hidden_size {
return Err(MetalGraphError::EncodingFailed(format!(
"norm_weight too short: need {hidden_size}, got {}",
norm_weight.len()
)));
}
let t0 = Instant::now();
let guard = self.acquire_buffers(hidden_size, intermediate_size)?;
let bufs = guard
.as_ref()
.ok_or_else(|| MetalGraphError::ExecutionFailed("buffers not allocated".into()))?;
let dt_acquire = t0.elapsed();
let t1 = Instant::now();
unsafe {
upload_f32(&bufs.hidden_buf, &hidden[..hidden_size]);
upload_f32(&bufs.attn_out_buf, &attn_out[..hidden_size]);
upload_f32(&bufs.norm_weight_buf, &norm_weight[..hidden_size]);
}
let dt_upload = t1.elapsed();
let t2 = Instant::now();
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
let dt_encode_setup = t2.elapsed();
let h = hidden_size as u32;
let inter = intermediate_size as u32;
self.dispatch_gemv_q1(
encoder,
&attn_proj_weight.buffer,
&bufs.attn_out_buf,
&bufs.proj_buf,
h,
h,
);
self.dispatch_residual_add(encoder, &bufs.hidden_buf, &bufs.proj_buf, h);
self.dispatch_rmsnorm(
encoder,
&bufs.hidden_buf,
&bufs.norm_weight_buf,
&bufs.normed_buf,
eps,
h,
);
self.dispatch_fused_gate_up_swiglu(
encoder,
&gate_up_weight.buffer,
&bufs.normed_buf,
&bufs.swiglu_buf,
inter,
h,
);
self.dispatch_gemv_q1(
encoder,
&down_weight.buffer,
&bufs.swiglu_buf,
&bufs.down_buf,
h,
inter,
);
self.dispatch_residual_add(encoder, &bufs.hidden_buf, &bufs.down_buf, h);
encoder.end_encoding();
cmd_buf.commit();
let t3 = Instant::now();
cmd_buf.wait_until_completed();
let dt_gpu_wait = t3.elapsed();
let t4 = Instant::now();
unsafe {
download_f32(&bufs.hidden_buf, &mut hidden[..hidden_size]);
}
let dt_download = t4.elapsed();
let dt_total = t_total.elapsed();
if call_num % 36 == 0 {
tracing::debug!(
"MetalGraph FFN #{}: acquire={}µs upload={}µs encode={}µs gpu_wait={}µs download={}µs total={}µs",
call_num,
dt_acquire.as_micros(),
dt_upload.as_micros(),
dt_encode_setup.as_micros(),
dt_gpu_wait.as_micros(),
dt_download.as_micros(),
dt_total.as_micros(),
);
}
Ok(())
}
pub fn encode_qkv_phase(
&self,
input: &[f32],
output: &mut [f32],
weight: &MetalWeightHandle,
n_rows: usize,
k: usize,
) -> Result<(), MetalGraphError> {
self.encode_gemv(weight, input, output, n_rows, k)
}
fn acquire_buffers(
&self,
hidden_size: usize,
intermediate_size: usize,
) -> Result<std::sync::MutexGuard<'_, Option<MetalBuffers>>, MetalGraphError> {
let mut guard = self
.buffers
.lock()
.map_err(|_| MetalGraphError::ExecutionFailed("buffer lock poisoned".into()))?;
let needs_alloc = match guard.as_ref() {
Some(b) => !b.matches(hidden_size, intermediate_size),
None => true,
};
if needs_alloc {
*guard = Some(MetalBuffers::allocate(
&self.device,
hidden_size,
intermediate_size,
)?);
}
Ok(guard)
}
pub fn device(&self) -> &Device {
&self.device
}
}