src/jit/trace_plan.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Device-neutral fusion plans (trace_plan.h). Moved out of the CUDA trace | ||
| 2 | // compiler so the Vulkan compiler builds exactly the same plans. | ||
| 3 | |||
| 4 | #include "trace_plan.h" | ||
| 5 | |||
| 6 | #include <algorithm> | ||
| 7 | #include <cstddef> | ||
| 8 | #include <cstdint> | ||
| 9 | |||
| 10 | namespace brotensor::jit::plan { | ||
| 11 | |||
| 12 | 340 | int elem_bytes(Dtype d) { | |
| 13 |
1/3✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 340 times.
|
340 | switch (d) { |
| 14 | 340 | case Dtype::FP32: return 4; | |
| 15 | case Dtype::FP16: | ||
| 16 | ✗ | case Dtype::BF16: return 2; | |
| 17 | ✗ | default: return 0; | |
| 18 | } | ||
| 19 | 340 | } | |
| 20 | |||
| 21 | 36 | bool dtype_supported(Dtype d) { return elem_bytes(d) != 0; } | |
| 22 | |||
| 23 | 36 | int vec_width_for(Dtype d) { | |
| 24 | 36 | const int e = elem_bytes(d); | |
| 25 |
1/2✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
|
36 | return e ? 16 / e : 1; |
| 26 | } | ||
| 27 | |||
| 28 | 32 | void row_norm_geometry(int cols, int rows, int lanes, int& tpr, int& rpb) { | |
| 29 |
1/2✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
|
32 | if (lanes < 1) lanes = 1; |
| 30 | 32 | const int per_row = (cols + lanes - 1) / lanes; // threads to cover a row | |
| 31 | 32 | int t = 32; | |
| 32 |
4/4✓ Branch 0 taken 16 times.
✓ Branch 1 taken 72 times.
✓ Branch 2 taken 56 times.
✓ Branch 3 taken 32 times.
|
88 | while (t < per_row && t < kRowThreads) t <<= 1; |
| 33 | 32 | int r = kRowThreads / t; | |
| 34 | // The out-of-range exit has to be block-uniform, or the threads that stay | ||
| 35 | // hit a barrier the ones that left never will. | ||
| 36 |
3/4✓ Branch 0 taken 16 times.
✓ Branch 1 taken 16 times.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
|
32 | while (r > 1 && rows % r != 0) r >>= 1; |
| 37 | 32 | tpr = t; | |
| 38 | 32 | rpb = r; | |
| 39 | 32 | } | |
| 40 | |||
| 41 | 8 | bool build_plan(const TraceDAG& dag, ElementwisePlan& plan) { | |
| 42 | 8 | int rows = 0, cols = 0; | |
| 43 | 8 | dag.dominant_shape(rows, cols); | |
| 44 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
|
8 | if (rows <= 0 || cols <= 0) return false; |
| 45 | |||
| 46 | 8 | plan.rows = rows; | |
| 47 | 8 | plan.cols = cols; | |
| 48 | 8 | plan.numel = static_cast<std::int64_t>(rows) * cols; | |
| 49 | |||
| 50 | 8 | int lanes = 8; | |
| 51 |
2/2✓ Branch 0 taken 38 times.
✓ Branch 1 taken 8 times.
|
46 | for (const auto& n : dag.nodes()) { |
| 52 | 38 | const bool is_in = (n.op == TraceOpKind::Input); | |
| 53 | 38 | const bool is_out = n.is_live_at_end; | |
| 54 |
4/4✓ Branch 0 taken 12 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 2 times.
|
38 | if (!is_in && !is_out) continue; |
| 55 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
|
36 | if (!dtype_supported(n.dtype)) return false; |
| 56 | |||
| 57 | 36 | const BroadcastKind bk = broadcast_of(n.rows, n.cols, rows, cols); | |
| 58 |
3/4✓ Branch 0 taken 22 times.
✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
|
36 | if (bk == BroadcastKind::Full && |
| 59 | 22 | static_cast<std::int64_t>(n.rows) * n.cols != plan.numel) { | |
| 60 | ✗ | return false; | |
| 61 | } | ||
| 62 |
3/4✓ Branch 0 taken 10 times.
✓ Branch 1 taken 26 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
|
36 | if (is_out && bk != BroadcastKind::Full) return false; |
| 63 | |||
| 64 | 36 | lanes = std::min(lanes, vec_width_for(n.dtype)); | |
| 65 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 14 times.
|
36 | if (bk == BroadcastKind::Row) plan.any_row_bcast = true; |
| 66 |
1/2✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
|
36 | if (bk == BroadcastKind::Col) plan.any_col_bcast = true; |
| 67 | |||
| 68 | 36 | const BufferSpec spec{n.id, n.dtype, bk}; | |
| 69 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 26 times.
|
36 | if (is_in) plan.inputs.push_back(spec); |
| 70 |
2/2✓ Branch 0 taken 26 times.
✓ Branch 1 taken 10 times.
|
36 | if (is_out) plan.outputs.push_back(spec); |
| 71 | } | ||
| 72 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (plan.outputs.empty()) return false; |
| 73 | |||
| 74 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (plan.numel % lanes != 0) lanes = 1; |
| 75 |
2/4✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | if ((plan.any_row_bcast || plan.any_col_bcast) && cols % lanes != 0) lanes = 1; |
| 76 | 8 | plan.vec = lanes; | |
| 77 | 8 | return true; | |
| 78 | 8 | } | |
| 79 | |||
| 80 | 8 | bool build_row_norm_plan(const TraceDAG& dag, RowNormPlan& plan) { | |
| 81 | 8 | const auto& nodes = dag.nodes(); | |
| 82 | 8 | const TraceNode* norm = nullptr; | |
| 83 |
2/2✓ Branch 0 taken 38 times.
✓ Branch 1 taken 8 times.
|
46 | for (const auto& n : nodes) { |
| 84 |
4/4✓ Branch 0 taken 36 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 30 times.
|
38 | if (n.op == TraceOpKind::RMSNorm || n.op == TraceOpKind::LayerNorm) { |
| 85 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (norm) return false; // more than one reduction |
| 86 | 8 | norm = &n; | |
| 87 | 8 | } | |
| 88 | } | ||
| 89 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
|
8 | if (!norm || norm->inputs.empty()) return false; |
| 90 | |||
| 91 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (!build_plan(dag, plan.ew)) return false; |
| 92 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (plan.ew.cols != norm->cols) return false; |
| 93 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (plan.ew.rows != norm->rows) return false; |
| 94 | |||
| 95 | 22 | auto input_index = [&](int node_id) { | |
| 96 |
1/2✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
|
42 | for (std::size_t i = 0; i < plan.ew.inputs.size(); ++i) { |
| 97 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 28 times.
|
42 | if (plan.ew.inputs[i].node_id == node_id) return static_cast<int>(i); |
| 98 | 28 | } | |
| 99 | ✗ | return -1; | |
| 100 | 14 | }; | |
| 101 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (norm->inputs.size() > 1) { |
| 102 | 8 | plan.gamma_input = input_index(norm->inputs[1]); | |
| 103 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (plan.gamma_input < 0) return false; |
| 104 | 8 | } | |
| 105 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
|
8 | if (norm->inputs.size() > 2) { |
| 106 | 6 | plan.beta_input = input_index(norm->inputs[2]); | |
| 107 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | if (plan.beta_input < 0) return false; |
| 108 | 6 | } | |
| 109 | |||
| 110 | 8 | plan.x_node = norm->inputs[0]; | |
| 111 | 8 | plan.norm_node = norm->id; | |
| 112 | 8 | plan.reduce = (norm->op == TraceOpKind::LayerNorm) ? RowReduce::Mean : RowReduce::RMS; | |
| 113 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | plan.eps = norm->scalar > 0.0f ? norm->scalar : 1e-5f; |
| 114 | |||
| 115 | // Reverse reachability from the reduction's data input. Node ids are | ||
| 116 | // assigned in creation order, which is topological, so one backward sweep | ||
| 117 | // is enough. | ||
| 118 | 8 | std::vector<char> in_pre(nodes.size(), 0); | |
| 119 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (plan.x_node >= 0) in_pre[static_cast<std::size_t>(plan.x_node)] = 1; |
| 120 |
2/2✓ Branch 0 taken 38 times.
✓ Branch 1 taken 8 times.
|
46 | for (std::size_t i = nodes.size(); i-- > 0;) { |
| 121 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 16 times.
|
38 | if (!in_pre[i]) continue; |
| 122 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 16 times.
|
24 | for (int src : nodes[i].inputs) { |
| 123 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (src >= 0) in_pre[static_cast<std::size_t>(src)] = 1; |
| 124 | } | ||
| 125 | } | ||
| 126 | 8 | in_pre[static_cast<std::size_t>(norm->id)] = 0; | |
| 127 | |||
| 128 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 38 times.
|
46 | for (const auto& n : nodes) { |
| 129 |
2/2✓ Branch 0 taken 12 times.
✓ Branch 1 taken 26 times.
|
38 | if (n.op == TraceOpKind::Input) continue; |
| 130 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
|
12 | if (n.id == norm->id) continue; |
| 131 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (in_pre[static_cast<std::size_t>(n.id)]) { |
| 132 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | plan.pre_ids.push_back(n.id); |
| 133 | 4 | } else { | |
| 134 | ✗ | plan.post_ids.push_back(n.id); | |
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | plan.pre_input.assign(plan.ew.inputs.size(), 0); |
| 139 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 26 times.
|
34 | for (std::size_t i = 0; i < plan.ew.inputs.size(); ++i) { |
| 140 | 26 | const int id = plan.ew.inputs[i].node_id; | |
| 141 | 26 | plan.pre_input[i] = in_pre[static_cast<std::size_t>(id)] ? 1 : 0; | |
| 142 | 26 | } | |
| 143 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | plan.pre_output.assign(plan.ew.outputs.size(), 0); |
| 144 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 8 times.
|
18 | for (std::size_t i = 0; i < plan.ew.outputs.size(); ++i) { |
| 145 | 10 | const int id = plan.ew.outputs[i].node_id; | |
| 146 | 10 | plan.pre_output[i] = in_pre[static_cast<std::size_t>(id)] ? 1 : 0; | |
| 147 | 10 | } | |
| 148 | |||
| 149 | // Pass two can skip the pre chain entirely when its result is one of the | ||
| 150 | // trace's own outputs — pass one writes it, pass two reads it back — and | ||
| 151 | // when nothing downstream of the reduction needs any *other* pre value. | ||
| 152 | 8 | plan.x_output = -1; | |
| 153 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
|
8 | if (!plan.pre_ids.empty()) { |
| 154 | 4 | int x_out = -1; | |
| 155 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
|
10 | for (std::size_t i = 0; i < plan.ew.outputs.size(); ++i) { |
| 156 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
|
6 | if (plan.ew.outputs[i].node_id == plan.x_node) x_out = static_cast<int>(i); |
| 157 | 6 | } | |
| 158 | 4 | bool post_only_needs_x = true; | |
| 159 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | for (int pid : plan.post_ids) { |
| 160 | ✗ | for (int src : nodes[static_cast<std::size_t>(pid)].inputs) { | |
| 161 | ✗ | if (src < 0) continue; | |
| 162 | ✗ | if (src == plan.x_node) continue; | |
| 163 | ✗ | if (nodes[static_cast<std::size_t>(src)].op == TraceOpKind::Input) continue; | |
| 164 | ✗ | if (in_pre[static_cast<std::size_t>(src)]) post_only_needs_x = false; | |
| 165 | } | ||
| 166 | } | ||
| 167 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
4 | if (x_out >= 0 && post_only_needs_x) plan.x_output = x_out; |
| 168 | 4 | } | |
| 169 | |||
| 170 | // Without the reload, pass two replays the pre chain from its inputs — | ||
| 171 | // and pass one has already overwritten whatever it stored. An in-place | ||
| 172 | // pre chain (`x += p` with x a pass-one output) would replay as | ||
| 173 | // x + p + p. Only the reload form is correct once pass one stores. | ||
| 174 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
|
8 | if (plan.x_output < 0) { |
| 175 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
|
12 | for (char w : plan.pre_output) { |
| 176 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | if (w) return false; |
| 177 | } | ||
| 178 | 6 | } | |
| 179 | |||
| 180 | // What pass two loads: with the reload it is the reduction's gamma/beta | ||
| 181 | // plus whatever the post chain reads; without it, everything. | ||
| 182 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | plan.post_input.assign(plan.ew.inputs.size(), plan.x_output >= 0 ? 0 : 1); |
| 183 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | if (plan.x_output >= 0) { |
| 184 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (plan.gamma_input >= 0) plan.post_input[static_cast<std::size_t>(plan.gamma_input)] = 1; |
| 185 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (plan.beta_input >= 0) plan.post_input[static_cast<std::size_t>(plan.beta_input)] = 1; |
| 186 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | for (int pid : plan.post_ids) { |
| 187 | ✗ | for (int src : nodes[static_cast<std::size_t>(pid)].inputs) { | |
| 188 | ✗ | if (src < 0) continue; | |
| 189 | ✗ | for (std::size_t i = 0; i < plan.ew.inputs.size(); ++i) { | |
| 190 | ✗ | if (plan.ew.inputs[i].node_id == src) plan.post_input[i] = 1; | |
| 191 | ✗ | } | |
| 192 | } | ||
| 193 | } | ||
| 194 | 2 | } | |
| 195 | |||
| 196 | // The row kernel's stride loop takes `vec` columns per thread per step. | ||
| 197 | // build_plan already reduced `vec` to the narrowest dtype in play; the row | ||
| 198 | // form additionally needs D to divide evenly so a vector never straddles a | ||
| 199 | // row boundary. | ||
| 200 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (plan.ew.cols % plan.ew.vec != 0) plan.ew.vec = 1; |
| 201 | 8 | return true; | |
| 202 | 8 | } | |
| 203 | |||
| 204 | ✗ | bool all_fp32(const TraceDAG& dag) { | |
| 205 | ✗ | for (const auto& n : dag.nodes()) { | |
| 206 | ✗ | if (n.dtype != Dtype::FP32) return false; | |
| 207 | } | ||
| 208 | ✗ | return true; | |
| 209 | ✗ | } | |
| 210 | |||
| 211 | } // namespace brotensor::jit::plan | ||
| 212 |