mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
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>
89 lines
3.1 KiB
Go
89 lines
3.1 KiB
Go
package loki
|
||
|
||
import (
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// La sortie enrichie : liaison et horloge lues, « [N/A] » et « [Not
|
||
// Supported] » valent zéro sans faire tomber la carte.
|
||
func TestParseGPUQueryLiaison(t *testing.T) {
|
||
out := `0, NVIDIA GeForce RTX 5060 Ti, 16311, 900, 12.0, 00000000:01:00.0, 1, 5, 8, 8, 14001
|
||
1, NVIDIA GeForce RTX 3060, 12288, 300, 8.6, 00000000:05:00.0, [N/A], 4, [Not Supported], 16, [N/A]
|
||
`
|
||
g := parseGPUQuery(out)
|
||
if len(g) != 2 {
|
||
t.Fatalf("%d carte(s), attendu 2 : %+v", len(g), g)
|
||
}
|
||
if g[0].BusID != "00000000:01:00.0" || g[0].LinkGen != 1 || g[0].LinkGenMax != 5 ||
|
||
g[0].LinkWidth != 8 || g[0].LinkWidthMax != 8 || g[0].MemClockMax != 14001 {
|
||
t.Errorf("carte 0 mal lue : %+v", g[0])
|
||
}
|
||
if g[1].LinkGen != 0 || g[1].LinkGenMax != 4 || g[1].LinkWidth != 0 || g[1].LinkWidthMax != 16 || g[1].MemClockMax != 0 {
|
||
t.Errorf("valeurs [N/A] mal tolérées : %+v", g[1])
|
||
}
|
||
if g[1].MemTotal != "12288" || g[1].Cap != "8.6" {
|
||
t.Errorf("champs historiques perdus : %+v", g[1])
|
||
}
|
||
if s := g[0].linkSummary(); s != "PCIe 5 ×8 (actuellement 1 ×8, réduit au repos)" {
|
||
t.Errorf("résumé de liaison : %q", s)
|
||
}
|
||
if s := g[1].linkSummary(); s != "PCIe 4 ×16" {
|
||
t.Errorf("résumé sans valeur actuelle : %q", s)
|
||
}
|
||
}
|
||
|
||
// La sortie historique (cinq colonnes) se lit comme avant, sans liaison.
|
||
func TestParseGPUQueryHistorique(t *testing.T) {
|
||
g := parseGPUQuery("0, NVIDIA GeForce RTX 3080 Ti, 12288, 1442, 8.6\n")
|
||
if len(g) != 1 || g[0].Name != "NVIDIA GeForce RTX 3080 Ti" || g[0].MemTotal != "12288" {
|
||
t.Fatalf("lecture historique cassée : %+v", g)
|
||
}
|
||
if g[0].LinkGenMax != 0 || g[0].BusID != "" || g[0].linkSummary() != "" {
|
||
t.Errorf("liaison inventée : %+v", g[0])
|
||
}
|
||
}
|
||
|
||
// Un pilote qui refuse un champ de liaison : la requête historique prend le
|
||
// relais, les cartes restent détectées. Une autre erreur (délai dépassé) n'est
|
||
// pas relancée.
|
||
func TestQueryGPUsRepli(t *testing.T) {
|
||
var calls []string
|
||
refuse := func(fields string) ([]byte, error) {
|
||
calls = append(calls, fields)
|
||
if strings.Contains(fields, "pcie") {
|
||
return []byte(`Field "pcie.link.gen.max" is not a valid field to query.` + "\n"), errors.New("exit status 2")
|
||
}
|
||
return []byte("0, NVIDIA GeForce RTX 3060, 12288, 300, 8.6\n"), nil
|
||
}
|
||
g, err := queryGPUs(refuse)
|
||
if err != nil || len(g) != 1 || g[0].Name != "NVIDIA GeForce RTX 3060" {
|
||
t.Fatalf("repli raté : %+v, %v", g, err)
|
||
}
|
||
if len(calls) != 2 || calls[1] != gpuBaseFields {
|
||
t.Errorf("requêtes : %q", calls)
|
||
}
|
||
|
||
calls = nil
|
||
hang := func(fields string) ([]byte, error) {
|
||
calls = append(calls, fields)
|
||
return nil, errors.New("signal: killed")
|
||
}
|
||
if _, err := queryGPUs(hang); err == nil {
|
||
t.Error("erreur avalée")
|
||
}
|
||
if len(calls) != 1 {
|
||
t.Errorf("un délai dépassé relancé : %d requêtes", len(calls))
|
||
}
|
||
|
||
calls = nil
|
||
ok := func(fields string) ([]byte, error) {
|
||
calls = append(calls, fields)
|
||
return []byte("0, NVIDIA GeForce RTX 3060, 12288, 300, 8.6, 00000000:05:00.0, 4, 4, 16, 16, 7501\n"), nil
|
||
}
|
||
if g, err := queryGPUs(ok); err != nil || len(calls) != 1 || g[0].LinkWidthMax != 16 {
|
||
t.Errorf("une seule requête attendue : %d, %+v, %v", len(calls), g, err)
|
||
}
|
||
}
|