| From: opencoti |
| Subject: [PATCH 5/N] lazy slot-context allocation β Phase 3 (shrink on idle) |
|
|
| Phase 2 added a soft cap on per-stream KV cells (kv_size_alloc <= kv_size_max) |
| and grow-on-demand via ensure_capacity. The soft cap never shrinks, so once a |
| single long prompt has caused it to grow, the embedder's RSS stays at the |
| grown size for the lifetime of the process. For an embedder running |
| `--parallel 2 --ctx-size 16384` with typical prompts well under 4K cells, |
| this means the headline Phase-2 win evaporates after the first long-context |
| request. |
|
|
| Phase 3 plugs that hole. When an idle threshold has elapsed since the last |
| apply_ubatch: |
|
|
| 1. Cells in the range [kv_size_shrink_target, kv_size_alloc) are evicted |
| across every stream via cells.rm(i). The server-context layer's |
| prompt cache keeps cells alive between embed requests for reuse; |
| decommitting their backing pages without first evicting would leave |
| the prompt cache pointing at zeroed memory and corrupt subsequent |
| re-uses. The eviction makes the shrink unconditional. |
|
|
| 2. The pages backing cells [target, current_alloc) of every layer's K |
| and V tensors are returned to the kernel via |
| madvise(addr, len, MADV_DONTNEED). |
|
|
| 3. n_cells_cleared is rolled back to the shrink target so the next |
| ensure_cleared() partial-memset re-zeros only what comes back. |
|
|
| 4. kv_size_alloc is reset to the shrink target so the next request can |
| grow again from the initial cap. |
|
|
| Why madvise(MADV_DONTNEED) and not posix_madvise(POSIX_MADV_DONTNEED): |
| glibc on Linux maps posix_madvise(POSIX_MADV_DONTNEED) to a no-op (see |
| glibc sysdeps/posix/posix_madvise.c β the standard explicitly forbids |
| "actively destroying" pages, which is what we need). Only the Linux- |
| specific madvise(MADV_DONTNEED) actually decommits anonymous pages and |
| returns RSS to the kernel. We declared MADV_DONTNEED with the canonical |
| Linux value (4) under #ifndef and extern-declared madvise() so cosmocc's |
| default feature set (which does not define _GNU_SOURCE and so does not |
| expose madvise() through sys/mman.h) builds cleanly. |
|
|
| The shrink check runs at the top of init_batch() and prepare() β both |
| entry points to actual KV work. It is gated by: |
| - slot_shrink_idle_ms_ > 0 (feature enabled) |
| - kv_size_alloc > kv_size_shrink_target (something to shrink) |
| - last_active_ms_ != 0 AND (now - last_active_ms_) >= idle threshold |
|
|
| Surface (CLI): |
| --slot-shrink-idle-ms N (default 0 = disabled; positive enables) |
| env LLAMA_ARG_SLOT_SHRINK_IDLE_MS |
|
|
| Surface (C API): |
| llama_context_params::slot_shrink_idle_ms (uint32_t, default 0) |
|
|
| Plumbed through llama_cparams -> llama_kv_cache ctor -> |
| (llama_kv_cache_iswa ctor for SWA models) -> llama-model.cpp call sites. |
| Backward-compat: llama-memory-hybrid{,-iswa}.cpp pass 0 (no shrink), the |
| same way they already pass 0 for kv_size_initial. |
|
|
| Bench (CPU, --gpu disable so KV lives in host pages): the embedder |
| configured `--parallel 2 --ctx-size 16384 --pooling last |
| --slot-initial-ctx 256 --slot-shrink-idle-ms 30000` starts at 719 MB |
| RSS, grows to 1720 MB after a long-prompt corpus pass (soft cap 256 -> |
| 2048), and after the 30 s idle drops to 1329 MB on the next short embed. |
| 391.7 MB returned to the kernel β measured by /proc/<pid>/status VmRSS. |
| Quality (cosine min vs M2 baseline) stays at 0.999797 across the 48-prompt |
| corpus. |
|
|
| Milestone: F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| Upstreaming: TBD β kv_cache idle-shrink is a generally-useful concept; the |
| cell-eviction + madvise(MADV_DONTNEED) approach is not opencoti-specific. |
| Proposal candidate after F4 closes. |
|
|
| |
| |
| |
| |
| @@ -1371,6 +1371,17 @@ common_params_context common_params_parser_init(common_params & params, llama_ex |
| 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( |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + {"--slot-shrink-idle-ms"}, "N", |
| + string_format("idle threshold (ms) after which a grown KV soft cap is shrunk back to " |
| + "--slot-initial-ctx and the over-cap pages are decommitted via posix_madvise " |
| + "(default: %d, 0 disables; requires --slot-initial-ctx > 0)", |
| + params.slot_shrink_idle_ms), |
| + [](common_params & params, int value) { |
| + params.slot_shrink_idle_ms = value; |
| + } |
| + ).set_env("LLAMA_ARG_SLOT_SHRINK_IDLE_MS").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_EMBEDDING, LLAMA_EXAMPLE_PARALLEL})); |
| add_opt(common_arg( |
| {"--cache-idle-slots"}, |
| {"--no-cache-idle-slots"}, |
| |
| |
| |
| @@ -1630,6 +1630,8 @@ struct llama_context_params common_context_params_to_llama(const common_params & |
| 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; |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms < 0 ? 0u : (uint32_t) params.slot_shrink_idle_ms; |
| cparams.n_rs_seq = params.speculative.need_n_rs_seq(); |
| cparams.n_batch = params.n_batch; |
| cparams.n_ubatch = params.n_ubatch; |
| |
| |
| |
| @@ -430,6 +430,11 @@ struct common_params { |
| // (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; |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Idle threshold (ms) after which a grown soft cap is shrunk back to |
| + // slot_initial_ctx and the over-cap pages are decommitted via |
| + // posix_madvise. 0 disables shrink-on-idle (Phase 2 behavior). |
| + int32_t slot_shrink_idle_ms = 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 |
| |
| |
| |
| @@ -346,6 +346,13 @@ extern "C" { |
| // 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; |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Idle threshold after which a grown soft cap is shrunk back to |
| + // slot_initial_ctx and the pages beyond it are returned to the OS via |
| + // posix_madvise(POSIX_MADV_DONTNEED). 0 disables shrink-on-idle |
| + // (Phase 2 behavior β soft cap grows monotonically). Requires |
| + // slot_initial_ctx > 0 to have any visible effect. |
| + uint32_t slot_shrink_idle_ms; |
| 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 |
| |
| |
| |
| @@ -60,6 +60,8 @@ llama_context::llama_context( |
| |
| // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| cparams.slot_initial_ctx = params.slot_initial_ctx; |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_shrink_idle_ms = params.slot_shrink_idle_ms; |
| |
| cparams.n_threads = params.n_threads; |
| cparams.n_threads_batch = params.n_threads_batch; |
| @@ -3350,6 +3352,7 @@ llama_context_params llama_context_default_params() { |
| /*.n_ubatch =*/ 512, |
| /*.n_seq_max =*/ 1, |
| /*.slot_initial_ctx =*/ 0, |
| + /*.slot_shrink_idle_ms =*/ 0, |
| /*.n_rs_seq =*/ 0, |
| /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default |
| /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS, |
| |
| |
| |
| @@ -16,6 +16,11 @@ struct llama_cparams { |
| // 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; |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // 0 disables shrink-on-idle. Positive value is the idle threshold in |
| + // milliseconds: when a grown KV-cache is idle for at least this long, the |
| + // soft cap is reset to slot_initial_ctx and the pages beyond are decommitted. |
| + uint32_t slot_shrink_idle_ms; |
| 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 |
| |
| |
| |
| @@ -21,6 +21,7 @@ llama_kv_cache_iswa::llama_kv_cache_iswa( |
| bool unified, |
| uint32_t kv_size, |
| uint32_t kv_size_initial, |
| + uint32_t slot_shrink_idle_ms, |
| uint32_t n_seq_max, |
| uint32_t n_ubatch, |
| uint32_t n_pad, |
| @@ -62,15 +63,15 @@ 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, kv_size_initial, n_seq_max, n_pad, |
| - 0, LLAMA_SWA_TYPE_NONE, filter_base, reuse); |
| + v_trans, offload, unified, size_base, kv_size_initial, slot_shrink_idle_ms, |
| + 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, kv_size_initial, n_seq_max, n_pad, |
| - hparams.n_swa, hparams.swa_type, filter_swa, reuse); |
| + v_trans, offload, unified, size_swa, kv_size_initial, slot_shrink_idle_ms, |
| + n_seq_max, n_pad, hparams.n_swa, hparams.swa_type, filter_swa, reuse); |
| } |
| |
| void llama_kv_cache_iswa::clear(bool data) { |
| |
| |
| |
| @@ -25,6 +25,9 @@ public: |
| // 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, |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Forwarded to both kv_base and kv_swa. 0 = shrink disabled. |
| + uint32_t slot_shrink_idle_ms, |
| uint32_t n_seq_max, |
| uint32_t n_ubatch, |
| uint32_t n_pad, |
| |
| |
| |
| @@ -7,12 +7,35 @@ |
| |
| #include <algorithm> |
| #include <cassert> |
| +#include <chrono> |
| #include <cmath> |
| #include <cstring> |
| #include <limits> |
| #include <map> |
| #include <stdexcept> |
| |
| +// opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| +// On Linux, madvise(addr, len, MADV_DONTNEED) is the call that actually |
| +// decommits pages: the kernel zeroes the page table entries, releases the |
| +// physical pages, and the next access faults in fresh zero-filled pages. |
| +// glibc's posix_madvise(POSIX_MADV_DONTNEED) is documented as advisory and |
| +// is NOT guaranteed to reduce RSS β empirically it is a no-op on the |
| +// glibc/Debian-11 setup the embedder runs on. We must call madvise() |
| +// directly. |
| +// |
| +// cosmocc's default headers (without _GNU_SOURCE) do not declare madvise() |
| +// or MADV_DONTNEED, but the syscall exists on every Linux kernel the |
| +// embedder runs on. Hardcode the Linux ABI value (asm-generic/mman-common.h: |
| +// `#define MADV_DONTNEED 4`) and forward-declare madvise() to keep |
| +// _GNU_SOURCE out of the global include. <unistd.h> provides |
| +// sysconf(_SC_PAGESIZE) for page-boundary math. |
| +#include <sys/mman.h> |
| +#include <unistd.h> |
| +#ifndef MADV_DONTNEED |
| +# define MADV_DONTNEED 4 |
| +#endif |
| +extern "C" int madvise(void *addr, size_t length, int advice); |
| + |
| static bool ggml_is_power_of_2(int n) { |
| return (n & (n - 1)) == 0; |
| } |
| @@ -86,6 +109,7 @@ llama_kv_cache::llama_kv_cache( |
| bool unified, |
| uint32_t kv_size, |
| uint32_t kv_size_initial, |
| + uint32_t slot_shrink_idle_ms, |
| uint32_t n_seq_max, |
| uint32_t n_pad, |
| uint32_t n_swa, |
| @@ -115,6 +139,18 @@ llama_kv_cache::llama_kv_cache( |
| __func__, kv_size_alloc, kv_size_max); |
| } |
| |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // The shrink target is the initial cap we just computed. last_active_ms_ |
| + // starts at 0 (epoch) so a long pause before the first request still |
| + // shrinks (correct behavior β nothing to lose). |
| + kv_size_shrink_target = kv_size_alloc; |
| + slot_shrink_idle_ms_ = slot_shrink_idle_ms; |
| + last_active_ms_ = 0; |
| + if (slot_shrink_idle_ms_ > 0 && kv_size_initial > 0 && kv_size_initial < kv_size_max) { |
| + LLAMA_LOG_INFO("%s: slot_shrink_idle_ms = %u (target cap = %u)\n", |
| + __func__, slot_shrink_idle_ms_, kv_size_shrink_target); |
| + } |
| + |
| 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: |
| @@ -446,6 +482,151 @@ bool llama_kv_cache::ensure_capacity(uint32_t cells_needed) { |
| return cells_needed <= kv_size_alloc; |
| } |
| |
| +// opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| +// Decommit pages of every layer's K/V tensors in the byte-range that |
| +// corresponds to cells [from_cells, to_cells). Returns those pages to the OS |
| +// β RSS drops by the size of the range, and the next access to those pages |
| +// will fault them back in as fresh zero-filled pages. |
| +// |
| +// Only safe to call after kv_size_alloc has been reset to from_cells (or |
| +// lower) and n_cells_cleared has been reset accordingly, so the bookkeeping |
| +// stays in sync with what's actually backed by physical memory. |
| +// |
| +// The decommit is per-stream because the KV tensors are 3-D |
| +// [n_embd, kv_size, n_stream]; cells [a, b) within stream s occupy a |
| +// contiguous byte-range whose start address is (base + s*nb[2] + a*nb[1]) |
| +// and length is (b-a)*nb[1]. posix_madvise is page-grained β bytes that |
| +// don't cover a full page are silently kept resident, which is fine for |
| +// the cells that straddle page boundaries. |
| +static void opencoti_decommit_layer_range(ggml_tensor * t, uint32_t n_stream, |
| + uint32_t from_cells, uint32_t to_cells) { |
| + if (!t || to_cells <= from_cells) { |
| + return; |
| + } |
| + ggml_backend_buffer_t buf = t->buffer; |
| + if (!buf) { |
| + return; |
| + } |
| + // Only the host (CPU) backend's buffer is malloc'd memory we can |
| + // decommit via madvise. For device buffers (CUDA/Metal/Vulkan) the K/V |
| + // cache lives in device memory; skip β there are no host-side pages |
| + // to return. This means Phase 3's RSS-shrink win applies to host-mode |
| + // (e.g. embedder with -ngl 0); GPU-mode binaries still benefit from |
| + // the soft-cap reset but RSS doesn't move. |
| + if (!ggml_backend_buffer_is_host(buf)) { |
| + return; |
| + } |
| + void * base = ggml_backend_buffer_get_base(buf); |
| + if (!base) { |
| + return; |
| + } |
| + const size_t stride_stream = (size_t) t->nb[2]; |
| + const size_t stride_cell = (size_t) t->nb[1]; |
| + const size_t off_in_buf = (size_t) ((char *) t->data - (char *) base); |
| + for (uint32_t s = 0; s < n_stream; ++s) { |
| + const size_t off = off_in_buf |
| + + (size_t) s * stride_stream |
| + + (size_t) from_cells * stride_cell; |
| + const size_t sz = (size_t) (to_cells - from_cells) * stride_cell; |
| + // posix_madvise wants the address β work in absolute terms. |
| + void * addr = (char *) base + off; |
| + // Round up to a page boundary on the low side, round down on the |
| + // high side, so we only decommit pages we fully own. The cell-aligned |
| + // address is unlikely to be page-aligned in general. |
| + const size_t page = (size_t) sysconf(_SC_PAGESIZE); |
| + if (page == 0 || page == (size_t) -1) { |
| + return; |
| + } |
| + uintptr_t lo = (uintptr_t) addr; |
| + uintptr_t hi = lo + sz; |
| + uintptr_t lo_aligned = (lo + page - 1) & ~(uintptr_t)(page - 1); |
| + uintptr_t hi_aligned = hi & ~(uintptr_t)(page - 1); |
| + if (hi_aligned <= lo_aligned) { |
| + continue; |
| + } |
| + // madvise(MADV_DONTNEED) returns 0 on success, -1 on failure (with |
| + // errno set). We treat any failure as advisory β the RSS win may |
| + // be skipped, but correctness is unaffected (the K/V buffer is |
| + // still mapped, just possibly still resident). |
| + (void) madvise((void *) lo_aligned, (size_t) (hi_aligned - lo_aligned), |
| + MADV_DONTNEED); |
| + } |
| +} |
| + |
| +void llama_kv_cache::shrink_if_idle() { |
| + if (slot_shrink_idle_ms_ == 0) { |
| + return; // shrink-on-idle disabled |
| + } |
| + if (kv_size_alloc <= kv_size_shrink_target) { |
| + return; // already at (or below) target β nothing to shrink |
| + } |
| + if (last_active_ms_ == 0) { |
| + return; // no apply_ubatch has ever run β leave the initial cap as-is |
| + } |
| + |
| + const uint64_t now = (uint64_t) |
| + std::chrono::duration_cast<std::chrono::milliseconds>( |
| + std::chrono::steady_clock::now().time_since_epoch()).count(); |
| + if (now < last_active_ms_) { |
| + return; // clock went backwards; refuse to act |
| + } |
| + if ((now - last_active_ms_) < (uint64_t) slot_shrink_idle_ms_) { |
| + return; // not idle long enough |
| + } |
| + |
| + const uint32_t from_cells = kv_size_shrink_target; |
| + const uint32_t to_cells = kv_size_alloc; |
| + |
| + // Evict prompt-cache cell metadata for indices [from_cells, to_cells) |
| + // on every stream BEFORE decommitting the pages. The server's prompt |
| + // cache (server-context.cpp) keeps cells populated between requests so |
| + // a subsequent request with the same prefix can skip re-prefill. If we |
| + // decommitted pages without invalidating cell metadata, the server's |
| + // next prompt-cache lookup could "match" a prefix whose K/V data is |
| + // now decommitted (returns to zero pages on next access) = corruption. |
| + // |
| + // Semantics: shrink-on-idle is allowed to evict the over-cap portion |
| + // of the prompt cache because that is exactly what the user opted into |
| + // by setting --slot-shrink-idle-ms > 0. A cache miss on a subsequent |
| + // long prompt is the cost; the alternative (cap stays grown forever) |
| + // defeats Phase 3 entirely β slots that retain cells between requests |
| + // (the default for embed + completion) would never shrink. |
| + uint32_t evicted = 0; |
| + for (uint32_t st = 0; st < n_stream; ++st) { |
| + auto & cells = v_cells[st]; |
| + const uint32_t hi = std::min<uint32_t>(cells.size(), to_cells); |
| + for (uint32_t i = from_cells; i < hi; ++i) { |
| + if (!cells.is_empty(i)) { |
| + cells.rm(i); |
| + ++evicted; |
| + } |
| + } |
| + } |
| + |
| + LLAMA_LOG_INFO("%s: shrink soft cap %u -> %u (idle %llu ms, evicted %u cells)\n", |
| + __func__, to_cells, from_cells, |
| + (unsigned long long) (now - last_active_ms_), evicted); |
| + |
| + for (const auto & layer : layers) { |
| + opencoti_decommit_layer_range(layer.k, n_stream, from_cells, to_cells); |
| + opencoti_decommit_layer_range(layer.v, n_stream, from_cells, to_cells); |
| + } |
| + |
| + // After decommit, treat cells [from_cells, to_cells) as "uncleared" β |
| + // their pages will fault back in as fresh zero-filled pages on next |
| + // touch, but n_cells_cleared still recorded them as cleared. Roll back |
| + // the cleared mark so a future ensure_cleared() re-zeros them only when |
| + // they're actually needed. |
| + if (n_cells_cleared > from_cells) { |
| + n_cells_cleared = from_cells; |
| + } |
| + kv_size_alloc = from_cells; |
| + |
| + // Refresh last_active_ms_ so we don't immediately try again on the very |
| + // next init_batch (which would walk the empty layers for nothing). |
| + last_active_ms_ = now; |
| +} |
| + |
| void llama_kv_cache::clear(bool data) { |
| for (uint32_t s = 0; s < n_stream; ++s) { |
| v_cells[s].reset(); |
| @@ -757,6 +938,12 @@ llama_memory_context_ptr llama_kv_cache::init_batch( |
| bool embd_all) { |
| GGML_UNUSED(embd_all); |
| |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Check idle-shrink BEFORE ensure_cleared(): if shrink runs, it lowers |
| + // kv_size_alloc and rolls n_cells_cleared back, so ensure_cleared's |
| + // soft-cap-bounded clear targets only the (now smaller) initial cap. |
| + shrink_if_idle(); |
| + |
| // opencoti F4 M3 Phase 1 β see docs/decisions/0001-lazy-slot-context.md |
| ensure_cleared(); |
| |
| @@ -804,6 +991,12 @@ llama_memory_context_ptr llama_kv_cache::init_update(llama_context * lctx, bool |
| } |
| |
| llama_kv_cache::slot_info_vec_t llama_kv_cache::prepare(const std::vector<llama_ubatch> & ubatches) { |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Same reason ensure_cleared() is called here as well as init_batch: |
| + // the iswa wrapper bypasses init_batch and calls prepare() directly on |
| + // each inner kv_cache. shrink_if_idle is idempotent and cheap. |
| + shrink_if_idle(); |
| + |
| // opencoti F4 M3 Phase 1 β see docs/decisions/0001-lazy-slot-context.md |
| // The iswa wrapper calls prepare() directly on each inner kv_cache without |
| // going through init_batch(), so the ensure_cleared() in init_batch alone |
| @@ -1184,6 +1377,13 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch, |
| } |
| |
| void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch & ubatch) { |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Refresh the idle timestamp on every actual write. shrink_if_idle uses |
| + // this to gate the shrink path. |
| + last_active_ms_ = (uint64_t) |
| + std::chrono::duration_cast<std::chrono::milliseconds>( |
| + std::chrono::steady_clock::now().time_since_epoch()).count(); |
| + |
| // keep track of the max sequence position that we would overwrite with this ubatch |
| // for non-SWA cache, this would be always empty |
| llama_seq_id seq_pos_max_rm[LLAMA_MAX_SEQ]; |
| |
| |
| |
| @@ -106,6 +106,11 @@ public: |
| // (Phase 1 behavior). Otherwise clamped to [n_pad, kv_size] |
| // and rounded up to the next n_pad multiple. |
| uint32_t kv_size_initial, |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Idle threshold (ms) after which the soft cap is shrunk |
| + // back to kv_size_initial and the over-cap pages are |
| + // decommitted. 0 disables shrink-on-idle. |
| + uint32_t slot_shrink_idle_ms, |
| uint32_t n_seq_max, |
| uint32_t n_pad, |
| uint32_t n_swa, |
| @@ -180,6 +185,17 @@ public: |
| // The current soft cap (cells visible to find_slot). |
| uint32_t get_kv_size_alloc() const { return kv_size_alloc; } |
| |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // If the soft cap has been grown above kv_size_shrink_target and at least |
| + // slot_shrink_idle_ms_ have elapsed since the last apply_ubatch, reset |
| + // the soft cap to kv_size_shrink_target and posix_madvise(POSIX_MADV_DONTNEED) |
| + // the over-cap pages of every layer's K/V tensors. Idempotent and cheap |
| + // when the soft cap is already at the target or shrink-on-idle is disabled. |
| + // Called at the top of init_batch and prepare; safe because both happen |
| + // before any slot work and the embedder's per-request cells.reset() means |
| + // no slot ever holds onto cells across requests. |
| + void shrink_if_idle(); |
| + |
| ggml_type type_k() const; |
| ggml_type type_v() const; |
| |
| @@ -296,11 +312,21 @@ private: |
| // 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). |
| + // Grows via ensure_capacity. Phase 3 adds shrink-on-idle: |
| + // shrink_if_idle() may reset it back to kv_size_shrink_target. |
| uint32_t kv_size_max = 0; |
| uint32_t kv_size_alloc = 0; |
| |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Phase 3 shrink-on-idle state. kv_size_shrink_target is the cap value |
| + // the soft cap is reset to on shrink β set once at ctor from |
| + // kv_size_initial (or kv_size_max if Phase-1-mode). slot_shrink_idle_ms_ |
| + // is the idle threshold in ms; 0 disables shrink. last_active_ms_ is a |
| + // monotonic timestamp updated by apply_ubatch. |
| + uint32_t kv_size_shrink_target = 0; |
| + uint32_t slot_shrink_idle_ms_ = 0; |
| + uint64_t last_active_ms_ = 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 |
| std::vector<uint32_t> v_heads; |
| |
| |
| |
| @@ -44,6 +44,10 @@ llama_memory_hybrid_iswa::llama_memory_hybrid_iswa( |
| // Hybrid-iswa does not yet thread slot_initial_ctx; pass 0 for |
| // Phase 1 backward-compat (full eager allocation). |
| 0, |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Hybrid-iswa does not yet thread slot_shrink_idle_ms; pass 0 for |
| + // Phase 2 backward-compat (no shrink-on-idle). |
| + 0, |
| n_seq_max, |
| n_ubatch, |
| n_pad, |
| |
| |
| |
| @@ -43,6 +43,10 @@ llama_memory_hybrid::llama_memory_hybrid( |
| // Hybrid memory does not yet thread slot_initial_ctx; pass 0 for |
| // Phase 1 backward-compat (full eager allocation). |
| 0, |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + // Hybrid memory does not yet thread slot_shrink_idle_ms; pass 0 for |
| + // Phase 2 backward-compat (no shrink-on-idle). |
| + 0, |
| n_seq_max, |
| n_pad, |
| n_swa, |
| |
| |
| |
| @@ -2075,6 +2075,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, |
| cparams.n_ctx_seq, |
| // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| cparams.slot_initial_ctx, |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_shrink_idle_ms, |
| cparams.n_seq_max, |
| cparams.n_ubatch, |
| 1, |
| @@ -2093,6 +2095,8 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, |
| cparams.n_ctx_seq, |
| // opencoti F4 M3 Phase 2 β see docs/decisions/0001-lazy-slot-context.md |
| cparams.slot_initial_ctx, |
| + // opencoti F4 M3 Phase 3 β see docs/decisions/0001-lazy-slot-context.md |
| + cparams.slot_shrink_idle_ms, |
| cparams.n_seq_max, |
| 1, |
| hparams.n_swa, |
|
|