Руководство по сборке локального AI‑стека
Ссылки на все части:
Часть 1. Общий план
Часть 2. Запуск llama‑cpp
Часть 3. Наполнение моделями
Часть 4. Запуск сервера генерации изображений
Часть 5. Open WebUI поверх llama.cpp
Часть 6. Конвертация моделей Hugging Face в GGUF
Часть 7. Финал. Офлайн‑развёртывание
Часть 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}'
Если вы запустили сервер в router mode (без -m, все модели в дропдауне), поле "model" обязательно.
Узнать правильный id:
curl -s http://127.0.0.1:8080/v1/models | jq -r '.data[].id'
Скопируйте id из ответа и подставьте в curl:
curl http://127.0.0.1:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{{"model": "...", "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, просто медленно.


