Руководство по сборке локального AI-стека
Конвертация моделей 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 1fi
Что это ловит. 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 1fi
Порог 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_IDfi[ -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")doneif [ ${#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_hubelse 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 1fi# --- 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 1fi# Проверка размера: 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 1fiecho "[*] Файл создан: $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 на пике).
ссылка на оригинал статьи https://habr.com/ru/articles/1099540/