PunchUNIUYO to monetise sports with new policy — VCESPN DeportesMax, el más rápido en tercera práctica en Bakú; Checo luce fuerteThe Jerusalem PostGov't warns against bringing Four Species into Israel without authorization ahead of SukkotInquirer2028 polls survey: Sara-Robin 12 points ahead of Leni-Bam tandem — OCTADaily MaverickEDUCATION: R6,600 a month to catch up: Inside SA’s costly private tutoring marketCNN TürkSermaye piyasası soruşturmasında yeni gelişme: 19 tutuklamaUOLRússia tenta criar alternativa à Starlink após perder acesso à rede de Elon MuskVarietyBlock the Merger Coalition Asks Court to Reject Paramount’s Settlement With States Over Warner Bros. DealWirtualna PolskaOdjechał samochodem osobowym. Zaginął 12-latek, policja z apelemThe IndependentDyfed-Powys Police hit by cyber attack as information at riskDaily MailBreakthrough brain cancer test cuts diagnosis from weeks to hours: 'Huge leap forward for patients'RapplerCarlos Yulo falls short of parallel bars medal, concludes stellar Asian Games
The Daily Newsstand · Free, Always
Friday, September 25, 2026

Векторный поиск: деплой без GPU на Triton Inference Server

Translate

Перед каждым ML-разработчиком, после обучения прекрасной модели, которая поднимет бизнесу метрики, а тебе годовую премию, возникает вопрос: а как ее задеплоить в сервисе, чтоб она начала работать?

А ты пошел отдыхать.

При этом модель должна не просто работать, а стабильно и быстро, плюс сервис должен иметь широкий набор инфраструктурных функций из коробки! С метриками, конечно же… Иначе как отследить ту невероятную пользу, которую наносит твоя модель? 

Все эти задачи решает NVIDIA Triton Inference Server - это опенсорс платформа для инференса моделей машинного обучения, и она успешно отвечает на все эти запросы. Что самое характерное, в некоторых случаях даже код писать не придется, песня же, а? 

Но обо всем по порядку, меня зовут Чернышев Стас, я ведущий ML-разработчик в команде поиска MAGNIT OMNI. Начнем с идеи и как мы ее воплотили в жизнь, а потом в технической секции залезем в конфиги и посмотрим, как это все настраивается.

Почему Triton?

До сих пор огромное количество моделей просто оборачивают в REST подобные фреймворки, ярким представителем которых является FastAPI, и это нормальное решение, многие с ним знакомы, хорошая документация, много туториалов. Но есть и минусы: у него отсутствуют специализированные инфраструктурные штуки из коробки:

  • gRPC

  • батчинг входных запросов

  • кэш

  • health check

  • метрики

Все это нужно писать вручную, разбираться, как работает, а затем поддерживать. Кроме того, если вы запустили свой сервис на CPU, то для перехода на GPU инференс нужно серьезно поработать с драйверами для вашего образа в Dockerfile. Но главный недостаток - скорость, на CPU даже инференс одного запроса занимал ~30 мс, что нас категорически не устраивало.

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

FastAPI

BentoML

Triton

Динамический батчинг

вручную

есть

есть

Кэш ответов

вручную

нет

есть (локальный / Redis)

hRPC

вручную

только в старых версиях API

есть

Метрики Prometheus

вручную

есть

есть

Инференс без Python

нет

нет

есть (нативные бэкенды)

BentoML удобен, но инференс происходит в Python-процессах со всеми вытекающими в виде GIL и накладных расходов на каждый запрос. Даже сами разработчики BentoML для ускорения предлагают запускать внутри него Triton. Ну и кэш-ответов: у нас три четверти запросов повторяются, так что кэш из коробки был обязательным пунктом.

Архитектура модели

Прежде чем рассказать, как мы деплоили модель, коротко расскажу, ЧТО мы деплоили :)

В качестве базовой модели эмбеддингов мы используем BERTA — компактную русскоязычную модель, полученную дистилляцией эмбеддингов FRIDA от SaluteDevices. 

Модель наследует от FRIDA систему префиксов — в частности, “search_query:” и “search_document:” для асимметричного поиска. По этому критерию мы разделили функционал на две независимых сервиса: эмбеддер запросов и документов. Для интересующихся оставлю ссылку тут - https://huggingface.co/sergeyzh/BERTA

