char-fork/llama.cpp

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

fairydreamingmemory : avoid allocating V cache for indexer (it's not used) (#28330)311d4211b

char/aliceai-t5-moe
23.5 KiB683 linesraw
1#include "llama-memory-hybrid-idx.h"
2
3#include "llama-impl.h"
4#include "llama-batch.h"
5#include "llama-io.h"
6#include "llama-model.h"
7
8
9#include <algorithm>
10#include <cassert>
11#include <cmath>
12#include <iterator>
13#include <stdexcept>
14
15//
16// llama_memory_hybrid_idx
17//
18
19llama_memory_hybrid_idx::llama_memory_hybrid_idx(
20        const llama_model & model,
21                            /* attn */
22                ggml_type   type_k,
23                ggml_type   type_v,
24                     bool   v_trans,
25                 uint32_t   kv_size,
26                 uint32_t   n_pad,
27                 uint32_t   n_swa,
28           llama_swa_type   swa_type,
29                            /* recurrent */
30                ggml_type   type_r,
31                ggml_type   type_s,
32                 uint32_t   rs_size,
33                            /* common */
34                 uint32_t   n_seq_max,
35                 uint32_t   n_rs_seq,
36                     bool   offload,
37                     bool   unified,
38                            /* layer filters */
39    const layer_filter_cb & filter_attn,
40    const layer_filter_cb & filter_recr,
41    const layer_filter_cb & filter_idx) :
42    llama_memory_hybrid(
43        model,
44        type_k, type_v, v_trans, kv_size, n_pad, n_swa, swa_type,
45        type_r, type_s, rs_size,
46        n_seq_max, n_rs_seq, offload, unified,
47        filter_attn, filter_recr),
48    hparams_idx(model.hparams),
49    mem_idx(filter_idx == nullptr ? nullptr : [&] {
50        // MQA with a single key head of indexer_head_size, as llama_kv_cache_dsa shapes its own
51        std::fill(hparams_idx.n_head_kv_arr.begin(), hparams_idx.n_head_kv_arr.end(), 1);
52        hparams_idx.n_embd_head_k_full = model.hparams.indexer_head_size;
53
54        // the cached indexer keys are raw, rotation happens after pooling at read time, so a
55        // K-shift must not rotate them while the stream copies in the same update still apply
56        hparams_idx.rope_type = LLAMA_ROPE_TYPE_NONE;
57
58        // fool llama_kv_cache into thinking this is a MLA cache, so it won't cache V tensors
59        hparams_idx.n_embd_head_k_mla_impl = model.hparams.indexer_head_size;
60        hparams_idx.n_embd_head_v_mla_impl = model.hparams.indexer_head_size;
61
62        LLAMA_LOG_INFO("%s: creating indexer KV cache, size = %u cells\n", __func__, kv_size);
63
64        return new llama_kv_cache(
65            model, hparams_idx, type_k, type_v, v_trans, offload, unified,
66            kv_size, n_seq_max, n_pad, n_swa, swa_type,
67            nullptr, filter_idx, nullptr, nullptr, "idx_");
68    }()) {}
69
70llama_memory_context_ptr llama_memory_hybrid_idx::init_batch(llama_batch_allocr & balloc, uint32_t n_ubatch, bool embd_all) {
71    // note: repeats llama_memory_hybrid::init_batch, as the indexer needs the attention slot infos that the base context hides
72    do {
73        balloc.split_reset();
74
75        // follow the recurrent pattern for creating the ubatch splits
76        std::vector<llama_ubatch> ubatches;
77
78        while (true) {
79            llama_ubatch ubatch;
80
81            if (embd_all) {
82                // if all tokens are output, split by sequence
83                ubatch = balloc.split_seq(n_ubatch);
84            } else {
85                // Use non-sequential split when KV cache is unified (needed for hellaswag/winogrande/multiple-choice)
86                const bool unified = (get_mem_attn()->get_n_stream() == 1);
87
88                // [TAG_RECURRENT_ROLLBACK_SPLITS]
89                // the trailing (1 + n_rs_seq) tokens of each seq must stay in the same ubatch
90                //   so that the rollback snapshots remain valid
91                const uint32_t n_rs_seq = get_mem_recr()->n_rs_seq;
92
93                ubatch = balloc.split_equal(n_ubatch, !unified, n_rs_seq > 0 ? n_rs_seq + 1 : 0);
94            }
95
96            if (ubatch.n_tokens == 0) {
97                break;
98            }
99
100            ubatches.push_back(std::move(ubatch)); // NOLINT
101        }
102
103        if (balloc.get_n_used() < balloc.get_n_tokens()) {
104            // failed to find a suitable split
105            break;
106        }
107
108        // prepare the recurrent batches first
109        if (!get_mem_recr()->prepare(ubatches)) {
110            // TODO: will the recurrent cache be in an undefined context at this point?
111            LLAMA_LOG_ERROR("%s: failed to prepare recurrent ubatches\n", __func__);
112            return std::make_unique<llama_memory_hybrid_idx_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
113        }
114
115        // prepare the attention cache
116        auto heads_attn = get_mem_attn()->prepare(ubatches);
117        if (heads_attn.empty()) {
118            LLAMA_LOG_ERROR("%s: failed to prepare attention ubatches\n", __func__);
119            return std::make_unique<llama_memory_hybrid_idx_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
120        }
121
122        // the indexer uses the attention cache's slot layout; a separate one can drift from it
123        llama_kv_cache::slot_info_vec_t heads_idx;
124        if (mem_idx) {
125            heads_idx = heads_attn;
126        }
127
128        return std::make_unique<llama_memory_hybrid_idx_context>(
129                this, std::move(heads_attn), std::move(heads_idx), std::move(ubatches));
130    } while(false);
131
132    return std::make_unique<llama_memory_hybrid_idx_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
133}
134
135llama_memory_context_ptr llama_memory_hybrid_idx::init_full() {
136    return std::make_unique<llama_memory_hybrid_idx_context>(this);
137}
138
139llama_memory_context_ptr llama_memory_hybrid_idx::init_update(llama_context * lctx, bool optimize) {
140    return std::make_unique<llama_memory_hybrid_idx_context>(this, lctx, optimize);
141}
142
143void llama_memory_hybrid_idx::clear(bool data) {
144    llama_memory_hybrid::clear(data);
145
146    if (mem_idx) {
147        mem_idx->clear(data);
148    }
149}
150
151bool llama_memory_hybrid_idx::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1) {
152    // same order as llama_memory_hybrid::seq_rm: the recurrent cache can refuse, so try it first
153    if (!get_mem_recr()->seq_rm(seq_id, p0, p1)) {
154        return false;
155    }
156
157    if (mem_idx) {
158        mem_idx->seq_rm(seq_id, p0, p1);
159    }
160
161    return get_mem_attn()->seq_rm(seq_id, p0, p1);
162}
163
164void llama_memory_hybrid_idx::seq_cp(llama_seq_id seq_id_src, llama_seq_id seq_id_dst, llama_pos p0, llama_pos p1) {
165    llama_memory_hybrid::seq_cp(seq_id_src, seq_id_dst, p0, p1);
166
167    if (mem_idx) {
168        mem_idx->seq_cp(seq_id_src, seq_id_dst, p0, p1);
169    }
170}
171
172void llama_memory_hybrid_idx::seq_keep(llama_seq_id seq_id) {
173    llama_memory_hybrid::seq_keep(seq_id);
174
175    if (mem_idx) {
176        mem_idx->seq_keep(seq_id);
177    }
178}
179
180void llama_memory_hybrid_idx::seq_add(llama_seq_id seq_id, llama_pos p0, llama_pos p1, llama_pos shift) {
181    llama_memory_hybrid::seq_add(seq_id, p0, p1, shift);
182
183    if (mem_idx) {
184        mem_idx->seq_add(seq_id, p0, p1, shift);
185    }
186}
187
188void llama_memory_hybrid_idx::seq_div(llama_seq_id seq_id, llama_pos p0, llama_pos p1, int d) {
189    llama_memory_hybrid::seq_div(seq_id, p0, p1, d);
190
191    if (mem_idx) {
192        mem_idx->seq_div(seq_id, p0, p1, d);
193    }
194}
195
196std::map<ggml_backend_buffer_type_t, size_t> llama_memory_hybrid_idx::memory_breakdown() const {
197    std::map<ggml_backend_buffer_type_t, size_t> mb = llama_memory_hybrid::memory_breakdown();
198
199    if (mem_idx) {
200        for (const auto & buft_size : mem_idx->memory_breakdown()) {
201            mb[buft_size.first] += buft_size.second;
202        }
203    }
204
205    return mb;
206}
207
208void llama_memory_hybrid_idx::state_write(llama_io_write_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) const {
209    llama_memory_hybrid::state_write(io, seq_id, flags);
210
211    // [TAG_HYBRID_IDX_STATE] the indexer section goes last, so it is a pure suffix: an old reader stops early instead of misparsing it
212    // The indexer mirrors the attention cache, so it uses the same PARTIAL_ONLY gate.
213    if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) {
214        if (mem_idx) {
215            mem_idx->state_write(io, seq_id, flags);
216        }
217    }
218
219}
220
221void llama_memory_hybrid_idx::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) {
222    // note: repeats llama_memory_hybrid::state_read
223    // the indexer needs the attention cache's cells, and a half-failed restore must leave all three caches alike
224
225    // [TAG_HYBRID_IDX_SINFO]
226    // the indexer restore adopts the attention cache's layout instead of searching for cells of its own
227    // two find_slot calls agree only while both caches see the same occupancy, which a restore cannot promise
228    llama_kv_cache::slot_info_vec_t sinfos_attn;
229
230    try {
231        if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) {
232            get_mem_attn()->state_read_sinfo(io, seq_id, flags, mem_idx ? &sinfos_attn : nullptr, nullptr);
233        }
234
235        get_mem_recr()->state_read(io, seq_id, flags);
236
237        // [TAG_HYBRID_IDX_STATE] must mirror the write order in state_write
238        if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) {
239            if (mem_idx) {
240                mem_idx->state_read_sinfo(io, seq_id, flags, nullptr, &sinfos_attn);
241            }
242        }
243
244    } catch (...) {
245        // a half-restored context is the one state the indexer cannot fix by itself: attention holds new cells, the indexer old ones
246        // drop what was being restored from all of them, which is a state they do agree on.
247        state_drop(seq_id);
248
249        throw;
250    }
251}
252
253void llama_memory_hybrid_idx::state_drop(llama_seq_id seq_id) {
254    // dropped directly, not via seq_rm: the recurrent cache may refuse it and then only the other two get cleared
255    if (seq_id < 0) {
256        clear(true);
257
258        return;
259    }
260
261    get_mem_attn()->seq_rm(seq_id, -1, -1);
262    get_mem_recr()->seq_rm(seq_id, -1, -1);
263
264    if (mem_idx) {
265        mem_idx->seq_rm(seq_id, -1, -1);
266    }
267}
268
269llama_kv_cache * llama_memory_hybrid_idx::get_mem_idx() const {
270    return mem_idx.get();
271}
272
273void llama_memory_hybrid_idx::set_input_qsa(
274        ggml_tensor * cell_blk,
275        ggml_tensor * blk_cells,
276        ggml_tensor * blk_pos,
277        ggml_tensor * bias,
278        const llama_ubatch * ubatch,
279        uint32_t ratio,
280        bool blk_bias) const {
281    GGML_ASSERT(ratio > 0);
282    GGML_ASSERT(get_mem_idx() != nullptr);
283
284    GGML_ASSERT(ggml_backend_buffer_is_host(cell_blk->buffer));
285
286    const int64_t n_kv     = cell_blk->ne[0];
287    const int64_t n_ns     = cell_blk->ne[1];        // streams in this ubatch
288    const int64_t n_blocks = blk_pos->ne[0]/(4*n_ns);
289    const int64_t n_tokens = ubatch->n_tokens;
290    const int64_t r        = ratio;
291
292    GGML_ASSERT(n_tokens % n_ns == 0);
293    const int64_t n_tps = n_tokens/n_ns;             // tokens per stream
294
295    int32_t * dst_cell_blk  = (int32_t *) cell_blk->data;
296    int32_t * dst_blk_cells = (int32_t *) blk_cells->data;
297    int32_t * dst_blk_pos   = (int32_t *) blk_pos->data;
298    float   * dst_bias      = (float   *) bias->data;
299
300    // a block is keyed on (sequence set, index bucket): a unified cache counts every sequence
301    // from zero, so the bucket alone would pool two sequences into one block
302    GGML_ASSERT(r <= 64);
303    const uint64_t slots_full = r == 64 ? ~uint64_t(0) : ((uint64_t(1) << r) - 1);
304
305    // TODO: this runs per ubatch and is O(n_kv) per stream, about 865 us at 33k context. the cost
306    //       is the per-cell scan rather than these allocations, so hoisting them buys nothing
307    std::vector<int32_t>  blk_of(n_kv);
308    std::vector<int32_t>  cell_grp(n_kv);
309    std::vector<int32_t>  grp_head(n_blocks);
310    std::vector<int32_t>  grp_next;
311    std::vector<int32_t>  grp_first;
312    std::vector<int32_t>  grp_slot0;
313    std::vector<uint64_t> grp_slots;
314    std::vector<int32_t>  grp_bid;
315    std::vector<int32_t>  bid_idx;
316    std::vector<int32_t>  bid_cell;
317    std::vector<int32_t>  bid_slot0;
318
319    std::vector<int32_t> order;
320    std::vector<int32_t> rank;
321
322    std::fill(dst_blk_pos, dst_blk_pos + 4*n_blocks*n_ns, 0);
323
324    for (int64_t s = 0; s < n_ns; ++s) {
325        // ubatch index s*n_tps belongs to this stream; ask which cells array it uses
326        const llama_seq_id seq_of_stream = ubatch->seq_id[s*n_tps][0];
327        const auto & cells = get_mem_idx()->get_cells(seq_of_stream);
328
329        int32_t * cur_cell_blk  = dst_cell_blk  + s*n_kv;
330        int32_t * cur_blk_cells = dst_blk_cells + s*(r*n_blocks);
331
332        std::fill(cur_blk_cells, cur_blk_cells + r*n_blocks, 0);
333
334        bid_idx  .clear();
335        bid_cell .clear();
336        bid_slot0.clear();
337
338        int n_seq_present = 0;
339
340        for (int sq = 0; sq < LLAMA_MAX_SEQ && n_seq_present < 2; ++sq) {
341            if (cells.seq_pos_min(sq) >= 0) {
342                n_seq_present++;
343            }
344        }
345
346        const bool one_seq = n_seq_present <= 1;
347
348        // a cell no block covers needs its own -inf, which a per-block bias cannot carry
349        // every cache path keeps the position below the cell window, so this stays false
350        bool oor = false;
351
352        bool dup = false;
353
354        bool ranked = false;
355
356        auto group_cells = [&]() {
357            // -1 means no usable block: an incomplete or short group cannot be pooled
358            std::fill(blk_of.begin(),   blk_of.end(),   -1);
359            std::fill(cell_grp.begin(), cell_grp.end(), -1);
360            std::fill(grp_head.begin(), grp_head.end(), -1);
361
362            grp_next .clear();
363            grp_first.clear();
364            grp_slot0.clear();
365            grp_slots.clear();
366            grp_bid  .clear();
367
368            oor = false;
369            dup = false;
370
371            for (int64_t j = 0; j < n_kv; ++j) {
372                if (cells.is_empty(j)) {
373                    continue;
374                }
375
376                const int64_t idx = ranked ? rank[j] : cells.pos_get(j);
377                const int64_t pb  = idx/r;
378
379                if (pb >= n_blocks) {
380                    oor = true;
381                    continue;
382                }
383
384                int32_t g = -1;
385
386                for (int32_t c = grp_head[pb]; c >= 0; c = grp_next[c]) {
387                    if (one_seq || cells.seq_get_all((uint32_t) grp_first[c]) == cells.seq_get_all((uint32_t) j)) {
388                        g = c;
389                        break;
390                    }
391                }
392
393                if (g < 0) {
394                    g = (int32_t) grp_first.size();
395
396                    grp_next .push_back(grp_head[pb]);
397                    grp_first.push_back((int32_t) j);
398                    grp_slot0.push_back(-1);
399                    grp_slots.push_back(0);
400                    grp_bid  .push_back(-1);
401
402                    grp_head[pb] = g;
403                }
404
405                const uint64_t bit = uint64_t(1) << (idx%r);
406
407                dup |= (grp_slots[g] & bit) != 0;
408
409                cell_grp[j]   = g;
410                grp_slots[g] |= bit;
411
412                if (idx%r == 0) {
413                    grp_slot0[g] = (int32_t) j;
414                }
415            }
416        };
417
418        group_cells();
419
420        // mrope repeats one position across an image, so rank cells instead of using the position
421        if (dup && ubatch->is_pos_2d() && one_seq) {
422            order.clear();
423            order.reserve(n_kv);
424
425            for (int64_t j = 0; j < n_kv; ++j) {
426                if (!cells.is_empty(j)) {
427                    order.push_back((int32_t) j);
428                }
429            }
430
431            // same total order the mrope causal mask uses: pos, then ext.y, then ext.x
432            std::sort(order.begin(), order.end(), [&cells](int32_t a, int32_t b) {
433                const llama_pos pa = cells.pos_get(a);
434                const llama_pos pb = cells.pos_get(b);
435
436                if (pa != pb) {
437                    return pa < pb;
438                }
439
440                const auto & ea = cells.ext_get(a);
441
442                return cells.ext_get(b).is_2d_gt(ea.x, ea.y);
443            });
444
445            rank.assign(n_kv, -1);
446
447            for (int64_t k = 0; k < (int64_t) order.size(); ++k) {
448                rank[order[k]] = (int32_t) k;
449            }
450
451            ranked = true;
452
453            group_cells();
454        }
455
456        GGML_ASSERT((!blk_bias || !oor) && "qsa: cell position runs past the cell window");
457
458        int32_t n_bid = 0;
459
460        for (int64_t pb = 0; pb < n_blocks; ++pb) {
461            for (int32_t g = grp_head[pb]; g >= 0; g = grp_next[g]) {
462                if (grp_slots[g] != slots_full) {
463                    continue;
464                }
465
466                grp_bid[g] = n_bid++;
467
468                bid_idx  .push_back((int32_t) (pb*r));
469                bid_cell .push_back(grp_first[g]);
470                bid_slot0.push_back(grp_slot0[g]);
471            }
472        }
473
474        GGML_ASSERT(n_bid <= n_blocks);
475
476        for (int32_t b = 0; b < n_bid; ++b) {
477            int32_t sec_pos[4] = { bid_idx[b], bid_idx[b], bid_idx[b], bid_idx[b] };
478
479            if (ranked) {
480                const int32_t   c = bid_slot0[b];
481                const llama_pos p = cells.pos_get(c);
482                const auto &    e = cells.ext_get(c);
483
484                sec_pos[0] = p;
485                sec_pos[1] = e.y;
486                sec_pos[2] = e.x;
487                sec_pos[3] = p;
488            }
489
490            for (int64_t sec = 0; sec < 4; ++sec) {
491                dst_blk_pos[sec*(n_blocks*n_ns) + s*n_blocks + b] = sec_pos[sec];
492            }
493        }
494
495        // unpooled cells all point at one spare block. a spare block exists only when some
496        // cell is unpooled: n_bid == n_blocks means every cell sits in a full block.
497        const bool     have_dead = n_bid < n_blocks;
498        const int32_t  dead_bid  = have_dead ? n_bid : n_blocks - 1;
499
500        for (int64_t j = 0; j < n_kv; ++j) {
501            const int32_t g = cell_grp[j];
502
503            blk_of[j] = g < 0 ? -1 : grp_bid[g];
504
505            if (blk_of[j] >= 0) {
506                const int64_t idx = ranked ? rank[j] : cells.pos_get(j);
507
508                cur_blk_cells[blk_of[j]*r + (idx%r)] = (int32_t) j;
509            }
510
511            cur_cell_blk[j] = blk_of[j] < 0 ? dead_bid : blk_of[j];
512        }
513
514        for (int64_t ii = 0; ii < n_tps; ++ii) {
515            const int64_t      i      = s*n_tps + ii;
516            const llama_seq_id seq_id = ubatch->seq_id[i][0];
517
518            int64_t q = ubatch->pos[i];
519
520            if (ranked) {
521                const llama_pos qt = ubatch->pos[i];
522                const llama_pos qy = ubatch->pos[i + n_tokens];
523                const llama_pos qx = ubatch->pos[i + n_tokens*2];
524
525                int64_t lo = 0;
526                int64_t hi = (int64_t) order.size();
527
528                while (lo < hi) {
529                    const int64_t   mid = (lo + hi)/2;
530                    const int32_t   c   = order[mid];
531                    const llama_pos pc  = cells.pos_get(c);
532
533                    if (pc < qt || (pc == qt && !cells.ext_get(c).is_2d_gt(qx, qy))) {
534                        lo = mid + 1;
535                    } else {
536                        hi = mid;
537                    }
538                }
539
540                q = lo - 1;
541            }
542
543            // the tail is an incomplete block and is always visible, as in the reference
544            const int64_t tail_start = (q + 1)/r*r;
545
546            if (blk_bias) {
547                // a block sits wholly inside or outside the tail, so one value covers it
548                // the caller adds the attention mask, which drops empty, foreign and future cells
549                float * cur_blk_bias = dst_bias + i*n_blocks;
550
551                for (int64_t b = 0; b < n_blocks; ++b) {
552                    if (b >= n_bid || !cells.seq_has((uint32_t) bid_cell[b], seq_id)) {
553                        cur_blk_bias[b] = -INFINITY;
554                        continue;
555                    }
556
557                    // finite, so it can never meet a -inf and produce a nan
558                    cur_blk_bias[b] = bid_idx[b] >= tail_start ? 1e9f : 0.0f;
559                }
560
561                // the spare block holds the unpooled cells, which are the incomplete tail, so
562                // it gets the tail value. it must stay finite: a sequence with fewer than
563                // `ratio` cells owns no full block, and a row of -inf only gives a nan.
564                if (have_dead) {
565                    cur_blk_bias[dead_bid] = 1e9f;
566                }
567
568                continue;
569            }
570
571            float * cur_bias = dst_bias + i*n_kv;
572
573            for (int64_t j = 0; j < n_kv; ++j) {
574                float v = -INFINITY;
575
576                if (!cells.is_empty(j) && cells.seq_has(j, seq_id)) {
577                    const int64_t idx = ranked ? rank[j] : cells.pos_get(j);
578
579                    if (idx <= q) {
580                        // finite, so it can never meet a -inf and produce a nan
581                        v = idx >= tail_start ? 1e9f : (blk_of[j] < 0 ? -INFINITY : 0.0f);
582                    }
583                }
584
585                cur_bias[j] = v;
586            }
587        }
588    }
589}
590
591//
592// llama_memory_hybrid_idx_context
593//
594
595// streams in each ubatch's slot info, matching get_k/get_v's `ns`
596static std::vector<uint32_t> llama_memory_hybrid_idx_ns(const llama_kv_cache::slot_info_vec_t & sinfos) {
597    std::vector<uint32_t> res;
598    res.reserve(sinfos.size());
599
600    for (const auto & sinfo : sinfos) {
601        res.push_back(sinfo.s1 - sinfo.s0 + 1);
602    }
603
604    return res;
605}
606
607llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(llama_memory_status status) :
608    llama_memory_hybrid_context(status) {}
609
610llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(llama_memory_hybrid_idx * mem) :
611    llama_memory_hybrid_context(mem),
612    mem(mem),
613    // graph reservation walks a full context, and qwen4exp builds the sparse attention only when this is set
614    // without it the reserved worst case is the dense graph, so ggml-alloc must grow the buffer on the first decode
615    ns_ubatch(mem->get_mem_idx() == nullptr ?
616        std::vector<uint32_t>() : std::vector<uint32_t>{ mem->get_mem_idx()->get_n_stream() }),
617    ctx_idx(mem->get_mem_idx() == nullptr ? nullptr :
618        new llama_kv_cache_context(mem->get_mem_idx())) {}
619
620llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(
621        llama_memory_hybrid_idx * mem,
622                  llama_context * lctx,
623                           bool   optimize) :
624    llama_memory_hybrid_context(mem, lctx, optimize),
625    mem(mem),
626    // update() applies a pending cross-stream seq_cp, else the copy keeps stale indexer keys
627    ctx_idx(mem->get_mem_idx() == nullptr ? nullptr :
628        mem->get_mem_idx()->init_update(lctx, optimize)) {}
629
630llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(
631        llama_memory_hybrid_idx * mem,
632                slot_info_vec_t   sinfos_attn,
633                slot_info_vec_t   sinfos_idx,
634      std::vector<llama_ubatch>   ubatches) :
635    // note: the base copies the ubatches; ctx_idx gets a copy of its own
636    llama_memory_hybrid_context(mem, std::move(sinfos_attn), ubatches),
637    mem(mem),
638    ns_ubatch(llama_memory_hybrid_idx_ns(sinfos_idx)),
639    ctx_idx(mem->get_mem_idx() == nullptr ? nullptr :
640        new llama_kv_cache_context(mem->get_mem_idx(), std::move(sinfos_idx), ubatches)) {}
641
642bool llama_memory_hybrid_idx_context::next() {
643    if (ctx_idx) {
644        ctx_idx->next();
645    }
646
647    ++i_cur;
648
649    return llama_memory_hybrid_context::next();
650}
651
652bool llama_memory_hybrid_idx_context::apply() {
653    bool res = llama_memory_hybrid_context::apply();
654
655    if (ctx_idx) {
656        res = res & ctx_idx->apply();
657    }
658
659    return res;
660}
661
662const llama_kv_cache_context * llama_memory_hybrid_idx_context::get_idx() const {
663    return static_cast<const llama_kv_cache_context *>(ctx_idx.get());
664}
665
666uint32_t llama_memory_hybrid_idx_context::get_n_stream() const {
667    GGML_ASSERT(i_cur < ns_ubatch.size());
668
669    return ns_ubatch[i_cur];
670}
671
672void llama_memory_hybrid_idx_context::set_input_qsa(
673        ggml_tensor * cell_blk,
674        ggml_tensor * blk_cells,
675        ggml_tensor * blk_pos,
676        ggml_tensor * bias,
677        const llama_ubatch * ubatch,
678        uint32_t ratio,
679        bool blk_bias) const {
680    GGML_ASSERT(mem != nullptr);
681
682    mem->set_input_qsa(cell_blk, blk_cells, blk_pos, bias, ubatch, ratio, blk_bias);
683}