12 KiB
LM Studio sur AlmaLinux 9 — Déblocages et solutions
Contexte
- Machine : gpu-01, AlmaLinux 9.7, GCC 11, libstdc++ → max
GLIBCXX_3.4.29 - Problème initial : LM Studio refuse de démarrer avec
GLIBCXX_3.4.30 not found - Cause racine :
watcher.node(module Node natif livré avec LM Studio) a été compilé avec GCC 12 et déclare une dépendance ELF versGLIBCXX_3.4.30. AlmaLinux 9 ne fournit que GCC 11 →GLIBCXX_3.4.29maximum.
Blocage 1 — LD_PRELOAD ne peut pas satisfaire DT_VERNEED
Ce qui a été tenté
Construire un shim .so qui exporte _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE sous le tag de version GLIBCXX_3.4.30, puis l'injecter via LD_PRELOAD.
// glibcxx_compat.c
__asm__(".symver _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE_compat,"
"_ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@GLIBCXX_3.4.30");
gcc -shared -fPIC -Wl,--version-script=glibcxx_compat.map \
-o libglibcxx_compat.so glibcxx_compat.c /usr/lib64/libstdc++.so.6
Pourquoi ça échoue
Dans ELF, DT_VERNEED lie une exigence de version à une bibliothèque nominalement identifiée. La section .gnu.version_r de watcher.node dit :
« J'ai besoin de
GLIBCXX_3.4.30fourni parlibstdc++.so.6. »
Le linker dynamique (ld.so) vérifie cette version directement dans libstdc++.so.6, pas dans les bibliothèques chargées via LD_PRELOAD. Un shim préchargé sous un autre nom de fichier ne peut pas satisfaire cette exigence — même s'il exporte le bon symbole sous le bon nom de version.
Erreur observée
Malgré LD_PRELOAD, le daemon plante toujours avec :
/lib64/libstdc++.so.6: version GLIBCXX_3.4.30 not found
Blocage 2 — Patch texte simple ne suffit pas (hash ELF non mis à jour)
Ce qui a été tenté
Remplacer la chaîne GLIBCXX_3.4.30 par GLIBCXX_3.4.29 directement dans le binaire avec sed ou un patch Python simple.
Pourquoi ça échoue
La section .gnu.version_r contient des structures Elf64_Vernaux :
typedef struct {
Elf64_Word vna_hash; // hash ELF du nom de version
Elf64_Half vna_flags;
Elf64_Half vna_other;
Elf64_Word vna_name; // offset dans la string table
Elf64_Word vna_next;
} Elf64_Vernaux;
Chaque entrée contient deux références au nom de version :
vna_name→ offset vers la chaîne dans.dynstrvna_hash→ hash ELF de cette chaîne (précalculé)
Un patch texte change la chaîne mais laisse vna_hash intact. Le linker dynamique vérifie le hash en premier — il ne trouve pas hash(GLIBCXX_3.4.29) dans libstdc++.so.6 et rejette le module.
Erreur observée
/lib64/libstdc++.so.6: version GLIBCXX_3.4.29 not found
(nouveau message — la chaîne a changé mais le hash est toujours celui de GLIBCXX_3.4.30)
Blocage 3 — Mauvais offset dans le parseur ELF
Ce qui a été tenté
Écrire patch-glibcxx-compat.py pour parser .gnu.version_r et patcher hash + chaîne.
Erreur de décodage
Structure Elf64_Verneed :
offset +0 : vn_version (2 bytes)
offset +2 : vn_cnt (2 bytes)
offset +4 : vn_file (4 bytes) ← oublié !
offset +8 : vn_aux (4 bytes)
offset +12 : vn_next (4 bytes)
Le premier jet lisait vn_aux à +4 (en sautant vn_file) — le parseur pointait vers de mauvaises entrées, ne trouvait rien à patcher.
Fix appliqué
Corriger les offsets dans le script Python :
vn_aux = struct.unpack_from(f'{endian}I', data, vneed_pos + 8)[0] # +8, pas +4
vn_next = struct.unpack_from(f'{endian}I', data, vneed_pos + 12)[0] # +12, pas +8
Blocage 4 — Patcher vers GLIBCXX_3.4.29 mais le symbole n'y existe pas
Ce qui a été tenté
Rediriger la dépendance de GLIBCXX_3.4.30 vers GLIBCXX_3.4.29 (version juste en dessous, disponible sur AlmaLinux 9).
Pourquoi ça échoue
Le symbole _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE n'existe pas sous GLIBCXX_3.4.29 dans la libstdc++ système. Il est exporté sous GLIBCXX_3.4.11 :
$ nm -D /lib64/libstdc++.so.6 | grep condition_variable | grep wait
...
_ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@@GLIBCXX_3.4.11
Patcher vers .4.29 donnait encore version GLIBCXX_3.4.29 not found (pas de ce symbole sous cette version).
Fix appliqué
Cibler GLIBCXX_3.4.11 dans le patcher — c'est là que le symbole vit réellement :
old_ver = b'GLIBCXX_3.4.30'
new_ver = b'GLIBCXX_3.4.11' # même longueur → patch in-place sûr
old_hash = elf_hash('GLIBCXX_3.4.30')
new_hash = elf_hash('GLIBCXX_3.4.11')
Les deux chaînes font 14 octets → remplacement in-place sans décalage de la table de chaînes.
Blocage 5 — bytearray.index(0, ...) lève ValueError
Ce qui a été tenté
Lire le nom de version depuis .dynstr :
name_end = data.index(0, name_off) # cherche le '\0' terminal
Pourquoi ça échoue
bytearray.index() attend un entier ou une séquence d'octets selon le contexte, mais la forme data.index(0, ...) sur bytearray lève ValueError: 0 is not in bytearray (comportement différent de bytes).
Fix appliqué
name_end = data.index(b'\x00', name_off) # forme bytes, toujours valide
Blocage 6 — gcc -lstdc++ introuvable
Ce qui a été tenté
Compiler le shim LD_PRELOAD (approche abandonnée) avec -lstdc++.
Pourquoi ça échoue
AlmaLinux 9 ne fournit pas le lien symbolique libstdc++.so (sans version) nécessaire au linker. Seul libstdc++.so.6 (avec version) est présent.
Fix appliqué
Passer le chemin complet :
gcc -shared -fPIC -Wl,--version-script=glibcxx_compat.map \
-o libglibcxx_compat.so glibcxx_compat.c /usr/lib64/libstdc++.so.6
Blocage 7 — Systemd tuait le daemon après démarrage (lms server start quitte après 0)
Ce qui a été tenté
Service Type=simple avec ExecStart=lms server start --port 1234 --bind 0.0.0.0.
Comportement observé
lms server start :
- Lance le daemon
llmsteren arrière-plan - Attend que le serveur HTTP soit prêt
- Affiche
Success! Server is now running on port 1234 - Quitte avec code 0
Systemd détecte la fin du processus principal → KillMode=control-group (défaut) → tue tout le cgroup → llmster mort → service inactive.
Le daemon survivait parfois grâce à un double-fork qui lui permettait d'échapper au cgroup, mais c'était non fiable et systemctl status montrait inactive même quand le serveur tournait.
Ce qui n'a pas fonctionné en intermédiaire
Lancer llmster directement via ExecStart + ExecStartPost=lms server start :
ExecStart=/opt/lmstudio/.lmstudio/llmster/0.0.12-1/llmster
ExecStartPost=/bin/bash -c 'sleep 5 && lms server start --port 1234 --bind 0.0.0.0'
Erreur :
Failed to authenticate: Invalid passkey for lms CLI client.
lms server start utilise un passkey stocké/généré lors du démarrage du daemon via lms. Démarrer llmster directement bypasse ce mécanisme → authentification impossible depuis lms.
Fix appliqué — wrapper run-llmster.sh
#!/bin/bash
LMS="${HOME}/.lmstudio/bin/lms"
# lms gère le passkey ET le démarrage du daemon
"$LMS" server start --port "$LMS_PORT" --bind "$LMS_BIND"
# Rester en vie tant que llmster tourne → systemd garde le service "active"
while pgrep -u "$(id -un)" -x llmster > /dev/null 2>&1; do
sleep 15
done
exit 1 # déclenche Restart=on-failure si le daemon meurt
Résultat :
- Le wrapper est le processus principal (
Type=simple) lms server startgère correctement passkey + démarrage du daemon- La boucle maintient le service
active (running)dans systemd - Si
llmstermeurt, le wrapper sort avec code 1 →Restart=on-failurerelance tout
Blocage 8 — Port 1234 inaccessible depuis le réseau
Symptôme
curl http://192.168.10.20:1234/v1/models depuis storage-01 → No route to host
Le serveur écoutait bien sur 0.0.0.0:1234 mais le pare-feu AlmaLinux bloquait le port.
Fix appliqué
firewall-cmd --permanent --add-port=1234/tcp
firewall-cmd --reload
Ajouté dans tasks/main.yml via ansible.posix.firewalld pour idempotence.
Solution finale
| Composant | Approche retenue |
|---|---|
| Dépendance GLIBCXX | Patch ELF de watcher.node : réécriture de vna_hash + chaîne GLIBCXX_3.4.30 → GLIBCXX_3.4.11 |
| Service systemd | Wrapper shell : lms server start + boucle de monitoring pgrep llmster |
| Firewall | firewall-cmd --permanent --add-port=1234/tcp |
Fichiers du rôle Ansible
roles/lm_studio/
├── files/
│ ├── patch-glibcxx-compat.py # parseur ELF, patch hash+string VERNEED
│ ├── install-lmstudio-compat.sh # télécharge bundle, patche watcher.node, bootstrap
│ └── run-llmster.sh # wrapper systemd
└── templates/
└── lm-studio.service.j2 # Type=simple, ExecStart=run-llmster.sh
Vérification
# Depuis storage-01
curl http://192.168.10.20:1234/v1/models
# → {"data":[...],"object":"list"}
# Sur gpu-01
systemctl status lm-studio
# → active (running), MainPID=run-llmster.sh, llmster dans le cgroup
Blocage 9 — llmster rebind sur 127.0.0.1 après chargement Vulkan
Symptôme
lms server start --bind 0.0.0.0 démarre correctement, mais après
lms load <modele> --gpu max avec le backend Vulkan, llmster rebind son
serveur HTTP sur 127.0.0.1:1234. Le port 1234 devient inaccessible depuis
le réseau LAN.
# Sur gpu-01 après lms load :
ss -tlnp | grep 1234
# LISTEN 0 128 127.0.0.1:1234 ← loopback seulement
Cause
Comportement interne de llmster lors de l'initialisation du backend Vulkan : le processus recrée son socket HTTP et le bind sur loopback plutôt que sur l'adresse configurée au démarrage.
Fix appliqué — socat proxy dans run-llmster.sh
Après le chargement du modèle, un proxy socat est lancé sur l'IP LAN pour forwarder vers le loopback. Les deux sockets sont non-conflictuels (IPs différentes) :
LAN_IP=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
socat TCP-LISTEN:1234,fork,bind="${LAN_IP}",reuseaddr TCP:127.0.0.1:1234 &
Pourquoi ne pas relancer lms server start --bind 0.0.0.0 ?
Relancer lms server start après le chargement recharge le modèle avec le
contexte par défaut (4096 tokens), écrasant le --context-length 65536
passé à lms load.
Blocage 10 — Gemma 4 crash Vulkan sur prompts longs (>800 tokens)
Symptôme
Avec google/gemma-4-e4b ou google/gemma-4-e2b, la première inférence
avec un prompt de plus de ~800 tokens provoque un crash du processus llmster :
HTTP 400 {"error":"The model has crashed without additional information. (Exit code: null)"}
Après le crash, llmster recharge le modèle avec le contexte par défaut (4096),
et les requêtes suivantes échouent avec n_keep >= n_ctx.
Cause
Bug dans l'implémentation Vulkan de llama.cpp pour l'architecture Gemma 4. Gemma 4 utilise une attention interleaved global/local avec un sliding window de 512 tokens. Quand le prompt dépasse 512 tokens, les layers d'attention globale crashent dans le shader Vulkan.
Seuil confirmé par test binaire :
- OK à 500 tokens (~100x "You are a helpful assistant.")
- CRASH à ~900 tokens (~130x)
La VRAM n'est pas en cause : avec GQA aggressif et sliding window, le KV cache de Gemma 4 à 65536 tokens est ~2 GB seulement (bien dans les 12GB VRAM).
Fix
Utiliser un modèle avec attention GQA standard (sans sliding window interleaved) :
- Qwen3.5-9B ✓ — support Vulkan mature, GQA standard
- Qwen2.5-7B-Instruct ✓ — alternative
Ne pas utiliser Gemma 4 E2B ou E4B avec le backend Vulkan de LM Studio jusqu'à correction du bug upstream dans llama.cpp.