GCC Code Coverage Report


Directory: brotensor/
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 242 inline void check_groups(const char* op, int C_in, int C_out, int groups) {
40
1/2
✓ Branch 0 taken 242 times.
✗ Branch 1 not taken.
242 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 242 }
46
47 484 inline int out_dim(int in, int pad, int dil, int k, int stride) {
48 484 return (in + 2 * pad - dil * (k - 1) - 1) / stride + 1;
49 }
50
51 } // namespace
52
53 93 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 93 times.
✗ Branch 1 not taken.
93 if (Wt.dtype != X.dtype) {
64 ✗ throw std::runtime_error("conv2d_forward: Wt dtype must match X");
65 }
66
3/4
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 38 times.
✓ Branch 2 taken 55 times.
✗ Branch 3 not taken.
93 if (bias && bias->dtype != X.dtype) {
67 ✗ throw std::runtime_error("conv2d_forward: bias dtype must match X");
68 }
69 93 check_groups("conv2d_forward", C_in, C_out, groups);
70 93 const int Cg_in = C_in / groups;
71 93 const int Cg_out = C_out / groups;
72 93 const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h);
73 93 const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w);
74
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 if (H_out <= 0 || W_out <= 0) {
75 ✗ throw std::runtime_error("conv2d_forward: non-positive output shape");
76 }
77 93 const int out_cols = C_out * H_out * W_out;
78
4/6
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 57 times.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 36 times.
93 if (Y.rows != N || Y.cols != out_cols || Y.dtype != X.dtype) {
79 57 Y.resize(N, out_cols, X.dtype);
80 57 }
81
2/4
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 93 times.
93 if (N == 0 || out_cols == 0) return;
82
83 93 const float* Xp = X.host_f32();
84 93 const float* Wp = Wt.host_f32();
85
2/2
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 38 times.
93 const float* Bp = bias ? bias->host_f32() : nullptr;
86 93 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 93 int oh_lo = (pad_h + stride_h - 1) / stride_h; // ceil(pad_h/stride_h)
95 93 int oh_hi = H - 1 + pad_h - (kH - 1) * dil_h;
96
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 oh_hi = (oh_hi >= 0) ? (oh_hi / stride_h) : -1; // floor(.../stride_h)
97
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 if (oh_lo < 0) oh_lo = 0;
98
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 if (oh_hi >= H_out) oh_hi = H_out - 1;
99
100 93 int ow_lo = (pad_w + stride_w - 1) / stride_w;
101 93 int ow_hi = W - 1 + pad_w - (kW - 1) * dil_w;
102
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 ow_hi = (ow_hi >= 0) ? (ow_hi / stride_w) : -1;
103
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 if (ow_lo < 0) ow_lo = 0;
104
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 if (ow_hi >= W_out) ow_hi = W_out - 1;
105
106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 93 times.
93 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 93 times.
✗ Branch 1 not taken.
225 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
111 132 const int n = static_cast<int>(ni);
112
2/2
✓ Branch 0 taken 5553 times.
✓ Branch 1 taken 50 times.
5603 for (int oc = 0; oc < C_out; ++oc) {
113 5553 const int g = oc / Cg_out;
114 5553 const int ic_base = g * Cg_in;
115 5553 const int w_oc_base = oc * Cg_in * kH * kW;
116
2/2
✓ Branch 0 taken 4150 times.
✓ Branch 1 taken 1403 times.
5553 const float bias_v = Bp ? Bp[oc] : 0.0f;
117
118 // Border pixel: same bounds-checked accumulation as before.
119 70146 auto compute_bordered = [&](int oh, int ow) -> float {
120 64593 const int in_h_origin = oh * stride_h - pad_h;
121 64593 const int in_w_origin = ow * stride_w - pad_w;
122 64593 float acc = 0.0f;
123
2/2
✓ Branch 0 taken 2086390 times.
✓ Branch 1 taken 64593 times.
2150983 for (int ic_local = 0; ic_local < Cg_in; ++ic_local) {
124 2086390 const int ic = ic_base + ic_local;
125 2086390 const int w_ic_base = w_oc_base + ic_local * kH * kW;
126 2086390 const int x_ic_base = (n * C_in + ic) * H * W;
127
2/2
✓ Branch 0 taken 8953484 times.
✓ Branch 1 taken 2086390 times.
11039874 for (int kh = 0; kh < kH; ++kh) {
128 8953484 const int in_h = in_h_origin + kh * dil_h;
129
4/4
✓ Branch 0 taken 7918409 times.
✓ Branch 1 taken 1035075 times.
✓ Branch 2 taken 6972926 times.
✓ Branch 3 taken 945483 times.
8953484 if (in_h < 0 || in_h >= H) continue;
130
2/2
✓ Branch 0 taken 31492588 times.
✓ Branch 1 taken 6972926 times.
38465514 for (int kw = 0; kw < kW; ++kw) {
131 31492588 const int in_w = in_w_origin + kw * dil_w;
132
4/4
✓ Branch 0 taken 28627970 times.
✓ Branch 1 taken 2864618 times.
✓ Branch 2 taken 2708874 times.
✓ Branch 3 taken 25919096 times.
31492588 if (in_w < 0 || in_w >= W) continue;
133 51838192 acc += Xp[x_ic_base + in_h * W + in_w] *
134 25919096 Wp[w_ic_base + kh * kW + kw];
135 25919096 }
136 6972926 }
137 2086390 }
138 64593 return acc;
139 };
140
141 // Interior pixel: every tap is guaranteed in-bounds — no checks.
142 2409909 auto compute_interior = [&](int oh, int ow) -> float {
143 2404356 const int in_h_origin = oh * stride_h - pad_h;
144 2404356 const int in_w_origin = ow * stride_w - pad_w;
145 2404356 float acc = 0.0f;
146
2/2
✓ Branch 0 taken 88435149 times.
✓ Branch 1 taken 2404356 times.
90839505 for (int ic_local = 0; ic_local < Cg_in; ++ic_local) {
147 88435149 const int ic = ic_base + ic_local;
148 88435149 const int w_ic_base = w_oc_base + ic_local * kH * kW;
149 88435149 const int x_ic_base = (n * C_in + ic) * H * W;
150
2/2
✓ Branch 0 taken 102367210 times.
✓ Branch 1 taken 88435149 times.
190802359 for (int kh = 0; kh < kH; ++kh) {
151 102367210 const int in_h = in_h_origin + kh * dil_h;
152 102367210 const int x_row_base = x_ic_base + in_h * W;
153 102367210 const int w_row_base = w_ic_base + kh * kW;
154
2/2
✓ Branch 0 taken 176722889 times.
✓ Branch 1 taken 102367210 times.
279090099 for (int kw = 0; kw < kW; ++kw) {
155 176722889 const int in_w = in_w_origin + kw * dil_w;
156 176722889 acc += Xp[x_row_base + in_w] * Wp[w_row_base + kw];
157 176722889 }
158 102367210 }
159 88435149 }
160 2404356 return acc;
161 };
162
163
2/2
✓ Branch 0 taken 76644 times.
✓ Branch 1 taken 5471 times.
82115 for (int oh = 0; oh < H_out; ++oh) {
164 76644 const int y_row_base = ((n * C_out + oc) * H_out + oh) * W_out;
165
4/4
✓ Branch 0 taken 76603 times.
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 1768 times.
✓ Branch 3 taken 74835 times.
76644 const bool oh_interior = has_interior && oh >= oh_lo && oh <= oh_hi;
166
2/2
✓ Branch 0 taken 3535 times.
✓ Branch 1 taken 73027 times.
76644 if (!oh_interior) {
167
2/2
✓ Branch 0 taken 47317 times.
✓ Branch 1 taken 3535 times.
50852 for (int ow = 0; ow < W_out; ++ow) {
168 47317 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
169 47317 }
170 3535 continue;
171 }
172
2/2
✓ Branch 0 taken 8663 times.
✓ Branch 1 taken 73027 times.
81690 for (int ow = 0; ow < ow_lo; ++ow) {
173 8663 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
174 8663 }
175
2/2
✓ Branch 0 taken 2414210 times.
✓ Branch 1 taken 73027 times.
2487237 for (int ow = ow_lo; ow <= ow_hi; ++ow) {
176 2414210 Yp[y_row_base + ow] = compute_interior(oh, ow) + bias_v;
177 2414210 }
178
2/2
✓ Branch 0 taken 8662 times.
✓ Branch 1 taken 73027 times.
81689 for (int ow = ow_hi + 1; ow < W_out; ++ow) {
179 8662 Yp[y_row_base + ow] = compute_bordered(oh, ow) + bias_v;
180 8662 }
181 73027 }
182 5471 }
183 50 });
184 93 }
185
186 77 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 77 times.
✗ Branch 1 not taken.
77 if (dY.dtype != Wt.dtype) {
196 ✗ throw std::runtime_error("conv2d_backward_input: dY dtype must match Wt");
197 }
198 77 check_groups("conv2d_backward_input", C_in, C_out, groups);
199 77 const int Cg_in = C_in / groups;
200 77 const int Cg_out = C_out / groups;
201 77 const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h);
202 77 const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w);
203
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 if (H_out <= 0 || W_out <= 0) {
204 ✗ throw std::runtime_error("conv2d_backward_input: non-positive output shape");
205 }
206 77 const int in_cols = C_in * H * W;
207
4/6
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 36 times.
77 if (dX.rows != N || dX.cols != in_cols || dX.dtype != Wt.dtype) {
208 41 dX.resize(N, in_cols, Wt.dtype);
209 41 }
210
2/4
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 77 times.
77 if (N == 0 || in_cols == 0) return;
211
212 77 const float* Wp = Wt.host_f32();
213 77 const float* dYp = dY.host_f32();
214 77 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 77 times.
✗ Branch 1 not taken.
176 parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) {
221 99 const int n = static_cast<int>(ni);
222
2/2
✓ Branch 0 taken 1081 times.
✓ Branch 1 taken 99 times.
1180 for (int c_in = 0; c_in < C_in; ++c_in) {
223 1081 const int g = c_in / Cg_in;
224 1081 const int c_in_local = c_in - g * Cg_in;
225 1081 const int oc_lo = g * Cg_out;
226 1081 const int oc_hi = oc_lo + Cg_out;
227
2/2
✓ Branch 0 taken 27622 times.
✓ Branch 1 taken 1081 times.
28703 for (int i = 0; i < H; ++i) {
228
2/2
✓ Branch 0 taken 1189353 times.
✓ Branch 1 taken 27622 times.
1216975 for (int j = 0; j < W; ++j) {
229 1189353 float acc = 0.0f;
230
2/2
✓ Branch 0 taken 1211802 times.
✓ Branch 1 taken 1189353 times.
2401155 for (int kh = 0; kh < kH; ++kh) {
231 1211802 const int num_h = i + pad_h - dil_h * kh;
232
4/4
✓ Branch 0 taken 1209420 times.
✓ Branch 1 taken 2382 times.
✓ Branch 2 taken 548 times.
✓ Branch 3 taken 1208872 times.
1211802 if (num_h < 0 || num_h % stride_h != 0) continue;
233 1208872 const int i_out = num_h / stride_h;
234
4/4
✓ Branch 0 taken 1208240 times.
✓ Branch 1 taken 632 times.
✓ Branch 2 taken 1148 times.
✓ Branch 3 taken 1207092 times.
1208872 if (i_out < 0 || i_out >= H_out) continue;
235
2/2
✓ Branch 0 taken 1265596 times.
✓ Branch 1 taken 1207092 times.
2472688 for (int kw = 0; kw < kW; ++kw) {
236 1265596 const int num_w = j + pad_w - dil_w * kw;
237
4/4
✓ Branch 0 taken 1259709 times.
✓ Branch 1 taken 5887 times.
✓ Branch 2 taken 1630 times.
✓ Branch 3 taken 1258079 times.
1265596 if (num_w < 0 || num_w % stride_w != 0) continue;
238 1258079 const int j_out = num_w / stride_w;
239
4/4
✓ Branch 0 taken 1257718 times.
✓ Branch 1 taken 361 times.
✓ Branch 2 taken 1253157 times.
✓ Branch 3 taken 4561 times.
1258079 if (j_out < 0 || j_out >= W_out) continue;
240
2/2
✓ Branch 0 taken 84414521 times.
✓ Branch 1 taken 1253157 times.
85667678 for (int c_out = oc_lo; c_out < oc_hi; ++c_out) {
241 84414521 const int dy_idx =
242 168829042 ((n * C_out + c_out) * H_out + i_out) *
243 168829042 W_out + j_out;
244 84414521 const int w_idx =
245 168829042 ((c_out * Cg_in + c_in_local) * kH + kh) *
246 168829042 kW + kw;
247 84414521 acc += dYp[dy_idx] * Wp[w_idx];
248 84414521 }
249 1253157 }
250 1207092 }
251 1189353 const int dx_idx = ((n * C_in + c_in) * H + i) * W + j;
252 1189353 dXp[dx_idx] = acc; // overwrite
253 1189353 }
254 27622 }
255 1081 }
256 99 });
257 77 }
258
259 72 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 72 times.
✗ Branch 1 not taken.
72 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 72 check_groups("conv2d_backward_weight", C_in, C_out, groups);
273 72 const int Cg_in = C_in / groups;
274 72 const int Cg_out = C_out / groups;
275 72 const int H_out = out_dim(H, pad_h, dil_h, kH, stride_h);
276 72 const int W_out = out_dim(W, pad_w, dil_w, kW, stride_w);
277
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 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 72 times.
✗ Branch 1 not taken.
72 if (dWt.rows != C_out || dWt.cols != Cg_in * kH * kW) {
281 ✗ throw std::runtime_error("conv2d_backward_weight: dWt shape mismatch");
282 }
283 72 const int total = C_out * Cg_in * kH * kW;
284
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 if (total == 0) return;
285
286 72 const float* Xp = X.host_f32();
287 72 const float* dYp = dY.host_f32();
288 72 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 72 times.
✓ Branch 1 taken 1451 times.
1523 for (int c_out = 0; c_out < C_out; ++c_out) {
300 1451 const int g = c_out / Cg_out;
301
2/2
✓ Branch 0 taken 45921 times.
✓ Branch 1 taken 1451 times.
47372 for (int c_in_local = 0; c_in_local < Cg_in; ++c_in_local) {
302 45921 const int c_in = g * Cg_in + c_in_local;
303
2/2
✓ Branch 0 taken 67011 times.
✓ Branch 1 taken 45921 times.
112932 for (int kh = 0; kh < kH; ++kh) {
304
2/2
✓ Branch 0 taken 130729 times.
✓ Branch 1 taken 67011 times.
197740 for (int kw = 0; kw < kW; ++kw) {
305 130729 float acc = 0.0f;
306
2/2
✓ Branch 0 taken 139273 times.
✓ Branch 1 taken 130729 times.
270002 for (int n = 0; n < N; ++n) {
307
2/2
✓ Branch 0 taken 2282785 times.
✓ Branch 1 taken 139273 times.
2422058 for (int i_out = 0; i_out < H_out; ++i_out) {
308 2282785 const int in_h =
309 2282785 i_out * stride_h - pad_h + kh * dil_h;
310
4/4
✓ Branch 0 taken 2248570 times.
✓ Branch 1 taken 34215 times.
✓ Branch 2 taken 34215 times.
✓ Branch 3 taken 2214355 times.
2282785 if (in_h < 0 || in_h >= H) continue;
311
2/2
✓ Branch 0 taken 86078659 times.
✓ Branch 1 taken 2214355 times.
88293014 for (int j_out = 0; j_out < W_out; ++j_out) {
312 86078659 const int in_w =
313 86078659 j_out * stride_w - pad_w + kw * dil_w;
314
4/4
✓ Branch 0 taken 85906422 times.
✓ Branch 1 taken 172237 times.
✓ Branch 2 taken 172237 times.
✓ Branch 3 taken 85734185 times.
86078659 if (in_w < 0 || in_w >= W) continue;
315 85734185 const int x_idx =
316 85734185 ((n * C_in + c_in) * H + in_h) * W + in_w;
317 85734185 const int dy_idx =
318 171468370 ((n * C_out + c_out) * H_out + i_out) *
319 171468370 W_out + j_out;
320 85734185 acc += dYp[dy_idx] * Xp[x_idx];
321 85734185 }
322 2214355 }
323 139273 }
324 130729 const int w_idx =
325 130729 ((c_out * Cg_in + c_in_local) * kH + kh) * kW + kw;
326 130729 dWp[w_idx] += acc; // accumulate
327 130729 }
328 67011 }
329 45921 }
330 1451 }
331 72 }
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