mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
Moniteur : un seul nvidia-smi pour tous les onglets, aucun pour un onglet caché
Chaque onglet ouvert (et chaque téléphone) interrogeait /api/vram toutes les 3 s, et chaque requête lançait son propre nvidia-smi — y compris depuis un onglet en arrière-plan que personne ne regarde, pendant que llama-server génère. Rien de tout cela ne touche au modèle : seules les jauges sont concernées. - gpuStatsCached : lecture partagée de 2 s, verrou tenu pendant la requête pour que des appels simultanés attendent la même lecture. nvidia-smi introuvable (Mac, CPU seul) mémorisé une minute ; une autre erreur se retente au délai normal. - Seul handleVram passe par le cache. Le déchargement (lecture « avant » et gpuSettle) garde des lectures fraîches : deux valeurs égales servies par le cache feraient conclure gpuSettle à tort. Le cache est vidé après un déchargement. - UI : statut, VRAM et RAM ne sont plus interrogés dans un onglet caché ; lecture immédiate au retour. - Tests : cache (réutilisation, erreur, binaire absent, appels simultanés) et échantillonneur du déchargement qui contourne le cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3dff7e0e04
commit
4f34694400
5 files changed
+214
-19
No files matched your search
@@ -1,10 +1,16 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// gpuSettle : le bilan « VRAM libérée » est calculé sur ces lectures. Chaque cas
|
||||
@@ -70,3 +76,97 @@ func TestVramRoutesRefusentGET(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gpuCache : les jauges de tous les onglets se partagent une lecture par
|
||||
// fenêtre de deux secondes, même quand les requêtes arrivent en même temps.
|
||||
func TestGpuCache(t *testing.T) {
|
||||
type step struct {
|
||||
at time.Duration // horloge depuis le départ
|
||||
err error // erreur rendue si la requête part
|
||||
calls int32 // requêtes cumulées attendues après l'appel
|
||||
}
|
||||
absent := &exec.Error{Name: "nvidia-smi", Err: exec.ErrNotFound}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
steps []step
|
||||
}{
|
||||
{"lecture réutilisée puis rafraîchie", []step{
|
||||
{0, nil, 1}, {time.Second, nil, 1}, {gpuCacheTTL, nil, 2},
|
||||
}},
|
||||
// Carte en réinitialisation : on retente au délai normal, pas plus tard.
|
||||
{"erreur retentée au délai normal", []step{
|
||||
{0, errors.New("exit status 9"), 1}, {time.Second, nil, 1}, {gpuCacheTTL, nil, 2},
|
||||
}},
|
||||
// Pas de nvidia-smi du tout : une recherche par minute suffit.
|
||||
{"binaire absent mémorisé", []step{
|
||||
{0, absent, 1}, {30 * time.Second, absent, 1}, {gpuCacheAbsent, absent, 2},
|
||||
}},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var cache gpuCache
|
||||
var calls int32
|
||||
t0 := time.Unix(1000, 0)
|
||||
for i, st := range c.steps {
|
||||
now := func() time.Time { return t0.Add(st.at) }
|
||||
cache.get(now, func() ([]gpuStat, error) {
|
||||
atomic.AddInt32(&calls, 1)
|
||||
if st.err != nil {
|
||||
return nil, st.err
|
||||
}
|
||||
return []gpuStat{{Name: "fake", Used: 1000}}, nil
|
||||
})
|
||||
if got := atomic.LoadInt32(&calls); got != st.calls {
|
||||
t.Fatalf("étape %d : %d requêtes, attendu %d", i, got, st.calls)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("appels simultanés", func(t *testing.T) {
|
||||
var cache gpuCache
|
||||
var calls int32
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 16; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
g := cache.get(time.Now, func() ([]gpuStat, error) {
|
||||
atomic.AddInt32(&calls, 1)
|
||||
time.Sleep(20 * time.Millisecond) // nvidia-smi n'est pas instantané
|
||||
return []gpuStat{{Name: "fake", Used: 1000}}, nil
|
||||
})
|
||||
if len(g) != 1 || g[0].Used != 1000 {
|
||||
t.Errorf("lecture partagée %+v", g)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if calls != 1 {
|
||||
t.Errorf("%d nvidia-smi lancés, attendu 1", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Le bilan du déchargement doit voir la mémoire PARTIR : son échantillonneur
|
||||
// ne passe jamais par le cache des jauges, même tout juste rempli.
|
||||
func TestGpuSettleSampleFrais(t *testing.T) {
|
||||
orig := nvidiaSmiQuery
|
||||
t.Cleanup(func() { nvidiaSmiQuery = orig; gpuMonitor = gpuCache{} })
|
||||
var calls int32
|
||||
nvidiaSmiQuery = func() ([]byte, error) {
|
||||
n := atomic.AddInt32(&calls, 1)
|
||||
return []byte(fmt.Sprintf("Fake GPU, %d, 24000, 0, 40\n", 20000-int(n)*1000)), nil
|
||||
}
|
||||
gpuMonitor = gpuCache{}
|
||||
if g := gpuStatsCached(); len(g) != 1 || g[0].Used != 19000 {
|
||||
t.Fatalf("jauges : %+v", g)
|
||||
}
|
||||
for want := 18000; want >= 17000; want -= 1000 {
|
||||
if used, ok := gpuSettleSample(); !ok || used != want {
|
||||
t.Errorf("échantillon = %d (%v), attendu %d frais", used, ok, want)
|
||||
}
|
||||
}
|
||||
if g := gpuStatsCached(); g[0].Used != 19000 {
|
||||
t.Errorf("jauges : %d, attendu la lecture en cache 19000", g[0].Used)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user