1use crate::internal::*;
2use ndarray::prelude::*;
3
4use crate::ops::cnn::pools::{ConcretePoolGeometry, PoolGeometry, PoolSpec};
5
6#[derive(Debug, Clone, new, Hash, PartialEq, Eq)]
7pub struct MaxPool {
8 pub pool_spec: PoolSpec,
9 pub with_index_outputs: Option<DatumType>,
10}
11
12impl Op for MaxPool {
13 fn name(&self) -> StaticName {
14 "MaxPool".into()
15 }
16
17 fn info(&self) -> TractResult<Vec<String>> {
18 Ok(self.pool_spec.info())
19 }
20
21 op_as_typed_op!();
22}
23
24impl EvalOp for MaxPool {
25 op_out_of_plan!();
26
27 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
28 let shape: TVec<TDim> = inputs[0].shape().iter().map(|d| d.to_dim()).collect();
29 self.to_optimized(&shape)?.eval(_ctx, inputs)
30 }
31}
32
33impl TypedOp for MaxPool {
34 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
35 let mut facts = self.pool_spec.output_facts(inputs)?;
36 if let Some(idt) = self.with_index_outputs {
37 facts.push(facts[0].clone());
38 facts[1].datum_type = idt;
39 }
40 Ok(facts)
41 }
42
43 fn declutter(
44 &self,
45 model: &TypedModel,
46 node: &TypedNode,
47 ) -> TractResult<Option<TypedModelPatch>> {
48 if self.with_index_outputs.is_some()
49 && node.outputs[1].successors.len() == 0
50 && !model.output_outlets()?.contains(&OutletId::new(node.id, 1))
51 {
52 let op = Self { with_index_outputs: None, ..self.clone() };
53 let mut patch = TypedModelPatch::default();
54 let mut wire = patch.tap_model(model, node.inputs[0])?;
55 wire = patch.wire_node(&node.name, op, &[wire])?[0];
56 patch.shunt_outside(model, node.id.into(), wire)?;
57 return Ok(Some(patch));
58 }
59 let fact = model.outlet_fact(node.inputs[0])?;
60 if let Some(pool_spec) = self.pool_spec.declutter(&fact.shape)? {
61 return Ok(Some(TypedModelPatch::replace_single_op(
62 model,
63 node,
64 &node.inputs,
65 Self { pool_spec, ..self.clone() },
66 )?));
67 }
68 Ok(None)
69 }
70
71 fn codegen(
75 &self,
76 model: &TypedModel,
77 node: &TypedNode,
78 ) -> TractResult<Option<TypedModelPatch>> {
79 let fact = model.outlet_fact(node.inputs[0])?;
80 if fact.shape.as_concrete().is_none() {
81 return Ok(None);
82 }
83 let mut op = self.to_optimized(&fact.shape.to_tvec())?;
84 op.geometry = op.geometry.optimize_if(fact.shape.as_concrete())?;
85 Ok(Some(TypedModelPatch::replace_single_op(model, node, &node.inputs, op)?))
86 }
87
88 as_op!();
89}
90
91impl MaxPool {
92 fn to_optimized(&self, input_shape: &[TDim]) -> TractResult<OptMaxPool> {
93 Ok(OptMaxPool {
94 pool_spec: self.pool_spec.clone(),
95 with_index_outputs: self.with_index_outputs,
96 geometry: self.pool_spec.compute_geo(input_shape)?,
97 })
98 }
99}
100
101#[derive(Debug, Clone, new, Hash, PartialEq, Eq)]
102pub struct OptMaxPool {
103 pub pool_spec: PoolSpec,
104 pub with_index_outputs: Option<DatumType>,
105 pub geometry: PoolGeometry,
106}
107
108impl Op for OptMaxPool {
109 fn name(&self) -> StaticName {
110 "OptMaxPool".into()
111 }
112
113 fn info(&self) -> TractResult<Vec<String>> {
114 Ok(self.pool_spec.info())
115 }
116
117 op_as_typed_op!();
118}
119
120impl EvalOp for OptMaxPool {
121 op_out_of_plan!();
122
123 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
124 let input = args_1!(inputs);
125 let geo = self.geometry.to_concrete(input.shape())?;
126 dispatch_numbers!(Self::eval_t(input.datum_type())(self, &*input, geo.as_ref()))
127 }
128}
129
130impl TypedOp for OptMaxPool {
131 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
132 let mut facts = self.pool_spec.output_facts(inputs)?;
133 if let Some(idt) = self.with_index_outputs {
134 facts.push(facts[0].clone());
135 facts[1].datum_type = idt;
136 }
137 Ok(facts)
138 }
139
140 as_op!();
141}
142
143impl OptMaxPool {
144 fn eval_t<T: Datum + Copy + num_traits::Bounded + PartialOrd>(
145 &self,
146 input: &Tensor,
147 geo: &ConcretePoolGeometry,
148 ) -> TractResult<TVec<TValue>> {
149 let input_dt = input.datum_type();
150
151 if self.with_index_outputs.is_none()
152 && let Some(values) = self.try_nchw_2x2_f32::<T>(input, geo)?
153 {
154 return Ok(tvec!(values.into_tvalue()));
155 }
156 if self.with_index_outputs.is_none()
157 && let Some(values) = self.try_row_spans::<T>(input, geo)?
158 {
159 return Ok(tvec!(values.into_tvalue()));
160 }
161
162 let input_plain = input.try_as_plain_ram()?;
163 let input: ArrayViewD<T> = input_plain.to_array_view()?;
164 let input_ptr = input.as_ptr();
165
166 let mut values = unsafe { ArrayD::<T>::uninit(&*geo.output_shape.shape).assume_init() };
167 let mut indices = if self.with_index_outputs.is_some() {
168 Some(unsafe { ArrayD::<i32>::uninit(&*geo.output_shape.shape).assume_init() })
169 } else {
170 None
171 };
172 let n = *geo.input_shape.n().unwrap_or(&1);
173 let n_stride_i = geo.input_shape.n_stride().unwrap_or(&0);
174 let n_stride_o = geo.output_shape.n_stride().unwrap_or(&0);
175 unsafe {
176 geo.patch.visit_output(|visitor| {
177 for n in 0..n {
178 let input_offset = n * n_stride_i;
179 let output_offset = n * n_stride_o;
180 for c in 0..*geo.input_shape.c() {
181 let input_offset = input_offset + geo.input_shape.c_stride() * c;
182 let output_offset = output_offset + geo.output_shape.c_stride() * c;
183 let max = visitor
184 .valid_offsets()
185 .map(|v| (v, *input_ptr.offset(v + input_offset as isize)))
186 .fold((0, T::min_value()), |acc, v| if acc.1 < v.1 { v } else { acc });
187 *values
188 .as_mut_ptr()
189 .offset(output_offset as isize + visitor.output_offset) = max.1;
190 if let Some(ref mut indices) = indices {
191 *indices
192 .as_mut_ptr()
193 .offset(output_offset as isize + visitor.output_offset) =
194 max.0 as i32 / geo.patch.spec.output_inner_stride as i32;
195 }
196 }
197 }
198 });
199 }
200 let mut values = values.into_tensor();
201 unsafe {
202 values.set_datum_type(input_dt);
203 }
204 if let Some(dt) = self.with_index_outputs {
205 Ok(tvec!(
206 values.into_tvalue(),
207 indices.unwrap().into_tensor().cast_to_dt(dt)?.into_owned().into_tvalue()
208 ))
209 } else {
210 Ok(tvec!(values.into_tvalue()))
211 }
212 }
213
214 fn try_nchw_2x2_f32<T: Datum>(
217 &self,
218 input: &Tensor,
219 geo: &ConcretePoolGeometry,
220 ) -> TractResult<Option<Tensor>> {
221 let patch = &geo.patch;
222 if T::datum_type() != f32::datum_type()
223 || self.pool_spec.data_format != crate::ops::nn::DataFormat::NCHW
224 || patch.rank() != 2
225 || *geo.input_shape.w_stride() != 1
226 || patch.spec.kernel_shape[..] != [2, 2]
227 || patch.spec.dilations[..] != [1, 1]
228 {
229 return Ok(None);
230 }
231 let mut values =
232 unsafe { Tensor::uninitialized_dt(input.datum_type(), &geo.output_shape.shape)? };
233 unsafe {
234 maxpool_2x2_f32(
235 input.as_ptr::<f32>()? as usize,
236 values.as_slice_mut_unchecked::<f32>(),
237 geo,
238 )?;
239 }
240 Ok(Some(values))
241 }
242
243 fn try_row_spans<T: Datum + Copy + num_traits::Bounded + PartialOrd>(
250 &self,
251 input: &Tensor,
252 geo: &ConcretePoolGeometry,
253 ) -> TractResult<Option<Tensor>> {
254 let patch = &geo.patch;
255 let ish = &geo.input_shape;
256 if patch.rank() != 2 {
257 return Ok(None);
258 }
259 let (h, w, c) = (ish.hw_dims()[0], ish.hw_dims()[1], *ish.c());
260 let px = if self.pool_spec.data_format.c_is_last() { c } else { 1 };
261 let contiguous = if px == 1 {
262 *ish.w_stride() == 1 && *ish.h_stride() == w && *ish.c_stride() == h * w
263 } else {
264 *ish.c_stride() == 1 && *ish.w_stride() == c && *ish.h_stride() == w * c
265 };
266 if !contiguous {
267 return Ok(None);
268 }
269 let (oh, ow) = (patch.output_shape[0], patch.output_shape[1]);
270 let spec = &patch.spec;
271 let (kh, kw) = (spec.kernel_shape[0], spec.kernel_shape[1]);
272 let (sh, sw) = (spec.strides[0], spec.strides[1]);
273 let (dh, dw) = (spec.dilations[0], spec.dilations[1]);
274 let (pt, pl) = (patch.pad_before[0] as isize, patch.pad_before[1] as isize);
275 let mut output = Tensor::zero_dt(input.datum_type(), &geo.output_shape.shape)?;
276 let mut output_plain = output.try_as_plain_ram_mut()?;
277 let output_slice = output_plain.as_slice_mut::<T>()?;
278 output_slice.fill(T::min_value());
279 let input_plain = input.try_as_plain_ram()?;
280 let input_slice = input_plain.as_slice::<T>()?;
281 let column_spans: TVec<(usize, usize, usize)> = (0..kw)
282 .filter_map(|kx| {
283 let offset = (kx * dw) as isize - pl;
284 let last = w as isize - 1 - offset;
285 if last < 0 {
286 return None;
287 }
288 let lo = if offset >= 0 { 0 } else { (-offset as usize).div_ceil(sw) };
289 let hi = (last as usize / sw + 1).min(ow);
290 (lo < hi).then(|| (lo, hi, ((lo * sw) as isize + offset) as usize))
291 })
292 .collect();
293 for (image, out_image) in
294 input_slice.chunks_exact(h * w * px).zip(output_slice.chunks_exact_mut(oh * ow * px))
295 {
296 for (oy, row) in out_image.chunks_exact_mut(ow * px).enumerate() {
297 for ky in 0..kh {
298 let iy = (oy * sh + ky * dh) as isize - pt;
299 if iy < 0 || iy >= h as isize {
300 continue;
301 }
302 let irow = &image[iy as usize * w * px..][..w * px];
303 for &(lo, hi, start) in &column_spans {
304 let (row, taps) = (&mut row[lo * px..hi * px], &irow[start * px..]);
305 if sw == 1 {
306 fold_max(row, taps.iter());
307 } else if px == 1 {
308 fold_max(row, taps.iter().step_by(sw));
309 } else {
310 for (pixel, taps) in row.chunks_exact_mut(px).zip(taps.chunks(sw * px))
311 {
312 fold_max(pixel, taps.iter());
313 }
314 }
315 }
316 }
317 }
318 }
319 Ok(Some(output))
320 }
321}
322
323fn fold_max<'a, T: Copy + PartialOrd + 'a>(row: &mut [T], taps: impl Iterator<Item = &'a T>) {
324 for (o, &v) in row.iter_mut().zip(taps) {
325 *o = if *o < v { v } else { *o };
326 }
327}
328
329unsafe fn maxpool_2x2_f32(
330 i_base: usize,
331 out: &mut [f32],
332 geo: &ConcretePoolGeometry,
333) -> TractResult<()> {
334 let ish = &geo.input_shape;
335 let osh = &geo.output_shape;
336 let (h, w) = (ish.hw_dims()[0] as isize, ish.hw_dims()[1] as isize);
337 let (oh, ow) = (geo.patch.output_shape[0], geo.patch.output_shape[1]);
338 let sh = geo.patch.spec.strides[0] as isize;
339 let sw = geo.patch.spec.strides[1] as isize;
340 let pt = geo.patch.pad_before[0] as isize;
341 let pl = geo.patch.pad_before[1] as isize;
342 let ih_stride = *ish.h_stride() as isize;
343 let oh_stride = *osh.h_stride() as isize;
344 let n = *ish.n().unwrap_or(&1) as isize;
345 let in_stride = *ish.n_stride().unwrap_or(&0) as isize;
346 let on_stride = *osh.n_stride().unwrap_or(&0) as isize;
347 let c = *ish.c() as isize;
348 let ic_stride = *ish.c_stride() as isize;
349 let oc_stride = *osh.c_stride() as isize;
350 let simd_s1 = sh == 1 && sw == 1;
353 let y0 = pt.max(0) as usize;
354 let y1 = ((h - 1 + pt).max(0) as usize).min(oh);
355 let x0 = pl.max(0) as usize;
356 let x1 = ((w - 1 + pl).max(0) as usize).min(ow);
357 let plane_len = oh * ow;
358 let one_plane = |p: isize, plane_out: *mut f32| unsafe {
359 let iptr = i_base as *const f32;
360 let nn = p / c;
361 let cc = p % c;
362 let in_base = nn * in_stride + cc * ic_stride;
363 if simd_s1 && y1 > y0 && x1 > x0 {
364 maxpool_2x2_s1_valid_f32(
365 iptr.offset(in_base + (y0 as isize - pt) * ih_stride + (x0 as isize - pl)),
366 plane_out.offset(y0 as isize * oh_stride + x0 as isize),
367 y1 - y0,
368 x1 - x0,
369 ih_stride,
370 oh_stride,
371 );
372 }
373 for oy in 0..oh {
374 let interior_y = simd_s1 && oy >= y0 && oy < y1;
375 for ox in 0..ow {
376 if interior_y && ox >= x0 && ox < x1 {
377 continue;
378 }
379 let mut m = f32::MIN;
380 for ky in 0..2 {
381 let iy = oy as isize * sh + ky - pt;
382 if iy < 0 || iy >= h {
383 continue;
384 }
385 let row = iptr.offset(in_base + iy * ih_stride);
386 for kx in 0..2 {
387 let ix = ox as isize * sw + kx - pl;
388 if ix < 0 || ix >= w {
389 continue;
390 }
391 m = m.max(*row.offset(ix));
392 }
393 }
394 *plane_out.offset(oy as isize * oh_stride + ox as isize) = m;
395 }
396 }
397 };
398 if oc_stride == plane_len as isize && on_stride == c * oc_stride {
399 tract_linalg::multithread::par_chunks_mut(out, plane_len, out.len(), |first, chunk| {
400 for (i, plane_out) in chunk.chunks_mut(plane_len).enumerate() {
401 one_plane(first as isize + i as isize, plane_out.as_mut_ptr());
402 }
403 Ok(())
404 })
405 } else {
406 for p in 0..(n * c) {
407 let out_base = (p / c) * on_stride + (p % c) * oc_stride;
408 unsafe { one_plane(p, out.as_mut_ptr().offset(out_base)) };
409 }
410 Ok(())
411 }
412}
413
414unsafe fn maxpool_2x2_s1_valid_f32(
415 iptr: *const f32,
416 optr: *mut f32,
417 oh: usize,
418 ow: usize,
419 ih_stride: isize,
420 oh_stride: isize,
421) {
422 unsafe {
423 for oy in 0..oh {
424 let row0 = iptr.offset(oy as isize * ih_stride);
425 let row1 = iptr.offset((oy as isize + 1) * ih_stride);
426 let dst = optr.offset(oy as isize * oh_stride);
427 let mut ox = 0usize;
428 #[cfg(target_arch = "aarch64")]
429 {
430 use std::arch::aarch64::*;
431 while ox + 4 <= ow {
432 let a = vld1q_f32(row0.add(ox));
433 let b = vld1q_f32(row0.add(ox + 1));
434 let c = vld1q_f32(row1.add(ox));
435 let d = vld1q_f32(row1.add(ox + 1));
436 vst1q_f32(dst.add(ox), vmaxq_f32(vmaxq_f32(a, b), vmaxq_f32(c, d)));
437 ox += 4;
438 }
439 }
440 #[cfg(target_arch = "x86_64")]
441 {
442 if is_x86_feature_detected!("avx") {
443 use std::arch::x86_64::*;
444 while ox + 8 <= ow {
445 let a = _mm256_loadu_ps(row0.add(ox));
446 let b = _mm256_loadu_ps(row0.add(ox + 1));
447 let c = _mm256_loadu_ps(row1.add(ox));
448 let d = _mm256_loadu_ps(row1.add(ox + 1));
449 _mm256_storeu_ps(
450 dst.add(ox),
451 _mm256_max_ps(_mm256_max_ps(a, b), _mm256_max_ps(c, d)),
452 );
453 ox += 8;
454 }
455 }
456 }
457 while ox < ow {
458 let m = (*row0.add(ox))
459 .max(*row0.add(ox + 1))
460 .max(*row1.add(ox))
461 .max(*row1.add(ox + 1));
462 *dst.add(ox) = m;
463 ox += 1;
464 }
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::ops::cnn::PaddingSpec;
473 use crate::ops::nn::DataFormat;
474
475 fn test_case() -> (TypedModel, TVec<TValue>) {
476 let mut model = TypedModel::default();
477 let source = model.add_source("data", f32::fact([1, 3, 8, 8])).unwrap();
478 let pool_spec = PoolSpec::new(
479 DataFormat::NCHW,
480 tvec![2, 2],
481 PaddingSpec::Valid,
482 None,
483 Some(tvec![2, 2]),
484 3,
485 3,
486 );
487 let op = MaxPool { pool_spec, with_index_outputs: None };
488 let out = model.wire_node("pool", op, &[source]).unwrap();
489 model.select_output_outlets(&out).unwrap();
490 let input = ndarray::Array4::from_shape_fn((1, 3, 8, 8), |(_, c, y, x)| {
491 (c * 64 + y * 8 + x) as f32
492 })
493 .into_tensor()
494 .into_tvalue();
495 (model, tvec!(input))
496 }
497
498 #[test]
499 fn maxpool_2x2_multithreaded_matches_reference() {
500 let cases = [
501 ((1usize, 24, 320, 320), PaddingSpec::SameUpper, tvec![1usize, 1]),
502 ((2, 8, 33, 17), PaddingSpec::Valid, tvec![2, 2]),
503 ];
504 for (shape, padding, strides) in cases {
505 let c = shape.1;
506 let pool_spec =
507 PoolSpec::new(DataFormat::NCHW, tvec![2, 2], padding, None, Some(strides), c, c);
508 let input = ndarray::Array4::from_shape_fn(shape, |(n, c, y, x)| {
509 ((n * 31 + c * 7 + y * 13 + x * 3) % 97) as f32 * 0.25 - 12.0
510 })
511 .into_tensor()
512 .into_tvalue();
513 let reference =
516 MaxPool { pool_spec: pool_spec.clone(), with_index_outputs: Some(DatumType::I32) }
517 .eval(&EvalContext::out_of_plan(), tvec!(input.clone()))
518 .unwrap();
519 let op = MaxPool { pool_spec, with_index_outputs: None };
520 let pool = tract_linalg::multithread::Executor::multithread(4);
521 let fast = tract_linalg::multithread::multithread_tract_scope(pool, || {
522 op.eval(&EvalContext::out_of_plan(), tvec!(input))
523 })
524 .unwrap();
525 assert_eq!(*fast[0], *reference[0]);
526 }
527 }
528
529 #[test]
530 fn optimized_maxpool_has_concrete_geometry() {
531 let (model, input) = test_case();
532 let plain = model.clone().into_runnable().unwrap().run(input.clone()).unwrap();
533
534 let optimized = model.into_optimized().unwrap();
535 let pool = optimized
536 .nodes
537 .iter()
538 .find_map(|n| n.op_as::<OptMaxPool>())
539 .expect("optimized model should contain an OptMaxPool");
540 assert!(
541 pool.geometry.is_concrete(),
542 "OptMaxPool geometry should be concrete after optimization"
543 );
544
545 let opt = optimized.into_runnable().unwrap().run(input).unwrap();
546 assert_eq!(*opt[0], *plain[0]);
547 }
548}