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 |