use crate::gemm::gemm_abt;
use crate::vae::error::{VaeError, VaeResult};
pub struct Conv2d {
pub weight: Vec<f32>,
pub bias: Vec<f32>,
pub out_ch: usize,
pub in_ch: usize,
pub k: usize,
pub pad: usize,
}
impl Conv2d {
pub fn from_weights(
weight: &[f32],
weight_shape: &[usize],
bias: &[f32],
pad: usize,
) -> VaeResult<Self> {
if weight_shape.len() != 4 {
return Err(VaeError::Shape(format!(
"conv weight must be 4-D [out,kH,kW,in], got {weight_shape:?}"
)));
}
let (out_ch, kh, kw, in_ch) = (
weight_shape[0],
weight_shape[1],
weight_shape[2],
weight_shape[3],
);
if kh != kw {
return Err(VaeError::Shape(format!(
"conv kernel must be square, got {kh}x{kw}"
)));
}
if weight.len() != out_ch * kh * kw * in_ch {
return Err(VaeError::Shape(format!(
"conv weight len {} != product {:?}",
weight.len(),
weight_shape
)));
}
if bias.len() != out_ch {
return Err(VaeError::Shape(format!(
"conv bias len {} != out_ch {out_ch}",
bias.len()
)));
}
Ok(Self {
weight: weight.to_vec(),
bias: bias.to_vec(),
out_ch,
in_ch,
k: kh,
pad,
})
}
pub fn forward(&self, input: &[f32], h: usize, w: usize) -> VaeResult<ConvOut> {
if input.len() != self.in_ch * h * w {
return Err(VaeError::Shape(format!(
"conv input len {} != in_ch*h*w {}",
input.len(),
self.in_ch * h * w
)));
}
#[cfg(all(feature = "metal", target_os = "macos"))]
{
if crate::vae::gpu::vae_gpu_enabled() {
if let Ok(out) = crate::vae::gpu::conv2d_gpu(
&self.weight,
&self.bias,
input,
self.in_ch,
self.out_ch,
h,
w,
self.k,
self.pad,
) {
return Ok(ConvOut {
data: out.data,
h: out.h,
w: out.w,
});
}
}
}
#[cfg(all(
feature = "native-cuda",
any(target_os = "linux", target_os = "windows")
))]
{
if crate::vae::cuda_gpu::vae_gpu_enabled() {
if let Ok(out) = crate::vae::cuda_gpu::conv2d_gpu(
&self.weight,
&self.bias,
input,
self.in_ch,
self.out_ch,
h,
w,
self.k,
self.pad,
) {
return Ok(ConvOut {
data: out.data,
h: out.h,
w: out.w,
});
}
}
}
let k = self.k;
let pad = self.pad;
let h_out = h + 2 * pad + 1 - k;
let w_out = w + 2 * pad + 1 - k;
let patch_dim = k * k * self.in_ch;
let spatial = h_out * w_out;
let mut patches = vec![0.0f32; spatial * patch_dim];
build_im2col(input, &mut patches, self.in_ch, h, w, k, pad, h_out, w_out);
let mut out_spatial = vec![0.0f32; spatial * self.out_ch];
gemm_abt(
&patches,
&self.weight,
&mut out_spatial,
spatial,
self.out_ch,
patch_dim,
);
let mut out = vec![0.0f32; self.out_ch * spatial];
for oc in 0..self.out_ch {
let b = self.bias[oc];
let dst = &mut out[oc * spatial..(oc + 1) * spatial];
for (hw, slot) in dst.iter_mut().enumerate() {
*slot = out_spatial[hw * self.out_ch + oc] + b;
}
}
Ok(ConvOut {
data: out,
h: h_out,
w: w_out,
})
}
}
pub struct ConvOut {
pub data: Vec<f32>,
pub h: usize,
pub w: usize,
}
#[allow(clippy::too_many_arguments)]
fn build_im2col(
input: &[f32],
patches: &mut [f32],
in_ch: usize,
h: usize,
w: usize,
k: usize,
pad: usize,
h_out: usize,
w_out: usize,
) {
let patch_dim = k * k * in_ch;
let hw_plane = h * w;
for oh in 0..h_out {
for ow in 0..w_out {
let row =
&mut patches[(oh * w_out + ow) * patch_dim..(oh * w_out + ow + 1) * patch_dim];
for kh in 0..k {
let ih = oh + kh;
if ih < pad || ih >= h + pad {
continue; }
let ih = ih - pad;
for kw in 0..k {
let iw = ow + kw;
if iw < pad || iw >= w + pad {
continue;
}
let iw = iw - pad;
let dst_base = (kh * k + kw) * in_ch;
let src_base = ih * w + iw;
for ci in 0..in_ch {
row[dst_base + ci] = input[ci * hw_plane + src_base];
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conv1x1_is_channelwise_matmul() {
let weight = vec![
1.0, 0.0, 0.0, 2.0, 1.0, 1.0, ];
let shape = [3usize, 1, 1, 2];
let bias = vec![0.0, 0.0, 10.0];
let conv = Conv2d::from_weights(&weight, &shape, &bias, 0).expect("conv");
let input = vec![3.0, 4.0, 5.0, 6.0];
let out = conv.forward(&input, 1, 2).expect("fwd");
assert_eq!((out.h, out.w), (1, 2));
assert_eq!(out.data, vec![3.0, 4.0, 10.0, 12.0, 18.0, 20.0]);
}
#[test]
fn conv3x3_same_padding_box_filter() {
let weight = vec![1.0f32; 9];
let shape = [1usize, 3, 3, 1];
let bias = vec![0.0];
let conv = Conv2d::from_weights(&weight, &shape, &bias, 1).expect("conv");
let input: Vec<f32> = (1..=9).map(|v| v as f32).collect(); let out = conv.forward(&input, 3, 3).expect("fwd");
assert_eq!((out.h, out.w), (3, 3));
assert_eq!(out.data[4], 45.0);
assert_eq!(out.data[0], 12.0);
}
}