char-fork/llama.cpp

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

cwritersycl: rfc: Use radix select for top_k (#28670)21f6b0d22

char/aliceai-t5-moe
23.0 KiB531 linesraw
1#include "topk-radix.hpp"
2
3#include "common.hpp"
4
5#include <algorithm>
6
7// Large-k top-k by radix select on an order-preserving unsigned key.
8//
9// The k-th largest key of a row is found by four most-significant-first passes over its
10// 8-bit digits: histogram the digit over the candidate set, walk the buckets from the
11// top, and recurse into the bucket where the running count reaches what is still
12// needed. Everything strictly above that bucket is in the top-k. A final pass emits
13// every column whose key beats the pivot, then exactly as many pivot-equal columns as
14// are still missing, so duplicate keys yield exactly k distinct indices.
15//
16// SLM holds only the histogram, so unlike the scan-merge kernels the cost does not grow
17// with k. One work-group owns a row and runs every pass, so a top-k is one launch and
18// needs no pool scratch. The row is re-read once per pass rather than compacted, which
19// keeps the candidate set implicit: (key & mask) == prefix.
20//
21// The output is the set of winning indices in no particular order, which is what the
22// reference op provides (it swaps its first two outputs to say so) and what
23// test-backend-ops compares.
24
25static constexpr int SYCL_TOP_K_RADIX_BITS       = 8;
26static constexpr int SYCL_TOP_K_RADIX_BUCKETS    = 1 << SYCL_TOP_K_RADIX_BITS;
27// Private histogram copies, interleaved per bucket so neighbouring lanes hit
28// neighbouring banks. Lanes of one instruction spread over the copies, which is what
29// bounds the atomic serialisation on tie-heavy rows.
30static constexpr int SYCL_TOP_K_RADIX_HIST_COPIES = 8;
31static constexpr int SYCL_TOP_K_RADIX_HIST_SIZE   = SYCL_TOP_K_RADIX_BUCKETS * SYCL_TOP_K_RADIX_HIST_COPIES;
32// Past the histogram: pivot digit, pivot bucket count, remaining need, then the two
33// emit counters.
34static constexpr int SYCL_TOP_K_RADIX_SLM_WORDS   = SYCL_TOP_K_RADIX_HIST_SIZE + 5;
35
36// Larger float <=> larger key. The reference comparator is a plain float '>', under which
37// -0.0 and +0.0 tie, so -0.0 is folded onto +0.0 first. NaN has no defined order in the
38// reference (its comparator is not a strict weak order on NaN); here a positive NaN keys
39// above +inf and a negative NaN below -inf, which at least makes the result deterministic.
40static inline uint32_t top_k_radix_key(float f) {
41    uint32_t u = sycl::bit_cast<uint32_t>(f);
42    if (u == 0x80000000u) {
43        u = 0u;
44    }
45    return (u & 0x80000000u) ? ~u : (u | 0x80000000u);
46}
47
48static void top_k_radix_select_f32(
49    const float *   src,
50    int32_t *       dst_idx,
51    const int       ncols,
52    const int       k,
53    uint32_t *      slm,
54    const sycl::nd_item<1> & item_ct1
55) {
56    using local_atomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed,
57                                          sycl::memory_scope::work_group,
58                                          sycl::access::address_space::local_space>;
59
60    const int tid        = item_ct1.get_local_id(0);
61    const int block_size = item_ct1.get_local_range(0);
62
63    uint32_t * hist     = slm;
64    uint32_t * s_digit  = slm + SYCL_TOP_K_RADIX_HIST_SIZE;
65    uint32_t * s_bucket = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 1;
66    uint32_t * s_need   = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 2;
67    uint32_t * s_cnt_gt = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 3;
68    uint32_t * s_cnt_eq = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 4;
69
70    if (tid == 0) {
71        *s_cnt_gt = 0;
72        *s_cnt_eq = 0;
73    }
74
75    const int copy = tid & (SYCL_TOP_K_RADIX_HIST_COPIES - 1);
76
77    uint32_t prefix = 0;   // digits fixed so far, in place
78    uint32_t mask   = 0;   // which bits of prefix are fixed
79    uint32_t need   = (uint32_t) k;
80
81    for (int shift = 32 - SYCL_TOP_K_RADIX_BITS; shift >= 0; shift -= SYCL_TOP_K_RADIX_BITS) {
82        for (int i = tid; i < SYCL_TOP_K_RADIX_HIST_SIZE; i += block_size) {
83            hist[i] = 0;
84        }
85        item_ct1.barrier(sycl::access::fence_space::local_space);
86
87        for (int col = tid; col < ncols; col += block_size) {
88            const uint32_t key = top_k_radix_key(src[col]);
89            if ((key & mask) == prefix) {
90                const uint32_t bucket = (key >> shift) & (SYCL_TOP_K_RADIX_BUCKETS - 1);
91                local_atomic(hist[bucket * SYCL_TOP_K_RADIX_HIST_COPIES + copy]).fetch_add(1u);
92            }
93        }
94        item_ct1.barrier(sycl::access::fence_space::local_space);
95
96        // Lane t takes bucket 255 - t, so an inclusive scan over lanes counts from the top
97        // bucket downward. The pivot is the unique bucket whose cumulative count first
98        // reaches need; the previous cumulative count is what the higher buckets contribute.
99        uint32_t cnt = 0;
100        if (tid < SYCL_TOP_K_RADIX_BUCKETS) {
101            const uint32_t * h = hist + (SYCL_TOP_K_RADIX_BUCKETS - 1 - tid) * SYCL_TOP_K_RADIX_HIST_COPIES;
102            for (int c = 0; c < SYCL_TOP_K_RADIX_HIST_COPIES; c++) {
103                cnt += h[c];
104            }
105        }
106        const uint32_t incl = sycl::inclusive_scan_over_group(item_ct1.get_group(), cnt, sycl::plus<uint32_t>());
107
108        if (tid < SYCL_TOP_K_RADIX_BUCKETS && incl >= need && incl - cnt < need) {
109            *s_digit = (uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1 - tid);
110            *s_bucket = cnt;
111            *s_need   = need - (incl - cnt);
112        }
113        item_ct1.barrier(sycl::access::fence_space::local_space);
114
115        const uint32_t digit      = *s_digit;
116        const uint32_t bucket_cnt = *s_bucket;
117        need   = *s_need;
118        prefix |= digit << shift;
119        mask   |= (uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1) << shift;
120
121        // Every candidate in the pivot bucket is wanted: the remaining digits cannot
122        // change the answer, and the masked emit below is exact as it stands.
123        if (bucket_cnt == need) {
124            break;
125        }
126        // The next pass rewrites hist and s_*; the reads above must land first.
127        item_ct1.barrier(sycl::access::fence_space::local_space);
128    }
129
130    item_ct1.barrier(sycl::access::fence_space::local_space);
131
132    // Exactly k - need columns have (key & mask) > prefix; the first need of the pivot-equal
133    // columns fill the tail. Both counters live in SLM since the whole row is this group.
134    const uint32_t base_eq = (uint32_t) k - need;
135
136    for (int col = tid; col < ncols; col += block_size) {
137        const uint32_t kp = top_k_radix_key(src[col]) & mask;
138        if (kp > prefix) {
139            const uint32_t pos = local_atomic(*s_cnt_gt).fetch_add(1u);
140            dst_idx[pos] = col;
141        } else if (kp == prefix) {
142            const uint32_t pos = local_atomic(*s_cnt_eq).fetch_add(1u);
143            if (pos < need) {
144                dst_idx[base_eq + pos] = col;
145            }
146        }
147    }
148}
149
150static void top_k_radix_f32_sycl(
151    ggml_backend_sycl_context & ctx,
152    const float * src,
153    int32_t * dst_indices,
154    const int64_t ncols,
155    const int64_t nrows,
156    const int k,
157    dpct::queue_ptr main_stream
158) {
159    GGML_ASSERT(ncols <= INT32_MAX);
160
161    // One group per row; every pass is a strided sweep of the row, so lanes in flight is the
162    // only lever, and the device's own limit is the answer -- there is nothing here that
163    // wants a smaller group. Must still cover the 256 buckets for the scan step.
164    const int block_size = ggml_sycl_info().max_work_group_sizes[ctx.device];
165    GGML_ASSERT(block_size >= SYCL_TOP_K_RADIX_BUCKETS);
166
167    const sycl::range<1> block_dims(block_size);
168    const sycl::range<1> grid_dims(nrows);
169
170    main_stream->submit([&](sycl::handler &cgh) {
171        sycl::local_accessor<uint32_t, 1> slm(sycl::range<1>(SYCL_TOP_K_RADIX_SLM_WORDS), cgh);
172
173        cgh.parallel_for(
174            sycl::nd_range<1>(grid_dims * block_dims, block_dims),
175            [=](sycl::nd_item<1> item_ct1) {
176                const int row = item_ct1.get_group(0);
177
178                top_k_radix_select_f32(
179                    src + (int64_t) row * ncols, dst_indices + (int64_t) row * k,
180                    (int) ncols, k,
181                    slm.get_multi_ptr<sycl::access::decorated::no>().get(),
182                    item_ct1);
183            });
184    });
185}
186
187// One work-group owns a whole row above, which leaves the device idle whenever a graph
188// has fewer rows than it has cores -- the common case at batch size 1, where the
189// sparse-attention indexer and the backend sampler both top-k a single row. The kernels
190// below spread one row over several groups instead.
191//
192// A digit pass now needs the whole row's histogram before any group can pick the pivot,
193// so the per-pass state moves to global memory and the passes become separate launches:
194// a work-group barrier no longer spans the row. Each group still accumulates into SLM
195// and contributes 256 global atomics at the end, so global traffic is per-group, not
196// per-element. The last group to finish a pass (the one whose fetch_add returns G - 1)
197// does the scan for the row and clears the histogram for the next pass, which keeps the
198// launch count at one per digit rather than two.
199//
200// Running all four digits unconditionally costs nothing in correctness: once a bucket
201// holds exactly the elements still needed, later digits only extend the prefix, and the
202// count of columns above that longer prefix grows by exactly as much as `need` shrinks.
203// The emit below therefore stays exact whatever pass the answer settled on.
204
205static constexpr int SYCL_TOP_K_RADIX_ROW_DONE   = SYCL_TOP_K_RADIX_BUCKETS + 0;
206static constexpr int SYCL_TOP_K_RADIX_ROW_PREFIX = SYCL_TOP_K_RADIX_BUCKETS + 1;
207static constexpr int SYCL_TOP_K_RADIX_ROW_MASK   = SYCL_TOP_K_RADIX_BUCKETS + 2;
208static constexpr int SYCL_TOP_K_RADIX_ROW_NEED   = SYCL_TOP_K_RADIX_BUCKETS + 3;
209static constexpr int SYCL_TOP_K_RADIX_ROW_CNT_GT = SYCL_TOP_K_RADIX_BUCKETS + 4;
210static constexpr int SYCL_TOP_K_RADIX_ROW_CNT_EQ = SYCL_TOP_K_RADIX_BUCKETS + 5;
211static constexpr int SYCL_TOP_K_RADIX_ROW_WORDS  = SYCL_TOP_K_RADIX_BUCKETS + 6;
212
213// How wide the split goes is a property of the device, not of the model: enough groups to
214// cover the cores, and no more. Past that the extra groups add histogram traffic without
215// adding bandwidth (measured on this device: 20 and 40 groups tie, 60 and 160 lose).
216//
217// nsm is max_compute_units / 16, i.e. it counts an Xe core as 16 EUs. That is a core's
218// width on Xe-HPG, but an Xe2 core is 8 XVEs wide, so on Battlemage the field reads half
219// the cores actually present (10 for a 20-core B60). The measured curve is flat from one
220// group per core to two and only falls off at three, so a factor of two covers the device
221// on Xe2 and lands in the flat region on Xe-HPG. It is the one number here that a correct
222// core count would remove; it was tuned on Xe2 and has not been measured on Xe-HPG.
223static constexpr int SYCL_TOP_K_RADIX_GROUPS_PER_NSM = 2;
224// Splitting trades one kernel for five. Below the width at which the single-group kernel
225// runs longer than those four extra launches, it wins on its own; measured break-even on
226// this device sits just under 64K columns.
227static constexpr int SYCL_TOP_K_RADIX_MIN_SPLIT_COLS = 65536;
228// A partition thinner than this cannot keep a group's sweep busy.
229static constexpr int SYCL_TOP_K_RADIX_MIN_PART_COLS  = 4096;
230
231static int top_k_radix_split_groups(const int device, const int64_t ncols, const int64_t nrows) {
232    const int64_t target = (int64_t) SYCL_TOP_K_RADIX_GROUPS_PER_NSM * ggml_sycl_info().devices[device].nsm;
233
234    // One group per row already, so a graph with rows enough to cover the device gains
235    // nothing from splitting and would only pay the extra launches.
236    if (ncols < SYCL_TOP_K_RADIX_MIN_SPLIT_COLS || nrows >= target) {
237        return 1;
238    }
239
240    const int64_t by_rows = target / nrows;   // floor: never overshoot a row that is nearly covered
241    const int64_t by_cols = ncols / SYCL_TOP_K_RADIX_MIN_PART_COLS;
242
243    return (int) std::max<int64_t>(1, std::min(by_rows, by_cols));
244}
245
246using top_k_radix_gatomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed,
247                                             sycl::memory_scope::device,
248                                             sycl::access::address_space::global_space>;
249
250static void top_k_radix_split_pass_f32(
251    const float *   src,
252    uint32_t *      state,
253    const int       ncols,
254    const int       k,
255    const int       shift,
256    const bool      first,
257    const int       part,
258    const int       nparts,
259    uint32_t *      slm,
260    const sycl::nd_item<1> & item_ct1
261) {
262    using local_atomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed,
263                                          sycl::memory_scope::work_group,
264                                          sycl::access::address_space::local_space>;
265
266    const int tid        = item_ct1.get_local_id(0);
267    const int block_size = item_ct1.get_local_range(0);
268
269    uint32_t * hist   = slm;
270    uint32_t * s_last = slm + SYCL_TOP_K_RADIX_HIST_SIZE;
271    uint32_t * s_row  = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 1;   // prefix, mask, need
272
273    // The previous launch is the barrier that publishes these, so a plain load is enough.
274    // One lane reads them and the group takes them from SLM: a device-scope atomic load
275    // is uncached here, and having every work-item issue three of them off the same
276    // address costs more than the whole sweep below.
277    if (tid == 0) {
278        s_row[0] = first ? 0u : state[SYCL_TOP_K_RADIX_ROW_PREFIX];
279        s_row[1] = first ? 0u : state[SYCL_TOP_K_RADIX_ROW_MASK];
280        s_row[2] = first ? (uint32_t) k : state[SYCL_TOP_K_RADIX_ROW_NEED];
281    }
282
283    for (int i = tid; i < SYCL_TOP_K_RADIX_HIST_SIZE; i += block_size) {
284        hist[i] = 0;
285    }
286    item_ct1.barrier(sycl::access::fence_space::local_space);
287
288    const uint32_t prefix = s_row[0];
289    const uint32_t mask   = s_row[1];
290    const uint32_t need   = s_row[2];
291
292    const int copy  = tid & (SYCL_TOP_K_RADIX_HIST_COPIES - 1);
293    const int chunk = (ncols + nparts - 1) / nparts;
294    const int col0  = part * chunk;
295    const int col1  = std::min(ncols, col0 + chunk);
296
297    for (int col = col0 + tid; col < col1; col += block_size) {
298        const uint32_t key = top_k_radix_key(src[col]);
299        if ((key & mask) == prefix) {
300            const uint32_t bucket = (key >> shift) & (SYCL_TOP_K_RADIX_BUCKETS - 1);
301            local_atomic(hist[bucket * SYCL_TOP_K_RADIX_HIST_COPIES + copy]).fetch_add(1u);
302        }
303    }
304    item_ct1.barrier(sycl::access::fence_space::local_space);
305
306    // One global atomic per bucket per group, not per element.
307    for (int b = tid; b < SYCL_TOP_K_RADIX_BUCKETS; b += block_size) {
308        uint32_t sum = 0;
309        for (int c = 0; c < SYCL_TOP_K_RADIX_HIST_COPIES; c++) {
310            sum += hist[b * SYCL_TOP_K_RADIX_HIST_COPIES + c];
311        }
312        if (sum) {
313            top_k_radix_gatomic(state[b]).fetch_add(sum);
314        }
315    }
316
317    // Publish this group's bins, then claim the scan if this group is the row's last.
318    // The group-wide barrier flushes the atomics above; only the claiming lane needs the
319    // release, so the device-scope fence is paid once per group rather than per work-item.
320    item_ct1.barrier(sycl::access::fence_space::global_and_local);
321    if (tid == 0) {
322        sycl::atomic_fence(sycl::memory_order::release, sycl::memory_scope::device);
323        sycl::atomic_ref<uint32_t, sycl::memory_order::acq_rel, sycl::memory_scope::device,
324                         sycl::access::address_space::global_space> done(state[SYCL_TOP_K_RADIX_ROW_DONE]);
325        *s_last = (done.fetch_add(1u) == (uint32_t) (nparts - 1)) ? 1u : 0u;
326    }
327    item_ct1.barrier(sycl::access::fence_space::local_space);
328
329    if (*s_last == 0u) {
330        return;
331    }
332    sycl::atomic_fence(sycl::memory_order::acquire, sycl::memory_scope::device);
333
334    // Lane t takes bucket 255 - t, so an inclusive scan counts down from the top bucket.
335    uint32_t cnt = 0;
336    if (tid < SYCL_TOP_K_RADIX_BUCKETS) {
337        cnt = top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_BUCKETS - 1 - tid]).load();
338    }
339    const uint32_t incl = sycl::inclusive_scan_over_group(item_ct1.get_group(), cnt, sycl::plus<uint32_t>());
340
341    if (tid < SYCL_TOP_K_RADIX_BUCKETS && incl >= need && incl - cnt < need) {
342        const uint32_t digit = (uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1 - tid);
343        top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_PREFIX]).store(prefix | (digit << shift));
344        top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_MASK]).store(
345            mask | ((uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1) << shift));
346        top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_NEED]).store(need - (incl - cnt));
347    }
348    item_ct1.barrier(sycl::access::fence_space::local_space);
349
350    // Clear for the next pass; the next launch is the barrier that orders this.
351    for (int b = tid; b < SYCL_TOP_K_RADIX_BUCKETS; b += block_size) {
352        top_k_radix_gatomic(state[b]).store(0u);
353    }
354    if (tid == 0) {
355        top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_DONE]).store(0u);
356    }
357}
358
359static void top_k_radix_split_emit_f32(
360    const float *   src,
361    int32_t *       dst_idx,
362    uint32_t *      state,
363    const int       ncols,
364    const int       k,
365    const int       part,
366    const int       nparts,
367    uint32_t *      slm,
368    const sycl::nd_item<1> & item_ct1
369) {
370    using local_atomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed,
371                                          sycl::memory_scope::work_group,
372                                          sycl::access::address_space::local_space>;
373
374    const int tid        = item_ct1.get_local_id(0);
375    const int block_size = item_ct1.get_local_range(0);
376
377    uint32_t * s_gt      = slm;
378    uint32_t * s_eq      = slm + 1;
379    uint32_t * s_base_gt = slm + 2;
380    uint32_t * s_base_eq = slm + 3;
381
382    uint32_t * s_row = slm + 4;   // prefix, mask, need
383
384    if (tid == 0) {
385        *s_gt = 0;
386        *s_eq = 0;
387        s_row[0] = state[SYCL_TOP_K_RADIX_ROW_PREFIX];
388        s_row[1] = state[SYCL_TOP_K_RADIX_ROW_MASK];
389        s_row[2] = state[SYCL_TOP_K_RADIX_ROW_NEED];
390    }
391    item_ct1.barrier(sycl::access::fence_space::local_space);
392
393    const uint32_t prefix = s_row[0];
394    const uint32_t mask   = s_row[1];
395    const uint32_t need   = s_row[2];
396
397    // Exactly k - need columns beat the pivot; the first need pivot-equal ones fill the tail.
398    const uint32_t base_eq = (uint32_t) k - need;
399
400    const int chunk = (ncols + nparts - 1) / nparts;
401    const int col0  = part * chunk;
402    const int col1  = std::min(ncols, col0 + chunk);
403
404    // Counting first and reserving one range per group keeps the row's two counters out of
405    // the inner loop: a per-element global atomic on a single address serialises the whole
406    // emit, and at k in the thousands that alone outweighs every read the kernel does.
407    for (int col = col0 + tid; col < col1; col += block_size) {
408        const uint32_t kp = top_k_radix_key(src[col]) & mask;
409        if (kp > prefix) {
410            local_atomic(*s_gt).fetch_add(1u);
411        } else if (kp == prefix) {
412            local_atomic(*s_eq).fetch_add(1u);
413        }
414    }
415    item_ct1.barrier(sycl::access::fence_space::local_space);
416
417    if (tid == 0) {
418        const uint32_t n_gt = *s_gt;
419        const uint32_t n_eq = *s_eq;
420        *s_base_gt = n_gt ? top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_CNT_GT]).fetch_add(n_gt) : 0u;
421        *s_base_eq = n_eq ? top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_CNT_EQ]).fetch_add(n_eq) : 0u;
422        *s_gt = 0;
423        *s_eq = 0;
424    }
425    item_ct1.barrier(sycl::access::fence_space::local_space);
426
427    const uint32_t base_gt_g = *s_base_gt;
428    const uint32_t base_eq_g = *s_base_eq;
429
430    for (int col = col0 + tid; col < col1; col += block_size) {
431        const uint32_t kp = top_k_radix_key(src[col]) & mask;
432        if (kp > prefix) {
433            dst_idx[base_gt_g + local_atomic(*s_gt).fetch_add(1u)] = col;
434        } else if (kp == prefix) {
435            const uint32_t pos = base_eq_g + local_atomic(*s_eq).fetch_add(1u);
436            if (pos < need) {
437                dst_idx[base_eq + pos] = col;
438            }
439        }
440    }
441}
442
443static void top_k_radix_split_f32_sycl(
444    ggml_backend_sycl_context & ctx,
445    const float * src,
446    int32_t * dst_indices,
447    const int64_t ncols,
448    const int64_t nrows,
449    const int k,
450    const int nparts,
451    dpct::queue_ptr main_stream
452) {
453    GGML_ASSERT(ncols <= INT32_MAX);
454    GGML_ASSERT(nparts > 1);
455
456    const int block_size = ggml_sycl_info().max_work_group_sizes[ctx.device];
457    GGML_ASSERT(block_size >= SYCL_TOP_K_RADIX_BUCKETS);
458
459    const size_t state_words = (size_t) nrows * SYCL_TOP_K_RADIX_ROW_WORDS;
460    ggml_sycl_pool_alloc<uint32_t> state_alloc(ctx.pool(), state_words);
461    uint32_t * state = state_alloc.get();
462
463    // Zero histogram, done counter and both emit counters. prefix/mask/need are seeded by
464    // the first pass, which ignores the stored values.
465    // The queue is in-order, so the passes below are already ordered after this fill.
466    SYCL_CHECK(CHECK_TRY_ERROR(main_stream->memset(state, 0, state_words * sizeof(uint32_t))));
467
468    const sycl::range<1> block_dims(block_size);
469    const sycl::range<1> grid_dims(nrows * nparts);
470
471    bool first = true;
472    for (int shift = 32 - SYCL_TOP_K_RADIX_BITS; shift >= 0; shift -= SYCL_TOP_K_RADIX_BITS) {
473        const bool is_first = first;
474        first = false;
475        main_stream->submit([&](sycl::handler &cgh) {
476            sycl::local_accessor<uint32_t, 1> slm(sycl::range<1>(SYCL_TOP_K_RADIX_HIST_SIZE + 4), cgh);
477
478            cgh.parallel_for(
479                sycl::nd_range<1>(grid_dims * block_dims, block_dims),
480                [=](sycl::nd_item<1> item_ct1) {
481                    const int g    = item_ct1.get_group(0);
482                    const int row  = g / nparts;
483                    const int part = g % nparts;
484
485                    top_k_radix_split_pass_f32(
486                        src + (int64_t) row * ncols,
487                        state + (int64_t) row * SYCL_TOP_K_RADIX_ROW_WORDS,
488                        (int) ncols, k, shift, is_first, part, nparts,
489                        slm.get_multi_ptr<sycl::access::decorated::no>().get(),
490                        item_ct1);
491                });
492        });
493    }
494
495    main_stream->submit([&](sycl::handler &cgh) {
496        sycl::local_accessor<uint32_t, 1> slm(sycl::range<1>(8), cgh);
497
498        cgh.parallel_for(
499            sycl::nd_range<1>(grid_dims * block_dims, block_dims),
500            [=](sycl::nd_item<1> item_ct1) {
501                const int g    = item_ct1.get_group(0);
502                const int row  = g / nparts;
503                const int part = g % nparts;
504
505                top_k_radix_split_emit_f32(
506                    src + (int64_t) row * ncols,
507                    dst_indices + (int64_t) row * k,
508                    state + (int64_t) row * SYCL_TOP_K_RADIX_ROW_WORDS,
509                    (int) ncols, k, part, nparts,
510                    slm.get_multi_ptr<sycl::access::decorated::no>().get(),
511                    item_ct1);
512            });
513    });
514}
515
516void ggml_sycl_top_k_radix(
517    ggml_backend_sycl_context & ctx,
518    const float *   src,
519    int32_t *       dst_indices,
520    const int64_t   ncols,
521    const int64_t   nrows,
522    const int       k,
523    dpct::queue_ptr main_stream
524) {
525    const int nparts = top_k_radix_split_groups(ctx.device, ncols, nrows);
526    if (nparts > 1) {
527        top_k_radix_split_f32_sycl(ctx, src, dst_indices, ncols, nrows, k, nparts, main_stream);
528    } else {
529        top_k_radix_f32_sycl(ctx, src, dst_indices, ncols, nrows, k, main_stream);
530    }
531}