diff --git a/README.md b/README.md index 5551b44..b8bbf9b 100644 --- a/README.md +++ b/README.md @@ -634,13 +634,16 @@ Ajoutées par ce fork : le preset) : `--load-mode none`, `mlock`, `dio`, `mmap+mlock` (ou `--no-mmap`, `--mlock` sur un moteur ancien, ou leurs `LLAMA_ARG_*`) gardent en RAM tous les poids laissés au CPU. Loki en estime la part — modèle - (toutes tranches) moins VRAM totale, tout le modèle sur macOS — face à la + (toutes tranches) moins VRAM totale, tout le modèle sur un Mac Apple Silicon — face à la RAM effective (limite du conteneur cgroup comprise) : avertissement au-delà de 80 % (llama.cpp #26110 : swap, décodage de 25 à 7,5 t/s ; mlock : processus tué) ; **refus** au lancement seulement quand la borne basse dépasse 90 % - (échec certain), avec la raison en clair. VRAM inconnue : avertissement, jamais - de refus. `mmap` et `auto` ne sont jamais concernés. Rien ne change dans la - ligne de commande. + (échec certain), avec la raison en clair, aussi affichée par l'interface. + VRAM inconnue (Mac Intel, AMD, Vulkan, `--device`) ou serveurs `--rpc` : + avertissement, jamais de refus. `mmap` et `auto` ne sont jamais concernés ; + `LLAMA_ARG_NO_MMAP`/`LLAMA_ARG_MLOCK` ne comptent que sur un moteur ancien + (un moteur à `--load-mode` les ignore). Rien ne change dans la ligne de + commande. - **Optimiseur sans perte** (`loki tune`, bouton **« Optimiser… »** dans l'éditeur du preset en service ; rien ne tourne de soi-même, aucune clé de config). Cherche, pour cette machine et ce build du moteur, les réglages diff --git a/internal/loki/backend_serve_cache.go b/internal/loki/backend_serve_cache.go index 1f64be9..ed536af 100644 --- a/internal/loki/backend_serve_cache.go +++ b/internal/loki/backend_serve_cache.go @@ -74,10 +74,13 @@ func probeCacheRAM(cfg map[string]string, extra []string, si *serveSysInfo) { } } } - si.UnifiedMem = runtime.GOOS == "darwin" - // Mémoire unifiée (macOS), AMD, Vulkan, --device choisi à la main : pas de - // total VRAM fiable, donc pas d'agrandissement. - if si.UnifiedMem || hasAnyFlag(extra, "-dev", "--device") || si.ArgEnv["LLAMA_ARG_DEVICE"] != "" { + // Mémoire unifiée : Apple Silicon seulement. Un Mac Intel à carte AMD a une + // VRAM à lui, que Loki ne sait pas mesurer (loadModeRisk la tient pour + // inconnue, jamais pour nulle). + si.UnifiedMem = runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" + // macOS, AMD, Vulkan, --device choisi à la main : pas de total VRAM fiable, + // donc pas d'agrandissement. + if runtime.GOOS == "darwin" || hasAnyFlag(extra, "-dev", "--device") || si.ArgEnv["LLAMA_ARG_DEVICE"] != "" { return } // La sélection du preset d'abord (cudaDeviceEnv l'accompagne toujours de diff --git a/internal/loki/backend_serve_moe.go b/internal/loki/backend_serve_moe.go index c148822..00a3579 100644 --- a/internal/loki/backend_serve_moe.go +++ b/internal/loki/backend_serve_moe.go @@ -181,7 +181,9 @@ func firstNonEmpty(vals ...string) string { } // loadArgEnv : les variables qui règlent le chargement sans drapeau. -var loadArgEnv = []string{"LLAMA_ARG_LOAD_MODE", "LLAMA_ARG_MLOCK", "LLAMA_ARG_NO_MMAP"} +// LLAMA_ARG_RPC n'en est pas une, mais dit que des poids partent sur des +// machines distantes (loadModeRisk). +var loadArgEnv = []string{"LLAMA_ARG_LOAD_MODE", "LLAMA_ARG_MLOCK", "LLAMA_ARG_NO_MMAP", "LLAMA_ARG_RPC"} // probeLoadEnv relève ces variables. Appelée après probeServeGPUs, qui // remet ArgEnv à zéro. @@ -227,6 +229,10 @@ func residentLoadMode(args []string, argEnv map[string]string) string { return "" } +// loadGuardMarker : la fin du message de refus, que l'interface reconnaît +// dans le journal du service (modelLoadError). +const loadGuardMarker = "LOAD_GUARD=off pour lancer quand même" + // loadGuardOff : LOAD_GUARD=off, la clé d'échappement du refus. func loadGuardOff(cfg map[string]string) bool { switch strings.ToLower(strings.TrimSpace(cfg["LOAD_GUARD"])) { @@ -256,13 +262,24 @@ func loadModeRisk(cfg map[string]string, args []string, si serveSysInfo) (warn, if isExternalConfig(cfg) { return "", "" } - mode := residentLoadMode(args, si.ArgEnv) + // Un moteur qui connaît --load-mode n'a plus LLAMA_ARG_MLOCK ni + // LLAMA_ARG_NO_MMAP : restées dans l'environnement, elles n'ont aucun effet + // et ne doivent pas faire refuser un lancement en mmap. + env := si.ArgEnv + if helpSupportsLoadMode(si.Help) && (env["LLAMA_ARG_MLOCK"] != "" || env["LLAMA_ARG_NO_MMAP"] != "") { + env = map[string]string{"LLAMA_ARG_LOAD_MODE": si.ArgEnv["LLAMA_ARG_LOAD_MODE"]} + } + mode := residentLoadMode(args, env) modelMiB := si.ModelBytes >> 20 if mode == "" || modelMiB <= 0 || si.RAMMiB <= 0 { return "", "" } + // Serveurs RPC : une part des poids part sur d'autres machines, la VRAM + // locale ne borne plus rien — inconnu, donc un avis au plus, jamais un refus. + remote := hasAnyFlag(args, "--rpc") || strings.TrimSpace(si.ArgEnv["LLAMA_ARG_RPC"]) != "" low, est, upTo := int64(0), modelMiB, true switch { + case remote: case si.UnifiedMem: low, est, upTo = modelMiB, modelMiB, false case si.VRAMMiB > 0: @@ -281,7 +298,7 @@ func loadModeRisk(cfg map[string]string, args []string, si serveSysInfo) (warn, if loadGuardOff(cfg) { return msg + " ; LOAD_GUARD=off : lancé quand même", "" } - return "", msg + " ; LOAD_GUARD=off pour lancer quand même" + return "", msg + " ; " + loadGuardMarker } if est*10 > ram*8 { return fmt.Sprintf("%s : %s%s de poids en RAM pour %s — au-delà, swap (décodage de 25 à 7,5 t/s dans "+ diff --git a/internal/loki/backend_serve_moe_test.go b/internal/loki/backend_serve_moe_test.go index f351df7..373ca29 100644 --- a/internal/loki/backend_serve_moe_test.go +++ b/internal/loki/backend_serve_moe_test.go @@ -128,6 +128,15 @@ func TestLoadModeRisk(t *testing.T) { {name: "LLAMA_ARG_NO_MMAP dans l'environnement", si: serveSysInfo{ModelBytes: 82 * gib, VRAMMiB: 16 << 10, RAMMiB: 60 << 10, ArgEnv: map[string]string{"LLAMA_ARG_NO_MMAP": "1"}}, refuse: true}, + // Corrections de relecture du lot 2 : pas de refus sur une borne fausse. + {name: "LLAMA_ARG_NO_MMAP sans effet sur un moteur à --load-mode : rien", + si: serveSysInfo{Help: "--load-mode", ModelBytes: 82 * gib, VRAMMiB: 16 << 10, RAMMiB: 60 << 10, + ArgEnv: map[string]string{"LLAMA_ARG_NO_MMAP": "1"}}}, + {name: "--rpc : une part des poids part ailleurs, avis seulement", + args: "--load-mode none --rpc 10.0.0.2:50052", si: serveSysInfo{ModelBytes: 82 * gib, VRAMMiB: 16 << 10, RAMMiB: 60 << 10}, warn: true}, + {name: "LLAMA_ARG_RPC : de même", + args: "--load-mode none", si: serveSysInfo{ModelBytes: 82 * gib, VRAMMiB: 16 << 10, RAMMiB: 60 << 10, + ArgEnv: map[string]string{"LLAMA_ARG_RPC": "10.0.0.2:50052"}}, warn: true}, {name: "limite du conteneur (RAM effective) : refus", args: "--load-mode none", si: serveSysInfo{ModelBytes: 40 * gib, VRAMMiB: 16 << 10, RAMMiB: 16 << 10}, refuse: true}, {name: "taille inconnue : rien", @@ -258,3 +267,19 @@ func TestJoinArgsRoundTrip(t *testing.T) { t.Errorf("aller-retour : %q", got) } } + +// Le refus de LOAD_GUARD s'arrête avant tout chargement : l'interface doit le +// dire plutôt qu'un « chargement… » sans fin, même après un chargement réussi +// plus haut dans le journal. +func TestModelLoadErrorLoadGuard(t *testing.T) { + _, refuse := loadModeRisk(map[string]string{}, splitArgs("--load-mode none"), + serveSysInfo{ModelBytes: 82 * gib, VRAMMiB: 16 << 10, RAMMiB: 60 << 10}) + log := "llama_model_load: loading model\nmain: model loaded\nserver is listening\n[ERREUR] " + refuse + "\n" + if got := modelLoadErrorFrom(log); !strings.Contains(got, "LOAD_GUARD=off") { + t.Errorf("refus non signalé : %q", got) + } + // Une tentative suivante qui charge efface le refus. + if got := modelLoadErrorFrom(log + "loading model\nmodel loaded\n"); got != "" { + t.Errorf("refus ancien encore signalé : %q", got) + } +} diff --git a/internal/loki/web_api.go b/internal/loki/web_api.go index 5d30480..e2b7cc8 100644 --- a/internal/loki/web_api.go +++ b/internal/loki/web_api.go @@ -96,8 +96,10 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { // qui exige un fork de llama.cpp. Renvoie "" quand le dernier chargement a // réussi ou qu'aucune tentative n'est visible (évite les faux positifs sur un // service simplement arrêté). -func modelLoadError() string { - log := serviceLogTail(200) +func modelLoadError() string { return modelLoadErrorFrom(serviceLogTail(200)) } + +// modelLoadErrorFrom : modelLoadError sur un journal donné (tests). +func modelLoadErrorFrom(log string) string { if log == "" { return "" } @@ -105,7 +107,9 @@ func modelLoadError() string { // On ne considère que ce qui suit la DERNIÈRE tentative de chargement. start := 0 for i, l := range lines { - if strings.Contains(l, "loading model") || strings.Contains(l, "load_model") { + // Un lancement refusé par LOAD_GUARD (backend_serve_moe.go) s'arrête + // avant tout chargement : c'est lui, la dernière tentative. + if strings.Contains(l, "loading model") || strings.Contains(l, "load_model") || strings.Contains(l, loadGuardMarker) { start = i } } @@ -124,6 +128,9 @@ func modelLoadError() string { strings.Contains(low, "split_mode_tensor not implemented"): // Les autres lignes qui citent SPLIT_MODE_TENSOR ne sont que des avis. reason = "le mode tensor (-sm tensor) n'a pas pu démarrer — SPLIT_MODE=off dans le preset" + case strings.Contains(l, loadGuardMarker): + reason = "lancement refusé : trop de poids resteraient en RAM avec ce mode de chargement — " + + "--load-mode mmap dans le preset, ou LOAD_GUARD=off pour passer outre" case strings.Contains(l, "has offset") && strings.Contains(l, "expected"): reason = "format de quantification non reconnu par ce moteur" case strings.Contains(low, "unknown model architecture"),