← Knowledge Store

Embeddings-рантайм

knowledge-store · workzone

Эмбеддер — один общий сервис для всей платформы: преобразует текст в вектор, к нему обращаются Harvester, Knowledge Store, Query Engine и Agent Engine. Несущее решение уже принято на стороне реестра моделей: эмбеддер вынесен отдельным сетевым сервисом (OpenAI-совместимый, веса грузятся лениво), а не вшит в backend. Поэтому масштаб здесь — вопрос эксплуатации, а не передизайна.

Потоки нагрузки

Четыре источника нагрузки сходятся в один общий рантайм.

Harvester · инжест
этап embed — фрагменты пачками при приёме
фон · может ждать
Knowledge Store · реэмбеддинг
массовый прогон при смене модели
фон · может ждать
Query Engine · поиск
вектор запроса на каждый запрос пользователя
юзер ждёт ответа
Agent Engine · поиск
примитив search KS в агентном цикле — вектор намерения
фон · мелкий · online-путь
embeddings :80
OpenAI-совместимый · model-agnostic · веса лениво, назначенная прогрета
вектор
Путь Объём Латентность Ожидание
Поиск · QE маленький запрос юзер ждёт ответа критично
Поиск · Agent маленький запрос фон, но мелкий терпит · online
Инжест · Harvester большие пачки фон терпит
Реэмбеддинг · KS массовый прогон, редко фон терпит

Неравномерность по источникам — не забота эмбеддера. В потоке фрагментов источника уже нет: эмбеддеру важен только суммарный объём, а темп и устойчивость приёма по каждому источнику держат воркеры приёма Harvester.

Узкое место — конкуренция, не мощность → Cache & Workers

Риск — не сам объём, а разная форма нагрузки у путей. Большой реиндекс на сотни тысяч фрагментов способен занять рантайм целиком — и запрос пользователя встанет в очередь за ним, поиск затормозит. Это вопрос приоритета, а не количества железа.

Поиск не делит лимит с массовым прогоном. Фоновая нагрузка (инжест + реэмбеддинг) идёт через очередь задач с ограниченным параллелизмом; запросы поиска зовут рантайм напрямую, вне этого лимита. Так интерактивный путь не встаёт в очередь за всем реиндексом — лимиты раздельные, не общий. Инжест и так исполняется фоновыми воркерами, их предел параллелизма просто не общий с поиском. Полной развязки это в v1 не даёт: рантайм один, и онлайн-запрос ещё подождёт текущую bulk-пачку на исполнении — но задержку ограничивает одна пачка, а не вся очередь; совсем поток разводят раздельные пулы online/bulk уже в v2 (ниже).
Агентный поиск едет прямым путём, не через bulk-очередь. Хотя агентный цикл — фоновый, его обращение к эмбеддеру это мелкий вектор запроса через тот же примитив search KS, что и у пользователя: ставить его в очередь за реиндексом значило бы заморозить цикл на минуты ради задачи на миллисекунды. Поэтому он делит online-путь с поиском, а не bulk-лимит. Объёма это не добавляет: темп агентных запросов ограничен сверху агентным потолком параллелизма, а не пропускной способностью эмбеддера.
Первый запрос не ждёт весов. Ленивая загрузка бережёт деплой, но на латентно-критичном пути обернулась бы секундами холодного старта. Поэтому рантайм греет назначенную модель проактивно — по её назначению в Admin и при собственном старте, а не на первом обращении поиска: к первому живому запросу веса уже в памяти. Желаемая модель персистится рядом с кэшем весов, поэтому перезапущенный контейнер перезагружает её сам — рестарт посреди закачки продолжает её, а не возвращает пустой рантайм.
Память — бюджет, а не надежда. Рантайм читает лимит своего контейнера при старте и держит загруженные модели, пока они помещаются: при смене новая модель грузится рядом со старой, и старая продолжает обслуживать поиск, пока новые веса не готовы, — вытеснение (LRU) срабатывает только под реальным давлением, так что просторный хост получает бесшовную смену, а маленький, как и раньше, держит пик в одну резидентную модель. Модель, не помещающаяся даже в одиночку, отклоняется при назначении (MODEL_TOO_LARGE; предпроверка идёт до любого коммита), а не всплывает позже OOM-убийством. Пофазовое состояние модели (loading / ready / error) отдаётся отдельным статусным endpoint'ом — экраны Admin показывают «загрузку весов» и «переиндексацию» как разные стадии, — а /healthz остаётся чистым liveness: контейнер, занятый загрузкой весов, жив, и autoheal-сторож не должен его пристреливать.
Эмбеддер не ответил — поиск деградирует, не падает. Обращение к рантайму на онлайн-пути идёт под ограниченным таймаутом: висеть в ожидании вектора, пока пользователь ждёт ответа, недопустимо. Не уложился или недоступен — поиск отдаёт результат по оставшимся примитивам, которым вектор не нужен (лексика, граф, фильтры): вектор лишь одна из двух текстовых ног, без него выдача беднеет, но запрос не остаётся без ответа — провал вектора виден честно, а не прячется за зависанием. На bulk-пути сбой ничего не роняет: фрагмент возвращается в очередь и берётся ретраем воркеров приёма, пользователь этого не ждёт.
Масштаб: v1 → v2

У сервиса два рычага мощности. Батчинг — рантайм копит запросы на несколько миллисекунд и считает их одной пачкой; даёт кратный рост пропускной способности на одном контейнере, берётся бесплатно с батчинг-способным рантаймом. Реплики за балансировщиком — N копий: эмбеддинг stateless (текст на входе, вектор на выходе, без состояния между запросами), поэтому любой запрос обслужит любая копия. Сначала выжимается батчинг, потом реплики.

Делаем сейчас v1
поиск ───→
embeddings :80
инжест · реэмб. очередь · лимит
  • Один логический сервис за адресом-конфигом.
  • Рантайм с динамическим батчингом — как требование.
  • Один хост → масштаб вертикально.
Откладываем v2
балансировщик
реплика
реплика
реплика
  • Пул из N реплик за тем же адресом.
  • Автомасштабирование под нагрузку.
  • Разделение пулов: «online» (поиск) vs «bulk» (инжест).
  • Требует оркестрации (k8s / GPU).
Переход v1 → v2 — без правок у потребителей. Оба деплоя живут за одним адресом (base_url): нарастить пул — это сменить цель адреса, код Harvester, Knowledge Store и Query Engine не меняется.