use metal::MTLResourceOptions;
use super::buffers::{alloc_buf, download_f32, upload_f32};
use super::error::{MetalGraphError, MetalWeightHandle};
use super::graph::MetalGraph;
const IM2COL_TILE_CAP_BYTES: usize = 256 * 1024 * 1024;
fn implicit_conv_disabled() -> bool {
use std::sync::OnceLock;
static DISABLED: OnceLock<bool> = OnceLock::new();
*DISABLED.get_or_init(|| {
matches!(
std::env::var("PICTOR_VAE_NO_IMPLICIT_CONV").ok().as_deref(),
Some("1")
)
})
}
impl MetalGraph {
#[allow(clippy::too_many_arguments)]
pub fn encode_conv2d_f32(
&self,
weight: &MetalWeightHandle,
input: &[f32],
bias: &[f32],
output: &mut [f32],
c_in: usize,
c_out: usize,
h: usize,
w: usize,
k: usize,
pad: usize,
) -> Result<(), MetalGraphError> {
if k == 0 {
return Err(MetalGraphError::InvalidDimensions(
"encode_conv2d_f32: kernel size k must be ≥ 1".into(),
));
}
let expected_in = c_in
.checked_mul(h)
.and_then(|x| x.checked_mul(w))
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: c_in*h*w overflow (c_in={c_in}, h={h}, w={w})"
))
})?;
if input.len() != expected_in {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: input len {} != c_in*h*w {expected_in}",
input.len()
)));
}
if bias.len() != c_out {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: bias len {} != c_out {c_out}",
bias.len()
)));
}
let h_pad = h + 2 * pad;
let w_pad = w + 2 * pad;
if h_pad + 1 < k || w_pad + 1 < k {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: kernel {k} larger than padded input {h_pad}x{w_pad}"
)));
}
let h_out = h_pad + 1 - k;
let w_out = w_pad + 1 - k;
let spatial = h_out.checked_mul(w_out).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: h_out*w_out overflow (h_out={h_out}, w_out={w_out})"
))
})?;
let expected_out = c_out.checked_mul(spatial).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: c_out*spatial overflow (c_out={c_out}, spatial={spatial})"
))
})?;
if output.len() != expected_out {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: output len {} != c_out*h_out*w_out {expected_out}",
output.len()
)));
}
if expected_in == 0 || expected_out == 0 {
return Ok(());
}
if k == 1 && pad == 0 {
let hw = h * w; let mut reshaped = vec![0f32; hw * c_in];
for ci in 0..c_in {
let src = &input[ci * hw..(ci + 1) * hw];
for (s, &v) in src.iter().enumerate() {
reshaped[s * c_in + ci] = v;
}
}
let mut out_spatial = vec![0f32; hw * c_out];
self.encode_gemm_f32(weight, &reshaped, &mut out_spatial, hw, c_out, c_in)?;
for oc in 0..c_out {
let b = bias[oc];
let dst = &mut output[oc * hw..(oc + 1) * hw];
for (s, slot) in dst.iter_mut().enumerate() {
*slot = out_spatial[s * c_out + oc] + b;
}
}
return Ok(());
}
let patch_dim = k
.checked_mul(k)
.and_then(|x| x.checked_mul(c_in))
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: patch_dim overflow (k={k}, c_in={c_in})"
))
})?;
let weight_floats = c_out.checked_mul(patch_dim).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: c_out*patch_dim overflow (c_out={c_out}, patch_dim={patch_dim})"
))
})?;
let weight_bytes = weight_floats
.checked_mul(std::mem::size_of::<f32>())
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(
"encode_conv2d_f32: weight byte size overflow".into(),
)
})?;
if weight.byte_len < weight_bytes {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32: weight handle holds {} bytes < c_out*patch_dim*4 {weight_bytes}",
weight.byte_len
)));
}
if k >= 3 && pad >= 1 && !implicit_conv_disabled() {
match self.encode_conv2d_f32_implicit(
weight, input, bias, output, c_in, c_out, h, w, k, pad, spatial, patch_dim, w_out,
) {
Ok(()) => return Ok(()),
Err(e) => {
tracing::debug!("conv2d implicit-GEMM path failed ({e}); im2col fallback");
}
}
}
self.encode_conv2d_f32_im2col(
weight, input, bias, output, c_in, c_out, h, w, k, pad, spatial, patch_dim, w_out,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_conv2d_f32_im2col(
&self,
weight: &MetalWeightHandle,
input: &[f32],
bias: &[f32],
output: &mut [f32],
c_in: usize,
c_out: usize,
h: usize,
w: usize,
k: usize,
pad: usize,
spatial: usize,
patch_dim: usize,
w_out: usize,
) -> Result<(), MetalGraphError> {
let shared = MTLResourceOptions::StorageModeShared;
let private = MTLResourceOptions::StorageModePrivate;
let input_bytes = std::mem::size_of_val(input) as u64;
let input_buf = alloc_buf(&self.device, input_bytes, shared)?;
unsafe { upload_f32(&input_buf, input) };
let patch_row_bytes = patch_dim
.checked_mul(std::mem::size_of::<f32>())
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(
"encode_conv2d_f32: patch row byte size overflow".into(),
)
})?;
let tile_rows = (IM2COL_TILE_CAP_BYTES / patch_row_bytes.max(1))
.max(1)
.min(spatial);
let patches_bytes = (tile_rows * patch_dim * std::mem::size_of::<f32>()) as u64;
let patches_buf = alloc_buf(&self.device, patches_bytes, private)?;
let out_tile_bytes = (tile_rows * c_out * std::mem::size_of::<f32>()) as u64;
let out_tile_buf = alloc_buf(&self.device, out_tile_bytes, shared)?;
let mut out_tile = vec![0f32; tile_rows * c_out];
let mut row_start = 0usize;
while row_start < spatial {
let rows = (spatial - row_start).min(tile_rows);
let n_elems = rows * patch_dim;
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_im2col_f32(
encoder,
&input_buf,
&patches_buf,
c_in as u32,
h as u32,
w as u32,
k as u32,
pad as u32,
w_out as u32,
row_start as u32,
n_elems as u32,
);
self.dispatch_gemm_f32(
encoder,
&weight.buffer,
&patches_buf,
&out_tile_buf,
c_out as u32,
patch_dim as u32,
rows as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&out_tile_buf, &mut out_tile[..rows * c_out]) };
for local_row in 0..rows {
let out_idx = row_start + local_row;
let base = local_row * c_out;
for oc in 0..c_out {
output[oc * spatial + out_idx] = out_tile[base + oc] + bias[oc];
}
}
row_start += rows;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_conv2d_f32_implicit(
&self,
weight: &MetalWeightHandle,
input: &[f32],
bias: &[f32],
output: &mut [f32],
c_in: usize,
c_out: usize,
h: usize,
w: usize,
k: usize,
pad: usize,
spatial: usize,
patch_dim: usize,
w_out: usize,
) -> Result<(), MetalGraphError> {
let shared = MTLResourceOptions::StorageModeShared;
let input_bytes = std::mem::size_of_val(input) as u64;
let input_buf = alloc_buf(&self.device, input_bytes, shared)?;
unsafe { upload_f32(&input_buf, input) };
let out_floats = c_out.checked_mul(spatial).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_conv2d_f32_implicit: c_out*spatial overflow (c_out={c_out}, spatial={spatial})"
))
})?;
let out_bytes = out_floats
.checked_mul(std::mem::size_of::<f32>())
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(
"encode_conv2d_f32_implicit: output byte size overflow".into(),
)
})? as u64;
let out_buf = alloc_buf(&self.device, out_bytes, shared)?;
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_conv2d_f32_implicit(
encoder,
&weight.buffer,
&input_buf,
&out_buf,
c_out as u32,
spatial as u32,
patch_dim as u32,
c_in as u32,
h as u32,
w as u32,
k as u32,
pad as u32,
w_out as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&out_buf, output) };
for oc in 0..c_out {
let b = bias[oc];
let dst = &mut output[oc * spatial..(oc + 1) * spatial];
for slot in dst.iter_mut() {
*slot += b;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn encode_groupnorm_f32(
&self,
x: &mut [f32],
weight: &[f32],
bias: &[f32],
channels: usize,
hw: usize,
num_groups: usize,
eps: f32,
) -> Result<(), MetalGraphError> {
if num_groups == 0 || channels % num_groups != 0 {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_groupnorm_f32: channels {channels} not divisible by num_groups {num_groups}"
)));
}
let expected = channels.checked_mul(hw).ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_groupnorm_f32: channels*hw overflow (channels={channels}, hw={hw})"
))
})?;
if x.len() != expected {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_groupnorm_f32: x len {} != channels*hw {expected}",
x.len()
)));
}
if weight.len() != channels || bias.len() != channels {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_groupnorm_f32: weight/bias len ({}/{}) != channels {channels}",
weight.len(),
bias.len()
)));
}
if expected == 0 {
return Ok(());
}
let shared = MTLResourceOptions::StorageModeShared;
let x_bytes = std::mem::size_of_val(&x[..]) as u64;
let aff_bytes = (channels * std::mem::size_of::<f32>()) as u64;
let x_buf = alloc_buf(&self.device, x_bytes, shared)?;
let w_buf = alloc_buf(&self.device, aff_bytes, shared)?;
let b_buf = alloc_buf(&self.device, aff_bytes, shared)?;
unsafe {
upload_f32(&x_buf, x);
upload_f32(&w_buf, weight);
upload_f32(&b_buf, bias);
}
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_groupnorm_f32(
encoder,
&x_buf,
&w_buf,
&b_buf,
channels as u32,
hw as u32,
num_groups as u32,
eps,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&x_buf, x) };
Ok(())
}
pub fn encode_silu_f32(&self, x: &mut [f32]) -> Result<(), MetalGraphError> {
if x.is_empty() {
return Ok(());
}
let shared = MTLResourceOptions::StorageModeShared;
let x_bytes = std::mem::size_of_val(&x[..]) as u64;
let x_buf = alloc_buf(&self.device, x_bytes, shared)?;
unsafe { upload_f32(&x_buf, x) };
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_silu_f32(encoder, &x_buf, x.len() as u32);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&x_buf, x) };
Ok(())
}
pub fn encode_upsample_nearest_f32(
&self,
input: &[f32],
output: &mut [f32],
c: usize,
h: usize,
w: usize,
) -> Result<(), MetalGraphError> {
let expected_in = c
.checked_mul(h)
.and_then(|x| x.checked_mul(w))
.ok_or_else(|| {
MetalGraphError::InvalidDimensions(format!(
"encode_upsample_nearest_f32: c*h*w overflow (c={c}, h={h}, w={w})"
))
})?;
if input.len() != expected_in {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_upsample_nearest_f32: input len {} != c*h*w {expected_in}",
input.len()
)));
}
let expected_out = expected_in.checked_mul(4).ok_or_else(|| {
MetalGraphError::InvalidDimensions(
"encode_upsample_nearest_f32: output size overflow".into(),
)
})?;
if output.len() != expected_out {
return Err(MetalGraphError::InvalidDimensions(format!(
"encode_upsample_nearest_f32: output len {} != c*4*h*w {expected_out}",
output.len()
)));
}
if expected_in == 0 {
return Ok(());
}
let shared = MTLResourceOptions::StorageModeShared;
let in_bytes = std::mem::size_of_val(input) as u64;
let out_bytes = std::mem::size_of_val(&output[..]) as u64;
let in_buf = alloc_buf(&self.device, in_bytes, shared)?;
let out_buf = alloc_buf(&self.device, out_bytes, shared)?;
unsafe { upload_f32(&in_buf, input) };
let cmd_buf = self.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
self.dispatch_upsample_nearest_f32(
encoder,
&in_buf,
&out_buf,
c as u32,
h as u32,
w as u32,
expected_out as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
unsafe { download_f32(&out_buf, output) };
Ok(())
}
}