Как устроена LLM · How LLMs work · урок 2 · 14 мин

Веса: что лежит в файле модели, точность и квантизация

Weights, precision and quantization

ТемыLLM изнутриВесаЖелезо

Зачем

После урока ты за минуту посчитаешь, сколько памяти GPU нужно модели в любой точности, и откроешь файл модели, понимая, что в нём лежит. На интервью это вопросы «will a 70B model fit on one GPU?» и «what is quantization and what does it cost?», а в работе — выбор модели и кванта (конкретного варианта сжатия, например Q4_K) под конкретную видеокарту.

Веса — именованные таблицы чисел, разложенные по слоям

Weights (веса), они же parameters (параметры), — это числа в матрицах модели, которые подбирает обучение. Файл модели хранит их как набор tensors (многомерных массивов) с именами, формой и типом чисел.

Посмотрим на настоящую модель Qwen3-8B на Hugging Face. Веса лежат в пяти файлах model-00001-of-00005.safetensors … 00005, а файл model.safetensors.index.json говорит, какой tensor в каком файле. Всего 399 tensors и 16 381 470 720 байт. Имена повторяют устройство модели из урока a1-1:

Tensor Форма Что это
model.embed_tokens.weight 151936 × 4096 таблица embeddings, 1,24 ГБ
model.layers.0.self_attn.q_proj.weight 4096 × 4096 query первого блока
model.layers.0.self_attn.k_proj.weight 1024 × 4096 key первого блока
model.layers.0.mlp.gate_proj.weight 12288 × 4096 одна из трёх матриц MLP
model.norm.weight, lm_head.weight 4096; 151936 × 4096 финальная норма и выходной слой

36 блоков по 11 tensors плюс 3 общих — как раз 399. Рядом лежат config.json (архитектура и размеры), файлы tokenizer и chat template. Веса без них бесполезны: программа не узнает, как их собрать и как подать текст.

Что внутри safetensors и GGUF

safetensors — формат Hugging Face, и устроен он просто. Первые 8 байт — число N (длина заголовка). Следующие N байт — JSON: для каждого tensor его dtype (тип чисел), shape (форма) и data_offsets (где лежат байты). Дальше — сплошной буфер чисел. У первого файла Qwen3-8B заголовок занимает 9 328 байт, и по нему видно, например, что model.embed_tokens.weight — BF16, [151936, 4096], байты с 0 по 1 244 659 712. Главное отличие от старых .bin и .pt: те сохранены через pickle, а pickle при загрузке может выполнить произвольный код. safetensors — только данные, поэтому файл из интернета безопасно открыть. Бонус: отдельный tensor можно прочитать, не загружая весь файл.

GGUF — формат экосистемы llama.cpp (на нём работают Ollama и LM Studio). Это один файл, где лежат и tensors, и стандартные metadata (метаданные): архитектура, длина context, число блоков, словарь tokenizer и часто tokenizer.chat_template. Цель формата по спецификации — single-file deployment: никаких внешних файлов. Имена tensors свои: token_embd.weight, blk.0.attn_q.weight, blk.0.ffn_up.weight. Hugging Face показывает metadata и список tensors GGUF прямо на странице файла.

Правило выбора простое: safetensors — для обучения и GPU-серверов вроде vLLM; GGUF — для llama.cpp, Ollama, ноутбука и Mac.

Точность: сколько байт на одно число

Формат Байт на параметр Устройство Где встречается
fp32 4 8 бит порядка, 23 мантиссы старое обучение, оптимизатор
fp16 2 5 бит порядка, 10 мантиссы inference на старых GPU
bf16 2 8 бит порядка, 7 мантиссы стандарт обучения и выпуска весов
fp8 (E4M3, E5M2) 1 4+3 или 5+2 бита новые GPU, быстрый inference
int8 1 целое + масштаб квантизация
int4 0,5 целое + масштаб на блок квантизация для одной GPU

bf16 (brain float 16) сохраняет порядок от fp32, то есть тот же диапазон, но жертвует точностью мантиссы. Для обучения диапазон важнее: числа не переполняются. Документация Hugging Face пишет, что почти все модели сейчас обучают в bf16, и fp32 на inference не даст лучшего результата, чем точность, в которой модель обучали. Статья о fp8 (2022) показала, что обучение в 8 битах дало качество, практически равное 16-битному, на моделях до 175B.

