Moteur : décodage spéculatif MTP en opt-in — tête détectée dans le fichier, garde-fous VRAM, et un essai raté ne se rejoue pas

Une tête MTP (Qwen3.6/3.8, intégrée ou publiée à part en mtp-*.gguf) accélère
le décodage sans rien changer à la sortie : chaque jeton émis est tiré par
l'échantillonneur du modèle cible, un jeton du brouillon n'est gardé que s'il
coïncide. Le prix est ailleurs — 1 à 2 Go de VRAM, un prefill parfois plus
lent, une fonction très récente du moteur — d'où l'opt-in : rien ne change
pour un preset existant.

- SPEC=off (défaut, clé absente comprise) / auto / mtp. La tête se reconnaît
  au TENSEUR blk.{N-1}.nextn.eh_proj (backend_gguf.go), comme llama.cpp : la
  clé nextn_predict_layers seule ne prouve rien.
- MODEL_DRAFT : résolu comme MMPROJ, mais introuvable ne bloque pas le
  lancement. Tête à part → -md + --spec-type draft-mtp explicite (llama.cpp ne
  devine que sur la première tranche) ; autre brouillon → draft-simple, sinon
  chargé en VRAM pour rien.
- auto seulement si --fit place tout (ni couches chiffrées, -ts, -ot, experts
  sur CPU, -sm row, --fit off, -dev), contexte chiffré (fit pourrait sinon le
  réduire), pas de vision, build officiel ≥ 11009, jamais qwen4exp. mtp impose,
  avec un avertissement.
- Aide du moteur lue à chaque fois : draft-mtp et --spec-draft-n-max requis ;
  --draft/--draft-max jamais émis. EXTRA_ARGS (--spec-type, -md, -hfd,
  --spec-default…) et LLAMA_ARG_SPEC_* gardent la main.
- Tirage greedy fixé (exact pour toute chaîne). probabilistic seulement avec
  SPEC=mtp, refusé avec mirostat ou adaptive-p. SPEC_N_MAX → --spec-draft-n-max.
- Jeton de tentative : « loki serve » le pose avant de lancer, le process web
  l'efface dès que le moteur répond (sans page ouverte). Resté au lancement
  suivant, même preset, moteur et build → auto coupé et dit. Couches poussées
  en RAM après chargement : même verdict. Les erreurs MTP/brouillon du journal
  ont leur message.
- Éditeur : « auto » et « MTP » écrivent SPEC, « non » écrit SPEC=off ; champ
  Brouillon ; la recherche HF propose la tête MTP du dépôt, décochée.
- Tests : table de specArgs (gardes, sauts, brouillons, tirage), place avant
  EXTRA_ARGS, cycle du jeton sur une vraie base, lecture du journal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 00:09:24 +02:00
1 parent 7f49310b7f
commit 3a6ad6f23b
10 files changed
+963 -36

No files matched your search