Делим на два сервиса

Так как нагрузочные паттерны очень разные и эмбеддер товаров не должен влиять на эмбеддер запросов, ну и для отказоустойчивости, конечно, решено было разделить их на два.

Эмбеддер запросов работает в онлайне, пользователь вводит запрос, а мы должны быстро вернуть его эмбеддинг. Поэтому первый приоритет - скорость ответа. 

Эмбеддинги товаров обновляются при изменении описания товара или модели векторного поиска - тут важнее пропускная способность.

Отказались от Python (почти)

На Python мы оставили только токенизацию — это маленькая модель на Python backend, которая добавляет префикс, зовет токенизатор и передает тензоры дальше. А само тело модели сконвертировали в ONNX и отдали нативному onnxruntime backend (полный список поддерживаемых бэкендов тут). Кстати, это тот самый случай, когда код писать не придется: кладем в репозиторий моделей веса (model.onnx) и config.pbtxt рядом, остальное Triton сделает сам. Это сразу дало серьезное ускорение по сравнению со стандартной реализацией инференса:

Как получали эмбеддинг сразу после обучения:

from sentence_transformers import SentenceTransformer

model_name = "your_best_model_checkpoint"
model = SentenceTransformer(model_name)
query = "search_query: молоко ультрапастеризованное 2.5%"
embedding = model.encode(query)`

И еще одна деталь: модель обучена с matryoshka-лоссом, поэтому первые 192 координаты 768-мерного эмбеддинга сохраняют почти все качество. Наружу мы отдаем именно их, предварительно отнормировав. Вектор занимает в 4 раза меньше места в хранилище, и поиск кандидатов работает быстрее.

Теперь схема моделей в сервисе выглядит следующим образом:

Схема моделей в сервисе

Схема моделей в сервисе

Основная часть готова, осталось подобрать оптимальные параметры работы, об этом подробнее ниже.

Техническая секция: пишем конфиг

Первое, на что стоит обратить внимание, при настройке модели - встроенный кэш, он может быть локальным, а может использовать удаленный Redis.

Включается в config.pbtxt модели:

response_cache {
  enable: true
}

А при старте сервера задается общий размер кэша для всех моделей. Ниже пример, как задать такой параметр:

tritonserver --model-repository ... --cache-config local,size=3221225472

Для Redis вместо local указываем --cache-config redis,host=<host> и --cache-config redis,port=<port>.

При нашем профиле нагрузки у эмбеддера запросов часто повторяются одни и те же входные данные, поэтому его использование обязательно и снимает большую часть работы модели. А чтобы после рестарта приложения не обрушивался весь поток трафика на совершенно не готовый к этому сервис, был реализован прогрев: при старте пода прогоняем топ-N запросов, которые покрывают большую часть трафика.

Второе — это батчевание: можно "копить" запросы в батчи и делать параллельный инференс для элементов батча. Задается в config.pbtxt конфиге как-то так:

max_batch_size: 8
dynamic_batching {
  preferred_batch_size: [ 2, 3 ]
  max_queue_delay_microseconds: 3000  # сколько максимум ждем, собирая батч
}

Третье — это количество экземпляров модели. Triton Inference Server позволяет одним изменением параметра создавать два независимых экземпляра модели, в этом же блоке можно выбрать, на чем будет происходить инференс:

instance_group [
  {
    count: 2
    kind: KIND_CPU
  }
]

Отдельно хочу рассказать про параметры для onnxruntime backend.

parameters { key: "intra_op_thread_count" value: { string_value: "5" } }
parameters { key: "inter_op_thread_count" value: { string_value: "1" } }

intra_op_thread_count — сколько потоков onnxruntime использует для параллельного вычисления одного оператора (матричные умножения и т.п.), по сути — сколько ядер получит один экземпляр модели. 

inter_op_thread_count — потоки для параллельного выполнения независимых операторов графа; у нас режим выполнения последовательный, поэтому 1. Итого при двух экземплярах: 2 × 5 = 10 ядер под инференс из 12 доступных, остаток забирают токенизация и сам Triton. Важно! Если суммарно потоков окажется больше, чем фактически выделено или имеется, сервис начнет тротлить: latency вырастет, а следом подтянутся и ошибки по таймаутам на клиентах.

Бенчмарки

В нашем случае описанное решение оказалось быстрее FastAPI приблизительно в 3 раза и чуть меньше — относительно Triton, но с инференсом целиком на Python backend.

Вообще же для сервиса запросов, задеплоенного на 12 ядрах и двух подах (=24 ядра), мы уже получили цифры, которых оказалось достаточно, чтобы не задействовать дорогостоящие решения на GPU:

Время

Concurrency

RPS

Avg (клиент), мс

р50, мс

р90, мс

р99, мс

Avg (сервер), мс

Cache Hit, %

Errors

8:45

10

212.8

47

41

58

64

12.6

57

0

8:47

20

424.3

47

42

61

74

13.6

65

0

8:49

30

614.3

48

43

64

87

19

72

0

8:51

40

800.6

50

44

68

90

22.9

75

0

8:53

50

983.1

50

45

73

95

27.4

76

0

8:55

60

1145.9

52

45

79

100

33.4

77

0

8:57

70

1282.5

54

45

87

115

43.9

78

0

8:59

80

1389.2

57

46

100

134

58.9

79

0

9:01

90

1483.4

60

46

118

150

87

79

0

9:03

100

1527.7

65

45

142

179

110

79

0

9:04

105

1571.1

66

45

149

188

124

79

0

Клиентские колонки замерялись через ghz с ноутбука и включают сетевой overhead. Avg (сервер) — средняя задержка запросов, дошедших до модели: кэш-хиты в нее не входят, они отдаются за ~0.007 мс. Поэтому при инференсе в 124 мс клиент в среднем видит 66 мс.

Тестирование эмбеддера документов проводилось на таких же двух подах с 12 ядрами. Тексты тут заметно длиннее — среднее описание товара ~136 символов против 1-3 слов в запросе, зато нет таких жестких требований по времени ответа:

Время

Concurrence

RPS

Avg (клиент), мс

р50, мс

р90, мс

р99, мс

Errors

15:45

70

119.5

579

562

897

1128

0

15:50

90

141.4

629

601

941

1170

0

15:54

110

150.7

719

703

1014

1529

0

15:58

130

149.8

854

845

1243

1598

0

16:02

150

146.5

1006

973

1467

1921

0

16:06

170

147.3

1132

1084

1634

2012

0

16:10

190

145.2

1283

1290

1820

2320

0

Как выбрать образ tritonserver или даже собрать самому под свои нужды

В зависимости от того, какими backend будем пользоваться, и на какой платформе (CPU или GPU) инференсить, подойдут разные версии образа. Они будут отличаться по весу и напрямую влиять на скорость поднятия сервиса — ведь его еще нужно спуллить из хранилища. Список преподготовленных образов можно найти в каталоге NGC: полный -py3, минимальный -py3-min и другие.

Так же можно самому собрать образ в необходимом конфиге: клонируем репозиторий triton-inference-server/server на ветке нужного релиза и запускаем скрипт build.py с необходимыми флагами (все подробности — в документации по сборке). Вот так выглядит пример сборки CPU-only образа под наш стек — python и onnxruntime бэкенды, gRPC + HTTP, метрики и локальный кэш:

git clone -b r26.02 https://github.com/triton-inference-server/server.git
cd server
./build.py -v \
    --enable-logging --enable-stats --enable-metrics --enable-cpu-metrics \
    --endpoint=grpc --endpoint=http \
    --backend=python --backend=onnxruntime \
    --cache=local

Что получилось в итоге

Сервис на обычных CPU-ядрах держит >1000 RPS с нормальной скоростью, кэш забирает на себя три четверти работы модели, а почти весь «инфраструктурный код» — это пара config.pbtxt. В нашем случае дорогие GPU так и не понадобились. Так что если ваша модель всё ещё завёрнута в FastAPI, тогда Triton идет к вам.

Модель задеплоена, метрики капают, невероятная польза наносится — можно наконец пойти отдыхать. Ну а годовая премия... посмотрим по метрикам.

P.S. В статье намеренно опущены подробные шаги по тому, как завернуть свою модель в Triton, постарался сфокусироваться на настройке и оптимизации. При первичном изучении технологии мне очень помогла эта статья на Хабре.

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.