townlet / backend /factory.py
Budlee's picture
CPU fallback when ZeroGPU quota exhausted; GPU/CPU chip in topbar
856bab5 verified
Raw
History Blame Contribute Delete
1.92 kB
"""Resolves the active LLM (per model_id) + image backends based on runtime
config.
LLM backends are cached per-model_id so a character that gets its model
swapped doesn't trigger a reload, and so multiple characters on the same
model share one instance. The first call for a given model_id triggers the
GGUF download/load (Real) or instantiates a canned-style cycle (Mock).
The image backend is mock-only: Flux 2 was dropped from the pipeline (the
frontend never called /generate_background and the cold-load was eating
into the ZeroGPU quota). MockImageBackend renders a Pillow gradient — fine
as a placeholder if the endpoint ever gets called.
"""
from __future__ import annotations
import os
from game.models import DEFAULT_MODEL_ID, get_spec
from . import config
from .interfaces import ImageBackend, LLMBackend
_llms: dict[str, LLMBackend] = {}
_image: ImageBackend | None = None
def _cache_key(model_id: str) -> str:
"""GPU and CPU Llama instances are loaded with different `n_gpu_layers`
and can't be reused across modes. Cache them under separate keys so the
CPU-fallback path (parent process, TOWNLET_FORCE_CPU=1) and the GPU
path (forked workers, no env var) don't collide.
"""
mode = "cpu" if os.getenv("TOWNLET_FORCE_CPU") == "1" else "gpu"
return f"{model_id}::{mode}"
def get_llm(model_id: str = DEFAULT_MODEL_ID) -> LLMBackend:
key = _cache_key(model_id)
if key not in _llms:
get_spec(model_id) # validates id
if config.USE_REAL_MODELS:
from .real import RealLLMBackend
_llms[key] = RealLLMBackend(model_id)
else:
from .mock import MockLLMBackend
_llms[key] = MockLLMBackend(model_id)
return _llms[key]
def get_image() -> ImageBackend:
global _image
if _image is None:
from .mock import MockImageBackend
_image = MockImageBackend()
return _image