src/cpu/conv_transpose2d.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // ─── CPU 2D transposed convolution ────────────────────────────────────────── | ||
| 2 | // | ||
| 3 | // FP32 scalar host implementations of: | ||
| 4 | // conv_transpose2d_forward / _backward_input / _backward_weight / _backward_bias | ||
| 5 | // | ||
| 6 | // The 2D counterpart of conv_transpose1d in conv1d.cpp, with H and W | ||
| 7 | // independently rescaled. The "learned upsample" primitive — SAM's mask | ||
| 8 | // decoder uses two 4x conv-transposes back-to-back; DPT depth heads use | ||
| 9 | // a 2x conv-transpose; many segmentation decoders rely on these instead | ||
| 10 | // of the cheaper bilinear upsample. | ||
| 11 | // | ||
| 12 | // ── Layout (NCHW) ─────────────────────────────────────────────────────────── | ||
| 13 | // X / dX : (N, C_in*H*W) flat index ((n*C_in + c)*H + h)*W + w | ||
| 14 | // Y / dY : (N, C_out*H_out*W_out) | ||
| 15 | // Wt / dWt: (C_in, (C_out/groups)*kH*kW) — input-channel-major | ||
| 16 | // flat index (c_in*Cg_out + oc_local) * (kH*kW) + (kh*kW + kw) | ||
| 17 | // bias : (C_out, 1) or null | ||
| 18 | // | ||
| 19 | // ── Accumulation (matches the conv2d / conv_transpose1d contract) ────────── | ||
| 20 | // *_forward / *_backward_input — output OVERWRITTEN. | ||
| 21 | // _backward_weight / _bias — dWt / dB ACCUMULATE (+=); caller zeros | ||
| 22 | // them first. | ||
| 23 | // | ||
| 24 | // Output spatial dims (torch ConvTranspose2d): | ||
| 25 | // H_out = (H-1)*stride_h - 2*pad_h + dil_h*(kH-1) + output_padding_h + 1 | ||
| 26 | // W_out = (W-1)*stride_w - 2*pad_w + dil_w*(kW-1) + output_padding_w + 1 | ||
| 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 | ✗ | [[noreturn]] void fail(const char* op, const std::string& reason) { | |
| 40 | ✗ | throw std::runtime_error(std::string("brotensor: ") + op + ": " + reason); | |
| 41 | ✗ | } | |
| 42 | |||
| 43 | 81 | void check_groups(const char* op, int C_in, int C_out, int groups) { | |
| 44 |
1/2✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
|
81 | if (groups < 1 || C_in % groups != 0 || C_out % groups != 0) { |
| 45 | ✗ | fail(op, "groups must be >=1 and divide both C_in and C_out"); | |
| 46 | } | ||
| 47 | 81 | } | |
| 48 | |||
| 49 | 227 | void require_fp32(const char* op, const ::brotensor::Tensor& t, | |
| 50 | const char* name) { | ||
| 51 |
1/2✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
|
227 | if (t.dtype != ::brotensor::Dtype::FP32) { |
| 52 | ✗ | fail(op, std::string(name) + " must be FP32 (CPU backend is FP32-only)"); | |
| 53 | } | ||
| 54 | 227 | } | |
| 55 | |||
| 56 | 162 | inline int convt2d_out(int L, int stride, int padding, int output_padding, | |
| 57 | int dilation, int kL) { | ||
| 58 | 324 | return (L - 1) * stride - 2 * padding + dilation * (kL - 1) | |
| 59 | 162 | + output_padding + 1; | |
| 60 | } | ||
| 61 | |||
| 62 | 81 | void check_geometry(const char* op, int kH, int kW, | |
| 63 | int stride_h, int stride_w, | ||
| 64 | int pad_h, int pad_w, | ||
| 65 | int output_padding_h, int output_padding_w, | ||
| 66 | int dil_h, int dil_w) { | ||
| 67 |
1/2✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
|
162 | if (kH < 1 || kW < 1 || stride_h < 1 || stride_w < 1 |
| 68 | 81 | || dil_h < 1 || dil_w < 1 || pad_h < 0 || pad_w < 0 | |
| 69 | 81 | || output_padding_h < 0 || output_padding_w < 0) { | |
| 70 | ✗ | fail(op, "kH/kW/stride/dilation >=1 and pad/output_padding >=0"); | |
| 71 | } | ||
| 72 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
81 | if (output_padding_h >= stride_h && output_padding_h >= dil_h) { |
| 73 | ✗ | fail(op, "output_padding_h must be < stride_h or < dil_h"); | |
| 74 | } | ||
| 75 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
81 | if (output_padding_w >= stride_w && output_padding_w >= dil_w) { |
| 76 | ✗ | fail(op, "output_padding_w must be < stride_w or < dil_w"); | |
| 77 | } | ||
| 78 | 81 | } | |
| 79 | |||
| 80 | } // namespace | ||
| 81 | |||
| 82 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 83 | // conv_transpose2d_forward | ||
| 84 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 85 | 72 | void conv_transpose2d_forward(const ::brotensor::Tensor& X, | |
| 86 | const ::brotensor::Tensor& Wt, | ||
| 87 | const ::brotensor::Tensor* bias, | ||
| 88 | int N, int C_in, int H, int W, | ||
| 89 | int C_out, int kH, int kW, | ||
| 90 | int stride_h, int stride_w, | ||
| 91 | int pad_h, int pad_w, | ||
| 92 | int output_padding_h, int output_padding_w, | ||
| 93 | int dil_h, int dil_w, int groups, | ||
| 94 | ::brotensor::Tensor& Y) { | ||
| 95 | 72 | const char* op = "conv_transpose2d_forward"; | |
| 96 | 72 | require_fp32(op, X, "X"); | |
| 97 | 72 | require_fp32(op, Wt, "Wt"); | |
| 98 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 57 times.
|
72 | if (bias) require_fp32(op, *bias, "bias"); |
| 99 | 72 | check_groups(op, C_in, C_out, groups); | |
| 100 | 144 | check_geometry(op, kH, kW, stride_h, stride_w, pad_h, pad_w, | |
| 101 | 72 | output_padding_h, output_padding_w, dil_h, dil_w); | |
| 102 | |||
| 103 | 72 | const int Cg_in = C_in / groups; | |
| 104 | 72 | const int Cg_out = C_out / groups; | |
| 105 | 144 | const int H_out = convt2d_out(H, stride_h, pad_h, output_padding_h, | |
| 106 | 72 | dil_h, kH); | |
| 107 | 144 | const int W_out = convt2d_out(W, stride_w, pad_w, output_padding_w, | |
| 108 | 72 | dil_w, kW); | |
| 109 |
1/4✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
72 | if (H_out <= 0 || W_out <= 0) fail(op, "non-positive output spatial size"); |
| 110 | |||
| 111 | 72 | const int kHW = kH * kW; | |
| 112 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | if (Wt.rows != C_in || Wt.cols != Cg_out * kHW) { |
| 113 | ✗ | fail(op, "Wt shape must be (C_in, (C_out/groups)*kH*kW)"); | |
| 114 | } | ||
| 115 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | if (X.rows != N || X.cols != C_in * H * W) { |
| 116 | ✗ | fail(op, "X shape must be (N, C_in*H*W)"); | |
| 117 | } | ||
| 118 |
3/4✓ Branch 0 taken 15 times.
✓ Branch 1 taken 57 times.
✓ Branch 2 taken 57 times.
✗ Branch 3 not taken.
|
72 | if (bias && (bias->rows != C_out || bias->cols != 1)) { |
| 119 | ✗ | fail(op, "bias shape must be (C_out, 1)"); | |
| 120 | } | ||
| 121 | |||
| 122 | 72 | const int out_cols = C_out * H_out * W_out; | |
| 123 |
4/6✓ Branch 0 taken 24 times.
✓ Branch 1 taken 48 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
|
72 | if (Y.rows != N || Y.cols != out_cols || Y.dtype != ::brotensor::Dtype::FP32) { |
| 124 | 48 | Y.resize(N, out_cols, ::brotensor::Dtype::FP32); | |
| 125 | 48 | } | |
| 126 |
2/4✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 72 times.
|
72 | if (N == 0 || out_cols == 0) return; |
| 127 | |||
| 128 | 72 | const float* Xp = X.host_f32(); | |
| 129 | 72 | const float* Wp = Wt.host_f32(); | |
| 130 |
2/2✓ Branch 0 taken 57 times.
✓ Branch 1 taken 15 times.
|
72 | const float* Bp = bias ? bias->host_f32() : nullptr; |
| 131 | 72 | float* Yp = Y.host_f32_mut(); | |
| 132 | |||
| 133 | // Seed every output pixel with its channel bias. Each n exclusively owns | ||
| 134 | // Y's batch slice n, so this parallelizes across n with no cross-thread | ||
| 135 | // writes; it fully completes (parallel_for blocks) before the scatter-add | ||
| 136 | // pass below starts touching the same Y buffer. | ||
| 137 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
148 | parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) { |
| 138 | 76 | const int n = static_cast<int>(ni); | |
| 139 |
2/2✓ Branch 0 taken 616 times.
✓ Branch 1 taken 76 times.
|
692 | for (int oc = 0; oc < C_out; ++oc) { |
| 140 |
2/2✓ Branch 0 taken 497 times.
✓ Branch 1 taken 119 times.
|
616 | const float bv = Bp ? Bp[oc] : 0.0f; |
| 141 | 616 | float* y_chan = | |
| 142 | 616 | Yp + (static_cast<long>(n) * C_out + oc) * H_out * W_out; | |
| 143 |
2/2✓ Branch 0 taken 315858 times.
✓ Branch 1 taken 616 times.
|
316474 | for (int o = 0; o < H_out * W_out; ++o) y_chan[o] = bv; |
| 144 | 616 | } | |
| 145 | 76 | }); | |
| 146 | |||
| 147 | // Interior region: the input rows/cols for which every kernel tap | ||
| 148 | // scatters into a valid output position (the mirror image of conv2d's | ||
| 149 | // gather-side split, applied to this scatter direction), computed once | ||
| 150 | // — independent of n/c_in. Only the thin border ring of input pixels | ||
| 151 | // needs the per-tap bounds check; the interior runs a branch-free | ||
| 152 | // kh/kw/oc_local loop. | ||
| 153 | 72 | int h_lo = (pad_h + stride_h - 1) / stride_h; | |
| 154 | 72 | int h_hi = H_out - 1 + pad_h - (kH - 1) * dil_h; | |
| 155 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | h_hi = (h_hi >= 0) ? (h_hi / stride_h) : -1; |
| 156 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | if (h_lo < 0) h_lo = 0; |
| 157 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | if (h_hi >= H) h_hi = H - 1; |
| 158 | |||
| 159 | 72 | int w_lo = (pad_w + stride_w - 1) / stride_w; | |
| 160 | 72 | int w_hi = W_out - 1 + pad_w - (kW - 1) * dil_w; | |
| 161 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | w_hi = (w_hi >= 0) ? (w_hi / stride_w) : -1; |
| 162 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | if (w_lo < 0) w_lo = 0; |
| 163 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | if (w_hi >= W) w_hi = W - 1; |
| 164 | |||
| 165 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 72 times.
|
72 | const bool has_interior = (h_lo <= h_hi) && (w_lo <= w_hi); |
| 166 | |||
| 167 | // Scatter-add. Input pixel (n, c_in, h, w) reaches output pixel | ||
| 168 | // ho = h*stride_h - pad_h + kh*dil_h | ||
| 169 | // wo = w*stride_w - pad_w + kw*dil_w | ||
| 170 | // in each output channel of c_in's group. Each n only ever scatters into | ||
| 171 | // Y's own batch slice n (oc is always paired with this same n), so this | ||
| 172 | // parallelizes across n with no cross-thread writes — the c_in loop | ||
| 173 | // inside stays sequential per-n, so the += accumulation across c_in into | ||
| 174 | // a shared oc is untouched by another thread. | ||
| 175 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
148 | parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) { |
| 176 | 76 | const int n = static_cast<int>(ni); | |
| 177 |
2/2✓ Branch 0 taken 1313 times.
✓ Branch 1 taken 74 times.
|
1387 | for (int c_in = 0; c_in < C_in; ++c_in) { |
| 178 | 1313 | const int g = c_in / Cg_in; | |
| 179 | 1313 | const int oc_base = g * Cg_out; | |
| 180 | 1313 | const float* x_chan = | |
| 181 | 1313 | Xp + (static_cast<long>(n) * C_in + c_in) * H * W; | |
| 182 | |||
| 183 | // Border pixel: same bounds-checked scatter as before. | ||
| 184 | 19281 | auto scatter_bordered = [&](int h, int w) { | |
| 185 | 17968 | const float xv = x_chan[static_cast<long>(h) * W + w]; | |
| 186 |
2/2✓ Branch 0 taken 20 times.
✓ Branch 1 taken 17948 times.
|
17968 | if (xv == 0.0f) return; |
| 187 | 17948 | const int ho_origin = h * stride_h - pad_h; | |
| 188 | 17948 | const int wo_origin = w * stride_w - pad_w; | |
| 189 |
2/2✓ Branch 0 taken 17948 times.
✓ Branch 1 taken 53986 times.
|
71934 | for (int kh = 0; kh < kH; ++kh) { |
| 190 | 53986 | const int ho = ho_origin + kh * dil_h; | |
| 191 |
4/4✓ Branch 0 taken 44461 times.
✓ Branch 1 taken 9525 times.
✓ Branch 2 taken 411 times.
✓ Branch 3 taken 44050 times.
|
53986 | if (ho < 0 || ho >= H_out) continue; |
| 192 |
2/2✓ Branch 0 taken 132486 times.
✓ Branch 1 taken 44050 times.
|
176536 | for (int kw = 0; kw < kW; ++kw) { |
| 193 | 132486 | const int wo = wo_origin + kw * dil_w; | |
| 194 |
4/4✓ Branch 0 taken 107113 times.
✓ Branch 1 taken 25373 times.
✓ Branch 2 taken 106131 times.
✓ Branch 3 taken 982 times.
|
132486 | if (wo < 0 || wo >= W_out) continue; |
| 195 |
2/2✓ Branch 0 taken 11223240 times.
✓ Branch 1 taken 106131 times.
|
11329371 | for (int oc_local = 0; oc_local < Cg_out; ++oc_local) { |
| 196 | 11223240 | const int oc = oc_base + oc_local; | |
| 197 | 11223240 | const int w_idx = | |
| 198 | 11223240 | (c_in * Cg_out + oc_local) * kHW | |
| 199 | 11223240 | + kh * kW + kw; | |
| 200 | 22446480 | Yp[(static_cast<long>(n) * C_out + oc) | |
| 201 | 11223240 | * H_out * W_out | |
| 202 | 11223240 | + static_cast<long>(ho) * W_out + wo] | |
| 203 | 22446480 | += xv * Wp[w_idx]; | |
| 204 | 11223240 | } | |
| 205 | 106131 | } | |
| 206 | 44050 | } | |
| 207 | 17968 | }; | |
| 208 | |||
| 209 | // Interior pixel: every tap guaranteed in-bounds — no checks. | ||
| 210 | 177177 | auto scatter_interior = [&](int h, int w) { | |
| 211 | 175864 | const float xv = x_chan[static_cast<long>(h) * W + w]; | |
| 212 |
2/2✓ Branch 0 taken 177 times.
✓ Branch 1 taken 175687 times.
|
175864 | if (xv == 0.0f) return; |
| 213 | 175687 | const int ho_origin = h * stride_h - pad_h; | |
| 214 | 175687 | const int wo_origin = w * stride_w - pad_w; | |
| 215 |
2/2✓ Branch 0 taken 175687 times.
✓ Branch 1 taken 453713 times.
|
629400 | for (int kh = 0; kh < kH; ++kh) { |
| 216 | 453713 | const int ho = ho_origin + kh * dil_h; | |
| 217 | 453713 | const int y_ho_base = ho * W_out; | |
| 218 | 453713 | const int w_kh_base = kh * kW; | |
| 219 |
2/2✓ Branch 0 taken 1214607 times.
✓ Branch 1 taken 453713 times.
|
1668320 | for (int kw = 0; kw < kW; ++kw) { |
| 220 | 1214607 | const int wo = wo_origin + kw * dil_w; | |
| 221 |
2/2✓ Branch 0 taken 119615749 times.
✓ Branch 1 taken 1214607 times.
|
120830356 | for (int oc_local = 0; oc_local < Cg_out; ++oc_local) { |
| 222 | 119615749 | const int oc = oc_base + oc_local; | |
| 223 | 119615749 | const int w_idx = | |
| 224 | 119615749 | (c_in * Cg_out + oc_local) * kHW | |
| 225 | 119615749 | + w_kh_base + kw; | |
| 226 | 239231498 | Yp[(static_cast<long>(n) * C_out + oc) | |
| 227 | 119615749 | * H_out * W_out + y_ho_base + wo] | |
| 228 | 239231498 | += xv * Wp[w_idx]; | |
| 229 | 119615749 | } | |
| 230 | 1214607 | } | |
| 231 | 453713 | } | |
| 232 | 175864 | }; | |
| 233 | |||
| 234 |
2/2✓ Branch 0 taken 14482 times.
✓ Branch 1 taken 1311 times.
|
15793 | for (int h = 0; h < H; ++h) { |
| 235 |
4/4✓ Branch 0 taken 14481 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 764 times.
✓ Branch 3 taken 13717 times.
|
14482 | const bool h_interior = has_interior && h >= h_lo && h <= h_hi; |
| 236 |
2/2✓ Branch 0 taken 886 times.
✓ Branch 1 taken 13594 times.
|
14482 | if (!h_interior) { |
| 237 |
2/2✓ Branch 0 taken 9951 times.
✓ Branch 1 taken 886 times.
|
10837 | for (int w = 0; w < W; ++w) scatter_bordered(h, w); |
| 238 | 886 | continue; | |
| 239 | } | ||
| 240 |
2/2✓ Branch 0 taken 7852 times.
✓ Branch 1 taken 13594 times.
|
21446 | for (int w = 0; w < w_lo; ++w) scatter_bordered(h, w); |
| 241 |
2/2✓ Branch 0 taken 175869 times.
✓ Branch 1 taken 13594 times.
|
189463 | for (int w = w_lo; w <= w_hi; ++w) scatter_interior(h, w); |
| 242 |
2/2✓ Branch 0 taken 166 times.
✓ Branch 1 taken 13594 times.
|
13760 | for (int w = w_hi + 1; w < W; ++w) scatter_bordered(h, w); |
| 243 | 13594 | } | |
| 244 | 1311 | } | |
| 245 | 74 | }); | |
| 246 | 72 | } | |
| 247 | |||
| 248 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 249 | // conv_transpose2d_backward_input | ||
| 250 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 251 | 5 | void conv_transpose2d_backward_input(const ::brotensor::Tensor& Wt, | |
| 252 | const ::brotensor::Tensor& dY, | ||
| 253 | int N, int C_in, int H, int W, | ||
| 254 | int C_out, int kH, int kW, | ||
| 255 | int stride_h, int stride_w, | ||
| 256 | int pad_h, int pad_w, | ||
| 257 | int output_padding_h, int output_padding_w, | ||
| 258 | int dil_h, int dil_w, int groups, | ||
| 259 | ::brotensor::Tensor& dX) { | ||
| 260 | 5 | const char* op = "conv_transpose2d_backward_input"; | |
| 261 | 5 | require_fp32(op, Wt, "Wt"); | |
| 262 | 5 | require_fp32(op, dY, "dY"); | |
| 263 | 5 | check_groups(op, C_in, C_out, groups); | |
| 264 | 10 | check_geometry(op, kH, kW, stride_h, stride_w, pad_h, pad_w, | |
| 265 | 5 | output_padding_h, output_padding_w, dil_h, dil_w); | |
| 266 | |||
| 267 | 5 | const int Cg_in = C_in / groups; | |
| 268 | 5 | const int Cg_out = C_out / groups; | |
| 269 | 10 | const int H_out = convt2d_out(H, stride_h, pad_h, output_padding_h, | |
| 270 | 5 | dil_h, kH); | |
| 271 | 10 | const int W_out = convt2d_out(W, stride_w, pad_w, output_padding_w, | |
| 272 | 5 | dil_w, kW); | |
| 273 |
1/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
5 | if (H_out <= 0 || W_out <= 0) fail(op, "non-positive output spatial size"); |
| 274 | 5 | const int kHW = kH * kW; | |
| 275 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | if (Wt.rows != C_in || Wt.cols != Cg_out * kHW) { |
| 276 | ✗ | fail(op, "Wt shape must be (C_in, (C_out/groups)*kH*kW)"); | |
| 277 | } | ||
| 278 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | if (dY.rows != N || dY.cols != C_out * H_out * W_out) { |
| 279 | ✗ | fail(op, "dY shape must be (N, C_out*H_out*W_out)"); | |
| 280 | } | ||
| 281 | 5 | const int in_cols = C_in * H * W; | |
| 282 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
5 | if (dX.rows != N || dX.cols != in_cols |
| 283 | ✗ | || dX.dtype != ::brotensor::Dtype::FP32) { | |
| 284 | 5 | dX.resize(N, in_cols, ::brotensor::Dtype::FP32); | |
| 285 | 5 | } | |
| 286 |
2/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
|
5 | if (N == 0 || in_cols == 0) return; |
| 287 | |||
| 288 | 5 | const float* Wp = Wt.host_f32(); | |
| 289 | 5 | const float* dYp = dY.host_f32(); | |
| 290 | 5 | float* dXp = dX.host_f32_mut(); | |
| 291 | |||
| 292 | // Adjoint of the transposed-conv scatter is a plain gather conv. Each n | ||
| 293 | // exclusively owns dX's batch slice n (dY/Wt are read-only), so this | ||
| 294 | // parallelizes across n with no cross-thread writes. | ||
| 295 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
11 | parallel_for(static_cast<std::size_t>(N), [&](std::size_t ni) { |
| 296 | 6 | const int n = static_cast<int>(ni); | |
| 297 |
2/2✓ Branch 0 taken 18 times.
✓ Branch 1 taken 6 times.
|
24 | for (int c_in = 0; c_in < C_in; ++c_in) { |
| 298 | 18 | const int g = c_in / Cg_in; | |
| 299 | 18 | const int oc_base = g * Cg_out; | |
| 300 |
2/2✓ Branch 0 taken 76 times.
✓ Branch 1 taken 18 times.
|
94 | for (int h = 0; h < H; ++h) { |
| 301 | 76 | const int ho_origin = h * stride_h - pad_h; | |
| 302 |
2/2✓ Branch 0 taken 386 times.
✓ Branch 1 taken 76 times.
|
462 | for (int w = 0; w < W; ++w) { |
| 303 | 386 | const int wo_origin = w * stride_w - pad_w; | |
| 304 | 386 | float acc = 0.0f; | |
| 305 |
2/2✓ Branch 0 taken 1393 times.
✓ Branch 1 taken 386 times.
|
1779 | for (int kh = 0; kh < kH; ++kh) { |
| 306 | 1393 | const int ho = ho_origin + kh * dil_h; | |
| 307 |
4/4✓ Branch 0 taken 1307 times.
✓ Branch 1 taken 86 times.
✓ Branch 2 taken 80 times.
✓ Branch 3 taken 1227 times.
|
1393 | if (ho < 0 || ho >= H_out) continue; |
| 308 |
2/2✓ Branch 0 taken 4451 times.
✓ Branch 1 taken 1227 times.
|
5678 | for (int kw = 0; kw < kW; ++kw) { |
| 309 | 4451 | const int wo = wo_origin + kw * dil_w; | |
| 310 |
4/4✓ Branch 0 taken 4267 times.
✓ Branch 1 taken 184 times.
✓ Branch 2 taken 4005 times.
✓ Branch 3 taken 262 times.
|
4451 | if (wo < 0 || wo >= W_out) continue; |
| 311 |
2/2✓ Branch 0 taken 11240 times.
✓ Branch 1 taken 4005 times.
|
15245 | for (int oc_local = 0; oc_local < Cg_out; ++oc_local) { |
| 312 | 11240 | const int oc = oc_base + oc_local; | |
| 313 | 11240 | const int w_idx = | |
| 314 | 11240 | (c_in * Cg_out + oc_local) * kHW | |
| 315 | 11240 | + kh * kW + kw; | |
| 316 | 11240 | const long dy_idx = | |
| 317 | 11240 | (static_cast<long>(n) * C_out + oc) | |
| 318 | 11240 | * H_out * W_out | |
| 319 | 11240 | + static_cast<long>(ho) * W_out + wo; | |
| 320 | 11240 | acc += dYp[dy_idx] * Wp[w_idx]; | |
| 321 | 11240 | } | |
| 322 | 4005 | } | |
| 323 | 1227 | } | |
| 324 | 772 | dXp[(static_cast<long>(n) * C_in + c_in) * H * W | |
| 325 | 1158 | + static_cast<long>(h) * W + w] = acc; | |
| 326 | 386 | } | |
| 327 | 76 | } | |
| 328 | 18 | } | |
| 329 | 6 | }); | |
| 330 | 5 | } | |
| 331 | |||
| 332 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 333 | // conv_transpose2d_backward_weight | ||
| 334 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 335 | 4 | void conv_transpose2d_backward_weight(const ::brotensor::Tensor& X, | |
| 336 | const ::brotensor::Tensor& dY, | ||
| 337 | int N, int C_in, int H, int W, | ||
| 338 | int C_out, int kH, int kW, | ||
| 339 | int stride_h, int stride_w, | ||
| 340 | int pad_h, int pad_w, | ||
| 341 | int output_padding_h, int output_padding_w, | ||
| 342 | int dil_h, int dil_w, int groups, | ||
| 343 | ::brotensor::Tensor& dWt) { | ||
| 344 | 4 | const char* op = "conv_transpose2d_backward_weight"; | |
| 345 | 4 | require_fp32(op, X, "X"); | |
| 346 | 4 | require_fp32(op, dY, "dY"); | |
| 347 | 4 | require_fp32(op, dWt, "dWt"); | |
| 348 | 4 | check_groups(op, C_in, C_out, groups); | |
| 349 | 8 | check_geometry(op, kH, kW, stride_h, stride_w, pad_h, pad_w, | |
| 350 | 4 | output_padding_h, output_padding_w, dil_h, dil_w); | |
| 351 | |||
| 352 | 4 | const int Cg_in = C_in / groups; | |
| 353 | 4 | const int Cg_out = C_out / groups; | |
| 354 | 8 | const int H_out = convt2d_out(H, stride_h, pad_h, output_padding_h, | |
| 355 | 4 | dil_h, kH); | |
| 356 | 8 | const int W_out = convt2d_out(W, stride_w, pad_w, output_padding_w, | |
| 357 | 4 | dil_w, kW); | |
| 358 |
1/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
4 | if (H_out <= 0 || W_out <= 0) fail(op, "non-positive output spatial size"); |
| 359 | 4 | const int kHW = kH * kW; | |
| 360 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (dWt.rows != C_in || dWt.cols != Cg_out * kHW) { |
| 361 | ✗ | fail(op, "dWt shape must be (C_in, (C_out/groups)*kH*kW)"); | |
| 362 | } | ||
| 363 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (X.rows != N || X.cols != C_in * H * W) { |
| 364 | ✗ | fail(op, "X shape must be (N, C_in*H*W)"); | |
| 365 | } | ||
| 366 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (dY.rows != N || dY.cols != C_out * H_out * W_out) { |
| 367 | ✗ | fail(op, "dY shape must be (N, C_out*H_out*W_out)"); | |
| 368 | } | ||
| 369 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
|
4 | if (C_in == 0 || Cg_out == 0 || kHW == 0) return; |
| 370 | |||
| 371 | 4 | const float* Xp = X.host_f32(); | |
| 372 | 4 | const float* dYp = dY.host_f32(); | |
| 373 | 4 | float* dWp = dWt.host_f32_mut(); | |
| 374 | |||
| 375 | // One accumulation per weight element; += into dWt (caller zeroed it). | ||
| 376 | // | ||
| 377 | // NOT parallelized over n: n is the innermost reduction axis (every | ||
| 378 | // batch item's contribution sums into the same dWp element), not an | ||
| 379 | // outer axis — parallelizing it would race every thread on the same | ||
| 380 | // dWt element. Left single-threaded per this task's scope. | ||
| 381 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 13 times.
|
17 | for (int c_in = 0; c_in < C_in; ++c_in) { |
| 382 | 13 | const int g = c_in / Cg_in; | |
| 383 | 13 | const int oc_base = g * Cg_out; | |
| 384 |
2/2✓ Branch 0 taken 36 times.
✓ Branch 1 taken 13 times.
|
49 | for (int oc_local = 0; oc_local < Cg_out; ++oc_local) { |
| 385 | 36 | const int oc = oc_base + oc_local; | |
| 386 |
2/2✓ Branch 0 taken 120 times.
✓ Branch 1 taken 36 times.
|
156 | for (int kh = 0; kh < kH; ++kh) { |
| 387 |
2/2✓ Branch 0 taken 408 times.
✓ Branch 1 taken 120 times.
|
528 | for (int kw = 0; kw < kW; ++kw) { |
| 388 | 408 | float acc = 0.0f; | |
| 389 |
2/2✓ Branch 0 taken 708 times.
✓ Branch 1 taken 408 times.
|
1116 | for (int n = 0; n < N; ++n) { |
| 390 | 708 | const float* x_chan = | |
| 391 | 708 | Xp + (static_cast<long>(n) * C_in + c_in) * H * W; | |
| 392 | 708 | const float* dy_chan = | |
| 393 | 1416 | dYp + (static_cast<long>(n) * C_out + oc) | |
| 394 | 708 | * H_out * W_out; | |
| 395 |
2/2✓ Branch 0 taken 3612 times.
✓ Branch 1 taken 708 times.
|
4320 | for (int h = 0; h < H; ++h) { |
| 396 | 3612 | const int ho = h * stride_h - pad_h + kh * dil_h; | |
| 397 |
4/4✓ Branch 0 taken 3408 times.
✓ Branch 1 taken 204 times.
✓ Branch 2 taken 3204 times.
✓ Branch 3 taken 204 times.
|
3612 | if (ho < 0 || ho >= H_out) continue; |
| 398 |
2/2✓ Branch 0 taken 19644 times.
✓ Branch 1 taken 3204 times.
|
22848 | for (int w = 0; w < W; ++w) { |
| 399 | 19644 | const int wo = | |
| 400 | 19644 | w * stride_w - pad_w + kw * dil_w; | |
| 401 |
4/4✓ Branch 0 taken 18720 times.
✓ Branch 1 taken 924 times.
✓ Branch 2 taken 924 times.
✓ Branch 3 taken 17796 times.
|
19644 | if (wo < 0 || wo >= W_out) continue; |
| 402 | 35592 | acc += x_chan[h * W + w] | |
| 403 | 17796 | * dy_chan[ho * W_out + wo]; | |
| 404 | 17796 | } | |
| 405 | 3204 | } | |
| 406 | 708 | } | |
| 407 | 408 | dWp[(c_in * Cg_out + oc_local) * kHW + kh * kW + kw] | |
| 408 | 816 | += acc; | |
| 409 | 408 | } | |
| 410 | 120 | } | |
| 411 | 36 | } | |
| 412 | 13 | } | |
| 413 | 4 | } | |
| 414 | |||
| 415 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 416 | // conv_transpose2d_backward_bias | ||
| 417 | // ════════════════════════════════════════════════════════════════════════════ | ||
| 418 | 2 | void conv_transpose2d_backward_bias(const ::brotensor::Tensor& dY, | |
| 419 | int N, int C_out, int H_out, int W_out, | ||
| 420 | ::brotensor::Tensor& dB) { | ||
| 421 | 2 | const char* op = "conv_transpose2d_backward_bias"; | |
| 422 | 2 | require_fp32(op, dY, "dY"); | |
| 423 | 2 | require_fp32(op, dB, "dB"); | |
| 424 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (dB.rows != C_out || dB.cols != 1) { |
| 425 | ✗ | fail(op, "dB shape must be (C_out, 1)"); | |
| 426 | } | ||
| 427 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (dY.rows != N || dY.cols != C_out * H_out * W_out) { |
| 428 | ✗ | fail(op, "dY shape must be (N, C_out*H_out*W_out)"); | |
| 429 | } | ||
| 430 |
4/8✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
|
2 | if (C_out == 0 || N == 0 || H_out == 0 || W_out == 0) return; |
| 431 | |||
| 432 | 2 | const float* dYp = dY.host_f32(); | |
| 433 | 2 | float* dBp = dB.host_f32_mut(); | |
| 434 | |||
| 435 | // Per-output-channel sum over (N, H_out, W_out); += into dB. | ||
| 436 | // | ||
| 437 | // NOT parallelized over n: same reason as backward_weight above — n is | ||
| 438 | // the reduction axis here, not the outer axis. | ||
| 439 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
|
7 | for (int oc = 0; oc < C_out; ++oc) { |
| 440 | 5 | float acc = 0.0f; | |
| 441 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
|
13 | for (int n = 0; n < N; ++n) { |
| 442 | 8 | const float* dy_chan = | |
| 443 | 8 | dYp + (static_cast<long>(n) * C_out + oc) * H_out * W_out; | |
| 444 |
2/2✓ Branch 0 taken 770 times.
✓ Branch 1 taken 8 times.
|
778 | for (int i = 0; i < H_out * W_out; ++i) acc += dy_chan[i]; |
| 445 | 8 | } | |
| 446 | 5 | dBp[oc] += acc; | |
| 447 | 5 | } | |
| 448 | 2 | } | |
| 449 | |||
| 450 | } // namespace brotensor::detail::cpu | ||
| 451 |