The Jerusalem PostEgypt approves military support for Saudi Arabia after Houthi attacks escalatePunchPolice arrest two over deaths of minors in BauchiInquirerUndas 2026: 4,790 police personnel to be deployed in SoccsksargenESPNThunder star Williams stretchered off after suffering significant leg injuryInquirer EntertainmentAndrea Brillantes bares diving mishap while shooting ‘Laya’ESPN DeportesReal Madrid, a 3 puntos del Barça: así va la tabla de LaLigaCollider'Deadpool's Director Is Presiding Over the Biggest Short Film Contest Ever [Exclusive]ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioAnime News NetworkRomelia War Chronicle ‒ Episode 4BBC NewsHow a movement of students brought down Serbia's VučićMintBrazil election 2026 results: What’s the net worth of Lula Da Silva as he gets re-elected as president for fourth term?Digital SpyUniversity Challenge contestants hit out at "absurd" BBC decision to edit out pride badges
The Daily Newsstand · Free, Always
Monday, October 26, 2026

Руководство по сборке локального AI-стека. Часть 6. Конвертация моделей Hugging Face в GGUF

Translate

Не все модели существуют в готовом .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, чтобы не засорять систему.

Процесс состоит из двух шагов:

  1. Конвертация .safetensors -> .gguf (F16). Это «перевод» весов из формата Hugging Face в формат llama.cpp. Размер файла примерно равен сумме .safetensors (может быть чуть больше из-за метаданных).

  2. Квантование F16 -> Q4_K_M. Это «сжатие» с потерей точности. Размер уменьшается в 3–4 раза, качество падает минимально.

Зачем нужен промежуточный F16. llama-quantize работает только с GGUF — он не умеет читать .safetensors. Поэтому F16-файл — обязательный промежуточный шаг. После квантования его можно удалить (если не нужны другие кванты из того же F16).

Что делает скрипт

Скрипт convert_to_gguf.sh — это «одна команда, весь пайплайн»:

  1. Проверяет системные пакеты (cmake, build-essential, python3-venv, git, curl, jq) через dpkg -s. Ставит только если отсутствуют.

  2. Собирает llama.cpp (если llama-quantize и llama-cli ещё нет).

  3. Создаёт venv (если нет) и ставит зависимости из llama.cpp/requirements/requirements-convert_hf_to_gguf.txt. Это официальный файл от llama.cpp, который тянет CPU-версию torch (через --extra-index-url https://download.pytorch.org/whl/cpu) — никаких CUDA-драйверов.

  4. Скачивает модель через hf download --local-dir hf_model.

  5. Sanity check: проверяет, что .safetensors — реальные веса, а не LFS-указатели (маленькие текстовые файлы). После hf download это всегда так, но проверка дешёвая.

  6. Конвертирует в models/<модель>-f16.gguf через convert_hf_to_gguf.py.

  7. Квантует в models/<модель>-Q4_K_M.gguf через llama-quantize.

  8. Проверяет размер Q4: если файл меньше 100 МБ — явно битый, выходит с ошибкой.

  9. Очищает 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.cpp master (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 был включён, или скрипт упал до очистки. Проверьте вывод.

Подводные камни

  1. Не запускайте конвертацию и llama-server одновременно при малом объёме RAM. convert_hf_to_gguf.py грузит .safetensors в память (для 9B — ~18 ГБ), а llama-server держит модель через mmap. Вместе они могут не влезть. Сначала конвертация, потом — сервер.

  2. F16 GGUF может быть больше суммы .safetensors на 5–10% (метаданные, выравнивание). Не пугайтесь, если ls -lh покажет чуть больше ожидаемого.

  3. Квантование не всегда даёт «ровно» Q4_K_M. Для некоторых архитектур llama-quantize может выдать предупреждение «some tensors require higher precision» и оставить часть весов в F16. Это нормально: критичные слои (например, output.weight) иногда остаются в F16 для стабильности.

  4. 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.

  5. 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 на пике).

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.