+12 -3
View File
@@ -1255,16 +1255,19 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<option value="bf16">bf16 (numérique différente de f16)</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : le type vient de la liste que llama-server
accepte pour --spec-type. Le nombre de jetons anticipés
<!-- Décodage spéculatif : « auto » et « MTP » écrivent la clé SPEC,
que Loki traduit au lancement (tête MTP détectée dans le fichier,
garde-fous VRAM) ; les autres types vont tels quels dans
--spec-type. « non » écrit SPEC=off. Le nombre de jetons anticipés
(--spec-draft-n-max) n'a de sens qu'une fois un type choisi, donc
sa ligne n'apparaît qu'à ce moment. -->
<div class="pe-row">
<span class="pe-row-l">Décodage spéculatif<span class="pe-sub">anticipe des jetons pour accélérer</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-spec" onchange="onSpecType()">
<option value="">non</option>
<option value="auto">auto (MTP si le modèle en a une et que la VRAM suit)</option>
<optgroup label="Modèle brouillon">
<option value="draft-mtp">MTP (intégré au modèle)</option>
<option value="draft-mtp">MTP (imposé)</option>
<option value="draft-eagle3">EAGLE-3</option>
<option value="draft-simple">modèle brouillon séparé</option>
<option value="draft-dflash">dFlash</option>
@@ -1283,6 +1286,12 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<span class="pe-row-l">Jetons anticipés<span class="pe-sub">par étape — défaut 3</span></span>
<span class="pe-row-c"><input id="s-spec-n" class="pe-val" type="number" min="1" step="1" placeholder="3" oninput="eaSetValued('--spec-draft-n-max', this.value)"></span>
</div>
<!-- Tête MTP publiée à part (mtp-*.gguf) ou petit modèle brouillon :
clé MODEL_DRAFT, résolue au lancement comme le projecteur vision. -->
<div class="pe-row" id="s-draft-row" style="display:none">
<span class="pe-row-l">Brouillon<span class="pe-sub">tête MTP à part ou petit modèle</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="m-draft" onchange="onPickDraft()"></select></span></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Raisonnement<span class="pe-sub" id="s-reasoning-sub">réflexion étape par étape</span></span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'off')"><span class="pe-sl"></span></label></span>
+71 -9
View File
@@ -165,7 +165,7 @@ async function openItem(kind, key){
document.getElementById('m-quant').value = currentQuantInTextarea();
populateSettings();
attachDownload(); // téléchargement encore en cours côté serveur ?
await Promise.all([loadGpuDevices(), populateModelPicker(), populateMmproj(),
await Promise.all([loadGpuDevices(), populateModelPicker(), populateMmproj(), populateDraft(),
populateDlDirs(), refreshInstalled()]);
if(seq !== openSeq) return;
}
@@ -523,7 +523,7 @@ async function deleteInstalled(m){
}
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
toast('modèle supprimé — ' + fmtSize(r.freed||0) + ' libérés');
await Promise.all([populateModelPicker(), populateMmproj(), populateDlDirs(),
await Promise.all([populateModelPicker(), populateMmproj(), populateDraft(), populateDlDirs(),
populateModelDirs(), refreshInstalled()]);
}
// Un .gguf donné est-il déjà sur le disque, complet ? Sert avant de proposer un
@@ -897,7 +897,7 @@ function populateSettings(){
set('s-reppen', cfgReadKey('REPEAT_PENALTY'));
set('s-moe', eaGetValued('--n-cpu-moe'));
set('s-spec-n', eaGetValued('--spec-draft-n-max'));
setSpecType(eaGetValued('--spec-type'));
setSpecType(eaGetValued('--spec-type') || specFromKey(cfgReadKey('SPEC')));
const chk = (id,v)=>{ const e=document.getElementById(id); if(e) e.checked=v; };
// Raisonnement : trois états dans le fichier, deux positions sur l'interrupteur.
// « off » est une interdiction explicite passée au moteur ; la clé ABSENTE, elle,
@@ -1046,7 +1046,14 @@ function syncLoadModeSub(mode){
const el = document.getElementById('s-loadmode-sub');
if(el) el.textContent = LOAD_MODE_SUB[mode] ?? mode;
}
// --- Décodage spéculatif (--spec-type / --spec-draft-n-max) ----------------
// --- Décodage spéculatif (SPEC, MODEL_DRAFT, --spec-type, --spec-draft-n-max)
// « auto » et « MTP » passent par la clé SPEC : Loki cherche la tête MTP dans
// le fichier et pose les garde-fous au lancement. Les autres types restent des
// --spec-type d'EXTRA_ARGS, qui ont toujours le dernier mot côté serveur.
function specFromKey(v){
v = String(v||'').trim().toLowerCase();
return v === 'auto' ? 'auto' : v === 'mtp' ? 'draft-mtp' : '';
}
// Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée
// par des virgules ; une valeur composée (ou un type sorti après cette version
// de loki) ne correspondrait à aucune option et serait silencieusement effacée
@@ -1072,17 +1079,55 @@ function syncSpecRow(){
if(!sel || !row) return;
const on = !!sel.value;
row.style.display = on ? '' : 'none';
// Le brouillon ne sert qu'à SPEC (auto / MTP) ; un brouillon déjà posé reste
// visible pour qu'on puisse le retirer.
const dr = document.getElementById('s-draft-row');
if(dr) dr.style.display = (sel.value === 'auto' || sel.value === 'draft-mtp' || cfgReadKey('MODEL_DRAFT')) ? '' : 'none';
return on;
}
function onSpecType(){
const v = document.getElementById('s-spec').value;
eaSetValued('--spec-type', v);
const viaKey = v === 'auto' || v === 'draft-mtp';
// « non » écrit SPEC=off : explicite, et identique au défaut du serveur.
cfgWriteKey('SPEC', v === 'auto' ? 'auto' : v === 'draft-mtp' ? 'mtp' : v ? '' : 'off');
eaSetValued('--spec-type', viaKey ? '' : v);
if(!v){
eaSetValued('--spec-draft-n-max', '');
document.getElementById('s-spec-n').value = '';
}
syncSpecRow();
}
// Liste des brouillons : tous les .gguf sauf les projecteurs vision. Une tête
// MTP publiée à part (mtp-*.gguf) est ce qu'on y choisira le plus souvent.
async function populateDraft(){
const sel = document.getElementById('m-draft');
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));
const hit = matchModel(cur, items);
let html = '<option value="">— aucun —</option>';
let matched = false;
for(const m of items){
const on = (m === hit) ? ' selected' : '';
if(on) matched = true;
html += '<option value="'+escHtml(m.value)+'"'+on+'>'+escHtml(m.name)+' ('+fmtSize(m.size)+')</option>';
}
if(cur && !matched){
html += '<option value="'+escHtml(cur)+'" selected>'+escHtml(baseName(cur)||cur)+' (introuvable)</option>';
}
sel.innerHTML = html;
syncSpecRow();
}
// Choisir un brouillon, c'est demander la spéculation : on passe en « auto » si
// elle était coupée (les garde-fous de Loki restent en place).
function onPickDraft(){
const v = document.getElementById('m-draft').value;
cfgWriteKey('MODEL_DRAFT', v);
const sel = document.getElementById('s-spec');
if(v && sel && !sel.value){ sel.value = 'auto'; onSpecType(); toast('décodage spéculatif : auto'); }
syncSpecRow();
}
// Replie/déplie l'éditeur du fichier .env (config brute) dans le modal preset.
function toggleRaw(){
@@ -1214,7 +1259,7 @@ let dlQueue = [];
// fichier. Le fichier a pu atterrir hors du dossier loki : l'option porte alors
// le chemin complet, pas le simple nom.
async function selectInstalled(fname){
await Promise.all([populateModelPicker(), populateMmproj(), populateDlDirs(), refreshInstalled()]);
await Promise.all([populateModelPicker(), populateMmproj(), populateDraft(), populateDlDirs(), refreshInstalled()]);
const pick = (id, cb)=>{
const s = document.getElementById(id);
if(!s) return false;
@@ -1222,6 +1267,7 @@ async function selectInstalled(fname){
if(!o) return false;
s.value = o.value; cb(); return true;
};
if(/^mtp-/i.test(fname)) return pick('m-draft', onPickDraft);
return isMmprojName(fname) ? pick('m-mmproj', onPickMmproj) : pick('m-model', onPickModel);
}
async function startDownload(){
@@ -1514,11 +1560,27 @@ async function hfPickRepo(repo){
}
o.appendChild(note);
// Tête MTP publiée à part (mtp-*.gguf) : même règle que le projecteur, elle
// ne vaut que pour SON modèle. Décochée : la spéculation reste un choix.
let dfChk = null;
const df = (r.drafts||[])[0] && hfBestProjector(r.drafts);
if(df){
const dn = document.createElement('label');
dn.className = 'hf-mm';
dfChk = document.createElement('input');
dfChk.type = 'checkbox';
const t = document.createElement('span');
t.textContent = ' installer aussi la tête MTP (décodage plus rapide, ~1-2 Go de VRAM) — '+df.name+' ('+fmtSize(df.size)+')'
+ (installed(df.name) ? ' — déjà sur le disque' : '');
dn.append(dfChk, t);
o.appendChild(dn);
}
const list = document.createElement('div'); list.className = 'hf-list';
for(const m of r.models){
const row = document.createElement('div');
row.className = 'hf-row';
row.onclick = ()=>hfInstall(m, mm && mmChk && mmChk.checked ? mm : null);
row.onclick = ()=>hfInstall(m, mm && mmChk && mmChk.checked ? mm : null, df && dfChk && dfChk.checked ? df : null);
const n = document.createElement('span'); n.className = 'hf-name';
n.textContent = m.quant || m.name;
const meta = document.createElement('span'); meta.className = 'hf-meta';
@@ -1565,14 +1627,14 @@ function hfBestProjector(list){
// Installe : le modèle d'abord, le projecteur ensuite (la file s'en charge). Le
// champ Vision du preset ne se remplit correctement que dans cet ordre.
async function hfInstall(model, projector){
async function hfInstall(model, projector, draft){
// Le fichier est là : rien à télécharger, donc rien à confirmer — la sonde le
// constate côté serveur et startDownloadURL le sélectionne.
const here = await modelIsOnDisk(model.name);
if(!here && model.verdict === 'trop' && !await askConfirm(
(model.why||'Ce modèle dépasse la mémoire disponible.')+'\n\nLe téléchargement fonctionnera, mais le moteur risque de ne pas le charger.',
{title:'Installer quand même ?', okText:'Installer', danger:true})) return;
dlQueue = projector ? [projector.url] : [];
dlQueue = [projector, draft].filter(Boolean).map(x => x.url);
document.getElementById('m-hf-url').value = model.url;
await startDownloadURL(model.url);
}