Все статьи
4 мин чтения

Инструкция: как уменьшить задержку (TTFT) при квантовании моделей до 4-бит

Пошаговое руководство по квантованию LLM до 4 бит с сохранением качества: выбираем метод, замеряем TTFT и скорость генерации, проверяем деградацию и запускаем модель локально.

LLM Arch ResearchCodeLabLLMИнструменты

Зачем квантовать?

Большие языковые модели (7B+ параметров) требуют значительных вычислительных ресурсов. Полноточная модель (FP16) на 7B параметров занимает ~14 ГБ VRAM. Квантование весов до 4 бит сокращает это примерно до 4 ГБ — достаточно для потребительской видеокарты.

Важная оговорка: квантование в первую очередь экономит память. Выигрыш по задержке зависит от того, какие вычислительные ядра выполняют модель. GPTQ и AWQ со специализированными ядрами (Marlin, ExLlama) обычно ускоряют генерацию, а 4-битный режим bitsandbytes экономит память, но при одиночных запросах может работать даже медленнее FP16. Поэтому задержку нужно не предполагать, а измерять.

Измерять будем две метрики, которые отвечают двум фазам генерации:

  • TTFT (Time To First Token) — время обработки промпта (prefill). Зависит от длины промпта и вычислительной мощности.
  • Tokens/sec — скорость генерации (decode). Ограничена в основном пропускной способностью памяти, поэтому 4-битные веса здесь помогают сильнее всего.

Если хочется разобраться, откуда берутся эти две фазы и как KV-кэш экономит вычисления на каждом шаге, загляните в LLM Arch Research — там генерация с KV-кэшем разобрана построчно.

Подготовка

pip install torch transformers accelerate bitsandbytes

Шаг 1: Замер baseline

Сначала замеряем модель без квантования. TTFT — это время генерации одного токена, скорость декодирования — время остальных токенов:

import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL = "Qwen/Qwen2.5-7B-Instruct"


def measure(model, tok, prompt, new_tokens=128):
    inputs = tok(prompt, return_tensors="pt").to(model.device)
    model.generate(**inputs, max_new_tokens=8)  # прогрев

    torch.cuda.synchronize()
    t0 = time.perf_counter()
    model.generate(**inputs, max_new_tokens=1, do_sample=False)
    torch.cuda.synchronize()
    ttft = time.perf_counter() - t0

    t0 = time.perf_counter()
    model.generate(**inputs, max_new_tokens=new_tokens, min_new_tokens=new_tokens, do_sample=False)
    torch.cuda.synchronize()
    decode = time.perf_counter() - t0 - ttft

    return {
        "ttft_ms": ttft * 1000,
        "tokens_per_sec": (new_tokens - 1) / decode,
        "peak_mem_gb": torch.cuda.max_memory_allocated() / 1e9,
    }


tok = AutoTokenizer.from_pretrained(MODEL)
fp16 = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.float16, device_map="auto")
print(measure(fp16, tok, "Объясни, что такое квантование нейросетей."))

Шаг 2: Быстрое квантование NF4 (bitsandbytes)

Самый простой путь — квантовать веса прямо при загрузке, без калибровки:

from transformers import BitsAndBytesConfig

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)
int4 = AutoModelForCausalLM.from_pretrained(MODEL, quantization_config=bnb, device_map="auto")
print(measure(int4, tok, "Объясни, что такое квантование нейросетей."))

Ключевые параметры:

  • bnb_4bit_quant_type="nf4" — формат NormalFloat4, точнее обычного INT4 для нормально распределённых весов.
  • bnb_4bit_compute_dtype=torch.bfloat16 — вычисления в bf16; по умолчанию они идут в fp32, и это заметно замедляет модель.
  • bnb_4bit_use_double_quant — квантует и сами коэффициенты масштаба, экономя ещё около 0,4 бита на параметр.

Шаг 3: Если нужна именно низкая задержка — GPTQ или AWQ

GPTQ и AWQ квантуют веса с калибровкой на небольшом наборе данных и запускаются на быстрых ядрах. Для популярных моделей готовые чекпоинты уже опубликованы, а сервер инференса вроде vLLM подхватывает их сам:

pip install vllm
vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ

Если квантуете сами, главные параметры такие:

  • group-size — сколько весов делят один коэффициент масштаба. 128 — разумный баланс; 64 и 32 точнее, но увеличивают размер модели.
  • Калибровочные данные — 128–512 примеров, похожих на ваши реальные запросы.

Шаг 4: Проверка качества

Скорость без качества ничего не стоит. Сравните исходную и квантованную модели на одних и тех же задачах — например, с помощью lm-evaluation-harness:

lm_eval --model hf \
  --model_args pretrained=Qwen/Qwen2.5-7B-Instruct,load_in_4bit=True \
  --tasks mmlu,hellaswag \
  --batch_size 8

Сведите в одну таблицу TTFT, tokens/sec, пиковую память и точность. Для 7B-моделей при аккуратном 4-битном квантовании просадка на общих бенчмарках обычно укладывается в 1–2 пункта, но на ваших доменных задачах она может быть другой — проверяйте именно их.

Шаг 5: Упростите системный промпт

После квантования модель может хуже следовать длинным и запутанным инструкциям. Упростите системный промпт: разбейте его на короткие пункты и уберите лишний контекст. Бонус — более короткий промпт сокращает prefill, а значит, и TTFT. Проверяйте варианты промпта A/B-тестом на своём наборе запросов.

Запуск локально

Квантованные модели в формате GGUF удобно запускать через Ollama или LM Studio. А подключив их к CodeLab, который поддерживает Ollama и LM Studio как LLM-провайдеров, вы получите AI-ассистента для кода, который работает полностью на вашей машине.

Рекомендации

  • Начинайте с group-size 128; уменьшайте его, только если просадка качества неприемлема.
  • Калибруйте на репрезентативных данных, а не на случайных текстах.
  • Проверяйте качество на доменно-специфичных тестах, а не только на общих бенчмарках.
  • Замеряйте задержку на целевом железе и с реальной нагрузкой: результаты при одиночном запросе и при батче могут отличаться в разы.