PunchNigeria Ebola-free after patient’s evacuation to Switzerland — WHOESPNSuffocating defense, JSN lift Seahawks to victoryESPN DeportesCuándo se entrega el Balón de Oro 2026: nominados, fecha, sede y todos los detallesBollywood HungamaJohn Abraham joins Indian Superbike League as Strategic Advisor: "It’s time to get India racing"The Jerusalem PostDamascus struggles to assert control in southern Syria amid ISIS threat, Druze tensions - analysisInquirerMarcos admin urged to adopt industrial policy, alternative miningNotJustOkPretty Nun Lyrics by ChellaZDF heuteEntdecken Sie das ZDF-NachrichtenstudioPopular ScienceCelebrate the flavors of fall with three spicy recipesХабрСвой тренажёр печати для детей: три ошибки и одна догадка, которая сработалаRTL BoulevardSabina uit B&B Vol Liefde boekt succes met erotische content: 'Tot nu toe loopt het heel erg goed'Screen RantLegend Of Zelda: Ocarina Of Time Remake Gets Official Last-Minute Update
The Daily Newsstand · Free, Always
Monday, October 26, 2026

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

Translate

На этом шаге мы соберём 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 - начинает заново.

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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.