src/cpu/batch_norm.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // ─── CPU BatchNorm ops ───────────────────────────────────────────────────── | ||
| 2 | // | ||
| 3 | // Standard NCHW BatchNorm, FP32-only (CPU backend is FP32-only). Statistics | ||
| 4 | // are reduced over (N, H, W) for each channel — i.e. one mean / var per C. | ||
| 5 | // This is the variant pretrained ResNet / DETR-ResNet50 / classic | ||
| 6 | // Mask2Former backbones use; differs from GroupNorm (which reduces over | ||
| 7 | // a (channels_per_group, H, W) tile within a single sample). | ||
| 8 | // | ||
| 9 | // Three slots: | ||
| 10 | // | ||
| 11 | // batch_norm_forward — training. Computes per-channel batch mean/var | ||
| 12 | // over (N, H, W); writes Y; updates running_mean / | ||
| 13 | // running_var in place via momentum; saves | ||
| 14 | // batch mean and rstd for the backward pass. | ||
| 15 | // Convention: | ||
| 16 | // running = (1 - momentum) * running | ||
| 17 | // + momentum * batch_stat | ||
| 18 | // (PyTorch's nn.BatchNorm2d convention; the "batch" | ||
| 19 | // variance fed into running_var is the *unbiased* | ||
| 20 | // estimator — the sum of squared deviations from the | ||
| 21 | // channel mean over M/(M-1).) | ||
| 22 | // | ||
| 23 | // batch_norm_inference — uses running_mean / running_var; pure forward; | ||
| 24 | // no state mutation. This is what loaded pretrained | ||
| 25 | // checkpoints want during inference. | ||
| 26 | // | ||
| 27 | // batch_norm_backward — given X + saved batch mean/rstd from forward, | ||
| 28 | // computes dX (overwritten) plus dGamma / dBeta | ||
| 29 | // (accumulated; caller zeros). | ||
| 30 | // | ||
| 31 | // Reduction width M = N * H * W. Running-var uses unbiased denom (M-1) when | ||
| 32 | // M > 1; for M==1 it stays equal to the biased value (matches PyTorch which | ||
| 33 | // just uses biased when bessel correction is undefined). Forward Y and | ||
| 34 | // backward dX use the *biased* var (= 1/M sum (x-mean)^2) — same as PyTorch. | ||
| 35 | |||
| 36 | #include <brotensor/tensor.h> | ||
| 37 | #include <brotensor/detail/cpu/thread_pool.h> | ||
| 38 | |||
| 39 | #include <cmath> | ||
| 40 | #include <cstddef> | ||
| 41 | #include <stdexcept> | ||
| 42 | #include <string> | ||
| 43 | #include <vector> | ||
| 44 | |||
| 45 | namespace brotensor::detail::cpu { | ||
| 46 | |||
| 47 | namespace { | ||
| 48 | |||
| 49 | 86 | inline void check_fp32(const ::brotensor::Tensor& t, | |
| 50 | const char* op, const char* name) { | ||
| 51 |
1/2✓ Branch 0 taken 86 times.
✗ Branch 1 not taken.
|
86 | if (t.dtype != Dtype::FP32) { |
| 52 | ✗ | throw std::runtime_error(std::string(op) + ": " + name + | |
| 53 | " must be FP32 (CPU backend is FP32-only)"); | ||
| 54 | } | ||
| 55 | 86 | } | |
| 56 | |||
| 57 | 61 | inline void check_per_channel(const ::brotensor::Tensor& t, | |
| 58 | int C, const char* op, const char* name) { | ||
| 59 |
1/2✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
|
61 | if (t.size() != C) { |
| 60 | ✗ | throw std::runtime_error(std::string(op) + ": " + name + | |
| 61 | " must have C elements"); | ||
| 62 | } | ||
| 63 | 61 | } | |
| 64 | |||
| 65 | } // namespace | ||
| 66 | |||
| 67 | 7 | void batch_norm_forward(const ::brotensor::Tensor& X, | |
| 68 | const ::brotensor::Tensor& gamma, | ||
| 69 | const ::brotensor::Tensor& beta, | ||
| 70 | ::brotensor::Tensor& running_mean, | ||
| 71 | ::brotensor::Tensor& running_var, | ||
| 72 | int N, int C, int H, int W, | ||
| 73 | float eps, float momentum, | ||
| 74 | ::brotensor::Tensor& Y, | ||
| 75 | ::brotensor::Tensor& saved_mean, | ||
| 76 | ::brotensor::Tensor& saved_rstd) { | ||
| 77 | 7 | check_fp32(X, "batch_norm_forward", "X"); | |
| 78 | 7 | check_fp32(gamma, "batch_norm_forward", "gamma"); | |
| 79 | 7 | check_fp32(beta, "batch_norm_forward", "beta"); | |
| 80 | 7 | check_fp32(running_mean, "batch_norm_forward", "running_mean"); | |
| 81 | 7 | check_fp32(running_var, "batch_norm_forward", "running_var"); | |
| 82 | 7 | check_per_channel(gamma, C, "batch_norm_forward", "gamma"); | |
| 83 | 7 | check_per_channel(beta, C, "batch_norm_forward", "beta"); | |
| 84 | 7 | check_per_channel(running_mean, C, "batch_norm_forward", "running_mean"); | |
| 85 | 7 | check_per_channel(running_var, C, "batch_norm_forward", "running_var"); | |
| 86 | |||
| 87 | 7 | const int spatial = H * W; | |
| 88 | 7 | const int cols = C * spatial; | |
| 89 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
7 | if (Y.rows != N || Y.cols != cols || Y.dtype != Dtype::FP32) { |
| 90 | 7 | Y.resize(N, cols, Dtype::FP32); | |
| 91 | 7 | } | |
| 92 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
7 | if (saved_mean.rows != C || saved_mean.cols != 1 || |
| 93 | ✗ | saved_mean.dtype != Dtype::FP32) { | |
| 94 | 7 | saved_mean.resize(C, 1, Dtype::FP32); | |
| 95 | 7 | } | |
| 96 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
7 | if (saved_rstd.rows != C || saved_rstd.cols != 1 || |
| 97 | ✗ | saved_rstd.dtype != Dtype::FP32) { | |
| 98 | 7 | saved_rstd.resize(C, 1, Dtype::FP32); | |
| 99 | 7 | } | |
| 100 |
2/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
|
7 | if (N == 0 || cols == 0) return; |
| 101 | |||
| 102 | 7 | const float* Xp = X.host_f32(); | |
| 103 | 7 | const float* gp = gamma.host_f32(); | |
| 104 | 7 | const float* bp = beta.host_f32(); | |
| 105 | 7 | float* rmp = running_mean.host_f32_mut(); | |
| 106 | 7 | float* rvp = running_var.host_f32_mut(); | |
| 107 | 7 | float* Yp = Y.host_f32_mut(); | |
| 108 | 7 | float* smp = saved_mean.host_f32_mut(); | |
| 109 | 7 | float* srp = saved_rstd.host_f32_mut(); | |
| 110 | |||
| 111 | 7 | const int M = N * spatial; | |
| 112 | 7 | const float inv_M = 1.0f / static_cast<float>(M); | |
| 113 | // Bessel correction factor for the running-var update. For M==1 we leave | ||
| 114 | // the biased estimate (matches PyTorch's behaviour). | ||
| 115 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 time.
|
7 | const float bessel = (M > 1) ? static_cast<float>(M) / |
| 116 | 6 | static_cast<float>(M - 1) | |
| 117 | : 1.0f; | ||
| 118 | |||
| 119 | // Each channel c owns rmp[c]/rvp[c]/smp[c]/srp[c] and Y's per-channel | ||
| 120 | // slice exclusively (X/gamma/beta are read-only shared, and no other c | ||
| 121 | // touches these locations), so this parallelizes across c with no | ||
| 122 | // cross-thread writes. | ||
| 123 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
36 | parallel_for(static_cast<std::size_t>(C), [&](std::size_t ci) { |
| 124 | 29 | const int c = static_cast<int>(ci); | |
| 125 | // Pass 1: the channel's mean across (N, H, W), then the sum of squared | ||
| 126 | // deviations from it. E[x^2] - E[x]^2 is one pass but cancels | ||
| 127 | // catastrophically once a channel's mean dwarfs its spread — a | ||
| 128 | // near-constant channel puts both terms on the same large value, so | ||
| 129 | // their FP32 difference is noise and can come out negative, making rstd | ||
| 130 | // NaN. Deviations are non-negative by construction, so var cannot go | ||
| 131 | // negative. | ||
| 132 | 29 | float sum = 0.0f; | |
| 133 |
2/2✓ Branch 0 taken 70 times.
✓ Branch 1 taken 29 times.
|
99 | for (int n = 0; n < N; ++n) { |
| 134 | 70 | const float* x_chan = Xp + (n * C + c) * spatial; | |
| 135 |
2/2✓ Branch 0 taken 2545 times.
✓ Branch 1 taken 70 times.
|
2615 | for (int s = 0; s < spatial; ++s) sum += x_chan[s]; |
| 136 | 70 | } | |
| 137 | 29 | const float mean = sum * inv_M; | |
| 138 | |||
| 139 | 29 | float sumsq = 0.0f; | |
| 140 |
2/2✓ Branch 0 taken 67 times.
✓ Branch 1 taken 29 times.
|
96 | for (int n = 0; n < N; ++n) { |
| 141 | 67 | const float* x_chan = Xp + (n * C + c) * spatial; | |
| 142 |
2/2✓ Branch 0 taken 2534 times.
✓ Branch 1 taken 67 times.
|
2601 | for (int s = 0; s < spatial; ++s) { |
| 143 | 2534 | const float d = x_chan[s] - mean; | |
| 144 | 2534 | sumsq += d * d; | |
| 145 | 2534 | } | |
| 146 | 67 | } | |
| 147 | 29 | const float var_b = sumsq * inv_M; // biased | |
| 148 | 29 | const float rstd = 1.0f / std::sqrt(var_b + eps); | |
| 149 | 29 | const float var_unb = var_b * bessel; // unbiased | |
| 150 | |||
| 151 | // Save for backward. | ||
| 152 | 29 | smp[c] = mean; | |
| 153 | 29 | srp[c] = rstd; | |
| 154 | |||
| 155 | // Update running stats (PyTorch convention). | ||
| 156 | 29 | rmp[c] = (1.0f - momentum) * rmp[c] + momentum * mean; | |
| 157 | 29 | rvp[c] = (1.0f - momentum) * rvp[c] + momentum * var_unb; | |
| 158 | |||
| 159 | // Pass 2: write Y = (x - mean) * rstd * gamma + beta. | ||
| 160 | 29 | const float gv = gp[c]; | |
| 161 | 29 | const float bv = bp[c]; | |
| 162 |
2/2✓ Branch 0 taken 69 times.
✓ Branch 1 taken 29 times.
|
98 | for (int n = 0; n < N; ++n) { |
| 163 | 69 | const float* x_chan = Xp + (n * C + c) * spatial; | |
| 164 | 69 | float* y_chan = Yp + (n * C + c) * spatial; | |
| 165 |
2/2✓ Branch 0 taken 2567 times.
✓ Branch 1 taken 69 times.
|
2636 | for (int s = 0; s < spatial; ++s) { |
| 166 | 2567 | y_chan[s] = (x_chan[s] - mean) * rstd * gv + bv; | |
| 167 | 2567 | } | |
| 168 | 69 | } | |
| 169 | 29 | }); | |
| 170 | 7 | } | |
| 171 | |||
| 172 | 6 | void batch_norm_inference(const ::brotensor::Tensor& X, | |
| 173 | const ::brotensor::Tensor& gamma, | ||
| 174 | const ::brotensor::Tensor& beta, | ||
| 175 | const ::brotensor::Tensor& running_mean, | ||
| 176 | const ::brotensor::Tensor& running_var, | ||
| 177 | int N, int C, int H, int W, | ||
| 178 | float eps, | ||
| 179 | ::brotensor::Tensor& Y) { | ||
| 180 | 6 | check_fp32(X, "batch_norm_inference", "X"); | |
| 181 | 6 | check_fp32(gamma, "batch_norm_inference", "gamma"); | |
| 182 | 6 | check_fp32(beta, "batch_norm_inference", "beta"); | |
| 183 | 6 | check_fp32(running_mean, "batch_norm_inference", "running_mean"); | |
| 184 | 6 | check_fp32(running_var, "batch_norm_inference", "running_var"); | |
| 185 | 6 | check_per_channel(gamma, C, "batch_norm_inference", "gamma"); | |
| 186 | 6 | check_per_channel(beta, C, "batch_norm_inference", "beta"); | |
| 187 | 6 | check_per_channel(running_mean, C, "batch_norm_inference", "running_mean"); | |
| 188 | 6 | check_per_channel(running_var, C, "batch_norm_inference", "running_var"); | |
| 189 | |||
| 190 | 6 | const int spatial = H * W; | |
| 191 | 6 | const int cols = C * spatial; | |
| 192 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
6 | if (Y.rows != N || Y.cols != cols || Y.dtype != Dtype::FP32) { |
| 193 | 6 | Y.resize(N, cols, Dtype::FP32); | |
| 194 | 6 | } | |
| 195 |
2/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
|
6 | if (N == 0 || cols == 0) return; |
| 196 | |||
| 197 | 6 | const float* Xp = X.host_f32(); | |
| 198 | 6 | const float* gp = gamma.host_f32(); | |
| 199 | 6 | const float* bp = beta.host_f32(); | |
| 200 | 6 | const float* rmp = running_mean.host_f32(); | |
| 201 | 6 | const float* rvp = running_var.host_f32(); | |
| 202 | 6 | float* Yp = Y.host_f32_mut(); | |
| 203 | |||
| 204 | // Precompute per-channel affine y = x * a_c + b_c. | ||
| 205 | // (x - mu) / sqrt(var + eps) * gamma + beta | ||
| 206 | // = x * (gamma / sqrt(var+eps)) + (beta - mu * gamma / sqrt(var+eps)) | ||
| 207 | // Each channel c owns Y's per-channel slice exclusively, so this | ||
| 208 | // parallelizes across c with no cross-thread writes. | ||
| 209 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
41 | parallel_for(static_cast<std::size_t>(C), [&](std::size_t ci) { |
| 210 | 35 | const int c = static_cast<int>(ci); | |
| 211 | 35 | const float inv = 1.0f / std::sqrt(rvp[c] + eps); | |
| 212 | 35 | const float a = gp[c] * inv; | |
| 213 | 35 | const float b = bp[c] - rmp[c] * a; | |
| 214 |
2/2✓ Branch 0 taken 51 times.
✓ Branch 1 taken 35 times.
|
86 | for (int n = 0; n < N; ++n) { |
| 215 | 51 | const float* x_chan = Xp + (n * C + c) * spatial; | |
| 216 | 51 | float* y_chan = Yp + (n * C + c) * spatial; | |
| 217 |
2/2✓ Branch 0 taken 1944 times.
✓ Branch 1 taken 51 times.
|
1995 | for (int s = 0; s < spatial; ++s) { |
| 218 | 1944 | y_chan[s] = x_chan[s] * a + b; | |
| 219 | 1944 | } | |
| 220 | 51 | } | |
| 221 | 35 | }); | |
| 222 | 6 | } | |
| 223 | |||
| 224 | 3 | void batch_norm_backward(const ::brotensor::Tensor& X, | |
| 225 | const ::brotensor::Tensor& gamma, | ||
| 226 | const ::brotensor::Tensor& saved_mean, | ||
| 227 | const ::brotensor::Tensor& saved_rstd, | ||
| 228 | const ::brotensor::Tensor& dY, | ||
| 229 | int N, int C, int H, int W, | ||
| 230 | ::brotensor::Tensor& dX, | ||
| 231 | ::brotensor::Tensor& dGamma, | ||
| 232 | ::brotensor::Tensor& dBeta) { | ||
| 233 | 3 | check_fp32(X, "batch_norm_backward", "X"); | |
| 234 | 3 | check_fp32(gamma, "batch_norm_backward", "gamma"); | |
| 235 | 3 | check_fp32(saved_mean, "batch_norm_backward", "saved_mean"); | |
| 236 | 3 | check_fp32(saved_rstd, "batch_norm_backward", "saved_rstd"); | |
| 237 | 3 | check_fp32(dY, "batch_norm_backward", "dY"); | |
| 238 | 3 | check_fp32(dGamma, "batch_norm_backward", "dGamma"); | |
| 239 | 3 | check_fp32(dBeta, "batch_norm_backward", "dBeta"); | |
| 240 | 3 | check_per_channel(gamma, C, "batch_norm_backward", "gamma"); | |
| 241 | 3 | check_per_channel(saved_mean, C, "batch_norm_backward", "saved_mean"); | |
| 242 | 3 | check_per_channel(saved_rstd, C, "batch_norm_backward", "saved_rstd"); | |
| 243 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
6 | if (dGamma.rows != C || dGamma.cols != 1 || |
| 244 | 3 | dBeta.rows != C || dBeta.cols != 1) { | |
| 245 | ✗ | throw std::runtime_error("batch_norm_backward: dGamma/dBeta must be (C,1)"); | |
| 246 | } | ||
| 247 | |||
| 248 | 3 | const int spatial = H * W; | |
| 249 | 3 | const int cols = C * spatial; | |
| 250 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | if (dY.rows != N || dY.cols != cols) { |
| 251 | ✗ | throw std::runtime_error("batch_norm_backward: dY shape mismatch"); | |
| 252 | } | ||
| 253 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | if (X.rows != N || X.cols != cols) { |
| 254 | ✗ | throw std::runtime_error("batch_norm_backward: X shape mismatch"); | |
| 255 | } | ||
| 256 |
1/6✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
3 | if (dX.rows != N || dX.cols != cols || dX.dtype != Dtype::FP32) { |
| 257 | 3 | dX.resize(N, cols, Dtype::FP32); | |
| 258 | 3 | } | |
| 259 |
2/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
|
3 | if (N == 0 || cols == 0) return; |
| 260 | |||
| 261 | 3 | const float* Xp = X.host_f32(); | |
| 262 | 3 | const float* gp = gamma.host_f32(); | |
| 263 | 3 | const float* mp = saved_mean.host_f32(); | |
| 264 | 3 | const float* rp = saved_rstd.host_f32(); | |
| 265 | 3 | const float* dYp = dY.host_f32(); | |
| 266 | 3 | float* dXp = dX.host_f32_mut(); | |
| 267 | 3 | float* dGp = dGamma.host_f32_mut(); | |
| 268 | 3 | float* dBp = dBeta.host_f32_mut(); | |
| 269 | |||
| 270 | 3 | const int M = N * spatial; | |
| 271 | 3 | const float inv_M = 1.0f / static_cast<float>(M); | |
| 272 | |||
| 273 | // Per-channel: derive dGamma, dBeta, and the two reduction sums used | ||
| 274 | // by the dX formula. Then a second pass over (N, H, W) writes dX. | ||
| 275 | // | ||
| 276 | // xhat = (x - mean) * rstd | ||
| 277 | // dxhat = dY * gamma | ||
| 278 | // dGamma_c += sum (dY * xhat) | ||
| 279 | // dBeta_c += sum dY | ||
| 280 | // dX = rstd * (dxhat - (sum dxhat + xhat * sum(dxhat*xhat)) / M) | ||
| 281 | // | ||
| 282 | // The outer axis here is already C (channel), and — unlike GroupNorm's | ||
| 283 | // batch axis — each channel c owns dGp[c]/dBp[c] and dX's per-channel | ||
| 284 | // slice exclusively: no other c ever touches these locations, so the | ||
| 285 | // per-channel reduction over (N, H, W) is entirely local to this c's | ||
| 286 | // work and safe to run concurrently with other c's. The one thing that | ||
| 287 | // must change to parallelize: xhat_buf was a single buffer shared and | ||
| 288 | // reused across every c in the original serial loop, so it's now | ||
| 289 | // declared LOCAL to the lambda (one per c) instead — safe even across | ||
| 290 | // threads since each invocation gets its own buffer. | ||
| 291 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
19 | parallel_for(static_cast<std::size_t>(C), [&](std::size_t ci) { |
| 292 | 16 | const int c = static_cast<int>(ci); | |
| 293 | 16 | std::vector<float> xhat_buf(static_cast<std::size_t>(M)); | |
| 294 | |||
| 295 | 16 | const float mean = mp[c]; | |
| 296 | 16 | const float rstd = rp[c]; | |
| 297 | 16 | const float gv = gp[c]; | |
| 298 | |||
| 299 | 16 | float sum_dY = 0.0f; | |
| 300 | 16 | float sum_dY_xh = 0.0f; | |
| 301 |
2/2✓ Branch 0 taken 43 times.
✓ Branch 1 taken 16 times.
|
59 | for (int n = 0; n < N; ++n) { |
| 302 | 43 | const float* x_chan = Xp + (n * C + c) * spatial; | |
| 303 | 43 | const float* dy_chan = dYp + (n * C + c) * spatial; | |
| 304 | 43 | float* xh_chan = xhat_buf.data() + n * spatial; | |
| 305 |
2/2✓ Branch 0 taken 1853 times.
✓ Branch 1 taken 43 times.
|
1896 | for (int s = 0; s < spatial; ++s) { |
| 306 | 1853 | const float xh = (x_chan[s] - mean) * rstd; | |
| 307 | 1853 | xh_chan[s] = xh; | |
| 308 | 1853 | sum_dY += dy_chan[s]; | |
| 309 | 1853 | sum_dY_xh += dy_chan[s] * xh; | |
| 310 | 1853 | } | |
| 311 | 43 | } | |
| 312 | |||
| 313 | 16 | dGp[c] += sum_dY_xh; // accumulate | |
| 314 | 16 | dBp[c] += sum_dY; // accumulate | |
| 315 | |||
| 316 | // For dX we need sum1 = sum dxhat = gv * sum_dY, | ||
| 317 | // and sum2 = sum (dxhat * xhat) = gv * sum_dY_xh. | ||
| 318 | 16 | const float sum1 = gv * sum_dY; | |
| 319 | 16 | const float sum2 = gv * sum_dY_xh; | |
| 320 | |||
| 321 |
2/2✓ Branch 0 taken 43 times.
✓ Branch 1 taken 16 times.
|
59 | for (int n = 0; n < N; ++n) { |
| 322 | 43 | const float* dy_chan = dYp + (n * C + c) * spatial; | |
| 323 | 43 | float* dx_chan = dXp + (n * C + c) * spatial; | |
| 324 | 43 | const float* xh_chan = xhat_buf.data() + n * spatial; | |
| 325 |
2/2✓ Branch 0 taken 1841 times.
✓ Branch 1 taken 43 times.
|
1884 | for (int s = 0; s < spatial; ++s) { |
| 326 | 1841 | const float xh = xh_chan[s]; | |
| 327 | 1841 | const float dxh = dy_chan[s] * gv; | |
| 328 | 1841 | dx_chan[s] = rstd * (dxh - (sum1 + xh * sum2) * inv_M); | |
| 329 | 1841 | } | |
| 330 | 43 | } | |
| 331 | 16 | }); | |
| 332 | 3 | } | |
| 333 | |||
| 334 | } // namespace brotensor::detail::cpu | ||
| 335 |