Руководство по сборке локального AI-стека. Часть 2. Запуск llama-cpp

—

от автора

Руководство по сборке локального 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 updatesudo 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.gitcd llama.cppcmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_UI=ONcmake --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 modelswget -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 - начинает заново.

Правила раскладки:

  1. Обычные (текстовые) модели — можно плоско, прямо в models/. Имя файла — <модель>-<квант>.gguf.

  2. Мультимодальные модели — всегда в отдельной подпапке, вместе с mmproj-F16.gguf. Это требование режима роутера: он ищет mmproj только в той же папке, где лежит модель, и имя проектора должно начинаться с mmproj.

  3. Один 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-модель работает как обычная текстовая; с ним — «видит» картинки.

Требования:

  1. Имя файла начинается с mmproj (например, mmproj-F16.gguf, mmproj-Qwen3VL-4B-F16.gguf).

  2. Лежит в той же папке, что и модель — особенно в режиме роутера, где 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 +afiLLAMA_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=0REASONING="${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 <<EOFUsage: $(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    shiftdone# --- 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 1fiif [ "$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    fidone# --- 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    fifi# KV-cache квантизация (экономит ~50% памяти под контекст)KV_ARGS=()if echo "$HELP_OUT" | grep -q -- "--cache-type-k"; then    KV_ARGS+=("-ctk" "q8_0" "-ctv" "q8_0")fiecho "[*] 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)fiif [ -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=0cleanup() {    [ -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    fifiif [ "$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=0for _ 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 1done[ "$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)"fiecho "  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 недоступен)"fiecho "------------------------------------------------------------"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 10done

Проверка после запуска

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.currentcat /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, просто медленно.

ссылка на оригинал статьи https://habr.com/ru/articles/1099482/