From 30ce3a4e86079c7848c3715fa968d6ba1498a6ef Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:13:56 +0000 Subject: [PATCH] =?UTF-8?q?MTP=20:=20MODEL=5FDRAFT=20=C3=A9gal=20au=20mod?= =?UTF-8?q?=C3=A8le=20principal=20ignor=C3=A9=20(poids=20charg=C3=A9s=20de?= =?UTF-8?q?ux=20fois)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Choisir le modèle lui-même comme brouillon dans l'éditeur de preset écrivait MODEL_DRAFT=, et Loki passait -md vers ce fichier : common_speculative_init_result charge alors un second exemplaire complet du modèle, au lieu de greffer le contexte MTP sur le modèle déjà en VRAM. Swift 27B : « allocating 9628.34 MiB on device 0 » pour le brouillon, moteur mort ; quand ça passait, couches renvoyées sur le CPU et 5 t/s au lieu de 22 (prefill 290 t/s au lieu de 890). probeSpec reconnaît ce cas (os.SameFile), specArgs l'ignore et le dit : la tête MTP intégrée sert de brouillon sans -md. L'éditeur ne propose plus le modèle principal dans la liste des brouillons. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014wfx8uW1un1WwWJ6xzAbkV --- internal/loki/backend_serve.go | 1 + internal/loki/backend_serve_spec.go | 22 +++++++++++++++++ internal/loki/backend_serve_spec_test.go | 31 ++++++++++++++++++++++++ internal/loki/ui/index.html | 9 +++++-- internal/loki/ui/src/js/07-models.js | 9 +++++-- 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/internal/loki/backend_serve.go b/internal/loki/backend_serve.go index 58715a3..41fc11f 100644 --- a/internal/loki/backend_serve.go +++ b/internal/loki/backend_serve.go @@ -527,6 +527,7 @@ type serveSysInfo struct { Draft string // MODEL_DRAFT résolu et vérifié ; vide = absent ou introuvable DraftErr string // pourquoi MODEL_DRAFT n'a pas été trouvé DraftGGUF *GGUFInfo // métadonnées du brouillon ; nil = illisibles + DraftIsModel bool // MODEL_DRAFT désigne le modèle principal lui-même (voir specArgs) SpecAutoBlocked string // un essai automatique précédent a échoué : la raison // Cartes listées par le moteur (--list-devices), lues seulement pour diff --git a/internal/loki/backend_serve_spec.go b/internal/loki/backend_serve_spec.go index 6304bce..ab268a1 100644 --- a/internal/loki/backend_serve_spec.go +++ b/internal/loki/backend_serve_spec.go @@ -113,6 +113,11 @@ func probeSpec(cfg map[string]string, si *serveSysInfo) { return } si.Draft = p + if a, err := os.Stat(p); err == nil { + if b, err := os.Stat(si.Model); err == nil && os.SameFile(a, b) { + si.DraftIsModel = true + } + } if g, err := ggufMeta(p); err == nil { si.DraftGGUF = &g } @@ -241,6 +246,23 @@ func specAutoBlocker(cfg map[string]string, extra []string, si serveSysInfo) str // et s'ils viennent de SPEC=auto (cmdServe pose alors le jeton de tentative). // Fonction pure ; cmdServe a déjà résolu MODEL_DRAFT et lu les GGUF. func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, notes []string, auto bool) { + // MODEL_DRAFT = le modèle principal lui-même (choisi dans la liste des + // brouillons de l'éditeur) : -md recharge alors les 27B une seconde fois + // (common_speculative_init_result charge tout fichier -md comme un modèle + // à part), quand la tête MTP intégrée se greffe sur le modèle déjà en VRAM. + // Vécu : « allocating 9628.34 MiB on device 0 » pour le brouillon, moteur + // mort ; ou, quand ça passait, des couches renvoyées sur le CPU et 5 t/s au + // lieu de 22. On l'ignore : même tête, sans les poids en double. + if si.DraftIsModel && strings.TrimSpace(cfg["MODEL_DRAFT"]) != "" { + c := make(map[string]string, len(cfg)) + for k, v := range cfg { + c[k] = v + } + delete(c, "MODEL_DRAFT") + args, notes, auto = specArgs(c, extra, si) + return args, append([]string{"MODEL_DRAFT est le modèle principal lui-même : ignoré — la tête MTP intégrée " + + "sert de brouillon sans recharger les poids (retire MODEL_DRAFT du preset)"}, notes...), auto + } mode := specMode(cfg) draftKey := strings.TrimSpace(cfg["MODEL_DRAFT"]) switch mode { diff --git a/internal/loki/backend_serve_spec_test.go b/internal/loki/backend_serve_spec_test.go index a3a28d4..bd19621 100644 --- a/internal/loki/backend_serve_spec_test.go +++ b/internal/loki/backend_serve_spec_test.go @@ -1,6 +1,7 @@ package loki import ( + "os" "reflect" "slices" "strings" @@ -107,6 +108,13 @@ func TestSpecArgs(t *testing.T) { s.Draft = "/models/mtp-Qwen3.6-27B-Q8_0.gguf" s.DraftGGUF = &GGUFInfo{Arch: "qwen35", BlockCount: 65, HasNextNTensor: true} }}, + {name: "MODEL_DRAFT = le modèle lui-même : pas de -md, la tête intégrée, et on le dit", + cfg: with(forced, "MODEL_DRAFT", "Qwen3.6-27B-MTP.gguf"), notes: 2, want: mtp, + si: func(s *serveSysInfo) { + s.Draft = s.Model + s.DraftGGUF = s.GGUF + s.DraftIsModel = true + }}, {name: "MODEL_DRAFT petit modèle : -md + draft-simple, sinon chargé pour rien", cfg: with(forced, "MODEL_DRAFT", "Qwen3-0.6B.gguf"), notes: 1, want: []string{"-md", "/models/Qwen3-0.6B.gguf", "--spec-type", "draft-simple", "--spec-draft-sampling", "greedy"}, @@ -493,3 +501,26 @@ func TestSynthAcceptanceWarned(t *testing.T) { t.Errorf("variable : %q", n) } } + +// probeSpec reconnaît un MODEL_DRAFT qui désigne le modèle principal. +func TestProbeSpecDraftIsModel(t *testing.T) { + t.Setenv("LOKI_HOME", t.TempDir()) + dir := t.TempDir() + model := dir + "/Swift-27B.gguf" + other := dir + "/mtp-Swift.gguf" + for _, p := range []string{model, other} { + if err := os.WriteFile(p, []byte("GGUF"), 0o644); err != nil { + t.Fatal(err) + } + } + si := serveSysInfo{Model: model} + probeSpec(map[string]string{"MODEL_DRAFT": model}, &si) + if !si.DraftIsModel { + t.Errorf("MODEL_DRAFT = le modèle, non reconnu (Draft=%q, err=%q)", si.Draft, si.DraftErr) + } + si = serveSysInfo{Model: model} + probeSpec(map[string]string{"MODEL_DRAFT": other}, &si) + if si.DraftIsModel || si.Draft != other { + t.Errorf("tête à part prise pour le modèle : %+v", si) + } +} diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index 2a56518..f874c44 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -6635,7 +6635,10 @@ async function populateDraft(){ if(!sel) return; const list = await jget('/api/models'); const cur = readEnvKey(document.getElementById('m-content').value, 'MODEL_DRAFT'); - const items = (list||[]).filter(m => !isMmprojName(m.name)); + // Le modèle principal n'est pas un brouillon : le choisir rechargeait tous + // ses poids une seconde fois (-md), quand sa tête MTP s'utilise sans. + const main = matchModel(currentModelInTextarea(), list); + const items = (list||[]).filter(m => !isMmprojName(m.name) && m !== main); const hit = matchModel(cur, items); let html = ''; let matched = false; @@ -6645,7 +6648,9 @@ async function populateDraft(){ html += ''; } if(cur && !matched){ - html += ''; + const self = main && matchModel(cur, [main]); + html += ''; } sel.innerHTML = html; syncSpecRow(); diff --git a/internal/loki/ui/src/js/07-models.js b/internal/loki/ui/src/js/07-models.js index 7c1ba6c..538a025 100644 --- a/internal/loki/ui/src/js/07-models.js +++ b/internal/loki/ui/src/js/07-models.js @@ -1383,7 +1383,10 @@ async function populateDraft(){ if(!sel) return; const list = await jget('/api/models'); const cur = readEnvKey(document.getElementById('m-content').value, 'MODEL_DRAFT'); - const items = (list||[]).filter(m => !isMmprojName(m.name)); + // Le modèle principal n'est pas un brouillon : le choisir rechargeait tous + // ses poids une seconde fois (-md), quand sa tête MTP s'utilise sans. + const main = matchModel(currentModelInTextarea(), list); + const items = (list||[]).filter(m => !isMmprojName(m.name) && m !== main); const hit = matchModel(cur, items); let html = ''; let matched = false; @@ -1393,7 +1396,9 @@ async function populateDraft(){ html += ''; } if(cur && !matched){ - html += ''; + const self = main && matchModel(cur, [main]); + html += ''; } sel.innerHTML = html; syncSpecRow();