From 6aaf9360b43390dadeea36063b5b3887ec9ad428 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 7 Oct 2026 13:26:50 +0200 Subject: [PATCH] =?UTF-8?q?Strata=20:=20experts=20plac=C3=A9s=20selon=20la?= =?UTF-8?q?=20RAM=20disponible,=20mode=20=C2=AB=20depuis=20le=20disque=20?= =?UTF-8?q?=C2=BB=20(mmap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vécu : 62 Go de RAM dont 18 déjà pris par le reste du serveur. Le mode « carte d'appoint hors RAM » avait été choisi sur la RAM TOTALE, et le premier lancement, chargé d'écrire experts.bin, a monté le moteur à ~41 Go de mémoire anonyme : le noyau l'a tué (OOM). - Placement décidé AU LANCEMENT sur la RAM disponible (MemAvailable), plus sur la RAM totale ; le mode retenu est écrit dans le journal du moteur. - Nouveau mode « disque » : --mmap-experts, les experts sont lus dans les fichiers du modèle par le cache système, rien n'est chargé au démarrage. Choisi d'office quand la RAM disponible ne suffit pas. - Plus de premier lancement qui écrit experts.bin : les modes « hors RAM » et « experts.bin mappé » ne servent que si le fichier existe déjà. - Réglage « Experts » dans la fenêtre de Strata : auto, en RAM, ou depuis le disque ; la configuration montre le mode du dernier lancement et la RAM disponible de la machine. Co-Authored-By: Claude Opus 5.5 --- internal/loki/backend_strata.go | 350 ++++++++++++++++++++------ internal/loki/backend_strata_test.go | 199 ++++++++------- internal/loki/ui/index.html | 18 +- internal/loki/ui/src/js/05-status.js | 2 +- internal/loki/ui/src/js/10b-strata.js | 16 +- 5 files changed, 410 insertions(+), 175 deletions(-) diff --git a/internal/loki/backend_strata.go b/internal/loki/backend_strata.go index f45f03d..416ca3e 100644 --- a/internal/loki/backend_strata.go +++ b/internal/loki/backend_strata.go @@ -129,9 +129,120 @@ const strataDropHeadroomGB = 7 // le contexte CUDA, l'encodeur d'images et la marge : 5,3 Gio sur une 3070 8 Go). func strataHelperExpertsGB(vramGB float64) float64 { return max(vramGB-2.5, 0) } -// strataMmapExtraGB : le mode mmap écrit les experts dans un fichier à part -// (experts.bin), à compter en plus du téléchargement. -const strataMmapExtraGB = 45 +// Où vivent les experts que les cartes ne gardent pas (clé STRATA_EXPERTS du +// preset : auto, ram ou disk), et le mode effectivement lancé : +// +// drop experts de la carte d'appoint hors RAM, le reste verrouillé en RAM +// (le plus rapide mesuré par AJEAN) — demande un experts.bin existant +// ram tous les experts chargés et verrouillés en RAM +// arena experts.bin existant mappé tel quel (cache système) +// disk experts lus dans les fichiers du modèle (--mmap-experts) : rien +// n'est chargé au démarrage, le cache système garde ce que la RAM +// permet. Un peu plus lent, mais jamais tué faute de RAM. +// +// Vécu (2026-10) : 62 Go de RAM dont 18 déjà pris par le reste du serveur, le +// mode « hors RAM » choisi sur la RAM TOTALE, et un premier lancement qui +// écrivait experts.bin en chargeant ~41 Go en mémoire anonyme : le noyau a tué +// le moteur. D'où : décision sur la RAM DISPONIBLE au lancement, et plus jamais +// de premier lancement qui écrit experts.bin. +const ( + strataExpertsAuto = "auto" + strataExpertsRAM = "ram" + strataExpertsDisk = "disk" +) + +// memAvailableGB : la RAM réellement disponible (MemAvailable : libre + cache +// récupérable), en Go. Hors Linux ou illisible : la RAM totale. +func memAvailableGB() float64 { + b, err := os.ReadFile("/proc/meminfo") + if err != nil { + return totalRAMGB() + } + for _, line := range strings.Split(string(b), "\n") { + if v, ok := strings.CutPrefix(line, "MemAvailable:"); ok { + f := strings.Fields(v) + if len(f) > 0 { + if kb, err := strconv.ParseFloat(f[0], 64); err == nil { + return kb * 1024 / 1e9 + } + } + } + } + return totalRAMGB() +} + +// strataArenaGB : la taille des experts du quant d'un preset (lue dans le nom +// de sa config d'installeur : strata-swift-iq3_xxs.json → IQ3_XXS). 0 = inconnu. +func strataArenaGB(cfg map[string]string) float64 { + tag := strings.ToUpper(strataConfigTag(strings.TrimSpace(cfg["STRATA_CONFIG"]))) + tag = strings.TrimPrefix(tag, "SWIFT-") + if q, ok := strataQuants[tag]; ok { + return q.ArenaGB + } + return 0 +} + +// strataPackHasExpertsBin : experts.bin est déjà dans le pack (écrit par un +// lancement précédent ou par l'installeur en mode peu de RAM). +func strataPackHasExpertsBin(args []string) bool { + for i := 0; i+1 < len(args); i++ { + if args[i] == "--pack" { + if _, err := os.Stat(filepath.Join(args[i+1], "experts.bin")); err == nil { + return true + } + } + } + return false +} + +// strataResolveExperts : le mode lancé, d'après le choix du preset et la RAM +// disponible (availGB) ; helperGB = VRAM de la carte d'appoint (0 sans). +// Pure : la décision se teste en table. +func strataResolveExperts(cfg map[string]string, args []string, availGB, helperGB float64) string { + binReady := strataPackHasExpertsBin(args) && strataEngineInstalled() == strataEngineAsset + dropReady := binReady && strataHelperOn(cfg) && cfg["STRATA_DROP"] != "0" + arena := strataArenaGB(cfg) + switch strings.ToLower(strings.TrimSpace(cfg["STRATA_EXPERTS"])) { + case strataExpertsDisk: + if binReady { + return "arena" + } + return "disk" + case strataExpertsRAM: + if dropReady { + return "drop" + } + return "ram" + } + if arena <= 0 { // quant inconnu : on ne parie pas sur la RAM + return "disk" + } + if dropReady && availGB >= arena-strataHelperExpertsGB(helperGB)+strataDropHeadroomGB { + return "drop" + } + if availGB >= arena+strataLowRAMHeadroomGB { + return "ram" + } + if binReady { + return "arena" + } + return "disk" +} + +// strataExpertsLabel : le mode en clair, pour l'interface et le journal. +func strataExpertsLabel(mode string) string { + switch mode { + case "drop": + return "verrouillés en RAM, hors ceux de la carte d'appoint" + case "ram": + return "chargés et verrouillés en RAM" + case "arena": + return "experts.bin mappé (cache système)" + case "disk": + return "lus depuis le disque (mmap)" + } + return mode +} // strataMaxCtx : contexte natif maximal de Qwen3.8 Flash Next. const strataMaxCtx = 262144 @@ -162,6 +273,7 @@ type strataEnv struct { Main int `json:"main"` // index nvidia-smi de la carte principale Helper int `json:"helper"` // index de la carte d'aide, -1 sans RAMGB float64 `json:"ram_gb"` + AvailGB float64 `json:"avail_gb"` // RAM disponible (+ celle du moteur Strata en marche) DiskFreeGB float64 `json:"disk_free_gb"` Python string `json:"python,omitempty"` } @@ -171,6 +283,7 @@ const strataMinArch = 75 func strataDetect() strataEnv { env := strataEnv{Main: -1, Helper: -1, RAMGB: totalRAMGB()} + env.AvailGB = min(memAvailableGB()+strataEngineRSSGB(), env.RAMGB) if f := diskFree(LokiHome()); f > 0 { env.DiskFreeGB = float64(f) / 1e9 } @@ -281,32 +394,60 @@ func strataHaveGB(family, quant string) float64 { strataDirGB(filepath.Join(strataDataDir(), "mtp")) } -// strataNeedsMmap : les experts ne tiennent pas en RAM avec la marge -// nécessaire, ils seront lus en mmap depuis un fichier à part (experts.bin). -func strataNeedsMmap(q strataQuant, ramGB float64) bool { - return ramGB < q.ArenaGB+strataLowRAMHeadroomGB -} - -// strataDropFits : avec une carte d'aide, les experts qu'elle garde ne sont pas -// en RAM ; le reste est verrouillé en RAM (plus rapide que le mmap : génération -// ~64 au lieu de ~56 tok/s, lecture +17 à +38 %). Il faut que ce reste tienne. -func strataDropFits(q strataQuant, env strataEnv) bool { - if env.Helper < 0 { - return false +// strataEngineRSSGB : la RAM tenue par le moteur Strata qui tourne (serveur et +// moteur, arbre du service), en Go. Ajoutée à la RAM disponible quand on prédit +// pour CETTE machine : sinon le moteur en marche ferait croire qu'il ne tient pas. +func strataEngineRSSGB() float64 { + if !strataActive() { + return 0 } - for _, g := range env.GPUs { - if g.Index == env.Helper { - return env.RAMGB >= q.ArenaGB-strataHelperExpertsGB(g.VRAMGB)+strataDropHeadroomGB + root := readServicePID() + if root <= 0 { + return 0 + } + entries, err := os.ReadDir("/proc") + if err != nil { + return 0 + } + parent := map[int]int{} + rss := map[int]float64{} + for _, e := range entries { + pid, err := strconv.Atoi(e.Name()) + if err != nil { + continue + } + b, err := os.ReadFile(filepath.Join("/proc", e.Name(), "status")) + if err != nil { + continue + } + for _, line := range strings.Split(string(b), "\n") { + if v, ok := strings.CutPrefix(line, "PPid:"); ok { + parent[pid], _ = strconv.Atoi(strings.TrimSpace(v)) + } else if v, ok := strings.CutPrefix(line, "VmRSS:"); ok { + if f := strings.Fields(v); len(f) > 0 { + kb, _ := strconv.ParseFloat(f[0], 64) + rss[pid] = kb * 1024 / 1e9 + } + } } } - return false + var total float64 + for pid, r := range rss { + for p, n := pid, 0; p > 0 && n < 64; p, n = parent[p], n+1 { + if p == root { + total += r + break + } + } + } + return total } // strataFit : ce qu'un quant demande sur cette machine, et s'il y tient. type strataFit struct { Quant strataQuant `json:"quant"` DiskGB float64 `json:"disk_gb"` - Mmap bool `json:"mmap"` + Mmap bool `json:"mmap"` // en mode auto, les experts seraient lus depuis le disque Drop bool `json:"drop"` // experts de la carte d'aide hors RAM, le reste verrouillé OK bool `json:"ok"` Why string `json:"why,omitempty"` @@ -329,9 +470,15 @@ type strataDetails struct { CacheRoot string `json:"cache_root"` Mmap bool `json:"mmap"` Drop bool `json:"drop"` - MainGPU string `json:"main_gpu"` - HelperGPU string `json:"helper_gpu,omitempty"` - VisionGPU string `json:"vision_gpu,omitempty"` + // Experts : le choix du preset (auto, ram, disk) ; Mode : le mode lancé + // (drop, ram, arena, disk) — celui du dernier lancement pour le preset + // actif, sinon celui que la RAM disponible donnerait maintenant. + Experts string `json:"experts"` + Mode string `json:"mode"` + ModeLabel string `json:"mode_label"` + MainGPU string `json:"main_gpu"` + HelperGPU string `json:"helper_gpu,omitempty"` + VisionGPU string `json:"vision_gpu,omitempty"` // pour les réglages : ce que la machine permet et l'état actuel HasHelper bool `json:"has_helper"` // une carte d'aide est disponible HelperOn bool `json:"helper_on"` @@ -353,7 +500,13 @@ func strataDetailsFor(presetID string, env strataEnv) *strataDetails { if json.Unmarshal(raw, &base) != nil { return nil } - out, err := strataBuildConfig(base, pc, "") + mode := strataResolveExperts(pc, strataArgList(base), env.AvailGB, strataHelperVRAM(pc, env.GPUs)) + if active, _ := strataPresetActive(presetID); active { + if m := getStr(bkState, strataLaunchModeKey); m != "" { + mode = m + } + } + out, err := strataBuildConfig(base, pc, "", mode) if err != nil { return nil } @@ -379,9 +532,11 @@ func strataDetailsFor(presetID string, env strataEnv) *strataDetails { Spec: val("--spec"), Prefill: val("--prefill"), ShortRead: val("--short-read"), CacheRoot: val("--prompt-cache-root"), MainGPU: gpuName(pc["STRATA_MAIN_GPU"]), } - if e, ok := out["env"].(map[string]any); ok { - d.Mmap = fmt.Sprint(e["STRATA_ARENA_MMAP"]) == "1" - d.Drop = fmt.Sprint(e["STRATA_REMOTE_DROP"]) == "1" + d.Mode, d.ModeLabel = mode, strataExpertsLabel(mode) + d.Mmap, d.Drop = mode == "disk" || mode == "arena", mode == "drop" + d.Experts = strings.ToLower(strings.TrimSpace(pc["STRATA_EXPERTS"])) + if d.Experts != strataExpertsRAM && d.Experts != strataExpertsDisk { + d.Experts = strataExpertsAuto } if h := strings.TrimSpace(pc["STRATA_HELPER_GPU"]); h != "" && h != "-1" { d.HasHelper = true @@ -402,15 +557,11 @@ func strataDetailsFor(presetID string, env strataEnv) *strataDetails { } func strataFitFor(q strataQuant, env strataEnv, haveGB float64) strataFit { - f := strataFit{Quant: q, Mmap: strataNeedsMmap(q, env.RAMGB)} - if f.Mmap && strataDropFits(q, env) { - f.Mmap, f.Drop = false, true - } + // Prédiction du mode auto sur la RAM DISPONIBLE (pas la RAM totale) : sous + // la marge, les experts seront lus depuis le disque. Le mode « hors RAM » de + // la carte d'appoint demande un experts.bin, qu'une installation neuve n'a pas. + f := strataFit{Quant: q, Mmap: env.AvailGB < q.ArenaGB+strataLowRAMHeadroomGB} f.DiskGB = q.DownloadGB - // les deux modes lisent les experts dans experts.bin, écrit au premier lancement - if f.Mmap || f.Drop { - f.DiskGB += strataMmapExtraGB - } // ce qui est déjà là ne se retélécharge pas ; il reste toujours les // environnements (Python, bibliothèques CUDA, moteur) : ~10 Go f.DiskGB = max(f.DiskGB-haveGB, 10) @@ -646,8 +797,9 @@ func strataRunInstall(req strataInstallReq) { "STRATA_MAIN_GPU=" + strconv.Itoa(env.Main), "STRATA_HELPER_GPU=" + strconv.Itoa(env.Helper), "STRATA_VISION=1", - "STRATA_MMAP=" + strataBool(fit.Mmap), - "STRATA_DROP=" + strataBool(fit.Drop), + // experts placés au lancement selon la RAM disponible (strataResolveExperts) + "STRATA_EXPERTS=" + strataExpertsAuto, + "STRATA_DROP=1", // la meilleure qualité validée par AJEAN : cache KV en fp16 (32K en // VRAM, le reste en RAM pour ne pas prendre la place des experts) et 3 // jetons de brouillon MTP (mesuré meilleur que 4) @@ -773,8 +925,9 @@ func strataArgList(cfg map[string]any) []string { } // strataBuildConfig lit la config de l'installeur et y applique les réglages -// du preset. Pure (testable) : ne lance rien. -func strataBuildConfig(base map[string]any, cfg map[string]string, apiKey string) (map[string]any, error) { +// du preset, avec les experts placés selon `mode` (strataResolveExperts). Pure +// (testable) : ne lance rien. +func strataBuildConfig(base map[string]any, cfg map[string]string, apiKey, mode string) (map[string]any, error) { raw, _ := json.Marshal(base) var out map[string]any if err := json.Unmarshal(raw, &out); err != nil { @@ -811,32 +964,39 @@ func strataBuildConfig(base map[string]any, cfg map[string]string, apiKey string } } } - // Mode « peu de RAM » : l'installeur écrit --mmap-experts / --resident-experts. - // On applique à la place la variante mesurée par AJEAN : le pack experts.bin - // mappé tel quel (STRATA_ARENA_MMAP, le système garde en cache ce que la RAM - // permet), sans part PCIe (le mappage n'en donne pas l'adresse GPU). Le - // moteur écrit experts.bin lui-même au premier démarrage s'il manque. + // Experts (strataResolveExperts) : les drapeaux de l'installeur + // (--mmap-experts / --resident-experts) sont remplacés par ceux du mode. env := map[string]any{} if e, ok := out["env"].(map[string]any); ok { env = e } - if hasHelper && cfg["STRATA_DROP"] == "1" && strataDropReady(args) { - // Les experts de la carte d'aide hors RAM, le reste verrouillé en RAM - // (strataDropFits) : la principale relit aussi par PCIe une part de ce - // qui lui manque (part automatique) ; la réserve de la carte d'aide est figée. - args = strataDropFlag(strataDropFlag(args, "--mmap-experts"), "--resident-experts") + args = strataDropFlag(strataDropFlag(args, "--mmap-experts"), "--resident-experts") + delete(env, "STRATA_ARENA_MMAP") + delete(env, "STRATA_REMOTE_DROP") + switch mode { + case "drop": + if !hasHelper { + break // sans carte d'appoint, c'est le mode « ram » + } + // Les experts de la carte d'aide hors RAM, le reste verrouillé en RAM : + // la principale relit aussi par PCIe une part de ce qui lui manque (part + // automatique) ; la réserve de la carte d'aide est figée. args = strataDropFlagValue(args, "--pcie-frac") - delete(env, "STRATA_ARENA_MMAP") env["STRATA_REMOTE_DROP"] = "1" // ses experts sont relus de sa VRAM à la lecture d'un prompt, par les fils // de copie : 8 fils et 32 tampons (le moteur en prendrait 32 et 128) env["STRATA_STAGER_THREADS"] = "8" env["STRATA_STAGER_RING"] = "32" - } else if cfg["STRATA_MMAP"] == "1" || cfg["STRATA_DROP"] == "1" || strataHasArg(args, "--mmap-experts") || strataHasArg(args, "--resident-experts") { - // (STRATA_DROP sans experts.bin : ce lancement l'écrit, en mmap) - args = strataDropFlag(strataDropFlag(args, "--mmap-experts"), "--resident-experts") + case "arena": + // experts.bin mappé tel quel (variante mesurée par AJEAN), sans part PCIe : + // le mappage n'en donne pas l'adresse GPU. args = strataSetArg(args, "--pcie-frac", "0") env["STRATA_ARENA_MMAP"] = "1" + case "disk": + // Lus dans les fichiers du modèle par le cache système : rien n'est + // chargé au démarrage. Pas de part PCIe non plus (mémoire non épinglée). + args = append(args, "--mmap-experts") + args = strataSetArg(args, "--pcie-frac", "0") } for _, kv := range strataEnvTuning { k, v, _ := strings.Cut(kv, "=") @@ -874,23 +1034,50 @@ func strataBuildConfig(base map[string]any, cfg map[string]string, apiKey string return out, nil } -// strataDropReady : le mode « experts de la carte d'aide hors RAM » lit les -// experts dans experts.bin (écrit au premier lancement, en mmap) et demande le -// moteur du paquet. -func strataDropReady(args []string) bool { - pack := "" - for i := 0; i+1 < len(args); i++ { - if args[i] == "--pack" { - pack = args[i+1] +// strataLaunchModeKey : le mode des experts du dernier lancement (bkState), +// montré dans la fenêtre de Strata pour le preset actif. +const strataLaunchModeKey = "strata_experts_mode" + +// strataHelperVRAM : VRAM (Go) de la carte d'appoint du preset, 0 sans. +func strataHelperVRAM(cfg map[string]string, gpus []strataGPU) float64 { + if !strataHelperOn(cfg) { + return 0 + } + h := strings.TrimSpace(cfg["STRATA_HELPER_GPU"]) + for _, g := range gpus { + if strconv.Itoa(g.Index) == h { + return g.VRAMGB } } - if pack == "" { - return false + return 0 +} + +// strataPresetActive : le preset id est-il celui en service ? +func strataPresetActive(id string) (bool, error) { + list, err := ListPresets() + if err != nil { + return false, err } - if _, err := os.Stat(filepath.Join(pack, "experts.bin")); err != nil { - return false + for _, p := range list { + if p.ID == id { + return p.Active, nil + } } - return strataEngineInstalled() == strataEngineAsset + return false, nil +} + +// strataGPUs : les cartes NVIDIA vues par nvidia-smi (nil si indisponible). +func strataGPUs() []strataGPU { + gpus, err := detectGPUs() + if err != nil { + return nil + } + var out []strataGPU + for _, g := range gpus { + mib, _ := strconv.ParseFloat(g.MemTotal, 64) + out = append(out, strataGPU{Index: g.Index, Name: g.Name, VRAMGB: mib / 1024}) + } + return out } // strataEngineMarker : le paquet dont vient le moteur installé. @@ -1009,7 +1196,14 @@ func serveStrata(cfg map[string]string) error { fmt.Fprintf(os.Stderr, "[loki serve] moteur %s non installé (%v) : l'ancien reste en place\n", strataEngineAsset, err) } apiKey, _ := effectiveAPIKeyErr() - final, err := strataBuildConfig(base, cfg, apiKey) + // Experts : décidé ICI, sur la RAM disponible au moment du lancement (l'ancien + // moteur est déjà arrêté), pas sur la RAM totale. + avail := memAvailableGB() + mode := strataResolveExperts(cfg, strataArgList(base), avail, strataHelperVRAM(cfg, strataGPUs())) + fmt.Fprintf(os.Stderr, "%s experts : %s (choix %s, %.0f Go de RAM disponibles, %.0f Go d'experts)\n", + strataServeMarker, strataExpertsLabel(mode), firstNonEmpty(cfg["STRATA_EXPERTS"], strataExpertsAuto), avail, strataArenaGB(cfg)) + _ = putStr(bkState, strataLaunchModeKey, mode) + final, err := strataBuildConfig(base, cfg, apiKey, mode) if err != nil { return err } @@ -1110,12 +1304,13 @@ func handleStrata(w http.ResponseWriter, r *http.Request) { // strataSettingsReq : les réglages modifiables d'un modèle installé. type strataSettingsReq struct { - Preset string `json:"preset"` - Ctx int `json:"ctx"` - KV string `json:"kv"` - Spec int `json:"spec"` - Vision bool `json:"vision"` - Helper bool `json:"helper"` + Preset string `json:"preset"` + Ctx int `json:"ctx"` + KV string `json:"kv"` + Spec int `json:"spec"` + Vision bool `json:"vision"` + Helper bool `json:"helper"` + Experts string `json:"experts"` // auto, ram ou disk ; vide = inchangé } // strataApplySettings réécrit les clés du preset (pure, testable). @@ -1128,11 +1323,18 @@ func strataApplySettings(content string, r strataSettingsReq) (string, error) { return "", fmt.Errorf("cache KV inconnu : %s", r.KV) case r.Spec < 2 || r.Spec > 4: return "", fmt.Errorf("MTP hors limites : %d", r.Spec) + case r.Experts != "" && r.Experts != strataExpertsAuto && r.Experts != strataExpertsRAM && r.Experts != strataExpertsDisk: + return "", fmt.Errorf("placement des experts inconnu : %s", r.Experts) } set := map[string]string{ "CTX": strconv.Itoa(r.Ctx), "STRATA_KV": r.KV, "STRATA_SPEC": strconv.Itoa(r.Spec), "STRATA_VISION": strataBool(r.Vision), "STRATA_HELPER": strataBool(r.Helper), } + keys := []string{"CTX", "STRATA_KV", "STRATA_SPEC", "STRATA_VISION", "STRATA_HELPER"} + if r.Experts != "" { + set["STRATA_EXPERTS"] = r.Experts + keys = append(keys, "STRATA_EXPERTS") + } var lines []string for _, l := range strings.Split(strings.TrimRight(content, "\n"), "\n") { k, _, _ := strings.Cut(strings.TrimSpace(l), "=") @@ -1141,7 +1343,7 @@ func strataApplySettings(content string, r strataSettingsReq) (string, error) { } lines = append(lines, l) } - for _, k := range []string{"CTX", "STRATA_KV", "STRATA_SPEC", "STRATA_VISION", "STRATA_HELPER"} { + for _, k := range keys { lines = append(lines, k+"="+set[k]) } return strings.Join(lines, "\n") + "\n", nil diff --git a/internal/loki/backend_strata_test.go b/internal/loki/backend_strata_test.go index 92ab9a6..dc7545f 100644 --- a/internal/loki/backend_strata_test.go +++ b/internal/loki/backend_strata_test.go @@ -31,7 +31,7 @@ func TestStrataBuildConfigApplyTuning(t *testing.T) { "args": []any{"--pack", "/p", "--prefill", "8192", "--max-context", "131072"}, "port": 8080.0, } - out, err := strataBuildConfig(base, map[string]string{"PORT": "8081", "CTX": "65536"}, "k") + out, err := strataBuildConfig(base, map[string]string{"PORT": "8081", "CTX": "65536"}, "k", "ram") if err != nil { t.Fatal(err) } @@ -59,7 +59,7 @@ func TestStrataBuildConfigApplyTuning(t *testing.T) { func TestStrataBuildConfigHelperGPU(t *testing.T) { base := map[string]any{"args": []any{"--pack", "/p", "--mmap-experts", "--vram-reserve-mib", "700"}, "gpu": 1.0, "vision": map[string]any{"exe": "v", "gpu": true}} - out, err := strataBuildConfig(base, map[string]string{"STRATA_HELPER_GPU": "0"}, "") + out, err := strataBuildConfig(base, map[string]string{"STRATA_HELPER_GPU": "0"}, "", "ram") if err != nil { t.Fatal(err) } @@ -84,70 +84,6 @@ func TestStrataBuildConfigHelperGPU(t *testing.T) { } } -func TestStrataDropFits(t *testing.T) { - q := strataQuants["IQ3_XXS"] - env := strataEnv{Main: 1, Helper: 0, RAMGB: 46.9, GPUs: []strataGPU{{Index: 0, VRAMGB: 7.8}, {Index: 1, VRAMGB: 15.9}}} - if !strataDropFits(q, env) { - t.Error("serveur de test (46 Go + 3070) : le mode hors RAM devrait tenir") - } - f := strataFitFor(q, env, 0) - if f.Mmap || !f.Drop { - t.Errorf("fit = mmap %v drop %v, attendu drop", f.Mmap, f.Drop) - } - env.Helper = -1 - if strataDropFits(q, env) { - t.Error("sans carte d'aide, pas de mode hors RAM") - } - env.Helper, env.RAMGB = 0, 32 - if strataDropFits(q, env) { - t.Error("32 Go : ne tient pas, doit rester en mmap") - } -} - -func TestStrataBuildConfigDrop(t *testing.T) { - home := t.TempDir() - t.Setenv("LOKI_HOME", home) - pack := filepath.Join(home, "pack") - _ = os.MkdirAll(pack, 0o755) - base := map[string]any{"args": []any{"--pack", pack, "--pcie-frac", "0.3"}, "env": map[string]any{}} - cfg := map[string]string{"STRATA_HELPER_GPU": "0", "STRATA_DROP": "1", "STRATA_MMAP": "0"} - // sans experts.bin ni moteur à jour : repli mmap (ce lancement écrit experts.bin) - out, _ := strataBuildConfig(base, cfg, "") - if out["env"].(map[string]any)["STRATA_ARENA_MMAP"] != "1" || out["env"].(map[string]any)["STRATA_REMOTE_DROP"] != nil { - t.Fatalf("repli mmap attendu : %v", out["env"]) - } - _ = os.WriteFile(filepath.Join(pack, "experts.bin"), []byte("x"), 0o644) - _ = os.MkdirAll(filepath.Join(strataSrcDir(), "engine-cuda12"), 0o755) - _ = os.WriteFile(strataEngineMarker(), []byte(strataEngineAsset+"\n"), 0o644) - out, _ = strataBuildConfig(base, cfg, "") - e := out["env"].(map[string]any) - if e["STRATA_REMOTE_DROP"] != "1" || e["STRATA_ARENA_MMAP"] != nil { - t.Errorf("mode hors RAM attendu : %v", e) - } - if a := strataArgs(t, out); strataHasArg(a, "--pcie-frac") { - t.Errorf("--pcie-frac gardé (la part PCIe doit être automatique) : %v", a) - } - // carte d'aide coupée : pas de mode hors RAM - cfg["STRATA_HELPER"] = "0" - out, _ = strataBuildConfig(base, cfg, "") - if out["env"].(map[string]any)["STRATA_REMOTE_DROP"] != nil { - t.Error("mode hors RAM sans carte d'aide") - } -} - -func TestStrataBuildConfigMmap(t *testing.T) { - base := map[string]any{"args": []any{"--pack", "/p", "--mmap-experts"}, "env": map[string]any{"X": "1"}} - out, err := strataBuildConfig(base, map[string]string{"STRATA_HELPER_GPU": "0"}, "") - if err != nil { - t.Fatal(err) - } - a := strataArgs(t, out) - env := out["env"].(map[string]any) - if strataHasArg(a, "--mmap-experts") || argAfter(a, "--pcie-frac") != "0" || env["STRATA_ARENA_MMAP"] != "1" || env["X"] != "1" { - t.Errorf("mmap validé non appliqué : %v %v", a, env) - } -} - func TestStrataCudaDevices(t *testing.T) { cases := []struct { main, helper, want string @@ -167,7 +103,7 @@ func TestStrataCudaDevices(t *testing.T) { func TestStrataRecommend(t *testing.T) { // la machine de test : 5060 Ti 16 Go + 3070 8 Go, 47 Go de RAM, disque large - env := strataEnv{Supported: true, Main: 1, Helper: 0, RAMGB: 47, DiskFreeGB: 400, + env := strataEnv{Supported: true, Main: 1, Helper: 0, RAMGB: 47, AvailGB: 47, DiskFreeGB: 400, GPUs: []strataGPU{{Index: 0, VRAMGB: 8, Arch: 86}, {Index: 1, VRAMGB: 16, Arch: 120}}} if got := strataRecommend("swift", env); got != "IQ3_XXS" { t.Errorf("swift : %q, attendu IQ3_XXS", got) @@ -177,12 +113,13 @@ func TestStrataRecommend(t *testing.T) { t.Errorf("qwen 47 Go : %q, attendu IQ3_XXS", got) } f := strataFitFor(strataQuants["IQ3_XXS"], env, 0) - // trop juste pour tout garder en RAM, mais la 3070 garde les siens : mode hors RAM - if !f.OK || f.Mmap || !f.Drop { - t.Errorf("IQ3_XXS sur 47 Go : ok=%v mmap=%v drop=%v (attendu ok, hors RAM)", f.OK, f.Mmap, f.Drop) + // trop juste pour tout garder en RAM : experts lus depuis le disque (le mode + // hors RAM demande un experts.bin, qu'une installation neuve n'a pas) + if !f.OK || !f.Mmap || f.Drop { + t.Errorf("IQ3_XXS sur 47 Go : ok=%v mmap=%v drop=%v (attendu ok, disque)", f.OK, f.Mmap, f.Drop) } // 64 Go : IQ3_S tient en RAM - env.RAMGB = 64 + env.RAMGB, env.AvailGB = 64, 64 if got := strataRecommend("qwen", env); got != "IQ3_S" { t.Errorf("qwen 64 Go : %q, attendu IQ3_S", got) } @@ -208,22 +145,6 @@ func TestStrataPresetName(t *testing.T) { } } -func TestStrataBuildConfigMmapFromPreset(t *testing.T) { - base := map[string]any{"args": []any{"--pack", "/p"}} - out, err := strataBuildConfig(base, map[string]string{"STRATA_MMAP": "1"}, "") - if err != nil { - t.Fatal(err) - } - a := strataArgs(t, out) - if argAfter(a, "--pcie-frac") != "0" || out["env"].(map[string]any)["STRATA_ARENA_MMAP"] != "1" { - t.Errorf("STRATA_MMAP=1 non appliqué : %v %v", a, out["env"]) - } - out, _ = strataBuildConfig(base, map[string]string{"STRATA_MMAP": "0"}, "") - if out["env"].(map[string]any)["STRATA_ARENA_MMAP"] != nil { - t.Errorf("mmap appliqué sans STRATA_MMAP : %v", out["env"]) - } -} - func TestStrataFitCountsExistingData(t *testing.T) { env := strataEnv{Supported: true, Main: 1, Helper: 0, RAMGB: 47, DiskFreeGB: 32, GPUs: []strataGPU{{Index: 0, VRAMGB: 8, Arch: 86}, {Index: 1, VRAMGB: 16, Arch: 120}}} @@ -237,7 +158,7 @@ func TestStrataFitCountsExistingData(t *testing.T) { func TestStrataBuildConfigPresetOverrides(t *testing.T) { base := map[string]any{"args": []any{"--kv", "int8", "--spec", "4"}} - out, _ := strataBuildConfig(base, map[string]string{"STRATA_KV": "fp16", "STRATA_SPEC": "3"}, "") + out, _ := strataBuildConfig(base, map[string]string{"STRATA_KV": "fp16", "STRATA_SPEC": "3"}, "", "ram") a := strataArgs(t, out) if argAfter(a, "--kv") != "fp16" || argAfter(a, "--spec") != "3" { t.Errorf("%v", a) @@ -307,7 +228,7 @@ func TestStrataApplySettings(t *testing.T) { t.Error("carte d'aide coupée mais encore utilisée") } base := map[string]any{"args": []any{"--vision", "--pack", "/p"}, "vision": map[string]any{"exe": "v"}} - b, _ := strataBuildConfig(base, cfg, "") + b, _ := strataBuildConfig(base, cfg, "", "ram") if strataHasArg(strataArgs(t, b), "--vision") || b["vision"] != nil || strataHasArg(strataArgs(t, b), "--expert-cache-device1") { t.Errorf("vision / aide coupées mais présentes : %v", b) } @@ -341,3 +262,103 @@ func TestModelLoadErrorStrata(t *testing.T) { t.Errorf("ancienne trace prise pour l'échec en cours : %q", got) } } + +// Où vont les experts : décidé sur la RAM DISPONIBLE. Vécu : 62 Go dont 18 pris +// ailleurs, mode « hors RAM » choisi sur la RAM totale, moteur tué par le noyau. +func TestStrataResolveExperts(t *testing.T) { + home := t.TempDir() + t.Setenv("LOKI_HOME", home) + pack := filepath.Join(home, "pack") + _ = os.MkdirAll(pack, 0o755) + args := []string{"--pack", pack} + cfg := map[string]string{"STRATA_CONFIG": "/x/strata-swift-iq3_xxs.json", "STRATA_HELPER_GPU": "1", "STRATA_DROP": "1"} + // IQ3_XXS : 42,9 Go d'experts + cases := []struct { + experts string + avail float64 + want string + }{ + {"", 44, "disk"}, // le cas vécu : 44 Go disponibles, pas de experts.bin + {"", 60, "ram"}, // assez de RAM pour tout charger avec la marge + {"auto", 52.8, "disk"}, + {"disk", 60, "disk"}, // choix de l'utilisateur + {"ram", 20, "ram"}, // choix de l'utilisateur, même à l'étroit + } + for _, c := range cases { + cfg["STRATA_EXPERTS"] = c.experts + if got := strataResolveExperts(cfg, args, c.avail, 12); got != c.want { + t.Errorf("experts=%q dispo=%.0f : %s, attendu %s", c.experts, c.avail, got, c.want) + } + } + // experts.bin présent (moteur du paquet) : le mode hors RAM et le mappage deviennent possibles + _ = os.WriteFile(filepath.Join(pack, "experts.bin"), []byte("x"), 0o644) + _ = os.MkdirAll(filepath.Join(strataSrcDir(), "engine-cuda12"), 0o755) + _ = os.WriteFile(strataEngineMarker(), []byte(strataEngineAsset+"\n"), 0o644) + cfg["STRATA_EXPERTS"] = "" + if got := strataResolveExperts(cfg, args, 44, 12); got != "drop" { + t.Errorf("44 Go + carte d'appoint 12 Go + experts.bin : %s, attendu drop", got) + } + if got := strataResolveExperts(cfg, args, 30, 12); got != "arena" { + t.Errorf("30 Go + experts.bin : %s, attendu arena", got) + } + cfg["STRATA_HELPER"] = "0" + if got := strataResolveExperts(cfg, args, 44, 12); got != "arena" { + t.Errorf("carte d'appoint coupée : %s, attendu arena", got) + } + cfg["STRATA_EXPERTS"] = "disk" + if got := strataResolveExperts(cfg, args, 60, 12); got != "arena" { + t.Errorf("disque demandé avec experts.bin : %s, attendu arena", got) + } + // quant inconnu : on ne parie pas sur la RAM + if got := strataResolveExperts(map[string]string{"STRATA_CONFIG": "/x/autre.json"}, nil, 500, 0); got != "disk" { + t.Errorf("quant inconnu : %s, attendu disk", got) + } +} + +func TestStrataBuildConfigModes(t *testing.T) { + base := map[string]any{"args": []any{"--pack", "/p", "--resident-experts", "--pcie-frac", "0.3"}, "env": map[string]any{"X": "1"}} + cfg := map[string]string{"STRATA_HELPER_GPU": "1"} + env := func(m map[string]any) map[string]any { return m["env"].(map[string]any) } + + out, _ := strataBuildConfig(base, cfg, "", "disk") + a := strataArgs(t, out) + if !strataHasArg(a, "--mmap-experts") || strataHasArg(a, "--resident-experts") || argAfter(a, "--pcie-frac") != "0" || + env(out)["STRATA_ARENA_MMAP"] != nil || env(out)["X"] != "1" { + t.Errorf("disk : %v %v", a, env(out)) + } + out, _ = strataBuildConfig(base, cfg, "", "ram") + a = strataArgs(t, out) + if strataHasArg(a, "--mmap-experts") || strataHasArg(a, "--resident-experts") || env(out)["STRATA_ARENA_MMAP"] != nil || env(out)["STRATA_REMOTE_DROP"] != nil { + t.Errorf("ram : %v %v", a, env(out)) + } + out, _ = strataBuildConfig(base, cfg, "", "arena") + if a = strataArgs(t, out); argAfter(a, "--pcie-frac") != "0" || env(out)["STRATA_ARENA_MMAP"] != "1" { + t.Errorf("arena : %v %v", a, env(out)) + } + out, _ = strataBuildConfig(base, cfg, "", "drop") + if a = strataArgs(t, out); strataHasArg(a, "--pcie-frac") || env(out)["STRATA_REMOTE_DROP"] != "1" { + t.Errorf("drop : la part PCIe doit être automatique : %v %v", a, env(out)) + } + // drop sans carte d'appoint : rien de spécial, c'est le mode ram + out, _ = strataBuildConfig(base, map[string]string{"STRATA_HELPER_GPU": "-1"}, "", "drop") + if env(out)["STRATA_REMOTE_DROP"] != nil { + t.Error("mode hors RAM sans carte d'appoint") + } +} + +func TestStrataApplySettingsExperts(t *testing.T) { + in := "ENGINE=strata\nSTRATA_EXPERTS=auto\n" + r := strataSettingsReq{Ctx: 131072, KV: "fp16", Spec: 3, Experts: "disk"} + out, err := strataApplySettings(in, r) + if err != nil || parseEnv(out)["STRATA_EXPERTS"] != "disk" { + t.Fatalf("%v %q", err, out) + } + r.Experts = "nvme" + if _, err := strataApplySettings(in, r); err == nil { + t.Error("placement inconnu accepté") + } + r.Experts = "" + if out, _ := strataApplySettings(in, r); parseEnv(out)["STRATA_EXPERTS"] != "auto" { + t.Errorf("vide doit laisser le choix en place : %q", out) + } +} diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index eb7ec46..2a56518 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -4521,7 +4521,7 @@ async function loadCfg(){ rows.push(row('CTX', d.ctx)); rows.push(row('KV', d.kv+(d.kv_resident ? ' · '+k(d.kv_resident)+' VRAM' : ''))); rows.push(row('MTP', d.spec)); - rows.push(row('EXPERTS', d.drop ? 'mlock' : d.mmap ? 'mmap' : 'RAM')); + rows.push(row('EXPERTS', ({drop:'mlock (hors appoint)', ram:'RAM', arena:'experts.bin', disk:'mmap (disque)'})[d.mode] || (d.mmap ? 'mmap' : 'RAM'), d.mode_label||'')); if(d.vision_gpu) rows.push(row('VISION', 'on', d.vision_gpu)); }else if(c.CTX) rows.push(row('CTX', c.CTX)); document.getElementById('cfg').innerHTML = rows.join('') + await engineCmdlineRow(); @@ -9188,8 +9188,9 @@ async function openStrata(presetId){ .sort((a,b)=>(a.index===env.main?-1:1)) .map(g=>g.name.replace(/^NVIDIA GeForce /,'')+' '+Math.round(g.vram_gb)+' Go'); document.getElementById('strata-machine').textContent = - 'Machine : '+cards.join(' + ')+', '+Math.round(env.ram_gb)+' Go de RAM, ' - +Math.round(env.disk_free_gb)+' Go libres. Réglages automatiques.'; + 'Machine : '+cards.join(' + ')+', '+Math.round(env.ram_gb)+' Go de RAM dont ' + +Math.round(env.avail_gb||env.ram_gb)+' disponibles, ' + +Math.round(env.disk_free_gb)+' Go de disque libres. Réglages automatiques.'; if(!strataFam) strataFam = (s.families[0]||{}).id || ''; if(strataTarget){ for(const f of s.families){ @@ -9241,7 +9242,7 @@ function strataRenderQuants(pickReco){ strataRenderDetails(fit); document.getElementById('strata-note').textContent = !fit ? 'Aucune version ne tient sur cette machine.' : fit.active ? 'Modèle actif.' : fit.preset ? 'Modèle installé, prêt à être activé.' - : fit.mmap ? 'RAM insuffisante pour tout charger : experts lus depuis le disque (mmap, ~45 Go de plus). Installation et activation automatiques.' + : fit.mmap ? 'RAM disponible insuffisante pour tout charger : les experts seront lus depuis le disque (mmap, un peu plus lent). Installation et activation automatiques.' : 'Installation et activation automatiques. Premier chargement : quelques minutes.'; } @@ -9270,6 +9271,9 @@ function strataRenderSettings(fit){ ['Contexte', ''], ['Cache KV', sel('strata-s-kv', [['fp16', 'fp16 (meilleure qualité)'],['int8', '8 bits (plus léger)']], d.kv)], ['Prédiction (MTP)', sel('strata-s-spec', [[2,'2'],[3,'3'],[4,'4']], d.spec)], + // Placement des experts : auto décide AU LANCEMENT sur la RAM disponible ; + // « disque » ne charge rien en RAM au démarrage (jamais tué faute de RAM). + ['Experts', sel('strata-s-experts', [['auto','auto (selon la RAM libre)'],['ram','en RAM (le plus rapide)'],['disk','depuis le disque (mmap, économe)']], d.experts||'auto')], ['Lire les images', sw('strata-s-vision', d.vision)], ]; if(d.has_helper) rows.push(["Carte d'appoint", sw('strata-s-helper', d.helper_on)]); @@ -9292,7 +9296,8 @@ async function strataSaveSettings(){ if(!fit || !fit.preset) return; const v = id => document.getElementById(id); const body = {preset: fit.preset, ctx: +v('strata-s-ctx').value, kv: v('strata-s-kv').value, spec: +v('strata-s-spec').value, - vision: v('strata-s-vision').checked, helper: v('strata-s-helper') ? v('strata-s-helper').checked : false}; + vision: v('strata-s-vision').checked, helper: v('strata-s-helper') ? v('strata-s-helper').checked : false, + experts: v('strata-s-experts') ? v('strata-s-experts').value : ''}; if(fit.active && !await askConfirm('Le modèle va redémarrer avec ces réglages (quelques minutes).', {title:'Strata — Qwen3.8 Flash Next', okText:'Enregistrer'})) return; const r = await jpost('/api/strata/settings', body); if(!r.ok){ toast('Erreur : '+(r.error||'')); return; } @@ -9316,7 +9321,8 @@ function strataRenderDetails(fit){ ['Contexte', (+d.ctx).toLocaleString('fr-FR')+' jetons'], ['Cache KV', d.kv + (d.kv_resident ? ' · '+k(d.kv_resident)+' en VRAM, le reste en RAM' : '')], ['Prédiction (MTP)', d.spec+' jetons'], - ['Experts hors GPU', d.drop ? "verrouillés en RAM, hors ceux de la carte d'appoint" : d.mmap ? 'lus depuis le disque (mmap)' : 'chargés en RAM'], + ['Experts hors GPU', (d.mode_label || (d.mmap ? 'lus depuis le disque (mmap)' : 'chargés en RAM')) + + (fit.active ? ' — dernier lancement' : ' — selon la RAM libre maintenant')], ['Lecture des prompts', 'blocs de '+k(d.prefill)+', lecture directe sous '+d.short_read+' jetons'], ["Démarrage d'une conversation", 'reprise après le prompt système (dès '+d.cache_root+' jetons)'], ['Version du moteur', 'paquet AJEAN MoE '+d.version], diff --git a/internal/loki/ui/src/js/05-status.js b/internal/loki/ui/src/js/05-status.js index c19b8a7..33bdd4b 100644 --- a/internal/loki/ui/src/js/05-status.js +++ b/internal/loki/ui/src/js/05-status.js @@ -330,7 +330,7 @@ async function loadCfg(){ rows.push(row('CTX', d.ctx)); rows.push(row('KV', d.kv+(d.kv_resident ? ' · '+k(d.kv_resident)+' VRAM' : ''))); rows.push(row('MTP', d.spec)); - rows.push(row('EXPERTS', d.drop ? 'mlock' : d.mmap ? 'mmap' : 'RAM')); + rows.push(row('EXPERTS', ({drop:'mlock (hors appoint)', ram:'RAM', arena:'experts.bin', disk:'mmap (disque)'})[d.mode] || (d.mmap ? 'mmap' : 'RAM'), d.mode_label||'')); if(d.vision_gpu) rows.push(row('VISION', 'on', d.vision_gpu)); }else if(c.CTX) rows.push(row('CTX', c.CTX)); document.getElementById('cfg').innerHTML = rows.join('') + await engineCmdlineRow(); diff --git a/internal/loki/ui/src/js/10b-strata.js b/internal/loki/ui/src/js/10b-strata.js index 73158b2..c4cd6ea 100644 --- a/internal/loki/ui/src/js/10b-strata.js +++ b/internal/loki/ui/src/js/10b-strata.js @@ -40,8 +40,9 @@ async function openStrata(presetId){ .sort((a,b)=>(a.index===env.main?-1:1)) .map(g=>g.name.replace(/^NVIDIA GeForce /,'')+' '+Math.round(g.vram_gb)+' Go'); document.getElementById('strata-machine').textContent = - 'Machine : '+cards.join(' + ')+', '+Math.round(env.ram_gb)+' Go de RAM, ' - +Math.round(env.disk_free_gb)+' Go libres. Réglages automatiques.'; + 'Machine : '+cards.join(' + ')+', '+Math.round(env.ram_gb)+' Go de RAM dont ' + +Math.round(env.avail_gb||env.ram_gb)+' disponibles, ' + +Math.round(env.disk_free_gb)+' Go de disque libres. Réglages automatiques.'; if(!strataFam) strataFam = (s.families[0]||{}).id || ''; if(strataTarget){ for(const f of s.families){ @@ -93,7 +94,7 @@ function strataRenderQuants(pickReco){ strataRenderDetails(fit); document.getElementById('strata-note').textContent = !fit ? 'Aucune version ne tient sur cette machine.' : fit.active ? 'Modèle actif.' : fit.preset ? 'Modèle installé, prêt à être activé.' - : fit.mmap ? 'RAM insuffisante pour tout charger : experts lus depuis le disque (mmap, ~45 Go de plus). Installation et activation automatiques.' + : fit.mmap ? 'RAM disponible insuffisante pour tout charger : les experts seront lus depuis le disque (mmap, un peu plus lent). Installation et activation automatiques.' : 'Installation et activation automatiques. Premier chargement : quelques minutes.'; } @@ -122,6 +123,9 @@ function strataRenderSettings(fit){ ['Contexte', ''], ['Cache KV', sel('strata-s-kv', [['fp16', 'fp16 (meilleure qualité)'],['int8', '8 bits (plus léger)']], d.kv)], ['Prédiction (MTP)', sel('strata-s-spec', [[2,'2'],[3,'3'],[4,'4']], d.spec)], + // Placement des experts : auto décide AU LANCEMENT sur la RAM disponible ; + // « disque » ne charge rien en RAM au démarrage (jamais tué faute de RAM). + ['Experts', sel('strata-s-experts', [['auto','auto (selon la RAM libre)'],['ram','en RAM (le plus rapide)'],['disk','depuis le disque (mmap, économe)']], d.experts||'auto')], ['Lire les images', sw('strata-s-vision', d.vision)], ]; if(d.has_helper) rows.push(["Carte d'appoint", sw('strata-s-helper', d.helper_on)]); @@ -144,7 +148,8 @@ async function strataSaveSettings(){ if(!fit || !fit.preset) return; const v = id => document.getElementById(id); const body = {preset: fit.preset, ctx: +v('strata-s-ctx').value, kv: v('strata-s-kv').value, spec: +v('strata-s-spec').value, - vision: v('strata-s-vision').checked, helper: v('strata-s-helper') ? v('strata-s-helper').checked : false}; + vision: v('strata-s-vision').checked, helper: v('strata-s-helper') ? v('strata-s-helper').checked : false, + experts: v('strata-s-experts') ? v('strata-s-experts').value : ''}; if(fit.active && !await askConfirm('Le modèle va redémarrer avec ces réglages (quelques minutes).', {title:'Strata — Qwen3.8 Flash Next', okText:'Enregistrer'})) return; const r = await jpost('/api/strata/settings', body); if(!r.ok){ toast('Erreur : '+(r.error||'')); return; } @@ -168,7 +173,8 @@ function strataRenderDetails(fit){ ['Contexte', (+d.ctx).toLocaleString('fr-FR')+' jetons'], ['Cache KV', d.kv + (d.kv_resident ? ' · '+k(d.kv_resident)+' en VRAM, le reste en RAM' : '')], ['Prédiction (MTP)', d.spec+' jetons'], - ['Experts hors GPU', d.drop ? "verrouillés en RAM, hors ceux de la carte d'appoint" : d.mmap ? 'lus depuis le disque (mmap)' : 'chargés en RAM'], + ['Experts hors GPU', (d.mode_label || (d.mmap ? 'lus depuis le disque (mmap)' : 'chargés en RAM')) + + (fit.active ? ' — dernier lancement' : ' — selon la RAM libre maintenant')], ['Lecture des prompts', 'blocs de '+k(d.prefill)+', lecture directe sous '+d.short_read+' jetons'], ["Démarrage d'une conversation", 'reprise après le prompt système (dès '+d.cache_root+' jetons)'], ['Version du moteur', 'paquet AJEAN MoE '+d.version],