GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.3% 266 / 0 / 285
Functions: 92.9% 13 / 0 / 14
Branches: 58.9% 146 / 0 / 248

src/cpu/conv_transpose2d.cpp
Line Branch Exec Source
1 // ─── CPU 2D transposed convolution ──────────────────────────────────────────
2 //
3 // FP32 scalar host implementations of:
4 // conv_transpose2d_forward / _backward_input / _backward_weight / _backward_bias
5 //
6 // The 2D counterpart of conv_transpose1d in conv1d.cpp, with H and W
7 // independently rescaled. The "learned upsample" primitive — SAM's mask
8 // decoder uses two 4x conv-transposes back-to-back; DPT depth heads use
9 // a 2x conv-transpose; many segmentation decoders rely on these instead
10 // of the cheaper bilinear upsample.
11 //
12 // ── Layout (NCHW) ───────────────────────────────────────────────────────────
13 // X / dX : (N, C_in*H*W) flat index ((n*C_in + c)*H + h)*W + w
14 // Y / dY : (N, C_out*H_out*W_out)
15 // Wt / dWt: (C_in, (C_out/groups)*kH*kW) — input-channel-major
16 // flat index (c_in*Cg_out + oc_local) * (kH*kW) + (kh*kW + kw)
17 // bias : (C_out, 1) or null
18 //
19 // ── Accumulation (matches the conv2d / conv_transpose1d contract) ──────────
20 // *_forward / *_backward_input — output OVERWRITTEN.
21 // _backward_weight / _bias — dWt / dB ACCUMULATE (+=); caller zeros
22 // them first.
23 //
24 // Output spatial dims (torch ConvTranspose2d):
25 // H_out = (H-1)*stride_h - 2*pad_h + dil_h*(kH-1) + output_padding_h + 1
26 // W_out = (W-1)*stride_w - 2*pad_w + dil_w*(kW-1) + output_padding_w + 1
27
28 #include <brotensor/tensor.h>
29 #include <brotensor/detail/cpu/thread_pool.h>
30
31 #include <cstddef>
32 #include <stdexcept>
33 #include <string>
34
35 namespace brotensor::detail::cpu {
36
37 namespace {
38
39 [[noreturn]] void fail(const char* op, const std::string& reason) {
40 throw std::runtime_error(std::string("brotensor: ") + op + ": " + reason);
41 }
42
43 81 void check_groups(const char* op, int C_in, int C_out, int groups) {
44
1/2
✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
81 if (groups < 1 || C_in % groups != 0 || C_out % groups != 0) {
45 fail(op, "groups must be >=1 and divide both C_in and C_out");
46 }
47 81 }
48
49 227 void require_fp32(const char* op, const ::brotensor::Tensor& t,
50 const char* name) {
51
1/2
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
227 if (t.dtype != ::brotensor::Dtype::FP32) {
52 fail(op, std::string(name) + " must be FP32 (CPU backend is FP32-only)");
53 }
54 227 }
55
56 162 inline int convt2d_out(int L, int stride, int padding, int output_padding,
57 int dilation, int kL) {
58 324 return (L - 1) * stride - 2 * padding + dilation * (kL - 1)
59 162 + output_padding + 1;
60 }
61
62 81 void check_geometry(const char* op, int kH, int kW,
63 int stride_h, int stride_w,
64 int pad_h, int pad_w,
65 int output_padding_h, int output_padding_w,
66 int dil_h, int dil_w) {
67
1/2
✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
162 if (kH < 1 || kW < 1 || stride_h < 1 || stride_w < 1
68 81 || dil_h < 1 || dil_w < 1 || pad_h < 0 || pad_w < 0
69 81 || output_padding_h < 0 || output_padding_w < 0) {
70 fail(op, "kH/kW/stride/dilation >=1 and pad/output_padding >=0");
71 }
72
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
81 if (output_padding_h >= stride_h && output_padding_h >= dil_h) {
73 fail(op, "output_padding_h must be < stride_h or < dil_h");
74 }
75
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
81 if (output_padding_w >= stride_w && output_padding_w >= dil_w) {
76 fail(op, "output_padding_w must be < stride_w or < dil_w");
77 }
78 81 }
79
80 } // namespace
81
82 // ════════════════════════════════════════════════════════════════════════════
83 // conv_transpose2d_forward
84 // ════════════════════════════════════════════════════════════════════════════
85 72 void conv_transpose2d_forward(const ::brotensor::Tensor& X,
86 const ::brotensor::Tensor& Wt,
87 const ::brotensor::Tensor* bias,
88 int N, int C_in, int H, int W,
89 int C_out, int kH, int kW,
90 int stride_h, int stride_w,
91 int pad_h, int pad_w,
92 int output_padding_h, int output_padding_w,
93 int dil_h, int dil_w, int groups,
94 ::brotensor::Tensor& Y) {
95 72 const char* op = "conv_transpose2d_forward";
96 72 require_fp32(op, X, "X");
97 72 require_fp32(op, Wt, "Wt");
98
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 57 times.
72 if (bias) require_fp32(op, *bias, "bias");
99 72 check_groups(op, C_in, C_out, groups);
100 144 check_geometry(op, kH, kW, stride_h, stride_w, pad_h, pad_w,
101 72 output_padding_h, output_padding_w, dil_h, dil_w);
102
103 72 const int Cg_in = C_in / groups;
104 72 const int Cg_out = C_out / groups;
105 144 const int H_out = convt2d_out(H, stride_h, pad_h, output_padding_h,
106 72 dil_h, kH);
107 144 const int W_out = convt2d_out(W, stride_w, pad_w, output_padding_w,
108 72 dil_w, kW);
109
1/4
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
72 if (H_out <= 0 || W_out <= 0) fail(op, "non-positive output spatial size");
110
111 72 const int kHW = kH * kW;
112
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (Wt.rows != C_in || Wt.cols != Cg_out * kHW) {
113 fail(op, "Wt shape must be (C_in, (C_out/groups)*kH*kW)");
114 }
115
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (X.rows != N || X.cols != C_in * H * W) {
116 fail(op, "X shape must be (N, C_in*H*W)");
117 }
118
3/4
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 57 times.
✓ Branch 2 taken 57 times.
✗ Branch 3 not taken.
72 if (bias && (bias->rows != C_out || bias->cols != 1)) {
119 fail(op, "bias shape must be (C_out, 1)");
120 }
121
122 72 const int out_cols = C_out * H_out * W_out;
123
4/6
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 48 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
72 if (Y.rows != N || Y.cols != out_cols || Y.dtype != ::brotensor::Dtype::FP32) {
124 48 Y.resize(N, out_cols, ::brotensor::Dtype::FP32);
125 48 }
126
2/4
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 72 times.
72 if (N == 0 || out_cols == 0) return;
127
128 72 const float* Xp = X.host_f32();
129 72 const float* Wp = Wt.host_f32();
130
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 15 times.
72 const float* Bp = bias ? bias->host_f32() : nullptr;
131 72 float* Yp = Y.host_f32_mut();
132
133 // Seed every output pixel with its channel bias. Each n exclusively owns
134 // Y's batch slice n, so this parallelizes across n with no cross-thread
135 // writes; it fully completes (parallel_for blocks) before the scatter-add
136 // pass below starts touching the same Y buffer.
137
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
148 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
138 76 const int n = static_cast<int>(ni);
139
2/2
✓ Branch 0 taken 616 times.
✓ Branch 1 taken 76 times.
692 for (int oc = 0; oc < C_out; ++oc) {
140
2/2
✓ Branch 0 taken 497 times.
✓ Branch 1 taken 119 times.
616 const float bv = Bp ? Bp[oc] : 0.0f;
141 616 float* y_chan =
142 616 Yp + (static_cast<long>(n) * C_out + oc) * H_out * W_out;
143
2/2
✓ Branch 0 taken 315858 times.
✓ Branch 1 taken 616 times.
316474 for (int o = 0; o < H_out * W_out; ++o) y_chan[o] = bv;
144 616 }
145 76 });
146
147 // Interior region: the input rows/cols for which every kernel tap
148 // scatters into a valid output position (the mirror image of conv2d's
149 // gather-side split, applied to this scatter direction), computed once
150 // — independent of n/c_in. Only the thin border ring of input pixels
151 // needs the per-tap bounds check; the interior runs a branch-free
152 // kh/kw/oc_local loop.
153 72 int h_lo = (pad_h + stride_h - 1) / stride_h;
154 72 int h_hi = H_out - 1 + pad_h - (kH - 1) * dil_h;
155
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 h_hi = (h_hi >= 0) ? (h_hi / stride_h) : -1;
156
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (h_lo < 0) h_lo = 0;
157
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (h_hi >= H) h_hi = H - 1;
158
159 72 int w_lo = (pad_w + stride_w - 1) / stride_w;
160 72 int w_hi = W_out - 1 + pad_w - (kW - 1) * dil_w;
161
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 w_hi = (w_hi >= 0) ? (w_hi / stride_w) : -1;
162
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (w_lo < 0) w_lo = 0;
163
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (w_hi >= W) w_hi = W - 1;
164
165
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 72 times.
72 const bool has_interior = (h_lo <= h_hi) && (w_lo <= w_hi);
166
167 // Scatter-add. Input pixel (n, c_in, h, w) reaches output pixel
168 // ho = h*stride_h - pad_h + kh*dil_h
169 // wo = w*stride_w - pad_w + kw*dil_w
170 // in each output channel of c_in's group. Each n only ever scatters into
171 // Y's own batch slice n (oc is always paired with this same n), so this
172 // parallelizes across n with no cross-thread writes — the c_in loop
173 // inside stays sequential per-n, so the += accumulation across c_in into
174 // a shared oc is untouched by another thread.
175
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
148 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
176 76 const int n = static_cast<int>(ni);
177
2/2
✓ Branch 0 taken 1313 times.
✓ Branch 1 taken 74 times.
1387 for (int c_in = 0; c_in < C_in; ++c_in) {
178 1313 const int g = c_in / Cg_in;
179 1313 const int oc_base = g * Cg_out;
180 1313 const float* x_chan =
181 1313 Xp + (static_cast<long>(n) * C_in + c_in) * H * W;
182
183 // Border pixel: same bounds-checked scatter as before.
184 19281 auto scatter_bordered = [&](int h, int w) {
185 17968 const float xv = x_chan[static_cast<long>(h) * W + w];
186
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 17948 times.
17968 if (xv == 0.0f) return;
187 17948 const int ho_origin = h * stride_h - pad_h;
188 17948 const int wo_origin = w * stride_w - pad_w;
189
2/2
✓ Branch 0 taken 17948 times.
✓ Branch 1 taken 53986 times.
71934 for (int kh = 0; kh < kH; ++kh) {
190 53986 const int ho = ho_origin + kh * dil_h;
191
4/4
✓ Branch 0 taken 44461 times.
✓ Branch 1 taken 9525 times.
✓ Branch 2 taken 411 times.
✓ Branch 3 taken 44050 times.
53986 if (ho < 0 || ho >= H_out) continue;
192
2/2
✓ Branch 0 taken 132486 times.
✓ Branch 1 taken 44050 times.
176536 for (int kw = 0; kw < kW; ++kw) {
193 132486 const int wo = wo_origin + kw * dil_w;
194
4/4
✓ Branch 0 taken 107113 times.
✓ Branch 1 taken 25373 times.
✓ Branch 2 taken 106131 times.
✓ Branch 3 taken 982 times.
132486 if (wo < 0 || wo >= W_out) continue;
195
2/2
✓ Branch 0 taken 11223240 times.
✓ Branch 1 taken 106131 times.
11329371 for (int oc_local = 0; oc_local < Cg_out; ++oc_local) {
196 11223240 const int oc = oc_base + oc_local;
197 11223240 const int w_idx =
198 11223240 (c_in * Cg_out + oc_local) * kHW
199 11223240 + kh * kW + kw;
200 22446480 Yp[(static_cast<long>(n) * C_out + oc)
201 11223240 * H_out * W_out
202 11223240 + static_cast<long>(ho) * W_out + wo]
203 22446480 += xv * Wp[w_idx];
204 11223240 }
205 106131 }
206 44050 }
207 17968 };
208
209 // Interior pixel: every tap guaranteed in-bounds — no checks.
210 177177 auto scatter_interior = [&](int h, int w) {
211 175864 const float xv = x_chan[static_cast<long>(h) * W + w];
212
2/2
✓ Branch 0 taken 177 times.
✓ Branch 1 taken 175687 times.
175864 if (xv == 0.0f) return;
213 175687 const int ho_origin = h * stride_h - pad_h;
214 175687 const int wo_origin = w * stride_w - pad_w;
215
2/2
✓ Branch 0 taken 175687 times.
✓ Branch 1 taken 453713 times.
629400 for (int kh = 0; kh < kH; ++kh) {
216 453713 const int ho = ho_origin + kh * dil_h;
217 453713 const int y_ho_base = ho * W_out;
218 453713 const int w_kh_base = kh * kW;
219
2/2
✓ Branch 0 taken 1214607 times.
✓ Branch 1 taken 453713 times.
1668320 for (int kw = 0; kw < kW; ++kw) {
220 1214607 const int wo = wo_origin + kw * dil_w;
221
2/2
✓ Branch 0 taken 119615749 times.
✓ Branch 1 taken 1214607 times.
120830356 for (int oc_local = 0; oc_local < Cg_out; ++oc_local) {
222 119615749 const int oc = oc_base + oc_local;
223 119615749 const int w_idx =
224 119615749 (c_in * Cg_out + oc_local) * kHW
225 119615749 + w_kh_base + kw;
226 239231498 Yp[(static_cast<long>(n) * C_out + oc)
227 119615749 * H_out * W_out + y_ho_base + wo]
228 239231498 += xv * Wp[w_idx];
229 119615749 }
230 1214607 }
231 453713 }
232 175864 };
233
234
2/2
✓ Branch 0 taken 14482 times.
✓ Branch 1 taken 1311 times.
15793 for (int h = 0; h < H; ++h) {
235
4/4
✓ Branch 0 taken 14481 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 764 times.
✓ Branch 3 taken 13717 times.
14482 const bool h_interior = has_interior && h >= h_lo && h <= h_hi;
236
2/2
✓ Branch 0 taken 886 times.
✓ Branch 1 taken 13594 times.
14482 if (!h_interior) {
237
2/2
✓ Branch 0 taken 9951 times.
✓ Branch 1 taken 886 times.
10837 for (int w = 0; w < W; ++w) scatter_bordered(h, w);
238 886 continue;
239 }
240
2/2
✓ Branch 0 taken 7852 times.
✓ Branch 1 taken 13594 times.
21446 for (int w = 0; w < w_lo; ++w) scatter_bordered(h, w);
241
2/2
✓ Branch 0 taken 175869 times.
✓ Branch 1 taken 13594 times.
189463 for (int w = w_lo; w <= w_hi; ++w) scatter_interior(h, w);
242
2/2
✓ Branch 0 taken 166 times.
✓ Branch 1 taken 13594 times.
13760 for (int w = w_hi + 1; w < W; ++w) scatter_bordered(h, w);
243 13594 }
244 1311 }
245 74 });
246 72 }
247
248 // ════════════════════════════════════════════════════════════════════════════
249 // conv_transpose2d_backward_input
250 // ════════════════════════════════════════════════════════════════════════════
251 5 void conv_transpose2d_backward_input(const ::brotensor::Tensor& Wt,
252 const ::brotensor::Tensor& dY,
253 int N, int C_in, int H, int W,
254 int C_out, int kH, int kW,
255 int stride_h, int stride_w,
256 int pad_h, int pad_w,
257 int output_padding_h, int output_padding_w,
258 int dil_h, int dil_w, int groups,
259 ::brotensor::Tensor& dX) {
260 5 const char* op = "conv_transpose2d_backward_input";
261 5 require_fp32(op, Wt, "Wt");
262 5 require_fp32(op, dY, "dY");
263 5 check_groups(op, C_in, C_out, groups);
264 10 check_geometry(op, kH, kW, stride_h, stride_w, pad_h, pad_w,
265 5 output_padding_h, output_padding_w, dil_h, dil_w);
266
267 5 const int Cg_in = C_in / groups;
268 5 const int Cg_out = C_out / groups;
269 10 const int H_out = convt2d_out(H, stride_h, pad_h, output_padding_h,
270 5 dil_h, kH);
271 10 const int W_out = convt2d_out(W, stride_w, pad_w, output_padding_w,
272 5 dil_w, kW);
273
1/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
5 if (H_out <= 0 || W_out <= 0) fail(op, "non-positive output spatial size");
274 5 const int kHW = kH * kW;
275
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (Wt.rows != C_in || Wt.cols != Cg_out * kHW) {
276 fail(op, "Wt shape must be (C_in, (C_out/groups)*kH*kW)");
277 }
278
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (dY.rows != N || dY.cols != C_out * H_out * W_out) {
279 fail(op, "dY shape must be (N, C_out*H_out*W_out)");
280 }
281 5 const int in_cols = C_in * H * W;
282
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
5 if (dX.rows != N || dX.cols != in_cols
283 || dX.dtype != ::brotensor::Dtype::FP32) {
284 5 dX.resize(N, in_cols, ::brotensor::Dtype::FP32);
285 5 }
286
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (N == 0 || in_cols == 0) return;
287
288 5 const float* Wp = Wt.host_f32();
289 5 const float* dYp = dY.host_f32();
290 5 float* dXp = dX.host_f32_mut();
291
292 // Adjoint of the transposed-conv scatter is a plain gather conv. Each n
293 // exclusively owns dX's batch slice n (dY/Wt are read-only), so this
294 // parallelizes across n with no cross-thread writes.
295
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
11 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
296 6 const int n = static_cast<int>(ni);
297
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 6 times.
24 for (int c_in = 0; c_in < C_in; ++c_in) {
298 18 const int g = c_in / Cg_in;
299 18 const int oc_base = g * Cg_out;
300
2/2
✓ Branch 0 taken 76 times.
✓ Branch 1 taken 18 times.
94 for (int h = 0; h < H; ++h) {
301 76 const int ho_origin = h * stride_h - pad_h;
302
2/2
✓ Branch 0 taken 386 times.
✓ Branch 1 taken 76 times.
462 for (int w = 0; w < W; ++w) {
303 386 const int wo_origin = w * stride_w - pad_w;
304 386 float acc = 0.0f;
305
2/2
✓ Branch 0 taken 1393 times.
✓ Branch 1 taken 386 times.
1779 for (int kh = 0; kh < kH; ++kh) {
306 1393 const int ho = ho_origin + kh * dil_h;
307
4/4
✓ Branch 0 taken 1307 times.
✓ Branch 1 taken 86 times.
✓ Branch 2 taken 80 times.
✓ Branch 3 taken 1227 times.
1393 if (ho < 0 || ho >= H_out) continue;
308
2/2
✓ Branch 0 taken 4451 times.
✓ Branch 1 taken 1227 times.
5678 for (int kw = 0; kw < kW; ++kw) {
309 4451 const int wo = wo_origin + kw * dil_w;
310
4/4
✓ Branch 0 taken 4267 times.
✓ Branch 1 taken 184 times.
✓ Branch 2 taken 4005 times.
✓ Branch 3 taken 262 times.
4451 if (wo < 0 || wo >= W_out) continue;
311
2/2
✓ Branch 0 taken 11240 times.
✓ Branch 1 taken 4005 times.
15245 for (int oc_local = 0; oc_local < Cg_out; ++oc_local) {
312 11240 const int oc = oc_base + oc_local;
313 11240 const int w_idx =
314 11240 (c_in * Cg_out + oc_local) * kHW
315 11240 + kh * kW + kw;
316 11240 const long dy_idx =
317 11240 (static_cast<long>(n) * C_out + oc)
318 11240 * H_out * W_out
319 11240 + static_cast<long>(ho) * W_out + wo;
320 11240 acc += dYp[dy_idx] * Wp[w_idx];
321 11240 }
322 4005 }
323 1227 }
324 772 dXp[(static_cast<long>(n) * C_in + c_in) * H * W
325 1158 + static_cast<long>(h) * W + w] = acc;
326 386 }
327 76 }
328 18 }
329 6 });
330 5 }
331
332 // ════════════════════════════════════════════════════════════════════════════
333 // conv_transpose2d_backward_weight
334 // ════════════════════════════════════════════════════════════════════════════
335 4 void conv_transpose2d_backward_weight(const ::brotensor::Tensor& X,
336 const ::brotensor::Tensor& dY,
337 int N, int C_in, int H, int W,
338 int C_out, int kH, int kW,
339 int stride_h, int stride_w,
340 int pad_h, int pad_w,
341 int output_padding_h, int output_padding_w,
342 int dil_h, int dil_w, int groups,
343 ::brotensor::Tensor& dWt) {
344 4 const char* op = "conv_transpose2d_backward_weight";
345 4 require_fp32(op, X, "X");
346 4 require_fp32(op, dY, "dY");
347 4 require_fp32(op, dWt, "dWt");
348 4 check_groups(op, C_in, C_out, groups);
349 8 check_geometry(op, kH, kW, stride_h, stride_w, pad_h, pad_w,
350 4 output_padding_h, output_padding_w, dil_h, dil_w);
351
352 4 const int Cg_in = C_in / groups;
353 4 const int Cg_out = C_out / groups;
354 8 const int H_out = convt2d_out(H, stride_h, pad_h, output_padding_h,
355 4 dil_h, kH);
356 8 const int W_out = convt2d_out(W, stride_w, pad_w, output_padding_w,
357 4 dil_w, kW);
358
1/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
4 if (H_out <= 0 || W_out <= 0) fail(op, "non-positive output spatial size");
359 4 const int kHW = kH * kW;
360
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (dWt.rows != C_in || dWt.cols != Cg_out * kHW) {
361 fail(op, "dWt shape must be (C_in, (C_out/groups)*kH*kW)");
362 }
363
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (X.rows != N || X.cols != C_in * H * W) {
364 fail(op, "X shape must be (N, C_in*H*W)");
365 }
366
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (dY.rows != N || dY.cols != C_out * H_out * W_out) {
367 fail(op, "dY shape must be (N, C_out*H_out*W_out)");
368 }
369
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
4 if (C_in == 0 || Cg_out == 0 || kHW == 0) return;
370
371 4 const float* Xp = X.host_f32();
372 4 const float* dYp = dY.host_f32();
373 4 float* dWp = dWt.host_f32_mut();
374
375 // One accumulation per weight element; += into dWt (caller zeroed it).
376 //
377 // NOT parallelized over n: n is the innermost reduction axis (every
378 // batch item's contribution sums into the same dWp element), not an
379 // outer axis — parallelizing it would race every thread on the same
380 // dWt element. Left single-threaded per this task's scope.
381
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 13 times.
17 for (int c_in = 0; c_in < C_in; ++c_in) {
382 13 const int g = c_in / Cg_in;
383 13 const int oc_base = g * Cg_out;
384
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 13 times.
49 for (int oc_local = 0; oc_local < Cg_out; ++oc_local) {
385 36 const int oc = oc_base + oc_local;
386
2/2
✓ Branch 0 taken 120 times.
✓ Branch 1 taken 36 times.
156 for (int kh = 0; kh < kH; ++kh) {
387
2/2
✓ Branch 0 taken 408 times.
✓ Branch 1 taken 120 times.
528 for (int kw = 0; kw < kW; ++kw) {
388 408 float acc = 0.0f;
389
2/2
✓ Branch 0 taken 708 times.
✓ Branch 1 taken 408 times.
1116 for (int n = 0; n < N; ++n) {
390 708 const float* x_chan =
391 708 Xp + (static_cast<long>(n) * C_in + c_in) * H * W;
392 708 const float* dy_chan =
393 1416 dYp + (static_cast<long>(n) * C_out + oc)
394 708 * H_out * W_out;
395
2/2
✓ Branch 0 taken 3612 times.
✓ Branch 1 taken 708 times.
4320 for (int h = 0; h < H; ++h) {
396 3612 const int ho = h * stride_h - pad_h + kh * dil_h;
397
4/4
✓ Branch 0 taken 3408 times.
✓ Branch 1 taken 204 times.
✓ Branch 2 taken 3204 times.
✓ Branch 3 taken 204 times.
3612 if (ho < 0 || ho >= H_out) continue;
398
2/2
✓ Branch 0 taken 19644 times.
✓ Branch 1 taken 3204 times.
22848 for (int w = 0; w < W; ++w) {
399 19644 const int wo =
400 19644 w * stride_w - pad_w + kw * dil_w;
401
4/4
✓ Branch 0 taken 18720 times.
✓ Branch 1 taken 924 times.
✓ Branch 2 taken 924 times.
✓ Branch 3 taken 17796 times.
19644 if (wo < 0 || wo >= W_out) continue;
402 35592 acc += x_chan[h * W + w]
403 17796 * dy_chan[ho * W_out + wo];
404 17796 }
405 3204 }
406 708 }
407 408 dWp[(c_in * Cg_out + oc_local) * kHW + kh * kW + kw]
408 816 += acc;
409 408 }
410 120 }
411 36 }
412 13 }
413 4 }
414
415 // ════════════════════════════════════════════════════════════════════════════
416 // conv_transpose2d_backward_bias
417 // ════════════════════════════════════════════════════════════════════════════
418 2 void conv_transpose2d_backward_bias(const ::brotensor::Tensor& dY,
419 int N, int C_out, int H_out, int W_out,
420 ::brotensor::Tensor& dB) {
421 2 const char* op = "conv_transpose2d_backward_bias";
422 2 require_fp32(op, dY, "dY");
423 2 require_fp32(op, dB, "dB");
424
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (dB.rows != C_out || dB.cols != 1) {
425 fail(op, "dB shape must be (C_out, 1)");
426 }
427
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (dY.rows != N || dY.cols != C_out * H_out * W_out) {
428 fail(op, "dY shape must be (N, C_out*H_out*W_out)");
429 }
430
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 if (C_out == 0 || N == 0 || H_out == 0 || W_out == 0) return;
431
432 2 const float* dYp = dY.host_f32();
433 2 float* dBp = dB.host_f32_mut();
434
435 // Per-output-channel sum over (N, H_out, W_out); += into dB.
436 //
437 // NOT parallelized over n: same reason as backward_weight above — n is
438 // the reduction axis here, not the outer axis.
439
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
7 for (int oc = 0; oc < C_out; ++oc) {
440 5 float acc = 0.0f;
441
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
13 for (int n = 0; n < N; ++n) {
442 8 const float* dy_chan =
443 8 dYp + (static_cast<long>(n) * C_out + oc) * H_out * W_out;
444
2/2
✓ Branch 0 taken 770 times.
✓ Branch 1 taken 8 times.
778 for (int i = 0; i < H_out * W_out; ++i) acc += dy_chan[i];
445 8 }
446 5 dBp[oc] += acc;
447 5 }
448 2 }
449
450 } // namespace brotensor::detail::cpu
451