Garder chaud le préfixe du chat entre deux questions (20 septembre 2026).
Le préfixe — identité + les 45 schémas de la trousse, 10 112 jetons rapportés
par Ollama — coûte 24 s de préremplissage à froid sur le 9b et 2,4 à 3,0 s
une fois en cache. Or ce cache meurt avec le runner : keep_alive est de
30 min, et Carlito revient d'une pause plus longue plusieurs fois par jour.
« Quelle heure est-il ? » après le déjeuner : 7 s de chargement + 24 s de
préremplissage avant un mot, pour un modèle qui n'a rien à réfléchir.
Ce module rejoue le préfixe exact du chat toutes les dix minutes : même
identité, même trousse, un « Bonjour » et un seul jeton de sortie. Chaud, la
requête coûte ~0,3 s de GPU ; froid, elle paie les 24 s pendant que personne
n'attend. Elle passe par l'admission de fond (engine/scheduling.py) : un tour
interactif la fait attendre, jamais l'inverse. Dix minutes, parce que trois
relances par demi-heure suffisent à tenir le runner résident et qu'une
relance de plus par heure ne rendrait rien.
Ce que ce module ne fait pas : charger un modèle que rien n'a demandé (seuls
le modèle par défaut et le modèle léger, s'ils sont locaux, sont chauffés —
jamais le 27b choisi dans un sélecteur), ni rendre le premier tour chaud
quand la trousse change (le préfixe rejoué est celui du bureau, 45 schémas
plus celui des questions interactives).
Classes
Functions:
moteur_local
moteur_local(app_state: Any) -> Any | None
Le vrai OllamaEngine derrière ses enveloppes, ou None hors Ollama local.
Source code in src/diapason/server/prechauffage.py
| def moteur_local(app_state: Any) -> Any | None:
"""Le vrai OllamaEngine derrière ses enveloppes, ou None hors Ollama local."""
inner = _deballer(getattr(app_state, "engine", None))
if inner is None:
return None
try:
from diapason.core.local_mode import host_is_local
except Exception: # noqa: BLE001 - sans le module, on ne chauffe rien
return None
if not host_is_local(str(getattr(inner, "_host", ""))):
return None
return inner
|
modeles_a_chauffer
modeles_a_chauffer(
config: Any, modele_du_serveur: str
) -> list[str]
Le modèle par défaut et le modèle léger, une fois chacun, jamais un distant.
Source code in src/diapason/server/prechauffage.py
| def modeles_a_chauffer(config: Any, modele_du_serveur: str) -> list[str]:
"""Le modèle par défaut et le modèle léger, une fois chacun, jamais un distant."""
intelligence = getattr(config, "intelligence", None)
candidats = [
str(modele_du_serveur or ""),
str(getattr(intelligence, "light_model", "") or ""),
]
try:
from diapason.server.cloud_router import is_cloud_model
except Exception: # noqa: BLE001 - sans routeur cloud, tout est local
def is_cloud_model(_: str) -> bool:
return False
retenus: list[str] = []
for nom in candidats:
nom = nom.strip()
if nom and nom != "default" and nom not in retenus and not is_cloud_model(nom):
retenus.append(nom)
return retenus
|
prompt_du_prefixe
prompt_du_prefixe(
app_state: Any, config: Any
) -> tuple[list[Message], list[dict]]
Le prompt du bureau, tel que routes._handle_stream le construit.
Source code in src/diapason/server/prechauffage.py
| def prompt_du_prefixe(app_state: Any, config: Any) -> tuple[list[Message], list[dict]]:
"""Le prompt du bureau, tel que routes._handle_stream le construit."""
from diapason.server.questions_chat import ajouter_consigne, schema_questions
from diapason.server.routes import _chat_tooling, _ensure_identity_prompt
from diapason.server.trousse_chat import TrousseChat
tooling = getattr(app_state, "_chat_tooling_cache", "absent")
if tooling == "absent":
tooling = _chat_tooling(app_state, config)
outils = list(tooling[0]) if tooling else []
messages = _ensure_identity_prompt(
[Message(role=Role.USER, content=QUESTION_TEMOIN)],
config,
client_supplied_system=False,
)
agent = getattr(config, "agent", None)
adaptative = bool(getattr(agent, "trousse_adaptative", False))
specs = list(TrousseChat(outils, messages, adaptative=adaptative).specs)
# Le bureau envoie interactiveQuestions à chaque tour ordinaire : la
# consigne rejoint le message système, le schéma des questions la trousse.
messages = ajouter_consigne(messages)
specs.append(schema_questions())
return messages, specs
|
prechauffer
prechauffer(
app_state: Any, config: Any
) -> list[tuple[str, float]]
Rejoue le préfixe sur chaque modèle à chauffer ; rend (modèle, durée en s).
Source code in src/diapason/server/prechauffage.py
| def prechauffer(app_state: Any, config: Any) -> list[tuple[str, float]]:
"""Rejoue le préfixe sur chaque modèle à chauffer ; rend (modèle, durée en s)."""
from diapason.engine.scheduling import background_work
moteur = moteur_local(app_state)
if moteur is None:
return []
messages, specs = prompt_du_prefixe(app_state, config)
rendu = json.dumps(specs, ensure_ascii=False, separators=(",", ":"))
logger.info(
"prefixe_forme identite=%d car. schemas=%d (%d car., empreinte %s)",
len(messages[0].content or "") if messages else 0,
len(specs),
len(rendu),
hashlib.sha256(rendu.encode()).hexdigest()[:12],
)
chauffes: list[tuple[str, float]] = []
for modele in modeles_a_chauffer(config, getattr(app_state, "model", "")):
depart = time.perf_counter()
try:
with background_work():
moteur.generate(
messages,
model=modele,
temperature=0.0,
max_tokens=1,
tools=specs,
)
chauffes.append((modele, round(time.perf_counter() - depart, 2)))
except (InferenceQueueTimeout, BackgroundStopped):
logger.debug("préchauffage du préfixe différé : moteur occupé (%s)", modele)
except Exception: # noqa: BLE001 - chauffer est un bonus, jamais une panne
logger.debug("préchauffage du préfixe raté pour %s", modele, exc_info=True)
return chauffes
|
entretenir_le_prefixe
async
entretenir_le_prefixe(
app: Any, *, intervalle_s: float = INTERVALLE_S
) -> None
Tâche de fond du serveur : rejoue le préfixe au démarrage puis à intervalle.
Source code in src/diapason/server/prechauffage.py
| async def entretenir_le_prefixe(
app: Any, *, intervalle_s: float = INTERVALLE_S
) -> None:
"""Tâche de fond du serveur : rejoue le préfixe au démarrage puis à intervalle."""
await asyncio.sleep(PREMIER_DELAI_S)
while True:
try:
chauffes = await asyncio.to_thread(
prechauffer, app.state, getattr(app.state, "config", None)
)
if chauffes:
# La durée dit si le préfixe était encore en cache (~0,3 s) ou
# s'il vient d'être recalculé (~24 s sur le 9b) : c'est la seule
# trace de ce que la pause a coûté, et personne ne l'a attendue.
logger.info(
"prefixe_chauffe %s",
", ".join(f"{m} en {d} s" for m, d in chauffes),
)
except asyncio.CancelledError:
raise
except Exception: # noqa: BLE001 - un raté ne doit pas arrêter la boucle
logger.debug("préchauffage du préfixe : tour raté", exc_info=True)
await asyncio.sleep(intervalle_s)
|