| From: opencoti |
| Subject: [PATCH 2/N] lazy slot-context allocation β Phase 2 (grow on demand + partial zero-fill) |
|
|
| Phase 2 of the F4 M3 lazy slot-context rollout. Builds on Phase 1 |
| (deferred zero-fill) by adding a soft allocation cap that |
| find_slot honors, a grow-on-demand path that bumps the cap when |
| real prompts need more cells, and a partial-range zero-fill so |
| RSS commits scale with actual workload β not with the configured |
| n_ctx_slot Γ slot_count product. |
|
|
| The end-state for the production embedder |
| (qwen3-0.6b, --parallel 2 --ctx-size 16384, CPU mode): |
| - With --slot-initial-ctx 0 (the default) Phase 2 is a no-op |
| layered on Phase 1: cap == n_ctx_seq from construction; full |
| buffer cleared on first batch (matches Phase 1 RSS profile). |
| - With --slot-initial-ctx 4096 (the recommended embedder |
| setting) the warm RSS scales with whatever cells real prompts |
| have touched. A run that only ever embeds 8β56-token requests |
| commits ~4096 Γ per-cell bytes, not the full 16384. |
|
|
| The patch threads `slot_initial_ctx` through: |
| - public API: llama_context_params (include/llama.h) |
| - internal: llama_cparams (src/llama-cparams.h) |
| - ctor chain: llama_context::ctor (src/llama-context.cpp) |
| llama_kv_cache::ctor (src/llama-kv-cache.{h,cpp}) |
| llama_kv_cache_iswa::ctor (src/llama-kv-cache-iswa.{h,cpp}) |
| llama_model::create_memory call sites (src/llama-model.cpp) |
| - hybrid mem: src/llama-memory-hybrid.cpp + -iswa.cpp pass 0 |
| (kept on Phase-1 behavior β recurrent/hybrid models |
| are not the F4 embedder target) |
| - CLI: common_params.slot_initial_ctx + --slot-initial-ctx |
| (common/common.{h,cpp}, common/arg.cpp) |
|
|
| Mechanism (inside llama_kv_cache): |
| - kv_size_max: hard cap (== constructor's kv_size). |
| - kv_size_alloc: soft cap; cells [0, kv_size_alloc) visible to |
| find_slot. Starts at min(slot_initial_ctx, |
| kv_size_max) β Phase 1 default of 0 collapses |
| to kv_size_max so find_slot sees everything. |
| - n_cells_cleared: half-open range of cells already zero-filled. |
| Phase 1's `bool cleared` is replaced; the cells |
| beyond n_cells_cleared have malloc'd address |
| space but uncommitted pages. |
| - ensure_cleared(up_to): |
| Partial memset of cells [n_cells_cleared, up_to) on each |
| layer's K and V tensor across every stream, using |
| ggml_backend_tensor_memset (host-or-device dispatch). The |
| common inference path passes up_to=0 which expands to the |
| current soft cap; state_read passes up_to=kv_size_max for |
| full coverage on the deserialize path. |
| - ensure_capacity(cells_needed): |
| Geometric grow (next-pow2, n_pad-aligned) of kv_size_alloc |
| up to kv_size_max. Monotonic β never shrinks (Phase 3 adds |
| shrink-on-idle). |
| - prepare(ubatches): |
| When find_slot fails inside the current soft cap, retries |
| after ensure_capacity(used_max + ubatch.n_tokens) + |
| ensure_cleared(). Bounded by kv_size_max; a genuine |
| overflow at the hard cap propagates up unchanged. |
|
|
| Bench: docs/decisions/0001-lazy-slot-context.md Β§6.2 Phase 2 row. |
| Milestone: F4 M3 β see docs/features/memory_embedder.md |
| Design: docs/decisions/0001-lazy-slot-context.md |
| Upstreaming: deferred; propose to llamafile + llama.cpp |
| maintainers after F4 M3's full phase-set ships and bench |
| numbers are public. |
|
|
| |
| |
| |
| |
| @@ -1361,6 +1361,16 @@ common_params_context common_params_parser_init(common_params & params, llama_ex |
| params.kv_unified = value; |
| } |
| ).set_env("LLAMA_ARG_KV_UNIFIED").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_PERPLEXITY, LLAMA_EXAMPLE_BATCHED, LLAMA_EXAMPLE_BENCH, LLAMA_EXAMPLE_PARALLEL})); |
| + add_opt(common_arg( |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + {"--slot-initial-ctx"}, "N", |
| + string_format("initial KV-cell allocation per sequence (default: %d, 0 = full n_ctx_seq, " |
| + "positive values cap initial allocation and enable grow-on-demand)", |
| + params.slot_initial_ctx), |
| + [](common_params & params, int value) { |
| + params.slot_initial_ctx = value; |
| + } |
| + ).set_env("LLAMA_ARG_SLOT_INITIAL_CTX").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_PARALLEL})); |
| add_opt(common_arg( |
| {"--cache-idle-slots"}, |
| {"--no-cache-idle-slots"}, |
| |
| |
| |
| @@ -1628,6 +1628,8 @@ struct llama_context_params common_context_params_to_llama(const common_params & |
| |
| cparams.n_ctx = params.n_ctx; |
| cparams.n_seq_max = params.n_parallel; |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_initial_ctx = params.slot_initial_ctx < 0 ? 0u : (uint32_t) params.slot_initial_ctx; |
| cparams.n_rs_seq = params.speculative.need_n_rs_seq(); |
| cparams.n_batch = params.n_batch; |
| cparams.n_ubatch = params.n_ubatch; |
| |
| |
| |
| @@ -425,6 +425,11 @@ struct ggml_opt_optimizer_params common_opt_lr_pars(void * userdata); |
| struct common_params { |
| int32_t n_predict = -1; // max. number of new tokens to predict, -1 == no limit |
| int32_t n_ctx = 0; // context size, 0 == context the model was trained with |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Initial KV-cell allocation per sequence. 0 = full n_ctx_seq allocation |
| + // (Phase 1 behavior β only zero-fill is deferred). Positive values cap |
| + // the initial allocation and enable grow-on-demand up to n_ctx_seq. |
| + int32_t slot_initial_ctx = 0; |
| int32_t n_batch = 2048; // logical batch size for prompt processing (must be >=32 to use BLAS) |
| int32_t n_ubatch = 512; // physical batch size for prompt processing (must be >=32 to use BLAS) |
| int32_t n_keep = 0; // number of tokens to keep from initial prompt |
| |
| |
| |
| @@ -338,6 +338,14 @@ extern "C" { |
| uint32_t n_batch; // logical maximum batch size that can be submitted to llama_decode |
| uint32_t n_ubatch; // physical maximum batch size |
| uint32_t n_seq_max; // max number of sequences (i.e. distinct states for recurrent models) |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Initial KV-cell allocation per sequence. 0 = behave like Phase 1 |
| + // (allocate the full n_ctx_seq up front, only zero-fill is deferred). |
| + // Positive values cap the initial allocation at min(slot_initial_ctx, |
| + // n_ctx_seq); subsequent prompts that exceed this cap trigger a grow |
| + // up to next_pow2 (still capped at n_ctx_seq). Reduces warm-state RSS |
| + // when typical prompts are much shorter than n_ctx_seq. |
| + uint32_t slot_initial_ctx; |
| uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback (0 = no rollback) [EXPERIMENTAL] |
| int32_t n_threads; // number of threads to use for generation |
| int32_t n_threads_batch; // number of threads to use for batch processing |
| |
| |
| |
| @@ -58,6 +58,9 @@ llama_context::llama_context( |
| cparams.n_rs_seq = 0; |
| } |
| |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_initial_ctx = params.slot_initial_ctx; |
| + |
| cparams.n_threads = params.n_threads; |
| cparams.n_threads_batch = params.n_threads_batch; |
| cparams.yarn_ext_factor = params.yarn_ext_factor >= 0.0f ? params.yarn_ext_factor : hparams.yarn_ext_factor; |
| @@ -3346,6 +3349,7 @@ llama_context_params llama_context_default_params() { |
| /*.n_batch =*/ 2048, |
| /*.n_ubatch =*/ 512, |
| /*.n_seq_max =*/ 1, |
| + /*.slot_initial_ctx =*/ 0, |
| /*.n_rs_seq =*/ 0, |
| /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default |
| /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS, |
| |
| |
| |
| @@ -12,6 +12,10 @@ struct llama_cparams { |
| uint32_t n_batch; |
| uint32_t n_ubatch; |
| uint32_t n_seq_max; |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // 0 = full n_ctx_seq allocation. Positive value caps the initial KV-cell |
| + // allocation per sequence and enables grow-on-demand up to n_ctx_seq. |
| + uint32_t slot_initial_ctx; |
| uint32_t n_rs_seq; // number of recurrent-state snapshots per seq for rollback |
| int32_t n_threads; // number of threads to use for generation |
| int32_t n_threads_batch; // number of threads to use for batch processing |
| |
| |
| |
| @@ -20,6 +20,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa( |
| bool swa_full, |
| bool unified, |
| uint32_t kv_size, |
| + uint32_t kv_size_initial, |
| uint32_t n_seq_max, |
| uint32_t n_ubatch, |
| uint32_t n_pad, |
| @@ -61,14 +62,14 @@ llama_kv_cache_iswa::llama_kv_cache_iswa( |
| |
| kv_base = std::make_unique<llama_kv_cache>( |
| model, type_k, type_v, |
| - v_trans, offload, unified, size_base, n_seq_max, n_pad, |
| + v_trans, offload, unified, size_base, kv_size_initial, n_seq_max, n_pad, |
| 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse); |
| |
| LLAMA_LOG_INFO("%s: creating SWA KV cache, size = %u cells\n", __func__, size_swa); |
| |
| kv_swa = std::make_unique<llama_kv_cache>( |
| model, type_k, type_v, |
| - v_trans, offload, unified, size_swa, n_seq_max, n_pad, |
| + v_trans, offload, unified, size_swa, kv_size_initial, n_seq_max, n_pad, |
| hparams.n_swa, hparams.swa_type, filter_swa, reuse); |
| } |
| |
| |
| |
| |
| @@ -22,6 +22,9 @@ public: |
| bool swa_full, |
| bool unified, |
| uint32_t kv_size, |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Forwarded to both kv_base and kv_swa. 0 = full kv_size. |
| + uint32_t kv_size_initial, |
| uint32_t n_seq_max, |
| uint32_t n_ubatch, |
| uint32_t n_pad, |
| |
| |
| |
| @@ -85,6 +85,7 @@ llama_kv_cache::llama_kv_cache( |
| bool offload, |
| bool unified, |
| uint32_t kv_size, |
| + uint32_t kv_size_initial, |
| uint32_t n_seq_max, |
| uint32_t n_pad, |
| uint32_t n_swa, |
| @@ -96,6 +97,24 @@ llama_kv_cache::llama_kv_cache( |
| |
| GGML_ASSERT(kv_size % n_pad == 0); |
| |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Initialize the hard cap and the soft cap. kv_size_initial==0 (the |
| + // legacy / Phase-1 default) means "no soft cap" so the whole kv_size |
| + // is visible to find_slot. A positive value is clamped to [n_pad, |
| + // kv_size] and rounded up to the next n_pad multiple. |
| + kv_size_max = kv_size; |
| + if (kv_size_initial == 0) { |
| + kv_size_alloc = kv_size_max; |
| + } else { |
| + uint32_t cap = std::min(kv_size_initial, kv_size_max); |
| + cap = std::max(cap, n_pad); |
| + cap = ((cap + n_pad - 1) / n_pad) * n_pad; |
| + cap = std::min(cap, kv_size_max); |
| + kv_size_alloc = cap; |
| + LLAMA_LOG_INFO("%s: slot_initial_ctx = %u cells (cap = %u)\n", |
| + __func__, kv_size_alloc, kv_size_max); |
| + } |
| + |
| const uint32_t n_layer_kv = hparams.n_layer_kv(); |
| |
| // define a comparator for the buft -> ctx map to ensure that the order is well-defined: |
| @@ -287,7 +306,9 @@ llama_kv_cache::llama_kv_cache( |
| } |
| |
| if (model.hparams.no_alloc) { |
| - cleared = true; |
| + // opencoti F4 M3 Phase 1+2 β dummy buffers are size 0; treat all |
| + // cells as "already cleared" so ensure_cleared is a no-op. |
| + n_cells_cleared = kv_size_max; |
| } else { |
| LLAMA_LOG_INFO("%s: KV cache zero-fill deferred until first batch\n", __func__); |
| } |
| @@ -348,20 +369,81 @@ llama_kv_cache::llama_kv_cache( |
| debug = LLAMA_KV_CACHE_DEBUG ? atoi(LLAMA_KV_CACHE_DEBUG) : 0; |
| } |
| |
| -// opencoti F4 M3 Phase 1 β see docs/decisions/0001-lazy-slot-context.md |
| -void llama_kv_cache::ensure_cleared() { |
| - if (cleared) { |
| +// opencoti F4 M3 Phase 1+2 β see docs/decisions/0001-lazy-slot-context.md |
| +void llama_kv_cache::ensure_cleared(uint32_t up_to_cells) { |
| + // up_to_cells == 0 means "clear up to the current soft cap". Callers |
| + // that need full coverage (state_read) pass kv_size_max. |
| + const uint32_t target = up_to_cells == 0 |
| + ? kv_size_alloc |
| + : std::min(up_to_cells, kv_size_max); |
| + |
| + if (n_cells_cleared >= target) { |
| return; |
| } |
| |
| - for (auto & [_, buf] : ctxs_bufs) { |
| - LLAMA_LOG_INFO("%s: %10s KV buffer zero-fill = %8.2f MiB (deferred)\n", __func__, |
| - ggml_backend_buffer_name(buf.get()), |
| - ggml_backend_buffer_get_size(buf.get())/1024.0/1024.0); |
| - ggml_backend_buffer_clear(buf.get(), 0); |
| + const uint32_t a = n_cells_cleared; |
| + const uint32_t b = target; |
| + |
| + // Per-layer, per-stream partial memset of the K and V tensors. |
| + // ggml_backend_tensor_memset handles the host/device dispatch β for the |
| + // CPU backend (the only one the embedder uses today) this is a direct |
| + // memset on the malloc'd buffer; for offloaded buffers it's a backend |
| + // op. Either way only bytes for cells [a, b) get touched, so on Linux |
| + // only those pages get committed to RSS. |
| + for (const auto & layer : layers) { |
| + for (uint32_t st = 0; st < n_stream; ++st) { |
| + if (layer.k) { |
| + const size_t off = (size_t) st * (size_t) layer.k->nb[2] |
| + + (size_t) a * (size_t) layer.k->nb[1]; |
| + const size_t sz = (size_t) (b - a) * (size_t) layer.k->nb[1]; |
| + ggml_backend_tensor_memset(layer.k, 0, off, sz); |
| + } |
| + if (layer.v) { |
| + const size_t off = (size_t) st * (size_t) layer.v->nb[2] |
| + + (size_t) a * (size_t) layer.v->nb[1]; |
| + const size_t sz = (size_t) (b - a) * (size_t) layer.v->nb[1]; |
| + ggml_backend_tensor_memset(layer.v, 0, off, sz); |
| + } |
| + } |
| + } |
| + |
| + LLAMA_LOG_INFO("%s: cleared KV cells [%u, %u) of %u (%s)\n", |
| + __func__, a, b, kv_size_max, |
| + up_to_cells == 0 ? "soft cap" : "full"); |
| + |
| + n_cells_cleared = b; |
| +} |
| + |
| +// opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| +bool llama_kv_cache::ensure_capacity(uint32_t cells_needed) { |
| + if (cells_needed <= kv_size_alloc) { |
| + return true; |
| + } |
| + |
| + if (kv_size_alloc >= kv_size_max) { |
| + return false; |
| + } |
| + |
| + // Grow geometrically (next power-of-two) and align up to n_pad. The |
| + // pow2 ramp avoids re-growing every few tokens for long prompts; n_pad |
| + // alignment preserves the constructor's GGML_ASSERT(kv_size % n_pad == 0) |
| + // invariant for find_slot. |
| + uint32_t new_cap = std::max<uint32_t>(kv_size_alloc, n_pad); |
| + while (new_cap < cells_needed && new_cap < kv_size_max) { |
| + new_cap *= 2; |
| } |
| + new_cap = ((new_cap + n_pad - 1) / n_pad) * n_pad; |
| + new_cap = std::min(new_cap, kv_size_max); |
| |
| - cleared = true; |
| + if (new_cap == kv_size_alloc) { |
| + return cells_needed <= kv_size_alloc; |
| + } |
| + |
| + LLAMA_LOG_INFO("%s: grow soft cap %u -> %u (need %u, max %u)\n", |
| + __func__, kv_size_alloc, new_cap, cells_needed, kv_size_max); |
| + |
| + kv_size_alloc = new_cap; |
| + return cells_needed <= kv_size_alloc; |
| } |
| |
| void llama_kv_cache::clear(bool data) { |
| @@ -371,14 +453,16 @@ void llama_kv_cache::clear(bool data) { |
| } |
| |
| if (data) { |
| - // opencoti F4 M3 Phase 1: if the deferred zero-fill has not happened |
| - // yet there is nothing to clear β v_cells.reset() above already |
| - // satisfies the logical "data cleared" state. Don't commit RSS just |
| - // to zero pages that have never been written. |
| - if (cleared) { |
| - for (auto & [_, buf] : ctxs_bufs) { |
| - ggml_backend_buffer_clear(buf.get(), 0); |
| - } |
| + // opencoti F4 M3 Phase 1+2: if no zero-fill has happened yet there |
| + // is nothing to clear β v_cells.reset() above already satisfies the |
| + // logical "data cleared" state. Don't commit RSS just to zero pages |
| + // that have never been written. When a partial clear is already on |
| + // the books, re-issue it against the current soft cap so any cells |
| + // touched between the first ensure_cleared and now are re-zeroed. |
| + if (n_cells_cleared > 0) { |
| + const uint32_t b = n_cells_cleared; |
| + n_cells_cleared = 0; |
| + ensure_cleared(b); |
| } |
| } |
| } |
| @@ -743,10 +827,28 @@ llama_kv_cache::slot_info_vec_t llama_kv_cache::prepare(const std::vector<llama_ |
| |
| for (const auto & ubatch : ubatches) { |
| // only find a suitable slot for the ubatch. don't modify the cells yet |
| - const auto sinfo_new = find_slot(ubatch, false); |
| + auto sinfo_new = find_slot(ubatch, false); |
| if (sinfo_new.empty()) { |
| - success = false; |
| - break; |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Phase 2 keeps the soft alloc cap (kv_size_alloc) below kv_size_max |
| + // until a real ubatch needs more cells than the cap exposes. When |
| + // find_slot fails, try to grow the cap to cover what we already |
| + // know is used plus the ubatch's token count, then retry. The grow |
| + // is bounded by kv_size_max; if we are already at the hard cap the |
| + // failure is genuine and we propagate it up. |
| + uint32_t used_max = 0; |
| + for (uint32_t st = 0; st < n_stream; ++st) { |
| + used_max = std::max(used_max, v_cells[st].used_max_p1()); |
| + } |
| + const uint32_t want = used_max + ubatch.n_tokens; |
| + if (ensure_capacity(want)) { |
| + ensure_cleared(); |
| + sinfo_new = find_slot(ubatch, false); |
| + } |
| + if (sinfo_new.empty()) { |
| + success = false; |
| + break; |
| + } |
| } |
| |
| // remember the position that we found |
| @@ -981,14 +1083,20 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch, |
| |
| uint32_t head_cur = v_heads[seq_to_stream[seq_id]]; |
| |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // The Phase-1 search swept the full hard cap (cells.size() == kv_size_max). |
| + // Phase 2 caps it at the current soft alloc; prepare() grows the cap and |
| + // retries when the soft cap is too small for the requested ubatch. |
| + const uint32_t cap = kv_size_alloc; |
| + |
| // if we have enough unused cells before the current head -> |
| // better to start searching from the beginning of the cache, hoping to fill it |
| if (head_cur > cells.get_used() + 2*n_tokens) { |
| head_cur = 0; |
| } |
| |
| - if (n_tokens > cells.size()) { |
| - LLAMA_LOG_ERROR("%s: n_tokens = %d > size = %u\n", __func__, n_tokens, cells.size()); |
| + if (n_tokens > cap) { |
| + // not necessarily fatal β prepare() may grow the cap and retry |
| return { }; |
| } |
| |
| @@ -999,8 +1107,8 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch, |
| const uint32_t n_test = cont ? n_tokens : 1; |
| |
| while (true) { |
| - if (head_cur + n_test > cells.size()) { |
| - n_tested += cells.size() - head_cur; |
| + if (head_cur + n_test > cap) { |
| + n_tested += cap - head_cur; |
| head_cur = 0; |
| continue; |
| } |
| @@ -1058,7 +1166,7 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch, |
| res.idxs[s].clear(); |
| } |
| |
| - if (n_tested >= cells.size()) { |
| + if (n_tested >= cap) { |
| //LLAMA_LOG_ERROR("%s: failed to find a slot for %d tokens\n", __func__, n_tokens); |
| return { }; |
| } |
| @@ -1961,10 +2069,13 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama |
| |
| GGML_ASSERT(seq_id == -1 || (seq_id >= 0 && (size_t) seq_id < seq_to_stream.size())); |
| |
| - // opencoti F4 M3 Phase 1 β see docs/decisions/0001-lazy-slot-context.md |
| - // state_read writes the saved KV blob into our tensors, which requires |
| - // backing storage. Force the deferred allocation before we read. |
| - ensure_cleared(); |
| + // opencoti F4 M3 Phase 1+2 β see docs/decisions/0001-lazy-slot-context.md |
| + // state_read writes the saved KV blob into our tensors at arbitrary |
| + // offsets, so we cannot rely on the soft cap. Force a full-range |
| + // zero-fill before we read β this commits all KV pages but only on |
| + // the explicit deserialize path. |
| + ensure_capacity(kv_size_max); |
| + ensure_cleared(kv_size_max); |
| |
| uint32_t n_stream_cur; |
| io.read(&n_stream_cur, sizeof(n_stream_cur)); |
| |
| |
| |
| @@ -101,6 +101,11 @@ public: |
| bool offload, |
| bool unified, |
| uint32_t kv_size, |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Initial allocation cap. 0 means the same as kv_size |
| + // (Phase 1 behavior). Otherwise clamped to [n_pad, kv_size] |
| + // and rounded up to the next n_pad multiple. |
| + uint32_t kv_size_initial, |
| uint32_t n_seq_max, |
| uint32_t n_pad, |
| uint32_t n_swa, |
| @@ -152,15 +157,28 @@ public: |
| |
| bool get_has_shift() const; |
| |
| - // opencoti F4 M3 Phase 1 β see docs/decisions/0001-lazy-slot-context.md |
| - // Idempotent: zeroes the KV-cache backend buffers on first call. |
| - // The buffers are malloc'd eagerly at construction (so the scheduler |
| - // can resolve tensor->buffer at reserve time) but NOT cleared β on |
| - // Linux, malloc reserves address space without committing pages, so |
| - // RSS stays low until something writes. This method does the memset |
| - // that commits the pages. Called from inference entry points |
| - // (init_batch, prepare, update, state_read). |
| - void ensure_cleared(); |
| + // opencoti F4 M3 Phase 1+2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Idempotent. Zeroes cells [n_cells_cleared, up_to_cells) of every KV |
| + // layer's K and V tensors across every stream. The buffers are malloc'd |
| + // eagerly at construction (so the scheduler can resolve tensor->buffer |
| + // at reserve time) but their pages are not committed to RSS until they |
| + // are first written. Partial memset (via ggml_backend_tensor_memset) |
| + // commits only the requested range β that is the Phase 2 RSS win. |
| + // |
| + // up_to_cells == 0 means "use the current soft cap kv_size_alloc" |
| + // (the normal inference path). Pass kv_size_max for paths that need |
| + // full coverage (state_read). |
| + void ensure_cleared(uint32_t up_to_cells = 0); |
| + |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Grow the soft allocation cap. Returns true iff the cap covers |
| + // cells_needed after the grow. Bound by kv_size_max. Idempotent at |
| + // the cap. |
| + bool ensure_capacity(uint32_t cells_needed); |
| + |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // The current soft cap (cells visible to find_slot). |
| + uint32_t get_kv_size_alloc() const { return kv_size_alloc; } |
| |
| ggml_type type_k() const; |
| ggml_type type_v() const; |
| @@ -266,12 +284,22 @@ private: |
| // ggml contexts for the KV cache along with the allocated backend buffers: |
| std::vector<std::pair<ggml_context_ptr, ggml_backend_buffer_ptr>> ctxs_bufs; |
| |
| - // opencoti F4 M3 Phase 1 β see docs/decisions/0001-lazy-slot-context.md |
| - // True once the backend buffers in ctxs_bufs have been zero-filled. |
| - // The constructor sets this true for no_alloc models (whose dummy |
| - // buffers are size 0 β clearing is a no-op) and false otherwise. The |
| - // first ensure_cleared() call zeroes the buffers and flips this true. |
| - bool cleared = false; |
| + // opencoti F4 M3 Phase 1+2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Cells in the half-open range [0, n_cells_cleared) are guaranteed to |
| + // be zeroed across every layer's K and V tensors and every stream. |
| + // ensure_cleared(up_to) grows this monotonically up to kv_size_alloc |
| + // (or kv_size_max for the explicit-full call). The constructor sets |
| + // this to kv_size_max for no_alloc models (whose dummy buffers are |
| + // size 0 β clearing is a no-op) and 0 otherwise. |
| + uint32_t n_cells_cleared = 0; |
| + |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // kv_size_max: hard cap on cells (set from constructor's kv_size). |
| + // kv_size_alloc: current soft cap. find_slot only scans cells [0, kv_size_alloc). |
| + // Grows via ensure_capacity, never shrinks (Phase 2; Phase 3 |
| + // adds shrink-on-idle). |
| + uint32_t kv_size_max = 0; |
| + uint32_t kv_size_alloc = 0; |
| |
| // the current index from where we start searching for a free slot in the ring buffer of KV cells (see find_slot()) |
| // note: this is not part of the KV state and it's only used to speed-up the find_slot() method |
| |
| |
| |
| @@ -40,6 +40,10 @@ llama_memory_hybrid_iswa::llama_memory_hybrid_iswa( |
| swa_full, |
| unified, |
| kv_size, |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Hybrid-iswa does not yet thread slot_initial_ctx; pass 0 for |
| + // Phase 1 backward-compat (full eager allocation). |
| + 0, |
| n_seq_max, |
| n_ubatch, |
| n_pad, |
| |
| |
| |
| @@ -39,6 +39,10 @@ llama_memory_hybrid::llama_memory_hybrid( |
| offload, |
| unified, |
| kv_size, |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + // Hybrid memory does not yet thread slot_initial_ctx; pass 0 for |
| + // Phase 1 backward-compat (full eager allocation). |
| + 0, |
| n_seq_max, |
| n_pad, |
| n_swa, |
| |
| |
| |
| @@ -2073,6 +2073,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, |
| params.swa_full, |
| cparams.kv_unified, |
| cparams.n_ctx_seq, |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_initial_ctx, |
| cparams.n_seq_max, |
| cparams.n_ubatch, |
| 1, |
| @@ -2089,6 +2091,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, |
| cparams.offload_kqv, |
| cparams.kv_unified, |
| cparams.n_ctx_seq, |
| + // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_initial_ctx, |
| cparams.n_seq_max, |
| 1, |
| hparams.n_swa, |
|
|