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
17.7 KiB502 linesraw
1#include "ggml-metal-fusion.h"
2
3#include "ggml-backend-impl.h"
4#include "ggml-metal-device.h"
5
6#include <algorithm>
7#include <string>
8#include <vector>
9
10// ---- helpers -------------------------------------------------------------
11
12// true if two tensors live in the same Metal buffer
13static bool ggml_metal_fusion_same_buffer(const ggml_tensor * a, const ggml_tensor * b) {
14    if (!a || !b) {
15        return false;
16    }
17
18    ggml_backend_buffer_t ba = a->view_src ? a->view_src->buffer : a->buffer;
19    ggml_backend_buffer_t bb = b->view_src ? b->view_src->buffer : b->buffer;
20
21    ggml_metal_buffer_t ca = (ggml_metal_buffer_t) ba->context;
22    ggml_metal_buffer_t cb = (ggml_metal_buffer_t) bb->context;
23
24    return ggml_metal_buffer_get_id(ca, a).metal == ggml_metal_buffer_get_id(cb, b).metal;
25}
26
27// ---- pattern checks ------------------------------------------------------
28
29// NORM/RMS_NORM + MUL + ADD: the weight/bias of each fused step must match the norm input
30// width, be contiguous rows, and the fused outputs must stay F32
31static bool ggml_metal_fusion_check_norm(
32        const ggml_metal_fusion      * fusion,
33        const ggml_tensor * const    * nodes,
34              ggml_metal_fusion_mode   mode) {
35    GGML_UNUSED(mode);
36
37    GGML_ASSERT(fusion->n_ops >= 2);
38
39    for (int j = 1; j < fusion->n_ops; j++) {
40        // the fused MUL/ADD must read the previous node as src0
41        if (nodes[j]->src[0] != nodes[j - 1]) {
42            return false;
43        }
44
45        // the weight/bias must have the same row width as the norm input
46        if (nodes[j]->src[1]->ne[0] != nodes[0]->ne[0]) {
47            return false;
48        }
49
50        if (!ggml_is_contiguous_rows(nodes[j]->src[1])) {
51            return false;
52        }
53
54        if (nodes[j]->type != GGML_TYPE_F32) {
55            return false;
56        }
57    }
58
59    return true;
60}
61
62// ADD x N: each ADD reads the previous ADD as src0, and all addends must share layout
63// (and, in FULL mode, live in the same Metal buffer)
64static bool ggml_metal_fusion_check_add_chain(
65        const ggml_metal_fusion      * fusion,
66        const ggml_tensor * const    * nodes,
67              ggml_metal_fusion_mode   mode) {
68    GGML_ASSERT(fusion->n_ops >= 2);
69
70    for (int j = 1; j < fusion->n_ops; j++) {
71        if (nodes[j]->src[0] != nodes[j - 1]) {
72            return false;
73        }
74
75        if (!ggml_are_same_layout(nodes[j]->src[1], nodes[j - 1]->src[1])) {
76            return false;
77        }
78
79        if (mode == GGML_METAL_FUSION_FULL) {
80            if (!ggml_metal_fusion_same_buffer(nodes[j]->src[1], nodes[0]->src[1])) {
81                return false;
82            }
83        }
84    }
85
86    return true;
87}
88
89// GATED_DELTA_NET + CPY: the trailing cpy scatters the gdn state snapshots into the recurrent
90// cache, so the gdn kernel writes them straight to the cache and the cpy is elided.
91// mirrors ggml_metal_op_can_fuse_gdn_cache (PR #25788). the gdn output has other consumers (the
92// attn scores view), so unlike the other patterns this is not an elision chain: the structural
93// checks live entirely in this callback (unsafe = true).
94static bool ggml_metal_fusion_check_gdn_cache(
95        const ggml_metal_fusion      * fusion,
96        const ggml_tensor * const    * nodes,
97              ggml_metal_fusion_mode    mode) {
98    GGML_UNUSED(fusion);
99
100    const ggml_tensor * gdn = nodes[0];
101    const ggml_tensor * cpy = nodes[1];
102
103    // the kernel skips the snapshot tail, so the gdn output must not be a graph output
104    if (gdn->type != GGML_TYPE_F32 || (gdn->flags & GGML_TENSOR_FLAG_OUTPUT)) {
105        return false;
106    }
107
108    if (cpy->op != GGML_OP_CPY || (cpy->flags & GGML_TENSOR_FLAG_OUTPUT)) {
109        return false;
110    }
111
112    const int64_t S_v      = gdn->src[2]->ne[0];
113    const int64_t H        = gdn->src[2]->ne[1];
114    const int64_t n_tokens = gdn->src[2]->ne[2];
115    const int64_t n_seqs   = gdn->src[2]->ne[3];
116    const int64_t K        = ggml_get_op_params_i32(gdn, 0);
117    const size_t  tail_off = ggml_row_size(GGML_TYPE_F32, S_v * H * n_tokens * n_seqs);
118
119    const int64_t D         = S_v * S_v * H;
120    const int64_t n_written = std::min<int64_t>(n_tokens, K);
121
122    const ggml_tensor * src = cpy->src[0]; // gdn snapshot tail view
123    const ggml_tensor * dst = cpy->src[1]; // cache view
124
125    // src must be this gdn's snapshot tail (contiguous, at the tail offset)
126    if (src->op != GGML_OP_VIEW || src->view_src != gdn ||
127        src->view_offs != tail_off || !ggml_is_contiguous(src)) {
128        return false;
129    }
130
131    const int64_t expected_ne[GGML_MAX_DIMS] = { D, n_seqs, n_written, 1 };
132    if (dst->type != GGML_TYPE_F32 ||
133        !std::equal(expected_ne, expected_ne + GGML_MAX_DIMS, dst->ne) ||
134        dst->nb[0] != ggml_type_size(GGML_TYPE_F32) ||
135        dst->nb[1] != ggml_row_size(GGML_TYPE_F32, D)) {
136        return false;
137    }
138
139    if (mode == GGML_METAL_FUSION_FULL) {
140        // the cache must be allocated so the kernel can write straight to its buffer
141        if (dst->data == nullptr) {
142            return false;
143        }
144    }
145
146    return true;
147}
148
149// MUL + SIN + SQR + MUL + ADD (snake activation)
150static bool ggml_metal_fusion_check_snake(
151        const ggml_metal_fusion      * fusion,
152        const ggml_tensor * const    * nodes,
153              ggml_metal_fusion_mode   mode) {
154    GGML_UNUSED(fusion);
155    GGML_UNUSED(mode);
156
157    const ggml_tensor * mul0     = nodes[0];
158    const ggml_tensor * sin_node = nodes[1];
159    const ggml_tensor * sqr      = nodes[2];
160    const ggml_tensor * mul1     = nodes[3];
161    const ggml_tensor * add      = nodes[4];
162
163    // x carries the full activation shape, a is the broadcast operand
164    const ggml_tensor * x = ggml_are_same_shape(mul0, mul0->src[0]) ? mul0->src[0] : mul0->src[1];
165    const ggml_tensor * a = (x == mul0->src[0]) ? mul0->src[1] : mul0->src[0];
166
167    // mul1 reads sqr and inv_b in either operand order
168    const ggml_tensor * inv_b = (mul1->src[0] == sqr) ? mul1->src[1] : mul1->src[0];
169
170    // closure check: the trailing add reads the same x as the leading mul
171    const ggml_tensor * x_in_add = (add->src[0] == mul1) ? add->src[1] : add->src[0];
172
173    // x is in the supported whitelist and every chain intermediate shares x's type.
174    // a and inv_b bind as device const float * in the kernel, so they stay F32.
175    const bool types_ok =
176        (x->type == GGML_TYPE_F32 || x->type == GGML_TYPE_F16 || x->type == GGML_TYPE_BF16) &&
177        (a->type    == GGML_TYPE_F32) && (inv_b->type    == GGML_TYPE_F32) &&
178        (mul0->type == x->type)       && (sin_node->type == x->type) &&
179        (sqr->type  == x->type)       && (mul1->type     == x->type) &&
180        (add->type  == x->type);
181
182    // a / inv_b collapse to [1, C, 1, 1], x and add stay 2D
183    const bool shape_ok = ggml_are_same_shape(a, inv_b) && a->ne[0] == 1 && a->ne[1] == x->ne[1];
184    const bool dim_ok =
185        (x->ne[2]     == 1) && (x->ne[3]     == 1) &&
186        (add->ne[2]   == 1) && (add->ne[3]   == 1) &&
187        (a->ne[2]     == 1) && (a->ne[3]     == 1) &&
188        (inv_b->ne[2] == 1) && (inv_b->ne[3] == 1);
189
190    // kernel reads x[idx] and a[c] / inv_b[c] linearly, so every operand is contiguous
191    const bool contig_ok =
192        ggml_is_contiguous(x) && ggml_is_contiguous(add) &&
193        ggml_is_contiguous(a) && ggml_is_contiguous(inv_b);
194
195    return types_ok && shape_ok && dim_ok && contig_ok && x_in_add == x;
196}
197
198// ---- patterns ------------------------------------------------------------
199
200static const ggml_op ops_norm_mul[]         = { GGML_OP_NORM, GGML_OP_MUL };
201static const ggml_op ops_norm_mul_add[]     = { GGML_OP_NORM, GGML_OP_MUL, GGML_OP_ADD };
202static const ggml_op ops_rms_norm_mul[]     = { GGML_OP_RMS_NORM, GGML_OP_MUL };
203static const ggml_op ops_rms_norm_mul_add[] = { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ADD };
204
205static const ggml_op ops_add_2[] = { GGML_OP_ADD, GGML_OP_ADD };
206static const ggml_op ops_add_3[] = { GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD };
207static const ggml_op ops_add_4[] = { GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD };
208static const ggml_op ops_add_5[] = { GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD };
209static const ggml_op ops_add_6[] = { GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD };
210static const ggml_op ops_add_7[] = { GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD, GGML_OP_ADD };
211static const ggml_op ops_snake[] = { GGML_OP_MUL, GGML_OP_SIN, GGML_OP_SQR, GGML_OP_MUL, GGML_OP_ADD };
212
213static const ggml_op ops_gdn_cache[] = { GGML_OP_GATED_DELTA_NET, GGML_OP_CPY };
214
215static const ggml_metal_fusion ggml_metal_fusions[] = {
216    { GGML_METAL_FUSION_NORM_MUL,     ops_norm_mul,         2, false, ggml_metal_fusion_check_norm },
217    { GGML_METAL_FUSION_NORM_MUL_ADD, ops_norm_mul_add,     3, false, ggml_metal_fusion_check_norm },
218    { GGML_METAL_FUSION_NORM_MUL,     ops_rms_norm_mul,     2, false, ggml_metal_fusion_check_norm },
219    { GGML_METAL_FUSION_NORM_MUL_ADD, ops_rms_norm_mul_add, 3, false, ggml_metal_fusion_check_norm },
220    { GGML_METAL_FUSION_ADD_CHAIN,    ops_add_2,            2, false, ggml_metal_fusion_check_add_chain },
221    { GGML_METAL_FUSION_ADD_CHAIN,    ops_add_3,            3, false, ggml_metal_fusion_check_add_chain },
222    { GGML_METAL_FUSION_ADD_CHAIN,    ops_add_4,            4, false, ggml_metal_fusion_check_add_chain },
223    { GGML_METAL_FUSION_ADD_CHAIN,    ops_add_5,            5, false, ggml_metal_fusion_check_add_chain },
224    { GGML_METAL_FUSION_ADD_CHAIN,    ops_add_6,            6, false, ggml_metal_fusion_check_add_chain },
225    { GGML_METAL_FUSION_ADD_CHAIN,    ops_add_7,            7, false, ggml_metal_fusion_check_add_chain },
226    { GGML_METAL_FUSION_SNAKE,        ops_snake,            5, false, ggml_metal_fusion_check_snake },
227    { GGML_METAL_FUSION_GDN_CACHE,    ops_gdn_cache,        2, true,  ggml_metal_fusion_check_gdn_cache },
228};
229
230const ggml_metal_fusion * ggml_metal_fusion_all(int * n) {
231    *n = (int) sizeof(ggml_metal_fusions) / sizeof(ggml_metal_fusions[0]);
232
233    return ggml_metal_fusions;
234}
235
236// ---- shared fusion info ---------------------------------------------------
237
238static std::string ggml_metal_fusion_label(const ggml_metal_fusion * fusion) {
239    GGML_ASSERT(fusion != nullptr);
240
241    std::string label;
242    for (int j = 0; j < fusion->n_ops; j++) {
243        if (j > 0) {
244            label += '+';
245        }
246        label += ggml_op_name(fusion->ops[j]);
247    }
248    return label;
249}
250
251struct ggml_metal_fusion_info {
252    std::vector<std::string> labels;
253    std::vector<uint64_t>    counts;
254    bool enabled;
255    bool stats;
256    bool labels_set;
257    int  debug;
258};
259
260struct ggml_metal_fusion_info * ggml_metal_fusion_info_init(bool enabled, int debug) {
261    ggml_metal_fusion_info * finfo = new ggml_metal_fusion_info;
262    finfo->enabled    = enabled;
263    finfo->stats      = debug > 0;
264    finfo->labels_set = false;
265    finfo->debug      = debug;
266
267    if (finfo->stats) {
268        ggml_metal_fusion_info_labels_init(finfo);
269    }
270
271    return finfo;
272}
273
274void ggml_metal_fusion_info_free(struct ggml_metal_fusion_info * finfo) {
275    delete finfo;
276}
277
278bool ggml_metal_fusion_info_enabled(const struct ggml_metal_fusion_info * finfo) {
279    return finfo->enabled;
280}
281
282bool ggml_metal_fusion_info_stats(const struct ggml_metal_fusion_info * finfo) {
283    return finfo->stats;
284}
285
286int ggml_metal_fusion_info_debug(const struct ggml_metal_fusion_info * finfo) {
287    return finfo->debug;
288}
289
290int ggml_metal_fusion_info_n_fusions(const struct ggml_metal_fusion_info * finfo) {
291    return (int) finfo->labels.size();
292}
293
294const char * ggml_metal_fusion_info_label(const struct ggml_metal_fusion_info * finfo, int idx) {
295    GGML_ASSERT(idx >= 0 && idx < (int) finfo->labels.size());
296    return finfo->labels[idx].c_str();
297}
298
299uint64_t ggml_metal_fusion_info_count(const struct ggml_metal_fusion_info * finfo, int idx) {
300    GGML_ASSERT(idx >= 0 && idx < (int) finfo->counts.size());
301    return finfo->counts[idx];
302}
303
304void ggml_metal_fusion_info_count_fusion(struct ggml_metal_fusion_info * finfo, const struct ggml_metal_fusion * fusion) {
305    if (!finfo->stats || fusion == nullptr) {
306        return;
307    }
308
309    int n = 0;
310    const ggml_metal_fusion * all = ggml_metal_fusion_all(&n);
311
312    int idx = -1;
313    for (int i = 0; i < n; i++) {
314        if (&all[i] == fusion) {
315            idx = i;
316            break;
317        }
318    }
319
320    if (idx >= 0 && idx < (int) finfo->counts.size()) {
321        finfo->counts[idx]++;
322    }
323}
324
325void ggml_metal_fusion_info_set_enabled(struct ggml_metal_fusion_info * finfo, bool enabled) {
326    finfo->enabled = enabled;
327}
328
329void ggml_metal_fusion_info_labels_init(struct ggml_metal_fusion_info * finfo) {
330    if (finfo->labels_set) {
331        return;
332    }
333
334    int n = 0;
335    const ggml_metal_fusion * all = ggml_metal_fusion_all(&n);
336
337    finfo->labels.clear();
338    finfo->counts.assign(n, 0);
339    finfo->labels.reserve(n);
340
341    for (int i = 0; i < n; i++) {
342        finfo->labels.emplace_back(ggml_metal_fusion_label(&all[i]));
343    }
344
345    finfo->labels_set = true;
346}
347
348void ggml_metal_fusion_info_stats_init(struct ggml_metal_fusion_info * finfo) {
349    finfo->stats = true;
350    ggml_metal_fusion_info_labels_init(finfo);
351}
352
353void ggml_metal_fusion_info_stats_reset(struct ggml_metal_fusion_info * finfo) {
354    std::fill(finfo->counts.begin(), finfo->counts.end(), 0);
355}
356
357int ggml_metal_fusion_info_stats_get(const struct ggml_metal_fusion_info * finfo, const char ** labels, uint64_t * counts, int n) {
358    const int n_fusions = (int) finfo->labels.size();
359
360    if (labels == nullptr) {
361        return n_fusions;
362    }
363
364    const int n_fill = std::min(n, n_fusions);
365    for (int i = 0; i < n_fill; i++) {
366        labels[i] = finfo->labels[i].c_str();
367        if (counts != nullptr) {
368            counts[i] = finfo->counts[i];
369        }
370    }
371
372    return n_fill;
373}
374
375// ---- queries -------------------------------------------------------------
376
377// find the longest pattern matching the node sequence starting at idx
378// (idx is a position in node_idxs, which maps to graph node indices)
379const ggml_metal_fusion * ggml_metal_fusion_next(
380        const ggml_cgraph * gf,
381        const int * node_idxs,
382        int n_idxs,
383        int idx,
384        ggml_metal_fusion_mode mode,
385        int * n_out) {
386    int n = 0;
387    const ggml_metal_fusion * all = ggml_metal_fusion_all(&n);
388
389    const ggml_metal_fusion * res = nullptr;
390    int best = 1;
391
392    for (int i = 0; i < n; i++) {
393        const ggml_metal_fusion * fusion = &all[i];
394
395        // only look for a longer match than the current best
396        if (fusion->n_ops <= best) {
397            continue;
398        }
399        if (idx + fusion->n_ops > n_idxs) {
400            continue;
401        }
402
403        const ggml_tensor * nodes[GGML_METAL_FUSION_MAX];
404
405        // the op sequence must match exactly
406        bool ok = true;
407        for (int j = 0; j < fusion->n_ops; j++) {
408            nodes[j] = gf->nodes[node_idxs[idx + j]];
409            if (nodes[j]->op != fusion->ops[j]) {
410                ok = false;
411                break;
412            }
413        }
414        if (!ok) {
415            continue;
416        }
417
418        if (!fusion->unsafe) {
419            // common element-wise chain constraints: each node reads the previous one,
420            // and all nodes have the same shape
421            for (int j = 1; j < fusion->n_ops && ok; j++) {
422                if (nodes[j]->src[0] != nodes[j - 1] && nodes[j]->src[1] != nodes[j - 1]) {
423                    ok = false;
424                    break;
425                }
426                if (!ggml_are_same_shape(nodes[j], nodes[j - 1])) {
427                    ok = false;
428                    break;
429                }
430            }
431            if (!ok) {
432                continue;
433            }
434
435            // all current fusions are single-output elision chains, so the last node is the only output
436            // TODO: multi-output fusions: store pattern-relative offsets in the table and translate them here
437            int outputs_buf[1];
438            outputs_buf[0] = node_idxs[idx + fusion->n_ops - 1];
439
440            // structural subgraph checks (op sequence, elidable uses, view containment)
441            if (!ggml_can_fuse_subgraph_ext(gf, node_idxs + idx, fusion->n_ops, fusion->ops, outputs_buf, 1)) {
442                continue;
443            }
444        }
445
446        // pattern-specific checks (the sole validator for unsafe patterns)
447        if (fusion->check && !fusion->check(fusion, nodes, mode)) {
448            continue;
449        }
450
451        best = fusion->n_ops;
452        res = fusion;
453    }
454
455    *n_out = best;
456
457    return res;
458}
459
460// optimize phase: maximum number of nodes starting at idx (a raw sequential graph index) that
461// could be fused, chaining patterns back-to-back. matching runs on the same filtered (view
462// transparent) node sequence that the compute phase uses, so the returned count is the raw index
463// span from idx to the last matched node (intermediate views are packed along).
464int ggml_metal_fusion_max(const ggml_cgraph * gf, int idx) {
465    // an empty/view node cannot start a pattern - pack it alone
466    if (ggml_op_is_empty(gf->nodes[idx]->op) || ggml_is_empty(gf->nodes[idx])) {
467        return 1;
468    }
469
470    // collect the non-empty node indices starting at idx
471    int idxs[GGML_METAL_FUSION_MAX];
472    int n_idxs = 0;
473    for (int i = idx; i < gf->n_nodes && n_idxs < GGML_METAL_FUSION_MAX; i++) {
474        if (!ggml_op_is_empty(gf->nodes[i]->op) && !ggml_is_empty(gf->nodes[i])) {
475            idxs[n_idxs++] = i;
476        }
477    }
478    if (n_idxs == 0) {
479        return 1;
480    }
481
482    int total = 0;
483    int i_f = 0;
484
485    while (i_f < n_idxs && total < GGML_METAL_FUSION_MAX) {
486        int len = 1;
487        const ggml_metal_fusion * fusion = ggml_metal_fusion_next(gf, idxs, n_idxs, i_f, GGML_METAL_FUSION_STRUCTURAL, &len);
488        if (!fusion || total + len > GGML_METAL_FUSION_MAX) {
489            break;
490        }
491
492        total += len;
493        i_f += len;
494    }
495
496    if (i_f == 0) {
497        return 1;
498    }
499
500    // map the matched non-empty nodes back to the raw index span (views are included)
501    return std::min(GGML_METAL_FUSION_MAX, idxs[i_f - 1] - idx + 1);
502}