From 7f49310b7fa3397be64a14b3d7cd16a5e47f6393 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 3 Oct 2026 23:57:08 +0200 Subject: [PATCH] =?UTF-8?q?Moteur=20:=20corrections=20de=20relecture=20des?= =?UTF-8?q?=20points=20de=20reprise=20=E2=80=94=20un=20slot=20de=20plus=20?= =?UTF-8?q?compte,=20et=20un=20=C2=AB=20--version=20=C2=BB=20coinc=C3=A9?= =?UTF-8?q?=20ne=20bloque=20plus=20le=20lancement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/loki/backend_engine_oci.go | 11 +++++++++- internal/loki/backend_serve_ckpt.go | 24 +++++++++++++++++++-- internal/loki/backend_serve_ckpt_test.go | 27 ++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/internal/loki/backend_engine_oci.go b/internal/loki/backend_engine_oci.go index d11d84c..6a125e4 100644 --- a/internal/loki/backend_engine_oci.go +++ b/internal/loki/backend_engine_oci.go @@ -26,6 +26,7 @@ package loki import ( "archive/tar" "compress/gzip" + "context" "encoding/json" "fmt" "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 // (0, "") si le binaire ne répond pas — un moteur cassé ne doit pas empêcher // 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) { if bin == "" || !isFile(bin) { 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)) out, _ := cmd.CombinedOutput() // --version sort parfois en code non nul return parseEngineBuild(string(out)) diff --git a/internal/loki/backend_serve_ckpt.go b/internal/loki/backend_serve_ckpt.go index 44f0c14..3074bf5 100644 --- a/internal/loki/backend_serve_ckpt.go +++ b/internal/loki/backend_serve_ckpt.go @@ -167,6 +167,24 @@ func ckptCount(cfg map[string]string, extra []string, argEnv map[string]string) 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). func ckptMiB(g *GGUFInfo) int64 { 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 "+ "(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 { 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)) return args, append(notes, fmt.Sprintf("%s → --checkpoint-min-step %d : points de reprise exacts plus rapprochés, "+ diff --git a/internal/loki/backend_serve_ckpt_test.go b/internal/loki/backend_serve_ckpt_test.go index 9770cdb..515604b 100644 --- a/internal/loki/backend_serve_ckpt_test.go +++ b/internal/loki/backend_serve_ckpt_test.go @@ -76,6 +76,12 @@ func TestCkptArgs(t *testing.T) { si: func(s *serveSysInfo) { s.RAMMiB = 0 }}, {name: "RAM libre trop juste pour 32 points", notes: 1, 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"}, 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}, @@ -218,3 +224,24 @@ func TestEngineKeepAfterUpdate(t *testing.T) { 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) + } + } +}