char-fork/llama.cpp

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

Ian Faustsycl: fix multiple warnings in compiling sycl backend (#26713)6602dd338

char/aliceai-t5-moe
28.6 KiB689 linesraw
1// Flash attention via oneMKL GEMM (XMX-accelerated).
2// Uses column_major::gemm for Q*K^T and S*V matmuls
3// with an online softmax SYCL kernel.
4//
5// All GQA query heads sharing a KV head are batched into single
6// GEMM calls, amortizing MKL launch overhead across K and V reuse.
7//
8
9#include "common.hpp"
10#include "fattn-common.hpp"
11#include "fattn-buffers.hpp"
12#include "convert.hpp"
13#include "fattn.hpp"
14
15#include <oneapi/mkl.hpp>
16#include <cstdio>
17#include <chrono>
18
19#define MKL_FA_CHUNK_SIZE_KV 8192
20
21// Number of query rows processed per tile. The score buffers (KQ_f32, S_f16)
22// are sized q_tile_rows * chunk_size, so this bounds their footprint
23// regardless of batch size (n_query_rows = n_queries * gqa_ratio). A typical
24// single-ubatch prefill (e.g. ubatch 1024 * gqa 8 = 8192 rows) is exactly one
25// tile, so it runs with no extra iterations. Larger batches tile and stay
26// bounded. Override with GGML_SYCL_MKL_FA_Q_TILE.
27#define MKL_FA_Q_TILE 8192
28
29#define MKL_FA_WG_SIZE 256
30
31using oneapi::mkl::transpose;
32using oneapi::mkl::blas::column_major::gemm;
33
34// ---------------------------------------------------------------------------
35// Helpers
36// ---------------------------------------------------------------------------
37
38// Pack all GQA Q heads for one KV head into fp16, applying q_scale.
39// Launches one kernel per GQA group — each kernel copies exactly
40// n_queries * DKQ elements using the per-group dst offset and
41// per-head source stride.
42static void mkl_fa_pack_q_fp16(
43    dpct::queue_ptr stream,
44    sycl::half * __restrict dst,
45    const float * __restrict q_src,
46    int n_queries, int DKQ,
47    int gqa_ratio, int kvh_base_head,
48    float q_scale, int64_t q_row_stride, int64_t q_head_stride,
49    int64_t wg_size) {
50
51    for (int iqg = 0; iqg < gqa_ratio; iqg++) {
52        int     iqh       = kvh_base_head + iqg;
53        sycl::half * dst_g = dst + (int64_t)iqg * n_queries * DKQ;
54
55        const int64_t n_elem = (int64_t)n_queries * DKQ;
56        const int64_t wg = ((n_elem + wg_size - 1) / wg_size) * wg_size;
57
58        stream->submit([&](sycl::handler & cgh) {
59            cgh.parallel_for(sycl::nd_range<1>(wg, wg_size),
60                [=](sycl::nd_item<1> item) {
61                    int64_t e = item.get_global_id(0);
62                    if (e >= n_elem) return;
63
64                    int64_t q = e / DKQ;
65                    int64_t d = e - q * DKQ;
66
67                    // Stride-aware source offset: handles permuted,
68                    // sliced, or contiguous Q tensor layouts.
69                    int64_t src_off = d
70                        + q * q_row_stride
71                        + (int64_t)iqh * q_head_stride;
72
73                    dst_g[e] = sycl::half(
74                        q_src[src_off] * q_scale);
75                });
76        });
77    }
78}
79
80// Zero-initialize the online softmax state arrays.
81// KQ_max → -inf, KQ_sum → 0, VKQ_accum → 0.
82// Merged into one kernel to avoid per-array launch overhead.
83static void mkl_fa_init_softmax_state(
84    dpct::queue_ptr stream,
85    float * kmax, float * ksum, float * vacc,
86    int n_query_rows, int DV, int64_t wg_size) {
87
88    const float    neg_inf   = -1e30f;
89    const int64_t  n_maxsum  = n_query_rows;
90    const int64_t  n_vacc    = (int64_t)n_query_rows * DV;
91    const int64_t  total     = (n_vacc > n_maxsum) ? n_vacc : n_maxsum;
92    const int64_t  wg = ((total + wg_size - 1) / wg_size) * wg_size;
93
94    stream->submit([&](sycl::handler & cgh) {
95        cgh.parallel_for(sycl::nd_range<1>(wg, wg_size),
96            [=](sycl::nd_item<1> item) {
97                int64_t i = item.get_global_id(0);
98                if (i < n_maxsum) {
99                    kmax[i] = neg_inf;
100                    ksum[i] = 0.0f;
101                }
102                if (i < n_vacc) {
103                    vacc[i] = 0.0f;
104                }
105            });
106    });
107}
108
109// Online softmax over one KV chunk for a tile of GQA query rows.
110// The tile spans absolute rows [q0, q0 + q_rows). Score buffers
111// (KQ_f32/S_f16) are indexed RELATIVE to the tile; the persistent state
112// (VKQ_accum/KQ_max/KQ_sum) and mask are indexed by ABSOLUTE row.
113// For each row: find local max → rescale previous VKQ_accum →
114// compute exp(s - max) → write S_f16 → update running max/sum.
115static void mkl_fa_online_softmax_chunk(
116    dpct::queue_ptr stream,
117    float * __restrict KQ_f32,
118    sycl::half * __restrict S_f16,
119    float * __restrict KQ_max,
120    float * __restrict KQ_sum,
121    float * __restrict VKQ_accum,
122    int q0, int q_rows, int n_queries, int DV,
123    int chunk_size, int chunk_start,
124    int kvh_head,
125    const sycl::half * mask_data, int64_t mask_head_stride,
126    int64_t mask_row_stride, int mask_n_heads,
127    float logit_softcap, int64_t wg_size) {
128
129    const int64_t wg = ((q_rows + wg_size - 1) / wg_size) * wg_size;
130
131    stream->submit([&](sycl::handler & cgh) {
132        cgh.parallel_for(sycl::nd_range<1>(wg, wg_size),
133            [=](sycl::nd_item<1> item) {
134                int jc_rel = item.get_global_id(0);
135                if (jc_rel >= q_rows) return;
136                int jc_abs = q0 + jc_rel;
137
138                const int gqa_group = jc_abs / n_queries;
139                const int q_row     = jc_abs % n_queries;
140
141                // Score buffers are tile-local (relative index).
142                const float * __restrict KQ_row = KQ_f32
143                    + jc_rel * (int64_t)chunk_size;
144                // Persistent accumulator is full-sized (absolute index).
145                float * __restrict vkq = VKQ_accum
146                    + jc_abs * (int64_t)DV;
147
148                const sycl::half * mask_h = nullptr;
149                int64_t m_stride = 0;
150                if (mask_data) {
151                    int m_head = (mask_n_heads > 1)
152                        ? (kvh_head + gqa_group) : 0;
153                    mask_h   = mask_data + (int64_t)m_head * mask_head_stride;
154                    m_stride = mask_row_stride;
155                }
156
157                // Row-wise local maximum (softcap before mask)
158                float local_max = -1e30f;
159                for (int i = 0; i < chunk_size; i++) {
160                    float s = KQ_row[i];
161                    if (logit_softcap != 0.0f) {
162                        s = logit_softcap * sycl::tanh(s);
163                    }
164                    if (mask_h) {
165                        s += (float)mask_h[q_row * m_stride
166                            + (chunk_start + i)];
167                    }
168                    if (s > local_max) local_max = s;
169                }
170
171                // Rescale previous accumulator by exp(old_max - new_max)
172                float old_max = KQ_max[jc_abs];
173                float new_max = (old_max > local_max) ? old_max : local_max;
174                float rescale = (old_max < -1e29f) ? 1.0f
175                    : sycl::native::exp(old_max - new_max);
176
177                for (int v = 0; v < DV; v++) {
178                    vkq[v] *= rescale;
179                }
180
181                // Softmax and write S_f16 (tile-local index)
182                float local_sum = 0.0f;
183                sycl::half * __restrict S_row = S_f16
184                    + jc_rel * (int64_t)chunk_size;
185
186                for (int i = 0; i < chunk_size; i++) {
187                    float s = KQ_row[i];
188                    if (logit_softcap != 0.0f) {
189                        s = logit_softcap * sycl::tanh(s);
190                    }
191                    if (mask_h) {
192                        s += (float)mask_h[q_row * m_stride
193                            + (chunk_start + i)];
194                    }
195                    float val = sycl::native::exp(s - new_max);
196                    S_row[i] = sycl::half(val);
197                    local_sum += val;
198                }
199
200                KQ_sum[jc_abs] = KQ_sum[jc_abs] * rescale + local_sum;
201                KQ_max[jc_abs] = new_max;
202            });
203    });
204}
205
206// Write one GQA group's normalized output to its destination head.
207static void mkl_fa_normalize_head(
208    dpct::queue_ptr stream,
209    float * __restrict dst_batch,
210    const float * __restrict VKQ_accum,
211    const float * __restrict KQ_sum,
212    int iqh, int n_queries, int DV, int n_q_heads,
213    int64_t src_offset, int64_t wg_size) {
214
215    const int64_t wg = ((n_queries + wg_size - 1) / wg_size) * wg_size;
216
217    stream->submit([&](sycl::handler & cgh) {
218        cgh.parallel_for(sycl::nd_range<1>(wg, wg_size),
219            [=](sycl::nd_item<1> item) {
220                int jc = item.get_global_id(0);
221                if (jc >= n_queries) return;
222
223                int     ksum_idx = (int)(src_offset / DV) + jc;
224                float   inv_sum  = 1.0f / KQ_sum[ksum_idx];
225                const float * __restrict src = VKQ_accum
226                    + src_offset + jc * (int64_t)DV;
227                // Interleaved dst layout (matching TILE):
228                // rows alternate between heads, then increment query.
229                // offset = (query * n_q_heads + head) * DV
230                float * __restrict dst_row = dst_batch
231                    + ((int64_t)jc * n_q_heads + iqh) * (int64_t)DV;
232
233                for (int v = 0; v < DV; v++) {
234                    dst_row[v] = src[v] * inv_sum;
235                }
236            });
237    });
238}
239
240// ---------------------------------------------------------------------------
241// Per-chunk dequant
242//
243// Rather than dequantizing all of K/V up front (footprint scales with
244// context), we dequant one KV-head chunk at a time into a dense
245// [this_chunk x D] fp16 buffer (row-major, lda = D). The source address of
246// element (head=ikvh, row=chunk_start+r, col=c) decomposes into independent
247// linear terms head_off(ikvh) + row_off(chunk_start) + (r,c), so slicing a
248// chunk is a clean pointer offset in every layout case. The true-Gemma-
249// interleave vs padded-seq-view distinction is resolved once when the
250// descriptor is built; slicing does not reintroduce it.
251// ---------------------------------------------------------------------------
252enum mkl_fa_kv_desc_mode {
253    MKL_FA_KV_MODE_F16_DENSE       = 0,
254    MKL_FA_KV_MODE_F16_INTERLEAVED = 1,
255    MKL_FA_KV_MODE_QUANT_CONTIG    = 2,
256    MKL_FA_KV_MODE_QUANT_NC        = 3,
257};
258
259struct mkl_fa_kv_desc {
260    const char *         data = nullptr;
261    ggml_type            type = GGML_TYPE_F16;
262    int64_t              D    = 0;      // ne[0]
263    int64_t              nb1  = 0;      // byte stride, seq dim
264    int64_t              nb2  = 0;      // byte stride, head dim
265    mkl_fa_kv_desc_mode  mode = MKL_FA_KV_MODE_F16_DENSE;
266    int64_t              ts   = 0;      // type size (mode 3 base offset)
267    int64_t              s01  = 0;      // nc row stride in blocks (mode 3)
268    int64_t              s02  = 0;      // nc head stride in blocks (mode 3)
269};
270
271static mkl_fa_kv_desc mkl_fa_make_desc(const ggml_tensor * T, bool interleaved, int n_kv_heads) {
272    mkl_fa_kv_desc d;
273    d.data = (const char *)T->data;
274    d.type = T->type;
275    d.D    = T->ne[0];
276    d.nb1  = (int64_t)T->nb[1];
277    d.nb2  = (int64_t)T->nb[2];
278    d.ts   = (int64_t)ggml_type_size(T->type);
279
280    if (T->type == GGML_TYPE_F16) {
281        d.mode = interleaved ? MKL_FA_KV_MODE_F16_INTERLEAVED
282                             : MKL_FA_KV_MODE_F16_DENSE;
283    } else if (ggml_is_contiguously_allocated(T) && !interleaved) {
284        d.mode = MKL_FA_KV_MODE_QUANT_CONTIG;
285    } else {
286        d.mode = MKL_FA_KV_MODE_QUANT_NC;
287        const int64_t bs          = (int64_t)ggml_blck_size(T->type);
288        const int64_t blk_per_row = T->ne[0] / bs;
289        // True Gemma interleave packs heads within a row (nb[2] < ne[1]*nb[1])
290        // → reconstruct physical strides. Padded seq-views (nb[2] > ne[1]*nb[1])
291        // already have correct physical strides.
292        const bool gemma = interleaved &&
293            ((int64_t)T->nb[2] < (int64_t)T->ne[1] * (int64_t)T->nb[1]);
294        if (gemma) {
295            d.s01 = (int64_t)n_kv_heads * blk_per_row;
296            d.s02 = blk_per_row;
297        } else {
298            d.s01 = d.nb1 / d.ts;
299            d.s02 = d.nb2 / d.ts;
300        }
301    }
302    return d;
303}
304
305// Dequant one KV-head chunk into a dense [this_chunk x D] fp16 buffer.
306static void mkl_fa_dequant_chunk(
307    dpct::queue_ptr stream, const mkl_fa_kv_desc & d, ggml_tensor * dst_ctx,
308    sycl::half * out, int ikvh, int chunk_start, int this_chunk) {
309
310    const int64_t D = d.D;
311    switch (d.mode) {
312        case MKL_FA_KV_MODE_F16_DENSE: {
313            const char * base = d.data + (int64_t)ikvh * d.nb2
314                + (int64_t)chunk_start * d.nb1;
315            stream->memcpy(out, base, (size_t)this_chunk * D * sizeof(sycl::half));
316            break;
317        }
318        case MKL_FA_KV_MODE_F16_INTERLEAVED: {
319            const char * base = d.data + (int64_t)ikvh * d.nb2
320                + (int64_t)chunk_start * d.nb1;
321            const int64_t row_halfs = d.nb1 / (int64_t)sizeof(sycl::half);
322            const sycl::half * src = (const sycl::half *)base;
323            stream->parallel_for(
324                sycl::range<2>((size_t)this_chunk, (size_t)D),
325                [=](sycl::item<2> it) {
326                    int64_t r = it.get_id(0);
327                    int64_t c = it.get_id(1);
328                    out[r * D + c] = src[r * row_halfs + c];
329                });
330            break;
331        }
332        case MKL_FA_KV_MODE_QUANT_CONTIG: {
333            const char * base = d.data + (int64_t)ikvh * d.nb2
334                + (int64_t)chunk_start * d.nb1;
335            to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(d.type, dst_ctx);
336            to_fp16(base, out, (int64_t)this_chunk * D, stream);
337            break;
338        }
339        default: {  // MKL_FA_KV_MODE_QUANT_NC
340            to_fp16_nc_sycl_t to_fp16 = ggml_get_to_fp16_nc_sycl(d.type);
341            const int64_t base_blocks = (int64_t)ikvh * d.s02
342                + (int64_t)chunk_start * d.s01;
343            const char * base = d.data + base_blocks * d.ts;
344            // ne02 = ne03 = 1 → s02/s03 inert; head+chunk offset carried by base.
345            to_fp16(base, out, D, this_chunk, 1, 1, d.s01, d.s02, d.s02, stream);
346            break;
347        }
348    }
349}
350
351// ---------------------------------------------------------------------------
352// MKL Flash Attention orchestrator
353//
354// Pipeline: dequantize K/V → for each KV head:
355//   pack GQA Q heads → MKL GEMM KQ → online softmax →
356//   MKL GEMM VKQ → accumulate → normalize → scatter to dst
357// ---------------------------------------------------------------------------
358void ggml_sycl_flash_attn_ext_mkl(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
359
360    const ggml_tensor * Q    = dst->src[0];
361    const ggml_tensor * K    = dst->src[1];
362    const ggml_tensor * V    = dst->src[2];
363    const ggml_tensor * mask = dst->src[3];
364    ggml_tensor * KQV = dst;
365
366    GGML_ASSERT(Q->type == GGML_TYPE_F32);
367    GGML_ASSERT(KQV->type == GGML_TYPE_F32);
368
369    // --- Op params ---
370    float scale = 1.0f, max_bias = 0.0f, logit_softcap = 0.0f;
371    memcpy(&scale,         (const float *)KQV->op_params + 0, sizeof(float));
372    memcpy(&max_bias,      (const float *)KQV->op_params + 1, sizeof(float));
373    memcpy(&logit_softcap, (const float *)KQV->op_params + 2, sizeof(float));
374
375    const float q_scale = scale;
376
377    // --- Dimensions ---
378    const int DKQ        = (int)K->ne[0];
379    const int DV         = (int)V->ne[0];
380    const int n_queries  = (int)Q->ne[1];
381    const int n_q_heads  = (int)Q->ne[2];
382    const int n_kv_heads = (int)K->ne[2];
383    const int n_batch    = (int)Q->ne[3];
384    const int n_kv       = (int)K->ne[1];
385    const int gqa_ratio  = n_q_heads / n_kv_heads;
386    const int n_query_rows = n_queries * gqa_ratio;
387
388    GGML_ASSERT(n_q_heads % n_kv_heads == 0);
389    GGML_ASSERT(max_bias == 0.0f);  // ALiBi not supported
390    GGML_ASSERT(Q->ne[3] == K->ne[3] || K->ne[3] == 1);
391
392    const int chunk_size = std::min(MKL_FA_CHUNK_SIZE_KV, n_kv);
393
394    // Query rows are processed in tiles of q_tile_rows so the score buffers
395    // (KQ_f32/S_f16 = q_tile_rows * chunk_size) stay bounded regardless of
396    // batch size. n_query_rows <= Q_TILE is a single tile (no extra work).
397    static int q_tile_env = ggml_sycl_get_env("GGML_SYCL_MKL_FA_Q_TILE", MKL_FA_Q_TILE);
398    const int q_tile_rows = std::max(1, std::min(q_tile_env, n_query_rows));
399
400    const int64_t wg_size = MKL_FA_WG_SIZE;
401
402    // --- Debug output (gated by GGML_SYCL_MKL_FA_DEBUG=1) ---
403    static int mkl_call_count = 0;
404    mkl_call_count++;
405    static int mkl_debug = ggml_sycl_get_env("GGML_SYCL_MKL_FA_DEBUG", 0);
406    const bool do_print = (mkl_debug == 1);
407
408    const int64_t q_row_stride  = Q->nb[1] / sizeof(float);
409    const int64_t q_head_stride = Q->nb[2] / sizeof(float);
410
411    const bool V_is_K_view = V->view_src
412        && (V->view_src == K || (V->view_src == K->view_src
413            && V->view_offs == K->view_offs));
414
415    // Early interleaved detection for debug output.
416    // True interleaved detection happens after dequant (nb12_fp16 == nb11_fp16),
417    // but we can pre-detect on the original tensor strides.
418    const bool k_early_interleaved =
419        ((int64_t)K->ne[1] * K->nb[1] != K->nb[2]);
420    const bool v_early_interleaved =
421        !V_is_K_view && ((int64_t)V->ne[1] * V->nb[1] != V->nb[2]);
422
423    if (do_print) {
424        GGML_LOG_INFO("[MKL-FA] #%d D=%d DV=%d n_q=%d n_kv=%d "
425                "n_qh=%d n_kvh=%d gqa=%d batch=%d K=%s V=%s "
426                "chunk=%d buf=%.1fMB%s%s\n",
427                mkl_call_count, DKQ, DV, n_queries, n_kv,
428                n_q_heads, n_kv_heads, gqa_ratio, n_batch,
429                ggml_type_name(K->type), ggml_type_name(V->type),
430                chunk_size,
431                (double)((int64_t)n_query_rows * chunk_size * sizeof(float))
432                    / (1024.0 * 1024.0),
433                k_early_interleaved ? " K_ILV" : "",
434                v_early_interleaved ? " V_ILV" : "");
435        GGML_LOG_INFO("[MKL-FA] #%d Q-nb1=%lld Q-nb2=%lld "
436                "q_rs=%lld q_hs=%lld dst_rs=%lld dst_hs=%lld\n",
437                mkl_call_count,
438                (long long)Q->nb[1], (long long)Q->nb[2],
439                (long long)q_row_stride, (long long)q_head_stride,
440                (long long)(KQV->nb[1] / sizeof(float)),
441                (long long)(KQV->nb[2] / sizeof(float)));
442    }
443
444    // --- Stream and allocators ---
445    dpct::queue_ptr stream = ctx.stream();
446
447#define MKL_TAKE_TIME(t0)  auto t0 = std::chrono::steady_clock::now()
448#define MKL_ACCUM(acc, t0) do { if (do_print) { \
449    acc += (int64_t)std::chrono::duration_cast \
450    <std::chrono::microseconds>(std::chrono::steady_clock::now() - (t0)).count(); \
451} } while(0)
452
453    int64_t gemm_kq_time_us  = 0;
454    int64_t gemm_vkq_time_us = 0;
455    int64_t softmax_time_us  = 0;
456    int64_t dequant_time_us  = 0;
457
458    MKL_TAKE_TIME(t_deq);
459
460    // --- K/V dequant descriptors ---
461    // Dequant is done per-chunk inside the KV loop (footprint independent of
462    // context). Output is always dense row-major fp16 [this_chunk x D], lda=D.
463    // Interleaved detection: ne[1]*nb[1] != nb[2] means heads are interleaved.
464    const bool k_interleaved =
465        ((int64_t)K->ne[1] * K->nb[1] != K->nb[2]) && K->ne[2] > 1;
466    const bool v_interleaved =
467        ((int64_t)V->ne[1] * V->nb[1] != V->nb[2]) && V->ne[2] > 1;
468
469    const mkl_fa_kv_desc K_desc = mkl_fa_make_desc(K, k_interleaved, n_kv_heads);
470    const mkl_fa_kv_desc V_desc = V_is_K_view
471        ? K_desc : mkl_fa_make_desc(V, v_interleaved, n_kv_heads);
472
473    MKL_ACCUM(dequant_time_us, t_deq);
474
475    // --- Resolve mask pointers ---
476    int64_t mask_head_stride = 0;
477    int64_t mask_row_stride  = 0;
478    int     mask_n_heads     = 0;
479
480    if (mask) {
481        // Use actual fp16 device size (2 bytes), NOT sizeof(sycl::half)
482        // which may be 4 on the host in oneAPI.
483        mask_head_stride = mask->nb[2] / 2;
484        mask_row_stride  = mask->nb[1] / 2;
485        mask_n_heads     = (int)mask->ne[2];
486    }
487
488    // --- Allocate intermediates from pool ---
489    ggml_sycl_pool & pool = ctx.pool();
490
491    ggml_sycl_pool_alloc<float>      KQ_f32(pool);      // [q_tile_rows x chunk]
492    ggml_sycl_pool_alloc<sycl::half> S_f16(pool);       // [q_tile_rows x chunk]
493    ggml_sycl_pool_alloc<float>      VKQ_chunk(pool);   // [q_tile_rows x DV]
494    ggml_sycl_pool_alloc<float>      VKQ_accum(pool);   // [n_query_rows x DV] (full)
495    ggml_sycl_pool_alloc<float>      KQ_max(pool);      // [n_query_rows] (full)
496    ggml_sycl_pool_alloc<float>      KQ_sum(pool);      // [n_query_rows] (full)
497    ggml_sycl_pool_alloc<sycl::half> Q_head_f16(pool);  // [n_query_rows x DKQ] (full)
498    ggml_sycl_pool_alloc<sycl::half> K_chunk_f16(pool); // [chunk x DKQ] (per-chunk dequant)
499    ggml_sycl_pool_alloc<sycl::half> V_chunk_f16(pool); // [chunk x DV] (per-chunk dequant)
500
501    KQ_f32.alloc((size_t)q_tile_rows * chunk_size);
502    S_f16.alloc((size_t)q_tile_rows * chunk_size);
503    VKQ_chunk.alloc((size_t)q_tile_rows * DV);
504    VKQ_accum.alloc((size_t)n_query_rows * DV);
505    KQ_max.alloc(n_query_rows);
506    KQ_sum.alloc(n_query_rows);
507    Q_head_f16.alloc((size_t)n_query_rows * DKQ);
508    K_chunk_f16.alloc((size_t)chunk_size * DKQ);
509
510    sycl::half * V_chunk_f16_ptr;
511    if (V_is_K_view) {
512        V_chunk_f16_ptr = K_chunk_f16.ptr;   // V aliases K (DV == DKQ)
513    } else {
514        V_chunk_f16.alloc((size_t)chunk_size * DV);
515        V_chunk_f16_ptr = V_chunk_f16.ptr;
516    }
517
518    sycl::half * Q_head_f16_ptr  = Q_head_f16.ptr;
519    float      * KQ_f32_ptr      = KQ_f32.ptr;
520    sycl::half * S_f16_ptr       = S_f16.ptr;
521    float      * VKQ_chunk_ptr   = VKQ_chunk.ptr;
522    float      * VKQ_accum_ptr   = VKQ_accum.ptr;
523    float      * KQ_max_ptr      = KQ_max.ptr;
524    float      * KQ_sum_ptr      = KQ_sum.ptr;
525    sycl::half * K_chunk_f16_ptr = K_chunk_f16.ptr;
526
527    const float alpha = 1.0f;
528    const float beta  = 0.0f;
529
530    for (int ib = 0; ib < n_batch; ib++) {
531        const float * Q_batch = (const float *)Q->data
532            + ib * (Q->nb[3] / sizeof(float));
533        float * dst_batch = (float *)KQV->data
534            + ib * (KQV->nb[3] / sizeof(float));
535
536        const sycl::half * mask_batch = nullptr;
537        if (mask) {
538            int m_batch = (mask->ne[3] > 1) ? ib : 0;
539            mask_batch = (const sycl::half *)mask->data
540                + m_batch * (mask->nb[3] / 2);  // 2 = actual fp16 device size
541        }
542
543        for (int ikvh = 0; ikvh < n_kv_heads; ikvh++) {
544            int kvh_base_head = ikvh * gqa_ratio;
545
546            // 1. Pack all GQA Q heads into fp16 (full n_query_rows)
547            mkl_fa_pack_q_fp16(stream,
548                Q_head_f16_ptr, Q_batch,
549                n_queries, DKQ,
550                gqa_ratio, kvh_base_head,
551                q_scale, q_row_stride, q_head_stride, wg_size);
552
553            // 2. Initialize softmax state (full n_query_rows)
554            mkl_fa_init_softmax_state(stream,
555                KQ_max_ptr, KQ_sum_ptr, VKQ_accum_ptr,
556                n_query_rows, DV, wg_size);
557
558            // Sync before MKL GEMM (MKL may use an internal queue)
559            stream->wait();
560
561            // 3. KV chunk loop (OUTER): dequant each chunk once, then tile queries.
562            for (int chunk_start = 0; chunk_start < n_kv; chunk_start += chunk_size) {
563                int this_chunk = std::min(chunk_size, n_kv - chunk_start);
564
565                // 3a. Dequant this KV chunk to dense fp16 (once per chunk)
566                {
567                    MKL_TAKE_TIME(t0);
568                    mkl_fa_dequant_chunk(stream, K_desc, KQV,
569                        K_chunk_f16_ptr, ikvh, chunk_start, this_chunk);
570                    if (!V_is_K_view) {
571                        mkl_fa_dequant_chunk(stream, V_desc, KQV,
572                            V_chunk_f16_ptr, ikvh, chunk_start, this_chunk);
573                    }
574                    stream->wait();  // dequant must be ready before MKL GEMM
575                    MKL_ACCUM(dequant_time_us, t0);
576                }
577
578                // 3b. Query tile loop (INNER) — bounds KQ_f32/S_f16 footprint.
579                for (int q0 = 0; q0 < n_query_rows; q0 += q_tile_rows) {
580                    int q_rows = std::min(q_tile_rows, n_query_rows - q0);
581
582                    // GEMM: KQ = Q_tile × K_chunk^T
583                    {
584                        MKL_TAKE_TIME(t0);
585                        sycl::event ev = gemm(*stream,
586                            transpose::trans, transpose::nontrans,
587                            this_chunk, q_rows, DKQ,
588                            alpha,
589                            K_chunk_f16_ptr, DKQ,
590                            Q_head_f16_ptr + (int64_t)q0 * DKQ, DKQ,
591                            beta,
592                            KQ_f32_ptr, this_chunk);
593                        try { ev.wait_and_throw(); } catch (sycl::exception & e) {
594                            GGML_LOG_INFO("[MKL-FA] GEMM KQ: %s\n", e.what());
595                            GGML_ABORT("MKL GEMM KQ failed");
596                        }
597                        MKL_ACCUM(gemm_kq_time_us, t0);
598                    }
599                    // Online softmax over this chunk for this query tile
600                    {
601                        MKL_TAKE_TIME(t0);
602                        mkl_fa_online_softmax_chunk(stream,
603                            KQ_f32_ptr, S_f16_ptr,
604                            KQ_max_ptr, KQ_sum_ptr, VKQ_accum_ptr,
605                            q0, q_rows, n_queries, DV,
606                            this_chunk, chunk_start,
607                            kvh_base_head,
608                            mask_batch, mask_head_stride,
609                            mask_row_stride, mask_n_heads,
610                            logit_softcap, wg_size);
611                        stream->wait();  // S_f16 must be ready for GEMM
612                        MKL_ACCUM(softmax_time_us, t0);
613                    }
614
615                    // GEMM: VKQ_chunk = S × V_chunk
616                    {
617                        MKL_TAKE_TIME(t0);
618                        sycl::event ev = gemm(*stream,
619                            transpose::nontrans, transpose::nontrans,
620                            DV, q_rows, this_chunk,
621                            alpha,
622                            V_chunk_f16_ptr, DV,
623                            S_f16_ptr, this_chunk,
624                            beta,
625                            VKQ_chunk_ptr, DV);
626                        try { ev.wait_and_throw(); } catch (sycl::exception & e) {
627                            GGML_LOG_INFO("[MKL-FA] GEMM VKQ: %s\n", e.what());
628                            GGML_ABORT("MKL GEMM VKQ failed");
629                        }
630                        MKL_ACCUM(gemm_vkq_time_us, t0);
631                    }
632                    // VKQ_accum[q0..] += VKQ_chunk
633                    {
634                        const int64_t n_total = (int64_t)q_rows * DV;
635                        const int64_t wg = ((n_total + wg_size - 1) / wg_size)
636                            * wg_size;
637                        float * accum = VKQ_accum_ptr + (int64_t)q0 * DV;
638                        stream->submit([&](sycl::handler & cgh) {
639                            cgh.parallel_for(sycl::nd_range<1>(wg, wg_size),
640                                [=](sycl::nd_item<1> item) {
641                                    int64_t i = item.get_global_id(0);
642                                    if (i < n_total) {
643                                        accum[i] += VKQ_chunk_ptr[i];
644                                    }
645                                });
646                        });
647                    }
648                }
649            }
650
651            // 4. Normalize and scatter each GQA head to dst
652            for (int iqg = 0; iqg < gqa_ratio; iqg++) {
653                int     iqh        = kvh_base_head + iqg;
654                int64_t src_offset = (int64_t)iqg * n_queries * DV;
655                mkl_fa_normalize_head(stream,
656                    dst_batch, VKQ_accum_ptr, KQ_sum_ptr,
657                    iqh, n_queries, DV, n_q_heads,
658                    src_offset, wg_size);
659            }
660        }
661    }
662
663#undef MKL_TAKE_TIME
664#undef MKL_ACCUM
665
666    if (do_print) {
667        const int64_t v_chunk_elems = V_is_K_view ? 0 : (int64_t)chunk_size * DV;
668        double total_mb = (double)(
669            (int64_t)q_tile_rows * chunk_size * sizeof(float)      // KQ_f32
670          + (int64_t)q_tile_rows * chunk_size * sizeof(sycl::half) // S_f16
671          + (int64_t)q_tile_rows * DV * sizeof(float)              // VKQ_chunk
672          + (int64_t)n_query_rows * DV * sizeof(float)             // VKQ_accum
673          + (int64_t)n_query_rows * sizeof(float)                  // KQ_max
674          + (int64_t)n_query_rows * sizeof(float)                  // KQ_sum
675          + (int64_t)n_query_rows * DKQ * sizeof(sycl::half)       // Q_head_f16
676          + (int64_t)chunk_size * DKQ * sizeof(sycl::half)         // K_chunk_f16
677          + v_chunk_elems * (int64_t)sizeof(sycl::half)            // V_chunk_f16
678        ) / (1024.0 * 1024.0);
679        GGML_LOG_INFO("[MKL-FA] #%d n_kv=%d n_q=%d q_tile=%d time_us: "
680                "dequant=%lld GEMM_KQ=%lld softmax=%lld GEMM_VKQ=%lld "
681                "buf_mb=%.1f\n",
682                mkl_call_count, n_kv, n_queries, q_tile_rows,
683                (long long)dequant_time_us,
684                (long long)gemm_kq_time_us,
685                (long long)softmax_time_us,
686                (long long)gemm_vkq_time_us,
687                total_mb);
688    }
689}