feat(llm): adaptive num_ctx tiers + fix KV cache priming num_ctx mismatch
Adds pick_num_ctx() which selects the smallest context window tier (8192, 16384, 32768) that fits the current messages with 25% headroom, capped at OLLAMA_NUM_CTX. Threads num_ctx through generation_task.py so every chat request uses the computed tier rather than a fixed 16384. Fixes a critical cache miss bug: KV cache priming in app.py and settings.py was sending requests without num_ctx, so Ollama sized the cache at its model default (different from the 16384 real requests used), forcing a full model reload on the first real user message. Both priming sites now call pick_num_ctx() and pass the matching value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ from fabledassistant.services.generation_buffer import (
|
||||
GenerationBuffer,
|
||||
GenerationState,
|
||||
)
|
||||
from fabledassistant.services.llm import ChatChunk, build_context, generate_completion, stream_chat, stream_chat_with_tools, summarize_history_for_context
|
||||
from fabledassistant.services.llm import ChatChunk, build_context, generate_completion, pick_num_ctx, stream_chat, stream_chat_with_tools, summarize_history_for_context
|
||||
from fabledassistant.services.chat import update_conversation_title
|
||||
from fabledassistant.services.settings import get_setting
|
||||
from fabledassistant.services.logging import log_generation
|
||||
@@ -168,6 +168,7 @@ async def _stream_with_retry(
|
||||
model: str,
|
||||
tools: list[dict],
|
||||
think: bool,
|
||||
num_ctx: int | None = None,
|
||||
) -> AsyncGenerator[ChatChunk, None]:
|
||||
"""stream_chat_with_tools with automatic retry on Ollama 500 errors.
|
||||
|
||||
@@ -184,7 +185,7 @@ async def _stream_with_retry(
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
async for chunk in stream_chat_with_tools(messages, model, tools=tools, think=think):
|
||||
async for chunk in stream_chat_with_tools(messages, model, tools=tools, think=think, num_ctx=num_ctx):
|
||||
yield chunk
|
||||
return
|
||||
except httpx.HTTPStatusError as exc:
|
||||
@@ -260,6 +261,11 @@ async def run_generation(
|
||||
voice_speech_style=voice_speech_style,
|
||||
)
|
||||
|
||||
# Pick the smallest context tier that fits the current messages.
|
||||
# Using the minimum needed tier reduces KV cache size and speeds up prefill.
|
||||
num_ctx = pick_num_ctx(messages)
|
||||
logger.debug("Adaptive num_ctx=%d for conv %d", num_ctx, conv_id)
|
||||
|
||||
# Emit context event
|
||||
buf.append_event("context", {"context": context_meta})
|
||||
|
||||
@@ -269,6 +275,7 @@ async def run_generation(
|
||||
t_start = time.monotonic()
|
||||
timing: dict = {
|
||||
"think": think,
|
||||
"num_ctx": num_ctx,
|
||||
"tools": [],
|
||||
"rounds": 0,
|
||||
"prompt_tokens": None,
|
||||
@@ -298,7 +305,7 @@ async def run_generation(
|
||||
buf.append_event("status", {"status": "Generating response..." if _round == 0 else "Composing response..."})
|
||||
t_stream = time.monotonic()
|
||||
|
||||
async for chunk in _stream_with_retry(messages, model, tools, think):
|
||||
async for chunk in _stream_with_retry(messages, model, tools, think, num_ctx=num_ctx):
|
||||
if buf.cancel_event.is_set():
|
||||
cancelled = True
|
||||
break
|
||||
@@ -535,7 +542,7 @@ async def run_assist_generation(
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
buf.content_so_far = ""
|
||||
async for chunk in stream_chat(messages, model, options={"num_predict": Config.OLLAMA_NUM_CTX}):
|
||||
async for chunk in stream_chat(messages, model, options={"num_predict": num_ctx}, num_ctx=num_ctx):
|
||||
buf.content_so_far += chunk
|
||||
buf.append_event("chunk", {"chunk": chunk})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user