char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Georgi Gerganovmetal : single-source fusion table + fusion debug rework (#28164)a2878d30d

char/aliceai-t5-moe
13.5 KiB447 linesraw
1#include "ggml-metal-common.h"
2#include "ggml-metal-fusion.h"
3
4#include "ggml.h"
5#include "ggml-impl.h"
6#include "ggml-backend-impl.h"
7
8#include <vector>
9
10bool ggml_metal_op_mul_mat_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm) {
11    const int64_t ne00 = op->src[0]->ne[0];
12    const int64_t ne11 = op->src[1]->ne[1];
13
14    return !ggml_is_transposed(op->src[0]) &&
15           !ggml_is_transposed(op->src[1]) &&
16           has_simdgroup_mm && ne00 >= 64 && ne11 > 8;
17}
18
19bool ggml_metal_op_mul_mat_id_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm) {
20    const int64_t ne00 = op->src[0]->ne[0];
21    const int64_t ne21 = op->src[2]->ne[1];
22
23    return has_simdgroup_mm && ne00 >= 64 && ne21 >= 32;
24}
25
26// represents a memory range (i.e. an interval from a starting address p0 to an ending address p1 in a given buffer pb)
27// the type indicates whether it is a source range (i.e. ops read data from it) or a destination range (i.e. ops write data to it)
28struct ggml_mem_range {
29    uint64_t pb; // buffer id
30
31    uint64_t p0; // begin
32    uint64_t p1; // end
33
34    ggml_mem_range_type pt;
35};
36
37struct ggml_mem_ranges {
38    std::vector<ggml_mem_range> ranges;
39
40    int debug = 0;
41};
42
43ggml_mem_ranges_t ggml_mem_ranges_init(int debug) {
44    auto * res = new ggml_mem_ranges;
45
46    res->ranges.reserve(256);
47    res->debug = debug;
48
49    return res;
50}
51
52void ggml_mem_ranges_free(ggml_mem_ranges_t mrs) {
53    delete mrs;
54}
55
56void ggml_mem_ranges_reset(ggml_mem_ranges_t mrs) {
57    mrs->ranges.clear();
58}
59
60static bool ggml_mem_ranges_add(ggml_mem_ranges_t mrs, ggml_mem_range mr) {
61    mrs->ranges.push_back(mr);
62
63    return true;
64}
65
66static ggml_mem_range ggml_mem_range_from_tensor(const ggml_tensor * tensor, ggml_mem_range_type pt) {
67    // always use the base tensor
68    tensor = tensor->view_src ? tensor->view_src : tensor;
69
70    GGML_ASSERT(!tensor->view_src);
71
72    ggml_mem_range mr;
73
74    if (tensor->buffer) {
75        // when the tensor is allocated, use the actual memory address range in the buffer
76        //
77        // take the actual allocated size with ggml_backend_buft_get_alloc_size()
78        // this can be larger than the tensor size if the buffer type allocates extra memory
79        // ref: https://github.com/ggml-org/llama.cpp/pull/15966
80        mr = {
81            /*.pb =*/ (uint64_t) tensor->buffer,
82            /*.p0 =*/ (uint64_t) tensor->data,
83            /*.p1 =*/ (uint64_t) tensor->data + ggml_backend_buft_get_alloc_size(tensor->buffer->buft, tensor),
84            /*.pt =*/ pt,
85        };
86    } else {
87        // otherwise, the pointer address is used as an unique id of the memory ranges
88        //   that the tensor will be using when it is allocated
89        mr = {
90            /*.pb =*/ (uint64_t) tensor,
91            /*.p0 =*/ 0,    //
92            /*.p1 =*/ 1024, // [0, 1024) is a dummy range, not used
93            /*.pt =*/ pt,
94        };
95    };
96
97    return mr;
98}
99
100static ggml_mem_range ggml_mem_range_from_tensor_src(const ggml_tensor * tensor) {
101    return ggml_mem_range_from_tensor(tensor, MEM_RANGE_TYPE_SRC);
102}
103
104static ggml_mem_range ggml_mem_range_from_tensor_dst(const ggml_tensor * tensor) {
105    return ggml_mem_range_from_tensor(tensor, MEM_RANGE_TYPE_DST);
106}
107
108static bool ggml_mem_ranges_add_src(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
109    GGML_ASSERT(tensor);
110
111    ggml_mem_range mr = ggml_mem_range_from_tensor_src(tensor);
112
113    if (mrs->debug > 2) {
114        GGML_LOG_DEBUG("%s: add src range buf=%lld, [%lld, %lld)\n", __func__, mr.pb, mr.p0, mr.p1);
115    }
116
117    return ggml_mem_ranges_add(mrs, mr);
118}
119
120static bool ggml_mem_ranges_add_dst(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
121    GGML_ASSERT(tensor);
122
123    ggml_mem_range mr = ggml_mem_range_from_tensor_dst(tensor);
124
125    if (mrs->debug > 2) {
126        GGML_LOG_DEBUG("%s: add dst range buf=%lld, [%lld, %lld)\n", __func__, mr.pb, mr.p0, mr.p1);
127    }
128
129    return ggml_mem_ranges_add(mrs, mr);
130}
131
132bool ggml_mem_ranges_add(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
133    for (int i = 0; i < GGML_MAX_SRC; i++) {
134        if (tensor->src[i]) {
135            ggml_mem_ranges_add_src(mrs, tensor->src[i]);
136        }
137    }
138
139    return ggml_mem_ranges_add_dst(mrs, tensor);
140}
141
142static bool ggml_mem_ranges_check(ggml_mem_ranges_t mrs, ggml_mem_range mr) {
143    for (size_t i = 0; i < mrs->ranges.size(); i++) {
144        const auto & cmp = mrs->ranges[i];
145
146        // two memory ranges cannot intersect if they are in different buffers
147        if (mr.pb != cmp.pb) {
148            continue;
149        }
150
151        // intersecting source ranges are allowed
152        if (mr.pt == MEM_RANGE_TYPE_SRC && cmp.pt == MEM_RANGE_TYPE_SRC) {
153            continue;
154        }
155
156        if (mr.p0 < cmp.p1 && mr.p1 >= cmp.p0) {
157            if (mrs->debug > 2) {
158                GGML_LOG_DEBUG("%s: the %s range buf=%lld, [%lld, %lld) overlaps with a previous %s range buf=%lld, [%lld, %lld)\n",
159                        __func__,
160                        mr.pt == MEM_RANGE_TYPE_SRC ? "src" : "dst",
161                        mr.pb, mr.p0, mr.p1,
162                        cmp.pt == MEM_RANGE_TYPE_SRC ? "src" : "dst",
163                        cmp.pb, cmp.p0, cmp.p1);
164            }
165
166            return false;
167        }
168    }
169
170    return true;
171}
172
173static bool ggml_mem_ranges_check_src(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
174    GGML_ASSERT(tensor);
175
176    ggml_mem_range mr = ggml_mem_range_from_tensor_src(tensor);
177
178    const bool res = ggml_mem_ranges_check(mrs, mr);
179
180    return res;
181}
182
183static bool ggml_mem_ranges_check_dst(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
184    GGML_ASSERT(tensor);
185
186    ggml_mem_range mr = ggml_mem_range_from_tensor_dst(tensor);
187
188    const bool res = ggml_mem_ranges_check(mrs, mr);
189
190    return res;
191}
192
193bool ggml_mem_ranges_check(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
194    for (int i = 0; i < GGML_MAX_SRC; i++) {
195        if (tensor->src[i]) {
196            if (!ggml_mem_ranges_check_src(mrs, tensor->src[i])) {
197                return false;
198            }
199        }
200    }
201
202    return ggml_mem_ranges_check_dst(mrs, tensor);
203}
204
205struct node_info {
206    ggml_tensor * node;
207
208    std::vector<ggml_tensor *> fused;
209
210    ggml_op op() const {
211        return node->op;
212    }
213
214    const ggml_tensor * dst() const {
215        return fused.empty() ? node : fused.back();
216    }
217
218    bool is_empty() const {
219        return ggml_op_is_empty(node->op);
220    }
221
222    void add_fused(ggml_tensor * t) {
223        fused.push_back(t);
224    }
225};
226
227static std::vector<int> ggml_metal_graph_optimize_reorder(const std::vector<node_info> & nodes) {
228    // helper to add node src and dst ranges
229    const auto & h_add = [](ggml_mem_ranges_t mrs, const node_info & node) {
230        for (int i = 0; i < GGML_MAX_SRC; i++) {
231            if (node.node->src[i]) {
232                if (!ggml_mem_ranges_add_src(mrs, node.node->src[i])) {
233                    return false;
234                }
235            }
236        }
237
238        // keep track of the sources of the fused nodes as well
239        for (const auto * fused : node.fused) {
240            for (int i = 0; i < GGML_MAX_SRC; i++) {
241                if (fused->src[i]) {
242                    if (!ggml_mem_ranges_add_src(mrs, fused->src[i])) {
243                        return false;
244                    }
245                }
246            }
247        }
248
249        return ggml_mem_ranges_add_dst(mrs, node.dst());
250    };
251
252    // helper to check if a node can run concurrently with the existing set of nodes
253    const auto & h_check = [](ggml_mem_ranges_t mrs, const node_info & node) {
254        for (int i = 0; i < GGML_MAX_SRC; i++) {
255            if (node.node->src[i]) {
256                if (!ggml_mem_ranges_check_src(mrs, node.node->src[i])) {
257                    return false;
258                }
259            }
260        }
261
262        for (const auto * fused : node.fused) {
263            for (int i = 0; i < GGML_MAX_SRC; i++) {
264                if (fused->src[i]) {
265                    if (!ggml_mem_ranges_check_src(mrs, fused->src[i])) {
266                        return false;
267                    }
268                }
269            }
270        }
271
272        return ggml_mem_ranges_check_dst(mrs, node.dst());
273    };
274
275    // perform reorders only across these types of ops
276    // can be expanded when needed
277    const auto & h_safe = [](ggml_op op) {
278        switch (op) {
279            case GGML_OP_MUL_MAT:
280            case GGML_OP_MUL_MAT_ID:
281            case GGML_OP_ROPE:
282            case GGML_OP_NORM:
283            case GGML_OP_RMS_NORM:
284            case GGML_OP_GROUP_NORM:
285            case GGML_OP_L2_NORM:
286            case GGML_OP_SUM_ROWS:
287            case GGML_OP_SSM_CONV:
288            case GGML_OP_SSM_SCAN:
289            case GGML_OP_CLAMP:
290            case GGML_OP_TRI:
291            case GGML_OP_DIAG:
292            case GGML_OP_MUL:
293            case GGML_OP_ADD:
294            case GGML_OP_SUB:
295            case GGML_OP_DIV:
296            case GGML_OP_GLU:
297            case GGML_OP_SCALE:
298            case GGML_OP_UNARY:
299            case GGML_OP_GET_ROWS:
300            case GGML_OP_SET_ROWS:
301            case GGML_OP_SET:
302            case GGML_OP_CPY:
303            case GGML_OP_CONT:
304            case GGML_OP_REPEAT:
305                return true;
306            default:
307                return ggml_op_is_empty(op);
308        }
309    };
310
311    const int n = nodes.size();
312
313    std::vector<int> res;
314    res.reserve(n);
315
316    std::vector<bool> used(n, false);
317
318    // the memory ranges for the set of currently concurrent nodes
319    ggml_mem_ranges_t mrs0 = ggml_mem_ranges_init(0);
320
321    // the memory ranges for the set of nodes that haven't been processed yet, when looking forward for a node to reorder
322    ggml_mem_ranges_t mrs1 = ggml_mem_ranges_init(0);
323
324    for (int i0 = 0; i0 < n; i0++) {
325        if (used[i0]) {
326            continue;
327        }
328
329        const auto & node0 = nodes[i0];
330
331        // the node is not concurrent with the existing concurrent set, so we have to "put a barrier" (i.e reset mrs0)
332        // but before we do that, look forward for some other nodes that can be added to the concurrent set mrs0
333        //
334        // note: we can always add empty nodes to the concurrent set as they don't read nor write anything
335        if (!node0.is_empty() && !h_check(mrs0, node0)) {
336            // this will hold the set of memory ranges from the nodes that haven't been processed yet
337            // if a node is not concurrent with this set, we cannot reorder it
338            ggml_mem_ranges_reset(mrs1);
339
340            // initialize it with the current node
341            h_add(mrs1, node0);
342
343            // that many nodes forward to search for a concurrent node
344            constexpr int N_FORWARD = 64;
345
346            for (int i1 = i0 + 1; i1 < i0 + N_FORWARD && i1 < n; i1++) {
347                if (used[i1]) {
348                    continue;
349                }
350
351                const auto & node1 = nodes[i1];
352
353                // disallow reordering of certain ops
354                if (!h_safe(node1.op())) {
355                    break;
356                }
357
358                const bool is_empty = node1.is_empty();
359
360                // to reorder a node and add it to the concurrent set, it has to be:
361                //   + empty or concurrent with all nodes in the existing concurrent set (mrs0)
362                //   + concurrent with all nodes prior to it that haven't been processed yet (mrs1)
363                if ((is_empty || h_check(mrs0, node1)) && h_check(mrs1, node1)) {
364                    // add the node to the existing concurrent set (i.e. reorder it for early execution)
365                    h_add(mrs0, node1);
366                    res.push_back(i1);
367
368                    // mark as used, so we skip re-processing it later
369                    used[i1] = true;
370                } else {
371                    // expand the set of nodes that haven't been processed yet
372                    h_add(mrs1, node1);
373                }
374            }
375
376            // finalize the concurrent set and begin a new one
377            ggml_mem_ranges_reset(mrs0);
378        }
379
380        // expand the concurrent set with the current node
381        {
382            h_add(mrs0, node0);
383            res.push_back(i0);
384        }
385    }
386
387    ggml_mem_ranges_free(mrs0);
388    ggml_mem_ranges_free(mrs1);
389
390    return res;
391}
392
393void ggml_graph_optimize(ggml_cgraph * gf) {
394    const int n = gf->n_nodes;
395
396    std::vector<node_info> nodes;
397    nodes.reserve(gf->n_nodes);
398
399    // fuse nodes:
400    // we don't want to make reorders that break fusing, so we first pack all fusable tensors
401    //   and perform the reorder over the fused nodes. after the reorder is done, we unfuse
402    //
403    // the fusable sequences are declared in the fusion table (ggml-metal-fuse.cpp), so the
404    // packing here is driven by the same patterns that the op encoders will later use
405    for (int i = 0; i < n; i++) {
406        node_info node = {
407            /*.node =*/ gf->nodes[i],
408            /*.fused =*/ {},
409        };
410
411        const int f = ggml_metal_fusion_max(gf, i);
412
413        // add the fused tensors into the node info so we can unfuse them later
414        for (int k = 1; k < f; k++) {
415            ++i;
416
417            // the .dst() becomes the last fused tensor
418            node.add_fused(gf->nodes[i]);
419        }
420
421        nodes.push_back(std::move(node));
422    }
423
424#if 1
425    // reorder to improve concurrency
426    const auto order = ggml_metal_graph_optimize_reorder(nodes);
427#else
428    std::vector<int> order(nodes.size());
429    for (size_t i = 0; i < nodes.size(); i++) {
430        order[i] = i;
431    }
432#endif
433
434    // unfuse
435    {
436        int j = 0;
437        for (const auto i : order) {
438            const auto & node = nodes[i];
439
440            gf->nodes[j++] = node.node;
441
442            for (auto * fused : node.fused) {
443                gf->nodes[j++] = fused;
444            }
445        }
446    }
447}