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.0% 136 / 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 1735 times.
✓ Branch 1 taken 100 times.
1835 for (int oc = 0; oc < C_out; ++oc) {
113 1735 const int g = oc / Cg_out;
114 1735 const int ic_base = g * Cg_in;
115 1735 const int w_oc_base = oc * Cg_in * kH * kW;
116
2/2
✓ Branch 0 taken 1637 times.
✓ Branch 1 taken 98 times.
1735 const float bias_v = Bp ? Bp[oc] : 0.0f;
117
118 // Border pixel: same bounds-checked accumulation as before.
119 36723 auto compute_bordered = [&](int oh, int ow) -> float {
120 34988 const int in_h_origin = oh * stride_h - pad_h;
121 34988 const int in_w_origin = ow * stride_w - pad_w;
122 34988 float acc = 0.0f;
123
2/2
✓ Branch 0 taken 1304810 times.
✓ Branch 1 taken 34988 times.
1339798 for (int ic_local = 0; ic_local < Cg_in; ++ic_local) {
124 1304810 const int ic = ic_base + ic_local;
125 1304810 const int w_ic_base = w_oc_base + ic_local * kH * kW;
126 1304810 const int x_ic_base = (n * C_in + ic) * H * W;
127
2/2
✓ Branch 0 taken 6060943 times.
✓ Branch 1 taken 1304810 times.
7365753 for (int kh = 0; kh < kH; ++kh) {
128 6060943 const int in_h = in_h_origin + kh * dil_h;
129
4/4
✓ Branch 0 taken 5453943 times.
✓ Branch 1 taken 607000 times.
✓ Branch 2 taken 4872344 times.
✓ Branch 3 taken 581599 times.
6060943 if (in_h < 0 || in_h >= H) continue;
130
2/2
✓ Branch 0 taken 26776238 times.
✓ Branch 1 taken 4872344 times.
31648582 for (int kw = 0; kw < kW; ++kw) {
131 26776238 const int in_w = in_w_origin + kw * dil_w;
132
4/4
✓ Branch 0 taken 24201615 times.
✓ Branch 1 taken 2574623 times.
✓ Branch 2 taken 2515734 times.
✓ Branch 3 taken 21685881 times.
26776238 if (in_w < 0 || in_w >= W) continue;
133 43371762 acc += Xp[x_ic_base + in_h * W + in_w] *
134 21685881 Wp[w_ic_base + kh * kW + kw];
135 21685881 }
136 4872344 }
137 1304810 }
138 34988 return acc;
139 };
140
141 // Interior pixel: every tap is guaranteed in-bounds — no checks.
142 75517 auto compute_interior = [&](int oh, int ow) -> float {
143 73782 const int in_h_origin = oh * stride_h - pad_h;
144 73782 const int in_w_origin = ow * stride_w - pad_w;
145 73782 float acc = 0.0f;
146
2/2
✓ Branch 0 taken 2352263 times.
✓ Branch 1 taken 73782 times.
2426045 for (int ic_local = 0; ic_local < Cg_in; ++ic_local) {
147 2352263 const int ic = ic_base + ic_local;
148 2352263 const int w_ic_base = w_oc_base + ic_local * kH * kW;
149 2352263 const int x_ic_base = (n * C_in + ic) * H * W;
150
2/2
✓ Branch 0 taken 7442974 times.
✓ Branch 1 taken 2352263 times.
9795237 for (int kh = 0; kh < kH; ++kh) {
151 7442974 const int in_h = in_h_origin + kh * dil_h;
152 7442974 const int x_row_base = x_ic_base + in_h * W;
153 7442974 const int w_row_base = w_ic_base + kh * kW;
154
2/2
✓ Branch 0 taken 28963262 times.
✓ Branch 1 taken 7442974 times.
36406236 for (int kw = 0; kw < kW; ++kw) {
155 28963262 const int in_w = in_w_origin + kw * dil_w;
156 28963262 acc += Xp[x_row_base + in_w] * Wp[w_row_base + kw];
157 28963262 }
158 7442974 }
159 2352263 }
160 73782 return acc;
161 };
162
163
2/2
✓ Branch 0 taken 11688 times.
✓ Branch 1 taken 1735 times.
13423 for (int oh = 0; oh < H_out; ++oh) {
164 11688 const int y_row_base = ((n * C_out + oc) * H_out + oh) * W_out;
165
3/4
✓ Branch 0 taken 11688 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1368 times.
✓ Branch 3 taken 10320 times.
11688 const bool oh_interior = has_interior && oh >= oh_lo && oh <= oh_hi;
166
2/2
✓ Branch 0 taken 2735 times.
✓ Branch 1 taken 8953 times.
11688 if (!oh_interior) {
167
2/2
✓ Branch 0 taken 21408 times.
✓ Branch 1 taken 2735 times.
24143 for (int ow = 0; ow < W_out; ++ow) {
168 21408 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
169 21408 }
170 2735 continue;
171 }
172
2/2
✓ Branch 0 taken 6808 times.
✓ Branch 1 taken 8953 times.
15761 for (int ow = 0; ow < ow_lo; ++ow) {
173 6808 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
174 6808 }
175
2/2
✓ Branch 0 taken 73947 times.
✓ Branch 1 taken 8953 times.
82900 for (int ow = ow_lo; ow <= ow_hi; ++ow) {
176 73947 Yp[y_row_base + ow] = compute_interior(oh, ow) + bias_v;
177 73947 }
178
2/2
✓ Branch 0 taken 6808 times.
✓ Branch 1 taken 8953 times.
15761 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 8953 }
182 1735 }
183 100 });
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 466 times.
✓ Branch 1 taken 79 times.
545 for (int c_in = 0; c_in < C_in; ++c_in) {
223 466 const int g = c_in / Cg_in;
224 466 const int c_in_local = c_in - g * Cg_in;
225 466 const int oc_lo = g * Cg_out;
226 466 const int oc_hi = oc_lo + Cg_out;
227
2/2
✓ Branch 0 taken 2308 times.
✓ Branch 1 taken 466 times.
2774 for (int i = 0; i < H; ++i) {
228
2/2
✓ Branch 0 taken 14398 times.
✓ Branch 1 taken 2308 times.
16706 for (int j = 0; j < W; ++j) {
229 14398 float acc = 0.0f;
230
2/2
✓ Branch 0 taken 36914 times.
✓ Branch 1 taken 14398 times.
51312 for (int kh = 0; kh < kH; ++kh) {
231 36914 const int num_h = i + pad_h - dil_h * kh;
232
4/4
✓ Branch 0 taken 34977 times.
✓ Branch 1 taken 1937 times.
✓ Branch 2 taken 994 times.
✓ Branch 3 taken 33983 times.
36914 if (num_h < 0 || num_h % stride_h != 0) continue;
233 33983 const int i_out = num_h / stride_h;
234
4/4
✓ Branch 0 taken 33928 times.
✓ Branch 1 taken 55 times.
✓ Branch 2 taken 1723 times.
✓ Branch 3 taken 32205 times.
33983 if (i_out < 0 || i_out >= H_out) continue;
235
2/2
✓ Branch 0 taken 90997 times.
✓ Branch 1 taken 32205 times.
123202 for (int kw = 0; kw < kW; ++kw) {
236 90997 const int num_w = j + pad_w - dil_w * kw;
237
4/4
✓ Branch 0 taken 86347 times.
✓ Branch 1 taken 4650 times.
✓ Branch 2 taken 2965 times.
✓ Branch 3 taken 83382 times.
90997 if (num_w < 0 || num_w % stride_w != 0) continue;
238 83382 const int j_out = num_w / stride_w;
239
4/4
✓ Branch 0 taken 83026 times.
✓ Branch 1 taken 356 times.
✓ Branch 2 taken 78448 times.
✓ Branch 3 taken 4578 times.
83382 if (j_out < 0 || j_out >= W_out) continue;
240
2/2
✓ Branch 0 taken 590430 times.
✓ Branch 1 taken 78448 times.
668878 for (int c_out = oc_lo; c_out < oc_hi; ++c_out) {
241 590430 const int dy_idx =
242 1180860 ((n * C_out + c_out) * H_out + i_out) *
243 1180860 W_out + j_out;
244 590430 const int w_idx =
245 1180860 ((c_out * Cg_in + c_in_local) * kH + kh) *
246 1180860 kW + kw;
247 590430 acc += dYp[dy_idx] * Wp[w_idx];
248 590430 }
249 78448 }
250 32205 }
251 14398 const int dx_idx = ((n * C_in + c_in) * H + i) * W + j;
252 14398 dXp[dx_idx] = acc; // overwrite
253 14398 }
254 2308 }
255 466 }
256 79 });
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