char-fork/llama.cpp

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

Nick Farrellsycl: make --fit respect --fit-target better (#27629)cc83d7b48

char/aliceai-t5-moe
21.2 KiB450 linesraw
1#include <array>
2#include <cstdint>
3#include <cstdio>
4#include <cstring>
5#include <string>
6#include <optional>
7#include <unordered_map>
8#include <vector>
9
10#include "fattn-onednn.hpp"
11#include "fattn-tile.hpp"
12#include "convert.hpp"
13
14// set minimum query length to treat as prefill (32)
15#define GGML_SYCL_FA_ONEDNN_MIN_Q 32
16
17bool ggml_sycl_fattn_onednn_binds_kv(const ggml_tensor * K, const ggml_tensor * V) {
18    if (K->type != GGML_TYPE_F16 || V->type != GGML_TYPE_F16) {
19        return false;
20    }
21    auto bindable = [](const ggml_tensor * t) {
22        return t->nb[0] == sizeof(sycl::half) && t->nb[1] % sizeof(sycl::half) == 0 &&
23               t->nb[2] % sizeof(sycl::half) == 0 && t->nb[3] % sizeof(sycl::half) == 0;
24    };
25    return bindable(K) && bindable(V);
26}
27
28bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst, bool use_shape_limit) {
29#if !GGML_SYCL_DNNL
30    GGML_UNUSED(dst);
31    GGML_UNUSED(use_shape_limit);
32    return false;
33#else
34    if (!g_ggml_sycl_fa_onednn) {
35        return false;
36    }
37    const ggml_tensor * Q     = dst->src[0];
38    const ggml_tensor * K     = dst->src[1];
39    const ggml_tensor * V     = dst->src[2];
40    const ggml_tensor * mask  = dst->src[3];
41    const ggml_tensor * sinks = dst->src[4];
42
43    // F16 KV: native SDPA at any KV length.
44    // Non-F16: dequant to F16 then SDPA at prefill lengths. Only the
45    // standard quantized KV cache types (Q4_0-Q8_0) and F32 are accepted
46    // because their to_fp16_sycl conversion is verified. BF16 and IQ*
47    // are excluded: BF16 needs a strided conversion kernel that does not
48    // exist yet; IQ types are model-weight-only quants with no dequant
49    // registration and are never used as KV caches.
50    if (K->type != GGML_TYPE_F16 || V->type != GGML_TYPE_F16) {
51        auto kt = K->type, vt = V->type;
52        bool k_ok = kt == GGML_TYPE_F32 || kt == GGML_TYPE_Q4_0 || kt == GGML_TYPE_Q4_1 ||
53                    kt == GGML_TYPE_Q5_0 || kt == GGML_TYPE_Q5_1 || kt == GGML_TYPE_Q8_0;
54        bool v_ok = vt == GGML_TYPE_F32 || vt == GGML_TYPE_Q4_0 || vt == GGML_TYPE_Q4_1 ||
55                    vt == GGML_TYPE_Q5_0 || vt == GGML_TYPE_Q5_1 || vt == GGML_TYPE_Q8_0;
56        if (!k_ok || !v_ok) {
57            return false;
58        }
59        if (use_shape_limit && (Q->ne[1] < 32 || K->ne[1] < 1024)) {
60            return false;
61        }
62        for (const ggml_tensor * t : {K, V}) {
63            if (t->type == GGML_TYPE_F16 && t->nb[1] % (t->ne[0] * 2) != 0) {
64                return false;
65            }
66        }
67    }
68    // This is the improved SPDA gate. Rather than gating Alchemist GPUs from all SPDA features, we instead target only the failing shapes.
69    // If the GPU being assessed isn't in the grouping below, it has full access to all SPDA shapes. Otherwise, if it's an Alchemist GPU, we block only the shapes with head sizes that fail.
70    // It is much easier to compare the device to a small list of failing cases than to define all the passing ones.
71    const gpu_arch arch = ggml_sycl_info().devices[ggml_sycl_get_device()].hw_info.arch;
72    bool support_spda = !(arch == gpu_arch::intel_gpu_dg2_g10 ||
73                         arch == gpu_arch::intel_gpu_dg2_g11 ||
74                         arch == gpu_arch::intel_gpu_dg2_g12);
75
76    if (!support_spda && K->ne[0] == 64) {
77        return false;
78    }
79    // Optional KV-length ceiling (GGML_SYCL_FA_ONEDNN_MAX_KV, 0 = unlimited). Escape hatch:
80    // very long sequences make the fused SDPA slow enough to risk the xe driver watchdog on
81    // some stacks; past the cap we fall back to the native FA kernel instead.
82    if (g_ggml_sycl_fa_onednn_max_kv > 0 && K->ne[1] > g_ggml_sycl_fa_onednn_max_kv) {
83        return false;
84    }
85    // gate for the following cases
86    // 1. if the oneDNN graph Add node has no input --> skip
87    // 2. types other than f16 need different logical_tensor declaration
88    // 3. the mask must be shape [1, 1, q, seq]
89    // 4. sinks: excludes attention sink (Xiao et al., 2024) that can't be modeled by oneDNN graph
90    if (!mask || mask->type != GGML_TYPE_F16 || mask->ne[2] != 1 || mask->ne[3] != 1 || sinks) {
91        return false;
92    }
93    float max_bias = 0.0f, logit_softcap = 0.0f;
94    memcpy(&max_bias,      (const float *) dst->op_params + 1, sizeof(float));
95    memcpy(&logit_softcap, (const float *) dst->op_params + 2, sizeof(float));
96    if (max_bias != 0.0f || logit_softcap != 0.0f) {
97        return false;
98    }
99    // K and V must share head_dim: the SDPA graph uses a single `d` for both.
100    const int64_t d = K->ne[0];
101    if (V->ne[0] != d || Q->ne[3] != 1) {
102        return false;
103    }
104    // GQA must divide evenly.
105    if (K->ne[2] == 0 || Q->ne[2] % K->ne[2] != 0) {
106        return false;
107    }
108    // Prefill only.
109    if (use_shape_limit && Q->ne[1] < GGML_SYCL_FA_ONEDNN_MIN_Q) {
110        return false;
111    }
112    return true;
113#endif
114}
115
116#if GGML_SYCL_DNNL
117
118#include "dnnl.hpp"
119#include "dnnl_sycl.hpp"
120#include "oneapi/dnnl/dnnl_graph.hpp"   // graph API lives only under oneapi/dnnl/, not at the include root
121
122using namespace dnnl;
123using namespace dnnl::graph;
124
125// strided src (f16 or f32) -> contiguous f16 [ne0,ne1,ne2,ne3] (ne0 innermost). nb* are BYTE strides.
126template <typename src_t>
127static void cont_to_f16_sycl(const char * src, sycl::half * dst,
128        int64_t ne0, int64_t ne1, int64_t ne2, int64_t ne3,
129        size_t nb1, size_t nb2, size_t nb3, dpct::queue_ptr stream) {
130    const int64_t n = ne0 * ne1 * ne2 * ne3;
131    stream->parallel_for(sycl::range<1>(n), [=](sycl::id<1> ix) {
132        const int64_t gid = ix[0];
133        int64_t       i   = gid;
134        const int64_t i0 = i % ne0; i /= ne0;
135        const int64_t i1 = i % ne1; i /= ne1;
136        const int64_t i2 = i % ne2; const int64_t i3 = i / ne2;
137        const src_t * p = (const src_t *) (src + i1 * nb1 + i2 * nb2 + i3 * nb3) + i0;
138        dst[gid] = (sycl::half) (*p);
139    });
140}
141
142// oneDNN SDPA out (f16 contiguous [mb,H,q,d]) -> ggml dst (f32 [head_dim,H,n_tok,mb], contiguous).
143static void permute_sdpa_out_sycl(const sycl::half * out, float * dst,
144        int64_t mb, int64_t H, int64_t q, int64_t d, dpct::queue_ptr stream) {
145    const int64_t n = mb * H * q * d;
146    stream->parallel_for(sycl::range<1>(n), [=](sycl::id<1> ix) {
147        const int64_t gid = ix[0];
148        int64_t       i   = gid;
149        const int64_t e = i % d; i /= d;
150        const int64_t t = i % q; i /= q;
151        const int64_t h = i % H; const int64_t b = i / H;
152        dst[e + h * d + t * d * H + b * d * H * q] = (float) out[gid];
153    });
154}
155
156struct sdpa_partition {
157    compiled_partition          cp;
158    std::vector<logical_tensor> ins;
159    logical_tensor              out;
160    size_t id_q = 0, id_k = 0, id_v = 0, id_scale = 0, id_mask = 0;
161    bool   ok = false;
162};
163
164// Build + compile the contiguous-input GQA SDPA graph (MatMul->Divide->Add->SoftMax->MatMul), f32 out.
165// Mirrors the hardware-verified scratch/onednn_sdpa_probe.cpp build_gqa (partitions=1, sdp_primitive_kernel_t).
166static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int seq, int d,
167                                 const std::array<int64_t, 5> & k_str, const std::array<int64_t, 5> & v_str) try {
168    using ltype = logical_tensor::layout_type;
169    using dt    = logical_tensor::data_type;
170    using ldims = logical_tensor::dims;
171    const dt    fi = dt::f32, t = dt::f16;
172    const int   rep = H / Hkv;
173    const ldims q_sz = {1, Hkv, rep, q, d}, kv_sz = {1, Hkv, 1, seq, d}, s_sz = {1, Hkv, rep, q, seq},
174                sc = {1, 1, 1, 1, 1}, msk = {1, 1, 1, q, seq}, o_sz = {1, Hkv, rep, q, d};
175    const ldims k_st(k_str.begin(), k_str.end()), v_st(v_str.begin(), v_str.end());
176    int64_t        id = 0;
177    sdpa_partition E;
178
179    auto query  = logical_tensor(id++, t,  q_sz, ltype::strided);
180    auto key    = logical_tensor(id++, t,  kv_sz, k_st);
181    auto score  = logical_tensor(id++, fi, s_sz, ltype::strided);
182    auto bmm1   = op(id++, op::kind::MatMul, "bmm1");
183    bmm1.set_attr<bool>(op::attr::transpose_b, true);          // key is [.., seq, d]
184    bmm1.add_inputs({query, key}); bmm1.add_outputs({score});
185
186    auto scale  = logical_tensor(id++, t,  sc,   ltype::strided);
187    auto scaled = logical_tensor(id++, fi, s_sz, ltype::strided);
188    auto sdiv   = op(id++, op::kind::Divide, "scale_div");     // score / (1/kq_scale) == score * kq_scale
189    sdiv.add_inputs({score, scale}); sdiv.add_outputs({scaled});
190
191    auto mask   = logical_tensor(id++, t,  msk,  ltype::strided);
192    auto masked = logical_tensor(id++, fi, s_sz, ltype::strided);
193    auto madd   = op(id++, op::kind::Add, "mask_add");
194    madd.add_inputs({scaled, mask}); madd.add_outputs({masked});
195
196    auto probs  = logical_tensor(id++, t,  s_sz, ltype::strided);
197    auto smax   = op(id++, op::kind::SoftMax, "softmax");
198    smax.set_attr<int64_t>(op::attr::axis, -1);
199    smax.set_attr<std::string>(op::attr::mode, "inf_as_zero");
200    smax.add_inputs({masked}); smax.add_outputs({probs});
201
202    auto value  = logical_tensor(id++, t,  kv_sz, v_st);
203    // f16 output is REQUIRED to hit sdp_primitive_kernel_t (the systolic micro-kernel); an f32 output
204    // falls to larger_partition_kernel_t which materializes N^2 (confirmed: scratch/onednn_sdpa_kernel_probe.cpp).
205    // converted to the f32 ggml dst in the permute below.
206    auto output = logical_tensor(id++, t,  o_sz, ltype::strided);   // f16 contiguous [mb,Hkv,rep,q,d]
207    auto bmm2   = op(id++, op::kind::MatMul, "bmm2");
208    bmm2.add_inputs({probs, value}); bmm2.add_outputs({output});
209
210    dnnl::graph::graph g(eng.get_kind());
211    g.add_op(bmm1); g.add_op(sdiv); g.add_op(madd); g.add_op(smax); g.add_op(bmm2);
212    g.finalize();
213
214    auto parts = g.get_partitions();
215    if (parts.size() != 1 || !parts[0].is_supported()) {
216        GGML_LOG_WARN("%s: oneDNN did not fuse the SDPA graph; falling back to TILE kernel\n", __func__);
217        return E;   // ok stays false -> caller falls back to TILE
218    }
219    E.ins      = parts[0].get_input_ports();
220    E.out      = parts[0].get_output_ports()[0];
221    E.cp       = parts[0].compile(E.ins, {E.out}, eng);
222    E.out      = E.cp.query_logical_tensor(E.out.get_id());
223    E.id_q     = query.get_id(); E.id_k = key.get_id(); E.id_v = value.get_id();
224    E.id_scale = scale.get_id(); E.id_mask = mask.get_id();
225    E.ok       = true;
226    return E;
227}
228catch (const std::exception & e) {
229    // compile() can reject a stride set the partitioner never inspects; memoise the failure so the
230    // fallback costs one build rather than one per call.
231    GGML_LOG_WARN("%s: oneDNN SDPA partition build failed (%s); falling back to TILE kernel\n", __func__, e.what());
232    return {};
233}
234
235void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tensor * dst) try {
236    const ggml_tensor * Q    = dst->src[0];
237    const ggml_tensor * K    = dst->src[1];
238    const ggml_tensor * V    = dst->src[2];
239    const ggml_tensor * mask = dst->src[3];
240
241    const int64_t d   = K->ne[0];   // head_dim
242    const int64_t seq = K->ne[1];   // n_kv
243    const int64_t Hkv = K->ne[2];   // n_head_kv
244    const int64_t H   = Q->ne[2];   // n_head
245    const int64_t q   = Q->ne[1];   // n_tok
246    const int64_t mb  = Q->ne[3];   // batch (== 1, gated)
247
248    float kq_scale = 1.0f;
249    memcpy(&kq_scale, (const float *) dst->op_params + 0, sizeof(float));
250
251    dpct::queue_ptr stream = ctx.stream();
252    dnnl::engine    eng    = ctx.engine_dnnl(stream);
253    dnnl::stream    strm   = ctx.stream_dnnl(stream);
254
255    const ggml_sycl_fattn_extra extra = ggml_sycl_fattn_get_extra(dst);
256
257    // Q: always f32 -- copy to dense f16.
258    std::optional<ggml_sycl_pool_alloc<sycl::half>> Qf_pool;
259    sycl::half * Qf_ptr = (sycl::half *) extra.Q_buffer_ptr;
260    if (!Qf_ptr) {
261        Qf_pool.emplace(ctx.pool(), (size_t) H * q * d);
262        Qf_ptr = Qf_pool->get();
263    }
264    cont_to_f16_sycl<float>((const char *) Q->data, Qf_ptr, d, q, H, mb, Q->nb[1], Q->nb[2], Q->nb[3], stream);
265
266    // K/V: bind the f16 cache in place. llama.cpp permutes it to [token][head][dim], so its head
267    // plane is strided rather than dense, which is what an explicit stride vector expresses.
268    // Quantized and f32 KV still stage a dense copy -- the layout the k_str/v_str defaults describe.
269    sycl::half * K_ptr = nullptr;
270    sycl::half * V_ptr = nullptr;
271    std::array<int64_t, 5> k_str{ Hkv * seq * d, seq * d, seq * d, d, 1 };
272    std::array<int64_t, 5> v_str = k_str;
273    std::optional<ggml_sycl_pool_alloc<sycl::half>> Kf_pool;
274    std::optional<ggml_sycl_pool_alloc<sycl::half>> Vf_pool;
275    // Helper: hand out reserved space, or fall back to the pool.
276    auto stage_k = [&](size_t n) { if (extra.K_buffer_ptr) { return (sycl::half *) extra.K_buffer_ptr; }
277                                  Kf_pool.emplace(ctx.pool(), n); return Kf_pool->get(); };
278    auto stage_v = [&](size_t n) { if (extra.V_buffer_ptr) { return (sycl::half *) extra.V_buffer_ptr; }
279                                  Vf_pool.emplace(ctx.pool(), n); return Vf_pool->get(); };
280
281    auto elem_strides = [](const ggml_tensor * t) {
282        const int64_t s1 = (int64_t) (t->nb[1] / t->nb[0]);
283        const int64_t s2 = (int64_t) (t->nb[2] / t->nb[0]);
284        const int64_t s3 = (int64_t) (t->nb[3] / t->nb[0]);
285        // dims are {mb=1, Hkv, rep=1, seq, d}; the size-1 dims at 0 and 2 never advance an address.
286        return std::array<int64_t, 5>{ s3, s2, s2, s1, 1 };
287    };
288
289    if (ggml_sycl_fattn_onednn_binds_kv(K, V)) {
290        K_ptr = (sycl::half *) K->data;
291        V_ptr = (sycl::half *) V->data;
292        k_str = elem_strides(K);
293        v_str = elem_strides(V);
294    } else if (K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16) {
295        K_ptr = stage_k((size_t) Hkv * seq * d);
296        V_ptr = stage_v((size_t) Hkv * seq * d);
297        cont_to_f16_sycl<sycl::half>((const char *) K->data, K_ptr, d, seq, Hkv, mb, K->nb[1], K->nb[2], K->nb[3], stream);
298        cont_to_f16_sycl<sycl::half>((const char *) V->data, V_ptr, d, seq, Hkv, mb, V->nb[1], V->nb[2], V->nb[3], stream);
299    } else if (ggml_is_quantized(K->type)) {
300        // Quantized K/V: dequant to dense F16 using pool, same lifetime as F16 path.
301        K_ptr = stage_k((size_t) ggml_nelements(K));
302        {
303            const char * K_data = (const char *)K->data;
304            const bool k_non_dense = ((int64_t)K->ne[1] * K->nb[1] != K->nb[2]) && K->ne[2] > 1;
305            const bool k_gemma = k_non_dense &&
306                ((int64_t)K->nb[2] < (int64_t)K->ne[1] * (int64_t)K->nb[1]);
307            if (ggml_is_contiguously_allocated(K) && !k_non_dense) {
308                to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(K->type, dst);
309                to_fp16(K_data, K_ptr, ggml_nelements(K), stream);
310            } else {
311                const size_t bs = ggml_blck_size(K->type);
312                const size_t ts = ggml_type_size(K->type);
313                to_fp16_nc_sycl_t to_fp16 = ggml_get_to_fp16_nc_sycl(K->type);
314                int64_t s01, s02, s03;
315                if (k_gemma) {
316                    const int64_t blk_per_row = (int64_t)K->ne[0] / bs;
317                    s01 = (int64_t)Hkv * blk_per_row;
318                    s02 = blk_per_row;
319                    s03 = (int64_t)K->ne[1] * s01;
320                } else {
321                    s01 = (int64_t)K->nb[1] / ts;
322                    s02 = (int64_t)K->nb[2] / ts;
323                    s03 = (int64_t)K->nb[3] / ts;
324                }
325                to_fp16(K_data, K_ptr,
326                        K->ne[0], K->ne[1], K->ne[2], K->ne[3],
327                        s01, s02, s03, stream);
328            }
329        }
330        // Quantized V: always dequant separately. Even when K and V share
331        // the same underlying allocation (V is a view of K with the same
332        // data pointer), their logical values differ because the quantized
333        // elements at different positions/offsets represent different K/V
334        // data. Master's F16 path also never aliases K and V.
335        V_ptr = stage_v((size_t) ggml_nelements(V));
336        {
337            const char * V_data = (const char *)V->data;
338            const bool v_non_dense = ((int64_t)V->ne[1] * V->nb[1] != V->nb[2]) && V->ne[2] > 1;
339            const bool v_gemma = v_non_dense &&
340                ((int64_t)V->nb[2] < (int64_t)V->ne[1] * (int64_t)V->nb[1]);
341            if (ggml_is_contiguously_allocated(V) && !v_non_dense) {
342                to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(V->type, dst);
343                to_fp16(V_data, V_ptr, ggml_nelements(V), stream);
344            } else {
345                const size_t bs = ggml_blck_size(V->type);
346                const size_t ts = ggml_type_size(V->type);
347                to_fp16_nc_sycl_t to_fp16 = ggml_get_to_fp16_nc_sycl(V->type);
348                int64_t s01, s02, s03;
349                if (v_gemma) {
350                    const int64_t blk_per_row = (int64_t)V->ne[0] / bs;
351                    s01 = (int64_t)V->ne[2] * blk_per_row;
352                    s02 = blk_per_row;
353                    s03 = (int64_t)V->ne[1] * s01;
354                } else {
355                    s01 = (int64_t)V->nb[1] / ts;
356                    s02 = (int64_t)V->nb[2] / ts;
357                    s03 = (int64_t)V->nb[3] / ts;
358                }
359                to_fp16(V_data, V_ptr,
360                        V->ne[0], V->ne[1], V->ne[2], V->ne[3],
361                        s01, s02, s03, stream);
362            }
363        }
364    } else {
365        // F32: strided copy to dense F16 via cont_to_f16_sycl<float>.
366        K_ptr = stage_k((size_t) ggml_nelements(K));
367        cont_to_f16_sycl<float>((const char *) K->data, K_ptr, K->ne[0], K->ne[1], K->ne[2], K->ne[3],
368                                K->nb[1], K->nb[2], K->nb[3], stream);
369        V_ptr = stage_v((size_t) ggml_nelements(V));
370        cont_to_f16_sycl<float>((const char *) V->data, V_ptr, V->ne[0], V->ne[1], V->ne[2], V->ne[3],
371                                V->nb[1], V->nb[2], V->nb[3], stream);
372    }
373
374    // divide-by-(1/scale) reproduces ggml's score *= kq_scale on the proven probe graph.
375    //
376    // The scale must not be uploaded with an async memcpy from a stack local: on the in-order
377    // queue that copy waits behind the K/V staging kernels, and once those take long enough
378    // (n_kv >= ~26k on B70) the host frame is recycled before the copy runs, feeding the SDPA a
379    // garbage scale (output collapses to a repeated token). Write the scalar from a kernel
380    // instead -- the value is captured into the command, so no host memory has to outlive the
381    // call, and the enqueue stays async.
382    const sycl::half scale_h = (sycl::half) (1.0f / kq_scale);
383    std::optional<ggml_sycl_pool_alloc<sycl::half>> scbuf;
384    sycl::half * scale_dev = (sycl::half *) extra.scale_buffer_ptr;
385    if (!scale_dev) {
386        scbuf.emplace(ctx.pool(), 1);
387        scale_dev = scbuf->get();
388    }
389    stream->single_task([=]() { *scale_dev = scale_h; });
390
391    // f16 contiguous SDPA out [mb,H,q,d]
392    std::optional<ggml_sycl_pool_alloc<sycl::half>> outf_pool;
393    sycl::half * outf_ptr = (sycl::half *) extra.out_buffer_ptr;
394    if (!outf_ptr) {
395        outf_pool.emplace(ctx.pool(), (size_t) H * q * d);
396        outf_ptr = outf_pool->get();
397    }
398
399    // compile once per (device, shape, KV strides), reuse across layers/calls. Stride 2 always
400    // repeats stride 1 and stride 4 is always 1, so the key covers every entry that can differ.
401    static std::unordered_map<std::string, sdpa_partition> cache;
402    char keyb[256];
403    snprintf(keyb, sizeof(keyb), "%d:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld", ggml_sycl_get_device(),
404             (long long) H, (long long) Hkv, (long long) q, (long long) seq, (long long) d,
405             (long long) k_str[0], (long long) k_str[1], (long long) k_str[3],
406             (long long) v_str[0], (long long) v_str[1], (long long) v_str[3]);
407    auto it = cache.find(keyb);
408    if (it == cache.end()) {
409        it = cache.emplace(keyb, build_sdpa(eng, (int) H, (int) Hkv, (int) q, (int) seq, (int) d, k_str, v_str)).first;
410    }
411    sdpa_partition & E = it->second;
412    if (!E.ok) {
413        // oneDNN can decline a shape or a stride set that _supported() never sees; build_sdpa warns per key.
414        ggml_sycl_flash_attn_ext_tile(ctx, dst);
415        return;
416    }
417
418    auto id2ptr = [&](size_t r) -> void * {
419        if (r == E.id_q)     return Qf_ptr;
420        if (r == E.id_k)     return K_ptr;
421        if (r == E.id_v)     return V_ptr;
422        if (r == E.id_scale) return scale_dev;
423        if (r == E.id_mask)  return (void *) mask->data;
424        return nullptr;
425    };
426    std::vector<tensor> ti;
427    ti.reserve(E.ins.size());
428    for (auto & lt : E.ins) {
429        ti.emplace_back(lt, eng, id2ptr(lt.get_id()));
430    }
431    tensor to(E.out, eng, outf_ptr);
432    E.cp.execute(strm, ti, {to});
433
434    permute_sdpa_out_sycl(outf_ptr, (float *) dst->data, mb, H, q, d, stream);
435    // Single device needs no sync: the dnnl stream wraps this same in-order queue, so the SDPA
436    // serializes with the staging kernels before it and the permute/pool reuse after it. The
437    // garbage output formerly blamed on the missing sync here was the scale use-after-return
438    // fixed above. Keep the conservative wait for multi-GPU, where other devices' streams can
439    // race the pool:
440    if (ggml_sycl_info().device_count > 1) {
441        stream->wait_and_throw();
442    }
443}
444catch (const std::exception & e) {
445    // any oneDNN/SYCL failure is non-fatal: fall back to the existing kernel (strictly additive).
446    GGML_LOG_WARN("%s: oneDNN SDPA failed (%s); falling back to TILE kernel\n", __func__, e.what());
447    ggml_sycl_flash_attn_ext_tile(ctx, dst);
448}
449
450#endif // GGML_SYCL_DNNL