Арифметика VRAM: параметры × байты

Память под веса = число параметров × байт на параметр. Документация Hugging Face формулирует так: модели из X млрд параметров в bf16 нужно примерно 2·X ГБ VRAM (видеопамяти).

Модель fp16 / bf16 fp8 / int8 int4 (0,5 Б) GGUF Q4_K (4,5 бита)
7B 14 ГБ 7 ГБ 3,5 ГБ 3,9 ГБ
27B 54 ГБ 27 ГБ 13,5 ГБ 15,2 ГБ
70B 140 ГБ 70 ГБ 35 ГБ 39,4 ГБ

Q4_K «весит» больше чистых 4 бит, потому что на каждый блок весов хранится масштаб: по таблице Hugging Face это 4,5 бита на вес. Сверка с реальным файлом: Qwen3-8B в bf16 — 16,38 ГБ, то есть 8,19 млрд × 2 байта.

К весам добавляются KV cache (растёт с длиной context, урок a1-4), activations (промежуточные результаты) и буферы библиотеки. Hugging Face измерил это на модели в 15,5B: bf16 — 29,0 ГиБ, 8 бит — 15,2 ГиБ, 4 бита — 9,5 ГиБ на коротком запросе. 4 бита дали не вчетверо меньше, а втрое: квантуются не все tensors, плюс буферы. Помни про единицы: 1 ГиБ = 1,074 ГБ, а nvidia-smi показывает МиБ.

Пример решения. Влезет ли 27B на карту 32 ГБ? В bf16 — 54 ГБ, нет. В fp8 — 27 ГБ: веса влезают, но на KV cache и буферы остаётся около 5 ГБ, это впритык и короткий context. В Q4_K — 15,2 ГБ, остаётся ~16 ГБ на длинный context. 70B даже в Q4_K (39,4 ГБ) на 32 ГБ не помещается: нужны две карты или одна на 48 ГБ и больше.

Ловушка MoE (mixture of experts — модель из многих «экспертов», где на каждый token работает малая часть): память определяет общее число параметров, а скорость — активное. Qwen3.5-35B-A3B держит в памяти все 35B, хотя считает 3B на token.

Квантизация: что теряем и как выбрать

Quantization (квантизация) — хранить веса в меньшем числе бит с масштабами на блок и разворачивать их обратно в момент вычисления. Это шаг перед inference, а не обучение: «дообучили модель в GGUF» — ошибка в терминах.

Методы, которые встретишь: GGUF-кванты для llama.cpp (Q8_0 и k-quants: Q6_K 6,56 бита, Q5_K 5,5, Q4_K 4,5, Q3_K 3,44 бита на вес); GPTQ и AWQ — 4 бита для GPU-серверов, считаются заранее на калибровочных данных; bitsandbytes — 8 и 4 бита прямо при загрузке; fp8 — на новых GPU в vLLM.

Цена — качество. Hugging Face пишет, что 8 и 4 бита обычно почти не портят ответы, но 4 бита «can often lead to different results», и проверять надо самому. Ниже 4 бит потери обычно заметнее, и без замера такой квант не берут. Скорость зависит от реализации: в bitsandbytes Hugging Face увидел даже замедление, а в llama.cpp и vLLM меньше байт обычно значит быстрее генерацию, потому что она упирается в чтение весов из памяти.

Как решает инженер: (1) посчитать память под веса и KV cache для нужной длины context; (2) взять самую сильную модель, которая влезает в 4–8 битах; (3) прогнать свой набор проверок на двух-трёх квантах и сравнить с 16 битами; (4) выбрать самый лёгкий квант, который не уронил качество за порог.

Схема

Термины · Key terms

