GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.8% 220 / 0 / 232
Functions: 100.0% 10 / 0 / 10
Branches: 68.5% 137 / 0 / 200

src/cpu/conv2d.cpp
Line Branch Exec Source
1 // ─── CPU conv2d ops (CHUNK 3) ──────────────────────────────────────────────
2 //
3 // FP32 scalar host implementations. Ports src/cuda/conv2d.cu — the plain
4 // direct-conv kernels (NOT the WMMA implicit-GEMM fast path), FP32 path only.
5 //
6 // Memory layout (matches the GPU exactly):
7 // X : NCHW — ((n*C_in + c_in) * H + h) * W + w
8 // Y : NCHW — ((n*C_out + c_out) * H_out + h_out) * W_out + w_out
9 // Wt : OIHW (grouped) — ((c_out*Cg_in + c_in_local) * kH + kh) * kW + kw
10 // where Cg_in = C_in/groups is the per-group input-channel count.
11 // bias: (C_out, 1), optional (may be null)
12 //
13 // Groups convention: output channel c_out belongs to group g = c_out/Cg_out;
14 // that group's absolute input channels start at g*Cg_in (Cg_in channels wide).
15 //
16 // Output-size formula (identical to the GPU):
17 // H_out = (H + 2*pad_h - dil_h*(kH-1) - 1) / stride_h + 1
18 // W_out = (W + 2*pad_w - dil_w*(kW-1) - 1) / stride_w + 1
19 //
20 // ACCUMULATION (matches the GPU kernels):
21 // conv2d_forward — Y OVERWRITTEN (kernel stores acc directly).
22 // conv2d_backward_input — dX OVERWRITTEN.
23 // conv2d_backward_weight — dWt ACCUMULATES (+=); GPU folds an FP32 scratch
24 // into the caller's dWt. Caller zeros dWt first.
25 // conv2d_backward_bias — dB ACCUMULATES (+=); GPU folds an FP32 scratch
26 // into the caller's dB. Caller zeros dB first.
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 192 inline void check_groups(const char* op, int C_in, int C_out, int groups) {
40
1/2
✓ Branch 0 taken 192 times.
✗ Branch 1 not taken.
192 if (groups < 1 || C_in % groups != 0 || C_out % groups != 0) {
41 throw std::runtime_error(
42 std::string(op) +
43 ": groups must be >=1 and divide both C_in and C_out");
44 }
45 192 }
46
47 384 inline int out_dim(int in, int pad, int dil, int k, int stride) {
48 384 return (in + 2 * pad - dil * (k - 1) - 1) / stride + 1;
49 }
50
51 } // namespace
52
53 71 void conv2d_forward(const ::brotensor::Tensor& X,
54 const ::brotensor::Tensor& Wt,
55 const ::brotensor::Tensor* bias,
56 int N, int C_in, int H, int W,
57 int C_out, int kH, int kW,
58 int stride_h, int stride_w,
59 int pad_h, int pad_w,
60 int dil_h, int dil_w,
61 int groups,
62 ::brotensor::Tensor& Y) {
63
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (Wt.dtype != X.dtype) {
64 throw std::runtime_error("conv2d_forward: Wt dtype must match X");
65 }
66
3/4
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 21 times.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
71 if (bias && bias->dtype != X.dtype) {
67 throw std::runtime_error("conv2d_forward: bias dtype must match X");
68 }
69 71 check_groups("conv2d_forward", C_in, C_out, groups);
70 71 const int Cg_in = C_in / groups;
71 71 const int Cg_out = C_out / groups;
72 71 const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h);
73 71 const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w);
74
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (H_out <= 0 || W_out <= 0) {
75 throw std::runtime_error("conv2d_forward: non-positive output shape");
76 }
77 71 const int out_cols = C_out * H_out * W_out;
78
4/6
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 47 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
71 if (Y.rows != N || Y.cols != out_cols || Y.dtype != X.dtype) {
79 47 Y.resize(N, out_cols, X.dtype);
80 47 }
81
2/4
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 71 times.
71 if (N == 0 || out_cols == 0) return;
82
83 71 const float* Xp = X.host_f32();
84 71 const float* Wp = Wt.host_f32();
85
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 21 times.
71 const float* Bp = bias ? bias->host_f32() : nullptr;
86 71 float* Yp = Y.host_f32_mut();
87
88 // Interior region: the output rows/cols for which every kernel tap is
89 // guaranteed in-bounds (the whole kH*kW*dil window sits inside the padded
90 // input), computed once — independent of n/oc. In-bounds is monotonic in
91 // (oh, ow), so the intersection across all kh (resp. kw) is itself a
92 // single contiguous range. Only the thin border ring outside that range
93 // needs the per-tap bounds check; the interior runs a branch-free loop.
94 71 int oh_lo = (pad_h + stride_h - 1) / stride_h; // ceil(pad_h/stride_h)
95 71 int oh_hi = H - 1 + pad_h - (kH - 1) * dil_h;
96
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 oh_hi = (oh_hi >= 0) ? (oh_hi / stride_h) : -1; // floor(.../stride_h)
97
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (oh_lo < 0) oh_lo = 0;
98
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (oh_hi >= H_out) oh_hi = H_out - 1;
99
100 71 int ow_lo = (pad_w + stride_w - 1) / stride_w;
101 71 int ow_hi = W - 1 + pad_w - (kW - 1) * dil_w;
102
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 ow_hi = (ow_hi >= 0) ? (ow_hi / stride_w) : -1;
103
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (ow_lo < 0) ow_lo = 0;
104
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (ow_hi >= W_out) ow_hi = W_out - 1;
105
106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 71 times.
71 const bool has_interior = (oh_lo <= oh_hi) && (ow_lo <= ow_hi);
107
108 // Each n exclusively owns Y's batch slice n (X/Wt/bias are read-only), so
109 // this parallelizes across n with no cross-thread writes.
110
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
171 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
111 100 const int n = static_cast<int>(ni);
112
2/2
✓ Branch 0 taken 1734 times.
✓ Branch 1 taken 88 times.
1822 for (int oc = 0; oc < C_out; ++oc) {
113 1734 const int g = oc / Cg_out;
114 1734 const int ic_base = g * Cg_in;
115 1734 const int w_oc_base = oc * Cg_in * kH * kW;
116
2/2
✓ Branch 0 taken 1636 times.
✓ Branch 1 taken 98 times.
1734 const float bias_v = Bp ? Bp[oc] : 0.0f;
117
118 // Border pixel: same bounds-checked accumulation as before.
119 36639 auto compute_bordered = [&](int oh, int ow) -> float {
120 34905 const int in_h_origin = oh * stride_h - pad_h;
121 34905 const int in_w_origin = ow * stride_w - pad_w;
122 34905 float acc = 0.0f;
123
2/2
✓ Branch 0 taken 1281909 times.
✓ Branch 1 taken 34905 times.
1316814 for (int ic_local = 0; ic_local < Cg_in; ++ic_local) {
124 1281909 const int ic = ic_base + ic_local;
125 1281909 const int w_ic_base = w_oc_base + ic_local * kH * kW;
126 1281909 const int x_ic_base = (n * C_in + ic) * H * W;
127
2/2
✓ Branch 0 taken 5980354 times.
✓ Branch 1 taken 1281909 times.
7262263 for (int kh = 0; kh < kH; ++kh) {
128 5980354 const int in_h = in_h_origin + kh * dil_h;
129
4/4
✓ Branch 0 taken 5364976 times.
✓ Branch 1 taken 615378 times.
✓ Branch 2 taken 4804429 times.
✓ Branch 3 taken 560547 times.
5980354 if (in_h < 0 || in_h >= H) continue;
130
2/2
✓ Branch 0 taken 26578039 times.
✓ Branch 1 taken 4804429 times.
31382468 for (int kw = 0; kw < kW; ++kw) {
131 26578039 const int in_w = in_w_origin + kw * dil_w;
132
4/4
✓ Branch 0 taken 24004530 times.
✓ Branch 1 taken 2573509 times.
✓ Branch 2 taken 2489662 times.
✓ Branch 3 taken 21514868 times.
26578039 if (in_w < 0 || in_w >= W) continue;
133 43029736 acc += Xp[x_ic_base + in_h * W + in_w] *
134 21514868 Wp[w_ic_base + kh * kW + kw];
135 21514868 }
136 4804429 }
137 1281909 }
138 34905 return acc;
139 };
140
141 // Interior pixel: every tap is guaranteed in-bounds — no checks.
142 74985 auto compute_interior = [&](int oh, int ow) -> float {
143 73251 const int in_h_origin = oh * stride_h - pad_h;
144 73251 const int in_w_origin = ow * stride_w - pad_w;
145 73251 float acc = 0.0f;
146
2/2
✓ Branch 0 taken 2298805 times.
✓ Branch 1 taken 73251 times.
2372056 for (int ic_local = 0; ic_local < Cg_in; ++ic_local) {
147 2298805 const int ic = ic_base + ic_local;
148 2298805 const int w_ic_base = w_oc_base + ic_local * kH * kW;
149 2298805 const int x_ic_base = (n * C_in + ic) * H * W;
150
2/2
✓ Branch 0 taken 7288011 times.
✓ Branch 1 taken 2298805 times.
9586816 for (int kh = 0; kh < kH; ++kh) {
151 7288011 const int in_h = in_h_origin + kh * dil_h;
152 7288011 const int x_row_base = x_ic_base + in_h * W;
153 7288011 const int w_row_base = w_ic_base + kh * kW;
154
2/2
✓ Branch 0 taken 28472998 times.
✓ Branch 1 taken 7288011 times.
35761009 for (int kw = 0; kw < kW; ++kw) {
155 28472998 const int in_w = in_w_origin + kw * dil_w;
156 28472998 acc += Xp[x_row_base + in_w] * Wp[w_row_base + kw];
157 28472998 }
158 7288011 }
159 2298805 }
160 73251 return acc;
161 };
162
163
2/2
✓ Branch 0 taken 11681 times.
✓ Branch 1 taken 1722 times.
13403 for (int oh = 0; oh < H_out; ++oh) {
164 11681 const int y_row_base = ((n * C_out + oc) * H_out + oh) * W_out;
165
4/4
✓ Branch 0 taken 11675 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 1376 times.
✓ Branch 3 taken 10299 times.
11681 const bool oh_interior = has_interior && oh >= oh_lo && oh <= oh_hi;
166
2/2
✓ Branch 0 taken 2736 times.
✓ Branch 1 taken 8933 times.
11681 if (!oh_interior) {
167
2/2
✓ Branch 0 taken 21407 times.
✓ Branch 1 taken 2736 times.
24143 for (int ow = 0; ow < W_out; ++ow) {
168 21407 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
169 21407 }
170 2736 continue;
171 }
172
2/2
✓ Branch 0 taken 6809 times.
✓ Branch 1 taken 8933 times.
15742 for (int ow = 0; ow < ow_lo; ++ow) {
173 6809 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
174 6809 }
175
2/2
✓ Branch 0 taken 73625 times.
✓ Branch 1 taken 8933 times.
82558 for (int ow = ow_lo; ow <= ow_hi; ++ow) {
176 73625 Yp[y_row_base + ow] = compute_interior(oh, ow) + bias_v;
177 73625 }
178
2/2
✓ Branch 0 taken 6808 times.
✓ Branch 1 taken 8933 times.
15741 for (int ow = ow_hi + 1; ow < W_out; ++ow) {
179 6808 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
180 6808 }
181 8933 }
182 1722 }
183 88 });
184 71 }
185
186 61 void conv2d_backward_input(const ::brotensor::Tensor& Wt,
187 const ::brotensor::Tensor& dY,
188 int N, int C_in, int H, int W,
189 int C_out, int kH, int kW,
190 int stride_h, int stride_w,
191 int pad_h, int pad_w,
192 int dil_h, int dil_w,
193 int groups,
194 ::brotensor::Tensor& dX) {
195
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
61 if (dY.dtype != Wt.dtype) {
196 throw std::runtime_error("conv2d_backward_input: dY dtype must match Wt");
197 }
198 61 check_groups("conv2d_backward_input", C_in, C_out, groups);
199 61 const int Cg_in = C_in / groups;
200 61 const int Cg_out = C_out / groups;
201 61 const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h);
202 61 const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w);
203
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
61 if (H_out <= 0 || W_out <= 0) {
204 throw std::runtime_error("conv2d_backward_input: non-positive output shape");
205 }
206 61 const int in_cols = C_in * H * W;
207
4/6
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 37 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
61 if (dX.rows != N || dX.cols != in_cols || dX.dtype != Wt.dtype) {
208 37 dX.resize(N, in_cols, Wt.dtype);
209 37 }
210
2/4
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 61 times.
61 if (N == 0 || in_cols == 0) return;
211
212 61 const float* Wp = Wt.host_f32();
213 61 const float* dYp = dY.host_f32();
214 61 float* dXp = dX.host_f32_mut();
215
216 // Gather form: one accumulation per input pixel, inverting the forward
217 // index relation in_h = stride_h*i_out - pad_h + dil_h*kh.
218 // Each n exclusively owns dX's batch slice n (dY/Wt are read-only), so
219 // this parallelizes across n with no cross-thread writes.
220
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
140 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
221 79 const int n = static_cast<int>(ni);
222
2/2
✓ Branch 0 taken 692 times.
✓ Branch 1 taken 147 times.
545 for (int c_in = 0; c_in < C_in; ++c_in) {
223 692 const int g = c_in / Cg_in;
224 692 const int c_in_local = c_in - g * Cg_in;
225 692 const int oc_lo = g * Cg_out;
226 692 const int oc_hi = oc_lo + Cg_out;
227
2/2
✓ Branch 0 taken 2521 times.
✓ Branch 1 taken 466 times.
2987 for (int i = 0; i < H; ++i) {
228
2/2
✓ Branch 0 taken 14294 times.
✓ Branch 1 taken 2295 times.
16589 for (int j = 0; j < W; ++j) {
229 14294 float acc = 0.0f;
230
2/2
✓ Branch 0 taken 35852 times.
✓ Branch 1 taken 14068 times.
49920 for (int kh = 0; kh < kH; ++kh) {
231 35852 const int num_h = i + pad_h - dil_h * kh;
232
4/4
✓ Branch 0 taken 33682 times.
✓ Branch 1 taken 2170 times.
✓ Branch 2 taken 750 times.
✓ Branch 3 taken 34432 times.
35852 if (num_h < 0 || num_h % stride_h != 0) continue;
233 34432 const int i_out = num_h / stride_h;
234
4/4
✓ Branch 0 taken 32792 times.
✓ Branch 1 taken 1640 times.
✓ Branch 2 taken 128 times.
✓ Branch 3 taken 32920 times.
34432 if (i_out < 0 || i_out >= H_out) continue;
235
2/2
✓ Branch 0 taken 88153 times.
✓ Branch 1 taken 30938 times.
119091 for (int kw = 0; kw < kW; ++kw) {
236 88153 const int num_w = j + pad_w - dil_w * kw;
237
4/4
✓ Branch 0 taken 81187 times.
✓ Branch 1 taken 6966 times.
✓ Branch 2 taken 628 times.
✓ Branch 3 taken 80559 times.
88153 if (num_w < 0 || num_w % stride_w != 0) continue;
238 80559 const int j_out = num_w / stride_w;
239
4/4
✓ Branch 0 taken 79568 times.
✓ Branch 1 taken 991 times.
✓ Branch 2 taken 73702 times.
✓ Branch 3 taken 5866 times.
80559 if (j_out < 0 || j_out >= W_out) continue;
240
2/2
✓ Branch 0 taken 526007 times.
✓ Branch 1 taken 73702 times.
599709 for (int c_out = oc_lo; c_out < oc_hi; ++c_out) {
241 526007 const int dy_idx =
242 1052014 ((n * C_out + c_out) * H_out + i_out) *
243 1052014 W_out + j_out;
244 526007 const int w_idx =
245 1052014 ((c_out * Cg_in + c_in_local) * kH + kh) *
246 1052014 kW + kw;
247 526007 acc += dYp[dy_idx] * Wp[w_idx];
248 526007 }
249 73702 }
250 30938 }
251 14068 const int dx_idx = ((n * C_in + c_in) * H + i) * W + j;
252 14068 dXp[dx_idx] = acc; // overwrite
253 14068 }
254 2295 }
255 466 }
256 147 });
257 61 }
258
259 60 void conv2d_backward_weight(const ::brotensor::Tensor& X,
260 const ::brotensor::Tensor& dY,
261 int N, int C_in, int H, int W,
262 int C_out, int kH, int kW,
263 int stride_h, int stride_w,
264 int pad_h, int pad_w,
265 int dil_h, int dil_w,
266 int groups,
267 ::brotensor::Tensor& dWt) {
268
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 if (dY.dtype != X.dtype || dWt.dtype != X.dtype) {
269 throw std::runtime_error(
270 "conv2d_backward_weight: X, dY, dWt dtype must match");
271 }
272 60 check_groups("conv2d_backward_weight", C_in, C_out, groups);
273 60 const int Cg_in = C_in / groups;
274 60 const int Cg_out = C_out / groups;
275 60 const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h);
276 60 const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w);
277
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 if (H_out <= 0 || W_out <= 0) {
278 throw std::runtime_error("conv2d_backward_weight: non-positive output shape");
279 }
280
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 if (dWt.rows != C_out || dWt.cols != Cg_in * kH * kW) {
281 throw std::runtime_error("conv2d_backward_weight: dWt shape mismatch");
282 }
283 60 const int total = C_out * Cg_in * kH * kW;
284
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 if (total == 0) return;
285
286 60 const float* Xp = X.host_f32();
287 60 const float* dYp = dY.host_f32();
288 60 float* dWp = dWt.host_f32_mut();
289
290 // One accumulation per weight element; accumulate (+=) into dWt to match
291 // the GPU's FP32-scratch-fold-into-dWt contract.
292 //
293 // NOT parallelized over n: here n is the innermost reduction axis (every
294 // batch item's contribution sums into the same dWp[w_idx]), not the outer
295 // axis — naively wrapping parallel_for around n would have every thread
296 // racing on the same dWt elements. Parallelizing over c_out instead would
297 // be race-free (each c_out row of dWt is disjoint), but that's a
298 // different axis than what this task scopes to, so left single-threaded.
299
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 587 times.
647 for (int c_out = 0; c_out < C_out; ++c_out) {
300 587 const int g = c_out / Cg_out;
301
2/2
✓ Branch 0 taken 11361 times.
✓ Branch 1 taken 587 times.
11948 for (int c_in_local = 0; c_in_local < Cg_in; ++c_in_local) {
302 11361 const int c_in = g * Cg_in + c_in_local;
303
2/2
✓ Branch 0 taken 32451 times.
✓ Branch 1 taken 11361 times.
43812 for (int kh = 0; kh < kH; ++kh) {
304
2/2
✓ Branch 0 taken 96169 times.
✓ Branch 1 taken 32451 times.
128620 for (int kw = 0; kw < kW; ++kw) {
305 96169 float acc = 0.0f;
306
2/2
✓ Branch 0 taken 104713 times.
✓ Branch 1 taken 96169 times.
200882 for (int n = 0; n < N; ++n) {
307
2/2
✓ Branch 0 taken 589345 times.
✓ Branch 1 taken 104713 times.
694058 for (int i_out = 0; i_out < H_out; ++i_out) {
308 589345 const int in_h =
309 589345 i_out * stride_h - pad_h + kh * dil_h;
310
4/4
✓ Branch 0 taken 555130 times.
✓ Branch 1 taken 34215 times.
✓ Branch 2 taken 34215 times.
✓ Branch 3 taken 520915 times.
589345 if (in_h < 0 || in_h >= H) continue;
311
2/2
✓ Branch 0 taken 3065539 times.
✓ Branch 1 taken 520915 times.
3586454 for (int j_out = 0; j_out < W_out; ++j_out) {
312 3065539 const int in_w =
313 3065539 j_out * stride_w - pad_w + kw * dil_w;
314
4/4
✓ Branch 0 taken 2893302 times.
✓ Branch 1 taken 172237 times.
✓ Branch 2 taken 172237 times.
✓ Branch 3 taken 2721065 times.
3065539 if (in_w < 0 || in_w >= W) continue;
315 2721065 const int x_idx =
316 2721065 ((n * C_in + c_in) * H + in_h) * W + in_w;
317 2721065 const int dy_idx =
318 5442130 ((n * C_out + c_out) * H_out + i_out) *
319 5442130 W_out + j_out;
320 2721065 acc += dYp[dy_idx] * Xp[x_idx];
321 2721065 }
322 520915 }
323 104713 }
324 96169 const int w_idx =
325 96169 ((c_out * Cg_in + c_in_local) * kH + kh) * kW + kw;
326 96169 dWp[w_idx] += acc; // accumulate
327 96169 }
328 32451 }
329 11361 }
330 587 }
331 60 }
332
333 31 void conv2d_backward_bias(const ::brotensor::Tensor& dY,
334 int N, int C_out, int H_out, int W_out,
335 ::brotensor::Tensor& dB) {
336
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 if (dB.dtype != dY.dtype) {
337 throw std::runtime_error("conv2d_backward_bias: dB dtype must match dY");
338 }
339
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 if (dB.rows != C_out || dB.cols != 1) {
340 throw std::runtime_error("conv2d_backward_bias: dB shape mismatch");
341 }
342
4/8
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 31 times.
31 if (C_out == 0 || N == 0 || H_out == 0 || W_out == 0) return;
343
344 31 const float* dYp = dY.host_f32();
345 31 float* dBp = dB.host_f32_mut();
346
347 31 const int spatial = H_out * W_out;
348 // Per-channel sum over (N, H_out, W_out); accumulate (+=) into dB to match
349 // the GPU's FP32-scratch-fold-into-dB contract.
350 //
351 // NOT parallelized over n: n is the reduction axis here too (summed into
352 // the same dBp[c_out] for every batch item), so it's left single-threaded
353 // for the same reason as conv2d_backward_weight above.
354
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 357 times.
388 for (int c_out = 0; c_out < C_out; ++c_out) {
355 357 float acc = 0.0f;
356
2/2
✓ Branch 0 taken 581 times.
✓ Branch 1 taken 357 times.
938 for (int n = 0; n < N; ++n) {
357 581 const int base = (n * C_out + c_out) * spatial;
358
2/2
✓ Branch 0 taken 13653 times.
✓ Branch 1 taken 581 times.
14234 for (int sp = 0; sp < spatial; ++sp) {
359 13653 acc += dYp[base + sp];
360 13653 }
361 581 }
362 357 dBp[c_out] += acc; // accumulate
363 357 }
364 31 }
365
366 } // namespace brotensor::detail::cpu
367