Руководство по сборке локального 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, просто медленно.
Комментарии (5)

SaemonZixel
26.10.2026 11:34Она же потом пригодится для «сверхбыстрых» задач (черновики, проверка идей).
Не хотите попробовать Gemma4-E2B? Скорость та-же, но она поумнее будет т.к. полный размер 4.6B. У неё есть хороший drafter. И mmproj тоже есть.

AntonovPsy Автор
26.10.2026 11:34Она тоже есть. Gemma в двух вариантах: E2B, E4B. Вполне хорошие модели. Не скажу, что пробовал все их возможности... До чего хватило любопытства, фантазии и желания - попробовал. В целом у каждой свои плюсы и минусы (субъективно). В разделе "Наполнение моделями" есть описания тех что опробовал (субъективные впечатления на разных задачах).
vovkats
Возник вопрос по применению, если ответ модели занимает 3–15 минут, насколько такой стек пригоден для интерактивного чата, особенно если им будут пользоваться несколько человек по сети?
Также, хотелось бы посмотреть реальные замеры производительности при запуске модели, которая значительно превышает доступную оперативку. Насколько я понимаю, даже с отключённым swap страницы весов модели могут постоянно вытесняться из page cache и повторно читаться с диска. Возникает вопрос, сильно ли это влияет на скорость генерации?
AntonovPsy Автор
На несколько пользователей (пробовал на 3) - хватает. На целый коллектив - не могу сказать. Делал для дома.
На счет запуска. С ограничением в 10 Гб (ОЗУ на ПК 16 Гб) стартовал модель Qwen3.5-35B (если верно помню 22 Гб). Работала без проблем с памятью и укладывалась в лимит. Оставлять ее не стал так как генерит медленно (CPU поскрипывает).
На счет скорости сложно сказать. Мои субъективные ощущения могу только озвучить, на моем Core i3 работает хорошо: 9B-12B модели пишут стримом, что удобно читать. Мелкие модели до 4B почти без задержек. Но точно не замерял.