use crate::encoder::EncoderExt;
use crate::{LibraryName, MetalStream};
use anyhow::ensure;
use metal::MTLSize;
use tract_core::internal::*;
use tract_core::ops::cnn::PoolSpec;
use tract_gpu::tensor::DeviceTensor;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct PoolParams {
n: i32,
ih: i32,
iw: i32,
c: i32,
oh: i32,
ow: i32,
kh: i32,
kw: i32,
stride_h: i32,
stride_w: i32,
pad_h: i32,
pad_w: i32,
dil_h: i32,
dil_w: i32,
count_include_pad: i32,
normalize: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolKind {
Max,
Sum {
count_include_pad: bool,
normalize: bool,
},
}
pub fn metal_pool_supported(pool_spec: &PoolSpec, kind: PoolKind, fact: &TypedFact) -> bool {
let layout_ok = pool_spec.data_format.c_is_last()
|| (kind == PoolKind::Max && pool_spec.data_format.has_n());
matches!(fact.datum_type, DatumType::F16 | DatumType::F32)
&& layout_ok
&& fact.rank() == 4
&& pool_spec.kernel_shape.len() == 2
&& fact.shape.as_concrete().is_some()
}
pub fn dispatch_metal_pool(
stream: &MetalStream,
pool_spec: &PoolSpec,
kind: PoolKind,
input: &DeviceTensor,
output: &DeviceTensor,
) -> TractResult<()> {
let dt = input.datum_type();
let tname = match dt {
DatumType::F32 => "f32",
DatumType::F16 => "f16",
_ => bail!("Metal pool: F32/F16 only, got {dt:?}"),
};
let in_shape = pool_spec.data_format.shape(input.shape())?;
let out_shape = pool_spec.data_format.shape(output.shape())?;
ensure!(in_shape.hw_rank() == 2, "Metal pool is 2D only");
let strides = pool_spec.strides();
let dilations = pool_spec.dilations();
let padding = pool_spec.computed_padding(in_shape.hw_dims());
let (count_include_pad, normalize) = match kind {
PoolKind::Max => (false, false),
PoolKind::Sum { count_include_pad, normalize } => (count_include_pad, normalize),
};
let params = PoolParams {
n: *in_shape.n().unwrap_or(&1) as i32,
ih: in_shape.hw_dims()[0] as i32,
iw: in_shape.hw_dims()[1] as i32,
c: *in_shape.c() as i32,
oh: out_shape.hw_dims()[0] as i32,
ow: out_shape.hw_dims()[1] as i32,
kh: pool_spec.kernel_shape[0] as i32,
kw: pool_spec.kernel_shape[1] as i32,
stride_h: strides[0] as i32,
stride_w: strides[1] as i32,
pad_h: padding[0].pad_before as i32,
pad_w: padding[1].pad_before as i32,
dil_h: dilations[0] as i32,
dil_w: dilations[1] as i32,
count_include_pad: count_include_pad as i32,
normalize: normalize as i32,
};
let channels_last = pool_spec.data_format.c_is_last();
let base = match kind {
PoolKind::Max if channels_last => "max_pool_2d",
PoolKind::Max => "max_pool_2d_nchw",
PoolKind::Sum { .. } => "sum_pool_2d",
};
ensure!(channels_last || kind == PoolKind::Max, "Metal sum pool is channels-last only");
let pipeline = stream.load_pipeline(LibraryName::NNOps, &format!("{base}_{tname}"))?;
stream.retain_tensor(input);
stream.retain_tensor(output);
let command_buffer = stream.command_buffer();
command_buffer.encode(|encoder| {
encoder.set_compute_pipeline_state(&pipeline);
encoder.set_metal_tensor(0, input, metal::MTLResourceUsage::Read);
encoder.set_metal_tensor(1, output, metal::MTLResourceUsage::Write);
encoder.set_slice(2, std::slice::from_ref(¶ms));
let (fastest, height, depth) = if channels_last {
(params.c, params.ow, params.oh * params.n)
} else {
(params.ow, params.oh, params.c * params.n)
};
let group_w = 32u64.min(fastest as u64).max(1);
encoder.dispatch_threads(
MTLSize { width: fastest as _, height: height as _, depth: depth as _ },
MTLSize { width: group_w, height: 1, depth: 1 },
);
});
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MetalTransform;
use crate::utils::with_borrowed_metal_stream;
use tract_core::ops::cnn::{MaxPool, PaddingSpec, SumPool};
use tract_core::ops::nn::DataFormat;
use tract_core::transform::ModelTransform;
use tract_gpu::tensor::IntoDevice;
fn ramp(dt: DatumType, shape: &[usize]) -> TractResult<Tensor> {
let len: usize = shape.iter().product();
let v: Vec<f32> = (0..len).map(|i| ((i * 17 % 61) as f32 - 30.0) / 8.0).collect();
Ok(Tensor::from_shape(shape, &v)?.cast_to_dt(dt)?.into_owned())
}
fn spec(k: usize, stride: usize, padding: PaddingSpec, c: usize) -> PoolSpec {
spec_fmt(DataFormat::NHWC, k, stride, padding, c)
}
fn spec_fmt(
data_format: DataFormat,
k: usize,
stride: usize,
padding: PaddingSpec,
c: usize,
) -> PoolSpec {
PoolSpec::new(data_format, tvec![k, k], padding, None, Some(tvec![stride, stride]), c, c)
}
fn check(
dt: DatumType,
shape: &[usize],
pool_spec: PoolSpec,
kind: PoolKind,
cpu: Box<dyn TypedOp>,
) -> TractResult<()> {
let input = ramp(dt, shape)?;
let mut model = TypedModel::default();
let i = model.add_source("i", dt.fact(shape))?;
let out = model.wire_node("pool", cpu, &[i])?;
model.select_output_outlets(&out)?;
let expected = model
.clone()
.into_optimized()?
.into_runnable()?
.run(tvec![input.clone().into_tvalue()])?[0]
.clone()
.into_tensor();
let got = with_borrowed_metal_stream(|stream| {
let i = input.clone().into_device()?;
let o_shape = pool_spec.output_shape(input.shape())?;
let o = unsafe { DeviceTensor::uninitialized_dt(dt, &o_shape.shape)? };
dispatch_metal_pool(stream, &pool_spec, kind, &i, &o)?;
stream.wait_until_completed()?;
Ok(o.to_host()?.into_tensor())
})?;
expected
.close_enough(&got, Approximation::Approximate)
.with_context(|| format!("{kind:?} dt={dt:?} shape={shape:?}"))
}
#[test]
fn max_pool_3x3_stride2() -> TractResult<()> {
let s = spec(3, 2, PaddingSpec::Valid, 32);
check(
DatumType::F32,
&[1, 16, 16, 32],
s.clone(),
PoolKind::Max,
Box::new(MaxPool::new(s, None)),
)
}
#[test]
fn max_pool_nchw() -> TractResult<()> {
let s = spec_fmt(DataFormat::NCHW, 2, 2, PaddingSpec::Valid, 24);
check(
DatumType::F32,
&[1, 24, 16, 16],
s.clone(),
PoolKind::Max,
Box::new(MaxPool::new(s, None)),
)
}
#[test]
fn max_pool_nchw_same_padding() -> TractResult<()> {
let s = spec_fmt(DataFormat::NCHW, 3, 2, PaddingSpec::SameUpper, 12);
check(
DatumType::F16,
&[2, 12, 11, 13],
s.clone(),
PoolKind::Max,
Box::new(MaxPool::new(s, None)),
)
}
#[test]
fn max_pool_same_padding() -> TractResult<()> {
let s = spec(3, 1, PaddingSpec::SameUpper, 48);
check(
DatumType::F32,
&[1, 13, 11, 48],
s.clone(),
PoolKind::Max,
Box::new(MaxPool::new(s, None)),
)
}
#[test]
fn avg_pool_3x3_same() -> TractResult<()> {
let s = spec(3, 1, PaddingSpec::SameUpper, 64);
check(
DatumType::F32,
&[1, 12, 12, 64],
s.clone(),
PoolKind::Sum { count_include_pad: false, normalize: true },
Box::new(SumPool::new(s, false, true)),
)
}
#[test]
fn avg_pool_count_include_pad() -> TractResult<()> {
let s = spec(3, 2, PaddingSpec::SameUpper, 16);
check(
DatumType::F32,
&[2, 9, 9, 16],
s.clone(),
PoolKind::Sum { count_include_pad: true, normalize: true },
Box::new(SumPool::new(s, true, true)),
)
}
#[test]
fn sum_pool_no_normalize() -> TractResult<()> {
let s = spec(2, 2, PaddingSpec::Valid, 32);
check(
DatumType::F32,
&[1, 8, 8, 32],
s.clone(),
PoolKind::Sum { count_include_pad: false, normalize: false },
Box::new(SumPool::new(s, false, false)),
)
}
#[test]
fn pool_f16() -> TractResult<()> {
let s = spec(3, 2, PaddingSpec::SameUpper, 96);
check(
DatumType::F16,
&[1, 14, 14, 96],
s.clone(),
PoolKind::Max,
Box::new(MaxPool::new(s, None)),
)
}
#[test]
fn pools_route_through_metal_transform() -> TractResult<()> {
let dt = DatumType::F32;
let shape = [1usize, 14, 14, 32];
let input = ramp(dt, &shape)?;
let mut model = TypedModel::default();
let i = model.add_source("i", dt.fact(shape))?;
let m = model.wire_node(
"max",
MaxPool::new(spec(3, 1, PaddingSpec::SameUpper, 32), None),
&[i],
)?[0];
let a = model.wire_node(
"avg",
SumPool::new(spec(3, 1, PaddingSpec::SameUpper, 32), false, true),
&[m],
)?;
model.select_output_outlets(&a)?;
let cpu = model
.clone()
.into_optimized()?
.into_runnable()?
.run(tvec![input.clone().into_tvalue()])?;
let metal = MetalTransform::default().transform_into(model.into_optimized()?)?;
let n_pools =
metal.nodes().iter().filter(|n| n.op_is::<crate::ops::pool::MetalPool>()).count();
assert_eq!(n_pools, 2, "both pools should be on the GPU");
let got = metal.into_runnable()?.run(tvec![input.into_tvalue()])?;
cpu[0]
.clone()
.into_tensor()
.close_enough(&got[0].clone().into_tensor(), Approximation::Approximate)?;
Ok(())
}
}