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