1use tract_data::itertools::izip;
2use tract_linalg::WeightType;
3use tract_linalg::block_quant::{BlockQuantFact, PackedBlockQuantFormat};
4use tract_linalg::mmm::retain_best;
5use tract_num_traits::Zero;
6
7use crate::internal::*;
8use crate::model::*;
9use crate::ops;
10use crate::ops::array::Pad;
11use crate::ops::array::PadMode;
12use crate::ops::binary::TypedBinOp;
13use crate::ops::cast::cast;
14use crate::ops::cnn::PaddingSpec::*;
15use crate::ops::cnn::conv::block_quant::{BlockQuantIntoShape, SplitGroupBlockQuant};
16use crate::ops::cnn::conv::lazy_im2col::LazyIm2Col;
17use crate::ops::cnn::conv::lazy_im2col::LazyIm2colParams;
18use crate::ops::cnn::wire_reshape_bias_for_bin;
19use crate::ops::einsum::EinSum;
20use crate::ops::math::{Add, Div, Mul, Sub};
21use crate::ops::math::{add, div, mul, sub};
22use crate::ops::matmul::ModePicker;
23use crate::ops::matmul::optimized::AddMatMulGeometry;
24use crate::ops::matmul::optimized::MapOutputAxisToInput;
25use crate::ops::matmul::optimized::MatMulOperand;
26use crate::ops::matmul::pack::{OptMatMulPack, OptSimpleMatMulPack};
27use crate::ops::matmul::quant::wire_ensure_q8_flavour;
28use crate::ops::nn::Reduce;
29
30use super::depth_wise::DepthWise;
31use super::im2col::Im2Col;
32use crate::ops::cnn::conv::KernelFormat;
33use crate::ops::cnn::pools::{ConcretePoolGeometry, PoolGeometry, PoolSpec};
34use crate::ops::matmul::optimized::{OptMatMul, ProtoFusedSpec};
35use crate::ops::nn::{BaseDataShape, DataFormat, DataShape};
36
37use tract_linalg::mmm::{MMMInputFormat, MatMatMul, Query};
38use tract_linalg::pack::{PackedFormat, PackedI8K4};
39
40#[derive(Debug, Clone, new, Hash, PartialEq, Eq)]
41pub struct Conv {
42 pub pool_spec: PoolSpec,
43 pub kernel_fmt: KernelFormat,
44 pub group: usize,
45 pub q_params: Option<DatumType>,
50}
51
52impl Conv {
53 pub fn input_channels(&self) -> usize {
54 self.pool_spec.input_channels
55 }
56
57 pub fn output_channels(&self) -> usize {
58 self.pool_spec.output_channels
59 }
60
61 pub fn wire_kernel_as_g_o_ihw(
62 &self,
63 model: &mut TypedModel,
64 name: &str,
65 mut kernel: OutletId,
66 ) -> TractResult<TVec<OutletId>> {
67 let fact = model.outlet_fact(kernel)?;
68 if fact.is_exotic() {
69 ensure!(self.kernel_fmt == KernelFormat::OIHW && fact.rank() >= 2);
70 kernel = model.wire_node(
71 format!("{name}.prep_kernel.g"),
72 SplitGroupBlockQuant { group: self.group },
73 &[kernel],
74 )?[0];
75 kernel = model.wire_node(
76 format!("{name}.prep_kernel.ihw"),
77 BlockQuantIntoShape {
78 shape: tvec!(
79 self.output_channels() / self.group,
80 self.input_channels() / self.group
81 * self.pool_spec.kernel_shape.iter().product::<usize>(),
82 ),
83 },
84 &[kernel],
85 )?[0];
86 Ok(tvec!(kernel))
87 } else {
88 for (ix, op) in self
89 .kernel_fmt
90 .kernel_as_group_o_ihw_ops(&fact.shape, self.group)
91 .into_iter()
92 .enumerate()
93 {
94 kernel = model.wire_node(format!("{name}.prep_kernel.{ix}"), op, &[kernel])?[0];
95 }
96 Ok(tvec!(kernel))
97 }
98 }
99
100 fn wire_pack_g_o_ihw(
101 &self,
102 model: &mut TypedModel,
103 name: &str,
104 format: &dyn MMMInputFormat,
105 kernel: OutletId,
106 ) -> TractResult<OutletId> {
107 let fact = model.outlet_fact(kernel)?;
108 let wire = if fact.is_exotic() {
109 let fact = model
110 .outlet_fact(kernel)?
111 .exotic_fact
112 .as_ref()
113 .and_then(|of| of.downcast_ref::<BlockQuantFact>())
114 .context("Only manage BlockQuant")?;
115 model.wire_node(
116 format!("{name}.prep_kernel.pack"),
117 OptSimpleMatMulPack {
118 packed_format: format
119 .downcast_ref::<PackedBlockQuantFormat>()
120 .context("Expect a block quant format")?
121 .clone(),
122 k: fact.k(),
123 m: fact.m(),
124 },
125 &[kernel],
126 )?
127 } else {
128 model.wire_node(
130 format!("{name}.prep_kernel.pack"),
131 OptMatMulPack {
132 packers: vec![dyn_clone::clone_box(format)],
133 k_axis: 2,
134 mn_axis: 1,
135 mode_picker: ModePicker::Single,
136 },
137 &[kernel],
138 )?
139 };
140 Ok(wire[0])
141 }
142
143 fn wire_bias_as_non_linear(
145 &self,
146 model: &mut TypedModel,
147 name: &str,
148 bias: OutletId,
149 c_group_axis: usize,
150 ) -> TractResult<(ProtoFusedSpec, OutletId)> {
151 use tract_linalg::BinOp::Add;
152 let fact = model.outlet_fact(bias)?;
153 if fact.shape.volume().is_one() {
154 Ok((ProtoFusedSpec::BinScalar(2, Add), bias))
155 } else {
156 let bias = AxisOp::wire_split_axis(
157 model,
158 format!("{name}.reformat_bias"),
159 bias,
160 0,
161 self.group,
162 )?[0];
163 let pfs =
164 ProtoFusedSpec::BinPerRow(2, Add, MapOutputAxisToInput(tvec!((c_group_axis, 0))));
165 Ok((pfs, bias))
166 }
167 }
168
169 pub unsafe fn wire_as_quant_im2col(
170 &self,
171 model: &mut TypedModel,
172 name: &str,
173 wires: &[OutletId],
174 ) -> TractResult<TVec<OutletId>> {
175 ensure!(self.q_params.is_some());
176 use crate::ops::matmul::quant as qmm;
177
178 let c_dt = self.q_params.unwrap();
179 let &[mut x, mut kernel, bias, mut x0, x_scale, mut k0, mut k_scale, y0, y_scale] = wires
180 else {
181 bail!("Wrong number of inputs")
182 };
183 wire_ensure_q8_flavour(model, name, &mut kernel, "k", &mut k0, i8::datum_type())?;
184 wire_ensure_q8_flavour(model, name, &mut x, "x", &mut x0, i8::datum_type())?;
185
186 let a_fact = model.outlet_fact(kernel)?.clone();
187 let b_fact = model.outlet_fact(x)?.clone();
188
189 let (_geo, m, k, n) = self.compute_geo(&b_fact)?;
190 let (mmm, packing) = self.choose_impl(&b_fact, &a_fact, m, k, &n)?;
191 let output_shape = self.pool_spec.output_shape(&b_fact.shape)?;
192
193 if !model.outlet_fact(k_scale)?.shape.volume().is_one() {
194 if !output_shape.fmt.c_is_last() {
197 k_scale = model.wire_node(
198 format!("{name}.a_scale_axis_fix"),
199 AxisOp::Add(1),
200 &[k_scale],
201 )?[0];
202 }
203 }
204
205 let abc_scale = qmm::combine_scales(model, name, k_scale, x_scale, y_scale)?;
206
207 let im2col = model.wire_node(
208 format!("{name}.im2col"),
209 Im2Col::new(
210 self.pool_spec.clone(),
211 self.group,
212 k,
213 &b_fact.shape,
214 mmm.clone(),
215 packing,
216 )?,
217 &[x, x0],
218 )?[0];
219
220 let g_o_ihw = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
221 let g_o_ihw_as_i32 =
222 model.wire_node(format!("{name}.kernel_as_i32"), cast(i32::datum_type()), &g_o_ihw)?;
223 let sum_ker_g_c_k = model.wire_node(
224 format!("{name}.sum_ker_g_c_k"),
225 Reduce::new(tvec!(2), ops::nn::Reducer::Sum),
226 &g_o_ihw_as_i32,
227 )?;
228 let sum_ker_a_g_c =
229 model.wire_node(format!("{name}.rm_k"), AxisOp::Rm(2), &sum_ker_g_c_k)?;
230 let sum_ker_n_g_c = model.wire_node(
232 format!("{name}.sum_ker_n_g_c.axis_0"),
233 AxisOp::Add(0),
234 &sum_ker_a_g_c,
235 )?;
236 let hw_position = if self.pool_spec.data_format.c_is_last() { 1 } else { 3 };
237 let sum_ker = model.wire_node(
238 format!("{name}.sum_ker_n_g_c"),
239 AxisOp::Add(hw_position),
240 &sum_ker_n_g_c,
241 )?;
242
243 ensure!(
244 mmm.packings()[packing].1.downcast_ref::<PackedFormat>().is_some()
245 || mmm.packings()[packing].1.downcast_ref::<PackedI8K4>().is_some(),
246 "Im2Col/QSumB support PackedFormat or PackedI8K4 activation packings"
247 );
248 let mut sum_x = model.wire_node(
249 format!("{name}.sum_x"),
250 super::QSumB { dt: b_fact.datum_type, n, r: mmm.nr(), k },
251 &[im2col],
252 )?;
253 sum_x = model.wire_node(format!("{name}.add_c"), AxisOp::Add(2), &sum_x)?;
255 if self.pool_spec.data_format.c_is_last() {
256 sum_x =
257 model.wire_node(format!("{name}.transpose_sum_b"), AxisOp::Move(3, 1), &sum_x)?;
258 }
259
260 let (mmm_output_shape, c_axis, h_axis) = self.mmm_output_shape(&output_shape)?;
261 let bias_name = &model.node(bias.node).name;
262 let bias =
263 model.wire_node(format!("{bias_name}.cast"), cast(mmm.internal_type()), &[bias])?[0];
264 let wire = self.wire_mm_weights_bias(
265 model,
266 name,
267 im2col,
268 g_o_ihw[0],
269 bias,
270 mmm,
271 packing,
272 i32::datum_type(),
273 mmm_output_shape.clone().into(),
274 k,
275 c_axis,
276 h_axis,
277 )?;
278
279 let wire = qmm::compensate_zero_points(
280 model,
281 name,
282 wire[0],
283 k.to_dim(),
284 k0,
285 x0,
286 sum_ker[0],
287 sum_x[0],
288 )?;
289
290 let wire = self.wire_remove_group(model, name, &[wire], &mmm_output_shape, c_axis)?;
291 let wire = self.wire_rm_n_if_needed(model, name, &wire)?;
292 let wire = qmm::requant(model, name, wire[0], c_dt, abc_scale, y0)?;
293 Self::wire_geo_reshape(model, name, &[wire], &output_shape)
294 }
295
296 pub fn wire_remove_group<D: DimLike>(
297 &self,
298 model: &mut TypedModel,
299 name: &str,
300 wire: &[OutletId],
301 mmm_output_shape: &[D],
302 c_axis: usize,
303 ) -> TractResult<TVec<OutletId>> {
304 let m = &mmm_output_shape[c_axis];
305 let op = if self.group == 1 {
306 AxisOp::Rm(c_axis - 1)
307 } else {
308 AxisOp::Reshape(
309 c_axis - 1,
310 tvec!(self.group.to_dim(), m.to_dim()),
311 tvec!(m.to_dim() * self.group),
312 )
313 };
314 model.wire_node(format!("{name}.reshape_group"), op, wire)
315 }
316
317 pub unsafe fn wire_as_im2col_pair(
318 &self,
319 model: &mut TypedModel,
320 name: &str,
321 wire: &[OutletId],
322 ) -> TractResult<TVec<OutletId>> {
323 let &[x, w, bias] = wire else { bail!("Wrong number of inputs") };
324 let x_fact = model.outlet_fact(x)?.clone();
325 let w_fact = model.outlet_fact(w)?.clone();
326 let c_dt = crate::ops::matmul::output_type(x_fact.datum_type);
327
328 let (_, m, k, n) = self.compute_geo(&x_fact)?;
329 let (mmm, packing) = self.choose_impl(&x_fact, &w_fact, m, k, &n)?;
330 let geo_output_shape = self.pool_spec.output_shape(&x_fact.shape)?;
331 let (mmm_output_shape, c_axis, h_axis) = self.mmm_output_shape(&geo_output_shape)?;
332
333 let padding =
334 model.add_const(format!("{name}.b0"), Tensor::zero_scalar_dt(x_fact.datum_type)?)?;
335
336 let mut wire: TVec<_> = wire.into();
337 wire[0] = model.wire_node(
338 format!("{name}.im2col"),
339 Im2Col::new(
340 self.pool_spec.clone(),
341 self.group,
342 k,
343 &x_fact.shape,
344 mmm.clone(),
345 packing,
346 )?,
347 &[wire[0], padding],
348 )?[0];
349
350 let g_o_ihw = self.wire_kernel_as_g_o_ihw(model, name, wire[1])?;
351
352 let wire = self
353 .wire_mm_weights_bias(
354 model,
355 name,
356 wire[0],
357 g_o_ihw[0],
358 bias,
359 mmm,
360 packing,
361 c_dt,
362 mmm_output_shape.clone().into(),
363 k.to_usize().unwrap(),
364 c_axis,
365 h_axis,
366 )
367 .context("in wire_opt_matmul")?;
368
369 let wire = self.wire_remove_group(model, name, &wire, &mmm_output_shape, c_axis)?;
370 let wire = self.wire_rm_n_if_needed(model, name, &wire)?;
371 Self::wire_geo_reshape(model, name, &wire, &geo_output_shape)
372 }
373
374 fn mmm_output_shape<D: DimLike>(
376 &self,
377 output_shape: &BaseDataShape<D, TVec<D>>,
378 ) -> TractResult<(TVec<D>, usize, usize)> {
379 let geo_collapsed_out: D = output_shape.hw_dims().iter().cloned().product();
380 let shape: BaseDataShape<D, TVec<D>> = output_shape.fmt.with_n().from_n_c_hw(
381 output_shape.n().cloned().unwrap_or_else(|| 1.into()),
382 output_shape.c().clone(),
383 tvec!(geo_collapsed_out),
384 )?;
385 let mut mmm_output_shape: TVec<D> = shape.shape.clone();
386 let mut c_axis = shape.c_axis();
387 let mut h_axis = shape.h_axis();
388 mmm_output_shape[shape.c_axis()] = mmm_output_shape[c_axis].clone() / self.group;
389 mmm_output_shape.insert(c_axis, self.group.into());
390 if h_axis > c_axis {
391 h_axis += 1;
392 }
393 c_axis += 1;
394 Ok((mmm_output_shape, c_axis, h_axis))
395 }
396
397 fn wire_rm_n_if_needed(
398 &self,
399 model: &mut TypedModel,
400 name: &str,
401 wire: &[OutletId],
402 ) -> TractResult<TVec<OutletId>> {
403 if self.pool_spec.data_format.has_n() {
404 Ok(wire.into())
405 } else {
406 model.wire_node(format!("{name}.rm_n"), AxisOp::Rm(0), wire)
407 }
408 }
409
410 fn wire_geo_reshape<D: DimLike>(
411 model: &mut TypedModel,
412 name: &str,
413 wire: &[OutletId],
414 output_shape: &BaseDataShape<D, TVec<D>>,
415 ) -> TractResult<TVec<OutletId>> {
416 let geo_collapsed_out: D = output_shape.hw_dims().iter().cloned().product();
417 model
418 .wire_node(
419 name,
420 AxisOp::Reshape(
421 output_shape.h_axis(),
422 tvec!(geo_collapsed_out.to_dim()),
423 output_shape.hw_dims().iter().map(|d| d.to_dim()).collect(),
424 ),
425 wire,
426 )
427 .context("in wire_geo_reshape")
428 }
429
430 pub unsafe fn wire_as_lazy_im2col(
431 &self,
432 model: &mut TypedModel,
433 name: &str,
434 wire: &[OutletId],
435 ) -> TractResult<TVec<OutletId>> {
436 let &[mut x, kernel, bias] = wire else { bail!("Wrong number of inputs") };
437 let mut x_fact = model.outlet_fact(x)?.clone();
438 let w_fact = model.outlet_fact(kernel)?.clone();
439 let (geo, m, k, n) = self.compute_geo(&x_fact)?;
440 let (mmm, packing) = self.choose_impl(&x_fact, &w_fact, m, k, &n)?;
441 debug!("{name} as lazy_im2col: m={m} k={k} n={n} {mmm:?}");
442 let input_shape = x_fact.shape.as_concrete().unwrap().to_vec();
443 let mut geo = geo.to_concrete(&input_shape)?.into_owned();
444 let mut input_shape: DataShape = self.pool_spec.data_format.shape(input_shape.into())?;
445 let padding = self.pool_spec.computed_padding(input_shape.hw_dims());
446 if padding.iter().any(|axis| axis.pad_before != 0 || axis.pad_after != 0) {
447 let mut pads = vec![(0, 0); x_fact.rank()];
448 for (ix, ax) in padding.iter().enumerate() {
449 pads[input_shape.h_axis() + ix] = (ax.pad_before, ax.pad_after);
450 }
451 let op = crate::ops::array::Pad {
452 mode: crate::ops::array::PadMode::Constant(
453 Tensor::zero_scalar_dt(x_fact.datum_type)?.into_arc_tensor(),
454 ),
455 pads,
456 };
457 x = model.wire_node(format!("{name}.pad"), op, &[x])?[0];
458 let valid_pool_spec = PoolSpec { padding: Valid, ..self.pool_spec.clone() };
459 x_fact = model.outlet_fact(x)?.clone();
460 let concrete_shape = x_fact.shape.as_concrete().unwrap();
461 input_shape = valid_pool_spec.data_format.shape(concrete_shape.into())?;
462 geo = valid_pool_spec
463 .compute_geo(&x_fact.shape)?
464 .to_concrete(concrete_shape)?
465 .into_owned();
466 }
467 let c_dt = crate::ops::matmul::output_type(x_fact.datum_type);
468 let c_stride = input_shape.c_stride();
469 let size_of_b = x_fact.datum_type.size_of() as isize;
470 let n_byte_offsets: Vec<isize> =
471 geo.patch.centers_offsets().into_iter().map(|x| x * size_of_b).collect();
472 let ci_per_group = self.input_channels() / self.group;
475 let k_byte_offsets: Vec<isize> = (0..ci_per_group)
476 .flat_map(|ici| {
477 geo.patch
478 .standard_layout_data_field
479 .iter()
480 .map(move |x| (x + (ici * c_stride) as isize) * size_of_b)
481 })
482 .collect();
483 let group_stride_bytes = (ci_per_group * c_stride) as isize * size_of_b;
484 let (mmm_output_shape, c_axis, h_axis) = self.mmm_output_shape(&geo.output_shape)?;
485 let packer = mmm.packings()[packing]
486 .1
487 .downcast_ref::<PackedFormat>()
488 .with_context(|| {
489 format_err!(
490 "Quand Im2Col expects regular packed format, got {:?}",
491 mmm.packings()[packing].1
492 )
493 })?
494 .clone();
495 let params = LazyIm2colParams { packer, n_byte_offsets, k_byte_offsets };
496 let x = model.wire_node(
497 format!("{name}.lazyIm2col"),
498 LazyIm2Col { params: Arc::new(params), group: self.group, group_stride_bytes },
499 &[x],
500 )?[0];
501
502 let kernel = self.wire_kernel_as_g_o_ihw(model, name, kernel)?[0];
503 let wire = self.wire_mm_weights_bias(
504 model,
505 name,
506 x,
507 kernel,
508 bias,
509 mmm,
510 packing,
511 c_dt,
512 mmm_output_shape.clone().into(),
513 k,
514 c_axis,
515 h_axis,
516 )?;
517
518 let wire = self.wire_remove_group(model, name, &wire, &mmm_output_shape, c_axis)?;
519 let wire = self.wire_rm_n_if_needed(model, name, &wire)?;
520 Self::wire_geo_reshape(model, name, &wire, &geo.output_shape)
521 }
522
523 #[allow(clippy::type_complexity)]
524 fn compute_geo(
525 &self,
526 input_fact: &TypedFact,
527 ) -> TractResult<(PoolGeometry, usize, usize, TDim)> {
528 let geo = self.pool_spec.compute_geo(&input_fact.shape)?;
529
530 trace!("output channels: {:?}", self.output_channels());
531 let m = self.output_channels() / self.group;
532 let k = self.input_channels() * self.pool_spec.kernel_shape.iter().product::<usize>()
533 / self.group;
534 let n: TDim =
535 self.pool_spec.output_shape(&input_fact.shape)?.hw_dims().iter().cloned().product();
536 Ok((geo, m, k, n))
537 }
538
539 fn choose_impl(
540 &self,
541 input_fact: &TypedFact,
542 weight_fact: &TypedFact,
543 m: usize,
544 k: usize,
545 n: &TDim,
546 ) -> TractResult<(Box<dyn MatMatMul>, usize)> {
547 let w_dt = weight_fact.datum_type;
548 let x_dt = input_fact.datum_type;
549
550 let acc = if x_dt.is_float() { x_dt } else { i32::datum_type() };
551 let query = Query {
554 weight: if weight_fact.is_exotic() {
555 let bqf = weight_fact
556 .exotic_fact
557 .as_ref()
558 .and_then(|of| of.downcast_ref::<BlockQuantFact>())
559 .unwrap();
560 WeightType::BlockQuant(bqf.format.clone())
561 } else {
562 w_dt.into()
563 },
564 activation: x_dt,
565 accumulators: tvec!(acc),
566 store: None,
567 allow_extractor: false,
568 m: Some(m),
569 k: Some(k),
570 n: n.as_usize(),
571 };
572 if weight_fact.is_exotic() {
573 let mut suitable = tract_linalg::MmmDispatch::native().suitable(&query);
574 retain_best(&mut suitable);
575 suitable
576 .into_iter()
577 .map(|(mmm, p, _)| (mmm, p))
578 .max_by_key(|(mmm, _)| mmm.mr() * mmm.nr())
579 .context("No matmul found")
580 } else {
581 let (mmm, packing, _) =
582 tract_linalg::MmmDispatch::native().pick(&query).context("No matmul found")?;
583 Ok((mmm, packing))
584 }
585 }
586
587 #[allow(clippy::too_many_arguments)]
588 fn wire_mm_weights_bias(
589 &self,
590 model: &mut TypedModel,
591 name: &str,
592 input: OutletId,
593 g_o_ihw: OutletId,
594 bias: OutletId,
595 mmm: Box<dyn MatMatMul>,
596 packing: usize,
597 c_datum_type: DatumType,
598 mmm_output_shape: ShapeFact,
599 k: usize,
600 c_m_axis: usize,
601 c_n_axis: usize,
602 ) -> TractResult<TVec<OutletId>> {
603 ensure!(model.outlet_fact(bias)?.datum_type == mmm.internal_type());
604 let a_pack = &mmm.packings()[packing].0;
605 let packed_ker = self
606 .wire_pack_g_o_ihw(model, name, &**a_pack, g_o_ihw)
607 .context("in kernel_as_packed_as")?;
608 let (mut c_to_a_axis_mapping, mut c_to_b_axis_mapping) = (tvec!(), tvec!());
609
610 c_to_a_axis_mapping.push((c_m_axis - 1, 0)); c_to_b_axis_mapping.push((0, 0)); c_to_b_axis_mapping.push((c_m_axis - 1, 1)); let geo = AddMatMulGeometry {
615 k: k.to_dim(),
616 c_to_a_axis_mapping: MapOutputAxisToInput(c_to_a_axis_mapping),
617 c_to_b_axis_mapping: MapOutputAxisToInput(c_to_b_axis_mapping),
618 };
619 let mut ops: Vec<ProtoFusedSpec> = vec![ProtoFusedSpec::AddMatMul {
620 geo,
621 a: MatMulOperand::Input(1),
622 b: MatMulOperand::Input(0),
623 packings: vec![(packing, None)],
624 }];
625 let mut wires: TVec<OutletId> = tvec!(input, packed_ker);
626 let bias_fact = model.outlet_fact(bias)?;
627 if bias_fact.konst.is_none() || !bias_fact.konst.as_ref().unwrap().is_all_zero()? {
628 let (fused, bias) = self.wire_bias_as_non_linear(model, name, bias, c_m_axis - 1)?;
629 wires.push(bias);
630 ops.push(fused);
631 }
632 ops.push(ProtoFusedSpec::Store(vec![unsafe {
633 mmm.c_view(Some(c_m_axis), Some(c_n_axis))
634 }]));
635 model.wire_node(
636 format!("{name}.matmatmul"),
637 OptMatMul::new(
638 vec![mmm],
639 ModePicker::Single,
640 c_datum_type.fact(mmm_output_shape),
641 Some(c_m_axis),
642 Some(c_n_axis),
643 ops,
644 packing == 0 && self.group == 1,
645 )?,
646 &wires,
647 )
648 }
649
650 pub fn wire_as_depth_wise(
651 &self,
652 model: &mut TypedModel,
653 name: &str,
654 wire: &[OutletId],
655 ) -> TractResult<OutletId> {
656 let &[x, kernel, mut bias] = wire else { bail!("Wrong number of inputs") };
657 let x_fact = model.outlet_fact(x)?.clone();
658 let x_shape = x_fact.shape.as_concrete().unwrap();
659 let ConcretePoolGeometry { input_shape, patch, output_shape } =
660 self.pool_spec.compute_geo(&x_fact.shape)?.to_concrete(x_shape)?.into_owned();
661 let kernel = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
662 let c_axis = self.pool_spec.data_format.shape(x_shape)?.c_axis();
663 bias = wire_reshape_bias_for_bin(
664 model,
665 name,
666 bias,
667 x_fact.rank(),
668 c_axis,
669 self.output_channels(),
670 )?[0];
671 let op = DepthWise::new(patch, input_shape, output_shape);
672 Ok(model.wire_node(name, op, &[x, kernel[0], bias])?[0])
673 }
674
675 fn try_blocked_conv(&self, input_fact: &TypedFact) -> Option<super::BlockedConv> {
681 let enabled = if cfg!(target_family = "wasm") {
686 !TRACT_DISABLE_BLOCKED_CONV.get()
687 } else {
688 TRACT_ENABLE_BLOCKED_CONV.get()
689 };
690 if !enabled {
691 return None;
692 }
693 if self.q_params.is_some() {
694 return None;
695 }
696 if input_fact.datum_type != f32::datum_type() {
697 return None;
698 }
699 if self.pool_spec.data_format != crate::ops::nn::DataFormat::NCHW {
700 return None;
701 }
702 if self.pool_spec.rank() != 2 || self.pool_spec.kernel_shape[1] != 1 {
703 return None;
704 }
705 if self.pool_spec.stride(1) != 1 || self.pool_spec.dilation(1) != 1 {
706 return None;
707 }
708 let group = self.group;
709 let oc = self.output_channels();
710 let c_in = self.input_channels();
711 if group == 0 || !oc.is_multiple_of(group) || !c_in.is_multiple_of(group) {
712 return None;
713 }
714 let ocg = oc / group;
715 if ocg == 0 || ocg > 8 {
718 return None;
719 }
720 let concrete = input_fact.shape.as_concrete()?;
721 let shape = self.pool_spec.data_format.shape(concrete).ok()?;
722 let h_axis = shape.h_axis();
723 let h_in = concrete[h_axis];
724 let w = concrete[h_axis + 1];
725 let pads = self.pool_spec.computed_padding(shape.hw_dims());
726 Some(super::BlockedConv {
727 n: *shape.n().unwrap_or(&1),
728 c_in,
729 h_in,
730 w,
731 oc,
732 group,
733 kh: self.pool_spec.kernel_shape[0],
734 stride_h: self.pool_spec.stride(0),
735 dil_h: self.pool_spec.dilation(0),
736 pad_before_h: pads[0].pad_before,
737 h_out: pads[0].convoluted,
738 })
739 }
740
741 fn wire_as_blocked_conv(
742 &self,
743 model: &mut TypedModel,
744 name: &str,
745 wire: &[OutletId],
746 op: super::BlockedConv,
747 ) -> TractResult<OutletId> {
748 let &[x, kernel, bias] = wire else { bail!("Wrong number of inputs") };
749 let g_o_ihw = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
752 Ok(model.wire_node(name, op, &[x, g_o_ihw[0], bias])?[0])
753 }
754
755 fn declutter_stride_slice_to_downsample(
756 &self,
757 model: &TypedModel,
758 node: &TypedNode,
759 ) -> TractResult<Option<TypedModelPatch>> {
760 let spatial_rank = self.pool_spec.rank();
761 if let Some(axis) = (0..spatial_rank).find(|&ax| {
762 self.pool_spec.stride(ax) > 1
763 && self.pool_spec.padding.valid_dim(ax, self.pool_spec.stride(ax) == 1)
764 && (self.pool_spec.kernel_shape[ax] == 1
765 || self.pool_spec.dilation(ax).is_multiple_of(self.pool_spec.stride(ax)))
766 }) {
767 let input_fact = model.outlet_fact(node.inputs[0])?;
768 let downsample_factor = self.pool_spec.stride(axis);
769 let mut new_op = self.clone();
770 if new_op.pool_spec.dilation(axis) > 1 {
771 new_op.pool_spec.dilations.as_mut().unwrap()[axis] =
772 new_op.pool_spec.dilations.as_mut().unwrap()[axis].divceil(downsample_factor);
773 }
774 new_op.pool_spec.strides.as_mut().unwrap()[axis] /= downsample_factor;
775 let mut patch = TypedModelPatch::default();
776 let mut taps = patch.taps(model, &node.inputs)?;
777 let shape = self.pool_spec.data_format.shape(&input_fact.shape)?;
778 taps[0] = patch.wire_node(
779 format!("{}.downsample.{}", node.name, axis),
780 crate::ops::Downsample::new(axis + shape.h_axis(), downsample_factor as isize, 0),
781 &[taps[0]],
782 )?[0];
783 let id = patch.wire_node(&*node.name, new_op, &taps)?[0];
784 patch.shunt_outside(model, OutletId::new(node.id, 0), id)?;
785 return Ok(Some(patch));
786 }
787 Ok(None)
788 }
789
790 fn declutter_as_einsum(
791 &self,
792 model: &TypedModel,
793 node: &TypedNode,
794 ) -> TractResult<Option<TypedModelPatch>> {
795 let (input_facts, output_facts) = model.node_facts(node.id)?;
796 let full_input_shape = input_facts[0].shape.to_tvec();
797 let input_shape = self.pool_spec.data_format.shape(&full_input_shape)?;
798 if self.group == 1
799 && self.pool_spec.strides().iter().all(|s| *s == 1)
800 && self.pool_spec.dilations().iter().all(|d| *d == 1)
801 && self.pool_spec.kernel_shape.iter().product::<usize>() == 1
802 && self
803 .pool_spec
804 .computed_padding(input_shape.hw_dims())
805 .iter()
806 .all(|pad| pad.pad_after.is_zero() && pad.pad_before.is_zero())
807 {
808 let mut axes = self.axes_mapping(&input_facts, &output_facts)?;
809 let mut patch = TypedModelPatch::new("declutter_as_einsum");
810 let mut taps = patch.taps(model, &node.inputs)?;
811 let name = &node.name;
812 let co = self.output_channels();
813 taps[1] =
814 self.wire_kernel_as_g_o_ihw(&mut patch, &format!("{name}.filters"), taps[1])?[0];
815 taps[1] =
816 patch.wire_node(format!("{name}.filters_as_co_ci"), AxisOp::Rm(0), &[taps[1]])?[0];
817
818 while axes.rank(InOut::In(1)) > 0 {
819 axes = axes.remove_axis_occurency(InOut::In(1), 0)?;
820 }
821 axes = axes
822 .with_extra_axis_occurency('O', InOut::In(1), 0)?
823 .with_extra_axis_occurency('I', InOut::In(1), 1)?;
824
825 let bias_fact = input_facts[2];
826 let wire = if self.q_params.is_some() {
827 if bias_fact.rank() == 1 {
828 axes = axes.linking('O', (InOut::In(2), 0))?;
829 }
830 let op = EinSum { axes, operating_dt: i32::datum_type(), q_params: self.q_params };
831 patch.wire_node(format!("{name}.einsum"), op, &taps)?[0]
832 } else {
833 axes = axes.remove_slot(InOut::In(2))?;
834 let op = EinSum { axes, operating_dt: input_facts[0].datum_type, q_params: None };
835 let mut wire = patch.wire_node(format!("{name}.einsum"), op, &taps[0..2])?[0];
836
837 if !bias_fact.konst.as_ref().map(|f| f.is_zero()).transpose()?.unwrap_or(false) {
838 let bias_current_shape =
839 if bias_fact.rank() == 0 { tvec!() } else { tvec!(co.to_dim()) };
840 let mut bias_shape = tvec!(1.to_dim(); input_shape.rank());
841 if bias_fact.rank() > 0 {
842 bias_shape[input_shape.c_axis()] = co.to_dim();
843 }
844 let b = patch.wire_node(
845 format!("{name}.bias.reshape"),
846 AxisOp::Reshape(0, bias_current_shape, bias_shape),
847 &[taps[2]],
848 )?[0];
849 wire = patch.wire_node(
850 format!("{name}.bias"),
851 crate::ops::math::add(),
852 &[wire, b],
853 )?[0];
854 }
855 wire
856 };
857 patch.node_mut(wire.node).name = node.name.to_string();
858 patch.shunt_outside(model, node.id.into(), wire)?;
859 return Ok(Some(patch));
860 }
861 Ok(None)
862 }
863
864 fn declutter_precursor_padding(
865 &self,
866 model: &TypedModel,
867 node: &TypedNode,
868 ) -> TractResult<Option<TypedModelPatch>> {
869 rule_if!(!matches!(
870 self.pool_spec.padding,
871 ExplicitOnnxPool(_, _, _) | SameLower | SameUpper
872 ));
873 let prec = model.node(node.inputs[0].node);
874 rule_if_some!(pad = prec.op_as::<Pad>());
875 rule_if_let!(PadMode::Constant(value) = &pad.mode);
876 let shape = self.pool_spec.data_format.shape(&model.outlet_fact(node.inputs[0])?.shape)?;
877 rule_if!(value.is_zero()?);
878 rule_if!(pad.pads[shape.c_axis()] == (0, 0));
879 if self.pool_spec.data_format.has_n() {
880 rule_if!(pad.pads[0] == (0, 0));
881 }
882 let mut before: TVec<usize> = pad.pads[shape.hw_axes()].iter().map(|pair| pair.0).collect();
883 let mut after: TVec<usize> = pad.pads[shape.hw_axes()].iter().map(|pair| pair.1).collect();
884 if let Explicit(bef, aft) = &self.pool_spec.padding {
885 izip!(&mut before, bef).for_each(|(pad, cv)| *pad += cv);
886 izip!(&mut after, aft).for_each(|(pad, cv)| *pad += cv);
887 }
888 let padding = Explicit(before, after);
889 let mut new = self.clone();
890 new.pool_spec.padding = padding;
891 let mut patch = TypedModelPatch::default();
892 let mut wire = patch.taps(model, &node.inputs)?;
893 wire[0] = patch.tap_model(model, prec.inputs[0])?;
894 let wire = patch.wire_node(&node.name, new, &wire)?;
895 patch.shunt_outside(model, node.id.into(), wire[0])?;
896 Ok(Some(patch))
897 }
898
899 fn declutter_channel_arithmetic_succ(
900 &self,
901 model: &TypedModel,
902 node: &TypedNode,
903 ) -> TractResult<Option<TypedModelPatch>> {
904 rule_if!(self.q_params.is_none());
905 rule_if!(self.group == 1);
906 rule_if_let!(&[succ_outlet] = &*node.outputs[0].successors);
907 let succ = model.node(succ_outlet.node);
908 rule_if_some!(bin = succ.op_as::<TypedBinOp>());
909 let other_input = succ.inputs[1 - succ_outlet.slot];
910 let axes_mapping = model.node_axes_mapping(succ.id)?;
911 let input_shape =
912 self.pool_spec.data_format.shape(&model.outlet_fact(node.inputs[0])?.shape)?;
913 let conv_c_axis = input_shape.c_axis();
914 rule_if!(
915 axes_mapping.axis((InOut::In(succ_outlet.slot), conv_c_axis))?.inputs
916 [1 - succ_outlet.slot]
917 .len()
918 == 1
919 );
920 let mut other_expected_shape = tvec!(1.to_dim(); input_shape.rank());
921 other_expected_shape[conv_c_axis] = self.output_channels().to_dim();
922 rule_if!(*other_expected_shape == *model.outlet_fact(other_input)?.shape);
923
924 let mut patch = TypedModelPatch::default();
925 let [input, mut kernel, mut bias] = *patch.taps(model, &node.inputs)? else {
926 panic!("Expect three inputs");
927 };
928 let name = &node.name;
929 let succ_name = &succ.name;
930
931 let operand = patch.tap_model(model, other_input)?;
932
933 let renamed_bias = format!("{name}.{succ_name}.bias");
934 let renamed_kernel = format!("{name}.{succ_name}.kernel");
935 bias = wire_reshape_bias_for_bin(
936 &mut patch,
937 format!("{renamed_bias}.reshape"),
938 bias,
939 1,
940 0,
941 self.output_channels(),
942 )?[0];
943
944 let operand = wire_reshape_bias_for_bin(
945 &mut patch,
946 format!("{renamed_bias}.reshape_operand"),
947 operand,
948 1,
949 0,
950 self.output_channels(),
951 )?[0];
952
953 let operand_fact = patch.outlet_fact(operand)?.shape.to_tvec();
954 let kernel_fact = patch.outlet_fact(kernel)?;
955 let mut operand_shape_for_kernel = tvec!(1.to_dim(); 2 + input_shape.hw_rank());
956 operand_shape_for_kernel[self.kernel_fmt.o_axis(&kernel_fact.shape)] =
957 self.output_channels().to_dim();
958 let operand_for_kernel = patch.wire_node(
959 format!("{renamed_kernel}.reshape_operand"),
960 AxisOp::Reshape(0, operand_fact, operand_shape_for_kernel),
961 &[operand],
962 )?[0];
963
964 if bin.0.is::<Sub>() && succ_outlet.slot == 0 {
965 bias = patch.wire_node(&renamed_bias, sub(), &[bias, operand])?[0];
966 } else if bin.0.is::<Sub>() {
967 bias = patch.wire_node(&renamed_bias, sub(), &[operand, bias])?[0];
968 } else if bin.0.is::<Div>() && succ_outlet.slot == 0 {
969 bias = patch.wire_node(&renamed_bias, div(), &[bias, operand])?[0];
970 kernel = patch.wire_node(&renamed_kernel, div(), &[kernel, operand_for_kernel])?[0];
971 } else if bin.0.is::<Div>() {
972 bias = patch.wire_node(&renamed_bias, div(), &[operand, bias])?[0];
973 kernel = patch.wire_node(&renamed_kernel, div(), &[operand_for_kernel, kernel])?[0];
974 } else if bin.0.is::<Add>() {
975 bias = patch.wire_node(&renamed_bias, add(), &[bias, operand])?[0];
976 } else if bin.0.is::<Mul>() {
977 bias = patch.wire_node(&renamed_bias, mul(), &[bias, operand])?[0];
978 kernel = patch.wire_node(&renamed_kernel, mul(), &[kernel, operand_for_kernel])?[0];
979 } else {
980 return Ok(None);
981 };
982 let wire = patch.wire_node(&node.name, self.clone(), &[input, kernel, bias])?[0];
983 patch.shunt_outside(model, succ_outlet.node.into(), wire)?;
984 Ok(Some(patch))
985 }
986}
987
988impl Op for Conv {
989 fn name(&self) -> StaticName {
990 "Conv".into()
991 }
992
993 fn info(&self) -> TractResult<Vec<String>> {
994 let mut info = self.pool_spec.info();
995 info.push(format!("Kernel {:?} (groups:{})", self.kernel_fmt, self.group));
996 Ok(info)
997 }
998
999 fn validation(&self) -> Validation {
1000 Validation::Rounding
1001 }
1002
1003 op_as_typed_op!();
1004}
1005
1006impl EvalOp for Conv {
1007 op_out_of_plan!();
1008
1009 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
1010 let mut model = TypedModel::default();
1011 let wire: TVec<OutletId> = inputs
1012 .iter()
1013 .enumerate()
1014 .map(|(ix, v)| model.add_source(format!("source.{ix}"), v.datum_type().fact(v.shape())))
1015 .collect::<TractResult<_>>()?;
1016 let wire = unsafe {
1017 if self.q_params.is_some() {
1018 self.wire_as_quant_im2col(&mut model, "im2col-adhoc", &wire)?
1019 } else {
1020 self.wire_as_im2col_pair(&mut model, "im2col-adhoc", &wire)?
1021 }
1022 };
1023 model.select_output_outlets(&wire)?;
1024 model.into_runnable()?.run(inputs)
1025 }
1026}
1027
1028impl TypedOp for Conv {
1029 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
1030 ensure!(self.q_params.is_some() || inputs[0].datum_type.is_float());
1031 let q_inputs = if self.q_params.is_some() { 6 } else { 0 };
1032 ensure!(inputs[1].datum_type.is_number() || self.kernel_fmt == KernelFormat::OIHW);
1033 if inputs.len() != 3 + q_inputs {
1034 bail!("Wrong number of inputs: expected {} got {}", 3 + q_inputs, inputs.len());
1035 }
1036 if self.q_params.is_some() {
1037 ensure!(inputs[2].datum_type == i32::datum_type());
1038 ensure!(inputs[3].datum_type == i32::datum_type());
1039 ensure!(inputs[4].datum_type.is_float());
1040 ensure!(inputs[5].datum_type == i32::datum_type());
1041 ensure!(inputs[6].datum_type.is_float());
1042 ensure!(inputs[7].datum_type == i32::datum_type());
1043 ensure!(inputs[8].datum_type.is_float());
1044 }
1045 ensure!(self.pool_spec.rank() + 2 == inputs[1].shape.len());
1046 if self.pool_spec.data_format.shape(&*inputs[0].shape)?.c()
1047 != &self.input_channels().to_dim()
1048 {
1049 bail!(
1050 "Inconsistent convolution: input is {:?}, but kernel expects {} input channels.\n{:?}",
1051 inputs[0],
1052 self.input_channels(),
1053 self
1054 );
1055 }
1056 if let ExplicitOnnxPool(bef, after, _) | Explicit(bef, after) = &self.pool_spec.padding {
1057 anyhow::ensure!(bef.len() == self.pool_spec.rank());
1058 anyhow::ensure!(after.len() == self.pool_spec.rank());
1059 }
1060 ensure!(
1061 inputs[2].rank() == 0
1062 || (inputs[2].rank() == 1
1063 && inputs[2].shape.volume() == self.output_channels().to_dim()),
1064 "Bias should be scalar or a vector with one value per output channel. Output channels is {}, bias is {:?}",
1065 self.output_channels(),
1066 inputs[2]
1067 );
1068 let mut fact = self.pool_spec.output_facts(inputs)?.remove(0);
1069 if let Some(dt) = self.q_params {
1070 fact.datum_type = dt;
1071 } else {
1072 ensure!(
1073 inputs[1].is_exotic() || inputs[0].datum_type == inputs[1].datum_type,
1074 "Convolution input, weights and bias must have the same type, got {inputs:?}",
1075 )
1076 }
1077 Ok(tvec!(fact))
1078 }
1079
1080 fn axes_mapping(
1081 &self,
1082 inputs: &[&TypedFact],
1083 outputs: &[&TypedFact],
1084 ) -> TractResult<AxesMapping> {
1085 let fact = &inputs[0];
1086 let shape = self.pool_spec.data_format.shape(&fact.shape)?;
1087 let mut axes = AxesMapping::disconnected(inputs, outputs)?
1088 .renaming((InOut::In(0), shape.c_axis()), 'I')?
1089 .renaming((InOut::Out(0), shape.c_axis()), 'O')?;
1090 if let Some(n_axis) = shape.n_axis() {
1091 axes = axes
1092 .renaming((InOut::In(0), n_axis), 'N')?
1093 .linking('N', (InOut::Out(0), n_axis))?;
1094 }
1095 let h_axis = shape.h_axis();
1096 let geo = "HWXYZ".chars().chain('a'..);
1097 let kernel_spatial_shape = &self.pool_spec.kernel_shape;
1098 let padding = self.pool_spec.computed_padding(shape.hw_dims());
1099 for ((ix, &dim), repr) in kernel_spatial_shape.iter().enumerate().zip(geo) {
1100 if dim == 1
1101 && self.pool_spec.dilation(ix) == 1
1102 && self.pool_spec.stride(ix) == 1
1103 && padding[ix].pad_before.is_zero()
1104 && padding[ix].pad_after.is_zero()
1105 {
1106 axes = axes
1107 .renaming((InOut::In(0), ix + h_axis), repr)?
1108 .linking(repr, (InOut::Out(0), ix + h_axis))?;
1109 }
1110 }
1111 if self.q_params.is_some() {
1112 for (qp_ix, qp) in inputs.iter().enumerate().skip(3) {
1113 if qp.rank() == 1 {
1114 axes = match qp_ix {
1115 3 | 4 => axes.linking('I', (InOut::In(qp_ix), 0))?,
1116 5 | 6 => axes.linking('O', (InOut::In(qp_ix), 0))?,
1117 7 | 8 => axes.linking('O', (InOut::In(qp_ix), 0))?,
1118 _ => unreachable!(),
1119 };
1120 }
1121 }
1122 }
1123 Ok(axes)
1124 }
1125
1126 fn declutter(
1127 &self,
1128 model: &TypedModel,
1129 node: &TypedNode,
1130 ) -> TractResult<Option<TypedModelPatch>> {
1131 macro_rules! pass {
1132 ($func:ident) => {
1133 if let Some(mut r) = self.$func(model, node).context(stringify!($func))? {
1134 trace!(stringify!($func));
1135 r.push_context(stringify!($func));
1136 return Ok(Some(r));
1137 }
1138 };
1139 }
1140 pass!(declutter_stride_slice_to_downsample);
1141 pass!(declutter_as_einsum);
1142 pass!(declutter_channel_arithmetic_succ);
1143 pass!(declutter_precursor_padding);
1144 Ok(None)
1145 }
1146
1147 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
1148 let shape = self.pool_spec.data_format.shape(inputs[0].shape.to_tvec())?;
1149 let kernel_spatial_shape = &self.pool_spec.kernel_shape;
1150 let output_dims = self.pool_spec.padding.compute(
1151 shape.hw_dims(),
1152 kernel_spatial_shape,
1153 &self
1154 .pool_spec
1155 .dilations
1156 .clone()
1157 .unwrap_or_else(|| tvec!(1; kernel_spatial_shape.len())),
1158 &self.pool_spec.strides.clone().unwrap_or_else(|| tvec!(1; kernel_spatial_shape.len())),
1159 );
1160 let n_output_points: TDim =
1161 output_dims.iter().map(|d| d.convoluted.clone()).product::<TDim>();
1162 let n_output_channels = self.output_channels().to_dim();
1163 let kernel_surface = kernel_spatial_shape.iter().product::<usize>().to_dim();
1164 let one = 1.to_dim();
1165 Ok(tvec!((
1166 Cost::FMA(inputs[0].datum_type),
1167 shape.n().cloned().unwrap_or(one)
1168 * shape.c()
1169 * n_output_channels
1170 * n_output_points
1171 * kernel_surface
1172 / self.group
1173 )))
1174 }
1175
1176 fn change_axes(
1177 &self,
1178 model: &TypedModel,
1179 node: &TypedNode,
1180 io: InOut,
1181 change: &AxisOp,
1182 ) -> TractResult<Option<AxisChangeConsequence>> {
1183 rule_if!(io != InOut::In(1));
1184 if io == InOut::In(2)
1185 && let &AxisOp::Rm(_) = change
1186 {
1187 return Ok(Some(AxisChangeConsequence {
1188 substitute_op: Some(Box::new(self.clone())),
1189 wire_changes: tvec!(),
1190 }));
1191 }
1192 let full_input_shape = model.outlet_fact(node.inputs[0])?.shape.to_tvec();
1193 let shape = self.pool_spec.data_format.shape(full_input_shape.clone())?;
1194 if let Some(n) = shape.n_axis() {
1196 assert_eq!(n, 0);
1197 if change == &AxisOp::Rm(n) {
1198 let op = Conv { pool_spec: self.pool_spec.dispose_n_axis(), ..self.clone() };
1199 return Ok(Some(AxisChangeConsequence {
1200 substitute_op: Some(Box::new(op)),
1201 wire_changes: tvec!(
1202 (InOut::In(0), change.clone()),
1203 (InOut::Out(0), change.clone())
1204 ),
1205 }));
1206 }
1207 rule_if!(change.transform_axis(n).map(|axis| axis == 0).unwrap_or(false));
1208 }
1209 let (new_format, axis_move) = match self.pool_spec.data_format {
1211 DataFormat::NCHW => {
1212 (DataFormat::NHWC, AxisOp::Move(shape.c_axis(), full_input_shape.len() - 1))
1213 }
1214 DataFormat::CHW => {
1215 (DataFormat::HWC, AxisOp::Move(shape.c_axis(), full_input_shape.len() - 1))
1216 }
1217 DataFormat::NHWC => (DataFormat::NCHW, AxisOp::Move(shape.c_axis(), 1)),
1218 DataFormat::HWC => (DataFormat::CHW, AxisOp::Move(shape.c_axis(), 0)),
1219 };
1220 if *change == axis_move {
1221 let mut new_op = self.clone();
1222 new_op.pool_spec.data_format = new_format;
1223 return Ok(Some(AxisChangeConsequence {
1224 substitute_op: Some(Box::new(new_op)),
1225 wire_changes: tvec!(
1226 (InOut::In(0), change.clone()),
1227 (InOut::Out(0), change.clone())
1228 ),
1229 }));
1230 }
1231 rule_if!(!model.node_input_facts(node.id)?[1].is_exotic());
1233 use AxisOp::*;
1234 let h_axis = shape.h_axis();
1235 let hw_axes = shape.hw_axes();
1236 let kh_axis = self.kernel_fmt.h_axis();
1237 let (geo_adjusted, kernel_adjusted) = match change {
1238 Rm(a)
1239 if hw_axes.contains(a)
1240 && hw_axes.len() > 1
1241 && self.pool_spec.dilation(a - h_axis) == 1
1242 && self.pool_spec.stride(a - h_axis) == 1
1243 && self.pool_spec.kernel_shape[a - h_axis] == 1 =>
1244 {
1245 let geo_axis = a - h_axis;
1246 (Rm(geo_axis), Rm(kh_axis + geo_axis))
1247 }
1248 Add(a) if hw_axes.contains(a) => (Add(a - h_axis), Add(a - h_axis + kh_axis)),
1249 Move(f, t) if hw_axes.contains(f) && hw_axes.contains(t) => {
1250 (Move(f - h_axis, t - h_axis), Move(f - h_axis + kh_axis, t - h_axis + kh_axis))
1251 }
1252 _ => return Ok(None),
1253 };
1254 let pool_spec = self.pool_spec.change_geo_axes(&geo_adjusted)?;
1255 let new_op = Conv { pool_spec, ..self.clone() };
1256 Ok(Some(AxisChangeConsequence {
1257 substitute_op: Some(Box::new(new_op)),
1258 wire_changes: tvec!(
1259 (InOut::In(0), change.clone()),
1260 (InOut::In(1), kernel_adjusted),
1261 (InOut::Out(0), change.clone())
1262 ),
1263 }))
1264 }
1265
1266 fn codegen(
1267 &self,
1268 model: &TypedModel,
1269 node: &TypedNode,
1270 ) -> TractResult<Option<TypedModelPatch>> {
1271 let input_fact = model.outlet_fact(node.inputs[0])?;
1272 unsafe {
1273 if self.q_params.is_some() {
1274 let mut patch = TypedModelPatch::new("quantized-codegen");
1275 let inputs = patch.taps(model, &node.inputs)?;
1276 let wire = self
1277 .wire_as_quant_im2col(&mut patch, &node.name, &inputs)
1278 .context("in wire_as_quant_im2col")?;
1279 patch.shunt_outside(model, node.id.into(), wire[0])?;
1280 patch.obliterate(node.id)?;
1281 Ok(Some(patch))
1282 } else if let Some(op) = self.try_blocked_conv(input_fact) {
1283 let mut patch = TypedModelPatch::new("blocked-conv");
1286 let inputs = patch.taps(model, &node.inputs)?;
1287 let wire = self
1288 .wire_as_blocked_conv(&mut patch, &node.name, &inputs, op)
1289 .context("wire_as_blocked_conv")?;
1290 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1291 patch.obliterate(node.id)?;
1292 Ok(Some(patch))
1293 } else if input_fact
1294 .shape
1295 .as_concrete()
1296 .map(|s| should_use_lazy(&self.pool_spec, self.group, s, input_fact.datum_type))
1297 .unwrap_or(false)
1298 {
1299 let mut patch = TypedModelPatch::new("lazy-im2col");
1300 let inputs = patch.taps(model, &node.inputs)?;
1301 let wire = self
1302 .wire_as_lazy_im2col(&mut patch, &node.name, &inputs)
1303 .context("wire_as_lazy_im2col")?[0];
1304 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1305 patch.obliterate(node.id)?;
1306 Ok(Some(patch))
1307 } else if self.group != 1
1308 && self.group == self.output_channels()
1309 && self.group == self.input_channels()
1310 && input_fact.shape.as_concrete().is_some()
1311 {
1312 let mut patch = TypedModelPatch::new("depth_wise");
1313 let inputs = patch.taps(model, &node.inputs)?;
1314 let wire = self
1315 .wire_as_depth_wise(&mut patch, &node.name, &inputs)
1316 .context("wire_as_depth_wise")?;
1317 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1318 patch.obliterate(node.id)?;
1319 Ok(Some(patch))
1320 } else {
1321 let mut patch = TypedModelPatch::new("im2col");
1322 let inputs = patch.taps(model, &node.inputs)?;
1323 let wire = self
1324 .wire_as_im2col_pair(&mut patch, &node.name, &inputs)
1325 .context("in wire_as_im2col_pair")?[0];
1326 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1327 patch.obliterate(node.id)?;
1328 Ok(Some(patch))
1329 }
1330 }
1331 }
1332
1333 as_op!();
1334}
1335
1336const DEFAULT_LAZY_IM2COL_MIN_KERNEL: usize = 6;
1345
1346crate::declare_knob!(
1347 TRACT_ENABLE_BLOCKED_CONV,
1348 bool,
1349 false,
1350 "Force-enable the direct blocked convolution on native targets (on by default on wasm)."
1351);
1352crate::declare_knob!(
1353 TRACT_DISABLE_BLOCKED_CONV,
1354 bool,
1355 false,
1356 "Force-disable the direct blocked convolution on wasm targets (off by default on native)."
1357);
1358crate::declare_knob!(
1359 TRACT_LAZY_IM2COL_MIN_KERNEL,
1360 usize,
1361 DEFAULT_LAZY_IM2COL_MIN_KERNEL,
1362 "Minimum convolution kernel volume before lazy im2col is preferred over eager."
1363);
1364crate::declare_knob!(
1365 TRACT_LAZY_IM2COL_MAX_EAGER_BYTES,
1366 usize,
1367 DEFAULT_LAZY_IM2COL_MAX_EAGER_BYTES,
1368 "Eager-im2col scratch-size ceiling, in bytes, above which lazy im2col is preferred."
1369);
1370
1371fn lazy_im2col_min_kernel() -> usize {
1372 TRACT_LAZY_IM2COL_MIN_KERNEL.get()
1373}
1374
1375#[cfg(target_family = "wasm")]
1393const DEFAULT_LAZY_IM2COL_MAX_EAGER_BYTES: usize = 1024 * 1024;
1394#[cfg(not(target_family = "wasm"))]
1395const DEFAULT_LAZY_IM2COL_MAX_EAGER_BYTES: usize = 4 * 1024 * 1024;
1396
1397fn lazy_im2col_max_eager_bytes() -> usize {
1398 TRACT_LAZY_IM2COL_MAX_EAGER_BYTES.get()
1399}
1400
1401fn should_use_lazy(
1402 pool_spec: &PoolSpec,
1403 group: usize,
1404 input_shape: &[usize],
1405 dt: DatumType,
1406) -> bool {
1407 let is_depthwise =
1412 group > 1 && group == pool_spec.input_channels && group == pool_spec.output_channels;
1413 if is_depthwise {
1414 return false;
1415 }
1416 let Ok(output_shape) = pool_spec.output_shape(input_shape) else { return false };
1417 if output_shape.n().unwrap_or(&1) != &1 {
1419 return false;
1420 }
1421 let kernel_volume = pool_spec.kernel_shape.iter().product::<usize>();
1422 if kernel_volume >= lazy_im2col_min_kernel() {
1425 return true;
1426 }
1427 let n: usize = output_shape.hw_dims().iter().product();
1431 let k = pool_spec.input_channels * kernel_volume / group;
1432 let eager_scratch_bytes = k.saturating_mul(n).saturating_mul(dt.size_of());
1433 if eager_scratch_bytes >= lazy_im2col_max_eager_bytes() {
1434 return true;
1435 }
1436 if kernel_volume == 1 {
1439 return false;
1440 }
1441 let m = (pool_spec.output_channels / group).max(1);
1448 let mr = tract_linalg::MmmDispatch::native()
1449 .preferred_kernel(dt, Some(m), Some(k), Some(n))
1450 .map(|mmm| mmm.mr())
1451 .unwrap_or(1);
1452 m <= mr
1453}
1454
1455#[allow(non_snake_case)]
1456#[cfg(test)]
1457mod test {
1458 use super::*;
1459 use crate::ops::array::Pad;
1460 use DataFormat::*;
1461
1462 #[test]
1463 fn onnx_basic_convinteger() {
1464 let op = Conv {
1465 pool_spec: PoolSpec {
1466 data_format: NCHW,
1467 kernel_shape: tvec!(2, 2),
1468 padding: Valid,
1469 dilations: None,
1470 strides: None,
1471 input_channels: 1,
1472 output_channels: 1,
1473 },
1474 kernel_fmt: KernelFormat::OIHW,
1475 group: 1,
1476 q_params: Some(i32::datum_type()),
1477 };
1478 let input = tvec!(
1479 rctensor4(&[[[[1u8, 2, 3], [4, 5, 6], [7, 8, 9]]]]),
1480 rctensor4(&[[[[1u8, 1], [1, 1]]]]),
1481 rctensor0(0u32),
1482 rctensor0(1u8),
1483 rctensor0(1.0f32),
1484 rctensor0(0u8),
1485 rctensor0(1.0f32),
1486 rctensor0(0i32),
1487 rctensor0(1.0f32),
1488 );
1489 let input = input.into_iter().map(IntoTValue::into_tvalue).collect::<TVec<_>>();
1490 let output = op.eval(&EvalContext::out_of_plan(), input).unwrap();
1491 assert_eq!(*output[0], tensor4(&[[[[8i32, 12], [20, 24]]]]));
1492 }
1493
1494 #[test]
1495 fn valid_conv_absorbs_precursor_pad() -> TractResult<()> {
1496 let mut model = TypedModel::default();
1497 let wire = tvec!(model.add_source("source", f32::fact(dims!(1, 10)))?);
1498 let wire = model.wire_node(
1499 "pad",
1500 Pad {
1501 pads: vec![(0, 0), (1, 0)],
1502 mode: ops::array::PadMode::Constant(rctensor0(0f32)),
1503 },
1504 &wire,
1505 )?;
1506 let kernel = model.add_const("kernel", rctensor3(&[[[1f32, 2f32]]]))?;
1507 let bias = model.add_const("bias", rctensor0(0f32))?;
1508 let wire = model.wire_node(
1509 "conv",
1510 Conv {
1511 pool_spec: PoolSpec {
1512 data_format: crate::ops::nn::DataFormat::CHW,
1513 dilations: None,
1514 strides: None,
1515 kernel_shape: tvec![2],
1516 padding: Explicit(tvec![0], tvec![0]),
1517 input_channels: 1,
1518 output_channels: 1,
1519 },
1520 kernel_fmt: crate::ops::cnn::KernelFormat::OIHW,
1521 group: 1,
1522 q_params: None,
1523 },
1524 &[wire[0], kernel, bias],
1525 )?;
1526 model.select_output_outlets(&wire)?;
1527 model.declutter()?;
1528 assert_eq!(model.nodes().len(), 4); let cv = model.nodes()[3].op_as::<Conv>().unwrap();
1530 assert_eq!(cv.pool_spec.padding, Explicit(tvec![1], tvec![0])); Ok(())
1532 }
1533}