Slots : SLOT_PERSIST garde l'état de la discussion à la bascule de preset, en opt-in

Passer de A à B puis revenir à A relance llama-server deux fois, et la
conversation de A est recalculée en entier au retour. Avec SLOT_PERSIST=on
(off par défaut), Loki demande au moteur d'écrire l'état du slot 0 juste
avant la bascule, et le recharge au retour avant le premier message de la
discussion — seulement si tout concorde, sinon calcul normal, en silence.

llama.cpp ne vérifie à la relecture que types et tailles : rien ne dit quel
build ni quels réglages ont calculé ce cache. La clé de validité est donc
l'empreinte de tout ce qui touche au calcul, prise par « loki serve » au
lancement : ligne de commande complète (modèle, CTX, types KV, NGL, lots,
gabarit, SPEC, EXTRA_ARGS…), LLAMA_ARG_*/GGML_*/CUDA_*, taille et date du
modèle et de ses tranches, du projecteur, du brouillon, du binaire et de
ses bibliothèques. Pas de cas « mise à jour du moteur » : elle change la clé.

- --slot-save-path posé aussi pour SLOT_PERSIST accepté, sous les mêmes
  conditions de sécurité que l'isolation du lot 1 (boucle locale ou clé
  d'API) ; refusé avec le décodage spéculatif (le save ne garde pas le
  brouillon ; /slots speculative revérifié à chaque fois), des poids sur
  CPU, un --slot-save-path à la main ; la ligne reste alors celle d'avant
- sauvegarde depuis l'interface ou une tâche, avant l'arrêt : le slot doit
  porter un tour de la discussion (tampon d'engineMarkMain), ≥ 4096 jetons,
  ≤ 8 Gio estimés, double de place libre ; nom provisoire, renommé
  seulement si aucune requête n'est partie pendant l'écriture
- rechargement avant la première requête de la discussion (ou son
  préchauffage) : même clé de moteur, même discussion, slot 0 vierge (/slots
  sans id_task) ; une seule tentative, fichier retiré ; toute erreur =
  calcul normal
- deux fichiers au plus ; clé retirée = tout effacé au lancement suivant ;
  SLOT_PERSIST préservé à la bascule comme HOST (sinon B effaçait l'état de A)
- README : SIDE_SLOT, le cache KV plein ne déclenche jamais de compaction
- tests : plan et refus, ligne inchangée, empreinte (clé d'API exclue,
  fichiers et réglages inclus), rotation, séquence save puis restore sur un
  faux moteur, refus à la sauvegarde et au rechargement, autre clé, défaut
  sans aucun appel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 10:17:08 +02:00
1 parent 7a57322a62
commit 28ba44eb46
12 files changed
+1133 -23

No files matched your search

+20 -1
View File
@@ -362,12 +362,24 @@ func cmdServe(args []string) error {
if spec == "auto" {
si.SpecAutoBlocked, specRecord = specAutoPeek(specMark)
}
probeSlotPersistEnv(cfg, &si)
if strings.Contains(si.Help, "--slot-save-path") && !hasAnyFlag(extra, "--slot-save-path") {
si.SlotDir = prepareSlotDir(LokiHome())
si.SlotDir = prepareSlotDir(LokiHome(), slotPersistOn(cfg))
}
llmArgs, env, notes := buildServeArgs(cfg, extra, bin, si)
applyServeEnv(env)
// SLOT_PERSIST (backend_serve_persist.go) : la clé de CE lancement, et les
// états gardés au-delà de deux retirés. Refusé pour ce preset (brouillon,
// poids sur CPU…) : ceux des autres presets restent, pour leur retour.
// Clé retirée : plus aucun état gardé sur le disque.
persistKey := ""
if ok, _ := slotPersistPlan(cfg, extra, si); ok {
persistKey = slotPersistKey(llmArgs, env, si, bin)
slotPersistPrune(si.SlotDir)
} else if !slotPersistOn(cfg) {
slotPersistPurge(filepath.Join(LokiHome(), "slots"))
}
for _, n := range notes {
fmt.Fprintln(os.Stderr, "[loki serve] "+n)
}
@@ -395,6 +407,10 @@ func cmdServe(args []string) error {
specAutoSettle(specMark, si.SpecAutoBlocked, specRecord, auto)
}
// Posée au dernier moment, port libre : l'ancien moteur est parti, la clé
// lue par le process web est celle du moteur qui répondra.
writeSlotPersistMarker(LokiHome(), persistKey)
fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n",
bin, filepath.Base(model), port)
@@ -644,6 +660,9 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
cram, cramNotes := cacheRAMArgs(cfg, extra, si)
args = append(append(args, cram...), slotSaveArgs(cfg, extra, si)...)
notes = append(notes, cramNotes...)
if n := slotPersistNote(slotPersistPlan(cfg, extra, si)); n != "" {
notes = append(notes, n)
}
// Points de reprise des hybrides (voir backend_serve_ckpt.go).
ck, ckNotes := ckptArgs(cfg, extra, si)
args = append(args, ck...)