Files
Loki/internal/loki/backend_serve_fidelity.go
T
MichaelandClaude Opus 5.5 2bf388f7b7 Moteur : garde-fous de fidélité — un cache KV quantifié ou un cache approché se voit, Loki n'en pose jamais
Le cache KV en q8_0 d'un preset MoE vivait dans EXTRA_ARGS : ni l'avertissement
de lenteur ni la liste de l'interface ne le voyaient, qui affichait « f16 (max
qualité) » pendant que le moteur tournait en q8_0. Rien n'empêchait non plus un
--context-shift ou un --cache-reuse de modifier en douce ce que voit le modèle.
Aucun drapeau n'est ajouté ni retiré : on DIT ce que la ligne finale change.

- Type de cache effectif (effectiveKVTypes) : KV_TYPE*, puis -ctk/-ctv et
  --cache-type-k/v d'EXTRA_ARGS par-dessus, la dernière occurrence gagne comme
  dans llama-server. warnSlowKV le reçoit désormais.
- Note au lancement quand ce cache n'est pas f16 : q8_0 modifie légèrement les
  sorties, q4_0 perte mesurable, bf16 numérique différente. Avec -ot ou
  --n-cpu-moe, --fit ne tourne pas : repasser en f16 demandera sans doute de
  relever --n-cpu-moe. Pas d'estimation de VRAM sans les métadonnées du GGUF.
- Avertissement pour --context-shift (jamais --no-context-shift) et
  --cache-reuse N>0 ; --swa-full, sans perte, n'en déclenche aucun. Vision
  chargée : llama.cpp les ignore, on le précise.
- Interface : options du cache KV étiquetées, sous-titre qui montre le type
  effectif (« défini par EXTRA_ARGS ») et les drapeaux de cache approché.
- Tests : notes et type effectif en table, aucun -ctk/-ctv sans KV_TYPE, corps
  réels du chat et de la compaction sans cache_prompt:false ni n_cache_reuse,
  et ces clés réservées au benchmark dans tout le paquet (arbre syntaxique).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:04:08 +02:00

154 lines
6.1 KiB
Go

