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();