Moteur : corrections de relecture des points de reprise — un slot de plus compte, et un « --version » coincé ne bloque plus le lancement

La liste des points de reprise de llama-server est PAR SLOT : la garde de RAM
de l'espacement d'office ne comptait qu'une liste, et sous-estimait d'autant
le pire cas avec PARALLEL > 1. Et « loki serve » lit désormais le build du
moteur avant de le lancer (hybrides seulement) : sans délai, un binaire coincé
à l'initialisation de ses backends bloquait le lancement, là où binHelp, lui,
était déjà borné.

- ckptSlots : --parallel d'EXTRA_ARGS, sinon PARALLEL, sinon 1 ; « auto » ou
  illisible compté pour 4. Le pire cas devient points × slots × poids.
- engineBuildOf borné à 15 s (WaitDelay pour un descendant qui garde la
  sortie) : passé le délai, build inconnu, ni avis ni drapeau. Profite aussi
  au panneau Moteur.
- Tests : PARALLEL=2 garde l'espacement, PARALLEL=4 et --parallel 4 le
  retirent ; table de ckptSlots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-03 23:57:08 +02:00
1 parent dd27b81af0
commit 7f49310b7f
3 files changed
+59 -3

No files matched your search

+10 -1
View File
@@ -26,6 +26,7 @@ package loki
import ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -281,11 +282,19 @@ func parseEngineBuild(out string) (build int, commit string) {
// engineBuildOf interroge un llama-server pour son numéro de build. Renvoie // engineBuildOf interroge un llama-server pour son numéro de build. Renvoie
// (0, "") si le binaire ne répond pas — un moteur cassé ne doit pas empêcher // (0, "") si le binaire ne répond pas — un moteur cassé ne doit pas empêcher
// d'en installer un autre. // d'en installer un autre.
//
// Borné dans le temps comme binHelp : « loki serve » le lance avant le moteur
// (points de reprise d'un hybride, voir ckptArgs), et un binaire qui reste
// coincé à l'initialisation de ses backends ne doit pas bloquer le lancement —
// passé le délai, build inconnu, donc ni avis ni drapeau.
func engineBuildOf(bin string) (int, string) { func engineBuildOf(bin string) (int, string) {
if bin == "" || !isFile(bin) { if bin == "" || !isFile(bin) {
return 0, "" return 0, ""
} }
cmd := hideCmd(exec.Command(bin, "--version")) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := hideCmd(exec.CommandContext(ctx, bin, "--version"))
cmd.WaitDelay = 2 * time.Second // un descendant qui garde la sortie ouverte ne retient pas Wait
cmd.Env = libraryPathEnv(filepath.Dir(bin)) cmd.Env = libraryPathEnv(filepath.Dir(bin))
out, _ := cmd.CombinedOutput() // --version sort parfois en code non nul out, _ := cmd.CombinedOutput() // --version sort parfois en code non nul
return parseEngineBuild(string(out)) return parseEngineBuild(string(out))
+22 -2
View File
@@ -167,6 +167,24 @@ func ckptCount(cfg map[string]string, extra []string, argEnv map[string]string)
return n return n
} }
// ckptSlots : nombre de slots que le moteur ouvrira, chacun avec sa propre liste
// de points de reprise. --parallel d'EXTRA_ARGS, sinon PARALLEL, sinon 1 (Loki
// pose toujours --parallel, voir buildServeArgs). Illisible ou « auto » (-1) :
// 4, ce que choisissent les moteurs récents — on compte large.
func ckptSlots(cfg map[string]string, extra []string) int {
v := flagValue(extra, "-np", "--parallel")
if v == "" {
v = strings.TrimSpace(cfg["PARALLEL"])
}
if v == "" {
return 1
}
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n > 0 {
return n
}
return 4
}
// ckptMiB : poids d'un point de reprise d'un hybride (état récurrent en f32). // ckptMiB : poids d'un point de reprise d'un hybride (état récurrent en f32).
func ckptMiB(g *GGUFInfo) int64 { func ckptMiB(g *GGUFInfo) int64 {
if g != nil { if g != nil {
@@ -254,10 +272,12 @@ func ckptArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
return args, append(notes, notice+" Pas d'espacement resserré d'office : RAM inconnue ou déjà prise par le modèle "+ return args, append(notes, notice+" Pas d'espacement resserré d'office : RAM inconnue ou déjà prise par le modèle "+
"(CKPT_MIN_STEP=2048 pour l'imposer).") "(CKPT_MIN_STEP=2048 pour l'imposer).")
} }
worst := int64(n) * ckptMiB(si.GGUF) // La liste est PAR SLOT : avec PARALLEL=4, quatre listes se remplissent.
total := n * ckptSlots(cfg, extra)
worst := int64(total) * ckptMiB(si.GGUF)
if si.RAMAvailMiB > 0 && worst > si.RAMAvailMiB/4 { if si.RAMAvailMiB > 0 && worst > si.RAMAvailMiB/4 {
return args, append(notes, fmt.Sprintf("%s Pas d'espacement resserré d'office : %d points de reprise (~%d Mio) "+ return args, append(notes, fmt.Sprintf("%s Pas d'espacement resserré d'office : %d points de reprise (~%d Mio) "+
"dépasseraient le quart de la RAM libre (CKPT_MIN_STEP=2048 pour l'imposer).", notice, n, worst)) "dépasseraient le quart de la RAM libre (CKPT_MIN_STEP=2048 pour l'imposer).", notice, total, worst))
} }
args = append(args, "--checkpoint-min-step", strconv.Itoa(ckptAutoMinStep)) args = append(args, "--checkpoint-min-step", strconv.Itoa(ckptAutoMinStep))
return args, append(notes, fmt.Sprintf("%s → --checkpoint-min-step %d : points de reprise exacts plus rapprochés, "+ return args, append(notes, fmt.Sprintf("%s → --checkpoint-min-step %d : points de reprise exacts plus rapprochés, "+
+27
View File
@@ -76,6 +76,12 @@ func TestCkptArgs(t *testing.T) {
si: func(s *serveSysInfo) { s.RAMMiB = 0 }}, si: func(s *serveSysInfo) { s.RAMMiB = 0 }},
{name: "RAM libre trop juste pour 32 points", notes: 1, {name: "RAM libre trop juste pour 32 points", notes: 1,
si: func(s *serveSysInfo) { s.RAMAvailMiB = 8000 }}, si: func(s *serveSysInfo) { s.RAMAvailMiB = 8000 }},
// Une liste par slot : 2 × 32 × ~150 Mio tient dans le quart de 50 Go, 4 × non.
{name: "PARALLEL=2 : deux listes, encore dans le quart de la RAM libre", cfg: map[string]string{"PARALLEL": "2"},
want: cms, notes: 1},
{name: "PARALLEL=4 : quatre listes, trop pour la RAM libre", cfg: map[string]string{"PARALLEL": "4"}, notes: 1},
{name: "--parallel 4 d'EXTRA_ARGS l'emporte sur PARALLEL", cfg: map[string]string{"PARALLEL": "1"},
extra: []string{"--parallel", "4"}, notes: 1},
{name: "CTX_CHECKPOINTS=16 passé", cfg: map[string]string{"CTX_CHECKPOINTS": "16"}, {name: "CTX_CHECKPOINTS=16 passé", cfg: map[string]string{"CTX_CHECKPOINTS": "16"},
want: []string{"--ctx-checkpoints", "16", "--checkpoint-min-step", "2048"}, notes: 1}, want: []string{"--ctx-checkpoints", "16", "--checkpoint-min-step", "2048"}, notes: 1},
{name: "CTX_CHECKPOINTS illisible", cfg: map[string]string{"CTX_CHECKPOINTS": "beaucoup"}, want: cms, notes: 2}, {name: "CTX_CHECKPOINTS illisible", cfg: map[string]string{"CTX_CHECKPOINTS": "beaucoup"}, want: cms, notes: 2},
@@ -218,3 +224,24 @@ func TestEngineKeepAfterUpdate(t *testing.T) {
t.Fatalf("même version : %v", keep) t.Fatalf("même version : %v", keep)
} }
} }
// Nombre de slots : chacun a sa liste de points de reprise.
func TestCkptSlots(t *testing.T) {
cases := []struct {
cfg map[string]string
extra []string
want int
}{
{want: 1},
{cfg: map[string]string{"PARALLEL": "3"}, want: 3},
{cfg: map[string]string{"PARALLEL": "3"}, extra: []string{"-np", "2"}, want: 2},
{extra: []string{"--parallel=6"}, want: 6},
{extra: []string{"--parallel", "-1"}, want: 4},
{cfg: map[string]string{"PARALLEL": "auto"}, want: 4},
}
for _, c := range cases {
if got := ckptSlots(c.cfg, c.extra); got != c.want {
t.Errorf("ckptSlots(%v, %v) = %d, attendu %d", c.cfg, c.extra, got, c.want)
}
}
}