Руководство по сборке локального AI-стека. Часть 6. Конвертация моделей 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 на пике).
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.