Files
gpu-rent/src/gpu_rent/remote/install_llamacpp.sh
T
Leonid Pershin 6871c511c4 Implement UP_STOP_ON_FAIL option to manage GPU state on installation failure
- Added a new configuration option `UP_STOP_ON_FAIL` to control whether the GPU should be stopped automatically if the `up` command fails, enhancing user control over resource management.
- Updated the CLI to include a `--keep-on-fail` flag, allowing users to prevent GPU shutdown during installation errors.
- Enhanced the installation scripts and documentation to reflect these changes, providing clearer guidance on the new behavior and configuration options.
- Improved error handling in the CLI to ensure proper cleanup of resources in case of failure, preventing unexpected billing for unused GPU resources.
2026-08-21 08:32:33 +03:00

472 lines
15 KiB
Bash
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.
#!/usr/bin/env bash
# Install llama-server for OpenAI-compatible API on loopback :8080.
#
# Default: official Linux release asset (Ubuntu Vulkan — GPU without compile).
# CUDA source build only as last resort (or LLAMACPP_BUILD_CUDA=1).
# Pin: LLAMACPP_TAG=b10545 LLAMACPP_ASSET_URL=... LLAMACPP_SHA256=...
set -euo pipefail
SWARM_USER="${SWARM_USER:-ubuntu}"
DATA_ROOT="/mnt/swarm_data"
LLAMA_ROOT="${DATA_ROOT}/llamacpp"
MODELS_DIR="${LLAMA_ROOT}/models"
BIN_DIR="${LLAMA_ROOT}/bin"
SRC_DIR="${LLAMA_ROOT}/src"
STAMP="${BIN_DIR}/.build-id"
UNIT="gpu-rent-llamacpp"
REPO="https://github.com/ggml-org/llama.cpp.git"
API_BASE="https://api.github.com/repos/ggml-org/llama.cpp"
log() { echo "[gpu-rent-llamacpp] $*" >&2; }
if [[ "$(id -u)" -ne 0 ]]; then
echo "нужен root" >&2
exit 1
fi
mkdir -p "$MODELS_DIR" "$BIN_DIR"
chown -R "${SWARM_USER}:${SWARM_USER}" "$LLAMA_ROOT"
SERVER_BIN="${BIN_DIR}/llama-server"
LLAMACPP_TAG="${LLAMACPP_TAG:-}"
LLAMACPP_ASSET_URL="${LLAMACPP_ASSET_URL:-}"
LLAMACPP_SHA256="${LLAMACPP_SHA256:-}"
# 1 = force CUDA compile; 0 = never compile (Vulkan/CPU prebuilt only)
LLAMACPP_BUILD_CUDA="${LLAMACPP_BUILD_CUDA:-}"
# auto | cuda | vulkan — default auto: CUDA if nvcc already on VM, else Vulkan prebuilt
LLAMACPP_BACKEND="${LLAMACPP_BACKEND:-auto}"
LLAMACPP_FORCE_REINSTALL="${LLAMACPP_FORCE_REINSTALL:-}"
LLAMACPP_NGL="${LLAMACPP_NGL:-}"
LLAMACPP_CTX="${LLAMACPP_CTX:-}"
LLAMACPP_HOST="${LLAMACPP_HOST:-127.0.0.1}"
LLAMACPP_PORT="${LLAMACPP_PORT:-8080}"
LLAMACPP_EXTRA_ARGS="${LLAMACPP_EXTRA_ARGS:-}"
have_nvcc() {
if command -v nvcc >/dev/null 2>&1; then
return 0
fi
if [[ -x /usr/local/cuda/bin/nvcc ]]; then
export PATH="/usr/local/cuda/bin:${PATH}"
return 0
fi
return 1
}
# Prefer CUDA when toolkit already present (GPU images / prior up). Vulkan = fast no-compile.
want_cuda_build() {
case "${LLAMACPP_BUILD_CUDA}" in
1|yes|true) return 0 ;;
0|no|false) return 1 ;;
esac
case "${LLAMACPP_BACKEND}" in
cuda) return 0 ;;
vulkan) return 1 ;;
*)
if have_nvcc; then
return 0
fi
return 1
;;
esac
}
if [[ "${LLAMACPP_FORCE_REINSTALL}" == "1" ]]; then
log "LLAMACPP_FORCE_REINSTALL=1 — удаляю старый бинарь"
rm -f "$SERVER_BIN" "$STAMP"
fi
# Upgrade path: previous default was Vulkan prebuilt; if nvcc is here, prefer CUDA.
if [[ -x "$SERVER_BIN" && -f "$STAMP" && "${LLAMACPP_BACKEND}" != "vulkan" && "${LLAMACPP_BUILD_CUDA}" != "0" ]]; then
if grep -q '^asset:' "$STAMP" 2>/dev/null && want_cuda_build; then
log "был Vulkan/CPU prebuilt, nvcc есть — пересобираю CUDA (лучше на NVIDIA)"
rm -f "$SERVER_BIN" "$STAMP"
fi
fi
# Prefer ubuntu CUDA (rare) → vulkan → cpu. Never Windows/macOS/cudart-only.
pick_linux_asset_url() {
python3 -c '
import json,sys
data=json.load(sys.stdin)
assets=data.get("assets") or []
cands=[]
for a in assets:
n=(a.get("name") or "").lower()
u=a.get("browser_download_url") or ""
if not (u.endswith(".zip") or u.endswith(".tar.gz")):
continue
if any(x in n for x in ("win","macos","android","darwin","xcframework","-ui.")):
continue
if "cudart" in n:
continue
score=0
if "ubuntu" in n and "x64" in n and "cuda" in n:
score=100
elif "linux" in n and "cuda" in n:
score=90
elif "ubuntu" in n and "vulkan" in n and "x64" in n:
score=50
elif "ubuntu" in n and "x64" in n and not any(
x in n for x in ("sycl","openvino","arm","s390","rocm")
):
score=30
elif "ubuntu" in n or "linux" in n:
score=10
if score:
cands.append((score, u, n))
cands.sort(reverse=True)
print(cands[0][1] if cands else "")
'
}
resolve_release_tag() {
# stdout = tag only (no log lines — callers capture via $())
if [[ -n "$LLAMACPP_TAG" ]]; then
echo "$LLAMACPP_TAG"
return
fi
curl -fsSL "${API_BASE}/releases/latest" | python3 -c \
'import json,sys; print(json.load(sys.stdin).get("tag_name") or "")'
}
cuda_architectures() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path("/mnt/swarm_data/.gpu-rent-gpu.json")
cap = "8.9"
if p.is_file():
try:
cap = str(json.loads(p.read_text()).get("compute_cap") or cap)
except Exception:
pass
parts = cap.split(".")
try:
maj, mnr = int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
print(f"{maj}{mnr}")
except ValueError:
print("89")
PY
}
ensure_build_deps() {
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq \
cmake build-essential git curl ca-certificates \
libcurl4-openssl-dev >/dev/null
if command -v nvcc >/dev/null 2>&1; then
return 0
fi
if [[ -x /usr/local/cuda/bin/nvcc ]]; then
export PATH="/usr/local/cuda/bin:${PATH}"
return 0
fi
log "ставлю nvidia-cuda-toolkit (нужен nvcc)…"
apt-get install -y -qq nvidia-cuda-toolkit >/dev/null
if command -v nvcc >/dev/null 2>&1; then
return 0
fi
if [[ -x /usr/local/cuda/bin/nvcc ]]; then
export PATH="/usr/local/cuda/bin:${PATH}"
return 0
fi
return 1
}
ensure_vulkan_runtime() {
if ldconfig -p 2>/dev/null | grep -q 'libvulkan\.so'; then
return 0
fi
export DEBIAN_FRONTEND=noninteractive
log "ставлю libvulkan1 (для Ubuntu Vulkan prebuilt)…"
apt-get install -y -qq libvulkan1 mesa-vulkan-drivers >/dev/null 2>&1 || \
apt-get install -y -qq libvulkan1 >/dev/null 2>&1 || true
}
install_from_archive_url() {
local url="$1"
local tmp kind
tmp="$(mktemp -d)"
(
cd "$tmp"
log "скачиваю prebuilt: $url"
curl -fL --progress-bar "$url" -o pkg.bin
if [[ -n "$LLAMACPP_SHA256" ]]; then
echo "${LLAMACPP_SHA256} pkg.bin" | sha256sum -c -
else
log "WARN: LLAMACPP_SHA256 не задан — checksum skip"
fi
mkdir -p out
# Do NOT grep -i zip — that matches "gzip" and breaks .tar.gz.
kind="$(file -b pkg.bin 2>/dev/null || true)"
case "$url" in
*.zip)
apt-get install -y -qq unzip >/dev/null 2>&1 || true
unzip -qo pkg.bin -d out
;;
*)
if [[ "$kind" == Zip\ archive* ]] || [[ "$kind" == *"Zip archive"* ]]; then
apt-get install -y -qq unzip >/dev/null 2>&1 || true
unzip -qo pkg.bin -d out
else
tar -xaf pkg.bin -C out 2>/dev/null \
|| tar -xzf pkg.bin -C out 2>/dev/null \
|| tar -xf pkg.bin -C out
fi
;;
esac
local found
found="$(find out -type f -name 'llama-server' | head -n1 || true)"
if [[ -z "$found" ]]; then
found="$(find out -type f -name 'server' | head -n1 || true)"
fi
if [[ -z "$found" ]]; then
log "в архиве нет llama-server (file says: ${kind:-unknown})"
exit 1
fi
install -m 755 "$found" "$SERVER_BIN"
# Shared libs next to binary (release tarballs ship .so alongside).
find out -type f \( -name '*.so' -o -name '*.so.*' \) -print0 2>/dev/null \
| while IFS= read -r -d '' so; do
install -m 755 "$so" "${BIN_DIR}/$(basename "$so")"
done
chown -R "${SWARM_USER}:${SWARM_USER}" "$BIN_DIR"
)
local rc=$?
rm -rf "$tmp"
return "$rc"
}
install_linux_release() {
local tag="$1"
local api url
api="${API_BASE}/releases/tags/${tag}"
url="$(curl -fsSL "$api" | pick_linux_asset_url)"
if [[ -z "$url" ]]; then
log "в release ${tag} нет Linux-ассета"
return 1
fi
if [[ "$url" == *vulkan* ]]; then
log "беру Ubuntu Vulkan prebuilt (GPU без compile; CUDA-сборка — LLAMACPP_BUILD_CUDA=1)"
ensure_vulkan_runtime
elif [[ "$url" == *cuda* ]]; then
log "беру Linux CUDA prebuilt"
else
log "WARN: Linux prebuilt без GPU backend (CPU) — ${url##*/}"
fi
if ! install_from_archive_url "$url"; then
return 1
fi
echo "asset:${tag}" >"$STAMP"
chown "${SWARM_USER}:${SWARM_USER}" "$STAMP"
}
# Quiet CUDA build: no cmake spam; heartbeat every 30s with last %.
build_cuda_from_source() {
local tag="$1"
local arch build_log pid pct line
arch="$(cuda_architectures)"
build_log="${LLAMA_ROOT}/build-cuda.log"
log "крайний случай: сборка CUDA из исходников (tag=${tag}, arch=${arch}, 515 мин)…"
log "полный лог: ${build_log}"
if ! ensure_build_deps; then
log "нет nvcc — CUDA-сборку пропускаем"
return 1
fi
log "nvcc $(nvcc --version 2>/dev/null | tail -n1 || echo '?')"
mkdir -p "$SRC_DIR"
export GIT_TERMINAL_PROMPT=0
if [[ -d "${SRC_DIR}/.git" ]]; then
git -C "$SRC_DIR" -c advice.detachedHead=false fetch --depth 1 origin tag "$tag" 2>>"$build_log" || true
if ! git -C "$SRC_DIR" -c advice.detachedHead=false checkout -f "$tag" >>"$build_log" 2>&1; then
rm -rf "$SRC_DIR"
git -c advice.detachedHead=false clone --depth 1 --branch "$tag" "$REPO" "$SRC_DIR" >>"$build_log" 2>&1
fi
else
rm -rf "$SRC_DIR"
git -c advice.detachedHead=false clone --depth 1 --branch "$tag" "$REPO" "$SRC_DIR" >>"$build_log" 2>&1
fi
cmake -S "$SRC_DIR" -B "${SRC_DIR}/build" \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_CUDA=ON \
-DCMAKE_CUDA_ARCHITECTURES="${arch}" \
-DLLAMA_BUILD_SERVER=ON \
-DLLAMA_BUILD_UI=OFF \
-DLLAMA_USE_PREBUILT_UI=OFF \
-DGGML_CCACHE=OFF \
>>"$build_log" 2>&1
# Background build + heartbeat (keeps SSH stream alive without 200 cmake lines).
cmake --build "${SRC_DIR}/build" -j"$(nproc)" --target llama-server \
>>"$build_log" 2>&1 &
pid=$!
while kill -0 "$pid" 2>/dev/null; do
pct="$(grep -oE '\[[[:space:]]*[0-9]+%\]' "$build_log" 2>/dev/null | tail -n1 || true)"
line="$(grep -E 'Building CUDA|Built target|Linking' "$build_log" 2>/dev/null | tail -n1 || true)"
if [[ -n "$pct" ]]; then
log "сборка CUDA ещё идёт… ${pct}${line:+ · ${line}}"
else
log "сборка CUDA ещё идёт… (cmake/nvcc, см. build.log)"
fi
sleep 30
done
if ! wait "$pid"; then
log "сборка упала — хвост ${build_log}:"
tail -n 40 "$build_log" >&2 || true
return 1
fi
local built="${SRC_DIR}/build/bin/llama-server"
if [[ ! -x "$built" ]]; then
log "сборка не дала ${built}"
return 1
fi
install -m 755 "$built" "$SERVER_BIN"
# CUDA build may need libs from build/bin
find "${SRC_DIR}/build/bin" -maxdepth 1 -type f \( -name '*.so' -o -name '*.so.*' \) -print0 2>/dev/null \
| while IFS= read -r -d '' so; do
install -m 755 "$so" "${BIN_DIR}/$(basename "$so")"
done
chown -R "${SWARM_USER}:${SWARM_USER}" "$BIN_DIR"
echo "cuda:${tag}:${arch}" >"$STAMP"
chown "${SWARM_USER}:${SWARM_USER}" "$STAMP"
log "CUDA binary → ${SERVER_BIN}"
return 0
}
normalize_tag() {
printf '%s' "$1" | tr -d '\r' | head -n1 | awk 'NF{print; exit}'
}
if [[ -x "$SERVER_BIN" ]]; then
log "llama-server уже есть: ${SERVER_BIN}"
else
if [[ -n "$LLAMACPP_ASSET_URL" ]]; then
log "скачиваю по LLAMACPP_ASSET_URL…"
install_from_archive_url "$LLAMACPP_ASSET_URL"
echo "asset-url" >"$STAMP"
chown "${SWARM_USER}:${SWARM_USER}" "$STAMP"
else
if [[ -z "$LLAMACPP_TAG" ]]; then
log "WARN: LLAMACPP_TAG не задан — latest (см. docs/llm.md)"
fi
tag="$(normalize_tag "$(resolve_release_tag)")"
if [[ -z "$tag" || "$tag" == *" "* || "$tag" == *"["* ]]; then
log "не удалось определить release tag (got: ${tag:-empty})"
exit 1
fi
installed=0
if want_cuda_build; then
log "backend: CUDA (nvcc есть или LLAMACPP_BACKEND/BUILD_CUDA) — сборка, Vulkan только если упадёт"
if build_cuda_from_source "$tag"; then
installed=1
else
log "CUDA-сборка не вышла — fallback на Linux prebuilt (Vulkan/CPU)"
if install_linux_release "$tag"; then
installed=1
fi
fi
else
log "backend: Linux prebuilt (нет nvcc / LLAMACPP_BACKEND=vulkan) — без compile"
if install_linux_release "$tag"; then
installed=1
else
log "prebuilt не вышел — крайний случай: CUDA из исходников"
if build_cuda_from_source "$tag"; then
installed=1
fi
fi
fi
if [[ "$installed" != "1" ]]; then
log "не удалось поставить llama-server"
exit 1
fi
fi
fi
if [[ ! -x "$SERVER_BIN" ]]; then
log "нет исполняемого ${SERVER_BIN}"
exit 1
fi
# Prefer a weights GGUF (skip mmproj), then attach --mmproj if present.
MODEL_ARG=""
MMPROJ_ARG=""
FIRST_GGUF="$(
find "$MODELS_DIR" -type f \( -name '*.gguf' -o -name '*.GGUF' \) \
! -iname '*mmproj*' 2>/dev/null | head -n1 || true
)"
MMPROJ_GGUF="$(
find "$MODELS_DIR" -type f \( -iname '*mmproj*.gguf' -o -iname '*mmproj*.GGUF' \) \
2>/dev/null | head -n1 || true
)"
if [[ -n "$FIRST_GGUF" ]]; then
MODEL_ARG="-m ${FIRST_GGUF}"
log "модель ${FIRST_GGUF}"
else
log "нет GGUF в ${MODELS_DIR} — положи файл вручную и systemctl restart ${UNIT}"
fi
if [[ -n "$MMPROJ_GGUF" ]]; then
MMPROJ_ARG="--mmproj ${MMPROJ_GGUF}"
log "mmproj ${MMPROJ_GGUF}"
fi
# GPU layers: share card with Swarm — full offload on mid+, leave headroom on low.
NGL=99
CTX=8192
if [[ -f "${DATA_ROOT}/.gpu-rent-gpu.json" ]]; then
eval "$(python3 - <<'PY'
import json
from pathlib import Path
gpu=json.loads(Path("/mnt/swarm_data/.gpu-rent-gpu.json").read_text())
vram=int(gpu.get("vram_mib") or 0)
gib=vram/1024.0
if gib < 16:
print("NGL=40"); print("CTX=4096")
elif gib < 24:
print("NGL=99"); print("CTX=8192")
elif gib < 48:
print("NGL=99"); print("CTX=16384")
else:
print("NGL=99"); print("CTX=32768")
PY
)" || true
fi
# Explicit overrides from gpu-rent.vars / .env (forwarded by provision).
if [[ -n "$LLAMACPP_NGL" ]]; then
NGL="$LLAMACPP_NGL"
fi
if [[ -n "$LLAMACPP_CTX" ]]; then
CTX="$LLAMACPP_CTX"
fi
log "llama.cpp -ngl ${NGL} -c ${CTX} host=${LLAMACPP_HOST} port=${LLAMACPP_PORT}${LLAMACPP_EXTRA_ARGS:+ extra=${LLAMACPP_EXTRA_ARGS}}"
cat >/etc/systemd/system/${UNIT}.service <<EOF
[Unit]
Description=gpu-rent llama.cpp server (loopback, GPU-tuned)
After=network-online.target local-fs.target
Wants=network-online.target
[Service]
Type=simple
User=${SWARM_USER}
Group=${SWARM_USER}
WorkingDirectory=${LLAMA_ROOT}
Environment=LD_LIBRARY_PATH=${BIN_DIR}
ExecStart=${SERVER_BIN} ${MODEL_ARG} ${MMPROJ_ARG} --host ${LLAMACPP_HOST} --port ${LLAMACPP_PORT} -ngl ${NGL} -c ${CTX} ${LLAMACPP_EXTRA_ARGS}
Restart=on-failure
RestartSec=8
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$UNIT"
systemctl restart "$UNIT" || log "unit стартовал с ошибкой (часто нет GGUF) — проверь journalctl -u ${UNIT}"
log "ok — http://127.0.0.1:8080 models=${MODELS_DIR}"