src/jit/trace_dag.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "trace_dag.h" | ||
| 2 | #include <cstring> | ||
| 3 | #include <stdexcept> | ||
| 4 | |||
| 5 | namespace brotensor::jit { | ||
| 6 | |||
| 7 | ✗ | const char* op_kind_name(TraceOpKind op) { | |
| 8 | ✗ | switch (op) { | |
| 9 | ✗ | case TraceOpKind::Input: return "Input"; | |
| 10 | ✗ | case TraceOpKind::Add: return "Add"; | |
| 11 | ✗ | case TraceOpKind::Sub: return "Sub"; | |
| 12 | ✗ | case TraceOpKind::Mul: return "Mul"; | |
| 13 | ✗ | case TraceOpKind::Div: return "Div"; | |
| 14 | ✗ | case TraceOpKind::AddScalar: return "AddScalar"; | |
| 15 | ✗ | case TraceOpKind::SubScalar: return "SubScalar"; | |
| 16 | ✗ | case TraceOpKind::MulScalar: return "MulScalar"; | |
| 17 | ✗ | case TraceOpKind::DivScalar: return "DivScalar"; | |
| 18 | ✗ | case TraceOpKind::ScalarSub: return "ScalarSub"; | |
| 19 | ✗ | case TraceOpKind::ScalarDiv: return "ScalarDiv"; | |
| 20 | ✗ | case TraceOpKind::FMA: return "FMA"; | |
| 21 | ✗ | case TraceOpKind::SiLU: return "SiLU"; | |
| 22 | ✗ | case TraceOpKind::GELU: return "GELU"; | |
| 23 | ✗ | case TraceOpKind::ReLU: return "ReLU"; | |
| 24 | ✗ | case TraceOpKind::RMSNorm: return "RMSNorm"; | |
| 25 | ✗ | case TraceOpKind::LayerNorm: return "LayerNorm"; | |
| 26 | ✗ | case TraceOpKind::Modulate: return "Modulate"; | |
| 27 | ✗ | case TraceOpKind::Tanh: return "Tanh"; | |
| 28 | ✗ | case TraceOpKind::Sigmoid: return "Sigmoid"; | |
| 29 | ✗ | case TraceOpKind::Copy: return "Copy"; | |
| 30 | ✗ | case TraceOpKind::Where: return "Where"; | |
| 31 | ✗ | case TraceOpKind::Clamp: return "Clamp"; | |
| 32 | ✗ | case TraceOpKind::Cast: return "Cast"; | |
| 33 | } | ||
| 34 | ✗ | return "Unknown"; | |
| 35 | ✗ | } | |
| 36 | |||
| 37 | 97 | BroadcastKind broadcast_of(int node_rows, int node_cols, int rows, int cols) { | |
| 38 |
3/6✓ Branch 0 taken 16 times.
✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
97 | if (node_rows == 1 && node_cols == 1 && rows * cols != 1) { |
| 39 | ✗ | return BroadcastKind::Scalar; | |
| 40 | } | ||
| 41 |
4/6✓ Branch 0 taken 16 times.
✓ Branch 1 taken 81 times.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 16 times.
|
97 | if (node_rows == 1 && node_cols == cols && rows != 1) { |
| 42 | 16 | return BroadcastKind::Row; | |
| 43 | } | ||
| 44 |
4/6✓ Branch 0 taken 81 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 76 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
|
81 | if (node_rows == rows && node_cols == 1 && cols != 1) { |
| 45 | 5 | return BroadcastKind::Col; | |
| 46 | } | ||
| 47 | // Anything else is addressed linearly. A node whose element count does not | ||
| 48 | // match the dominant shape is rejected by the compiler before it gets | ||
| 49 | // here (see trace_compiler_cuda.cpp's plan builder). | ||
| 50 | 76 | return BroadcastKind::Full; | |
| 51 | 97 | } | |
| 52 | |||
| 53 | 4 | bool TraceDAG::slot_matches(int slot, const Tensor& t) const { | |
| 54 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | if (slot < 0 || slot >= static_cast<int>(nodes_.size())) return false; |
| 55 | 4 | const TraceNode& n = nodes_[static_cast<std::size_t>(slot)]; | |
| 56 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
8 | return n.buffer == t.data && n.rows == t.rows && n.cols == t.cols && |
| 57 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | n.dtype == t.dtype && n.device == t.device; |
| 58 | 4 | } | |
| 59 | |||
| 60 | 37 | int TraceDAG::register_input(const Tensor& t) { | |
| 61 | 37 | auto it = input_ptr_to_slot_.find(t.data); | |
| 62 |
2/4✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 37 times.
|
37 | if (it != input_ptr_to_slot_.end() && slot_matches(it->second, t)) { |
| 63 | ✗ | return it->second; | |
| 64 | } | ||
| 65 | |||
| 66 | 37 | int id = static_cast<int>(nodes_.size()); | |
| 67 | 37 | TraceNode node; | |
| 68 | 37 | node.id = id; | |
| 69 | 37 | node.op = TraceOpKind::Input; | |
| 70 | 37 | node.buffer = t.data; | |
| 71 | 37 | node.device = t.device; | |
| 72 | 37 | node.dtype = t.dtype; | |
| 73 | 37 | node.rows = t.rows; | |
| 74 | 37 | node.cols = t.cols; | |
| 75 | 37 | node.is_inplace = false; | |
| 76 | 37 | node.is_live_at_end = false; | |
| 77 | |||
| 78 |
1/2✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
|
37 | nodes_.push_back(std::move(node)); |
| 79 |
1/2✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
|
37 | input_ptr_to_slot_[t.data] = id; |
| 80 | 37 | return id; | |
| 81 | 37 | } | |
| 82 | |||
| 83 | 72 | int TraceDAG::add_node(TraceOpKind op, const std::vector<int>& inputs, float scalar, | |
| 84 | void* buffer, const Device& dev, Dtype dt, int rows, int cols, | ||
| 85 | float scalar2) { | ||
| 86 | 72 | int id = static_cast<int>(nodes_.size()); | |
| 87 | 72 | TraceNode node; | |
| 88 | 72 | node.id = id; | |
| 89 | 72 | node.op = op; | |
| 90 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | node.inputs = inputs; |
| 91 | 72 | node.scalar = scalar; | |
| 92 | 72 | node.scalar2 = scalar2; | |
| 93 | 72 | node.buffer = buffer; | |
| 94 | 72 | node.device = dev; | |
| 95 | 72 | node.dtype = dt; | |
| 96 | 72 | node.rows = rows; | |
| 97 | 72 | node.cols = cols; | |
| 98 | 72 | node.is_inplace = false; | |
| 99 | 72 | node.is_live_at_end = false; | |
| 100 | |||
| 101 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | nodes_.push_back(std::move(node)); |
| 102 | 72 | return id; | |
| 103 | 72 | } | |
| 104 | |||
| 105 | 3 | int TraceDAG::add_inplace_node(TraceOpKind op, int target_slot, int arg_slot, | |
| 106 | void* buffer, float scalar, float scalar2) { | ||
| 107 | 3 | int id = static_cast<int>(nodes_.size()); | |
| 108 | 3 | const auto& target_node = nodes_.at(target_slot); | |
| 109 | |||
| 110 | 3 | TraceNode node; | |
| 111 | 3 | node.id = id; | |
| 112 | 3 | node.op = op; | |
| 113 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | if (arg_slot >= 0) { |
| 114 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | node.inputs = {target_slot, arg_slot}; |
| 115 | 2 | } else { | |
| 116 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | node.inputs = {target_slot}; |
| 117 | } | ||
| 118 | 3 | node.scalar = scalar; | |
| 119 | 3 | node.scalar2 = scalar2; | |
| 120 | 3 | node.buffer = buffer; | |
| 121 | 3 | node.device = target_node.device; | |
| 122 | 3 | node.dtype = target_node.dtype; | |
| 123 | 3 | node.rows = target_node.rows; | |
| 124 | 3 | node.cols = target_node.cols; | |
| 125 | 3 | node.is_inplace = true; | |
| 126 | 3 | node.inplace_target_slot = target_slot; | |
| 127 | 3 | node.is_live_at_end = true; | |
| 128 | |||
| 129 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | nodes_.push_back(std::move(node)); |
| 130 | 3 | return id; | |
| 131 | 3 | } | |
| 132 | |||
| 133 | 11 | void TraceDAG::analyze_liveness() { | |
| 134 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 11 times.
|
79 | for (auto& n : nodes_) { |
| 135 | 68 | n.outputs.clear(); | |
| 136 | } | ||
| 137 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 11 times.
|
79 | for (const auto& n : nodes_) { |
| 138 |
2/2✓ Branch 0 taken 59 times.
✓ Branch 1 taken 68 times.
|
127 | for (int in_id : n.inputs) { |
| 139 |
2/4✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 59 times.
|
59 | if (in_id >= 0 && in_id < static_cast<int>(nodes_.size())) { |
| 140 | 59 | nodes_[in_id].outputs.push_back(n.id); | |
| 141 | 59 | } | |
| 142 | } | ||
| 143 | } | ||
| 144 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 11 times.
|
79 | for (auto& n : nodes_) { |
| 145 |
2/2✓ Branch 0 taken 34 times.
✓ Branch 1 taken 34 times.
|
68 | if (n.op == TraceOpKind::Input) { |
| 146 | 34 | n.is_live_at_end = false; | |
| 147 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 31 times.
|
68 | } else if (n.is_inplace) { |
| 148 | 3 | n.is_live_at_end = true; | |
| 149 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 21 times.
|
34 | } else if (n.outputs.empty()) { |
| 150 | 10 | n.is_live_at_end = true; | |
| 151 | 10 | } else { | |
| 152 | 21 | n.is_live_at_end = false; | |
| 153 | } | ||
| 154 | } | ||
| 155 | 11 | } | |
| 156 | |||
| 157 | 20 | void TraceDAG::dominant_shape(int& rows, int& cols) const { | |
| 158 | 20 | rows = 0; | |
| 159 | 20 | cols = 0; | |
| 160 | 20 | int64_t best = 0; | |
| 161 |
2/2✓ Branch 0 taken 118 times.
✓ Branch 1 taken 20 times.
|
138 | for (const auto& n : nodes_) { |
| 162 | 118 | const int64_t numel = static_cast<int64_t>(n.rows) * static_cast<int64_t>(n.cols); | |
| 163 |
2/2✓ Branch 0 taken 98 times.
✓ Branch 1 taken 20 times.
|
118 | if (numel > best) { |
| 164 | 20 | best = numel; | |
| 165 | 20 | rows = n.rows; | |
| 166 | 20 | cols = n.cols; | |
| 167 | 20 | } | |
| 168 | } | ||
| 169 | 20 | } | |
| 170 | |||
| 171 | 11 | uint64_t TraceDAG::compute_hash() const { | |
| 172 | // 64-bit FNV-1a hash | ||
| 173 | 11 | uint64_t hash = 14695981039346656037ULL; | |
| 174 | 829 | auto hash_val = [&hash](uint64_t val) { | |
| 175 | 818 | hash ^= val; | |
| 176 | 818 | hash *= 1099511628211ULL; | |
| 177 | 818 | }; | |
| 178 | |||
| 179 | 11 | hash_val(nodes_.size()); | |
| 180 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 11 times.
|
79 | for (const auto& n : nodes_) { |
| 181 | 68 | hash_val(static_cast<uint64_t>(n.op)); | |
| 182 | 68 | hash_val(static_cast<uint64_t>(n.device.type)); | |
| 183 | 68 | hash_val(static_cast<uint64_t>(n.device.index)); | |
| 184 | 68 | hash_val(static_cast<uint64_t>(n.dtype)); | |
| 185 | 68 | hash_val(static_cast<uint64_t>(n.rows)); | |
| 186 | 68 | hash_val(static_cast<uint64_t>(n.cols)); | |
| 187 | 68 | hash_val(n.is_inplace ? 1ULL : 0ULL); | |
| 188 | 68 | hash_val(n.is_live_at_end ? 1ULL : 0ULL); | |
| 189 | |||
| 190 | 68 | uint32_t scalar_bits = 0; | |
| 191 | 68 | std::memcpy(&scalar_bits, &n.scalar, sizeof(float)); | |
| 192 | 68 | hash_val(scalar_bits); | |
| 193 | |||
| 194 | 68 | uint32_t scalar2_bits = 0; | |
| 195 | 68 | std::memcpy(&scalar2_bits, &n.scalar2, sizeof(float)); | |
| 196 | 68 | hash_val(scalar2_bits); | |
| 197 | |||
| 198 | 68 | hash_val(n.inputs.size()); | |
| 199 |
2/2✓ Branch 0 taken 59 times.
✓ Branch 1 taken 68 times.
|
127 | for (int in_id : n.inputs) { |
| 200 | 59 | hash_val(static_cast<uint64_t>(in_id)); | |
| 201 | } | ||
| 202 | } | ||
| 203 | 11 | return hash; | |
| 204 | } | ||
| 205 | |||
| 206 | 15 | void TraceDAG::reset() { | |
| 207 | 15 | nodes_.clear(); | |
| 208 | 15 | input_ptr_to_slot_.clear(); | |
| 209 | 15 | } | |
| 210 | |||
| 211 | 123 | TraceContext& TraceContext::current() { | |
| 212 |
2/2✓ Branch 0 taken 122 times.
✓ Branch 1 taken 1 time.
|
123 | static thread_local TraceContext instance; |
| 213 | 123 | return instance; | |
| 214 | } | ||
| 215 | |||
| 216 | namespace { | ||
| 217 | |||
| 218 | // The hook Tensor calls from its move ctor / move assignment / destructor. | ||
| 219 | // Installed by begin(), which is the earliest point at which a symbolic | ||
| 220 | // tensor can exist, so there is no static-initialisation order to get wrong. | ||
| 221 | 23 | void slot_retarget(int slot, const Tensor* from, Tensor* to) { | |
| 222 | 23 | TraceContext::current().retarget(slot, from, to); | |
| 223 | 23 | } | |
| 224 | |||
| 225 | 1 | bool clamp_inplace_hook(Tensor& y, float lo, float hi) { | |
| 226 | 1 | auto& ctx = TraceContext::current(); | |
| 227 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
|
1 | if (!ctx.is_active()) return false; |
| 228 | 1 | const int slot_y = ctx.get_or_register_slot(y); | |
| 229 | 1 | ctx.record_inplace_op(TraceOpKind::Clamp, slot_y, -1, lo, y, hi); | |
| 230 | 1 | return true; | |
| 231 | 1 | } | |
| 232 | |||
| 233 | 3 | bool cast_out_hook(const Tensor& src, Tensor& dst, Dtype out_dtype) { | |
| 234 | 3 | auto& ctx = TraceContext::current(); | |
| 235 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | if (!ctx.is_active()) return false; |
| 236 | 1 | const int slot_src = ctx.get_or_register_slot(src); | |
| 237 | 1 | ctx.record_cast_out(slot_src, src, dst, out_dtype); | |
| 238 | 1 | return true; | |
| 239 | 3 | } | |
| 240 | |||
| 241 | } // namespace | ||
| 242 | |||
| 243 | 23 | void TraceContext::retarget(int slot, const Tensor* from, Tensor* to) noexcept { | |
| 244 |
1/2✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
|
23 | auto it = slot_to_tensor_.find(slot); |
| 245 | // A stale entry — the slot was already materialised or re-tracked — must | ||
| 246 | // not be clobbered by a tensor that no longer stands for it. | ||
| 247 |
4/8✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 23 times.
|
23 | if (it == slot_to_tensor_.end() || it->second != from) return; |
| 248 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 22 times.
|
23 | if (to) { |
| 249 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | it->second = to; |
| 250 | 1 | } else { | |
| 251 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | slot_to_tensor_.erase(it); |
| 252 | } | ||
| 253 | 23 | } | |
| 254 | |||
| 255 | 26 | void TraceContext::release_tracked_() noexcept { | |
| 256 |
5/8✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 26 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
28 | for (auto& kv : slot_to_tensor_) { |
| 257 | 2 | Tensor* t = kv.second; | |
| 258 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (!t) continue; |
| 259 | // It never owned anything, and its value was never written anywhere, | ||
| 260 | // so the honest result is an empty tensor rather than a shape with no | ||
| 261 | // storage behind it. | ||
| 262 | 2 | t->jit_slot = -1; | |
| 263 | 2 | t->rows = 0; | |
| 264 | 2 | t->cols = 0; | |
| 265 | } | ||
| 266 | 26 | slot_to_tensor_.clear(); | |
| 267 | 26 | } | |
| 268 | |||
| 269 | 13 | void TraceContext::begin() { | |
| 270 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | if (active_) { |
| 271 | ✗ | throw std::runtime_error("brotensor::jit: Nested begin_trace() calls are not permitted"); | |
| 272 | } | ||
| 273 | 13 | detail::jit_slot_retarget = &slot_retarget; | |
| 274 | 13 | detail::jit_clamp_inplace_hook = &clamp_inplace_hook; | |
| 275 | 13 | detail::jit_cast_out_hook = &cast_out_hook; | |
| 276 | 13 | dag_.reset(); | |
| 277 | 13 | active_ptr_to_slot_.clear(); | |
| 278 | 13 | release_tracked_(); | |
| 279 | 13 | active_ = true; | |
| 280 | 13 | } | |
| 281 | |||
| 282 | 2 | void TraceContext::discard() { | |
| 283 | 2 | dag_.reset(); | |
| 284 | 2 | active_ptr_to_slot_.clear(); | |
| 285 | 2 | release_tracked_(); | |
| 286 | 2 | active_ = false; | |
| 287 | 2 | } | |
| 288 | |||
| 289 | 11 | void TraceContext::materialize_() { | |
| 290 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 11 times.
|
79 | for (TraceNode& n : dag_.nodes()) { |
| 291 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 55 times.
|
68 | if (!n.is_live_at_end) continue; |
| 292 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 9 times.
|
13 | if (n.buffer != nullptr) continue; // caller already owns the storage |
| 293 | |||
| 294 | 9 | auto it = slot_to_tensor_.find(n.id); | |
| 295 |
2/4✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
|
9 | if (it == slot_to_tensor_.end() || it->second == nullptr) { |
| 296 | // The caller dropped the Tensor, so nothing can read this value. | ||
| 297 | // Leave it an interior value of the fused kernel rather than | ||
| 298 | // allocating a buffer no one will look at. | ||
| 299 | ✗ | n.is_live_at_end = false; | |
| 300 | ✗ | continue; | |
| 301 | } | ||
| 302 | |||
| 303 | 9 | Tensor* held = it->second; | |
| 304 | 9 | Tensor buf = Tensor::empty_on(n.device, n.rows, n.cols, n.dtype); | |
| 305 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | if (buf.data == nullptr) { |
| 306 | ✗ | n.is_live_at_end = false; | |
| 307 | ✗ | continue; | |
| 308 | } | ||
| 309 | // Drop the tracking first: the move assignment below must see an | ||
| 310 | // ordinary tensor on both sides, or it would retarget the slot it is | ||
| 311 | // in the middle of retiring. | ||
| 312 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
|
9 | slot_to_tensor_.erase(it); |
| 313 | 9 | held->jit_slot = -1; | |
| 314 | 9 | *held = std::move(buf); | |
| 315 | 9 | n.buffer = held->data; | |
| 316 | 9 | } | |
| 317 | 11 | } | |
| 318 | |||
| 319 | 11 | TraceDAG TraceContext::end() { | |
| 320 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | if (!active_) { |
| 321 | ✗ | throw std::runtime_error("brotensor::jit: end_trace() called without matching begin_trace()"); | |
| 322 | } | ||
| 323 | 11 | dag_.analyze_liveness(); | |
| 324 | 11 | materialize_(); | |
| 325 | 11 | active_ = false; | |
| 326 | 11 | active_ptr_to_slot_.clear(); | |
| 327 | // Whatever is still tracked stands for a value the fused kernel only ever | ||
| 328 | // holds in registers; there is nothing to hand back. | ||
| 329 | 11 | release_tracked_(); | |
| 330 | |||
| 331 | 11 | bool any_output = false; | |
| 332 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
|
65 | for (const TraceNode& n : dag_.nodes()) { |
| 333 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 54 times.
|
65 | if (n.is_live_at_end) { any_output = true; break; } |
| 334 | } | ||
| 335 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | if (!any_output) { |
| 336 | ✗ | dag_.reset(); | |
| 337 | ✗ | throw std::runtime_error( | |
| 338 | "brotensor::jit: the trace has no observable result — every value it " | ||
| 339 | "produced was dropped before end_trace(), so there is nothing for the " | ||
| 340 | "fused kernel to write. Keep the result Tensor alive across " | ||
| 341 | "end_trace(), store() it into a buffer you own, or write in place " | ||
| 342 | "into one of the inputs"); | ||
| 343 | } | ||
| 344 | 11 | return std::move(dag_); | |
| 345 | ✗ | } | |
| 346 | |||
| 347 | 63 | int TraceContext::get_or_register_slot(const Tensor& t) { | |
| 348 | // A symbolic intermediate already *is* a node: it names its own slot, and | ||
| 349 | // it has no buffer to key on. | ||
| 350 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 41 times.
|
63 | if (t.jit_slot >= 0) return t.jit_slot; |
| 351 | |||
| 352 | // Everything else is caller-owned storage, keyed on its address. The | ||
| 353 | // address is stable for the trace's lifetime because the caller owns it — | ||
| 354 | // but a slot recorded earlier can still be stale if that tensor was freed | ||
| 355 | // and its address reused, so confirm the node still describes the tensor | ||
| 356 | // in hand before reusing its slot. | ||
| 357 | 41 | auto it = active_ptr_to_slot_.find(t.data); | |
| 358 |
4/4✓ Branch 0 taken 37 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 37 times.
|
41 | if (it != active_ptr_to_slot_.end() && dag_.slot_matches(it->second, t)) { |
| 359 | 4 | return it->second; | |
| 360 | } | ||
| 361 | 37 | int slot = dag_.register_input(t); | |
| 362 | 37 | active_ptr_to_slot_[t.data] = slot; | |
| 363 | 37 | return slot; | |
| 364 | 63 | } | |
| 365 | |||
| 366 | 33 | Tensor TraceContext::record_op(TraceOpKind op, const std::vector<int>& in_slots, | |
| 367 | float scalar, const Device& dev, Dtype dt, | ||
| 368 | int rows, int cols, float scalar2) { | ||
| 369 | 66 | const int slot = dag_.add_node(op, in_slots, scalar, /*buffer=*/nullptr, | |
| 370 | 33 | dev, dt, rows, cols, scalar2); | |
| 371 | 33 | Tensor out; | |
| 372 | 33 | out.device = dev; | |
| 373 | 33 | out.dtype = dt; | |
| 374 | 33 | out.rows = rows; | |
| 375 | 33 | out.cols = cols; | |
| 376 | 33 | out.jit_slot = slot; | |
| 377 | // With copy elision `out` is already the caller's object; without it, the | ||
| 378 | // move ctor retargets this entry onto wherever it lands. | ||
| 379 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | slot_to_tensor_[slot] = &out; |
| 380 | 33 | return out; | |
| 381 | 33 | } | |
| 382 | |||
| 383 | 1 | void TraceContext::record_store(int src_slot, const Tensor& dst) { | |
| 384 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | if (dst.jit_slot >= 0) { |
| 385 | ✗ | throw std::runtime_error( | |
| 386 | "brotensor::jit: store() needs a destination the caller owns; its " | ||
| 387 | "target is a traced intermediate, which has no buffer to write to"); | ||
| 388 | } | ||
| 389 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | const int slot = dag_.add_node(TraceOpKind::Copy, {src_slot}, 0.0f, dst.data, |
| 390 | 1 | dst.device, dst.dtype, dst.rows, dst.cols); | |
| 391 | 1 | active_ptr_to_slot_[dst.data] = slot; | |
| 392 | 1 | } | |
| 393 | |||
| 394 | 1 | void TraceContext::record_cast_out(int src_slot, const Tensor& src, Tensor& dst, Dtype out_dtype) { | |
| 395 |
2/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
1 | if (dst.jit_slot < 0 && dst.data != nullptr && dst.device == src.device && |
| 396 | ✗ | dst.rows == src.rows && dst.cols == src.cols && dst.dtype == out_dtype) { | |
| 397 | ✗ | const int slot = dag_.add_node(TraceOpKind::Cast, {src_slot}, 0.0f, dst.data, | |
| 398 | ✗ | dst.device, out_dtype, dst.rows, dst.cols); | |
| 399 | ✗ | active_ptr_to_slot_[dst.data] = slot; | |
| 400 | ✗ | return; | |
| 401 | } | ||
| 402 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | dst = record_op(TraceOpKind::Cast, {src_slot}, 0.0f, src.device, out_dtype, |
| 403 | 1 | src.rows, src.cols); | |
| 404 | 1 | } | |
| 405 | |||
| 406 | 3 | int TraceContext::record_inplace_op(TraceOpKind op, int target_slot, int arg_slot, | |
| 407 | float scalar, Tensor& target_tensor, | ||
| 408 | float scalar2) { | ||
| 409 | 6 | int slot = dag_.add_inplace_node(op, target_slot, arg_slot, target_tensor.data, | |
| 410 | 3 | scalar, scalar2); | |
| 411 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | if (target_tensor.jit_slot >= 0) { |
| 412 | // In-place on a traced intermediate: it has no buffer, and from here | ||
| 413 | // on it stands for the node this op just produced. | ||
| 414 | ✗ | slot_to_tensor_.erase(target_tensor.jit_slot); | |
| 415 | ✗ | target_tensor.jit_slot = slot; | |
| 416 | ✗ | slot_to_tensor_[slot] = &target_tensor; | |
| 417 | ✗ | } else { | |
| 418 | 3 | active_ptr_to_slot_[target_tensor.data] = slot; | |
| 419 | } | ||
| 420 | 3 | return slot; | |
| 421 | } | ||
| 422 | |||
| 423 | } // namespace brotensor::jit | ||
| 424 |