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>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-03 23:04:08 +02:00
1 parent a42c184ed2
commit 2bf388f7b7
8 files changed
+545 -19

No files matched your search

+11 -1
View File
@@ -359,7 +359,10 @@ func cmdServe(args []string) error {
if err := waitPortFree(host, port, 5*time.Second); err != nil {
return err
}
warnSlowKV(serveKVTypes(cfg))
// Le type EFFECTIF : un -ctk/-ctv d'EXTRA_ARGS l'emporte sur KV_TYPE, et c'est
// lui que le moteur accélère ou non.
kt, vt, _ := effectiveKVTypes(cfg, extra)
warnSlowKV(kt, vt)
fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n",
bin, filepath.Base(model), port)
@@ -568,6 +571,13 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
// EXTRA_ARGS (déjà découpé comme le ferait le shell — les guillemets gardent
// ensemble un chemin qui contient des espaces) ferme la marche.
args = append(args, extra...)
// Garde-fous de fidélité (voir backend_serve_fidelity.go) : ils ne touchent à
// aucun drapeau, ils DISENT ce que la ligne finale change aux calculs.
ek, ev, fromExtra := effectiveKVTypes(cfg, extra)
if n := kvFidelityNote(ek, ev, fromExtra, extra); n != "" {
notes = append(notes, n)
}
notes = append(notes, lossyCacheNotes(extra, si.MMProj != "")...)
return args, env, notes
}
+3 -1
View File
@@ -86,6 +86,7 @@ func TestBuildServeArgs(t *testing.T) {
"-c", "65536", "-t", "8", "-tb", "16", "-b", "4096", "-ub", "1024",
"--host", "127.0.0.1", "--port", "9090",
"--parallel", "2", "-ngl", "all", "-ctk", "q8_0", "-ctv", "q8_0"},
wantNotes: 1, // cache quantifié : dit, jamais retiré
},
{
name: "THREADS=0 et THREADS_BATCH=0 : aucun drapeau, llama.cpp prend ses cœurs physiques",
@@ -165,7 +166,7 @@ func TestBuildServeArgs(t *testing.T) {
name: "cache KV séparé K/V",
cfg: map[string]string{"NGL": "28", "KV_TYPE": "q8_0", "KV_TYPE_V": "q4_0"},
si: serveSysInfo{Help: helpRecent},
want: with("--parallel", "1", "-ngl", "28", "-ctk", "q8_0", "-ctv", "q4_0"),
want: with("--parallel", "1", "-ngl", "28", "-ctk", "q8_0", "-ctv", "q4_0"), wantNotes: 1,
},
{
// Preset MoE typique : experts sur CPU via EXTRA_ARGS, qui pose aussi
@@ -177,6 +178,7 @@ func TestBuildServeArgs(t *testing.T) {
si: serveSysInfo{Help: helpRecent},
want: with("-ngl", "99", "--parallel", "1", "-ot", `blk\.(\d+)\.ffn_.*_exps=CPU`,
"--n-cpu-moe", "30", "-ctk", "q8_0", "-ctv", "q8_0", "-fa", "on"),
wantNotes: 1, // q8_0 venu d'EXTRA_ARGS : vu comme celui de KV_TYPE
},
{
name: "--n-gpu-layers=N dans EXTRA_ARGS : pas de -ngl en double",
+153
View File
@@ -0,0 +1,153 @@
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
}
@@ -0,0 +1,250 @@
package loki
import (
"context"
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
)
func TestEffectiveKVTypes(t *testing.T) {
for _, c := range []struct {
name string
cfg map[string]string
extra string
k, v string
fromExtra bool
}{
{"rien : défaut moteur", map[string]string{}, "", "", "", false},
{"KV_TYPE seul", map[string]string{"KV_TYPE": "q8_0"}, "", "q8_0", "q8_0", false},
{"EXTRA_ARGS l'emporte sur KV_TYPE", map[string]string{"KV_TYPE": "f16"},
"--cache-type-k q8_0 --cache-type-v q8_0", "q8_0", "q8_0", true},
{"formes courtes, la dernière gagne", map[string]string{},
"-ctk q4_0 -ctv q4_0 -ctk q8_0", "q8_0", "q4_0", true},
{"forme --flag=valeur", map[string]string{}, "--cache-type-v=q8_0", "", "q8_0", true},
{"K seul dans EXTRA_ARGS, V du preset", map[string]string{"KV_TYPE_V": "q4_0"},
"-ctk q8_0", "q8_0", "q4_0", true},
} {
t.Run(c.name, func(t *testing.T) {
k, v, fe := effectiveKVTypes(c.cfg, splitArgs(c.extra))
if k != c.k || v != c.v || fe != c.fromExtra {
t.Fatalf("got %q/%q extra=%v, want %q/%q extra=%v", k, v, fe, c.k, c.v, c.fromExtra)
}
})
}
}
func TestKVFidelityNote(t *testing.T) {
for _, c := range []struct {
name string
k, v string
fromExtra bool
extra string
want []string // fragments attendus ; nil = aucune note
}{
{"défaut moteur : rien à dire", "", "", false, "", nil},
{"f16 explicite : rien à dire", "f16", "f16", false, "", nil},
{"f32 ne perd rien", "f32", "f32", false, "", nil},
{"q8_0 : léger écart", "q8_0", "q8_0", false, "", []string{"q8_0/q8_0", "KV_TYPE", "modifie légèrement"}},
{"q4_0 : perte mesurable", "q4_0", "q4_0", false, "", []string{"perte mesurable"}},
{"mixte : le pire des deux", "q8_0", "q4_0", false, "", []string{"perte mesurable"}},
{"bf16 : numérique différente", "bf16", "bf16", false, "", []string{"mantisse"}},
{"V seul quantifié", "", "q8_0", false, "", []string{"f16/q8_0"}},
{"type inconnu : supposé altérer", "q3_k", "q3_k", false, "", []string{"modifie les sorties"}},
{"venu d'EXTRA_ARGS, placement figé : --fit ne compense pas", "q8_0", "q8_0", true,
"-ot per_layer_token_embd.weight=CPU --n-cpu-moe 40 --cache-type-k q8_0",
[]string{"défini par EXTRA_ARGS", "relever --n-cpu-moe"}},
} {
t.Run(c.name, func(t *testing.T) {
got := kvFidelityNote(c.k, c.v, c.fromExtra, splitArgs(c.extra))
if c.want == nil {
if got != "" {
t.Fatalf("note inattendue : %q", got)
}
return
}
for _, w := range c.want {
if !strings.Contains(got, w) {
t.Fatalf("note %q sans %q", got, w)
}
}
if !strings.Contains(c.extra, "-ot") && strings.Contains(got, "--fit") {
t.Fatalf("placement libre, mais la note parle de --fit : %q", got)
}
})
}
}
func TestLossyCacheNotes(t *testing.T) {
for _, c := range []struct {
name string
extra string
mmproj bool
want []string // un fragment par note attendue, dans l'ordre
}{
{"rien", "", false, nil},
{"--context-shift", "--context-shift", false, []string{"jette des jetons"}},
{"--no-context-shift ne déclenche rien", "--no-context-shift", false, nil},
{"la dernière occurrence décide", "--context-shift --no-context-shift", false, nil},
{"--cache-reuse 256", "--cache-reuse 256", false, []string{"--cache-reuse 256"}},
{"--cache-reuse=256", "--cache-reuse=256", false, []string{"--cache-reuse 256"}},
{"--cache-reuse 0 : désactivé", "--cache-reuse 0", false, nil},
{"--swa-full est sans perte", "--swa-full", false, nil},
{"les deux", "--context-shift --cache-reuse 64", false, []string{"jette", "--cache-reuse 64"}},
{"vision : llama.cpp les ignore, on le dit", "--context-shift --cache-reuse 64", true,
[]string{"l'ignore", "l'ignore"}},
} {
t.Run(c.name, func(t *testing.T) {
got := lossyCacheNotes(splitArgs(c.extra), c.mmproj)
if len(got) != len(c.want) {
t.Fatalf("notes = %q, en attendait %d", got, len(c.want))
}
for i, w := range c.want {
if !strings.Contains(got[i], w) {
t.Fatalf("note %q sans %q", got[i], w)
}
}
})
}
}
// Ce que buildServeArgs fait des garde-fous : JAMAIS de drapeau ajouté ou
// retiré, seulement des notes. Sans KV_TYPE, aucun -ctk/-ctv : le moteur garde
// son f16.
func TestBuildServeArgsFidelite(t *testing.T) {
const bin = "/opt/llama/llama-server"
si := serveSysInfo{Help: helpRecent, Model: "/models/m.gguf"}
for _, c := range []struct {
name string
cfg map[string]string
notes []string // fragments attendus parmi les notes ; nil = aucune note de fidélité
}{
{"KV_TYPE absent : ni -ctk ni -ctv, rien à dire", map[string]string{"NGL": "28"}, nil},
{"--swa-full : sans perte, silence", map[string]string{"NGL": "28", "EXTRA_ARGS": "--swa-full"}, nil},
{"cache quantifié par EXTRA_ARGS : vu", map[string]string{"NGL": "28",
"EXTRA_ARGS": "--cache-type-k q8_0 --cache-type-v q8_0"}, []string{"défini par EXTRA_ARGS"}},
{"--cache-reuse 256 : averti", map[string]string{"NGL": "28", "EXTRA_ARGS": "--cache-reuse 256"},
[]string{"--cache-reuse 256"}},
{"--context-shift : averti", map[string]string{"NGL": "28", "EXTRA_ARGS": "--context-shift"},
[]string{"--context-shift"}},
} {
t.Run(c.name, func(t *testing.T) {
extra := splitArgs(c.cfg["EXTRA_ARGS"])
args, _, notes := buildServeArgs(c.cfg, extra, bin, si)
if c.cfg["KV_TYPE"] == "" && !hasAnyFlag(extra, "-ctk", "-ctv", "--cache-type-k", "--cache-type-v") &&
hasAnyFlag(args, "-ctk", "-ctv", "--cache-type-k", "--cache-type-v") {
t.Fatalf("Loki a posé un type de cache de lui-même : %q", args)
}
// La ligne de commande se termine exactement par EXTRA_ARGS, intouché.
if tail := args[len(args)-len(extra):]; strings.Join(tail, " ") != strings.Join(extra, " ") {
t.Fatalf("EXTRA_ARGS modifié : %q", tail)
}
all := strings.Join(notes, "\n")
if c.notes == nil {
if strings.Contains(all, "cache KV") || strings.Contains(all, "avertissement") {
t.Fatalf("note de fidélité inattendue : %q", notes)
}
return
}
for _, w := range c.notes {
if !strings.Contains(all, w) {
t.Fatalf("notes %q sans %q", notes, w)
}
}
})
}
}
// Aucune requête de Loki vers le moteur ne coupe le cache de préfixe
// (cache_prompt:false) ni n'active la réutilisation approximative
// (n_cache_reuse). On vérifie les corps RÉELLEMENT envoyés par le chat et par
// la compaction, contre un faux llama-server local.
func TestRequetesSansCacheApproximatif(t *testing.T) {
testHome(t)
var mu sync.Mutex
var bodies []map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
_ = json.NewDecoder(r.Body).Decode(&body)
mu.Lock()
bodies = append(bodies, body)
mu.Unlock()
if body["stream"] == true {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(200)
_, _ = w.Write([]byte(sseChunk("bonjour")))
_, _ = w.Write([]byte(`data: {"choices":[{"delta":{},"finish_reason":"stop"}]}` + "\n\ndata: [DONE]\n\n"))
return
}
sendJSON(w, 200, map[string]any{"choices": []any{map[string]any{"message": map[string]any{"content": "- résumé"}}}})
}))
t.Cleanup(srv.Close)
u, err := url.Parse(srv.URL)
if err != nil {
t.Fatal(err)
}
if err := SetConfigKey("PORT", u.Port()); err != nil {
t.Fatal(err)
}
if _, err := runChat(context.Background(), []Message{{Role: "user", Content: "salut"}}, 0.7, Caps{},
func(StreamEvent) bool { return true }); err != nil {
t.Fatalf("runChat : %v", err)
}
if _, err := summarizeTranscript(context.Background(), "user: bonjour\nassistant: salut"); err != nil {
t.Fatalf("compaction : %v", err)
}
mu.Lock()
defer mu.Unlock()
if len(bodies) < 2 {
t.Fatalf("%d requête(s) reçue(s), attendu chat + compaction", len(bodies))
}
for _, b := range bodies {
if v, ok := b["cache_prompt"]; ok && v != true {
t.Errorf("cache_prompt=%v envoyé au moteur : le préfixe serait recalculé ou faussé", v)
}
if _, ok := b["n_cache_reuse"]; ok {
t.Errorf("n_cache_reuse envoyé au moteur : réutilisation approximative du cache")
}
}
}
// Filet sur tout le paquet : les clés cache_prompt et n_cache_reuse n'existent
// dans le code QUE pour le benchmark, qui mesure le prefill et doit donc couper
// le cache. Lecture de l'arbre syntaxique (pas du texte) : un commentaire qui
// les mentionne ne compte pas, un nouveau fichier est couvert d'office.
func TestCleCacheReserveeAuBench(t *testing.T) {
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
allowed := map[string]bool{"llm_bench.go": true}
fset := token.NewFileSet()
for _, f := range files {
if strings.HasSuffix(f, "_test.go") || allowed[f] {
continue
}
file, err := parser.ParseFile(fset, f, nil, parser.SkipObjectResolution)
if err != nil {
t.Fatal(err)
}
ast.Inspect(file, func(n ast.Node) bool {
lit, ok := n.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
if s, err := strconv.Unquote(lit.Value); err == nil && (s == "cache_prompt" || s == "n_cache_reuse") {
t.Errorf("%s : clé %q hors du benchmark", fset.Position(lit.Pos()), s)
}
return true
})
}
}
+2 -1
View File
@@ -78,7 +78,8 @@ var configTemplate = []struct{ key, help string }{
"vide = défaut du moteur (32) ; plus haut = lots courts sur CPU, à mesurer"},
{"CUDA_GRAPH_OPT", "on = GGML_CUDA_GRAPH_OPT=1, branches Q/K/V en parallèle au décodage (expérimental, " +
"sortie identique, gain de 0 à quelques %) ; off puis redémarrage s'il plante"},
{"KV_TYPE", "quantization du cache KV (q8_0, q4_0…) ; KV_TYPE_K / KV_TYPE_V pour les séparer"},
{"KV_TYPE", "quantization du cache KV ; vide = f16, sorties de référence ; q8_0 les modifie légèrement, " +
"q4_0 avec perte mesurable ; KV_TYPE_K / KV_TYPE_V pour les séparer"},
{"REASONING", "passthrough du mode raisonnement (on/auto/deepseek)"},
{"REASONING_BUDGET", "plafond de tokens de réflexion ; -1 = illimité"},
{"REASONING_EFFORT", "intensité du raisonnement : vide (auto) / none / low / medium / high / xhigh"},
+63 -8
View File
@@ -3457,13 +3457,16 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<span class="pe-row-c"><input id="s-ubatch" class="pe-val" type="number" min="0" step="128" placeholder="512" oninput="cfgWriteKey('UBATCH', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Cache KV<span class="pe-sub">compression du contexte</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value)">
<!-- Seul f16 laisse les calculs du modèle intacts : chaque autre
option le dit. Le sous-titre montre le type EFFECTIF — un
-ctk/-ctv d'EXTRA_ARGS l'emporte sur cette liste (syncKVSub). -->
<span class="pe-row-l">Cache KV<span class="pe-sub" id="s-kv-sub">compression du contexte</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value); syncKVSub()">
<option value="">f16 (max qualité)</option>
<option value="q8_0">q8_0 (léger)</option>
<option value="q4_0">q4_0 (très léger)</option>
<option value="q5_1">q5_1 (lent sur CUDA)</option>
<option value="bf16">bf16</option>
<option value="q8_0">q8_0 (léger · modifie légèrement les sorties)</option>
<option value="q4_0">q4_0 (très léger · perte mesurable)</option>
<option value="q5_1">q5_1 (lent sur CUDA · modifie les sorties)</option>
<option value="bf16">bf16 (numérique différente de f16)</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : le type vient de la liste que llama-server
@@ -3576,7 +3579,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<span class="pe-row-c"><span id="m-raw-caret" class="pe-caret"></span></span>
</div>
<div id="m-raw-body" class="pe-row stack" style="display:none">
<textarea id="m-content" class="pe-inp" spellcheck="false"></textarea>
<textarea id="m-content" class="pe-inp" spellcheck="false" oninput="syncKVSub()"></textarea>
</div>
</div>
</div>
@@ -5770,7 +5773,7 @@ function cfgWriteKey(key, val){
// jeton par jeton pour les interrupteurs (mlock, flash-attn, n-cpu-moe…), sans
// toucher aux autres drapeaux (--jinja, --device…) qui restent en config brute.
function eaTokens(){ return cfgReadKey('EXTRA_ARGS').split(/\s+/).filter(Boolean); }
function eaSetTokens(t){ cfgWriteKey('EXTRA_ARGS', t.join(' ')); }
function eaSetTokens(t){ cfgWriteKey('EXTRA_ARGS', t.join(' ')); syncKVSub(); }
function eaHasFlag(flag){ return eaTokens().includes(flag); }
function eaToggleFlag(flag, on){
const t = eaTokens().filter(x=>x!==flag);
@@ -5798,6 +5801,7 @@ function populateSettings(){
set('s-ubatch', cfgReadKey('UBATCH'));
set('s-tbatch', cfgReadKey('THREADS_BATCH'));
set('s-kv', cfgReadKey('KV_TYPE'));
syncKVSub();
// Échantillonnage : envoyé PAR REQUÊTE (applySampling côté serveur), donc pas
// besoin de redémarrer le moteur pour qu'un changement prenne effet.
set('s-temp', cfgReadKey('TEMP'));
@@ -5845,6 +5849,57 @@ function populateSettings(){
set('s-loadmode', lm);
syncLoadModeSub(lm);
}
// Cache KV : le sous-titre dit ce que le moteur utilisera VRAIMENT, comme
// effectiveKVTypes côté Go. EXTRA_ARGS ferme la ligne de commande et la dernière
// occurrence gagne : un --cache-type-k q8_0 écrit à la main l'emporte sur la
// liste, qui afficherait sinon « f16 » pendant que le moteur tourne en q8_0.
// Les drapeaux de cache qui changent ce que voit le modèle (--context-shift,
// --cache-reuse > 0) sont signalés au même endroit. Rien n'est jamais modifié
// ici : c'est un affichage.
const KV_FIDELITY = {
'bf16': 'numérique différente de f16',
'q8_0': 'modifie légèrement les sorties', 'q5_0': 'modifie légèrement les sorties', 'q5_1': 'modifie légèrement les sorties',
'q4_0': 'perte mesurable', 'q4_1': 'perte mesurable', 'iq4_nl': 'perte mesurable',
};
const KV_RANK = {'':0, 'f16':0, 'f32':0, 'bf16':1, 'q4_0':3, 'q4_1':3, 'iq4_nl':3};
function eaLastValue(flags){
let v = '';
const t = eaTokens();
t.forEach((a, i) => {
const eq = a.indexOf('='), name = eq > 0 ? a.slice(0, eq) : a;
if(!flags.includes(name)) return;
if(eq > 0) v = a.slice(eq+1);
else if(i+1 < t.length) v = t[i+1];
});
return v;
}
function syncKVSub(){
const el = document.getElementById('s-kv-sub');
if(!el) return;
const kv = cfgReadKey('KV_TYPE');
const xk = eaLastValue(['-ctk','--cache-type-k']), xv = eaLastValue(['-ctv','--cache-type-v']);
const k = (xk || cfgReadKey('KV_TYPE_K') || kv).toLowerCase();
const v = (xv || cfgReadKey('KV_TYPE_V') || kv).toLowerCase();
const rank = x => (x in KV_RANK) ? KV_RANK[x] : 2;
const worst = rank(v) > rank(k) ? v : k;
const parts = [];
if(rank(worst) === 0){
parts.push(xk || xv ? 'f16 (défini par EXTRA_ARGS)' : 'compression du contexte');
} else {
const label = KV_FIDELITY[worst] || 'modifie les sorties';
parts.push((xk || xv ? 'défini par EXTRA_ARGS : ' : '') + (k||'f16')+'/'+(v||'f16')+' — '+label);
// Placement figé à la main : --fit ne tourne pas et n'absorbera pas le
// surplus d'un retour en f16.
if(eaTokens().some(a => ['-ot','--override-tensor','--n-cpu-moe','-ncmoe','--cpu-moe','-cmoe'].includes(a.split('=')[0])))
parts.push('repasser en f16 : plus de VRAM, --n-cpu-moe à relever');
}
let shift = false;
eaTokens().forEach(a => { if(a === '--context-shift') shift = true; else if(a === '--no-context-shift') shift = false; });
if(shift) parts.push('--context-shift : jette des jetons quand le contexte est plein');
const reuse = parseInt(eaLastValue(['--cache-reuse']), 10);
if(reuse > 0) parts.push('--cache-reuse : cache réutilisé de façon approchée');
el.textContent = parts.join(' · ');
}
// Chargement du modèle. llama.cpp récent a REMPLACÉ --mlock / --no-mmap par
// --load-mode (auto, none, mmap, mlock, mmap+mlock, dio). L'éditeur écrit la
// forme moderne ; au lancement, loki serve la retraduit pour un moteur ancien
+10 -7
View File
@@ -1234,13 +1234,16 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<span class="pe-row-c"><input id="s-ubatch" class="pe-val" type="number" min="0" step="128" placeholder="512" oninput="cfgWriteKey('UBATCH', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Cache KV<span class="pe-sub">compression du contexte</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value)">
<!-- Seul f16 laisse les calculs du modèle intacts : chaque autre
option le dit. Le sous-titre montre le type EFFECTIF — un
-ctk/-ctv d'EXTRA_ARGS l'emporte sur cette liste (syncKVSub). -->
<span class="pe-row-l">Cache KV<span class="pe-sub" id="s-kv-sub">compression du contexte</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value); syncKVSub()">
<option value="">f16 (max qualité)</option>
<option value="q8_0">q8_0 (léger)</option>
<option value="q4_0">q4_0 (très léger)</option>
<option value="q5_1">q5_1 (lent sur CUDA)</option>
<option value="bf16">bf16</option>
<option value="q8_0">q8_0 (léger · modifie légèrement les sorties)</option>
<option value="q4_0">q4_0 (très léger · perte mesurable)</option>
<option value="q5_1">q5_1 (lent sur CUDA · modifie les sorties)</option>
<option value="bf16">bf16 (numérique différente de f16)</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : le type vient de la liste que llama-server
@@ -1353,7 +1356,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<span class="pe-row-c"><span id="m-raw-caret" class="pe-caret"></span></span>
</div>
<div id="m-raw-body" class="pe-row stack" style="display:none">
<textarea id="m-content" class="pe-inp" spellcheck="false"></textarea>
<textarea id="m-content" class="pe-inp" spellcheck="false" oninput="syncKVSub()"></textarea>
</div>
</div>
</div>
+53 -1
View File
@@ -856,7 +856,7 @@ function cfgWriteKey(key, val){
// jeton par jeton pour les interrupteurs (mlock, flash-attn, n-cpu-moe…), sans
// toucher aux autres drapeaux (--jinja, --device…) qui restent en config brute.
function eaTokens(){ return cfgReadKey('EXTRA_ARGS').split(/\s+/).filter(Boolean); }
function eaSetTokens(t){ cfgWriteKey('EXTRA_ARGS', t.join(' ')); }
function eaSetTokens(t){ cfgWriteKey('EXTRA_ARGS', t.join(' ')); syncKVSub(); }
function eaHasFlag(flag){ return eaTokens().includes(flag); }
function eaToggleFlag(flag, on){
const t = eaTokens().filter(x=>x!==flag);
@@ -884,6 +884,7 @@ function populateSettings(){
set('s-ubatch', cfgReadKey('UBATCH'));
set('s-tbatch', cfgReadKey('THREADS_BATCH'));
set('s-kv', cfgReadKey('KV_TYPE'));
syncKVSub();
// Échantillonnage : envoyé PAR REQUÊTE (applySampling côté serveur), donc pas
// besoin de redémarrer le moteur pour qu'un changement prenne effet.
set('s-temp', cfgReadKey('TEMP'));
@@ -931,6 +932,57 @@ function populateSettings(){
set('s-loadmode', lm);
syncLoadModeSub(lm);
}
// Cache KV : le sous-titre dit ce que le moteur utilisera VRAIMENT, comme
// effectiveKVTypes côté Go. EXTRA_ARGS ferme la ligne de commande et la dernière
// occurrence gagne : un --cache-type-k q8_0 écrit à la main l'emporte sur la
// liste, qui afficherait sinon « f16 » pendant que le moteur tourne en q8_0.
// Les drapeaux de cache qui changent ce que voit le modèle (--context-shift,
// --cache-reuse > 0) sont signalés au même endroit. Rien n'est jamais modifié
// ici : c'est un affichage.
const KV_FIDELITY = {
'bf16': 'numérique différente de f16',
'q8_0': 'modifie légèrement les sorties', 'q5_0': 'modifie légèrement les sorties', 'q5_1': 'modifie légèrement les sorties',
'q4_0': 'perte mesurable', 'q4_1': 'perte mesurable', 'iq4_nl': 'perte mesurable',
};
const KV_RANK = {'':0, 'f16':0, 'f32':0, 'bf16':1, 'q4_0':3, 'q4_1':3, 'iq4_nl':3};
function eaLastValue(flags){
let v = '';
const t = eaTokens();
t.forEach((a, i) => {
const eq = a.indexOf('='), name = eq > 0 ? a.slice(0, eq) : a;
if(!flags.includes(name)) return;
if(eq > 0) v = a.slice(eq+1);
else if(i+1 < t.length) v = t[i+1];
});
return v;
}
function syncKVSub(){
const el = document.getElementById('s-kv-sub');
if(!el) return;
const kv = cfgReadKey('KV_TYPE');
const xk = eaLastValue(['-ctk','--cache-type-k']), xv = eaLastValue(['-ctv','--cache-type-v']);
const k = (xk || cfgReadKey('KV_TYPE_K') || kv).toLowerCase();
const v = (xv || cfgReadKey('KV_TYPE_V') || kv).toLowerCase();
const rank = x => (x in KV_RANK) ? KV_RANK[x] : 2;
const worst = rank(v) > rank(k) ? v : k;
const parts = [];
if(rank(worst) === 0){
parts.push(xk || xv ? 'f16 (défini par EXTRA_ARGS)' : 'compression du contexte');
} else {
const label = KV_FIDELITY[worst] || 'modifie les sorties';
parts.push((xk || xv ? 'défini par EXTRA_ARGS : ' : '') + (k||'f16')+'/'+(v||'f16')+' — '+label);
// Placement figé à la main : --fit ne tourne pas et n'absorbera pas le
// surplus d'un retour en f16.
if(eaTokens().some(a => ['-ot','--override-tensor','--n-cpu-moe','-ncmoe','--cpu-moe','-cmoe'].includes(a.split('=')[0])))
parts.push('repasser en f16 : plus de VRAM, --n-cpu-moe à relever');
}
let shift = false;
eaTokens().forEach(a => { if(a === '--context-shift') shift = true; else if(a === '--no-context-shift') shift = false; });
if(shift) parts.push('--context-shift : jette des jetons quand le contexte est plein');
const reuse = parseInt(eaLastValue(['--cache-reuse']), 10);
if(reuse > 0) parts.push('--cache-reuse : cache réutilisé de façon approchée');
el.textContent = parts.join(' · ');
}
// Chargement du modèle. llama.cpp récent a REMPLACÉ --mlock / --no-mmap par
// --load-mode (auto, none, mmap, mlock, mmap+mlock, dio). L'éditeur écrit la
// forme moderne ; au lancement, loki serve la retraduit pour un moteur ancien