mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-13 19:54:42 +02:00
feat: initial commit — Ansible roles storage-01/gpu-01 opérationnels
This commit is contained in:
commit
b3fce8af5d
70 changed files with 2688 additions and 0 deletions
57
ansible/roles/lm_studio/files/install-lmstudio-compat.sh
Normal file
57
ansible/roles/lm_studio/files/install-lmstudio-compat.sh
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install LM Studio on AlmaLinux 9.
|
||||
#
|
||||
# AlmaLinux 9 ships GCC 11 → libstdc++ provides up to GLIBCXX_3.4.29.
|
||||
# LM Studio bundles watcher.node (compiled with GCC 12) which requires
|
||||
# _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@GLIBCXX_3.4.30.
|
||||
# That symbol exists in the system libstdc++ under GLIBCXX_3.4.11.
|
||||
#
|
||||
# Fix: ELF-patch watcher.node to rewrite the VERNEED entry from
|
||||
# GLIBCXX_3.4.30 → GLIBCXX_3.4.11 (same symbol, correct version tag).
|
||||
# Both vna_hash and the version string are updated in-place.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Ansible copy task places files in /tmp
|
||||
if [ -f "/tmp/patch-glibcxx-compat.py" ]; then
|
||||
PATCHER="/tmp/patch-glibcxx-compat.py"
|
||||
else
|
||||
PATCHER="${SCRIPT_DIR}/patch-glibcxx-compat.py"
|
||||
fi
|
||||
|
||||
# --- Fetch installer metadata ---
|
||||
echo "[lmstudio-install] Fetching installer metadata..."
|
||||
INSTALL_SH=$(curl -fsSL https://lmstudio.ai/install.sh)
|
||||
VERSION=$(echo "$INSTALL_SH" | grep '^APP_VERSION=' | cut -d'"' -f2)
|
||||
ARTIFACT_URL_BASE=$(echo "$INSTALL_SH" | grep '^ARTIFACT_DOWNLOAD_URL=' | cut -d'"' -f2)
|
||||
|
||||
ARTIFACT="${VERSION}-linux-x64.full.tar.gz"
|
||||
URL="https://${ARTIFACT_URL_BASE}/${ARTIFACT}"
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
echo "[lmstudio-install] Downloading ${ARTIFACT}..."
|
||||
curl -fsSL "$URL" -o "$TMPDIR/bundle.tar.gz"
|
||||
|
||||
echo "[lmstudio-install] Extracting bundle..."
|
||||
tar xf "$TMPDIR/bundle.tar.gz" -C "$TMPDIR"
|
||||
|
||||
# Patch watcher.node before bootstrap so the daemon never sees GLIBCXX_3.4.30
|
||||
echo "[lmstudio-install] Patching watcher.node (GLIBCXX_3.4.30 → GLIBCXX_3.4.11)..."
|
||||
find "$TMPDIR" -name 'watcher.node' | while read -r node; do
|
||||
python3 "$PATCHER" "$node"
|
||||
done
|
||||
|
||||
echo "[lmstudio-install] Running bootstrap..."
|
||||
LMS_BOOTSTRAP_INSTALL_SH=1 "$TMPDIR/llmster" bootstrap
|
||||
|
||||
# Also patch the installed copy in ~/.lmstudio after bootstrap
|
||||
echo "[lmstudio-install] Patching installed watcher.node copies..."
|
||||
find "${HOME}/.lmstudio" -name 'watcher.node' 2>/dev/null | while read -r node; do
|
||||
python3 "$PATCHER" "$node"
|
||||
done
|
||||
|
||||
echo "[lmstudio-install] Installation complete."
|
||||
147
ansible/roles/lm_studio/files/patch-glibcxx-compat.py
Normal file
147
ansible/roles/lm_studio/files/patch-glibcxx-compat.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Patch ELF .so/.node files that require GLIBCXX_3.4.30 to use GLIBCXX_3.4.29
|
||||
instead. Required for AlmaLinux 9 / RHEL 9 which ships GCC 11 (max GLIBCXX_3.4.29).
|
||||
|
||||
Both the version string AND the vna_hash in .gnu.version_r are updated so the
|
||||
dynamic linker finds the version correctly at runtime.
|
||||
"""
|
||||
import sys
|
||||
import struct
|
||||
|
||||
|
||||
def elf_hash(name: str) -> int:
|
||||
"""ELF version symbol hash (not GNU hash — used in .gnu.version_r)."""
|
||||
h = 0
|
||||
for c in name:
|
||||
h = ((h << 4) + ord(c)) & 0xFFFFFFFF
|
||||
g = h & 0xF0000000
|
||||
if g:
|
||||
h ^= g >> 24
|
||||
h &= (~g) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def patch_file(path: str) -> bool:
|
||||
with open(path, 'rb') as f:
|
||||
data = bytearray(f.read())
|
||||
|
||||
old_ver = b'GLIBCXX_3.4.30'
|
||||
new_ver = b'GLIBCXX_3.4.11'
|
||||
|
||||
if old_ver not in data:
|
||||
print(f' skip (no GLIBCXX_3.4.30): {path}')
|
||||
return False
|
||||
|
||||
# ----- ELF header -----
|
||||
if data[:4] != b'\x7fELF':
|
||||
print(f' skip (not ELF): {path}')
|
||||
return False
|
||||
|
||||
ei_class = data[4] # 1=32-bit, 2=64-bit
|
||||
ei_data = data[5] # 1=LE, 2=BE
|
||||
endian = '<' if ei_data == 1 else '>'
|
||||
|
||||
if ei_class == 2:
|
||||
# 64-bit ELF
|
||||
e_shoff_fmt = f'{endian}Q'
|
||||
ehdr_size = 64
|
||||
shdr_size = 64
|
||||
shdr_fmt = f'{endian}IIQQQQIIQQ' # 64-byte shdr
|
||||
(e_shoff,) = struct.unpack_from(f'{endian}Q', data, 40)
|
||||
(e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 58)
|
||||
shdr_type_off = 4
|
||||
shdr_off_off = 24 # sh_offset within shdr
|
||||
shdr_link_off = 48 # sh_link within shdr
|
||||
else:
|
||||
# 32-bit ELF
|
||||
(e_shoff,) = struct.unpack_from(f'{endian}I', data, 32)
|
||||
(e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 46)
|
||||
shdr_type_off = 4
|
||||
shdr_off_off = 16
|
||||
shdr_link_off = 28
|
||||
|
||||
SHT_GNU_verneed = 0x6FFFFFFE
|
||||
|
||||
patched = False
|
||||
# Target GLIBCXX_3.4.11 — that's where condition_variable::wait actually lives
|
||||
# in AlmaLinux 9's libstdc++.so.6 (same byte length: 14 chars each)
|
||||
old_hash = elf_hash('GLIBCXX_3.4.30')
|
||||
new_hash = elf_hash('GLIBCXX_3.4.11')
|
||||
old_hash_bytes = struct.pack(f'{endian}I', old_hash)
|
||||
new_hash_bytes = struct.pack(f'{endian}I', new_hash)
|
||||
|
||||
for i in range(e_shnum):
|
||||
shdr_base = e_shoff + i * e_shentsize
|
||||
sh_type = struct.unpack_from(f'{endian}I', data, shdr_base + shdr_type_off)[0]
|
||||
|
||||
if sh_type != SHT_GNU_verneed:
|
||||
continue
|
||||
|
||||
# sh_offset and sh_link (index of associated string table section)
|
||||
if ei_class == 2:
|
||||
sh_offset = struct.unpack_from(f'{endian}Q', data, shdr_base + 24)[0]
|
||||
sh_size = struct.unpack_from(f'{endian}Q', data, shdr_base + 32)[0]
|
||||
sh_link = struct.unpack_from(f'{endian}I', data, shdr_base + 40)[0]
|
||||
sh_info = struct.unpack_from(f'{endian}I', data, shdr_base + 44)[0]
|
||||
else:
|
||||
sh_offset = struct.unpack_from(f'{endian}I', data, shdr_base + 16)[0]
|
||||
sh_size = struct.unpack_from(f'{endian}I', data, shdr_base + 20)[0]
|
||||
sh_link = struct.unpack_from(f'{endian}I', data, shdr_base + 24)[0]
|
||||
sh_info = struct.unpack_from(f'{endian}I', data, shdr_base + 28)[0]
|
||||
|
||||
# String table section referenced by sh_link
|
||||
strtab_shdr = e_shoff + sh_link * e_shentsize
|
||||
if ei_class == 2:
|
||||
strtab_off = struct.unpack_from(f'{endian}Q', data, strtab_shdr + 24)[0]
|
||||
else:
|
||||
strtab_off = struct.unpack_from(f'{endian}I', data, strtab_shdr + 16)[0]
|
||||
|
||||
# Walk Elf64_Verneed / Elf64_Vernaux entries
|
||||
# Elf64_Verneed: vn_version(2)+vn_cnt(2)+vn_file(4)+vn_aux(4)+vn_next(4) = 16 bytes
|
||||
# Elf64_Vernaux: vna_hash(4)+vna_flags(2)+vna_other(2)+vna_name(4)+vna_next(4) = 16 bytes
|
||||
vneed_pos = sh_offset
|
||||
for _ in range(sh_info): # sh_info = number of Verneed entries
|
||||
vn_cnt = struct.unpack_from(f'{endian}H', data, vneed_pos + 2)[0]
|
||||
vn_aux = struct.unpack_from(f'{endian}I', data, vneed_pos + 8)[0] # +8 (after vn_file)
|
||||
vn_next = struct.unpack_from(f'{endian}I', data, vneed_pos + 12)[0]
|
||||
|
||||
vaux_pos = vneed_pos + vn_aux
|
||||
for _ in range(vn_cnt):
|
||||
vna_hash = struct.unpack_from(f'{endian}I', data, vaux_pos)[0]
|
||||
vna_name = struct.unpack_from(f'{endian}I', data, vaux_pos + 8)[0]
|
||||
vna_next = struct.unpack_from(f'{endian}I', data, vaux_pos + 12)[0]
|
||||
|
||||
# Read the version name from the string table
|
||||
name_off = strtab_off + vna_name
|
||||
name_end = data.index(b'\x00', name_off)
|
||||
ver_name = bytes(data[name_off:name_end])
|
||||
|
||||
if ver_name == old_ver:
|
||||
# Patch hash
|
||||
struct.pack_into(f'{endian}I', data, vaux_pos, new_hash)
|
||||
# Patch string (same byte length — safe in-place)
|
||||
data[name_off:name_off + len(old_ver)] = new_ver
|
||||
patched = True
|
||||
|
||||
if vna_next == 0:
|
||||
break
|
||||
vaux_pos += vna_next
|
||||
|
||||
if vn_next == 0:
|
||||
break
|
||||
vneed_pos += vn_next
|
||||
|
||||
if patched:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
print(f' patched (hash+string): {path}')
|
||||
return True
|
||||
else:
|
||||
print(f' skip (GLIBCXX_3.4.30 not in verneed): {path}')
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
for path in sys.argv[1:]:
|
||||
patch_file(path)
|
||||
45
ansible/roles/lm_studio/files/run-llmster.sh
Normal file
45
ansible/roles/lm_studio/files/run-llmster.sh
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
# LM Studio service wrapper.
|
||||
#
|
||||
# Lets lms server start handle daemon launch (passkey setup, etc.),
|
||||
# optionally auto-loads a model (LMS_MODEL + LMS_CONTEXT_LENGTH),
|
||||
# then monitors the llmster process and exits when it dies so systemd
|
||||
# can restart the service.
|
||||
set -e
|
||||
|
||||
LMS="${HOME}/.lmstudio/bin/lms"
|
||||
LMS_PORT="${LMS_PORT:-1234}"
|
||||
LMS_BIND="${LMS_BIND:-0.0.0.0}"
|
||||
LMS_MODEL="${LMS_MODEL:-}"
|
||||
LMS_CONTEXT_LENGTH="${LMS_CONTEXT_LENGTH:-65536}"
|
||||
LMS_GPU_OFFLOAD="${LMS_GPU_OFFLOAD:-max}"
|
||||
|
||||
echo "Starting LM Studio server (port ${LMS_PORT}, bind ${LMS_BIND})..."
|
||||
"$LMS" server start --port "$LMS_PORT" --bind "$LMS_BIND"
|
||||
|
||||
if [[ -n "$LMS_MODEL" ]]; then
|
||||
echo "Unloading any previously loaded models..."
|
||||
"$LMS" unload --all 2>/dev/null || true
|
||||
echo "Auto-loading model ${LMS_MODEL} (context ${LMS_CONTEXT_LENGTH}, gpu ${LMS_GPU_OFFLOAD})..."
|
||||
"$LMS" load "$LMS_MODEL" --context-length "$LMS_CONTEXT_LENGTH" --gpu "$LMS_GPU_OFFLOAD"
|
||||
fi
|
||||
|
||||
# llmster bind sur 127.0.0.1 après chargement du backend Vulkan.
|
||||
# socat proxy sur l'IP LAN (non-conflictuel avec le loopback) → llmster.
|
||||
if [[ "$LMS_BIND" != "127.0.0.1" ]]; then
|
||||
LAN_IP=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||||
if [[ -n "$LAN_IP" ]]; then
|
||||
echo "Starting socat proxy ${LAN_IP}:${LMS_PORT} → 127.0.0.1:${LMS_PORT}..."
|
||||
socat TCP-LISTEN:"${LMS_PORT}",fork,bind="${LAN_IP}",reuseaddr TCP:127.0.0.1:"${LMS_PORT}" &
|
||||
SOCAT_PID=$!
|
||||
echo "socat PID: ${SOCAT_PID}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Server started. Monitoring llmster daemon..."
|
||||
while pgrep -u "$(id -un)" -x llmster > /dev/null 2>&1; do
|
||||
sleep 15
|
||||
done
|
||||
|
||||
echo "llmster daemon stopped unexpectedly, exiting..."
|
||||
exit 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue