mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
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:
1 parent
2bf388f7b7
commit
c4f051a0e9
5 files changed
+232
-114
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ""
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user