src/cpu/matmul.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // ─── CPU matmul ops (CHUNK 2) ────────────────────────────────────────────── | ||
| 2 | // | ||
| 3 | // FP32 scalar host implementations. Ports src/cuda/matmul.cu and | ||
| 4 | // src/cuda/matmul_backward.cu — FP32 path only, row-major throughout. | ||
| 5 | // | ||
| 6 | // forward: C(M, N) = A(M, K) @ B(K, N) | ||
| 7 | // backward: dA(M, K) += dC(M, N) @ B^T(N, K) | ||
| 8 | // dB(K, N) += A^T(K, M) @ dC(M, N) | ||
| 9 | // | ||
| 10 | // ACCUMULATION: the GPU backward kernels atomicAdd partial products into the | ||
| 11 | // caller's dA / dB buffers, so they ACCUMULATE (+=). The caller is responsible | ||
| 12 | // for zeroing dA / dB before the call if a fresh gradient is wanted; the GPU | ||
| 13 | // kernel also requires dA / dB to be pre-sized to (M, K) / (K, N). | ||
| 14 | |||
| 15 | #include <brotensor/tensor.h> | ||
| 16 | #include <brotensor/detail/cpu/thread_pool.h> | ||
| 17 | |||
| 18 | #include <cmath> | ||
| 19 | #include <cstdint> | ||
| 20 | #include <stdexcept> | ||
| 21 | |||
| 22 | namespace brotensor::detail::cpu { | ||
| 23 | |||
| 24 | 14 | void matmul(const ::brotensor::Tensor& A, const ::brotensor::Tensor& B, | |
| 25 | ::brotensor::Tensor& C) { | ||
| 26 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | if (A.dtype != B.dtype) { |
| 27 | ✗ | throw std::runtime_error("matmul: A and B must share dtype"); | |
| 28 | } | ||
| 29 | 14 | const int M = A.rows; | |
| 30 | 14 | const int K = A.cols; | |
| 31 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | if (B.rows != K) { |
| 32 | ✗ | throw std::runtime_error("matmul: shape mismatch (A.cols != B.rows)"); | |
| 33 | } | ||
| 34 | 14 | const int N = B.cols; | |
| 35 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
14 | if (C.rows != M || C.cols != N) C.resize(M, N); |
| 36 |
2/4✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
|
14 | if (M == 0 || N == 0) return; |
| 37 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | if (K == 0) { |
| 38 | ✗ | C.zero(); | |
| 39 | ✗ | return; | |
| 40 | } | ||
| 41 | 14 | const float* Ap = A.host_f32(); | |
| 42 | 14 | const float* Bp = B.host_f32(); | |
| 43 | 14 | float* Cp = C.host_f32_mut(); | |
| 44 | // m-k-n order: broadcast A[m,k], walk B's row k and C's row m contiguously | ||
| 45 | // in the innermost loop (both stride-1) instead of striding through B by N | ||
| 46 | // floats per k step. Each m exclusively owns C's row m (B is read-only), | ||
| 47 | // so the outer loop parallelizes across m with no cross-thread writes. | ||
| 48 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
299 | parallel_for(static_cast<std::size_t>(M), [&](std::size_t mi) { |
| 49 | 285 | const int m = static_cast<int>(mi); | |
| 50 | 285 | float* Crow = Cp + static_cast<size_t>(m) * N; | |
| 51 |
2/2✓ Branch 0 taken 20076 times.
✓ Branch 1 taken 285 times.
|
20361 | for (int n = 0; n < N; ++n) Crow[n] = 0.0f; |
| 52 |
2/2✓ Branch 0 taken 13166 times.
✓ Branch 1 taken 285 times.
|
13451 | for (int k = 0; k < K; ++k) { |
| 53 | 13166 | const float a_mk = Ap[m * K + k]; | |
| 54 | 13166 | const float* Brow = Bp + static_cast<size_t>(k) * N; | |
| 55 |
2/2✓ Branch 0 taken 814466 times.
✓ Branch 1 taken 13166 times.
|
827632 | for (int n = 0; n < N; ++n) { |
| 56 | 814466 | Crow[n] += a_mk * Brow[n]; | |
| 57 | 814466 | } | |
| 58 | 13166 | } | |
| 59 | 285 | }); | |
| 60 | 14 | } | |
| 61 | |||
| 62 | // Batched A @ B^T, 16-bit (FP16/BF16), FP32 accumulation. Reference triple | ||
| 63 | // loop mirroring fp16_internal::launch_matmul_ABT_batched_impl semantics: | ||
| 64 | // C[b][m,n] = sum_k A[b][m,k] * B[b][n,k] (+ bias[n], then activation). | ||
| 65 | // bias is per-N (broadcast over rows), length N. C is caller-sized. | ||
| 66 | 51 | void matmul_abt(const ::brotensor::Tensor& A, const ::brotensor::Tensor& B, | |
| 67 | ::brotensor::Tensor& C, | ||
| 68 | int batch, int M, int N, int K, | ||
| 69 | long long strideA, long long strideB, long long strideC, | ||
| 70 | const ::brotensor::Tensor* bias, int act) { | ||
| 71 |
2/2✓ Branch 0 taken 49 times.
✓ Branch 1 taken 2 times.
|
51 | if (A.dtype != B.dtype || A.dtype != C.dtype) { |
| 72 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | throw std::runtime_error("matmul_abt: A, B, C must share dtype"); |
| 73 | } | ||
| 74 | 49 | const bool is_fp16 = (A.dtype == ::brotensor::Dtype::FP16); | |
| 75 | 49 | const bool is_bf16 = (A.dtype == ::brotensor::Dtype::BF16); | |
| 76 |
4/4✓ Branch 0 taken 22 times.
✓ Branch 1 taken 27 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 1 time.
|
49 | if (!is_fp16 && !is_bf16) { |
| 77 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | throw std::runtime_error("matmul_abt: dtype must be FP16 or BF16"); |
| 78 | } | ||
| 79 |
4/4✓ Branch 0 taken 20 times.
✓ Branch 1 taken 28 times.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 1 time.
|
48 | if (bias && bias->dtype != A.dtype) { |
| 80 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | throw std::runtime_error("matmul_abt: bias dtype must match operands"); |
| 81 | } | ||
| 82 |
6/6✓ Branch 0 taken 45 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 44 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 43 times.
|
47 | if (batch <= 0 || M == 0 || N == 0) return; |
| 83 | |||
| 84 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 21 times.
|
43 | const uint16_t* Ap = is_fp16 ? A.host_fp16() : A.host_bf16(); |
| 85 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 21 times.
|
43 | const uint16_t* Bp = is_fp16 ? B.host_fp16() : B.host_bf16(); |
| 86 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 21 times.
|
43 | uint16_t* Cp = is_fp16 ? C.host_fp16_mut() : C.host_bf16_mut(); |
| 87 | 43 | const uint16_t* bp = | |
| 88 |
4/4✓ Branch 0 taken 24 times.
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 9 times.
|
43 | bias ? (is_fp16 ? bias->host_fp16() : bias->host_bf16()) : nullptr; |
| 89 | |||
| 90 | 6723 | auto to_f32 = [&](uint16_t b) { | |
| 91 |
2/2✓ Branch 0 taken 3350 times.
✓ Branch 1 taken 3330 times.
|
6680 | return is_fp16 ? ::brotensor::fp16_bits_to_fp32(b) |
| 92 | 3330 | : ::brotensor::bf16_bits_to_fp32(b); | |
| 93 | }; | ||
| 94 | 683 | auto from_f32 = [&](float v) { | |
| 95 |
2/2✓ Branch 0 taken 322 times.
✓ Branch 1 taken 318 times.
|
640 | return is_fp16 ? ::brotensor::fp32_to_fp16_bits(v) |
| 96 | 318 | : ::brotensor::fp32_to_bf16_bits(v); | |
| 97 | }; | ||
| 98 | 683 | auto apply_act = [&](float v) -> float { | |
| 99 |
7/7✓ Branch 0 taken 100 times.
✓ Branch 1 taken 96 times.
✓ Branch 2 taken 96 times.
✓ Branch 3 taken 96 times.
✓ Branch 4 taken 24 times.
✓ Branch 5 taken 132 times.
✓ Branch 6 taken 96 times.
|
640 | switch (act) { |
| 100 | 100 | case 0: return v; // none | |
| 101 |
2/2✓ Branch 0 taken 69 times.
✓ Branch 1 taken 63 times.
|
132 | case 1: return v > 0.0f ? v : 0.0f; // relu |
| 102 | case 2: { // gelu (tanh) | ||
| 103 | 96 | const float c = 0.7978845608028654f; // sqrt(2/pi) | |
| 104 | 96 | const float t = c * (v + 0.044715f * v * v * v); | |
| 105 | 96 | return 0.5f * v * (1.0f + std::tanh(t)); | |
| 106 | } | ||
| 107 | case 3: // gelu (exact) | ||
| 108 | 96 | return 0.5f * v * (1.0f + std::erf(v * 0.7071067811865476f)); | |
| 109 | case 4: // silu | ||
| 110 | 96 | return v / (1.0f + std::exp(-v)); | |
| 111 | case 5: // quick gelu | ||
| 112 | 96 | return v / (1.0f + std::exp(-1.702f * v)); | |
| 113 | 24 | default: return v; | |
| 114 | } | ||
| 115 | 640 | }; | |
| 116 | |||
| 117 |
2/2✓ Branch 0 taken 43 times.
✓ Branch 1 taken 69 times.
|
112 | for (int b = 0; b < batch; ++b) { |
| 118 | 69 | const uint16_t* Ab = Ap + static_cast<long long>(b) * strideA; | |
| 119 | 69 | const uint16_t* Bb = Bp + static_cast<long long>(b) * strideB; | |
| 120 | 69 | uint16_t* Cb = Cp + static_cast<long long>(b) * strideC; | |
| 121 |
2/2✓ Branch 0 taken 154 times.
✓ Branch 1 taken 69 times.
|
223 | for (int m = 0; m < M; ++m) { |
| 122 |
2/2✓ Branch 0 taken 640 times.
✓ Branch 1 taken 154 times.
|
794 | for (int n = 0; n < N; ++n) { |
| 123 | 640 | float acc = 0.0f; | |
| 124 |
2/2✓ Branch 0 taken 3128 times.
✓ Branch 1 taken 640 times.
|
3768 | for (int k = 0; k < K; ++k) { |
| 125 | 3128 | acc += to_f32(Ab[m * K + k]) * to_f32(Bb[n * K + k]); | |
| 126 | 3128 | } | |
| 127 |
2/2✓ Branch 0 taken 216 times.
✓ Branch 1 taken 424 times.
|
640 | if (bp) acc += to_f32(bp[n]); |
| 128 | 640 | Cb[m * N + n] = from_f32(apply_act(acc)); | |
| 129 | 640 | } | |
| 130 | 154 | } | |
| 131 | 69 | } | |
| 132 | 47 | } | |
| 133 | |||
| 134 | 8 | void matmul_backward(const ::brotensor::Tensor& A, const ::brotensor::Tensor& B, | |
| 135 | const ::brotensor::Tensor& dC, | ||
| 136 | ::brotensor::Tensor& dA, ::brotensor::Tensor& dB) { | ||
| 137 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
16 | if (A.dtype != B.dtype || A.dtype != dC.dtype || |
| 138 | 8 | A.dtype != dA.dtype || A.dtype != dB.dtype) { | |
| 139 | ✗ | throw std::runtime_error("matmul_backward: dtype mismatch"); | |
| 140 | } | ||
| 141 | 8 | const int M = A.rows; | |
| 142 | 8 | const int K = A.cols; | |
| 143 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (B.rows != K) { |
| 144 | ✗ | throw std::runtime_error("matmul_backward: shape mismatch (A.cols != B.rows)"); | |
| 145 | } | ||
| 146 | 8 | const int N = B.cols; | |
| 147 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (dC.rows != M || dC.cols != N) { |
| 148 | ✗ | throw std::runtime_error("matmul_backward: dC shape mismatch"); | |
| 149 | } | ||
| 150 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (dA.rows != M || dA.cols != K) { |
| 151 | ✗ | throw std::runtime_error("matmul_backward: dA must be pre-sized to (M, K)"); | |
| 152 | } | ||
| 153 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (dB.rows != K || dB.cols != N) { |
| 154 | ✗ | throw std::runtime_error("matmul_backward: dB must be pre-sized to (K, N)"); | |
| 155 | } | ||
| 156 |
3/6✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
|
8 | if (M == 0 || N == 0 || K == 0) return; |
| 157 | |||
| 158 | 8 | const float* Ap = A.host_f32(); | |
| 159 | 8 | const float* Bp = B.host_f32(); | |
| 160 | 8 | const float* dCp = dC.host_f32(); | |
| 161 | 8 | float* dAp = dA.host_f32_mut(); | |
| 162 | 8 | float* dBp = dB.host_f32_mut(); | |
| 163 | |||
| 164 | // dA[m, k] += sum_n dC[m, n] * B[k, n] (accumulate — matches GPU atomicAdd) | ||
| 165 | // Each m exclusively owns dA's row m (dC/B are read-only), so this | ||
| 166 | // parallelizes across m with no cross-thread writes. | ||
| 167 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
87 | parallel_for(static_cast<std::size_t>(M), [&](std::size_t mi) { |
| 168 | 79 | const int m = static_cast<int>(mi); | |
| 169 |
2/2✓ Branch 0 taken 1285 times.
✓ Branch 1 taken 79 times.
|
1364 | for (int k = 0; k < K; ++k) { |
| 170 | 1285 | float acc = 0.0f; | |
| 171 |
2/2✓ Branch 0 taken 17263 times.
✓ Branch 1 taken 1285 times.
|
18548 | for (int n = 0; n < N; ++n) { |
| 172 | 17263 | acc += dCp[m * N + n] * Bp[k * N + n]; | |
| 173 | 17263 | } | |
| 174 | 1285 | dAp[m * K + k] += acc; | |
| 175 | 1285 | } | |
| 176 | 79 | }); | |
| 177 | // dB[k, n] += sum_m A[m, k] * dC[m, n] (accumulate — matches GPU atomicAdd) | ||
| 178 | // k-m-n order: each k exclusively owns dB's row k (accumulated over all | ||
| 179 | // m), so this axis parallelizes across k with no cross-thread writes — | ||
| 180 | // unlike an m-outer order, which would need every thread to accumulate | ||
| 181 | // into the same shared dB rows across m. dC's row m and dB's row k are | ||
| 182 | // still walked contiguously (both stride-1) in the innermost loop; only | ||
| 183 | // the A[m,k] broadcast load is strided, which doesn't block | ||
| 184 | // vectorization of the n loop. | ||
| 185 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
126 | parallel_for(static_cast<std::size_t>(K), [&](std::size_t ki) { |
| 186 | 118 | const int k = static_cast<int>(ki); | |
| 187 | 118 | float* dBrow = dBp + static_cast<size_t>(k) * N; | |
| 188 |
2/2✓ Branch 0 taken 1237 times.
✓ Branch 1 taken 118 times.
|
1355 | for (int m = 0; m < M; ++m) { |
| 189 | 1237 | const float a_mk = Ap[m * K + k]; | |
| 190 | 1237 | const float* dCrow = dCp + static_cast<size_t>(m) * N; | |
| 191 |
2/2✓ Branch 0 taken 16022 times.
✓ Branch 1 taken 1237 times.
|
17259 | for (int n = 0; n < N; ++n) { |
| 192 | 16022 | dBrow[n] += a_mk * dCrow[n]; | |
| 193 | 16022 | } | |
| 194 | 1237 | } | |
| 195 | 118 | }); | |
| 196 | 8 | } | |
| 197 | |||
| 198 | } // namespace brotensor::detail::cpu | ||
| 199 |