Сжатие декодерных эмбеддеров: гайд по PQ и бинарному квантованию | 2026 | AiManual
AiManual Logo Ai / Manual.
02 Июл 2026 Гайд

Полное руководство по сжатию декодерных эмбеддеров: от 8B до продакшена без потери recall

Научитесь сжимать эмбеддинги Qwen3-Embedding и NV-Embed с 4 ГБ до 128 МБ без потери качества. Product Quantization, бинарное квантование, FAISS и продакшен-сове

Почему Float32 эмбеддинги — роскошь в 2026 году

Каждый инженер RAG сталкивается с этим: вы размечаете 100 миллионов чанков, прогоняете их через NV-Embed-v2 (или Qwen3-Embedding), и получаете 100 миллионов векторов размерностью 4096. В float32 это 100M × 4096 × 4 байта = 1.6 ТБ. Даже на SSD за $2000 это больно. А latency? При полном сканировании 1.6 ТБ вы не впишетесь ни в один SLA.

Но есть и хорошая новость: современные методы сжатия позволяют уменьшить объём в 10–30 раз, теряя всего 1–2% recall. И это не магия — это Product Quantization (PQ) и бинарное квантование. Заодно вы получаете ускорение поиска на порядок, потому что расстояния считаются через предвычисленные таблицы или XOR.

💡
Сжатие эмбеддингов — не про жадность, а про масштабирование. Без него вы либо платите бешеные деньги за инфраструктуру, либо режете качество, ограничивая количество документов.

Декодерные эмбеддеры: мощь, за которую приходится платить

Декодерные эмбеддеры (NV-Embed, Qwen3-Embedding, MiniMax-M2.5) — это LLM, которые вместо генерации токенов выдают векторное представление. Они захватили лидерство в MTEB, потому что понимают контекст глубже, чем BERT-подобные модели. Но плата — размерность: часто 4096 или даже 8192 (у MiniMax).

Например, Qwen3-Embedding выдаёт 4096-мерные векторы. Если вы используете его для корпуса из 50 млн чанков, хранилище займёт ~800 ГБ. А ведь надо ещё держать индекс для быстрого поиска — с IVF-PQ он тоже может быть сжат, но лучше делать сжатие на уровне векторов.

Кстати, архитектура этих моделей — обычный decoder-only transformer. Если вам интересно, почему квадратичная сложность само-внимания не убивает производительность, почитайте про альтернативу свёрточным декодером.

Product Quantization: как упаковать гигабайты в мегабайты

Классический PQ (Jégou et al., 2011) до сих пор остаётся золотым стандартом. Идея: разбить вектор на m подвекторов, для каждого обучить k центроидов (кластеризация k-means), заменить подвектор номером ближайшего центроида. Итоговый вектор кодируется m × log2(k) бит.

Типичные настройки: m = 64, k = 256. Тогда сжатие с 4096 × 32 = 131072 бит до 64 × 8 = 512 бит — в 256 раз! Однако recall может упасть на 5–10%. В 2026 году инженеры используют оптимизированный PQ с residual-квантованием (как в REAP-квантовании MiniMax-M2.5) — оно уменьшает артефакты и сохраняет >98% recall.

Подробнее о том, как REAP даёт ещё 19–50% сжатия, я разобрал в отдельной статье.

Бинарное квантование: когда каждый бит на счету

Бинарное квантование (BQ) — самый радикальный метод: каждый компонент заменяется своим знаком (0 если <0, 1 если ≥0). Так мы получаем 4096 бит = 512 байт на вектор. Сжатие в 32 раза. Поиск идёт через Hamming distance (XOR + popcount) — это молниеносно на CPU за счёт инструкций popcnt.

Проблема: recall падает сильнее, особенно для длинных хвостов. Но есть хитрость: использовать бинарный индекс как фильтр — быстро отсечь 99% кандидатов, а затем переранжировать топ-k по полным float32-векторам (которые хранятся отдельно, но только для малой доли кандидатов).

Этот подход (bi-encoder + re-ranker) даёт почти полное сохранение recall при поиске за 1–2 мс на запрос. Даже на слабом железе.

Если вы используете GPU с ограниченной памятью, бинарное квантование — спасение. Но не ждите чуда на задачах, где важна семантическая тонкость (юридические или медицинские документы). Там лучше PQ с residual кодированием.

Пошаговый план: от эмбеддингов 8B до продакшен-индекса

Теперь к делу. Возьмём Qwen3-Embedding (или NV-Embed — код универсален) и сожмём эмбеддинги корпуса из 1M документов с помощью FAISS. Будем использовать IVF-индекс с PQ-кодированием.

1 Генерация эмбеддингов

Сначала получаем эмбеддинги. Если ваша модель не помещается в VRAM, можно использовать CPU инференс или снять ограничения через хардверный хак — например, мод RTX 4090 на 48 ГБ.

import torch
from transformers import AutoModel, AutoTokenizer

model_name = "Qwen/Qwen3-Embedding-8B"  # актуально на июль 2026
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name, torch_dtype=torch.float16, device_map="auto")

def get_embedding(text: str) -> list:
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=8192).to("cuda")
    with torch.no_grad():
        output = model(**inputs)
        # у Qwen3-Embedding эмбеддинг — это last_hidden_state[:, -1, :]
        emb = output.last_hidden_state[:, -1, :].cpu().float().numpy().flatten()
    return emb.tolist()

# Для 1M документов — итеративно сохраняем в numpy
import numpy as np
# embeddings = np.memmap('embeddings.bin', dtype='float32', mode='w+', shape=(1_000_000, 4096))
# for i, doc in enumerate(docs): embeddings[i] = get_embedding(doc)

