mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Sur Unraid, /mnt/user/… n'est pas un disque mais shfs, un démon FUSE : chaque fsync de loki.db et chaque page d'un gros MoE mmappé relue depuis le disque (modèle plus gros que la RAM) le traverse, à chaque token. Rien n'est perdu ni altéré, mais le décodage ralentit — sans que rien ne le dise. Loki ne déplace rien : changer le chemin hôte d'une installation donnerait un /data vide. Il le voit et le dit, sur un ton d'information : - sys_storagefuse.go lit /proc/self/mountinfo (type lu après le séparateur « - », montage le plus long qui contient le chemin) pour LOKI_HOME et chaque dossier de modèles ; Linux en conteneur seulement, mis en cache une minute puisque /api/status est interrogé en boucle ; - conseil en jaune au démarrage et champ « hint » de /api/status, affiché en encart discret, distinct du bandeau rouge de perte de données ; - compose Unraid et README : « Exclusive access » recommandé (même chemin, FUSE court-circuité), /mnt/<pool> en option avancée avec migration manuelle et mise en garde sur un pool inexistant (RAM). La détection de thrash du cache de pages est laissée de côté : elle exige un signal majflt calibré par modèle sur le serveur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.5 KiB
Go
101 lines
3.5 KiB
Go
package loki
|
|
|
|
import (
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Extrait réaliste d'un conteneur Loki sur Unraid : racine overlay, /data et
|
|
// /models en bind depuis /mnt/user (fuse.shfs), un /data/models imbriqué sur
|
|
// un pool btrfs, champs optionnels de longueur variable, un chemin à espace.
|
|
const mountinfoUnraid = `612 553 0:156 / / rw,relatime master:221 - overlay overlay rw,lowerdir=/var/lib/docker/l
|
|
613 612 0:159 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
|
|
620 612 0:52 /appdata/loki/data /data rw,noatime - fuse.shfs shfs rw,user_id=0,group_id=0,allow_other
|
|
621 612 0:52 /appdata/loki/models /models rw,noatime shared:5 master:7 - fuse.shfs shfs rw,user_id=0
|
|
622 620 0:61 /appdata/loki/fast /data/models rw,noatime - btrfs /dev/nvme0n1p1 rw,ssd
|
|
623 612 0:52 /media /mnt/mes\040modeles rw,noatime - fuse.shfs shfs rw
|
|
garbage line
|
|
624 612 8:1 / /broken rw
|
|
`
|
|
|
|
func TestParseMountinfo(t *testing.T) {
|
|
m := parseMountinfo(strings.NewReader(mountinfoUnraid))
|
|
if len(m) != 6 {
|
|
t.Fatalf("attendu 6 montages valides, obtenu %d : %+v", len(m), m)
|
|
}
|
|
// Champs optionnels (shared:5 master:7) : le type se lit après « - ».
|
|
if m[3].point != "/models" || m[3].fsType != "fuse.shfs" {
|
|
t.Fatalf("ligne à champs optionnels mal lue : %+v", m[3])
|
|
}
|
|
if m[5].point != "/mnt/mes modeles" {
|
|
t.Fatalf("échappement \\040 non décodé : %q", m[5].point)
|
|
}
|
|
}
|
|
|
|
func TestMountFSType(t *testing.T) {
|
|
m := parseMountinfo(strings.NewReader(mountinfoUnraid))
|
|
cases := []struct {
|
|
path, want string
|
|
}{
|
|
{"/data", "fuse.shfs"},
|
|
{"/data/loki.db", "fuse.shfs"},
|
|
{"/data/models", "btrfs"}, // montage imbriqué : le plus long gagne
|
|
{"/data/models/x.gguf", "btrfs"},
|
|
{"/data/modelsX", "fuse.shfs"}, // préfixe de chaîne ≠ préfixe de chemin
|
|
{"/models", "fuse.shfs"},
|
|
{"/mnt/mes modeles/a.gguf", "fuse.shfs"},
|
|
{"/opt/loki", "overlay"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := mountFSType(m, c.path); got != c.want {
|
|
t.Errorf("mountFSType(%q) = %q, attendu %q", c.path, got, c.want)
|
|
}
|
|
}
|
|
if got := mountFSType(nil, "/data"); got != "" {
|
|
t.Errorf("sans montage connu : %q, attendu \"\"", got)
|
|
}
|
|
}
|
|
|
|
func TestShfsPaths(t *testing.T) {
|
|
m := parseMountinfo(strings.NewReader(mountinfoUnraid))
|
|
got := shfsPaths(m, []string{"/data", "/data/models", "/models", "/models/", "/opt"})
|
|
want := []string{"/data", "/models"}
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("shfsPaths = %v, attendu %v", got, want)
|
|
}
|
|
// Partages en « Exclusive access » : plus de fuse.shfs, plus de conseil.
|
|
excl := `1 0 0:1 / / rw - overlay overlay rw
|
|
2 1 0:2 /appdata/loki/data /data rw - btrfs /dev/nvme0n1p1 rw
|
|
3 1 0:3 /loki/models /models rw - xfs /dev/md1p1 rw
|
|
`
|
|
if p := shfsPaths(parseMountinfo(strings.NewReader(excl)), []string{"/data", "/models"}); len(p) != 0 {
|
|
t.Fatalf("aucun chemin ne devrait être signalé, obtenu %v", p)
|
|
}
|
|
}
|
|
|
|
func TestStorageHintText(t *testing.T) {
|
|
if s := storageHintText(nil); s != "" {
|
|
t.Fatalf("rien à signaler : %q", s)
|
|
}
|
|
s := storageHintText([]string{"/data", "/models"})
|
|
for _, w := range []string{"/data, /models", "fuse.shfs", "Exclusive access"} {
|
|
if !strings.Contains(s, w) {
|
|
t.Errorf("conseil sans %q : %s", w, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStorageFuseHintOutsideContainer(t *testing.T) {
|
|
t.Setenv("LOKI_CONTAINER", "")
|
|
if s := storageFuseHint(); s != "" {
|
|
t.Fatalf("hors conteneur, aucun conseil attendu : %q", s)
|
|
}
|
|
if runtime.GOOS != "linux" {
|
|
t.Setenv("LOKI_CONTAINER", "1")
|
|
if s := storageFuseHint(); s != "" {
|
|
t.Fatalf("hors Linux, aucun conseil attendu : %q", s)
|
|
}
|
|
}
|
|
}
|