package loki
import (
"fmt"
"strconv"
"strings"
)
// Garde-fous de fidélité. Loki ne choisit JAMAIS à la place de l'utilisateur un
// réglage qui change ce que le modèle calcule : pas de cache KV quantifié posé
// d'office, pas de réutilisation approximative du cache, pas de contexte qui
// glisse en jetant des jetons. Ces réglages restent possibles — ce sont des
// compromis légitimes quand la VRAM manque — mais ils doivent se VOIR : un
// preset qui tourne en q8_0 depuis EXTRA_ARGS pendant que l'interface affiche
// « f16 » est exactement l'altération silencieuse qu'on refuse.
//
// Tout ici est pur : buildServeArgs en tire des notes, cmdServe les écrit.
// effectiveKVTypes donne les types de cache K et V que le moteur utilisera
// VRAIMENT : ceux du preset (serveKVTypes), puis ceux d'EXTRA_ARGS par-dessus.
// EXTRA_ARGS ferme la ligne de commande et llama-server retient la dernière
// occurrence d'un drapeau : un -ctk q8_0 écrit à la main l'emporte sur KV_TYPE.
// fromExtra dit qu'EXTRA_ARGS a tranché au moins l'un des deux. Vide = défaut
// du moteur (f16).
func effectiveKVTypes(cfg map[string]string, extra []string) (k, v string, fromExtra bool) {
k, v = serveKVTypes(cfg)
if s := flagValue(extra, "-ctk", "--cache-type-k"); s != "" {
k, fromExtra = s, true
}
if s := flagValue(extra, "-ctv", "--cache-type-v"); s != "" {
v, fromExtra = s, true
}
return k, v, fromExtra
}
// kvFidelity classe un type de cache par rapport au f16 de référence. Rang 0 =
// mêmes calculs (f16, ou f32 qui ne perd rien) ; plus le rang monte, plus les
// sorties s'en écartent. Un type inconnu (sorti après cette version) est
// supposé altérer : on préfère un avertissement de trop à un silence.
func kvFidelity(t string) (rank int, label string) {
switch strings.ToLower(strings.TrimSpace(t)) {
case "", "f16", "f32":
return 0, ""
case "bf16":
return 1, "numérique différente de f16 (mantisse plus courte)"
case "q8_0", "q5_0", "q5_1":
return 2, "modifie légèrement les sorties par rapport à f16"
case "q4_0", "q4_1", "iq4_nl":
return 3, "perte mesurable par rapport à f16"
default:
return 2, "modifie les sorties par rapport à f16"
}
}
// kvFidelityNote dit, quand le cache n'est pas en f16, ce que ce choix coûte en
// fidélité et d'où il vient. Le choix reste celui du preset : Loki ne le change
// jamais, ni dans un sens ni dans l'autre. Pas d'estimation de la VRAM qu'un
// retour en f16 demanderait : sans les métadonnées du GGUF, ce serait un chiffre
// inventé. En revanche, si le placement est figé à la main (-ot, --n-cpu-moe…),
// --fit ne tourne pas et ne rattrapera pas ce surplus — mieux vaut le savoir
// avant de basculer et de finir en « out of memory ».
func kvFidelityNote(k, v string, fromExtra bool, extra []string) string {
rk, lk := kvFidelity(k)
rv, lv := kvFidelity(v)
if rk == 0 && rv == 0 {
return ""
}
label := lk
if rv > rk {
label = lv
}
src := "KV_TYPE"
if fromExtra {
src = "défini par EXTRA_ARGS"
}
note := fmt.Sprintf("cache KV %s/%s (%s) : %s — choix du preset, laissé tel quel.",
orF16(k), orF16(v), src, label)
if placementFixed(extra) {
note += " Repasser en f16 demande plus de VRAM, et le placement fixé par -ot/--n-cpu-moe " +
"empêche --fit de compenser : il faudrait sans doute relever --n-cpu-moe."
}
return note
}
func orF16(t string) string {
if t == "" {
return "f16"
}
return t
}
// placementFixed : l'utilisateur a placé lui-même des tenseurs. llama.cpp
// abandonne alors son placement automatique (« tensor_buft_overrides already
// set by user, abort ») — rien ne viendra absorber un cache plus gros.
func placementFixed(extra []string) bool {
return hasAnyFlag(extra, "-ot", "--override-tensor", "--n-cpu-moe", "-ncmoe", "--cpu-moe", "-cmoe")
}
// lossyCacheNotes avertit des drapeaux de cache qui changent ce que voit le
// modèle. Seuls deux le font :
//
// - --context-shift : contexte plein → le moteur JETTE des jetons anciens et
// continue, le modèle perd une partie de la conversation sans le savoir ;
// - --cache-reuse N (N > 0) : recolle des morceaux de cache calculés sous un
// AUTRE préfixe, simplement décalés — le résultat n'est plus celui d'un
// calcul complet. --cache-reuse 0 le désactive : rien à dire.
//
// --swa-full n'en fait PAS partie : il garde le cache complet des couches à
// fenêtre glissante, ce qui rend la réutilisation du préfixe exacte (au prix de
// VRAM), et ne fait rien sur un modèle sans SWA. Correspondance exacte des noms
// (hasAnyFlag/flagValue) : --no-context-shift ne déclenche rien, et la dernière
// occurrence gagne comme dans llama-server.
//
// Avec la vision chargée, llama.cpp désactive lui-même les deux (non pris en
// charge en multimodal) : on le dit plutôt que de crier au loup. Les modèles
// hybrides ou récurrents les ignorent aussi, mais sans métadonnées du GGUF on
// ne sait pas les reconnaître ici : on se tait sur ce point plutôt que deviner.
func lossyCacheNotes(extra []string, mmproj bool) []string {
var notes []string
ignored := ""
if mmproj {
ignored = " (vision chargée : llama.cpp l'ignore de toute façon)"
}
if contextShiftOn(extra) {
notes = append(notes, "avertissement : --context-shift (EXTRA_ARGS) — contexte plein, le moteur jette "+
"des jetons anciens et le modèle perd une partie de la conversation sans le savoir"+ignored+".")
}
if s := flagValue(extra, "--cache-reuse"); s != "" {
if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && n > 0 {
notes = append(notes, fmt.Sprintf("avertissement : --cache-reuse %d (EXTRA_ARGS) — réutilise des morceaux "+
"de cache calculés sous un autre préfixe : les sorties ne sont plus exactement celles d'un calcul "+
"complet%s.", n, ignored))
}
}
return notes
}
// contextShiftOn : --context-shift et --no-context-shift peuvent cohabiter
// (preset copié, puis corrigé) ; comme llama-server, la dernière occurrence
// décide.
func contextShiftOn(extra []string) bool {
on := false
for _, a := range extra {
name, _, _ := strings.Cut(a, "=")
switch name {
case "--context-shift":
on = true
case "--no-context-shift":
on = false
}
}
return on
}