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 let k0_fact = model.outlet_fact(k0)?;
194 if k0_fact.rank() == 1 && !k0_fact.shape.volume().is_one() {
195 k0 = model.wire_node(
196 format!("{name}.k0_group_channels"),
197 AxisOp::Reshape(
198 0,
199 k0_fact.shape.to_tvec(),
200 tvec!(self.group.to_dim(), (self.output_channels() / self.group).to_dim(),),
201 ),
202 &[k0],
203 )?[0];
204 if !output_shape.fmt.c_is_last() {
205 k0 = model.wire_node(format!("{name}.k0_spatial_axis"), AxisOp::Add(2), &[k0])?[0];
206 }
207 }
208
209 if !model.outlet_fact(k_scale)?.shape.volume().is_one() {
210 if !output_shape.fmt.c_is_last() {
213 k_scale = model.wire_node(
214 format!("{name}.a_scale_axis_fix"),
215 AxisOp::Add(1),
216 &[k_scale],
217 )?[0];
218 }
219 }
220
221 let abc_scale = qmm::combine_scales(model, name, k_scale, x_scale, y_scale)?;
222
223 let im2col = model.wire_node(
224 format!("{name}.im2col"),
225 Im2Col::new(
226 self.pool_spec.clone(),
227 self.group,
228 k,
229 &b_fact.shape,
230 mmm.clone(),
231 packing,
232 )?,
233 &[x, x0],
234 )?[0];
235
236 let g_o_ihw = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
237 let g_o_ihw_as_i32 =
238 model.wire_node(format!("{name}.kernel_as_i32"), cast(i32::datum_type()), &g_o_ihw)?;
239 let sum_ker_g_c_k = model.wire_node(
240 format!("{name}.sum_ker_g_c_k"),
241 Reduce::new(tvec!(2), ops::nn::Reducer::Sum),
242 &g_o_ihw_as_i32,
243 )?;
244 let sum_ker_a_g_c =
245 model.wire_node(format!("{name}.rm_k"), AxisOp::Rm(2), &sum_ker_g_c_k)?;
246 let sum_ker_n_g_c = model.wire_node(
248 format!("{name}.sum_ker_n_g_c.axis_0"),
249 AxisOp::Add(0),
250 &sum_ker_a_g_c,
251 )?;
252 let hw_position = if self.pool_spec.data_format.c_is_last() { 1 } else { 3 };
253 let sum_ker = model.wire_node(
254 format!("{name}.sum_ker_n_g_c"),
255 AxisOp::Add(hw_position),
256 &sum_ker_n_g_c,
257 )?;
258
259 ensure!(
260 mmm.packings()[packing].1.downcast_ref::<PackedFormat>().is_some()
261 || mmm.packings()[packing].1.downcast_ref::<PackedI8K4>().is_some(),
262 "Im2Col/QSumB support PackedFormat or PackedI8K4 activation packings"
263 );
264 let mut sum_x = model.wire_node(
265 format!("{name}.sum_x"),
266 super::QSumB { dt: b_fact.datum_type, n, r: mmm.nr(), k },
267 &[im2col],
268 )?;
269 sum_x = model.wire_node(format!("{name}.add_c"), AxisOp::Add(2), &sum_x)?;
271 if self.pool_spec.data_format.c_is_last() {
272 sum_x =
273 model.wire_node(format!("{name}.transpose_sum_b"), AxisOp::Move(3, 1), &sum_x)?;
274 }
275
276 let (mmm_output_shape, c_axis, h_axis) = self.mmm_output_shape(&output_shape)?;
277 let bias_name = &model.node(bias.node).name;
278 let bias =
279 model.wire_node(format!("{bias_name}.cast"), cast(mmm.internal_type()), &[bias])?[0];
280 let wire = self.wire_mm_weights_bias(
281 model,
282 name,
283 im2col,
284 g_o_ihw[0],
285 bias,
286 mmm,
287 packing,
288 i32::datum_type(),
289 mmm_output_shape.clone().into(),
290 k,
291 c_axis,
292 h_axis,
293 )?;
294
295 let wire = qmm::compensate_zero_points(
296 model,
297 name,
298 wire[0],
299 k.to_dim(),
300 k0,
301 x0,
302 sum_ker[0],
303 sum_x[0],
304 )?;
305
306 let wire = self.wire_remove_group(model, name, &[wire], &mmm_output_shape, c_axis)?;
307 let wire = self.wire_rm_n_if_needed(model, name, &wire)?;
308 let wire = qmm::requant(model, name, wire[0], c_dt, abc_scale, y0)?;
309 Self::wire_geo_reshape(model, name, &[wire], &output_shape)
310 }
311
312 pub fn wire_remove_group<D: DimLike>(
313 &self,
314 model: &mut TypedModel,
315 name: &str,
316 wire: &[OutletId],
317 mmm_output_shape: &[D],
318 c_axis: usize,
319 ) -> TractResult<TVec<OutletId>> {
320 let m = &mmm_output_shape[c_axis];
321 let op = if self.group == 1 {
322 AxisOp::Rm(c_axis - 1)
323 } else {
324 AxisOp::Reshape(
325 c_axis - 1,
326 tvec!(self.group.to_dim(), m.to_dim()),
327 tvec!(m.to_dim() * self.group),
328 )
329 };
330 model.wire_node(format!("{name}.reshape_group"), op, wire)
331 }
332
333 pub unsafe fn wire_as_im2col_pair(
334 &self,
335 model: &mut TypedModel,
336 name: &str,
337 wire: &[OutletId],
338 ) -> TractResult<TVec<OutletId>> {
339 let &[x, w, bias] = wire else { bail!("Wrong number of inputs") };
340 let x_fact = model.outlet_fact(x)?.clone();
341 let w_fact = model.outlet_fact(w)?.clone();
342 let c_dt = crate::ops::matmul::output_type(x_fact.datum_type);
343
344 let (_, m, k, n) = self.compute_geo(&x_fact)?;
345 let (mmm, packing) = self.choose_impl(&x_fact, &w_fact, m, k, &n)?;
346 let geo_output_shape = self.pool_spec.output_shape(&x_fact.shape)?;
347 let (mmm_output_shape, c_axis, h_axis) = self.mmm_output_shape(&geo_output_shape)?;
348
349 let padding =
350 model.add_const(format!("{name}.b0"), Tensor::zero_scalar_dt(x_fact.datum_type)?)?;
351
352 let mut wire: TVec<_> = wire.into();
353 wire[0] = model.wire_node(
354 format!("{name}.im2col"),
355 Im2Col::new(
356 self.pool_spec.clone(),
357 self.group,
358 k,
359 &x_fact.shape,
360 mmm.clone(),
361 packing,
362 )?,
363 &[wire[0], padding],
364 )?[0];
365
366 let g_o_ihw = self.wire_kernel_as_g_o_ihw(model, name, wire[1])?;
367
368 let wire = self
369 .wire_mm_weights_bias(
370 model,
371 name,
372 wire[0],
373 g_o_ihw[0],
374 bias,
375 mmm,
376 packing,
377 c_dt,
378 mmm_output_shape.clone().into(),
379 k.to_usize().unwrap(),
380 c_axis,
381 h_axis,
382 )
383 .context("in wire_opt_matmul")?;
384
385 let wire = self.wire_remove_group(model, name, &wire, &mmm_output_shape, c_axis)?;
386 let wire = self.wire_rm_n_if_needed(model, name, &wire)?;
387 Self::wire_geo_reshape(model, name, &wire, &geo_output_shape)
388 }
389
390 fn mmm_output_shape<D: DimLike>(
392 &self,
393 output_shape: &BaseDataShape<D, TVec<D>>,
394 ) -> TractResult<(TVec<D>, usize, usize)> {
395 let geo_collapsed_out: D = output_shape.hw_dims().iter().cloned().product();
396 let shape: BaseDataShape<D, TVec<D>> = output_shape.fmt.with_n().from_n_c_hw(
397 output_shape.n().cloned().unwrap_or_else(|| 1.into()),
398 output_shape.c().clone(),
399 tvec!(geo_collapsed_out),
400 )?;
401 let mut mmm_output_shape: TVec<D> = shape.shape.clone();
402 let mut c_axis = shape.c_axis();
403 let mut h_axis = shape.h_axis();
404 mmm_output_shape[shape.c_axis()] = mmm_output_shape[c_axis].clone() / self.group;
405 mmm_output_shape.insert(c_axis, self.group.into());
406 if h_axis > c_axis {
407 h_axis += 1;
408 }
409 c_axis += 1;
410 Ok((mmm_output_shape, c_axis, h_axis))
411 }
412
413 fn wire_rm_n_if_needed(
414 &self,
415 model: &mut TypedModel,
416 name: &str,
417 wire: &[OutletId],
418 ) -> TractResult<TVec<OutletId>> {
419 if self.pool_spec.data_format.has_n() {
420 Ok(wire.into())
421 } else {
422 model.wire_node(format!("{name}.rm_n"), AxisOp::Rm(0), wire)
423 }
424 }
425
426 fn wire_geo_reshape<D: DimLike>(
427 model: &mut TypedModel,
428 name: &str,
429 wire: &[OutletId],
430 output_shape: &BaseDataShape<D, TVec<D>>,
431 ) -> TractResult<TVec<OutletId>> {
432 let geo_collapsed_out: D = output_shape.hw_dims().iter().cloned().product();
433 model
434 .wire_node(
435 name,
436 AxisOp::Reshape(
437 output_shape.h_axis(),
438 tvec!(geo_collapsed_out.to_dim()),
439 output_shape.hw_dims().iter().map(|d| d.to_dim()).collect(),
440 ),
441 wire,
442 )
443 .context("in wire_geo_reshape")
444 }
445
446 pub unsafe fn wire_as_lazy_im2col(
447 &self,
448 model: &mut TypedModel,
449 name: &str,
450 wire: &[OutletId],
451 ) -> TractResult<TVec<OutletId>> {
452 let &[mut x, kernel, bias] = wire else { bail!("Wrong number of inputs") };
453 let mut x_fact = model.outlet_fact(x)?.clone();
454 let w_fact = model.outlet_fact(kernel)?.clone();
455 let (geo, m, k, n) = self.compute_geo(&x_fact)?;
456 let (mmm, packing) = self.choose_impl(&x_fact, &w_fact, m, k, &n)?;
457 debug!("{name} as lazy_im2col: m={m} k={k} n={n} {mmm:?}");
458 let input_shape = x_fact.shape.as_concrete().unwrap().to_vec();
459 let mut geo = geo.to_concrete(&input_shape)?.into_owned();
460 let mut input_shape: DataShape = self.pool_spec.data_format.shape(input_shape.into())?;
461 let padding = self.pool_spec.computed_padding(input_shape.hw_dims());
462 if padding.iter().any(|axis| axis.pad_before != 0 || axis.pad_after != 0) {
463 let mut pads = vec![(0, 0); x_fact.rank()];
464 for (ix, ax) in padding.iter().enumerate() {
465 pads[input_shape.h_axis() + ix] = (ax.pad_before, ax.pad_after);
466 }
467 let op = crate::ops::array::Pad {
468 mode: crate::ops::array::PadMode::Constant(
469 Tensor::zero_scalar_dt(x_fact.datum_type)?.into_arc_tensor(),
470 ),
471 pads,
472 };
473 x = model.wire_node(format!("{name}.pad"), op, &[x])?[0];
474 let valid_pool_spec = PoolSpec { padding: Valid, ..self.pool_spec.clone() };
475 x_fact = model.outlet_fact(x)?.clone();
476 let concrete_shape = x_fact.shape.as_concrete().unwrap();
477 input_shape = valid_pool_spec.data_format.shape(concrete_shape.into())?;
478 geo = valid_pool_spec
479 .compute_geo(&x_fact.shape)?
480 .to_concrete(concrete_shape)?
481 .into_owned();
482 }
483 let c_dt = crate::ops::matmul::output_type(x_fact.datum_type);
484 let c_stride = input_shape.c_stride();
485 let size_of_b = x_fact.datum_type.size_of() as isize;
486 let n_byte_offsets: Vec<isize> =
487 geo.patch.centers_offsets().into_iter().map(|x| x * size_of_b).collect();
488 let ci_per_group = self.input_channels() / self.group;
491 let k_byte_offsets: Vec<isize> = (0..ci_per_group)
492 .flat_map(|ici| {
493 geo.patch
494 .standard_layout_data_field
495 .iter()
496 .map(move |x| (x + (ici * c_stride) as isize) * size_of_b)
497 })
498 .collect();
499 let group_stride_bytes = (ci_per_group * c_stride) as isize * size_of_b;
500 let (mmm_output_shape, c_axis, h_axis) = self.mmm_output_shape(&geo.output_shape)?;
501 let packer = mmm.packings()[packing]
502 .1
503 .downcast_ref::<PackedFormat>()
504 .with_context(|| {
505 format_err!(
506 "Quand Im2Col expects regular packed format, got {:?}",
507 mmm.packings()[packing].1
508 )
509 })?
510 .clone();
511 let params = LazyIm2colParams { packer, n_byte_offsets, k_byte_offsets };
512 let x = model.wire_node(
513 format!("{name}.lazyIm2col"),
514 LazyIm2Col { params: Arc::new(params), group: self.group, group_stride_bytes },
515 &[x],
516 )?[0];
517
518 let kernel = self.wire_kernel_as_g_o_ihw(model, name, kernel)?[0];
519 let wire = self.wire_mm_weights_bias(
520 model,
521 name,
522 x,
523 kernel,
524 bias,
525 mmm,
526 packing,
527 c_dt,
528 mmm_output_shape.clone().into(),
529 k,
530 c_axis,
531 h_axis,
532 )?;
533
534 let wire = self.wire_remove_group(model, name, &wire, &mmm_output_shape, c_axis)?;
535 let wire = self.wire_rm_n_if_needed(model, name, &wire)?;
536 Self::wire_geo_reshape(model, name, &wire, &geo.output_shape)
537 }
538
539 #[allow(clippy::type_complexity)]
540 fn compute_geo(
541 &self,
542 input_fact: &TypedFact,
543 ) -> TractResult<(PoolGeometry, usize, usize, TDim)> {
544 let geo = self.pool_spec.compute_geo(&input_fact.shape)?;
545
546 trace!("output channels: {:?}", self.output_channels());
547 let m = self.output_channels() / self.group;
548 let k = self.input_channels() * self.pool_spec.kernel_shape.iter().product::<usize>()
549 / self.group;
550 let n: TDim =
551 self.pool_spec.output_shape(&input_fact.shape)?.hw_dims().iter().cloned().product();
552 Ok((geo, m, k, n))
553 }
554
555 fn choose_impl(
556 &self,
557 input_fact: &TypedFact,
558 weight_fact: &TypedFact,
559 m: usize,
560 k: usize,
561 n: &TDim,
562 ) -> TractResult<(Box<dyn MatMatMul>, usize)> {
563 let w_dt = weight_fact.datum_type;
564 let x_dt = input_fact.datum_type;
565
566 let acc = if x_dt.is_float() { x_dt } else { i32::datum_type() };
567 let query = Query {
570 weight: if weight_fact.is_exotic() {
571 let bqf = weight_fact
572 .exotic_fact
573 .as_ref()
574 .and_then(|of| of.downcast_ref::<BlockQuantFact>())
575 .unwrap();
576 WeightType::BlockQuant(bqf.format.clone())
577 } else {
578 w_dt.into()
579 },
580 activation: x_dt,
581 accumulators: tvec!(acc),
582 store: None,
583 allow_extractor: false,
584 m: Some(m),
585 k: Some(k),
586 n: n.as_usize(),
587 };
588 if weight_fact.is_exotic() {
589 let mut suitable = tract_linalg::MmmDispatch::native().suitable(&query);
590 retain_best(&mut suitable);
591 suitable
592 .into_iter()
593 .map(|(mmm, p, _)| (mmm, p))
594 .max_by_key(|(mmm, _)| mmm.mr() * mmm.nr())
595 .context("No matmul found")
596 } else {
597 let (mmm, packing, _) =
598 tract_linalg::MmmDispatch::native().pick(&query).context("No matmul found")?;
599 Ok((mmm, packing))
600 }
601 }
602
603 #[allow(clippy::too_many_arguments)]
604 fn wire_mm_weights_bias(
605 &self,
606 model: &mut TypedModel,
607 name: &str,
608 input: OutletId,
609 g_o_ihw: OutletId,
610 bias: OutletId,
611 mmm: Box<dyn MatMatMul>,
612 packing: usize,
613 c_datum_type: DatumType,
614 mmm_output_shape: ShapeFact,
615 k: usize,
616 c_m_axis: usize,
617 c_n_axis: usize,
618 ) -> TractResult<TVec<OutletId>> {
619 ensure!(model.outlet_fact(bias)?.datum_type == mmm.internal_type());
620 let a_pack = &mmm.packings()[packing].0;
621 let packed_ker = self
622 .wire_pack_g_o_ihw(model, name, &**a_pack, g_o_ihw)
623 .context("in kernel_as_packed_as")?;
624 let (mut c_to_a_axis_mapping, mut c_to_b_axis_mapping) = (tvec!(), tvec!());
625
626 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 {
631 k: k.to_dim(),
632 c_to_a_axis_mapping: MapOutputAxisToInput(c_to_a_axis_mapping),
633 c_to_b_axis_mapping: MapOutputAxisToInput(c_to_b_axis_mapping),
634 };
635 let mut ops: Vec<ProtoFusedSpec> = vec![ProtoFusedSpec::AddMatMul {
636 geo,
637 a: MatMulOperand::Input(1),
638 b: MatMulOperand::Input(0),
639 packings: vec![(packing, None)],
640 }];
641 let mut wires: TVec<OutletId> = tvec!(input, packed_ker);
642 let bias_fact = model.outlet_fact(bias)?;
643 if bias_fact.konst.is_none() || !bias_fact.konst.as_ref().unwrap().is_all_zero()? {
644 let (fused, bias) = self.wire_bias_as_non_linear(model, name, bias, c_m_axis - 1)?;
645 wires.push(bias);
646 ops.push(fused);
647 }
648 ops.push(ProtoFusedSpec::Store(vec![unsafe {
649 mmm.c_view(Some(c_m_axis), Some(c_n_axis))
650 }]));
651 model.wire_node(
652 format!("{name}.matmatmul"),
653 OptMatMul::new(
654 vec![mmm],
655 ModePicker::Single,
656 c_datum_type.fact(mmm_output_shape),
657 Some(c_m_axis),
658 Some(c_n_axis),
659 ops,
660 packing == 0 && self.group == 1,
661 )?,
662 &wires,
663 )
664 }
665
666 pub fn wire_as_depth_wise(
667 &self,
668 model: &mut TypedModel,
669 name: &str,
670 wire: &[OutletId],
671 ) -> TractResult<OutletId> {
672 let &[x, kernel, mut bias] = wire else { bail!("Wrong number of inputs") };
673 let x_fact = model.outlet_fact(x)?.clone();
674 let x_shape = x_fact.shape.as_concrete().unwrap();
675 let ConcretePoolGeometry { input_shape, patch, output_shape } =
676 self.pool_spec.compute_geo(&x_fact.shape)?.to_concrete(x_shape)?.into_owned();
677 let kernel = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
678 let kernel = if self.pool_spec.data_format.c_is_last()
680 && !model.outlet_fact(kernel[0])?.is_exotic()
681 {
682 model.wire_node(format!("{name}.kernel_c_inner"), AxisOp::Move(0, 2), &kernel)?
683 } else {
684 kernel
685 };
686 let c_axis = self.pool_spec.data_format.shape(x_shape)?.c_axis();
687 bias = wire_reshape_bias_for_bin(
688 model,
689 name,
690 bias,
691 x_fact.rank(),
692 c_axis,
693 self.output_channels(),
694 )?[0];
695 let op = DepthWise::new(patch, input_shape, output_shape);
696 Ok(model.wire_node(name, op, &[x, kernel[0], bias])?[0])
697 }
698
699 fn can_direct_spatial(&self, input_fact: &TypedFact) -> bool {
704 if self.q_params.is_some()
705 || input_fact.datum_type != f32::datum_type()
706 || self.pool_spec.data_format != crate::ops::nn::DataFormat::NCHW
707 || self.group != 1
708 || self.pool_spec.rank() != 2
709 || TRACT_DISABLE_DIRECT_SPATIAL.get()
710 {
711 return false;
712 }
713 let kvol = self.pool_spec.kernel_shape.iter().product::<usize>();
714 let k_limit = if amx_owns_fat_spatial() { 64 } else { 576 };
715 if kvol < 2 || self.input_channels() * kvol > k_limit {
716 return false;
717 }
718 if tract_linalg::routines::conv_w4_f32().is_none()
719 || (!self.output_channels().is_multiple_of(4)
720 && tract_linalg::routines::depthwise_w_f32().is_none())
721 {
722 return false;
723 }
724 let Some(shape) = input_fact.shape.as_concrete() else {
725 return false;
726 };
727 let w_in =
730 self.pool_spec.data_format.shape(shape).ok().and_then(|s| s.hw_dims().last().copied());
731 if w_in.is_some_and(|w| w < MIN_DIRECT_SPATIAL_W) {
732 return false;
733 }
734 let Ok(output) = self.pool_spec.output_shape(shape) else {
735 return false;
736 };
737 output.hw_dims().last().is_some_and(|w| *w >= MIN_DIRECT_SPATIAL_W)
738 }
739
740 fn wire_as_direct_spatial(
741 &self,
742 model: &mut TypedModel,
743 name: &str,
744 wire: &[OutletId],
745 ) -> TractResult<OutletId> {
746 let &[x, kernel, mut bias] = wire else { bail!("Wrong number of inputs") };
747 let x_fact = model.outlet_fact(x)?.clone();
748 let x_shape = x_fact.shape.as_concrete().unwrap();
749 let ConcretePoolGeometry { input_shape, patch, output_shape } =
750 self.pool_spec.compute_geo(&x_fact.shape)?.to_concrete(x_shape)?.into_owned();
751 let kernel = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
752 let c_axis = self.pool_spec.data_format.shape(x_shape)?.c_axis();
753 bias = wire_reshape_bias_for_bin(
754 model,
755 name,
756 bias,
757 x_fact.rank(),
758 c_axis,
759 self.output_channels(),
760 )?[0];
761 let op = super::DirectSpatialConv::new(patch, input_shape, output_shape);
762 Ok(model.wire_node(name, op, &[x, kernel[0], bias])?[0])
763 }
764
765 fn try_blocked_conv(&self, input_fact: &TypedFact) -> Option<super::BlockedConv> {
771 let enabled = if cfg!(target_family = "wasm") {
776 !TRACT_DISABLE_BLOCKED_CONV.get()
777 } else {
778 TRACT_ENABLE_BLOCKED_CONV.get()
779 };
780 if !enabled {
781 return None;
782 }
783 if self.q_params.is_some() {
784 return None;
785 }
786 if input_fact.datum_type != f32::datum_type() {
787 return None;
788 }
789 if self.pool_spec.data_format != crate::ops::nn::DataFormat::NCHW {
790 return None;
791 }
792 if self.pool_spec.rank() != 2 || self.pool_spec.kernel_shape[1] != 1 {
793 return None;
794 }
795 if self.pool_spec.stride(1) != 1 || self.pool_spec.dilation(1) != 1 {
796 return None;
797 }
798 let group = self.group;
799 let oc = self.output_channels();
800 let c_in = self.input_channels();
801 if group == 0 || !oc.is_multiple_of(group) || !c_in.is_multiple_of(group) {
802 return None;
803 }
804 let ocg = oc / group;
805 if ocg == 0 || ocg > 8 {
808 return None;
809 }
810 let concrete = input_fact.shape.as_concrete()?;
811 let shape = self.pool_spec.data_format.shape(concrete).ok()?;
812 let h_axis = shape.h_axis();
813 let h_in = concrete[h_axis];
814 let w = concrete[h_axis + 1];
815 let pads = self.pool_spec.computed_padding(shape.hw_dims());
816 Some(super::BlockedConv {
817 n: *shape.n().unwrap_or(&1),
818 c_in,
819 h_in,
820 w,
821 oc,
822 group,
823 kh: self.pool_spec.kernel_shape[0],
824 stride_h: self.pool_spec.stride(0),
825 dil_h: self.pool_spec.dilation(0),
826 pad_before_h: pads[0].pad_before,
827 h_out: pads[0].convoluted,
828 })
829 }
830
831 fn wire_as_blocked_conv(
832 &self,
833 model: &mut TypedModel,
834 name: &str,
835 wire: &[OutletId],
836 op: super::BlockedConv,
837 ) -> TractResult<OutletId> {
838 let &[x, kernel, bias] = wire else { bail!("Wrong number of inputs") };
839 let g_o_ihw = self.wire_kernel_as_g_o_ihw(model, name, kernel)?;
842 Ok(model.wire_node(name, op, &[x, g_o_ihw[0], bias])?[0])
843 }
844
845 fn declutter_stride_slice_to_downsample(
846 &self,
847 model: &TypedModel,
848 node: &TypedNode,
849 ) -> TractResult<Option<TypedModelPatch>> {
850 let spatial_rank = self.pool_spec.rank();
851 if let Some(axis) = (0..spatial_rank).find(|&ax| {
852 self.pool_spec.stride(ax) > 1
853 && self.pool_spec.padding.valid_dim(ax, self.pool_spec.stride(ax) == 1)
854 && (self.pool_spec.kernel_shape[ax] == 1
855 || self.pool_spec.dilation(ax).is_multiple_of(self.pool_spec.stride(ax)))
856 }) {
857 let input_fact = model.outlet_fact(node.inputs[0])?;
858 let downsample_factor = self.pool_spec.stride(axis);
859 let mut new_op = self.clone();
860 if new_op.pool_spec.dilation(axis) > 1 {
861 new_op.pool_spec.dilations.as_mut().unwrap()[axis] =
862 new_op.pool_spec.dilations.as_mut().unwrap()[axis].divceil(downsample_factor);
863 }
864 new_op.pool_spec.strides.as_mut().unwrap()[axis] /= downsample_factor;
865 let mut patch = TypedModelPatch::default();
866 let mut taps = patch.taps(model, &node.inputs)?;
867 let shape = self.pool_spec.data_format.shape(&input_fact.shape)?;
868 taps[0] = patch.wire_node(
869 format!("{}.downsample.{}", node.name, axis),
870 crate::ops::Downsample::new(axis + shape.h_axis(), downsample_factor as isize, 0),
871 &[taps[0]],
872 )?[0];
873 let id = patch.wire_node(&*node.name, new_op, &taps)?[0];
874 patch.shunt_outside(model, OutletId::new(node.id, 0), id)?;
875 return Ok(Some(patch));
876 }
877 Ok(None)
878 }
879
880 fn declutter_as_einsum(
881 &self,
882 model: &TypedModel,
883 node: &TypedNode,
884 ) -> TractResult<Option<TypedModelPatch>> {
885 let (input_facts, output_facts) = model.node_facts(node.id)?;
886 let full_input_shape = input_facts[0].shape.to_tvec();
887 let input_shape = self.pool_spec.data_format.shape(&full_input_shape)?;
888 if self.group == 1
889 && self.pool_spec.strides().iter().all(|s| *s == 1)
890 && self.pool_spec.dilations().iter().all(|d| *d == 1)
891 && self.pool_spec.kernel_shape.iter().product::<usize>() == 1
892 && self
893 .pool_spec
894 .computed_padding(input_shape.hw_dims())
895 .iter()
896 .all(|pad| pad.pad_after.is_zero() && pad.pad_before.is_zero())
897 {
898 let mut axes = self.axes_mapping(&input_facts, &output_facts)?;
899 let mut patch = TypedModelPatch::new("declutter_as_einsum");
900 let mut taps = patch.taps(model, &node.inputs)?;
901 let name = &node.name;
902 let co = self.output_channels();
903 taps[1] =
904 self.wire_kernel_as_g_o_ihw(&mut patch, &format!("{name}.filters"), taps[1])?[0];
905 taps[1] =
906 patch.wire_node(format!("{name}.filters_as_co_ci"), AxisOp::Rm(0), &[taps[1]])?[0];
907
908 while axes.rank(InOut::In(1)) > 0 {
909 axes = axes.remove_axis_occurency(InOut::In(1), 0)?;
910 }
911 axes = axes
912 .with_extra_axis_occurency('O', InOut::In(1), 0)?
913 .with_extra_axis_occurency('I', InOut::In(1), 1)?;
914
915 let bias_fact = input_facts[2];
916 let wire = if self.q_params.is_some() {
917 if bias_fact.rank() == 1 {
918 axes = axes.linking('O', (InOut::In(2), 0))?;
919 }
920 let op = EinSum { axes, operating_dt: i32::datum_type(), q_params: self.q_params };
921 patch.wire_node(format!("{name}.einsum"), op, &taps)?[0]
922 } else {
923 axes = axes.remove_slot(InOut::In(2))?;
924 let op = EinSum { axes, operating_dt: input_facts[0].datum_type, q_params: None };
925 let mut wire = patch.wire_node(format!("{name}.einsum"), op, &taps[0..2])?[0];
926
927 if !bias_fact.konst.as_ref().map(|f| f.is_zero()).transpose()?.unwrap_or(false) {
928 let bias_current_shape =
929 if bias_fact.rank() == 0 { tvec!() } else { tvec!(co.to_dim()) };
930 let mut bias_shape = tvec!(1.to_dim(); input_shape.rank());
931 if bias_fact.rank() > 0 {
932 bias_shape[input_shape.c_axis()] = co.to_dim();
933 }
934 let b = patch.wire_node(
935 format!("{name}.bias.reshape"),
936 AxisOp::Reshape(0, bias_current_shape, bias_shape),
937 &[taps[2]],
938 )?[0];
939 wire = patch.wire_node(
940 format!("{name}.bias"),
941 crate::ops::math::add(),
942 &[wire, b],
943 )?[0];
944 }
945 wire
946 };
947 patch.node_mut(wire.node).name = node.name.to_string();
948 patch.shunt_outside(model, node.id.into(), wire)?;
949 return Ok(Some(patch));
950 }
951 Ok(None)
952 }
953
954 fn declutter_precursor_padding(
955 &self,
956 model: &TypedModel,
957 node: &TypedNode,
958 ) -> TractResult<Option<TypedModelPatch>> {
959 rule_if!(!matches!(
960 self.pool_spec.padding,
961 ExplicitOnnxPool(_, _, _) | SameLower | SameUpper
962 ));
963 let prec = model.node(node.inputs[0].node);
964 rule_if_some!(pad = prec.op_as::<Pad>());
965 rule_if_let!(PadMode::Constant(value) = &pad.mode);
966 let shape = self.pool_spec.data_format.shape(&model.outlet_fact(node.inputs[0])?.shape)?;
967 rule_if!(value.is_zero()?);
968 rule_if!(pad.pads[shape.c_axis()] == (0, 0));
969 if self.pool_spec.data_format.has_n() {
970 rule_if!(pad.pads[0] == (0, 0));
971 }
972 let mut before: TVec<usize> = pad.pads[shape.hw_axes()].iter().map(|pair| pair.0).collect();
973 let mut after: TVec<usize> = pad.pads[shape.hw_axes()].iter().map(|pair| pair.1).collect();
974 if let Explicit(bef, aft) = &self.pool_spec.padding {
975 izip!(&mut before, bef).for_each(|(pad, cv)| *pad += cv);
976 izip!(&mut after, aft).for_each(|(pad, cv)| *pad += cv);
977 }
978 let padding = Explicit(before, after);
979 let mut new = self.clone();
980 new.pool_spec.padding = padding;
981 let mut patch = TypedModelPatch::default();
982 let mut wire = patch.taps(model, &node.inputs)?;
983 wire[0] = patch.tap_model(model, prec.inputs[0])?;
984 let wire = patch.wire_node(&node.name, new, &wire)?;
985 patch.shunt_outside(model, node.id.into(), wire[0])?;
986 Ok(Some(patch))
987 }
988
989 fn declutter_channel_arithmetic_succ(
990 &self,
991 model: &TypedModel,
992 node: &TypedNode,
993 ) -> TractResult<Option<TypedModelPatch>> {
994 rule_if!(self.q_params.is_none());
995 rule_if!(self.group == 1);
996 rule_if_let!(&[succ_outlet] = &*node.outputs[0].successors);
997 let succ = model.node(succ_outlet.node);
998 rule_if_some!(bin = succ.op_as::<TypedBinOp>());
999 let other_input = succ.inputs[1 - succ_outlet.slot];
1000 let axes_mapping = model.node_axes_mapping(succ.id)?;
1001 let input_shape =
1002 self.pool_spec.data_format.shape(&model.outlet_fact(node.inputs[0])?.shape)?;
1003 let conv_c_axis = input_shape.c_axis();
1004 rule_if!(
1005 axes_mapping.axis((InOut::In(succ_outlet.slot), conv_c_axis))?.inputs
1006 [1 - succ_outlet.slot]
1007 .len()
1008 == 1
1009 );
1010 let mut other_expected_shape = tvec!(1.to_dim(); input_shape.rank());
1011 other_expected_shape[conv_c_axis] = self.output_channels().to_dim();
1012 rule_if!(*other_expected_shape == *model.outlet_fact(other_input)?.shape);
1013
1014 let mut patch = TypedModelPatch::default();
1015 let [input, mut kernel, mut bias] = *patch.taps(model, &node.inputs)? else {
1016 panic!("Expect three inputs");
1017 };
1018 let name = &node.name;
1019 let succ_name = &succ.name;
1020
1021 let operand = patch.tap_model(model, other_input)?;
1022
1023 let renamed_bias = format!("{name}.{succ_name}.bias");
1024 let renamed_kernel = format!("{name}.{succ_name}.kernel");
1025 bias = wire_reshape_bias_for_bin(
1026 &mut patch,
1027 format!("{renamed_bias}.reshape"),
1028 bias,
1029 1,
1030 0,
1031 self.output_channels(),
1032 )?[0];
1033
1034 let operand = wire_reshape_bias_for_bin(
1035 &mut patch,
1036 format!("{renamed_bias}.reshape_operand"),
1037 operand,
1038 1,
1039 0,
1040 self.output_channels(),
1041 )?[0];
1042
1043 let operand_fact = patch.outlet_fact(operand)?.shape.to_tvec();
1044 let kernel_fact = patch.outlet_fact(kernel)?;
1045 let mut operand_shape_for_kernel = tvec!(1.to_dim(); 2 + input_shape.hw_rank());
1046 operand_shape_for_kernel[self.kernel_fmt.o_axis(&kernel_fact.shape)] =
1047 self.output_channels().to_dim();
1048 let operand_for_kernel = patch.wire_node(
1049 format!("{renamed_kernel}.reshape_operand"),
1050 AxisOp::Reshape(0, operand_fact, operand_shape_for_kernel),
1051 &[operand],
1052 )?[0];
1053
1054 if bin.0.is::<Sub>() && succ_outlet.slot == 0 {
1055 bias = patch.wire_node(&renamed_bias, sub(), &[bias, operand])?[0];
1056 } else if bin.0.is::<Sub>() {
1057 bias = patch.wire_node(&renamed_bias, sub(), &[operand, bias])?[0];
1058 } else if bin.0.is::<Div>() && succ_outlet.slot == 0 {
1059 bias = patch.wire_node(&renamed_bias, div(), &[bias, operand])?[0];
1060 kernel = patch.wire_node(&renamed_kernel, div(), &[kernel, operand_for_kernel])?[0];
1061 } else if bin.0.is::<Div>() {
1062 bias = patch.wire_node(&renamed_bias, div(), &[operand, bias])?[0];
1063 kernel = patch.wire_node(&renamed_kernel, div(), &[operand_for_kernel, kernel])?[0];
1064 } else if bin.0.is::<Add>() {
1065 bias = patch.wire_node(&renamed_bias, add(), &[bias, operand])?[0];
1066 } else if bin.0.is::<Mul>() {
1067 bias = patch.wire_node(&renamed_bias, mul(), &[bias, operand])?[0];
1068 kernel = patch.wire_node(&renamed_kernel, mul(), &[kernel, operand_for_kernel])?[0];
1069 } else {
1070 return Ok(None);
1071 };
1072 let wire = patch.wire_node(&node.name, self.clone(), &[input, kernel, bias])?[0];
1073 patch.shunt_outside(model, succ_outlet.node.into(), wire)?;
1074 Ok(Some(patch))
1075 }
1076}
1077
1078impl Op for Conv {
1079 fn name(&self) -> StaticName {
1080 "Conv".into()
1081 }
1082
1083 fn info(&self) -> TractResult<Vec<String>> {
1084 let mut info = self.pool_spec.info();
1085 info.push(format!("Kernel {:?} (groups:{})", self.kernel_fmt, self.group));
1086 Ok(info)
1087 }
1088
1089 fn validation(&self) -> Validation {
1090 Validation::Rounding
1091 }
1092
1093 op_as_typed_op!();
1094}
1095
1096impl EvalOp for Conv {
1097 op_out_of_plan!();
1098
1099 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
1100 let mut model = TypedModel::default();
1101 let wire: TVec<OutletId> = inputs
1102 .iter()
1103 .enumerate()
1104 .map(|(ix, v)| model.add_source(format!("source.{ix}"), v.datum_type().fact(v.shape())))
1105 .collect::<TractResult<_>>()?;
1106 let wire = unsafe {
1107 if self.q_params.is_some() {
1108 self.wire_as_quant_im2col(&mut model, "im2col-adhoc", &wire)?
1109 } else {
1110 self.wire_as_im2col_pair(&mut model, "im2col-adhoc", &wire)?
1111 }
1112 };
1113 model.select_output_outlets(&wire)?;
1114 model.into_runnable()?.run(inputs)
1115 }
1116}
1117
1118impl TypedOp for Conv {
1119 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
1120 ensure!(self.q_params.is_some() || inputs[0].datum_type.is_float());
1121 let q_inputs = if self.q_params.is_some() { 6 } else { 0 };
1122 ensure!(inputs[1].datum_type.is_number() || self.kernel_fmt == KernelFormat::OIHW);
1123 if inputs.len() != 3 + q_inputs {
1124 bail!("Wrong number of inputs: expected {} got {}", 3 + q_inputs, inputs.len());
1125 }
1126 if self.q_params.is_some() {
1127 ensure!(inputs[2].datum_type == i32::datum_type());
1128 ensure!(inputs[3].datum_type == i32::datum_type());
1129 ensure!(inputs[4].datum_type.is_float());
1130 ensure!(inputs[5].datum_type == i32::datum_type());
1131 ensure!(inputs[6].datum_type.is_float());
1132 ensure!(inputs[7].datum_type == i32::datum_type());
1133 ensure!(inputs[8].datum_type.is_float());
1134 }
1135 ensure!(self.pool_spec.rank() + 2 == inputs[1].shape.len());
1136 if self.pool_spec.data_format.shape(&*inputs[0].shape)?.c()
1137 != &self.input_channels().to_dim()
1138 {
1139 bail!(
1140 "Inconsistent convolution: input is {:?}, but kernel expects {} input channels.\n{:?}",
1141 inputs[0],
1142 self.input_channels(),
1143 self
1144 );
1145 }
1146 if let ExplicitOnnxPool(bef, after, _) | Explicit(bef, after) = &self.pool_spec.padding {
1147 anyhow::ensure!(bef.len() == self.pool_spec.rank());
1148 anyhow::ensure!(after.len() == self.pool_spec.rank());
1149 }
1150 ensure!(
1151 inputs[2].rank() == 0
1152 || (inputs[2].rank() == 1
1153 && inputs[2].shape.volume() == self.output_channels().to_dim()),
1154 "Bias should be scalar or a vector with one value per output channel. Output channels is {}, bias is {:?}",
1155 self.output_channels(),
1156 inputs[2]
1157 );
1158 let mut fact = self.pool_spec.output_facts(inputs)?.remove(0);
1159 if let Some(dt) = self.q_params {
1160 fact.datum_type = dt;
1161 } else {
1162 ensure!(
1163 inputs[1].is_exotic() || inputs[0].datum_type == inputs[1].datum_type,
1164 "Convolution input, weights and bias must have the same type, got {inputs:?}",
1165 )
1166 }
1167 Ok(tvec!(fact))
1168 }
1169
1170 fn axes_mapping(
1171 &self,
1172 inputs: &[&TypedFact],
1173 outputs: &[&TypedFact],
1174 ) -> TractResult<AxesMapping> {
1175 let fact = &inputs[0];
1176 let shape = self.pool_spec.data_format.shape(&fact.shape)?;
1177 let mut axes = AxesMapping::disconnected(inputs, outputs)?
1178 .renaming((InOut::In(0), shape.c_axis()), 'I')?
1179 .renaming((InOut::Out(0), shape.c_axis()), 'O')?;
1180 if let Some(n_axis) = shape.n_axis() {
1181 axes = axes
1182 .renaming((InOut::In(0), n_axis), 'N')?
1183 .linking('N', (InOut::Out(0), n_axis))?;
1184 }
1185 let h_axis = shape.h_axis();
1186 let geo = "HWXYZ".chars().chain('a'..);
1187 let kernel_spatial_shape = &self.pool_spec.kernel_shape;
1188 let padding = self.pool_spec.computed_padding(shape.hw_dims());
1189 for ((ix, &dim), repr) in kernel_spatial_shape.iter().enumerate().zip(geo) {
1190 if dim == 1
1191 && self.pool_spec.dilation(ix) == 1
1192 && self.pool_spec.stride(ix) == 1
1193 && padding[ix].pad_before.is_zero()
1194 && padding[ix].pad_after.is_zero()
1195 {
1196 axes = axes
1197 .renaming((InOut::In(0), ix + h_axis), repr)?
1198 .linking(repr, (InOut::Out(0), ix + h_axis))?;
1199 }
1200 }
1201 if self.q_params.is_some() {
1202 for (qp_ix, qp) in inputs.iter().enumerate().skip(3) {
1203 if qp.rank() == 1 {
1204 axes = match qp_ix {
1205 3 | 4 => axes.linking('I', (InOut::In(qp_ix), 0))?,
1206 5 | 6 => axes.linking('O', (InOut::In(qp_ix), 0))?,
1207 7 | 8 => axes.linking('O', (InOut::In(qp_ix), 0))?,
1208 _ => unreachable!(),
1209 };
1210 }
1211 }
1212 }
1213 Ok(axes)
1214 }
1215
1216 fn declutter(
1217 &self,
1218 model: &TypedModel,
1219 node: &TypedNode,
1220 ) -> TractResult<Option<TypedModelPatch>> {
1221 macro_rules! pass {
1222 ($func:ident) => {
1223 if let Some(mut r) = self.$func(model, node).context(stringify!($func))? {
1224 trace!(stringify!($func));
1225 r.push_context(stringify!($func));
1226 return Ok(Some(r));
1227 }
1228 };
1229 }
1230 pass!(declutter_stride_slice_to_downsample);
1231 pass!(declutter_as_einsum);
1232 pass!(declutter_channel_arithmetic_succ);
1233 pass!(declutter_precursor_padding);
1234 Ok(None)
1235 }
1236
1237 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
1238 let shape = self.pool_spec.data_format.shape(inputs[0].shape.to_tvec())?;
1239 let kernel_spatial_shape = &self.pool_spec.kernel_shape;
1240 let output_dims = self.pool_spec.padding.compute(
1241 shape.hw_dims(),
1242 kernel_spatial_shape,
1243 &self
1244 .pool_spec
1245 .dilations
1246 .clone()
1247 .unwrap_or_else(|| tvec!(1; kernel_spatial_shape.len())),
1248 &self.pool_spec.strides.clone().unwrap_or_else(|| tvec!(1; kernel_spatial_shape.len())),
1249 );
1250 let n_output_points: TDim =
1251 output_dims.iter().map(|d| d.convoluted.clone()).product::<TDim>();
1252 let n_output_channels = self.output_channels().to_dim();
1253 let kernel_surface = kernel_spatial_shape.iter().product::<usize>().to_dim();
1254 let one = 1.to_dim();
1255 Ok(tvec!((
1256 Cost::FMA(inputs[0].datum_type),
1257 shape.n().cloned().unwrap_or(one)
1258 * shape.c()
1259 * n_output_channels
1260 * n_output_points
1261 * kernel_surface
1262 / self.group
1263 )))
1264 }
1265
1266 fn suggested_axis_changes(&self) -> TractResult<TVec<(InOut, AxisOp)>> {
1276 if tract_linalg::routines::depthwise_c_f32().is_none() {
1277 return Ok(tvec!());
1278 }
1279 let fmt = self.pool_spec.data_format;
1280 let depthwise = self.group > 1
1281 && self.group == self.input_channels()
1282 && self.group == self.output_channels();
1283 if !depthwise || self.q_params.is_some() || self.input_channels() < 16 || fmt.c_is_last() {
1284 return Ok(tvec!());
1285 }
1286 let rank = self.pool_spec.rank() + 1 + fmt.has_n() as usize;
1287 let c_axis = if fmt.has_n() { 1 } else { 0 };
1288 Ok(tvec!((InOut::In(0), AxisOp::Move(c_axis, rank - 1))))
1289 }
1290
1291 fn change_axes(
1292 &self,
1293 model: &TypedModel,
1294 node: &TypedNode,
1295 io: InOut,
1296 change: &AxisOp,
1297 ) -> TractResult<Option<AxisChangeConsequence>> {
1298 rule_if!(io != InOut::In(1));
1299 if io == InOut::In(2)
1300 && let &AxisOp::Rm(_) = change
1301 {
1302 return Ok(Some(AxisChangeConsequence {
1303 substitute_op: Some(Box::new(self.clone())),
1304 wire_changes: tvec!(),
1305 }));
1306 }
1307 let full_input_shape = model.outlet_fact(node.inputs[0])?.shape.to_tvec();
1308 let shape = self.pool_spec.data_format.shape(full_input_shape.clone())?;
1309 if shape.n_axis().is_none() && change == &AxisOp::Add(0) {
1312 let op = Conv { pool_spec: self.pool_spec.with_n_axis(), ..self.clone() };
1313 return Ok(Some(AxisChangeConsequence {
1314 substitute_op: Some(Box::new(op)),
1315 wire_changes: tvec!(
1316 (InOut::In(0), change.clone()),
1317 (InOut::Out(0), change.clone())
1318 ),
1319 }));
1320 }
1321 if let Some(n) = shape.n_axis() {
1323 assert_eq!(n, 0);
1324 if change == &AxisOp::Rm(n) {
1325 let op = Conv { pool_spec: self.pool_spec.dispose_n_axis(), ..self.clone() };
1326 return Ok(Some(AxisChangeConsequence {
1327 substitute_op: Some(Box::new(op)),
1328 wire_changes: tvec!(
1329 (InOut::In(0), change.clone()),
1330 (InOut::Out(0), change.clone())
1331 ),
1332 }));
1333 }
1334 rule_if!(change.transform_axis(n).map(|axis| axis == 0).unwrap_or(false));
1335 }
1336 let (new_format, axis_move) = match self.pool_spec.data_format {
1338 DataFormat::NCHW => {
1339 (DataFormat::NHWC, AxisOp::Move(shape.c_axis(), full_input_shape.len() - 1))
1340 }
1341 DataFormat::CHW => {
1342 (DataFormat::HWC, AxisOp::Move(shape.c_axis(), full_input_shape.len() - 1))
1343 }
1344 DataFormat::NHWC => (DataFormat::NCHW, AxisOp::Move(shape.c_axis(), 1)),
1345 DataFormat::HWC => (DataFormat::CHW, AxisOp::Move(shape.c_axis(), 0)),
1346 };
1347 if *change == axis_move {
1348 let mut new_op = self.clone();
1349 new_op.pool_spec.data_format = new_format;
1350 return Ok(Some(AxisChangeConsequence {
1351 substitute_op: Some(Box::new(new_op)),
1352 wire_changes: tvec!(
1353 (InOut::In(0), change.clone()),
1354 (InOut::Out(0), change.clone())
1355 ),
1356 }));
1357 }
1358 rule_if!(!model.node_input_facts(node.id)?[1].is_exotic());
1360 use AxisOp::*;
1361 let h_axis = shape.h_axis();
1362 let hw_axes = shape.hw_axes();
1363 let kh_axis = self.kernel_fmt.h_axis();
1364 let (geo_adjusted, kernel_adjusted) = match change {
1365 Rm(a)
1366 if hw_axes.contains(a)
1367 && hw_axes.len() > 1
1368 && self.pool_spec.dilation(a - h_axis) == 1
1369 && self.pool_spec.stride(a - h_axis) == 1
1370 && self.pool_spec.kernel_shape[a - h_axis] == 1 =>
1371 {
1372 let geo_axis = a - h_axis;
1373 (Rm(geo_axis), Rm(kh_axis + geo_axis))
1374 }
1375 Add(a) if hw_axes.contains(a) => (Add(a - h_axis), Add(a - h_axis + kh_axis)),
1376 Move(f, t) if hw_axes.contains(f) && hw_axes.contains(t) => {
1377 (Move(f - h_axis, t - h_axis), Move(f - h_axis + kh_axis, t - h_axis + kh_axis))
1378 }
1379 _ => return Ok(None),
1380 };
1381 let pool_spec = self.pool_spec.change_geo_axes(&geo_adjusted)?;
1382 let new_op = Conv { pool_spec, ..self.clone() };
1383 Ok(Some(AxisChangeConsequence {
1384 substitute_op: Some(Box::new(new_op)),
1385 wire_changes: tvec!(
1386 (InOut::In(0), change.clone()),
1387 (InOut::In(1), kernel_adjusted),
1388 (InOut::Out(0), change.clone())
1389 ),
1390 }))
1391 }
1392
1393 fn codegen(
1394 &self,
1395 model: &TypedModel,
1396 node: &TypedNode,
1397 ) -> TractResult<Option<TypedModelPatch>> {
1398 let input_fact = model.outlet_fact(node.inputs[0])?;
1399 unsafe {
1400 if self.q_params.is_some() {
1401 let mut patch = TypedModelPatch::new("quantized-codegen");
1402 let inputs = patch.taps(model, &node.inputs)?;
1403 let wire = self
1404 .wire_as_quant_im2col(&mut patch, &node.name, &inputs)
1405 .context("in wire_as_quant_im2col")?;
1406 patch.shunt_outside(model, node.id.into(), wire[0])?;
1407 patch.obliterate(node.id)?;
1408 Ok(Some(patch))
1409 } else if let Some(op) = self.try_blocked_conv(input_fact) {
1410 let mut patch = TypedModelPatch::new("blocked-conv");
1413 let inputs = patch.taps(model, &node.inputs)?;
1414 let wire = self
1415 .wire_as_blocked_conv(&mut patch, &node.name, &inputs, op)
1416 .context("wire_as_blocked_conv")?;
1417 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1418 patch.obliterate(node.id)?;
1419 Ok(Some(patch))
1420 } else if self.can_direct_spatial(input_fact) {
1421 let mut patch = TypedModelPatch::new("direct-spatial");
1422 let inputs = patch.taps(model, &node.inputs)?;
1423 let wire = self
1424 .wire_as_direct_spatial(&mut patch, &node.name, &inputs)
1425 .context("wire_as_direct_spatial")?;
1426 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1427 patch.obliterate(node.id)?;
1428 Ok(Some(patch))
1429 } else if input_fact
1430 .shape
1431 .as_concrete()
1432 .map(|s| should_use_lazy(&self.pool_spec, self.group, s, input_fact.datum_type))
1433 .unwrap_or(false)
1434 {
1435 let mut patch = TypedModelPatch::new("lazy-im2col");
1436 let inputs = patch.taps(model, &node.inputs)?;
1437 let wire = self
1438 .wire_as_lazy_im2col(&mut patch, &node.name, &inputs)
1439 .context("wire_as_lazy_im2col")?[0];
1440 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1441 patch.obliterate(node.id)?;
1442 Ok(Some(patch))
1443 } else if self.group != 1
1444 && self.group == self.output_channels()
1445 && self.group == self.input_channels()
1446 && input_fact.shape.as_concrete().is_some()
1447 {
1448 let mut patch = TypedModelPatch::new("depth_wise");
1449 let inputs = patch.taps(model, &node.inputs)?;
1450 let wire = self
1451 .wire_as_depth_wise(&mut patch, &node.name, &inputs)
1452 .context("wire_as_depth_wise")?;
1453 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1454 patch.obliterate(node.id)?;
1455 Ok(Some(patch))
1456 } else {
1457 let mut patch = TypedModelPatch::new("im2col");
1458 let inputs = patch.taps(model, &node.inputs)?;
1459 let wire = self
1460 .wire_as_im2col_pair(&mut patch, &node.name, &inputs)
1461 .context("in wire_as_im2col_pair")?[0];
1462 patch.shunt_outside(model, OutletId::new(node.id, 0), wire)?;
1463 patch.obliterate(node.id)?;
1464 Ok(Some(patch))
1465 }
1466 }
1467 }
1468
1469 as_op!();
1470}
1471
1472const DEFAULT_LAZY_IM2COL_MIN_KERNEL: usize = 6;
1481
1482const MIN_DIRECT_SPATIAL_W: usize = 128;
1485
1486fn amx_owns_fat_spatial() -> bool {
1489 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
1490 {
1491 tract_linalg::arm64::has_amx()
1492 }
1493 #[cfg(not(all(target_arch = "aarch64", target_os = "macos")))]
1494 {
1495 false
1496 }
1497}
1498
1499crate::declare_knob!(
1500 scopable TRACT_DISABLE_DIRECT_SPATIAL,
1501 bool,
1502 false,
1503 "Disable the direct spatial convolution, falling back to im2col and a matrix product."
1504);
1505
1506crate::declare_knob!(
1507 scopable TRACT_ENABLE_BLOCKED_CONV,
1508 bool,
1509 false,
1510 "Force-enable the direct blocked convolution on native targets (on by default on wasm)."
1511);
1512crate::declare_knob!(
1513 scopable TRACT_DISABLE_BLOCKED_CONV,
1514 bool,
1515 false,
1516 "Force-disable the direct blocked convolution on wasm targets (off by default on native)."
1517);
1518crate::declare_knob!(
1519 scopable TRACT_LAZY_IM2COL_MIN_KERNEL,
1520 usize,
1521 DEFAULT_LAZY_IM2COL_MIN_KERNEL,
1522 "Minimum convolution kernel volume before lazy im2col is preferred over eager."
1523);
1524crate::declare_knob!(
1525 scopable TRACT_LAZY_IM2COL_MAX_EAGER_BYTES,
1526 usize,
1527 DEFAULT_LAZY_IM2COL_MAX_EAGER_BYTES,
1528 "Eager-im2col scratch-size ceiling, in bytes, above which lazy im2col is preferred."
1529);
1530
1531fn lazy_im2col_min_kernel() -> usize {
1532 TRACT_LAZY_IM2COL_MIN_KERNEL.get()
1533}
1534
1535#[cfg(target_family = "wasm")]
1553const DEFAULT_LAZY_IM2COL_MAX_EAGER_BYTES: usize = 1024 * 1024;
1554#[cfg(not(target_family = "wasm"))]
1555const DEFAULT_LAZY_IM2COL_MAX_EAGER_BYTES: usize = 4 * 1024 * 1024;
1556
1557fn lazy_im2col_max_eager_bytes() -> usize {
1558 TRACT_LAZY_IM2COL_MAX_EAGER_BYTES.get()
1559}
1560
1561fn should_use_lazy(
1562 pool_spec: &PoolSpec,
1563 group: usize,
1564 input_shape: &[usize],
1565 dt: DatumType,
1566) -> bool {
1567 let is_depthwise =
1572 group > 1 && group == pool_spec.input_channels && group == pool_spec.output_channels;
1573 if is_depthwise {
1574 return false;
1575 }
1576 let Ok(output_shape) = pool_spec.output_shape(input_shape) else { return false };
1577 if output_shape.n().unwrap_or(&1) != &1 {
1579 return false;
1580 }
1581 let kernel_volume = pool_spec.kernel_shape.iter().product::<usize>();
1582 if kernel_volume >= lazy_im2col_min_kernel() {
1585 return true;
1586 }
1587 let n: usize = output_shape.hw_dims().iter().product();
1591 let k = pool_spec.input_channels * kernel_volume / group;
1592 let eager_scratch_bytes = k.saturating_mul(n).saturating_mul(dt.size_of());
1593 if eager_scratch_bytes >= lazy_im2col_max_eager_bytes() {
1594 return true;
1595 }
1596 if kernel_volume == 1 {
1599 return false;
1600 }
1601 let m = (pool_spec.output_channels / group).max(1);
1608 let mr = tract_linalg::MmmDispatch::native()
1609 .preferred_kernel(dt, Some(m), Some(k), Some(n))
1610 .map(|mmm| mmm.mr())
1611 .unwrap_or(1);
1612 m <= mr
1613}
1614
1615#[allow(non_snake_case)]
1616#[cfg(test)]
1617mod test {
1618 use super::*;
1619 use crate::ops::array::Pad;
1620 use DataFormat::*;
1621
1622 #[test]
1623 fn onnx_basic_convinteger() {
1624 let op = Conv {
1625 pool_spec: PoolSpec {
1626 data_format: NCHW,
1627 kernel_shape: tvec!(2, 2),
1628 padding: Valid,
1629 dilations: None,
1630 strides: None,
1631 input_channels: 1,
1632 output_channels: 1,
1633 },
1634 kernel_fmt: KernelFormat::OIHW,
1635 group: 1,
1636 q_params: Some(i32::datum_type()),
1637 };
1638 let input = tvec!(
1639 rctensor4(&[[[[1u8, 2, 3], [4, 5, 6], [7, 8, 9]]]]),
1640 rctensor4(&[[[[1u8, 1], [1, 1]]]]),
1641 rctensor0(0u32),
1642 rctensor0(1u8),
1643 rctensor0(1.0f32),
1644 rctensor0(0u8),
1645 rctensor0(1.0f32),
1646 rctensor0(0i32),
1647 rctensor0(1.0f32),
1648 );
1649 let input = input.into_iter().map(IntoTValue::into_tvalue).collect::<TVec<_>>();
1650 let output = op.eval(&EvalContext::out_of_plan(), input).unwrap();
1651 assert_eq!(*output[0], tensor4(&[[[[8i32, 12], [20, 24]]]]));
1652 }
1653
1654 #[test]
1655 fn valid_conv_absorbs_precursor_pad() -> TractResult<()> {
1656 let mut model = TypedModel::default();
1657 let wire = tvec!(model.add_source("source", f32::fact(dims!(1, 10)))?);
1658 let wire = model.wire_node(
1659 "pad",
1660 Pad {
1661 pads: vec![(0, 0), (1, 0)],
1662 mode: ops::array::PadMode::Constant(rctensor0(0f32)),
1663 },
1664 &wire,
1665 )?;
1666 let kernel = model.add_const("kernel", rctensor3(&[[[1f32, 2f32]]]))?;
1667 let bias = model.add_const("bias", rctensor0(0f32))?;
1668 let wire = model.wire_node(
1669 "conv",
1670 Conv {
1671 pool_spec: PoolSpec {
1672 data_format: crate::ops::nn::DataFormat::CHW,
1673 dilations: None,
1674 strides: None,
1675 kernel_shape: tvec![2],
1676 padding: Explicit(tvec![0], tvec![0]),
1677 input_channels: 1,
1678 output_channels: 1,
1679 },
1680 kernel_fmt: crate::ops::cnn::KernelFormat::OIHW,
1681 group: 1,
1682 q_params: None,
1683 },
1684 &[wire[0], kernel, bias],
1685 )?;
1686 model.select_output_outlets(&wire)?;
1687 model.declutter()?;
1688 assert_eq!(model.nodes().len(), 4); let cv = model.nodes()[3].op_as::<Conv>().unwrap();
1690 assert_eq!(cv.pool_spec.padding, Explicit(tvec![1], tvec![0])); Ok(())
1692 }
1693
1694 fn filled(shape: &[usize], mut f: impl FnMut(usize) -> f32) -> Tensor {
1695 let n: usize = shape.iter().product();
1696 let data: Vec<f32> = (0..n).map(|i| f(i)).collect();
1697 Tensor::from_shape(shape, &data).unwrap()
1698 }
1699
1700 fn run_lazy(
1701 fmt: DataFormat,
1702 input: &Tensor,
1703 kernel: &Tensor,
1704 padding: crate::ops::cnn::PaddingSpec,
1705 strides: Option<TVec<usize>>,
1706 dilations: Option<TVec<usize>>,
1707 group: usize,
1708 ic: usize,
1709 oc: usize,
1710 kernel_shape: TVec<usize>,
1711 ) -> TractResult<(TypedModel, Tensor)> {
1712 let mut model = TypedModel::default();
1713 let wire = tvec!(model.add_source("input", f32::fact(input.shape().to_vec()))?);
1714 let k = model.add_const("kernel", kernel.clone().into_arc_tensor())?;
1715 let bias = model.add_const("bias", rctensor0(0f32))?;
1716 let op = Conv {
1717 pool_spec: PoolSpec {
1718 data_format: fmt,
1719 kernel_shape,
1720 padding,
1721 dilations,
1722 strides,
1723 input_channels: ic,
1724 output_channels: oc,
1725 },
1726 kernel_fmt: KernelFormat::OIHW,
1727 group,
1728 q_params: None,
1729 };
1730 let wire = model.wire_node("conv", op, &[wire[0], k, bias])?;
1731 model.select_output_outlets(&wire)?;
1732 let model = model.into_optimized()?;
1733 let out = model.clone().into_runnable()?.run(tvec!(input.clone().into_tvalue()))?;
1734 Ok((model, (*out[0]).clone()))
1735 }
1736
1737 #[test]
1740 fn pointwise_nchw_strided_pack() -> TractResult<()> {
1741 check_pointwise(81, 79, 16, 32, true)?;
1744 check_pointwise(5, 7, 3, 5, false)?;
1745 check_pointwise(4, 16, 4, 8, false)?;
1746 Ok(())
1747 }
1748
1749 fn sme_on_this_cpu() -> bool {
1750 #[cfg(target_arch = "aarch64")]
1751 {
1752 tract_linalg::arm64::has_sme()
1753 }
1754 #[cfg(not(target_arch = "aarch64"))]
1755 {
1756 false
1757 }
1758 }
1759
1760 fn check_pointwise(
1761 h: usize,
1762 w: usize,
1763 ic: usize,
1764 oc: usize,
1765 require_sme: bool,
1766 ) -> TractResult<()> {
1767 use tract_linalg::strided_panel::StridedKMajor;
1768 let input = filled(&[1, ic, h, w], |i| (i % 7) as f32 + 1.0);
1769 let kernel = filled(&[oc, ic, 1, 1], |i| (i % 5) as f32 - 2.0);
1770 let (model, out) =
1771 run_lazy(NCHW, &input, &kernel, Valid, None, None, 1, ic, oc, tvec![1, 1])?;
1772 let uses_sme = model.nodes().iter().any(|n| {
1773 n.op_as::<OptMatMul>()
1774 .is_some_and(|op| op.mmm.iter().any(|m| m.name() == "sme_mmm_f32_32x32"))
1775 });
1776 if require_sme && sme_on_this_cpu() {
1777 let names: Vec<_> = model
1778 .nodes()
1779 .iter()
1780 .filter_map(|n| n.op_as::<OptMatMul>())
1781 .flat_map(|op| op.mmm.iter().map(|m| m.name().to_string()))
1782 .collect();
1783 ensure!(uses_sme, "{h}x{w} ic{ic} oc{oc} picked {names:?}");
1784 }
1785 if uses_sme {
1786 ensure!(
1787 model.nodes().iter().any(|n| {
1788 n.op_as::<OptMatMulPack>().is_some_and(|op| {
1789 op.packers.iter().any(|p| p.downcast_ref::<StridedKMajor>().is_some())
1790 })
1791 }),
1792 "{h}x{w}: SME matmul still packs the activation"
1793 );
1794 ensure!(
1795 model
1796 .nodes()
1797 .iter()
1798 .any(|n| { n.op_as::<OptMatMul>().is_some_and(|op| op.trivial_path) }),
1799 "{h}x{w}: strided pack turned off the trivial matmul path"
1800 );
1801 }
1802 let want = pointwise_reference(&input, &kernel);
1803 out.close_enough(&want, Approximation::Approximate)
1804 .with_context(|| format!("pointwise {h}x{w} ic{ic} oc{oc}"))?;
1805 Ok(())
1806 }
1807
1808 fn pointwise_reference(input: &Tensor, kernel: &Tensor) -> Tensor {
1809 let (ic, h, w) = (input.shape()[1], input.shape()[2], input.shape()[3]);
1810 let oc = kernel.shape()[0];
1811 let iv = input.to_plain_array_view::<f32>().unwrap();
1812 let iv = iv.as_slice().unwrap();
1813 let kv = kernel.to_plain_array_view::<f32>().unwrap();
1814 let kv = kv.as_slice().unwrap();
1815 let mut out = vec![0f32; oc * h * w];
1816 for o in 0..oc {
1817 for y in 0..h {
1818 for x in 0..w {
1819 let mut acc = 0f32;
1820 for c in 0..ic {
1821 acc += iv[c * h * w + y * w + x] * kv[o * ic + c];
1822 }
1823 out[o * h * w + y * w + x] = acc;
1824 }
1825 }
1826 }
1827 Tensor::from_shape(&[1, oc, h, w], &out).unwrap()
1828 }
1829}