From c4f051a0e91af9222cf89dbface005462a06fc33 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 3 Oct 2026 23:11:31 +0200 Subject: [PATCH] =?UTF-8?q?Moteur=20:=20corrections=20de=20relecture=20des?= =?UTF-8?q?=20garde-fous=20de=20fid=C3=A9lit=C3=A9=20=E2=80=94=20les=20var?= =?UTF-8?q?iables=20LLAMA=5FARG=5F*=20et=20le=20glissement=20par=20d=C3=A9?= =?UTF-8?q?faut=20des=20anciens=20moteurs=20se=20voient=20aussi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les garde-fous ne lisaient qu'EXTRA_ARGS et KV_TYPE. Or llama.cpp applique ses variables LLAMA_ARG_* avant la ligne de commande : un conteneur lancé avec LLAMA_ARG_CACHE_TYPE_K=q8_0 ou LLAMA_ARG_CACHE_REUSE=256 changeait les calculs sans un mot. Et un llama-server d'avant --context-shift glisse le contexte PAR DÉFAUT : il jetait des jetons alors que rien n'était écrit nulle part. - Type de cache effectif : variables, puis KV_TYPE*, puis EXTRA_ARGS, la source la plus forte est nommée dans la note ; warnSlowKV suit. - --context-shift et --cache-reuse lus aussi dans LLAMA_ARG_CONTEXT_SHIFT et LLAMA_ARG_CACHE_REUSE ; moteur ancien (aide sans --context-shift) : on dit qu'il glisse et que --no-context-shift l'en empêche. Aucun drapeau ajouté. - Vision reconnue aussi par --mmproj d'EXTRA_ARGS et LLAMA_ARG_MMPROJ. - Placement figé lu par tensorOverride : --n-cpu-moe 0 ne compte plus, --n-cpu-ffn et LLAMA_ARG_N_CPU_MOE si. Même règle dans l'interface. - Test du paquet : les étiquettes json:"cache_prompt" des structures sont aussi refusées hors du benchmark. Co-Authored-By: Claude Opus 5.5 --- internal/loki/backend_serve.go | 11 +- internal/loki/backend_serve_fidelity.go | 162 ++++++++++++------- internal/loki/backend_serve_fidelity_test.go | 159 ++++++++++++------ internal/loki/ui/index.html | 7 +- internal/loki/ui/src/js/07-models.js | 7 +- 5 files changed, 232 insertions(+), 114 deletions(-) diff --git a/internal/loki/backend_serve.go b/internal/loki/backend_serve.go index 25ce703..a02f329 100644 --- a/internal/loki/backend_serve.go +++ b/internal/loki/backend_serve.go @@ -341,6 +341,7 @@ func cmdServe(args []string) error { // launchQueuesEnv). Après la sélection GPU, qui fait partie de la réponse. probeServeGPUs(cfg, extra, &si) probeExpertEnv(&si) + probeFidelityEnv(&si) llmArgs, env, notes := buildServeArgs(cfg, extra, bin, si) applyServeEnv(env) @@ -361,7 +362,7 @@ func cmdServe(args []string) error { } // 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) + kt, vt, _ := effectiveKVTypes(cfg, extra, si.ArgEnv) warnSlowKV(kt, vt) fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n", @@ -385,7 +386,7 @@ type serveSysInfo struct { LaunchQueues string // CUDA_SCALE_LAUNCH_QUEUES déjà dans l'environnement : choix de l'utilisateur, intouché GPUs int // GPU visibles (CUDA_VISIBLE_DEVICES, sinon nvidia-smi) ; 0 = inconnu ou non sondé - ArgEnv map[string]string // LLAMA_ARG_* de l'environnement qui décident du pipeline et de --fit (serveArgEnv, fitArgEnv) + ArgEnv map[string]string // LLAMA_ARG_* de l'environnement qui décident du pipeline, de --fit et du cache (serveArgEnv, fitArgEnv, fidelityArgEnv) UserEnv map[string]string // GGML_* des réglages d'expert déjà posés (voir expertEnvKeys) : intouchés } @@ -573,11 +574,11 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS 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 != "" { + ek, ev, src := effectiveKVTypes(cfg, extra, si.ArgEnv) + if n := kvFidelityNote(ek, ev, src, extra, si.ArgEnv); n != "" { notes = append(notes, n) } - notes = append(notes, lossyCacheNotes(extra, si.MMProj != "")...) + notes = append(notes, lossyCacheNotes(extra, si)...) return args, env, notes } diff --git a/internal/loki/backend_serve_fidelity.go b/internal/loki/backend_serve_fidelity.go index 5006c51..a2cc92c 100644 --- a/internal/loki/backend_serve_fidelity.go +++ b/internal/loki/backend_serve_fidelity.go @@ -2,6 +2,7 @@ package loki import ( "fmt" + "os" "strconv" "strings" ) @@ -16,21 +17,55 @@ import ( // // Tout ici est pur : buildServeArgs en tire des notes, cmdServe les écrit. +// fidelityArgEnv : les LLAMA_ARG_* qui, sans drapeau, posent un cache +// quantifié, un cache approché ou la vision. llama.cpp les applique AVANT la +// ligne de commande : un -ctk de KV_TYPE ou d'EXTRA_ARGS les écrase, mais seuls +// ils décident. LLAMA_ARG_NO_CONTEXT_SHIFT est celle des moteurs anciens. +var fidelityArgEnv = []string{"LLAMA_ARG_CACHE_TYPE_K", "LLAMA_ARG_CACHE_TYPE_V", + "LLAMA_ARG_CONTEXT_SHIFT", "LLAMA_ARG_NO_CONTEXT_SHIFT", "LLAMA_ARG_CACHE_REUSE", + "LLAMA_ARG_MMPROJ", "LLAMA_ARG_MMPROJ_URL"} + +// probeFidelityEnv complète ArgEnv avec fidelityArgEnv. Appelée après +// probeServeGPUs, qui crée ArgEnv. +func probeFidelityEnv(si *serveSysInfo) { + if si.ArgEnv == nil { + si.ArgEnv = map[string]string{} + } + for _, k := range fidelityArgEnv { + if v, ok := os.LookupEnv(k); ok && v != "" { + si.ArgEnv[k] = v + } + } +} + // 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) +// VRAIMENT. Trois sources, de la plus faible à la plus forte : les variables +// LLAMA_ARG_CACHE_TYPE_K/V (lues avant la ligne de commande), le preset +// (serveKVTypes, traduit en -ctk/-ctv), puis EXTRA_ARGS, qui ferme la ligne : +// llama-server retient la dernière occurrence d'un drapeau, un -ctk q8_0 écrit +// à la main l'emporte donc sur KV_TYPE. src nomme la source la plus forte qui a +// tranché l'un des deux (vide : aucune). Vide = défaut du moteur (f16). +func effectiveKVTypes(cfg map[string]string, extra []string, argEnv map[string]string) (k, v, src string) { + k, v = argEnv["LLAMA_ARG_CACHE_TYPE_K"], argEnv["LLAMA_ARG_CACHE_TYPE_V"] + if k != "" || v != "" { + src = "défini par LLAMA_ARG_CACHE_TYPE_K/V" + } + if ck, cv := serveKVTypes(cfg); ck != "" || cv != "" { + src = "KV_TYPE" + if ck != "" { + k = ck + } + if cv != "" { + v = cv + } + } if s := flagValue(extra, "-ctk", "--cache-type-k"); s != "" { - k, fromExtra = s, true + k, src = s, "défini par EXTRA_ARGS" } if s := flagValue(extra, "-ctv", "--cache-type-v"); s != "" { - v, fromExtra = s, true + v, src = s, "défini par EXTRA_ARGS" } - return k, v, fromExtra + return k, v, src } // kvFidelity classe un type de cache par rapport au f16 de référence. Rang 0 = @@ -56,10 +91,11 @@ func kvFidelity(t string) (rank int, label string) { // 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 { +// inventé. En revanche, si le placement est figé à la main (-ot, --n-cpu-moe N>0, +// en drapeau ou en variable : tensorOverride), --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, src string, extra []string, argEnv map[string]string) string { rk, lk := kvFidelity(k) rv, lv := kvFidelity(v) if rk == 0 && rv == 0 { @@ -69,13 +105,9 @@ func kvFidelityNote(k, v string, fromExtra bool, extra []string) string { 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.", + note := fmt.Sprintf("cache KV %s/%s (%s) : %s — choix de l'utilisateur, laissé tel quel.", orF16(k), orF16(v), src, label) - if placementFixed(extra) { + if tensorOverride(extra, argEnv) != "" { 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." } @@ -89,18 +121,12 @@ func orF16(t string) string { 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 +// lossyCacheNotes avertit des réglages 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 ; +// - le glissement de contexte : 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. @@ -108,46 +134,74 @@ func placementFixed(extra []string) bool { // --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. +// (flagValue) : --no-context-shift ne déclenche rien, et la dernière occurrence +// gagne comme dans llama-server. Les variables LLAMA_ARG_* comptent quand la +// ligne de commande se tait. // -// 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 { +// Avec la vision chargée (MMPROJ, --mmproj d'EXTRA_ARGS ou LLAMA_ARG_MMPROJ), +// 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, si serveSysInfo) []string { var notes []string ignored := "" - if mmproj { + if si.MMProj != "" || hasAnyFlag(extra, "-mm", "--mmproj", "-mmu", "--mmproj-url") || + si.ArgEnv["LLAMA_ARG_MMPROJ"] != "" || si.ArgEnv["LLAMA_ARG_MMPROJ_URL"] != "" { 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 "+ + switch on, src := contextShift(extra, si.ArgEnv, si.Help); { + case on && src == "": + notes = append(notes, "avertissement : ce llama-server ancien glisse le contexte par défaut — contexte "+ + "plein, il jette des jetons anciens et le modèle perd une partie de la conversation sans le savoir"+ + ignored+". --no-context-shift dans EXTRA_ARGS l'en empêche.") + case on: + notes = append(notes, "avertissement : --context-shift ("+src+") — 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)) - } + s, src := flagValue(extra, "--cache-reuse"), "EXTRA_ARGS" + if s == "" { + s, src = si.ArgEnv["LLAMA_ARG_CACHE_REUSE"], "LLAMA_ARG_CACHE_REUSE" + } + if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && n > 0 { + notes = append(notes, fmt.Sprintf("avertissement : --cache-reuse %d (%s) — 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, src, 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 +// contextShift dit si le moteur glissera le contexte, et qui l'a décidé (vide : +// son propre défaut). --context-shift et --no-context-shift peuvent cohabiter +// dans EXTRA_ARGS (preset copié, puis corrigé) ; comme llama-server, la +// dernière occurrence décide. Sans drapeau, la variable du moteur, puis son +// défaut — qui a changé : les llama-server d'avant --context-shift (mi-2025) ne +// connaissent que --no-context-shift et GLISSENT par défaut. Aide vide (moteur +// inconnu) : on ne suppose rien. +func contextShift(extra []string, argEnv map[string]string, help string) (on bool, src string) { + set := false for _, a := range extra { name, _, _ := strings.Cut(a, "=") switch name { case "--context-shift": - on = true + on, set = true, true case "--no-context-shift": - on = false + on, set = false, true } } - return on + if set { + return on, "EXTRA_ARGS" + } + if strings.Contains(help, "--no-context-shift") && !strings.Contains(help, "--context-shift") { + // Moteur ancien : seul LLAMA_ARG_NO_CONTEXT_SHIFT (lu comme un booléen + // vrai) l'arrête ; LLAMA_ARG_CONTEXT_SHIFT lui est inconnue. + if envTruthy(argEnv["LLAMA_ARG_NO_CONTEXT_SHIFT"]) { + return false, "LLAMA_ARG_NO_CONTEXT_SHIFT" + } + return true, "" + } + if envTruthy(argEnv["LLAMA_ARG_CONTEXT_SHIFT"]) { + return true, "LLAMA_ARG_CONTEXT_SHIFT" + } + return false, "" } diff --git a/internal/loki/backend_serve_fidelity_test.go b/internal/loki/backend_serve_fidelity_test.go index 9365dcb..4bd62a5 100644 --- a/internal/loki/backend_serve_fidelity_test.go +++ b/internal/loki/backend_serve_fidelity_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "net/url" "path/filepath" + "reflect" "strconv" "strings" "sync" @@ -17,27 +18,36 @@ import ( ) func TestEffectiveKVTypes(t *testing.T) { + const ea, kv = "défini par EXTRA_ARGS", "KV_TYPE" + const env = "défini par LLAMA_ARG_CACHE_TYPE_K/V" for _, c := range []struct { - name string - cfg map[string]string - extra string - k, v string - fromExtra bool + name string + cfg map[string]string + extra string + argEnv map[string]string + k, v string + src string }{ - {"rien : défaut moteur", map[string]string{}, "", "", "", false}, - {"KV_TYPE seul", map[string]string{"KV_TYPE": "q8_0"}, "", "q8_0", "q8_0", false}, + {"rien : défaut moteur", map[string]string{}, "", nil, "", "", ""}, + {"KV_TYPE seul", map[string]string{"KV_TYPE": "q8_0"}, "", nil, "q8_0", "q8_0", kv}, {"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}, + "--cache-type-k q8_0 --cache-type-v q8_0", nil, "q8_0", "q8_0", ea}, {"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}, + "-ctk q4_0 -ctv q4_0 -ctk q8_0", nil, "q8_0", "q4_0", ea}, + {"forme --flag=valeur", map[string]string{}, "--cache-type-v=q8_0", nil, "", "q8_0", ea}, {"K seul dans EXTRA_ARGS, V du preset", map[string]string{"KV_TYPE_V": "q4_0"}, - "-ctk q8_0", "q8_0", "q4_0", true}, + "-ctk q8_0", nil, "q8_0", "q4_0", ea}, + // Les variables passent AVANT la ligne de commande : seules, elles + // décident ; un -ctk venu de KV_TYPE les écrase. + {"variables seules", map[string]string{}, "", + map[string]string{"LLAMA_ARG_CACHE_TYPE_K": "q8_0", "LLAMA_ARG_CACHE_TYPE_V": "q8_0"}, "q8_0", "q8_0", env}, + {"KV_TYPE_K écrase la variable K, la variable V reste", map[string]string{"KV_TYPE_K": "f16"}, "", + map[string]string{"LLAMA_ARG_CACHE_TYPE_K": "q4_0", "LLAMA_ARG_CACHE_TYPE_V": "q8_0"}, "f16", "q8_0", kv}, } { 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) + k, v, src := effectiveKVTypes(c.cfg, splitArgs(c.extra), c.argEnv) + if k != c.k || v != c.v || src != c.src { + t.Fatalf("got %q/%q src=%q, want %q/%q src=%q", k, v, src, c.k, c.v, c.src) } }) } @@ -45,27 +55,35 @@ func TestEffectiveKVTypes(t *testing.T) { 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 + name string + k, v string + src string + extra string + argEnv map[string]string + want []string // fragments attendus ; nil = aucune note + fit bool // la note doit parler de --fit (placement figé) }{ - {"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"}}, + {"défaut moteur : rien à dire", "", "", "", "", nil, nil, false}, + {"f16 explicite : rien à dire", "f16", "f16", "KV_TYPE", "", nil, nil, false}, + {"f32 ne perd rien", "f32", "f32", "KV_TYPE", "", nil, nil, false}, + {"q8_0 : léger écart", "q8_0", "q8_0", "KV_TYPE", "", nil, + []string{"q8_0/q8_0", "KV_TYPE", "modifie légèrement"}, false}, + {"q4_0 : perte mesurable", "q4_0", "q4_0", "KV_TYPE", "", nil, []string{"perte mesurable"}, false}, + {"mixte : le pire des deux", "q8_0", "q4_0", "KV_TYPE", "", nil, []string{"perte mesurable"}, false}, + {"bf16 : numérique différente", "bf16", "bf16", "KV_TYPE", "", nil, []string{"mantisse"}, false}, + {"V seul quantifié", "", "q8_0", "KV_TYPE", "", nil, []string{"f16/q8_0"}, false}, + {"type inconnu : supposé altérer", "q3_k", "q3_k", "KV_TYPE", "", nil, []string{"modifie les sorties"}, false}, + {"venu d'EXTRA_ARGS, placement figé : --fit ne compense pas", "q8_0", "q8_0", "défini par EXTRA_ARGS", + "-ot per_layer_token_embd.weight=CPU --n-cpu-moe 40 --cache-type-k q8_0", nil, + []string{"défini par EXTRA_ARGS", "relever --n-cpu-moe"}, true}, + // --n-cpu-moe 0 ne place rien : --fit tourne, rien à en dire. + {"--n-cpu-moe 0 : placement libre", "q8_0", "q8_0", "défini par EXTRA_ARGS", + "--n-cpu-moe 0 -ctk q8_0 -ctv q8_0", nil, []string{"q8_0/q8_0"}, false}, + {"experts sur CPU par variable : placement figé aussi", "q8_0", "q8_0", "KV_TYPE", "", + map[string]string{"LLAMA_ARG_N_CPU_MOE": "30"}, []string{"relever --n-cpu-moe"}, true}, } { t.Run(c.name, func(t *testing.T) { - got := kvFidelityNote(c.k, c.v, c.fromExtra, splitArgs(c.extra)) + got := kvFidelityNote(c.k, c.v, c.src, splitArgs(c.extra), c.argEnv) if c.want == nil { if got != "" { t.Fatalf("note inattendue : %q", got) @@ -77,34 +95,62 @@ func TestKVFidelityNote(t *testing.T) { 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) + if strings.Contains(got, "--fit") != c.fit { + t.Fatalf("note %q : mention de --fit attendue = %v", got, c.fit) } }) } } func TestLossyCacheNotes(t *testing.T) { + // Aides des deux générations de llama-server : l'ancienne ne connaît que + // --no-context-shift et glisse par défaut, la récente ne glisse pas. + const helpShiftOld = "--no-context-shift disables context shift on infinite text generation (default: disabled)" + const helpShiftNew = "--context-shift, --no-context-shift whether to use context shift on infinite text generation (default: disabled)" + envOf := func(kv ...string) map[string]string { + m := map[string]string{} + for i := 0; i+1 < len(kv); i += 2 { + m[kv[i]] = kv[i+1] + } + return m + } for _, c := range []struct { - name string - extra string - mmproj bool - want []string // un fragment par note attendue, dans l'ordre + name string + extra string + si serveSysInfo + 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"}}, + {"rien", "", serveSysInfo{}, nil}, + {"--context-shift", "--context-shift", serveSysInfo{}, []string{"--context-shift (EXTRA_ARGS)"}}, + {"--no-context-shift ne déclenche rien", "--no-context-shift", serveSysInfo{}, nil}, + {"la dernière occurrence décide", "--context-shift --no-context-shift", serveSysInfo{}, nil}, + {"--cache-reuse 256", "--cache-reuse 256", serveSysInfo{}, []string{"--cache-reuse 256 (EXTRA_ARGS)"}}, + {"--cache-reuse=256", "--cache-reuse=256", serveSysInfo{}, []string{"--cache-reuse 256"}}, + {"--cache-reuse 0 : désactivé", "--cache-reuse 0", serveSysInfo{}, nil}, + {"--swa-full est sans perte", "--swa-full", serveSysInfo{}, nil}, + {"les deux", "--context-shift --cache-reuse 64", serveSysInfo{}, []string{"jette", "--cache-reuse 64"}}, + {"vision (MMPROJ) : llama.cpp les ignore, on le dit", "--context-shift --cache-reuse 64", + serveSysInfo{MMProj: "/m/mmproj.gguf"}, []string{"l'ignore", "l'ignore"}}, + {"vision par --mmproj d'EXTRA_ARGS : pareil", "--mmproj mmproj-F16.gguf --cache-reuse 64", + serveSysInfo{}, []string{"l'ignore"}}, + // Les variables du moteur comptent quand la ligne de commande se tait. + {"LLAMA_ARG_CACHE_REUSE", "", serveSysInfo{ArgEnv: envOf("LLAMA_ARG_CACHE_REUSE", "128")}, + []string{"--cache-reuse 128 (LLAMA_ARG_CACHE_REUSE)"}}, + {"--cache-reuse 0 d'EXTRA_ARGS écrase la variable", "--cache-reuse 0", + serveSysInfo{ArgEnv: envOf("LLAMA_ARG_CACHE_REUSE", "128")}, nil}, + {"LLAMA_ARG_CONTEXT_SHIFT=1", "", serveSysInfo{Help: helpShiftNew, ArgEnv: envOf("LLAMA_ARG_CONTEXT_SHIFT", "1")}, + []string{"(LLAMA_ARG_CONTEXT_SHIFT)"}}, + {"moteur récent sans drapeau : rien", "", serveSysInfo{Help: helpShiftNew}, nil}, + // Défaut d'un moteur ancien : il jette des jetons sans qu'on ait rien écrit. + {"moteur ancien : glisse par défaut, on le dit", "", serveSysInfo{Help: helpShiftOld}, + []string{"glisse le contexte par défaut"}}, + {"moteur ancien, --no-context-shift : rien", "--no-context-shift", serveSysInfo{Help: helpShiftOld}, nil}, + {"moteur ancien, LLAMA_ARG_NO_CONTEXT_SHIFT=1 : rien", "", + serveSysInfo{Help: helpShiftOld, ArgEnv: envOf("LLAMA_ARG_NO_CONTEXT_SHIFT", "1")}, nil}, + {"moteur inconnu (aide vide) : on ne suppose rien", "", serveSysInfo{}, nil}, } { t.Run(c.name, func(t *testing.T) { - got := lossyCacheNotes(splitArgs(c.extra), c.mmproj) + got := lossyCacheNotes(splitArgs(c.extra), c.si) if len(got) != len(c.want) { t.Fatalf("notes = %q, en attendait %d", got, len(c.want)) } @@ -236,12 +282,23 @@ func TestCleCacheReserveeAuBench(t *testing.T) { if err != nil { t.Fatal(err) } + banned := func(s string) bool { return s == "cache_prompt" || s == "n_cache_reuse" } ast.Inspect(file, func(n ast.Node) bool { + // Un champ de structure sérialisé (étiquette json:"cache_prompt") + // enverrait la clé aussi sûrement qu'une map : on lit les étiquettes. + if f, ok := n.(*ast.Field); ok && f.Tag != nil { + if tag, err := strconv.Unquote(f.Tag.Value); err == nil { + if name, _, _ := strings.Cut(reflect.StructTag(tag).Get("json"), ","); banned(name) { + t.Errorf("%s : champ %q hors du benchmark", fset.Position(f.Pos()), name) + } + } + return true + } 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") { + if s, err := strconv.Unquote(lit.Value); err == nil && banned(s) { t.Errorf("%s : clé %q hors du benchmark", fset.Position(lit.Pos()), s) } return true diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index c4abbf9..4ebb4bb 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -5889,8 +5889,11 @@ function syncKVSub(){ 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]))) + // surplus d'un retour en f16. Comme tensorOverride côté Go : un + // --n-cpu-moe 0 ne place rien. + const placed = f => { const n = eaLastValue(f).trim(); return n !== '' && n !== '0'; }; + if(eaTokens().some(a => ['-ot','--override-tensor','--cpu-moe','-cmoe'].includes(a.split('=')[0])) || + placed(['--n-cpu-moe','-ncmoe']) || placed(['--n-cpu-ffn','-ncffn'])) parts.push('repasser en f16 : plus de VRAM, --n-cpu-moe à relever'); } let shift = false; diff --git a/internal/loki/ui/src/js/07-models.js b/internal/loki/ui/src/js/07-models.js index 0fde783..c9b3a21 100644 --- a/internal/loki/ui/src/js/07-models.js +++ b/internal/loki/ui/src/js/07-models.js @@ -972,8 +972,11 @@ function syncKVSub(){ 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]))) + // surplus d'un retour en f16. Comme tensorOverride côté Go : un + // --n-cpu-moe 0 ne place rien. + const placed = f => { const n = eaLastValue(f).trim(); return n !== '' && n !== '0'; }; + if(eaTokens().some(a => ['-ot','--override-tensor','--cpu-moe','-cmoe'].includes(a.split('=')[0])) || + placed(['--n-cpu-moe','-ncmoe']) || placed(['--n-cpu-ffn','-ncffn'])) parts.push('repasser en f16 : plus de VRAM, --n-cpu-moe à relever'); } let shift = false;