GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 98.5% 201 / 0 / 204
Functions: 100.0% 8 / 0 / 8
Branches: 57.1% 97 / 0 / 170

src/cpu/resample.cpp
Line Branch Exec Source
1 // ─── CPU resample ops (CHUNK 4) ────────────────────────────────────────────
2 //
3 // FP32 scalar host implementations. Ports src/cuda/resample.cu — FP32 path
4 // only (CPU is FP32-only). 2x spatial nearest / bilinear upsample, 2x average
5 // downsample, plus their backward passes. All tensors NCHW row-major.
6 //
7 // Memory layout (matches the GPU exactly):
8 // X / Y / dX / dY : NCHW — ((n*C + c) * H + h) * W + w
9 // Upsample: H_out = 2*H, W_out = 2*W.
10 // Downsample: H_out = H/2, W_out = W/2 (H, W must be even).
11 //
12 // Bilinear sampling convention (matches the GPU kernel verbatim — HALF-PIXEL,
13 // NOT align-corners):
14 // src_y = (oh + 0.5) * 0.5 - 0.5 (scale 0.5 because output is 2x input)
15 // src_x = (ow + 0.5) * 0.5 - 0.5
16 // y0 = floor(src_y), fy = src_y - y0 (then border-clamped indices)
17 // value = bilinear interp of the 4 clamped neighbours.
18 //
19 // ACCUMULATION (matches the GPU kernels):
20 // upsample_nearest_2x — Y OVERWRITTEN.
21 // upsample_bilinear_2x — Y OVERWRITTEN.
22 // downsample_avg_2x — Y OVERWRITTEN.
23 // upsample_nearest_2x_backward — dX OVERWRITTEN (gather of 4 dY values).
24 // upsample_bilinear_2x_backward— dX OVERWRITTEN (GPU memsets dX to 0 then
25 // atomic-scatters; net effect = overwrite).
26 // downsample_avg_2x_backward — dX OVERWRITTEN.
27
28 #include <brotensor/tensor.h>
29
30 #include <cmath>
31 #include <stdexcept>
32 #include <string>
33 #include <vector>
34
35 namespace brotensor::detail::cpu {
36
37 namespace {
38
39 41 inline void check_fp32(const ::brotensor::Tensor& t,
40 const char* op, const char* name) {
41
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 if (t.dtype != Dtype::FP32) {
42 throw std::runtime_error(std::string(op) + ": " + name +
43 " must be FP32 (CPU backend is FP32-only)");
44 }
45 41 }
46
47 692 inline int clampi(int v, int lo, int hi) {
48
4/4
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 664 times.
✓ Branch 2 taken 28 times.
✓ Branch 3 taken 636 times.
692 return v < lo ? lo : (v > hi ? hi : v);
49 }
50
51 } // namespace
52
53 // ─── Forward ───────────────────────────────────────────────────────────────
54
55 8 void upsample_nearest_2x(const ::brotensor::Tensor& X,
56 int N, int C, int H, int W,
57 ::brotensor::Tensor& Y) {
58 8 check_fp32(X, "upsample_nearest_2x", "X");
59 8 const int H_out = 2 * H, W_out = 2 * W;
60 8 const int cols = C * H_out * W_out;
61
1/6
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
8 if (Y.rows != N || Y.cols != cols || Y.dtype != Dtype::FP32) {
62 8 Y.resize(N, cols, Dtype::FP32);
63 8 }
64
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (N == 0 || cols == 0) return;
65
66 8 const float* Xp = X.host_f32();
67 8 float* Yp = Y.host_f32_mut();
68
69
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 13 times.
21 for (int n = 0; n < N; ++n) {
70
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 13 times.
73 for (int c = 0; c < C; ++c) {
71
2/2
✓ Branch 0 taken 736 times.
✓ Branch 1 taken 60 times.
796 for (int oh = 0; oh < H_out; ++oh) {
72 736 const int ih = oh / 2;
73
2/2
✓ Branch 0 taken 11872 times.
✓ Branch 1 taken 736 times.
12608 for (int ow = 0; ow < W_out; ++ow) {
74 11872 const int iw = ow / 2;
75 11872 Yp[((n * C + c) * H_out + oh) * W_out + ow] =
76 11872 Xp[((n * C + c) * H + ih) * W + iw];
77 11872 }
78 736 }
79 60 }
80 13 }
81 8 }
82
83 8 void upsample_bilinear_2x(const ::brotensor::Tensor& X,
84 int N, int C, int H, int W,
85 ::brotensor::Tensor& Y) {
86 8 check_fp32(X, "upsample_bilinear_2x", "X");
87 8 const int H_out = 2 * H, W_out = 2 * W;
88 8 const int cols = C * H_out * W_out;
89
1/6
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
8 if (Y.rows != N || Y.cols != cols || Y.dtype != Dtype::FP32) {
90 8 Y.resize(N, cols, Dtype::FP32);
91 8 }
92
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (N == 0 || cols == 0) return;
93
94 8 const float* Xp = X.host_f32();
95 8 float* Yp = Y.host_f32_mut();
96
97 // src_y/src_x and the derived taps depend only on (oh)/(ow), never on
98 // (n, c) — precompute once instead of redoing it inside the (n, c) loop.
99 struct RowTap { int y0, y1; float fy; };
100 struct ColTap { int x0, x1; float fx; };
101 8 std::vector<RowTap> row_tap(H_out);
102
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 88 times.
96 for (int oh = 0; oh < H_out; ++oh) {
103 88 const float src_y = (oh + 0.5f) * 0.5f - 0.5f;
104 88 const int y0 = static_cast<int>(std::floor(src_y));
105 88 row_tap[oh].fy = src_y - y0;
106
1/2
✓ Branch 0 taken 88 times.
✗ Branch 1 not taken.
88 row_tap[oh].y0 = clampi(y0, 0, H - 1);
107
1/2
✓ Branch 0 taken 88 times.
✗ Branch 1 not taken.
88 row_tap[oh].y1 = clampi(y0 + 1, 0, H - 1);
108 88 }
109
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::vector<ColTap> col_tap(W_out);
110
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 106 times.
114 for (int ow = 0; ow < W_out; ++ow) {
111 106 const float src_x = (ow + 0.5f) * 0.5f - 0.5f;
112 106 const int x0 = static_cast<int>(std::floor(src_x));
113 106 col_tap[ow].fx = src_x - x0;
114
1/2
✓ Branch 0 taken 106 times.
✗ Branch 1 not taken.
106 col_tap[ow].x0 = clampi(x0, 0, W - 1);
115
1/2
✓ Branch 0 taken 106 times.
✗ Branch 1 not taken.
106 col_tap[ow].x1 = clampi(x0 + 1, 0, W - 1);
116 106 }
117
118
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 8 times.
21 for (int n = 0; n < N; ++n) {
119
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 13 times.
73 for (int c = 0; c < C; ++c) {
120 60 const int base = (n * C + c) * H;
121
2/2
✓ Branch 0 taken 736 times.
✓ Branch 1 taken 60 times.
796 for (int oh = 0; oh < H_out; ++oh) {
122 736 const RowTap& r = row_tap[oh];
123
2/2
✓ Branch 0 taken 11872 times.
✓ Branch 1 taken 736 times.
12608 for (int ow = 0; ow < W_out; ++ow) {
124 11872 const ColTap& cx = col_tap[ow];
125 11872 const float v00 = Xp[(base + r.y0) * W + cx.x0];
126 11872 const float v01 = Xp[(base + r.y0) * W + cx.x1];
127 11872 const float v10 = Xp[(base + r.y1) * W + cx.x0];
128 11872 const float v11 = Xp[(base + r.y1) * W + cx.x1];
129 11872 const float top = v00 + (v01 - v00) * cx.fx;
130 11872 const float bot = v10 + (v11 - v10) * cx.fx;
131 11872 Yp[((n * C + c) * H_out + oh) * W_out + ow] =
132 11872 top + (bot - top) * r.fy;
133 11872 }
134 736 }
135 60 }
136 13 }
137 8 }
138
139 7 void downsample_avg_2x(const ::brotensor::Tensor& X,
140 int N, int C, int H, int W,
141 ::brotensor::Tensor& Y) {
142 7 check_fp32(X, "downsample_avg_2x", "X");
143
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if ((H & 1) || (W & 1)) {
144 throw std::runtime_error("downsample_avg_2x: H and W must be even");
145 }
146 7 const int H_out = H / 2, W_out = W / 2;
147 7 const int cols = C * H_out * W_out;
148
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 (Y.rows != N || Y.cols != cols || Y.dtype != Dtype::FP32) {
149 7 Y.resize(N, cols, Dtype::FP32);
150 7 }
151
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 == 0) return;
152
153 7 const float* Xp = X.host_f32();
154 7 float* Yp = Y.host_f32_mut();
155
156
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 11 times.
18 for (int n = 0; n < N; ++n) {
157
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 11 times.
65 for (int c = 0; c < C; ++c) {
158
2/2
✓ Branch 0 taken 172 times.
✓ Branch 1 taken 54 times.
226 for (int oh = 0; oh < H_out; ++oh) {
159 172 const int ih = oh * 2;
160
2/2
✓ Branch 0 taken 712 times.
✓ Branch 1 taken 172 times.
884 for (int ow = 0; ow < W_out; ++ow) {
161 712 const int iw = ow * 2;
162 712 const int b = ((n * C + c) * H + ih) * W + iw;
163 712 Yp[((n * C + c) * H_out + oh) * W_out + ow] =
164 2136 0.25f * (Xp[b] + Xp[b + 1] +
165 1424 Xp[b + W] + Xp[b + W + 1]);
166 712 }
167 172 }
168 54 }
169 11 }
170 7 }
171
172 // ─── Backward ──────────────────────────────────────────────────────────────
173
174 6 void upsample_nearest_2x_backward(const ::brotensor::Tensor& dY,
175 int N, int C, int H, int W,
176 ::brotensor::Tensor& dX) {
177 6 check_fp32(dY, "upsample_nearest_2x_backward", "dY");
178 6 const int H_out = 2 * H, W_out = 2 * W;
179 6 const int cols_in = C * H * W;
180
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 (dX.rows != N || dX.cols != cols_in || dX.dtype != Dtype::FP32) {
181 6 dX.resize(N, cols_in, Dtype::FP32);
182 6 }
183
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 if (N == 0 || cols_in == 0) return;
184
185 6 const float* dYp = dY.host_f32();
186 6 float* dXp = dX.host_f32_mut();
187
188 // One accumulation per input pixel; gather the 4 contributing dY values.
189
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 10 times.
16 for (int n = 0; n < N; ++n) {
190
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 10 times.
56 for (int c = 0; c < C; ++c) {
191
2/2
✓ Branch 0 taken 312 times.
✓ Branch 1 taken 46 times.
358 for (int ih = 0; ih < H; ++ih) {
192
2/2
✓ Branch 0 taken 2592 times.
✓ Branch 1 taken 312 times.
2904 for (int iw = 0; iw < W; ++iw) {
193 2592 float s = 0.0f;
194
2/2
✓ Branch 0 taken 5184 times.
✓ Branch 1 taken 2592 times.
7776 for (int a = 0; a < 2; ++a) {
195
2/2
✓ Branch 0 taken 10368 times.
✓ Branch 1 taken 5184 times.
15552 for (int b = 0; b < 2; ++b) {
196 31104 s += dYp[((n * C + c) * H_out + 2 * ih + a) *
197 20736 W_out + 2 * iw + b];
198 10368 }
199 5184 }
200 2592 dXp[((n * C + c) * H + ih) * W + iw] = s; // overwrite
201 2592 }
202 312 }
203 46 }
204 10 }
205 6 }
206
207 6 void upsample_bilinear_2x_backward(const ::brotensor::Tensor& dY,
208 int N, int C, int H, int W,
209 ::brotensor::Tensor& dX) {
210 6 check_fp32(dY, "upsample_bilinear_2x_backward", "dY");
211 6 const int H_out = 2 * H, W_out = 2 * W;
212 6 const int cols_in = C * H * W;
213
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 (dX.rows != N || dX.cols != cols_in || dX.dtype != Dtype::FP32) {
214 6 dX.resize(N, cols_in, Dtype::FP32);
215 6 }
216
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (N == 0 || cols_in == 0) return;
217
218 6 const float* dYp = dY.host_f32();
219 6 float* dXp = dX.host_f32_mut();
220
221 // GPU memsets dX to 0 then atomic-scatters — net effect is OVERWRITE.
222 6 const int total_in = N * cols_in;
223
2/2
✓ Branch 0 taken 2592 times.
✓ Branch 1 taken 6 times.
2598 for (int i = 0; i < total_in; ++i) dXp[i] = 0.0f;
224
225 // Same per-row / per-column tap hoist as the forward pass.
226 struct RowTap { int y0, y1; float fy; };
227 struct ColTap { int x0, x1; float fx; };
228 6 std::vector<RowTap> row_tap(H_out);
229
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 72 times.
78 for (int oh = 0; oh < H_out; ++oh) {
230 72 const float src_y = (oh + 0.5f) * 0.5f - 0.5f;
231 72 const int y0 = static_cast<int>(std::floor(src_y));
232 72 row_tap[oh].fy = src_y - y0;
233
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 row_tap[oh].y0 = clampi(y0, 0, H - 1);
234
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 row_tap[oh].y1 = clampi(y0 + 1, 0, H - 1);
235 72 }
236
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::vector<ColTap> col_tap(W_out);
237
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 80 times.
86 for (int ow = 0; ow < W_out; ++ow) {
238 80 const float src_x = (ow + 0.5f) * 0.5f - 0.5f;
239 80 const int x0 = static_cast<int>(std::floor(src_x));
240 80 col_tap[ow].fx = src_x - x0;
241
1/2
✓ Branch 0 taken 80 times.
✗ Branch 1 not taken.
80 col_tap[ow].x0 = clampi(x0, 0, W - 1);
242
1/2
✓ Branch 0 taken 80 times.
✗ Branch 1 not taken.
80 col_tap[ow].x1 = clampi(x0 + 1, 0, W - 1);
243 80 }
244
245
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 6 times.
16 for (int n = 0; n < N; ++n) {
246
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 10 times.
56 for (int c = 0; c < C; ++c) {
247 46 const int base = (n * C + c) * H;
248
2/2
✓ Branch 0 taken 624 times.
✓ Branch 1 taken 46 times.
670 for (int oh = 0; oh < H_out; ++oh) {
249 624 const RowTap& r = row_tap[oh];
250
2/2
✓ Branch 0 taken 10368 times.
✓ Branch 1 taken 624 times.
10992 for (int ow = 0; ow < W_out; ++ow) {
251 10368 const ColTap& cx = col_tap[ow];
252 10368 const float w00 = (1.0f - r.fy) * (1.0f - cx.fx);
253 10368 const float w01 = (1.0f - r.fy) * cx.fx;
254 10368 const float w10 = r.fy * (1.0f - cx.fx);
255 10368 const float w11 = r.fy * cx.fx;
256 10368 const float g =
257 10368 dYp[((n * C + c) * H_out + oh) * W_out + ow];
258 10368 dXp[(base + r.y0) * W + cx.x0] += w00 * g;
259 10368 dXp[(base + r.y0) * W + cx.x1] += w01 * g;
260 10368 dXp[(base + r.y1) * W + cx.x0] += w10 * g;
261 10368 dXp[(base + r.y1) * W + cx.x1] += w11 * g;
262 10368 }
263 624 }
264 46 }
265 10 }
266 6 }
267
268 6 void downsample_avg_2x_backward(const ::brotensor::Tensor& dY,
269 int N, int C, int H, int W,
270 ::brotensor::Tensor& dX) {
271 6 check_fp32(dY, "downsample_avg_2x_backward", "dY");
272
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if ((H & 1) || (W & 1)) {
273 throw std::runtime_error("downsample_avg_2x_backward: H and W must be even");
274 }
275 6 const int H_out = H / 2, W_out = W / 2;
276 6 const int cols_in = C * H * W;
277
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 (dX.rows != N || dX.cols != cols_in || dX.dtype != Dtype::FP32) {
278 6 dX.resize(N, cols_in, Dtype::FP32);
279 6 }
280
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (N == 0 || cols_in == 0) return;
281
282 6 const float* dYp = dY.host_f32();
283 6 float* dXp = dX.host_f32_mut();
284
285 // One read per input pixel; each maps to a single output pixel, scale 1/4.
286
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 10 times.
16 for (int n = 0; n < N; ++n) {
287
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 10 times.
56 for (int c = 0; c < C; ++c) {
288
2/2
✓ Branch 0 taken 312 times.
✓ Branch 1 taken 46 times.
358 for (int ih = 0; ih < H; ++ih) {
289 312 const int oh = ih / 2;
290
2/2
✓ Branch 0 taken 2592 times.
✓ Branch 1 taken 312 times.
2904 for (int iw = 0; iw < W; ++iw) {
291 2592 const int ow = iw / 2;
292 2592 dXp[((n * C + c) * H + ih) * W + iw] =
293 2592 0.25f * dYp[((n * C + c) * H_out + oh) * W_out + ow];
294 2592 }
295 312 }
296 46 }
297 10 }
298 6 }
299
300 } // namespace brotensor::detail::cpu
301