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.
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
}
+108 -54
View File
@@ -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, ""
}
+108 -51
View File
@@ -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
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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;