mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-26 09:26:55 -04:00
Fixes the paged-pool burst-degradation bug (OTHER_PATHS_INVESTIGATION.md section C Part 2): on a long-lived llama-server with LLAMA_KV_PAGED=1, a high-fan-out prefill burst strands KV blocks in the host-side paged pool, so a later lower-npl prefill draws from a depleted/fragmented pool and its throughput collapses (the benchmark's "restart per npl" crutch). Decode is unaffected. The fix changes only host-side block accounting and placement, never KV values or compute, and is gated behind LLAMA_KV_PAGED (LLAMA_PAGED_NO_RECLAIM=1 restores the pre-fix behavior). Fix-1 reclaim trailing blocks: PagedKVManager::truncate(seq, n_keep) frees every block beyond ceil(n_keep/bs) (ref-counted); called from llama_kv_cache::seq_rm for the p1==MAX && p0>0 partial-tail case so the manager tracks the kv-cache exactly. Fix-2 defrag on empty: when the pool is fully idle, defrag_free_pool() relinks the free queue into ascending block-id order (FreeBlockQueue::rebuild), preserving content-cache hashes. Fix-3 release on slot completion: server_slot::release() issues prompt_clear() under the paged engine so a finished-idle slot returns its blocks promptly. Validation (DGX GB10, q36-27b-nvfp4 = qwen35 hybrid; HEAD f7409c2 = patch 0023): - Bit-exact: greedy md5 identical across paged off / paged on / paged on+NO_RECLAIM (5951a5b4d624ce891e22ab5fca9bc439), == the 0023 baseline. test-backend-ops unaffected (no ggml op touched). - Host unit test: truncate reclaims exactly 16 trailing blocks; defrag restores ascending popleft order. UNIT PASS. - Model A/B (one binary, NO_RECLAIM): fragmentation prefill ratio 0.944 -> 0.998; 64 idle slots strand 2048 blocks, reclaim returns the pool to fresh (2527). - Server A/B (FRESH-npl8 -> BURST-npl64 -> POST-npl8): POST-npl8 prefill collapses 488 -> 44 t/s with NO_RECLAIM (the bug; investigation saw 507 -> 65), restored to 532 t/s (fresh 525, within 1%) with the fix. Paged release-log count 17 -> 96 (Fix-3 fires per slot completion). Canary tokens identical fresh-vs-post in both arms (bit-exact serving). Assisted-by: Claude:opus-4.8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
60 lines
2.7 KiB
C++
60 lines
2.7 KiB
C++
// Host-side unit test for the paged-pool burst-reclaim fix (patch 0024).
|
|
// Compiles paged-kv-manager.cpp directly; no ggml / llama / GPU dependency.
|
|
//
|
|
// Fix-1 PagedKVManager::truncate(seq, n_keep) reclaims the trailing blocks
|
|
// beyond ceil(n_keep/bs) (ref-counted), so a partial tail seq_rm no
|
|
// longer strands blocks whose cells were cleared.
|
|
// Fix-2 defrag_free_pool() relinks the free queue into ascending block-id
|
|
// order once the pool is fully idle, undoing a burst's scrambled frees
|
|
// so a later prefill pops physically contiguous blocks again.
|
|
|
|
#include "paged-kv-manager.h"
|
|
#include <cstdio>
|
|
|
|
using paged::PagedKVManager;
|
|
|
|
int main() {
|
|
int rc = 0;
|
|
|
|
// ---- Fix-1: truncate reclaims the trailing block suffix -----------------
|
|
{
|
|
PagedKVManager m(/*num_blocks=*/64, /*block_size=*/16, /*caching=*/true);
|
|
const size_t f0 = m.num_free_blocks(); // 63 (block 0 reserved as null)
|
|
m.allocate(0, 512); // ceil(512/16)=32 blocks
|
|
const size_t f1 = m.num_free_blocks(); // 31
|
|
m.truncate(0, 256); // keep ceil(256/16)=16, free 16
|
|
const size_t f2 = m.num_free_blocks(); // 47
|
|
printf("[unit Fix-1] free=%zu alloc512=%zu truncate256=%zu reclaimed=%zu (expect 16)\n",
|
|
f0, f1, f2, f2 - f1);
|
|
if (f2 - f1 != 16) rc = 1;
|
|
m.truncate(0, 16); // keep 1 block, free 15 more
|
|
const size_t f3 = m.num_free_blocks(); // 62
|
|
printf("[unit Fix-1] truncate16=%zu (expect %zu)\n", f3, f0 - 1);
|
|
if (f3 != f0 - 1) rc = 1;
|
|
m.free(0);
|
|
if (m.num_free_blocks() != f0) { printf("[unit Fix-1] free mismatch\n"); rc = 1; }
|
|
}
|
|
|
|
// ---- Fix-2: defrag restores ascending popleft order ---------------------
|
|
{
|
|
PagedKVManager m(/*num_blocks=*/64, /*block_size=*/16, /*caching=*/false);
|
|
for (int s = 0; s < 8; ++s) m.allocate(s, 16); // pop blocks 1..8
|
|
const int scrambled[8] = {3, 7, 1, 5, 0, 6, 2, 4}; // free out of order
|
|
for (int i = 0; i < 8; ++i) m.free(scrambled[i]);
|
|
m.defrag_free_pool(); // all idle -> compact
|
|
m.allocate(100, 16 * 3); // pop 3 blocks
|
|
const auto bt = m.block_table(100);
|
|
bool asc = true;
|
|
printf("[unit Fix-2] post-defrag block_table:");
|
|
for (size_t i = 0; i < bt.size(); ++i) {
|
|
printf(" %d", bt[i]);
|
|
if (i && bt[i] < bt[i - 1]) asc = false;
|
|
}
|
|
printf(" ascending=%s (expect YES)\n", asc ? "YES" : "NO");
|
|
if (!asc) rc = 1;
|
|
}
|
|
|
|
printf("UNIT %s\n", rc == 0 ? "PASS" : "FAIL");
|
|
return rc;
|
|
}
|