2 Обучение квантователя (IndexPQ)

FAISS предоставляет готовый класс IndexPQ. Важно: подбирать параметры m и nbits на репрезентативной выборке (10–100K векторов).

import faiss
import numpy as np

# Загружаем выборку для обучения
X_train = np.random.random((50000, 4096)).astype('float32')  # вместо этого реальные эмбеддинги

m = 64            # число подвекторов
n_bits = 8        # 256 центроидов на подвектор

pq = faiss.ProductQuantizer(d=4096, M=m, nbits=n_bits)
pq.train(X_train)

# Создаём индекс (можно без IVF для начала)
index = faiss.IndexPQ(4096, m, nbits)
index.pq = pq
index.is_trained = True

3 Кодирование и построение индекса

Кодируем все векторы и добавляем их в индекс. При добавлении FAISS автоматически считает коды.

import time

X_all = np.memmap('embeddings.bin', dtype='float32', mode='r', shape=(1_000_000, 4096))

batch_size = 10000
for i in range(0, len(X_all), batch_size):
    batch = X_all[i:i+batch_size]
    index.add(batch)
    if (i // batch_size) % 10 == 0:
        print(f"Added {i+len(batch)} vectors")

# Сохраняем индекс на диск
faiss.write_index(index, "pq_index.faiss")

4 Поиск и оценка recall

Проверяем качество: для тестовых запросов сравниваем nearest neighbours по сжатому индексу и по точному (brute-force).

# Точный индекс для сравнения
index_bf = faiss.IndexFlatIP(4096)  # inner product
index_bf.add(X_all[:100000])  # ограничимся 100k для скорости

# Выбираем 1000 запросов
queries = np.random.random((1000, 4096)).astype('float32')

# Поиск по PQ-индексу
index.nprobe = 10  # для IVF-PQ; у нас просто PQ, nprobe не используется, но параметр есть
D_pq, I_pq = index.search(queries, 10)

# Точный поиск
D_bf, I_bf = index_bf.search(queries, 10)

# Recall@10: сколько из верхних 10 точного поиска попали в верхние 10 PQ
recall = 0
for i in range(len(queries)):
    recall += len(set(I_bf[i]) & set(I_pq[i])) / 10.0
print(f"Recall@10: {recall/len(queries)*100:.2f}%")

5 Интеграция в продакшен

В продакшене лучше:

  • Использовать IVF-PQ (IndexIVFPQ) — сначала кластеризация по coarse quantizer, затем PQ в каждой ячейке. Это даёт sub-linear поиск.
  • Хранить коды в сжатом виде (uint8). Для размера 64 × 8 бит = 64 байта на вектор.
  • Распараллеливать добавление: faiss.IndexIVFPQ поддерживает параллельное добавление через omp.
  • Настраивать nprobe (количество просматриваемых ячеек) — это trade-off скорость/recall.
# Пример IVF-PQ
nlist = 1000  # число ячеек coarse quantizer
quantizer = faiss.IndexFlatL2(4096)
index_ivfpq = faiss.IndexIVFPQ(quantizer, 4096, nlist, m, n_bits)
index_ivfpq.train(X_train)
index_ivfpq.add_with_ids(X_all, ids)  # ids = np.arange(1M)
faiss.write_index(index_ivfpq, "ivfpq_index.faiss")

Грабли, на которые я наступал

  • Не нормировал эмбеддинги перед обучением PQ. Если вы используете косинусную близость, векторы должны быть L2-нормализованы. Иначе PCA и PQ работают с анизотропным пространством — recall падает на 20%.
  • Обучал PQ на слишком маленькой выборке. Минимум 100K векторов, иначе центроиды не покрывают распределение.
  • Использовал коды как черный ящик. При переиндексации датасета нужно переобучать квантователь. Если данные меняются — хотя бы дообучать центроиды на смеси старых и новых.
  • Забывал про asymmetry distance computation. FAISS для PQ вычисляет расстояние между запросом (в float) и кодами через предвычисленные таблицы, что правильно. Но если вы реализуете своё — не вздумайте декодировать все коды в float и считать обычное расстояние, это убивает скорость.

Когда сжатие не нужно: ловушки и крайние случаи

Не сжимайте, если:

  1. У вас менее 1 миллиона векторов — overhead от декодирования кодов может не оправдаться.
  2. Эмбеддинги уже разрежены (например, SPLADE). PQ только испортит разреженность.
  3. Вы используете бинарное квантование для поиска ближайших соседей по косинусу — Hamming distance не эквивалентна косинусу. Нужно сначала нормализовать и отобразить в единичную сферу.
  4. Ваш датасет содержит кластерные аномалии (много почти одинаковых документов). PQ будет путать их с похожими, но разными.

Что дальше?

Квантование декодерных эмбеддеров стало мейнстримом к 2026 году. NVIDIA встроила PQ прямо в TensorRT для моделей эмбеддингов. Alibaba выпустила Qwen3-Embedding с поддержкой сжатия на уровне архитектуры. А техники вроде REAP от MiniMax стирают грань между сжатием и lossless.

Мой совет: не пытайтесь внедрить всё сразу. Начните с IVF-PQ, измерьте recall, добавьте бинарный фильтр. Через месяц вы удивитесь, как жили без этого.

Кросс-архитектурные оптимизации вроде тех, что реализованы в свёрточном декодере из России, возможно, изменят саму природу эмбеддингов, но сжатие останется с нами навсегда.

Подписаться на канал