From 2bf388f7b799025788046b265949bebd7ff4f391 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 3 Oct 2026 23:04:08 +0200 Subject: [PATCH] =?UTF-8?q?Moteur=20:=20garde-fous=20de=20fid=C3=A9lit?= =?UTF-8?q?=C3=A9=20=E2=80=94=20un=20cache=20KV=20quantifi=C3=A9=20ou=20un?= =?UTF-8?q?=20cache=20approch=C3=A9=20se=20voit,=20Loki=20n'en=20pose=20ja?= =?UTF-8?q?mais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/loki/backend_serve.go | 12 +- internal/loki/backend_serve_args_test.go | 4 +- internal/loki/backend_serve_fidelity.go | 153 ++++++++++++ internal/loki/backend_serve_fidelity_test.go | 250 +++++++++++++++++++ internal/loki/sys_service.go | 3 +- internal/loki/ui/index.html | 71 +++++- internal/loki/ui/src/index.tmpl.html | 17 +- internal/loki/ui/src/js/07-models.js | 54 +++- 8 files changed, 545 insertions(+), 19 deletions(-) create mode 100644 internal/loki/backend_serve_fidelity.go create mode 100644 internal/loki/backend_serve_fidelity_test.go diff --git a/internal/loki/backend_serve.go b/internal/loki/backend_serve.go index 66cf7ec..25ce703 100644 --- a/internal/loki/backend_serve.go +++ b/internal/loki/backend_serve.go @@ -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 } diff --git a/internal/loki/backend_serve_args_test.go b/internal/loki/backend_serve_args_test.go index 7602ff2..515963c 100644 --- a/internal/loki/backend_serve_args_test.go +++ b/internal/loki/backend_serve_args_test.go @@ -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", diff --git a/internal/loki/backend_serve_fidelity.go b/internal/loki/backend_serve_fidelity.go new file mode 100644 index 0000000..5006c51 --- /dev/null +++ b/internal/loki/backend_serve_fidelity.go @@ -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 +} diff --git a/internal/loki/backend_serve_fidelity_test.go b/internal/loki/backend_serve_fidelity_test.go new file mode 100644 index 0000000..9365dcb --- /dev/null +++ b/internal/loki/backend_serve_fidelity_test.go @@ -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 + }) + } +} diff --git a/internal/loki/sys_service.go b/internal/loki/sys_service.go index 5c7f6e9..929dda0 100644 --- a/internal/loki/sys_service.go +++ b/internal/loki/sys_service.go @@ -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"}, diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index a25112e..c4abbf9 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -3457,13 +3457,16 @@ html[data-files="1"] #files-btn{color:var(--accent)}
- Cache KVcompression du contexte - - - - - + + + +
+ Cache KVcompression du contexte +