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:
MichaelandClaude Opus 5.5 committed 2026-10-04 11:03:02 +02:00
1 parent 889a85888d
commit 986a636c20
8 files changed
+481 -67

No files matched your search

+45 -34
View File
@@ -115,50 +115,56 @@ func devPersistPut(key string, devs []map[string]any) {
}
}
// fillMissingMemory complète les mémoires que le moteur n'a pas su lire. Quand
// une carte est déjà saturée par le modèle en cours, llama.cpp annonce 0 Mio —
// l'UI n'avait alors rien à afficher pour elle, ce qui donnait une liste
// incohérente (une carte avec sa taille, l'autre sans).
// annotateDevices complète les cartes listées par le moteur avec ce que
// nvidia-smi sait d'elles (UNE lecture, detectGPUs). Fonction pure.
//
// On complète depuis nvidia-smi PAR CORRESPONDANCE DE NOM, et uniquement pour
// ça : la mémoire totale est une donnée matérielle fixe, indépendante du
// backend. L'ordre et les identifiants des devices, eux, appartiennent au
// moteur (CUDA0 et Vulkan0 ne désignent pas la même carte) et ne doivent jamais
// venir de nvidia-smi. Un nom en double (deux cartes identiques) rend la
// correspondance ambiguë : on préfère alors ne rien dire.
func fillMissingMemory(devs []map[string]any) {
missing := false
for _, d := range devs {
if n, _ := d["total_mib"].(int); n <= 0 {
missing = true
}
}
if !missing {
return
}
gpus, err := detectGPUs()
if err != nil {
return
}
totals := map[string]int{}
// Mémoire totale manquante : quand une carte est déjà saturée par le modèle en
// cours, llama.cpp annonce 0 Mio — l'UI n'avait alors rien à afficher pour
// elle, ce qui donnait une liste incohérente (une carte avec sa taille, l'autre
// sans). Liaison PCIe et horloge mémoire (clé « link ») : de quoi choisir
// l'ordre des cartes en connaissance de cause (voir le guide de placement de
// l'éditeur), jamais appliqué d'office.
//
// Tout passe PAR CORRESPONDANCE DE NOM : ce sont des données matérielles fixes,
// indépendantes du backend. L'ordre et les identifiants des devices, eux,
// appartiennent au moteur (CUDA0 et Vulkan0 ne désignent pas la même carte) et
// ne doivent jamais venir de nvidia-smi. Un nom en double (deux cartes
// identiques) rend la correspondance ambiguë : on préfère alors ne rien dire.
// La liaison ne s'attache qu'aux cartes CUDA : un moteur Vulkan énumère dans
// son propre ordre, et le conseil qui l'accompagne parle de CUDA.
func annotateDevices(devs []map[string]any, gpus []gpuInfo) {
byName := map[string]gpuInfo{}
dup := map[string]bool{}
for _, g := range gpus {
name := strings.TrimSpace(g.Name)
if _, seen := totals[name]; seen {
if _, seen := byName[name]; seen {
dup[name] = true
continue
}
if mb, err := strconv.Atoi(strings.TrimSpace(g.MemTotal)); err == nil {
totals[name] = mb
}
byName[name] = g
}
for _, d := range devs {
if n, _ := d["total_mib"].(int); n > 0 {
name, _ := d["name"].(string)
name = strings.TrimSpace(name)
g, ok := byName[name]
if !ok || dup[name] {
continue
}
name, _ := d["name"].(string)
if mb, ok := totals[strings.TrimSpace(name)]; ok && !dup[strings.TrimSpace(name)] {
d["total_mib"] = mb
if n, _ := d["total_mib"].(int); n <= 0 {
if mb, err := strconv.Atoi(strings.TrimSpace(g.MemTotal)); err == nil {
d["total_mib"] = mb
}
}
if id, _ := d["id"].(string); !strings.HasPrefix(id, "CUDA") {
continue
}
if g.LinkGenMax == 0 && g.LinkWidthMax == 0 && g.MemClockMax == 0 {
continue
}
d["link"] = map[string]any{
"gen": g.LinkGen, "gen_max": g.LinkGenMax,
"width": g.LinkWidth, "width_max": g.LinkWidthMax,
"mem_clock_mhz": g.MemClockMax,
}
}
}
@@ -238,7 +244,12 @@ func handleBackendDevices(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"ok": true, "devices": devs, "stale": true})
return
}
fillMissingMemory(devs)
// Une seule lecture nvidia-smi par énumération (et l'énumération est en
// cache dix minutes) : mémoire manquante et liaison PCIe ensemble. Sans
// nvidia-smi (Mac, AMD), rien n'est ajouté.
if gpus, gerr := detectGPUs(); gerr == nil {
annotateDevices(devs, gpus)
}
// Quand une carte est déjà saturée par le modèle en cours, le moteur peut
// annoncer 0 Mio de mémoire : c'est une lecture transitoire, on ne la fige
// pas dans le cache (sinon l'UI affiche « 0 Go » pendant dix minutes).