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 |