Руководство по сборке локального AI-стека. Часть 2. Запуск llama-cpp
На этом шаге мы соберём llama.cpp, скачаем компактную модель для теста, разберёмся с ключевыми опциями (mmap, лимиты памяти, mmproj) и запустим сервер. После этого у вас будет работающий чат по адресу http://127.0.0.1:8080 — с родным веб-интерфейсом llama.cpp (пока без Open WebUI).
Что нужно понять до начала
llama.cpp — это C++ движок для инференса LLM. Он умеет:
Работать на CPU (без GPU) — то, что нам нужно.
Загружать модель через
mmap— не грузить её целиком в RAM, а подгружать страницы по требованию. Это позволяет запускать модель больше доступной памяти.Ограничивать потребление памяти через
cgroup(черезsystemd-run).Работать в двух режимах: одна модель (
-m) или «роутер» моделей (--models-dir), когда в UI виден список всех.gguf, а в памяти держится одна.
Тестовая модель — Qwen3.5-2B-Q4_K_M (~1.5 ГБ), чтобы проверить пайплайн быстро, не дожидаясь загрузки 9–10 ГБ модели. Она же потом пригодится для «сверхбыстрых» задач (черновики, проверка идей).
Шаг 1. Системные зависимости
sudo apt update
sudo apt install -y \
cmake build-essential libopenblas-dev \
python3 python3-venv python3-pip \
git curl wget jq
libopenblas-dev — ускоряет матричные операции на CPU. build-essential — компилятор для сборки llama.cpp. python3-venv понадобится позже для Open WebUI, но поставим сразу.
Шаг 2. Сборка llama.cpp
Скрипт run_llama_mmap.sh собирает llama.cpp автоматически при первом запуске, если бинарника нет. Но полезно понимать, что происходит:
git clone --depth=1 https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_UI=ON
cmake --build build --config Release -j"$(nproc)"
-DLLAMA_BUILD_UI=ON— собирает родной веб-интерфейсllama.cpp(Svelte). Без него сервер отдаёт только API, а корень (/) пустой.-j$(nproc)— параллельная сборка на всех ядрах. На 4-ядерном CPU занимает 5–10 минут, на 8+ — 3–5 минут.Сборка требует Node.js для UI, но современные версии
llama.cppвстраивают UI как статику, так что Node.js не нужен. Если сборка ругнётся на UI —-DLLAMA_BUILD_UI=OFF, но тогда родного интерфейса не будет (только API, а чат вы будете смотреть через Open WebUI).
После сборки появляются бинарники в llama.cpp/build/bin/:
llama-server— HTTP-сервер с API и UI (то, что использует наш скрипт).llama-cli— CLI для одной генерации (для отладки).llama-quantize— утилита квантования (пригодится в части про конвертацию моделей).
Шаг 3. Скачивание тестовой модели
Модель — Qwen3.5-2B-Q4_K_M. Квантование Q4_K_M — «золотая середина»: файл ~1.5 ГБ, качество близко к F16.
Качаем через wget (без Python):
mkdir -p models
wget -c -O models/Qwen3.5-2B-Q4_K_M.gguf \
"https://huggingface.co/unsloth/Qwen3.5-2B-GGUF/resolve/main/Qwen3.5-2B-Q4\_K\_M.gguf"
-c — докачка при обрыве. Для больших файлов лучше wget -c, чем curl, потому что curl без -C - начинает заново.
Правила раскладки:
Обычные (текстовые) модели — можно плоско, прямо в
models/. Имя файла —<модель>-<квант>.gguf.Мультимодальные модели — всегда в отдельной подпапке, вместе с
mmproj-F16.gguf. Это требование режима роутера: он ищетmmprojтолько в той же папке, где лежит модель, и имя проектора должно начинаться сmmproj.Один
mmprojна несколько квантований (например,Q4_K_MиQ8_0одной модели, если одинаковые) — можно сделать симлинк:ln -s mmproj-F16.gguf mmproj-Q8_0.ggufНо проще — один
mmprojна папку, а в папке одно квантование.
Почему mmproj — это отдельный файл, а не часть модели. mmproj (multimodal projector) — это «переходник» между визуальным энкодером и языковой моделью. В llama.cpp он подключается отдельным флагом --mmproj. Без него VL-модель работает как обычная текстовая (картинки не понимает), с ним — «видит». Подробнее — в шаге 5 ниже.
Шаг 4. Ключевые опции llama-server
Скрипт run_llama_mmap.sh — это обёртка над llama-server, которая добавляет умные дефолты. Разберём важные флаги, которые он использует.
--load-mode mmap — главная опция
--load-mode mmap
Заставляет llama.cpp использовать mmap для загрузки весов. Что это даёт:
Модель не грузится в RAM целиком. Ядро отображает файл в адресное пространство процесса и подгружает страницы по требованию. Когда модель обращается к слою, страница подгружается с диска; когда слой больше не нужен — ядро может её вытеснить.
Позволяет запускать модель больше RAM. Например, 20-гиговая модель на 10 ГБ RAM. Ядро держит в памяти «горячие» страницы, остальные — на диске.
Нет износа SSD. В отличие от swap,
mmapне пишет страницы обратно — данные уже на диске, их не нужно сохранять.
Альтернативы:
--load-mode none— грузит всю модель в RAM. Упадёт сOOM, если модель больше памяти.--load-mode mlock— грузит и блокирует в RAM (запрещает вытеснять). Быстро, но только если модель помещается.--load-mode dio— DirectIO, чтение в обход page cache. В некоторых версияхllama.cppбагован (создаёт временные буферы размером с модель). Не используйте без проверки.
Что происходит на практике. Первый запрос к модели — самый медленный: ядро подгружает нужные страницы с диска. На HDD это 5–10 минут для 20-гиговой модели. Последующие запросы быстрее — часть страниц уже в page cache. Чем больше RAM свободно, тем больше «горячих» страниц, тем быстрее.
--no-repack — экономия памяти на CPU
--no-repack
llama.cpp при загрузке может создавать вторую копию весов, переупакованную для более быстрых вычислений (CPU_REPACK). Это почти удваивает пиковое потребление RAM. Для модели, которая и так не влезает, — фатально (был реальный случай: CPU model buffer 9.4 ГБ + CPU_REPACK 11.2 ГБ = 20.6 ГБ при лимите 10 ГБ -> OOM). --no-repack отключает переупаковку. Цена — ~10–15% скорости вычислений. Для нашего сценария «запустить хоть как-то» — правильный компромисс.
-ctk q8_0 -ctv q8_0 — квантизация KV-cache
--cache-type-k q8_0 --cache-type-v q8_0
KV-cache — это память под контекст (история диалога). По умолчанию он в f16, что для длинного контекста съедает гигабайты. q8_0 сжимает его в 2 раза (~50% экономии) с минимальной потерей качества. Для моделей, которые «еле влезают» в лимит, это критично.
--reasoning on | off | auto — «размышления» Qwen3.x
--reasoning auto
--reasoning-budget 768
Новые Qwen-модели (3.x, 3.5) поддерживают режим «размышлений»: перед ответом модель генерирует цепочку рассуждений (reasoning_content), потом — финальный ответ. Это улучшает качество на сложных задачах (математика, RAG, поэзия), но:
На тривиальных вопросах («Привет») модель может «думать» 400+ токенов, что на CPU занимает минуты. Отсюда
--reasoning offдля интерактивного чата.--reasoning-budget N— жёсткий лимит токенов на размышления. ПослеNтокенов движок принудительно закрывает блок размышлений и заставляет модель отвечать. Без него reasoning может «съесть» весьctx-sizeи на ответ не останется места (реальный случай: модель «думала правильно», но ответа так и не дала — упёрлась в контекст).
Рекомендация: auto (по умолчанию в скрипте) с --reasoning-budget 768. Для быстрых ответов — -rn (off).
--ctx-size — размер контекста
--ctx-size 3096
Сколько токенов модель «помнит» (промпт + история + reasoning + ответ). Больше — лучше для длинных диалогов и RAG, но пропорционально больше памяти под KV-cache. Подбирайте под задачу: для «Привет/Как дела» хватит 2048, для саммари статьи — 4096+.
--threads — число потоков CPU
--threads $(( $(nproc) - 2 ))
Скрипт берёт nproc - 2: оставляет 2 ядра системе, чтобы она не подвисала, пока модель считает. Для 4-ядерного CPU — 2 потока, для 8-ядерного — 6. Больше потоков ≠ быстрее: на гипертрединге может быть медленнее из-за конкуренции за кэш.
--models-dir и --models-max 1 — режим роутера
--models-dir ./models --models-max 1
Вместо -m <модель> скрипт по умолчанию запускает роутер: в UI виден дропдаун со всеми .gguf из models/, можно переключаться без перезапуска сервера. --models-max 1 гарантирует, что в памяти держится одна модель: при переключении старая выгружается, новая грузится. Для лимита 10 ГБ — обязательно, иначе роутер попытается держать несколько моделей и получит OOM.
Альтернатива — режим одной модели: -m <имя> (или -s для интерактивного выбора). Тогда сервер грузит только эту модель, без роутера. Чуть быстрее стартует, чуть меньше памяти накладных расходов. Удобно, когда модель уже выбрана.
Лимиты памяти через systemd-run
systemd-run --user --unit=llama-server-$$ \ -p MemoryMax=10G \ -p MemoryHigh=9G \ -p MemorySwapMax=0 \ -- llama-server ...
Скрипт оборачивает запуск в cgroup с лимитами:
MemoryMax=10G— жёсткий лимит. При превышенииcgroupубивает процесс (OOM-kill). Система при этом остаётся жива.MemoryHigh=9G— мягкий лимит. При приближении к нему ядро начинает агрессивно вытеснять страницыmmapобратно на диск (не дожидаясьOOM).MemorySwapMax=0— запрет swap. Без этого ядро могло бы сбрасывать анонимную память (KV-cache, буферы) в swap, что дляmmap-модели — катастрофа по I/O.
Значения MEM_MAX/MEM_HIGH можно переопределить через env: MEM_MAX=14G ./run_llama_mmap.sh.
Шаг 5. mmproj — что это и как раскладывать
mmproj (multimodal projector) — это небольшой файл (~300–600 МБ), который «переводит» выходы визуального энкодера (ViT) в пространство, понятное языковой модели. Без него VL-модель работает как обычная текстовая; с ним — «видит» картинки.
Требования:
Имя файла начинается с
mmproj(например,mmproj-F16.gguf,mmproj-Qwen3VL-4B-F16.gguf).Лежит в той же папке, что и модель — особенно в режиме роутера, где
llama.cppищет проектор по папке модели.
Проверка, что модель «видит» картинки. После запуска сервера в логе должно появиться:
clip_model_load: loaded meta data ...
clip_model_load: model size = ...
Если этих строк нет — mmproj не подхватился, и при попытке отправить картинку сервер скажет Images require a vision-capable model.
Что делать, если mmproj нет для вашей модели:
Не страшно, если vision не нужен — модель работает как текстовая.
Если vision нужен, но
mmprojнет — ищите его в том же репозитории на Hugging Face (обычноmmproj-*.ggufлежит рядом с.gguf-моделью).Не путайте
mmprojс LLM-энкодером для генерации изображений (там--llmвsd.cpp— это текстовый энкодер, не то же самое).
Код запуска run_llama_mmap.sh
#!/usr/bin/env bash
# Запуск llama-server с mmap для модели, превышающей RAM.
# Usage: ./run_llama_mmap.sh
#
# Переопределяемые env: MEM_MAX, MEM_HIGH, PORT, HOST, CTX_SIZE
# MEM_MAX=10G — жёсткий лимит (OOM-kill при превышении)
# MEM_HIGH=9G — мягкий лимит (начинается активное вытеснение)
# CTX_SIZE=2048 — маленький контекст экономит KV-cache
#
# Мониторинг в другом терминале:
# watch -n1 'free -h; echo; cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/*/memory.pressure 2>/dev/null'
set -euo pipefail
# --- Конфигурация ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LLAMA_DIR="$SCRIPT_DIR/llama.cpp"
MODELS_DIR="$SCRIPT_DIR/models"
# --- API-ключ (аутентификация) ---
# По умолчанию llama-server работает БЕЗ аутентификации.
# Чтобы включить защиту, задайте ключ:
# - вручную: API_KEY="ваш-секретный-ключ"
# - или оставьте пустым: API_KEY=""
#
# Если API_KEY пуст — флаг --api-key НЕ добавляется, сервер открыт.
# Если API_KEY задан — флаг --api-key добавляется, и тогда:
# 1. Все API-запросы требуют заголовок Authorization: Bearer <ключ>.
# 2. Web UI загружается свободно (страница чата открывается),
# но для отправки сообщений нужно вписать ключ в настройках:
# Settings (иконка шестерёнки) -> General -> поле "API Key".
# --- Чтение секретов ---
SECRETS_FILE="$SCRIPT_DIR/.secrets"
if [ -f "$SECRETS_FILE" ]; then
# shellcheck disable=SC1090
set -a; source "$SECRETS_FILE"; set +a
fi
LLAMA_API_KEY="${LLAMA_API_KEY:-}"
LOAD_MODE="mmap"
PORT="${PORT:-8080}"
HOST="${HOST:-0.0.0.0}"
CTX_SIZE="${CTX_SIZE:-3096}"
REASONING_BUDGET="${REASONING_BUDGET:-768}" # по умолчанию 768 токенов на размышления
MEM_MAX="${MEM_MAX:-10G}"
MEM_HIGH="${MEM_HIGH:-9G}"
NPROC="$(nproc)"
THREADS=$(( NPROC - 2 ))
[ "$THREADS" -lt 2 ] && THREADS=2
# --- Разбор аргументов скрипта ---
SINGLE_MODEL=0
REASONING="${REASONING:-auto}"
while [ $# -gt 0 ]; do
case "$1" in
-s|--single) SINGLE_MODEL=1 ;;
-m|--model) MODEL_NAME_ARG="$2"; SINGLE_MODEL=1; shift 2 ;;
-r|--reasoning) REASONING="on" ;;
-rn|--reasoning-off) REASONING="off" ;;
-h|--help)
cat <<EOF
Usage: $(basename "$0") [-s|--single] [-r|--reasoning] [-rn|--reasoning-off]
Без флагов router mode — все .gguf из ./models видны в UI,
переключение через дропдаун,
в памяти одна модель (--models-max 1),
режим размышлений auto.
-s, --single выбор одной модели из консоли и загрузка сервера только с этой моделью.
-r, --reasoning включить режим размышлений
-rn, --reasoning-off выключить режим размышлений
EOF
exit 0
;;
*) echo "Неизвестный флаг: $1 (см. --help)"; exit 1 ;;
esac
shift
done
# --- 1. Сборка llama.cpp при первом запуске ---
LLAMA_SERVER="$LLAMA_DIR/build/bin/llama-server"
if [ ! -x "$LLAMA_SERVER" ]; then
echo "[*] Собираю llama.cpp (первый запуск, может занять несколько минут)..."
if [ ! -d "$LLAMA_DIR/.git" ]; then
git clone --depth=1 https://github.com/ggerganov/llama.cpp.git "$LLAMA_DIR"
fi
cmake -S "$LLAMA_DIR" -B "$LLAMA_DIR/build" -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_UI=ON
cmake --build "$LLAMA_DIR/build" --config Release -j "$NPROC"
fi
[ -x "$LLAMA_SERVER" ] || { echo "[!] Сборка не удалась: $LLAMA_SERVER не найден"; exit 1; }
# --- 2. Модели ---
mkdir -p "$MODELS_DIR"
# Папка с медиа-моделями (исключаем её из списка текстовых)
MEDIA_MODELS_DIR="$MODELS_DIR/media"
mapfile -t MODELS < <(
find "$MODELS_DIR" -maxdepth 2 -type f -name "*.gguf" \
! -name "*mmproj*" \
! -path "$MEDIA_MODELS_DIR/*" \
| sort
)
if [ ${#MODELS[@]} -eq 0 ]; then
echo "[!] В $MODELS_DIR нет .gguf файлов."; exit 1
fi
if [ "$SINGLE_MODEL" -eq 1 ]; then
# --- Режим одной модели
if [ -n "${MODEL_NAME_ARG:-}" ]; then
# Нормализация: добавить .gguf, если нет
[[ "$MODEL_NAME_ARG" != *.gguf ]] && MODEL_NAME_ARG="${MODEL_NAME_ARG}.gguf"
MODEL_PATH=""
for m in "${MODELS[@]}"; do
if [ "$(basename "$m")" = "$MODEL_NAME_ARG" ]; then
MODEL_PATH="$m"
break
fi
done
[ -n "$MODEL_PATH" ] || {
echo "[!] Модель '$MODEL_NAME_ARG' не найдена. Доступные:"
for m in "${MODELS[@]}"; do echo " $(basename "$m")"; done
exit 1
}
else
if [ ${#MODELS[@]} -eq 1 ]; then
MODEL_PATH="${MODELS[0]}"
else
echo "[*] Найдено несколько моделей:"
select MODEL_PATH in "${MODELS[@]}"; do
[ -n "${MODEL_PATH:-}" ] && break
done
fi
fi
echo "[*] Режим одной модели: $MODEL_PATH ($(du -h "$MODEL_PATH" | cut -f1))"
else
# --- Режим роутера моделей
echo "[*] Router mode: каталог $MODELS_DIR (${#MODELS[@]} шт.), --models-max 1"
fi
# --- 3. Проверка занятости порта ---
for h in 127.0.0.1 0.0.0.0; do
if (echo >/dev/tcp/$h/$PORT) 2>/dev/null; then
echo "[!] Порт $PORT уже занят ($h). Остановите другой процесс или смените PORT."
exit 1
fi
done
# --- 4. Определение доступных флагов (на случай старых/новых версий) ---
HELP_OUT="$("$LLAMA_SERVER" --help 2>&1 || true)"
OPT_FLAGS=()
echo "$HELP_OUT" | grep -q -- "--no-warmup" && OPT_FLAGS+=("--no-warmup")
echo "$HELP_OUT" | grep -q -- "--flash-attn" && OPT_FLAGS+=("-fa" "on")
echo "$HELP_OUT" | grep -q -- "--no-repack" && OPT_FLAGS+=("--no-repack")
# Отключение reasoning для Qwen3.x (если поддерживается)
REASON_ARGS=()
if echo "$HELP_OUT" | grep -q -- "--reasoning "; then
case "$REASONING" in
on) REASON_ARGS+=(--reasoning on) ;;
auto) REASON_ARGS+=(--reasoning auto) ;;
off) REASON_ARGS+=(--reasoning off) ;;
esac
# Бюджет: 0 для off, REASONING_BUDGET для on/auto
if echo "$HELP_OUT" | grep -q -- "--reasoning-budget"; then
if [ "$REASONING" = "off" ]; then
REASON_ARGS+=(--reasoning-budget 0)
else
REASON_ARGS+=(--reasoning-budget "$REASONING_BUDGET")
fi
fi
fi
# KV-cache квантизация (экономит ~50% памяти под контекст)
KV_ARGS=()
if echo "$HELP_OUT" | grep -q -- "--cache-type-k"; then
KV_ARGS+=("-ctk" "q8_0" "-ctv" "q8_0")
fi
echo "[*] Load mode: $LOAD_MODE"
[ ${#OPT_FLAGS[@]} -gt 0 ] && echo "[*] Флаги: ${OPT_FLAGS[*]}"
[ ${#KV_ARGS[@]} -gt 0 ] && echo "[*] KV-квантизация: ${KV_ARGS[*]}"
[ ${#REASON_ARGS[@]} -gt 0 ] && echo "[*] Reasoning: ${REASON_ARGS[*]}"
# Ищет mmproj-файл, соответствующий модели
# только при запуске одной модели, роутер находит автоматически.
# $1 — полный путь к .gguf файлу модели.
# Возвращает путь к mmproj через echo или пустую строку, если не найден.
find_mmproj_for_model() {
local model_path="$1"
local model_dir model_file model_base short_name
model_dir="$(dirname "$model_path")"
model_file="$(basename "$model_path")"
# Если это сам файл mmproj — не ищем для него проектор
if [[ "$model_file" == *mmproj* ]]; then
return 1
fi
model_base="${model_file%.gguf}"
short_name="$(echo "$model_base" | sed -E 's/[-_](Q[0-9]+(_[A-Z0-9]+)*|F16|F32|BF16|UD-Q[0-9]+(_[A-Z0-9]+)*)$//')"
# Приоритет 1: точное совпадение "mmproj-<short_name>.gguf"
local candidate="$model_dir/mmproj-${short_name}.gguf"
if [ -f "$candidate" ]; then
echo "$candidate"
return 0
fi
# Приоритет 2: любой mmproj в папке, имя которого содержит short_name
# (плоская структура: mmproj-<имя-модели>.gguf рядом с моделью)
for f in "$model_dir"/*mmproj*.gguf "$model_dir"/*.mmproj*.gguf; do
[ -f "$f" ] || continue
if [[ "$(basename "$f")" == *"$short_name"* ]]; then
echo "$f"
return 0
fi
done
# Приоритет 3: любой mmproj в папке модели
# (структура роутера: model.gguf + mmproj-F16.gguf в одной подпапке)
for f in "$model_dir"/*mmproj*.gguf; do
[ -f "$f" ] && { echo "$f"; return 0; }
done
echo ""
return 1
}
# --- 5. Сборка команды ---
SERVER_CMD=(
"$LLAMA_SERVER"
--host "$HOST"
--port "$PORT"
--threads "$THREADS"
--ctx-size "$CTX_SIZE"
--load-mode "$LOAD_MODE"
-lv 4
"${OPT_FLAGS[@]}"
"${KV_ARGS[@]}"
"${REASON_ARGS[@]}"
)
if [ "$SINGLE_MODEL" -eq 1 ]; then
# Одна модель, заданная явно
SERVER_CMD+=(-m "$MODEL_PATH")
# ищем mmproj для мультимодальных моделей
MMPROJ_PATH="$(find_mmproj_for_model "$MODEL_PATH" || true)"
if [ -n "$MMPROJ_PATH" ]; then
echo "[*] Найден mmproj: $(basename "$MMPROJ_PATH")"
else
# Не ошибка: модель может быть текстовой
echo "[*] mmproj не найден (модель будет работать как текстовая)"
fi
[ -n "$MMPROJ_PATH" ] && SERVER_CMD+=(--mmproj "$MMPROJ_PATH")
else
# Router mode — модель в памяти одна, но можно переключаться в UI
SERVER_CMD+=(--models-dir "$MODELS_DIR" --models-max 1)
fi
if [ -n "$LLAMA_API_KEY" ]; then
SERVER_CMD+=(--api-key "$LLAMA_API_KEY")
echo "[*] Аутентификация включена (API-ключ задан)"
else
echo "[*] Аутентификация отключена (ключ не задан)"
fi
# --- 6. Подготовка фона и trap ---
LOG_FILE="$(mktemp -t llama-server.XXXXXX.log)"
SERVER_PID=""
TAIL_PID=""
UNIT_NAME="llama-server-$$"
USE_SYSTEMD=0
cleanup() {
[ -t 1 ] && echo ""
[ -t 1 ] && echo "[*] Завершение..."
if [ "$USE_SYSTEMD" -eq 1 ]; then
systemctl --user stop "$UNIT_NAME" 2>/dev/null || true
fi
if [ -n "$SERVER_PID" ]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null || true
rm -f "$LOG_FILE"
[ -t 1 ] && echo "[*] Готово."
}
trap cleanup EXIT INT TERM
# --- 7. Запуск с лимитом памяти через systemd-run ---
if command -v systemd-run &>/dev/null; then
USE_SYSTEMD=1
echo "[*] Запуск под systemd-run: MemoryMax=$MEM_MAX, MemoryHigh=$MEM_HIGH, swap=off"
if ! systemd-run --user --unit="$UNIT_NAME" \
-p MemoryMax="$MEM_MAX" \
-p MemoryHigh="$MEM_HIGH" \
-p MemorySwapMax=0 \
-p StandardOutput=file:"$LOG_FILE" \
-p StandardError=file:"$LOG_FILE" \
-- "${SERVER_CMD[@]}" >/dev/null 2>&1; then
echo "[!] systemd-run недоступен, откатываюсь на запуск без лимита."
USE_SYSTEMD=0
fi
fi
if [ "$USE_SYSTEMD" -eq 0 ]; then
echo "[*] Запуск в фоне без лимита памяти"
"${SERVER_CMD[@]}" >"$LOG_FILE" 2>&1 &
SERVER_PID=$!
fi
# Пробрасываем лог сервера в терминал в реальном времени
# если скрипт привязан к терминалу
[ -t 1 ] && tail -n +1 -f "$LOG_FILE" &
TAIL_PID=$!
# --- 8. Ожидание готовности ---
echo "[*] Жду готовности сервера (до 600 с)..."
READY=0
for _ in $(seq 1 600); do
if [ "$USE_SYSTEMD" -eq 1 ]; then
systemctl --user is-active "$UNIT_NAME" &>/dev/null || {
echo "[!] Сервер умер на старте. Последние строки лога:"
tail -n 40 "$LOG_FILE"; exit 1;
}
else
kill -0 "$SERVER_PID" 2>/dev/null || {
echo "[!] Сервер умер на старте. Последние строки лога:"
tail -n 40 "$LOG_FILE"; exit 1;
}
fi
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
READY=1; break
fi
sleep 1
done
[ "$READY" -eq 1 ] || { echo "[!] Сервер не поднялся за 120 с."; exit 1; }
# --- 9. Плашка для пользователя ---
echo ""
echo "============================================================"
echo " llama.cpp server is UP"
echo "------------------------------------------------------------"
echo " Web UI: http://127.0.0.1:$PORT"
echo " OpenAI API: http://127.0.0.1:$PORT/v1"
echo " Шпаргалка: file://$SCRIPT_DIR/web/models.html"
if [ "$SINGLE_MODEL" -eq 1 ]; then
echo " Режим: одна модель ($(basename "$MODEL_PATH"))"
else
echo " Режим: router (модели из ./models, max 1 в RAM)"
fi
echo " Threads: $THREADS (из $NPROC)"
echo " Ctx size: $CTX_SIZE"
if [ "$USE_SYSTEMD" -eq 1 ]; then
echo " Mem cap: $MEM_MAX (systemd-run, swap=off)"
else
echo " Mem cap: БЕЗ ЛИМИТА (systemd-run недоступен)"
fi
echo "------------------------------------------------------------"
echo " Ctrl+C — остановить."
echo "============================================================"
echo ""
# --- 10. Ожидание и мониторинг падений ---
while true; do
if [ "$USE_SYSTEMD" -eq 1 ]; then
systemctl --user is-active "$UNIT_NAME" &>/dev/null || { echo "[!] Сервер остановлен."; break; }
else
kill -0 "$SERVER_PID" 2>/dev/null || { echo "[!] Сервер остановлен."; break; }
fi
sleep 10
done
Проверка после запуска
1. Сервер поднялся:
./run_llama_mmap.sh -m Qwen3.5-2B-Q4_K_M
В терминале должна появиться плашка:
============================================================
llama.cpp server is UP
------------------------------------------------------------
Web UI: http://127.0.0.1:8080
OpenAI API: http://127.0.0.1:8080/v1
...2. Родной UI открывается:
Откройте
http://127.0.0.1:8080в браузере.Выберите модель в дропдауне (router mode) или она уже выбрана (single mode).
Отправьте «Привет» — должен прийти ответ.
3. API отвечает:
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Привет"}],"stream":false}'4. Проверить память:
# RSS процесса (сколько реально в RAM)
ps -o pid,comm,rss,%mem -C llama-server
# cgroup: сколько занято и какое давление
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/llama-server-*.service/memory.current
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/llama-server-*.service/memory.pressure
Если memory.current близко к MEM_MAX, а memory.pressure растёт — модель активно «подгружает» страницы с диска. Это нормально для mmap, просто медленно.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.