src/cpu/interp2d.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // ─── CPU arbitrary-scale 2D resample ──────────────────────────────────────── | ||
| 2 | // | ||
| 3 | // FP32 scalar host implementations. The general 2D counterpart to the fixed-2x | ||
| 4 | // upsample_*_2x ops in resample.cpp — supports any (H_in, W_in) -> (H_out, | ||
| 5 | // W_out) on an NCHW tensor, with nearest / bilinear / bicubic modes. CPU is | ||
| 6 | // FP32-only. | ||
| 7 | // | ||
| 8 | // Memory layout (NCHW flat — matches resample.cpp): | ||
| 9 | // X / Y / dX / dY : ((n * C + c) * H + h) * W + w | ||
| 10 | // | ||
| 11 | // Sampling convention — PyTorch align_corners=False / half-pixel (matches the | ||
| 12 | // existing upsample_bilinear_2x exactly when (H_out, W_out) == (2H, 2W)): | ||
| 13 | // src_y = (oh + 0.5) * (H_in / H_out) - 0.5 | ||
| 14 | // src_x = (ow + 0.5) * (W_in / W_out) - 0.5 | ||
| 15 | // | ||
| 16 | // nearest : Y[oh,ow] = X[clamp(round_half_to_even(src), 0, dim-1)] | ||
| 17 | // bilinear : 2x2 tap weighted blend (border-clamped indices) | ||
| 18 | // bicubic : 4x4 cubic-convolution tap, border-clamped — forward only. | ||
| 19 | // mode 2 uses a = -0.5 (Catmull-Rom, matches PIL/Pillow BICUBIC); | ||
| 20 | // mode 3 uses a = -0.75 (matches torch.nn.functional.interpolate | ||
| 21 | // mode="bicubic" and OpenCV). The two differ only in that constant. | ||
| 22 | // | ||
| 23 | // ACCUMULATION: | ||
| 24 | // interp2d_forward — Y OVERWRITTEN. | ||
| 25 | // interp2d_backward — dX OVERWRITTEN (zero-then-scatter; resampling has no | ||
| 26 | // learnable parameters, so the adjoint overwrites dX). | ||
| 27 | // | ||
| 28 | // Identity check: if (H_out, W_out) == (H_in, W_in) and mode == 0 or 1, the | ||
| 29 | // op is the identity (within rounding) — the test relies on this. | ||
| 30 | |||
| 31 | #include <brotensor/tensor.h> | ||
| 32 | |||
| 33 | #include <cmath> | ||
| 34 | #include <stdexcept> | ||
| 35 | #include <string> | ||
| 36 | #include <vector> | ||
| 37 | |||
| 38 | namespace brotensor::detail::cpu { | ||
| 39 | |||
| 40 | namespace { | ||
| 41 | |||
| 42 | 82 | inline void check_fp32(const ::brotensor::Tensor& t, | |
| 43 | const char* op, const char* name) { | ||
| 44 |
1/2✓ Branch 0 taken 82 times.
✗ Branch 1 not taken.
|
82 | if (t.dtype != Dtype::FP32) { |
| 45 | ✗ | throw std::runtime_error(std::string("brotensor: ") + op + ": " + | |
| 46 | ✗ | name + " must be FP32 (CPU backend is " | |
| 47 | "FP32-only)"); | ||
| 48 | } | ||
| 49 | 82 | } | |
| 50 | |||
| 51 | 2602 | inline int clampi(int v, int lo, int hi) { | |
| 52 |
4/4✓ Branch 0 taken 149 times.
✓ Branch 1 taken 2453 times.
✓ Branch 2 taken 175 times.
✓ Branch 3 taken 2278 times.
|
2602 | return v < lo ? lo : (v > hi ? hi : v); |
| 53 | } | ||
| 54 | |||
| 55 | 82 | inline void check_args(const char* op, | |
| 56 | int N, int C, int H_in, int W_in, | ||
| 57 | int H_out, int W_out, int mode, | ||
| 58 | bool allow_bicubic) { | ||
| 59 |
1/2✓ Branch 0 taken 82 times.
✗ Branch 1 not taken.
|
164 | if (N < 0 || C < 0 || H_in < 0 || W_in < 0 || |
| 60 | 82 | H_out < 0 || W_out < 0) { | |
| 61 |
0/10✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
1 | throw std::runtime_error(std::string("brotensor: ") + op + |
| 62 | ": N, C, H_in, W_in, H_out, W_out must be " | ||
| 63 | "non-negative"); | ||
| 64 | } | ||
| 65 | 82 | const int max_mode = allow_bicubic ? 3 : 1; | |
| 66 |
2/2✓ Branch 0 taken 81 times.
✓ Branch 1 taken 1 time.
|
82 | if (mode < 0 || mode > max_mode) { |
| 67 | 1 | const char* msg = allow_bicubic | |
| 68 | ? ": mode must be 0 (nearest), 1 (bilinear), 2 (bicubic a=-0.5, " | ||
| 69 | "PIL), or 3 (bicubic a=-0.75, torch)" | ||
| 70 | : ": mode must be 0 (nearest) or 1 (bilinear) — bicubic " | ||
| 71 | "backward is not implemented"; | ||
| 72 |
5/10✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
|
1 | throw std::runtime_error(std::string("brotensor: ") + op + msg); |
| 73 | } | ||
| 74 |
3/6✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 81 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 81 times.
✗ Branch 5 not taken.
|
81 | if ((H_out > 0 && H_in == 0) || (W_out > 0 && W_in == 0)) { |
| 75 | ✗ | throw std::runtime_error(std::string("brotensor: ") + op + | |
| 76 | ": input spatial dims must be > 0 when " | ||
| 77 | "output spatial dims are > 0"); | ||
| 78 | } | ||
| 79 | 82 | } | |
| 80 | |||
| 81 | // Keys cubic-convolution kernel with coefficient `a`. a = -0.5 is Catmull-Rom | ||
| 82 | // (matches PIL/Pillow BICUBIC); a = -0.75 matches PyTorch | ||
| 83 | // interpolate(mode="bicubic") and OpenCV. |t| in [0,1] uses the first branch, | ||
| 84 | // |t| in [1,2] the second, otherwise 0. | ||
| 85 | 820 | inline float cubic_keys(float t, float a) { | |
| 86 |
2/2✓ Branch 0 taken 406 times.
✓ Branch 1 taken 414 times.
|
820 | const float at = t < 0.0f ? -t : t; |
| 87 |
2/2✓ Branch 0 taken 365 times.
✓ Branch 1 taken 455 times.
|
820 | if (at < 1.0f) { |
| 88 | 365 | return ((a + 2.0f) * at - (a + 3.0f)) * at * at + 1.0f; | |
| 89 | } | ||
| 90 |
2/2✓ Branch 0 taken 410 times.
✓ Branch 1 taken 45 times.
|
455 | if (at < 2.0f) { |
| 91 | // a*t^3 - 5a*t^2 + 8a*t - 4a, Horner in |t|. | ||
| 92 | 410 | return ((a * at - 5.0f * a) * at + 8.0f * a) * at - 4.0f * a; | |
| 93 | } | ||
| 94 | 45 | return 0.0f; | |
| 95 | 820 | } | |
| 96 | |||
| 97 | } // namespace | ||
| 98 | |||
| 99 | // ─── Forward ─────────────────────────────────────────────────────────────── | ||
| 100 | |||
| 101 | // Corner-aligned source coordinate: out pixel `o` maps to o*(in-1)/(out-1), | ||
| 102 | // with the degenerate out==1 case pinned to 0 (torch align_corners=True). | ||
| 103 | 185 | inline double align_corners_src(int o, int in_dim, int out_dim) { | |
| 104 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 181 times.
|
185 | if (out_dim <= 1) return 0.0; |
| 105 | 362 | return static_cast<double>(o) * static_cast<double>(in_dim - 1) / | |
| 106 | 181 | static_cast<double>(out_dim - 1); | |
| 107 | 185 | } | |
| 108 | |||
| 109 | // Shared forward worker for both the half-pixel (align_corners=False) and the | ||
| 110 | // corner-aligned (align_corners=True) resample — they differ only in the | ||
| 111 | // source-coordinate mapping, so the tap math below is identical. | ||
| 112 | 74 | static void interp2d_forward_impl(const ::brotensor::Tensor& X, | |
| 113 | int N, int C, int H_in, int W_in, | ||
| 114 | int H_out, int W_out, int mode, bool align, | ||
| 115 | ::brotensor::Tensor& Y, const char* op) { | ||
| 116 | 74 | check_fp32(X, op, "X"); | |
| 117 | 74 | check_args(op, N, C, H_in, W_in, H_out, W_out, mode, | |
| 118 | /*allow_bicubic=*/true); | ||
| 119 | |||
| 120 | 74 | const int cols = C * H_out * W_out; | |
| 121 |
4/6✓ Branch 0 taken 39 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 39 times.
|
74 | if (Y.rows != N || Y.cols != cols || Y.dtype != Dtype::FP32) { |
| 122 | 35 | Y.resize(N, cols, Dtype::FP32); | |
| 123 | 35 | } | |
| 124 |
2/4✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 74 times.
|
74 | if (N == 0 || cols == 0) return; |
| 125 | |||
| 126 | 74 | const float* Xp = X.host_f32(); | |
| 127 | 74 | float* Yp = Y.host_f32_mut(); | |
| 128 | |||
| 129 | // Half-pixel scale (unused on the align path, which reads from in-1/out-1). | ||
| 130 | 74 | const double sy = static_cast<double>(H_in) / static_cast<double>(H_out); | |
| 131 | 74 | const double sx = static_cast<double>(W_in) / static_cast<double>(W_out); | |
| 132 | |||
| 133 | // Per-row / per-column tap tables. src_y/src_x and the derived taps depend | ||
| 134 | // only on (oh) or (ow) — never on (n, c) — so compute them once here | ||
| 135 | // instead of redoing it inside the (n, c) loop below. | ||
| 136 | struct RowNearest { int iy; }; | ||
| 137 | struct ColNearest { int ix; }; | ||
| 138 | struct RowBilinear { int y0, y1; float fy; }; | ||
| 139 | struct ColBilinear { int x0, x1; float fx; }; | ||
| 140 | struct RowBicubic { int iy[4]; float wy[4]; }; | ||
| 141 | struct ColBicubic { int ix[4]; float wx[4]; }; | ||
| 142 | |||
| 143 | 74 | std::vector<RowNearest> row_nearest; | |
| 144 | 74 | std::vector<ColNearest> col_nearest; | |
| 145 | 74 | std::vector<RowBilinear> row_bilinear; | |
| 146 | 74 | std::vector<ColBilinear> col_bilinear; | |
| 147 | 74 | std::vector<RowBicubic> row_bicubic; | |
| 148 | 74 | std::vector<ColBicubic> col_bicubic; | |
| 149 | |||
| 150 |
2/2✓ Branch 0 taken 66 times.
✓ Branch 1 taken 8 times.
|
74 | if (mode == 0) { |
| 151 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | row_nearest.resize(H_out); |
| 152 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 62 times.
|
70 | for (int oh = 0; oh < H_out; ++oh) { |
| 153 |
3/4✓ Branch 0 taken 14 times.
✓ Branch 1 taken 48 times.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
|
62 | const double src_y = align ? align_corners_src(oh, H_in, H_out) |
| 154 | 48 | : (oh + 0.5) * sy - 0.5; | |
| 155 |
1/2✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
|
62 | row_nearest[oh].iy = clampi( |
| 156 | 62 | static_cast<int>(std::nearbyint(src_y)), 0, H_in - 1); | |
| 157 | 62 | } | |
| 158 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | col_nearest.resize(W_out); |
| 159 |
2/2✓ Branch 0 taken 76 times.
✓ Branch 1 taken 8 times.
|
84 | for (int ow = 0; ow < W_out; ++ow) { |
| 160 |
3/4✓ Branch 0 taken 17 times.
✓ Branch 1 taken 59 times.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
|
76 | const double src_x = align ? align_corners_src(ow, W_in, W_out) |
| 161 | 59 | : (ow + 0.5) * sx - 0.5; | |
| 162 |
1/2✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
|
76 | col_nearest[ow].ix = clampi( |
| 163 | 76 | static_cast<int>(std::nearbyint(src_x)), 0, W_in - 1); | |
| 164 | 76 | } | |
| 165 |
2/2✓ Branch 0 taken 57 times.
✓ Branch 1 taken 9 times.
|
74 | } else if (mode == 1) { |
| 166 |
1/2✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
|
57 | row_bilinear.resize(H_out); |
| 167 |
2/2✓ Branch 0 taken 57 times.
✓ Branch 1 taken 321 times.
|
378 | for (int oh = 0; oh < H_out; ++oh) { |
| 168 |
3/4✓ Branch 0 taken 41 times.
✓ Branch 1 taken 280 times.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
|
321 | const double src_y = align ? align_corners_src(oh, H_in, H_out) |
| 169 | 280 | : (oh + 0.5) * sy - 0.5; | |
| 170 | 321 | const int y0 = static_cast<int>(std::floor(src_y)); | |
| 171 | 321 | row_bilinear[oh].fy = static_cast<float>(src_y - y0); | |
| 172 |
1/2✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
|
321 | row_bilinear[oh].y0 = clampi(y0, 0, H_in - 1); |
| 173 |
1/2✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
|
321 | row_bilinear[oh].y1 = clampi(y0 + 1, 0, H_in - 1); |
| 174 | 321 | } | |
| 175 |
1/2✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
|
57 | col_bilinear.resize(W_out); |
| 176 |
2/2✓ Branch 0 taken 388 times.
✓ Branch 1 taken 57 times.
|
445 | for (int ow = 0; ow < W_out; ++ow) { |
| 177 |
3/4✓ Branch 0 taken 61 times.
✓ Branch 1 taken 327 times.
✓ Branch 2 taken 61 times.
✗ Branch 3 not taken.
|
388 | const double src_x = align ? align_corners_src(ow, W_in, W_out) |
| 178 | 327 | : (ow + 0.5) * sx - 0.5; | |
| 179 | 388 | const int x0 = static_cast<int>(std::floor(src_x)); | |
| 180 | 388 | col_bilinear[ow].fx = static_cast<float>(src_x - x0); | |
| 181 |
1/2✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
|
388 | col_bilinear[ow].x0 = clampi(x0, 0, W_in - 1); |
| 182 |
1/2✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
|
388 | col_bilinear[ow].x1 = clampi(x0 + 1, 0, W_in - 1); |
| 183 | 388 | } | |
| 184 | 57 | } else { | |
| 185 | // bicubic — 4x4 cubic-convolution, border-clamped. | ||
| 186 | // mode 2: a=-0.5 (PIL); mode 3: a=-0.75 (torch). | ||
| 187 | 9 | const float a = (mode == 3) ? -0.75f : -0.5f; | |
| 188 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | row_bicubic.resize(H_out); |
| 189 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 89 times.
|
98 | for (int oh = 0; oh < H_out; ++oh) { |
| 190 |
3/4✓ Branch 0 taken 24 times.
✓ Branch 1 taken 65 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
|
89 | const double src_y = align ? align_corners_src(oh, H_in, H_out) |
| 191 | 65 | : (oh + 0.5) * sy - 0.5; | |
| 192 | 89 | const int y0 = static_cast<int>(std::floor(src_y)); | |
| 193 | 89 | const float fy = static_cast<float>(src_y - y0); | |
| 194 |
2/2✓ Branch 0 taken 356 times.
✓ Branch 1 taken 89 times.
|
445 | for (int k = 0; k < 4; ++k) { |
| 195 |
1/2✓ Branch 0 taken 356 times.
✗ Branch 1 not taken.
|
356 | row_bicubic[oh].wy[k] = cubic_keys(fy - (k - 1), a); |
| 196 |
1/2✓ Branch 0 taken 356 times.
✗ Branch 1 not taken.
|
356 | row_bicubic[oh].iy[k] = clampi(y0 + k - 1, 0, H_in - 1); |
| 197 | 356 | } | |
| 198 | 89 | } | |
| 199 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | col_bicubic.resize(W_out); |
| 200 |
2/2✓ Branch 0 taken 116 times.
✓ Branch 1 taken 9 times.
|
125 | for (int ow = 0; ow < W_out; ++ow) { |
| 201 |
3/4✓ Branch 0 taken 28 times.
✓ Branch 1 taken 88 times.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
|
116 | const double src_x = align ? align_corners_src(ow, W_in, W_out) |
| 202 | 88 | : (ow + 0.5) * sx - 0.5; | |
| 203 | 116 | const int x0 = static_cast<int>(std::floor(src_x)); | |
| 204 | 116 | const float fx = static_cast<float>(src_x - x0); | |
| 205 |
2/2✓ Branch 0 taken 464 times.
✓ Branch 1 taken 116 times.
|
580 | for (int k = 0; k < 4; ++k) { |
| 206 |
1/2✓ Branch 0 taken 464 times.
✗ Branch 1 not taken.
|
464 | col_bicubic[ow].wx[k] = cubic_keys(fx - (k - 1), a); |
| 207 |
1/2✓ Branch 0 taken 464 times.
✗ Branch 1 not taken.
|
464 | col_bicubic[ow].ix[k] = clampi(x0 + k - 1, 0, W_in - 1); |
| 208 | 464 | } | |
| 209 | 116 | } | |
| 210 | } | ||
| 211 | |||
| 212 |
2/2✓ Branch 0 taken 92 times.
✓ Branch 1 taken 74 times.
|
166 | for (int n = 0; n < N; ++n) { |
| 213 |
2/2✓ Branch 0 taken 192 times.
✓ Branch 1 taken 92 times.
|
284 | for (int c = 0; c < C; ++c) { |
| 214 | 192 | const int xbase = (n * C + c) * H_in * W_in; | |
| 215 | 192 | const int ybase = (n * C + c) * H_out * W_out; | |
| 216 | |||
| 217 |
2/2✓ Branch 0 taken 1895 times.
✓ Branch 1 taken 192 times.
|
2087 | for (int oh = 0; oh < H_out; ++oh) { |
| 218 |
2/2✓ Branch 0 taken 23937 times.
✓ Branch 1 taken 1895 times.
|
25832 | for (int ow = 0; ow < W_out; ++ow) { |
| 219 |
2/2✓ Branch 0 taken 20955 times.
✓ Branch 1 taken 2982 times.
|
23937 | if (mode == 0) { |
| 220 | // nearest — round_half_to_even then clamp. | ||
| 221 | 2982 | const int iy = row_nearest[oh].iy; | |
| 222 | 2982 | const int ix = col_nearest[ow].ix; | |
| 223 | 2982 | Yp[ybase + oh * W_out + ow] = | |
| 224 | 2982 | Xp[xbase + iy * W_in + ix]; | |
| 225 |
2/2✓ Branch 0 taken 14795 times.
✓ Branch 1 taken 6160 times.
|
23937 | } else if (mode == 1) { |
| 226 | // bilinear — 2x2 tap, border-clamped. | ||
| 227 | 14795 | const RowBilinear& r = row_bilinear[oh]; | |
| 228 | 14795 | const ColBilinear& cx = col_bilinear[ow]; | |
| 229 | 14795 | const float v00 = Xp[xbase + r.y0 * W_in + cx.x0]; | |
| 230 | 14795 | const float v01 = Xp[xbase + r.y0 * W_in + cx.x1]; | |
| 231 | 14795 | const float v10 = Xp[xbase + r.y1 * W_in + cx.x0]; | |
| 232 | 14795 | const float v11 = Xp[xbase + r.y1 * W_in + cx.x1]; | |
| 233 | 14795 | const float top = v00 + (v01 - v00) * cx.fx; | |
| 234 | 14795 | const float bot = v10 + (v11 - v10) * cx.fx; | |
| 235 | 14795 | Yp[ybase + oh * W_out + ow] = | |
| 236 | 14795 | top + (bot - top) * r.fy; | |
| 237 | 14795 | } else { | |
| 238 | // bicubic — 4x4 cubic-convolution, border-clamped. | ||
| 239 | 6160 | const RowBicubic& r = row_bicubic[oh]; | |
| 240 | 6160 | const ColBicubic& cx = col_bicubic[ow]; | |
| 241 | 6160 | float acc = 0.0f; | |
| 242 |
2/2✓ Branch 0 taken 24640 times.
✓ Branch 1 taken 6160 times.
|
30800 | for (int j = 0; j < 4; ++j) { |
| 243 | 24640 | float row = 0.0f; | |
| 244 |
2/2✓ Branch 0 taken 98560 times.
✓ Branch 1 taken 24640 times.
|
123200 | for (int i = 0; i < 4; ++i) { |
| 245 | 98560 | row += cx.wx[i] * Xp[xbase + r.iy[j] * W_in + cx.ix[i]]; | |
| 246 | 98560 | } | |
| 247 | 24640 | acc += r.wy[j] * row; | |
| 248 | 24640 | } | |
| 249 | 6160 | Yp[ybase + oh * W_out + ow] = acc; | |
| 250 | } | ||
| 251 | 23937 | } | |
| 252 | 1895 | } | |
| 253 | 192 | } | |
| 254 | 92 | } | |
| 255 | 74 | } | |
| 256 | |||
| 257 | 62 | void interp2d_forward(const ::brotensor::Tensor& X, | |
| 258 | int N, int C, int H_in, int W_in, | ||
| 259 | int H_out, int W_out, int mode, | ||
| 260 | ::brotensor::Tensor& Y) { | ||
| 261 | 124 | interp2d_forward_impl(X, N, C, H_in, W_in, H_out, W_out, mode, | |
| 262 | 62 | /*align=*/false, Y, "interp2d_forward"); | |
| 263 | 62 | } | |
| 264 | |||
| 265 | 12 | void interp2d_align_corners_forward(const ::brotensor::Tensor& X, | |
| 266 | int N, int C, int H_in, int W_in, | ||
| 267 | int H_out, int W_out, int mode, | ||
| 268 | ::brotensor::Tensor& Y) { | ||
| 269 | 24 | interp2d_forward_impl(X, N, C, H_in, W_in, H_out, W_out, mode, | |
| 270 | 12 | /*align=*/true, Y, "interp2d_align_corners_forward"); | |
| 271 | 12 | } | |
| 272 | |||
| 273 | // ─── Backward ────────────────────────────────────────────────────────────── | ||
| 274 | |||
| 275 | 7 | void interp2d_backward(const ::brotensor::Tensor& dY, | |
| 276 | int N, int C, int H_in, int W_in, | ||
| 277 | int H_out, int W_out, int mode, | ||
| 278 | ::brotensor::Tensor& dX) { | ||
| 279 | 7 | check_fp32(dY, "interp2d_backward", "dY"); | |
| 280 | 7 | check_args("interp2d_backward", N, C, H_in, W_in, H_out, W_out, mode, | |
| 281 | /*allow_bicubic=*/false); | ||
| 282 | |||
| 283 | 7 | const int cols_in = C * H_in * W_in; | |
| 284 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
7 | if (dX.rows != N || dX.cols != cols_in || dX.dtype != Dtype::FP32) { |
| 285 | 7 | dX.resize(N, cols_in, Dtype::FP32); | |
| 286 | 7 | } | |
| 287 |
2/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
|
7 | if (N == 0 || cols_in == 0) return; |
| 288 | |||
| 289 | 7 | const float* dYp = dY.host_f32(); | |
| 290 | 7 | float* dXp = dX.host_f32_mut(); | |
| 291 | |||
| 292 | // Adjoint: zero dX, then scatter each output gradient onto the input | ||
| 293 | // position(s) it sampled — with the same weights as the forward pass. | ||
| 294 | 7 | const int total_in = N * cols_in; | |
| 295 |
2/2✓ Branch 0 taken 7060 times.
✓ Branch 1 taken 7 times.
|
7067 | for (int i = 0; i < total_in; ++i) dXp[i] = 0.0f; |
| 296 | |||
| 297 |
2/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
|
7 | if (H_out == 0 || W_out == 0) return; |
| 298 | |||
| 299 | 7 | const double sy = static_cast<double>(H_in) / static_cast<double>(H_out); | |
| 300 | 7 | const double sx = static_cast<double>(W_in) / static_cast<double>(W_out); | |
| 301 | |||
| 302 | // Per-row / per-column tap tables — same rationale as the forward pass: | ||
| 303 | // src_y/src_x and the derived taps depend only on (oh)/(ow), not (n, c). | ||
| 304 | struct RowNearest { int iy; }; | ||
| 305 | struct ColNearest { int ix; }; | ||
| 306 | struct RowBilinear { int y0, y1; float fy; }; | ||
| 307 | struct ColBilinear { int x0, x1; float fx; }; | ||
| 308 | |||
| 309 | 7 | std::vector<RowNearest> row_nearest; | |
| 310 | 7 | std::vector<ColNearest> col_nearest; | |
| 311 | 7 | std::vector<RowBilinear> row_bilinear; | |
| 312 | 7 | std::vector<ColBilinear> col_bilinear; | |
| 313 | |||
| 314 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
|
7 | if (mode == 0) { |
| 315 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | row_nearest.resize(H_out); |
| 316 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 21 times.
|
24 | for (int oh = 0; oh < H_out; ++oh) { |
| 317 | 21 | const double src_y = (oh + 0.5) * sy - 0.5; | |
| 318 |
1/2✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
|
21 | row_nearest[oh].iy = clampi( |
| 319 | 21 | static_cast<int>(std::nearbyint(src_y)), 0, H_in - 1); | |
| 320 | 21 | } | |
| 321 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | col_nearest.resize(W_out); |
| 322 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 23 times.
|
26 | for (int ow = 0; ow < W_out; ++ow) { |
| 323 | 23 | const double src_x = (ow + 0.5) * sx - 0.5; | |
| 324 |
1/2✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
|
23 | col_nearest[ow].ix = clampi( |
| 325 | 23 | static_cast<int>(std::nearbyint(src_x)), 0, W_in - 1); | |
| 326 | 23 | } | |
| 327 | 3 | } else { | |
| 328 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | row_bilinear.resize(H_out); |
| 329 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 46 times.
|
50 | for (int oh = 0; oh < H_out; ++oh) { |
| 330 | 46 | const double src_y = (oh + 0.5) * sy - 0.5; | |
| 331 | 46 | const int y0 = static_cast<int>(std::floor(src_y)); | |
| 332 | 46 | row_bilinear[oh].fy = static_cast<float>(src_y - y0); | |
| 333 |
1/2✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
|
46 | row_bilinear[oh].y0 = clampi(y0, 0, H_in - 1); |
| 334 |
1/2✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
|
46 | row_bilinear[oh].y1 = clampi(y0 + 1, 0, H_in - 1); |
| 335 | 46 | } | |
| 336 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | col_bilinear.resize(W_out); |
| 337 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 45 times.
|
49 | for (int ow = 0; ow < W_out; ++ow) { |
| 338 | 45 | const double src_x = (ow + 0.5) * sx - 0.5; | |
| 339 | 45 | const int x0 = static_cast<int>(std::floor(src_x)); | |
| 340 | 45 | col_bilinear[ow].fx = static_cast<float>(src_x - x0); | |
| 341 |
1/2✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
|
45 | col_bilinear[ow].x0 = clampi(x0, 0, W_in - 1); |
| 342 |
1/2✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
|
45 | col_bilinear[ow].x1 = clampi(x0 + 1, 0, W_in - 1); |
| 343 | 45 | } | |
| 344 | } | ||
| 345 | |||
| 346 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 7 times.
|
21 | for (int n = 0; n < N; ++n) { |
| 347 |
2/2✓ Branch 0 taken 50 times.
✓ Branch 1 taken 14 times.
|
64 | for (int c = 0; c < C; ++c) { |
| 348 | 50 | const int xbase = (n * C + c) * H_in * W_in; | |
| 349 | 50 | const int ybase = (n * C + c) * H_out * W_out; | |
| 350 | |||
| 351 |
2/2✓ Branch 0 taken 751 times.
✓ Branch 1 taken 50 times.
|
801 | for (int oh = 0; oh < H_out; ++oh) { |
| 352 |
2/2✓ Branch 0 taken 11344 times.
✓ Branch 1 taken 751 times.
|
12095 | for (int ow = 0; ow < W_out; ++ow) { |
| 353 | 11344 | const float g = dYp[ybase + oh * W_out + ow]; | |
| 354 | |||
| 355 |
2/2✓ Branch 0 taken 10650 times.
✓ Branch 1 taken 694 times.
|
11344 | if (mode == 0) { |
| 356 | 694 | const int iy = row_nearest[oh].iy; | |
| 357 | 694 | const int ix = col_nearest[ow].ix; | |
| 358 | 694 | dXp[xbase + iy * W_in + ix] += g; | |
| 359 | 694 | } else { | |
| 360 | 10650 | const RowBilinear& r = row_bilinear[oh]; | |
| 361 | 10650 | const ColBilinear& cx = col_bilinear[ow]; | |
| 362 | 10650 | const float w00 = (1.0f - r.fy) * (1.0f - cx.fx); | |
| 363 | 10650 | const float w01 = (1.0f - r.fy) * cx.fx; | |
| 364 | 10650 | const float w10 = r.fy * (1.0f - cx.fx); | |
| 365 | 10650 | const float w11 = r.fy * cx.fx; | |
| 366 | 10650 | dXp[xbase + r.y0 * W_in + cx.x0] += w00 * g; | |
| 367 | 10650 | dXp[xbase + r.y0 * W_in + cx.x1] += w01 * g; | |
| 368 | 10650 | dXp[xbase + r.y1 * W_in + cx.x0] += w10 * g; | |
| 369 | 10650 | dXp[xbase + r.y1 * W_in + cx.x1] += w11 * g; | |
| 370 | } | ||
| 371 | 11344 | } | |
| 372 | 751 | } | |
| 373 | 50 | } | |
| 374 | 14 | } | |
| 375 | 7 | } | |
| 376 | |||
| 377 | } // namespace brotensor::detail::cpu | ||
| 378 |