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) + } + } +}