src/jit/trace_eager.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // ─── Eager trace execution — Vulkan DAGs with no fusion ───────────────────── | ||
| 2 | // | ||
| 3 | // The trace JIT compiles a DAG to host code (brass, CPU), to PTX (CUDA) or to | ||
| 4 | // SPIR-V (Vulkan, trace_compiler_vulkan.cpp). A Vulkan DAG the Vulkan compiler | ||
| 5 | // has no fusion for (two reductions, an op with no elementwise form, ...), or | ||
| 6 | // a build without that compiler, must still never reach the CPU compiler: its | ||
| 7 | // host code would walk device addresses outside the device's ordering (a | ||
| 8 | // Vulkan address is not even a host pointer). Such a trace is therefore | ||
| 9 | // executed by replaying the DAG one node at a time through the ordinary | ||
| 10 | // device-dispatched ops, on the current stream, in program order. | ||
| 11 | // | ||
| 12 | // That keeps every trace correct on Vulkan — same results as the eager op | ||
| 13 | // sequence the trace replaces, same TraceHandle replay/rebind contract — but | ||
| 14 | // it is not fused: one launch per traced op, and an intermediate the fused | ||
| 15 | // kernel would keep in registers is a scratch tensor here (allocated per | ||
| 16 | // execute(), released after its last consumer). launch_count() and | ||
| 17 | // fusion_name() ("eager-unfused") say so. | ||
| 18 | // | ||
| 19 | // Operand rules mirror the fused compilers: every value carries its node's | ||
| 20 | // (rows, cols); a binary operand may be the full shape, a (1, cols) row or a | ||
| 21 | // (1, 1) scalar; a norm/modulate gain is a length-cols vector; an operand of | ||
| 22 | // another dtype is cast to the node's dtype first (math is FP32 inside each | ||
| 23 | // op either way). Anything else is rejected when the trace is compiled, so | ||
| 24 | // end_trace() throws and the caller takes its eager path. | ||
| 25 | |||
| 26 | #include "trace_compiler.h" | ||
| 27 | #include "trace_dag.h" | ||
| 28 | #include "trace_cache.h" | ||
| 29 | |||
| 30 | #include <brotensor/ops.h> | ||
| 31 | |||
| 32 | #include <chrono> | ||
| 33 | #include <cstddef> | ||
| 34 | #include <cstring> | ||
| 35 | #include <memory> | ||
| 36 | #include <stdexcept> | ||
| 37 | #include <string> | ||
| 38 | #include <vector> | ||
| 39 | |||
| 40 | namespace brotensor::jit::eager { | ||
| 41 | |||
| 42 | namespace { | ||
| 43 | |||
| 44 | ✗ | [[noreturn]] void reject(const TraceNode& n, const char* why) { | |
| 45 | ✗ | throw std::runtime_error(std::string("brotensor::jit: eager trace execution cannot run ") + | |
| 46 | ✗ | op_kind_name(n.op) + " on " + to_string(n.device) + ": " + why); | |
| 47 | ✗ | } | |
| 48 | |||
| 49 | 3 | bool is_unary(TraceOpKind op) { | |
| 50 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | switch (op) { |
| 51 | case TraceOpKind::AddScalar: case TraceOpKind::SubScalar: | ||
| 52 | case TraceOpKind::MulScalar: case TraceOpKind::DivScalar: | ||
| 53 | case TraceOpKind::ScalarSub: case TraceOpKind::SiLU: | ||
| 54 | case TraceOpKind::GELU: case TraceOpKind::ReLU: | ||
| 55 | case TraceOpKind::Tanh: case TraceOpKind::Sigmoid: | ||
| 56 | case TraceOpKind::Copy: case TraceOpKind::Clamp: | ||
| 57 | case TraceOpKind::Cast: | ||
| 58 | 2 | return true; | |
| 59 | default: | ||
| 60 | 1 | return false; | |
| 61 | } | ||
| 62 | 3 | } | |
| 63 | |||
| 64 | 1 | bool is_binary(TraceOpKind op) { | |
| 65 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
1 | return op == TraceOpKind::Add || op == TraceOpKind::Sub || |
| 66 | ✗ | op == TraceOpKind::Mul || op == TraceOpKind::Div; | |
| 67 | } | ||
| 68 | |||
| 69 | 14 | int64_t numel(int rows, int cols) { return static_cast<int64_t>(rows) * cols; } | |
| 70 | |||
| 71 | // Shape and op checks, done once at compile time so a trace this executor | ||
| 72 | // cannot run fails in end_trace() rather than on some later replay. | ||
| 73 | 2 | void validate(const TraceDAG& dag) { | |
| 74 | 2 | const auto& nodes = dag.nodes(); | |
| 75 |
1/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
2 | if (nodes.empty()) throw std::runtime_error("brotensor::jit: empty trace"); |
| 76 | 2 | const Device dev = nodes.front().device; | |
| 77 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (const TraceNode& n : nodes) { |
| 78 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | if (n.device != dev) reject(n, "the trace spans more than one device"); |
| 79 |
4/6✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
|
6 | if (n.dtype != Dtype::FP32 && n.dtype != Dtype::FP16 && n.dtype != Dtype::BF16) { |
| 80 | ✗ | reject(n, "only FP32 / FP16 / BF16 values are supported"); | |
| 81 | } | ||
| 82 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
|
6 | if (n.op == TraceOpKind::Input) continue; |
| 83 | 7 | auto operand = [&](std::size_t k) -> const TraceNode& { | |
| 84 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (k >= n.inputs.size() || n.inputs[k] < 0 || |
| 85 | 4 | n.inputs[k] >= static_cast<int>(nodes.size())) { | |
| 86 | ✗ | reject(n, "missing operand"); | |
| 87 | } | ||
| 88 | 4 | return nodes[static_cast<std::size_t>(n.inputs[k])]; | |
| 89 | }; | ||
| 90 | 3 | auto vector_like = [&](const TraceNode& v) { | |
| 91 | ✗ | return numel(v.rows, v.cols) == n.cols && (v.rows == 1 || v.cols == 1); | |
| 92 | }; | ||
| 93 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | if (is_unary(n.op)) { |
| 94 | 2 | const TraceNode& a = operand(0); | |
| 95 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (numel(a.rows, a.cols) != numel(n.rows, n.cols)) reject(n, "operand shape mismatch"); |
| 96 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
3 | } else if (is_binary(n.op)) { |
| 97 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | for (std::size_t k = 0; k < 2; ++k) { |
| 98 | 2 | const TraceNode& a = operand(k); | |
| 99 | 2 | const BroadcastKind bk = broadcast_of(a.rows, a.cols, n.rows, n.cols); | |
| 100 |
3/4✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
|
2 | if (bk == BroadcastKind::Full && numel(a.rows, a.cols) != numel(n.rows, n.cols)) { |
| 101 | ✗ | reject(n, "operand is neither the full shape, a row, a column nor a scalar"); | |
| 102 | } | ||
| 103 | 2 | } | |
| 104 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
1 | } else if (n.op == TraceOpKind::Where) { |
| 105 | ✗ | for (std::size_t k = 0; k < 3; ++k) { | |
| 106 | ✗ | const TraceNode& a = operand(k); | |
| 107 | ✗ | const BroadcastKind bk = broadcast_of(a.rows, a.cols, n.rows, n.cols); | |
| 108 | ✗ | if (bk == BroadcastKind::Full && numel(a.rows, a.cols) != numel(n.rows, n.cols)) { | |
| 109 | ✗ | reject(n, "Where operand is neither the full shape, a row, a column nor a scalar"); | |
| 110 | } | ||
| 111 | ✗ | } | |
| 112 | ✗ | } else if (n.op == TraceOpKind::FMA) { | |
| 113 | ✗ | for (std::size_t k = 0; k < 3; ++k) { | |
| 114 | ✗ | if (numel(operand(k).rows, operand(k).cols) != numel(n.rows, n.cols)) { | |
| 115 | ✗ | reject(n, "FMA operands must be the full shape"); | |
| 116 | } | ||
| 117 | ✗ | } | |
| 118 | ✗ | } else if (n.op == TraceOpKind::RMSNorm || n.op == TraceOpKind::LayerNorm || | |
| 119 | ✗ | n.op == TraceOpKind::Modulate) { | |
| 120 | ✗ | if (numel(operand(0).rows, operand(0).cols) != numel(n.rows, n.cols)) { | |
| 121 | ✗ | reject(n, "operand shape mismatch"); | |
| 122 | } | ||
| 123 | ✗ | const std::size_t want = n.op == TraceOpKind::Modulate ? 3 : n.inputs.size(); | |
| 124 | ✗ | for (std::size_t k = 1; k < want; ++k) { | |
| 125 | ✗ | if (!vector_like(operand(k))) reject(n, "gain / shift must be a length-cols vector"); | |
| 126 | ✗ | } | |
| 127 | ✗ | } else { | |
| 128 | ✗ | reject(n, "no eager form"); | |
| 129 | } | ||
| 130 | } | ||
| 131 | 2 | } | |
| 132 | |||
| 133 | 5 | Tensor view_of(const TraceNode& n, void* buffer) { | |
| 134 | 5 | return Tensor::view(n.device, buffer, n.rows, n.cols, n.dtype); | |
| 135 | } | ||
| 136 | |||
| 137 | // `t` reshaped to (rows, cols) without copying (same element count). | ||
| 138 | 4 | Tensor reshaped(const Tensor& t, int rows, int cols) { | |
| 139 | 4 | return Tensor::view(t.device, t.data, rows, cols, t.dtype); | |
| 140 | } | ||
| 141 | |||
| 142 | 4 | Tensor as_dtype(const Tensor& t, Dtype dt, int rows, int cols) { | |
| 143 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | if (t.dtype == dt) return reshaped(t, rows, cols); |
| 144 | 2 | Tensor out; | |
| 145 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | brotensor::cast(t, out, dt); |
| 146 | 2 | out.rows = rows; | |
| 147 | 2 | out.cols = cols; | |
| 148 | 2 | return out; | |
| 149 | 6 | } | |
| 150 | |||
| 151 | 1 | Tensor as_dtype(const Tensor& t, Dtype dt) { | |
| 152 | 1 | return as_dtype(t, dt, t.rows, t.cols); | |
| 153 | } | ||
| 154 | |||
| 155 | // Copies `src` into `dst` unless they already are the same storage. | ||
| 156 | 4 | void assign(Tensor& dst, const Tensor& src) { | |
| 157 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
|
4 | if (dst.data == src.data) return; |
| 158 |
3/6✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
|
3 | if (dst.device.is_cpu() && src.device.is_cpu() && dst.dtype == src.dtype) { |
| 159 | 3 | std::memcpy(dst.host_raw_mut(), src.host_raw(), dst.bytes()); | |
| 160 | 3 | return; | |
| 161 | } | ||
| 162 | ✗ | brotensor::copy_d2d(src, 0, dst, 0, static_cast<int>(dst.size())); | |
| 163 | 4 | } | |
| 164 | |||
| 165 | // A (1, cols) row, (rows, 1) column, or (1, 1) scalar operand spread to the full (rows, cols). | ||
| 166 | 1 | Tensor expand(const Tensor& small, int rows, int cols, Dtype dt) { | |
| 167 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | const Dtype work_dt = small.device.is_cpu() ? Dtype::FP32 : dt; |
| 168 | 1 | Tensor full = Tensor::zeros_on(small.device, rows, cols, work_dt); | |
| 169 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | Tensor s = as_dtype(small, work_dt); |
| 170 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | if (small.size() == 1) { |
| 171 | ✗ | Tensor col = reshaped(full, rows * cols, 1); | |
| 172 | ✗ | brotensor::add_row_bias_inplace(col, reshaped(s, 1, 1)); | |
| 173 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | } else if (small.rows == rows && small.cols == 1) { |
| 174 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | brotensor::add_channel_bias_inplace(full, reshaped(s, rows, 1), rows, cols); |
| 175 | 1 | } else { | |
| 176 | ✗ | brotensor::add_row_bias_inplace(full, reshaped(s, 1, cols)); | |
| 177 | } | ||
| 178 |
2/6✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
1 | return work_dt == dt ? full : as_dtype(full, dt); |
| 179 | 1 | } | |
| 180 | |||
| 181 | ✗ | class Program { | |
| 182 | public: | ||
| 183 | 4 | explicit Program(const TraceDAG& dag) : nodes_(dag.nodes()) { | |
| 184 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | last_use_.assign(nodes_.size(), -1); |
| 185 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (const TraceNode& n : nodes_) { |
| 186 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 6 times.
|
10 | for (int in : n.inputs) { |
| 187 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (in >= 0) last_use_[static_cast<std::size_t>(in)] = n.id; |
| 188 | } | ||
| 189 | } | ||
| 190 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (const TraceNode& n : nodes_) { |
| 191 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
|
6 | if (n.op == TraceOpKind::Input) continue; |
| 192 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | const bool needed = n.buffer != nullptr || last_use_[static_cast<std::size_t>(n.id)] >= 0; |
| 193 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | if (needed) ++launches_; |
| 194 | } | ||
| 195 | 4 | } | |
| 196 | |||
| 197 | 2 | std::size_t launches() const { return launches_; } | |
| 198 | |||
| 199 | // Input buffers in node order, then the buffers of live-at-end nodes in | ||
| 200 | // node order — the order TraceCompiler::compile_and_cache rebinds in. | ||
| 201 | ✗ | std::shared_ptr<Program> rebound(const std::vector<void*>& in, | |
| 202 | const std::vector<void*>& out) const { | ||
| 203 | ✗ | auto p = std::make_shared<Program>(*this); | |
| 204 | ✗ | std::size_t i = 0, o = 0; | |
| 205 | ✗ | for (TraceNode& n : p->nodes_) { | |
| 206 | ✗ | if (n.op == TraceOpKind::Input) { | |
| 207 | ✗ | if (i < in.size()) n.buffer = in[i++]; | |
| 208 | ✗ | } | |
| 209 | ✗ | if (n.is_live_at_end) { | |
| 210 | ✗ | if (o < out.size()) n.buffer = out[o++]; | |
| 211 | ✗ | } | |
| 212 | } | ||
| 213 | ✗ | return p; | |
| 214 | ✗ | } | |
| 215 | |||
| 216 | 2 | void run() const { | |
| 217 | 2 | std::vector<Tensor> vals(nodes_.size()); | |
| 218 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (const TraceNode& n : nodes_) { |
| 219 | 6 | const std::size_t id = static_cast<std::size_t>(n.id); | |
| 220 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
|
6 | if (n.op == TraceOpKind::Input) { |
| 221 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | vals[id] = view_of(n, n.buffer); |
| 222 | 3 | continue; | |
| 223 | } | ||
| 224 | // A value nothing reads and nothing stores is dead code. | ||
| 225 |
3/4✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
3 | if (n.buffer == nullptr && last_use_[id] < 0) continue; |
| 226 | |||
| 227 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
4 | Tensor dst = n.buffer ? view_of(n, n.buffer) |
| 228 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | : Tensor::empty_on(n.device, n.rows, n.cols, n.dtype); |
| 229 |
5/8✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 time.
|
4 | if (n.device.is_cpu() && n.dtype != Dtype::FP32 && |
| 230 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | n.op != TraceOpKind::Copy && n.op != TraceOpKind::Cast) { |
| 231 | ✗ | TraceNode fp32_node = n; | |
| 232 | ✗ | fp32_node.dtype = Dtype::FP32; | |
| 233 | ✗ | Tensor tmp = Tensor::empty_on(n.device, n.rows, n.cols, Dtype::FP32); | |
| 234 | ✗ | evaluate(fp32_node, vals, tmp); | |
| 235 | ✗ | brotensor::cast(tmp, dst, n.dtype); | |
| 236 | ✗ | } else { | |
| 237 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | evaluate(n, vals, dst); |
| 238 | } | ||
| 239 | 3 | vals[id] = std::move(dst); | |
| 240 | |||
| 241 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
|
7 | for (int in : n.inputs) { |
| 242 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | if (in >= 0 && last_use_[static_cast<std::size_t>(in)] == n.id) { |
| 243 | 4 | vals[static_cast<std::size_t>(in)] = Tensor(); | |
| 244 | 4 | } | |
| 245 | } | ||
| 246 | 3 | } | |
| 247 | 2 | } | |
| 248 | |||
| 249 | private: | ||
| 250 | 6 | const Tensor& arg(const TraceNode& n, std::size_t k, const std::vector<Tensor>& vals) const { | |
| 251 | 6 | return vals[static_cast<std::size_t>(n.inputs[k])]; | |
| 252 | } | ||
| 253 | |||
| 254 | // Operand k as a full-shape tensor of the node's dtype. | ||
| 255 | 4 | Tensor full_arg(const TraceNode& n, std::size_t k, const std::vector<Tensor>& vals) const { | |
| 256 | 4 | const Tensor& a = arg(n, k, vals); | |
| 257 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
|
4 | if (numel(a.rows, a.cols) != numel(n.rows, n.cols)) return expand(a, n.rows, n.cols, n.dtype); |
| 258 | 3 | return as_dtype(a, n.dtype, n.rows, n.cols); | |
| 259 | 4 | } | |
| 260 | |||
| 261 | // A gain / shift vector as (1, cols) in the node's dtype. | ||
| 262 | ✗ | Tensor row_arg(const TraceNode& n, std::size_t k, const std::vector<Tensor>& vals) const { | |
| 263 | ✗ | return as_dtype(arg(n, k, vals), n.dtype, 1, n.cols); | |
| 264 | } | ||
| 265 | |||
| 266 | 3 | void evaluate(const TraceNode& n, const std::vector<Tensor>& vals, Tensor& dst) const { | |
| 267 |
2/17✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
|
3 | switch (n.op) { |
| 268 | case TraceOpKind::Copy: | ||
| 269 | case TraceOpKind::Cast: | ||
| 270 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | assign(dst, full_arg(n, 0, vals)); |
| 271 | 2 | return; | |
| 272 | case TraceOpKind::Clamp: | ||
| 273 | ✗ | assign(dst, full_arg(n, 0, vals)); | |
| 274 | ✗ | brotensor::clamp(dst, n.scalar, n.scalar2); | |
| 275 | ✗ | return; | |
| 276 | case TraceOpKind::Where: { | ||
| 277 | ✗ | Tensor res = brotensor::jit::where(arg(n, 0, vals), | |
| 278 | ✗ | full_arg(n, 1, vals), | |
| 279 | ✗ | full_arg(n, 2, vals)); | |
| 280 | ✗ | assign(dst, res); | |
| 281 | return; | ||
| 282 | ✗ | } | |
| 283 | case TraceOpKind::AddScalar: | ||
| 284 | case TraceOpKind::SubScalar: | ||
| 285 | ✗ | assign(dst, full_arg(n, 0, vals)); | |
| 286 | ✗ | brotensor::add_scalar_inplace(dst, n.op == TraceOpKind::AddScalar ? n.scalar : -n.scalar); | |
| 287 | ✗ | return; | |
| 288 | case TraceOpKind::MulScalar: | ||
| 289 | case TraceOpKind::DivScalar: | ||
| 290 | ✗ | assign(dst, full_arg(n, 0, vals)); | |
| 291 | ✗ | brotensor::scale_inplace(dst, n.op == TraceOpKind::MulScalar ? n.scalar : 1.0f / n.scalar); | |
| 292 | ✗ | return; | |
| 293 | case TraceOpKind::ScalarSub: | ||
| 294 | ✗ | assign(dst, full_arg(n, 0, vals)); | |
| 295 | ✗ | brotensor::scale_inplace(dst, -1.0f); | |
| 296 | ✗ | brotensor::add_scalar_inplace(dst, n.scalar); | |
| 297 | ✗ | return; | |
| 298 | ✗ | case TraceOpKind::SiLU: brotensor::silu_forward(full_arg(n, 0, vals), dst); return; | |
| 299 | ✗ | case TraceOpKind::GELU: brotensor::gelu_forward(full_arg(n, 0, vals), dst); return; | |
| 300 | ✗ | case TraceOpKind::ReLU: brotensor::relu_forward(full_arg(n, 0, vals), dst); return; | |
| 301 | ✗ | case TraceOpKind::Tanh: brotensor::tanh_forward(full_arg(n, 0, vals), dst); return; | |
| 302 | ✗ | case TraceOpKind::Sigmoid: brotensor::sigmoid_forward(full_arg(n, 0, vals), dst); return; | |
| 303 | case TraceOpKind::Add: | ||
| 304 | case TraceOpKind::Sub: | ||
| 305 | case TraceOpKind::Mul: | ||
| 306 | case TraceOpKind::Div: | ||
| 307 | 1 | binary(n, vals, dst); | |
| 308 | 1 | return; | |
| 309 | case TraceOpKind::FMA: { | ||
| 310 | ✗ | Tensor t = full_arg(n, 0, vals); | |
| 311 | ✗ | Tensor prod = Tensor::empty_on(n.device, n.rows, n.cols, n.dtype); | |
| 312 | ✗ | assign(prod, t); | |
| 313 | ✗ | brotensor::mul_inplace(prod, full_arg(n, 1, vals)); | |
| 314 | ✗ | brotensor::add_inplace(prod, full_arg(n, 2, vals)); | |
| 315 | ✗ | assign(dst, prod); | |
| 316 | return; | ||
| 317 | ✗ | } | |
| 318 | case TraceOpKind::RMSNorm: { | ||
| 319 | ✗ | Tensor g = n.inputs.size() > 1 ? row_arg(n, 1, vals) : ones(n); | |
| 320 | ✗ | into(dst, full_arg(n, 0, vals), [&](const Tensor& x, Tensor& y) { | |
| 321 | ✗ | brotensor::rms_norm_forward(x, g, n.scalar, y); | |
| 322 | ✗ | }); | |
| 323 | return; | ||
| 324 | ✗ | } | |
| 325 | case TraceOpKind::LayerNorm: { | ||
| 326 | ✗ | Tensor g = n.inputs.size() > 1 ? row_arg(n, 1, vals) : ones(n); | |
| 327 | ✗ | Tensor b = n.inputs.size() > 2 ? row_arg(n, 2, vals) | |
| 328 | ✗ | : Tensor::zeros_on(n.device, 1, n.cols, n.dtype); | |
| 329 | ✗ | into(dst, full_arg(n, 0, vals), [&](const Tensor& x, Tensor& y) { | |
| 330 | ✗ | brotensor::layernorm_forward_inference_batched(x, g, b, y, n.scalar); | |
| 331 | ✗ | }); | |
| 332 | return; | ||
| 333 | ✗ | } | |
| 334 | case TraceOpKind::Modulate: { | ||
| 335 | ✗ | Tensor sc = row_arg(n, 1, vals); | |
| 336 | ✗ | Tensor sh = row_arg(n, 2, vals); | |
| 337 | ✗ | into(dst, full_arg(n, 0, vals), [&](const Tensor& x, Tensor& y) { | |
| 338 | ✗ | brotensor::modulate(x, sc, sh, y); | |
| 339 | ✗ | }); | |
| 340 | return; | ||
| 341 | ✗ | } | |
| 342 | default: | ||
| 343 | ✗ | reject(n, "no eager form"); | |
| 344 | } | ||
| 345 | 3 | } | |
| 346 | |||
| 347 | ✗ | Tensor ones(const TraceNode& n) const { | |
| 348 | ✗ | Tensor t = Tensor::zeros_on(n.device, 1, n.cols, n.dtype); | |
| 349 | ✗ | brotensor::add_scalar_inplace(t, 1.0f); | |
| 350 | ✗ | return t; | |
| 351 | ✗ | } | |
| 352 | |||
| 353 | // Runs a row op whose output may not alias its input: through a scratch | ||
| 354 | // tensor when dst is the input's storage (an in-place target). | ||
| 355 | template <class F> | ||
| 356 | ✗ | void into(Tensor& dst, const Tensor& x, F&& op) const { | |
| 357 | ✗ | if (dst.data != x.data) { | |
| 358 | ✗ | op(x, dst); | |
| 359 | ✗ | return; | |
| 360 | } | ||
| 361 | ✗ | Tensor tmp = Tensor::empty_on(dst.device, dst.rows, dst.cols, dst.dtype); | |
| 362 | ✗ | op(x, tmp); | |
| 363 | ✗ | assign(dst, tmp); | |
| 364 | ✗ | } | |
| 365 | |||
| 366 | 1 | void binary(const TraceNode& n, const std::vector<Tensor>& vals, Tensor& dst) const { | |
| 367 | 1 | const Tensor& ra = arg(n, 0, vals); | |
| 368 | 1 | const Tensor& rb = arg(n, 1, vals); | |
| 369 | // Multiply by a row / scalar has a kernel of its own; no expansion. | ||
| 370 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | if (n.op == TraceOpKind::Mul) { |
| 371 | ✗ | const bool a_small = numel(ra.rows, ra.cols) != numel(n.rows, n.cols); | |
| 372 | ✗ | const bool b_small = numel(rb.rows, rb.cols) != numel(n.rows, n.cols); | |
| 373 | ✗ | if (a_small != b_small) { | |
| 374 | ✗ | const Tensor& small = a_small ? ra : rb; | |
| 375 | ✗ | if (small.size() == 1 || (small.rows == 1 && small.cols == n.cols)) { | |
| 376 | ✗ | const Tensor big = full_arg(n, a_small ? 1 : 0, vals); | |
| 377 | ✗ | const Tensor v = as_dtype(small, n.dtype); | |
| 378 | ✗ | Tensor tmp = dst.data == big.data | |
| 379 | ✗ | ? Tensor::empty_on(n.device, n.rows, n.cols, n.dtype) | |
| 380 | ✗ | : reshaped(dst, n.rows, n.cols); | |
| 381 | ✗ | if (small.size() == 1) { | |
| 382 | ✗ | Tensor y = reshaped(tmp, n.rows * n.cols, 1); | |
| 383 | ✗ | brotensor::broadcast_mul(reshaped(big, n.rows * n.cols, 1), reshaped(v, 1, 1), y); | |
| 384 | ✗ | } else { | |
| 385 | ✗ | brotensor::broadcast_mul(big, reshaped(v, 1, n.cols), tmp); | |
| 386 | } | ||
| 387 | ✗ | assign(dst, tmp); | |
| 388 | return; | ||
| 389 | ✗ | } | |
| 390 | ✗ | } | |
| 391 | ✗ | } | |
| 392 | 1 | Tensor a = full_arg(n, 0, vals); | |
| 393 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | Tensor b = full_arg(n, 1, vals); |
| 394 | // dst = a op b, with dst possibly the storage of a (an in-place | ||
| 395 | // target) or of b. Work in dst when it is a's storage or neither. | ||
| 396 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
2 | Tensor acc = dst.data == b.data && dst.data != a.data |
| 397 | ✗ | ? Tensor::empty_on(n.device, n.rows, n.cols, n.dtype) | |
| 398 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | : reshaped(dst, n.rows, n.cols); |
| 399 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | assign(acc, a); |
| 400 |
1/5✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
|
1 | switch (n.op) { |
| 401 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | case TraceOpKind::Add: brotensor::add_inplace(acc, b); break; |
| 402 | ✗ | case TraceOpKind::Sub: brotensor::axpby_inplace(acc, b, 1.0f, -1.0f); break; | |
| 403 | ✗ | case TraceOpKind::Mul: brotensor::mul_inplace(acc, b); break; | |
| 404 | ✗ | case TraceOpKind::Div: brotensor::div_inplace(acc, b); break; | |
| 405 | ✗ | default: reject(n, "not a binary op"); | |
| 406 | } | ||
| 407 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | assign(dst, acc); |
| 408 | 1 | } | |
| 409 | |||
| 410 | std::vector<TraceNode> nodes_; | ||
| 411 | std::vector<int> last_use_; | ||
| 412 | 2 | std::size_t launches_ = 0; | |
| 413 | }; | ||
| 414 | |||
| 415 | 2 | std::shared_ptr<TraceHandleImpl> make_handle(std::shared_ptr<const Program> prog) { | |
| 416 | 2 | auto h = std::make_shared<TraceHandleImpl>(); | |
| 417 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
4 | h->execute_fn = [prog] { prog->run(); }; |
| 418 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | h->rebind_fn = [prog](const std::vector<void*>& in, const std::vector<void*>& out) { |
| 419 | ✗ | return make_handle(prog->rebound(in, out)); | |
| 420 | ✗ | }; | |
| 421 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | h->launch_count = prog->launches(); |
| 422 | 2 | h->fusion_name = "eager-unfused"; | |
| 423 | 2 | return h; | |
| 424 | 2 | } | |
| 425 | |||
| 426 | } // namespace | ||
| 427 | |||
| 428 | 2 | std::shared_ptr<TraceHandleImpl> compile_eager(const TraceDAG& dag) { | |
| 429 | 2 | const auto t0 = std::chrono::steady_clock::now(); | |
| 430 | 2 | validate(dag); | |
| 431 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | auto h = make_handle(std::make_shared<const Program>(dag)); |
| 432 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
4 | h->compile_us = std::chrono::duration<double, std::micro>( |
| 433 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | std::chrono::steady_clock::now() - t0).count(); |
| 434 | 2 | return h; | |
| 435 | 2 | } | |
| 436 | |||
| 437 | } // namespace brotensor::jit::eager | ||
| 438 |