L'anneau des complétions (perf_log.go) est porté en Rust (src/perfring.rs) :
perte de cache contre la complétion précédente de la même discussion et de
la même nature, ce qui s'est intercalé, seuil d'alerte du preset (dans
l'ordre du lancement, 2048 sur un modèle hybride), ligne [perf], résumé de
/api/perf/summary. ChatShared le porte : tours, vérifications, résumés de
compaction et préchauffage de la façade y rangent leurs complétions sans
aller-retour au Go.
Côté Go, quand la façade tient la conversation, perfRecord range chez elle
(/internal/facade/v1/perf/record) et perfSnapshot relit son anneau
(optimiseur) ; /api/perf/summary lui est remis. Façade injoignable : l'anneau
du Go prend le relais. La route /internal/v1/turn/perf, plus appelée, est
retirée.
Parité : journal tiré du Go (parity_perf_test.go → tests/parity_perf/
perf.json : 14 complétions, résumé, onze seuils), rejoué par
src/perfring/tests.rs ; deux tours qui perdent le cache (tests/lot6.rs),
l'API de l'anneau pour le Go, et TestFacadePerfAnneau côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
En mode Code, le modèle recopie sans cesse ce qui est déjà dans le
contexte : chemins, diffs, arguments JSON, fichiers réécrits. ngram-mod
de llama.cpp y cherche la suite des 24 derniers jetons et propose d'un
coup les 48 à 64 suivants, sans modèle brouillon. La clé SPEC (off par
défaut, inchangé) gagne deux valeurs : ngram, et mtp+ngram qui y ajoute
la tête MTP. Même vérification exacte que MTP : distribution inchangée,
mais pas le même texte au bit près (vérification par lots), donc on
compare des sessions rejouées, pas des diffs.
- forme explicite seulement : --spec-type ngram-mod
--spec-ngram-mod-n-match 24 --spec-ngram-mod-n-min 48
--spec-ngram-mod-n-max 64 ; jamais --spec-default (TODO amont, contenu
susceptible de changer avec le moteur)
- garde-fou d'aide sur « --spec-ngram-mod-n-match » (pas le simple mot
ngram-mod, encore listé par les moteurs d'avant le renommage) ; absent =
SPEC ignoré, note au journal
- mtp+ngram : --spec-type draft-mtp,ngram-mod seulement avec draft-mtp
dans l'aide ET une tête MTP réelle (tenseur nextn ou MODEL_DRAFT) ;
sinon n-grammes seuls, avec la raison — jamais « failed to create MTP
context » en boucle
- EXTRA_ARGS/LLAMA_ARG_* : mêmes exclusions qu'avant, plus --spec_type et
tout --spec-ngram-* (--spec-type s'additionne sans prévenir) ; pour
ngram seul, un --spec-draft-* fait aussi taire Loki
- poids sur CPU (experts MoE, -ot, NGL partiel) : refusé tant que
GGML_OP_OFFLOAD_MIN_BATCH (env ou OP_OFFLOAD_MIN_BATCH) ne dépasse pas
le lot de vérification de 65 jetons, avec la suggestion
OP_OFFLOAD_MIN_BATCH=128 ; avertissements pour un modèle plus gros que
la RAM, les points de reprise d'un hybride, NGL imposé (logits)
- avec ngram, une tête MTP ou MODEL_DRAFT inutilisés sont signalés
- --spec-synth-len/--spec-synth-rates (et LLAMA_ARG_SPEC_SYNTH_LEN) :
acceptation au hasard, avertissement au lancement ; Loki ne les pose
jamais
- télémétrie : /api/perf/summary donne draft_n, draft_accepted et
draft_rate par nature de requête
- UI : deux options du sélecteur de décodage spéculatif ; README et
configTemplate
- tests : forme explicite, garde d'aide, repli de mtp+ngram, exclusions,
seuil d'offload, ligne inchangée sans SPEC, acceptation par nature
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Une compaction, un dernier message rendu autrement au tour suivant ou une
tâche planifiée qui a pris le slot laissent au message suivant un recalcul
inévitable : des secondes sur un 27B, des dizaines sur un MoE aux experts en
RAM, avant le premier mot. Nouvelle clé PREWARM (off par défaut) : dès que le
moteur local est libre, Loki lui envoie la requête du prochain tour suivie
d'un message utilisateur « . », max_tokens 1, sans flux. Le moteur calcule le
préfixe et pose un point de reprise au début de ce message ; le vrai tour ne
calcule plus que le sien. Sans la clé, aucune requête de plus (testé).
- assemblage partagé, contenu vivant : turnViewDry (même code que turnView,
sans rien toucher à la discussion, décision PROJ_SNAPSHOT via
projSnapDecide), wireMessages, turnReasoning et buildChatPayload, extraits
de runChat ; corps identique à l'octet près à celui d'avant (matrice
d'options et de modes, testée contre une copie de l'ancien code)
- jamais devant un vrai travail : toute requête de Loki l'annule au départ
(compteur de llm_slots.go, sous son verrou), sauf un tour de chat dont les
messages sérialisés, outils et arguments du gabarit prolongent exactement
le préfixe préchauffé
- moteur local, un seul slot (/props total_slots), pas pendant un tour, une
tâche, un bench, un travail annexe ni une requête en vol ; un seul à la
fois ; pas si le prochain tour compactera, ni à moins de 10 min de minuit
- requête brute : ni compaction, ni relance, ni effort appris, ni CtxUsed,
ni stats ; erreurs lâchées (une ligne de journal par statut) ; rien persisté
- déclencheurs : fin de tour sans file d'attente, fin de tâche (projet forcé
levé, slot effacé) ; full = aussi changement de discussion après 3 s
- capacités du dernier tour gardées en mémoire par discussion ; mode Code =
rôle d'un message ordinaire, une demande de plan diverge et annule
- télémétrie kind=prewarm ; la vérification du cache RAM après une tâche se
fait sur lui
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les pistes de réutilisation du cache dépendent de deux faits que rien ne
mesurait : le gabarit du modèle rend-il encore le raisonnement d'un tour
passé, et le rendu d'un tour d'outil reste-t-il un préfixe exact quand un
message utilisateur s'y ajoute ? La sonde pose ces deux questions au moteur
local par POST /apply-template — un rendu sans état, qui ne prend ni slot ni
place dans la file — et n'en tire que des verdicts oui/non/inconnu.
Elle ne touche à aucune construction de prompt : Loki ne renvoie toujours
aucun reasoning_content au modèle. Toute évolution qui voudrait s'en servir
passera sa propre revue, et lira « unknown » comme « ne rien changer ».
- moteur local seulement ; preset externe : aucune requête, rien d'affiché
- jamais de complétion de repli : avec un slot, elle évincerait le cache
- en tâche de fond après une complétion complète du fil principal, /health
à 200, au plus une vérification par minute ; authHeader ; 3 s par requête
- mêmes outils, chat_template_kwargs et reasoning_effort que la complétion
- add_generation_prompt=false demandé au moteur ; un moteur qui l'ignore
donne « inconnu » plutôt qu'un faux « instable »
- tour d'outil bien formé (identifiant de 9 alphanumériques, résultat lié)
- 404, 401, exception du gabarit, délai : inconnu ; 503 relancé
- cache par build_info + modèle + empreinte du gabarit + forme de requête
- verdict dans /api/perf/summary (champ template) et une ligne [tplprobe]
au journal par verdict nouveau
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'ancien bench ne lisait pas le statut HTTP, inventait un partage 15/85
du temps quand les timings manquaient (et l'enregistrait comme mesuré),
tuilait un petit corpus qu'un brouillon recopiait, et ne disait rien du
prefill à 30k ni du chemin où le cache est repris. Synchrone, il était
coupé par les proxys (60-100 s) pendant que le moteur continuait, et un
simple GET suffisait à le lancer.
- Tâche de fond : POST /api/bench (202, 405 sur GET, 409 si occupé),
/api/bench/status pour la progression, /api/bench/cancel ; chaque
requête au moteur porte le contexte annulable.
- Verrou de génération tenu pendant toute la mesure : chat, tâches et
compaction reçoivent un refus clair ; les clients /v1 un 503 avec
Retry-After ; /slots occupé (client externe, CLI) refuse aussi.
- Mode rapide par défaut : préchauffage jeté + la ligne courte, sur le
chat avec gabarit, raisonnement et échantillonnage du preset, seed fixe.
- Mode complet (bouton « bench complet », loki bench --full) : prefill à
froid à D = min(CTX/2, 32k, ce qui laisse tenir les tours), 16k si des
poids tournent sur CPU, puis 3 tours user → assistant → user de code
jamais vu. Reprise du cache lue dans cache_n, jamais supposée ; note
pour les hybrides (points de contrôle). Pas d'ignore_eos.
- Rien n'est enregistré sans réponses 200 et timings réels, ni pour un
résultat partiel (budget de 6 min par requête, contexte plein).
- Empreinte enregistrée (configuration + CUDA_VISIBLE_DEVICES + protocole) ;
les anciennes mesures retombent sur le nom du modèle. Types de cache KV
et build du moteur affichés, KV quantifié signalé (zone grise).
- Linux : indication « possible thrash » (read_bytes, VmRSS), jamais
enregistrée.
- Effacement du slot en fin de bench inchangé (engineSideJob, différé :
il tourne aussi sur erreur et annulation).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La relecture du relevé par complétion a trouvé quatre écarts, tous
d'affichage ou de robustesse ; rien ne touche aux requêtes ni au contexte.
- Compaction : la réponse lue d'un bloc passait de Decoder à Unmarshal,
plus strict (un octet parasite après le JSON faisait échouer une
compaction qui passait). Retour au Decoder, et perfWire fait de même.
- API externe : son cache suit ses propres règles, sans slot ni points de
reprise à surveiller ici. Plus de ligne ambre « recalculés » pour elle ;
le relevé reste dans l'anneau.
- Seuil d'alerte : même priorité qu'au lancement — -cms d'EXTRA_ARGS, puis
LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT, puis CKPT_MIN_STEP ; -ub d'EXTRA_ARGS
avant UBATCH.
- UI : « recalculés après sous-agent, client /v1 » au lieu des identifiants
internes (subagent, foreign).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jusqu'ici, impossible de savoir si une ligne volatile s'était glissée dans
le prompt, si une mise à jour du moteur avait cassé les points de reprise
ou si l'acceptation du MTP s'était effondrée : le chunk final de llama.cpp
porte ces chiffres (cache_n, draft_n, cached_tokens), on les jetait.
Lecture seule : aucun champ ajouté aux requêtes, et le comptage du
contexte (usage.prompt_tokens → CtxUsed → compaction) ne change pas.
- perf_log.go : décodage à part et tolérant (perfWire) — un compteur mal
typé ne jette plus le chunk final ni ne fait échouer une compaction ;
cached_tokens, strictement entier dans streamChunk, y déménage aussi.
- Inconnu n'est pas 0 : pointeurs, et l'UI garde « prefill N tok » quand
le moteur ne dit rien de son cache.
- Nature (main, subagent, verify, task, compact, bench, foreign) portée
par le contexte, pas par Caps ; TTFT au premier delta de tout type.
- Perte de cache = total précédent − cache_n, seulement quand les messages
prolongent strictement ceux de la complétion précédente de la même
discussion et de la même nature (empreintes cumulées) ; jamais sur un
flux coupé. Ce qui s'est intercalé (sous-agent, client /v1, bench) est
nommé.
- Anneau de 5000 entrées en mémoire, sans texte ; GET /api/perf/summary
(derrière la clé) : taux de cache, recalcul par tour et par nature,
médianes pp/tg par profondeur, acceptation du brouillon.
- Ligne [perf] sur stderr seulement avec LOKI_PERF_LOG.
- UI : « prefill X nouveaux / Y en cache », ambre au-delà d'un seuil
relevé sur un modèle hybride (espacement des points de reprise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>