mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 11:04:43 +02:00
369 lines
12 KiB
Markdown
369 lines
12 KiB
Markdown
# 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 vers `GLIBCXX_3.4.30`. AlmaLinux 9 ne fournit que GCC 11 → `GLIBCXX_3.4.29` maximum.
|
|
|
|
---
|
|
|
|
## 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`.
|
|
|
|
```c
|
|
// 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.30` fourni par `libstdc++.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` :
|
|
|
|
```c
|
|
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 :
|
|
1. `vna_name` → offset vers la chaîne dans `.dynstr`
|
|
2. `vna_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 :
|
|
```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` :
|
|
|
|
```bash
|
|
$ 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 :
|
|
|
|
```python
|
|
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` :
|
|
```python
|
|
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é
|
|
|
|
```python
|
|
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 :
|
|
```bash
|
|
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` :
|
|
1. Lance le daemon `llmster` en arrière-plan
|
|
2. Attend que le serveur HTTP soit prêt
|
|
3. Affiche `Success! Server is now running on port 1234`
|
|
4. **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` :
|
|
|
|
```ini
|
|
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`
|
|
|
|
```bash
|
|
#!/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 start` gère correctement passkey + démarrage du daemon
|
|
- La boucle maintient le service `active (running)` dans systemd
|
|
- Si `llmster` meurt, le wrapper sort avec code 1 → `Restart=on-failure` relance 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é
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
# 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) :
|
|
|
|
```bash
|
|
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.
|