perf(llm): route background tasks to dedicated model to preserve KV cache
Background tasks (title generation, tag suggestions, project summaries, RSS classification) were using qwen3:8b and wiping its KV cache after every response, preventing prefix cache hits on subsequent user messages. Adds OLLAMA_BACKGROUND_MODEL (default: qwen2.5:0.5b) config var and routes all background LLM calls to it, keeping qwen3:8b's KV cache warm between user messages for consistent sub-second TTFT. Also adds infinite scroll to KnowledgeView (replaces load-more button) and bakes spaCy en_core_web_sm into the Docker image to eliminate the pip install on every startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -249,6 +249,7 @@ def create_app() -> Quart:
|
||||
# Also ensure the embedding model is pulled (no warm needed).
|
||||
asyncio.create_task(_warm_user_models())
|
||||
asyncio.create_task(_pull_model(Config.EMBEDDING_MODEL, warm=False))
|
||||
asyncio.create_task(_pull_model(Config.OLLAMA_BACKGROUND_MODEL, warm=False))
|
||||
|
||||
# After models are pulled, backfill embeddings for existing notes.
|
||||
# Runs in the background so it never blocks the server from accepting requests.
|
||||
|
||||
@@ -24,6 +24,10 @@ class Config:
|
||||
)
|
||||
OLLAMA_URL: str = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
OLLAMA_MODEL: str = os.environ.get("OLLAMA_MODEL", "qwen3:latest")
|
||||
# Lightweight model for background tasks (title generation, tag suggestions,
|
||||
# project summaries, RSS classification). Using a separate model keeps the
|
||||
# main model's KV cache intact between user messages, enabling prefix cache hits.
|
||||
OLLAMA_BACKGROUND_MODEL: str = os.environ.get("OLLAMA_BACKGROUND_MODEL", "qwen2.5:0.5b")
|
||||
# KV cache context window for generation. Keep this as small as practical —
|
||||
# a larger context forces more KV cache into CPU RAM, drastically slowing prefill.
|
||||
# 16384 covers ~30+ message conversations with our system prompt comfortably.
|
||||
|
||||
@@ -238,7 +238,7 @@ async def save_response_as_note(user_id: int, message_id: int) -> dict:
|
||||
},
|
||||
{"role": "user", "content": msg.content[:2000]},
|
||||
]
|
||||
title = await generate_completion(prompt_messages, model)
|
||||
title = await generate_completion(prompt_messages, Config.OLLAMA_BACKGROUND_MODEL)
|
||||
title = title.strip().strip('"\'').strip()[:100]
|
||||
except Exception:
|
||||
logger.warning("Failed to generate note title, using fallback", exc_info=True)
|
||||
|
||||
@@ -138,7 +138,7 @@ async def _generate_title(messages: list[dict], model: str) -> str:
|
||||
},
|
||||
{"role": "user", "content": "\n\n".join(conv_lines)},
|
||||
]
|
||||
title = await generate_completion(prompt_messages, model, max_tokens=30)
|
||||
title = await generate_completion(prompt_messages, Config.OLLAMA_BACKGROUND_MODEL, max_tokens=30)
|
||||
title = title.strip().strip('"\'').strip()
|
||||
return title[:100] if title else ""
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ async def generate_project_summary(user_id: int, project_id: int) -> None:
|
||||
from fabledassistant.services.llm import generate_completion
|
||||
from fabledassistant.config import Config
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
summary = await generate_completion(messages, model=Config.OLLAMA_MODEL, max_tokens=400)
|
||||
summary = await generate_completion(messages, model=Config.OLLAMA_BACKGROUND_MODEL, max_tokens=400)
|
||||
if not summary:
|
||||
return
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ async def classify_items_batch(
|
||||
return {}
|
||||
|
||||
if model is None:
|
||||
model = Config.OLLAMA_MODEL
|
||||
model = Config.OLLAMA_BACKGROUND_MODEL
|
||||
|
||||
vocab = STANDARD_TOPICS + [t for t in user_include_topics if t not in STANDARD_TOPICS]
|
||||
items_block = "\n".join(
|
||||
|
||||
@@ -7,7 +7,6 @@ import re
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.llm import generate_completion
|
||||
from fabledassistant.services.notes import get_all_tags
|
||||
from fabledassistant.services.settings import get_setting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,7 +21,7 @@ async def suggest_tags(user_id: int, title: str, body: str, current_tags: list[s
|
||||
return []
|
||||
|
||||
existing_tags = await get_all_tags(user_id)
|
||||
model = await get_setting(user_id, "default_model", Config.OLLAMA_MODEL)
|
||||
model = Config.OLLAMA_BACKGROUND_MODEL
|
||||
|
||||
existing_list = ", ".join(f"#{t}" for t in existing_tags) if existing_tags else "(none yet)"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user