Moteur : corrections de relecture des garde-fous de fidélité — les variables LLAMA_ARG_* et le glissement par défaut des anciens moteurs se voient aussi

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 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-03 23:11:31 +02:00
1 parent 2bf388f7b7
commit c4f051a0e9
5 files changed
+232 -114

No files matched your search

+6 -5
View File
@@ -341,6 +341,7 @@ func cmdServe(args []string) error {
// launchQueuesEnv). Après la sélection GPU, qui fait partie de la réponse. // launchQueuesEnv). Après la sélection GPU, qui fait partie de la réponse.
probeServeGPUs(cfg, extra, &si) probeServeGPUs(cfg, extra, &si)
probeExpertEnv(&si) probeExpertEnv(&si)
probeFidelityEnv(&si)
llmArgs, env, notes := buildServeArgs(cfg, extra, bin, si) llmArgs, env, notes := buildServeArgs(cfg, extra, bin, si)
applyServeEnv(env) 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 // 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. // lui que le moteur accélère ou non.
kt, vt, _ := effectiveKVTypes(cfg, extra) kt, vt, _ := effectiveKVTypes(cfg, extra, si.ArgEnv)
warnSlowKV(kt, vt) warnSlowKV(kt, vt)
fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n", 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é 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é 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 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...) args = append(args, extra...)
// Garde-fous de fidélité (voir backend_serve_fidelity.go) : ils ne touchent à // 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. // aucun drapeau, ils DISENT ce que la ligne finale change aux calculs.
ek, ev, fromExtra := effectiveKVTypes(cfg, extra) ek, ev, src := effectiveKVTypes(cfg, extra, si.ArgEnv)
if n := kvFidelityNote(ek, ev, fromExtra, extra); n != "" { if n := kvFidelityNote(ek, ev, src, extra, si.ArgEnv); n != "" {
notes = append(notes, n) notes = append(notes, n)
} }
notes = append(notes, lossyCacheNotes(extra, si.MMProj != "")...) notes = append(notes, lossyCacheNotes(extra, si)...)
return args, env, notes return args, env, notes
} }
+108 -54
View File
@@ -2,6 +2,7 @@ package loki
import ( import (
"fmt" "fmt"
"os"
"strconv" "strconv"
"strings" "strings"
) )
@@ -16,21 +17,55 @@ import (
// //
// Tout ici est pur : buildServeArgs en tire des notes, cmdServe les écrit. // 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 // 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. // VRAIMENT. Trois sources, de la plus faible à la plus forte : les variables
// EXTRA_ARGS ferme la ligne de commande et llama-server retient la dernière // LLAMA_ARG_CACHE_TYPE_K/V (lues avant la ligne de commande), le preset
// occurrence d'un drapeau : un -ctk q8_0 écrit à la main l'emporte sur KV_TYPE. // (serveKVTypes, traduit en -ctk/-ctv), puis EXTRA_ARGS, qui ferme la ligne :
// fromExtra dit qu'EXTRA_ARGS a tranché au moins l'un des deux. Vide = défaut // llama-server retient la dernière occurrence d'un drapeau, un -ctk q8_0 écrit
// du moteur (f16). // à la main l'emporte donc sur KV_TYPE. src nomme la source la plus forte qui a
func effectiveKVTypes(cfg map[string]string, extra []string) (k, v string, fromExtra bool) { // tranché l'un des deux (vide : aucune). Vide = défaut du moteur (f16).
k, v = serveKVTypes(cfg) 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 != "" { 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 != "" { 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 = // 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 // 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 // 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 // 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…), // inventé. En revanche, si le placement est figé à la main (-ot, --n-cpu-moe N>0,
// --fit ne tourne pas et ne rattrapera pas ce surplus — mieux vaut le savoir // en drapeau ou en variable : tensorOverride), --fit ne tourne pas et ne
// avant de basculer et de finir en « out of memory ». // rattrapera pas ce surplus — mieux vaut le savoir avant de basculer et de
func kvFidelityNote(k, v string, fromExtra bool, extra []string) string { // finir en « out of memory ».
func kvFidelityNote(k, v, src string, extra []string, argEnv map[string]string) string {
rk, lk := kvFidelity(k) rk, lk := kvFidelity(k)
rv, lv := kvFidelity(v) rv, lv := kvFidelity(v)
if rk == 0 && rv == 0 { if rk == 0 && rv == 0 {
@@ -69,13 +105,9 @@ func kvFidelityNote(k, v string, fromExtra bool, extra []string) string {
if rv > rk { if rv > rk {
label = lv label = lv
} }
src := "KV_TYPE" note := fmt.Sprintf("cache KV %s/%s (%s) : %s — choix de l'utilisateur, laissé tel quel.",
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) 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 " + 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." "empêche --fit de compenser : il faudrait sans doute relever --n-cpu-moe."
} }
@@ -89,18 +121,12 @@ func orF16(t string) string {
return t return t
} }
// placementFixed : l'utilisateur a placé lui-même des tenseurs. llama.cpp // lossyCacheNotes avertit des réglages de cache qui changent ce que voit le
// 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 : // modèle. Seuls deux le font :
// //
// - --context-shift : contexte plein → le moteur JETTE des jetons anciens et // - le glissement de contexte : contexte plein → le moteur JETTE des jetons
// continue, le modèle perd une partie de la conversation sans le savoir ; // 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 // - --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 // 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. // 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 à // --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 // 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 // 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 // (flagValue) : --no-context-shift ne déclenche rien, et la dernière occurrence
// occurrence gagne comme dans llama-server. // 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 // Avec la vision chargée (MMPROJ, --mmproj d'EXTRA_ARGS ou LLAMA_ARG_MMPROJ),
// charge en multimodal) : on le dit plutôt que de crier au loup. Les modèles // llama.cpp désactive lui-même les deux (non pris en charge en multimodal) : on
// hybrides ou récurrents les ignorent aussi, mais sans métadonnées du GGUF on // le dit plutôt que de crier au loup. Les modèles hybrides ou récurrents les
// ne sait pas les reconnaître ici : on se tait sur ce point plutôt que deviner. // ignorent aussi, mais sans métadonnées du GGUF on ne sait pas les reconnaître
func lossyCacheNotes(extra []string, mmproj bool) []string { // ici : on se tait sur ce point plutôt que deviner.
func lossyCacheNotes(extra []string, si serveSysInfo) []string {
var notes []string var notes []string
ignored := "" 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)" ignored = " (vision chargée : llama.cpp l'ignore de toute façon)"
} }
if contextShiftOn(extra) { switch on, src := contextShift(extra, si.ArgEnv, si.Help); {
notes = append(notes, "avertissement : --context-shift (EXTRA_ARGS) — contexte plein, le moteur jette "+ 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+".") "des jetons anciens et le modèle perd une partie de la conversation sans le savoir"+ignored+".")
} }
if s := flagValue(extra, "--cache-reuse"); s != "" { s, src := flagValue(extra, "--cache-reuse"), "EXTRA_ARGS"
if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && n > 0 { if s == "" {
notes = append(notes, fmt.Sprintf("avertissement : --cache-reuse %d (EXTRA_ARGS) — réutilise des morceaux "+ s, src = si.ArgEnv["LLAMA_ARG_CACHE_REUSE"], "LLAMA_ARG_CACHE_REUSE"
"de cache calculés sous un autre préfixe : les sorties ne sont plus exactement celles d'un calcul "+ }
"complet%s.", n, ignored)) 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 return notes
} }
// contextShiftOn : --context-shift et --no-context-shift peuvent cohabiter // contextShift dit si le moteur glissera le contexte, et qui l'a décidé (vide :
// (preset copié, puis corrigé) ; comme llama-server, la dernière occurrence // son propre défaut). --context-shift et --no-context-shift peuvent cohabiter
// décide. // dans EXTRA_ARGS (preset copié, puis corrigé) ; comme llama-server, la
func contextShiftOn(extra []string) bool { // dernière occurrence décide. Sans drapeau, la variable du moteur, puis son
on := false // 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 { for _, a := range extra {
name, _, _ := strings.Cut(a, "=") name, _, _ := strings.Cut(a, "=")
switch name { switch name {
case "--context-shift": case "--context-shift":
on = true on, set = true, true
case "--no-context-shift": 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, ""
} }
+108 -51
View File
@@ -10,6 +10,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"path/filepath" "path/filepath"
"reflect"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -17,27 +18,36 @@ import (
) )
func TestEffectiveKVTypes(t *testing.T) { 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 { for _, c := range []struct {
name string name string
cfg map[string]string cfg map[string]string
extra string extra string
k, v string argEnv map[string]string
fromExtra bool k, v string
src string
}{ }{
{"rien : défaut moteur", map[string]string{}, "", "", "", false}, {"rien : défaut moteur", map[string]string{}, "", nil, "", "", ""},
{"KV_TYPE seul", map[string]string{"KV_TYPE": "q8_0"}, "", "q8_0", "q8_0", false}, {"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"}, {"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{}, {"formes courtes, la dernière gagne", map[string]string{},
"-ctk q4_0 -ctv q4_0 -ctk q8_0", "q8_0", "q4_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", "", "q8_0", true}, {"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"}, {"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) { t.Run(c.name, func(t *testing.T) {
k, v, fe := effectiveKVTypes(c.cfg, splitArgs(c.extra)) k, v, src := effectiveKVTypes(c.cfg, splitArgs(c.extra), c.argEnv)
if k != c.k || v != c.v || fe != c.fromExtra { if k != c.k || v != c.v || src != c.src {
t.Fatalf("got %q/%q extra=%v, want %q/%q extra=%v", k, v, fe, c.k, c.v, c.fromExtra) 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) { func TestKVFidelityNote(t *testing.T) {
for _, c := range []struct { for _, c := range []struct {
name string name string
k, v string k, v string
fromExtra bool src string
extra string extra string
want []string // fragments attendus ; nil = aucune note 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}, {"défaut moteur : rien à dire", "", "", "", "", nil, nil, false},
{"f16 explicite : rien à dire", "f16", "f16", false, "", nil}, {"f16 explicite : rien à dire", "f16", "f16", "KV_TYPE", "", nil, nil, false},
{"f32 ne perd rien", "f32", "f32", false, "", nil}, {"f32 ne perd rien", "f32", "f32", "KV_TYPE", "", nil, nil, false},
{"q8_0 : léger écart", "q8_0", "q8_0", false, "", []string{"q8_0/q8_0", "KV_TYPE", "modifie légèrement"}}, {"q8_0 : léger écart", "q8_0", "q8_0", "KV_TYPE", "", nil,
{"q4_0 : perte mesurable", "q4_0", "q4_0", false, "", []string{"perte mesurable"}}, []string{"q8_0/q8_0", "KV_TYPE", "modifie légèrement"}, false},
{"mixte : le pire des deux", "q8_0", "q4_0", false, "", []string{"perte mesurable"}}, {"q4_0 : perte mesurable", "q4_0", "q4_0", "KV_TYPE", "", nil, []string{"perte mesurable"}, false},
{"bf16 : numérique différente", "bf16", "bf16", false, "", []string{"mantisse"}}, {"mixte : le pire des deux", "q8_0", "q4_0", "KV_TYPE", "", nil, []string{"perte mesurable"}, false},
{"V seul quantifié", "", "q8_0", false, "", []string{"f16/q8_0"}}, {"bf16 : numérique différente", "bf16", "bf16", "KV_TYPE", "", nil, []string{"mantisse"}, false},
{"type inconnu : supposé altérer", "q3_k", "q3_k", false, "", []string{"modifie les sorties"}}, {"V seul quantifié", "", "q8_0", "KV_TYPE", "", nil, []string{"f16/q8_0"}, false},
{"venu d'EXTRA_ARGS, placement figé : --fit ne compense pas", "q8_0", "q8_0", true, {"type inconnu : supposé altérer", "q3_k", "q3_k", "KV_TYPE", "", nil, []string{"modifie les sorties"}, false},
"-ot per_layer_token_embd.weight=CPU --n-cpu-moe 40 --cache-type-k q8_0", {"venu d'EXTRA_ARGS, placement figé : --fit ne compense pas", "q8_0", "q8_0", "défini par EXTRA_ARGS",
[]string{"défini par EXTRA_ARGS", "relever --n-cpu-moe"}}, "-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) { 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 c.want == nil {
if got != "" { if got != "" {
t.Fatalf("note inattendue : %q", got) t.Fatalf("note inattendue : %q", got)
@@ -77,34 +95,62 @@ func TestKVFidelityNote(t *testing.T) {
t.Fatalf("note %q sans %q", got, w) t.Fatalf("note %q sans %q", got, w)
} }
} }
if !strings.Contains(c.extra, "-ot") && strings.Contains(got, "--fit") { if strings.Contains(got, "--fit") != c.fit {
t.Fatalf("placement libre, mais la note parle de --fit : %q", got) t.Fatalf("note %q : mention de --fit attendue = %v", got, c.fit)
} }
}) })
} }
} }
func TestLossyCacheNotes(t *testing.T) { 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 { for _, c := range []struct {
name string name string
extra string extra string
mmproj bool si serveSysInfo
want []string // un fragment par note attendue, dans l'ordre want []string // un fragment par note attendue, dans l'ordre
}{ }{
{"rien", "", false, nil}, {"rien", "", serveSysInfo{}, nil},
{"--context-shift", "--context-shift", false, []string{"jette des jetons"}}, {"--context-shift", "--context-shift", serveSysInfo{}, []string{"--context-shift (EXTRA_ARGS)"}},
{"--no-context-shift ne déclenche rien", "--no-context-shift", false, nil}, {"--no-context-shift ne déclenche rien", "--no-context-shift", serveSysInfo{}, nil},
{"la dernière occurrence décide", "--context-shift --no-context-shift", false, nil}, {"la dernière occurrence décide", "--context-shift --no-context-shift", serveSysInfo{}, nil},
{"--cache-reuse 256", "--cache-reuse 256", false, []string{"--cache-reuse 256"}}, {"--cache-reuse 256", "--cache-reuse 256", serveSysInfo{}, []string{"--cache-reuse 256 (EXTRA_ARGS)"}},
{"--cache-reuse=256", "--cache-reuse=256", false, []string{"--cache-reuse 256"}}, {"--cache-reuse=256", "--cache-reuse=256", serveSysInfo{}, []string{"--cache-reuse 256"}},
{"--cache-reuse 0 : désactivé", "--cache-reuse 0", false, nil}, {"--cache-reuse 0 : désactivé", "--cache-reuse 0", serveSysInfo{}, nil},
{"--swa-full est sans perte", "--swa-full", false, nil}, {"--swa-full est sans perte", "--swa-full", serveSysInfo{}, nil},
{"les deux", "--context-shift --cache-reuse 64", false, []string{"jette", "--cache-reuse 64"}}, {"les deux", "--context-shift --cache-reuse 64", serveSysInfo{}, []string{"jette", "--cache-reuse 64"}},
{"vision : llama.cpp les ignore, on le dit", "--context-shift --cache-reuse 64", true, {"vision (MMPROJ) : llama.cpp les ignore, on le dit", "--context-shift --cache-reuse 64",
[]string{"l'ignore", "l'ignore"}}, 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) { 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) { if len(got) != len(c.want) {
t.Fatalf("notes = %q, en attendait %d", 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
banned := func(s string) bool { return s == "cache_prompt" || s == "n_cache_reuse" }
ast.Inspect(file, func(n ast.Node) bool { 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) lit, ok := n.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING { if !ok || lit.Kind != token.STRING {
return true 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) t.Errorf("%s : clé %q hors du benchmark", fset.Position(lit.Pos()), s)
} }
return true return true
+5 -2
View File
@@ -5889,8 +5889,11 @@ function syncKVSub(){
const label = KV_FIDELITY[worst] || 'modifie les sorties'; const label = KV_FIDELITY[worst] || 'modifie les sorties';
parts.push((xk || xv ? 'défini par EXTRA_ARGS : ' : '') + (k||'f16')+'/'+(v||'f16')+' — '+label); 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 // Placement figé à la main : --fit ne tourne pas et n'absorbera pas le
// surplus d'un retour en f16. // surplus d'un retour en f16. Comme tensorOverride côté Go : un
if(eaTokens().some(a => ['-ot','--override-tensor','--n-cpu-moe','-ncmoe','--cpu-moe','-cmoe'].includes(a.split('=')[0]))) // --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'); parts.push('repasser en f16 : plus de VRAM, --n-cpu-moe à relever');
} }
let shift = false; let shift = false;
+5 -2
View File
@@ -972,8 +972,11 @@ function syncKVSub(){
const label = KV_FIDELITY[worst] || 'modifie les sorties'; const label = KV_FIDELITY[worst] || 'modifie les sorties';
parts.push((xk || xv ? 'défini par EXTRA_ARGS : ' : '') + (k||'f16')+'/'+(v||'f16')+' — '+label); 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 // Placement figé à la main : --fit ne tourne pas et n'absorbera pas le
// surplus d'un retour en f16. // surplus d'un retour en f16. Comme tensorOverride côté Go : un
if(eaTokens().some(a => ['-ot','--override-tensor','--n-cpu-moe','-ncmoe','--cpu-moe','-cmoe'].includes(a.split('=')[0]))) // --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'); parts.push('repasser en f16 : plus de VRAM, --n-cpu-moe à relever');
} }
let shift = false; let shift = false;