mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
Placement : liaison PCIe des cartes et guide d'ordre dans l'éditeur, en conseil
Deux cartes inégales (5060 Ti + 3060) se placent par deux règles du moteur qui peuvent s'opposer : --fit remplit d'abord la DERNIÈRE carte, qui porte la couche de sortie, et les experts MoE en RAM sont recopiés au prefill vers la PREMIÈRE. Pour choisir l'ordre il faut voir la liaison de chaque carte ; l'éditeur n'en montrait rien. - detectGPUs lit aussi la liaison PCIe (génération et largeur, actuelles et maximales), le bus et l'horloge mémoire max, dans la même invocation de nvidia-smi. « [N/A] » vaut zéro ; un pilote qui refuse un champ fait retomber sur la requête historique (jamais de GPU perdu), un délai dépassé n'est pas relancé. Borné à 10 s. La largeur du bus mémoire n'existe pas dans nvidia-smi : absente. - /api/backends/devices : une seule lecture nvidia-smi par énumération complète mémoire manquante ET liaison (annotateDevices, pure), par nom de carte, CUDA seulement, rien pour deux cartes homonymes. La lecture des jauges (gpuStatsCached) n'est pas touchée. - Éditeur : liaison max par carte (l'actuelle et l'horloge en info-bulle), guide de placement dans l'ordre du moteur (--device compris), marges FIT_TARGET carte par carte, signalement d'un --tensor-split ou d'un CUDA_VISIBLE_DEVICES propre au preset, lien « inverser l'ordre » ; l'ordre choisi survit aux cases cochées. - Pas de bouton de mesure : comparer deux ordres demande deux rechargements et un ordre inversé peut manquer de VRAM — le guide explique la marche à suivre (copie du preset, bench complet sur chacun). - loki gpu affiche la liaison. Rien ne change dans la ligne de commande du moteur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
889a85888d
commit
986a636c20
8 files changed
+481
-67
No files matched your search
@@ -732,9 +732,11 @@ function renderGpu(){
|
||||
// Mémoire à 0 = le moteur n'a pas pu la lire (carte déjà saturée par le
|
||||
// modèle en cours) : on n'affiche pas « 0,0 Go », qui serait un mensonge.
|
||||
const gb = d.total_mib > 0 ? ' · ' + (d.total_mib/1024).toFixed(1).replace('.', ',') + ' Go' : '';
|
||||
const link = gpuLinkText(d);
|
||||
return '<div class="pe-row">'
|
||||
+ '<span class="pe-row-l">' + gpuEsc(gpuLabel(d))
|
||||
+ '<span class="pe-sub">' + gpuEsc(d.id) + gb + '</span></span>'
|
||||
+ '<span class="pe-sub" title="' + gpuEsc(gpuLinkTip(d)) + '">' + gpuEsc(d.id) + gb
|
||||
+ (link ? ' · ' + gpuEsc(link) : '') + '</span></span>'
|
||||
+ '<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" value="' + gpuEsc(d.id) + '"'
|
||||
+ (sel.includes(d.id) ? ' checked' : '') + ' onchange="onGpuPick()"><span class="pe-sl"></span></label></span>'
|
||||
+ '</div>';
|
||||
@@ -744,10 +746,83 @@ function renderGpu(){
|
||||
document.getElementById('m-gpu-note').textContent = sel.length === gpuDevices.length
|
||||
? 'Toutes les cartes sont utilisées.'
|
||||
: 'Ce modèle n\'utilisera que : ' + sel.map(id => gpuLabel(gpuById(id))).join(', ') + '.';
|
||||
const guide = document.getElementById('m-gpu-guide');
|
||||
if(guide){ guide.innerHTML = sel.length > 1 ? placementGuideHtml(sel) : ''; guide.hidden = sel.length < 2; }
|
||||
group.hidden = false;
|
||||
if(sel.length > 1) paintSplit();
|
||||
}
|
||||
|
||||
// Liaison PCIe d'une carte (nvidia-smi, joint par nom côté serveur, CUDA
|
||||
// seulement) : le MAXIMUM, seul parlant — l'actuel descend au repos.
|
||||
function gpuLinkText(d){
|
||||
const l = d && d.link;
|
||||
if(!l || !(l.gen_max || l.width_max)) return '';
|
||||
return 'PCIe' + (l.gen_max ? ' ' + l.gen_max : '') + (l.width_max ? ' ×' + l.width_max : '');
|
||||
}
|
||||
function gpuLinkTip(d){
|
||||
const l = d && d.link;
|
||||
if(!l) return '';
|
||||
const t = [];
|
||||
if(l.gen || l.width) t.push('liaison actuelle : PCIe ' + (l.gen||'?') + ' ×' + (l.width||'?') + ' (réduite au repos, normale sous charge)');
|
||||
if(l.mem_clock_mhz) t.push('horloge mémoire max : ' + l.mem_clock_mhz + ' MHz');
|
||||
return t.join('\n');
|
||||
}
|
||||
|
||||
// Guide de placement entre cartes inégales. CONSEIL seulement : rien n'est
|
||||
// écrit dans le preset sans un clic, et Loki ne change jamais l'ordre ni les
|
||||
// marges d'office. Deux règles du moteur, qui peuvent s'opposer :
|
||||
// - --fit (placement auto) remplit les cartes en partant de la DERNIÈRE, qui
|
||||
// porte aussi la couche de sortie : un modèle dense décode plus vite si la
|
||||
// carte rapide est en dernier, ou si la lente garde une marge plus large ;
|
||||
// - les experts MoE restés en RAM sont recopiés, au prefill, vers la PREMIÈRE
|
||||
// carte : à elle la liaison PCIe la plus large.
|
||||
// « Première » et « dernière » s'entendent dans l'ordre du moteur, --device
|
||||
// compris — pas « CUDA0 ».
|
||||
function placementGuideHtml(sel){
|
||||
const name = id => gpuEsc(gpuLabel(gpuById(id)));
|
||||
const order = sel.map((id, i) => (i+1) + '. ' + name(id)
|
||||
+ (gpuLinkText(gpuById(id)) ? ' (' + gpuEsc(gpuLinkText(gpuById(id))) + ')' : '')).join(' → ');
|
||||
const parts = ['<b>Ordre vu par le moteur</b> : ' + order
|
||||
+ (sel.length === 2 ? ' · <button class="pe-link" onclick="reverseGpuOrder()">inverser l\'ordre</button>' : '')];
|
||||
parts.push('• Modèle dense en placement auto : --fit remplit d\'abord la <b>dernière</b> carte, qui porte aussi '
|
||||
+ 'la couche de sortie — la plus rapide en dernier, ou une marge FIT_TARGET plus large sur la lente '
|
||||
+ '(ex. FIT_TARGET=1024,3072 quand la lente est seconde ; 1024 au minimum).');
|
||||
parts.push('• Experts MoE sur CPU : le prefill les recopie vers la <b>première</b> carte — à elle la liaison PCIe la plus large.');
|
||||
const ft = cfgReadKey('FIT_TARGET');
|
||||
if(ft){
|
||||
const v = ft.split(/[,/]/).map(s => s.trim());
|
||||
parts.push('FIT_TARGET : ' + (v.length === 1 ? gpuEsc(v[0]) + ' Mio sur chaque carte'
|
||||
: sel.map((id, i) => name(id) + ' ' + gpuEsc(v[i] || '1024') + ' Mio').join(' · ')));
|
||||
}
|
||||
if(eaGetValued('--tensor-split'))
|
||||
parts.push('--tensor-split fixe la répartition : --fit et FIT_TARGET ne jouent pas.');
|
||||
const cvd = cfgReadKey('CUDA_VISIBLE_DEVICES');
|
||||
if(cvd) parts.push('CUDA_VISIBLE_DEVICES=' + gpuEsc(cvd) + ' dans ce preset : l\'ordre réel peut différer de cette '
|
||||
+ 'liste, établie avec la sélection de la machine — et les marges FIT_TARGET suivent l\'ordre réel.');
|
||||
parts.push('Les deux règles peuvent s\'opposer : à départager par la mesure, preset par preset. Loki ne bascule pas '
|
||||
+ 'le moteur tout seul pour comparer (deux rechargements complets, et un ordre inversé peut manquer de VRAM sur la '
|
||||
+ 'petite carte) : duplique le preset, inverse l\'ordre dans la copie, puis « bench complet » sur chacun. '
|
||||
+ 'Vérifie « offloaded N/N layers » au journal.');
|
||||
return parts.join('<br>');
|
||||
}
|
||||
|
||||
// Inverse l'ordre des deux cartes (--device), et la répartition avec elles.
|
||||
// Geste explicite de l'utilisateur, écrit dans le preset édité seulement.
|
||||
function reverseGpuOrder(){
|
||||
const sel = selectedGpuIds();
|
||||
if(sel.length !== 2) return;
|
||||
const rev = sel.slice().reverse();
|
||||
eaSetValued('--device', gpuNaturalOrder(rev) ? '' : rev.join(','));
|
||||
const ts = eaGetValued('--tensor-split');
|
||||
if(ts) eaSetValued('--tensor-split', ts.split(',').reverse().join(','));
|
||||
renderGpu();
|
||||
toast('ordre : ' + rev.map(id => gpuLabel(gpuById(id))).join(' → '));
|
||||
}
|
||||
// Toutes les cartes, dans l'ordre du moteur : aucune contrainte à écrire.
|
||||
function gpuNaturalOrder(ids){
|
||||
return ids.length === gpuDevices.length && ids.every((id, i) => id === gpuDevices[i].id);
|
||||
}
|
||||
|
||||
function gpuById(id){ return gpuDevices.find(d => d.id === id) || {id}; }
|
||||
// Nom lisible : « NVIDIA GeForce RTX 5060 Ti » → « RTX 5060 Ti ». CUDA0/Vulkan1
|
||||
// ne disent rien à personne ; le numéro reste en sous-titre pour ceux qui
|
||||
@@ -865,9 +940,13 @@ function resetSplit(){
|
||||
function onGpuPick(){
|
||||
const ids = [...document.querySelectorAll('#m-gpu-list input[type=checkbox]:checked')].map(c => c.value);
|
||||
if(!ids.length){ toast('gardez au moins une carte'); renderGpu(); return; }
|
||||
// Toutes cochées = pas de contrainte : on retire le flag plutôt que de figer
|
||||
// des noms de device qui changeraient avec le moteur.
|
||||
eaSetValued('--device', ids.length === gpuDevices.length ? '' : ids.join(','));
|
||||
// L'ordre déjà choisi (« inverser l'ordre ») est gardé pour les cartes restées
|
||||
// cochées ; les nouvelles viennent à la suite, dans l'ordre du moteur.
|
||||
const prev = eaGetValued('--device').split(',').filter(id => ids.includes(id));
|
||||
const order = prev.concat(ids.filter(id => !prev.includes(id)));
|
||||
// Toutes cochées dans l'ordre du moteur = pas de contrainte : on retire le
|
||||
// flag plutôt que de figer des noms de device qui changeraient avec le moteur.
|
||||
eaSetValued('--device', gpuNaturalOrder(order) ? '' : order.join(','));
|
||||
// Une répartition écrite pour N cartes n'a plus de sens pour N-1.
|
||||
if(ids.length < 2) eaSetValued('--tensor-split', '');
|
||||
renderGpu();
|
||||
|
||||
Reference in new issue
Block a user