Руководство по сборке локального AI‑стека
Ссылки на все части:
Часть 1. Общий план
Часть 2. Запуск llama‑cpp
Часть 3. Наполнение моделями
Часть 4. Запуск сервера генерации изображений
Часть 5. Open WebUI поверх llama.cpp
Часть 6. Конвертация моделей Hugging Face в GGUF
Часть 7. Финал. Офлайн‑развёртывание
Конвертация моделей Hugging Face → GGUF
Не все модели существуют в готовом .gguf‑формате. Иногда нужна модель, которую сообщество ещё не квантовало, или хочется получить конкретный квант (Q4_K_M, Q5_K_M, Q8_0), которого нет в готовых сборках. В таких случаях её нужно сконвертировать самостоятельно: из «сырых» весов Hugging Face (.safetensors) в GGUF, а затем — в нужный квант.
Скрипт convert_to_gguf.sh автоматизирует весь процесс: от установки зависимостей до финального файла .gguf, с очисткой промежуточных данных.
Что нужно понять до начала
Конвертация — это всегда Python‑скрипт. В отличие от квантования (llama-quantize — C++ бинарник), конвертер convert_hf_to_gguf.py требует transformers, safetensors, sentencepiece и torch (CPU‑версию). Полностью отказаться от Python не получится — но можно изолировать его в venv, чтобы не засорять систему.
Процесс состоит из двух шагов:
Конвертация
.safetensors→.gguf(F16). Это «перевод» весов из формата Hugging Face в форматllama.cpp. Размер файла примерно равен сумме.safetensors(может быть чуть больше из‑за метаданных).Квантование F16 → Q4_K_M. Это «сжатие» с потерей точности. Размер уменьшается в 3–4 раза, качество падает минимально.
Зачем нужен промежуточный F16. llama-quantize работает только с GGUF — он не умеет читать .safetensors. Поэтому F16-файл — обязательный промежуточный шаг. После квантования его можно удалить (если не нужны другие кванты из того же F16).
Что делает скрипт
Скрипт convert_to_gguf.sh — это «одна команда, весь пайплайн»:
Проверяет системные пакеты (
cmake,build-essential,python3-venv,git,curl,jq) черезdpkg -s. Ставит только если отсутствуют.Собирает
llama.cpp(еслиllama-quantizeиllama-cliещё нет).Создаёт
venv(если нет) и ставит зависимости изllama.cpp/requirements/requirements-convert_hf_to_gguf.txt. Это официальный файл отllama.cpp, который тянет CPU‑версиюtorch(через--extra-index-urlhttps://download.pytorch.org/whl/cpu) — никаких CUDA‑драйверов.Скачивает модель через
hf download --local-dir hf_model.Sanity check: проверяет, что
.safetensors— реальные веса, а не LFS‑указатели (маленькие текстовые файлы). Послеhf downloadэто всегда так, но проверка дешёвая.Конвертирует в
models/<модель>-f16.ggufчерезconvert_hf_to_gguf.py.Квантует в
models/<модель>-Q4_K_M.ggufчерезllama-quantize.Проверяет размер Q4: если файл меньше 100 МБ — явно битый, выходит с ошибкой.
Очищает
hf_model/и промежуточный F16 после успешной проверки.
Всё, что попало в CLEANUP_PATHS, удаляется при выходе (успех / ошибка / Ctrl+C) через trap cleanup EXIT INT TERM. Это гарантирует, что при сбое не останутся «полу‑скачанные» гигабайты. Флаг KEEP_INTERMEDIATE=1 отключает очистку — для отладки.
Ключевые моменты
CPU‑версия torch — критично
В requirements-convert_hf_to_gguf.txt уже указан --extra-index-url https://download.pytorch.org/whl/cpu и torch==2.11.0. Это гарантирует, что:
Не притянутся
nvidia-*пакеты (~2 ГБ).Не потребуются CUDA‑драйверы.
Конвертация пойдёт на CPU (что для
convert_hf_to_gguf.py— норма, он не использует GPU).
Если ставить torch вручную без --index-url, pip возьмёт CUDA‑версию с PyPI. Это не сломает конвертацию (CPU‑путь всё равно используется), но займёт лишние 2 ГБ и может потребовать libcuda.so (которого нет).
hf download vs wget vs git clone
Скрипт использует hf download — официальный CLI от Hugging Face. Он:
Не требует
git-lfs(качает через HTTPS).Поддерживает gated‑модели (с токеном).
Умеет выбирать конкретные файлы (если нужно только часть репозитория).
Альтернативы:
wget/curl— только для одного файла по прямой ссылке. Для полного репозитория (сconfig.json,tokenizer.json, несколькими.safetensors‑шардами) — неудобно.git cloneсgit-lfs— если хочется именно git‑историю. Но для конвертации это избыточно:git-lfsнужно ставить, а.safetensorsвсё равно скачаются как «большие файлы».
Sanity check: LFS‑указатели
hf download качает реальные веса, но проверка на LFS‑указатели — дешёвая страховка:
if grep -ql "git-lfs.github.com/spec" "$HF_DIR"/*.safetensors 2>/dev/null; then echo "[!] В $HF_DIR обнаружены LFS-указатели вместо весов." >&2 exit 1 fi
Что это ловит. LFS‑указатели — это маленькие (130 байт) текстовые файлы, которые git создаёт вместо реальных весов, если git-lfs не установлен. convert_hf_to_gguf.py на них упадёт с невнятной ошибкой («cannot load safetensors»), а проверка даёт понятное сообщение.
Проверка размера Q4
Q4_SIZE=$(stat -c%s "$Q4_GGUF") if [ "$Q4_SIZE" -lt $((100 * 1024 * 1024)) ]; then echo "[!] Подозрительно маленький файл: $(du -h "$Q4_GGUF" | cut -f1)" >&2 exit 1 fi
Порог 100 МБ — с большим запасом. Даже крошечная модель 1.5B в Q4_K_M весит ~1 ГБ. Если файл меньше — что‑то пошло не так на этапе квантования (например, llama-quantize упал, но создал пустой файл). Это дешёвая проверка, которая ловит 99% проблем.
Очистка: CLEANUP_PATHS + trap
CLEANUP_PATHS=() # ... CLEANUP_PATHS+=("$HF_DIR") CLEANUP_PATHS+=("$F16_GGUF") cleanup() { local ec=$? trap - EXIT INT TERM set +e # ... for p in "${CLEANUP_PATHS[@]}"; do [ -e "$p" ] && rm -rf "$p" done exit "$ec" } trap cleanup EXIT INT TERM
Почему это важно. hf_model для большой модели — это 10–20 ГБ .safetensors + F16 GGUF ещё столько же. Если скрипт упадёт на середине (например, OOM при конвертации), без очистки останется 20–40 ГБ «мусора». trap гарантирует, что при любом исходе (успех, ошибка, Ctrl+C) промежуточные файлы удаляются.
Q4_K_M в CLEANUP_PATHS не добавляется — это итоговый продукт, его нужно сохранить.
KEEP_INTERMEDIATE=1 — для отладки. Если конвертация падает, полезно посмотреть, что в hf_model и -f16.gguf:
KEEP_INTERMEDIATE=1 ./convert_to_gguf.sh Qwen/Qwen3.5-2B
Архитектуры, которые «не идут»
Не все модели конвертируются «из коробки». Реальные случаи:
KDA / Lissa —
convert_hf_to_gguf.pyне знает архитектуруkda. Даже установкаfla(flash‑linear‑attention) не помогает: в скрипте нет класса‑обработчика для неё. Решение: ждать, пока поддержка войдёт вllama.cppmaster (PR #18381), или использовать Python‑инференс напрямую черезtransformers.Совсем новые архитектуры (например, Mamba‑гибриды) — могут требовать
transformersизgit(pip install git+https://github.com/huggingface/transformers.git), если в стабильном релизе их ещё нет.Модели с кастомным
trust_remote_code— иногдаconvert_hf_to_gguf.pyтребует флаг--trust-remote-code, иначе не загрузитconfig.json.
Как проверить заранее. Зайдите в llama.cpp/conversion/ и поищите файл с именем архитектуры:
ls llama.cpp/conversion/ | grep -i <архитектура>
Например, grep -i qwen покажет qwen.py. Если для вашей модели нет файла — конвертация не пройдёт без патчей.
Использование
Базовый запуск:
./convert_to_gguf.sh Qwen/Qwen3.5-2B
Без аргумента — спросит имя интерактивно:
./convert_to_gguf.sh # Введите имя модели на Hugging Face (например, Qwen/Qwen3.5-2B): _
С сохранением промежуточных файлов:
KEEP_INTERMEDIATE=1 ./convert_to_gguf.sh Qwen/Qwen3.5-2B
Что появится в models/:
models/ ├── Qwen3.5-2B-Q4_K_M.gguf ← итоговый файл (готов к запуску) └── (после успеха: hf_model/ и Qwen3.5-2B-f16.gguf удалены)
Время работы:
Модель | Скачивание | Конвертация | Квантование | Итого |
|---|---|---|---|---|
Qwen3.5–2B | 2–5 мин | 1–2 мин | 30 сек | ~5 мин |
Qwen3.5–9B | 10–20 мин | 5–10 мин | 1–2 мин | ~20 мин |
Qwen3.5–35B | 30–60 мин | 20–40 мин | 3–5 мин | ~1.5 часа |
На HDD — дольше в 2–3 раза (особенно скачивание и чтение при конвертации).
Код convert_to_gguf.sh
#!/usr/bin/env bash # Конвертация модели с Hugging Face в GGUF (F16) и квантование в Q4_K_M. # Использование: ./convert_to_gguf.sh [USER/REPO] # Пример: ./convert_to_gguf.sh Qwen/Qwen3.5-2B # # Скрипт сам: # - доустанавливает системные пакеты (через sudo apt); # - создаёт/активирует venv и ставит зависимости из llama.cpp/requirements; # - скачивает модель через `hf download`, если папки hf_model нет; # - конвертирует в F16 GGUF, квантует в Q4_K_M; # - проверяет квант через llama-cli; # - удаляет hf_model и промежуточный F16 только после успешной проверки. set -euo pipefail # --- 0. Параметры --- MODEL_ID="${1:-}" if [ -z "$MODEL_ID" ]; then read -r -p "Введите имя модели на Hugging Face (например, Qwen/Qwen3.5-2B): " MODEL_ID fi [ -n "$MODEL_ID" ] || { echo "[!] Имя модели не задано." >&2; exit 1; } # Короткое имя для файлов (последняя часть после /) MODEL_SHORT="${MODEL_ID##*/}" # --- 1. Пути --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LLAMA_DIR="$SCRIPT_DIR/llama.cpp" REQ_DIR="$LLAMA_DIR/requirements" HF_DIR="$SCRIPT_DIR/hf_model" OUT_DIR="$SCRIPT_DIR/models" VENV_DIR="$SCRIPT_DIR/venv" mkdir -p "$OUT_DIR" # --- 1a. Очистка на выходе (trap) --- # CLEANUP_PATHS — всё, что нужно удалить при выходе. Q4_K_M сюда НЕ добавляем. CLEANUP_PATHS=() cleanup() { local ec=$? trap - EXIT INT TERM # защита от рекурсии set +e # очистка не должна падать echo "" if [ "${KEEP_INTERMEDIATE:-0}" = "1" ]; then echo "[i] KEEP_INTERMEDIATE=1: промежуточные файлы сохранены." exit "$ec" fi if [ ${#CLEANUP_PATHS[@]} -gt 0 ]; then echo "[*] Очистка промежуточных файлов..." for p in "${CLEANUP_PATHS[@]}"; do if [ -e "$p" ]; then echo " rm -rf $p" rm -rf "$p" fi done fi exit "$ec" } trap cleanup EXIT INT TERM # --- 2. Системные пакеты (ставим только отсутствующие) --- SYS_PKGS=(cmake build-essential libopenblas-dev python3 python3-venv python3-pip git curl wget jq) MISSING_SYS=() for pkg in "${SYS_PKGS[@]}"; do # dpkg -s: код 0 = установлен, иначе — нет dpkg -s "$pkg" &>/dev/null || MISSING_SYS+=("$pkg") done if [ ${#MISSING_SYS[@]} -gt 0 ]; then echo "[*] Установка системных пакетов: ${MISSING_SYS[*]}" sudo apt update -qq sudo apt install -y -qq "${MISSING_SYS[@]}" else echo "[*] Все системные пакеты уже установлены." fi # --- 3. Сборка llama.cpp (если нужно) --- LLAMA_QUANTIZE="$LLAMA_DIR/build/bin/llama-quantize" LLAMA_CLI="$LLAMA_DIR/build/bin/llama-cli" if [ ! -x "$LLAMA_QUANTIZE" ] || [ ! -x "$LLAMA_CLI" ]; then echo "[*] Сборка llama.cpp..." if [ ! -d "$LLAMA_DIR/.git" ]; then git clone --depth=1 https://github.com/ggerganov/llama.cpp "$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 # --- 4. venv и зависимости конвертера --- # Используем официальный requirements-файл: он тянет CPU-версию torch. if [ ! -d "$VENV_DIR" ]; then echo "[*] Создание venv..." python3 -m venv "$VENV_DIR" fi # активируем окружение source "$VENV_DIR/bin/activate" REQ_FILE="$REQ_DIR/requirements-convert_hf_to_gguf.txt" [ -f "$REQ_FILE" ] || { echo "[!] Не найден $REQ_FILE"; exit 1; } echo "[*] Установка зависимостей конвертера (CPU-only torch)..." # Проверяем «маркерные» пакеты из requirements-convert_hf_to_gguf.txt. # Если хоть одного нет — ставим весь файл (pip сам проверит версии и доставит). # Это быстрее, чем `pip install -r` при полном наборе, и надёжнее, чем # ручной список версий (при обновлении requirements всё подхватится). MARKER_PY_PKGS=(torch numpy sentencepiece transformers gguf protobuf huggingface_hub) MISSING_PY=() for pkg in "${MARKER_PY_PKGS[@]}"; do pip show "$pkg" &>/dev/null || MISSING_PY+=("$pkg") done # fla нужен для архитектур линейного внимания (KDA/Lissa и др.). if [ ${#MISSING_PY[@]} -gt 0 ]; then echo "[*] Отсутствуют Python-пакеты: ${MISSING_PY[*]}" echo "[*] Установка из $REQ_FILE (CPU-only torch)..." pip install --no-cache-dir -q -r "$REQ_FILE" pip install --no-cache-dir -q huggingface_hub else echo "[*] Зависимости конвертера уже установлены." fi # --- 5. Загрузка модели --- CLEANUP_PATHS+=("$HF_DIR") if [ -d "$HF_DIR" ] && [ -n "$(ls -A "$HF_DIR" 2>/dev/null)" ]; then echo "[*] Папка $HF_DIR уже существует, пропускаем загрузку." else echo "[*] Загрузка $MODEL_ID в $HF_DIR..." # --local-dir — без промежуточного кэша, сразу в папку. hf download "$MODEL_ID" --local-dir "$HF_DIR" fi # --- 5a. Sanity check: веса не LFS-указатели --- # После `hf download` это всегда так, но проверка дешёвая и не мешает. if grep -ql "git-lfs.github.com/spec" "$HF_DIR"/*.safetensors 2>/dev/null; then echo "[!] В $HF_DIR обнаружены LFS-указатели вместо весов." >&2 exit 1 fi # --- 6. Конвертация в F16 GGUF --- F16_GGUF="$OUT_DIR/${MODEL_SHORT}-f16.gguf" CLEANUP_PATHS+=("$F16_GGUF") echo "[*] Конвертация в $F16_GGUF..." python3 "$LLAMA_DIR/convert_hf_to_gguf.py" "$HF_DIR" --outfile "$F16_GGUF" # Проверка размера F16 (должен быть сопоставим с суммой safetensors) echo "[*] Размер F16: $(du -h "$F16_GGUF" | cut -f1)" # --- 7. Квантование в Q4_K_M --- Q4_GGUF="$OUT_DIR/${MODEL_SHORT}-Q4_K_M.gguf" echo "[*] Квантование в $Q4_GGUF..." "$LLAMA_QUANTIZE" "$F16_GGUF" "$Q4_GGUF" Q4_K_M # --- 8. Проверка квантованной модели --- # Минимальный sanity check: файл существует и размер разумный. # Полноценный тест — через llama-server вручную (или в отдельном скрипте). if [ ! -f "$Q4_GGUF" ]; then echo "[!] Квантованный файл не создан: $Q4_GGUF" >&2 exit 1 fi # Проверка размера: Q4_K_M обычно 1/4..1/5 от F16. Если файл < 100 МБ — явно битый. Q4_SIZE=$(stat -c%s "$Q4_GGUF") if [ "$Q4_SIZE" -lt $((100 * 1024 * 1024)) ]; then echo "[!] Подозрительно маленький файл: $(du -h "$Q4_GGUF" | cut -f1)" >&2 CLEANUP_PATHS+=("$Q4_GGUF") exit 1 fi echo "[*] Файл создан: $Q4_GGUF ($(du -h "$Q4_GGUF" | cut -f1))" # --- 9. Очистка --- # Ничего не делаем: cleanup() через trap EXIT/INT/TERM удалит # содержимое CLEANUP_PATHS (если KEEP_INTERMEDIATE != 1). echo "[*] Готово: $Q4_GGUF" exit 0
Проверка результата
1. Файл создан:
ls -lh models/Qwen3.5-2B-Q4_K_M.gguf
Должен быть ~1–1.5 ГБ (для 2B в Q4_K_M).
2. Модель загружается в llama-server:
./run_llama_mmap.sh -m Qwen3.5-2B-Q4_K_M
В логе — model loaded, а в UI — ответ на «Привет».
3. Промежуточные файлы удалены:
ls -d hf_model 2>/dev/null # должно быть "No such file" ls models/*-f16.gguf 2>/dev/null # должно быть пусто
Если что‑то осталось — KEEP_INTERMEDIATE был включён, или скрипт упал до очистки. Проверьте вывод.
Подводные камни
Не запускайте конвертацию и
llama-serverодновременно при малом объёме RAM.convert_hf_to_gguf.pyгрузит.safetensorsв память (для 9B — ~18 ГБ), аllama-serverдержит модель черезmmap. Вместе они могут не влезть. Сначала конвертация, потом — сервер.F16 GGUF может быть больше суммы
.safetensorsна 5–10% (метаданные, выравнивание). Не пугайтесь, еслиls -lhпокажет чуть больше ожидаемого.Квантование не всегда даёт «ровно» Q4_K_M. Для некоторых архитектур
llama-quantizeможет выдать предупреждение «some tensors require higher precision» и оставить часть весов в F16. Это нормально: критичные слои (например,output.weight) иногда остаются в F16 для стабильности.Tokenizer для мультимодальных моделей. Некоторые модели (например, MOSS‑Image) требуют отдельный
tokenizer.json, который не входит в.safetensorsи не подхватываетсяconvert_hf_to_gguf.pyавтоматически. Его нужно скачать отдельно (hf download <repo> tokenizer.json --local-dir ...) и указать вMODEL_MAPчерез--tokenizer. Для обычных LLM (Qwen, Gemma) это не нужно — токенизатор встроен в GGUF.Gated‑модели. Если модель требует принятия лицензии на Hugging Face,
hf downloadпопросит токен. Залогиньтесь один раз:hf auth loginТокен сохранится в
~/.cache/huggingface/, дальнейшие загрузки пройдут без запроса.
Итог
convert_to_gguf.sh — это «одна команда, весь пайплайн» для получения .gguf из «сырых» весов Hugging Face. Он:
Ставит отсутствующие зависимости (быстро при повторных запусках).
Использует CPU‑версию
torch(без CUDA‑драйверов).Гарантирует очистку при любом исходе через
trap.Даёт понятные сообщения об ошибках (LFS‑указатели, слишком маленький Q4, отсутствующий requirements).
Не пытается «спасти» неподдерживаемые архитектуры (KDA) — честно падает с
Failed to detect model architecture.
Когда использовать:
Модель есть на Hugging Face, но нет готового
.gguf(или нет нужного кванта).Хочется
Q5_K_MилиQ8_0, а сообщество сделало толькоQ4_K_M.Нужна конкретная ревизия модели.
Когда НЕ использовать:
Готовый
.ggufуже есть — скачайте его (hf downloadилиwget), это в 3–5 раз быстрее.Архитектура не поддерживается
convert_hf_to_gguf.py(проверьтеconversion/заранее).Нет 30–60 ГБ свободного места (
.safetensors+ F16 + Q4 на пике).


