src/dispatch.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Backend registration + per-operand dispatch: the dispatcher core. | ||
| 2 | // | ||
| 3 | // Storage is two parallel std::array<>s indexed by static_cast<int>(Device). | ||
| 4 | // Registration happens during static init (CPU) and at brotensor::init() | ||
| 5 | // time (CUDA/Metal probe success). Both are single-threaded entry points | ||
| 6 | // before any op call, so no mutex is required. | ||
| 7 | |||
| 8 | #include <brotensor/detail/dispatch.h> | ||
| 9 | #include <brotensor/tensor.h> | ||
| 10 | |||
| 11 | #include <array> | ||
| 12 | #include <cstddef> | ||
| 13 | #include <cstring> | ||
| 14 | #include <initializer_list> | ||
| 15 | #include <stdexcept> | ||
| 16 | #include <string> | ||
| 17 | |||
| 18 | namespace brotensor::detail { | ||
| 19 | |||
| 20 | namespace { | ||
| 21 | |||
| 22 | constexpr int kNumDevices = 3; // CPU, CUDA, Metal | ||
| 23 | |||
| 24 | // Upper bound on operands a single dispatch call inspects. The widest op is | ||
| 25 | // resblock_backward (25 operands); 32 leaves headroom. dispatch_with_opts | ||
| 26 | // throws if a future op ever exceeds this rather than silently truncating. | ||
| 27 | constexpr std::size_t kMaxOperands = 32; | ||
| 28 | |||
| 29 | struct Slot { | ||
| 30 | OpsVTable ops{}; | ||
| 31 | AllocVTable alloc{}; | ||
| 32 | bool registered = false; | ||
| 33 | }; | ||
| 34 | |||
| 35 | 252279 | std::array<Slot, kNumDevices>& slots() { | |
| 36 | static std::array<Slot, kNumDevices> s{}; | ||
| 37 | 252279 | return s; | |
| 38 | } | ||
| 39 | |||
| 40 | 14 | const char* dev_name(Device d) { | |
| 41 |
2/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
14 | switch (d) { |
| 42 | 8 | case Device::CPU: return "CPU"; | |
| 43 | 6 | case Device::CUDA: return "CUDA"; | |
| 44 | ✗ | case Device::Metal: return "Metal"; | |
| 45 | } | ||
| 46 | ✗ | return "?"; | |
| 47 | 14 | } | |
| 48 | |||
| 49 | 2 | [[noreturn]] void throw_unregistered(Device d) { | |
| 50 | 2 | std::string msg = "brotensor: backend "; | |
| 51 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | msg += dev_name(d); |
| 52 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | msg += " not registered"; |
| 53 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
2 | throw std::runtime_error(msg); |
| 54 | 2 | } | |
| 55 | |||
| 56 | // A tensor is "committed" to a device once it owns real storage. A freshly | ||
| 57 | // default-constructed (or resized-to-empty) tensor has data == nullptr and | ||
| 58 | // carries no device affinity yet — it is a wildcard that adopts the op's | ||
| 59 | // device. This is what lets callers pass an unsized output tensor to a GPU op | ||
| 60 | // without first pinning it to the right backend by hand. | ||
| 61 | 209189 | inline bool committed(const Tensor& t) { return t.data != nullptr; } | |
| 62 | |||
| 63 | 4 | [[noreturn]] void throw_device_mismatch(Device resolved, int idx, Device got) { | |
| 64 | 4 | std::string msg = "brotensor: dispatch: operand "; | |
| 65 |
2/4✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | msg += std::to_string(idx); |
| 66 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | msg += " is on "; |
| 67 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | msg += dev_name(got); |
| 68 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | msg += " but the op resolved to "; |
| 69 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | msg += dev_name(resolved); |
| 70 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | throw std::runtime_error(msg); |
| 71 | 4 | } | |
| 72 | |||
| 73 | // Resolve the op's device from the first committed operand; verify every other | ||
| 74 | // committed operand agrees (uncommitted operands are skipped). Returns the | ||
| 75 | // vtable for the resolved device. | ||
| 76 | 64609 | const OpsVTable& dispatch_v(std::initializer_list<const Tensor*> ts) { | |
| 77 | 64609 | Device dev = Device::CPU; | |
| 78 | 64609 | bool found = false; | |
| 79 |
2/2✓ Branch 0 taken 18 times.
✓ Branch 1 taken 64613 times.
|
64631 | for (const Tensor* t : ts) { |
| 80 |
3/4✓ Branch 0 taken 64613 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 64591 times.
|
64613 | if (t && committed(*t)) { dev = t->device; found = true; break; } |
| 81 | } | ||
| 82 |
4/6✓ Branch 0 taken 18 times.
✓ Branch 1 taken 64591 times.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
|
64609 | if (!found && ts.size() != 0 && *ts.begin() != nullptr) { |
| 83 | // All operands empty — degenerate but not an error (e.g. a fresh | ||
| 84 | // output passed before any input). Keep operand 0's tag. | ||
| 85 | 18 | dev = (*ts.begin())->device; | |
| 86 | 18 | } | |
| 87 | 64609 | int idx = 0; | |
| 88 |
2/2✓ Branch 0 taken 139368 times.
✓ Branch 1 taken 64607 times.
|
203975 | for (const Tensor* t : ts) { |
| 89 |
5/6✓ Branch 0 taken 139368 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85391 times.
✓ Branch 3 taken 53977 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 85389 times.
|
139368 | if (t && committed(*t) && t->device != dev) { |
| 90 | 2 | throw_device_mismatch(dev, idx, t->device); | |
| 91 | } | ||
| 92 | 139366 | ++idx; | |
| 93 | } | ||
| 94 | 64607 | return ops_for(dev); | |
| 95 | } | ||
| 96 | |||
| 97 | // Same resolution rule as dispatch_v, over a plain operand array — used by the | ||
| 98 | // optional-operand dispatch path so it needs no heap allocation. Null entries | ||
| 99 | // (skipped optional operands) are tolerated. | ||
| 100 | 754 | const OpsVTable& resolve_over(const Tensor* const* all, std::size_t count) { | |
| 101 | 754 | Device dev = Device::CPU; | |
| 102 | 754 | bool found = false; | |
| 103 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 758 times.
|
760 | for (std::size_t i = 0; i < count; ++i) { |
| 104 |
4/4✓ Branch 0 taken 756 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 752 times.
|
758 | if (all[i] && committed(*all[i])) { |
| 105 | 752 | dev = all[i]->device; | |
| 106 | 752 | found = true; | |
| 107 | 752 | break; | |
| 108 | } | ||
| 109 | 6 | } | |
| 110 |
2/2✓ Branch 0 taken 752 times.
✓ Branch 1 taken 2 times.
|
754 | if (!found) { |
| 111 | // No committed operand — adopt the first non-null operand's tag. | ||
| 112 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | for (std::size_t i = 0; i < count; ++i) { |
| 113 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (all[i]) { dev = all[i]->device; break; } |
| 114 | ✗ | } | |
| 115 | 2 | } | |
| 116 |
2/2✓ Branch 0 taken 5162 times.
✓ Branch 1 taken 752 times.
|
5914 | for (std::size_t i = 0; i < count; ++i) { |
| 117 |
6/6✓ Branch 0 taken 4452 times.
✓ Branch 1 taken 710 times.
✓ Branch 2 taken 3763 times.
✓ Branch 3 taken 689 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 3761 times.
|
5162 | if (all[i] && committed(*all[i]) && all[i]->device != dev) { |
| 118 | 2 | throw_device_mismatch(dev, static_cast<int>(i), all[i]->device); | |
| 119 | } | ||
| 120 | 5160 | } | |
| 121 | 752 | return ops_for(dev); | |
| 122 | } | ||
| 123 | |||
| 124 | 1 | [[noreturn]] void throw_too_many_operands(std::size_t n) { | |
| 125 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
2 | throw std::runtime_error( |
| 126 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
1 | "brotensor: dispatch: operand count " + std::to_string(n) + |
| 127 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | " exceeds the fixed dispatch buffer (" + |
| 128 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | std::to_string(kMaxOperands) + ")"); |
| 129 | 1 | } | |
| 130 | |||
| 131 | } // namespace | ||
| 132 | |||
| 133 | 315 | void register_backend(Device d, const OpsVTable& ops, const AllocVTable& alloc) { | |
| 134 | 315 | auto& s = slots()[static_cast<int>(d)]; | |
| 135 | 315 | std::memcpy(&s.ops, &ops, sizeof(OpsVTable)); | |
| 136 | 315 | std::memcpy(&s.alloc, &alloc, sizeof(AllocVTable)); | |
| 137 | 315 | s.registered = true; | |
| 138 | 315 | } | |
| 139 | |||
| 140 | 8140 | bool is_registered(Device d) { | |
| 141 | 8140 | return slots()[static_cast<int>(d)].registered; | |
| 142 | } | ||
| 143 | |||
| 144 | 65450 | const OpsVTable& ops_for(Device d) { | |
| 145 | 65450 | auto& s = slots()[static_cast<int>(d)]; | |
| 146 |
2/2✓ Branch 0 taken 65449 times.
✓ Branch 1 taken 1 time.
|
65450 | if (!s.registered) throw_unregistered(d); |
| 147 | 65449 | return s.ops; | |
| 148 | } | ||
| 149 | |||
| 150 | 178374 | const AllocVTable& alloc_for(Device d) { | |
| 151 | 178374 | auto& s = slots()[static_cast<int>(d)]; | |
| 152 |
2/2✓ Branch 0 taken 178373 times.
✓ Branch 1 taken 1 time.
|
178374 | if (!s.registered) throw_unregistered(d); |
| 153 | 178373 | return s.alloc; | |
| 154 | } | ||
| 155 | |||
| 156 | // ─── dispatch overloads ──────────────────────────────────────────────────── | ||
| 157 | // | ||
| 158 | // Each forwards to dispatch_v, which resolves the device from the first | ||
| 159 | // committed operand and skips uncommitted (data == nullptr) wildcards. | ||
| 160 | |||
| 161 | 1709 | const OpsVTable& dispatch(const Tensor& a) { | |
| 162 | 1709 | return dispatch_v({&a}); | |
| 163 | } | ||
| 164 | |||
| 165 | 53978 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b) { | |
| 166 | 53978 | return dispatch_v({&a, &b}); | |
| 167 | } | ||
| 168 | |||
| 169 | 7151 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b, const Tensor& c) { | |
| 170 | 7151 | return dispatch_v({&a, &b, &c}); | |
| 171 | } | ||
| 172 | |||
| 173 | 1340 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b, const Tensor& c, | |
| 174 | const Tensor& d) { | ||
| 175 | 1340 | return dispatch_v({&a, &b, &c, &d}); | |
| 176 | } | ||
| 177 | |||
| 178 | 89 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b, const Tensor& c, | |
| 179 | const Tensor& d, const Tensor& e) { | ||
| 180 | 89 | return dispatch_v({&a, &b, &c, &d, &e}); | |
| 181 | } | ||
| 182 | |||
| 183 | 107 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b, const Tensor& c, | |
| 184 | const Tensor& d, const Tensor& e, const Tensor& f) { | ||
| 185 | 107 | return dispatch_v({&a, &b, &c, &d, &e, &f}); | |
| 186 | } | ||
| 187 | |||
| 188 | 77 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b, const Tensor& c, | |
| 189 | const Tensor& d, const Tensor& e, const Tensor& f, | ||
| 190 | const Tensor& g) { | ||
| 191 | 77 | return dispatch_v({&a, &b, &c, &d, &e, &f, &g}); | |
| 192 | } | ||
| 193 | |||
| 194 | 158 | const OpsVTable& dispatch(const Tensor& a, const Tensor& b, const Tensor& c, | |
| 195 | const Tensor& d, const Tensor& e, const Tensor& f, | ||
| 196 | const Tensor& g, const Tensor& h) { | ||
| 197 | 158 | return dispatch_v({&a, &b, &c, &d, &e, &f, &g, &h}); | |
| 198 | } | ||
| 199 | |||
| 200 | 56 | const OpsVTable& dispatch_with_opts(const Tensor& a, | |
| 201 | std::initializer_list<const Tensor*> opts) { | ||
| 202 | 56 | const std::size_t count = 1 + opts.size(); | |
| 203 |
2/2✓ Branch 0 taken 55 times.
✓ Branch 1 taken 1 time.
|
56 | if (count > kMaxOperands) throw_too_many_operands(count); |
| 204 | const Tensor* all[kMaxOperands]; | ||
| 205 | 55 | std::size_t n = 0; | |
| 206 | 55 | all[n++] = &a; | |
| 207 |
2/2✓ Branch 0 taken 117 times.
✓ Branch 1 taken 55 times.
|
172 | for (const Tensor* p : opts) all[n++] = p; |
| 208 | 55 | return resolve_over(all, n); | |
| 209 | } | ||
| 210 | |||
| 211 | 699 | const OpsVTable& dispatch_with_opts(const Tensor& a, const Tensor& b, | |
| 212 | std::initializer_list<const Tensor*> opts) { | ||
| 213 | 699 | const std::size_t count = 2 + opts.size(); | |
| 214 |
1/2✓ Branch 0 taken 699 times.
✗ Branch 1 not taken.
|
699 | if (count > kMaxOperands) throw_too_many_operands(count); |
| 215 | const Tensor* all[kMaxOperands]; | ||
| 216 | 699 | std::size_t n = 0; | |
| 217 | 699 | all[n++] = &a; | |
| 218 | 699 | all[n++] = &b; | |
| 219 |
2/2✓ Branch 0 taken 3592 times.
✓ Branch 1 taken 699 times.
|
4291 | for (const Tensor* p : opts) all[n++] = p; |
| 220 | 699 | return resolve_over(all, n); | |
| 221 | } | ||
| 222 | |||
| 223 | // ─── output adoption ─────────────────────────────────────────────────────── | ||
| 224 | // | ||
| 225 | // An uncommitted output tensor (data == nullptr) has no device affinity yet. | ||
| 226 | // The wrapper calls this after dispatch so the tensor is pinned to the op's | ||
| 227 | // device before the backend impl resizes/allocates it. | ||
| 228 | 70679 | void adopt_output(Tensor& t, Device d) { | |
| 229 |
2/2✓ Branch 0 taken 12706 times.
✓ Branch 1 taken 57973 times.
|
70679 | if (t.data == nullptr) t.device = d; |
| 230 | 70679 | } | |
| 231 | |||
| 232 | 4 | [[noreturn]] void throw_not_implemented(const char* op_name, Device d) { | |
| 233 | 4 | std::string msg = "brotensor: "; | |
| 234 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | msg += op_name; |
| 235 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | msg += ": not implemented on "; |
| 236 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | msg += dev_name(d); |
| 237 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | throw std::runtime_error(msg); |
| 238 | 4 | } | |
| 239 | |||
| 240 | } // namespace brotensor::detail | ||
| 241 |