Files
Loki/internal/loki/sys_storagefuse.go
T
MichaelandClaude Opus 5.5 056f307983 Stockage : Loki signale un /data ou /models servi par le FUSE d'Unraid
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>
2026-10-04 02:31:59 +02:00

178 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package loki
// sys_storagefuse.go — conseil de PERFORMANCE pour Unraid : /data et /models
// servis par la couche FUSE des partages (fuse.shfs).
//
// Sur Unraid, /mnt/user/… n'est pas un disque mais un démon en espace
// utilisateur (shfs) qui agrège array et pools. Tout y passe : chaque fsync de
// loki.db, et surtout chaque page du .gguf que le noyau doit relire quand un
// gros MoE mmappé ne tient pas en RAM — pendant le décodage, à chaque token.
// Rien de perdu, rien d'altéré (les octets du modèle sont les mêmes), mais tout
// est plus lent.
//
// Loki ne déplace RIEN : changer le chemin hôte d'une installation existante
// donnerait un /data vide et ferait croire à une perte de données (voir le
// README). Il se contente de le VOIR et de le DIRE, sur un ton d'information —
// pas le bandeau rouge de sys_datavolume.go, réservé à la perte de données. Le
// remède sans changement de chemin : passer les partages en « Exclusive
// access » (Unraid 6.12+), qui court-circuite shfs.
import (
"bufio"
"io"
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
// mountEntry : ce qu'on retient d'une ligne de /proc/self/mountinfo.
type mountEntry struct {
point string // point de montage (5ᵉ champ), échappements décodés
fsType string // type de système de fichiers (1ᵉʳ champ après « - »)
}
// parseMountinfo lit le format de /proc/<pid>/mountinfo. Le nombre de champs
// optionnels (shared:N, master:N…) varie d'une ligne à l'autre : le type de
// système de fichiers se lit donc APRÈS le séparateur « - », jamais à un index
// fixe. Les lignes malformées sont ignorées.
func parseMountinfo(r io.Reader) []mountEntry {
var out []mountEntry
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 64*1024), 1024*1024)
for sc.Scan() {
fields := strings.Fields(sc.Text())
if len(fields) < 5 {
continue
}
sep := -1
for i := 5; i < len(fields); i++ {
if fields[i] == "-" {
sep = i
break
}
}
if sep < 0 || sep+1 >= len(fields) {
continue
}
out = append(out, mountEntry{
point: path.Clean(unescapeMountinfo(fields[4])),
fsType: fields[sep+1],
})
}
return out
}
// unescapeMountinfo décode les séquences octales du noyau (\040 espace,
// \011 tabulation, \012 saut de ligne, \134 antislash).
func unescapeMountinfo(s string) string {
if !strings.Contains(s, `\`) {
return s
}
var b strings.Builder
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+3 < len(s) {
if v, err := strconv.ParseUint(s[i+1:i+4], 8, 8); err == nil {
b.WriteByte(byte(v))
i += 3
continue
}
}
b.WriteByte(s[i])
}
return b.String()
}
// mountFSType renvoie le type de système de fichiers qui porte path : celui du
// point de montage le plus LONG qui le contient (/data/models peut être un
// montage imbriqué distinct de /data). À longueur égale, la dernière ligne
// gagne — c'est le montage empilé par-dessus, celui qu'on voit vraiment.
// "" quand aucun montage ne couvre le chemin. Chemins Linux : package path, et
// non filepath, pour que les tests lisent la même chose sous Windows.
func mountFSType(mounts []mountEntry, target string) string {
p := path.Clean(target)
best, fs := -1, ""
for _, m := range mounts {
mp := m.point
if mp != "/" && p != mp && !strings.HasPrefix(p, mp+"/") {
continue
}
if len(mp) >= best {
best, fs = len(mp), m.fsType
}
}
return fs
}
// shfsPaths renvoie, parmi paths, ceux qui passent par fuse.shfs (dédoublonnés,
// dans l'ordre reçu).
func shfsPaths(mounts []mountEntry, paths []string) []string {
var out []string
seen := map[string]bool{}
for _, p := range paths {
if strings.TrimSpace(p) == "" {
continue
}
p = path.Clean(p)
if seen[p] {
continue
}
seen[p] = true
if mountFSType(mounts, p) == "fuse.shfs" {
out = append(out, p)
}
}
return out
}
// storageHintText formule le conseil pour les chemins concernés ("" si aucun).
// Conseil uniquement : les fichiers sont intacts, seule la vitesse en pâtit.
func storageHintText(paths []string) string {
if len(paths) == 0 {
return ""
}
return "ℹ️ Stockage via la couche FUSE des partages Unraid (fuse.shfs) : " + strings.Join(paths, ", ") + ". " +
"Les écritures de la base et les pages du modèle relues depuis le disque (gros modèle qui ne tient pas en RAM) " +
"traversent un démon en espace utilisateur, ce qui ralentit le décodage. Rien n'est perdu. " +
"Remède sans changer de chemin : partages appdata/modèles en « Exclusive access » (Unraid 6.12+). " +
"Voir docker-compose.unraid.yml."
}
// storageHint mis en cache : /api/status est interrogé en boucle, et la liste
// des dossiers de modèles passe par la base. Les montages ne bougent pas pendant
// la vie du conteneur ; une minute suffit à refléter un dossier ajouté dans l'UI.
var storageHintCache struct {
sync.Mutex
at time.Time
val string
}
// storageFuseHint renvoie le conseil « /data ou /models sur fuse.shfs » ("" si
// sans objet). Linux en conteneur seulement, comme dataVolumeWarning : hors
// conteneur, l'utilisateur gère ses chemins.
func storageFuseHint() string {
if runtime.GOOS != "linux" || os.Getenv("LOKI_CONTAINER") == "" {
return ""
}
c := &storageHintCache
c.Lock()
defer c.Unlock()
if !c.at.IsZero() && time.Since(c.at) < time.Minute {
return c.val
}
c.at = time.Now()
c.val = ""
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return "" // illisible : on ne conseille rien plutôt que de deviner
}
defer f.Close()
mounts := parseMountinfo(f)
paths := append([]string{LokiHome()}, modelDirs()...)
c.val = storageHintText(shfsPaths(mounts, paths))
return c.val
}