src/cpu/deform_conv2d.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // ─── CPU modulated deformable conv2d (torchvision deform_conv2d v2) ───────── | ||
| 2 | // | ||
| 3 | // FP32 scalar host implementation, forward/inference only. Mirrors | ||
| 4 | // torchvision's deformable_im2col + GEMM, fused into a single direct loop: | ||
| 5 | // for each output pixel and conv group, each kH×kW tap is bilinearly sampled | ||
| 6 | // from X at a per-tap, per-pixel-shifted location (offset field) with ZERO | ||
| 7 | // padding outside the input, optionally reweighted by the mask modulator, then | ||
| 8 | // reduced against the OIHW weight. | ||
| 9 | // | ||
| 10 | // Layouts match conv2d_forward (X/Y/Wt NCHW/OIHW). offset/mask are dense NCHW | ||
| 11 | // fields produced by the model's offset_conv / modulator_conv: | ||
| 12 | // offset: (N, deform_groups*2*kH*kW, H_out, W_out), channel | ||
| 13 | // grp*(2*kH*kW) + 2*(kh*kW+kw) [+1 for the col axis]. | ||
| 14 | // mask: (N, deform_groups*kH*kW, H_out, W_out), channel | ||
| 15 | // grp*(kH*kW) + (kh*kW+kw); null == all modulators 1. | ||
| 16 | // | ||
| 17 | // bilinear_interpolate matches torchvision exactly (the (h<=-1 || h>=H ...) | ||
| 18 | // early-out plus per-corner in-bounds guards), so taps that fall on or past the | ||
| 19 | // border contribute zero — NOT clamped. | ||
| 20 | |||
| 21 | #include <brotensor/tensor.h> | ||
| 22 | #include <brotensor/detail/cpu/thread_pool.h> | ||
| 23 | |||
| 24 | #include <cmath> | ||
| 25 | #include <cstddef> | ||
| 26 | #include <stdexcept> | ||
| 27 | #include <string> | ||
| 28 | #include <vector> | ||
| 29 | |||
| 30 | namespace brotensor::detail::cpu { | ||
| 31 | |||
| 32 | namespace { | ||
| 33 | |||
| 34 | 12 | inline int out_dim(int in, int pad, int dil, int k, int stride) { | |
| 35 | 12 | return (in + 2 * pad - dil * (k - 1) - 1) / stride + 1; | |
| 36 | } | ||
| 37 | |||
| 38 | // torchvision bilinear_interpolate: zero outside [0,H)×[0,W), per-corner guard. | ||
| 39 | 137564 | inline float bilinear(const float* in, int H, int W, float h, float w) { | |
| 40 |
6/6✓ Branch 0 taken 129996 times.
✓ Branch 1 taken 7568 times.
✓ Branch 2 taken 122306 times.
✓ Branch 3 taken 7690 times.
✓ Branch 4 taken 6036 times.
✓ Branch 5 taken 110592 times.
|
254192 | if (h <= -1.0f || static_cast<float>(H) <= h || |
| 41 |
2/2✓ Branch 0 taken 116628 times.
✓ Branch 1 taken 5678 times.
|
122306 | w <= -1.0f || static_cast<float>(W) <= w) { |
| 42 | 26972 | return 0.0f; | |
| 43 | } | ||
| 44 | 110592 | int h_low = static_cast<int>(std::floor(h)); | |
| 45 | 110592 | int w_low = static_cast<int>(std::floor(w)); | |
| 46 | 110592 | int h_high = h_low + 1; | |
| 47 | 110592 | int w_high = w_low + 1; | |
| 48 | 110592 | float lh = h - h_low, lw = w - w_low; | |
| 49 | 110592 | float hh = 1.0f - lh, hw = 1.0f - lw; | |
| 50 |
4/4✓ Branch 0 taken 104360 times.
✓ Branch 1 taken 6232 times.
✓ Branch 2 taken 4494 times.
✓ Branch 3 taken 99866 times.
|
110592 | float v1 = (h_low >= 0 && w_low >= 0) ? in[h_low * W + w_low] : 0.0f; |
| 51 |
4/4✓ Branch 0 taken 104360 times.
✓ Branch 1 taken 6232 times.
✓ Branch 2 taken 4224 times.
✓ Branch 3 taken 100136 times.
|
110592 | float v2 = (h_low >= 0 && w_high <= W - 1) ? in[h_low * W + w_high] : 0.0f; |
| 52 |
4/4✓ Branch 0 taken 104794 times.
✓ Branch 1 taken 5798 times.
✓ Branch 2 taken 4412 times.
✓ Branch 3 taken 100382 times.
|
110592 | float v3 = (h_high <= H - 1 && w_low >= 0) ? in[h_high * W + w_low] : 0.0f; |
| 53 |
4/4✓ Branch 0 taken 104794 times.
✓ Branch 1 taken 5798 times.
✓ Branch 2 taken 4276 times.
✓ Branch 3 taken 100518 times.
|
110592 | float v4 = (h_high <= H - 1 && w_high <= W - 1) ? in[h_high * W + w_high] : 0.0f; |
| 54 | 110592 | float w1 = hh * hw, w2 = hh * lw, w3 = lh * hw, w4 = lh * lw; | |
| 55 | 110592 | return w1 * v1 + w2 * v2 + w3 * v3 + w4 * v4; | |
| 56 | 137564 | } | |
| 57 | |||
| 58 | } // namespace | ||
| 59 | |||
| 60 | 6 | void deform_conv2d_forward(const ::brotensor::Tensor& X, | |
| 61 | const ::brotensor::Tensor& offset, | ||
| 62 | const ::brotensor::Tensor* mask, | ||
| 63 | const ::brotensor::Tensor& Wt, | ||
| 64 | const ::brotensor::Tensor* bias, | ||
| 65 | int N, int C_in, int H, int W, | ||
| 66 | int C_out, int kH, int kW, | ||
| 67 | int stride_h, int stride_w, | ||
| 68 | int pad_h, int pad_w, | ||
| 69 | int dil_h, int dil_w, | ||
| 70 | int groups, int deform_groups, | ||
| 71 | ::brotensor::Tensor& Y) { | ||
| 72 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
11 | if (Wt.dtype != X.dtype || offset.dtype != X.dtype || |
| 73 |
4/4✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 time.
|
6 | (mask && mask->dtype != X.dtype) || (bias && bias->dtype != X.dtype)) { |
| 74 | ✗ | throw std::runtime_error( | |
| 75 | "deform_conv2d_forward: X, offset, mask, Wt, bias dtype must match"); | ||
| 76 | } | ||
| 77 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | if (groups < 1 || C_in % groups != 0 || C_out % groups != 0) { |
| 78 | ✗ | throw std::runtime_error( | |
| 79 | "deform_conv2d_forward: groups must divide C_in and C_out"); | ||
| 80 | } | ||
| 81 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | if (deform_groups < 1 || C_in % deform_groups != 0) { |
| 82 | ✗ | throw std::runtime_error( | |
| 83 | "deform_conv2d_forward: deform_groups must divide C_in"); | ||
| 84 | } | ||
| 85 | 6 | const int Cg_in = C_in / groups; | |
| 86 | 6 | const int Cg_out = C_out / groups; | |
| 87 | 6 | const int c_per_off_grp = C_in / deform_groups; | |
| 88 | 6 | const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h); | |
| 89 | 6 | const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w); | |
| 90 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | if (H_out <= 0 || W_out <= 0) { |
| 91 | ✗ | throw std::runtime_error("deform_conv2d_forward: non-positive output shape"); | |
| 92 | } | ||
| 93 | 6 | const int out_cols = C_out * H_out * W_out; | |
| 94 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
6 | if (Y.rows != N || Y.cols != out_cols || Y.dtype != X.dtype) { |
| 95 | 6 | Y.resize(N, out_cols, X.dtype); | |
| 96 | 6 | } | |
| 97 |
2/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
|
6 | if (N == 0 || out_cols == 0) return; |
| 98 | |||
| 99 | 6 | const float* Xp = X.host_f32(); | |
| 100 | 6 | const float* Op = offset.host_f32(); | |
| 101 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
|
6 | const float* Mp = mask ? mask->host_f32() : nullptr; |
| 102 | 6 | const float* Wp = Wt.host_f32(); | |
| 103 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
|
6 | const float* Bp = bias ? bias->host_f32() : nullptr; |
| 104 | 6 | float* Yp = Y.host_f32_mut(); | |
| 105 | |||
| 106 | 6 | const int ksz = kH * kW; | |
| 107 | 6 | const int off_row_stride = deform_groups * 2 * ksz * H_out * W_out; // per-n | |
| 108 | 6 | const int mask_row_stride = deform_groups * ksz * H_out * W_out; // per-n | |
| 109 | |||
| 110 | // The modulated bilinear sample depends only on (n, oh, ow, ic, tap) — it | ||
| 111 | // never depends on oc — but the original oc -> ic_local -> kh,kw loop | ||
| 112 | // order resampled it once per output channel (O(C_out) redundant work). | ||
| 113 | // Reorder to n -> g -> oh -> ow: sample each group's (Cg_in * ksz) taps | ||
| 114 | // once into a scratch column (im2col-style), then dot that column against | ||
| 115 | // every oc's weight row within the group. The inner accumulation order | ||
| 116 | // (ic_local-major, tap-minor) is preserved exactly, so results match the | ||
| 117 | // original bit-for-bit. | ||
| 118 | // Each n exclusively owns Y's batch slice n (X/offset/mask/Wt/bias are | ||
| 119 | // read-only), so this parallelizes across n with no cross-thread writes. | ||
| 120 | // The im2col scratch `col` MUST be declared inside this lambda (fresh | ||
| 121 | // per invocation, even across threads) rather than hoisted above the | ||
| 122 | // loop — a single shared vector reused across n/g would race under | ||
| 123 | // parallel_for (every thread scribbling into the same buffer). | ||
| 124 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
13 | parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) { |
| 125 | 7 | const int n = static_cast<int>(ni); | |
| 126 | 7 | std::vector<float> col(static_cast<size_t>(Cg_in) * ksz); | |
| 127 | 7 | const float* off_n = Op + static_cast<size_t>(n) * off_row_stride; | |
| 128 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 time.
|
7 | const float* mask_n = Mp ? Mp + static_cast<size_t>(n) * mask_row_stride : nullptr; |
| 129 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 7 times.
|
15 | for (int g = 0; g < groups; ++g) { |
| 130 | 8 | const int ic_base = g * Cg_in; | |
| 131 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 71 times.
|
79 | for (int oh = 0; oh < H_out; ++oh) { |
| 132 | 71 | const int in_h_origin = oh * stride_h - pad_h; | |
| 133 |
2/2✓ Branch 0 taken 709 times.
✓ Branch 1 taken 71 times.
|
780 | for (int ow = 0; ow < W_out; ++ow) { |
| 134 | 709 | const int in_w_origin = ow * stride_w - pad_w; | |
| 135 | 709 | const int sp = oh * W_out + ow; | |
| 136 | |||
| 137 |
2/2✓ Branch 0 taken 5980 times.
✓ Branch 1 taken 709 times.
|
6689 | for (int ic_local = 0; ic_local < Cg_in; ++ic_local) { |
| 138 | 5980 | const int ic = ic_base + ic_local; | |
| 139 | 5980 | const int off_grp = ic / c_per_off_grp; | |
| 140 | 5980 | const float* in_ch = Xp + (static_cast<size_t>(n) * C_in + ic) * H * W; | |
| 141 | 5980 | const float* off_grp_base = | |
| 142 | 5980 | off_n + static_cast<size_t>(off_grp) * 2 * ksz * H_out * W_out; | |
| 143 | 5980 | const float* mask_grp_base = | |
| 144 |
2/2✓ Branch 0 taken 5020 times.
✓ Branch 1 taken 960 times.
|
5980 | mask_n ? mask_n + static_cast<size_t>(off_grp) * ksz * H_out * W_out |
| 145 | : nullptr; | ||
| 146 | 5980 | float* col_ic = col.data() + ic_local * ksz; | |
| 147 |
2/2✓ Branch 0 taken 25148 times.
✓ Branch 1 taken 5980 times.
|
31128 | for (int kh = 0; kh < kH; ++kh) { |
| 148 |
2/2✓ Branch 0 taken 25148 times.
✓ Branch 1 taken 137564 times.
|
162712 | for (int kw = 0; kw < kW; ++kw) { |
| 149 | 137564 | const int tap = kh * kW + kw; | |
| 150 | 137564 | const float off_y = off_grp_base[((2 * tap) * H_out + oh) * W_out + ow]; | |
| 151 | 137564 | const float off_x = off_grp_base[((2 * tap + 1) * H_out + oh) * W_out + ow]; | |
| 152 |
2/2✓ Branch 0 taken 128924 times.
✓ Branch 1 taken 8640 times.
|
137564 | const float m = mask_grp_base |
| 153 | 128924 | ? mask_grp_base[(tap * H_out + oh) * W_out + ow] : 1.0f; | |
| 154 | 137564 | const float y = in_h_origin + kh * dil_h + off_y; | |
| 155 | 137564 | const float x = in_w_origin + kw * dil_w + off_x; | |
| 156 |
1/2✓ Branch 0 taken 137564 times.
✗ Branch 1 not taken.
|
137564 | const float val = bilinear(in_ch, H, W, y, x); |
| 157 | 137564 | col_ic[tap] = m * val; | |
| 158 | 137564 | } | |
| 159 | 25148 | } | |
| 160 | 5980 | } | |
| 161 | |||
| 162 | 709 | const int col_len = Cg_in * ksz; | |
| 163 |
2/2✓ Branch 0 taken 3960 times.
✓ Branch 1 taken 709 times.
|
4669 | for (int oc_local = 0; oc_local < Cg_out; ++oc_local) { |
| 164 | 3960 | const int oc = g * Cg_out + oc_local; | |
| 165 | 3960 | const int w_oc_base = oc * Cg_in * ksz; | |
| 166 |
2/2✓ Branch 0 taken 3312 times.
✓ Branch 1 taken 648 times.
|
3960 | const float bias_v = Bp ? Bp[oc] : 0.0f; |
| 167 | 3960 | float acc = bias_v; | |
| 168 |
2/2✓ Branch 0 taken 1038448 times.
✓ Branch 1 taken 3960 times.
|
1042408 | for (int k = 0; k < col_len; ++k) { |
| 169 | 1038448 | acc += Wp[w_oc_base + k] * col[k]; | |
| 170 | 1038448 | } | |
| 171 | 3960 | Yp[(static_cast<size_t>(n) * C_out + oc) * H_out * W_out + sp] = acc; | |
| 172 | 3960 | } | |
| 173 | 709 | } | |
| 174 | 71 | } | |
| 175 | 8 | } | |
| 176 | 7 | }); | |
| 177 | 6 | } | |
| 178 | |||
| 179 | } // namespace brotensor::detail::cpu | ||
| 180 |