From 2585d2c2e15d324f3290c5e50437b89a6c8bf5b4 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Fri, 21 Aug 2026 09:47:32 +0200 Subject: [PATCH] =?UTF-8?q?NGL=3D999=20:=20la=20sentinelle=20=C2=AB=20tout?= =?UTF-8?q?es=20les=20couches=20=C2=BB=20devient=20-ngl=20auto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le correctif précédent ne pouvait pas s'appliquer. Il ne posait « -ngl auto » que si la clé NGL était ABSENTE de config.env — or defaultConfig() y sème NGL=999 sur chaque installation neuve. La quasi-totalité des configurations la portent donc sans que personne ne l'ait choisie, le code la lisait comme un choix délibéré, et l'abandon revenait intact : W common_fit_params: failed to fit params to free device memory: n_gpu_layers already set by user to 999, abort 999 n'a jamais été un nombre de couches : c'est la sentinelle historique « toutes ». Sur un moteur qui sait mesurer la VRAM libre, elle devient donc « -ngl auto », et Loki le dit sur stderr plutôt que de le faire en douce. Tout autre nombre reste intouché — c'est un vrai choix. NGL=all force l'ancien comportement, NGL=auto n'envoie toujours aucun drapeau, et un moteur ancien reçoit toujours 999 (l'omettre le ferait tourner 100 % CPU). - La décision sort dans nglArgs(), fonction pure : la seule question qui demande le moteur arrive déjà tranchée, donc elle se teste sans lancer llama-server. 10 cas couverts. - binFitsLayersItself() double la lecture fine de l'aide par la présence de --load-mode. Les deux sont arrivés dans la même vague ; une description reformulée ou une colonne plus large ne doit pas faire conclure « moteur incapable » et réimposer 999. - L'aide de la clé et le sous-titre du champ disent la nouvelle règle. Co-Authored-By: Claude Opus 5 --- internal/loki/backend_serve.go | 70 +++++++++++++++++------ internal/loki/backend_serve_flags_test.go | 35 ++++++++++++ internal/loki/sys_service.go | 2 +- internal/loki/ui/index.html | 2 +- internal/loki/ui/src/index.tmpl.html | 2 +- 5 files changed, 91 insertions(+), 20 deletions(-) diff --git a/internal/loki/backend_serve.go b/internal/loki/backend_serve.go index f805d37..4126254 100644 --- a/internal/loki/backend_serve.go +++ b/internal/loki/backend_serve.go @@ -84,6 +84,41 @@ func binSupportsNGLAuto(bin string) bool { return strings.Contains(h[i:end], "'auto'") } +// binFitsLayersItself : ce moteur sait-il répartir les couches tout seul ? +// +// La lecture fine de l'aide (binSupportsNGLAuto) reste la source de vérité, mais +// elle dépend de la mise en page d'un texte d'aide — une description reformulée +// ou une colonne plus large, et on conclut « non » sur un moteur parfaitement +// capable, donc on lui réimpose 999 et l'abandon revient. --load-mode est arrivé +// dans la même vague que « -ngl auto » : sa présence sert de second témoin, plus +// grossier mais insensible à la mise en page. +func binFitsLayersItself(bin string) bool { + return binSupportsNGLAuto(bin) || binSupportsLoadMode(bin) +} + +// nglArgs traduit la valeur NGL du preset en arguments, et renvoie au passage la +// note à afficher quand Loki a substitué quelque chose. Fonction pure : la seule +// question qui demande le moteur (« sait-il répartir les couches tout seul ? ») +// arrive déjà tranchée, ce qui la rend testable sans lancer llama-server. +func nglArgs(ngl string, fitsItself bool) ([]string, string) { + ngl = strings.TrimSpace(ngl) + switch { + case strings.EqualFold(ngl, "auto"): + return nil, "" // rien du tout : le moteur décide seul + case ngl == "999" && fitsItself: + return []string{"-ngl", "auto"}, + "NGL=999 (sentinelle « toutes les couches ») → -ngl auto : ce moteur mesure lui-même la " + + "VRAM libre. NGL=all pour forcer l'ancien comportement." + case ngl != "": + return []string{"-ngl", ngl}, "" + case fitsItself: + return []string{"-ngl", "auto"}, "" + default: + // Moteur ancien : omettre -ngl le ferait tourner 100 % CPU. + return []string{"-ngl", "999"}, "" + } +} + // hasAnyFlag dit si l'utilisateur a déjà posé l'un de ces drapeaux dans // EXTRA_ARGS. Loki ajoute plusieurs valeurs par défaut (--parallel, -ngl) : les // ajouter EN PLUS de celles de l'utilisateur fait râler le moteur (« argument @@ -240,31 +275,32 @@ func cmdServe(args []string) error { if !hasAnyFlag(extra, "--parallel", "-np") { llmArgs = append(llmArgs, "--parallel", get("PARALLEL", "1")) } - // Couches GPU. Trois cas, dans cet ordre : + // Couches GPU. Quatre cas, dans cet ordre : // - // NGL= → -ngl (l'utilisateur tranche) - // NGL=auto → rien du tout (compat : anciens presets) + // NGL=auto → rien du tout (le moteur décide seul) + // NGL=999 → -ngl auto sur un moteur récent (voir plus bas) + // NGL= → -ngl (l'utilisateur tranche pour de vrai) // clé absente → -ngl auto si le moteur le propose, sinon -ngl 999 // - // Imposer un nombre désarme common_fit_params, qui mesure la VRAM libre et + // Imposer un NOMBRE désarme common_fit_params, qui mesure la VRAM libre et // choisit combien de couches y tiennent : « n_gpu_layers already set by user // to 999, abort ». Le moteur pousse alors tout sur le GPU, échoue en // cudaMalloc ou se replie à moitié sur le CPU — débit effondré, GPU à 100 %. - // D'où « auto » par défaut dès que le moteur sait le faire. Sur un moteur - // plus ancien, qui ne connaît pas la valeur, on garde 999 : omettre -ngl le - // ferait tourner 100 % CPU. + // + // 999 est traité à part parce que ce n'a JAMAIS été un nombre de couches : + // c'est la sentinelle historique « toutes », semée dans config.env par + // defaultConfig() sur chaque installation neuve. La quasi-totalité des + // configurations la portent donc sans que personne ne l'ait choisie — la + // traiter comme un choix délibéré, c'est condamner tout le monde à l'abandon + // ci-dessus. Sur un moteur qui connaît « auto », 999 devient donc auto, et on + // le DIT sur stderr plutôt que de le faire en douce. Qui veut réellement + // forcer tout sur le GPU écrit NGL=all (ou un nombre qui n'est pas 999). if !hasAnyFlag(extra, "-ngl", "--n-gpu-layers", "--gpu-layers") { - ngl := get("NGL", "") - switch { - case strings.EqualFold(ngl, "auto"): - // rien : le moteur décide seul - case ngl != "": - llmArgs = append(llmArgs, "-ngl", ngl) - case binSupportsNGLAuto(bin): - llmArgs = append(llmArgs, "-ngl", "auto") - default: - llmArgs = append(llmArgs, "-ngl", "999") + args, note := nglArgs(get("NGL", ""), binFitsLayersItself(bin)) + if note != "" { + fmt.Fprintln(os.Stderr, "[loki serve] "+note) } + llmArgs = append(llmArgs, args...) } if ktv != "" { llmArgs = append(llmArgs, "-ctk", ktv) diff --git a/internal/loki/backend_serve_flags_test.go b/internal/loki/backend_serve_flags_test.go index adf18b5..02e7679 100644 --- a/internal/loki/backend_serve_flags_test.go +++ b/internal/loki/backend_serve_flags_test.go @@ -56,3 +56,38 @@ func TestNormalizeLoadFlags(t *testing.T) { }) } } + +func TestNGLArgs(t *testing.T) { + cases := []struct { + name string + ngl string + fitsItself bool + want []string + wantNote bool + }{ + // Le cas qui motive tout : defaultConfig() sème NGL=999 sur chaque + // installation neuve. Personne ne l'a choisi, et le moteur récent + // abandonne son calcul de VRAM dès qu'on lui impose un nombre. + {"sentinelle 999, moteur récent", "999", true, []string{"-ngl", "auto"}, true}, + {"sentinelle 999, moteur ancien", "999", false, []string{"-ngl", "999"}, false}, + {"nombre choisi, jamais touché", "28", true, []string{"-ngl", "28"}, false}, + {"nombre choisi, moteur ancien", "28", false, []string{"-ngl", "28"}, false}, + {"all reste all", "all", true, []string{"-ngl", "all"}, false}, + {"auto : aucun drapeau", "auto", true, nil, false}, + {"auto insensible à la casse", "AUTO", true, nil, false}, + {"espaces parasites autour de la sentinelle", " 999 ", true, []string{"-ngl", "auto"}, true}, + {"clé absente, moteur récent", "", true, []string{"-ngl", "auto"}, false}, + {"clé absente, moteur ancien", "", false, []string{"-ngl", "999"}, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, note := nglArgs(c.ngl, c.fitsItself) + if !reflect.DeepEqual(got, c.want) { + t.Fatalf("args = %q, want %q", got, c.want) + } + if (note != "") != c.wantNote { + t.Fatalf("note = %q, en voulait-on une ? %v", note, c.wantNote) + } + }) + } +} diff --git a/internal/loki/sys_service.go b/internal/loki/sys_service.go index 2101a77..6ff307d 100644 --- a/internal/loki/sys_service.go +++ b/internal/loki/sys_service.go @@ -67,7 +67,7 @@ var configTemplate = []struct{ key, help string }{ {"HOST", "adresse d'écoute du moteur (défaut 0.0.0.0)"}, {"PORT", "port du moteur (défaut 8080)"}, {"CTX", "taille du contexte (défaut 32768)"}, - {"NGL", "couches déportées sur le GPU (défaut 999 = tout ; auto = ce qui tient en VRAM)"}, + {"NGL", "couches déportées sur le GPU (999 ou auto = ce qui tient en VRAM ; all = tout, quitte à saturer)"}, {"BATCH", "batch (défaut 2048)"}, {"UBATCH", "micro-batch (défaut 512)"}, {"THREADS", "threads CPU, 0 = auto"}, diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index aaf7cb0..c9e51bd 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -3121,7 +3121,7 @@ html[data-files="1"] #files-btn{color:var(--accent)} tok
- Couches sur GPU999 = tout sur le GPU · auto = ce qui tient en VRAM + Couches sur GPU999 ou auto = ce qui tient en VRAM · all = tout, quitte à saturer
diff --git a/internal/loki/ui/src/index.tmpl.html b/internal/loki/ui/src/index.tmpl.html index 2cb5706..e062cdd 100644 --- a/internal/loki/ui/src/index.tmpl.html +++ b/internal/loki/ui/src/index.tmpl.html @@ -984,7 +984,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid tok
- Couches sur GPU999 = tout sur le GPU · auto = ce qui tient en VRAM + Couches sur GPU999 ou auto = ce qui tient en VRAM · all = tout, quitte à saturer