char-fork/llama.cpp

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

Frosty40sycl: add fused top-k MoE (#25217)efb3036c1

char/aliceai-t5-moe
22.5 KiB620 linesraw
1#include <cfloat>
2#include <initializer_list>
3#include <vector>
4
5#include "ggml.h"
6#include "ggml-impl.h"
7#include "ggml-backend-impl.h"
8#include "topk-moe.hpp"
9
10// SYCL port of ggml-cuda/topk-moe.cu. The kernel is a translation of the CUDA no-bias, no-PDL
11// path of topk_moe_cuda; the fusion-detection helpers below are ported near-verbatim from
12// ggml-cuda.cu (pure graph / pointer inspection, backend-agnostic). Bias is not implemented here:
13// if a routing bias is detected, the fusion is declined and the eager path runs unchanged.
14
15struct ggml_sycl_topk_moe_args {
16    bool sigmoid{};
17    bool softmax{};
18    bool delayed_softmax{};
19    bool prob_bias{};
20    bool norm{};
21    bool scale{};
22};
23
24struct topk_moe_config {
25    bool use_sigmoid;
26    bool with_norm;
27    bool delayed_softmax;
28};
29
30// warp-local softmax used for both the pre-top-k logits and the post-top-k delayed path
31template <int experts_per_thread, bool use_limit>
32static inline void softmax_warp_inplace(float (&vals)[experts_per_thread], const int limit, const int lane) {
33    float max_val = -INFINITY;
34#pragma unroll
35    for (int i = 0; i < experts_per_thread; i++) {
36        const int  idx    = lane + i * WARP_SIZE;
37        const bool active = !use_limit || (idx < limit);
38        if (active) {
39            max_val = sycl::fmax(max_val, vals[i]);
40        }
41    }
42    max_val = warp_reduce_max<WARP_SIZE>(max_val);
43
44    float sum = 0.f;
45#pragma unroll
46    for (int i = 0; i < experts_per_thread; i++) {
47        const int  idx    = lane + i * WARP_SIZE;
48        const bool active = !use_limit || (idx < limit);
49        if (active) {
50            const float val = sycl::exp(vals[i] - max_val);
51            vals[i]         = val;
52            sum += val;
53        } else {
54            vals[i] = 0.f;
55        }
56    }
57    sum = warp_reduce_sum<WARP_SIZE>(sum);
58
59    const float inv_sum = 1.0f / sum;
60#pragma unroll
61    for (int i = 0; i < experts_per_thread; i++) {
62        const int idx = lane + i * WARP_SIZE;
63        if (!use_limit || idx < limit) {
64            vals[i] *= inv_sum;
65        }
66    }
67}
68
69template <int experts_per_thread, bool use_limit>
70static inline void sigmoid_warp_inplace(float (&vals)[experts_per_thread], const int limit, const int lane) {
71#pragma unroll
72    for (int i = 0; i < experts_per_thread; i++) {
73        const int  idx    = lane + i * WARP_SIZE;
74        const bool active = !use_limit || (idx < limit);
75        vals[i]           = active ? 1.f / (1.f + sycl::exp(-vals[i])) : -INFINITY;
76    }
77}
78
79/*
80    This kernel does the following:
81    1. optionally softmax/sigmoid over the logits per token [n_experts, n_tokens]
82    2. argmax reduce over the top-k (n_experts_used) logits
83    3. write weights + ids to global memory
84    4. optionally normalize the weights or apply softmax over the selected logits
85
86    It is intended as a fusion of the softmax->top-k->get_rows pipeline for MoE models.
87    One sub-group handles one row/token, mirroring topk_moe_cuda's one-warp-per-row layout.
88*/
89template <int n_experts>
90static void topk_moe_kernel(const float * __restrict__ logits,
91                            float * __restrict__       weights,
92                            int32_t * __restrict__     ids,
93                            const int                  n_rows,
94                            const int                  n_expert_used,
95                            const float                clamp_val,
96                            const float                scale_val,
97                            const topk_moe_config       config) {
98    auto      item_ct1 = sycl::ext::oneapi::this_work_item::get_nd_item<1>();
99    const int row       = item_ct1.get_group(0);
100    if (row >= n_rows) {
101        return;
102    }
103    const int lane = item_ct1.get_local_id(0);
104
105    logits  += (size_t) n_experts * row;
106    weights += (size_t) n_expert_used * row;
107    ids     += (size_t) n_experts * row;  // ids row stride is n_experts (matches the argsort tensor)
108
109    constexpr int experts_per_thread = (n_experts > WARP_SIZE) ? n_experts / WARP_SIZE : 1;
110
111    float wt[experts_per_thread];
112#pragma unroll
113    for (int i = 0; i < experts_per_thread; i++) {
114        wt[i] = -INFINITY;
115    }
116#pragma unroll
117    for (int i = 0; i < n_experts; i += WARP_SIZE) {
118        const int expert  = i + lane;
119        wt[i / WARP_SIZE] = (n_experts % WARP_SIZE == 0 || expert < n_experts) ? logits[expert] : -INFINITY;
120    }
121
122    if (!config.delayed_softmax) {
123        if (config.use_sigmoid) {
124            sigmoid_warp_inplace<experts_per_thread, false>(wt, n_experts, lane);
125        } else {
126            softmax_warp_inplace<experts_per_thread, false>(wt, n_experts, lane);
127        }
128    }
129
130    // Sanitize NaN to -FLT_MAX so the iterative argmax produces unique expert IDs. NaN comparisons
131    // always return false, which would cause the same expert to be selected repeatedly.
132#pragma unroll
133    for (int i = 0; i < experts_per_thread; i++) {
134        if (sycl::isnan(wt[i])) {
135            wt[i] = -FLT_MAX;
136        }
137    }
138
139    // each thread now holds either a portion of the softmax distribution or the raw logits. Do the
140    // argmax reduce over n_expert_used, each time marking the selected expert as -inf to exclude it
141    // from the next iteration.
142
143    float wt_sum = 0.f;
144    float output_weights[experts_per_thread];
145#pragma unroll
146    for (int i = 0; i < experts_per_thread; i++) {
147        output_weights[i] = 0.f;
148    }
149
150    const sycl::sub_group sg = item_ct1.get_sub_group();
151
152    for (int k = 0; k < n_expert_used; k++) {
153        float max_val    = wt[0];
154        int   max_expert = lane;
155#pragma unroll
156        for (int i = 1; i < experts_per_thread; i++) {
157            const int expert = lane + i * WARP_SIZE;
158            if ((n_experts % WARP_SIZE == 0 || expert < n_experts) && wt[i] > max_val) {
159                max_val    = wt[i];
160                max_expert = expert;
161            }
162        }
163#pragma unroll
164        for (int mask = WARP_SIZE / 2; mask > 0; mask >>= 1) {
165            const float val    = dpct::permute_sub_group_by_xor(sg, max_val, mask);
166            const int   expert = dpct::permute_sub_group_by_xor(sg, max_expert, mask);
167            if (val > max_val || (val == max_val && expert < max_expert)) {
168                max_val    = val;
169                max_expert = expert;
170            }
171        }
172
173        if ((max_expert & (WARP_SIZE - 1)) == lane) {
174            wt[max_expert / WARP_SIZE] = -INFINITY;
175        }
176        if ((k & (WARP_SIZE - 1)) == lane) {
177            output_weights[k / WARP_SIZE] = max_val;
178        }
179        if ((max_expert & (WARP_SIZE - 1)) == lane) {
180            ids[k] = max_expert;
181            if (config.with_norm) {
182                wt_sum += max_val;
183            }
184        }
185    }
186
187    if (config.with_norm) {
188        wt_sum          = warp_reduce_sum<WARP_SIZE>(wt_sum);
189        wt_sum          = sycl::fmax(wt_sum, clamp_val);
190        const float inv = 1.0f / wt_sum;
191#pragma unroll
192        for (int i = 0; i < experts_per_thread; i++) {
193            output_weights[i] *= inv;
194        }
195    }
196
197    if (config.delayed_softmax) {
198        softmax_warp_inplace<experts_per_thread, true>(output_weights, n_expert_used, lane);
199    }
200
201#pragma unroll
202    for (int i = 0; i < experts_per_thread; i++) {
203        const int idx = i * WARP_SIZE + lane;
204        if (idx < n_expert_used) {
205            weights[idx] = output_weights[i] * scale_val;
206        }
207    }
208}
209
210template <int n_experts>
211static void launch_topk_moe(queue_ptr stream, const float * logits, float * weights, int32_t * ids, int n_rows,
212                            int n_expert_used, float clamp_val, float scale_val, const topk_moe_config & config) {
213    const sycl::range<1> block_dims(WARP_SIZE);
214    const sycl::range<1> block_nums(n_rows);
215    stream->parallel_for(sycl::nd_range<1>(block_nums * block_dims, block_dims),
216                         [=](sycl::nd_item<1> item_ct1) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
217                             topk_moe_kernel<n_experts>(logits, weights, ids, n_rows, n_expert_used, clamp_val,
218                                                        scale_val, config);
219                             GGML_UNUSED(item_ct1);
220                         });
221}
222
223static void ggml_sycl_op_topk_moe(ggml_backend_sycl_context &     ctx,
224                                  const ggml_tensor *             logits,
225                                  ggml_tensor *                   weights,
226                                  ggml_tensor *                   ids,
227                                  const ggml_tensor *             clamp,
228                                  const ggml_tensor *             scale,
229                                  const ggml_sycl_topk_moe_args & args) {
230    GGML_ASSERT(logits->type  == GGML_TYPE_F32);
231    GGML_ASSERT(weights->type == GGML_TYPE_F32);
232    GGML_ASSERT(ids->type     == GGML_TYPE_I32);
233
234    const int n_experts     = logits->ne[0];
235    const int n_rows        = logits->ne[1];
236    const int n_expert_used = weights->ne[1];
237
238    GGML_ASSERT(ids->nb[1] / ggml_type_size(ids->type) == (size_t) n_experts);
239
240    const float * logits_d  = (const float *) logits->data;
241    float *       weights_d = (float *) weights->data;
242    int32_t *     ids_d     = (int32_t *) ids->data;
243
244    const bool  with_norm = clamp != nullptr;
245    const float clamp_val = clamp ? ggml_get_op_params_f32(clamp, 0) : -INFINITY;
246    const float scale_val = scale ? ggml_get_op_params_f32(scale, 0) : 1.0f;
247
248    topk_moe_config config;
249    config.use_sigmoid     = args.sigmoid;
250    config.with_norm       = with_norm;
251    config.delayed_softmax = args.delayed_softmax;
252
253    queue_ptr stream = ctx.stream();
254    ggml_sycl_set_device(ctx.device);
255
256    switch (n_experts) {
257        case 1:
258            launch_topk_moe<1>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
259                               config);
260            break;
261        case 2:
262            launch_topk_moe<2>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
263                               config);
264            break;
265        case 4:
266            launch_topk_moe<4>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
267                               config);
268            break;
269        case 8:
270            launch_topk_moe<8>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
271                               config);
272            break;
273        case 16:
274            launch_topk_moe<16>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
275                                config);
276            break;
277        case 32:
278            launch_topk_moe<32>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
279                                config);
280            break;
281        case 64:
282            launch_topk_moe<64>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
283                                config);
284            break;
285        case 128:
286            launch_topk_moe<128>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
287                                 config);
288            break;
289        case 256:
290            launch_topk_moe<256>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
291                                 config);
292            break;
293        case 512:
294            launch_topk_moe<512>(stream, logits_d, weights_d, ids_d, n_rows, n_expert_used, clamp_val, scale_val,
295                                 config);
296            break;
297        default:
298            GGML_ASSERT(false && "fatal error");
299            break;
300    }
301}
302
303static bool ggml_sycl_should_use_topk_moe(const ggml_tensor * gating_op, const ggml_tensor * weights,
304                                          const ggml_tensor * logits, const ggml_tensor * ids) {
305    const int n_expert = ids->nb[1] / ids->nb[0];
306    if ((n_expert & (n_expert - 1)) != 0 || n_expert > 512) {
307        return false;
308    }
309
310    if (!ggml_is_contiguous(weights) || !ggml_is_contiguous(logits)) {
311        return false;
312    }
313
314    if (gating_op->op == GGML_OP_SOFT_MAX) {
315        float scale    = 1.0f;
316        float max_bias = 0.0f;
317
318        memcpy(&scale, (const float *) gating_op->op_params + 0, sizeof(float));
319        memcpy(&max_bias, (const float *) gating_op->op_params + 1, sizeof(float));
320
321        if (!ggml_is_contiguous(gating_op->src[0])) {
322            return false;
323        }
324        if (scale != 1.0f || max_bias != 0.0f) {
325            return false;
326        }
327        // don't fuse when masks or sinks are present
328        if (gating_op->src[1] || gating_op->src[2]) {
329            return false;
330        }
331    } else if (gating_op->op == GGML_OP_UNARY) {
332        if (ggml_get_unary_op(gating_op) != GGML_UNARY_OP_SIGMOID) {
333            return false;
334        }
335    }
336
337    return true;
338}
339
340// ported from ggml_cuda_topk_moe_fusion - pure graph inspection, backend-agnostic
341static bool ggml_sycl_topk_moe_fusion(const ggml_cgraph * cgraph, int node_idx, ggml_sycl_topk_moe_args & args) {
342    args = ggml_sycl_topk_moe_args{};
343
344    const int      n_nodes = cgraph->n_nodes;
345    ggml_tensor ** nodes   = cgraph->nodes;
346
347    if (nodes[node_idx]->op == GGML_OP_SOFT_MAX) {
348        args.softmax = true;
349    }
350
351    if (nodes[node_idx]->op == GGML_OP_UNARY) {
352        if (ggml_get_unary_op(nodes[node_idx]) != GGML_UNARY_OP_SIGMOID) {
353            return false;
354        }
355        args.sigmoid = true;
356    }
357
358    if (nodes[node_idx]->op == GGML_OP_ARGSORT) {
359        args.delayed_softmax = true;
360    }
361
362    node_idx++;
363
364    if (args.sigmoid || args.softmax) {
365        // SOFTMAX -> RESHAPE
366        if (node_idx >= n_nodes || nodes[node_idx]->op != GGML_OP_RESHAPE ||
367            nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
368            return false;
369        }
370        ggml_tensor * probs_reshaped = nodes[node_idx];
371        node_idx++;
372
373        if (node_idx >= n_nodes) {
374            return false;
375        }
376
377        // src of bias add is the unreshaped probs (-2 instead of -1)
378        if (nodes[node_idx]->op == GGML_OP_ADD && nodes[node_idx]->src[0] == nodes[node_idx - 2]) {
379            args.prob_bias = true;
380            node_idx++;
381        }
382        // RESHAPE/ADD -> ARGSORT
383        if (node_idx >= n_nodes || nodes[node_idx]->op != GGML_OP_ARGSORT) {
384            return false;
385        }
386
387        if (args.prob_bias && nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
388            return false;
389        } else if (!args.prob_bias && nodes[node_idx]->src[0] != nodes[node_idx - 2]) {
390            return false;
391        }
392
393        node_idx++;
394
395        // ARGSORT -> VIEW
396        if (node_idx >= n_nodes || nodes[node_idx]->op != GGML_OP_VIEW ||
397            nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
398            return false;
399        }
400        node_idx++;
401
402        if (node_idx >= n_nodes || nodes[node_idx]->op != GGML_OP_GET_ROWS) {
403            return false;
404        }
405
406        // GET_ROWS
407        if (nodes[node_idx]->src[0] != probs_reshaped || nodes[node_idx]->src[1] != nodes[node_idx - 1]) {
408            return false;
409        }
410        node_idx++;
411    } else if (args.delayed_softmax) {
412        if (node_idx - 2 < 0) {
413            return false;
414        }
415        ggml_tensor * probs_reshaped = nodes[node_idx - 2];
416
417        // VIEW -> ARGSORT
418        if (node_idx >= n_nodes || nodes[node_idx]->op != GGML_OP_VIEW ||
419            nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
420            return false;
421        }
422        node_idx++;
423
424        // GET_ROWS
425        if (node_idx >= n_nodes || nodes[node_idx]->src[1] != nodes[node_idx - 1] ||
426            nodes[node_idx]->src[0] != probs_reshaped) {
427            return false;
428        }
429        node_idx++;
430
431        static const std::vector<ggml_op> remaining_ops = { GGML_OP_RESHAPE, GGML_OP_SOFT_MAX, GGML_OP_RESHAPE };
432
433        for (const ggml_op op : remaining_ops) {
434            if (node_idx >= n_nodes || nodes[node_idx]->op != op || nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
435                return false;
436            }
437            node_idx++;
438        }
439    }
440
441    // at this point we can check for norm + scale; everything is now at least valid up to the norm
442    if (node_idx >= n_nodes) {
443        return true;
444    }
445
446    if (nodes[node_idx]->op == GGML_OP_RESHAPE) {
447        // check RESHAPE -> SUM_ROWS -> CLAMP -> DIV -> RESHAPE
448        static const std::vector<ggml_op> norm_ops = { GGML_OP_RESHAPE, GGML_OP_SUM_ROWS, GGML_OP_CLAMP };
449
450        args.norm = true;
451        for (const ggml_op op : norm_ops) {
452            if (nodes[node_idx]->op == op && nodes[node_idx]->src[0] == nodes[node_idx - 1]) {
453                node_idx++;
454            } else {
455                args.norm = false;
456                return true;
457            }
458        }
459
460        // DIV <- CLAMP, RESHAPE
461        if (nodes[node_idx]->op != GGML_OP_DIV || nodes[node_idx]->src[1] != nodes[node_idx - 1] ||
462            nodes[node_idx]->src[0] != nodes[node_idx - 3]) {
463            args.norm = false;
464            return true;
465        }
466        node_idx++;
467
468        if (nodes[node_idx]->op != GGML_OP_RESHAPE || nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
469            args.norm = false;
470            return true;
471        }
472        node_idx++;
473    }
474
475    if (nodes[node_idx]->op == GGML_OP_SCALE && nodes[node_idx]->src[0] == nodes[node_idx - 1]) {
476        args.scale = true;
477    }
478
479    return true;
480}
481
482// returns whether the write (out) nodes overwrite the read nodes in operation
483// ported from ggml_cuda_check_fusion_memory_ranges - pure pointer/range inspection
484static bool ggml_sycl_check_fusion_memory_ranges(const ggml_cgraph * cgraph, const int node_idx,
485                                                 const int node_count, const int * out_nodes, const int out_count,
486                                                 const bool is_topk_moe = false) {
487    auto nodes_overlap = [&](const ggml_tensor * a, const ggml_tensor * b) {
488        const int64_t a_start = (int64_t) a->data;
489        const int64_t a_end   = a_start + ggml_backend_buft_get_alloc_size(a->buffer->buft, a);
490
491        const int64_t b_start = (int64_t) b->data;
492        const int64_t b_end   = b_start + ggml_backend_buft_get_alloc_size(b->buffer->buft, b);
493
494        if ((b_start <= a_start && a_start < b_end) || (a_start <= b_start && b_start < a_end)) {
495            return true;
496        }
497
498        return false;
499    };
500
501    bool is_ok = true;
502    // exception for topk-moe, as each row is read entirely before writing
503    if (ggml_nrows(cgraph->nodes[node_idx]) == 1 && is_topk_moe) {
504        return true;
505    }
506
507    for (int i = 0; i < out_count; ++i) {
508        const ggml_tensor * dst = cgraph->nodes[out_nodes[i]];
509
510        for (int j = node_idx; j < node_idx + node_count; ++j) {
511            // loop over all srcs of all nodes in the fusion. If the src overlaps the destination and
512            // the src is not an intermediate node that's being elided, then disable fusion.
513            for (int src_idx = 0; src_idx < GGML_MAX_SRC; ++src_idx) {
514                const ggml_tensor * src = cgraph->nodes[j]->src[src_idx];
515
516                if (!src || src->op == GGML_OP_NONE) {
517                    continue;
518                }
519
520                if (nodes_overlap(dst, src)) {
521                    bool found = false;
522
523                    for (int k = node_idx; k < j; ++k) {
524                        if (cgraph->nodes[k] == src) {
525                            found = true;
526                            break;
527                        }
528                    }
529
530                    if (!found) {
531                        is_ok = false;
532                        break;
533                    }
534                }
535            }
536        }
537    }
538
539    return is_ok;
540}
541
542int ggml_sycl_fuse(ggml_backend_sycl_context & ctx, ggml_cgraph * cgraph, int i) {
543    if (!g_ggml_sycl_enable_fusion) {
544        return 0;
545    }
546
547    return ggml_sycl_fuse_topk_moe(ctx, cgraph, i);
548}
549
550int ggml_sycl_fuse_topk_moe(ggml_backend_sycl_context & ctx, ggml_cgraph * cgraph, int i) {
551    ggml_tensor * node = cgraph->nodes[i];
552
553    if (node->op != GGML_OP_UNARY && node->op != GGML_OP_SOFT_MAX && node->op != GGML_OP_ARGSORT) {
554        return 0;
555    }
556
557    ggml_sycl_topk_moe_args args;
558    if (!ggml_sycl_topk_moe_fusion(cgraph, i, args)) {
559        return 0;
560    }
561
562    // this kernel implements the no-bias path only; decline anything with a routing bias
563    if (args.prob_bias) {
564        return 0;
565    }
566
567    const ggml_tensor * logits  = node->src[0];
568    ggml_tensor *       weights = nullptr;
569    ggml_tensor *       ids     = nullptr;
570    const ggml_tensor * clamp   = nullptr;
571    const ggml_tensor * scale   = nullptr;
572
573    std::vector<ggml_op> ops;
574    int                  out_nodes[2];
575
576    if (!args.delayed_softmax) {
577        const ggml_op gating_op = args.sigmoid ? GGML_OP_UNARY : GGML_OP_SOFT_MAX;
578        ops.insert(ops.end(), { gating_op, GGML_OP_RESHAPE, GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS });
579        out_nodes[0] = i + 3;
580        ids          = cgraph->nodes[i + 3];
581
582        if (args.norm) {
583            ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_SUM_ROWS, GGML_OP_CLAMP, GGML_OP_DIV, GGML_OP_RESHAPE });
584            clamp = cgraph->nodes[i + (int) ops.size() - 3];
585        }
586        if (args.scale) {
587            ops.insert(ops.end(), { GGML_OP_SCALE });
588            scale = cgraph->nodes[i + (int) ops.size() - 1];
589        }
590
591        weights      = cgraph->nodes[i + (int) ops.size() - 1];
592        out_nodes[1] = i + (int) ops.size() - 1;
593
594        if (ggml_can_fuse_subgraph(cgraph, i, ops.size(), ops.data(), out_nodes, 2) &&
595            ggml_sycl_should_use_topk_moe(node, weights, logits, ids) &&
596            ggml_sycl_check_fusion_memory_ranges(cgraph, i, (int) ops.size(), out_nodes, 2, /*is_topk_moe=*/true)) {
597            ggml_sycl_op_topk_moe(ctx, logits, weights, ids, clamp, scale, args);
598            return (int) ops.size() - 1;
599        }
600    } else if (!args.norm && !args.prob_bias) {
601        // gpt-oss style: argsort -> view -> get_rows -> reshape -> softmax -> reshape, no norm/bias
602        ops.insert(ops.end(),
603                   { GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS, GGML_OP_RESHAPE, GGML_OP_SOFT_MAX,
604                     GGML_OP_RESHAPE });
605        weights                     = cgraph->nodes[i + 5];
606        ids                         = cgraph->nodes[i + 1];
607        const ggml_tensor * softmax = cgraph->nodes[i + 4];
608        out_nodes[0]                = i + 1;
609        out_nodes[1]                = i + 5;
610
611        if (ggml_can_fuse_subgraph(cgraph, i, ops.size(), ops.data(), out_nodes, 2) &&
612            ggml_sycl_should_use_topk_moe(softmax, weights, logits, ids) &&
613            ggml_sycl_check_fusion_memory_ranges(cgraph, i, (int) ops.size(), out_nodes, 2, /*is_topk_moe=*/true)) {
614            ggml_sycl_op_topk_moe(ctx, logits, weights, ids, clamp, scale, args);
615            return (int) ops.size() - 1;
616        }
617    }
618
619    return 0;
620}