1use crate::internal::*;
23
24const WB: usize = 16;
26
27#[cfg(all(target_feature = "simd128", not(target_feature = "relaxed-simd")))]
28macro_rules! blocked_madd {
29 ($acc:expr, $a:expr, $b:expr) => {
30 f32x4_add($acc, f32x4_mul($a, $b))
31 };
32}
33
34#[cfg(target_feature = "relaxed-simd")]
35macro_rules! blocked_madd {
36 ($acc:expr, $a:expr, $b:expr) => {
37 f32x4_relaxed_madd($a, $b, $acc)
38 };
39}
40
41#[derive(Debug, Clone, Hash, PartialEq, Eq)]
45pub struct BlockedConv {
46 pub n: usize,
47 pub c_in: usize,
48 pub h_in: usize,
49 pub w: usize,
50 pub oc: usize,
51 pub group: usize,
52 pub kh: usize,
53 pub stride_h: usize,
54 pub dil_h: usize,
55 pub pad_before_h: usize,
56 pub h_out: usize,
57}
58
59impl BlockedConv {
60 #[inline]
61 fn icg(&self) -> usize {
62 self.c_in / self.group
63 }
64 #[inline]
65 fn ocg(&self) -> usize {
66 self.oc / self.group
67 }
68}
69
70impl Op for BlockedConv {
71 fn name(&self) -> StaticName {
72 "BlockedConv".into()
73 }
74
75 fn info(&self) -> TractResult<Vec<String>> {
76 Ok(vec![format!(
77 "N={} C={}->OC={} group={} kh={} (icg={} ocg={}) HxW={}x{} -> H_out={} pad_before={} stride_h={} dil_h={}",
78 self.n,
79 self.c_in,
80 self.oc,
81 self.group,
82 self.kh,
83 self.icg(),
84 self.ocg(),
85 self.h_in,
86 self.w,
87 self.h_out,
88 self.pad_before_h,
89 self.stride_h,
90 self.dil_h,
91 )])
92 }
93
94 op_as_typed_op!();
95}
96
97impl EvalOp for BlockedConv {
98 op_out_of_plan!();
99
100 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
101 let x_t = inputs[0].cast_to::<f32>()?;
102 let k_t = inputs[1].cast_to::<f32>()?;
103 let b_t = inputs[2].cast_to::<f32>()?;
104 let x = unsafe { x_t.as_slice_unchecked::<f32>() };
106 let kernel = unsafe { k_t.as_slice_unchecked::<f32>() };
107 let bias_raw = unsafe { b_t.as_slice_unchecked::<f32>() };
108 let bias_vec: Vec<f32> = match bias_raw.len() {
111 0 => vec![0.0; self.oc],
112 1 => vec![bias_raw[0]; self.oc],
113 _ => bias_raw.to_vec(),
114 };
115 let bias = bias_vec.as_slice();
116
117 let mut output =
118 unsafe { Tensor::uninitialized::<f32>(&[self.n, self.oc, self.h_out, self.w])? };
119 let out = unsafe { output.as_slice_mut_unchecked::<f32>() };
120
121 let ocg = self.ocg();
122 match ocg {
123 1 => self.run_simd::<1>(x, kernel, bias, out),
124 2 => self.run_simd::<2>(x, kernel, bias, out),
125 3 => self.run_simd::<3>(x, kernel, bias, out),
126 4 => self.run_simd::<4>(x, kernel, bias, out),
127 5 => self.run_simd::<5>(x, kernel, bias, out),
128 6 => self.run_simd::<6>(x, kernel, bias, out),
129 8 => self.run_simd::<8>(x, kernel, bias, out),
130 _ => self.run_simd_generic(x, kernel, bias, out),
131 }
132
133 Ok(tvec!(output.into_tvalue()))
134 }
135}
136
137impl BlockedConv {
138 #[allow(clippy::needless_range_loop)]
151 #[allow(dead_code)]
152 fn run<const OCG: usize>(&self, x: &[f32], kernel: &[f32], bias: &[f32], out: &mut [f32]) {
153 let (icg, w, h_in, h_out, kh) = (self.icg(), self.w, self.h_in, self.h_out, self.kh);
154 let (sh, dh, pb) =
155 (self.stride_h as isize, self.dil_h as isize, self.pad_before_h as isize);
156 let kstride_oc = icg * kh; let n_full = w / WB; for ni in 0..self.n {
159 let x_n = &x[ni * self.c_in * h_in * w..];
160 let out_n = &mut out[ni * self.oc * h_out * w..];
161 for g in 0..self.group {
162 let oc0 = g * OCG;
163 let ic0 = g * icg;
164 for oh in 0..h_out {
165 for blk in 0..n_full {
167 let wb = blk * WB;
168 let mut acc = [[0f32; WB]; OCG];
169 for ocl in 0..OCG {
170 let b = bias[oc0 + ocl];
171 for j in 0..WB {
172 acc[ocl][j] = b;
173 }
174 }
175 for kh_i in 0..kh {
176 let ih = oh as isize * sh + kh_i as isize * dh - pb;
177 if ih < 0 || ih >= h_in as isize {
178 continue;
179 }
180 let row0 = ((ic0 * h_in + ih as usize) * w + wb) as isize;
181 for icl in 0..icg {
182 let row_base = (row0 + (icl * h_in * w) as isize) as usize;
183 for ocl in 0..OCG {
184 let wv = unsafe {
185 *kernel.get_unchecked(
186 (oc0 + ocl) * kstride_oc + icl * kh + kh_i,
187 )
188 };
189 let a = &mut acc[ocl];
190 for j in 0..WB {
191 a[j] += unsafe { *x_n.get_unchecked(row_base + j) } * wv;
192 }
193 }
194 }
195 }
196 for ocl in 0..OCG {
197 let ob = ((oc0 + ocl) * h_out + oh) * w + wb;
198 for j in 0..WB {
199 unsafe { *out_n.get_unchecked_mut(ob + j) = acc[ocl][j] };
200 }
201 }
202 }
203 let wb = n_full * WB;
205 if wb < w {
206 let rem = w - wb;
207 for ocl in 0..OCG {
208 let b = bias[oc0 + ocl];
209 let ob = ((oc0 + ocl) * h_out + oh) * w + wb;
210 for j in 0..rem {
211 out_n[ob + j] = b;
212 }
213 }
214 for kh_i in 0..kh {
215 let ih = oh as isize * sh + kh_i as isize * dh - pb;
216 if ih < 0 || ih >= h_in as isize {
217 continue;
218 }
219 let ih = ih as usize;
220 for icl in 0..icg {
221 let row_base = ((ic0 + icl) * h_in + ih) * w + wb;
222 for ocl in 0..OCG {
223 let wv = kernel[(oc0 + ocl) * kstride_oc + icl * kh + kh_i];
224 let ob = ((oc0 + ocl) * h_out + oh) * w + wb;
225 for j in 0..rem {
226 out_n[ob + j] += x_n[row_base + j] * wv;
227 }
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236
237 #[allow(clippy::needless_range_loop)]
240 #[allow(dead_code)]
241 fn run_generic(&self, x: &[f32], kernel: &[f32], bias: &[f32], out: &mut [f32]) {
242 let (icg, ocg, w, h_in, h_out, kh) =
243 (self.icg(), self.ocg(), self.w, self.h_in, self.h_out, self.kh);
244 let (sh, dh, pb) =
245 (self.stride_h as isize, self.dil_h as isize, self.pad_before_h as isize);
246 let kstride_oc = icg * kh;
247 let mut acc = vec![0f32; ocg * w];
248 for ni in 0..self.n {
249 let x_n = &x[ni * self.c_in * h_in * w..];
250 let out_n = &mut out[ni * self.oc * h_out * w..];
251 for g in 0..self.group {
252 let oc0 = g * ocg;
253 let ic0 = g * icg;
254 for oh in 0..h_out {
255 for ocl in 0..ocg {
256 let b = bias[oc0 + ocl];
257 for j in 0..w {
258 acc[ocl * w + j] = b;
259 }
260 }
261 for kh_i in 0..kh {
262 let ih = oh as isize * sh + kh_i as isize * dh - pb;
263 if ih < 0 || ih >= h_in as isize {
264 continue;
265 }
266 let ih = ih as usize;
267 for icl in 0..icg {
268 let ic = ic0 + icl;
269 let row = &x_n[(ic * h_in + ih) * w..(ic * h_in + ih) * w + w];
270 for ocl in 0..ocg {
271 let wv = kernel[(oc0 + ocl) * kstride_oc + icl * kh + kh_i];
272 let a = &mut acc[ocl * w..ocl * w + w];
273 for j in 0..w {
274 a[j] += row[j] * wv;
275 }
276 }
277 }
278 }
279 for ocl in 0..ocg {
280 let ob = ((oc0 + ocl) * h_out + oh) * w;
281 out_n[ob..ob + w].copy_from_slice(&acc[ocl * w..ocl * w + w]);
282 }
283 }
284 }
285 }
286 }
287
288 #[cfg(target_feature = "simd128")]
296 #[allow(clippy::needless_range_loop)]
297 fn run_simd<const OCG: usize>(&self, x: &[f32], kernel: &[f32], bias: &[f32], out: &mut [f32]) {
298 use std::arch::wasm32::*;
299
300 let (icg, w, h_in, h_out, kh) = (self.icg(), self.w, self.h_in, self.h_out, self.kh);
301 let (sh, dh, pb) =
302 (self.stride_h as isize, self.dil_h as isize, self.pad_before_h as isize);
303 let kstride_oc = icg * kh;
304 let n_full = w / WB;
305 for ni in 0..self.n {
306 let x_n = &x[ni * self.c_in * h_in * w..];
307 let out_n = &mut out[ni * self.oc * h_out * w..];
308 for g in 0..self.group {
309 let oc0 = g * OCG;
310 let ic0 = g * icg;
311 for oh in 0..h_out {
312 for blk in 0..n_full {
313 let wb = blk * WB;
314 let mut acc: [[v128; 4]; OCG] = {
315 let z = f32x4_splat(0.0);
316 [[z; 4]; OCG]
317 };
318 for ocl in 0..OCG {
319 let b = f32x4_splat(bias[oc0 + ocl]);
320 acc[ocl][0] = b;
321 acc[ocl][1] = b;
322 acc[ocl][2] = b;
323 acc[ocl][3] = b;
324 }
325 for kh_i in 0..kh {
326 let ih = oh as isize * sh + kh_i as isize * dh - pb;
327 if ih < 0 || ih >= h_in as isize {
328 continue;
329 }
330 let row0 = ((ic0 * h_in + ih as usize) * w + wb) as usize;
331 for icl in 0..icg {
332 let row_base = row0 + (icl * h_in * w);
333 let x_slice = &x_n[row_base..row_base + WB];
334 for ocl in 0..OCG {
335 let wv = f32x4_splat(
336 kernel[(oc0 + ocl) * kstride_oc + icl * kh + kh_i],
337 );
338 unsafe {
339 let xp = x_slice.as_ptr() as *const v128;
340 let ap = acc[ocl].as_mut_ptr();
341 let a0 = v128_load(ap.add(0));
342 let a1 = v128_load(ap.add(1));
343 let a2 = v128_load(ap.add(2));
344 let a3 = v128_load(ap.add(3));
345 let x0 = v128_load(xp.add(0));
346 let x1 = v128_load(xp.add(1));
347 let x2 = v128_load(xp.add(2));
348 let x3 = v128_load(xp.add(3));
349 v128_store(ap.add(0), blocked_madd!(a0, x0, wv));
350 v128_store(ap.add(1), blocked_madd!(a1, x1, wv));
351 v128_store(ap.add(2), blocked_madd!(a2, x2, wv));
352 v128_store(ap.add(3), blocked_madd!(a3, x3, wv));
353 }
354 }
355 }
356 }
357 for ocl in 0..OCG {
358 let ob = ((oc0 + ocl) * h_out + oh) * w + wb;
359 let out_slice = &mut out_n[ob..ob + WB];
360 unsafe {
361 let op = out_slice.as_mut_ptr() as *mut v128;
362 v128_store(op.add(0), acc[ocl][0]);
363 v128_store(op.add(1), acc[ocl][1]);
364 v128_store(op.add(2), acc[ocl][2]);
365 v128_store(op.add(3), acc[ocl][3]);
366 }
367 }
368 }
369 let wb = n_full * WB;
370 if wb < w {
371 let rem = w - wb;
372 for ocl in 0..OCG {
373 let b = bias[oc0 + ocl];
374 let ob = ((oc0 + ocl) * h_out + oh) * w + wb;
375 for j in 0..rem {
376 out_n[ob + j] = b;
377 }
378 }
379 for kh_i in 0..kh {
380 let ih = oh as isize * sh + kh_i as isize * dh - pb;
381 if ih < 0 || ih >= h_in as isize {
382 continue;
383 }
384 let ih = ih as usize;
385 for icl in 0..icg {
386 let row_base = ((ic0 + icl) * h_in + ih) * w + wb;
387 for ocl in 0..OCG {
388 let wv = kernel[(oc0 + ocl) * kstride_oc + icl * kh + kh_i];
389 let ob = ((oc0 + ocl) * h_out + oh) * w + wb;
390 for j in 0..rem {
391 out_n[ob + j] += x_n[row_base + j] * wv;
392 }
393 }
394 }
395 }
396 }
397 }
398 }
399 }
400 }
401
402 #[cfg(not(target_feature = "simd128"))]
404 #[inline(always)]
405 fn run_simd<const OCG: usize>(&self, x: &[f32], kernel: &[f32], bias: &[f32], out: &mut [f32]) {
406 self.run::<OCG>(x, kernel, bias, out);
407 }
408
409 #[cfg(target_feature = "simd128")]
411 #[allow(clippy::needless_range_loop)]
412 fn run_simd_generic(&self, x: &[f32], kernel: &[f32], bias: &[f32], out: &mut [f32]) {
413 use std::arch::wasm32::*;
414
415 let (icg, ocg, w, h_in, h_out, kh) =
416 (self.icg(), self.ocg(), self.w, self.h_in, self.h_out, self.kh);
417 let (sh, dh, pb) =
418 (self.stride_h as isize, self.dil_h as isize, self.pad_before_h as isize);
419 let kstride_oc = icg * kh;
420 let mut acc = vec![0f32; ocg * w];
421 for ni in 0..self.n {
422 let x_n = &x[ni * self.c_in * h_in * w..];
423 let out_n = &mut out[ni * self.oc * h_out * w..];
424 for g in 0..self.group {
425 let oc0 = g * ocg;
426 let ic0 = g * icg;
427 for oh in 0..h_out {
428 for ocl in 0..ocg {
429 let b = bias[oc0 + ocl];
430 let a = &mut acc[ocl * w..ocl * w + w];
431 for j in 0..w {
432 a[j] = b;
433 }
434 }
435 for kh_i in 0..kh {
436 let ih = oh as isize * sh + kh_i as isize * dh - pb;
437 if ih < 0 || ih >= h_in as isize {
438 continue;
439 }
440 let ih = ih as usize;
441 for icl in 0..icg {
442 let ic = ic0 + icl;
443 let row = &x_n[(ic * h_in + ih) * w..(ic * h_in + ih) * w + w];
444 for ocl in 0..ocg {
445 let wv_scalar = kernel[(oc0 + ocl) * kstride_oc + icl * kh + kh_i];
446 let a = &mut acc[ocl * w..ocl * w + w];
447 let n4 = w & !3;
448 if n4 > 0 {
449 unsafe {
450 let wv = f32x4_splat(wv_scalar);
451 let xp = row.as_ptr() as *const v128;
452 let ap = a.as_mut_ptr() as *mut v128;
453 for j in 0..n4 / 4 {
454 let av = v128_load(ap.add(j));
455 let xv = v128_load(xp.add(j));
456 v128_store(ap.add(j), blocked_madd!(av, xv, wv));
457 }
458 }
459 }
460 for j in n4..w {
461 a[j] += row[j] * wv_scalar;
462 }
463 }
464 }
465 }
466 for ocl in 0..ocg {
467 let ob = ((oc0 + ocl) * h_out + oh) * w;
468 out_n[ob..ob + w].copy_from_slice(&acc[ocl * w..ocl * w + w]);
469 }
470 }
471 }
472 }
473 }
474
475 #[cfg(not(target_feature = "simd128"))]
476 #[inline(always)]
477 fn run_simd_generic(&self, x: &[f32], kernel: &[f32], bias: &[f32], out: &mut [f32]) {
478 self.run_generic(x, kernel, bias, out);
479 }
480}
481
482impl TypedOp for BlockedConv {
483 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
484 ensure!(inputs.len() == 3, "BlockedConv expects 3 inputs (X, kernel, bias)");
485 Ok(tvec!(f32::datum_type().fact([self.n, self.oc, self.h_out, self.w])))
486 }
487
488 fn cost(&self, _inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
489 let macs = self.n * self.oc * self.h_out * self.w * self.icg() * self.kh;
490 Ok(tvec!((Cost::FMA(f32::datum_type()), macs.to_dim())))
491 }
492
493 as_op!();
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[allow(clippy::too_many_arguments)]
504 fn reference(op: &BlockedConv, x: &[f32], kernel: &[f32], bias: &[f32]) -> Vec<f32> {
505 let (icg, ocg) = (op.icg(), op.ocg());
506 let (h_in, w, kh) = (op.h_in, op.w, op.kh);
507 let (sh, dh, pb) = (op.stride_h as isize, op.dil_h as isize, op.pad_before_h as isize);
508 let mut out = vec![0f32; op.n * op.oc * op.h_out * w];
509 for ni in 0..op.n {
510 for oc in 0..op.oc {
511 let g = oc / ocg;
512 for oh in 0..op.h_out {
513 for wi in 0..w {
514 let mut acc = bias[oc];
515 for kh_i in 0..kh {
516 let ih = oh as isize * sh + kh_i as isize * dh - pb;
517 if ih < 0 || ih >= h_in as isize {
518 continue;
519 }
520 let ih = ih as usize;
521 for icl in 0..icg {
522 let ic = g * icg + icl;
523 let xv = x[((ni * op.c_in + ic) * h_in + ih) * w + wi];
524 acc += xv * kernel[oc * (icg * kh) + icl * kh + kh_i];
525 }
526 }
527 out[((ni * op.oc + oc) * op.h_out + oh) * w + wi] = acc;
528 }
529 }
530 }
531 }
532 out
533 }
534
535 fn run_case(c_in: usize, oc: usize, group: usize, kh: usize, h_in: usize, w: usize, pb: usize) {
536 let icg = c_in / group;
537 let h_out = h_in + pb - (kh - 1); let op = BlockedConv {
539 n: 1,
540 c_in,
541 h_in,
542 w,
543 oc,
544 group,
545 kh,
546 stride_h: 1,
547 dil_h: 1,
548 pad_before_h: pb,
549 h_out,
550 };
551 let x: Vec<f32> = (0..c_in * h_in * w).map(|i| ((i as f32 * 0.137).sin()) * 0.7).collect();
552 let kernel: Vec<f32> =
553 (0..oc * icg * kh).map(|i| ((i as f32 * 0.091).cos()) * 0.3).collect();
554 let bias: Vec<f32> = (0..oc).map(|i| (i as f32 * 0.05) - 0.1).collect();
555
556 let want = reference(&op, &x, &kernel, &bias);
557 let got = op
558 .eval(
559 &EvalContext::out_of_plan(),
560 tvec![
561 Tensor::from_shape(&[1, c_in, h_in, w], &x).unwrap().into_tvalue(),
562 Tensor::from_shape(&[oc, icg * kh], &kernel).unwrap().into_tvalue(),
563 Tensor::from_shape(&[oc], &bias).unwrap().into_tvalue(),
564 ],
565 )
566 .unwrap();
567 let got_view = got[0].to_plain_array_view::<f32>().unwrap();
568 let got = got_view.as_slice().unwrap();
569 assert_eq!(got.len(), want.len());
570 let max_abs = got.iter().zip(&want).map(|(a, b)| (a - b).abs()).fold(0.0, f32::max);
571 assert!(
572 max_abs < 1e-5,
573 "BlockedConv mismatch (c_in={c_in} oc={oc} g={group} kh={kh} h={h_in} w={w} pb={pb}): max_abs={max_abs}"
574 );
575 }
576
577 #[test]
578 fn blocked_conv_matches_reference() {
579 run_case(64, 10, 2, 5, 12, 96, 4);
581 run_case(4, 4, 2, 3, 5, 20, 1);
583 run_case(8, 6, 2, 4, 7, 5, 2);
585 run_case(6, 3, 1, 3, 8, 33, 0);
587 run_case(4, 2, 2, 2, 6, 17, 1);
589 run_case(14, 14, 2, 3, 6, 20, 1);
591 }
592}