weights / parameters
числа в матрицах модели, которые подбирает обучение
tensor
многомерный массив чисел с именем, формой и типом
safetensors
безопасный формат весов: JSON-заголовок и сырые байты, без исполняемого кода
GGUF
однофайловый формат llama.cpp: tensors плюс metadata и tokenizer
dtype
тип чисел tensor: BF16, F16, F8, Q4_K и другие
bf16
16-битный формат с диапазоном fp32 и укороченной мантиссой
fp8
8-битный формат с плавающей точкой, варианты E4M3 и E5M2
quantization
сжатие весов до меньшего числа бит с масштабами на блок
VRAM
память видеокарты, главный предел для модели
KV cache
память под keys и values прошлых tokens, растёт с длиной context
MoE (mixture of experts)
модель, где на token работает часть экспертов, но в памяти лежат все

Сделай сам

Прочитай заголовок настоящего файла модели, не скачивая его 3 ГБ. Запусти 12 строк на Python без пакетов:

import json, struct, urllib.request

URL = "https://huggingface.co/Qwen/Qwen3-8B/resolve/main/model-00001-of-00005.safetensors"

def get(a, b):
    req = urllib.request.Request(URL, headers={"Range": f"bytes={a}-{b}"})
    return urllib.request.urlopen(req).read()

n = struct.unpack("<Q", get(0, 7))[0]          # первые 8 байт: длина заголовка
header = json.loads(get(8, 8 + n - 1))          # JSON: имя → dtype, shape, offsets
header.pop("__metadata__", None)
for name, t in sorted(header.items())[:6]:
    print(name, t["dtype"], t["shape"])
print(len(header), "tensors in this shard")

Запиши: длину заголовка в байтах, формы трёх tensors и сколько ГБ займёт model.embed_tokens.weight в bf16 и в 4 битах. Потом на бумаге посчитай память под веса 27B в bf16, fp8 и Q4_K.

Обсуди с тьютором: покажи вывод скрипта и свой расчёт для 27B и спроси, какой квант ты бы выбрал для карты на 32 ГБ при context 32 000 tokens и почему.

Проверь себя · ответ под вопросом

1Сколько памяти нужно под веса модели 70B в bf16, fp8 и int4, и почему реальная цифра будет больше?
140 ГБ, 70 ГБ и 35 ГБ: параметры умножаем на 2, 1 и 0,5 байта. Сверху нужны KV cache, который растёт с длиной context, activations и буферы библиотеки. В GGUF Q4_K ещё масштабы блоков: 4,5 бита на вес, около 39,4 ГБ.
2Почему safetensors безопаснее старых файлов .bin и .pt?
Старые файлы сохранены через pickle, а загрузка pickle может выполнить произвольный код из файла. safetensors хранит только JSON-заголовок с именами, типами и формами и сырые байты чисел. Открыть его — значит прочитать данные, ничего не исполняя.
3Модель MoE 35B-A3B: сколько памяти она займёт в bf16 и почему не как модель 3B?
Около 70 ГБ под веса, потому что в памяти лежат все 35 млрд параметров: router может выбрать любого эксперта. 3B — это активные параметры на token, они определяют объём вычислений и скорость, а не память.

Interview questions · US / UK

1We have a single 80 GB GPU. Can we serve a 70B dense model on it? Walk me through the numbers.
Strong answer: bf16 weights are about 140 GB, so no; fp8 is about 70 GB, which leaves too little for KV cache and buffers at useful context; 4-bit (GPTQ, AWQ or Q4_K at roughly 35–40 GB) fits with room for KV cache; validate quality of the quant on our own eval against a 16-bit reference; alternative is two GPUs with tensor parallelism.
2What is the difference between bf16 and fp16, and why did training move to bf16?
Strong answer: both are 2 bytes; bf16 keeps fp32's 8 exponent bits so the same dynamic range, with only 7 mantissa bits; fp16 has 5 exponent bits and overflows or underflows more easily; bf16 avoids loss scaling headaches in training; for inference serve in the dtype the model was trained in, fp32 adds nothing.
3Follow-up: you quantized a model to 4 bits and users say answers got worse. How do you investigate?
Strong answer: reproduce on a fixed eval set with repeated runs against the 16-bit baseline; compare several quants (8-bit, 5-bit, 4-bit, different methods like AWQ vs GPTQ vs GGUF k-quants); check per-language and per-task slices, not only the average; check chat template and tokenizer metadata in the quantized file; pick the lightest quant that stays above the agreed threshold.

Ключевые фразы · Say it aloud

Материалы