К содержимому
MoranaLabs.
Инженерные гайды5 мин чтения0 просмотров

HNSW тюнинг руками: M, ef_construction и ef_search без догадок 

Дефолтные параметры HNSW из туториалов убивают сервера на проде по памяти. Разбираем физику графового индекса: как M и ef_construction реально влияют на RAM и latency, и как калибровать векторный поиск под SLA.

0xReality

Дефолтные параметры HNSW из туториалов — это гарантированный Out Of Memory на проде при попытке загрузить базу чуть больше игрушечной. Я вообще запрещаю тащить этот алгоритм в проект, пока не будет жестко обоснован бюджет по памяти узла и SLA на latency. Графовые индексы жрут RAM как не в себя.

«Постойте, но все блоги пишут, что HNSW — это state of the art для векторного поиска. Ставишь M=16, ef_construction=200 и оно летит».

Летит оно только на датасете из ста тысяч векторов. Когда у вас их десятки миллионов, а железо не резиновое, эти догадки заканчиваются переполнением памяти, выселением в своп и деградацией p99 latency до секунд. Векторный поиск на edge-устройствах или выделенных железках клиента не прощает инженерии наугад.

HNSW тюнинг руками: M, ef_construction и ef_search без догадок

Чтобы управлять системой, нужно понимать физику ее работы. Никакой магии в аббревиатурах нет.

Параметр M — это максимальное количество двунаправленных связей (ребер), которые алгоритм создает для каждого нового узла на базовом уровне графа. Это прямой мультипликатор потребления оперативной памяти. Каждое ребро — это указатель (обычно 4 байта). Рост M с 16 до 64 делает граф более плотным, что критически важно для высокоразмерных датасетов сложной топологии. Точность поиска (recall) растет. Но вместе с ней кратно растет объем метаданных узлов.

«Но ведь M=64 всегда дает лучшую точность, почему не поставить максимум?»

Потому что граф перестает влезать в L3-кэш процессора. HNSW — это memory-bound алгоритм. Как только метаданные узла становятся слишком толстыми, обход графа превращается в череду cache misses. Вы платите за прирост recall астрономическим падением пропускной способности (throughput) и скачками latency на случайном доступе к RAM.

ef_construction — размер динамического списка ближайших соседей, который используется только при построении индекса. Чем шире этот луч, тем качественнее алгоритм находит истинных соседей для прокладки ребер. Высокий ef_construction поднимает потолок достижимой точности графа, но делает процесс индексации мучительно долгим. Если этот параметр изначально занижен, граф получится «рыхлым» с локальными оптимумами — и никакой тюнинг при поиске это уже не исправит.

ef_search — размер луча при поиске. Единственный параметр, который можно дергать в рантайме без перестроения индекса. Балансирует между скоростью ответа и качеством выдачи.

Методика подбора под SLA

«Зачем вникать в физику, если можно прогнать grid search по всем трем параметрам и найти оптимум?»

Потому что перебор M и ef_construction требует полного перестроения графа с нуля на каждой итерации. На 100 миллионах векторов ваш наивный grid search завершится примерно в следующем году.

Правильный тюнинг идет от бизнес-требований. Сначала мы фиксируем целевую метрику качества. Допустим, продакт требует recall@10 не ниже 0.95. Дальше считаем жесткий лимит по RAM, который мы можем отдать под индекс, и вычисляем максимально допустимый параметр M.

Формула простая: размер вектора + объем ребер. Если мы упираемся в память, M придется резать. После фиксации M и сборки графа с умеренным ef_construction (например, 100-200), мы начинаем калибровку.

def calibrate_ef_search(index, test_queries, ground_truth, target_recall=0.95):
    # Идем от быстрого к точному
    for ef in [10, 20, 50, 100, 200, 400, 800]:
        index.set_ef(ef)
        start_t = time.perf_counter()
        labels, _ = index.knn_query(test_queries, k=10)
        latency = time.perf_counter() - start_t
        recall = compute_recall(labels, ground_truth)
        
        if recall >= target_recall:
            return ef, latency, recall
            
    raise RuntimeError("Target recall unreachable. Rebuild index with higher M or ef_construction.")

Если цикл выбросил RuntimeError, значит ваш граф физически не содержит нужных путей. Увеличение ef_search до бесконечности будет просто сканировать бесполезные узлы, убивая latency, но не принося новых релевантных векторов. Только в этот момент вы имеете техническое обоснование пойти и увеличить M или ef_construction, заплатив временем билда.

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

«Если граф не влезает в память при нужном M, мы просто включим квантизацию, и HNSW ужмется в 10 раз!»

Ужмется. И вытащит на свет другие проблемы. Квантизация векторов — это всегда потеря информации. На практике мы используем три подхода:

  • Scalar Quantization (SQ8): Сжимает float32 в int8. Практически бесплатно по latency, режет потребление RAM под векторы в 4 раза, но откусывает 1-3% от итогового recall. Самый частый дефолт в проде.
  • Product Quantization (PQ): Дробит вектор на подпространства и хранит только индексы центроидов. Позволяет сжать базу в 10-20 раз, но добавляет мощный CPU-оверхед на асимметричное вычисление дистанций во время обхода графа.
  • Binary Quantization: Экстремальный сценарий. Векторы становятся битовыми строками, дистанция вычисляется через XOR (расстояние Хэмминга). Граф летает, но это работает только если модель эмбеддингов изначально обучалась выдавать бинарные вектора. Иначе recall пробивает дно.

Но есть одна фундаментальная проблема HNSW, которую не скроет никакая квантизация. Этот алгоритм отвратительно работает с частыми обновлениями и удалениями.

Архитектура графа не позволяет физически вырвать узел из середины сложной структуры, не сломав связность сети. Поэтому удаленные векторы просто помечаются логическим флагом (tombstones). Граф сохраняет свой объем в памяти, а при поиске алгоритм тратит такты процессора на обход «мертвых» душ. Если в вашей базе происходит постоянная ротация данных, HNSW быстро деградирует. Потребуются регулярные и ресурсоемкие ребилды графа сбоку, с последующим горячим свапом индексов.

Честный трейд-офф заключается в том, что HNSW — это король статических или редко меняющихся баз при наличии избытка оперативной памяти. Если же ваша база активно мутирует каждую секунду, или бюджет RAM на edge-устройстве жестко ограничен гигабайтом, связка классического Inverted File (IVF) и PQ даст гораздо более предсказуемый профиль нагрузки. Она хуже по recall на сверхвысоких требованиях, но не упадет с OOM посреди ночи.

  • #Edge AI
  • #HNSW
  • #Memory Optimization
  • #Performance Tuning
  • #Vector Search
ПоделитьсяTelegramX
рассылка

Новые статьи — на почту

Лонгриды про ML в проде, edge и компьютерное зрение — сразу после выхода.

Канал в Telegram: morana.log

Без спама. Нажимая «Подписаться», соглашаетесь с обработкой персональных данных

бесплатный pdf-гайд

Edge AI или облако: когда тащить нейросеть на железо

Признаки, фреймворк выбора и прикидка экономии — короткий PDF-гайд на почту.

PDF · 5 страниц · без спама. Нажимая «Получить», соглашаетесь с обработкой персональных данных

Читать дальше
— заявка

Опишите задачу  ответим как инженеры. 

Оставьте имя и Telegram — остальное обсудим. Без брифов на 40 слайдов и звонков по три раза.

Отвечаем за пару часовотвечает инженер, а не отдел продажNDA по запросу

Сюда напишем — это быстрее всего

Или сразу написать в Telegram

Без спама и звонков-роботов. Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности и обработкой персональных данных.