mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 04:36:44 +02:00
Optimiseur : loki tune et bouton « Optimiser », essais sans perte sur un moteur privé
Chercher à la main le bon micro-lot, les threads ou la marge --fit d'un preset demandait de dupliquer, basculer et comparer des bench complets un par un. L'optimiseur le fait sur un moteur d'essai, sans jamais toucher à config.env ni à ce que calcule le modèle, et n'écrit rien sans un clic. - Isolation : moteur principal arrêté puis relancé ; chaque essai est une copie de la configuration (LOKI_HOME/tune/run), lancée par « loki serve » sur 127.0.0.1 et un port libre, groupe de processus tué en sortie - Verrou exclusif inter-processus (LOKI_HOME/tune.lock, PID + démarrage + binaire) vérifié par serviceAction start/restart et par le chat, les tâches, bash_bg, /v1, le bench, la bascule et l'enregistrement de preset, GPU, clé d'API, moteur ; essai orphelin arrêté avant tout démarrage du moteur - Essais : lots, threads, délestage, marges --fit, files CUDA ; placement --fit et SPEC/CUDA_GRAPH_OPT/--backend-sampling seulement sur demande - EXTRA_ARGS jeton par jeton, liste blanche seulement ; ligne de commande de chaque essai composée à blanc et comparée à la référence (« dénature »), contexte, slots et cache KV recontrôlés une fois chargé - Mesure : bench complet du lot 1 à profondeur fixe ; score = durée d'un tour type (médianes de la télémétrie) ; gain > max(3 %, écart entre passages) - Application sur clic : copie du preset, ou preset actuel sauvegardé, réécrit clé par clé, vérifié par une sonde et rétabli en cas d'échec Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f87c7b615b
commit
9bffff78c2
36 files changed
+4580
-16
No files matched your search
@@ -1350,6 +1350,12 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<div class="pe-row stack" id="s-loadmode-warn-row" style="display:none">
|
||||
<div class="pe-note" id="s-loadmode-warn" style="color:var(--warn)"></div>
|
||||
</div>
|
||||
<!-- Optimiseur sans perte (backend_tune*.go) : essais sur un moteur
|
||||
privé, mesurés au bench complet. Rien n'est écrit sans un clic. -->
|
||||
<div class="pe-row" id="m-tune-row">
|
||||
<span class="pe-row-l">Optimiseur<span class="pe-sub" id="m-tune-sub">lots, threads, marges --fit… mesurés sur un moteur privé · rien n'est écrit sans ton accord</span></span>
|
||||
<span class="pe-row-c"><button class="pe-link" onclick="openTuneModal()">Optimiser…</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1417,6 +1423,24 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Optimiseur sans perte (29-tune.js) : au-dessus de l'éditeur de preset, d'où
|
||||
il s'ouvre. Les options cochées ici ne s'enregistrent nulle part. -->
|
||||
<div id="tune-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:40;align-items:center;justify-content:center;padding:20px" onclick="if(event.target===this)closeTuneModal()">
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:4px;width:100%;max-width:640px;display:flex;flex-direction:column;max-height:90vh">
|
||||
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||
<strong>Optimiser le preset</strong>
|
||||
<button onclick="closeTuneModal()" style="margin:0" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
|
||||
</div>
|
||||
<div id="tune-body" style="padding:16px 18px;display:flex;flex-direction:column;gap:10px;overflow:auto"></div>
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:6px;flex-wrap:wrap">
|
||||
<button id="tune-cancel" onclick="cancelTuneUI()" style="display:none">annuler l'optimisation</button>
|
||||
<button id="tune-copy" onclick="applyTuneUI('copy')" style="display:none">enregistrer dans une copie</button>
|
||||
<button id="tune-apply" onclick="applyTuneUI('preset')" style="display:none;border-color:var(--accent);color:var(--accent)">appliquer à ce preset…</button>
|
||||
<button id="tune-start" onclick="startTuneUI()" style="border-color:var(--accent);color:var(--accent)">lancer</button>
|
||||
<button onclick="closeTuneModal()">fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="marked.min.js"></script>
|
||||
<script>
|
||||
@@JS@@
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// Optimiseur sans perte (backend_tune*.go). Il tourne côté serveur, en tâche de
|
||||
// fond : la modale lance, suit, montre les mesures — et n'écrit RIEN sans un
|
||||
// clic (copie du preset, ou ce preset après confirmation du diff).
|
||||
let tunePoll = null, tuneState = 'form';
|
||||
// Étapes de base (tuneStages côté serveur) ; placement et opt-in ont leur case.
|
||||
const TUNE_STAGES = [['lots','micro-lot et lot'], ['threads','threads'], ['delestage','délestage'], ['marges','marges --fit'], ['files','files CUDA']];
|
||||
function closeTuneModal(){ clearTimeout(tunePoll); hideModal('tune-modal'); }
|
||||
function tuneBtns(state){
|
||||
tuneState = state;
|
||||
const show = (id, on) => { const b = document.getElementById(id); if(b) b.style.display = on ? '' : 'none'; };
|
||||
show('tune-start', state !== 'running');
|
||||
const sb = document.getElementById('tune-start');
|
||||
if(sb) sb.textContent = state === 'form' ? 'lancer' : 'nouvelle mesure…';
|
||||
show('tune-cancel', state === 'running');
|
||||
show('tune-copy', state === 'result');
|
||||
show('tune-apply', state === 'result');
|
||||
}
|
||||
function tuneSec(s){
|
||||
s = Math.max(0, Math.round(s||0));
|
||||
return s >= 60 ? Math.floor(s/60) + ' min ' + String(s%60).padStart(2,'0') + ' s' : s + ' s';
|
||||
}
|
||||
async function openTuneModal(){
|
||||
showModal('tune-modal');
|
||||
const body = document.getElementById('tune-body');
|
||||
body.innerHTML = '<div class="muted">…</div>';
|
||||
let st = {};
|
||||
try{ st = await jget('/api/tune/status'); }catch(_){}
|
||||
// Une mesure finie (modale fermée pendant qu'elle tournait) : son résultat
|
||||
// d'abord, avec ses boutons — « nouvelle mesure… » ramène au formulaire.
|
||||
if(st.running || st.applying || st.apply || (st.result && st.result.preset_id === editingKey)){ tuneWatch(); return; }
|
||||
let last = null;
|
||||
if(editingKey){ try{ last = await jget('/api/tune/last?id=' + encodeURIComponent(editingKey)); }catch(_){} }
|
||||
tuneForm(last && last.ok ? last : null, st.elsewhere);
|
||||
}
|
||||
function tuneForm(last, elsewhere){
|
||||
const body = document.getElementById('tune-body');
|
||||
let h = '<div class="pe-note">Des essais sur un moteur <b>privé</b> (127.0.0.1), chacun mesuré au bench complet : '
|
||||
+ 'micro-lot et lot, threads (poids sur CPU), seuil de délestage, marges <code>--fit</code> et files CUDA (2 cartes ou plus). '
|
||||
+ 'Jamais touchés : modèle, contexte, cache KV, raisonnement, échantillonnage, slots. Le moteur principal est arrêté pendant la mesure '
|
||||
+ '(chat et tâches en pause), puis relancé. Rien n\'est écrit sans ton accord.</div>'
|
||||
+ '<div class="pe-note muted">Mesure la version <b>enregistrée</b> du preset en service.</div>'
|
||||
+ '<div style="display:flex;gap:10px;flex-wrap:wrap;font-size:12px"><span class="muted">Étapes :</span>'
|
||||
+ TUNE_STAGES.map(s => '<label style="display:flex;gap:4px;align-items:center"><input type="checkbox" class="tune-stage" value="' + s[0] + '" checked> ' + s[1] + '</label>').join('')
|
||||
+ '</div>'
|
||||
+ '<label style="display:flex;gap:8px;align-items:flex-start"><input type="checkbox" id="tune-placement"> <span>Inclure le placement'
|
||||
+ '<span class="muted" style="display:block;font-size:11px">--fit au lieu des experts MoE ou du --tensor-split placés à la main : réécrit EXTRA_ARGS, confirmé à part</span></span></label>'
|
||||
+ '<label style="display:flex;gap:8px;align-items:flex-start"><input type="checkbox" id="tune-optin"> <span>Inclure les options opt-in'
|
||||
+ '<span class="muted" style="display:block;font-size:11px">SPEC (sortie inchangée, jugée sur prose et code), SPEC_N_MAX, CUDA_GRAPH_OPT (expérimental), --backend-sampling</span></span></label>'
|
||||
+ '<label style="display:flex;gap:8px;align-items:center">Budget <input type="number" id="tune-budget" class="pe-val" min="5" max="360" step="5" value="30" style="width:80px"> minutes'
|
||||
+ '<span class="muted" style="font-size:11px">— au-delà, résultat partiel</span></label>';
|
||||
if(elsewhere) h += '<div class="pe-note" style="color:var(--warn)">Une optimisation tourne déjà (' + escHtml(elsewhere) + ').</div>';
|
||||
if(last && last.result){
|
||||
const r = last.result;
|
||||
const when = new Date(r.at*1000).toLocaleString();
|
||||
h += '<div class="pe-note">Dernière optimisation : ' + escHtml(when) + (r.best ? ' — ' + escHtml(r.best) + ', −' + (r.gain*100).toFixed(1) + ' % par tour' : ' — rien de mieux trouvé') + '</div>';
|
||||
if(last.engine_changed) h += '<div class="pe-note" style="color:var(--warn)">Le moteur a changé depuis (' + escHtml(last.engine_changed) + ') : les meilleurs réglages ont pu bouger — relance l\'optimisation.</div>';
|
||||
if(last.preset_changed) h += '<div class="pe-note muted">Le preset a été modifié depuis cette mesure.</div>';
|
||||
}
|
||||
body.innerHTML = h;
|
||||
tuneBtns('form');
|
||||
}
|
||||
async function startTuneUI(){
|
||||
if(tuneState !== 'form'){
|
||||
let last = null;
|
||||
if(editingKey){ try{ last = await jget('/api/tune/last?id=' + encodeURIComponent(editingKey)); }catch(_){} }
|
||||
tuneForm(last && last.ok ? last : null);
|
||||
return;
|
||||
}
|
||||
const placement = !!(document.getElementById('tune-placement')||{}).checked;
|
||||
const optin = !!(document.getElementById('tune-optin')||{}).checked;
|
||||
const budget = parseInt((document.getElementById('tune-budget')||{}).value, 10) || 30;
|
||||
// Toutes cochées = toutes (liste vide) ; aucune = seulement placement / opt-in.
|
||||
const boxes = [...document.querySelectorAll('.tune-stage')];
|
||||
const picked = boxes.filter(b => b.checked).map(b => b.value);
|
||||
const stages = picked.length === boxes.length ? [] : (picked.length ? picked : ['aucune']);
|
||||
const msg = 'Le moteur principal va être ARRÊTÉ pendant jusqu\'à ' + budget + ' minutes : chat, tâches et clients /v1 attendent. '
|
||||
+ 'Chaque essai recharge le modèle. Lancer l\'optimisation ?';
|
||||
if(!await askConfirm(msg, {title:'Optimiser', okText:'Lancer'})) return;
|
||||
let r;
|
||||
try{ r = await jpost('/api/tune', {placement, optin, budget_min: budget, stages}); }catch(e){ r = {ok:false, error:e.message}; }
|
||||
if(!r.ok){ document.getElementById('tune-body').insertAdjacentHTML('beforeend', '<div style="color:var(--err)">' + escHtml(r.error||'?') + '</div>'); return; }
|
||||
tuneWatch();
|
||||
}
|
||||
async function cancelTuneUI(){ try{ await jpost('/api/tune/cancel', {}); }catch(_){} }
|
||||
function tuneTrialsHTML(trials){
|
||||
if(!trials || !trials.length) return '';
|
||||
let h = '<table style="width:100%;font-size:12px;border-collapse:collapse"><tr class="muted"><td>étape</td><td>essai</td>'
|
||||
+ '<td style="text-align:right">tour</td><td style="text-align:right">VRAM libre</td><td>statut</td></tr>';
|
||||
for(const t of trials){
|
||||
const color = t.status === 'retenu' ? 'var(--ok)' : (t.status === 'échec' || t.status === 'écarté') ? 'var(--warn)' : 'inherit';
|
||||
const runs = (t.runs||[]);
|
||||
let tip = '';
|
||||
if(runs.length){
|
||||
const r0 = runs[0];
|
||||
tip = 'prose ' + (r0.prose_pp||0).toFixed(0) + ' / ' + (r0.prose_tg||0).toFixed(1) + ' t/s';
|
||||
if(r0.depth) tip += ' · à ' + r0.depth + ' jetons : froid ' + (r0.cold_pp||0).toFixed(0) + ', tours ' + (r0.cached_pp||0).toFixed(0) + ', decode ' + (r0.tg||0).toFixed(1) + ' t/s';
|
||||
tip += ' · ' + runs.length + ' passage(s)';
|
||||
}
|
||||
if(t.offloaded) tip += ' · couches ' + t.offloaded;
|
||||
h += '<tr title="' + escHtml(tip) + '"><td class="muted" style="padding:3px 6px 3px 0">' + escHtml(t.stage) + '</td>'
|
||||
+ '<td>' + escHtml(t.label) + '</td>'
|
||||
+ '<td style="text-align:right">' + (t.turn_sec ? t.turn_sec.toFixed(1) + ' s' : '—') + '</td>'
|
||||
+ '<td style="text-align:right">' + (t.headroom_mib >= 0 ? t.headroom_mib + ' Mio' : '—') + '</td>'
|
||||
+ '<td style="color:' + color + ';padding-left:6px">' + escHtml(t.status) + (t.why ? ' <span class="muted">· ' + escHtml(t.why) + '</span>' : '') + '</td></tr>';
|
||||
}
|
||||
return h + '</table>';
|
||||
}
|
||||
function tuneResultHTML(r){
|
||||
let h = '<div class="pe-note muted">Score : durée d\'un tour type — ' + (r.weights.k).toFixed(0) + ' jetons lus (cache repris), '
|
||||
+ (r.weights.g).toFixed(0) + ' écrits, ' + (r.weights.f_cold*100).toFixed(0) + ' % des tours relus à froid à ' + r.depth + ' jetons ('
|
||||
+ escHtml(r.weights.from) + '). Un gain compte au-delà de 3 % et de l\'écart entre passages. Moteur ' + escHtml(r.engine||'?') + '.</div>';
|
||||
h += tuneTrialsHTML([r.baseline].concat(r.trials||[]));
|
||||
for(const s of (r.skipped||[])) h += '<div class="muted" style="font-size:11px">sauté — ' + escHtml(s) + '</div>';
|
||||
for(const n of (r.notes||[])) h += '<div style="font-size:11px;color:var(--warn)">' + escHtml(n) + '</div>';
|
||||
if(r.partial) h += '<div style="font-size:11px;color:var(--warn)">résultat partiel : ' + escHtml(r.partial) + '</div>';
|
||||
if(r.best){
|
||||
h += '<div style="margin-top:6px"><b style="color:var(--ok)">Meilleur : ' + escHtml(r.best) + '</b> — −' + (r.gain*100).toFixed(1) + ' % par tour</div>'
|
||||
+ '<div class="pe-note" style="white-space:pre-wrap;font-family:var(--mono,monospace);font-size:11px">' + escHtml((r.changes||[]).join('\n')) + '</div>';
|
||||
if(r.placement) h += '<div class="pe-note" style="color:var(--warn)">Ce résultat réécrit EXTRA_ARGS (placement) : à confirmer à part.</div>';
|
||||
} else {
|
||||
h += '<div style="margin-top:6px">La configuration actuelle reste la meilleure : rien à changer.</div>';
|
||||
}
|
||||
return h;
|
||||
}
|
||||
function tuneWatch(){
|
||||
clearTimeout(tunePoll);
|
||||
const body = document.getElementById('tune-body');
|
||||
const tick = async () => {
|
||||
let st;
|
||||
try{ st = await jget('/api/tune/status'); }
|
||||
catch(_){ tunePoll = setTimeout(tick, 3000); return; }
|
||||
if(st.running){
|
||||
tuneBtns('running');
|
||||
const eta = st.eta_sec >= 0 ? ' · reste ~' + tuneSec(st.eta_sec) : '';
|
||||
body.innerHTML = '<div class="muted" style="text-align:center">⏳ ' + escHtml(st.phase||'…') + '<br>' + tuneSec(st.elapsed_sec) + eta
|
||||
+ '<br><span style="font-size:11px">moteur principal arrêté · chat et tâches en pause</span></div>' + tuneTrialsHTML(st.trials);
|
||||
tunePoll = setTimeout(tick, 2000);
|
||||
return;
|
||||
}
|
||||
let h = '';
|
||||
if(st.error) h += '<div style="color:var(--err)">' + escHtml(st.error) + '</div>';
|
||||
if(st.result) h += tuneResultHTML(st.result);
|
||||
const a = st.apply;
|
||||
if(a){
|
||||
h += '<div class="pe-note">' + (a.log||[]).map(escHtml).join('<br>') + '</div>';
|
||||
if(a.message) h += '<div style="color:var(--ok)">' + escHtml(a.message) + '</div>';
|
||||
if(a.error) h += '<div style="color:var(--err)">' + escHtml(a.error) + '</div>';
|
||||
}
|
||||
body.innerHTML = h || '<div class="muted">rien à montrer</div>';
|
||||
const canApply = !!(st.result && st.result.best) && !st.applying && !(a && a.message);
|
||||
tuneBtns(canApply ? 'result' : 'done');
|
||||
if(st.applying){ tunePoll = setTimeout(tick, 2000); }
|
||||
};
|
||||
tick();
|
||||
}
|
||||
async function applyTuneUI(target){
|
||||
let st = {};
|
||||
try{ st = await jget('/api/tune/status'); }catch(_){}
|
||||
const r = st.result;
|
||||
if(!r || !r.best) return;
|
||||
const diff = (r.changes||[]).map(c => '• ' + c).join('\n');
|
||||
if(target === 'copy'){
|
||||
if(!await askConfirm('Créer une COPIE de « ' + r.preset_name + ' » avec :\n\n' + diff + '\n\nLe preset d\'origine ne bouge pas.',
|
||||
{title:'Enregistrer dans une copie', okText:'Créer la copie'})) return;
|
||||
} else {
|
||||
if(!await askConfirm('Réécrire « ' + r.preset_name + ' » (sauvegardé avant) avec :\n\n' + diff
|
||||
+ '\n\nLe moteur redémarre, puis une sonde vérifie un prompt à ' + r.depth + ' jetons ; en cas d\'échec, l\'ancienne version est rétablie.',
|
||||
{title:'Appliquer à ce preset', okText:'Appliquer'})) return;
|
||||
if(r.placement && !await askConfirm('Ce résultat RÉÉCRIT EXTRA_ARGS (placement des experts ou --tensor-split retirés pour --fit). Confirmer ?',
|
||||
{title:'Placement', okText:'Confirmer'})) return;
|
||||
}
|
||||
let res;
|
||||
try{ res = await jpost('/api/tune/apply', {target}); }catch(e){ res = {ok:false, error:e.message}; }
|
||||
if(!res.ok){ toast('erreur : ' + (res.error||'')); return; }
|
||||
if(target === 'copy'){ toast(res.message || 'copie créée'); KINDS.preset.reload(); return; }
|
||||
tuneWatch();
|
||||
}
|
||||
Reference in new issue
Block a user